@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.
Files changed (149) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +4 -5
  3. package/dist/bds-runtime-version.d.ts +3 -0
  4. package/dist/bds-runtime-version.d.ts.map +1 -0
  5. package/dist/bds-runtime-version.js +15 -0
  6. package/dist/bds-runtime-version.js.map +7 -0
  7. package/dist/command-surface.js +3 -3
  8. package/dist/command-surface.js.map +2 -2
  9. package/dist/commands.d.ts +57 -4
  10. package/dist/commands.d.ts.map +1 -1
  11. package/dist/commands.js +184 -68
  12. package/dist/commands.js.map +2 -2
  13. package/dist/directory-picker.d.ts +27 -0
  14. package/dist/directory-picker.d.ts.map +1 -0
  15. package/dist/directory-picker.js +112 -0
  16. package/dist/directory-picker.js.map +7 -0
  17. package/dist/help-text.d.ts.map +1 -1
  18. package/dist/help-text.js +1 -5
  19. package/dist/help-text.js.map +2 -2
  20. package/dist/i18n/locales/en.d.ts +49 -14
  21. package/dist/i18n/locales/en.d.ts.map +1 -1
  22. package/dist/i18n/locales/en.js +49 -15
  23. package/dist/i18n/locales/en.js.map +2 -2
  24. package/dist/i18n/locales/zh-CN.d.ts.map +1 -1
  25. package/dist/i18n/locales/zh-CN.js +50 -16
  26. package/dist/i18n/locales/zh-CN.js.map +2 -2
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2 -0
  30. package/dist/index.js.map +2 -2
  31. package/dist/interactive-prompts.d.ts +1 -2
  32. package/dist/interactive-prompts.d.ts.map +1 -1
  33. package/dist/interactive-prompts.js +12 -23
  34. package/dist/interactive-prompts.js.map +3 -3
  35. package/dist/llbot-launch.d.ts +20 -0
  36. package/dist/llbot-launch.d.ts.map +1 -0
  37. package/dist/llbot-launch.js +40 -0
  38. package/dist/llbot-launch.js.map +7 -0
  39. package/dist/log-filter.d.ts +18 -2
  40. package/dist/log-filter.d.ts.map +1 -1
  41. package/dist/log-filter.js +54 -4
  42. package/dist/log-filter.js.map +2 -2
  43. package/dist/log-translator.d.ts +36 -0
  44. package/dist/log-translator.d.ts.map +1 -0
  45. package/dist/log-translator.js +279 -0
  46. package/dist/log-translator.js.map +7 -0
  47. package/dist/logs.d.ts +2 -0
  48. package/dist/logs.d.ts.map +1 -1
  49. package/dist/logs.js +24 -11
  50. package/dist/logs.js.map +2 -2
  51. package/dist/main.js +9 -6
  52. package/dist/main.js.map +2 -2
  53. package/dist/module-commands.d.ts.map +1 -1
  54. package/dist/module-commands.js +22 -8
  55. package/dist/module-commands.js.map +2 -2
  56. package/dist/module-fingerprint.d.ts.map +1 -1
  57. package/dist/module-fingerprint.js +1 -0
  58. package/dist/module-fingerprint.js.map +2 -2
  59. package/dist/module-pack-build.d.ts +14 -4
  60. package/dist/module-pack-build.d.ts.map +1 -1
  61. package/dist/module-pack-build.js +81 -108
  62. package/dist/module-pack-build.js.map +2 -2
  63. package/dist/module-toggle.d.ts +1 -1
  64. package/dist/module-toggle.d.ts.map +1 -1
  65. package/dist/module-toggle.js.map +1 -1
  66. package/dist/node-service-probe.d.ts +18 -0
  67. package/dist/node-service-probe.d.ts.map +1 -0
  68. package/dist/node-service-probe.js +97 -0
  69. package/dist/node-service-probe.js.map +7 -0
  70. package/dist/pack-lifecycle.d.ts +15 -1
  71. package/dist/pack-lifecycle.d.ts.map +1 -1
  72. package/dist/pack-lifecycle.js +249 -16
  73. package/dist/pack-lifecycle.js.map +2 -2
  74. package/dist/player-session.d.ts +13 -0
  75. package/dist/player-session.d.ts.map +1 -0
  76. package/dist/player-session.js +26 -0
  77. package/dist/player-session.js.map +7 -0
  78. package/dist/registry.d.ts +1 -1
  79. package/dist/registry.d.ts.map +1 -1
  80. package/dist/registry.js +4 -1
  81. package/dist/registry.js.map +2 -2
  82. package/dist/repl-windows/format-display.d.ts.map +1 -1
  83. package/dist/repl-windows/format-display.js +5 -1
  84. package/dist/repl-windows/format-display.js.map +2 -2
  85. package/dist/repl-windows/host.d.ts +6 -3
  86. package/dist/repl-windows/host.d.ts.map +1 -1
  87. package/dist/repl-windows/host.js +12 -6
  88. package/dist/repl-windows/host.js.map +2 -2
  89. package/dist/repl-windows/index.d.ts +1 -0
  90. package/dist/repl-windows/index.d.ts.map +1 -1
  91. package/dist/repl-windows/index.js +3 -0
  92. package/dist/repl-windows/index.js.map +2 -2
  93. package/dist/repl-windows/service-window.d.ts +1 -1
  94. package/dist/repl-windows/service-window.d.ts.map +1 -1
  95. package/dist/repl-windows/service-window.js +9 -1
  96. package/dist/repl-windows/service-window.js.map +2 -2
  97. package/dist/repl-windows/sfmc-window.d.ts +7 -0
  98. package/dist/repl-windows/sfmc-window.d.ts.map +1 -0
  99. package/dist/repl-windows/sfmc-window.js +28 -0
  100. package/dist/repl-windows/sfmc-window.js.map +7 -0
  101. package/dist/repl.d.ts.map +1 -1
  102. package/dist/repl.js +37 -22
  103. package/dist/repl.js.map +2 -2
  104. package/dist/runtime.d.ts +2 -2
  105. package/dist/runtime.d.ts.map +1 -1
  106. package/dist/runtime.js +15 -2
  107. package/dist/runtime.js.map +2 -2
  108. package/dist/send-target.d.ts +2 -0
  109. package/dist/send-target.d.ts.map +1 -1
  110. package/dist/send-target.js +12 -2
  111. package/dist/send-target.js.map +2 -2
  112. package/dist/services.d.ts +58 -5
  113. package/dist/services.d.ts.map +1 -1
  114. package/dist/services.js +273 -50
  115. package/dist/services.js.map +2 -2
  116. package/dist/sfmc-modules-root.d.ts.map +1 -1
  117. package/dist/sfmc-modules-root.js +6 -2
  118. package/dist/sfmc-modules-root.js.map +2 -2
  119. package/dist/ui-command.d.ts +6 -0
  120. package/dist/ui-command.d.ts.map +1 -0
  121. package/dist/ui-command.js +68 -0
  122. package/dist/ui-command.js.map +7 -0
  123. package/dist/wizard.d.ts.map +1 -1
  124. package/dist/wizard.js +95 -32
  125. package/dist/wizard.js.map +2 -2
  126. package/dist/world-packs.d.ts +2 -0
  127. package/dist/world-packs.d.ts.map +1 -1
  128. package/dist/world-packs.js +161 -18
  129. package/dist/world-packs.js.map +2 -2
  130. package/package.json +21 -15
  131. package/scripts/module-install/check-modules.mjs +178 -0
  132. package/scripts/module-install/fetch-module.mjs +1083 -0
  133. package/scripts/module-install/lib/catalog.mjs +96 -0
  134. package/scripts/module-install/lib/config-seeding.mjs +128 -0
  135. package/scripts/module-install/lib/io.mjs +37 -0
  136. package/scripts/module-install/lib/link-from.mjs +80 -0
  137. package/scripts/module-install/lib/lock.mjs +78 -0
  138. package/scripts/module-install/lib/npm-resolver.mjs +55 -0
  139. package/scripts/module-install/lib/packages.mjs +136 -0
  140. package/scripts/module-install/lib/paths.mjs +48 -0
  141. package/scripts/module-install/lib/registry-index.mjs +50 -0
  142. package/dist/cmd-remote.d.ts +0 -13
  143. package/dist/cmd-remote.d.ts.map +0 -1
  144. package/dist/cmd-remote.js +0 -38
  145. package/dist/cmd-remote.js.map +0 -7
  146. package/dist/remote-agent.d.ts +0 -18
  147. package/dist/remote-agent.d.ts.map +0 -1
  148. package/dist/remote-agent.js +0 -198
  149. package/dist/remote-agent.js.map +0 -7
