@sfmc-bds/cli 0.2.0-beta.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +663 -0
- package/README.md +4 -5
- package/dist/bds-runtime-version.d.ts +3 -0
- package/dist/bds-runtime-version.d.ts.map +1 -0
- package/dist/bds-runtime-version.js +15 -0
- package/dist/bds-runtime-version.js.map +7 -0
- package/dist/command-surface.js +3 -3
- package/dist/command-surface.js.map +2 -2
- package/dist/commands.d.ts +57 -4
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +184 -68
- package/dist/commands.js.map +2 -2
- package/dist/directory-picker.d.ts +27 -0
- package/dist/directory-picker.d.ts.map +1 -0
- package/dist/directory-picker.js +112 -0
- package/dist/directory-picker.js.map +7 -0
- package/dist/help-text.d.ts.map +1 -1
- package/dist/help-text.js +1 -5
- package/dist/help-text.js.map +2 -2
- package/dist/i18n/locales/en.d.ts +49 -14
- package/dist/i18n/locales/en.d.ts.map +1 -1
- package/dist/i18n/locales/en.js +49 -15
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/zh-CN.d.ts.map +1 -1
- package/dist/i18n/locales/zh-CN.js +50 -16
- package/dist/i18n/locales/zh-CN.js.map +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +2 -2
- package/dist/interactive-prompts.d.ts +1 -2
- package/dist/interactive-prompts.d.ts.map +1 -1
- package/dist/interactive-prompts.js +12 -23
- package/dist/interactive-prompts.js.map +3 -3
- package/dist/llbot-launch.d.ts +20 -0
- package/dist/llbot-launch.d.ts.map +1 -0
- package/dist/llbot-launch.js +40 -0
- package/dist/llbot-launch.js.map +7 -0
- package/dist/log-filter.d.ts +18 -2
- package/dist/log-filter.d.ts.map +1 -1
- package/dist/log-filter.js +54 -4
- package/dist/log-filter.js.map +2 -2
- package/dist/log-translator.d.ts +36 -0
- package/dist/log-translator.d.ts.map +1 -0
- package/dist/log-translator.js +279 -0
- package/dist/log-translator.js.map +7 -0
- package/dist/logs.d.ts +2 -0
- package/dist/logs.d.ts.map +1 -1
- package/dist/logs.js +24 -11
- package/dist/logs.js.map +2 -2
- package/dist/main.js +9 -6
- package/dist/main.js.map +2 -2
- package/dist/module-commands.d.ts.map +1 -1
- package/dist/module-commands.js +22 -8
- package/dist/module-commands.js.map +2 -2
- package/dist/module-fingerprint.d.ts.map +1 -1
- package/dist/module-fingerprint.js +1 -0
- package/dist/module-fingerprint.js.map +2 -2
- package/dist/module-pack-build.d.ts +14 -4
- package/dist/module-pack-build.d.ts.map +1 -1
- package/dist/module-pack-build.js +81 -108
- package/dist/module-pack-build.js.map +2 -2
- package/dist/module-toggle.d.ts +1 -1
- package/dist/module-toggle.d.ts.map +1 -1
- package/dist/module-toggle.js.map +1 -1
- package/dist/node-service-probe.d.ts +18 -0
- package/dist/node-service-probe.d.ts.map +1 -0
- package/dist/node-service-probe.js +97 -0
- package/dist/node-service-probe.js.map +7 -0
- package/dist/pack-lifecycle.d.ts +15 -1
- package/dist/pack-lifecycle.d.ts.map +1 -1
- package/dist/pack-lifecycle.js +249 -16
- package/dist/pack-lifecycle.js.map +2 -2
- package/dist/player-session.d.ts +13 -0
- package/dist/player-session.d.ts.map +1 -0
- package/dist/player-session.js +26 -0
- package/dist/player-session.js.map +7 -0
- package/dist/registry.d.ts +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +4 -1
- package/dist/registry.js.map +2 -2
- package/dist/repl-windows/format-display.d.ts.map +1 -1
- package/dist/repl-windows/format-display.js +5 -1
- package/dist/repl-windows/format-display.js.map +2 -2
- package/dist/repl-windows/host.d.ts +6 -3
- package/dist/repl-windows/host.d.ts.map +1 -1
- package/dist/repl-windows/host.js +12 -6
- package/dist/repl-windows/host.js.map +2 -2
- package/dist/repl-windows/index.d.ts +1 -0
- package/dist/repl-windows/index.d.ts.map +1 -1
- package/dist/repl-windows/index.js +3 -0
- package/dist/repl-windows/index.js.map +2 -2
- package/dist/repl-windows/service-window.d.ts +1 -1
- package/dist/repl-windows/service-window.d.ts.map +1 -1
- package/dist/repl-windows/service-window.js +9 -1
- package/dist/repl-windows/service-window.js.map +2 -2
- package/dist/repl-windows/sfmc-window.d.ts +7 -0
- package/dist/repl-windows/sfmc-window.d.ts.map +1 -0
- package/dist/repl-windows/sfmc-window.js +28 -0
- package/dist/repl-windows/sfmc-window.js.map +7 -0
- package/dist/repl.d.ts.map +1 -1
- package/dist/repl.js +37 -22
- package/dist/repl.js.map +2 -2
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +15 -2
- package/dist/runtime.js.map +2 -2
- package/dist/send-target.d.ts +2 -0
- package/dist/send-target.d.ts.map +1 -1
- package/dist/send-target.js +12 -2
- package/dist/send-target.js.map +2 -2
- package/dist/services.d.ts +58 -5
- package/dist/services.d.ts.map +1 -1
- package/dist/services.js +273 -50
- package/dist/services.js.map +2 -2
- package/dist/sfmc-modules-root.d.ts.map +1 -1
- package/dist/sfmc-modules-root.js +6 -2
- package/dist/sfmc-modules-root.js.map +2 -2
- package/dist/ui-command.d.ts +6 -0
- package/dist/ui-command.d.ts.map +1 -0
- package/dist/ui-command.js +68 -0
- package/dist/ui-command.js.map +7 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +95 -32
- package/dist/wizard.js.map +2 -2
- package/dist/world-packs.d.ts +2 -0
- package/dist/world-packs.d.ts.map +1 -1
- package/dist/world-packs.js +161 -18
- package/dist/world-packs.js.map +2 -2
- package/package.json +21 -15
- package/scripts/module-install/check-modules.mjs +178 -0
- package/scripts/module-install/fetch-module.mjs +1083 -0
- package/scripts/module-install/lib/catalog.mjs +96 -0
- package/scripts/module-install/lib/config-seeding.mjs +128 -0
- package/scripts/module-install/lib/io.mjs +37 -0
- package/scripts/module-install/lib/link-from.mjs +80 -0
- package/scripts/module-install/lib/lock.mjs +78 -0
- package/scripts/module-install/lib/npm-resolver.mjs +55 -0
- package/scripts/module-install/lib/packages.mjs +136 -0
- package/scripts/module-install/lib/paths.mjs +48 -0
- package/scripts/module-install/lib/registry-index.mjs +50 -0
- package/dist/cmd-remote.d.ts +0 -13
- package/dist/cmd-remote.d.ts.map +0 -1
- package/dist/cmd-remote.js +0 -38
- package/dist/cmd-remote.js.map +0 -7
- package/dist/remote-agent.d.ts +0 -18
- package/dist/remote-agent.d.ts.map +0 -1
- package/dist/remote-agent.js +0 -198
- package/dist/remote-agent.js.map +0 -7
package/dist/services.d.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* services.ts — CLI 进程编排与服务生命周期管理
|
|
3
|
+
*
|
|
4
|
+
* 核心管理对象(ServiceName):
|
|
5
|
+
* - `bds`:Minecraft 基岩版服务端(支持 stdin 管道指令交互与优雅停服)
|
|
6
|
+
* - `db`:db-server SQLite HTTP 服务(端口健康检查与 loopback 探活)
|
|
7
|
+
* - `tunnel`:云端 QQ Webhook 到本机 db-server 的 SSH 反向隧道
|
|
8
|
+
* - `qq`:qq-bridge 消息网桥服务(支持官方 Bot 与 LLBot 双后端)
|
|
9
|
+
* - `llbot`:LLBot 独立机器人进程(条件启动)
|
|
10
|
+
*
|
|
11
|
+
* 核心能力:
|
|
12
|
+
* - 进程生命周期:按依赖次序编排服务启动(db → qq → bds)、优雅停止与异常自动拉起
|
|
13
|
+
* - 多源探活感知:自动探测托管进程与外部独立进程状态
|
|
14
|
+
* - 日志流统一聚合:捕获并归一化各服务的 stdout / stderr 数据流
|
|
15
|
+
*/
|
|
16
|
+
import type { QQBackend } from "@sfmc-bds/sdk/node/config";
|
|
1
17
|
import { type ChildProcess } from "node:child_process";
|
|
2
18
|
import { EventEmitter } from "node:events";
|
|
3
19
|
import { type ServiceId } from "./runtime.js";
|
|
@@ -7,8 +23,31 @@ export interface LogLine {
|
|
|
7
23
|
text: string;
|
|
8
24
|
stream: "stdout" | "stderr";
|
|
9
25
|
}
|
|
10
|
-
|
|
26
|
+
/** 子进程输出分行:过滤空串以及 Windows CRLF 拆分后残留的纯空白行。 */
|
|
27
|
+
export declare function nonBlankOutputLines(text: string): string[];
|
|
28
|
+
export type ServiceName = "bds" | "db" | "tunnel" | "qq" | "llbot";
|
|
11
29
|
export declare const SERVICE_NAMES: ServiceName[];
|
|
30
|
+
export declare function setArgvDaemonize(on: boolean): void;
|
|
31
|
+
/** 单服务 start 结果:optional 服务 validate 失败记为 skipped,不抛错 */
|
|
32
|
+
export type StartOutcome = {
|
|
33
|
+
status: "started";
|
|
34
|
+
} | {
|
|
35
|
+
status: "already";
|
|
36
|
+
} | {
|
|
37
|
+
status: "skipped";
|
|
38
|
+
reason: string;
|
|
39
|
+
};
|
|
40
|
+
export interface StartAllResult {
|
|
41
|
+
started: ServiceName[];
|
|
42
|
+
skipped: Array<{
|
|
43
|
+
name: ServiceName;
|
|
44
|
+
reason: string;
|
|
45
|
+
}>;
|
|
46
|
+
failed: Array<{
|
|
47
|
+
name: ServiceName;
|
|
48
|
+
reason: string;
|
|
49
|
+
}>;
|
|
50
|
+
}
|
|
12
51
|
export interface ServiceStatus {
|
|
13
52
|
name: ServiceName;
|
|
14
53
|
title: string;
|
|
@@ -17,6 +56,7 @@ export interface ServiceStatus {
|
|
|
17
56
|
uptime: string;
|
|
18
57
|
ownership?: "managed" | "external";
|
|
19
58
|
}
|
|
59
|
+
export declare function getQqBackendMode(): QQBackend;
|
|
20
60
|
interface ServiceDef {
|
|
21
61
|
name: ServiceName;
|
|
22
62
|
title: string;
|
|
@@ -29,9 +69,16 @@ interface ServiceDef {
|
|
|
29
69
|
stopTimeout: number;
|
|
30
70
|
autoRestart: boolean;
|
|
31
71
|
restartDelay: number;
|
|
72
|
+
/**
|
|
73
|
+
* 可选服务:validate 失败时 start 返回 skipped(startAll 不当作失败)。
|
|
74
|
+
* 用于 official 下跳过 llbot、以及 qq_enabled=false。
|
|
75
|
+
*/
|
|
76
|
+
optional?: boolean;
|
|
32
77
|
validate?: () => string | null;
|
|
33
78
|
/** 启动前钩子(如 BDS 装载一致性校验);失败则禁止 spawn */
|
|
34
79
|
beforeStart?: () => Promise<void>;
|
|
80
|
+
/** 原始日志行观察器,不得阻塞子进程输出。 */
|
|
81
|
+
onLogLine?: (line: string) => void;
|
|
35
82
|
}
|
|
36
83
|
declare class Service {
|
|
37
84
|
name: ServiceName;
|
|
@@ -44,13 +91,19 @@ declare class Service {
|
|
|
44
91
|
events: EventEmitter<any>;
|
|
45
92
|
private def;
|
|
46
93
|
private manualStop;
|
|
94
|
+
private updateInProgress;
|
|
95
|
+
private restartTimer;
|
|
47
96
|
constructor(def: ServiceDef);
|
|
48
97
|
get uptime(): string;
|
|
49
98
|
pushLog(text: string, stream: "stdout" | "stderr"): void;
|
|
50
|
-
start(): Promise<
|
|
99
|
+
start(): Promise<StartOutcome>;
|
|
100
|
+
private scheduleRestart;
|
|
101
|
+
/** 更新器独立进程停服时,禁止将计划内退出当作崩溃并自动拉起。 */
|
|
102
|
+
beginUpdate(): void;
|
|
103
|
+
endUpdate(): void;
|
|
51
104
|
stop(): Promise<void>;
|
|
52
105
|
forceStop(): void;
|
|
53
|
-
restart(): Promise<
|
|
106
|
+
restart(): Promise<StartOutcome>;
|
|
54
107
|
getRecentLogs(n: number): LogLine[];
|
|
55
108
|
/**
|
|
56
109
|
* 探测发现进程已死后回写本地状态(不设 manualStop,以便 exit 回调仍可按需 autoRestart)。
|
|
@@ -61,7 +114,7 @@ declare class Service {
|
|
|
61
114
|
export declare let services: Record<ServiceName, Service>;
|
|
62
115
|
export declare function refreshServices(): void;
|
|
63
116
|
export declare const START_ORDER: ServiceName[];
|
|
64
|
-
export declare function startAll(): Promise<
|
|
117
|
+
export declare function startAll(): Promise<StartAllResult>;
|
|
65
118
|
export declare function stopAll(): Promise<void>;
|
|
66
119
|
export declare function forceStopAll(): void;
|
|
67
120
|
export type ServiceStateEvent = {
|
|
@@ -73,7 +126,7 @@ export type ServiceStateEvent = {
|
|
|
73
126
|
export declare function onServiceStateChange(fn: (ev: ServiceStateEvent) => void): () => void;
|
|
74
127
|
/**
|
|
75
128
|
* 统一运行态查询(权威入口):OS/健康探测 + 回写 Service 内存标志。
|
|
76
|
-
* status / Tab 发送目标 /
|
|
129
|
+
* status / Tab 发送目标 / reload 等均应走此接口,勿直接读 `service.running`。
|
|
77
130
|
*/
|
|
78
131
|
export declare function queryServicesRuntime(): Promise<ServiceStatus[]>;
|
|
79
132
|
/** 单服务是否在跑(含外部实例) */
|
package/dist/services.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../src/services.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../src/services.ts"],"names":[],"mappings":"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"}
|
package/dist/services.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
ensureCoreConfigs,
|
|
3
|
-
loadEnsuredConfig,
|
|
4
2
|
DEFAULT_BDS_UPDATER_CONFIG,
|
|
5
3
|
DEFAULT_DB_CONFIG,
|
|
6
|
-
DEFAULT_QQ_CONFIG
|
|
4
|
+
DEFAULT_QQ_CONFIG,
|
|
5
|
+
ensureCoreConfigs,
|
|
6
|
+
loadEnsuredConfig,
|
|
7
|
+
qqRuntimeStatusPath,
|
|
8
|
+
readJson
|
|
7
9
|
} from "@sfmc-bds/sdk/node/config";
|
|
10
|
+
import { bdsExePath, bdsSpawnEnvExtra, ensureBdsExecutable } from "@sfmc-bds/bds-tools/host-platform";
|
|
8
11
|
import {
|
|
9
12
|
clearBdsPidFile,
|
|
10
13
|
isProcessAlive,
|
|
@@ -12,17 +15,33 @@ import {
|
|
|
12
15
|
readBdsPidFile,
|
|
13
16
|
writeBdsPidFile
|
|
14
17
|
} from "@sfmc-bds/bds-tools/process-probe";
|
|
18
|
+
import { postBdsLifecycleEvent } from "@sfmc-bds/bds-tools/qq-events-notify";
|
|
15
19
|
import { spawn } from "node:child_process";
|
|
16
20
|
import { EventEmitter } from "node:events";
|
|
17
21
|
import fs from "node:fs";
|
|
18
22
|
import path from "node:path";
|
|
23
|
+
import { t } from "./i18n/index.js";
|
|
24
|
+
import { resolveLlbotLaunch } from "./llbot-launch.js";
|
|
19
25
|
import { inferLevel, pushLog as pushUnifiedLog } from "./logs.js";
|
|
20
|
-
import {
|
|
26
|
+
import { findNodeServicePids } from "./node-service-probe.js";
|
|
21
27
|
import { ensurePackUpdateConfigFile } from "./pack-update/index.js";
|
|
22
|
-
import {
|
|
28
|
+
import { recordBdsVersion } from "./bds-runtime-version.js";
|
|
29
|
+
import { reportBdsPlayerSession } from "./player-session.js";
|
|
30
|
+
import { ROOT, spawnService } from "./runtime.js";
|
|
23
31
|
import { ROOT as ROOT2 } from "./runtime.js";
|
|
24
|
-
|
|
32
|
+
function nonBlankOutputLines(text) {
|
|
33
|
+
return text.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
34
|
+
}
|
|
35
|
+
const SERVICE_NAMES = ["bds", "db", "tunnel", "qq", "llbot"];
|
|
36
|
+
let argvDaemonize = false;
|
|
37
|
+
function setArgvDaemonize(on) {
|
|
38
|
+
argvDaemonize = on;
|
|
39
|
+
}
|
|
25
40
|
let dbHealthPort = 3001;
|
|
41
|
+
let qqBackendMode = "official";
|
|
42
|
+
function getQqBackendMode() {
|
|
43
|
+
return qqBackendMode;
|
|
44
|
+
}
|
|
26
45
|
async function probeDbHealth(port) {
|
|
27
46
|
try {
|
|
28
47
|
const res = await fetch(`http://127.0.0.1:${port}/api/health`, { signal: AbortSignal.timeout(2e3) });
|
|
@@ -31,6 +50,28 @@ async function probeDbHealth(port) {
|
|
|
31
50
|
return false;
|
|
32
51
|
}
|
|
33
52
|
}
|
|
53
|
+
async function probeQqRuntime() {
|
|
54
|
+
const status = readJson(qqRuntimeStatusPath(ROOT));
|
|
55
|
+
if (!status?.pid || status.pid <= 0) return { alive: false, pid: 0 };
|
|
56
|
+
if (!await isProcessAlive(status.pid)) return { alive: false, pid: 0 };
|
|
57
|
+
return { alive: true, pid: status.pid };
|
|
58
|
+
}
|
|
59
|
+
const tunnelRuntimePath = path.join(ROOT, ".sfmc", "tunnel.runtime.json");
|
|
60
|
+
async function probeTunnelRuntime() {
|
|
61
|
+
const pid = readJson(tunnelRuntimePath)?.pid;
|
|
62
|
+
return typeof pid === "number" && pid > 0 && await isProcessAlive(pid) ? pid : 0;
|
|
63
|
+
}
|
|
64
|
+
function writeTunnelRuntime(pid) {
|
|
65
|
+
fs.mkdirSync(path.dirname(tunnelRuntimePath), { recursive: true });
|
|
66
|
+
fs.writeFileSync(tunnelRuntimePath, JSON.stringify({ pid, startedAt: Date.now() }) + "\n");
|
|
67
|
+
}
|
|
68
|
+
function clearTunnelRuntime(pid) {
|
|
69
|
+
if (readJson(tunnelRuntimePath)?.pid !== pid) return;
|
|
70
|
+
try {
|
|
71
|
+
fs.unlinkSync(tunnelRuntimePath);
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
34
75
|
class Service {
|
|
35
76
|
name;
|
|
36
77
|
title;
|
|
@@ -42,10 +83,17 @@ class Service {
|
|
|
42
83
|
events = new EventEmitter();
|
|
43
84
|
def;
|
|
44
85
|
manualStop = false;
|
|
86
|
+
updateInProgress = false;
|
|
87
|
+
restartTimer = null;
|
|
45
88
|
constructor(def) {
|
|
46
89
|
this.name = def.name;
|
|
47
90
|
this.title = def.title;
|
|
48
91
|
this.def = def;
|
|
92
|
+
if (def.name === "tunnel") {
|
|
93
|
+
this.events.on("output", (text, level) => {
|
|
94
|
+
pushUnifiedLog(text, "tunnel", level);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
49
97
|
}
|
|
50
98
|
get uptime() {
|
|
51
99
|
if (!this.startTime || !this.running) return "\u2014";
|
|
@@ -63,31 +111,50 @@ class Service {
|
|
|
63
111
|
this.events.emit("log", line);
|
|
64
112
|
const level = stream === "stderr" ? "error" : inferLevel(text);
|
|
65
113
|
pushUnifiedLog(text, this.name, level);
|
|
114
|
+
this.def.onLogLine?.(text);
|
|
66
115
|
}
|
|
67
116
|
async start() {
|
|
68
|
-
if (this.
|
|
117
|
+
if (this.updateInProgress) throw new Error("BDS \u6B63\u5728\u66F4\u65B0\uFF0C\u6682\u4E0D\u53EF\u542F\u52A8");
|
|
118
|
+
if (this.running) return { status: "already" };
|
|
69
119
|
if (this.name === "bds") {
|
|
70
120
|
const probe = await probeBdsStatus({ rootDir: ROOT });
|
|
71
121
|
if (probe.state !== "stopped") {
|
|
72
122
|
throw new Error(
|
|
73
|
-
t("svc.bdsAlreadyRunning", {
|
|
123
|
+
t("svc.bdsAlreadyRunning", {
|
|
124
|
+
pid: String(probe.pid),
|
|
125
|
+
kind: t(probe.state === "managed" ? "svc.running" : "svc.runningExternal")
|
|
126
|
+
})
|
|
74
127
|
);
|
|
75
128
|
}
|
|
76
129
|
}
|
|
130
|
+
if (this.name === "db" || this.name === "qq") {
|
|
131
|
+
const external = await findNodeServicePids(this.name);
|
|
132
|
+
if (external.length > 0) {
|
|
133
|
+
return { status: "already" };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (this.name === "tunnel" && await probeTunnelRuntime()) return { status: "already" };
|
|
77
137
|
if (this.def.validate) {
|
|
78
138
|
const v = this.def.validate();
|
|
79
|
-
if (v)
|
|
139
|
+
if (v) {
|
|
140
|
+
if (this.def.optional) return { status: "skipped", reason: v };
|
|
141
|
+
throw new Error(v);
|
|
142
|
+
}
|
|
80
143
|
}
|
|
81
144
|
if (this.def.beforeStart) {
|
|
82
145
|
await this.def.beforeStart();
|
|
83
146
|
}
|
|
147
|
+
if (this.updateInProgress) throw new Error("BDS \u6B63\u5728\u66F4\u65B0\uFF0C\u6682\u4E0D\u53EF\u542F\u52A8");
|
|
84
148
|
this.manualStop = false;
|
|
149
|
+
const daemonize = argvDaemonize && process.platform !== "win32";
|
|
85
150
|
const spawnOpts = {
|
|
86
151
|
cwd: this.def.cwd,
|
|
87
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
88
|
-
env: this.def.env ? { ...process.env, ...this.def.env } : process.env
|
|
152
|
+
stdio: daemonize ? "ignore" : ["pipe", "pipe", "pipe"],
|
|
153
|
+
env: this.def.env ? { ...process.env, ...this.def.env } : process.env,
|
|
154
|
+
detached: daemonize
|
|
89
155
|
};
|
|
90
156
|
const child = this.def.service ? spawnService(this.def.service, this.def.args ?? [], spawnOpts) : spawn(this.def.cmd, this.def.args ?? [], spawnOpts);
|
|
157
|
+
if (daemonize) child.unref();
|
|
91
158
|
this.proc = child;
|
|
92
159
|
this.pid = child.pid ?? 0;
|
|
93
160
|
this.running = true;
|
|
@@ -95,35 +162,86 @@ class Service {
|
|
|
95
162
|
if (this.name === "bds" && this.pid > 0) {
|
|
96
163
|
writeBdsPidFile(this.pid, ROOT);
|
|
97
164
|
}
|
|
165
|
+
if (this.name === "tunnel" && this.pid > 0) writeTunnelRuntime(this.pid);
|
|
98
166
|
this.events.emit("output", `started (PID ${this.pid})`, "info");
|
|
99
167
|
this.events.emit("state", { name: this.name, running: true, pid: this.pid });
|
|
168
|
+
let lifecycleReported = false;
|
|
169
|
+
const reportExit = (code) => {
|
|
170
|
+
if (this.name !== "bds" || lifecycleReported) return;
|
|
171
|
+
lifecycleReported = true;
|
|
172
|
+
const planned = this.manualStop || this.updateInProgress;
|
|
173
|
+
void postBdsLifecycleEvent(planned ? "stop" : "crash", planned ? void 0 : `code=${code ?? "?"}`, {
|
|
174
|
+
port: dbHealthPort
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
if (this.name === "bds") {
|
|
178
|
+
child.once("spawn", () => {
|
|
179
|
+
void postBdsLifecycleEvent("start", void 0, { port: dbHealthPort });
|
|
180
|
+
});
|
|
181
|
+
}
|
|
100
182
|
child.on("error", (e) => {
|
|
101
183
|
this.events.emit("output", `process error: ${e.message}`, "error");
|
|
184
|
+
reportExit();
|
|
102
185
|
this.cleanup();
|
|
186
|
+
this.scheduleRestart();
|
|
103
187
|
});
|
|
188
|
+
let versionLineBuffer = "";
|
|
104
189
|
child.stdout?.on("data", (d) => {
|
|
105
|
-
|
|
190
|
+
if (this.name === "bds" && this.startTime) {
|
|
191
|
+
versionLineBuffer += d.toString();
|
|
192
|
+
const versionLines = versionLineBuffer.split(/\r?\n/);
|
|
193
|
+
versionLineBuffer = (versionLines.pop() ?? "").slice(-4096);
|
|
194
|
+
for (const versionLine of versionLines) recordBdsVersion(ROOT, versionLine, this.pid, this.startTime.getTime());
|
|
195
|
+
}
|
|
196
|
+
for (const line of nonBlankOutputLines(d.toString())) {
|
|
106
197
|
this.pushLog(line, "stdout");
|
|
107
198
|
}
|
|
108
199
|
});
|
|
109
200
|
child.stderr?.on("data", (d) => {
|
|
110
|
-
for (const line of d.toString()
|
|
201
|
+
for (const line of nonBlankOutputLines(d.toString())) {
|
|
111
202
|
this.pushLog(line, "stderr");
|
|
112
203
|
}
|
|
113
204
|
});
|
|
114
205
|
child.on("exit", (code) => {
|
|
206
|
+
if (this.proc && this.proc !== child) return;
|
|
115
207
|
this.events.emit("output", `exited (code: ${code})`, "info");
|
|
208
|
+
reportExit(code);
|
|
116
209
|
this.cleanup();
|
|
117
|
-
|
|
118
|
-
setTimeout(() => {
|
|
119
|
-
void this.start();
|
|
120
|
-
}, this.def.restartDelay);
|
|
121
|
-
}
|
|
210
|
+
this.scheduleRestart();
|
|
122
211
|
});
|
|
212
|
+
return { status: "started" };
|
|
213
|
+
}
|
|
214
|
+
scheduleRestart() {
|
|
215
|
+
if (this.manualStop || this.updateInProgress || !this.def.autoRestart || this.restartTimer) return;
|
|
216
|
+
this.restartTimer = setTimeout(() => {
|
|
217
|
+
this.restartTimer = null;
|
|
218
|
+
if (!this.updateInProgress && !this.manualStop && !this.running) {
|
|
219
|
+
void this.start().catch((e) => {
|
|
220
|
+
this.events.emit("output", `restart failed: ${e.message}`, "error");
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}, this.def.restartDelay);
|
|
224
|
+
}
|
|
225
|
+
/** 更新器独立进程停服时,禁止将计划内退出当作崩溃并自动拉起。 */
|
|
226
|
+
beginUpdate() {
|
|
227
|
+
if (this.name !== "bds") return;
|
|
228
|
+
if (this.updateInProgress) throw new Error("BDS \u66F4\u65B0\u5DF2\u5728\u8FDB\u884C\u4E2D");
|
|
229
|
+
this.updateInProgress = true;
|
|
230
|
+
if (this.restartTimer) {
|
|
231
|
+
clearTimeout(this.restartTimer);
|
|
232
|
+
this.restartTimer = null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
endUpdate() {
|
|
236
|
+
this.updateInProgress = false;
|
|
123
237
|
}
|
|
124
238
|
async stop() {
|
|
125
|
-
if (!this.proc || !this.running) return;
|
|
126
239
|
this.manualStop = true;
|
|
240
|
+
if (this.restartTimer) {
|
|
241
|
+
clearTimeout(this.restartTimer);
|
|
242
|
+
this.restartTimer = null;
|
|
243
|
+
}
|
|
244
|
+
if (!this.proc || !this.running) return;
|
|
127
245
|
this.events.emit("output", "stopping...", "info");
|
|
128
246
|
if (this.def.stopCommand && this.proc.stdin) {
|
|
129
247
|
this.proc.stdin.write(this.def.stopCommand + "\n");
|
|
@@ -150,6 +268,10 @@ class Service {
|
|
|
150
268
|
forceStop() {
|
|
151
269
|
const child = this.proc;
|
|
152
270
|
this.manualStop = true;
|
|
271
|
+
if (this.restartTimer) {
|
|
272
|
+
clearTimeout(this.restartTimer);
|
|
273
|
+
this.restartTimer = null;
|
|
274
|
+
}
|
|
153
275
|
this.cleanup();
|
|
154
276
|
if (!child) return;
|
|
155
277
|
try {
|
|
@@ -159,7 +281,7 @@ class Service {
|
|
|
159
281
|
}
|
|
160
282
|
async restart() {
|
|
161
283
|
await this.stop();
|
|
162
|
-
|
|
284
|
+
return this.start();
|
|
163
285
|
}
|
|
164
286
|
getRecentLogs(n) {
|
|
165
287
|
return this.logs.slice(-n);
|
|
@@ -184,6 +306,7 @@ class Service {
|
|
|
184
306
|
clearBdsPidFile(ROOT);
|
|
185
307
|
}
|
|
186
308
|
}
|
|
309
|
+
if (this.name === "tunnel" && exitingPid > 0) clearTunnelRuntime(exitingPid);
|
|
187
310
|
if (wasRunning) {
|
|
188
311
|
this.events.emit("state", { name: this.name, running: false, pid: 0 });
|
|
189
312
|
}
|
|
@@ -192,31 +315,26 @@ class Service {
|
|
|
192
315
|
function createServices() {
|
|
193
316
|
ensureCoreConfigs(ROOT, ["bds_updater", "qq_config", "db_config"]);
|
|
194
317
|
ensurePackUpdateConfigFile();
|
|
195
|
-
const bdsCfg = loadEnsuredConfig(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
"bds_updater",
|
|
199
|
-
{ ...DEFAULT_BDS_UPDATER_CONFIG }
|
|
200
|
-
);
|
|
201
|
-
const qqCfg = loadEnsuredConfig(
|
|
202
|
-
ROOT,
|
|
203
|
-
"qq_config.json",
|
|
204
|
-
"qq_config",
|
|
205
|
-
{ ...DEFAULT_QQ_CONFIG }
|
|
206
|
-
);
|
|
207
|
-
const dbCfg = loadEnsuredConfig(
|
|
208
|
-
ROOT,
|
|
209
|
-
"db_config.json",
|
|
210
|
-
"db_config",
|
|
211
|
-
{ ...DEFAULT_DB_CONFIG }
|
|
212
|
-
);
|
|
318
|
+
const bdsCfg = loadEnsuredConfig(ROOT, "bds_updater.json", "bds_updater", { ...DEFAULT_BDS_UPDATER_CONFIG });
|
|
319
|
+
const qqCfg = loadEnsuredConfig(ROOT, "qq_config.json", "qq_config", { ...DEFAULT_QQ_CONFIG });
|
|
320
|
+
const dbCfg = loadEnsuredConfig(ROOT, "db_config.json", "db_config", { ...DEFAULT_DB_CONFIG });
|
|
213
321
|
const bdsPath = bdsCfg.bds_path ?? ROOT;
|
|
214
|
-
const
|
|
215
|
-
const
|
|
216
|
-
const
|
|
322
|
+
const useLlbotBackend = qqCfg.qq_backend === "llbot";
|
|
323
|
+
const qqEnabled = qqCfg.qq_enabled !== false;
|
|
324
|
+
const qqExternal = !useLlbotBackend && qqCfg.official?.transport === "webhook";
|
|
325
|
+
const tunnelCfg = qqCfg.official?.tunnel;
|
|
326
|
+
const tunnelEnabled = qqEnabled && qqExternal && tunnelCfg?.enabled === true;
|
|
327
|
+
const tunnelHost = String(tunnelCfg?.host ?? "").trim();
|
|
328
|
+
const tunnelRemotePort = tunnelCfg?.remotePort ?? 13001;
|
|
329
|
+
const llbotEnabled = qqCfg.llbot?.enabled !== false;
|
|
330
|
+
const llbotLaunch = resolveLlbotLaunch(qqCfg.llbot?.path, qqCfg.llbot?.cwd);
|
|
331
|
+
const llbotPath = llbotLaunch.exe;
|
|
332
|
+
const llbotCwd = llbotLaunch.cwd;
|
|
217
333
|
const dbPort = dbCfg.db_port ?? 3001;
|
|
218
334
|
dbHealthPort = dbPort;
|
|
219
|
-
|
|
335
|
+
qqBackendMode = useLlbotBackend ? "llbot" : "official";
|
|
336
|
+
const bdsExe = bdsExePath(path.resolve(bdsPath));
|
|
337
|
+
const qqTitle = useLlbotBackend ? "QQ (llbot)" : "QQ (official)";
|
|
220
338
|
return {
|
|
221
339
|
bds: new Service({
|
|
222
340
|
name: "bds",
|
|
@@ -224,6 +342,7 @@ function createServices() {
|
|
|
224
342
|
cmd: bdsExe,
|
|
225
343
|
args: [],
|
|
226
344
|
cwd: bdsPath,
|
|
345
|
+
env: bdsSpawnEnvExtra(bdsPath),
|
|
227
346
|
stopCommand: "stop",
|
|
228
347
|
stopTimeout: 3e4,
|
|
229
348
|
autoRestart: bdsCfg.crash_restart !== false,
|
|
@@ -233,13 +352,18 @@ function createServices() {
|
|
|
233
352
|
return null;
|
|
234
353
|
},
|
|
235
354
|
beforeStart: async () => {
|
|
355
|
+
ensureBdsExecutable(bdsExe);
|
|
236
356
|
const { scanAndInstallInbox } = await import("./world-packs.js");
|
|
237
357
|
await scanAndInstallInbox({ interactive: false });
|
|
238
358
|
const { runPackUpdatesOnBdsStart } = await import("./pack-update/index.js");
|
|
239
359
|
await runPackUpdatesOnBdsStart();
|
|
240
360
|
const { ensurePacksReady } = await import("./pack-lifecycle.js");
|
|
241
361
|
await ensurePacksReady();
|
|
242
|
-
}
|
|
362
|
+
},
|
|
363
|
+
onLogLine: (line) => reportBdsPlayerSession(line, {
|
|
364
|
+
port: dbPort,
|
|
365
|
+
...dbCfg.http_auth ? { authToken: dbCfg.http_auth } : {}
|
|
366
|
+
})
|
|
243
367
|
}),
|
|
244
368
|
db: new Service({
|
|
245
369
|
name: "db",
|
|
@@ -251,14 +375,61 @@ function createServices() {
|
|
|
251
375
|
restartDelay: 3e3,
|
|
252
376
|
env: { DB_PORT: String(dbPort) }
|
|
253
377
|
}),
|
|
378
|
+
tunnel: new Service({
|
|
379
|
+
name: "tunnel",
|
|
380
|
+
title: "QQ SSH Tunnel",
|
|
381
|
+
cmd: "ssh",
|
|
382
|
+
args: [
|
|
383
|
+
"-N",
|
|
384
|
+
"-T",
|
|
385
|
+
"-o",
|
|
386
|
+
"BatchMode=yes",
|
|
387
|
+
"-o",
|
|
388
|
+
"ConnectTimeout=10",
|
|
389
|
+
"-o",
|
|
390
|
+
"ExitOnForwardFailure=yes",
|
|
391
|
+
"-o",
|
|
392
|
+
"ServerAliveInterval=30",
|
|
393
|
+
"-o",
|
|
394
|
+
"ServerAliveCountMax=3",
|
|
395
|
+
"-R",
|
|
396
|
+
`127.0.0.1:${tunnelRemotePort}:127.0.0.1:${dbPort}`,
|
|
397
|
+
tunnelHost
|
|
398
|
+
],
|
|
399
|
+
cwd: ROOT,
|
|
400
|
+
stopTimeout: 1e4,
|
|
401
|
+
autoRestart: true,
|
|
402
|
+
restartDelay: 5e3,
|
|
403
|
+
optional: !tunnelEnabled,
|
|
404
|
+
validate: () => {
|
|
405
|
+
if (!tunnelEnabled) return "QQ tunnel disabled (official webhook tunnel.enabled=false)";
|
|
406
|
+
if (!/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(tunnelHost)) return "invalid official.tunnel.host";
|
|
407
|
+
if (!Number.isInteger(tunnelRemotePort) || tunnelRemotePort < 1 || tunnelRemotePort > 65535) {
|
|
408
|
+
return "invalid official.tunnel.remotePort";
|
|
409
|
+
}
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
}),
|
|
254
413
|
qq: new Service({
|
|
255
414
|
name: "qq",
|
|
256
|
-
title:
|
|
415
|
+
title: qqTitle,
|
|
257
416
|
service: "qq",
|
|
258
417
|
cwd: ROOT,
|
|
259
418
|
stopTimeout: 1e4,
|
|
419
|
+
// 缺凭据时 validate 拦下,不会 spawn→exit(1)→autoRestart 死循环
|
|
260
420
|
autoRestart: true,
|
|
261
|
-
restartDelay: 3e3
|
|
421
|
+
restartDelay: 3e3,
|
|
422
|
+
optional: !qqEnabled || qqExternal,
|
|
423
|
+
validate: () => {
|
|
424
|
+
if (!qqEnabled) return "QQ bridge disabled (qq_enabled=false)";
|
|
425
|
+
if (qqExternal) return "QQ bridge runs externally (official.transport=webhook)";
|
|
426
|
+
if (!useLlbotBackend) {
|
|
427
|
+
if (!String(qqCfg.official?.app_id ?? "").trim() || !String(qqCfg.official?.app_secret ?? "").trim()) {
|
|
428
|
+
return "missing official.app_id / official.app_secret";
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
262
433
|
}),
|
|
263
434
|
llbot: new Service({
|
|
264
435
|
name: "llbot",
|
|
@@ -269,8 +440,11 @@ function createServices() {
|
|
|
269
440
|
stopTimeout: 1e4,
|
|
270
441
|
autoRestart: false,
|
|
271
442
|
restartDelay: 5e3,
|
|
443
|
+
// official 或未启用时真正 skip,不 throw
|
|
444
|
+
optional: !useLlbotBackend || !llbotEnabled,
|
|
272
445
|
validate: () => {
|
|
273
|
-
if (!
|
|
446
|
+
if (!useLlbotBackend) return "LLBot skipped (qq_backend!=llbot)";
|
|
447
|
+
if (!llbotEnabled) return "LLBot disabled (llbot.enabled=false)";
|
|
274
448
|
if (!fs.existsSync(llbotPath)) return `not found: ${llbotPath}`;
|
|
275
449
|
return null;
|
|
276
450
|
}
|
|
@@ -282,20 +456,30 @@ function refreshServices() {
|
|
|
282
456
|
forceStopAll();
|
|
283
457
|
services = createServices();
|
|
284
458
|
}
|
|
285
|
-
const START_ORDER = ["db", "qq", "llbot", "bds"];
|
|
459
|
+
const START_ORDER = ["db", "tunnel", "qq", "llbot", "bds"];
|
|
286
460
|
async function startAll() {
|
|
461
|
+
const result = { started: [], skipped: [], failed: [] };
|
|
287
462
|
for (const name of START_ORDER) {
|
|
288
463
|
const svc = services[name];
|
|
289
464
|
if (!svc) continue;
|
|
290
465
|
try {
|
|
291
|
-
await svc.start();
|
|
466
|
+
const outcome = await svc.start();
|
|
467
|
+
if (outcome.status === "skipped") {
|
|
468
|
+
result.skipped.push({ name, reason: outcome.reason });
|
|
469
|
+
svc.events.emit("output", `skipped: ${outcome.reason}`, "info");
|
|
470
|
+
} else {
|
|
471
|
+
result.started.push(name);
|
|
472
|
+
}
|
|
292
473
|
} catch (e) {
|
|
293
|
-
|
|
474
|
+
const reason = e.message;
|
|
475
|
+
result.failed.push({ name, reason });
|
|
476
|
+
svc.events.emit("output", `start error: ${reason}`, "error");
|
|
294
477
|
}
|
|
295
478
|
}
|
|
479
|
+
return result;
|
|
296
480
|
}
|
|
297
481
|
async function stopAll() {
|
|
298
|
-
const pending = [...START_ORDER].reverse().map((name) => services[name]).filter((service) => Boolean(service
|
|
482
|
+
const pending = [...START_ORDER].reverse().map((name) => services[name]).filter((service) => Boolean(service)).map((service) => service.stop());
|
|
299
483
|
await Promise.allSettled(pending);
|
|
300
484
|
}
|
|
301
485
|
function forceStopAll() {
|
|
@@ -366,6 +550,42 @@ async function queryServicesRuntime() {
|
|
|
366
550
|
} else if (await probeDbHealth(dbHealthPort)) {
|
|
367
551
|
running = true;
|
|
368
552
|
ownership = "external";
|
|
553
|
+
const pids = await findNodeServicePids("db");
|
|
554
|
+
pid = pids[0] ?? 0;
|
|
555
|
+
}
|
|
556
|
+
} else if (name === "qq") {
|
|
557
|
+
const managed = await reconcileManagedAlive(service);
|
|
558
|
+
if (managed) {
|
|
559
|
+
running = true;
|
|
560
|
+
pid = service.pid;
|
|
561
|
+
uptime = service.uptime;
|
|
562
|
+
ownership = "managed";
|
|
563
|
+
} else {
|
|
564
|
+
const probe = await probeQqRuntime();
|
|
565
|
+
if (probe.alive) {
|
|
566
|
+
running = true;
|
|
567
|
+
pid = probe.pid;
|
|
568
|
+
ownership = "external";
|
|
569
|
+
} else {
|
|
570
|
+
const pids = await findNodeServicePids("qq");
|
|
571
|
+
if (pids.length > 0) {
|
|
572
|
+
running = true;
|
|
573
|
+
pid = pids[0];
|
|
574
|
+
ownership = "external";
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
} else if (name === "tunnel") {
|
|
579
|
+
const managed = await reconcileManagedAlive(service);
|
|
580
|
+
if (managed) {
|
|
581
|
+
running = true;
|
|
582
|
+
pid = service.pid;
|
|
583
|
+
uptime = service.uptime;
|
|
584
|
+
ownership = "managed";
|
|
585
|
+
} else {
|
|
586
|
+
pid = await probeTunnelRuntime();
|
|
587
|
+
running = pid > 0;
|
|
588
|
+
if (running) ownership = "external";
|
|
369
589
|
}
|
|
370
590
|
} else {
|
|
371
591
|
const managed = await reconcileManagedAlive(service);
|
|
@@ -396,11 +616,14 @@ export {
|
|
|
396
616
|
SERVICE_NAMES,
|
|
397
617
|
START_ORDER,
|
|
398
618
|
forceStopAll,
|
|
619
|
+
getQqBackendMode,
|
|
399
620
|
isServiceRunning,
|
|
621
|
+
nonBlankOutputLines,
|
|
400
622
|
onServiceStateChange,
|
|
401
623
|
queryServicesRuntime,
|
|
402
624
|
refreshServices,
|
|
403
625
|
services,
|
|
626
|
+
setArgvDaemonize,
|
|
404
627
|
startAll,
|
|
405
628
|
stopAll
|
|
406
629
|
};
|