cortico 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.
Files changed (311) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/package.json +43 -0
  4. package/src/boot.ts +46 -0
  5. package/src/bot.ts +1342 -0
  6. package/src/config-file.ts +41 -0
  7. package/src/core/README.md +92 -0
  8. package/src/core/billing.ts +26 -0
  9. package/src/core/blobs.ts +106 -0
  10. package/src/core/bus.ts +311 -0
  11. package/src/core/config-schema.ts +201 -0
  12. package/src/core/config.ts +262 -0
  13. package/src/core/core.ts +699 -0
  14. package/src/core/cost.ts +244 -0
  15. package/src/core/event-store.ts +304 -0
  16. package/src/core/fork.ts +113 -0
  17. package/src/core/generation.ts +118 -0
  18. package/src/core/instance-lock.ts +215 -0
  19. package/src/core/ipc-logger.ts +105 -0
  20. package/src/core/language.ts +62 -0
  21. package/src/core/log-context.ts +38 -0
  22. package/src/core/loop.ts +1858 -0
  23. package/src/core/markers.ts +48 -0
  24. package/src/core/prefix.ts +121 -0
  25. package/src/core/run.ts +95 -0
  26. package/src/core/secrets.ts +18 -0
  27. package/src/core/session.ts +56 -0
  28. package/src/core/sessions.ts +261 -0
  29. package/src/core/state.ts +104 -0
  30. package/src/core/template.ts +72 -0
  31. package/src/core/timers.ts +129 -0
  32. package/src/core/tool-log.ts +135 -0
  33. package/src/core/transcript.ts +92 -0
  34. package/src/core/truncate.ts +121 -0
  35. package/src/core/types.ts +1140 -0
  36. package/src/core/usage-log.ts +79 -0
  37. package/src/core/util.ts +386 -0
  38. package/src/deploy-listing.ts +160 -0
  39. package/src/deploy.ts +162 -0
  40. package/src/extensions/README.md +72 -0
  41. package/src/extensions/dry-mount.ts +390 -0
  42. package/src/extensions/manifest.ts +153 -0
  43. package/src/extensions/runtime.ts +42 -0
  44. package/src/extensions.ts +519 -0
  45. package/src/launcher.ts +285 -0
  46. package/src/paths.ts +155 -0
  47. package/src/protocol/open-responses/LICENSE +201 -0
  48. package/src/protocol/open-responses/README.md +14 -0
  49. package/src/protocol/open-responses/context-helpers.ts +27 -0
  50. package/src/protocol/open-responses/context-log.ts +59 -0
  51. package/src/protocol/open-responses/context.ts +89 -0
  52. package/src/protocol/open-responses/generated.ts +115 -0
  53. package/src/protocol/open-responses/index.ts +33 -0
  54. package/src/protocol/open-responses/openapi.json +4230 -0
  55. package/src/protocol/open-responses/stream.ts +146 -0
  56. package/src/protocol/open-responses/tokens.ts +31 -0
  57. package/src/providers/README.md +86 -0
  58. package/src/providers/base.ts +111 -0
  59. package/src/providers/configuration.ts +97 -0
  60. package/src/providers/console/config.ts +21 -0
  61. package/src/providers/console/settings.ts +443 -0
  62. package/src/providers/console/strings.ts +74 -0
  63. package/src/providers/console/types.ts +11 -0
  64. package/src/providers/llamacpp/archive.ts +148 -0
  65. package/src/providers/llamacpp/catalog.ts +247 -0
  66. package/src/providers/llamacpp/config.ts +36 -0
  67. package/src/providers/llamacpp/console/client.ts +6 -0
  68. package/src/providers/llamacpp/console/models-panel.ts +120 -0
  69. package/src/providers/llamacpp/console/runtime-panel.ts +173 -0
  70. package/src/providers/llamacpp/console/server.ts +153 -0
  71. package/src/providers/llamacpp/index.ts +95 -0
  72. package/src/providers/llamacpp/native.ts +51 -0
  73. package/src/providers/llamacpp/options.ts +117 -0
  74. package/src/providers/llamacpp/runtime-store.ts +139 -0
  75. package/src/providers/llamacpp/runtime.ts +214 -0
  76. package/src/providers/llamacpp/server.ts +286 -0
  77. package/src/providers/llamacpp/strings.ts +230 -0
  78. package/src/providers/openai-responses-compat/config.ts +20 -0
  79. package/src/providers/openai-responses-compat/index.ts +89 -0
  80. package/src/providers/openai-responses-compat/native.ts +127 -0
  81. package/src/providers/openai-responses-compat/strings.ts +19 -0
  82. package/src/providers/pricebook.ts +75 -0
  83. package/src/providers/registry.ts +137 -0
  84. package/src/providers/strings.ts +71 -0
  85. package/src/providers/transport/chat.ts +156 -0
  86. package/src/providers/transport/errors.ts +48 -0
  87. package/src/providers/transport/history.ts +68 -0
  88. package/src/providers/transport/native-input.ts +83 -0
  89. package/src/providers/transport/native-types.ts +13 -0
  90. package/src/providers/transport/response-assembly.ts +191 -0
  91. package/src/providers/transport/response-http.ts +196 -0
  92. package/src/providers/transport/response-meters.ts +27 -0
  93. package/src/providers/transport/responses-input.ts +56 -0
  94. package/src/web/README.md +84 -0
  95. package/src/web/client/console-pages/builtins/llm-settings/panel.ts +625 -0
  96. package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +168 -0
  97. package/src/web/client/console-pages/builtins/llm-settings/strings.ts +213 -0
  98. package/src/web/client/console-pages/builtins.ts +10 -0
  99. package/src/web/client/console-pages/context.ts +135 -0
  100. package/src/web/client/console-pages/host.ts +633 -0
  101. package/src/web/client/console-pages/loader.ts +132 -0
  102. package/src/web/client/console-pages/strings.ts +90 -0
  103. package/src/web/client/console-pages/tools/strings.ts +49 -0
  104. package/src/web/client/console-pages/tools/view.ts +215 -0
  105. package/src/web/client/core/api.ts +200 -0
  106. package/src/web/client/core/language.ts +56 -0
  107. package/src/web/client/core/lifecycle.ts +132 -0
  108. package/src/web/client/core/router.ts +199 -0
  109. package/src/web/client/core/stream.ts +157 -0
  110. package/src/web/client/core/websocket.ts +82 -0
  111. package/src/web/client/features/appearance/index.ts +333 -0
  112. package/src/web/client/features/appearance/strings.ts +87 -0
  113. package/src/web/client/features/config/index.ts +11 -0
  114. package/src/web/client/features/config/strings.ts +41 -0
  115. package/src/web/client/features/config/view.ts +428 -0
  116. package/src/web/client/features/core/events.ts +197 -0
  117. package/src/web/client/features/core/index.ts +254 -0
  118. package/src/web/client/features/core/run.ts +56 -0
  119. package/src/web/client/features/core/runlog.ts +214 -0
  120. package/src/web/client/features/core/sessions.ts +53 -0
  121. package/src/web/client/features/core/strings.ts +157 -0
  122. package/src/web/client/features/extensions/index.ts +422 -0
  123. package/src/web/client/features/extensions/strings.ts +164 -0
  124. package/src/web/client/features/feature.ts +93 -0
  125. package/src/web/client/features/live/context.ts +280 -0
  126. package/src/web/client/features/live/fork.ts +156 -0
  127. package/src/web/client/features/live/index.ts +392 -0
  128. package/src/web/client/features/live/onboarding.ts +113 -0
  129. package/src/web/client/features/live/protocol.ts +122 -0
  130. package/src/web/client/features/live/sessions.ts +71 -0
  131. package/src/web/client/features/live/status.ts +68 -0
  132. package/src/web/client/features/live/strings.ts +252 -0
  133. package/src/web/client/features/live/timeline.ts +590 -0
  134. package/src/web/client/features/prompts/editor.ts +292 -0
  135. package/src/web/client/features/prompts/index.ts +346 -0
  136. package/src/web/client/features/prompts/strings.ts +136 -0
  137. package/src/web/client/features/prompts/view.ts +256 -0
  138. package/src/web/client/features/providers/index.ts +100 -0
  139. package/src/web/client/features/providers/strings.ts +23 -0
  140. package/src/web/client/features/settings/general.ts +28 -0
  141. package/src/web/client/features/settings/index.ts +82 -0
  142. package/src/web/client/features/settings/strings.ts +33 -0
  143. package/src/web/client/features/storage/index.ts +1 -0
  144. package/src/web/client/features/storage/strings.ts +45 -0
  145. package/src/web/client/features/storage/view.ts +154 -0
  146. package/src/web/client/features/usage/chart.ts +486 -0
  147. package/src/web/client/features/usage/index.ts +407 -0
  148. package/src/web/client/features/usage/labels.ts +46 -0
  149. package/src/web/client/features/usage/range.ts +36 -0
  150. package/src/web/client/features/usage/state.ts +37 -0
  151. package/src/web/client/features/usage/strings.ts +191 -0
  152. package/src/web/client/features/usage/tooltip.ts +86 -0
  153. package/src/web/client/features/usage/types.ts +96 -0
  154. package/src/web/client/features/worlds/index.ts +424 -0
  155. package/src/web/client/features/worlds/strings.ts +121 -0
  156. package/src/web/client/main.ts +273 -0
  157. package/src/web/client/shell/avatar.ts +233 -0
  158. package/src/web/client/shell/index.ts +549 -0
  159. package/src/web/client/shell/strings.ts +89 -0
  160. package/src/web/client/theme/handoff.ts +43 -0
  161. package/src/web/client/theme/palette.ts +109 -0
  162. package/src/web/client/theme/registry.ts +130 -0
  163. package/src/web/client/theme/storage.ts +72 -0
  164. package/src/web/client/theme/strings.ts +123 -0
  165. package/src/web/client/theme/studio.ts +366 -0
  166. package/src/web/client/ui/actions.ts +109 -0
  167. package/src/web/client/ui/data.ts +185 -0
  168. package/src/web/client/ui/dom.ts +23 -0
  169. package/src/web/client/ui/fields.ts +191 -0
  170. package/src/web/client/ui/format.ts +75 -0
  171. package/src/web/client/ui/icons.ts +183 -0
  172. package/src/web/client/ui/images.ts +91 -0
  173. package/src/web/client/ui/index.ts +89 -0
  174. package/src/web/client/ui/lamp.ts +125 -0
  175. package/src/web/client/ui/log.ts +98 -0
  176. package/src/web/client/ui/overlay.ts +184 -0
  177. package/src/web/client/ui/page.ts +8 -0
  178. package/src/web/client/ui/prompt-input.tsx +311 -0
  179. package/src/web/client/ui/sheet.ts +131 -0
  180. package/src/web/client/ui/strings.ts +71 -0
  181. package/src/web/console-pages.ts +367 -0
  182. package/src/web/files.ts +107 -0
  183. package/src/web/path-picker.ts +296 -0
  184. package/src/web/public/index.html +17 -0
  185. package/src/web/public/styles.css +1715 -0
  186. package/src/web/server.ts +1883 -0
  187. package/src/web/shared/client-panel.ts +603 -0
  188. package/src/web/shared/console-protocol.ts +560 -0
  189. package/src/web/shared/css.d.ts +1 -0
  190. package/src/web/shared/path-picker.ts +23 -0
  191. package/src/web/shared/theme.ts +256 -0
  192. package/src/web/theme-store.ts +37 -0
  193. package/src/world.ts +449 -0
  194. package/src/worlds/bilibili/ENV_PROMPT.md +5 -0
  195. package/src/worlds/bilibili/README.md +176 -0
  196. package/src/worlds/bilibili/audience-admission.ts +995 -0
  197. package/src/worlds/bilibili/client.ts +528 -0
  198. package/src/worlds/bilibili/coalescing-buffer.ts +125 -0
  199. package/src/worlds/bilibili/config.ts +185 -0
  200. package/src/worlds/bilibili/console/client.ts +202 -0
  201. package/src/worlds/bilibili/definition.ts +35 -0
  202. package/src/worlds/bilibili/gift-frame.ts +131 -0
  203. package/src/worlds/bilibili/normalize.ts +452 -0
  204. package/src/worlds/bilibili/overlay/announcement.ts +76 -0
  205. package/src/worlds/bilibili/overlay/assets.ts +82 -0
  206. package/src/worlds/bilibili/overlay/model.ts +494 -0
  207. package/src/worlds/bilibili/overlay/project.ts +202 -0
  208. package/src/worlds/bilibili/overlay/server.ts +286 -0
  209. package/src/worlds/bilibili/overlay/types.ts +217 -0
  210. package/src/worlds/bilibili/overlay/web/app.js +690 -0
  211. package/src/worlds/bilibili/overlay/web/editor.css +297 -0
  212. package/src/worlds/bilibili/overlay/web/editor.html +102 -0
  213. package/src/worlds/bilibili/overlay/web/editor.js +2116 -0
  214. package/src/worlds/bilibili/overlay/web/overlay.html +13 -0
  215. package/src/worlds/bilibili/overlay/web/styles.css +258 -0
  216. package/src/worlds/bilibili/protobuf.ts +123 -0
  217. package/src/worlds/bilibili/wire.ts +94 -0
  218. package/src/worlds/bilibili/world.ts +1389 -0
  219. package/src/worlds/console-fixture/ENV_PROMPT.md +1 -0
  220. package/src/worlds/console-fixture/console/client.ts +72 -0
  221. package/src/worlds/console-fixture/world.ts +73 -0
  222. package/src/worlds/index.ts +15 -0
  223. package/src/worlds/minecraft/ENV_PROMPT.md +32 -0
  224. package/src/worlds/minecraft/ENV_PROMPT_CAMERA.md +2 -0
  225. package/src/worlds/minecraft/LICENSE-mineflayer-pathfinder.txt +21 -0
  226. package/src/worlds/minecraft/README.md +917 -0
  227. package/src/worlds/minecraft/blueprint-plan.ts +915 -0
  228. package/src/worlds/minecraft/blueprint-registry.ts +566 -0
  229. package/src/worlds/minecraft/blueprint-repair.ts +236 -0
  230. package/src/worlds/minecraft/blueprint-resource.ts +272 -0
  231. package/src/worlds/minecraft/blueprint.ts +492 -0
  232. package/src/worlds/minecraft/body-lease.ts +340 -0
  233. package/src/worlds/minecraft/bridge.ts +749 -0
  234. package/src/worlds/minecraft/check.ts +651 -0
  235. package/src/worlds/minecraft/chests.ts +292 -0
  236. package/src/worlds/minecraft/client-launch.ts +272 -0
  237. package/src/worlds/minecraft/client-options.ts +85 -0
  238. package/src/worlds/minecraft/client-skins.ts +164 -0
  239. package/src/worlds/minecraft/client-window.ps1 +91 -0
  240. package/src/worlds/minecraft/client.ts +423 -0
  241. package/src/worlds/minecraft/combat-context.ts +109 -0
  242. package/src/worlds/minecraft/combat.ts +1552 -0
  243. package/src/worlds/minecraft/config.ts +499 -0
  244. package/src/worlds/minecraft/console/access.ts +218 -0
  245. package/src/worlds/minecraft/console/client.ts +244 -0
  246. package/src/worlds/minecraft/console/log.ts +110 -0
  247. package/src/worlds/minecraft/console/mount.ts +420 -0
  248. package/src/worlds/minecraft/console/skin.ts +257 -0
  249. package/src/worlds/minecraft/console/style.css +154 -0
  250. package/src/worlds/minecraft/console/world.ts +449 -0
  251. package/src/worlds/minecraft/deaths.ts +135 -0
  252. package/src/worlds/minecraft/definition.ts +18 -0
  253. package/src/worlds/minecraft/engine-child.ts +274 -0
  254. package/src/worlds/minecraft/engine-ipc.ts +98 -0
  255. package/src/worlds/minecraft/entity-facts.ts +132 -0
  256. package/src/worlds/minecraft/escape.ts +407 -0
  257. package/src/worlds/minecraft/executor.ts +14494 -0
  258. package/src/worlds/minecraft/explored.ts +156 -0
  259. package/src/worlds/minecraft/geometry.ts +205 -0
  260. package/src/worlds/minecraft/goal-plan.ts +647 -0
  261. package/src/worlds/minecraft/item-break.ts +126 -0
  262. package/src/worlds/minecraft/item-facts.ts +146 -0
  263. package/src/worlds/minecraft/item-pick.ts +67 -0
  264. package/src/worlds/minecraft/level-dat.ts +176 -0
  265. package/src/worlds/minecraft/log.ts +132 -0
  266. package/src/worlds/minecraft/mineflayer-fixes.ts +1180 -0
  267. package/src/worlds/minecraft/names.ts +377 -0
  268. package/src/worlds/minecraft/pathfinder-lib.d.ts +13 -0
  269. package/src/worlds/minecraft/pathfinder-perf.ts +796 -0
  270. package/src/worlds/minecraft/piglin.ts +49 -0
  271. package/src/worlds/minecraft/policy.ts +547 -0
  272. package/src/worlds/minecraft/precheck.ts +655 -0
  273. package/src/worlds/minecraft/proxy.ts +445 -0
  274. package/src/worlds/minecraft/ranged.ts +635 -0
  275. package/src/worlds/minecraft/readouts.ts +102 -0
  276. package/src/worlds/minecraft/round.ts +26 -0
  277. package/src/worlds/minecraft/search-observation.ts +92 -0
  278. package/src/worlds/minecraft/server-config.ts +467 -0
  279. package/src/worlds/minecraft/server.ts +658 -0
  280. package/src/worlds/minecraft/show.ts +84 -0
  281. package/src/worlds/minecraft/skills.ts +1763 -0
  282. package/src/worlds/minecraft/terrain.ts +1735 -0
  283. package/src/worlds/minecraft/window.ts +96 -0
  284. package/src/worlds/minecraft/works.ts +179 -0
  285. package/src/worlds/minecraft/world.ts +6438 -0
  286. package/src/worlds/qq/ENV_PROMPT.md +5 -0
  287. package/src/worlds/qq/config.ts +108 -0
  288. package/src/worlds/qq/console/client.ts +126 -0
  289. package/src/worlds/qq/console/events.ts +155 -0
  290. package/src/worlds/qq/console/gate.ts +232 -0
  291. package/src/worlds/qq/console/roster.ts +203 -0
  292. package/src/worlds/qq/console/style.css +65 -0
  293. package/src/worlds/qq/conversation.ts +28 -0
  294. package/src/worlds/qq/definition.ts +62 -0
  295. package/src/worlds/qq/driver.ts +375 -0
  296. package/src/worlds/qq/history-tools.ts +209 -0
  297. package/src/worlds/qq/normalize.ts +323 -0
  298. package/src/worlds/qq/qface-map.ts +282 -0
  299. package/src/worlds/qq/vision-prompt.ts +27 -0
  300. package/src/worlds/qq/vision.ts +642 -0
  301. package/src/worlds/qq/vlm.ts +142 -0
  302. package/src/worlds/qq/world.ts +1503 -0
  303. package/src/worlds/terminal/ENV_PROMPT.md +11 -0
  304. package/src/worlds/terminal/config.ts +9 -0
  305. package/src/worlds/terminal/definition.ts +14 -0
  306. package/src/worlds/terminal/world.ts +700 -0
  307. package/src/worlds/websearch/ENV_PROMPT.md +1 -0
  308. package/src/worlds/websearch/brave-client.ts +261 -0
  309. package/src/worlds/websearch/config.ts +75 -0
  310. package/src/worlds/websearch/definition.ts +20 -0
  311. package/src/worlds/websearch/world.ts +171 -0
