Compare commits

...

9 Commits

22 changed files with 777 additions and 129 deletions

View File

@ -24,6 +24,7 @@
"metas", "metas",
"MIAI", "MIAI",
"michat", "michat",
"mify",
"mina", "mina",
"mindnote", "mindnote",
"openai", "openai",

BIN
bun.lockb

Binary file not shown.

View File

@ -1,9 +1,14 @@
const functionMap = { const functionMap = {
egg: { egg: {
xName: "小煎蛋", xName: "小煎蛋",
xAuthor: "zhaoyingbo", xAuthor: "Yingbo",
xIcon: "🍳", xIcon: "🍳",
}, },
webAgent: {
xName: "小煎蛋 简报助手",
xAuthor: "Yingbo",
xIcon: "📎",
},
groupAgent: { groupAgent: {
xName: "小煎蛋 Group Agent", xName: "小煎蛋 Group Agent",
xAuthor: "YIBinary ❤️ Yingbo", xAuthor: "YIBinary ❤️ Yingbo",
@ -11,12 +16,12 @@ const functionMap = {
}, },
sheetDB: { sheetDB: {
xName: "小煎蛋 Sheet DB", xName: "小煎蛋 Sheet DB",
xAuthor: "zhaoyingbo", xAuthor: "Yingbo",
xIcon: "🍪", xIcon: "🍪",
}, },
gitlabAgent: { gitlabAgent: {
xName: "小煎蛋 Gitlab Agent", xName: "小煎蛋 Gitlab Agent",
xAuthor: "zhaoyingbo", xAuthor: "Yingbo",
xIcon: "🐙", xIcon: "🐙",
}, },
soupAgent: { soupAgent: {

View File

@ -35,12 +35,6 @@ const agent = async (ctx: Context) => {
excludedMessageIds: [loadingMessageId, messageId], excludedMessageIds: [loadingMessageId, messageId],
excludeMentions: [appInfo.appName], excludeMentions: [appInfo.appName],
}) })
// 如果没有聊天记录,返回错误信息
if (chatHistory.length === 0) {
logger.info("No chat history found")
await message.updateOrReply(cardGender.genErrorCard("未找到聊天记录"))
return
}
logger.debug(`Chat history: ${JSON.stringify(chatHistory)}`) logger.debug(`Chat history: ${JSON.stringify(chatHistory)}`)
// 根据Mention拼装原始消息 // 根据Mention拼装原始消息

View File

@ -0,0 +1,135 @@
import { parseJsonString } from "@egg/hooks"
import { z } from "zod"
import { zodToJsonSchema } from "zod-to-json-schema"
import { Context } from "../../types"
import llm from "../../utils/llm"
import { cleanLLMRes } from "../../utils/llm/base"
/**
*
*/
const BaseIntentSchema = z.object({
intent: z.number().min(1).max(13),
})
/**
*
*/
const BriefingSchema = BaseIntentSchema.extend({
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(2),
message: z.string().min(1),
})
/**
*
*/
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>
/**
* JSON模式
*/
const jsonSchema = zodToJsonSchema(IntentSchema)
/**
*
* @param {Context} ctx -
* @returns {Promise<Intent>} -
*/
const agent = async (ctx: Context): Promise<Intent> => {
const {
larkBody: { msgText },
logger,
requestId,
} = ctx
let attempts = 0
while (attempts < 3) {
const res = await llm.invoke(
"intentRecognitionNext",
{
userInput: msgText,
time: new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }),
jsonSchema,
},
requestId,
0.5,
true
)
const rawIntent = cleanLLMRes(res as string)
const parsedIntent = parseJsonString(rawIntent, null)
if (parsedIntent) {
try {
IntentSchema.parse(parsedIntent)
logger.debug("Intent is valid: " + JSON.stringify(parsedIntent))
return parsedIntent
} catch (e: any) {
logger.error("Invalid intent: " + String(e.errors))
}
} else {
logger.error("Parsed intent is null")
}
attempts++
}
return {
intent: 1,
}
}
const isBaseIntent = (intent: Intent): intent is BaseIntent => {
return intent.intent !== 5 && intent.intent !== 2
}
const isBriefing = (intent: Intent): intent is Briefing => {
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 === 2
}
/**
*
*/
const intentAgent = {
agent,
isBaseIntent,
isBriefing,
isCommonResponse,
isBriefingLink,
}
export default intentAgent

View File

@ -0,0 +1,266 @@
import db from "../../db"
import { SheetModel } from "../../db/sheet"
import { Context } from "../../types"
import llm from "../../utils/llm"
import { extractSheetIds, validateLink } from "../../utils/string"
/**
*
* @param {Context} ctx -
* @param {string} link -
* @returns {Promise<any>} -
* @throws {Error} -
*/
export const crawlWebPage = async (
ctx: Context,
link: string
): Promise<any> => {
const { attachService } = ctx
const crawRes = await attachService.crawlWeb(link)
if (!crawRes || crawRes?.code) throw new Error("网页抓取失败")
return crawRes
}
/**
*
* @param {Context} ctx -
* @param {string} userDescription -
* @param {any} content -
* @returns {Promise<string>} -
* @throws {Error} -
*/
export const generateSummary = async (
ctx: Context,
userDescription: string,
content: any
): Promise<string> => {
const { requestId } = ctx
const llmRes = (await llm.invoke(
"summaryWeb",
{
description: userDescription,
content: content,
},
requestId,
1
)) as string
if (!llmRes) throw new Error("模型总结失败")
return llmRes
}
/**
*
* @param {Context} ctx -
* @param {string} link -
* @param {string} userDescription -
* @param {string} llmRes -
* @returns {Promise<string>} -
*/
const insert2Sheet = async (
ctx: Context,
link: string,
userDescription: string,
llmRes: string
) => {
const {
larkBody: { userId },
logger,
larkService,
} = ctx
try {
const chat = await db.chat.getAndCreate(ctx)
if (!chat || !chat?.sheet || !chat?.expand?.sheet) {
logger.info("No sheet found, skip insert2Sheet")
return ""
}
const { sheetToken, range, sheetUrl } = chat.expand.sheet as SheetModel
await larkService.sheet.insertRows(sheetToken, range, [
[
userId || "",
link,
userDescription,
llmRes,
new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }),
],
])
return sheetUrl
} catch (error: any) {
logger.error(`Failed to insert2Sheet: ${error}`)
return ""
}
}
/**
*
* @param {Context} ctx -
* @param {string} link -
* @param {string} userDescription -
* @param {string} llmRes -
* @param {string} sheetLink -
* @returns {any} -
*/
const genReportCard = (
ctx: Context,
link: string,
userDescription: string,
llmRes: string,
sheetLink: string
) => {
const { larkCard } = ctx
const cardGender = larkCard.child("webAgent")
const description = userDescription
? `**用户描述📝**\n${userDescription}\n`
: ""
const sheetLinkMd = sheetLink
? `**小提示🔍**
[](${sheetLink})`
: ""
const content = `
使🍳
**🌟**
[${link}](${link})
${description}
**AI简报🌈**
${llmRes}
${sheetLinkMd}
`
return cardGender.genCard("markdownSuccessCard", {
content,
})
}
/**
*
* @param {Context} ctx -
* @param {string} link -
* @param {string} userDescription -
*/
const agent = async (ctx: Context, link: string, userDescription: string) => {
const {
larkService: { message },
larkCard,
logger,
attachService,
} = ctx
const cardGender = larkCard.child("webAgent")
try {
// 校验链接是否合法
validateLink(link)
// 发送一个loading卡片
await message.updateOrReply(
cardGender.genSuccessCard("正在为您收集简报,请稍等片刻~")
)
// // 抓取网页
// const crawRes = await crawlWebPage(ctx, link)
// // 调用模型生成简报
// const llmRes = await generateSummary(ctx, userDescription, crawRes)
// 调用mify服务生成简报
const llmRes = await attachService.mifyCrawler(link, userDescription)
// 插入到表格
const sheetLink = await insert2Sheet(ctx, link, userDescription, llmRes)
// 发送简报卡片
await message.updateOrReply(
genReportCard(ctx, link, userDescription, llmRes, sheetLink)
)
} catch (error: any) {
logger.error(`Failed gen report: ${error}`)
await message.updateOrReply(
cardGender.genErrorCard(`简报生成失败: ${error.message}`)
)
}
}
/**
*
* @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

@ -131,10 +131,8 @@ const chat2Soup = async (ctx: Context) => {
larkBody: { msgText, chatId, messageId }, larkBody: { msgText, chatId, messageId },
logger, logger,
attachService, attachService,
larkCard,
larkService: { message }, larkService: { message },
} = ctx } = ctx
const cardGender = larkCard.child("soupAgent")
message.setReplyMessage(messageId, "text") message.setReplyMessage(messageId, "text")
const activeGame = await db.soupGame.getActiveOneByChatId(chatId) const activeGame = await db.soupGame.getActiveOneByChatId(chatId)
if (!activeGame) { if (!activeGame) {
@ -150,9 +148,7 @@ const chat2Soup = async (ctx: Context) => {
}) })
if (!res) { if (!res) {
logger.error(`chatId: ${chatId} failed to get soup result`) logger.error(`chatId: ${chatId} failed to get soup result`)
await message.updateOrReply( await message.updateOrReply(SoupGameMessage.chatFailed)
cardGender.genErrorCard(SoupGameMessage.chatFailed)
)
return return
} }
// 用户答对了 // 用户答对了

View File

@ -3,6 +3,7 @@ import { RecordModel } from "pocketbase"
import { Context } from "../../types" import { Context } from "../../types"
import { managePbError } from "../../utils/pbTools" import { managePbError } from "../../utils/pbTools"
import pbClient from "../pbClient" import pbClient from "../pbClient"
import { SheetModel } from "../sheet"
const DB_NAME = "chat" const DB_NAME = "chat"
@ -13,18 +14,29 @@ export interface Chat {
mode: "group" | "p2p" | "topic" mode: "group" | "p2p" | "topic"
weeklySummary: boolean weeklySummary: boolean
dailySummary: boolean dailySummary: boolean
sheet: string
} }
export type ChatModel = Chat & RecordModel export type ChatModel = Chat & RecordModel
export interface ChatModelWithExpand extends ChatModel {
expand: {
sheet: SheetModel
}
}
/** /**
* *
* @param id * @param id
* @returns * @returns
*/ */
const getOneByChatId = (chatId: string) => const getOneByChatId = (chatId: string) =>
managePbError<ChatModel>(() => managePbError<ChatModelWithExpand>(() =>
pbClient.collection(DB_NAME).getFirstListItem(`chatId = "${chatId}"`) pbClient
.collection<ChatModelWithExpand>(DB_NAME)
.getFirstListItem(`chatId = "${chatId}"`, {
expand: "sheet",
})
) )
/** /**
@ -60,10 +72,20 @@ const getAndCreate = async ({ larkService, logger, larkBody }: Context) => {
mode: chat_mode, mode: chat_mode,
weeklySummary: false, weeklySummary: false,
dailySummary: false, dailySummary: false,
sheet: "",
} }
return create(newChat) 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 * @param id
@ -97,6 +119,7 @@ const getNeedSummaryChats = async (timeScope: "daily" | "weekly" | "all") => {
} }
const chat = { const chat = {
update,
getAndCreate, getAndCreate,
getOneByChatId, getOneByChatId,
updateSummary, updateSummary,

View File

@ -4,6 +4,7 @@ import gitlabProject from "./gitlabProject/index."
import grpSumLog from "./grpSumLog" import grpSumLog from "./grpSumLog"
import log from "./log" import log from "./log"
import receiveGroup from "./receiveGroup" import receiveGroup from "./receiveGroup"
import sheet from "./sheet"
import soupGame from "./soupGame" import soupGame from "./soupGame"
import user from "./user" import user from "./user"
@ -16,6 +17,7 @@ const db = {
grpSumLog, grpSumLog,
gitlabProject, gitlabProject,
soupGame, soupGame,
sheet,
} }
export default db 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

@ -12,8 +12,10 @@ initSchedule()
await initAppConfig() await initAppConfig()
const server = Bun.serve({ const bunServer = Bun.serve({
async fetch(req) { fetch: async (req, server) => {
// 设置超时时间
server.timeout(req, 30)
// 生成上下文 // 生成上下文
const ctx = await genContext(req) const ctx = await genContext(req)
const { path, genResp, logger } = ctx const { path, genResp, logger } = ctx
@ -43,6 +45,7 @@ const server = Bun.serve({
return genResp.healthCheck("hello, there is egg, glade to serve you!") return genResp.healthCheck("hello, there is egg, glade to serve you!")
} catch (error: any) { } catch (error: any) {
// 错误处理 // 错误处理
logger.error(error.message)
return genResp.serverError(error.message || "server error") return genResp.serverError(error.message || "server error")
} }
}, },
@ -54,4 +57,4 @@ const server = Bun.serve({
port: 3000, port: 3000,
}) })
logger.info(`Listening on ${server.hostname}:${server.port}`) logger.info(`Listening on ${bunServer.hostname}:${bunServer.port}`)

View File

@ -22,15 +22,15 @@
"@eslint/js": "^9.19.0", "@eslint/js": "^9.19.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.2.0", "bun-types": "^1.2.1",
"eslint": "^9.19.0", "eslint": "^9.19.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",
"lint-staged": "^15.4.2", "lint-staged": "^15.4.3",
"oxlint": "^0.13.2", "oxlint": "^0.13.2",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"typescript-eslint": "^8.21.0" "typescript-eslint": "^8.22.0"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5.5.4" "typescript": "^5.5.4"
@ -39,7 +39,7 @@
"@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.28.2", "@egg/net-tool": "^1.31.1",
"@egg/path-tool": "^1.4.1", "@egg/path-tool": "^1.4.1",
"@langchain/core": "^0.3.36", "@langchain/core": "^0.3.36",
"@langchain/langgraph": "^0.2.41", "@langchain/langgraph": "^0.2.41",

View File

@ -1,8 +1,8 @@
import groupAgent from "../../controller/groupAgent" import groupAgent from "../../controller/groupAgent"
import intentAgent from "../../controller/intentAgent"
import reportAgent from "../../controller/reportAgent"
import soupAgent from "../../controller/soupAgent" import soupAgent from "../../controller/soupAgent"
import { Context } from "../../types" import { Context } from "../../types"
import llm from "../../utils/llm"
import { cleanLLMRes } from "../../utils/llm/base"
import { isNotP2POrAtBot } from "../../utils/message" import { isNotP2POrAtBot } from "../../utils/message"
/** /**
@ -49,111 +49,88 @@ const filterIllegalMsg = async (ctx: Context): Promise<boolean> => {
return true return true
} }
/**
* ID消息
* @param {Context} ctx -
*/
const manageIdMsg = ({
larkBody: { chatId },
larkCard,
larkService,
}: Context) =>
larkService.message.sendCard2Chat(
chatId,
larkCard.genTempCard("chatId", { chat_id: chatId })
)
/** /**
* *
* @param {Context} ctx - * @param {Context} ctx -
*/ */
const manageIntent = async (ctx: Context) => { const manageIntent = async (ctx: Context) => {
const { const {
body,
logger, logger,
larkService: { message }, larkService: { message },
attachService, attachService,
larkBody: { msgText, chatId }, larkBody: { msgText, chatId },
larkCard, larkCard,
requestId,
} = ctx } = ctx
logger.info(`bot req text: ${msgText}`) logger.info(`bot req text: ${msgText}`)
await message.updateOrReply( await message.updateOrReply(
larkCard.genPendingCard("正在理解您的意图,请稍等...") larkCard.genPendingCard("正在理解您的意图,请稍等...")
) )
try { try {
const llmRes = (await llm.invoke( const intentRes = await intentAgent.agent(ctx)
"intentRecognition", logger.info(`intentRes: ${JSON.stringify(intentRes)}`)
{ if (intentAgent.isBriefing(intentRes)) {
userInput: msgText, reportAgent
time: new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }), .agent(ctx, intentRes.link, intentRes.userDescription)
}, .catch(() => null)
requestId
)) as string
const cleanedLlmRes = cleanLLMRes(llmRes)
logger.info(`intentRecognition llm res: ${cleanedLlmRes}`)
// 返回值不是数字,说明是通识回答
const intent = Number(cleanedLlmRes)
if (isNaN(intent)) {
await message.updateOrReply(larkCard.genSuccessCard(cleanedLlmRes))
return return
} }
if (intentAgent.isBriefingLink(intentRes)) {
switch (intent) { reportAgent.setSheet(ctx, intentRes.link).catch(() => null)
// 获取聊天ID return
case 1: }
await manageIdMsg(ctx) if (intentAgent.isCommonResponse(intentRes)) {
break await message.updateOrReply(larkCard.genSuccessCard(intentRes.message))
// CI监控 return
case 2: }
await attachService.ciMonitor(chatId) if (intentAgent.isBaseIntent(intentRes)) {
break switch (intentRes.intent) {
// 生成简报 case 3:
case 3: await message.updateOrReply(
await message.updateOrReply( larkCard.genTempCard("chatId", { chat_id: chatId }) as string
larkCard.genSuccessCard("正在为您收集简报,请稍等片刻~") )
) break
await attachService.reportCollector(body) case 4:
break await attachService.ciMonitor(chatId)
// 获取帮助 break
case 4: case 6:
await message.updateOrReply( await message.updateOrReply(
larkCard.genTempCard("eggGuide", { chat_id: chatId }) as string larkCard.genTempCard("eggGuide", { chat_id: chatId }) as string
) )
break break
// 开启日报订阅 case 7:
case 5: groupAgent.report
groupAgent.report.setSubscription(ctx, "daily", true) .setSubscription(ctx, "daily", true)
break .catch(() => null)
// 开启周报订阅 break
case 6: case 8:
groupAgent.report.setSubscription(ctx, "weekly", true) groupAgent.report
break .setSubscription(ctx, "weekly", true)
// 关闭日报订阅 .catch(() => null)
case 7: break
groupAgent.report.setSubscription(ctx, "daily", false) case 9:
break groupAgent.report
// 关闭周报订阅 .setSubscription(ctx, "daily", false)
case 8: .catch(() => null)
groupAgent.report.setSubscription(ctx, "weekly", false) break
break case 10:
// 立即发送日报 groupAgent.report
case 9: .setSubscription(ctx, "weekly", false)
groupAgent.report.gen4Test(ctx, "daily") .catch(() => null)
break break
// 立即发送周报 case 11:
case 10: groupAgent.report.gen4Test(ctx, "daily").catch(() => null)
groupAgent.report.gen4Test(ctx, "weekly") break
break case 12:
// 开始海龟汤游戏 groupAgent.report.gen4Test(ctx, "weekly").catch(() => null)
case 11: break
soupAgent.startOrStopGame(ctx, true, "manual") case 13:
break soupAgent.startOrStopGame(ctx, true, "manual").catch(() => null)
// 通识回答 break
case 12: case 1:
default: default:
groupAgent.agent(ctx) groupAgent.agent(ctx).catch(() => null)
break break
}
} }
} catch (error) { } catch (error) {
logger.error(`manageIntent error: ${error}`) logger.error(`manageIntent error: ${error}`)

View File

@ -1,6 +1,9 @@
import type { LarkEvent } from "@egg/lark-msg-tool" import type { LarkEvent } from "@egg/lark-msg-tool"
import { NetToolBase } from "@egg/net-tool" import { NetToolBase } from "@egg/net-tool"
import { APP_CONFIG } from "../../constant/config"
import { LarkServer } from "../../types"
interface Chat2SoupParams { interface Chat2SoupParams {
user_query: string user_query: string
soup_id: string soup_id: string
@ -84,6 +87,53 @@ class AttachService extends NetToolBase {
const URL = "https://lark-egg.ai.xiaomi.com/soup/chat" const URL = "https://lark-egg.ai.xiaomi.com/soup/chat"
return this.post<Chat2SoupResp>(URL, body).catch(() => null) return this.post<Chat2SoupResp>(URL, body).catch(() => null)
} }
/**
*
* @param {string} url - URL
* @returns {Promise<string>}
*/
async crawlWeb(url: string) {
const URL = "https://lark-egg.ai.xiaomi.com/tools/web/crawler"
return this.get<LarkServer.BaseRes<string>>(URL, { url }).catch(() => null)
}
/**
* 使mify爬虫抓取网页内容
* @param {string} link -
* @param {string} userDescription -
* @returns {Promise<any>}
*/
async mifyCrawler(link: string, userDescription: string) {
const URL = "https://mify-be.pt.xiaomi.com/api/v1/workflows/run"
return this.post(
URL,
{
inputs: {
link,
userDescription,
},
response_mode: "blocking",
user: "egg-server",
},
{},
{
Authorization: `Bearer ${APP_CONFIG.MIFY_CRAWLER_TOKEN}`,
}
)
.then((res) => {
const llmRes = res.data.outputs.content
if (!llmRes) throw new Error("模型总结失败")
if (llmRes === "crawlerErr") throw new Error("网页抓取失败")
return llmRes as string
})
.catch((error) => {
if (["网页抓取失败", "模型总结失败"].includes(error.message)) {
throw error
}
throw new Error("MIFY爬虫请求失败")
})
}
} }
export default AttachService export default AttachService

8
test/archive/wiki.ts Normal file
View File

@ -0,0 +1,8 @@
import initAppConfig from "../../constant/config"
import genLarkService from "../../utils/genLarkService"
await initAppConfig()
const service = genLarkService("egg", "test")
service.wiki.getNodeInfo("V4ZkwhDR8iRCqIk7X81k1rBj4Sc").then(console.log)

View File

@ -1,15 +1,59 @@
import { parseJsonString } from "@egg/hooks"
import { z } from "zod"
import { zodToJsonSchema } from "zod-to-json-schema"
import initAppConfig from "../../constant/config" import initAppConfig from "../../constant/config"
import llm from "../../utils/llm" import llm from "../../utils/llm"
import { cleanLLMRes } from "../../utils/llm/base"
await initAppConfig() await initAppConfig()
const BaseIntentSchema = z.object({
intent: z.number().min(1).max(13),
})
const BriefingSchema = BaseIntentSchema.extend({
intent: z.literal(3),
link: z.string().url(),
userDescription: z.string().min(1),
})
const CommonResponseSchema = BaseIntentSchema.extend({
intent: z.literal(12),
message: z.string().min(1),
})
const IntentSchema = z.union([
BriefingSchema,
CommonResponseSchema,
BaseIntentSchema,
])
const jsonSchema = zodToJsonSchema(IntentSchema)
const res = await llm.invoke( const res = await llm.invoke(
"intentRecognition", "intentRecognitionNext",
{ {
userInput: "你是干嘛的", userInput:
"https://mp.weixin.qq.com/s/-0J8XbXJU6Bu-UihRtgGAQ Airbnb死磕React Native惨败微软却玩出花Office、Outlook全线接入Copilot成最大赢家 推荐大家看一下rn助力微软copilot 跨平台实现",
time: new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }), time: new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }),
jsonSchema,
}, },
"test" "test",
0,
true
) )
console.log(res) const rawIntent = cleanLLMRes(res as string)
const parsedIntent = parseJsonString(rawIntent, null)
console.log("🚀 ~ parsedIntent:", parsedIntent)
try {
IntentSchema.parse(parsedIntent)
console.log("Intent is valid:", parsedIntent)
} catch (e: any) {
console.error("Invalid intent:", e.errors)
}
console.log(cleanLLMRes(res as string))

