feat: 添加聊天功能支持,更新相关数据模型和配置;优化群组总结逻辑

This commit is contained in:
zhaoyingbo 2025-01-12 06:16:41 +00:00
parent a31f0fd249
commit feb0ada324
10 changed files with 210 additions and 282 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -14,6 +14,7 @@ export interface AppInfoModel extends RecordModel {
appId: string
appSecret: string
appName: string
errChatId: string
}
export const APP_CONFIG: Record<string, string> = {}

View File

@ -7,4 +7,5 @@ export enum RespMessage {
cancelWeeklySuccess = "周报订阅取消成功",
registerFailed = "订阅失败",
cancelFailed = "取消订阅失败",
summaryFailed = "总结失败",
}

View File

@ -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<void>}
*/
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<void>}
*/
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<void>}
*/
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

106
db/chat/index.ts Normal file
View File

@ -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<ChatModel>(() =>
pbClient.collection(DB_NAME).getFirstListItem(`chatId = "${chatId}"`)
)
/**
*
* @param chat
* @returns
*/
const create = (chat: Chat) =>
managePbError<ChatModel>(() => 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<ChatModel>(() =>
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<ChatModel[]>(() =>
pbClient.collection(DB_NAME).getFullList({ filter: filterMap[timeScope] })
)
}
const chat = {
getAndCreate,
getOneByChatId,
updateSummary,
getNeedSummaryChats,
}
export default chat

View File

@ -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
}

View File

@ -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<GroupSummarySubscriptionModel>(() =>
pbClient.collection(DB_NAME).create(subscription)
)
const update = async (
id: string,
subscription: Partial<GroupSummarySubscription>
) =>
managePbError<GroupSummarySubscriptionModel>(() =>
pbClient.collection(DB_NAME).update(id, subscription)
)
const getAll = async (filter: string = "") =>
managePbError<GrpSumSubWithApp[]>(() =>
pbClient.collection(DB_NAME).getFullList({
filter,
expand: "app",
})
)
const getByFilter = async (filter: string) =>
managePbError<GrpSumSubWithApp>(() =>
pbClient.collection(DB_NAME).getFirstListItem(filter, { expand: "app" })
)
const grpSumSub = {
create,
update,
getAll,
getByFilter,
}
export default grpSumSub

View File

@ -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,
}

View File

@ -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",

View File

@ -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
}
// 立即发送日简报