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

@ -0,0 +1,139 @@
/**
* @tool calculator
* @description 数学计算工具
* @version 1.0.0
*/
module.exports = {
/**
* 获取工具元信息
* @returns {Object} 工具元信息
*/
getMetadata() {
return {
name: 'calculator',
description: '提供基础数学计算功能,支持加减乘除运算',
version: '1.0.0',
author: 'PromptX Framework',
category: 'math',
tags: ['calculator', 'math', 'arithmetic']
}
},
/**
* 获取参数Schema
* @returns {Object} JSON Schema定义
*/
getSchema() {
return {
type: 'object',
properties: {
operation: {
type: 'string',
enum: ['add', 'subtract', 'multiply', 'divide'],
description: '数学运算类型'
},
a: {
type: 'number',
description: '第一个操作数'
},
b: {
type: 'number',
description: '第二个操作数'
}
},
required: ['operation', 'a', 'b'],
additionalProperties: false
}
},
/**
* 验证参数
* @param {Object} params - 输入参数
* @returns {boolean} 验证结果
*/
validate(params) {
const { operation, a, b } = params
// 检查必需参数
if (!operation || typeof a !== 'number' || typeof b !== 'number') {
return false
}
// 检查操作类型
const validOperations = ['add', 'subtract', 'multiply', 'divide']
if (!validOperations.includes(operation)) {
return false
}
// 检查除零
if (operation === 'divide' && b === 0) {
return false
}
return true
},
/**
* 执行计算
* @param {Object} params - 计算参数
* @param {string} params.operation - 运算类型 ('add', 'subtract', 'multiply', 'divide')
* @param {number} params.a - 第一个操作数
* @param {number} params.b - 第二个操作数
* @returns {Object} 计算结果
*/
async execute(params) {
const { operation, a, b } = params
let result
let expression
switch (operation) {
case 'add':
result = a + b
expression = `${a} + ${b}`
break
case 'subtract':
result = a - b
expression = `${a} - ${b}`
break
case 'multiply':
result = a * b
expression = `${a} × ${b}`
break
case 'divide':
if (b === 0) {
throw new Error('Division by zero is not allowed')
}
result = a / b
expression = `${a} ÷ ${b}`
break
default:
throw new Error(`Unknown operation: ${operation}`)
}
return {
expression,
result,
operation,
operands: { a, b },
formatted: `${expression} = ${result}`
}
},
/**
* 工具初始化(可选)
*/
async init() {
// 可以在这里进行工具初始化工作
return true
},
/**
* 工具清理(可选)
*/
async cleanup() {
// 可以在这里进行清理工作
return true
}
}

View File

