@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.
- package/LICENSE +663 -0
- 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/dbserver.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 +18 -15
|
@@ -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
|
+
}
|
package/dist/onebot.js
CHANGED
|
@@ -88,8 +88,10 @@ class OneBotDispatcher {
|
|
|
88
88
|
botSelfId = null;
|
|
89
89
|
opts;
|
|
90
90
|
dedup = new RecentMessageDedup();
|
|
91
|
+
forward;
|
|
91
92
|
constructor(opts) {
|
|
92
93
|
this.opts = opts;
|
|
94
|
+
this.forward = opts.forward ?? tryForward;
|
|
93
95
|
}
|
|
94
96
|
/** 测试/调试用 */
|
|
95
97
|
get selfId() {
|
|
@@ -112,15 +114,31 @@ class OneBotDispatcher {
|
|
|
112
114
|
if (this.dedup.seen(event.message_id)) return;
|
|
113
115
|
const text = extractText(event.message);
|
|
114
116
|
if (!text) return;
|
|
117
|
+
const senderCard = event.sender?.card;
|
|
118
|
+
const senderNick = event.sender?.nickname;
|
|
119
|
+
const fromName = senderCard || senderNick || `QQ_${event.user_id}`;
|
|
120
|
+
const userId = String(event.user_id);
|
|
121
|
+
const fromId = `qq_${userId}`;
|
|
122
|
+
const groupId = String(event.group_id);
|
|
123
|
+
if (this.opts.commandRouter) {
|
|
124
|
+
const role = String(event.sender?.role ?? "").toLowerCase();
|
|
125
|
+
const isGroupAdmin = role === "owner" || role === "admin";
|
|
126
|
+
const inbound = {
|
|
127
|
+
backend: "llbot",
|
|
128
|
+
groupId,
|
|
129
|
+
userId,
|
|
130
|
+
userName: fromName,
|
|
131
|
+
text,
|
|
132
|
+
isGroupAdmin,
|
|
133
|
+
...event.message_id !== void 0 ? { msgId: String(event.message_id) } : {}
|
|
134
|
+
};
|
|
135
|
+
if (await this.opts.commandRouter.handle(inbound)) return;
|
|
136
|
+
}
|
|
115
137
|
if (!this.opts.db.channelId) {
|
|
116
138
|
log.warn("bridge_channel_id \u672A\u914D\u7F6E,\u8DF3\u8FC7 (\u53EF\u7528 reload \u91CD\u8BFB\u914D\u7F6E)");
|
|
117
139
|
return;
|
|
118
140
|
}
|
|
119
|
-
|
|
120
|
-
const senderNick = event.sender?.nickname;
|
|
121
|
-
const fromName = senderCard || senderNick || `QQ_${event.user_id}`;
|
|
122
|
-
const fromId = `qq_${String(event.user_id)}`;
|
|
123
|
-
await tryForward(this.opts.db, fromId, fromName, text);
|
|
141
|
+
await this.forward(this.opts.db, fromId, fromName, text);
|
|
124
142
|
}
|
|
125
143
|
}
|
|
126
144
|
export {
|
package/dist/onebot.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/onebot.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * onebot.ts \u2014 OneBot 11 \u4E8B\u4EF6\u5206\u53D1\n *\n * \
|
|
5
|
-
"mappings": "
|
|
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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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/project-root.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { resolveRuntimeRoot } from "@sfmc-bds/sdk/node/config";
|
|
2
|
-
import { dirname
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
5
|
-
const PROJECT_ROOT = resolveRuntimeRoot(
|
|
5
|
+
const PROJECT_ROOT = resolveRuntimeRoot(HERE);
|
|
6
6
|
export {
|
|
7
7
|
PROJECT_ROOT
|
|
8
8
|
};
|
package/dist/project-root.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/project-root.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * project-root.ts \u2014 \u4ED3\u6839\u552F\u4E00\u89E3\u6790\u70B9\uFF08DRY/DIP\
|
|
5
|
-
"mappings": "AAIA,SAAS,0BAA0B;AACnC,SAAS,
|
|
4
|
+
"sourcesContent": ["/**\n * project-root.ts \u2014 \u4ED3\u6839\u552F\u4E00\u89E3\u6790\u70B9\uFF08DRY/DIP\uFF1Aenv / log \u52FF\u5404\u81EA\u4E0A\u6EAF\uFF09\n */\n\nimport { resolveRuntimeRoot } from \"@sfmc-bds/sdk/node/config\";\nimport { dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst HERE = dirname(fileURLToPath(import.meta.url));\n\n/** SFMC_ROOT > \u4ECE\u672C\u6587\u4EF6\u5411\u4E0A\u627E sfmc-monorepo */\nexport const PROJECT_ROOT: string = resolveRuntimeRoot(HERE);\n"],
|
|
5
|
+
"mappings": "AAIA,SAAS,0BAA0B;AACnC,SAAS,eAAe;AACxB,SAAS,qBAAqB;AAE9B,MAAM,OAAO,QAAQ,cAAc,YAAY,GAAG,CAAC;AAG5C,MAAM,eAAuB,mBAAmB,IAAI;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
qqRuntimeStatusPath
|
|
3
|
+
} from "@sfmc-bds/sdk/node/config";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { PROJECT_ROOT } from "./project-root.js";
|
|
7
|
+
function writeQqRuntimeStatus(backend) {
|
|
8
|
+
const status = {
|
|
9
|
+
pid: process.pid,
|
|
10
|
+
backend,
|
|
11
|
+
startedAt: Date.now()
|
|
12
|
+
};
|
|
13
|
+
const file = qqRuntimeStatusPath(PROJECT_ROOT);
|
|
14
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
15
|
+
fs.writeFileSync(file, `${JSON.stringify(status, null, 2)}
|
|
16
|
+
`, "utf8");
|
|
17
|
+
}
|
|
18
|
+
function clearQqRuntimeStatus() {
|
|
19
|
+
const file = qqRuntimeStatusPath(PROJECT_ROOT);
|
|
20
|
+
try {
|
|
21
|
+
if (fs.existsSync(file)) fs.unlinkSync(file);
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function installQqRuntimeStatusHooks(backend) {
|
|
26
|
+
writeQqRuntimeStatus(backend);
|
|
27
|
+
const clear = () => {
|
|
28
|
+
clearQqRuntimeStatus();
|
|
29
|
+
};
|
|
30
|
+
process.once("exit", clear);
|
|
31
|
+
process.once("SIGINT", () => {
|
|
32
|
+
clear();
|
|
33
|
+
process.exit(0);
|
|
34
|
+
});
|
|
35
|
+
process.once("SIGTERM", () => {
|
|
36
|
+
clear();
|
|
37
|
+
process.exit(0);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
clearQqRuntimeStatus,
|
|
42
|
+
installQqRuntimeStatusHooks,
|
|
43
|
+
writeQqRuntimeStatus
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=runtime-status.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/runtime-status.ts"],
|
|
4
|
+
"sourcesContent": ["/**\r\n * runtime-status.ts \u2014 \u5199/\u6E05 `.sfmc/qq.runtime.json`\uFF0C\u4F9B CLI \u63A2\u6D4B\u5916\u90E8 qq\r\n */\r\n\r\nimport {\r\n qqRuntimeStatusPath,\r\n type QqRuntimeStatus,\r\n} from \"@sfmc-bds/sdk/node/config\";\r\nimport fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport { PROJECT_ROOT } from \"./project-root.js\";\r\n\r\nexport function writeQqRuntimeStatus(backend: \"official\" | \"llbot\"): void {\r\n const status: QqRuntimeStatus = {\r\n pid: process.pid,\r\n backend,\r\n startedAt: Date.now(),\r\n };\r\n const file = qqRuntimeStatusPath(PROJECT_ROOT);\r\n fs.mkdirSync(path.dirname(file), { recursive: true });\r\n fs.writeFileSync(file, `${JSON.stringify(status, null, 2)}\\n`, \"utf8\");\r\n}\r\n\r\nexport function clearQqRuntimeStatus(): void {\r\n const file = qqRuntimeStatusPath(PROJECT_ROOT);\r\n try {\r\n if (fs.existsSync(file)) fs.unlinkSync(file);\r\n } catch {\r\n /* ignore */\r\n }\r\n}\r\n\r\n/** \u6CE8\u518C\u8FDB\u7A0B\u9000\u51FA\u65F6\u6E05\u7406\u5FC3\u8DF3\u6587\u4EF6 */\r\nexport function installQqRuntimeStatusHooks(backend: \"official\" | \"llbot\"): void {\r\n writeQqRuntimeStatus(backend);\r\n const clear = (): void => {\r\n clearQqRuntimeStatus();\r\n };\r\n process.once(\"exit\", clear);\r\n process.once(\"SIGINT\", () => {\r\n clear();\r\n process.exit(0);\r\n });\r\n process.once(\"SIGTERM\", () => {\r\n clear();\r\n process.exit(0);\r\n });\r\n}\r\n\r"],
|
|
5
|
+
"mappings": "AAIA;AAAA,EACE;AAAA,OAEK;AACP,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,oBAAoB;AAEtB,SAAS,qBAAqB,SAAqC;AACxE,QAAM,SAA0B;AAAA,IAC9B,KAAK,QAAQ;AAAA,IACb;AAAA,IACA,WAAW,KAAK,IAAI;AAAA,EACtB;AACA,QAAM,OAAO,oBAAoB,YAAY;AAC7C,KAAG,UAAU,KAAK,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AACpD,KAAG,cAAc,MAAM,GAAG,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AAAA,GAAM,MAAM;AACvE;AAEO,SAAS,uBAA6B;AAC3C,QAAM,OAAO,oBAAoB,YAAY;AAC7C,MAAI;AACF,QAAI,GAAG,WAAW,IAAI,EAAG,IAAG,WAAW,IAAI;AAAA,EAC7C,QAAQ;AAAA,EAER;AACF;AAGO,SAAS,4BAA4B,SAAqC;AAC/E,uBAAqB,OAAO;AAC5B,QAAM,QAAQ,MAAY;AACxB,yBAAqB;AAAA,EACvB;AACA,UAAQ,KAAK,QAAQ,KAAK;AAC1B,UAAQ,KAAK,UAAU,MAAM;AAC3B,UAAM;AACN,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACD,UAAQ,KAAK,WAAW,MAAM;AAC5B,UAAM;AACN,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACH;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/ws-server.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { WebSocket, WebSocketServer } from "ws";
|
|
3
|
+
import { llbotWsApi } from "./llbot-ws-api.js";
|
|
3
4
|
import { log } from "./log.js";
|
|
4
5
|
const localRequire = createRequire(import.meta.url);
|
|
5
6
|
async function loadWs() {
|
|
@@ -17,16 +18,19 @@ async function startWsServer(opts) {
|
|
|
17
18
|
wss.on("connection", (sock, req) => {
|
|
18
19
|
const path = req.url ?? "/";
|
|
19
20
|
log.info(`LLBot \u5DF2\u8FDE\u63A5 (${path})`);
|
|
21
|
+
llbotWsApi.attach(sock);
|
|
20
22
|
sock.on("message", (raw) => {
|
|
21
23
|
try {
|
|
22
24
|
const text = raw.toString("utf-8");
|
|
23
25
|
const parsed = JSON.parse(text);
|
|
26
|
+
if (llbotWsApi.tryHandleResponse(parsed)) return;
|
|
24
27
|
void opts.dispatcher.handle(parsed);
|
|
25
28
|
} catch (e) {
|
|
26
29
|
log.error(`\u89E3\u6790\u6D88\u606F\u5931\u8D25: ${e.message}`);
|
|
27
30
|
}
|
|
28
31
|
});
|
|
29
32
|
sock.on("close", () => {
|
|
33
|
+
llbotWsApi.detach(sock);
|
|
30
34
|
log.info("LLBot \u5DF2\u65AD\u5F00");
|
|
31
35
|
});
|
|
32
36
|
sock.on("error", (err) => {
|
package/dist/ws-server.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/ws-server.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * ws-server.ts \u2014 WebSocket \u670D\u52A1
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/**\n * ws-server.ts \u2014 WebSocket \u670D\u52A1\u7AEF\uFF08\u9762\u5411 LLBot reverse-ws \u53CD\u5411\u8FDE\u63A5\uFF09\n *\n * \u76D1\u542C\u7AEF\u53E3\uFF1A\u9ED8\u8BA4 3002\n * \u6838\u5FC3\u673A\u5236\uFF1A\n * - \u4F9D\u8D56\u52A0\u8F7D\uFF1A\u4F18\u5148\u52A0\u8F7D\u672C\u5730 `ws` \u6A21\u5757\uFF0C\u7F3A\u5931\u65F6\u56DE\u9000\u81F3\u5DE5\u4F5C\u533A\u5171\u4EAB\u4F9D\u8D56\n * - \u6D88\u606F\u89E3\u5305\u4E0E\u8DEF\u7531\u5206\u6D41\uFF1A\u81EA\u52A8\u5C06\u63A5\u6536\u5230\u7684 WebSocket \u5E27\u89E3\u6790\u4E3A JSON\uFF0CAPI echo \u56DE\u5305\u4F18\u5148\u6D88\u8D39\uFF0C\u5176\u4F59\u4E8B\u4EF6\u6295\u9012\u81F3\u4E8B\u4EF6\u5206\u53D1\u5668\n */\n\nimport type { IncomingMessage } from \"node:http\";\nimport { createRequire } from \"node:module\";\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { llbotWsApi } from \"./llbot-ws-api.js\";\nimport { log } from \"./log.js\";\nimport type { OneBotDispatcher } from \"./onebot.js\";\n\nexport interface WsServerOptions {\n port: number;\n dispatcher: OneBotDispatcher;\n}\n\nconst localRequire = createRequire(import.meta.url);\n\nasync function loadWs(): Promise<typeof import(\"ws\")> {\n try {\n return localRequire(\"ws\") as typeof import(\"ws\");\n } catch {\n // \u515C\u5E95\uFF1A\u590D\u7528 db-server \u7684 ws\n return localRequire(\"../db-server/node_modules/ws\") as typeof import(\"ws\");\n }\n}\n\n/**\n * \u542F\u52A8 WebSocket \u670D\u52A1\u5668\u5E76\u6302\u63A5 LLBot \u53CD\u5411\u8FDE\u63A5\u76D1\u542C\u3002\n *\n * @param opts \u5305\u542B\u76D1\u542C\u7AEF\u53E3\u4E0E OneBot \u4E8B\u4EF6\u5206\u53D1\u5668\u7684\u914D\u7F6E\u9879\u3002\n * @returns \u8FD0\u884C\u4E2D\u7684 WebSocketServer \u5B9E\u4F8B\u3002\n */\nexport async function startWsServer(opts: WsServerOptions): Promise<WebSocketServer> {\n\n const ws = await loadWs();\n const { WebSocketServer: WSS } = ws;\n const wss = new WSS({ port: opts.port });\n log.info(`WebSocket \u670D\u52A1\u542F\u52A8 ws://0.0.0.0:${opts.port}`);\n\n wss.on(\"connection\", (sock: WebSocket, req: IncomingMessage) => {\n const path = req.url ?? \"/\";\n log.info(`LLBot \u5DF2\u8FDE\u63A5 (${path})`);\n llbotWsApi.attach(sock);\n sock.on(\"message\", (raw) => {\n try {\n const text = raw.toString(\"utf-8\");\n const parsed: unknown = JSON.parse(text);\n // API \u56DE\u5305\uFF08echo\uFF09\u4F18\u5148\u6D88\u8D39\uFF0C\u907F\u514D\u8BEF\u5165\u4E8B\u4EF6\u5206\u53D1\n if (llbotWsApi.tryHandleResponse(parsed)) return;\n // \u5F02\u6B65\u4F46\u4E0D\u963B\u585E socket \u5FAA\u73AF\n void opts.dispatcher.handle(parsed);\n } catch (e) {\n log.error(`\u89E3\u6790\u6D88\u606F\u5931\u8D25: ${(e as Error).message}`);\n }\n });\n sock.on(\"close\", () => {\n llbotWsApi.detach(sock);\n log.info(\"LLBot \u5DF2\u65AD\u5F00\");\n });\n sock.on(\"error\", (err) => {\n log.error(`WebSocket \u8FDE\u63A5\u9519\u8BEF: ${err.message}`);\n });\n });\n\n wss.on(\"error\", (err) => {\n log.error(`WebSocket \u670D\u52A1\u5668\u9519\u8BEF: ${err.message}`);\n });\n\n return wss;\n}\n"],
|
|
5
|
+
"mappings": "AAUA,SAAS,qBAAqB;AAC9B,SAAS,WAAW,uBAAuB;AAC3C,SAAS,kBAAkB;AAC3B,SAAS,WAAW;AAQpB,MAAM,eAAe,cAAc,YAAY,GAAG;AAElD,eAAe,SAAuC;AACpD,MAAI;AACF,WAAO,aAAa,IAAI;AAAA,EAC1B,QAAQ;AAEN,WAAO,aAAa,8BAA8B;AAAA,EACpD;AACF;AAQA,eAAsB,cAAc,MAAiD;AAEnF,QAAM,KAAK,MAAM,OAAO;AACxB,QAAM,EAAE,iBAAiB,IAAI,IAAI;AACjC,QAAM,MAAM,IAAI,IAAI,EAAE,MAAM,KAAK,KAAK,CAAC;AACvC,MAAI,KAAK,mDAA+B,KAAK,IAAI,EAAE;AAEnD,MAAI,GAAG,cAAc,CAAC,MAAiB,QAAyB;AAC9D,UAAM,OAAO,IAAI,OAAO;AACxB,QAAI,KAAK,6BAAc,IAAI,GAAG;AAC9B,eAAW,OAAO,IAAI;AACtB,SAAK,GAAG,WAAW,CAAC,QAAQ;AAC1B,UAAI;AACF,cAAM,OAAO,IAAI,SAAS,OAAO;AACjC,cAAM,SAAkB,KAAK,MAAM,IAAI;AAEvC,YAAI,WAAW,kBAAkB,MAAM,EAAG;AAE1C,aAAK,KAAK,WAAW,OAAO,MAAM;AAAA,MACpC,SAAS,GAAG;AACV,YAAI,MAAM,yCAAY,EAAY,OAAO,EAAE;AAAA,MAC7C;AAAA,IACF,CAAC;AACD,SAAK,GAAG,SAAS,MAAM;AACrB,iBAAW,OAAO,IAAI;AACtB,UAAI,KAAK,0BAAW;AAAA,IACtB,CAAC;AACD,SAAK,GAAG,SAAS,CAAC,QAAQ;AACxB,UAAI,MAAM,uCAAmB,IAAI,OAAO,EAAE;AAAA,IAC5C,CAAC;AAAA,EACH,CAAC;AAED,MAAI,GAAG,SAAS,CAAC,QAAQ;AACvB,QAAI,MAAM,6CAAoB,IAAI,OAAO,EAAE;AAAA,EAC7C,CAAC;AAED,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sfmc-bds/qq-bridge",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "SFMC QQ ↔ MC bridge —
|
|
3
|
+
"version": "0.2.0-beta.5",
|
|
4
|
+
"description": "SFMC QQ ↔ MC bridge — official QQ Open Platform Bot and/or LLBot OneBot 11",
|
|
5
5
|
"author": "Shiroha7z",
|
|
6
|
-
"license": "
|
|
6
|
+
"license": "AGPL-3.0-only",
|
|
7
7
|
"type": "module",
|
|
8
|
-
"homepage": "https://github.com/DogeLakeDev/ScriptsForMinecraftServer/tree/main/qq-bridge#readme",
|
|
8
|
+
"homepage": "https://github.com/DogeLakeDev/ScriptsForMinecraftServer/tree/main/packages/qq-bridge#readme",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/DogeLakeDev/ScriptsForMinecraftServer.git",
|
|
12
|
-
"directory": "qq-bridge"
|
|
12
|
+
"directory": "packages/qq-bridge"
|
|
13
13
|
},
|
|
14
14
|
"bugs": {
|
|
15
15
|
"url": "https://github.com/DogeLakeDev/ScriptsForMinecraftServer/issues"
|
|
@@ -30,21 +30,24 @@
|
|
|
30
30
|
"dist",
|
|
31
31
|
"README.md"
|
|
32
32
|
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"ws": "^8.21.1",
|
|
35
|
+
"@sfmc-bds/sdk": "0.2.0-beta.10"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@sfmc-bds/tools": "0.2.0-beta.11"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"registry": "https://registry.npmjs.org/"
|
|
43
|
+
},
|
|
33
44
|
"scripts": {
|
|
34
45
|
"build": "sfmc-esbuild-transpile",
|
|
35
46
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
36
47
|
"dev": "tsx src/index.ts",
|
|
37
48
|
"format": "prettier --write \"src/**/*.{ts,tsx,js,json}\"",
|
|
38
|
-
"prepublishOnly": "npm run build",
|
|
39
49
|
"start": "node dist/index.js",
|
|
50
|
+
"test": "pnpm run build && node --test \"dist/**/*.test.js\"",
|
|
40
51
|
"typecheck": "tsc7 --noEmit"
|
|
41
|
-
},
|
|
42
|
-
"dependencies": {
|
|
43
|
-
"@sfmc-bds/sdk": "^0.2.0-beta.6",
|
|
44
|
-
"ws": "^8.21.1"
|
|
45
|
-
},
|
|
46
|
-
"publishConfig": {
|
|
47
|
-
"access": "public",
|
|
48
|
-
"registry": "https://registry.npmjs.org/"
|
|
49
52
|
}
|
|
50
|
-
}
|
|
53
|
+
}
|