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.
- package/LICENSE +21 -0
- package/README.md +274 -2
- package/THIRD_PARTY_NOTICES.md +90 -0
- package/bin/modelstage.mjs +137 -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 +63 -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 +654 -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 +396 -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/shared/type-labels.mjs +31 -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 +524 -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 +117 -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 +92 -0
- package/src/web/roster/reveal.js +77 -0
- package/src/web/roster/rows.js +248 -0
- package/src/web/roster/snapshot.js +39 -0
- package/src/web/roster/type-filter.js +97 -0
- package/src/web/roster/view.js +761 -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/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/parse-jsonc.mjs +86 -0
- package/tool/resolve-live2d-runtimes.mjs +276 -0
- package/tool/spine-motion-rules.json +101 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
// unpack/plugin-runner.mjs —— 在 node 侧的 vm 沙盒里跑资产插件(17b 的第一步)
|
|
2
|
+
//
|
|
3
|
+
// 做什么:把"插件在无 DOM 的 node 环境里跑起来"这件事收成一个模块 —— 装载一次插件(lib 14 个 + index.js),
|
|
4
|
+
// 之后对任意容器文件调 canHandle / resolve,返回插件的原始产物(prefix / entry / files)。
|
|
5
|
+
//
|
|
6
|
+
// 为什么独立:沙盒装载细节(注入 sha256/inflate/deflate、把 fetch 打桩到 404 让插件走默认规则)
|
|
7
|
+
// 与 17a 的 tool/check-plugin-node.mjs 完全一致;这里抽出来供**服务端常驻**复用,
|
|
8
|
+
// 不复制一份逻辑,也不 import 一个 CLI 脚本(那个带主流程副作用)。
|
|
9
|
+
//
|
|
10
|
+
// 三条硬约束(都是踩过的坑,别改):
|
|
11
|
+
// 1. **跨 realm 禁止 instanceof**:vm 里建出来的 Map/Blob 与宿主各有一套内置构造器,
|
|
12
|
+
// `instanceof Map` 恒 false、`Object.keys(map)` 恒空(Map 没有自有可枚举键)。
|
|
13
|
+
// 一律用鸭子类型(有 size 且能 for...of 迭代 / 有 arrayBuffer 方法);比 size 更硬的判据是 entries 非空。
|
|
14
|
+
// 2. **装载只做一次**:沙盒创建 0.9ms、lib 41ms、index.js 6.4ms(实测 58ms);但 resolve 本体是秒级,
|
|
15
|
+
// 所以真正重要的是"别每次请求都重建沙盒"(会重复 58ms + 丢掉插件内部的 profile 缓存)。
|
|
16
|
+
// 3. **产物不复制**:resolve 返回的文件值可能是 Blob(插件侧 valueTypes 实测 ["Blob"])也可能是
|
|
17
|
+
// Uint8Array/Buffer,落盘时逐个取、取完立刻丢引用(详见 unpackBytes 注释)。
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { createLogger } from '../core/log.mjs';
|
|
22
|
+
// 插件发现复用 registry 的扫描(纪律第二条:禁止相似功能多处实现)
|
|
23
|
+
import { readPlugins } from '../plugins/registry.mjs';
|
|
24
|
+
// 装载细节按清单的 runtime 交给对应适配器(node = vm 沙盒 / exec = 子进程,后者本步仅骨架)。
|
|
25
|
+
// 本文件不再直接碰 vm / zlib / crypto —— 那些都在 runtime/node-vm.mjs 里。
|
|
26
|
+
import { loadWithRuntime, readExecPermission, listRuntimes } from './runtime/index.mjs';
|
|
27
|
+
|
|
28
|
+
// ../../.. ← 本文件在 src/server/unpack/,回退三级才是项目根(少算一级会把 plugins 指到 src/plugins)
|
|
29
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..');
|
|
30
|
+
const PLUGINS_DIR = path.join(ROOT, 'plugins');
|
|
31
|
+
/** 只用于"回退"这类必须留痕的事件(插件 A 解失败、插件 B 接住)—— 静默回退会让插件 bug 永远查不出来 */
|
|
32
|
+
const log = createLogger('unpack');
|
|
33
|
+
/** 插件禁用开关(与 registry 同一份配置;被禁用的插件不参与服务端解包) */
|
|
34
|
+
const PLUGIN_SWITCH_FILE = path.join(ROOT, 'config', 'plugins.json');
|
|
35
|
+
/** 能承担服务端解包的角色:契约 §一 的 asset-provider("能解析资源、交出资产") */
|
|
36
|
+
const SERVER_UNPACK_ROLE = 'asset-provider';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 已装载插件:插件 id → {ok, api?, error?, loadMs?}。
|
|
40
|
+
*
|
|
41
|
+
* 为什么是 Map 而不是单个变量:契约 §一 是「放目录即加能力」,多插件必须能并存;
|
|
42
|
+
* 原来的单例缓存等于把"只支持一个插件"焊死在代码里(加第二个插件就得改代码)。
|
|
43
|
+
* 失败的插件也写进 Map(记原因),避免每次调用都重试同一个坏插件。
|
|
44
|
+
*/
|
|
45
|
+
const pluginCache = new Map();
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 发现能承担服务端解包的插件清单。
|
|
49
|
+
*
|
|
50
|
+
* 复用 registry 的扫描(readPlugins)—— 不在这里另写一套目录遍历:
|
|
51
|
+
* 两套扫描必然漂移(一边认 files[] 顺序、一边认 entry,谁都说不清真相)。
|
|
52
|
+
* @returns {Array<{id: string, files: string[], version: unknown}>} files 是相对 plugins 目录的 POSIX 路径
|
|
53
|
+
*/
|
|
54
|
+
export function listProviderPlugins() {
|
|
55
|
+
return readPlugins(PLUGINS_DIR, PLUGIN_SWITCH_FILE)
|
|
56
|
+
.filter((m) => Array.isArray(m.provides) && m.provides.includes(SERVER_UNPACK_ROLE))
|
|
57
|
+
.map((m) => ({
|
|
58
|
+
id: m.id,
|
|
59
|
+
files: m.files,
|
|
60
|
+
version: m.version,
|
|
61
|
+
// 运行时三件套(registry 已带缺省值:runtime 缺省 'node',另两个 node 下为 null)
|
|
62
|
+
runtime: m.runtime,
|
|
63
|
+
entry: m.entry,
|
|
64
|
+
frameProtocol: m.frameProtocol,
|
|
65
|
+
// exec 运行时的另外三个声明也必须透传:
|
|
66
|
+
// interpreter / depsDir / requestTimeoutMs —— 少一个,适配器就只能用缺省值
|
|
67
|
+
// (缺 depsDir 时它会去插件目录找 .deps,而带 C 扩展的依赖在本项目里必须放本地盘)。
|
|
68
|
+
interpreter: m.interpreter,
|
|
69
|
+
depsDir: m.depsDir,
|
|
70
|
+
requestTimeoutMs: m.requestTimeoutMs,
|
|
71
|
+
// builtin 标记必须透传:内置提供者(builtin-spine/builtin-live2d)也声明 asset-provider,
|
|
72
|
+
// 但它们没有 node 代码、不能被当成插件装载。丢掉这个标记,调用方(解包/装载流程)
|
|
73
|
+
// 会把它们混进 node 插件列表、按 runtime 去 load,必然失败。
|
|
74
|
+
builtin: m.builtin === true,
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* provider 集合的指纹段(写进解包缓存键,插件升级/增减时缓存自然失效)。
|
|
80
|
+
*
|
|
81
|
+
* 单插件时返回该插件 id —— 与历史硬编码值**逐字相同**,所以现有缓存不会因为这次通用化失效;
|
|
82
|
+
* 多插件时才拼成 `a@1+b@2`(顺序无关,排序后拼)。
|
|
83
|
+
* @returns {string}
|
|
84
|
+
*/
|
|
85
|
+
export function providerFingerprint() {
|
|
86
|
+
const list = listProviderPlugins();
|
|
87
|
+
if (!list.length) return 'none';
|
|
88
|
+
if (list.length === 1) return list[0].id;
|
|
89
|
+
return list.map((m) => `${m.id}@${m.version === null || m.version === undefined ? '0' : m.version}`).sort().join('+');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 装载指定插件(同步;按 id 缓存,失败的也缓存原因)。
|
|
94
|
+
*
|
|
95
|
+
* 错误隔离:本函数**绝不抛**给上层 —— 一个插件语法错误/装不起来,只让它自己进
|
|
96
|
+
* `{ok:false,error}`,其它插件与服务启动都不受影响(上层按 ok 决定跳过)。
|
|
97
|
+
* @param {string} pluginId 插件 id(= plugins/ 下的目录名 = plugin.json 的 id)
|
|
98
|
+
* @returns {{ok: boolean, api?: object, error?: string, loadMs?: number}}
|
|
99
|
+
*/
|
|
100
|
+
export function ensurePlugin(pluginId) {
|
|
101
|
+
const id = String(pluginId || '');
|
|
102
|
+
if (!id) return { ok: false, error: '缺少 pluginId' };
|
|
103
|
+
const hit = pluginCache.get(id);
|
|
104
|
+
if (hit) return hit;
|
|
105
|
+
let result;
|
|
106
|
+
try {
|
|
107
|
+
result = loadPlugin(id);
|
|
108
|
+
} catch (e) {
|
|
109
|
+
// 二次兜底:loadPlugin 内部已 catch,这里防"catch 里再抛"把整个调用点带崩
|
|
110
|
+
result = { ok: false, error: String((e && e.message) || e) };
|
|
111
|
+
}
|
|
112
|
+
pluginCache.set(id, result);
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 真正建沙盒并装载一个插件。
|
|
118
|
+
* 加载顺序完全按清单的 `files[]`(插件自己在 plugin.json 里声明先后,runner 不再写死一串 lib 名)。
|
|
119
|
+
* @param {string} pluginId
|
|
120
|
+
* @returns {{ok: boolean, api?: object, error?: string, loadMs?: number}}
|
|
121
|
+
*/
|
|
122
|
+
function loadPlugin(pluginId) {
|
|
123
|
+
const t0 = Date.now();
|
|
124
|
+
try {
|
|
125
|
+
const meta = listProviderPlugins().find((m) => m.id === pluginId);
|
|
126
|
+
if (!meta) throw new Error(`未找到可解包的插件:${pluginId}(需要 provides 含 ${SERVER_UNPACK_ROLE})`);
|
|
127
|
+
// 装载细节按清单的 runtime 交给对应适配器:
|
|
128
|
+
// "node" → vm 沙盒里按 files[] 顺序跑 classic script(历史行为,逐字节等价)
|
|
129
|
+
// "exec" → 宿主 spawn 插件目录里的可执行文件,帧协议通信(本步只有骨架,未实现)
|
|
130
|
+
// 门禁(exec 默认关闭)与适配器选择都在 runtime/index.mjs,本函数不认识具体运行时。
|
|
131
|
+
const r = loadWithRuntime(meta, { pluginsDir: PLUGINS_DIR, root: ROOT });
|
|
132
|
+
if (!r.ok) return { ok: false, error: r.error, code: r.code };
|
|
133
|
+
return { ok: true, api: r.api, loadMs: Date.now() - t0 };
|
|
134
|
+
} catch (e) {
|
|
135
|
+
// 只记原因、不抛:一个插件坏掉不能带走别的插件,也不能带走服务启动
|
|
136
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 挑一个能处理该文件的插件:按清单顺序逐个试 `canHandle`。
|
|
142
|
+
*
|
|
143
|
+
* 返回的 kind 与历史返回值一一对应(单插件时行为逐字不变):
|
|
144
|
+
* picked 找到了(api 可用)
|
|
145
|
+
* not-handled 所有插件都说不处理
|
|
146
|
+
* canhandle-threw 有插件抛错且无人认领
|
|
147
|
+
* plugin-unavailable 所有插件都装不起来(附原因)
|
|
148
|
+
*
|
|
149
|
+
* 为什么是 async:`canHandle` 的契约是"同步返回 boolean",但那只对 node 运行时成立 ——
|
|
150
|
+
* exec 运行时的插件在**另一个进程**里,判定必须过帧协议(异步)。`await` 一个同步 boolean
|
|
151
|
+
* 是完全合法的(拿到的就是它自己),所以这里改成 await 对 node 插件零影响,
|
|
152
|
+
* 却让"判定在子进程里做"成为可能。
|
|
153
|
+
* @param {object} file 交给插件判定的上下文(含 bytes)
|
|
154
|
+
* @returns {Promise<{kind: string, id?: string, api?: object, message?: string}>}
|
|
155
|
+
*/
|
|
156
|
+
/**
|
|
157
|
+
* 列出**所有**声明能处理该文件的插件(按清单顺序)。
|
|
158
|
+
*
|
|
159
|
+
* 为什么不是"取第一个就完事":不同插件的能力边界不一样 ——
|
|
160
|
+
* JS 的 spine 插件认领容器头,但它的**纹理解码表**里没有 ASTC(50)/PVRTC(49) 等格式,
|
|
161
|
+
* 于是"canHandle 说认、resolve 却抛错";而 UnityPy 那条通道(Python)能解。
|
|
162
|
+
* 只取第一个认领者的话,这类包永远解不开 —— 明明有插件能解,却被前一个插件的**能力缺口**卡死。
|
|
163
|
+
* ⇒ 改成"列出所有候选,由调用方按顺序尝试,失败就回退"。
|
|
164
|
+
*
|
|
165
|
+
* 语义不变的部分:**顺序仍是清单顺序**(先 live2d 再 spine 再 unitypy),
|
|
166
|
+
* 所以单插件、以及"第一个就能解"的常见情况,行为与改前逐字一致。
|
|
167
|
+
*
|
|
168
|
+
* @param {object} file 交给插件判定的上下文(含 bytes)
|
|
169
|
+
* @returns {Promise<{handlers: Array<{id: string, api: object}>,
|
|
170
|
+
* loadErrors: string[], firstHandleError: string|null,
|
|
171
|
+
* firstReject: object|null, allUnavailable: boolean}>}
|
|
172
|
+
*/
|
|
173
|
+
async function listHandlers(file) {
|
|
174
|
+
const handlers = [];
|
|
175
|
+
const loadErrors = [];
|
|
176
|
+
let firstHandleError = null;
|
|
177
|
+
let firstReject = null; // 插件给出的"为什么不认领"(可选钩子 explainReject)
|
|
178
|
+
const candidates = listProviderPlugins();
|
|
179
|
+
for (const meta of candidates) {
|
|
180
|
+
const loaded = ensurePlugin(meta.id);
|
|
181
|
+
if (!loaded.ok) {
|
|
182
|
+
loadErrors.push(`${meta.id}: ${loaded.error}`);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
let handled = false;
|
|
186
|
+
try {
|
|
187
|
+
// canHandle 可能是异步的(exec 运行时在子进程里判定);await 同步 boolean 也成立
|
|
188
|
+
handled = !!(await loaded.api.canHandle(file));
|
|
189
|
+
} catch (e) {
|
|
190
|
+
// 单个插件判定抛错不终止:继续问下一个插件
|
|
191
|
+
if (!firstHandleError) firstHandleError = `${meta.id}: ${String((e && e.message) || e)}`;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (handled) {
|
|
195
|
+
handlers.push({ id: meta.id, api: loaded.api });
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
// 不认领时顺带问一句"为什么":canHandle 的签名只有 boolean,原因只有插件内部知道
|
|
199
|
+
// (典型场景:Cubism 2.1 的 .moc 包,两个 provider 都不认,用户只看到"插件判定不处理该文件")。
|
|
200
|
+
if (!firstReject && typeof loaded.api.explainReject === 'function') {
|
|
201
|
+
try {
|
|
202
|
+
const why = await loaded.api.explainReject(file);
|
|
203
|
+
if (why && why.message) firstReject = { id: meta.id, code: why.code || null, message: String(why.message) };
|
|
204
|
+
} catch (e) {
|
|
205
|
+
// 解释失败不影响主判定(它只负责把话说清楚),但记下错误便于排障
|
|
206
|
+
firstReject = { id: meta.id, code: null, message: '', error: String((e && e.message) || e) };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
handlers,
|
|
212
|
+
loadErrors,
|
|
213
|
+
firstHandleError,
|
|
214
|
+
firstReject,
|
|
215
|
+
allUnavailable: candidates.length > 0 && loadErrors.length === candidates.length,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** 值是否是"字节/Blob 形状"(跨 realm:一律鸭子类型,不用 instanceof) */
|
|
220
|
+
function byteish(v) {
|
|
221
|
+
if (!v) return false;
|
|
222
|
+
if (typeof v.arrayBuffer === 'function') return true; // Blob / ArrayBuffer 视图(跨 realm 也成立)
|
|
223
|
+
if (typeof v.length === 'number' && typeof v.slice === 'function') return true; // Uint8Array / Buffer
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** 输入值的人类可读类型(错误信息用:说清"你传进来的到底是什么") */
|
|
228
|
+
function typeName(v) {
|
|
229
|
+
if (v === null) return 'null';
|
|
230
|
+
if (v === undefined) return 'undefined';
|
|
231
|
+
if (Array.isArray(v)) return 'Array';
|
|
232
|
+
return typeof v;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** 普通对象形态 → view(**必须逐值校验形状**,否则任何对象都会被当成路径字典) */
|
|
236
|
+
function viewFromObject(files) {
|
|
237
|
+
const keys = Object.keys(files);
|
|
238
|
+
if (!keys.length) {
|
|
239
|
+
return { ok: false, entries: [], take: async () => null, release: () => {}, error: '产物为空(普通对象形态,0 个键)' };
|
|
240
|
+
}
|
|
241
|
+
const bad = keys.filter((k) => !byteish(files[k]));
|
|
242
|
+
if (bad.length) {
|
|
243
|
+
// 关键:这里**不能**降级成"把键当路径"——那会把 {ok,entries,take,release} 解释成 4~5 个"文件",
|
|
244
|
+
// 产出"看起来成功"的假结果(与 asset-harvester 的 files=0 同类事故)。
|
|
245
|
+
return {
|
|
246
|
+
ok: false,
|
|
247
|
+
entries: [],
|
|
248
|
+
take: async () => null,
|
|
249
|
+
release: () => {},
|
|
250
|
+
error: `产物形状非法:${keys.length} 个键里只有 ${keys.length - bad.length} 个是字节/Blob`
|
|
251
|
+
+ `(非法键示例:${bad.slice(0, 5).join(', ')};收到类型:${typeName(files)})`,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
ok: true,
|
|
256
|
+
entries: keys.map((k) => ({ path: k })),
|
|
257
|
+
take: async (k) => toBytes(files[k]),
|
|
258
|
+
release: () => {},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* 取插件产出的"文件集合"视图(**内部工具**;调用方请直接用 `unpackFile(...).files`)。
|
|
264
|
+
*
|
|
265
|
+
* 判型纪律(两条事故换来的):
|
|
266
|
+
* · 跨 realm 不用 instanceof(vm 里建出的 Map/Blob 与宿主各有一套构造器);
|
|
267
|
+
* · 判型失败**绝不静默换一种解释**:非文件集合一律 ok:false + 说清收到什么类型。
|
|
268
|
+
* 早期版本会把任何对象按 Object.keys 当路径字典,于是把 view 对象本身({ok,entries,take,release})
|
|
269
|
+
* 解释成 4~5 个"文件"并报 ok:true —— 假成功比报错危险得多。
|
|
270
|
+
*
|
|
271
|
+
* @param {unknown} files 插件 raw 产物(沙盒 Map / 普通对象 / 已是 view)
|
|
272
|
+
* @returns {{ok: boolean, entries: Array<{path: string}>, take: Function, release: Function, error?: string}}
|
|
273
|
+
*/
|
|
274
|
+
export function filesView(files) {
|
|
275
|
+
// 已经是 view(鸭子类型):二次包装是调用方 bug,必须响亮报错,不能"再解释一遍"
|
|
276
|
+
if (files && typeof files.take === 'function' && Array.isArray(files.entries)) {
|
|
277
|
+
throw new Error('filesView: 传入的已经是文件视图(不要二次包装);请直接用 unpackFile(...).files');
|
|
278
|
+
}
|
|
279
|
+
// Map 形态:有 size 且 forEach 可用
|
|
280
|
+
const isMap = files && typeof files.size === 'number' && typeof files.forEach === 'function';
|
|
281
|
+
if (!isMap) {
|
|
282
|
+
if (files && typeof files === 'object') return viewFromObject(files);
|
|
283
|
+
return {
|
|
284
|
+
ok: false,
|
|
285
|
+
entries: [],
|
|
286
|
+
take: async () => null,
|
|
287
|
+
release: () => {},
|
|
288
|
+
error: `产物不是文件集合:收到 ${typeName(files)}`,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const entries = [];
|
|
292
|
+
let firstBad = null;
|
|
293
|
+
files.forEach((v, k) => {
|
|
294
|
+
entries.push({ path: String(k) });
|
|
295
|
+
if (firstBad === null && !byteish(v)) firstBad = String(k);
|
|
296
|
+
});
|
|
297
|
+
if (!entries.length) {
|
|
298
|
+
return { ok: false, entries: [], take: async () => null, release: () => {}, error: '产物为空(Map 形态,0 个条目)' };
|
|
299
|
+
}
|
|
300
|
+
if (firstBad !== null) {
|
|
301
|
+
return {
|
|
302
|
+
ok: false,
|
|
303
|
+
entries: [],
|
|
304
|
+
take: async () => null,
|
|
305
|
+
release: () => {},
|
|
306
|
+
error: `产物形状非法:Map 里的值不是字节/Blob(首个非法键:${firstBad})`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
ok: true,
|
|
311
|
+
entries,
|
|
312
|
+
take: async (k) => toBytes(files.get(k)),
|
|
313
|
+
// 写完一个就删掉,让沙盒里的值尽快可回收(42MB 包 RSS 增量约 210MB,逐个释放能显著削峰)
|
|
314
|
+
release: (k) => { try { files.delete(k); } catch { /* 删不掉不影响正确性 */ } },
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** 把 Blob / Uint8Array / Buffer 统一成 Uint8Array(跨 realm:只看有没有 arrayBuffer 方法) */
|
|
319
|
+
async function toBytes(v) {
|
|
320
|
+
if (!v) return null;
|
|
321
|
+
if (typeof v.arrayBuffer === 'function') return new Uint8Array(await v.arrayBuffer());
|
|
322
|
+
if (typeof v.length === 'number' && typeof v.slice === 'function') return new Uint8Array(v);
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* 解包一个容器文件(读字节 → canHandle → resolve)。
|
|
328
|
+
* @param {{rootId: string, rel: string, absPath: string}} opts
|
|
329
|
+
* @returns {Promise<{ok: boolean, reason?: string, message?: string,
|
|
330
|
+
* prefix?: string, entry?: string,
|
|
331
|
+
* // ⚠ files 是**文件视图**(不是插件 raw Map):{ok, entries:[{path}], take(path), release(path)}
|
|
332
|
+
* files?: {ok: boolean, entries: Array<{path: string}>, take: Function, release: Function, error?: string},
|
|
333
|
+
* readMs?: number, unpackMs?: number}>}
|
|
334
|
+
*/
|
|
335
|
+
/**
|
|
336
|
+
* @param {{rootId: string, rel: string, absPath: string,
|
|
337
|
+
* onProgress?: (p: {phase: string, ms: number|null}) => void}} opts
|
|
338
|
+
* onProgress:可选的进度回调。只有 exec 运行时会上报(子进程在自然边界发进度帧);
|
|
339
|
+
* node 插件是进程内同步执行、没有中间边界,一律不发 —— 传了也不会被调用。
|
|
340
|
+
* **挂在 file 上而不是模块级变量**:file 是每次调用现造的,天然按请求隔离
|
|
341
|
+
* (同插件并发两个解包时,A 的进度不会报给 B)。
|
|
342
|
+
*/
|
|
343
|
+
export async function unpackFile({ rootId, rel, absPath, onProgress }) {
|
|
344
|
+
let bytes;
|
|
345
|
+
const t0 = Date.now();
|
|
346
|
+
try {
|
|
347
|
+
// 零拷贝视图:`new Uint8Array(buf)` 会把 Buffer 的**内容再拷一份**(19MB 包就是 +19MB 峰值),
|
|
348
|
+
// 而这里只需要一段只读字节视图。必须带 byteOffset/byteLength —— 小文件可能来自 Buffer 池,
|
|
349
|
+
// 只写 buf.buffer 会把池里相邻的其它内容也包进来。
|
|
350
|
+
// 只读约束:本文件与插件层都**不原地写**这段字节(grep 确认无 bytes[i]= / set / fill / copy),
|
|
351
|
+
// 因此与 Buffer 共享内存在语义上是安全的。
|
|
352
|
+
const buf = fs.readFileSync(absPath);
|
|
353
|
+
bytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
354
|
+
} catch (e) {
|
|
355
|
+
return { ok: false, reason: 'read-failed', message: String((e && e.message) || e) };
|
|
356
|
+
}
|
|
357
|
+
const readMs = Date.now() - t0;
|
|
358
|
+
const name = path.basename(String(rel));
|
|
359
|
+
// absPath 必须一起交给插件:node 插件有 bytes 就够了,但 **exec 插件在另一个进程里**,
|
|
360
|
+
// 它拿不到这段内存 —— 只能靠路径自己去读文件头(帧协议里传字符串,不传 20MB 字节)。
|
|
361
|
+
// 早前这里没放 absPath,于是 exec 插件的 canHandle 永远收到 null ⇒ 永远说"不认"。
|
|
362
|
+
// onProgress 一起交给插件:exec 适配器把它绑到本次请求的挂起记录上(见 exec.mjs 的 request)
|
|
363
|
+
const file = { rootId, rel, name, size: bytes.byteLength, bytes, absPath, onProgress };
|
|
364
|
+
// 列出所有声明能处理的插件(按清单顺序),逐个尝试:
|
|
365
|
+
// canHandle 认领只代表"这个容器我认识",不代表**这个包的全部内容我都能解** ——
|
|
366
|
+
// JS 插件的纹理解码表里没有 ASTC(50)/PVRTC(49) 等格式,会在 resolve 阶段抛错;
|
|
367
|
+
// 而后面还有插件(UnityPy/Python)能解。所以认领者失败要能回退,否则这类包永远打不开。
|
|
368
|
+
const { handlers, loadErrors, firstHandleError, firstReject, allUnavailable } = await listHandlers(file);
|
|
369
|
+
if (!handlers.length) {
|
|
370
|
+
if (allUnavailable) return { ok: false, reason: 'plugin-unavailable', message: loadErrors.join('; ') };
|
|
371
|
+
if (firstHandleError) return { ok: false, reason: 'canhandle-threw', message: firstHandleError };
|
|
372
|
+
// not-handled 时优先用插件给出的原因(如"Cubism 2.1 的 .moc 不支持"),
|
|
373
|
+
// 取不到才回落到原来那句固定文案 —— 后者说清了"没人认领",但说不出"为什么"。
|
|
374
|
+
const why = firstReject;
|
|
375
|
+
return {
|
|
376
|
+
ok: false,
|
|
377
|
+
reason: 'not-handled',
|
|
378
|
+
message: why && why.message ? why.message : '插件判定不处理该文件',
|
|
379
|
+
...(why && why.code ? { code: why.code } : {}),
|
|
380
|
+
...(why && why.id ? { by: why.id } : {}),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** 每个认领者的尝试结果(成功时不带;失败时原样交给上层做诊断) */
|
|
385
|
+
const attempts = [];
|
|
386
|
+
for (const h of handlers) {
|
|
387
|
+
const t1 = Date.now();
|
|
388
|
+
let res;
|
|
389
|
+
try {
|
|
390
|
+
res = await h.api.resolve(file);
|
|
391
|
+
} catch (e) {
|
|
392
|
+
attempts.push({ id: h.id, reason: 'resolve-threw', message: String((e && e.message) || e) });
|
|
393
|
+
// 回退必须留痕:否则"JS 插件有 bug 但被 UnityPy 掩盖"会一直查不出来
|
|
394
|
+
log.warn('插件解包失败,回退到下一个候选', { plugin: h.id, rel, err: String((e && e.message) || e).slice(0, 300) });
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const unpackMs = Date.now() - t1;
|
|
398
|
+
if (!res || !res.prefix || res.entry === undefined) {
|
|
399
|
+
attempts.push({ id: h.id, reason: 'bad-result', message: '插件返回值缺 prefix/entry' });
|
|
400
|
+
log.warn('插件返回不合契约,回退到下一个候选', { plugin: h.id, rel });
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
let view;
|
|
404
|
+
try {
|
|
405
|
+
view = filesView(res.files);
|
|
406
|
+
} catch (e) {
|
|
407
|
+
attempts.push({ id: h.id, reason: 'bad-files', message: String((e && e.message) || e) });
|
|
408
|
+
log.warn('插件产物无法解释为文件集合,回退', { plugin: h.id, rel });
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (!view.ok) {
|
|
412
|
+
attempts.push({ id: h.id, reason: 'empty-files', message: view.error || '产物为空' });
|
|
413
|
+
log.warn('插件产物为空,回退到下一个候选', { plugin: h.id, rel });
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
// prefix/entry 直取插件返回值:不在这里做任何拼装,避免与前端解包路径产生第二套语义
|
|
417
|
+
return {
|
|
418
|
+
ok: true,
|
|
419
|
+
pluginId: h.id,
|
|
420
|
+
prefix: String(res.prefix),
|
|
421
|
+
entry: String(res.entry),
|
|
422
|
+
files: view,
|
|
423
|
+
readMs,
|
|
424
|
+
unpackMs,
|
|
425
|
+
// 只有真的回退过才带这个字段:它是"前一个插件没解成"的证据,不是常态噪音
|
|
426
|
+
...(attempts.length ? { attempts } : {}),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// 全部认领者都失败:报**第一个**失败(通常最有信息量,例如"暂不支持纹理格式 50"),
|
|
431
|
+
// 同时把全部尝试附上,便于判断是"能力缺口"还是"某个插件坏了"。
|
|
432
|
+
const firstFail = attempts[0];
|
|
433
|
+
return {
|
|
434
|
+
ok: false,
|
|
435
|
+
reason: firstFail.reason,
|
|
436
|
+
message: firstFail.message,
|
|
437
|
+
attempts,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* 问插件"这个容器解包要吃多少内存"(契约 插件.md §2.2 的 decompressedBytes)。
|
|
443
|
+
*
|
|
444
|
+
* 代价控制:插件侧走 blocksSummary(只读头 + blocksInfo,**不解压数据块**),
|
|
445
|
+
* 实测 19 包 P50 0.13ms / P90 0.91ms(整包 probe 是 20/56/173ms)。
|
|
446
|
+
* 拿不到(插件不支持/不是它的格式/解析失败)就返回 null ⇒ 核心走回落,绝不阻断加载。
|
|
447
|
+
* @param {{rootId:string, rel:string, absPath:string}} opts
|
|
448
|
+
* @returns {{decompressedBytes:number, compressedBytes:number}|null}
|
|
449
|
+
*/
|
|
450
|
+
export function estimateMemoryOf({ rootId, rel, absPath }) {
|
|
451
|
+
const candidates = listProviderPlugins();
|
|
452
|
+
if (!candidates.length) return null;
|
|
453
|
+
const name = path.basename(String(rel));
|
|
454
|
+
/**
|
|
455
|
+
* 只读文件头部:块表(blocksInfo)几乎总在头部,读到 256KB 就能算;
|
|
456
|
+
* 少数把块表放文件尾的包(实测 aijier_3)会解析失败 ⇒ 退回全量读。
|
|
457
|
+
* 头部字节在多个插件间复用(每个插件各读一次纯属浪费)。
|
|
458
|
+
*/
|
|
459
|
+
const readHead = () => {
|
|
460
|
+
const fd = fs.openSync(absPath, 'r');
|
|
461
|
+
try {
|
|
462
|
+
const size = fs.fstatSync(fd).size;
|
|
463
|
+
const len = Math.min(size, MEMORY_PROBE_HEAD_BYTES);
|
|
464
|
+
const buf = Buffer.allocUnsafe(len);
|
|
465
|
+
fs.readSync(fd, buf, 0, len, 0);
|
|
466
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
467
|
+
} finally {
|
|
468
|
+
fs.closeSync(fd);
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
let headBytes = null;
|
|
472
|
+
let fullBytes = null;
|
|
473
|
+
for (const meta of candidates) {
|
|
474
|
+
const loaded = ensurePlugin(meta.id);
|
|
475
|
+
// 插件装不起来 / 不提供内存声明 ⇒ 试下一个(不阻断,核心会走回落)
|
|
476
|
+
if (!loaded.ok || typeof loaded.api.estimateMemory !== 'function') continue;
|
|
477
|
+
try {
|
|
478
|
+
if (!headBytes) headBytes = readHead();
|
|
479
|
+
let declared = null;
|
|
480
|
+
try {
|
|
481
|
+
declared = loaded.api.estimateMemory({ rootId, rel, name, size: 0, bytes: headBytes });
|
|
482
|
+
} catch {
|
|
483
|
+
declared = null;
|
|
484
|
+
}
|
|
485
|
+
if (declared && declared.decompressedBytes > 0) return declared;
|
|
486
|
+
// 头部不够(块表在尾 / 头部信息不足)⇒ 老老实实读全量再算一次
|
|
487
|
+
if (!fullBytes) {
|
|
488
|
+
const buf = fs.readFileSync(absPath);
|
|
489
|
+
fullBytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
490
|
+
}
|
|
491
|
+
declared = loaded.api.estimateMemory({ rootId, rel, name, size: fullBytes.byteLength, bytes: fullBytes });
|
|
492
|
+
if (declared && declared.decompressedBytes > 0) return declared;
|
|
493
|
+
} catch {
|
|
494
|
+
// 这个插件算不出来(不是它的格式):继续问下一个
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** 内存声明的"只读头部"字节数:块表几乎总在头部,256KB 够绝大部分包 */
|
|
501
|
+
const MEMORY_PROBE_HEAD_BYTES = 256 * 1024;
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* 运行器自身的状态(供 /api/unpack/stats 与排障用)。
|
|
505
|
+
*
|
|
506
|
+
* 返回 `plugins[]` 列出每个 provider 插件的装载状态;
|
|
507
|
+
* 顶层仍保留 pluginId/loaded/loadMs/error/pluginDir —— 单插件时与历史值逐字一致,
|
|
508
|
+
* `/api/unpack/stats` 的消费方不用改。注意:这里**只读缓存状态**,不会触发装载。
|
|
509
|
+
*/
|
|
510
|
+
export function runnerInfo() {
|
|
511
|
+
const plugins = listProviderPlugins().map((meta) => {
|
|
512
|
+
const state = pluginCache.get(meta.id);
|
|
513
|
+
return {
|
|
514
|
+
pluginId: meta.id,
|
|
515
|
+
loaded: !!(state && state.ok),
|
|
516
|
+
loadMs: (state && state.loadMs) || 0,
|
|
517
|
+
error: state && state.ok === false ? state.error : null,
|
|
518
|
+
pluginDir: path.relative(ROOT, path.join(PLUGINS_DIR, meta.id)),
|
|
519
|
+
// 运行时信息(诊断用):让"这个插件走 vm 沙盒还是 exec 通道"一看就知道
|
|
520
|
+
runtime: meta.runtime || 'node',
|
|
521
|
+
entry: meta.entry || null,
|
|
522
|
+
frameProtocol: meta.frameProtocol === undefined ? null : meta.frameProtocol,
|
|
523
|
+
};
|
|
524
|
+
});
|
|
525
|
+
const first = plugins[0] || { pluginId: null, loaded: false, loadMs: 0, error: null, pluginDir: null };
|
|
526
|
+
// exec 通道是否放行(默认 false):排障时"插件明明写了 exec 却没生效"多半卡在这
|
|
527
|
+
const execGate = readExecPermission(ROOT);
|
|
528
|
+
return { ...first, plugins, runtimes: listRuntimes(), execAllowed: execGate.allowed, execGateSource: execGate.source };
|
|
529
|
+
}
|