diff --git a/apps/bym.js b/apps/bym.js index c860f6d..337524c 100644 --- a/apps/bym.js +++ b/apps/bym.js @@ -18,7 +18,7 @@ import { KickOutTool } from '../utils/tools/KickOutTool.js' import { SetTitleTool } from '../utils/tools/SetTitleTool.js' import {SerpTool} from '../utils/tools/SerpTool.js' import { APTool } from '../utils/tools/APTool.js' -import { ContentSearchTool } from '../utils/tools/ContentSearchTool.js' +import { CustomSearchTool } from '../utils/tools/CustomSearchTool.js' import { UrlExtractionTool } from '../utils/tools/UrlExtractionTool.js' // 角色映射表 @@ -170,7 +170,7 @@ ${Object.values(faceMap).map(face => `[/${face}]`).join(',')} new APTool(), new WebsiteTool(), new UrlExtractionTool(), - new ContentSearchTool(), + new CustomSearchTool(), new WeatherTool() ] if (Config.azSerpKey) { diff --git a/utils/tools/ContentSearchTool.js b/utils/tools/ContentSearchTool.js deleted file mode 100644 index f457664..0000000 --- a/utils/tools/ContentSearchTool.js +++ /dev/null @@ -1,160 +0,0 @@ -import { AbstractTool } from './AbstractTool.js'; -import fetch from 'node-fetch'; -import { Config } from '../config.js'; - -/** - * 内容搜索和分析工具类 - 使用 Gemini API - * @class ContentSearchTool - * @extends {AbstractTool} - */ -export class ContentSearchTool extends AbstractTool { - name = 'ContentSearchTool'; - - parameters = { - properties: { - content: { // 改为 content 参数 - type: 'string', - description: '需要分析的文本内容', - }, - task: { // 新增 task 参数 - type: 'string', - description: '分析任务类型(如:总结、分析、问答等)', - default: 'summarize' - }, - length: { - type: 'integer', - description: '期望的输出长度(句子数),默认为3', - }, - }, - required: ['content'], - }; - - description = '使用 Gemini API 进行内容分析,支持文本总结、深度分析、问答等功能。'; - - /** - * 工具执行函数 - * @param {Object} opt - 工具参数 - * @param {string} opt.content - 需要分析的内容 - * @param {string} [opt.task='summarize'] - 分析任务类型 - * @param {number} [opt.length=3] - 输出长度 - * @returns {Promise} - 包含答案和分析的对象 - */ - func = async function (opt) { - const { content, task = 'summarize', length = 3 } = opt; - - if (!content?.trim()) { - throw new Error('分析内容不能为空'); - } - - try { - const result = await this.analyzeWithGemini(content, task, length); - console.log(`分析结果: ${JSON.stringify(result)}`); - return result; - } catch (error) { - console.error('内容分析失败:', error); - throw new Error(`内容分析失败: ${error.message}`); - } - }; - - /** - * 使用 Gemini API 进行内容分析 - * @param {string} content - 需要分析的内容 - * @param {string} task - 分析任务类型 - * @param {number} length - 输出长度 - * @returns {Promise} - 分析结果 - * @private - */ - async analyzeWithGemini(content, task, length) { - const apiKey = Config.geminiKey; - const apiBaseUrl = Config.geminiBaseUrl; - const apiUrl = `${apiBaseUrl}/v1beta/models/gemini-2.0-flash-exp:generateContent?key=${apiKey}`; - - if (!apiKey || !apiBaseUrl) { - throw new Error('Gemini API 配置缺失'); - } - - const response = await fetch(apiUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - contents: [{ - parts: [{ - text: this.constructPrompt(content, task, length) - }] - }], - tools: [{ - googleSearch: {} - }], - generationConfig: { - temperature: 0.7, - topP: 0.8, - topK: 40, - } - }) - }); - - const data = await response.json(); - - if (!response.ok) { - throw new Error(`API 请求失败: ${data.error?.message || '未知错误'}`); - } - - return this.processGeminiResponse(data); - } - - /** - * 构建分析提示词 - * @param {string} content - 需要分析的内容 - * @param {string} task - 分析任务类型 - * @param {number} length - 输出长度 - * @returns {string} - 格式化的提示词 - * @private - */ - constructPrompt(content, task, length) { - const taskPrompts = { - summarize: `Please provide a ${length} sentence summary of the following content:`, - analyze: `Please provide a ${length} point analysis of the following content:`, - qa: 'Please answer questions based on the following content:', - extract: 'Please extract key information from the following content:', - }; - - const prompt = taskPrompts[task] || taskPrompts.summarize; - return `${prompt}\n\nContent: ${content}`; - } - - /** - * 处理 Gemini API 响应 - * @param {Object} data - API 响应数据 - * @returns {Object} - 处理后的结果对象 - * @private - */ - processGeminiResponse(data) { - if (!data?.candidates?.[0]?.content?.parts?.[0]?.text) { - throw new Error('无效的 API 响应'); - } - - const analysis = data.candidates[0].content.parts[0].text; - - // 提取参考信息(如果有) - const references = data.candidates?.[0]?.groundingMetadata?.groundingChunks - ?.filter(chunk => chunk.web) - ?.map(chunk => ({ - title: chunk.web.title, - url: chunk.web.uri - })) - ?.filter((v, i, a) => - a.findIndex(t => (t.title === v.title && t.url === v.url)) === i - ) || []; - - return { - analysis, - references, - metadata: { - timestamp: new Date().toISOString(), - model: 'gemini-2.0-flash-exp' - } - }; - } -} \ No newline at end of file diff --git a/utils/tools/CustomSearchTool.js b/utils/tools/CustomSearchTool.js new file mode 100644 index 0000000..d614e4e --- /dev/null +++ b/utils/tools/CustomSearchTool.js @@ -0,0 +1,188 @@ +import { AbstractTool } from './AbstractTool.js'; +import fetch from 'node-fetch'; +import { Config } from '../config.js'; + +/** + * 自定义搜索工具类 - 使用 Gemini API + * @class CustomSearchTool + * @extends {AbstractTool} + */ +export class CustomSearchTool extends AbstractTool { + name = 'CustomSearchTool'; + + parameters = { + properties: { + query: { + type: 'string', + description: '要搜索的内容或关键词', + }, + length: { + type: 'integer', + description: '期望的摘要长度(句子数),默认为3', + }, + imageBase64: { + type: 'string', + description: '可选的图片base64数据', + } + }, + required: ['query'], + }; + + description = '使用 Gemini API 进行智能搜索,根据输入的内容或关键词提供全面的搜索结果和摘要。支持自定义摘要长度和图片识别。'; + + /** + * 工具执行函数 + * @param {Object} opt - 工具参数 + * @param {string} opt.query - 搜索内容或关键词 + * @param {number} [opt.length=3] - 摘要长度 + * @param {string|string[]} [opt.imageBase64] - 图片base64数据 + * @returns {Promise} - 包含答案和来源的对象 + */ + func = async function (opt) { + const { query, length = 3, imageBase64 } = opt; + + if (!query?.trim()) { + throw new Error('搜索内容或关键词不能为空'); + } + + try { + const result = await this.searchWithGemini(query, length, imageBase64); + console.debug(`[CustomSearchTool] 搜索结果:`, result); + return result; + } catch (error) { + console.error('[CustomSearchTool] 搜索失败:', error); + throw new Error(`搜索失败: ${error.message}`); + } + }; + + /** + * 使用 Gemini API 进行搜索 + * @param {string} query - 搜索内容或关键词 + * @param {number} length - 摘要长度 + * @param {string|string[]} [imageBase64] - 图片base64数据 + * @returns {Promise} - 包含答案和来源的对象 + * @private + */ + async searchWithGemini(query, length, imageBase64) { + const apiKey = Config.geminiKey; + const apiBaseUrl = Config.geminiBaseUrl; + const apiUrl = `${apiBaseUrl}/v1beta/models/gemini-2.0-flash-exp:generateContent?key=${apiKey}`; + + if (!apiKey || !apiBaseUrl) { + throw new Error('Gemini API 配置缺失'); + } + + const requestBody = { + "systemInstruction": { + "parts": [{ + "text": "你是一个有用的助手,你更喜欢说中文。你会根据用户的问题,通过搜索引擎获取最新的信息来回答问题。你的回答会尽可能准确、客观。" + }] + }, + "contents": [{ + "parts": [{ + "text": this.constructPrompt(query, length) + }], + "role": "user" + }], + "tools": [{ + "googleSearch": {} + }] + }; + + // 处理图片数据 + if (imageBase64) { + const imageArray = Array.isArray(imageBase64) ? imageBase64 : [imageBase64]; + imageArray.forEach(image => { + requestBody.contents[0].parts.push({ + "inline_data": { + "mime_type": "image/jpeg", + "data": image + } + }); + }); + } + + try { + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody) + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(`API 请求失败: ${data.error?.message || '未知错误'}`); + } + + return this.processGeminiResponse(data); + } catch (error) { + console.error('[CustomSearchTool] API调用失败:', error); + throw error; + } + } + + /** + * 构建提示词 + * @param {string} query - 搜索内容或关键词 + * @param {number} length - 摘要长度 + * @returns {string} - 格式化的提示词 + * @private + */ + constructPrompt(query, length) { + return `请对以下内容进行搜索并提供${length}句话的详细总结。 + 需要搜索的内容: ${query} + 请确保回答准确、客观,并包含相关事实和信息。`; + } + + /** + * 处理 Gemini API 响应 + * @param {Object} data - API 响应数据 + * @returns {Object} - 处理后的结果对象 + * @private + */ + processGeminiResponse(data) { + if (!data?.candidates?.[0]?.content?.parts) { + throw new Error('无效的 API 响应'); + } + + // 合并所有文本部分作为答案 + const answer = data.candidates[0].content.parts + .map(part => part.text) + .filter(Boolean) + .join('\n'); + + // 处理来源信息 + let sources = []; + if (data.candidates?.[0]?.groundingMetadata?.groundingChunks) { + sources = data.candidates[0].groundingMetadata.groundingChunks + .filter(chunk => chunk.web) + .map(chunk => { + let url = chunk.web.uri; + // 替换特定的URL前缀 + if (url.includes('https://vertexaisearch.cloud.google.com/grounding-api-redirect')) { + url = url.replace( + 'https://vertexaisearch.cloud.google.com/grounding-api-redirect', + 'https://miao.news' + ); + } + return { + title: chunk.web.title || '未知标题', + url: url + }; + }) + .filter((v, i, a) => + a.findIndex(t => (t.title === v.title && t.url === v.url)) === i + ); + } + + console.debug('[CustomSearchTool] 处理后的来源信息:', sources); + + return { + answer, + sources + }; + } +} \ No newline at end of file