@@ -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
- export type ServiceName = "bds" | "db" | "qq" | "llbot";
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<void>;
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<void>;
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<void>;
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 发送目标 / remote / reload 等均应走此接口,勿直接读 `service.running`。
129
+ * status / Tab 发送目标 / reload 等均应走此接口,勿直接读 `service.running`。
77
130
  */
78
131
  export declare function queryServicesRuntime(): Promise<ServiceStatus[]>;
79
132
  /** 单服务是否在跑(含外部实例) */
@@ -1 +1 @@
1
- {"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../src/services.ts"],"names":[],"mappings":"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"}
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 { ROOT, spawnService } from "./runtime.js";
26
+ import { findNodeServicePids } from "./node-service-probe.js";
21
27
  import { ensurePackUpdateConfigFile } from "./pack-update/index.js";
22
- import { t } from "./i18n/index.js";
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
- const SERVICE_NAMES = ["bds", "db", "qq", "llbot"];
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.running) return;
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", { pid: String(probe.pid), kind: t(probe.state === "managed" ? "svc.running" : "svc.runningExternal") })
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) throw new Error(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
- for (const line of d.toString().split("\n").filter(Boolean)) {
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().split("\n").filter(Boolean)) {
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
- if (!this.manualStop && this.def.autoRestart) {
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
- await this.start();
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
- ROOT,
197
- "bds_updater.json",
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 llbotEnabled = qqCfg.llbot_enabled !== false;
215
- const llbotPath = qqCfg.llbot_path ?? "D:\\LLBot-CLI-win-x64\\llbot.exe";
216
- const llbotCwd = qqCfg.llbot_cwd ?? "D:\\LLBot-CLI-win-x64";
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
- const bdsExe = path.resolve(bdsPath, "bedrock_server.exe");
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: "QQ Bridge",
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 (!llbotEnabled) return "LLBot disabled (llbot_enabled=false)";
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
- svc.events.emit("output", `start error: ${e.message}`, "error");
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?.running)).map((service) => service.stop());
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
  };