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
JavaScript
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