diff --git a/bun.lockb b/bun.lockb index 9006595..b5538e6 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/constant/config.ts b/constant/config.ts index a7bc496..fa0ce02 100644 --- a/constant/config.ts +++ b/constant/config.ts @@ -14,6 +14,7 @@ export interface AppInfoModel extends RecordModel { appId: string appSecret: string appName: string + errChatId: string } export const APP_CONFIG: Record = {} diff --git a/constant/message.ts b/constant/message.ts index 5c0ee27..9498ca1 100644 --- a/constant/message.ts +++ b/constant/message.ts @@ -7,4 +7,5 @@ export enum RespMessage { cancelWeeklySuccess = "周报订阅取消成功", registerFailed = "订阅失败", cancelFailed = "取消订阅失败", + summaryFailed = "总结失败", } diff --git a/controller/groupAgent/report.ts b/controller/groupAgent/report.ts index 34c5398..4a58d6d 100644 --- a/controller/groupAgent/report.ts +++ b/controller/groupAgent/report.ts @@ -1,9 +1,5 @@ -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" @@ -17,26 +13,22 @@ import getChatHistory from "./chatHistory" * @param {any} subscription - 订阅信息 * @returns {Promise} */ -const genReport = async ( +const genSummary = async ( ctx: Context, timeScope: "daily" | "weekly", - subscription: GrpSumSubWithApp + trigger: "auto" | "manual" ) => { - const { logger, requestId, larkCard } = ctx + const { logger, requestId, larkCard, larkService, appInfo, larkBody } = ctx + logger.info(`genSummary ${timeScope} by ${trigger}`) const cardGender = larkCard.child("groupAgent") try { - const { - chatId, - expand: { - app: { appId, appSecret, appName }, - }, - } = subscription - // 组织接口 - const larkService = new LarkService({ - appId, - appSecret, - requestId, - }) + // 获取群聊信息 + const chat = await db.chat.getAndCreate(ctx) + if (!chat) { + throw new Error("Failed to get chat info") + } + const { chatId } = chat + // 获取时间范围 const { startTime, endTime } = getTimeRange(timeScope) @@ -50,9 +42,10 @@ const genReport = async ( chatId, startTime, endTime, - excludeMentions: [appName], + excludeMentions: [appInfo.appName], } ) + if (chatHistory.length === 0) { logger.info(`No message in chat ${chatId}`) return @@ -69,29 +62,46 @@ const genReport = async ( }, 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) + + // 发送卡片消息,手动触发时回复原消息 + if (trigger === "manual") { + await larkService.message.replyCard(larkBody.messageId, cardContent) + } else { + await larkService.message.sendCard2Chat(chatId, cardContent) + } + // 记录总结日志 await db.grpSumLog.create({ - subscription: subscription.id, + chat: chat.id, content: JSON.stringify(cardContent), langfuseLink: `http://langfuse.ai.srv/project/cm1j2tkj9001gukrgdvc1swuw/sessions/${requestId}`, }) } catch (error: any) { - logger.error( - `Failed to summarize chat ${subscription.chatId}: ${error.message}` + logger.error(`Failed to summarize chat: ${error.message}`) + const errorCard = cardGender.genErrorCard( + `${RespMessage.summaryFailed}: ${error.message}` ) + // 手动触发时回复原消息 + if (trigger === "manual") { + await larkService.message.replyCard(larkBody.messageId, errorCard) + } + // 自动触发发送给自己的订阅群 + else { + await larkService.message.sendCard2Chat(appInfo.errChatId, errorCard) + } } } @@ -100,39 +110,30 @@ const genReport = async ( * @returns {Promise} */ const genAllReport = async (timeScope: "daily" | "weekly" = "daily") => { - const ctx = await genContext(new Request("https://baidu.com")) + const ctx = await genContext( + new Request("https://lark-egg-preview.ai.xiaomi.com") + ) const { logger } = ctx - + logger.info(`genAllReport ${timeScope}`) 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") + // 获取所有需要自动总结的群组 + const chatList = await db.chat.getNeedSummaryChats("all") + logger.debug(`chatList: ${JSON.stringify(chatList)}`) + if (!chatList || chatList.length === 0) { + logger.info(`No chat need to summarize`) 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) + // 总结 + for (const chat of chatList) { + const newCtx = await genContext( + new Request("https://lark-egg-preview.ai.xiaomi.com") + ) + newCtx.larkBody.chatId = chat.chatId + let scope = "daily" as "daily" | "weekly" + if (timeScope === "weekly" && chat.weeklySummary) { + scope = "weekly" + } + await genSummary(newCtx, scope, "auto") } } catch (e: any) { logger.error(`Auto summary error: ${e.message}`) @@ -147,102 +148,57 @@ const genAllReport = async (timeScope: "daily" | "weekly" = "daily") => { 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) + await genSummary(ctx, timeScope, "manual") } catch (error: any) { logger.error(`Failed to summarize chat ${chatId}: ${error.message}`) } } /** - * 注册消息总结的订阅 - * @returns + * 设置订阅 + * @param {Context} ctx - 请求上下文 + * @param {string} timeScope - 订阅时间范围 + * @param {boolean} value - 订阅值 + * @returns {Promise} */ -const subscribe = async ( - { app, larkService, logger, larkBody, larkCard }: Context, - timeScope: "daily" | "weekly" +const setSubscription = async ( + ctx: Context, + timeScope: "daily" | "weekly", + value: boolean ) => { + const { larkService, logger, larkBody, larkCard } = ctx const cardGender = larkCard.child("groupAgent") - const sendErrorMsg = () => + const sendErrorMsg = (message: string) => larkService.message.replyCard( larkBody.messageId, - cardGender.genErrorCard(RespMessage.registerFailed) + cardGender.genErrorCard( + `${ + value ? RespMessage.registerFailed : RespMessage.cancelFailed + }: ${message}` + ) ) try { - // 判断是否有 chatId 和 userId - if (!larkBody.chatId || !larkBody.userId) { - logger.error(`chatId or userId is empty`) - return + const { chatId } = larkBody + if (!chatId) { + throw new Error("Invalid chatId") } - - // 获取用户信息 - const user = await db.user.getByCtx({ larkBody, larkService } as Context) - if (!user) { - logger.error(`Failed to get user info`) - await sendErrorMsg() - return + // 获取群组信息 + const chat = await db.chat.getAndCreate(ctx) + if (!chat) { + throw new Error("Failed to get chat info") } - // 先查询是否已经存在订阅 - 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.replyCard( - larkBody.messageId, - 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 + // 更新订阅信息, 如果订阅信息没有变化则不更新 + if (chat[`${timeScope}Summary`] !== value) { + logger.info("value is different, update subscription") + const res = await db.chat.updateSummary(chat.id, timeScope, value) + if (!res) { + throw new Error("Failed to update subscription") + } } // 发送成功消息 await larkService.message.replyCard( @@ -255,92 +211,15 @@ const subscribe = async ( ) } 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.replyCard( - larkBody.messageId, - 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.replyCard( - larkBody.messageId, - 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.replyCard( - larkBody.messageId, - cardGender.genSuccessCard( - timeScope === "daily" - ? RespMessage.cancelDailySuccess - : RespMessage.cancelWeeklySuccess - ) - ) - } catch (e: any) { - logger.error(`Unsubscribe error: ${e.message}`) - await sendErrorMsg() + await sendErrorMsg(e.message) } } const report = { - genReport, + genSummary, genAllReport, gen4Test, - subscribe, - unsubscribe, + setSubscription, } export default report diff --git a/db/chat/index.ts b/db/chat/index.ts new file mode 100644 index 0000000..9923988 --- /dev/null +++ b/db/chat/index.ts @@ -0,0 +1,106 @@ +import { RecordModel } from "pocketbase" + +import { Context } from "../../types" +import { managePbError } from "../../utils/pbTools" +import pbClient from "../pbClient" + +const DB_NAME = "chat" + +export interface Chat { + chatId: string + name: string + avatar: string + mode: "group" | "p2p" | "topic" + weeklySummary: boolean + dailySummary: boolean +} + +export type ChatModel = Chat & RecordModel + +/** + * 获取单个群组信息 + * @param id + * @returns + */ +const getOneByChatId = (chatId: string) => + managePbError(() => + pbClient.collection(DB_NAME).getFirstListItem(`chatId = "${chatId}"`) + ) + +/** + * 创建群组 + * @param chat + * @returns + */ +const create = (chat: Chat) => + managePbError(() => pbClient.collection(DB_NAME).create(chat)) + +/** + * 获取并创建群组 + * @param chatId + * @param context + * @returns + */ +const getAndCreate = async ({ larkService, logger, larkBody }: Context) => { + const { chatId } = larkBody + if (!chatId) { + logger.error(`chatId is empty`) + return null + } + const chat = await getOneByChatId(chatId) + if (chat) return chat + logger.info(`chat ${chatId} not found, try to get from lark`) + const chatInfo = await larkService.chat.getChatInfo(chatId) + if (!chatInfo || chatInfo.code !== 0) return null + const { name, avatar, chat_mode } = chatInfo.data + const newChat = { + chatId, + name, + avatar, + mode: chat_mode, + weeklySummary: false, + dailySummary: false, + } + return create(newChat) +} + +/** + * 更新群组总结 + * @param id + * @param timeScope + * @param value + * @returns + */ +const updateSummary = async ( + id: string, + timeScope: "daily" | "weekly", + value: boolean +) => + managePbError(() => + pbClient.collection(DB_NAME).update(id, { [`${timeScope}Summary`]: value }) + ) + +/** + * 获取需要总结的群组 + * @param timeScope + * @returns + */ +const getNeedSummaryChats = async (timeScope: "daily" | "weekly" | "all") => { + const filterMap = { + daily: "dailySummary = true", + weekly: "weeklySummary = true", + all: "dailySummary = true && weeklySummary = true", + } + return managePbError(() => + pbClient.collection(DB_NAME).getFullList({ filter: filterMap[timeScope] }) + ) +} + +const chat = { + getAndCreate, + getOneByChatId, + updateSummary, + getNeedSummaryChats, +} + +export default chat diff --git a/db/grpSumLog/index.ts b/db/grpSumLog/index.ts index 7eb923e..39e8c34 100644 --- a/db/grpSumLog/index.ts +++ b/db/grpSumLog/index.ts @@ -3,10 +3,10 @@ import { RecordModel } from "pocketbase" import { managePbError } from "../../utils/pbTools" import pbClient from "../pbClient" -const DB_NAME = "groupSummaryLog" +const DB_NAME = "grpSumLog" export interface GroupSummaryLog { - subscription: string + chat: string content: string langfuseLink: string } diff --git a/db/grpSumSub/index.ts b/db/grpSumSub/index.ts deleted file mode 100644 index 8ca7c1d..0000000 --- a/db/grpSumSub/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { RecordModel } from "pocketbase" - -import { AppInfoModel } from "../../constant/config" -import { managePbError } from "../../utils/pbTools" -import pbClient from "../pbClient" - -const DB_NAME = "groupSummarySubscription" - -export interface GroupSummarySubscription { - app: string - initiator: string - terminator: string - chatId: string - timeScope: "daily" | "weekly" -} - -export type GroupSummarySubscriptionModel = GroupSummarySubscription & - RecordModel - -export interface GrpSumSubWithApp extends GroupSummarySubscriptionModel { - expand: { - app: AppInfoModel - } -} - -const create = async (subscription: GroupSummarySubscription) => - managePbError(() => - pbClient.collection(DB_NAME).create(subscription) - ) - -const update = async ( - id: string, - subscription: Partial -) => - managePbError(() => - pbClient.collection(DB_NAME).update(id, subscription) - ) - -const getAll = async (filter: string = "") => - managePbError(() => - pbClient.collection(DB_NAME).getFullList({ - filter, - expand: "app", - }) - ) - -const getByFilter = async (filter: string) => - managePbError(() => - pbClient.collection(DB_NAME).getFirstListItem(filter, { expand: "app" }) - ) - -const grpSumSub = { - create, - update, - getAll, - getByFilter, -} - -export default grpSumSub diff --git a/db/index.ts b/db/index.ts index 947726f..2ca54b0 100644 --- a/db/index.ts +++ b/db/index.ts @@ -1,18 +1,18 @@ import apiKey from "./apiKey" +import chat from "./chat" import gitlabProject from "./gitlabProject/index." import grpSumLog from "./grpSumLog" -import grpSumSub from "./grpSumSub" import log from "./log" import receiveGroup from "./receiveGroup" import user from "./user" const db = { + chat, apiKey, receiveGroup, log, user, grpSumLog, - grpSumSub, gitlabProject, } diff --git a/package.json b/package.json index 40944b6..5956d69 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,11 @@ "devDependencies": { "@commitlint/cli": "^19.6.1", "@commitlint/config-conventional": "^19.6.0", - "@eslint/js": "^9.17.0", + "@eslint/js": "^9.18.0", "@types/node-schedule": "^2.1.7", "@types/uuid": "^10.0.0", "bun-types": "^1.1.43", - "eslint": "^9.17.0", + "eslint": "^9.18.0", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^4.1.4", "husky": "^9.1.7", @@ -39,11 +39,11 @@ "@egg/hooks": "^1.2.0", "@egg/lark-msg-tool": "^1.21.0", "@egg/logger": "^1.6.0", - "@egg/net-tool": "^1.21.0", + "@egg/net-tool": "^1.22.0", "@egg/path-tool": "^1.4.1", - "@langchain/core": "^0.3.27", + "@langchain/core": "^0.3.29", "@langchain/langgraph": "^0.2.39", - "@langchain/openai": "^0.3.16", + "@langchain/openai": "^0.3.17", "joi": "^17.13.3", "langfuse-langchain": "^3.32.0", "node-schedule": "^2.1.1", diff --git a/routes/bot/eventMsg.ts b/routes/bot/eventMsg.ts index b5d29e9..3990fa9 100644 --- a/routes/bot/eventMsg.ts +++ b/routes/bot/eventMsg.ts @@ -185,7 +185,7 @@ const manageCMDMsg = async (ctx: Context) => { logger.info( `bot command is register, chatId: ${chatId}, timeScope: daily` ) - groupAgent.report.subscribe(ctx, "daily") + groupAgent.report.setSubscription(ctx, "daily", true) return } // 注册群组周报 @@ -193,7 +193,7 @@ const manageCMDMsg = async (ctx: Context) => { logger.info( `bot command is register, chatId: ${chatId}, timeScope: weekly` ) - groupAgent.report.subscribe(ctx, "weekly") + groupAgent.report.setSubscription(ctx, "weekly", true) return } @@ -202,7 +202,7 @@ const manageCMDMsg = async (ctx: Context) => { logger.info( `bot command is unregister, chatId: ${chatId}, timeScope: daily` ) - groupAgent.report.unsubscribe(ctx, "daily") + groupAgent.report.setSubscription(ctx, "daily", false) return } // 注销群组周报 @@ -210,7 +210,7 @@ const manageCMDMsg = async (ctx: Context) => { logger.info( `bot command is unregister, chatId: ${chatId}, timeScope: weekly` ) - groupAgent.report.unsubscribe(ctx, "weekly") + groupAgent.report.setSubscription(ctx, "weekly", false) return } // 立即发送日简报