mirror of
https://github.com/ikechan8370/chatgpt-plugin.git
synced 2025-12-16 05:17:10 +00:00
feat: support chatglm.cn
This commit is contained in:
parent
8bdf9a3b6d
commit
c69358cd80
7 changed files with 287 additions and 23 deletions
69
apps/chat.js
69
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 = {}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
184
client/ChatGLM4Client.js
Normal file
184
client/ChatGLM4Client.js
Normal file
|
|
@ -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
client/test/ChatGLM4ClientTest.js
Normal file
17
client/test/ChatGLM4ClientTest.js
Normal file
|
|
@ -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()
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { GoogleGeminiClient } from './GoogleGeminiClient.js'
|
||||
import { GoogleGeminiClient } from '../GoogleGeminiClient.js'
|
||||
|
||||
async function test () {
|
||||
const client = new GoogleGeminiClient({
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue