import { LarkService } from "@egg/net-tool" import { APP_MAP } from "../../constant/config" import { RespMessage } from "../../constant/message" import db from "../../db" import { GrpSumSubWithApp } from "../../db/grpSumSub" import { Context } from "../../types" import genContext from "../../utils/genContext" import llm from "../../utils/llm" import { getTimeRange } from "../../utils/time" import getChatHistory from "./chatHistory" /** * 总结指定聊天记录 * @param {Context} ctx - 请求上下文 * @param {string} timeScope - 时间范围 * @param {any} subscription - 订阅信息 * @returns {Promise} */ const genReport = async ( ctx: Context, timeScope: "daily" | "weekly", subscription: GrpSumSubWithApp ) => { const { logger, requestId, larkCard } = ctx const cardGender = larkCard.child("groupAgent") try { const { chatId, expand: { app: { appId, appSecret, appName }, }, } = subscription // 组织接口 const larkService = new LarkService({ appId, appSecret, requestId, }) // 获取时间范围 const { startTime, endTime } = getTimeRange(timeScope) // 计时开始 const processStart = Date.now() // 获取聊天记录 const { messages: chatHistory } = await getChatHistory( { larkService, logger } as Context, { chatId, startTime, endTime, excludeMentions: [appName], } ) if (chatHistory.length === 0) { logger.info(`No message in chat ${chatId}`) return } // 使用大模型总结消息 const llmRes = await llm.invoke( `${timeScope}Summary`, { chatHistory: JSON.stringify(chatHistory), time: new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai", }), }, requestId ) // 计时结束 const processEnd = Date.now() const processingTime = ((processEnd - processStart) / 1000).toFixed(2) logger.info( `LLM takes time: ${processingTime}s, see detail: http://langfuse.ai.srv/project/cm1j2tkj9001gukrgdvc1swuw/sessions/${requestId}` ) // 生成卡片内容 const cardContent = cardGender.genCard("autoReport", { llmRes, timeScope: timeScope === "daily" ? "今日日报" : "本周周报", }) // 发送卡片消息 await larkService.message.sendCard2Chat(chatId, cardContent) // 记录总结日志 await db.grpSumLog.create({ subscription: subscription.id, content: JSON.stringify(cardContent), langfuseLink: `http://langfuse.ai.srv/project/cm1j2tkj9001gukrgdvc1swuw/sessions/${requestId}`, }) } catch (error: any) { logger.error( `Failed to summarize chat ${subscription.chat_id}: ${error.message}` ) } } /** * 自动总结聊天记录 * @returns {Promise} */ const genAllReport = async (timeScope: "daily" | "weekly" = "daily") => { const ctx = await genContext(new Request("https://baidu.com")) const { logger } = ctx try { // 获取全部需要自动总结的群组 let subList = await db.grpSumSub.getAll( `terminator = ""${timeScope === "daily" ? ' && timeScope = "daily"' : ""}` ) // 没有需要总结的群组 if (!subList || subList.length === 0) { logger.info("No group needs to be summarized") return } // 如果是周五,获取了需要日报和周报的订阅,根据chatId,过滤掉需要周报的日报订阅 if (timeScope === "weekly") { const dailySubList = subList.filter((sub) => sub.timeScope === "daily") const weeklySubList = subList.filter((sub) => sub.timeScope === "weekly") // 过滤掉需要周报的日报订阅 subList = dailySubList .filter( (dailySub) => !weeklySubList.find( (weeklySub) => weeklySub.chatId === dailySub.chatId ) ) .concat(weeklySubList) } // 一个一个群组的总结,避免触发频率限制 for (const sub of subList) { await genReport(ctx, sub.timeScope, sub) } } catch (e: any) { logger.error(`Auto summary error: ${e.message}`) } } /** * 立即生成日报或周报(测试用) * @param {Context} ctx - 请求上下文 * @returns {Promise} */ const gen4Test = async (ctx: Context, timeScope: "daily" | "weekly") => { const { logger, larkCard, larkService, larkBody: { chatId }, } = ctx try { logger.info(`timeScope: ${timeScope}`) // 获取需要总结的chatId if (!chatId) { logger.error("Invalid request body") return } // 获取订阅信息 const sub = await db.grpSumSub.getByFilter( `terminator = "" && chatId = "${chatId}" && timeScope = "${timeScope}"` ) // 没有订阅信息 if (!sub) { logger.error(`No subscription found for chat ${chatId}`) await larkService.message.sendCard2Chat( chatId, larkCard.genErrorCard( `本群未订阅${timeScope === "daily" ? "日报" : "周报"}` ) ) return } // 总结 await genReport(ctx, timeScope, sub) } catch (error: any) { logger.error(`Failed to summarize chat ${chatId}: ${error.message}`) } } /** * 注册消息总结的订阅 * @returns */ const subscribe = async ( { app, larkService, logger, larkBody, larkCard }: Context, timeScope: "daily" | "weekly" ) => { const cardGender = larkCard.child("groupAgent") const sendErrorMsg = () => larkService.message.sendCard2Chat( larkBody.chatId, cardGender.genErrorCard(RespMessage.registerFailed) ) try { // 判断是否有 chatId 和 userId if (!larkBody.chatId || !larkBody.userId) { logger.error(`chatId or userId is empty`) return } // 获取用户信息 const user = await db.user.getByCtx({ larkBody, larkService } as Context) if (!user) { logger.error(`Failed to get user info`) await sendErrorMsg() return } // 先查询是否已经存在订阅 const sub = await db.grpSumSub.getByFilter( `terminator = "" && chatId = "${larkBody.chatId} && timeScope = "${timeScope}"` ) if (sub) { logger.info( `chatId: ${larkBody.chatId} has been registered, timeScope: ${timeScope}` ) // 发送已经注册过了的消息 await larkService.message.sendCard2Chat( larkBody.chatId, cardGender.genSuccessCard( timeScope === "daily" ? RespMessage.hasRegisteredDaily : RespMessage.hasRegisteredWeekly ) ) return } // 注册订阅 const createRes = await db.grpSumSub.create({ app: APP_MAP[app].id, initiator: user.id, terminator: "", chatId: larkBody.chatId, timeScope, }) if (!createRes) { logger.error( `Failed to register chatId: ${larkBody.chatId}, timeScope: ${timeScope}` ) await sendErrorMsg() return } // 发送成功消息 await larkService.message.sendCard2Chat( larkBody.chatId, cardGender.genSuccessCard( timeScope === "daily" ? RespMessage.registerDailySuccess : RespMessage.registerWeeklySuccess ) ) } catch (e: any) { logger.error(`Subscribe error: ${e.message}`) await sendErrorMsg() } } /** * 取消消息总结的订阅 * @returns */ const unsubscribe = async ( { logger, larkBody, larkService, larkCard }: Context, timeScope: "daily" | "weekly" ) => { const cardGender = larkCard.child("groupAgent") const sendErrorMsg = () => larkService.message.sendCard2Chat( larkBody.chatId, cardGender.genErrorCard(RespMessage.cancelFailed) ) try { // 判断是否有 chatId 和 userId if (!larkBody.chatId || !larkBody.userId) { logger.error(`chatId or userId is empty`) return } // 获取用户信息 const user = await db.user.getByCtx({ larkBody, larkService } as Context) if (!user) { logger.error(`Failed to get user info`) await sendErrorMsg() return } // 先查询是否已经存在订阅 const sub = await db.grpSumSub.getByFilter( `terminator = "" && chatId = "${larkBody.chatId} && timeScope = "${timeScope}"` ) if (!sub) { logger.info( `chatId: ${larkBody.chatId} has not been registered, timeScope: ${timeScope}` ) // 发送未注册的消息 await larkService.message.sendCard2Chat( larkBody.chatId, cardGender.genSuccessCard( timeScope === "daily" ? RespMessage.cancelDailySuccess : RespMessage.cancelWeeklySuccess ) ) return } // 更新订阅 const updateRes = await db.grpSumSub.update(sub.id, { terminator: user.id, }) if (!updateRes) { logger.error( `Failed to cancel chatId: ${larkBody.chatId}, timeScope: ${timeScope}` ) await sendErrorMsg() return } // 发送成功消息 await larkService.message.sendCard2Chat( larkBody.chatId, cardGender.genSuccessCard( timeScope === "daily" ? RespMessage.cancelDailySuccess : RespMessage.cancelWeeklySuccess ) ) } catch (e: any) { logger.error(`Unsubscribe error: ${e.message}`) await sendErrorMsg() } } const report = { genReport, genAllReport, gen4Test, subscribe, unsubscribe, } export default report