diff --git a/src/services/tools/codeExecution.ts b/src/services/tools/codeExecution.ts new file mode 100644 index 0000000..20c3488 --- /dev/null +++ b/src/services/tools/codeExecution.ts @@ -0,0 +1,223 @@ +/** + * Code Execution 工具服务 + * 使用 Piston API 实现代码执行 + * Piston API 文档: https://github.com/engineer-man/piston + */ + +export interface CodeExecutionInput { + code: string; + language: string; + stdin?: string; +} + +export interface CodeExecutionResponse { + success: boolean; + output?: string; + error?: string; + language?: string; + version?: string; +} + +// Piston API 支持的语言映射 +const LANGUAGE_MAP: Record = { + python: { language: 'python', version: '3.10.0' }, + python3: { language: 'python', version: '3.10.0' }, + javascript: { language: 'javascript', version: '18.15.0' }, + js: { language: 'javascript', version: '18.15.0' }, + typescript: { language: 'typescript', version: '5.0.3' }, + ts: { language: 'typescript', version: '5.0.3' }, + java: { language: 'java', version: '15.0.2' }, + c: { language: 'c', version: '10.2.0' }, + cpp: { language: 'c++', version: '10.2.0' }, + 'c++': { language: 'c++', version: '10.2.0' }, + go: { language: 'go', version: '1.16.2' }, + rust: { language: 'rust', version: '1.68.2' }, + ruby: { language: 'ruby', version: '3.0.1' }, + php: { language: 'php', version: '8.2.3' }, + swift: { language: 'swift', version: '5.3.3' }, + kotlin: { language: 'kotlin', version: '1.8.20' }, + scala: { language: 'scala', version: '3.2.2' }, + r: { language: 'r', version: '4.1.1' }, + bash: { language: 'bash', version: '5.2.0' }, + shell: { language: 'bash', version: '5.2.0' }, + sql: { language: 'sqlite3', version: '3.36.0' }, + lua: { language: 'lua', version: '5.4.4' }, + perl: { language: 'perl', version: '5.36.0' }, + haskell: { language: 'haskell', version: '9.0.1' }, + elixir: { language: 'elixir', version: '1.14.3' }, + clojure: { language: 'clojure', version: '1.10.3' }, +}; + +// Piston API 端点 +const PISTON_API_URL = 'https://emkc.org/api/v2/piston/execute'; + +/** + * 执行代码 + */ +export async function executeCode(input: CodeExecutionInput): Promise { + const { code, language, stdin } = input; + + // 获取语言映射 + const langConfig = LANGUAGE_MAP[language.toLowerCase()]; + if (!langConfig) { + return { + success: false, + error: `不支持的编程语言: ${language}。支持的语言: ${Object.keys(LANGUAGE_MAP).join(', ')}`, + }; + } + + try { + const response = await fetch(PISTON_API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + language: langConfig.language, + version: langConfig.version, + files: [ + { + name: getFileName(langConfig.language), + content: code, + }, + ], + stdin: stdin || '', + args: [], + compile_timeout: 10000, + run_timeout: 5000, + compile_memory_limit: -1, + run_memory_limit: -1, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Piston API error:', errorText); + return { + success: false, + error: `代码执行 API 错误: ${response.status}`, + }; + } + + const data = await response.json(); + + // 检查编译错误 + if (data.compile && data.compile.code !== 0) { + return { + success: false, + error: data.compile.stderr || data.compile.output || '编译错误', + language: langConfig.language, + version: langConfig.version, + }; + } + + // 检查运行错误 + if (data.run && data.run.code !== 0) { + return { + success: true, // 代码执行了,但有运行时错误 + output: data.run.stdout || '', + error: data.run.stderr || '', + language: langConfig.language, + version: langConfig.version, + }; + } + + return { + success: true, + output: data.run?.stdout || data.run?.output || '', + error: data.run?.stderr || '', + language: langConfig.language, + version: langConfig.version, + }; + } catch (error) { + console.error('Code execution error:', error); + return { + success: false, + error: error instanceof Error ? error.message : '未知错误', + }; + } +} + +/** + * 根据语言获取文件名 + */ +function getFileName(language: string): string { + const fileNameMap: Record = { + python: 'main.py', + javascript: 'main.js', + typescript: 'main.ts', + java: 'Main.java', + c: 'main.c', + 'c++': 'main.cpp', + go: 'main.go', + rust: 'main.rs', + ruby: 'main.rb', + php: 'main.php', + swift: 'main.swift', + kotlin: 'Main.kt', + scala: 'Main.scala', + r: 'main.r', + bash: 'main.sh', + sqlite3: 'main.sql', + lua: 'main.lua', + perl: 'main.pl', + haskell: 'Main.hs', + elixir: 'main.exs', + clojure: 'main.clj', + }; + + return fileNameMap[language] || 'main.txt'; +} + +/** + * 格式化代码执行结果为文本(完整版 - 发送给 AI) + */ +export function formatExecutionResult(response: CodeExecutionResponse): string { + if (!response.success && !response.output) { + return `❌ 执行失败: ${response.error}`; + } + + let result = ''; + + if (response.language && response.version) { + result += `**语言**: ${response.language} (v${response.version})\n\n`; + } + + if (response.output) { + result += `## 输出结果\n\`\`\`\n${response.output}\n\`\`\`\n`; + } + + if (response.error) { + result += `\n## 错误信息\n\`\`\`\n${response.error}\n\`\`\`\n`; + } + + return result || '执行完成,无输出'; +} + +/** + * 格式化代码执行结果为简短文本(显示给用户) + */ +export function formatExecutionResultShort(response: CodeExecutionResponse, language: string): string { + if (!response.success && !response.output) { + return `❌ 代码执行失败: ${response.error}`; + } + + const langDisplay = response.language || language; + const versionDisplay = response.version ? ` (v${response.version})` : ''; + + if (response.error && !response.output) { + return `⚠️ ${langDisplay}${versionDisplay} 代码执行出错`; + } + + // 计算输出行数 + const outputLines = response.output?.split('\n').length || 0; + + return `✅ ${langDisplay}${versionDisplay} 代码执行完成,输出 ${outputLines} 行`; +} + +/** + * 获取支持的语言列表 + */ +export function getSupportedLanguages(): string[] { + return Object.keys(LANGUAGE_MAP); +} diff --git a/src/services/tools/executor.ts b/src/services/tools/executor.ts new file mode 100644 index 0000000..0332020 --- /dev/null +++ b/src/services/tools/executor.ts @@ -0,0 +1,133 @@ +/** + * 统一工具执行器 + * 根据工具名称和输入参数执行对应的工具 + */ + +import { + webSearch, + formatSearchResults, + formatSearchResultsShort, + type WebSearchInput, + type WebSearchResponse, +} from './webSearch'; +import { + executeCode, + formatExecutionResult, + formatExecutionResultShort, + type CodeExecutionInput, + type CodeExecutionResponse, +} from './codeExecution'; +import { + webFetch, + formatFetchResult, + formatFetchResultShort, + type WebFetchInput, + type WebFetchResponse, +} from './webFetch'; + +export interface ToolExecutionResult { + success: boolean; + /** 完整结果 - 发送给 AI */ + fullResult: string; + /** 简短结果 - 显示给用户 */ + displayResult: string; + /** 原始数据 */ + rawData?: unknown; +} + +/** + * 执行工具 + * @param toolName 工具名称 + * @param input 工具输入参数 + * @returns 执行结果(包含完整版和简短版) + */ +export async function executeTool( + toolName: string, + input: Record +): Promise { + try { + switch (toolName) { + case 'web_search': { + const query = String(input.query || ''); + const searchInput: WebSearchInput = { query }; + const response: WebSearchResponse = await webSearch(searchInput); + return { + success: response.success, + fullResult: formatSearchResults(response), + displayResult: formatSearchResultsShort(response, query), + rawData: response, + }; + } + + case 'code_execution': { + const language = String(input.language || 'python'); + const codeInput: CodeExecutionInput = { + code: String(input.code || ''), + language, + stdin: input.stdin ? String(input.stdin) : undefined, + }; + const response: CodeExecutionResponse = await executeCode(codeInput); + return { + success: response.success, + fullResult: formatExecutionResult(response), + displayResult: formatExecutionResultShort(response, language), + rawData: response, + }; + } + + case 'web_fetch': { + const fetchInput: WebFetchInput = { + url: String(input.url || ''), + }; + const response: WebFetchResponse = await webFetch(fetchInput); + return { + success: response.success, + fullResult: formatFetchResult(response), + displayResult: formatFetchResultShort(response), + rawData: response, + }; + } + + default: + return { + success: false, + fullResult: `未知的工具: ${toolName}`, + displayResult: `❌ 未知的工具: ${toolName}`, + }; + } + } catch (error) { + console.error(`Tool execution error (${toolName}):`, error); + const errorMsg = error instanceof Error ? error.message : '未知错误'; + return { + success: false, + fullResult: `工具执行错误: ${errorMsg}`, + displayResult: `❌ 工具执行错误: ${errorMsg}`, + }; + } +} + +/** + * 获取所有可用工具的信息 + */ +export function getAvailableTools() { + return [ + { + id: 'web_search', + name: '网络搜索', + description: '搜索互联网获取最新信息', + icon: '🔍', + }, + { + id: 'code_execution', + name: '代码执行', + description: '执行代码并返回结果', + icon: '💻', + }, + { + id: 'web_fetch', + name: '网页获取', + description: '获取指定 URL 的网页内容', + icon: '🌐', + }, + ]; +} diff --git a/src/services/tools/index.ts b/src/services/tools/index.ts new file mode 100644 index 0000000..2d9e781 --- /dev/null +++ b/src/services/tools/index.ts @@ -0,0 +1,8 @@ +/** + * 工具服务统一导出 + */ + +export * from './webSearch'; +export * from './codeExecution'; +export * from './webFetch'; +export * from './executor'; diff --git a/src/services/tools/webFetch.ts b/src/services/tools/webFetch.ts new file mode 100644 index 0000000..f530236 --- /dev/null +++ b/src/services/tools/webFetch.ts @@ -0,0 +1,240 @@ +/** + * Web Fetch 工具服务 + * 使用 Jina Reader API 获取网页内容 + * Jina Reader API: https://r.jina.ai/{url} + */ + +export interface WebFetchInput { + url: string; +} + +export interface WebFetchResponse { + success: boolean; + title?: string; + content?: string; + url?: string; + error?: string; +} + +// Jina Reader API 端点 +const JINA_READER_API = 'https://r.jina.ai'; + +/** + * 获取网页内容 + */ +export async function webFetch(input: WebFetchInput): Promise { + const { url } = input; + + // 验证 URL + if (!isValidUrl(url)) { + return { + success: false, + error: '无效的 URL 格式', + }; + } + + try { + // 使用 Jina Reader API 获取网页内容 + const response = await fetch(`${JINA_READER_API}/${url}`, { + method: 'GET', + headers: { + 'Accept': 'text/plain', + 'X-Return-Format': 'markdown', + }, + }); + + if (!response.ok) { + // 尝试使用备用方法 + return await fallbackFetch(url); + } + + const content = await response.text(); + + // 解析标题(Jina Reader 返回的内容通常以标题开头) + const titleMatch = content.match(/^#\s*(.+)$/m); + const title = titleMatch ? titleMatch[1] : extractTitleFromUrl(url); + + return { + success: true, + title, + content: content.trim(), + url, + }; + } catch (error) { + console.error('Web fetch error:', error); + // 尝试备用方法 + return await fallbackFetch(url); + } +} + +/** + * 备用获取方法 - 直接获取 HTML 并提取文本 + */ +async function fallbackFetch(url: string): Promise { + try { + const response = await fetch(url, { + method: 'GET', + headers: { + 'User-Agent': 'Mozilla/5.0 (compatible; CCHCode-UI/1.0; +https://github.com/cchcode)', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + }, + }); + + if (!response.ok) { + return { + success: false, + error: `无法访问该网页: ${response.status} ${response.statusText}`, + url, + }; + } + + const html = await response.text(); + + // 提取标题 + const titleMatch = html.match(/]*>([^<]+)<\/title>/i); + const title = titleMatch ? titleMatch[1].trim() : extractTitleFromUrl(url); + + // 简单提取正文内容(移除脚本、样式和HTML标签) + const content = extractTextFromHtml(html); + + return { + success: true, + title, + content: content.slice(0, 10000), // 限制内容长度 + url, + }; + } catch (error) { + console.error('Fallback fetch error:', error); + return { + success: false, + error: error instanceof Error ? error.message : '获取网页内容失败', + url, + }; + } +} + +/** + * 验证 URL 格式 + */ +function isValidUrl(url: string): boolean { + try { + const parsed = new URL(url); + return ['http:', 'https:'].includes(parsed.protocol); + } catch { + return false; + } +} + +/** + * 从 URL 提取标题 + */ +function extractTitleFromUrl(url: string): string { + try { + const parsed = new URL(url); + return parsed.hostname; + } catch { + return url; + } +} + +/** + * 从 HTML 提取纯文本 + */ +function extractTextFromHtml(html: string): string { + // 移除脚本和样式 + let text = html + .replace(/]*>[\s\S]*?<\/script>/gi, '') + .replace(/]*>[\s\S]*?<\/style>/gi, '') + .replace(/]*>[\s\S]*?<\/noscript>/gi, '') + .replace(//g, ''); + + // 保留一些结构化标签 + text = text + .replace(/]*>/gi, '\n\n## ') + .replace(/<\/h[1-6]>/gi, '\n') + .replace(/]*>/gi, '\n') + .replace(/<\/p>/gi, '\n') + .replace(//gi, '\n') + .replace(/]*>/gi, '\n- ') + .replace(/<\/li>/gi, '') + .replace(/<[^>]+>/g, ' '); + + // 清理空白字符 + text = text + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/\s+/g, ' ') + .replace(/\n\s*\n/g, '\n\n') + .trim(); + + return text; +} + +/** + * 格式化网页获取结果为文本(完整版 - 发送给 AI) + */ +export function formatFetchResult(response: WebFetchResponse): string { + if (!response.success) { + return `❌ 获取失败: ${response.error}`; + } + + let result = ''; + + if (response.title) { + result += `# ${response.title}\n\n`; + } + + if (response.url) { + result += `**来源**: ${response.url}\n\n`; + } + + if (response.content) { + result += `---\n\n${response.content}`; + } + + return result || '获取完成,但页面无内容'; +} + +/** + * 格式化网页获取结果为简短文本(显示给用户) + */ +export function formatFetchResultShort(response: WebFetchResponse): string { + if (!response.success) { + return `❌ 网页获取失败: ${response.error}`; + } + + // 提取网站域名 + let domain = ''; + if (response.url) { + try { + const url = new URL(response.url); + domain = url.hostname.replace('www.', ''); + } catch { + domain = response.url; + } + } + + // 计算内容字数 + const contentLength = response.content?.length || 0; + const contentDesc = contentLength > 1000 + ? `${Math.round(contentLength / 1000)}k 字符` + : `${contentLength} 字符`; + + let result = `🌐 已获取网页内容`; + + if (response.title) { + result += `\n📄 标题: ${response.title.slice(0, 50)}${response.title.length > 50 ? '...' : ''}`; + } + + if (domain) { + result += `\n📎 来源: ${domain}`; + } + + result += `\n📊 内容: ${contentDesc}`; + + return result; +} diff --git a/src/services/tools/webSearch.ts b/src/services/tools/webSearch.ts new file mode 100644 index 0000000..bcb4cf8 --- /dev/null +++ b/src/services/tools/webSearch.ts @@ -0,0 +1,138 @@ +/** + * Web Search 工具服务 + * 使用 Tavily API 实现网络搜索 + */ + +export interface SearchResult { + title: string; + url: string; + content: string; + score: number; +} + +export interface WebSearchResponse { + success: boolean; + answer?: string; + results?: SearchResult[]; + error?: string; +} + +export interface WebSearchInput { + query: string; +} + +/** + * 执行网络搜索 + */ +export async function webSearch(input: WebSearchInput): Promise { + const apiKey = process.env.TAVILY_API_KEY; + + if (!apiKey) { + return { + success: false, + error: 'Tavily API key not configured', + }; + } + + try { + const response = await fetch('https://api.tavily.com/search', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + api_key: apiKey, + query: input.query, + search_depth: 'basic', + include_answer: true, + include_raw_content: false, + max_results: 5, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Tavily API error:', errorText); + return { + success: false, + error: `Search API error: ${response.status}`, + }; + } + + const data = await response.json(); + + return { + success: true, + answer: data.answer, + results: data.results?.map((r: { title: string; url: string; content: string; score: number }) => ({ + title: r.title, + url: r.url, + content: r.content, + score: r.score, + })), + }; + } catch (error) { + console.error('Web search error:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } +} + +/** + * 格式化搜索结果为文本(完整版 - 发送给 AI) + */ +export function formatSearchResults(response: WebSearchResponse): string { + if (!response.success) { + return `搜索失败: ${response.error}`; + } + + let result = ''; + + if (response.answer) { + result += `## 搜索摘要\n${response.answer}\n\n`; + } + + if (response.results && response.results.length > 0) { + result += `## 搜索结果\n\n`; + response.results.forEach((item, index) => { + result += `### ${index + 1}. ${item.title}\n`; + result += `**链接**: ${item.url}\n`; + result += `${item.content}\n\n`; + }); + } + + return result || '未找到相关结果'; +} + +/** + * 格式化搜索结果为简短文本(显示给用户) + */ +export function formatSearchResultsShort(response: WebSearchResponse, query: string): string { + if (!response.success) { + return `搜索失败: ${response.error}`; + } + + const resultCount = response.results?.length || 0; + + // 提取来源网站并生成 Markdown 链接(每个链接单独一行避免 autolink 合并) + const sourceLinks = response.results?.slice(0, 3).map((r, index) => { + try { + const url = new URL(r.url); + const hostname = url.hostname.replace('www.', ''); + // 使用序号 + Markdown 链接格式 + return `${index + 1}. [${hostname}](${r.url})`; + } catch { + return `${index + 1}. ${r.title.slice(0, 20)}`; + } + }) || []; + + let result = `> 🔍 已搜索「${query}」,找到 ${resultCount} 个相关结果`; + + if (sourceLinks.length > 0) { + result += `\n\n**🔗 来源:**\n${sourceLinks.join('\n')}`; + } + + return result; +}