11
test/llm/mifyCrawler.ts Normal file
View File

@ -0,0 +1,11 @@
import initAppConfig from "../../constant/config"
import { AttachService } from "../../services"
await initAppConfig()
const server = new AttachService()
server
.mifyCrawler("https://lacus.site", "详细介绍alpine")
.then(console.log)
.catch(console.error)

18
test/llm/summaryWeb.ts Normal file

File diff suppressed because one or more lines are too long

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://juejin.cn/post/7463301526800826404 openai agent实现大概是云端起个chrome用playwright搞得感觉vm技术是ai重要基建之一\"}","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

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

@ -1,3 +1,5 @@
import loggerIns from "@egg/logger"
import { JsonOutputParser } from "@langchain/core/output_parsers"
import { PromptTemplate } from "@langchain/core/prompts" import { PromptTemplate } from "@langchain/core/prompts"
import { adjustTimeRange, getSpecificTime, getTimeRange } from "../time" import { adjustTimeRange, getSpecificTime, getTimeRange } from "../time"
@ -15,24 +17,51 @@ const invoke = async (
promptName: string, promptName: string,
variables: Record<string, any>, variables: Record<string, any>,
requestId: string, requestId: string,
temperature = 0 temperature = 0,
jsonMode = false
) => { ) => {
const { langfuse, langfuseHandler } = await getLangfuse("invoke", requestId) const logger = loggerIns.child({ requestId })
const prompt = await langfuse.getPrompt(promptName) const attemptInvoke = async () => {
const { langfuse, langfuseHandler } = await getLangfuse("invoke", requestId)
const prompt = await langfuse.getPrompt(promptName)
const langchainTextPrompt = PromptTemplate.fromTemplate( const langchainTextPrompt = PromptTemplate.fromTemplate(
prompt.getLangchainPrompt() prompt.getLangchainPrompt()
).withConfig({ ).withConfig({
metadata: { langfusePrompt: prompt }, metadata: { langfusePrompt: prompt },
}) })
const chain = langchainTextPrompt.pipe(await getModel(temperature)) const chain = langchainTextPrompt.pipe(await getModel(temperature))
const { content } = await chain.invoke(variables, { if (jsonMode) {
callbacks: [langfuseHandler], chain.pipe(new JsonOutputParser())
}) }
return content const { content } = await chain.invoke(variables, {
callbacks: [langfuseHandler],
})
return content
}
let result
let attempts = 0
do {
try {
result = await attemptInvoke()
break
} catch (e) {
logger.error(`🚀 ~ invoke ~ attemptInvoke ~ e: ${e}`)
attempts++
}
} while (attempts < 3)
if (!result) {
logger.error("Failed to invoke after 3 attempts")
return ""
}
return result
} }
/** /**
@ -42,6 +71,7 @@ const invoke = async (
* @returns * @returns
*/ */
const timeParser = async (userInput: string, requestId: string) => { const timeParser = async (userInput: string, requestId: string) => {
const logger = loggerIns.child({ requestId })
const time = new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }) const time = new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })
const weekDay = `星期${"日一二三四五六"[new Date().getDay()]}` const weekDay = `星期${"日一二三四五六"[new Date().getDay()]}`
const invokeParser = async () => { const invokeParser = async () => {
@ -63,7 +93,7 @@ const timeParser = async (userInput: string, requestId: string) => {
)) as string )) as string
return JSON.parse(res.replaceAll("`", "")) return JSON.parse(res.replaceAll("`", ""))
} catch (e) { } catch (e) {
console.error("🚀 ~ timeParser ~ invokeParser ~ e", e) logger.error(`🚀 ~ timeParser ~ invokeParser ~ e: ${e}`)
// 如果解析失败,则返回空字符串 // 如果解析失败,则返回空字符串
return { s: "", e: "" } return { s: "", e: "" }
} }

33
utils/string.ts Normal file
View File

@ -0,0 +1,33 @@
/**
* sheetToken range
*
* @param {string} url - URL
* @returns {{sheetToken: string, range: string | null} | null} - sheetToken range null
*/
export const extractSheetIds = (url: string) => {
// 定义匹配 wiki 和 sheets 两种URL格式的正则表达式
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], // sheetToken 在组1
range: match[2] || null, // range 在组2如果没有则返回 null
}
}
return null // 如果没有匹配,则返回 null
}
/**
*
* @param {string} link -
* @throws {Error} -
*/
export const validateLink = (link: string): void => {
if (!link) throw new Error("链接不能为空")
try {
new URL(link)
} catch {
throw new Error("链接格式不正确")
}
}