refector:@ refrence 架构重构

This commit is contained in:
sean
2025-06-12 14:18:19 +08:00
parent 5d6e678bd2
commit c46cd24fe4
12 changed files with 2790 additions and 0 deletions

View File

@ -0,0 +1,127 @@
/**
* BaseDiscovery - 资源发现基础抽象类
*
* 按照DPML协议架构文档设计提供统一的资源发现接口
* 所有具体的Discovery实现都应该继承这个基类
*/
class BaseDiscovery {
/**
* 构造函数
* @param {string} source - 发现源类型 (PACKAGE, PROJECT, USER, INTERNET)
* @param {number} priority - 优先级,数字越小优先级越高
*/
constructor(source, priority = 0) {
if (!source) {
throw new Error('Discovery source is required')
}
this.source = source
this.priority = priority
this.cache = new Map()
}
/**
* 抽象方法:发现资源
* 子类必须实现此方法
* @returns {Promise<Array>} 发现的资源列表
*/
async discover() {
throw new Error('discover method must be implemented by subclass')
}
/**
* 获取发现器信息
* @returns {Object} 发现器元数据
*/
getDiscoveryInfo() {
return {
source: this.source,
priority: this.priority,
description: `${this.source} resource discovery`
}
}
/**
* 验证资源结构
* @param {Object} resource - 待验证的资源对象
* @throws {Error} 如果资源结构无效
*/
validateResource(resource) {
if (!resource || typeof resource !== 'object') {
throw new Error('Resource must be an object')
}
if (!resource.id || !resource.reference) {
throw new Error('Resource must have id and reference')
}
// 验证ID格式 (protocol:resourcePath)
if (typeof resource.id !== 'string' || !resource.id.includes(':')) {
throw new Error('Resource id must be in format "protocol:resourcePath"')
}
// 验证引用格式 (@protocol://path)
if (typeof resource.reference !== 'string' || !resource.reference.startsWith('@')) {
throw new Error('Resource reference must be in DPML format "@protocol://path"')
}
}
/**
* 规范化资源对象,添加元数据
* @param {Object} resource - 原始资源对象
* @returns {Object} 规范化后的资源对象
*/
normalizeResource(resource) {
// 验证资源结构
this.validateResource(resource)
// 创建规范化的资源对象
const normalizedResource = {
id: resource.id,
reference: resource.reference,
metadata: {
source: this.source,
priority: this.priority,
timestamp: new Date(),
...resource.metadata // 保留现有元数据
}
}
return normalizedResource
}
/**
* 清理缓存
*/
clearCache() {
this.cache.clear()
}
/**
* 获取缓存大小
* @returns {number} 缓存条目数量
*/
getCacheSize() {
return this.cache.size
}
/**
* 从缓存获取资源
* @param {string} key - 缓存键
* @returns {*} 缓存的值或undefined
*/
getFromCache(key) {
return this.cache.get(key)
}
/**
* 设置缓存
* @param {string} key - 缓存键
* @param {*} value - 缓存值
*/
setCache(key, value) {
this.cache.set(key, value)
}
}
module.exports = BaseDiscovery

View File

