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

@ -491,7 +491,9 @@ class MCPServerCommand {
return result;
},
'promptx_dacp': (args) => [args]
'promptx_dacp': (args) => [args],
'promptx_tool': (args) => [args]
};
const mapper = paramMapping[toolName];

View File

@ -0,0 +1,271 @@
const BasePouchCommand = require('../core/pouch/BasePouchCommand')
const { getGlobalResourceManager } = require('../core/resource')
const ToolExecutor = require('../tool/ToolExecutor')
const logger = require('../utils/logger')
/**
* PromptX Tool命令处理器
* 实现promptx_tool MCP工具执行通过@tool协议声明的工具
*/
class PromptXToolCommand extends BasePouchCommand {
constructor() {
super()
this.toolExecutor = new ToolExecutor()
this.resourceManager = null
}
/**
* 获取或初始化ResourceManager
*/
async getResourceManager() {
if (!this.resourceManager) {
this.resourceManager = getGlobalResourceManager()
// 确保ResourceManager已初始化
if (!this.resourceManager.initialized) {
await this.resourceManager.initializeWithNewArchitecture()
}
}
return this.resourceManager
}
// BasePouchCommand的抽象方法实现
getPurpose() {
return '执行通过@tool协议声明的JavaScript工具'
}
async getContent(args) {
try {
// 处理参数:如果是数组,取第一个元素;否则直接使用
const toolArgs = Array.isArray(args) ? args[0] : args
// 执行工具调用
const result = await this.executeToolInternal(toolArgs)
// 格式化响应
if (result.success) {
return `🔧 Tool执行成功
📋 工具资源: ${result.tool_resource}
📊 执行结果:
${JSON.stringify(result.result, null, 2)}
⏱️ 性能指标:
- 执行时间: ${result.metadata.execution_time_ms}ms
- 时间戳: ${result.metadata.timestamp}
- 版本: ${result.metadata.version}`
} else {
return `❌ Tool执行失败
📋 工具资源: ${result.tool_resource}
❌ 错误信息: ${result.error.message}
🏷️ 错误类型: ${result.error.type}
🔢 错误代码: ${result.error.code}
⏱️ 执行时间: ${result.metadata.execution_time_ms}ms`
}
} catch (error) {
return `❌ Tool执行异常
错误详情: ${error.message}
💡 请检查:
1. 工具资源引用格式是否正确 (@tool://tool-name)
2. 工具参数是否有效
3. 工具文件是否存在并可执行`
}
}
getPATEOAS(args) {
return {
currentState: 'tool_executed',
nextActions: [
{
action: 'execute_another_tool',
description: '执行其他工具',
method: 'promptx tool'
},
{
action: 'view_available_tools',
description: '查看可用工具',
method: 'promptx welcome'
}
]
}
}
/**
* 内部工具执行方法
* @param {Object} args - 命令参数
* @param {string} args.tool_resource - 工具资源引用,格式:@tool://tool-name
* @param {Object} args.parameters - 传递给工具的参数
* @param {Object} args.context - 执行上下文信息(可选)
* @returns {Promise<Object>} 执行结果
*/
async executeToolInternal(args) {
const startTime = Date.now()
try {
// 1. 参数验证
this.validateArguments(args)
const { tool_resource, parameters, context = {} } = args
logger.debug(`[PromptXTool] 开始执行工具: ${tool_resource}`)
// 2. 通过ResourceManager解析工具资源
const resourceManager = await this.getResourceManager()
const toolInfo = await resourceManager.loadResource(tool_resource)
// 3. 准备工具执行上下文
const executionContext = {
...context,
tool_resource,
timestamp: new Date().toISOString(),
execution_id: this.generateExecutionId()
}
// 4. 使用ToolExecutor执行工具
const result = await this.toolExecutor.execute(
toolInfo.content,
parameters,
executionContext
)
// 5. 格式化成功结果
return this.formatSuccessResult(result, tool_resource, startTime)
} catch (error) {
// 6. 格式化错误结果
logger.error(`[PromptXTool] 工具执行失败: ${error.message}`, error)
return this.formatErrorResult(error, args.tool_resource, startTime)
}
}
/**
* 验证命令参数
* @param {Object} args - 命令参数
*/
validateArguments(args) {
if (!args) {
throw new Error('Missing arguments')
}
if (!args.tool_resource) {
throw new Error('Missing required parameter: tool_resource')
}
if (!args.tool_resource.startsWith('@tool://')) {
throw new Error('Invalid tool_resource format. Must start with @tool://')
}
if (!args.parameters || typeof args.parameters !== 'object') {
throw new Error('Missing or invalid parameters. Must be an object')
}
}
/**
* 格式化成功结果
* @param {*} result - 工具执行结果
* @param {string} toolResource - 工具资源引用
* @param {number} startTime - 开始时间
* @returns {Object} 格式化的成功结果
*/
formatSuccessResult(result, toolResource, startTime) {
const duration = Date.now() - startTime
return {
success: true,
tool_resource: toolResource,
result: result,
metadata: {
execution_time_ms: duration,
timestamp: new Date().toISOString(),
version: '1.0.0'
}
}
}
/**
* 格式化错误结果
* @param {Error} error - 错误对象
* @param {string} toolResource - 工具资源引用(可能为空)
* @param {number} startTime - 开始时间
* @returns {Object} 格式化的错误结果
*/
formatErrorResult(error, toolResource, startTime) {
const duration = Date.now() - startTime
return {
success: false,
tool_resource: toolResource || 'unknown',
error: {
type: error.constructor.name,
message: error.message,
code: this.getErrorCode(error)
},
metadata: {
execution_time_ms: duration,
timestamp: new Date().toISOString(),
version: '1.0.0'
}
}
}
/**
* 根据错误类型获取错误代码
* @param {Error} error - 错误对象
* @returns {string} 错误代码
*/
getErrorCode(error) {
if (error.message.includes('not found')) {
return 'TOOL_NOT_FOUND'
}
if (error.message.includes('Invalid tool_resource format')) {
return 'INVALID_TOOL_RESOURCE'
}
if (error.message.includes('Missing')) {
return 'MISSING_PARAMETER'
}
if (error.message.includes('syntax')) {
return 'TOOL_SYNTAX_ERROR'
}
if (error.message.includes('timeout')) {
return 'EXECUTION_TIMEOUT'
}
return 'UNKNOWN_ERROR'
}
/**
* 生成执行ID
* @returns {string} 唯一的执行ID
*/
generateExecutionId() {
return `tool_exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}
/**
* 获取工具命令的元信息
* @returns {Object} 命令元信息
*/
getMetadata() {
return {
name: 'promptx_tool',
description: '执行通过@tool协议声明的工具',
version: '1.0.0',
author: 'PromptX Framework',
supports: {
protocols: ['@tool://'],
formats: ['.tool.js'],
features: [
'JavaScript工具执行',
'参数验证',
'错误处理',
'执行监控',
'上下文传递'
]
}
}
}
}
module.exports = PromptXToolCommand

View File

@ -31,7 +31,8 @@ class PouchCLI {
learn: commands.LearnCommand,
recall: commands.RecallCommand,
remember: commands.RememberCommand,
dacp: commands.DACPCommand
dacp: commands.DACPCommand,
tool: commands.PromptXToolCommand
})
// 将命令注册到状态机

View File

@ -9,6 +9,7 @@ const LearnCommand = require('./LearnCommand')
const RecallCommand = require('./RecallCommand')
const RememberCommand = require('./RememberCommand')
const DACPCommand = require('./DACPCommand')
const PromptXToolCommand = require('../../../commands/PromptXToolCommand')
module.exports = {
InitCommand,
@ -17,5 +18,6 @@ module.exports = {
LearnCommand,
RecallCommand,
RememberCommand,
DACPCommand
DACPCommand,
PromptXToolCommand
}

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 - 文件绝对路径

View File

@ -0,0 +1,116 @@
const ResourceProtocol = require('./ResourceProtocol');
/**
* Tool协议处理器
* 处理 @tool://tool-name 格式的资源引用
* 从注册表中查找并加载工具JavaScript代码
*/
class ToolProtocol extends ResourceProtocol {
constructor() {
super('tool');
this.registryManager = null;
}
/**
* 设置注册表管理器引用
* @param {Object} manager - ResourceManager实例
*/
setRegistryManager(manager) {
this.registryManager = manager;
}
/**
* 解析工具资源路径
* @param {string} toolPath - 工具名称,如 "calculator"
* @param {Object} queryParams - 查询参数(可选)
* @returns {Promise<Object>} 工具代码和元数据
*/
async resolve(toolPath, queryParams = {}) {
if (!this.registryManager) {
throw new Error('ToolProtocol: Registry manager not set');
}
// 1. 从注册表查找tool资源
const toolResource = this.registryManager.registryData
.findResourceById(toolPath, 'tool');
if (!toolResource) {
throw new Error(`Tool '${toolPath}' not found in registry`);
}
// 2. 加载tool文件内容
const toolContent = await this.registryManager
.loadResourceByProtocol(toolResource.reference);
// 3. 验证工具代码格式
this.validateToolContent(toolContent, toolPath);
// 4. 返回工具信息
return {
id: toolPath,
content: toolContent,
metadata: toolResource,
source: toolResource.source || 'unknown'
};
}
/**
* 验证工具内容格式
* @param {string} content - 工具文件内容
* @param {string} toolPath - 工具路径
*/
validateToolContent(content, toolPath) {
if (!content || typeof content !== 'string') {
throw new Error(`Tool '${toolPath}': Invalid or empty content`);
}
// 基本的JavaScript语法检查
try {
// 尝试创建一个函数来验证语法
new Function(content);
} catch (syntaxError) {
throw new Error(`Tool '${toolPath}': JavaScript syntax error - ${syntaxError.message}`);
}
}
/**
* 获取协议信息
* @returns {Object} 协议描述信息
*/
getProtocolInfo() {
return {
name: 'tool',
description: 'Tool资源协议 - 加载可执行的JavaScript工具',
syntax: 'tool://{tool_id}',
examples: [
'tool://calculator',
'tool://send-email',
'tool://data-processor',
'tool://api-client'
],
supportedFileTypes: ['.tool.js'],
usageNote: '工具文件必须导出符合PromptX Tool Interface的对象'
};
}
/**
* 检查缓存策略
* @param {string} toolPath - 工具路径
* @returns {boolean} 是否应该缓存
*/
shouldCache(toolPath) {
// 工具代码通常比较稳定,启用缓存以提高性能
return true;
}
/**
* 获取缓存键
* @param {string} toolPath - 工具路径
* @returns {string} 缓存键
*/
getCacheKey(toolPath) {
return `tool://${toolPath}`;
}
}
module.exports = ToolProtocol;

