Merge branch 'v2' into v2

This commit is contained in:
ifeif 2024-03-03 00:36:26 +08:00 committed by GitHub
commit fe4288a3d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 761 additions and 90 deletions

View file

@ -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') {

View file

@ -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 {

View file

@ -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

View file

@ -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,

108
apps/vocal.js Normal file
View file

@ -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('生成失败,请查看日志')
}
}
}

149
client/SunoClient.js Normal file
View file

@ -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
}
}
}

View file

@ -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()

View file

@ -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'

View file

@ -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' }))

View file

@ -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) {

View file

@ -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 {

View file

@ -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<string>} 最终下载文件的存储位置
*/
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
}

View file

@ -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()

View file

@ -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}`)
}

8
utils/jwt.js Normal file
View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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 => {