@ -0,0 +1,178 @@
const fs = require('fs-extra')
const path = require('path')
/**
* CrossPlatformFileScanner - 跨平台文件扫描器
*
* 替代glob库使用Node.js原生fs API实现跨平台文件扫描
* 避免glob在Windows上的兼容性问题
*/
class CrossPlatformFileScanner {
/**
* 递归扫描目录,查找匹配的文件
* @param {string} baseDir - 基础目录
* @param {Object} options - 扫描选项
* @param {Array<string>} options.extensions - 文件扩展名列表,如 ['.role.md', '.execution.md']
* @param {Array<string>} options.subdirs - 限制扫描的子目录,如 ['domain', 'execution']
* @param {number} options.maxDepth - 最大扫描深度默认10
* @returns {Promise<Array<string>>} 匹配的文件路径列表
*/
async scanFiles(baseDir, options = {}) {
const {
extensions = [],
subdirs = null,
maxDepth = 10
} = options
if (!await fs.pathExists(baseDir)) {
return []
}
const results = []
await this._scanRecursive(baseDir, baseDir, extensions, subdirs, maxDepth, 0, results)
return results
}
/**
* 扫描特定类型的资源文件
* @param {string} baseDir - 基础目录
* @param {string} resourceType - 资源类型 ('role', 'execution', 'thought')
* @returns {Promise<Array<string>>} 匹配的文件路径列表
*/
async scanResourceFiles(baseDir, resourceType) {
const resourceConfig = {
role: {
extensions: ['.role.md'],
subdirs: ['domain'] // 角色文件通常在domain目录下
},
execution: {
extensions: ['.execution.md'],
subdirs: ['execution'] // 执行模式文件在execution目录下
},
thought: {
extensions: ['.thought.md'],
subdirs: ['thought'] // 思维模式文件在thought目录下
}
}
const config = resourceConfig[resourceType]
if (!config) {
throw new Error(`Unsupported resource type: ${resourceType}`)
}
return await this.scanFiles(baseDir, config)
}
/**
* 递归扫描目录的内部实现
* @param {string} currentDir - 当前扫描目录
* @param {string} baseDir - 基础目录
* @param {Array<string>} extensions - 文件扩展名列表
* @param {Array<string>|null} subdirs - 限制扫描的子目录
* @param {number} maxDepth - 最大深度
* @param {number} currentDepth - 当前深度
* @param {Array<string>} results - 结果数组
* @private
*/
async _scanRecursive(currentDir, baseDir, extensions, subdirs, maxDepth, currentDepth, results) {
if (currentDepth >= maxDepth) {
return
}
try {
const entries = await fs.readdir(currentDir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name)
if (entry.isFile()) {
// 检查文件扩展名
if (this._matchesExtensions(entry.name, extensions)) {
results.push(fullPath)
}
} else if (entry.isDirectory()) {
// 检查是否应该扫描这个子目录
if (this._shouldScanDirectory(entry.name, subdirs, currentDepth)) {
await this._scanRecursive(
fullPath,
baseDir,
extensions,
subdirs,
maxDepth,
currentDepth + 1,
results
)
}
}
}
} catch (error) {
// 忽略权限错误或其他文件系统错误
console.warn(`[CrossPlatformFileScanner] Failed to scan directory ${currentDir}: ${error.message}`)
}
}
/**
* 检查文件名是否匹配指定扩展名
* @param {string} fileName - 文件名
* @param {Array<string>} extensions - 扩展名列表
* @returns {boolean} 是否匹配
* @private
*/
_matchesExtensions(fileName, extensions) {
if (!extensions || extensions.length === 0) {
return true // 如果没有指定扩展名,匹配所有文件
}
return extensions.some(ext => fileName.endsWith(ext))
}
/**
* 检查是否应该扫描指定目录
* @param {string} dirName - 目录名
* @param {Array<string>|null} subdirs - 允许扫描的子目录列表
* @param {number} currentDepth - 当前深度
* @returns {boolean} 是否应该扫描
* @private
*/
_shouldScanDirectory(dirName, subdirs, currentDepth) {
// 跳过隐藏目录和node_modules
if (dirName.startsWith('.') || dirName === 'node_modules') {
return false
}
// 如果没有指定子目录限制,扫描所有目录
if (!subdirs || subdirs.length === 0) {
return true
}
// 在根级别,只扫描指定的子目录
if (currentDepth === 0) {
return subdirs.includes(dirName)
}
// 在更深层级,扫描所有目录
return true
}
/**
* 规范化路径,确保跨平台兼容性
* @param {string} filePath - 文件路径
* @returns {string} 规范化后的路径
*/
normalizePath(filePath) {
return path.normalize(filePath).replace(/\\/g, '/')
}
/**
* 生成相对路径,确保跨平台兼容性
* @param {string} from - 起始路径
* @param {string} to - 目标路径
* @returns {string} 规范化的相对路径
*/
getRelativePath(from, to) {
const relativePath = path.relative(from, to)
return relativePath.replace(/\\/g, '/')
}
}
module.exports = CrossPlatformFileScanner

