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.

359 lines (309 loc) 11.7 kB
const logger = require('../../../utils/logger') const RegistryData = require('../RegistryData') const ResourceData = require('../ResourceData') const fs = require('fs-extra') const path = require('path') /** * ProjectDiscovery - 项目级资源发现器(恢复重构前完整逻辑) * * 核心设计原则: * 1. 完全基于@project协议,支持HTTP/本地模式 * 2. 优先使用注册表,fallback到动态扫描 * 3. 恢复重构前的专业目录结构处理能力 * 4. 恢复完整的ResourceData构建和文件验证逻辑 */ class ProjectDiscovery { constructor() { this.source = 'PROJECT' this.priority = 2 this.projectProtocol = null } /** * 获取ProjectProtocol实例 */ getProjectProtocol() { if (!this.projectProtocol) { const { getGlobalResourceManager } = require('../../resource') const resourceManager = getGlobalResourceManager() this.projectProtocol = resourceManager.protocols.get('project') } return this.projectProtocol } /** * 发现项目级资源注册表 * @returns {Promise<Map>} 资源注册表 Map<resourceId, reference> */ async discoverRegistry() { try { // 1. 优先尝试从注册表加载 const registryMap = await this.loadFromRegistry() if (registryMap.size > 0) { logger.debug(`ProjectDiscovery 从注册表加载 ${registryMap.size} 个资源`) return registryMap } // 2. Fallback: 动态扫描生成注册表 logger.debug('ProjectDiscovery 注册表不存在,使用动态扫描') const resources = await this.scanProjectResources() return this.buildRegistryFromResources(resources) } catch (error) { logger.warn(`[ProjectDiscovery] Registry discovery failed: ${error.message}`) return new Map() } } /** * 从注册表文件加载资源 * @returns {Promise<Map>} 资源注册表 */ async loadFromRegistry() { try { const protocol = this.getProjectProtocol() // 使用@project协议检查注册表文件 const registryPath = await protocol.resolvePath('.promptx/resource/project.registry.json') if (!await fs.pathExists(registryPath)) { return new Map() } // 加载并解析注册表 const registryData = await RegistryData.fromFile('project', registryPath) return registryData.getResourceMap(true) // 带前缀 } catch (error) { logger.warn(`[ProjectDiscovery] Failed to load registry: ${error.message}`) return new Map() } } /** * 动态扫描项目资源 - 恢复重构前的专业扫描逻辑 * @returns {Promise<Array>} 资源列表 */ async scanProjectResources() { try { const protocol = this.getProjectProtocol() // 使用@project协议获取资源目录 const resourceDir = await protocol.resolvePath('.promptx/resource') if (!await fs.pathExists(resourceDir)) { logger.debug('ProjectDiscovery 项目资源目录不存在') return [] } // 创建临时注册表来收集资源 const tempRegistry = RegistryData.createEmpty('project', null) // 扫描专业目录结构 await this._scanDirectory(resourceDir, tempRegistry) // 转换为资源列表 const resources = [] for (const resource of tempRegistry.resources) { resources.push({ id: resource.id, protocol: resource.protocol, reference: resource.reference, source: resource.source }) } logger.info(`[ProjectDiscovery] ✅ 项目扫描完成,发现 ${resources.length} 个资源`) return resources } catch (error) { logger.warn(`[ProjectDiscovery] 扫描项目资源失败: ${error.message}`) return [] } } /** * 扫描目录并添加资源到注册表(通用递归扫描) * @param {string} resourcesDir - 资源目录 * @param {RegistryData} registryData - 注册表数据 * @private */ async _scanDirectory(resourcesDir, registryData) { try { // 递归扫描整个resource目录 await this._recursiveScan(resourcesDir, '', registryData) } catch (error) { logger.warn(`[ProjectDiscovery] 扫描资源目录失败: ${error.message}`) } } /** * 递归扫描目录 * @param {string} currentPath - 当前扫描路径 * @param {string} relativePath - 相对于resource目录的路径 * @param {RegistryData} registryData - 注册表数据 * @private */ async _recursiveScan(currentPath, relativePath, registryData) { try { const items = await fs.readdir(currentPath) for (const item of items) { const itemPath = path.join(currentPath, item) const stat = await fs.stat(itemPath) const newRelativePath = relativePath ? `${relativePath}/${item}` : item if (stat.isDirectory()) { // 递归扫描子目录 await this._recursiveScan(itemPath, newRelativePath, registryData) } else { // 处理文件 await this._processFile(itemPath, newRelativePath, registryData) } } } catch (error) { logger.warn(`[ProjectDiscovery] 扫描${currentPath}失败: ${error.message}`) } } /** * 处理单个文件 * @param {string} filePath - 文件完整路径 * @param {string} relativePath - 相对路径 * @param {RegistryData} registryData - 注册表数据 * @private */ async _processFile(filePath, relativePath, registryData) { const fileName = path.basename(filePath) let protocol = null let resourceId = null // 根据文件名后缀识别资源类型 if (fileName.endsWith('.role.md')) { protocol = 'role' resourceId = path.basename(fileName, '.role.md') } else if (fileName.endsWith('.thought.md')) { protocol = 'thought' resourceId = path.basename(fileName, '.thought.md') } else if (fileName.endsWith('.execution.md')) { protocol = 'execution' resourceId = path.basename(fileName, '.execution.md') } else if (fileName.endsWith('.knowledge.md')) { protocol = 'knowledge' resourceId = path.basename(fileName, '.knowledge.md') } else if (fileName.endsWith('.tool.js')) { protocol = 'tool' resourceId = path.basename(fileName, '.tool.js') } else if (fileName.endsWith('.manual.md')) { protocol = 'manual' resourceId = path.basename(fileName, '.manual.md') } if (protocol && resourceId) { // 验证文件内容 if (await this._validateResourceFile(filePath, protocol)) { const reference = `@project://.promptx/resource/${relativePath}` const resourceData = new ResourceData({ id: resourceId, source: 'project', protocol: protocol, name: ResourceData._generateDefaultName(resourceId, protocol), description: ResourceData._generateDefaultDescription(resourceId, protocol), reference: reference, metadata: { scannedAt: new Date().toISOString(), path: relativePath } }) registryData.addResource(resourceData) logger.debug(`[ProjectDiscovery] 发现${protocol}资源: ${resourceId} at ${relativePath}`) } } } /** * 验证资源文件格式(恢复重构前逻辑) * @param {string} filePath - 文件路径 * @param {string} protocol - 协议类型 * @returns {Promise<boolean>} 是否是有效的资源文件 */ async _validateResourceFile(filePath, protocol) { try { const content = await fs.readFile(filePath, 'utf8') 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>') case 'knowledge': // knowledge类型比较灵活,只要文件有内容就认为是有效的 return true case 'manual': return trimmedContent.includes('<manual>') && trimmedContent.includes('</manual>') case 'tool': // tool文件是JavaScript,进行基本的语法验证 try { new Function(trimmedContent) return true } catch (e) { logger.warn(`[ProjectDiscovery] Invalid JavaScript in tool file ${filePath}: ${e.message}`) return false } default: return false } } catch (error) { logger.warn(`[ProjectDiscovery] Failed to validate ${filePath}: ${error.message}`) return false } } /** * 从资源列表构建注册表Map * @param {Array} resources - 资源列表 * @returns {Map} 资源注册表 */ buildRegistryFromResources(resources) { const registryMap = new Map() resources.forEach(resource => { const key = `project:${resource.id}` registryMap.set(key, resource.reference) }) return registryMap } /** * 生成并保存项目注册表文件 * @returns {Promise<RegistryData>} 生成的注册表数据 */ async generateRegistry() { try { const protocol = this.getProjectProtocol() // 获取注册表文件路径 const registryPath = await protocol.resolvePath('.promptx/resource/project.registry.json') // 创建注册表数据 const registryData = RegistryData.createEmpty('project', registryPath) // 扫描资源目录 const resourceDir = await protocol.resolvePath('.promptx/resource') if (await fs.pathExists(resourceDir)) { await this._scanDirectory(resourceDir, registryData) } // 确保目录存在 await fs.ensureDir(path.dirname(registryPath)) // 保存注册表 await registryData.save() logger.info(`[ProjectDiscovery] ✅ 项目注册表生成完成,发现 ${registryData.size} 个资源`) return registryData } catch (error) { logger.error(`[ProjectDiscovery] 生成注册表失败: ${error.message}`) return RegistryData.createEmpty('project') } } /** * 获取注册表数据(兼容旧接口) * @returns {Promise<RegistryData>} 注册表数据 */ async getRegistryData() { try { const protocol = this.getProjectProtocol() const registryPath = await protocol.resolvePath('.promptx/resource/project.registry.json') if (await fs.pathExists(registryPath)) { const registryData = await RegistryData.fromFile('project', registryPath) if (registryData.size > 0) { logger.info(`[ProjectDiscovery] 📋 从注册表加载 ${registryData.size} 个资源`) return registryData } } // 动态生成注册表 logger.info(`[ProjectDiscovery] 📋 项目注册表无效,重新生成`) return await this.generateRegistry() } catch (error) { logger.error(`[ProjectDiscovery] 获取注册表数据失败: ${error.message}`) return RegistryData.createEmpty('project') } } } module.exports = ProjectDiscovery