@sfmc-bds/qq-bridge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # @sfmc-bds/qq-bridge
2
+
3
+ SFMC QQ ↔ MC 桥接:LLBot OneBot 11 reverse-WS(端口 3002),将群消息转发至 db-server。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install @sfmc-bds/qq-bridge
9
+ node node_modules/@sfmc-bds/qq-bridge/dist/index.js
10
+ ```
11
+
12
+ 通常由 `@sfmc-bds/cli` 或 SEA 包统一拉起。
13
+
14
+ ## 依赖
15
+
16
+ - `@sfmc-bds/sdk` ^0.1.0
17
+ - Node.js >= 22.13
18
+
19
+ ## 仓库
20
+
21
+ <https://github.com/DogeLakeDev/ScriptsForMinecraftServer/tree/main/qq-bridge>
@@ -0,0 +1,20 @@
1
+ /**
2
+ * config.ts — 加载 configs/qq_config.json
3
+ *
4
+ * 行为与旧 index.js 保持完全一致:
5
+ * - 文件不存在或解析失败: 进程退出 (旧版 process.exit(1))
6
+ * - 字段缺失: 走默认值
7
+ * - reload 时仅覆盖原对象 (mutate), 保留运行时引用的同一份对象
8
+ */
9
+ import type { QQBridgeConfig } from "./types.js";
10
+ /** 统一通过 SDK 解析项目根:env SFMC_ROOT > __dirname 上溯。SEA / npm 一致。 */
11
+ export declare const ROOT_DIR: string;
12
+ export declare const CFG_PATH: string;
13
+ /** 进程启动时加载一次。失败直接退出,与旧实现一致。 */
14
+ export declare function loadInitialConfig(): QQBridgeConfig;
15
+ /**
16
+ * 重新读取配置文件,合并到传入对象上 (mutate)。
17
+ * 旧实现是 Object.assign(cfg, newCfg),保留运行时对原 cfg 对象的引用 — 同样行为。
18
+ */
19
+ export declare function reloadInto(cfg: QQBridgeConfig): void;
20
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAKjD,gEAAgE;AAChE,eAAO,MAAM,QAAQ,EAAE,MAA2D,CAAC;AACnF,eAAO,MAAM,QAAQ,EAAE,MAA+C,CAAC;AAqBvE,+BAA+B;AAC/B,wBAAgB,iBAAiB,IAAI,cAAc,CAOlD;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,cAAc,GAAG,IAAI,CAQpD"}
package/dist/config.js ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * config.ts — 加载 configs/qq_config.json
3
+ *
4
+ * 行为与旧 index.js 保持完全一致:
5
+ * - 文件不存在或解析失败: 进程退出 (旧版 process.exit(1))
6
+ * - 字段缺失: 走默认值
7
+ * - reload 时仅覆盖原对象 (mutate), 保留运行时引用的同一份对象
8
+ */
9
+ import { configPath, readJson, resolveRuntimeRoot } from "@sfmc-bds/sdk/node/config";
10
+ import { dirname, resolve } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { log } from "./log.js";
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+ /** 统一通过 SDK 解析项目根:env SFMC_ROOT > __dirname 上溯。SEA / npm 一致。 */
16
+ export const ROOT_DIR = resolveRuntimeRoot(resolve(__dirname, "..", ".."));
17
+ export const CFG_PATH = configPath(ROOT_DIR, "qq_config.json");
18
+ function applyDefaults(raw) {
19
+ return {
20
+ qq_enabled: raw.qq_enabled !== false,
21
+ qq_ws_port: parseInt(String(raw.qq_ws_port ?? "3002"), 10),
22
+ qq_group_id: String(raw.qq_group_id ?? ""),
23
+ bridge_channel_id: String(raw.bridge_channel_id ?? ""),
24
+ db_host: String(raw.db_host ?? "127.0.0.1"),
25
+ db_port: parseInt(String(raw.db_port ?? "3001"), 10),
26
+ mctoqq_prefix: String(raw.mctoqq_prefix ?? "[MC]"),
27
+ ...raw,
28
+ };
29
+ }
30
+ function readFromDisk() {
31
+ const raw = readJson(CFG_PATH);
32
+ if (!raw)
33
+ throw new Error(`配置文件不存在或无法解析: ${CFG_PATH}`);
34
+ return applyDefaults(raw);
35
+ }
36
+ /** 进程启动时加载一次。失败直接退出,与旧实现一致。 */
37
+ export function loadInitialConfig() {
38
+ try {
39
+ return readFromDisk();
40
+ }
41
+ catch (e) {
42
+ log.error(`无法读取配置: ${CFG_PATH}: ${e.message}`);
43
+ process.exit(1);
44
+ }
45
+ }
46
+ /**
47
+ * 重新读取配置文件,合并到传入对象上 (mutate)。
48
+ * 旧实现是 Object.assign(cfg, newCfg),保留运行时对原 cfg 对象的引用 — 同样行为。
49
+ */
50
+ export function reloadInto(cfg) {
51
+ try {
52
+ const fresh = readFromDisk();
53
+ Object.assign(cfg, fresh);
54
+ }
55
+ catch (e) {
56
+ // reload 失败不抛,旧实现也是只 log
57
+ log.error(`重载配置失败: ${e.message}`);
58
+ }
59
+ }
60
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACrF,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAG/B,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAEtC,gEAAgE;AAChE,MAAM,CAAC,MAAM,QAAQ,GAAW,kBAAkB,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;AACnF,MAAM,CAAC,MAAM,QAAQ,GAAW,UAAU,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;AAEvE,SAAS,aAAa,CAAC,GAA4B;IACjD,OAAO;QACL,UAAU,EAAE,GAAG,CAAC,UAAU,KAAK,KAAK;QACpC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC;QAC1D,WAAW,EAAE,MAAM,CAAC,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC;QAC1C,iBAAiB,EAAE,MAAM,CAAC,GAAG,CAAC,iBAAiB,IAAI,EAAE,CAAC;QACtD,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,OAAO,IAAI,WAAW,CAAC;QAC3C,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC;QACpD,aAAa,EAAE,MAAM,CAAC,GAAG,CAAC,aAAa,IAAI,MAAM,CAAC;QAClD,GAAG,GAAG;KACP,CAAC;AACJ,CAAC;AAED,SAAS,YAAY;IACnB,MAAM,GAAG,GAAG,QAAQ,CAA0B,QAAQ,CAAC,CAAC;IACxD,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,iBAAiB,QAAQ,EAAE,CAAC,CAAC;IACvD,OAAO,aAAa,CAAC,GAAG,CAAC,CAAC;AAC5B,CAAC;AAED,+BAA+B;AAC/B,MAAM,UAAU,iBAAiB;IAC/B,IAAI,CAAC;QACH,OAAO,YAAY,EAAE,CAAC;IACxB,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,GAAG,CAAC,KAAK,CAAC,WAAW,QAAQ,KAAM,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;QAC1D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,GAAmB;IAC5C,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,YAAY,EAAE,CAAC;QAC7B,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC5B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,yBAAyB;QACzB,GAAG,CAAC,KAAK,CAAC,WAAY,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;IAC/C,CAAC;AACH,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * console.ts — 交互式控制台 (readline)
3
+ *
4
+ * 命令:
5
+ * help 显示帮助
6
+ * reload 重新读取 configs/qq_config.json
7
+ * status 显示当前状态
8
+ * stop 退出进程
9
+ *
10
+ * 行为与旧实现完全一致。
11
+ */
12
+ import type { QQBridgeConfig } from "./types.js";
13
+ export interface ConsoleOptions {
14
+ config: QQBridgeConfig;
15
+ initialEnabled: boolean;
16
+ wsPort: number;
17
+ botSelfIdRef: {
18
+ value: string | null;
19
+ };
20
+ }
21
+ export declare function startConsole(opts: ConsoleOptions): void;
22
+ //# sourceMappingURL=console.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console.d.ts","sourceRoot":"","sources":["../src/console.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,cAAc,CAAC;IACvB,cAAc,EAAE,OAAO,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE;QAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;CACxC;AA8DD,wBAAgB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAKvD"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * console.ts — 交互式控制台 (readline)
3
+ *
4
+ * 命令:
5
+ * help 显示帮助
6
+ * reload 重新读取 configs/qq_config.json
7
+ * status 显示当前状态
8
+ * stop 退出进程
9
+ *
10
+ * 行为与旧实现完全一致。
11
+ */
12
+ import { createInterface } from "node:readline";
13
+ import { log } from "./log.js";
14
+ import { reloadInto } from "./config.js";
15
+ function showHelp() {
16
+ console.log("可用命令:");
17
+ console.log(" help — 显示本帮助");
18
+ console.log(" reload — 重新读取 qq_config.json");
19
+ console.log(" status — 显示当前状态");
20
+ console.log(" stop — 停止服务");
21
+ }
22
+ function showStatus(opts) {
23
+ const { config: cfg, initialEnabled, wsPort, botSelfIdRef } = opts;
24
+ console.log("=== QQBridge 状态 ===");
25
+ console.log(` 启用: ${initialEnabled}`);
26
+ console.log(` WS 端口: ${wsPort}`);
27
+ console.log(` LLBot HTTP: (db-server 直连,不经过本进程)`);
28
+ console.log(` QQ 群: ${cfg.qq_group_id || "未配置"}`);
29
+ console.log(` Channel: ${cfg.bridge_channel_id || "未配置"}`);
30
+ console.log(` db-server: ${cfg.db_host}:${cfg.db_port}`);
31
+ console.log(` Bot self_id: ${botSelfIdRef.value || "未捕获"}`);
32
+ console.log(` MC→QQ 前缀: ${cfg.mctoqq_prefix}`);
33
+ }
34
+ function handleCommand(line, opts, rl) {
35
+ const cmd = line.trim().toLowerCase();
36
+ switch (cmd) {
37
+ case "help":
38
+ showHelp();
39
+ return false;
40
+ case "reload": {
41
+ const before = {
42
+ qq_group_id: opts.config.qq_group_id,
43
+ bridge_channel_id: opts.config.bridge_channel_id,
44
+ db_host: opts.config.db_host,
45
+ db_port: opts.config.db_port,
46
+ };
47
+ reloadInto(opts.config);
48
+ log.info(`配置已重载 (qq_group_id=${opts.config.qq_group_id}, channel=${opts.config.bridge_channel_id}, db=${opts.config.db_host}:${opts.config.db_port})`);
49
+ // 静默消费未使用变量警告
50
+ void before;
51
+ return false;
52
+ }
53
+ case "status":
54
+ showStatus(opts);
55
+ return false;
56
+ case "stop":
57
+ case "exit":
58
+ case "quit":
59
+ log.info("正在停止...");
60
+ rl.close();
61
+ process.exit(0);
62
+ return true; // 不可达,但满足 noImplicitReturns
63
+ case "":
64
+ return false;
65
+ default:
66
+ log.info(`未知命令: ${cmd} (输入 help 查看帮助)`);
67
+ return false;
68
+ }
69
+ }
70
+ export function startConsole(opts) {
71
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
72
+ rl.on("line", (line) => {
73
+ handleCommand(line, opts, rl);
74
+ });
75
+ }
76
+ //# sourceMappingURL=console.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console.js","sourceRoot":"","sources":["../src/console.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * dbserver.ts — 转发 OneBot 群消息到 db-server /api/sfmc/messages
3
+ *
4
+ * 与旧实现保持行为一致: 仅 POST 200 视为成功,否则抛错; 失败不重试 (主流程不阻塞)。
5
+ * 超时: 与 db-server 内部 HTTP 风格一致,使用 req.on("timeout") + req.destroy。
6
+ */
7
+ export interface DBServerConfig {
8
+ host: string;
9
+ port: number;
10
+ channelId: string;
11
+ }
12
+ /**
13
+ * 构造一条 IncomingChatMessage 并 POST 到 db-server。
14
+ * fromid 形如 `qq_<user_id>`,与旧实现保持完全一致。
15
+ */
16
+ export declare function forwardGroupMessage(cfg: DBServerConfig, fromId: string, fromName: string, content: string, now?: number): Promise<void>;
17
+ /** 包装 try/catch,失败仅 log (与旧实现一致,不抛给主循环)。 */
18
+ export declare function tryForward(cfg: DBServerConfig, fromId: string, fromName: string, content: string): Promise<void>;
19
+ //# sourceMappingURL=dbserver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dbserver.d.ts","sourceRoot":"","sources":["../src/dbserver.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AA+CD;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,GAAG,GAAE,MAAmB,GACvB,OAAO,CAAC,IAAI,CAAC,CAcf;AAED,4CAA4C;AAC5C,wBAAsB,UAAU,CAC9B,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAOf"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * dbserver.ts — 转发 OneBot 群消息到 db-server /api/sfmc/messages
3
+ *
4
+ * 与旧实现保持行为一致: 仅 POST 200 视为成功,否则抛错; 失败不重试 (主流程不阻塞)。
5
+ * 超时: 与 db-server 内部 HTTP 风格一致,使用 req.on("timeout") + req.destroy。
6
+ */
7
+ import { request as httpRequest } from "node:http";
8
+ import { log } from "./log.js";
9
+ const REQUEST_TIMEOUT_MS = 5_000;
10
+ function postJSON(url, body, timeoutMs) {
11
+ const payload = JSON.stringify(body);
12
+ const options = {
13
+ hostname: url.hostname,
14
+ port: url.port || "3001",
15
+ path: url.pathname,
16
+ method: "POST",
17
+ headers: {
18
+ "Content-Type": "application/json",
19
+ "Content-Length": Buffer.byteLength(payload),
20
+ },
21
+ timeout: timeoutMs,
22
+ };
23
+ return new Promise((resolve, reject) => {
24
+ const req = httpRequest(options, (res) => {
25
+ let buf = "";
26
+ res.setEncoding("utf-8");
27
+ res.on("data", (chunk) => {
28
+ buf += chunk;
29
+ });
30
+ res.on("end", () => {
31
+ const code = res.statusCode ?? 0;
32
+ if (code >= 200 && code < 300) {
33
+ try {
34
+ resolve(JSON.parse(buf));
35
+ }
36
+ catch {
37
+ // 允许返回非 JSON: 仍视为成功,作为 void 抛出
38
+ resolve(buf);
39
+ }
40
+ }
41
+ else {
42
+ reject(new Error(`db-server ${code}: ${buf.slice(0, 200)}`));
43
+ }
44
+ });
45
+ });
46
+ req.on("timeout", () => {
47
+ req.destroy(new Error(`db-server 超时 ${timeoutMs}ms`));
48
+ });
49
+ req.on("error", reject);
50
+ req.write(payload);
51
+ req.end();
52
+ });
53
+ }
54
+ /**
55
+ * 构造一条 IncomingChatMessage 并 POST 到 db-server。
56
+ * fromid 形如 `qq_<user_id>`,与旧实现保持完全一致。
57
+ */
58
+ export async function forwardGroupMessage(cfg, fromId, fromName, content, now = Date.now()) {
59
+ const message = {
60
+ id: `${fromId}_${now}`,
61
+ channelId: cfg.channelId,
62
+ fromid: fromId,
63
+ fromName,
64
+ type: "text",
65
+ content,
66
+ showTimestamp: true,
67
+ timestamp: now,
68
+ };
69
+ const body = { messages: [message] };
70
+ const url = new URL(`http://${cfg.host}:${cfg.port}/api/sfmc/messages`);
71
+ await postJSON(url, body, REQUEST_TIMEOUT_MS);
72
+ }
73
+ /** 包装 try/catch,失败仅 log (与旧实现一致,不抛给主循环)。 */
74
+ export async function tryForward(cfg, fromId, fromName, content) {
75
+ try {
76
+ await forwardGroupMessage(cfg, fromId, fromName, content);
77
+ log.info(`QQ → MC: ${fromName}: ${content.slice(0, 60)}`);
78
+ }
79
+ catch (e) {
80
+ log.error(`转发到 db-server 失败: ${e.message}`);
81
+ }
82
+ }
83
+ //# sourceMappingURL=dbserver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dbserver.js","sourceRoot":"","sources":["../src/dbserver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * index.ts — QQ ↔ MC 桥接进程入口
4
+ *
5
+ * 端口: 3002 (默认) — WebSocket (LLBot reverse-ws)
6
+ *
7
+ * 数据流:
8
+ * QQ → MC: LLBot ──WS:3002──→ qq-bridge ──POST──→ db-server:3001/api/sfmc/messages
9
+ * MC → QQ: SAPI ──POST──→ db-server:3001 ──(内部)──→ LLBot:3004/send_group_msg
10
+ *
11
+ * 注意: 本进程只做 WS 入口,不再起 HTTP server。MC→QQ 由 db-server 直连 LLBot
12
+ * (见 db-server/src/domain/bridge.ts:forwardToQQBridge)
13
+ *
14
+ * 循环防护:
15
+ * 1. 跳过 sender.user_id === botSelfId 的回声
16
+ * (botSelfId 通过 LLBot 的 lifecycle 元事件捕获)
17
+ * 2. 5 秒内同 message_id 短期去重 (防 race)
18
+ */
19
+ export {};
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;GAgBG"}
package/dist/index.js ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * index.ts — QQ ↔ MC 桥接进程入口
4
+ *
5
+ * 端口: 3002 (默认) — WebSocket (LLBot reverse-ws)
6
+ *
7
+ * 数据流:
8
+ * QQ → MC: LLBot ──WS:3002──→ qq-bridge ──POST──→ db-server:3001/api/sfmc/messages
9
+ * MC → QQ: SAPI ──POST──→ db-server:3001 ──(内部)──→ LLBot:3004/send_group_msg
10
+ *
11
+ * 注意: 本进程只做 WS 入口,不再起 HTTP server。MC→QQ 由 db-server 直连 LLBot
12
+ * (见 db-server/src/domain/bridge.ts:forwardToQQBridge)
13
+ *
14
+ * 循环防护:
15
+ * 1. 跳过 sender.user_id === botSelfId 的回声
16
+ * (botSelfId 通过 LLBot 的 lifecycle 元事件捕获)
17
+ * 2. 5 秒内同 message_id 短期去重 (防 race)
18
+ */
19
+ import { loadInitialConfig } from "./config.js";
20
+ import { OneBotDispatcher } from "./onebot.js";
21
+ import { startWsServer } from "./ws-server.js";
22
+ import { startConsole } from "./console.js";
23
+ import { log } from "./log.js";
24
+ async function main() {
25
+ const cfg = loadInitialConfig();
26
+ if (!cfg.qq_enabled) {
27
+ log.info("已禁用 (qq_enabled = false)");
28
+ process.exit(0);
29
+ }
30
+ // 让控制台 status 命令读到 bot 当前的 self_id
31
+ const botSelfIdRef = { value: null };
32
+ const dispatcher = new OneBotDispatcher({
33
+ qqGroupId: cfg.qq_group_id,
34
+ db: {
35
+ host: cfg.db_host,
36
+ port: cfg.db_port,
37
+ channelId: cfg.bridge_channel_id,
38
+ },
39
+ });
40
+ // botSelfId 通过 dispatcher 内部维护; 控制台读 self_id 时取最新值
41
+ Object.defineProperty(botSelfIdRef, "value", {
42
+ get: () => dispatcher.selfId,
43
+ });
44
+ await startWsServer({ port: cfg.qq_ws_port, dispatcher });
45
+ log.info(`等待 LLBot 连接 (主群: ${cfg.qq_group_id || "未配置"}, channel: ${cfg.bridge_channel_id || "未配置"}, db: ${cfg.db_host}:${cfg.db_port})`);
46
+ startConsole({
47
+ config: cfg,
48
+ initialEnabled: cfg.qq_enabled,
49
+ wsPort: cfg.qq_ws_port,
50
+ botSelfIdRef,
51
+ });
52
+ log.info("启动完成");
53
+ }
54
+ main().catch((e) => {
55
+ log.error(`未捕获异常: ${e.message}`);
56
+ process.exit(1);
57
+ });
58
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAG/B,KAAK,UAAU,IAAI;IACjB,MAAM,GAAG,GAAmB,iBAAiB,EAAE,CAAC;IAEhD,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC;QACpB,GAAG,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;QACrC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,mCAAmC;IACnC,MAAM,YAAY,GAA6B,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAE/D,MAAM,UAAU,GAAG,IAAI,gBAAgB,CAAC;QACtC,SAAS,EAAE,GAAG,CAAC,WAAW;QAC1B,EAAE,EAAE;YACF,IAAI,EAAE,GAAG,CAAC,OAAO;YACjB,IAAI,EAAE,GAAG,CAAC,OAAO;YACjB,SAAS,EAAE,GAAG,CAAC,iBAAiB;SACjC;KACF,CAAC,CAAC;IAEH,mDAAmD;IACnD,MAAM,CAAC,cAAc,CAAC,YAAY,EAAE,OAAO,EAAE;QAC3C,GAAG,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,MAAM;KAC7B,CAAC,CAAC;IAEH,MAAM,aAAa,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,UAAU,EAAE,UAAU,EAAE,CAAC,CAAC;IAE1D,GAAG,CAAC,IAAI,CACN,oBAAoB,GAAG,CAAC,WAAW,IAAI,KAAK,cAAc,GAAG,CAAC,iBAAiB,IAAI,KAAK,SAAS,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,OAAO,GAAG,CAC/H,CAAC;IAEF,YAAY,CAAC;QACX,MAAM,EAAE,GAAG;QACX,cAAc,EAAE,GAAG,CAAC,UAAU;QAC9B,MAAM,EAAE,GAAG,CAAC,UAAU;QACtB,YAAY;KACb,CAAC,CAAC;IAEH,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AACnB,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE;IACjB,GAAG,CAAC,KAAK,CAAC,UAAW,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;IAC5C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
package/dist/log.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * log.ts — qq-bridge 统一日志实例
3
+ *
4
+ * 通过 @sfmc-bds/logs 共享包接入,source = "qq"。
5
+ * sfmc 主进程通过捕获本进程 stdout 汇聚展示。
6
+ *
7
+ * bare 模式: stdout 只输出纯 text,不加时间戳/source/level 前缀
8
+ * (由 sfmc 的 formatLog 统一添加,避免重复)。error 走 stderr。
9
+ */
10
+ export declare const log: import("@sfmc-bds/sdk/logs").Logger;
11
+ //# sourceMappingURL=log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,eAAO,MAAM,GAAG,qCAGd,CAAC"}
package/dist/log.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * log.ts — qq-bridge 统一日志实例
3
+ *
4
+ * 通过 @sfmc-bds/logs 共享包接入,source = "qq"。
5
+ * sfmc 主进程通过捕获本进程 stdout 汇聚展示。
6
+ *
7
+ * bare 模式: stdout 只输出纯 text,不加时间戳/source/level 前缀
8
+ * (由 sfmc 的 formatLog 统一添加,避免重复)。error 走 stderr。
9
+ */
10
+ import { createLogger, createStdoutSink } from "@sfmc-bds/sdk/logs";
11
+ export const log = createLogger({
12
+ source: "qq",
13
+ sinks: [createStdoutSink({ bare: true })],
14
+ });
15
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEpE,MAAM,CAAC,MAAM,GAAG,GAAG,YAAY,CAAC;IAC9B,MAAM,EAAE,IAAI;IACZ,KAAK,EAAE,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;CAC1C,CAAC,CAAC"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * onebot.ts — OneBot 11 事件分发
3
+ *
4
+ * 职责:
5
+ * 1. 维护 botSelfId (从 lifecycle 元事件捕获)
6
+ * 2. 5s 短期 message_id 去重 (防 LLBot 偶发重发)
7
+ * 3. 群消息段 → 纯文本 (text / at / image / face / reply / forward / record / video / file)
8
+ * 4. 命中规则后调用 tryForward 投递
9
+ *
10
+ * 行为与旧 index.js 完全一致; 类型上把 sender/message 都收紧为 narrow 类型。
11
+ */
12
+ import { type DBServerConfig } from "./dbserver.js";
13
+ import type { OneBotGroupMessageEvent } from "./types.js";
14
+ /**
15
+ * OneBot 11 消息段数组 (或纯字符串) → 纯文本。
16
+ * 行为与旧实现完全一致:
17
+ * - text: 原样拼接
18
+ * - at: "@昵称 " 或 "@qq " (有 name 优先 name)
19
+ * - 其它: [图片]/[表情]/[回复]/[转发]/[语音]/[视频]/[文件]
20
+ * - 未知段: 跳过
21
+ */
22
+ export declare function extractText(message: OneBotGroupMessageEvent["message"]): string;
23
+ export declare class RecentMessageDedup {
24
+ private readonly map;
25
+ private lastCleanup;
26
+ seen(messageId: string | number | undefined): boolean;
27
+ }
28
+ export interface DispatcherOptions {
29
+ /** 群 ID 字符串,空 = 接收所有群(旧行为:空时不匹配任何群,不会触发) */
30
+ qqGroupId: string;
31
+ /** db-server 配置 + 目标 channelId */
32
+ db: DBServerConfig;
33
+ }
34
+ export declare class OneBotDispatcher {
35
+ private botSelfId;
36
+ private readonly opts;
37
+ private readonly dedup;
38
+ constructor(opts: DispatcherOptions);
39
+ /** 测试/调试用 */
40
+ get selfId(): string | null;
41
+ /** 收到一条原始 JSON 字符串(已 parse) */
42
+ handle(rawEvent: unknown): Promise<void>;
43
+ }
44
+ //# sourceMappingURL=onebot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"onebot.d.ts","sourceRoot":"","sources":["../src/onebot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAChE,OAAO,KAAK,EAMV,uBAAuB,EAQxB,MAAM,YAAY,CAAC;AAwCpB;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,uBAAuB,CAAC,SAAS,CAAC,GAAG,MAAM,CA8B/E;AAMD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA6B;IACjD,OAAO,CAAC,WAAW,CAAK;IAExB,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO;CAetD;AAGD,MAAM,WAAW,iBAAiB;IAChC,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,kCAAkC;IAClC,EAAE,EAAE,cAAc,CAAC;CACpB;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAoB;IACzC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA4B;gBAEtC,IAAI,EAAE,iBAAiB;IAInC,aAAa;IACb,IAAI,MAAM,IAAI,MAAM,GAAG,IAAI,CAE1B;IAED,+BAA+B;IACzB,MAAM,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAoC/C"}
package/dist/onebot.js ADDED
@@ -0,0 +1,172 @@
1
+ /**
2
+ * onebot.ts — OneBot 11 事件分发
3
+ *
4
+ * 职责:
5
+ * 1. 维护 botSelfId (从 lifecycle 元事件捕获)
6
+ * 2. 5s 短期 message_id 去重 (防 LLBot 偶发重发)
7
+ * 3. 群消息段 → 纯文本 (text / at / image / face / reply / forward / record / video / file)
8
+ * 4. 命中规则后调用 tryForward 投递
9
+ *
10
+ * 行为与旧 index.js 完全一致; 类型上把 sender/message 都收紧为 narrow 类型。
11
+ */
12
+ import { log } from "./log.js";
13
+ import { tryForward } from "./dbserver.js";
14
+ // ── 类型守卫 ────────────────────────────────────────────────────
15
+ function isGroupMessageEvent(e) {
16
+ return e.post_type === "message" && e.message_type === "group";
17
+ }
18
+ function isLifecycleEvent(e) {
19
+ return e.post_type === "meta_event" && e.meta_event_type === "lifecycle";
20
+ }
21
+ function isTextSegment(s) {
22
+ return s?.type === "text";
23
+ }
24
+ function isAtSegment(s) {
25
+ return s?.type === "at";
26
+ }
27
+ function isImageSegment(s) {
28
+ return s?.type === "image";
29
+ }
30
+ function isFaceSegment(s) {
31
+ return s?.type === "face";
32
+ }
33
+ function isReplySegment(s) {
34
+ return s?.type === "reply";
35
+ }
36
+ function isForwardSegment(s) {
37
+ return s?.type === "forward";
38
+ }
39
+ function isRecordSegment(s) {
40
+ return s?.type === "record";
41
+ }
42
+ function isVideoSegment(s) {
43
+ return s?.type === "video";
44
+ }
45
+ function isFileSegment(s) {
46
+ return s?.type === "file";
47
+ }
48
+ // ── 消息段 → 纯文本 ─────────────────────────────────────────
49
+ /**
50
+ * OneBot 11 消息段数组 (或纯字符串) → 纯文本。
51
+ * 行为与旧实现完全一致:
52
+ * - text: 原样拼接
53
+ * - at: "@昵称 " 或 "@qq " (有 name 优先 name)
54
+ * - 其它: [图片]/[表情]/[回复]/[转发]/[语音]/[视频]/[文件]
55
+ * - 未知段: 跳过
56
+ */
57
+ export function extractText(message) {
58
+ if (typeof message === "string")
59
+ return message;
60
+ if (!Array.isArray(message))
61
+ return "";
62
+ const parts = [];
63
+ for (const seg of message) {
64
+ if (!seg || typeof seg !== "object")
65
+ continue;
66
+ if (isTextSegment(seg)) {
67
+ const text = seg.data.text;
68
+ if (typeof text === "string")
69
+ parts.push(text);
70
+ }
71
+ else if (isAtSegment(seg)) {
72
+ const name = seg.data.name ? `@${seg.data.name} ` : `@${seg.data.qq ?? "?"} `;
73
+ parts.push(name);
74
+ }
75
+ else if (isImageSegment(seg)) {
76
+ parts.push("[图片]");
77
+ }
78
+ else if (isFaceSegment(seg)) {
79
+ parts.push("[表情]");
80
+ }
81
+ else if (isReplySegment(seg)) {
82
+ parts.push("[回复]");
83
+ }
84
+ else if (isForwardSegment(seg)) {
85
+ parts.push("[转发]");
86
+ }
87
+ else if (isRecordSegment(seg)) {
88
+ parts.push("[语音]");
89
+ }
90
+ else if (isVideoSegment(seg)) {
91
+ parts.push("[视频]");
92
+ }
93
+ else if (isFileSegment(seg)) {
94
+ parts.push("[文件]");
95
+ }
96
+ // 未知段:跳过
97
+ }
98
+ return parts.join("").trim();
99
+ }
100
+ // ── 短期 message_id 去重 (懒清理) ────────────────────────────
101
+ const DEDUP_WINDOW_MS = 5_000;
102
+ const CLEANUP_INTERVAL_MS = 1_000;
103
+ export class RecentMessageDedup {
104
+ map = new Map();
105
+ lastCleanup = 0;
106
+ seen(messageId) {
107
+ if (messageId === undefined || messageId === null)
108
+ return false;
109
+ const key = String(messageId);
110
+ const now = Date.now();
111
+ if (now - this.lastCleanup > CLEANUP_INTERVAL_MS) {
112
+ this.lastCleanup = now;
113
+ for (const [k, ts] of this.map) {
114
+ if (now - ts > DEDUP_WINDOW_MS)
115
+ this.map.delete(k);
116
+ }
117
+ }
118
+ const prev = this.map.get(key);
119
+ if (prev !== undefined && now - prev < DEDUP_WINDOW_MS)
120
+ return true;
121
+ this.map.set(key, now);
122
+ return false;
123
+ }
124
+ }
125
+ export class OneBotDispatcher {
126
+ botSelfId = null;
127
+ opts;
128
+ dedup = new RecentMessageDedup();
129
+ constructor(opts) {
130
+ this.opts = opts;
131
+ }
132
+ /** 测试/调试用 */
133
+ get selfId() {
134
+ return this.botSelfId;
135
+ }
136
+ /** 收到一条原始 JSON 字符串(已 parse) */
137
+ async handle(rawEvent) {
138
+ const event = rawEvent;
139
+ if (!event || typeof event !== "object")
140
+ return;
141
+ if (isLifecycleEvent(event)) {
142
+ if (event.self_id !== undefined && this.botSelfId === null) {
143
+ this.botSelfId = String(event.self_id);
144
+ log.info(`LLBot self_id = ${this.botSelfId}`);
145
+ }
146
+ return;
147
+ }
148
+ if (!isGroupMessageEvent(event))
149
+ return;
150
+ if (!this.opts.qqGroupId || String(event.group_id) !== this.opts.qqGroupId)
151
+ return;
152
+ // 循环防护 1: 跳过 bot 自己发的消息回声
153
+ if (this.botSelfId && String(event.sender?.user_id) === this.botSelfId)
154
+ return;
155
+ // 循环防护 2: 短期去重 (LLBot 偶发重发)
156
+ if (this.dedup.seen(event.message_id))
157
+ return;
158
+ const text = extractText(event.message);
159
+ if (!text)
160
+ return;
161
+ if (!this.opts.db.channelId) {
162
+ log.warn("bridge_channel_id 未配置,跳过 (可用 reload 重读配置)");
163
+ return;
164
+ }
165
+ const senderCard = event.sender?.card;
166
+ const senderNick = event.sender?.nickname;
167
+ const fromName = senderCard || senderNick || `QQ_${event.user_id}`;
168
+ const fromId = `qq_${String(event.user_id)}`;
169
+ await tryForward(this.opts.db, fromId, fromName, text);
170
+ }
171
+ }
172
+ //# sourceMappingURL=onebot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"onebot.js","sourceRoot":"","sources":["../src/onebot.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * types.ts — OneBot 11 / 配置 / db-server 消息载荷类型
3
+ *
4
+ * 只声明本进程真正用到的字段;OneBot 11 完整规范参考
5
+ * https://github.com/botuniverse/onebot-11
6
+ */
7
+ export type OneBotSegmentType = "text" | "at" | "image" | "face" | "reply" | "forward" | "record" | "video" | "file" | (string & {});
8
+ export interface OneBotTextSegment {
9
+ type: "text";
10
+ data: {
11
+ text: string;
12
+ };
13
+ }
14
+ export interface OneBotAtSegment {
15
+ type: "at";
16
+ data: {
17
+ qq: string;
18
+ name?: string;
19
+ };
20
+ }
21
+ export interface OneBotImageSegment {
22
+ type: "image";
23
+ data: Record<string, unknown>;
24
+ }
25
+ export interface OneBotFaceSegment {
26
+ type: "face";
27
+ data: Record<string, unknown>;
28
+ }
29
+ export interface OneBotReplySegment {
30
+ type: "reply";
31
+ data: Record<string, unknown>;
32
+ }
33
+ export interface OneBotForwardSegment {
34
+ type: "forward";
35
+ data: Record<string, unknown>;
36
+ }
37
+ export interface OneBotRecordSegment {
38
+ type: "record";
39
+ data: Record<string, unknown>;
40
+ }
41
+ export interface OneBotVideoSegment {
42
+ type: "video";
43
+ data: Record<string, unknown>;
44
+ }
45
+ export interface OneBotFileSegment {
46
+ type: "file";
47
+ data: Record<string, unknown>;
48
+ }
49
+ export interface OneBotUnknownSegment {
50
+ type: string;
51
+ data: Record<string, unknown>;
52
+ }
53
+ export type OneBotSegment = OneBotTextSegment | OneBotAtSegment | OneBotImageSegment | OneBotFaceSegment | OneBotReplySegment | OneBotForwardSegment | OneBotRecordSegment | OneBotVideoSegment | OneBotFileSegment | OneBotUnknownSegment;
54
+ export interface OneBotSender {
55
+ user_id: number;
56
+ nickname: string;
57
+ card?: string;
58
+ [k: string]: unknown;
59
+ }
60
+ export interface OneBotGroupMessageEvent {
61
+ post_type: "message";
62
+ message_type: "group";
63
+ sub_type?: string;
64
+ self_id: number | string;
65
+ user_id: number;
66
+ group_id: number;
67
+ message_id: number | string;
68
+ sender: OneBotSender;
69
+ message: OneBotSegment[] | string;
70
+ raw_message?: string;
71
+ time?: number;
72
+ [k: string]: unknown;
73
+ }
74
+ export interface OneBotLifecycleEvent {
75
+ post_type: "meta_event";
76
+ meta_event_type: "lifecycle";
77
+ self_id: number | string;
78
+ sub_type?: string;
79
+ [k: string]: unknown;
80
+ }
81
+ export type OneBotMessageEvent = OneBotGroupMessageEvent;
82
+ export type OneBotEvent = OneBotGroupMessageEvent | OneBotLifecycleEvent | {
83
+ [k: string]: unknown;
84
+ };
85
+ /** 本进程用到的字段全是 required:config 加载时 (config.ts#applyDefaults)
86
+ * 已经把 optional 字段全部填默认值。本地类型不能放宽 optional,
87
+ * 否则调用点 (index.ts / dispatcher.ts) 都要补 undefined 兜底。 */
88
+ export interface QQBridgeConfig {
89
+ qq_enabled: boolean;
90
+ qq_ws_port: number;
91
+ qq_group_id: string;
92
+ bridge_channel_id: string;
93
+ db_host: string;
94
+ db_port: number;
95
+ mctoqq_prefix: string;
96
+ /** LLBot 字段(由 config.ts#applyDefaults 之外的 raw 透传补齐)。 */
97
+ llbot_enabled?: boolean;
98
+ llbot_path?: string;
99
+ llbot_cwd?: string;
100
+ llbot_host?: string;
101
+ llbot_port?: number;
102
+ llbot_token?: string;
103
+ llbot_http?: string;
104
+ [k: string]: unknown;
105
+ }
106
+ export interface IncomingChatMessage {
107
+ id: string;
108
+ channelId: string;
109
+ fromid: string;
110
+ fromName: string;
111
+ type: "text";
112
+ content: string;
113
+ showTimestamp: boolean;
114
+ timestamp: number;
115
+ }
116
+ export interface PostMessagesBody {
117
+ messages: IncomingChatMessage[];
118
+ }
119
+ export interface PostMessagesResponse {
120
+ ok: boolean;
121
+ received?: number;
122
+ [k: string]: unknown;
123
+ }
124
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * types.ts — OneBot 11 / 配置 / db-server 消息载荷类型
3
+ *
4
+ * 只声明本进程真正用到的字段;OneBot 11 完整规范参考
5
+ * https://github.com/botuniverse/onebot-11
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * ws-server.ts — WebSocket 服务 (LLBot reverse-ws 入口)
3
+ *
4
+ * 端口: 3002 (默认)
5
+ * 行为与旧实现完全一致:
6
+ * - 优先加载本地 node_modules/ws
7
+ * - 加载失败时 fallback 到 ../db-server/node_modules/ws
8
+ * - 每个连接把消息 JSON.parse 后交给 dispatcher
9
+ *
10
+ * fallback 用 createRequire 加载: 避免 TypeScript 在编译期检查
11
+ * ../db-server/node_modules/ws 的相对路径(不在 tsconfig 包含范围)。
12
+ */
13
+ import { WebSocketServer } from "ws";
14
+ import type { OneBotDispatcher } from "./onebot.js";
15
+ export interface WsServerOptions {
16
+ port: number;
17
+ dispatcher: OneBotDispatcher;
18
+ }
19
+ export declare function startWsServer(opts: WsServerOptions): Promise<WebSocketServer>;
20
+ //# sourceMappingURL=ws-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-server.d.ts","sourceRoot":"","sources":["../src/ws-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,EAAa,eAAe,EAAE,MAAM,IAAI,CAAC;AAEhD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,gBAAgB,CAAC;CAC9B;AAaD,wBAAsB,aAAa,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,CAgCnF"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * ws-server.ts — WebSocket 服务 (LLBot reverse-ws 入口)
3
+ *
4
+ * 端口: 3002 (默认)
5
+ * 行为与旧实现完全一致:
6
+ * - 优先加载本地 node_modules/ws
7
+ * - 加载失败时 fallback 到 ../db-server/node_modules/ws
8
+ * - 每个连接把消息 JSON.parse 后交给 dispatcher
9
+ *
10
+ * fallback 用 createRequire 加载: 避免 TypeScript 在编译期检查
11
+ * ../db-server/node_modules/ws 的相对路径(不在 tsconfig 包含范围)。
12
+ */
13
+ import { createRequire } from "node:module";
14
+ import { WebSocket, WebSocketServer } from "ws";
15
+ import { log } from "./log.js";
16
+ const localRequire = createRequire(import.meta.url);
17
+ async function loadWs() {
18
+ try {
19
+ return localRequire("ws");
20
+ }
21
+ catch {
22
+ // 兜底:复用 db-server 的 ws
23
+ return localRequire("../db-server/node_modules/ws");
24
+ }
25
+ }
26
+ export async function startWsServer(opts) {
27
+ const ws = await loadWs();
28
+ const { WebSocketServer: WSS } = ws;
29
+ const wss = new WSS({ port: opts.port });
30
+ log.info(`WebSocket 服务启动 ws://0.0.0.0:${opts.port}`);
31
+ wss.on("connection", (sock, req) => {
32
+ const path = req.url ?? "/";
33
+ log.info(`LLBot 已连接 (${path})`);
34
+ sock.on("message", (raw) => {
35
+ try {
36
+ const text = raw.toString("utf-8");
37
+ const parsed = JSON.parse(text);
38
+ // 异步但不阻塞 socket 循环
39
+ void opts.dispatcher.handle(parsed);
40
+ }
41
+ catch (e) {
42
+ log.error(`解析消息失败: ${e.message}`);
43
+ }
44
+ });
45
+ sock.on("close", () => {
46
+ log.info("LLBot 已断开");
47
+ });
48
+ sock.on("error", (err) => {
49
+ log.error(`WebSocket 连接错误: ${err.message}`);
50
+ });
51
+ });
52
+ wss.on("error", (err) => {
53
+ log.error(`WebSocket 服务器错误: ${err.message}`);
54
+ });
55
+ return wss;
56
+ }
57
+ //# sourceMappingURL=ws-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-server.js","sourceRoot":"","sources":["../src/ws-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,IAAI,CAAC;AAChD,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAQ/B,MAAM,YAAY,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAEpD,KAAK,UAAU,MAAM;IACnB,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,IAAI,CAAwB,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,uBAAuB;QACvB,OAAO,YAAY,CAAC,8BAA8B,CAAwB,CAAC;IAC7E,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAqB;IACvD,MAAM,EAAE,GAAG,MAAM,MAAM,EAAE,CAAC;IAC1B,MAAM,EAAE,eAAe,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;IACpC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IACzC,GAAG,CAAC,IAAI,CAAC,+BAA+B,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IAErD,GAAG,CAAC,EAAE,CAAC,YAAY,EAAE,CAAC,IAAe,EAAE,GAAoB,EAAE,EAAE;QAC7D,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC;QAC5B,GAAG,CAAC,IAAI,CAAC,cAAc,IAAI,GAAG,CAAC,CAAC;QAChC,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,EAAE;YACzB,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;gBACnC,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBACzC,mBAAmB;gBACnB,KAAK,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YACtC,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,GAAG,CAAC,KAAK,CAAC,WAAY,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;YAC/C,CAAC;QACH,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACpB,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACxB,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACvB,GAAG,CAAC,KAAK,CAAC,mBAAmB,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QAC9C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;QACtB,GAAG,CAAC,KAAK,CAAC,oBAAoB,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,CAAC;AACb,CAAC"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@sfmc-bds/qq-bridge",
3
+ "version": "0.1.0",
4
+ "description": "SFMC QQ ↔ MC bridge — LLBot OneBot 11 reverse-WS on port 3002, forwards events to db-server",
5
+ "author": "Shiroha7z",
6
+ "type": "module",
7
+ "license": "ISC",
8
+ "homepage": "https://github.com/DogeLakeDev/ScriptsForMinecraftServer/tree/main/qq-bridge#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/DogeLakeDev/ScriptsForMinecraftServer.git",
12
+ "directory": "qq-bridge"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/DogeLakeDev/ScriptsForMinecraftServer/issues"
16
+ },
17
+ "keywords": [
18
+ "minecraft-bedrock",
19
+ "sfmc",
20
+ "qq",
21
+ "onebot",
22
+ "llbot",
23
+ "scriptsforminecraftserver"
24
+ ],
25
+ "main": "dist/index.js",
26
+ "files": [
27
+ "dist",
28
+ "README.md"
29
+ ],
30
+ "scripts": {
31
+ "start": "node dist/index.js",
32
+ "dev": "tsx src/index.ts",
33
+ "build": "tsc -p tsconfig.json",
34
+ "typecheck": "tsc --noEmit",
35
+ "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
36
+ "format": "prettier --write \"src/**/*.{ts,tsx,js,json}\"",
37
+ "prepublishOnly": "npm run build"
38
+ },
39
+ "dependencies": {
40
+ "@sfmc-bds/sdk": "^0.1.0",
41
+ "ws": "^8.21.1"
42
+ },
43
+ "engines": {
44
+ "node": ">=22.13.0"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public",
48
+ "registry": "https://registry.npmjs.org/"
49
+ }
50
+ }