@sfmc-bds/qq-bridge 0.2.0-beta.9 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +69 -2
  2. package/deploy/install-nginx-webhook.sh +34 -0
  3. package/deploy/nginx-webhook-location.conf +10 -0
  4. package/deploy/qq-bridge.service +23 -0
  5. package/deploy/qq-config-sync.ps1 +106 -0
  6. package/deploy/qq-tunnel.ps1 +15 -0
  7. package/deploy/sync-qq-config.mjs +229 -0
  8. package/dist/commands/account-format.js +41 -0
  9. package/dist/commands/account-format.js.map +7 -0
  10. package/dist/commands/content-format.js +114 -0
  11. package/dist/commands/content-format.js.map +7 -0
  12. package/dist/commands/db-api.js +21 -6
  13. package/dist/commands/db-api.js.map +2 -2
  14. package/dist/commands/handlers.js +392 -524
  15. package/dist/commands/handlers.js.map +2 -2
  16. package/dist/commands/index.js +5 -5
  17. package/dist/commands/index.js.map +3 -3
  18. package/dist/commands/interaction-router.js +38 -133
  19. package/dist/commands/interaction-router.js.map +2 -2
  20. package/dist/commands/join-settings-ui.js +1 -24
  21. package/dist/commands/join-settings-ui.js.map +2 -2
  22. package/dist/commands/menu-format.js +36 -33
  23. package/dist/commands/menu-format.js.map +2 -2
  24. package/dist/commands/menu-panel-sync.js +39 -13
  25. package/dist/commands/menu-panel-sync.js.map +2 -2
  26. package/dist/commands/message-text.js +91 -0
  27. package/dist/commands/message-text.js.map +7 -0
  28. package/dist/commands/pending.js +23 -16
  29. package/dist/commands/pending.js.map +2 -2
  30. package/dist/commands/permissions.js +17 -0
  31. package/dist/commands/permissions.js.map +7 -0
  32. package/dist/commands/registry.js +1 -1
  33. package/dist/commands/registry.js.map +2 -2
  34. package/dist/commands/render.js +7 -16
  35. package/dist/commands/render.js.map +2 -2
  36. package/dist/commands/reply-llbot.js +18 -7
  37. package/dist/commands/reply-llbot.js.map +2 -2
  38. package/dist/commands/reply-official.js +55 -39
  39. package/dist/commands/reply-official.js.map +3 -3
  40. package/dist/commands/router.js +116 -23
  41. package/dist/commands/router.js.map +2 -2
  42. package/dist/config.js +35 -27
  43. package/dist/config.js.map +2 -2
  44. package/dist/console.js +3 -3
  45. package/dist/console.js.map +2 -2
  46. package/dist/index.js +30 -16
  47. package/dist/index.js.map +2 -2
  48. package/dist/llbot-ws-api.js +2 -2
  49. package/dist/llbot-ws-api.js.map +2 -2
  50. package/dist/official/events.js +32 -7
  51. package/dist/official/events.js.map +2 -2
  52. package/dist/official/gateway.js +10 -1
  53. package/dist/official/gateway.js.map +2 -2
  54. package/dist/official/webhook.js +165 -0
  55. package/dist/official/webhook.js.map +7 -0
  56. package/dist/onebot.js +0 -4
  57. package/dist/onebot.js.map +2 -2
  58. package/package.json +4 -4
  59. package/dist/commands/join-settings-ui.test.js +0 -43
  60. package/dist/commands/join-settings-ui.test.js.map +0 -7
  61. package/dist/commands/menu-panel-sync.test.js +0 -27
  62. package/dist/commands/menu-panel-sync.test.js.map +0 -7
  63. package/dist/commands/render.test.js +0 -44
  64. package/dist/commands/render.test.js.map +0 -7
  65. package/dist/commands/router.test.js +0 -92
  66. package/dist/commands/router.test.js.map +0 -7
  67. package/dist/llbot-ws-api.test.js +0 -32
  68. package/dist/llbot-ws-api.test.js.map +0 -7
  69. package/dist/official/events.test.js +0 -123
  70. package/dist/official/events.test.js.map +0 -7
@@ -0,0 +1,165 @@
1
+ import { createPrivateKey, createPublicKey, sign, verify } from "node:crypto";
2
+ import { createServer } from "node:http";
3
+ import { log } from "../log.js";
4
+ const MAX_BODY_BYTES = 64 * 1024;
5
+ const MAX_PENDING_EVENTS = 512;
6
+ const MAX_CLOCK_SKEW_MS = 10 * 60 * 1e3;
7
+ const PKCS8_ED25519_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
8
+ function webhookPrivateKey(appSecret) {
9
+ const secret = Buffer.from(appSecret, "utf8");
10
+ if (secret.length === 0) throw new Error("QQ AppSecret \u4E0D\u80FD\u4E3A\u7A7A");
11
+ const seed = Buffer.alloc(32);
12
+ for (let i = 0; i < seed.length; i++) seed[i] = secret[i % secret.length];
13
+ return createPrivateKey({
14
+ key: Buffer.concat([PKCS8_ED25519_PREFIX, seed]),
15
+ format: "der",
16
+ type: "pkcs8"
17
+ });
18
+ }
19
+ function sendJson(res, status, value) {
20
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
21
+ res.end(JSON.stringify(value));
22
+ }
23
+ async function readBody(req) {
24
+ const chunks = [];
25
+ let length = 0;
26
+ for await (const chunk of req) {
27
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
28
+ length += bytes.length;
29
+ if (length > MAX_BODY_BYTES) throw new Error("body_too_large");
30
+ chunks.push(bytes);
31
+ }
32
+ return Buffer.concat(chunks);
33
+ }
34
+ function validTimestamp(value) {
35
+ if (!/^\d{10,13}$/.test(value)) return false;
36
+ const number = Number(value);
37
+ const millis = value.length === 10 ? number * 1e3 : number;
38
+ return Math.abs(Date.now() - millis) <= MAX_CLOCK_SKEW_MS;
39
+ }
40
+ async function dispatchEvent(payload, opts) {
41
+ const data = payload.d;
42
+ switch (payload.t) {
43
+ case "GROUP_MESSAGE_CREATE":
44
+ await opts.dispatcher.handleGroupMessage(data);
45
+ break;
46
+ case "GROUP_AT_MESSAGE_CREATE":
47
+ await opts.dispatcher.handleGroupAtMessage(data);
48
+ break;
49
+ case "C2C_MESSAGE_CREATE":
50
+ await opts.dispatcher.handleC2cMessage(data);
51
+ break;
52
+ case "INTERACTION_CREATE":
53
+ await opts.interactionRouter?.handle(data);
54
+ break;
55
+ default:
56
+ if (payload.t) log.info(`\u5B98\u65B9 Webhook \u5FFD\u7565\u672A\u5904\u7406\u4E8B\u4EF6 t=${payload.t}`);
57
+ }
58
+ }
59
+ async function startOfficialWebhook(opts) {
60
+ if (!Number.isInteger(opts.port) || opts.port < 1 || opts.port > 65535) {
61
+ throw new Error("official.webhook.port \u5FC5\u987B\u662F 1\u201365535 \u7684\u7AEF\u53E3");
62
+ }
63
+ if (!/^\/[A-Za-z0-9/_-]+$/.test(opts.path) || opts.path.includes("//")) {
64
+ throw new Error("official.webhook.path \u5FC5\u987B\u662F\u666E\u901A\u7EDD\u5BF9\u8DEF\u5F84");
65
+ }
66
+ const privateKey = webhookPrivateKey(opts.appSecret);
67
+ const publicKey = createPublicKey(privateKey);
68
+ let pending = Promise.resolve();
69
+ let pendingCount = 0;
70
+ const recentEvents = /* @__PURE__ */ new Map();
71
+ const server = createServer((req, res) => {
72
+ void (async () => {
73
+ if (req.url !== opts.path || req.method !== "POST") {
74
+ sendJson(res, 404, { error: "not_found" });
75
+ return;
76
+ }
77
+ if (req.headers["x-bot-appid"] !== opts.appId) {
78
+ sendJson(res, 403, { error: "wrong_appid" });
79
+ return;
80
+ }
81
+ if (Number(req.headers["content-length"] ?? 0) > MAX_BODY_BYTES) {
82
+ sendJson(res, 413, { error: "body_too_large" });
83
+ return;
84
+ }
85
+ let raw;
86
+ try {
87
+ raw = await readBody(req);
88
+ } catch {
89
+ sendJson(res, 413, { error: "body_too_large" });
90
+ return;
91
+ }
92
+ let payload;
93
+ try {
94
+ payload = JSON.parse(raw.toString("utf8"));
95
+ if (!payload || typeof payload !== "object") throw new Error("invalid");
96
+ } catch {
97
+ sendJson(res, 400, { error: "invalid_json" });
98
+ return;
99
+ }
100
+ if (payload.op === 13) {
101
+ const challenge = payload.d;
102
+ if (typeof challenge?.plain_token !== "string" || typeof challenge.event_ts !== "string") {
103
+ sendJson(res, 400, { error: "invalid_challenge" });
104
+ return;
105
+ }
106
+ const signature2 = sign(
107
+ null,
108
+ Buffer.from(`${challenge.event_ts}${challenge.plain_token}`, "utf8"),
109
+ privateKey
110
+ ).toString("hex");
111
+ sendJson(res, 200, { plain_token: challenge.plain_token, signature: signature2 });
112
+ return;
113
+ }
114
+ const timestamp = req.headers["x-signature-timestamp"];
115
+ const signature = req.headers["x-signature-ed25519"];
116
+ if (typeof timestamp !== "string" || typeof signature !== "string" || !validTimestamp(timestamp) || !/^[0-9a-fA-F]{128}$/.test(signature) || !verify(null, Buffer.concat([Buffer.from(timestamp, "utf8"), raw]), publicKey, Buffer.from(signature, "hex"))) {
117
+ sendJson(res, 403, { error: "invalid_signature" });
118
+ return;
119
+ }
120
+ if (payload.op !== 0 || typeof payload.t !== "string" || !payload.d || typeof payload.d !== "object") {
121
+ sendJson(res, 400, { error: "invalid_event" });
122
+ return;
123
+ }
124
+ const eventId = String(payload.id ?? payload.d.id ?? "");
125
+ const dedupKey = eventId ? `${payload.t}:${eventId}` : "";
126
+ const lastSeen = dedupKey ? recentEvents.get(dedupKey) : void 0;
127
+ if (lastSeen && Date.now() - lastSeen < MAX_CLOCK_SKEW_MS) {
128
+ sendJson(res, 200, { ok: true });
129
+ return;
130
+ }
131
+ if (pendingCount >= MAX_PENDING_EVENTS) {
132
+ sendJson(res, 503, { error: "queue_full" });
133
+ return;
134
+ }
135
+ if (dedupKey) {
136
+ recentEvents.set(dedupKey, Date.now());
137
+ if (recentEvents.size > 2048) recentEvents.delete(recentEvents.keys().next().value);
138
+ }
139
+ pendingCount++;
140
+ pending = pending.then(() => dispatchEvent(payload, opts)).catch((e) => {
141
+ log.error(`\u5B98\u65B9 Webhook \u4E8B\u4EF6\u5904\u7406\u5931\u8D25: ${e.message}`);
142
+ }).finally(() => {
143
+ pendingCount--;
144
+ });
145
+ sendJson(res, 200, { ok: true });
146
+ })().catch((e) => {
147
+ log.error(`\u5B98\u65B9 Webhook \u8BF7\u6C42\u5904\u7406\u5931\u8D25: ${e.message}`);
148
+ if (!res.headersSent) sendJson(res, 500, { error: "internal_error" });
149
+ });
150
+ });
151
+ server.requestTimeout = 1e4;
152
+ await new Promise((resolve, reject) => {
153
+ server.once("error", reject);
154
+ server.listen(opts.port, "127.0.0.1", () => {
155
+ server.off("error", reject);
156
+ resolve();
157
+ });
158
+ });
159
+ log.info(`\u5B98\u65B9 Webhook \u5DF2\u76D1\u542C http://127.0.0.1:${opts.port}${opts.path}`);
160
+ return { stop: () => new Promise((resolve, reject) => server.close((e) => e ? reject(e) : resolve())) };
161
+ }
162
+ export {
163
+ startOfficialWebhook
164
+ };
165
+ //# sourceMappingURL=webhook.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/official/webhook.ts"],
4
+ "sourcesContent": ["/** QQ \u5B98\u65B9\u673A\u5668\u4EBA HTTP \u56DE\u8C03\u5165\u53E3\uFF1BHTTPS \u7531\u540C\u673A\u53CD\u5411\u4EE3\u7406\u7EC8\u6B62\u3002 */\nimport { createPrivateKey, createPublicKey, sign, verify, type KeyObject } from \"node:crypto\";\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from \"node:http\";\nimport type { InteractionRouter } from \"../commands/interaction-router.js\";\nimport { log } from \"../log.js\";\nimport type { OfficialAtMessageDispatcher, OfficialC2cMessage, OfficialGroupAtMessage } from \"./events.js\";\n\nconst MAX_BODY_BYTES = 64 * 1024;\nconst MAX_PENDING_EVENTS = 512;\nconst MAX_CLOCK_SKEW_MS = 10 * 60 * 1000;\nconst PKCS8_ED25519_PREFIX = Buffer.from(\"302e020100300506032b657004220420\", \"hex\");\n\nexport interface OfficialWebhookOptions {\n appId: string;\n appSecret: string;\n port: number;\n path: string;\n dispatcher: OfficialAtMessageDispatcher;\n interactionRouter?: InteractionRouter;\n}\n\ntype CallbackPayload = { op?: number; t?: string; d?: unknown; id?: string; s?: number };\n\n/** QQ \u56DE\u8C03\u534F\u8BAE\u4F7F\u7528 AppSecret \u7684 UTF-8 \u5B57\u8282\u5FAA\u73AF\u8865\u9F50\u4E3A 32 \u5B57\u8282 Ed25519 \u79CD\u5B50\u3002 */\nfunction webhookPrivateKey(appSecret: string): KeyObject {\n const secret = Buffer.from(appSecret, \"utf8\");\n if (secret.length === 0) throw new Error(\"QQ AppSecret \u4E0D\u80FD\u4E3A\u7A7A\");\n const seed = Buffer.alloc(32);\n for (let i = 0; i < seed.length; i++) seed[i] = secret[i % secret.length]!;\n return createPrivateKey({\n key: Buffer.concat([PKCS8_ED25519_PREFIX, seed]),\n format: \"der\",\n type: \"pkcs8\",\n });\n}\n\nfunction sendJson(res: ServerResponse, status: number, value: Record<string, unknown>): void {\n res.writeHead(status, { \"Content-Type\": \"application/json; charset=utf-8\", \"Cache-Control\": \"no-store\" });\n res.end(JSON.stringify(value));\n}\n\nasync function readBody(req: IncomingMessage): Promise<Buffer> {\n const chunks: Buffer[] = [];\n let length = 0;\n for await (const chunk of req) {\n const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);\n length += bytes.length;\n if (length > MAX_BODY_BYTES) throw new Error(\"body_too_large\");\n chunks.push(bytes);\n }\n return Buffer.concat(chunks);\n}\n\nfunction validTimestamp(value: string): boolean {\n if (!/^\\d{10,13}$/.test(value)) return false;\n const number = Number(value);\n const millis = value.length === 10 ? number * 1000 : number;\n return Math.abs(Date.now() - millis) <= MAX_CLOCK_SKEW_MS;\n}\n\nasync function dispatchEvent(payload: CallbackPayload, opts: OfficialWebhookOptions): Promise<void> {\n const data = payload.d;\n switch (payload.t) {\n case \"GROUP_MESSAGE_CREATE\":\n await opts.dispatcher.handleGroupMessage(data as OfficialGroupAtMessage);\n break;\n case \"GROUP_AT_MESSAGE_CREATE\":\n await opts.dispatcher.handleGroupAtMessage(data as OfficialGroupAtMessage);\n break;\n case \"C2C_MESSAGE_CREATE\":\n await opts.dispatcher.handleC2cMessage(data as OfficialC2cMessage);\n break;\n case \"INTERACTION_CREATE\":\n await opts.interactionRouter?.handle(data);\n break;\n default:\n if (payload.t) log.info(`\u5B98\u65B9 Webhook \u5FFD\u7565\u672A\u5904\u7406\u4E8B\u4EF6 t=${payload.t}`);\n }\n}\n\n/** \u672C\u5730\u76D1\u542C HTTP\uFF1B\u516C\u7F51\u4EC5\u901A\u8FC7 HTTPS \u53CD\u5411\u4EE3\u7406\u8F6C\u53D1\u6307\u5B9A\u8DEF\u5F84\u3002 */\nexport async function startOfficialWebhook(opts: OfficialWebhookOptions): Promise<{ stop: () => Promise<void> }> {\n if (!Number.isInteger(opts.port) || opts.port < 1 || opts.port > 65535) {\n throw new Error(\"official.webhook.port \u5FC5\u987B\u662F 1\u201365535 \u7684\u7AEF\u53E3\");\n }\n if (!/^\\/[A-Za-z0-9/_-]+$/.test(opts.path) || opts.path.includes(\"//\")) {\n throw new Error(\"official.webhook.path \u5FC5\u987B\u662F\u666E\u901A\u7EDD\u5BF9\u8DEF\u5F84\");\n }\n\n const privateKey = webhookPrivateKey(opts.appSecret);\n const publicKey = createPublicKey(privateKey);\n let pending = Promise.resolve();\n let pendingCount = 0;\n const recentEvents = new Map<string, number>();\n const server: Server = createServer((req, res) => {\n void (async () => {\n if (req.url !== opts.path || req.method !== \"POST\") {\n sendJson(res, 404, { error: \"not_found\" });\n return;\n }\n if (req.headers[\"x-bot-appid\"] !== opts.appId) {\n sendJson(res, 403, { error: \"wrong_appid\" });\n return;\n }\n if (Number(req.headers[\"content-length\"] ?? 0) > MAX_BODY_BYTES) {\n sendJson(res, 413, { error: \"body_too_large\" });\n return;\n }\n\n let raw: Buffer;\n try {\n raw = await readBody(req);\n } catch {\n sendJson(res, 413, { error: \"body_too_large\" });\n return;\n }\n let payload: CallbackPayload;\n try {\n payload = JSON.parse(raw.toString(\"utf8\")) as CallbackPayload;\n if (!payload || typeof payload !== \"object\") throw new Error(\"invalid\");\n } catch {\n sendJson(res, 400, { error: \"invalid_json\" });\n return;\n }\n\n if (payload.op === 13) {\n const challenge = payload.d as { plain_token?: unknown; event_ts?: unknown } | undefined;\n if (typeof challenge?.plain_token !== \"string\" || typeof challenge.event_ts !== \"string\") {\n sendJson(res, 400, { error: \"invalid_challenge\" });\n return;\n }\n const signature = sign(\n null,\n Buffer.from(`${challenge.event_ts}${challenge.plain_token}`, \"utf8\"),\n privateKey\n ).toString(\"hex\");\n sendJson(res, 200, { plain_token: challenge.plain_token, signature });\n return;\n }\n\n const timestamp = req.headers[\"x-signature-timestamp\"];\n const signature = req.headers[\"x-signature-ed25519\"];\n if (\n typeof timestamp !== \"string\" ||\n typeof signature !== \"string\" ||\n !validTimestamp(timestamp) ||\n !/^[0-9a-fA-F]{128}$/.test(signature) ||\n !verify(null, Buffer.concat([Buffer.from(timestamp, \"utf8\"), raw]), publicKey, Buffer.from(signature, \"hex\"))\n ) {\n sendJson(res, 403, { error: \"invalid_signature\" });\n return;\n }\n if (payload.op !== 0 || typeof payload.t !== \"string\" || !payload.d || typeof payload.d !== \"object\") {\n sendJson(res, 400, { error: \"invalid_event\" });\n return;\n }\n const eventId = String(payload.id ?? (payload.d as { id?: unknown }).id ?? \"\");\n const dedupKey = eventId ? `${payload.t}:${eventId}` : \"\";\n const lastSeen = dedupKey ? recentEvents.get(dedupKey) : undefined;\n if (lastSeen && Date.now() - lastSeen < MAX_CLOCK_SKEW_MS) {\n sendJson(res, 200, { ok: true });\n return;\n }\n if (pendingCount >= MAX_PENDING_EVENTS) {\n sendJson(res, 503, { error: \"queue_full\" });\n return;\n }\n\n if (dedupKey) {\n recentEvents.set(dedupKey, Date.now());\n if (recentEvents.size > 2048) recentEvents.delete(recentEvents.keys().next().value!);\n }\n pendingCount++;\n pending = pending\n .then(() => dispatchEvent(payload, opts))\n .catch((e: unknown) => {\n log.error(`\u5B98\u65B9 Webhook \u4E8B\u4EF6\u5904\u7406\u5931\u8D25: ${(e as Error).message}`);\n })\n .finally(() => {\n pendingCount--;\n });\n sendJson(res, 200, { ok: true });\n })().catch((e: unknown) => {\n log.error(`\u5B98\u65B9 Webhook \u8BF7\u6C42\u5904\u7406\u5931\u8D25: ${(e as Error).message}`);\n if (!res.headersSent) sendJson(res, 500, { error: \"internal_error\" });\n });\n });\n server.requestTimeout = 10_000;\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject);\n server.listen(opts.port, \"127.0.0.1\", () => {\n server.off(\"error\", reject);\n resolve();\n });\n });\n log.info(`\u5B98\u65B9 Webhook \u5DF2\u76D1\u542C http://127.0.0.1:${opts.port}${opts.path}`);\n return { stop: () => new Promise<void>((resolve, reject) => server.close((e) => (e ? reject(e) : resolve()))) };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["signature"]
7
+ }
package/dist/onebot.js CHANGED
@@ -134,10 +134,6 @@ class OneBotDispatcher {
134
134
  };