@@ -0,0 +1,658 @@
1
+ /** 管理本地 Minecraft 服务器进程、stdin 指令及 TCP 连通性检测。停止时先发送 save-all flush 和 stop,超时后强制终止。 */
2
+ import { spawn, type ChildProcess } from 'node:child_process';
3
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { join } from 'node:path';
6
+ import { connect } from 'node:net';
7
+ import type { Logger } from '../../core/types.ts';
8
+ import {
9
+ loadProperties, readProperty, saveProperties, settingsFrom, writeProperty,
10
+ } from './server-config.ts';
11
+ import { readLevelDat } from './level-dat.ts';
12
+
13
+ export type MinecraftServerPhase = 'stopped' | 'starting' | 'running' | 'error';
14
+
15
+ export interface MinecraftServerState {
16
+ enabled: boolean;
17
+ phase: MinecraftServerPhase;
18
+ address: string;
19
+ detail: string | null;
20
+ pid: number | null;
21
+ /** TCP 检测只确认端口可连接。 */
22
+ reachable: boolean;
23
+ /** 当前托管进程所用目录;停机时为下一次启动的配置目录。 */
24
+ serverDir: string;
25
+ /** 路径配置是否齐备(不齐时前端提示去配置) */
26
+ configured: boolean;
27
+ }
28
+
29
+ interface MinecraftServerOptions {
30
+ enabled?: () => boolean;
31
+ /** 含 server.jar 的目录(启动前读取配置;'' = 未配置) */
32
+ serverDir: () => string;
33
+ /** java 路径;'' = 自动(serverDir 邻近 jdk → PATH 上的 java) */
34
+ javaPath: () => string;
35
+ /** JVM 参数(内存等),空格分隔 */
36
+ jvmArgs: () => string;
37
+ /** bot 要连的地址(托管只对本机地址有意义,探测也用它) */
38
+ host: () => string;
39
+ port: () => number;
40
+ log: Logger;
41
+ /** 测试注入:替换 spawn 目标 */
42
+ commandOverride?: { command: string; args: string[] };
43
+ healthIntervalMs?: number;
44
+ healthTimeoutMs?: number;
45
+ /** 测试注入:低频存档周期 */
46
+ autoSaveMs?: number;
47
+ onPhase?: (phase: MinecraftServerPhase, detail: string | null) => void;
48
+ /**
49
+ * 就绪之后回读一次实际难度的结果(每次 running 一次)。见 `queryDifficulty`。
50
+ * `difficulty: null` = 问了但没问出来(服务端没在期限内回话、或回的话认不出来)。
51
+ */
52
+ onDifficulty?: (fact: MinecraftDifficultyFact) => void;
53
+ /** 测试注入:难度回读的等待上限 */
54
+ difficultyTimeoutMs?: number;
55
+ }
56
+
57
+ /** 回读到的难度事实。全是读数,怎么措辞由消费方决定。 */
58
+ export interface MinecraftDifficultyFact {
59
+ /** 规范化后的难度名(peaceful/easy/normal/hard);认不出来 = null */
60
+ difficulty: 'peaceful' | 'easy' | 'normal' | 'hard' | null;
61
+ /** 服务端回的那一行原文(截断);没回话 = null */
62
+ raw: string | null;
63
+ /** 同一刻 server.properties 里的 difficulty=;读不到 = null */
64
+ properties: string | null;
65
+ }
66
+
67
+ const PROGRESS_RE = /Preparing (start region|spawn area)/;
68
+ /** 原版启动完成行:「Done (12.345s)! For help, type "help"」 */
69
+ const READY_RE = /\bDone \([\d.]+s\)!/;
70
+ /** 每见一次进度行给的宽限 */
71
+ const PROGRESS_GRACE_MS = 60_000;
72
+ /**
73
+ * `difficulty` 查询的回应行。
74
+ *
75
+ * 原版专用服务端控制台打的是 en_us(`commands.difficulty.query` = "The difficulty is %s"),
76
+ * 但服务端换语言包、或第三方核心自带中文的情况都有过,拿不准就两套都认:
77
+ *
78
+ * en 「The difficulty is Easy」 / 设置后「The difficulty has been set to Easy」
79
+ * zh 「难度为简单」/「游戏难度为简单」 / 设置后「难度已设置为简单」
80
+ *
81
+ * 行首还有日志前缀(`[12:34:56] [Server thread/INFO]: `),所以不锚定行首。
82
+ */
83
+ const DIFFICULTY_REPLY_RE =
84
+ /(?:The difficulty (?:is|has been set to)|(?:游戏)?难度(?:为|已设(?:置|定)为|已设为))\s*([A-Za-z_]+|和平|简单|普通|困难)/;
85
+ const DIFFICULTY_TIMEOUT_MS = 10_000;
86
+
87
+ function normalizeDifficulty(raw: string): MinecraftDifficultyFact['difficulty'] {
88
+ const zh: Record<string, MinecraftDifficultyFact['difficulty']> = {
89
+ 和平: 'peaceful', 简单: 'easy', 普通: 'normal', 困难: 'hard',
90
+ };
91
+ if (zh[raw]) return zh[raw];
92
+ const en = raw.toLowerCase();
93
+ return en === 'peaceful' || en === 'easy' || en === 'normal' || en === 'hard' ? en : null;
94
+ }
95
+
96
+ /** 从服务端一段 stdout 里认出难度回应;认不出返回 null。 */
97
+ export function parseDifficultyReply(text: string): { difficulty: MinecraftDifficultyFact['difficulty']; raw: string } | null {
98
+ const m = DIFFICULTY_REPLY_RE.exec(text);
99
+ if (!m) return null;
100
+ return { difficulty: normalizeDifficulty(m[1]), raw: m[0] };
101
+ }
102
+ /**
103
+ * 按主世界对齐各维度的 keepInventory;Paper/Bukkit 为每个维度独立存储规则。
104
+ * 其余 gamerule 保留各维度设置。
105
+ */
106
+ const WORLD_WIDE_GAME_RULES = ['keepInventory'] as const;
107
+
108
+ /** 待对齐的一个副维度:`id` 给 `execute in` 用,`dir` 是报给人看的目录名 */
109
+ interface DimensionRules {
110
+ id: 'minecraft:the_nether' | 'minecraft:the_end';
111
+ dir: string;
112
+ rules: Record<string, string> | null;
113
+ }
114
+
115
+ /** 以主世界保存的 GameRules 为目标,为其他维度生成修改指令。 */
116
+ export function planGameRuleAlignment(
117
+ overworld: Record<string, string> | null,
118
+ others: readonly DimensionRules[],
119
+ ): { commands: string[]; drift: string[] } {
120
+ const commands: string[] = [];
121
+ const drift: string[] = [];
122
+ for (const rule of WORLD_WIDE_GAME_RULES) {
123
+ const want = overworld?.[rule];
124
+ if (want === undefined) continue;
125
+ for (const other of others) {
126
+ if (other.rules === null) {
127
+ drift.push(`${other.dir} 的 ${rule} 读不到`);
128
+ continue;
129
+ }
130
+ const has = other.rules[rule];
131
+ if (has === want) continue;
132
+ drift.push(`${other.dir} 的 ${rule}=${has ?? '(没写)'},主世界是 ${want}`);
133
+ commands.push(`execute in ${other.id} run gamerule ${rule} ${want}`);
134
+ }
135
+ }
136
+ return { commands, drift };
137
+ }
138
+
139
+ /** 定期发送 save-all flush。 */
140
+ const AUTO_SAVE_MS = 90_000;
141
+ /** 停止指令后等待的毫秒数;超时强制终止进程。 */
142
+ const GRACEFUL_EXIT_MS = 15_000;
143
+ const HARD_SIGNALS: NodeJS.Signals[] = process.platform === 'win32'
144
+ ? ['SIGHUP', 'SIGBREAK']
145
+ : ['SIGHUP'];
146
+
147
+ /** java 可执行文件的文件名。Windows 上带 `.exe`,别处不带。 */
148
+ export const JAVA_BINARY = process.platform === 'win32' ? 'java.exe' : 'java';
149
+
150
+ /** serverDir 邻近的便携 JDK:<serverDir>/../jdk/<任意版本>/bin/<java 可执行文件> */
151
+ function findAdjacentJava(serverDir: string): string | null {
152
+ const jdkRoot = join(serverDir, '..', 'jdk');
153
+ if (!existsSync(jdkRoot)) return null;
154
+ for (const entry of readdirSync(jdkRoot)) {
155
+ const candidate = join(jdkRoot, entry, 'bin', JAVA_BINARY);
156
+ if (existsSync(candidate)) return candidate;
157
+ }
158
+ return null;
159
+ }
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // 世界身份(realm):存档目录里的 cortico-realm.json
163
+ // ---------------------------------------------------------------------------
164
+
165
+ /** 存档身份写入存档目录的标记文件;目录改名或复制保留该身份。 */
166
+ export const REALM_MARKER_FILE = 'cortico-realm.json';
167
+
168
+ /** 落在存档目录里的那份 JSON。 */
169
+ interface RealmMarker {
170
+ /** 机器层的稳定键:首次纳管时随机发一次,之后只读不改 */
171
+ uuid: string;
172
+ /** 首次纳管时的目录名;当前存档名取自 server.properties 的 level-name。 */
173
+ levelName: string;
174
+ /** 首次纳管时刻(ISO) */
175
+ createdAt: string;
176
+ }
177
+
178
+ /** 本地存档优先使用标记文件中的 UUID;无法读取或写入时使用地址和 level-name 生成临时键。外部服务器不在此推断存档身份。 */
179
+ interface MinecraftRealm {
180
+ /** 受管世界的稳定 uuid;marker 读写不成时为 null */
181
+ uuid: string | null;
182
+ /** 当下的存档目录名(server.properties 的 level-name);对她只说这个 */
183
+ levelName: string;
184
+ }
185
+
186
+ function realmMarkerPath(worldDir: string): string {
187
+ return join(worldDir, REALM_MARKER_FILE);
188
+ }
189
+
190
+ export function readRealmMarker(worldDir: string): RealmMarker | null {
191
+ const file = realmMarkerPath(worldDir);
192
+ if (!existsSync(file)) return null;
193
+ try {
194
+ const parsed: unknown = JSON.parse(readFileSync(file, 'utf8'));
195
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return null;
196
+ const { uuid, levelName, createdAt } = parsed as Record<string, unknown>;
197
+ if (typeof uuid !== 'string' || !uuid.trim()) return null;
198
+ return {
199
+ uuid,
200
+ levelName: typeof levelName === 'string' ? levelName : '',
201
+ createdAt: typeof createdAt === 'string' ? createdAt : '',
202
+ };
203
+ } catch {
204
+ return null;
205
+ }
206
+ }
207
+
208
+ /** 目录中已有标记时复用;否则创建标记。写入失败返回临时身份。 */
209
+ function ensureRealmMarker(worldDir: string, levelName: string): RealmMarker {
210
+ const existing = readRealmMarker(worldDir);
211
+ if (existing) return existing;
212
+ const marker: RealmMarker = { uuid: randomUUID(), levelName, createdAt: new Date().toISOString() };
213
+ mkdirSync(worldDir, { recursive: true });
214
+ writeFileSync(realmMarkerPath(worldDir), `${JSON.stringify(marker, null, 2)}\n`, 'utf8');
215
+ return marker;
216
+ }
217
+
218
+ export class MinecraftServerManager {
219
+ private phase: MinecraftServerPhase = 'stopped';
220
+ private detail: string | null = null;
221
+ private proc: ChildProcess | null = null;
222
+ private healthTimer: ReturnType<typeof setInterval> | null = null;
223
+ private logTail = '';
224
+
225
+ private saveTimer: ReturnType<typeof setInterval> | null = null;
226
+ private healthDeadline = 0;
227
+ /** 已装上的硬信号兜底监听;没有托管进程时为 null(不占监听位) */
228
+ private hardSignalHook: (() => void) | null = null;
229
+ private realmCache: { worldDir: string; realm: MinecraftRealm } | null = null;
230
+ /** 托管进程使用的目录;运行期间配置改动留到下次启动。 */
231
+ private activeServerDir: string | null = null;
232
+ private realmWarned = false;
233
+ /** 正在穿过启动探针的生命周期代次;stop 会使该代次立即失效。 */
234
+ private startingGeneration: number | null = null;
235
+ private lifecycleGeneration = 0;
236
+ private difficultyWait: { proc: ChildProcess; timer: ReturnType<typeof setTimeout> } | null = null;
237
+
238
+ constructor(private readonly opts: MinecraftServerOptions) {}
239
+
240
+ get address(): string {
241
+ return `${this.opts.host()}:${this.opts.port()}`;
242
+ }
243
+
244
+ directory(): string {
245
+ return this.activeServerDir ?? this.opts.serverDir();
246
+ }
247
+
248
+ async state(): Promise<MinecraftServerState> {
249
+ const serverDir = this.directory();
250
+ const enabled = this.opts.enabled?.() ?? true;
251
+ return {
252
+ enabled,
253
+ phase: this.phase,
254
+ address: this.address,
255
+ detail: enabled ? this.detail : '受管服务器开关已关闭',
256
+ pid: this.proc?.pid ?? null,
257
+ reachable: enabled ? await this.probe() : false,
258
+ serverDir,
259
+ configured: serverDir !== '' && existsSync(join(serverDir, 'server.jar')),
260
+ };
261
+ }
262
+
263
+ /** 首次查询本地存档身份时可能创建标记文件。 */
264
+ realm(): MinecraftRealm | null {
265
+ const serverDir = this.directory();
266
+ if (!serverDir || !existsSync(serverDir)) return null;
267
+ let levelName: string;
268
+ try {
269
+ levelName = settingsFrom(loadProperties(serverDir)).levelName;
270
+ } catch (err) {
271
+ this.warnRealmOnce(`读 server.properties 认不出存档名: ${String(err)}`);
272
+ return null;
273
+ }
274
+ const worldDir = join(serverDir, levelName);
275
+ const hit = this.realmCache;
276
+ if (hit && hit.worldDir === worldDir && existsSync(realmMarkerPath(worldDir))) return hit.realm;
277
+ let uuid: string | null = null;
278
+ try {
279
+ uuid = ensureRealmMarker(worldDir, levelName).uuid;
280
+ } catch (err) {
281
+ this.warnRealmOnce(`写不进 ${join(levelName, REALM_MARKER_FILE)}(${String(err)});这个世界只能按弱身份算`);
282
+ }
283
+ const realm: MinecraftRealm = { uuid, levelName };
284
+ this.realmCache = uuid ? { worldDir, realm } : null;
285
+ return realm;
286
+ }
287
+
288
+ private warnRealmOnce(detail: string): void {
289
+ if (this.realmWarned) return;
290
+ this.realmWarned = true;
291
+ this.opts.log.warn(`MC 世界身份不可用: ${detail}`);
292
+ }
293
+
294
+ async start(): Promise<MinecraftServerState> {
295
+ if (!(this.opts.enabled?.() ?? true)) return this.state();
296
+ if (this.startingGeneration !== null || this.phase === 'starting' || this.phase === 'running') {
297
+ return this.state();
298
+ }
299
+ const generation = ++this.lifecycleGeneration;
300
+ this.startingGeneration = generation;
301
+ try {
302
+ return await this.spawnServer(generation);
303
+ } finally {
304
+ if (this.startingGeneration === generation) this.startingGeneration = null;
305
+ }
306
+ }
307
+
308
+ private async spawnServer(generation: number): Promise<MinecraftServerState> {
309
+ if (await this.probe()) {
310
+ if (generation !== this.lifecycleGeneration || !(this.opts.enabled?.() ?? true)) return this.state();
311
+ this.detail = '端口已可连接,未启动托管进程';
312
+ return this.state();
313
+ }
314
+ if (generation !== this.lifecycleGeneration || !(this.opts.enabled?.() ?? true)) return this.state();
315
+ const launch = this.resolveLaunch();
316
+ if ('error' in launch) {
317
+ this.setPhase('error', launch.error);
318
+ return this.state();
319
+ }
320
+ this.activeServerDir = launch.cwd ?? this.opts.serverDir();
321
+ const portFix = this.opts.commandOverride ? null : this.alignServerPort();
322
+ if (portFix) this.opts.log.warn(`MC 服务器端口纠偏: ${portFix}`);
323
+ const realm = this.realm();
324
+ if (realm) {
325
+ this.opts.log.info(`MC 世界纳管: 存档「${realm.levelName}」${realm.uuid ? '' : '(无身份标记,按弱身份算)'}`);
326
+ }
327
+ this.logTail = '';
328
+ const proc = spawn(launch.command, launch.args, {
329
+ cwd: launch.cwd,
330
+ stdio: ['pipe', 'pipe', 'pipe'],
331
+ windowsHide: true,
332
+ });
333
+ this.proc = proc;
334
+ // 对端退出后再写 stdin,EPIPE 从流上异步冒出来,write 外面的 try/catch 接不住。
335
+ proc.stdin?.on('error', (err) => this.opts.log.debug('MC 服务器 stdin 写入失败', { err }));
336
+ this.setPhase('starting', portFix ? `世界加载中(${portFix})` : '世界加载中');
337
+ const tail = (chunk: Buffer) => {
338
+ const text = chunk.toString();
339
+ this.logTail = (this.logTail + text).slice(-2000);
340
+ this.consumeDifficultyReply(proc, text);
341
+ if (this.proc !== proc || this.phase !== 'starting') return;
342
+ if (PROGRESS_RE.test(text)) this.healthDeadline = Date.now() + PROGRESS_GRACE_MS;
343
+ if (READY_RE.test(text)) {
344
+ this.setPhase('running', null);
345
+ this.clearHealthTimer();
346
+ this.beginAutoSave();
347
+ this.queryDifficulty();
348
+ this.alignGameRules();
349
+ this.opts.log.info(`MC 服务器就绪 ${this.address}(服务端自报 Done)`);
350
+ }
351
+ };
352
+ proc.stdout?.on('data', tail);
353
+ proc.stderr?.on('data', tail);
354
+ proc.on('error', (err) => {
355
+ if (this.proc !== proc) return;
356
+ this.fail(`进程启动失败: ${err.message}`);
357
+ });
358
+ proc.on('exit', (code) => {
359
+ if (this.proc !== proc) return;
360
+ this.proc = null;
361
+ this.clearDifficultyWait();
362
+ this.removeHardSignalHook();
363
+ if (this.phase === 'stopped') return;
364
+ const detail = `进程退出 code=${code};日志尾部: ${this.logTail.slice(-400)}`;
365
+ this.finish(code === 0 ? 'stopped' : 'error', detail);
366
+ });
367
+ this.installHardSignalHook();
368
+ this.opts.log.info(`MC 服务器启动中 pid=${proc.pid} ${this.address}`);
369
+ this.beginHealthPolling();
370
+ return this.state();
371
+ }
372
+
373
+ async stop(): Promise<MinecraftServerState> {
374
+ ++this.lifecycleGeneration;
375
+ this.startingGeneration = null;
376
+ this.clearHealthTimer();
377
+ this.clearSaveTimer();
378
+ this.clearDifficultyWait();
379
+ this.removeHardSignalHook();
380
+ this.setPhase('stopped', null);
381
+ const proc = this.proc;
382
+ this.proc = null;
383
+ if (proc && proc.exitCode === null) {
384
+ await this.gracefulKill(proc);
385
+ this.opts.log.info('MC 服务器已停止');
386
+ }
387
+ this.activeServerDir = null;
388
+ return this.state();
389
+ }
390
+
391
+ /** 仅在托管进程存在时监听终止信号,收到信号后尝试向 stdin 写入存档和停止指令。 */
392
+ private installHardSignalHook(): void {
393
+ if (this.hardSignalHook) return;
394
+ const hook = (): void => this.saveOnHardSignal();
395
+ for (const sig of HARD_SIGNALS) process.on(sig, hook);
396
+ this.hardSignalHook = hook;
397
+ }
398
+
399
+ private removeHardSignalHook(): void {
400
+ const hook = this.hardSignalHook;
401
+ if (!hook) return;
402
+ this.hardSignalHook = null;
403
+ for (const sig of HARD_SIGNALS) process.removeListener(sig, hook);
404
+ }
405
+
406
+ /** 信号回调只写入指令,不等待退出。 */
407
+ private saveOnHardSignal(): void {
408
+ const proc = this.proc;
409
+ if (!proc || proc.exitCode !== null) return;
410
+ this.opts.log.warn('收到硬关信号:先给 MC 服务器补一次 save-all + stop(兜底,正门是控制台关机键)');
411
+ this.command('save-all flush');
412
+ this.command('stop');
413
+ }
414
+
415
+ /** 发送 save-all flush 和 stop 后等待进程退出,超时强制终止;未核验存档结果。 */
416
+ private async gracefulKill(proc: ChildProcess): Promise<void> {
417
+ try {
418
+ proc.stdin?.write('save-all flush\n');
419
+ proc.stdin?.write('stop\n');
420
+ } catch {
421
+ /* stdin 已关 */
422
+ }
423
+ await new Promise<void>((resolve) => {
424
+ const force = setTimeout(() => {
425
+ try { proc.kill('SIGKILL'); } catch { /* 已退出 */ }
426
+ resolve();
427
+ }, GRACEFUL_EXIT_MS);
428
+ proc.once('exit', () => {
429
+ clearTimeout(force);
430
+ resolve();
431
+ });
432
+ });
433
+ }
434
+
435
+
436
+ private beginAutoSave(): void {
437
+ this.clearSaveTimer();
438
+ this.saveTimer = setInterval(() => {
439
+ if (this.phase !== 'running') { this.clearSaveTimer(); return; }
440
+ this.command('save-all');
441
+ }, this.opts.autoSaveMs ?? AUTO_SAVE_MS);
442
+ this.saveTimer.unref?.();
443
+ }
444
+
445
+ private clearSaveTimer(): void {
446
+ if (this.saveTimer) clearInterval(this.saveTimer);
447
+ this.saveTimer = null;
448
+ }
449
+
450
+ /**
451
+ * 服务端就绪后用无参 difficulty 查询启动时实际难度。
452
+ * 仅自管服务器有 stdin 可查询;已发送而未收到答复时报告,运行期变更不在此监听。
453
+ */
454
+ private queryDifficulty(): void {
455
+ this.clearDifficultyWait();
456
+ const proc = this.proc;
457
+ if (!proc || !this.command('difficulty')) return;
458
+ const timer = setTimeout(() => {
459
+ if (this.difficultyWait?.proc !== proc) return;
460
+ this.difficultyWait = null;
461
+ this.opts.log.warn('MC 难度回读:服务端没有在期限内回话');
462
+ this.reportDifficulty({ difficulty: null, raw: null, properties: this.propertiesDifficulty() });
463
+ }, this.opts.difficultyTimeoutMs ?? DIFFICULTY_TIMEOUT_MS);
464
+ timer.unref?.();
465
+ this.difficultyWait = { proc, timer };
466
+ }
467
+
468
+ private consumeDifficultyReply(proc: ChildProcess, text: string): void {
469
+ if (this.difficultyWait?.proc !== proc) return;
470
+ const hit = parseDifficultyReply(text);
471
+ if (!hit) return;
472
+ this.clearDifficultyWait();
473
+ this.reportDifficulty({
474
+ difficulty: hit.difficulty,
475
+ raw: hit.raw,
476
+ properties: this.propertiesDifficulty(),
477
+ });
478
+ }
479
+
480
+ private reportDifficulty(fact: MinecraftDifficultyFact): void {
481
+ this.opts.log.info(
482
+ `MC 难度回读: ${fact.difficulty ?? '没认出来'}` +
483
+ `${fact.raw ? `(服务端原话「${fact.raw}」)` : ''}` +
484
+ `${fact.properties ? `;server.properties 写的是 ${fact.properties}` : ''}`,
485
+ );
486
+ this.opts.onDifficulty?.(fact);
487
+ }
488
+
489
+ private clearDifficultyWait(): void {
490
+ if (this.difficultyWait) clearTimeout(this.difficultyWait.timer);
491
+ this.difficultyWait = null;
492
+ }
493
+
494
+ /** 读取配置中的难度;无法读取返回 null。 */
495
+ private propertiesDifficulty(): string | null {
496
+ const dir = this.directory();
497
+ if (!dir) return null;
498
+ try {
499
+ const value = readProperty(loadProperties(dir), 'difficulty');
500
+ return value?.trim() || null;
501
+ } catch {
502
+ return null;
503
+ }
504
+ }
505
+
506
+ /** 使用 level.dat 中最近保存的主世界 GameRules;运行中尚未保存的更改不在此快照中。 */
507
+ private alignGameRules(): void {
508
+ const serverDir = this.directory();
509
+ if (!serverDir || !existsSync(serverDir)) return;
510
+ let levelName: string;
511
+ try {
512
+ levelName = settingsFrom(loadProperties(serverDir)).levelName;
513
+ } catch {
514
+ return;
515
+ }
516
+ const rulesOf = (dir: string): Record<string, string> | null =>
517
+ readLevelDat(join(serverDir, dir, 'level.dat'))?.gameRules ?? null;
518
+ const plan = planGameRuleAlignment(rulesOf(levelName), [
519
+ { id: 'minecraft:the_nether', dir: `${levelName}_nether`, rules: rulesOf(`${levelName}_nether`) },
520
+ { id: 'minecraft:the_end', dir: `${levelName}_the_end`, rules: rulesOf(`${levelName}_the_end`) },
521
+ ]);
522
+ if (plan.drift.length === 0) return;
523
+ const sent = plan.commands.length > 0 && plan.commands.every((line) => this.command(line));
524
+ this.opts.log.warn(
525
+ `MC gamerule 各维度对不上:${plan.drift.join(';')}`
526
+ + (plan.commands.length === 0
527
+ ? ''
528
+ : sent
529
+ ? `;已发送 ${plan.commands.length} 条 gamerule 修改指令`
530
+ : `;无法写入 stdin,请手动执行:${plan.commands.join(' / ')}`),
531
+ );
532
+ }
533
+
534
+ /** 仅向受托管服务器的 stdin 写入指令;外部服务器返回 false。 */
535
+ command(line: string): boolean {
536
+ const proc = this.proc;
537
+ if (!proc || proc.exitCode !== null || !proc.stdin?.writable) return false;
538
+ try {
539
+ proc.stdin.write(`${line}\n`);
540
+ return true;
541
+ } catch {
542
+ return false;
543
+ }
544
+ }
545
+
546
+ probe(): Promise<boolean> {
547
+ return new Promise((resolve) => {
548
+ const sock = connect({ host: this.opts.host(), port: this.opts.port(), timeout: 1500 });
549
+ const done = (ok: boolean) => {
550
+ sock.destroy();
551
+ resolve(ok);
552
+ };
553
+ sock.once('connect', () => done(true));
554
+ sock.once('timeout', () => done(false));
555
+ sock.once('error', () => done(false));
556
+ });
557
+ }
558
+
559
+ private resolveLaunch():
560
+ | { command: string; args: string[]; cwd: string | undefined }
561
+ | { error: string } {
562
+ if (this.opts.commandOverride) {
563
+ return { command: this.opts.commandOverride.command, args: this.opts.commandOverride.args, cwd: undefined };
564
+ }
565
+ const serverDir = this.opts.serverDir();
566
+ if (!serverDir) {
567
+ return { error: '未配置服务器目录:在本 World 配置里填 worlds.minecraft.local.serverDir(含 server.jar 的目录)' };
568
+ }
569
+ const jar = join(serverDir, 'server.jar');
570
+ if (!existsSync(jar)) return { error: `找不到 ${jar};确认 worlds.minecraft.local.serverDir 配置` };
571
+ let java = this.opts.javaPath();
572
+ if (!java) {
573
+ java = findAdjacentJava(serverDir) ?? 'java';
574
+ } else if (!existsSync(java)) {
575
+ return { error: `找不到 java: ${java};确认 worlds.minecraft.local.javaPath 配置` };
576
+ }
577
+ return {
578
+ command: java,
579
+ args: [...this.opts.jvmArgs().split(/\s+/).filter(Boolean), '-jar', 'server.jar', 'nogui'],
580
+ cwd: serverDir,
581
+ };
582
+ }
583
+
584
+ /** 启动前将 server.properties 的 server-port 写为配置端口。 */
585
+ private alignServerPort(): string | null {
586
+ const serverDir = this.directory();
587
+ if (!serverDir) return null;
588
+ try {
589
+ const lines = loadProperties(serverDir);
590
+ if (lines.length === 0) return null;
591
+ const want = String(this.opts.port());
592
+ const got = (readProperty(lines, 'server-port') ?? '').trim();
593
+ if (got === want) return null;
594
+ saveProperties(serverDir, writeProperty(lines, 'server-port', want));
595
+ return `server.properties 里的 server-port 是 ${got || '(空)'},已改回 ${want}`;
596
+ } catch (err) {
597
+ this.opts.log.warn(`读写 server.properties 失败: ${String(err)}`);
598
+ return null;
599
+ }
600
+ }
601
+
602
+ private beginHealthPolling(): void {
603
+ this.clearHealthTimer();
604
+ const interval = this.opts.healthIntervalMs ?? 2_000;
605
+ const proc = this.proc;
606
+ this.healthDeadline = Date.now() + (this.opts.healthTimeoutMs ?? 120_000);
607
+ this.healthTimer = setInterval(async () => {
608
+ if (this.phase !== 'starting') {
609
+ this.clearHealthTimer();
610
+ return;
611
+ }
612
+ const reachable = await this.probe();
613
+ if (this.phase !== 'starting' || this.proc !== proc) return;
614
+ if (reachable) {
615
+ this.setPhase('running', null);
616
+ this.clearHealthTimer();
617
+ this.beginAutoSave();
618
+ this.queryDifficulty();
619
+ this.alignGameRules();
620
+ this.opts.log.info(`MC 服务器就绪 ${this.address}`);
621
+ return;
622
+ }
623
+ if (Date.now() > this.healthDeadline) {
624
+ const orphan = this.proc;
625
+ this.proc = null;
626
+ this.fail('启动超时(世界生成过久或端口不对)');
627
+ if (orphan) void this.gracefulKill(orphan);
628
+ }
629
+ }, interval);
630
+ }
631
+
632
+ private clearHealthTimer(): void {
633
+ if (this.healthTimer) clearInterval(this.healthTimer);
634
+ this.healthTimer = null;
635
+ }
636
+
637
+ private fail(detail: string): void {
638
+ this.finish('error', detail);
639
+ }
640
+
641
+ /** 清理进程引用、定时器及待处理的难度查询。 */
642
+ private finish(phase: 'error' | 'stopped', detail: string): void {
643
+ this.clearHealthTimer();
644
+ this.clearSaveTimer();
645
+ this.removeHardSignalHook();
646
+ if (this.proc === null) this.activeServerDir = null;
647
+ this.setPhase(phase, detail);
648
+ if (phase === 'error') this.opts.log.error(`MC 服务器异常: ${detail}`);
649
+ else this.opts.log.info(`MC 服务器已退出: ${detail}`);
650
+ }
651
+
652
+ private setPhase(next: MinecraftServerPhase, detail: string | null): void {
653
+ const changed = this.phase !== next;
654
+ this.phase = next;
655
+ this.detail = detail;
656
+ if (changed) this.opts.onPhase?.(next, detail);
657
+ }
658
+ }