feat: 抽象网络请求类 & 内容转为ctx向内传递
All checks were successful
Egg Server MIflow / build-image (push) Successful in 1m5s
All checks were successful
Egg Server MIflow / build-image (push) Successful in 1m5s
This commit is contained in:
parent
b9045bcfa1
commit
09e352a9c1
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -9,6 +9,8 @@
|
||||
"eamodio",
|
||||
"esbenp",
|
||||
"Gruntfuggly",
|
||||
"metas",
|
||||
"mina",
|
||||
"tseslint",
|
||||
"wlpbbgiky",
|
||||
"Yoav"
|
||||
|
@ -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}`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
28
index.ts
28
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}`)
|
||||
|
55
log/index.ts
Normal file
55
log/index.ts
Normal file
@ -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
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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<string>} 返回包含ChatId卡片的JSON字符串
|
||||
*/
|
||||
const makeChatIdCard = async (body: LarkAction.Data) => {
|
||||
const makeChatIdCard = async (body: LarkAction.Data): Promise<string> => {
|
||||
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<void>} 无返回值
|
||||
*/
|
||||
const manageBtnClick = async (body: LarkAction.Data) => {
|
||||
const manageBtnClick = async ({
|
||||
body,
|
||||
larkService,
|
||||
logger,
|
||||
}: Context.Data): Promise<void> => {
|
||||
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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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<Response>} 返回响应对象
|
||||
*/
|
||||
export const manageBotReq = async (ctx: Context.Data): Promise<Response> => {
|
||||
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()
|
||||
}
|
||||
|
@ -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<Response>} 返回响应对象
|
||||
*/
|
||||
export const manageMessageReq = async (
|
||||
ctx: Context.Data
|
||||
): Promise<Response> => {
|
||||
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<string, any>,
|
||||
open_id: {} as Record<string, any>,
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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 | Response>} 如果校验失败,返回响应对象;否则返回 false
|
||||
*/
|
||||
const validateSheetReq = async (
|
||||
ctx: Context.Data
|
||||
): Promise<false | Response> => {
|
||||
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<Response>} 返回响应对象
|
||||
*/
|
||||
export const manageSheetReq = async (ctx: Context.Data): Promise<Response> => {
|
||||
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()
|
||||
}
|
||||
|
@ -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}`)
|
||||
}
|
||||
}
|
||||
|
@ -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<string>} 返回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<string>} 返回报告收集结果。
|
||||
*/
|
||||
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
|
||||
|
@ -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 }
|
||||
|
15
services/lark/auth.ts
Normal file
15
services/lark/auth.ts
Normal file
@ -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
|
28
services/lark/base.ts
Normal file
28
services/lark/base.ts
Normal file
@ -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<T = any>(params: any): Promise<T> {
|
||||
return super.request<T>(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
|
@ -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)<LarkServer.BatchDocMetaRes>(
|
||||
URL,
|
||||
data,
|
||||
{
|
||||
user_id_type,
|
||||
}
|
||||
)
|
||||
return this.post<LarkServer.BatchDocMetaRes>(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
|
||||
|
@ -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
|
||||
|
@ -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 <T = LarkServer.BaseRes>({
|
||||
url,
|
||||
method,
|
||||
queryParams,
|
||||
payload,
|
||||
additionalHeaders,
|
||||
appName = "egg",
|
||||
}: {
|
||||
url: string
|
||||
method: string
|
||||
queryParams?: any
|
||||
payload?: any
|
||||
additionalHeaders?: any
|
||||
appName?: string
|
||||
}): Promise<T> => {
|
||||
const headersWithAuth = {
|
||||
Authorization: `Bearer ${await db.tenantAccessToken.get(appName)}`,
|
||||
...additionalHeaders,
|
||||
}
|
||||
return netTool<T>({
|
||||
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) =>
|
||||
<T = LarkServer.BaseRes>(
|
||||
url: string,
|
||||
queryParams?: any,
|
||||
additionalHeaders?: any
|
||||
): Promise<T> =>
|
||||
larkNetTool({
|
||||
url,
|
||||
method: "get",
|
||||
queryParams,
|
||||
additionalHeaders,
|
||||
appName,
|
||||
})
|
||||
|
||||
/**
|
||||
* 发送POST请求并返回一个解析为响应数据的Promise。
|
||||
*
|
||||
* @param appName - 应用名称,用于获取授权令牌。
|
||||
* @returns 一个函数,该函数接受URL、有效负载、查询参数和附加头,并返回一个解析为响应数据的Promise。
|
||||
*/
|
||||
larkNetTool.post =
|
||||
(appName?: string) =>
|
||||
<T = LarkServer.BaseRes>(
|
||||
url: string,
|
||||
payload?: any,
|
||||
queryParams?: any,
|
||||
additionalHeaders?: any
|
||||
): Promise<T> =>
|
||||
larkNetTool({
|
||||
url,
|
||||
method: "post",
|
||||
payload,
|
||||
queryParams,
|
||||
additionalHeaders,
|
||||
appName,
|
||||
})
|
||||
|
||||
/**
|
||||
* 发送DELETE请求并返回一个解析为响应数据的Promise。
|
||||
*
|
||||
* @param appName - 应用名称,用于获取授权令牌。
|
||||
* @returns 一个函数,该函数接受URL、有效负载和附加头,并返回一个解析为响应数据的Promise。
|
||||
*/
|
||||
larkNetTool.del =
|
||||
(appName?: string) =>
|
||||
<T = LarkServer.BaseRes>(
|
||||
url: string,
|
||||
payload: any,
|
||||
additionalHeaders?: any
|
||||
): Promise<T> =>
|
||||
larkNetTool({ url, method: "delete", payload, additionalHeaders, appName })
|
||||
|
||||
/**
|
||||
* 发送PATCH请求并返回一个解析为响应数据的Promise。
|
||||
*
|
||||
* @param appName - 应用名称,用于获取授权令牌。
|
||||
* @returns 一个函数,该函数接受URL、有效负载和附加头,并返回一个解析为响应数据的Promise。
|
||||
*/
|
||||
larkNetTool.patch =
|
||||
(appName?: string) =>
|
||||
<T = LarkServer.BaseRes>(
|
||||
url: string,
|
||||
payload: any,
|
||||
additionalHeaders?: any
|
||||
): Promise<T> =>
|
||||
larkNetTool({ url, method: "patch", payload, additionalHeaders, appName })
|
||||
|
||||
export default larkNetTool
|
@ -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)<LarkServer.BaseRes>(URL, {
|
||||
return this.post<LarkServer.BaseRes>(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)<LarkServer.BaseRes>(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<LarkServer.BaseRes>(path, { content })
|
||||
}
|
||||
|
||||
const message = {
|
||||
send,
|
||||
update,
|
||||
}
|
||||
|
||||
export default message
|
||||
export default LarkMessageService
|
||||
|
@ -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)<LarkServer.BaseRes>(URL, {
|
||||
class LarkSheetService extends LarkBaseService {
|
||||
/**
|
||||
* 向电子表格中插入行。
|
||||
* @param {string} sheetToken - 表格令牌。
|
||||
* @param {string} range - 插入数据的范围。
|
||||
* @param {string[][]} values - 要插入的值。
|
||||
* @returns {Promise<LarkServer.BaseRes>} 返回一个包含响应数据的Promise。
|
||||
*/
|
||||
async insertRows(sheetToken: string, range: string, values: string[][]) {
|
||||
const path = `/sheets/v2/spreadsheets/${sheetToken}/values_append?insertDataOption=INSERT_ROWS`
|
||||
return this.post<LarkServer.BaseRes>(path, {
|
||||
valueRange: {
|
||||
range,
|
||||
values,
|
||||
@ -18,8 +19,16 @@ const insertRows =
|
||||
})
|
||||
}
|
||||
|
||||
const sheet = {
|
||||
insertRows,
|
||||
/**
|
||||
* 获取指定范围内的电子表格数据。
|
||||
* @param {string} sheetToken - 表格令牌。
|
||||
* @param {string} range - 要获取数据的范围。
|
||||
* @returns {Promise<LarkServer.SpreadsheetRes>} 返回一个包含响应数据的Promise。
|
||||
*/
|
||||
async getRange(sheetToken: string, range: string) {
|
||||
const path = `/sheets/v2/spreadsheets/${sheetToken}/values/${range}?valueRenderOption=ToString`
|
||||
return this.get<LarkServer.SpreadsheetRes>(path)
|
||||
}
|
||||
}
|
||||
|
||||
export default sheet
|
||||
export default LarkSheetService
|
||||
|
@ -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)<LarkServer.UserSessionRes>(URL, { code })
|
||||
}
|
||||
class LarkUserService extends LarkBaseService {
|
||||
/**
|
||||
* 登录凭证校验
|
||||
* @param {string} code 登录凭证
|
||||
* @returns
|
||||
*/
|
||||
async code2Login(code: string) {
|
||||
const path = `/mina/v2/tokenLoginValidate`
|
||||
return this.post<LarkServer.UserSessionRes>(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)<LarkServer.UserInfoRes>(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<LarkServer.UserInfoRes>(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)<LarkServer.BatchUserInfoRes>(
|
||||
URL,
|
||||
getParams
|
||||
)
|
||||
return this.get<LarkServer.BatchUserInfoRes>(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
|
||||
|
@ -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 <T = any>({
|
||||
url,
|
||||
method,
|
||||
queryParams,
|
||||
payload,
|
||||
additionalHeaders,
|
||||
}: NetRequestParams): Promise<T> => {
|
||||
// 拼接完整的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 = <T = any>(
|
||||
url: string,
|
||||
queryParams?: any,
|
||||
additionalHeaders?: any
|
||||
): Promise<T> => netTool({ url, method: "get", queryParams, additionalHeaders })
|
||||
|
||||
/**
|
||||
* 发送POST请求并返回一个解析为响应数据的Promise。
|
||||
*
|
||||
* @param url - 要发送请求的URL。
|
||||
* @param payload - 请求的有效负载数据。
|
||||
* @param queryParams - 要包含在URL中的查询参数。
|
||||
* @param additionalHeaders - 要包含在请求中的附加头。
|
||||
* @returns 一个解析为响应数据的Promise。
|
||||
*/
|
||||
netTool.post = <T = any>(
|
||||
url: string,
|
||||
payload?: any,
|
||||
queryParams?: any,
|
||||
additionalHeaders?: any
|
||||
): Promise<T> =>
|
||||
netTool({ url, method: "post", payload, queryParams, additionalHeaders })
|
||||
|
||||
/**
|
||||
* 发送PUT请求并返回一个解析为响应数据的Promise。
|
||||
*
|
||||
* @param url - 要发送请求的URL。
|
||||
* @param payload - 请求的有效负载数据。
|
||||
* @param queryParams - 要包含在URL中的查询参数。
|
||||
* @param additionalHeaders - 要包含在请求中的附加头。
|
||||
* @returns 一个解析为响应数据的Promise。
|
||||
*/
|
||||
netTool.put = <T = any>(
|
||||
url: string,
|
||||
payload: any,
|
||||
queryParams?: any,
|
||||
additionalHeaders?: any
|
||||
): Promise<T> =>
|
||||
netTool({ url, method: "put", payload, queryParams, additionalHeaders })
|
||||
|
||||
/**
|
||||
* 发送DELETE请求并返回一个解析为响应数据的Promise。
|
||||
*
|
||||
* @param url - 要发送请求的URL。
|
||||
* @param payload - 请求的有效负载数据。
|
||||
* @param queryParams - 要包含在URL中的查询参数。
|
||||
* @param additionalHeaders - 要包含在请求中的附加头。
|
||||
* @returns 一个解析为响应数据的Promise。
|
||||
*/
|
||||
netTool.del = <T = any>(
|
||||
url: string,
|
||||
payload: any,
|
||||
queryParams?: any,
|
||||
additionalHeaders?: any
|
||||
): Promise<T> =>
|
||||
netTool({ url, method: "delete", payload, queryParams, additionalHeaders })
|
||||
|
||||
/**
|
||||
* 发送PATCH请求并返回一个解析为响应数据的Promise。
|
||||
*
|
||||
* @param url - 要发送请求的URL。
|
||||
* @param payload - 请求的有效负载数据。
|
||||
* @param queryParams - 要包含在URL中的查询参数。
|
||||
* @param additionalHeaders - 要包含在请求中的附加头。
|
||||
* @returns 一个解析为响应数据的Promise。
|
||||
*/
|
||||
netTool.patch = <T = any>(
|
||||
url: string,
|
||||
payload: any,
|
||||
queryParams?: any,
|
||||
additionalHeaders?: any
|
||||
): Promise<T> =>
|
||||
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
|
17
types/context.ts
Normal file
17
types/context.ts
Normal file
@ -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
|
||||
}
|
||||
}
|
@ -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 }
|
||||
|
@ -58,10 +58,28 @@ export namespace LarkServer {
|
||||
code: number
|
||||
}
|
||||
|
||||
export interface ValueRange {
|
||||
majorDimension: string // 插入维度
|
||||
range: string // 返回数据的范围,为空时表示查询范围没有数据
|
||||
revision: number // sheet 的版本号
|
||||
values: Array<Array<any>> // 查询得到的值
|
||||
}
|
||||
|
||||
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 {
|
||||
|
42
utils/genContext.ts
Normal file
42
utils/genContext.ts
Normal file
@ -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<Context.Data>} 返回包含请求上下文的对象。
|
||||
*/
|
||||
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
|
424
utils/netTool.ts
Normal file
424
utils/netTool.ts
Normal file
@ -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<T = any>({
|
||||
url,
|
||||
method,
|
||||
queryParams,
|
||||
payload,
|
||||
additionalHeaders,
|
||||
}: NetRequestParams): Promise<T> {
|
||||
// 拼接完整的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<T = any>(
|
||||
url: string,
|
||||
queryParams?: any,
|
||||
additionalHeaders?: any
|
||||
): Promise<T> {
|
||||
return this.request({ url, method: "get", queryParams, additionalHeaders })
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送POST请求并返回一个解析为响应数据的Promise。
|
||||
*
|
||||
* @param url - 要发送请求的URL。
|
||||
* @param payload - 请求的有效负载数据。
|
||||
* @param queryParams - 要包含在URL中的查询参数。
|
||||
* @param additionalHeaders - 要包含在请求中的附加头。
|
||||
* @returns 一个解析为响应数据的Promise。
|
||||
*/
|
||||
protected post<T = any>(
|
||||
url: string,
|
||||
payload?: any,
|
||||
queryParams?: any,
|
||||
additionalHeaders?: any
|
||||
): Promise<T> {
|
||||
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<T = any>(
|
||||
url: string,
|
||||
payload: any,
|
||||
queryParams?: any,
|
||||
additionalHeaders?: any
|
||||
): Promise<T> {
|
||||
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<T = any>(
|
||||
url: string,
|
||||
payload: any,
|
||||
queryParams?: any,
|
||||
additionalHeaders?: any
|
||||
): Promise<T> {
|
||||
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<T = any>(
|
||||
url: string,
|
||||
payload: any,
|
||||
queryParams?: any,
|
||||
additionalHeaders?: any
|
||||
): Promise<T> {
|
||||
return this.request({
|
||||
url,
|
||||
method: "patch",
|
||||
payload,
|
||||
queryParams,
|
||||
additionalHeaders,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class NetTool extends NetToolBase {
|
||||
public request<T = any>({
|
||||
url,
|
||||
method,
|
||||
queryParams,
|
||||
payload,
|
||||
additionalHeaders,
|
||||
}: NetRequestParams): Promise<T> {
|
||||
return super.request<T>({
|
||||
url,
|
||||
method,
|
||||
queryParams,
|
||||
payload,
|
||||
additionalHeaders,
|
||||
})
|
||||
}
|
||||
public get<T = any>(
|
||||
url: string,
|
||||
queryParams?: any,
|
||||
additionalHeaders?: any
|
||||
): Promise<T> {
|
||||
return super.get<T>(url, queryParams, additionalHeaders)
|
||||
}
|
||||
public post<T = any>(
|
||||
url: string,
|
||||
payload?: any,
|
||||
queryParams?: any,
|
||||
additionalHeaders?: any
|
||||
): Promise<T> {
|
||||
return super.post<T>(url, payload, queryParams, additionalHeaders)
|
||||
}
|
||||
public put<T = any>(
|
||||
url: string,
|
||||
payload: any,
|
||||
queryParams?: any,
|
||||
additionalHeaders?: any
|
||||
): Promise<T> {
|
||||
return super.put<T>(url, payload, queryParams, additionalHeaders)
|
||||
}
|
||||
public del<T = any>(
|
||||
url: string,
|
||||
payload: any,
|
||||
queryParams?: any,
|
||||
additionalHeaders?: any
|
||||
): Promise<T> {
|
||||
return super.del<T>(url, payload, queryParams, additionalHeaders)
|
||||
}
|
||||
public patch<T = any>(
|
||||
url: string,
|
||||
payload: any,
|
||||
queryParams?: any,
|
||||
additionalHeaders?: any
|
||||
): Promise<T> {
|
||||
return super.patch<T>(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
|
@ -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)),
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ export const managePbError = async <T>(
|
||||
try {
|
||||
return await dbFunc()
|
||||
} catch (err: any) {
|
||||
console.log("🚀 ~ managePbError ~ err:", err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user