@sfmc-bds/qq-bridge 0.1.1-beta.4 → 0.2.0-beta.5

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 (64) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +14 -3
  3. package/dist/commands/db-api.js +89 -0
  4. package/dist/commands/db-api.js.map +7 -0
  5. package/dist/commands/handlers.js +660 -0
  6. package/dist/commands/handlers.js.map +7 -0
  7. package/dist/commands/index.js +55 -0
  8. package/dist/commands/index.js.map +7 -0
  9. package/dist/commands/interaction-router.js +149 -0
  10. package/dist/commands/interaction-router.js.map +7 -0
  11. package/dist/commands/join-settings-ui.js +108 -0
  12. package/dist/commands/join-settings-ui.js.map +7 -0
  13. package/dist/commands/join-settings-ui.test.js +43 -0
  14. package/dist/commands/join-settings-ui.test.js.map +7 -0
  15. package/dist/commands/menu-format.js +45 -0
  16. package/dist/commands/menu-format.js.map +7 -0
  17. package/dist/commands/menu-panel-sync.js +130 -0
  18. package/dist/commands/menu-panel-sync.js.map +7 -0
  19. package/dist/commands/menu-panel-sync.test.js +27 -0
  20. package/dist/commands/menu-panel-sync.test.js.map +7 -0
  21. package/dist/commands/pending.js +33 -0
  22. package/dist/commands/pending.js.map +7 -0
  23. package/dist/commands/registry.js +40 -0
  24. package/dist/commands/registry.js.map +7 -0
  25. package/dist/commands/render.js +46 -0
  26. package/dist/commands/render.js.map +7 -0
  27. package/dist/commands/render.test.js +44 -0
  28. package/dist/commands/render.test.js.map +7 -0
  29. package/dist/commands/reply-llbot.js +63 -0
  30. package/dist/commands/reply-llbot.js.map +7 -0
  31. package/dist/commands/reply-official.js +66 -0
  32. package/dist/commands/reply-official.js.map +7 -0
  33. package/dist/commands/router.js +53 -0
  34. package/dist/commands/router.js.map +7 -0
  35. package/dist/commands/router.test.js +92 -0
  36. package/dist/commands/router.test.js.map +7 -0
  37. package/dist/commands/types.js +1 -0
  38. package/dist/commands/types.js.map +7 -0
  39. package/dist/config.js +12 -2
  40. package/dist/config.js.map +2 -2
  41. package/dist/console.js +40 -20
  42. package/dist/console.js.map +2 -2
  43. package/dist/dbserver.js.map +2 -2
  44. package/dist/index.js +117 -20
  45. package/dist/index.js.map +2 -2
  46. package/dist/llbot-ws-api.js +90 -0
  47. package/dist/llbot-ws-api.js.map +7 -0
  48. package/dist/llbot-ws-api.test.js +32 -0
  49. package/dist/llbot-ws-api.test.js.map +7 -0
  50. package/dist/official/events.js +136 -0
  51. package/dist/official/events.js.map +7 -0
  52. package/dist/official/events.test.js +123 -0
  53. package/dist/official/events.test.js.map +7 -0
  54. package/dist/official/gateway.js +226 -0
  55. package/dist/official/gateway.js.map +7 -0
  56. package/dist/onebot.js +23 -5
  57. package/dist/onebot.js.map +2 -2
  58. package/dist/project-root.js +2 -2
  59. package/dist/project-root.js.map +2 -2
  60. package/dist/runtime-status.js +45 -0
  61. package/dist/runtime-status.js.map +7 -0
  62. package/dist/ws-server.js +4 -0
  63. package/dist/ws-server.js.map +2 -2
  64. package/package.json +18 -15
