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

115 lines
4.2 KiB
TypeScript

/**
* HTTP 请求工具(1 个)
*
* http_request — 发送 HTTP/REST API 请求
*
* 使用 Node.js 18+ 内置 fetch API。
* 响应体截断到 50KB 防止结果过大。
*/
import type { IMetonaTool, ToolExecutionContext } from '../../types/metona-tool';
import type { MetonaToolDef } from '../../../harness/types';
import { MetonaToolCategory, MetonaRiskLevel } from '../../../harness/types';
const ALLOWED_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'] as const;
const MAX_BODY_BYTES = 50 * 1024; // 50KB
export class HttpRequestTool implements IMetonaTool {
readonly definition: MetonaToolDef = {
name: 'http_request',
description: 'Send an HTTP/REST API request. Supports GET/POST/PUT/PATCH/DELETE/HEAD methods with custom headers and body. Response body is truncated to 50KB.',
parameters: {
type: 'object',
properties: {
url: { type: 'string', description: 'Request URL (must start with http:// or https://)' },
method: {
type: 'string',
description: 'HTTP method (default GET)',
enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'],
},
headers: { type: 'object', description: 'Request headers as key-value pairs' },
body: { type: 'string', description: 'Request body (string)' },
timeout: { type: 'number', description: 'Timeout in milliseconds (default 30000, max 60000)' },
},
required: ['url'],
},
category: MetonaToolCategory.NETWORK,
riskLevel: MetonaRiskLevel.LOW,
requiresPermission: false,
timeoutMs: 30_000,
};
async execute(args: Record<string, unknown>, _context: ToolExecutionContext): Promise<unknown> {
try {
const url = args.url as string;
const method = ((args.method as string) ?? 'GET').toUpperCase();
const headers = (args.headers as Record<string, string> | undefined) ?? undefined;
const body = args.body as string | undefined;
const timeout = Math.min(60_000, Math.max(1, (args.timeout as number) ?? 30_000));
// 校验 URL
if (!url || !/^https?:\/\//i.test(url)) {
return { error: 'Invalid URL', success: false };
}
// 校验 method
if (!(ALLOWED_METHODS as readonly string[]).includes(method)) {
return {
error: `Invalid method: ${method}. Must be one of: ${ALLOWED_METHODS.join(', ')}`,
success: false,
};
}
// 超时控制
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
const fetchOptions: RequestInit = {
method,
headers,
signal: controller.signal,
redirect: 'follow',
};
// GET/HEAD 不应携带 body
if (body !== undefined && method !== 'GET' && method !== 'HEAD') {
fetchOptions.body = body;
}
const response = await fetch(url, fetchOptions);
const text = await response.text();
// 截断到 50KB
const truncated = text.length > MAX_BODY_BYTES;
const safeBody = truncated ? text.slice(0, MAX_BODY_BYTES) : text;
// 只返回 content-type 和 content-length
const filteredHeaders: Record<string, string> = {};
const contentType = response.headers.get('content-type');
if (contentType) filteredHeaders['content-type'] = contentType;
const contentLength = response.headers.get('content-length');
if (contentLength) filteredHeaders['content-length'] = contentLength;
return {
status: response.status,
statusText: response.statusText,
headers: filteredHeaders,
body: safeBody,
truncated,
ok: response.ok,
success: true, // v0.3.1 修复 WARN-4: 成功路径添加 success 字段
};
} finally {
clearTimeout(timer);
}
} catch (error) {
// 区分超时(AbortError)与其他网络错误
if (error instanceof Error && error.name === 'AbortError') {
return { error: 'Request timeout', success: false };
}
const errMsg = error instanceof Error ? error.message : String(error);
return { error: errMsg, success: false };
}
}
}