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,173 @@
1
+ // panel/dialog.js —— 自绘弹窗(文本输入 / 危险确认),零依赖、纯逻辑
2
+ //
3
+ // 为什么要自绘而不是浏览器原生弹窗(输入框 / 确认框):
4
+ // ① 原生弹窗是**同步阻塞**的,会把整个 JS 线程停住 —— 阻塞期间渲染循环停摆、
5
+ // 就绪信号不推进、日志不落盘,与项目契约《事件驱动优先》直接冲突;
6
+ // ② 原生 chrome 的字体/配色/按钮文案全由浏览器决定(英文环境下会变成 Cancel/OK),
7
+ // 深色面板弹出一个系统白底窗口,视觉断层,手机上还可能被虚拟键盘顶飞;
8
+ // ③ DOM 级弹窗可以直接查元素断言,不需要 stub 全局函数 —— 自动化更稳。
9
+ //
10
+ // 两条硬纪律(踩过就会懂):
11
+ // · **任何**关闭路径(确定/取消/Esc/点遮罩/被新弹窗顶掉)都必须 resolve,
12
+ // 否则调用方的 await 永远挂住,表现为"点了没反应";
13
+ // · 同一时刻只允许一个弹窗:重复调用先把前一个 resolve 掉再开新的。
14
+
15
+ import { el } from './dom.js';
16
+
17
+ /** 样式表只注入一次(面板会重建,重复插 <link> 浪费) */
18
+ const CSS_HREF = 'panel/dialog.css';
19
+ const CSS_ATTR = 'data-dialog-css';
20
+
21
+ /** 当前打开的弹窗句柄(单例约束) */
22
+ let current = null;
23
+
24
+ /** 注入样式表(幂等;失败不阻塞弹窗可用) */
25
+ function ensureStylesheet(doc) {
26
+ if (doc.querySelector(`link[${CSS_ATTR}]`)) return;
27
+ const link = doc.createElement('link');
28
+ link.rel = 'stylesheet';
29
+ link.href = CSS_HREF;
30
+ link.setAttribute(CSS_ATTR, '1');
31
+ (doc.head || doc.documentElement).append(link);
32
+ }
33
+
34
+ /**
35
+ * 打开一个弹窗并在关闭时 resolve。
36
+ * @param {object} spec
37
+ * @param {string} spec.title 标题
38
+ * @param {string} [spec.message] 说明文字
39
+ * @param {boolean} [spec.withInput] 是否带输入框
40
+ * @param {string} [spec.value] 输入框初始值
41
+ * @param {string} [spec.placeholder] 占位提示
42
+ * @param {number} [spec.maxlength] 最大长度(带实时字数反馈)
43
+ * @param {boolean} [spec.danger] 危险操作(确定键用危险色 + 默认焦点给取消)
44
+ * @param {string} [spec.confirmText] 确定键文案
45
+ * @param {string} [spec.cancelText] 取消键文案
46
+ * @returns {Promise<any>} 带输入 ⇒ 字符串或 null;确认 ⇒ boolean
47
+ */
48
+ function openDialog(spec) {
49
+ // 单例:先把上一个按"取消"收掉,避免两个遮罩叠放且其 Promise 悬空
50
+ if (current) current.close(spec.withInput ? null : false);
51
+
52
+ const doc = document;
53
+ ensureStylesheet(doc);
54
+
55
+ const mask = el('div', 'dlg-mask');
56
+ const panel = el('div', 'dlg-panel');
57
+ panel.setAttribute('role', 'dialog');
58
+ panel.setAttribute('aria-modal', 'true');
59
+
60
+ const titleEl = el('p', 'dlg-title', spec.title || '');
61
+ panel.append(titleEl);
62
+ if (spec.message) panel.append(el('p', 'dlg-message', spec.message));
63
+
64
+ let input = null;
65
+ let counter = null;
66
+ if (spec.withInput) {
67
+ const row = el('div', 'dlg-input-row');
68
+ input = el('input', 'dlg-input');
69
+ input.type = 'text';
70
+ input.value = spec.value || '';
71
+ input.placeholder = spec.placeholder || '';
72
+ if (spec.maxlength) input.maxLength = spec.maxlength;
73
+ input.setAttribute('aria-label', spec.title || '输入');
74
+ row.append(input);
75
+ if (spec.maxlength) {
76
+ // 实时字数:没有反馈用户不知道为什么打不进去
77
+ counter = el('span', 'dlg-counter', `${input.value.length}/${spec.maxlength}`);
78
+ row.append(counter);
79
+ }
80
+ panel.append(row);
81
+ }
82
+
83
+ const actions = el('div', 'dlg-actions');
84
+ const btnCancel = el('button', 'dlg-btn dlg-btn--cancel', spec.cancelText || '取消');
85
+ btnCancel.type = 'button';
86
+ const btnOk = el('button', `dlg-btn dlg-btn--primary${spec.danger ? ' dlg-btn--danger' : ''}`, spec.confirmText || '确定');
87
+ btnOk.type = 'button';
88
+ actions.append(btnCancel, btnOk);
89
+ panel.append(actions);
90
+ mask.append(panel);
91
+ doc.body.append(mask);
92
+
93
+ let closed = false;
94
+ return new Promise((resolve) => {
95
+ /** 统一的关闭出口:保证只 resolve 一次,并摘掉全局监听 */
96
+ const finish = (ok) => {
97
+ if (closed) return;
98
+ closed = true;
99
+ doc.removeEventListener('keydown', onKey, true);
100
+ try { mask.remove(); } catch { /* 已被移除 */ }
101
+ if (current === handle) current = null;
102
+ if (spec.withInput) resolve(ok ? String(input.value || '').trim() : null);
103
+ else resolve(!!ok);
104
+ };
105
+
106
+ /** Enter 提交 / Esc 取消(捕获阶段,避免被页面其它快捷键抢先) */
107
+ function onKey(ev) {
108
+ if (ev.key === 'Escape') { ev.stopPropagation(); finish(false); return; }
109
+ if (ev.key === 'Enter' && spec.withInput && ev.target === input) {
110
+ ev.preventDefault();
111
+ ev.stopPropagation();
112
+ finish(true); // 输入框里按 Enter = 确定
113
+ }
114
+ }
115
+ doc.addEventListener('keydown', onKey, true);
116
+
117
+ btnCancel.addEventListener('click', () => finish(false));
118
+ btnOk.addEventListener('click', () => finish(true));
119
+ mask.addEventListener('click', (ev) => { if (ev.target === mask) finish(false); });
120
+
121
+ if (input) {
122
+ input.addEventListener('input', () => {
123
+ // 粘贴/程序化赋值可能绕过 maxlength ⇒ 这里再夹一次并同步字数,避免出现 "40/30"
124
+ if (spec.maxlength && input.value.length > spec.maxlength) {
125
+ input.value = input.value.slice(0, spec.maxlength);
126
+ }
127
+ if (counter && spec.maxlength) counter.textContent = `${input.value.length}/${spec.maxlength}`;
128
+ });
129
+ // 打开即聚焦并**全选原值**:改名时用户能直接打字覆盖
130
+ try {
131
+ input.focus();
132
+ input.select();
133
+ } catch { /* 忽略 */ }
134
+ }
135
+
136
+ const handle = { close: (ok) => finish(ok) };
137
+ current = handle;
138
+
139
+ // danger 时默认焦点给「取消」:防误触把图片删掉
140
+ try {
141
+ if (spec.danger) btnCancel.focus();
142
+ else if (!spec.withInput) btnOk.focus();
143
+ } catch { /* 忽略 */ }
144
+ });
145
+ }
146
+
147
+ /**
148
+ * 文本输入弹窗。
149
+ * @param {object} opts 见 openDialog
150
+ * @returns {Promise<string|null>} 确定 ⇒ 去空白后的文本;取消/关闭 ⇒ null
151
+ */
152
+ export function askText(opts = {}) {
153
+ return openDialog({
154
+ ...opts,
155
+ withInput: true,
156
+ value: opts.value || '',
157
+ placeholder: opts.placeholder || '',
158
+ maxlength: opts.maxlength === undefined ? 30 : opts.maxlength,
159
+ });
160
+ }
161
+
162
+ /**
163
+ * 确认弹窗。
164
+ * @param {object} opts { title, message, danger, confirmText, cancelText }
165
+ * @returns {Promise<boolean>} 确定 ⇒ true;取消/关闭 ⇒ false
166
+ */
167
+ export function askConfirm(opts = {}) {
168
+ return openDialog({
169
+ ...opts,
170
+ withInput: false,
171
+ danger: !!opts.danger,
172
+ });
173
+ }
@@ -0,0 +1,35 @@
1
+ // panel/dom.js —— 面板 DOM 小工具(view.js 与各控件渲染模块共用)
2
+ //
3
+ // 拆出来的原因:view.js 有行数上限,控件渲染要分模块;共用的三个小工具集中放这里,
4
+ // 避免各模块各写一份导致行为漂移。
5
+
6
+ // el 的唯一实现在 infra/dom.js;这里 re-export,本模块的 9 个 import 方无需改动。
7
+ // 判据是"函数体只有一份",不是"文本只出现一次" —— 别再在这里加回一份实现。
8
+ export { el } from '../infra/dom.js';
9
+
10
+ /** 未接线标记(data-pending + 可选 id) */
11
+ export function markPending(node, id) {
12
+ node.dataset.pending = 'true';
13
+ if (id) node.id = id;
14
+ return node;
15
+ }
16
+
17
+ /**
18
+ * 标成不可点控件,并区分两种原因:
19
+ * · 能力不支持(schema 过滤给了 reason)→ 写 reason,不打"待接线"
20
+ * · 未接线 → data-pending + 统一的 pending 文案
21
+ * @param {HTMLElement} node
22
+ * @param {object} ctl 控件定义
23
+ * @param {{disabledReason?: Function}} ctx
24
+ */
25
+ export function markDisabled(node, ctl, ctx) {
26
+ node.disabled = true;
27
+ const reason = ctx && ctx.disabledReason ? ctx.disabledReason(ctl.id) : null;
28
+ if (reason) {
29
+ node.title = `${reason}:${ctl.label || ''}`.trim();
30
+ node.dataset.disabledReason = reason; // 便于断言与排查
31
+ return;
32
+ }
33
+ markPending(node, null);
34
+ node.title = '待接线(M6)';
35
+ }
@@ -0,0 +1,58 @@
1
+ // panel/icons.js —— 面板内联图标(不引图标库,与顶栏/底栏同一套线条风格)
2
+ //
3
+ // 单独成模块的原因:panel/view.js 要守 400 行上限,图标只是数据 + 一个小工厂。
4
+
5
+ /** 设置动作按钮的图标(按按钮文字取,避免再维护一套 id 表) */
6
+ export const ACTION_ICONS = {
7
+ 导出设置: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polygon points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>',
8
+ 导入设置: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polygon points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>',
9
+ 恢复默认: '<path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 3v6h6"/>',
10
+ };
11
+
12
+ /**
13
+ * 顶栏图标。形状取自 lucide 图标库的同名组件(PanelLeft / PanelRight):
14
+ * PanelLeft = rect(3,3,18,18,r2) + path M9 3v18
15
+ * PanelRight = rect(3,3,18,18,r2) + path M15 3v18
16
+ * 顶栏按钮在 index.html 里只放空 <svg> 占位,path 由 applyTopbarIcons() 从这里注入,
17
+ * 保证图标只有这一处来源。
18
+ */
19
+ export const TOPBAR_ICONS = {
20
+ panelLeft: '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/>',
21
+ panelRight: '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M15 3v18"/>',
22
+ };
23
+
24
+ /** 把顶栏图标的 path 注入到按钮里的 <svg>;返回注入个数 */
25
+ export function applyTopbarIcons(doc = document) {
26
+ const map = { 'btn-library': TOPBAR_ICONS.panelLeft, 'btn-menu': TOPBAR_ICONS.panelRight };
27
+ let filled = 0;
28
+ for (const [id, body] of Object.entries(map)) {
29
+ const svg = doc.querySelector(`#${id} svg`);
30
+ if (!svg) continue;
31
+ svg.innerHTML = body;
32
+ filled += 1;
33
+ }
34
+ return filled;
35
+ }
36
+
37
+ /** 分组标题图标 */
38
+ export const GROUP_ICONS = {
39
+ motion: '<path d="M4 4h16v16H4z"/><path d="M4 9h16"/><path d="m9 4 2 5"/><path d="m15 4 2 5"/><path d="M8 14h8v6H8z"/>',
40
+ // Shirt:lucide 图标库的 Shirt path
41
+ skin: '<path d="M20.38 3.46 16 2a4 4 0 0 1-8 0L3.62 3.46a2 2 0 0 0-1.34 2.23l.58 3.47a1 1 0 0 0 .99.84H6v10c0 1.1.9 2 2 2h8a2 2 0 0 0 2-2V10h2.15a1 1 0 0 0 .99-.84l.58-3.47a2 2 0 0 0-1.34-2.23z"/>',
42
+ play: '<polygon points="6 3 20 12 6 21 6 3"/>',
43
+ filter: '<path d="m5 3 7 9v6h2v-6l7-9H5z"/><path d="M12 18v3"/>',
44
+ pma: '<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="3"/>',
45
+ runtime: '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-2.9 1.2 2 2 0 1 1-4 0 1.7 1.7 0 0 0-2.9-1.2l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1A1.7 1.7 0 0 0 3 15a2 2 0 1 1 0-4 1.7 1.7 0 0 0 1.2-2.9l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1A1.7 1.7 0 0 0 10 4.6a2 2 0 1 1 4 0 1.7 1.7 0 0 0 2.9 1.2l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1A1.7 1.7 0 0 0 21 11a2 2 0 1 1 0 4z"/>',
46
+ theme: '<circle cx="13.5" cy="6.5" r="1.5"/><circle cx="17.5" cy="10.5" r="1.5"/><circle cx="8.5" cy="7.5" r="1.5"/><circle cx="6.5" cy="12.5" r="1.5"/><path d="M12 2a10 10 0 1 0 0 20 2.5 2.5 0 0 0 0-5 2.5 2.5 0 0 1 0-5h5a3 3 0 0 0 3-3 7 7 0 0 0-8-7z"/>',
47
+ debug: '<path d="M14.7 6.3a4 4 0 0 1 5 5L21 13l-8 8-6-6 8-8z"/><path d="m6 15-3 3 3 3 3-3"/>',
48
+ settings: '<path d="M4 6h10M18 6h2M4 12h4M12 12h8M4 18h10M18 18h2"/><circle cx="16" cy="6" r="2"/><circle cx="10" cy="12" r="2"/><circle cx="16" cy="18" r="2"/>',
49
+ };
50
+
51
+ /** 生成包住 svg 的 span(aria-hidden,装饰性) */
52
+ export function svgIcon(body) {
53
+ const span = document.createElement('span');
54
+ span.className = 'btn-icon';
55
+ span.setAttribute('aria-hidden', 'true');
56
+ span.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${body}</svg>`;
57
+ return span;
58
+ }
@@ -0,0 +1,195 @@
1
+ // panel/memory-bar.js —— 内存三色条(总 / 可用 / 服务端已占用),两处复用同一实现
2
+ //
3
+ // 数据只有一个来源:GET /api/unpack/capacity(不带 sourceMb)
4
+ // memTotalMb 物理总内存
5
+ // memAvailableMb MemAvailable(含可回收 buff/cache)
6
+ // rssMb 服务端进程已占用
7
+ // 这里**不显示 peakEstMb**(那是"某个包解包要多少"的预估,与"当前机器内存状况"是两件事)。
8
+ //
9
+ // 为什么不做定时刷新:项目契约要求事件驱动。数字停在拉取那一刻是预期行为 ——
10
+ // 用户要新数字,重新加载模型或重开设置面板即可。
11
+ //
12
+ // 无障碍:整条 role="img" + aria-label 写全三个数值;文字行**始终可见**(触摸端没有 hover)。
13
+ //
14
+ // 失败态(与"主动隐藏"是两件事,别混):
15
+ // · 拉取失败 / 数据不完整 ⇒ **保留占位**,条上写「内存:不可用(原因)」,
16
+ // 原因至少能分出 超时 / HTTP xxx / 数据不完整 三种(用户不必猜是接口挂了还是自己被关了保护)。
17
+ // · 开关关掉("显示状态信息" / 内存保护关)⇒ 走 hide(),整块不占位 —— 这是主动隐藏,维持原语义。
18
+ // · 下次轮询/刷新拿到数据 ⇒ 自动恢复正常条(清掉 unavailable 标记)。
19
+
20
+ import { el } from './dom.js';
21
+ import { stageLog } from '../app/status.js';
22
+
23
+ /**
24
+ * 把三个内存量拆成三段百分比,并保证**恰好合 100%**。
25
+ *
26
+ * 为什么需要专门修正:三段各自四舍五入后相加常是 99.99 或 100.01,
27
+ * 条子末尾会露一条缝(或溢出)—— 视觉上像渲染错误。所以余数补给最大的一段。
28
+ *
29
+ * @param {{total:number, server:number, avail:number}} m 三个量(MB)
30
+ * @returns {{server:number, other:number, avail:number}} 三段百分比(2 位小数,和恰为 100)
31
+ */
32
+ export function splitPercents(m) {
33
+ const total = Number(m && m.total) > 0 ? Number(m.total) : 0;
34
+ if (!total) return { server: 0, other: 100, avail: 0 };
35
+ const clamp = (v) => Math.max(0, Math.min(100, Number(v) || 0));
36
+ const round2 = (v) => Math.round(v * 100) / 100;
37
+ let server = clamp((Number(m.server) || 0) / total * 100);
38
+ let avail = clamp((Number(m.avail) || 0) / total * 100);
39
+ // 两者之和可能超过 100(rss 与 MemAvailable 口径不同,极端情况下会重叠)⇒ 按比例压回
40
+ if (server + avail > 100) {
41
+ const k = 100 / (server + avail);
42
+ server *= k;
43
+ avail *= k;
44
+ }
45
+ server = round2(server);
46
+ avail = round2(avail);
47
+ let other = round2(100 - server - avail);
48
+ // 余数修正:把 0.01 级别的误差补给**最大的一段**,避免条子末尾露缝
49
+ const drift = round2(100 - (server + other + avail));
50
+ if (drift !== 0) {
51
+ const biggest = Math.max(server, other, avail);
52
+ if (biggest === server) server = round2(server + drift);
53
+ else if (biggest === avail) avail = round2(avail + drift);
54
+ else other = round2(other + drift);
55
+ }
56
+ return { server, other, avail };
57
+ }
58
+
59
+ /** 建一条三色条的 DOM(不含数据):轨道 + 三段 + 文字行 */
60
+ function buildBar() {
61
+ const root = el('div', 'mem-bar');
62
+ root.setAttribute('role', 'img');
63
+ const track = el('div', 'mem-bar-track');
64
+ const segServer = el('span', 'mem-bar-seg mem-bar-seg--server');
65
+ const segOther = el('span', 'mem-bar-seg mem-bar-seg--other');
66
+ const segAvail = el('span', 'mem-bar-seg mem-bar-seg--avail');
67
+ track.append(segServer, segOther, segAvail);
68
+ const text = el('p', 'mem-bar-text', '');
69
+ root.append(track, text);
70
+ return { root, segServer, segOther, segAvail, text };
71
+ }
72
+
73
+ /**
74
+ * 挂一条内存条到容器里(容器由调用方提供;面板重建会换容器,所以刷新时现查)。
75
+ * @param {string} selector 容器选择器
76
+ * @returns {{refresh: () => Promise<boolean>, hide: () => void, root: () => HTMLElement|null}}
77
+ */
78
+ export function mountMemoryBar(selector) {
79
+ const getHost = () => document.querySelector(selector);
80
+
81
+ /** 现查(或首次创建)本容器的条子 */
82
+ const ensure = () => {
83
+ const host = getHost();
84
+ if (!host) return null;
85
+ let bar = host.querySelector('.mem-bar');
86
+ if (!bar) {
87
+ const built = buildBar();
88
+ host.replaceChildren(built.root);
89
+ bar = built.root;
90
+ }
91
+ return bar;
92
+ };
93
+
94
+ const hide = () => {
95
+ const host = getHost();
96
+ if (!host) return;
97
+ host.replaceChildren(); // 不留空壳:没数据就整块不占位
98
+ host.hidden = true;
99
+ };
100
+
101
+ /**
102
+ * 拉取失败时:**保留占位、把原因写在条上**,而不是整块消失。
103
+ *
104
+ * 为什么要这个(不再一律 hide):条子突然消失,用户分不清是哪一种情况 ——
105
+ * ① 接口挂了/超时 ② 服务端没数据 ③ 内存保护被关。三者在界面上曾长得一模一样。
106
+ * 与 `hide()` 的分工:hide() 只服务"开关关掉、本来就不该显示"这种**主动隐藏**;
107
+ * 拉取失败属于**该显示但显示不出**,要留着位置说明白。
108
+ *
109
+ * 硬约束:**绝不用 0 或上一次的旧值冒充当前值** —— 三段宽度归零、dataset 数值清空,
110
+ * 文字只写原因。宁可空轨道 + 一句原因,也不给假数字。
111
+ * @param {string} reason 已归类的可读原因(超时 / HTTP xxx / 数据不完整)
112
+ */
113
+ const showUnavailable = (reason) => {
114
+ const bar = ensure();
115
+ if (!bar) return false;
116
+ const host = getHost();
117
+ host.hidden = false; // 保留占位
118
+ bar.dataset.unavailable = '1'; // 供断言与排障:这是"不可用态",不是正常条
119
+ // 清掉上一次的数值痕迹,避免任何消费方(含断言快照)把旧数字当成当前值
120
+ delete bar.dataset.pctServer;
121
+ delete bar.dataset.pctOther;
122
+ delete bar.dataset.pctAvail;
123
+ for (const cls of ['--server', '--other', '--avail']) {
124
+ const seg = bar.querySelector(`.mem-bar-seg${cls}`);
125
+ if (seg) seg.style.setProperty('--seg-w', '0%');
126
+ }
127
+ const label = `内存:不可用(${reason})`;
128
+ bar.setAttribute('aria-label', label);
129
+ const text = bar.querySelector('.mem-bar-text');
130
+ if (text) text.textContent = label;
131
+ return false;
132
+ };
133
+
134
+ /** 把 fetch 异常归类成用户看得懂的原因(三种失败要能区分开) */
135
+ const reasonOfFetchError = (err) => {
136
+ const msg = String((err && err.message) || err || '');
137
+ // AbortSignal.timeout() 超时抛的是 name='TimeoutError' 的 DOMException
138
+ if ((err && err.name === 'TimeoutError') || /abort|timeout/i.test(msg)) return '查询超时';
139
+ const m = msg.match(/^HTTP (\d{3})$/);
140
+ if (m) return `HTTP ${m[1]}`;
141
+ return msg || '接口不可达';
142
+ };
143
+
144
+ /**
145
+ * 拉一次数据并刷新显示。
146
+ * @returns {Promise<boolean>} 是否成功显示
147
+ */
148
+ const refresh = async () => {
149
+ const host = getHost();
150
+ if (!host) return false;
151
+ let j = null;
152
+ try {
153
+ const res = await fetch('/api/unpack/capacity', {
154
+ headers: { Accept: 'application/json' },
155
+ signal: AbortSignal.timeout(2000),
156
+ });
157
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
158
+ j = await res.json();
159
+ } catch (err) {
160
+ // 失败:留占位 + 把原因写在条上(不静默、也不显示假数字)
161
+ const reason = reasonOfFetchError(err);
162
+ stageLog('warn', `内存条数据取不到:${reason}`, { where: 'memory-bar', err: String((err && err.message) || err) });
163
+ return showUnavailable(reason);
164
+ }
165
+ const total = Number(j.memTotalMb);
166
+ const avail = Number(j.memAvailableMb);
167
+ const server = Number(j.rssMb);
168
+ if (!(total > 0) || !Number.isFinite(avail) || !Number.isFinite(server)) {
169
+ stageLog('warn', '内存条数据不完整(字段缺失或非法)', { total, avail, server });
170
+ return showUnavailable('数据不完整');
171
+ }
172
+
173
+ const bar = ensure();
174
+ if (!bar) return false;
175
+ host.hidden = false;
176
+ delete bar.dataset.unavailable; // 恢复成正常条(下一轮拿到数据就切回来)
177
+ const p = splitPercents({ total, server, avail });
178
+ const segS = bar.querySelector('.mem-bar-seg--server');
179
+ const segO = bar.querySelector('.mem-bar-seg--other');
180
+ const segA = bar.querySelector('.mem-bar-seg--avail');
181
+ // 宽度是"数据驱动的动态值" ⇒ 用 CSS 自定义属性下发,样式表里只用 var()
182
+ segS.style.setProperty('--seg-w', `${p.server}%`);
183
+ segO.style.setProperty('--seg-w', `${p.other}%`);
184
+ segA.style.setProperty('--seg-w', `${p.avail}%`);
185
+ bar.dataset.pctServer = String(p.server);
186
+ bar.dataset.pctOther = String(p.other);
187
+ bar.dataset.pctAvail = String(p.avail);
188
+ bar.setAttribute('aria-label', `内存:总 ${total} MB,可用 ${avail} MB,服务端已占用 ${server} MB`);
189
+ const text = bar.querySelector('.mem-bar-text');
190
+ if (text) text.textContent = `总 ${total} MB · 可用 ${avail} MB · 服务端 ${server} MB`;
191
+ return true;
192
+ };
193
+
194
+ return { refresh, hide, root: () => getHost() && getHost().querySelector('.mem-bar') };
195
+ }