mirror of
https://github.com/ikechan8370/chatgpt-plugin.git
synced 2025-12-17 13:57:10 +00:00
芜湖,更新了
更新了URL提取工具,更新一个二合一工具(参考了sf插件),现在可以搜索和代码执行(未测试)
This commit is contained in:
parent
9dafe4e343
commit
4d48f2be10
3 changed files with 441 additions and 2 deletions
260
utils/tools/MultiTool.js
Normal file
260
utils/tools/MultiTool.js
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
import { AbstractTool } from './AbstractTool.js';
|
||||
import fetch from 'node-fetch';
|
||||
import { Config } from '../config.js';
|
||||
import common from '../../../../lib/common/common.js';
|
||||
|
||||
/**
|
||||
* 多功能工具类 - 支持搜索和代码执行
|
||||
* @class MultiTool
|
||||
* @extends {AbstractTool}
|
||||
*/
|
||||
export class MultiTool extends AbstractTool {
|
||||
name = 'MultiTool';
|
||||
|
||||
parameters = {
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: '要处理的内容(可以是搜索查询或代码)',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
description: '操作类型:search(搜索)或 code(代码执行)',
|
||||
enum: ['search', 'code']
|
||||
},
|
||||
language: {
|
||||
type: 'string',
|
||||
description: '当type为code时的编程语言',
|
||||
},
|
||||
length: {
|
||||
type: 'integer',
|
||||
description: '当type为search时的摘要长度(句子数),默认为3',
|
||||
}
|
||||
},
|
||||
required: ['query', 'type'],
|
||||
};
|
||||
|
||||
description = '多功能工具:支持使用 Gemini API 进行智能搜索和代码执行。';
|
||||
|
||||
/**
|
||||
* 工具执行函数
|
||||
* @param {Object} opt - 工具参数
|
||||
* @param {string} opt.query - 处理内容
|
||||
* @param {string} opt.type - 操作类型
|
||||
* @param {string} [opt.language] - 编程语言
|
||||
* @param {number} [opt.length] - 摘要长度
|
||||
* @param {Object} e - 事件对象
|
||||
*/
|
||||
func = async function (opt, e) {
|
||||
const { query, type, language, length = 3 } = opt;
|
||||
|
||||
if (!query?.trim()) {
|
||||
throw new Error('处理内容不能为空');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.processRequest(query, type, language, length);
|
||||
console.debug(`[MultiTool] 处理结果:`, result);
|
||||
|
||||
// 构建转发消息
|
||||
const forwardMsg = this.constructForwardMessage(result, type);
|
||||
e.reply(await common.makeForwardMsg(e, forwardMsg, `${e.sender.card || e.sender.nickname || e.user_id}的${type === 'search' ? '搜索' : '代码执行'}结果`));
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('[MultiTool] 处理失败:', error);
|
||||
throw new Error(`操作失败: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理请求
|
||||
* @param {string} query - 处理内容
|
||||
* @param {string} type - 操作类型
|
||||
* @param {string} language - 编程语言
|
||||
* @param {number} length - 摘要长度
|
||||
*/
|
||||
async processRequest(query, type, language, 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 requestBody = {
|
||||
"systemInstruction": {
|
||||
"parts": [{
|
||||
"text": this.getSystemInstruction(type)
|
||||
}]
|
||||
},
|
||||
"contents": [{
|
||||
"parts": [{
|
||||
"text": this.constructPrompt(query, type, language, length)
|
||||
}],
|
||||
"role": "user"
|
||||
}],
|
||||
"tools": [{
|
||||
"googleSearch": {}
|
||||
}, {
|
||||
"code_execution": {}
|
||||
}],
|
||||
"generationConfig": {
|
||||
"temperature": 0.1,
|
||||
"topK": 1,
|
||||
"topP": 1,
|
||||
"maxOutputTokens": 2048,
|
||||
}
|
||||
};
|
||||
|
||||
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, type);
|
||||
} catch (error) {
|
||||
console.error('[MultiTool] API调用失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统指令
|
||||
* @param {string} type - 操作类型
|
||||
*/
|
||||
getSystemInstruction(type) {
|
||||
if (type === 'search') {
|
||||
return "你是一个有用的助手,你更喜欢说中文。你会根据用户的问题,通过搜索引擎获取最新的信息来回答问题。你的回答会尽可能准确、客观。";
|
||||
} else {
|
||||
return "你是一个代码执行助手。请执行用户提供的代码并返回结果。同时提供代码分析和改进建议。如果有错误,请提供详细的错误信息和修复方案。";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建提示词
|
||||
* @param {string} query - 处理内容
|
||||
* @param {string} type - 操作类型
|
||||
* @param {string} language - 编程语言
|
||||
* @param {number} length - 摘要长度
|
||||
*/
|
||||
constructPrompt(query, type, language, length) {
|
||||
if (type === 'search') {
|
||||
return `请对以下内容进行搜索并提供${length}句话的详细总结。
|
||||
需要搜索的内容: ${query}
|
||||
请确保回答准确、客观,并包含相关事实和信息。`;
|
||||
} else {
|
||||
return `请执行以下${language}代码并提供执行结果和分析:\n\`\`\`${language}\n${query}\n\`\`\`\n请提供:\n1. 代码执行结果\n2. 代码分析和可能的改进建议`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Gemini API 响应
|
||||
* @param {Object} data - API 响应数据
|
||||
* @param {string} type - 操作类型
|
||||
*/
|
||||
processGeminiResponse(data, type) {
|
||||
if (!data?.candidates?.[0]?.content?.parts) {
|
||||
throw new Error('无效的 API 响应');
|
||||
}
|
||||
|
||||
const response = data.candidates[0].content.parts
|
||||
.map(part => part.text)
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
|
||||
if (type === 'search') {
|
||||
let sources = [];
|
||||
if (data.candidates?.[0]?.groundingMetadata?.groundingChunks) {
|
||||
sources = data.candidates[0].groundingMetadata.groundingChunks
|
||||
.filter(chunk => chunk.web)
|
||||
.map(chunk => ({
|
||||
title: chunk.web.title || '未知标题',
|
||||
url: this.processUrl(chunk.web.uri)
|
||||
}))
|
||||
.filter((v, i, a) =>
|
||||
a.findIndex(t => (t.title === v.title && t.url === v.url)) === i
|
||||
);
|
||||
}
|
||||
return { answer: response, sources };
|
||||
} else {
|
||||
let output = '';
|
||||
let explanation = '';
|
||||
let error = null;
|
||||
|
||||
if (response.includes('执行结果:')) {
|
||||
const parts = response.split(/(?=执行结果:|代码分析:)/);
|
||||
parts.forEach(part => {
|
||||
if (part.startsWith('执行结果:')) {
|
||||
output = part.replace('执行结果:', '').trim();
|
||||
} else if (part.startsWith('代码分析:')) {
|
||||
explanation = part.replace('代码分析:', '').trim();
|
||||
}
|
||||
});
|
||||
} else if (response.includes('错误:')) {
|
||||
error = response;
|
||||
} else {
|
||||
output = response;
|
||||
}
|
||||
|
||||
return { output, explanation, error, executionTime: Date.now() };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理URL
|
||||
* @param {string} url - 原始URL
|
||||
*/
|
||||
processUrl(url) {
|
||||
if (url.includes('https://vertexaisearch.cloud.google.com/grounding-api-redirect')) {
|
||||
return url.replace(
|
||||
'https://vertexaisearch.cloud.google.com/grounding-api-redirect',
|
||||
'https://miao.news'
|
||||
);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建转发消息
|
||||
* @param {Object} result - 处理结果
|
||||
* @param {string} type - 操作类型
|
||||
*/
|
||||
constructForwardMessage(result, type) {
|
||||
const forwardMsg = [];
|
||||
|
||||
if (type === 'search') {
|
||||
const { answer, sources } = result;
|
||||
forwardMsg.push(answer);
|
||||
if (sources && sources.length > 0) {
|
||||
forwardMsg.push('信息来源:');
|
||||
sources.forEach((source, index) => {
|
||||
forwardMsg.push(`${index + 1}. ${source.title}\n${source.url}`);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const { output, explanation, error } = result;
|
||||
if (error) {
|
||||
forwardMsg.push(`执行出错:\n${error}`);
|
||||
} else {
|
||||
forwardMsg.push(`执行结果:\n${output}`);
|
||||
if (explanation) {
|
||||
forwardMsg.push(`\n代码分析:\n${explanation}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return forwardMsg;
|
||||
}
|
||||
}
|
||||
175
utils/tools/UrlExtractionTool.js
Normal file
175
utils/tools/UrlExtractionTool.js
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
import { AbstractTool } from './AbstractTool.js'
|
||||
import fetch from 'node-fetch'
|
||||
|
||||
// 假设的日志记录器,实际应用中应替换为专业的日志库
|
||||
const logger = {
|
||||
mark: console.log,
|
||||
error: console.error,
|
||||
};
|
||||
|
||||
/**
|
||||
* URL提取工具类
|
||||
* @class UrlExtractionTool
|
||||
* @extends {AbstractTool}
|
||||
*/
|
||||
export class UrlExtractionTool extends AbstractTool {
|
||||
// 工具名称
|
||||
name = 'UrlExtractionTool';
|
||||
|
||||
// 工具参数
|
||||
parameters = {
|
||||
properties: {
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'The message containing URLs to be extracted.',
|
||||
},
|
||||
},
|
||||
required: ['message'],
|
||||
};
|
||||
|
||||
// 工具描述
|
||||
description = 'Extracts URLs from a given message and retrieves the content of those URLs. Returns the extracted content to the AI.'; // 更新描述
|
||||
|
||||
/**
|
||||
* 工具执行函数
|
||||
* @param {Object} opt - 工具参数
|
||||
* @param {Object} ai - AI对象 (未使用)
|
||||
* @returns {Promise<string>} - 提取的URL内容
|
||||
*/
|
||||
func = async function (opt, ai) {
|
||||
let { message } = opt;
|
||||
if (!message) {
|
||||
return 'The message parameter is required.';
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await processMessageWithUrls(message);
|
||||
logger.mark(`[URL Extraction] Processed message: ${message}, Extracted content: ${result}`);
|
||||
return result; // 直接返回提取的内容
|
||||
} catch (error) {
|
||||
logger.error(`[URL Extraction] URL extraction failed: ${error.message}`);
|
||||
return `URL extraction failed, please check the logs. ${error.message}`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查URL是否为不需要提取内容的文件类型
|
||||
* @param {string} url URL地址
|
||||
* @returns {boolean} 是否为不需要提取的文件类型
|
||||
*/
|
||||
function isSkippedUrl(url) {
|
||||
// 使用 Set 存储扩展名以提高查找效率
|
||||
const skippedExtensions = new Set([
|
||||
// 图片
|
||||
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico', 'tiff', 'tif', 'raw', 'cr2', 'nef', 'arw', 'dng', 'heif', 'heic', 'avif', 'jfif', 'psd', 'ai',
|
||||
// 视频
|
||||
'mp4', 'webm', 'mkv', 'flv', 'avi', 'mov', 'wmv', 'rmvb', 'm4v', '3gp', 'mpeg', 'mpg', 'ts', 'mts',
|
||||
// 可执行文件和二进制文件
|
||||
'exe', 'msi', 'dll', 'sys', 'bin', 'dat', 'iso', 'img', 'dmg', 'pkg', 'deb', 'rpm', 'apk', 'ipa', 'jar', 'class', 'pyc', 'o', 'so', 'dylib',
|
||||
// 压缩文件
|
||||
'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'tgz', 'tbz', 'cab', 'ace', 'arc',
|
||||
]);
|
||||
|
||||
// 优化关键词匹配
|
||||
const skipKeywords = /\/(images?|photos?|pics?|videos?|medias?|downloads?|uploads?|binaries|assets)\//i;
|
||||
|
||||
// 从URL中提取扩展名
|
||||
const extension = url.split('.').pop().toLowerCase();
|
||||
|
||||
return skippedExtensions.has(extension) || skipKeywords.test(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文本中提取URL
|
||||
* @param {string} text 需要提取URL的文本
|
||||
* @returns {string[]} URL数组
|
||||
*/
|
||||
function extractUrls(text) {
|
||||
// 更精确的正则表达式来匹配 URL
|
||||
const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g;
|
||||
const matches = text.match(urlRegex) || [];
|
||||
|
||||
// 简化 URL 清理逻辑
|
||||
return matches.map(url => {
|
||||
try {
|
||||
// 解码URL
|
||||
return decodeURIComponent(url);
|
||||
} catch (e) {
|
||||
// 解码失败则返回原URL
|
||||
return url;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从URL提取内容
|
||||
* @param {string} url 需要提取内容的URL
|
||||
* @returns {Promise<Object>} 提取的内容
|
||||
*/
|
||||
async function extractUrlContent(url) {
|
||||
if (isSkippedUrl(url)) {
|
||||
logger.mark(`[URL提取]跳过不需要处理的URL类型: ${url}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.mark(`[URL提取]开始从URL获取内容: ${url}`);
|
||||
const response = await fetch(`https://lbl.news/api/extract?url=${encodeURIComponent(url)}`);
|
||||
if (!response.ok) {
|
||||
// 更详细的错误处理
|
||||
const errorMsg = await response.text();
|
||||
throw new Error(`提取内容失败: ${response.status} ${response.statusText} - ${errorMsg}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
logger.mark(`[URL提取]成功获取URL内容: ${url}`);
|
||||
return data;
|
||||
} catch (error) {
|
||||
logger.error(`[URL提取]提取内容失败: ${error.message}, URL: ${url}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理消息中的URL并提取内容
|
||||
* @param {string} message 用户消息
|
||||
* @returns {Promise<string>} 提取的URL内容
|
||||
*/
|
||||
async function processMessageWithUrls(message) {
|
||||
const urls = extractUrls(message);
|
||||
if (urls.length === 0) {
|
||||
return ''; // 没有 URL 则返回空字符串
|
||||
}
|
||||
|
||||
logger.mark(`[URL处理]从消息中提取到${urls.length}个URL`);
|
||||
let extractedContent = '';
|
||||
|
||||
// 使用 Promise.all 并发处理多个 URL
|
||||
const contents = await Promise.all(
|
||||
urls.map(async url => {
|
||||
if (isSkippedUrl(url)) {
|
||||
logger.mark(`[URL处理]跳过URL: ${url}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.mark(`[URL处理]开始处理URL: ${url}`);
|
||||
const content = await extractUrlContent(url);
|
||||
if (content) {
|
||||
logger.mark(`[URL处理]成功提取URL内容: ${url}`);
|
||||
// 格式化提取的内容
|
||||
return { url, content: content.content };
|
||||
}
|
||||
return null;
|
||||
})
|
||||
);
|
||||
|
||||
// 组合提取的内容
|
||||
contents.forEach(item => {
|
||||
if (item) {
|
||||
const urlContent = `\n\n提取的URL内容(${item.url}):\n内容: ${item.content}`;
|
||||
extractedContent += urlContent;
|
||||
}
|
||||
});
|
||||
|
||||
return extractedContent; // 返回提取的内容字符串
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue