From b221098c37a5db50cbc0ac4c4ae9f8d667be72c1 Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Tue, 12 Mar 2024 18:18:06 +0800 Subject: [PATCH] fix: refactor qwen mode --- apps/chat.js | 1084 ++------------------------------- model/core.js | 1127 +++++++++++++++++++++++++++++++++++ utils/alibaba/qwen-api.js | 48 +- utils/alibaba/qwen-api.ts | 42 +- utils/alibaba/types.ts | 503 +++++++++------- utils/openai/chatgpt-api.ts | 2 +- 6 files changed, 1505 insertions(+), 1301 deletions(-) create mode 100644 model/core.js diff --git a/apps/chat.js b/apps/chat.js index 5b51043..20f305d 100644 --- a/apps/chat.js +++ b/apps/chat.js @@ -1,28 +1,21 @@ 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 AzureTTS from '../utils/tts/microsoft-azure.js' import VoiceVoxTTS from '../utils/tts/voicevox.js' import { completeJSON, - extractContentFromFile, formatDate, formatDate2, generateAudio, getDefaultReplySetting, getImageOcrText, getImg, - getMasterQQ, - getMaxModelTokens, - getOrDownloadFile, getUin, getUserData, getUserReplySetting, - isCN, isImage, makeForwardMsg, randomString, @@ -30,77 +23,16 @@ import { renderUrl } from '../utils/common.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 { ConversationManager, originalValues } from '../model/conversation.js' -import BingDrawClient from '../utils/BingDraw.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 { generateSuggestedResponse, 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 { ClaudeAPIClient } from '../client/ClaudeAPIClient.js' -import { getMessageById, upsertMessage } from '../utils/history.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() @@ -112,7 +44,6 @@ let proxy = getProxy() * 这里使用动态数据获取,以便于锅巴动态更新数据 */ // 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 ? { @@ -126,6 +57,7 @@ const newFetch = (url, options = {}) => { return fetch(url, mergedOptions) } + export class chatgpt extends plugin { constructor (e) { let toggleMode = Config.toggleMode @@ -779,7 +711,7 @@ export class chatgpt extends plugin { num: 0 } } - } else if (use !== 'poe') { + } else { switch (use) { case 'api': { key = `CHATGPT:CONVERSATIONS:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}` @@ -832,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) @@ -856,7 +791,7 @@ 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 } @@ -904,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) @@ -1186,7 +1119,10 @@ export class chatgpt extends plugin { if (!Config.enableToolbox) { return } - let cacheData = { file: '', status: '' } + let cacheData = { + file: '', + status: '' + } cacheData.file = randomString() const cacheresOption = { method: 'POST', @@ -1228,919 +1164,18 @@ 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 - }) - return await this.chatGPTApi.sendMessage(prompt, conversation) - } else if (use === 'claude') { - // slack已经不可用,移除 - const client = new ClaudeAPIClient({ - key: Config.claudeApiKey, - model: Config.claudeApiModel || 'claude-3-sonnet-20240229', - debug: true, - baseUrl: Config.claudeApiBaseUrl - // temperature: Config.claudeApiTemperature || 0.5 - }) - let rsp = await client.sendMessage(prompt, { - stream: false, - parentMessageId: conversation.parentMessageId, - conversationId: conversation.conversationId, - system: Config.claudeSystemPrompt - }) - return rsp - } else if (use === 'claude2') { - let { conversationId } = conversation - let client = new ClaudeAIClient({ - 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) - } - }, - 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 + if (cacheData.error || cacheData.status != 200) { + await this.reply(`出现错误:${cacheData.error || 'server error ' + cacheData.status}`, true) } 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.debug(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 - } + 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) } } @@ -2265,7 +1300,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 => { @@ -2351,9 +1389,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: { @@ -2393,56 +1438,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/model/core.js b/model/core.js new file mode 100644 index 0000000..0e107e1 --- /dev/null +++ b/model/core.js @@ -0,0 +1,1127 @@ +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) + + ((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) + } + } + } + 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已经不可用,移除 + const client = new ClaudeAPIClient({ + key: Config.claudeApiKey, + model: Config.claudeApiModel || 'claude-3-sonnet-20240229', + debug: true, + baseUrl: Config.claudeApiBaseUrl + // temperature: Config.claudeApiTemperature || 0.5 + }) + let rsp = await client.sendMessage(prompt, { + stream: false, + parentMessageId: conversation.parentMessageId, + conversationId: conversation.conversationId, + system: Config.claudeSystemPrompt + }) + return rsp + } else if (use === 'claude2') { + let { conversationId } = conversation + let client = new ClaudeAIClient({ + 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 + } + opts.systemMessage = await handleSystem(e, opts.systemMessage) + + 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) + 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/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/openai/chatgpt-api.ts b/utils/openai/chatgpt-api.ts index ebca3c8..ca29740 100644 --- a/utils/openai/chatgpt-api.ts +++ b/utils/openai/chatgpt-api.ts @@ -176,7 +176,7 @@ export class ChatGPTAPI { completionParams ) console.log(`maxTokens: ${maxTokens}, numTokens: ${numTokens}`) - const result: types.ChatMessage & { conversation: openai.ChatCompletionRequestMessage[] }= { + const result: types.ChatMessage & { conversation: openai.ChatCompletionRequestMessage[] } = { role: 'assistant', id: uuidv4(), conversationId,