View File

@ -0,0 +1,159 @@
const PackageDiscovery = require('./PackageDiscovery')
const ProjectDiscovery = require('./ProjectDiscovery')
/**
* DiscoveryManager - 资源发现管理器
*
* 统一管理多个资源发现器,按照文档架构设计:
* 1. 按优先级排序发现器 (数字越小优先级越高)
* 2. 并行执行资源发现
* 3. 收集并合并所有发现的资源
* 4. 提供容错机制,单个发现器失败不影响整体
*/
class DiscoveryManager {
/**
* 构造函数
* @param {Array} discoveries - 自定义发现器列表,如果不提供则使用默认配置
*/
constructor(discoveries = null) {
if (discoveries) {
this.discoveries = [...discoveries]
} else {
// 默认发现器配置:只包含包级和项目级发现
this.discoveries = [
new PackageDiscovery(), // 优先级: 1
new ProjectDiscovery() // 优先级: 2
]
}
// 按优先级排序
this._sortDiscoveriesByPriority()
}
/**
* 添加发现器
* @param {Object} discovery - 实现了发现器接口的对象
*/
addDiscovery(discovery) {
if (!discovery || typeof discovery.discover !== 'function') {
throw new Error('Discovery must implement discover method')
}
this.discoveries.push(discovery)
this._sortDiscoveriesByPriority()
}
/**
* 移除发现器
* @param {string} source - 发现器源类型
*/
removeDiscovery(source) {
this.discoveries = this.discoveries.filter(discovery => discovery.source !== source)
}
/**
* 发现所有资源(并行模式)
* @returns {Promise<Array>} 所有发现的资源列表
*/
async discoverAll() {
const discoveryPromises = this.discoveries.map(async (discovery) => {
try {
const resources = await discovery.discover()
return Array.isArray(resources) ? resources : []
} catch (error) {
console.warn(`[DiscoveryManager] ${discovery.source} discovery failed: ${error.message}`)
return []
}
})
// 并行执行所有发现器
const discoveryResults = await Promise.allSettled(discoveryPromises)
// 收集所有成功的结果
const allResources = []
discoveryResults.forEach((result, index) => {
if (result.status === 'fulfilled') {
allResources.push(...result.value)
} else {
console.warn(`[DiscoveryManager] ${this.discoveries[index].source} discovery rejected: ${result.reason}`)
}
})
return allResources
}
/**
* 按源类型发现资源
* @param {string} source - 发现器源类型
* @returns {Promise<Array>} 指定源的资源列表
*/
async discoverBySource(source) {
const discovery = this._findDiscoveryBySource(source)
if (!discovery) {
throw new Error(`Discovery source ${source} not found`)
}
return await discovery.discover()
}
/**
* 获取所有发现器信息
* @returns {Array} 发现器信息列表
*/
getDiscoveryInfo() {
return this.discoveries.map(discovery => {
if (typeof discovery.getDiscoveryInfo === 'function') {
return discovery.getDiscoveryInfo()
} else {
return {
source: discovery.source || 'UNKNOWN',
priority: discovery.priority || 0,
description: 'No description available'
}
}
})
}
/**
* 清理所有发现器缓存
*/
clearCache() {
this.discoveries.forEach(discovery => {
if (typeof discovery.clearCache === 'function') {
discovery.clearCache()
}
})
}
/**
* 获取发现器数量
* @returns {number} 注册的发现器数量
*/
getDiscoveryCount() {
return this.discoveries.length
}
/**
* 按优先级排序发现器
* @private
*/
_sortDiscoveriesByPriority() {
this.discoveries.sort((a, b) => {
const priorityA = a.priority || 0
const priorityB = b.priority || 0
return priorityA - priorityB // 升序排序,数字越小优先级越高
})
}
/**
* 根据源类型查找发现器
* @param {string} source - 发现器源类型
* @returns {Object|undefined} 找到的发现器或undefined
* @private
*/
_findDiscoveryBySource(source) {
return this.discoveries.find(discovery => discovery.source === source)
}
}
module.exports = DiscoveryManager

