From 3b1641ca5f8a321ff3dda09b776eecd1dae77682 Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Tue, 15 Aug 2023 21:41:52 +0800 Subject: [PATCH 1/6] fix: add code verefication --- server/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/index.js b/server/index.js index c70f79d..455363a 100644 --- a/server/index.js +++ b/server/index.js @@ -187,6 +187,10 @@ export async function createServer() { server.post('/page', async (request, reply) => { const body = request.body || {} if (body.code) { + const pattern = /^[a-zA-Z0-9]+$/ + if (!pattern.test(body.code)) { + reply.send({error: 'bad request'}) + } const dir = 'resources/ChatGPTCache/page' const filename = body.code + '.json' const filepath = path.join(dir, filename) From c2c6ea43deb6e7e881378bee37a97e82cc06a554 Mon Sep 17 00:00:00 2001 From: HalcyonAlcedo <41666148+HalcyonAlcedo@users.noreply.github.com> Date: Thu, 24 Aug 2023 22:33:23 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E6=B7=BB=E5=8A=A0Bard=E5=92=8C=E6=98=9F?= =?UTF-8?q?=E7=81=ABAPI=E6=94=AF=E6=8C=81=20(#551)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 修复后台API反代地址未能正确显示的问题 * 更新渲染页面配置 * 添加个人聊天模式配置 * 将用户数据获取改到common中 * 修复错误的渲染页面参数 * 修复bug * 添加Live2D * 修复渲染页面错误 * 修复渲染传入值 * 更新渲染 * 修复图表渲染bug * 调整live2d模型大小 * 修复live2d无法关闭问题 * 修复错误的传值 * 修复ai命名 * 更新渲染 * 添加用户独立设定 * 更新渲染配置适配个人设置 * 修复合并导致的渲染文件异常删除 * 修复用户数据缺失问题 * 修复旧版本数据缺失问题 * 修复bing参数不存在问题,兼容miao的截图 * 修复受限token重试时不被排除的问题 * 修复个人模式下结束对话的模式错误 * 更新渲染页面,将预览版转为正式版 * 修复传统渲染无法调用截图功能的问题 * 文字模式也进行一次缓存 * 更新README * Update README.md * 更新渲染 * 更新渲染页面 * 添加版本信息 * 遗漏参数 * 丢失引用 * 补充路由 * 添加云转码功能 * 判断node-silk是否正常合成 * 云转码提示 * 修复图片渲染出错 * 云转码支持发送Buffer * 添加云转码模式支持 * 更新描述 * 更新后台渲染页面 * 更新配置 * 更新渲染页面 * 添加云渲染 * 修复错误的接口调用 * 修复遗漏的数据转换 * 修复获取的图片数据异常问题 * 更新后台配置 * 更新渲染页面 * 修复云渲染访问地址错误 * 更新渲染页面 * 修复遗漏的模型文件 * 修复live2d问题 * 更新live2d以及相关配置 * 修复遗漏的数据参数 * 修复新live2d情况下云渲染错误的问题 * 适配云渲染1.1.2等待参数 * 添加云服务api检查 * 更新渲染页面 * 添加live2d加载检测 * 修复错误的属性判断 * 添加云渲染DPR * 更新sydney支持内容生成 * 修改文件模式语音转码接收模式 * 添加云转码时recordUrl检查 * 更新后台配置项 * 修复错误的文本描述 * 更新后台页面 * 添加全局对话模式设置,更新后台面板 * 添加第三方渲染服务适配 * 修复第三方服务器live2d加载导致的渲染失败问题 * 修复后台地址无法实时保存的问题 * 添加live2d模型透明度设置 * 合并更新 * 更新渲染页面 * 更新渲染页面 * 使dpr对本地渲染也生效 * 更新渲染页面 * 添加网页截图功能 * 添加后台配置项 * 添加配置导出和导入功能 * 运行通过参数传递用户token * 登录时将token作为参数返回 * 修复错误 * 添加密码修改和用户删除接口 * 修正密码比对 * 修复user错误 * 优化数据保存时的返回值 * 添加系统额外服务检查api * 添加AccessToken配置 * 修复错误的导入提示 * 添加ws连接 * 添加ws用户信息获取 * 修复错误的循环 * 修正ws参数 * 添加群消息获取权限 * 添加用户多端登录支持 * 修复错误的路径引用 * 修复错误的路径引用 * 修复错误 * 修复页面数据获取失败问题 * 修复异常的中断 * 添加配置视图 * 更新配置面板 * 添加用户版本信息 * 更新配置视图 * 修复错误的视图绑定 * 修改视图文件位置,添加mediaLink相关代码 * 修复错误的视图配置绑定 * 更新依赖,添加qq消息组件初始化信息获取 * 修复异常的群名称无法获取问题 * 修改注释 * 撤销对management的错误合并 * 添加Sydney图片识别功能 * 更新配置文件和后台页面 * 修改view配置 * 修复node版本过低导致的FormData无法调用,尝试添加反代 * 国外图片识别不使用反代 * fix: 修复云转码导致的语音重复发送问题 * fix: 修复qq消息可越权获取的问题 * feat: 添加代理post操作 * fix: 修复一些字符串格式的数字导致的配置加载错误 * fix: 修复错误的云服务api网址格式 * fix: 修复错误的云转码接口调用格式 * fix: 修复错误的精度 * 添加配置项适配 * feat: 添加群消息合并功能 * 添加历史记录的消息已读标签 * feat: 添加设定相关接口 * improvement: 在多少绘图失败后尝试使用cn进行绘图 * fix: 修复bing可能存在的无标题引用导致的错误 * fix: 修复绘图获取失败的问题 * 添加bard支持 * feat: 添加图片处理 * 添加bard图片处理能力 * 添加锅巴描述 * fix: 修复空图片链接报错问题 * 添加星火api支持,尚未连接上下文 * feat: 添加星火api上下文 * feat: 增加星火设定 * 添加调试信息 * 添加星火v2支持 * 修复连接地址错误 * feat: 添加星火助手功能 * 添加图片处理和群组消息结束 * feat: 添加星火图片识别支持 * 改回全部使用form-data,优化文本 * 添加对私人星火助手的支持 * 添加xhweb错误内容输出 * 添加星火预设问题库重写 * feat: 添加星火回复内容替换,添加锅巴配置 * 添加星火助手支持 * fix: 修复星火空设定时额外占用消息 --------- Co-authored-by: ikechan8370 --- apps/chat.js | 255 ++++++++++++++++++--- apps/management.js | 15 ++ guoba.support.js | 96 ++++++++ package.json | 1 + resources/view/setting_view.json | 105 ++++++++- utils/bard.js | 373 +++++++++++++++++++++++++++++++ utils/config.js | 14 ++ utils/xinghuo/xinghuo.js | 359 +++++++++++++++++++++++++++-- yarn.lock | 43 +--- 9 files changed, 1174 insertions(+), 87 deletions(-) create mode 100644 utils/bard.js diff --git a/apps/chat.js b/apps/chat.js index 14dcd2f..81dd6bd 100644 --- a/apps/chat.js +++ b/apps/chat.js @@ -39,6 +39,7 @@ import { SlackClaudeClient } from '../utils/slack/slackClient.js' import { getPromptByName } from '../utils/prompts.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' @@ -161,6 +162,14 @@ export class chatgpt extends plugin { /** 执行方法 */ fnc: 'xh' }, + { + reg: '^#星火助手', + fnc: 'newxhBotConversation' + }, + { + reg: '^#星火(搜索|查找)助手', + fnc: 'searchxhBot' + }, { /** 命令正则匹配 */ reg: toggleMode === 'at' ? '^[^#][sS]*' : '^#chat[^gpt][sS]*', @@ -248,7 +257,7 @@ export class chatgpt extends plugin { * @param e * @returns {Promise} */ - async bingCaptcha (e) { + async bingCaptcha(e) { let bingTokens = JSON.parse(await redis.get('CHATGPT:BING_TOKENS')) if (!bingTokens) { await e.reply('尚未绑定必应token:必应过码必须绑定token') @@ -302,36 +311,41 @@ export class chatgpt extends plugin { async destroyConversations(e) { const userData = await getUserData(e.user_id) const use = (userData.mode === 'default' ? null : userData.mode) || await redis.get('CHATGPT:USE') - await redis.del(`CHATGPT:WRONG_EMOTION:${e.sender.user_id}`) + await redis.del(`CHATGPT:WRONG_EMOTION:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`) if (use === 'claude') { // let client = new SlackClaudeClient({ // slackUserToken: Config.slackUserToken, // slackChannelId: Config.slackChannelId // }) // await client.endConversation() - await redis.del(`CHATGPT:SLACK_CONVERSATION:${e.sender.user_id}`) + await redis.del(`CHATGPT:SLACK_CONVERSATION:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`) await e.reply('claude对话已结束') return } if (use === 'xh') { - await redis.del(`CHATGPT:CONVERSATIONS_XH:${e.sender.user_id}`) + await redis.del(`CHATGPT:CONVERSATIONS_XH:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`) await e.reply('星火对话已结束') return } + if (use === 'bard') { + await redis.del(`CHATGPT:CONVERSATIONS_BARD:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`) + await e.reply('Bard对话已结束') + return + } let ats = e.message.filter(m => m.type === 'at') const isAtMode = Config.toggleMode === 'at' if (isAtMode) ats = ats.filter(item => item.qq !== Bot.uin) if (ats.length === 0) { if (use === 'api3') { - await redis.del(`CHATGPT:QQ_CONVERSATION:${e.sender.user_id}`) + await redis.del(`CHATGPT:QQ_CONVERSATION:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`) await this.reply('已退出当前对话,该对话仍然保留。请@我进行聊天以开启新的对话', true) } else if (use === 'bing' && (Config.toneStyle === 'Sydney' || Config.toneStyle === 'Custom')) { - let c = await redis.get(`CHATGPT:CONVERSATIONS_BING:${e.sender.user_id}`) + let c = await redis.get(`CHATGPT:CONVERSATIONS_BING:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`) if (!c) { await this.reply('当前没有开启对话', true) return } else { - await redis.del(`CHATGPT:CONVERSATIONS_BING:${e.sender.user_id}`) + await redis.del(`CHATGPT:CONVERSATIONS_BING:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`) } const conversation = { store: new KeyvFile({ filename: 'cache.json' }), @@ -475,7 +489,18 @@ export class chatgpt extends plugin { for (let i = 0; i < cs.length; i++) { await redis.del(cs[i]) if (Config.debug) { - logger.info('delete slack conversation of qq: ' + cs[i]) + logger.info('delete xh conversation of qq: ' + cs[i]) + } + deleted++ + } + break + } + case 'bard': { + let cs = await redis.keys('CHATGPT:CONVERSATIONS_BARD:*') + for (let i = 0; i < cs.length; i++) { + await redis.del(cs[i]) + if (Config.debug) { + logger.info('delete bard conversation of qq: ' + cs[i]) } deleted++ } @@ -1005,6 +1030,10 @@ export class chatgpt extends plugin { key = `CHATGPT:CONVERSATIONS_XH:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}` break } + case 'bard': { + key = `CHATGPT:CONVERSATIONS_BARD:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}` + break + } } let ctime = new Date() previousConversation = (key ? await redis.get(key) : null) || JSON.stringify({ @@ -1038,6 +1067,12 @@ export class chatgpt extends plugin { await e.reply([chatMessage.text, segment.image(`base64://${chatMessage.image}`)]) return false } + // 处理星火和bard图片 + if ((use === 'bard' || use === 'xh') && chatMessage?.images) { + chatMessage.images.forEach(async element => { + await e.reply([element.tag, segment.image(element.url)]) + }) + } if (use === 'api' && !chatMessage) { // 字数超限直接返回 return false @@ -1059,6 +1094,11 @@ export class chatgpt extends plugin { } else if (chatMessage.id) { previousConversation.parentMessageId = chatMessage.id } + if (use === 'bard' && !chatMessage.error) { + previousConversation.parentMessageId = chatMessage.responseID + previousConversation.clientId = chatMessage.choiceID + previousConversation.invocationId = chatMessage._reqID + } if (Config.debug) { logger.info(chatMessage) } @@ -1091,9 +1131,9 @@ export class chatgpt extends plugin { response.length / 2 < endIndex ? [response.substring(startIndex), response.substring(0, startIndex)] : [ - response.substring(0, endIndex + 1), - response.substring(endIndex + 1) - ] + response.substring(0, endIndex + 1), + response.substring(endIndex + 1) + ] const match = ttsArr[0].match(emotionReg) response = ttsArr[1].replace(/\n/, '').trim() if (match) { @@ -1647,7 +1687,6 @@ export class chatgpt extends plugin { }) } } - console.log(response) // 处理内容生成的图片 if (response.details.imageTag) { if (Config.debug) { @@ -1714,23 +1753,23 @@ export class chatgpt extends plugin { 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 (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)) + // token过期了 + // let bingTokens = JSON.parse(await redis.get('CHATGPT:BING_TOKENS')) + // const badBingToken = bingTokens.findIndex(element => element.Token === bingToken) + // // 可能是微软抽风,给三次机会 + // 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)) logger.warn(`token${bingToken}疑似不存在或已过期,再试试`) retry = retry - 0.1 } else { @@ -1845,16 +1884,71 @@ export class chatgpt extends plugin { } } case '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值)') } let client = new XinghuoClient({ - ssoSessionId + ssoSessionId, + cache: cacheOptions }) - let response = await client.sendMessage(prompt, conversation?.conversationId) + // 获取图片资源 + const image = await getImg(e) + let response = await client.sendMessage(prompt, conversation?.conversationId, image ? image[0] : undefined) return response } + case '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: 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 + } + } default: { let completionParams = {} if (Config.model) { @@ -2206,6 +2300,105 @@ export class chatgpt extends plugin { return true } + async newxhBotConversation(e) { + let botId = e.msg.replace(/^#星火助手/, '').trim() + if (Config.xhmode != 'web') { + await e.reply('星火助手仅支持体验版使用', true) + return true + } + if (!botId) { + await e.reply('无效助手id', true) + } else { + const ssoSessionId = Config.xinghuoToken + if (!ssoSessionId) { + await e.reply(`未绑定星火token,请使用#chatgpt设置星火token命令绑定token`, true) + return true + } + let client = new XinghuoClient({ + ssoSessionId, + cache: null + }) + try { + let chatId = await client.createChatList(botId) + let botInfoRes = await fetch(`https://xinghuo.xfyun.cn/iflygpt/bot/getBotInfo?chatId=${chatId.chatListId}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Cookie: 'ssoSessionId=' + ssoSessionId + ';', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/113.0.5672.69 Mobile/15E148 Safari/604.1', + } + }) + if (botInfoRes.ok) { + let botInfo = await botInfoRes.json() + if (botInfo.flag) { + let ctime = new Date() + await redis.set( + `CHATGPT:CONVERSATIONS_XH:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}`, + JSON.stringify({ + sender: e.sender, + ctime, + utime: ctime, + num: 0, + conversation: { + conversationId: { + chatid: chatId.chatListId, + botid: botId + } + } + }), + Config.conversationPreserveTime > 0 ? { EX: Config.conversationPreserveTime } : {} + ) + await e.reply(`成功创建助手对话\n助手名称:${botInfo.data.bot_name}\n助手描述:${botInfo.data.bot_desc}`, true) + } else { + await e.reply(`创建助手对话失败,${botInfo.desc}`, true) + } + } else { + await e.reply(`创建助手对话失败,服务器异常`, true) + } + } catch (error) { + await e.reply(`创建助手对话失败 ${error}`, true) + } + } + return true + } + + async searchxhBot(e) { + let searchBot = e.msg.replace(/^#星火(搜索|查找)助手/, '').trim() + const ssoSessionId = Config.xinghuoToken + if (!ssoSessionId) { + await e.reply(`未绑定星火token,请使用#chatgpt设置星火token命令绑定token`, true) + return true + } + const cacheresOption = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Cookie: 'ssoSessionId=' + ssoSessionId + ';', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/113.0.5672.69 Mobile/15E148 Safari/604.1', + }, + body: JSON.stringify({ + botType: '', + pageIndex: 1, + pageSize: 45, + searchValue: searchBot + }) + } + const searchBots = await fetch('https://xinghuo.xfyun.cn/iflygpt/bot/page', cacheresOption) + const bots = await searchBots.json() + if (Config.debug) { + logger.info(bots) + } + if (bots.code === 0) { + if (bots.data.pageList.length > 0) { + this.reply(await makeForwardMsg(this.e, bots.data.pageList.map(msg => `${msg.bot.botId} - ${msg.bot.botName}`))) + } else { + await e.reply(`未查到相关助手`, true) + } + } else { + await e.reply(`搜索助手失败`, true) + } + } + async emptyQueue(e) { await redis.lTrim('CHATGPT:CHAT_QUEUE', 1, 0) await this.reply('已清空当前等待队列') @@ -2354,7 +2547,7 @@ export class chatgpt extends plugin { return await this.chatGPTApi.sendMessage(prompt, sendMessageOption) } - async solveBingCaptcha (e) { + async solveBingCaptcha(e) { try { let id = e.bingCaptchaId let regionId = e.regionId diff --git a/apps/management.js b/apps/management.js index 93aca06..763a2b5 100644 --- a/apps/management.js +++ b/apps/management.js @@ -108,6 +108,11 @@ export class ChatgptManagement extends plugin { fnc: 'useXinghuoBasedSolution', permission: 'master' }, + { + reg: '^#chatgpt切换(Bard|bard)$', + fnc: 'useBardBasedSolution', + permission: 'master' + }, { reg: '^#chatgpt(必应|Bing)切换', fnc: 'changeBingTone', @@ -834,6 +839,16 @@ azure语音:Azure 语音是微软 Azure 平台提供的一项语音服务, } } + async useBardBasedSolution () { + let use = await redis.get('CHATGPT:USE') + if (use !== 'bard') { + await redis.set('CHATGPT:USE', 'bard') + await this.reply('已切换到基于Bard的解决方案') + } else { + await this.reply('当前已经是Bard模式了') + } + } + async changeBingTone (e) { let tongStyle = e.msg.replace(/^#chatgpt(必应|Bing)切换/, '') if (!tongStyle) { diff --git a/guoba.support.js b/guoba.support.js index 8a1c3a4..148406f 100644 --- a/guoba.support.js +++ b/guoba.support.js @@ -611,12 +611,108 @@ export function supportGuoba () { label: '以下为星火方式的配置', component: 'Divider' }, + { + field: 'xhmode', + label: '星火模式', + bottomHelpMessage: '设置星火使用的对话模式', + component: 'Select', + componentProps: { + options: [ + { label: '体验版', value: 'web' }, + { label: '讯飞星火认知大模型V1.5', value: 'api' }, + { label: '讯飞星火认知大模型V2.0', value: 'apiv2' }, + { label: '讯飞星火助手', value: 'assistants' } + ] + } + }, { field: 'xinghuoToken', label: '星火Cookie', bottomHelpMessage: '获取对话页面的ssoSessionId cookie。不要带等号和分号', + component: 'InputPassword' + }, + { + field: 'xhAppId', + label: 'AppId', + bottomHelpMessage: '应用页面获取', component: 'Input' }, + { + field: 'xhAPISecret', + label: 'APISecret', + bottomHelpMessage: '应用页面获取', + component: 'InputPassword' + }, + { + field: 'xhAPIKey', + label: '星火APIKey', + bottomHelpMessage: '应用页面获取', + component: 'InputPassword' + }, + { + field: 'xhAssistants', + label: '助手接口', + bottomHelpMessage: '助手页面获取', + component: 'Input' + }, + { + field: 'xhTemperature', + label: '核采样阈值', + bottomHelpMessage: '核采样阈值。用于决定结果随机性,取值越高随机性越强即相同的问题得到的不同答案的可能性越高', + component: 'InputNumber' + }, + { + field: 'xhMaxTokens', + label: '最大Token', + bottomHelpMessage: '模型回答的tokens的最大长度', + component: 'InputNumber' + }, + { + field: 'xhPromptSerialize', + label: '序列化设定', + bottomHelpMessage: '是否将设定内容进行json序列化', + component: 'Switch' + }, + { + field: 'xhPrompt', + label: '设定', + bottomHelpMessage: '若开启序列化,请传入json数据,例如[{ \"role\": \"user\", \"content\": \"现在是10点\" },{ \"role\": \"assistant\", \"content\": \"了解,现在10点了\" }]', + component: 'InputTextArea' + }, + { + field: 'xhRetRegExp', + label: '回复替换正则', + bottomHelpMessage: '要替换文本的正则', + component: 'Input' + }, + { + field: 'xhRetReplace', + label: '回复内容替换', + bottomHelpMessage: '替换回复内容中的文本', + component: 'Input' + }, + { + label: '以下为Bard方式的配置', + component: 'Divider' + }, + { + field: 'bardPsid', + label: 'BardCookie', + bottomHelpMessage: '获取https://bard.google.com/页面的cookie,可完整输入,需至少包含__Secure-1PSID和__Secure-1PSIDTS', + component: 'Input' + }, + { + field: 'bardReverseProxy', + label: 'Bard反代地址', + bottomHelpMessage: 'bard反代服务器地址,用于绕过地区限制', + component: 'Input' + }, + { + field: 'bardForceUseReverse', + label: 'Bard使用反代', + bottomHelpMessage: '开启后将通过反代访问bard', + component: 'Switch' + }, { label: '以下为杂七杂八的配置', component: 'Divider' diff --git a/package.json b/package.json index c5aa66d..9171460 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@waylaidwanderer/chatgpt-api": "^1.37.1", "asn1.js": "^5.0.0", "chatgpt": "^5.2.4", + "crypto": "^1.0.1", "delay": "^6.0.0", "diff": "^5.1.0", "emoji-strip": "^1.0.1", diff --git a/resources/view/setting_view.json b/resources/view/setting_view.json index 05f12c3..487dfd7 100644 --- a/resources/view/setting_view.json +++ b/resources/view/setting_view.json @@ -81,6 +81,10 @@ "label": "星火", "value": "xh" }, + { + "label": "Bard", + "value": "bard" + }, { "label": "浏览器", "value": "browser" @@ -709,10 +713,109 @@ "title": "星火", "tab": "xh", "view": [ + { + "type": "select", + "label": "模式", + "data": "xhmode", + "items": [ + { + "label": "讯飞星火认知大模型体验版", + "value": "web" + }, + { + "label": "讯飞星火认知大模型V1.5", + "value": "api" + }, + { + "label": "讯飞星火认知大模型V2.0", + "value": "apiv2" + }, + { + "label": "讯飞星火助手", + "value": "assistants" + } + ] + }, { "type": "password", - "label": "星火Cookie", + "label": "Cookie", "data": "xinghuoToken" + }, + { + "type": "text", + "label": "AppId", + "data": "xhAppId" + }, + { + "type": "password", + "label": "APISecret", + "data": "xhAPISecret" + }, + { + "type": "password", + "label": "APIKey", + "data": "xhAPIKey" + }, + { + "type": "text", + "label": "助手接口", + "data": "xhAssistants" + }, + { + "type": "number", + "label": "核采样阈值", + "placeholder": "核采样阈值。用于决定结果随机性,取值越高随机性越强即相同的问题得到的不同答案的可能性越高", + "data": "xhTemperature" + }, + { + "type": "number", + "label": "最大Token", + "placeholder": "模型回答的tokens的最大长度", + "data": "xhMaxTokens" + }, + { + "type": "check", + "label": "序列化设定", + "data": "xhPromptSerialize" + }, + { + "type": "textarea", + "label": "设定", + "placeholder": "若开启序列化,请传入json数据,例如[{ \"role\": \"user\", \"content\": \"现在是10点\" },{ \"role\": \"assistant\", \"content\": \"了解,现在10点了\" }]", + "data": "xhPrompt" + }, + { + "type": "text", + "label": "回复替换正则", + "placeholder": "要替换文本的正则", + "data": "xhRetRegExp" + }, + { + "type": "text", + "label": "回复内容替换", + "placeholder": "替换回复内容中的文本", + "data": "xhRetReplace" + } + ] + }, + { + "title": "Bard", + "tab": "bard", + "view": [ + { + "type": "password", + "label": "BardCookie", + "data": "bardPsid" + }, + { + "type": "url", + "label": "Bard反代地址", + "data": "bardReverseProxy" + }, + { + "type": "check", + "label": "使用Bard反代", + "data": "bardForceUseReverse" } ] } diff --git a/utils/bard.js b/utils/bard.js new file mode 100644 index 0000000..dff940c --- /dev/null +++ b/utils/bard.js @@ -0,0 +1,373 @@ +// https://github.com/EvanZhouDev/bard-ai + +class Bard { + static JSON = "json"; + static MD = "markdown"; + + // ID derived from Cookie + SNlM0e; + + // HTTPS Headers + #headers; + + // Resolution status of initialization call + #initPromise; + + #bardURL = "https://bard.google.com"; + + // Wether or not to log events to console + #verbose = false; + + // Fetch function + #fetch = fetch; + + constructor(cookie, config) { + // Register some settings + if (config?.verbose == true) this.#verbose = true; + if (config?.fetch) this.#fetch = config.fetch; + // 可变更访问地址,利用反向代理绕过区域限制 + if (config?.bardURL) this.#bardURL = config.bardURL; + + // If a Cookie is provided, initialize + if (cookie) { + this.#initPromise = this.#init(cookie); + } else { + throw new Error("Please provide a Cookie when initializing Bard."); + } + this.cookie = cookie; + } + + // You can also choose to initialize manually + async #init(cookie) { + this.#verbose && console.log("🚀 Starting intialization"); + // Assign headers + this.#headers = { + Host: this.#bardURL.match(/^https?:\/\/([^\/]+)\/?$/)[1], + "X-Same-Domain": "1", + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36", + "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", + Origin: this.#bardURL, + Referer: this.#bardURL, + Cookie: (typeof cookie === "object") ? (Object.entries(cookie).map(([key, val]) => `${key}=${val};`).join("")) : ("__Secure-1PSID=" + cookie), + }; + + let responseText; + // Attempt to retrieve SNlM0e + try { + this.#verbose && + console.log("🔒 Authenticating your Google account"); + responseText = await this.#fetch(this.#bardURL, { + method: "GET", + headers: this.#headers, + credentials: "include", + }) + .then((response) => response.text()) + } catch (e) { + // Failure to get server + throw new Error( + "Could not fetch Google Bard. You may be disconnected from internet: " + + e + ); + } + + try { + const SNlM0e = responseText.match(/SNlM0e":"(.*?)"/)[1]; + // Assign SNlM0e and return it + this.SNlM0e = SNlM0e; + this.#verbose && console.log("✅ Initialization finished\n"); + return SNlM0e; + } catch { + throw new Error( + "Could not use your Cookie. Make sure that you copied correctly the Cookie with name __Secure-1PSID exactly. If you are sure your cookie is correct, you may also have reached your rate limit." + ); + } + } + + async #uploadImage(name, buffer) { + this.#verbose && console.log("🖼️ Starting image processing"); + let size = buffer.byteLength; + let formBody = [ + `${encodeURIComponent("File name")}=${encodeURIComponent([name])}`, + ]; + + try { + this.#verbose && + console.log("💻 Finding Google server destination"); + let response = await this.#fetch( + "https://content-push.googleapis.com/upload/", + { + method: "POST", + headers: { + "X-Goog-Upload-Command": "start", + "X-Goog-Upload-Protocol": "resumable", + "X-Goog-Upload-Header-Content-Length": size, + "X-Tenant-Id": "bard-storage", + "Push-Id": "feeds/mcudyrk2a4khkz", + }, + body: formBody, + credentials: "include", + } + ); + + const uploadUrl = response.headers.get("X-Goog-Upload-URL"); + this.#verbose && console.log("📤 Sending your image"); + response = await this.#fetch(uploadUrl, { + method: "POST", + headers: { + "X-Goog-Upload-Command": "upload, finalize", + "X-Goog-Upload-Offset": 0, + "X-Tenant-Id": "bard-storage", + }, + body: buffer, + credentials: "include", + }); + + const imageFileLocation = await response.text(); + + this.#verbose && console.log("✅ Image finished working\n"); + return imageFileLocation; + } catch (e) { + throw new Error( + "Could not fetch Google Bard. You may be disconnected from internet: " + + e + ); + } + } + + // Query Bard + async #query(message, config) { + let formatMarkdown = (text, images) => { + if (!images) return text; + + for (let imageData of images) { + const formattedTag = `!${imageData.tag}(${imageData.url})`; + text = text.replace( + new RegExp(`(?!\\!)\\[${imageData.tag.slice(1, -1)}\\]`), + formattedTag + ); + } + + return text; + } + + let { ids, imageBuffer } = config; + + // Wait until after init + await this.#initPromise; + + this.#verbose && console.log("🔎 Starting Bard Query"); + + // If user has not run init + if (!this.SNlM0e) { + throw new Error( + "Please initialize Bard first. If you haven't passed in your Cookie into the class, run Bard.init(cookie)." + ); + } + + this.#verbose && console.log("🏗️ Building Request"); + // HTTPS parameters + const params = { + bl: "boq_assistant-bard-web-server_20230711.08_p0", + _reqID: ids?._reqID ?? "0", + rt: "c", + }; + + // If IDs are provided, but doesn't have every one of the expected IDs, error + const messageStruct = [ + [message], + null, + [null, null, null], + ]; + + if (imageBuffer) { + let imageLocation = await this.#uploadImage( + `bard-ai_upload`, + imageBuffer + ); + messageStruct[0].push(0, null, [ + [[imageLocation, 1], "bard-ai_upload"], + ]); + } + + if (ids) { + const { conversationID, responseID, choiceID } = ids; + messageStruct[2] = [conversationID, responseID, choiceID]; + } + + // HTTPs data + const data = { + "f.req": JSON.stringify([null, JSON.stringify(messageStruct)]), + at: this.SNlM0e, + }; + + // URL that we are submitting to + const url = new URL( + "/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate", + this.#bardURL + ); + + // Append parameters to the URL + for (const key in params) { + url.searchParams.append(key, params[key]); + } + + // Encode the data + const formBody = Object.entries(data) + .map( + ([property, value]) => + `${encodeURIComponent(property)}=${encodeURIComponent( + value + )}` + ) + .join("&"); + + this.#verbose && console.log("💭 Sending message to Bard"); + // Send the fetch request + const chatData = await this.#fetch(url.toString(), { + method: "POST", + headers: this.#headers, + body: formBody, + credentials: "include", + }) + .then((response) => { + return response.text(); + }) + .then((text) => { + return JSON.parse(text.split("\n")[3])[0][2]; + }) + .then((rawData) => JSON.parse(rawData)); + + this.#verbose && console.log("🧩 Parsing output"); + // Get first Bard-recommended answer + const answer = chatData[4][0]; + + // Text of that answer + const text = answer[1][0]; + + // Get data about images in that answer + const images = + answer[4]?.map((x) => ({ + tag: x[2], + url: x[3][0][0], + info: { + raw: x[0][0][0], + source: x[1][0][0], + alt: x[0][4], + website: x[1][1], + favicon: x[1][3], + }, + })) ?? []; + + this.#verbose && console.log("✅ All done!\n"); + // Put everything together and return + return { + content: formatMarkdown(text, images), + images: images, + ids: { + conversationID: chatData[1][0], + responseID: chatData[1][1], + choiceID: answer[0], + _reqID: String(parseInt(ids?._reqID ?? 0) + 100000), + }, + }; + } + + async #parseConfig(config) { + let result = { + useJSON: false, + imageBuffer: undefined, // Returns as {extension, filename} + ids: undefined, + }; + + // Verify that format is one of the two types + if (config?.format) { + switch (config.format) { + case Bard.JSON: + result.useJSON = true; + break; + case Bard.MD: + result.useJSON = false; + break; + default: + throw new Error( + "Format can obly be Bard.JSON for JSON output or Bard.MD for Markdown output." + ); + } + } + + // Verify that the image passed in is either a path to a jpeg, jpg, png, or webp, or that it is a Buffer + if (config?.image) { + if ( + config.image instanceof ArrayBuffer + ) { + result.imageBuffer = config.image; + } else if ( + typeof config.image === "string" && + /\.(jpeg|jpg|png|webp)$/.test(config.image) + ) { + let fs; + + try { + fs = await import("fs") + } catch { + throw new Error( + "Loading from an image file path is not supported in a browser environment.", + ); + } + + result.imageBuffer = fs.readFileSync( + config.image, + ).buffer; + } else { + throw new Error( + "Provide your image as a file path to a .jpeg, .jpg, .png, or .webp, or a Buffer." + ); + } + } + + // Verify that all values in IDs exist + if (config?.ids) { + if (config.ids.conversationID && config.ids.responseID && config.ids.choiceID && config.ids._reqID) { + result.ids = config.ids; + } else { + throw new Error( + "Please provide the IDs exported exactly as given." + ); + } + } + return result; + } + + // Ask Bard a question! + async ask(message, config) { + let { useJSON, imageBuffer, ids } = await this.#parseConfig(config); + let response = await this.#query(message, { imageBuffer, ids }); + return useJSON ? response : response.content; + } + + createChat(ids) { + let bard = this; + class Chat { + ids = ids; + + async ask(message, config) { + let { useJSON, imageBuffer } = await bard.#parseConfig(config); + let response = await bard.#query(message, { + imageBuffer, + ids: this.ids, + }); + this.ids = response.ids; + return useJSON ? response : response.content; + } + + export() { + return this.ids; + } + } + + return new Chat(); + } +} + +export default Bard; diff --git a/utils/config.js b/utils/config.js index d410283..d4780b2 100644 --- a/utils/config.js +++ b/utils/config.js @@ -50,6 +50,17 @@ const defaultConfig = { plus: false, useGPT4: false, xinghuoToken: '', + xhmode: 'web', + xhAppId: '', + xhAPISecret: '', + xhAPIKey: '', + xhAssistants: '', + xhTemperature: 0.5, + xhMaxTokens: 1024, + xhPromptSerialize: false, + xhPrompt: '', + xhRetRegExp: '', + xhRetReplace: '', promptPrefixOverride: 'Your answer shouldn\'t be too verbose. Prefer to answer in Chinese.', assistantLabel: 'ChatGPT', // thinkingTips: true, @@ -113,6 +124,9 @@ const defaultConfig = { slackClaudeEnableGlobalPreset: true, slackClaudeGlobalPreset: '', slackClaudeSpecifiedChannel: '', + bardPsid: '', + bardReverseProxy: '', + bardForceUseReverse: false, cloudTranscode: 'https://silk.201666.xyz', cloudRender: false, cloudMode: 'url', diff --git a/utils/xinghuo/xinghuo.js b/utils/xinghuo/xinghuo.js index 8fecea0..49878e6 100644 --- a/utils/xinghuo/xinghuo.js +++ b/utils/xinghuo/xinghuo.js @@ -2,6 +2,8 @@ import fetch from 'node-fetch' import { Config } from '../config.js' import { createParser } from 'eventsource-parser' import https from 'https' +import WebSocket from 'ws' +import { config } from 'process' const referer = atob('aHR0cHM6Ly94aW5naHVvLnhmeXVuLmNuL2NoYXQ/aWQ9') const origin = atob('aHR0cHM6Ly94aW5naHVvLnhmeXVuLmNu') @@ -13,8 +15,24 @@ try { } catch (err) { logger.warn('未安装form-data,无法使用星火模式') } +let crypto +try { + crypto = (await import('crypto')).default +} catch (err) { + logger.warn('未安装crypto,无法使用星火api模式') +} +async function getKeyv() { + let Keyv + try { + Keyv = (await import('keyv')).default + } catch (error) { + throw new Error('keyv依赖未安装,请使用pnpm install keyv安装') + } + return Keyv +} export default class XinghuoClient { - constructor (opts) { + constructor(opts) { + this.cache = opts.cache this.ssoSessionId = opts.ssoSessionId this.headers = { Referer: referer, @@ -24,19 +42,249 @@ export default class XinghuoClient { } } - async sendMessage (prompt, chatId) { + apiErrorInfo(code) { + switch (code) { + case 10000: return '升级为ws出现错误' + case 10001: return '通过ws读取用户的消息出错' + case 10002: return '通过ws向用户发送消息 错' + case 10003: return '用户的消息格式有错误' + case 10004: return '用户数据的schema错误' + case 10005: return '用户参数值有错误' + case 10006: return '用户并发错误:当前用户已连接,同一用户不能多处同时连接。' + case 10007: return '用户流量受限:服务正在处理用户当前的问题,需等待处理完成后再发送新的请求。(必须要等大模型完全回复之后,才能发送下一个问题)' + case 10008: return '服务容量不足,联系工作人员' + case 10009: return '和引擎建立连接失败' + case 10010: return '接收引擎数据的错误' + case 10011: return '发送数据给引擎的错误' + case 10012: return '引擎内部错误' + case 10013: return '输入内容审核不通过,涉嫌违规,请重新调整输入内容' + case 10014: return '输出内容涉及敏感信息,审核不通过,后续结果无法展示给用户' + case 10015: return 'appid在黑名单中' + case 10016: return 'appid授权类的错误。比如:未开通此功能,未开通对应版本,token不足,并发超过授权 等等' + case 10017: return '清除历史失败' + case 10019: return '表示本次会话内容有涉及违规信息的倾向;建议开发者收到此错误码后给用户一个输入涉及违规的提示' + case 10110: return '服务忙,请稍后再试' + case 10163: return '请求引擎的参数异常 引擎的schema 检查不通过' + case 10222: return '引擎网络异常' + case 10907: return 'token数量超过上限。对话历史+问题的字数太多,需要精简输入' + case 11200: return '授权错误:该appId没有相关功能的授权 或者 业务量超过限制' + case 11201: return '授权错误:日流控超限。超过当日最大访问量的限制' + case 11202: return '授权错误:秒级流控超限。秒级并发超过授权路数限制' + case 11203: return '授权错误:并发流控超限。并发路数超过授权路数限制' + default: return '无效错误代码' + } + } + + promptBypassPreset(prompt) { + switch (prompt) { + case '你是谁': + return '你是谁,叫什么' + case '你是谁啊': + return '你是谁啊,叫什么' + default: + return prompt + } + } + + async initCache() { + if (!this.conversationsCache) { + const cacheOptions = this.cache || {} + cacheOptions.namespace = cacheOptions.namespace || 'xh' + let Keyv = await getKeyv() + this.conversationsCache = new Keyv(cacheOptions) + } + } + + async getWsUrl() { + if (!crypto) return false + const APISecret = Config.xhAPISecret + const APIKey = Config.xhAPIKey + let APILink = '/v1.1/chat' + if (Config.xhmode == 'apiv2') { + APILink = '/v2.1/chat' + } + const date = new Date().toGMTString() + const algorithm = 'hmac-sha256' + const headers = 'host date request-line' + const signatureOrigin = `host: spark-api.xf-yun.com\ndate: ${date}\nGET ${APILink} HTTP/1.1` + const hmac = crypto.createHmac('sha256', APISecret) + hmac.update(signatureOrigin) + const signature = hmac.digest('base64') + const authorizationOrigin = `api_key="${APIKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"` + const authorization = Buffer.from(authorizationOrigin).toString('base64') + const v = { + authorization: authorization, + date: date, + host: "spark-api.xf-yun.com" + } + const url = `wss://spark-api.xf-yun.com${APILink}?${Object.keys(v).map(key => `${key}=${v[key]}`).join('&')}` + return url + } + + async uploadImage(url) { + // 获取图片 + let response = await fetch(url, { + method: 'GET', + }) + const blob = await response.blob() + const arrayBuffer = await blob.arrayBuffer() + const buffer = Buffer.from(arrayBuffer) + // 上传oss + const formData = new FormData() + formData.append('file', buffer, 'image.png') + const respOss = await fetch('https://xinghuo.xfyun.cn/iflygpt/oss/sign', { + method: 'POST', + headers: { + Cookie: 'ssoSessionId=' + this.ssoSessionId + ';', + }, + body: formData + }) + if (respOss.ok) { + const ossData = await respOss.json() + // 上传接口 + const sparkdeskUrl = `${ossData.data.url}&authorization=${Buffer.from(ossData.data.authorization).toString('base64')}&date=${ossData.data.date}&host=${ossData.data.host}` + const respSparkdes = await fetch(sparkdeskUrl, { + method: 'POST', + headers: { + Cookie: 'ssoSessionId=' + this.ssoSessionId + ';', + authorization: Buffer.from(ossData.data.authorization).toString('base64') + }, + body: buffer + }) + if (respSparkdes.ok) { + const sparkdesData = await respSparkdes.json() + return { + url: sparkdesData.data.link, + file: buffer + } + } else { + try { + const sparkdesData = await respSparkdes.json() + logger.error('星火图片Sparkdes:发送失败' + sparkdesData.desc) + } catch (error) { + logger.error('星火图片Sparkdes:发送失败') + } + return false + } + } else { + try { + const ossData = await respOss.json() + logger.error('星火图片OSS:上传失败' + ossData.desc) + } catch (error) { + logger.error('星火图片OSS:上传失败') + } + return false + } + } + + async apiMessage(prompt, chatId, ePrompt = []) { + if (!chatId) chatId = (Math.floor(Math.random() * 1000000) + 100000).toString() + + // 初始化缓存 + await this.initCache() + const conversationKey = `ChatXH_${chatId}` + const conversation = (await this.conversationsCache.get(conversationKey)) || { + messages: [], + createdAt: Date.now() + } + + // 获取ws链接 + const wsUrl = Config.xhmode == 'assistants' ? Config.xhAssistants : await this.getWsUrl() + if (!wsUrl) throw new Error('缺少依赖:crypto。请安装依赖后重试') + + // 编写消息内容 + const wsSendData = { + header: { + app_id: Config.xhAppId, + uid: chatId + }, + parameter: { + chat: { + domain: Config.xhmode == 'api' ? "general" : "generalv2", + temperature: Config.xhTemperature, // 核采样阈值 + max_tokens: Config.xhMaxTokens, // tokens最大长度 + chat_id: chatId + } + }, + payload: { + message: { + "text": [ + ...ePrompt, + ...conversation.messages, + { "role": "user", "content": prompt } + ] + } + } + } + if (Config.debug) { + logger.info(wsSendData.payload.message.text) + } + + return new Promise((resolve, reject) => { + const socket = new WebSocket(wsUrl) + let resMessage = '' + socket.on('open', () => { + socket.send(JSON.stringify(wsSendData)) + }) + socket.on('message', async (message) => { + try { + const messageData = JSON.parse(message) + if (messageData.header.code != 0) { + reject(`接口发生错误:Error Code ${messageData.header.code} ,${this.apiErrorInfo(messageData.header.code)}`) + } + if (messageData.header.status == 0 || messageData.header.status == 1) { + resMessage += messageData.payload.choices.text[0].content + } + if (messageData.header.status == 2) { + resMessage += messageData.payload.choices.text[0].content + conversation.messages.push({ + role: 'user', + content: prompt + }) + conversation.messages.push({ + role: 'assistant', + content: resMessage + }) + // 超过规定token去除一半曾经的对话记录 + if (messageData.payload.usage.text.total_tokens >= Config.xhMaxTokens) { + const half = Math.floor(conversation.messages.length / 2) + conversation.messages.splice(0, half) + } + await this.conversationsCache.set(conversationKey, conversation) + resolve(resMessage) + } + } catch (error) { + reject(new Error(error)) + } + }) + socket.on('error', (error) => { + reject(error) + }) + }) + } + + async webMessage(prompt, chatId, botId) { if (!FormData) { throw new Error('缺少依赖:form-data。请安装依赖后重试') } - if (!chatId) { - chatId = (await this.createChatList()).chatListId - } - let requestP = new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { let formData = new FormData() formData.setBoundary('----WebKitFormBoundarycATE2QFHDn9ffeWF') formData.append('clientType', '2') formData.append('chatId', chatId) - formData.append('text', prompt) + if (prompt.image) { + prompt.text = prompt.text.replace("[图片]", "") // 清理消息中中首个被使用的图片 + const imgdata = await this.uploadImage(prompt.image) + if (imgdata) { + formData.append('fileUrl', imgdata.url) + formData.append('file', imgdata.file, 'image.png') + } + } + formData.append('text', prompt.text) + if (botId) { + formData.append('isBot', '1') + formData.append('botId', botId) + } let randomNumber = Math.floor(Math.random() * 1000) let fd = '439' + randomNumber.toString().padStart(3, '0') formData.append('fd', fd) @@ -57,7 +305,7 @@ export default class XinghuoClient { logger.error('星火statusCode:' + statusCode) } let response = '' - function onMessage (data) { + function onMessage(data) { // console.log(data) if (data === '') { return resolve({ @@ -65,8 +313,18 @@ export default class XinghuoClient { response }) } + if (data.charAt(0) === '{') { + try { + response = JSON.parse(data).value + if (Config.debug) { + logger.info(response) + } + } catch (err) { + reject(err) + } + } try { - if (data) { + if (data && data !== '[error]') { response += atob(data.trim()) if (Config.debug) { logger.info(response) @@ -112,22 +370,81 @@ export default class XinghuoClient { // req.write(formData.stringify()) req.end() }) - const { response } = await requestP - // logger.info(response) - // let responseText = atob(response) - return { - conversationId: chatId, - text: response + } + + async sendMessage(prompt, chatId, image) { + // 对星火预设的问题进行重写,避免收到预设回答 + prompt = this.promptBypassPreset(prompt) + if (Config.xhmode == 'api' || Config.xhmode == 'apiv2' || Config.xhmode == 'assistants') { + if (!Config.xhAppId || !Config.xhAPISecret || !Config.xhAPIKey) throw new Error('未配置api') + let Prompt = [] + // 设定 + if (Config.xhPromptSerialize) { + try { + Prompt = JSON.parse(Config.xhPrompt) + } catch (error) { + Prompt = [] + logger.warn('星火设定序列化失败,本次对话不附带设定') + } + } else { + Prompt = Config.xhPrompt ? [{ "role": "user", "content": Config.xhPrompt }] : [] + } + let response = await this.apiMessage(prompt, chatId, Prompt) + if (Config.xhRetRegExp) { + response = response.replace(new RegExp(Config.xhRetRegExp, 'g'), Config.xhRetReplace) + } + return { + conversationId: chatId, + text: response + } + } else if (Config.xhmode == 'web') { + let botId = false + if (chatId && typeof chatId === 'object') { + chatId = chatId.chatid + botId = chatId.botid + } + if (!chatId) { + chatId = (await this.createChatList()).chatListId + } + let { response } = await this.webMessage({ text: prompt, image: image }, chatId, botId) + // logger.info(response) + // let responseText = atob(response) + // 处理图片 + let images + if (response.includes('multi_image_url')) { + images = [{ + tag: '', + url: JSON.parse(/{([^}]*)}/g.exec(response)[0]).url + }] + response = '我已经完成作品,欢迎您提出宝贵的意见和建议,帮助我快速进步~~' + } + if (botId) { + chatId = { + chatid: chatId, + botid: botId + } + } + if (Config.xhRetRegExp) { + response = response.replace(new RegExp(Config.xhRetRegExp, 'g'), Config.xhRetReplace) + } + return { + conversationId: chatId, + text: response, + images: images + } + } else { + throw new Error('星火模式错误') } } - async createChatList () { + async createChatList(bot = false) { let createChatListRes = await fetch(createChatUrl, { method: 'POST', headers: Object.assign(this.headers, { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + Botweb: bot ? 1 : 0 }), - body: '{}' + body: bot ? `{"BotWeb": 1, "botId": "${bot}"}` : '{}' }) if (createChatListRes.status !== 200) { let errorRes = await createChatListRes.text() @@ -139,8 +456,8 @@ export default class XinghuoClient { if (createChatListRes.data?.id) { logger.info('星火对话创建成功:' + createChatListRes.data.id) } else { - logger.error('星火对话创建成功: ' + JSON.stringify(createChatListRes)) - throw new Error('星火对话创建成功:' + JSON.stringify(createChatListRes)) + logger.error('星火对话创建失败: ' + JSON.stringify(createChatListRes)) + throw new Error('星火对话创建失败:' + JSON.stringify(createChatListRes)) } return { chatListId: createChatListRes.data?.id, @@ -149,6 +466,6 @@ export default class XinghuoClient { } } -function atob (s) { +function atob(s) { return Buffer.from(s, 'base64').toString() } diff --git a/yarn.lock b/yarn.lock index 56def67..3a3a2c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1400,6 +1400,11 @@ cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +crypto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037" + integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig== + data-uri-to-buffer@^4.0.0: version "4.0.1" resolved "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz" @@ -1792,7 +1797,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-fifo@^1.0.0, fast-fifo@^1.1.0, fast-fifo@^1.2.0: +fast-fifo@^1.0.0: version "1.2.0" resolved "https://registry.npmmirror.com/fast-fifo/-/fast-fifo-1.2.0.tgz" integrity sha512-NcvQXt7Cky1cNau15FWy64IjuO8X0JijhTBBrJj1YlxlDfRkJXNaK9RFUjwpfDPzMdv7wB38jr53l9tkNLxnWg== @@ -1836,7 +1841,7 @@ fastify-plugin@^4.0.0, fastify-plugin@^4.3.0: resolved "https://registry.npmmirror.com/fastify-plugin/-/fastify-plugin-4.5.0.tgz" integrity sha512-79ak0JxddO0utAXAQ5ccKhvs6vX2MGyHHMMsmZkBANrq3hXc1CHzvNPHOcvTsVMEPl5I+NT+RO4YKMGehOfSIg== -fastify@^4.11.0, fastify@^4.18.0, fastify@>=4: +fastify@^4.11.0: version "4.18.0" resolved "https://registry.npmmirror.com/fastify/-/fastify-4.18.0.tgz" integrity sha512-L5o/2GEkBastQ3HV0dtKo7SUZ497Z1+q4fcqAoPyq6JCQ/8zdk1JQEoTQwnBWCp+EmA7AQa6mxNqSAEhzP0RwQ== @@ -2248,9 +2253,9 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" -https-proxy-agent@7.0.1: +https-proxy-agent@7.0.1, https-proxy-agent@^7.0.0: version "7.0.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.1.tgz#0277e28f13a07d45c663633841e20a40aaafe0ab" + resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.1.tgz" integrity sha512-Eun8zV0kcYS1g19r78osiQLEFIRspRUDd9tIfBCTBPBeMieF/EsJNL8VI3xOIdYRDEkjQnqOYPsZ2DsWsVsFwQ== dependencies: agent-base "^7.0.2" @@ -2272,14 +2277,6 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" -https-proxy-agent@^7.0.0: - version "7.0.1" - resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.1.tgz" - integrity sha512-Eun8zV0kcYS1g19r78osiQLEFIRspRUDd9tIfBCTBPBeMieF/EsJNL8VI3xOIdYRDEkjQnqOYPsZ2DsWsVsFwQ== - dependencies: - agent-base "^7.0.2" - debug "4" - human-signals@^2.1.0: version "2.1.0" resolved "https://registry.npmmirror.com/human-signals/-/human-signals-2.1.0.tgz" @@ -2733,13 +2730,6 @@ keyv@^4.5.2, keyv@^4.5.3: dependencies: json-buffer "3.0.1" -keyv@^4.5.3: - version "4.5.3" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.3.tgz#00873d2b046df737963157bd04f294ca818c9c25" - integrity sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug== - dependencies: - json-buffer "3.0.1" - kind-of@^2.0.1: version "2.0.1" resolved "https://registry.npmmirror.com/kind-of/-/kind-of-2.0.1.tgz" @@ -3803,13 +3793,6 @@ semver@^7.3.4, semver@^7.3.5, semver@^7.3.8, semver@^7.5.0, semver@^7.5.4: dependencies: lru-cache "^6.0.0" -semver@^7.5.4: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== - dependencies: - lru-cache "^6.0.0" - send@0.18.0: version "0.18.0" resolved "https://registry.npmmirror.com/send/-/send-0.18.0.tgz" @@ -3995,14 +3978,6 @@ streamx@^2.15.0: fast-fifo "^1.1.0" queue-tick "^1.0.1" -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - fast-fifo "^1.1.0" - queue-tick "^1.0.1" - "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz" From 502cc810bbd1d4cdeb15c62fbb208bc2fa635e04 Mon Sep 17 00:00:00 2001 From: sigeisment <896063260@qq.com> Date: Thu, 24 Aug 2023 22:35:29 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E8=83=BD=E5=90=A6=E5=8A=A0=E5=85=A5=20Azur?= =?UTF-8?q?e=20OpenAI=20=E7=9A=84=E7=9B=B8=E5=85=B3=E6=94=AF=E6=8C=81=20#4?= =?UTF-8?q?71=20(#536)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 能否加入 Azure OpenAI 的相关支持 #471 * Azure OpenAI 锅巴配置 * 增加Azure OpenAI依赖 * 动态加载Azure OpenAI依赖 --------- Co-authored-by: sigeisment Co-authored-by: ikechan8370 --- apps/chat.js | 34 ++++++++++++++++++++++++++++++++++ apps/management.js | 15 +++++++++++++++ config/config.example.json | 2 ++ guoba.support.js | 22 ++++++++++++++++++++++ package.json | 1 + utils/common.js | 1 + 6 files changed, 75 insertions(+) diff --git a/apps/chat.js b/apps/chat.js index 81dd6bd..14a1019 100644 --- a/apps/chat.js +++ b/apps/chat.js @@ -69,6 +69,12 @@ import { SendMessageToSpecificGroupOrUserTool } from '../utils/tools/SendMessage import { SetTitleTool } from '../utils/tools/SetTitleTool.js' import { createCaptcha, solveCaptcha, solveCaptchaOneShot } from '../utils/bingCaptcha.js' +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) { @@ -1034,6 +1040,10 @@ export class chatgpt extends plugin { key = `CHATGPT:CONVERSATIONS_BARD:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}` break } + case 'azure': { + key = `CHATGPT:CONVERSATIONS_AZURE:${e.sender.user_id}` + break + } } let ctime = new Date() previousConversation = (key ? await redis.get(key) : null) || JSON.stringify({ @@ -1041,6 +1051,7 @@ export class chatgpt extends plugin { ctime, utime: ctime, num: 0, + messages: [{ role: 'system', content: 'You are an AI assistant that helps people find information.' }], conversation: {} }) previousConversation = JSON.parse(previousConversation) @@ -1048,6 +1059,7 @@ export class chatgpt extends plugin { logger.info({ previousConversation }) } conversation = { + messages: previousConversation.messages, conversationId: previousConversation.conversation?.conversationId, parentMessageId: previousConversation.parentMessageId, clientId: previousConversation.clientId, @@ -1093,6 +1105,11 @@ export class chatgpt extends plugin { } } else if (chatMessage.id) { previousConversation.parentMessageId = chatMessage.id + } else if (chatMessage.message) { + if (previousConversation.messages.length > 10) { + previousConversation.messages.shift() + } + previousConversation.messages.push(chatMessage.message) } if (use === 'bard' && !chatMessage.error) { previousConversation.parentMessageId = chatMessage.responseID @@ -1901,6 +1918,23 @@ export class chatgpt extends plugin { let response = await client.sendMessage(prompt, conversation?.conversationId, image ? image[0] : undefined) return response } + case '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.apiKey)) + const deploymentName = Config.azureDeploymentName + const { choices } = await client.getChatCompletions(deploymentName, msg) + let completion = choices[0].message; + return {'text' : completion.content, 'message': completion} case 'bard': { // 处理cookie const matchesPSID = /__Secure-1PSID=([^;]+)/.exec(Config.bardPsid) diff --git a/apps/management.js b/apps/management.js index 763a2b5..4a7e070 100644 --- a/apps/management.js +++ b/apps/management.js @@ -108,6 +108,11 @@ export class ChatgptManagement extends plugin { fnc: 'useXinghuoBasedSolution', permission: 'master' }, + { + reg: '^#chatgpt切换azure$', + fnc: 'useAzureBasedSolution', + permission: 'master' + }, { reg: '^#chatgpt切换(Bard|bard)$', fnc: 'useBardBasedSolution', @@ -838,6 +843,15 @@ azure语音:Azure 语音是微软 Azure 平台提供的一项语音服务, await this.reply('当前已经是星火模式了') } } + async useAzureBasedSolution () { + let use = await redis.get('CHATGPT:USE') + if (use !== 'azure') { + await redis.set('CHATGPT:USE', 'azure') + await this.reply('已切换到基于Azure的解决方案') + } else { + await this.reply('当前已经是Azure模式了') + } + } async useBardBasedSolution () { let use = await redis.get('CHATGPT:USE') @@ -894,6 +908,7 @@ azure语音:Azure 语音是微软 Azure 平台提供的一项语音服务, let mode = await redis.get('CHATGPT:USE') const modeMap = { browser: '浏览器', + azure: 'Azure', // apiReverse: 'API2', api: 'API', bing: '必应', diff --git a/config/config.example.json b/config/config.example.json index 6d2d673..35dae41 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -19,6 +19,8 @@ "openAiBaseUrl": "https://mondstadt.d201.eu.org/v1", "OpenAiPlatformRefreshToken": "", "openAiForceUseReverse": false, + "azureDeploymentName":"", + "azureUrl":"", "drawCD": 30, "model": "", "temperature": 0.8, diff --git a/guoba.support.js b/guoba.support.js index 148406f..0bb7319 100644 --- a/guoba.support.js +++ b/guoba.support.js @@ -849,6 +849,28 @@ export function supportGuoba () { label: '合成emoji的API地址,默认谷歌厨房', component: 'Input' }, + { + label: '以下为Azure chatGPT的配置', + component: 'Divider' + }, + { + field: 'apiKey', + label: 'Azure API Key', + bottomHelpMessage: '管理密钥,用于访问Azure的API接口', + component: 'InputPassword' + }, + { + field: 'azureUrl', + label: '端点地址', + bottomHelpMessage: 'https://xxxx.openai.azure.com/', + component: 'Input' + }, + { + field: 'azureDeploymentName', + label: '部署名称', + bottomHelpMessage: '创建部署时输入的名称', + component: 'Input' + }, { label: '以下为后台与渲染相关配置', component: 'Divider' diff --git a/package.json b/package.json index 9171460..e6a60d7 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "type": "module", "author": "ikechan8370", "dependencies": { + "@azure/openai": "^1.0.0-beta.1", "@fastify/cookie": "^8.3.0", "@fastify/cors": "^8.2.0", "@fastify/static": "^6.9.0", diff --git a/utils/common.js b/utils/common.js index 403d100..b25e51d 100644 --- a/utils/common.js +++ b/utils/common.js @@ -717,6 +717,7 @@ export async function getUserData (user) { chat: [], mode: '', cast: { + azure: '', api: '', // API设定 bing: '', // 必应设定 bing_resource: '', // 必应扩展资料 From 246aaad987ab43cfcacd185f9f79a8051456e7a3 Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Thu, 24 Aug 2023 23:00:51 +0800 Subject: [PATCH 4/6] fix: syntax error --- apps/chat.js | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/chat.js b/apps/chat.js index 14a1019..fdad866 100644 --- a/apps/chat.js +++ b/apps/chat.js @@ -1935,6 +1935,7 @@ export class chatgpt extends plugin { const { choices } = await client.getChatCompletions(deploymentName, msg) let completion = choices[0].message; return {'text' : completion.content, 'message': completion} + } case 'bard': { // 处理cookie const matchesPSID = /__Secure-1PSID=([^;]+)/.exec(Config.bardPsid) From d6ed1bf37f4366ae64c988d92910bb1ef46d9915 Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Fri, 25 Aug 2023 14:51:16 +0800 Subject: [PATCH 5/6] fix: trim undefined error --- apps/chat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/chat.js b/apps/chat.js index fdad866..7411269 100644 --- a/apps/chat.js +++ b/apps/chat.js @@ -1210,7 +1210,7 @@ export class chatgpt extends plugin { let quotemessage = [] if (chatMessage?.quote) { chatMessage.quote.forEach(function (item, index) { - if (item.text.trim() !== '') { + if (item.text && item.text.trim() !== '') { quotemessage.push(item) } }) From 043b80ddd40e4896fd97e954e8bb68e468864bd4 Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Sat, 26 Aug 2023 10:52:32 +0800 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=E4=B8=80=E4=BA=9B?= =?UTF-8?q?headers=E4=BB=A5=E5=87=8F=E5=B0=91=E5=A5=87=E6=80=AA=E7=9A=8450?= =?UTF-8?q?2=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- utils/SydneyAIClient.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/utils/SydneyAIClient.js b/utils/SydneyAIClient.js index 6a47799..be9fb1f 100644 --- a/utils/SydneyAIClient.js +++ b/utils/SydneyAIClient.js @@ -70,25 +70,25 @@ export default class SydneyAIClient { accept: 'application/json', 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', 'content-type': 'application/json', - 'sec-ch-ua': '"Microsoft Edge";v="113", "Chromium";v="113", "Not-A.Brand";v="24"', + // 'sec-ch-ua': '"Microsoft Edge";v="113", "Chromium";v="113", "Not-A.Brand";v="24"', // 'sec-ch-ua-arch': '"x86"', // 'sec-ch-ua-bitness': '"64"', // 'sec-ch-ua-full-version': '"112.0.1722.7"', // 'sec-ch-ua-full-version-list': '"Chromium";v="112.0.5615.20", "Microsoft Edge";v="112.0.1722.7", "Not:A-Brand";v="99.0.0.0"', - 'sec-ch-ua-mobile': '?0', + // 'sec-ch-ua-mobile': '?0', // 'sec-ch-ua-model': '', - 'sec-ch-ua-platform': '"macOS"', + // 'sec-ch-ua-platform': '"macOS"', // 'sec-ch-ua-platform-version': '"15.0.0"', - 'sec-fetch-dest': 'empty', - 'sec-fetch-mode': 'cors', - 'sec-fetch-site': 'same-origin', - 'x-ms-client-request-id': crypto.randomUUID(), - 'x-ms-useragent': 'azsdk-js-api-client-factory/1.0.0-beta.1 core-rest-pipeline/1.10.3 OS/macOS', + // 'sec-fetch-dest': 'empty', + // 'sec-fetch-mode': 'cors', + // 'sec-fetch-site': 'same-origin', + // 'x-ms-client-request-id': crypto.randomUUID(), + // 'x-ms-useragent': 'azsdk-js-api-client-factory/1.0.0-beta.1 core-rest-pipeline/1.10.3 OS/macOS', // cookie: this.opts.cookies || `_U=${this.opts.userToken}`, Referer: 'https://edgeservices.bing.com/edgesvc/chat?udsframed=1&form=SHORUN&clientscopes=chat,noheader,channelstable,', - 'Referrer-Policy': 'origin-when-cross-origin', + // 'Referrer-Policy': 'origin-when-cross-origin', // Workaround for request being blocked due to geolocation - 'x-forwarded-for': '1.1.1.1' + // 'x-forwarded-for': '1.1.1.1' } } if (this.opts.cookies || this.opts.userToken) {