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.
- package/LICENSE +21 -0
- package/README.md +267 -2
- package/THIRD_PARTY_NOTICES.md +90 -0
- package/bin/modelstage.mjs +101 -0
- package/config/diag-upload.example.json +7 -0
- package/config/models/live2d.assetbundls.json +1 -0
- package/config/models/live2d.azurlane.json +1 -0
- package/config/models/live2d.azurlane_bundle.json +1 -0
- package/config/models/live2d.azurlane_spine.json +1 -0
- package/config/models/live2d.bd2.json +1 -0
- package/config/models/live2d.bluearchive.json +1 -0
- package/config/models/live2d.live2d.json +1 -0
- package/config/models/live2d.nikke.json +1 -0
- package/config/models/live2d.nikke_2.json +1 -0
- package/config/models/live2d.npc.json +1 -0
- package/config/models/spine.assetbundls.json +1 -0
- package/config/models/spine.azurlane.json +1 -0
- package/config/models/spine.azurlane_bundle.json +1 -0
- package/config/models/spine.azurlane_spine.json +1 -0
- package/config/models/spine.bd2.json +1 -0
- package/config/models/spine.bluearchive.json +1 -0
- package/config/models/spine.live2d.json +1 -0
- package/config/models/spine.nikke.json +1 -0
- package/config/models/spine.nikke_2.json +1 -0
- package/config/models/spine.npc.json +1 -0
- package/config/names/NIKKE.json +1058 -0
- package/config/names/README.md +47 -0
- package/config/names//345/244/251/345/221/275/344/271/213/345/255/220.json +42897 -0
- package/config/names//346/243/225/350/211/262/345/260/230/345/237/2032.json +4016 -0
- package/config/names//347/242/247/350/223/235/350/210/252/347/272/277.json +38342 -0
- package/config/plugins.json +13 -0
- package/config/resources.example.json +40 -0
- package/package.json +58 -4
- package/plugins/unity-live2d-bundle/README.md +98 -0
- package/plugins/unity-live2d-bundle/index.js +891 -0
- package/plugins/unity-live2d-bundle/lib/anim-to-motion.js +402 -0
- package/plugins/unity-live2d-bundle/lib/bundle-probe.js +244 -0
- package/plugins/unity-live2d-bundle/lib/bytes.js +189 -0
- package/plugins/unity-live2d-bundle/lib/common-strings.js +46 -0
- package/plugins/unity-live2d-bundle/lib/dxt.js +197 -0
- package/plugins/unity-live2d-bundle/lib/errors.js +71 -0
- package/plugins/unity-live2d-bundle/lib/etc2.js +371 -0
- package/plugins/unity-live2d-bundle/lib/extract.js +242 -0
- package/plugins/unity-live2d-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
- package/plugins/unity-live2d-bundle/lib/lzma-d-min.js +34 -0
- package/plugins/unity-live2d-bundle/lib/model3.js +137 -0
- package/plugins/unity-live2d-bundle/lib/png.js +424 -0
- package/plugins/unity-live2d-bundle/lib/serialized.js +233 -0
- package/plugins/unity-live2d-bundle/lib/texdecode.js +190 -0
- package/plugins/unity-live2d-bundle/lib/typetree.js +215 -0
- package/plugins/unity-live2d-bundle/lib/unityfs.js +396 -0
- package/plugins/unity-live2d-bundle/plugin.json +81 -0
- package/plugins/unity-live2d-bundle/profiles/unityfs-live2d.json +26 -0
- package/plugins/unity-spine-bundle/index.js +535 -0
- package/plugins/unity-spine-bundle/lib/bytes.js +189 -0
- package/plugins/unity-spine-bundle/lib/common-strings.js +46 -0
- package/plugins/unity-spine-bundle/lib/dxt.js +197 -0
- package/plugins/unity-spine-bundle/lib/errors.js +79 -0
- package/plugins/unity-spine-bundle/lib/etc2.js +371 -0
- package/plugins/unity-spine-bundle/lib/extract.js +242 -0
- package/plugins/unity-spine-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
- package/plugins/unity-spine-bundle/lib/lzma-d-min.js +34 -0
- package/plugins/unity-spine-bundle/lib/png.js +424 -0
- package/plugins/unity-spine-bundle/lib/serialized.js +233 -0
- package/plugins/unity-spine-bundle/lib/texdecode.js +190 -0
- package/plugins/unity-spine-bundle/lib/typetree.js +215 -0
- package/plugins/unity-spine-bundle/lib/unityfs.js +396 -0
- package/plugins/unity-spine-bundle/plugin.json +75 -0
- package/src/server/README.md +69 -0
- package/src/server/assets/browse.mjs +56 -0
- package/src/server/assets/bundle-probe.mjs +94 -0
- package/src/server/assets/probe.mjs +184 -0
- package/src/server/core/config.mjs +173 -0
- package/src/server/core/env-num.mjs +60 -0
- package/src/server/core/env.mjs +49 -0
- package/src/server/core/fs-portable.mjs +37 -0
- package/src/server/core/log.mjs +107 -0
- package/src/server/core/paths.mjs +44 -0
- package/src/server/data/catalog.mjs +1091 -0
- package/src/server/data/entry-states.mjs +499 -0
- package/src/server/data/live2d.mjs +272 -0
- package/src/server/data/manifest-cache.mjs +58 -0
- package/src/server/data/manifest-stamp.mjs +135 -0
- package/src/server/data/model-meta.mjs +114 -0
- package/src/server/data/names.mjs +106 -0
- package/src/server/data/shape-probe-worker.mjs +33 -0
- package/src/server/data/shape-probe.mjs +626 -0
- package/src/server/data/spine.mjs +336 -0
- package/src/server/data/tree.mjs +126 -0
- package/src/server/http/atomic-json.mjs +145 -0
- package/src/server/http/background-solids.mjs +270 -0
- package/src/server/http/backgrounds.mjs +388 -0
- package/src/server/http/diag-upload.mjs +264 -0
- package/src/server/http/diag.mjs +173 -0
- package/src/server/http/ready.mjs +167 -0
- package/src/server/http/static.mjs +312 -0
- package/src/server/http/write.mjs +647 -0
- package/src/server/index.mjs +114 -0
- package/src/server/plugins/builtin-providers.json +30 -0
- package/src/server/plugins/registry.mjs +650 -0
- package/src/server/plugins/slot-catalog.mjs +286 -0
- package/src/server/routes/background-solids.mjs +56 -0
- package/src/server/routes/backgrounds.mjs +77 -0
- package/src/server/routes/config.mjs +38 -0
- package/src/server/routes/data.mjs +313 -0
- package/src/server/routes/diag.mjs +37 -0
- package/src/server/routes/entries.mjs +170 -0
- package/src/server/routes/ready.mjs +122 -0
- package/src/server/serve.mjs +334 -0
- package/src/server/unpack/capacity.mjs +217 -0
- package/src/server/unpack/mem-probe.mjs +27 -0
- package/src/server/unpack/plugin-runner.mjs +529 -0
- package/src/server/unpack/plugin-store.mjs +736 -0
- package/src/server/unpack/probe-store.mjs +73 -0
- package/src/server/unpack/routes.mjs +359 -0
- package/src/server/unpack/runtime/exec.mjs +558 -0
- package/src/server/unpack/runtime/index.mjs +141 -0
- package/src/server/unpack/runtime/node-vm.mjs +111 -0
- package/src/server/unpack/unpack-worker.mjs +105 -0
- package/src/server/unpack/worker-client.mjs +184 -0
- package/src/server/unpack/write-result.mjs +84 -0
- package/src/shared/bundle-shape.mjs +83 -0
- package/src/shared/entry-reasons.mjs +119 -0
- package/src/shared/names.mjs +45 -0
- package/src/web/README.md +193 -0
- package/src/web/app/anim-list.js +222 -0
- package/src/web/app/background-solids.js +150 -0
- package/src/web/app/backgrounds.js +152 -0
- package/src/web/app/camera-log.js +28 -0
- package/src/web/app/context.js +72 -0
- package/src/web/app/dir-picker.js +242 -0
- package/src/web/app/entry-report.js +71 -0
- package/src/web/app/memory-bars.js +91 -0
- package/src/web/app/memory-budget.js +228 -0
- package/src/web/app/memory-guard.js +204 -0
- package/src/web/app/model-actions.js +682 -0
- package/src/web/app/renderer-setting.js +70 -0
- package/src/web/app/root-manager.js +254 -0
- package/src/web/app/status.js +138 -0
- package/src/web/app/topbar-actions.js +357 -0
- package/src/web/app/transport-actions.js +297 -0
- package/src/web/app/ui-state.js +559 -0
- package/src/web/app/url-params.js +171 -0
- package/src/web/app/view-settings.js +186 -0
- package/src/web/components.css +465 -0
- package/src/web/debug/diag-hud.js +95 -0
- package/src/web/debug/drag.js +369 -0
- package/src/web/debug/log.js +320 -0
- package/src/web/debug/model-cmp.js +292 -0
- package/src/web/debug/panel.css +474 -0
- package/src/web/debug/panel.js +712 -0
- package/src/web/debug/race-probe.js +82 -0
- package/src/web/debug/upload.js +130 -0
- package/src/web/engine/adapters/atlas.js +47 -0
- package/src/web/engine/adapters/index.js +22 -0
- package/src/web/engine/adapters/renderer.js +223 -0
- package/src/web/engine/adapters/skeleton.js +105 -0
- package/src/web/engine/adapters/texture.js +64 -0
- package/src/web/engine/animation.js +181 -0
- package/src/web/engine/appearance.js +136 -0
- package/src/web/engine/camera-adapter.js +212 -0
- package/src/web/engine/effect-chain.js +138 -0
- package/src/web/engine/engine-state.js +120 -0
- package/src/web/engine/filters.js +224 -0
- package/src/web/engine/fit.js +197 -0
- package/src/web/engine/frame-gate.js +33 -0
- package/src/web/engine/gl-driver.js +56 -0
- package/src/web/engine/gl.js +77 -0
- package/src/web/engine/index.js +20 -0
- package/src/web/engine/loader.js +55 -0
- package/src/web/engine/model-info.js +26 -0
- package/src/web/engine/pipeline.js +200 -0
- package/src/web/engine/player.js +427 -0
- package/src/web/engine/pma/blend.js +76 -0
- package/src/web/engine/pma/detect.js +118 -0
- package/src/web/engine/pma/index.js +8 -0
- package/src/web/engine/pma/resolve.js +112 -0
- package/src/web/engine/pma/upload.js +127 -0
- package/src/web/engine/pma.js +5 -0
- package/src/web/engine/render-frame.js +93 -0
- package/src/web/engine/runtime-capabilities.js +209 -0
- package/src/web/engine/runtime.js +37 -0
- package/src/web/engine/skins.js +165 -0
- package/src/web/engine/textures.js +174 -0
- package/src/web/index.html +115 -0
- package/src/web/infra/api.js +531 -0
- package/src/web/infra/diag.js +509 -0
- package/src/web/infra/document.js +269 -0
- package/src/web/infra/dom.js +31 -0
- package/src/web/infra/events.mjs +27 -0
- package/src/web/infra/log.js +153 -0
- package/src/web/infra/mem-stats.js +112 -0
- package/src/web/infra/plugin-host-registration.mjs +285 -0
- package/src/web/infra/plugin-host-report.mjs +32 -0
- package/src/web/infra/plugin-host-unload.mjs +79 -0
- package/src/web/infra/plugin-host-unpack.mjs +899 -0
- package/src/web/infra/plugin-host-warmup.mjs +180 -0
- package/src/web/infra/plugin-host.js +628 -0
- package/src/web/infra/ready.js +461 -0
- package/src/web/infra/schedule.js +51 -0
- package/src/web/infra/shortcuts.js +129 -0
- package/src/web/infra/slot-registry.js +402 -0
- package/src/web/infra/vfs.js +247 -0
- package/src/web/live2d/bridge.js +400 -0
- package/src/web/live2d/model.js +321 -0
- package/src/web/live2d/overlay.js +142 -0
- package/src/web/live2d/pixi-app.js +111 -0
- package/src/web/live2d/playback-probe.js +92 -0
- package/src/web/live2d/probe-geometry.js +329 -0
- package/src/web/live2d/probe.js +119 -0
- package/src/web/live2d/release.js +66 -0
- package/src/web/live2d/runtime-adapter.js +289 -0
- package/src/web/live2d/stage-ctx.js +152 -0
- package/src/web/live2d/stage.js +204 -0
- package/src/web/live2d/vfs-settings.js +174 -0
- package/src/web/live2d/view.js +89 -0
- package/src/web/main.js +629 -0
- package/src/web/panel/background-list.js +373 -0
- package/src/web/panel/bg-source.js +291 -0
- package/src/web/panel/color-picker.css +199 -0
- package/src/web/panel/color-picker.js +278 -0
- package/src/web/panel/dialog.css +116 -0
- package/src/web/panel/dialog.js +173 -0
- package/src/web/panel/dom.js +35 -0
- package/src/web/panel/icons.js +58 -0
- package/src/web/panel/memory-bar.js +195 -0
- package/src/web/panel/panel-settings.js +1263 -0
- package/src/web/panel/panel.css +1304 -0
- package/src/web/panel/render-choice.js +104 -0
- package/src/web/panel/schema.js +784 -0
- package/src/web/panel/settings-io.js +291 -0
- package/src/web/panel/shell-controls.js +274 -0
- package/src/web/panel/slider.js +210 -0
- package/src/web/panel/toolbar-schema.js +111 -0
- package/src/web/panel/toolbar.js +195 -0
- package/src/web/panel/transport-picker.js +375 -0
- package/src/web/panel/transport.js +321 -0
- package/src/web/panel/view.js +553 -0
- package/src/web/plugin/unpack-worker.js +185 -0
- package/src/web/render/camera/constants.js +60 -0
- package/src/web/render/camera/index.js +123 -0
- package/src/web/render/camera/spine.js +124 -0
- package/src/web/render/capture.js +30 -0
- package/src/web/render/interaction.js +527 -0
- package/src/web/render/live2d.js +627 -0
- package/src/web/render/playback.js +47 -0
- package/src/web/render/reset-view.js +29 -0
- package/src/web/render/spine.js +386 -0
- package/src/web/render/surface-switch.js +176 -0
- package/src/web/render/surface.js +571 -0
- package/src/web/render/surfaces.js +70 -0
- package/src/web/roster/filter.js +31 -0
- package/src/web/roster/list.js +276 -0
- package/src/web/roster/persist.js +52 -0
- package/src/web/roster/reveal.js +77 -0
- package/src/web/roster/rows.js +248 -0
- package/src/web/roster/snapshot.js +37 -0
- package/src/web/roster/view.js +670 -0
- package/src/web/spine/canvas-api-groups.js +267 -0
- package/src/web/spine/canvas-api.js +97 -0
- package/src/web/spine/canvas-document.js +80 -0
- package/src/web/spine/canvas-engine.js +193 -0
- package/src/web/spine/canvas-entry.js +28 -0
- package/src/web/spine/canvas-pixels.js +36 -0
- package/src/web/spine/canvas-runtime.js +127 -0
- package/src/web/spine/canvas-view.js +108 -0
- package/src/web/spine/canvas.js +7 -0
- package/src/web/spine/runtime-hint.js +73 -0
- package/src/web/spine/runtimes.js +212 -0
- package/src/web/style.css +493 -0
- package/tool/lib/bundle-probe-node.mjs +121 -0
- package/tool/lib/check-perms.mjs +114 -0
- package/tool/lib/contract-checks.mjs +582 -0
- package/tool/lib/cubism-core.mjs +122 -0
- package/tool/lib/export-bundle.mjs +328 -0
- package/tool/lib/paths.mjs +20 -0
- package/tool/lib/runtime-guides.mjs +65 -0
- package/tool/lib/runtime-package.mjs +403 -0
- package/tool/lib/spine-meta.mjs +227 -0
- package/tool/lib/spine-motions.mjs +104 -0
- package/tool/lib/spine-runtime.mjs +131 -0
- package/tool/lib/test-lock.mjs +178 -0
- package/tool/lib/test-report.mjs +133 -0
- package/tool/parse-jsonc.mjs +86 -0
- package/tool/resolve-live2d-runtimes.mjs +276 -0
- package/tool/spine-motion-rules.json +101 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
// infra/plugin-host.js —— 前端插件宿主(资产提供者 canHandle/resolve 的调用方)
|
|
2
|
+
//
|
|
3
|
+
// 为什么需要它:插件是**浏览器脚本**(plugins/<id>/index.js 顶层读 window.SpineViewerPlugins),
|
|
4
|
+
// 本实现之前完全没有宿主 —— 所以插件产出的资源(bundle 类)根本进不了渲染链路。
|
|
5
|
+
// 这个文件补的就是那一层:注入脚本 → 收集 provider → 提供取字节通道 → 供上层 findProvider/resolve。
|
|
6
|
+
//
|
|
7
|
+
// 宿主注入面:插件看到的接口形态在页面重写后保持稳定(插件不需要跟着改)的 ESM 版;
|
|
8
|
+
// 插件侧**零改动**(这是方案 B 的核心优点)。
|
|
9
|
+
//
|
|
10
|
+
// 契约依据:资产提供者契约 §一(canHandle/resolve 形状)、插件契约 §3.1(canHandle 纯判定)、
|
|
11
|
+
// §3.6(取字节通道 = host.fetchFile(rootId, rel))、§3.7(插件异常隔离)。
|
|
12
|
+
//
|
|
13
|
+
// 三条硬规则(都在下面落实):
|
|
14
|
+
// ① 失败隔离:任何一个插件脚本 404/抛错,都只记警告,不影响页面与其它插件
|
|
15
|
+
// ② 只加载启用清单:/api/plugins 已按 config/plugins.json 过滤,被禁用的插件不会出现在清单里
|
|
16
|
+
// ③ canHandle 由宿主调用、宿主兜异常:插件内部抛错一律当"不认这个文件",不冒泡到调用方
|
|
17
|
+
|
|
18
|
+
// 插槽注册表(DOM-free、可 node 直测):只登记不渲染,S2 才接渲染
|
|
19
|
+
import { createSlotRegistry } from './slot-registry.js';
|
|
20
|
+
import { EVENTS } from './events.mjs';
|
|
21
|
+
import { createRegistration } from './plugin-host-registration.mjs';
|
|
22
|
+
import { createUnpack } from './plugin-host-unpack.mjs';
|
|
23
|
+
import { createReport } from './plugin-host-report.mjs';
|
|
24
|
+
import { createUnload } from './plugin-host-unload.mjs';
|
|
25
|
+
import { createWarmup } from './plugin-host-warmup.mjs';
|
|
26
|
+
// 内置分组 id(纯数据,DOM-free):注册表据此区分"引用内置组"与"插件新建组"
|
|
27
|
+
import { PANEL_GROUPS } from '../panel/schema.js';
|
|
28
|
+
|
|
29
|
+
import { getJson } from './api.js';
|
|
30
|
+
// 资源包"形态"判据与文案:与 node 侧 /api/unpack 共用同一份(见 src/shared/bundle-shape.mjs 顶部说明)
|
|
31
|
+
|
|
32
|
+
/** 宿主提示通道:优先用注入的 log(项目调试日志),否则退回 console */
|
|
33
|
+
function makeLogger(log) {
|
|
34
|
+
const push = (level, msg) => {
|
|
35
|
+
try {
|
|
36
|
+
if (typeof log === 'function') { log(level, msg); return; }
|
|
37
|
+
} catch { /* 日志失败不影响主流程 */ }
|
|
38
|
+
const line = `[plugin-host] ${msg}`;
|
|
39
|
+
if (level === 'error') console.error(line);
|
|
40
|
+
else if (level === 'warn') console.warn(line);
|
|
41
|
+
else console.log(line);
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
info: (m) => push('info', m),
|
|
45
|
+
warn: (m) => push('warn', m),
|
|
46
|
+
error: (m) => push('error', m),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
/** 注入一个 <script> 并等它结束(成功/失败都 resolve,不抛) */
|
|
53
|
+
function injectScript(src) {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
const el = document.createElement('script');
|
|
56
|
+
el.src = src;
|
|
57
|
+
el.async = false;
|
|
58
|
+
el.onload = () => resolve(true);
|
|
59
|
+
el.onerror = () => resolve(false);
|
|
60
|
+
document.head.appendChild(el);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 创建插件宿主。
|
|
66
|
+
* @param {{log?: Function, baseUrl?: string}} [opts]
|
|
67
|
+
* @returns {object} 宿主查询面(见文件末尾 return)
|
|
68
|
+
*/
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// 【模块地图】—— 想知道"某个功能在哪个文件",看这一张表即可(不含行号,因此不用维护)
|
|
71
|
+
//
|
|
72
|
+
// plugin-host.js 本体:装配各模块 + 对外 API(hostApi / 返回面)+ 装载编排与插槽表初始化
|
|
73
|
+
// plugin-host-registration.mjs 注册与校验:provider / 效果 / 老接口控件 / 通知(全部拒绝码与校验顺序在这里)
|
|
74
|
+
// plugin-host-unpack.mjs 资源取用与解包:预筛 → 读字节 → worker / 服务端解包 → 注册 VFS(自持 workerPool)
|
|
75
|
+
// plugin-host-unload.mjs 卸载与回收:逆序跑 disposer → 清四张表 → 终止 worker → 派发重建事件
|
|
76
|
+
// plugin-host-warmup.mjs 预热:容量探测 + 在途去重 + 服务端事件流(自持 capCache / warmupInflight)
|
|
77
|
+
// plugin-host-report.mjs 状态报告:只读快照(discovered / providers / loadState)
|
|
78
|
+
// events.mjs 事件名常量(唯一来源;单测禁止其它前端文件再写字面量)
|
|
79
|
+
//
|
|
80
|
+
// 下面各区只说明"这一段负责什么、和哪些状态耦合",不再记录函数行号——
|
|
81
|
+
// 要定位具体函数,进对应模块后按名字搜。
|
|
82
|
+
//
|
|
83
|
+
// ── 分区 · 基础工具与统计发布 ────────────────────────────────────────────────────────────────
|
|
84
|
+
// 职责:日志适配、类型转换、事件派发封装、把插槽/效果统计挂到 window(面板与测试读这些数据集)。
|
|
85
|
+
// 状态耦合:say / emit(依赖)、slotRegistry / pluginEffects(只读汇总)。
|
|
86
|
+
//
|
|
87
|
+
// ── 分区 · 注册入口组装(hostApi;实现在 plugin-host-registration.mjs) ──────────────────────
|
|
88
|
+
// 职责:把 ctx 与各表以"窄视图"注入注册模块,再把返回的方法拼成 hostApi。
|
|
89
|
+
// 状态耦合:providers / loadState / registeredEffects(经注册模块写)、slotRegistry(读)、loadingPluginId(读)。
|
|
90
|
+
//
|
|
91
|
+
// ── 分区 · 装载与卸载回收(loadPlugin / effectsReport;卸载在 -unload.mjs) ──────────────────
|
|
92
|
+
// 职责:注入脚本 → 登记 loadState;卸载由 -unload.mjs 负责(逆序 dispose → 清表 → 派发事件)。
|
|
93
|
+
// 状态耦合:discovered / loaded(**唯一写者**都在这段)、loadingPluginId(窗口开关)、
|
|
94
|
+
// slotRegistry(初始化)、loadState(写)。
|
|
95
|
+
//
|
|
96
|
+
// ── 分区 · 资源取用与解包(已整体搬出到 plugin-host-unpack.mjs) ─────────────────────────────
|
|
97
|
+
// 职责:把"资源根里的文件"变成"可用字节 / 解包产物 / VFS 前缀",**不碰 DOM、不碰面板**。
|
|
98
|
+
// 状态耦合:discovered(读清单找 provider)、providers(读 provider 表)、say / emit;
|
|
99
|
+
// workerPool 与该区常量由解包模块自持,宿主只拿引用做卸载清理。
|
|
100
|
+
//
|
|
101
|
+
// ── 分区 · 装载编排(report 在 -report.mjs) ─────────────────────────────────────────────────
|
|
102
|
+
// 职责:发现清单 → 建注册表 → 注入脚本 → 统计结果;以及 provider 查询与回收报告。
|
|
103
|
+
// 状态耦合:几乎全部只读(loaded / discovered / providers / pluginEffects /
|
|
104
|
+
// registeredEffects / slotRegistry)。
|
|
105
|
+
//
|
|
106
|
+
|
|
107
|
+
// ── 高频共享状态:已收敛为一个显式 `ctx`(B0 完成)────────────────────────
|
|
108
|
+
// 位置:工厂体开头(搜 `const ctx = {`)。**唯一一份定义**,内部函数一律用 `ctx.xxx` 读写;
|
|
109
|
+
// 不再有同名的工厂作用域变量(有就是漏改,用 grep `\bc<名字>\b` 自查能发现)。
|
|
110
|
+
// 为什么收敛:这 9 个状态被 5~12 个函数跨分区读写,依赖关系原先只能靠通读全文才看得见;
|
|
111
|
+
// 收进一个具名对象后,"谁依赖什么"体现在 `ctx.` 前缀上,也是将来把这些函数搬出本文件的前提
|
|
112
|
+
// (B1+ 只需传一个 ctx 参数,不必重连一整片闭包)。
|
|
113
|
+
//
|
|
114
|
+
// 字段 | 类型 | 谁写 | 谁读 | 时序约束 / 改动时注意
|
|
115
|
+
// ctx.say | 日志器(初始化后不变) | 无 | 12 个函数 | 只加不换:换实现会改变全部日志格式
|
|
116
|
+
// ctx.emit | 派发器(初始化后不变) | 无 | 8 个函数 | 事件名是外部契约(main/panel 订阅)
|
|
117
|
+
// ctx.discovered | 数组 | doLoadAll(唯一) | 9 个函数 | 只在装载期整体替换;运行期读到的是最近一次快照
|
|
118
|
+
// ctx.loaded | 布尔 | doLoadAll(唯一) | 9 个函数 | 是"已装载"的唯一判定(isLoaded 直接返回它)
|
|
119
|
+
// ctx.providers | Map | register / unloadPlugin | 8 个函数 | 覆盖会被显式拒绝;删要连 disposer 一起
|
|
120
|
+
// ctx.slotRegistry | 对象或 null | initSlotRegistry(唯一) | 8 个函数 | **惰性初始化**:脚本注入前必须已建好,否则注册静默丢弃
|
|
121
|
+
// ctx.loadState | Map | register / loadPlugin / unloadPlugin | 7 个函数 | 装载状态与错误文案的唯一来源,卸载要删否则残留
|
|
122
|
+
// ctx.registeredEffects | Map | registerEffect / unloadPlugin | 6 个函数 | 能力键白名单来源;删漏会让控件显示"未注册成功"
|
|
123
|
+
// ctx.pluginEffects | Map | addEffect / unloadPlugin | 5 个函数 | 插件级 disposer 容器;卸载按**逆序**执行,依赖它
|
|
124
|
+
//
|
|
125
|
+
// 留在闭包(**不**进 ctx,理由:引用数 ≤4,进 ctx 只会把 ctx 变成杂物袋):
|
|
126
|
+
// base、manifestProvides、manifestEffects、registrationRejects、loadingPluginId、lastLoadedPluginId、
|
|
127
|
+
// pluginSettings、loadingPromise、workerPool、capCache/CAP_TTL_MS、warmupInflight、WARMUP_PHASE_TEXT、
|
|
128
|
+
// ORDINARY_ASSET_EXT、MAX_BUNDLE_BYTES、MAX_FRAGMENT_CHARS、hostApi
|
|
129
|
+
//
|
|
130
|
+
// ── 与"将来拆分"的关系 ────────────────────────────────────────────────────
|
|
131
|
+
// 调研结论(docs/设计/plugin-host拆分调研.md):**暂不拆文件**。B0(状态收敛)已完成;
|
|
132
|
+
// 若继续 B1,就是把 ctx 作为参数把"资源取用/解包"这组(23 个函数、64% 代码)整体搬出去。
|
|
133
|
+
// ============================================================================
|
|
134
|
+
export function createPluginHost(opts = {}) {
|
|
135
|
+
/**
|
|
136
|
+
* 高频共享状态容器(B0 收敛)。
|
|
137
|
+
*
|
|
138
|
+
* 为什么要有它:这 9 个状态原本是工厂作用域的散装闭包变量,被 5~12 个内部函数直接读写,
|
|
139
|
+
* 依赖关系只能靠通读全文才看得见。收进一个具名对象后,"谁依赖什么"在函数签名与
|
|
140
|
+
* `ctx.` 前缀上一眼可辨 —— 这是将来把这些函数搬出本文件(B1+)的前提:
|
|
141
|
+
* 届时它们只需要接收一个 ctx 参数,而不是重连一整片闭包。
|
|
142
|
+
*
|
|
143
|
+
* 约定(改这个对象之前先读):
|
|
144
|
+
* · 只有**高频**状态进 ctx(引用数 ≥5);低频状态留在原地,避免 ctx 变成杂物袋
|
|
145
|
+
* · 全部读写都必须经过 `ctx.`,不许再把名字解构回局部变量(否则又变回隐式闭包)
|
|
146
|
+
* · 三条时序约束见文件头「9 个高频共享状态」表:slotRegistry 惰性初始化、
|
|
147
|
+
* loadingPluginId 窗口语义(不在本对象内,仍在闭包)、workerPool 生命周期
|
|
148
|
+
*/
|
|
149
|
+
const ctx = {
|
|
150
|
+
/** 日志器(初始化后不变) */
|
|
151
|
+
say: makeLogger(opts.log),
|
|
152
|
+
/** diag 事件通道(由 main.js 注入 app/status.js 的 diagEvent;缺省空函数) */
|
|
153
|
+
emit: typeof opts.diagEvent === 'function' ? opts.diagEvent : () => {},
|
|
154
|
+
/** id → provider(插件 register 上来的对象);写者:register / unloadPlugin */
|
|
155
|
+
providers: new Map(),
|
|
156
|
+
/** 注册成功的效果(effectId → { pluginId, fragment });写者:registerEffect / unloadPlugin */
|
|
157
|
+
registeredEffects: new Map(),
|
|
158
|
+
/** id → { loaded, error, files, ms }(装载状态与错误文案的唯一来源) */
|
|
159
|
+
loadState: new Map(),
|
|
160
|
+
/** 插件级 effect 容器:pluginId → [{ kind, label, dispose }](注册顺序,回收时逆序执行) */
|
|
161
|
+
pluginEffects: new Map(),
|
|
162
|
+
/** 插槽注册表(**惰性初始化**:必须在注入任何插件脚本之前建好,否则注册会被静默丢弃) */
|
|
163
|
+
slotRegistry: null,
|
|
164
|
+
/** 本次发现的插件清单(装载期整体替换,运行期读到的是最近一次快照) */
|
|
165
|
+
discovered: [],
|
|
166
|
+
/** 是否已完成一次装载(doLoadAll 是唯一写者;卸载时要同步维护,否则 isLoaded 会骗人) */
|
|
167
|
+
loaded: false,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// 宿主报告:搬到 plugin-host-report.mjs(B1)—— 纯读,只依赖 ctx
|
|
171
|
+
const report = createReport({ ctx });
|
|
172
|
+
// diag 事件通道(由 main.js 注入 app/status.js 的 diagEvent):解包来源是**验收要读**的事实,
|
|
173
|
+
// 不能只写 console —— 页面上层与测试都靠 diag 取证。
|
|
174
|
+
const base = String(opts.baseUrl || '').replace(/\/$/, '');
|
|
175
|
+
|
|
176
|
+
// 资源取用与解包(已搬到 plugin-host-unpack.mjs)—— 参数只有 io / state / limits 三个窄视图
|
|
177
|
+
// workerPool 由该模块**创建并持有**(生命周期归它),这里只拿引用给卸载流程做清理
|
|
178
|
+
const unpack = createUnpack({
|
|
179
|
+
io: { say: ctx.say, emit: ctx.emit },
|
|
180
|
+
state: { providers: ctx.providers, getDiscovered: () => ctx.discovered },
|
|
181
|
+
limits: { base },
|
|
182
|
+
});
|
|
183
|
+
const { materialize, fetchFile, providersFor, findProvider, prefilterReason, pool: workerPool } = unpack;
|
|
184
|
+
|
|
185
|
+
/** id → provider(插件 register 上来的对象) */
|
|
186
|
+
/** id → 清单里声明的角色(provides)——插件自己 register 时不一定带,以清单为准 */
|
|
187
|
+
const manifestProvides = new Map();
|
|
188
|
+
// 清单里声明的 effects(pluginId → [{id,label,params}]):能力键白名单与控件校验的唯一依据。
|
|
189
|
+
// 与 manifestProvides 同一模式 —— 角色/效果都以**清单**为准,注册时不能自说自话。
|
|
190
|
+
const manifestEffects = new Map();
|
|
191
|
+
|
|
192
|
+
// 卸载与回收(plugin-host-unload.mjs,B1):workerPool 由 unpack 模块创建并持有,这里传引用供终止清理
|
|
193
|
+
// 放在 manifestEffects 声明之后:它要在**初始化时**拿到这张表(提前引用会撞 TDZ)
|
|
194
|
+
const { unloadPlugin, unloadAll } = createUnload({
|
|
195
|
+
ctx, manifestEffects, workerPool, publishSlotStats, publishEffectStats,
|
|
196
|
+
});
|
|
197
|
+
// 注册成功的效果(effectId → { pluginId, fragment }):只有进这里的 id 才进能力键白名单
|
|
198
|
+
/** 当前正在注入脚本的插件 id(register* 靠它认领"这是谁的注册") */
|
|
199
|
+
let loadingPluginId = '';
|
|
200
|
+
/** 效果表达式长度上限:见 registerEffect 里"超长直接拒"的理由 */
|
|
201
|
+
const MAX_FRAGMENT_CHARS = 2000;
|
|
202
|
+
/** id → { loaded, error, files, ms } 装载状态(排障用) */
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 插件级 effect 容器:pluginId → [{ kind, label, dispose }](**注册顺序**,回收时逆序执行)。
|
|
206
|
+
*
|
|
207
|
+
* 为什么要有它:在此之前 register* 只是往宿主表里塞、从不删除,
|
|
208
|
+
* 插件卸载/禁用没有任何可执行的回收动作 —— 插槽一旦能改界面,残留就只能靠人肉排查。
|
|
209
|
+
* 这里给每个插件一份"我注册了什么"的清单,把回收变成一次可验证的批量操作。
|
|
210
|
+
*/
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 窗口外注册的显式拒绝记录(最近 50 条,供排障与验收读取)。
|
|
214
|
+
*
|
|
215
|
+
* 为什么单独记:以前异步注册只有一条 warn,既看不出是哪个插件,也没法被判据读取;
|
|
216
|
+
* 典型表现是"插件装了、控件不出现、日志里一句话都搜不到"。
|
|
217
|
+
*/
|
|
218
|
+
const registrationRejects = [];
|
|
219
|
+
|
|
220
|
+
/** 最近一次装载完成的插件 id:窗口外注册时用它指出"大概是哪个插件的异步回调" */
|
|
221
|
+
let lastLoadedPluginId = '';
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 插槽注册表(装载时按 /api/plugins 的 slotCatalog 建)。
|
|
225
|
+
*
|
|
226
|
+
* 为什么在 doLoadAll 里建、而不是构造期建:目录与"每个插件声明了哪些插槽"都来自那一份响应;
|
|
227
|
+
* 构造期建只能用空目录,之后还得重建(重建会把已登记项丢掉)。
|
|
228
|
+
* 阶段口径 openStage='S1':本阶段所有插槽都未开放 ⇒ 注册一律被 slot-not-open 拒,
|
|
229
|
+
* 这正是 S1 的语义(只搭基础设施,不动界面)。
|
|
230
|
+
*/
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 每个插件在清单里声明的设置项(`plugin.json` 的 settings[])。
|
|
234
|
+
* 用途:插件 render(props) 要能看到"自己声明的键当前是多少"——
|
|
235
|
+
* 面板侧据此构造 `props.settings`(当前值 ?? 声明默认值),插件不必再自己去猜键名。
|
|
236
|
+
* 缺省空 Map(老插件没声明 settings 时就是空对象,插件侧应容忍)。
|
|
237
|
+
*/
|
|
238
|
+
const pluginSettings = new Map();
|
|
239
|
+
|
|
240
|
+
/** 登记一个可回收 effect(同一插件按注册顺序追加) */
|
|
241
|
+
// ── 分区 · 基础工具与统计发布 ──────────────────────────────────────────────────────────────
|
|
242
|
+
// 本区放"其它区都要用的小工具":效果容器入口(addEffect)、阶段归一(normalizeStage)、
|
|
243
|
+
// 两处 window 统计发布(插槽/效果)。注册拒绝出口与老控件包装已随注册模块搬走。
|
|
244
|
+
// 状态耦合:addEffect / publishEffectStats 写 pluginEffects;publishSlotStats 读 slotRegistry。
|
|
245
|
+
function addEffect(pluginId, effect) {
|
|
246
|
+
const id = String(pluginId || '');
|
|
247
|
+
if (!id || !effect || typeof effect.dispose !== 'function') return false;
|
|
248
|
+
const list = ctx.pluginEffects.get(id) || [];
|
|
249
|
+
list.push(effect);
|
|
250
|
+
ctx.pluginEffects.set(id, list);
|
|
251
|
+
publishEffectStats();
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 当前阶段:**只信服务端**(`/api/plugins` 的 currentStage,由 slot-catalog 的 CURRENT_STAGE 派生)。
|
|
257
|
+
*
|
|
258
|
+
* 唯一来源原则:阶段是"服务端开放到哪一步"的事实,前端自己存一份必然漂移
|
|
259
|
+
* (上一轮就埋过一个 CURRENT_SLOT_STAGE 常量,已经删掉)。
|
|
260
|
+
* 回退 'S1' 只是过渡:字段缺失时按最保守处理(所有插槽都未开放)——
|
|
261
|
+
* 宁可全部拒绝并报 slot-not-open,也不要因为读不到就放开。
|
|
262
|
+
* @param {unknown} raw 服务端下发的 currentStage
|
|
263
|
+
* @returns {string}
|
|
264
|
+
*/
|
|
265
|
+
function normalizeStage(raw) {
|
|
266
|
+
const text = String(raw == null ? '' : raw).trim().toUpperCase();
|
|
267
|
+
return /^S[1-9]$/.test(text) ? text : 'S1';
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* 把插槽统计挂到 window.__SLOT_STATS__(测试读它;**不动既有 dataset 字段**)。
|
|
271
|
+
* 为什么用 window 而不是 dataset:dataset 参与 outerHTML 快照比对(测试侧定案),
|
|
272
|
+
* 往里塞会变的统计会让"还原性"断言互相打架。
|
|
273
|
+
*/
|
|
274
|
+
function publishSlotStats() {
|
|
275
|
+
try {
|
|
276
|
+
if (typeof window !== 'undefined' && window && typeof window === 'object') {
|
|
277
|
+
window.__SLOT_STATS__ = ctx.slotRegistry ? ctx.slotRegistry.stats() : null;
|
|
278
|
+
}
|
|
279
|
+
} catch { /* 无 window(node 测试)时忽略 */ }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** 把 effect 总数写到 dataset(不新增 dataset 字段族,只加一个计数便于验收) */
|
|
283
|
+
function publishEffectStats() {
|
|
284
|
+
try {
|
|
285
|
+
let total = 0;
|
|
286
|
+
for (const list of ctx.pluginEffects.values()) total += list.length;
|
|
287
|
+
document.documentElement.dataset.pluginEffects = String(total);
|
|
288
|
+
} catch { /* 无 DOM(测试环境)时忽略 */ }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
/** /api/plugins 返回的清单快照 */
|
|
295
|
+
/**
|
|
296
|
+
* 进行中的 loadAll(并发去重)。
|
|
297
|
+
* 为什么需要:启动预热与"用户一打开就点资源包"可能同时触发装载,
|
|
298
|
+
* 两路各发一次 /api/plugins、各评估一遍插件,既浪费也会让提供者注册更晚。
|
|
299
|
+
* 这里让后来者复用同一个 Promise:清单只取一次、插件只装载一次。
|
|
300
|
+
*/
|
|
301
|
+
let loadingPromise = null;
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
/** 插件侧可见的宿主面(插件脚本顶层会读 window.SpineViewerPlugins) */
|
|
305
|
+
// ── 分区 · 注册入口组装(hostApi;实现在 plugin-host-registration.mjs) ────────────────────
|
|
306
|
+
// 本区只做两件事:把 ctx 与各表以"窄视图"注入注册模块,再把返回的方法拼成 hostApi。
|
|
307
|
+
// 共同约束(仍在注册模块里执行):**只接受装载期同步调用**(loadingPluginId 非空),
|
|
308
|
+
// 异步注册一律显式拒绝并给原因。
|
|
309
|
+
// 状态耦合:providers / loadState / registeredEffects(经注册模块写);
|
|
310
|
+
// slotRegistry、manifestEffects、loadingPluginId(读)。
|
|
311
|
+
// 参数刻意用"窄视图"而不是整块 ctx:io(日志/事件)/ tables(它读写的三张表 + 插槽表 getter)/
|
|
312
|
+
// decls(清单声明与拒绝记录)/ probe(装载期窗口与最后装载者,必须现读)/ sinks(回收登记与统计发布)
|
|
313
|
+
const registration = createRegistration({
|
|
314
|
+
io: { say: ctx.say, emit: ctx.emit },
|
|
315
|
+
tables: {
|
|
316
|
+
providers: ctx.providers,
|
|
317
|
+
loadState: ctx.loadState,
|
|
318
|
+
registeredEffects: ctx.registeredEffects,
|
|
319
|
+
// 惰性初始化:必须用 getter 现取,快照会在"注册表还没建好"时永远拿到 null
|
|
320
|
+
getSlotRegistry: () => ctx.slotRegistry,
|
|
321
|
+
},
|
|
322
|
+
decls: { manifestProvides, manifestEffects, registrationRejects, maxFragmentChars: MAX_FRAGMENT_CHARS },
|
|
323
|
+
probe: {
|
|
324
|
+
getLoadingPluginId: () => loadingPluginId,
|
|
325
|
+
getLastLoadedPluginId: () => lastLoadedPluginId,
|
|
326
|
+
dispatch: (name, detail) => { try { window.dispatchEvent(new CustomEvent(name, { detail })); } catch { /* 忽略 */ } },
|
|
327
|
+
},
|
|
328
|
+
sinks: { addEffect, publishSlotStats },
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* 插件侧可见的宿主面(插件脚本顶层会读 window.SpineViewerPlugins)。
|
|
333
|
+
*
|
|
334
|
+
* B2 之后这里**只剩组装**:注册类方法来自 plugin-host-registration.mjs,
|
|
335
|
+
* 本文件只补"不是注册入口"的成员(version / fetchFile)。
|
|
336
|
+
*/
|
|
337
|
+
const hostApi = {
|
|
338
|
+
version: 1,
|
|
339
|
+
fetchFile,
|
|
340
|
+
register: registration.registerProvider,
|
|
341
|
+
registerEffect: registration.registerEffect,
|
|
342
|
+
registerControls: registration.registerControls,
|
|
343
|
+
slots: registration.slots,
|
|
344
|
+
notify: registration.notify,
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
/** 按清单串行注入某插件的全部脚本(顺序即 plugin.json 的 files[] 顺序:lib/* 先、index.js 后) */
|
|
348
|
+
// ── 分区 · 装载与卸载回收(装配 + effectsReport) ──────────────────────────────────────────
|
|
349
|
+
// 装载:注入脚本 → 登记 loadState;卸载与回收在 plugin-host-unload.mjs(逆序 dispose → 清表 → 派发事件)。
|
|
350
|
+
// 状态耦合:loadState(写)、loaded(读)、providers/registeredEffects/pluginEffects(卸载时删)。
|
|
351
|
+
async function loadPlugin(pl) {
|
|
352
|
+
const id = String(pl && pl.id ? pl.id : '');
|
|
353
|
+
if (!id) return;
|
|
354
|
+
const t0 = Date.now();
|
|
355
|
+
ctx.loadState.set(id, { loaded: false, error: null, files: 0, ms: 0 });
|
|
356
|
+
loadingPluginId = id; // 让 register* 知道这次注册属于哪个插件
|
|
357
|
+
const files = Array.isArray(pl.files) ? pl.files : [];
|
|
358
|
+
let okCount = 0;
|
|
359
|
+
for (const f of files) {
|
|
360
|
+
const ok = await injectScript(`${base}/plugins/${f}`);
|
|
361
|
+
if (ok) okCount += 1;
|
|
362
|
+
else {
|
|
363
|
+
const st = ctx.loadState.get(id);
|
|
364
|
+
if (st && !st.error) st.error = `脚本加载失败: ${f}`;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
loadingPluginId = '';
|
|
368
|
+
const st = ctx.loadState.get(id) || {};
|
|
369
|
+
// 生效判定要分角色(契约 §一 五类角色):
|
|
370
|
+
// asset-provider 必须 register() 交出 canHandle/resolve;
|
|
371
|
+
// 只提供 panel 的插件没有 register() 可言,它交的是 effects/controls ——
|
|
372
|
+
// 拿 register() 当唯一成功标志会把它误判成"加载失败",进而把它的控件也置灰。
|
|
373
|
+
// 生效判定:交出过插槽控件、或注册过效果,都算"这个插件在工作"
|
|
374
|
+
const slotCount = ctx.slotRegistry ? (ctx.slotRegistry.stats().byPlugin[id] || 0) : 0;
|
|
375
|
+
const didPanelWork = slotCount > 0
|
|
376
|
+
|| [...ctx.registeredEffects.values()].some((e) => e.pluginId === id);
|
|
377
|
+
const providerOk = ctx.providers.has(id);
|
|
378
|
+
const error = st.error || (providerOk || didPanelWork ? null : '脚本已加载但没有调用 register()');
|
|
379
|
+
ctx.loadState.set(id, { ...st, loaded: providerOk || didPanelWork, error, files: okCount, ms: Date.now() - t0 });
|
|
380
|
+
lastLoadedPluginId = id;
|
|
381
|
+
if (error) ctx.say.warn(`插件 ${id} 未生效:${error}(其余插件与页面不受影响)`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* 回收清单(可读):每个插件登记了多少 effect、现在还剩多少控件节/效果/提供者。
|
|
387
|
+
*
|
|
388
|
+
* 为什么需要:本轮界面还没接插槽,但"回收到底发生了没有"必须能被读取(验收与排障都靠它)。
|
|
389
|
+
* @returns {{totalEffects:number, plugins:Array, rejects:Array}}
|
|
390
|
+
*/
|
|
391
|
+
function effectsReport() {
|
|
392
|
+
const ids = [...new Set([...ctx.pluginEffects.keys(), ...ctx.loadState.keys(), ...ctx.providers.keys(),
|
|
393
|
+
...(ctx.slotRegistry ? Object.keys(ctx.slotRegistry.stats().byPlugin) : [])])];
|
|
394
|
+
const plugins = ids.filter(Boolean).map((id) => {
|
|
395
|
+
const list = ctx.pluginEffects.get(id) || [];
|
|
396
|
+
return {
|
|
397
|
+
id,
|
|
398
|
+
effects: list.length,
|
|
399
|
+
kinds: list.map((e) => e.kind),
|
|
400
|
+
slots: ctx.slotRegistry ? (ctx.slotRegistry.stats().byPlugin[id] || 0) : 0,
|
|
401
|
+
effectsRegistered: [...ctx.registeredEffects.values()].filter((e) => e.pluginId === id).length,
|
|
402
|
+
provider: ctx.providers.has(id),
|
|
403
|
+
loaded: !!(ctx.loadState.get(id) || {}).loaded,
|
|
404
|
+
};
|
|
405
|
+
});
|
|
406
|
+
const totalEffects = [...ctx.pluginEffects.values()].reduce((n, l) => n + l.length, 0);
|
|
407
|
+
return { totalEffects, plugins, rejects: registrationRejects.slice() };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ---------------------------------------------------------------- 预筛与物化(resolve → VFS)
|
|
411
|
+
//
|
|
412
|
+
// 为什么要预筛:canHandle 必须看**完整容器字节**才能判定(它要解析 UnityFS 的 CAB 节点,
|
|
413
|
+
// 实测只给前 64 字节时连真 bundle 都判 false),而单个包 44MB、单次判定 ~95ms。
|
|
414
|
+
// 对目录里每个文件都这么来一遍是不可接受的,所以先按"根类型 + 后缀 + 大小"筛掉绝大多数,
|
|
415
|
+
// 只对候选包读全量字节。
|
|
416
|
+
//
|
|
417
|
+
// **别把这层去掉**:直接对整目录跑 findProvider 会让一次扫描读几百 MB、卡住主线程。
|
|
418
|
+
|
|
419
|
+
/** 普通模型资产的后缀:这些不可能是资源包,别为它们读全量 */
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
// 预热:搬到 plugin-host-warmup.mjs(B1)—— 只依赖 ctx.say / ctx.emit + 服务端事件流
|
|
423
|
+
const warmup = createWarmup({ ctx });
|
|
424
|
+
|
|
425
|
+
// ── 分区 · 资源取用与解包(已整体搬出到 plugin-host-unpack.mjs) ───────────────────────────
|
|
426
|
+
// 解包主流程(缓存命中 → provider 解析 → 产物校验 → 注册 VFS)与预热都在搬出去的模块里,
|
|
427
|
+
// 本区只剩装配那一行。状态耦合(在模块内):providers / discovered(读)、say / emit。
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* 读启用清单并逐个加载。**任何失败都不抛**:宿主是增强项,不能拖垮页面。
|
|
431
|
+
*
|
|
432
|
+
* 幂等 + 并发去重(装载入口每次都会先调它,热路径必须几乎零成本):
|
|
433
|
+
* · 已装载完成 → 直接返回上次结果,不再请求 /api/plugins;
|
|
434
|
+
* · 正在装载 → 后来者复用同一个 Promise(清单只取一次、插件只评估一次)。
|
|
435
|
+
* 这解决了"页面刚起、插件脚本还在注入时就发起装载"的时序竞争。
|
|
436
|
+
* @returns {Promise<{discovered: number, registered: number}>}
|
|
437
|
+
*/
|
|
438
|
+
// ── 分区 · 装载编排(loadAll / doLoadAll / initSlotRegistry) ──────────────────────────────
|
|
439
|
+
// 装载编排与插槽注册表初始化留在这里(它们要驱动脚本注入与窗口开关);
|
|
440
|
+
// provider 查询与状态报告分别在 -unpack.mjs / -report.mjs。
|
|
441
|
+
// 状态耦合:discovered / loaded(**唯一写者** doLoadAll)、slotRegistry(唯一写者 initSlotRegistry)。
|
|
442
|
+
async function loadAll() {
|
|
443
|
+
if (ctx.loaded) return { discovered: ctx.discovered.length, registered: countRegistered() };
|
|
444
|
+
if (loadingPromise) return loadingPromise;
|
|
445
|
+
loadingPromise = doLoadAll().finally(() => { loadingPromise = null; });
|
|
446
|
+
return loadingPromise;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** 已成功装载(注册提供者)的插件数 */
|
|
450
|
+
function countRegistered() {
|
|
451
|
+
return [...ctx.loadState.values()].filter((s) => s.loaded).length;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* 按 `/api/plugins` 的响应建插槽注册表(**必须在注入任何插件脚本之前**)。
|
|
456
|
+
*
|
|
457
|
+
* 为什么单独成函数:它要读四类信息(目录 / 每插件声明 / 当前阶段 / 每插件 settings 键),
|
|
458
|
+
* 塞在 doLoadAll 里会把装载流程读断(也过不了"单函数 50 行"的口径)。
|
|
459
|
+
* @param {object} data /api/plugins 响应
|
|
460
|
+
* @param {Array} discovered 本次发现的插件清单
|
|
461
|
+
*/
|
|
462
|
+
function initSlotRegistry(data, discoveredList) {
|
|
463
|
+
const declared = {};
|
|
464
|
+
for (const pl of discoveredList) {
|
|
465
|
+
const pid = String(pl && pl.id ? pl.id : '');
|
|
466
|
+
declared[pid] = Array.isArray(pl && pl.slots) ? pl.slots : [];
|
|
467
|
+
pluginSettings.set(pid, Array.isArray(pl && pl.settings) ? pl.settings : []);
|
|
468
|
+
}
|
|
469
|
+
ctx.slotRegistry = createSlotRegistry({
|
|
470
|
+
catalog: Array.isArray(data && data.slotCatalog) ? data.slotCatalog : [],
|
|
471
|
+
declaredByPlugin: declared,
|
|
472
|
+
// 阶段只信服务端(currentStage);字段缺失时 normalizeStage 回退 'S1'(过渡兜底)
|
|
473
|
+
openStage: normalizeStage(data && data.currentStage),
|
|
474
|
+
// 内置控件 id:键值插槽(panel.control.*)只能指向它们(替换既有控件,不许静默新建)
|
|
475
|
+
builtinControlIds: new Set(PANEL_GROUPS.flatMap((g) => (g.controls || []).map((c) => String((c && c.id) || '')))),
|
|
476
|
+
// 内置组 id:新建分组必须带 plugin.<pluginId>. 前缀(设计 §七·一)
|
|
477
|
+
groupIds: new Set(PANEL_GROUPS.map((g) => String(g.id || ''))),
|
|
478
|
+
// spec.setting 校验用:该插件清单里声明过的 settings 键
|
|
479
|
+
settingsKeysOf: (pid) => (pluginSettings.get(String(pid || '')) || []).map((s) => String(s && s.key || '')),
|
|
480
|
+
isInLoadWindow: () => !!loadingPluginId,
|
|
481
|
+
onReject: (rej) => {
|
|
482
|
+
ctx.say.warn(`插槽注册被拒(${rej.code}):${rej.why}`);
|
|
483
|
+
ctx.emit('plugin.slot-rejected', rej);
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
publishSlotStats();
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** loadAll 的实际执行体(内部使用;失败只记日志不抛) */
|
|
490
|
+
async function doLoadAll() {
|
|
491
|
+
try {
|
|
492
|
+
// 幂等 GET ⇒ 走统一层(保留 no-store:改完插件要立刻看到,不能读缓存)。
|
|
493
|
+
// bundle-failures.js 拉的是同一个 URL,两处并发时会被 api.js 合并成一次真实请求。
|
|
494
|
+
const data = await getJson(`${base}/api/plugins`, { cache: 'no-store' });
|
|
495
|
+
ctx.discovered = Array.isArray(data && data.plugins) ? data.plugins : [];
|
|
496
|
+
// 建插槽注册表(**必须在注入任何插件脚本之前**):目录与"谁声明了什么"都来自这一份响应,
|
|
497
|
+
// 晚一步建就会出现"插件已经能 register、注册表还不存在"的空窗。
|
|
498
|
+
initSlotRegistry(data, ctx.discovered);
|
|
499
|
+
} catch (e) {
|
|
500
|
+
ctx.say.warn(`插件清单获取失败(页面其余功能不受影响):${(e && e.message) || e}`);
|
|
501
|
+
return { discovered: 0, registered: 0 };
|
|
502
|
+
}
|
|
503
|
+
if (!ctx.discovered.length) {
|
|
504
|
+
ctx.say.info('没有启用任何插件(config/plugins.json 可开关)');
|
|
505
|
+
ctx.loaded = true;
|
|
506
|
+
return { discovered: 0, registered: 0 };
|
|
507
|
+
}
|
|
508
|
+
// 内置装载器(builtin:true)是**纯声明条目**:它没有 plugin.json 那样的代码文件可注入,
|
|
509
|
+
// 存在的意义是给服务端提供者注册表一份统一的"颜色 / 候选 / 指纹"表(契约 §一·五)。
|
|
510
|
+
// 过去把它们当普通插件走"注入脚本 → 等 register()"的生效判定,files 为空 ⇒ 循环不跑,
|
|
511
|
+
// 必然产出「插件未生效:脚本已加载但没有调用 register()」这种误报(实测 builtin-spine /
|
|
512
|
+
// builtin-live2d 各一条)。这里把它们从"需要装载"的名单里分出来:只声明、不装载、不算失败。
|
|
513
|
+
// exec 运行时(runtime:"exec")同理:它的代码在**服务端进程**里(宿主 spawn 一个子进程),
|
|
514
|
+
// 浏览器里没有脚本可注入 —— 前端只消费它解包结果(`/api/unpack/resolve` 那一路)。
|
|
515
|
+
// files[] 本来就是空的,混进"需要注入脚本"的名单 ⇒ 必然报同样的假告警
|
|
516
|
+
// (实测 8804:「插件 unitypy-bundle 未生效:脚本已加载但没有调用 register()」)。
|
|
517
|
+
const scripts = ctx.discovered.filter((p) => p && p.builtin !== true && p.runtime !== 'exec');
|
|
518
|
+
const builtinDecls = ctx.discovered.filter((p) => p && p.builtin === true);
|
|
519
|
+
const execDecls = ctx.discovered.filter((p) => p && p.runtime === 'exec');
|
|
520
|
+
ctx.say.info(`发现 ${ctx.discovered.length} 个启用插件:${ctx.discovered.map((p) => p.id).join(', ')}`
|
|
521
|
+
+ (builtinDecls.length ? `(其中 ${builtinDecls.length} 个为内置装载器声明,无代码需注入)` : '')
|
|
522
|
+
+ (execDecls.length ? `(${execDecls.length} 个为 exec 运行时,代码在服务端进程里跑,前端只登记声明)` : ''));
|
|
523
|
+
// 声明登记对所有提供者都要做(含 exec / 内置):归属着色与候选名单靠这张表,
|
|
524
|
+
// 跳过它们会让"这个包是谁解的"查不到声明色。
|
|
525
|
+
for (const pl of [...scripts, ...execDecls]) {
|
|
526
|
+
if (pl && pl.id) {
|
|
527
|
+
manifestProvides.set(pl.id, Array.isArray(pl.provides) ? pl.provides.map(String) : []);
|
|
528
|
+
// effects 可选;形状不合法在这里就归一成空数组(服务端已校验过,这里只防脏数据)
|
|
529
|
+
manifestEffects.set(pl.id, Array.isArray(pl.effects) ? pl.effects.filter((e) => e && e.id) : []);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
for (const pl of scripts) {
|
|
533
|
+
try { await loadPlugin(pl); } catch (e) { ctx.say.warn(`插件 ${pl && pl.id} 装载异常:${(e && e.message) || e}`); }
|
|
534
|
+
}
|
|
535
|
+
ctx.loaded = true;
|
|
536
|
+
const ok = countRegistered();
|
|
537
|
+
// "未生效"只对账**需要装载代码的**那些:内置声明不参与 register(),算进分母就又造假告警
|
|
538
|
+
ctx.say.info(`插件装载完成:需装载 ${scripts.length} 个、注册 ${ok} 个提供者`
|
|
539
|
+
+ (ok < scripts.length ? `(${scripts.length - ok} 个未生效,见上方警告)` : ''));
|
|
540
|
+
// 面板侧要知道"插件已经交完声明了" —— 它启动时先建面板,插件是之后异步注入的,
|
|
541
|
+
// 不通知就永远看不到插件控件(实测:控件节收到了,但面板不会自己重建)。
|
|
542
|
+
try {
|
|
543
|
+
window.dispatchEvent(new CustomEvent(EVENTS.PLUGINS_LOADED, {
|
|
544
|
+
detail: { discovered: ctx.discovered.length, registered: ok, slots: ctx.slotRegistry ? ctx.slotRegistry.stats().total : 0 },
|
|
545
|
+
}));
|
|
546
|
+
} catch { /* 忽略 */ }
|
|
547
|
+
return { discovered: ctx.discovered.length, registered: ok };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
// 插件脚本顶层读的就是它:必须在注入脚本**之前**挂好
|
|
553
|
+
window.SpineViewerPlugins = hostApi;
|
|
554
|
+
// 设计 §二 的命名收敛:插槽 API 挂在 ModelStagePlugins 命名空间上;
|
|
555
|
+
// 旧的 SpineViewerPlugins 保留(现有插件仍用它),迁移留给 S2 之后
|
|
556
|
+
try {
|
|
557
|
+
window.ModelStagePlugins = Object.assign(window.ModelStagePlugins || {}, { slots: hostApi.slots });
|
|
558
|
+
} catch { /* 无 window 环境忽略 */ }
|
|
559
|
+
|
|
560
|
+
return {
|
|
561
|
+
loadAll,
|
|
562
|
+
/** 卸载单个插件(逆序回收它的 effect,并从宿主表移除痕迹) */
|
|
563
|
+
unloadPlugin,
|
|
564
|
+
/** 卸载全部已装载插件 */
|
|
565
|
+
unloadAll,
|
|
566
|
+
/** 回收清单:每插件登记了哪些 effect、还剩多少控件/效果/提供者(验收读它) */
|
|
567
|
+
effectsReport,
|
|
568
|
+
/**
|
|
569
|
+
* 插槽登记明细(含 spec.render):装配期拿它把"插件声明的控件"合进面板。
|
|
570
|
+
* 返回副本,面板侧改动不会污染注册表。
|
|
571
|
+
*/
|
|
572
|
+
slotSections: () => (ctx.slotRegistry ? ctx.slotRegistry.renderEntries() : []),
|
|
573
|
+
/** 某插件在清单里声明的 settings[](面板侧构造 props.settings 用) */
|
|
574
|
+
pluginSettings: (pluginId) => (pluginSettings.get(String(pluginId || '')) || []).map((s) => ({ ...s })),
|
|
575
|
+
providersFor,
|
|
576
|
+
findProvider,
|
|
577
|
+
fetchFile,
|
|
578
|
+
/** 资源包 → VirtualFS(预筛 → 读全量 → resolve → register),不碰渲染 */
|
|
579
|
+
materialize,
|
|
580
|
+
warmup,
|
|
581
|
+
/** 预筛判定(排障用:为什么某个文件没被当成资源包) */
|
|
582
|
+
prefilterReason,
|
|
583
|
+
report,
|
|
584
|
+
isLoaded: () => ctx.loaded,
|
|
585
|
+
host: hostApi,
|
|
586
|
+
/**
|
|
587
|
+
* 面板层用:插件提交的控件节(含来源插件 id)。
|
|
588
|
+
* 返回副本,避免面板侧改动污染宿主记录。
|
|
589
|
+
*/
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* 面板层用:**注册成功**的效果 id 列表 —— 能力键白名单的唯一来源(契约 §9.3)。
|
|
593
|
+
* 只声明但没注册成功的不在列表里 ⇒ 对应控件会置灰。
|
|
594
|
+
*/
|
|
595
|
+
registeredEffectIds: () => [...ctx.registeredEffects.keys()],
|
|
596
|
+
/**
|
|
597
|
+
* 面板层用:已注册效果的完整声明(按**清单 effects[] 声明顺序** = 效果链的叠加顺序)。
|
|
598
|
+
* fragment 给渲染层拼着色器,params 给"档位 → uniform"换算。
|
|
599
|
+
* @returns {Array<{id:string, pluginId:string, fragment:string, label:string, params:Array}>}
|
|
600
|
+
*/
|
|
601
|
+
effectDeclarations: () => {
|
|
602
|
+
const out = [];
|
|
603
|
+
for (const [pluginId, list] of manifestEffects.entries()) {
|
|
604
|
+
for (const eff of list || []) {
|
|
605
|
+
const reg = ctx.registeredEffects.get(String(eff.id));
|
|
606
|
+
if (!reg || reg.pluginId !== pluginId) continue; // 只报注册成功的
|
|
607
|
+
out.push({
|
|
608
|
+
id: String(eff.id),
|
|
609
|
+
pluginId,
|
|
610
|
+
fragment: reg.fragment,
|
|
611
|
+
label: String(eff.label || eff.id),
|
|
612
|
+
params: Array.isArray(eff.params) ? eff.params.map((p) => ({ ...p })) : [],
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return out;
|
|
617
|
+
},
|
|
618
|
+
/** 面板层用:某个 effect id 属于哪个插件(置灰时写原因) */
|
|
619
|
+
effectOwner: (effectId) => {
|
|
620
|
+
const hit = ctx.registeredEffects.get(String(effectId));
|
|
621
|
+
return hit ? hit.pluginId : '';
|
|
622
|
+
},
|
|
623
|
+
/** 面板层用:插件装载失败清单(id → error),控件置灰时给出"为什么" */
|
|
624
|
+
pluginFailures: () => [...ctx.loadState.entries()]
|
|
625
|
+
.filter(([, st]) => st && st.error)
|
|
626
|
+
.map(([id, st]) => ({ id, error: String(st.error) })),
|
|
627
|
+
};
|
|
628
|
+
}
|