@@ -0,0 +1,90 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { log } from "./log.js";
3
+ const DEFAULT_TIMEOUT_MS = 8e3;
4
+ class LlbotWsApi {
5
+ sock = null;
6
+ pending = /* @__PURE__ */ new Map();
7
+ /** 当前是否有可用的 LLBot reverse-ws 连接 */
8
+ get connected() {
9
+ return !!this.sock && this.sock.readyState === 1;
10
+ }
11
+ attach(sock) {
12
+ if (this.sock && this.sock !== sock) {
13
+ this.rejectAll(new Error("LLBot WebSocket \u88AB\u65B0\u8FDE\u63A5\u66FF\u6362"));
14
+ }
15
+ this.sock = sock;
16
+ log.info("LLBot WS API \u5DF2\u6302\u63A5\uFF08\u53EF\u7528 reverse-ws \u53D1\u6D88\u606F\uFF09");
17
+ }
18
+ detach(sock) {
19
+ if (this.sock !== sock) return;
20
+ this.sock = null;
21
+ this.rejectAll(new Error("LLBot WebSocket \u5DF2\u65AD\u5F00"));
22
+ log.info("LLBot WS API \u5DF2\u5378\u8F7D");
23
+ }
24
+ /**
25
+ * 若帧是 API 回包(有 echo、无 post_type),消费并返回 true。
26
+ * 事件帧返回 false,交给 dispatcher。
27
+ */
28
+ tryHandleResponse(raw) {
29
+ if (!raw || typeof raw !== "object") return false;
30
+ const obj = raw;
31
+ if (obj.post_type != null) return false;
32
+ const echo = obj.echo;
33
+ if (echo == null || echo === "") return false;
34
+ const key = String(echo);
35
+ const p = this.pending.get(key);
36
+ if (!p) return false;
37
+ this.pending.delete(key);
38
+ clearTimeout(p.timer);
39
+ p.resolve(obj);
40
+ return true;
41
+ }
42
+ async call(action, params, timeoutMs = DEFAULT_TIMEOUT_MS) {
43
+ if (!this.connected || !this.sock) {
44
+ throw new Error("LLBot reverse-ws \u672A\u8FDE\u63A5\uFF0C\u65E0\u6CD5\u8C03\u7528 API");
45
+ }
46
+ const echo = `sfmc_${Date.now().toString(36)}_${randomBytes(4).toString("hex")}`;
47
+ const payload = JSON.stringify({ action, params, echo });
48
+ return new Promise((resolve, reject) => {
49
+ const timer = setTimeout(() => {
50
+ this.pending.delete(echo);
51
+ reject(new Error(`LLBot WS API \u8D85\u65F6 (${action})`));
52
+ }, timeoutMs);
53
+ this.pending.set(echo, { resolve, reject, timer });
54
+ try {
55
+ this.sock.send(payload);
56
+ } catch (e) {
57
+ this.pending.delete(echo);
58
+ clearTimeout(timer);
59
+ reject(e instanceof Error ? e : new Error(String(e)));
60
+ }
61
+ });
62
+ }
63
+ async sendGroupMsg(groupId, text) {
64
+ const gid = typeof groupId === "number" ? groupId : parseInt(String(groupId), 10);
65
+ if (!Number.isFinite(gid)) throw new Error(`\u65E0\u6548 group_id: ${groupId}`);
66
+ const result = await this.call("send_group_msg", {
67
+ group_id: gid,
68
+ message: [{ type: "text", data: { text } }]
69
+ });
70
+ const ret = result.retcode ?? (result.status === "ok" ? 0 : -1);
71
+ if (ret !== 0) {
72
+ throw new Error(
73
+ `LLBot send_group_msg retcode=${ret}: ${result.message || result.wording || result.status || "fail"}`
74
+ );
75
+ }
76
+ }
77
+ rejectAll(err) {
78
+ for (const [k, p] of this.pending) {
79
+ clearTimeout(p.timer);
80
+ p.reject(err);
81
+ this.pending.delete(k);
82
+ }
83
+ }
84
+ }
85
+ const llbotWsApi = new LlbotWsApi();
86
+ export {
87
+ LlbotWsApi,
88
+ llbotWsApi
89
+ };
90
+ //# sourceMappingURL=llbot-ws-api.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/llbot-ws-api.ts"],
4
+ "sourcesContent": ["/**\n * llbot-ws-api.ts \u2014 \u7ECF LLBot reverse-WS \u8C03\u7528 OneBot API\n *\n * \u591A\u6570 LLBot \u90E8\u7F72\u53EA\u5F00 reverse-ws\u3001\u4E0D\u5F00\u6B63\u5411 HTTP(3004)\u3002\n * \u6307\u4EE4\u56DE\u590D\u5E94\u8D70\u5DF2\u8FDE\u63A5\u7684 WebSocket\uFF0C\u800C\u4E0D\u662F\u5047\u5B9A 3004 \u53EF\u8FBE\u3002\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport type { WebSocket } from \"ws\";\nimport { log } from \"./log.js\";\n\nexport type LlbotApiResult = {\n status?: string;\n retcode?: number;\n data?: unknown;\n message?: string;\n wording?: string;\n echo?: string;\n};\n\ntype Pending = {\n resolve: (v: LlbotApiResult) => void;\n reject: (e: Error) => void;\n timer: ReturnType<typeof setTimeout>;\n};\n\nconst DEFAULT_TIMEOUT_MS = 8000;\n\nexport class LlbotWsApi {\n private sock: WebSocket | null = null;\n private readonly pending = new Map<string, Pending>();\n\n /** \u5F53\u524D\u662F\u5426\u6709\u53EF\u7528\u7684 LLBot reverse-ws \u8FDE\u63A5 */\n get connected(): boolean {\n return !!this.sock && this.sock.readyState === 1; // OPEN\n }\n\n attach(sock: WebSocket): void {\n if (this.sock && this.sock !== sock) {\n this.rejectAll(new Error(\"LLBot WebSocket \u88AB\u65B0\u8FDE\u63A5\u66FF\u6362\"));\n }\n this.sock = sock;\n log.info(\"LLBot WS API \u5DF2\u6302\u63A5\uFF08\u53EF\u7528 reverse-ws \u53D1\u6D88\u606F\uFF09\");\n }\n\n detach(sock: WebSocket): void {\n if (this.sock !== sock) return;\n this.sock = null;\n this.rejectAll(new Error(\"LLBot WebSocket \u5DF2\u65AD\u5F00\"));\n log.info(\"LLBot WS API \u5DF2\u5378\u8F7D\");\n }\n\n /**\n * \u82E5\u5E27\u662F API \u56DE\u5305\uFF08\u6709 echo\u3001\u65E0 post_type\uFF09\uFF0C\u6D88\u8D39\u5E76\u8FD4\u56DE true\u3002\n * \u4E8B\u4EF6\u5E27\u8FD4\u56DE false\uFF0C\u4EA4\u7ED9 dispatcher\u3002\n */\n tryHandleResponse(raw: unknown): boolean {\n if (!raw || typeof raw !== \"object\") return false;\n const obj = raw as Record<string, unknown>;\n if (obj.post_type != null) return false;\n const echo = obj.echo;\n if (echo == null || echo === \"\") return false;\n const key = String(echo);\n const p = this.pending.get(key);\n if (!p) return false;\n this.pending.delete(key);\n clearTimeout(p.timer);\n p.resolve(obj as LlbotApiResult);\n return true;\n }\n\n async call(action: string, params: Record<string, unknown>, timeoutMs = DEFAULT_TIMEOUT_MS): Promise<LlbotApiResult> {\n if (!this.connected || !this.sock) {\n throw new Error(\"LLBot reverse-ws \u672A\u8FDE\u63A5\uFF0C\u65E0\u6CD5\u8C03\u7528 API\");\n }\n const echo = `sfmc_${Date.now().toString(36)}_${randomBytes(4).toString(\"hex\")}`;\n const payload = JSON.stringify({ action, params, echo });\n\n return new Promise<LlbotApiResult>((resolve, reject) => {\n const timer = setTimeout(() => {\n this.pending.delete(echo);\n reject(new Error(`LLBot WS API \u8D85\u65F6 (${action})`));\n }, timeoutMs);\n this.pending.set(echo, { resolve, reject, timer });\n try {\n this.sock!.send(payload);\n } catch (e) {\n this.pending.delete(echo);\n clearTimeout(timer);\n reject(e instanceof Error ? e : new Error(String(e)));\n }\n });\n }\n\n async sendGroupMsg(groupId: string | number, text: string): Promise<void> {\n const gid = typeof groupId === \"number\" ? groupId : parseInt(String(groupId), 10);\n if (!Number.isFinite(gid)) throw new Error(`\u65E0\u6548 group_id: ${groupId}`);\n const result = await this.call(\"send_group_msg\", {\n group_id: gid,\n message: [{ type: \"text\", data: { text } }],\n });\n const ret = result.retcode ?? (result.status === \"ok\" ? 0 : -1);\n if (ret !== 0) {\n throw new Error(\n `LLBot send_group_msg retcode=${ret}: ${result.message || result.wording || result.status || \"fail\"}`\n );\n }\n }\n\n private rejectAll(err: Error): void {\n for (const [k, p] of this.pending) {\n clearTimeout(p.timer);\n p.reject(err);\n this.pending.delete(k);\n }\n }\n}\n\n/** \u8FDB\u7A0B\u5185\u5355\u4F8B\uFF1Aws-server \u6302\u63A5\uFF0Creply-llbot / \u5176\u5B83\u51FA\u53E3\u590D\u7528 */\nexport const llbotWsApi = new LlbotWsApi();\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,32 @@
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
@@ -0,0 +1,7 @@
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
+ }
@@ -0,0 +1,136 @@
1
+ import { stripOfficialAtMention } from "@sfmc-bds/sdk/node/qq-official";
2
+ import { RecentMessageDedup } from "../onebot.js";
3
+ import { tryForward } from "../dbserver.js";
4
+ import { log } from "../log.js";
5
+ function detectOfficialGroupAdmin(msg) {
6
+ const candidates = [
7
+ msg.author?.member_role,
8
+ msg.author?.roles,
9
+ msg.member?.role,
10
+ msg.member?.roles
11
+ ];
12
+ for (const c of candidates) {
13
+ if (c == null) continue;
14
+ if (Array.isArray(c)) {
15
+ const joined = c.map((x) => String(x).toLowerCase()).join(",");
16
+ if (/\b(owner|admin|2|3)\b/.test(joined)) return true;
17
+ continue;
18
+ }
19
+ const s = String(c).toLowerCase();
20
+ if (s === "owner" || s === "admin" || s === "2" || s === "3") return true;
21
+ }
22
+ return false;
23
+ }
24
+ function attachmentsPlaceholder(attachments) {
25
+ if (!Array.isArray(attachments) || attachments.length === 0) return "";
26
+ const parts = [];
27
+ for (const att of attachments) {
28
+ const ct = String(att.content_type ?? "").toLowerCase();
29
+ const name = String(att.filename ?? "").toLowerCase();
30
+ if (ct.startsWith("image/") || /\.(png|jpe?g|gif|webp|bmp)$/i.test(name)) {
31
+ parts.push("[\u56FE\u7247]");
32
+ } else if (ct.startsWith("video/") || /\.(mp4|mov|avi|mkv)$/i.test(name)) {
33
+ parts.push("[\u89C6\u9891]");
34
+ } else if (ct.startsWith("audio/") || /\.(mp3|wav|amr|silk)$/i.test(name)) {
35
+ parts.push("[\u8BED\u97F3]");
36
+ } else {
37
+ parts.push("[\u6587\u4EF6]");
38
+ }
39
+ }
40
+ return parts.join("");
41
+ }
42
+ function extractOfficialText(msg) {
43
+ const base = stripOfficialAtMention(String(msg.content ?? ""));
44
+ const suffix = attachmentsPlaceholder(msg.attachments);
45
+ return `${base}${suffix ? (base ? " " : "") + suffix : ""}`.trim();
46
+ }
47
+ class OfficialAtMessageDispatcher {
48
+ opts;
49
+ dedup = new RecentMessageDedup();
50
+ forward;
51
+ loggedMissingOpenid = false;
52
+ constructor(opts) {
53
+ this.opts = opts;
54
+ this.forward = opts.forward ?? tryForward;
55
+ }
56
+ async handleGroupAtMessage(msg) {
57
+ if (!msg || typeof msg !== "object") return;
58
+ if (msg.author?.bot) return;
59
+ const groupOpenid = String(msg.group_openid ?? "");
60
+ if (!groupOpenid) {
61
+ log.warn("\u5B98\u65B9\u4E8B\u4EF6\u7F3A group_openid\uFF0C\u8DF3\u8FC7");
62
+ return;
63
+ }
64
+ if (!this.opts.groupOpenid) {
65
+ if (!this.loggedMissingOpenid) {
66
+ log.warn(
67
+ `qq_group_openid \u672A\u914D\u7F6E\u3002\u6536\u5230\u7FA4 openid=${groupOpenid}\uFF0C\u8BF7\u5199\u5165 configs/qq_config.json \u540E\u91CD\u542F\uFF08\u672C\u6761\u4E0D\u8F6C\u53D1\uFF09`
68
+ );
69
+ this.loggedMissingOpenid = true;
70
+ } else {
71
+ log.info(`qq_group_openid \u672A\u914D\u7F6E\uFF0C\u4E22\u5F03\u7FA4\u6D88\u606F openid=${groupOpenid}`);
72
+ }
73
+ return;
74
+ }
75
+ if (groupOpenid !== this.opts.groupOpenid) {
76
+ log.info(`\u975E\u76EE\u6807\u7FA4 openid=${groupOpenid}\uFF0C\u8DF3\u8FC7`);
77
+ return;
78
+ }
79
+ if (this.dedup.seen(msg.id)) return;
80
+ const text = extractOfficialText(msg);
81
+ if (!text) return;
82
+ const memberId = String(msg.author?.member_openid || msg.author?.id || "unknown");
83
+ const fromName = String(msg.author?.username || `QQ_${memberId.slice(0, 8)}`);
84
+ const fromId = `qq_${memberId}`;
85
+ if (this.opts.commandRouter) {
86
+ const inbound = {
87
+ backend: "official",
88
+ scene: "group",
89
+ groupId: groupOpenid,
90
+ userId: memberId,
91
+ userName: fromName,
92
+ text,
93
+ isGroupAdmin: detectOfficialGroupAdmin(msg),
94
+ ...msg.id ? { msgId: String(msg.id) } : {}
95
+ };
96
+ if (await this.opts.commandRouter.handle(inbound)) return;
97
+ }
98
+ if (!this.opts.db.channelId) {
99
+ log.warn("bridge_channel_id \u672A\u914D\u7F6E\uFF0C\u8DF3\u8FC7");
100
+ return;
101
+ }
102
+ await this.forward(this.opts.db, fromId, fromName, text);
103
+ }
104
+ /**
105
+ * 单聊:仅走指令路由(菜单点击落点),不转发 MC。
106
+ */
107
+ async handleC2cMessage(msg) {
108
+ if (!msg || typeof msg !== "object") return;
109
+ if (msg.author?.bot) return;
110
+ if (this.dedup.seen(msg.id)) return;
111
+ const text = extractOfficialText(msg);
112
+ if (!text) return;
113
+ const userId = String(msg.author?.user_openid || msg.author?.id || "unknown");
114
+ const fromName = String(msg.author?.username || `QQ_${userId.slice(0, 8)}`);
115
+ if (!this.opts.commandRouter) {
116
+ log.info(`C2C \u65E0\u6307\u4EE4\u8DEF\u7531\uFF0C\u5FFD\u7565 text=${text.slice(0, 40)}`);
117
+ return;
118
+ }
119
+ const inbound = {
120
+ backend: "official",
121
+ scene: "c2c",
122
+ groupId: `c2c:${userId}`,
123
+ userId,
124
+ userName: fromName,
125
+ text,
126
+ ...msg.id ? { msgId: String(msg.id) } : {}
127
+ };
128
+ await this.opts.commandRouter.handle(inbound);
129
+ }
130
+ }
131
+ export {
132
+ OfficialAtMessageDispatcher,
133
+ attachmentsPlaceholder,
134
+ extractOfficialText
135
+ };
136
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/official/events.ts"],
4
+ "sourcesContent": ["/**\n * official/events.ts \u2014 \u5B98\u65B9 GROUP_AT / C2C \u89E3\u6790\u4E0E\u8F6C\u53D1\n */\n\nimport { stripOfficialAtMention } from \"@sfmc-bds/sdk/node/qq-official\";\nimport type { CommandRouter } from \"../commands/index.js\";\nimport { RecentMessageDedup } from \"../onebot.js\";\nimport { tryForward, type DBServerConfig } from \"../dbserver.js\";\nimport { log } from \"../log.js\";\n\nexport type OfficialAttachment = {\n url?: string;\n content_type?: string;\n filename?: string;\n};\n\nexport type OfficialGroupAtMessage = {\n id?: string;\n content?: string;\n group_openid?: string;\n timestamp?: string;\n author?: {\n id?: string;\n member_openid?: string;\n username?: string;\n bot?: boolean;\n /** \u82E5\u5E73\u53F0\u4E0B\u53D1\u5219\u89E3\u6790\uFF1B\u65E0\u5219\u65E0\u6CD5\u5224\u5B9A\u7FA4\u7BA1 */\n roles?: unknown;\n member_role?: unknown;\n };\n member?: {\n roles?: unknown;\n role?: unknown;\n };\n attachments?: OfficialAttachment[];\n};\n\n/** \u4ECE\u5B98\u65B9\u4E8B\u4EF6\u53EF\u9009\u5B57\u6BB5\u63A8\u65AD\u7FA4\u4E3B/\u7FA4\u7BA1\uFF08\u65E0\u53EF\u9760\u5B57\u6BB5\u5219 false\uFF09 */\nfunction detectOfficialGroupAdmin(msg: OfficialGroupAtMessage): boolean {\n const candidates: unknown[] = [\n msg.author?.member_role,\n msg.author?.roles,\n msg.member?.role,\n msg.member?.roles,\n ];\n for (const c of candidates) {\n if (c == null) continue;\n if (Array.isArray(c)) {\n const joined = c.map((x) => String(x).toLowerCase()).join(\",\");\n if (/\\b(owner|admin|2|3)\\b/.test(joined)) return true;\n continue;\n }\n const s = String(c).toLowerCase();\n if (s === \"owner\" || s === \"admin\" || s === \"2\" || s === \"3\") return true;\n }\n return false;\n}\n\n/** C2C \u5355\u804A\u4E8B\u4EF6\u4F53 */\nexport type OfficialC2cMessage = {\n id?: string;\n content?: string;\n timestamp?: string;\n author?: {\n id?: string;\n user_openid?: string;\n username?: string;\n bot?: boolean;\n };\n attachments?: OfficialAttachment[];\n};\n\nexport type OfficialForwardFn = (\n db: DBServerConfig,\n fromId: string,\n fromName: string,\n content: string\n) => Promise<void>;\n\nexport type OfficialDispatcherOptions = {\n /** \u5DF2\u914D\u7F6E\u7684\u7FA4 openid\uFF1B\u7A7A = \u53EA\u6253\u65E5\u5FD7\u4E0D\u8F6C\u53D1 */\n groupOpenid: string;\n db: DBServerConfig;\n /** \u53EF\u6CE8\u5165\uFF0C\u9ED8\u8BA4 tryForward\uFF08\u4FBF\u4E8E\u5355\u6D4B\uFF09 */\n forward?: OfficialForwardFn;\n /** QQ \u4FA7\u6307\u4EE4\u8DEF\u7531\uFF1B\u547D\u4E2D\u5219\u4E0D\u8F6C\u53D1 MC */\n commandRouter?: CommandRouter;\n};\n\n/**\n * \u628A attachments \u8F6C\u6210\u4E0E OneBot \u4E00\u81F4\u7684\u5360\u4F4D\u7B26\u540E\u7F00\u3002\n */\nexport function attachmentsPlaceholder(attachments: OfficialAttachment[] | undefined): string {\n if (!Array.isArray(attachments) || attachments.length === 0) return \"\";\n const parts: string[] = [];\n for (const att of attachments) {\n const ct = String(att.content_type ?? \"\").toLowerCase();\n const name = String(att.filename ?? \"\").toLowerCase();\n if (ct.startsWith(\"image/\") || /\\.(png|jpe?g|gif|webp|bmp)$/i.test(name)) {\n parts.push(\"[\u56FE\u7247]\");\n } else if (ct.startsWith(\"video/\") || /\\.(mp4|mov|avi|mkv)$/i.test(name)) {\n parts.push(\"[\u89C6\u9891]\");\n } else if (ct.startsWith(\"audio/\") || /\\.(mp3|wav|amr|silk)$/i.test(name)) {\n parts.push(\"[\u8BED\u97F3]\");\n } else {\n parts.push(\"[\u6587\u4EF6]\");\n }\n }\n return parts.join(\"\");\n}\n\n/** \u4ECE\u5B98\u65B9\u4E8B\u4EF6\u62BD\u51FA\u7EAF\u6587\u672C\uFF08\u53BB @bot + \u9644\u4EF6\u5360\u4F4D\uFF09 */\nexport function extractOfficialText(msg: {\n content?: string;\n attachments?: OfficialAttachment[];\n}): string {\n const base = stripOfficialAtMention(String(msg.content ?? \"\"));\n const suffix = attachmentsPlaceholder(msg.attachments);\n return `${base}${suffix ? (base ? \" \" : \"\") + suffix : \"\"}`.trim();\n}\n\nexport class OfficialAtMessageDispatcher {\n private readonly opts: OfficialDispatcherOptions;\n private readonly dedup = new RecentMessageDedup();\n private readonly forward: OfficialForwardFn;\n private loggedMissingOpenid = false;\n\n constructor(opts: OfficialDispatcherOptions) {\n this.opts = opts;\n this.forward = opts.forward ?? tryForward;\n }\n\n async handleGroupAtMessage(msg: OfficialGroupAtMessage): Promise<void> {\n if (!msg || typeof msg !== \"object\") return;\n if (msg.author?.bot) return;\n\n const groupOpenid = String(msg.group_openid ?? \"\");\n if (!groupOpenid) {\n log.warn(\"\u5B98\u65B9\u4E8B\u4EF6\u7F3A group_openid\uFF0C\u8DF3\u8FC7\");\n return;\n }\n\n if (!this.opts.groupOpenid) {\n if (!this.loggedMissingOpenid) {\n log.warn(\n `qq_group_openid \u672A\u914D\u7F6E\u3002\u6536\u5230\u7FA4 openid=${groupOpenid}\uFF0C\u8BF7\u5199\u5165 configs/qq_config.json \u540E\u91CD\u542F\uFF08\u672C\u6761\u4E0D\u8F6C\u53D1\uFF09`\n );\n this.loggedMissingOpenid = true;\n } else {\n log.info(`qq_group_openid \u672A\u914D\u7F6E\uFF0C\u4E22\u5F03\u7FA4\u6D88\u606F openid=${groupOpenid}`);\n }\n return;\n }\n\n if (groupOpenid !== this.opts.groupOpenid) {\n log.info(`\u975E\u76EE\u6807\u7FA4 openid=${groupOpenid}\uFF0C\u8DF3\u8FC7`);\n return;\n }\n\n if (this.dedup.seen(msg.id)) return;\n\n const text = extractOfficialText(msg);\n if (!text) return;\n\n const memberId = String(msg.author?.member_openid || msg.author?.id || \"unknown\");\n const fromName = String(msg.author?.username || `QQ_${memberId.slice(0, 8)}`);\n const fromId = `qq_${memberId}`;\n\n if (this.opts.commandRouter) {\n const inbound = {\n backend: \"official\" as const,\n scene: \"group\" as const,\n groupId: groupOpenid,\n userId: memberId,\n userName: fromName,\n text,\n isGroupAdmin: detectOfficialGroupAdmin(msg),\n ...(msg.id ? { msgId: String(msg.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\uFF0C\u8DF3\u8FC7\");\n return;\n }\n\n await this.forward(this.opts.db, fromId, fromName, text);\n }\n\n /**\n * \u5355\u804A\uFF1A\u4EC5\u8D70\u6307\u4EE4\u8DEF\u7531\uFF08\u83DC\u5355\u70B9\u51FB\u843D\u70B9\uFF09\uFF0C\u4E0D\u8F6C\u53D1 MC\u3002\n */\n async handleC2cMessage(msg: OfficialC2cMessage): Promise<void> {\n if (!msg || typeof msg !== \"object\") return;\n if (msg.author?.bot) return;\n if (this.dedup.seen(msg.id)) return;\n\n const text = extractOfficialText(msg);\n if (!text) return;\n\n const userId = String(msg.author?.user_openid || msg.author?.id || \"unknown\");\n const fromName = String(msg.author?.username || `QQ_${userId.slice(0, 8)}`);\n\n if (!this.opts.commandRouter) {\n log.info(`C2C \u65E0\u6307\u4EE4\u8DEF\u7531\uFF0C\u5FFD\u7565 text=${text.slice(0, 40)}`);\n return;\n }\n\n const inbound = {\n backend: \"official\" as const,\n scene: \"c2c\" as const,\n groupId: `c2c:${userId}`,\n userId,\n userName: fromName,\n text,\n ...(msg.id ? { msgId: String(msg.id) } : {}),\n };\n await this.opts.commandRouter.handle(inbound);\n }\n}\n"],
5
+ "mappings": "AAIA,SAAS,8BAA8B;AAEvC,SAAS,0BAA0B;AACnC,SAAS,kBAAuC;AAChD,SAAS,WAAW;AA8BpB,SAAS,yBAAyB,KAAsC;AACtE,QAAM,aAAwB;AAAA,IAC5B,IAAI,QAAQ;AAAA,IACZ,IAAI,QAAQ;AAAA,IACZ,IAAI,QAAQ;AAAA,IACZ,IAAI,QAAQ;AAAA,EACd;AACA,aAAW,KAAK,YAAY;AAC1B,QAAI,KAAK,KAAM;AACf,QAAI,MAAM,QAAQ,CAAC,GAAG;AACpB,YAAM,SAAS,EAAE,IAAI,CAAC,MAAM,OAAO,CAAC,EAAE,YAAY,CAAC,EAAE,KAAK,GAAG;AAC7D,UAAI,wBAAwB,KAAK,MAAM,EAAG,QAAO;AACjD;AAAA,IACF;AACA,UAAM,IAAI,OAAO,CAAC,EAAE,YAAY;AAChC,QAAI,MAAM,WAAW,MAAM,WAAW,MAAM,OAAO,MAAM,IAAK,QAAO;AAAA,EACvE;AACA,SAAO;AACT;AAoCO,SAAS,uBAAuB,aAAuD;AAC5F,MAAI,CAAC,MAAM,QAAQ,WAAW,KAAK,YAAY,WAAW,EAAG,QAAO;AACpE,QAAM,QAAkB,CAAC;AACzB,aAAW,OAAO,aAAa;AAC7B,UAAM,KAAK,OAAO,IAAI,gBAAgB,EAAE,EAAE,YAAY;AACtD,UAAM,OAAO,OAAO,IAAI,YAAY,EAAE,EAAE,YAAY;AACpD,QAAI,GAAG,WAAW,QAAQ,KAAK,+BAA+B,KAAK,IAAI,GAAG;AACxE,YAAM,KAAK,gBAAM;AAAA,IACnB,WAAW,GAAG,WAAW,QAAQ,KAAK,wBAAwB,KAAK,IAAI,GAAG;AACxE,YAAM,KAAK,gBAAM;AAAA,IACnB,WAAW,GAAG,WAAW,QAAQ,KAAK,yBAAyB,KAAK,IAAI,GAAG;AACzE,YAAM,KAAK,gBAAM;AAAA,IACnB,OAAO;AACL,YAAM,KAAK,gBAAM;AAAA,IACnB;AAAA,EACF;AACA,SAAO,MAAM,KAAK,EAAE;AACtB;AAGO,SAAS,oBAAoB,KAGzB;AACT,QAAM,OAAO,uBAAuB,OAAO,IAAI,WAAW,EAAE,CAAC;AAC7D,QAAM,SAAS,uBAAuB,IAAI,WAAW;AACrD,SAAO,GAAG,IAAI,GAAG,UAAU,OAAO,MAAM,MAAM,SAAS,EAAE,GAAG,KAAK;AACnE;AAEO,MAAM,4BAA4B;AAAA,EACtB;AAAA,EACA,QAAQ,IAAI,mBAAmB;AAAA,EAC/B;AAAA,EACT,sBAAsB;AAAA,EAE9B,YAAY,MAAiC;AAC3C,SAAK,OAAO;AACZ,SAAK,UAAU,KAAK,WAAW;AAAA,EACjC;AAAA,EAEA,MAAM,qBAAqB,KAA4C;AACrE,QAAI,CAAC,OAAO,OAAO,QAAQ,SAAU;AACrC,QAAI,IAAI,QAAQ,IAAK;AAErB,UAAM,cAAc,OAAO,IAAI,gBAAgB,EAAE;AACjD,QAAI,CAAC,aAAa;AAChB,UAAI,KAAK,+DAAuB;AAChC;AAAA,IACF;AAEA,QAAI,CAAC,KAAK,KAAK,aAAa;AAC1B,UAAI,CAAC,KAAK,qBAAqB;AAC7B,YAAI;AAAA,UACF,qEAAkC,WAAW;AAAA,QAC/C;AACA,aAAK,sBAAsB;AAAA,MAC7B,OAAO;AACL,YAAI,KAAK,iFAAoC,WAAW,EAAE;AAAA,MAC5D;AACA;AAAA,IACF;AAEA,QAAI,gBAAgB,KAAK,KAAK,aAAa;AACzC,UAAI,KAAK,mCAAe,WAAW,oBAAK;AACxC;AAAA,IACF;AAEA,QAAI,KAAK,MAAM,KAAK,IAAI,EAAE,EAAG;AAE7B,UAAM,OAAO,oBAAoB,GAAG;AACpC,QAAI,CAAC,KAAM;AAEX,UAAM,WAAW,OAAO,IAAI,QAAQ,iBAAiB,IAAI,QAAQ,MAAM,SAAS;AAChF,UAAM,WAAW,OAAO,IAAI,QAAQ,YAAY,MAAM,SAAS,MAAM,GAAG,CAAC,CAAC,EAAE;AAC5E,UAAM,SAAS,MAAM,QAAQ;AAE7B,QAAI,KAAK,KAAK,eAAe;AAC3B,YAAM,UAAU;AAAA,QACd,SAAS;AAAA,QACT,OAAO;AAAA,QACP,SAAS;AAAA,QACT,QAAQ;AAAA,QACR,UAAU;AAAA,QACV;AAAA,QACA,cAAc,yBAAyB,GAAG;AAAA,QAC1C,GAAI,IAAI,KAAK,EAAE,OAAO,OAAO,IAAI,EAAE,EAAE,IAAI,CAAC;AAAA,MAC5C;AACA,UAAI,MAAM,KAAK,KAAK,cAAc,OAAO,OAAO,EAAG;AAAA,IACrD;AAEA,QAAI,CAAC,KAAK,KAAK,GAAG,WAAW;AAC3B,UAAI,KAAK,wDAA0B;AACnC;AAAA,IACF;AAEA,UAAM,KAAK,QAAQ,KAAK,KAAK,IAAI,QAAQ,UAAU,IAAI;AAAA,EACzD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,iBAAiB,KAAwC;AAC7D,QAAI,CAAC,OAAO,OAAO,QAAQ,SAAU;AACrC,QAAI,IAAI,QAAQ,IAAK;AACrB,QAAI,KAAK,MAAM,KAAK,IAAI,EAAE,EAAG;AAE7B,UAAM,OAAO,oBAAoB,GAAG;AACpC,QAAI,CAAC,KAAM;AAEX,UAAM,SAAS,OAAO,IAAI,QAAQ,eAAe,IAAI,QAAQ,MAAM,SAAS;AAC5E,UAAM,WAAW,OAAO,IAAI,QAAQ,YAAY,MAAM,OAAO,MAAM,GAAG,CAAC,CAAC,EAAE;AAE1E,QAAI,CAAC,KAAK,KAAK,eAAe;AAC5B,UAAI,KAAK,6DAAqB,KAAK,MAAM,GAAG,EAAE,CAAC,EAAE;AACjD;AAAA,IACF;AAEA,UAAM,UAAU;AAAA,MACd,SAAS;AAAA,MACT,OAAO;AAAA,MACP,SAAS,OAAO,MAAM;AAAA,MACtB;AAAA,MACA,UAAU;AAAA,MACV;AAAA,MACA,GAAI,IAAI,KAAK,EAAE,OAAO,OAAO,IAAI,EAAE,EAAE,IAAI,CAAC;AAAA,IAC5C;AACA,UAAM,KAAK,KAAK,cAAc,OAAO,OAAO;AAAA,EAC9C;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,123 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ OfficialAtMessageDispatcher,
5
+ attachmentsPlaceholder,
6
+ extractOfficialText
7
+ } from "./events.js";
8
+ test("extractOfficialText \u53BB @ \u5E76\u62FC\u9644\u4EF6\u5360\u4F4D", () => {
9
+ assert.equal(
10
+ extractOfficialText({
11
+ content: "<@!botid> hello",
12
+ attachments: [{ content_type: "image/png" }]
13
+ }),
14
+ "hello [\u56FE\u7247]"
15
+ );
16
+ });
17
+ test("attachmentsPlaceholder \u6309\u7C7B\u578B\u5206\u7C7B", () => {
18
+ assert.equal(
19
+ attachmentsPlaceholder([
20
+ { content_type: "image/jpeg" },
21
+ { content_type: "video/mp4" },
22
+ { filename: "a.mp3" },
23
+ { filename: "doc.pdf" }
24
+ ]),
25
+ "[\u56FE\u7247][\u89C6\u9891][\u8BED\u97F3][\u6587\u4EF6]"
26
+ );
27
+ });
28
+ test("OfficialAtMessageDispatcher \u8FC7\u6EE4\u4E0E\u8F6C\u53D1\u8F7D\u8377", async () => {
29
+ const forwarded = [];
30
+ const forward = async (_db, fromId, fromName, content) => {
31
+ forwarded.push({ fromId, fromName, content });
32
+ };
33
+ const db = { host: "127.0.0.1", port: 3001, channelId: "bridge" };
34
+ const missingOpenid = new OfficialAtMessageDispatcher({
35
+ groupOpenid: "",
36
+ db,
37
+ forward
38
+ });
39
+ await missingOpenid.handleGroupAtMessage({
40
+ id: "1",
41
+ group_openid: "G1",
42
+ content: "hi",
43
+ author: { member_openid: "M1", username: "U", bot: false }
44
+ });
45
+ assert.equal(forwarded.length, 0);
46
+ const d = new OfficialAtMessageDispatcher({
47
+ groupOpenid: "G1",
48
+ db,
49
+ forward
50
+ });
51
+ await d.handleGroupAtMessage({
52
+ id: "2",
53
+ group_openid: "G1",
54
+ content: "bot echo",
55
+ author: { member_openid: "BOT", bot: true }
56
+ });
57
+ assert.equal(forwarded.length, 0);
58
+ await d.handleGroupAtMessage({
59
+ id: "3",
60
+ group_openid: "OTHER",
61
+ content: "wrong",
62
+ author: { member_openid: "M2", username: "Bob", bot: false }
63
+ });
64
+ assert.equal(forwarded.length, 0);
65
+ await d.handleGroupAtMessage({
66
+ id: "4",
67
+ group_openid: "G1",
68
+ content: "<@!x> hello world",
69
+ author: { member_openid: "M3", username: "Alice", bot: false }
70
+ });
71
+ assert.equal(forwarded.length, 1);
72
+ assert.deepEqual(forwarded[0], {
73
+ fromId: "qq_M3",
74
+ fromName: "Alice",
75
+ content: "hello world"
76
+ });
77
+ await d.handleGroupAtMessage({
78
+ id: "4",
79
+ group_openid: "G1",
80
+ content: "dup",
81
+ author: { member_openid: "M3", username: "Alice", bot: false }
82
+ });
83
+ assert.equal(forwarded.length, 1);
84
+ });
85
+ test("OfficialAtMessageDispatcher \u6307\u4EE4\u547D\u4E2D\u4E0D\u8F6C\u53D1", async () => {
86
+ const forwarded = [];
87
+ const handled = [];
88
+ const forward = async (_db, _a, _b, content) => {
89
+ forwarded.push(content);
90
+ };
91
+ const commandRouter = {
92
+ async handle(inbound) {
93
+ if (inbound.text === "ping" || inbound.text === "/ping") {
94
+ handled.push(inbound.text);
95
+ return true;
96
+ }
97
+ return false;
98
+ }
99
+ };
100
+ const d = new OfficialAtMessageDispatcher({
101
+ groupOpenid: "G1",
102
+ db: { host: "127.0.0.1", port: 3001, channelId: "bridge" },
103
+ forward,
104
+ commandRouter
105
+ });
106
+ await d.handleGroupAtMessage({
107
+ id: "c1",
108
+ group_openid: "G1",
109
+ content: "<@!x> /ping",
110
+ author: { member_openid: "M9", username: "Op", bot: false }
111
+ });
112
+ assert.deepEqual(handled, ["/ping"]);
113
+ assert.equal(forwarded.length, 0);
114
+ await d.handleGroupAtMessage({
115
+ id: "c2",
116
+ group_openid: "G1",
117
+ content: "<@!x> chat",
118
+ author: { member_openid: "M9", username: "Op", bot: false }
119
+ });
120
+ assert.equal(forwarded.length, 1);
121
+ assert.equal(forwarded[0], "chat");
122
+ });
123
+ //# sourceMappingURL=events.test.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/official/events.test.ts"],
4
+ "sourcesContent": ["import assert from \"node:assert/strict\";\nimport test from \"node:test\";\nimport {\n OfficialAtMessageDispatcher,\n attachmentsPlaceholder,\n extractOfficialText,\n} from \"./events.js\";\n\ntest(\"extractOfficialText \u53BB @ \u5E76\u62FC\u9644\u4EF6\u5360\u4F4D\", () => {\n assert.equal(\n extractOfficialText({\n content: \"<@!botid> hello\",\n attachments: [{ content_type: \"image/png\" }],\n }),\n \"hello [\u56FE\u7247]\"\n );\n});\n\ntest(\"attachmentsPlaceholder \u6309\u7C7B\u578B\u5206\u7C7B\", () => {\n assert.equal(\n attachmentsPlaceholder([\n { content_type: \"image/jpeg\" },\n { content_type: \"video/mp4\" },\n { filename: \"a.mp3\" },\n { filename: \"doc.pdf\" },\n ]),\n \"[\u56FE\u7247][\u89C6\u9891][\u8BED\u97F3][\u6587\u4EF6]\"\n );\n});\n\ntest(\"OfficialAtMessageDispatcher \u8FC7\u6EE4\u4E0E\u8F6C\u53D1\u8F7D\u8377\", async () => {\n const forwarded: Array<{ fromId: string; fromName: string; content: string }> = [];\n const forward = async (\n _db: unknown,\n fromId: string,\n fromName: string,\n content: string\n ): Promise<void> => {\n forwarded.push({ fromId, fromName, content });\n };\n\n const db = { host: \"127.0.0.1\", port: 3001, channelId: \"bridge\" };\n\n const missingOpenid = new OfficialAtMessageDispatcher({\n groupOpenid: \"\",\n db,\n forward,\n });\n await missingOpenid.handleGroupAtMessage({\n id: \"1\",\n group_openid: \"G1\",\n content: \"hi\",\n author: { member_openid: \"M1\", username: \"U\", bot: false },\n });\n assert.equal(forwarded.length, 0);\n\n const d = new OfficialAtMessageDispatcher({\n groupOpenid: \"G1\",\n db,\n forward,\n });\n\n await d.handleGroupAtMessage({\n id: \"2\",\n group_openid: \"G1\",\n content: \"bot echo\",\n author: { member_openid: \"BOT\", bot: true },\n });\n assert.equal(forwarded.length, 0);\n\n await d.handleGroupAtMessage({\n id: \"3\",\n group_openid: \"OTHER\",\n content: \"wrong\",\n author: { member_openid: \"M2\", username: \"Bob\", bot: false },\n });\n assert.equal(forwarded.length, 0);\n\n await d.handleGroupAtMessage({\n id: \"4\",\n group_openid: \"G1\",\n content: \"<@!x> hello world\",\n author: { member_openid: \"M3\", username: \"Alice\", bot: false },\n });\n assert.equal(forwarded.length, 1);\n assert.deepEqual(forwarded[0], {\n fromId: \"qq_M3\",\n fromName: \"Alice\",\n content: \"hello world\",\n });\n\n // \u53BB\u91CD\n await d.handleGroupAtMessage({\n id: \"4\",\n group_openid: \"G1\",\n content: \"dup\",\n author: { member_openid: \"M3\", username: \"Alice\", bot: false },\n });\n assert.equal(forwarded.length, 1);\n});\n\ntest(\"OfficialAtMessageDispatcher \u6307\u4EE4\u547D\u4E2D\u4E0D\u8F6C\u53D1\", async () => {\n const forwarded: string[] = [];\n const handled: string[] = [];\n const forward = async (_db: unknown, _a: string, _b: string, content: string): Promise<void> => {\n forwarded.push(content);\n };\n const commandRouter = {\n async handle(inbound: { text: string }): Promise<boolean> {\n if (inbound.text === \"ping\" || inbound.text === \"/ping\") {\n handled.push(inbound.text);\n return true;\n }\n return false;\n },\n };\n\n const d = new OfficialAtMessageDispatcher({\n groupOpenid: \"G1\",\n db: { host: \"127.0.0.1\", port: 3001, channelId: \"bridge\" },\n forward,\n commandRouter: commandRouter as never,\n });\n\n await d.handleGroupAtMessage({\n id: \"c1\",\n group_openid: \"G1\",\n content: \"<@!x> /ping\",\n author: { member_openid: \"M9\", username: \"Op\", bot: false },\n });\n assert.deepEqual(handled, [\"/ping\"]);\n assert.equal(forwarded.length, 0);\n\n await d.handleGroupAtMessage({\n id: \"c2\",\n group_openid: \"G1\",\n content: \"<@!x> chat\",\n author: { member_openid: \"M9\", username: \"Op\", bot: false },\n });\n assert.equal(forwarded.length, 1);\n assert.equal(forwarded[0], \"chat\");\n});\n\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }