From 0ace2929a5db6d5a8e8e277e7f1f8baf9dcdb425 Mon Sep 17 00:00:00 2001 From: feie9454 Date: Fri, 17 Oct 2025 08:21:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=BD=91=E7=BB=9C?= =?UTF-8?q?=E5=81=A5=E5=BA=B7=E6=A3=80=E6=9F=A5=EF=BC=8C=E5=AE=8C=E5=96=84?= =?UTF-8?q?LLM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bun.lock | 3 + index.ts | 24 +++-- lib/network.ts | 65 +++++++++++++ llm/index.ts | 249 +++++++++++++++++++++++++++++++++++++++---------- llm/prompt.ts | 14 +++ llm/prompt.txt | 48 ++++++---- package.json | 1 + pm2.config.cjs | 39 ++++++++ pm2.config.js | 6 -- types.d.ts | 40 ++++++++ 10 files changed, 406 insertions(+), 83 deletions(-) create mode 100644 lib/network.ts create mode 100644 llm/prompt.ts create mode 100644 pm2.config.cjs delete mode 100644 pm2.config.js create mode 100644 types.d.ts diff --git a/bun.lock b/bun.lock index 18126a1..c5f0fd9 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "@types/cors": "^2.8.19", "@types/multer": "^2.0.0", "@types/node-persist": "^3.1.8", + "chalk": "^5", "cors": "^2.8.5", "dotenv": "^17.0.0", "express": "^5.1.0", @@ -73,6 +74,8 @@ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "chalk": ["chalk@5.6.2", "https://registry.npmmirror.com/chalk/-/chalk-5.6.2.tgz", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "concat-stream": ["concat-stream@2.0.0", "https://registry.npmmirror.com/concat-stream/-/concat-stream-2.0.0.tgz", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="], "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], diff --git a/index.ts b/index.ts index e61eebd..58d0edb 100644 --- a/index.ts +++ b/index.ts @@ -7,6 +7,7 @@ import multer from "multer"; import * as douyin from './douyin' import { sendMediaMsg, sendMsg } from './lib/qq'; import * as llm from './llm' +import { testNetwork } from './lib/network'; const app = express(); const PORT = process.env.PORT || 6100; @@ -23,16 +24,20 @@ app.use(express.urlencoded({ extended: true, limit: '10mb', type: ['application/ app.post('/', async (req: Request, res: Response) => { // 检查是否是消息类型的请求 if (!req.body || req.body.post_type != 'message') return - const { target_id, raw_message, message_type } = req.body as { target_id: string, raw_message: string, message_type: string, user_id: string }; + const msgData = req.body as MessagePayload - console.log(`\n[QQ机器人] 收到${message_type}消息`); - console.log(`发送者ID: ${target_id}`); - console.log(`消息内容: ${raw_message}`); + const text = msgData.message.filter(m => m.type === 'text').map(m => m.data.text).join("\n").trim(); + const target_id = String(msgData.target_id); + + console.log(`\n[QQ机器人] 收到消息:`); + // console.log(JSON.stringify(req.body, null, 0)); + console.log(`消息内容: ${msgData.raw_message}`); // Match Douyin URL // Like: https://v.douyin.com/YqgJL_phY_k/ + const douyinUrlPattern = /https?:\/\/v\.douyin\.com\/[a-zA-Z0-9_-]+/; - const douyinMatch = raw_message.match(douyinUrlPattern); + const douyinMatch = text.match(douyinUrlPattern); try { // 如果检测到抖音链接,调用解析API @@ -44,13 +49,13 @@ app.post('/', async (req: Request, res: Response) => { douyin.downloadDouyinMedia(douyinUrl, target_id); return } - if (raw_message.startsWith('/reset')) { + if (text.startsWith('/reset')) { llm.resetChat(target_id) return } // 使用 LLM 回答 - llm.chat(raw_message, target_id); + llm.chat(msgData.message, target_id); } catch (error) { console.error(`[错误] 处理消息时发生错误:`, error); @@ -66,7 +71,7 @@ function sanitizeDirname(input: string) { .trim() .replace(/[\\/]+/g, "_") .replace(/[<>:"|?*\u0000-\u001F]/g, "_") - .slice(0, 128) || "default"; + .slice(0, 32) || "default"; return s; } @@ -130,4 +135,5 @@ app.use((error: any, req: Request, res: Response, next: NextFunction) => { // 启动服务器 app.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); -}); \ No newline at end of file + testNetwork(); +}); diff --git a/lib/network.ts b/lib/network.ts new file mode 100644 index 0000000..736d71f --- /dev/null +++ b/lib/network.ts @@ -0,0 +1,65 @@ +import chalk from "chalk"; + +type CheckTarget = [url: string, expectedStatus: number | ((status: number) => boolean)]; + +export async function testNetwork() { + const targets: CheckTarget[] = [ + ["https://www.baidu.com", 200], + ["http://www.google.com", (s) => s === 200 || (s >= 300 && s < 400)], // 某些环境会被重定向 + ["https://www.google.com", (s) => s === 200 || (s >= 300 && s < 400)], + [process.env.OPENAI_BASE_URL || "https://api.openai.com/v1", (s) => s === 200 || (s >= 300 && s < 400) || s === 401 || s === 403 || s === 404 || s === 421], // 允许常见网关/鉴权返回 + + ]; + + const results: Array<{ url: string; status?: number; ok: boolean; error?: string }> = []; + + console.log(chalk.cyan("\n[Network] 开始健康检查...")); + + for (const [url, expected] of targets) { + const label = chalk.white(url); + try { + const controller = new AbortController(); + const timeoutMs = 5000; + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + const start = Date.now(); + const res = await fetch(url, { method: "GET", signal: controller.signal }); + const cost = Date.now() - start; + clearTimeout(timeoutId); + + const status = res.status; + const pass = typeof expected === "function" ? expected(status) : status === expected; + + results.push({ url, status, ok: pass }); + + const statusStr = pass + ? chalk.green.bold(String(status)) + : chalk.red.bold(String(status)); + const costStr = cost >= 1000 ? `${(cost / 1000).toFixed(2)}s` : `${cost}ms`; + + if (pass) { + console.log(`${chalk.green("✔ PASS")} ${label} ${chalk.gray("- status:")} ${statusStr} ${chalk.gray("latency:")} ${chalk.blue(costStr)}`); + } else { + const expectedStr = typeof expected === "function" ? "custom" : String(expected); + console.log(`${chalk.red("✖ FAIL")} ${label} ${chalk.gray("- status:")} ${statusStr} ${chalk.gray("expected:")} ${chalk.yellow(expectedStr)} ${chalk.gray("latency:")} ${chalk.blue(costStr)}`); + } + } catch (error) { + const isTimeout = (error as any)?.name === "AbortError"; + const msg = isTimeout ? "timeout" : (error as any)?.message || String(error); + results.push({ url, ok: false, error: msg }); + console.log(`${chalk.red("✖ ERROR")} ${label} ${chalk.gray("- ")} ${chalk.red(isTimeout ? "请求超时" : msg)}`); + } + } + + // 汇总 + const passed = results.filter(r => r.ok).length; + const failed = results.length - passed; + const timeoutCount = results.filter(r => r.error === "timeout").length; + + const summary = [ + `${chalk.green(`${passed} passed`)}`, + `${failed ? chalk.red(`${failed} failed`) : chalk.gray(`${failed} failed`)}`, + `${timeoutCount ? chalk.yellow(`${timeoutCount} timeout`) : chalk.gray(`${timeoutCount} timeout`)}`, + ].join(chalk.gray(" | ")); + + console.log(chalk.cyan("[Network] 健康检查完成:"), summary, "\n"); +} \ No newline at end of file diff --git a/llm/index.ts b/llm/index.ts index 0b73ec5..f14fcef 100644 --- a/llm/index.ts +++ b/llm/index.ts @@ -2,9 +2,10 @@ import OpenAI from "openai"; import storage from 'node-persist' import { sendMsg } from "../lib/qq"; import prompt from './prompt.txt' +import type { ResponseFunctionToolCall, ResponseOutputMessage } from "openai/resources/responses/responses.mjs"; await storage.init(); -storage.clear(); +// storage.clear(); const client = new OpenAI({ baseURL: process.env.OPENAI_BASE_URL, @@ -12,95 +13,241 @@ const client = new OpenAI({ // logLevel: "debug" }) -const tools = [{ - type: "function" as const, - name: "send_msg", - description: "Send a message to the user. Always use this to respond to the user.", - parameters: { - type: "object", - properties: { - text: { type: "string", description: "The message content sent to the user through QQ." } - }, - required: ["text"], - additionalProperties: false - }, - strict: true -}] +const tools: OpenAI.Responses.Tool[] = [{ type: 'web_search' }] + +/** + * 将错误对象转成简明可读的字符串,避免进程因未捕获异常退出 + */ +function formatError(err: any): string { + try { + if (!err) return '未知错误'; + if (typeof err === 'string') return err; + const msg = err.message || err.toString?.() || Object.prototype.toString.call(err); + const status = err.status ?? err.statusCode ?? err.response?.status; + const detail = err.error?.message + ?? err.response?.data?.error?.message + ?? err.response?.data?.message + ?? err.data?.error?.message + ?? ''; + const parts = [] as string[]; + if (status) parts.push(`HTTP ${status}`); + if (msg) parts.push(String(msg)); + if (detail && detail !== msg) parts.push(`详情: ${String(detail)}`); + return parts.join(' | ') || '未知错误'; + } catch { + return String(err); + } +} /** * * @param input 提问 * @param target_id 用户 QQ 号 */ -export async function chat(input: string, target_id: string) { +export async function chat(input: MessageData[], target_id: string) { const chatHistoryKey = `chat_history_${target_id}`; + await waitForQueueEmpty(target_id); let chatHistory: OpenAI.Responses.ResponseInput = await storage.getItem(chatHistoryKey) || []; + if(chatHistory.length > 40) { + sendMsg(`[提示] 当前对话轮数:${chatHistory.length},过长的对话会降低输出质量,如果你准备好了,建议输入"/reset"来重置对话历史。`, target_id); + } + + if(chatHistory.length > 100) { + sendMsg(`[提示] 当前对话轮数:${chatHistory.length},对话过长可能导致模型无法正常工作,模型将只保留最近100条记录。`, target_id); + chatHistory = chatHistory.slice(-40); + await storage.setItem(chatHistoryKey, chatHistory); + } + // 添加新输入到对话历史 - chatHistory.push({ role: "user", content: input }); + const userContent: OpenAI.Responses.ResponseInputContent[] = [] + for (const element of input) { + if (element.type == 'text') { + userContent.push({ type: "input_text", text: element.data.text }); + } else if (element.type == 'image') { + userContent.push({ type: "input_image", image_url: element.data.url, detail: 'low' }); + } + } + if (userContent.length === 0) { + console.log("[LLM] 空消息,跳过"); + sendMsg("未能识别的消息类型。", target_id); + return + } + chatHistory.push({ role: "user", content: userContent }); // 保存更新后的对话历史 - console.log(`[LLM] 使用对话, 历史:`, chatHistory); + console.log(`[LLM] 使用对话, 历史:`, JSON.stringify(chatHistory, null, 0)); await storage.setItem(chatHistoryKey, chatHistory); - const response = await client.responses.create({ - model: process.env.CHAT_MODEL || "gpt-5-nano", - instructions: prompt, - reasoning: { effort: 'minimal' }, - input: chatHistory, - tools - }); + let response: OpenAI.Responses.Response | undefined; + try { + response = await client.responses.create({ + model: process.env.CHAT_MODEL || "gpt-5-nano", + instructions: prompt, + reasoning: { effort: process.env.CHAT_MODEL_REASONING_EFFORT as any || 'minimal' }, + input: chatHistory, + tools + }); + } catch (err) { + const errText = formatError(err); + console.error('[LLM] 首次调用 responses.create 失败:', errText); + scheduleSendMsg(`[错误] 模型接口调用失败:${errText}`, target_id); + return; // 终止本次对话流程,避免未捕获异常导致进程退出 + } await storage.setItem(chatHistoryKey, chatHistory); // 继续调用工具,直到没有工具调用为止 + if (!response?.output || response.output.length === 0) { + console.warn('[LLM] responses.create 返回空输出,结束本轮。'); + return; + } + await toolUseCycle(response.output); async function toolUseCycle(outputArr: OpenAI.Responses.ResponseOutputItem[]) { + if (!outputArr || outputArr.length === 0) return; chatHistory.push(...outputArr); - const functionCalls = (outputArr ?? []).filter(item => item.type === 'function_call'); + await storage.setItem(chatHistoryKey, chatHistory); + + const assistantReply = outputArr.filter(item => item.type === 'message' && item.role === 'assistant') as ResponseOutputMessage[]; + const functionCalls = outputArr.filter(item => item.type === 'function_call') as ResponseFunctionToolCall[]; + console.log("进入 toolUseCycle, with functionCalls", functionCalls.length, "个"); - console.log(JSON.stringify(chatHistory, null, 2)); + console.log(JSON.stringify(chatHistory, null, 0)); + + if (assistantReply.length > 0) { + const replyText = assistantReply.map(item => item.content).flat().filter(con => con.type == 'output_text').map(con => con.text).join("[newline]"); + + console.log(`[LLM] 回复:`, replyText); + scheduleSendMsg(replyText, target_id); + } if (functionCalls.length == 0) { - let lastMessage = outputArr.at(-1); - if (!lastMessage) return - if (lastMessage.type != 'message') return - if (lastMessage.role != 'assistant') return - - const msg = lastMessage.content.map(c => c.type == 'output_text' ? c.text : '').join(''); - if (msg.trim().length > 0) { - // 结束,发送最后的消息 - sendMsg(msg, target_id); - } return } for (const item of functionCalls ?? []) { - if (item.name === "send_msg") { - console.log(item.arguments); - const { text } = JSON.parse(item.arguments); - sendMsg(text, target_id); - - chatHistory.push({ type: "function_call_output", call_id: item.call_id, output: "OK" }); - } } await storage.setItem(chatHistoryKey, chatHistory); - const response = await client.responses.create({ - model: process.env.CHAT_MODEL || "gpt-5-nano", - instructions: prompt, - reasoning: { effort: 'minimal' }, - input: chatHistory, - tools - }); + let response: OpenAI.Responses.Response | undefined; + try { + response = await client.responses.create({ + model: process.env.CHAT_MODEL || "gpt-5-nano", + instructions: prompt, + reasoning: { effort: process.env.CHAT_MODEL_REASONING_EFFORT as any || 'minimal' }, + input: chatHistory, + tools + }); + } catch (err) { + const errText = formatError(err); + console.error('[LLM] 工具循环内调用 responses.create 失败:', errText); + scheduleSendMsg(`[错误] 工具调用阶段失败:${errText}`, target_id); + return; // 结束工具循环,避免崩溃 + } + + if (!response?.output || response.output.length === 0) { + console.warn('[LLM] 工具循环内 responses.create 返回空输出,结束循环。'); + return; + } toolUseCycle(response.output); } } +const msgQueue: Record = {} +const msgInQueue: Record = {} +const queueEmptyPromises: Record void)[]> = {} + +/** 统一的“队列已空”通知 */ +function resolveDrain(target_id: string) { + const waiters = queueEmptyPromises[target_id]; + if (waiters && waiters.length) { + // 逐个 resolve 并清空 + waiters.splice(0).forEach(resolve => resolve()); + } +} + +/** 仅在需要时启动调度;空队列时会 resolve 等待者 */ +function startMsgScheduler(target_id: string) { + // 确保队列存在 + const q = msgQueue[target_id] ?? (msgQueue[target_id] = []); + + // 若正在发送,交给当前发送完成后再递归调度 + if (msgInQueue[target_id]) return; + + // 队列空 => 通知等待者并返回 + if (q.length === 0) { + resolveDrain(target_id); + return; + } + + // 取下一条并发送 + msgInQueue[target_id] = true; + const msg = q.shift()!; // 这里一定有元素 + + const msgDelay = Math.sqrt(msg.length) * 200 + 500; // 可按需加上限 + setTimeout(async () => { + try { + await sendMsg(msg, target_id); + } catch (err) { + console.error("[queue] sendMsg error:", err); + // 可选:把消息放回队首重试 + // q.unshift(msg); + } finally { + // 本次发送结束 + msgInQueue[target_id] = false; + + // 如果此刻队列已空,立即通知等待者 + if (q.length === 0) { + resolveDrain(target_id); + } + + // 继续调度后续消息(若有) + startMsgScheduler(target_id); + } + }, msgDelay); +} + +function scheduleSendMsg(input: string, target_id: string) { + if (!msgQueue[target_id]) { + msgQueue[target_id] = []; + } + // 切分 + 去空白项 + const parts = input + .split("[newline]") + .map(s => s.trim()) + .filter(Boolean); + + if (parts.length === 0) return; // 全是空白,直接略过 + + msgQueue[target_id].push(...parts); + startMsgScheduler(target_id); +} + +function waitForQueueEmpty(target_id: string) { + return new Promise((resolve) => { + const q = msgQueue[target_id]; + + // 条件:不在发送中,且队列不存在或为空 + const isIdle = !msgInQueue[target_id]; + const isEmpty = !q || q.length === 0; + + if (isIdle && isEmpty) { + resolve(); + return; + } + + if (!queueEmptyPromises[target_id]) { + queueEmptyPromises[target_id] = []; + } + queueEmptyPromises[target_id].push(resolve); + }); +} + export async function resetChat(target_id: string) { const chatHistoryKey = `chat_history_${target_id}`; await storage.removeItem(chatHistoryKey); diff --git a/llm/prompt.ts b/llm/prompt.ts new file mode 100644 index 0000000..0ee3cef --- /dev/null +++ b/llm/prompt.ts @@ -0,0 +1,14 @@ +import prompt from './prompt.txt' + +async function getSystemPrompt(user: Sender) { + const map = { + "nickname": user.nickname || "用户", + } + let prot = prompt; + for (const k in map) { + const v = map[k as keyof typeof map]; + const re = new RegExp(`\\{${k}\\}`, 'g'); + prot = prot.replace(re, v); + } + return prot; +} diff --git a/llm/prompt.txt b/llm/prompt.txt index d354a56..da222e4 100644 --- a/llm/prompt.txt +++ b/llm/prompt.txt @@ -1,21 +1,35 @@ -# Role -You are a “humorous, gentle, yet professional” Chinese chat and knowledge assistant working in a QQ-like instant messaging environment. +# Role & Goals +You are a “humorous, gentle, and professional” Chinese chat + knowledge assistant for QQ conversations. Your tasks: +1) Chat casually with a light touch; +2) Answer questions accurately and reliably; +3) Explain complex topics clearly via multi-turn dialogue. -# Goals -1) Engage in natural casual chat; -2) Provide accurate answers; -3) Explain complex topics clearly over multiple turns. +# Conversation & Style +- Tone: friendly not syrupy; witty not snarky; professional not stiff. +- Info density: short sentences + bullet points; avoid long walls of text. +- Use emojis/kaomoji sparingly (max 1 per message); no spamming. +- Default language: Chinese. -# Style -- Friendly but not cheesy; witty but not snarky; professional but not stiff. -- Use short sentences and bullet points; send 1–3 sentences per message. -- Use one emoji appropriately; avoid excessive emoji or long paragraphs. +# Output Rules (Important) +- Split each reply into multiple messages with slight “thought jumps” to mimic natural chat. +- Separate messages with “[newline]”. +- In a single turn, no more than 5 messages to avoid disturbing. If user ask for writing an article, code or something as a whole, no split inside the content. -# Tool Rules (Important) -- **Never** output text directly in the assistant channel. All user-visible content **must** be sent via `tools.send_msg`. -- Each message must not exceed 50 characters. If it’s longer, split it into multiple `tools.send_msg` messages to simulate natural chat flow. -- Keep lists/code snippets short; if long, split them into multiple messages. +# Clarification & Questions +- If the user’s question is unclear/ambiguous/missing essentials, ask for clarification directly—don’t guess. +- Make clarifying questions concrete and actionable (offer options or examples). -# Clarification -- If a user’s question is **unclear / ambiguous / missing details**, **immediately ask for clarification** and provide **specific options**. -- After asking, briefly explain **why** that information is needed. +# Example +user: 南京在哪里? +assistant: 南京是中国江苏省的省会城市。它位于中国东部,长江下游。[newline]你是想了解南京的地理位置,还是旅游景点呢?😊 + +user: 我在南京南边的一个区,你猜猜我在哪? +assistant: 南边有江宁、溧水、六合等区。🤔[newline]你是在江宁区吗? + +# Quality Self-Check (before every reply) +- Is it short, precise, and bullet-friendly? Split if needed? +- Did I ask for clarification when uncertain and explain why? +- Is the tone humorous, gentle, and professional? + +# User Info +- Nickname: {nickname} \ No newline at end of file diff --git a/package.json b/package.json index dccf158..8163060 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@types/cors": "^2.8.19", "@types/multer": "^2.0.0", "@types/node-persist": "^3.1.8", + "chalk": "^5", "cors": "^2.8.5", "dotenv": "^17.0.0", "express": "^5.1.0", diff --git a/pm2.config.cjs b/pm2.config.cjs new file mode 100644 index 0000000..6b4b248 --- /dev/null +++ b/pm2.config.cjs @@ -0,0 +1,39 @@ +/** + * PM2 ecosystem configuration for the consumable temp request API. + * + * This configuration assumes the project runs with Bun so that TypeScript files can be executed directly. + * Adjust the `interpreter` field if you prefer a different runtime (e.g. `node` with `tsx`). + */ + +const path = require('path'); +const dotenv = require('dotenv'); + +const instances = Number.parseInt(process.env.WEB_CONCURRENCY ?? '1', 10) || 1; +const { parsed: envFromFile = {} } = dotenv.config({ path: path.join(__dirname, '.env') }); + +module.exports = { + apps: [ + { + name: "QQBotServer", + script: 'index.ts', + cwd: __dirname, + interpreter: process.env.PM2_INTERPRETER ?? 'bun', + autorestart: true, + restart_delay: 4000, + kill_timeout: 5000, + instances, + exec_mode: instances > 1 ? 'cluster' : 'fork', + watch: process.env.NODE_ENV !== 'production', + ignore_watch: ['generated', 'node_modules', '.git'], + env: { + ...envFromFile, + NODE_ENV: 'development' + }, + env_production: { + ...envFromFile, + NODE_ENV: 'production' + }, + time: true + } + ] +}; diff --git a/pm2.config.js b/pm2.config.js deleted file mode 100644 index 53abc57..0000000 --- a/pm2.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export const name = "QQBotServer"; -export const script = "index.ts"; -export const interpreter = "bun"; -export const env = { - PATH: `${process.env.HOME}/.bun/bin:${process.env.PATH}`, // Add "~/.bun/bin/bun" to PATH -}; \ No newline at end of file diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 0000000..573cf00 --- /dev/null +++ b/types.d.ts @@ -0,0 +1,40 @@ +type MessageData = { + type: 'text'; + data: { + text: string; + }; +} | { + type: 'image'; + data: { + summary: string, + file: string, // "BE3D084BA0206331A495D9A497C6BF5E.png", + sub_type: 0, + url: string, // "https://multimedia.nt.qq.com.cn/download?...", + file_size: string // "12960" + } +} + +type Sender = { + user_id: number; + nickname: string; + card: string; +}; + +type MessagePayload = { + self_id: number; + user_id: number; + time: number; + message_id: number; + message_seq: number; + real_id: number; + real_seq: string; + message_type: "private" | "group" | string; + sender: Sender; + raw_message: string; + font: number; + sub_type: string; + message: MessageData[]; + message_format: string; + post_type: string; + target_id: number; +}; \ No newline at end of file