413 lines
11 KiB
TypeScript
413 lines
11 KiB
TypeScript
import { genCardOptions, LarkEvent } from "@egg/lark-msg-tool"
|
||
|
||
import { functionOptionList } from "../../constant/card"
|
||
import { Context, LarkServer } from "../../types"
|
||
import llm from "../../utils/llm"
|
||
import getChatHistory from "./chatHistory"
|
||
|
||
/**
|
||
* 生成群组选择器
|
||
* @param ctx - 上下文数据
|
||
* @param innerList - 内部群组列表
|
||
* @param commonVal - 通用值
|
||
*/
|
||
const genGroupSelector = (
|
||
{ larkCard }: Context.Data,
|
||
innerList: LarkServer.ChatGroupData[],
|
||
commonVal: Record<string, any> = {}
|
||
) => {
|
||
const cardGender = larkCard.child("groupAgent")
|
||
// 组织群组数据
|
||
const groupOptions = genCardOptions(
|
||
innerList.reduce(
|
||
(acc, item) => {
|
||
acc[item.name] = `${item.chat_id}|${item.name}`
|
||
return acc
|
||
},
|
||
{} as Record<string, string>
|
||
)
|
||
)
|
||
return cardGender.genCard("groupSelector", {
|
||
groupOptions,
|
||
...commonVal,
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 生成功能选择器
|
||
* @param ctx - 上下文数据
|
||
* @param commonVal - 通用值
|
||
*/
|
||
const genFunctionSelector = (
|
||
{ larkCard }: Context.Data,
|
||
commonVal: Record<string, any> = {}
|
||
) => {
|
||
const cardGender = larkCard.child("groupAgent")
|
||
// 组织功能数据
|
||
const functionOptions = genCardOptions(
|
||
functionOptionList.reduce(
|
||
(acc, item) => {
|
||
acc[item.name] = `${item.id}|${item.name}`
|
||
return acc
|
||
},
|
||
{} as Record<string, string>
|
||
)
|
||
)
|
||
return cardGender.genCard("functionSelector", {
|
||
functionOptions,
|
||
...commonVal,
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 生成时间范围选择器
|
||
* @param ctx - 上下文数据,包含body和larkService
|
||
*/
|
||
const genTimeScopeSelector = (
|
||
{ larkCard }: Context.Data,
|
||
commonVal: Record<string, any> = {}
|
||
) => {
|
||
return larkCard.child("groupAgent").genCard("timeScopeSelector", commonVal)
|
||
}
|
||
|
||
const sendGroupReport = async (
|
||
ctx: Context.Data,
|
||
messageId: string,
|
||
{
|
||
chatId,
|
||
chatName,
|
||
functionId,
|
||
functionName,
|
||
timeScope,
|
||
startTime,
|
||
endTime,
|
||
mentions,
|
||
}: {
|
||
chatId: string
|
||
chatName: string
|
||
functionId: string
|
||
functionName: string
|
||
timeScope?: string
|
||
startTime?: string
|
||
endTime?: string
|
||
mentions?: LarkEvent.Mention[]
|
||
}
|
||
) => {
|
||
const { larkService, logger, larkCard, requestId } = ctx
|
||
const cardGender = larkCard.child("groupAgent")
|
||
const updateCard = (content: any) =>
|
||
larkService.message.update(messageId, content)
|
||
// action需要返回loading的消息,event需要主动update卡片,所以loading就放外边了
|
||
// 记录发送loading消息后的时间戳
|
||
const processStart = Date.now()
|
||
// 获取聊天记录
|
||
const chatHistory = await getChatHistory(ctx, {
|
||
chatId,
|
||
timeScope,
|
||
startTime,
|
||
endTime,
|
||
mentions,
|
||
})
|
||
// 如果没有历史记录则返回错误消息
|
||
if (chatHistory.length === 0) {
|
||
logger.error("Chat history is empty")
|
||
await updateCard(cardGender.genErrorCard("未找到聊天记录"))
|
||
return
|
||
}
|
||
logger.debug(`Chat history: ${JSON.stringify(chatHistory)}`)
|
||
|
||
try {
|
||
const llmRes = await llm.invoke(
|
||
functionId,
|
||
{
|
||
chatHistory: JSON.stringify(chatHistory),
|
||
time: new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }),
|
||
},
|
||
requestId
|
||
)
|
||
// 记录大模型返回结果后的时间戳
|
||
const processEnd = Date.now()
|
||
// 计算时间差并存储在processingTime变量中,以秒为单位
|
||
const processingTime = ((processEnd - processStart) / 1000).toFixed(2)
|
||
logger.info(`LLM takes time: ${processingTime}s, result: ${llmRes}`)
|
||
|
||
// 动态生成content内容
|
||
const timeRange = timeScope
|
||
? `总结天数:**${timeScope}**`
|
||
: `时间范围:**${startTime}** 至 **${endTime}**`
|
||
const mentionsList = mentions
|
||
? `圈选的用户:\n${mentions
|
||
.filter((v) => v.id.user_id)
|
||
.map((mention: any) => `- ${mention.name}`)
|
||
.join("\n")}`
|
||
: ""
|
||
|
||
const content = `群聊:**${chatName}**,功能:**${functionName}**,${timeRange}\n\n${mentionsList}\n\n以下内容由AI模型生成,耗时:**${processingTime}s**`
|
||
|
||
// 更新消息卡片
|
||
await larkService.message.update(
|
||
messageId,
|
||
cardGender.genCard("resultReport", {
|
||
content,
|
||
llmRes,
|
||
})
|
||
)
|
||
} catch (error: any) {
|
||
logger.error(`LLM error: ${error.message}`)
|
||
await larkService.message.update(
|
||
messageId,
|
||
cardGender.genErrorCard("LLM调用失败: " + error.message)
|
||
)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解析用户输入中的值并发送对应的表单卡片
|
||
* @param ctx - 上下文数据
|
||
* @param innerList - 内部群组列表
|
||
*/
|
||
const parseGroupAgentQuery = async (
|
||
ctx: Context.Data,
|
||
innerList: LarkServer.ChatGroupData[],
|
||
mentions?: LarkEvent.Mention[]
|
||
) => {
|
||
// TODO:处理在群聊里的情况,不用获取群组名称
|
||
const {
|
||
larkBody: { msgText, chatId: rawChatId },
|
||
larkService,
|
||
larkCard,
|
||
logger,
|
||
requestId,
|
||
} = ctx
|
||
const cardGender = larkCard.child("groupAgent")
|
||
|
||
// 发送一个loading的消息
|
||
const {
|
||
data: { message_id },
|
||
} = await larkService.message.sendCard2Chat(
|
||
rawChatId,
|
||
cardGender.genPendingCard("分析中,请稍等...")
|
||
)
|
||
|
||
const updateCard = (content: any) =>
|
||
larkService.message.update(message_id, content)
|
||
|
||
// 组织群组数据
|
||
const groupInfo = JSON.stringify(
|
||
innerList.map((v) => ({
|
||
name: v.name,
|
||
id: v.chat_id,
|
||
}))
|
||
)
|
||
// 获取功能信息
|
||
const functionInfo = JSON.stringify(functionOptionList)
|
||
|
||
// 使用大模型解析用户输入
|
||
const { chatId, chatName, functionName, functionId, startTime, endTime } =
|
||
await llm.parseGroupAgentQuery(msgText, groupInfo, functionInfo, requestId)
|
||
logger.info(
|
||
`Parsed group agent query: chatId: ${chatId}, chatName: ${chatName}, functionName: ${functionName}, functionId: ${functionId}, startTime: ${startTime}, endTime: ${endTime}`
|
||
)
|
||
|
||
// 判断顺序是 群组 -> 功能 -> 时间范围
|
||
|
||
// 返回群组选择器,其他的值往里边丢就行
|
||
if (!chatId || !chatName) {
|
||
logger.info("Send group selector")
|
||
updateCard(
|
||
genGroupSelector(ctx, innerList, {
|
||
functionName,
|
||
functionId,
|
||
startTime,
|
||
endTime,
|
||
mentions,
|
||
})
|
||
)
|
||
return
|
||
}
|
||
// 返回功能选择器,其他的值往里边丢就行
|
||
if (!functionId || !functionName) {
|
||
logger.info("Send function selector")
|
||
updateCard(
|
||
genFunctionSelector(ctx, {
|
||
chatId,
|
||
chatName,
|
||
startTime,
|
||
endTime,
|
||
mentions,
|
||
})
|
||
)
|
||
return
|
||
}
|
||
// 返回时间范围选择器,其他的值往里边丢就行
|
||
if (!startTime || !endTime) {
|
||
logger.info("Send time scope selector")
|
||
updateCard(
|
||
genTimeScopeSelector(ctx, {
|
||
chatId,
|
||
chatName,
|
||
functionId,
|
||
functionName,
|
||
mentions,
|
||
})
|
||
)
|
||
return
|
||
}
|
||
|
||
logger.info("Send group report")
|
||
// 设置齐全,返回结果报告
|
||
updateCard(cardGender.genPendingCard("正在爬楼中,请稍等..."))
|
||
sendGroupReport(ctx, message_id, {
|
||
chatId,
|
||
chatName,
|
||
functionId,
|
||
functionName,
|
||
startTime,
|
||
endTime,
|
||
mentions,
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 管理事件消息
|
||
* @param ctx - 上下文数据
|
||
*/
|
||
const manageEventMsg = async (ctx: Context.Data) => {
|
||
const {
|
||
larkBody: { msgText, chatType, chatId: rawChatId, mentions },
|
||
larkService,
|
||
logger,
|
||
} = ctx
|
||
|
||
// 获取群组信息
|
||
const { data: innerList } = await larkService.chat.getInnerList()
|
||
logger.info(`Inner list: ${JSON.stringify(innerList)}`)
|
||
const sendCard = (content: string) =>
|
||
larkService.message.sendCard2Chat(rawChatId, content)
|
||
|
||
// 去过去掉所有非必要的信息为空的话
|
||
if (msgText.replace("/groupchat", "").replaceAll(" ", "") === "") {
|
||
// 私聊发送正常的群组选择器
|
||
if (chatType === "p2p") {
|
||
logger.info("Send group selector to p2p chat")
|
||
await sendCard(genGroupSelector(ctx, innerList, { mentions }))
|
||
return
|
||
}
|
||
// 如果是群聊,获取群聊名称并发送功能
|
||
const {
|
||
data: { name: chatName },
|
||
} = await larkService.chat.getChatInfo(rawChatId)
|
||
logger.info(`Send function selector to group chat: ${chatName}`)
|
||
await sendCard(genFunctionSelector(ctx, { chatName, mentions }))
|
||
return
|
||
}
|
||
|
||
logger.info(`User input: ${msgText}, chatType: ${chatType}, use llm to parse`)
|
||
// 用户有输入,使用大模型进行解析发送对应卡片
|
||
await parseGroupAgentQuery(ctx, innerList, mentions)
|
||
return
|
||
}
|
||
|
||
/**
|
||
* 管理Action消息
|
||
* @param ctx - 上下文数据
|
||
*/
|
||
const manageActionMsg = async (ctx: Context.Data) => {
|
||
const {
|
||
larkBody: { actionOption, actionValue, messageId },
|
||
larkCard,
|
||
logger,
|
||
} = ctx
|
||
const cardGender = larkCard.child("groupAgent")
|
||
logger.debug(`Action option: ${JSON.stringify(actionOption)}`)
|
||
logger.debug(`Action value: ${JSON.stringify(actionValue)}`)
|
||
let { chatId, chatName, functionId, functionName } = actionValue
|
||
const { timeScope, startTime, endTime, cardName, mentions } = actionValue
|
||
// 如果是群组选择器返回值
|
||
if (cardName === "groupSelector") {
|
||
const [newChatId, newChatName] = (actionOption ?? "").split("|")
|
||
if (!newChatId || !newChatName) {
|
||
logger.error(
|
||
`Invalid targetChatId or targetChatName: ${JSON.stringify(actionOption)}`
|
||
)
|
||
return cardGender.genErrorCard("Invalid targetChatId or targetChatName")
|
||
}
|
||
chatId = newChatId
|
||
chatName = newChatName
|
||
}
|
||
// 如果是功能选择器返回值
|
||
if (cardName === "functionSelector") {
|
||
const [newFunctionId, newFunctionName] = (actionOption ?? "").split("|")
|
||
if (!newFunctionId || !newFunctionName) {
|
||
logger.error(
|
||
`Invalid functionId or functionName: ${JSON.stringify(actionOption)}`
|
||
)
|
||
return cardGender.genErrorCard("Invalid functionId or functionName")
|
||
}
|
||
functionId = newFunctionId
|
||
functionName = newFunctionName
|
||
}
|
||
// 时间返回的返回值就会带在timeScope里,不需要再处理
|
||
// 理论上来说,这里的chatId, chatName肯定是有值的,不需要再判断
|
||
// 判断是否需要返回功能选择器
|
||
if (!functionId || !functionName) {
|
||
logger.info("Send function selector")
|
||
return genFunctionSelector(ctx, {
|
||
chatId,
|
||
chatName,
|
||
startTime,
|
||
endTime,
|
||
timeScope,
|
||
mentions,
|
||
})
|
||
}
|
||
|
||
// 判断是否需要返回时间范围选择器
|
||
if (!(timeScope || (startTime && endTime))) {
|
||
logger.info("Send time scope selector")
|
||
return genTimeScopeSelector(ctx, {
|
||
chatId,
|
||
chatName,
|
||
functionId,
|
||
functionName,
|
||
mentions,
|
||
})
|
||
}
|
||
|
||
logger.info("Send group report")
|
||
// 设置齐全,返回结果报告
|
||
sendGroupReport(ctx, messageId, {
|
||
chatId,
|
||
chatName,
|
||
functionId,
|
||
functionName,
|
||
timeScope,
|
||
startTime,
|
||
endTime,
|
||
mentions,
|
||
})
|
||
return cardGender.genPendingCard("正在爬楼中,请稍等...")
|
||
}
|
||
|
||
/**
|
||
* 群组Agent的主入口
|
||
* @param ctx - 上下文数据
|
||
*/
|
||
const manual = async (ctx: Context.Data) => {
|
||
const {
|
||
larkBody: { isEvent, isAction },
|
||
logger,
|
||
} = ctx
|
||
try {
|
||
// 如果是Event,则解析自然语言并发送对应的卡片
|
||
if (isEvent) return await manageEventMsg(ctx)
|
||
// 如果是Action,则取出用户选的值并判断是否需要继续发送表单卡片或者开始大模型推理
|
||
if (isAction) return await manageActionMsg(ctx)
|
||
} catch (e: any) {
|
||
logger.error(`Group agent error: ${e.message}`)
|
||
return ctx.larkCard.child("groupAgent").genErrorCard("Group agent error")
|
||
}
|
||
}
|
||
|
||
export default manual
|