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:
@ -57,6 +57,10 @@ class CrossPlatformFileScanner {
|
||||
knowledge: {
|
||||
extensions: ['.knowledge.md'],
|
||||
subdirs: null // 不限制子目录,在所有地方查找knowledge文件
|
||||
},
|
||||
tool: {
|
||||
extensions: ['.tool.js'],
|
||||
subdirs: null // 不限制子目录,在所有地方查找tool文件
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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.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 - 文件绝对路径
|
||||
|
||||
116
src/lib/core/resource/protocols/ToolProtocol.js
Normal file
116
src/lib/core/resource/protocols/ToolProtocol.js
Normal 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;
|
||||
@ -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)
|
||||
}
|
||||
|
||||
// 逻辑协议设置完成,不输出日志避免干扰用户界面
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user