diff --git a/bun.lockb b/bun.lockb index d3b8a97..4949ab6 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/constant/function.ts b/constant/function.ts index 4445ba1..5d852a7 100644 --- a/constant/function.ts +++ b/constant/function.ts @@ -1,9 +1,14 @@ const functionMap = { egg: { xName: "小煎蛋", - xAuthor: "zhaoyingbo", + xAuthor: "Yingbo", xIcon: "🍳", }, + webAgent: { + xName: "小煎蛋 简报助手", + xAuthor: "Yingbo", + xIcon: "📎", + }, groupAgent: { xName: "小煎蛋 Group Agent", xAuthor: "YIBinary ❤️ Yingbo", @@ -11,12 +16,12 @@ const functionMap = { }, sheetDB: { xName: "小煎蛋 Sheet DB", - xAuthor: "zhaoyingbo", + xAuthor: "Yingbo", xIcon: "🍪", }, gitlabAgent: { xName: "小煎蛋 Gitlab Agent", - xAuthor: "zhaoyingbo", + xAuthor: "Yingbo", xIcon: "🐙", }, soupAgent: { diff --git a/controller/intentAgent/index.ts b/controller/intentAgent/index.ts new file mode 100644 index 0000000..df8cb99 --- /dev/null +++ b/controller/intentAgent/index.ts @@ -0,0 +1,122 @@ +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(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, +]) + +type BaseIntent = z.infer +type Briefing = z.infer +type CommonResponse = z.infer +export type Intent = z.infer + +/** + * JSON模式 + */ +const jsonSchema = zodToJsonSchema(IntentSchema) + +/** + * 代理函数 + * @param {Context} ctx - 上下文对象 + * @returns {Promise} - 返回意图对象 + */ +const agent = async (ctx: Context): Promise => { + const { + larkBody: { msgText }, + logger, + } = 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, + }, + "test", + 0, + 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: 13, + } +} + +const isBaseIntent = (intent: Intent): intent is BaseIntent => { + return intent.intent !== 3 && intent.intent !== 12 +} + +const isBriefing = (intent: Intent): intent is Briefing => { + return intent.intent === 3 +} + +const isCommonResponse = (intent: Intent): intent is CommonResponse => { + return intent.intent === 12 +} + +/** + * 意图代理对象 + */ +const intentAgent = { + agent, + isBaseIntent, + isBriefing, + isCommonResponse, +} + +export default intentAgent diff --git a/controller/reportAgent/index.ts b/controller/reportAgent/index.ts new file mode 100644 index 0000000..d6943f6 --- /dev/null +++ b/controller/reportAgent/index.ts @@ -0,0 +1,198 @@ +import db from "../../db" +import { Context } from "../../types" +import llm from "../../utils/llm" +import { extractSheetIds, validateLink } from "../../utils/string" + +/** + * 爬取网页内容 + * @param {Context} ctx - 上下文对象 + * @param {string} link - 网页链接 + * @returns {Promise} - 返回爬取结果 + * @throws {Error} - 当爬取失败时抛出错误 + */ +const crawlWebPage = async (ctx: Context, link: string): Promise => { + 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} - 返回简报内容 + * @throws {Error} - 当生成简报失败时抛出错误 + */ +const generateSummary = async ( + ctx: Context, + userDescription: string, + content: any +): Promise => { + 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} - 返回表格链接 + */ +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?.webSummarySheetLink) { + logger.info("No webSummarySheetLink 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 + } + + await larkService.sheet.insertRows(sheetToken, range, [ + [ + userId || "", + link, + userDescription, + llmRes, + new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }), + ], + ]) + return chat.webSummarySheetLink + } 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, + } = 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) + // 插入到表格 + 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}`) + ) + } +} + +const reportAgent = { + agent, +} + +export default reportAgent diff --git a/controller/soupAgent/index.ts b/controller/soupAgent/index.ts index bc4e369..f471c96 100644 --- a/controller/soupAgent/index.ts +++ b/controller/soupAgent/index.ts @@ -131,10 +131,8 @@ const chat2Soup = async (ctx: Context) => { larkBody: { msgText, chatId, messageId }, logger, attachService, - larkCard, larkService: { message }, } = ctx - const cardGender = larkCard.child("soupAgent") message.setReplyMessage(messageId, "text") const activeGame = await db.soupGame.getActiveOneByChatId(chatId) if (!activeGame) { @@ -150,9 +148,7 @@ const chat2Soup = async (ctx: Context) => { }) if (!res) { logger.error(`chatId: ${chatId} failed to get soup result`) - await message.updateOrReply( - cardGender.genErrorCard(SoupGameMessage.chatFailed) - ) + await message.updateOrReply(SoupGameMessage.chatFailed) return } // 用户答对了 diff --git a/db/chat/index.ts b/db/chat/index.ts index 9923988..638b0b1 100644 --- a/db/chat/index.ts +++ b/db/chat/index.ts @@ -13,6 +13,7 @@ export interface Chat { mode: "group" | "p2p" | "topic" weeklySummary: boolean dailySummary: boolean + webSummarySheetLink: string } export type ChatModel = Chat & RecordModel @@ -60,6 +61,7 @@ const getAndCreate = async ({ larkService, logger, larkBody }: Context) => { mode: chat_mode, weeklySummary: false, dailySummary: false, + webSummarySheetLink: "", } return create(newChat) } diff --git a/index.ts b/index.ts index 7192885..4a036d2 100644 --- a/index.ts +++ b/index.ts @@ -12,8 +12,10 @@ initSchedule() await initAppConfig() -const server = Bun.serve({ - async fetch(req) { +const bunServer = Bun.serve({ + fetch: async (req, server) => { + // 设置超时时间 + server.timeout(req, 30) // 生成上下文 const ctx = await genContext(req) const { path, genResp, logger } = ctx @@ -43,6 +45,7 @@ const server = Bun.serve({ return genResp.healthCheck("hello, there is egg, glade to serve you!") } catch (error: any) { // 错误处理 + logger.error(error.message) return genResp.serverError(error.message || "server error") } }, @@ -54,4 +57,4 @@ const server = Bun.serve({ port: 3000, }) -logger.info(`Listening on ${server.hostname}:${server.port}`) +logger.info(`Listening on ${bunServer.hostname}:${bunServer.port}`) diff --git a/package.json b/package.json index f5c21b6..16d4d46 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^4.1.4", "husky": "^9.1.7", - "lint-staged": "^15.4.2", + "lint-staged": "^15.4.3", "oxlint": "^0.13.2", "prettier": "^3.4.2", "typescript-eslint": "^8.21.0" @@ -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.28.2", + "@egg/net-tool": "^1.30.2", "@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 c317ba3..0e1d3bd 100644 --- a/routes/bot/eventMsg.ts +++ b/routes/bot/eventMsg.ts @@ -1,8 +1,8 @@ import groupAgent from "../../controller/groupAgent" +import intentAgent from "../../controller/intentAgent" +import reportAgent from "../../controller/reportAgent" import soupAgent from "../../controller/soupAgent" import { Context } from "../../types" -import llm from "../../utils/llm" -import { cleanLLMRes } from "../../utils/llm/base" import { isNotP2POrAtBot } from "../../utils/message" /** @@ -69,91 +69,66 @@ const manageIdMsg = ({ */ const manageIntent = async (ctx: Context) => { const { - body, logger, larkService: { message }, attachService, larkBody: { msgText, chatId }, larkCard, - requestId, } = ctx logger.info(`bot req text: ${msgText}`) await message.updateOrReply( larkCard.genPendingCard("正在理解您的意图,请稍等...") ) try { - const llmRes = (await llm.invoke( - "intentRecognition", - { - userInput: msgText, - time: new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }), - }, - 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)) + const intentRes = await intentAgent.agent(ctx) + logger.info(`intentRes: ${JSON.stringify(intentRes)}`) + if (intentAgent.isBriefing(intentRes)) { + reportAgent.agent(ctx, intentRes.link, intentRes.userDescription) return } - - switch (intent) { - // 获取聊天ID - case 1: - await manageIdMsg(ctx) - break - // CI监控 - case 2: - await attachService.ciMonitor(chatId) - break - // 生成简报 - case 3: - await message.updateOrReply( - larkCard.genSuccessCard("正在为您收集简报,请稍等片刻~") - ) - await attachService.reportCollector(body) - break - // 获取帮助 - case 4: - await message.updateOrReply( - larkCard.genTempCard("eggGuide", { chat_id: chatId }) as string - ) - break - // 开启日报订阅 - case 5: - groupAgent.report.setSubscription(ctx, "daily", true) - break - // 开启周报订阅 - case 6: - groupAgent.report.setSubscription(ctx, "weekly", true) - break - // 关闭日报订阅 - case 7: - groupAgent.report.setSubscription(ctx, "daily", false) - break - // 关闭周报订阅 - case 8: - groupAgent.report.setSubscription(ctx, "weekly", false) - break - // 立即发送日报 - case 9: - groupAgent.report.gen4Test(ctx, "daily") - break - // 立即发送周报 - case 10: - groupAgent.report.gen4Test(ctx, "weekly") - break - // 开始海龟汤游戏 - case 11: - soupAgent.startOrStopGame(ctx, true, "manual") - break - // 通识回答 - case 12: - default: - groupAgent.agent(ctx) - break + if (intentAgent.isCommonResponse(intentRes)) { + await message.updateOrReply(larkCard.genSuccessCard(intentRes.message)) + return + } + if (intentAgent.isBaseIntent(intentRes)) { + switch (intentRes.intent) { + case 1: + await manageIdMsg(ctx) + break + case 2: + await attachService.ciMonitor(chatId) + break + case 4: + await message.updateOrReply( + larkCard.genTempCard("eggGuide", { chat_id: chatId }) as string + ) + break + case 5: + groupAgent.report.setSubscription(ctx, "daily", true) + break + case 6: + groupAgent.report.setSubscription(ctx, "weekly", true) + break + case 7: + groupAgent.report.setSubscription(ctx, "daily", false) + break + case 8: + groupAgent.report.setSubscription(ctx, "weekly", false) + break + case 9: + groupAgent.report.gen4Test(ctx, "daily") + break + case 10: + groupAgent.report.gen4Test(ctx, "weekly") + break + case 11: + soupAgent.startOrStopGame(ctx, true, "manual") + break + case 13: + default: + groupAgent.agent(ctx) + break + } } } catch (error) { logger.error(`manageIntent error: ${error}`) diff --git a/services/attach/index.ts b/services/attach/index.ts index 23b5922..e58423f 100644 --- a/services/attach/index.ts +++ b/services/attach/index.ts @@ -1,6 +1,8 @@ import type { LarkEvent } from "@egg/lark-msg-tool" import { NetToolBase } from "@egg/net-tool" +import { LarkServer } from "../../types" + interface Chat2SoupParams { user_query: string soup_id: string @@ -84,6 +86,16 @@ class AttachService extends NetToolBase { const URL = "https://lark-egg.ai.xiaomi.com/soup/chat" return this.post(URL, body).catch(() => null) } + + /** + * 抓取网页内容 + * @param {string} url - 网页URL。 + * @returns {Promise} 返回抓取的网页内容。 + */ + async crawlWeb(url: string) { + const URL = "https://lark-egg.ai.xiaomi.com/tools/web/crawl" + return this.get>(URL, { url }).catch(() => null) + } } export default AttachService diff --git a/test/archive/wiki.ts b/test/archive/wiki.ts new file mode 100644 index 0000000..f18a245 --- /dev/null +++ b/test/archive/wiki.ts @@ -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) diff --git a/test/llm/intentRecognition.ts b/test/llm/intentRecognition.ts index 711b005..1a72294 100644 --- a/test/llm/intentRecognition.ts +++ b/test/llm/intentRecognition.ts @@ -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 llm from "../../utils/llm" +import { cleanLLMRes } from "../../utils/llm/base" 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( - "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" }), + 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)) diff --git a/test/llm/summaryWeb.ts b/test/llm/summaryWeb.ts new file mode 100644 index 0000000..d1e3dd5 --- /dev/null +++ b/test/llm/summaryWeb.ts @@ -0,0 +1,18 @@ +import initAppConfig from "../../constant/config" +import llm from "../../utils/llm" +import { cleanLLMRes } from "../../utils/llm/base" + +await initAppConfig() + +const res = await llm.invoke( + "summaryWeb", + { + description: "这是个不错的博客!", + content: + "RainSun 时光日记 主页 归档 分类 标签 关于 | Alpine 系统命令记录 VSCode远程链接安装必要的依赖\napk add gcompat libstdc++ curl bash git bash bash-doc bash-completion\n安装sshd\napk add openssh nano\n修改配置\nnano /etc/ssh/sshd_config\n添加如下内容\nPermitRootLogin yes\nAllowTcpForwarding yes\nPermitTunnel yes\n添加私钥并将公钥写入 2023-04-12 系统 linux system linux Alpine Gitlab相关 Gitlab 安装升级操作指令集https://blog.csdn.net/qq_43626147/article/details/109160229\nhttps://cloud.tencent.com/developer/article/1622317\n安装Gitlab安装必要依赖yum -y install curl policycoreutils policycoreutils-python openssh-server openssh-clients postfix\npostfix是发送邮件用的,需要配置一下,编辑文件/etc/postfix/main.cfinet_interfaces=all\ninet_protocols=all修改完执行\nsystemctl start postfix\nsystemctl enable postfix\n配置yum源curl -sS https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.rpm.sh | sudo bash\n然后去清华官网下载对应版本的 2022-05-30 系统 gitlab gitlab 工具 Mega 用户认证加密系统 Mega 用户认证加密系统文件下载https://assets.lacus.site/Document/SecurityWhitepaper.pdf\nTODO\nMethod to Protect Passwords in Databases for Web Applications 注册过程密码评分密码使用 ZXCVBN password strength estimator library (v4.4.2), 进行密码评分,评分结果在0-4之间\n密码至少需要有八位,因为即使过短的密码也可能获取到1的评分\n针对评分需要给用户合适的提醒\nLength < 8 Too short\nScore 0 and Length >= 8 Too weak\nScore 1 and Length >= 8 Weak\nScore 2 and Length >= 8 Medium\nScore 3 and Length >= 8 Good\nScore 4 and Length >= 8 Strong密码处理Password Processing Function (PPF 2022-03-09 系统 Mega用户认证加密系统 Mega 登录 加密 小爱课程表开发者插件使用教程 小爱课程表开发者插件使用教程首先非常感谢您能够参与本次的新版开发者平台的内测。\n该教程已经迁移至\nhttps://open-schedule-prod.ai.xiaomi.com/docs/#/help/ 2021-12-14 工具 开发者插件 小爱课程表 小工具笔记 从requirements.txt更新python包安装pip install pip-upgrader\n用法激活您的virtualenv(这很重要,因为它还将在当前virtualenv中安装新版本的升级软件包)。\ncd进入您的项目目录,然后运行:\npip-upgrade\n高级用法如果需求放置在非标准位置,请将其作为参数发送:\npip-upgrade path/to/requirements.txt如果您已经知道要升级的软件包,只需将它们作为参数发送:\npip-upgrade -p django -p celery -p dateutil如果您需要升级到发行前/发行后版本,请添加 –prerelease请在命令中参数。 –skip-virtualenv-check (install the packages anyway)–skip-package-installation (don’t install any package. just update the requirements file(s)) 2021-09-26 工具 工具 frp内网穿透简单配置 frp 内网穿透家里宽带80和443端口被封掉了\n手里恰好有一台只部署了mongodb的阿里云vps,装一下frp顶一下\nTips服务端是公网VPS,客户端是需要穿透出去的机器\n这个流程针对两边都是linux系统\nfrps 服务端执行下方shell开启脚本(阿里云镜像)\nwget https://code.aliyun.com/MvsCode/frps-onekey/raw/master/install-frps.sh -O ./install-frps.sh\nchmod 700 ./install-frps.sh\n./install-frps.sh install\nshell来源:https://github.com/MvsCode/frps-onekey 跟着引导一步一步走就行,都用默认就好,回车走到底 里边会有http和https的端口设置,如果这两个端口没被占用就直接回车,要是被占用了就得换一个查看当前端口占用情况:netstat -ntlp 安装完了会看到这个\n==============================================\nYou Serv 2021-03-26 系统 frp frp 你不知道的JavaScript(中)学习笔记 类型和语法类型内置类型JS有七种内置类型空值(null)、未定义(undefined)、布尔值(boolean)、数字(Number)、字符串(String)、对象(Object)、符号(Symbol)\ntypeof检测null会得到”object”, 使用(!a && typeof a === 'object')即可得到正确的结果\ntypeof检测函数会得到”function” ,但是函数是object的子类型,具体来说函数是“可调用对象”。\ntypeof检测数组会得到”object”,数组也是object的子类型。\n值和类型JS的变量是没有类型的,只有值才有,变量可以随时持有任何类型的值\n对于未定义变量的检测typeof检测对于未定义变量也返回undefined,这个机制可以帮助我们检测一个全局变量是否存在\nif (a) { ... // ReferenceError: a is not defined\n} if (typeof a !== undefined) { ... // OK\n}\n值数组创建的”稀疏数组”中的空白单元会表现 2021-03-03 语言 js js 基础 rocket-chat部署流程 rocket-chat部署流程前言巧合之下看到了一位UP的视频讲到了这个,和之前我一直很想写的聊天室很相似,功能也十分完善,所以打算本地部署一下试试,表示真香 本流程仅学习使用,正式对外服务需要进行备案,请谨慎 使用docker部署rocket-chat依赖mongodb,并且使用 MongoDB replica set 来提高效率\n新创建的mongodb容器打算只为rocket-chat服务,需要建立一个新的网桥,因为使用Docker DNS Server进行通信,所以也没必要指定网桥的网段\ndocker network create --driver bridge rocketchat经过测试,太高版本的mongodb没办法执行官方给的MongoDB replica set设置代码,最终将mongodb的版本确定为3.4\n启动mongodb容器(注意更改路径)\ndocker run \\ --name mongo \\ -v .../path/to/data/db:/data/db \\ -v .../path/to/data/dump:/data/dump \\ < 2021-01-18 系统 rocket-chat rocket-chat mongoDB学习分享 MongoDBMongoDB简介2007年10月,MongoDB由10gen团队所发展。2009年2月首度推出\nMongoDB是由C++语言编写的,是一个基于分布式文件存储的开源和文档数据库系统,属于NoSQL。其数据存储形式为文档,数据结构由Key-Value键值组成\nMongoDB数据库按粒度从小到大由文档(document)、集合(collection)和数据库(database)三部分组成\n主要特点\n非关系型数据库,面向文档存储,基于文档数据模型(document data model)\nBSON(Binary JSON)格式存储数据,类似于JSON\n无架构(schema-less),可灵活扩展,存储和查询方便\n支持索引和次级索引(secondary indexes): 次级索引是指文档或row有一个 主键(primary key)作为索引,同时允许文档或row内部还拥有一个索引(通过B-tree实现),提升查询的效率\n可扩展性,采用低成本的横向扩展模式\n高可用\n高性能 缺点:聚合查询比较麻烦、不支持JOIN多表关联查询、不支持事物、没有严格的范式(主外键)约束、数据占用空间 2020-12-11 数据库 mongoDB 数据库 mongoDB 前端面试题总结 百度提前批面试一面如何清除浮动链接\n给浮动元素的父元素添加高度\nclear:both;\n伪元素清除浮动\n给父元素使用overflow:hidden;\nbr标签清浮动\npx、em、rem说一下?em:相对于父级的字体大小\nrem: 相对于根节点的字体大小\n浏览器默认字体大小16px\n如何实现一个元素水平、垂直居中position 然后改margin\nposition 然后transform:translate(-50%,-50%);\nposition 上下左右定位0,然后margin:auto;\nflex\n单行文字:height撑满text-align:center;\nCSS选择第一个元素、最后一个元素、3的整数倍子元素伪类选择器链接 n\n2n+1\n4n+1\n4n+4\n4n\n5n-2\n-n+3 0\n1\n1\n4\n-\n-\n3 1\n3\n5\n8\n4\n3\n2 2\n5\n9\n12\n8\n8\n1 3\n7\n13\n16\n12\n13\n- 4\n9\n17\n20\n16\n18\n- 5\n11\n21\n24\n20\n23\n- 说一下伪类和伪元素的区别链接\n官方文档\n“伪元素”和“伪类”都带一 2020-10-07 面试 题库 面试 js 基础 123…5 搜索 关键词 Hexo Fluid 总访问量 次 总访客数 人 吉ICP备18005655号 | 吉公网安备 22010202000634号", + }, + "test", + 1 +) + +console.log(cleanLLMRes(res as string)) diff --git a/test/reportAgent/agent.http b/test/reportAgent/agent.http new file mode 100644 index 0000000..853d520 --- /dev/null +++ b/test/reportAgent/agent.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://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"}}} \ No newline at end of file diff --git a/utils/llm/index.ts b/utils/llm/index.ts index c61e039..7de1405 100644 --- a/utils/llm/index.ts +++ b/utils/llm/index.ts @@ -1,3 +1,5 @@ +import loggerIns from "@egg/logger" +import { JsonOutputParser } from "@langchain/core/output_parsers" import { PromptTemplate } from "@langchain/core/prompts" import { adjustTimeRange, getSpecificTime, getTimeRange } from "../time" @@ -15,24 +17,51 @@ const invoke = async ( promptName: string, variables: Record, requestId: string, - temperature = 0 + temperature = 0, + jsonMode = false ) => { - const { langfuse, langfuseHandler } = await getLangfuse("invoke", requestId) - const prompt = await langfuse.getPrompt(promptName) + const logger = loggerIns.child({ requestId }) + const attemptInvoke = async () => { + const { langfuse, langfuseHandler } = await getLangfuse("invoke", requestId) + const prompt = await langfuse.getPrompt(promptName) - const langchainTextPrompt = PromptTemplate.fromTemplate( - prompt.getLangchainPrompt() - ).withConfig({ - metadata: { langfusePrompt: prompt }, - }) + const langchainTextPrompt = PromptTemplate.fromTemplate( + prompt.getLangchainPrompt() + ).withConfig({ + metadata: { langfusePrompt: prompt }, + }) - const chain = langchainTextPrompt.pipe(await getModel(temperature)) + const chain = langchainTextPrompt.pipe(await getModel(temperature)) - const { content } = await chain.invoke(variables, { - callbacks: [langfuseHandler], - }) + if (jsonMode) { + 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 */ const timeParser = async (userInput: string, requestId: string) => { + const logger = loggerIns.child({ requestId }) const time = new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }) const weekDay = `星期${"日一二三四五六"[new Date().getDay()]}` const invokeParser = async () => { @@ -63,7 +93,7 @@ const timeParser = async (userInput: string, requestId: string) => { )) as string return JSON.parse(res.replaceAll("`", "")) } catch (e) { - console.error("🚀 ~ timeParser ~ invokeParser ~ e", e) + logger.error(`🚀 ~ timeParser ~ invokeParser ~ e: ${e}`) // 如果解析失败,则返回空字符串 return { s: "", e: "" } } diff --git a/utils/string.ts b/utils/string.ts new file mode 100644 index 0000000..db6c8a3 --- /dev/null +++ b/utils/string.ts @@ -0,0 +1,33 @@ +/** + * 从小米链接中提取 sheetToken 和 range。 + * + * @param {string} url - 要提取数据的URL。 + * @returns {{sheetToken: string, range: string} | 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 match = url.match(pattern) + if (match) { + return { + sheetToken: match[1] || match[3], // 对于第一种URL格式,sheetToken 在组1;对于第二种格式,在组3 + range: match[2] || match[4], // range 在第一种URL格式中是组2,在第二种格式是组4 + } + } + 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("链接格式不正确") + } +}