feat: 抽象网络请求类 & 内容转为ctx向内传递
All checks were successful
Egg Server MIflow / build-image (push) Successful in 1m5s

This commit is contained in:
zhaoyingbo 2024-08-16 09:12:11 +00:00
parent b9045bcfa1
commit 09e352a9c1
31 changed files with 1064 additions and 700 deletions

View File

@ -9,6 +9,8 @@
"eamodio",
"esbenp",
"Gruntfuggly",
"metas",
"mina",
"tseslint",
"wlpbbgiky",
"Yoav"

BIN
bun.lockb

Binary file not shown.

View File

@ -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}`)
}
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 URLPromise
*/
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 URLPromise
*/
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 URLPromise
*/
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 URLPromise
*/
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

View File

@ -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 IDID类型应与查询参数receive_id_type
* @param {MsgType} msg_type textpostimagefileaudiomediastickerinteractiveshare_chatshare_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 IDID类型应与查询参数receive_id_type
* @param {MsgType} msg_type textpostimagefileaudiomediastickerinteractiveshare_chatshare_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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,6 @@ export const managePbError = async <T>(
try {
return await dbFunc()
} catch (err: any) {
console.log("🚀 ~ managePbError ~ err:", err)
return null
}
}