Skip to content

Commit

Permalink
feat: support chatglm.cn
Browse files Browse the repository at this point in the history
  • Loading branch information
ikechan8370 committed Jan 17, 2024
1 parent 8bdf9a3 commit c69358c
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 23 deletions.
69 changes: 53 additions & 16 deletions apps/chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -196,6 +197,12 @@ export class chatgpt extends plugin {
reg: '^#星火(搜索|查找)助手',
fnc: 'searchxhBot'
},
{
/** 命令正则匹配 */
reg: '^#glm4[sS]*',
/** 执行方法 */
fnc: 'glm4'
},
{
/** 命令正则匹配 */
reg: '^#qwen[sS]*',
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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')
}
Expand Down Expand Up @@ -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 = {}
Expand Down
25 changes: 20 additions & 5 deletions apps/management.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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) {
Expand Down
184 changes: 184 additions & 0 deletions client/ChatGLM4Client.js
Original file line number Diff line number Diff line change
@@ -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
}
}
}
17 changes: 17 additions & 0 deletions client/test/ChatGLM4ClientTest.js
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GoogleGeminiClient } from './GoogleGeminiClient.js'
import { GoogleGeminiClient } from '../GoogleGeminiClient.js'

async function test () {
const client = new GoogleGeminiClient({
Expand Down
Loading

0 comments on commit c69358c

Please sign in to comment.