From 6b34c6538ac3f502854bcade9825a950f0f060e5 Mon Sep 17 00:00:00 2001 From: zhaoyingbo Date: Mon, 13 Jan 2025 08:39:29 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E6=B5=B7?= =?UTF-8?q?=E9=BE=9F=E6=B1=A4Server=E8=AF=B7=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/soupAgent/index.ts | 13 +++++++++++++ routes/bot/eventMsg.ts | 19 +++++++++++++------ services/attach/index.ts | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 controller/soupAgent/index.ts diff --git a/controller/soupAgent/index.ts b/controller/soupAgent/index.ts new file mode 100644 index 0000000..6ba73b9 --- /dev/null +++ b/controller/soupAgent/index.ts @@ -0,0 +1,13 @@ +import db from "../../db" +import { Context } from "../../types" +/** + * 开启或者停止游戏 + * @param ctx + * @param value + */ +const startOrStopGame = async (ctx: Context, value: boolean) => { + const chat = await db.chat.getAndCreate(ctx) + if (!chat) { + throw new Error("chat not found") + } +} diff --git a/routes/bot/eventMsg.ts b/routes/bot/eventMsg.ts index 3990fa9..6008e2c 100644 --- a/routes/bot/eventMsg.ts +++ b/routes/bot/eventMsg.ts @@ -4,6 +4,16 @@ import groupAgent from "../../controller/groupAgent" import createKVTemp from "../../controller/sheet/createKVTemp" import { Context } from "../../types" +/** + * 判断是否为非群聊和非艾特机器人的消息 + * @param {Context} ctx - 上下文数据,包含body, logger和larkService + * @returns {boolean} 是否为非法消息 + */ +const isNotP2POrAtBot = (ctx: Context) => { + const { larkBody, appInfo } = ctx + return !larkBody.isP2P && !larkBody.isAtBot(appInfo.appName) +} + /** * 过滤出非法消息,如果发表情包就直接发回去 * @param {Context} ctx - 上下文数据,包含body, logger和larkService @@ -14,18 +24,12 @@ const filterIllegalMsg = async ({ logger, larkService, larkBody, - appInfo, }: Context): Promise => { const { chatId, msgType, msgText } = larkBody // 没有chatId的消息不处理 logger.info(`bot req chatId: ${chatId}`) if (!chatId) return true - // 非私聊和群聊中艾特机器人的消息不处理 - if (!larkBody.isP2P && !larkBody.isAtBot(appInfo.appName)) { - return true - } - // 获取msgType logger.info(`bot req msgType: ${msgType}`) // 放行纯文本消息 @@ -239,6 +243,9 @@ const manageCMDMsg = async (ctx: Context) => { export const manageEventMsg = async (ctx: Context) => { // 过滤非法消息 if (await filterIllegalMsg(ctx)) return + // TODO: 海龟汤 + // 非群聊和非艾特机器人的消息不处理 + if (isNotP2POrAtBot(ctx)) return // 处理命令消息 await manageCMDMsg(ctx) } diff --git a/services/attach/index.ts b/services/attach/index.ts index 055b202..d0e5239 100644 --- a/services/attach/index.ts +++ b/services/attach/index.ts @@ -1,6 +1,23 @@ import type { LarkEvent } from "@egg/lark-msg-tool" import { NetToolBase } from "@egg/net-tool" +interface Chat2SoupParams { + user_query: string + soup_id: string + history: string +} + +interface Chat2SoupResp { + type: "GAME" | "END" | "OTHER" + content: string +} + +interface Soup { + title: string + query: string + answer: string +} + class AttachService extends NetToolBase { protected hostMap: Record = { dev: "https://lark-egg-preview.ai.xiaomi.com", @@ -41,6 +58,23 @@ class AttachService extends NetToolBase { await this.post(URL, body).catch(() => "") } } + + /** + * 开始海龟汤 + */ + async startSoup() { + const URL = "http://10.224.124.13:8778/soup" + return this.post(URL, {}).catch(() => null) + } + + /** + * 和海龟汤聊天 + * @param {Chat2SoupParams} body - 聊天参数 + */ + async chat2Soup(body: Chat2SoupParams) { + const URL = "http://10.224.124.13:8778/host" + return this.post(URL, body).catch(() => null) + } } export default AttachService From 0a427c17cc076812575767f72978e783927017b5 Mon Sep 17 00:00:00 2001 From: zhaoyingbo Date: Tue, 14 Jan 2025 09:19:00 +0000 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E6=B5=B7?= =?UTF-8?q?=E9=BE=9F=E6=B1=A4Agent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 2 + bun.lockb | Bin 167140 -> 166150 bytes constant/card.ts | 18 +++ constant/function.ts | 5 + constant/message.ts | 7 ++ controller/soupAgent/index.ts | 201 +++++++++++++++++++++++++++++++++- db/index.ts | 2 + db/soupGame/index.ts | 69 ++++++++++++ package.json | 6 +- routes/bot/eventMsg.ts | 15 +-- services/attach/index.ts | 9 ++ test/soupAgent/chat.http | 4 + test/soupAgent/startGame.http | 4 + test/soupAgent/stopGame.http | 4 + utils/message.ts | 20 ++++ 15 files changed, 348 insertions(+), 18 deletions(-) create mode 100644 db/soupGame/index.ts create mode 100644 test/soupAgent/chat.http create mode 100644 test/soupAgent/startGame.http create mode 100644 test/soupAgent/stopGame.http create mode 100644 utils/message.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 6a18bf9..0bb386f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -33,10 +33,12 @@ "qwen", "tseslint", "userid", + "wangyifei", "wlpbbgiky", "Xauthor", "Xicon", "Xname", + "Yingbo", "Yoav", "zhaoyingbo" ], diff --git a/bun.lockb b/bun.lockb index b5538e6cfda542a8444ceebe5a177f0a57ab2e36..4704e0b98847431ee8fe7bdc14d2b768d1820bdd 100755 GIT binary patch delta 25642 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 V@|2xaUk&Iiug>d{C~vOwzW^yxw)+48 diff --git a/constant/card.ts b/constant/card.ts index b96d4b9..44e6118 100644 --- a/constant/card.ts +++ b/constant/card.ts @@ -47,9 +47,27 @@ const autoReport = { }, } +const markdownSuccessCard = { + config: { + update_multi: true, + }, + elements: [ + { + tag: "markdown", + content: "${content}", + }, + { + tag: "hr", + }, + cardComponent.commonNote, + ], + header: cardComponent.successHeader, +} + const cardMap = { resultReport, autoReport, + markdownSuccessCard, } export default cardMap diff --git a/constant/function.ts b/constant/function.ts index 8e0fc2e..4445ba1 100644 --- a/constant/function.ts +++ b/constant/function.ts @@ -19,6 +19,11 @@ const functionMap = { xAuthor: "zhaoyingbo", xIcon: "🐙", }, + soupAgent: { + xName: "海龟汤 Agent", + xAuthor: "wangyifei15 🕹️ Yingbo", + xIcon: "🕹️", + }, } export default functionMap diff --git a/constant/message.ts b/constant/message.ts index 9498ca1..8f4ed25 100644 --- a/constant/message.ts +++ b/constant/message.ts @@ -9,3 +9,10 @@ export enum RespMessage { cancelFailed = "取消订阅失败", summaryFailed = "总结失败", } + +export enum SoupGameMessage { + startFailed = "游戏启动失败", + hasStarted = "游戏已经在进行中啦!", + hasStopped = "游戏已经结束啦!", + chatFailed = "模型调用失败,请再试一次吧!", +} diff --git a/controller/soupAgent/index.ts b/controller/soupAgent/index.ts index 6ba73b9..8100683 100644 --- a/controller/soupAgent/index.ts +++ b/controller/soupAgent/index.ts @@ -1,13 +1,206 @@ +import { SoupGameMessage } from "../../constant/message" import db from "../../db" +import { SoupGame } from "../../db/soupGame" import { Context } from "../../types" +import { isP2POrAtBot } from "../../utils/message" + /** * 开启或者停止游戏 * @param ctx * @param value */ -const startOrStopGame = async (ctx: Context, value: boolean) => { - const chat = await db.chat.getAndCreate(ctx) - if (!chat) { - throw new Error("chat not found") +const startOrStopGame = async ( + ctx: Context, + value: boolean, + which: "auto" | "manual" = "manual" +) => { + const { + logger, + larkBody: { chatId, messageId }, + attachService, + larkCard, + larkService, + } = ctx + const cardGender = larkCard.child("soupAgent") + if (!chatId) { + logger.error("chatId is required") + return } + // 获取正在进行中的游戏 + const activeGame = await db.soupGame.getActiveOneByChatId(chatId) + if (!activeGame) { + logger.info(`chatId: ${chatId} has no active game`) + } + // 停止游戏 + if (!value) { + // 没有进行中的游戏 + if (!activeGame) { + await larkService.message.replyCard( + messageId, + cardGender.genSuccessCard(SoupGameMessage.hasStopped) + ) + return + } + // 有进行中的游戏,关闭游戏 + logger.info(`chatId: ${chatId} is closing the game`) + const res = await db.soupGame.close(activeGame.id) + if (!res) { + logger.error(`chatId: ${chatId} failed to close the game`) + await larkService.message.replyCard( + messageId, + cardGender.genErrorCard(SoupGameMessage.startFailed) + ) + } + // 手动结束 + if (which === "manual") { + await larkService.message.replyCard( + messageId, + cardGender.genCard("markdownSuccessCard", { + content: ` +游戏结束! + +**汤面:**${activeGame?.query} + +**汤底:**${activeGame?.answer}`, + }) + ) + } else { + // 自动结束 + await larkService.message.replyCard( + messageId, + cardGender.genCard("markdownSuccessCard", { + llmRes: ` +恭喜您回答正确!游戏结束! + +**汤面:**${activeGame?.query} + +**汤底:**${activeGame?.answer}`, + }) + ) + } + return + } + + // 开始游戏,有进行中的游戏 + if (activeGame) { + logger.info(`chatId: ${chatId} has an active game`) + await larkService.message.replyCard( + messageId, + cardGender.genSuccessCard(SoupGameMessage.hasStarted) + ) + return + } + logger.info(`chatId: ${chatId} is starting a new game`) + // 没有进行中的游戏,开始新游戏 + const game = await attachService.startSoup() + if (!game) { + logger.error(`chatId: ${chatId} failed to start a new game`) + await larkService.message.replyCard( + messageId, + cardGender.genErrorCard(SoupGameMessage.startFailed) + ) + return + } + // 写到数据库 + const newSoupGame: SoupGame = { + ...game, + chatId, + history: [], + active: true, + } + const res = await db.soupGame.create(newSoupGame) + if (!res) { + logger.error(`chatId: ${chatId} failed to create a new game`) + await larkService.message.replyCard( + messageId, + cardGender.genErrorCard(SoupGameMessage.startFailed) + ) + return + } + logger.info(`chatId: ${chatId} created a new game`) + // 回复用户模型的消息 + await larkService.message.replyCard( + messageId, + cardGender.genCard("markdownSuccessCard", { + content: ` +游戏开始啦! + +**题目:**${game.title} + +**汤面:**${game.query} + +艾特机器人说“结束游戏”即可结束游戏 + `, + }) + ) } + +const chat2Soup = async (ctx: Context) => { + const { + larkBody: { msgText, chatId, messageId }, + logger, + attachService, + larkCard, + larkService, + } = ctx + const cardGender = larkCard.child("soupAgent") + const activeGame = await db.soupGame.getActiveOneByChatId(chatId) + if (!activeGame) { + logger.info(`chatId: ${chatId} has no active game`) + return + } + const { + data: { message_id }, + } = await larkService.message.reply(messageId, "text", "模型生成中...") + + const res = await attachService.chat2Soup({ + user_query: msgText, + soup_id: activeGame.title, + history: "", + }) + if (!res) { + logger.error(`chatId: ${chatId} failed to get soup result`) + await larkService.message.replyCard( + messageId, + cardGender.genErrorCard(SoupGameMessage.chatFailed) + ) + return + } + // 用户答对了 + if (res.type === "END") { + await startOrStopGame(ctx, false, "auto") + return + } + // 继续游戏,更新历史记录 + await db.soupGame.insertHistory(activeGame.id, [ + ...activeGame.history, + msgText, + ]) + // 回复用户模型的消息 + await larkService.message.update(message_id, res.content, true) +} + +/** + * 海龟汤游戏 + * @param ctx + */ +const soupAgent = async (ctx: Context) => { + const { + larkBody: { msgText, chatId }, + } = ctx + if (!chatId) return + if (msgText === "开始游戏" && isP2POrAtBot(ctx)) { + startOrStopGame(ctx, true) + return true + } + if (msgText === "结束游戏" && isP2POrAtBot(ctx)) { + startOrStopGame(ctx, false) + return true + } + const activeGame = await db.soupGame.getActiveOneByChatId(chatId) + if (!activeGame) return false + chat2Soup(ctx) + return true +} + +export default soupAgent diff --git a/db/index.ts b/db/index.ts index 2ca54b0..bba1bf3 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 soupGame from "./soupGame" import user from "./user" const db = { @@ -14,6 +15,7 @@ const db = { user, grpSumLog, gitlabProject, + soupGame, } export default db diff --git a/db/soupGame/index.ts b/db/soupGame/index.ts new file mode 100644 index 0000000..bbf64ed --- /dev/null +++ b/db/soupGame/index.ts @@ -0,0 +1,69 @@ +import { RecordModel } from "pocketbase" + +import { managePbError } from "../../utils/pbTools" +import pbClient from "../pbClient" + +const DB_NAME = "soupGame" + +export interface SoupGame { + chatId: string + title: string + query: string + answer: string + history: string[] + active: boolean +} + +export type SoupGameModel = SoupGame & RecordModel + +/** + * 创建一个新的SoupGame记录 + * @param {SoupGame} soupGame - SoupGame对象 + * @returns {Promise} - 创建的SoupGame记录 + */ +const create = (soupGame: SoupGame) => + managePbError(() => + pbClient.collection(DB_NAME).create(soupGame) + ) + +/** + * 根据chatId获取一个活跃的SoupGame记录 + * @param {string} chatId - 聊天ID + * @returns {Promise} - 获取的SoupGame记录 + */ +const getActiveOneByChatId = (chatId: string) => + managePbError(() => + pbClient + .collection(DB_NAME) + .getFirstListItem(`chatId = "${chatId}" && active = true`) + ) + +/** + * 根据chatId关闭一个SoupGame记录 + * @param {string} chatId - 聊天ID + * @returns {Promise} - 更新的SoupGame记录 + */ +const close = (id: string) => + managePbError(() => + pbClient.collection(DB_NAME).update(id, { active: false }) + ) + +/** + * 根据chatId插入历史记录 + * @param {string} chatId - 聊天ID + * @param {string} history - 历史记录 + * @returns {Promise} - 更新的SoupGame记录 + */ +const insertHistory = (id: string, history: string[]) => + managePbError(() => + pbClient.collection(DB_NAME).update(id, { history }) + ) + +const soupGame = { + create, + getActiveOneByChatId, + close, + insertHistory, +} + +export default soupGame diff --git a/package.json b/package.json index 5956d69..b5beb78 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "lint-staged": "^15.3.0", "oxlint": "^0.13.2", "prettier": "^3.4.2", - "typescript-eslint": "^8.19.1" + "typescript-eslint": "^8.20.0" }, "peerDependencies": { "typescript": "^5.5.4" @@ -39,9 +39,9 @@ "@egg/hooks": "^1.2.0", "@egg/lark-msg-tool": "^1.21.0", "@egg/logger": "^1.6.0", - "@egg/net-tool": "^1.22.0", + "@egg/net-tool": "^1.23.0", "@egg/path-tool": "^1.4.1", - "@langchain/core": "^0.3.29", + "@langchain/core": "^0.3.30", "@langchain/langgraph": "^0.2.39", "@langchain/openai": "^0.3.17", "joi": "^17.13.3", diff --git a/routes/bot/eventMsg.ts b/routes/bot/eventMsg.ts index c176c7b..e762105 100644 --- a/routes/bot/eventMsg.ts +++ b/routes/bot/eventMsg.ts @@ -2,17 +2,9 @@ import tempMap from "../../constant/template" import gitlabEvent from "../../controller/gitlabEvent" import groupAgent from "../../controller/groupAgent" import createKVTemp from "../../controller/sheet/createKVTemp" +import soupAgent from "../../controller/soupAgent" import { Context } from "../../types" - -/** - * 判断是否为非群聊和非艾特机器人的消息 - * @param {Context} ctx - 上下文数据,包含body, logger和larkService - * @returns {boolean} 是否为非法消息 - */ -const isNotP2POrAtBot = (ctx: Context) => { - const { larkBody, appInfo } = ctx - return !larkBody.isP2P && !larkBody.isAtBot(appInfo.appName) -} +import { isNotP2POrAtBot } from "../../utils/message" /** * 过滤出非法消息,如果发表情包就直接发回去 @@ -247,7 +239,8 @@ const manageCMDMsg = async (ctx: Context) => { export const manageEventMsg = async (ctx: Context) => { // 过滤非法消息 if (await filterIllegalMsg(ctx)) return - // TODO: 海龟汤 + // 海龟汤 + if (await soupAgent(ctx)) return // 非群聊和非艾特机器人的消息不处理 if (isNotP2POrAtBot(ctx)) return // 处理命令消息 diff --git a/services/attach/index.ts b/services/attach/index.ts index d0e5239..f9ea160 100644 --- a/services/attach/index.ts +++ b/services/attach/index.ts @@ -13,8 +13,17 @@ interface Chat2SoupResp { } interface Soup { + /** + * 海龟汤ID + */ title: string + /** + * 海龟汤内容 + */ query: string + /** + * 海龟汤答案 + */ answer: string } diff --git a/test/soupAgent/chat.http b/test/soupAgent/chat.http new file mode 100644 index 0000000..00e2198 --- /dev/null +++ b/test/soupAgent/chat.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":"a953974feed34108023c8b93b2050ee8","token":"tV9djUKSjzVnekV7xTg2Od06NFTcsBnj","create_time":"1736840977726","event_type":"im.message.receive_v1","tenant_key":"2ee61fe50f4f1657","app_id":"cli_a1eff35b43b89063"},"event":{"message":{"chat_id":"oc_8c789ce8f4ecc6695bb63ca6ec4c61ea","chat_type":"group","content":"{\"text\":\"高跟鞋上带刀子么\"}","create_time":"1736840977558","message_id":"om_d8740d16da00fb65ece605492f8d0c9a","message_type":"text","update_time":"1736840977558"},"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/soupAgent/startGame.http b/test/soupAgent/startGame.http new file mode 100644 index 0000000..a64c521 --- /dev/null +++ b/test/soupAgent/startGame.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":"5831cd388aa714f2c7a1169116f7713e","token":"tV9djUKSjzVnekV7xTg2Od06NFTcsBnj","create_time":"1736839850758","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":"1736839850411","mentions":[{"id":{"open_id":"ou_032f507d08f9a7f28b042fcd086daef5","union_id":"on_7111660fddd8302ce47bf1999147c011","user_id":""},"key":"@_user_1","name":"小煎蛋","tenant_key":"2ee61fe50f4f1657"}],"message_id":"om_150b786da5e9fa5f1adcd9aa0b2a2caf","message_type":"text","update_time":"1736839850411"},"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/soupAgent/stopGame.http b/test/soupAgent/stopGame.http new file mode 100644 index 0000000..01b8122 --- /dev/null +++ b/test/soupAgent/stopGame.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":"774d0a7ef767c8414f31d3da3372fea7","token":"tV9djUKSjzVnekV7xTg2Od06NFTcsBnj","create_time":"1736840049741","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":"1736840049538","mentions":[{"id":{"open_id":"ou_032f507d08f9a7f28b042fcd086daef5","union_id":"on_7111660fddd8302ce47bf1999147c011","user_id":""},"key":"@_user_1","name":"小煎蛋","tenant_key":"2ee61fe50f4f1657"}],"message_id":"om_9fda842ec5ede6f97fcdcc0fa6231203","message_type":"text","update_time":"1736840049538"},"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/message.ts b/utils/message.ts new file mode 100644 index 0000000..a9fe74b --- /dev/null +++ b/utils/message.ts @@ -0,0 +1,20 @@ +import { Context } from "../types" + +/** + * 判断是否为非群聊和非艾特机器人的消息 + * @param {Context} ctx - 上下文数据,包含body, logger和larkService + * @returns {boolean} 是否为非法消息 + */ +export const isNotP2POrAtBot = (ctx: Context) => { + const { larkBody, appInfo } = ctx + return !larkBody.isP2P && !larkBody.isAtBot(appInfo.appName) +} + +/** + * 判断是否为群聊或者艾特机器人的消息 + * @param ctx + * @returns + */ +export const isP2POrAtBot = (ctx: Context) => { + return !isNotP2POrAtBot(ctx) +}