From eea0748de77f4600449b811929f25a95eafec608 Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Wed, 21 Feb 2024 14:38:13 +0800 Subject: [PATCH 01/21] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81suno=EF=BC=9A`#?= =?UTF-8?q?suno+prompt`=E6=88=96`#=E5=88=9B=E4=BD=9C=E6=AD=8C=E6=9B=B2+pro?= =?UTF-8?q?mpt`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/draw.js | 8 +- apps/vocal.js | 83 ++++++++++++++++++++ client/SunoClient.js | 142 ++++++++++++++++++++++++++++++++++ client/test/SunoClientTest.js | 11 +++ guoba.support.js | 16 ++++ utils/config.js | 2 + utils/jwt.js | 8 ++ 7 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 apps/vocal.js create mode 100644 client/SunoClient.js create mode 100644 client/test/SunoClientTest.js create mode 100644 utils/jwt.js 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/vocal.js b/apps/vocal.js new file mode 100644 index 0000000..d0d5a7d --- /dev/null +++ b/apps/vocal.js @@ -0,0 +1,83 @@ +import plugin from '../../../lib/plugins/plugin.js' +import { SunoClient } from '../client/SunoClient.js' +import { Config } from '../utils/config.js' +import { downloadFile } from '../utils/common.js' +import common from '../../../lib/common/common.js' + +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 + } + 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() + if (credit < 10) { + tried++ + logger.info(`账户${email}余额不足,尝试下一个账户`) + continue + } + let description = e.msg.replace(/#((创作)?歌曲|suno|Suno)/, '') + let songs = await client.createSong(description) + let messages = ['提示词:' + description] + for (let song of songs) { + messages.push(`歌名:${song.title}, 风格: ${song.metadata.tags}, 长度: ${song.metadata.duration}秒\n歌词:\n${song.metadata.prompt}`) + messages.push(segment.image(song.image_url)) + let videoPath = await downloadFile(song.video_url, `suno/${song.title}.mp4`) + messages.push(segment.video(videoPath)) + } + await e.reply(common.makeForwardMsg(e, messages, '音乐合成结果')) + } + } catch (err) { + console.error(err) + await e.reply('生成失败,请查看日志') + } + } +} diff --git a/client/SunoClient.js b/client/SunoClient.js new file mode 100644 index 0000000..a37c791 --- /dev/null +++ b/client/SunoClient.js @@ -0,0 +1,142 @@ +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.mark('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 = [] + while (!allDone) { + let queryRes = await newFetch(queryUrl, { + headers: { + Authorization: `Bearer ${sess}` + } + }) + if (queryRes.status !== 200) { + throw new Error('Failed to query song') + } + let queryData = await queryRes.json() + logger.debug(queryData) + allDone = queryData.every(clip => clip.status === 'complete') + songs = queryData + await common.sleep(1000) + } + 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.mark('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 4b118a6..ab6bf59 100644 --- a/guoba.support.js +++ b/guoba.support.js @@ -792,6 +792,22 @@ export function supportGuoba () { bottomHelpMessage: '对https://generativelanguage.googleapis.com的反代', component: 'Input' }, + { + 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/config.js b/utils/config.js index beccf06..df31894 100644 --- a/utils/config.js +++ b/utils/config.js @@ -175,6 +175,8 @@ const defaultConfig = { // origin: https://generativelanguage.googleapis.com geminiBaseUrl: 'https://gemini.ikechan8370.com', chatglmRefreshToken: '', + sunoSessToken: '', + sunoClientToken: '', version: 'v2.7.10' } const _path = process.cwd() 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 +} From ba3422cd10a409a377e17b4d9b263cd27d0994b6 Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Wed, 21 Feb 2024 14:43:00 +0800 Subject: [PATCH 02/21] =?UTF-8?q?fix:=20suno=E4=BD=99=E9=A2=9D=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=EF=BC=9A`#suno=E4=BD=99=E9=A2=9D`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/vocal.js | 21 ++++++++++++++++++++- client/SunoClient.js | 4 ++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/vocal.js b/apps/vocal.js index d0d5a7d..84dfa8f 100644 --- a/apps/vocal.js +++ b/apps/vocal.js @@ -48,6 +48,24 @@ export class Vocal extends plugin { 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 tried = 0 + let msg = '' + 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 }) + msg += `用户${email}余额:${credit}\n` + } + await e.reply(msg) + return true + } await e.reply('正在生成,请稍后') try { let sessTokens = Config.sunoSessToken.split(',') @@ -59,12 +77,13 @@ export class Vocal extends plugin { 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 description = e.msg.replace(/#((创作)?歌曲|suno|Suno)/, '') + let songs = await client.createSong(description) let messages = ['提示词:' + description] for (let song of songs) { diff --git a/client/SunoClient.js b/client/SunoClient.js index a37c791..ba79ddb 100644 --- a/client/SunoClient.js +++ b/client/SunoClient.js @@ -17,7 +17,7 @@ export class SunoClient { let lastToken = this.sessToken let payload = decrypt(lastToken) let sid = JSON.parse(payload).sid - logger.mark('sid: ' + 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: { @@ -120,7 +120,7 @@ export class SunoClient { let lastToken = this.sessToken let payload = decrypt(lastToken) let sid = JSON.parse(payload).sid - logger.mark('sid: ' + 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', From c46c8fe4585ff5ff854d170ae08cc35b5bdbf628 Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Wed, 21 Feb 2024 14:44:59 +0800 Subject: [PATCH 03/21] =?UTF-8?q?fix:=20=E5=BE=AA=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/vocal.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/vocal.js b/apps/vocal.js index 84dfa8f..03d0062 100644 --- a/apps/vocal.js +++ b/apps/vocal.js @@ -52,12 +52,10 @@ export class Vocal extends plugin { if (description === '额度' || description === 'credit' || description === '余额') { let sessTokens = Config.sunoSessToken.split(',') let clientTokens = Config.sunoClientToken.split(',') - let tried = 0 let msg = '' - while (tried < sessTokens.length) { - let index = tried - let sess = sessTokens[index] - let clientToken = clientTokens[index] + 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 }) From 76caf5d04083c3c01318c35f360da86a9bf0a079 Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Wed, 21 Feb 2024 14:50:44 +0800 Subject: [PATCH 04/21] =?UTF-8?q?fix:=20=E9=82=AE=E7=AE=B1=E6=89=93?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/vocal.js | 6 ++++-- utils/common.js | 51 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/apps/vocal.js b/apps/vocal.js index 03d0062..be8d969 100644 --- a/apps/vocal.js +++ b/apps/vocal.js @@ -1,7 +1,7 @@ import plugin from '../../../lib/plugins/plugin.js' import { SunoClient } from '../client/SunoClient.js' import { Config } from '../utils/config.js' -import { downloadFile } from '../utils/common.js' +import { downloadFile, maskEmail } from '../utils/common.js' import common from '../../../lib/common/common.js' export class Vocal extends plugin { @@ -59,7 +59,9 @@ export class Vocal extends plugin { let client = new SunoClient({ sessToken: sess, clientToken }) let { credit, email } = await client.queryCredit() logger.info({ credit, email }) - msg += `用户${email}余额:${credit}\n` + msg += `用户: ${maskEmail(email)} 余额:${credit}\n` + msg += '-------------------\n' + msg += 'Notice:每首歌消耗5credit,每次生成2首歌' } await e.reply(msg) return true diff --git a/utils/common.js b/utils/common.js index 3a00052..5adccbc 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 @@ -1261,3 +1261,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 +} From 6560cec87e40fb3ab7be29f5e04e2b81dd0e2711 Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Wed, 21 Feb 2024 14:52:17 +0800 Subject: [PATCH 05/21] =?UTF-8?q?fix:=20=E6=96=87=E6=9C=AC=E4=B9=B1?= =?UTF-8?q?=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/vocal.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/vocal.js b/apps/vocal.js index be8d969..4884ec2 100644 --- a/apps/vocal.js +++ b/apps/vocal.js @@ -60,9 +60,9 @@ export class Vocal extends plugin { let { credit, email } = await client.queryCredit() logger.info({ credit, email }) msg += `用户: ${maskEmail(email)} 余额:${credit}\n` - msg += '-------------------\n' - msg += 'Notice:每首歌消耗5credit,每次生成2首歌' } + msg += '-------------------\n' + msg += 'Notice:每首歌消耗5credit,每次生成2首歌' await e.reply(msg) return true } From 324f447401b48c497d75c29b081c899c2b3d8b9c Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Wed, 21 Feb 2024 14:58:18 +0800 Subject: [PATCH 06/21] =?UTF-8?q?fix:=20=E6=AD=BB=E5=BE=AA=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/vocal.js | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/vocal.js b/apps/vocal.js index 4884ec2..c5780f7 100644 --- a/apps/vocal.js +++ b/apps/vocal.js @@ -93,6 +93,7 @@ export class Vocal extends plugin { messages.push(segment.video(videoPath)) } await e.reply(common.makeForwardMsg(e, messages, '音乐合成结果')) + return true } } catch (err) { console.error(err) From 7635781695ab88be3f045020a6bb3c66038b516a Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Wed, 21 Feb 2024 15:03:12 +0800 Subject: [PATCH 07/21] =?UTF-8?q?fix:=20=E4=BD=99=E9=A2=9D=E4=B8=8D?= =?UTF-8?q?=E8=B6=B3=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/vocal.js | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/vocal.js b/apps/vocal.js index c5780f7..e6ae112 100644 --- a/apps/vocal.js +++ b/apps/vocal.js @@ -95,6 +95,7 @@ export class Vocal extends plugin { await e.reply(common.makeForwardMsg(e, messages, '音乐合成结果')) return true } + await e.reply('所有账户余额不足') } catch (err) { console.error(err) await e.reply('生成失败,请查看日志') From 3561f7c99dfb41b06be22d5ccb9a167459fbe52a Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Wed, 21 Feb 2024 15:09:12 +0800 Subject: [PATCH 08/21] fix: query song add fallback --- client/SunoClient.js | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/client/SunoClient.js b/client/SunoClient.js index ba79ddb..4e99e13 100644 --- a/client/SunoClient.js +++ b/client/SunoClient.js @@ -57,20 +57,27 @@ export class SunoClient { 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 = [] - while (!allDone) { - let queryRes = await newFetch(queryUrl, { - headers: { - Authorization: `Bearer ${sess}` + 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') } - }) - if (queryRes.status !== 200) { - throw new 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) } - let queryData = await queryRes.json() - logger.debug(queryData) - allDone = queryData.every(clip => clip.status === 'complete') - songs = queryData await common.sleep(1000) + timeout-- } return songs } From 4cbca97c5669ffc4a99a2a0faa38d281da03a34b Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Wed, 21 Feb 2024 19:14:23 +0800 Subject: [PATCH 09/21] =?UTF-8?q?fix:=20=E5=87=8F=E5=B0=91=E9=87=8D?= =?UTF-8?q?=E8=AF=95=E6=AC=A1=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- utils/BingDraw.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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' })) From 0cdd2be29aca883c48350010b6ce6434c50c6861 Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Thu, 22 Feb 2024 12:21:23 +0800 Subject: [PATCH 10/21] =?UTF-8?q?fix:=20=E4=BD=BF=E7=94=A8icqq=E5=8F=91?= =?UTF-8?q?=E9=80=81=E8=A7=86=E9=A2=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/management.js | 41 +++++++++++++---------------------------- apps/vocal.js | 10 +++++++--- utils/common.js | 6 +++++- 3 files changed, 25 insertions(+), 32 deletions(-) diff --git a/apps/management.js b/apps/management.js index 3a574c8..0145bcc 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', @@ -237,7 +222,7 @@ export class ChatgptManagement extends plugin { }, { /** 命令正则匹配 */ - reg: '^#(关闭|打开)群聊上下文$', + reg: '^#(chatgpt)?(关闭|打开)群聊上下文$', /** 执行方法 */ fnc: 'enableGroupContext', permission: 'master' @@ -248,16 +233,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' }, @@ -275,7 +260,7 @@ export class ChatgptManagement extends plugin { fnc: 'commandHelp' }, { - reg: '^#语音切换.*', + reg: '^#(chatgpt)?语音切换.*', fnc: 'ttsSwitch', permission: 'master' }, @@ -897,7 +882,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模式了') } @@ -1564,7 +1549,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 index e6ae112..d6adcb8 100644 --- a/apps/vocal.js +++ b/apps/vocal.js @@ -3,6 +3,7 @@ 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) { @@ -87,10 +88,13 @@ export class Vocal extends plugin { let songs = await client.createSong(description) let messages = ['提示词:' + description] for (let song of songs) { - messages.push(`歌名:${song.title}, 风格: ${song.metadata.tags}, 长度: ${song.metadata.duration}秒\n歌词:\n${song.metadata.prompt}`) + 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`) - messages.push(segment.video(videoPath)) + // 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 diff --git a/utils/common.js b/utils/common.js index 5adccbc..808be0a 100644 --- a/utils/common.js +++ b/utils/common.js @@ -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 From ec2e123e724dac28f0982c15fa73177d9e4d3c38 Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Thu, 22 Feb 2024 12:38:33 +0800 Subject: [PATCH 11/21] fix: claude2 --- utils/claude.ai/index.js | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/utils/claude.ai/index.js b/utils/claude.ai/index.js index ba12726..ca290bb 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/${this.organizationId}/chat_conversations/${conversationId}/completion` const cycleTLS = await initCycleTLS() let streamDataRes = await cycleTLS(url, { ja3: this.JA3, From 9ef463fddcb98afe7bb2f5e9c07eb5745052d9af Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Thu, 22 Feb 2024 12:40:47 +0800 Subject: [PATCH 12/21] fix: claude2 url error --- utils/claude.ai/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/claude.ai/index.js b/utils/claude.ai/index.js index ca290bb..d5672e7 100644 --- a/utils/claude.ai/index.js +++ b/utils/claude.ai/index.js @@ -137,7 +137,7 @@ export class ClaudeAIClient { timezone: 'Asia/Hong_Kong' } let host = Config.claudeAIReverseProxy || 'https://claude.ai' - let url = host + `/api/${this.organizationId}/chat_conversations/${conversationId}/completion` + let url = host + `/api/organizations/${this.organizationId}/chat_conversations/${conversationId}/completion` const cycleTLS = await initCycleTLS() let streamDataRes = await cycleTLS(url, { ja3: this.JA3, From e5e85621d90d0e63866ce28999da51313666e57b Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Thu, 22 Feb 2024 13:04:19 +0800 Subject: [PATCH 13/21] fix: claude2 response format --- utils/claude.ai/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/claude.ai/index.js b/utils/claude.ai/index.js index d5672e7..4eeb663 100644 --- a/utils/claude.ai/index.js +++ b/utils/claude.ai/index.js @@ -155,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 { From e1d40ba0090673abf939319ac25cec2b1eca6510 Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Fri, 23 Feb 2024 00:18:04 +0800 Subject: [PATCH 14/21] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0=E5=9F=BA?= =?UTF-8?q?=E4=BA=8ELLM=E7=9A=84=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/chat.js | 3 +- apps/entertainment.js | 2 +- guoba.support.js | 440 ++++++++++++++++++++------------------- utils/config.js | 1 + utils/translate.js | 108 +++++++++- utils/xinghuo/xinghuo.js | 2 +- 6 files changed, 341 insertions(+), 215 deletions(-) diff --git a/apps/chat.js b/apps/chat.js index 22e5567..b682e87 100644 --- a/apps/chat.js +++ b/apps/chat.js @@ -1942,7 +1942,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/entertainment.js b/apps/entertainment.js index 3b888d8..31db478 100644 --- a/apps/entertainment.js +++ b/apps/entertainment.js @@ -56,7 +56,7 @@ export class Entertainment extends plugin { fnc: 'wordcloud_new' }, { - reg: '^#((寄批踢|gpt|GPT)?翻.*|chatgpt翻译帮助)', + reg: '^#((寄批踢|gpt|GPT)?翻[sS]*|chatgpt翻译帮助)', fnc: 'translate' }, { diff --git a/guoba.support.js b/guoba.support.js index ab6bf59..9115d88 100644 --- a/guoba.support.js +++ b/guoba.support.js @@ -27,184 +27,6 @@ export function supportGuoba () { configInfo: { // 配置项 schemas schemas: [ - { - 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: 'toggleMode', label: '触发方式', @@ -217,45 +39,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: '代理服务器地址', @@ -268,6 +57,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' @@ -792,6 +595,221 @@ 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' diff --git a/utils/config.js b/utils/config.js index df31894..5e9dd77 100644 --- a/utils/config.js +++ b/utils/config.js @@ -177,6 +177,7 @@ const defaultConfig = { chatglmRefreshToken: '', sunoSessToken: '', sunoClientToken: '', + translateSource: 'openai', version: 'v2.7.10' } const _path = process.cwd() diff --git a/utils/translate.js b/utils/translate.js index 96e1d11..706da20 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,101 @@ 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 = '中', from = 'auto', ai = Config.translateSource) { + try { + const 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.` + 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 => { From 35ad437df23e53a3e33ae9c22caa9afb07d61eec Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Fri, 23 Feb 2024 00:25:32 +0800 Subject: [PATCH 15/21] =?UTF-8?q?fix:=20=E8=AF=AD=E7=A7=8D=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- utils/translate.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/utils/translate.js b/utils/translate.js index 706da20..c061195 100644 --- a/utils/translate.js +++ b/utils/translate.js @@ -112,9 +112,12 @@ export async function translateOld (msg, to = 'auto') { * @param ai ai来源,支持openai, gemini, xh, qwen * @returns {Promise<*|string>} */ -export async function translate (msg, to = '中', from = 'auto', ai = Config.translateSource) { +export async function translate (msg, to = 'auto', from = 'auto', ai = Config.translateSource) { try { - const lang = translateLangSupports.find(item => item.abbr == to)?.code + 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来源错误') From 4886042e3c74858aa5cc712c48375695fe6c4fc7 Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Fri, 23 Feb 2024 00:32:36 +0800 Subject: [PATCH 16/21] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0=E6=8C=87?= =?UTF-8?q?=E4=BB=A4=E5=88=87=E6=8D=A2=E7=BF=BB=E8=AF=91=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/entertainment.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/apps/entertainment.js b/apps/entertainment.js index 31db478..4722895 100644 --- a/apps/entertainment.js +++ b/apps/entertainment.js @@ -59,6 +59,10 @@ export class Entertainment extends plugin { reg: '^#((寄批踢|gpt|GPT)?翻[sS]*|chatgpt翻译帮助)', fnc: 'translate' }, + { + reg: '^#(chatgpt)?(设置|修改)翻译来源(openai|gemini|星火|通义千问|xh|qwen)$', + fnc: 'translateSource' + }, { reg: '^#ocr', fnc: 'ocr' @@ -187,6 +191,23 @@ ${translateLangLabels} return true } + translateSource (e) { + let command = e.msg + if (command.includes('openai')) { + Config.translateSource = 'openai' + } else if (command.msg.includes('gemini')) { + Config.translateSource = 'gemini' + } else if (command.msg.includes('星火')) { + Config.translateSource = 'xh' + } else if (command.msg.includes('通义千问')) { + Config.translateSource = 'qwen' + } else if (command.msg.includes('xh')) { + Config.translateSource = 'xh' + } else if (command.msg.includes('qwen')) { + Config.translateSource = 'qwen' + } + } + async wordcloud (e) { if (e.isGroup) { let groupId = e.group_id From edafe602c1163486cf58fd4ed5b974a4d928cd8d Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Fri, 23 Feb 2024 00:34:11 +0800 Subject: [PATCH 17/21] =?UTF-8?q?fix:=20=E5=8F=8D=E9=A6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/entertainment.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/entertainment.js b/apps/entertainment.js index 4722895..ba193f2 100644 --- a/apps/entertainment.js +++ b/apps/entertainment.js @@ -205,7 +205,10 @@ ${translateLangLabels} Config.translateSource = 'xh' } else if (command.msg.includes('qwen')) { Config.translateSource = 'qwen' + } else { + e.reply('暂不支持该翻译源') } + e.reply('√成功设置翻译源为' + Config.translateSource) } async wordcloud (e) { From f1b950ce58b796dcf64ba034754736bdaeafd21c Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Fri, 23 Feb 2024 00:35:49 +0800 Subject: [PATCH 18/21] =?UTF-8?q?fix:=20=E7=AC=94=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/entertainment.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/entertainment.js b/apps/entertainment.js index ba193f2..b32fa08 100644 --- a/apps/entertainment.js +++ b/apps/entertainment.js @@ -195,15 +195,15 @@ ${translateLangLabels} let command = e.msg if (command.includes('openai')) { Config.translateSource = 'openai' - } else if (command.msg.includes('gemini')) { + } else if (command.includes('gemini')) { Config.translateSource = 'gemini' - } else if (command.msg.includes('星火')) { + } else if (command.includes('星火')) { Config.translateSource = 'xh' - } else if (command.msg.includes('通义千问')) { + } else if (command.includes('通义千问')) { Config.translateSource = 'qwen' - } else if (command.msg.includes('xh')) { + } else if (command.includes('xh')) { Config.translateSource = 'xh' - } else if (command.msg.includes('qwen')) { + } else if (command.includes('qwen')) { Config.translateSource = 'qwen' } else { e.reply('暂不支持该翻译源') From bf75c002c02c6443b9858427e02207b8de265031 Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Sat, 24 Feb 2024 16:52:01 +0800 Subject: [PATCH 19/21] fix: remove long translation result forward --- apps/entertainment.js | 8 ++++---- utils/translate.js | 9 +++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/entertainment.js b/apps/entertainment.js index b32fa08..1de8d70 100644 --- a/apps/entertainment.js +++ b/apps/entertainment.js @@ -170,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, '翻译结果') diff --git a/utils/translate.js b/utils/translate.js index c061195..d290a19 100644 --- a/utils/translate.js +++ b/utils/translate.js @@ -122,6 +122,15 @@ export async function translate (msg, to = 'auto', from = 'auto', ai = Config.tr // 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({ From cf7da0d2c52fb026d99ee31acc719753bb6a8284 Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Wed, 28 Feb 2024 16:36:25 +0800 Subject: [PATCH 20/21] fix: replace getMemberMap with gml --- apps/chat.js | 6 +++--- utils/chat.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/chat.js b/apps/chat.js index b7de8bf..20497e1 100644 --- a/apps/chat.js +++ b/apps/chat.js @@ -870,8 +870,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 = await this.e.bot.gml let me = mm.get(getUin(e)) || {} let card = me.card let nickname = me.nickname @@ -1580,7 +1580,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 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) { From 58e6201e6e0aaa67f84e094b53ce0482d58075d4 Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Wed, 28 Feb 2024 16:39:57 +0800 Subject: [PATCH 21/21] fix: replace getMemberMap with gml --- apps/chat.js | 2 +- utils/face.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/chat.js b/apps/chat.js index 9fd5cfb..f01f02c 100644 --- a/apps/chat.js +++ b/apps/chat.js @@ -908,7 +908,7 @@ export class chatgpt extends plugin { prompt = msg.trim() try { if (e.isGroup) { - let mm = await this.e.bot.gml + let mm = this.e.bot.gml let me = mm.get(getUin(e)) || {} let card = me.card let nickname = me.nickname 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}`) }