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,749 @@
1
+ /**
2
+ * 管理 Mineflayer 连接、重连和 prismarine-viewer。
3
+ * 重连会替换 bot 实例,World 与执行器须通过 bridge.bot 取得当前实例。
4
+ */
5
+ import mineflayer from 'mineflayer';
6
+ import pathfinderPkg, { pathfinder, Movements } from 'mineflayer-pathfinder';
7
+
8
+ // goals 不是 cjs-module-lexer 能静态识别的命名导出,只能从默认导出上取
9
+ const { goals } = pathfinderPkg;
10
+ import { Vec3 } from 'vec3';
11
+ import type { Logger } from '../../core/types.ts';
12
+ import type { RouteProbe, TargetDiag } from './executor.ts';
13
+ import type { MinecraftLog } from './log.ts';
14
+ import { installMineflayerFixes, installPathfinderToolSelection } from './mineflayer-fixes.ts';
15
+ import {
16
+ installPathfinderPerf, setDigBackoff, setNoPlaceCells, setSiteZones, type SiteZone,
17
+ } from './pathfinder-perf.ts';
18
+ import { isGravityBlock, isSpawnAnchorBlock } from './policy.ts';
19
+ import type { ShowTempo } from './show.ts';
20
+ import { pocketScan, standCellsAround } from './terrain.ts';
21
+
22
+ interface BridgeOptions {
23
+ host: string;
24
+ port: number;
25
+ username: string;
26
+ version: string;
27
+ /** prismarine-viewer 网页端口;0=不开 viewer */
28
+ viewerPort: number;
29
+ log: Logger;
30
+ /** World 日志;不给就不记(合成与放置的包流走它) */
31
+ diag?: MinecraftLog;
32
+ /** 寻路垫脚方块名单(顺序即优先级,空数组=禁垫);spawn 与风格热改时求值 */
33
+ scaffoldBlocks?: () => string[];
34
+ /** 挖/垫代价系数(mc_policy 的 travel 档位);spawn 与设置热改时求值 */
35
+ movementCosts?: () => { placeCost: number; digCost: number };
36
+ /**
37
+ * 在建的蓝图工地(已绑定锚点、游标未满的那些)。寻路器据此**不在工地体积里垫脚
38
+ * 搭路**,并把工地建材降到垫脚候选末位;走与挖不受限。每次寻路搜索现取。
39
+ */
40
+ blueprintZones?: () => readonly SiteZone[];
41
+ /**
42
+ * 成果登记里的一格(维度已由调用方合上)。寻路器不往登记格自己、也不往它头顶
43
+ * 垫脚搭路;走与挖不受限。每次候选移动生成时现问。
44
+ */
45
+ workCell?: (x: number, y: number, z: number) => boolean;
46
+ /** 容器 GUI 演出节拍;摄像机没开/演出关着时回 null(craft 用,每次 bot.craft 现取) */
47
+ showTempo?: () => ShowTempo | null;
48
+ /** spawn 完成(含重连后) */
49
+ onSpawn: () => void;
50
+ /** 同一连接内的死亡重生;连接代次和资源保持不变。 */
51
+ onRespawn?: () => void;
52
+ /** 断线后通知;attempt 为本次断线前连续连接失败次数,连接成功归零。 */
53
+ onDisconnect: (reason: string, willReconnect: boolean, attempt: number) => void;
54
+ /** 连接告警通过 World 事件通道投递。 */
55
+ onAlarm?: (text: string) => void;
56
+ /** 停止期间不创建连接或安排重连。 */
57
+ shuttingDown?: () => boolean;
58
+ }
59
+
60
+ const RECONNECT_DELAYS_MS = [3_000, 10_000, 30_000, 60_000];
61
+
62
+ const DIG_BACKOFF_TRIES = 3;
63
+
64
+ const DIG_BACKOFF_MS = 60_000;
65
+
66
+ interface Cell { x: number; y: number; z: number }
67
+
68
+ const cellKey = (p: Cell): string => `${Math.floor(p.x)},${Math.floor(p.y)},${Math.floor(p.z)}`;
69
+
70
+ /** 「暂时挖不动」的一格,连同它进退避的时刻 */
71
+ interface DigBackoffCell extends Cell {
72
+ since: number;
73
+ }
74
+
75
+ /** 本机端口连续拒连达到此次数时,提示检查服务器是否已启动。 */
76
+ const REFUSED_ALARM_AT = 5;
77
+ const REFUSED_ALARM_EVERY = 10;
78
+ /** 连续连接被拒绝达到阈值后的重试间隔。 */
79
+ const REFUSED_DELAY_MS = 120_000;
80
+
81
+ function isLocalHost(host: string): boolean {
82
+ return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '0.0.0.0';
83
+ }
84
+
85
+ /** 路线试算的总超时与单次迭代预算;partial 时继续原搜索。 */
86
+ const PROBE_TIMEOUT_MS = 400;
87
+ const PROBE_TICK_MS = 60;
88
+ const PROBE_WALL_MS = 500;
89
+
90
+ interface ProbePath {
91
+ status: string;
92
+ /** A* closed set 大小;=1 表示只展开了起点 */
93
+ visitedNodes?: number;
94
+ path: Array<{ x: number; y: number; z: number; toBreak?: unknown[]; toPlace?: unknown[] }>;
95
+ }
96
+
97
+ /** 最后一次检测到水后禁用疾跑的物理 tick 数;20 tick 约一秒。 */
98
+ const SPRINT_WET_TICKS = 40;
99
+
100
+ const WATER_BLOCKS = new Set(['water', 'bubble_column', 'kelp', 'kelp_plant', 'seagrass', 'tall_seagrass']);
101
+
102
+ /** 水面附近 isInWater 会逐 tick 变化;检测到水后禁用疾跑 40 tick(约两秒)。 */
103
+ function suppressSprintNearWater(bot: mineflayer.Bot, movements: Movements): void {
104
+ let wet = 0;
105
+ // isInWater 由 prismarine-physics 每 tick 写在实体上,prismarine-entity 的类型里没有
106
+ const inWater = (): boolean => Boolean((bot.entity as unknown as { isInWater?: boolean }).isInWater);
107
+ bot.on('physicsTick', () => {
108
+ const feet = bot.blockAt(bot.entity.position);
109
+ const soaked = inWater() || (feet !== null && WATER_BLOCKS.has(feet.name));
110
+ wet = soaked ? SPRINT_WET_TICKS : Math.max(0, wet - 1);
111
+ movements.allowSprinting = wet === 0;
112
+ });
113
+ }
114
+
115
+ /** prismarine-viewer 只用到 mineflayer() 这一个入口 */
116
+ interface ViewerModule {
117
+ mineflayer(bot: mineflayer.Bot, opts: { port: number; firstPerson: boolean }): void;
118
+ }
119
+
120
+ /** 上游 viewer.close() 不返回 Promise,关闭后轮询端口是否可绑定,超时记录警告。 */
121
+ const VIEWER_RELEASE_MS = 3_000;
122
+ const VIEWER_RELEASE_POLL_MS = 50;
123
+
124
+ /** 端口可绑返回 true(探完即关) */
125
+ async function probePort(port: number): Promise<boolean> {
126
+ const { createServer } = await import('node:net');
127
+ return new Promise((resolve) => {
128
+ const srv = createServer();
129
+ srv.once('error', () => resolve(false));
130
+ srv.listen(port, () => srv.close(() => resolve(true)));
131
+ });
132
+ }
133
+
134
+ const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
135
+
136
+ /**
137
+ * 每代连接登记监听端口、外部进程句柄等资源,失效时逆序关闭。
138
+ * dispose 之后收到的注册立即执行 closer,回收异步启动迟到的资源。
139
+ */
140
+ class ResourceBag {
141
+ private readonly closers: Array<{ name: string; close: () => Promise<void> | void }> = [];
142
+ private draining: Promise<void> | null = null;
143
+ /** draining 在 dispose 的同步部分完成后赋值;此前由此标记阻止新资源登记。 */
144
+ private closed = false;
145
+
146
+ constructor(private readonly log: Logger) {}
147
+
148
+ get disposed(): boolean {
149
+ return this.closed;
150
+ }
151
+
152
+ register(name: string, close: () => Promise<void> | void): void {
153
+ if (this.closed) {
154
+ void this.run(name, close);
155
+ return;
156
+ }
157
+ this.closers.push({ name, close });
158
+ }
159
+
160
+ dispose(): Promise<void> {
161
+ if (this.draining !== null) return this.draining;
162
+ this.closed = true;
163
+ const items = this.closers.splice(0).reverse();
164
+ this.draining = (async () => {
165
+ for (const item of items) await this.run(item.name, item.close);
166
+ })();
167
+ return this.draining;
168
+ }
169
+
170
+ private async run(name: string, close: () => Promise<void> | void): Promise<void> {
171
+ try {
172
+ await close();
173
+ } catch (err) {
174
+ this.log.warn(`${name} 关闭失败: ${(err as Error).message}`);
175
+ }
176
+ }
177
+ }
178
+
179
+ export class Bridge {
180
+ private _bot: mineflayer.Bot | null = null;
181
+ private started = false;
182
+ private stopped = false;
183
+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
184
+ private reconnectAttempt = 0;
185
+ /** 每次 connect 递增的连接世代;资源和迟到回调均据此判定归属。 */
186
+ private generation = 0;
187
+ /** 已经作废的最高世代;世代单调作废,一代结束才有下一代。 */
188
+ private disposedThrough = -1;
189
+ private readonly bags = new Map<number, ResourceBag>();
190
+ private readonly disposing = new Set<Promise<void>>();
191
+ private viewer: { gen: number; url: string } | null = null;
192
+ private _invSynced = false;
193
+ private scaffoldComplained = '';
194
+ private refusedStreak = 0;
195
+ private liveMovements: Movements | null = null;
196
+
197
+ /**
198
+ * 挖掘失败按格坐标记账;连续失败 DIG_BACKOFF_TRIES 次进入退避,
199
+ * DIG_BACKOFF_MS 后过期。
200
+ */
201
+ private digFails = new Map<string, { tries: number; lastAt: number; since: number | null; cell: Cell }>();
202
+
203
+ constructor(private readonly opts: BridgeOptions) {}
204
+
205
+ /** 当前连接的 bot;未连接或重连中为 null。 */
206
+ get bot(): mineflayer.Bot | null {
207
+ return this._bot;
208
+ }
209
+
210
+ /** 登录后收到窗口 0 的 window_items 才将空物品栏视为真实状态。 */
211
+ get invSynced(): boolean {
212
+ return this._invSynced;
213
+ }
214
+
215
+ get connected(): boolean {
216
+ return this._bot !== null && (this._bot as unknown as { entity?: unknown }).entity !== undefined;
217
+ }
218
+
219
+ /** 连接生命周期已启用;不要求当前已经完成登录。 */
220
+ get active(): boolean {
221
+ return this.started;
222
+ }
223
+
224
+ get reconnects(): number {
225
+ return this.reconnectAttempt;
226
+ }
227
+
228
+ get viewerUrl(): string | null {
229
+ return this.viewer !== null && this.viewer.gen === this.generation ? this.viewer.url : null;
230
+ }
231
+
232
+ /** 已作废代次使用关闭状态的 ResourceBag,迟到资源登记时立即关闭。 */
233
+ private bagFor(gen: number): ResourceBag {
234
+ const existing = this.bags.get(gen);
235
+ if (existing) return existing;
236
+ const bag = new ResourceBag(this.opts.log);
237
+ this.bags.set(gen, bag);
238
+ if (gen <= this.disposedThrough) void this.disposeGeneration(gen);
239
+ return bag;
240
+ }
241
+
242
+ /** 逆序关闭资源;返回值只等待调用时已登记的资源。 */
243
+ private disposeGeneration(gen: number): Promise<void> {
244
+ const bag = this.bags.get(gen);
245
+ this.disposedThrough = Math.max(this.disposedThrough, gen);
246
+ if (!bag) return Promise.resolve();
247
+ this.bags.delete(gen);
248
+ const done = bag.dispose().finally(() => void this.disposing.delete(done));
249
+ this.disposing.add(done);
250
+ return done;
251
+ }
252
+
253
+ /** 等所有在途回收落地:端口释放必须排在新一代绑定之前 */
254
+ private async settleDisposals(): Promise<void> {
255
+ while (this.disposing.size > 0) await Promise.allSettled([...this.disposing]);
256
+ }
257
+
258
+ start(): void {
259
+ if (this.started) return;
260
+ this.started = true;
261
+ this.stopped = false;
262
+ this.connect();
263
+ }
264
+
265
+ async stop(): Promise<void> {
266
+ this.started = false;
267
+ this.stopped = true;
268
+ if (this.reconnectTimer) {
269
+ clearTimeout(this.reconnectTimer);
270
+ this.reconnectTimer = null;
271
+ }
272
+ const bot = this._bot;
273
+ this._bot = null;
274
+ this._invSynced = false;
275
+ this.liveMovements = null;
276
+ this.digFails.clear();
277
+ this.reconnectAttempt = 0;
278
+ this.refusedStreak = 0;
279
+ this.viewer = null;
280
+ if (bot) {
281
+ try {
282
+ bot.quit();
283
+ } catch {
284
+ /* 已断开 */
285
+ }
286
+ }
287
+ this.disposedThrough = Math.max(this.disposedThrough, this.generation);
288
+ for (const gen of [...this.bags.keys()].sort((a, b) => b - a)) {
289
+ await this.disposeGeneration(gen);
290
+ }
291
+ await this.settleDisposals();
292
+ }
293
+
294
+ /**
295
+ * 服务器进入 running 时取消退避并立即重连;已连接或正在连接时不操作。
296
+ */
297
+ reconnectNow(reason: string): void {
298
+ if (this.stopped || this._bot !== null) return;
299
+ if (this.reconnectTimer) {
300
+ clearTimeout(this.reconnectTimer);
301
+ this.reconnectTimer = null;
302
+ }
303
+ this.refusedStreak = 0;
304
+ this.opts.log.info(`minecraft 立刻重连: ${reason}`);
305
+ this.connect();
306
+ }
307
+
308
+ private connect(): void {
309
+ if (this.stopped || this.opts.shuttingDown?.()) return;
310
+ const { host, port, username, version, log } = this.opts;
311
+ const gen = ++this.generation;
312
+ for (const old of [...this.bags.keys()]) {
313
+ if (old < gen) void this.disposeGeneration(old);
314
+ }
315
+ this.bagFor(gen);
316
+ log.info(`minecraft 连接 ${host}:${port} as ${username} (${version})`);
317
+ let bot: mineflayer.Bot;
318
+ try {
319
+ bot = mineflayer.createBot({ host, port, username, version, auth: 'offline' });
320
+ } catch (err) {
321
+ log.warn(`createBot 失败: ${(err as Error).message}`);
322
+ this.scheduleReconnect(String((err as Error).message));
323
+ return;
324
+ }
325
+ this._bot = bot;
326
+ this._invSynced = false;
327
+ /** 修补须通过插件注入,等待 Mineflayer 的 inject_allowed。 */
328
+ bot.loadPlugin((b) => installMineflayerFixes(b, log, this.opts.diag, this.opts.showTempo));
329
+ bot.loadPlugin(pathfinder);
330
+ installPathfinderPerf(log);
331
+ (bot._client as unknown as { on(ev: string, cb: (pkt: { windowId: number }) => void): void }).on(
332
+ 'window_items',
333
+ (pkt) => {
334
+ if (this._bot === bot && this.generation === gen && pkt.windowId === 0) this._invSynced = true;
335
+ },
336
+ );
337
+
338
+ bot.once('spawn', () => {
339
+ if (this.stopped || this._bot !== bot || this.generation !== gen) return;
340
+ this.reconnectAttempt = 0;
341
+ this.refusedStreak = 0;
342
+ this.installSpawnGear(bot, gen);
343
+ this.opts.onSpawn();
344
+ /** spawn 在死亡重生时也触发,连接装配使用 once;持续监听处理同连接重生。 */
345
+ /** respawn 包也用于维度切换,不能单独识别死亡重生。 */
346
+ bot.on('spawn', () => {
347
+ if (this.stopped || this._bot !== bot || this.generation !== gen) return;
348
+ this.opts.onRespawn?.();
349
+ });
350
+ });
351
+
352
+ const onGone = (reason: string) => {
353
+ if (this._bot !== bot || this.generation !== gen) return;
354
+ this._bot = null;
355
+ this._invSynced = false;
356
+ this.viewer = null;
357
+ void this.disposeGeneration(gen);
358
+ this.liveMovements = null;
359
+ this.digFails.clear();
360
+ const willReconnect = !this.stopped;
361
+ this.opts.onDisconnect(reason, willReconnect, this.reconnectAttempt);
362
+ if (willReconnect) this.scheduleReconnect(reason);
363
+ };
364
+ bot.once('end', (reason) => onGone(String(reason)));
365
+ bot.once('kicked', (reason) => onGone(`kicked: ${JSON.stringify(reason)}`));
366
+ bot.on('error', (err) => {
367
+ log.warn(`minecraft 连接错误: ${err.message}`);
368
+ this.noteConnectError(err);
369
+ });
370
+ }
371
+
372
+ /** 连续 ECONNREFUSED 达到阈值时告警,并延长重连间隔。其他错误重置计数。 */
373
+ private noteConnectError(err: Error): void {
374
+ const code = (err as NodeJS.ErrnoException).code ?? '';
375
+ if (code !== 'ECONNREFUSED' && !err.message.includes('ECONNREFUSED')) {
376
+ this.refusedStreak = 0;
377
+ return;
378
+ }
379
+ this.refusedStreak += 1;
380
+ const n = this.refusedStreak;
381
+ if (n < REFUSED_ALARM_AT || (n - REFUSED_ALARM_AT) % REFUSED_ALARM_EVERY !== 0) return;
382
+ const where = `${this.opts.host}:${this.opts.port}`;
383
+ this.opts.log.error(`minecraft 连续 ${n} 次被 ${where} 拒连:连接被拒绝`);
384
+ this.opts.onAlarm?.(
385
+ isLocalHost(this.opts.host)
386
+ ? `连着 ${n} 次连不上 ${where},端口上根本没有进程在听 —— MC 服务器没在跑,去控制台的 Minecraft 面板把它启动起来;` +
387
+ '在那之前我进不了游戏,做不了任何事。'
388
+ : `连着 ${n} 次连不上 ${where},对面端口没有进程在听 —— 那台服务器没在跑。`,
389
+ );
390
+ }
391
+
392
+ /** 一条连接只装一次的那些东西(寻路器、挖掘退避、viewer);死亡重生不重装 */
393
+ private installSpawnGear(bot: mineflayer.Bot, gen: number): void {
394
+ const log = this.opts.log;
395
+ installPathfinderToolSelection(bot, log);
396
+ const movements = new Movements(bot);
397
+ movements.canDig = true;
398
+ movements.allow1by1towers = true;
399
+ this.applyTuning(bot, movements);
400
+ this.liveMovements = movements;
401
+ bot.pathfinder.setMovements(movements);
402
+ bot.pathfinder.tickTimeout = 60;
403
+ suppressSprintNearWater(bot, movements);
404
+ this.installDigBackoff(bot);
405
+ this.installPathDiag(bot);
406
+ this.startViewer(bot, gen);
407
+ }
408
+
409
+ /** 把垫脚名单与挖/垫代价装到 movements 上;spawn 与风格热改共用 */
410
+ private applyTuning(bot: mineflayer.Bot, movements: Movements): void {
411
+ const log = this.opts.log;
412
+
413
+ /** 寻路单次落差限制为一格。 */
414
+ movements.maxDropDown = 2;
415
+
416
+ /** 上游 lava 的 diggable=true;额外加入 blocksCantBreak 禁止寻路挖掘。 */
417
+ const lava = (bot.registry.blocksByName as Record<string, { id: number } | undefined>).lava;
418
+ if (lava) movements.blocksCantBreak.add(lava.id);
419
+
420
+ // 维度切换只由 transit 发起;普通寻路把传送面与门框当作空间边界。
421
+ const portalBlocks = bot.registry.blocksByName as Record<string, { id: number } | undefined>;
422
+ for (const name of [
423
+ 'nether_portal', 'end_portal', 'end_gateway',
424
+ 'obsidian', 'crying_obsidian', 'end_portal_frame',
425
+ ]) {
426
+ const portal = portalBlocks[name];
427
+ if (!portal) continue;
428
+ if (name === 'nether_portal' || name === 'end_portal' || name === 'end_gateway') {
429
+ movements.blocksToAvoid.add(portal.id);
430
+ }
431
+ movements.blocksCantBreak.add(portal.id);
432
+ }
433
+
434
+ // 寻路不得挖穿容器、工作站及下列功能方块;取出或拆除须走 take/collect。
435
+ // 上游默认保护名单只有 chest,需在此补齐。
436
+ const blocksByName = bot.registry.blocksByName as Record<string, { id: number } | undefined>;
437
+ for (const name of [
438
+ 'chest', 'trapped_chest', 'barrel', 'ender_chest',
439
+ 'furnace', 'blast_furnace', 'smoker', 'crafting_table',
440
+ 'bookshelf', 'enchanting_table', 'cake',
441
+ 'brewing_stand', 'lectern', 'smithing_table',
442
+ 'anvil', 'chipped_anvil', 'damaged_anvil',
443
+ 'beacon', 'cauldron', 'water_cauldron', 'lava_cauldron', 'powder_snow_cauldron',
444
+ ]) {
445
+ const b = blocksByName[name];
446
+ if (b) movements.blocksCantBreak.add(b.id);
447
+ }
448
+
449
+ /** 启用开门并禁止挖门;状态通行判据由 pathfinder-perf 的 applyDoorState 提供。 */
450
+ movements.canOpenDoors = true;
451
+ for (const name of Object.keys(blocksByName)) {
452
+ if (!name.endsWith('_door') && !name.endsWith('_fence_gate')) continue;
453
+ const b = blocksByName[name];
454
+ if (b) movements.blocksCantBreak.add(b.id);
455
+ }
456
+
457
+ // 床与 respawn_anchor 禁止被寻路挖穿;显式拆除仍走执行器确认路径。
458
+ for (const name of Object.keys(blocksByName)) {
459
+ if (!isSpawnAnchorBlock(name)) continue;
460
+ const b = blocksByName[name];
461
+ if (b) movements.blocksCantBreak.add(b.id);
462
+ }
463
+
464
+ const wanted = this.opts.scaffoldBlocks?.();
465
+ if (wanted) {
466
+ /** scafoldingBlocks 使用物品 ID;1.20.6 的物品和方块 ID 不同,须查询 itemsByName。 */
467
+ const byName = bot.registry.itemsByName as Record<string, { id: number } | undefined>;
468
+ // 重力方块失去支撑后会下落,不能作为寻路器按固定落点记账的垫脚料。
469
+ const heavy = wanted.filter((n) => isGravityBlock(n));
470
+ const usable = wanted.filter((n) => !isGravityBlock(n));
471
+ const complaints: string[] = [];
472
+ if (heavy.length > 0) {
473
+ complaints.push(`scaffoldBlocks 里的重力方块不收(垫下去会自己掉,垫不住): ${heavy.join('、')}`);
474
+ }
475
+ const ids = usable.map((n) => byName[n]?.id).filter((id): id is number => id !== undefined);
476
+ const unknown = usable.filter((n) => byName[n] === undefined);
477
+ if (unknown.length > 0) complaints.push(`scaffoldBlocks 里不认识的方块名被忽略: ${unknown.join('、')}`);
478
+ if (ids.length > 0 || wanted.length === 0) {
479
+ movements.scafoldingBlocks = ids;
480
+ } else {
481
+ complaints.push('scaffoldBlocks 全部无效,沿用寻路器默认(泥土、圆石)');
482
+ }
483
+ const key = complaints.join('\n');
484
+ if (key !== this.scaffoldComplained) {
485
+ this.scaffoldComplained = key;
486
+ for (const c of complaints) log.warn(c);
487
+ }
488
+ }
489
+ setSiteZones(movements, this.opts.blueprintZones ?? null);
490
+ setNoPlaceCells(movements, this.opts.workCell ?? null);
491
+ setDigBackoff(movements, (x, y, z) => this.digBackedOff(x, y, z));
492
+ const costs = this.opts.movementCosts?.();
493
+ if (costs) {
494
+ movements.placeCost = costs.placeCost;
495
+ movements.digCost = costs.digCost;
496
+ }
497
+ }
498
+
499
+ /**
500
+ * 挖掘失败退避的记账口。挂 mineflayer 的挖掘结局事件而不是寻路器的
501
+ * `path_reset('dig_error')`:后者不带坐标,而退避是按格记的。
502
+ */
503
+ private installDigBackoff(bot: mineflayer.Bot): void {
504
+ bot.on('diggingAborted', (block) => this.noteDigFailure(block.position));
505
+ bot.on('diggingCompleted', (block) => {
506
+ this.digFails.delete(cellKey(block.position));
507
+ });
508
+ }
509
+
510
+ private noteDigFailure(p: Cell): void {
511
+ const now = Date.now();
512
+ const key = cellKey(p);
513
+ const rec = this.digFails.get(key);
514
+ if (rec === undefined || now - rec.lastAt > DIG_BACKOFF_MS) {
515
+ this.pruneDigFails(now);
516
+ this.digFails.set(key, {
517
+ tries: 1, lastAt: now, since: null,
518
+ cell: { x: Math.floor(p.x), y: Math.floor(p.y), z: Math.floor(p.z) },
519
+ });
520
+ return;
521
+ }
522
+ rec.tries += 1;
523
+ rec.lastAt = now;
524
+ if (rec.tries < DIG_BACKOFF_TRIES || rec.since !== null) return;
525
+ rec.since = now;
526
+ this.opts.diag?.write({
527
+ lane: 'path', event: 'dig-backoff',
528
+ msg: `(${rec.cell.x}, ${rec.cell.y}, ${rec.cell.z}) 连挖 ${rec.tries} 次没挖动,`
529
+ + `${DIG_BACKOFF_MS / 1000} 秒内寻路绕开`,
530
+ data: { cell: rec.cell, tries: rec.tries, ms: DIG_BACKOFF_MS },
531
+ });
532
+ }
533
+
534
+ private pruneDigFails(now: number): void {
535
+ for (const [key, rec] of this.digFails) {
536
+ if (now - rec.lastAt > DIG_BACKOFF_MS) this.digFails.delete(key);
537
+ }
538
+ }
539
+
540
+ /** 这一格此刻挖不动(寻路器据此不把它排进 toBreak) */
541
+ private digBackedOff(x: number, y: number, z: number): boolean {
542
+ if (this.digFails.size === 0) return false;
543
+ const key = cellKey({ x, y, z });
544
+ const rec = this.digFails.get(key);
545
+ if (rec?.since == null) return false;
546
+ if (Date.now() - rec.since <= DIG_BACKOFF_MS) return true;
547
+ this.digFails.delete(key);
548
+ return false;
549
+ }
550
+
551
+ /** `ts` 之后进退避的格子。goto 受阻回执据此说清「哪一格挖不动、绕开了」 */
552
+ digBackoffSince(ts: number): DigBackoffCell[] {
553
+ const now = Date.now();
554
+ const out: DigBackoffCell[] = [];
555
+ for (const rec of this.digFails.values()) {
556
+ if (rec.since === null || rec.since < ts || now - rec.since > DIG_BACKOFF_MS) continue;
557
+ out.push({ ...rec.cell, since: rec.since });
558
+ }
559
+ return out.sort((a, b) => a.since - b.since);
560
+ }
561
+
562
+ /** 移动风格热改:把新名单/代价装回当前 movements 并触发重算 */
563
+ retune(): void {
564
+ const bot = this._bot;
565
+ if (!bot || !this.liveMovements || !(bot as { pathfinder?: unknown }).pathfinder) return;
566
+ this.applyTuning(bot, this.liveMovements);
567
+ bot.pathfinder.setMovements(this.liveMovements);
568
+ }
569
+
570
+ /** getPathFromTo 生成器用于路线试算,避免 getPathTo 覆盖当前寻路的 A* 上下文。 */
571
+ probeRoutes(
572
+ target: { x: number; y: number; z: number },
573
+ /** 试算与执行须使用同一目标。省略时使用以 target 为中心、半径 1 的 GoalNear;水平寻路显式传入 GoalNearXZ。 */
574
+ goal: InstanceType<typeof goals.Goal> = new goals.GoalNear(target.x, target.y, target.z, 1),
575
+ ): RouteProbe[] | null {
576
+ const bot = this._bot;
577
+ if (!bot?.entity || typeof bot.pathfinder?.getPathFromTo !== 'function') return null;
578
+ const me = bot.entity.position;
579
+ const startDist = Math.hypot(me.x - target.x, me.y - target.y, me.z - target.z);
580
+ const out: RouteProbe[] = [];
581
+ for (const profile of ['style', 'dig', 'walk'] as const) {
582
+ const m = new Movements(bot);
583
+ m.canDig = profile !== 'walk';
584
+ m.allow1by1towers = profile === 'style';
585
+ this.applyTuning(bot, m);
586
+ if (profile !== 'style') m.scafoldingBlocks = [];
587
+ let result: ProbePath | null = null;
588
+ try {
589
+ const gen = bot.pathfinder.getPathFromTo(m, me, goal, {
590
+ timeout: PROBE_TIMEOUT_MS, tickTimeout: PROBE_TICK_MS,
591
+ });
592
+ const deadline = Date.now() + PROBE_WALL_MS;
593
+ for (const step of gen) {
594
+ result = (step?.result ?? null) as ProbePath | null;
595
+ if (!result || result.status !== 'partial' || Date.now() > deadline) break;
596
+ }
597
+ } catch (err) {
598
+ this.opts.log.warn(`路线试算失败(${profile}): ${(err as Error).message}`);
599
+ }
600
+ if (!result) {
601
+ out.push({ profile, status: 'noPath', steps: 0, place: 0, breaks: 0, endDist: startDist });
602
+ continue;
603
+ }
604
+ const path = result.path ?? [];
605
+ const last = path[path.length - 1];
606
+ const endDist = last
607
+ ? Math.hypot(last.x - target.x, last.y - target.y, last.z - target.z)
608
+ : startDist;
609
+ let place = 0;
610
+ let breaks = 0;
611
+ for (const mv of path) {
612
+ place += mv.toPlace?.length ?? 0;
613
+ breaks += mv.toBreak?.length ?? 0;
614
+ }
615
+ const status = result.status === 'success' ? 'complete'
616
+ : result.status === 'partial' || result.status === 'timeout' || result.status === 'noPath'
617
+ ? result.status as RouteProbe['status']
618
+ : 'noPath';
619
+ out.push({
620
+ profile, status, steps: path.length, place, breaks,
621
+ endDist: Math.round(endDist * 10) / 10,
622
+ ...(typeof result.visitedNodes === 'number' ? { visited: result.visitedNodes } : {}),
623
+ });
624
+ }
625
+ return out;
626
+ }
627
+
628
+ /** 检查目标附近落脚格与最多 128 格的空间连通性;未加载区域不作封闭结论。 */
629
+ probeTarget(target: { x: number; y: number; z: number }): TargetDiag | null {
630
+ const bot = this._bot;
631
+ if (!bot?.entity) return null;
632
+ const read = (x: number, y: number, z: number) => {
633
+ const b = bot.blockAt(new Vec3(x, y, z));
634
+ return b ? { name: b.name, solid: b.boundingBox === 'block' } : null;
635
+ };
636
+ const t = { x: Math.floor(target.x), y: Math.floor(target.y), z: Math.floor(target.z) };
637
+ const stand = standCellsAround(read, t);
638
+ if (stand.length === 0) return { kind: 'noStand' };
639
+ const size = pocketScan(read, stand);
640
+ return size === null ? { kind: 'open' } : { kind: 'sealed', size };
641
+ }
642
+
643
+ /** 每类寻路记录使用独立的五秒窗口;期间重复项计数,下一次输出附抑制数量。 */
644
+ private installPathDiag(bot: mineflayer.Bot): void {
645
+ const diag = this.opts.diag;
646
+ if (!diag) return;
647
+ const RESET_ZH: Record<string, string> = {
648
+ goal_updated: '目标更换', movements_updated: '移动规则更新', block_updated: '方块变化',
649
+ chunk_loaded: '区块加载', goal_moved: '目标移动', dig_error: '挖掘失败',
650
+ no_scaffolding_blocks: '没有搭路方块', place_error: '放置失败', stuck: '卡住',
651
+ };
652
+ // 抑制计数与时间窗按 key 独立记录;key 来自 RESET_ZH、update、goal、reached 的有限集合。
653
+ const suppressed = new Map<string, number>();
654
+ const lastAt = new Map<string, number>();
655
+ const write = (key: string, event: string, msg: string, data?: Record<string, unknown>): void => {
656
+ const now = Date.now();
657
+ if (now - (lastAt.get(key) ?? 0) < 5_000) {
658
+ suppressed.set(key, (suppressed.get(key) ?? 0) + 1);
659
+ return;
660
+ }
661
+ const n = suppressed.get(key) ?? 0;
662
+ const tail = n > 0 ? `(此前 ${n} 条同类未记)` : '';
663
+ diag.write({ lane: 'path', event, msg: msg + tail, data });
664
+ suppressed.set(key, 0);
665
+ lastAt.set(key, now);
666
+ };
667
+ bot.on('path_update', (r) => {
668
+ write(`update:${r.status}`, 'update',
669
+ `寻路 ${r.status}:${r.path.length} 步,搜了 ${r.visitedNodes} 节点/${Math.round(r.time)}ms`,
670
+ { status: r.status, pathLen: r.path.length, visitedNodes: r.visitedNodes, timeMs: Math.round(r.time) });
671
+ });
672
+ bot.on('path_reset', (reason) => {
673
+ write(`reset:${reason}`, 'reset', `寻路重置:${RESET_ZH[reason] ?? reason}`, { reason });
674
+ });
675
+ bot.on('goal_updated', (goal, dynamic) => {
676
+ write(`goal:${goal ? 'set' : 'clear'}`, 'goal',
677
+ goal ? `新寻路目标${dynamic ? '(动态)' : ''}:${goal.constructor?.name ?? 'Goal'}` : '寻路目标已撤销');
678
+ });
679
+ bot.on('goal_reached', () => {
680
+ write('reached', 'reached', '寻路到达目标');
681
+ });
682
+ }
683
+
684
+ /** 按需动态加载 viewer。 */
685
+ private loadViewer(): Promise<ViewerModule> {
686
+ return import('prismarine-viewer') as unknown as Promise<ViewerModule>;
687
+ }
688
+
689
+ private startViewer(bot: mineflayer.Bot, gen: number): void {
690
+ if (this.opts.viewerPort <= 0 || this.viewerUrl !== null) return;
691
+ const port = this.opts.viewerPort;
692
+ /** 上游 viewer 不暴露 http server 的 error 处理接口,启动前先探测端口;探测与绑定之间仍有竞争窗口。 */
693
+ void (async () => {
694
+ try {
695
+ await this.settleDisposals();
696
+ const free = await probePort(port);
697
+ if (!free) {
698
+ this.opts.log.warn(
699
+ `viewer 端口 ${port} 已被占用,本次不开画面(worlds.minecraft.viewerPort 可改)`,
700
+ );
701
+ return;
702
+ }
703
+ const mod = await this.loadViewer();
704
+ if (this.stopped || this._bot !== bot || this.generation !== gen || this.bagFor(gen).disposed) return;
705
+ mod.mineflayer(bot, { port, firstPerson: true });
706
+ // 取得句柄后必须同步注册到资源袋,中间不能 await,以免 stop 时漏收。
707
+ const close = (bot as unknown as { viewer?: { close?: () => void } }).viewer?.close;
708
+ this.bagFor(gen).register('prismarine-viewer', () => this.releaseViewer(gen, port, close));
709
+ if (this.stopped || this.generation !== gen) return; // 旧代不得改新代状态
710
+ this.viewer = { gen, url: `http://127.0.0.1:${port}` };
711
+ this.opts.log.info(`prismarine-viewer 已启动 http://127.0.0.1:${port}`);
712
+ } catch (err) {
713
+ this.opts.log.warn(`prismarine-viewer 启动失败(不影响游玩): ${(err as Error).message}`);
714
+ }
715
+ })();
716
+ }
717
+
718
+ /** viewer.close() 不返回 Promise,关闭后通过端口绑定探测确认释放。 */
719
+ private async releaseViewer(gen: number, port: number, close?: () => void): Promise<void> {
720
+ if (this.viewer?.gen === gen) this.viewer = null;
721
+ if (!close) {
722
+ this.opts.log.warn(`viewer 没有暴露 close,端口 ${port} 无法主动释放`);
723
+ return;
724
+ }
725
+ close();
726
+ const deadline = Date.now() + VIEWER_RELEASE_MS;
727
+ for (;;) {
728
+ if (await probePort(port)) return;
729
+ if (Date.now() >= deadline) {
730
+ this.opts.log.warn(`viewer 已关闭,端口 ${port} 在 ${VIEWER_RELEASE_MS / 1000}s 内仍不可绑定`);
731
+ return;
732
+ }
733
+ await sleep(VIEWER_RELEASE_POLL_MS);
734
+ }
735
+ }
736
+
737
+ private scheduleReconnect(reason: string): void {
738
+ if (this.stopped || this.opts.shuttingDown?.() || this.reconnectTimer) return;
739
+ const delay = this.refusedStreak >= REFUSED_ALARM_AT
740
+ ? REFUSED_DELAY_MS
741
+ : RECONNECT_DELAYS_MS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
742
+ this.reconnectAttempt++;
743
+ this.opts.log.info(`minecraft ${delay / 1000}s 后重连(第 ${this.reconnectAttempt} 次): ${reason}`);
744
+ this.reconnectTimer = setTimeout(() => {
745
+ this.reconnectTimer = null;
746
+ this.connect();
747
+ }, delay);
748
+ }
749
+ }