feat(tools.js): 修复第三方服务商无法使用总结一下问题 && 为KIMI类官方模型添加工具调用(tool_calls) [#82]

This commit is contained in:
zhiyu1998 2025-08-02 21:30:18 +08:00
parent d45fa452ec
commit 8eb5bc80d5
4 changed files with 187 additions and 32 deletions

2
.gitignore vendored
View File

@ -23,3 +23,5 @@ yarn.lock
.vitepress .vitepress
bun.lockb bun.lockb
.next .next
CLAUDE.md
.claude

View File

@ -56,7 +56,8 @@ import {
TWITTER_TWEET_INFO, TWITTER_TWEET_INFO,
WEIBO_SINGLE_INFO, WEIBO_SINGLE_INFO,
WEISHI_VIDEO_INFO, WEISHI_VIDEO_INFO,
XHS_REQ_LINK XHS_REQ_LINK,
CRAWL_TOOL
} from "../constants/tools.js"; } from "../constants/tools.js";
import BiliInfoModel from "../model/bili-info.js"; import BiliInfoModel from "../model/bili-info.js";
import config from "../model/config.js"; import config from "../model/config.js";
@ -2644,33 +2645,115 @@ export class tools extends plugin {
if (e.msg.startsWith("#总结一下")) { if (e.msg.startsWith("#总结一下")) {
name = "网页总结"; name = "网页总结";
summaryLink = e.msg.replace("#总结一下", ""); // 如果需要进一步处理 summaryLink可以在这里添加相关逻辑 summaryLink = e.msg.replace("#总结一下", "");
} else { } else {
({ name: name, summaryLink: summaryLink } = contentEstimator(e.msg)); ({ name, summaryLink } = contentEstimator(e.msg));
} }
// 判断是否有总结的条件 // 判断是否有总结的条件
if (_.isEmpty(this.aiApiKey) || _.isEmpty(this.aiApiKey)) { if (_.isEmpty(this.aiApiKey)) {
// e.reply(`没有配置 Kimi无法为您总结${ HELP_DOC }`) // e.reply(`没有配置 Kimi无法为您总结${ HELP_DOC }`)
await this.tempSummary(name, summaryLink, e); await this.tempSummary(name, summaryLink, e);
return true; return true;
} }
const builder = await new OpenaiBuilder() const builder = await new OpenaiBuilder()
.setBaseURL(this.aiBaseURL) .setBaseURL(this.aiBaseURL)
.setApiKey(this.aiApiKey) .setApiKey(this.aiApiKey)
.setModel(this.aiModel) .setModel(this.aiModel)
.setPrompt(SUMMARY_PROMPT) .setPrompt(SUMMARY_PROMPT);
.build();
e.reply(`${ this.identifyPrefix }识别:${ name },正在为您总结,请稍等...`, true, { recallMsg: MESSAGE_RECALL_TIME }); if (this.aiModel.includes('deepseek')) {
const { ans: kimiAns, model } = await builder.kimi(summaryLink); builder.setProvider('deepseek');
}
await builder.build();
e.reply(`${this.identifyPrefix}识别:${name},正在为您总结,请稍等...`, true, { recallMsg: MESSAGE_RECALL_TIME });
let messages = [{ role: "user", content: summaryLink }];
// 兜底策略:检测模型是否支持 tool_calls
if (!this.aiModel.includes("kimi") && !this.aiModel.includes("moonshot")) {
// 不支持 tool_calls 的模型,直接爬取内容并总结
try {
// 直接使用llmRead爬取链接内容
const crawled_content = await llmRead(summaryLink);
// 重新构造消息,将爬取到的内容直接放入对话历史
messages = [
{ role: "user", content: `这是网页链接: ${summaryLink}` },
{ role: "assistant", content: `好的,我已经爬取了网页内容,内容如下:\n${crawled_content}` },
{ role: "user", content: "请根据以上内容进行总结。" }
];
// 调用kimi进行总结此时不传递任何工具
const response = await builder.chat(messages); // 不传递 CRAWL_TOOL
const { ans: kimiAns, model } = response;
// 估算阅读时间并提取标题
const stats = estimateReadingTime(kimiAns);
const titleMatch = kimiAns.match(/(Title|标题)([:])\s*(.*)/)?.[3];
e.reply(`${titleMatch || '未知标题'}》 预计阅读时间: ${stats.minutes} 分钟,总字数: ${stats.words}`);
// 将总结内容格式化为合并转发消息
const Msg = await Bot.makeForwardMsg(textArrayToMakeForward(e, [`「R插件 x ${model}」联合为您总结内容:`, kimiAns]));
await e.reply(Msg);
} catch (error) {
e.reply(`总结失败: ${error.message}`);
}
return true;
}
// 为了防止无限循环,设置一个最大循环次数
for (let i = 0; i < 5; i++) {
const response = await builder.chat(messages, [CRAWL_TOOL]);
// 如果Kimi返回了工具调用
if (response.tool_calls) {
const tool_calls = response.tool_calls;
messages.push({
role: 'assistant',
content: null,
tool_calls: tool_calls,
});
// 遍历并处理每一个工具调用
for (const tool_call of tool_calls) {
if (tool_call.function.name === 'crawl') {
try {
const args = JSON.parse(tool_call.function.arguments);
const urlToCrawl = args.url;
// 执行爬取操作
const crawled_content = await llmRead(urlToCrawl);
messages.push({
role: 'tool',
tool_call_id: tool_call.id,
name: 'crawl',
content: crawled_content,
});
} catch (error) {
messages.push({
role: 'tool',
tool_call_id: tool_call.id,
name: 'crawl',
content: `爬取错误: ${error.message}`,
});
}
}
}
} else {
// 如果没有工具调用,说明得到了最终的总结
const { ans: kimiAns, model } = response;
// 计算阅读时间 // 计算阅读时间
const stats = estimateReadingTime(kimiAns); const stats = estimateReadingTime(kimiAns);
const titleMatch = kimiAns.match(/(Title|标题)([:])\s*(.*?)\n/)?.[3]; const titleMatch = kimiAns.match(/(Title|标题)([:])\s*(.*?)\n/)?.[3];
e.reply(`${ titleMatch }》 预计阅读时间: ${ stats.minutes } 分钟,总字数: ${ stats.words }`); e.reply(`${titleMatch || '未知标题'}》 预计阅读时间: ${stats.minutes} 分钟,总字数: ${stats.words}`);
const Msg = await Bot.makeForwardMsg(textArrayToMakeForward(e, [`「R插件 x ${ model }」联合为您总结内容:`, kimiAns])); const Msg = await Bot.makeForwardMsg(textArrayToMakeForward(e, [`「R插件 x ${model}」联合为您总结内容:`, kimiAns]));
await e.reply(Msg); await e.reply(Msg);
return true; return true;
} }
}
e.reply("处理超出限制,请重试");
return true;
}
/** /**
* 临时AI接口 * 临时AI接口

View File

@ -259,3 +259,26 @@ export const PearAPI_CRAWLER = "https://api.pearktrue.cn/api/llmreader/?url={}&t
* @type {string} * @type {string}
*/ */
export const PearAPI_DEEPSEEK = "https://api.pearktrue.cn/api/deepseek/"; export const PearAPI_DEEPSEEK = "https://api.pearktrue.cn/api/deepseek/";
/**
* TOOL_CALL 爬虫工具
* 用于Kimi模型Tool-Calling的爬虫工具
* 当Kimi模型判断需要从网页获取信息时会调用此工具
*/
export const CRAWL_TOOL = {
type: "function",
function: {
name: "crawl",
description: "根据网站地址URL获取网页内容。",
parameters: {
type: "object",
required: ["url"],
properties: {
url: {
type: "string",
description: "需要获取内容的网站地址URL通常情况下从搜索结果中可以获取网站的地址。"
}
}
}
}
};

