feat: 支持自动获取总结用的sheet文档,同时支持wiiki

This commit is contained in:
zhaoyingbo 2025-01-28 13:01:39 +00:00
parent fe3783bdea
commit e695a5836c
11 changed files with 199 additions and 48 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -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<typeof BaseIntentSchema>
type Briefing = z.infer<typeof BriefingSchema>
type BriefingLink = z.infer<typeof BriefingLinkSchema>
type CommonResponse = z.infer<typeof CommonResponseSchema>
export type Intent = z.infer<typeof IntentSchema>
@ -93,20 +100,24 @@ const agent = async (ctx: Context): Promise<Intent> => {
}
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

View File

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

View File

@ -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<ChatModel>(() =>
pbClient.collection(DB_NAME).getFirstListItem(`chatId = "${chatId}"`)
managePbError<ChatModelWithExpand>(() =>
pbClient
.collection<ChatModelWithExpand>(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<Chat>) =>
managePbError<ChatModel>(() => 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,

View File

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

44
db/sheet/index.ts Normal file
View File

@ -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<SheetModel>(() =>
pbClient.collection(DB_NAME).getFirstListItem(`sheetUrl = "${sheetUrl}"`)
)
const update = (id: string, sheet: Partial<SheetModel>) =>
managePbError<SheetModel>(() =>
pbClient.collection(DB_NAME).update(id, sheet)
)
const create = (sheet: Sheet) =>
managePbError<SheetModel>(() => pbClient.collection(DB_NAME).create(sheet))
const sheet = {
getByUrl,
update,
create,
}
export default sheet

View File

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

View File

@ -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)) {

View File

@ -93,7 +93,7 @@ class AttachService extends NetToolBase {
* @returns {Promise<string>}
*/
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<LarkServer.BaseRes<string>>(URL, { url }).catch(() => null)
}
}

View File

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

View File

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