UNPKG

dpml-prompt

Version:

DPML-powered AI prompt framework - Revolutionary AI-First CLI system based on Deepractice Prompt Markup Language. Build sophisticated AI agents with structured prompts, memory systems, and execution frameworks.

377 lines (322 loc) 11.5 kB
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<string>} 扫描基础目录路径 */ async _getBaseDirectory() { throw new Error('Subclass must implement _getBaseDirectory() method') } /** * 统一的资源扫描逻辑 * @param {RegistryData} registryData - 注册表数据对象 * @returns {Promise<void>} */ 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<string>} extensions - 文件扩展名列表 * @returns {Promise<Array<string>>} 匹配的文件路径列表 */ 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) // 所有资源类型都直接返回基础名称,不添加前缀 // 协议信息已经在resource对象的protocol字段中 return 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<boolean>} 是否有效 */ 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('<role>') && trimmedContent.includes('</role>') } catch (error) { return false } } /** * 验证Thought文件 * @param {string} filePath - 文件路径 * @returns {Promise<boolean>} 是否有效 */ async _validateThoughtFile(filePath) { try { const content = await fs.readFile(filePath, 'utf8') const trimmedContent = content.trim() if (trimmedContent.length === 0) { return false } return trimmedContent.includes('<thought>') && trimmedContent.includes('</thought>') } catch (error) { return false } } /** * 验证Execution文件 * @param {string} filePath - 文件路径 * @returns {Promise<boolean>} 是否有效 */ async _validateExecutionFile(filePath) { try { const content = await fs.readFile(filePath, 'utf8') const trimmedContent = content.trim() if (trimmedContent.length === 0) { return false } return trimmedContent.includes('<execution>') && trimmedContent.includes('</execution>') } catch (error) { return false } } /** * 验证Knowledge文件 * @param {string} filePath - 文件路径 * @returns {Promise<boolean>} 是否有效 */ 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<boolean>} 是否有效 */ 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<RegistryData>} 生成的注册表数据 */ 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<string|null>} 注册表文件路径 */ async _getRegistryPath() { // 默认返回null,子类可以重写 return null } /** * 文件系统存在性检查 * @param {string} filePath - 文件路径 * @returns {Promise<boolean>} 文件是否存在 */ async _fsExists(filePath) { return await fs.pathExists(filePath) } } module.exports = FilePatternDiscovery