refector:@ refrence 架构重构
This commit is contained in:
292
src/lib/core/resource/EnhancedResourceRegistry.js
Normal file
292
src/lib/core/resource/EnhancedResourceRegistry.js
Normal file
@ -0,0 +1,292 @@
|
||||
/**
|
||||
* EnhancedResourceRegistry - 增强的资源注册表
|
||||
*
|
||||
* 按照DPML协议架构文档设计,支持:
|
||||
* 1. 资源元数据管理(source, priority, timestamp)
|
||||
* 2. 智能合并策略(优先级和时间戳)
|
||||
* 3. 发现源优先级管理
|
||||
* 4. 批量操作支持
|
||||
*/
|
||||
class EnhancedResourceRegistry {
|
||||
constructor() {
|
||||
// 主索引:resourceId -> reference
|
||||
this.index = new Map()
|
||||
|
||||
// 元数据索引:resourceId -> metadata
|
||||
this.metadata = new Map()
|
||||
|
||||
// 发现源优先级映射
|
||||
this.sourcePriority = {
|
||||
'USER': 1, // 最高优先级
|
||||
'PROJECT': 2,
|
||||
'PACKAGE': 3,
|
||||
'INTERNET': 4 // 最低优先级
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册单个资源
|
||||
* @param {Object} resource - 资源对象
|
||||
* @param {string} resource.id - 资源ID
|
||||
* @param {string} resource.reference - 资源引用
|
||||
* @param {Object} resource.metadata - 资源元数据
|
||||
*/
|
||||
register(resource) {
|
||||
this._validateResource(resource)
|
||||
|
||||
const { id, reference, metadata } = resource
|
||||
|
||||
// 如果资源已存在,检查是否应该覆盖
|
||||
if (this.has(id)) {
|
||||
const existingMetadata = this.metadata.get(id)
|
||||
if (!this._shouldOverride(existingMetadata, metadata)) {
|
||||
return // 不覆盖,保持现有资源
|
||||
}
|
||||
}
|
||||
|
||||
// 注册资源
|
||||
this.index.set(id, reference)
|
||||
this.metadata.set(id, { ...metadata })
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量注册资源
|
||||
* @param {Array} resources - 资源数组
|
||||
*/
|
||||
registerBatch(resources) {
|
||||
if (!Array.isArray(resources)) {
|
||||
throw new Error('Resources must be an array')
|
||||
}
|
||||
|
||||
resources.forEach(resource => {
|
||||
try {
|
||||
if (resource && typeof resource === 'object') {
|
||||
this.register(resource)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`[EnhancedResourceRegistry] Failed to register resource: ${error.message}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并另一个注册表
|
||||
* @param {EnhancedResourceRegistry} otherRegistry - 另一个注册表实例
|
||||
*/
|
||||
merge(otherRegistry) {
|
||||
if (!(otherRegistry instanceof EnhancedResourceRegistry)) {
|
||||
throw new Error('Can only merge with another EnhancedResourceRegistry instance')
|
||||
}
|
||||
|
||||
// 获取所有资源并批量注册(会自动处理优先级)
|
||||
const otherResources = otherRegistry.list().map(id => ({
|
||||
id,
|
||||
reference: otherRegistry.resolve(id),
|
||||
metadata: otherRegistry.getMetadata(id)
|
||||
}))
|
||||
|
||||
this.registerBatch(otherResources)
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析资源ID到引用
|
||||
* @param {string} resourceId - 资源ID
|
||||
* @returns {string} 资源引用
|
||||
*/
|
||||
resolve(resourceId) {
|
||||
// 1. 直接查找
|
||||
if (this.index.has(resourceId)) {
|
||||
return this.index.get(resourceId)
|
||||
}
|
||||
|
||||
// 2. 向后兼容:尝试添加协议前缀
|
||||
const protocols = ['role', 'thought', 'execution', 'memory']
|
||||
|
||||
for (const protocol of protocols) {
|
||||
const fullId = `${protocol}:${resourceId}`
|
||||
if (this.index.has(fullId)) {
|
||||
return this.index.get(fullId)
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Resource '${resourceId}' not found`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查资源是否存在
|
||||
* @param {string} resourceId - 资源ID
|
||||
* @returns {boolean} 是否存在
|
||||
*/
|
||||
has(resourceId) {
|
||||
try {
|
||||
this.resolve(resourceId)
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资源元数据
|
||||
* @param {string} resourceId - 资源ID
|
||||
* @returns {Object|null} 元数据对象或null
|
||||
*/
|
||||
getMetadata(resourceId) {
|
||||
// 直接查找
|
||||
if (this.metadata.has(resourceId)) {
|
||||
return { ...this.metadata.get(resourceId) }
|
||||
}
|
||||
|
||||
// 向后兼容查找
|
||||
const protocols = ['role', 'thought', 'execution', 'memory']
|
||||
|
||||
for (const protocol of protocols) {
|
||||
const fullId = `${protocol}:${resourceId}`
|
||||
if (this.metadata.has(fullId)) {
|
||||
return { ...this.metadata.get(fullId) }
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有资源ID
|
||||
* @param {string} [protocol] - 可选的协议过滤器
|
||||
* @returns {Array<string>} 资源ID列表
|
||||
*/
|
||||
list(protocol = null) {
|
||||
const allIds = Array.from(this.index.keys())
|
||||
|
||||
if (!protocol) {
|
||||
return allIds
|
||||
}
|
||||
|
||||
return allIds.filter(id => id.startsWith(`${protocol}:`))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取注册表大小
|
||||
* @returns {number} 资源数量
|
||||
*/
|
||||
size() {
|
||||
return this.index.size
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空注册表
|
||||
*/
|
||||
clear() {
|
||||
this.index.clear()
|
||||
this.metadata.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除资源
|
||||
* @param {string} resourceId - 资源ID
|
||||
*/
|
||||
remove(resourceId) {
|
||||
// 尝试直接移除
|
||||
if (this.index.has(resourceId)) {
|
||||
this.index.delete(resourceId)
|
||||
this.metadata.delete(resourceId)
|
||||
return
|
||||
}
|
||||
|
||||
// 向后兼容移除
|
||||
const protocols = ['role', 'thought', 'execution', 'memory']
|
||||
|
||||
for (const protocol of protocols) {
|
||||
const fullId = `${protocol}:${resourceId}`
|
||||
if (this.index.has(fullId)) {
|
||||
this.index.delete(fullId)
|
||||
this.metadata.delete(fullId)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从发现管理器结果加载资源
|
||||
* @param {Array} discoveryResults - 发现器返回的资源数组
|
||||
*/
|
||||
loadFromDiscoveryResults(discoveryResults) {
|
||||
if (!Array.isArray(discoveryResults)) {
|
||||
console.warn('[EnhancedResourceRegistry] Discovery results must be an array')
|
||||
return
|
||||
}
|
||||
|
||||
this.registerBatch(discoveryResults)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证资源对象
|
||||
* @param {Object} resource - 资源对象
|
||||
* @private
|
||||
*/
|
||||
_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')
|
||||
}
|
||||
|
||||
if (!resource.metadata || typeof resource.metadata !== 'object') {
|
||||
throw new Error('Resource must have metadata with source and priority')
|
||||
}
|
||||
|
||||
if (!resource.metadata.source || typeof resource.metadata.priority !== 'number') {
|
||||
throw new Error('Resource must have metadata with source and priority')
|
||||
}
|
||||
|
||||
// 验证ID格式
|
||||
if (typeof resource.id !== 'string' || !resource.id.includes(':')) {
|
||||
throw new Error('Resource id must be in format "protocol:resourcePath"')
|
||||
}
|
||||
|
||||
// 验证引用格式
|
||||
if (typeof resource.reference !== 'string' || !resource.reference.startsWith('@')) {
|
||||
throw new Error('Resource reference must be in DPML format "@protocol://path"')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否应该覆盖现有资源
|
||||
* @param {Object} existingMetadata - 现有资源元数据
|
||||
* @param {Object} newMetadata - 新资源元数据
|
||||
* @returns {boolean} 是否应该覆盖
|
||||
* @private
|
||||
*/
|
||||
_shouldOverride(existingMetadata, newMetadata) {
|
||||
// 1. 按发现源优先级比较
|
||||
const existingSourcePriority = this.sourcePriority[existingMetadata.source] || 999
|
||||
const newSourcePriority = this.sourcePriority[newMetadata.source] || 999
|
||||
|
||||
if (newSourcePriority < existingSourcePriority) {
|
||||
return true // 新资源优先级更高
|
||||
}
|
||||
|
||||
if (newSourcePriority > existingSourcePriority) {
|
||||
return false // 现有资源优先级更高
|
||||
}
|
||||
|
||||
// 2. 相同优先级,按数字优先级比较
|
||||
if (newMetadata.priority < existingMetadata.priority) {
|
||||
return true // 数字越小优先级越高
|
||||
}
|
||||
|
||||
if (newMetadata.priority > existingMetadata.priority) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 3. 相同优先级,按时间戳比较(新的覆盖旧的)
|
||||
const existingTime = existingMetadata.timestamp ? new Date(existingMetadata.timestamp).getTime() : 0
|
||||
const newTime = newMetadata.timestamp ? new Date(newMetadata.timestamp).getTime() : 0
|
||||
|
||||
return newTime >= existingTime
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EnhancedResourceRegistry
|
||||
127
src/lib/core/resource/discovery/BaseDiscovery.js
Normal file
127
src/lib/core/resource/discovery/BaseDiscovery.js
Normal 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
|
||||
178
src/lib/core/resource/discovery/CrossPlatformFileScanner.js
Normal file
178
src/lib/core/resource/discovery/CrossPlatformFileScanner.js
Normal 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
|
||||
159
src/lib/core/resource/discovery/DiscoveryManager.js
Normal file
159
src/lib/core/resource/discovery/DiscoveryManager.js
Normal 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
|
||||
228
src/lib/core/resource/discovery/PackageDiscovery.js
Normal file
228
src/lib/core/resource/discovery/PackageDiscovery.js
Normal 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
|
||||
223
src/lib/core/resource/discovery/ProjectDiscovery.js
Normal file
223
src/lib/core/resource/discovery/ProjectDiscovery.js
Normal 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
|
||||
Reference in New Issue
Block a user