View File

@ -6,7 +6,7 @@ export class OpenaiBuilder {
this.apiKey = ""; // 默认API密钥 this.apiKey = ""; // 默认API密钥
this.prompt = "描述一下这个图片"; // 默认提示 this.prompt = "描述一下这个图片"; // 默认提示
this.model = 'claude-3-haiku-20240307' this.model = 'claude-3-haiku-20240307'
this.path = ''; // 上传文件的路径 this.provider = "kimi"; // 默认提供商
} }
setBaseURL(baseURL) { setBaseURL(baseURL) {
@ -29,6 +29,11 @@ export class OpenaiBuilder {
return this; return this;
} }
setProvider(provider) {
this.provider = provider;
return this;
}
setPath(path) { setPath(path) {
this.path = path; this.path = path;
return this; return this;
@ -48,24 +53,66 @@ export class OpenaiBuilder {
return this; return this;
} }
async kimi(query) { /**
// 请求Kimi * 调用与OpenAI兼容的API如Kimi/Moonshot
const completion = await this.client.post("/v1/chat/completions", { * @param {Array<Object>} messages - 发送给模型的消息列表
model: "moonshot-v1-8k", * @param {Array<Object>} [tools=[]] - (可选) 一个描述可供模型使用的工具的数组
messages: [ * @returns {Promise<Object>} 返回一个包含模型响应的对象如果模型决定调用工具则包含 'tool_calls' 字段否则包含 'ans' 文本响应
{ */
"role": "system", async chat(messages, tools = []) {
"content": this.prompt, if (this.provider === 'deepseek') {
}, const content = messages.find(m => m.role === 'user')?.content;
{ const ans = await deepSeekChat(content, this.prompt);
role: "user",
content: query
},
],
});
return { return {
"model": "月之暗面 Kimi", "model": "deepseek",
"ans": completion.data.choices[0].message.content "ans": ans
}
}
// 准备发送给API的消息
let requestMessages = [...messages];
// 检查是否已存在系统提示
const hasSystemPrompt = requestMessages.some(m => m.role === 'system');
// 如果没有系统提示并且builder中已设置则添加
if (!hasSystemPrompt && this.prompt) {
requestMessages.unshift({
role: 'system',
content: this.prompt,
});
}
// 构建API请求的负载
const payload = {
model: this.model, // 使用在builder中设置的模型
messages: requestMessages,
};
// 如果提供了工具,将其添加到负载中,并让模型自动决定是否使用
if (tools && tools.length > 0) {
payload.tools = tools;
payload.tool_choice = "auto";
}
// 发送POST请求到聊天完成端点
const completion = await this.client.post("/v1/chat/completions", payload);
const message = completion.data.choices[0].message;
// 从响应中获取实际使用的模型名称
const modelName = completion.data.model;
// 如果模型的响应中包含工具调用
if (message.tool_calls) {
return {
"model": modelName,
"tool_calls": message.tool_calls
}
}
// 否则,返回包含文本答案的响应
return {
"model": modelName,
"ans": message.content
} }
} }
} }