From 79ab6cbd40ab4ca99a13b1affb8c632ec764d3b7 Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Fri, 8 Mar 2024 14:38:39 +0800 Subject: [PATCH] feat: support claude api fix #659 --- apps/button.js | 7 +- apps/chat.js | 502 ++++------------------------- apps/management.js | 70 ++++ apps/md.js | 2 +- apps/prompts.js | 17 +- client/ClaudeAPIClient.js | 188 +++++++++++ client/GoogleGeminiClient.js | 2 +- client/test/ClaudeApiClientTest.js | 27 ++ client/test/GozeClientTest.js | 2 +- guoba.support.js | 62 ++-- index.js | 25 +- model/conversation.js | 362 +++++++++++++++++++++ server/index.js | 113 ++++--- utils/common.js | 15 - utils/config.js | 10 +- utils/history.js | 14 + utils/translate.js | 4 +- 17 files changed, 859 insertions(+), 563 deletions(-) create mode 100644 client/ClaudeAPIClient.js create mode 100644 client/test/ClaudeApiClientTest.js create mode 100644 model/conversation.js create mode 100644 utils/history.js diff --git a/apps/button.js b/apps/button.js index 72b3a82..2596425 100644 --- a/apps/button.js +++ b/apps/button.js @@ -177,8 +177,11 @@ export class ChatGPTButtonHandler extends plugin { if (Config.chatglmRefreshToken) { buttons[buttons[0].length >= 4 ? 1 : 0].push(createButtonBase('ChatGLM4', '#glm4', false)) } - if (Config.claudeAISessionKey) { - buttons[buttons[0].length >= 4 ? 1 : 0].push(createButtonBase('Claude', '#claude.ai', false)) + // 两个claude只显示一个 优先API + if (Config.claudeApiKey) { + buttons[buttons[0].length >= 4 ? 1 : 0].push(createButtonBase('Claude', '#claude', false)) + } else if (Config.claudeAISessionKey) { + buttons[buttons[0].length >= 4 ? 1 : 0].push(createButtonBase('Claude.ai', '#claude.ai', false)) } rows.push({ buttons: buttons[0] diff --git a/apps/chat.js b/apps/chat.js index d01838e..ff57f56 100644 --- a/apps/chat.js +++ b/apps/chat.js @@ -8,7 +8,6 @@ import SydneyAIClient from '../utils/SydneyAIClient.js' import { PoeClient } from '../utils/poe/index.js' import AzureTTS from '../utils/tts/microsoft-azure.js' import VoiceVoxTTS from '../utils/tts/voicevox.js' -import Version from '../utils/version.js' import { completeJSON, extractContentFromFile, @@ -20,7 +19,6 @@ import { getImg, getMasterQQ, getMaxModelTokens, - getMessageById, getOrDownloadFile, getUin, getUserData, @@ -30,9 +28,9 @@ import { makeForwardMsg, randomString, render, - renderUrl, - upsertMessage + renderUrl } from '../utils/common.js' + import { ChatGPTPuppeteer } from '../utils/browser.js' import { KeyvFile } from 'keyv-file' import { OfficialChatGPTClient } from '../utils/message.js' @@ -41,8 +39,7 @@ import { deleteConversation, getConversations, getLatestMessageIdByConversationI import { convertSpeaker, speakers } from '../utils/tts.js' import ChatGLMClient from '../utils/chatglm.js' import { convertFaces } from '../utils/face.js' -import { SlackClaudeClient } from '../utils/slack/slackClient.js' -import { getPromptByName } from '../utils/prompts.js' +import { originalValues, ConversationManager } from '../model/conversation.js' import BingDrawClient from '../utils/BingDraw.js' import XinghuoClient from '../utils/xinghuo/xinghuo.js' import Bard from '../utils/bard.js' @@ -82,6 +79,8 @@ import { CustomGoogleGeminiClient } from '../client/CustomGoogleGeminiClient.js' import { resizeAndCropImage } from '../utils/dalle.js' import fs from 'fs' import { ChatGLM4Client } from '../client/ChatGLM4Client.js' +import { ClaudeAPIClient } from '../client/ClaudeAPIClient.js' +import { getMessageById, upsertMessage } from '../utils/history.js' const roleMap = { owner: 'group owner', @@ -107,8 +106,6 @@ try { let version = Config.version let proxy = getProxy() -const originalValues = ['星火', '通义千问', '克劳德', '克劳德2', '必应', 'api', 'API', 'api3', 'API3', 'glm', '巴德', '双子星', '双子座', '智谱'] -const correspondingValues = ['xh', 'qwen', 'claude', 'claude2', 'bing', 'api', 'api', 'api3', 'api3', 'chatglm', 'bard', 'gemini', 'gemini', 'chatglm4'] /** * 每个对话保留的时长。单个对话内ai是保留上下文的。超时后销毁对话,再次对话创建新的对话。 * 单位:秒 @@ -167,10 +164,6 @@ export class chatgpt extends plugin { /** 执行方法 */ fnc: 'bing' }, - { - reg: '^#claude开启新对话', - fnc: 'newClaudeConversation' - }, { /** 命令正则匹配 */ reg: '^#claude(2|3|.ai)[sS]*', @@ -339,360 +332,13 @@ export class chatgpt extends plugin { * @returns {Promise} */ async destroyConversations (e) { - const userData = await getUserData(e.user_id) - const match = e.msg.trim().match('^#?(.*)(结束|新开|摧毁|毁灭|完结)对话') - console.log(match[1]) - let use - if (match[1] && match[1] != 'chatgpt') { - use = correspondingValues[originalValues.indexOf(match[1])] - } else { - use = (userData.mode === 'default' ? null : userData.mode) || await redis.get('CHATGPT:USE') - } - console.log(use) - await redis.del(`CHATGPT:WRONG_EMOTION:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`) - if (use === 'claude') { - // let client = new SlackClaudeClient({ - // slackUserToken: Config.slackUserToken, - // slackChannelId: Config.slackChannelId - // }) - // await client.endConversation() - await redis.del(`CHATGPT:SLACK_CONVERSATION:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`) - await this.reply('claude对话已结束') - return - } - if (use === 'claude2') { - await redis.del(`CHATGPT:CLAUDE2_CONVERSATION:${e.sender.user_id}`) - await this.reply('claude2对话已结束') - return - } - if (use === 'xh') { - await redis.del(`CHATGPT:CONVERSATIONS_XH:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`) - await this.reply('星火对话已结束') - return - } - if (use === 'bard') { - await redis.del(`CHATGPT:CONVERSATIONS_BARD:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`) - await this.reply('Bard对话已结束') - return - } - let ats = e.message.filter(m => m.type === 'at') - const isAtMode = Config.toggleMode === 'at' - if (isAtMode) ats = ats.filter(item => item.qq !== getUin(e)) - if (ats.length === 0) { - if (use === 'api3') { - await redis.del(`CHATGPT:QQ_CONVERSATION:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`) - await this.reply('已退出当前对话,该对话仍然保留。请@我进行聊天以开启新的对话', true) - } else if (use === 'bing') { - let c = await redis.get(`CHATGPT:CONVERSATIONS_BING:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`) - if (!c) { - await this.reply('当前没有开启对话', true) - return - } else { - await redis.del(`CHATGPT:CONVERSATIONS_BING:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`) - } - const conversation = { - store: new KeyvFile({ filename: 'cache.json' }), - namespace: Config.toneStyle - } - let Keyv - try { - Keyv = (await import('keyv')).default - } catch (err) { - await this.reply('依赖keyv未安装,请执行pnpm install keyv', true) - } - const conversationsCache = new Keyv(conversation) - logger.info(`SydneyUser_${e.sender.user_id}`, await conversationsCache.get(`SydneyUser_${e.sender.user_id}`)) - await conversationsCache.delete(`SydneyUser_${e.sender.user_id}`) - await this.reply('已退出当前对话,该对话仍然保留。请@我进行聊天以开启新的对话', true) - } else if (use === 'chatglm') { - const conversation = { - store: new KeyvFile({ filename: 'cache.json' }), - namespace: 'chatglm_6b' - } - let Keyv - try { - Keyv = (await import('keyv')).default - } catch (err) { - await this.reply('依赖keyv未安装,请执行pnpm install keyv', true) - } - const conversationsCache = new Keyv(conversation) - logger.info(`ChatGLMUser_${e.sender.user_id}`, await conversationsCache.get(`ChatGLMUser_${e.sender.user_id}`)) - await conversationsCache.delete(`ChatGLMUser_${e.sender.user_id}`) - await this.reply('已退出当前对话,该对话仍然保留。请@我进行聊天以开启新的对话', true) - } else if (use === 'api') { - let c = await redis.get(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`) - if (!c) { - await this.reply('当前没有开启对话', true) - } else { - await redis.del(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`) - await this.reply('已结束当前对话,请@我进行聊天以开启新的对话', true) - } - } else if (use === 'qwen') { - let c = await redis.get(`CHATGPT:CONVERSATIONS_QWEN:${e.sender.user_id}`) - if (!c) { - await this.reply('当前没有开启对话', true) - } else { - await redis.del(`CHATGPT:CONVERSATIONS_QWEN:${e.sender.user_id}`) - await this.reply('已结束当前对话,请@我进行聊天以开启新的对话', true) - } - } else if (use === 'gemini') { - let c = await redis.get(`CHATGPT:CONVERSATIONS_GEMINI:${e.sender.user_id}`) - if (!c) { - await this.reply('当前没有开启对话', true) - } else { - await redis.del(`CHATGPT:CONVERSATIONS_GEMINI:${e.sender.user_id}`) - await this.reply('已结束当前对话,请@我进行聊天以开启新的对话', true) - } - } else if (use === 'chatglm4') { - let c = await redis.get(`CHATGPT:CONVERSATIONS_CHATGLM4:${e.sender.user_id}`) - if (!c) { - await this.reply('当前没有开启对话', true) - } else { - await redis.del(`CHATGPT:CONVERSATIONS_CHATGLM4:${e.sender.user_id}`) - await this.reply('已结束当前对话,请@我进行聊天以开启新的对话', true) - } - } else if (use === 'bing') { - let c = await redis.get(`CHATGPT:CONVERSATIONS_BING:${e.sender.user_id}`) - if (!c) { - await this.reply('当前没有开启对话', true) - } else { - await redis.del(`CHATGPT:CONVERSATIONS_BING:${e.sender.user_id}`) - await this.reply('已结束当前对话,请@我进行聊天以开启新的对话', true) - } - } else if (use === 'browser') { - let c = await redis.get(`CHATGPT:CONVERSATIONS_BROWSER:${e.sender.user_id}`) - if (!c) { - await this.reply('当前没有开启对话', true) - } else { - await redis.del(`CHATGPT:CONVERSATIONS_BROWSER:${e.sender.user_id}`) - await this.reply('已结束当前对话,请@我进行聊天以开启新的对话', true) - } - } - } else { - let at = ats[0] - let qq = at.qq - let atUser = _.trimStart(at.text, '@') - if (use === 'api3') { - await redis.del(`CHATGPT:QQ_CONVERSATION:${qq}`) - await this.reply(`${atUser}已退出TA当前的对话,TA仍可以@我进行聊天以开启新的对话`, true) - } else if (use === 'bing') { - const conversation = { - store: new KeyvFile({ filename: 'cache.json' }), - namespace: Config.toneStyle - } - let Keyv - try { - Keyv = (await import('keyv')).default - } catch (err) { - await this.reply('依赖keyv未安装,请执行pnpm install keyv', true) - } - const conversationsCache = new Keyv(conversation) - await conversationsCache.delete(`SydneyUser_${qq}`) - await this.reply('已退出当前对话,该对话仍然保留。请@我进行聊天以开启新的对话', true) - } else if (use === 'chatglm') { - const conversation = { - store: new KeyvFile({ filename: 'cache.json' }), - namespace: 'chatglm_6b' - } - let Keyv - try { - Keyv = (await import('keyv')).default - } catch (err) { - await this.reply('依赖keyv未安装,请执行pnpm install keyv', true) - } - const conversationsCache = new Keyv(conversation) - logger.info(`ChatGLMUser_${e.sender.user_id}`, await conversationsCache.get(`ChatGLMUser_${e.sender.user_id}`)) - await conversationsCache.delete(`ChatGLMUser_${qq}`) - await this.reply('已退出当前对话,该对话仍然保留。请@我进行聊天以开启新的对话', true) - } else if (use === 'api') { - let c = await redis.get(`CHATGPT:CONVERSATIONS:${qq}`) - if (!c) { - await this.reply(`当前${atUser}没有开启对话`, true) - } else { - await redis.del(`CHATGPT:CONVERSATIONS:${qq}`) - await this.reply(`已结束${atUser}的对话,TA仍可以@我进行聊天以开启新的对话`, true) - } - } else if (use === 'qwen') { - let c = await redis.get(`CHATGPT:CONVERSATIONS_QWEN:${qq}`) - if (!c) { - await this.reply(`当前${atUser}没有开启对话`, true) - } else { - await redis.del(`CHATGPT:CONVERSATIONS_QWEN:${qq}`) - await this.reply(`已结束${atUser}的对话,TA仍可以@我进行聊天以开启新的对话`, true) - } - } else if (use === 'gemini') { - let c = await redis.get(`CHATGPT:CONVERSATIONS_GEMINI:${qq}`) - if (!c) { - await this.reply(`当前${atUser}没有开启对话`, true) - } else { - await redis.del(`CHATGPT:CONVERSATIONS_GEMINI:${qq}`) - await this.reply(`已结束${atUser}的对话,TA仍可以@我进行聊天以开启新的对话`, true) - } - } else if (use === 'chatglm4') { - let c = await redis.get(`CHATGPT:CONVERSATIONS_CHATGLM4:${qq}`) - if (!c) { - await this.reply(`当前${atUser}没有开启对话`, true) - } else { - await redis.del(`CHATGPT:CONVERSATIONS_CHATGLM4:${qq}`) - await this.reply(`已结束${atUser}的对话,TA仍可以@我进行聊天以开启新的对话`, true) - } - } else if (use === 'bing') { - let c = await redis.get(`CHATGPT:CONVERSATIONS_BING:${qq}`) - if (!c) { - await this.reply(`当前${atUser}没有开启对话`, true) - } else { - await redis.del(`CHATGPT:CONVERSATIONS_BING:${qq}`) - await this.reply(`已结束${atUser}的对话,TA仍可以@我进行聊天以开启新的对话`, true) - } - } else if (use === 'browser') { - let c = await redis.get(`CHATGPT:CONVERSATIONS_BROWSER:${qq}`) - if (!c) { - await this.reply(`当前${atUser}没有开启对话`, true) - } else { - await redis.del(`CHATGPT:CONVERSATIONS_BROWSER:${qq}`) - await this.reply(`已结束${atUser}的对话,TA仍可以@我进行聊天以开启新的对话`, true) - } - } - } + let manager = new ConversationManager(e) + await manager.endConversation.bind(this)(e) } async endAllConversations (e) { - const match = e.msg.trim().match('^#?(.*)(结束|新开|摧毁|毁灭|完结)全部对话') - console.log(match[1]) - let use - if (match[1] && match[1] != 'chatgpt') { - use = correspondingValues[originalValues.indexOf(match[1])] - } else { - use = await redis.get('CHATGPT:USE') || 'api' - } - console.log(use) - let deleted = 0 - switch (use) { - case 'claude': { - let cs = await redis.keys('CHATGPT:SLACK_CONVERSATION:*') - let we = await redis.keys('CHATGPT:WRONG_EMOTION:*') - for (let i = 0; i < cs.length; i++) { - await redis.del(cs[i]) - if (Config.debug) { - logger.info('delete slack conversation of qq: ' + cs[i]) - } - deleted++ - } - for (const element of we) { - await redis.del(element) - } - break - } - case 'xh': { - let cs = await redis.keys('CHATGPT:CONVERSATIONS_XH:*') - for (let i = 0; i < cs.length; i++) { - await redis.del(cs[i]) - if (Config.debug) { - logger.info('delete xh conversation of qq: ' + cs[i]) - } - deleted++ - } - break - } - case 'bard': { - let cs = await redis.keys('CHATGPT:CONVERSATIONS_BARD:*') - for (let i = 0; i < cs.length; i++) { - await redis.del(cs[i]) - if (Config.debug) { - logger.info('delete bard conversation of qq: ' + cs[i]) - } - deleted++ - } - break - } - case 'bing': { - let cs = await redis.keys('CHATGPT:CONVERSATIONS_BING:*') - let we = await redis.keys('CHATGPT:WRONG_EMOTION:*') - for (let i = 0; i < cs.length; i++) { - await redis.del(cs[i]) - if (Config.debug) { - logger.info('delete bing conversation of qq: ' + cs[i]) - } - deleted++ - } - for (const element of we) { - await redis.del(element) - } - break - } - case 'api': { - let cs = await redis.keys('CHATGPT:CONVERSATIONS:*') - for (let i = 0; i < cs.length; i++) { - await redis.del(cs[i]) - if (Config.debug) { - logger.info('delete api conversation of qq: ' + cs[i]) - } - deleted++ - } - break - } - case 'api3': { - let qcs = await redis.keys('CHATGPT:QQ_CONVERSATION:*') - for (let i = 0; i < qcs.length; i++) { - await redis.del(qcs[i]) - // todo clean last message id - if (Config.debug) { - logger.info('delete conversation bind: ' + qcs[i]) - } - deleted++ - } - break - } - case 'chatglm': { - let qcs = await redis.keys('CHATGPT:CONVERSATIONS_CHATGLM:*') - for (let i = 0; i < qcs.length; i++) { - await redis.del(qcs[i]) - // todo clean last message id - if (Config.debug) { - logger.info('delete chatglm conversation bind: ' + qcs[i]) - } - deleted++ - } - break - } - case 'qwen': { - let qcs = await redis.keys('CHATGPT:CONVERSATIONS_QWEN:*') - for (let i = 0; i < qcs.length; i++) { - await redis.del(qcs[i]) - // todo clean last message id - if (Config.debug) { - logger.info('delete qwen conversation bind: ' + qcs[i]) - } - deleted++ - } - break - } - case 'gemini': { - let qcs = await redis.keys('CHATGPT:CONVERSATIONS_GEMINI:*') - for (let i = 0; i < qcs.length; i++) { - await redis.del(qcs[i]) - // todo clean last message id - if (Config.debug) { - logger.info('delete gemini conversation bind: ' + qcs[i]) - } - deleted++ - } - break - } - case 'chatglm4': { - let qcs = await redis.keys('CHATGPT:CONVERSATIONS_CHATGLM4:*') - for (let i = 0; i < qcs.length; i++) { - await redis.del(qcs[i]) - // todo clean last message id - if (Config.debug) { - logger.info('delete chatglm4 conversation bind: ' + qcs[i]) - } - deleted++ - } - break - } - } - await this.reply(`结束了${deleted}个用户的对话。`, true) + let manager = new ConversationManager(e) + await manager.endAllConversations.bind(this)(e) } async deleteConversation (e) { @@ -1132,7 +778,7 @@ export class chatgpt extends plugin { num: 0 } } - } else if (use !== 'poe' && use !== 'claude') { + } else if (use !== 'poe') { switch (use) { case 'api': { key = `CHATGPT:CONVERSATIONS:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}` @@ -1174,6 +820,10 @@ export class chatgpt extends plugin { key = `CHATGPT:CONVERSATIONS_GEMINI:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}` break } + case 'claude': { + key = `CHATGPT:CONVERSATIONS_CLAUDE:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}` + break + } case 'chatglm4': { key = `CHATGPT:CONVERSATIONS_CHATGLM4:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}` break @@ -1224,7 +874,7 @@ export class chatgpt extends plugin { // 字数超限直接返回 return false } - if (use !== 'api3' && use !== 'poe' && use !== 'claude') { + if (use !== 'api3' && use !== 'poe') { previousConversation.conversation = { conversationId: chatMessage.conversationId } @@ -1258,7 +908,7 @@ export class chatgpt extends plugin { } let response = chatMessage?.text?.replace('\n\n\n', '\n') // 过滤无法正常显示的emoji - if (use === 'claude') response = response.replace(/:[a-zA-Z_]+:/g, '') + // if (use === 'claude') response = response.replace(/:[a-zA-Z_]+:/g, '') let mood = 'blandness' if (!response) { await this.reply('没有任何回复', true) @@ -1347,7 +997,7 @@ export class chatgpt extends plugin { }) } // 处理内容和引用中的图片 - const regex = /\b((?:https?|ftp|file):\/\/[-a-zA-Z0-9+&@#\/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#\/%=~_|])/g + const regex = /\b((?:https?|ftp|file):\/\/[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|])/g let responseUrls = response.match(regex) let imgUrls = [] if (responseUrls) { @@ -1439,10 +1089,6 @@ export class chatgpt extends plugin { this.reply('当前对话超过上限,已重置对话', false, { at: true }) await redis.del(`CHATGPT:CONVERSATIONS_BING:${e.sender.user_id}`) return false - } else if (response === 'Unexpected message author.') { - this.reply('无法回答当前话题,已重置对话', false, { at: true }) - await redis.del(`CHATGPT:CONVERSATIONS_BING:${e.sender.user_id}`) - return false } else if (response === 'Throttled: Request is throttled.') { this.reply('今日对话已达上限') return false @@ -1476,7 +1122,7 @@ export class chatgpt extends plugin { } } catch (err) { logger.error(err) - if (use !== 'bing') { + if (use === 'api3') { // 异常了也要腾地方(todo 大概率后面的也会异常,要不要一口气全杀了) await redis.lPop('CHATGPT:CHAT_QUEUE', 0) } @@ -1484,11 +1130,11 @@ export class chatgpt extends plugin { await this.destroyConversations(err) await this.reply('当前对话异常,已经清除,请重试', true, { recallMsg: e.isGroup ? 10 : 0 }) } else { - if (err.length < 200) { - await this.reply(`出现错误:${err}`, true, { recallMsg: e.isGroup ? 10 : 0 }) + let errorMessage = err?.message || err?.data?.message || (typeof (err) === 'object' ? JSON.stringify(err) : err) || '未能确认错误类型!' + if (errorMessage.length < 200) { + await this.reply(`出现错误:${errorMessage}`, true, { recallMsg: e.isGroup ? 10 : 0 }) } else { - // 这里是否还需要上传到缓存服务器呐?多半是代理服务器的问题,本地也修不了,应该不用吧。 - await this.renderImage(e, use, `通信异常,错误信息如下 \n \`\`\`${err?.message || err?.data?.message || (typeof (err) === 'object' ? JSON.stringify(err) : err) || '未能确认错误类型!'}\`\`\``, prompt) + await this.renderImage(e, use, `出现异常,错误信息如下 \n \`\`\`${errorMessage}\`\`\``, prompt) } } } @@ -1535,6 +1181,9 @@ export class chatgpt extends plugin { } async cacheContent (e, use, content, prompt, quote = [], mood = '', suggest = '', imgUrls = []) { + if (!Config.enableToolbox) { + return + } let cacheData = { file: '', status: '' } cacheData.file = randomString() const cacheresOption = { @@ -1544,8 +1193,8 @@ export class chatgpt extends plugin { }, body: JSON.stringify({ content: { - content: new Buffer.from(content).toString('base64'), - prompt: new Buffer.from(prompt).toString('base64'), + content: Buffer.from(content).toString('base64'), + prompt: Buffer.from(prompt).toString('base64'), senderName: e.sender.nickname, style: Config.toneStyle, mood, @@ -1576,7 +1225,7 @@ export class chatgpt extends plugin { async renderImage (e, use, content, prompt, quote = [], mood = '', suggest = '', imgUrls = []) { let cacheData = await this.cacheContent(e, use, content, prompt, quote, mood, suggest, imgUrls) - const template = use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index' + // const template = use !== 'bing' ? 'content/ChatGPT/index' : 'content/Bing/index' if (cacheData.error || cacheData.status != 200) { await this.reply(`出现错误:${cacheData.error || 'server error ' + cacheData.status}`, true) } else { await this.reply(await renderUrl(e, (Config.viewHost ? `${Config.viewHost}/` : `http://127.0.0.1:${Config.serverPort || 3321}/`) + `page/${cacheData.file}?qr=${Config.showQRCode ? 'true' : 'false'}`, { retType: Config.quoteReply ? 'base64' : '', Viewport: { width: parseInt(Config.chatViewWidth), height: parseInt(parseInt(Config.chatViewWidth) * 0.56) }, func: (parseFloat(Config.live2d) && !Config.viewHost) ? 'window.Live2d == true' : '', deviceScaleFactor: parseFloat(Config.cloudDPR) }), e.isGroup && Config.quoteReply) } } @@ -1902,28 +1551,43 @@ export class chatgpt extends plugin { text: response.data } } else if (use === 'claude') { - let client = new SlackClaudeClient({ - slackUserToken: Config.slackUserToken, - slackChannelId: Config.slackChannelId + // slack已经不可用,移除 + // let client = new SlackClaudeClient({ + // slackUserToken: Config.slackUserToken, + // slackChannelId: Config.slackChannelId + // }) + // let conversationId = await redis.get(`CHATGPT:SLACK_CONVERSATION:${e.sender.user_id}`) + // if (!conversationId) { + // // 如果是新对话 + // if (Config.slackClaudeEnableGlobalPreset && (useCast?.slack || Config.slackClaudeGlobalPreset)) { + // // 先发送设定 + // let prompt = (useCast?.slack || Config.slackClaudeGlobalPreset) + // let emotion = await AzureTTS.getEmotionPrompt(e) + // if (emotion) { + // prompt = prompt + '\n' + emotion + // } + // await client.sendMessage(prompt, e) + // logger.info('claudeFirst:', prompt) + // } + // } + // let text = await client.sendMessage(prompt, e) + // return { + // text + // } + const client = new ClaudeAPIClient({ + key: Config.claudeApiKey, + model: Config.claudeApiModel || 'claude-3-sonnet-20240229', + debug: true, + baseUrl: Config.claudeApiBaseUrl + // temperature: Config.claudeApiTemperature || 0.5 }) - let conversationId = await redis.get(`CHATGPT:SLACK_CONVERSATION:${e.sender.user_id}`) - if (!conversationId) { - // 如果是新对话 - if (Config.slackClaudeEnableGlobalPreset && (useCast?.slack || Config.slackClaudeGlobalPreset)) { - // 先发送设定 - let prompt = (useCast?.slack || Config.slackClaudeGlobalPreset) - let emotion = await AzureTTS.getEmotionPrompt(e) - if (emotion) { - prompt = prompt + '\n' + emotion - } - await client.sendMessage(prompt, e) - logger.info('claudeFirst:', prompt) - } - } - let text = await client.sendMessage(prompt, e) - return { - text - } + let rsp = await client.sendMessage(prompt, { + stream: false, + parentMessageId: conversation.parentMessageId, + conversationId: conversation.conversationId, + system: Config.claudeSystemPrompt + }) + return rsp } else if (use === 'claude2') { let { conversationId } = conversation let client = new ClaudeAIClient({ @@ -2518,44 +2182,6 @@ export class chatgpt extends plugin { } } - async newClaudeConversation (e) { - let presetName = e.msg.replace(/^#claude开启新对话/, '').trim() - let client = new SlackClaudeClient({ - slackUserToken: Config.slackUserToken, - slackChannelId: Config.slackChannelId - }) - let response - if (!presetName || presetName === '空' || presetName === '无设定') { - let conversationId = await redis.get(`CHATGPT:SLACK_CONVERSATION:${e.sender.user_id}`) - if (conversationId) { - // 如果有对话进行中,先删除 - logger.info('开启Claude新对话,但旧对话未结束,自动结束上一次对话') - await redis.del(`CHATGPT:SLACK_CONVERSATION:${e.sender.user_id}`) - await redis.del(`CHATGPT:WRONG_EMOTION:${e.sender.user_id}`) - } - response = await client.sendMessage('', e) - await this.reply(response, true) - } else { - let preset = getPromptByName(presetName) - if (!preset) { - await this.reply('没有这个设定', true) - } else { - let conversationId = await redis.get(`CHATGPT:SLACK_CONVERSATION:${e.sender.user_id}`) - if (conversationId) { - // 如果有对话进行中,先删除 - logger.info('开启Claude新对话,但旧对话未结束,自动结束上一次对话') - await redis.del(`CHATGPT:SLACK_CONVERSATION:${e.sender.user_id}`) - await redis.del(`CHATGPT:WRONG_EMOTION:${e.sender.user_id}`) - } - logger.info('send preset: ' + preset.content) - response = await client.sendMessage(preset.content, e) + - await client.sendMessage(await AzureTTS.getEmotionPrompt(e), e) - await this.reply(response, true) - } - } - return true - } - async newxhBotConversation (e) { let botId = e.msg.replace(/^#星火助手/, '').trim() if (Config.xhmode != 'web') { diff --git a/apps/management.js b/apps/management.js index 5b6d7eb..8afc879 100644 --- a/apps/management.js +++ b/apps/management.js @@ -23,6 +23,7 @@ import VoiceVoxTTS, { supportConfigurations as voxRoleList } from '../utils/tts/ import { supportConfigurations as azureRoleList } from '../utils/tts/microsoft-azure.js' import fetch from 'node-fetch' import { newFetch } from '../utils/proxy.js' +import { createServer, runServer, stopServer } from '../server/index.js' export class ChatgptManagement extends plugin { constructor (e) { @@ -180,6 +181,11 @@ export class ChatgptManagement extends plugin { fnc: 'setAPIKey', permission: 'master' }, + { + reg: '^#chatgpt设置(claude|Claude)(Key|key)$', + fnc: 'setClaudeKey', + permission: 'master' + }, { reg: '^#chatgpt设置(Gemini|gemini)(Key|key)$', fnc: 'setGeminiKey', @@ -316,6 +322,11 @@ export class ChatgptManagement extends plugin { fnc: 'setXinghuoModel', permission: 'master' }, + { + reg: '^#chatgpt设置(claude|Claude)模型$', + fnc: 'setClaudeModel', + permission: 'master' + }, { reg: '^#chatgpt必应(禁用|禁止|关闭|启用|开启)搜索$', fnc: 'switchBingSearch', @@ -330,6 +341,11 @@ export class ChatgptManagement extends plugin { reg: '^#chatgpt(开启|关闭)(api|API)流$', fnc: 'switchStream', permission: 'master' + }, + { + reg: '^#chatgpt(开启|关闭)(工具箱|后台服务)$', + fnc: 'switchToolbox', + permission: 'master' } ] }) @@ -1255,6 +1271,25 @@ azure语音:Azure 语音是微软 Azure 平台提供的一项语音服务, this.finish('saveAPIKey') } + async setClaudeKey (e) { + this.setContext('saveClaudeKey') + await this.reply('请发送Claude API Key', true) + return false + } + + async saveClaudeKey () { + if (!this.e.msg) return + let token = this.e.msg + if (!token.startsWith('sk-ant')) { + await this.reply('Claude API Key格式错误。如果是格式特殊的非官方Key请前往锅巴或工具箱手动设置', true) + this.finish('saveClaudeKey') + return + } + Config.claudeKey = token + await this.reply('Claude API Key设置成功', true) + this.finish('saveClaudeKey') + } + async setGeminiKey (e) { this.setContext('saveGeminiKey') await this.reply('请发送Gemini API Key.获取地址:https://makersuite.google.com/app/apikey', true) @@ -1675,6 +1710,20 @@ azure语音:Azure 语音是微软 Azure 平台提供的一项语音服务, this.finish('saveAPIModel') } + async setClaudeModel (e) { + this.setContext('saveClaudeModel') + await this.reply('请发送Claude模型,官方推荐模型:\nclaude-3-opus-20240229\nclaude-3-sonnet-20240229', true) + return false + } + + async saveClaudeModel () { + if (!this.e.msg) return + let token = this.e.msg + Config.claudeApiModel = token + await this.reply('Claude模型设置成功', true) + this.finish('saveClaudeModel') + } + async setOpenAiBaseUrl (e) { this.setContext('saveOpenAiBaseUrl') await this.reply('请发送API反代', true) @@ -1775,4 +1824,25 @@ azure语音:Azure 语音是微软 Azure 平台提供的一项语音服务, await this.reply('好的,已经关闭API流式输出') } } + + async switchToolbox (e) { + if (e.msg.includes('开启')) { + if (Config.enableToolbox) { + await this.reply('已经开启了') + return + } + Config.enableToolbox = true + await this.reply('开启中', true) + await runServer() + await this.reply('好的,已经打开工具箱') + } else { + if (!Config.enableToolbox) { + await this.reply('已经是关闭的了') + return + } + Config.enableToolbox = false + await stopServer() + await this.reply('好的,已经关闭工具箱') + } + } } diff --git a/apps/md.js b/apps/md.js index 4409a88..1690fa1 100644 --- a/apps/md.js +++ b/apps/md.js @@ -1,5 +1,5 @@ import plugin from '../../../lib/plugins/plugin.js' -import {Config} from '../utils/config.js' +import { Config } from '../utils/config.js' export class ChatGPTMarkdownHandler extends plugin { constructor () { diff --git a/apps/prompts.js b/apps/prompts.js index ab22a06..1f60f14 100644 --- a/apps/prompts.js +++ b/apps/prompts.js @@ -66,21 +66,6 @@ export class help extends plugin { fnc: 'helpPrompt', permission: 'master' } - // { - // reg: '^#(chatgpt|ChatGPT)(开启|关闭)洗脑$', - // fnc: 'setSydneyBrainWash', - // permission: 'master' - // }, - // { - // reg: '^#(chatgpt|ChatGPT)(设置)?洗脑强度', - // fnc: 'setSydneyBrainWashStrength', - // permission: 'master' - // }, - // { - // reg: '^#(chatgpt|ChatGPT)(设置)?洗脑名称', - // fnc: 'setSydneyBrainWashName', - // permission: 'master' - // } ] }) } @@ -152,7 +137,7 @@ export class help extends plugin { const keyMap = { api: 'promptPrefixOverride', bing: 'sydney', - claude: 'slackClaudeGlobalPreset', + claude: 'claudeSystemPrompt', qwen: 'promptPrefixOverride', gemini: 'geminiPrompt', xh: 'xhPrompt' diff --git a/client/ClaudeAPIClient.js b/client/ClaudeAPIClient.js new file mode 100644 index 0000000..0752e8d --- /dev/null +++ b/client/ClaudeAPIClient.js @@ -0,0 +1,188 @@ +import crypto from 'crypto' +import { newFetch } from '../utils/proxy.js' +import _ from 'lodash' +import { getMessageById, upsertMessage } from '../utils/history.js' +import { BaseClient } from './BaseClient.js' + +const BASEURL = 'https://api.anthropic.com' + +/** + * @typedef {Object} Content + * @property {string} model + * @property {string} system + * @property {number} max_tokens + * @property {boolean} stream + * @property {Array<{ + * role: 'user'|'assistant', + * content: string|Array<{ + * type: 'text'|'image', + * text?: string, + * source?: { + * type: 'base64', + * media_type: 'image/jpeg'|'image/png'|'image/gif'|'image/webp', + * data: string + * } + * }> + * }>} messages + * + * Claude消息的基本格式 + */ + +/** + * @typedef {Object} ClaudeResponse + * @property {string} id + * @property {string} type + * @property {number} role + * @property {number} model + * @property {number} stop_reason + * @property {number} stop_sequence + * @property {number} role + * @property {boolean} stream + * @property {Array<{ + * type: string, + * text: string + * }>} content + * @property {Array<{ + * input_tokens: number, + * output_tokens: number, + * }>} usage + * + * Claude响应的基本格式 + */ + +export class ClaudeAPIClient extends BaseClient { + constructor (props) { + if (!props.upsertMessage) { + props.upsertMessage = async function umGemini (message) { + return await upsertMessage(message, 'Claude') + } + } + if (!props.getMessageById) { + props.getMessageById = async function umGemini (message) { + return await getMessageById(message, 'Claude') + } + } + super(props) + this.model = props.model + this.key = props.key + if (!this.key) { + throw new Error('no claude API key') + } + this.baseUrl = props.baseUrl || BASEURL + this.supportFunction = false + this.debug = props.debug + } + + async getHistory (parentMessageId, userId = this.userId, opt = {}) { + const history = [] + let cursor = parentMessageId + if (!cursor) { + return history + } + do { + let parentMessage = await this.getMessageById(cursor) + if (!parentMessage) { + break + } else { + history.push(parentMessage) + cursor = parentMessage.parentMessageId + if (!cursor) { + break + } + } + } while (true) + return history.reverse() + } + + /** + * + * @param text + * @param {{conversationId: string?, parentMessageId: string?, stream: boolean?, onProgress: function?, functionResponse: FunctionResponse?, system: string?, image: string?, model: string?}} opt + * @returns {Promise<{conversationId: string?, parentMessageId: string, text: string, id: string}>} + */ + async sendMessage (text, opt = {}) { + let history = await this.getHistory(opt.parentMessageId) + /** + * 发送的body + * @type {Content} + * @see https://docs.anthropic.com/claude/reference/messages_post + */ + let body = {} + if (opt.system) { + body.system = opt.system + } + const idThis = crypto.randomUUID() + const idModel = crypto.randomUUID() + /** + * @type {Array<{ + * role: 'user'|'assistant', + * content: string|Array<{ + * type: 'text'|'image', + * text?: string, + * source?: { + * type: 'base64', + * media_type: 'image/jpeg'|'image/png'|'image/gif'|'image/webp', + * data: string + * } + * }> + * }>} + */ + let thisContent = [{ type: 'text', text }] + if (opt.image) { + thisContent.push({ + type: 'image', + source: { + type: 'base64', + media_type: 'image/jpeg', + data: opt.image + } + }) + } + const thisMessage = { + role: 'user', + content: thisContent, + id: idThis, + parentMessageId: opt.parentMessageId || undefined + } + history.push(_.cloneDeep(thisMessage)) + let messages = history.map(h => { return { role: h.role, content: h.content } }) + body = Object.assign(body, { + model: opt.model || this.model || 'claude-3-opus-20240229', + max_tokens: opt.max_tokens || 1024, + messages, + stream: false + }) + let url = `${this.baseUrl}/v1/messages` + let result = await newFetch(url, { + headers: { + 'anthropic-version': '2023-06-01', + 'x-api-key': this.key, + 'content-type': 'application/json' + }, + method: 'POST', + body: JSON.stringify(body) + }) + if (result.status !== 200) { + throw new Error(await result.text()) + } + /** + * @type {ClaudeResponse} + */ + let response = await result.json() + if (this.debug) { + console.log(JSON.stringify(response)) + } + await this.upsertMessage(thisMessage) + const respMessage = Object.assign(response, { + id: idModel, + parentMessageId: idThis + }) + await this.upsertMessage(respMessage) + return { + text: response.content[0].text, + conversationId: '', + parentMessageId: idThis, + id: idModel + } + } +} diff --git a/client/GoogleGeminiClient.js b/client/GoogleGeminiClient.js index 5197cf3..3a2ab19 100644 --- a/client/GoogleGeminiClient.js +++ b/client/GoogleGeminiClient.js @@ -1,6 +1,6 @@ import { BaseClient } from './BaseClient.js' -import { getMessageById, upsertMessage } from '../utils/common.js' +import { getMessageById, upsertMessage } from '../utils/history.js' import crypto from 'crypto' let GoogleGenerativeAI, HarmBlockThreshold, HarmCategory try { diff --git a/client/test/ClaudeApiClientTest.js b/client/test/ClaudeApiClientTest.js new file mode 100644 index 0000000..b551867 --- /dev/null +++ b/client/test/ClaudeApiClientTest.js @@ -0,0 +1,27 @@ +// import { ClaudeAPIClient } from '../ClaudeAPIClient.js' +// +// async function test () { +// const client = new ClaudeAPIClient({ +// key: 'sk-ant-api03-**************************************', +// model: 'claude-3-opus-20240229', +// debug: true, +// // baseUrl: 'http://claude-api.ikechan8370.com' +// }) +// let rsp = await client.sendMessage('你好') +// console.log(rsp) +// } +// global.store = {} +// global.redis = { +// set: (key, val) => { +// global.store[key] = val +// }, +// get: (key) => { +// return global.store[key] +// } +// } +// global.logger = { +// info: console.log, +// warn: console.warn, +// error: console.error +// } +// test() diff --git a/client/test/GozeClientTest.js b/client/test/GozeClientTest.js index 2c94663..302c1e2 100644 --- a/client/test/GozeClientTest.js +++ b/client/test/GozeClientTest.js @@ -1,6 +1,6 @@ import { SlackCozeClient } from '../CozeSlackClient.js' import fs from 'fs' -global.store = {} +// global.store = {} // global.redis = { // set: (key, val) => { diff --git a/guoba.support.js b/guoba.support.js index 683c636..b0b0a0f 100644 --- a/guoba.support.js +++ b/guoba.support.js @@ -57,6 +57,12 @@ export function supportGuoba () { bottomHelpMessage: '将输出更多调试信息,如果不希望控制台刷屏的话,可以关闭', component: 'Switch' }, + { + field: 'enableToolbox', + label: '开启工具箱', + bottomHelpMessage: '独立的后台管理面板(默认3321端口),与锅巴类似。工具箱会有额外占用,启动速度稍慢,酌情开启。修改后需重启生效!!!', + component: 'Switch' + }, { field: 'enableMd', label: 'QQ开启markdown', @@ -334,50 +340,44 @@ export function supportGuoba () { component: 'Input' }, { - label: '以下为Slack Claude方式的配置', + label: '以下为Claude API方式的配置', component: 'Divider' }, { - field: 'slackUserToken', - label: 'Slack用户Token', - bottomHelpMessage: 'slackUserToken,在OAuth&Permissions页面获取。需要具有channels:history, chat:write, groups:history, im:history, mpim:history 这几个scope', + field: 'claudeApiKey', + label: 'claude API Key', + bottomHelpMessage: '前往 https://console.anthropic.com/settings/keys 注册和生成', + component: 'InputPassword' + }, + { + field: 'claudeApiModel', + label: 'claude API 模型', + bottomHelpMessage: '如 claude-3-sonnet-20240229 或 claude-3-opus-20240229', component: 'Input' }, { - field: 'slackBotUserToken', - label: 'Slack Bot Token', - bottomHelpMessage: 'slackBotUserToken,在OAuth&Permissions页面获取。需要channels:history,groups:history,im:history 这几个scope', + field: 'claudeApiBaseUrl', + label: 'claude API 反代', component: 'Input' }, { - field: 'slackClaudeUserId', - label: 'Slack成员id', - bottomHelpMessage: '在Slack中点击Claude头像查看详情,其中的成员ID复制过来', - component: 'Input' + field: 'claudeApiMaxToken', + label: 'claude 最大回复token数', + component: 'InputNumber' }, { - field: 'slackSigningSecret', - label: 'Slack签名密钥', - bottomHelpMessage: 'Signing Secret。在Basic Information页面获取', - component: 'Input' + field: 'claudeApiTemperature', + label: 'claude 温度', + component: 'InputNumber', + componentProps: { + min: 0, + max: 1 + } }, { - field: 'slackClaudeSpecifiedChannel', - label: 'Slack指定频道', - bottomHelpMessage: '为空时,将为每个qq号建立私有频道。若填写了,对话将发生在本频道。和其他人公用workspace时建议用这个', - component: 'Input' - }, - { - field: 'slackClaudeEnableGlobalPreset', - label: 'Claude使用全局设定', - bottomHelpMessage: '开启后,所有人每次发起新对话时,会先发送设定过去再开始对话,达到类似Bing自设定的效果。', - component: 'Switch' - }, - { - field: 'slackClaudeGlobalPreset', - label: 'Slack全局设定', - bottomHelpMessage: '若启用全局设定,每个人都会默认使用这里的设定。', - component: 'Input' + field: 'claudeSystemPrompt', + label: 'claude 设定', + component: 'InputTextArea' }, { label: '以下为Claude2方式的配置', diff --git a/index.js b/index.js index d2139ac..6993a38 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,16 @@ import fs from 'node:fs' import { Config } from './utils/config.js' -import { createServer } from './server/index.js' +import { createServer, runServer } from './server/index.js' + +logger.info('**************************************') +logger.info('chatgpt-plugin加载中') if (!global.segment) { - global.segment = (await import('oicq')).segment + try { + global.segment = (await import('icqq')).segment + } catch (err) { + global.segment = (await import('oicq')).segment + } } const files = fs.readdirSync('./plugins/chatgpt-plugin/apps').filter(file => file.endsWith('.js')) @@ -19,7 +26,6 @@ ret = await Promise.allSettled(ret) let apps = {} for (let i in files) { let name = files[i].replace('.js', '') - if (ret[i].status !== 'fulfilled') { logger.error(`载入插件错误:${logger.red(name)}`) logger.error(ret[i].reason) @@ -27,13 +33,22 @@ for (let i in files) { } apps[name] = ret[i].value[Object.keys(ret[i].value)[0]] } +global.chatgpt = { +} // 启动服务器 -await createServer() -logger.info('**************************************') +if (Config.enableToolbox) { + logger.info('开启工具箱配置项,工具箱启动中') + await createServer() + await runServer() + logger.info('工具箱启动成功') +} else { + logger.info('提示:当前配置未开启chatgpt工具箱,可通过锅巴或`#chatgpt开启工具箱`指令开启') +} logger.info('chatgpt-plugin加载成功') logger.info(`当前版本${Config.version}`) logger.info('仓库地址 https://github.com/ikechan8370/chatgpt-plugin') +logger.info('文档地址 https://www.yunzai.chat') logger.info('插件群号 559567232') logger.info('**************************************') diff --git a/model/conversation.js b/model/conversation.js new file mode 100644 index 0000000..ced1084 --- /dev/null +++ b/model/conversation.js @@ -0,0 +1,362 @@ +import { getUin, getUserData } from '../utils/common.js' +import { Config } from '../utils/config.js' +import { KeyvFile } from 'keyv-file' +import _ from 'lodash' + +export const originalValues = ['星火', '通义千问', '克劳德', '克劳德2', '必应', 'api', 'API', 'api3', 'API3', 'glm', '巴德', '双子星', '双子座', '智谱'] +export const correspondingValues = ['xh', 'qwen', 'claude', 'claude2', 'bing', 'api', 'api', 'api3', 'api3', 'chatglm', 'bard', 'gemini', 'gemini', 'chatglm4'] + +export class ConversationManager { + async endConversation (e) { + const userData = await getUserData(e.user_id) + const match = e.msg.trim().match('^#?(.*)(结束|新开|摧毁|毁灭|完结)对话') + console.log(match[1]) + let use + if (match[1] && match[1] != 'chatgpt') { + use = correspondingValues[originalValues.indexOf(match[1])] + } else { + use = (userData.mode === 'default' ? null : userData.mode) || await redis.get('CHATGPT:USE') + } + console.log(use) + await redis.del(`CHATGPT:WRONG_EMOTION:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`) + // fast implementation + if (use === 'claude') { + await redis.del(`CHATGPT:CONVERSATIONS_CLAUDE:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`) + await this.reply('claude对话已结束') + return + } + if (use === 'claude2') { + await redis.del(`CHATGPT:CLAUDE2_CONVERSATION:${e.sender.user_id}`) + await this.reply('claude.ai对话已结束') + return + } + if (use === 'xh') { + await redis.del(`CHATGPT:CONVERSATIONS_XH:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`) + await this.reply('星火对话已结束') + return + } + if (use === 'bard') { + await redis.del(`CHATGPT:CONVERSATIONS_BARD:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`) + await this.reply('Bard对话已结束') + return + } + let ats = e.message.filter(m => m.type === 'at') + const isAtMode = Config.toggleMode === 'at' + if (isAtMode) ats = ats.filter(item => item.qq !== getUin(e)) + if (ats.length === 0) { + if (use === 'api3') { + await redis.del(`CHATGPT:QQ_CONVERSATION:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`) + await this.reply('已退出当前对话,该对话仍然保留。请@我进行聊天以开启新的对话', true) + } else if (use === 'bing') { + let c = await redis.get(`CHATGPT:CONVERSATIONS_BING:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`) + if (!c) { + await this.reply('当前没有开启对话', true) + return + } else { + await redis.del(`CHATGPT:CONVERSATIONS_BING:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`) + } + const conversation = { + store: new KeyvFile({ filename: 'cache.json' }), + namespace: Config.toneStyle + } + let Keyv + try { + Keyv = (await import('keyv')).default + } catch (err) { + await this.reply('依赖keyv未安装,请执行pnpm install keyv', true) + } + const conversationsCache = new Keyv(conversation) + logger.info(`SydneyUser_${e.sender.user_id}`, await conversationsCache.get(`SydneyUser_${e.sender.user_id}`)) + await conversationsCache.delete(`SydneyUser_${e.sender.user_id}`) + await this.reply('已退出当前对话,该对话仍然保留。请@我进行聊天以开启新的对话', true) + } else if (use === 'chatglm') { + const conversation = { + store: new KeyvFile({ filename: 'cache.json' }), + namespace: 'chatglm_6b' + } + let Keyv + try { + Keyv = (await import('keyv')).default + } catch (err) { + await this.reply('依赖keyv未安装,请执行pnpm install keyv', true) + } + const conversationsCache = new Keyv(conversation) + logger.info(`ChatGLMUser_${e.sender.user_id}`, await conversationsCache.get(`ChatGLMUser_${e.sender.user_id}`)) + await conversationsCache.delete(`ChatGLMUser_${e.sender.user_id}`) + await this.reply('已退出当前对话,该对话仍然保留。请@我进行聊天以开启新的对话', true) + } else if (use === 'api') { + let c = await redis.get(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`) + if (!c) { + await this.reply('当前没有开启对话', true) + } else { + await redis.del(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`) + await this.reply('已结束当前对话,请@我进行聊天以开启新的对话', true) + } + } else if (use === 'qwen') { + let c = await redis.get(`CHATGPT:CONVERSATIONS_QWEN:${e.sender.user_id}`) + if (!c) { + await this.reply('当前没有开启对话', true) + } else { + await redis.del(`CHATGPT:CONVERSATIONS_QWEN:${e.sender.user_id}`) + await this.reply('已结束当前对话,请@我进行聊天以开启新的对话', true) + } + } else if (use === 'gemini') { + let c = await redis.get(`CHATGPT:CONVERSATIONS_GEMINI:${e.sender.user_id}`) + if (!c) { + await this.reply('当前没有开启对话', true) + } else { + await redis.del(`CHATGPT:CONVERSATIONS_GEMINI:${e.sender.user_id}`) + await this.reply('已结束当前对话,请@我进行聊天以开启新的对话', true) + } + } else if (use === 'chatglm4') { + let c = await redis.get(`CHATGPT:CONVERSATIONS_CHATGLM4:${e.sender.user_id}`) + if (!c) { + await this.reply('当前没有开启对话', true) + } else { + await redis.del(`CHATGPT:CONVERSATIONS_CHATGLM4:${e.sender.user_id}`) + await this.reply('已结束当前对话,请@我进行聊天以开启新的对话', true) + } + } else if (use === 'bing') { + let c = await redis.get(`CHATGPT:CONVERSATIONS_BING:${e.sender.user_id}`) + if (!c) { + await this.reply('当前没有开启对话', true) + } else { + await redis.del(`CHATGPT:CONVERSATIONS_BING:${e.sender.user_id}`) + await this.reply('已结束当前对话,请@我进行聊天以开启新的对话', true) + } + } else if (use === 'browser') { + let c = await redis.get(`CHATGPT:CONVERSATIONS_BROWSER:${e.sender.user_id}`) + if (!c) { + await this.reply('当前没有开启对话', true) + } else { + await redis.del(`CHATGPT:CONVERSATIONS_BROWSER:${e.sender.user_id}`) + await this.reply('已结束当前对话,请@我进行聊天以开启新的对话', true) + } + } + } else { + let at = ats[0] + let qq = at.qq + let atUser = _.trimStart(at.text, '@') + if (use === 'api3') { + await redis.del(`CHATGPT:QQ_CONVERSATION:${qq}`) + await this.reply(`${atUser}已退出TA当前的对话,TA仍可以@我进行聊天以开启新的对话`, true) + } else if (use === 'bing') { + const conversation = { + store: new KeyvFile({ filename: 'cache.json' }), + namespace: Config.toneStyle + } + let Keyv + try { + Keyv = (await import('keyv')).default + } catch (err) { + await this.reply('依赖keyv未安装,请执行pnpm install keyv', true) + } + const conversationsCache = new Keyv(conversation) + await conversationsCache.delete(`SydneyUser_${qq}`) + await this.reply('已退出当前对话,该对话仍然保留。请@我进行聊天以开启新的对话', true) + } else if (use === 'chatglm') { + const conversation = { + store: new KeyvFile({ filename: 'cache.json' }), + namespace: 'chatglm_6b' + } + let Keyv + try { + Keyv = (await import('keyv')).default + } catch (err) { + await this.reply('依赖keyv未安装,请执行pnpm install keyv', true) + } + const conversationsCache = new Keyv(conversation) + logger.info(`ChatGLMUser_${e.sender.user_id}`, await conversationsCache.get(`ChatGLMUser_${e.sender.user_id}`)) + await conversationsCache.delete(`ChatGLMUser_${qq}`) + await this.reply('已退出当前对话,该对话仍然保留。请@我进行聊天以开启新的对话', true) + } else if (use === 'api') { + let c = await redis.get(`CHATGPT:CONVERSATIONS:${qq}`) + if (!c) { + await this.reply(`当前${atUser}没有开启对话`, true) + } else { + await redis.del(`CHATGPT:CONVERSATIONS:${qq}`) + await this.reply(`已结束${atUser}的对话,TA仍可以@我进行聊天以开启新的对话`, true) + } + } else if (use === 'qwen') { + let c = await redis.get(`CHATGPT:CONVERSATIONS_QWEN:${qq}`) + if (!c) { + await this.reply(`当前${atUser}没有开启对话`, true) + } else { + await redis.del(`CHATGPT:CONVERSATIONS_QWEN:${qq}`) + await this.reply(`已结束${atUser}的对话,TA仍可以@我进行聊天以开启新的对话`, true) + } + } else if (use === 'gemini') { + let c = await redis.get(`CHATGPT:CONVERSATIONS_GEMINI:${qq}`) + if (!c) { + await this.reply(`当前${atUser}没有开启对话`, true) + } else { + await redis.del(`CHATGPT:CONVERSATIONS_GEMINI:${qq}`) + await this.reply(`已结束${atUser}的对话,TA仍可以@我进行聊天以开启新的对话`, true) + } + } else if (use === 'chatglm4') { + let c = await redis.get(`CHATGPT:CONVERSATIONS_CHATGLM4:${qq}`) + if (!c) { + await this.reply(`当前${atUser}没有开启对话`, true) + } else { + await redis.del(`CHATGPT:CONVERSATIONS_CHATGLM4:${qq}`) + await this.reply(`已结束${atUser}的对话,TA仍可以@我进行聊天以开启新的对话`, true) + } + } else if (use === 'bing') { + let c = await redis.get(`CHATGPT:CONVERSATIONS_BING:${qq}`) + if (!c) { + await this.reply(`当前${atUser}没有开启对话`, true) + } else { + await redis.del(`CHATGPT:CONVERSATIONS_BING:${qq}`) + await this.reply(`已结束${atUser}的对话,TA仍可以@我进行聊天以开启新的对话`, true) + } + } else if (use === 'browser') { + let c = await redis.get(`CHATGPT:CONVERSATIONS_BROWSER:${qq}`) + if (!c) { + await this.reply(`当前${atUser}没有开启对话`, true) + } else { + await redis.del(`CHATGPT:CONVERSATIONS_BROWSER:${qq}`) + await this.reply(`已结束${atUser}的对话,TA仍可以@我进行聊天以开启新的对话`, true) + } + } + } + } + + async endAllConversations (e) { + const match = e.msg.trim().match('^#?(.*)(结束|新开|摧毁|毁灭|完结)全部对话') + console.log(match[1]) + let use + if (match[1] && match[1] != 'chatgpt') { + use = correspondingValues[originalValues.indexOf(match[1])] + } else { + use = await redis.get('CHATGPT:USE') || 'api' + } + console.log(use) + let deleted = 0 + switch (use) { + case 'claude': { + let cs = await redis.keys('CHATGPT:CONVERSATIONS_CLAUDE:*') + let we = await redis.keys('CHATGPT:WRONG_EMOTION:*') + for (let i = 0; i < cs.length; i++) { + await redis.del(cs[i]) + if (Config.debug) { + logger.info('delete claude conversation of qq: ' + cs[i]) + } + deleted++ + } + for (const element of we) { + await redis.del(element) + } + break + } + case 'xh': { + let cs = await redis.keys('CHATGPT:CONVERSATIONS_XH:*') + for (let i = 0; i < cs.length; i++) { + await redis.del(cs[i]) + if (Config.debug) { + logger.info('delete xh conversation of qq: ' + cs[i]) + } + deleted++ + } + break + } + case 'bard': { + let cs = await redis.keys('CHATGPT:CONVERSATIONS_BARD:*') + for (let i = 0; i < cs.length; i++) { + await redis.del(cs[i]) + if (Config.debug) { + logger.info('delete bard conversation of qq: ' + cs[i]) + } + deleted++ + } + break + } + case 'bing': { + let cs = await redis.keys('CHATGPT:CONVERSATIONS_BING:*') + let we = await redis.keys('CHATGPT:WRONG_EMOTION:*') + for (let i = 0; i < cs.length; i++) { + await redis.del(cs[i]) + if (Config.debug) { + logger.info('delete bing conversation of qq: ' + cs[i]) + } + deleted++ + } + for (const element of we) { + await redis.del(element) + } + break + } + case 'api': { + let cs = await redis.keys('CHATGPT:CONVERSATIONS:*') + for (let i = 0; i < cs.length; i++) { + await redis.del(cs[i]) + if (Config.debug) { + logger.info('delete api conversation of qq: ' + cs[i]) + } + deleted++ + } + break + } + case 'api3': { + let qcs = await redis.keys('CHATGPT:QQ_CONVERSATION:*') + for (let i = 0; i < qcs.length; i++) { + await redis.del(qcs[i]) + // todo clean last message id + if (Config.debug) { + logger.info('delete conversation bind: ' + qcs[i]) + } + deleted++ + } + break + } + case 'chatglm': { + let qcs = await redis.keys('CHATGPT:CONVERSATIONS_CHATGLM:*') + for (let i = 0; i < qcs.length; i++) { + await redis.del(qcs[i]) + // todo clean last message id + if (Config.debug) { + logger.info('delete chatglm conversation bind: ' + qcs[i]) + } + deleted++ + } + break + } + case 'qwen': { + let qcs = await redis.keys('CHATGPT:CONVERSATIONS_QWEN:*') + for (let i = 0; i < qcs.length; i++) { + await redis.del(qcs[i]) + // todo clean last message id + if (Config.debug) { + logger.info('delete qwen conversation bind: ' + qcs[i]) + } + deleted++ + } + break + } + case 'gemini': { + let qcs = await redis.keys('CHATGPT:CONVERSATIONS_GEMINI:*') + for (let i = 0; i < qcs.length; i++) { + await redis.del(qcs[i]) + // todo clean last message id + if (Config.debug) { + logger.info('delete gemini conversation bind: ' + qcs[i]) + } + deleted++ + } + break + } + case 'chatglm4': { + let qcs = await redis.keys('CHATGPT:CONVERSATIONS_CHATGLM4:*') + for (let i = 0; i < qcs.length; i++) { + await redis.del(qcs[i]) + // todo clean last message id + if (Config.debug) { + logger.info('delete chatglm4 conversation bind: ' + qcs[i]) + } + deleted++ + } + break + } + } + await this.reply(`结束了${deleted}个用户的对话。`, true) + } +} diff --git a/server/index.js b/server/index.js index 6fc232a..1a75d52 100644 --- a/server/index.js +++ b/server/index.js @@ -20,39 +20,9 @@ import Guoba from './modules/guoba.js' import SettingView from './modules/setting_view.js' const __dirname = path.resolve() -const server = fastify({ - logger: Config.debug -}) - -async function setUserData(qq, data) { - const dir = 'resources/ChatGPTCache/user' - const filename = `${qq}.json` - const filepath = path.join(dir, filename) - fs.mkdirSync(dir, { recursive: true }) - fs.writeFileSync(filepath, JSON.stringify(data)) -} - -await server.register(cors, { - origin: '*' -}) -await server.register(fstatic, { - root: path.join(__dirname, 'plugins/chatgpt-plugin/server/static/') -}) -await server.register(websocket, { - cors: true, - options: { - maxPayload: 1048576 - } -}) -await server.register(fastifyCookie) -await server.register(webRoute) -await server.register(webUser) -await server.register(SettingView) -await server.register(webPrompt) -await server.register(Guoba) // 无法访问端口的情况下创建与media的通讯 -async function mediaLink() { +async function mediaLink () { const ip = await getPublicIP() const testServer = await fetch(`${Config.cloudTranscode}/check`, { @@ -74,7 +44,7 @@ async function mediaLink() { ws.send(JSON.stringify({ command: 'register', region: getUin(), - type: 'server', + type: 'server' })) }) ws.on('message', async (message) => { @@ -108,14 +78,13 @@ async function mediaLink() { if (data.qq && data.passwd) { const token = randomString(32) if (data.qq == getUin() && await redis.get('CHATGPT:ADMIN_PASSWD') == data.passwd) { - AddUser({ user: data.qq, token: token, autho: 'admin' }) - ws.send(JSON.stringify({ command: data.command, state: true, autho: 'admin', token: token, region: getUin(), type: 'server' })) - + AddUser({ user: data.qq, token, autho: 'admin' }) + ws.send(JSON.stringify({ command: data.command, state: true, autho: 'admin', token, region: getUin(), type: 'server' })) } else { const user = await getUserData(data.qq) if (user.passwd != '' && user.passwd === data.passwd) { - AddUser({ user: data.qq, token: token, autho: 'user' }) - ws.send(JSON.stringify({ command: data.command, state: true, autho: 'user', token: token, region: getUin(), type: 'server' })) + AddUser({ user: data.qq, token, autho: 'user' }) + ws.send(JSON.stringify({ command: data.command, state: true, autho: 'user', token, region: getUin(), type: 'server' })) } else { ws.send(JSON.stringify({ command: data.command, state: false, error: `用户名密码错误,如果忘记密码请私聊机器人输入 ${data.qq == getUin() ? '#修改管理密码' : '#修改用户密码'} 进行修改`, region: getUin(), type: 'server' })) } @@ -141,7 +110,6 @@ async function mediaLink() { console.log(error) } }) - } else { console.log('本地服务网络正常,无需开启通讯') } @@ -152,7 +120,38 @@ async function mediaLink() { // 未完工,暂不开启这个功能 // mediaLink() -export async function createServer() { +export async function createServer () { + let server = fastify({ + logger: Config.debug + }) + + async function setUserData (qq, data) { + const dir = 'resources/ChatGPTCache/user' + const filename = `${qq}.json` + const filepath = path.join(dir, filename) + fs.mkdirSync(dir, { recursive: true }) + fs.writeFileSync(filepath, JSON.stringify(data)) + } + + await server.register(cors, { + origin: '*' + }) + await server.register(fstatic, { + root: path.join(__dirname, 'plugins/chatgpt-plugin/server/static/') + }) + await server.register(websocket, { + cors: true, + options: { + maxPayload: 1048576 + } + }) + await server.register(fastifyCookie) + await server.register(webRoute) + await server.register(webUser) + await server.register(SettingView) + await server.register(webPrompt) + await server.register(Guoba) + // 页面数据获取 server.post('/page', async (request, reply) => { const body = request.body || {} @@ -316,7 +315,7 @@ export async function createServer() { Bot.sendPrivateMsg(parseInt(data.id), data.message, data.quotable) } } - await connection.socket.send(JSON.stringify({ command: data.command, state: true, })) + await connection.socket.send(JSON.stringify({ command: data.command, state: true })) } else { await connection.socket.send(JSON.stringify({ command: data.command, state: false, error: '参数不足' })) } @@ -370,7 +369,7 @@ export async function createServer() { seq: e.seq, rand: e.rand, message: e.message, - user_name: e.sender.nickname, + user_name: e.sender.nickname }, read: true } @@ -380,12 +379,12 @@ export async function createServer() { break default: - await connection.socket.send(JSON.stringify({ "data": data })) + await connection.socket.send(JSON.stringify({ data })) break } } catch (error) { console.error(error) - await connection.socket.send(JSON.stringify({ "error": error.message })) + await connection.socket.send(JSON.stringify({ error: error.message })) } }) connection.socket.on('close', () => { @@ -395,7 +394,7 @@ export async function createServer() { }) return request } - Bot.on("message", e => { + Bot.on('message', e => { const messageData = { notice: 'clientMessage', message: e.message, @@ -411,7 +410,7 @@ export async function createServer() { seq: e.seq, rand: e.rand, message: e.message, - user_name: e.sender.nickname, + user_name: e.sender.nickname } } if (clients) { @@ -486,10 +485,10 @@ export async function createServer() { for (let [keyPath, value] of Object.entries(chatdata)) { if (keyPath === 'blockWords' || keyPath === 'promptBlockWords' || keyPath === 'initiativeChatGroups') { value = value.toString().split(/[,,;;\|]/) } if (Config[keyPath] != value) { - //检查云服务api + // 检查云服务api if (keyPath === 'cloudTranscode') { - const referer = request.headers.referer; - const origin = referer.match(/(https?:\/\/[^/]+)/)[1]; + const referer = request.headers.referer + const origin = referer.match(/(https?:\/\/[^/]+)/)[1] const checkCloud = await fetch(`${value}/check`, { method: 'POST', @@ -562,7 +561,7 @@ export async function createServer() { // 系统服务测试 server.post('/serverTest', async (request, reply) => { let serverState = { - cache: false, //待移除 + cache: false, // 待移除 cloud: false } if (Config.cloudTranscode) { @@ -575,6 +574,15 @@ export async function createServer() { return reply }) + global.chatgpt.server = server + return server +} + +export async function runServer () { + let server = global.chatgpt.server + if (!server) { + server = await createServer() + } server.listen({ port: Config.serverPort || 3321, host: '::' @@ -586,3 +594,10 @@ export async function createServer() { } }) } + +export async function stopServer () { + let server = global.chatgpt.server + if (server) { + await server.close() + } +} diff --git a/utils/common.js b/utils/common.js index 808be0a..592e9ca 100644 --- a/utils/common.js +++ b/utils/common.js @@ -74,21 +74,6 @@ export function randomString (length = 5) { return str.substr(0, length) } -export async function upsertMessage (message, suffix = '') { - if (suffix) { - suffix = '_' + suffix - } - await redis.set(`CHATGPT:MESSAGE${suffix}:${message.id}`, JSON.stringify(message)) -} - -export async function getMessageById (id, suffix = '') { - if (suffix) { - suffix = '_' + suffix - } - let messageStr = await redis.get(`CHATGPT:MESSAGE${suffix}:${id}`) - return JSON.parse(messageStr) -} - export async function tryTimes (promiseFn, maxTries = 10) { try { return await promiseFn() diff --git a/utils/config.js b/utils/config.js index fce6809..ffd17db 100644 --- a/utils/config.js +++ b/utils/config.js @@ -178,9 +178,17 @@ const defaultConfig = { chatglmRefreshToken: '', sunoSessToken: '', sunoClientToken: '', + + claudeApiKey: '', + claudeApiBaseUrl: 'http://claude-api.ikechan8370.com', + claudeApiMaxToken: 1024, + claudeApiTemperature: 0.8, + claudeApiModel: '', // claude-3-opus-20240229 claude-3-sonnet-20240229 + claudeSystemPrompt: '', // claude api 设定 translateSource: 'openai', enableMd: false, // 第三方md,非QQBot。需要适配器实现segment.markdown和segment.button方可使用,否则不建议开启,会造成各种错误 - version: 'v2.7.10' + enableToolbox: false, // 默认关闭工具箱节省占用和加速启动 + version: 'v2.8.0' } const _path = process.cwd() let config = {} diff --git a/utils/history.js b/utils/history.js new file mode 100644 index 0000000..96bae9f --- /dev/null +++ b/utils/history.js @@ -0,0 +1,14 @@ +export async function upsertMessage (message, suffix = '') { + if (suffix) { + suffix = '_' + suffix + } + await redis.set(`CHATGPT:MESSAGE${suffix}:${message.id}`, JSON.stringify(message)) +} + +export async function getMessageById (id, suffix = '') { + if (suffix) { + suffix = '_' + suffix + } + let messageStr = await redis.get(`CHATGPT:MESSAGE${suffix}:${id}`) + return JSON.parse(messageStr) +} \ No newline at end of file diff --git a/utils/translate.js b/utils/translate.js index d290a19..a9330df 100644 --- a/utils/translate.js +++ b/utils/translate.js @@ -5,9 +5,7 @@ import { ChatGPTAPI } from './openai/chatgpt-api.js' import { newFetch } from './proxy.js' import { CustomGoogleGeminiClient } from '../client/CustomGoogleGeminiClient.js' import XinghuoClient from './xinghuo/xinghuo.js' -import {getImg, getMessageById, upsertMessage} from './common.js' -import {QwenApi} from "./alibaba/qwen-api.js"; -import {v4 as uuid} from "uuid"; +import { QwenApi } from './alibaba/qwen-api.js' // 代码参考:https://github.com/yeyang52/yenai-plugin/blob/b50b11338adfa5a4ef93912eefd2f1f704e8b990/model/api/funApi.js#L25 export const translateLangSupports = [