modelstage 0.0.0-stage → 0.5.28

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 (281) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +274 -2
  3. package/THIRD_PARTY_NOTICES.md +90 -0
  4. package/bin/modelstage.mjs +137 -0
  5. package/config/diag-upload.example.json +7 -0
  6. package/config/models/live2d.assetbundls.json +1 -0
  7. package/config/models/live2d.azurlane.json +1 -0
  8. package/config/models/live2d.azurlane_bundle.json +1 -0
  9. package/config/models/live2d.azurlane_spine.json +1 -0
  10. package/config/models/live2d.bd2.json +1 -0
  11. package/config/models/live2d.bluearchive.json +1 -0
  12. package/config/models/live2d.live2d.json +1 -0
  13. package/config/models/live2d.nikke.json +1 -0
  14. package/config/models/live2d.nikke_2.json +1 -0
  15. package/config/models/live2d.npc.json +1 -0
  16. package/config/models/spine.assetbundls.json +1 -0
  17. package/config/models/spine.azurlane.json +1 -0
  18. package/config/models/spine.azurlane_bundle.json +1 -0
  19. package/config/models/spine.azurlane_spine.json +1 -0
  20. package/config/models/spine.bd2.json +1 -0
  21. package/config/models/spine.bluearchive.json +1 -0
  22. package/config/models/spine.live2d.json +1 -0
  23. package/config/models/spine.nikke.json +1 -0
  24. package/config/models/spine.nikke_2.json +1 -0
  25. package/config/models/spine.npc.json +1 -0
  26. package/config/names/NIKKE.json +1058 -0
  27. package/config/names/README.md +47 -0
  28. package/config/names//345/244/251/345/221/275/344/271/213/345/255/220.json +42897 -0
  29. package/config/names//346/243/225/350/211/262/345/260/230/345/237/2032.json +4016 -0
  30. package/config/names//347/242/247/350/223/235/350/210/252/347/272/277.json +38342 -0
  31. package/config/plugins.json +13 -0
  32. package/config/resources.example.json +40 -0
  33. package/package.json +63 -4
  34. package/plugins/unity-live2d-bundle/README.md +98 -0
  35. package/plugins/unity-live2d-bundle/index.js +891 -0
  36. package/plugins/unity-live2d-bundle/lib/anim-to-motion.js +402 -0
  37. package/plugins/unity-live2d-bundle/lib/bundle-probe.js +244 -0
  38. package/plugins/unity-live2d-bundle/lib/bytes.js +189 -0
  39. package/plugins/unity-live2d-bundle/lib/common-strings.js +46 -0
  40. package/plugins/unity-live2d-bundle/lib/dxt.js +197 -0
  41. package/plugins/unity-live2d-bundle/lib/errors.js +71 -0
  42. package/plugins/unity-live2d-bundle/lib/etc2.js +371 -0
  43. package/plugins/unity-live2d-bundle/lib/extract.js +242 -0
  44. package/plugins/unity-live2d-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
  45. package/plugins/unity-live2d-bundle/lib/lzma-d-min.js +34 -0
  46. package/plugins/unity-live2d-bundle/lib/model3.js +137 -0
  47. package/plugins/unity-live2d-bundle/lib/png.js +424 -0
  48. package/plugins/unity-live2d-bundle/lib/serialized.js +233 -0
  49. package/plugins/unity-live2d-bundle/lib/texdecode.js +190 -0
  50. package/plugins/unity-live2d-bundle/lib/typetree.js +215 -0
  51. package/plugins/unity-live2d-bundle/lib/unityfs.js +396 -0
  52. package/plugins/unity-live2d-bundle/plugin.json +81 -0
  53. package/plugins/unity-live2d-bundle/profiles/unityfs-live2d.json +26 -0
  54. package/plugins/unity-spine-bundle/index.js +535 -0
  55. package/plugins/unity-spine-bundle/lib/bytes.js +189 -0
  56. package/plugins/unity-spine-bundle/lib/common-strings.js +46 -0
  57. package/plugins/unity-spine-bundle/lib/dxt.js +197 -0
  58. package/plugins/unity-spine-bundle/lib/errors.js +79 -0
  59. package/plugins/unity-spine-bundle/lib/etc2.js +371 -0
  60. package/plugins/unity-spine-bundle/lib/extract.js +242 -0
  61. package/plugins/unity-spine-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
  62. package/plugins/unity-spine-bundle/lib/lzma-d-min.js +34 -0
  63. package/plugins/unity-spine-bundle/lib/png.js +424 -0
  64. package/plugins/unity-spine-bundle/lib/serialized.js +233 -0
  65. package/plugins/unity-spine-bundle/lib/texdecode.js +190 -0
  66. package/plugins/unity-spine-bundle/lib/typetree.js +215 -0
  67. package/plugins/unity-spine-bundle/lib/unityfs.js +396 -0
  68. package/plugins/unity-spine-bundle/plugin.json +75 -0
  69. package/src/server/README.md +69 -0
  70. package/src/server/assets/browse.mjs +56 -0
  71. package/src/server/assets/bundle-probe.mjs +94 -0
  72. package/src/server/assets/probe.mjs +184 -0
  73. package/src/server/core/config.mjs +173 -0
  74. package/src/server/core/env-num.mjs +60 -0
  75. package/src/server/core/env.mjs +49 -0
  76. package/src/server/core/fs-portable.mjs +37 -0
  77. package/src/server/core/log.mjs +107 -0
  78. package/src/server/core/paths.mjs +44 -0
  79. package/src/server/data/catalog.mjs +1091 -0
  80. package/src/server/data/entry-states.mjs +499 -0
  81. package/src/server/data/live2d.mjs +272 -0
  82. package/src/server/data/manifest-cache.mjs +58 -0
  83. package/src/server/data/manifest-stamp.mjs +135 -0
  84. package/src/server/data/model-meta.mjs +114 -0
  85. package/src/server/data/names.mjs +106 -0
  86. package/src/server/data/shape-probe-worker.mjs +33 -0
  87. package/src/server/data/shape-probe.mjs +626 -0
  88. package/src/server/data/spine.mjs +336 -0
  89. package/src/server/data/tree.mjs +126 -0
  90. package/src/server/http/atomic-json.mjs +145 -0
  91. package/src/server/http/background-solids.mjs +270 -0
  92. package/src/server/http/backgrounds.mjs +388 -0
  93. package/src/server/http/diag-upload.mjs +264 -0
  94. package/src/server/http/diag.mjs +173 -0
  95. package/src/server/http/ready.mjs +167 -0
  96. package/src/server/http/static.mjs +312 -0
  97. package/src/server/http/write.mjs +654 -0
  98. package/src/server/index.mjs +114 -0
  99. package/src/server/plugins/builtin-providers.json +30 -0
  100. package/src/server/plugins/registry.mjs +650 -0
  101. package/src/server/plugins/slot-catalog.mjs +286 -0
  102. package/src/server/routes/background-solids.mjs +56 -0
  103. package/src/server/routes/backgrounds.mjs +77 -0
  104. package/src/server/routes/config.mjs +38 -0
  105. package/src/server/routes/data.mjs +313 -0
  106. package/src/server/routes/diag.mjs +37 -0
  107. package/src/server/routes/entries.mjs +170 -0
  108. package/src/server/routes/ready.mjs +122 -0
  109. package/src/server/serve.mjs +396 -0
  110. package/src/server/unpack/capacity.mjs +217 -0
  111. package/src/server/unpack/mem-probe.mjs +27 -0
  112. package/src/server/unpack/plugin-runner.mjs +529 -0
  113. package/src/server/unpack/plugin-store.mjs +736 -0
  114. package/src/server/unpack/probe-store.mjs +73 -0
  115. package/src/server/unpack/routes.mjs +359 -0
  116. package/src/server/unpack/runtime/exec.mjs +558 -0
  117. package/src/server/unpack/runtime/index.mjs +141 -0
  118. package/src/server/unpack/runtime/node-vm.mjs +111 -0
  119. package/src/server/unpack/unpack-worker.mjs +105 -0
  120. package/src/server/unpack/worker-client.mjs +184 -0
  121. package/src/server/unpack/write-result.mjs +84 -0
  122. package/src/shared/bundle-shape.mjs +83 -0
  123. package/src/shared/entry-reasons.mjs +119 -0
  124. package/src/shared/names.mjs +45 -0
  125. package/src/shared/type-labels.mjs +31 -0
  126. package/src/web/README.md +193 -0
  127. package/src/web/app/anim-list.js +222 -0
  128. package/src/web/app/background-solids.js +150 -0
  129. package/src/web/app/backgrounds.js +152 -0
  130. package/src/web/app/camera-log.js +28 -0
  131. package/src/web/app/context.js +72 -0
  132. package/src/web/app/dir-picker.js +242 -0
  133. package/src/web/app/entry-report.js +71 -0
  134. package/src/web/app/memory-bars.js +91 -0
  135. package/src/web/app/memory-budget.js +228 -0
  136. package/src/web/app/memory-guard.js +204 -0
  137. package/src/web/app/model-actions.js +682 -0
  138. package/src/web/app/renderer-setting.js +70 -0
  139. package/src/web/app/root-manager.js +254 -0
  140. package/src/web/app/status.js +138 -0
  141. package/src/web/app/topbar-actions.js +357 -0
  142. package/src/web/app/transport-actions.js +297 -0
  143. package/src/web/app/ui-state.js +559 -0
  144. package/src/web/app/url-params.js +171 -0
  145. package/src/web/app/view-settings.js +186 -0
  146. package/src/web/components.css +524 -0
  147. package/src/web/debug/diag-hud.js +95 -0
  148. package/src/web/debug/drag.js +369 -0
  149. package/src/web/debug/log.js +320 -0
  150. package/src/web/debug/model-cmp.js +292 -0
  151. package/src/web/debug/panel.css +474 -0
  152. package/src/web/debug/panel.js +712 -0
  153. package/src/web/debug/race-probe.js +82 -0
  154. package/src/web/debug/upload.js +130 -0
  155. package/src/web/engine/adapters/atlas.js +47 -0
  156. package/src/web/engine/adapters/index.js +22 -0
  157. package/src/web/engine/adapters/renderer.js +223 -0
  158. package/src/web/engine/adapters/skeleton.js +105 -0
  159. package/src/web/engine/adapters/texture.js +64 -0
  160. package/src/web/engine/animation.js +181 -0
  161. package/src/web/engine/appearance.js +136 -0
  162. package/src/web/engine/camera-adapter.js +212 -0
  163. package/src/web/engine/effect-chain.js +138 -0
  164. package/src/web/engine/engine-state.js +120 -0
  165. package/src/web/engine/filters.js +224 -0
  166. package/src/web/engine/fit.js +197 -0
  167. package/src/web/engine/frame-gate.js +33 -0
  168. package/src/web/engine/gl-driver.js +56 -0
  169. package/src/web/engine/gl.js +77 -0
  170. package/src/web/engine/index.js +20 -0
  171. package/src/web/engine/loader.js +55 -0
  172. package/src/web/engine/model-info.js +26 -0
  173. package/src/web/engine/pipeline.js +200 -0
  174. package/src/web/engine/player.js +427 -0
  175. package/src/web/engine/pma/blend.js +76 -0
  176. package/src/web/engine/pma/detect.js +118 -0
  177. package/src/web/engine/pma/index.js +8 -0
  178. package/src/web/engine/pma/resolve.js +112 -0
  179. package/src/web/engine/pma/upload.js +127 -0
  180. package/src/web/engine/pma.js +5 -0
  181. package/src/web/engine/render-frame.js +93 -0
  182. package/src/web/engine/runtime-capabilities.js +209 -0
  183. package/src/web/engine/runtime.js +37 -0
  184. package/src/web/engine/skins.js +165 -0
  185. package/src/web/engine/textures.js +174 -0
  186. package/src/web/index.html +117 -0
  187. package/src/web/infra/api.js +531 -0
  188. package/src/web/infra/diag.js +509 -0
  189. package/src/web/infra/document.js +269 -0
  190. package/src/web/infra/dom.js +31 -0
  191. package/src/web/infra/events.mjs +27 -0
  192. package/src/web/infra/log.js +153 -0
  193. package/src/web/infra/mem-stats.js +112 -0
  194. package/src/web/infra/plugin-host-registration.mjs +285 -0
  195. package/src/web/infra/plugin-host-report.mjs +32 -0
  196. package/src/web/infra/plugin-host-unload.mjs +79 -0
  197. package/src/web/infra/plugin-host-unpack.mjs +899 -0
  198. package/src/web/infra/plugin-host-warmup.mjs +180 -0
  199. package/src/web/infra/plugin-host.js +628 -0
  200. package/src/web/infra/ready.js +461 -0
  201. package/src/web/infra/schedule.js +51 -0
  202. package/src/web/infra/shortcuts.js +129 -0
  203. package/src/web/infra/slot-registry.js +402 -0
  204. package/src/web/infra/vfs.js +247 -0
  205. package/src/web/live2d/bridge.js +400 -0
  206. package/src/web/live2d/model.js +321 -0
  207. package/src/web/live2d/overlay.js +142 -0
  208. package/src/web/live2d/pixi-app.js +111 -0
  209. package/src/web/live2d/playback-probe.js +92 -0
  210. package/src/web/live2d/probe-geometry.js +329 -0
  211. package/src/web/live2d/probe.js +119 -0
  212. package/src/web/live2d/release.js +66 -0
  213. package/src/web/live2d/runtime-adapter.js +289 -0
  214. package/src/web/live2d/stage-ctx.js +152 -0
  215. package/src/web/live2d/stage.js +204 -0
  216. package/src/web/live2d/vfs-settings.js +174 -0
  217. package/src/web/live2d/view.js +89 -0
  218. package/src/web/main.js +629 -0
  219. package/src/web/panel/background-list.js +373 -0
  220. package/src/web/panel/bg-source.js +291 -0
  221. package/src/web/panel/color-picker.css +199 -0
  222. package/src/web/panel/color-picker.js +278 -0
  223. package/src/web/panel/dialog.css +116 -0
  224. package/src/web/panel/dialog.js +173 -0
  225. package/src/web/panel/dom.js +35 -0
  226. package/src/web/panel/icons.js +58 -0
  227. package/src/web/panel/memory-bar.js +195 -0
  228. package/src/web/panel/panel-settings.js +1263 -0
  229. package/src/web/panel/panel.css +1304 -0
  230. package/src/web/panel/render-choice.js +104 -0
  231. package/src/web/panel/schema.js +784 -0
  232. package/src/web/panel/settings-io.js +291 -0
  233. package/src/web/panel/shell-controls.js +274 -0
  234. package/src/web/panel/slider.js +210 -0
  235. package/src/web/panel/toolbar-schema.js +111 -0
  236. package/src/web/panel/toolbar.js +195 -0
  237. package/src/web/panel/transport-picker.js +375 -0
  238. package/src/web/panel/transport.js +321 -0
  239. package/src/web/panel/view.js +553 -0
  240. package/src/web/plugin/unpack-worker.js +185 -0
  241. package/src/web/render/camera/constants.js +60 -0
  242. package/src/web/render/camera/index.js +123 -0
  243. package/src/web/render/camera/spine.js +124 -0
  244. package/src/web/render/capture.js +30 -0
  245. package/src/web/render/interaction.js +527 -0
  246. package/src/web/render/live2d.js +627 -0
  247. package/src/web/render/playback.js +47 -0
  248. package/src/web/render/reset-view.js +29 -0
  249. package/src/web/render/spine.js +386 -0
  250. package/src/web/render/surface-switch.js +176 -0
  251. package/src/web/render/surface.js +571 -0
  252. package/src/web/render/surfaces.js +70 -0
  253. package/src/web/roster/filter.js +31 -0
  254. package/src/web/roster/list.js +276 -0
  255. package/src/web/roster/persist.js +92 -0
  256. package/src/web/roster/reveal.js +77 -0
  257. package/src/web/roster/rows.js +248 -0
  258. package/src/web/roster/snapshot.js +39 -0
  259. package/src/web/roster/type-filter.js +97 -0
  260. package/src/web/roster/view.js +761 -0
  261. package/src/web/spine/canvas-api-groups.js +267 -0
  262. package/src/web/spine/canvas-api.js +97 -0
  263. package/src/web/spine/canvas-document.js +80 -0
  264. package/src/web/spine/canvas-engine.js +193 -0
  265. package/src/web/spine/canvas-entry.js +28 -0
  266. package/src/web/spine/canvas-pixels.js +36 -0
  267. package/src/web/spine/canvas-runtime.js +127 -0
  268. package/src/web/spine/canvas-view.js +108 -0
  269. package/src/web/spine/canvas.js +7 -0
  270. package/src/web/spine/runtime-hint.js +73 -0
  271. package/src/web/spine/runtimes.js +212 -0
  272. package/src/web/style.css +493 -0
  273. package/tool/lib/bundle-probe-node.mjs +121 -0
  274. package/tool/lib/paths.mjs +20 -0
  275. package/tool/lib/runtime-guides.mjs +65 -0
  276. package/tool/lib/runtime-package.mjs +403 -0
  277. package/tool/lib/spine-meta.mjs +227 -0
  278. package/tool/lib/spine-motions.mjs +104 -0
  279. package/tool/parse-jsonc.mjs +86 -0
  280. package/tool/resolve-live2d-runtimes.mjs +276 -0
  281. package/tool/spine-motion-rules.json +101 -0
