modelstage 0.0.0-stage → 0.5.24

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 (286) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +267 -2
  3. package/THIRD_PARTY_NOTICES.md +90 -0
  4. package/bin/modelstage.mjs +101 -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 +58 -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 +647 -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 +334 -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/web/README.md +193 -0
  126. package/src/web/app/anim-list.js +222 -0
  127. package/src/web/app/background-solids.js +150 -0
  128. package/src/web/app/backgrounds.js +152 -0
  129. package/src/web/app/camera-log.js +28 -0
  130. package/src/web/app/context.js +72 -0
  131. package/src/web/app/dir-picker.js +242 -0
  132. package/src/web/app/entry-report.js +71 -0
  133. package/src/web/app/memory-bars.js +91 -0
  134. package/src/web/app/memory-budget.js +228 -0
  135. package/src/web/app/memory-guard.js +204 -0
  136. package/src/web/app/model-actions.js +682 -0
  137. package/src/web/app/renderer-setting.js +70 -0
  138. package/src/web/app/root-manager.js +254 -0
  139. package/src/web/app/status.js +138 -0
  140. package/src/web/app/topbar-actions.js +357 -0
  141. package/src/web/app/transport-actions.js +297 -0
  142. package/src/web/app/ui-state.js +559 -0
  143. package/src/web/app/url-params.js +171 -0
  144. package/src/web/app/view-settings.js +186 -0
  145. package/src/web/components.css +465 -0
  146. package/src/web/debug/diag-hud.js +95 -0
  147. package/src/web/debug/drag.js +369 -0
  148. package/src/web/debug/log.js +320 -0
  149. package/src/web/debug/model-cmp.js +292 -0
  150. package/src/web/debug/panel.css +474 -0
  151. package/src/web/debug/panel.js +712 -0
  152. package/src/web/debug/race-probe.js +82 -0
  153. package/src/web/debug/upload.js +130 -0
  154. package/src/web/engine/adapters/atlas.js +47 -0
  155. package/src/web/engine/adapters/index.js +22 -0
  156. package/src/web/engine/adapters/renderer.js +223 -0
  157. package/src/web/engine/adapters/skeleton.js +105 -0
  158. package/src/web/engine/adapters/texture.js +64 -0
  159. package/src/web/engine/animation.js +181 -0
  160. package/src/web/engine/appearance.js +136 -0
  161. package/src/web/engine/camera-adapter.js +212 -0
  162. package/src/web/engine/effect-chain.js +138 -0
  163. package/src/web/engine/engine-state.js +120 -0
  164. package/src/web/engine/filters.js +224 -0
  165. package/src/web/engine/fit.js +197 -0
  166. package/src/web/engine/frame-gate.js +33 -0
  167. package/src/web/engine/gl-driver.js +56 -0
  168. package/src/web/engine/gl.js +77 -0
  169. package/src/web/engine/index.js +20 -0
  170. package/src/web/engine/loader.js +55 -0
  171. package/src/web/engine/model-info.js +26 -0
  172. package/src/web/engine/pipeline.js +200 -0
  173. package/src/web/engine/player.js +427 -0
  174. package/src/web/engine/pma/blend.js +76 -0
  175. package/src/web/engine/pma/detect.js +118 -0
  176. package/src/web/engine/pma/index.js +8 -0
  177. package/src/web/engine/pma/resolve.js +112 -0
  178. package/src/web/engine/pma/upload.js +127 -0
  179. package/src/web/engine/pma.js +5 -0
  180. package/src/web/engine/render-frame.js +93 -0
  181. package/src/web/engine/runtime-capabilities.js +209 -0
  182. package/src/web/engine/runtime.js +37 -0
  183. package/src/web/engine/skins.js +165 -0
  184. package/src/web/engine/textures.js +174 -0
  185. package/src/web/index.html +115 -0
  186. package/src/web/infra/api.js +531 -0
  187. package/src/web/infra/diag.js +509 -0
  188. package/src/web/infra/document.js +269 -0
  189. package/src/web/infra/dom.js +31 -0
  190. package/src/web/infra/events.mjs +27 -0
  191. package/src/web/infra/log.js +153 -0
  192. package/src/web/infra/mem-stats.js +112 -0
  193. package/src/web/infra/plugin-host-registration.mjs +285 -0
  194. package/src/web/infra/plugin-host-report.mjs +32 -0
  195. package/src/web/infra/plugin-host-unload.mjs +79 -0
  196. package/src/web/infra/plugin-host-unpack.mjs +899 -0
  197. package/src/web/infra/plugin-host-warmup.mjs +180 -0
  198. package/src/web/infra/plugin-host.js +628 -0
  199. package/src/web/infra/ready.js +461 -0
  200. package/src/web/infra/schedule.js +51 -0
  201. package/src/web/infra/shortcuts.js +129 -0
  202. package/src/web/infra/slot-registry.js +402 -0
  203. package/src/web/infra/vfs.js +247 -0
  204. package/src/web/live2d/bridge.js +400 -0
  205. package/src/web/live2d/model.js +321 -0
  206. package/src/web/live2d/overlay.js +142 -0
  207. package/src/web/live2d/pixi-app.js +111 -0
  208. package/src/web/live2d/playback-probe.js +92 -0
  209. package/src/web/live2d/probe-geometry.js +329 -0
  210. package/src/web/live2d/probe.js +119 -0
  211. package/src/web/live2d/release.js +66 -0
  212. package/src/web/live2d/runtime-adapter.js +289 -0
  213. package/src/web/live2d/stage-ctx.js +152 -0
  214. package/src/web/live2d/stage.js +204 -0
  215. package/src/web/live2d/vfs-settings.js +174 -0
  216. package/src/web/live2d/view.js +89 -0
  217. package/src/web/main.js +629 -0
  218. package/src/web/panel/background-list.js +373 -0
  219. package/src/web/panel/bg-source.js +291 -0
  220. package/src/web/panel/color-picker.css +199 -0
  221. package/src/web/panel/color-picker.js +278 -0
  222. package/src/web/panel/dialog.css +116 -0
  223. package/src/web/panel/dialog.js +173 -0
  224. package/src/web/panel/dom.js +35 -0
  225. package/src/web/panel/icons.js +58 -0
  226. package/src/web/panel/memory-bar.js +195 -0
  227. package/src/web/panel/panel-settings.js +1263 -0
  228. package/src/web/panel/panel.css +1304 -0
  229. package/src/web/panel/render-choice.js +104 -0
  230. package/src/web/panel/schema.js +784 -0
  231. package/src/web/panel/settings-io.js +291 -0
  232. package/src/web/panel/shell-controls.js +274 -0
  233. package/src/web/panel/slider.js +210 -0
  234. package/src/web/panel/toolbar-schema.js +111 -0
  235. package/src/web/panel/toolbar.js +195 -0
  236. package/src/web/panel/transport-picker.js +375 -0
  237. package/src/web/panel/transport.js +321 -0
  238. package/src/web/panel/view.js +553 -0
  239. package/src/web/plugin/unpack-worker.js +185 -0
  240. package/src/web/render/camera/constants.js +60 -0
  241. package/src/web/render/camera/index.js +123 -0
  242. package/src/web/render/camera/spine.js +124 -0
  243. package/src/web/render/capture.js +30 -0
  244. package/src/web/render/interaction.js +527 -0
  245. package/src/web/render/live2d.js +627 -0
  246. package/src/web/render/playback.js +47 -0
  247. package/src/web/render/reset-view.js +29 -0
  248. package/src/web/render/spine.js +386 -0
  249. package/src/web/render/surface-switch.js +176 -0
  250. package/src/web/render/surface.js +571 -0
  251. package/src/web/render/surfaces.js +70 -0
  252. package/src/web/roster/filter.js +31 -0
  253. package/src/web/roster/list.js +276 -0
  254. package/src/web/roster/persist.js +52 -0
  255. package/src/web/roster/reveal.js +77 -0
  256. package/src/web/roster/rows.js +248 -0
  257. package/src/web/roster/snapshot.js +37 -0
  258. package/src/web/roster/view.js +670 -0
  259. package/src/web/spine/canvas-api-groups.js +267 -0
  260. package/src/web/spine/canvas-api.js +97 -0
  261. package/src/web/spine/canvas-document.js +80 -0
  262. package/src/web/spine/canvas-engine.js +193 -0
  263. package/src/web/spine/canvas-entry.js +28 -0
  264. package/src/web/spine/canvas-pixels.js +36 -0
  265. package/src/web/spine/canvas-runtime.js +127 -0
  266. package/src/web/spine/canvas-view.js +108 -0
  267. package/src/web/spine/canvas.js +7 -0
  268. package/src/web/spine/runtime-hint.js +73 -0
  269. package/src/web/spine/runtimes.js +212 -0
  270. package/src/web/style.css +493 -0
  271. package/tool/lib/bundle-probe-node.mjs +121 -0
  272. package/tool/lib/check-perms.mjs +114 -0
  273. package/tool/lib/contract-checks.mjs +582 -0
  274. package/tool/lib/cubism-core.mjs +122 -0
  275. package/tool/lib/export-bundle.mjs +328 -0
  276. package/tool/lib/paths.mjs +20 -0
  277. package/tool/lib/runtime-guides.mjs +65 -0
  278. package/tool/lib/runtime-package.mjs +403 -0
  279. package/tool/lib/spine-meta.mjs +227 -0
  280. package/tool/lib/spine-motions.mjs +104 -0
  281. package/tool/lib/spine-runtime.mjs +131 -0
  282. package/tool/lib/test-lock.mjs +178 -0
  283. package/tool/lib/test-report.mjs +133 -0
  284. package/tool/parse-jsonc.mjs +86 -0
  285. package/tool/resolve-live2d-runtimes.mjs +276 -0
  286. package/tool/spine-motion-rules.json +101 -0