@ -0,0 +1,202 @@
/**
* @tool send-email
* @description 邮件发送工具(演示版)
* @version 1.0.0
*/
module.exports = {
/**
* 获取工具元信息
* @returns {Object} 工具元信息
*/
getMetadata() {
return {
name: 'send-email',
description: '邮件发送工具,支持发送格式化邮件(演示版本)',
version: '1.0.0',
author: 'PromptX Framework',
category: 'communication',
tags: ['email', 'communication', 'notification'],
demo: true
}
},
/**
* 获取参数Schema
* @returns {Object} JSON Schema定义
*/
getSchema() {
return {
type: 'object',
properties: {
to: {
type: 'string',
format: 'email',
description: '收件人邮箱地址'
},
subject: {
type: 'string',
minLength: 1,
description: '邮件主题'
},
content: {
type: 'string',
minLength: 1,
description: '邮件内容'
},
cc: {
type: 'array',
items: {
type: 'string',
format: 'email'
},
description: '抄送邮箱列表(可选)'
},
priority: {
type: 'string',
enum: ['low', 'normal', 'high'],
default: 'normal',
description: '邮件优先级'
}
},
required: ['to', 'subject', 'content'],
additionalProperties: false
}
},
/**
* 验证参数
* @param {Object} params - 输入参数
* @returns {boolean} 验证结果
*/
validate(params) {
const { to, subject, content } = params
// 检查必需参数
if (!to || !subject || !content) {
return false
}
// 简单的邮箱格式验证
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(to)) {
return false
}
// 检查抄送邮箱格式(如果存在)
if (params.cc && Array.isArray(params.cc)) {
for (const ccEmail of params.cc) {
if (!emailRegex.test(ccEmail)) {
return false
}
}
}
return true
},
/**
* 发送邮件(演示版本)
* @param {Object} params - 邮件参数
* @param {string} params.to - 收件人邮箱
* @param {string} params.subject - 邮件主题
* @param {string} params.content - 邮件内容
* @param {string[]} params.cc - 抄送邮箱列表(可选)
* @param {string} params.priority - 邮件优先级(可选)
* @returns {Object} 发送结果
*/
async execute(params) {
const { to, subject, content, cc = [], priority = 'normal' } = params
// 生成邮件ID演示用
const emailId = `email_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
// 格式化邮件内容
const formattedEmail = this.formatEmail({
to,
subject,
content,
cc,
priority,
timestamp: new Date().toISOString()
})
// 模拟发送延迟
await this.simulateDelay(500)
// 演示版本:不实际发送邮件,只返回格式化结果
return {
success: true,
email_id: emailId,
to,
subject,
content_preview: content.slice(0, 100) + (content.length > 100 ? '...' : ''),
cc_count: cc.length,
priority,
timestamp: new Date().toISOString(),
formatted_email: formattedEmail,
demo_note: '这是演示版本,邮件未实际发送',
status: 'demo_sent'
}
},
/**
* 格式化邮件内容
* @param {Object} emailData - 邮件数据
* @returns {string} 格式化的邮件
*/
formatEmail(emailData) {
const { to, subject, content, cc, priority, timestamp } = emailData
let formatted = `收件人: ${to}\n`
if (cc.length > 0) {
formatted += `抄送: ${cc.join(', ')}\n`
}
formatted += `主题: ${subject}\n`
formatted += `优先级: ${this.getPriorityText(priority)}\n`
formatted += `时间: ${new Date(timestamp).toLocaleString('zh-CN')}\n`
formatted += `\n内容:\n${content}\n`
return formatted
},
/**
* 获取优先级文本
* @param {string} priority - 优先级
* @returns {string} 优先级文本
*/
getPriorityText(priority) {
const priorityMap = {
low: '低',
normal: '普通',
high: '高'
}
return priorityMap[priority] || '普通'
},
/**
* 模拟网络延迟
* @param {number} ms - 延迟毫秒数
*/
async simulateDelay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
},
/**
* 工具初始化
*/
async init() {
// 在实际版本中,这里可以初始化邮件服务连接
return true
},
/**
* 工具清理
*/
async cleanup() {
// 在实际版本中,这里可以关闭邮件服务连接
return true
}
}

View File

@ -96,6 +96,44 @@ program
}
})
// Tool命令
program
.command('tool <arguments>')
.description('🔧 tool锦囊 - 执行通过@tool协议声明的JavaScript工具')
.action(async (argumentsJson, options) => {
try {
let args = {};
// 支持两种调用方式:
// 1. 从MCP传来的对象通过cli.execute调用
// 2. 从CLI传来的JSON字符串直接命令行调用
if (typeof argumentsJson === 'object') {
args = argumentsJson;
} else if (typeof argumentsJson === 'string') {
try {
args = JSON.parse(argumentsJson);
} catch (error) {
console.error('❌ 参数解析错误请提供有效的JSON格式');
console.error('格式示例: \'{"tool_resource": "@tool://calculator", "parameters": {"operation": "add", "a": 25, "b": 37}}\'');
process.exit(1);
}
}
// 验证必需参数
if (!args.tool_resource || !args.parameters) {
console.error('❌ 缺少必需参数');
console.error('必需参数: tool_resource (工具资源引用), parameters (工具参数)');
console.error('格式示例: \'{"tool_resource": "@tool://calculator", "parameters": {"operation": "add", "a": 25, "b": 37}}\'');
process.exit(1);
}
await cli.execute('tool', args);
} catch (error) {
console.error(`❌ Tool命令执行失败: ${error.message}`);
process.exit(1);
}
})
// MCP Server命令
program
.command('mcp-server')

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'] };
}
}
// 否则使用默认验证

File diff suppressed because it is too large Load Diff

31
test-tool-validation.js Normal file
View File

@ -0,0 +1,31 @@
const fs = require('fs');
const path = require('path');
// 测试工具文件验证
function testToolValidation() {
const toolPath = path.join(process.cwd(), 'prompt/tool/calculator.tool.js');
console.log(`🔍 Testing tool validation for: ${toolPath}`);
try {
const content = fs.readFileSync(toolPath, 'utf8');
console.log('📄 File content loaded, length:', content.length);
// 模拟我们的验证逻辑
console.log('✅ Contains module.exports:', content.includes('module.exports'));
console.log('✅ Contains getMetadata:', content.includes('getMetadata'));
console.log('✅ Contains execute:', content.includes('execute'));
// 尝试语法检查
try {
new Function(content);
console.log('✅ JavaScript syntax is valid');
} catch (syntaxError) {
console.log('❌ JavaScript syntax error:', syntaxError.message);
}
} catch (error) {
console.log('❌ Failed to read file:', error.message);
}
}
testToolValidation();

19
test-tool.js Normal file
View File

@ -0,0 +1,19 @@
const { getGlobalResourceManager } = require('./src/lib/core/resource');
async function testToolResource() {
try {
const rm = getGlobalResourceManager();
if (!rm.initialized) {
await rm.initializeWithNewArchitecture();
}
console.log('🔍 Testing tool resource loading...');
const result = await rm.loadResource('@tool://calculator');
console.log('✅ Tool resource loaded successfully:', result);
} catch (error) {
console.log('❌ Tool resource loading failed:', error.message);
console.log('Error details:', error);
}
}
testToolResource();