diff --git a/apps/chat.js b/apps/chat.js index b7de8bf..5742189 100644 --- a/apps/chat.js +++ b/apps/chat.js @@ -81,6 +81,7 @@ import { getChatHistoryGroup } from '../utils/chat.js' import { CustomGoogleGeminiClient } from '../client/CustomGoogleGeminiClient.js' import { resizeAndCropImage } from '../utils/dalle.js' import fs from 'fs' +import { ChatGLM4Client } from '../client/ChatGLM4Client.js' const roleMap = { owner: 'group owner', @@ -196,6 +197,12 @@ export class chatgpt extends plugin { reg: '^#星火(搜索|查找)助手', fnc: 'searchxhBot' }, + { + /** 命令正则匹配 */ + reg: '^#glm4[sS]*', + /** 执行方法 */ + fnc: 'glm4' + }, { /** 命令正则匹配 */ reg: '^#qwen[sS]*', @@ -419,6 +426,14 @@ export class chatgpt extends plugin { await redis.del(`CHATGPT:CONVERSATIONS_GEMINI:${e.sender.user_id}`) await this.reply('已结束当前对话,请@我进行聊天以开启新的对话', true) } + } else if (use === 'chatglm4') { + let c = await redis.get(`CHATGPT:CONVERSATIONS_CHATGLM4:${e.sender.user_id}`) + if (!c) { + await this.reply('当前没有开启对话', true) + } else { + await redis.del(`CHATGPT:CONVERSATIONS_CHATGLM4:${e.sender.user_id}`) + await this.reply('已结束当前对话,请@我进行聊天以开启新的对话', true) + } } else if (use === 'bing') { let c = await redis.get(`CHATGPT:CONVERSATIONS_BING:${e.sender.user_id}`) if (!c) { @@ -496,6 +511,14 @@ export class chatgpt extends plugin { await redis.del(`CHATGPT:CONVERSATIONS_GEMINI:${qq}`) await this.reply(`已结束${atUser}的对话,TA仍可以@我进行聊天以开启新的对话`, true) } + } else if (use === 'chatglm4') { + let c = await redis.get(`CHATGPT:CONVERSATIONS_CHATGLM4:${qq}`) + if (!c) { + await this.reply(`当前${atUser}没有开启对话`, true) + } else { + await redis.del(`CHATGPT:CONVERSATIONS_CHATGLM4:${qq}`) + await this.reply(`已结束${atUser}的对话,TA仍可以@我进行聊天以开启新的对话`, true) + } } else if (use === 'bing') { let c = await redis.get(`CHATGPT:CONVERSATIONS_BING:${qq}`) if (!c) { @@ -639,6 +662,18 @@ export class chatgpt extends plugin { } break } + case 'chatglm4': { + let qcs = await redis.keys('CHATGPT:CONVERSATIONS_CHATGLM4:*') + for (let i = 0; i < qcs.length; i++) { + await redis.del(qcs[i]) + // todo clean last message id + if (Config.debug) { + logger.info('delete chatglm4 conversation bind: ' + qcs[i]) + } + deleted++ + } + break + } } await this.reply(`结束了${deleted}个用户的对话。`, true) } @@ -972,24 +1007,8 @@ export class chatgpt extends plugin { } } } - let userSetting = await getUserReplySetting(this.e) let useTTS = !!userSetting.useTTS - let speaker - if (Config.ttsMode === 'vits-uma-genshin-honkai') { - speaker = convertSpeaker(userSetting.ttsRole || Config.defaultTTSRole) - } else if (Config.ttsMode === 'azure') { - speaker = userSetting.ttsRoleAzure || Config.azureTTSSpeaker - } else if (Config.ttsMode === 'voicevox') { - speaker = userSetting.ttsRoleVoiceVox || Config.voicevoxTTSSpeaker - } - // 每个回答可以指定 - let trySplit = prompt.split('回答:') - if (trySplit.length > 1 && speakers.indexOf(convertSpeaker(trySplit[0])) > -1) { - useTTS = true - speaker = convertSpeaker(trySplit[0]) - prompt = trySplit[1] - } const isImg = await getImg(e) if (Config.imgOcr && !!isImg) { let imgOcrText = await getImageOcrText(e) @@ -1138,6 +1157,10 @@ export class chatgpt extends plugin { key = `CHATGPT:CONVERSATIONS_GEMINI:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}` break } + case 'chatglm4': { + key = `CHATGPT:CONVERSATIONS_CHATGLM4:${(e.isGroup && Config.groupMerge) ? e.group_id.toString() : e.sender.user_id}` + break + } } let ctime = new Date() previousConversation = (key ? await redis.get(key) : null) || JSON.stringify({ @@ -1177,6 +1200,7 @@ export class chatgpt extends plugin { await e.reply([element.tag, segment.image(element.url)]) }) } + // chatglm4图片,调整至sendMessage中处理 if (use === 'api' && !chatMessage) { // 字数超限直接返回 return false @@ -1460,6 +1484,10 @@ export class chatgpt extends plugin { return await this.otherMode(e, 'gemini') } + async glm4 (e) { + return await this.otherMode(e, 'chatglm4') + } + async gemini (e) { return await this.otherMode(e, 'gemini') } @@ -2159,6 +2187,15 @@ export class chatgpt extends plugin { } option.system = system return await client.sendMessage(prompt, option) + } else if (use === 'chatglm4') { + const client = new ChatGLM4Client({ + refreshToken: Config.chatglmRefreshToken + }) + let resp = await client.sendMessage(prompt, conversation) + if (resp.image) { + e.reply(segment.image(resp.image), true) + } + return resp } else { // openai api let completionParams = {} diff --git a/apps/management.js b/apps/management.js index 0ee651b..fcf8654 100644 --- a/apps/management.js +++ b/apps/management.js @@ -97,11 +97,11 @@ export class ChatgptManagement extends plugin { fnc: 'useOpenAIAPIBasedSolution', permission: 'master' }, - { - reg: '^#chatgpt切换(ChatGLM|chatglm)$', - fnc: 'useChatGLMSolution', - permission: 'master' - }, + // { + // reg: '^#chatgpt切换(ChatGLM|chatglm)$', + // fnc: 'useChatGLMSolution', + // permission: 'master' + // }, { reg: '^#chatgpt切换API3$', fnc: 'useReversedAPIBasedSolution2', @@ -152,6 +152,11 @@ export class ChatgptManagement extends plugin { fnc: 'useQwenSolution', permission: 'master' }, + { + reg: '^#chatgpt切换(智谱|智谱清言|ChatGLM|ChatGLM4|chatglm)$', + fnc: 'useGLM4Solution', + permission: 'master' + }, { reg: '^#chatgpt(必应|Bing)切换', fnc: 'changeBingTone', @@ -1019,6 +1024,16 @@ azure语音:Azure 语音是微软 Azure 平台提供的一项语音服务, } } + async useGLM4Solution () { + let use = await redis.get('CHATGPT:USE') + if (use !== 'chatglm4') { + await redis.set('CHATGPT:USE', 'chatglm4') + await this.reply('已切换到基于ChatGLM的解决方案') + } else { + await this.reply('当前已经是ChatGLM模式了') + } + } + async changeBingTone (e) { let tongStyle = e.msg.replace(/^#chatgpt(必应|Bing)切换/, '') if (!tongStyle) { diff --git a/client/ChatGLM4Client.js b/client/ChatGLM4Client.js new file mode 100644 index 0000000..b7031c5 --- /dev/null +++ b/client/ChatGLM4Client.js @@ -0,0 +1,184 @@ +import { BaseClient } from './BaseClient.js' +import https from 'https' +import { Config } from '../utils/config.js' +import { createParser } from 'eventsource-parser' + +const BASEURL = 'https://chatglm.cn/chatglm/backend-api/assistant/stream' + +export class ChatGLM4Client extends BaseClient { + constructor (props) { + super(props) + this.baseUrl = props.baseUrl || BASEURL + this.supportFunction = false + this.debug = props.debug + this._refreshToken = props.refreshToken + } + + async getAccessToken (refreshToken = this._refreshToken) { + if (redis) { + let lastToken = await redis.get('CHATGPT:CHATGLM4_ACCESS_TOKEN') + if (lastToken) { + this._accessToken = lastToken + // todo check token through user info endpoint + return + } + } + let res = await fetch('https://chatglm.cn/chatglm/backend-api/v1/user/refresh', { + method: 'POST', + body: '{}', + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + Origin: 'https://www.chatglm.cn', + Referer: 'https://www.chatglm.cn/main/detail', + Authorization: `Bearer ${refreshToken}` + } + }) + let tokenRsp = await res.json() + let token = tokenRsp?.result?.accessToken + if (token) { + this._accessToken = token + redis && await redis.set('CHATGPT:CHATGLM4_ACCESS_TOKEN', token, { EX: 7000 }) + // accessToken will expire in 2 hours + } + } + + // todo https://chatglm.cn/chatglm/backend-api/v3/user/info query remain times + /** + * + * @param text + * @param {{conversationId: string?, stream: boolean?, onProgress: function?, image: string?}} opt + * @returns {Promise<{conversationId: string?, parentMessageId: string?, text: string, id: string, image: string?}>} + */ + async sendMessage (text, opt = {}) { + await this.getAccessToken() + if (!this._accessToken) { + throw new Error('accessToken for www.chatglm.cn not set') + } + let { conversationId, onProgress } = opt + const body = { + assistant_id: '65940acff94777010aa6b796', // chatglm4 + conversation_id: conversationId || '', + meta_data: { + is_test: false, + input_question_type: 'xxxx', + channel: '' + }, + messages: [ + { + role: 'user', + content: [ + { + type: 'text', + text + } + ] + } + ] + } + let conversationResponse + let statusCode + let messageId + let image + let requestP = new Promise((resolve, reject) => { + let option = { + method: 'POST', + headers: { + accept: 'text/event-stream', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + authorization: `Bearer ${this._accessToken}`, + 'content-type': 'application/json', + referer: 'https://www.chatglm.cn/main/alltoolsdetail', + origin: 'https://www.chatglm.cn' + }, + referrer: 'https://www.chatglm.cn/main/alltoolsdetail' + } + const req = https.request(BASEURL, option, (res) => { + statusCode = res.statusCode + let response + + function onMessage (data) { + try { + const convoResponseEvent = JSON.parse(data) + conversationResponse = convoResponseEvent + if (convoResponseEvent.conversation_id) { + conversationId = convoResponseEvent.conversation_id + } + + if (convoResponseEvent.id) { + messageId = convoResponseEvent.id + } + + const partialResponse = + convoResponseEvent?.parts?.[0] + if (partialResponse) { + if (Config.debug) { + logger.info(JSON.stringify(convoResponseEvent)) + } + response = partialResponse + if (onProgress && typeof onProgress === 'function') { + onProgress(partialResponse) + } + } + let content = convoResponseEvent?.content[0] + if (content.type === 'image' && content.status === 'finish') { + image = content.image[0].image_url + } + if (convoResponseEvent.status === 'finish') { + resolve({ + error: null, + response, + conversationId, + messageId, + conversationResponse, + image + }) + } + } 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() + reject(resString) + }) + }) + req.on('error', (err) => { + reject(err) + }) + + req.on('timeout', () => { + req.destroy() + reject(new Error('Request time out')) + }) + + req.write(JSON.stringify(body)) + req.end() + }) + const res = await requestP + return { + text: res?.response?.content[0]?.text, + conversationId: res.conversationId, + id: res.messageId, + image, + raw: res?.response + } + } +} diff --git a/client/test/ChatGLM4ClientTest.js b/client/test/ChatGLM4ClientTest.js new file mode 100644 index 0000000..7d6b122 --- /dev/null +++ b/client/test/ChatGLM4ClientTest.js @@ -0,0 +1,17 @@ +import { ChatGLM4Client } from '../ChatGLM4Client.js' + +async function sendMsg () { + const client = new ChatGLM4Client({ + refreshToken: '', + debug: true + }) + let res = await client.sendMessage('你好啊') + console.log(res) +} +// global.redis = null +// global.logger = { +// info: console.log, +// warn: console.warn, +// error: console.error +// } +// sendMsg() diff --git a/client/GoogleGeminiClientTest.js b/client/test/GoogleGeminiClientTest.js similarity index 69% rename from client/GoogleGeminiClientTest.js rename to client/test/GoogleGeminiClientTest.js index d55bb79..c1895d0 100644 --- a/client/GoogleGeminiClientTest.js +++ b/client/test/GoogleGeminiClientTest.js @@ -1,4 +1,4 @@ -import { GoogleGeminiClient } from './GoogleGeminiClient.js' +import { GoogleGeminiClient } from '../GoogleGeminiClient.js' async function test () { const client = new GoogleGeminiClient({ diff --git a/guoba.support.js b/guoba.support.js index f408b08..fe8303b 100644 --- a/guoba.support.js +++ b/guoba.support.js @@ -514,6 +514,16 @@ export function supportGuoba () { bottomHelpMessage: '使用GPT-4,注意试用配额较低,如果用不了就关掉', component: 'Switch' }, + { + label: '以下为智谱清言(ChatGLM)方式的配置。', + component: 'Divider' + }, + { + field: 'chatglmRefreshToken', + label: 'refresh token', + bottomHelpMessage: 'chatglm_refresh_token 6个月有效期', + component: 'Input' + }, { label: '以下为Slack Claude方式的配置', component: 'Divider' diff --git a/utils/config.js b/utils/config.js index 4705aa5..1332ff6 100644 --- a/utils/config.js +++ b/utils/config.js @@ -169,7 +169,8 @@ const defaultConfig = { geminiPrompt: 'You are Gemini. Your answer shouldn\'t be too verbose. Prefer to answer in Chinese.', // origin: https://generativelanguage.googleapis.com geminiBaseUrl: 'https://gemini.ikechan8370.com', - version: 'v2.7.8' + chatglmRefreshToken: '', + version: 'v2.7.9' } const _path = process.cwd() let config = {}