View File

@ -0,0 +1,228 @@
const BaseDiscovery = require('./BaseDiscovery')
const fs = require('fs-extra')
const path = require('path')
const CrossPlatformFileScanner = require('./CrossPlatformFileScanner')
/**
* PackageDiscovery - 包级资源发现器
*
* 负责发现NPM包内的资源
* 1. 从 src/resource.registry.json 加载静态注册表
* 2. 扫描 prompt/ 目录发现动态资源
*
* 优先级1 (最高优先级)
*/
class PackageDiscovery extends BaseDiscovery {
constructor() {
super('PACKAGE', 1)
this.fileScanner = new CrossPlatformFileScanner()
}
/**
* 发现包级资源
* @returns {Promise<Array>} 发现的资源列表
*/
async discover() {
const resources = []
try {
// 1. 加载静态注册表资源
const registryResources = await this._loadStaticRegistryResources()
resources.push(...registryResources)
// 2. 扫描prompt目录资源
const scanResources = await this._scanPromptDirectory()
resources.push(...scanResources)
// 3. 规范化所有资源
return resources.map(resource => this.normalizeResource(resource))
} catch (error) {
console.warn(`[PackageDiscovery] Discovery failed: ${error.message}`)
return []
}
}
/**
* 从静态注册表加载资源
* @returns {Promise<Array>} 注册表中的资源列表
*/
async _loadStaticRegistryResources() {
try {
const registry = await this._loadStaticRegistry()
const resources = []
if (registry.protocols) {
// 遍历所有协议
for (const [protocol, protocolInfo] of Object.entries(registry.protocols)) {
if (protocolInfo.registry) {
// 遍历协议下的所有资源
for (const [resourceId, resourceInfo] of Object.entries(protocolInfo.registry)) {
const reference = typeof resourceInfo === 'string'
? resourceInfo
: resourceInfo.file
if (reference) {
resources.push({
id: `${protocol}:${resourceId}`,
reference: reference
})
}
}
}
}
}
return resources
} catch (error) {
console.warn(`[PackageDiscovery] Failed to load static registry: ${error.message}`)
return []
}
}
/**
* 加载静态注册表文件
* @returns {Promise<Object>} 注册表内容
*/
async _loadStaticRegistry() {
const packageRoot = await this._findPackageRoot()
const registryPath = path.join(packageRoot, 'src', 'resource.registry.json')
if (!await fs.pathExists(registryPath)) {
throw new Error('Static registry file not found')
}
return await fs.readJSON(registryPath)
}
/**
* 扫描prompt目录发现资源
* @returns {Promise<Array>} 扫描发现的资源列表
*/
async _scanPromptDirectory() {
try {
const packageRoot = await this._findPackageRoot()
const promptDir = path.join(packageRoot, 'prompt')
if (!await fs.pathExists(promptDir)) {
return []
}
const resources = []
// 定义要扫描的资源类型
const resourceTypes = ['role', 'execution', 'thought']
// 并行扫描所有资源类型
for (const resourceType of resourceTypes) {
const files = await this.fileScanner.scanResourceFiles(promptDir, resourceType)
for (const filePath of files) {
const suffix = `.${resourceType}.md`
const id = this._extractResourceId(filePath, resourceType, suffix)
const reference = this._generatePackageReference(filePath, packageRoot)
resources.push({
id: id,
reference: reference
})
}
}
return resources
} catch (error) {
console.warn(`[PackageDiscovery] Failed to scan prompt directory: ${error.message}`)
return []
}
}
/**
* 文件扫描可以被测试mock
* @param {string} baseDir - 基础目录
* @param {string} resourceType - 资源类型
* @returns {Promise<Array>} 匹配的文件路径列表
*/
async _scanFiles(baseDir, resourceType) {
return await this.fileScanner.scanResourceFiles(baseDir, resourceType)
}
/**
* 查找包根目录
* @returns {Promise<string>} 包根目录路径
*/
async _findPackageRoot() {
const cacheKey = 'packageRoot'
const cached = this.getFromCache(cacheKey)
if (cached) {
return cached
}
const packageRoot = await this._findPackageJsonWithPrompt()
if (!packageRoot) {
throw new Error('Package root with prompt directory not found')
}
this.setCache(cacheKey, packageRoot)
return packageRoot
}
/**
* 查找包含prompt目录的package.json
* @returns {Promise<string|null>} 包根目录路径或null
*/
async _findPackageJsonWithPrompt() {
let currentDir = __dirname
while (currentDir !== path.parse(currentDir).root) {
const packageJsonPath = path.join(currentDir, 'package.json')
const promptDirPath = path.join(currentDir, 'prompt')
// 检查是否同时存在package.json和prompt目录
const [hasPackageJson, hasPromptDir] = await Promise.all([
fs.pathExists(packageJsonPath),
fs.pathExists(promptDirPath)
])
if (hasPackageJson && hasPromptDir) {
// 验证是否是PromptX包
try {
const packageJson = await fs.readJSON(packageJsonPath)
if (packageJson.name === 'promptx' || packageJson.name === 'dpml-prompt') {
return currentDir
}
} catch (error) {
// 忽略package.json读取错误
}
}
currentDir = path.dirname(currentDir)
}
return null
}
/**
* 生成包引用路径
* @param {string} filePath - 文件绝对路径
* @param {string} packageRoot - 包根目录
* @returns {string} @package://相对路径
*/
_generatePackageReference(filePath, packageRoot) {
const relativePath = this.fileScanner.getRelativePath(packageRoot, filePath)
return `@package://${relativePath}`
}
/**
* 提取资源ID
* @param {string} filePath - 文件路径
* @param {string} protocol - 协议类型
* @param {string} suffix - 文件后缀
* @returns {string} 资源ID (protocol:resourceName)
*/
_extractResourceId(filePath, protocol, suffix) {
const fileName = path.basename(filePath, suffix)
return `${protocol}:${fileName}`
}
}
module.exports = PackageDiscovery

