diff --git a/apps/chat.js b/apps/chat.js index 00d88e6..cefe17e 100644 --- a/apps/chat.js +++ b/apps/chat.js @@ -907,8 +907,8 @@ export class chatgpt extends plugin { if (e.user_id == getUin(e)) return false prompt = msg.trim() try { - if (e.isGroup && typeof this.e.group.getMemberMap === 'function') { - let mm = await this.e.group.getMemberMap() + if (e.isGroup) { + let mm = this.e.bot.gml let me = mm.get(getUin(e)) || {} let card = me.card let nickname = me.nickname @@ -1595,7 +1595,7 @@ export class chatgpt extends plugin { bingAIClient.opts.userToken = bingToken bingAIClient.opts.cookies = cookies // opt.messageType = allThrottled ? 'Chat' : 'SearchQuery' - if (Config.enableGroupContext && e.isGroup && typeof e.group.getMemberMap === 'function') { + if (Config.enableGroupContext && e.isGroup) { try { opt.groupId = e.group_id opt.qq = e.sender.user_id @@ -1931,7 +1931,8 @@ export class chatgpt extends plugin { let response = await client.sendMessage(prompt, { e, chatId: conversation?.conversationId, - image: image ? image[0] : undefined + image: image ? image[0] : undefined, + system: Config.xhPrompt }) return response } else if (use === 'azure') { diff --git a/apps/draw.js b/apps/draw.js index 339d29c..d28a706 100644 --- a/apps/draw.js +++ b/apps/draw.js @@ -324,10 +324,14 @@ export class dalle extends plugin { const index = bingTokens.findIndex(element => element.Token === bingToken) bingTokens[index].Usage += 1 await redis.set('CHATGPT:BING_TOKENS', JSON.stringify(bingTokens)) - + let cookie + if (bingToken.includes('=')) { + cookie = bingToken + } let client = new BingDrawClient({ baseUrl: Config.sydneyReverseProxy, - userToken: bingToken + userToken: bingToken, + cookies: cookie }) await redis.set(`CHATGPT:DRAW:${e.sender.user_id}`, 'c', { EX: 30 }) try { diff --git a/apps/entertainment.js b/apps/entertainment.js index 3b888d8..1de8d70 100644 --- a/apps/entertainment.js +++ b/apps/entertainment.js @@ -56,9 +56,13 @@ export class Entertainment extends plugin { fnc: 'wordcloud_new' }, { - reg: '^#((寄批踢|gpt|GPT)?翻.*|chatgpt翻译帮助)', + reg: '^#((寄批踢|gpt|GPT)?翻[sS]*|chatgpt翻译帮助)', fnc: 'translate' }, + { + reg: '^#(chatgpt)?(设置|修改)翻译来源(openai|gemini|星火|通义千问|xh|qwen)$', + fnc: 'translateSource' + }, { reg: '^#ocr', fnc: 'ocr' @@ -166,10 +170,10 @@ ${translateLangLabels} await this.reply(err.message, e.isGroup) return false } - const totalLength = Array.isArray(result) - ? result.reduce((acc, cur) => acc + cur.length, 0) - : result.length - if (totalLength > 300 || multiText) { + // const totalLength = Array.isArray(result) + // ? result.reduce((acc, cur) => acc + cur.length, 0) + // : result.length + if (multiText) { // 多条翻译结果 if (Array.isArray(result)) { result = await makeForwardMsg(e, result, '翻译结果') @@ -187,6 +191,26 @@ ${translateLangLabels} return true } + translateSource (e) { + let command = e.msg + if (command.includes('openai')) { + Config.translateSource = 'openai' + } else if (command.includes('gemini')) { + Config.translateSource = 'gemini' + } else if (command.includes('星火')) { + Config.translateSource = 'xh' + } else if (command.includes('通义千问')) { + Config.translateSource = 'qwen' + } else if (command.includes('xh')) { + Config.translateSource = 'xh' + } else if (command.includes('qwen')) { + Config.translateSource = 'qwen' + } else { + e.reply('暂不支持该翻译源') + } + e.reply('√成功设置翻译源为' + Config.translateSource) + } + async wordcloud (e) { if (e.isGroup) { let groupId = e.group_id diff --git a/apps/management.js b/apps/management.js index a5ddbbc..bdc4012 100644 --- a/apps/management.js +++ b/apps/management.js @@ -22,22 +22,7 @@ import loader from '../../../lib/plugins/loader.js' import VoiceVoxTTS, { supportConfigurations as voxRoleList } from '../utils/tts/voicevox.js' import { supportConfigurations as azureRoleList } from '../utils/tts/microsoft-azure.js' import fetch from 'node-fetch' -import { getProxy } from '../utils/proxy.js' - -let proxy = getProxy() -const newFetch = (url, options = {}) => { - const defaultOptions = Config.proxy - ? { - agent: proxy(Config.proxy) - } - : {} - const mergedOptions = { - ...defaultOptions, - ...options - } - - return fetch(url, mergedOptions) -} +import { newFetch } from '../utils/proxy.js' export class ChatgptManagement extends plugin { constructor (e) { @@ -87,11 +72,11 @@ export class ChatgptManagement extends plugin { fnc: 'migrateBingAccessToken', permission: 'master' }, - { - reg: '^#chatgpt切换浏览器$', - fnc: 'useBrowserBasedSolution', - permission: 'master' - }, + // { + // reg: '^#chatgpt切换浏览器$', + // fnc: 'useBrowserBasedSolution', + // permission: 'master' + // }, { reg: '^#chatgpt切换API$', fnc: 'useOpenAIAPIBasedSolution', @@ -243,7 +228,7 @@ export class ChatgptManagement extends plugin { }, { /** 命令正则匹配 */ - reg: '^#(关闭|打开)群聊上下文$', + reg: '^#(chatgpt)?(关闭|打开)群聊上下文$', /** 执行方法 */ fnc: 'enableGroupContext', permission: 'master' @@ -254,16 +239,16 @@ export class ChatgptManagement extends plugin { permission: 'master' }, { - reg: '^#(设置|修改)管理密码', + reg: '^#(chatgpt)?(设置|修改)管理密码', fnc: 'setAdminPassword', permission: 'master' }, { - reg: '^#(设置|修改)用户密码', + reg: '^#(chatgpt)?(设置|修改)用户密码', fnc: 'setUserPassword' }, { - reg: '^#工具箱', + reg: '^#(chatgpt)?工具箱', fnc: 'toolsPage', permission: 'master' }, @@ -281,7 +266,7 @@ export class ChatgptManagement extends plugin { fnc: 'commandHelp' }, { - reg: '^#语音切换.*', + reg: '^#(chatgpt)?语音切换.*', fnc: 'ttsSwitch', permission: 'master' }, @@ -910,7 +895,7 @@ azure语音:Azure 语音是微软 Azure 平台提供的一项语音服务, let use = await redis.get('CHATGPT:USE') if (use !== 'bing') { await redis.set('CHATGPT:USE', 'bing') - await this.reply('已切换到基于微软新必应的解决方案,如果已经对话过务必执行`#结束对话`避免引起404错误') + await this.reply('已切换到基于微软Copilot(必应)的解决方案,如果已经对话过务必执行`#结束对话`避免引起404错误') } else { await this.reply('当前已经是必应Bing模式了') } @@ -1577,7 +1562,7 @@ azure语音:Azure 语音是微软 Azure 平台提供的一项语音服务, const data = await response.json() const chatdata = data.chatConfig || {} for (let [keyPath, value] of Object.entries(chatdata)) { - if (keyPath === 'blockWords' || keyPath === 'promptBlockWords' || keyPath === 'initiativeChatGroups') { value = value.toString().split(/[,,;;\|]/) } + if (keyPath === 'blockWords' || keyPath === 'promptBlockWords' || keyPath === 'initiativeChatGroups') { value = value.toString().split(/[,,;;|]/) } if (Config[keyPath] != value) { changeConfig.push({ item: keyPath, diff --git a/apps/vocal.js b/apps/vocal.js new file mode 100644 index 0000000..d6adcb8 --- /dev/null +++ b/apps/vocal.js @@ -0,0 +1,108 @@ +import plugin from '../../../lib/plugins/plugin.js' +import { SunoClient } from '../client/SunoClient.js' +import { Config } from '../utils/config.js' +import { downloadFile, maskEmail } from '../utils/common.js' +import common from '../../../lib/common/common.js' +import lodash from 'lodash' + +export class Vocal extends plugin { + constructor (e) { + super({ + name: 'ChatGPT-Plugin 音乐合成', + dsc: '基于Suno等AI的饮月生成!', + event: 'message', + priority: 500, + rule: [ + { + reg: '^#((创作)?歌曲|suno|Suno)', + fnc: 'createSong', + permission: 'master' + } + ] + }) + this.task = [ + { + // 设置十分钟左右的浮动 + cron: '0/1 * * * ?', + // cron: '*/2 * * * *', + name: '保持suno心跳', + fnc: this.heartbeat.bind(this) + } + ] + } + + async heartbeat (e) { + let sessTokens = Config.sunoSessToken.split(',') + let clientTokens = Config.sunoClientToken.split(',') + for (let i = 0; i < sessTokens.length; i++) { + let sessToken = sessTokens[i] + let clientToken = clientTokens[i] + if (sessToken && clientToken) { + let client = new SunoClient({ sessToken, clientToken }) + await client.heartbeat() + } + } + } + + async createSong (e) { + if (!Config.sunoClientToken || !Config.sunoSessToken) { + await e.reply('未配置Suno Token') + return true + } + let description = e.msg.replace(/#((创作)?歌曲|suno|Suno)/, '') + if (description === '额度' || description === 'credit' || description === '余额') { + let sessTokens = Config.sunoSessToken.split(',') + let clientTokens = Config.sunoClientToken.split(',') + let msg = '' + for (let i = 0; i < sessTokens.length; i++) { + let sess = sessTokens[i] + let clientToken = clientTokens[i] + let client = new SunoClient({ sessToken: sess, clientToken }) + let { credit, email } = await client.queryCredit() + logger.info({ credit, email }) + msg += `用户: ${maskEmail(email)} 余额:${credit}\n` + } + msg += '-------------------\n' + msg += 'Notice:每首歌消耗5credit,每次生成2首歌' + await e.reply(msg) + return true + } + await e.reply('正在生成,请稍后') + try { + let sessTokens = Config.sunoSessToken.split(',') + let clientTokens = Config.sunoClientToken.split(',') + let tried = 0 + while (tried < sessTokens.length) { + let index = tried + let sess = sessTokens[index] + let clientToken = clientTokens[index] + let client = new SunoClient({ sessToken: sess, clientToken }) + let { credit, email } = await client.queryCredit() + logger.info({ credit, email }) + if (credit < 10) { + tried++ + logger.info(`账户${email}余额不足,尝试下一个账户`) + continue + } + + let songs = await client.createSong(description) + let messages = ['提示词:' + description] + for (let song of songs) { + messages.push(`歌名:${song.title}\n风格: ${song.metadata.tags}\n长度: ${lodash.round(song.metadata.duration, 0)}秒\n歌词:\n${song.metadata.prompt}\n`) + messages.push(`音频链接:${song.audio_url}\n视频链接:${song.video_url}\n封面链接:${song.image_url}\n`) + messages.push(segment.image(song.image_url)) + // let videoPath = await downloadFile(song.video_url, `suno/${song.title}.mp4`, false, false, { + // 'User-Agent': '' + // }) + messages.push(segment.video(song.video_url)) + } + await e.reply(common.makeForwardMsg(e, messages, '音乐合成结果')) + return true + } + await e.reply('所有账户余额不足') + } catch (err) { + console.error(err) + await e.reply('生成失败,请查看日志') + } + } +} diff --git a/client/SunoClient.js b/client/SunoClient.js new file mode 100644 index 0000000..4e99e13 --- /dev/null +++ b/client/SunoClient.js @@ -0,0 +1,149 @@ +import { newFetch } from '../utils/proxy.js' +import common from '../../../lib/common/common.js' +import { decrypt } from '../utils/jwt.js' +import { FormData } from 'node-fetch' + +export class SunoClient { + constructor (options) { + this.options = options + this.sessToken = options.sessToken + this.clientToken = options.clientToken + if (!this.clientToken || !this.sessToken) { + throw new Error('Token is required') + } + } + + async getToken () { + let lastToken = this.sessToken + let payload = decrypt(lastToken) + let sid = JSON.parse(payload).sid + logger.debug('sid: ' + sid) + let tokenRes = await newFetch(`https://clerk.suno.ai/v1/client/sessions/${sid}/tokens/api?_clerk_js_version=4.70.0`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Cookie: `__client=${this.clientToken};`, + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', + Origin: 'https://app.suno.ai', + Referer: 'https://app.suno.ai/create/' + } + }) + let tokenData = await tokenRes.json() + let token = tokenData.jwt + logger.info('new token got: ' + token) + return token + } + + async createSong (description) { + let sess = await this.getToken() + let createRes = await newFetch('https://studio-api.suno.ai/api/generate/v2/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${sess}`, + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', + Origin: 'https://app.suno.ai', + Referer: 'https://app.suno.ai/create/', + Cookie: `__sess=${sess}` + }, + body: JSON.stringify({ gpt_description_prompt: description, mv: 'chirp-v2-engine-v13', prompt: '' }) + }) + + if (createRes.status !== 200) { + console.log(await createRes.json()) + throw new Error('Failed to create song ' + createRes.status) + } + let createData = await createRes.json() + let ids = createData?.clips?.map(clip => clip.id) + let queryUrl = `https://studio-api.suno.ai/api/feed/?ids=${ids[0]}%2C${ids[1]}` + let allDone = false; let songs = [] + let timeout = 60 + while (timeout > 0 && !allDone) { + try { + let queryRes = await newFetch(queryUrl, { + headers: { + Authorization: `Bearer ${sess}` + } + }) + if (queryRes.status !== 200) { + logger.error(await queryRes.text()) + console.error('Failed to query song') + } + let queryData = await queryRes.json() + logger.debug(queryData) + allDone = queryData.every(clip => clip.status === 'complete') + songs = queryData + } catch (err) { + console.error(err) + } + await common.sleep(1000) + timeout-- + } + return songs + } + + async queryUser (sess) { + if (!sess) { + sess = await this.getToken() + } + let userRes = await newFetch('https://studio-api.suno.ai/api/session/', { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${sess}`, + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', + Origin: 'https://app.suno.ai', + Referer: 'https://app.suno.ai/create/', + Cookie: `__sess=${sess}` + } + }) + let userData = await userRes.json() + logger.debug(userData) + let user = userData?.user.email + return user + } + + async queryCredit () { + let sess = await this.getToken() + let infoRes = await newFetch('https://studio-api.suno.ai/api/billing/info/', { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${sess}`, + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', + Origin: 'https://app.suno.ai', + Referer: 'https://app.suno.ai/create/', + Cookie: `__sess=${sess}` + } + }) + let infoData = await infoRes.json() + logger.debug(infoData) + let credit = infoData?.total_credits_left + let email = await this.queryUser(sess) + return { + email, credit + } + } + + async heartbeat () { + let lastToken = this.sessToken + let payload = decrypt(lastToken) + let sid = JSON.parse(payload).sid + logger.debug('sid: ' + sid) + let heartbeatUrl = `https://clerk.suno.ai/v1/client/sessions/${sid}/touch?_clerk_js_version=4.70.0` + let heartbeatRes = await fetch(heartbeatUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Cookie: `__client=${this.clientToken};`, + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', + Origin: 'https://app.suno.ai', + Referer: 'https://app.suno.ai/create/' + }, + body: 'active_organization_id=' + }) + logger.debug(await heartbeatRes.text()) + if (heartbeatRes.status === 200) { + logger.debug('heartbeat success') + return true + } + } +} diff --git a/client/test/SunoClientTest.js b/client/test/SunoClientTest.js new file mode 100644 index 0000000..35661ef --- /dev/null +++ b/client/test/SunoClientTest.js @@ -0,0 +1,11 @@ +import { SunoClient } from '../SunoClient.js' + +async function test () { + const options = { + } + let client = new SunoClient(options) + let res = await client.createSong('guacamole') + console.log(res) +} + +test() diff --git a/guoba.support.js b/guoba.support.js index 87c7130..e333e5e 100644 --- a/guoba.support.js +++ b/guoba.support.js @@ -223,45 +223,12 @@ export function supportGuoba () { ] } }, - { - field: 'groupMerge', - label: '群组消息合并', - bottomHelpMessage: '开启后,群聊消息将被视为同一对话', - component: 'Switch' - }, { field: 'allowOtherMode', label: '允许其他模式', bottomHelpMessage: '开启后,则允许用户使用#chat1/#chat3/#chatglm/#bing等命令无视全局模式进行聊天', component: 'Switch' }, - { - field: 'quoteReply', - label: '图片引用消息', - bottomHelpMessage: '在回复图片时引用原始消息', - component: 'Switch' - }, - { - field: 'showQRCode', - label: '启用二维码', - bottomHelpMessage: '在图片模式中启用二维码。该对话内容将被发送至第三方服务器以进行渲染展示,如果不希望对话内容被上传到第三方服务器请关闭此功能', - component: 'Switch' - }, - { - field: 'drawCD', - label: '绘图CD', - helpMessage: '单位:秒', - bottomHelpMessage: '绘图指令的CD时间,主人不受限制', - component: 'InputNumber', - componentProps: { - min: 0 - } - }, - { - field: 'enableDraw', - label: '绘图功能开关', - component: 'Switch' - }, { field: 'proxy', label: '代理服务器地址', @@ -274,6 +241,20 @@ export function supportGuoba () { bottomHelpMessage: '将输出更多调试信息,如果不希望控制台刷屏的话,可以关闭', component: 'Switch' }, + { + field: 'translateSource', + label: '翻译来源', + bottomHelpMessage: '#gpt翻译使用的AI来源', + component: 'Select', + componentProps: { + options: [ + { label: 'OpenAI', value: 'openai' }, + { label: 'Gemini', value: 'gemini' }, + { label: '星火', value: 'xh' }, + { label: '通义千问', value: 'qwen' } + ] + } + }, { label: '以下为服务超时配置。', component: 'Divider' @@ -798,6 +779,237 @@ export function supportGuoba () { bottomHelpMessage: '对https://generativelanguage.googleapis.com的反代', component: 'Input' }, + { + label: '以下为一些杂项配置。', + component: 'Divider' + }, + { + field: 'blockWords', + label: '输出黑名单', + bottomHelpMessage: '检查输出结果中是否有违禁词,如果存在黑名单中的违禁词则不输出。英文逗号隔开', + component: 'InputTextArea' + }, + { + field: 'promptBlockWords', + label: '输入黑名单', + bottomHelpMessage: '检查输入结果中是否有违禁词,如果存在黑名单中的违禁词则不输出。英文逗号隔开', + component: 'InputTextArea' + }, + { + field: 'whitelist', + label: '对话白名单', + bottomHelpMessage: '默认设置为添加群号。优先级高于黑名单。\n' + + '注意:需要添加QQ号时在前面添加^(例如:^123456),此全局添加白名单,即除白名单以外的所有人都不能使用插件对话。\n' + + '如果需要在某个群里独享moment,即群聊中只有白名单上的qq号能用,则使用(群号^qq)的格式(例如:123456^123456)。\n' + + '白名单优先级:混合制 > qq > 群号。\n' + + '黑名单优先级: 群号 > qq > 混合制。', + component: 'Input' + }, + { + field: 'blacklist', + label: '对话黑名单', + bottomHelpMessage: '参考白名单设置规则。', + component: 'Input' + }, + { + field: 'imgOcr', + label: '图片识别', + bottomHelpMessage: '是否识别消息中图片的文字内容,需要同时包含图片和消息才生效', + component: 'Switch' + }, + { + field: 'enablePrivateChat', + label: '是否允许私聊机器人', + component: 'Switch' + }, + { + field: 'defaultUsePicture', + label: '全局图片模式', + bottomHelpMessage: '全局默认以图片形式回复', + component: 'Switch' + }, + { + field: 'defaultUseTTS', + label: '全局语音模式', + bottomHelpMessage: '全局默认以语音形式回复,使用默认角色音色', + component: 'Switch' + }, + { + field: 'ttsMode', + label: '语音模式源', + bottomHelpMessage: '语音模式下使用何种语音源进行文本->音频转换', + component: 'Select', + componentProps: { + options: [ + { + label: 'vits-uma-genshin-honkai', + value: 'vits-uma-genshin-honkai' + }, + { + label: '微软Azure', + value: 'azure' + }, + { + label: 'VoiceVox', + value: 'voicevox' + } + ] + } + }, + { + field: 'defaultTTSRole', + label: 'vits默认角色', + bottomHelpMessage: 'vits-uma-genshin-honkai语音模式下,未指定角色时使用的角色。若留空,将使用随机角色回复。若用户通过指令指定了角色,将忽略本设定', + component: 'Select', + componentProps: { + options: [{ + label: '随机', + value: '随机' + }].concat(speakers.map(s => { return { label: s, value: s } })) + } + }, + { + field: 'azureTTSSpeaker', + label: 'Azure默认角色', + bottomHelpMessage: '微软Azure语音模式下,未指定角色时使用的角色。若用户通过指令指定了角色,将忽略本设定', + component: 'Select', + componentProps: { + options: [{ + label: '随机', + value: '随机' + }, + ...azureRoleList.flatMap(item => [ + item.roleInfo + ]).map(s => ({ + label: s, + value: s + }))] + } + }, + { + field: 'voicevoxTTSSpeaker', + label: 'VoiceVox默认角色', + bottomHelpMessage: 'VoiceVox语音模式下,未指定角色时使用的角色。若留空,将使用随机角色回复。若用户通过指令指定了角色,将忽略本设定', + component: 'Select', + componentProps: { + options: [{ + label: '随机', + value: '随机' + }, + ...voxRoleList.flatMap(item => [ + ...item.styles.map(style => `${item.name}-${style.name}`), + item.name + ]).map(s => ({ + label: s, + value: s + }))] + } + }, + { + field: 'ttsRegex', + label: '语音过滤正则表达式', + bottomHelpMessage: '语音模式下,配置此项以过滤不想被读出来的内容。表达式测试地址:https://www.runoob.com/regexp/regexp-syntax.html', + component: 'Input' + }, + { + field: 'ttsAutoFallbackThreshold', + label: '语音转文字阈值', + helpMessage: '语音模式下,字数超过这个阈值就降级为文字', + bottomHelpMessage: '语音转为文字的阈值', + component: 'InputNumber', + componentProps: { + min: 0, + max: 299 + } + }, + { + field: 'alsoSendText', + label: '语音同时发送文字', + bottomHelpMessage: '语音模式下,同时发送文字版,避免音质较低听不懂', + component: 'Switch' + }, + { + field: 'autoJapanese', + label: 'vits模式日语输出', + bottomHelpMessage: '使用vits语音时,将机器人的文字回复翻译成日文后获取语音。' + + '若想使用插件的翻译功能,发送"#chatgpt翻译帮助"查看使用方法,支持图片翻译,引用翻译...', + component: 'Switch' + }, + { + field: 'autoUsePicture', + label: '长文本自动转图片', + bottomHelpMessage: '字数大于阈值会自动用图片发送,即使是文本模式', + component: 'Switch' + }, + { + field: 'autoUsePictureThreshold', + label: '自动转图片阈值', + helpMessage: '长文本自动转图片开启后才生效', + bottomHelpMessage: '自动转图片的字数阈值', + component: 'InputNumber', + componentProps: { + min: 0 + } + }, + { + field: 'conversationPreserveTime', + label: '对话保留时长', + helpMessage: '单位:秒', + bottomHelpMessage: '每个人发起的对话保留时长。超过这个时长没有进行对话,再进行对话将开启新的对话。', + component: 'InputNumber', + componentProps: { + min: 0 + } + }, + { + field: 'groupMerge', + label: '群组消息合并', + bottomHelpMessage: '开启后,群聊消息将被视为同一对话', + component: 'Switch' + }, + { + field: 'quoteReply', + label: '图片引用消息', + bottomHelpMessage: '在回复图片时引用原始消息', + component: 'Switch' + }, + { + field: 'showQRCode', + label: '启用二维码', + bottomHelpMessage: '在图片模式中启用二维码。该对话内容将被发送至第三方服务器以进行渲染展示,如果不希望对话内容被上传到第三方服务器请关闭此功能', + component: 'Switch' + }, + { + field: 'drawCD', + label: '绘图CD', + helpMessage: '单位:秒', + bottomHelpMessage: '绘图指令的CD时间,主人不受限制', + component: 'InputNumber', + componentProps: { + min: 0 + } + }, + { + field: 'enableDraw', + label: '绘图功能开关', + component: 'Switch' + }, + { + label: '以下为Suno音乐合成的配置。', + component: 'Divider' + }, + { + field: 'sunoSessToken', + label: 'sunoSessToken', + bottomHelpMessage: 'suno的__sess token,需要与sunoClientToken一一对应数量相同,多个用逗号隔开', + component: 'InputTextArea' + }, + { + field: 'sunoClientToken', + label: 'sunoClientToken', + bottomHelpMessage: 'suno的__client token,需要与sunoSessToken一一对应数量相同,多个用逗号隔开', + component: 'InputTextArea' + }, { label: '以下为杂七杂八的配置', component: 'Divider' diff --git a/utils/BingDraw.js b/utils/BingDraw.js index b64f89f..e7fb736 100644 --- a/utils/BingDraw.js +++ b/utils/BingDraw.js @@ -56,7 +56,7 @@ export default class BingDrawClient { fetchOptions.agent = proxy(Config.proxy) } let success = false - let retry = 5 + let retry = 1 let response while (!success && retry >= 0) { response = await fetch(url, Object.assign(fetchOptions, { body, redirect: 'manual', method: 'POST', credentials: 'include' })) diff --git a/utils/chat.js b/utils/chat.js index 097cee4..10bcd09 100644 --- a/utils/chat.js +++ b/utils/chat.js @@ -16,7 +16,7 @@ export async function getChatHistoryGroup (e, num) { } chats = chats.slice(0, num) try { - let mm = await e.group.getMemberMap() + let mm = await e.bot.gml for (const chat of chats) { if (e.adapter === 'shamrock') { if (chat.sender?.user_id === 0) { diff --git a/utils/claude.ai/index.js b/utils/claude.ai/index.js index ba12726..4eeb663 100644 --- a/utils/claude.ai/index.js +++ b/utils/claude.ai/index.js @@ -130,19 +130,14 @@ export class ClaudeAIClient { async sendMessage (text, conversationId, attachments = []) { let body = { - conversation_uuid: conversationId, - organization_uuid: this.organizationId, - text, attachments, - completion: { - incremental: true, - model: 'claude-2.1', - prompt: text, - timezone: 'Asia/Hong_Kong' - } + files: [], + model: 'claude-2.1', + prompt: text, + timezone: 'Asia/Hong_Kong' } let host = Config.claudeAIReverseProxy || 'https://claude.ai' - let url = host + '/api/append_message' + let url = host + `/api/organizations/${this.organizationId}/chat_conversations/${conversationId}/completion` const cycleTLS = await initCycleTLS() let streamDataRes = await cycleTLS(url, { ja3: this.JA3, @@ -160,7 +155,7 @@ export class ClaudeAIClient { let streamData = streamDataRes.body // console.log(streamData) let responseText = '' - let streams = streamData.split('\n\n') + let streams = streamData.split('\n').filter(s => s?.includes('data: ')) for (let s of streams) { let jsonStr = s.replace('data: ', '').trim() try { diff --git a/utils/common.js b/utils/common.js index 3a00052..808be0a 100644 --- a/utils/common.js +++ b/utils/common.js @@ -14,7 +14,7 @@ import { translate } from './translate.js' import uploadRecord from './uploadRecord.js' import Version from './version.js' import fetch, { FormData, fileFromSync } from 'node-fetch' -import https from "https"; +import https from 'https' let pdfjsLib try { pdfjsLib = (await import('pdfjs-dist')).default @@ -1055,10 +1055,14 @@ export async function getOrDownloadFile (destPath, url, ignoreCertificateError = * @param destPath 目标路径,如received/abc.pdf. 目前如果文件名重复会覆盖。 * @param absolute 是否是绝对路径,默认为false,此时拼接在data/chatgpt下 * @param ignoreCertificateError 忽略证书错误 + * @param headers * @returns {Promise} 最终下载文件的存储位置 */ -export async function downloadFile (url, destPath, absolute = false, ignoreCertificateError = true) { +export async function downloadFile (url, destPath, absolute = false, ignoreCertificateError = true, headers) { let init = {} + if (headers) { + init.headers = headers + } if (ignoreCertificateError && url.startsWith('https')) { init.agent = new https.Agent({ rejectUnauthorized: !ignoreCertificateError @@ -1261,3 +1265,52 @@ export async function extractContentFromFile (fileMsgElem, e) { return {} } } + +/** + * generated by ai + * @param email + * @returns {string} + */ +export function maskEmail (email) { + // 使用正则表达式匹配电子邮件地址的用户名和域名部分 + const regex = /^([^@]+)@([^@]+)$/ + const match = email.match(regex) + + if (!match) { + throw new Error('Invalid email format') + } + + // 获取用户名和域名 + const username = match[1] + const domain = match[2] + + // 对用户名部分进行部分打码 + const maskedUsername = maskString(username) + + // 对域名部分进行部分打码 + const maskedDomain = maskString(domain) + + // 构造新的电子邮件地址 + const maskedEmail = maskedUsername + '@' + maskedDomain + + return maskedEmail +} + +/** + * generated by ai + * @param str + * @returns {*|string} + */ +function maskString (str) { + // 如果字符串长度小于等于2,直接返回原字符串 + if (str.length <= 2) { + return str + } + + // 取字符串的前三个字符和后三个字符,中间使用*代替 + const firstThreeChars = str.substring(0, 3) + const lastThreeChars = str.substring(str.length - 3) + const maskedChars = '*'.repeat(str.length - 6) + + return firstThreeChars + maskedChars + lastThreeChars +} diff --git a/utils/config.js b/utils/config.js index 66ccd76..1ac37b2 100644 --- a/utils/config.js +++ b/utils/config.js @@ -176,6 +176,9 @@ const defaultConfig = { // origin: https://generativelanguage.googleapis.com geminiBaseUrl: 'https://gemini.ikechan8370.com', chatglmRefreshToken: '', + sunoSessToken: '', + sunoClientToken: '', + translateSource: 'openai', version: 'v2.7.10' } const _path = process.cwd() diff --git a/utils/face.js b/utils/face.js index 3e96fe4..52d4456 100644 --- a/utils/face.js +++ b/utils/face.js @@ -470,7 +470,7 @@ export async function convertFaces (msg, handleAt = false, e) { let groupCardQQMap = {} if (handleAt) { try { - groupMembers = await e.group.getMemberMap() + groupMembers = e.bot.gml } catch (err) { console.error(`Failed to get group members: ${err}`) } diff --git a/utils/jwt.js b/utils/jwt.js new file mode 100644 index 0000000..1af66f3 --- /dev/null +++ b/utils/jwt.js @@ -0,0 +1,8 @@ +export function decrypt (jwtToken) { + const [encodedHeader, encodedPayload, signature] = jwtToken.split('.') + + const decodedHeader = Buffer.from(encodedHeader, 'base64').toString('utf-8') + const decodedPayload = Buffer.from(encodedPayload, 'base64').toString('utf-8') + + return decodedPayload +} diff --git a/utils/translate.js b/utils/translate.js index 96e1d11..d290a19 100644 --- a/utils/translate.js +++ b/utils/translate.js @@ -1,5 +1,13 @@ import md5 from 'md5' import _ from 'lodash' +import { Config } from './config.js' +import { ChatGPTAPI } from './openai/chatgpt-api.js' +import { newFetch } from './proxy.js' +import { CustomGoogleGeminiClient } from '../client/CustomGoogleGeminiClient.js' +import XinghuoClient from './xinghuo/xinghuo.js' +import {getImg, getMessageById, upsertMessage} from './common.js' +import {QwenApi} from "./alibaba/qwen-api.js"; +import {v4 as uuid} from "uuid"; // 代码参考:https://github.com/yeyang52/yenai-plugin/blob/b50b11338adfa5a4ef93912eefd2f1f704e8b990/model/api/funApi.js#L25 export const translateLangSupports = [ @@ -20,7 +28,7 @@ export const translateLangSupports = [ { code: 'zh-CHS', label: '中文', abbr: '中', alphabet: 'Z' } ] const API_ERROR = '出了点小问题,待会再试试吧' -export async function translate (msg, to = 'auto') { +export async function translateOld (msg, to = 'auto') { let from = 'auto' if (to !== 'auto') to = translateLangSupports.find(item => item.abbr == to)?.code if (!to) return `未找到翻译的语种,支持的语言为:\n${translateLangSupports.map(item => item.abbr).join(',')}\n` @@ -95,3 +103,113 @@ export async function translate (msg, to = 'auto') { return API_ERROR } } + +/** + * + * @param msg 要翻译的 + * @param from 语种 + * @param to 语种 + * @param ai ai来源,支持openai, gemini, xh, qwen + * @returns {Promise<*|string>} + */ +export async function translate (msg, to = 'auto', from = 'auto', ai = Config.translateSource) { + try { + let lang = '中' + if (to !== 'auto') { + lang = translateLangSupports.find(item => item.abbr == to)?.code + } + if (!lang) return `未找到翻译的语种,支持的语言为:\n${translateLangSupports.map(item => item.abbr).join(',')}\n` + // if ai is not in the list, throw error + if (!['openai', 'gemini', 'xh', 'qwen'].includes(ai)) throw new Error('ai来源错误') + let system = `You will be provided with a sentence in the language with language code [${from}], and your task is to translate it into [${lang}]. Just print the result without any other words.` + if (Array.isArray(msg)) { + let result = [] + for (let i = 0; i < msg.length; i++) { + let item = msg[i] + let res = await translate(item, to, from, ai) + result.push(res) + } + return result + } + switch (ai) { + case 'openai': { + let api = new ChatGPTAPI({ + apiBaseUrl: Config.openAiBaseUrl, + apiKey: Config.apiKey, + fetch: newFetch + }) + const res = await api.sendMessage(msg, { + systemMessage: system, + completionParams: { + model: 'gpt-3.5-turbo' + } + }) + return res.text + } + case 'gemini': { + let client = new CustomGoogleGeminiClient({ + key: Config.geminiKey, + model: Config.geminiModel, + baseUrl: Config.geminiBaseUrl, + debug: Config.debug + }) + let option = { + stream: false, + onProgress: (data) => { + if (Config.debug) { + logger.info(data) + } + }, + system + } + let res = await client.sendMessage(msg, option) + return res.text + } + case 'xh': { + let client = new XinghuoClient({ + ssoSessionId: Config.xinghuoToken + }) + let response = await client.sendMessage(msg, { system }) + return response.text + } + case '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 + } + let opts = { + apiKey: Config.qwenApiKey, + debug: false, + systemMessage: system, + completionParams, + fetch: newFetch + } + let client = new QwenApi(opts) + let option = { + timeoutMs: 600000, + completionParams + } + let result + try { + result = await client.sendMessage(msg, option) + } catch (err) { + logger.error(err) + throw new Error(err) + } + return result.text + } + } + } catch (e) { + logger.error(e) + logger.info('基于LLM的翻译失败,转用老版翻译') + return await translateOld(msg, to) + } +} diff --git a/utils/xinghuo/xinghuo.js b/utils/xinghuo/xinghuo.js index 3040f79..ba6e5bb 100644 --- a/utils/xinghuo/xinghuo.js +++ b/utils/xinghuo/xinghuo.js @@ -395,7 +395,7 @@ export default class XinghuoClient { logger.warn('星火设定序列化失败,本次对话不附带设定') } } else { - Prompt = Config.xhPrompt ? [{ role: 'system', content: Config.xhPrompt }] : [] + Prompt = option.system ? [{ role: 'system', content: option.system }] : [] } if (Config.xhPromptEval) { Prompt.forEach(obj => {