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,70 @@
|
|
|
1
|
+
// app/renderer-setting.js —— 面板/交互层设置下发的唯一通道(契约 INV-01)
|
|
2
|
+
//
|
|
3
|
+
// 为什么要有这个模块:设置只能经 `renderer.setSetting(key, value)` 落地,不允许任何调用点
|
|
4
|
+
// 直接摸舞台的 set 方法。之前 9 处直连的问题不是"写错",而是**绕过渲染器**:
|
|
5
|
+
// Live2D 模式下那些调用会打到隐藏的 spine 舞台上(改了没人看的对象),表现为"点了没反应"。
|
|
6
|
+
//
|
|
7
|
+
// 这个模块集中三件事:
|
|
8
|
+
// ① 只认 state.currentRenderer(当前渲染面),不认 stage
|
|
9
|
+
// ② 三态:渲染器没声明该能力键 ⇒ 记一条 warn 并返回 false(控件由面板置灰,这里兜底)
|
|
10
|
+
// ③ 契约 §六:setSetting 返回 false 时记 warn,并回读 getSetting 的**生效值**供调用方回退
|
|
11
|
+
//
|
|
12
|
+
// 注意:同一条键只警告一次(Set 去重)。否则"恢复上次偏好"这类路径会在每次装载时刷屏。
|
|
13
|
+
import { state } from './context.js';
|
|
14
|
+
import { stageLog, stageStatus } from './status.js';
|
|
15
|
+
import { repaintActive } from '../render/playback.js';
|
|
16
|
+
|
|
17
|
+
/** 已警告过的键(避免同一条能力缺失反复刷日志) */
|
|
18
|
+
const warned = new Set();
|
|
19
|
+
|
|
20
|
+
/** 记一次去重后的警告 */
|
|
21
|
+
function warnOnce(key, message) {
|
|
22
|
+
if (warned.has(key)) return;
|
|
23
|
+
warned.add(key);
|
|
24
|
+
stageLog('warn', message, { key: String(key || ''), source: 'renderer-setting' });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 渲染器是否声明了某能力键(未声明 ⇒ 面板置灰,这里不硬发) */
|
|
28
|
+
export function rendererSupports(key) {
|
|
29
|
+
const r = state.currentRenderer;
|
|
30
|
+
const list = (r && r.capabilities && r.capabilities.settings) || [];
|
|
31
|
+
return list.includes(key);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 经渲染器下发一项设置。
|
|
36
|
+
* @param {string} key 能力键(skin / mixMode / mixTime / animation / fps / speed / pma …)
|
|
37
|
+
* @param {any} value 目标值
|
|
38
|
+
* @returns {{ok: boolean, applied: any}} ok=false 时 applied 是回读到的生效值(可能为 null)
|
|
39
|
+
*/
|
|
40
|
+
export function setRendererSetting(key, value) {
|
|
41
|
+
const renderer = state.currentRenderer;
|
|
42
|
+
if (!renderer || typeof renderer.setSetting !== 'function') {
|
|
43
|
+
warnOnce(key, `当前渲染器不可用,设置 ${key} 未下发`);
|
|
44
|
+
return { ok: false, applied: null };
|
|
45
|
+
}
|
|
46
|
+
if (!rendererSupports(key)) {
|
|
47
|
+
// 这里**故意不记日志**:能力不支持的控件在 applyCapabilities 阶段就根本不构建,
|
|
48
|
+
// "该项不可用(面板应置灰)"指向一个用户看不到的东西;同类告警在面板侧也已删除。
|
|
49
|
+
// 能力是否支持属于自检结论(selfCheck 的 checked/violations),不该每次运行时刷一条。
|
|
50
|
+
return { ok: false, applied: null };
|
|
51
|
+
}
|
|
52
|
+
let ok = false;
|
|
53
|
+
try { ok = renderer.setSetting(key, value) === true; } catch { ok = false; }
|
|
54
|
+
// 暂停中改设置:引擎/渲染循环都停着,必须显式补一帧,否则"改了看不见"
|
|
55
|
+
if (state.playing === false) { try { repaintActive(state.surface); } catch { /* 忽略 */ } }
|
|
56
|
+
let applied = null;
|
|
57
|
+
try { applied = renderer.getSetting ? renderer.getSetting(key) : null; } catch { applied = null; }
|
|
58
|
+
if (!ok) {
|
|
59
|
+
// 契约 §六:返回 false 必须留痕,并让调用方拿到生效值做回退
|
|
60
|
+
warnOnce(key, `设置 ${key}=${String(value)} 未被渲染器接受,已回退到生效值 ${String(applied)}`);
|
|
61
|
+
}
|
|
62
|
+
return { ok, applied };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 回读某键的生效值(不改变任何状态) */
|
|
66
|
+
export function getRendererSetting(key) {
|
|
67
|
+
const renderer = state.currentRenderer;
|
|
68
|
+
if (!renderer || typeof renderer.getSetting !== 'function') return null;
|
|
69
|
+
try { return renderer.getSetting(key); } catch { return null; }
|
|
70
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// app/root-manager.js —— 资源根管理(左栏):新增 / 改名 / 启停 / 删除
|
|
2
|
+
//
|
|
3
|
+
// 为什么是"输入服务端绝对路径"而不是目录选择器:
|
|
4
|
+
// 模型是**服务端**扫描自己文件系统加载的,浏览器 File System Access API 拿到的是客户端
|
|
5
|
+
// FileSystemDirectoryHandle(浏览器沙箱内的句柄),根本无法变成服务端可读的路径 —— 传过去
|
|
6
|
+
// 毫无意义。项目也没有"服务端目录浏览"接口(那属于 M5 插件接口范畴),所以这里用文本框收
|
|
7
|
+
// 绝对路径 + 用已有根做 datalist 提示 + 把后端校验错误原样显示给用户(400/409/404 都有文案)。
|
|
8
|
+
//
|
|
9
|
+
// 每个动作结束后都用后端返回的 rootsAll 重建列表(含停用项),再回调 onChanged 让名册树刷新。
|
|
10
|
+
|
|
11
|
+
/** 后端错误文案优先,其次状态码,最后兜底 —— 一律原样呈现,不吞不猜 */
|
|
12
|
+
function describeError(status, payload, formatError) {
|
|
13
|
+
const text = payload && typeof payload.error === 'string' && payload.error
|
|
14
|
+
? payload.error
|
|
15
|
+
: (typeof formatError === 'function' ? formatError({ status }) : `HTTP ${status}`);
|
|
16
|
+
return `${text}(HTTP ${status})`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 创建资源根管理器。
|
|
21
|
+
* @param {{els: object, onChanged?: Function, formatError?: Function}} deps
|
|
22
|
+
* els 需要:list(根条目容器)、form/pathInput/nameInput(新增表单)、msg(提示区)、
|
|
23
|
+
* count(数量文案)、toggleBtn/body(管理区展开/收起)
|
|
24
|
+
*/
|
|
25
|
+
export function createRootManager({ els, onChanged, formatError } = {}) {
|
|
26
|
+
const state = {
|
|
27
|
+
/** 最近一次拿到的完整根列表(含停用项):GET 只给启用的,所以以 POST 返回的 rootsAll 为准 */
|
|
28
|
+
roots: [],
|
|
29
|
+
/** 正在内联编辑改名的根 id */
|
|
30
|
+
editingId: null,
|
|
31
|
+
/** 正在二次确认删除的根 id */
|
|
32
|
+
confirmingId: null,
|
|
33
|
+
/** 最近一次动作结果(断言用) */
|
|
34
|
+
last: null,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** 统一的提示:文案 + 级别(ok/err),同时写进 dataset 供断言读 */
|
|
38
|
+
function notify(text, level = 'info') {
|
|
39
|
+
if (els.msg) {
|
|
40
|
+
els.msg.textContent = text || '';
|
|
41
|
+
els.msg.dataset.level = level;
|
|
42
|
+
}
|
|
43
|
+
document.documentElement.dataset.rootMsg = `${level}|${text || ''}`;
|
|
44
|
+
return text;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 发一次写请求:返回 {ok, status, data};HTTP 非 2xx 不抛,交给调用方决定怎么提示 */
|
|
48
|
+
async function post(body) {
|
|
49
|
+
const res = await fetch('/api/roots', {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { 'content-type': 'application/json' },
|
|
52
|
+
body: JSON.stringify(body),
|
|
53
|
+
// 写操作:**不加去重、不自动重试**(重复写会建两个根),只补超时信号防"永久转圈"。
|
|
54
|
+
// 8s 依据:服务端只改一份 resources.json + 落盘备份(backupConfigFile),正常 <1s,
|
|
55
|
+
// 留 8 倍余量覆盖 CIFS 落盘抖动。不用 api.js:它的默认重试会重发写请求。
|
|
56
|
+
signal: AbortSignal.timeout(8000),
|
|
57
|
+
});
|
|
58
|
+
let data = null;
|
|
59
|
+
try { data = await res.json(); } catch { data = null; }
|
|
60
|
+
state.last = { action: body && body.action, status: res.status, data };
|
|
61
|
+
return { ok: res.ok, status: res.status, data };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 用一份完整根列表重建 UI(后端给的 rootsAll 优先;没有就退回 GET 的 roots) */
|
|
65
|
+
function render(roots) {
|
|
66
|
+
if (Array.isArray(roots)) state.roots = roots;
|
|
67
|
+
const list = state.roots;
|
|
68
|
+
if (els.count) els.count.textContent = String(list.length);
|
|
69
|
+
if (!els.list) return list;
|
|
70
|
+
els.list.replaceChildren();
|
|
71
|
+
if (!list.length) {
|
|
72
|
+
const empty = document.createElement('p');
|
|
73
|
+
empty.className = 'muted-hint';
|
|
74
|
+
empty.textContent = '还没有资源根';
|
|
75
|
+
els.list.append(empty);
|
|
76
|
+
return list;
|
|
77
|
+
}
|
|
78
|
+
for (const root of list) els.list.append(renderRow(root));
|
|
79
|
+
return list;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 单条根:名字 / id / 路径 / 启停状态 + 三个操作(改名为内联编辑,删除二次确认) */
|
|
83
|
+
function renderRow(root) {
|
|
84
|
+
const row = document.createElement('div');
|
|
85
|
+
row.className = 'root-row';
|
|
86
|
+
row.dataset.rootId = root.id;
|
|
87
|
+
row.dataset.enabled = String(root.enabled !== false);
|
|
88
|
+
|
|
89
|
+
const title = document.createElement('span');
|
|
90
|
+
title.dataset.role = 'name';
|
|
91
|
+
title.className = 'root-name';
|
|
92
|
+
title.textContent = `${root.name || root.id}${root.enabled === false ? '(已停用)' : ''}`;
|
|
93
|
+
row.append(title);
|
|
94
|
+
|
|
95
|
+
const pathEl = document.createElement('span');
|
|
96
|
+
pathEl.dataset.role = 'path';
|
|
97
|
+
pathEl.className = 'muted-hint root-path';
|
|
98
|
+
pathEl.textContent = root.path || '';
|
|
99
|
+
row.append(pathEl);
|
|
100
|
+
|
|
101
|
+
if (state.editingId === root.id) {
|
|
102
|
+
const input = document.createElement('input');
|
|
103
|
+
input.type = 'text';
|
|
104
|
+
input.dataset.role = 'rename-input';
|
|
105
|
+
input.className = 'root-rename-input';
|
|
106
|
+
input.value = root.name || root.id;
|
|
107
|
+
row.append(input);
|
|
108
|
+
row.append(miniBtn('保存', 'rename-save', () => submitRename(root.id, input.value)));
|
|
109
|
+
row.append(miniBtn('取消', 'rename-cancel', () => { state.editingId = null; render(); }));
|
|
110
|
+
return row;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
row.append(miniBtn('改名', 'rename', () => { state.editingId = root.id; render(); }));
|
|
114
|
+
row.append(miniBtn(root.enabled === false ? '启用' : '停用', 'toggle',
|
|
115
|
+
() => submitToggle(root.id, root.enabled === false)));
|
|
116
|
+
|
|
117
|
+
if (state.confirmingId === root.id) {
|
|
118
|
+
const ask = document.createElement('span');
|
|
119
|
+
ask.dataset.role = 'confirm';
|
|
120
|
+
ask.className = 'root-confirm-ask';
|
|
121
|
+
ask.textContent = '确认删除?';
|
|
122
|
+
row.append(ask);
|
|
123
|
+
row.append(miniBtn('确认删除', 'remove-confirm', () => submitRemove(root.id)));
|
|
124
|
+
row.append(miniBtn('取消', 'remove-cancel', () => { state.confirmingId = null; render(); }));
|
|
125
|
+
} else {
|
|
126
|
+
row.append(miniBtn('删除', 'remove', () => { state.confirmingId = root.id; render(); }));
|
|
127
|
+
}
|
|
128
|
+
return row;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 小按钮工厂:统一 class 与 dataset,便于断言按 data-role 定位 */
|
|
132
|
+
function miniBtn(text, role, onClick) {
|
|
133
|
+
const btn = document.createElement('button');
|
|
134
|
+
btn.type = 'button';
|
|
135
|
+
btn.className = 'mini-btn';
|
|
136
|
+
btn.textContent = text;
|
|
137
|
+
btn.dataset.role = role;
|
|
138
|
+
btn.addEventListener('click', onClick);
|
|
139
|
+
return btn;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 用后端最新列表刷新(后端成功响应都带 rootsAll: 含停用项) */
|
|
143
|
+
function applyResult(data) {
|
|
144
|
+
const next = data && (data.rootsAll || data.roots);
|
|
145
|
+
if (Array.isArray(next)) render(next);
|
|
146
|
+
if (typeof onChanged === 'function') {
|
|
147
|
+
try { onChanged(data); } catch { /* 名册刷新失败不影响管理列表 */ }
|
|
148
|
+
}
|
|
149
|
+
return Array.isArray(next) ? next : state.roots;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 最近一次"添加资源根"的失败原因(空 = 上次成功或还没试过) */
|
|
153
|
+
let lastAddError = '';
|
|
154
|
+
|
|
155
|
+
// ------------------------------------------------------------------ 四个动作
|
|
156
|
+
|
|
157
|
+
/** 新增:path 必填(服务端绝对路径);name 可空(空则用目录名) */
|
|
158
|
+
async function submitAdd(pathValue, nameValue) {
|
|
159
|
+
const path = String(pathValue || '').trim();
|
|
160
|
+
const name = String(nameValue || '').trim();
|
|
161
|
+
// 失败原因要能被目录选择对话框取到:它弹在屏幕中央,左栏那行提示在用户背后(实测用户看不到)。
|
|
162
|
+
// 返回值仍是布尔(既有调用方与测试都按布尔断言),文案另存一份。
|
|
163
|
+
const fail = (msg) => { lastAddError = msg; notify(msg, 'err'); return false; };
|
|
164
|
+
lastAddError = '';
|
|
165
|
+
if (!path) return fail('请填写要添加的目录(服务端绝对路径)');
|
|
166
|
+
const body = { action: 'add', path };
|
|
167
|
+
if (name) body.name = name;
|
|
168
|
+
const res = await post(body);
|
|
169
|
+
if (!res.ok) return fail(describeError(res.status, res.data, formatError));
|
|
170
|
+
const merged = !!(res.data && res.data.changed && res.data.changed.merged);
|
|
171
|
+
const root = (res.data && res.data.changed && res.data.changed.root) || {};
|
|
172
|
+
applyResult(res.data);
|
|
173
|
+
notify(merged
|
|
174
|
+
? `已合并到同名根:${root.name || name || root.id}`
|
|
175
|
+
: `已添加资源根:${root.name || name || root.id}(${root.path || path})`, 'ok');
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** 改名:命中已有名字时后端返回 merged:true —— 必须如实提示"已合并",不能显示成改名成功 */
|
|
180
|
+
async function submitRename(id, nameValue, apiOnly = false) {
|
|
181
|
+
const name = String(nameValue || '').trim();
|
|
182
|
+
if (!apiOnly && !name) { notify('新名字不能为空', 'err'); return false; }
|
|
183
|
+
const res = await post({ action: 'rename', id, name });
|
|
184
|
+
if (!res.ok) { notify(describeError(res.status, res.data, formatError), 'err'); return false; }
|
|
185
|
+
const changed = (res.data && res.data.changed) || {};
|
|
186
|
+
state.editingId = null;
|
|
187
|
+
applyResult(res.data);
|
|
188
|
+
if (changed.merged) {
|
|
189
|
+
notify(`已合并到同名根「${changed.to}」(条目由 ${changedFrom(res)} 变为 ${(res.data.roots || []).length} 个)`, 'warn');
|
|
190
|
+
} else {
|
|
191
|
+
notify(`已改名为「${changed.to || name}」`, 'ok');
|
|
192
|
+
}
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** toggle 必须传真布尔,否则后端 400(字符串 "false" 会被拒) */
|
|
197
|
+
async function submitToggle(id, enabled) {
|
|
198
|
+
const res = await post({ action: 'toggle', id, enabled: enabled === true });
|
|
199
|
+
if (!res.ok) { notify(describeError(res.status, res.data, formatError), 'err'); return false; }
|
|
200
|
+
applyResult(res.data);
|
|
201
|
+
notify(`已${enabled === true ? '启用' : '停用'}:${id}`, 'ok');
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** 删除:界面上先二次确认,再发请求 */
|
|
206
|
+
async function submitRemove(id) {
|
|
207
|
+
const res = await post({ action: 'remove', id });
|
|
208
|
+
if (!res.ok) { notify(describeError(res.status, res.data, formatError), 'err'); return false; }
|
|
209
|
+
state.confirmingId = null;
|
|
210
|
+
applyResult(res.data);
|
|
211
|
+
notify(`已删除资源根:${id}`, 'ok');
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** 合并提示里需要"合并前后条目数":后端不给 before,这里用 roots 数量差反推 */
|
|
216
|
+
function changedFrom(res) {
|
|
217
|
+
const after = (res.data && res.data.roots) || [];
|
|
218
|
+
return String(after.length + 1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** 目录浏览器的默认起点(上次添加的目录的父目录);空则从 / 开始 */
|
|
222
|
+
let pickerStart = '';
|
|
223
|
+
|
|
224
|
+
// 「选择目录」:打开服务端目录浏览器,选完目录直接添加,面板里不填路径
|
|
225
|
+
if (els.triggerBtn) {
|
|
226
|
+
els.triggerBtn.addEventListener('click', () => {
|
|
227
|
+
if (!els.picker) { notify('目录浏览器未就绪', 'err'); return; }
|
|
228
|
+
els.picker.open(pickerStart);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 管理区展开/收起
|
|
233
|
+
if (els.toggleBtn && els.body) {
|
|
234
|
+
els.toggleBtn.addEventListener('click', () => {
|
|
235
|
+
const open = els.body.hidden;
|
|
236
|
+
els.body.hidden = !open;
|
|
237
|
+
els.toggleBtn.setAttribute('aria-expanded', String(open));
|
|
238
|
+
els.toggleBtn.textContent = open ? '收起' : '管理';
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
render, post, notify,
|
|
244
|
+
/** 目录浏览器实例(测试/排障用:open/goto/state) */
|
|
245
|
+
get picker() { return els.picker || null; },
|
|
246
|
+
/** 设置目录浏览器的默认起点(由装配方按现有根推算) */
|
|
247
|
+
setPickerStart(path) { pickerStart = String(path || ''); },
|
|
248
|
+
submitAdd, submitRename, submitToggle, submitRemove,
|
|
249
|
+
/** 最近一次添加失败的原因(对话框用它把错误显示在自己身上) */
|
|
250
|
+
getLastAddError: () => lastAddError,
|
|
251
|
+
get roots() { return state.roots; },
|
|
252
|
+
get last() { return state.last; },
|
|
253
|
+
};
|
|
254
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// app/status.js —— 舞台状态行的唯一写入口
|
|
2
|
+
//
|
|
3
|
+
// 一条状态行有三个去处,全部由这里统一分发(顺序不能颠倒):
|
|
4
|
+
// ① 进 window.__WEB__.stageLog(自动化断言的唯一真相,前端一切"发生了什么"都靠它)
|
|
5
|
+
// ② 进调试日志浮层的缓冲(若已建)
|
|
6
|
+
// ③ 按"显示状态信息"开关决定要不要写到舞台顶部那一行
|
|
7
|
+
//
|
|
8
|
+
// 为什么不让调用方自己写 DOM:状态行是"最近一次发生了什么",多处各写一处必然漂移;
|
|
9
|
+
// 而且 error/warn 必须始终可见,info 只在被要求时显示——这条策略只能有一份实现。
|
|
10
|
+
|
|
11
|
+
import { dom, state } from './context.js';
|
|
12
|
+
import { createLogger } from '../infra/log.js';
|
|
13
|
+
|
|
14
|
+
// 适配层:状态行的文本同时进统一日志工厂(面板与"上传日志"只认工厂这一份)。
|
|
15
|
+
// 注意:状态行的**展示策略**仍在本文件(什么时候写 stageHint),工厂只管"记一条"。
|
|
16
|
+
const logStage = createLogger('stage');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 记录一条状态行(**无条件执行**,与"显不显示"完全无关)。
|
|
20
|
+
*
|
|
21
|
+
* 记录有三个去处,缺一不可:
|
|
22
|
+
* ① `window.__WEB__.stageLog` —— 自动化断言的唯一真相
|
|
23
|
+
* ② 调试日志浮层的缓冲(面板看得到)
|
|
24
|
+
* ③ 统一日志工厂("上传日志"导出)—— 只有 `toLog` 为真的调用才进
|
|
25
|
+
*
|
|
26
|
+
* 为什么要单独抽出来:以前这条记录写在显示分支之后,两处 early return(没有 stageHint、
|
|
27
|
+
* 或「显示状态信息」被关掉)会**连日志一起跳过**。后果是:用户为了少看 UI 噪音关掉状态行,
|
|
28
|
+
* 真出问题想上传日志时恰好是空的——最需要日志的时刻没有日志。
|
|
29
|
+
* 现在记录在前、显示在后,开关只影响"看不看得见",不影响"有没有记"。
|
|
30
|
+
*
|
|
31
|
+
* @param {'info'|'warn'|'error'} level
|
|
32
|
+
* @param {string} line 已转成字符串的文本
|
|
33
|
+
* @param {object} [detail] 结构化上下文(仅 toLog 时进工厂)
|
|
34
|
+
* @param {{toLog?: boolean}} [flags] toLog=true ⇒ 同时写入统一日志工厂
|
|
35
|
+
*/
|
|
36
|
+
function record(level, line, detail, flags) {
|
|
37
|
+
if (typeof window !== 'undefined' && window.__WEB__) {
|
|
38
|
+
const bag = (window.__WEB__.stageLog = window.__WEB__.stageLog || []);
|
|
39
|
+
bag.push({ at: Date.now(), level, line });
|
|
40
|
+
if (bag.length > 200) bag.shift();
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
console.info(`%c[STAGE:${level}]`, 'color:#8b8b8b', line);
|
|
44
|
+
} catch { /* 控制台不可用时忽略 */ }
|
|
45
|
+
if (state.debugLogHandle && typeof state.debugLogHandle.log === 'function') {
|
|
46
|
+
try { state.debugLogHandle.log('STAGE', line, { level }); } catch { /* 日志失败不影响业务 */ }
|
|
47
|
+
}
|
|
48
|
+
if (!flags || flags.toLog !== true) return;
|
|
49
|
+
try {
|
|
50
|
+
const lv = level === 'warn' || level === 'error' ? level : 'info';
|
|
51
|
+
logStage[lv](line, detail);
|
|
52
|
+
} catch { /* 工厂异常不影响状态行 */ }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 按「显示状态信息」开关与级别决定要不要写舞台顶部那一行。
|
|
57
|
+
* 只碰 DOM,不碰任何日志通路。
|
|
58
|
+
* @param {string} line
|
|
59
|
+
* @param {'info'|'warn'|'error'} level
|
|
60
|
+
* @param {{show?: boolean}} opts
|
|
61
|
+
*/
|
|
62
|
+
function renderStatusLine(line, level, opts) {
|
|
63
|
+
const visible = state.showStatus !== false;
|
|
64
|
+
if (!dom.stageHint) return;
|
|
65
|
+
if (!visible) {
|
|
66
|
+
dom.stageHint.textContent = '';
|
|
67
|
+
dom.stageHint.hidden = true;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const show = opts.show === true || level === 'error' || level === 'warn';
|
|
71
|
+
if (show) {
|
|
72
|
+
dom.stageHint.textContent = line;
|
|
73
|
+
dom.stageHint.hidden = false;
|
|
74
|
+
} else {
|
|
75
|
+
dom.stageHint.textContent = '';
|
|
76
|
+
dom.stageHint.hidden = true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 写一条状态行(**只上状态行**,不额外记日志)。
|
|
82
|
+
*
|
|
83
|
+
* ---- 什么时候用 stageStatus、什么时候用 stageLog(总则,新增状态行前先读这段)----
|
|
84
|
+
* 判据:这句话说的是"**现在是什么**"还是"**刚刚发生了什么**"?
|
|
85
|
+
* · "现在是什么"(设置回显 / 操作回执 / 进度 / 装载结果) → **stageStatus**
|
|
86
|
+
* 只占状态行,不额外记日志 —— 状态行本来就是"当前状态",再记一条只会把日志刷屏。
|
|
87
|
+
* · "刚刚发生了什么"(失败 / 被拒绝 / 跳过了什么) → **stageLog**
|
|
88
|
+
* 带结构化 detail 进面板与"上传日志",排查时要靠它回溯。
|
|
89
|
+
* 例外:错误与告警类即使描述的是某种状态,也一律走 stageLog —— 人需要能回溯,不能只闪一下。
|
|
90
|
+
* 注意:日志不能走 console.error,否则页面错误统计会把正常状态当成异常。
|
|
91
|
+
* @param {string} text
|
|
92
|
+
* @param {'info'|'warn'|'error'} [level]
|
|
93
|
+
* @param {{show?: boolean}} [opts] show=true 表示这条 info 也要显示在状态行上
|
|
94
|
+
* @returns {string} 写进状态行的文本
|
|
95
|
+
*/
|
|
96
|
+
export function stageStatus(text, level = 'info', opts = {}) {
|
|
97
|
+
const line = String(text == null ? '' : text);
|
|
98
|
+
record(level, line, undefined, { toLog: false });
|
|
99
|
+
renderStatusLine(line, level, opts || {});
|
|
100
|
+
return line;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 写状态行 + 记一条**带结构化 detail** 的日志((a) 类:既要人看见,也要进面板与上传)。
|
|
105
|
+
*
|
|
106
|
+
* 为什么单开一个入口:失败类与"预期内不可用"要进日志,而且必须带可排查上下文
|
|
107
|
+
* (错误对象、输入、当前模型…)——纯文本的 stageStatus 记不出结构化字段。
|
|
108
|
+
* **状态行的可见性策略不变**(error/warn 始终可见),日志记录也不受显示开关影响。
|
|
109
|
+
* @param {'info'|'warn'|'error'} level
|
|
110
|
+
* @param {string} text 人话(同时写进状态行与日志的 msg)
|
|
111
|
+
* @param {object} [detail] 结构化上下文(对象原样保留,不会被字符串化成 [object Object])
|
|
112
|
+
* @param {{show?: boolean}} [opts] 同 stageStatus
|
|
113
|
+
* @returns {string} 写进状态行的文本
|
|
114
|
+
*/
|
|
115
|
+
export function stageLog(level, text, detail, opts) {
|
|
116
|
+
const lv = level === 'warn' || level === 'error' ? level : 'info';
|
|
117
|
+
const line = String(text == null ? '' : text);
|
|
118
|
+
record(lv, line, detail, { toLog: true });
|
|
119
|
+
renderStatusLine(line, lv, opts || {});
|
|
120
|
+
return line;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 向诊断探针上报一条"应用语义"事件(plugin.load / bundle.parse / model.mount 等)。
|
|
125
|
+
* 探针不在(未装/被禁用)时静默跳过——日志是旁路,不能影响主流程。
|
|
126
|
+
* @param {string} name 事件名(点号分隔,如 model.mount)
|
|
127
|
+
* @param {object} [data] 结构化字段
|
|
128
|
+
* @param {{level?: string, src?: string}} [opts]
|
|
129
|
+
*/
|
|
130
|
+
export function diagEvent(name, data, opts) {
|
|
131
|
+
try {
|
|
132
|
+
const diag = typeof window !== 'undefined' ? window.__DIAG__ : null;
|
|
133
|
+
if (!diag || typeof diag.event !== 'function') return null;
|
|
134
|
+
return diag.event(name, data || {}, opts || {});
|
|
135
|
+
} catch {
|
|
136
|
+
return null; // 上报失败不影响业务
|
|
137
|
+
}
|
|
138
|
+
}
|