feat(services): 添加 AI 工具服务实现
- codeExecution: 代码执行工具定义 - webFetch: 网页抓取工具定义 - webSearch: 网络搜索工具定义 - executor: 工具执行器统一处理
This commit is contained in:
parent
e4cdcc5141
commit
ab9dd5aff8
223
src/services/tools/codeExecution.ts
Normal file
223
src/services/tools/codeExecution.ts
Normal file
@ -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<string, { language: string; version: string }> = {
|
||||
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<CodeExecutionResponse> {
|
||||
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<string, string> = {
|
||||
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);
|
||||
}
|
||||
133
src/services/tools/executor.ts
Normal file
133
src/services/tools/executor.ts
Normal file
@ -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<string, unknown>
|
||||
): Promise<ToolExecutionResult> {
|
||||
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: '🌐',
|
||||
},
|
||||
];
|
||||
}
|
||||
8
src/services/tools/index.ts
Normal file
8
src/services/tools/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 工具服务统一导出
|
||||
*/
|
||||
|
||||
export * from './webSearch';
|
||||
export * from './codeExecution';
|
||||
export * from './webFetch';
|
||||
export * from './executor';
|
||||
240
src/services/tools/webFetch.ts
Normal file
240
src/services/tools/webFetch.ts
Normal file
@ -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<WebFetchResponse> {
|
||||
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<WebFetchResponse> {
|
||||
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[^>]*>([^<]+)<\/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(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
||||
.replace(/<noscript[^>]*>[\s\S]*?<\/noscript>/gi, '')
|
||||
.replace(/<!--[\s\S]*?-->/g, '');
|
||||
|
||||
// 保留一些结构化标签
|
||||
text = text
|
||||
.replace(/<h[1-6][^>]*>/gi, '\n\n## ')
|
||||
.replace(/<\/h[1-6]>/gi, '\n')
|
||||
.replace(/<p[^>]*>/gi, '\n')
|
||||
.replace(/<\/p>/gi, '\n')
|
||||
.replace(/<br\s*\/?>/gi, '\n')
|
||||
.replace(/<li[^>]*>/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;
|
||||
}
|
||||
138
src/services/tools/webSearch.ts
Normal file
138
src/services/tools/webSearch.ts
Normal file
@ -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<WebSearchResponse> {
|
||||
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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user