@sfmc-bds/qq-bridge 0.1.1-beta.3 → 0.2.0-beta.4
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.
- package/README.md +14 -3
- package/dist/commands/db-api.js +89 -0
- package/dist/commands/db-api.js.map +7 -0
- package/dist/commands/handlers.js +660 -0
- package/dist/commands/handlers.js.map +7 -0
- package/dist/commands/index.js +55 -0
- package/dist/commands/index.js.map +7 -0
- package/dist/commands/interaction-router.js +149 -0
- package/dist/commands/interaction-router.js.map +7 -0
- package/dist/commands/join-settings-ui.js +108 -0
- package/dist/commands/join-settings-ui.js.map +7 -0
- package/dist/commands/join-settings-ui.test.js +43 -0
- package/dist/commands/join-settings-ui.test.js.map +7 -0
- package/dist/commands/menu-format.js +45 -0
- package/dist/commands/menu-format.js.map +7 -0
- package/dist/commands/menu-panel-sync.js +130 -0
- package/dist/commands/menu-panel-sync.js.map +7 -0
- package/dist/commands/menu-panel-sync.test.js +27 -0
- package/dist/commands/menu-panel-sync.test.js.map +7 -0
- package/dist/commands/pending.js +33 -0
- package/dist/commands/pending.js.map +7 -0
- package/dist/commands/registry.js +40 -0
- package/dist/commands/registry.js.map +7 -0
- package/dist/commands/render.js +46 -0
- package/dist/commands/render.js.map +7 -0
- package/dist/commands/render.test.js +44 -0
- package/dist/commands/render.test.js.map +7 -0
- package/dist/commands/reply-llbot.js +63 -0
- package/dist/commands/reply-llbot.js.map +7 -0
- package/dist/commands/reply-official.js +66 -0
- package/dist/commands/reply-official.js.map +7 -0
- package/dist/commands/router.js +53 -0
- package/dist/commands/router.js.map +7 -0
- package/dist/commands/router.test.js +92 -0
- package/dist/commands/router.test.js.map +7 -0
- package/dist/commands/types.js +1 -0
- package/dist/commands/types.js.map +7 -0
- package/dist/config.js +12 -2
- package/dist/config.js.map +2 -2
- package/dist/console.js +40 -20
- package/dist/console.js.map +2 -2
- package/dist/index.js +117 -20
- package/dist/index.js.map +2 -2
- package/dist/llbot-ws-api.js +90 -0
- package/dist/llbot-ws-api.js.map +7 -0
- package/dist/llbot-ws-api.test.js +32 -0
- package/dist/llbot-ws-api.test.js.map +7 -0
- package/dist/official/events.js +136 -0
- package/dist/official/events.js.map +7 -0
- package/dist/official/events.test.js +123 -0
- package/dist/official/events.test.js.map +7 -0
- package/dist/official/gateway.js +226 -0
- package/dist/official/gateway.js.map +7 -0
- package/dist/onebot.js +23 -5
- package/dist/onebot.js.map +2 -2
- package/dist/project-root.js +2 -2
- package/dist/project-root.js.map +2 -2
- package/dist/runtime-status.js +45 -0
- package/dist/runtime-status.js.map +7 -0
- package/dist/ws-server.js +4 -0
- package/dist/ws-server.js.map +2 -2
- package/package.json +6 -5
|
@@ -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": "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",
|
|
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
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import {
|
|
2
|
+
QQ_INTENT_GROUP_C2C_INTERACTION,
|
|
3
|
+
QqAccessTokenManager,
|
|
4
|
+
fetchGatewayUrl
|
|
5
|
+
} from "@sfmc-bds/sdk/node/qq-official";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
import { log } from "../log.js";
|
|
8
|
+
const localRequire = createRequire(import.meta.url);
|
|
9
|
+
const OP_DISPATCH = 0;
|
|
10
|
+
const OP_HEARTBEAT = 1;
|
|
11
|
+
const OP_IDENTIFY = 2;
|
|
12
|
+
const OP_RESUME = 6;
|
|
13
|
+
const OP_RECONNECT = 7;
|
|
14
|
+
const OP_INVALID_SESSION = 9;
|
|
15
|
+
const OP_HELLO = 10;
|
|
16
|
+
const OP_HEARTBEAT_ACK = 11;
|
|
17
|
+
async function loadWs() {
|
|
18
|
+
try {
|
|
19
|
+
return localRequire("ws");
|
|
20
|
+
} catch {
|
|
21
|
+
return localRequire("../db-server/node_modules/ws");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function startOfficialGateway(opts) {
|
|
25
|
+
const tokenManager = opts.tokenManager ?? new QqAccessTokenManager(opts.creds);
|
|
26
|
+
let stopped = false;
|
|
27
|
+
let ws = null;
|
|
28
|
+
let heartbeatTimer = null;
|
|
29
|
+
let reconnectTimer = null;
|
|
30
|
+
let sessionId = null;
|
|
31
|
+
let lastSeq = null;
|
|
32
|
+
let reconnectAttempt = 0;
|
|
33
|
+
let botSelfId = null;
|
|
34
|
+
const clearHeartbeat = () => {
|
|
35
|
+
if (heartbeatTimer) {
|
|
36
|
+
clearInterval(heartbeatTimer);
|
|
37
|
+
heartbeatTimer = null;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const sendJson = (payload) => {
|
|
41
|
+
if (!ws || ws.readyState !== 1) return;
|
|
42
|
+
ws.send(JSON.stringify(payload));
|
|
43
|
+
};
|
|
44
|
+
const startHeartbeat = (intervalMs) => {
|
|
45
|
+
clearHeartbeat();
|
|
46
|
+
const period = Math.max(5e3, intervalMs);
|
|
47
|
+
heartbeatTimer = setInterval(() => {
|
|
48
|
+
sendJson({ op: OP_HEARTBEAT, d: lastSeq });
|
|
49
|
+
}, period);
|
|
50
|
+
};
|
|
51
|
+
const handleDispatch = async (t, d, s) => {
|
|
52
|
+
if (typeof s === "number") lastSeq = s;
|
|
53
|
+
if (t && t !== "READY" && t !== "RESUMED") {
|
|
54
|
+
const preview = t === "GROUP_AT_MESSAGE_CREATE" || t === "C2C_MESSAGE_CREATE" ? (() => {
|
|
55
|
+
const msg = d;
|
|
56
|
+
return ` group=${msg.group_openid ?? "\u2014"} author=${msg.author?.id ?? "\u2014"} content=${JSON.stringify(String(msg.content ?? "").slice(0, 80))}`;
|
|
57
|
+
})() : "";
|
|
58
|
+
log.info(`\u5B98\u65B9 Dispatch t=${t}${preview}`);
|
|
59
|
+
}
|
|
60
|
+
if (t === "READY") {
|
|
61
|
+
const ready = d;
|
|
62
|
+
sessionId = String(ready.session_id ?? "") || null;
|
|
63
|
+
botSelfId = ready.user?.id ? String(ready.user.id) : null;
|
|
64
|
+
reconnectAttempt = 0;
|
|
65
|
+
log.info(
|
|
66
|
+
`\u5B98\u65B9 Gateway READY (session=${sessionId ?? "?"}, bot=${ready.user?.username ?? botSelfId ?? "?"}, intents=${QQ_INTENT_GROUP_C2C_INTERACTION})`
|
|
67
|
+
);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (t === "RESUMED") {
|
|
71
|
+
reconnectAttempt = 0;
|
|
72
|
+
log.info("\u5B98\u65B9 Gateway \u4F1A\u8BDD\u5DF2\u6062\u590D (RESUMED)");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (t === "GROUP_AT_MESSAGE_CREATE") {
|
|
76
|
+
await opts.dispatcher.handleGroupAtMessage(d);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (t === "C2C_MESSAGE_CREATE") {
|
|
80
|
+
await opts.dispatcher.handleC2cMessage(d);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (t === "INTERACTION_CREATE") {
|
|
84
|
+
if (opts.interactionRouter) {
|
|
85
|
+
await opts.interactionRouter.handle(d);
|
|
86
|
+
} else {
|
|
87
|
+
log.warn("\u6536\u5230 INTERACTION_CREATE \u4F46\u672A\u914D\u7F6E interactionRouter");
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const scheduleReconnect = () => {
|
|
93
|
+
if (stopped) return;
|
|
94
|
+
clearHeartbeat();
|
|
95
|
+
const delay = Math.min(6e4, 1e3 * 2 ** Math.min(reconnectAttempt, 5));
|
|
96
|
+
reconnectAttempt += 1;
|
|
97
|
+
log.warn(`\u5B98\u65B9 Gateway \u5C06\u5728 ${delay}ms \u540E\u91CD\u8FDE (attempt=${reconnectAttempt})`);
|
|
98
|
+
reconnectTimer = setTimeout(() => {
|
|
99
|
+
void connect();
|
|
100
|
+
}, delay);
|
|
101
|
+
};
|
|
102
|
+
const connect = async () => {
|
|
103
|
+
if (stopped) return;
|
|
104
|
+
try {
|
|
105
|
+
const token = await tokenManager.getAccessToken();
|
|
106
|
+
const gatewayUrl = await fetchGatewayUrl(opts.creds, { tokenManager });
|
|
107
|
+
const wsMod = await loadWs();
|
|
108
|
+
const { WebSocket } = wsMod;
|
|
109
|
+
await new Promise((resolve, reject) => {
|
|
110
|
+
const sock = new WebSocket(gatewayUrl);
|
|
111
|
+
ws = sock;
|
|
112
|
+
let helloDone = false;
|
|
113
|
+
const fail = (err) => {
|
|
114
|
+
try {
|
|
115
|
+
sock.close();
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
reject(err);
|
|
119
|
+
};
|
|
120
|
+
sock.on("open", () => {
|
|
121
|
+
log.info(`\u5B98\u65B9 Gateway \u5DF2\u8FDE\u63A5 ${gatewayUrl}`);
|
|
122
|
+
});
|
|
123
|
+
sock.on("message", (raw) => {
|
|
124
|
+
void (async () => {
|
|
125
|
+
let payload;
|
|
126
|
+
try {
|
|
127
|
+
payload = JSON.parse(raw.toString("utf-8"));
|
|
128
|
+
} catch (e) {
|
|
129
|
+
log.error(`\u5B98\u65B9 Gateway \u89E3\u6790\u5931\u8D25: ${e.message}`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (payload.op === OP_HELLO) {
|
|
133
|
+
const interval = Number(
|
|
134
|
+
payload.d?.heartbeat_interval ?? 41250
|
|
135
|
+
);
|
|
136
|
+
startHeartbeat(interval);
|
|
137
|
+
if (sessionId && lastSeq !== null) {
|
|
138
|
+
sendJson({
|
|
139
|
+
op: OP_RESUME,
|
|
140
|
+
d: {
|
|
141
|
+
token: `QQBot ${token}`,
|
|
142
|
+
session_id: sessionId,
|
|
143
|
+
seq: lastSeq
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
} else {
|
|
147
|
+
log.info(`\u5B98\u65B9 Gateway Identify intents=${QQ_INTENT_GROUP_C2C_INTERACTION} (GROUP_C2C+INTERACTION)`);
|
|
148
|
+
sendJson({
|
|
149
|
+
op: OP_IDENTIFY,
|
|
150
|
+
d: {
|
|
151
|
+
token: `QQBot ${token}`,
|
|
152
|
+
intents: QQ_INTENT_GROUP_C2C_INTERACTION,
|
|
153
|
+
shard: [0, 1],
|
|
154
|
+
properties: {
|
|
155
|
+
$os: process.platform,
|
|
156
|
+
$browser: "sfmc-qq-bridge",
|
|
157
|
+
$device: "sfmc-qq-bridge"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
helloDone = true;
|
|
163
|
+
resolve();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (payload.op === OP_HEARTBEAT_ACK) return;
|
|
167
|
+
if (payload.op === OP_RECONNECT) {
|
|
168
|
+
log.warn("\u5B98\u65B9 Gateway \u8981\u6C42\u91CD\u8FDE (op=7)");
|
|
169
|
+
try {
|
|
170
|
+
sock.close();
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (payload.op === OP_INVALID_SESSION) {
|
|
176
|
+
log.warn("\u5B98\u65B9 Gateway Invalid Session\uFF0C\u6E05\u7A7A session \u540E\u91CD\u8FDE");
|
|
177
|
+
sessionId = null;
|
|
178
|
+
lastSeq = null;
|
|
179
|
+
try {
|
|
180
|
+
sock.close();
|
|
181
|
+
} catch {
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (payload.op === OP_DISPATCH) {
|
|
186
|
+
await handleDispatch(payload.t, payload.d, payload.s ?? null);
|
|
187
|
+
}
|
|
188
|
+
})();
|
|
189
|
+
});
|
|
190
|
+
sock.on("close", () => {
|
|
191
|
+
clearHeartbeat();
|
|
192
|
+
ws = null;
|
|
193
|
+
if (!helloDone) {
|
|
194
|
+
fail(new Error("Gateway \u5728 Hello \u524D\u5173\u95ED"));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (!stopped) scheduleReconnect();
|
|
198
|
+
});
|
|
199
|
+
sock.on("error", (err) => {
|
|
200
|
+
log.error(`\u5B98\u65B9 Gateway \u9519\u8BEF: ${err.message}`);
|
|
201
|
+
if (!helloDone) fail(err);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
} catch (e) {
|
|
205
|
+
log.error(`\u5B98\u65B9 Gateway \u8FDE\u63A5\u5931\u8D25: ${e.message}`);
|
|
206
|
+
scheduleReconnect();
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
await connect();
|
|
210
|
+
return {
|
|
211
|
+
stop: () => {
|
|
212
|
+
stopped = true;
|
|
213
|
+
clearHeartbeat();
|
|
214
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
215
|
+
try {
|
|
216
|
+
ws?.close();
|
|
217
|
+
} catch {
|
|
218
|
+
}
|
|
219
|
+
ws = null;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
export {
|
|
224
|
+
startOfficialGateway
|
|
225
|
+
};
|
|
226
|
+
//# sourceMappingURL=gateway.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/official/gateway.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * official/gateway.ts \u2014 QQ \u5F00\u653E\u5E73\u53F0 Gateway\uFF08WebSocket\uFF09\u5BA2\u6237\u7AEF\n *\n * \u6D41\u7A0B: getAccessToken \u2192 GET /gateway/bot \u2192 Hello \u2192 Identify \u2192 \u5FC3\u8DF3 / Resume\n * \u4EC5\u8BA2\u9605 GROUP_AND_C2C_EVENT\uFF0C\u5904\u7406 GROUP_AT_MESSAGE_CREATE\u3002\n */\n\nimport {\n QQ_INTENT_GROUP_C2C_INTERACTION,\n QqAccessTokenManager,\n fetchGatewayUrl,\n type QqOfficialCredentials,\n} from \"@sfmc-bds/sdk/node/qq-official\";\nimport { createRequire } from \"node:module\";\nimport { log } from \"../log.js\";\nimport type { InteractionRouter } from \"../commands/interaction-router.js\";\nimport type { OfficialAtMessageDispatcher, OfficialC2cMessage, OfficialGroupAtMessage } from \"./events.js\";\n\nconst localRequire = createRequire(import.meta.url);\n\nconst OP_DISPATCH = 0;\nconst OP_HEARTBEAT = 1;\nconst OP_IDENTIFY = 2;\nconst OP_RESUME = 6;\nconst OP_RECONNECT = 7;\nconst OP_INVALID_SESSION = 9;\nconst OP_HELLO = 10;\nconst OP_HEARTBEAT_ACK = 11;\n\ntype WsPayload = {\n op: number;\n d?: unknown;\n s?: number | null;\n t?: string;\n};\n\nexport type OfficialGatewayOptions = {\n creds: QqOfficialCredentials;\n dispatcher: OfficialAtMessageDispatcher;\n /** INTERACTION \u56DE\u8C03\u5206\u53D1 */\n interactionRouter?: InteractionRouter;\n /** \u6D4B\u8BD5\u53EF\u6CE8\u5165\uFF1B\u9ED8\u8BA4\u7528 tokenManager */\n tokenManager?: QqAccessTokenManager;\n};\n\nasync function loadWs(): Promise<typeof import(\"ws\")> {\n try {\n return localRequire(\"ws\") as typeof import(\"ws\");\n } catch {\n return localRequire(\"../db-server/node_modules/ws\") as typeof import(\"ws\");\n }\n}\n\n/**\n * \u542F\u52A8\u5B98\u65B9 Gateway\uFF1B\u8FD4\u56DE stop()\u3002\u65AD\u7EBF\u81EA\u52A8\u91CD\u8FDE\uFF08\u6307\u6570\u9000\u907F\uFF0C\u4E0A\u9650 60s\uFF09\u3002\n */\nexport async function startOfficialGateway(opts: OfficialGatewayOptions): Promise<{ stop: () => void }> {\n const tokenManager =\n opts.tokenManager ?? new QqAccessTokenManager(opts.creds);\n let stopped = false;\n let ws: import(\"ws\").WebSocket | null = null;\n let heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n let sessionId: string | null = null;\n let lastSeq: number | null = null;\n let reconnectAttempt = 0;\n let botSelfId: string | null = null;\n\n const clearHeartbeat = (): void => {\n if (heartbeatTimer) {\n clearInterval(heartbeatTimer);\n heartbeatTimer = null;\n }\n };\n\n const sendJson = (payload: WsPayload): void => {\n if (!ws || ws.readyState !== 1 /* OPEN */) return;\n ws.send(JSON.stringify(payload));\n };\n\n const startHeartbeat = (intervalMs: number): void => {\n clearHeartbeat();\n const period = Math.max(5000, intervalMs);\n heartbeatTimer = setInterval(() => {\n sendJson({ op: OP_HEARTBEAT, d: lastSeq });\n }, period);\n };\n\n const handleDispatch = async (t: string | undefined, d: unknown, s: number | null | undefined): Promise<void> => {\n if (typeof s === \"number\") lastSeq = s;\n // \u8BCA\u65AD\uFF1A\u4EFB\u4F55 Dispatch \u90FD\u6253\u4E00\u884C\uFF08\u6392\u969C\u300C@\u4E86\u4F46\u6CA1\u65E5\u5FD7\u300D\uFF09\n if (t && t !== \"READY\" && t !== \"RESUMED\") {\n const preview =\n t === \"GROUP_AT_MESSAGE_CREATE\" || t === \"C2C_MESSAGE_CREATE\"\n ? (() => {\n const msg = d as { group_openid?: string; content?: string; author?: { id?: string } };\n return ` group=${msg.group_openid ?? \"\u2014\"} author=${msg.author?.id ?? \"\u2014\"} content=${JSON.stringify(String(msg.content ?? \"\").slice(0, 80))}`;\n })()\n : \"\";\n log.info(`\u5B98\u65B9 Dispatch t=${t}${preview}`);\n }\n if (t === \"READY\") {\n const ready = d as { session_id?: string; user?: { id?: string; username?: string } };\n sessionId = String(ready.session_id ?? \"\") || null;\n botSelfId = ready.user?.id ? String(ready.user.id) : null;\n reconnectAttempt = 0;\n log.info(\n `\u5B98\u65B9 Gateway READY (session=${sessionId ?? \"?\"}, bot=${ready.user?.username ?? botSelfId ?? \"?\"}, intents=${QQ_INTENT_GROUP_C2C_INTERACTION})`\n );\n return;\n }\n if (t === \"RESUMED\") {\n reconnectAttempt = 0;\n log.info(\"\u5B98\u65B9 Gateway \u4F1A\u8BDD\u5DF2\u6062\u590D (RESUMED)\");\n return;\n }\n if (t === \"GROUP_AT_MESSAGE_CREATE\") {\n await opts.dispatcher.handleGroupAtMessage(d as OfficialGroupAtMessage);\n return;\n }\n if (t === \"C2C_MESSAGE_CREATE\") {\n await opts.dispatcher.handleC2cMessage(d as OfficialC2cMessage);\n return;\n }\n if (t === \"INTERACTION_CREATE\") {\n if (opts.interactionRouter) {\n await opts.interactionRouter.handle(d);\n } else {\n log.warn(\"\u6536\u5230 INTERACTION_CREATE \u4F46\u672A\u914D\u7F6E interactionRouter\");\n }\n return;\n }\n };\n\n const scheduleReconnect = (): void => {\n if (stopped) return;\n clearHeartbeat();\n const delay = Math.min(60_000, 1000 * 2 ** Math.min(reconnectAttempt, 5));\n reconnectAttempt += 1;\n log.warn(`\u5B98\u65B9 Gateway \u5C06\u5728 ${delay}ms \u540E\u91CD\u8FDE (attempt=${reconnectAttempt})`);\n reconnectTimer = setTimeout(() => {\n void connect();\n }, delay);\n };\n\n const connect = async (): Promise<void> => {\n if (stopped) return;\n try {\n const token = await tokenManager.getAccessToken();\n const gatewayUrl = await fetchGatewayUrl(opts.creds, { tokenManager });\n const wsMod = await loadWs();\n const { WebSocket } = wsMod;\n\n await new Promise<void>((resolve, reject) => {\n const sock = new WebSocket(gatewayUrl);\n ws = sock;\n let helloDone = false;\n\n const fail = (err: Error): void => {\n try {\n sock.close();\n } catch {\n /* ignore */\n }\n reject(err);\n };\n\n sock.on(\"open\", () => {\n log.info(`\u5B98\u65B9 Gateway \u5DF2\u8FDE\u63A5 ${gatewayUrl}`);\n });\n\n sock.on(\"message\", (raw) => {\n void (async () => {\n let payload: WsPayload;\n try {\n payload = JSON.parse(raw.toString(\"utf-8\")) as WsPayload;\n } catch (e) {\n log.error(`\u5B98\u65B9 Gateway \u89E3\u6790\u5931\u8D25: ${(e as Error).message}`);\n return;\n }\n\n if (payload.op === OP_HELLO) {\n const interval = Number(\n (payload.d as { heartbeat_interval?: number } | undefined)?.heartbeat_interval ?? 41250\n );\n startHeartbeat(interval);\n if (sessionId && lastSeq !== null) {\n sendJson({\n op: OP_RESUME,\n d: {\n token: `QQBot ${token}`,\n session_id: sessionId,\n seq: lastSeq,\n },\n });\n } else {\n log.info(`\u5B98\u65B9 Gateway Identify intents=${QQ_INTENT_GROUP_C2C_INTERACTION} (GROUP_C2C+INTERACTION)`);\n sendJson({\n op: OP_IDENTIFY,\n d: {\n token: `QQBot ${token}`,\n intents: QQ_INTENT_GROUP_C2C_INTERACTION,\n shard: [0, 1],\n properties: {\n $os: process.platform,\n $browser: \"sfmc-qq-bridge\",\n $device: \"sfmc-qq-bridge\",\n },\n },\n });\n }\n helloDone = true;\n resolve();\n return;\n }\n\n if (payload.op === OP_HEARTBEAT_ACK) return;\n\n if (payload.op === OP_RECONNECT) {\n log.warn(\"\u5B98\u65B9 Gateway \u8981\u6C42\u91CD\u8FDE (op=7)\");\n try {\n sock.close();\n } catch {\n /* ignore */\n }\n return;\n }\n\n if (payload.op === OP_INVALID_SESSION) {\n log.warn(\"\u5B98\u65B9 Gateway Invalid Session\uFF0C\u6E05\u7A7A session \u540E\u91CD\u8FDE\");\n sessionId = null;\n lastSeq = null;\n try {\n sock.close();\n } catch {\n /* ignore */\n }\n return;\n }\n\n if (payload.op === OP_DISPATCH) {\n await handleDispatch(payload.t, payload.d, payload.s ?? null);\n }\n })();\n });\n\n sock.on(\"close\", () => {\n clearHeartbeat();\n ws = null;\n if (!helloDone) {\n fail(new Error(\"Gateway \u5728 Hello \u524D\u5173\u95ED\"));\n return;\n }\n if (!stopped) scheduleReconnect();\n });\n\n sock.on(\"error\", (err) => {\n log.error(`\u5B98\u65B9 Gateway \u9519\u8BEF: ${err.message}`);\n if (!helloDone) fail(err);\n });\n });\n } catch (e) {\n log.error(`\u5B98\u65B9 Gateway \u8FDE\u63A5\u5931\u8D25: ${(e as Error).message}`);\n scheduleReconnect();\n }\n };\n\n await connect();\n\n return {\n stop: () => {\n stopped = true;\n clearHeartbeat();\n if (reconnectTimer) clearTimeout(reconnectTimer);\n try {\n ws?.close();\n } catch {\n /* ignore */\n }\n ws = null;\n },\n };\n}\n\n/** \u4F9B status \u547D\u4EE4\u8BFB\u53D6\uFF08\u5F53\u524D\u8FDB\u7A0B\u5185\u65E0\u5168\u5C40 self_id \u5BFC\u51FA\u65F6\u7531 index \u6301\u6709\uFF09 */\nexport type OfficialGatewayHandle = Awaited<ReturnType<typeof startOfficialGateway>>;\n\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|