首先先简单的建一个koa的nodejs服务器 用于转发接口和网络处理
const app = new Koa()
const router = new Router()
app.use(cors({ credentials: true }))
app.use(bodyParser())
//代码下面写
useChatGpt(router)
app.use(router.routes()).use(router.allowedMethods())
app.listen(3004)
useChatGpt源代码
// 导入必要的依赖库
import { ChatGPTAPI } from 'chatgpt'
import { nanoid } from 'nanoid'
import axios from 'axios'
import fnv1a from '@sindresorhus/fnv1a'
import { sendMsg, getTokenFunc } from './feishu.js'
import { logger } from './log.js'
// 确定当前是否在本地环境
const isLocal = process.env?.NODE_ENV === 'local'
// 设置应用ID和应用密钥
const app_id = 'cli_a3677e7712799013'
const app_secret = 'PyAHvXwQP68zPJmbq07mGcvdwc3xteOP'
// 获取访问令牌
const getToken = getTokenFunc({ app_id, app_secret })
// 封装发送飞书消息的方法
const sendFsMsg = async (params) => sendMsg({ ...params, token: isLocal ? null : await getToken() })
// 设置 ChatGPT 的 API 密钥
const KEY = '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'
// 初始化 ChatGPT 池子
const ChatGPTPool = {
[KEY]: {
api: new ChatGPTAPI({ apiKey: KEY }),
waiting: false,
preRes: null,
},
}
// 初始化查询队列
const QueryQueue = []
// 辅助函数:去除消息中的特殊字符和关键字
const trimText = (text) => text.replace(/addKey|removeKey|@_user_\d+/gi, '').trim()
// 发送 ChatGPT API 密钥列表
const sendKeyList = ({ chat_id }) => {
const content = JSON.stringify({
zh_cn: {
title: `APIKeys`,
content: [
[
{
tag: 'text',
text: Object.keys(ChatGPTPool).join('\n'),
},
],
],
},
})
sendFsMsg({
receive_id_type: 'chat_id',
receive_id: chat_id,
content,
msg_type: 'post',
})
}
// 用于预处理数据并根据规则决定是否发送到QueryQueue,如果没有发送到QueryQueue,则返回false。
const preProcess = ({ data }) => {
// 获取事件的消息内容和会话ID。
const { content, chat_id } = data.event.message
// 从消息内容中解析出文本。
const { text: _text } = JSON.parse(content)
// 对文本进行预处理,去除开头和结尾的空格。
const text = trimText(_text)
// 定义用于匹配关键词的正则表达式。
const keyWords = /addKey|removeKey|^@_all/i
// 如果文本中没有关键词,则返回false。
if (!keyWords.test(_text)) {
return false
} else {
// 否则,获取API密钥。
const apiKey = text
// 如果文本中包含“addKey”关键词且该API密钥不存在于ChatGPTPool中,则将其添加到ChatGPTPool中并发送密钥列表给用户。
if (/addKey/i.test(_text) && !ChatGPTPool[apiKey]) {
ChatGPTPool[apiKey] = {
api: new ChatGPTAPI({ apiKey }),
waiting: false,
}
sendKeyList({ chat_id })
} else if (/removeKey/i.test(_text)) {
// 如果文本中包含“removeKey”关键词,则从ChatGPTPool中删除该API密钥并发送密钥列表给用户。
delete ChatGPTPool[apiKey]
sendKeyList({ chat_id })
}
return true
}
}
// 用于从QueryQueue中获取数据并调用聊天机器人API进行聊天,如果没有数据,则立即返回。
const callGPTWithRetry = () => {
if (!QueryQueue.length) return
// 获取队列中的第一个数据。
const data = QueryQueue[0]
// 获取用户ID。
const { user_id } = data.event.sender.sender_id
// 计算哈希值并获取聊天机器人对象。
const gptIndex = Number(fnv1a(user_id)) % Object.keys(ChatGPTPool).length
const gpt = Object.values(ChatGPTPool)[gptIndex]
// 如果聊天机器人正在等待回复,则立即返回。
if (gpt.waiting) return
// 将该 GPT 模型标记为正在处理任务,并从队列中移除该任务
gpt.waiting = true
QueryQueue.shift()
// 解析该任务的内容,并将其发送给聊天 GPT 的 API
// 获取消息内容和会话ID。
const { content, chat_id } = data.event.message
// 从消息内容中解析出文本。
const { text } = JSON.parse(content)
// 最多重试 7 次,如果重试次数超过这个值,则放弃该任务,并回复一个“请求超时”的消息
const MAX_RETRY = 7
let retryCount = 0
// 记录日志
logger.info(
{ GELF: true, _queryQueueLen: QueryQueue.length, _data: data, _retryCount: retryCount },
`callGPTWithRetry`,
)
// 发送回复消息
const sendReply = ({ res, req }) => {
// 将返回结果保存到 GPT 模型中,并标记该 GPT 模型为可用状态
gpt.preRes = res
gpt.waiting = false
// 组装回复消息
const title = trimText(text)
const content = JSON.stringify({
zh_cn: {
content: [
[
{
tag: 'at',
user_id,
user_name: 'user',
},
{
tag: 'text',
text: ` 回应下:${title.slice(0, 200) + `${title.length > 200 ? '...' : ''}`}`,
},
],
[
{
tag: 'text',
text: '',
},
],
[
{
tag: 'text',
text: !!res ? res.text : '请求超时,请稍后重试!',
},
],
],
},
})
// 发送回复消息到飞书
sendFsMsg({
receive_id_type: 'chat_id',
receive_id: chat_id,
content,
msg_type: 'post',
})
// 记录日志,然后继续处理队列中的下一个任务
callGPTWithRetry()
}
// 发送聊天 GPT 的消息,并处理返回结果
const sendChatGptMsg = () => {
if (retryCount > MAX_RETRY) {
// 如果重试次数超过了最大重试次数,则认为该任务已经失败,将该 GPT 模型删除,并发送一个空的返回结果
const failedKey = Object.keys(ChatGPTPool)[gptIndex]
const allKeys = Object.keys(ChatGPTPool)
logger.info({ GELF: true, _key: failedKey, _allKeys: allKeys }, `Over max retryCount`)
//然后尝试删除当前的 `ChatGPTPool` 中的失败的 GPT 实例(如果 `ChatGPTPool` 中有多个实例)。最后,调用 `sendReply` 函数,参数为 `{res: null}`。
if (allKeys.length > 1) {
delete ChatGPTPool[failedKey]
}
return sendReply({ res: null })
}
//如果 `retryCount` 小于等于 `MAX_RETRY`,则创建 `query` 和 `req` 变量,分别表示待发送的文本和请求参数对象
const query = trimText(text)
const req = {
conversationId: gpt.preRes?.conversationId,
parentMessageId: gpt.preRes?.id,
messageId: nanoid(),
text: query,
role: 'user',
}
//然后使用 GPT 实例 `gpt` 中的 `api.sendMessage` 方法发送 GPT 请求,并在请求成功后调用 `sendReply` 函数,参数为 `{res, req}`。
gpt.api
.sendMessage(query, req)
.then(async (res) => sendReply({ res, req }))
//如果请求失败,则将 `retryCount` 加1,并记录一条日志,然后使用 `setTimeout` 在1秒后重新调用 `sendChatGptMsg` 函数。
.catch((err) => {
retryCount++
logger.info({ GELF: true, _data: data, _err: err }, `Retry:${retryCount}`)
setTimeout(() => sendChatGptMsg(), 1000)
})
}
sendChatGptMsg()
}
//然后获取请求体中的数据并对其进行预处理。如果数据中包含一个 challenge 属性,则返回一个包含 challenge 值的 JSON 对象。否则,将响应体设置为字符串 'ok'
// 并将数据推送到 QueryQueue 数组中,然后调用 callGPTWithRetry() 函数来处理查询队列中的数据。
export async function useChatGpt(router) {
router.all('/lids/cr-service/feishu/subscribe', async (ctx, next) => {
const data = ctx.request.body
if (data.challenge) {
return (ctx.body = { challenge: data.challenge })
}
ctx.body = 'ok'
if (!preProcess({ data })) {
QueryQueue.push(data)
callGPTWithRetry()
}
})
}