135
135
  if (await this.opts.commandRouter.handle(inbound)) return;
136
136
  }
137
- if (!this.opts.db.channelId) {
138
- log.warn("bridge_channel_id \u672A\u914D\u7F6E,\u8DF3\u8FC7 (\u53EF\u7528 reload \u91CD\u8BFB\u914D\u7F6E)");
139
- return;
140
- }
141
137
  await this.forward(this.opts.db, fromId, fromName, text);
142
138
  }
143
139
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/onebot.ts"],
4
- "sourcesContent": ["/**\n * onebot.ts \u2014 OneBot 11 \u534F\u8BAE\u4E8B\u4EF6\u5206\u53D1\u4E0E\u6D88\u606F\u89E3\u6790\u4E2D\u5FC3\n *\n * \u6838\u5FC3\u673A\u5236\uFF1A\n * - \u673A\u5668\u4EBA\u81EA\u8EAB\u8EAB\u4EFD\u6355\u83B7\uFF1A\u4ECE OneBot lifecycle \u5143\u4E8B\u4EF6\u4E2D\u63D0\u53D6 `botSelfId`\uFF0C\u9632\u6B62\u81EA\u8EAB\u6D88\u606F\u56DE\u73AF\n * - \u6D88\u606F\u77ED\u671F\u53BB\u91CD\uFF1A\u7EF4\u62A4 5 \u79D2\u7A97\u53E3\u7684\u9AD8\u901F Map \u5E42\u7B49\u7F13\u5B58\uFF0C\u9632\u8303\u5916\u90E8\u5BA2\u6237\u7AEF\u5076\u53D1\u91CD\u590D\u63A8\u9001\n * - \u590D\u5408\u5BCC\u6587\u672C\u89C4\u6574\u5316\uFF1A\u5C06\u5305\u542B text / at / image / face / reply \u7B49\u591A\u6BB5\u590D\u5408\u5185\u5BB9\u683C\u5F0F\u5316\u4E3A\u6574\u6D01\u6587\u672C\n * - \u76EE\u6807\u7FA4\u4E0E\u547D\u4EE4\u5206\u6D41\uFF1A\u5BF9\u9F50\u914D\u7F6E\u7684\u76EE\u6807\u7FA4 ID\uFF0C\u6307\u4EE4\u4F18\u5148\u4EA4\u7531\u6307\u4EE4\u8DEF\u7531\u5668\u6D88\u8D39\uFF0C\u5176\u4F59\u6D88\u606F\u8F6C\u53D1\u81F3 MC\n */\n\nimport { log } from \"./log.js\";\nimport type { CommandRouter } from \"./commands/index.js\";\nimport { tryForward, type DBServerConfig } from \"./dbserver.js\";\nimport type {\n OneBotAtSegment,\n OneBotEvent,\n OneBotFaceSegment,\n OneBotFileSegment,\n OneBotForwardSegment,\n OneBotGroupMessageEvent,\n OneBotImageSegment,\n OneBotLifecycleEvent,\n OneBotRecordSegment,\n OneBotReplySegment,\n OneBotSegment,\n OneBotTextSegment,\n OneBotVideoSegment,\n} from \"./types.js\";\n\n\n// \u2500\u2500 \u7C7B\u578B\u5B88\u536B \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction isGroupMessageEvent(e: OneBotEvent): e is OneBotGroupMessageEvent {\n return e.post_type === \"message\" && e.message_type === \"group\";\n}\n\nfunction isLifecycleEvent(e: OneBotEvent): e is OneBotLifecycleEvent {\n return e.post_type === \"meta_event\" && e.meta_event_type === \"lifecycle\";\n}\n\nfunction isTextSegment(s: OneBotSegment | undefined): s is OneBotTextSegment {\n return s?.type === \"text\";\n}\nfunction isAtSegment(s: OneBotSegment | undefined): s is OneBotAtSegment {\n return s?.type === \"at\";\n}\nfunction isImageSegment(s: OneBotSegment | undefined): s is OneBotImageSegment {\n return s?.type === \"image\";\n}\nfunction isFaceSegment(s: OneBotSegment | undefined): s is OneBotFaceSegment {\n return s?.type === \"face\";\n}\nfunction isReplySegment(s: OneBotSegment | undefined): s is OneBotReplySegment {\n return s?.type === \"reply\";\n}\nfunction isForwardSegment(s: OneBotSegment | undefined): s is OneBotForwardSegment {\n return s?.type === \"forward\";\n}\nfunction isRecordSegment(s: OneBotSegment | undefined): s is OneBotRecordSegment {\n return s?.type === \"record\";\n}\nfunction isVideoSegment(s: OneBotSegment | undefined): s is OneBotVideoSegment {\n return s?.type === \"video\";\n}\nfunction isFileSegment(s: OneBotSegment | undefined): s is OneBotFileSegment {\n return s?.type === \"file\";\n}\n\n// \u2500\u2500 \u6D88\u606F\u6BB5 \u2192 \u7EAF\u6587\u672C \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n/**\n * OneBot 11 \u6D88\u606F\u6BB5\u6570\u7EC4 (\u6216\u7EAF\u5B57\u7B26\u4E32) \u2192 \u7EAF\u6587\u672C\u3002\n * \u884C\u4E3A\u4E0E\u65E7\u5B9E\u73B0\u5B8C\u5168\u4E00\u81F4:\n * - text: \u539F\u6837\u62FC\u63A5\n * - at: \"@\u6635\u79F0 \" \u6216 \"@qq \" (\u6709 name \u4F18\u5148 name)\n * - \u5176\u5B83: [\u56FE\u7247]/[\u8868\u60C5]/[\u56DE\u590D]/[\u8F6C\u53D1]/[\u8BED\u97F3]/[\u89C6\u9891]/[\u6587\u4EF6]\n * - \u672A\u77E5\u6BB5: \u8DF3\u8FC7\n */\nexport function extractText(message: OneBotGroupMessageEvent[\"message\"]): string {\n if (typeof message === \"string\") return message;\n if (!Array.isArray(message)) return \"\";\n const parts: string[] = [];\n for (const seg of message) {\n if (!seg || typeof seg !== \"object\") continue;\n if (isTextSegment(seg)) {\n const text = seg.data.text;\n if (typeof text === \"string\") parts.push(text);\n } else if (isAtSegment(seg)) {\n const name = seg.data.name ? `@${seg.data.name} ` : `@${seg.data.qq ?? \"?\"} `;\n parts.push(name);\n } else if (isImageSegment(seg)) {\n parts.push(\"[\u56FE\u7247]\");\n } else if (isFaceSegment(seg)) {\n parts.push(\"[\u8868\u60C5]\");\n } else if (isReplySegment(seg)) {\n parts.push(\"[\u56DE\u590D]\");\n } else if (isForwardSegment(seg)) {\n parts.push(\"[\u8F6C\u53D1]\");\n } else if (isRecordSegment(seg)) {\n parts.push(\"[\u8BED\u97F3]\");\n } else if (isVideoSegment(seg)) {\n parts.push(\"[\u89C6\u9891]\");\n } else if (isFileSegment(seg)) {\n parts.push(\"[\u6587\u4EF6]\");\n }\n // \u672A\u77E5\u6BB5:\u8DF3\u8FC7\n }\n return parts.join(\"\").trim();\n}\n\n// \u2500\u2500 \u77ED\u671F message_id \u53BB\u91CD (\u61D2\u6E05\u7406) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst DEDUP_WINDOW_MS = 5_000;\nconst CLEANUP_INTERVAL_MS = 1_000;\n\nexport class RecentMessageDedup {\n private readonly map = new Map<string, number>();\n private lastCleanup = 0;\n\n seen(messageId: string | number | undefined): boolean {\n if (messageId === undefined || messageId === null) return false;\n const key = String(messageId);\n const now = Date.now();\n if (now - this.lastCleanup > CLEANUP_INTERVAL_MS) {\n this.lastCleanup = now;\n for (const [k, ts] of this.map) {\n if (now - ts > DEDUP_WINDOW_MS) this.map.delete(k);\n }\n }\n const prev = this.map.get(key);\n if (prev !== undefined && now - prev < DEDUP_WINDOW_MS) return true;\n this.map.set(key, now);\n return false;\n }\n}\n\n// \u2500\u2500 OneBot \u4E8B\u4EF6\u5206\u53D1\u5668 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nexport interface DispatcherOptions {\n /** \u7FA4 ID \u5B57\u7B26\u4E32,\u7A7A = \u63A5\u6536\u6240\u6709\u7FA4(\u65E7\u884C\u4E3A:\u7A7A\u65F6\u4E0D\u5339\u914D\u4EFB\u4F55\u7FA4,\u4E0D\u4F1A\u89E6\u53D1) */\n qqGroupId: string;\n /** db-server \u914D\u7F6E + \u76EE\u6807 channelId */\n db: DBServerConfig;\n /** QQ \u4FA7\u6307\u4EE4\u8DEF\u7531\uFF1B\u547D\u4E2D\u5219\u4E0D\u8F6C\u53D1 MC */\n commandRouter?: CommandRouter;\n /** \u53EF\u6CE8\u5165\u8F6C\u53D1\uFF08\u5355\u6D4B\uFF09 */\n forward?: typeof tryForward;\n}\n\nexport class OneBotDispatcher {\n private botSelfId: string | null = null;\n private readonly opts: DispatcherOptions;\n private readonly dedup = new RecentMessageDedup();\n private readonly forward: typeof tryForward;\n\n constructor(opts: DispatcherOptions) {\n this.opts = opts;\n this.forward = opts.forward ?? tryForward;\n }\n\n /** \u6D4B\u8BD5/\u8C03\u8BD5\u7528 */\n get selfId(): string | null {\n return this.botSelfId;\n }\n\n /** \u6536\u5230\u4E00\u6761\u539F\u59CB JSON \u5B57\u7B26\u4E32(\u5DF2 parse) */\n async handle(rawEvent: unknown): Promise<void> {\n const event = rawEvent as OneBotEvent;\n if (!event || typeof event !== \"object\") return;\n\n if (isLifecycleEvent(event)) {\n if (event.self_id !== undefined && this.botSelfId === null) {\n this.botSelfId = String(event.self_id);\n log.info(`LLBot self_id = ${this.botSelfId}`);\n }\n return;\n }\n\n if (!isGroupMessageEvent(event)) return;\n if (!this.opts.qqGroupId || String(event.group_id) !== this.opts.qqGroupId) return;\n\n // \u5FAA\u73AF\u9632\u62A4 1: \u8DF3\u8FC7 bot \u81EA\u5DF1\u53D1\u7684\u6D88\u606F\u56DE\u58F0\n if (this.botSelfId && String(event.sender?.user_id) === this.botSelfId) return;\n\n // \u5FAA\u73AF\u9632\u62A4 2: \u77ED\u671F\u53BB\u91CD (LLBot \u5076\u53D1\u91CD\u53D1)\n if (this.dedup.seen(event.message_id)) return;\n\n const text = extractText(event.message);\n if (!text) return;\n\n const senderCard = event.sender?.card;\n const senderNick = event.sender?.nickname;\n const fromName = senderCard || senderNick || `QQ_${event.user_id}`;\n const userId = String(event.user_id);\n const fromId = `qq_${userId}`;\n const groupId = String(event.group_id);\n\n if (this.opts.commandRouter) {\n const role = String(event.sender?.role ?? \"\").toLowerCase();\n const isGroupAdmin = role === \"owner\" || role === \"admin\";\n const inbound = {\n backend: \"llbot\" as const,\n groupId,\n userId,\n userName: fromName,\n text,\n isGroupAdmin,\n ...(event.message_id !== undefined ? { msgId: String(event.message_id) } : {}),\n };\n if (await this.opts.commandRouter.handle(inbound)) return;\n }\n\n if (!this.opts.db.channelId) {\n log.warn(\"bridge_channel_id \u672A\u914D\u7F6E,\u8DF3\u8FC7 (\u53EF\u7528 reload \u91CD\u8BFB\u914D\u7F6E)\");\n return;\n }\n\n await this.forward(this.opts.db, fromId, fromName, text);\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * onebot.ts \u2014 OneBot 11 \u534F\u8BAE\u4E8B\u4EF6\u5206\u53D1\u4E0E\u6D88\u606F\u89E3\u6790\u4E2D\u5FC3\n *\n * \u6838\u5FC3\u673A\u5236\uFF1A\n * - \u673A\u5668\u4EBA\u81EA\u8EAB\u8EAB\u4EFD\u6355\u83B7\uFF1A\u4ECE OneBot lifecycle \u5143\u4E8B\u4EF6\u4E2D\u63D0\u53D6 `botSelfId`\uFF0C\u9632\u6B62\u81EA\u8EAB\u6D88\u606F\u56DE\u73AF\n * - \u6D88\u606F\u77ED\u671F\u53BB\u91CD\uFF1A\u7EF4\u62A4 5 \u79D2\u7A97\u53E3\u7684\u9AD8\u901F Map \u5E42\u7B49\u7F13\u5B58\uFF0C\u9632\u8303\u5916\u90E8\u5BA2\u6237\u7AEF\u5076\u53D1\u91CD\u590D\u63A8\u9001\n * - \u590D\u5408\u5BCC\u6587\u672C\u89C4\u6574\u5316\uFF1A\u5C06\u5305\u542B text / at / image / face / reply \u7B49\u591A\u6BB5\u590D\u5408\u5185\u5BB9\u683C\u5F0F\u5316\u4E3A\u6574\u6D01\u6587\u672C\n * - \u76EE\u6807\u7FA4\u4E0E\u547D\u4EE4\u5206\u6D41\uFF1A\u5BF9\u9F50\u914D\u7F6E\u7684\u76EE\u6807\u7FA4 ID\uFF0C\u6307\u4EE4\u4F18\u5148\u4EA4\u7531\u6307\u4EE4\u8DEF\u7531\u5668\u6D88\u8D39\uFF0C\u5176\u4F59\u6D88\u606F\u8F6C\u53D1\u81F3 MC\n */\n\nimport { log } from \"./log.js\";\nimport type { CommandRouter } from \"./commands/index.js\";\nimport { tryForward, type DBServerConfig } from \"./dbserver.js\";\nimport type {\n OneBotAtSegment,\n OneBotEvent,\n OneBotFaceSegment,\n OneBotFileSegment,\n OneBotForwardSegment,\n OneBotGroupMessageEvent,\n OneBotImageSegment,\n OneBotLifecycleEvent,\n OneBotRecordSegment,\n OneBotReplySegment,\n OneBotSegment,\n OneBotTextSegment,\n OneBotVideoSegment,\n} from \"./types.js\";\n\n\n// \u2500\u2500 \u7C7B\u578B\u5B88\u536B \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction isGroupMessageEvent(e: OneBotEvent): e is OneBotGroupMessageEvent {\n return e.post_type === \"message\" && e.message_type === \"group\";\n}\n\nfunction isLifecycleEvent(e: OneBotEvent): e is OneBotLifecycleEvent {\n return e.post_type === \"meta_event\" && e.meta_event_type === \"lifecycle\";\n}\n\nfunction isTextSegment(s: OneBotSegment | undefined): s is OneBotTextSegment {\n return s?.type === \"text\";\n}\nfunction isAtSegment(s: OneBotSegment | undefined): s is OneBotAtSegment {\n return s?.type === \"at\";\n}\nfunction isImageSegment(s: OneBotSegment | undefined): s is OneBotImageSegment {\n return s?.type === \"image\";\n}\nfunction isFaceSegment(s: OneBotSegment | undefined): s is OneBotFaceSegment {\n return s?.type === \"face\";\n}\nfunction isReplySegment(s: OneBotSegment | undefined): s is OneBotReplySegment {\n return s?.type === \"reply\";\n}\nfunction isForwardSegment(s: OneBotSegment | undefined): s is OneBotForwardSegment {\n return s?.type === \"forward\";\n}\nfunction isRecordSegment(s: OneBotSegment | undefined): s is OneBotRecordSegment {\n return s?.type === \"record\";\n}\nfunction isVideoSegment(s: OneBotSegment | undefined): s is OneBotVideoSegment {\n return s?.type === \"video\";\n}\nfunction isFileSegment(s: OneBotSegment | undefined): s is OneBotFileSegment {\n return s?.type === \"file\";\n}\n\n// \u2500\u2500 \u6D88\u606F\u6BB5 \u2192 \u7EAF\u6587\u672C \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n/**\n * OneBot 11 \u6D88\u606F\u6BB5\u6570\u7EC4 (\u6216\u7EAF\u5B57\u7B26\u4E32) \u2192 \u7EAF\u6587\u672C\u3002\n * \u884C\u4E3A\u4E0E\u65E7\u5B9E\u73B0\u5B8C\u5168\u4E00\u81F4:\n * - text: \u539F\u6837\u62FC\u63A5\n * - at: \"@\u6635\u79F0 \" \u6216 \"@qq \" (\u6709 name \u4F18\u5148 name)\n * - \u5176\u5B83: [\u56FE\u7247]/[\u8868\u60C5]/[\u56DE\u590D]/[\u8F6C\u53D1]/[\u8BED\u97F3]/[\u89C6\u9891]/[\u6587\u4EF6]\n * - \u672A\u77E5\u6BB5: \u8DF3\u8FC7\n */\nexport function extractText(message: OneBotGroupMessageEvent[\"message\"]): string {\n if (typeof message === \"string\") return message;\n if (!Array.isArray(message)) return \"\";\n const parts: string[] = [];\n for (const seg of message) {\n if (!seg || typeof seg !== \"object\") continue;\n if (isTextSegment(seg)) {\n const text = seg.data.text;\n if (typeof text === \"string\") parts.push(text);\n } else if (isAtSegment(seg)) {\n const name = seg.data.name ? `@${seg.data.name} ` : `@${seg.data.qq ?? \"?\"} `;\n parts.push(name);\n } else if (isImageSegment(seg)) {\n parts.push(\"[\u56FE\u7247]\");\n } else if (isFaceSegment(seg)) {\n parts.push(\"[\u8868\u60C5]\");\n } else if (isReplySegment(seg)) {\n parts.push(\"[\u56DE\u590D]\");\n } else if (isForwardSegment(seg)) {\n parts.push(\"[\u8F6C\u53D1]\");\n } else if (isRecordSegment(seg)) {\n parts.push(\"[\u8BED\u97F3]\");\n } else if (isVideoSegment(seg)) {\n parts.push(\"[\u89C6\u9891]\");\n } else if (isFileSegment(seg)) {\n parts.push(\"[\u6587\u4EF6]\");\n }\n // \u672A\u77E5\u6BB5:\u8DF3\u8FC7\n }\n return parts.join(\"\").trim();\n}\n\n// \u2500\u2500 \u77ED\u671F message_id \u53BB\u91CD (\u61D2\u6E05\u7406) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst DEDUP_WINDOW_MS = 5_000;\nconst CLEANUP_INTERVAL_MS = 1_000;\n\nexport class RecentMessageDedup {\n private readonly map = new Map<string, number>();\n private lastCleanup = 0;\n\n seen(messageId: string | number | undefined): boolean {\n if (messageId === undefined || messageId === null) return false;\n const key = String(messageId);\n const now = Date.now();\n if (now - this.lastCleanup > CLEANUP_INTERVAL_MS) {\n this.lastCleanup = now;\n for (const [k, ts] of this.map) {\n if (now - ts > DEDUP_WINDOW_MS) this.map.delete(k);\n }\n }\n const prev = this.map.get(key);\n if (prev !== undefined && now - prev < DEDUP_WINDOW_MS) return true;\n this.map.set(key, now);\n return false;\n }\n}\n\n// \u2500\u2500 OneBot \u4E8B\u4EF6\u5206\u53D1\u5668 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nexport interface DispatcherOptions {\n /** \u7FA4 ID \u5B57\u7B26\u4E32,\u7A7A = \u63A5\u6536\u6240\u6709\u7FA4(\u65E7\u884C\u4E3A:\u7A7A\u65F6\u4E0D\u5339\u914D\u4EFB\u4F55\u7FA4,\u4E0D\u4F1A\u89E6\u53D1) */\n qqGroupId: string;\n /** db-server \u914D\u7F6E + \u76EE\u6807 channelId */\n db: DBServerConfig;\n /** QQ \u4FA7\u6307\u4EE4\u8DEF\u7531\uFF1B\u547D\u4E2D\u5219\u4E0D\u8F6C\u53D1 MC */\n commandRouter?: CommandRouter;\n /** \u53EF\u6CE8\u5165\u8F6C\u53D1\uFF08\u5355\u6D4B\uFF09 */\n forward?: typeof tryForward;\n}\n\nexport class OneBotDispatcher {\n private botSelfId: string | null = null;\n private readonly opts: DispatcherOptions;\n private readonly dedup = new RecentMessageDedup();\n private readonly forward: typeof tryForward;\n\n constructor(opts: DispatcherOptions) {\n this.opts = opts;\n this.forward = opts.forward ?? tryForward;\n }\n\n /** \u6D4B\u8BD5/\u8C03\u8BD5\u7528 */\n get selfId(): string | null {\n return this.botSelfId;\n }\n\n /** \u6536\u5230\u4E00\u6761\u539F\u59CB JSON \u5B57\u7B26\u4E32(\u5DF2 parse) */\n async handle(rawEvent: unknown): Promise<void> {\n const event = rawEvent as OneBotEvent;\n if (!event || typeof event !== \"object\") return;\n\n if (isLifecycleEvent(event)) {\n if (event.self_id !== undefined && this.botSelfId === null) {\n this.botSelfId = String(event.self_id);\n log.info(`LLBot self_id = ${this.botSelfId}`);\n }\n return;\n }\n\n if (!isGroupMessageEvent(event)) return;\n if (!this.opts.qqGroupId || String(event.group_id) !== this.opts.qqGroupId) return;\n\n // \u5FAA\u73AF\u9632\u62A4 1: \u8DF3\u8FC7 bot \u81EA\u5DF1\u53D1\u7684\u6D88\u606F\u56DE\u58F0\n if (this.botSelfId && String(event.sender?.user_id) === this.botSelfId) return;\n\n // \u5FAA\u73AF\u9632\u62A4 2: \u77ED\u671F\u53BB\u91CD (LLBot \u5076\u53D1\u91CD\u53D1)\n if (this.dedup.seen(event.message_id)) return;\n\n const text = extractText(event.message);\n if (!text) return;\n\n const senderCard = event.sender?.card;\n const senderNick = event.sender?.nickname;\n const fromName = senderCard || senderNick || `QQ_${event.user_id}`;\n const userId = String(event.user_id);\n const fromId = `qq_${userId}`;\n const groupId = String(event.group_id);\n\n if (this.opts.commandRouter) {\n const role = String(event.sender?.role ?? \"\").toLowerCase();\n const isGroupAdmin = role === \"owner\" || role === \"admin\";\n const inbound = {\n backend: \"llbot\" as const,\n groupId,\n userId,\n userName: fromName,\n text,\n isGroupAdmin,\n ...(event.message_id !== undefined ? { msgId: String(event.message_id) } : {}),\n };\n if (await this.opts.commandRouter.handle(inbound)) return;\n }\n\n await this.forward(this.opts.db, fromId, fromName, text);\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sfmc-bds/qq-bridge",
3
- "version": "0.2.0-beta.9",
3
+ "version": "0.2.0",
4
4
  "description": "SFMC QQ ↔ MC bridge — official QQ Open Platform Bot and/or LLBot OneBot 11",
5
5
  "author": "Shiroha7z",
6
6
  "license": "AGPL-3.0-only",
@@ -28,14 +28,15 @@
28
28
  "main": "dist/index.js",
29
29
  "files": [
30
30
  "dist",
31
+ "deploy",
31
32
  "README.md"
32
33
  ],
33
34
  "dependencies": {
34
35
  "ws": "^8.21.1",
35
- "@sfmc-bds/sdk": "0.2.0-beta.14"
36
+ "@sfmc-bds/sdk": "0.2.0"
36
37
  },
37
38
  "devDependencies": {
38
- "@sfmc-bds/tools": "0.2.0-beta.15"
39
+ "@sfmc-bds/tools": "0.2.0"
39
40
  },
40
41
  "publishConfig": {
41
42
  "access": "public",
@@ -47,7 +48,6 @@
47
48
  "dev": "tsx src/index.ts",
48
49
  "format": "prettier --write \"src/**/*.{ts,tsx,js,json}\"",
49
50
  "start": "node dist/index.js",
50
- "test": "pnpm run build && node --test \"dist/**/*.test.js\"",
51
51
  "typecheck": "tsc7 --noEmit"
52
52
  }
53
53
  }
