diff --git a/.vscode/settings.json b/.vscode/settings.json index 0cec1de..4339465 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,8 @@ "eamodio", "esbenp", "Gruntfuggly", + "metas", + "mina", "tseslint", "wlpbbgiky", "Yoav" diff --git a/bun.lockb b/bun.lockb index 94d0484..f9abe84 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/db/tenantAccessToken/index.ts b/db/tenantAccessToken/index.ts index 6516ba7..a1931ee 100644 --- a/db/tenantAccessToken/index.ts +++ b/db/tenantAccessToken/index.ts @@ -1,3 +1,4 @@ +import loggerIns from "../../log" import appInfo from "../appInfo" import pbClient from "../pbClient" @@ -19,7 +20,7 @@ const update = async (id: string, appName: string, value: string) => { } tokenCache[appName] = value - console.log(`reset ${appName} access token success`, value) + loggerIns.info(`reset ${appName} access token success: ${value}`) } /** diff --git a/index.ts b/index.ts index 2ae0048..f68ae1d 100644 --- a/index.ts +++ b/index.ts @@ -1,37 +1,41 @@ +import loggerIns from "./log" import { manageBotReq } from "./routes/bot" import { manageMessageReq } from "./routes/message" import { manageMicroAppReq } from "./routes/microApp" import { manageSheetReq } from "./routes/sheet" import { initSchedule } from "./schedule" -import netTool from "./services/netTool" +import genContext from "./utils/genContext" import { makeCheckPathTool } from "./utils/pathTools" initSchedule() const server = Bun.serve({ async fetch(req) { + // 生成上下文 + const ctx = await genContext(req) try { - // 打印当前路由 - console.log("🚀 ~ serve ~ req.url", req.url) // 路由处理 - const { exactCheck, startsWithCheck } = makeCheckPathTool(req.url) + const { exactCheck, startsWithCheck, fullCheck } = makeCheckPathTool( + req.url + ) + // 非根路由打印 + if (!fullCheck("/")) ctx.logger.info(`${req.method} ${req.url}`) // 机器人 - if (exactCheck("/bot")) return await manageBotReq(req) + if (exactCheck("/bot")) return await manageBotReq(ctx) // 消息代理发送 - if (exactCheck("/message")) return await manageMessageReq(req) + if (exactCheck("/message")) return await manageMessageReq(ctx) // 表格代理操作 - if (exactCheck("/sheet")) return await manageSheetReq(req) + if (exactCheck("/sheet")) return await manageSheetReq(ctx) // 小程序 - if (startsWithCheck("/micro_app")) return await manageMicroAppReq(req) + if (startsWithCheck("/micro_app")) return await manageMicroAppReq(ctx) // 其他 - return netTool.ok("hello, there is egg, glade to serve you!") + return ctx.genResp.ok("hello, there is egg, glade to serve you!") } catch (error: any) { // 错误处理 - console.error("🚀 ~ serve ~ error", error) - return netTool.serverError(error.message || "server error") + return ctx.genResp.serverError(error.message || "server error") } }, port: 3000, }) -console.log(`Listening on ${server.hostname}:${server.port}`) +loggerIns.info(`Listening on ${server.hostname}:${server.port}`) diff --git a/log/index.ts b/log/index.ts new file mode 100644 index 0000000..dc532d4 --- /dev/null +++ b/log/index.ts @@ -0,0 +1,55 @@ +import "winston-daily-rotate-file" + +import winston, { format } from "winston" + +const isProd = process.env.NODE_ENV === "production" + +const transports: any[] = [ + new winston.transports.Console({ + level: "info", + }), +] + +if (isProd) { + const config = { + datePattern: "YYYY-MM-DD", + zippedArchive: true, + maxSize: "20m", + maxFiles: "14d", + } + + transports.push( + new winston.transports.DailyRotateFile({ + level: "info", + filename: "/home/work/log/egg-info-%DATE%.log", + ...config, + }) + ) + transports.push( + new winston.transports.DailyRotateFile({ + level: "debug", + filename: "/home/work/log/egg-debug-%DATE%.log", + ...config, + }) + ) +} + +const loggerIns = winston.createLogger({ + level: "silly", + format: format.combine( + format.colorize({ + level: !isProd, + }), // 开发环境下输出彩色日志 + format.simple(), // 简单文本格式化 + format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), + format.printf(({ level, message, timestamp, requestId }) => { + const singleLineMessage = isProd + ? message.replace(/\n/g, " ") // 将换行符替换为空格 + : message + return `${timestamp} [${level}]${requestId ? ` [RequestId: ${requestId}]` : ""}: ${singleLineMessage}` + }) + ), + transports, +}) + +export default loggerIns diff --git a/package.json b/package.json index 5e0d93b..c1733fa 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@commitlint/config-conventional": "^19.2.2", "@eslint/js": "^9.7.0", "@types/node-schedule": "^2.1.7", + "@types/uuid": "^10.0.0", "bun-types": "latest", "eslint": "^9.7.0", "eslint-plugin-simple-import-sort": "^12.1.1", @@ -33,6 +34,10 @@ }, "dependencies": { "node-schedule": "^2.1.1", - "pocketbase": "^0.21.3" + "p-limit": "^6.1.0", + "pocketbase": "^0.21.3", + "uuid": "^10.0.0", + "winston": "^3.14.2", + "winston-daily-rotate-file": "^5.0.0" } -} +} \ No newline at end of file diff --git a/routes/bot/actionMsg.ts b/routes/bot/actionMsg.ts index 745eae9..20a6334 100644 --- a/routes/bot/actionMsg.ts +++ b/routes/bot/actionMsg.ts @@ -1,14 +1,14 @@ import { sleep } from "bun" -import service from "../../services" -import { LarkAction } from "../../types" +import { Context, LarkAction } from "../../types" import { getActionType, getIsActionMsg } from "../../utils/msgTools" /** * 返回ChatId卡片 * @param {LarkAction.Data} body + * @returns {Promise} 返回包含ChatId卡片的JSON字符串 */ -const makeChatIdCard = async (body: LarkAction.Data) => { +const makeChatIdCard = async (body: LarkAction.Data): Promise => { await sleep(500) return JSON.stringify({ type: "template", @@ -30,34 +30,38 @@ const ACTION_MAP = { /** * 处理按钮点击事件 - * @param {LarkAction.Data} body + * @param {Context.Data} ctx - 上下文数据,包含body, larkService和logger + * @returns {Promise} 无返回值 */ -const manageBtnClick = async (body: LarkAction.Data) => { +const manageBtnClick = async ({ + body, + larkService, + logger, +}: Context.Data): Promise => { const { action } = body?.action?.value as { action: keyof typeof ACTION_MAP } + logger.info(`got button click action: ${action}`) if (!action) return const func = ACTION_MAP[action] if (!func) return const card = await func(body) if (!card) return // 更新飞书的卡片 - await service.lark.message.update()(body.open_message_id, card) + await larkService.message.update(body.open_message_id, card) } /** * 处理Action消息 - * @param {LarkAction.Data} body + * @param {Context.Data} ctx - 上下文数据 * @returns {boolean} 是否在本函数中处理了消息 */ -export const manageActionMsg = (body: LarkAction.Data) => { +export const manageActionMsg = (ctx: Context.Data): boolean => { // 过滤非Action消息 - if (!getIsActionMsg(body)) { + if (!getIsActionMsg(ctx.body)) { return false } - const actionType = getActionType(body) - if (actionType === "button") { - manageBtnClick(body) - } + const actionType = getActionType(ctx.body) + if (actionType === "button") manageBtnClick(ctx) return true } diff --git a/routes/bot/eventMsg.ts b/routes/bot/eventMsg.ts index 56e386b..4158556 100644 --- a/routes/bot/eventMsg.ts +++ b/routes/bot/eventMsg.ts @@ -1,5 +1,5 @@ -import service from "../../services" -import { LarkEvent } from "../../types" +import { LarkService } from "../../services" +import { Context, LarkEvent } from "../../types" import { getChatId, getChatType, @@ -14,27 +14,32 @@ import { * @param {LarkEvent.Data} body * @returns {boolean} 是否为P2P或者群聊并且艾特了小煎蛋 */ -const getIsP2pOrGroupAtBot = (body: LarkEvent.Data) => { +const getIsP2pOrGroupAtBot = (body: LarkEvent.Data): boolean => { const isP2p = getChatType(body) === "p2p" const isAtBot = getMentions(body)?.some?.( (mention) => mention.name === "小煎蛋" ) - return isP2p || isAtBot + return Boolean(isP2p || isAtBot) } /** * 过滤出非法消息,如果发表情包就直接发回去 - * @param {LarkEvent.Data} body + * @param {Context.Data} ctx - 上下文数据,包含body, logger和larkService * @returns {boolean} 是否为非法消息 */ -const filterIllegalMsg = (body: LarkEvent.Data) => { +const filterIllegalMsg = ({ + body, + logger, + larkService, +}: Context.Data): boolean => { // 没有chatId的消息不处理 const chatId = getChatId(body) + logger.debug(`bot req chatId: ${chatId}`) if (!chatId) return true // 获取msgType const msgType = getMsgType(body) - + logger.debug(`bot req msgType: ${msgType}`) // 放行纯文本消息 if (msgType === "text") { // 过滤艾特全体成员的消息 @@ -47,16 +52,18 @@ const filterIllegalMsg = (body: LarkEvent.Data) => { // 发表情包就直接发回去 if (msgType === "sticker") { + logger.info(`got a sticker message, chatId: ${chatId}`) const content = body?.event?.message?.content - service.lark.message.send()("chat_id", chatId, "sticker", content) + larkService.message.send("chat_id", chatId, "sticker", content) } // 非表情包只在私聊或者群聊中艾特小煎蛋时才回复 else if (getIsP2pOrGroupAtBot(body)) { + logger.info(`got a illegal message, chatId: ${chatId}`) const content = JSON.stringify({ text: "哇!这是什么东东?我只懂普通文本啦![可爱]", }) - service.lark.message.send()("chat_id", chatId, "text", content) + larkService.message.send("chat_id", chatId, "text", content) } // 非纯文本,全不放行 @@ -65,9 +72,10 @@ const filterIllegalMsg = (body: LarkEvent.Data) => { /** * 发送ID消息 - * @param chatId - 发送消息的chatId + * @param {string} chatId - 发送消息的chatId + * @param {LarkService} service - Lark服务实例 */ -const manageIdMsg = async (chatId: string) => { +const manageIdMsg = (chatId: string, service: LarkService): void => { const content = JSON.stringify({ type: "template", data: { @@ -80,33 +88,46 @@ const manageIdMsg = async (chatId: string) => { }, }, }) - service.lark.message.send()("chat_id", chatId, "interactive", content) + service.message.send("chat_id", chatId, "interactive", content) } /** * 处理命令消息 - * @param body - 消息体 - * @returns + * @param {Context.Data} ctx - 上下文数据,包含body, logger, larkService和attachService + * @returns {boolean} 是否处理了命令消息 */ -const manageCMDMsg = (body: LarkEvent.Data) => { +const manageCMDMsg = ({ + body, + logger, + larkService, + attachService, +}: Context.Data): boolean => { const text = getMsgText(body) - console.log("🚀 ~ manageCMDMsg ~ text:", text) + logger.debug(`bot req text: ${text}`) const chatId = getChatId(body) + // 处理命令消息 if (text.trim() === "/id") { - manageIdMsg(chatId) + logger.info(`bot command is /id, chatId: ${chatId}`) + manageIdMsg(chatId, larkService) return true } + // CI监控 if (text.trim() === "/ci") { - service.attach.ciMonitor(chatId) + logger.info(`bot command is /ci, chatId: ${chatId}`) + attachService.ciMonitor(chatId) return true } + // 简报 if (text.includes("share") && text.includes("简报")) { - service.attach.reportCollector(body) + logger.info(`bot command is share report, chatId: ${chatId}`) // 这个用时比较久,先发一条提醒用户收到了请求 - const content = JSON.stringify({ - text: "正在为您收集简报,请稍等片刻~", - }) - service.lark.message.send()("chat_id", chatId, "text", content) + larkService.message.send( + "chat_id", + chatId, + "text", + "正在为您收集简报,请稍等片刻~" + ) + attachService.reportCollector(body) return true } return false @@ -114,10 +135,11 @@ const manageCMDMsg = (body: LarkEvent.Data) => { /** * 回复引导消息 - * @param {LarkEvent.Data} body + * @param {Context.Data} ctx - 上下文数据,包含body, larkService和logger */ -const replyGuideMsg = async (body: LarkEvent.Data) => { +const replyGuideMsg = ({ body, larkService, logger }: Context.Data): void => { const chatId = getChatId(body) + logger.info(`reply guide message, chatId: ${chatId}`) const content = JSON.stringify({ type: "template", data: { @@ -131,28 +153,28 @@ const replyGuideMsg = async (body: LarkEvent.Data) => { }, }, }) - await service.lark.message.send()("chat_id", chatId, "interactive", content) + larkService.message.send("chat_id", chatId, "interactive", content) } /** * 处理Event消息 - * @param {LarkUserAction} body + * @param {Context.Data} ctx - 上下文数据 * @returns {boolean} 是否在本函数中处理了消息 */ -export const manageEventMsg = (body: LarkEvent.Data) => { +export const manageEventMsg = (ctx: Context.Data): boolean => { // 过滤非Event消息 - if (!getIsEventMsg(body)) { + if (!getIsEventMsg(ctx.body)) { return false } // 过滤非法消息 - if (filterIllegalMsg(body)) { + if (filterIllegalMsg(ctx)) { return true } // 处理命令消息 - if (manageCMDMsg(body)) { + if (manageCMDMsg(ctx)) { return true } // 返回引导消息 - replyGuideMsg(body) + replyGuideMsg(ctx) return true } diff --git a/routes/bot/index.ts b/routes/bot/index.ts index 664a861..86147df 100644 --- a/routes/bot/index.ts +++ b/routes/bot/index.ts @@ -1,18 +1,32 @@ -import netTool from "../../services/netTool" +import { Context } from "../../types" import { manageActionMsg } from "./actionMsg" import { manageEventMsg } from "./eventMsg" -export const manageBotReq = async (req: Request) => { - const body = (await req.json()) as any - console.log("🚀 ~ manageBotReq ~ body:", body) - // 验证机器人 - if (body?.type === "url_verification") { - return Response.json({ challenge: body?.challenge }) +/** + * 处理机器人请求 + * @param {Context.Data} ctx - 上下文数据,包含请求体、日志记录器和响应生成器 + * @returns {Promise} 返回响应对象 + */ +export const manageBotReq = async (ctx: Context.Data): Promise => { + const { body } = ctx + + // 检查请求体是否为空 + if (!body) { + return ctx.genResp.badRequest("bot req body is empty") } + + // 验证机器人 + if (body.type === "url_verification") { + ctx.logger.info(`bot challenge: ${body.challenge}`) + return Response.json({ challenge: body.challenge }) + } + // 处理Event消息 - if (manageEventMsg(body)) return netTool.ok() + if (manageEventMsg(ctx)) return ctx.genResp.ok() + // 处理Action消息 - if (manageActionMsg(body)) return netTool.ok() - // 其他 - return netTool.ok() + if (manageActionMsg(ctx)) return ctx.genResp.ok() + + // 其他情况,返回成功响应 + return ctx.genResp.ok() } diff --git a/routes/message/index.ts b/routes/message/index.ts index e65b727..ce8281a 100644 --- a/routes/message/index.ts +++ b/routes/message/index.ts @@ -1,34 +1,49 @@ import db from "../../db" -import service from "../../services" -import netTool from "../../services/netTool" -import { DB, LarkServer, MsgProxy } from "../../types" +import { Context, DB, LarkServer, MsgProxy } from "../../types" import { safeJsonStringify } from "../../utils/pathTools" const LOG_COLLECTION = "message_log" -const validateMessageReq = (body: MsgProxy.Body) => { +/** + * 校验消息请求的参数 + * @param {Context.Data} ctx - 上下文数据,包含请求体和响应生成器 + * @returns {false | Response} 如果校验失败,返回响应对象;否则返回 false + */ +const validateMessageReq = ({ + body, + genResp, +}: Context.Data): false | Response => { if (!body.api_key) { - return netTool.badRequest("api_key is required") + return genResp.badRequest("api_key is required") } if (!body.group_id && !body.receive_id) { - return netTool.badRequest("group_id or receive_id is required") + return genResp.badRequest("group_id or receive_id is required") } if (body.receive_id && !body.receive_id_type) { - return netTool.badRequest("receive_id_type is required") + return genResp.badRequest("receive_id_type is required") } if (!body.msg_type) { - return netTool.badRequest("msg_type is required") + return genResp.badRequest("msg_type is required") } if (!body.content) { - return netTool.badRequest("content is required") + return genResp.badRequest("content is required") } return false } -export const manageMessageReq = async (req: Request) => { - const body = (await req.json()) as MsgProxy.Body +/** + * 处理消息请求 + * @param {Context.Data} ctx - 上下文数据,包含请求体、日志记录器、响应生成器和 Lark 服务 + * @returns {Promise} 返回响应对象 + */ +export const manageMessageReq = async ( + ctx: Context.Data +): Promise => { + const { body: rawBody, genResp, larkService } = ctx + const body = rawBody as MsgProxy.Body + // 校验参数 - const validateRes = validateMessageReq(body) + const validateRes = validateMessageReq(ctx) if (validateRes) return validateRes // 处理消息内容 @@ -37,7 +52,7 @@ export const manageMessageReq = async (req: Request) => { ? safeJsonStringify(body.content) : body.content - // 遍历所有id发送消息,保存所有对应的messageId + // 初始化发送结果对象 const sendRes = { chat_id: {} as Record, open_id: {} as Record, @@ -55,30 +70,30 @@ export const manageMessageReq = async (req: Request) => { final_content: finalContent, } - // 校验api_key + // 校验 api_key const apiKeyInfo = await db.apiKey.getOne(body.api_key) if (!apiKeyInfo) { const error = "api key not found" db.log.create(LOG_COLLECTION, { ...baseLog, error }) - return netTool.notFound(error) + return genResp.notFound(error) } - // 获取app name + // 获取 app name const appName = apiKeyInfo.expand?.app?.name if (!appName) { const error = "app name not found" db.log.create(LOG_COLLECTION, { ...baseLog, error }) - return netTool.notFound(error) + return genResp.notFound(error) } - // 如果有group_id,则发送给所有group_id中的人 + // 如果有 group_id,则发送给所有 group_id 中的人 if (body.group_id) { // 获取所有接收者 const group = await db.messageGroup.getOne(body.group_id!) if (!group) { const error = "message group not found" db.log.create(LOG_COLLECTION, { ...baseLog, error }) - return netTool.notFound(error) + return genResp.notFound(error) } const { chat_id, open_id, union_id, user_id, email } = group @@ -87,8 +102,9 @@ export const manageMessageReq = async (req: Request) => { const makeSendFunc = (receive_id_type: LarkServer.ReceiveIDType) => { return (receive_id: string) => { sendList.push( - service.lark.message - .send(appName)( + larkService + .child(appName) + .message.send( receive_id_type, receive_id, body.msg_type, @@ -109,12 +125,13 @@ export const manageMessageReq = async (req: Request) => { if (email) email.map(makeSendFunc("email")) } - // 如果有receive_id,则发送给所有receive_id中的人 + // 如果有 receive_id,则发送给所有 receive_id 中的人 if (body.receive_id && body.receive_id_type) { body.receive_id.split(",").forEach((receive_id) => { sendList.push( - service.lark.message - .send(appName)( + larkService + .child(appName) + .message.send( body.receive_id_type, receive_id, body.msg_type, @@ -128,14 +145,14 @@ export const manageMessageReq = async (req: Request) => { } try { - // 里边有错误处理,这里不用担心执行不完 - await Promise.all(sendList) + // 发送消息 + await Promise.allSettled(sendList) // 保存消息记录 db.log.create(LOG_COLLECTION, { ...baseLog, send_result: sendRes }) - return netTool.ok(sendRes) + return genResp.ok(sendRes) } catch { const error = "send msg failed" db.log.create(LOG_COLLECTION, { ...baseLog, error }) - return netTool.serverError(error, sendRes) + return genResp.serverError(error, sendRes) } } diff --git a/routes/microApp/index.ts b/routes/microApp/index.ts index 1fed703..9f6ef0e 100644 --- a/routes/microApp/index.ts +++ b/routes/microApp/index.ts @@ -1,5 +1,4 @@ -import service from "../../services" -import netTool from "../../services/netTool" +import { Context } from "../../types" import { makeCheckPathTool } from "../../utils/pathTools" /** @@ -7,34 +6,28 @@ import { makeCheckPathTool } from "../../utils/pathTools" * @param req * @returns */ -const manageLogin = async (req: Request) => { +const manageLogin = async (ctx: Context.Data) => { + const { req, larkService, genResp, logger } = ctx + logger.info("micro app login") const url = new URL(req.url) const code = url.searchParams.get("code") const appName = url.searchParams.get("app_name") || undefined if (!code) { - return netTool.badRequest("code not found") + return genResp.badRequest("code not found") } const { code: resCode, data, - msg, - } = await service.lark.user.code2Login(appName)(code) + message, + } = await larkService.child(appName).user.code2Login(code) - console.log("🚀 ~ manageLogin:", resCode, data, msg) + logger.debug(`get user session: ${JSON.stringify(data)}`) if (resCode !== 0) { - return Response.json({ - code: resCode, - message: msg, - data: null, - }) + return genResp.serverError(message) } - return Response.json({ - code: 0, - message: "success", - data, - }) + return genResp.ok(data) } /** @@ -42,35 +35,29 @@ const manageLogin = async (req: Request) => { * @param req * @returns */ -const manageBatchUser = async (req: Request) => { - const body = (await req.json()) as any - console.log("🚀 ~ manageBatchUser ~ body:", body) +const manageBatchUser = async (ctx: Context.Data) => { + const { body, genResp, larkService, logger } = ctx + logger.info("batch get user info") + if (!body) return genResp.badRequest("req body is empty") const { user_ids, user_id_type, app_name } = body if (!user_ids) { - return netTool.badRequest("user_ids not found") + return genResp.badRequest("user_ids not found") } if (!user_id_type) { - return netTool.badRequest("user_id_type not found") + return genResp.badRequest("user_id_type not found") } - const { code, data, msg } = await service.lark.user.batchGet(app_name)( - user_ids, - user_id_type - ) + const { code, data, message } = await larkService + .child(app_name) + .user.batchGet(user_ids, user_id_type) + + logger.debug(`batch get user info: ${JSON.stringify(data)}`) - console.log("🚀 ~ manageBatchUser:", code, data, msg) if (code !== 0) { - return Response.json({ - code, - message: msg, - data: null, - }) + return genResp.serverError(message) } - return Response.json({ - code, - message: "success", - data: data.items, - }) + + return genResp.ok(data) } /** @@ -78,15 +65,15 @@ const manageBatchUser = async (req: Request) => { * @param req * @returns */ -export const manageMicroAppReq = async (req: Request) => { - const { exactCheck } = makeCheckPathTool(req.url, "/micro_app") +export const manageMicroAppReq = async (ctx: Context.Data) => { + const { exactCheck } = makeCheckPathTool(ctx.req.url, "/micro_app") // 处理登录请求 if (exactCheck("/login")) { - return manageLogin(req) + return manageLogin(ctx) } // 处理批量获取用户信息请求 if (exactCheck("/batch_user")) { - return manageBatchUser(req) + return manageBatchUser(ctx) } - return netTool.ok() + return ctx.genResp.ok() } diff --git a/routes/sheet/index.ts b/routes/sheet/index.ts index fa8a45b..2a80c57 100644 --- a/routes/sheet/index.ts +++ b/routes/sheet/index.ts @@ -1,59 +1,72 @@ import db from "../../db" -import service from "../../services" -import netTool from "../../services/netTool" +import { Context } from "../../types" import { SheetProxy } from "../../types/sheetProxy" -const validateSheetReq = async (body: SheetProxy.Body) => { +/** + * 校验表格请求的参数 + * @param {Context.Data} ctx - 上下文数据,包含请求体和响应生成器 + * @returns {Promise} 如果校验失败,返回响应对象;否则返回 false + */ +const validateSheetReq = async ( + ctx: Context.Data +): Promise => { + const { body, genResp } = ctx if (!body.api_key) { - return netTool.badRequest("api_key is required") + return genResp.badRequest("api_key is required") } if (!body.sheet_token) { - return netTool.badRequest("sheet_token is required") + return genResp.badRequest("sheet_token is required") } if (!body.range) { - return netTool.badRequest("range is required") + return genResp.badRequest("range is required") } if (!body.values) { - return netTool.badRequest("values is required") + return genResp.badRequest("values is required") } if (!SheetProxy.isType(body.type)) { - return netTool.badRequest("type is invalid") + return genResp.badRequest("type is invalid") } return false } -export const manageSheetReq = async (req: Request) => { - const body = (await req.json()) as SheetProxy.Body +/** + * 处理表格请求 + * @param {Context.Data} ctx - 上下文数据,包含请求体、响应生成器和 Lark 服务 + * @returns {Promise} 返回响应对象 + */ +export const manageSheetReq = async (ctx: Context.Data): Promise => { + const { body: rawBody, genResp, larkService } = ctx + const body = rawBody as SheetProxy.Body + // 校验参数 - const validateRes = await validateSheetReq(body) + const validateRes = await validateSheetReq(ctx) if (validateRes) return validateRes - // 校验api_key + // 校验 api_key const apiKeyInfo = await db.apiKey.getOne(body.api_key) if (!apiKeyInfo) { - return netTool.notFound("api key not found") + return genResp.notFound("api key not found") } // 获取 app name const appName = apiKeyInfo.expand?.app?.name if (!appName) { - return netTool.notFound("app name not found") + return genResp.notFound("app name not found") } if (body.type === "insert") { // 插入行 - const insertRes = await service.lark.sheet.insertRows(appName)( - body.sheet_token, - body.range, - body.values - ) + const insertRes = await larkService + .child(appName) + .sheet.insertRows(body.sheet_token, body.range, body.values) if (insertRes?.code !== 0) { - return netTool.serverError(insertRes?.msg, insertRes?.data) + return genResp.serverError(insertRes?.message) } - // 返回 - return netTool.ok(insertRes?.data) + // 返回插入结果 + return genResp.ok(insertRes?.data) } - return netTool.ok() + // 默认返回成功响应 + return genResp.ok() } diff --git a/schedule/accessToken.ts b/schedule/accessToken.ts index 6ac783c..ec149e8 100644 --- a/schedule/accessToken.ts +++ b/schedule/accessToken.ts @@ -1,20 +1,30 @@ -import db from "../db" -import netTool from "../services/netTool" +import pLimit from "p-limit" -const URL = - "https://open.f.mioffice.cn/open-apis/auth/v3/tenant_access_token/internal" +import db from "../db" +import loggerIns from "../log" +import { LarkService } from "../services" export const resetAccessToken = async () => { try { const appList = await db.appInfo.getFullList() - for (const app of appList) { - const { tenant_access_token } = await netTool.post(URL, { - app_id: app.app_id, - app_secret: app.app_secret, - }) - await db.tenantAccessToken.update(app.id, app.name, tenant_access_token) - } - } catch (error) { - console.error("🚀 ~ resetAccessToken ~ error", error) + const limit = pLimit(3) + const service = new LarkService("", "schedule") + const promiseList = appList.map((app) => + limit(() => + service.auth.getAk(app.app_id, app.app_secret).then((res) => { + if (res.code !== 0) return + return db.tenantAccessToken.update( + app.id, + app.name, + res.tenant_access_token + ) + }) + ) + ) + await Promise.allSettled(promiseList) + } catch (error: any) { + loggerIns + .child({ requestId: "schedule" }) + .error(`resetAccessToken error: ${error.message}`) } } diff --git a/services/attach/index.ts b/services/attach/index.ts index c650c21..b230121 100644 --- a/services/attach/index.ts +++ b/services/attach/index.ts @@ -1,37 +1,26 @@ import { LarkEvent } from "../../types" -import netTool from "../netTool" +import { NetToolBase } from "../../utils/netTool" -/** - * 请求 CI 监控 - */ -const ciMonitor = async (chat_id: string) => { - const URL = `https://ci-monitor.xiaomiwh.cn/gitlab/ci?chat_id=${chat_id}` - try { - const res = await netTool.get(URL) - return (res as string) || "" - } catch { - return "" +class AttachService extends NetToolBase { + /** + * 监控CI状态 + * @param {string} chat_id - 聊天ID。 + * @returns {Promise} 返回CI监控结果。 + */ + async ciMonitor(chat_id: string) { + const URL = `https://ci-monitor.xiaomiwh.cn/gitlab/ci?chat_id=${chat_id}` + return this.get(URL).catch(() => "") + } + + /** + * 收集报告数据 + * @param {LarkEvent.Data} body - 报告数据。 + * @returns {Promise} 返回报告收集结果。 + */ + async reportCollector(body: LarkEvent.Data) { + const URL = "https://report.imoaix.cn/report" + return this.post(URL, body).catch(() => "") } } -/** - * 请求简报收集器 - * @param body - * @returns - */ -const reportCollector = async (body: LarkEvent.Data) => { - const URL = "https://report.imoaix.cn/report" - try { - const res = await netTool.post(URL, body) - return (res as string) || "" - } catch { - return "" - } -} - -const attach = { - ciMonitor, - reportCollector, -} - -export default attach +export default AttachService diff --git a/services/index.ts b/services/index.ts index c6eee96..0b646ab 100644 --- a/services/index.ts +++ b/services/index.ts @@ -1,9 +1,4 @@ -import attach from "./attach" -import lark from "./lark" +import AttachService from "./attach" +import LarkService from "./lark" -const service = { - attach, - lark, -} - -export default service +export { AttachService, LarkService } diff --git a/services/lark/auth.ts b/services/lark/auth.ts new file mode 100644 index 0000000..7cee1be --- /dev/null +++ b/services/lark/auth.ts @@ -0,0 +1,15 @@ +import LarkBaseService from "./base" + +class LarkAuthService extends LarkBaseService { + getAk(app_id: string, app_secret: string) { + return this.post<{ tenant_access_token: string; code: number }>( + "/auth/v3/tenant_access_token/internal", + { + app_id, + app_secret, + } + ) + } +} + +export default LarkAuthService diff --git a/services/lark/base.ts b/services/lark/base.ts new file mode 100644 index 0000000..1b5a224 --- /dev/null +++ b/services/lark/base.ts @@ -0,0 +1,28 @@ +import db from "../../db" +import { NetError, NetToolBase } from "../../utils/netTool" + +class LarkBaseService extends NetToolBase { + constructor(appName: string, requestId: string) { + super({ + prefix: "https://open.f.mioffice.cn/open-apis", + requestId, + getHeaders: async () => ({ + Authorization: `Bearer ${await db.tenantAccessToken.get(appName)}`, + }), + }) + } + + protected async request(params: any): Promise { + return super.request(params).catch((error: NetError) => { + const res = { + code: error.code, + data: null, + message: error.message, + } as T + this.logger.error("larkNetTool catch error: ", JSON.stringify(res)) + return res + }) + } +} + +export default LarkBaseService diff --git a/services/lark/drive.ts b/services/lark/drive.ts index 610c62c..30d2054 100644 --- a/services/lark/drive.ts +++ b/services/lark/drive.ts @@ -1,11 +1,21 @@ import { LarkServer } from "../../types" -import larkNetTool from "./larkNetTool" +import LarkBaseService from "./base" -const batchGetMeta = - (appName?: string) => - async (docTokens: string[], doc_type = "doc", user_id_type = "user_id") => { - const URL = - "https://open.f.mioffice.cn/open-apis/drive/v1/metas/batch_query" +class LarkDriveService extends LarkBaseService { + /** + * 批量获取文档元数据。 + * + * @param {string[]} docTokens - 文档令牌数组。 + * @param {string} [doc_type="doc"] - 文档类型,默认为 "doc"。 + * @param {string} [user_id_type="user_id"] - 用户ID类型,默认为 "user_id"。 + * @returns {Promise<{ code: number, data: { metas: any[], failed_list: any[] }, message: string }>} 包含元数据和失败列表的响应对象。 + */ + async batchGetMeta( + docTokens: string[], + doc_type = "doc", + user_id_type = "user_id" + ) { + const path = "/drive/v1/metas/batch_query" // 如果docTokens长度超出150,需要分批请求 const docTokensLen = docTokens.length const maxLen = 150 @@ -20,13 +30,9 @@ const batchGetMeta = doc_type, })), } - return larkNetTool.post(appName)( - URL, - data, - { - user_id_type, - } - ) + return this.post(path, data, { + user_id_type, + }) } ) const responses = await Promise.all(requestMap) @@ -44,12 +50,9 @@ const batchGetMeta = metas, failed_list, }, - msg: "success", + message: "success", } } - -const drive = { - batchGetMeta, } -export default drive +export default LarkDriveService diff --git a/services/lark/index.ts b/services/lark/index.ts index d04fa35..39143c1 100644 --- a/services/lark/index.ts +++ b/services/lark/index.ts @@ -1,13 +1,30 @@ -import drive from "./drive" -import message from "./message" -import sheet from "./sheet" -import user from "./user" +import LarkAuthService from "./auth" +import LarkDriveService from "./drive" +import LarkMessageService from "./message" +import LarkSheetService from "./sheet" +import LarkUserService from "./user" -const lark = { - message, - user, - drive, - sheet, +class LarkService { + drive: LarkDriveService + message: LarkMessageService + user: LarkUserService + sheet: LarkSheetService + auth: LarkAuthService + requestId: string + + constructor(appName: string, requestId: string) { + this.drive = new LarkDriveService(appName, requestId) + this.message = new LarkMessageService(appName, requestId) + this.user = new LarkUserService(appName, requestId) + this.sheet = new LarkSheetService(appName, requestId) + this.auth = new LarkAuthService(appName, requestId) + this.requestId = requestId + } + + child(appName?: string) { + if (!appName) return this + return new LarkService(appName, this.requestId) + } } -export default lark +export default LarkService diff --git a/services/lark/larkNetTool.ts b/services/lark/larkNetTool.ts deleted file mode 100644 index 66cf818..0000000 --- a/services/lark/larkNetTool.ts +++ /dev/null @@ -1,125 +0,0 @@ -import db from "../../db" -import { LarkServer } from "../../types" -import netTool from "../netTool" - -/** - * 发送网络请求并返回一个解析为响应数据的Promise。 - * @param url - 要发送请求的URL。 - * @param method - 请求使用的HTTP方法。 - * @param queryParams - 要包含在URL中的查询参数。 - * @param payload - 请求的有效负载数据。 - * @param additionalHeaders - 要包含在请求中的附加头。 - * @param appName - 应用名称,用于获取授权令牌。 - * @returns 一个解析为响应数据的Promise。 - * @throws 如果网络响应不成功或存在解析错误,则抛出错误。 - */ -const larkNetTool = async ({ - url, - method, - queryParams, - payload, - additionalHeaders, - appName = "egg", -}: { - url: string - method: string - queryParams?: any - payload?: any - additionalHeaders?: any - appName?: string -}): Promise => { - const headersWithAuth = { - Authorization: `Bearer ${await db.tenantAccessToken.get(appName)}`, - ...additionalHeaders, - } - return netTool({ - url, - method, - queryParams, - payload, - additionalHeaders: headersWithAuth, - }).catch((error) => { - console.error("larkNetTool catch error: ", error) - return { - code: 1, - data: null, - msg: error.message || "网络请求异常", - } as T - }) -} - -/** - * 发送GET请求并返回一个解析为响应数据的Promise。 - * - * @param appName - 应用名称,用于获取授权令牌。 - * @returns 一个函数,该函数接受URL、查询参数和附加头,并返回一个解析为响应数据的Promise。 - */ -larkNetTool.get = - (appName?: string) => - ( - url: string, - queryParams?: any, - additionalHeaders?: any - ): Promise => - larkNetTool({ - url, - method: "get", - queryParams, - additionalHeaders, - appName, - }) - -/** - * 发送POST请求并返回一个解析为响应数据的Promise。 - * - * @param appName - 应用名称,用于获取授权令牌。 - * @returns 一个函数,该函数接受URL、有效负载、查询参数和附加头,并返回一个解析为响应数据的Promise。 - */ -larkNetTool.post = - (appName?: string) => - ( - url: string, - payload?: any, - queryParams?: any, - additionalHeaders?: any - ): Promise => - larkNetTool({ - url, - method: "post", - payload, - queryParams, - additionalHeaders, - appName, - }) - -/** - * 发送DELETE请求并返回一个解析为响应数据的Promise。 - * - * @param appName - 应用名称,用于获取授权令牌。 - * @returns 一个函数,该函数接受URL、有效负载和附加头,并返回一个解析为响应数据的Promise。 - */ -larkNetTool.del = - (appName?: string) => - ( - url: string, - payload: any, - additionalHeaders?: any - ): Promise => - larkNetTool({ url, method: "delete", payload, additionalHeaders, appName }) - -/** - * 发送PATCH请求并返回一个解析为响应数据的Promise。 - * - * @param appName - 应用名称,用于获取授权令牌。 - * @returns 一个函数,该函数接受URL、有效负载和附加头,并返回一个解析为响应数据的Promise。 - */ -larkNetTool.patch = - (appName?: string) => - ( - url: string, - payload: any, - additionalHeaders?: any - ): Promise => - larkNetTool({ url, method: "patch", payload, additionalHeaders, appName }) - -export default larkNetTool diff --git a/services/lark/message.ts b/services/lark/message.ts index f9b8c52..8fb0802 100644 --- a/services/lark/message.ts +++ b/services/lark/message.ts @@ -1,46 +1,40 @@ import { LarkServer } from "../../types/larkServer" -import larkNetTool from "./larkNetTool" +import LarkBaseService from "./base" -/** - * 发送卡片 - * @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对应不同内容 - */ -const send = - (appName?: string) => - async ( +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对应不同内容 + */ + async send( receive_id_type: LarkServer.ReceiveIDType, receive_id: string, msg_type: LarkServer.MsgType, content: string - ) => { - const URL = `https://open.f.mioffice.cn/open-apis/im/v1/messages?receive_id_type=${receive_id_type}` + ) { + const path = `/im/v1/messages?receive_id_type=${receive_id_type}` if (msg_type === "text" && !content.includes('"text"')) { content = JSON.stringify({ text: content }) } - return larkNetTool.post(appName)(URL, { + return this.post(path, { receive_id, msg_type, content, }) } -/** - * 更新卡片 - * @param {string} message_id 消息id - * @param {string} content 消息内容,JSON结构序列化后的字符串。不同msg_type对应不同内容 - */ -const update = - (appName?: string) => async (message_id: string, content: string) => { - const URL = `https://open.f.mioffice.cn/open-apis/im/v1/messages/${message_id}` - return larkNetTool.patch(appName)(URL, { content }) + /** + * 更新卡片 + * @param {string} message_id 消息id + * @param {string} content 消息内容,JSON结构序列化后的字符串。不同msg_type对应不同内容 + */ + async update(message_id: string, content: string) { + const path = `/im/v1/messages/${message_id}` + return this.patch(path, { content }) } - -const message = { - send, - update, } -export default message +export default LarkMessageService diff --git a/services/lark/sheet.ts b/services/lark/sheet.ts index 8ec5eab..de97dba 100644 --- a/services/lark/sheet.ts +++ b/services/lark/sheet.ts @@ -1,16 +1,17 @@ import { LarkServer } from "../../types/larkServer" -import larkNetTool from "./larkNetTool" +import LarkBaseService from "./base" -/** - * 向电子表格中插入行。 - * @param appName - 应用程序的名称(可选)。 - * @returns 一个函数,该函数接受表格令牌、范围和要插入的值。 - */ -const insertRows = - (appName?: string) => - async (sheetToken: string, range: string, values: string[][]) => { - const URL = `https://open.f.mioffice.cn/open-apis/sheets/v2/spreadsheets/${sheetToken}/values_append?insertDataOption=INSERT_ROWS` - return larkNetTool.post(appName)(URL, { +class LarkSheetService extends LarkBaseService { + /** + * 向电子表格中插入行。 + * @param {string} sheetToken - 表格令牌。 + * @param {string} range - 插入数据的范围。 + * @param {string[][]} values - 要插入的值。 + * @returns {Promise} 返回一个包含响应数据的Promise。 + */ + async insertRows(sheetToken: string, range: string, values: string[][]) { + const path = `/sheets/v2/spreadsheets/${sheetToken}/values_append?insertDataOption=INSERT_ROWS` + return this.post(path, { valueRange: { range, values, @@ -18,8 +19,16 @@ const insertRows = }) } -const sheet = { - insertRows, + /** + * 获取指定范围内的电子表格数据。 + * @param {string} sheetToken - 表格令牌。 + * @param {string} range - 要获取数据的范围。 + * @returns {Promise} 返回一个包含响应数据的Promise。 + */ + async getRange(sheetToken: string, range: string) { + const path = `/sheets/v2/spreadsheets/${sheetToken}/values/${range}?valueRenderOption=ToString` + return this.get(path) + } } -export default sheet +export default LarkSheetService diff --git a/services/lark/user.ts b/services/lark/user.ts index bca43a0..0a0e332 100644 --- a/services/lark/user.ts +++ b/services/lark/user.ts @@ -1,39 +1,38 @@ import { LarkServer } from "../../types/larkServer" -import larkNetTool from "./larkNetTool" +import LarkBaseService from "./base" -/** - * 登录凭证校验 - * @param code - * @returns - */ -const code2Login = (appName?: string) => async (code: string) => { - const URL = `https://open.f.mioffice.cn/open-apis/mina/v2/tokenLoginValidate` - return larkNetTool.post(appName)(URL, { code }) -} +class LarkUserService extends LarkBaseService { + /** + * 登录凭证校验 + * @param {string} code 登录凭证 + * @returns + */ + async code2Login(code: string) { + const path = `/mina/v2/tokenLoginValidate` + return this.post(path, { code }) + } -/** - * 获取用户信息 - * @param user_id - * @returns - */ -const get = - (appName?: string) => - async (user_id: string, user_id_type: "open_id" | "user_id") => { - const URL = `https://open.f.mioffice.cn/open-apis/contact/v3/users/${user_id}` - return larkNetTool.get(appName)(URL, { + /** + * 获取用户信息 + * @param {string} user_id 用户ID + * @param {"open_id" | "user_id"} user_id_type 用户ID类型 + * @returns + */ + async getOne(user_id: string, user_id_type: "open_id" | "user_id") { + const path = `/contact/v3/users/${user_id}` + return this.get(path, { user_id_type, }) } -/** - * 批量获取用户信息 - * @param user_ids - * @returns - */ -const batchGet = - (appName?: string) => - async (user_ids: string[], user_id_type: "open_id" | "user_id") => { - const URL = `https://open.f.mioffice.cn/open-apis/contact/v3/users/batch` + /** + * 批量获取用户信息 + * @param {string[]} user_ids 用户ID数组 + * @param {"open_id" | "user_id"} user_id_type 用户ID类型 + * @returns + */ + async batchGet(user_ids: string[], user_id_type: "open_id" | "user_id") { + const path = `/contact/v3/users/batch` // 如果user_id长度超出50,需要分批请求, const userCount = user_ids.length @@ -47,10 +46,7 @@ const batchGet = const getParams = `${user_idsSlice .map((id) => `user_ids=${id}`) .join("&")}&user_id_type=${user_id_type}` - return larkNetTool.get(appName)( - URL, - getParams - ) + return this.get(path, getParams) } ) @@ -65,14 +61,9 @@ const batchGet = data: { items, }, - msg: "success", + message: "success", } } - -const user = { - code2Login, - batchGet, - get, } -export default user +export default LarkUserService diff --git a/services/netTool.ts b/services/netTool.ts deleted file mode 100644 index 082a37f..0000000 --- a/services/netTool.ts +++ /dev/null @@ -1,232 +0,0 @@ -interface NetRequestParams { - url: string - method: string - queryParams?: any - payload?: any - additionalHeaders?: any -} - -/** - * 记录响应详情并返回响应日志对象。 - * @param response - 响应对象。 - * @param method - 请求使用的HTTP方法。 - * @param headers - 请求头。 - * @param requestBody - 请求体。 - * @param responseBody - 响应体。 - * @returns 响应日志对象。 - */ -const logResponse = ( - response: Response, - method: string, - headers: any, - requestBody: any, - responseBody: any -) => { - const responseLog = { - ok: response.ok, - status: response.status, - statusText: response.statusText, - url: response.url, - method: method, - requestHeaders: headers, - responseHeaders: response.headers, - requestBody, - responseBody, - } - console.log("🚀 ~ responseLog:", JSON.stringify(responseLog, null, 2)) - return responseLog -} - -/** - * 发送网络请求并返回一个解析为响应数据的Promise。 - * @param url - 要发送请求的URL。 - * @param method - 请求使用的HTTP方法。 - * @param queryParams - 要包含在URL中的查询参数。 - * @param payload - 请求的有效负载数据。 - * @param additionalHeaders - 要包含在请求中的附加头。 - * @returns 一个解析为响应数据的Promise。 - * @throws 如果网络响应不成功或存在解析错误,则抛出错误。 - */ -const netTool = async ({ - url, - method, - queryParams, - payload, - additionalHeaders, -}: NetRequestParams): Promise => { - // 拼接完整的URL - let fullUrl = url - if (queryParams) { - if (typeof queryParams === "string") { - fullUrl = `${url}?${queryParams}` - } else { - const queryString = new URLSearchParams(queryParams).toString() - if (queryString) fullUrl = `${url}?${queryString}` - } - } - - // 设置请求头 - const headers = { - "Content-Type": "application/json", - ...additionalHeaders, - } - - // 发送请求 - const res = await fetch(fullUrl, { - method, - body: JSON.stringify(payload), - headers, - }) - // 获取响应数据 - let resData: any = null - let resText: string = "" - - try { - resText = await res.text() - resData = JSON.parse(resText) - } catch { - /* empty */ - } - - // 记录响应 - logResponse(res, method, headers, payload, resData || resText) - if (!res.ok) { - if (resData?.msg) { - throw new Error(resData.msg) - } - if (resText) { - throw new Error(resText) - } - throw new Error("网络响应异常") - } - // http 错误码正常,但解析异常 - if (!resData) { - throw new Error("解析响应数据异常") - } - return resData as T -} - -/** - * 发送GET请求并返回一个解析为响应数据的Promise。 - * - * @param url - 要发送请求的URL。 - * @param queryParams - 要包含在URL中的查询参数。 - * @param additionalHeaders - 要包含在请求中的附加头。 - * @returns 一个解析为响应数据的Promise。 - */ -netTool.get = ( - url: string, - queryParams?: any, - additionalHeaders?: any -): Promise => netTool({ url, method: "get", queryParams, additionalHeaders }) - -/** - * 发送POST请求并返回一个解析为响应数据的Promise。 - * - * @param url - 要发送请求的URL。 - * @param payload - 请求的有效负载数据。 - * @param queryParams - 要包含在URL中的查询参数。 - * @param additionalHeaders - 要包含在请求中的附加头。 - * @returns 一个解析为响应数据的Promise。 - */ -netTool.post = ( - url: string, - payload?: any, - queryParams?: any, - additionalHeaders?: any -): Promise => - netTool({ url, method: "post", payload, queryParams, additionalHeaders }) - -/** - * 发送PUT请求并返回一个解析为响应数据的Promise。 - * - * @param url - 要发送请求的URL。 - * @param payload - 请求的有效负载数据。 - * @param queryParams - 要包含在URL中的查询参数。 - * @param additionalHeaders - 要包含在请求中的附加头。 - * @returns 一个解析为响应数据的Promise。 - */ -netTool.put = ( - url: string, - payload: any, - queryParams?: any, - additionalHeaders?: any -): Promise => - netTool({ url, method: "put", payload, queryParams, additionalHeaders }) - -/** - * 发送DELETE请求并返回一个解析为响应数据的Promise。 - * - * @param url - 要发送请求的URL。 - * @param payload - 请求的有效负载数据。 - * @param queryParams - 要包含在URL中的查询参数。 - * @param additionalHeaders - 要包含在请求中的附加头。 - * @returns 一个解析为响应数据的Promise。 - */ -netTool.del = ( - url: string, - payload: any, - queryParams?: any, - additionalHeaders?: any -): Promise => - netTool({ url, method: "delete", payload, queryParams, additionalHeaders }) - -/** - * 发送PATCH请求并返回一个解析为响应数据的Promise。 - * - * @param url - 要发送请求的URL。 - * @param payload - 请求的有效负载数据。 - * @param queryParams - 要包含在URL中的查询参数。 - * @param additionalHeaders - 要包含在请求中的附加头。 - * @returns 一个解析为响应数据的Promise。 - */ -netTool.patch = ( - url: string, - payload: any, - queryParams?: any, - additionalHeaders?: any -): Promise => - netTool({ url, method: "patch", payload, queryParams, additionalHeaders }) - -/** - * 创建一个表示400 Bad Request的响应对象。 - * - * @param msg - 错误消息。 - * @param requestId - 请求ID。 - * @returns 一个表示400 Bad Request的响应对象。 - */ -netTool.badRequest = (msg: string, requestId?: string) => - Response.json({ code: 400, msg, requestId }, { status: 400 }) - -/** - * 创建一个表示404 Not Found的响应对象。 - * - * @param msg - 错误消息。 - * @param requestId - 请求ID。 - * @returns 一个表示404 Not Found的响应对象。 - */ -netTool.notFound = (msg: string, requestId?: string) => - Response.json({ code: 404, msg, requestId }, { status: 404 }) - -/** - * 创建一个表示500 Internal Server Error的响应对象。 - * - * @param msg - 错误消息。 - * @param data - 错误数据。 - * @param requestId - 请求ID。 - * @returns 一个表示500 Internal Server Error的响应对象。 - */ -netTool.serverError = (msg: string, data?: any, requestId?: string) => - Response.json({ code: 500, msg, data, requestId }, { status: 500 }) - -/** - * 创建一个表示200 OK的响应对象。 - * - * @param data - 响应数据。 - * @param requestId - 请求ID。 - * @returns 一个表示200 OK的响应对象。 - */ -netTool.ok = (data?: any, requestId?: string) => - Response.json({ code: 0, msg: "success", data, requestId }) - -export default netTool diff --git a/types/context.ts b/types/context.ts new file mode 100644 index 0000000..258728e --- /dev/null +++ b/types/context.ts @@ -0,0 +1,17 @@ +import { Logger } from "winston" + +import { AttachService, LarkService } from "../services" +import NetTool from "../utils/netTool" + +export namespace Context { + export interface Data { + req: Request + requestId: string + logger: Logger + genResp: NetTool + body: any + text: string + larkService: LarkService + attachService: AttachService + } +} diff --git a/types/index.ts b/types/index.ts index 277c1b6..e9f3a20 100644 --- a/types/index.ts +++ b/types/index.ts @@ -1,7 +1,8 @@ +import type { Context } from "./context" import type { DB } from "./db" import type { LarkAction } from "./larkAction" import type { LarkEvent } from "./larkEvent" import type { LarkServer } from "./larkServer" import type { MsgProxy } from "./msgProxy" -export { DB, LarkAction, LarkEvent, LarkServer, MsgProxy } +export { Context, DB, LarkAction, LarkEvent, LarkServer, MsgProxy } diff --git a/types/larkServer.ts b/types/larkServer.ts index 5fa5a96..1781d03 100644 --- a/types/larkServer.ts +++ b/types/larkServer.ts @@ -58,10 +58,28 @@ export namespace LarkServer { code: number } + export interface ValueRange { + majorDimension: string // 插入维度 + range: string // 返回数据的范围,为空时表示查询范围没有数据 + revision: number // sheet 的版本号 + values: Array> // 查询得到的值 + } + + export interface SpreadsheetData { + revision: number // sheet 的版本号 + spreadsheetToken: string // spreadsheet 的 token + valueRange: ValueRange // 值与范围 + } + export interface BaseRes { code: number data: any - msg: string + // 在错误处理中msg会被赋值为message + message: string + } + + export interface SpreadsheetRes extends BaseRes { + data: SpreadsheetData } export interface UserSessionRes extends BaseRes { diff --git a/utils/genContext.ts b/utils/genContext.ts new file mode 100644 index 0000000..7997c53 --- /dev/null +++ b/utils/genContext.ts @@ -0,0 +1,42 @@ +import { v4 as uuid } from "uuid" + +import loggerIns from "../log" +import { AttachService, LarkService } from "../services" +import { Context } from "../types" +import NetTool from "./netTool" + +/** + * 生成请求上下文。 + * + * @param {Request} req - 请求对象。 + * @returns {Promise} 返回包含请求上下文的对象。 + */ +const genContext = async (req: Request) => { + const requestId = uuid() + const logger = loggerIns.child({ requestId }) + const genResp = new NetTool({ requestId }) + const larkService = new LarkService("egg", requestId) + const attachService = new AttachService({ requestId }) + + let body: any = null + let text: string = "" + try { + text = await req.text() + body = JSON.parse(text) + } catch { + /* empty */ + } + logger.debug(`req body: ${text}`) + return { + req, + requestId, + logger, + genResp, + body, + text, + larkService, + attachService, + } as Context.Data +} + +export default genContext diff --git a/utils/netTool.ts b/utils/netTool.ts new file mode 100644 index 0000000..cc044fc --- /dev/null +++ b/utils/netTool.ts @@ -0,0 +1,424 @@ +import { Logger } from "winston" + +import loggerIns from "../log" + +interface NetRequestParams { + url: string + method: string + queryParams?: any + payload?: any + additionalHeaders?: any +} + +interface NetErrorDetail { + httpStatus: number + code: number + message: string +} + +export class NetError extends Error { + public code: number + public message: string + public httpStatus: number + + constructor({ code, message, httpStatus }: NetErrorDetail) { + super(message) + this.code = code + this.message = message + this.httpStatus = httpStatus + } +} + +/** + * 网络工具类,提供发送HTTP请求的方法。 + */ +class NetToolBase { + protected prefix: string + protected headers: any + protected getHeaders: () => any + protected logger: Logger + protected requestId: string + + /** + * 创建一个网络工具类实例。 + * + * @param {Object} params - 构造函数参数。 + * @param {string} [params.prefix] - URL前缀。 + * @param {any} [params.headers] - 默认请求头。 + * @param {Function} [params.getHeaders] - 获取请求头的方法。 + * @param {string} [params.requestId] - 请求ID。 + */ + constructor({ + prefix, + headers, + getHeaders, + requestId, + }: { + prefix?: string + headers?: any + getHeaders?: () => any + requestId?: string + } = {}) { + this.prefix = prefix || "" + this.headers = headers || {} + this.getHeaders = getHeaders || (() => ({})) + this.requestId = requestId || "" + this.logger = loggerIns.child({ requestId }) + } + + /** + * 记录响应详情并返回响应日志对象。 + * @param response - 响应对象。 + * @param method - 请求使用的HTTP方法。 + * @param headers - 请求头。 + * @param requestBody - 请求体。 + * @param responseBody - 响应体。 + * @returns 响应日志对象。 + */ + private logResponse( + response: Response, + method: string, + headers: any, + requestBody: any, + responseBody: any + ) { + const responseLog = { + ok: response.ok, + status: response.status, + statusText: response.statusText, + url: response.url, + method: method, + requestHeaders: headers, + responseHeaders: response.headers, + requestBody, + responseBody, + } + this.logger.http(JSON.stringify(responseLog, null, 2)) + return responseLog + } + + /** + * 发送网络请求并返回一个解析为响应数据的Promise。 + * @param url - 要发送请求的URL。 + * @param method - 请求使用的HTTP方法。 + * @param queryParams - 要包含在URL中的查询参数。 + * @param payload - 请求的有效负载数据。 + * @param additionalHeaders - 要包含在请求中的附加头。 + * @returns 一个解析为响应数据的Promise。 + * @throws 如果网络响应不成功或存在解析错误,则抛出错误。 + */ + protected async request({ + url, + method, + queryParams, + payload, + additionalHeaders, + }: NetRequestParams): Promise { + // 拼接完整的URL + let fullUrl = `${this.prefix}${url}` + if (queryParams) { + if (typeof queryParams === "string") { + fullUrl = `${fullUrl}?${queryParams}` + } else { + const queryString = new URLSearchParams(queryParams).toString() + if (queryString) fullUrl = `${fullUrl}?${queryString}` + } + } + + // 设置请求头 + const headers = { + ...this.headers, + ...(await this.getHeaders()), + ...additionalHeaders, + } + // 设置请求Header + if (!(payload instanceof FormData)) { + headers["Content-Type"] = "application/json" + } + + // 处理请求数据 + const body = payload instanceof FormData ? payload : JSON.stringify(payload) + + // 发送请求 + const res = await fetch(fullUrl, { + method, + body, + headers, + }) + // 获取响应数据 + let resData: any = null + let resText: string = "" + + try { + resText = await res.text() + resData = JSON.parse(resText) + } catch { + /* empty */ + } + + // 记录响应 + this.logResponse(res, method, headers, payload, resData || resText) + if (!res.ok) { + if (resData?.message || resData?.msg) { + throw new NetError({ + httpStatus: res.status, + code: resData?.code, + message: resData?.message || resData?.msg, + }) + } + throw new NetError({ + httpStatus: res.status, + code: res.status, + message: resText || "网络响应异常", + }) + } + // http 错误码正常,但解析异常 + if (!resData) { + throw new NetError({ + httpStatus: res.status, + code: 1, + message: "解析响应数据异常", + }) + } + // 响应数据异常 + if ("code" in resData && resData.code !== 0) { + throw new NetError({ + httpStatus: res.status, + code: resData.code, + message: resData.message || resData.msg || "网络请求失败", + }) + } + return resData as T + } + + /** + * 发送GET请求并返回一个解析为响应数据的Promise。 + * + * @param url - 要发送请求的URL。 + * @param queryParams - 要包含在URL中的查询参数。 + * @param additionalHeaders - 要包含在请求中的附加头。 + * @returns 一个解析为响应数据的Promise。 + */ + protected get( + url: string, + queryParams?: any, + additionalHeaders?: any + ): Promise { + return this.request({ url, method: "get", queryParams, additionalHeaders }) + } + + /** + * 发送POST请求并返回一个解析为响应数据的Promise。 + * + * @param url - 要发送请求的URL。 + * @param payload - 请求的有效负载数据。 + * @param queryParams - 要包含在URL中的查询参数。 + * @param additionalHeaders - 要包含在请求中的附加头。 + * @returns 一个解析为响应数据的Promise。 + */ + protected post( + url: string, + payload?: any, + queryParams?: any, + additionalHeaders?: any + ): Promise { + return this.request({ + url, + method: "post", + payload, + queryParams, + additionalHeaders, + }) + } + + /** + * 发送PUT请求并返回一个解析为响应数据的Promise。 + * + * @param url - 要发送请求的URL。 + * @param payload - 请求的有效负载数据。 + * @param queryParams - 要包含在URL中的查询参数。 + * @param additionalHeaders - 要包含在请求中的附加头。 + * @returns 一个解析为响应数据的Promise。 + */ + protected put( + url: string, + payload: any, + queryParams?: any, + additionalHeaders?: any + ): Promise { + return this.request({ + url, + method: "put", + payload, + queryParams, + additionalHeaders, + }) + } + + /** + * 发送DELETE请求并返回一个解析为响应数据的Promise。 + * + * @param url - 要发送请求的URL。 + * @param payload - 请求的有效负载数据。 + * @param queryParams - 要包含在URL中的查询参数。 + * @param additionalHeaders - 要包含在请求中的附加头。 + * @returns 一个解析为响应数据的Promise。 + */ + protected del( + url: string, + payload: any, + queryParams?: any, + additionalHeaders?: any + ): Promise { + return this.request({ + url, + method: "delete", + payload, + queryParams, + additionalHeaders, + }) + } + + /** + * 发送PATCH请求并返回一个解析为响应数据的Promise。 + * + * @param url - 要发送请求的URL。 + * @param payload - 请求的有效负载数据。 + * @param queryParams - 要包含在URL中的查询参数。 + * @param additionalHeaders - 要包含在请求中的附加头。 + * @returns 一个解析为响应数据的Promise。 + */ + protected patch( + url: string, + payload: any, + queryParams?: any, + additionalHeaders?: any + ): Promise { + return this.request({ + url, + method: "patch", + payload, + queryParams, + additionalHeaders, + }) + } +} + +class NetTool extends NetToolBase { + public request({ + url, + method, + queryParams, + payload, + additionalHeaders, + }: NetRequestParams): Promise { + return super.request({ + url, + method, + queryParams, + payload, + additionalHeaders, + }) + } + public get( + url: string, + queryParams?: any, + additionalHeaders?: any + ): Promise { + return super.get(url, queryParams, additionalHeaders) + } + public post( + url: string, + payload?: any, + queryParams?: any, + additionalHeaders?: any + ): Promise { + return super.post(url, payload, queryParams, additionalHeaders) + } + public put( + url: string, + payload: any, + queryParams?: any, + additionalHeaders?: any + ): Promise { + return super.put(url, payload, queryParams, additionalHeaders) + } + public del( + url: string, + payload: any, + queryParams?: any, + additionalHeaders?: any + ): Promise { + return super.del(url, payload, queryParams, additionalHeaders) + } + public patch( + url: string, + payload: any, + queryParams?: any, + additionalHeaders?: any + ): Promise { + return super.patch(url, payload, queryParams, additionalHeaders) + } + /** + * 创建一个表示400 Bad Request的响应对象。 + * + * @param message - 错误消息。 + * @returns 一个表示400 Bad Request的响应对象。 + */ + badRequest(message: string) { + this.logger.error(`return a bad request response: ${message}`) + return Response.json( + { code: 400, message, requestId: this.requestId }, + { status: 400 } + ) + } + + /** + * 创建一个表示404 Not Found的响应对象。 + * + * @param message - 错误消息。 + * @returns 一个表示404 Not Found的响应对象。 + */ + notFound(message: string) { + this.logger.error(`return a not found response: ${message}`) + return Response.json( + { code: 404, message, requestId: this.requestId }, + { status: 404 } + ) + } + + /** + * 创建一个表示500 Internal Server Error的响应对象。 + * + * @param message - 错误消息。 + * @param data - 错误数据。 + * @returns 一个表示500 Internal Server Error的响应对象。 + */ + serverError(message: string, data?: any) { + this.logger.error(`return a server error response: ${message}`) + return Response.json( + { code: 500, message, data, requestId: this.requestId }, + { status: 500 } + ) + } + + /** + * 创建一个表示200 OK的响应对象。 + * + * @param data - 响应数据。 + * @returns 一个表示200 OK的响应对象。 + */ + ok(data?: any) { + this.logger.info(`return a ok response: ${JSON.stringify(data)}`) + return Response.json({ + code: 0, + message: "success", + data, + requestId: this.requestId, + }) + } +} + +export { NetToolBase } + +export default NetTool diff --git a/utils/pathTools.ts b/utils/pathTools.ts index 683ff15..8861f8a 100644 --- a/utils/pathTools.ts +++ b/utils/pathTools.ts @@ -1,3 +1,61 @@ +/** + * 创建一个路径检查工具,用于精确匹配和前缀匹配路径。 + * @param {string} url - 要检查的基础 URL。 + * @param {string} [prefix] - 可选的路径前缀。 + * @returns {object} 包含路径检查方法的对象。 + */ +export const makeCheckPathTool = (url: string, prefix?: string) => { + const { pathname } = new URL(url) + const makePath = (path: string) => `${prefix || ""}${path}` + return { + /** + * 检查路径是否与基础 URL 的路径精确匹配。 + * @param {string} path - 要检查的路径。 + * @returns {boolean} 如果路径精确匹配则返回 true,否则返回 false。 + */ + exactCheck: (path: string) => { + return pathname === makePath(path) + }, + /** + * 检查路径是否以基础 URL 的路径为前缀。 + * @param {string} path - 要检查的路径。 + * @returns {boolean} 如果路径以基础 URL 的路径为前缀则返回 true,否则返回 false。 + */ + startsWithCheck: (path: string) => pathname.startsWith(makePath(path)), + /** + * 检查完整路径是否与基础 URL 的路径精确匹配。 + * @param {string} path - 要检查的路径。 + * @returns {boolean} 如果完整路径与基础 URL 的路径精确匹配则返回 true,否则返回 false。 + */ + fullCheck: (path: string) => pathname === path, + } +} + +/** + * 裁剪路径字符串,如果路径长度超过20个字符,则只保留最后两级目录。 + * + * @param {string} path - 要处理的路径字符串。 + * @returns {string} - 裁剪后的路径字符串,如果长度不超过20个字符则返回原路径。 + */ +export const shortenPath = (path: string): string => { + if (path.length <= 20) { + return path + } + + const parts = path.split("/") + if (parts.length <= 2) { + return path + } + + return `.../${parts[parts.length - 2]}/${parts[parts.length - 1]}` +} + +/** + * 安全地将对象转换为 JSON 字符串。 + * 如果转换失败,则返回对象的字符串表示。 + * @param {any} obj - 要转换的对象。 + * @returns {string} - JSON 字符串或对象的字符串表示。 + */ export const safeJsonStringify = (obj: any) => { try { return JSON.stringify(obj) @@ -5,16 +63,3 @@ export const safeJsonStringify = (obj: any) => { return String(obj) } } - -export const makeCheckPathTool = (url: string, prefix?: string) => { - const { pathname } = new URL(url) - const makePath = (path: string) => `${prefix || ""}${path}` - return { - // 精确匹配 - exactCheck: (path: string) => { - return pathname === makePath(path) - }, - // 前缀匹配 - startsWithCheck: (path: string) => pathname.startsWith(makePath(path)), - } -} diff --git a/utils/pbTools.ts b/utils/pbTools.ts index 58fd310..7f24592 100644 --- a/utils/pbTools.ts +++ b/utils/pbTools.ts @@ -16,7 +16,6 @@ export const managePbError = async ( try { return await dbFunc() } catch (err: any) { - console.log("🚀 ~ managePbError ~ err:", err) return null } }