diff --git a/.vscode/settings.json b/.vscode/settings.json index 056a9f7..8389a5d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,14 +5,17 @@ "Chakroun", "commitlint", "dbaeumer", + "deepseek", "devcontainer", "devcontainers", "eamodio", "esbenp", "Gruntfuggly", + "langchain", "metas", "mina", "mindnote", + "openai", "openchat", "tseslint", "userid", diff --git a/Dockerfile b/Dockerfile index c624fe2..2d17ea3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,13 +2,14 @@ FROM micr.cloud.mioffice.cn/zhaoyingbo/bun:alpine-cn WORKDIR /app -COPY package*.json ./ +# COPY package*.json ./ -COPY bun.lockb ./ +# COPY bun.lockb ./ -COPY .npmrc ./ +# COPY .npmrc ./ -RUN bun install + +# RUN bun install COPY . . diff --git a/bun.lockb b/bun.lockb index 00e541a..060c37b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/db/appConfig/index.ts b/db/appConfig/index.ts new file mode 100644 index 0000000..427740c --- /dev/null +++ b/db/appConfig/index.ts @@ -0,0 +1,40 @@ +import { RecordModel } from "pocketbase" + +import { managePb404 } from "../../utils/pbTools" +import pbClient from "../pbClient" + +interface AppConfigRecordModel extends RecordModel { + value: string +} + +/** + * 获取配置 + * @param key + * @returns + */ +const get = async (key: string) => { + const config = await managePb404(() => + pbClient.collection("config").getFirstListItem(`key='${key}'`) + ) + if (!config) return "" + return config.value +} + +/** + * 获取Deepseek的apiKey + * @returns {string} ak + */ +const getDeepseekApiKey = async () => get("deepseek_api_key") + +/** + * 获取OpenAI的key + * @returns {string} ak + */ +const getOpenAIApiKey = async () => get("openai_api_key") + +const appConfig = { + getOpenAIApiKey, + getDeepseekApiKey, +} + +export default appConfig diff --git a/db/groupAgentConfig/index.ts b/db/groupAgentConfig/index.ts new file mode 100644 index 0000000..8cd6838 --- /dev/null +++ b/db/groupAgentConfig/index.ts @@ -0,0 +1,27 @@ +import { DB } from "../../types" +import { managePb404 } from "../../utils/pbTools" +import pbClient from "../pbClient" + +const get = async (userId: string) => + managePb404(() => + pbClient + .collection("group_agent_config") + .getFirstListItem(`user_id='${userId}'`) + ) + +const upsert = async (data: Partial) => { + const { user_id } = data + const old = await get(user_id!) + if (old) { + await pbClient.collection("group_agent_config").update(old.id, data) + return old.id + } + return pbClient.collection("group_agent_config").create(data) +} + +const groupAgentConfig = { + get, + upsert, +} + +export default groupAgentConfig diff --git a/db/index.ts b/db/index.ts index 9e4d405..4c11aba 100644 --- a/db/index.ts +++ b/db/index.ts @@ -1,5 +1,7 @@ import apiKey from "./apiKey" +import appConfig from "./appConfig" import appInfo from "./appInfo" +import groupAgentConfig from "./groupAgentConfig" import log from "./log" import messageGroup from "./messageGroup" import tenantAccessToken from "./tenantAccessToken" @@ -10,6 +12,8 @@ const db = { messageGroup, log, tenantAccessToken, + groupAgentConfig, + appConfig, } export default db diff --git a/package.json b/package.json index 05e3f7b..b59d7c6 100644 --- a/package.json +++ b/package.json @@ -36,9 +36,10 @@ "dependencies": { "@egg/hooks": "^1.2.0", "@egg/lark-msg-tool": "^1.2.1", - "@egg/logger": "^1.4.2", - "@egg/net-tool": "^1.6.3", + "@egg/logger": "^1.4.3", + "@egg/net-tool": "^1.6.5", "@egg/path-tool": "^1.3.0", + "@langchain/openai": "^0.3.0", "joi": "^17.13.3", "node-schedule": "^2.1.1", "p-limit": "^6.1.0", diff --git a/routes/bot/actionMsg.ts b/routes/bot/actionMsg.ts index 14274d2..bd2d174 100644 --- a/routes/bot/actionMsg.ts +++ b/routes/bot/actionMsg.ts @@ -1,15 +1,15 @@ -import type { LarkAction } from "@egg/lark-msg-tool" import { getActionType, getIsActionMsg } from "@egg/lark-msg-tool" import { sleep } from "bun" import { Context } from "../../types" +import groupAgent from "./groupAgent" /** * 返回ChatId卡片 * @param {LarkAction.Data} body * @returns {Promise} 返回包含ChatId卡片的JSON字符串 */ -const makeChatIdCard = async (body: LarkAction.Data): Promise => { +const makeChatIdCard = async ({ body }: Context.Data): Promise => { await sleep(500) return JSON.stringify({ type: "template", @@ -27,6 +27,7 @@ const makeChatIdCard = async (body: LarkAction.Data): Promise => { const ACTION_MAP = { chat_id: makeChatIdCard, + group_selector: groupAgent.setChatGroupContext, } /** @@ -34,11 +35,8 @@ const ACTION_MAP = { * @param {Context.Data} ctx - 上下文数据,包含body, larkService和logger * @returns {Promise} 无返回值 */ -const manageBtnClick = async ({ - body, - larkService, - logger, -}: Context.Data): Promise => { +const manageBtnClick = async (ctx: Context.Data): Promise => { + const { body, larkService, logger } = ctx const { action } = body?.action?.value as { action: keyof typeof ACTION_MAP } @@ -46,7 +44,7 @@ const manageBtnClick = async ({ if (!action) return const func = ACTION_MAP[action] if (!func) return - const card = await func(body) + const card = await func(ctx) if (!card) return // 更新飞书的卡片 await larkService.message.update(body.open_message_id, card) @@ -64,5 +62,6 @@ export const manageActionMsg = (ctx: Context.Data): boolean => { } const actionType = getActionType(ctx.body) if (actionType === "button") manageBtnClick(ctx) + if (actionType === "select_static") manageBtnClick(ctx) return true } diff --git a/routes/bot/eventMsg.ts b/routes/bot/eventMsg.ts index 0a1f5dd..a72d9a9 100644 --- a/routes/bot/eventMsg.ts +++ b/routes/bot/eventMsg.ts @@ -11,6 +11,7 @@ import { import { LarkService } from "../../services" import { Context } from "../../types" import createKVTemp from "../sheet/createKVTemp" +import groupAgent from "./groupAgent" /** * 是否为P2P或者群聊并且艾特了小煎蛋 @@ -37,12 +38,17 @@ const filterIllegalMsg = ({ }: Context.Data): boolean => { // 没有chatId的消息不处理 const chatId = getChatId(body) - logger.debug(`bot req chatId: ${chatId}`) + logger.info(`bot req chatId: ${chatId}`) if (!chatId) return true + // 非私聊和群聊中艾特小煎蛋的消息不处理 + if (!getIsP2pOrGroupAtBot(body)) { + return true + } + // 获取msgType const msgType = getMsgType(body) - logger.debug(`bot req msgType: ${msgType}`) + logger.info(`bot req msgType: ${msgType}`) // 放行纯文本消息 if (msgType === "text") { // 过滤艾特全体成员的消息 @@ -79,19 +85,19 @@ const filterIllegalMsg = ({ * @param {LarkService} service - Lark服务实例 */ const manageIdMsg = (chatId: string, service: LarkService): void => { - const content = JSON.stringify({ - type: "template", - data: { - config: { - update_multi: true, - }, - template_id: "ctp_AAi3NnHb6zgK", - template_variable: { - chat_id: chatId, - }, - }, + service.message.sendTemp("chat_id", chatId, "ctp_AAi3NnHb6zgK", { + chat_id: chatId, + }) +} + +/** + * 回复引导消息 + * @param {Context.Data} ctx - 上下文数据,包含body, larkService和logger + */ +const manageHelpMsg = (chatId: string, service: LarkService): void => { + service.message.sendTemp("chat_id", chatId, "ctp_AAyVx5R39xU9", { + chat_id: chatId, }) - service.message.send("chat_id", chatId, "interactive", content) } /** @@ -102,15 +108,20 @@ const manageIdMsg = (chatId: string, service: LarkService): void => { const manageCMDMsg = (ctx: Context.Data): boolean => { const { body, logger, larkService, attachService } = ctx const text = getMsgText(body) - logger.debug(`bot req text: ${text}`) + logger.info(`bot req text: ${text}`) const chatId = getChatId(body) - if (!chatId) return false // 处理命令消息 if (text.trim() === "/id") { logger.info(`bot command is /id, chatId: ${chatId}`) manageIdMsg(chatId, larkService) return true } + // 帮助 + if (text.trim() === "/help") { + logger.info(`bot command is /help, chatId: ${chatId}`) + manageHelpMsg(chatId, larkService) + return true + } // CI监控 if (text.trim() === "/ci") { logger.info(`bot command is /ci, chatId: ${chatId}`) @@ -136,33 +147,21 @@ const manageCMDMsg = (ctx: Context.Data): boolean => { createKVTemp.createFromEvent(ctx) return true } + // 选择群组信息 + if (text.trim() === "/sg") { + logger.info(`bot command is /sg, chatId: ${chatId}`) + groupAgent.sendGroupSelector(ctx) + return true + } + // 获取当前群组信息 + if (text.trim() === "/cg") { + logger.info(`bot command is /cg, chatId: ${chatId}`) + groupAgent.getCurrentGroup(ctx) + return true + } return false } -/** - * 回复引导消息 - * @param {Context.Data} ctx - 上下文数据,包含body, larkService和logger - */ -const replyGuideMsg = ({ body, larkService, logger }: Context.Data): void => { - const chatId = getChatId(body) - logger.info(`reply guide message, chatId: ${chatId}`) - if (!chatId) return - const content = JSON.stringify({ - type: "template", - data: { - config: { - enable_forward: false, - update_multi: true, - }, - template_id: "ctp_AAyVx5R39xU9", - template_variable: { - chat_id: chatId, - }, - }, - }) - larkService.message.send("chat_id", chatId, "interactive", content) -} - /** * 处理Event消息 * @param {Context.Data} ctx - 上下文数据 @@ -181,7 +180,7 @@ export const manageEventMsg = (ctx: Context.Data): boolean => { if (manageCMDMsg(ctx)) { return true } - // 返回引导消息 - replyGuideMsg(ctx) + // 群组消息处理 + groupAgent.manageGroupMsg(ctx) return true } diff --git a/routes/bot/groupAgent/groupManager.ts b/routes/bot/groupAgent/groupManager.ts new file mode 100644 index 0000000..854e09f --- /dev/null +++ b/routes/bot/groupAgent/groupManager.ts @@ -0,0 +1,74 @@ +import { getChatId, LarkAction, LarkEvent } from "@egg/lark-msg-tool" + +import db from "../../../db" +import { Context } from "../../../types" +import { genGroupAgentSuccessMsg } from "../../../utils/genMsg" + +/** + * 发送群组选择器 + * @param ctx - 上下文数据,包含body和larkService + */ +const sendGroupSelector = async ({ larkService, body }: Context.Data) => { + const chatId = getChatId(body)! + const { data: innerList } = await larkService.chat.getInnerList() + // 组织群组数据 + const groups = innerList.map((v) => ({ + text: v.name, + value: `${v.chat_id}|${v.name}`, + })) + larkService.message.sendTemp("chat_id", chatId, "ctp_AA00oqPWPTdc", { + groups, + }) +} + +/** + * 获取当前群组 + * @param ctx - 上下文数据,包含body和larkService + */ +const getCurrentGroup = async (ctx: Context.Data, needSendMsg = true) => { + const body = ctx.body as LarkEvent.Data + const chatId = getChatId(body)! + const group = await db.groupAgentConfig.get( + body.event.sender.sender_id.user_id + ) + if (!needSendMsg) return group + if (!group) { + await sendGroupSelector(ctx) + return + } + const msg = genGroupAgentSuccessMsg(`当前群组:${group.chat_name}`) + ctx.larkService.message.send("chat_id", chatId, "interactive", msg) +} + +/** + * 设置群组上下文 + * @param ctx - 上下文数据,包含body, larkService和logger + */ +const setChatGroupContext = async (ctx: Context.Data) => { + const { larkService, logger } = ctx + const body = ctx.body as LarkAction.Data + const targetId = body?.action?.option?.split?.("|")[0] + const targetName = body?.action?.option?.split?.("|")[1] + if (!targetId || !targetName) { + logger.error( + `invalid targetId or targetName: ${JSON.stringify(body?.action)}` + ) + } + // 更新群组数据 + await db.groupAgentConfig.upsert({ + user_id: body.user_id, + chat_id: targetId, + chat_name: targetName, + }) + // 更新成功消息 + const successMsg = genGroupAgentSuccessMsg(`已将群组切换至 ${targetName}`) + larkService.message.update(body.open_message_id, successMsg) +} + +const groupManager = { + sendGroupSelector, + setChatGroupContext, + getCurrentGroup, +} + +export default groupManager diff --git a/routes/bot/groupAgent/index.ts b/routes/bot/groupAgent/index.ts new file mode 100644 index 0000000..7a06954 --- /dev/null +++ b/routes/bot/groupAgent/index.ts @@ -0,0 +1,118 @@ +import { getChatId, getMsgText, LarkEvent } from "@egg/lark-msg-tool" + +import db from "../../../db" +import { Context } from "../../../types" +import { + genGroupAgentErrorMsg, + genGroupAgentSuccessMsg, +} from "../../../utils/genMsg" +import llm from "../../../utils/llm" +import groupManager from "./groupManager" + +/** + * 根据聊天历史回答用户的问题 + * @param ctx - 上下文数据 + * @param userInput - 用户输入 + * @param targetChatId - 目标群组ID + * @param loadingMsgId - loading消息ID + */ +const chat2llm = async ( + ctx: Context.Data, + userInput: string, + targetChatId: string, + loadingMsgId?: string +) => { + const { logger, body } = ctx + // 发送消息给Deepseek模型解析时间,返回格式为 YYYY-MM-DD HH:mm:ss + const { startTime, endTime } = await llm.parseTime(userInput) + logger.info(`Parsed result: startTime = ${startTime}, endTime = ${endTime}`) + // 获取服务器的时区偏移量(以分钟为单位) + const serverTimezoneOffset = new Date().getTimezoneOffset() + // 上海时区的偏移量(UTC+8,以分钟为单位) + const shanghaiTimezoneOffset = -8 * 60 + // 计算时间戳,调整为上海时区 + const startTimeTimestamp = + Math.round(new Date(startTime).getTime() / 1000) + + (shanghaiTimezoneOffset - serverTimezoneOffset) * 60 + const endTimeTimestamp = + Math.round(new Date(endTime).getTime() / 1000) + + (shanghaiTimezoneOffset - serverTimezoneOffset) * 60 + + // 获取群聊中的历史记录 + const { data: chatHistory } = await ctx.larkService.message.getHistory( + targetChatId, + String(startTimeTimestamp), + String(endTimeTimestamp) + ) + // 如果没有历史记录则返回错误消息 + if (chatHistory.length === 0) { + logger.error("Chat history is empty") + const content = genGroupAgentErrorMsg("未找到聊天记录") + if (loadingMsgId) { + await ctx.larkService.message.update(loadingMsgId, content) + } else { + await ctx.larkService.message.sendInteractive2Chat( + getChatId(body), + content + ) + } + return + } + logger.debug(`Chat history: ${JSON.stringify(chatHistory)}`) + const llmRes = await llm.queryWithChatHistory( + userInput, + JSON.stringify(chatHistory) + ) + logger.info(`LLM result: ${llmRes.content}`) + const successMsg = genGroupAgentSuccessMsg(llmRes.content as string) + // 发送LLM结果 + if (loadingMsgId) { + await ctx.larkService.message.update(loadingMsgId, successMsg) + } else { + await ctx.larkService.message.sendInteractive2Chat( + getChatId(body), + successMsg + ) + } +} + +/** + * 群组代理处理消息 + * @param ctx - 上下文数据 + */ +const manageGroupMsg = async (ctx: Context.Data) => { + const { logger } = ctx + logger.info("Start to manage group message") + const body = ctx.body as LarkEvent.Data + // 获取用户输入 + const userInput = getMsgText(body) + // 先获取当前对话的目标群组ID + const group = await groupManager.getCurrentGroup(ctx, false) + // 没有目标群组ID则发送群组选择器,并保存当前问题,在选择后立即处理 + if (!group || !group.chat_id) { + logger.info("No group found, send group selector") + groupManager.sendGroupSelector(ctx) + db.groupAgentConfig.upsert({ + user_id: body.event.sender.sender_id.user_id, + pre_query: userInput, + }) + return + } + // 发送一个loading的消息 + const loadingRes = await ctx.larkService.message.sendInteractive2Chat( + getChatId(body), + genGroupAgentSuccessMsg("小煎蛋正在爬楼中,请稍等...") + ) + if (loadingRes.code !== 0) { + logger.error("Failed to send loading message") + } + const { message_id } = loadingRes.data + await chat2llm(ctx, userInput, group.chat_id, message_id) +} + +const groupAgent = { + ...groupManager, + manageGroupMsg, +} + +export default groupAgent diff --git a/routes/sheet/createKVTemp.ts b/routes/sheet/createKVTemp.ts index 472db8a..5a3f107 100644 --- a/routes/sheet/createKVTemp.ts +++ b/routes/sheet/createKVTemp.ts @@ -1,7 +1,7 @@ import { getChatId, getChatType, getUserId } from "@egg/lark-msg-tool" import { Context, LarkServer } from "../../types" -import { genSheetDbErrorMsg, genTempMsg } from "../../utils/genMsg" +import { genSheetDbErrorMsg } from "../../utils/genMsg" /** * 创建键值多维表格 @@ -93,8 +93,12 @@ const createFromEvent = async (ctx: Context.Data) => { if (addRes.code !== 0) throw new Error(addRes.message) } // 全部成功,发送成功消息 - const successMsg = genTempMsg("ctp_AA00oqPWPXtG", createRes.data) - ctx.larkService.message.send("chat_id", chatId, "interactive", successMsg) + ctx.larkService.message.sendTemp( + "chat_id", + chatId, + "ctp_AA00oqPWPXtG", + createRes.data + ) } catch (e: any) { ctx.logger.error(`create KV bitable failed: ${e.message}`) const errorMsg = genSheetDbErrorMsg(e.message) diff --git a/services/lark/auth.ts b/services/lark/auth.ts index 7cee1be..35ad008 100644 --- a/services/lark/auth.ts +++ b/services/lark/auth.ts @@ -1,12 +1,12 @@ import LarkBaseService from "./base" class LarkAuthService extends LarkBaseService { - getAk(app_id: string, app_secret: string) { + getAk(appId: string, appSecret: string) { return this.post<{ tenant_access_token: string; code: number }>( "/auth/v3/tenant_access_token/internal", { - app_id, - app_secret, + app_id: appId, + app_secret: appSecret, } ) } diff --git a/services/lark/base.ts b/services/lark/base.ts index 2728e2a..cd17d15 100644 --- a/services/lark/base.ts +++ b/services/lark/base.ts @@ -21,7 +21,7 @@ class LarkBaseService extends NetToolBase { data: error.data, message: error.message, } as T - this.logger.error("larkNetTool catch error: ", JSON.stringify(res)) + this.logger.error(`larkNetTool catch error: ${JSON.stringify(res)}`) return res }) } diff --git a/services/lark/chat.ts b/services/lark/chat.ts new file mode 100644 index 0000000..c92a78e --- /dev/null +++ b/services/lark/chat.ts @@ -0,0 +1,33 @@ +import { LarkServer } from "../../types" +import LarkBaseService from "./base" + +class LarkChatService extends LarkBaseService { + /** + * 获取机器人所在群列表 + */ + async getInnerList() { + const path = "/im/v1/chats" + const chatList = [] + let hasMore = true + let pageToken = "" + while (hasMore) { + const { data, code } = await this.get< + LarkServer.BaseListRes + >(path, { + page_size: 100, + page_token: pageToken, + }) + if (code !== 0) break + chatList.push(...data.items) + hasMore = data.has_more + pageToken = data.page_token + } + return { + code: 0, + data: chatList, + message: "ok", + } + } +} + +export default LarkChatService diff --git a/services/lark/drive.ts b/services/lark/drive.ts index 411a58a..ddd2b94 100644 --- a/services/lark/drive.ts +++ b/services/lark/drive.ts @@ -6,14 +6,14 @@ class LarkDriveService extends LarkBaseService { * 批量获取文档元数据。 * * @param docTokens - 文档令牌数组。 - * @param doc_type - 文档类型,默认为 "doc"。 - * @param user_id_type - 用户ID类型,默认为 "user_id"。 + * @param docType - 文档类型,默认为 "doc"。 + * @param userIdType - 用户ID类型,默认为 "user_id"。 * @returns 包含元数据和失败列表的响应对象。 */ async batchGetMeta( docTokens: string[], - doc_type = "doc", - user_id_type = "user_id" + docType = "doc", + userIdType = "user_id" ) { const path = "/drive/v1/metas/batch_query" // 如果docTokens长度超出150,需要分批请求 @@ -27,11 +27,11 @@ class LarkDriveService extends LarkBaseService { const data = { request_docs: docTokensSlice.map((id) => ({ doc_token: id, - doc_type, + doc_type: docType, })), } return this.post(path, data, { - user_id_type, + user_id_type: userIdType, }) } ) @@ -40,7 +40,7 @@ class LarkDriveService extends LarkBaseService { return res.data?.metas || [] }) - const failed_list = responses.flatMap((res) => { + const failedList = responses.flatMap((res) => { return res.data?.failed_list || [] }) @@ -48,7 +48,7 @@ class LarkDriveService extends LarkBaseService { code: 0, data: { metas, - failed_list, + failedList, }, message: "success", } diff --git a/services/lark/index.ts b/services/lark/index.ts index 39143c1..5a2b056 100644 --- a/services/lark/index.ts +++ b/services/lark/index.ts @@ -1,4 +1,5 @@ import LarkAuthService from "./auth" +import LarkChatService from "./chat" import LarkDriveService from "./drive" import LarkMessageService from "./message" import LarkSheetService from "./sheet" @@ -10,6 +11,7 @@ class LarkService { user: LarkUserService sheet: LarkSheetService auth: LarkAuthService + chat: LarkChatService requestId: string constructor(appName: string, requestId: string) { @@ -18,6 +20,7 @@ class LarkService { this.user = new LarkUserService(appName, requestId) this.sheet = new LarkSheetService(appName, requestId) this.auth = new LarkAuthService(appName, requestId) + this.chat = new LarkChatService(appName, requestId) this.requestId = requestId } diff --git a/services/lark/message.ts b/services/lark/message.ts index 8fb0802..a3a0992 100644 --- a/services/lark/message.ts +++ b/services/lark/message.ts @@ -1,40 +1,114 @@ import { LarkServer } from "../../types/larkServer" +import { genTempMsg } from "../../utils/genMsg" import LarkBaseService from "./base" class LarkMessageService extends LarkBaseService { /** * 发送卡片 - * @param {LarkServer.ReceiveIDType} receive_id_type 消息接收者id类型 open_id/user_id/union_id/email/chat_id - * @param {string} receive_id 消息接收者的ID,ID类型应与查询参数receive_id_type 对应 - * @param {MsgType} msg_type 消息类型 包括:text、post、image、file、audio、media、sticker、interactive、share_chat、share_user - * @param {string} content 消息内容,JSON结构序列化后的字符串。不同msg_type对应不同内容 + * @param receiveIdType 消息接收者id类型 open_id/user_id/union_id/email/chat_id + * @param receiveId 消息接收者的ID,ID类型应与查询参数receiveIdType 对应 + * @param msgType 消息类型 包括:text、post、image、file、audio、media、sticker、interactive、share_chat、share_user + * @param content 消息内容,JSON结构序列化后的字符串。不同msgType对应不同内容 */ async send( - receive_id_type: LarkServer.ReceiveIDType, - receive_id: string, - msg_type: LarkServer.MsgType, + receiveIdType: LarkServer.ReceiveIDType, + receiveId: string, + msgType: LarkServer.MsgType, content: string ) { - const path = `/im/v1/messages?receive_id_type=${receive_id_type}` - if (msg_type === "text" && !content.includes('"text"')) { + const path = `/im/v1/messages?receive_id_type=${receiveIdType}` + if (msgType === "text" && !content.includes('"text"')) { content = JSON.stringify({ text: content }) } - return this.post(path, { - receive_id, - msg_type, + return this.post>(path, { + receive_id: receiveId, + msg_type: msgType, content, }) } /** - * 更新卡片 - * @param {string} message_id 消息id - * @param {string} content 消息内容,JSON结构序列化后的字符串。不同msg_type对应不同内容 + * 发送模板消息 + * @param receiveIdType 消息接收者id类型 open_id/user_id/union_id/email/chat_id + * @param receiveId 消息接收者的ID,ID类型应与查询参数receiveIdType 对应 + * @param templateId 模板ID + * @param variable 模板变量 */ - async update(message_id: string, content: string) { - const path = `/im/v1/messages/${message_id}` + async sendTemp( + receiveIdType: LarkServer.ReceiveIDType, + receiveId: string, + templateId: string, + variable: any + ) { + return this.send( + receiveIdType, + receiveId, + "interactive", + genTempMsg(templateId, variable) + ) + } + + /** + * 发送富文本信息 + * @param receiveId 消息接收者的ID,ID类型应与查询参数receiveIdType 对应 + * @param content 消息内容 + */ + async sendInteractive2Chat(receiveId: string, content: string) { + return this.send("chat_id", receiveId, "interactive", content) + } + + /** + * 发送文本信息 + * @param receiveId 消息接收者的ID,ID类型应与查询参数receiveIdType 对应 + * @param content 消息内容 + */ + async sendText2Chat(receiveId: string, content: string) { + return this.send("chat_id", receiveId, "text", content) + } + + /** + * 更新卡片 + * @param messageId 消息id + * @param content 消息内容,JSON结构序列化后的字符串。不同msgType对应不同内容 + */ + async update(messageId: string, content: string) { + const path = `/im/v1/messages/${messageId}` return this.patch(path, { content }) } + + /** + * 获取消息历史记录 + * @param chatId 会话ID + * @param startTime 开始时间 秒级时间戳 + * @param endTime 结束时间 秒级时间戳 + */ + async getHistory(chatId: string, startTime: string, endTime: string) { + const path = `/im/v1/messages` + const messageList = [] as LarkServer.MessageData[] + let hasMore = true + let pageToken = "" + while (hasMore) { + const { code, data } = await this.get< + LarkServer.BaseListRes + >(path, { + container_id_type: "chat", + container_id: chatId, + start_time: startTime, + end_time: endTime, + page_size: 50, + page_token: pageToken, + }) + if (code !== 0) break + messageList.push(...data.items) + hasMore = data.has_more + pageToken = data.page_token + } + return { + code: 0, + data: messageList, + message: "ok", + } + } } export default LarkMessageService diff --git a/services/lark/sheet.ts b/services/lark/sheet.ts index 88ce7f1..4601ddb 100644 --- a/services/lark/sheet.ts +++ b/services/lark/sheet.ts @@ -4,10 +4,10 @@ import LarkBaseService from "./base" class LarkSheetService extends LarkBaseService { /** * 向电子表格中插入行。 - * @param {string} sheetToken - 表格令牌。 - * @param {string} range - 插入数据的范围。 - * @param {string[][]} values - 要插入的值。 - * @returns {Promise} 返回一个包含响应数据的Promise。 + * @param sheetToken 表格令牌。 + * @param range 插入数据的范围。 + * @param values 要插入的值。 + * @returns 返回一个包含响应数据的Promise。 */ async insertRows(sheetToken: string, range: string, values: string[][]) { const path = `/sheets/v2/spreadsheets/${sheetToken}/values_append?insertDataOption=INSERT_ROWS` @@ -21,9 +21,9 @@ class LarkSheetService extends LarkBaseService { /** * 获取指定范围内的电子表格数据。 - * @param {string} sheetToken - 表格令牌。 - * @param {string} range - 要获取数据的范围。 - * @returns {Promise} 返回一个包含响应数据的Promise。 + * @param sheetToken 表格令牌。 + * @param range 要获取数据的范围。 + * @returns 返回一个包含响应数据的Promise。 */ async getRange(sheetToken: string, range: string) { const path = `/sheets/v2/spreadsheets/${sheetToken}/values/${range}?valueRenderOption=ToString` @@ -32,35 +32,38 @@ class LarkSheetService extends LarkBaseService { /** * 获取指定表格的所有数据表(多维表格专用) - * @param {string} appToken - 表格令牌。 - * @returns {Promise >(path, { page_size: 100, + page_token: pageToken, }) if (code !== 0) break - res.push(...data.items) - has_more = data.has_more + tableList.push(...data.items) + hasMore = data.has_more + pageToken = data.page_token } return { code: 0, - data: res, + data: tableList, message: "ok", } } /** * 获取指定数据表的所有视图(多维表格专用) - * @param {string} appToken - 表格令牌。 - * @param {string} tableId - 表格ID。 - * @returns {Promise} 返回一个包含响应数据的Promise。 + * @param appToken 表格令牌。 + * @param tableId 表格ID。 + * @returns 返回一个包含响应数据的Promise。 */ async getRecords(appToken: string, tableId: string) { const path = `/bitable/v1/apps/${appToken}/tables/${tableId}/records` diff --git a/services/lark/user.ts b/services/lark/user.ts index 0a0e332..5758eb9 100644 --- a/services/lark/user.ts +++ b/services/lark/user.ts @@ -4,7 +4,7 @@ import LarkBaseService from "./base" class LarkUserService extends LarkBaseService { /** * 登录凭证校验 - * @param {string} code 登录凭证 + * @param code 登录凭证 * @returns */ async code2Login(code: string) { @@ -14,38 +14,38 @@ class LarkUserService extends LarkBaseService { /** * 获取用户信息 - * @param {string} user_id 用户ID - * @param {"open_id" | "user_id"} user_id_type 用户ID类型 + * @param userId 用户ID + * @param userIdType 用户ID类型 * @returns */ - async getOne(user_id: string, user_id_type: "open_id" | "user_id") { - const path = `/contact/v3/users/${user_id}` + async getOne(userId: string, userIdType: "open_id" | "user_id") { + const path = `/contact/v3/users/${userId}` return this.get(path, { - user_id_type, + user_id_type: userIdType, }) } /** * 批量获取用户信息 - * @param {string[]} user_ids 用户ID数组 - * @param {"open_id" | "user_id"} user_id_type 用户ID类型 + * @param userIds 用户ID数组 + * @param userIdType 用户ID类型 * @returns */ - async batchGet(user_ids: string[], user_id_type: "open_id" | "user_id") { + async batchGet(userIds: string[], userIdType: "open_id" | "user_id") { const path = `/contact/v3/users/batch` // 如果user_id长度超出50,需要分批请求, - const userCount = user_ids.length + const userCount = userIds.length const maxLen = 50 const requestMap = Array.from( { length: Math.ceil(userCount / maxLen) }, (_, index) => { const start = index * maxLen - const user_idsSlice = user_ids.slice(start, start + maxLen) - const getParams = `${user_idsSlice + const getParams = `${userIds + .slice(start, start + maxLen) .map((id) => `user_ids=${id}`) - .join("&")}&user_id_type=${user_id_type}` + .join("&")}&user_id_type=${userIdType}` return this.get(path, getParams) } ) diff --git a/test/getChatHistory.ts b/test/getChatHistory.ts new file mode 100644 index 0000000..ffbcee4 --- /dev/null +++ b/test/getChatHistory.ts @@ -0,0 +1,14 @@ +import { LarkService } from "../services" + +const service = new LarkService("egg", "") + +const currentTime = Math.floor(new Date().getTime() / 1000) +const yesterdayTime = currentTime - 24 * 60 * 60 + +const res = await service.message.getHistory( + "oc_c83f627bde3da39b01bbbfb026a00111", + yesterdayTime.toString(), + currentTime.toString() +) + +console.log(JSON.stringify(res, null, 2)) diff --git a/test/getInnerList.ts b/test/getInnerList.ts new file mode 100644 index 0000000..c541c5b --- /dev/null +++ b/test/getInnerList.ts @@ -0,0 +1,7 @@ +import { LarkService } from "../services" + +const service = new LarkService("egg", "") + +const res = await service.chat.getInnerList() + +console.log(JSON.stringify(res, null, 2)) diff --git a/types/db.ts b/types/db.ts index 79e047c..6e4f11c 100644 --- a/types/db.ts +++ b/types/db.ts @@ -1,6 +1,13 @@ import { RecordModel } from "pocketbase" export namespace DB { + export interface GroupAgentConfig extends RecordModel { + user_id: string + chat_id: string + chat_name: string + pre_query?: string + } + export interface AppInfo extends RecordModel { name: string app_id: string diff --git a/types/larkServer.ts b/types/larkServer.ts index 1e6c3a8..6ec3ad3 100644 --- a/types/larkServer.ts +++ b/types/larkServer.ts @@ -95,6 +95,39 @@ export namespace LarkServer { view_private_owner_id?: string } + export interface MessageData { + message_id: string + root_id: string + parent_id: string + msg_type: MsgType + create_time: string + update_time: string + deleted: boolean + updated: boolean + chat_id: string + sender: { + id: string + id_type: "open_id" | "app_id" + sender_type: "user" | "app" + } + body: { + content: string + } + mentions: any[] + upper_message_id: string + } + + export interface ChatGroupData { + avatar: string + chat_id: string + description: string + external: boolean + name: string + owner_id: string + owner_id_type: "open_id" | "user_id" + tenant_key: string + } + export interface BaseRes { code: number data: T diff --git a/utils/genMsg.ts b/utils/genMsg.ts index 688852f..2e062b6 100644 --- a/utils/genMsg.ts +++ b/utils/genMsg.ts @@ -56,6 +56,7 @@ export const genTempMsg = (id: string, variable: any) => data: { config: { update_multi: true, + enable_forward: false, }, template_id: id, template_variable: variable, @@ -68,7 +69,7 @@ export const genTempMsg = (id: string, variable: any) => * @returns {string} JSON 字符串 */ export const genSheetDbErrorMsg = (content: string) => - genErrorMsg("🍳 小煎蛋 Sheet DB 错误提醒", content) + genErrorMsg("🍪 小煎蛋 Sheet DB 错误提醒", content) /** * 生成 Sheet DB 成功消息的 JSON 字符串 @@ -76,4 +77,20 @@ export const genSheetDbErrorMsg = (content: string) => * @returns {string} JSON 字符串 */ export const genSheetDbSuccessMsg = (content: string) => - genSuccessMsg("🍳 感谢使用小煎蛋 Sheet DB", content) + genSuccessMsg("🍪 感谢使用小煎蛋 Sheet DB", content) + +/** + * 生成 Group Agent 错误消息的 JSON 字符串 + * @param {string} content - 消息内容 + * @returns {string} JSON 字符串 + */ +export const genGroupAgentErrorMsg = (content: string) => + genErrorMsg("🧑‍💻 小煎蛋 Group Agent 错误提醒", content) + +/** + * 生成 Group Agent 成功消息的 JSON 字符串 + * @param {string} content - 消息内容 + * @returns {string} JSON 字符串 + */ +export const genGroupAgentSuccessMsg = (content: string) => + genSuccessMsg("🧑‍💻 感谢使用小煎蛋 Group Agent", content) diff --git a/utils/llm.ts b/utils/llm.ts new file mode 100644 index 0000000..466983a --- /dev/null +++ b/utils/llm.ts @@ -0,0 +1,75 @@ +import { ChatOpenAI } from "@langchain/openai" +import { z } from "zod" + +import db from "../db" + +/** + * 获取Deepseek模型 + * @param temperature 温度 + */ +const getDeepseekModel = async (temperature = 0) => { + const model = "deepseek-chat" + const apiKey = await db.appConfig.getDeepseekApiKey() + const baseURL = "https://api.deepseek.com" + return new ChatOpenAI({ apiKey, temperature, model }, { baseURL }) +} + +const timeConfig = z.object({ + startTime: z.string().describe("开始时间,格式为 YYYY-MM-DD HH:mm:ss"), + endTime: z.string().describe("结束时间,格式为 YYYY-MM-DD HH:mm:ss"), +}) + +/** + * 解析时间 + * @param userInput 用户输入 + * @returns + */ +const parseTime = async (userInput: string) => { + const model = await getDeepseekModel() + const structuredLlm = model.withStructuredOutput(timeConfig, { name: "time" }) + return await structuredLlm.invoke( + ` + 当前时间为 ${new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })} + 你是一个专业的语义解析工程师,给定以下用户输入,帮我解析出开始时间和结束时间 + 如果不包含时间信息,请返回当天的起始时间到当前时间 + 用户输入: + \`\`\` + ${userInput.replaceAll("`", " ")} + \`\`\` + ` + ) +} + +/** + * 根据聊天历史回答用户的问题 + * @param userInput 用户输入 + * @param chatHistory 聊天历史 + * @returns + */ +const queryWithChatHistory = async (userInput: string, chatHistory: string) => { + const model = await getDeepseekModel(0.5) + return await model.invoke( + ` + 当前时间为 ${new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })} + 你是一个专业的聊天记录分析工程师,给定以下用户输入和聊天历史,帮我回答用户的问题 + 如果无法回答或者用户的问题不清晰,请引导用户问出更具体的问题 + + 用户输入: + \`\`\` + ${userInput.replaceAll("`", " ")} + \`\`\` + 聊天历史: + \`\`\` + ${chatHistory.replaceAll("`", " ")} + \`\`\` + ` + ) +} + +const llm = { + getDeepseekModel, + parseTime, + queryWithChatHistory, +} + +export default llm