@@ -1,43 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import {
4
- buildJoinSettingsPanel,
5
- parseCfgInteractionData
6
- } from "./join-settings-ui.js";
7
- import { renderOfficial } from "./render.js";
8
- test("parseCfgInteractionData", () => {
9
- assert.deepEqual(parseCfgInteractionData("cfg:allowlist:off"), {
10
- field: "allowlist_enabled",
11
- value: false
12
- });
13
- assert.deepEqual(parseCfgInteractionData("cfg:approval:on"), {
14
- field: "require_approval",
15
- value: true
16
- });
17
- assert.equal(parseCfgInteractionData("join:approve:x"), null);
18
- });
19
- test("official \u914D\u7F6E\u9762\u677F\u4E3A INTERACTION \u56DE\u8C03\u6309\u94AE", () => {
20
- const panel = buildJoinSettingsPanel({
21
- settings: { allowlist_enabled: true, require_approval: false, treat_group_admins_as_admins: false },
22
- backend: "official",
23
- adminOpenids: ["admin1"]
24
- });
25
- assert.equal(panel.buttons?.length, 2);
26
- assert.equal(panel.buttons?.[0]?.actionType, 1);
27
- assert.equal(panel.buttons?.[0]?.command, "cfg:allowlist:off");
28
- assert.equal(panel.buttons?.[1]?.command, "cfg:approval:on");
29
- const rendered = renderOfficial(panel);
30
- assert.equal(rendered.msgType, 2);
31
- assert.equal(rendered.keyboardButtons?.[0]?.actionType, 1);
32
- assert.equal(rendered.keyboardButtons?.[0]?.data, "cfg:allowlist:off");
33
- });
34
- test("llbot \u914D\u7F6E\u9762\u677F\u4E3A\u6587\u672C\u547D\u4EE4\u7F16\u53F7", () => {
35
- const panel = buildJoinSettingsPanel({
36
- settings: { allowlist_enabled: false, require_approval: true },
37
- backend: "llbot"
38
- });
39
- assert.equal(panel.buttons?.[0]?.command, "\u914D\u7F6E \u767D\u540D\u5355 \u5F00");
40
- assert.equal(panel.buttons?.[1]?.command, "\u914D\u7F6E \u5BA1\u6279 \u5173");
41
- assert.equal(panel.buttons?.[0]?.actionType, void 0);
42
- });
43
- //# sourceMappingURL=join-settings-ui.test.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../src/commands/join-settings-ui.test.ts"],
4
- "sourcesContent": ["/**\n * commands/join-settings-ui.test.ts\n */\nimport assert from \"node:assert/strict\";\nimport test from \"node:test\";\nimport {\n buildJoinSettingsPanel,\n parseCfgInteractionData,\n} from \"./join-settings-ui.js\";\nimport { renderOfficial } from \"./render.js\";\n\ntest(\"parseCfgInteractionData\", () => {\n assert.deepEqual(parseCfgInteractionData(\"cfg:allowlist:off\"), {\n field: \"allowlist_enabled\",\n value: false,\n });\n assert.deepEqual(parseCfgInteractionData(\"cfg:approval:on\"), {\n field: \"require_approval\",\n value: true,\n });\n assert.equal(parseCfgInteractionData(\"join:approve:x\"), null);\n});\n\ntest(\"official \u914D\u7F6E\u9762\u677F\u4E3A INTERACTION \u56DE\u8C03\u6309\u94AE\", () => {\n const panel = buildJoinSettingsPanel({\n settings: { allowlist_enabled: true, require_approval: false, treat_group_admins_as_admins: false },\n backend: \"official\",\n adminOpenids: [\"admin1\"],\n });\n assert.equal(panel.buttons?.length, 2);\n assert.equal(panel.buttons?.[0]?.actionType, 1);\n assert.equal(panel.buttons?.[0]?.command, \"cfg:allowlist:off\");\n assert.equal(panel.buttons?.[1]?.command, \"cfg:approval:on\");\n const rendered = renderOfficial(panel);\n assert.equal(rendered.msgType, 2);\n assert.equal(rendered.keyboardButtons?.[0]?.actionType, 1);\n assert.equal(rendered.keyboardButtons?.[0]?.data, \"cfg:allowlist:off\");\n});\n\ntest(\"llbot \u914D\u7F6E\u9762\u677F\u4E3A\u6587\u672C\u547D\u4EE4\u7F16\u53F7\", () => {\n const panel = buildJoinSettingsPanel({\n settings: { allowlist_enabled: false, require_approval: true },\n backend: \"llbot\",\n });\n assert.equal(panel.buttons?.[0]?.command, \"\u914D\u7F6E \u767D\u540D\u5355 \u5F00\");\n assert.equal(panel.buttons?.[1]?.command, \"\u914D\u7F6E \u5BA1\u6279 \u5173\");\n assert.equal(panel.buttons?.[0]?.actionType, undefined);\n});\n"],
5
- "mappings": "AAGA,OAAO,YAAY;AACnB,OAAO,UAAU;AACjB;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,sBAAsB;AAE/B,KAAK,2BAA2B,MAAM;AACpC,SAAO,UAAU,wBAAwB,mBAAmB,GAAG;AAAA,IAC7D,OAAO;AAAA,IACP,OAAO;AAAA,EACT,CAAC;AACD,SAAO,UAAU,wBAAwB,iBAAiB,GAAG;AAAA,IAC3D,OAAO;AAAA,IACP,OAAO;AAAA,EACT,CAAC;AACD,SAAO,MAAM,wBAAwB,gBAAgB,GAAG,IAAI;AAC9D,CAAC;AAED,KAAK,gFAAmC,MAAM;AAC5C,QAAM,QAAQ,uBAAuB;AAAA,IACnC,UAAU,EAAE,mBAAmB,MAAM,kBAAkB,OAAO,8BAA8B,MAAM;AAAA,IAClG,SAAS;AAAA,IACT,cAAc,CAAC,QAAQ;AAAA,EACzB,CAAC;AACD,SAAO,MAAM,MAAM,SAAS,QAAQ,CAAC;AACrC,SAAO,MAAM,MAAM,UAAU,CAAC,GAAG,YAAY,CAAC;AAC9C,SAAO,MAAM,MAAM,UAAU,CAAC,GAAG,SAAS,mBAAmB;AAC7D,SAAO,MAAM,MAAM,UAAU,CAAC,GAAG,SAAS,iBAAiB;AAC3D,QAAM,WAAW,eAAe,KAAK;AACrC,SAAO,MAAM,SAAS,SAAS,CAAC;AAChC,SAAO,MAAM,SAAS,kBAAkB,CAAC,GAAG,YAAY,CAAC;AACzD,SAAO,MAAM,SAAS,kBAAkB,CAAC,GAAG,MAAM,mBAAmB;AACvE,CAAC;AAED,KAAK,4EAAqB,MAAM;AAC9B,QAAM,QAAQ,uBAAuB;AAAA,IACnC,UAAU,EAAE,mBAAmB,OAAO,kBAAkB,KAAK;AAAA,IAC7D,SAAS;AAAA,EACX,CAAC;AACD,SAAO,MAAM,MAAM,UAAU,CAAC,GAAG,SAAS,wCAAU;AACpD,SAAO,MAAM,MAAM,UAAU,CAAC,GAAG,SAAS,kCAAS;AACnD,SAAO,MAAM,MAAM,UAAU,CAAC,GAAG,YAAY,MAAS;AACxD,CAAC;",
6
- "names": []
7
- }
@@ -1,27 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { CommandRegistry } from "./registry.js";
4
- import { registerBuiltinCommands } from "./handlers.js";
5
- import { buildMenuItems, buildPanelItems } from "./menu-panel-sync.js";
6
- test("buildMenuItems / buildPanelItems \u4E0E registry \u5BF9\u9F50\u4E14 \u226410/20", () => {
7
- const registry = new CommandRegistry();
8
- registerBuiltinCommands(registry);
9
- const userCount = registry.userMenu().length;
10
- const total = registry.all().length;
11
- const menu = buildMenuItems(registry);
12
- const panel = buildPanelItems(registry);
13
- assert.ok(menu.length >= 5);
14
- assert.ok(menu.length <= 10);
15
- assert.equal(menu.length, Math.min(10, userCount));
16
- assert.equal(panel.length, Math.min(20, total));
17
- assert.equal(menu[0]?.type, "send_message");
18
- assert.equal(panel[0]?.type, "command");
19
- assert.ok(menu.some((i) => i.send_message === "/ping"));
20
- assert.ok(menu.some((i) => i.send_message === "/admin"));
21
- assert.ok(!menu.some((i) => i.send_message === "/kick"));
22
- assert.ok(panel.some((i) => i.name === "/bind"));
23
- assert.ok(panel.some((i) => i.name === "/kick"));
24
- assert.ok(panel.every((i) => (i.name?.length ?? 0) <= 14));
25
- assert.ok(menu.every((i) => (i.name?.length ?? 0) <= 10));
26
- });
27
- //# sourceMappingURL=menu-panel-sync.test.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../src/commands/menu-panel-sync.test.ts"],
4
- "sourcesContent": ["/**\n * commands/menu-panel-sync.test.ts\n */\nimport assert from \"node:assert/strict\";\nimport test from \"node:test\";\nimport { CommandRegistry } from \"./registry.js\";\nimport { registerBuiltinCommands } from \"./handlers.js\";\nimport { buildMenuItems, buildPanelItems } from \"./menu-panel-sync.js\";\n\ntest(\"buildMenuItems / buildPanelItems \u4E0E registry \u5BF9\u9F50\u4E14 \u226410/20\", () => {\n const registry = new CommandRegistry();\n registerBuiltinCommands(registry);\n const userCount = registry.userMenu().length;\n const total = registry.all().length;\n const menu = buildMenuItems(registry);\n const panel = buildPanelItems(registry);\n assert.ok(menu.length >= 5);\n assert.ok(menu.length <= 10);\n assert.equal(menu.length, Math.min(10, userCount));\n assert.equal(panel.length, Math.min(20, total));\n assert.equal(menu[0]?.type, \"send_message\");\n assert.equal(panel[0]?.type, \"command\");\n assert.ok(menu.some((i) => i.send_message === \"/ping\"));\n assert.ok(menu.some((i) => i.send_message === \"/admin\"));\n // \u7BA1\u7406\u9879\u4E0D\u8FDB C2C \u4E3B\u83DC\u5355\n assert.ok(!menu.some((i) => i.send_message === \"/kick\"));\n // \u9762\u677F\u65E0\u72EC\u7ACB command \u5B57\u6BB5\uFF1Aname \u5373\u586B\u5165\u8F93\u5165\u6846\u5185\u5BB9\uFF1B\u53EF\u542B\u7BA1\u7406\u9879\n assert.ok(panel.some((i) => i.name === \"/bind\"));\n assert.ok(panel.some((i) => i.name === \"/kick\"));\n assert.ok(panel.every((i) => (i.name?.length ?? 0) <= 14));\n assert.ok(menu.every((i) => (i.name?.length ?? 0) <= 10));\n});\n"],
5
- "mappings": "AAGA,OAAO,YAAY;AACnB,OAAO,UAAU;AACjB,SAAS,uBAAuB;AAChC,SAAS,+BAA+B;AACxC,SAAS,gBAAgB,uBAAuB;AAEhD,KAAK,mFAA0D,MAAM;AACnE,QAAM,WAAW,IAAI,gBAAgB;AACrC,0BAAwB,QAAQ;AAChC,QAAM,YAAY,SAAS,SAAS,EAAE;AACtC,QAAM,QAAQ,SAAS,IAAI,EAAE;AAC7B,QAAM,OAAO,eAAe,QAAQ;AACpC,QAAM,QAAQ,gBAAgB,QAAQ;AACtC,SAAO,GAAG,KAAK,UAAU,CAAC;AAC1B,SAAO,GAAG,KAAK,UAAU,EAAE;AAC3B,SAAO,MAAM,KAAK,QAAQ,KAAK,IAAI,IAAI,SAAS,CAAC;AACjD,SAAO,MAAM,MAAM,QAAQ,KAAK,IAAI,IAAI,KAAK,CAAC;AAC9C,SAAO,MAAM,KAAK,CAAC,GAAG,MAAM,cAAc;AAC1C,SAAO,MAAM,MAAM,CAAC,GAAG,MAAM,SAAS;AACtC,SAAO,GAAG,KAAK,KAAK,CAAC,MAAM,EAAE,iBAAiB,OAAO,CAAC;AACtD,SAAO,GAAG,KAAK,KAAK,CAAC,MAAM,EAAE,iBAAiB,QAAQ,CAAC;AAEvD,SAAO,GAAG,CAAC,KAAK,KAAK,CAAC,MAAM,EAAE,iBAAiB,OAAO,CAAC;AAEvD,SAAO,GAAG,MAAM,KAAK,CAAC,MAAM,EAAE,SAAS,OAAO,CAAC;AAC/C,SAAO,GAAG,MAAM,KAAK,CAAC,MAAM,EAAE,SAAS,OAAO,CAAC;AAC/C,SAAO,GAAG,MAAM,MAAM,CAAC,OAAO,EAAE,MAAM,UAAU,MAAM,EAAE,CAAC;AACzD,SAAO,GAAG,KAAK,MAAM,CAAC,OAAO,EAAE,MAAM,UAAU,MAAM,EAAE,CAAC;AAC1D,CAAC;",
6
- "names": []
7
- }
@@ -1,44 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { renderLlbot, renderOfficial } from "./render.js";
4
- const menuResult = {
5
- text: "SFMC QQ \u6307\u4EE4\n\xB7 menu \u2014 \u663E\u793A\u6307\u4EE4\u83DC\u5355",
6
- markdown: "## SFMC QQ \u6307\u4EE4\n\n- **menu**\uFF1A\u663E\u793A\u6307\u4EE4\u83DC\u5355",
7
- buttons: [
8
- { id: "cmd_menu", label: "menu", command: "/menu" },
9
- { id: "cmd_ping", label: "ping", command: "/ping" }
10
- ]
11
- };
12
- test("renderOfficial \u5E26\u952E\u76D8\u7528 markdown", () => {
13
- const p = renderOfficial(menuResult);
14
- assert.equal(p.msgType, 2);
15
- assert.equal(p.markdown, menuResult.markdown);
16
- assert.equal(p.keyboardButtons?.length, 2);
17
- assert.equal(p.keyboardButtons?.[1]?.data, "/ping");
18
- });
19
- test("renderOfficial \u7EAF\u6587\u672C", () => {
20
- const p = renderOfficial({ text: "pong \xB7 backend=official" });
21
- assert.equal(p.msgType, 0);
22
- assert.equal(p.content, "pong \xB7 backend=official");
23
- assert.equal(p.keyboardButtons, void 0);
24
- });
25
- test("renderLlbot \u7F16\u53F7\u884C", () => {
26
- const p = renderLlbot(menuResult);
27
- assert.match(p.text, /\[1\] menu/);
28
- assert.match(p.text, /\[2\] ping/);
29
- assert.match(p.text, /60 秒/);
30
- });
31
- test("renderLlbot \u6B63\u6587\u5DF2\u7F16\u53F7\u65F6\u4E0D\u91CD\u590D [n]", () => {
32
- const r = {
33
- text: "SFMC \u6307\u4EE4\n\u8BF4\u660E\n\n1. ping \u2014 \u8FDE\u901A\n2. \u72B6\u6001 \u2014 \u6458\u8981",
34
- buttons: [
35
- { id: "cmd_ping", label: "ping", command: "/ping" },
36
- { id: "cmd_status", label: "\u72B6\u6001", command: "/status" }
37
- ]
38
- };
39
- const p = renderLlbot(r);
40
- assert.match(p.text, /1\. ping/);
41
- assert.doesNotMatch(p.text, /\[1\]/);
42
- assert.match(p.text, /60 秒/);
43
- });
44
- //# sourceMappingURL=render.test.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../src/commands/render.test.ts"],
4
- "sourcesContent": ["/**\n * commands/render.test.ts\n */\nimport assert from \"node:assert/strict\";\nimport test from \"node:test\";\nimport { renderLlbot, renderOfficial } from \"./render.js\";\nimport type { CommandResult } from \"./types.js\";\n\nconst menuResult: CommandResult = {\n text: \"SFMC QQ \u6307\u4EE4\\n\u00B7 menu \u2014 \u663E\u793A\u6307\u4EE4\u83DC\u5355\",\n markdown: \"## SFMC QQ \u6307\u4EE4\\n\\n- **menu**\uFF1A\u663E\u793A\u6307\u4EE4\u83DC\u5355\",\n buttons: [\n { id: \"cmd_menu\", label: \"menu\", command: \"/menu\" },\n { id: \"cmd_ping\", label: \"ping\", command: \"/ping\" },\n ],\n};\n\ntest(\"renderOfficial \u5E26\u952E\u76D8\u7528 markdown\", () => {\n const p = renderOfficial(menuResult);\n assert.equal(p.msgType, 2);\n assert.equal(p.markdown, menuResult.markdown);\n assert.equal(p.keyboardButtons?.length, 2);\n assert.equal(p.keyboardButtons?.[1]?.data, \"/ping\");\n});\n\ntest(\"renderOfficial \u7EAF\u6587\u672C\", () => {\n const p = renderOfficial({ text: \"pong \u00B7 backend=official\" });\n assert.equal(p.msgType, 0);\n assert.equal(p.content, \"pong \u00B7 backend=official\");\n assert.equal(p.keyboardButtons, undefined);\n});\n\ntest(\"renderLlbot \u7F16\u53F7\u884C\", () => {\n const p = renderLlbot(menuResult);\n assert.match(p.text, /\\[1\\] menu/);\n assert.match(p.text, /\\[2\\] ping/);\n assert.match(p.text, /60 \u79D2/);\n});\n\ntest(\"renderLlbot \u6B63\u6587\u5DF2\u7F16\u53F7\u65F6\u4E0D\u91CD\u590D [n]\", () => {\n const r: CommandResult = {\n text: \"SFMC \u6307\u4EE4\\n\u8BF4\u660E\\n\\n1. ping \u2014 \u8FDE\u901A\\n2. \u72B6\u6001 \u2014 \u6458\u8981\",\n buttons: [\n { id: \"cmd_ping\", label: \"ping\", command: \"/ping\" },\n { id: \"cmd_status\", label: \"\u72B6\u6001\", command: \"/status\" },\n ],\n };\n const p = renderLlbot(r);\n assert.match(p.text, /1\\. ping/);\n assert.doesNotMatch(p.text, /\\[1\\]/);\n assert.match(p.text, /60 \u79D2/);\n});\n"],
5
- "mappings": "AAGA,OAAO,YAAY;AACnB,OAAO,UAAU;AACjB,SAAS,aAAa,sBAAsB;AAG5C,MAAM,aAA4B;AAAA,EAChC,MAAM;AAAA,EACN,UAAU;AAAA,EACV,SAAS;AAAA,IACP,EAAE,IAAI,YAAY,OAAO,QAAQ,SAAS,QAAQ;AAAA,IAClD,EAAE,IAAI,YAAY,OAAO,QAAQ,SAAS,QAAQ;AAAA,EACpD;AACF;AAEA,KAAK,oDAAgC,MAAM;AACzC,QAAM,IAAI,eAAe,UAAU;AACnC,SAAO,MAAM,EAAE,SAAS,CAAC;AACzB,SAAO,MAAM,EAAE,UAAU,WAAW,QAAQ;AAC5C,SAAO,MAAM,EAAE,iBAAiB,QAAQ,CAAC;AACzC,SAAO,MAAM,EAAE,kBAAkB,CAAC,GAAG,MAAM,OAAO;AACpD,CAAC;AAED,KAAK,qCAAsB,MAAM;AAC/B,QAAM,IAAI,eAAe,EAAE,MAAM,6BAA0B,CAAC;AAC5D,SAAO,MAAM,EAAE,SAAS,CAAC;AACzB,SAAO,MAAM,EAAE,SAAS,4BAAyB;AACjD,SAAO,MAAM,EAAE,iBAAiB,MAAS;AAC3C,CAAC;AAED,KAAK,kCAAmB,MAAM;AAC5B,QAAM,IAAI,YAAY,UAAU;AAChC,SAAO,MAAM,EAAE,MAAM,YAAY;AACjC,SAAO,MAAM,EAAE,MAAM,YAAY;AACjC,SAAO,MAAM,EAAE,MAAM,MAAM;AAC7B,CAAC;AAED,KAAK,0EAA6B,MAAM;AACtC,QAAM,IAAmB;AAAA,IACvB,MAAM;AAAA,IACN,SAAS;AAAA,MACP,EAAE,IAAI,YAAY,OAAO,QAAQ,SAAS,QAAQ;AAAA,MAClD,EAAE,IAAI,cAAc,OAAO,gBAAM,SAAS,UAAU;AAAA,IACtD;AAAA,EACF;AACA,QAAM,IAAI,YAAY,CAAC;AACvB,SAAO,MAAM,EAAE,MAAM,UAAU;AAC/B,SAAO,aAAa,EAAE,MAAM,OAAO;AACnC,SAAO,MAAM,EAAE,MAAM,MAAM;AAC7B,CAAC;",
6
- "names": []
7
- }
@@ -1,92 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { registerBuiltinCommands } from "./handlers.js";
4
- import { PendingChoiceStore } from "./pending.js";
5
- import { CommandRegistry } from "./registry.js";
6
- import { CommandRouter } from "./router.js";
7
- function makeReplyCapture() {
8
- const sent = [];
9
- return {
10
- sent,
11
- port: {
12
- async send(_t, result) {
13
- sent.push(result);
14
- }
15
- }
16
- };
17
- }
18
- function makeRouter(reply) {
19
- const registry = new CommandRegistry();
20
- registerBuiltinCommands(registry);
21
- return new CommandRouter({
22
- registry,
23
- pending: new PendingChoiceStore(6e4),
24
- reply,
25
- startedAt: Date.now() - 5e3,
26
- runtimeInfo: { sandbox: false, appIdHint: "1905\u20266765" }
27
- });
28
- }
29
- test("router \u547D\u4E2D ping", async () => {
30
- const { port, sent } = makeReplyCapture();
31
- const router = makeRouter(port);
32
- const inbound = {
33
- backend: "official",
34
- groupId: "G",
35
- userId: "U",
36
- userName: "Alice",
37
- text: "/ping",
38
- msgId: "m1"
39
- };
40
- assert.equal(await router.handle(inbound), true);
41
- assert.equal(sent.length, 1);
42
- assert.match(sent[0].text, /^pong/);
43
- });
44
- test("registry \u9996\u8BCD\u5339\u914D\u300C\u7533\u8BF7\u5165\u670D Steve\u300D", () => {
45
- const registry = new CommandRegistry();
46
- registerBuiltinCommands(registry);
47
- const cmd = registry.resolve("\u7533\u8BF7\u5165\u670D Steve");
48
- assert.ok(cmd);
49
- assert.equal(cmd.name, "join");
50
- assert.equal(registry.resolve("\u8E22\u4EBA Alex")?.name, "kick");
51
- });
52
- test("router \u672A\u547D\u4E2D\u900F\u4F20", async () => {
53
- const { port, sent } = makeReplyCapture();
54
- const router = makeRouter(port);
55
- assert.equal(
56
- await router.handle({
57
- backend: "official",
58
- groupId: "G",
59
- userId: "U",
60
- userName: "A",
61
- text: "hello world"
62
- }),
63
- false
64
- );
65
- assert.equal(sent.length, 0);
66
- });
67
- test("router llbot \u7F16\u53F7\u4F1A\u8BDD", async () => {
68
- const { port, sent } = makeReplyCapture();
69
- const router = makeRouter(port);
70
- const base = {
71
- backend: "llbot",
72
- groupId: "123",
73
- userId: "99",
74
- userName: "Bob",
75
- text: "\u83DC\u5355"
76
- };
77
- assert.equal(await router.handle(base), true);
78
- assert.equal(sent.length, 1);
79
- assert.ok(sent[0].buttons && sent[0].buttons.length >= 2);
80
- assert.equal(
81
- await router.handle({ ...base, text: "1" }),
82
- true
83
- );
84
- assert.equal(sent.length, 2);
85
- assert.match(sent[1].text, /^pong/);
86
- });
87
- test("registry normalizeTrigger", async () => {
88
- const { normalizeTrigger } = await import("./registry.js");
89
- assert.equal(normalizeTrigger(" /Ping "), "ping");
90
- assert.equal(normalizeTrigger("\u83DC\u5355"), "\u83DC\u5355");
91
- });
92
- //# sourceMappingURL=router.test.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../src/commands/router.test.ts"],
4
- "sourcesContent": ["/**\n * commands/router.test.ts\n */\nimport assert from \"node:assert/strict\";\nimport test from \"node:test\";\nimport { registerBuiltinCommands } from \"./handlers.js\";\nimport { PendingChoiceStore } from \"./pending.js\";\nimport { CommandRegistry } from \"./registry.js\";\nimport { CommandRouter } from \"./router.js\";\nimport type { CommandResult, InboundMessage, ReplyPort } from \"./types.js\";\n\nfunction makeReplyCapture(): { port: ReplyPort; sent: CommandResult[] } {\n const sent: CommandResult[] = [];\n return {\n sent,\n port: {\n async send(_t, result) {\n sent.push(result);\n },\n },\n };\n}\n\nfunction makeRouter(reply: ReplyPort): CommandRouter {\n const registry = new CommandRegistry();\n registerBuiltinCommands(registry);\n return new CommandRouter({\n registry,\n pending: new PendingChoiceStore(60_000),\n reply,\n startedAt: Date.now() - 5000,\n runtimeInfo: { sandbox: false, appIdHint: \"1905\u20266765\" },\n });\n}\n\ntest(\"router \u547D\u4E2D ping\", async () => {\n const { port, sent } = makeReplyCapture();\n const router = makeRouter(port);\n const inbound: InboundMessage = {\n backend: \"official\",\n groupId: \"G\",\n userId: \"U\",\n userName: \"Alice\",\n text: \"/ping\",\n msgId: \"m1\",\n };\n assert.equal(await router.handle(inbound), true);\n assert.equal(sent.length, 1);\n assert.match(sent[0]!.text, /^pong/);\n});\n\ntest(\"registry \u9996\u8BCD\u5339\u914D\u300C\u7533\u8BF7\u5165\u670D Steve\u300D\", () => {\n const registry = new CommandRegistry();\n registerBuiltinCommands(registry);\n const cmd = registry.resolve(\"\u7533\u8BF7\u5165\u670D Steve\");\n assert.ok(cmd);\n assert.equal(cmd!.name, \"join\");\n assert.equal(registry.resolve(\"\u8E22\u4EBA Alex\")?.name, \"kick\");\n});\n\ntest(\"router \u672A\u547D\u4E2D\u900F\u4F20\", async () => {\n const { port, sent } = makeReplyCapture();\n const router = makeRouter(port);\n assert.equal(\n await router.handle({\n backend: \"official\",\n groupId: \"G\",\n userId: \"U\",\n userName: \"A\",\n text: \"hello world\",\n }),\n false\n );\n assert.equal(sent.length, 0);\n});\n\ntest(\"router llbot \u7F16\u53F7\u4F1A\u8BDD\", async () => {\n const { port, sent } = makeReplyCapture();\n const router = makeRouter(port);\n const base: InboundMessage = {\n backend: \"llbot\",\n groupId: \"123\",\n userId: \"99\",\n userName: \"Bob\",\n text: \"\u83DC\u5355\",\n };\n assert.equal(await router.handle(base), true);\n assert.equal(sent.length, 1);\n assert.ok(sent[0]!.buttons && sent[0]!.buttons.length >= 2);\n\n assert.equal(\n await router.handle({ ...base, text: \"1\" }),\n true\n );\n assert.equal(sent.length, 2);\n // \u7B2C 1 \u4E2A\u6309\u94AE\u5BF9\u5E94 ping\uFF08\u4E3B\u83DC\u5355\u5DF2\u53BB\u6389\u81EA\u6307 menu\uFF09\n assert.match(sent[1]!.text, /^pong/);\n});\n\ntest(\"registry normalizeTrigger\", async () => {\n const { normalizeTrigger } = await import(\"./registry.js\");\n assert.equal(normalizeTrigger(\" /Ping \"), \"ping\");\n assert.equal(normalizeTrigger(\"\u83DC\u5355\"), \"\u83DC\u5355\");\n});\n"],
5
- "mappings": "AAGA,OAAO,YAAY;AACnB,OAAO,UAAU;AACjB,SAAS,+BAA+B;AACxC,SAAS,0BAA0B;AACnC,SAAS,uBAAuB;AAChC,SAAS,qBAAqB;AAG9B,SAAS,mBAA+D;AACtE,QAAM,OAAwB,CAAC;AAC/B,SAAO;AAAA,IACL;AAAA,IACA,MAAM;AAAA,MACJ,MAAM,KAAK,IAAI,QAAQ;AACrB,aAAK,KAAK,MAAM;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,WAAW,OAAiC;AACnD,QAAM,WAAW,IAAI,gBAAgB;AACrC,0BAAwB,QAAQ;AAChC,SAAO,IAAI,cAAc;AAAA,IACvB;AAAA,IACA,SAAS,IAAI,mBAAmB,GAAM;AAAA,IACtC;AAAA,IACA,WAAW,KAAK,IAAI,IAAI;AAAA,IACxB,aAAa,EAAE,SAAS,OAAO,WAAW,iBAAY;AAAA,EACxD,CAAC;AACH;AAEA,KAAK,4BAAkB,YAAY;AACjC,QAAM,EAAE,MAAM,KAAK,IAAI,iBAAiB;AACxC,QAAM,SAAS,WAAW,IAAI;AAC9B,QAAM,UAA0B;AAAA,IAC9B,SAAS;AAAA,IACT,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,MAAM;AAAA,IACN,OAAO;AAAA,EACT;AACA,SAAO,MAAM,MAAM,OAAO,OAAO,OAAO,GAAG,IAAI;AAC/C,SAAO,MAAM,KAAK,QAAQ,CAAC;AAC3B,SAAO,MAAM,KAAK,CAAC,EAAG,MAAM,OAAO;AACrC,CAAC;AAED,KAAK,+EAA6B,MAAM;AACtC,QAAM,WAAW,IAAI,gBAAgB;AACrC,0BAAwB,QAAQ;AAChC,QAAM,MAAM,SAAS,QAAQ,gCAAY;AACzC,SAAO,GAAG,GAAG;AACb,SAAO,MAAM,IAAK,MAAM,MAAM;AAC9B,SAAO,MAAM,SAAS,QAAQ,mBAAS,GAAG,MAAM,MAAM;AACxD,CAAC;AAED,KAAK,yCAAgB,YAAY;AAC/B,QAAM,EAAE,MAAM,KAAK,IAAI,iBAAiB;AACxC,QAAM,SAAS,WAAW,IAAI;AAC9B,SAAO;AAAA,IACL,MAAM,OAAO,OAAO;AAAA,MAClB,SAAS;AAAA,MACT,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,MAAM;AAAA,IACR,CAAC;AAAA,IACD;AAAA,EACF;AACA,SAAO,MAAM,KAAK,QAAQ,CAAC;AAC7B,CAAC;AAED,KAAK,yCAAqB,YAAY;AACpC,QAAM,EAAE,MAAM,KAAK,IAAI,iBAAiB;AACxC,QAAM,SAAS,WAAW,IAAI;AAC9B,QAAM,OAAuB;AAAA,IAC3B,SAAS;AAAA,IACT,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AACA,SAAO,MAAM,MAAM,OAAO,OAAO,IAAI,GAAG,IAAI;AAC5C,SAAO,MAAM,KAAK,QAAQ,CAAC;AAC3B,SAAO,GAAG,KAAK,CAAC,EAAG,WAAW,KAAK,CAAC,EAAG,QAAQ,UAAU,CAAC;AAE1D,SAAO;AAAA,IACL,MAAM,OAAO,OAAO,EAAE,GAAG,MAAM,MAAM,IAAI,CAAC;AAAA,IAC1C;AAAA,EACF;AACA,SAAO,MAAM,KAAK,QAAQ,CAAC;AAE3B,SAAO,MAAM,KAAK,CAAC,EAAG,MAAM,OAAO;AACrC,CAAC;AAED,KAAK,6BAA6B,YAAY;AAC5C,QAAM,EAAE,iBAAiB,IAAI,MAAM,OAAO,eAAe;AACzD,SAAO,MAAM,iBAAiB,SAAS,GAAG,MAAM;AAChD,SAAO,MAAM,iBAAiB,cAAI,GAAG,cAAI;AAC3C,CAAC;",
6
- "names": []
7
- }
@@ -1,32 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { EventEmitter } from "node:events";
3
- import test from "node:test";
4
- import { LlbotWsApi } from "./llbot-ws-api.js";
5
- class FakeSock extends EventEmitter {
6
- readyState = 1;
7
- sent = [];
8
- send(data) {
9
- this.sent.push(data);
10
- }
11
- }
12
- test("WS send_group_msg \u7B49 echo \u56DE\u5305", async () => {
13
- const api = new LlbotWsApi();
14
- const sock = new FakeSock();
15
- api.attach(sock);
16
- const p = api.sendGroupMsg("1058759051", "hello");
17
- assert.equal(sock.sent.length, 1);
18
- const req = JSON.parse(sock.sent[0]);
19
- assert.equal(req.action, "send_group_msg");
20
- assert.equal(req.params.group_id, 1058759051);
21
- assert.equal(
22
- api.tryHandleResponse({ status: "ok", retcode: 0, echo: req.echo, data: {} }),
23
- true
24
- );
25
- await p;
26
- });
27
- test("\u4E8B\u4EF6\u5E27\u4E0D\u6D88\u8D39", () => {
28
- const api = new LlbotWsApi();
29
- assert.equal(api.tryHandleResponse({ post_type: "message", echo: "x" }), false);
30
- assert.equal(api.tryHandleResponse({ retcode: 0 }), false);
31
- });
32
- //# sourceMappingURL=llbot-ws-api.test.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/llbot-ws-api.test.ts"],
4
- "sourcesContent": ["/**\n * llbot-ws-api.test.ts\n */\nimport assert from \"node:assert/strict\";\nimport { EventEmitter } from \"node:events\";\nimport test from \"node:test\";\nimport { LlbotWsApi } from \"./llbot-ws-api.js\";\n\nclass FakeSock extends EventEmitter {\n readyState = 1;\n sent: string[] = [];\n send(data: string): void {\n this.sent.push(data);\n }\n}\n\ntest(\"WS send_group_msg \u7B49 echo \u56DE\u5305\", async () => {\n const api = new LlbotWsApi();\n const sock = new FakeSock();\n api.attach(sock as unknown as import(\"ws\").WebSocket);\n\n const p = api.sendGroupMsg(\"1058759051\", \"hello\");\n assert.equal(sock.sent.length, 1);\n const req = JSON.parse(sock.sent[0]!) as { action: string; echo: string; params: { group_id: number } };\n assert.equal(req.action, \"send_group_msg\");\n assert.equal(req.params.group_id, 1058759051);\n\n assert.equal(\n api.tryHandleResponse({ status: \"ok\", retcode: 0, echo: req.echo, data: {} }),\n true\n );\n await p;\n});\n\ntest(\"\u4E8B\u4EF6\u5E27\u4E0D\u6D88\u8D39\", () => {\n const api = new LlbotWsApi();\n assert.equal(api.tryHandleResponse({ post_type: \"message\", echo: \"x\" }), false);\n assert.equal(api.tryHandleResponse({ retcode: 0 }), false);\n});\n"],
5
- "mappings": "AAGA,OAAO,YAAY;AACnB,SAAS,oBAAoB;AAC7B,OAAO,UAAU;AACjB,SAAS,kBAAkB;AAE3B,MAAM,iBAAiB,aAAa;AAAA,EAClC,aAAa;AAAA,EACb,OAAiB,CAAC;AAAA,EAClB,KAAK,MAAoB;AACvB,SAAK,KAAK,KAAK,IAAI;AAAA,EACrB;AACF;AAEA,KAAK,8CAA+B,YAAY;AAC9C,QAAM,MAAM,IAAI,WAAW;AAC3B,QAAM,OAAO,IAAI,SAAS;AAC1B,MAAI,OAAO,IAAyC;AAEpD,QAAM,IAAI,IAAI,aAAa,cAAc,OAAO;AAChD,SAAO,MAAM,KAAK,KAAK,QAAQ,CAAC;AAChC,QAAM,MAAM,KAAK,MAAM,KAAK,KAAK,CAAC,CAAE;AACpC,SAAO,MAAM,IAAI,QAAQ,gBAAgB;AACzC,SAAO,MAAM,IAAI,OAAO,UAAU,UAAU;AAE5C,SAAO;AAAA,IACL,IAAI,kBAAkB,EAAE,QAAQ,MAAM,SAAS,GAAG,MAAM,IAAI,MAAM,MAAM,CAAC,EAAE,CAAC;AAAA,IAC5E;AAAA,EACF;AACA,QAAM;AACR,CAAC;AAED,KAAK,wCAAU,MAAM;AACnB,QAAM,MAAM,IAAI,WAAW;AAC3B,SAAO,MAAM,IAAI,kBAAkB,EAAE,WAAW,WAAW,MAAM,IAAI,CAAC,GAAG,KAAK;AAC9E,SAAO,MAAM,IAAI,kBAAkB,EAAE,SAAS,EAAE,CAAC,GAAG,KAAK;AAC3D,CAAC;",
6
- "names": []
7
- }