Files
thzxx 0fc589e73b feat: 升级至 v0.3.2 — 工具体系扩展至 26 个 + Context Window 可配置化
## 主要变更

### 1. Context Window 可配置化(v0.3.1 延续)
- DeepSeek/Agnes contextWindow 不再写死 1M,可在设置中配置(min 4096)
- 修复 Engine 128K vs Adapter 1M 不一致 bug,Engine 从 adapter.getContextWindow() 读取
- 热重载 configSig 加入 contextWindow,配置变化即时生效

### 2. 新增 11 个工具(15 → 26 个)
- Git 工具集(4):git_status, git_diff, git_log, git_commit
- 开发工具集(3):lint_code, run_tests, project_info
- HTTP 请求(1):http_request(Node 18+ fetch + AbortController)
- TODO 管理(1):todo_write(会话级内存 + LRU 淘汰)
- 结构化思考(1):think(无副作用思考空间)
- 图片查看(1):view_image(base64 data URL,多模态 LLM 支持)

### 3. 审计修复(2 FAIL + 14 WARN)
- FAIL-1: registry.ts truncateResult 添加 dataUrl 白名单(图片不被截断)
- FAIL-2: todo.ts 添加 LRU 策略 + clearSession 静态方法
- WARN-1: run_tests filter 字符白名单校验,防 cmd 元字符注入
- WARN-2: dataUrl 检测前置到 stringify 之前,避免大图片无意义序列化
- WARN-3: todo.ts 实现真正 LRU(访问刷新位置,非 FIFO)
- WARN-4: git_diff maxBuffer 提升至 5MB,支持超大变更集
- WARN-5: log.info 移至 DelegateTaskTool 注册后,输出正确的 26
- WARN-6: riskColors 添加 critical: 'error' 键
- WARN-7: 所有 execFileAsync 显式设置 encoding: 'utf-8'
- WARN-8: git_log --author 拆分为独立参数
- WARN-9: todo_write 权限从 WRITE 改为 READ
- WARN-10: description 区分与 task_manager 的不同用途

### 4. PolicyEngine 策略
- 新增 11 条策略,26 个工具 + mcp_* 全覆盖
- git_commit: WRITE + requireConfirmation
- todo_write: READ(内存操作)
2026-07-14 21:15:54 +08:00

175 lines
5.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Tool Registry — 工具注册表
*
* 管理所有可用工具(内置 + MCP),提供查找、注册、注销功能。
* 提供 per-tool 超时强制和结果大小限制,防止卡死和上下文溢出。
*/
import type {
MetonaToolDef,
MetonaToolCall,
MetonaToolResult,
} from '../types';
import type { IMetonaTool, ToolRegistryEntry, ToolExecutionContext } from '../types/metona-tool';
/** 工具返回值最大字符数(约 50KB),超过则截断 */
const MAX_RESULT_CHARS = 50_000;
export class ToolRegistry {
private tools = new Map<string, ToolRegistryEntry>();
/** 注册内置工具 */
registerBuiltin(tool: IMetonaTool): void {
this.tools.set(tool.definition.name, {
tool,
source: 'builtin',
enabled: true,
});
}
/** 注册 MCP 工具 */
registerMCP(serverName: string, tool: IMetonaTool): void {
this.tools.set(tool.definition.name, {
tool,
source: 'mcp',
serverName,
enabled: true,
});
}
/** 注销 MCP Server 提供的所有工具 */
unregisterMCPTools(serverName: string): void {
for (const [name, entry] of this.tools) {
if (entry.source === 'mcp' && entry.serverName === serverName) {
this.tools.delete(name);
}
}
}
/** 获取工具 */
get(name: string): IMetonaTool | undefined {
const entry = this.tools.get(name);
if (!entry?.enabled) return undefined;
return entry.tool;
}
/** 列出所有已启用工具的定义 */
listTools(): MetonaToolDef[] {
return Array.from(this.tools.values())
.filter((e) => e.enabled)
.map((e) => e.tool.definition);
}
/** 列出所有工具定义(含已禁用的,供设置 UI 使用) */
listAllTools(): Array<MetonaToolDef & { enabled: boolean }> {
return Array.from(this.tools.values()).map((e) => ({
...e.tool.definition,
enabled: e.enabled,
}));
}
/** 设置工具启用/禁用状态(供 IPC tools:toggle 调用) */
setToolEnabled(name: string, enabled: boolean): void {
const entry = this.tools.get(name);
if (entry) {
entry.enabled = enabled;
}
}
/** 执行工具(带超时强制和结果大小限制) */
async execute(
toolCall: MetonaToolCall,
context: ToolExecutionContext,
): Promise<MetonaToolResult> {
const tool = this.get(toolCall.name);
if (!tool) {
return {
toolCallId: toolCall.id,
toolName: toolCall.name,
result: null,
success: false,
error: `Unknown tool: ${toolCall.name}`,
durationMs: 0,
timestamp: Date.now(),
};
}
const startTs = Date.now();
const timeoutMs = tool.definition.timeoutMs;
// M-15 修复: 使用 try/finally 清理 setTimeout,防止事件循环 timer 堆积
// 工具正常完成时未触发的 timer 会持续占用事件循环 timeoutMs 毫秒
let timer: ReturnType<typeof setTimeout> | undefined;
try {
// 带超时执行 — 使用 Promise.race 防止工具卡死阻塞 Agent Loop
const result = await Promise.race([
tool.execute(toolCall.args, context),
new Promise<never>((_, reject) => {
timer = setTimeout(
() => reject(new Error(`Tool execution timed out after ${timeoutMs}ms`)),
timeoutMs,
);
}),
]);
// 结果大小限制 — 防止过大返回值耗尽 LLM 上下文窗口
const safeResult = this.truncateResult(result);
return {
toolCallId: toolCall.id,
toolName: toolCall.name,
result: safeResult,
success: true,
durationMs: Date.now() - startTs,
timestamp: Date.now(),
};
} catch (error) {
return {
toolCallId: toolCall.id,
toolName: toolCall.name,
result: null,
success: false,
error: (error as Error).message,
durationMs: Date.now() - startTs,
timestamp: Date.now(),
};
} finally {
// M-15 修复: 无论工具成功或失败,清理 timeout timer
if (timer) clearTimeout(timer);
}
}
/**
* 截断过大的工具返回值,防止 LLM 上下文溢出
*
* v0.3.1 修复 FAIL-1: view_image 返回的 dataUrl 需完整传输给多模态 LLM,
* 截断会导致 base64 损坏、图片无法显示。对含 dataUrl 字段的结果跳过截断。
* view_image 已在工具内部限制文件大小 5MBbase64 后约 6.7MB
* 多模态 LLM 能处理此量级数据。
*
* v0.3.1 修复 WARN-2: dataUrl 检测前置到 stringify 之前,
* 避免对 5MB+ 的图片对象做无意义的 JSON.stringify(约 6.7MB 字符串)。
*/
private truncateResult(result: unknown): unknown {
// 先检测 dataUrl 白名单:图片类结果跳过截断(避免 base64 损坏 + 避免无意义的序列化)
if (typeof result === 'object' && result !== null && 'dataUrl' in result) {
return result;
}
const str = typeof result === 'string' ? result : JSON.stringify(result);
if (str.length <= MAX_RESULT_CHARS) return result;
return {
_truncated: true,
_original_size: str.length,
_preview: str.slice(0, MAX_RESULT_CHARS),
_message: `Result truncated: original ${str.length} chars exceeds limit ${MAX_RESULT_CHARS}`,
};
}
/** 获取工具数量 */
get size(): number {
return Array.from(this.tools.values()).filter((e) => e.enabled).length;
}
}