/** * 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, _context: ToolExecutionContext): Promise { try { const url = args.url as string; const method = ((args.method as string) ?? 'GET').toUpperCase(); const headers = (args.headers as Record | 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 = {}; 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 }; } } }