feat: 完成海龟汤Agent
This commit is contained in:
parent
68c00f520c
commit
0a427c17cc
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -33,10 +33,12 @@
|
||||
"qwen",
|
||||
"tseslint",
|
||||
"userid",
|
||||
"wangyifei",
|
||||
"wlpbbgiky",
|
||||
"Xauthor",
|
||||
"Xicon",
|
||||
"Xname",
|
||||
"Yingbo",
|
||||
"Yoav",
|
||||
"zhaoyingbo"
|
||||
],
|
||||
|
@ -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
|
||||
|
@ -19,6 +19,11 @@ const functionMap = {
|
||||
xAuthor: "zhaoyingbo",
|
||||
xIcon: "🐙",
|
||||
},
|
||||
soupAgent: {
|
||||
xName: "海龟汤 Agent",
|
||||
xAuthor: "wangyifei15 🕹️ Yingbo",
|
||||
xIcon: "🕹️",
|
||||
},
|
||||
}
|
||||
|
||||
export default functionMap
|
||||
|
@ -9,3 +9,10 @@ export enum RespMessage {
|
||||
cancelFailed = "取消订阅失败",
|
||||
summaryFailed = "总结失败",
|
||||
}
|
||||
|
||||
export enum SoupGameMessage {
|
||||
startFailed = "游戏启动失败",
|
||||
hasStarted = "游戏已经在进行中啦!",
|
||||
hasStopped = "游戏已经结束啦!",
|
||||
chatFailed = "模型调用失败,请再试一次吧!",
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
69
db/soupGame/index.ts
Normal file
69
db/soupGame/index.ts
Normal file
@ -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<SoupGameModel>} - 创建的SoupGame记录
|
||||
*/
|
||||
const create = (soupGame: SoupGame) =>
|
||||
managePbError<SoupGameModel>(() =>
|
||||
pbClient.collection(DB_NAME).create(soupGame)
|
||||
)
|
||||
|
||||
/**
|
||||
* 根据chatId获取一个活跃的SoupGame记录
|
||||
* @param {string} chatId - 聊天ID
|
||||
* @returns {Promise<SoupGameModel>} - 获取的SoupGame记录
|
||||
*/
|
||||
const getActiveOneByChatId = (chatId: string) =>
|
||||
managePbError<SoupGameModel>(() =>
|
||||
pbClient
|
||||
.collection(DB_NAME)
|
||||
.getFirstListItem(`chatId = "${chatId}" && active = true`)
|
||||
)
|
||||
|
||||
/**
|
||||
* 根据chatId关闭一个SoupGame记录
|
||||
* @param {string} chatId - 聊天ID
|
||||
* @returns {Promise<SoupGameModel>} - 更新的SoupGame记录
|
||||
*/
|
||||
const close = (id: string) =>
|
||||
managePbError<SoupGameModel>(() =>
|
||||
pbClient.collection(DB_NAME).update(id, { active: false })
|
||||
)
|
||||
|
||||
/**
|
||||
* 根据chatId插入历史记录
|
||||
* @param {string} chatId - 聊天ID
|
||||
* @param {string} history - 历史记录
|
||||
* @returns {Promise<SoupGameModel>} - 更新的SoupGame记录
|
||||
*/
|
||||
const insertHistory = (id: string, history: string[]) =>
|
||||
managePbError<SoupGameModel>(() =>
|
||||
pbClient.collection(DB_NAME).update(id, { history })
|
||||
)
|
||||
|
||||
const soupGame = {
|
||||
create,
|
||||
getActiveOneByChatId,
|
||||
close,
|
||||
insertHistory,
|
||||
}
|
||||
|
||||
export default soupGame
|
@ -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",
|
||||
|
@ -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
|
||||
// 处理命令消息
|
||||
|
@ -13,8 +13,17 @@ interface Chat2SoupResp {
|
||||
}
|
||||
|
||||
interface Soup {
|
||||
/**
|
||||
* 海龟汤ID
|
||||
*/
|
||||
title: string
|
||||
/**
|
||||
* 海龟汤内容
|
||||
*/
|
||||
query: string
|
||||
/**
|
||||
* 海龟汤答案
|
||||
*/
|
||||
answer: string
|
||||
}
|
||||
|
||||
|
4
test/soupAgent/chat.http
Normal file
4
test/soupAgent/chat.http
Normal 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":"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"}}}
|
4
test/soupAgent/startGame.http
Normal file
4
test/soupAgent/startGame.http
Normal 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":"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"}}}
|
4
test/soupAgent/stopGame.http
Normal file
4
test/soupAgent/stopGame.http
Normal 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":"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"}}}
|
20
utils/message.ts
Normal file
20
utils/message.ts
Normal file
@ -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)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user