feat: 实现基于文件模式的灵活资源发现架构
## 🏗️ 新增 FilePatternDiscovery 基类 - 统一的文件模式识别逻辑,支持 .role.md, .thought.md, .execution.md, .knowledge.md, .tool.js - 递归扫描任意目录结构,完全基于文件扩展名识别资源类型 - 统一的资源验证和引用路径生成机制 ## 🔄 重构 ProjectDiscovery 和 PackageDiscovery - 继承 FilePatternDiscovery 基类,大幅简化代码 - 子类只需重写 _getBaseDirectory() 指定扫描目录 - 移除重复的文件扫描和验证逻辑,提升维护性 ## 🎯 实现完全灵活的目录结构支持 - resource/ 下支持任意目录组织方式 - 目录名称仅有语义意义,不影响资源发现 - 支持深层嵌套和扁平化结构 ## 🔧 修复 InitCommand 参数处理 - 优化空对象参数的处理逻辑 - 保持向后兼容的同时提升健壮性 ## ✅ 测试验证 - welcome/action/init 命令全面测试通过 - 包级61个资源 + 项目级8个资源正确发现 - project 协议和工具文件识别正常工作 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -39,10 +39,8 @@ class InitCommand extends BasePouchCommand {
|
|||||||
} else if (args && typeof args[0] === 'string') {
|
} else if (args && typeof args[0] === 'string') {
|
||||||
// CLI格式
|
// CLI格式
|
||||||
workingDirectory = args[0]
|
workingDirectory = args[0]
|
||||||
} else if (args && args.length > 0 && args[0]) {
|
|
||||||
// 兜底:直接取第一个参数
|
|
||||||
workingDirectory = args[0]
|
|
||||||
}
|
}
|
||||||
|
// 注意:如果args[0]是空对象{},workingDirectory保持undefined,走后续的自动检测逻辑
|
||||||
|
|
||||||
let projectPath
|
let projectPath
|
||||||
|
|
||||||
|
|||||||
380
src/lib/core/resource/discovery/FilePatternDiscovery.js
Normal file
380
src/lib/core/resource/discovery/FilePatternDiscovery.js
Normal file
@ -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<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)
|
||||||
|
|
||||||
|
// 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<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
|
||||||
@ -1,11 +1,8 @@
|
|||||||
const BaseDiscovery = require('./BaseDiscovery')
|
const FilePatternDiscovery = require('./FilePatternDiscovery')
|
||||||
const RegistryData = require('../RegistryData')
|
const RegistryData = require('../RegistryData')
|
||||||
const ResourceData = require('../ResourceData')
|
|
||||||
const ResourceFileNaming = require('../ResourceFileNaming')
|
|
||||||
const logger = require('../../../utils/logger')
|
const logger = require('../../../utils/logger')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
const CrossPlatformFileScanner = require('./CrossPlatformFileScanner')
|
|
||||||
const { getDirectoryService } = require('../../../utils/DirectoryService')
|
const { getDirectoryService } = require('../../../utils/DirectoryService')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,10 +14,9 @@ const { getDirectoryService } = require('../../../utils/DirectoryService')
|
|||||||
*
|
*
|
||||||
* 优先级:1 (最高优先级)
|
* 优先级:1 (最高优先级)
|
||||||
*/
|
*/
|
||||||
class PackageDiscovery extends BaseDiscovery {
|
class PackageDiscovery extends FilePatternDiscovery {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('PACKAGE', 1)
|
super('PACKAGE', 1)
|
||||||
this.fileScanner = new CrossPlatformFileScanner()
|
|
||||||
this.directoryService = getDirectoryService()
|
this.directoryService = getDirectoryService()
|
||||||
// 将在_getRegistryPath()中动态计算
|
// 将在_getRegistryPath()中动态计算
|
||||||
this.registryPath = null
|
this.registryPath = null
|
||||||
@ -83,9 +79,17 @@ class PackageDiscovery extends BaseDiscovery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取注册表路径
|
* 实现基类要求的方法:获取包扫描基础目录
|
||||||
|
* @returns {Promise<string>} 包资源目录路径
|
||||||
|
*/
|
||||||
|
async _getBaseDirectory() {
|
||||||
|
const packageRoot = await this._findPackageRoot()
|
||||||
|
return path.join(packageRoot, 'resource')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重写基类方法:获取注册表文件路径
|
||||||
* @returns {Promise<string>} 注册表文件路径
|
* @returns {Promise<string>} 注册表文件路径
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
async _getRegistryPath() {
|
async _getRegistryPath() {
|
||||||
if (!this.registryPath) {
|
if (!this.registryPath) {
|
||||||
@ -156,28 +160,17 @@ class PackageDiscovery extends BaseDiscovery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成包级资源注册表(用于构建时)
|
* 生成包级资源注册表(用于构建时)使用新的基类方法
|
||||||
* @param {string} packageRoot - 包根目录
|
* @param {string} packageRoot - 包根目录
|
||||||
* @returns {Promise<RegistryData>} 生成的注册表数据
|
* @returns {Promise<RegistryData>} 生成的注册表数据
|
||||||
*/
|
*/
|
||||||
async generateRegistry(packageRoot) {
|
async generateRegistry(packageRoot) {
|
||||||
logger.info(`[PackageDiscovery] 🏗️ 开始生成包级资源注册表...`)
|
logger.info(`[PackageDiscovery] 🏗️ 开始生成包级资源注册表...`)
|
||||||
|
|
||||||
const registryData = RegistryData.createEmpty('package', this.registryPath)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 扫描包级资源目录
|
// 使用基类的统一生成方法
|
||||||
const resourceDir = path.join(packageRoot, 'resource')
|
const resourceDir = path.join(packageRoot, 'resource')
|
||||||
|
return await super.generateRegistry(resourceDir)
|
||||||
if (await fs.pathExists(resourceDir)) {
|
|
||||||
await this._scanDirectory(resourceDir, registryData)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存注册表
|
|
||||||
await registryData.save()
|
|
||||||
|
|
||||||
logger.info(`[PackageDiscovery] ✅ 包级注册表生成完成,共 ${registryData.size} 个资源`)
|
|
||||||
return registryData
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[PackageDiscovery] ❌ 注册表生成失败: ${error.message}`)
|
logger.error(`[PackageDiscovery] ❌ 注册表生成失败: ${error.message}`)
|
||||||
@ -186,267 +179,21 @@ class PackageDiscovery extends BaseDiscovery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 扫描目录并添加资源到注册表
|
* 扫描目录并添加资源到注册表(使用新的基类方法)
|
||||||
* @param {string} promptDir - prompt目录路径
|
* @param {string} promptDir - prompt目录路径
|
||||||
* @param {RegistryData} registryData - 注册表数据
|
* @param {RegistryData} registryData - 注册表数据
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
async _scanDirectory(promptDir, registryData) {
|
async _scanDirectory(promptDir, registryData) {
|
||||||
try {
|
try {
|
||||||
// 统一扫描:扫描prompt下所有目录的所有资源类型文件
|
// 使用基类的统一文件模式扫描
|
||||||
const resourceTypes = ['role', 'execution', 'thought', 'knowledge', 'tool']
|
await this._scanResourcesByFilePattern(registryData)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`[PackageDiscovery] 扫描目录失败: ${error.message}`)
|
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<boolean>} 是否有效
|
|
||||||
* @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,38 +244,23 @@ class PackageDiscovery extends BaseDiscovery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 扫描prompt目录发现资源
|
* 扫描prompt目录发现资源(使用新的基类方法)
|
||||||
* @returns {Promise<Array>} 扫描发现的资源列表
|
* @returns {Promise<Array>} 扫描发现的资源列表
|
||||||
*/
|
*/
|
||||||
async _scanPromptDirectory() {
|
async _scanPromptDirectory() {
|
||||||
try {
|
try {
|
||||||
const packageRoot = await this._findPackageRoot()
|
// 使用新的基类扫描方法
|
||||||
const promptDir = path.join(packageRoot, 'prompt')
|
const registryData = RegistryData.createEmpty('package', null)
|
||||||
|
await this._scanResourcesByFilePattern(registryData)
|
||||||
if (!await fs.pathExists(promptDir)) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 转换为旧格式兼容性
|
||||||
const resources = []
|
const resources = []
|
||||||
|
for (const resource of registryData.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({
|
resources.push({
|
||||||
id: id,
|
id: resource.id,
|
||||||
reference: reference
|
reference: resource.reference
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return resources
|
return resources
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -537,15 +269,6 @@ class PackageDiscovery extends BaseDiscovery {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 文件扫描(可以被测试mock)
|
|
||||||
* @param {string} baseDir - 基础目录
|
|
||||||
* @param {string} resourceType - 资源类型
|
|
||||||
* @returns {Promise<Array>} 匹配的文件路径列表
|
|
||||||
*/
|
|
||||||
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对象(新架构方法)
|
* 获取RegistryData对象(新架构方法)
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
const BaseDiscovery = require('./BaseDiscovery')
|
const FilePatternDiscovery = require('./FilePatternDiscovery')
|
||||||
const logger = require('../../../utils/logger')
|
const logger = require('../../../utils/logger')
|
||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const CrossPlatformFileScanner = require('./CrossPlatformFileScanner')
|
|
||||||
const RegistryData = require('../RegistryData')
|
const RegistryData = require('../RegistryData')
|
||||||
const ResourceData = require('../ResourceData')
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ProjectDiscovery - 项目级资源发现器
|
* ProjectDiscovery - 项目级资源发现器
|
||||||
@ -16,10 +14,9 @@ const ResourceData = require('../ResourceData')
|
|||||||
*
|
*
|
||||||
* 优先级:2
|
* 优先级:2
|
||||||
*/
|
*/
|
||||||
class ProjectDiscovery extends BaseDiscovery {
|
class ProjectDiscovery extends FilePatternDiscovery {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('PROJECT', 2)
|
super('PROJECT', 2)
|
||||||
this.fileScanner = new CrossPlatformFileScanner()
|
|
||||||
this.registryData = null
|
this.registryData = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,43 +147,33 @@ class ProjectDiscovery extends BaseDiscovery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 扫描项目资源
|
* 实现基类要求的方法:获取项目扫描基础目录
|
||||||
|
* @returns {Promise<string>} 项目资源目录路径
|
||||||
|
*/
|
||||||
|
async _getBaseDirectory() {
|
||||||
|
const projectRoot = await this._findProjectRoot()
|
||||||
|
return path.join(projectRoot, '.promptx', 'resource')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描项目资源(使用新的基类方法)
|
||||||
* @param {string} projectRoot - 项目根目录
|
* @param {string} projectRoot - 项目根目录
|
||||||
* @returns {Promise<Array>} 扫描发现的资源列表
|
* @returns {Promise<Array>} 扫描发现的资源列表
|
||||||
*/
|
*/
|
||||||
async _scanProjectResources(projectRoot) {
|
async _scanProjectResources(projectRoot) {
|
||||||
try {
|
try {
|
||||||
const resourcesDir = path.join(projectRoot, '.promptx', 'resource')
|
// 使用新的基类扫描方法
|
||||||
|
const registryData = RegistryData.createEmpty('project', null)
|
||||||
|
await this._scanResourcesByFilePattern(registryData)
|
||||||
|
|
||||||
|
// 转换为旧格式兼容性
|
||||||
const resources = []
|
const resources = []
|
||||||
|
for (const resource of registryData.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({
|
resources.push({
|
||||||
id: id,
|
id: resource.id,
|
||||||
reference: reference
|
reference: resource.reference
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
logger.warn(`[ProjectDiscovery] Failed to scan ${resourceType} resources: ${error.message}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resources
|
return resources
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -196,17 +183,7 @@ class ProjectDiscovery extends BaseDiscovery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件扫描(可以被测试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 - 文件路径
|
* @param {string} filePath - 文件路径
|
||||||
* @returns {Promise<boolean>} 文件是否存在
|
* @returns {Promise<boolean>} 文件是否存在
|
||||||
*/
|
*/
|
||||||
@ -214,115 +191,6 @@ class ProjectDiscovery extends BaseDiscovery {
|
|||||||
return await fs.pathExists(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>')
|
|
||||||
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 - 项目根目录
|
* @param {string} projectRoot - 项目根目录
|
||||||
@ -347,17 +215,14 @@ class ProjectDiscovery extends BaseDiscovery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 扫描目录并添加资源到注册表
|
* 扫描目录并添加资源到注册表(使用新的基类方法)
|
||||||
* @param {string} resourcesDir - 资源目录
|
* @param {string} resourcesDir - 资源目录
|
||||||
* @param {RegistryData} registryData - 注册表数据
|
* @param {RegistryData} registryData - 注册表数据
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
async _scanDirectory(resourcesDir, registryData) {
|
async _scanDirectory(resourcesDir, registryData) {
|
||||||
// 扫描role目录
|
// 使用基类的统一文件模式扫描
|
||||||
const roleDir = path.join(resourcesDir, 'role')
|
await this._scanResourcesByFilePattern(registryData)
|
||||||
if (await this._fsExists(roleDir)) {
|
|
||||||
await this._scanRoleDirectory(roleDir, registryData)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -475,14 +340,22 @@ class ProjectDiscovery extends BaseDiscovery {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重写基类方法:获取注册表文件路径
|
||||||
|
* @returns {Promise<string>} 注册表文件路径
|
||||||
|
*/
|
||||||
|
async _getRegistryPath() {
|
||||||
|
const projectRoot = await this._findProjectRoot()
|
||||||
|
return path.join(projectRoot, '.promptx', 'resource', 'project.registry.json')
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取RegistryData对象(新架构方法)
|
* 获取RegistryData对象(新架构方法)
|
||||||
* @returns {Promise<RegistryData>} 项目级RegistryData对象
|
* @returns {Promise<RegistryData>} 项目级RegistryData对象
|
||||||
*/
|
*/
|
||||||
async getRegistryData() {
|
async getRegistryData() {
|
||||||
try {
|
try {
|
||||||
const projectRoot = await this._findProjectRoot()
|
const registryPath = await this._getRegistryPath()
|
||||||
const registryPath = path.join(projectRoot, '.promptx', 'resource', 'project.registry.json')
|
|
||||||
|
|
||||||
// 尝试加载现有的注册表文件
|
// 尝试加载现有的注册表文件
|
||||||
if (await this._fsExists(registryPath)) {
|
if (await this._fsExists(registryPath)) {
|
||||||
@ -499,11 +372,13 @@ class ProjectDiscovery extends BaseDiscovery {
|
|||||||
|
|
||||||
// 如果注册表无效,重新生成
|
// 如果注册表无效,重新生成
|
||||||
logger.info(`[ProjectDiscovery] 📋 项目注册表无效,重新生成`)
|
logger.info(`[ProjectDiscovery] 📋 项目注册表无效,重新生成`)
|
||||||
return await this.generateRegistry(projectRoot)
|
const baseDirectory = await this._getBaseDirectory()
|
||||||
|
return await this.generateRegistry(baseDirectory)
|
||||||
} else {
|
} else {
|
||||||
// 如果没有注册表文件,生成新的
|
// 如果没有注册表文件,生成新的
|
||||||
logger.info(`[ProjectDiscovery] 📋 项目注册表不存在,生成新注册表`)
|
logger.info(`[ProjectDiscovery] 📋 项目注册表不存在,生成新注册表`)
|
||||||
return await this.generateRegistry(projectRoot)
|
const baseDirectory = await this._getBaseDirectory()
|
||||||
|
return await this.generateRegistry(baseDirectory)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`[ProjectDiscovery] Failed to load RegistryData: ${error.message}`)
|
logger.warn(`[ProjectDiscovery] Failed to load RegistryData: ${error.message}`)
|
||||||
|
|||||||
Reference in New Issue
Block a user