@@ -0,0 +1,180 @@
1
+ // src/web/infra/plugin-host-warmup.mjs —— 预热链(从 plugin-host.js 抽出,B1)
2
+ //
3
+ // 为什么单独成模块:预热是"选中即触发"的后台任务,它的全部状态(容量缓存、在途去重、阶段文案)
4
+ // 只服务于这一件事;搬出去后 plugin-host 里只剩"对外暴露 warmup"。
5
+ //
6
+ // 依赖约定:
7
+ // · ctx —— 只用到 ctx.say / ctx.emit(工具按调研结论仍随 ctx 传入;本模块不读状态)
8
+ // · postStream —— 服务端事件流(由 ./api.js 提供)
9
+ // · memoryGuard —— 内存保护开关(决定 allowOverLimit;由 ../app/memory-guard.js 提供)
10
+ // 模块自有状态(低频,随模块走):capCache / CAP_TTL_MS / warmupInflight / WARMUP_PHASE_TEXT。
11
+ //
12
+ // ⚠️ 行为契约(验收口径,搬迁时逐字保留):预热**只看内存**、不设时间门槛;
13
+ // 去重命中时**静默返回**(连 diag 也不发);失败一律记 info 不记 error。
14
+
15
+ import * as memoryGuard from '../app/memory-guard.js';
16
+ import { postStream } from './api.js';
17
+
18
+ /**
19
+ * @param {{ctx: object}} deps
20
+ * @returns {(file: {rootId: string, rel: string, size?: number}) => void} warmup
21
+ */
22
+ export function createWarmup({ ctx }) {
23
+ /** 容量探测缓存(60s);失败时**保守不放行**(reason=fallback),绝不当成"随便解包" */
24
+ let capCache = null;
25
+ const CAP_TTL_MS = 60 * 1000;
26
+
27
+ /**
28
+ * 问服务端"这个包现在能预热吗"。判断全在服务端:可用内存**现取** × 0.5 预算 vs 预估峰值,
29
+ * 预热**只看内存**(时间不再是门槛:旧规则"预估 >6s 不预热"会让 15MB 以上的包永远进不了缓存)。\n * 前端只读 `allowWarmup` 一个布尔,不自己算任何阈值。
30
+ * @param {{rootId: string, rel: string}} file
31
+ * @returns {Promise<{allowWarmup: boolean, reason: string, peakEstMb?: number, budgetMb?: number, estUnpackSec?: number}>}
32
+ */
33
+ async function fetchCapacity(file) {
34
+ const key = `${file.rootId}/${file.rel}`;
35
+ if (capCache && capCache.key === key && Date.now() - capCache.at < CAP_TTL_MS) return capCache.value;
36
+ const fallback = { allowWarmup: false, reason: 'fallback:capacity-unavailable' };
37
+ try {
38
+ const qs = `?rootId=${encodeURIComponent(file.rootId)}&rel=${encodeURIComponent(file.rel)}`;
39
+ const res = await fetch(`/api/unpack/capacity${qs}`, { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(1500) });
40
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
41
+ const j = await res.json();
42
+ const value = {
43
+ allowWarmup: j.allowWarmup === true,
44
+ reason: String(j.reason || 'unknown'),
45
+ peakEstMb: j.peakEstMb,
46
+ budgetMb: j.budgetMb,
47
+ estUnpackSec: j.estUnpackSec,
48
+ // 判据显示要贴真实值:可用内存与总内存一起带出来(不在这里重算任何阈值)
49
+ memAvailableMb: j.memAvailableMb,
50
+ memTotalMb: j.memTotalMb,
51
+ };
52
+ capCache = { at: Date.now(), key, value };
53
+ return value;
54
+ } catch {
55
+ capCache = { at: Date.now(), key, value: fallback };
56
+ return fallback;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * 预热:**选中即触发**,让服务端把**当前这一个包**解包落盘,供下次命中。
62
+ * 绝不预热整个根(19 个包 × 10s 白烧)。
63
+ *
64
+ * 两条必须:
65
+ * · 只预热当前包;
66
+ * · **必打 diag 日志** `unpack.warmup {rootId, rel, sizeMb, skipped?, ms}`,否则"为什么这个包没预热"无从查证。
67
+ * 不加任何时间门槛:能不能预热只由内存准入决定(服务端给 allowWarmup),不看预估耗时、不延迟。
68
+ * @param {{rootId: string, rel: string, size?: number}} file
69
+ */
70
+ function warmup(file) {
71
+ const sizeMb = Number(file && file.size) / 1048576;
72
+ const emitWarm = (extra) => ctx.emit('unpack.warmup', { rootId: file && file.rootId, rel: file && file.rel, sizeMb: +sizeMb.toFixed(1), ...extra });
73
+ if (!file || !file.rootId || !file.rel) return;
74
+ // 上限来自服务端(CPU 维度与内存维度取小);拿不到则保守 12
75
+ fetchCapacity(file).then((cap) => {
76
+ if (!cap.allowWarmup) {
77
+ emitWarm({ skipped: cap.reason, peakEstMb: cap.peakEstMb, budgetMb: cap.budgetMb, estUnpackSec: cap.estUnpackSec });
78
+ ctx.say.info(`跳过预热:${file.rel} ${sizeMb.toFixed(1)}MB(${cap.reason};预估峰值 ${cap.peakEstMb}MB / 预算 ${cap.budgetMb}MB / 预估耗时 ${cap.estUnpackSec}s)`);
79
+ return;
80
+ }
81
+ scheduleWarmup(file, emitWarm, cap);
82
+ }).catch(() => {
83
+ // fetchCapacity 内部已有兜底(allowWarmup=false),这里防御未知异常:保守跳过
84
+ emitWarm({ skipped: 'fallback:capacity-unavailable' });
85
+ });
86
+ }
87
+
88
+ /**
89
+ * 在途预热去重:key = `${rootId}/${rel}`。
90
+ *
91
+ * 为什么必须去重:`model-actions` 每次装载都会调 warmup(),同一个模型被装载多次就排多次请求;
92
+ * 而服务端的解包是**串行**的(plugin-store 的 inflight + 队列),后到的那几个全部 join 到
93
+ * 同一个 inflight 上 —— 于是每个都等足同一段时长、每个都被前台的 8.5s 掐断一次、每个再重试一次。
94
+ * 实测(500 条日志那批):同一个 rel 被重复预热 4~6 次
95
+ * xukufu_3_hx:ms=1969 / 19933 / 12770 / 18598(hit=true) 四条,全是同一次解包的不同等待者。
96
+ *
97
+ * 为什么"完成即删"而不是"永久记住已预热":插件版本升级、服务端缓存被清(LRU/手工删目录)之后,
98
+ * 同一个包仍然需要能重新预热 —— 记死就再也不会补了。
99
+ */
100
+ const warmupInflight = new Set();
101
+
102
+ /**
103
+ * `phase.name` → 界面措辞(契约 §四 ④:服务端只发 read/resolve/write 三个名字,文案归前端)。
104
+ * 放这一处常量、别散落到各处:措辞变了只改这里,也避免服务端被逼着下发界面文字。
105
+ * 注意**没有百分比**:解压是一次性整段,服务端给不出可信进度,假进度条比没有更糟。
106
+ */
107
+ const WARMUP_PHASE_TEXT = { read: '读盘中', resolve: '解压中', write: '写盘中' };
108
+
109
+ /**
110
+ * 真正执行预热:**选中即触发**,不等空闲、不延迟。
111
+ *
112
+ * 为什么删掉了 requestIdleCallback(timeout 3000) + setTimeout(2000):
113
+ * 那两个限制加起来最坏 5s 后才开始预热,而页面在 9s 就已经要加载同一个包了 ——
114
+ * 首次打开任何新资源,缓存都来不及做好,前端只能自己解一遍(实测 4.3s / 19MB 白花)。
115
+ * 服务端解包是原生能力,不是"抢 CPU 的备选",不该被推迟到空闲。
116
+ */
117
+ function scheduleWarmup(file, emitWarm, cap) {
118
+ const key = `${file.rootId}/${file.rel}`;
119
+ // 去重命中时**静默返回**(连 diag 也不发):验收口径是「unpack.warmup 条数 == 不同 rel 个数」,
120
+ // 这里多发一条就等于把噪音从网络层搬到日志层。say 没有 debug 级,所以干脆不说话。
121
+ if (warmupInflight.has(key)) return;
122
+ warmupInflight.add(key);
123
+ (async () => {
124
+ const t0 = performance.now();
125
+ let events = 0;
126
+ try {
127
+ // 预热是**后台**任务,不能借用前台的判失败策略:
128
+ // · 不重试 —— 服务端解包是串行的,重发只会把队列压得更长。postStream 不走 fetchWithRetry,
129
+ // 天生不重试,所以这一条与上一轮定的 `retries: 0` 等价(那条约定的目的不变)。
130
+ // · **不设总时长上限** —— 慢机器解 30s、60s 都属正常。判失败只看「有没有进展」:
131
+ // postStream 每收到任意一帧(含 ping)就重置停摆计时,连续 15s 无帧才中止(契约 §四)。
132
+ // 上一轮那个 120s 固定上限是"没有进展信号时"的妥协;有了事件流就该撤掉它。
133
+ const j = await postStream('/api/unpack/resolve',
134
+ { rootId: file.rootId, rel: file.rel, allowOverLimit: !memoryGuard.isMemoryGuardOn(), stream: true },
135
+ {
136
+ onEvent: (name, data) => {
137
+ events += 1;
138
+ // 进度只进**现有**日志/状态行,不新造 UI 控件;也不给百分比(服务端给不出可信的)
139
+ if (name === 'queued') ctx.say.info(`预热排队中:${file.rel}(前面约 ${Number((data && data.waiting) || 0)} 个)`);
140
+ else if (name === 'start') ctx.say.info(`预热开始解包:${file.rel}`);
141
+ else if (name === 'phase') {
142
+ const ph = (data && data.name) || '';
143
+ ctx.say.info(`预热${WARMUP_PHASE_TEXT[ph] || `${ph || '进行'}中`}:${file.rel}(${Number((data && data.ms) || 0)}ms)`);
144
+ }
145
+ // ping 不显示:它只用于维持停摆计时(postStream 内部每帧都会重置计时器)
146
+ },
147
+ onStall: () => {
148
+ // 措辞铁律(契约 §四):这里是"停摆",不是"超时"
149
+ ctx.say.info(`预热停摆(15s 无事件),已放弃等待;服务端仍会解完并写缓存:${file.rel}`);
150
+ },
151
+ });
152
+ emitWarm({
153
+ ok: !!j.ok,
154
+ hit: !!(j && j.hit),
155
+ peakEstMb: cap && cap.peakEstMb,
156
+ ms: Math.round(performance.now() - t0),
157
+ events,
158
+ ...(j && j.reason ? { reason: j.reason } : {}),
159
+ });
160
+ if (j && j.ok) {
161
+ // done 即完成:hit:true(已有缓存)与 hit:false(本次已落盘)都算成功
162
+ ctx.say.info(`预热完成:${file.rel}(${j.hit ? '已有缓存' : '已落盘'},${Math.round(performance.now() - t0)}ms)`);
163
+ } else {
164
+ // 流内 error / 停摆:都只记 info —— 预热失败不是错误,前台装载有自己的路径
165
+ ctx.say.info(`后台预热未完成:${file.rel}(${(j && j.message) || (j && j.reason) || '未知原因'})—— 服务端仍会解完并写缓存,下次命中即可`);
166
+ }
167
+ } catch (e) {
168
+ const why = String((e && e.message) || e).slice(0, 120);
169
+ emitWarm({ ok: false, error: why, events });
170
+ // 预热失败**不是错误**:前台装载有自己的路径(本地解包 / 服务端命中),预热只是"下次更快"。
171
+ // 记 info 不记 error/warn —— 否则每次装载都在日志里刷一条红,把真问题淹掉。
172
+ ctx.say.info(`后台预热未完成:${file.rel}(${why})—— 服务端仍会解完并写缓存,下次命中即可`);
173
+ } finally {
174
+ // 成功/失败/超时都要移除,否则一次失败会让这个 rel 永远不再预热
175
+ warmupInflight.delete(key);
176
+ }
177
+ })();
178
+ }
179
+ return warmup;
180
+ }