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 appId: string
appSecret: string appSecret: string
appName: string appName: string
errChatId: string
} }
export const APP_CONFIG: Record<string, string> = {} export const APP_CONFIG: Record<string, string> = {}

View File

@ -7,4 +7,5 @@ export enum RespMessage {
cancelWeeklySuccess = "周报订阅取消成功", cancelWeeklySuccess = "周报订阅取消成功",
registerFailed = "订阅失败", registerFailed = "订阅失败",
cancelFailed = "取消订阅失败", 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 { RespMessage } from "../../constant/message"
import db from "../../db" import db from "../../db"
import { GrpSumSubWithApp } from "../../db/grpSumSub"
import { Context } from "../../types" import { Context } from "../../types"
import genContext from "../../utils/genContext" import genContext from "../../utils/genContext"
import llm from "../../utils/llm" import llm from "../../utils/llm"
@ -17,26 +13,22 @@ import getChatHistory from "./chatHistory"
* @param {any} subscription - * @param {any} subscription -
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
const genReport = async ( const genSummary = async (
ctx: Context, ctx: Context,
timeScope: "daily" | "weekly", 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") const cardGender = larkCard.child("groupAgent")
try { try {
const { // 获取群聊信息
chatId, const chat = await db.chat.getAndCreate(ctx)
expand: { if (!chat) {
app: { appId, appSecret, appName }, throw new Error("Failed to get chat info")
}, }
} = subscription const { chatId } = chat
// 组织接口
const larkService = new LarkService({
appId,
appSecret,
requestId,
})
// 获取时间范围 // 获取时间范围
const { startTime, endTime } = getTimeRange(timeScope) const { startTime, endTime } = getTimeRange(timeScope)
@ -50,9 +42,10 @@ const genReport = async (
chatId, chatId,
startTime, startTime,
endTime, endTime,
excludeMentions: [appName], excludeMentions: [appInfo.appName],
} }
) )
if (chatHistory.length === 0) { if (chatHistory.length === 0) {
logger.info(`No message in chat ${chatId}`) logger.info(`No message in chat ${chatId}`)
return return
@ -69,29 +62,46 @@ const genReport = async (
}, },
requestId requestId
) )
// 计时结束 // 计时结束
const processEnd = Date.now() const processEnd = Date.now()
const processingTime = ((processEnd - processStart) / 1000).toFixed(2) const processingTime = ((processEnd - processStart) / 1000).toFixed(2)
logger.info( logger.info(
`LLM takes time: ${processingTime}s, see detail: http://langfuse.ai.srv/project/cm1j2tkj9001gukrgdvc1swuw/sessions/${requestId}` `LLM takes time: ${processingTime}s, see detail: http://langfuse.ai.srv/project/cm1j2tkj9001gukrgdvc1swuw/sessions/${requestId}`
) )
// 生成卡片内容 // 生成卡片内容
const cardContent = cardGender.genCard("autoReport", { const cardContent = cardGender.genCard("autoReport", {
llmRes, llmRes,
timeScope: timeScope === "daily" ? "今日日报" : "本周周报", 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({ await db.grpSumLog.create({
subscription: subscription.id, chat: chat.id,
content: JSON.stringify(cardContent), content: JSON.stringify(cardContent),
langfuseLink: `http://langfuse.ai.srv/project/cm1j2tkj9001gukrgdvc1swuw/sessions/${requestId}`, langfuseLink: `http://langfuse.ai.srv/project/cm1j2tkj9001gukrgdvc1swuw/sessions/${requestId}`,
}) })
} catch (error: any) { } catch (error: any) {
logger.error( logger.error(`Failed to summarize chat: ${error.message}`)
`Failed to summarize chat ${subscription.chatId}: ${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>} * @returns {Promise<void>}
*/ */
const genAllReport = async (timeScope: "daily" | "weekly" = "daily") => { 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 const { logger } = ctx
logger.info(`genAllReport ${timeScope}`)
try { try {
// 获取全部需要自动总结的群组 // 获取所有需要自动总结的群组
let subList = await db.grpSumSub.getAll( const chatList = await db.chat.getNeedSummaryChats("all")
`terminator = ""${timeScope === "daily" ? ' && timeScope = "daily"' : ""}` logger.debug(`chatList: ${JSON.stringify(chatList)}`)
) if (!chatList || chatList.length === 0) {
logger.info(`No chat need to summarize`)
// 没有需要总结的群组
if (!subList || subList.length === 0) {
logger.info("No group needs to be summarized")
return return
} }
// 总结
// 如果是周五获取了需要日报和周报的订阅根据chatId过滤掉需要周报的日报订阅 for (const chat of chatList) {
if (timeScope === "weekly") { const newCtx = await genContext(
const dailySubList = subList.filter((sub) => sub.timeScope === "daily") new Request("https://lark-egg-preview.ai.xiaomi.com")
const weeklySubList = subList.filter((sub) => sub.timeScope === "weekly") )
// 过滤掉需要周报的日报订阅 newCtx.larkBody.chatId = chat.chatId
subList = dailySubList let scope = "daily" as "daily" | "weekly"
.filter( if (timeScope === "weekly" && chat.weeklySummary) {
(dailySub) => scope = "weekly"
!weeklySubList.find( }
(weeklySub) => weeklySub.chatId === dailySub.chatId await genSummary(newCtx, scope, "auto")
)
)
.concat(weeklySubList)
}
// 一个一个群组的总结,避免触发频率限制
for (const sub of subList) {
await genReport(ctx, sub.timeScope, sub)
} }
} catch (e: any) { } catch (e: any) {
logger.error(`Auto summary error: ${e.message}`) 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 gen4Test = async (ctx: Context, timeScope: "daily" | "weekly") => {
const { const {
logger, logger,
larkCard,
larkService,
larkBody: { chatId }, larkBody: { chatId },
} = ctx } = ctx
try { try {
logger.info(`timeScope: ${timeScope}`) 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) { } catch (error: any) {
logger.error(`Failed to summarize chat ${chatId}: ${error.message}`) 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 ( const setSubscription = async (
{ app, larkService, logger, larkBody, larkCard }: Context, ctx: Context,
timeScope: "daily" | "weekly" timeScope: "daily" | "weekly",
value: boolean
) => { ) => {
const { larkService, logger, larkBody, larkCard } = ctx
const cardGender = larkCard.child("groupAgent") const cardGender = larkCard.child("groupAgent")
const sendErrorMsg = () => const sendErrorMsg = (message: string) =>
larkService.message.replyCard( larkService.message.replyCard(
larkBody.messageId, larkBody.messageId,
cardGender.genErrorCard(RespMessage.registerFailed) cardGender.genErrorCard(
`${
value ? RespMessage.registerFailed : RespMessage.cancelFailed
}: ${message}`
)
) )
try { try {
// 判断是否有 chatId 和 userId const { chatId } = larkBody
if (!larkBody.chatId || !larkBody.userId) { if (!chatId) {
logger.error(`chatId or userId is empty`) throw new Error("Invalid chatId")
return
} }
// 获取群组信息
// 获取用户信息 const chat = await db.chat.getAndCreate(ctx)
const user = await db.user.getByCtx({ larkBody, larkService } as Context) if (!chat) {
if (!user) { throw new Error("Failed to get chat info")
logger.error(`Failed to get user info`)
await sendErrorMsg()
return
} }
// 先查询是否已经存在订阅 // 更新订阅信息, 如果订阅信息没有变化则不更新
const sub = await db.grpSumSub.getByFilter( if (chat[`${timeScope}Summary`] !== value) {
`terminator = "" && chatId = "${larkBody.chatId} && timeScope = "${timeScope}"` logger.info("value is different, update subscription")
) const res = await db.chat.updateSummary(chat.id, timeScope, value)
if (sub) { if (!res) {
logger.info( throw new Error("Failed to update subscription")
`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
} }
// 发送成功消息 // 发送成功消息
await larkService.message.replyCard( await larkService.message.replyCard(
@ -255,92 +211,15 @@ const subscribe = async (
) )
} catch (e: any) { } catch (e: any) {
logger.error(`Subscribe error: ${e.message}`) logger.error(`Subscribe error: ${e.message}`)
await sendErrorMsg() await sendErrorMsg(e.message)
}
}
/**
*
* @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()
} }
} }
const report = { const report = {
genReport, genSummary,
genAllReport, genAllReport,
gen4Test, gen4Test,
subscribe, setSubscription,
unsubscribe,
} }
export default report 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 { managePbError } from "../../utils/pbTools"
import pbClient from "../pbClient" import pbClient from "../pbClient"
const DB_NAME = "groupSummaryLog" const DB_NAME = "grpSumLog"
export interface GroupSummaryLog { export interface GroupSummaryLog {
subscription: string chat: string
content: string content: string
langfuseLink: 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 apiKey from "./apiKey"
import chat from "./chat"
import gitlabProject from "./gitlabProject/index." import gitlabProject from "./gitlabProject/index."
import grpSumLog from "./grpSumLog" import grpSumLog from "./grpSumLog"
import grpSumSub from "./grpSumSub"
import log from "./log" import log from "./log"
import receiveGroup from "./receiveGroup" import receiveGroup from "./receiveGroup"
import user from "./user" import user from "./user"
const db = { const db = {
chat,
apiKey, apiKey,
receiveGroup, receiveGroup,
log, log,
user, user,
grpSumLog, grpSumLog,
grpSumSub,
gitlabProject, gitlabProject,
} }

View File

@ -19,11 +19,11 @@
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^19.6.1", "@commitlint/cli": "^19.6.1",
"@commitlint/config-conventional": "^19.6.0", "@commitlint/config-conventional": "^19.6.0",
"@eslint/js": "^9.17.0", "@eslint/js": "^9.18.0",
"@types/node-schedule": "^2.1.7", "@types/node-schedule": "^2.1.7",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"bun-types": "^1.1.43", "bun-types": "^1.1.43",
"eslint": "^9.17.0", "eslint": "^9.18.0",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.1.4", "eslint-plugin-unused-imports": "^4.1.4",
"husky": "^9.1.7", "husky": "^9.1.7",
@ -39,11 +39,11 @@
"@egg/hooks": "^1.2.0", "@egg/hooks": "^1.2.0",
"@egg/lark-msg-tool": "^1.21.0", "@egg/lark-msg-tool": "^1.21.0",
"@egg/logger": "^1.6.0", "@egg/logger": "^1.6.0",
"@egg/net-tool": "^1.21.0", "@egg/net-tool": "^1.22.0",
"@egg/path-tool": "^1.4.1", "@egg/path-tool": "^1.4.1",
"@langchain/core": "^0.3.27", "@langchain/core": "^0.3.29",
"@langchain/langgraph": "^0.2.39", "@langchain/langgraph": "^0.2.39",
"@langchain/openai": "^0.3.16", "@langchain/openai": "^0.3.17",
"joi": "^17.13.3", "joi": "^17.13.3",
"langfuse-langchain": "^3.32.0", "langfuse-langchain": "^3.32.0",
"node-schedule": "^2.1.1", "node-schedule": "^2.1.1",

View File

@ -185,7 +185,7 @@ const manageCMDMsg = async (ctx: Context) => {
logger.info( logger.info(
`bot command is register, chatId: ${chatId}, timeScope: daily` `bot command is register, chatId: ${chatId}, timeScope: daily`
) )
groupAgent.report.subscribe(ctx, "daily") groupAgent.report.setSubscription(ctx, "daily", true)
return return
} }
// 注册群组周报 // 注册群组周报
@ -193,7 +193,7 @@ const manageCMDMsg = async (ctx: Context) => {
logger.info( logger.info(
`bot command is register, chatId: ${chatId}, timeScope: weekly` `bot command is register, chatId: ${chatId}, timeScope: weekly`
) )
groupAgent.report.subscribe(ctx, "weekly") groupAgent.report.setSubscription(ctx, "weekly", true)
return return
} }
@ -202,7 +202,7 @@ const manageCMDMsg = async (ctx: Context) => {
logger.info( logger.info(
`bot command is unregister, chatId: ${chatId}, timeScope: daily` `bot command is unregister, chatId: ${chatId}, timeScope: daily`
) )
groupAgent.report.unsubscribe(ctx, "daily") groupAgent.report.setSubscription(ctx, "daily", false)
return return
} }
// 注销群组周报 // 注销群组周报
@ -210,7 +210,7 @@ const manageCMDMsg = async (ctx: Context) => {
logger.info( logger.info(
`bot command is unregister, chatId: ${chatId}, timeScope: weekly` `bot command is unregister, chatId: ${chatId}, timeScope: weekly`
) )
groupAgent.report.unsubscribe(ctx, "weekly") groupAgent.report.setSubscription(ctx, "weekly", false)
return return
} }
// 立即发送日简报 // 立即发送日简报