mirror of
https://github.com/ikechan8370/chatgpt-plugin.git
synced 2025-12-16 13:27:08 +00:00
feat: 支持suno:#suno+prompt或#创作歌曲+prompt
This commit is contained in:
parent
63edc9403c
commit
eea0748de7
7 changed files with 268 additions and 2 deletions
|
|
@ -324,10 +324,14 @@ export class dalle extends plugin {
|
||||||
const index = bingTokens.findIndex(element => element.Token === bingToken)
|
const index = bingTokens.findIndex(element => element.Token === bingToken)
|
||||||
bingTokens[index].Usage += 1
|
bingTokens[index].Usage += 1
|
||||||
await redis.set('CHATGPT:BING_TOKENS', JSON.stringify(bingTokens))
|
await redis.set('CHATGPT:BING_TOKENS', JSON.stringify(bingTokens))
|
||||||
|
let cookie
|
||||||
|
if (bingToken.includes('=')) {
|
||||||
|
cookie = bingToken
|
||||||
|
}
|
||||||
let client = new BingDrawClient({
|
let client = new BingDrawClient({
|
||||||
baseUrl: Config.sydneyReverseProxy,
|
baseUrl: Config.sydneyReverseProxy,
|
||||||
userToken: bingToken
|
userToken: bingToken,
|
||||||
|
cookies: cookie
|
||||||
})
|
})
|
||||||
await redis.set(`CHATGPT:DRAW:${e.sender.user_id}`, 'c', { EX: 30 })
|
await redis.set(`CHATGPT:DRAW:${e.sender.user_id}`, 'c', { EX: 30 })
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
83
apps/vocal.js
Normal file
83
apps/vocal.js
Normal file
|
|
@ -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('生成失败,请查看日志')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
142
client/SunoClient.js
Normal file
142
client/SunoClient.js
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
client/test/SunoClientTest.js
Normal file
11
client/test/SunoClientTest.js
Normal 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()
|
||||||
|
|
@ -792,6 +792,22 @@ export function supportGuoba () {
|
||||||
bottomHelpMessage: '对https://generativelanguage.googleapis.com的反代',
|
bottomHelpMessage: '对https://generativelanguage.googleapis.com的反代',
|
||||||
component: 'Input'
|
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: '以下为杂七杂八的配置',
|
label: '以下为杂七杂八的配置',
|
||||||
component: 'Divider'
|
component: 'Divider'
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,8 @@ const defaultConfig = {
|
||||||
// origin: https://generativelanguage.googleapis.com
|
// origin: https://generativelanguage.googleapis.com
|
||||||
geminiBaseUrl: 'https://gemini.ikechan8370.com',
|
geminiBaseUrl: 'https://gemini.ikechan8370.com',
|
||||||
chatglmRefreshToken: '',
|
chatglmRefreshToken: '',
|
||||||
|
sunoSessToken: '',
|
||||||
|
sunoClientToken: '',
|
||||||
version: 'v2.7.10'
|
version: 'v2.7.10'
|
||||||
}
|
}
|
||||||
const _path = process.cwd()
|
const _path = process.cwd()
|
||||||
|
|
|
||||||
8
utils/jwt.js
Normal file
8
utils/jwt.js
Normal 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
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue