diff --git a/apps/button.js b/apps/button.js index 72b3a82..7bff6ff 100644 --- a/apps/button.js +++ b/apps/button.js @@ -1,5 +1,5 @@ import plugin from '../../../lib/plugins/plugin.js' -import {Config} from "../utils/config.js"; +import { Config } from '../utils/config.js' const PLUGIN_CHAT = 'ChatGpt 对话' const PLUGIN_MANAGEMENT = 'ChatGPT-Plugin 管理' @@ -13,6 +13,7 @@ const FUNCTION_XH = 'xh' const FUNCTION_QWEN = 'qwen' const FUNCTION_GLM4 = 'glm4' const FUNCTION_CLAUDE2 = 'claude2' +const FUNCTION_CLAUDE = 'claude' const FUNCTION_END = 'destroyConversations' const FUNCTION_END_ALL = 'endAllConversations' @@ -66,6 +67,7 @@ export class ChatGPTButtonHandler extends plugin { case `[${PLUGIN_CHAT}][${FUNCTION_XH}]`: case `[${PLUGIN_CHAT}][${FUNCTION_QWEN}]`: case `[${PLUGIN_CHAT}][${FUNCTION_CLAUDE2}]`: + case `[${PLUGIN_CHAT}][${FUNCTION_CLAUDE}]`: case `[${PLUGIN_CHAT}][${FUNCTION_GLM4}]`: case `[${PLUGIN_CHAT}][${FUNCTION_CHAT}]`: { return this.makeButtonChat(options?.btnData) @@ -177,8 +179,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 88e7288..cb9f42d 100644 --- a/apps/chat.js +++ b/apps/chat.js @@ -1,114 +1,41 @@ import plugin from '../../../lib/plugins/plugin.js' import common from '../../../lib/common/common.js' import _ from 'lodash' -import { Config, defaultOpenAIAPI } from '../utils/config.js' +import { Config } from '../utils/config.js' import { v4 as uuid } from 'uuid' -import { ChatGPTAPI } from '../utils/openai/chatgpt-api.js' -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, formatDate, formatDate2, generateAudio, getDefaultReplySetting, getImageOcrText, getImg, - getMasterQQ, - getMaxModelTokens, - getMessageById, - getOrDownloadFile, getUin, getUserData, getUserReplySetting, - isCN, isImage, 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' + import fetch from 'node-fetch' import { deleteConversation, getConversations, getLatestMessageIdByConversationId } from '../utils/conversation.js' 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 BingDrawClient from '../utils/BingDraw.js' +import { ConversationManager, originalValues } from '../model/conversation.js' import XinghuoClient from '../utils/xinghuo/xinghuo.js' -import Bard from '../utils/bard.js' -import { JinyanTool } from '../utils/tools/JinyanTool.js' -import { SendVideoTool } from '../utils/tools/SendBilibiliTool.js' -import { KickOutTool } from '../utils/tools/KickOutTool.js' -import { EditCardTool } from '../utils/tools/EditCardTool.js' -import { SearchVideoTool } from '../utils/tools/SearchBilibiliTool.js' -import { SearchMusicTool } from '../utils/tools/SearchMusicTool.js' -import { QueryStarRailTool } from '../utils/tools/QueryStarRailTool.js' -import { WebsiteTool } from '../utils/tools/WebsiteTool.js' -import { WeatherTool } from '../utils/tools/WeatherTool.js' -import { SerpTool } from '../utils/tools/SerpTool.js' -import { SerpIkechan8370Tool } from '../utils/tools/SerpIkechan8370Tool.js' -import { SendPictureTool } from '../utils/tools/SendPictureTool.js' -import { SerpImageTool } from '../utils/tools/SearchImageTool.js' -import { ImageCaptionTool } from '../utils/tools/ImageCaptionTool.js' -import { SendAudioMessageTool } from '../utils/tools/SendAudioMessageTool.js' -import { ProcessPictureTool } from '../utils/tools/ProcessPictureTool.js' -import { APTool } from '../utils/tools/APTool.js' -import { QueryGenshinTool } from '../utils/tools/QueryGenshinTool.js' -import { HandleMessageMsgTool } from '../utils/tools/HandleMessageMsgTool.js' -import { QueryUserinfoTool } from '../utils/tools/QueryUserinfoTool.js' -import { EliMovieTool } from '../utils/tools/EliMovieTool.js' -import { EliMusicTool } from '../utils/tools/EliMusicTool.js' -import { SendMusicTool } from '../utils/tools/SendMusicTool.js' -import { SendDiceTool } from '../utils/tools/SendDiceTool.js' -import { SendAvatarTool } from '../utils/tools/SendAvatarTool.js' -import { SendMessageToSpecificGroupOrUserTool } from '../utils/tools/SendMessageToSpecificGroupOrUserTool.js' -import { SetTitleTool } from '../utils/tools/SetTitleTool.js' -import { solveCaptchaOneShot } from '../utils/bingCaptcha.js' -import { ClaudeAIClient } from '../utils/claude.ai/index.js' import { getProxy } from '../utils/proxy.js' -import { QwenApi } from '../utils/alibaba/qwen-api.js' -import { getChatHistoryGroup } from '../utils/chat.js' -import { CustomGoogleGeminiClient } from '../client/CustomGoogleGeminiClient.js' -import { resizeAndCropImage } from '../utils/dalle.js' -import fs from 'fs' -import { ChatGLM4Client } from '../client/ChatGLM4Client.js' +import { generateSuggestedResponse } from '../utils/chat.js' +import Core from '../model/core.js' -const roleMap = { - owner: 'group owner', - admin: 'group administrator' -} - -try { - await import('@azure/openai') -} catch (err) { - logger.warn('【Azure-Openai】依赖@azure/openai未安装,Azure OpenAI不可用 请执行pnpm install @azure/openai安装') -} - -try { - await import('emoji-strip') -} catch (err) { - logger.warn('【ChatGPT-Plugin】依赖emoji-strip未安装,会导致azure语音模式下朗读emoji的问题,建议执行pnpm install emoji-strip安装') -} -try { - await import('keyv') -} catch (err) { - logger.warn('【ChatGPT-Plugin】依赖keyv未安装,可能影响Sydney模式下Bing对话,建议执行pnpm install keyv安装') -} 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是保留上下文的。超时后销毁对话,再次对话创建新的对话。 * 单位:秒 @@ -117,7 +44,6 @@ const correspondingValues = ['xh', 'qwen', 'claude', 'claude2', 'bing', 'api', ' * 这里使用动态数据获取,以便于锅巴动态更新数据 */ // const CONVERSATION_PRESERVE_TIME = Config.conversationPreserveTime -const defaultPropmtPrefix = ', a large language model trained by OpenAI. You answer as concisely as possible for each response (e.g. don’t be verbose). It is very important that you answer as concisely as possible, so please remember this. If you are generating a list, do not have too many items. Keep the number of items short.' const newFetch = (url, options = {}) => { const defaultOptions = Config.proxy ? { @@ -131,6 +57,7 @@ const newFetch = (url, options = {}) => { return fetch(url, mergedOptions) } + export class chatgpt extends plugin { constructor (e) { let toggleMode = Config.toggleMode @@ -167,10 +94,6 @@ export class chatgpt extends plugin { /** 执行方法 */ fnc: 'bing' }, - { - reg: '^#claude开启新对话', - fnc: 'newClaudeConversation' - }, { /** 命令正则匹配 */ reg: '^#claude(2|3|.ai)[sS]*', @@ -297,15 +220,18 @@ export class chatgpt extends plugin { } let handler = e.runtime?.handler || {} const btns = await handler.call('chatgpt.button.post', this.e, data) - const btnElement = { - type: 'button', - content: btns - } - if (Array.isArray(msg)) { - msg.push(btnElement) - } else { - msg = [msg, btnElement] + if (btns) { + const btnElement = { + type: 'button', + content: btns + } + if (Array.isArray(msg)) { + msg.push(btnElement) + } else { + msg = [msg, btnElement] + } } + return e.reply(msg, quote, data) } } @@ -339,360 +265,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 +711,7 @@ export class chatgpt extends plugin { num: 0 } } - } else if (use !== 'poe' && use !== 'claude') { + } else { switch (use) { case 'api': { key = `CHATGPT:CONVERSATIONS:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}` @@ -1146,10 +725,6 @@ export class chatgpt extends plugin { key = `CHATGPT:CONVERSATIONS_CHATGLM:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}` break } - case 'browser': { - key = `CHATGPT:CONVERSATIONS_BROWSER:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}` - break - } case 'claude2': { key = `CHATGPT:CLAUDE2_CONVERSATION:${e.sender.user_id}` break @@ -1174,6 +749,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 @@ -1185,7 +764,10 @@ export class chatgpt extends plugin { ctime, utime: ctime, num: 0, - messages: [{ role: 'system', content: 'You are an AI assistant that helps people find information.' }], + messages: [{ + role: 'system', + content: 'You are an AI assistant that helps people find information.' + }], conversation: {} }) previousConversation = JSON.parse(previousConversation) @@ -1209,14 +791,14 @@ export class chatgpt extends plugin { if (Config.debug) { logger.mark({ conversation }) } - let chatMessage = await this.sendMessage(prompt, conversation, use, e) + let chatMessage = await Core.sendMessage.bind(this)(prompt, conversation, use, e) if (chatMessage?.noMsg) { return false } // 处理星火和bard图片 if ((use === 'bard' || use === 'xh') && chatMessage?.images) { - chatMessage.images.forEach(async element => { - await this.reply([element.tag, segment.image(element.url)]) + chatMessage.images.forEach(element => { + this.reply([element.tag, segment.image(element.url)]) }) } // chatglm4图片,调整至sendMessage中处理 @@ -1224,7 +806,7 @@ export class chatgpt extends plugin { // 字数超限直接返回 return false } - if (use !== 'api3' && use !== 'poe' && use !== 'claude') { + if (use !== 'api3') { previousConversation.conversation = { conversationId: chatMessage.conversationId } @@ -1257,8 +839,6 @@ 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, '') let mood = 'blandness' if (!response) { await this.reply('没有任何回复', true) @@ -1347,7 +927,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) { @@ -1411,8 +991,7 @@ export class chatgpt extends plugin { await this.reply('合成语音发生错误~') } } - } else if (userSetting.usePicture || (Config.autoUsePicture && response.length > Config.autoUsePictureThreshold)) { - // todo use next api of chatgpt to complete incomplete respoonse + } else if (userSetting.usePicture || (!Config.enableMd && Config.autoUsePicture && response.length > Config.autoUsePictureThreshold)) { try { await this.renderImage(e, use, response, prompt, quotemessage, mood, chatMessage.suggestedResponses, imgUrls) } catch (err) { @@ -1429,10 +1008,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 @@ -1449,7 +1024,13 @@ export class chatgpt extends plugin { if (quotemessage.length > 0) { this.reply(await makeForwardMsg(this.e, quotemessage.map(msg => `${msg.text} - ${msg.url}`))) } - + if (chatMessage?.conversation && Config.enableSuggestedResponses && !chatMessage.suggestedResponses && Config.apiKey) { + try { + chatMessage.suggestedResponses = await generateSuggestedResponse(chatMessage.conversation) + } catch (err) { + logger.debug('生成建议回复失败', err) + } + } this.reply(responseText, e.isGroup, { btnData: { use, @@ -1466,7 +1047,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) } @@ -1474,11 +1055,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) } } } @@ -1513,7 +1094,7 @@ export class chatgpt extends plugin { } async glm4 (e) { - return await this.otherMode(e, 'chatglm4') + return await this.otherMode(e, 'chatglm4', '#glm4') } async gemini (e) { @@ -1525,7 +1106,13 @@ export class chatgpt extends plugin { } async cacheContent (e, use, content, prompt, quote = [], mood = '', suggest = '', imgUrls = []) { - let cacheData = { file: '', status: '' } + if (!Config.enableToolbox) { + return + } + let cacheData = { + file: '', + status: '' + } cacheData.file = randomString() const cacheresOption = { method: 'POST', @@ -1534,8 +1121,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, @@ -1566,986 +1153,22 @@ 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' - 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) } - } - - async sendMessage (prompt, conversation = {}, use, e) { - if (!conversation) { - conversation = { - timeoutMs: Config.defaultTimeoutMs - } - } - if (Config.debug) { - logger.mark(`using ${use} mode`) - } - const userData = await getUserData(e.user_id) - const useCast = userData.cast || {} - if (use === 'bing') { - let throttledTokens = [] - let { - bingToken, - allThrottled - } = await getAvailableBingToken(conversation, throttledTokens) - let cookies - if (bingToken?.indexOf('=') > -1) { - cookies = bingToken - } - let bingAIClient - const cacheOptions = { - namespace: Config.toneStyle, - store: new KeyvFile({ filename: 'cache.json' }) - } - bingAIClient = new SydneyAIClient({ - userToken: bingToken, // "_U" cookie from bing.com - cookies, - debug: Config.debug, - cache: cacheOptions, - user: e.sender.user_id, - proxy: Config.proxy - }) - // Sydney不实现上下文传递,删除上下文索引 - delete conversation.clientId - delete conversation.invocationId - delete conversation.conversationSignature - let response - let reply = '' - let retry = 3 - let errorMessage = '' - - do { - try { - let opt = _.cloneDeep(conversation) || {} - opt.toneStyle = Config.toneStyle - // 如果当前没有开启对话或者当前是Sydney模式、Custom模式,则本次对话携带拓展资料 - let c = await redis.get(`CHATGPT:CONVERSATIONS_BING:${e.sender.user_id}`) - if (!c) { - opt.context = useCast?.bing_resource || Config.sydneyContext - } - // 重新拿存储的token,因为可能之前有过期的被删了 - let abtrs = await getAvailableBingToken(conversation, throttledTokens) - bingToken = abtrs.bingToken - // eslint-disable-next-line no-unused-vars - allThrottled = abtrs.allThrottled - if (bingToken?.indexOf('=') > -1) { - cookies = bingToken - } - if (!bingAIClient.opts) { - bingAIClient.opts = {} - } - bingAIClient.opts.userToken = bingToken - bingAIClient.opts.cookies = cookies - // opt.messageType = allThrottled ? 'Chat' : 'SearchQuery' - if (Config.enableGroupContext && e.isGroup) { - try { - opt.groupId = e.group_id - opt.qq = e.sender.user_id - opt.nickname = e.sender.card - opt.groupName = e.group.name || e.group_name - opt.botName = e.isGroup ? (e.group.pickMember(getUin(e)).card || e.group.pickMember(getUin(e)).nickname) : e.bot.nickname - let master = (await getMasterQQ())[0] - if (master && e.group) { - opt.masterName = e.group.pickMember(parseInt(master)).card || e.group.pickMember(parseInt(master)).nickname - } - if (master && !e.group) { - opt.masterName = e.bot.getFriendList().get(parseInt(master))?.nickname - } - opt.chats = await getChatHistoryGroup(e, Config.groupContextLength) - } catch (err) { - logger.warn('获取群聊聊天记录失败,本次对话不携带聊天记录', err) - } - } - let toSummaryFileContent - try { - if (e.source) { - let seq = e.isGroup ? e.source.seq : e.source.time - if (e.adapter === 'shamrock') { - seq = e.source.message_id - } - let msgs = e.isGroup ? await e.group.getChatHistory(seq, 1) : await e.friend.getChatHistory(seq, 1) - let sourceMsg = msgs[msgs.length - 1] - let fileMsgElem = sourceMsg.file || sourceMsg.message.find(msg => msg.type === 'file') - if (fileMsgElem) { - toSummaryFileContent = await extractContentFromFile(fileMsgElem, e) - } - } - } catch (err) { - logger.warn('读取文件内容出错, 忽略文件内容', err) - } - opt.toSummaryFileContent = toSummaryFileContent - // 写入图片数据 - if (Config.sydneyImageRecognition) { - const image = await getImg(e) - opt.imageUrl = image ? image[0] : undefined - } - if (Config.enableGenerateContents) { - opt.onImageCreateRequest = prompt => { - logger.mark(`开始生成内容:${prompt}`) - if (Config.bingAPDraw) { - // 调用第三方API进行绘图 - let apDraw = new APTool() - apDraw.func({ - prompt - }, e) - } else { - let client = new BingDrawClient({ - baseUrl: Config.sydneyReverseProxy, - userToken: bingToken - }) - redis.set(`CHATGPT:DRAW:${e.sender.user_id}`, 'c', { EX: 30 }).then(() => { - try { - client.getImages(prompt, e) - } catch (err) { - redis.del(`CHATGPT:DRAW:${e.sender.user_id}`) - this.reply('绘图失败:' + err) - } - }) - } - } - } - response = await bingAIClient.sendMessage(prompt, opt, (token) => { - reply += token - }) - if (response.details.adaptiveCards?.[0]?.body?.[0]?.text?.trim()) { - if (response.response === undefined) { - response.response = response.details.adaptiveCards?.[0]?.body?.[0]?.text?.trim() - } - response.response = response.response.replace(/\[\^[0-9]+\^\]/g, (str) => { - return str.replace(/[/^]/g, '') - }) - // 有了新的引用属性 - // response.quote = response.details.adaptiveCards?.[0]?.body?.[0]?.text?.replace(/\[\^[0-9]+\^\]/g, '').replace(response.response, '').split('\n') - } - response.suggestedResponses = response.details.suggestedResponses?.map(s => s.text).join('\n') - // 新引用属性读取数据 - if (response.details.sourceAttributions) { - response.quote = [] - for (let quote of response.details.sourceAttributions) { - response.quote.push({ - text: quote.providerDisplayName || '', - url: quote.seeMoreUrl, - imageLink: quote.imageLink || '' - }) - } - } - // 如果token曾经有异常,则清除异常 - let Tokens = JSON.parse((await redis.get('CHATGPT:BING_TOKENS')) || '[]') - const TokenIndex = Tokens?.findIndex(element => element.Token === abtrs.bingToken) - if (TokenIndex > 0 && Tokens[TokenIndex].exception) { - delete Tokens[TokenIndex].exception - await redis.set('CHATGPT:BING_TOKENS', JSON.stringify(Tokens)) - } - errorMessage = '' - break - } catch (error) { - logger.error(error) - const message = error?.message || error?.data?.message || error || '出错了' - const { maxConv } = error - if (message && typeof message === 'string' && message.indexOf('CaptchaChallenge') > -1) { - if (bingToken) { - if (maxConv >= 20 && Config.bingCaptchaOneShotUrl) { - // maxConv为30说明token有效,可以通过解验证码码服务过码 - await this.reply('出现必应验证码,尝试解决中') - try { - let captchaResolveResult = await solveCaptchaOneShot(bingToken) - if (captchaResolveResult?.success) { - await this.reply('验证码已解决') - } else { - logger.error(captchaResolveResult) - errorMessage = message - await this.reply('验证码解决失败: ' + captchaResolveResult.error) - retry = 0 - } - } catch (err) { - logger.error(err) - await this.reply('验证码解决失败: ' + err) - retry = 0 - } - } else { - // 未登录用户maxConv目前为5或10,出验证码是ip或MUID问题 - logger.warn(`token [${bingToken}] 出现必应验证码,请前往网页版或app手动解决`) - errorMessage = message - retry = 0 - } - } else { - retry = 0 - } - } else if (message && typeof message === 'string' && message.indexOf('限流') > -1) { - throttledTokens.push(bingToken) - let bingTokens = JSON.parse(await redis.get('CHATGPT:BING_TOKENS')) - const badBingToken = bingTokens.findIndex(element => element.Token === bingToken) - const now = new Date() - const hours = now.getHours() - now.setHours(hours + 6) - bingTokens[badBingToken].State = '受限' - bingTokens[badBingToken].DisactivationTime = now - await redis.set('CHATGPT:BING_TOKENS', JSON.stringify(bingTokens)) - // 不减次数 - } else if (message && typeof message === 'string' && message.indexOf('UnauthorizedRequest') > -1) { - // token过期了 - let bingTokens = JSON.parse(await redis.get('CHATGPT:BING_TOKENS')) - const badBingToken = bingTokens.findIndex(element => element.Token === bingToken) - if (badBingToken > 0) { - // 可能是微软抽风,给三次机会 - if (bingTokens[badBingToken]?.exception) { - if (bingTokens[badBingToken].exception <= 3) { - bingTokens[badBingToken].exception += 1 - } else { - bingTokens[badBingToken].exception = 0 - bingTokens[badBingToken].State = '过期' - } - } else { - bingTokens[badBingToken].exception = 1 - } - await redis.set('CHATGPT:BING_TOKENS', JSON.stringify(bingTokens)) - } else { - retry = retry - 1 - } - errorMessage = 'UnauthorizedRequest:必应token不正确或已过期' - // logger.warn(`token${bingToken}疑似不存在或已过期,再试试`) - // retry = retry - 1 - } else { - retry-- - errorMessage = message === 'Timed out waiting for response. Try enabling debug mode to see more information.' ? (reply ? `${reply}\n不行了,我的大脑过载了,处理不过来了!` : '必应的小脑瓜不好使了,不知道怎么回答!') : message - } - } - } while (retry > 0) - if (errorMessage) { - if (errorMessage.includes('CaptchaChallenge')) { - if (bingToken) { - errorMessage = '出现验证码,请使用当前账户前往https://www.bing.com/chat或Edge侧边栏或移动端APP手动解除验证码' - } else { - errorMessage = '未配置必应账户,建议绑定必应账户再使用必应模式' - } - } - return { - text: errorMessage, - error: true - } - } else if (response?.response) { - return { - text: response?.response, - quote: response?.quote, - suggestedResponses: response.suggestedResponses, - conversationId: response.conversationId, - clientId: response.clientId, - invocationId: response.invocationId, - conversationSignature: response.conversationSignature, - parentMessageId: response.apology ? conversation.parentMessageId : response.messageId, - bingToken - } - } else { - logger.debug('no message') - return { - noMsg: true - } - } - } else if (use === 'api3') { - // official without cloudflare - let accessToken = await redis.get('CHATGPT:TOKEN') - if (!accessToken) { - throw new Error('未绑定ChatGPT AccessToken,请使用#chatgpt设置token命令绑定token') - } - this.chatGPTApi = new OfficialChatGPTClient({ - accessToken, - apiReverseUrl: Config.api, - timeoutMs: 120000 - }) - let sendMessageResult = await this.chatGPTApi.sendMessage(prompt, conversation) - // 更新最后一条prompt - await redis.set(`CHATGPT:CONVERSATION_LAST_MESSAGE_PROMPT:${sendMessageResult.conversationId}`, prompt) - // 更新最后一条messageId - await redis.set(`CHATGPT:CONVERSATION_LAST_MESSAGE_ID:${sendMessageResult.conversationId}`, sendMessageResult.id) - await redis.set(`CHATGPT:QQ_CONVERSATION:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`, sendMessageResult.conversationId) - if (!conversation.conversationId) { - // 如果是对话的创建者 - await redis.set(`CHATGPT:CONVERSATION_CREATER_ID:${sendMessageResult.conversationId}`, e.sender.user_id) - await redis.set(`CHATGPT:CONVERSATION_CREATER_NICK_NAME:${sendMessageResult.conversationId}`, e.sender.card) - } - return sendMessageResult - } else if (use === 'chatglm') { - const cacheOptions = { - namespace: 'chatglm_6b', - store: new KeyvFile({ filename: 'cache.json' }) - } - this.chatGPTApi = new ChatGLMClient({ - user: e.sender.user_id, - cache: cacheOptions - }) - let sendMessageResult = await this.chatGPTApi.sendMessage(prompt, conversation) - return sendMessageResult - } else if (use === 'poe') { - const cookie = await redis.get('CHATGPT:POE_TOKEN') - if (!cookie) { - throw new Error('未绑定Poe Cookie,请使用#chatgpt设置Poe token命令绑定cookie') - } - let client = new PoeClient({ - quora_cookie: cookie, - proxy: Config.proxy - }) - await client.setCredentials() - await client.getChatId() - let ai = 'a2' // todo - await client.sendMsg(ai, prompt) - const response = await client.getResponse(ai) - return { - text: response.data - } - } else if (use === 'claude') { - 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 - } - } else if (use === 'claude2') { - let { conversationId } = conversation - let client = new ClaudeAIClient({ - organizationId: Config.claudeAIOrganizationId, - sessionKey: Config.claudeAISessionKey, - debug: Config.debug, - proxy: Config.proxy - }) - let toSummaryFileContent - try { - if (e.source) { - let msgs = e.isGroup ? await e.group.getChatHistory(e.source.seq, 1) : await e.friend.getChatHistory(e.source.time, 1) - let sourceMsg = msgs[0] - let fileMsgElem = sourceMsg.message.find(msg => msg.type === 'file') - if (fileMsgElem) { - toSummaryFileContent = await extractContentFromFile(fileMsgElem, e) - } - } - } catch (err) { - logger.warn('读取文件内容出错, 忽略文件内容', err) - } - - let attachments = [] - if (toSummaryFileContent?.content) { - attachments.push({ - extracted_content: toSummaryFileContent.content, - file_name: toSummaryFileContent.name, - file_type: 'pdf', - file_size: 200312, - totalPages: 20 - }) - logger.info(toSummaryFileContent.content) - } - if (conversationId) { - return await client.sendMessage(prompt, conversationId, attachments) - } else { - let conv = await client.createConversation() - return await client.sendMessage(prompt, conv.uuid, attachments) - } - } else if (use === 'xh') { - const cacheOptions = { - namespace: 'xh', - store: new KeyvFile({ filename: 'cache.json' }) - } - const ssoSessionId = Config.xinghuoToken - if (!ssoSessionId) { - // throw new Error('未绑定星火token,请使用#chatgpt设置星火token命令绑定token。(获取对话页面的ssoSessionId cookie值)') - logger.warn('未绑定星火token,请使用#chatgpt设置星火token命令绑定token。(获取对话页面的ssoSessionId cookie值)') - } - let client = new XinghuoClient({ - ssoSessionId, - cache: cacheOptions - }) - // 获取图片资源 - const image = await getImg(e) - let response = await client.sendMessage(prompt, { - e, - chatId: conversation?.conversationId, - image: image ? image[0] : undefined, - system: Config.xhPrompt - }) - return response - } else if (use === 'azure') { - let azureModel - try { - azureModel = await import('@azure/openai') - } catch (error) { - throw new Error('未安装@azure/openai包,请执行pnpm install @azure/openai安装') - } - let OpenAIClient = azureModel.OpenAIClient - let AzureKeyCredential = azureModel.AzureKeyCredential - let msg = conversation.messages - let content = { - role: 'user', - content: prompt - } - msg.push(content) - const client = new OpenAIClient(Config.azureUrl, new AzureKeyCredential(Config.azApiKey)) - const deploymentName = Config.azureDeploymentName - const { choices } = await client.getChatCompletions(deploymentName, msg) - let completion = choices[0].message - return { - text: completion.content, - message: completion - } - } else if (use === 'qwen') { - let completionParams = { - parameters: { - top_p: Config.qwenTopP || 0.5, - top_k: Config.qwenTopK || 50, - seed: Config.qwenSeed > 0 ? Config.qwenSeed : Math.floor(Math.random() * 114514), - temperature: Config.qwenTemperature || 1, - enable_search: !!Config.qwenEnableSearch - } - } - if (Config.qwenModel) { - completionParams.model = Config.qwenModel - } - const currentDate = new Date().toISOString().split('T')[0] - - async function um (message) { - return await upsertMessage(message, 'QWEN') - } - - async function gm (id) { - return await getMessageById(id, 'QWEN') - } - - let opts = { - apiKey: Config.qwenApiKey, - debug: false, - upsertMessage: um, - getMessageById: gm, - systemMessage: `You are ${Config.assistantLabel} ${useCast?.api || Config.promptPrefixOverride || defaultPropmtPrefix} - Current date: ${currentDate}`, - completionParams, - assistantLabel: Config.assistantLabel, - fetch: newFetch - } - this.qwenApi = new QwenApi(opts) - let option = { - timeoutMs: 600000, - completionParams - } - if (conversation) { - if (!conversation.conversationId) { - conversation.conversationId = uuid() - } - option = Object.assign(option, conversation) - } - let msg - try { - msg = await this.qwenApi.sendMessage(prompt, option) - } catch (err) { - logger.error(err) - throw new Error(err) - } - return msg - } else if (use === 'bard') { - // 处理cookie - const matchesPSID = /__Secure-1PSID=([^;]+)/.exec(Config.bardPsid) - const matchesPSIDTS = /__Secure-1PSIDTS=([^;]+)/.exec(Config.bardPsid) - const cookie = { - '__Secure-1PSID': matchesPSID[1], - '__Secure-1PSIDTS': matchesPSIDTS[1] - } - if (!matchesPSID[1] || !matchesPSIDTS[1]) { - throw new Error('未绑定bard') - } - // 处理图片 - const image = await getImg(e) - let imageBuff - if (image) { - try { - let imgResponse = await fetch(image[0]) - if (imgResponse.ok) { - imageBuff = await imgResponse.arrayBuffer() - } - } catch (error) { - logger.warn(`错误的图片链接${image[0]}`) - } - } - // 发送数据 - let bot = new Bard(cookie, { - fetch, - bardURL: Config.bardForceUseReverse ? Config.bardReverseProxy : 'https://bard.google.com' - }) - let chat = await bot.createChat(conversation?.conversationId - ? { - conversationID: conversation.conversationId, - responseID: conversation.parentMessageId, - choiceID: conversation.clientId, - _reqID: conversation.invocationId - } - : {}) - let response = await chat.ask(prompt, { - image: imageBuff, - format: Bard.JSON - }) - return { - conversationId: response.ids.conversationID, - responseID: response.ids.responseID, - choiceID: response.ids.choiceID, - _reqID: response.ids._reqID, - text: response.content, - images: response.images - } - } else if (use === 'gemini') { - let client = new CustomGoogleGeminiClient({ - e, - userId: e.sender.user_id, - key: Config.geminiKey, - model: Config.geminiModel, - baseUrl: Config.geminiBaseUrl, - debug: Config.debug - }) - let option = { - stream: false, - onProgress: (data) => { - if (Config.debug) { - logger.info(data) - } + // 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) }, - parentMessageId: conversation.parentMessageId, - conversationId: conversation.conversationId - } - if (Config.geminiModel.includes('vision')) { - const image = await getImg(e) - let imageUrl = image ? image[0] : undefined - if (imageUrl) { - let md5 = imageUrl.split(/[/-]/).find(s => s.length === 32)?.toUpperCase() - let imageLoc = await getOrDownloadFile(`ocr/${md5}.png`, imageUrl) - let outputLoc = imageLoc.replace(`${md5}.png`, `${md5}_512.png`) - await resizeAndCropImage(imageLoc, outputLoc, 512) - let buffer = fs.readFileSync(outputLoc) - option.image = buffer.toString('base64') - } - } - if (Config.smartMode) { - /** - * @type {AbstractTool[]} - */ - let tools = [ - new QueryStarRailTool(), - new WebsiteTool(), - new SendPictureTool(), - new SendVideoTool(), - // new ImageCaptionTool(), - new SearchVideoTool(), - new SendAvatarTool(), - new SerpImageTool(), - new SearchMusicTool(), - new SendMusicTool(), - // new SerpIkechan8370Tool(), - // new SerpTool(), - new SendAudioMessageTool(), - // new ProcessPictureTool(), - new APTool(), - // new HandleMessageMsgTool(), - new SendMessageToSpecificGroupOrUserTool(), - // new SendDiceTool(), - new QueryGenshinTool() - ] - if (Config.amapKey) { - tools.push(new WeatherTool()) - } - if (e.isGroup) { - tools.push(new QueryUserinfoTool()) - // let self = e.group.pickMember(e.self_id) - if (e.group.is_admin || e.group.is_owner) { - tools.push(new EditCardTool()) - tools.push(new JinyanTool()) - tools.push(new KickOutTool()) - } - if (e.group.is_owner) { - tools.push(new SetTitleTool()) - } - } - switch (Config.serpSource) { - case 'ikechan8370': { - tools.push(new SerpIkechan8370Tool()) - break - } - case 'azure': { - if (!Config.azSerpKey) { - logger.warn('未配置bing搜索密钥,转为使用ikechan8370搜索源') - tools.push(new SerpIkechan8370Tool()) - } else { - tools.push(new SerpTool()) - } - break - } - default: { - tools.push(new SerpIkechan8370Tool()) - } - } - client.addTools(tools) - } - let system = Config.geminiPrompt - if (Config.enableGroupContext && e.isGroup) { - let chats = await getChatHistoryGroup(e, Config.groupContextLength) - const namePlaceholder = '[name]' - const defaultBotName = 'GeminiPro' - const groupContextTip = Config.groupContextTip - let botName = e.isGroup ? (e.group.pickMember(getUin(e)).card || e.group.pickMember(getUin(e)).nickname) : e.bot.nickname - system = system.replaceAll(namePlaceholder, botName || defaultBotName) + - ((Config.enableGroupContext && e.group_id) ? groupContextTip : '') - system += 'Attention, you are currently chatting in a qq group, then one who asks you now is' + `${e.sender.card || e.sender.nickname}(${e.sender.user_id}).` - system += `the group name is ${e.group.name || e.group_name}, group id is ${e.group_id}.` - system += `Your nickname is ${botName} in the group,` - if (chats) { - system += 'There is the conversation history in the group, you must chat according to the conversation history context"' - system += chats - .map(chat => { - let sender = chat.sender || {} - return `【${sender.card || sender.nickname}】(qq:${sender.user_id}, ${roleMap[sender.role] || 'normal user'},${sender.area ? 'from ' + sender.area + ', ' : ''} ${sender.age} years old, 群头衔:${sender.title}, gender: ${sender.sex}, time:${formatDate(new Date(chat.time * 1000))}, messageId: ${chat.message_id}) 说:${chat.raw_message}` - }) - .join('\n') - } - } - option.system = system - return await client.sendMessage(prompt, option) - } else if (use === 'chatglm4') { - const client = new ChatGLM4Client({ - refreshToken: Config.chatglmRefreshToken - }) - let resp = await client.sendMessage(prompt, conversation) - if (resp.image) { - this.reply(segment.image(resp.image), true) - } - return resp - } else { - // openai api - let completionParams = {} - if (Config.model) { - completionParams.model = Config.model - } - const currentDate = new Date().toISOString().split('T')[0] - let promptPrefix = `You are ${Config.assistantLabel} ${useCast?.api || Config.promptPrefixOverride || defaultPropmtPrefix} - Current date: ${currentDate}` - let maxModelTokens = getMaxModelTokens(completionParams.model) - let system = promptPrefix - if (maxModelTokens >= 16000 && Config.enableGroupContext) { - try { - let opt = {} - opt.groupId = e.group_id - opt.qq = e.sender.user_id - opt.nickname = e.sender.card - opt.groupName = e.group.name || e.group_name - opt.botName = e.isGroup ? (e.group.pickMember(getUin(e)).card || e.group.pickMember(getUin(e)).nickname) : e.bot.nickname - let master = (await getMasterQQ())[0] - if (master && e.group) { - opt.masterName = e.group.pickMember(parseInt(master)).card || e.group.pickMember(parseInt(master)).nickname - } - if (master && !e.group) { - opt.masterName = e.bot.getFriendList().get(parseInt(master))?.nickname - } - let chats = await getChatHistoryGroup(e, Config.groupContextLength) - opt.chats = chats - const namePlaceholder = '[name]' - const defaultBotName = 'ChatGPT' - const groupContextTip = Config.groupContextTip - system = system.replaceAll(namePlaceholder, opt.botName || defaultBotName) + - ((Config.enableGroupContext && opt.groupId) ? groupContextTip : '') - system += 'Attention, you are currently chatting in a qq group, then one who asks you now is' + `${opt.nickname}(${opt.qq})。` - system += `the group name is ${opt.groupName}, group id is ${opt.groupId}。` - if (opt.botName) { - system += `Your nickname is ${opt.botName} in the group,` - } - if (chats) { - system += 'There is the conversation history in the group, you must chat according to the conversation history context"' - system += chats - .map(chat => { - let sender = chat.sender || {} - // if (sender.user_id === e.bot.uin && chat.raw_message.startsWith('建议的回复')) { - if (chat.raw_message.startsWith('建议的回复')) { - // 建议的回复太容易污染设定导致对话太固定跑偏了 - return '' - } - return `【${sender.card || sender.nickname}】(qq:${sender.user_id}, ${roleMap[sender.role] || 'normal user'},${sender.area ? 'from ' + sender.area + ', ' : ''} ${sender.age} years old, 群头衔:${sender.title}, gender: ${sender.sex}, time:${formatDate(new Date(chat.time * 1000))}, messageId: ${chat.message_id}) 说:${chat.raw_message}` - }) - .join('\n') - } - } catch (err) { - if (e.isGroup) { - logger.warn('获取群聊聊天记录失败,本次对话不携带聊天记录', err) - } - } - // logger.info(system) - } - let opts = { - apiBaseUrl: Config.openAiBaseUrl, - apiKey: Config.apiKey, - debug: false, - upsertMessage, - getMessageById, - systemMessage: system, - completionParams, - assistantLabel: Config.assistantLabel, - fetch: newFetch, - maxModelTokens - } - let openAIAccessible = (Config.proxy || !(await isCN())) // 配了代理或者服务器在国外,默认认为不需要反代 - if (opts.apiBaseUrl !== defaultOpenAIAPI && openAIAccessible && !Config.openAiForceUseReverse) { - // 如果配了proxy(或者不在国内),而且有反代,但是没开启强制反代,将baseurl删掉 - delete opts.apiBaseUrl - } - this.chatGPTApi = new ChatGPTAPI(opts) - let option = { - timeoutMs: 600000, - completionParams, - stream: Config.apiStream, - onProgress: (data) => { - if (Config.debug) { - logger.info(data?.text || data.functionCall || data) - } - } - // systemMessage: promptPrefix - } - option.systemMessage = system - if (conversation) { - if (!conversation.conversationId) { - conversation.conversationId = uuid() - } - option = Object.assign(option, conversation) - } - if (Config.smartMode) { - let isAdmin = e.sender.role === 'admin' || e.sender.role === 'owner' - let sender = e.sender.user_id - let serpTool - switch (Config.serpSource) { - case 'ikechan8370': { - serpTool = new SerpIkechan8370Tool() - break - } - case 'azure': { - if (!Config.azSerpKey) { - logger.warn('未配置bing搜索密钥,转为使用ikechan8370搜索源') - serpTool = new SerpIkechan8370Tool() - } else { - serpTool = new SerpTool() - } - break - } - default: { - serpTool = new SerpIkechan8370Tool() - } - } - let fullTools = [ - new EditCardTool(), - new QueryStarRailTool(), - new WebsiteTool(), - new JinyanTool(), - new KickOutTool(), - new WeatherTool(), - new SendPictureTool(), - new SendVideoTool(), - new ImageCaptionTool(), - new SearchVideoTool(), - new SendAvatarTool(), - new SerpImageTool(), - new SearchMusicTool(), - new SendMusicTool(), - new SerpIkechan8370Tool(), - new SerpTool(), - new SendAudioMessageTool(), - new ProcessPictureTool(), - new APTool(), - new HandleMessageMsgTool(), - new QueryUserinfoTool(), - new EliMusicTool(), - new EliMovieTool(), - new SendMessageToSpecificGroupOrUserTool(), - new SendDiceTool(), - new QueryGenshinTool(), - new SetTitleTool() - ] - // todo 3.0再重构tool的插拔和管理 - let tools = [ - new SendAvatarTool(), - new SendDiceTool(), - new SendMessageToSpecificGroupOrUserTool(), - // new EditCardTool(), - new QueryStarRailTool(), - new QueryGenshinTool(), - new ProcessPictureTool(), - new WebsiteTool(), - // new JinyanTool(), - // new KickOutTool(), - new WeatherTool(), - new SendPictureTool(), - new SendAudioMessageTool(), - new APTool(), - // new HandleMessageMsgTool(), - serpTool, - new QueryUserinfoTool() - ] - try { - await import('../../avocado-plugin/apps/avocado.js') - tools.push(...[new EliMusicTool(), new EliMovieTool()]) - } catch (err) { - tools.push(...[new SendMusicTool(), new SearchMusicTool()]) - logger.mark(logger.green('【ChatGPT-Plugin】插件avocado-plugin未安装') + ',安装后可查看最近热映电影与体验可玩性更高的点歌工具。\n可前往 https://github.com/Qz-Sean/avocado-plugin 获取') - } - if (e.isGroup) { - let botInfo = await e.bot.getGroupMemberInfo(e.group_id, getUin(e), true) - if (botInfo.role !== 'member') { - // 管理员才给这些工具 - tools.push(...[new EditCardTool(), new JinyanTool(), new KickOutTool(), new HandleMessageMsgTool(), new SetTitleTool()]) - // 用于撤回和加精的id - if (e.source?.seq) { - let source = (await e.group.getChatHistory(e.source?.seq, 1)).pop() - option.systemMessage += `\nthe last message is replying to ${source.message_id}"\n` - } else { - option.systemMessage += `\nthe last message id is ${e.message_id}. ` - } - } - } - let img = await getImg(e) - if (img?.length > 0 && Config.extraUrl) { - tools.push(new ImageCaptionTool()) - tools.push(new ProcessPictureTool()) - prompt += `\nthe url of the picture(s) above: ${img.join(', ')}` - } else { - tools.push(new SerpImageTool()) - tools.push(...[new SearchVideoTool(), - new SendVideoTool()]) - } - let funcMap = {} - let fullFuncMap = {} - tools.forEach(tool => { - funcMap[tool.name] = { - exec: tool.func, - function: tool.function() - } - }) - fullTools.forEach(tool => { - fullFuncMap[tool.name] = { - exec: tool.func, - function: tool.function() - } - }) - if (!option.completionParams) { - option.completionParams = {} - } - option.completionParams.functions = Object.keys(funcMap).map(k => funcMap[k].function) - let msg - try { - msg = await this.chatGPTApi.sendMessage(prompt, option) - logger.info(msg) - while (msg.functionCall) { - if (msg.text) { - await this.reply(msg.text.replace('\n\n\n', '\n')) - } - let { - name, - arguments: args - } = msg.functionCall - args = JSON.parse(args) - // 感觉换成targetGroupIdOrUserQQNumber这种表意比较清楚的变量名,效果会好一丢丢 - if (!args.groupId) { - args.groupId = e.group_id + '' || e.sender.user_id + '' - } - try { - parseInt(args.groupId) - } catch (err) { - args.groupId = e.group_id + '' || e.sender.user_id + '' - } - let functionResult = await fullFuncMap[name.trim()].exec(Object.assign({ - isAdmin, - sender - }, args), e) - logger.mark(`function ${name} execution result: ${functionResult}`) - option.parentMessageId = msg.id - option.name = name - // 不然普通用户可能会被openai限速 - await common.sleep(300) - msg = await this.chatGPTApi.sendMessage(functionResult, option, 'function') - logger.info(msg) - } - } catch (err) { - if (err.message?.indexOf('context_length_exceeded') > 0) { - logger.warn(err) - await redis.del(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`) - await redis.del(`CHATGPT:WRONG_EMOTION:${e.sender.user_id}`) - await this.reply('字数超限啦,将为您自动结束本次对话。') - return null - } else { - logger.error(err) - throw new Error(err) - } - } - return msg - } else { - let msg - try { - msg = await this.chatGPTApi.sendMessage(prompt, option) - } catch (err) { - if (err.message?.indexOf('context_length_exceeded') > 0) { - logger.warn(err) - await redis.del(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`) - await redis.del(`CHATGPT:WRONG_EMOTION:${e.sender.user_id}`) - await this.reply('字数超限啦,将为您自动结束本次对话。') - return null - } else { - logger.error(err) - throw new Error(err) - } - } - return msg - } + func: (parseFloat(Config.live2d) && !Config.viewHost) ? 'window.Live2d == true' : '', + deviceScaleFactor: parseFloat(Config.cloudDPR) + }), e.isGroup && Config.quoteReply) } } - 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') { @@ -2667,7 +1290,10 @@ export class chatgpt extends plugin { logger.mark('all conversations: ', conversations) } // let conversationsFirst10 = conversations.slice(0, 10) - await render(e, 'chatgpt-plugin', 'conversation/chatgpt', { conversations, version }) + await render(e, 'chatgpt-plugin', 'conversation/chatgpt', { + conversations, + version + }) let text = '对话列表\n' text += '对话id | 对话发起者 \n' conversations.forEach(c => { @@ -2753,9 +1379,16 @@ export class chatgpt extends plugin { start: beforeTomorrowFormatted } } + let subscription = await subscriptionRes.json() - let { hard_limit_usd: hardLimit, access_until: expiresAt } = subscription - const { end, start } = getDates() + let { + hard_limit_usd: hardLimit, + access_until: expiresAt + } = subscription + const { + end, + start + } = getDates() let usageRes = await newFetch(`${Config.openAiBaseUrl}/dashboard/billing/usage?start_date=${start}&end_date=${end}`, { method: 'GET', headers: { @@ -2769,30 +1402,6 @@ export class chatgpt extends plugin { this.reply('总额度:$' + hardLimit + '\n已经使用额度:$' + totalUsage / 100 + '\n当前剩余额度:$' + left + '\n到期日期(UTC):' + expiresAt) } - /** - * #chatgpt - * @param prompt 问题 - * @param conversation 对话 - */ - async chatgptBrowserBased (prompt, conversation) { - let option = { markdown: true } - if (Config['2captchaToken']) { - option.captchaToken = Config['2captchaToken'] - } - // option.debug = true - option.email = Config.username - option.password = Config.password - this.chatGPTApi = new ChatGPTPuppeteer(option) - logger.info(`chatgpt prompt: ${prompt}`) - let sendMessageOption = { - timeoutMs: 120000 - } - if (conversation) { - sendMessageOption = Object.assign(sendMessageOption, conversation) - } - return await this.chatGPTApi.sendMessage(prompt, sendMessageOption) - } - /** * 其他模式 * @param e @@ -2819,56 +1428,3 @@ export class chatgpt extends plugin { return true } } - -async function getAvailableBingToken (conversation, throttled = []) { - let allThrottled = false - if (!await redis.get('CHATGPT:BING_TOKENS')) { - return { - bingToken: null, - allThrottled - } - // throw new Error('未绑定Bing Cookie,请使用#chatgpt设置必应token命令绑定Bing Cookie') - } - - let bingToken = '' - let bingTokens = JSON.parse(await redis.get('CHATGPT:BING_TOKENS')) - const normal = bingTokens.filter(element => element.State === '正常') - const restricted = bingTokens.filter(element => element.State === '受限') - - // 判断受限的token是否已经可以解除 - for (const restrictedToken of restricted) { - const now = new Date() - const tk = new Date(restrictedToken.DisactivationTime) - if (tk <= now) { - const index = bingTokens.findIndex(element => element.Token === restrictedToken.Token) - bingTokens[index].Usage = 0 - bingTokens[index].State = '正常' - } - } - if (normal.length > 0) { - const minElement = normal.reduce((min, current) => { - return current.Usage < min.Usage ? current : min - }) - bingToken = minElement.Token - } else if (restricted.length > 0 && restricted.some(x => throttled.includes(x.Token))) { - allThrottled = true - const minElement = restricted.reduce((min, current) => { - return current.Usage < min.Usage ? current : min - }) - bingToken = minElement.Token - } else { - // throw new Error('全部Token均已失效,暂时无法使用') - return { - bingToken: null, - allThrottled - } - } - // 记录使用情况 - const index = bingTokens.findIndex(element => element.Token === bingToken) - bingTokens[index].Usage += 1 - await redis.set('CHATGPT:BING_TOKENS', JSON.stringify(bingTokens)) - return { - bingToken, - allThrottled - } -} diff --git a/apps/entertainment.js b/apps/entertainment.js index b977a5a..b0ba5b3 100644 --- a/apps/entertainment.js +++ b/apps/entertainment.js @@ -4,7 +4,6 @@ import { generateHello } from '../utils/randomMessage.js' import { generateVitsAudio } from '../utils/tts.js' import fs from 'fs' import { emojiRegex, googleRequestUrl } from '../utils/emoj/index.js' -import fetch from 'node-fetch' import { getImageOcrText, getImg, makeForwardMsg, mkdirs, renderUrl } from '../utils/common.js' import uploadRecord from '../utils/uploadRecord.js' import { makeWordcloud } from '../utils/wordcloud/wordcloud.js' @@ -343,7 +342,7 @@ ${translateLangLabels} logger.info('combine ' + e.msg) let resultFileLoc = `data/chatgpt/emoji/${left}_${right}.jpg` if (fs.existsSync(resultFileLoc)) { - let image = segment.image(fs.createReadStream(resultFileLoc)) + let image = segment.image(resultFileLoc) image.asface = true await this.reply(image, true) return true @@ -370,12 +369,12 @@ ${translateLangLabels} await this.reply('不支持合成', true) return false } - let response = await fetch(url) - const resultBlob = await response.blob() - const resultArrayBuffer = await resultBlob.arrayBuffer() - const resultBuffer = Buffer.from(resultArrayBuffer) - await fs.writeFileSync(resultFileLoc, resultBuffer) - let image = segment.image(fs.createReadStream(resultFileLoc)) + // let response = await fetch(url) + // const resultBlob = await response.blob() + // const resultArrayBuffer = await resultBlob.arrayBuffer() + // const resultBuffer = Buffer.from(resultArrayBuffer) + // await fs.writeFileSync(resultFileLoc, resultBuffer) + let image = segment.image(url) image.asface = true await this.reply(image, true) return true diff --git a/apps/management.js b/apps/management.js index 5d0d61c..f77a4f2 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) { @@ -98,13 +99,8 @@ export class ChatgptManagement extends plugin { permission: 'master' }, { - reg: '^#chatgpt切换(Poe|poe)$', - fnc: 'useClaudeBasedSolution', - permission: 'master' - }, - { - reg: '^#chatgpt切换(Claude|claude|slack)$', - fnc: 'useSlackClaudeBasedSolution', + reg: '^#chatgpt切换(Claude|claude)$', + fnc: 'useClaudeAPIBasedSolution', permission: 'master' }, { @@ -181,6 +177,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', @@ -322,6 +323,11 @@ export class ChatgptManagement extends plugin { fnc: 'setXinghuoModel', permission: 'master' }, + { + reg: '^#chatgpt设置(claude|Claude)模型$', + fnc: 'setClaudeModel', + permission: 'master' + }, { reg: '^#chatgpt必应(禁用|禁止|关闭|启用|开启)搜索$', fnc: 'switchBingSearch', @@ -336,6 +342,11 @@ export class ChatgptManagement extends plugin { reg: '^#chatgpt(开启|关闭)(api|API)流$', fnc: 'switchStream', permission: 'master' + }, + { + reg: '^#chatgpt(开启|关闭)(工具箱|后台服务)$', + fnc: 'switchToolbox', + permission: 'master' } ] }) @@ -920,23 +931,13 @@ azure语音:Azure 语音是微软 Azure 平台提供的一项语音服务, } } - async useClaudeBasedSolution (e) { - let use = await redis.get('CHATGPT:USE') - if (use !== 'poe') { - await redis.set('CHATGPT:USE', 'poe') - await this.reply('已切换到基于Quora\'s POE的解决方案') - } else { - await this.reply('当前已经是POE模式了') - } - } - - async useSlackClaudeBasedSolution () { + async useClaudeAPIBasedSolution () { let use = await redis.get('CHATGPT:USE') if (use !== 'claude') { await redis.set('CHATGPT:USE', 'claude') - await this.reply('已切换到基于slack claude机器人的解决方案') + await this.reply('已切换到基于ClaudeAPI的解决方案') } else { - await this.reply('当前已经是claude模式了') + await this.reply('当前已经是Claude模式了') } } @@ -946,7 +947,7 @@ azure语音:Azure 语音是微软 Azure 平台提供的一项语音服务, await redis.set('CHATGPT:USE', 'claude2') await this.reply('已切换到基于claude.ai的解决方案') } else { - await this.reply('当前已经是claude2模式了') + await this.reply('当前已经是claude.ai模式了') } } @@ -1268,6 +1269,25 @@ azure语音:Azure 语音是微软 Azure 平台提供的一项语音服务, this.finish('saveAPIKey') } + async setClaudeKey (e) { + this.setContext('saveClaudeKey') + await this.reply('请发送Claude API Key。\n如果要设置多个key请用逗号隔开。\n此操作会覆盖当前配置,请谨慎操作', 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.claudeApiKey = 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) @@ -1688,6 +1708,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\nclaude-3-haiku-20240307', 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) @@ -1788,4 +1822,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..e768cea 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 () { @@ -34,7 +34,8 @@ function transUse (use) { qwen: '通义千问 ' + Config.qwenModel, claude2: 'Claude 3 Sonnet', glm4: 'ChatGLM4', - chat3: 'ChatGPT官网' + chat3: 'ChatGPT官网', + claude: Config.claudeApiModel } return useMap[use] || use } diff --git a/apps/prompts.js b/apps/prompts.js index ab22a06..481d2ad 100644 --- a/apps/prompts.js +++ b/apps/prompts.js @@ -1,10 +1,8 @@ import plugin from '../../../lib/plugins/plugin.js' -import fs from 'fs' -import _ from 'lodash' import { Config } from '../utils/config.js' import { getMasterQQ, limitString, makeForwardMsg, maskQQ, getUin } from '../utils/common.js' import { deleteOnePrompt, getPromptByName, readPrompts, saveOnePrompt } from '../utils/prompts.js' -import AzureTTS from "../utils/tts/microsoft-azure.js"; +import AzureTTS from '../utils/tts/microsoft-azure.js' export class help extends plugin { constructor (e) { super({ @@ -66,21 +64,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 +135,7 @@ export class help extends plugin { const keyMap = { api: 'promptPrefixOverride', bing: 'sydney', - claude: 'slackClaudeGlobalPreset', + claude: 'claudeSystemPrompt', qwen: 'promptPrefixOverride', gemini: 'geminiPrompt', xh: 'xhPrompt' @@ -168,6 +151,20 @@ export class help extends plugin { if (use === 'xh') { Config.xhPromptSerialize = false } + if (use === 'bing') { + /** + * @type {{user: string, bot: string}[]} examples + */ + let examples = prompt.example + for (let i = 1; i <= 3; i++) { + Config[`chatExampleUser${i}`] = '' + Config[`chatExampleBot${i}`] = '' + } + for (let i = 1; i <= examples.length; i++) { + Config[`chatExampleUser${i}`] = examples[i - 1].user + Config[`chatExampleBot${i}`] = examples[i - 1].bot + } + } await redis.set(`CHATGPT:PROMPT_USE_${use}`, promptName) await e.reply(`你当前正在使用${use}模式,已将该模式设定应用为"${promptName}"。更该设定后建议结束对话以使设定更好生效`, true) } else { @@ -347,13 +344,23 @@ export class help extends plugin { let extraData = JSON.parse(await redis.get('CHATGPT:UPLOAD_PROMPT')) const { currentUse, description } = extraData const { content } = getPromptByName(currentUse) + let examples = [] + for (let i = 1; i < 4; i++) { + if (Config[`chatExampleUser${i}`]) { + examples.push({ + user: Config[`chatExampleUser${i}`], + bot: Config[`chatExampleBot${i}`] + }) + } + } let toUploadBody = { title: currentUse, prompt: content, qq: master || (getUin(this.e) + ''), // 上传者设定为主人qq或机器人qq use: extraData.use === 'bing' ? 'Bing' : 'ChatGPT', r18, - description + description, + examples } logger.info(toUploadBody) let response = await fetch('https://chatgpt.roki.best/prompt', { @@ -448,8 +455,8 @@ export class help extends plugin { await e.reply('没有这个设定', true) return true } - const { prompt, title } = r.data - saveOnePrompt(title, prompt) + const { prompt, title, examples } = r.data + saveOnePrompt(title, prompt, examples) e.reply(`导入成功。您现在可以使用 #chatgpt使用设定${title} 来体验这个设定了。`) } else { await e.reply('导入失败:' + r.msg) diff --git a/apps/update.js b/apps/update.js index 4de2b6d..f22c8a6 100644 --- a/apps/update.js +++ b/apps/update.js @@ -3,21 +3,11 @@ import plugin from '../../../lib/plugins/plugin.js' import { createRequire } from 'module' import _ from 'lodash' import { Restart } from '../../other/restart.js' -import fs from 'fs' import {} from '../utils/common.js' -const _path = process.cwd() const require = createRequire(import.meta.url) const { exec, execSync } = require('child_process') -const checkAuth = async function (e) { - if (!e.isMaster) { - e.reply('只有主人才能命令ChatGPT哦~(*/ω\*)') - return false - } - return true -} - // 是否在更新中 let uping = false diff --git a/apps/vocal.js b/apps/vocal.js index 74ce9d8..e6e1e90 100644 --- a/apps/vocal.js +++ b/apps/vocal.js @@ -1,7 +1,7 @@ import plugin from '../../../lib/plugins/plugin.js' import { SunoClient } from '../client/SunoClient.js' import { Config } from '../utils/config.js' -import { downloadFile, maskEmail } from '../utils/common.js' +import { maskEmail } from '../utils/common.js' import common from '../../../lib/common/common.js' import lodash from 'lodash' @@ -86,6 +86,10 @@ export class Vocal extends plugin { } let songs = await client.createSong(description) + if (!songs || songs.length === 0) { + e.reply('生成失败,可能是提示词太长或者违规,请检查日志') + return + } let messages = ['提示词:' + description] for (let song of songs) { messages.push(`歌名:${song.title}\n风格: ${song.metadata.tags}\n长度: ${lodash.round(song.metadata.duration, 0)}秒\n歌词:\n${song.metadata.prompt}\n`) diff --git a/client/ClaudeAPIClient.js b/client/ClaudeAPIClient.js new file mode 100644 index 0000000..15072b9 --- /dev/null +++ b/client/ClaudeAPIClient.js @@ -0,0 +1,195 @@ +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 + * @property {{ + * type: string, + * message: string, + * }} error + * 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)) + } + if (response.type === 'error') { + logger.error(response.error.message) + throw new Error(response.error.type) + } + 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/SunoClient.js b/client/SunoClient.js index 4e99e13..70bd0c9 100644 --- a/client/SunoClient.js +++ b/client/SunoClient.js @@ -65,14 +65,18 @@ export class SunoClient { Authorization: `Bearer ${sess}` } }) + if (queryRes.status === 401) { + sess = await this.getToken() + continue + } if (queryRes.status !== 200) { logger.error(await queryRes.text()) console.error('Failed to query song') } let queryData = await queryRes.json() logger.debug(queryData) - allDone = queryData.every(clip => clip.status === 'complete') - songs = queryData + allDone = queryData.every(clip => clip.status === 'complete' || clip.status === 'error') + songs = queryData.filter(clip => clip.status === 'complete') } catch (err) { console.error(err) } 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/config/config.example.json b/config/config.example.json index d42ceb8..8c4fed0 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -89,13 +89,6 @@ "whitelist": [], "blacklist": [], "ttsRegex": "/匹配规则/匹配模式", - "slackUserToken": "", - "slackBotUserToken": "", - "slackSigningSecret": "", - "slackClaudeUserId": "", - "slackClaudeEnableGlobalPreset": true, - "slackClaudeGlobalPreset": "", - "slackClaudeSpecifiedChannel": "", "cloudTranscode": "https://silk.201666.xyz", "cloudRender": false, "cloudMode": "url", diff --git a/guoba.support.js b/guoba.support.js index 3115d31..d0cafd6 100644 --- a/guoba.support.js +++ b/guoba.support.js @@ -241,6 +241,12 @@ export function supportGuoba () { bottomHelpMessage: '将输出更多调试信息,如果不希望控制台刷屏的话,可以关闭', component: 'Switch' }, + { + field: 'enableToolbox', + label: '开启工具箱', + bottomHelpMessage: '独立的后台管理面板(默认3321端口),与锅巴类似。工具箱会有额外占用,启动速度稍慢,酌情开启。修改后需重启生效!!!', + component: 'Switch' + }, { field: 'enableMd', label: 'QQ开启markdown', @@ -479,6 +485,42 @@ export function supportGuoba () { bottomHelpMessage: '开启Sydney的图片识别功能,建议和OCR只保留一个开启', component: 'Switch' }, + { + field: 'chatExampleUser1', + label: '前置对话第一轮(用户)', + bottomHelpMessage: '会强行插入该轮对话,能有效抑制抱歉', + component: 'InputTextArea' + }, + { + field: 'chatExampleBot1', + label: '前置对话第一轮(AI)', + bottomHelpMessage: '会强行插入该轮对话,能有效抑制抱歉', + component: 'InputTextArea' + }, + { + field: 'chatExampleUser2', + label: '前置对话第二轮(用户)', + bottomHelpMessage: '会强行插入该轮对话,能有效抑制抱歉', + component: 'InputTextArea' + }, + { + field: 'chatExampleBot2', + label: '前置对话第二轮(AI)', + bottomHelpMessage: '会强行插入该轮对话,能有效抑制抱歉', + component: 'InputTextArea' + }, + { + field: 'chatExampleUser3', + label: '前置对话第三轮(用户)', + bottomHelpMessage: '会强行插入该轮对话,能有效抑制抱歉', + component: 'InputTextArea' + }, + { + field: 'chatExampleBot3', + label: '前置对话第三轮(AI)', + bottomHelpMessage: '会强行插入该轮对话,能有效抑制抱歉', + component: 'InputTextArea' + }, { label: '以下为API3方式的配置', component: 'Divider' @@ -518,50 +560,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/model/core.js b/model/core.js new file mode 100644 index 0000000..235c035 --- /dev/null +++ b/model/core.js @@ -0,0 +1,1159 @@ +import { Config, defaultOpenAIAPI } from '../utils/config.js' +import { + extractContentFromFile, + formatDate, + getImg, + getMasterQQ, getMaxModelTokens, + getOrDownloadFile, + getUin, + getUserData, + isCN +} from '../utils/common.js' +import { KeyvFile } from 'keyv-file' +import SydneyAIClient from '../utils/SydneyAIClient.js' +import _ from 'lodash' +import { getChatHistoryGroup } from '../utils/chat.js' +import { APTool } from '../utils/tools/APTool.js' +import BingDrawClient from '../utils/BingDraw.js' +import { solveCaptchaOneShot } from '../utils/bingCaptcha.js' +import { OfficialChatGPTClient } from '../utils/message.js' +import ChatGLMClient from '../utils/chatglm.js' +import { ClaudeAPIClient } from '../client/ClaudeAPIClient.js' +import { ClaudeAIClient } from '../utils/claude.ai/index.js' +import XinghuoClient from '../utils/xinghuo/xinghuo.js' +import { getMessageById, upsertMessage } from '../utils/history.js' +import { v4 as uuid } from 'uuid' +import fetch from 'node-fetch' +import Bard from '../utils/bard.js' +import { CustomGoogleGeminiClient } from '../client/CustomGoogleGeminiClient.js' +import { resizeAndCropImage } from '../utils/dalle.js' +import fs from 'fs' +import { QueryStarRailTool } from '../utils/tools/QueryStarRailTool.js' +import { WebsiteTool } from '../utils/tools/WebsiteTool.js' +import { SendPictureTool } from '../utils/tools/SendPictureTool.js' +import { SendVideoTool } from '../utils/tools/SendBilibiliTool.js' +import { SearchVideoTool } from '../utils/tools/SearchBilibiliTool.js' +import { SendAvatarTool } from '../utils/tools/SendAvatarTool.js' +import { SerpImageTool } from '../utils/tools/SearchImageTool.js' +import { SearchMusicTool } from '../utils/tools/SearchMusicTool.js' +import { SendMusicTool } from '../utils/tools/SendMusicTool.js' +import { SendAudioMessageTool } from '../utils/tools/SendAudioMessageTool.js' +import { SendMessageToSpecificGroupOrUserTool } from '../utils/tools/SendMessageToSpecificGroupOrUserTool.js' +import { QueryGenshinTool } from '../utils/tools/QueryGenshinTool.js' +import { WeatherTool } from '../utils/tools/WeatherTool.js' +import { QueryUserinfoTool } from '../utils/tools/QueryUserinfoTool.js' +import { EditCardTool } from '../utils/tools/EditCardTool.js' +import { JinyanTool } from '../utils/tools/JinyanTool.js' +import { KickOutTool } from '../utils/tools/KickOutTool.js' +import { SetTitleTool } from '../utils/tools/SetTitleTool.js' +import { SerpIkechan8370Tool } from '../utils/tools/SerpIkechan8370Tool.js' +import { SerpTool } from '../utils/tools/SerpTool.js' +import common from '../../../lib/common/common.js' +import { SendDiceTool } from '../utils/tools/SendDiceTool.js' +import { EliMovieTool } from '../utils/tools/EliMovieTool.js' +import { EliMusicTool } from '../utils/tools/EliMusicTool.js' +import { HandleMessageMsgTool } from '../utils/tools/HandleMessageMsgTool.js' +import { ProcessPictureTool } from '../utils/tools/ProcessPictureTool.js' +import { ImageCaptionTool } from '../utils/tools/ImageCaptionTool.js' +import { ChatGPTAPI } from '../utils/openai/chatgpt-api.js' +import { newFetch } from '../utils/proxy.js' +import { ChatGLM4Client } from '../client/ChatGLM4Client.js' +import { QwenApi } from '../utils/alibaba/qwen-api.js' + +const roleMap = { + owner: 'group owner', + admin: 'group administrator' +} + +const defaultPropmtPrefix = ', a large language model trained by OpenAI. You answer as concisely as possible for each response (e.g. don’t be verbose). It is very important that you answer as concisely as possible, so please remember this. If you are generating a list, do not have too many items. Keep the number of items short.' + +async function handleSystem (e, system) { + if (Config.enableGroupContext) { + try { + let opt = {} + opt.groupId = e.group_id + opt.qq = e.sender.user_id + opt.nickname = e.sender.card + opt.groupName = e.group.name || e.group_name + opt.botName = e.isGroup ? (e.group.pickMember(getUin(e)).card || e.group.pickMember(getUin(e)).nickname) : e.bot.nickname + let master = (await getMasterQQ())[0] + if (master && e.group) { + opt.masterName = e.group.pickMember(parseInt(master)).card || e.group.pickMember(parseInt(master)).nickname + } + if (master && !e.group) { + opt.masterName = e.bot.getFriendList().get(parseInt(master))?.nickname + } + let chats = await getChatHistoryGroup(e, Config.groupContextLength) + opt.chats = chats + const namePlaceholder = '[name]' + const defaultBotName = 'ChatGPT' + const groupContextTip = Config.groupContextTip + system = system.replaceAll(namePlaceholder, opt.botName || defaultBotName) + + ((opt.groupId) ? groupContextTip : '') + system += 'Attention, you are currently chatting in a qq group, then one who asks you now is' + `${opt.nickname}(${opt.qq})。` + system += `the group name is ${opt.groupName}, group id is ${opt.groupId}。` + if (opt.botName) { + system += `Your nickname is ${opt.botName} in the group,` + } + if (chats) { + system += 'There is the conversation history in the group, you must chat according to the conversation history context"' + system += chats + .map(chat => { + let sender = chat.sender || {} + // if (sender.user_id === e.bot.uin && chat.raw_message.startsWith('建议的回复')) { + if (chat.raw_message.startsWith('建议的回复')) { + // 建议的回复太容易污染设定导致对话太固定跑偏了 + return '' + } + return `【${sender.card || sender.nickname}】(qq:${sender.user_id}, ${roleMap[sender.role] || 'normal user'},${sender.area ? 'from ' + sender.area + ', ' : ''} ${sender.age} years old, 群头衔:${sender.title}, gender: ${sender.sex}, time:${formatDate(new Date(chat.time * 1000))}, messageId: ${chat.message_id}) 说:${chat.raw_message}` + }) + .join('\n') + } + } catch (err) { + if (e.isGroup) { + logger.warn('获取群聊聊天记录失败,本次对话不携带聊天记录', err) + } + } + } + return system +} + +class Core { + async sendMessage (prompt, conversation = {}, use, e) { + if (!conversation) { + conversation = { + timeoutMs: Config.defaultTimeoutMs + } + } + if (Config.debug) { + logger.mark(`using ${use} mode`) + } + const userData = await getUserData(e.user_id) + const useCast = userData.cast || {} + if (use === 'bing') { + let throttledTokens = [] + let { + bingToken, + allThrottled + } = await getAvailableBingToken(conversation, throttledTokens) + let cookies + if (bingToken?.indexOf('=') > -1) { + cookies = bingToken + } + let bingAIClient + const cacheOptions = { + namespace: Config.toneStyle, + store: new KeyvFile({ filename: 'cache.json' }) + } + bingAIClient = new SydneyAIClient({ + userToken: bingToken, // "_U" cookie from bing.com + cookies, + debug: Config.debug, + cache: cacheOptions, + user: e.sender.user_id, + proxy: Config.proxy + }) + // Sydney不实现上下文传递,删除上下文索引 + delete conversation.clientId + delete conversation.invocationId + delete conversation.conversationSignature + let response + let reply = '' + let retry = 3 + let errorMessage = '' + + do { + try { + let opt = _.cloneDeep(conversation) || {} + opt.toneStyle = Config.toneStyle + // 如果当前没有开启对话或者当前是Sydney模式、Custom模式,则本次对话携带拓展资料 + let c = await redis.get(`CHATGPT:CONVERSATIONS_BING:${e.sender.user_id}`) + if (!c) { + opt.context = useCast?.bing_resource || Config.sydneyContext + } + // 重新拿存储的token,因为可能之前有过期的被删了 + let abtrs = await getAvailableBingToken(conversation, throttledTokens) + bingToken = abtrs.bingToken + // eslint-disable-next-line no-unused-vars + allThrottled = abtrs.allThrottled + if (bingToken?.indexOf('=') > -1) { + cookies = bingToken + } + if (!bingAIClient.opts) { + bingAIClient.opts = {} + } + bingAIClient.opts.userToken = bingToken + bingAIClient.opts.cookies = cookies + // opt.messageType = allThrottled ? 'Chat' : 'SearchQuery' + if (Config.enableGroupContext && e.isGroup) { + try { + opt.groupId = e.group_id + opt.qq = e.sender.user_id + opt.nickname = e.sender.card + opt.groupName = e.group.name || e.group_name + opt.botName = e.isGroup ? (e.group.pickMember(getUin(e)).card || e.group.pickMember(getUin(e)).nickname) : e.bot.nickname + let master = (await getMasterQQ())[0] + if (master && e.group) { + opt.masterName = e.group.pickMember(parseInt(master)).card || e.group.pickMember(parseInt(master)).nickname + } + if (master && !e.group) { + opt.masterName = e.bot.getFriendList().get(parseInt(master))?.nickname + } + opt.chats = await getChatHistoryGroup(e, Config.groupContextLength) + } catch (err) { + logger.warn('获取群聊聊天记录失败,本次对话不携带聊天记录', err) + } + } + let toSummaryFileContent + try { + if (e.source) { + let seq = e.isGroup ? e.source.seq : e.source.time + if (e.adapter === 'shamrock') { + seq = e.source.message_id + } + let msgs = e.isGroup ? await e.group.getChatHistory(seq, 1) : await e.friend.getChatHistory(seq, 1) + let sourceMsg = msgs[msgs.length - 1] + let fileMsgElem = sourceMsg.file || sourceMsg.message.find(msg => msg.type === 'file') + if (fileMsgElem) { + toSummaryFileContent = await extractContentFromFile(fileMsgElem, e) + } + } + } catch (err) { + logger.warn('读取文件内容出错, 忽略文件内容', err) + } + opt.toSummaryFileContent = toSummaryFileContent + // 写入图片数据 + if (Config.sydneyImageRecognition) { + const image = await getImg(e) + opt.imageUrl = image ? image[0] : undefined + } + if (Config.enableGenerateContents) { + opt.onImageCreateRequest = prompt => { + logger.mark(`开始生成内容:${prompt}`) + if (Config.bingAPDraw) { + // 调用第三方API进行绘图 + let apDraw = new APTool() + apDraw.func({ + prompt + }, e) + } else { + let client = new BingDrawClient({ + baseUrl: Config.sydneyReverseProxy, + userToken: bingToken + }) + redis.set(`CHATGPT:DRAW:${e.sender.user_id}`, 'c', { EX: 30 }).then(() => { + try { + client.getImages(prompt, e) + } catch (err) { + redis.del(`CHATGPT:DRAW:${e.sender.user_id}`) + this.reply('绘图失败:' + err) + } + }) + } + } + } + response = await bingAIClient.sendMessage(prompt, opt, (token) => { + reply += token + }) + if (response.details.adaptiveCards?.[0]?.body?.[0]?.text?.trim()) { + if (response.response === undefined) { + response.response = response.details.adaptiveCards?.[0]?.body?.[0]?.text?.trim() + } + response.response = response.response.replace(/\[\^[0-9]+\^\]/g, (str) => { + return str.replace(/[/^]/g, '') + }) + // 有了新的引用属性 + // response.quote = response.details.adaptiveCards?.[0]?.body?.[0]?.text?.replace(/\[\^[0-9]+\^\]/g, '').replace(response.response, '').split('\n') + } + response.suggestedResponses = response.details.suggestedResponses?.map(s => s.text).join('\n') + // 新引用属性读取数据 + if (response.details.sourceAttributions) { + response.quote = [] + for (let quote of response.details.sourceAttributions) { + response.quote.push({ + text: quote.providerDisplayName || '', + url: quote.seeMoreUrl, + imageLink: quote.imageLink || '' + }) + } + } + // 如果token曾经有异常,则清除异常 + let Tokens = JSON.parse((await redis.get('CHATGPT:BING_TOKENS')) || '[]') + const TokenIndex = Tokens?.findIndex(element => element.Token === abtrs.bingToken) + if (TokenIndex > 0 && Tokens[TokenIndex].exception) { + delete Tokens[TokenIndex].exception + await redis.set('CHATGPT:BING_TOKENS', JSON.stringify(Tokens)) + } + errorMessage = '' + break + } catch (error) { + logger.error(error) + const message = error?.message || error?.data?.message || error || '出错了' + const { maxConv } = error + if (message && typeof message === 'string' && message.indexOf('CaptchaChallenge') > -1) { + if (bingToken) { + if (maxConv >= 20 && Config.bingCaptchaOneShotUrl) { + // maxConv为30说明token有效,可以通过解验证码码服务过码 + await this.reply('出现必应验证码,尝试解决中') + try { + let captchaResolveResult = await solveCaptchaOneShot(bingToken) + if (captchaResolveResult?.success) { + await this.reply('验证码已解决') + } else { + logger.error(captchaResolveResult) + errorMessage = message + await this.reply('验证码解决失败: ' + captchaResolveResult.error) + retry = 0 + } + } catch (err) { + logger.error(err) + await this.reply('验证码解决失败: ' + err) + retry = 0 + } + } else { + // 未登录用户maxConv目前为5或10,出验证码是ip或MUID问题 + logger.warn(`token [${bingToken}] 出现必应验证码,请前往网页版或app手动解决`) + errorMessage = message + retry = 0 + } + } else { + retry = 0 + } + } else if (message && typeof message === 'string' && message.indexOf('限流') > -1) { + throttledTokens.push(bingToken) + let bingTokens = JSON.parse(await redis.get('CHATGPT:BING_TOKENS')) + const badBingToken = bingTokens.findIndex(element => element.Token === bingToken) + const now = new Date() + const hours = now.getHours() + now.setHours(hours + 6) + bingTokens[badBingToken].State = '受限' + bingTokens[badBingToken].DisactivationTime = now + await redis.set('CHATGPT:BING_TOKENS', JSON.stringify(bingTokens)) + // 不减次数 + } else if (message && typeof message === 'string' && message.indexOf('UnauthorizedRequest') > -1) { + // token过期了 + let bingTokens = JSON.parse(await redis.get('CHATGPT:BING_TOKENS')) + const badBingToken = bingTokens.findIndex(element => element.Token === bingToken) + if (badBingToken > 0) { + // 可能是微软抽风,给三次机会 + if (bingTokens[badBingToken]?.exception) { + if (bingTokens[badBingToken].exception <= 3) { + bingTokens[badBingToken].exception += 1 + } else { + bingTokens[badBingToken].exception = 0 + bingTokens[badBingToken].State = '过期' + } + } else { + bingTokens[badBingToken].exception = 1 + } + await redis.set('CHATGPT:BING_TOKENS', JSON.stringify(bingTokens)) + } else { + retry = retry - 1 + } + errorMessage = 'UnauthorizedRequest:必应token不正确或已过期' + // logger.warn(`token${bingToken}疑似不存在或已过期,再试试`) + // retry = retry - 1 + } else { + retry-- + errorMessage = message === 'Timed out waiting for response. Try enabling debug mode to see more information.' ? (reply ? `${reply}\n不行了,我的大脑过载了,处理不过来了!` : '必应的小脑瓜不好使了,不知道怎么回答!') : message + } + } + } while (retry > 0) + if (errorMessage) { + if (errorMessage.includes('CaptchaChallenge')) { + if (bingToken) { + errorMessage = '出现验证码,请使用当前账户前往https://www.bing.com/chat或Edge侧边栏或移动端APP手动解除验证码' + } else { + errorMessage = '未配置必应账户,建议绑定必应账户再使用必应模式' + } + } + return { + text: errorMessage, + error: true + } + } else if (response?.response) { + return { + text: response?.response, + quote: response?.quote, + suggestedResponses: response.suggestedResponses, + conversationId: response.conversationId, + clientId: response.clientId, + invocationId: response.invocationId, + conversationSignature: response.conversationSignature, + parentMessageId: response.apology ? conversation.parentMessageId : response.messageId, + bingToken + } + } else { + logger.debug('no message') + return { + noMsg: true + } + } + } else if (use === 'api3') { + // official without cloudflare + let accessToken = await redis.get('CHATGPT:TOKEN') + if (!accessToken) { + throw new Error('未绑定ChatGPT AccessToken,请使用#chatgpt设置token命令绑定token') + } + this.chatGPTApi = new OfficialChatGPTClient({ + accessToken, + apiReverseUrl: Config.api, + timeoutMs: 120000 + }) + let sendMessageResult = await this.chatGPTApi.sendMessage(prompt, conversation) + // 更新最后一条prompt + await redis.set(`CHATGPT:CONVERSATION_LAST_MESSAGE_PROMPT:${sendMessageResult.conversationId}`, prompt) + // 更新最后一条messageId + await redis.set(`CHATGPT:CONVERSATION_LAST_MESSAGE_ID:${sendMessageResult.conversationId}`, sendMessageResult.id) + await redis.set(`CHATGPT:QQ_CONVERSATION:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`, sendMessageResult.conversationId) + if (!conversation.conversationId) { + // 如果是对话的创建者 + await redis.set(`CHATGPT:CONVERSATION_CREATER_ID:${sendMessageResult.conversationId}`, e.sender.user_id) + await redis.set(`CHATGPT:CONVERSATION_CREATER_NICK_NAME:${sendMessageResult.conversationId}`, e.sender.card) + } + return sendMessageResult + } else if (use === 'chatglm') { + const cacheOptions = { + namespace: 'chatglm_6b', + store: new KeyvFile({ filename: 'cache.json' }) + } + this.chatGPTApi = new ChatGLMClient({ + user: e.sender.user_id, + cache: cacheOptions + }) + return await this.chatGPTApi.sendMessage(prompt, conversation) + } else if (use === 'claude') { + // slack已经不可用,移除 + let keys = Config.claudeApiKey?.split(/[,;]/).map(key => key.trim()).filter(key => key) + let choiceIndex = Math.floor(Math.random() * keys.length) + let key = keys[choiceIndex] + logger.info(`使用API Key:${key}`) + while (keys.length >= 0) { + let errorMessage = '' + const client = new ClaudeAPIClient({ + key, + model: Config.claudeApiModel || 'claude-3-sonnet-20240229', + debug: true, + baseUrl: Config.claudeApiBaseUrl + // temperature: Config.claudeApiTemperature || 0.5 + }) + try { + let rsp = await client.sendMessage(prompt, { + stream: false, + parentMessageId: conversation.parentMessageId, + conversationId: conversation.conversationId, + system: Config.claudeSystemPrompt + }) + return rsp + } catch (err) { + errorMessage = err.message + switch (err.message) { + case 'rate_limit_error': { + // api没钱了或者当月/日/时/分额度耗尽 + // throw new Error('claude API额度耗尽或触发速率限制') + break + } + case 'authentication_error': { + // 无效的key + // throw new Error('claude API key无效') + break + } + default: + } + logger.warn(`claude api 错误:[${key}] ${errorMessage}`) + } + if (keys.length === 0) { + throw new Error(errorMessage) + } + keys.splice(choiceIndex, 1) + choiceIndex = Math.floor(Math.random() * keys.length) + key = keys[choiceIndex] + logger.info(`使用API Key:${key}`) + } + } else if (use === 'claude2') { + let { conversationId } = conversation + let client = new ClaudeAIClient({ + organizationId: Config.claudeAIOrganizationId, + sessionKey: Config.claudeAISessionKey, + debug: Config.debug, + proxy: Config.proxy + }) + let toSummaryFileContent + try { + if (e.source) { + let msgs = e.isGroup ? await e.group.getChatHistory(e.source.seq, 1) : await e.friend.getChatHistory(e.source.time, 1) + let sourceMsg = msgs[0] + let fileMsgElem = sourceMsg.message.find(msg => msg.type === 'file') + if (fileMsgElem) { + toSummaryFileContent = await extractContentFromFile(fileMsgElem, e) + } + } + } catch (err) { + logger.warn('读取文件内容出错, 忽略文件内容', err) + } + + let attachments = [] + if (toSummaryFileContent?.content) { + attachments.push({ + extracted_content: toSummaryFileContent.content, + file_name: toSummaryFileContent.name, + file_type: 'pdf', + file_size: 200312, + totalPages: 20 + }) + logger.info(toSummaryFileContent.content) + } + if (conversationId) { + return await client.sendMessage(prompt, conversationId, attachments) + } else { + let conv = await client.createConversation() + return await client.sendMessage(prompt, conv.uuid, attachments) + } + } else if (use === 'xh') { + const cacheOptions = { + namespace: 'xh', + store: new KeyvFile({ filename: 'cache.json' }) + } + const ssoSessionId = Config.xinghuoToken + if (!ssoSessionId) { + // throw new Error('未绑定星火token,请使用#chatgpt设置星火token命令绑定token。(获取对话页面的ssoSessionId cookie值)') + logger.warn('未绑定星火token,请使用#chatgpt设置星火token命令绑定token。(获取对话页面的ssoSessionId cookie值)') + } + let client = new XinghuoClient({ + ssoSessionId, + cache: cacheOptions + }) + // 获取图片资源 + const image = await getImg(e) + let response = await client.sendMessage(prompt, { + e, + chatId: conversation?.conversationId, + image: image ? image[0] : undefined, + system: Config.xhPrompt + }) + return response + } else if (use === 'azure') { + let azureModel + try { + azureModel = await import('@azure/openai') + } catch (error) { + throw new Error('未安装@azure/openai包,请执行pnpm install @azure/openai安装') + } + let OpenAIClient = azureModel.OpenAIClient + let AzureKeyCredential = azureModel.AzureKeyCredential + let msg = conversation.messages + let content = { + role: 'user', + content: prompt + } + msg.push(content) + const client = new OpenAIClient(Config.azureUrl, new AzureKeyCredential(Config.azApiKey)) + const deploymentName = Config.azureDeploymentName + const { choices } = await client.getChatCompletions(deploymentName, msg) + let completion = choices[0].message + return { + text: completion.content, + message: completion + } + } else if (use === 'qwen') { + let completionParams = { + parameters: { + top_p: Config.qwenTopP || 0.5, + top_k: Config.qwenTopK || 50, + seed: Config.qwenSeed > 0 ? Config.qwenSeed : Math.floor(Math.random() * 114514), + temperature: Config.qwenTemperature || 1, + enable_search: !!Config.qwenEnableSearch, + result_format: 'message' + } + } + if (Config.qwenModel) { + completionParams.model = Config.qwenModel + } + const currentDate = new Date().toISOString().split('T')[0] + + async function um (message) { + return await upsertMessage(message, 'QWEN') + } + + async function gm (id) { + return await getMessageById(id, 'QWEN') + } + + let opts = { + apiKey: Config.qwenApiKey, + debug: Config.debug, + upsertMessage: um, + getMessageById: gm, + systemMessage: `You are ${Config.assistantLabel} ${useCast?.api || Config.promptPrefixOverride || defaultPropmtPrefix} + Current date: ${currentDate}`, + completionParams, + assistantLabel: Config.assistantLabel, + fetch: newFetch + } + + let option = { + timeoutMs: 600000, + completionParams + } + if (conversation) { + if (!conversation.conversationId) { + conversation.conversationId = uuid() + } + option = Object.assign(option, conversation) + } + if (Config.smartMode) { + let isAdmin = ['admin', 'owner'].includes(e.sender.role) + let sender = e.sender.user_id + const { + funcMap, + fullFuncMap, + promptAddition, + systemAddition + } = await collectTools(e) + if (!option.completionParams) { + option.completionParams = {} + } + promptAddition && (prompt += promptAddition) + option.systemMessage = await handleSystem(e, opts.systemMessage) + systemAddition && (option.systemMessage += systemAddition) + opts.completionParams.parameters.tools = Object.keys(funcMap) + .map(k => funcMap[k].function) + .map(obj => { + return { + type: 'function', + function: obj + } + }) + let msg + try { + this.qwenApi = new QwenApi(opts) + msg = await this.qwenApi.sendMessage(prompt, option) + logger.info(msg) + while (msg.functionCall) { + if (msg.text) { + await this.reply(msg.text.replace('\n\n\n', '\n')) + } + let { + name, + arguments: args + } = msg.functionCall + args = JSON.parse(args) + // 感觉换成targetGroupIdOrUserQQNumber这种表意比较清楚的变量名,效果会好一丢丢 + if (!args.groupId) { + args.groupId = e.group_id + '' || e.sender.user_id + '' + } + try { + parseInt(args.groupId) + } catch (err) { + args.groupId = e.group_id + '' || e.sender.user_id + '' + } + let functionResult = await fullFuncMap[name.trim()].exec.bind(this)(Object.assign({ + isAdmin, + sender + }, args), e) + logger.mark(`function ${name} execution result: ${functionResult}`) + option.parentMessageId = msg.id + option.name = name + // 不然普通用户可能会被openai限速 + await common.sleep(300) + msg = await this.qwenApi.sendMessage(functionResult, option, 'tool') + logger.info(msg) + } + } catch (err) { + logger.error(err) + throw new Error(err) + } + return msg + } else { + let msg + try { + this.qwenApi = new QwenApi(opts) + msg = await this.qwenApi.sendMessage(prompt, option) + } catch (err) { + logger.error(err) + throw new Error(err) + } + return msg + } + } else if (use === 'bard') { + // 处理cookie + const matchesPSID = /__Secure-1PSID=([^;]+)/.exec(Config.bardPsid) + const matchesPSIDTS = /__Secure-1PSIDTS=([^;]+)/.exec(Config.bardPsid) + const cookie = { + '__Secure-1PSID': matchesPSID[1], + '__Secure-1PSIDTS': matchesPSIDTS[1] + } + if (!matchesPSID[1] || !matchesPSIDTS[1]) { + throw new Error('未绑定bard') + } + // 处理图片 + const image = await getImg(e) + let imageBuff + if (image) { + try { + let imgResponse = await fetch(image[0]) + if (imgResponse.ok) { + imageBuff = await imgResponse.arrayBuffer() + } + } catch (error) { + logger.warn(`错误的图片链接${image[0]}`) + } + } + // 发送数据 + let bot = new Bard(cookie, { + fetch, + bardURL: Config.bardForceUseReverse ? Config.bardReverseProxy : 'https://bard.google.com' + }) + let chat = await bot.createChat(conversation?.conversationId + ? { + conversationID: conversation.conversationId, + responseID: conversation.parentMessageId, + choiceID: conversation.clientId, + _reqID: conversation.invocationId + } + : {}) + let response = await chat.ask(prompt, { + image: imageBuff, + format: Bard.JSON + }) + return { + conversationId: response.ids.conversationID, + responseID: response.ids.responseID, + choiceID: response.ids.choiceID, + _reqID: response.ids._reqID, + text: response.content, + images: response.images + } + } else if (use === 'gemini') { + let client = new CustomGoogleGeminiClient({ + e, + userId: e.sender.user_id, + key: Config.geminiKey, + model: Config.geminiModel, + baseUrl: Config.geminiBaseUrl, + debug: Config.debug + }) + let option = { + stream: false, + onProgress: (data) => { + if (Config.debug) { + logger.info(data) + } + }, + parentMessageId: conversation.parentMessageId, + conversationId: conversation.conversationId + } + if (Config.geminiModel.includes('vision')) { + const image = await getImg(e) + let imageUrl = image ? image[0] : undefined + if (imageUrl) { + let md5 = imageUrl.split(/[/-]/).find(s => s.length === 32)?.toUpperCase() + let imageLoc = await getOrDownloadFile(`ocr/${md5}.png`, imageUrl) + let outputLoc = imageLoc.replace(`${md5}.png`, `${md5}_512.png`) + await resizeAndCropImage(imageLoc, outputLoc, 512) + let buffer = fs.readFileSync(outputLoc) + option.image = buffer.toString('base64') + } + } + if (Config.smartMode) { + /** + * @type {AbstractTool[]} + */ + let tools = [ + new QueryStarRailTool(), + new WebsiteTool(), + new SendPictureTool(), + new SendVideoTool(), + new SearchVideoTool(), + new SendAvatarTool(), + new SerpImageTool(), + new SearchMusicTool(), + new SendMusicTool(), + new SendAudioMessageTool(), + new APTool(), + new SendMessageToSpecificGroupOrUserTool(), + new QueryGenshinTool() + ] + if (Config.amapKey) { + tools.push(new WeatherTool()) + } + if (e.isGroup) { + tools.push(new QueryUserinfoTool()) + if (e.group.is_admin || e.group.is_owner) { + tools.push(new EditCardTool()) + tools.push(new JinyanTool()) + tools.push(new KickOutTool()) + } + if (e.group.is_owner) { + tools.push(new SetTitleTool()) + } + } + switch (Config.serpSource) { + case 'ikechan8370': { + tools.push(new SerpIkechan8370Tool()) + break + } + case 'azure': { + if (!Config.azSerpKey) { + logger.warn('未配置bing搜索密钥,转为使用ikechan8370搜索源') + tools.push(new SerpIkechan8370Tool()) + } else { + tools.push(new SerpTool()) + } + break + } + default: { + tools.push(new SerpIkechan8370Tool()) + } + } + client.addTools(tools) + } + let system = Config.geminiPrompt + if (Config.enableGroupContext && e.isGroup) { + let chats = await getChatHistoryGroup(e, Config.groupContextLength) + const namePlaceholder = '[name]' + const defaultBotName = 'GeminiPro' + const groupContextTip = Config.groupContextTip + let botName = e.isGroup ? (e.group.pickMember(getUin(e)).card || e.group.pickMember(getUin(e)).nickname) : e.bot.nickname + system = system.replaceAll(namePlaceholder, botName || defaultBotName) + + ((Config.enableGroupContext && e.group_id) ? groupContextTip : '') + system += 'Attention, you are currently chatting in a qq group, then one who asks you now is' + `${e.sender.card || e.sender.nickname}(${e.sender.user_id}).` + system += `the group name is ${e.group.name || e.group_name}, group id is ${e.group_id}.` + system += `Your nickname is ${botName} in the group,` + if (chats) { + system += 'There is the conversation history in the group, you must chat according to the conversation history context"' + system += chats + .map(chat => { + let sender = chat.sender || {} + return `【${sender.card || sender.nickname}】(qq:${sender.user_id}, ${roleMap[sender.role] || 'normal user'},${sender.area ? 'from ' + sender.area + ', ' : ''} ${sender.age} years old, 群头衔:${sender.title}, gender: ${sender.sex}, time:${formatDate(new Date(chat.time * 1000))}, messageId: ${chat.message_id}) 说:${chat.raw_message}` + }) + .join('\n') + } + } + option.system = system + return await client.sendMessage(prompt, option) + } else if (use === 'chatglm4') { + const client = new ChatGLM4Client({ + refreshToken: Config.chatglmRefreshToken + }) + let resp = await client.sendMessage(prompt, conversation) + if (resp.image) { + this.reply(segment.image(resp.image), true) + } + return resp + } else { + // openai api + let completionParams = {} + if (Config.model) { + completionParams.model = Config.model + } + const currentDate = new Date().toISOString().split('T')[0] + let promptPrefix = `You are ${Config.assistantLabel} ${useCast?.api || Config.promptPrefixOverride || defaultPropmtPrefix} + Current date: ${currentDate}` + let maxModelTokens = getMaxModelTokens(completionParams.model) + // let system = promptPrefix + let system = await handleSystem(e, promptPrefix, maxModelTokens) + logger.debug(system) + let opts = { + apiBaseUrl: Config.openAiBaseUrl, + apiKey: Config.apiKey, + debug: false, + upsertMessage, + getMessageById, + systemMessage: system, + completionParams, + assistantLabel: Config.assistantLabel, + fetch: newFetch, + maxModelTokens + } + let openAIAccessible = (Config.proxy || !(await isCN())) // 配了代理或者服务器在国外,默认认为不需要反代 + if (opts.apiBaseUrl !== defaultOpenAIAPI && openAIAccessible && !Config.openAiForceUseReverse) { + // 如果配了proxy(或者不在国内),而且有反代,但是没开启强制反代,将baseurl删掉 + delete opts.apiBaseUrl + } + this.chatGPTApi = new ChatGPTAPI(opts) + let option = { + timeoutMs: 600000, + completionParams, + stream: Config.apiStream, + onProgress: (data) => { + if (Config.debug) { + logger.info(data?.text || data.functionCall || data) + } + } + // systemMessage: promptPrefix + } + option.systemMessage = system + if (conversation) { + if (!conversation.conversationId) { + conversation.conversationId = uuid() + } + option = Object.assign(option, conversation) + } + if (Config.smartMode) { + let isAdmin = ['admin', 'owner'].includes(e.sender.role) + let sender = e.sender.user_id + const { + funcMap, + fullFuncMap, + promptAddition, + systemAddition + } = await collectTools(e) + if (!option.completionParams) { + option.completionParams = {} + } + promptAddition && (prompt += promptAddition) + systemAddition && (option.systemMessage += systemAddition) + option.completionParams.functions = Object.keys(funcMap).map(k => funcMap[k].function) + let msg + try { + msg = await this.chatGPTApi.sendMessage(prompt, option) + logger.info(msg) + while (msg.functionCall) { + if (msg.text) { + await this.reply(msg.text.replace('\n\n\n', '\n')) + } + let { + name, + arguments: args + } = msg.functionCall + args = JSON.parse(args) + // 感觉换成targetGroupIdOrUserQQNumber这种表意比较清楚的变量名,效果会好一丢丢 + if (!args.groupId) { + args.groupId = e.group_id + '' || e.sender.user_id + '' + } + try { + parseInt(args.groupId) + } catch (err) { + args.groupId = e.group_id + '' || e.sender.user_id + '' + } + let functionResult = await fullFuncMap[name.trim()].exec.bind(this)(Object.assign({ + isAdmin, + sender + }, args), e) + logger.mark(`function ${name} execution result: ${functionResult}`) + option.parentMessageId = msg.id + option.name = name + // 不然普通用户可能会被openai限速 + await common.sleep(300) + msg = await this.chatGPTApi.sendMessage(functionResult, option, 'function') + logger.info(msg) + } + } catch (err) { + if (err.message?.indexOf('context_length_exceeded') > 0) { + logger.warn(err) + await redis.del(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`) + await redis.del(`CHATGPT:WRONG_EMOTION:${e.sender.user_id}`) + await this.reply('字数超限啦,将为您自动结束本次对话。') + return null + } else { + logger.error(err) + throw new Error(err) + } + } + return msg + } else { + let msg + try { + msg = await this.chatGPTApi.sendMessage(prompt, option) + } catch (err) { + if (err.message?.indexOf('context_length_exceeded') > 0) { + logger.warn(err) + await redis.del(`CHATGPT:CONVERSATIONS:${e.sender.user_id}`) + await redis.del(`CHATGPT:WRONG_EMOTION:${e.sender.user_id}`) + await this.reply('字数超限啦,将为您自动结束本次对话。') + return null + } else { + logger.error(err) + throw new Error(err) + } + } + return msg + } + } + } +} + +/** + * 收集tools + * @param e + * @return {Promise<{systemAddition, funcMap: {}, promptAddition: string, fullFuncMap: {}}>} + */ +async function collectTools (e) { + let serpTool + switch (Config.serpSource) { + case 'ikechan8370': { + serpTool = new SerpIkechan8370Tool() + break + } + case 'azure': { + if (!Config.azSerpKey) { + logger.warn('未配置bing搜索密钥,转为使用ikechan8370搜索源') + serpTool = new SerpIkechan8370Tool() + } else { + serpTool = new SerpTool() + } + break + } + default: { + serpTool = new SerpIkechan8370Tool() + } + } + let fullTools = [ + new EditCardTool(), + new QueryStarRailTool(), + new WebsiteTool(), + new JinyanTool(), + new KickOutTool(), + new WeatherTool(), + new SendPictureTool(), + new SendVideoTool(), + new ImageCaptionTool(), + new SearchVideoTool(), + new SendAvatarTool(), + new SerpImageTool(), + new SearchMusicTool(), + new SendMusicTool(), + new SerpIkechan8370Tool(), + new SerpTool(), + new SendAudioMessageTool(), + new ProcessPictureTool(), + new APTool(), + new HandleMessageMsgTool(), + new QueryUserinfoTool(), + new EliMusicTool(), + new EliMovieTool(), + new SendMessageToSpecificGroupOrUserTool(), + new SendDiceTool(), + new QueryGenshinTool(), + new SetTitleTool() + ] + // todo 3.0再重构tool的插拔和管理 + let tools = [ + new SendAvatarTool(), + new SendDiceTool(), + new SendMessageToSpecificGroupOrUserTool(), + // new EditCardTool(), + new QueryStarRailTool(), + new QueryGenshinTool(), + new ProcessPictureTool(), + new WebsiteTool(), + // new JinyanTool(), + // new KickOutTool(), + new WeatherTool(), + new SendPictureTool(), + new SendAudioMessageTool(), + new APTool(), + // new HandleMessageMsgTool(), + serpTool, + new QueryUserinfoTool() + ] + try { + await import('../../avocado-plugin/apps/avocado.js') + tools.push(...[new EliMusicTool(), new EliMovieTool()]) + } catch (err) { + tools.push(...[new SendMusicTool(), new SearchMusicTool()]) + logger.debug(logger.green('【ChatGPT-Plugin】插件avocado-plugin未安装') + ',安装后可查看最近热映电影与体验可玩性更高的点歌工具。\n可前往 https://github.com/Qz-Sean/avocado-plugin 获取') + } + let systemAddition = '' + if (e.isGroup) { + let botInfo = await e.bot.getGroupMemberInfo(e.group_id, getUin(e), true) + if (botInfo.role !== 'member') { + // 管理员才给这些工具 + tools.push(...[new EditCardTool(), new JinyanTool(), new KickOutTool(), new HandleMessageMsgTool(), new SetTitleTool()]) + // 用于撤回和加精的id + if (e.source?.seq) { + let source = (await e.group.getChatHistory(e.source?.seq, 1)).pop() + systemAddition += `\nthe last message is replying to ${source.message_id}"\n` + } else { + systemAddition += `\nthe last message id is ${e.message_id}. ` + } + } + } + let promptAddition = '' + let img = await getImg(e) + if (img?.length > 0 && Config.extraUrl) { + tools.push(new ImageCaptionTool()) + tools.push(new ProcessPictureTool()) + promptAddition += `\nthe url of the picture(s) above: ${img.join(', ')}` + } else { + tools.push(new SerpImageTool()) + tools.push(...[new SearchVideoTool(), + new SendVideoTool()]) + } + let funcMap = {} + let fullFuncMap = {} + tools.forEach(tool => { + funcMap[tool.name] = { + exec: tool.func, + function: tool.function() + } + }) + fullTools.forEach(tool => { + fullFuncMap[tool.name] = { + exec: tool.func, + function: tool.function() + } + }) + return { + funcMap, + fullFuncMap, + systemAddition, + promptAddition + } +} + +async function getAvailableBingToken (conversation, throttled = []) { + let allThrottled = false + if (!await redis.get('CHATGPT:BING_TOKENS')) { + return { + bingToken: null, + allThrottled + } + // throw new Error('未绑定Bing Cookie,请使用#chatgpt设置必应token命令绑定Bing Cookie') + } + + let bingToken = '' + let bingTokens = JSON.parse(await redis.get('CHATGPT:BING_TOKENS')) + const normal = bingTokens.filter(element => element.State === '正常') + const restricted = bingTokens.filter(element => element.State === '受限') + + // 判断受限的token是否已经可以解除 + for (const restrictedToken of restricted) { + const now = new Date() + const tk = new Date(restrictedToken.DisactivationTime) + if (tk <= now) { + const index = bingTokens.findIndex(element => element.Token === restrictedToken.Token) + bingTokens[index].Usage = 0 + bingTokens[index].State = '正常' + } + } + if (normal.length > 0) { + const minElement = normal.reduce((min, current) => { + return current.Usage < min.Usage ? current : min + }) + bingToken = minElement.Token + } else if (restricted.length > 0 && restricted.some(x => throttled.includes(x.Token))) { + allThrottled = true + const minElement = restricted.reduce((min, current) => { + return current.Usage < min.Usage ? current : min + }) + bingToken = minElement.Token + } else { + // throw new Error('全部Token均已失效,暂时无法使用') + return { + bingToken: null, + allThrottled + } + } + // 记录使用情况 + const index = bingTokens.findIndex(element => element.Token === bingToken) + bingTokens[index].Usage += 1 + await redis.set('CHATGPT:BING_TOKENS', JSON.stringify(bingTokens)) + return { + bingToken, + allThrottled + } +} + +export default new Core() diff --git a/package.json b/package.json index 44f2ee1..c6bc357 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "chatgpt-plugin", + "version": "2.8.1", "type": "module", "author": "ikechan8370", "dependencies": { @@ -9,7 +10,6 @@ "@fastify/static": "^6.9.0", "@fastify/websocket": "^8.2.0", "@google/generative-ai": "^0.1.1", - "@slack/bolt": "^3.13.2", "asn1.js": "^5.0.0", "diff": "^5.1.0", "emoji-strip": "^1.0.1", @@ -40,9 +40,6 @@ "node-silk": "^0.1.0", "nodejs-pptx": "^1.2.4", "pdfjs-dist": "^3.11.174", - "puppeteer-extra": "^3.3.6", - "puppeteer-extra-plugin-recaptcha": "^3.6.8", - "puppeteer-extra-plugin-stealth": "^2.11.2", "sharp": "^0.32.3", "xlsx": "^0.18.5" }, 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/SydneyAIClient.js b/utils/SydneyAIClient.js index bee3eef..7d3334b 100644 --- a/utils/SydneyAIClient.js +++ b/utils/SydneyAIClient.js @@ -1,23 +1,23 @@ import fetch, { - Headers, - Request, - Response, + // Headers, + // Request, + // Response, FormData } from 'node-fetch' import crypto from 'crypto' import WebSocket from 'ws' -import { Config, pureSydneyInstruction } from './config.js' +import { Config } from './config.js' import { formatDate, getMasterQQ, isCN, getUserData, limitString } from './common.js' import moment from 'moment' import { getProxy } from './proxy.js' import common from '../../../lib/common/common.js' - -if (!globalThis.fetch) { - globalThis.fetch = fetch - globalThis.Headers = Headers - globalThis.Request = Request - globalThis.Response = Response -} +// +// if (!globalThis.fetch) { +// globalThis.fetch = fetch +// globalThis.Headers = Headers +// globalThis.Request = Request +// globalThis.Response = Response +// } // workaround for ver 7.x and ver 5.x let proxy = getProxy() @@ -65,34 +65,40 @@ export default class SydneyAIClient { accept: 'application/json', 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', 'content-type': 'application/json', - // 'sec-ch-ua': '"Microsoft Edge";v="113", "Chromium";v="113", "Not-A.Brand";v="24"', - // 'sec-ch-ua-arch': '"x86"', - // 'sec-ch-ua-bitness': '"64"', - // 'sec-ch-ua-full-version': '"112.0.1722.7"', - // 'sec-ch-ua-full-version-list': '"Chromium";v="112.0.5615.20", "Microsoft Edge";v="112.0.1722.7", "Not:A-Brand";v="99.0.0.0"', - // 'sec-ch-ua-mobile': '?0', - // 'sec-ch-ua-model': '', - // 'sec-ch-ua-platform': '"macOS"', - // 'sec-ch-ua-platform-version': '"15.0.0"', - // 'sec-fetch-dest': 'empty', - // 'sec-fetch-mode': 'cors', - // 'sec-fetch-site': 'same-origin', - // 'x-ms-client-request-id': crypto.randomUUID(), - // 'x-ms-useragent': 'azsdk-js-api-client-factory/1.0.0-beta.1 core-rest-pipeline/1.10.3 OS/macOS', + 'sec-ch-ua': '"Microsoft Edge";v="113", "Chromium";v="113", "Not-A.Brand";v="24"', + 'sec-ch-ua-arch': '"x86"', + 'sec-ch-ua-bitness': '"64"', + 'sec-ch-ua-full-version': '"112.0.1722.7"', + 'sec-ch-ua-full-version-list': '"Chromium";v="112.0.5615.20", "Microsoft Edge";v="112.0.1722.7", "Not:A-Brand";v="99.0.0.0"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-model': '', + 'sec-ch-ua-platform': '"macOS"', + 'sec-ch-ua-platform-version': '"15.0.0"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + 'x-ms-client-request-id': crypto.randomUUID(), + 'x-ms-useragent': 'azsdk-js-api-client-factory/1.0.0-beta.1 core-rest-pipeline/1.10.3 OS/macOS', // cookie: this.opts.cookies || `_U=${this.opts.userToken}`, - Referer: 'https://edgeservices.bing.com/edgesvc/chat?udsframed=1&form=SHORUN&clientscopes=chat,noheader,channelstable,' - // 'Referrer-Policy': 'origin-when-cross-origin', + Referer: 'https://edgeservices.bing.com/edgesvc/chat?udsframed=1&form=SHORUN&clientscopes=chat,noheader,channelstable,', + 'Referrer-Policy': 'origin-when-cross-origin' // Workaround for request being blocked due to geolocation // 'x-forwarded-for': '1.1.1.1' } } let initCk = 'SRCHD=AF=NOFORM; PPLState=1; SRCHHPGUSR=HV=' + new Date().getTime() + ';' - if (this.opts.userToken) { + if (this.opts.userToken || this.opts.cookies) { // 疑似无需token了 - fetchOptions.headers.cookie = `${initCk} _U=${this.opts.userToken}` - let proTag = await redis.get('CHATGPT:COPILOT_PRO_TAG:' + this.opts.userToken) + if (!this.opts.cookies) { + fetchOptions.headers.cookie = `${initCk} _U=${this.opts.userToken}` + } else { + fetchOptions.headers.cookie = this.opts.cookies + } + // let hash = md5(this.opts.cookies || this.opts.userToken) + let hash = crypto.createHash('md5').update(this.opts.cookies || this.opts.userToken).digest('hex') + let proTag = await redis.get('CHATGPT:COPILOT_PRO_TAG:' + hash) if (!proTag) { - let indexContentRes = await fetch('https://www.bing.com', { + let indexContentRes = await fetch('https://www.bing.com/chat', { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0', Cookie: `_U=${this.opts.userToken}` @@ -104,7 +110,7 @@ export default class SydneyAIClient { } else { proTag = 'false' } - await redis.set('CHATGPT:COPILOT_PRO_TAG:' + this.opts.userToken, proTag, { EX: 7200 }) + await redis.set('CHATGPT:COPILOT_PRO_TAG:' + hash, proTag, { EX: 7200 }) } if (proTag === 'true') { logger.info('当前账户为copilot pro用户') @@ -123,12 +129,12 @@ export default class SydneyAIClient { this.opts.host = 'https://edgeservices.bing.com/edgesvc' } logger.mark('使用host:' + this.opts.host) - let response = await fetch(`${this.opts.host}/turing/conversation/create?bundleVersion=1.1381.12`, fetchOptions) + let response = await fetch(`${this.opts.host}/turing/conversation/create?bundleVersion=1.1626.12`, fetchOptions) let text = await response.text() let retry = 10 while (retry >= 0 && response.status === 200 && !text) { await common.sleep(400) - response = await fetch(`${this.opts.host}/turing/conversation/create?bundleVersion=1.1381.12`, fetchOptions) + response = await fetch(`${this.opts.host}/turing/conversation/create?bundleVersion=1.1626.12`, fetchOptions) text = await response.text() retry-- } @@ -334,6 +340,21 @@ export default class SydneyAIClient { if (!text) { previousMessages = pm } else { + let example = [] + for (let i = 1; i < 4; i++) { + if (Config[`chatExampleUser${i}`]) { + example.push(...[ + { + text: Config[`chatExampleUser${i}`], + author: 'user' + }, + { + text: Config[`chatExampleBot${i}`], + author: 'bot' + } + ]) + } + } previousMessages = [ { text, @@ -343,6 +364,7 @@ export default class SydneyAIClient { text: '好的。', author: 'bot' }, + ...example, ...pm ] } @@ -379,6 +401,7 @@ export default class SydneyAIClient { // 'cricinfo', // 'cricinfov2', 'dv3sugg', + 'autosave', // 'gencontentv3', 'iycapbing', 'iyxapbing', @@ -387,12 +410,21 @@ export default class SydneyAIClient { // 'revimgsrc1', // 'revimgur', // 'clgalileo', - 'eredirecturl', + // 'eredirecturl', // copilot 'uquopt', - 'papynoapi', - 'gndlogcf', - 'sapsgrd' + // 'botthrottle', + // 'dlimitationnc', + // 'hourthrot', + // 'gndlogcf', + // 'ciorigimage', + // 'codeintfile', + 'eredirecturl', + // 'ldsummary', + // 'ldqa', + // 'sdretrieval', + // "gencontentv3", + // 'gpt4tmncnp' ] if (!isCreative) { optionsSets.push('clgalileo') @@ -453,28 +485,30 @@ export default class SydneyAIClient { 'GeneratedCode' ], sliceIds: [ - 'sappbcbt', - 'inlineadsv2ho-prod', - 'bgstream', - 'dlidlat', - 'autotts', - 'dlid', - 'sydoroff', - 'voicemap', - '72enasright', - 'semseronomon', - 'srchqryfix', - 'cmcpupsalltf', - 'proupsallcf', - '206mems0', - '0209bicv3', - '205dcl1bt15', - 'etlog', - 'fpallsticy', - '0208papynoa', - 'sapsgrd', - '1pgptwdes', - 'newzigpt' + // 'supllmnfe', + // 'nodescf', + // 'stcheckcf', + // 'invldrqcf', + // 'v6voice', + // 'vnextr100', + // 'sydvrate100', + // 'vnextvoice', + // 'scmcbasecf', + // 'cmcpupsalltf', + // 'sydtransjson', + // 'thdnsrchcf', + // '220dcl1bt15', + // '311dlicnc', + // '0215wcrwippsr', + // '0305hrthrot', + // '0130gpt4t', + // 'bingfccf', + // 'dissagrds0', + // '0228scs', + // 'scprompt1', + // '228pyfilenfb', + // 'ecipc', + // '3022tpvs0' ], requestId: crypto.randomUUID(), traceId: genRanHex(32), @@ -532,11 +566,17 @@ export default class SydneyAIClient { spokenTextMode: 'None', conversationId, previousMessages, - plugins: [ - // { - // id: 'c310c353-b9f0-4d76-ab0d-1dd5e979cf68' - // } - ] + // plugins: [ + // { + // id: 'c310c353-b9f0-4d76-ab0d-1dd5e979cf68', + // category: 1 + // } + // ], + // extraExtensionParameters: { + // 'gpt-creator-persona': { + // personaId: 'copilot' + // } + // } } if (encryptedconversationsignature) { diff --git a/utils/alibaba/qwen-api.js b/utils/alibaba/qwen-api.js index c1e12a9..2577e5c 100644 --- a/utils/alibaba/qwen-api.js +++ b/utils/alibaba/qwen-api.js @@ -63,7 +63,7 @@ import * as types from './types.js'; import globalFetch from 'node-fetch'; var CHATGPT_MODEL = 'qwen-turbo'; // qwen-plus var USER_LABEL_DEFAULT = 'User'; -var ASSISTANT_LABEL_DEFAULT = '同义千问'; +var ASSISTANT_LABEL_DEFAULT = '通义千问'; var QwenApi = /** @class */ (function () { /** * Creates a new client wrapper around Qwen's chat completion API, mimicing the official ChatGPT webapp's functionality as closely as possible. @@ -76,11 +76,11 @@ var QwenApi = /** @class */ (function () { this._apiBaseUrl = apiBaseUrl; this._debug = !!debug; this._fetch = fetch; - this._completionParams = __assign({ model: CHATGPT_MODEL, parameters: __assign({ top_p: 0.5, top_k: 50, temperature: 1.0, seed: 114514, enable_search: true, result_format: "text", incremental_output: false }, parameters) }, completionParams); + this._completionParams = __assign({ model: CHATGPT_MODEL, parameters: __assign({ top_p: 0.5, top_k: 50, temperature: 1.0, seed: 114514, enable_search: true, result_format: "message", incremental_output: false }, parameters) }, completionParams); this._systemMessage = systemMessage; if (this._systemMessage === undefined) { var currentDate = new Date().toISOString().split('T')[0]; - this._systemMessage = "You are ChatGPT, a large language model trained by Qwen. Answer as concisely as possible.\nKnowledge cutoff: 2021-09-01\nCurrent date: ".concat(currentDate); + this._systemMessage = "You are Qwen, a large language model trained by Alibaba Cloud. Answer as concisely as possible.\nCurrent date: ".concat(currentDate); } this._getMessageById = getMessageById !== null && getMessageById !== void 0 ? getMessageById : this._defaultGetMessageById; this._upsertMessage = upsertMessage !== null && upsertMessage !== void 0 ? upsertMessage : this._defaultUpsertMessage; @@ -120,7 +120,7 @@ var QwenApi = /** @class */ (function () { * @param opts.timeoutMs - Optional timeout in milliseconds (defaults to no timeout) * @param opts.onProgress - Optional callback which will be invoked every time the partial response is updated * @param opts.abortSignal - Optional callback used to abort the underlying `fetch` call using an [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) - * @param completionParams - Optional overrides to send to the [Qwen chat completion API](https://platform.openai.com/docs/api-reference/chat/create). Options like `temperature` and `presence_penalty` can be tweaked to change the personality of the assistant. + * @param opts.completionParams - Optional overrides to send to the [Qwen chat completion API](https://platform.openai.com/docs/api-reference/chat/create). Options like `temperature` and `presence_penalty` can be tweaked to change the personality of the assistant. * * @returns The response from ChatGPT */ @@ -128,7 +128,7 @@ var QwenApi = /** @class */ (function () { if (opts === void 0) { opts = {}; } if (role === void 0) { role = 'user'; } return __awaiter(this, void 0, void 0, function () { - var parentMessageId, _a, messageId, timeoutMs, completionParams, conversationId, abortSignal, abortController, message, latestQuestion, _b, messages, maxTokens, numTokens, result, responseP; + var parentMessageId, _a, messageId, timeoutMs, completionParams, conversationId, abortSignal, abortController, message, latestQuestion, parameters, _b, messages, maxTokens, numTokens, result, responseP; var _this = this; return __generator(this, function (_c) { switch (_c.label) { @@ -148,6 +148,9 @@ var QwenApi = /** @class */ (function () { text: text, }; latestQuestion = message; + parameters = Object.assign(this._completionParams.parameters, completionParams.parameters); + completionParams = Object.assign(this._completionParams, completionParams); + completionParams.parameters = parameters; return [4 /*yield*/, this._buildMessages(text, role, opts, completionParams)]; case 1: _b = _c.sent(), messages = _b.messages, maxTokens = _b.maxTokens, numTokens = _b.numTokens; @@ -158,28 +161,31 @@ var QwenApi = /** @class */ (function () { conversationId: conversationId, parentMessageId: messageId, text: undefined, + functionCall: undefined, + conversation: [] }; - this._completionParams.input = { messages: messages }; + completionParams.input = { messages: messages }; responseP = new Promise(function (resolve, reject) { return __awaiter(_this, void 0, void 0, function () { var url, headers, body, res, reason, msg, error, response, err_1; - return __generator(this, function (_a) { - switch (_a.label) { + var _a, _b, _c, _d, _e; + return __generator(this, function (_f) { + switch (_f.label) { case 0: url = "".concat(this._apiBaseUrl, "/services/aigc/text-generation/generation"); headers = { 'Content-Type': 'application/json', Authorization: "Bearer ".concat(this._apiKey) }; - body = __assign(__assign({}, this._completionParams), completionParams); + body = completionParams; if (this._debug) { console.log(JSON.stringify(body)); } if (this._debug) { console.log("sendMessage (".concat(numTokens, " tokens)"), body); } - _a.label = 1; + _f.label = 1; case 1: - _a.trys.push([1, 6, , 7]); + _f.trys.push([1, 6, , 7]); return [4 /*yield*/, this._fetch(url, { method: 'POST', headers: headers, @@ -187,11 +193,11 @@ var QwenApi = /** @class */ (function () { signal: abortSignal })]; case 2: - res = _a.sent(); + res = _f.sent(); if (!!res.ok) return [3 /*break*/, 4]; return [4 /*yield*/, res.text()]; case 3: - reason = _a.sent(); + reason = _f.sent(); msg = "Qwen error ".concat(res.status || res.statusText, ": ").concat(reason); error = new types.ChatGPTError(msg, { cause: res }); error.statusCode = res.status; @@ -199,18 +205,22 @@ var QwenApi = /** @class */ (function () { return [2 /*return*/, reject(error)]; case 4: return [4 /*yield*/, res.json()]; case 5: - response = _a.sent(); + response = _f.sent(); if (this._debug) { console.log(response); } + if (((_e = (_d = (_c = (_b = (_a = response.output) === null || _a === void 0 ? void 0 : _a.choices) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.message) === null || _d === void 0 ? void 0 : _d.tool_calls) === null || _e === void 0 ? void 0 : _e.length) > 0) { + // function call result + result.functionCall = response.output.choices[0].message.tool_calls[0].function; + } if (response === null || response === void 0 ? void 0 : response.request_id) { result.id = response.request_id; } result.detail = response; - result.text = response.output.text; + result.text = response.output.choices[0].message.content; return [2 /*return*/, resolve(result)]; case 6: - err_1 = _a.sent(); + err_1 = _f.sent(); return [2 /*return*/, reject(err_1)]; case 7: return [2 /*return*/]; } @@ -278,7 +288,8 @@ var QwenApi = /** @class */ (function () { ? messages.concat([ { role: role, - content: text + content: text, + name: role === 'tool' ? opts.name : undefined } ]) : messages; @@ -337,7 +348,8 @@ var QwenApi = /** @class */ (function () { nextMessages = nextMessages.slice(0, systemMessageOffset).concat(__spreadArray([ { role: parentMessageRole, - content: parentMessage.text + content: parentMessage.functionCall ? parentMessage.functionCall.arguments : parentMessage.text, + name: parentMessage.functionCall ? parentMessage.functionCall.name : undefined } ], nextMessages.slice(systemMessageOffset), true)); parentMessageId = parentMessage.parentMessageId; diff --git a/utils/alibaba/qwen-api.ts b/utils/alibaba/qwen-api.ts index c308f31..4bcc79c 100644 --- a/utils/alibaba/qwen-api.ts +++ b/utils/alibaba/qwen-api.ts @@ -7,11 +7,12 @@ import * as tokenizer from './tokenizer' import * as types from './types' import globalFetch from 'node-fetch' import {qwen, Role} from "./types"; +import {openai} from "../openai/types"; const CHATGPT_MODEL = 'qwen-turbo' // qwen-plus const USER_LABEL_DEFAULT = 'User' -const ASSISTANT_LABEL_DEFAULT = '同义千问' +const ASSISTANT_LABEL_DEFAULT = '通义千问' export class QwenApi { protected _apiKey: string @@ -64,7 +65,7 @@ export class QwenApi { temperature: 1.0, seed: 114514, enable_search: true, - result_format: "text", + result_format: "message", incremental_output: false, ...parameters }, @@ -75,7 +76,7 @@ export class QwenApi { if (this._systemMessage === undefined) { const currentDate = new Date().toISOString().split('T')[0] - this._systemMessage = `You are ChatGPT, a large language model trained by Qwen. Answer as concisely as possible.\nKnowledge cutoff: 2021-09-01\nCurrent date: ${currentDate}` + this._systemMessage = `You are Qwen, a large language model trained by Alibaba Cloud. Answer as concisely as possible.\nCurrent date: ${currentDate}` } this._getMessageById = getMessageById ?? this._defaultGetMessageById @@ -120,7 +121,7 @@ export class QwenApi { * @param opts.timeoutMs - Optional timeout in milliseconds (defaults to no timeout) * @param opts.onProgress - Optional callback which will be invoked every time the partial response is updated * @param opts.abortSignal - Optional callback used to abort the underlying `fetch` call using an [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) - * @param completionParams - Optional overrides to send to the [Qwen chat completion API](https://platform.openai.com/docs/api-reference/chat/create). Options like `temperature` and `presence_penalty` can be tweaked to change the personality of the assistant. + * @param opts.completionParams - Optional overrides to send to the [Qwen chat completion API](https://platform.openai.com/docs/api-reference/chat/create). Options like `temperature` and `presence_penalty` can be tweaked to change the personality of the assistant. * * @returns The response from ChatGPT */ @@ -129,7 +130,7 @@ export class QwenApi { opts: types.SendMessageOptions = {}, role: Role = 'user', ): Promise { - const { + let { parentMessageId, messageId = uuidv4(), timeoutMs, @@ -155,21 +156,30 @@ export class QwenApi { const latestQuestion = message + let parameters = Object.assign( + this._completionParams.parameters, + completionParams.parameters + ) + completionParams = Object.assign(this._completionParams, completionParams) + completionParams.parameters = parameters const { messages, maxTokens, numTokens } = await this._buildMessages( text, role, opts, completionParams ) + console.log(`maxTokens: ${maxTokens}, numTokens: ${numTokens}`) - const result: types.ChatMessage = { + const result: types.ChatMessage & { conversation: qwen.ChatCompletionRequestMessage[] } = { role: 'assistant', id: uuidv4(), conversationId, parentMessageId: messageId, text: undefined, + functionCall: undefined, + conversation: [] } - this._completionParams.input = { messages } + completionParams.input = { messages } const responseP = new Promise( async (resolve, reject) => { const url = `${this._apiBaseUrl}/services/aigc/text-generation/generation` @@ -177,10 +187,7 @@ export class QwenApi { 'Content-Type': 'application/json', Authorization: `Bearer ${this._apiKey}` } - const body = { - ...this._completionParams, - ...completionParams - } + const body = completionParams if (this._debug) { console.log(JSON.stringify(body)) } @@ -212,12 +219,15 @@ export class QwenApi { if (this._debug) { console.log(response) } - + if (response.output?.choices?.[0]?.message?.tool_calls?.length > 0) { + // function call result + result.functionCall = response.output.choices[0].message.tool_calls[0].function + } if (response?.request_id) { result.id = response.request_id } result.detail = response - result.text = response.output.text + result.text = response.output.choices[0].message.content return resolve(result) } catch (err) { return reject(err) @@ -283,7 +293,8 @@ export class QwenApi { ? messages.concat([ { role, - content: text + content: text, + name: role === 'tool' ? opts.name : undefined } ]) : messages @@ -338,7 +349,8 @@ export class QwenApi { nextMessages = nextMessages.slice(0, systemMessageOffset).concat([ { role: parentMessageRole, - content: parentMessage.text + content: parentMessage.functionCall ? parentMessage.functionCall.arguments : parentMessage.text, + name: parentMessage.functionCall ? parentMessage.functionCall.name : undefined }, ...nextMessages.slice(systemMessageOffset) ]) diff --git a/utils/alibaba/types.ts b/utils/alibaba/types.ts index 188269e..1057a82 100644 --- a/utils/alibaba/types.ts +++ b/utils/alibaba/types.ts @@ -1,80 +1,82 @@ import Keyv from 'keyv' +import {openai} from "../openai/types"; -export type Role = 'user' | 'assistant' | 'system' +export type Role = 'user' | 'assistant' | 'system' | 'tool' export type FetchFn = typeof fetch export type QWenAPIOptions = { - apiKey: string + apiKey: string - /** @defaultValue `'https://dashscope.aliyuncs.com/api/v1'` **/ - apiBaseUrl?: string + /** @defaultValue `'https://dashscope.aliyuncs.com/api/v1'` **/ + apiBaseUrl?: string - apiOrg?: string + apiOrg?: string - /** @defaultValue `false` **/ - debug?: boolean + /** @defaultValue `false` **/ + debug?: boolean - completionParams?: Partial< - Omit - > - parameters?: qwen.QWenParameters, + completionParams?: Partial< + Omit + > + parameters?: qwen.QWenParameters, - systemMessage?: string + systemMessage?: string - messageStore?: Keyv - getMessageById?: GetMessageByIdFunction - upsertMessage?: UpsertMessageFunction + messageStore?: Keyv + getMessageById?: GetMessageByIdFunction + upsertMessage?: UpsertMessageFunction - fetch?: FetchFn + fetch?: FetchFn } export type SendMessageOptions = { - /** - * function role name - */ - name?: string - messageId?: string - stream?: boolean - systemMessage?: string - parentMessageId?: string - conversationId?: string - timeoutMs?: number - onProgress?: (partialResponse: ChatMessage) => void - abortSignal?: AbortSignal - completionParams?: Partial< - Omit - > + /** + * function role name + */ + name?: string + messageId?: string + stream?: boolean + systemMessage?: string + parentMessageId?: string + conversationId?: string + timeoutMs?: number + onProgress?: (partialResponse: ChatMessage) => void + abortSignal?: AbortSignal + completionParams?: Partial< + Omit + > } export type MessageActionType = 'next' | 'variant' export type SendMessageBrowserOptions = { - conversationId?: string - parentMessageId?: string - messageId?: string - action?: MessageActionType - timeoutMs?: number - onProgress?: (partialResponse: ChatMessage) => void - abortSignal?: AbortSignal + conversationId?: string + parentMessageId?: string + messageId?: string + action?: MessageActionType + timeoutMs?: number + onProgress?: (partialResponse: ChatMessage) => void + abortSignal?: AbortSignal } export interface ChatMessage { - id: string - text: string - role: Role - parentMessageId?: string - conversationId?: string - detail?: - | qwen.CreateChatCompletionResponse - | CreateChatCompletionStreamResponse + id: string + text: string + role: Role + parentMessageId?: string + conversationId?: string + detail?: + | qwen.CreateChatCompletionResponse + | CreateChatCompletionStreamResponse + functionCall?: qwen.FunctionCall } export class ChatGPTError extends Error { - statusCode?: number - statusText?: string - isFinal?: boolean - accountId?: string + statusCode?: number + statusText?: string + isFinal?: boolean + accountId?: string } /** Returns a chat message from a store by it's ID (or null if not found). */ @@ -84,230 +86,289 @@ export type GetMessageByIdFunction = (id: string) => Promise export type UpsertMessageFunction = (message: ChatMessage) => Promise export interface CreateChatCompletionStreamResponse - extends openai.CreateChatCompletionDeltaResponse { - usage: CreateCompletionStreamResponseUsage + extends openai.CreateChatCompletionDeltaResponse { + usage: CreateCompletionStreamResponseUsage } export interface CreateCompletionStreamResponseUsage - extends openai.CreateCompletionResponseUsage { - estimated: true + extends openai.CreateCompletionResponseUsage { + estimated: true } /** * https://chat.openapi.com/backend-api/conversation */ export type ConversationJSONBody = { - /** - * The action to take - */ - action: string + /** + * The action to take + */ + action: string - /** - * The ID of the conversation - */ - conversation_id?: string + /** + * The ID of the conversation + */ + conversation_id?: string - /** - * Prompts to provide - */ - messages: Prompt[] + /** + * Prompts to provide + */ + messages: Prompt[] - /** - * The model to use - */ - model: string + /** + * The model to use + */ + model: string - /** - * The parent message ID - */ - parent_message_id: string + /** + * The parent message ID + */ + parent_message_id: string } export type Prompt = { - /** - * The content of the prompt - */ - content: PromptContent + /** + * The content of the prompt + */ + content: PromptContent - /** - * The ID of the prompt - */ - id: string + /** + * The ID of the prompt + */ + id: string - /** - * The role played in the prompt - */ - role: Role + /** + * The role played in the prompt + */ + role: Role } export type ContentType = 'text' export type PromptContent = { - /** - * The content type of the prompt - */ - content_type: ContentType + /** + * The content type of the prompt + */ + content_type: ContentType - /** - * The parts to the prompt - */ - parts: string[] + /** + * The parts to the prompt + */ + parts: string[] } export type ConversationResponseEvent = { - message?: Message - conversation_id?: string - error?: string | null + message?: Message + conversation_id?: string + error?: string | null } export type Message = { - id: string - content: MessageContent - role: Role - user: string | null - create_time: string | null - update_time: string | null - end_turn: null - weight: number - recipient: string - metadata: MessageMetadata + id: string + content: MessageContent + role: Role + user: string | null + create_time: string | null + update_time: string | null + end_turn: null + weight: number + recipient: string + metadata: MessageMetadata } export type MessageContent = { - content_type: string - parts: string[] + content_type: string + parts: string[] } export type MessageMetadata = any export namespace qwen { - export interface CreateChatCompletionDeltaResponse { - id: string - object: 'chat.completion.chunk' - created: number - model: string - choices: [ - { - delta: { - role: Role - content?: string, - function_call?: {name: string, arguments: string} - } - index: number - finish_reason: string | null - } - ] - } + export interface CreateChatCompletionDeltaResponse { + id: string + object: 'chat.completion.chunk' + created: number + model: string + choices: [ + { + delta: { + role: Role + content?: string, + function_call?: { name: string, arguments: string } + } + index: number + finish_reason: string | null + } + ] + } + + /** + * + * @export + * @interface ChatCompletionRequestMessage + */ + export interface ChatCompletionRequestMessage { + /** + * The role of the author of this message. + * @type {string} + * @memberof ChatCompletionRequestMessage + */ + role: ChatCompletionRequestMessageRoleEnum + /** + * The contents of the message + * @type {string} + * @memberof ChatCompletionRequestMessage + */ + content: string /** - * - * @export - * @interface ChatCompletionRequestMessage + * role为tool表示当前message为function_call的调用结果,name是function的名称,需要和上轮response中的tool_calls[i].function.name参数保持一致,content为function的输出。 */ - export interface ChatCompletionRequestMessage { - /** - * The role of the author of this message. - * @type {string} - * @memberof ChatCompletionRequestMessage - */ - role: ChatCompletionRequestMessageRoleEnum - /** - * The contents of the message - * @type {string} - * @memberof ChatCompletionRequestMessage - */ - content: string - } + name?: string + } - export declare const ChatCompletionRequestMessageRoleEnum: { - readonly System: 'system' - readonly User: 'user' - readonly Assistant: 'assistant' - } - export declare type ChatCompletionRequestMessageRoleEnum = - (typeof ChatCompletionRequestMessageRoleEnum)[keyof typeof ChatCompletionRequestMessageRoleEnum] + export interface FunctionCall { + name: string + arguments: string + } + + export declare const ChatCompletionRequestMessageRoleEnum: { + readonly System: 'system' + readonly User: 'user' + readonly Assistant: 'assistant' + readonly Tool: 'tool' + } + export declare type ChatCompletionRequestMessageRoleEnum = + (typeof ChatCompletionRequestMessageRoleEnum)[keyof typeof ChatCompletionRequestMessageRoleEnum] - export interface QWenInput { - messages: Array - } + export interface QWenInput { + messages: Array + } - export interface QWenParameters { - result_format: string - top_p: number - top_k: number - seed: number - temperature: number - enable_search: boolean - incremental_output: boolean - } + export interface QWenParameters { + result_format: "text" | "message" + top_p: number + top_k: number + seed: number + temperature: number + enable_search: boolean + incremental_output: boolean + tools: Tools[] + } + + export interface Tools { + type: "function" + function: QwenFunction + } + + export interface QwenFunction { + name: string + description: string + parameters: QwenFunctionParameters + } + + export interface QwenFunctionParameters { + type: "object" + properties: Properties; + required?: string[] + } + + interface Properties { + [key: string]: Property; + } + + interface Property { + type: string; + description?: string; + enum?: string[]; + } + + /** + * + * @export + * @interface CreateChatCompletionRequest + */ + export interface CreateChatCompletionRequest { + /** + * ID of the model to use. Currently, only `gpt-3.5-turbo` and `gpt-3.5-turbo-0301` are supported. + * @type {string} + * @memberof CreateChatCompletionRequest + */ + model: string + /** + * The messages to generate chat completions for, in the [chat format](/docs/guides/chat/introduction). + * @type {Array} + * @memberof CreateChatCompletionRequest + */ + input?: QWenInput + + parameters: QWenParameters + } + + /** + * + * @export + * @interface CreateChatCompletionResponse + */ + export interface CreateChatCompletionResponse { /** * - * @export - * @interface CreateChatCompletionRequest + * @type {string} + * @memberof CreateChatCompletionResponse */ - export interface CreateChatCompletionRequest { - /** - * ID of the model to use. Currently, only `gpt-3.5-turbo` and `gpt-3.5-turbo-0301` are supported. - * @type {string} - * @memberof CreateChatCompletionRequest - */ - model: string - /** - * The messages to generate chat completions for, in the [chat format](/docs/guides/chat/introduction). - * @type {Array} - * @memberof CreateChatCompletionRequest - */ - input?: QWenInput + request_id: string + /** + * + * @type {QWenOutput} + * @memberof CreateChatCompletionResponse + */ + output: QWenOutput + /** + * + * @type {CreateCompletionResponseUsage} + * @memberof CreateChatCompletionResponse + */ + usage?: CreateCompletionResponseUsage + } - parameters: QWenParameters - } + export interface QWenOutput { + finish_reason: string + text?: string + choices?: Choice[] + } + + export interface Choice { + finish_reason: string + message: ResponseMessage + } + + export interface ResponseMessage { + role: Role + content: string + tool_calls: ToolCall[] + } + + export interface ToolCall { + function: FunctionCall + type: "function" + } + /** + * + * @export + * @interface CreateCompletionResponseUsage + */ + export interface CreateCompletionResponseUsage { /** * - * @export - * @interface CreateChatCompletionResponse + * @type {number} + * @memberof CreateCompletionResponseUsage */ - export interface CreateChatCompletionResponse { - /** - * - * @type {string} - * @memberof CreateChatCompletionResponse - */ - request_id: string - /** - * - * @type {QWenOutput} - * @memberof CreateChatCompletionResponse - */ - output: QWenOutput - /** - * - * @type {CreateCompletionResponseUsage} - * @memberof CreateChatCompletionResponse - */ - usage?: CreateCompletionResponseUsage - } - export interface QWenOutput { - finish_reason: string - text: string - } + input_tokens: number /** * - * @export - * @interface CreateCompletionResponseUsage + * @type {number} + * @memberof CreateCompletionResponseUsage */ - export interface CreateCompletionResponseUsage { - /** - * - * @type {number} - * @memberof CreateCompletionResponseUsage - */ - input_tokens: number - /** - * - * @type {number} - * @memberof CreateCompletionResponseUsage - */ - output_tokens: number - } + output_tokens: number + } } diff --git a/utils/bard.js b/utils/bard.js index dff940c..a28e060 100644 --- a/utils/bard.js +++ b/utils/bard.js @@ -1,373 +1,373 @@ // https://github.com/EvanZhouDev/bard-ai class Bard { - static JSON = "json"; - static MD = "markdown"; + static JSON = 'json' + static MD = 'markdown' // ID derived from Cookie - SNlM0e; + SNlM0e // HTTPS Headers - #headers; + #headers // Resolution status of initialization call - #initPromise; + #initPromise - #bardURL = "https://bard.google.com"; + #bardURL = 'https://bard.google.com' // Wether or not to log events to console - #verbose = false; + #verbose = false // Fetch function - #fetch = fetch; + #fetch = fetch - constructor(cookie, config) { - // Register some settings - if (config?.verbose == true) this.#verbose = true; - if (config?.fetch) this.#fetch = config.fetch; - // 可变更访问地址,利用反向代理绕过区域限制 - if (config?.bardURL) this.#bardURL = config.bardURL; + constructor (cookie, config) { + // Register some settings + if (config?.verbose == true) this.#verbose = true + if (config?.fetch) this.#fetch = config.fetch + // 可变更访问地址,利用反向代理绕过区域限制 + if (config?.bardURL) this.#bardURL = config.bardURL - // If a Cookie is provided, initialize - if (cookie) { - this.#initPromise = this.#init(cookie); - } else { - throw new Error("Please provide a Cookie when initializing Bard."); - } - this.cookie = cookie; + // If a Cookie is provided, initialize + if (cookie) { + this.#initPromise = this.#init(cookie) + } else { + throw new Error('Please provide a Cookie when initializing Bard.') + } + this.cookie = cookie } // You can also choose to initialize manually - async #init(cookie) { - this.#verbose && console.log("🚀 Starting intialization"); - // Assign headers - this.#headers = { - Host: this.#bardURL.match(/^https?:\/\/([^\/]+)\/?$/)[1], - "X-Same-Domain": "1", - "User-Agent": - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36", - "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", - Origin: this.#bardURL, - Referer: this.#bardURL, - Cookie: (typeof cookie === "object") ? (Object.entries(cookie).map(([key, val]) => `${key}=${val};`).join("")) : ("__Secure-1PSID=" + cookie), - }; + async #init (cookie) { + this.#verbose && console.log('🚀 Starting intialization') + // Assign headers + this.#headers = { + Host: this.#bardURL.match(/^https?:\/\/([^\/]+)\/?$/)[1], + 'X-Same-Domain': '1', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36', + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + Origin: this.#bardURL, + Referer: this.#bardURL, + Cookie: (typeof cookie === 'object') ? (Object.entries(cookie).map(([key, val]) => `${key}=${val};`).join('')) : ('__Secure-1PSID=' + cookie) + } - let responseText; - // Attempt to retrieve SNlM0e - try { - this.#verbose && - console.log("🔒 Authenticating your Google account"); - responseText = await this.#fetch(this.#bardURL, { - method: "GET", - headers: this.#headers, - credentials: "include", - }) - .then((response) => response.text()) - } catch (e) { - // Failure to get server - throw new Error( - "Could not fetch Google Bard. You may be disconnected from internet: " + + let responseText + // Attempt to retrieve SNlM0e + try { + this.#verbose && + console.log('🔒 Authenticating your Google account') + responseText = await this.#fetch(this.#bardURL, { + method: 'GET', + headers: this.#headers, + credentials: 'include' + }) + .then((response) => response.text()) + } catch (e) { + // Failure to get server + throw new Error( + 'Could not fetch Google Bard. You may be disconnected from internet: ' + e - ); - } + ) + } - try { - const SNlM0e = responseText.match(/SNlM0e":"(.*?)"/)[1]; - // Assign SNlM0e and return it - this.SNlM0e = SNlM0e; - this.#verbose && console.log("✅ Initialization finished\n"); - return SNlM0e; - } catch { - throw new Error( - "Could not use your Cookie. Make sure that you copied correctly the Cookie with name __Secure-1PSID exactly. If you are sure your cookie is correct, you may also have reached your rate limit." - ); - } + try { + const SNlM0e = responseText.match(/SNlM0e":"(.*?)"/)[1] + // Assign SNlM0e and return it + this.SNlM0e = SNlM0e + this.#verbose && console.log('✅ Initialization finished\n') + return SNlM0e + } catch { + throw new Error( + 'Could not use your Cookie. Make sure that you copied correctly the Cookie with name __Secure-1PSID exactly. If you are sure your cookie is correct, you may also have reached your rate limit.' + ) + } } - async #uploadImage(name, buffer) { - this.#verbose && console.log("🖼️ Starting image processing"); - let size = buffer.byteLength; - let formBody = [ - `${encodeURIComponent("File name")}=${encodeURIComponent([name])}`, - ]; + async #uploadImage (name, buffer) { + this.#verbose && console.log('🖼️ Starting image processing') + let size = buffer.byteLength + let formBody = [ + `${encodeURIComponent('File name')}=${encodeURIComponent([name])}` + ] - try { - this.#verbose && - console.log("💻 Finding Google server destination"); - let response = await this.#fetch( - "https://content-push.googleapis.com/upload/", - { - method: "POST", - headers: { - "X-Goog-Upload-Command": "start", - "X-Goog-Upload-Protocol": "resumable", - "X-Goog-Upload-Header-Content-Length": size, - "X-Tenant-Id": "bard-storage", - "Push-Id": "feeds/mcudyrk2a4khkz", - }, - body: formBody, - credentials: "include", - } - ); + try { + this.#verbose && + console.log('💻 Finding Google server destination') + let response = await this.#fetch( + 'https://content-push.googleapis.com/upload/', + { + method: 'POST', + headers: { + 'X-Goog-Upload-Command': 'start', + 'X-Goog-Upload-Protocol': 'resumable', + 'X-Goog-Upload-Header-Content-Length': size, + 'X-Tenant-Id': 'bard-storage', + 'Push-Id': 'feeds/mcudyrk2a4khkz' + }, + body: formBody, + credentials: 'include' + } + ) - const uploadUrl = response.headers.get("X-Goog-Upload-URL"); - this.#verbose && console.log("📤 Sending your image"); - response = await this.#fetch(uploadUrl, { - method: "POST", - headers: { - "X-Goog-Upload-Command": "upload, finalize", - "X-Goog-Upload-Offset": 0, - "X-Tenant-Id": "bard-storage", - }, - body: buffer, - credentials: "include", - }); + const uploadUrl = response.headers.get('X-Goog-Upload-URL') + this.#verbose && console.log('📤 Sending your image') + response = await this.#fetch(uploadUrl, { + method: 'POST', + headers: { + 'X-Goog-Upload-Command': 'upload, finalize', + 'X-Goog-Upload-Offset': 0, + 'X-Tenant-Id': 'bard-storage' + }, + body: buffer, + credentials: 'include' + }) - const imageFileLocation = await response.text(); + const imageFileLocation = await response.text() - this.#verbose && console.log("✅ Image finished working\n"); - return imageFileLocation; - } catch (e) { - throw new Error( - "Could not fetch Google Bard. You may be disconnected from internet: " + + this.#verbose && console.log('✅ Image finished working\n') + return imageFileLocation + } catch (e) { + throw new Error( + 'Could not fetch Google Bard. You may be disconnected from internet: ' + e - ); - } + ) + } } // Query Bard - async #query(message, config) { - let formatMarkdown = (text, images) => { - if (!images) return text; + async #query (message, config) { + let formatMarkdown = (text, images) => { + if (!images) return text - for (let imageData of images) { - const formattedTag = `!${imageData.tag}(${imageData.url})`; - text = text.replace( - new RegExp(`(?!\\!)\\[${imageData.tag.slice(1, -1)}\\]`), - formattedTag - ); - } - - return text; + for (let imageData of images) { + const formattedTag = `!${imageData.tag}(${imageData.url})` + text = text.replace( + new RegExp(`(?!\\!)\\[${imageData.tag.slice(1, -1)}\\]`), + formattedTag + ) } - let { ids, imageBuffer } = config; + return text + } - // Wait until after init - await this.#initPromise; + let { ids, imageBuffer } = config - this.#verbose && console.log("🔎 Starting Bard Query"); + // Wait until after init + await this.#initPromise - // If user has not run init - if (!this.SNlM0e) { - throw new Error( - "Please initialize Bard first. If you haven't passed in your Cookie into the class, run Bard.init(cookie)." - ); - } + this.#verbose && console.log('🔎 Starting Bard Query') - this.#verbose && console.log("🏗️ Building Request"); - // HTTPS parameters - const params = { - bl: "boq_assistant-bard-web-server_20230711.08_p0", - _reqID: ids?._reqID ?? "0", - rt: "c", - }; + // If user has not run init + if (!this.SNlM0e) { + throw new Error( + "Please initialize Bard first. If you haven't passed in your Cookie into the class, run Bard.init(cookie)." + ) + } - // If IDs are provided, but doesn't have every one of the expected IDs, error - const messageStruct = [ - [message], - null, - [null, null, null], - ]; + this.#verbose && console.log('🏗️ Building Request') + // HTTPS parameters + const params = { + bl: 'boq_assistant-bard-web-server_20230711.08_p0', + _reqID: ids?._reqID ?? '0', + rt: 'c' + } - if (imageBuffer) { - let imageLocation = await this.#uploadImage( - `bard-ai_upload`, - imageBuffer - ); - messageStruct[0].push(0, null, [ - [[imageLocation, 1], "bard-ai_upload"], - ]); - } + // If IDs are provided, but doesn't have every one of the expected IDs, error + const messageStruct = [ + [message], + null, + [null, null, null] + ] - if (ids) { - const { conversationID, responseID, choiceID } = ids; - messageStruct[2] = [conversationID, responseID, choiceID]; - } + if (imageBuffer) { + let imageLocation = await this.#uploadImage( + 'bard-ai_upload', + imageBuffer + ) + messageStruct[0].push(0, null, [ + [[imageLocation, 1], 'bard-ai_upload'] + ]) + } - // HTTPs data - const data = { - "f.req": JSON.stringify([null, JSON.stringify(messageStruct)]), - at: this.SNlM0e, - }; + if (ids) { + const { conversationID, responseID, choiceID } = ids + messageStruct[2] = [conversationID, responseID, choiceID] + } - // URL that we are submitting to - const url = new URL( - "/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate", - this.#bardURL - ); + // HTTPs data + const data = { + 'f.req': JSON.stringify([null, JSON.stringify(messageStruct)]), + at: this.SNlM0e + } - // Append parameters to the URL - for (const key in params) { - url.searchParams.append(key, params[key]); - } + // URL that we are submitting to + const url = new URL( + '/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate', + this.#bardURL + ) - // Encode the data - const formBody = Object.entries(data) - .map( - ([property, value]) => + // Append parameters to the URL + for (const key in params) { + url.searchParams.append(key, params[key]) + } + + // Encode the data + const formBody = Object.entries(data) + .map( + ([property, value]) => `${encodeURIComponent(property)}=${encodeURIComponent( value )}` - ) - .join("&"); + ) + .join('&') - this.#verbose && console.log("💭 Sending message to Bard"); - // Send the fetch request - const chatData = await this.#fetch(url.toString(), { - method: "POST", - headers: this.#headers, - body: formBody, - credentials: "include", + this.#verbose && console.log('💭 Sending message to Bard') + // Send the fetch request + const chatData = await this.#fetch(url.toString(), { + method: 'POST', + headers: this.#headers, + body: formBody, + credentials: 'include' + }) + .then((response) => { + return response.text() }) - .then((response) => { - return response.text(); - }) - .then((text) => { - return JSON.parse(text.split("\n")[3])[0][2]; - }) - .then((rawData) => JSON.parse(rawData)); + .then((text) => { + return JSON.parse(text.split('\n')[3])[0][2] + }) + .then((rawData) => JSON.parse(rawData)) - this.#verbose && console.log("🧩 Parsing output"); - // Get first Bard-recommended answer - const answer = chatData[4][0]; + this.#verbose && console.log('🧩 Parsing output') + // Get first Bard-recommended answer + const answer = chatData[4][0] - // Text of that answer - const text = answer[1][0]; + // Text of that answer + const text = answer[1][0] - // Get data about images in that answer - const images = + // Get data about images in that answer + const images = answer[4]?.map((x) => ({ - tag: x[2], - url: x[3][0][0], - info: { - raw: x[0][0][0], - source: x[1][0][0], - alt: x[0][4], - website: x[1][1], - favicon: x[1][3], - }, - })) ?? []; + tag: x[2], + url: x[3][0][0], + info: { + raw: x[0][0][0], + source: x[1][0][0], + alt: x[0][4], + website: x[1][1], + favicon: x[1][3] + } + })) ?? [] - this.#verbose && console.log("✅ All done!\n"); - // Put everything together and return - return { - content: formatMarkdown(text, images), - images: images, - ids: { - conversationID: chatData[1][0], - responseID: chatData[1][1], - choiceID: answer[0], - _reqID: String(parseInt(ids?._reqID ?? 0) + 100000), - }, - }; + this.#verbose && console.log('✅ All done!\n') + // Put everything together and return + return { + content: formatMarkdown(text, images), + images, + ids: { + conversationID: chatData[1][0], + responseID: chatData[1][1], + choiceID: answer[0], + _reqID: String(parseInt(ids?._reqID ?? 0) + 100000) + } + } } - async #parseConfig(config) { - let result = { - useJSON: false, - imageBuffer: undefined, // Returns as {extension, filename} - ids: undefined, - }; + async #parseConfig (config) { + let result = { + useJSON: false, + imageBuffer: undefined, // Returns as {extension, filename} + ids: undefined + } - // Verify that format is one of the two types - if (config?.format) { - switch (config.format) { - case Bard.JSON: - result.useJSON = true; - break; - case Bard.MD: - result.useJSON = false; - break; - default: - throw new Error( - "Format can obly be Bard.JSON for JSON output or Bard.MD for Markdown output." - ); - } + // Verify that format is one of the two types + if (config?.format) { + switch (config.format) { + case Bard.JSON: + result.useJSON = true + break + case Bard.MD: + result.useJSON = false + break + default: + throw new Error( + 'Format can obly be Bard.JSON for JSON output or Bard.MD for Markdown output.' + ) } + } - // Verify that the image passed in is either a path to a jpeg, jpg, png, or webp, or that it is a Buffer - if (config?.image) { - if ( - config.image instanceof ArrayBuffer - ) { - result.imageBuffer = config.image; - } else if ( - typeof config.image === "string" && + // Verify that the image passed in is either a path to a jpeg, jpg, png, or webp, or that it is a Buffer + if (config?.image) { + if ( + config.image instanceof ArrayBuffer + ) { + result.imageBuffer = config.image + } else if ( + typeof config.image === 'string' && /\.(jpeg|jpg|png|webp)$/.test(config.image) - ) { - let fs; + ) { + let fs - try { - fs = await import("fs") - } catch { - throw new Error( - "Loading from an image file path is not supported in a browser environment.", - ); - } + try { + fs = await import('fs') + } catch { + throw new Error( + 'Loading from an image file path is not supported in a browser environment.' + ) + } - result.imageBuffer = fs.readFileSync( - config.image, - ).buffer; - } else { - throw new Error( - "Provide your image as a file path to a .jpeg, .jpg, .png, or .webp, or a Buffer." - ); - } + result.imageBuffer = fs.readFileSync( + config.image + ).buffer + } else { + throw new Error( + 'Provide your image as a file path to a .jpeg, .jpg, .png, or .webp, or a Buffer.' + ) } + } - // Verify that all values in IDs exist - if (config?.ids) { - if (config.ids.conversationID && config.ids.responseID && config.ids.choiceID && config.ids._reqID) { - result.ids = config.ids; - } else { - throw new Error( - "Please provide the IDs exported exactly as given." - ); - } + // Verify that all values in IDs exist + if (config?.ids) { + if (config.ids.conversationID && config.ids.responseID && config.ids.choiceID && config.ids._reqID) { + result.ids = config.ids + } else { + throw new Error( + 'Please provide the IDs exported exactly as given.' + ) } - return result; + } + return result } // Ask Bard a question! - async ask(message, config) { - let { useJSON, imageBuffer, ids } = await this.#parseConfig(config); - let response = await this.#query(message, { imageBuffer, ids }); - return useJSON ? response : response.content; + async ask (message, config) { + let { useJSON, imageBuffer, ids } = await this.#parseConfig(config) + let response = await this.#query(message, { imageBuffer, ids }) + return useJSON ? response : response.content } - createChat(ids) { - let bard = this; - class Chat { - ids = ids; + createChat (ids) { + let bard = this + class Chat { + ids = ids - async ask(message, config) { - let { useJSON, imageBuffer } = await bard.#parseConfig(config); - let response = await bard.#query(message, { - imageBuffer, - ids: this.ids, - }); - this.ids = response.ids; - return useJSON ? response : response.content; - } - - export() { - return this.ids; - } + async ask (message, config) { + let { useJSON, imageBuffer } = await bard.#parseConfig(config) + let response = await bard.#query(message, { + imageBuffer, + ids: this.ids + }) + this.ids = response.ids + return useJSON ? response : response.content } - return new Chat(); + export () { + return this.ids + } + } + + return new Chat() } } -export default Bard; +export default Bard diff --git a/utils/bingCaptcha.js b/utils/bingCaptcha.js index d0ebf11..5805e55 100644 --- a/utils/bingCaptcha.js +++ b/utils/bingCaptcha.js @@ -1,7 +1,7 @@ import fetch from 'node-fetch' // this file is deprecated -import {Config} from './config.js' +import { Config } from './config.js' import HttpsProxyAgent from 'https-proxy-agent' const newFetch = (url, options = {}) => { diff --git a/utils/browser.js b/utils/browser.js index 5866790..cd0fb37 100644 --- a/utils/browser.js +++ b/utils/browser.js @@ -1,10 +1,5 @@ import lodash from 'lodash' -import { Config } from '../utils/config.js' -import StealthPlugin from 'puppeteer-extra-plugin-stealth' -import { getOpenAIAuth } from './openai-auth.js' -import { v4 as uuidv4 } from 'uuid' -import common from '../../../lib/common/common.js' -const chatUrl = 'https://chat.openai.com/chat' +import { Config } from './config.js' let puppeteer = {} class Puppeteer { @@ -48,19 +43,9 @@ class Puppeteer { async initPupp () { if (!lodash.isEmpty(puppeteer)) return puppeteer - puppeteer = (await import('puppeteer-extra')).default - const pluginStealth = StealthPlugin() - puppeteer.use(pluginStealth) - if (Config['2captchaToken']) { - const pluginCaptcha = (await import('puppeteer-extra-plugin-recaptcha')).default - puppeteer.use(pluginCaptcha({ - provider: { - id: '2captcha', - token: Config['2captchaToken'] // REPLACE THIS WITH YOUR OWN 2CAPTCHA API KEY ⚡ - }, - visualFeedback: true - })) - } + puppeteer = (await import('puppeteer')).default + // const pluginStealth = StealthPlugin() + // puppeteer.use(pluginStealth) return puppeteer } @@ -109,25 +94,10 @@ export class ChatGPTPuppeteer extends Puppeteer { constructor (opts = {}) { super() const { - email, - password, - markdown = true, - debug = false, - isGoogleLogin = false, - minimize = true, - captchaToken, - executablePath + debug = false } = opts - this._email = email - this._password = password - - this._markdown = !!markdown this._debug = !!debug - this._isGoogleLogin = !!isGoogleLogin - this._minimize = !!minimize - this._captchaToken = captchaToken - this._executablePath = executablePath } async getBrowser () { @@ -138,394 +108,6 @@ export class ChatGPTPuppeteer extends Puppeteer { } } - async init () { - // if (this.inited) { - // return true - // } - logger.info('init chatgpt browser') - try { - // this.browser = await getBrowser({ - // captchaToken: this._captchaToken, - // executablePath: this._executablePath - // }) - this.browser = await this.getBrowser() - this._page = - (await this.browser.pages())[0] || (await this.browser.newPage()) - await maximizePage(this._page) - this._page.on('request', this._onRequest.bind(this)) - this._page.on('response', this._onResponse.bind(this)) - // bypass cloudflare and login - let preCookies = await redis.get('CHATGPT:RAW_COOKIES') - if (preCookies) { - await this._page.setCookie(...JSON.parse(preCookies)) - } - // const url = this._page.url().replace(/\/$/, '') - // bypass annoying popup modals - await this._page.evaluateOnNewDocument(() => { - window.localStorage.setItem('oai/apps/hasSeenOnboarding/chat', 'true') - const chatGPTUpdateDates = ['2022-12-15', '2022-12-19', '2023-01-09', '2023-01-30', '2023-02-10'] - chatGPTUpdateDates.forEach(date => { - window.localStorage.setItem( - `oai/apps/hasSeenReleaseAnnouncement/${date}`, - 'true' - ) - }) - }) - await this._page.goto(chatUrl, { - waitUntil: 'networkidle2' - }) - let timeout = 30000 - try { - while (timeout > 0 && (await this._page.title()).toLowerCase().indexOf('moment') > -1) { - // if meet captcha - if (Config['2captchaToken']) { - await this._page.solveRecaptchas() - } - await common.sleep(300) - timeout = timeout - 300 - } - } catch (e) { - // navigation后获取title会报错,报错说明已经在navigation了正合我意。 - } - if (timeout < 0) { - logger.error('wait for cloudflare navigation timeout. 可能遇见验证码') - throw new Error('wait for cloudflare navigation timeout. 可能遇见验证码') - } - try { - await this._page.waitForNavigation({ timeout: 3000 }) - } catch (e) {} - - if (!await this.getIsAuthenticated()) { - await redis.del('CHATGPT:RAW_COOKIES') - logger.info('需要登录,准备进行自动化登录') - await getOpenAIAuth({ - email: this._email, - password: this._password, - browser: this.browser, - page: this._page, - isGoogleLogin: this._isGoogleLogin - }) - logger.info('登录完成') - } else { - logger.info('无需登录') - } - } catch (err) { - if (this.browser) { - await this.browser.close() - } - - this.browser = null - this._page = null - - throw err - } - - const url = this._page.url().replace(/\/$/, '') - - if (url !== chatUrl) { - await this._page.goto(chatUrl, { - waitUntil: 'networkidle2' - }) - } - - // dismiss welcome modal (and other modals) - do { - const modalSelector = '[data-headlessui-state="open"]' - - if (!(await this._page.$(modalSelector))) { - break - } - - try { - await this._page.click(`${modalSelector} button:last-child`) - } catch (err) { - // "next" button not found in welcome modal - break - } - - await common.sleep(300) - } while (true) - - if (!await this.getIsAuthenticated()) { - return false - } - - if (this._minimize) { - await minimizePage(this._page) - } - - return true - } - - _onRequest = (request) => { - const url = request.url() - if (!isRelevantRequest(url)) { - return - } - - const method = request.method() - let body - - if (method === 'POST') { - body = request.postData() - - try { - body = JSON.parse(body) - } catch (_) { - } - - // if (url.endsWith('/conversation') && typeof body === 'object') { - // const conversationBody: types.ConversationJSONBody = body - // const conversationId = conversationBody.conversation_id - // const parentMessageId = conversationBody.parent_message_id - // const messageId = conversationBody.messages?.[0]?.id - // const prompt = conversationBody.messages?.[0]?.content?.parts?.[0] - - // // TODO: store this info for the current sendMessage request - // } - } - - if (this._debug) { - console.log('\nrequest', { - url, - method, - headers: request.headers(), - body - }) - } - } - - _onResponse = async (response) => { - const request = response.request() - - const url = response.url() - if (!isRelevantRequest(url)) { - return - } - - const status = response.status() - - let body - try { - body = await response.json() - } catch (_) { - } - - if (this._debug) { - console.log('\nresponse', { - url, - ok: response.ok(), - status, - statusText: response.statusText(), - headers: response.headers(), - body, - request: { - method: request.method(), - headers: request.headers(), - body: request.postData() - } - }) - } - - if (url.endsWith('/conversation')) { - if (status === 403) { - await this.handle403Error() - } - } else if (url.endsWith('api/auth/session')) { - if (status === 403) { - await this.handle403Error() - } else { - const session = body - if (session?.accessToken) { - this._accessToken = session.accessToken - } - } - } - } - - async handle403Error () { - console.log(`ChatGPT "${this._email}" session expired; refreshing...`) - try { - await maximizePage(this._page) - await this._page.reload({ - waitUntil: 'networkidle2', - timeout: Config.chromeTimeoutMS // 2 minutes - }) - if (this._minimize) { - await minimizePage(this._page) - } - } catch (err) { - console.error( - `ChatGPT "${this._email}" error refreshing session`, - err.toString() - ) - } - } - - async getIsAuthenticated () { - try { - const inputBox = await this._getInputBox() - return !!inputBox - } catch (err) { - // can happen when navigating during login - return false - } - } - - async sendMessage ( - message, - opts = {} - ) { - const { - conversationId, - parentMessageId = uuidv4(), - messageId = uuidv4(), - action = 'next', - // TODO - timeoutMs, - // onProgress, - onConversationResponse - } = opts - - const inputBox = await this._getInputBox() - if (!inputBox || !this._accessToken) { - console.log(`chatgpt re-authenticating ${this._email}`) - let isAuthenticated = false - - try { - isAuthenticated = await this.init() - } catch (err) { - console.warn( - `chatgpt error re-authenticating ${this._email}`, - err.toString() - ) - throw err - } - let timeout = 100000 - if (isAuthenticated) { - while (!this._accessToken) { - // wait for async response hook result - await common.sleep(300) - timeout = timeout - 300 - if (timeout < 0) { - const error = new Error('Not signed in') - error.statusCode = 401 - throw error - } - } - } else if (!this._accessToken) { - const error = new Error('Not signed in') - error.statusCode = 401 - throw error - } - } - - const url = 'https://chat.openai.com/backend-api/conversation' - const body = { - action, - messages: [ - { - id: messageId, - role: 'user', - content: { - content_type: 'text', - parts: [message] - } - } - ], - model: Config.plus ? Config.useGPT4 ? 'gpt-4' : 'text-davinci-002-render-sha' : 'text-davinci-002-render-sha', - parent_message_id: parentMessageId - } - - if (conversationId) { - body.conversation_id = conversationId - } - - // console.log('>>> EVALUATE', url, this._accessToken, body) - const result = await this._page.evaluate( - browserPostEventStream, - url, - this._accessToken, - body, - timeoutMs - ) - // console.log('<<< EVALUATE', result) - - if (result.error) { - const error = new Error(result.error.message) - error.statusCode = result.error.statusCode - error.statusText = result.error.statusText - - if (error.statusCode === 403) { - await this.handle403Error() - } - - throw error - } - - // TODO: support sending partial response events - if (onConversationResponse) { - onConversationResponse(result.conversationResponse) - } - - return { - text: result.response, - conversationId: result.conversationResponse.conversation_id, - id: messageId, - parentMessageId - } - - // const lastMessage = await this.getLastMessage() - - // await inputBox.focus() - // const paragraphs = message.split('\n') - // for (let i = 0; i < paragraphs.length; i++) { - // await inputBox.type(paragraphs[i], { delay: 0 }) - // if (i < paragraphs.length - 1) { - // await this._page.keyboard.down('Shift') - // await inputBox.press('Enter') - // await this._page.keyboard.up('Shift') - // } else { - // await inputBox.press('Enter') - // } - // } - - // const responseP = new Promise(async (resolve, reject) => { - // try { - // do { - // await common.sleep(1000) - - // // TODO: this logic needs some work because we can have repeat messages... - // const newLastMessage = await this.getLastMessage() - // if ( - // newLastMessage && - // lastMessage?.toLowerCase() !== newLastMessage?.toLowerCase() - // ) { - // return resolve(newLastMessage) - // } - // } while (true) - // } catch (err) { - // return reject(err) - // } - // }) - - // if (timeoutMs) { - // return pTimeout(responseP, { - // milliseconds: timeoutMs - // }) - // } else { - // return responseP - // } - } - - async resetThread () { - try { - await this._page.click('nav > a:nth-child(1)') - } catch (err) { - // ignore for now - } - } - async close () { if (this.browser) { await this.browser.close() @@ -533,510 +115,6 @@ export class ChatGPTPuppeteer extends Puppeteer { this._page = null this.browser = null } - - protected - - async _getInputBox () { - // [data-id="root"] - return this._page?.$('textarea') - } } export default new ChatGPTPuppeteer() - -export async function minimizePage (page) { - const session = await page.target().createCDPSession() - const goods = await session.send('Browser.getWindowForTarget') - const { windowId } = goods - await session.send('Browser.setWindowBounds', { - windowId, - bounds: { windowState: 'minimized' } - }) -} - -export async function maximizePage (page) { - const session = await page.target().createCDPSession() - const goods = await session.send('Browser.getWindowForTarget') - const { windowId } = goods - await session.send('Browser.setWindowBounds', { - windowId, - bounds: { windowState: 'normal' } - }) -} - -export function isRelevantRequest (url) { - let pathname - - try { - const parsedUrl = new URL(url) - pathname = parsedUrl.pathname - url = parsedUrl.toString() - } catch (_) { - return false - } - - if (!url.startsWith('https://chat.openai.com')) { - return false - } - - if ( - !pathname.startsWith('/backend-api/') && - !pathname.startsWith('/api/auth/session') - ) { - return false - } - - if (pathname.endsWith('backend-api/moderations')) { - return false - } - - return true -} - -/** - * This function is injected into the ChatGPT webapp page using puppeteer. It - * has to be fully self-contained, so we copied a few third-party sources and - * included them in here. - */ -export async function browserPostEventStream ( - url, - accessToken, - body, - timeoutMs -) { - // Workaround for https://github.com/esbuild-kit/tsx/issues/113 - globalThis.__name = () => undefined - - const BOM = [239, 187, 191] - - let conversationResponse - let conversationId = body?.conversation_id - let messageId = body?.messages?.[0]?.id - let response = '' - - try { - console.log('browserPostEventStream', url, accessToken, body) - - let abortController = null - if (timeoutMs) { - abortController = new AbortController() - } - - const res = await fetch(url, { - method: 'POST', - body: JSON.stringify(body), - signal: abortController?.signal, - headers: { - accept: 'text/event-stream', - 'x-openai-assistant-app-id': '', - authorization: `Bearer ${accessToken}`, - 'content-type': 'application/json' - } - }) - - console.log('browserPostEventStream response', res) - - if (!res.ok) { - return { - error: { - message: `ChatGPTAPI error ${res.status || res.statusText}`, - statusCode: res.status, - statusText: res.statusText - }, - response: null, - conversationId, - messageId - } - } - - const responseP = new Promise( - async (resolve, reject) => { - function onMessage (data) { - if (data === '[DONE]') { - return resolve({ - error: null, - response, - conversationId, - messageId, - conversationResponse - }) - } - try { - const _checkJson = JSON.parse(data) - } catch (error) { - console.log('warning: parse error.') - return - } - try { - const convoResponseEvent = - JSON.parse(data) - conversationResponse = convoResponseEvent - if (convoResponseEvent.conversation_id) { - conversationId = convoResponseEvent.conversation_id - } - - if (convoResponseEvent.message?.id) { - messageId = convoResponseEvent.message.id - } - - const partialResponse = - convoResponseEvent.message?.content?.parts?.[0] - if (partialResponse) { - response = partialResponse - } - } catch (err) { - console.warn('fetchSSE onMessage unexpected error', err) - reject(err) - } - } - - const parser = createParser((event) => { - if (event.type === 'event') { - onMessage(event.data) - } - }) - - for await (const chunk of streamAsyncIterable(res.body)) { - const str = new TextDecoder().decode(chunk) - parser.feed(str) - } - } - ) - - if (timeoutMs) { - if (abortController) { - // This will be called when a timeout occurs in order for us to forcibly - // ensure that the underlying HTTP request is aborted. - responseP.cancel = () => { - abortController.abort() - } - } - console.log({ pTimeout }) - return await pTimeout(responseP, { - milliseconds: timeoutMs, - message: 'ChatGPT timed out waiting for response' - }) - } else { - return await responseP - } - } catch (err) { - const errMessageL = err.toString().toLowerCase() - - if ( - response && - (errMessageL === 'error: typeerror: terminated' || - errMessageL === 'typeerror: terminated') - ) { - // OpenAI sometimes forcefully terminates the socket from their end before - // the HTTP request has resolved cleanly. In my testing, these cases tend to - // happen when OpenAI has already send the last `response`, so we can ignore - // the `fetch` error in this case. - return { - error: null, - response, - conversationId, - messageId, - conversationResponse - } - } - - return { - error: { - message: err.toString(), - statusCode: err.statusCode || err.status || err.response?.statusCode, - statusText: err.statusText || err.response?.statusText - }, - response: null, - conversationId, - messageId, - conversationResponse - } - } - // async function pTimeout (promise, option) { - // return await pTimeout(promise, option) - // } - async function * streamAsyncIterable (stream) { - const reader = stream.getReader() - try { - while (true) { - const { done, value } = await reader.read() - if (done) { - return - } - yield value - } - } finally { - reader.releaseLock() - } - } - - // @see https://github.com/rexxars/eventsource-parser - function createParser (onParse) { - // Processing state - let isFirstChunk - let buffer - let startingPosition - let startingFieldLength - - // Event state - let eventId - let eventName - let data - - reset() - return { feed, reset } - - function reset () { - isFirstChunk = true - buffer = '' - startingPosition = 0 - startingFieldLength = -1 - - eventId = undefined - eventName = undefined - data = '' - } - - function feed (chunk) { - buffer = buffer ? buffer + chunk : chunk - - // Strip any UTF8 byte order mark (BOM) at the start of the stream. - // Note that we do not strip any non - UTF8 BOM, as eventsource streams are - // always decoded as UTF8 as per the specification. - if (isFirstChunk && hasBom(buffer)) { - buffer = buffer.slice(BOM.length) - } - - isFirstChunk = false - - // Set up chunk-specific processing state - const length = buffer.length - let position = 0 - let discardTrailingNewline = false - - // Read the current buffer byte by byte - while (position < length) { - // EventSource allows for carriage return + line feed, which means we - // need to ignore a linefeed character if the previous character was a - // carriage return - // @todo refactor to reduce nesting, consider checking previous byte? - // @todo but consider multiple chunks etc - if (discardTrailingNewline) { - if (buffer[position] === '\n') { - ++position - } - discardTrailingNewline = false - } - - let lineLength = -1 - let fieldLength = startingFieldLength - let character - - for ( - let index = startingPosition; - lineLength < 0 && index < length; - ++index - ) { - character = buffer[index] - if (character === ':' && fieldLength < 0) { - fieldLength = index - position - } else if (character === '\r') { - discardTrailingNewline = true - lineLength = index - position - } else if (character === '\n') { - lineLength = index - position - } - } - - if (lineLength < 0) { - startingPosition = length - position - startingFieldLength = fieldLength - break - } else { - startingPosition = 0 - startingFieldLength = -1 - } - - parseEventStreamLine(buffer, position, fieldLength, lineLength) - - position += lineLength + 1 - } - - if (position === length) { - // If we consumed the entire buffer to read the event, reset the buffer - buffer = '' - } else if (position > 0) { - // If there are bytes left to process, set the buffer to the unprocessed - // portion of the buffer only - buffer = buffer.slice(position) - } - } - - function parseEventStreamLine ( - lineBuffer, - index, - fieldLength, - lineLength - ) { - if (lineLength === 0) { - // We reached the last line of this event - if (data.length > 0) { - onParse({ - type: 'event', - id: eventId, - event: eventName || undefined, - data: data.slice(0, -1) // remove trailing newline - }) - - data = '' - eventId = undefined - } - eventName = undefined - return - } - - const noValue = fieldLength < 0 - const field = lineBuffer.slice( - index, - index + (noValue ? lineLength : fieldLength) - ) - let step = 0 - - if (noValue) { - step = lineLength - } else if (lineBuffer[index + fieldLength + 1] === ' ') { - step = fieldLength + 2 - } else { - step = fieldLength + 1 - } - - const position = index + step - const valueLength = lineLength - step - const value = lineBuffer - .slice(position, position + valueLength) - .toString() - - if (field === 'data') { - data += value ? `${value}\n` : '\n' - } else if (field === 'event') { - eventName = value - } else if (field === 'id' && !value.includes('\u0000')) { - eventId = value - } else if (field === 'retry') { - const retry = parseInt(value, 10) - if (!Number.isNaN(retry)) { - onParse({ type: 'reconnect-interval', value: retry }) - } - } - } - } - - function hasBom (buffer) { - return BOM.every( - (charCode, index) => buffer.charCodeAt(index) === charCode - ) - } - - // @see https://github.com/sindresorhus/p-timeout - function pTimeout ( - promise, - options - ) { - const { - milliseconds, - fallback, - message, - customTimers = { setTimeout, clearTimeout } - } = options - - let timer - - const cancelablePromise = new Promise((resolve, reject) => { - if (typeof milliseconds !== 'number' || Math.sign(milliseconds) !== 1) { - throw new TypeError( - `Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\`` - ) - } - - if (milliseconds === Number.POSITIVE_INFINITY) { - resolve(promise) - return - } - - if (options.signal) { - const { signal } = options - if (signal.aborted) { - reject(getAbortedReason(signal)) - } - - signal.addEventListener('abort', () => { - reject(getAbortedReason(signal)) - }) - } - - timer = customTimers.setTimeout.call( - undefined, - () => { - if (fallback) { - try { - resolve(fallback()) - } catch (error) { - reject(error) - } - - return - } - - const errorMessage = - typeof message === 'string' - ? message - : `Promise timed out after ${milliseconds} milliseconds` - const timeoutError = - message instanceof Error ? message : new Error(errorMessage) - - if (typeof promise.cancel === 'function') { - promise.cancel() - } - - reject(timeoutError) - }, - milliseconds - ) - ;(async () => { - try { - resolve(await promise) - } catch (error) { - reject(error) - } finally { - customTimers.clearTimeout.call(undefined, timer) - } - })() - }) - - cancelablePromise.clear = () => { - customTimers.clearTimeout.call(undefined, timer) - timer = undefined - } - - return cancelablePromise - } - /** - TODO: Remove below function and just 'reject(signal.reason)' when targeting Node 18. - */ - function getAbortedReason (signal) { - const reason = - signal.reason === undefined - ? getDOMException('This operation was aborted.') - : signal.reason - - return reason instanceof Error ? reason : getDOMException(reason) - } - /** - TODO: Remove AbortError and just throw DOMException when targeting Node 18. - */ - function getDOMException (errorMessage) { - return globalThis.DOMException === undefined - ? new Error(errorMessage) - : new DOMException(errorMessage) - } -} diff --git a/utils/chat.js b/utils/chat.js index 10bcd09..94ad34f 100644 --- a/utils/chat.js +++ b/utils/chat.js @@ -1,3 +1,5 @@ +import { Config } from './config.js' +import { newFetch } from './proxy.js' export async function getChatHistoryGroup (e, num) { // if (e.adapter === 'shamrock') { @@ -58,3 +60,43 @@ async function pickMemberAsync (e, userId) { }) }) } + +export async function generateSuggestedResponse (conversations) { + let prompt = 'Attention! you do not need to answer any question according to the provided conversation! \nYou are a suggested questions generator, you should generate three suggested questions according to the provided conversation for the user in the next turn, the three questions should not be too long, and must be superated with newline. The suggested questions should be suitable in the context of the provided conversation, and should not be too long. \nNow give your 3 suggested questions, use the same language with the user.' + const res = await newFetch(`${Config.openAiBaseUrl}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${Config.apiKey}` + }, + body: JSON.stringify({ + model: 'gpt-3.5-turbo-16k', + temperature: 0.7, + messages: [ + { + role: 'system', + content: 'you are a suggested questions generator, you should generate three suggested questions according to the provided conversation for the user in the next turn, the three questions should not be too long, and must be superated with newline. Always use the same language with the user\'s content in the last turn. you should response like: \nWhat is ChatGPT?\nCan you write a poem aboud spring?\nWhat can you do?' + }, + { + role: 'user', + content: 'User:\n\n我想知道今天的天气\n\nAI:\n\n今天北京的天气是晴转多云,最高气温12度,最低气温2度,空气质量优。\n\n' + prompt + }, + { + role: 'assistant', + content: '这个天气适合穿什么衣物?\n今天北京的湿度怎么样?\n这个季节北京有什么适合游玩的地方?' + }, + { + role: 'user', + content: JSON.stringify(conversations) + prompt + } + ] + }) + }) + if (res.status === 200) { + const resJson = await res.json() + if (resJson) { return resJson.choices[0].message.content } + } else { + logger.error('generateSuggestedResponse error: ' + res.status) + return null + } +} diff --git a/utils/common.js b/utils/common.js index 808be0a..55f26e3 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() @@ -102,63 +87,7 @@ export async function tryTimes (promiseFn, maxTries = 10) { } export async function makeForwardMsg (e, msg = [], dec = '') { - if (Version.isTrss) { - return common.makeForwardMsg(e, msg, dec) - } - let nickname = e.bot.nickname - if (e.isGroup) { - try { - let info = await e.bot.getGroupMemberInfo(e.group_id, getUin(e)) - nickname = info.card || info.nickname - } catch (err) { - console.error(`Failed to get group member info: ${err}`) - } - } - let userInfo = { - user_id: getUin(e), - nickname - } - - let forwardMsg = [] - msg.forEach((v) => { - forwardMsg.push({ - ...userInfo, - message: v - }) - }) - let is_sign = true - /** 制作转发内容 */ - if (e.isGroup) { - forwardMsg = await e.group.makeForwardMsg(forwardMsg) - } else if (e.friend) { - forwardMsg = await e.friend.makeForwardMsg(forwardMsg) - } else { - return msg.join('\n') - } - let forwardMsg_json = forwardMsg.data - if (typeof (forwardMsg_json) === 'object') { - if (forwardMsg_json.app === 'com.tencent.multimsg' && forwardMsg_json.meta?.detail) { - let detail = forwardMsg_json.meta.detail - let resid = detail.resid - let fileName = detail.uniseq - let preview = '' - for (let val of detail.news) { - preview += `${val.text}` - } - forwardMsg.data = `转发的聊天记录${preview}
${detail.summary}
` - forwardMsg.type = 'xml' - forwardMsg.id = 35 - } - } - forwardMsg.data = forwardMsg.data - .replace(/\n/g, '') - .replace(/(.+?)<\/title>/g, '___') - .replace(/___+/, `<title color="#777777" size="26">${dec}`) - if (!is_sign) { - forwardMsg.data = forwardMsg.data - .replace('转发的', '不可转发的') - } - return forwardMsg + return common.makeForwardMsg(e, msg, dec) } // @see https://github.com/sindresorhus/p-timeout diff --git a/utils/config.js b/utils/config.js index 71e00a4..40a662d 100644 --- a/utils/config.js +++ b/utils/config.js @@ -44,6 +44,12 @@ const defaultConfig = { sydneyGPTs: 'Copilot', sydneyImageRecognition: false, sydneyMoodTip: 'Your response should be divided into two parts, namely, the text and your mood. The mood available to you can only include: blandness, happy, shy, frustrated, disgusted, and frightened.All content should be replied in this format {"text": "", "mood": ""}.All content except mood should be placed in text, It is important to ensure that the content you reply to can be parsed by json.', + chatExampleUser1: '', + chatExampleUser2: '', + chatExampleUser3: '', + chatExampleBot1: '', + chatExampleBot2: '', + chatExampleBot3: '', enableSuggestedResponses: false, sydneyEnableSearch: false, api: defaultChatGPTAPI, @@ -66,13 +72,8 @@ const defaultConfig = { xhRetReplace: '', promptPrefixOverride: 'Your answer shouldn\'t be too verbose. Prefer to answer in Chinese.', assistantLabel: 'ChatGPT', - // thinkingTips: true, - username: '', - password: '', - UA: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36', headless: false, chromePath: '', - '2captchaToken': '', proxy: '', debug: true, defaultTimeoutMs: 120000, @@ -179,9 +180,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: true, // 默认关闭工具箱节省占用和加速启动 + version: 'v2.8.1' } 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/jwt.js b/utils/jwt.js index 1af66f3..b1a1d77 100644 --- a/utils/jwt.js +++ b/utils/jwt.js @@ -1,7 +1,7 @@ export function decrypt (jwtToken) { const [encodedHeader, encodedPayload, signature] = jwtToken.split('.') - const decodedHeader = Buffer.from(encodedHeader, 'base64').toString('utf-8') + // const decodedHeader = Buffer.from(encodedHeader, 'base64').toString('utf-8') const decodedPayload = Buffer.from(encodedPayload, 'base64').toString('utf-8') return decodedPayload diff --git a/utils/openai-auth.js b/utils/openai-auth.js deleted file mode 100644 index a10709d..0000000 --- a/utils/openai-auth.js +++ /dev/null @@ -1,281 +0,0 @@ -import { Config } from '../utils/config.js' -import random from 'random' -import common from '../../../lib/common/common.js' - -let hasRecaptchaPlugin = !!Config['2captchaToken'] - -export async function getOpenAIAuth (opt) { - let { - email, - password, - browser, - page, - timeoutMs = Config.chromeTimeoutMS, - isGoogleLogin = false, - captchaToken = Config['2captchaToken'], - executablePath = Config.chromePath - } = opt - const origBrowser = browser - const origPage = page - - try { - const userAgent = await browser.userAgent() - if (!page) { - page = (await browser.pages())[0] || (await browser.newPage()) - page.setDefaultTimeout(timeoutMs) - } - await page.goto('https://chat.openai.com/auth/login', { - waitUntil: 'networkidle2' - }) - logger.mark('chatgpt checkForChatGPTAtCapacity') - - await checkForChatGPTAtCapacity(page) - - // NOTE: this is where you may encounter a CAPTCHA - if (hasRecaptchaPlugin) { - logger.mark('RecaptchaPlugin key exists, try to solve recaptchas') - await page.solveRecaptchas() - } - - logger.mark('chatgpt checkForChatGPTAtCapacity again') - await checkForChatGPTAtCapacity(page) - - // once we get to this point, the Cloudflare cookies should be available - - // login as well (optional) - if (email && password) { - let retry = 3 - while (retry > 0) { - try { - await waitForConditionOrAtCapacity(page, () => - page.waitForSelector('#__next .btn-primary', { timeout: timeoutMs / 3 }) - ) - } catch (e) { - await checkForChatGPTAtCapacity(page) - } - retry-- - } - await waitForConditionOrAtCapacity(page, () => - page.waitForSelector('#__next .btn-primary', { timeout: timeoutMs / 3 }) - ) - await common.sleep(500) - - // click login button and wait for navigation to finish - do { - await Promise.all([ - page.waitForNavigation({ - waitUntil: 'networkidle2', - timeout: timeoutMs - }), - page.click('#__next .btn-primary') - ]) - await common.sleep(1000) - } while (page.url().endsWith('/auth/login')) - logger.mark('进入登录页面') - await checkForChatGPTAtCapacity(page) - - let submitP - - if (isGoogleLogin) { - await page.click('button[data-provider="google"]') - await page.waitForSelector('input[type="email"]') - await page.type('input[type="email"]', email, { delay: 10 }) - await Promise.all([ - page.waitForNavigation(), - await page.keyboard.press('Enter') - ]) - await page.waitForSelector('input[type="password"]', { visible: true }) - await page.type('input[type="password"]', password, { delay: 10 }) - submitP = () => page.keyboard.press('Enter') - } else { - await page.waitForSelector('#username') - await page.type('#username', email, { delay: 20 }) - await common.sleep(100) - - if (hasRecaptchaPlugin) { - // console.log('solveRecaptchas()') - const res = await page.solveRecaptchas() - // console.log('solveRecaptchas result', res) - } - - await page.click('button[type="submit"]') - await page.waitForSelector('#password', { timeout: timeoutMs }) - await page.type('#password', password, { delay: 10 }) - submitP = () => page.click('button[type="submit"]') - } - - await Promise.all([ - waitForConditionOrAtCapacity(page, () => - page.waitForNavigation({ - waitUntil: 'networkidle2', - timeout: timeoutMs - }) - ), - submitP() - ]) - } else { - await common.sleep(2000) - await checkForChatGPTAtCapacity(page) - } - - const pageCookies = await page.cookies() - await redis.set('CHATGPT:RAW_COOKIES', JSON.stringify(pageCookies)) - const cookies = pageCookies.reduce( - (map, cookie) => ({ ...map, [cookie.name]: cookie }), - {} - ) - - const authInfo = { - userAgent, - clearanceToken: cookies.cf_clearance?.value, - sessionToken: cookies['__Secure-next-auth.session-token']?.value, - cookies - } - logger.info('chatgpt登录成功') - - return authInfo - } catch (err) { - throw err - } finally { - await page.screenshot({ - type: 'png', - path: './error.png' - }) - if (origBrowser) { - if (page && page !== origPage) { - await page.close() - } - } else if (browser) { - await browser.close() - } - - page = null - browser = null - } -} - -async function checkForChatGPTAtCapacity (page, opts = {}) { - const { - timeoutMs = Config.chromeTimeoutMS, // 2 minutes - pollingIntervalMs = 3000, - retries = 10 - } = opts - - // console.log('checkForChatGPTAtCapacity', page.url()) - let isAtCapacity = false - let numTries = 0 - - do { - try { - await solveSimpleCaptchas(page) - - const res = await page.$x("//div[contains(., 'ChatGPT is at capacity')]") - isAtCapacity = !!res?.length - - if (isAtCapacity) { - if (++numTries >= retries) { - break - } - - // try refreshing the page if chatgpt is at capacity - await page.reload({ - waitUntil: 'networkidle2', - timeout: timeoutMs - }) - - await common.sleep(pollingIntervalMs) - } - } catch (err) { - // ignore errors likely due to navigation - ++numTries - break - } - } while (isAtCapacity) - - if (isAtCapacity) { - const error = new Error('ChatGPT is at capacity') - error.statusCode = 503 - throw error - } -} - -async function waitForConditionOrAtCapacity ( - page, - condition, - opts = {} -) { - const { pollingIntervalMs = 500 } = opts - - return new Promise((resolve, reject) => { - let resolved = false - - async function waitForCapacityText () { - if (resolved) { - return - } - - try { - await checkForChatGPTAtCapacity(page) - - if (!resolved) { - setTimeout(waitForCapacityText, pollingIntervalMs) - } - } catch (err) { - if (!resolved) { - resolved = true - return reject(err) - } - } - } - - condition() - .then(() => { - if (!resolved) { - resolved = true - resolve() - } - }) - .catch((err) => { - if (!resolved) { - resolved = true - reject(err) - } - }) - - setTimeout(waitForCapacityText, pollingIntervalMs) - }) -} - -async function solveSimpleCaptchas (page) { - try { - const verifyYouAreHuman = await page.$('text=Verify you are human') - if (verifyYouAreHuman) { - logger.mark('encounter cloudflare simple captcha "Verify you are human"') - await common.sleep(2000) - await verifyYouAreHuman.click({ - delay: random.int(5, 25) - }) - await common.sleep(1000) - } - const verifyYouAreHumanCN = await page.$('text=确认您是真人') - if (verifyYouAreHumanCN) { - logger.mark('encounter cloudflare simple captcha "确认您是真人"') - await common.sleep(2000) - await verifyYouAreHumanCN.click({ - delay: random.int(5, 25) - }) - await common.sleep(1000) - } - - const cloudflareButton = await page.$('.hcaptcha-box') - if (cloudflareButton) { - await common.sleep(2000) - await cloudflareButton.click({ - delay: random.int(5, 25) - }) - await common.sleep(1000) - } - } catch (err) { - // ignore errors - } -} diff --git a/utils/openai/chatgpt-api.js b/utils/openai/chatgpt-api.js index a57fe4a..92f45da 100644 --- a/utils/openai/chatgpt-api.js +++ b/utils/openai/chatgpt-api.js @@ -173,7 +173,8 @@ var ChatGPTAPI = /** @class */ (function () { conversationId: conversationId, parentMessageId: messageId, text: '', - functionCall: null + functionCall: undefined, + conversation: [] }; responseP = new Promise(function (resolve, reject) { return __awaiter(_this, void 0, void 0, function () { var url, headers, body, res, reason, msg, error, response, message_1, res_1, err_1; @@ -208,6 +209,7 @@ var ChatGPTAPI = /** @class */ (function () { var _a; if (data === '[DONE]') { result.text = result.text.trim(); + result.conversation = messages; return resolve(result); } try { @@ -293,6 +295,7 @@ var ChatGPTAPI = /** @class */ (function () { return [2 /*return*/, reject(new Error("OpenAI error: ".concat(((_b = res_1 === null || res_1 === void 0 ? void 0 : res_1.detail) === null || _b === void 0 ? void 0 : _b.message) || (res_1 === null || res_1 === void 0 ? void 0 : res_1.detail) || 'unknown')))]; } result.detail = response; + result.conversation = messages; return [2 /*return*/, resolve(result)]; case 6: err_1 = _c.sent(); diff --git a/utils/openai/chatgpt-api.ts b/utils/openai/chatgpt-api.ts index ac3bd96..4277199 100644 --- a/utils/openai/chatgpt-api.ts +++ b/utils/openai/chatgpt-api.ts @@ -7,7 +7,7 @@ import * as tokenizer from './tokenizer' import * as types from './types' import globalFetch from 'node-fetch' import { fetchSSE } from './fetch-sse' -import {openai, Role} from "./types"; +import {ChatCompletionRequestMessage, openai, Role} from "./types"; const CHATGPT_MODEL = 'gpt-3.5-turbo-0613' @@ -176,16 +176,17 @@ export class ChatGPTAPI { completionParams ) console.log(`maxTokens: ${maxTokens}, numTokens: ${numTokens}`) - const result: types.ChatMessage = { + const result: types.ChatMessage & { conversation: openai.ChatCompletionRequestMessage[] } = { role: 'assistant', id: uuidv4(), conversationId, parentMessageId: messageId, - text: undefined, - functionCall: undefined + text: '', + functionCall: undefined, + conversation: [] } - const responseP = new Promise( + const responseP = new Promise( async (resolve, reject) => { const url = `${this._apiBaseUrl}/chat/completions` const headers = { @@ -223,6 +224,7 @@ export class ChatGPTAPI { onMessage: (data: string) => { if (data === '[DONE]') { result.text = result.text.trim() + result.conversation = messages return resolve(result) } @@ -318,7 +320,7 @@ export class ChatGPTAPI { } result.detail = response - + result.conversation = messages return resolve(result) } catch (err) { return reject(err) @@ -548,4 +550,4 @@ export class ChatGPTAPI { ): Promise { await this._messageStore.set(message.id, message) } -} \ No newline at end of file +} diff --git a/utils/poe/credential.js b/utils/poe/credential.js deleted file mode 100644 index b16a86d..0000000 --- a/utils/poe/credential.js +++ /dev/null @@ -1,48 +0,0 @@ -import fetch from 'node-fetch' -import { readFileSync, writeFile } from 'fs' - -const scrape = async (pbCookie, proxy) => { - let option = { headers: { cookie: `${pbCookie}` } } - if (proxy) { - option.agent = proxy - } - const _setting = await fetch( - 'https://poe.com/api/settings', - option - ) - if (_setting.status !== 200) throw new Error('Failed to fetch token') - const appSettings = await _setting.json() - console.log(appSettings) - const { tchannelData: { channel: channelName } } = appSettings - return { - channelName, - appSettings, - formKey: appSettings.formKey - } -} - -const getUpdatedSettings = async (channelName, pbCookie, proxy) => { - let option = { headers: { cookie: `${pbCookie}` } } - if (proxy) { - option.agent = proxy - } - const _setting = await fetch( - `https://poe.com/api/settings?channel=${channelName}`, - option - ) - if (_setting.status !== 200) throw new Error('Failed to fetch token') - const appSettings = await _setting.json() - const { tchannelData: { minSeq } } = appSettings - const credentials = JSON.parse(readFileSync('config.json', 'utf8')) - credentials.app_settings.tchannelData.minSeq = minSeq - writeFile('config.json', JSON.stringify(credentials, null, 4), function (err) { - if (err) { - console.log(err) - } - }) - return { - minSeq - } -} - -export { scrape, getUpdatedSettings } diff --git a/utils/poe/graphql/AddHumanMessageMutation.graphql b/utils/poe/graphql/AddHumanMessageMutation.graphql deleted file mode 100644 index 01e6bc8..0000000 --- a/utils/poe/graphql/AddHumanMessageMutation.graphql +++ /dev/null @@ -1,52 +0,0 @@ -mutation AddHumanMessageMutation( - $chatId: BigInt! - $bot: String! - $query: String! - $source: MessageSource - $withChatBreak: Boolean! = false -) { - messageCreateWithStatus( - chatId: $chatId - bot: $bot - query: $query - source: $source - withChatBreak: $withChatBreak - ) { - message { - id - __typename - messageId - text - linkifiedText - authorNickname - state - vote - voteReason - creationTime - suggestedReplies - chat { - id - shouldShowDisclaimer - } - } - messageLimit{ - canSend - numMessagesRemaining - resetTime - shouldShowReminder - } - chatBreak { - id - __typename - messageId - text - linkifiedText - authorNickname - state - vote - voteReason - creationTime - suggestedReplies - } - } -} diff --git a/utils/poe/graphql/AddMessageBreakMutation.graphql b/utils/poe/graphql/AddMessageBreakMutation.graphql deleted file mode 100644 index b28d990..0000000 --- a/utils/poe/graphql/AddMessageBreakMutation.graphql +++ /dev/null @@ -1,17 +0,0 @@ -mutation AddMessageBreakMutation($chatId: BigInt!) { - messageBreakCreate(chatId: $chatId) { - message { - id - __typename - messageId - text - linkifiedText - authorNickname - state - vote - voteReason - creationTime - suggestedReplies - } - } -} diff --git a/utils/poe/graphql/AutoSubscriptionMutation.graphql b/utils/poe/graphql/AutoSubscriptionMutation.graphql deleted file mode 100644 index 6cf7bf7..0000000 --- a/utils/poe/graphql/AutoSubscriptionMutation.graphql +++ /dev/null @@ -1,7 +0,0 @@ -mutation AutoSubscriptionMutation($subscriptions: [AutoSubscriptionQuery!]!) { - autoSubscribe(subscriptions: $subscriptions) { - viewer { - id - } - } -} diff --git a/utils/poe/graphql/BioFragment.graphql b/utils/poe/graphql/BioFragment.graphql deleted file mode 100644 index c421803..0000000 --- a/utils/poe/graphql/BioFragment.graphql +++ /dev/null @@ -1,8 +0,0 @@ -fragment BioFragment on Viewer { - id - poeUser { - id - uid - bio - } -} diff --git a/utils/poe/graphql/ChatAddedSubscription.graphql b/utils/poe/graphql/ChatAddedSubscription.graphql deleted file mode 100644 index 664b107..0000000 --- a/utils/poe/graphql/ChatAddedSubscription.graphql +++ /dev/null @@ -1,5 +0,0 @@ -subscription ChatAddedSubscription { - chatAdded { - ...ChatFragment - } -} diff --git a/utils/poe/graphql/ChatFragment.graphql b/utils/poe/graphql/ChatFragment.graphql deleted file mode 100644 index 605645f..0000000 --- a/utils/poe/graphql/ChatFragment.graphql +++ /dev/null @@ -1,6 +0,0 @@ -fragment ChatFragment on Chat { - id - chatId - defaultBotNickname - shouldShowDisclaimer -} diff --git a/utils/poe/graphql/ChatPaginationQuery.graphql b/utils/poe/graphql/ChatPaginationQuery.graphql deleted file mode 100644 index f2452cd..0000000 --- a/utils/poe/graphql/ChatPaginationQuery.graphql +++ /dev/null @@ -1,26 +0,0 @@ -query ChatPaginationQuery($bot: String!, $before: String, $last: Int! = 10) { - chatOfBot(bot: $bot) { - id - __typename - messagesConnection(before: $before, last: $last) { - pageInfo { - hasPreviousPage - } - edges { - node { - id - __typename - messageId - text - linkifiedText - authorNickname - state - vote - voteReason - creationTime - suggestedReplies - } - } - } - } -} diff --git a/utils/poe/graphql/ChatViewQuery.graphql b/utils/poe/graphql/ChatViewQuery.graphql deleted file mode 100644 index c330107..0000000 --- a/utils/poe/graphql/ChatViewQuery.graphql +++ /dev/null @@ -1,8 +0,0 @@ -query ChatViewQuery($bot: String!) { - chatOfBot(bot: $bot) { - id - chatId - defaultBotNickname - shouldShowDisclaimer - } -} diff --git a/utils/poe/graphql/DeleteHumanMessagesMutation.graphql b/utils/poe/graphql/DeleteHumanMessagesMutation.graphql deleted file mode 100644 index 42692c6..0000000 --- a/utils/poe/graphql/DeleteHumanMessagesMutation.graphql +++ /dev/null @@ -1,7 +0,0 @@ -mutation DeleteHumanMessagesMutation($messageIds: [BigInt!]!) { - messagesDelete(messageIds: $messageIds) { - viewer { - id - } - } -} diff --git a/utils/poe/graphql/HandleFragment.graphql b/utils/poe/graphql/HandleFragment.graphql deleted file mode 100644 index f53c484..0000000 --- a/utils/poe/graphql/HandleFragment.graphql +++ /dev/null @@ -1,8 +0,0 @@ -fragment HandleFragment on Viewer { - id - poeUser { - id - uid - handle - } -} diff --git a/utils/poe/graphql/LoginWithVerificationCodeMutation.graphql b/utils/poe/graphql/LoginWithVerificationCodeMutation.graphql deleted file mode 100644 index 723b1f4..0000000 --- a/utils/poe/graphql/LoginWithVerificationCodeMutation.graphql +++ /dev/null @@ -1,13 +0,0 @@ -mutation LoginWithVerificationCodeMutation( - $verificationCode: String! - $emailAddress: String - $phoneNumber: String -) { - loginWithVerificationCode( - verificationCode: $verificationCode - emailAddress: $emailAddress - phoneNumber: $phoneNumber - ) { - status - } -} diff --git a/utils/poe/graphql/MessageAddedSubscription.graphql b/utils/poe/graphql/MessageAddedSubscription.graphql deleted file mode 100644 index 0492baa..0000000 --- a/utils/poe/graphql/MessageAddedSubscription.graphql +++ /dev/null @@ -1,5 +0,0 @@ -subscription MessageAddedSubscription($chatId: BigInt!) { - messageAdded(chatId: $chatId) { - ...MessageFragment - } -} diff --git a/utils/poe/graphql/MessageDeletedSubscription.graphql b/utils/poe/graphql/MessageDeletedSubscription.graphql deleted file mode 100644 index 54c1c16..0000000 --- a/utils/poe/graphql/MessageDeletedSubscription.graphql +++ /dev/null @@ -1,6 +0,0 @@ -subscription MessageDeletedSubscription($chatId: BigInt!) { - messageDeleted(chatId: $chatId) { - id - messageId - } -} diff --git a/utils/poe/graphql/MessageFragment.graphql b/utils/poe/graphql/MessageFragment.graphql deleted file mode 100644 index cc86081..0000000 --- a/utils/poe/graphql/MessageFragment.graphql +++ /dev/null @@ -1,13 +0,0 @@ -fragment MessageFragment on Message { - id - __typename - messageId - text - linkifiedText - authorNickname - state - vote - voteReason - creationTime - suggestedReplies -} diff --git a/utils/poe/graphql/MessageRemoveVoteMutation.graphql b/utils/poe/graphql/MessageRemoveVoteMutation.graphql deleted file mode 100644 index d5e6e61..0000000 --- a/utils/poe/graphql/MessageRemoveVoteMutation.graphql +++ /dev/null @@ -1,7 +0,0 @@ -mutation MessageRemoveVoteMutation($messageId: BigInt!) { - messageRemoveVote(messageId: $messageId) { - message { - ...MessageFragment - } - } -} diff --git a/utils/poe/graphql/MessageSetVoteMutation.graphql b/utils/poe/graphql/MessageSetVoteMutation.graphql deleted file mode 100644 index 76000df..0000000 --- a/utils/poe/graphql/MessageSetVoteMutation.graphql +++ /dev/null @@ -1,7 +0,0 @@ -mutation MessageSetVoteMutation($messageId: BigInt!, $voteType: VoteType!, $reason: String) { - messageSetVote(messageId: $messageId, voteType: $voteType, reason: $reason) { - message { - ...MessageFragment - } - } -} diff --git a/utils/poe/graphql/SendVerificationCodeForLoginMutation.graphql b/utils/poe/graphql/SendVerificationCodeForLoginMutation.graphql deleted file mode 100644 index 45af479..0000000 --- a/utils/poe/graphql/SendVerificationCodeForLoginMutation.graphql +++ /dev/null @@ -1,12 +0,0 @@ -mutation SendVerificationCodeForLoginMutation( - $emailAddress: String - $phoneNumber: String -) { - sendVerificationCode( - verificationReason: login - emailAddress: $emailAddress - phoneNumber: $phoneNumber - ) { - status - } -} diff --git a/utils/poe/graphql/ShareMessagesMutation.graphql b/utils/poe/graphql/ShareMessagesMutation.graphql deleted file mode 100644 index 92e80db..0000000 --- a/utils/poe/graphql/ShareMessagesMutation.graphql +++ /dev/null @@ -1,9 +0,0 @@ -mutation ShareMessagesMutation( - $chatId: BigInt! - $messageIds: [BigInt!]! - $comment: String -) { - messagesShare(chatId: $chatId, messageIds: $messageIds, comment: $comment) { - shareCode - } -} diff --git a/utils/poe/graphql/SignupWithVerificationCodeMutation.graphql b/utils/poe/graphql/SignupWithVerificationCodeMutation.graphql deleted file mode 100644 index 06b2826..0000000 --- a/utils/poe/graphql/SignupWithVerificationCodeMutation.graphql +++ /dev/null @@ -1,13 +0,0 @@ -mutation SignupWithVerificationCodeMutation( - $verificationCode: String! - $emailAddress: String - $phoneNumber: String -) { - signupWithVerificationCode( - verificationCode: $verificationCode - emailAddress: $emailAddress - phoneNumber: $phoneNumber - ) { - status - } -} diff --git a/utils/poe/graphql/StaleChatUpdateMutation.graphql b/utils/poe/graphql/StaleChatUpdateMutation.graphql deleted file mode 100644 index de203d4..0000000 --- a/utils/poe/graphql/StaleChatUpdateMutation.graphql +++ /dev/null @@ -1,7 +0,0 @@ -mutation StaleChatUpdateMutation($chatId: BigInt!) { - staleChatUpdate(chatId: $chatId) { - message { - ...MessageFragment - } - } -} diff --git a/utils/poe/graphql/SummarizePlainPostQuery.graphql b/utils/poe/graphql/SummarizePlainPostQuery.graphql deleted file mode 100644 index afa2a84..0000000 --- a/utils/poe/graphql/SummarizePlainPostQuery.graphql +++ /dev/null @@ -1,3 +0,0 @@ -query SummarizePlainPostQuery($comment: String!) { - summarizePlainPost(comment: $comment) -} diff --git a/utils/poe/graphql/SummarizeQuotePostQuery.graphql b/utils/poe/graphql/SummarizeQuotePostQuery.graphql deleted file mode 100644 index 5147c3c..0000000 --- a/utils/poe/graphql/SummarizeQuotePostQuery.graphql +++ /dev/null @@ -1,3 +0,0 @@ -query SummarizeQuotePostQuery($comment: String, $quotedPostId: BigInt!) { - summarizeQuotePost(comment: $comment, quotedPostId: $quotedPostId) -} diff --git a/utils/poe/graphql/SummarizeSharePostQuery.graphql b/utils/poe/graphql/SummarizeSharePostQuery.graphql deleted file mode 100644 index cb4a623..0000000 --- a/utils/poe/graphql/SummarizeSharePostQuery.graphql +++ /dev/null @@ -1,3 +0,0 @@ -query SummarizeSharePostQuery($comment: String!, $chatId: BigInt!, $messageIds: [BigInt!]!) { - summarizeSharePost(comment: $comment, chatId: $chatId, messageIds: $messageIds) -} diff --git a/utils/poe/graphql/UserSnippetFragment.graphql b/utils/poe/graphql/UserSnippetFragment.graphql deleted file mode 100644 index 17fc842..0000000 --- a/utils/poe/graphql/UserSnippetFragment.graphql +++ /dev/null @@ -1,14 +0,0 @@ -fragment UserSnippetFragment on PoeUser { - id - uid - bio - handle - fullName - viewerIsFollowing - isPoeOnlyUser - profilePhotoURLTiny: profilePhotoUrl(size: tiny) - profilePhotoURLSmall: profilePhotoUrl(size: small) - profilePhotoURLMedium: profilePhotoUrl(size: medium) - profilePhotoURLLarge: profilePhotoUrl(size: large) - isFollowable -} diff --git a/utils/poe/graphql/ViewerInfoQuery.graphql b/utils/poe/graphql/ViewerInfoQuery.graphql deleted file mode 100644 index 1ecaf9e..0000000 --- a/utils/poe/graphql/ViewerInfoQuery.graphql +++ /dev/null @@ -1,21 +0,0 @@ -query ViewerInfoQuery { - viewer { - id - uid - ...ViewerStateFragment - ...BioFragment - ...HandleFragment - hasCompletedMultiplayerNux - poeUser { - id - ...UserSnippetFragment - } - messageLimit{ - canSend - numMessagesRemaining - resetTime - shouldShowReminder - } - } -} - diff --git a/utils/poe/graphql/ViewerStateFragment.graphql b/utils/poe/graphql/ViewerStateFragment.graphql deleted file mode 100644 index 3cd83e9..0000000 --- a/utils/poe/graphql/ViewerStateFragment.graphql +++ /dev/null @@ -1,30 +0,0 @@ -fragment ViewerStateFragment on Viewer { - id - __typename - iosMinSupportedVersion: integerGate(gateName: "poe_ios_min_supported_version") - iosMinEncouragedVersion: integerGate( - gateName: "poe_ios_min_encouraged_version" - ) - macosMinSupportedVersion: integerGate( - gateName: "poe_macos_min_supported_version" - ) - macosMinEncouragedVersion: integerGate( - gateName: "poe_macos_min_encouraged_version" - ) - showPoeDebugPanel: booleanGate(gateName: "poe_show_debug_panel") - enableCommunityFeed: booleanGate(gateName: "enable_poe_shares_feed") - linkifyText: booleanGate(gateName: "poe_linkify_response") - enableSuggestedReplies: booleanGate(gateName: "poe_suggested_replies") - removeInviteLimit: booleanGate(gateName: "poe_remove_invite_limit") - enableInAppPurchases: booleanGate(gateName: "poe_enable_in_app_purchases") - availableBots { - nickname - displayName - profilePicture - isDown - disclaimer - subtitle - poweredBy - } -} - diff --git a/utils/poe/graphql/ViewerStateUpdatedSubscription.graphql b/utils/poe/graphql/ViewerStateUpdatedSubscription.graphql deleted file mode 100644 index dd6d2d1..0000000 --- a/utils/poe/graphql/ViewerStateUpdatedSubscription.graphql +++ /dev/null @@ -1,5 +0,0 @@ -subscription ViewerStateUpdatedSubscription { - viewerStateUpdated { - ...ViewerStateFragment - } -} diff --git a/utils/poe/index.js b/utils/poe/index.js deleted file mode 100644 index 5a60356..0000000 --- a/utils/poe/index.js +++ /dev/null @@ -1,299 +0,0 @@ -import { readFileSync } from 'fs' -import { scrape } from './credential.js' -import fetch from 'node-fetch' -import crypto from 'crypto' -import { Config } from '../config.js' - -let proxy -if (Config.proxy) { - try { - proxy = (await import('https-proxy-agent')).default - } catch (e) { - console.warn('未安装https-proxy-agent,请在插件目录下执行pnpm add https-proxy-agent') - } -} -// used when test as a single file -// const _path = process.cwd() -const _path = process.cwd() + '/plugins/chatgpt-plugin/utils/poe' -const gqlDir = `${_path}/graphql` -const queries = { - // chatViewQuery: readFileSync(gqlDir + '/ChatViewQuery.graphql', 'utf8'), - addMessageBreakMutation: readFileSync(gqlDir + '/AddMessageBreakMutation.graphql', 'utf8'), - chatPaginationQuery: readFileSync(gqlDir + '/ChatPaginationQuery.graphql', 'utf8'), - addHumanMessageMutation: readFileSync(gqlDir + '/AddHumanMessageMutation.graphql', 'utf8'), - loginMutation: readFileSync(gqlDir + '/LoginWithVerificationCodeMutation.graphql', 'utf8'), - signUpWithVerificationCodeMutation: readFileSync(gqlDir + '/SignupWithVerificationCodeMutation.graphql', 'utf8'), - sendVerificationCodeMutation: readFileSync(gqlDir + '/SendVerificationCodeForLoginMutation.graphql', 'utf8') -} -const optionMap = [ - { title: 'Claude (Powered by Anthropic)', value: 'a2' }, - { title: 'Sage (Powered by OpenAI - logical)', value: 'capybara' }, - { title: 'Dragonfly (Powered by OpenAI - simpler)', value: 'nutria' }, - { title: 'ChatGPT (Powered by OpenAI - current)', value: 'chinchilla' }, - { title: 'Claude+', value: 'a2_2' }, - { title: 'GPT-4', value: 'beaver' } -] -export class PoeClient { - constructor (props) { - this.config = props - } - - headers = { - 'Content-Type': 'application/json', - Referrer: 'https://poe.com/', - Origin: 'https://poe.com', - 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36' - } - - chatId = 0 - bot = '' - - reConnectWs = false - - async setCredentials () { - let result = await scrape(this.config.quora_cookie, this.config.proxy ? proxy(Config.proxy) : null) - console.log(result) - this.config.quora_formkey = result.appSettings.formkey - this.config.channel_name = result.channelName - this.config.app_settings = result.appSettings - - // set value - this.headers['poe-formkey'] = this.config.quora_formkey // unused - this.headers['poe-tchannel'] = this.config.channel_name - this.headers.Cookie = this.config.quora_cookie - console.log(this.headers) - } - - async subscribe () { - const query = { - queryName: 'subscriptionsMutation', - variables: { - subscriptions: [ - { - subscriptionName: 'messageAdded', - query: 'subscription subscriptions_messageAdded_Subscription(\n $chatId: BigInt!\n) {\n messageAdded(chatId: $chatId) {\n id\n messageId\n creationTime\n state\n ...ChatMessage_message\n ...chatHelpers_isBotMessage\n }\n}\n\nfragment ChatMessageDownvotedButton_message on Message {\n ...MessageFeedbackReasonModal_message\n ...MessageFeedbackOtherModal_message\n}\n\nfragment ChatMessageDropdownMenu_message on Message {\n id\n messageId\n vote\n text\n ...chatHelpers_isBotMessage\n}\n\nfragment ChatMessageFeedbackButtons_message on Message {\n id\n messageId\n vote\n voteReason\n ...ChatMessageDownvotedButton_message\n}\n\nfragment ChatMessageOverflowButton_message on Message {\n text\n ...ChatMessageDropdownMenu_message\n ...chatHelpers_isBotMessage\n}\n\nfragment ChatMessageSuggestedReplies_SuggestedReplyButton_message on Message {\n messageId\n}\n\nfragment ChatMessageSuggestedReplies_message on Message {\n suggestedReplies\n ...ChatMessageSuggestedReplies_SuggestedReplyButton_message\n}\n\nfragment ChatMessage_message on Message {\n id\n messageId\n text\n author\n linkifiedText\n state\n ...ChatMessageSuggestedReplies_message\n ...ChatMessageFeedbackButtons_message\n ...ChatMessageOverflowButton_message\n ...chatHelpers_isHumanMessage\n ...chatHelpers_isBotMessage\n ...chatHelpers_isChatBreak\n ...chatHelpers_useTimeoutLevel\n ...MarkdownLinkInner_message\n}\n\nfragment MarkdownLinkInner_message on Message {\n messageId\n}\n\nfragment MessageFeedbackOtherModal_message on Message {\n id\n messageId\n}\n\nfragment MessageFeedbackReasonModal_message on Message {\n id\n messageId\n}\n\nfragment chatHelpers_isBotMessage on Message {\n ...chatHelpers_isHumanMessage\n ...chatHelpers_isChatBreak\n}\n\nfragment chatHelpers_isChatBreak on Message {\n author\n}\n\nfragment chatHelpers_isHumanMessage on Message {\n author\n}\n\nfragment chatHelpers_useTimeoutLevel on Message {\n id\n state\n text\n messageId\n}\n' - }, - { - subscriptionName: 'viewerStateUpdated', - query: 'subscription subscriptions_viewerStateUpdated_Subscription {\n viewerStateUpdated {\n id\n ...ChatPageBotSwitcher_viewer\n }\n}\n\nfragment BotHeader_bot on Bot {\n displayName\n ...BotImage_bot\n}\n\nfragment BotImage_bot on Bot {\n profilePicture\n displayName\n}\n\nfragment BotLink_bot on Bot {\n displayName\n}\n\nfragment ChatPageBotSwitcher_viewer on Viewer {\n availableBots {\n id\n ...BotLink_bot\n ...BotHeader_bot\n }\n}\n' - } - ] - }, - query: 'mutation subscriptionsMutation(\n $subscriptions: [AutoSubscriptionQuery!]!\n) {\n autoSubscribe(subscriptions: $subscriptions) {\n viewer {\n id\n }\n }\n}\n' - } - - await this.makeRequest(query) - } - - async makeRequest (request) { - let payload = JSON.stringify(request) - let baseString = payload + this.headers['poe-formkey'] + 'WpuLMiXEKKE98j56k' - const md5 = crypto.createHash('md5').update(baseString).digest('hex') - let option = { - method: 'POST', - headers: Object.assign(this.headers, { - 'poe-tag-id': md5, - 'content-type': 'application/json' - }), - body: payload - } - if (this.config.proxy) { - option.agent = proxy(Config.proxy) - } - const response = await fetch('https://poe.com/api/gql_POST', option) - let text = await response.text() - try { - let result = JSON.parse(text) - console.log({ result }) - return result - } catch (e) { - console.error(text) - throw e - } - } - - async getBot (displayName) { - let r - let retry = 10 - while (retry >= 0) { - let url = `https://poe.com/_next/data/${this.nextData.buildId}/${displayName}.json` - let option = { - headers: this.headers - } - if (this.config.proxy) { - option.agent = proxy(Config.proxy) - } - let r = await fetch(url, option) - let res = await r.text() - try { - let chatData = (JSON.parse(res)).pageProps.payload.chatOfBotDisplayName - return chatData - } catch (e) { - r = res - retry-- - } - } - throw new Error(r) - } - - async getChatId () { - let option = { - headers: this.headers - } - if (this.config.proxy) { - option.agent = proxy(Config.proxy) - } - let r = await fetch('https://poe.com', option) - let text = await r.text() - const jsonRegex = /