feat: 实现@tool协议完整功能 - JavaScript工具执行框架

 核心功能
- 新增ToolProtocol处理器,支持@tool://协议解析
- 实现PromptXToolCommand,统一MCP/CLI工具调用
- 完善ToolExecutor,支持工具实例化和参数验证
- 新增calculator和send-email示例工具

🔧 技术改进
- 优化PackageDiscovery统一资源扫描逻辑
- 增强CrossPlatformFileScanner支持.tool.js文件
- 完善ResourceManager集成ToolProtocol
- 更新MCP工具定义支持promptx_tool

📋 详细变更
Core:
- src/lib/core/resource/protocols/ToolProtocol.js: 新增工具协议处理器
- src/lib/commands/PromptXToolCommand.js: 新增工具命令处理器
- src/lib/tool/ToolExecutor.js: 增强工具执行器兼容性

Discovery:
- src/lib/core/resource/discovery/PackageDiscovery.js: 统一资源扫描
- src/lib/core/resource/discovery/CrossPlatformFileScanner.js: 支持tool文件
- src/lib/core/resource/discovery/ProjectDiscovery.js: 增加tool验证

Integration:
- src/lib/core/resource/resourceManager.js: 集成ToolProtocol
- src/lib/mcp/toolDefinitions.js: 新增promptx_tool定义
- src/lib/commands/MCPServerCommand.js: 支持tool参数转换
- src/bin/promptx.js: 新增tool命令行支持

Tools:
- prompt/tool/calculator.tool.js: 数学计算工具示例
- prompt/tool/send-email.tool.js: 邮件发送工具示例

Registry:
- src/package.registry.json: 自动生成包含2个tool资源

🧪 测试验证
-  @tool://calculator 数学计算: 25 + 37 = 62
-  @tool://send-email 邮件发送演示版本
-  CLI和MCP双模式支持
-  完整的错误处理和执行元数据
-  资源自动发现和注册

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sean
2025-06-28 14:15:24 +08:00
parent 70093018f8
commit 40e0c01c59
17 changed files with 1725 additions and 646 deletions

View File

@ -57,6 +57,10 @@ class CrossPlatformFileScanner {
knowledge: {
extensions: ['.knowledge.md'],
subdirs: null // 不限制子目录在所有地方查找knowledge文件
},
tool: {
extensions: ['.tool.js'],
subdirs: null // 不限制子目录在所有地方查找tool文件
}
}

View File

@ -193,16 +193,15 @@ class PackageDiscovery extends BaseDiscovery {
*/
async _scanDirectory(promptDir, registryData) {
try {
// 扫描domain目录下的角色
const domainDir = path.join(promptDir, 'domain')
if (await fs.pathExists(domainDir)) {
await this._scanDomainDirectory(domainDir, registryData)
}
// 统一扫描扫描prompt下所有目录的所有资源类型文件
const resourceTypes = ['role', 'execution', 'thought', 'knowledge', 'tool']
// 扫描core目录下的资源
const coreDir = path.join(promptDir, 'core')
if (await fs.pathExists(coreDir)) {
await this._scanCoreDirectory(coreDir, 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) {
@ -210,6 +209,88 @@ class PackageDiscovery extends BaseDiscovery {
}
}
/**
* 处理单个资源文件
* @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
}
}
/**
* 扫描domain目录角色资源
* @param {string} domainDir - domain目录路径
@ -431,7 +512,7 @@ class PackageDiscovery extends BaseDiscovery {
const resources = []
// 定义要扫描的资源类型
const resourceTypes = ['role', 'execution', 'thought', 'knowledge']
const resourceTypes = ['role', 'execution', 'thought', 'knowledge', 'tool']
// 并行扫描所有资源类型
for (const resourceType of resourceTypes) {

View File

@ -160,7 +160,7 @@ class ProjectDiscovery extends BaseDiscovery {
const resources = []
// 定义要扫描的资源类型
const resourceTypes = ['role', 'execution', 'thought', 'knowledge']
const resourceTypes = ['role', 'execution', 'thought', 'knowledge', 'tool']
// 并行扫描所有资源类型
for (const resourceType of resourceTypes) {
@ -254,6 +254,9 @@ class ProjectDiscovery extends BaseDiscovery {
// knowledge类型比较灵活只要文件有内容就认为是有效的
// 可以是纯文本、链接、图片等任何形式的知识内容
return true
case 'tool':
// tool类型必须是有效的JavaScript代码
return this._validateToolFile(content)
default:
return false
}
@ -263,6 +266,34 @@ class ProjectDiscovery extends BaseDiscovery {
}
}
/**
* 验证Tool文件是否为有效的JavaScript代码
* @param {string} content - 文件内容
* @returns {boolean} 是否为有效的Tool文件
*/
_validateToolFile(content) {
try {
// 1. 基本的JavaScript语法检查
new Function(content);
// 2. 检查是否包含module.exportsCommonJS格式
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 - 文件绝对路径