diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 041d7bb..f8bb848 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,9 +7,10 @@ "customizations": { "vscode": { "settings": { - "files.autoSave": "off", + "files.autoSave": "afterDelay", "editor.guides.bracketPairs": true, - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true }, "extensions": [ "dbaeumer.vscode-eslint", diff --git a/.gitea/workflows/cicd.yaml b/.gitea/workflows/cicd.yaml index aea4444..add2521 100644 --- a/.gitea/workflows/cicd.yaml +++ b/.gitea/workflows/cicd.yaml @@ -1,7 +1,7 @@ name: Egg CI/CD on: [push] -jobs: +jobs: build-image: runs-on: mi-server container: catthehacker/ubuntu:act-latest @@ -22,7 +22,8 @@ jobs: uses: docker/build-push-action@v4 with: push: true - tags: git.yingbo.im:333/zhaoyingbo/egg_server:latest + tags: git.yingbo.im:333/zhaoyingbo/egg_server:${{ github.sha }} + deploy: needs: build-image runs-on: mi-server @@ -40,7 +41,7 @@ jobs: key: ${{ secrets.SERVER_KEY }} port: ${{ secrets.SERVER_PORT }} source: docker-compose.yml - target: /home/yingbo/docker/egg_server + target: /home/deploy/docker/egg_server # 登录服务器,执行docker-compose命令 - name: Login to the server and execute docker-compose command uses: appleboy/ssh-action@master @@ -51,6 +52,6 @@ jobs: port: ${{ secrets.SERVER_PORT }} script: | docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} git.yingbo.im:333 - docker compose -f /home/yingbo/docker/egg_server/docker-compose.yml down - docker compose -f /home/yingbo/docker/egg_server/docker-compose.yml pull - docker compose -f /home/yingbo/docker/egg_server/docker-compose.yml up -d + cd /home/deploy/docker/egg_server + sed -i "s/sha/${{ github.sha }}/g" docker-compose.yml + docker-compose up -d --force-recreate --no-deps egg_server diff --git a/bun.lockb b/bun.lockb index 5e250ba..e9c8673 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/db/index.ts b/db/index.ts new file mode 100644 index 0000000..d8078b4 --- /dev/null +++ b/db/index.ts @@ -0,0 +1,9 @@ +import messageGroup from "./messageGroup"; +import tenantAccessToken from "./tenantAccessToken"; + +const db = { + messageGroup, + tenantAccessToken, +}; + +export default db; diff --git a/db/messageGroup/index.ts b/db/messageGroup/index.ts new file mode 100644 index 0000000..c0e910f --- /dev/null +++ b/db/messageGroup/index.ts @@ -0,0 +1,13 @@ +import { managePb404 } from "../../utils/pbTools"; +import pbClient from "../pbClient"; + +const getOne = (groupId: string) => + managePb404( + async () => await pbClient.collection("message_group").getOne(groupId) + ); + +const messageGroup = { + getOne, +}; + +export default messageGroup; diff --git a/db/messageGroup/typings.d.ts b/db/messageGroup/typings.d.ts new file mode 100644 index 0000000..eaecbe5 --- /dev/null +++ b/db/messageGroup/typings.d.ts @@ -0,0 +1,14 @@ +interface PBMessageGroup { + collectionId: string; + collectionName: string; + updated: string; + created: string; + desc: string; + id: string; + name: string; + email?: string[]; + chat_id?: string[]; + open_id?: string[]; + union_id?: string[]; + user_id?: string[]; +} diff --git a/db/pbClient.ts b/db/pbClient.ts new file mode 100644 index 0000000..4d7da78 --- /dev/null +++ b/db/pbClient.ts @@ -0,0 +1,5 @@ +import PocketBase from 'pocketbase'; + +const pbClient = new PocketBase('https://eggpb.imoaix.cn') + +export default pbClient; diff --git a/db/tenantAccessToken/index.ts b/db/tenantAccessToken/index.ts new file mode 100644 index 0000000..342f327 --- /dev/null +++ b/db/tenantAccessToken/index.ts @@ -0,0 +1,28 @@ +import pbClient from "../pbClient"; + +/** + * 更新租户的token + * @param {string} value 新的token + */ +const update = async (value: string) => { + await pbClient.collection("config").update("ugel8f0cpk0rut6", { value }); + console.log("reset access token success", value); +}; + +/** + * 获取租户的token + * @returns {string} 租户的token + */ +const get = async () => { + const { value } = await pbClient + .collection("config") + .getOne("ugel8f0cpk0rut6"); + return value as string; +}; + +const tenantAccessToken = { + update, + get, +}; + +export default tenantAccessToken; diff --git a/docker-compose.yml b/docker-compose.yml index 3c484ab..818f2de 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,9 @@ version: "3" services: - server: - image: git.yingbo.im:333/zhaoyingbo/egg_server:latest + egg_server: + image: git.yingbo.im:333/zhaoyingbo/egg_server:sha container_name: egg_server restart: always ports: - - 3000:3000 \ No newline at end of file + - 3003:3000 diff --git a/index.ts b/index.ts index ea12ce2..a81b93a 100644 --- a/index.ts +++ b/index.ts @@ -1,4 +1,8 @@ import { manageBotReq } from "./routes/bot"; +import { manageMessageReq } from "./routes/message"; +import { initSchedule } from "./schedule"; + +initSchedule() Bun.serve({ async fetch(req) { @@ -7,7 +11,10 @@ Bun.serve({ if (url.pathname === "/") return new Response("hello, glade to see you!"); // 机器人 if (url.pathname === '/bot') return await manageBotReq(req); - return Response.json({a: 'b'}); + // 消息发送 + if (url.pathname === '/message') return await manageMessageReq(req); + // 其他 + return new Response('OK') }, port: 3000 }); \ No newline at end of file diff --git a/package.json b/package.json index 83bba9a..bb54114 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "typescript": "^5.0.0" }, "dependencies": { + "@types/node-schedule": "^2.1.6", + "node-schedule": "^2.1.1", "pocketbase": "^0.21.1" } } \ No newline at end of file diff --git a/routes/bot/eventMsg.ts b/routes/bot/eventMsg.ts new file mode 100644 index 0000000..48e4b99 --- /dev/null +++ b/routes/bot/eventMsg.ts @@ -0,0 +1,108 @@ +import { sendMsg } from "../../utils/sendMsg"; + +/** + * 是否为事件消息 + * @param {LarkMessageEvent} body + */ +const isEventMsg = (body: LarkMessageEvent) => { + return body?.header?.event_type === "im.message.receive_v1"; +} + + +/** + * 获取事件文本类型 + * @param {LarkMessageEvent} body + * @returns + */ +const getMsgType = (body: LarkMessageEvent) => { + return body?.event?.message?.message_type +} + + +/** + * 获取对话流Id + * @param {LarkMessageEvent} body + * @returns + */ +const getChatId = (body: LarkMessageEvent) => { + return body?.event?.message?.chat_id +} + +/** + * 获取文本内容并剔除艾特信息 + * @param {LarkMessageEvent} body + * @returns {string} 文本内容 + */ +const getMsgText = (body: LarkMessageEvent) => { + // TODO: 如果之后想支持单独提醒,这里需要做模板解析 + try { + const { text } = JSON.parse(body?.event?.message?.content) + // 去掉@_user_1相关的内容,例如 '@_user_1 测试' -> '测试' + const textWithoutAt = text.replace(/@_user_\d+/g, '') + // 去除空格和换行 + const textWithoutSpace = textWithoutAt.replace(/[\s\n]/g, '') + return textWithoutSpace + } + catch (e) { + return '' + } +} + +/** + * 过滤出非法消息,如果发表情包就直接发回去 + * @param {LarkMessageEvent} body + * @returns {boolean} 是否为非法消息 + */ +const filterIllegalMsg = (body: LarkMessageEvent) => { + const chatId = getChatId(body) + if (!chatId) return true + const msgType = getMsgType(body) + if (msgType === 'sticker') { + const content = body?.event?.message?.content + sendMsg('chat_id', chatId, 'sticker', content) + return true + } + if (msgType !== 'text') { + const textList = [ + '仅支持普通文本内容[黑脸]', + '唔...我只能处理普通文本哦[泣不成声]', + '噢!这似乎是个非普通文本[看]', + '哇!这是什么东东?我只懂普通文本啦![可爱]', + '只能处理普通文本内容哦[捂脸]', + ] + const content = JSON.stringify({ text: textList[Math.floor(Math.random() * textList.length)] }) + sendMsg('chat_id', chatId, 'text', content) + return true + } + return false +} + + +/** + * 过滤出info指令 + * @param {LarkMessageEvent} body + * @returns {boolean} 是否为info指令 + */ +const filterGetInfoCommand = (body: LarkMessageEvent) => { + const chatId = getChatId(body) + const text = getMsgText(body) + if (text !== 'info') return false + const content = JSON.stringify({ text: JSON.stringify(body)}) + sendMsg('chat_id', chatId, 'text', content) + return true +} + +export const manageEventMsg = async (body: LarkMessageEvent) => { + // 过滤非Event消息 + if (!isEventMsg(body)) { + return false; + } + // 过滤非法消息 + if (filterIllegalMsg(body)) { + return true + } + // 过滤info指令 + if (filterGetInfoCommand(body)) { + return true + } +} \ No newline at end of file diff --git a/routes/bot/index.ts b/routes/bot/index.ts index 73d8701..48f5f4a 100644 --- a/routes/bot/index.ts +++ b/routes/bot/index.ts @@ -1,3 +1,5 @@ +import { manageEventMsg } from "./eventMsg" + export const manageBotReq = async (req: Request) => { const body = await req.json() as any // 验证机器人 @@ -5,5 +7,7 @@ export const manageBotReq = async (req: Request) => { console.log("🚀 ~ manageBotReq ~ url_verification:") return Response.json({ challenge: body?.challenge }) } + // 处理Event消息 + if (await manageEventMsg(body)) return new Response("success") return new Response("hello, glade to see you!") } \ No newline at end of file diff --git a/routes/message/index.ts b/routes/message/index.ts new file mode 100644 index 0000000..7c70e3a --- /dev/null +++ b/routes/message/index.ts @@ -0,0 +1,82 @@ +import db from "../../db"; +import { sendMsg } from "../../utils/sendMsg"; + +interface MessageReqJson { + group_id: string; + msg_type: MsgType; + content: string; +} + +const validateMessageReq = (body: MessageReqJson) => { + if (!body.group_id) { + return new Response("group_id is required"); + } + if (!body.msg_type) { + return new Response("msg_type is required"); + } + if (!body.content) { + return new Response("content is required"); + } + return false; +}; + +export const manageMessageReq = async (req: Request) => { + const body = (await req.json()) as MessageReqJson; + // 校验参数 + const validateRes = validateMessageReq(body); + if (validateRes) { + return validateRes; + } + // 获取所有接收者 + const group = (await db.messageGroup.getOne(body.group_id)) as PBMessageGroup; + if (!group) { + return new Response("group not found"); + } + + const { chat_id, open_id, union_id, user_id, email } = group; + // 遍历所有id发送消息,保存所有对应的messageId + const sendRes = { + chat_id: {} as Record, + open_id: {} as Record, + union_id: {} as Record, + user_id: {} as Record, + email: {} as Record, + }; + // 发送消息列表 + const sendList = [] as Promise[]; + + // 构造发送消息函数 + const makeSendFunc = (receive_id_type: ReceiveIDType) => { + return (receive_id: string) => { + sendList.push( + sendMsg(receive_id_type, receive_id, body.msg_type, body.content).then( + (res) => { + sendRes[receive_id_type][receive_id] = res; + } + ) + ); + }; + }; + + // 发送消息 + if (chat_id) chat_id.map(makeSendFunc("chat_id")); + if (open_id) open_id.map(makeSendFunc("open_id")); + if (union_id) union_id.map(makeSendFunc("union_id")); + if (user_id) user_id.map(makeSendFunc("user_id")); + if (email) email.map(makeSendFunc("email")); + + try { + await Promise.all(sendList); + return Response.json({ + code: 200, + msg: "ok", + data: sendRes, + }); + } catch { + return Response.json({ + code: 400, + msg: "send msg failed", + data: sendRes, + }); + } +}; diff --git a/routes/message/readme.md b/routes/message/readme.md new file mode 100644 index 0000000..aa1cf60 --- /dev/null +++ b/routes/message/readme.md @@ -0,0 +1 @@ +# 批量发送消息,给已经订阅的用户和群组发送消息 diff --git a/schedule/accessToken.ts b/schedule/accessToken.ts new file mode 100644 index 0000000..39a2a63 --- /dev/null +++ b/schedule/accessToken.ts @@ -0,0 +1,20 @@ +import db from "../db" + +export const resetAccessToken = async () => { + const URL = 'https://open.f.mioffice.cn/open-apis/auth/v3/tenant_access_token/internal' + const app_id = 'cli_a1eff35b43b89063' + const app_secret = 'IFSl8ig5DMwMnFjwPiljCfoEWlgRwDxW' + const res = await fetch(URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + app_id, + app_secret + }) + }) + const { tenant_access_token } = await res.json() as any + await db.tenantAccessToken.update(tenant_access_token) + return tenant_access_token +} \ No newline at end of file diff --git a/schedule/index.ts b/schedule/index.ts new file mode 100644 index 0000000..d1c5945 --- /dev/null +++ b/schedule/index.ts @@ -0,0 +1,9 @@ +import { resetAccessToken } from "./accessToken"; +import schedule from 'node-schedule' + +export const initSchedule = async () => { + // 定时任务,每15分钟刷新一次token + schedule.scheduleJob('*/15 * * * *', resetAccessToken); + // 立即执行一次 + resetAccessToken() +} diff --git a/test.ts b/test.ts new file mode 100644 index 0000000..5c3fa86 --- /dev/null +++ b/test.ts @@ -0,0 +1,5 @@ +console.log( + JSON.stringify({ + text: "hello", + }) +); diff --git a/typings.d.ts b/typings.d.ts new file mode 100644 index 0000000..a500c3a --- /dev/null +++ b/typings.d.ts @@ -0,0 +1,445 @@ +/** + * 用户信息 + */ +interface User { + /** + * id + */ + id: string; + /** + * 用户名 + * @example zhaoyingbo + */ + userId: string; + /** + * open_id + */ + openId: string; + /** + * 提醒列表 + */ + remindList: string[]; +} + +/** + * 提醒列表 + */ +interface Remind { + /** + * id + */ + id: string; + /** + * 所有者信息,绑定用户表的id + */ + owner: string; + /** + * 消息Id + */ + messageId: string; + /** + * 接收者类型 + */ + subscriberType: "open_id" | "user_id" | "union_id" | "email" | "chat_id"; + /** + * 接收者Id + */ + subscriberId: string; + /** + * 是否需要回复,不需要回复的也不会重复提醒 + */ + needReply: boolean; + /** + * 延迟时间 + */ + delayTime: number; + /** + * 卡片信息,用于绘制初始卡片、确认卡片、取消卡片、延迟卡片 + */ + cardInfo: { + /** + * 提醒标题,必须要有 + */ + title: string; + /** + * 插图key + */ + imageKey?: string; + /** + * 提醒内容,为空不显示 + */ + content?: string; + /** + * 确认文本,为空不显示,为需要回复卡片时,如果为空则默认为“完成” + */ + confirmText?: string; + /** + * 取消文本,为空不显示 + */ + cancelText?: string; + /** + * 延迟文本,为空不显示 + */ + delayText?: string; + } | null; + /** + * 卡片模板信息 + */ + templateInfo: { + /** + * 卡片模板ID,会注入变量 + * ${owner} 所有者 + * ${remindTime} 提醒时间 + */ + pendingTemplateId: string; + /** + * 交互之后的卡片模板ID,如果有这个就不会用下边三个但是都会注入变量 + * ${owner} 所有者 + * ${remindTime} 提醒时间 + * ${result} 交互结果,会读卡片按钮绑定的变量text,如果没有则是绑定的result对应的 已确认、已取消、已延迟 + * ${interactTime} 交互时间 + */ + interactedTemplateId: string; + /** + * 确认之后的卡片模板ID + */ + confirmedTemplateId: string; + /** + * 取消之后的卡片模板ID + */ + cancelededTemplateId: string; + /** + * 延迟之后的卡片模板ID + */ + delayedTemplateId: string; + } | null; + + /** + * 提醒时间 + */ + remindTimes: RemindTime[]; + + /** + * 是否启用 + */ + enabled: boolean; + /** + * 下次提醒的时间,格式为yyyy-MM-dd HH:mm + */ + nextRemindTime: string; + /** + * 下次提醒时间的中文 + */ + nextRemindTimeCHS: string; +} + +/** + * 提醒时间 + * 为了支持多个时间点提醒,将时间存成数组 + */ +interface RemindTime { + /** + * 重复类型 + * single: 一次性 + * daily: 每天 + * weekly: 每周 + * monthly: 每月 + * yearly: 每年 + * workday: 工作日 + * holiday: 节假日 + */ + frequency: + | "single" + | "daily" + | "weekly" + | "monthly" + | "yearly" + | "workday" + | "holiday"; + /** + * 提醒时间,格式为HH:mm, single类型时仅作展示用,类型为yyyy-MM-dd HH:mm + */ + time: string; + /** + * 星期几[1-7],当frequency为weekly时有效 + */ + daysOfWeek: number[]; + /** + * 每月的几号[1-31],当frequency为monthly时有效 + */ + daysOfMonth: number[]; + /** + * 每年的哪天提醒,当frequency为 yearly 时有效,格式为MM-dd + */ + dayOfYear: string; +} + +/** + * 提醒记录 + * 记录提醒时间,回答结果等 + */ +interface RemindRecord { + /** + * 记录Id + */ + id: string; + /** + * 关联的提醒Id + */ + remindId: string; + /** + * 发送的卡片Id + */ + messageId: string; + /** + * 提醒状态 + * pending: 待确认 + * delay: 已延迟 + * confirmed: 已确认 + * canceled: 已取消 + */ + status: "pending" | "delayed" | "confirmed" | "canceled"; + /** + * 本次提醒时间,格式为yyyy-MM-dd HH:mm + */ + remindTime: string; + /** + * 用户交互的时间,格式为yyyy-MM-dd HH:mm + */ + interactTime: string; + /** + * 用户回答的结果,类似每天 07:00 + */ + result: object; +} + +/** + * 消息事件头 + */ +interface Header { + /** + * 事件ID + * @example 0f8ab23b60993cf8dd15c8cde4d7b0f5 + */ + event_id: string; + /** + * token + * @example tV9djUKSjzVnekV7xTg2Od06NFTcsBnj + */ + token: string; + /** + * 创建时间戳 + * @example 1693565712117 + */ + create_time: string; + /** + * 事件类型 + * @example im.message.receive_v1 + */ + event_type: string; + /** + * tenant_key + * @example 2ee61fe50f4f1657 + */ + tenant_key: string; + /** + * app_id + * @example cli_a1eff35b43b89063 + */ + app_id: string; +} + +/** + * 用户ID信息 + */ +interface UserIdInfo { + /** + * 用户标记 + * @example ou_032f507d08f9a7f28b042fcd086daef5 + */ + open_id: string; + /** + * 用户标记 + * @example on_7111660fddd8302ce47bf1999147c011 + */ + union_id: string; + /** + * 用户名 + * @example zhaoyingbo + */ + user_id: string; +} + +/** + * 被AT的人的信息 + */ +interface Mention { + /** + * 被艾特的人的ID信息 + */ + id: UserIdInfo; + /** + * 对应到文本内的内容 + * @example "@_user_1" + */ + key: string; + /** + * 用户名 + * @example 小煎蛋 + */ + name: string; + /** + * 应用ID + * @example 2ee61fe50f4f1657 + */ + tenant_key: string; +} + +/** + * 消息内容信息 + */ +interface Message { + /** + * 对话流ID + * @example oc_433b1cb7a9dbb7ebe70a4e1a59cb8bb1 + */ + chat_id: string; + /** + * 消息类型 + * @example group | p2p + */ + chat_type: string; + /** + * JSON字符串文本内容 + * @example "{\"text\":\"@_user_1 测试\"}" + */ + content: string; + /** + * 消息发送时间戳 + * @example 1693565711996 + */ + create_time: string; + /** + * 被艾特的人信息 + */ + mentions?: Mention[]; + /** + * 当前消息的ID + * @example om_038fc0eceed6224a1abc1cdaa4266405 + */ + message_id: string; + /** + * 消息类型 + * @example text、post、image、file、audio、media、sticker、interactive、share_chat、share_user + */ + message_type: string; +} + +/** + * 消息发送者信息 + */ +interface Sender { + /** + * id 相关信息 + */ + sender_id: UserIdInfo; + /** + * 发送者类型 + * @example user + */ + sender_type: string; + /** + * 应用ID + * @example 2ee61fe50f4f1657 + */ + tenant_key: string; +} + +/** + * 事件详情 + */ +interface Event { + message: Message; + sender: Sender; +} + +/** + * 事件订阅信息 + */ +interface LarkMessageEvent { + /** + * 协议版本 + * @example 2.0 + */ + schema: string; + /** + * 事件头 + */ + header: Header; + /** + * 事件详情 + */ + event: Event; +} + +/** + * 用户Action信息 + */ +interface LarkUserAction { + /** + * open_id + */ + open_id: string; + /** + * 用户名 + * @example zhaoyingbo + */ + user_id: string; + /** + * 当前消息的ID + * @example om_038fc0eceed6224a1abc1cdaa4266405 + */ + open_message_id: string; + /** + * 对话流ID + * @example oc_433b1cb7a9dbb7ebe70a4e1a59cb8bb1 + */ + open_chat_id: string; + /** + * 应用ID + * @example 2ee61fe50f4f1657 + */ + tenant_key: string; + /** + * token + * @example tV9djUKSjzVnekV7xTg2Od06NFTcsBnj + */ + token: string; + /** + * 事件结果 + */ + action: { + /** + * 传的参数 + */ + value: any; + /** + * 标签名 + * @example picker_datetime + */ + tag: string; + /** + * 选择的事件 + * @example 2023-09-03 10:35 +0800 + */ + option: string; + /** + * 时区 + */ + timezone: string; + }; +} + +type ReceiveIDType = "open_id" | "user_id" | "union_id" | "email" | "chat_id"; + +type MsgType = "text" | "post" | "image" | "file" | "audio" | "media" | "sticker" | "interactive" | "share_chat" | "share_user"; \ No newline at end of file diff --git a/utils/pbTools.ts b/utils/pbTools.ts new file mode 100644 index 0000000..77ed703 --- /dev/null +++ b/utils/pbTools.ts @@ -0,0 +1,11 @@ +export const managePb404 = async (dbFunc: Function) => { + try { + return await dbFunc() + } catch (err: any) { + console.log("🚀 ~ manage404 ~ err:", err) + // 没有这个提醒就返回空 + if (err?.message === "The requested resource wasn't found.") { + return null + } else throw err; + } +} diff --git a/utils/sendMsg.ts b/utils/sendMsg.ts new file mode 100644 index 0000000..eb603a6 --- /dev/null +++ b/utils/sendMsg.ts @@ -0,0 +1,47 @@ +import db from "../db"; + +/** + * 发送卡片 + * @param {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对应不同内容 + * @returns {string} 消息id + */ +export const sendMsg = async ( + receive_id_type: ReceiveIDType, + receive_id: string, + msg_type: MsgType, + content: string +) => { + const URL = `https://open.f.mioffice.cn/open-apis/im/v1/messages?receive_id_type=${receive_id_type}`; + const tenant_access_token = await db.tenantAccessToken.get(); + const header = { + "Content-Type": "application/json", + Authorization: `Bearer ${tenant_access_token}`, + }; + const body = { receive_id, msg_type, content }; + const res = await fetch(URL, { + method: "POST", + headers: header, + body: JSON.stringify(body), + }); + const data = (await res.json()) as any; + if (data.code !== 0) { + console.log("sendMsg error", data); + return { + code: data.code, + msg: data.msg, + }; + } + console.log("sendMsg success", data); + return { + code: 0, + msg: "success", + data: { + message_id: data.data.message_id, + }, + }; +}; + +// sendMsg('user_id', 'liuke9', 'text', JSON.stringify({text: '这是测试消息,不要回复'}))