diff --git a/src/lib/core/pouch/commands/InitCommand.js b/src/lib/core/pouch/commands/InitCommand.js index e7550ac..1ea40f1 100644 --- a/src/lib/core/pouch/commands/InitCommand.js +++ b/src/lib/core/pouch/commands/InitCommand.js @@ -39,10 +39,8 @@ class InitCommand extends BasePouchCommand { } else if (args && typeof args[0] === 'string') { // CLI格式 workingDirectory = args[0] - } else if (args && args.length > 0 && args[0]) { - // 兜底:直接取第一个参数 - workingDirectory = args[0] } + // 注意:如果args[0]是空对象{},workingDirectory保持undefined,走后续的自动检测逻辑 let projectPath diff --git a/src/lib/core/resource/discovery/FilePatternDiscovery.js b/src/lib/core/resource/discovery/FilePatternDiscovery.js new file mode 100644 index 0000000..c8c2637 --- /dev/null +++ b/src/lib/core/resource/discovery/FilePatternDiscovery.js @@ -0,0 +1,380 @@ +const BaseDiscovery = require('./BaseDiscovery') +const logger = require('../../../utils/logger') +const fs = require('fs-extra') +const path = require('path') +const CrossPlatformFileScanner = require('./CrossPlatformFileScanner') +const RegistryData = require('../RegistryData') +const ResourceData = require('../ResourceData') + +/** + * FilePatternDiscovery - 基于文件模式的资源发现基类 + * + * 统一的文件模式识别逻辑,支持: + * - *.role.md (角色资源) + * - *.thought.md (思维模式) + * - *.execution.md (执行模式) + * - *.knowledge.md (知识资源) + * - *.tool.js (工具资源) + * + * 子类只需要重写 _getBaseDirectory() 方法指定扫描目录 + */ +class FilePatternDiscovery extends BaseDiscovery { + constructor(source, priority) { + super(source, priority) + this.fileScanner = new CrossPlatformFileScanner() + + // 定义资源类型及其文件模式(遵循ResourceProtocol标准) + this.resourcePatterns = { + 'role': { + extensions: ['.role.md'], + validator: this._validateRoleFile.bind(this) + }, + 'thought': { + extensions: ['.thought.md'], + validator: this._validateThoughtFile.bind(this) + }, + 'execution': { + extensions: ['.execution.md'], + validator: this._validateExecutionFile.bind(this) + }, + 'knowledge': { + extensions: ['.knowledge.md'], + validator: this._validateKnowledgeFile.bind(this) + }, + 'tool': { + extensions: ['.tool.js'], + validator: this._validateToolFile.bind(this) + } + } + } + + /** + * 抽象方法:获取扫描基础目录 + * 子类必须实现此方法来指定各自的扫描根目录 + * @returns {Promise} 扫描基础目录路径 + */ + async _getBaseDirectory() { + throw new Error('Subclass must implement _getBaseDirectory() method') + } + + /** + * 统一的资源扫描逻辑 + * @param {RegistryData} registryData - 注册表数据对象 + * @returns {Promise} + */ + async _scanResourcesByFilePattern(registryData) { + const baseDirectory = await this._getBaseDirectory() + + if (!await fs.pathExists(baseDirectory)) { + logger.debug(`[${this.source}] 扫描目录不存在: ${baseDirectory}`) + return + } + + logger.debug(`[${this.source}] 开始扫描目录: ${baseDirectory}`) + + // 并行扫描所有资源类型 + const resourceTypes = Object.keys(this.resourcePatterns) + + for (const resourceType of resourceTypes) { + try { + const pattern = this.resourcePatterns[resourceType] + const files = await this._scanResourceFiles(baseDirectory, resourceType, pattern.extensions) + + for (const filePath of files) { + await this._processResourceFile(filePath, resourceType, registryData, baseDirectory, pattern.validator) + } + + logger.debug(`[${this.source}] ${resourceType} 类型扫描完成,发现 ${files.length} 个文件`) + + } catch (error) { + logger.warn(`[${this.source}] 扫描 ${resourceType} 类型失败: ${error.message}`) + } + } + } + + /** + * 扫描特定类型的资源文件 + * @param {string} baseDirectory - 基础目录 + * @param {string} resourceType - 资源类型 + * @param {Array} extensions - 文件扩展名列表 + * @returns {Promise>} 匹配的文件路径列表 + */ + async _scanResourceFiles(baseDirectory, resourceType, extensions) { + const allFiles = [] + + for (const extension of extensions) { + try { + // 使用现有的CrossPlatformFileScanner但扩展支持任意扩展名 + const files = await this.fileScanner.scanFiles(baseDirectory, { + extensions: [extension], + recursive: true, + maxDepth: 10 + }) + allFiles.push(...files) + } catch (error) { + logger.warn(`[${this.source}] 扫描 ${extension} 文件失败: ${error.message}`) + } + } + + return allFiles + } + + /** + * 处理单个资源文件 + * @param {string} filePath - 文件路径 + * @param {string} resourceType - 资源类型 + * @param {RegistryData} registryData - 注册表数据 + * @param {string} baseDirectory - 基础目录 + * @param {Function} validator - 文件验证器 + */ + async _processResourceFile(filePath, resourceType, registryData, baseDirectory, validator) { + try { + // 1. 验证文件内容 + const isValid = await validator(filePath) + if (!isValid) { + logger.debug(`[${this.source}] 文件验证失败,跳过: ${filePath}`) + return + } + + // 2. 提取资源ID(遵循ResourceProtocol命名标准) + const resourceId = this._extractResourceId(filePath, resourceType) + if (!resourceId) { + logger.warn(`[${this.source}] 无法提取资源ID: ${filePath}`) + return + } + + // 3. 生成引用路径 + const reference = this._generateReference(filePath, baseDirectory) + + // 4. 创建ResourceData对象 + const resourceData = new ResourceData({ + id: resourceId, + source: this.source.toLowerCase(), + protocol: resourceType, + name: ResourceData._generateDefaultName(resourceId, resourceType), + description: ResourceData._generateDefaultDescription(resourceId, resourceType), + reference: reference, + metadata: { + scannedAt: new Date().toISOString(), + filePath: filePath, + fileType: resourceType + } + }) + + // 5. 添加到注册表 + registryData.addResource(resourceData) + + logger.debug(`[${this.source}] 成功处理资源: ${resourceId} -> ${reference}`) + + } catch (error) { + logger.warn(`[${this.source}] 处理资源文件失败: ${filePath} - ${error.message}`) + } + } + + /** + * 提取资源ID(遵循ResourceProtocol标准) + * @param {string} filePath - 文件路径 + * @param {string} resourceType - 资源类型 + * @returns {string|null} 资源ID + */ + _extractResourceId(filePath, resourceType) { + const fileName = path.basename(filePath) + const pattern = this.resourcePatterns[resourceType] + + if (!pattern) { + return null + } + + // 尝试匹配扩展名 + for (const extension of pattern.extensions) { + if (fileName.endsWith(extension)) { + const baseName = fileName.slice(0, -extension.length) + + // role类型直接返回基础名称,其他类型添加前缀 + if (resourceType === 'role') { + return baseName + } else { + return `${resourceType}:${baseName}` + } + } + } + + return null + } + + /** + * 生成资源引用路径 + * @param {string} filePath - 文件绝对路径 + * @param {string} baseDirectory - 基础目录 + * @returns {string} 资源引用路径 + */ + _generateReference(filePath, baseDirectory) { + const relativePath = path.relative(baseDirectory, filePath) + const protocolPrefix = this.source.toLowerCase() === 'project' ? '@project://' : '@package://' + + // 对于project源,添加.promptx/resource前缀 + if (this.source.toLowerCase() === 'project') { + return `${protocolPrefix}.promptx/resource/${relativePath.replace(/\\/g, '/')}` + } else { + return `${protocolPrefix}resource/${relativePath.replace(/\\/g, '/')}` + } + } + + // ==================== 文件验证器 ==================== + + /** + * 验证Role文件 + * @param {string} filePath - 文件路径 + * @returns {Promise} 是否有效 + */ + async _validateRoleFile(filePath) { + try { + const content = await fs.readFile(filePath, 'utf8') + const trimmedContent = content.trim() + + if (trimmedContent.length === 0) { + return false + } + + // 检查DPML标签 + return trimmedContent.includes('') && trimmedContent.includes('') + } catch (error) { + return false + } + } + + /** + * 验证Thought文件 + * @param {string} filePath - 文件路径 + * @returns {Promise} 是否有效 + */ + async _validateThoughtFile(filePath) { + try { + const content = await fs.readFile(filePath, 'utf8') + const trimmedContent = content.trim() + + if (trimmedContent.length === 0) { + return false + } + + return trimmedContent.includes('') && trimmedContent.includes('') + } catch (error) { + return false + } + } + + /** + * 验证Execution文件 + * @param {string} filePath - 文件路径 + * @returns {Promise} 是否有效 + */ + async _validateExecutionFile(filePath) { + try { + const content = await fs.readFile(filePath, 'utf8') + const trimmedContent = content.trim() + + if (trimmedContent.length === 0) { + return false + } + + return trimmedContent.includes('') && trimmedContent.includes('') + } catch (error) { + return false + } + } + + /** + * 验证Knowledge文件 + * @param {string} filePath - 文件路径 + * @returns {Promise} 是否有效 + */ + async _validateKnowledgeFile(filePath) { + try { + const content = await fs.readFile(filePath, 'utf8') + const trimmedContent = content.trim() + + // knowledge文件比较灵活,只要有内容就认为有效 + return trimmedContent.length > 0 + } catch (error) { + return false + } + } + + /** + * 验证Tool文件(遵循ResourceProtocol标准) + * @param {string} filePath - 文件路径 + * @returns {Promise} 是否有效 + */ + async _validateToolFile(filePath) { + try { + const content = await fs.readFile(filePath, 'utf8') + + // 1. 检查JavaScript语法 + new Function(content) + + // 2. 检查CommonJS导出 + if (!content.includes('module.exports')) { + return false + } + + // 3. 检查必需的方法(遵循ResourceProtocol标准) + const requiredMethods = ['getMetadata', 'execute'] + const hasRequiredMethods = requiredMethods.some(method => + content.includes(method) + ) + + return hasRequiredMethods + + } catch (error) { + return false + } + } + + /** + * 生成注册表(通用方法) + * @param {string} baseDirectory - 扫描基础目录 + * @returns {Promise} 生成的注册表数据 + */ + async generateRegistry(baseDirectory) { + const registryPath = await this._getRegistryPath() + const registryData = RegistryData.createEmpty(this.source.toLowerCase(), registryPath) + + logger.info(`[${this.source}] 开始生成注册表,扫描目录: ${baseDirectory}`) + + try { + await this._scanResourcesByFilePattern(registryData) + + // 保存注册表文件 + if (registryPath) { + await registryData.save() + } + + logger.info(`[${this.source}] ✅ 注册表生成完成,共发现 ${registryData.size} 个资源`) + return registryData + + } catch (error) { + logger.error(`[${this.source}] ❌ 注册表生成失败: ${error.message}`) + throw error + } + } + + /** + * 获取注册表文件路径(子类可以重写) + * @returns {Promise} 注册表文件路径 + */ + async _getRegistryPath() { + // 默认返回null,子类可以重写 + return null + } + + /** + * 文件系统存在性检查 + * @param {string} filePath - 文件路径 + * @returns {Promise} 文件是否存在 + */ + async _fsExists(filePath) { + return await fs.pathExists(filePath) + } +} + +module.exports = FilePatternDiscovery \ No newline at end of file diff --git a/src/lib/core/resource/discovery/PackageDiscovery.js b/src/lib/core/resource/discovery/PackageDiscovery.js index 2193ed0..633f3ad 100644 --- a/src/lib/core/resource/discovery/PackageDiscovery.js +++ b/src/lib/core/resource/discovery/PackageDiscovery.js @@ -1,11 +1,8 @@ -const BaseDiscovery = require('./BaseDiscovery') +const FilePatternDiscovery = require('./FilePatternDiscovery') const RegistryData = require('../RegistryData') -const ResourceData = require('../ResourceData') -const ResourceFileNaming = require('../ResourceFileNaming') const logger = require('../../../utils/logger') const path = require('path') const fs = require('fs-extra') -const CrossPlatformFileScanner = require('./CrossPlatformFileScanner') const { getDirectoryService } = require('../../../utils/DirectoryService') /** @@ -17,10 +14,9 @@ const { getDirectoryService } = require('../../../utils/DirectoryService') * * 优先级:1 (最高优先级) */ -class PackageDiscovery extends BaseDiscovery { +class PackageDiscovery extends FilePatternDiscovery { constructor() { super('PACKAGE', 1) - this.fileScanner = new CrossPlatformFileScanner() this.directoryService = getDirectoryService() // 将在_getRegistryPath()中动态计算 this.registryPath = null @@ -83,9 +79,17 @@ class PackageDiscovery extends BaseDiscovery { } /** - * 获取注册表路径 + * 实现基类要求的方法:获取包扫描基础目录 + * @returns {Promise} 包资源目录路径 + */ + async _getBaseDirectory() { + const packageRoot = await this._findPackageRoot() + return path.join(packageRoot, 'resource') + } + + /** + * 重写基类方法:获取注册表文件路径 * @returns {Promise} 注册表文件路径 - * @private */ async _getRegistryPath() { if (!this.registryPath) { @@ -156,28 +160,17 @@ class PackageDiscovery extends BaseDiscovery { } /** - * 生成包级资源注册表(用于构建时) - * @param {string} packageRoot - 包根目录 + * 生成包级资源注册表(用于构建时)使用新的基类方法 + * @param {string} packageRoot - 包根目录 * @returns {Promise} 生成的注册表数据 */ async generateRegistry(packageRoot) { logger.info(`[PackageDiscovery] 🏗️ 开始生成包级资源注册表...`) - const registryData = RegistryData.createEmpty('package', this.registryPath) - try { - // 扫描包级资源目录 + // 使用基类的统一生成方法 const resourceDir = path.join(packageRoot, 'resource') - - if (await fs.pathExists(resourceDir)) { - await this._scanDirectory(resourceDir, registryData) - } - - // 保存注册表 - await registryData.save() - - logger.info(`[PackageDiscovery] ✅ 包级注册表生成完成,共 ${registryData.size} 个资源`) - return registryData + return await super.generateRegistry(resourceDir) } catch (error) { logger.error(`[PackageDiscovery] ❌ 注册表生成失败: ${error.message}`) @@ -186,267 +179,21 @@ class PackageDiscovery extends BaseDiscovery { } /** - * 扫描目录并添加资源到注册表 + * 扫描目录并添加资源到注册表(使用新的基类方法) * @param {string} promptDir - prompt目录路径 * @param {RegistryData} registryData - 注册表数据 * @private */ async _scanDirectory(promptDir, registryData) { try { - // 统一扫描:扫描prompt下所有目录的所有资源类型文件 - const resourceTypes = ['role', 'execution', 'thought', 'knowledge', 'tool'] - - for (const resourceType of resourceTypes) { - const files = await this.fileScanner.scanResourceFiles(promptDir, resourceType) - - for (const filePath of files) { - await this._processResourceFile(filePath, resourceType, registryData, promptDir) - } - } + // 使用基类的统一文件模式扫描 + await this._scanResourcesByFilePattern(registryData) } catch (error) { logger.warn(`[PackageDiscovery] 扫描目录失败: ${error.message}`) } } - /** - * 处理单个资源文件 - * @param {string} filePath - 文件路径 - * @param {string} resourceType - 资源类型 - * @param {RegistryData} registryData - 注册表数据 - * @param {string} promptDir - prompt目录路径 - * @private - */ - async _processResourceFile(filePath, resourceType, registryData, promptDir) { - try { - // 提取资源ID - const fileName = path.basename(filePath) - let resourceId - - if (resourceType === 'tool') { - // tool文件:calculator.tool.js -> calculator - resourceId = fileName.replace('.tool.js', '') - } else { - // 其他文件:assistant.role.md -> assistant - resourceId = fileName.replace(`.${resourceType}.md`, '') - } - - // 生成引用路径 - const relativePath = path.relative(path.dirname(promptDir), filePath) - const reference = `@package://${relativePath.replace(/\\/g, '/')}` - - // 创建资源数据 - const resourceData = new ResourceData({ - id: resourceId, - source: 'package', - protocol: resourceType, - name: ResourceData._generateDefaultName(resourceId, resourceType), - description: ResourceData._generateDefaultDescription(resourceId, resourceType), - reference: reference, - metadata: { - scannedAt: new Date().toISOString() - } - }) - - // 对tool文件进行语法验证 - if (resourceType === 'tool') { - if (await this._validateToolFile(filePath)) { - registryData.addResource(resourceData) - } else { - logger.warn(`[PackageDiscovery] Tool文件验证失败,跳过: ${filePath}`) - } - } else { - registryData.addResource(resourceData) - } - - } catch (error) { - logger.warn(`[PackageDiscovery] 处理资源文件失败: ${filePath} - ${error.message}`) - } - } - - /** - * 验证Tool文件格式 - * @param {string} filePath - Tool文件路径 - * @returns {Promise} 是否有效 - * @private - */ - async _validateToolFile(filePath) { - try { - const content = await fs.readFile(filePath, 'utf8') - - // 检查JavaScript语法 - new Function(content) - - // 检查必需的exports - if (!content.includes('module.exports')) { - return false - } - - // 检查必需的方法 - const requiredMethods = ['getMetadata', 'execute'] - return requiredMethods.some(method => content.includes(method)) - - } catch (error) { - return false - } - } - - /** - * 扫描role目录(角色资源) - * @param {string} roleDir - role目录路径 - * @param {RegistryData} registryData - 注册表数据 - * @private - */ - async _scanRoleDirectory(roleDir, registryData) { - const items = await fs.readdir(roleDir) - - for (const item of items) { - const itemPath = path.join(roleDir, item) - const stat = await fs.stat(itemPath) - - if (stat.isDirectory()) { - // 查找角色文件 - const roleFile = path.join(itemPath, `${item}.role.md`) - if (await fs.pathExists(roleFile)) { - const reference = `@package://resource/role/${item}/${item}.role.md` - - const resourceData = new ResourceData({ - id: item, - source: 'package', - protocol: 'role', - name: ResourceData._generateDefaultName(item, 'role'), - description: ResourceData._generateDefaultDescription(item, 'role'), - reference: reference, - metadata: { - scannedAt: new Date().toISOString() - } - }) - - registryData.addResource(resourceData) - } - - // 查找thought文件 - 使用统一命名管理器 - const thoughtDir = path.join(itemPath, 'thought') - if (await fs.pathExists(thoughtDir)) { - const thoughtFiles = await ResourceFileNaming.scanTagFiles(thoughtDir, 'thought') - - for (const thoughtFile of thoughtFiles) { - const thoughtId = ResourceFileNaming.extractResourceId(thoughtFile, 'thought') - if (thoughtId) { - const fileName = path.basename(thoughtFile) - const reference = `@package://resource/role/${item}/thought/${fileName}` - - const resourceData = new ResourceData({ - id: thoughtId, - source: 'package', - protocol: 'thought', - name: ResourceData._generateDefaultName(thoughtId, 'thought'), - description: ResourceData._generateDefaultDescription(thoughtId, 'thought'), - reference: reference, - metadata: { - scannedAt: new Date().toISOString() - } - }) - - registryData.addResource(resourceData) - } - } - } - - // 查找execution文件 - const executionDir = path.join(itemPath, 'execution') - if (await fs.pathExists(executionDir)) { - const executionFiles = await fs.readdir(executionDir) - for (const execFile of executionFiles) { - if (execFile.endsWith('.execution.md')) { - const execId = path.basename(execFile, '.execution.md') - const reference = `@package://resource/role/${item}/execution/${execFile}` - - const resourceData = new ResourceData({ - id: execId, - source: 'package', - protocol: 'execution', - name: ResourceData._generateDefaultName(execId, 'execution'), - description: ResourceData._generateDefaultDescription(execId, 'execution'), - reference: reference, - metadata: { - scannedAt: new Date().toISOString() - } - }) - - registryData.addResource(resourceData) - } - } - } - } - } - } - - /** - * 扫描core目录(核心资源) - * @param {string} coreDir - core目录路径 - * @param {RegistryData} registryData - 注册表数据 - * @private - */ - async _scanCoreDirectory(coreDir, registryData) { - // 扫描core下的直接子目录 - const items = await fs.readdir(coreDir) - - for (const item of items) { - const itemPath = path.join(coreDir, item) - const stat = await fs.stat(itemPath) - - if (stat.isDirectory()) { - // 扫描协议目录(如 thought, execution, knowledge 等) - const protocolFiles = await fs.readdir(itemPath) - - for (const file of protocolFiles) { - if (file.endsWith('.md')) { - const match = file.match(/^(.+)\.(\w+)\.md$/) - if (match) { - const [, id, protocol] = match - const reference = `@package://resource/core/${item}/${file}` - - const resourceData = new ResourceData({ - id: id, - source: 'package', - protocol: protocol, - name: ResourceData._generateDefaultName(id, protocol), - description: ResourceData._generateDefaultDescription(id, protocol), - reference: reference, - metadata: { - scannedAt: new Date().toISOString() - } - }) - - registryData.addResource(resourceData) - } - } - } - } else if (item.endsWith('.md')) { - // 处理core目录下的直接文件 - const match = item.match(/^(.+)\.(\w+)\.md$/) - if (match) { - const [, id, protocol] = match - const reference = `@package://resource/core/${item}` - - const resourceData = new ResourceData({ - id: id, - source: 'package', - protocol: protocol, - name: ResourceData._generateDefaultName(id, protocol), - description: ResourceData._generateDefaultDescription(id, protocol), - reference: reference, - metadata: { - scannedAt: new Date().toISOString() - } - }) - - registryData.addResource(resourceData) - } - } - } - } /** * 加载包级硬编码注册表 (性能优化核心方法) @@ -497,37 +244,22 @@ class PackageDiscovery extends BaseDiscovery { } /** - * 扫描prompt目录发现资源 + * 扫描prompt目录发现资源(使用新的基类方法) * @returns {Promise} 扫描发现的资源列表 */ async _scanPromptDirectory() { try { - const packageRoot = await this._findPackageRoot() - const promptDir = path.join(packageRoot, 'prompt') - - if (!await fs.pathExists(promptDir)) { - return [] - } - + // 使用新的基类扫描方法 + const registryData = RegistryData.createEmpty('package', null) + await this._scanResourcesByFilePattern(registryData) + + // 转换为旧格式兼容性 const resources = [] - - // 定义要扫描的资源类型 - const resourceTypes = ['role', 'execution', 'thought', 'knowledge', 'tool'] - - // 并行扫描所有资源类型 - 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 - }) - } + for (const resource of registryData.resources) { + resources.push({ + id: resource.id, + reference: resource.reference + }) } return resources @@ -537,15 +269,6 @@ class PackageDiscovery extends BaseDiscovery { } } - /** - * 文件扫描(可以被测试mock) - * @param {string} baseDir - 基础目录 - * @param {string} resourceType - 资源类型 - * @returns {Promise} 匹配的文件路径列表 - */ - async _scanFiles(baseDir, resourceType) { - return await this.fileScanner.scanResourceFiles(baseDir, resourceType) - } /** * 检测执行环境类型 @@ -777,28 +500,6 @@ class PackageDiscovery extends BaseDiscovery { } } - /** - * 生成包引用路径 - * @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}` - } /** * 获取RegistryData对象(新架构方法) diff --git a/src/lib/core/resource/discovery/ProjectDiscovery.js b/src/lib/core/resource/discovery/ProjectDiscovery.js index 57def70..c0a7ffb 100644 --- a/src/lib/core/resource/discovery/ProjectDiscovery.js +++ b/src/lib/core/resource/discovery/ProjectDiscovery.js @@ -1,10 +1,8 @@ -const BaseDiscovery = require('./BaseDiscovery') +const FilePatternDiscovery = require('./FilePatternDiscovery') const logger = require('../../../utils/logger') const fs = require('fs-extra') const path = require('path') -const CrossPlatformFileScanner = require('./CrossPlatformFileScanner') const RegistryData = require('../RegistryData') -const ResourceData = require('../ResourceData') /** * ProjectDiscovery - 项目级资源发现器 @@ -16,10 +14,9 @@ const ResourceData = require('../ResourceData') * * 优先级:2 */ -class ProjectDiscovery extends BaseDiscovery { +class ProjectDiscovery extends FilePatternDiscovery { constructor() { super('PROJECT', 2) - this.fileScanner = new CrossPlatformFileScanner() this.registryData = null } @@ -150,42 +147,32 @@ class ProjectDiscovery extends BaseDiscovery { } /** - * 扫描项目资源 + * 实现基类要求的方法:获取项目扫描基础目录 + * @returns {Promise} 项目资源目录路径 + */ + async _getBaseDirectory() { + const projectRoot = await this._findProjectRoot() + return path.join(projectRoot, '.promptx', 'resource') + } + + /** + * 扫描项目资源(使用新的基类方法) * @param {string} projectRoot - 项目根目录 * @returns {Promise} 扫描发现的资源列表 */ async _scanProjectResources(projectRoot) { try { - const resourcesDir = path.join(projectRoot, '.promptx', 'resource') + // 使用新的基类扫描方法 + const registryData = RegistryData.createEmpty('project', null) + await this._scanResourcesByFilePattern(registryData) + + // 转换为旧格式兼容性 const resources = [] - - // 定义要扫描的资源类型 - const resourceTypes = ['role', 'execution', 'thought', 'knowledge', 'tool'] - - // 并行扫描所有资源类型 - 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) { - logger.warn(`[ProjectDiscovery] Failed to scan ${resourceType} resources: ${error.message}`) - } + for (const resource of registryData.resources) { + resources.push({ + id: resource.id, + reference: resource.reference + }) } return resources @@ -196,17 +183,7 @@ class ProjectDiscovery extends BaseDiscovery { } /** - * 文件扫描(可以被测试mock) - * @param {string} baseDir - 基础目录 - * @param {string} resourceType - 资源类型 - * @returns {Promise} 匹配的文件路径列表 - */ - async _scanFiles(baseDir, resourceType) { - return await this.fileScanner.scanResourceFiles(baseDir, resourceType) - } - - /** - * 文件系统存在性检查(可以被测试mock) + * 文件系统存在性检查(保留用于向后兼容) * @param {string} filePath - 文件路径 * @returns {Promise} 文件是否存在 */ @@ -214,115 +191,6 @@ class ProjectDiscovery extends BaseDiscovery { return await fs.pathExists(filePath) } - /** - * 读取文件内容(可以被测试mock) - * @param {string} filePath - 文件路径 - * @returns {Promise} 文件内容 - */ - async _readFile(filePath) { - return await fs.readFile(filePath, 'utf8') - } - - /** - * 验证资源文件格式 - * @param {string} filePath - 文件路径 - * @param {string} protocol - 协议类型 - * @returns {Promise} 是否是有效的资源文件 - */ - 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('') && trimmedContent.includes('') - case 'execution': - return trimmedContent.includes('') && trimmedContent.includes('') - case 'thought': - return trimmedContent.includes('') && trimmedContent.includes('') - case 'knowledge': - // knowledge类型比较灵活,只要文件有内容就认为是有效的 - // 可以是纯文本、链接、图片等任何形式的知识内容 - return true - case 'tool': - // tool类型必须是有效的JavaScript代码 - return this._validateToolFile(content) - default: - return false - } - } catch (error) { - logger.warn(`[ProjectDiscovery] Failed to validate ${filePath}: ${error.message}`) - return false - } - } - - /** - * 验证Tool文件是否为有效的JavaScript代码 - * @param {string} content - 文件内容 - * @returns {boolean} 是否为有效的Tool文件 - */ - _validateToolFile(content) { - try { - // 1. 基本的JavaScript语法检查 - new Function(content); - - // 2. 检查是否包含module.exports(CommonJS格式) - if (!content.includes('module.exports')) { - return false; - } - - // 3. 检查是否包含工具必需的方法(getMetadata, execute等) - const requiredMethods = ['getMetadata', 'execute']; - const hasRequiredMethods = requiredMethods.some(method => - content.includes(method) - ); - - return hasRequiredMethods; - } catch (syntaxError) { - // JavaScript语法错误 - 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 (对于role类型返回resourceName,对于其他类型返回protocol:resourceName) - */ - _extractResourceId(filePath, protocol, suffix) { - const fileName = path.basename(filePath, suffix) - - // role类型不需要前缀,其他类型需要前缀 - if (protocol === 'role') { - return fileName - } else { - return `${protocol}:${fileName}` - } - } - /** * 生成项目级注册表文件 * @param {string} projectRoot - 项目根目录 @@ -347,17 +215,14 @@ class ProjectDiscovery extends BaseDiscovery { } /** - * 扫描目录并添加资源到注册表 + * 扫描目录并添加资源到注册表(使用新的基类方法) * @param {string} resourcesDir - 资源目录 * @param {RegistryData} registryData - 注册表数据 * @private */ async _scanDirectory(resourcesDir, registryData) { - // 扫描role目录 - const roleDir = path.join(resourcesDir, 'role') - if (await this._fsExists(roleDir)) { - await this._scanRoleDirectory(roleDir, registryData) - } + // 使用基类的统一文件模式扫描 + await this._scanResourcesByFilePattern(registryData) } /** @@ -475,14 +340,22 @@ class ProjectDiscovery extends BaseDiscovery { } } + /** + * 重写基类方法:获取注册表文件路径 + * @returns {Promise} 注册表文件路径 + */ + async _getRegistryPath() { + const projectRoot = await this._findProjectRoot() + return path.join(projectRoot, '.promptx', 'resource', 'project.registry.json') + } + /** * 获取RegistryData对象(新架构方法) * @returns {Promise} 项目级RegistryData对象 */ async getRegistryData() { try { - const projectRoot = await this._findProjectRoot() - const registryPath = path.join(projectRoot, '.promptx', 'resource', 'project.registry.json') + const registryPath = await this._getRegistryPath() // 尝试加载现有的注册表文件 if (await this._fsExists(registryPath)) { @@ -499,11 +372,13 @@ class ProjectDiscovery extends BaseDiscovery { // 如果注册表无效,重新生成 logger.info(`[ProjectDiscovery] 📋 项目注册表无效,重新生成`) - return await this.generateRegistry(projectRoot) + const baseDirectory = await this._getBaseDirectory() + return await this.generateRegistry(baseDirectory) } else { // 如果没有注册表文件,生成新的 logger.info(`[ProjectDiscovery] 📋 项目注册表不存在,生成新注册表`) - return await this.generateRegistry(projectRoot) + const baseDirectory = await this._getBaseDirectory() + return await this.generateRegistry(baseDirectory) } } catch (error) { logger.warn(`[ProjectDiscovery] Failed to load RegistryData: ${error.message}`)