diff --git a/bun.lockb b/bun.lockb index 4949ab6..244d5e1 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/controller/intentAgent/index.ts b/controller/intentAgent/index.ts index df8cb99..82642f2 100644 --- a/controller/intentAgent/index.ts +++ b/controller/intentAgent/index.ts @@ -17,16 +17,21 @@ const BaseIntentSchema = z.object({ * 简报模式 */ const BriefingSchema = BaseIntentSchema.extend({ - intent: z.literal(3), + intent: z.literal(5), link: z.string().url(), userDescription: z.string().min(1), }) +const BriefingLinkSchema = z.object({ + intent: z.literal(14), + link: z.string().url(), +}) + /** * 通用响应模式 */ const CommonResponseSchema = BaseIntentSchema.extend({ - intent: z.literal(12), + intent: z.literal(2), message: z.string().min(1), }) @@ -37,10 +42,12 @@ const IntentSchema = z.union([ BriefingSchema, CommonResponseSchema, BaseIntentSchema, + BriefingLinkSchema, ]) type BaseIntent = z.infer type Briefing = z.infer +type BriefingLink = z.infer type CommonResponse = z.infer export type Intent = z.infer @@ -93,20 +100,24 @@ const agent = async (ctx: Context): Promise => { } return { - intent: 13, + intent: 1, } } const isBaseIntent = (intent: Intent): intent is BaseIntent => { - return intent.intent !== 3 && intent.intent !== 12 + return intent.intent !== 5 && intent.intent !== 2 } const isBriefing = (intent: Intent): intent is Briefing => { - return intent.intent === 3 + return intent.intent === 5 +} + +const isBriefingLink = (intent: Intent): intent is BriefingLink => { + return intent.intent === 14 } const isCommonResponse = (intent: Intent): intent is CommonResponse => { - return intent.intent === 12 + return intent.intent === 2 } /** @@ -117,6 +128,7 @@ const intentAgent = { isBaseIntent, isBriefing, isCommonResponse, + isBriefingLink, } export default intentAgent diff --git a/controller/reportAgent/index.ts b/controller/reportAgent/index.ts index d6943f6..5168fe4 100644 --- a/controller/reportAgent/index.ts +++ b/controller/reportAgent/index.ts @@ -1,4 +1,5 @@ import db from "../../db" +import { SheetModel } from "../../db/sheet" import { Context } from "../../types" import llm from "../../utils/llm" import { extractSheetIds, validateLink } from "../../utils/string" @@ -65,35 +66,11 @@ const insert2Sheet = async ( } = ctx try { const chat = await db.chat.getAndCreate(ctx) - if (!chat?.webSummarySheetLink) { - logger.info("No webSummarySheetLink found, skip insert2Sheet") + if (!chat || !chat?.sheet || !chat?.expand?.sheet) { + logger.info("No sheet found, skip insert2Sheet") return "" } - let sheetToken = "" - let range = "" - if (chat.webSummarySheetLink.includes("wiki")) { - const extractRes = extractSheetIds(chat.webSummarySheetLink) - if (!extractRes) { - logger.error("Failed to extract sheetToken and range") - return "" - } - const wikiData = await larkService.wiki.getNodeInfo(extractRes.sheetToken) - if (!wikiData || wikiData.code) { - logger.error("Failed to get wiki data") - return "" - } - sheetToken = wikiData.data.obj_token - range = extractRes.range - } else { - const extractRes = extractSheetIds(chat.webSummarySheetLink) - if (!extractRes) { - logger.error("Failed to extract sheetToken and range") - return "" - } - sheetToken = extractRes.sheetToken - range = extractRes.range - } - + const { sheetToken, range, sheetUrl } = chat.expand.sheet as SheetModel await larkService.sheet.insertRows(sheetToken, range, [ [ userId || "", @@ -103,7 +80,7 @@ const insert2Sheet = async ( new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }), ], ]) - return chat.webSummarySheetLink + return sheetUrl } catch (error: any) { logger.error(`Failed to insert2Sheet: ${error}`) return "" @@ -191,8 +168,93 @@ const agent = async (ctx: Context, link: string, userDescription: string) => { } } +/** + * 解析表格信息 + * @param {Context} ctx - 上下文对象 + * @param {string} sheetUrl - 表格链接 + * @returns {Promise<{sheetToken: string, range: string}>} - 返回表格信息 + * @throws {Error} - 当解析失败时抛出错误 + */ +const parseSheetInfo = async ( + ctx: Context, + sheetUrl: string +): Promise<{ sheetToken: string; range: string }> => { + const { larkService } = ctx + const token = extractSheetIds(sheetUrl) + if (!token || !token.sheetToken) throw new Error("链接格式错误") + + const getRange = async (sheetToken: string) => { + const res = await larkService.sheet.getMetaInfo(sheetToken) + if (!res || !res?.data?.sheets?.length) + throw new Error("获取工作表信息失败") + return res.data.sheets[0].sheetId + } + // 如果不是Wiki链接,直接提取sheetToken和range + if (!sheetUrl.includes("wiki")) { + if (token.range) return token as { sheetToken: string; range: string } + // 获取第一个工作表作为range + token.range = await getRange(token.sheetToken) + return token as { sheetToken: string; range: string } + } + // 如果是Wiki链接,需要先获取sheetToken + const res = await larkService.wiki.getNodeInfo(token?.sheetToken) + if (!res || !res?.data?.node?.obj_token) throw new Error("获取Wiki信息失败") + token.sheetToken = res.data.node.obj_token + if (token.range) return token as { sheetToken: string; range: string } + token.range = await getRange(token.sheetToken) + return token as { sheetToken: string; range: string } +} + +/** + * 设置简报表格 + * @param {Context} ctx - 上下文对象 + * @param {string} sheetUrl - 表格链接 + */ +const setSheet = async (ctx: Context, sheetUrl: string) => { + const { larkCard, larkService, logger } = ctx + const cardGender = larkCard.child("webAgent") + try { + // 获取chat信息 + const chat = await db.chat.getAndCreate(ctx) + if (!chat) throw new Error("获取聊天信息失败") + // 获取是否已经有存在的sheet信息 + const existSheet = await db.sheet.getByUrl(sheetUrl) + // 如果已经存在,则直接写入chat表 + if (existSheet) { + const updateRes = await db.chat.update(chat.id, { sheet: existSheet.id }) + if (!updateRes) throw new Error("更新chat记录失败") + await larkService.message.updateOrReply( + cardGender.genSuccessCard("简报汇总表设置成功") + ) + return + } + + // 获取sheet的token和range + const sheetInfo = await parseSheetInfo(ctx, sheetUrl) + + // 创建sheet记录 + const createRes = await db.sheet.create({ + sheetToken: sheetInfo.sheetToken, + range: sheetInfo.range, + sheetUrl, + }) + if (!createRes) throw new Error("创建sheet记录失败") + const updateRes = await db.chat.update(chat.id, { sheet: createRes.id }) + if (!updateRes) throw new Error("更新chat记录失败") + await larkService.message.updateOrReply( + cardGender.genSuccessCard("简报汇总表创建成功") + ) + } catch (error: any) { + logger.error(`Failed setSheet: ${error}`) + await larkService.message.updateOrReply( + cardGender.genErrorCard(`简报汇总表设置失败: ${error.message}`) + ) + } +} + const reportAgent = { agent, + setSheet, // 添加 setSheet 到导出的对象中 } export default reportAgent diff --git a/db/chat/index.ts b/db/chat/index.ts index 638b0b1..a7a16cb 100644 --- a/db/chat/index.ts +++ b/db/chat/index.ts @@ -3,6 +3,7 @@ import { RecordModel } from "pocketbase" import { Context } from "../../types" import { managePbError } from "../../utils/pbTools" import pbClient from "../pbClient" +import { SheetModel } from "../sheet" const DB_NAME = "chat" @@ -13,19 +14,29 @@ export interface Chat { mode: "group" | "p2p" | "topic" weeklySummary: boolean dailySummary: boolean - webSummarySheetLink: string + sheet: string } export type ChatModel = Chat & RecordModel +export interface ChatModelWithExpand extends ChatModel { + expand: { + sheet: SheetModel + } +} + /** * 获取单个群组信息 * @param id * @returns */ const getOneByChatId = (chatId: string) => - managePbError(() => - pbClient.collection(DB_NAME).getFirstListItem(`chatId = "${chatId}"`) + managePbError(() => + pbClient + .collection(DB_NAME) + .getFirstListItem(`chatId = "${chatId}"`, { + expand: "sheet", + }) ) /** @@ -61,11 +72,20 @@ const getAndCreate = async ({ larkService, logger, larkBody }: Context) => { mode: chat_mode, weeklySummary: false, dailySummary: false, - webSummarySheetLink: "", + sheet: "", } return create(newChat) } +/** + * 更新群组 + * @param id + * @param chat + * @returns + */ +const update = (id: string, chat: Partial) => + managePbError(() => pbClient.collection(DB_NAME).update(id, chat)) + /** * 更新群组总结 * @param id @@ -99,6 +119,7 @@ const getNeedSummaryChats = async (timeScope: "daily" | "weekly" | "all") => { } const chat = { + update, getAndCreate, getOneByChatId, updateSummary, diff --git a/db/index.ts b/db/index.ts index bba1bf3..2989840 100644 --- a/db/index.ts +++ b/db/index.ts @@ -4,6 +4,7 @@ import gitlabProject from "./gitlabProject/index." import grpSumLog from "./grpSumLog" import log from "./log" import receiveGroup from "./receiveGroup" +import sheet from "./sheet" import soupGame from "./soupGame" import user from "./user" @@ -16,6 +17,7 @@ const db = { grpSumLog, gitlabProject, soupGame, + sheet, } export default db diff --git a/db/sheet/index.ts b/db/sheet/index.ts new file mode 100644 index 0000000..f46062a --- /dev/null +++ b/db/sheet/index.ts @@ -0,0 +1,44 @@ +import { RecordModel } from "pocketbase" + +import { managePbError } from "../../utils/pbTools" +import pbClient from "../pbClient" + +const DB_NAME = "sheet" + +export interface Sheet { + /*** + * 表格区间 + */ + range: string + /*** + * 表格Token + */ + sheetToken: string + /*** + * 表格链接 + */ + sheetUrl: string +} + +export type SheetModel = Sheet & RecordModel + +const getByUrl = (sheetUrl: string) => + managePbError(() => + pbClient.collection(DB_NAME).getFirstListItem(`sheetUrl = "${sheetUrl}"`) + ) + +const update = (id: string, sheet: Partial) => + managePbError(() => + pbClient.collection(DB_NAME).update(id, sheet) + ) + +const create = (sheet: Sheet) => + managePbError(() => pbClient.collection(DB_NAME).create(sheet)) + +const sheet = { + getByUrl, + update, + create, +} + +export default sheet diff --git a/package.json b/package.json index 16d4d46..ac06a14 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "@eslint/js": "^9.19.0", "@types/node-schedule": "^2.1.7", "@types/uuid": "^10.0.0", - "bun-types": "^1.2.0", + "bun-types": "^1.2.1", "eslint": "^9.19.0", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^4.1.4", @@ -30,7 +30,7 @@ "lint-staged": "^15.4.3", "oxlint": "^0.13.2", "prettier": "^3.4.2", - "typescript-eslint": "^8.21.0" + "typescript-eslint": "^8.22.0" }, "peerDependencies": { "typescript": "^5.5.4" @@ -39,7 +39,7 @@ "@egg/hooks": "^1.2.0", "@egg/lark-msg-tool": "^1.21.0", "@egg/logger": "^1.6.0", - "@egg/net-tool": "^1.30.2", + "@egg/net-tool": "^1.31.1", "@egg/path-tool": "^1.4.1", "@langchain/core": "^0.3.36", "@langchain/langgraph": "^0.2.41", diff --git a/routes/bot/eventMsg.ts b/routes/bot/eventMsg.ts index 0e1d3bd..0b2bd32 100644 --- a/routes/bot/eventMsg.ts +++ b/routes/bot/eventMsg.ts @@ -83,7 +83,13 @@ const manageIntent = async (ctx: Context) => { const intentRes = await intentAgent.agent(ctx) logger.info(`intentRes: ${JSON.stringify(intentRes)}`) if (intentAgent.isBriefing(intentRes)) { - reportAgent.agent(ctx, intentRes.link, intentRes.userDescription) + reportAgent + .agent(ctx, intentRes.link, intentRes.userDescription) + .catch(() => null) + return + } + if (intentAgent.isBriefingLink(intentRes)) { + reportAgent.setSheet(ctx, intentRes.link).catch(() => null) return } if (intentAgent.isCommonResponse(intentRes)) { diff --git a/services/attach/index.ts b/services/attach/index.ts index e58423f..09d6b37 100644 --- a/services/attach/index.ts +++ b/services/attach/index.ts @@ -93,7 +93,7 @@ class AttachService extends NetToolBase { * @returns {Promise} 返回抓取的网页内容。 */ async crawlWeb(url: string) { - const URL = "https://lark-egg.ai.xiaomi.com/tools/web/crawl" + const URL = "https://lark-egg.ai.xiaomi.com/tools/web/crawler" return this.get>(URL, { url }).catch(() => null) } } diff --git a/test/reportAgent/setUrl.http b/test/reportAgent/setUrl.http new file mode 100644 index 0000000..af5ad4d --- /dev/null +++ b/test/reportAgent/setUrl.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 https://xiaomi.f.mioffice.cn/wiki/V4ZkwhDR8iRCqIk7X81k1rBj4Sc?sheet=a7a0f7\"}","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/utils/string.ts b/utils/string.ts index db6c8a3..e24ed49 100644 --- a/utils/string.ts +++ b/utils/string.ts @@ -2,17 +2,17 @@ * 从小米链接中提取 sheetToken 和 range。 * * @param {string} url - 要提取数据的URL。 - * @returns {{sheetToken: string, range: string} | null} - 包含 sheetToken 和 range 的对象,如果没有匹配则返回 null。 + * @returns {{sheetToken: string, range: string | null} | null} - 包含 sheetToken 和 range 的对象,如果没有匹配则返回 null。 */ export const extractSheetIds = (url: string) => { // 定义匹配 wiki 和 sheets 两种URL格式的正则表达式 - const pattern = - /wiki\/([\w\d]+)\?sheet=([\w\d]+)|sheets\/([\w\d]+)\?sheet=([\w\d]+)/ + const pattern = /(?:wiki|sheets)\/([\w\d]+)(?:\?sheet=([\w\d]+))?/ const match = url.match(pattern) + console.log("🚀 ~ extractSheetIds ~ match:", match) if (match) { return { - sheetToken: match[1] || match[3], // 对于第一种URL格式,sheetToken 在组1;对于第二种格式,在组3 - range: match[2] || match[4], // range 在第一种URL格式中是组2,在第二种格式是组4 + sheetToken: match[1], // sheetToken 在组1 + range: match[2] || null, // range 在组2,如果没有则返回 null } } return null // 如果没有匹配,则返回 null