/** * Tool Handlers - 系统与网络操作 */ import * as fs from 'fs/promises'; import * as path from 'path'; import { spawn } from 'child_process'; import { checkPathAllowed, checkCommandAllowed } from './tool-security.js'; import { mainWindow } from './main.js'; import { sendLog, resolvePath, type ToolResult } from './tool-handlers-shared.js'; import { getWorkspaceDir } from './workspace.js'; import { getSetting } from './db/sqlite.js'; /** 当前工具命令进程(用于用户手动终止) */ let _toolProc: ReturnType | null = null; export async function handleRunCommand(params: { command: string; cwd?: string; timeout?: number }): Promise { try { const cmdCheck = checkCommandAllowed(params.command); if (!cmdCheck.ok) { sendLog('warn', `🔧 run_command 被拦截`, `${params.command.slice(0, 100)} | ${cmdCheck.reason}`); return { success: false, error: cmdCheck.reason }; } const cwd = params.cwd ? path.resolve(params.cwd) : getWorkspaceDir(); const dirCheck = checkPathAllowed(cwd, 'read'); if (!dirCheck.ok) { sendLog('warn', `🔧 run_command 目录被拦截`, `${cwd} | ${dirCheck.reason}`); return { success: false, error: dirCheck.reason }; } sendLog('info', `🔧 run_command 执行`, `${params.command.slice(0, 200)} | cwd: ${cwd}`); return new Promise((resolve) => { const isWin = process.platform === 'win32'; const shell = isWin ? 'cmd.exe' : 'bash'; const actualCmd = isWin ? `chcp 65001 >nul && ${params.command}` : params.command; const shellArgs = isWin ? ['/c', actualCmd] : ['-c', actualCmd]; const startTime = Date.now(); _toolProc = spawn(shell, shellArgs, { cwd, env: { ...process.env, ...(isWin ? {} : { TERM: 'xterm-256color' }), LANG: 'en_US.UTF-8', LC_ALL: 'en_US.UTF-8', PYTHONIOENCODING: 'utf-8' }, stdio: ['pipe', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; // 实时推送到工作空间终端 _toolProc.stdout?.on('data', (data: Buffer) => { const str = data.toString('utf-8'); stdout += str; mainWindow?.webContents.send('cmd:output', { command: params.command, type: 'stdout', data: str }); }); _toolProc.stderr?.on('data', (data: Buffer) => { const str = data.toString('utf-8'); stderr += str; mainWindow?.webContents.send('cmd:output', { command: params.command, type: 'stderr', data: str }); }); _toolProc.on('close', (code) => { _toolProc = null; const duration = Date.now() - startTime; sendLog( code === 0 ? 'success' : 'warn', `🔧 run_command 完成`, `exitCode: ${code} | ${duration}ms | stdout: ${stdout.length}B | stderr: ${stderr.length}B` ); mainWindow?.webContents.send('cmd:done', { command: params.command, exitCode: code }); resolve({ success: code === 0, stdout, stderr, exitCode: code, duration, cwd, command: params.command, }); }); _toolProc.on('error', (err) => { _toolProc = null; const duration = Date.now() - startTime; sendLog('error', `🔧 run_command 失败`, `${err.message} | ${duration}ms`); mainWindow?.webContents.send('cmd:done', { command: params.command, exitCode: 1 }); resolve({ success: false, stdout, stderr: stderr + (stderr ? '\n' : '') + err.message, exitCode: 1, error: err.message, duration, cwd, command: params.command, }); }); }); } catch (err) { sendLog('error', `🔧 run_command 异常`, (err as Error).message); return { success: false, stdout: '', stderr: (err as Error).message, exitCode: 1, error: (err as Error).message }; } } /** 终止当前工具命令进程 */ export function killToolProcess(): boolean { if (!_toolProc) { sendLog('warn', `🔧 cmd:kill`, '无正在运行的工具命令'); return false; } try { _toolProc.kill('SIGTERM'); } catch { /* ignore */ } _toolProc = null; sendLog('info', `🔧 cmd:kill`, '工具命令已终止'); return true; } /** HTTP 请求超时(毫秒),默认 30s,可通过 IPC 设置 */ let HTTP_TIMEOUT = 30_000; /** 设置 HTTP 请求超时(毫秒) */ export function setHTTPTimeout(ms: number): void { HTTP_TIMEOUT = Math.max(5000, Math.min(300_000, ms)); // 5s ~ 300s } // ── LRU 搜索缓存 ────────────────────────────────── interface CacheEntry { results: Array<{ title: string; url: string; snippet: string; engine: string; reachable?: boolean }>; time: number; } const _searchCache = new Map(); const CACHE_MAX = 200; const CACHE_TTL = 5 * 60 * 1000; // 5 分钟 function cacheGet(query: string): CacheEntry | null { const entry = _searchCache.get(query); if (!entry) return null; if (Date.now() - entry.time > CACHE_TTL) { _searchCache.delete(query); return null; } return entry; } function cacheSet(query: string, entry: CacheEntry): void { if (_searchCache.size >= CACHE_MAX) { const firstKey = _searchCache.keys().next().value; if (firstKey !== undefined) _searchCache.delete(firstKey); } _searchCache.set(query, entry); } // ── 反爬请求头 ──────────────────────────────────── const DESKTOP_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'; const MOBILE_UA = 'Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36'; /** 构建 fetch 请求头,根据 UA 类型动态设置 */ function buildFetchHeaders(url: string, useMobileUA: boolean): Record { const ua = useMobileUA ? MOBILE_UA : DESKTOP_UA; let referer = 'https://www.google.com/'; try { const u = new URL(url); referer = `${u.protocol}//${u.hostname}/`; } catch { /* ignore */ } return { 'User-Agent': ua, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Accept-Encoding': 'gzip, deflate, br', 'Cache-Control': 'no-cache', 'DNT': '1', 'Connection': 'keep-alive', 'Upgrade-Insecure-Requests': '1', 'Sec-Fetch-Dest': 'document', 'Sec-Fetch-Mode': 'navigate', 'Sec-Fetch-Site': 'none', 'Sec-Fetch-User': '?1', 'Pragma': 'no-cache', 'Referer': referer, }; } /** 带超时的 fetch 封装 */ async function fetchWithTimeout(url: string, timeout = HTTP_TIMEOUT, headers?: Record): Promise { const _headers = headers || buildFetchHeaders(url, false); const controller = new AbortController(); const tid = setTimeout(() => controller.abort(), timeout); try { const resp = await fetch(url, { headers: _headers, signal: controller.signal, redirect: 'follow' }); clearTimeout(tid); return resp; } catch { clearTimeout(tid); return null; } } /** 完整 HTML 实体映射(常见实体) */ const HTML_ENTITIES: Record = { ' ': ' ', '<': '<', '>': '>', '&': '&', '"': '"', ''': "'", ''': "'", ' ': ' ', ' ': ' ', '©': '\u00A9', '®': '\u00AE', '™': '\u2122', '€': '\u20AC', '£': '\u00A3', '¥': '\u00A5', '°': '\u00B0', '·': '\u00B7', '…': '\u2026', '—': '\u2014', '–': '\u2013', '‘': '\u2018', '’': '\u2019', '“': '\u201C', '”': '\u201D', '•': '\u2022', '×': '\u00D7', '÷': '\u00F7', '±': '\u00B1', 'µ': '\u00B5', '¶': '\u00B6', '§': '\u00A7', '«': '\u00AB', '»': '\u00BB', '¡': '\u00A1', '¿': '\u00BF', '¬': '\u00AC', '­': '\u00AD', '¯': '\u00AF', '´': '\u00B4', '¸': '\u00B8', 'Œ': '\u0152', 'œ': '\u0153', 'Š': '\u0160', 'š': '\u0161', 'Ÿ': '\u0178', 'ˆ': '\u02C6', '˜': '\u02DC', }; /** 解码 HTML 实体 */ function decodeHTMLEntities(text: string): string { let result = text; for (const [entity, char] of Object.entries(HTML_ENTITIES)) { result = result.replaceAll(entity, char); } // 数字实体: { 和  result = result.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))); result = result.replace(/&#(\d+);/g, (_, dec) => String.fromCharCode(parseInt(dec, 10))); return result; } /** 将 HTML 转换为可读文本(保留结构) */ function htmlToText(html: string): string { let text = html; // 移除 script/style/nav/header/footer 等噪音标签及其内容 text = text.replace(//gi, ''); text = text.replace(//gi, ''); text = text.replace(//gi, ''); text = text.replace(//gi, ''); text = text.replace(//gi, ''); text = text.replace(//gi, ''); text = text.replace(//gi, ''); text = text.replace(//gi, ''); text = text.replace(//gi, ''); // 移除 HTML 注释 text = text.replace(//g, ''); // 块级标签转为换行 text = text.replace(/<\/(p|div|h[1-6]|li|tr|blockquote|section|article|pre|br|hr)[^>]*>/gi, '\n'); text = text.replace(/<(br|hr)[^>]*\/?>/gi, '\n'); // 表格单元用制表符分隔 text = text.replace(/<\/(td|th)[^>]*>/gi, '\t'); // 移除剩余标签 text = text.replace(/<[^>]+>/g, ''); // 解码 HTML 实体 text = decodeHTMLEntities(text); // 清理多余空白(保留换行结构) text = text.replace(/[ \t]+/g, ' '); text = text.replace(/\n\s*\n\s*\n+/g, '\n\n'); text = text.split('\n').map(l => l.trim()).join('\n'); return text.trim(); } import { browserOpen, browserExtract, browserClose } from './browser.js'; // ────────────────────────────────────────────────── // web_fetch 重试配置 // ────────────────────────────────────────────────── const FETCH_MAX_RETRIES = 2; // fetch 阶段最多重试 2 次 const FETCH_RETRY_DELAYS = [1500, 3000]; // 指数退避 (ms) /** fetch 全部失败后自动回退到浏览器渲染 */ const BROWSER_FALLBACK_ENABLED = true; /** 内容过短阈值:小于此字符数且是 HTML 时,自动升级到浏览器渲染 */ const SHORT_CONTENT_THRESHOLD = 200; /** 需要回退浏览器的 HTTP 状态码(反爬/拦截信号) */ const BROWSER_FALLBACK_STATUSES = new Set([403, 503, 429, 502]); /** 浏览器回退:打开 URL → 等待渲染 → 提取内容 */ async function browserFallback(url: string, maxChars: number): Promise<{ text: string; method: string } | null> { try { const openResult = await browserOpen(url); if (!openResult.success) return null; await new Promise(r => setTimeout(r, 2500)); // 等待 JS 渲染 const extractResult = await browserExtract(); browserClose(); if (extractResult.success && extractResult.text && extractResult.text.length > 0) { let text = extractResult.text; if (maxChars > 0 && text.length > maxChars) text = text.slice(0, maxChars); return { text, method: 'browser' }; } return null; } catch { try { browserClose(); } catch { /* */ } return null; } } export async function handleWebFetch(params: { url: string; max_chars?: number; extract_mode?: string; mobile_ua?: boolean; retry?: boolean }): Promise { const url = params.url; if (!url.startsWith('http://') && !url.startsWith('https://')) { return { success: false, error: '仅支持 http/https 协议' }; } const maxChars = params.max_chars || 0; // 0 = 不截断 const useMobileUA = params.mobile_ua === true; const shouldRetry = params.retry !== false; // 默认开启重试 sendLog('info', `🌐 web_fetch`, `${url} (${maxChars > 0 ? maxChars + ' chars' : '完整内容'}${useMobileUA ? ', 移动端UA' : ''})`); // ── Phase 1 & 2: HTTP fetch with UA switching ── let lastError = ''; const maxAttempts = shouldRetry ? FETCH_MAX_RETRIES + 1 : 1; for (let attempt = 0; attempt < maxAttempts; attempt++) { if (attempt > 0) { const delay = FETCH_RETRY_DELAYS[attempt - 1] || 3000; sendLog('warn', `🌐 web_fetch 重试`, `第 ${attempt + 1}/${maxAttempts} 次, 等待 ${delay}ms`); await new Promise(r => setTimeout(r, delay)); } // 重试时切换 UA:首次用指定 UA,第二次换另一种,第三次用移动端 const useMobile = attempt >= 2 ? true : (attempt === 1 ? !useMobileUA : useMobileUA); const headers = buildFetchHeaders(url, useMobile); try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), HTTP_TIMEOUT); let resp: Response; try { resp = await fetch(url, { headers, signal: controller.signal, redirect: 'follow' }); } finally { clearTimeout(timeoutId); } if (!resp.ok) { // 反爬信号(403/503/429)→ 跳过后续 fetch,直接走浏览器回退 if (BROWSER_FALLBACK_STATUSES.has(resp.status) && BROWSER_FALLBACK_ENABLED) { sendLog('warn', `🌐 web_fetch HTTP ${resp.status},触发浏览器回退`, url); break; // 跳出 fetch 循环,走浏览器回退 } // 5xx 服务器错误可重试 if (resp.status >= 500 && attempt < maxAttempts - 1) { lastError = `HTTP ${resp.status}: ${resp.statusText}(将重试)`; continue; } return { success: false, error: `HTTP ${resp.status}: ${resp.statusText}。可尝试用 browser_open 直接打开页面。` }; } const contentType = resp.headers.get('content-type') || ''; if (!contentType.includes('text') && !contentType.includes('html') && !contentType.includes('json') && !contentType.includes('xml')) { return { success: false, error: `不支持的内容类型: ${contentType}。如果是二进制文件,请用 download_file 工具。` }; } const contentLength = resp.headers.get('content-length'); if (contentLength && parseInt(contentLength, 10) > 10 * 1024 * 1024) { return { success: false, error: `页面过大 (${(parseInt(contentLength, 10) / 1024 / 1024).toFixed(1)}MB),超过 10MB 限制` }; } let text = await resp.text(); const isHTML = contentType.includes('html'); if (isHTML) { text = htmlToText(text); } // ── SPA/短内容自动升级到浏览器渲染 ── if (isHTML && text.length < SHORT_CONTENT_THRESHOLD && BROWSER_FALLBACK_ENABLED) { sendLog('info', `🌐 web_fetch 内容过短(${text.length}字符)`, `自动升级到浏览器渲染: ${url}`); const browserResult = await browserFallback(url, maxChars); if (browserResult) { sendLog('success', `🌐 web_fetch 浏览器渲染完成`, `${browserResult.text.length} 字符`); return { success: true, url, content: browserResult.text, content_type: 'text/html (browser)', status: resp.status, truncated: maxChars > 0 && browserResult.text.length >= maxChars, length: browserResult.text.length, retries: attempt, method: 'browser', }; } sendLog('warn', `🌐 web_fetch 浏览器渲染失败,使用原始内容`); } const truncated = maxChars > 0 && text.length > maxChars; if (truncated) text = text.slice(0, maxChars); sendLog('success', `🌐 web_fetch 完成`, `HTTP ${resp.status} | ${text.length} chars${truncated ? ' (已截断)' : ''}${attempt > 0 ? ` | 重试${attempt}次后成功` : ''}`); return { success: true, url, content: text, content_type: contentType, status: resp.status, truncated, length: text.length, retries: attempt, method: useMobile ? 'fetch(mobile)' : 'fetch(desktop)', }; } catch (err) { const errMsg = (err as Error).message; if (errMsg.includes('abort') && attempt < maxAttempts - 1) { lastError = `请求超时 (${HTTP_TIMEOUT / 1000}s)(将重试)`; continue; } if (errMsg.includes('abort')) { // 超时 → 浏览器回退 sendLog('warn', `🌐 web_fetch 超时,触发浏览器回退`, url); break; } // 网络错误可重试 if (attempt < maxAttempts - 1) { lastError = errMsg + '(将重试)'; continue; } // 最后一次也失败了 → 浏览器回退 if (BROWSER_FALLBACK_ENABLED) { sendLog('warn', `🌐 web_fetch 所有 fetch 均失败(${errMsg}),触发浏览器回退`, url); break; } return { success: false, error: `fetch 失败: ${errMsg}。可尝试用 browser_open 直接打开页面。` }; } } // ── Phase 3: 浏览器回退(fetch 全部失败后的最后手段)── if (BROWSER_FALLBACK_ENABLED) { sendLog('info', `🌐 web_fetch 回退到浏览器渲染`, url); const browserResult = await browserFallback(url, maxChars); if (browserResult) { sendLog('success', `🌐 web_fetch 浏览器渲染完成`, `${browserResult.text.length} 字符`); return { success: true, url, content: browserResult.text, content_type: 'text/html (browser)', status: 200, truncated: maxChars > 0 && browserResult.text.length >= maxChars, length: browserResult.text.length, retries: maxAttempts, method: 'browser(fallback)', }; } return { success: false, error: `fetch 和浏览器渲染均失败。${lastError ? '最后错误: ' + lastError : '请检查 URL 是否可访问。'}` }; } return { success: false, error: lastError || '所有重试均失败' }; } /** * SearXNG 元搜索引擎 — JSON API 搜索 * 支持 70+ 引擎聚合、隐私保护、无广告 */ async function handleWebSearchSearxng( query: string, maxResults: number, timeRange: string, _enhanceSnippets: boolean ): Promise { const url = getSetting('searxng_url', ''); if (!url) { return { success: false, error: 'SearXNG API 地址未配置。请在 SearXNG 设置中填写 API 地址。' }; } const engines = getSetting('searxng_engines', 'google,bing,duckduckgo,wikipedia'); const language = getSetting('searxng_language', 'zh-CN'); const safesearch = getSetting('searxng_safesearch', 1); const cfgTimeRange = getSetting('searxng_time_range', ''); const effectiveTimeRange = timeRange || cfgTimeRange; const cfgMaxResults = getSetting('searxng_max_results', 20); const effectiveMaxResults = Math.min(maxResults, cfgMaxResults); const format = getSetting('searxng_format', 'json'); const isHtml = format === 'html'; const params = new URLSearchParams({ q: query }); if (!isHtml) params.set('format', 'json'); if (engines) params.set('engines', engines); if (language) params.set('language', language); if (safesearch > 0) params.set('safesearch', String(safesearch)); if (effectiveTimeRange) params.set('time_range', effectiveTimeRange); const authKey = getSetting('searxng_auth_key', ''); const apiUrl = `${url.replace(/\/+$/, '')}/search?${params.toString()}`; sendLog('info', `🔍 SearXNG 搜索`, `${query} → ${url} (${format})`); try { const headers = buildFetchHeaders(apiUrl, false); if (authKey) headers['Authorization'] = `Bearer ${authKey}`; const resp = await fetchWithTimeout(apiUrl, HTTP_TIMEOUT, headers); if (!resp || !resp.ok) { return { success: false, error: `SearXNG 搜索失败 (HTTP ${resp?.status || 'timeout'})。请检查 SearXNG 实例是否运行正常:${url}。`, _searxng_url: apiUrl }; } // ── HTML 模式:返回原始网页文本,让 AI 自行分析 ── if (isHtml) { const html = await resp.text(); const text = htmlToText(html); const truncated = effectiveMaxResults > 0 && text.length > effectiveMaxResults * 500; const content = truncated ? text.slice(0, effectiveMaxResults * 500) + '\n...(已截断)' : text; sendLog('success', `🔍 SearXNG HTML 完成`, `${text.length} 字符`); return { success: true, url: apiUrl, content, content_type: 'text/html', length: content.length, truncated, format: 'html', _hint: '这是 SearXNG 搜索结果页面的纯文本提取。请从中提取有用的搜索结果信息来回答用户问题。' }; } const data = await resp.json() as { query: string; number_of_results?: number; results?: Array<{ title: string; url: string; content: string; engine: string; score?: number; category?: string; }>; unresponsive_engines?: Array<[string, string]>; }; const rawResults = (data.results || []).slice(0, effectiveMaxResults); if (rawResults.length === 0) { return { success: false, error: 'SearXNG 未返回任何结果,请尝试其他关键词或调整搜索引擎配置' }; } // 转换为兼容格式 const results = rawResults.map(r => ({ title: (r.title || '').replace(/<[^>]+>/g, '').trim(), url: r.url, snippet: (r.content || '').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim(), engine: r.engine || 'searxng', weight: 80, reachable: true, })); const engineNames = [...new Set(rawResults.map(r => r.engine))].filter(Boolean); const unresponsive = data.unresponsive_engines || []; const engineStats: Record = {}; for (const engine of engineNames) { engineStats[engine] = `${rawResults.filter(r => r.engine === engine).length} 条`; } for (const [eng, reason] of unresponsive) { engineStats[eng] = `⚠️ ${reason}`; } sendLog('success', `🔍 SearXNG 完成`, `${results.length} 条结果, 引擎: ${engineNames.join(', ')}`); return buildSearchResponse( results.map(r => ({ title: r.title, url: r.url, snippet: r.snippet, engine: `searxng:${r.engine}`, weight: 80, reachable: true, })), effectiveMaxResults, false, engineStats ); } catch (err) { sendLog('error', `🔍 SearXNG 异常`, (err as Error).message); return { success: false, error: `SearXNG 搜索异常: ${(err as Error).message}` }; } } /** * Web Search — 联网搜索(并行多引擎 + LRU 缓存 + 智能排序 + 时间过滤 + 摘要增强) * 同时请求 Bing / 百度 / 搜狗 / 360搜索,智能排序后返回 * 如果启用了 SearXNG,则使用 SearXNG JSON API 替代 */ export async function handleWebSearch(params: { query: string; max_results?: number; time_range?: string; enhance_snippets?: boolean }): Promise { try { const query = params.query; if (!query || query.trim().length === 0) { return { success: false, error: '搜索关键词不能为空' }; } const maxResults = Math.min(params.max_results || 15, 20); const timeRange = params.time_range || ''; const enhanceSnippets = params.enhance_snippets !== false; // 默认开启 // ── SearXNG 模式:如果启用了 SearXNG,走 JSON API ── const searxngEnabled = getSetting('searxng_enabled', false); if (searxngEnabled) { return handleWebSearchSearxng(query, maxResults, timeRange, enhanceSnippets); } const cacheKey = `${query}|${maxResults}|${timeRange}`; // ── 时间范围 → URL 参数映射 ── let bingFreshness = ''; if (timeRange) { const tr = timeRange.toLowerCase(); if (tr === 'day' || tr === '1d' || tr === '一天') { bingFreshness = 'Day'; } else if (tr === 'week' || tr === '1w' || tr === '一周') { bingFreshness = 'Week'; } else if (tr === 'month' || tr === '1m' || tr === '一月') { bingFreshness = 'Month'; } } // ── 1. 检查缓存 ── const cached = cacheGet(cacheKey); if (cached) { sendLog('success', `🔍 web_search 缓存命中`, `"${query}" → ${cached.results.length} 条`); return buildSearchResponse(cached.results, maxResults, true); } sendLog('info', `🔍 web_search`, `"${query}" (${maxResults} 条) [并行]`); // ── 2. 并行请求所有搜索引擎 ── const engines: Array<{ name: string; weight: number; fetcher: () => Promise> }> = [ { name: 'bing', weight: 90, fetcher: async () => { const bingUrl = `https://www.bing.com/search?q=${encodeURIComponent(query)}&count=${maxResults}${bingFreshness ? '&freshness=' + bingFreshness : ''}`; const resp = await fetchWithTimeout(bingUrl); if (!resp?.ok) throw new Error(`Bing ${resp?.status}`); return parseBingResults(await resp.text(), maxResults); } }, { name: 'baidu', weight: 80, fetcher: async () => { const resp = await fetchWithTimeout( `https://www.baidu.com/s?wd=${encodeURIComponent(query)}&rn=${maxResults}` ); if (!resp?.ok) throw new Error(`百度 ${resp?.status}`); return parseBaiduResults(await resp.text(), maxResults); } }, { name: 'sogou', weight: 75, fetcher: async () => { const resp = await fetchWithTimeout( `https://www.sogou.com/web?query=${encodeURIComponent(query)}` ); if (!resp?.ok) throw new Error(`搜狗 ${resp?.status}`); return parseSogouResults(await resp.text(), maxResults); } }, { name: '360搜索', weight: 75, fetcher: async () => { const resp = await fetchWithTimeout( `https://www.so.com/s?q=${encodeURIComponent(query)}` ); if (!resp?.ok) throw new Error(`360 ${resp?.status}`); return parse360Results(await resp.text(), maxResults); } }, ]; // 所有引擎并行执行,收集结果 const settled = await Promise.allSettled(engines.map(e => e.fetcher().then(results => ({ engine: e.name, results })) )); // 汇总各引擎结果,合并去重(保留引擎和权重信息) const engineWeights = new Map(engines.map(e => [e.name, e.weight])); const allResults: Array<{ title: string; url: string; snippet: string; engine: string; weight: number; reachable?: boolean }> = []; const seenUrls = new Set(); const engineStats: Record = {}; for (let i = 0; i < settled.length; i++) { const s = settled[i]; const engineName = engines[i].name; if (s.status === 'fulfilled') { const r = s.value; engineStats[engineName] = `${r.results.length} 条`; for (const item of r.results) { const normalized = item.url.replace(/\/+$/, '').toLowerCase(); if (!seenUrls.has(normalized)) { seenUrls.add(normalized); allResults.push({ ...item, engine: engineName, weight: engineWeights.get(engineName) || 50 }); } } } else { engineStats[engineName] = `失败: ${(s.reason as Error)?.message || 'timeout'}`; sendLog('warn', `🔍 ${engineName} 搜索失败`, (s.reason as Error)?.message || '未知'); } } if (allResults.length === 0) { return { success: false, error: '所有搜索引擎均未返回结果,请尝试其他关键词' }; } // ── 3. 清理摘要 ── for (const r of allResults) { r.title = r.title.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim(); r.snippet = r.snippet.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim(); } // ── 4. URL 可达性预检 + 质量评分 ── const topResults = allResults.slice(0, Math.min(maxResults * 2, allResults.length)); // 多检查一些用于排序 const CONCURRENCY = 5; for (let i = 0; i < topResults.length; i += CONCURRENCY) { const batch = topResults.slice(i, i + CONCURRENCY); const checks = batch.map(async r => { try { const resp = await fetchWithTimeout(r.url, 3000, { 'User-Agent': DESKTOP_UA, 'Accept': 'text/html,*/*', }); r.reachable = resp?.ok === true; } catch { r.reachable = false; } }); await Promise.allSettled(checks); } // ── 5. 智能排序:可达优先 + 引擎权重 + 摘要质量 ── const scored = topResults.map(r => { let score = (r.weight / 100) * 50; // 引擎权重贡献 50% if (r.reachable === true) score += 30; // 可达 +30 else if (r.reachable === false) score -= 20; // 不可达 -20 // 摘要质量:越长越好(上限100字符),有内容的加分 const snippetLen = Math.min(r.snippet.length, 100); score += (snippetLen / 100) * 20; // 摘要质量贡献 20% return { ...r, _score: Math.round(score * 10) / 10 }; }); scored.sort((a, b) => b._score - a._score); // ── 5.5 摘要自动增强:snippet 太短/为空时,用 web_fetch 补充 top 3 ── if (enhanceSnippets) { const needEnhance = scored.slice(0, Math.min(5, maxResults)).filter(r => r.snippet.length < 30 && r.reachable !== false); if (needEnhance.length > 0) { sendLog('info', `🔍 自动补充摘要`, `${needEnhance.length} 个结果摘要过短,尝试 web_fetch`); const enhanced = await Promise.allSettled(needEnhance.slice(0, 3).map(async (r) => { try { const fetchResult = await handleWebFetch({ url: r.url, max_chars: 500, retry: false }); if (fetchResult.success && fetchResult.content) { const text = String(fetchResult.content).replace(/\s+/g, ' ').trim(); r.snippet = text.length > 200 ? text.slice(0, 200) + '…' : text; (r as any)._enhanced = true; } } catch { /* enhancement is optional */ } })); const enhancedCount = enhanced.filter(e => e.status === 'fulfilled').length; if (enhancedCount > 0) sendLog('success', `🔍 摘要增强完成`, `${enhancedCount} 个`); } } // 截取最终结果 const finalResults = scored.slice(0, maxResults); // ── 6. 缓存结果 ── cacheSet(cacheKey, { results: finalResults, time: Date.now() }); return buildSearchResponse(finalResults, maxResults, false, engineStats); } catch (err) { return { success: false, error: (err as Error).message }; } } /** 构建搜索响应(格式化文本 + 结构化 JSON) */ function buildSearchResponse( results: Array<{ title: string; url: string; snippet: string; engine: string; weight?: number; reachable?: boolean; _score?: number; _enhanced?: boolean }>, maxResults: number, fromCache: boolean, engineStats?: Record, ): ToolResult { const sliced = results.slice(0, maxResults); // 格式化文本(保持向后兼容) const now = new Date(); const dateStr = `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日`; let formatted = `[当前日期: ${dateStr}(搜索时的实时日期)]`; if (fromCache) formatted += ` [缓存]`; formatted += `\n\n`; if (engineStats) { const statsStr = Object.entries(engineStats).map(([k, v]) => `${k}: ${v}`).join(' | '); formatted += `[引擎状态: ${statsStr}]\n\n`; } formatted += sliced.map((r, i) => { const marks: string[] = []; if (r.reachable === false) marks.push('⚠️可能不可达'); if (r._score !== undefined) marks.push(`${r._score}分`); if (r._enhanced) marks.push('📝增强摘要'); const markStr = marks.length > 0 ? ` [${marks.join(' ')}]` : ''; return `[${i + 1}] ${r.title}${markStr}\n URL: ${r.url}\n ${r.snippet}`; }).join('\n\n'); // 结构化 JSON(新增) const structured = sliced.map((r, i) => ({ index: i + 1, title: r.title, url: r.url, snippet: r.snippet, engine: r.engine, reachable: r.reachable, ...(r._score !== undefined && { score: r._score }), ...(r._enhanced && { enhanced_snippet: true }), })); return { success: true, query: '', results: sliced, total: sliced.length, formatted, structured, from_cache: fromCache, engine_stats: engineStats || {}, }; } /** 解析百度 HTML 搜索结果 */ function parseBaiduResults(html: string, maxResults: number): Array<{ title: string; url: string; snippet: string }> { const results: Array<{ title: string; url: string; snippet: string }> = []; // 百度结果块:
// 标题:

内的 title // 摘要:
...
const blockRegex = /
]*>([\s\S]*?)(?=
]*class="[^"]*t[^"]*"[^>]*>[\s\S]*?]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/); if (!titleMatch) continue; let url = titleMatch[1].trim(); const title = decodeHTML(titleMatch[2].replace(/<[^>]+>/g, '').trim()); // 百度有时使用 data-url 属性 if (!url.startsWith('http')) { const dataUrlMatch = block.match(/data-url="(https?:\/\/[^"]*)"/); if (dataUrlMatch) url = dataUrlMatch[1]; } // 提取摘要 const snippetMatch = block.match(/
]*>([\s\S]*?)<\/div>/) || block.match(/]*>([\s\S]*?)<\/span>/); const snippet = snippetMatch ? decodeHTML(snippetMatch[1].replace(/<[^>]+>/g, '').trim()) : ''; if (title) { results.push({ title, url: url.startsWith('http') ? url : `https://www.baidu.com${url}`, snippet }); } } return results; } /** 解析搜狗 HTML 搜索结果 */ function parseSogouResults(html: string, maxResults: number): Array<{ title: string; url: string; snippet: string }> { const results: Array<{ title: string; url: string; snippet: string }> = []; // 搜狗结果块:
const blockRegex = /]*class="[^"]*(?:vrwrap|rb)[^"]*"[^>]*>([\s\S]*?)(?=]*class="[^"]*(?:vrwrap|rb)[^"]*"|]*id="pagebar)/gi; let blockMatch; while ((blockMatch = blockRegex.exec(html)) !== null && results.length < maxResults) { const block = blockMatch[1]; // 提取标题和 URL:标题 const linkMatch = block.match(/]*href="(https?:\/\/[^"]*)"[^>]*>([\s\S]*?)<\/a>/i); if (!linkMatch) continue; const url = decodeHTML(linkMatch[1].trim()); const title = decodeHTML(linkMatch[2].replace(/<[^>]+>/g, '').trim()); // 跳过搜狗自身链接 if (url.includes('sogou.com') || url.includes('sogo.com')) continue; // 提取摘要:

const snippetMatch = block.match(/]*class="[^"]*star-wiki[^"]*"[^>]*>([\s\S]*?)<\/p>/i) || block.match(/]*class="[^"]*space-txt[^"]*"[^>]*>([\s\S]*?)<\/div>/i) || block.match(/]*class="[^"]*str_info[^"]*"[^>]*>([\s\S]*?)<\/p>/i); const snippet = snippetMatch ? decodeHTML(snippetMatch[1].replace(/<[^>]+>/g, '').trim()) : ''; if (title && url.startsWith('http')) { results.push({ title, url, snippet }); } } return results; } /** 解析 360 搜索 HTML 搜索结果 */ function parse360Results(html: string, maxResults: number): Array<{ title: string; url: string; snippet: string }> { const results: Array<{ title: string; url: string; snippet: string }> = []; // 360 结果块:

  • const blockRegex = /<(?:li[^>]*class="[^"]*res-list[^"]*"|div[^>]*class="[^"]*result[^"]*")[^>]*>([\s\S]*?)(?=<(?:li[^>]*class="[^"]*res-list|div[^>]*class="[^"]*result)|<\/ul>|<\/div>\s*<\/div>\s*$)/gi; let blockMatch; while ((blockMatch = blockRegex.exec(html)) !== null && results.length < maxResults) { const block = blockMatch[1]; // 提取标题和 URL const linkMatch = block.match(/]*href="(https?:\/\/[^"]*)"[^>]*>([\s\S]*?)<\/a>/i); if (!linkMatch) continue; const url = decodeHTML(linkMatch[1].trim()); const title = decodeHTML(linkMatch[2].replace(/<[^>]+>/g, '').trim()); // 跳过 360 自身链接 if (url.includes('so.com') && !url.includes('www.so.com/link')) continue; // 提取摘要:

    const snippetMatch = block.match(/]*class="[^"]*res-desc[^"]*"[^>]*>([\s\S]*?)<\/p>/i) || block.match(/]*class="[^"]*res-rich[^"]*"[^>]*>([\s\S]*?)<\/div>/i) || block.match(/]*class="[^"]*res-summary[^"]*"[^>]*>([\s\S]*?)<\/p>/i) || block.match(/]*>([\s\S]*?)<\/dd>/i); const snippet = snippetMatch ? decodeHTML(snippetMatch[1].replace(/<[^>]+>/g, '').trim()) : ''; if (title && url.startsWith('http')) { results.push({ title, url, snippet }); } } return results; } /** 解析 Bing HTML 搜索结果 */ function parseBingResults(html: string, maxResults: number): Array<{ title: string; url: string; snippet: string }> { const results: Array<{ title: string; url: string; snippet: string }> = []; // 匹配

  • 块 const blockRegex = /
  • ]*>([\s\S]*?)<\/li>/gi; let blockMatch; while ((blockMatch = blockRegex.exec(html)) !== null && results.length < maxResults) { const block = blockMatch[1]; const titleMatch = block.match(/]*href="(https?:\/\/[^"]*)"[^>]*>([\s\S]*?)<\/a>/); const snippetMatch = block.match(/]*>([\s\S]*?)<\/p>/) || block.match(/
    ]*>([\s\S]*?)<\/div>/); if (titleMatch) { const url = titleMatch[1].trim(); const title = decodeHTML(titleMatch[2].replace(/<[^>]+>/g, '').trim()); const snippet = snippetMatch ? decodeHTML(snippetMatch[1].replace(/<[^>]+>/g, '').trim()) : ''; if (title && url.startsWith('http')) { results.push({ title, url, snippet }); } } } return results; } /** 简单 HTML 实体解码(用于搜索结果解析) */ function decodeHTML(text: string): string { return decodeHTMLEntities(text); } export async function handleDownloadFile(params: { url: string; destination: string }): Promise { try { const destPath = resolvePath(params.destination); const allowed = checkPathAllowed(destPath, 'write'); if (!allowed.ok) return { success: false, error: allowed.reason }; if (!params.url.startsWith('http://') && !params.url.startsWith('https://')) { return { success: false, error: '仅支持 http/https 协议' }; } sendLog('info', `⬇️ download_file`, `${params.url} → ${destPath}`); // 带超时的下载(大文件给更长超时) const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 60_000); let resp: Response; try { resp = await fetch(params.url, { signal: controller.signal }); } finally { clearTimeout(timeoutId); } if (!resp.ok) return { success: false, error: `HTTP ${resp.status}: ${resp.statusText}` }; const buf = Buffer.from(await resp.arrayBuffer()); if (buf.length > 50 * 1024 * 1024) { return { success: false, error: '文件超过 50MB 限制' }; } const destDir = path.dirname(destPath); await fs.mkdir(destDir, { recursive: true }); await fs.writeFile(destPath, buf); sendLog('success', `⬇️ download_file 完成`, `${buf.length} bytes`); return { success: true, url: params.url, destination: destPath, size: buf.length, content_type: resp.headers.get('content-type') || 'unknown' }; } catch (err) { const errMsg = (err as Error).message; if (errMsg.includes('abort')) { return { success: false, error: '下载超时 (60s)' }; } return { success: false, error: errMsg }; } } export async function handleCompress(params: { action: string; path: string; destination?: string; format?: string }): Promise { try { const format = params.format || 'tar.gz'; if (params.action === 'create') { const srcPath = resolvePath(params.path); const check = checkPathAllowed(srcPath, 'read'); if (!check.ok) return { success: false, error: check.reason }; const destPath = params.destination ? resolvePath(params.destination) : srcPath + (format === 'zip' ? '.zip' : '.tar.gz'); const destCheck = checkPathAllowed(destPath, 'write'); if (!destCheck.ok) return { success: false, error: destCheck.reason }; return new Promise((resolve) => { const cmd = format === 'zip' ? `zip -r "${destPath}" "${path.basename(srcPath)}"` : `tar czf "${destPath}" "${path.basename(srcPath)}"`; const proc = spawn('bash', ['-c', cmd], { cwd: path.dirname(srcPath), stdio: ['pipe', 'pipe', 'pipe'] }); let stderr = ''; proc.stderr.on('data', (d: Buffer) => { stderr += d.toString(); }); proc.on('close', (code) => { if (code === 0) { fs.stat(destPath).then(s => { resolve({ success: true, action: 'create', archive: destPath, size: s.size }); }).catch(() => resolve({ success: true, action: 'create', archive: destPath })); } else { resolve({ success: false, error: stderr || `压缩失败 (exit ${code})` }); } }); proc.on('error', (err) => resolve({ success: false, error: err.message })); }); } else { // extract const archivePath = resolvePath(params.path); const check = checkPathAllowed(archivePath, 'read'); if (!check.ok) return { success: false, error: check.reason }; const destDir = params.destination ? resolvePath(params.destination) : path.dirname(archivePath); const destCheck = checkPathAllowed(destDir, 'write'); if (!destCheck.ok) return { success: false, error: destCheck.reason }; await fs.mkdir(destDir, { recursive: true }); return new Promise((resolve) => { const isZip = archivePath.endsWith('.zip'); const cmd = isZip ? `unzip -o "${archivePath}" -d "${destDir}"` : `tar xzf "${archivePath}" -C "${destDir}"`; const proc = spawn('bash', ['-c', cmd], { stdio: ['pipe', 'pipe', 'pipe'] }); let stderr = ''; proc.stderr.on('data', (d: Buffer) => { stderr += d.toString(); }); proc.on('close', (code) => { resolve(code === 0 ? { success: true, action: 'extract', destination: destDir } : { success: false, error: stderr || `解压失败 (exit ${code})` } ); }); proc.on('error', (err) => resolve({ success: false, error: err.message })); }); } } catch (err) { return { success: false, error: (err as Error).message }; } } // ── datetime: 获取系统精确时间 ── export function handleDateTime(params: { format?: string; timezone?: string }): ToolResult { try { const now = new Date(); const tz = params.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone; const fmt = params.format || 'full'; const iso = now.toISOString(); const unixSec = Math.floor(now.getTime() / 1000); const unixMs = now.getTime(); // 中文日期(无前导零月份) const y = now.getFullYear(); const m = now.getMonth() + 1; const d = now.getDate(); const weekdays = ['日', '一', '二', '三', '四', '五', '六']; const weekday = `星期${weekdays[now.getDay()]}`; const dateStr = `${y}年${m}月${d}日 ${weekday}`; // 24小时制时间 const h = now.getHours(); const min = now.getMinutes(); const sec = now.getSeconds(); const time24 = `${String(h).padStart(2, '0')}:${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}`; // 时段 const period = h < 6 ? '凌晨' : h < 12 ? '上午' : h < 14 ? '中午' : h < 18 ? '下午' : '晚上'; // 人性化时间(12小时制 + 时段) const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h; const timeFriendly = `${period} ${h12}:${String(min).padStart(2, '0')}`; // 完整人性化字符串 const friendly = `${y}年${m}月${d}日 ${weekday} ${timeFriendly}`; let result: Record; switch (fmt) { case 'iso': result = { iso, timezone: tz }; break; case 'unix': result = { unix_seconds: unixSec, unix_milliseconds: unixMs, timezone: tz }; break; case 'date': result = { date: dateStr, weekday, timezone: tz }; break; case 'time': result = { time_24h: time24, period, time_friendly: timeFriendly, timezone: tz }; break; default: // full result = { iso, unix_seconds: unixSec, unix_milliseconds: unixMs, date: dateStr, weekday, time_24h: time24, period, time_friendly: timeFriendly, friendly, timezone: tz, year: y, month: m, day: d, hour: h, minute: min, second: sec, millisecond: now.getMilliseconds(), day_of_week: now.getDay(), }; } return { success: true, ...result }; } catch (err) { return { success: false, error: (err as Error).message }; } } // ── calculator: 安全数学计算器(纯 JS 递归下降解析,无 eval) ── export function handleCalculator(params: { expression: string }): ToolResult { try { const expr = params.expression; if (!expr || expr.length > 500) { return { success: false, error: '表达式为空或过长(最大500字符)' }; } const result = safeCalc(expr); return { success: true, expression: expr, result }; } catch (err) { return { success: false, error: (err as Error).message, expression: params.expression }; } } function safeCalc(expr: string): number { expr = expr.replace(/\s+/g, ''); if (!/^[\d+\-*/().%^]+$/.test(expr)) { throw new Error('表达式包含非法字符'); } let pos = 0; function parseExpression(): number { let left = parseTerm(); while (pos < expr.length) { if (expr[pos] === '+') { pos++; left += parseTerm(); } else if (expr[pos] === '-') { pos++; left -= parseTerm(); } else break; } return left; } function parseTerm(): number { let left = parsePower(); while (pos < expr.length) { if (expr[pos] === '*') { pos++; left *= parsePower(); } else if (expr[pos] === '/') { pos++; const d = parsePower(); if (d === 0) throw new Error('除数不能为零'); left /= d; } else if (expr[pos] === '%') { pos++; left %= parsePower(); } else break; } return left; } function parsePower(): number { let left = parseUnary(); while (pos < expr.length && expr[pos] === '*' && pos + 1 < expr.length && expr[pos + 1] === '*') { pos += 2; left = Math.pow(left, parseUnary()); } return left; } function parseUnary(): number { if (expr[pos] === '-') { pos++; return -parseAtom(); } if (expr[pos] === '+') { pos++; return parseAtom(); } return parseAtom(); } function parseAtom(): number { if (expr[pos] === '(') { pos++; const val = parseExpression(); if (pos >= expr.length || expr[pos] !== ')') throw new Error('缺少右括号'); pos++; return val; } const start = pos; while (pos < expr.length && /[\d.]/.test(expr[pos])) pos++; if (start === pos) throw new Error(`意外字符: ${expr[pos] || 'EOF'}`); const num = parseFloat(expr.slice(start, pos)); if (isNaN(num)) throw new Error(`无效数字: ${expr.slice(start, pos)}`); return num; } const result = parseExpression(); if (pos < expr.length) throw new Error(`表达式末尾有意外字符: ${expr.slice(pos)}`); if (!isFinite(result)) throw new Error('计算结果为无穷大'); return result; } // ── random: 随机数生成 ── export function handleRandom(params: { type?: string; min?: number; max?: number; count?: number; items?: string[]; length?: number }): ToolResult { try { const type = params.type || 'int'; switch (type) { case 'int': { const min = params.min ?? 0; const max = params.max ?? 100; if (min > max) return { success: false, error: 'min 不能大于 max' }; const count = Math.min(params.count ?? 1, 100); if (count === 1) { return { success: true, type: 'int', result: Math.floor(Math.random() * (max - min + 1)) + min, range: [min, max] }; } const results: number[] = []; for (let i = 0; i < count; i++) results.push(Math.floor(Math.random() * (max - min + 1)) + min); return { success: true, type: 'int', results, count, range: [min, max] }; } case 'float': { const min = params.min ?? 0; const max = params.max ?? 1; if (min > max) return { success: false, error: 'min 不能大于 max' }; const count = Math.min(params.count ?? 1, 100); if (count === 1) { return { success: true, type: 'float', result: Number((Math.random() * (max - min) + min).toFixed(6)), range: [min, max] }; } const results: number[] = []; for (let i = 0; i < count; i++) results.push(Number((Math.random() * (max - min) + min).toFixed(6))); return { success: true, type: 'float', results, count, range: [min, max] }; } case 'pick': { const items = params.items; if (!items || items.length === 0) return { success: false, error: 'pick 类型需要提供 items 数组' }; const count = Math.min(params.count ?? 1, items.length); // Fisher-Yates 部分洗牌 const pool = [...items]; const picked: string[] = []; for (let i = 0; i < count; i++) { const idx = Math.floor(Math.random() * pool.length); picked.push(pool[idx]); pool.splice(idx, 1); } return { success: true, type: 'pick', result: count === 1 ? picked[0] : picked, from: items.length, count }; } case 'string': { const length = Math.min(params.length ?? 8, 256); const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let s = ''; for (let i = 0; i < length; i++) s += chars.charAt(Math.floor(Math.random() * chars.length)); // 使用 crypto 增强随机性 const buf = require('crypto').randomBytes(Math.ceil(length * 0.75)); let bIdx = 0; let result = ''; for (let i = 0; i < length; i++) { const r = buf[bIdx++] || Math.floor(Math.random() * 256); if (bIdx >= buf.length) bIdx = 0; result += chars.charAt(r % chars.length); } return { success: true, type: 'string', result, length, charset: 'alphanumeric' }; } default: return { success: false, error: `未知类型: ${type}。支持: int / float / pick / string` }; } } catch (err) { return { success: false, error: (err as Error).message }; } } // ── uuid: 生成 UUID v4 ── export function handleUUID(params: { count?: number }): ToolResult { try { const crypto = require('crypto'); const count = Math.min(params.count ?? 1, 20); if (count === 1) { return { success: true, result: crypto.randomUUID() }; } const results: string[] = []; for (let i = 0; i < count; i++) results.push(crypto.randomUUID()); return { success: true, results, count }; } catch (err) { return { success: false, error: (err as Error).message }; } } // ── json_format: JSON 格式化 + 验证 ── export function handleJsonFormat(params: { json: string; indent?: number; sort_keys?: boolean }): ToolResult { try { const parsed = JSON.parse(params.json); const indent = params.indent ?? 2; let formatted: string; if (params.sort_keys) { formatted = JSON.stringify(sortObjectKeys(parsed), null, indent); } else { formatted = JSON.stringify(parsed, null, indent); } const size = Buffer.byteLength(formatted, 'utf-8'); return { success: true, formatted, original_size: params.json.length, formatted_size: size, keys: Object.keys(parsed).length }; } catch (err) { return { success: false, error: `JSON 解析失败: ${(err as Error).message}`, input_preview: params.json.slice(0, 200) }; } function sortObjectKeys(obj: any): any { if (Array.isArray(obj)) return obj.map(sortObjectKeys); if (obj !== null && typeof obj === 'object') { const sorted: Record = {}; for (const k of Object.keys(obj).sort()) sorted[k] = sortObjectKeys(obj[k]); return sorted; } return obj; } } // ── hash: 哈希计算 ── export function handleHash(params: { text: string; algorithm?: string }): ToolResult { try { const crypto = require('crypto'); const algo = (params.algorithm || 'sha256').toLowerCase().replace('-', ''); const validAlgos = ['md5', 'sha1', 'sha256', 'sha384', 'sha512']; if (!validAlgos.includes(algo)) { return { success: false, error: `不支持的算法: ${algo}。支持: ${validAlgos.join(', ')}` }; } const hash = crypto.createHash(algo).update(params.text, 'utf-8').digest('hex'); return { success: true, algorithm: algo, hash, input_length: params.text.length }; } catch (err) { return { success: false, error: (err as Error).message }; } }