@@ -0,0 +1,312 @@
1
+ // serve-static.mjs —— 静态资源类路由(从 serve.mjs 拆出,控制单文件规模)
2
+ //
3
+ // 负责:/plugins/*、/spine-runtimes/*(含 index.json 版本清单)、以及 src/web 静态托管。
4
+ // 与数据无关,故不依赖 createServerData;目录根由调用方注入。
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { MIME } from '../assets/probe.mjs';
8
+ // 版本排序与工具同口径(语义版本,4.0.10 > 4.0.9)—— 别在这里另写一套比较函数
9
+ import { compareSemver } from '../../../tool/lib/runtime-package.mjs';
10
+
11
+ /** 把 URL 解析到某个根目录下的真实文件;越界或不存在返回 null */
12
+ export function resolveUnder(baseDir, pathname, prefix) {
13
+ let rel;
14
+ try {
15
+ rel = decodeURIComponent(pathname.slice(prefix.length));
16
+ } catch {
17
+ return null;
18
+ }
19
+ const abs = path.resolve(baseDir, rel);
20
+ if (abs !== baseDir && !abs.startsWith(baseDir + path.sep)) return null;
21
+ try {
22
+ return fs.statSync(abs).isFile() ? abs : null;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ /** 流式发送磁盘文件(长度已知、禁缓存) */
29
+ /** 纯文本响应头:原来 9 处各写一遍同一个字面量,集中一处(改 charset 只改这里) */
30
+ const MIME_TEXT_PLAIN = 'text/plain; charset=utf-8';
31
+ export function sendFile(req, res, abs, extraHeaders) {
32
+ let stat;
33
+ try {
34
+ stat = fs.statSync(abs);
35
+ } catch {
36
+ res.writeHead(404, { 'Content-Type': MIME_TEXT_PLAIN });
37
+ return res.end('404: not found');
38
+ }
39
+ if (!stat.isFile()) {
40
+ res.writeHead(404, { 'Content-Type': MIME_TEXT_PLAIN });
41
+ return res.end('404: not found');
42
+ }
43
+ const ext = path.extname(abs).toLowerCase();
44
+ res.writeHead(200, Object.assign({
45
+ 'Content-Type': MIME[ext] || 'application/octet-stream',
46
+ 'Content-Length': stat.size,
47
+ 'Cache-Control': 'no-store',
48
+ }, extraHeaders || {}));
49
+ if (req.method === 'HEAD') return res.end();
50
+ fs.createReadStream(abs).pipe(res);
51
+ }
52
+
53
+ /**
54
+ * 扫运行时目录得到版本清单(客户端不写死版本)。
55
+ *
56
+ * 支持传**多个**目录并合并去重:项目自带(src/spine-runtimes)与用户数据根
57
+ * (<dataRoot>/runtimes/spine,由 tool/fetch-runtimes.mjs 下载)都要算进来 ——
58
+ * 只扫一个目录时会出现"resolver 找得到、前端却列不出来"的假缺失。
59
+ * 排序用语义版本(4.0.10 必须排在 4.0.9 之后),不能靠字符串比较。
60
+ *
61
+ * @param {...(string|string[])} dirs 一个或多个目录
62
+ * @returns {string[]}
63
+ */
64
+ export function listRuntimeVersions(...dirs) {
65
+ const flat = dirs.flat().filter(Boolean);
66
+ const found = new Set();
67
+ const addFrom = (dir) => {
68
+ for (const f of fs.readdirSync(dir)) {
69
+ const m = /^spine-webgl-(.+)\.js$/.exec(f);
70
+ if (m) found.add(m[1]);
71
+ }
72
+ };
73
+ for (const dir of flat) {
74
+ try {
75
+ addFrom(dir);
76
+ // 分版本子目录(<dir>/<ver>/spine-webgl-<ver>.js)也要列出来 ——
77
+ // 那是 tool/fetch-runtimes.mjs 的下载布局;只扫平铺会让"下载成功但前端列不出来"。
78
+ const subs = fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
79
+ for (const sub of subs) {
80
+ try { addFrom(path.join(dir, sub)); } catch { /* 读不了就跳过 */ }
81
+ }
82
+ } catch { /* 目录不存在 = 这一路没有版本,继续看下一个 */ }
83
+ }
84
+ return [...found].sort(compareSemver);
85
+ }
86
+
87
+ /**
88
+ * 组装给前端的"缺运行时怎么补"提示。
89
+ *
90
+ * 为什么由服务端出:提示里最有用的一条是**落地目录的绝对路径**,而 dataRoot 只有服务端知道;
91
+ * 前端自己拼只能写相对描述,用户还得猜。
92
+ * 前端用法:把 versions 里没有的版本号代入 templates.spineFetch / templates.spine3Curl。
93
+ *
94
+ * @param {{dataRoot?: string, dataRuntimesDir?: string, live2dRuntimesDir?: string}} ctx
95
+ * @returns {object}
96
+ */
97
+ export function buildRuntimeHint(ctx) {
98
+ const dataRoot = (ctx && ctx.dataRoot) || '';
99
+ const spineDir = (ctx && ctx.dataRuntimesDir) || (dataRoot ? path.join(dataRoot, 'runtimes', 'spine') : '');
100
+ const live2dDir = (ctx && ctx.live2dRuntimesDir) || (dataRoot ? path.join(dataRoot, 'runtimes', 'live2d') : '');
101
+ return {
102
+ dataRoot,
103
+ spineDir,
104
+ live2dDir,
105
+ // {ver} 由前端替换成实际缺的版本号
106
+ spineFetch: 'node tool/fetch-runtimes.mjs --spine {ver}',
107
+ spineTarget: spineDir ? path.join(spineDir, '{ver}', 'spine-webgl-{ver}.js') : '',
108
+ spine3Curl: 'curl -L -o "' + (spineDir ? path.join(spineDir, '{ver}', 'spine-webgl-{ver}.js') : 'spine-webgl-{ver}.js')
109
+ + '" https://raw.githubusercontent.com/EsotericSoftware/spine-runtimes/{ver}/spine-ts/build/spine-webgl.js',
110
+ spine3Note: '3.x 官方未发 npm(最早 4.0.1)⇒ 只能用上面这条 curl 自备;4.x 可用 spineFetch 自动下载',
111
+ live2dGuideUrl: 'https://www.live2d.com/en/sdk/download/web/',
112
+ live2dTarget: live2dDir,
113
+ live2dFiles: ['live2dcubismcore.js', 'pixi.min.js', 'index.min.js'],
114
+ live2dNote: 'Live2D Cubism Core 需你本人同意官方专有许可,本项目不代下载',
115
+ };
116
+ }
117
+
118
+ /**
119
+ * 在「项目自带 → 数据根」里找 /spine-runtimes/<file> 对应的实际文件。
120
+ *
121
+ * 数据根侧要兼容两种布局:平铺 <dir>/<file> 与分版本 <dir>/<ver>/<file>(下载布局)。
122
+ * 前端请求的永远是平铺 URL(`/spine-runtimes/spine-webgl-<ver>.js`),所以这里做一次映射。
123
+ * @param {object} ctx
124
+ * @param {string} p URL 路径
125
+ * @returns {string|null}
126
+ */
127
+ function resolveRuntimeFile(ctx, p) {
128
+ const fromProject = resolveUnder(ctx.runtimeDir, p, '/spine-runtimes/');
129
+ if (fromProject) return fromProject;
130
+ if (!ctx.dataRuntimesDir) return null;
131
+ const flat = resolveUnder(ctx.dataRuntimesDir, p, '/spine-runtimes/');
132
+ if (flat) return flat;
133
+ // 分版本布局:只处理 spine-webgl-<ver>.js 这一种命名(版本号受正则约束,无法穿越目录)
134
+ const base = path.basename(p);
135
+ const m = /^spine-webgl-([\w.-]+)\.js$/.exec(base);
136
+ if (!m) return null;
137
+ const nested = path.join(ctx.dataRuntimesDir, m[1], base);
138
+ try {
139
+ return fs.statSync(nested).isFile() ? nested : null;
140
+ } catch {
141
+ return null;
142
+ }
143
+ }
144
+
145
+ /** /plugins/* 、/spine-runtimes/*;处理了返回 true */
146
+ export function serveAssets(ctx, req, res, p) {
147
+ if (p.startsWith('/plugins/')) return servePluginFile(ctx, req, res, p);
148
+ if (p === '/spine-runtimes/index.json') {
149
+ // 两个目录合并:项目自带优先(同版本以项目自带为准,与 resolver 的选择顺序一致)
150
+ const body = JSON.stringify({
151
+ versions: listRuntimeVersions(ctx.runtimeDir, ctx.dataRuntimesDir),
152
+ runtimeHint: buildRuntimeHint(ctx),
153
+ });
154
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': Buffer.byteLength(body), 'Cache-Control': 'no-store' });
155
+ res.end(req.method === 'HEAD' ? undefined : body);
156
+ return true;
157
+ }
158
+ if (p.startsWith('/spine-runtimes/')) {
159
+ const abs = resolveRuntimeFile(ctx, p);
160
+ if (!abs) {
161
+ res.writeHead(404, { 'Content-Type': MIME_TEXT_PLAIN });
162
+ res.end('404: ' + p);
163
+ return true;
164
+ }
165
+ sendFile(req, res, abs, { 'Content-Type': 'text/javascript; charset=utf-8' });
166
+ return true;
167
+ }
168
+ return false;
169
+ }
170
+
171
+ // ---------------------------------------------------------------- 插件静态下发(白名单)
172
+ //
173
+ // 为什么不能像以前那样"plugins/ 目录下有什么就发什么":
174
+ // 插件目录里除了代码还会有笔记、临时产物(历史上出现过自测脚本与卡点清单这类文件),
175
+ // 这些既不该对外可读,也不属于插件契约。所以只下发 plugin.json **显式声明**过的文件:
176
+ // · files[] —— 插件代码(index.js / lib/*.js)
177
+ // · assets[] —— 插件自己的数据文件(如 profiles/<游戏>.json;插件运行时会自己 fetch 它)
178
+ // · entry —— 入口(通常已在 files[] 里,兜底再收一次)
179
+ //
180
+ // 待收敛提示:容器判定/解包逻辑现在有三份副本(插件 lib/bundle-probe.js、
181
+ // tool/lib/bundle-probe-node.mjs、src/server/plugins/registry.mjs 的 detectBundleContainer),
182
+ // 接线完成后统一收敛到插件一份(本轮不做,见 M5 步骤说明)。
183
+
184
+ /** 插件白名单缓存:pluginsDir::id → Set<相对路径>;null 表示该插件没有可用清单 */
185
+ const pluginWhitelistCache = new Map();
186
+
187
+ /**
188
+ * 读某插件的可下发白名单(未声明的一律不发)。
189
+ * @returns {Set<string>|null} null = 清单缺失/损坏 ⇒ 该插件不可下发
190
+ */
191
+ export function pluginWhitelist(pluginsDir, id) {
192
+ const key = `${pluginsDir}::${id}`;
193
+ if (pluginWhitelistCache.has(key)) return pluginWhitelistCache.get(key);
194
+ let list = null;
195
+ try {
196
+ const manifest = JSON.parse(fs.readFileSync(path.join(pluginsDir, id, 'plugin.json'), 'utf8'));
197
+ const declared = [];
198
+ if (Array.isArray(manifest.files)) declared.push(...manifest.files);
199
+ if (Array.isArray(manifest.assets)) declared.push(...manifest.assets);
200
+ if (manifest.entry) declared.push(manifest.entry);
201
+ list = new Set(declared.map((f) => String(f).replace(/^\.?\//, '')));
202
+ } catch {
203
+ list = null;
204
+ }
205
+ pluginWhitelistCache.set(key, list);
206
+ return list;
207
+ }
208
+
209
+ /** 路径段校验:拒绝绝对路径、空段、. 与 ..(穿越) */
210
+ function isSafeRelPath(rel) {
211
+ if (!rel || rel.includes('\0')) return false;
212
+ if (path.isAbsolute(rel)) return false;
213
+ const segs = rel.split('/');
214
+ return segs.every((s) => s !== '' && s !== '.' && s !== '..');
215
+ }
216
+
217
+ /**
218
+ * /plugins/<id>/<file>:白名单下发。处理了返回 true。
219
+ * 语义:未在 plugin.json 声明 ⇒ 403(有清单但该文件不在白名单);清单缺失/插件不存在 ⇒ 404。
220
+ */
221
+ export function servePluginFile(ctx, req, res, p) {
222
+ const deny = (status, msg) => {
223
+ res.writeHead(status, { 'Content-Type': MIME_TEXT_PLAIN, 'Cache-Control': 'no-store' });
224
+ res.end(msg);
225
+ };
226
+ let rel;
227
+ try {
228
+ rel = decodeURIComponent(p.slice('/plugins/'.length));
229
+ } catch {
230
+ return deny(400, `400: bad encoding ${p}`);
231
+ }
232
+ const slash = rel.indexOf('/');
233
+ const id = slash > 0 ? rel.slice(0, slash) : '';
234
+ const sub = slash > 0 ? rel.slice(slash + 1) : '';
235
+ if (!id || !sub) return deny(404, `404: ${p}`);
236
+ if (!isSafeRelPath(id) || !isSafeRelPath(sub)) return deny(403, `403: 路径非法 ${p}`);
237
+ const allow = pluginWhitelist(ctx.pluginsDir, id);
238
+ if (!allow) return deny(404, `404: 插件不存在或清单不可读 ${id}`);
239
+ if (!allow.has(sub)) return deny(403, `403: 未在 plugin.json 声明,拒绝下发 ${sub}`);
240
+ const abs = resolveUnder(ctx.pluginsDir, p, '/plugins/');
241
+ if (!abs) return deny(404, `404: ${p}`);
242
+ sendFile(req, res, abs);
243
+ return true;
244
+ }
245
+
246
+ /** 静态托管 src/web(开发期禁缓存);处理了返回 true */
247
+ export function serveStatic(ctx, req, res, p) {
248
+ let rel;
249
+ try {
250
+ rel = decodeURIComponent(p);
251
+ } catch {
252
+ res.writeHead(400, { 'Content-Type': MIME_TEXT_PLAIN });
253
+ res.end('400: bad encoding');
254
+ return true;
255
+ }
256
+ if (rel === '/' || rel === '') rel = '/index.html';
257
+ // 共享模块:src/shared 与 src/web 平级,前端按 /shared/xxx.mjs 引入。
258
+ // 不加这段会被当成 src/web/shared/... ⇒ 404 ⇒ 引用它的模块整条链路加载失败(页面白屏)。
259
+ if (rel.startsWith('/shared/')) {
260
+ const sharedRoot = path.resolve(ctx.webRoot, '..', 'shared');
261
+ const sub = `.${rel.slice('/shared'.length)}`;
262
+ const sharedAbs = path.resolve(sharedRoot, sub);
263
+ if (sharedAbs !== sharedRoot && !sharedAbs.startsWith(sharedRoot + path.sep)) {
264
+ res.writeHead(403, { 'Content-Type': MIME_TEXT_PLAIN });
265
+ res.end('403: forbidden');
266
+ return true;
267
+ }
268
+ if (!fs.existsSync(sharedAbs) || !fs.statSync(sharedAbs).isFile()) {
269
+ res.writeHead(404, { 'Content-Type': MIME_TEXT_PLAIN });
270
+ res.end(`404: ${p}`);
271
+ return true;
272
+ }
273
+ sendFile(req, res, sharedAbs);
274
+ return true;
275
+ }
276
+ const abs = path.resolve(ctx.webRoot, `.${rel}`);
277
+ if (abs !== ctx.webRoot && !abs.startsWith(ctx.webRoot + path.sep)) {
278
+ res.writeHead(403, { 'Content-Type': MIME_TEXT_PLAIN });
279
+ res.end('403: forbidden');
280
+ return true;
281
+ }
282
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
283
+ res.writeHead(404, { 'Content-Type': MIME_TEXT_PLAIN });
284
+ res.end(`404: ${p}`);
285
+ return true;
286
+ }
287
+ sendFile(req, res, abs);
288
+ return true;
289
+ }
290
+
291
+ /**
292
+ * 绑定型适配器:目录固定,调用方只需 (req,res,p)。
293
+ * @param {{root: string, webRoot: string, runtimeDir: string, pluginsDir: string}} opts
294
+ */
295
+ export function createStaticRoutes(opts) {
296
+ const ctx = {
297
+ root: opts.root,
298
+ webRoot: opts.webRoot,
299
+ runtimeDir: opts.runtimeDir,
300
+ pluginsDir: opts.pluginsDir,
301
+ // 用户数据根下的运行时(tool/fetch-runtimes.mjs 的落点)。缺省为空 = 不参与探测,
302
+ // 行为与"没有这个字段"完全一致。
303
+ dataRuntimesDir: opts.dataRuntimesDir || '',
304
+ live2dRuntimesDir: opts.live2dRuntimesDir || '',
305
+ dataRoot: opts.dataRoot || '',
306
+ };
307
+ return {
308
+ serveAssets: (req, res, p) => serveAssets(ctx, req, res, p),
309
+ serveStatic: (req, res, p) => serveStatic(ctx, req, res, p),
310
+ pluginWhitelist: (id) => pluginWhitelist(ctx.pluginsDir, id),
311
+ };
312
+ }