feat(group-agent): 新增支持群组问答
Some checks failed
Egg Server MIflow / build-image (push) Failing after 5m7s

This commit is contained in:
zhaoyingbo 2024-09-25 09:14:10 +00:00
parent 88ccafed92
commit b992ee0b21
26 changed files with 660 additions and 124 deletions

View File

@ -5,14 +5,17 @@
"Chakroun",
"commitlint",
"dbaeumer",
"deepseek",
"devcontainer",
"devcontainers",
"eamodio",
"esbenp",
"Gruntfuggly",
"langchain",
"metas",
"mina",
"mindnote",
"openai",
"openchat",
"tseslint",
"userid",

View File

@ -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 . .

BIN
bun.lockb

Binary file not shown.

40
db/appConfig/index.ts Normal file
View File

@ -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<AppConfigRecordModel>(() =>
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

View File

@ -0,0 +1,27 @@
import { DB } from "../../types"
import { managePb404 } from "../../utils/pbTools"
import pbClient from "../pbClient"
const get = async (userId: string) =>
managePb404<DB.GroupAgentConfig>(() =>
pbClient
.collection("group_agent_config")
.getFirstListItem(`user_id='${userId}'`)
)
const upsert = async (data: Partial<DB.GroupAgentConfig>) => {
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

View File

@ -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

View File

@ -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",

View File

@ -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<string>} ChatId卡片的JSON字符串
*/
const makeChatIdCard = async (body: LarkAction.Data): Promise<string> => {
const makeChatIdCard = async ({ body }: Context.Data): Promise<string> => {
await sleep(500)
return JSON.stringify({
type: "template",
@ -27,6 +27,7 @@ const makeChatIdCard = async (body: LarkAction.Data): Promise<string> => {
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<void>}
*/
const manageBtnClick = async ({
body,
larkService,
logger,
}: Context.Data): Promise<void> => {
const manageBtnClick = async (ctx: Context.Data): Promise<void> => {
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
}

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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,
}
)
}

View File

@ -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
})
}

33
services/lark/chat.ts Normal file
View File

@ -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<LarkServer.ChatGroupData>
>(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

View File

@ -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<LarkServer.BatchDocMetaRes>(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",
}

View File

@ -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
}

View File

@ -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 IDID类型应与查询参数receive_id_type
* @param {MsgType} msg_type textpostimagefileaudiomediastickerinteractiveshare_chatshare_user
* @param {string} content JSON结构序列化后的字符串msg_type对应不同内容
* @param receiveIdType id类型 open_id/user_id/union_id/email/chat_id
* @param receiveId IDID类型应与查询参数receiveIdType
* @param msgType textpostimagefileaudiomediastickerinteractiveshare_chatshare_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<LarkServer.BaseRes>(path, {
receive_id,
msg_type,
return this.post<LarkServer.BaseRes<{ message_id: string }>>(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 IDID类型应与查询参数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 IDID类型应与查询参数receiveIdType
* @param content
*/
async sendInteractive2Chat(receiveId: string, content: string) {
return this.send("chat_id", receiveId, "interactive", content)
}
/**
*
* @param receiveId IDID类型应与查询参数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<LarkServer.BaseRes>(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<LarkServer.MessageData>
>(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

View File

@ -4,10 +4,10 @@ import LarkBaseService from "./base"
class LarkSheetService extends LarkBaseService {
/**
*
* @param {string} sheetToken -
* @param {string} range -
* @param {string[][]} values -
* @returns {Promise<LarkServer.BaseRes>} 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<LarkServer.SpreadsheetRes>} 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<LarkServer.BaseRes} 返回一个包含响应数据的Promise
* @param appToken
* @returns Promise
*/
async getTables(appToken: string) {
const path = `/bitable/v1/apps/${appToken}/tables`
let has_more = true
const res = [] as LarkServer.TableData[]
while (has_more) {
const tableList = [] as LarkServer.TableData[]
let hasMore = true
let pageToken = ""
while (hasMore) {
const { data, code } = await this.get<
LarkServer.BaseListRes<LarkServer.TableData>
>(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<LarkServer.BaseRes} 返回一个包含响应数据的Promise
* @param appToken
* @param tableId ID
* @returns Promise
*/
async getViews(appToken: string, tableId: string) {
const path = `/bitable/v1/apps/${appToken}/tables/${tableId}/views`
@ -85,9 +88,9 @@ class LarkSheetService extends LarkBaseService {
/**
* ()
* @param {string} appToken -
* @param {string} tableId - ID
* @returns {Promise<LarkServer.BaseRes>} Promise
* @param appToken
* @param tableId ID
* @returns Promise
*/
async getRecords(appToken: string, tableId: string) {
const path = `/bitable/v1/apps/${appToken}/tables/${tableId}/records`

View File

@ -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<LarkServer.UserInfoRes>(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<LarkServer.BatchUserInfoRes>(path, getParams)
}
)

14
test/getChatHistory.ts Normal file
View File

@ -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))

7
test/getInnerList.ts Normal file
View File

@ -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))

View File

@ -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

View File

@ -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<T = any> {
code: number
data: T

View File

@ -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)

75
utils/llm.ts Normal file
View File

@ -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