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,292 @@
1
+ /**
2
+ * 按维度与坐标保存容器上次读取的内容,供容器选择、快照和蓝图材料统计使用。
3
+ * 炉子另外记录槽位与预计完成时间;工作站记录放置时刻。
4
+ */
5
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
6
+ import { dirname } from 'node:path';
7
+ import type { ItemStack } from './terrain.ts';
8
+
9
+ /** 炉子族方块名(原版三种炉子;槽位名照原版 input/fuel/output) */
10
+ export const FURNACE_BLOCKS = ['furnace', 'smoker', 'blast_furnace'] as const;
11
+
12
+ /** 箱子族方块名(能当仓库用的那些);工作台、炉子族、酿造台都不在内 */
13
+ export const CHEST_BLOCKS = ['chest', 'trapped_chest', 'barrel', 'ender_chest'] as const;
14
+
15
+ const CHEST_KINDS = new Set<string>(CHEST_BLOCKS);
16
+
17
+ /** 炉子槽位与预计完成时间的上次读数;未开窗时不持续同步。 */
18
+ interface FurnaceState {
19
+ input: ItemStack | null;
20
+ fuel: ItemStack | null;
21
+ output: ItemStack | null;
22
+ /** 下料(或最近一次读到槽位)的时刻 */
23
+ loadedAt: number;
24
+ /** 预计完成时间;null 表示未记录预计时间。 */
25
+ expectedDoneAt: number | null;
26
+ /** 到期事件已经发过,不重复发 */
27
+ notified?: boolean;
28
+ }
29
+
30
+ export interface ChestRecord {
31
+ x: number;
32
+ y: number;
33
+ z: number;
34
+ dimension: string;
35
+ items: ItemStack[];
36
+ usedSlots: number;
37
+ slots: number;
38
+ /** 方块名;旧档案没有这个字段,读档按 chest 兜底 */
39
+ name?: string;
40
+ /** 我们自己放下的时刻;捡现成的没有 */
41
+ placedAt?: number;
42
+ /** 炉子族的三槽位;箱子族没有 */
43
+ furnace?: FurnaceState;
44
+ }
45
+
46
+ /** 缺少 name 的记录按 chest 处理。 */
47
+ export function chestBlockName(rec: ChestRecord): string {
48
+ return rec.name ?? 'chest';
49
+ }
50
+
51
+ /** 类别名按整词或 `_query` 后缀匹配,与 collect 共用此规则。 */
52
+ export function matchItemName(query: string, name: string): boolean {
53
+ return name === query || name.endsWith(`_${query}`);
54
+ }
55
+
56
+ /** 物品和方块名字的注册表接口;缺失时回退到后缀匹配。 */
57
+ export interface NameRegistry {
58
+ itemsByName?: Record<string, unknown>;
59
+ blocksByName?: Record<string, unknown>;
60
+ }
61
+
62
+ /** 名字是否存在于当前注册表。 */
63
+ export function isRealId(reg: NameRegistry | null | undefined, name: string): boolean {
64
+ return reg != null
65
+ && (reg.itemsByName?.[name] !== undefined || reg.blocksByName?.[name] !== undefined);
66
+ }
67
+
68
+ /**
69
+ * 注册表中存在的物品 id 只作精确匹配;其余查询按 `_query` 后缀匹配类别。
70
+ * 材料选择与放置核对共用此规则。
71
+ */
72
+ export function matchMaterialName(
73
+ reg: NameRegistry | null | undefined,
74
+ query: string,
75
+ name: string,
76
+ ): boolean {
77
+ if (name === query) return true;
78
+ if (isRealId(reg, query)) return false;
79
+ return name.endsWith(`_${query}`);
80
+ }
81
+
82
+ function chestKey(dimension: string, p: { x: number; y: number; z: number }): string {
83
+ return `${dimension}:${p.x},${p.y},${p.z}`;
84
+ }
85
+
86
+ /** 容量包括空槽和同种物品的未满栈;stackMax 必须取自物品注册表。 */
87
+ export function hasRoom(rec: ChestRecord, item: string, stackMax: number): boolean {
88
+ if (rec.usedSlots < rec.slots) return true;
89
+ return rec.items.some((i) => matchItemName(item, i.name) && i.count % stackMax !== 0);
90
+ }
91
+
92
+ export function hasItem(rec: ChestRecord, item: string): boolean {
93
+ return rec.items.some((i) => matchItemName(item, i.name) && i.count > 0);
94
+ }
95
+
96
+ export class ChestBook {
97
+ private readonly map = new Map<string, ChestRecord>();
98
+
99
+ constructor(private readonly file: string | null) {
100
+ this.load();
101
+ }
102
+
103
+ get(dimension: string, p: { x: number; y: number; z: number }): ChestRecord | undefined {
104
+ return this.map.get(chestKey(dimension, p));
105
+ }
106
+
107
+ remember(
108
+ dimension: string,
109
+ p: { x: number; y: number; z: number },
110
+ items: ItemStack[],
111
+ usedSlots: number,
112
+ slots: number,
113
+ ): void {
114
+ /** 刷新内容时保留原 placedAt。 */
115
+ const prev = this.map.get(chestKey(dimension, p));
116
+ const rec: ChestRecord = {
117
+ x: p.x, y: p.y, z: p.z, dimension, items, usedSlots, slots,
118
+ ...(prev?.name !== undefined ? { name: prev.name } : {}),
119
+ ...(prev?.placedAt !== undefined ? { placedAt: prev.placedAt } : {}),
120
+ };
121
+ this.map.set(chestKey(dimension, p), rec);
122
+ this.save();
123
+ }
124
+
125
+ /** 更新方块名及可选的放置时间,保留原内容记录。 */
126
+ rememberStation(
127
+ dimension: string,
128
+ p: { x: number; y: number; z: number },
129
+ name: string,
130
+ placedAt?: number,
131
+ ): void {
132
+ const k = chestKey(dimension, p);
133
+ const prev = this.map.get(k);
134
+ this.map.set(k, {
135
+ x: p.x, y: p.y, z: p.z, dimension,
136
+ items: prev?.items ?? [],
137
+ usedSlots: prev?.usedSlots ?? 0,
138
+ slots: prev?.slots ?? 0,
139
+ name,
140
+ ...(placedAt !== undefined ? { placedAt } : prev?.placedAt !== undefined ? { placedAt: prev.placedAt } : {}),
141
+ ...(prev?.furnace !== undefined ? { furnace: prev.furnace } : {}),
142
+ });
143
+ this.save();
144
+ }
145
+
146
+ /** expectedDoneAt 由调用方计算;槽位读数同步到 items。 */
147
+ rememberFurnace(
148
+ dimension: string,
149
+ p: { x: number; y: number; z: number },
150
+ name: string,
151
+ state: { input: ItemStack | null; fuel: ItemStack | null; output: ItemStack | null },
152
+ loadedAt: number,
153
+ expectedDoneAt: number | null,
154
+ ): void {
155
+ const k = chestKey(dimension, p);
156
+ const prev = this.map.get(k);
157
+ const items = [state.input, state.fuel, state.output].filter((s): s is ItemStack => s !== null);
158
+ this.map.set(k, {
159
+ x: p.x, y: p.y, z: p.z, dimension,
160
+ items, usedSlots: items.length, slots: 3,
161
+ name,
162
+ ...(prev?.placedAt !== undefined ? { placedAt: prev.placedAt } : {}),
163
+ furnace: { ...state, loadedAt, expectedDoneAt },
164
+ });
165
+ this.save();
166
+ }
167
+
168
+ due(now: number): ChestRecord[] {
169
+ const out: ChestRecord[] = [];
170
+ for (const rec of this.map.values()) {
171
+ const f = rec.furnace;
172
+ if (f && f.expectedDoneAt !== null && f.expectedDoneAt <= now && !f.notified) out.push(rec);
173
+ }
174
+ return out;
175
+ }
176
+
177
+ markNotified(dimension: string, p: { x: number; y: number; z: number }): void {
178
+ const rec = this.map.get(chestKey(dimension, p));
179
+ if (!rec?.furnace || rec.furnace.notified) return;
180
+ rec.furnace.notified = true;
181
+ this.save();
182
+ }
183
+
184
+ loadedFurnaces(dimension: string): ChestRecord[] {
185
+ const out: ChestRecord[] = [];
186
+ for (const rec of this.map.values()) {
187
+ const f = rec.furnace;
188
+ if (rec.dimension !== dimension || !f) continue;
189
+ if (f.input || f.output) out.push(rec);
190
+ }
191
+ return out;
192
+ }
193
+
194
+ /**
195
+ * 当前维度账上的箱子族,不含炉子族与工作台。
196
+ * 记录表示上次看见的状态,调用方须按历史读数措辞。
197
+ */
198
+ chestsIn(dimension: string): ChestRecord[] {
199
+ const out: ChestRecord[] = [];
200
+ for (const rec of this.map.values()) {
201
+ if (rec.dimension !== dimension || rec.furnace) continue;
202
+ if (CHEST_KINDS.has(chestBlockName(rec))) out.push(rec);
203
+ }
204
+ return out;
205
+ }
206
+
207
+ forget(dimension: string, p: { x: number; y: number; z: number }): boolean {
208
+ const removed = this.map.delete(chestKey(dimension, p));
209
+ if (removed) this.save();
210
+ return removed;
211
+ }
212
+
213
+ inCells(dimension: string, cells: ReadonlyArray<{ x: number; y: number; z: number }>): ChestRecord[] {
214
+ if (this.map.size === 0) return [];
215
+ const keys = new Set(cells.map((c) => chestKey(dimension, c)));
216
+ const out: ChestRecord[] = [];
217
+ for (const [k, rec] of this.map) {
218
+ if (keys.has(k)) out.push(rec);
219
+ }
220
+ return out;
221
+ }
222
+
223
+ /** 汇总当前维度容器的上次观测数量,供蓝图材料统计使用。 */
224
+ tally(dimension: string): Record<string, number> {
225
+ const out: Record<string, number> = {};
226
+ for (const rec of this.map.values()) {
227
+ if (rec.dimension !== dimension) continue;
228
+ for (const it of rec.items) out[it.name] = (out[it.name] ?? 0) + it.count;
229
+ }
230
+ return out;
231
+ }
232
+
233
+ stat(): string {
234
+ const n = this.map.size;
235
+ if (!this.file || !existsSync(this.file)) return n === 0 ? '(无文件)' : `${n} 个(未落盘)`;
236
+ const kb = (this.fileSize() / 1024).toFixed(1);
237
+ return `${n} 个 / ${kb}KB`;
238
+ }
239
+
240
+ clear(): string {
241
+ const n = this.map.size;
242
+ this.map.clear();
243
+ if (this.file) {
244
+ mkdirSync(dirname(this.file), { recursive: true });
245
+ writeFileSync(this.file, '{}\n', 'utf8');
246
+ }
247
+ return n === 0 ? '容器账本是空的' : `容器账本已清空(${n} 个)`;
248
+ }
249
+
250
+ private fileSize(): number {
251
+ return statSync(this.file!).size;
252
+ }
253
+
254
+ private load(): void {
255
+ if (!this.file || !existsSync(this.file)) return;
256
+ const raw = readFileSync(this.file, 'utf8').trim();
257
+ if (!raw) return;
258
+ const data = JSON.parse(raw) as Record<string, ChestRecord>;
259
+ for (const [k, v] of Object.entries(data)) {
260
+ if (typeof v?.x === 'number' && typeof v.y === 'number' && typeof v.z === 'number') {
261
+ const f = v.furnace;
262
+ this.map.set(k, {
263
+ x: v.x, y: v.y, z: v.z,
264
+ dimension: typeof v.dimension === 'string' ? v.dimension : 'overworld',
265
+ items: Array.isArray(v.items) ? v.items : [],
266
+ usedSlots: typeof v.usedSlots === 'number' ? v.usedSlots : 0,
267
+ slots: typeof v.slots === 'number' ? v.slots : 27,
268
+ ...(typeof v.name === 'string' ? { name: v.name } : {}),
269
+ ...(typeof v.placedAt === 'number' ? { placedAt: v.placedAt } : {}),
270
+ ...(f && typeof f === 'object' ? {
271
+ furnace: {
272
+ input: f.input ?? null,
273
+ fuel: f.fuel ?? null,
274
+ output: f.output ?? null,
275
+ loadedAt: typeof f.loadedAt === 'number' ? f.loadedAt : 0,
276
+ expectedDoneAt: typeof f.expectedDoneAt === 'number' ? f.expectedDoneAt : null,
277
+ ...(f.notified === true ? { notified: true } : {}),
278
+ },
279
+ } : {}),
280
+ });
281
+ }
282
+ }
283
+ }
284
+
285
+ private save(): void {
286
+ if (!this.file) return;
287
+ mkdirSync(dirname(this.file), { recursive: true });
288
+ const obj: Record<string, ChestRecord> = {};
289
+ for (const [k, v] of this.map) obj[k] = v;
290
+ writeFileSync(this.file, `${JSON.stringify(obj)}\n`, 'utf8');
291
+ }
292
+ }
@@ -0,0 +1,272 @@
1
+ /**
2
+ * 从 Mojang 版本 JSON 生成客户端 Java 命令,支持 inheritsFrom 继承。
3
+ * client.ts 管理进程生命周期。
4
+ */
5
+ import { createHash } from 'node:crypto';
6
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+
9
+ interface Rule {
10
+ action?: 'allow' | 'disallow';
11
+ os?: { name?: string; arch?: string; version?: string };
12
+ features?: Record<string, boolean>;
13
+ }
14
+
15
+ interface Artifact {
16
+ path?: string;
17
+ url?: string;
18
+ }
19
+
20
+ interface LibraryEntry {
21
+ name?: string;
22
+ rules?: Rule[];
23
+ downloads?: { artifact?: Artifact };
24
+ }
25
+
26
+ type ArgEntry = string | { rules?: Rule[]; value?: string | string[] };
27
+
28
+ interface VersionJson {
29
+ id?: string;
30
+ inheritsFrom?: string;
31
+ jar?: string;
32
+ mainClass?: string;
33
+ assetIndex?: { id?: string };
34
+ arguments?: { game?: ArgEntry[]; jvm?: ArgEntry[] };
35
+ libraries?: LibraryEntry[];
36
+ type?: string;
37
+ }
38
+
39
+ interface ClientLaunchInput {
40
+ /** .minecraft 目录 */
41
+ gameDir: string;
42
+ /** versions/<id>/<id>.json 的 id;空 = 目录里唯一那个版本 */
43
+ versionId: string;
44
+ javaPath: string;
45
+ username: string;
46
+ /** 额外 JVM 参数(内存等),空格分隔 */
47
+ jvmArgs?: string[];
48
+ width: number;
49
+ height: number;
50
+ /** 进游戏直连的服务器;null = 停在主菜单 */
51
+ joinServer?: { host: string; port: number } | null;
52
+ /** natives 解压目录;缺省 <gameDir>/natives/<versionId> */
53
+ nativesDir?: string;
54
+ os?: NodeJS.Platform;
55
+ arch?: string;
56
+ }
57
+
58
+ interface ClientLaunch {
59
+ command: string;
60
+ args: string[];
61
+ cwd: string;
62
+ nativesDir: string;
63
+ mainClass: string;
64
+ /** 需要解压 dll 的 jar 绝对路径(client.ts 启动前处理) */
65
+ nativeJars: string[];
66
+ /** 实际用到的版本 id 链,child → root */
67
+ versionChain: string[];
68
+ }
69
+
70
+ const OS_NAMES: Partial<Record<NodeJS.Platform, string>> = {
71
+ win32: 'windows',
72
+ darwin: 'osx',
73
+ linux: 'linux',
74
+ };
75
+
76
+ /** 离线账号 UUID:以 OfflinePlayer:<名字> 的 MD5 生成版本 3 UUID。 */
77
+ export function offlineUuid(username: string): string {
78
+ const h = createHash('md5').update(`OfflinePlayer:${username}`).digest();
79
+ h[6] = (h[6] & 0x0f) | 0x30;
80
+ h[8] = (h[8] & 0x3f) | 0x80;
81
+ const hex = h.toString('hex');
82
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
83
+ }
84
+
85
+ /** maven 坐标 → libraries 下的相对路径(Fabric 的库常常只给名字不给 downloads) */
86
+ export function mavenPath(name: string): string {
87
+ const [coords, ext = 'jar'] = name.split('@');
88
+ const parts = coords.split(':');
89
+ const [group, artifact, version, classifier] = parts;
90
+ const file = `${artifact}-${version}${classifier ? `-${classifier}` : ''}.${ext}`;
91
+ return [...group.split('.'), artifact, version, file].join('/');
92
+ }
93
+
94
+ export function rulesAllow(
95
+ rules: Rule[] | undefined,
96
+ ctx: { os: string; arch: string; features: Record<string, boolean> },
97
+ ): boolean {
98
+ if (!rules || rules.length === 0) return true;
99
+ let allowed = false;
100
+ for (const rule of rules) {
101
+ let match = true;
102
+ if (rule.os) {
103
+ if (rule.os.name && rule.os.name !== ctx.os) match = false;
104
+ if (rule.os.arch && rule.os.arch !== ctx.arch) match = false;
105
+ // os.version 是 osx 专用的正则,windows 上一律不匹配
106
+ if (rule.os.version && ctx.os !== 'osx') match = false;
107
+ }
108
+ if (rule.features) {
109
+ for (const [key, want] of Object.entries(rule.features)) {
110
+ if (Boolean(ctx.features[key]) !== want) match = false;
111
+ }
112
+ }
113
+ if (match) allowed = (rule.action ?? 'allow') === 'allow';
114
+ }
115
+ return allowed;
116
+ }
117
+
118
+ function flattenArgs(
119
+ entries: ArgEntry[] | undefined,
120
+ ctx: { os: string; arch: string; features: Record<string, boolean> },
121
+ ): string[] {
122
+ const out: string[] = [];
123
+ for (const entry of entries ?? []) {
124
+ if (typeof entry === 'string') { out.push(entry); continue; }
125
+ if (!rulesAllow(entry.rules, ctx)) continue;
126
+ if (typeof entry.value === 'string') out.push(entry.value);
127
+ else if (Array.isArray(entry.value)) out.push(...entry.value);
128
+ }
129
+ return out;
130
+ }
131
+
132
+ function substitute(arg: string, vars: Record<string, string>): string {
133
+ return arg.replace(/\$\{([\w.]+)\}/g, (whole, key: string) => vars[key] ?? whole);
134
+ }
135
+
136
+ /** child → root 的版本 JSON 链;循环继承与缺文件都当错误 */
137
+ function loadVersionChain(gameDir: string, versionId: string): VersionJson[] | { error: string } {
138
+ const chain: VersionJson[] = [];
139
+ const seen = new Set<string>();
140
+ let id = versionId;
141
+ while (id) {
142
+ if (seen.has(id)) return { error: `版本继承成环: ${[...seen].join(' → ')} → ${id}` };
143
+ seen.add(id);
144
+ const file = join(gameDir, 'versions', id, `${id}.json`);
145
+ if (!existsSync(file)) return { error: `找不到版本文件 ${file}` };
146
+ let json: VersionJson;
147
+ try {
148
+ json = JSON.parse(readFileSync(file, 'utf8')) as VersionJson;
149
+ } catch (err) {
150
+ return { error: `版本文件解析失败 ${file}: ${(err as Error).message}` };
151
+ }
152
+ chain.push(json);
153
+ id = json.inheritsFrom ?? '';
154
+ }
155
+ return chain;
156
+ }
157
+
158
+ /** 只装了一个版本时不必填版本号 */
159
+ export function soleVersionId(gameDir: string): string | null {
160
+ const dir = join(gameDir, 'versions');
161
+ if (!existsSync(dir)) return null;
162
+ // 目录名即版本 id;有 <id>.json 的才算装好了
163
+ const ids = readdirSync(dir).filter((name) => existsSync(join(dir, name, `${name}.json`)));
164
+ return ids.length === 1 ? ids[0] : null;
165
+ }
166
+
167
+ export function buildClientLaunch(input: ClientLaunchInput): ClientLaunch | { error: string } {
168
+ const { gameDir } = input;
169
+ if (!gameDir) return { error: '未配置客户端游戏目录(worlds.minecraft.client.gameDir)' };
170
+ if (!existsSync(gameDir)) return { error: `客户端游戏目录不存在: ${gameDir}` };
171
+ const versionId = input.versionId || soleVersionId(gameDir) || '';
172
+ if (!versionId) {
173
+ return { error: `${join(gameDir, 'versions')} 下不是恰好一个版本,请在 worlds.minecraft.client.versionId 里指定` };
174
+ }
175
+ const chain = loadVersionChain(gameDir, versionId);
176
+ if ('error' in chain) return chain;
177
+
178
+ const os = OS_NAMES[input.os ?? process.platform] ?? 'linux';
179
+ const arch = (input.arch ?? process.arch) === 'ia32' ? 'x86' : (input.arch ?? process.arch);
180
+ const features: Record<string, boolean> = {
181
+ is_demo_user: false,
182
+ has_custom_resolution: true,
183
+ has_quick_plays_support: false,
184
+ is_quick_play_singleplayer: false,
185
+ is_quick_play_multiplayer: Boolean(input.joinServer),
186
+ is_quick_play_realms: false,
187
+ };
188
+ const ctx = { os, arch, features };
189
+
190
+ // child 在前:mainClass / assetIndex 取最先声明的那个,classpath 也让 loader 排在前面
191
+ const pick = <K extends keyof VersionJson>(key: K): VersionJson[K] | undefined =>
192
+ chain.find((v) => v[key] !== undefined)?.[key];
193
+ const mainClass = pick('mainClass');
194
+ if (!mainClass) return { error: `版本 ${versionId} 没有 mainClass` };
195
+ const root = chain[chain.length - 1];
196
+ const jarId = pick('jar') ?? root.id ?? versionId;
197
+ const versionJar = join(gameDir, 'versions', jarId, `${jarId}.jar`);
198
+ if (!existsSync(versionJar)) return { error: `找不到版本主 jar: ${versionJar}` };
199
+ const assetIndexId = pick('assetIndex')?.id;
200
+ if (!assetIndexId) return { error: `版本 ${versionId} 没有 assetIndex` };
201
+
202
+ const librariesDir = join(gameDir, 'libraries');
203
+ const classpath: string[] = [];
204
+ const nativeJars: string[] = [];
205
+ const seenPaths = new Set<string>();
206
+ for (const version of chain) {
207
+ for (const lib of version.libraries ?? []) {
208
+ if (!rulesAllow(lib.rules, ctx)) continue;
209
+ const rel = lib.downloads?.artifact?.path ?? (lib.name ? mavenPath(lib.name) : null);
210
+ if (!rel) continue;
211
+ const abs = join(librariesDir, rel);
212
+ if (seenPaths.has(abs)) continue;
213
+ seenPaths.add(abs);
214
+ if (!existsSync(abs)) continue;
215
+ if (/natives/i.test(rel)) nativeJars.push(abs);
216
+ classpath.push(abs);
217
+ }
218
+ }
219
+ classpath.push(versionJar);
220
+
221
+ const nativesDir = input.nativesDir ?? join(gameDir, 'natives', versionId);
222
+ const separator = os === 'windows' ? ';' : ':';
223
+ const vars: Record<string, string> = {
224
+ auth_player_name: input.username,
225
+ version_name: versionId,
226
+ game_directory: gameDir,
227
+ assets_root: join(gameDir, 'assets'),
228
+ game_assets: join(gameDir, 'assets'),
229
+ assets_index_name: assetIndexId,
230
+ auth_uuid: offlineUuid(input.username),
231
+ auth_access_token: '0',
232
+ auth_session: '0',
233
+ clientid: '',
234
+ auth_xuid: '',
235
+ user_type: 'msa',
236
+ user_properties: '{}',
237
+ version_type: pick('type') ?? 'release',
238
+ resolution_width: String(input.width),
239
+ resolution_height: String(input.height),
240
+ natives_directory: nativesDir,
241
+ launcher_name: 'cortico',
242
+ launcher_version: '1',
243
+ classpath: classpath.join(separator),
244
+ classpath_separator: separator,
245
+ library_directory: librariesDir,
246
+ quickPlayMultiplayer: input.joinServer ? `${input.joinServer.host}:${input.joinServer.port}` : '',
247
+ quickPlayPath: '',
248
+ };
249
+
250
+ /** 继承链均未声明 JVM 参数时,使用默认的三项参数。 */
251
+ const jvmEntries = chain.flatMap((v) => flattenArgs(v.arguments?.jvm, ctx));
252
+ const jvmArgs = jvmEntries.length > 0
253
+ ? jvmEntries
254
+ : ['-Djava.library.path=${natives_directory}', '-cp', '${classpath}'];
255
+ const gameEntries = chain.flatMap((v) => flattenArgs(v.arguments?.game, ctx));
256
+
257
+ const args = [
258
+ ...jvmArgs.map((a) => substitute(a, vars)),
259
+ ...(input.jvmArgs ?? []),
260
+ mainClass,
261
+ ...gameEntries.map((a) => substitute(a, vars)),
262
+ ];
263
+ return {
264
+ command: input.javaPath || 'java',
265
+ args,
266
+ cwd: gameDir,
267
+ nativesDir,
268
+ mainClass,
269
+ nativeJars,
270
+ versionChain: chain.map((v, i) => v.id ?? (i === 0 ? versionId : '?')),
271
+ };
272
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * 启动前写入 options.txt 和 SpectatorPlus 客户端配置。
3
+ * 共用 gameDir 的客户端共用设置文件;独立设置需要不同目录。
4
+ */
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
6
+ import { dirname, join } from 'node:path';
7
+ import type { Logger } from '../../core/types.ts';
8
+
9
+ /**
10
+ * 合并 options.txt 时保留行序与未覆盖键,缺失键追加到末尾。
11
+ * 值允许包含冒号,解析时只分割第一个冒号。
12
+ */
13
+ export function mergeOptions(prev: string, overrides: Record<string, string>): string {
14
+ const remaining = new Map(Object.entries(overrides));
15
+ const lines = prev.split(/\r?\n/).map((line) => {
16
+ const at = line.indexOf(':');
17
+ if (at <= 0) return line;
18
+ const key = line.slice(0, at);
19
+ if (!remaining.has(key)) return line;
20
+ const value = remaining.get(key)!;
21
+ remaining.delete(key);
22
+ return `${key}:${value}`;
23
+ });
24
+ while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
25
+ for (const [key, value] of remaining) lines.push(`${key}:${value}`);
26
+ return `${lines.join('\n')}\n`;
27
+ }
28
+
29
+ /** 写入 options.txt,保留未覆盖键;缺失文件只包含传入键。 */
30
+ function writeOptions(gameDir: string, overrides: Record<string, string>, note: string, log: Logger): void {
31
+ const file = join(gameDir, 'options.txt');
32
+ try {
33
+ const prev = existsSync(file) ? readFileSync(file, 'utf8') : '';
34
+ const next = mergeOptions(prev, overrides);
35
+ if (next === prev) return;
36
+ writeFileSync(file, next, 'utf8');
37
+ log.info(`已调整 ${file}:${note}`);
38
+ } catch (err) {
39
+ log.warn(`options.txt 调整失败(不影响启动): ${(err as Error).message}`);
40
+ }
41
+ }
42
+
43
+ export function applyLaunchOptions(gameDir: string, log: Logger): void {
44
+ writeOptions(
45
+ gameDir,
46
+ { pauseOnLostFocus: 'false', onboardAccessibility: 'false', soundCategory_master: '1.0' },
47
+ '失焦不弹暂停菜单,跳过首启引导屏,主音量拉满',
48
+ log,
49
+ );
50
+ }
51
+
52
+ /** 将 chatVisibility 设为 FULL,以启用聊天框和命令输入。 */
53
+ export function applyChatVisible(gameDir: string, log: Logger): void {
54
+ writeOptions(gameDir, { chatVisibility: '0' }, '聊天框与命令行可用', log);
55
+ }
56
+
57
+ /** SpectatorPlus 的 openScreens 控制附身目标的 GUI 同步,由 client.syncGui 配置。 */
58
+ export function mergeSpectatorPlusConfig(prev: string, overrides: Record<string, boolean>): string {
59
+ let obj: Record<string, unknown> = {};
60
+ if (prev.trim()) {
61
+ const parsed: unknown = JSON.parse(prev);
62
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
63
+ obj = parsed as Record<string, unknown>;
64
+ }
65
+ }
66
+ for (const [k, v] of Object.entries(overrides)) obj[k] = v;
67
+ return `${JSON.stringify(obj, null, 2)}\n`;
68
+ }
69
+
70
+ /** 启动前合并 config/spectatorplus/client.json;缺失文件只写 openScreens。 */
71
+ export function applySpectatorPlusConfig(gameDir: string, log: Logger, openScreens: boolean): void {
72
+ const file = join(gameDir, 'config', 'spectatorplus', 'client.json');
73
+ try {
74
+ const prev = existsSync(file) ? readFileSync(file, 'utf8') : '';
75
+ const next = mergeSpectatorPlusConfig(prev, { openScreens });
76
+ if (next === prev) return;
77
+ mkdirSync(dirname(file), { recursive: true });
78
+ writeFileSync(file, next, 'utf8');
79
+ log.info(`已调整 ${file}:${openScreens
80
+ ? 'bot 开箱子/合成/熔炉时把界面同步到摄像机画面上(GUI 演出)'
81
+ : 'bot 开箱子/熔炉时不把界面同步到摄像机画面上'}`);
82
+ } catch (err) {
83
+ log.warn(`SpectatorPlus 配置调整失败(不影响启动): ${(err as Error).message}`);
84
+ }
85
+ }