View File

@ -0,0 +1,223 @@
const BaseDiscovery = require('./BaseDiscovery')
const fs = require('fs-extra')
const path = require('path')
const CrossPlatformFileScanner = require('./CrossPlatformFileScanner')
/**
* ProjectDiscovery - 项目级资源发现器
*
* 负责发现项目本地的资源:
* 1. 扫描 .promptx/resource/ 目录
* 2. 发现用户自定义的角色、执行模式、思维模式等
*
* 优先级2
*/
class ProjectDiscovery extends BaseDiscovery {
constructor() {
super('PROJECT', 2)
this.fileScanner = new CrossPlatformFileScanner()
}
/**
* 发现项目级资源
* @returns {Promise<Array>} 发现的资源列表
*/
async discover() {
try {
// 1. 查找项目根目录
const projectRoot = await this._findProjectRoot()
// 2. 检查.promptx目录是否存在
const hasPrompxDir = await this._checkPrompxDirectory(projectRoot)
if (!hasPrompxDir) {
return []
}
// 3. 扫描项目资源
const resources = await this._scanProjectResources(projectRoot)
// 4. 规范化所有资源
return resources.map(resource => this.normalizeResource(resource))
} catch (error) {
console.warn(`[ProjectDiscovery] Discovery failed: ${error.message}`)
return []
}
}
/**
* 查找项目根目录
* @returns {Promise<string>} 项目根目录路径
*/
async _findProjectRoot() {
const cacheKey = 'projectRoot'
const cached = this.getFromCache(cacheKey)
if (cached) {
return cached
}
let currentDir = process.cwd()
// 向上查找包含package.json的目录
while (currentDir !== path.dirname(currentDir)) {
const packageJsonPath = path.join(currentDir, 'package.json')
if (await this._fsExists(packageJsonPath)) {
this.setCache(cacheKey, currentDir)
return currentDir
}
currentDir = path.dirname(currentDir)
}
// 如果没找到package.json返回当前工作目录
const fallbackRoot = process.cwd()
this.setCache(cacheKey, fallbackRoot)
return fallbackRoot
}
/**
* 检查.promptx目录是否存在
* @param {string} projectRoot - 项目根目录
* @returns {Promise<boolean>} 是否存在.promptx/resource目录
*/
async _checkPrompxDirectory(projectRoot) {
const promptxResourcePath = path.join(projectRoot, '.promptx', 'resource')
return await this._fsExists(promptxResourcePath)
}
/**
* 扫描项目资源
* @param {string} projectRoot - 项目根目录
* @returns {Promise<Array>} 扫描发现的资源列表
*/
async _scanProjectResources(projectRoot) {
try {
const resourcesDir = path.join(projectRoot, '.promptx', 'resource')
const resources = []
// 定义要扫描的资源类型
const resourceTypes = ['role', 'execution', 'thought']
// 并行扫描所有资源类型
for (const resourceType of resourceTypes) {
try {
const files = await this.fileScanner.scanResourceFiles(resourcesDir, resourceType)
for (const filePath of files) {
// 验证文件内容
const isValid = await this._validateResourceFile(filePath, resourceType)
if (!isValid) {
continue
}
const suffix = `.${resourceType}.md`
const id = this._extractResourceId(filePath, resourceType, suffix)
const reference = this._generateProjectReference(filePath, projectRoot)
resources.push({
id: id,
reference: reference
})
}
} catch (error) {
console.warn(`[ProjectDiscovery] Failed to scan ${resourceType} resources: ${error.message}`)
}
}
return resources
} catch (error) {
console.warn(`[ProjectDiscovery] Failed to scan project resources: ${error.message}`)
return []
}
}
/**
* 文件扫描可以被测试mock
* @param {string} baseDir - 基础目录
* @param {string} resourceType - 资源类型
* @returns {Promise<Array>} 匹配的文件路径列表
*/
async _scanFiles(baseDir, resourceType) {
return await this.fileScanner.scanResourceFiles(baseDir, resourceType)
}
/**
* 文件系统存在性检查可以被测试mock
* @param {string} filePath - 文件路径
* @returns {Promise<boolean>} 文件是否存在
*/
async _fsExists(filePath) {
return await fs.pathExists(filePath)
}
/**
* 读取文件内容可以被测试mock
* @param {string} filePath - 文件路径
* @returns {Promise<string>} 文件内容
*/
async _readFile(filePath) {
return await fs.readFile(filePath, 'utf8')
}
/**
* 验证资源文件格式
* @param {string} filePath - 文件路径
* @param {string} protocol - 协议类型
* @returns {Promise<boolean>} 是否是有效的资源文件
*/
async _validateResourceFile(filePath, protocol) {
try {
const content = await this._readFile(filePath)
if (!content || typeof content !== 'string') {
return false
}
const trimmedContent = content.trim()
if (trimmedContent.length === 0) {
return false
}
// 根据协议类型验证DPML标签
switch (protocol) {
case 'role':
return trimmedContent.includes('<role>') && trimmedContent.includes('</role>')
case 'execution':
return trimmedContent.includes('<execution>') && trimmedContent.includes('</execution>')
case 'thought':
return trimmedContent.includes('<thought>') && trimmedContent.includes('</thought>')
default:
return false
}
} catch (error) {
console.warn(`[ProjectDiscovery] Failed to validate ${filePath}: ${error.message}`)
return false
}
}
/**
* 生成项目引用路径
* @param {string} filePath - 文件绝对路径
* @param {string} projectRoot - 项目根目录
* @returns {string} @project://相对路径
*/
_generateProjectReference(filePath, projectRoot) {
const relativePath = this.fileScanner.getRelativePath(projectRoot, filePath)
return `@project://${relativePath}`
}
/**
* 提取资源ID
* @param {string} filePath - 文件路径
* @param {string} protocol - 协议类型
* @param {string} suffix - 文件后缀
* @returns {string} 资源ID (protocol:resourceName)
*/
_extractResourceId(filePath, protocol, suffix) {
const fileName = path.basename(filePath, suffix)
return `${protocol}:${fileName}`
}
}
module.exports = ProjectDiscovery