View File

@ -11,6 +11,7 @@ const RoleProtocol = require('./protocols/RoleProtocol')
const ThoughtProtocol = require('./protocols/ThoughtProtocol')
const ExecutionProtocol = require('./protocols/ExecutionProtocol')
const KnowledgeProtocol = require('./protocols/KnowledgeProtocol')
const ToolProtocol = require('./protocols/ToolProtocol')
const UserProtocol = require('./protocols/UserProtocol')
const FileProtocol = require('./protocols/FileProtocol')
@ -46,6 +47,7 @@ class ResourceManager {
this.protocols.set('thought', new ThoughtProtocol())
this.protocols.set('execution', new ExecutionProtocol())
this.protocols.set('knowledge', new KnowledgeProtocol())
this.protocols.set('tool', new ToolProtocol())
}
/**
@ -110,6 +112,7 @@ class ResourceManager {
const executionProtocol = this.protocols.get('execution')
const thoughtProtocol = this.protocols.get('thought')
const knowledgeProtocol = this.protocols.get('knowledge')
const toolProtocol = this.protocols.get('tool')
if (roleProtocol) {
roleProtocol.setRegistryManager(this)
@ -123,6 +126,9 @@ class ResourceManager {
if (knowledgeProtocol) {
knowledgeProtocol.setRegistryManager(this)
}
if (toolProtocol) {
toolProtocol.setRegistryManager(this)
}
// 逻辑协议设置完成,不输出日志避免干扰用户界面
}

View File

@ -150,6 +150,50 @@ const TOOL_DEFINITIONS = [
context: z.object({}).optional().describe('上下文信息')
})
})
},
{
name: 'promptx_tool',
description: '🔧 [工具执行器] 执行通过@tool协议声明的JavaScript工具 - 支持角色配置中定义的专业工具能力,如@tool://calculator数学计算、@tool://send-email邮件发送等。提供安全沙箱执行、参数验证、错误处理和性能监控。',
inputSchema: {
type: 'object',
properties: {
tool_resource: {
type: 'string',
description: '工具资源引用,格式:@tool://tool-name如@tool://calculator',
pattern: '^@tool://.+'
},
parameters: {
type: 'object',
description: '传递给工具的参数对象'
},
context: {
type: 'object',
description: '执行上下文信息(可选)',
properties: {
role_id: {
type: 'string',
description: '当前激活的角色ID'
},
session_id: {
type: 'string',
description: '会话ID'
}
}
}
},
required: ['tool_resource', 'parameters']
},
zodSchema: z.object({
tool_resource: z.string()
.regex(/^@tool:\/\/.+/, '工具资源必须以@tool://开头')
.describe('工具资源引用,格式:@tool://tool-name'),
parameters: z.object({}).passthrough()
.describe('传递给工具的参数对象'),
context: z.object({
role_id: z.string().optional().describe('当前激活的角色ID'),
session_id: z.string().optional().describe('会话ID')
}).optional().describe('执行上下文信息')
})
}
];

View File

@ -95,15 +95,29 @@ class ToolExecutor {
script.runInContext(context);
// 获取导出的工具
const ToolClass = context.module.exports;
// 获取导出的工具
const exported = context.module.exports;
if (!ToolClass || typeof ToolClass !== 'function') {
throw new Error(`工具未正确导出: ${toolName}`);
if (!exported) {
throw new Error(`工具未正确导出: ${toolName}`);
}
// 创建工具实例
return new ToolClass();
// 支持两种导出方式:
// 1. 导出类(构造函数)- 需要实例化
// 2. 导出对象 - 直接使用
let toolInstance;
if (typeof exported === 'function') {
// 导出的是类,需要实例化
toolInstance = new exported();
} else if (typeof exported === 'object') {
// 导出的是对象,直接使用
toolInstance = exported;
} else {
throw new Error(`工具导出格式不正确,必须是类或对象: ${toolName}`);
}
return toolInstance;
} catch (error) {
throw new Error(`工具代码执行失败 ${toolName}: ${error.message}`);
@ -185,7 +199,18 @@ class ToolExecutor {
validateParameters(tool, parameters) {
// 如果工具有自定义validate方法使用它
if (typeof tool.validate === 'function') {
return tool.validate(parameters);
const result = tool.validate(parameters);
// 支持两种返回格式:
// 1. boolean - 转换为标准格式
// 2. {valid: boolean, errors?: array} - 标准格式
if (typeof result === 'boolean') {
return { valid: result, errors: result ? [] : ['Validation failed'] };
} else if (result && typeof result === 'object') {
return result;
} else {
return { valid: false, errors: ['Invalid validation result'] };
}
}
// 否则使用默认验证