diff --git a/apps/chat.js b/apps/chat.js index 0031748..122f31d 100644 --- a/apps/chat.js +++ b/apps/chat.js @@ -35,6 +35,7 @@ import { ChatgptManagement } from './management.js' import { getPromptByName } from '../utils/prompts.js' import Translate from '../utils/baiduTranslate.js' import emojiStrip from 'emoji-strip' +import XinghuoClient from "../utils/xinghuo/xinghuo.js"; try { await import('keyv') } catch (err) { @@ -115,6 +116,12 @@ export class chatgpt extends plugin { /** 执行方法 */ fnc: 'claude' }, + { + /** 命令正则匹配 */ + reg: '^#xh[sS]*', + /** 执行方法 */ + fnc: 'xh' + }, { /** 命令正则匹配 */ reg: toggleMode === 'at' ? '^[^#][sS]*' : '^#chat[^gpt][sS]*', @@ -239,6 +246,11 @@ export class chatgpt extends plugin { await e.reply('claude对话已结束') return } + if (use === 'xh') { + await redis.del(`CHATGPT:CONVERSATIONS_XH:${e.sender.user_id}`) + await e.reply('星火对话已结束') + return + } let ats = e.message.filter(m => m.type === 'at') if (ats.length === 0) { if (use === 'api3') { @@ -389,6 +401,17 @@ export class chatgpt extends plugin { } break } + case 'xh': { + let cs = await redis.keys('CHATGPT:CONVERSATIONS_XH:*') + for (let i = 0; i < cs.length; i++) { + await redis.del(cs[i]) + if (Config.debug) { + logger.info('delete slack conversation of qq: ' + cs[i]) + } + deleted++ + } + break + } case 'bing': { let cs = await redis.keys('CHATGPT:CONVERSATIONS_BING:*') let we = await redis.keys('CHATGPT:WRONG_EMOTION:*') @@ -929,6 +952,10 @@ export class chatgpt extends plugin { key = `CHATGPT:CONVERSATIONS_BROWSER:${e.sender.user_id}` break } + case 'xh': { + key = `CHATGPT:CONVERSATIONS_XH:${e.sender.user_id}` + break + } } let ctime = new Date() previousConversation = (key ? await redis.get(key) : null) || JSON.stringify({ @@ -1351,7 +1378,7 @@ export class chatgpt extends plugin { let ats = e.message.filter(m => m.type === 'at') if (!e.atme && ats.length > 0) { if (Config.debug) { - logger.mark('艾特别人了,没艾特我,忽略#bing') + logger.mark('艾特别人了,没艾特我,忽略#claude') } return false } @@ -1362,6 +1389,28 @@ export class chatgpt extends plugin { await this.abstractChat(e, prompt, 'claude') return true } + async xh (e) { + if (!e.isMaster && e.isPrivate && !Config.enablePrivateChat) { + // await this.reply('ChatGpt私聊通道已关闭。') + return false + } + if (!Config.allowOtherMode) { + return false + } + let ats = e.message.filter(m => m.type === 'at') + if (!e.atme && ats.length > 0) { + if (Config.debug) { + logger.mark('艾特别人了,没艾特我,忽略#xh') + } + return false + } + let prompt = _.replace(e.raw_message.trimStart(), '#xh', '').trim() + if (prompt.length === 0) { + return false + } + await this.abstractChat(e, prompt, 'xh') + return true + } async cacheContent (e, use, content, prompt, quote = [], mood = '', suggest = '', imgUrls = []) { let cacheData = { file: '', cacheUrl: Config.cacheUrl, status: '' } @@ -1744,6 +1793,17 @@ export class chatgpt extends plugin { text } } + case 'xh': { + const ssoSessionId = Config.xinghuoToken + if (!ssoSessionId) { + throw new Error('未绑定星火token,请使用#chatgpt设置星火token命令绑定token。(获取对话页面的ssoSessionId cookie值)') + } + let client = new XinghuoClient({ + ssoSessionId + }) + let response = await client.sendMessage(prompt, conversation?.conversationId) + return response + } default: { let completionParams = {} if (Config.model) { diff --git a/apps/help.js b/apps/help.js index 287a2cb..e45f50e 100644 --- a/apps/help.js +++ b/apps/help.js @@ -13,8 +13,8 @@ let helpData = [ }, { icon: 'chat', - title: '#chat1/#chat3/#chatglm/#bing', - desc: '分别使用API/API3/ChatGLM/Bing模式与机器人聊天,无论主人设定了何种全局模式' + title: '#chat1/#chat3/#chatglm/#bing/#claude/#xh', + desc: '分别使用API/API3/ChatGLM/Bing/Claude/星火模式与机器人聊天,无论主人设定了何种全局模式' }, { icon: 'chat-private', @@ -191,6 +191,11 @@ let helpData = [ title: '#chatgpt设置APIKey', desc: '设置APIKey' }, + { + icon: 'key', + title: '#chatgpt设置星火token', + desc: '设置星火ssoSessionId(对话页面的ssoSessionId cookie值)' + }, { icon: 'eat', title: '#chatgpt设置(API|Sydney)设定', diff --git a/apps/management.js b/apps/management.js index c913f8e..27585e8 100644 --- a/apps/management.js +++ b/apps/management.js @@ -102,6 +102,11 @@ export class ChatgptManagement extends plugin { fnc: 'useSlackClaudeBasedSolution', permission: 'master' }, + { + reg: '^#chatgpt切换星火$', + fnc: 'useXinghuoBasedSolution', + permission: 'master' + }, { reg: '^#chatgpt(必应|Bing)切换', fnc: 'changeBingTone', @@ -149,6 +154,11 @@ export class ChatgptManagement extends plugin { fnc: 'setAPIPromptPrefix', permission: 'master' }, + { + reg: '^#chatgpt设置星火token', + fnc: 'setXinghuoToken', + permission: 'master' + }, { reg: '^#chatgpt设置(Bing|必应|Sydney|悉尼|sydney|bing)设定', fnc: 'setBingPromptPrefix', @@ -805,6 +815,16 @@ export class ChatgptManagement extends plugin { } } + async useXinghuoBasedSolution () { + let use = await redis.get('CHATGPT:USE') + if (use !== 'xh') { + await redis.set('CHATGPT:USE', 'xh') + await this.reply('已切换到基于星火的解决方案') + } else { + await this.reply('当前已经是星火模式了') + } + } + async changeBingTone (e) { let tongStyle = e.msg.replace(/^#chatgpt(必应|Bing)切换/, '') if (!tongStyle) { @@ -1083,6 +1103,21 @@ export class ChatgptManagement extends plugin { this.finish('saveAPIKey') } + async setXinghuoToken () { + this.setContext('saveXinghuoToken') + await this.reply('请发送星火的ssoSessionId', true) + return false + } + + async saveXinghuoToken () { + if (!this.e.msg) return + let token = this.e.msg + // todo + Config.xinghuoToken = token + await this.reply('星火ssoSessionId设置成功', true) + this.finish('saveXinghuoToken') + } + async setAPIPromptPrefix (e) { this.setContext('saveAPIPromptPrefix') await this.reply('请发送用于API模式的设定', true) diff --git a/guoba.support.js b/guoba.support.js index 1d90bba..ef5957e 100644 --- a/guoba.support.js +++ b/guoba.support.js @@ -555,6 +555,16 @@ export function supportGuoba () { bottomHelpMessage: '如 http://localhost:8080', component: 'Input' }, + { + label: '以下为星火方式的配置', + component: 'Divider' + }, + { + field: 'xinghuoToken', + label: '星火Cookie', + bottomHelpMessage: '获取对话页面的ssoSessionId cookie。不要带等号和分号', + component: 'Input' + }, { label: '以下为杂七杂八的配置', component: 'Divider' diff --git a/package.json b/package.json index 37ee843..984e99f 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,11 @@ "chatgpt": "^5.1.1", "delay": "^5.0.0", "diff": "^5.1.0", + "emoji-strip": "^1.0.1", "eventsource": "^2.0.2", "eventsource-parser": "^1.0.0", "fastify": "^4.13.0", + "form-data": "^4.0.0", "https-proxy-agent": "5.0.1", "keyv": "^4.5.2", "keyv-file": "^0.2.0", @@ -26,8 +28,7 @@ "random": "^4.1.0", "undici": "^5.21.0", "uuid": "^9.0.0", - "ws": "^8.13.0", - "emoji-strip": "^1.0.1" + "ws": "^8.13.0" }, "optionalDependencies": { "@node-rs/jieba": "^1.6.2", diff --git a/utils/config.js b/utils/config.js index 3ee11a7..7e8af1c 100644 --- a/utils/config.js +++ b/utils/config.js @@ -47,6 +47,7 @@ const defaultConfig = { apiForceUseReverse: false, plus: false, useGPT4: false, + xinghuoToken: '', promptPrefixOverride: 'Your answer shouldn\'t be too verbose. Prefer to answer in Chinese.', assistantLabel: 'ChatGPT', // thinkingTips: true, diff --git a/utils/xinghuo/xinghuo.js b/utils/xinghuo/xinghuo.js new file mode 100644 index 0000000..2160607 --- /dev/null +++ b/utils/xinghuo/xinghuo.js @@ -0,0 +1,151 @@ +import fetch from 'node-fetch' +import { Config } from '../config.js' +import { createParser } from 'eventsource-parser' +import https from 'https' + +const referer = atob('aHR0cHM6Ly94aW5naHVvLnhmeXVuLmNuL2NoYXQ/aWQ9') +const origin = atob('aHR0cHM6Ly94aW5naHVvLnhmeXVuLmNu') +const createChatUrl = atob('aHR0cHM6Ly94aW5naHVvLnhmeXVuLmNuL2lmbHlncHQvdS9jaGF0LWxpc3QvdjEvY3JlYXRlLWNoYXQtbGlzdA==') +const chatUrl = atob('aHR0cHM6Ly94aW5naHVvLnhmeXVuLmNuL2lmbHlncHQtY2hhdC91L2NoYXRfbWVzc2FnZS9jaGF0') +let FormData +try { + FormData = (await import('form-data')).default +} catch (err) { + logger.warn('未安装form-data,无法使用星火模式') +} +export default class XinghuoClient { + constructor (opts) { + this.ssoSessionId = opts.ssoSessionId + this.headers = { + Referer: referer, + Cookie: 'ssoSessionId=' + this.ssoSessionId + ';', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/113.0.5672.69 Mobile/15E148 Safari/604.1', + Origin: origin + } + } + + async sendMessage (prompt, chatId) { + if (!FormData) { + throw new Error('缺少依赖:form-data。请安装依赖后重试') + } + if (!chatId) { + chatId = (await this.createChatList()).chatListId + } + let requestP = new Promise((resolve, reject) => { + let formData = new FormData() + formData.setBoundary('----WebKitFormBoundarycATE2QFHDn9ffeWF') + formData.append('clientType', '2') + formData.append('chatId', chatId) + formData.append('text', prompt) + let randomNumber = Math.floor(Math.random() * 1000) + let fd = '439' + randomNumber.toString().padStart(3, '0') + formData.append('fd', fd) + this.headers.Referer = referer + chatId + let option = { + method: 'POST', + headers: Object.assign(this.headers, { + Accept: 'text/event-stream', + 'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundarycATE2QFHDn9ffeWF' + }), + // body: formData, + referrer: this.headers.Referer + } + let statusCode + const req = https.request(chatUrl, option, (res) => { + statusCode = res.statusCode + if (statusCode !== 200) { + logger.error('星火statusCode:' + statusCode) + } + let response = '' + function onMessage (data) { + // console.log(data) + if (data === '') { + return resolve({ + error: null, + response + }) + } + try { + if (data) { + response += atob(data.trim()) + if (Config.debug) { + logger.info(response) + } + } + } catch (err) { + console.warn('fetchSSE onMessage unexpected error', err) + reject(err) + } + } + + const parser = createParser((event) => { + if (event.type === 'event') { + onMessage(event.data) + } + }) + const errBody = [] + res.on('data', (chunk) => { + if (statusCode === 200) { + let str = chunk.toString() + parser.feed(str) + } + errBody.push(chunk) + }) + + // const body = [] + // res.on('data', (chunk) => body.push(chunk)) + res.on('end', () => { + const resString = Buffer.concat(errBody).toString() + // logger.info({ resString }) + reject(resString) + }) + }) + formData.pipe(req) + req.on('error', (err) => { + logger.error(err) + reject(err) + }) + req.on('timeout', () => { + req.destroy() + reject(new Error('Request time out')) + }) + // req.write(formData.stringify()) + req.end() + }) + const { response } = await requestP + // logger.info(response) + // let responseText = atob(response) + return { + conversationId: chatId, + text: response + } + } + + async createChatList () { + let createChatListRes = await fetch(createChatUrl, { + method: 'POST', + headers: Object.assign(this.headers, { + 'Content-Type': 'application/json' + }), + body: '{}' + }) + if (createChatListRes.status !== 200) { + let errorRes = await createChatListRes.text() + let errorText = '星火对话创建失败:' + errorRes + logger.error(errorText) + throw new Error(errorText) + } + createChatListRes = await createChatListRes.json() + if (createChatListRes.data?.id) { + logger.info('星火对话创建成功:' + createChatListRes.data.id) + } + return { + chatListId: createChatListRes.data?.id, + title: createChatListRes.data?.title + } + } +} + +function atob (s) { + return Buffer.from(s, 'base64').toString() +}