diff --git a/.env.example b/.env.example index ecd57aa..fd9e288 100644 --- a/.env.example +++ b/.env.example @@ -12,4 +12,6 @@ LANGFUSE_BASE_URL= # Map of apps such as '{"michat": { "app_id": "123", "app_secret": "456", "app_name": "Mi Chat" }}' LARK_APP_MAP= -ATTACH_APP_SECRET= \ No newline at end of file +ATTACH_APP_SECRET= + +DATABASE_URL= \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 915a8f8..54ef425 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/cache.ts b/cache.ts deleted file mode 100644 index db0fd97..0000000 --- a/cache.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { parseJsonString } from "@egg/hooks" - -console.log(parseJsonString(Bun.env.LARK_APP_MAP, []), []) diff --git a/constant/appMap.ts b/constant/appMap.ts new file mode 100644 index 0000000..58459be --- /dev/null +++ b/constant/appMap.ts @@ -0,0 +1,13 @@ +import { parseJsonString } from "@egg/hooks" + +export interface AppInfo { + app_id: string + app_secret: string + app_name: string +} + +// 获取所有应用信息 +export const appMap = parseJsonString(process.env.LARK_APP_MAP, {}) as Record< + string, + AppInfo +> diff --git a/constant/card.ts b/constant/card.ts index 317db36..d7519eb 100644 --- a/constant/card.ts +++ b/constant/card.ts @@ -149,6 +149,29 @@ const resultReport = { header: cardComponent.successHeader, } +const autoReport = { + config: { + update_multi: true, + }, + elements: [ + { + tag: "markdown", + content: "${llmRes}", + }, + { + tag: "hr", + }, + cardComponent.commonNote, + ], + header: { + template: "turquoise", + title: { + content: "${xIcon} ${xName} ${timeScope}", + tag: "plain_text", + }, + }, +} + export const functionOptionList = [ { id: "summary-qwen-72b-instruct-int4", @@ -161,6 +184,7 @@ const cardMap = { timeScopeSelector, resultReport, groupSelector, + autoReport, } export default cardMap diff --git a/constant/function.ts b/constant/function.ts index 7107948..5b4fd08 100644 --- a/constant/function.ts +++ b/constant/function.ts @@ -5,7 +5,7 @@ const functionMap = { xIcon: "🍳", }, groupAgent: { - xName: "Group Agent", + xName: "Mi Chat群聊助手", xAuthor: "AI创新应用组", xIcon: "🔥", }, diff --git a/constant/message.ts b/constant/message.ts new file mode 100644 index 0000000..39d3ec6 --- /dev/null +++ b/constant/message.ts @@ -0,0 +1,5 @@ +export enum RespMessage { + hasRegistered = "本群已订阅日报,周报", + registerSuccess = "周报、日报订阅成功", + cancelSuccess = "周报、日报订阅取消成功", +} diff --git a/routes/bot/groupAgent/chatHistory.ts b/controller/groupAgent/chatHistory.ts similarity index 97% rename from routes/bot/groupAgent/chatHistory.ts rename to controller/groupAgent/chatHistory.ts index 12a660c..50d5e0b 100644 --- a/routes/bot/groupAgent/chatHistory.ts +++ b/controller/groupAgent/chatHistory.ts @@ -1,7 +1,8 @@ import { parseJsonString } from "@egg/hooks" import { LarkEvent } from "@egg/lark-msg-tool" +import { Lark } from "@egg/net-tool" -import { Context, LarkServer } from "../../../types" +import { Context } from "../../types" interface Message { user: string @@ -163,7 +164,7 @@ const getChatHistory = async ( * @param chat - 聊天消息数据 * @returns 文本消息内容 */ - const getText = (chat: LarkServer.MessageData) => { + const getText = (chat: Lark.MessageData) => { let { text } = parseJsonString(chat.body.content, { text: "" }) as { text: string } @@ -187,7 +188,7 @@ const getChatHistory = async ( * @param chat - 聊天消息数据 * @returns post 消息内容 */ - const getPost = (chat: LarkServer.MessageData) => { + const getPost = (chat: Lark.MessageData) => { const content = parseJsonString(chat.body.content, null) if (!content) return "" return extractTextFromJson(content) diff --git a/routes/bot/groupAgent/index.ts b/controller/groupAgent/index.ts similarity index 93% rename from routes/bot/groupAgent/index.ts rename to controller/groupAgent/index.ts index 15f868c..8703048 100644 --- a/routes/bot/groupAgent/index.ts +++ b/controller/groupAgent/index.ts @@ -1,8 +1,8 @@ import { genCardOptions, LarkEvent } from "@egg/lark-msg-tool" -import { functionOptionList } from "../../../constant/card" -import { Context, LarkServer } from "../../../types" -import llm from "../../../utils/llm" +import { functionOptionList } from "../../constant/card" +import { Context, LarkServer } from "../../types" +import llm from "../../utils/llm" import getChatHistory from "./chatHistory" /** @@ -290,7 +290,7 @@ const manageEventMsg = async (ctx: Context.Data) => { // 私聊发送正常的群组选择器 if (chatType === "p2p") { logger.info("Send group selector to p2p chat") - sendCard(genGroupSelector(ctx, innerList, { mentions })) + await sendCard(genGroupSelector(ctx, innerList, { mentions })) return } // 如果是群聊,获取群聊名称并发送功能 @@ -298,7 +298,7 @@ const manageEventMsg = async (ctx: Context.Data) => { data: { name: chatName }, } = await larkService.chat.getChatInfo(rawChatId) logger.info(`Send function selector to group chat: ${chatName}`) - sendCard(genFunctionSelector(ctx, { chatName, mentions })) + await sendCard(genFunctionSelector(ctx, { chatName, mentions })) return } @@ -396,11 +396,17 @@ const manageActionMsg = async (ctx: Context.Data) => { const groupAgent = async (ctx: Context.Data) => { const { larkBody: { isEvent, isAction }, + logger, } = ctx - // 如果是Event,则解析自然语言并发送对应的卡片 - if (isEvent) return manageEventMsg(ctx) - // 如果是Action,则取出用户选的值并判断是否需要继续发送表单卡片或者开始大模型推理 - if (isAction) return manageActionMsg(ctx) + try { + // 如果是Event,则解析自然语言并发送对应的卡片 + if (isEvent) return await manageEventMsg(ctx) + // 如果是Action,则取出用户选的值并判断是否需要继续发送表单卡片或者开始大模型推理 + if (isAction) return await manageActionMsg(ctx) + } catch (e: any) { + logger.error(`Group agent error: ${e.message}`) + return ctx.larkCard.child("groupAgent").genErrorCard("Group agent error") + } } export default groupAgent diff --git a/controller/groupAgent/report.ts b/controller/groupAgent/report.ts new file mode 100644 index 0000000..cd47f22 --- /dev/null +++ b/controller/groupAgent/report.ts @@ -0,0 +1,190 @@ +import { LarkService } from "@egg/net-tool" + +import { appMap } from "../../constant/appMap" +import prisma from "../../prisma" +import { Context } from "../../types" +import genContext from "../../utils/genContext" +import llm from "../../utils/llm" +import { getTimeRange } from "../../utils/time" +import getChatHistory from "./chatHistory" + +interface Subscription { + id: bigint + chat_id: string + robot_id: string + initiator: string + terminator: string + created_at: Date + updated_at: Date +} + +/** + * 总结指定聊天记录 + * @param {Context.Data} ctx - 请求上下文 + * @param {string} timeScope - 时间范围 + * @param {Subscription} subscription - 订阅信息 + * @returns {Promise} + */ +const genReport = async ( + ctx: Context.Data, + timeScope: "daily" | "weekly", + subscription: Subscription +) => { + const { logger, requestId, larkCard } = ctx + const cardGender = larkCard.child("groupAgent") + try { + const { chat_id: chatId, robot_id: robotId } = subscription + // 获取接口信息 + const appInfo = appMap[robotId] + if (!appInfo) { + logger.error(`Failed to get app info for ${robotId}`) + return + } + // 组织接口 + const larkService = new LarkService({ + appId: appInfo.app_id, + appSecret: appInfo.app_secret, + requestId, + }) + // 获取时间范围 + const { startTime, endTime } = getTimeRange(timeScope) + + // 计时开始 + const processStart = Date.now() + + // 获取聊天记录 + const chatHistory = await getChatHistory( + { larkService, logger } as Context.Data, + { + chatId, + startTime, + endTime, + } + ) + 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}` + ) + // 发送卡片消息 + await larkService.message.sendCard2Chat( + chatId, + cardGender.genCard("autoReport", { + llmRes, + timeScope: timeScope === "daily" ? "今日日报" : "本周周报", + }) + ) + // 记录发送的卡片 + await prisma.chat_agent_message_log.create({ + data: { + subscription_id: subscription.id, + initiator: subscription.initiator, + langfuse_link: `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("")) + const { logger } = ctx + + try { + // 获取全部需要自动总结的群组 + const subscriptionList = + await prisma.chat_agent_summary_subscription.findMany({ + where: { + terminator: "", + }, + }) + + if (subscriptionList.length === 0) { + logger.info("No group needs to be summarized") + return + } + + // 一个一个群组的总结,避免触发频率限制 + for (const subscription of subscriptionList) { + await genReport(ctx, timeScope, subscription) + } + } catch (e: any) { + logger.error(`Auto summary error: ${e.message}`) + } +} + +/** + * 立即生成日报或周报(测试用) + * @param {Context.Data} ctx - 请求上下文 + * @returns {Promise} + */ +const gen4Test = async (ctx: Context.Data, 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 subscription = await prisma.chat_agent_summary_subscription.findFirst( + { + where: { + chat_id: chatId, + terminator: "", + }, + } + ) + // 没有订阅信息 + if (!subscription) { + logger.error(`No subscription found for chat ${chatId}`) + await larkService.message.sendCard2Chat( + chatId, + larkCard.genErrorCard("本群未订阅日报、周报") + ) + return + } + // 总结 + await genReport(ctx, timeScope, subscription) + } catch (error: any) { + logger.error(`Failed to summarize chat ${chatId}: ${error.message}`) + } +} + +const report = { + genReport, + genAllReport, + gen4Test, +} + +export default report diff --git a/controller/groupAgent/subscription.ts b/controller/groupAgent/subscription.ts new file mode 100644 index 0000000..0b6a203 --- /dev/null +++ b/controller/groupAgent/subscription.ts @@ -0,0 +1,120 @@ +import { RespMessage } from "../../constant/message" +import prisma from "../../prisma" +import { Context } from "../../types" + +/** + * 注册消息总结的订阅 + * @returns + */ +const subscribe = async ({ + app, + larkService, + logger, + larkBody, + larkCard, +}: Context.Data) => { + try { + const cardGender = larkCard.child("groupAgent") + // 判断是否有 chatId 和 userId + if (!larkBody.chatId || !larkBody.userId) { + logger.error(`chatId or userId is empty`) + return + } + // 先查询是否已经存在订阅 + const subscription = await prisma.chat_agent_summary_subscription.findFirst( + { + where: { + chat_id: larkBody.chatId, + terminator: "", + }, + } + ) + // 如果已经存在订阅,则返回已经注册过了 + if (subscription) { + logger.info(`chatId: ${larkBody.chatId} has been registered`) + // 发送已经注册过的消息 + await larkService.message.sendCard2Chat( + larkBody.chatId, + cardGender.genSuccessCard(RespMessage.hasRegistered) + ) + return + } + // 注册订阅 + await prisma.chat_agent_summary_subscription.create({ + data: { + chat_id: larkBody.chatId, + robot_id: app, + initiator: larkBody.userId, + }, + }) + // 发送成功消息 + await larkService.message.sendCard2Chat( + larkBody.chatId, + cardGender.genSuccessCard(RespMessage.registerSuccess) + ) + } catch (e: any) { + logger.error(`Subscribe error: ${e.message}`) + } +} + +/** + * 取消消息总结的订阅 + * @returns + */ +const unsubscribe = async ({ + logger, + larkBody, + larkService, + larkCard, +}: Context.Data) => { + try { + const cardGender = larkCard.child("groupAgent") + // 判断是否有 chatId 和 userId + if (!larkBody.chatId || !larkBody.userId) { + logger.error(`chatId or userId is empty`) + return + } + // 查找现有的订阅 + const subscription = await prisma.chat_agent_summary_subscription.findFirst( + { + where: { + chat_id: larkBody.chatId, + terminator: "", + }, + } + ) + // 如果没有找到订阅,则返回错误 + if (!subscription) { + logger.info(`chatId: ${larkBody.chatId} has not been registered`) + // 发送已经取消订阅的消息 + await larkService.message.sendCard2Chat( + larkBody.chatId, + cardGender.genSuccessCard(RespMessage.cancelSuccess) + ) + return + } + // 更新订阅,设置终止者和终止时间 + await prisma.chat_agent_summary_subscription.update({ + where: { + id: subscription.id, + }, + data: { + terminator: larkBody.userId, + }, + }) + // 发送成功消息 + await larkService.message.sendCard2Chat( + larkBody.chatId, + cardGender.genSuccessCard(RespMessage.cancelSuccess) + ) + } catch (e: any) { + logger.error(`Unsubscribe error: ${e.message}`) + } +} + +const subscription = { + subscribe, + unsubscribe, +} + +export default subscription diff --git a/docker/deploy/Dockerfile b/docker/deploy/Dockerfile index 475fe5b..0e96c07 100644 --- a/docker/deploy/Dockerfile +++ b/docker/deploy/Dockerfile @@ -2,10 +2,16 @@ FROM micr.cloud.mioffice.cn/zhaoyingbo/bun:alpine-cn WORKDIR /app -COPY . . +COPY package*.json ./ + +COPY bun.lockb ./ RUN bun install +COPY . . + +RUN bunx prisma generate + EXPOSE 3000 -CMD ["bun", "run", "start"] \ No newline at end of file +CMD ["bun", "start"] \ No newline at end of file diff --git a/docker/mysql/docker-compose.yml b/docker/mysql/docker-compose.yml new file mode 100644 index 0000000..fb3d905 --- /dev/null +++ b/docker/mysql/docker-compose.yml @@ -0,0 +1,16 @@ +version: "3.8" + +services: + mysql: + image: micr.cloud.mioffice.cn/zhaoyingbo/mysql:8.0 + container_name: mysql + environment: + MYSQL_ROOT_PASSWORD: rootpassword + MYSQL_DATABASE: testdb + ports: + - "3306:3306" + volumes: + - mysql-data:/var/lib/mysql + +volumes: + mysql-data: diff --git a/index.ts b/index.ts index 7444631..c70b315 100644 --- a/index.ts +++ b/index.ts @@ -1,5 +1,6 @@ import logger from "@egg/logger" +import prisma from "./prisma" import { manageBotReq } from "./routes/bot" import { manageMessageReq } from "./routes/message" import { manageMicroAppReq } from "./routes/microApp" @@ -43,7 +44,18 @@ const server = Bun.serve({ return genResp.serverError(error.message || "server error") } }, + error(error) { + logger.error(`Error: ${error}`) + logger.error(`Stack: ${error.stack}`) + return new Response("Internal Error", { status: 500 }) + }, port: 3000, }) logger.info(`Listening on ${server.hostname}:${server.port}`) + +// 关闭数据库连接 +process.on("SIGINT", async () => { + await prisma.$disconnect() + process.exit(0) +}) diff --git a/package.json b/package.json index 929c70b..2c7443c 100644 --- a/package.json +++ b/package.json @@ -37,12 +37,13 @@ }, "dependencies": { "@egg/hooks": "^1.2.0", - "@egg/lark-msg-tool": "^1.15.1", + "@egg/lark-msg-tool": "^1.17.0", "@egg/logger": "^1.6.0", "@egg/net-tool": "^1.16.1", "@egg/path-tool": "^1.4.1", "@langchain/core": "^0.3.19", "@langchain/openai": "^0.3.14", + "@prisma/client": "^5.22.0", "joi": "^17.13.3", "langfuse-langchain": "^3.31.0", "node-schedule": "^2.1.1", diff --git a/prisma/index.ts b/prisma/index.ts new file mode 100644 index 0000000..fdc7831 --- /dev/null +++ b/prisma/index.ts @@ -0,0 +1,5 @@ +import { PrismaClient } from "@prisma/client" + +const prisma = new PrismaClient() + +export default prisma diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..1e9e1db --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,31 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? +// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "mysql" + url = env("DATABASE_URL") +} + +model chat_agent_summary_subscription { + id BigInt @id @default(autoincrement()) // 摘要订阅 ID + chat_id String @default("") // 关联的聊天 ID + robot_id String @default("") // 机器人 ID + initiator String @default("") // 发起者 ID + terminator String @default("") // 终止者 ID + created_at DateTime @default(now()) // 创建时间 + updated_at DateTime @updatedAt // 更新时间 +} + +model chat_agent_message_log { + id BigInt @id @default(autoincrement()) // 消息日志 ID + subscription_id BigInt @default(0) // 关联的摘要订阅 ID + initiator String @default("") // 发起者 ID + langfuse_link String @default("") // Langfuse 日志 +} diff --git a/routes/bot/actionMsg.ts b/routes/bot/actionMsg.ts index 6f5ef80..4595f79 100644 --- a/routes/bot/actionMsg.ts +++ b/routes/bot/actionMsg.ts @@ -1,5 +1,5 @@ +import groupAgent from "../../controller/groupAgent" import { Context } from "../../types" -import groupAgent from "./groupAgent" const GROUP_MAP = { groupAgent, diff --git a/routes/bot/eventMsg.ts b/routes/bot/eventMsg.ts index 38e4859..b6688a4 100644 --- a/routes/bot/eventMsg.ts +++ b/routes/bot/eventMsg.ts @@ -1,24 +1,9 @@ -import { LarkBody } from "@egg/lark-msg-tool" - import tempMap from "../../constant/template" +import groupAgent from "../../controller/groupAgent" +import report from "../../controller/groupAgent/report" +import subscription from "../../controller/groupAgent/subscription" import { Context } from "../../types" import createKVTemp from "../sheet/createKVTemp" -import groupAgent from "./groupAgent" - -/** - * 是否为P2P或者群聊并且艾特了机器人 - * @param larkBody - */ -const getIsP2pOrGroupAtBot = async ( - larkBody: LarkBody, - appInfo: Context.APP_INFO -): Promise => { - const isP2p = larkBody.chatType === "p2p" - const isAtBot = larkBody.mentions?.some?.( - (mention) => mention.name === appInfo.app_name - ) - return Boolean(isP2p || isAtBot) -} /** * 过滤出非法消息,如果发表情包就直接发回去 @@ -38,7 +23,7 @@ const filterIllegalMsg = async ({ if (!chatId) return true // 非私聊和群聊中艾特机器人的消息不处理 - if (!(await getIsP2pOrGroupAtBot(larkBody, appInfo))) { + if (!larkBody.isP2P && !larkBody.isAtBot(appInfo.app_name)) { return true } @@ -114,7 +99,7 @@ const manageCMDMsg = (ctx: Context.Data) => { logger, larkService, attachService, - larkBody: { msgText, chatId, chatType }, + larkBody: { msgText, chatId, isInGroup }, } = ctx logger.info(`bot req text: ${msgText}`) @@ -126,29 +111,29 @@ const manageCMDMsg = (ctx: Context.Data) => { } // 仅限群组功能 - if (chatType === "group") { + if (isInGroup) { // 注册群组 if (msgText === "开启日报、周报") { logger.info(`bot command is register, chatId: ${chatId}`) - attachService.groupAgent(app, body, "register") + subscription.subscribe(ctx) return } // 注销群组 if (msgText === "关闭日报、周报") { logger.info(`bot command is unregister, chatId: ${chatId}`) - attachService.groupAgent(app, body, "unregister") + subscription.unsubscribe(ctx) return } // 立即发送日简报 if (msgText === "总结日报") { logger.info(`bot command is summary, chatId: ${chatId}`) - attachService.groupAgent(app, body, "summary", "daily") + report.gen4Test(ctx, "daily") return } // 立即发送周简报 if (msgText === "总结周报") { logger.info(`bot command is summary, chatId: ${chatId}`) - attachService.groupAgent(app, body, "summary", "weekly") + report.gen4Test(ctx, "weekly") return } } diff --git a/routes/message/index.ts b/routes/message/index.ts index 2906c8a..192ed69 100644 --- a/routes/message/index.ts +++ b/routes/message/index.ts @@ -1,6 +1,7 @@ import { stringifyJson } from "@egg/hooks" import { LarkService } from "@egg/net-tool" +import { appMap } from "../../constant/appMap" import db from "../../db" import { Context, DB, LarkServer, MsgProxy } from "../../types" @@ -41,7 +42,7 @@ const validateMessageReq = ({ export const manageMessageReq = async ( ctx: Context.Data ): Promise => { - const { body: rawBody, genResp, appMap, requestId } = ctx + const { body: rawBody, genResp, requestId } = ctx const body = rawBody as MsgProxy.Body // 校验参数 diff --git a/routes/microApp/index.ts b/routes/microApp/index.ts index 2d8d25a..20f9702 100644 --- a/routes/microApp/index.ts +++ b/routes/microApp/index.ts @@ -1,5 +1,6 @@ import { LarkService } from "@egg/net-tool" +import { appMap } from "../../constant/appMap" import { Context } from "../../types" /** @@ -8,7 +9,7 @@ import { Context } from "../../types" * @returns */ const manageLogin = async (ctx: Context.Data) => { - const { req, genResp, logger, appMap, requestId } = ctx + const { req, genResp, logger, requestId } = ctx logger.info("micro app login") const url = new URL(req.url) const code = url.searchParams.get("code") @@ -52,7 +53,7 @@ const manageLogin = async (ctx: Context.Data) => { * @returns */ const manageBatchUser = async (ctx: Context.Data) => { - const { body, genResp, logger, appMap, requestId } = ctx + const { body, genResp, logger, requestId } = ctx logger.info("batch get user info") if (!body) return genResp.badRequest("req body is empty") const { user_ids, user_id_type, app_name } = body diff --git a/routes/sheet/index.ts b/routes/sheet/index.ts index f118aab..fd00674 100644 --- a/routes/sheet/index.ts +++ b/routes/sheet/index.ts @@ -1,6 +1,7 @@ import { LarkService } from "@egg/net-tool" import Joi from "joi" +import { appMap } from "../../constant/appMap" import db from "../../db" import { Context } from "../../types" import { SheetProxy } from "../../types/sheetProxy" @@ -64,7 +65,7 @@ const validateSheetReq = async ({ * @returns {Promise} 返回响应对象 */ export const manageSheetReq = async (ctx: Context.Data): Promise => { - const { body: rawBody, genResp, appMap, requestId } = ctx + const { body: rawBody, genResp, requestId } = ctx const body = rawBody as SheetProxy.InsertData // 校验参数 diff --git a/schedule/index.ts b/schedule/index.ts index 88520d4..8a4448a 100644 --- a/schedule/index.ts +++ b/schedule/index.ts @@ -1,6 +1,11 @@ +import schedule from "node-schedule" + +import report from "../controller/groupAgent/report" + export const initSchedule = async () => { - // // 定时任务,每15分钟刷新一次token - // schedule.scheduleJob("*/15 * * * *", resetAccessToken) - // // 立即执行一次 - // resetAccessToken() + // 定时任务,每周一到周四晚上 8 点 0 分 0 秒执行 + schedule.scheduleJob("0 20 * * 1-4", () => report.genAllReport("daily")) + + // 定时任务,每周五晚上 8 点 0 分 0 秒执行 + schedule.scheduleJob("0 20 * * 5", () => report.genAllReport("weekly")) } diff --git a/services/attach/index.ts b/services/attach/index.ts index f64c18b..055b202 100644 --- a/services/attach/index.ts +++ b/services/attach/index.ts @@ -41,25 +41,6 @@ class AttachService extends NetToolBase { await this.post(URL, body).catch(() => "") } } - - /** - * 代理MiChat事件 - * @param {LarkEvent.Data} body - 事件数据。 - * @returns {Promise} 返回空。 - */ - async groupAgent( - app: string, - body: LarkEvent.Data, - func: "register" | "unregister" | "summary", - timeScope?: "daily" | "weekly" - ) { - const URL = `${this.hostMap[Bun.env.NODE_ENV!]}/chat_agent/${func}` - return this.post(URL, body, { - timeScope, - app, - secret: Bun.env.ATTACH_APP_SECRET, - }).catch(() => "") - } } export default AttachService diff --git a/test/groupAgent/register.http b/test/groupAgent/register.http new file mode 100644 index 0000000..cc9bd60 --- /dev/null +++ b/test/groupAgent/register.http @@ -0,0 +1,4 @@ +POST http://localhost:3000/bot?app=egg HTTP/1.1 +content-type: application/json + +{"schema":"2.0","header":{"event_id":"c94518fbcb9d66cc93b4144cc69e4f0c","token":"tV9djUKSjzVnekV7xTg2Od06NFTcsBnj","create_time":"1732612280153","event_type":"im.message.receive_v1","tenant_key":"2ee61fe50f4f1657","app_id":"cli_a1eff35b43b89063"},"event":{"message":{"chat_id":"oc_8c789ce8f4ecc6695bb63ca6ec4c61ea","chat_type":"group","content":"{\"text\":\"@_user_1 开启日报、周报\"}","create_time":"1732612279943","mentions":[{"id":{"open_id":"ou_032f507d08f9a7f28b042fcd086daef5","union_id":"on_7111660fddd8302ce47bf1999147c011","user_id":""},"key":"@_user_1","name":"小煎蛋","tenant_key":"2ee61fe50f4f1657"}],"message_id":"om_4ab57cb6e889eeb81ca061b137238189","message_type":"text","update_time":"1732612279943"},"sender":{"sender_id":{"open_id":"ou_470ac13b8b50fc472d9d8ee71e03de26","union_id":"on_9dacc59a539023df8b168492f5e5433c","user_id":"zhaoyingbo"},"sender_type":"user","tenant_key":"2ee61fe50f4f1657"}}} \ No newline at end of file diff --git a/test/groupAgent/summary.http b/test/groupAgent/summary.http new file mode 100644 index 0000000..44ee03b --- /dev/null +++ b/test/groupAgent/summary.http @@ -0,0 +1,4 @@ +POST http://localhost:3000/bot?app=egg HTTP/1.1 +content-type: application/json + +{"schema":"2.0","header":{"event_id":"ad5a706e2175e4bc539da53fd54b6fa0","token":"tV9djUKSjzVnekV7xTg2Od06NFTcsBnj","create_time":"1732619538646","event_type":"im.message.receive_v1","tenant_key":"2ee61fe50f4f1657","app_id":"cli_a1eff35b43b89063"},"event":{"message":{"chat_id":"oc_8c789ce8f4ecc6695bb63ca6ec4c61ea","chat_type":"group","content":"{\"text\":\"@_user_1 总结日报\"}","create_time":"1732619538450","mentions":[{"id":{"open_id":"ou_032f507d08f9a7f28b042fcd086daef5","union_id":"on_7111660fddd8302ce47bf1999147c011","user_id":""},"key":"@_user_1","name":"小煎蛋","tenant_key":"2ee61fe50f4f1657"}],"message_id":"om_988659d8dcb79a54a02016ea1884e3df","message_type":"text","update_time":"1732619538450"},"sender":{"sender_id":{"open_id":"ou_470ac13b8b50fc472d9d8ee71e03de26","union_id":"on_9dacc59a539023df8b168492f5e5433c","user_id":"zhaoyingbo"},"sender_type":"user","tenant_key":"2ee61fe50f4f1657"}}} \ No newline at end of file diff --git a/test/groupAgent/unregister.http b/test/groupAgent/unregister.http new file mode 100644 index 0000000..0d45138 --- /dev/null +++ b/test/groupAgent/unregister.http @@ -0,0 +1,4 @@ +POST http://localhost:3000/bot?app=egg HTTP/1.1 +content-type: application/json + +{"schema":"2.0","header":{"event_id":"6d212cfdfa9e5bc91e008eabef0c6288","token":"tV9djUKSjzVnekV7xTg2Od06NFTcsBnj","create_time":"1732613767405","event_type":"im.message.receive_v1","tenant_key":"2ee61fe50f4f1657","app_id":"cli_a1eff35b43b89063"},"event":{"message":{"chat_id":"oc_8c789ce8f4ecc6695bb63ca6ec4c61ea","chat_type":"group","content":"{\"text\":\"@_user_1 关闭日报、周报\"}","create_time":"1732613767202","mentions":[{"id":{"open_id":"ou_032f507d08f9a7f28b042fcd086daef5","union_id":"on_7111660fddd8302ce47bf1999147c011","user_id":""},"key":"@_user_1","name":"小煎蛋","tenant_key":"2ee61fe50f4f1657"}],"message_id":"om_344daf29eed184d808f52a73a1c2644d","message_type":"text","update_time":"1732613767202"},"sender":{"sender_id":{"open_id":"ou_470ac13b8b50fc472d9d8ee71e03de26","union_id":"on_9dacc59a539023df8b168492f5e5433c","user_id":"zhaoyingbo"},"sender_type":"user","tenant_key":"2ee61fe50f4f1657"}}} \ No newline at end of file diff --git a/types/context.ts b/types/context.ts index a823225..d5efa36 100644 --- a/types/context.ts +++ b/types/context.ts @@ -3,17 +3,13 @@ import { LarkService, NetTool } from "@egg/net-tool" import { PathCheckTool } from "@egg/path-tool" import { Logger } from "winston" +import { AppInfo } from "../constant/appMap" import cardMap from "../constant/card" import functionMap from "../constant/function" import tempMap from "../constant/template" import { AttachService } from "../services" export namespace Context { - export interface APP_INFO { - app_id: string - app_secret: string - app_name: string - } export interface Data { req: Request requestId: string @@ -28,7 +24,6 @@ export namespace Context { path: PathCheckTool searchParams: URLSearchParams app: "michat" | "egg" | string - appInfo: APP_INFO - appMap: Record + appInfo: AppInfo } } diff --git a/utils/genContext.ts b/utils/genContext.ts index 4f9adbd..a61885a 100644 --- a/utils/genContext.ts +++ b/utils/genContext.ts @@ -1,22 +1,16 @@ -import { parseJsonString } from "@egg/hooks" import { LarkBody, LarkCard } from "@egg/lark-msg-tool" import loggerIns from "@egg/logger" import { LarkService, NetTool } from "@egg/net-tool" import { PathCheckTool } from "@egg/path-tool" import { v4 as uuid } from "uuid" +import { appMap } from "../constant/appMap" import cardMap from "../constant/card" import functionMap from "../constant/function" import tempMap from "../constant/template" import { AttachService } from "../services" import { Context } from "../types" -// 获取所有应用信息 -const appMap = parseJsonString(process.env.LARK_APP_MAP, {}) as Record< - string, - Context.APP_INFO -> - /** * 获取之前的requestId。 * @@ -83,7 +77,6 @@ const genContext = async (req: Request) => { searchParams, app, appInfo, - appMap, } as Context.Data } diff --git a/utils/time.ts b/utils/time.ts new file mode 100644 index 0000000..a15a426 --- /dev/null +++ b/utils/time.ts @@ -0,0 +1,30 @@ +/** + * 获取指定时间范围的开始时间和结束时间 + * @param {("daily" | "weekly")} timeScope - 时间范围,可以是 "daily" 或 "weekly" + * @returns {{ startTime: string, endTime: string }} - 格式化后的开始时间和结束时间 + */ +export const getTimeRange = (timeScope: "daily" | "weekly") => { + const formatDate = (date: Date) => { + const pad = (num: number) => (num < 10 ? "0" + num : num) + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` + } + + const now = new Date() + const startTime = new Date(now) + if (timeScope === "daily") { + startTime.setHours(0, 0, 0, 0) + } else { + const day = now.getDay() + const diff = day === 0 ? 6 : day - 1 // adjust when day is sunday + startTime.setDate(now.getDate() - diff) + startTime.setHours(0, 0, 0, 0) + } + + const endTime = new Date(now) + endTime.setHours(23, 59, 59, 999) + + return { + startTime: formatDate(startTime), + endTime: formatDate(endTime), + } +}