飞书调用chatgpt接口实战

811 阅读4分钟

首先先简单的建一个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()
        }
    })
}