## 🎯 解决的问题 - 修复工具代码在分析阶段因require失败导致getDependencies()无法调用的问题 - 工具的第一行require('axios')失败时,整个脚本执行中断,module.exports未被设置 ## 🔧 实现方案 - 新增createAnalysisRequire()方法,为分析阶段提供mock require - 内置模块(path、fs等)使用真实require,第三方模块返回Proxy mock对象 - 执行阶段继续使用createSmartRequire()进行真实依赖解析 ## ✅ 修复效果 - ✅ 分析阶段:axios/cheerio被正确mock,脚本完整执行 - ✅ 依赖提取:getDependencies()正常调用,返回['axios@^1.6.0', 'cheerio@^1.0.0-rc.12'] - ✅ 工具实例化:module.exports正确设置,工具对象创建成功 ## 🧪 测试验证 - ✅ heywhale-activity-scraper工具依赖分析成功 - ✅ mock require日志正常输出 - ✅ 后续依赖安装准备完成 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
644 lines
18 KiB
JavaScript
644 lines
18 KiB
JavaScript
const path = require('path');
|
||
const fs = require('fs').promises;
|
||
const { spawn } = require('child_process');
|
||
const vm = require('vm');
|
||
|
||
/**
|
||
* ToolSandbox - 工具沙箱环境管理器
|
||
*
|
||
* 基于现有协议系统的工具执行环境,支持:
|
||
* - @tool:// 协议定位工具
|
||
* - @user://.promptx/toolbox 沙箱隔离
|
||
* - 自动依赖管理
|
||
* - 可复用的执行环境
|
||
*/
|
||
class ToolSandbox {
|
||
constructor(toolReference, options = {}) {
|
||
this.toolReference = toolReference; // @tool://url-validator
|
||
this.resourceManager = null; // ResourceManager实例
|
||
this.toolId = null; // 工具ID,如 url-validator
|
||
this.toolContent = null; // 工具文件内容
|
||
this.toolInstance = null; // 工具实例
|
||
this.dependencies = []; // 依赖列表
|
||
this.sandboxPath = null; // 沙箱目录路径
|
||
this.sandboxContext = null; // VM沙箱上下文
|
||
|
||
// 状态标志
|
||
this.isAnalyzed = false;
|
||
this.isPrepared = false;
|
||
|
||
// 配置选项
|
||
this.options = {
|
||
timeout: 30000,
|
||
enableDependencyInstall: true,
|
||
forceReinstall: false,
|
||
...options
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 设置ResourceManager实例
|
||
* @param {ResourceManager} resourceManager
|
||
*/
|
||
setResourceManager(resourceManager) {
|
||
this.resourceManager = resourceManager;
|
||
}
|
||
|
||
/**
|
||
* 分析工具:加载工具内容,提取元信息和依赖
|
||
* @returns {Promise<Object>} 分析结果
|
||
*/
|
||
async analyze() {
|
||
if (this.isAnalyzed) {
|
||
return this.getAnalysisResult();
|
||
}
|
||
|
||
if (!this.resourceManager) {
|
||
throw new Error('ResourceManager not set. Call setResourceManager() first.');
|
||
}
|
||
|
||
try {
|
||
// 1. 解析工具引用,提取工具ID
|
||
this.toolId = this.extractToolId(this.toolReference);
|
||
|
||
// 2. 通过协议系统加载工具
|
||
const toolResult = await this.resourceManager.loadResource(this.toolReference);
|
||
if (!toolResult.success) {
|
||
// 调试:尝试不同的查找方式
|
||
console.log(`🔍 调试:尝试查找工具 ${this.toolReference}`);
|
||
const directLookup = this.resourceManager.registryData.findResourceById(`tool:${this.toolId}`, 'tool');
|
||
console.log(` - 直接查找 tool:${this.toolId}: ${directLookup ? '找到' : '未找到'}`);
|
||
|
||
throw new Error(`Failed to load tool: ${toolResult.error.message}`);
|
||
}
|
||
|
||
this.toolContent = toolResult.content;
|
||
|
||
// 3. 设置沙箱路径
|
||
this.sandboxPath = await this.resolveSandboxPath();
|
||
|
||
// 4. 在基础沙箱中分析工具
|
||
await this.analyzeToolInSandbox();
|
||
|
||
this.isAnalyzed = true;
|
||
return this.getAnalysisResult();
|
||
|
||
} catch (error) {
|
||
throw new Error(`Tool analysis failed: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 准备依赖:安装依赖,准备执行环境
|
||
* @returns {Promise<Object>} 准备结果
|
||
*/
|
||
async prepareDependencies() {
|
||
if (!this.isAnalyzed) {
|
||
await this.analyze();
|
||
}
|
||
|
||
if (this.isPrepared && !this.options.forceReinstall) {
|
||
return { success: true, message: 'Dependencies already prepared' };
|
||
}
|
||
|
||
try {
|
||
// 1. 确保沙箱目录存在
|
||
await this.ensureSandboxDirectory();
|
||
|
||
// 2. 如果有依赖,安装它们
|
||
if (this.dependencies.length > 0) {
|
||
await this.installDependencies();
|
||
}
|
||
|
||
// 3. 创建执行沙箱环境
|
||
await this.createExecutionSandbox();
|
||
|
||
this.isPrepared = true;
|
||
return {
|
||
success: true,
|
||
sandboxPath: this.sandboxPath,
|
||
dependencies: this.dependencies
|
||
};
|
||
|
||
} catch (error) {
|
||
throw new Error(`Dependency preparation failed: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 执行工具
|
||
* @param {Object} parameters - 工具参数
|
||
* @returns {Promise<Object>} 执行结果
|
||
*/
|
||
async execute(parameters = {}) {
|
||
if (!this.isPrepared) {
|
||
await this.prepareDependencies();
|
||
}
|
||
|
||
try {
|
||
// 1. 参数验证
|
||
await this.validateParameters(parameters);
|
||
|
||
// 2. 在沙箱中执行工具
|
||
const result = await this.executeInSandbox(parameters);
|
||
|
||
return {
|
||
success: true,
|
||
data: result,
|
||
metadata: {
|
||
toolId: this.toolId,
|
||
sandboxPath: this.sandboxPath,
|
||
executionTime: Date.now()
|
||
}
|
||
};
|
||
|
||
} catch (error) {
|
||
return {
|
||
success: false,
|
||
error: {
|
||
message: error.message,
|
||
stack: error.stack
|
||
},
|
||
metadata: {
|
||
toolId: this.toolId,
|
||
sandboxPath: this.sandboxPath
|
||
}
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 提取工具ID
|
||
* @param {string} toolReference - @tool://url-validator
|
||
* @returns {string} 工具ID
|
||
*/
|
||
extractToolId(toolReference) {
|
||
const match = toolReference.match(/^@tool:\/\/(.+)$/);
|
||
if (!match) {
|
||
throw new Error(`Invalid tool reference format: ${toolReference}`);
|
||
}
|
||
return match[1];
|
||
}
|
||
|
||
/**
|
||
* 解析沙箱路径
|
||
* @returns {Promise<string>} 沙箱绝对路径
|
||
*/
|
||
async resolveSandboxPath() {
|
||
// 使用 @user://.promptx/toolbox/{toolId} 作为沙箱路径
|
||
const userDataReference = `@user://.promptx/toolbox/${this.toolId}`;
|
||
const result = await this.resourceManager.resolveProtocolReference(userDataReference);
|
||
|
||
if (!result.success) {
|
||
throw new Error(`Failed to resolve sandbox path: ${result.error}`);
|
||
}
|
||
|
||
// 通过UserProtocol解析实际路径
|
||
const userProtocol = this.resourceManager.protocols.get('user');
|
||
const sandboxPath = await userProtocol.resolvePath(
|
||
`.promptx/toolbox/${this.toolId}`,
|
||
new Map()
|
||
);
|
||
|
||
return sandboxPath;
|
||
}
|
||
|
||
/**
|
||
* 在基础沙箱中分析工具
|
||
*/
|
||
async analyzeToolInSandbox() {
|
||
const sandbox = this.createSandbox({
|
||
supportDependencies: false,
|
||
sandboxPath: process.cwd()
|
||
});
|
||
const script = new vm.Script(this.toolContent, { filename: `${this.toolId}.js` });
|
||
const context = vm.createContext(sandbox);
|
||
|
||
try {
|
||
script.runInContext(context);
|
||
} catch (error) {
|
||
// 使用智能错误过滤处理require错误
|
||
const filteredError = this._filterRequireError(error);
|
||
if (filteredError) {
|
||
throw filteredError;
|
||
}
|
||
// 如果是预期的require错误,继续执行
|
||
}
|
||
|
||
const exported = context.module.exports;
|
||
|
||
if (!exported) {
|
||
throw new Error(`Tool does not export anything: ${this.toolId}`);
|
||
}
|
||
|
||
// 创建工具实例
|
||
let toolInstance;
|
||
if (typeof exported === 'function') {
|
||
toolInstance = new exported();
|
||
} else if (typeof exported === 'object') {
|
||
toolInstance = exported;
|
||
} else {
|
||
throw new Error(`Invalid tool export format: ${this.toolId}`);
|
||
}
|
||
|
||
// 提取依赖
|
||
if (typeof toolInstance.getDependencies === 'function') {
|
||
try {
|
||
this.dependencies = toolInstance.getDependencies() || [];
|
||
} catch (error) {
|
||
console.warn(`[ToolSandbox] Failed to get dependencies for ${this.toolId}: ${error.message}`);
|
||
this.dependencies = [];
|
||
}
|
||
}
|
||
|
||
this.toolInstance = toolInstance;
|
||
}
|
||
|
||
/**
|
||
* 智能过滤require错误
|
||
* @param {Error} error - 捕获的错误
|
||
* @returns {Error|null} - 如果是真正的错误则返回Error对象,如果是预期的require错误则返回null
|
||
* @private
|
||
*/
|
||
_filterRequireError(error) {
|
||
// 检查是否是MODULE_NOT_FOUND错误
|
||
if (error.code === 'MODULE_NOT_FOUND') {
|
||
const missingModule = this._extractMissingModuleName(error.message);
|
||
|
||
if (missingModule) {
|
||
// 获取已声明的依赖列表
|
||
const declaredDependencies = this._extractDeclaredDependencies();
|
||
|
||
// 检查缺失的模块是否在依赖声明中
|
||
if (this._isDeclaredInDependencies(missingModule, declaredDependencies)) {
|
||
console.log(`[ToolSandbox] 依赖 ${missingModule} 未安装,将在prepareDependencies阶段安装`);
|
||
return null; // 预期的错误,忽略
|
||
} else {
|
||
return new Error(`未声明的依赖: ${missingModule},请在getDependencies()中添加此依赖`);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 其他错误直接返回
|
||
return error;
|
||
}
|
||
|
||
/**
|
||
* 从错误信息中提取缺失的模块名
|
||
* @param {string} errorMessage - 错误信息
|
||
* @returns {string|null} - 模块名或null
|
||
* @private
|
||
*/
|
||
_extractMissingModuleName(errorMessage) {
|
||
// 匹配 "Cannot find module 'moduleName'" 或 "Cannot resolve module 'moduleName'"
|
||
const match = errorMessage.match(/Cannot (?:find|resolve) module ['"]([^'"]+)['"]/);
|
||
return match ? match[1] : null;
|
||
}
|
||
|
||
/**
|
||
* 尝试从工具代码中提取已声明的依赖
|
||
* @returns {string[]} - 依赖列表
|
||
* @private
|
||
*/
|
||
_extractDeclaredDependencies() {
|
||
try {
|
||
// 尝试通过正则表达式从代码中提取getDependencies的返回值
|
||
const dependencyMatch = this.toolContent.match(/getDependencies\s*\(\s*\)\s*\{[\s\S]*?return\s*\[([\s\S]*?)\]/);
|
||
|
||
if (dependencyMatch) {
|
||
const dependencyString = dependencyMatch[1];
|
||
// 提取字符串字面量
|
||
const stringMatches = dependencyString.match(/['"]([^'"]+)['"]/g);
|
||
if (stringMatches) {
|
||
return stringMatches.map(str => str.slice(1, -1)); // 去掉引号
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.warn(`[ToolSandbox] 无法解析依赖声明: ${error.message}`);
|
||
}
|
||
|
||
return [];
|
||
}
|
||
|
||
/**
|
||
* 检查模块是否在依赖声明中
|
||
* @param {string} moduleName - 模块名
|
||
* @param {string[]} declaredDependencies - 已声明的依赖列表
|
||
* @returns {boolean} - 是否已声明
|
||
* @private
|
||
*/
|
||
_isDeclaredInDependencies(moduleName, declaredDependencies) {
|
||
return declaredDependencies.some(dep => {
|
||
// 支持 "axios@^1.6.0" 格式,提取模块名部分
|
||
const depName = dep.split('@')[0];
|
||
return depName === moduleName;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 确保沙箱目录存在
|
||
*/
|
||
async ensureSandboxDirectory() {
|
||
try {
|
||
await fs.access(this.sandboxPath);
|
||
} catch (error) {
|
||
if (error.code === 'ENOENT') {
|
||
await fs.mkdir(this.sandboxPath, { recursive: true });
|
||
} else {
|
||
throw error;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 安装依赖
|
||
*/
|
||
async installDependencies() {
|
||
if (this.dependencies.length === 0) {
|
||
return;
|
||
}
|
||
|
||
// 1. 创建package.json
|
||
await this.createPackageJson();
|
||
|
||
// 2. 使用内置pnpm安装依赖
|
||
await this.runPnpmInstall();
|
||
}
|
||
|
||
/**
|
||
* 创建package.json
|
||
*/
|
||
async createPackageJson() {
|
||
const packageJsonPath = path.join(this.sandboxPath, 'package.json');
|
||
|
||
// 检查是否已存在且不强制重装
|
||
if (!this.options.forceReinstall) {
|
||
try {
|
||
await fs.access(packageJsonPath);
|
||
return; // 已存在,跳过
|
||
} catch (error) {
|
||
// 不存在,继续创建
|
||
}
|
||
}
|
||
|
||
const packageJson = {
|
||
name: `toolbox-${this.toolId}`,
|
||
version: '1.0.0',
|
||
description: `Sandbox for tool: ${this.toolId}`,
|
||
private: true,
|
||
dependencies: {}
|
||
};
|
||
|
||
// 解析依赖格式 ["validator@^13.11.0", "lodash"]
|
||
for (const dep of this.dependencies) {
|
||
if (dep.includes('@')) {
|
||
const [name, version] = dep.split('@');
|
||
packageJson.dependencies[name] = version;
|
||
} else {
|
||
packageJson.dependencies[dep] = 'latest';
|
||
}
|
||
}
|
||
|
||
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
||
}
|
||
|
||
/**
|
||
* 运行pnpm安装
|
||
*/
|
||
async runPnpmInstall() {
|
||
return new Promise((resolve, reject) => {
|
||
// 获取内置pnpm路径 - 直接从node_modules获取
|
||
const pnpmModulePath = require.resolve('pnpm');
|
||
const pnpmBinPath = path.join(path.dirname(pnpmModulePath), 'bin', 'pnpm.cjs');
|
||
|
||
const pnpm = spawn('node', [pnpmBinPath, 'install'], {
|
||
cwd: this.sandboxPath,
|
||
stdio: 'pipe'
|
||
});
|
||
|
||
let stdout = '';
|
||
let stderr = '';
|
||
|
||
pnpm.stdout.on('data', (data) => {
|
||
stdout += data.toString();
|
||
});
|
||
|
||
pnpm.stderr.on('data', (data) => {
|
||
stderr += data.toString();
|
||
});
|
||
|
||
pnpm.on('close', (code) => {
|
||
if (code === 0) {
|
||
resolve({ stdout, stderr });
|
||
} else {
|
||
reject(new Error(`pnpm install failed with code ${code}: ${stderr}`));
|
||
}
|
||
});
|
||
|
||
pnpm.on('error', (error) => {
|
||
reject(new Error(`Failed to spawn pnpm: ${error.message}`));
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 创建执行沙箱环境
|
||
*/
|
||
async createExecutionSandbox() {
|
||
this.sandboxContext = this.createSandbox({
|
||
supportDependencies: true,
|
||
sandboxPath: this.sandboxPath
|
||
});
|
||
|
||
// 在智能沙箱中重新加载工具
|
||
const script = new vm.Script(this.toolContent, { filename: `${this.toolId}.js` });
|
||
const context = vm.createContext(this.sandboxContext);
|
||
|
||
script.runInContext(context);
|
||
const exported = context.module.exports;
|
||
|
||
if (typeof exported === 'function') {
|
||
this.toolInstance = new exported();
|
||
} else if (typeof exported === 'object') {
|
||
this.toolInstance = exported;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 创建统一沙箱环境
|
||
* @param {Object} options - 沙箱配置
|
||
* @param {boolean} options.supportDependencies - 是否支持依赖解析
|
||
* @param {string} options.sandboxPath - 沙箱工作目录
|
||
* @returns {Object} 沙箱环境对象
|
||
*/
|
||
createSandbox(options = {}) {
|
||
const {
|
||
supportDependencies = false,
|
||
sandboxPath = process.cwd()
|
||
} = options;
|
||
|
||
return {
|
||
require: supportDependencies ?
|
||
this.createSmartRequire(sandboxPath) :
|
||
this.createAnalysisRequire(),
|
||
module: { exports: {} },
|
||
exports: {},
|
||
console: console,
|
||
Buffer: Buffer,
|
||
process: this.createProcessMock(sandboxPath),
|
||
setTimeout: setTimeout,
|
||
clearTimeout: clearTimeout,
|
||
setInterval: setInterval,
|
||
clearInterval: clearInterval,
|
||
Object: Object,
|
||
Array: Array,
|
||
String: String,
|
||
Number: Number,
|
||
Boolean: Boolean,
|
||
Date: Date,
|
||
JSON: JSON,
|
||
Math: Math,
|
||
RegExp: RegExp,
|
||
Error: Error,
|
||
URL: URL
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 创建完整的process对象mock
|
||
* @param {string} sandboxPath - 沙箱工作目录
|
||
* @returns {Object} mock的process对象
|
||
*/
|
||
createProcessMock(sandboxPath) {
|
||
return {
|
||
env: process.env,
|
||
version: process.version,
|
||
platform: process.platform,
|
||
arch: process.arch,
|
||
hrtime: process.hrtime,
|
||
cwd: () => sandboxPath,
|
||
pid: process.pid,
|
||
uptime: process.uptime
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 创建分析阶段的mock require
|
||
* 让所有require调用都成功,脚本能完整执行到module.exports
|
||
* @returns {Function} mock require函数
|
||
*/
|
||
createAnalysisRequire() {
|
||
return (moduleName) => {
|
||
// Node.js内置模块使用真实require
|
||
const builtinModules = ['path', 'fs', 'url', 'crypto', 'util', 'os', 'events', 'stream'];
|
||
|
||
try {
|
||
// 检查是否是内置模块
|
||
if (builtinModules.includes(moduleName) || moduleName.startsWith('node:')) {
|
||
return require(moduleName);
|
||
}
|
||
} catch (e) {
|
||
// 内置模块加载失败,继续mock处理
|
||
}
|
||
|
||
// 第三方模块返回通用mock对象
|
||
console.log(`[ToolSandbox] 分析阶段mock模块: ${moduleName}`);
|
||
return new Proxy({}, {
|
||
get: () => () => ({}), // 所有属性和方法都返回空函数/对象
|
||
apply: () => ({}), // 如果被当作函数调用
|
||
construct: () => ({}) // 如果被当作构造函数
|
||
});
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 创建支持依赖解析的require函数
|
||
* @param {string} sandboxPath - 沙箱路径
|
||
* @returns {Function} 智能require函数
|
||
*/
|
||
createSmartRequire(sandboxPath) {
|
||
return (moduleName) => {
|
||
try {
|
||
return require(require.resolve(moduleName, {
|
||
paths: [
|
||
path.join(sandboxPath, 'node_modules'),
|
||
sandboxPath,
|
||
process.cwd() + '/node_modules'
|
||
]
|
||
}));
|
||
} catch (error) {
|
||
return require(moduleName);
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 参数验证
|
||
*/
|
||
async validateParameters(parameters) {
|
||
if (typeof this.toolInstance.validate === 'function') {
|
||
const result = this.toolInstance.validate(parameters);
|
||
|
||
if (typeof result === 'boolean' && !result) {
|
||
throw new Error('Parameter validation failed');
|
||
} else if (result && typeof result === 'object' && !result.valid) {
|
||
throw new Error(`Parameter validation failed: ${result.errors?.join(', ')}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 在沙箱中执行工具
|
||
*/
|
||
async executeInSandbox(parameters) {
|
||
if (!this.toolInstance || typeof this.toolInstance.execute !== 'function') {
|
||
throw new Error(`Tool ${this.toolId} does not have execute method`);
|
||
}
|
||
|
||
return await this.toolInstance.execute(parameters);
|
||
}
|
||
|
||
/**
|
||
* 获取分析结果
|
||
*/
|
||
getAnalysisResult() {
|
||
return {
|
||
toolId: this.toolId,
|
||
dependencies: this.dependencies,
|
||
sandboxPath: this.sandboxPath,
|
||
hasMetadata: typeof this.toolInstance?.getMetadata === 'function',
|
||
hasSchema: typeof this.toolInstance?.getSchema === 'function'
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 清理沙箱资源
|
||
*/
|
||
async cleanup() {
|
||
// 可选:清理临时文件、关闭连接等
|
||
this.sandboxContext = null;
|
||
this.toolInstance = null;
|
||
}
|
||
|
||
/**
|
||
* 获取工具元信息
|
||
*/
|
||
getToolMetadata() {
|
||
if (this.toolInstance && typeof this.toolInstance.getMetadata === 'function') {
|
||
return this.toolInstance.getMetadata();
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 获取工具Schema
|
||
*/
|
||
getToolSchema() {
|
||
if (this.toolInstance && typeof this.toolInstance.getSchema === 'function') {
|
||
return this.toolInstance.getSchema();
|
||
}
|
||
return null;
|
||
}
|
||
}
|
||
|
||
module.exports = ToolSandbox; |