dsh-mpkg-wallpaper 3.13.6 → 3.13.8

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/README.en.md CHANGED
@@ -325,7 +325,7 @@ These keys **exist and participate in the logic** but have no widget on the sett
325
325
  | `aquaTintStrength` | 45 | How much of the wallpaper colour is mixed into panel tints; read at runtime |
326
326
  | `glassColor` / `glassAlpha` | empty / 12 | Early WebGL liquid-glass leftovers: backup/restore only, **no widget and no reader** |
327
327
  | `webInteraction` | `pointer` | Web-wallpaper interaction mode (`off`/`pointer`/`full`); use `?mpwinteract=…` or write the stored settings |
328
- | `sceneRendererUrl` | `http://127.0.0.1:8899/` | Scene-renderer address, overridable |
328
+ | `sceneRendererUrl` | `http://127.0.0.1:8902/webloader/` | Scene-renderer address, overridable (default = the one-stop :8902 bench serving `/webloader/` itself; the old default was `:8899`, changed 2026-09-25) |
329
329
  | `npVolume` | 100 | The **volume level** in the Now playing card (0..100, applied to the real element); the default profile **never writes element volume**, only your own change does |
330
330
  | `glassWindow` | — | **Retired and deleted** (2026-09-19): no widget, no reader; covered by `settingsBlur` + `dialogBlur`/`popoverBlur` |
331
331
 
@@ -406,7 +406,7 @@ Start here when something is wrong: one click sends state back to the local host
406
406
  Two different numbers — do not mix them up:
407
407
 
408
408
  - The **main table holds 158 flags**, in the **renderer repository**, [`we-scene-demo/docs/README-DIAGNOSTICS.md`](../we-scene-demo/docs/README-DIAGNOSTICS.md) (generated by `diag-flag-check.mjs`, cross-checked against `web/diag-flags.json`).
409
- - The plugin panel only ships the **common / escape-hatch table** (the 10 flags with `common:true`), appended to the renderer URL, e.g. `http://127.0.0.1:8899/?id=3719111841&audit=3`:
409
+ - The plugin panel only ships the **common / escape-hatch table** (the 10 flags with `common:true`), appended to the renderer URL, e.g. `http://127.0.0.1:8902/webloader/?id=3719111841&audit=3`:
410
410
 
411
411
  | Flag | Purpose | Default |
412
412
  |---|---|---|
@@ -464,7 +464,7 @@ A "WE install" means the Steam edition of Wallpaper Engine (appid **431960**). T
464
464
 
465
465
  The plugin offers two paths:
466
466
 
467
- 1. **External renderer iframe (preferred)**: when the local we-scene renderer on `:8899` is online, the scene is mounted as an iframe (`lib/client.js:3133-3190`). If the renderer is offline or the first frame times out, a **watchdog** falls back to the static frame and says so in a hint bar; a manual "remount renderer" button is provided. The renderer address can be overridden with `sceneRendererUrl`.
467
+ 1. **External renderer iframe (preferred)**: when the local renderer (default `http://127.0.0.1:8902/webloader/`, previously `:8899`) is online, the scene is mounted as an iframe (`lib/client.js:3133-3190`). If the renderer is offline or the first frame times out, a **watchdog** falls back to the static frame and says so in a hint bar; a manual "remount renderer" button is provided. The renderer address can be overridden with `sceneRendererUrl`.
468
468
  2. **In-container asset extraction (fallback, `lib/pkg-extract.js`, MIT, adopted from [elysia395/dsh-wallpaper-engine](https://github.com/elysia395/dsh-wallpaper-engine))**:
469
469
  - **Static frame**: parses the PKG (LZ4) and decodes TEX textures, picking the main texture by "area × format weight × path penalty" (`lib/pkg-extract.js:1422-1491`)
470
470
  - **Layer composite**: parses every image layer of `scene.json` and composites them on a canvas using source coordinates/sizes (at most 24 layers, `lib/pkg-extract.js:1734-1829`)
package/README.md CHANGED
@@ -326,7 +326,7 @@ node tools/build-bundle.mjs
326
326
  | `aquaTintStrength` | 45 | 面板取色的壁纸主色混合比例;运行时读取(`lib/client.js:5575`),无控件 |
327
327
  | `glassColor` / `glassAlpha` | 空 / 12 | 早期 WebGL 液态玻璃的遗留键:只随备份导出/导入与「恢复默认」走动,**无控件、无读取点**(`lib/client.js:11968`、`:11986`) |
328
328
  | `webInteraction` | `pointer` | 网页壁纸交互档位(`off`/`pointer`/`full`):**无面板控件**,用 URL `?mpwinteract=…` 或写存档 |
329
- | `sceneRendererUrl` | `http://127.0.0.1:8899/` | 场景渲染器地址,可覆盖(`lib/client.js:3129-3132`) |
329
+ | `sceneRendererUrl` | `http://127.0.0.1:8902/webloader/` | 场景渲染器地址,可覆盖(默认 = 一站式测试台 :8902 自己直供的 `/webloader/`;旧默认是 `:8899`,2026-09-25 改) |
330
330
  | `npVolume` | 100 | Now playing 卡片里的**音量电平**(0..100,落到真实元素);默认档**从不写元素音量**,只有你动过才写 |
331
331
  | `glassWindow` | — | **已退役删除**(2026-09-19):无控件、无读取点,功能已被 `settingsBlur` + `dialogBlur`/`popoverBlur` 覆盖;源码与 zh/en 字典 0 残留 |
332
332
 
@@ -407,7 +407,7 @@ node tools/build-bundle.mjs
407
407
  这两个数字口径不同,别混:
408
408
 
409
409
  - **主表共 158 个开关**,在**渲染器仓**的 [`we-scene-demo/docs/README-DIAGNOSTICS.md`](../we-scene-demo/docs/README-DIAGNOSTICS.md)(由 `diag-flag-check.mjs` 生成、与 `web/diag-flags.json` 对拍)。
410
- - 插件面板里只放**常用/逃生口那一张小表**(10 个,`common:true` 集合),拼在渲染器地址后面用,例如 `http://127.0.0.1:8899/?id=3719111841&audit=3`:
410
+ - 插件面板里只放**常用/逃生口那一张小表**(10 个,`common:true` 集合),拼在渲染器地址后面用,例如 `http://127.0.0.1:8902/webloader/?id=3719111841&audit=3`:
411
411
 
412
412
  | 开关 | 用途 | 默认 |
413
413
  |---|---|---|
@@ -465,7 +465,7 @@ node tools/build-bundle.mjs
465
465
 
466
466
  插件提供两条路径:
467
467
 
468
- 1. **外部渲染器 iframe(首选)**:本机 `:8899` 的 we-scene 渲染器在线时,场景以 iframe 挂载(`lib/client.js:3133-3190`)。渲染器离线/首帧超时则由**看门狗**回退静态帧并在提示条说明,可手动「重新挂载渲染器」。渲染器地址可用 `sceneRendererUrl` 覆盖。
468
+ 1. **外部渲染器 iframe(首选)**:本机渲染器(默认 `http://127.0.0.1:8902/webloader/`,旧默认 `:8899`)在线时,场景以 iframe 挂载(`lib/client.js:3133-3190`)。渲染器离线/首帧超时则由**看门狗**回退静态帧并在提示条说明,可手动「重新挂载渲染器」。渲染器地址可用 `sceneRendererUrl` 覆盖。
469
469
  2. **容器内素材提取(兜底,`lib/pkg-extract.js`,MIT,采用自 [elysia395/dsh-wallpaper-engine](https://github.com/elysia395/dsh-wallpaper-engine))**:
470
470
  - **静态帧**:解析 PKG(LZ4 解压)+ TEX 纹理解码,按"面积 × 格式权重 × 路径惩罚"选主纹理(`lib/pkg-extract.js:1422-1491`)
471
471
  - **图层合成**:解析 `scene.json` 的全部 image 图层,按源坐标/尺寸在 canvas 上合成(最多 24 层,`lib/pkg-extract.js:1734-1829`)
package/lib/client.js CHANGED
@@ -2677,7 +2677,12 @@ function mpwThumbFail(el, why) {
2677
2677
  .trim();
2678
2678
  }
2679
2679
 
2680
- /** 解析 project.json 文本,提取标题 + 可调参数信息。 */
2680
+ /** 解析 project.json 文本,提取标题 + **官方 user properties 全字段**。
2681
+ * ①(2026-09-24 用户 bug「WE 自带选项只显示一项且展不开」修复) 旧实现只留
2682
+ * `{key,label,value,type,options,displayOnly,important}` —— `group`(分组头)、
2683
+ * `condition`(条件显示)、`min/max/step/precision`(控件量程)、`order/index`(官方排序)
2684
+ * **全部被丢掉** ⇒ 面板只能渲染成一堆只读文本行、且无法按官方口径分组/联动。
2685
+ * 现在**逐字段保留**(分组/条件/量程/排序都能用),并由 mpwPropsModel 决定渲染形态。 */
2681
2686
  function extractProjectInfo(jsonText) {
2682
2687
  try {
2683
2688
  // ①(P-177 同口径) 包内 project.json 走宽容解析(官方允许尾逗号);健康文件与 JSON.parse 逐位等价
@@ -2685,23 +2690,38 @@ function mpwThumbFail(el, why) {
2685
2690
  const props = json.general && json.general.properties;
2686
2691
  const list = [];
2687
2692
  if (props) {
2693
+ let seq = 0;
2688
2694
  for (const key of Object.keys(props)) {
2689
2695
  if (!safePropKey(key)) continue;
2690
2696
  const p = props[key];
2691
2697
  if (!p || typeof p !== "object") continue;
2698
+ // ①(修正) 原来 `if (!label) continue` 把**没有 text 的条目整条丢掉**
2699
+ // (语料 70 包实测:无 type 的 HTML 营销块 102 条、group 也有空文案的)——
2700
+ // 丢掉的后果不只是"少一行",还会把分组头吞掉 ⇒ 后面所有子项失去归属。
2701
+ // 现在一律保留(渲染与否交给 mpwPropsModel 按官方口径裁定并记账)。
2692
2702
  const label = cleanLabel(p.text);
2693
- if (!label) continue;
2694
2703
  let value = p.value;
2695
2704
  if (Array.isArray(p.options) && p.options.length) {
2696
2705
  const opt = p.options.find((o) => String(o.value) === String(value));
2697
2706
  if (opt) value = opt.label;
2698
2707
  }
2699
2708
  // type "text" 是纯展示性条目(作者信息/说明),不给输入框(⑥)
2700
- const displayOnly = p.type === "text";
2709
+ const displayOnly = p.type === "text" || p.type === "txt" || !p.type;
2701
2710
  // ① 关键开关识别(entry animation 开场动画 / prompt box 提示框)
2702
2711
  const keyText = (label + " " + (p.text || "")).toLowerCase();
2703
2712
  const important = /entry\s*animation|开场动画|入场动画|prompt\s*box|提示框|水印|盗版|防盗|开始时间|随现实时间|timevarying|morningtime|daytime|dusktime|nighttime|时间变化|时间段/.test(keyText);
2704
- list.push({ key, label, value: value === void 0 ? "" : value, type: p.type, options: p.options || null, displayOnly, important });
2713
+ const num = (v) => (typeof v === "number" && isFinite(v) ? v : (typeof v === "string" && v.trim() !== "" && isFinite(Number(v)) ? Number(v) : null));
2714
+ list.push({
2715
+ key, label, value: value === void 0 ? "" : value, type: p.type,
2716
+ options: Array.isArray(p.options) ? p.options : null,
2717
+ displayOnly, important,
2718
+ // ①(新) 官方字段:分组归属、条件显示、控件量程、排序
2719
+ group: typeof p.group === "string" && p.group ? p.group : null,
2720
+ conditions: typeof p.condition === "string" ? p.condition : (typeof p.conditions === "string" ? p.conditions : null),
2721
+ min: num(p.min), max: num(p.max), step: num(p.step),
2722
+ precision: num(p.precision),
2723
+ order: num(p.order), index: num(p.index), seq: seq++,
2724
+ });
2705
2725
  }
2706
2726
  }
2707
2727
  return { title: json.title || "", properties: list };
@@ -2722,6 +2742,253 @@ function mpwThumbFail(el, why) {
2722
2742
  }
2723
2743
  }
2724
2744
 
2745
+ /* ═══ MPW-PROPS-MODEL-BEGIN ═══
2746
+ 官方 user properties(`project.json → general.properties`)的**面板行模型**。
2747
+ 纯函数、无 DOM:同一份代码在 Node 里跑断言(tools/props-panel-wiring-test.mjs)。
2748
+ 口径(行为结论来自渲染器侧同名面板的真包/真机验证;代码为本仓自研,未复制):
2749
+ · 一个 group 类型条目 = **分组头**;子项 = 其后(按官方 order 排序)直到下一个 group 的条目,
2750
+ `group` 字段显式指认时以字段为准。
2751
+ · `condition` 是**表达式**(真包语料:`clock.value`、`tishi.value==0`、`!timevarying.value`、
2752
+ `a.value == true && b.value == true`)⇒ 走白名单递归下降求值,**绝不 eval**;
2753
+ condition 引用的属性自身被门控隐藏时,本项一并隐藏(传递闭包)。
2754
+ · 白名单外/引用缺失 ⇒ 返回 null(**未知**):未知一律**照显示**(宁多显示不误藏)并记账。
2755
+ · 类型 → 控件:bool 开关 / slider 量程滑杆 / combo 下拉 / color 取色 / textinput 文本框;
2756
+ file·directory·scenetexture 需要渲染器侧资源通道,插件侧**只读如实展示**(不猜);
2757
+ `schemecolor`(编辑器配色)与 `usershortcut`(官方语义未实现)**不渲染但逐条记账**。
2758
+ · 排序:`order` → `index` → 文件内顺序。
2759
+ ═══ */
2760
+ const MPW_PROP_CONTROL_TYPES = { bool: "bool", slider: "slider", combo: "combo", color: "color", textinput: "textinput" };
2761
+ const MPW_PROP_READONLY_TYPES = { file: "选择文件(需要渲染器资源通道,插件侧未接线)", directory: "选择文件夹(需要渲染器资源通道,插件侧未接线)", scenetexture: "贴图替换(需要渲染器资源通道,插件侧未接线)" };
2762
+ const MPW_PROP_SKIP_TYPES = { schemecolor: "编辑器配色(官方不在用户属性面板渲染)", usershortcut: "官方语义未实现(不猜)" };
2763
+ /** 官方排序键:order 优先,其次 index,再退文件内出现顺序。 */
2764
+ function mpwPropSorted(allProps) {
2765
+ const list = Array.isArray(allProps) ? allProps.slice() : [];
2766
+ const ord = (p) => (typeof p.order === "number" && isFinite(p.order) ? p.order : (typeof p.index === "number" && isFinite(p.index) ? p.index : 1e9));
2767
+ list.sort((a, b) => (ord(a) - ord(b)) || ((a.seq || 0) - (b.seq || 0)));
2768
+ return list;
2769
+ }
2770
+ /** 条目分类:group / control / readonly / note / skip(不渲染的必须带 why,不静默丢)。 */
2771
+ function mpwPropKind(p) {
2772
+ const type = p && p.type ? String(p.type).toLowerCase() : "";
2773
+ // ① `schemecolor` 是**属性名**(不是类型):它是编辑器配色,官方用户面板不渲染
2774
+ if (p && p.key === "schemecolor") return { kind: "skip", why: MPW_PROP_SKIP_TYPES.schemecolor };
2775
+ if (type === "group") return { kind: "group", why: "" };
2776
+ if (MPW_PROP_CONTROL_TYPES[type]) return { kind: "control", why: "" };
2777
+ if (MPW_PROP_READONLY_TYPES[type]) return { kind: "readonly", why: MPW_PROP_READONLY_TYPES[type] };
2778
+ if (MPW_PROP_SKIP_TYPES[type]) return { kind: "skip", why: MPW_PROP_SKIP_TYPES[type] };
2779
+ if (type === "text" || type === "txt") return { kind: "note", why: "" };
2780
+ // 无 type:官方口径=HTML 营销块,不渲染;有文案的按作者说明行展示(可读性),空文案的记账跳过
2781
+ if (!type) return p.label ? { kind: "note", why: "无 type(官方不在面板渲染;此处按作者说明行展示)" } : { kind: "skip", why: "无 type 且无文案" };
2782
+ return { kind: "skip", why: "未知类型 " + type + "(不猜)" };
2783
+ }
2784
+ /** 条件表达式 → token 流;白名单外返回 null(调用方按"未知"处理)。 */
2785
+ function mpwPropCondTokens(src) {
2786
+ const s = String(src == null ? "" : src);
2787
+ const out = [];
2788
+ let i = 0;
2789
+ while (i < s.length) {
2790
+ const c = s[i];
2791
+ if (/\s/.test(c)) { i++; continue }
2792
+ if (c === "(" || c === ")") { out.push({ t: "punc", v: c }); i++; continue }
2793
+ if (c === "!" && s[i + 1] === "=") { out.push({ t: "op", v: "!=" }); i += 2; continue }
2794
+ if (c === "!") { out.push({ t: "op", v: "!" }); i++; continue }
2795
+ if (c === "&" && s[i + 1] === "&") { out.push({ t: "op", v: "&&" }); i += 2; continue }
2796
+ if (c === "|" && s[i + 1] === "|") { out.push({ t: "op", v: "||" }); i += 2; continue }
2797
+ if (c === "=" && s[i + 1] === "=") { out.push({ t: "op", v: "==" }); i += 2; continue }
2798
+ if (c === '"' || c === "'") {
2799
+ let j = i + 1, buf = "";
2800
+ while (j < s.length && s[j] !== c) { if (s[j] === "\\") j++; buf += s[j] === void 0 ? "" : s[j]; j++ }
2801
+ if (j >= s.length) return null; // 未闭合字符串 = 白名单外
2802
+ out.push({ t: "str", v: buf }); i = j + 1; continue;
2803
+ }
2804
+ if (/[0-9]/.test(c) || (c === "-" && /[0-9.]/.test(s[i + 1] || ""))) {
2805
+ let j = i + 1;
2806
+ while (j < s.length && /[0-9.eE+-]/.test(s[j])) j++;
2807
+ const num = Number(s.slice(i, j));
2808
+ if (!isFinite(num)) return null;
2809
+ out.push({ t: "num", v: num }); i = j; continue;
2810
+ }
2811
+ if (/[A-Za-z_$]/.test(c)) {
2812
+ let j = i;
2813
+ while (j < s.length && /[A-Za-z0-9_$.]/.test(s[j])) j++;
2814
+ const id = s.slice(i, j);
2815
+ if (id === "true" || id === "false") out.push({ t: "bool", v: id === "true" });
2816
+ else if (/^[A-Za-z_$][\w$]*\.value$/.test(id)) out.push({ t: "ref", v: id.slice(0, -6) });
2817
+ else return null; // 只认 `<属性名>.value`
2818
+ i = j; continue;
2819
+ }
2820
+ return null;
2821
+ }
2822
+ return out;
2823
+ }
2824
+ /** 值的真值口径(官方两态 bool / 数值 / 字符串)。 */
2825
+ function mpwPropTruthy(v) {
2826
+ if (typeof v === "boolean") return v;
2827
+ if (typeof v === "number") return v !== 0;
2828
+ if (typeof v === "string") { const s = v.trim().toLowerCase(); return !(s === "" || s === "0" || s === "false" || s === "off" || s === "no"); }
2829
+ return false;
2830
+ }
2831
+ /** `==` 口径:bool 字面量/数值/字符串三态归一到可比对的值。 */
2832
+ function mpwPropEq(a, b) {
2833
+ const numish = (x) => (typeof x === "number" && isFinite(x)) || (typeof x === "string" && x.trim() !== "" && isFinite(Number(x)));
2834
+ if (typeof a === "boolean" || typeof b === "boolean") return mpwPropTruthy(a) === mpwPropTruthy(b);
2835
+ if (numish(a) && numish(b)) return Number(a) === Number(b);
2836
+ return String(a) === String(b);
2837
+ }
2838
+ /** 条件求值(白名单子集):true / false / null(未知 ⇒ 调用方照显示)。 */
2839
+ function mpwPropCondEval(expr, values) {
2840
+ if (expr === null || expr === void 0) return true;
2841
+ const text = String(expr).trim();
2842
+ if (!text) return true;
2843
+ const toks = mpwPropCondTokens(text);
2844
+ if (!toks || !toks.length) return null;
2845
+ let pos = 0;
2846
+ const peek = () => toks[pos];
2847
+ const eat = (t, v) => { const x = toks[pos]; return !!x && x.t === t && (v === void 0 || x.v === v) ? (pos++, x) : null };
2848
+ const val = (n) => {
2849
+ if (!n) return null;
2850
+ if (n.t === "ref") return Object.prototype.hasOwnProperty.call(values, n.v) ? { v: values[n.v] } : null; // 引用缺失 ⇒ 未知
2851
+ return { v: n.t === "bool" ? n.v : (n.t === "num" ? n.v : n.v) };
2852
+ };
2853
+ let bad = false;
2854
+ const prim = () => {
2855
+ if (eat("punc", "(")) { const r = or(); if (!eat("punc", ")")) { bad = true; return null } return r }
2856
+ const n = eat("ref") || eat("bool") || eat("num") || eat("str");
2857
+ if (!n) { bad = true; return null }
2858
+ return val(n);
2859
+ };
2860
+ const cmp = () => {
2861
+ let a = prim();
2862
+ if (bad) return null;
2863
+ const op = eat("op", "==") || eat("op", "!=");
2864
+ if (!op) return a;
2865
+ const b = prim();
2866
+ if (bad) return null;
2867
+ if (a === null || b === null) return null; // 任一侧未知 ⇒ 未知
2868
+ return { v: op.v === "==" ? mpwPropEq(a.v, b.v) : !mpwPropEq(a.v, b.v) };
2869
+ };
2870
+ const unary = () => {
2871
+ if (eat("op", "!")) { const r = unary(); if (bad || r === null) return r; return { v: !mpwPropTruthy(r.v) } }
2872
+ return cmp();
2873
+ };
2874
+ const and = () => {
2875
+ let l = unary();
2876
+ while (eat("op", "&&")) {
2877
+ const r = unary();
2878
+ if (bad) return null;
2879
+ if (l === null || r === null) l = null; else l = { v: mpwPropTruthy(l.v) && mpwPropTruthy(r.v) };
2880
+ }
2881
+ return l;
2882
+ };
2883
+ function or() {
2884
+ let l = and();
2885
+ while (eat("op", "||")) {
2886
+ const r = and();
2887
+ if (bad) return null;
2888
+ if (l === null || r === null) l = null; else l = { v: mpwPropTruthy(l.v) || mpwPropTruthy(r.v) };
2889
+ }
2890
+ return l;
2891
+ }
2892
+ const res = or();
2893
+ if (bad || pos !== toks.length) return null; // 有残余 token ⇒ 白名单外
2894
+ if (res === null) return null;
2895
+ return mpwPropTruthy(res.v);
2896
+ }
2897
+ /** 行模型:把官方属性表变成"(分组头 + 子行)序列 + 记账"。 */
2898
+ function mpwPropsModel(allProps, edits, opts) {
2899
+ const list = mpwPropSorted(allProps);
2900
+ const e = (edits && typeof edits === "object") ? edits : {};
2901
+ const values = {};
2902
+ for (const p of list) values[p.key] = e[p.key] !== void 0 ? e[p.key] : p.value;
2903
+ // ① 分类 + 显式 group 字段指认的集合
2904
+ const kinds = list.map((p) => mpwPropKind(p));
2905
+ const explicitGroups = new Set();
2906
+ for (const p of list) if (p.group) explicitGroups.add(p.group);
2907
+ // ② 归属:显式字段优先;否则"其后(排序后)直到下一个 group 的条目"
2908
+ const rows = [];
2909
+ let curGroup = null;
2910
+ const depthOf = (p, i) => {
2911
+ const k = kinds[i];
2912
+ if (k.kind === "group") { curGroup = p.key; return 0 }
2913
+ if (p.group && explicitGroups.has(p.group)) return 1;
2914
+ return curGroup ? 1 : 0;
2915
+ };
2916
+ list.forEach((p, i) => {
2917
+ const k = kinds[i];
2918
+ const depth = depthOf(p, i);
2919
+ rows.push({
2920
+ key: p.key, label: p.label, type: p.type ? String(p.type).toLowerCase() : "",
2921
+ kind: k.kind, kindWhy: k.why, depth,
2922
+ group: k.kind === "group" ? null : (p.group && explicitGroups.has(p.group) ? p.group : curGroup),
2923
+ value: values[p.key], def: p.value, edited: e[p.key] !== void 0,
2924
+ options: p.options || null, min: p.min, max: p.max, step: p.step, precision: p.precision,
2925
+ // ① 官方的字段名是 `condition`(单数);提取结果里存成 `conditions`(避免与行的
2926
+ // condition 同名字段混淆)⇒ 两个都认,手工构造的属性表也能直接用。
2927
+ condition: (typeof p.conditions === "string" ? p.conditions : (typeof p.condition === "string" ? p.condition : null)), important: !!p.important, displayOnly: !!p.displayOnly,
2928
+ });
2929
+ });
2930
+ // ③ 条件门控(含传递闭包):自身 condition 为假,或其 condition 引用的属性已被隐藏
2931
+ const hidden = {};
2932
+ const refsOf = (r) => {
2933
+ if (!r.condition) return [];
2934
+ const toks = mpwPropCondTokens(r.condition) || [];
2935
+ return toks.filter((t) => t.t === "ref").map((t) => t.v);
2936
+ };
2937
+ for (let round = 0; round < 4; round++) {
2938
+ let changed = false;
2939
+ for (const r of rows) {
2940
+ if (hidden[r.key]) continue;
2941
+ let res;
2942
+ if (r.condition) {
2943
+ const refs = refsOf(r);
2944
+ if (refs.some((k2) => hidden[k2])) res = false; // 父属性被门控 ⇒ 子属性一并隐藏
2945
+ else res = mpwPropCondEval(r.condition, values);
2946
+ } else res = true;
2947
+ r.condResult = res;
2948
+ if (res === false) { hidden[r.key] = true; changed = true; }
2949
+ }
2950
+ if (!changed) break;
2951
+ }
2952
+ // ①(修正) **先算完所有行的 visible,再做"空分组"收口**:
2953
+ // 同一趟里算会让"子项还没轮到 ⇒ visible 仍是 undefined"被当成不可见,分组头全被判空。
2954
+ for (const r of rows) r.visible = !hidden[r.key];
2955
+ for (const r of rows) {
2956
+ // 分组头:没有任何可见子项时也隐藏(不留空壳分组)
2957
+ if (r.kind === "group" && r.visible) {
2958
+ const mine = rows.filter((x) => x.group === r.key && x.visible && x.kind !== "group");
2959
+ r.childCount = mine.length;
2960
+ r.controlCount = mine.filter((x) => x.kind === "control").length;
2961
+ if (!mine.length) r.visible = false;
2962
+ }
2963
+ }
2964
+ const counts = { total: rows.length, groups: 0, controls: 0, readonly: 0, notes: 0, skipped: 0, hidden: 0, condUnknown: 0, visible: 0 };
2965
+ for (const r of rows) {
2966
+ if (r.kind === "group") counts.groups++;
2967
+ else if (r.kind === "control") counts.controls++;
2968
+ else if (r.kind === "readonly") counts.readonly++;
2969
+ else if (r.kind === "note") counts.notes++;
2970
+ else counts.skipped++;
2971
+ if (r.visible) counts.visible++; else if (r.kind !== "skip") counts.hidden++;
2972
+ if (r.condition && r.condResult === null) counts.condUnknown++;
2973
+ }
2974
+ return { rows, counts };
2975
+ }
2976
+ /* ═══ MPW-PROPS-MODEL-END ═══ */
2977
+ /** 官方 color 属性是**线性 0..1 浮点三通道**(`"r g b"`),不是 0..255。取色控件用 #rrggbb 中转。 */
2978
+ function mpwPropColorToHex(v) {
2979
+ const arr = Array.isArray(v) ? v : String(v === void 0 || v === null ? "" : v).trim().split(/[\s,]+/);
2980
+ if (arr.length < 3) return null;
2981
+ const n = arr.slice(0, 3).map(Number);
2982
+ if (!n.every((x) => isFinite(x))) return null;
2983
+ return "#" + n.map((x) => Math.max(0, Math.min(255, Math.round(x * 255))).toString(16).padStart(2, "0")).join("");
2984
+ }
2985
+ function mpwPropHexToColor(hex) {
2986
+ const m = /^#?([0-9a-fA-F]{6})$/.exec(String(hex || "").trim());
2987
+ if (!m) return null;
2988
+ const s = m[1];
2989
+ return [0, 2, 4].map((i) => String(Math.round((parseInt(s.slice(i, i + 2), 16) / 255) * 1e6) / 1e6)).join(" ");
2990
+ }
2991
+
2725
2992
 
2726
2993
  // ═══════════════════════════════════════════════════════════════════
2727
2994
  // 背景 DOM(一个固定 img 层;用 <img> 保证 GIF 动画可靠播放,⑪)
@@ -2772,7 +3039,7 @@ function mpwThumbFail(el, why) {
2772
3039
  // ①(新) 网页壁纸 iframe(web wallpaper):独立沙箱层,覆盖整屏。
2773
3040
  // ①(2026-09-13 注释更正 —— 之前写着"去掉 allow-same-origin"但属性里**并没有去掉**,
2774
3041
  // 属于注释与代码不符,会误导安全判断。现状与事实如下:
2775
- // · iframe 带 `allow-same-origin`,因此它与宿主**同源**(渲染器在 127.0.0.1:8899,
3042
+ // · iframe 带 `allow-same-origin`,因此它与宿主**同源**(渲染器在 127.0.0.1:8902/webloader,
2776
3043
  // cookie 按主机不按端口隔离 → 同 site)→ 它对插件宿主 `/api/mpkg-wallpaper/...`
2777
3044
  // 的 fetch **会自动带上 DSH 的会话 cookie**(这是"自定义场景/缩略图上报"能工作的前提)。
2778
3045
  // · DSH 的会话 cookie 是 `HttpOnly; SameSite=Strict; Path=/`(见 dsh-client-connection
@@ -3160,6 +3427,9 @@ function mpwThumbFail(el, why) {
3160
3427
  try { mpwWebIxDisarm("teardown"); } catch {} // ①(第 11 条) 卸载网页壁纸必须退出交互模式(否则残留输入面)
3161
3428
  try { if (window.__mpwLnReset) window.__mpwLnReset(); } catch {} // ①(批次15 B3) 方向键守卫复位
3162
3429
  if (!frame) return;
3430
+ /* ①(2026-09-25 任务 ㉑ 追加) 撤 src 之前**先明确停源**:撤 src 让文档销毁这条依赖浏览器行为,
3431
+ 而"隐藏但保留"的窗口期(看门狗兜底/总开关关掉)里声音是真会漏出去的 ⇒ 两条都做。 */
3432
+ try { sendRendererAudioPolicy(frame, true, true, "teardown") } catch (e) {}
3163
3433
  disposeWebFrame(frame);
3164
3434
  try { frame.removeAttribute("src"); } catch {}
3165
3435
  frame.style.display = "none";
@@ -3979,6 +4249,7 @@ function mpwThumbFail(el, why) {
3979
4249
  if (wd.fallback) {
3980
4250
  // 兜底后渲染器终于出画(迟到首帧)→ 自动恢复实时渲染
3981
4251
  wd.fallback = false;
4252
+ try { const f0 = bgElements().frame; if (f0) { sendRendererAudioPolicy(f0, true, false, "scene-recovered"); applyWebMute(f0) } } catch (e) {}
3982
4253
  mpwSceneClearFallback();
3983
4254
  mpwSceneSetHint(mpwSceneT("scnRender.wd.recovered", "场景渲染器已出画,自动恢复实时渲染"));
3984
4255
  try { fetch(HOST_BASE + "/diag", { method: "POST", body: JSON.stringify({ kind: "scene-render-health", why: "late-first-frame-recovered", ident: wd.ident, armedAt: wd.armedAt, at: new Date().toISOString() }) }).catch(() => {}) } catch {}
@@ -3990,6 +4261,12 @@ function mpwThumbFail(el, why) {
3990
4261
  if (!wd || wd.fallback) return;
3991
4262
  wd.fallback = true;
3992
4263
  wd.fallbackAt = Date.now();
4264
+ /* ①(2026-09-25 任务 ㉑ 追加) **兜底隐藏 iframe 时,必须把帧内声音停掉**:
4265
+ 这条路径是"**隐藏但保留元素与 src**"(等迟到首帧恢复),而隐藏的 iframe 里音频照样在放 ——
4266
+ 真机现场就是"壁纸没有自动播放(显示静态帧)、声音却在响,而且**不是当前壁纸的**音频"。
4267
+ 跨源帧没有别的通道 ⇒ 走 postMessage `park`(渲染器侧 pause 元素 + ctx.suspend)。 */
4268
+ try { const f0 = bgElements().frame; if (f0) sendRendererAudioPolicy(f0, true, true, "scene-fallback:" + String(why || "")) } catch (e) {}
4269
+ try { window.__mpwSceneFallbackPark = { at: Date.now(), why: String(why || "") } } catch (e) {}
3993
4270
  try {
3994
4271
  const { img, wrap } = bgElements();
3995
4272
  if (wrap) wrap.classList.add("mpw-scene-fallback");
@@ -4289,6 +4566,45 @@ function mpwThumbFail(el, why) {
4289
4566
  } catch {}
4290
4567
  } catch {}
4291
4568
  }
4569
+ /** ①(2026-09-25 任务 ㉑) **场景渲染器帧的图级音频策略**通道(`{type:"mpw-audio-policy"}`)。
4570
+ * 为什么必须单独有它(真机读数,见 docs/reports-issue0924a3-audio-leak.md):
4571
+ * 场景渲染器 iframe 在 `127.0.0.1:8902`、插件页在 `127.0.0.1:3080` ⇒ **不同源**,
4572
+ * 于是 `applyWebMute` 的三条既有通道**一条都到不了帧里**:
4573
+ * · `frame.contentDocument` = null(逐元素写 `muted` 那趟扫不到);
4574
+ * · `window.frameElement` = null(帧内读不到宿主写的 `frame.muted` expando);
4575
+ * · shim policy 只对 `?mpwshim=1` 的网页壁纸帧发。
4576
+ * 实测(静音=开 + 场景档 + 刷新):帧内 4 条 `<audio>` 全是 `muted:false/volume:0.25`、
4577
+ * AudioContext `running`、接 destination 那条边的抽头 rms≈0.013 → **静音开着照样出声**。
4578
+ * `postMessage` 是**跨源唯一可达**的通道;渲染器侧收口是一个 master GainNode(图级归零),
4579
+ * 见 demo.html 的 `mpwAudioSetPolicy` / `mpwAudioConnectMaster`。
4580
+ * 幂等 + **刷新后必发**:`contentWindow` 换了(帧重载/换 src/刷新)⇒ 即使值相同也重发一次 ——
4581
+ * 这正是"刷新之后壁纸又开始出声"的那条缝(旧写法只在挂载/切歌时下发)。 */
4582
+ function sendRendererAudioPolicy(frame, mute, park, why) {
4583
+ try {
4584
+ const cw = frame && frame.contentWindow;
4585
+ if (!cw || typeof cw.postMessage !== "function") return false;
4586
+ const want = { muted: !!mute, volume: npWantVolume() };
4587
+ /* ①(2026-09-25 任务 ㉑ 追加) `park` = “把这一页收起来”(宿主隐藏/看门狗兜底/省电暂停):
4588
+ 渲染器侧收到就**停源**(pause 元素 + ctx.suspend),不是只压增益。 */
4589
+ if (park !== undefined) want.park = !!park;
4590
+ const prev = frame.__mpwRendererAudioPolicy;
4591
+ const stale = !prev || prev.win !== cw;
4592
+ if (!stale && prev.muted === want.muted && prev.volume === want.volume && prev.park === want.park) return false;
4593
+ frame.__mpwRendererAudioPolicy = { win: cw, muted: want.muted, volume: want.volume, park: want.park, at: Date.now() };
4594
+ let posted = false;
4595
+ try { cw.postMessage({ type: "mpw-audio-policy", muted: want.muted, volume: want.volume, park: want.park, why: String(why || "") }, "*"); posted = true } catch (e) { /* 跨源不可达时如实留 false */ }
4596
+ /* 同源时再走一遍**真能力** API(跨源读不到 `__wp` ⇒ 静默跳过;幂等由渲染器侧保证)。 */
4597
+ try { if (cw.__wp && typeof cw.__wp.setAudioPolicy === "function") cw.__wp.setAudioPolicy({ muted: want.muted, volume: want.volume, park: want.park }) } catch (e) {}
4598
+ /* 可查台账(探针/tools/audio-leak-frame-probe.mjs 读它证明"刷新后重新下发过")。 */
4599
+ try {
4600
+ const rec = window.__mpwRendererAudioPush || (window.__mpwRendererAudioPush = { n: 0, last: null, log: [] });
4601
+ rec.n++;
4602
+ rec.last = { at: Date.now(), muted: want.muted, volume: want.volume, park: want.park, why: String(why || ""), stale: stale, posted: posted, sameOrigin: (() => { try { return !!cw.document } catch (e) { return false } })() };
4603
+ rec.log.push(rec.last); if (rec.log.length > 12) rec.log.shift();
4604
+ } catch (e) {}
4605
+ return posted;
4606
+ } catch (e) { return false }
4607
+ }
4292
4608
  /** ①(新) web 壁纸静音:iframe.muted + 内部所有 audio/video 元素 muted。
4293
4609
  * (仅 iframe.muted 对部分用 WebAudio/动态创建的壁纸无效,双保险。)
4294
4610
  * ①(NP-3) 我们自己的曲目播放器在放音时**强制静音帧内**(见 npAudioOwns):壁纸目录里
@@ -4307,6 +4623,14 @@ function mpwThumbFail(el, why) {
4307
4623
  的 `__mpwAudioBusRepatch` 台账),这里是同一类问题的插件侧落点。
4308
4624
  语义不变:`mute` 的算法、释放路径(只撤自己按过的)与失败留痕都不动。 */
4309
4625
  if (frame && frame.muted !== mute) frame.muted = mute;
4626
+ /* ①(2026-09-25 任务 ㉑) **图级静音通道**(跨源唯一可达的那条):场景渲染器帧必须走它 ——
4627
+ 它的音轨挂在 WebAudio 图上,元素 `muted` 只覆盖元素路径,而 8902 与 3080 不同源,
4628
+ 上面那三条老通道一条都到不了。这里**每次 applyWebMute 都尝试下发**(含帧 load、
4629
+ 800ms/2s 周期、以及刷新后的第一次),幂等由 `sendRendererAudioPolicy` 自己保证。 */
4630
+ try { sendRendererAudioPolicy(frame, mute); } catch {}
4631
+ /* ①(2026-09-25 任务 ㉑ 追加) 帧内音频归属/暂停状态也要跟着走:`npFrameSoundBlocked()` 为真
4632
+ (卡片暂停/我们真的在放音)时,把这一页**收起来**(停源),否则只是压增益。 */
4633
+ try { if (npFrameSoundBlocked()) sendRendererAudioPolicy(frame, true, true, "np-owns"); } catch {}
4310
4634
  // ①(2026-09-16 I 项) shim 通道:帧内静音由 shim 执行(不透明源读不到帧内元素)
4311
4635
  if (frame && mpwWebShimUrl(frame.getAttribute("src"))) {
4312
4636
  const want = { muted: mute, speed: npPolicySpeed(), volume: npWantVolume() };
@@ -5528,7 +5852,7 @@ function mpwThumbFail(el, why) {
5528
5852
  // 链路:插件宿主 /raw 暴露容器字节(PKGM0018 场景束 / PKGV scene.pkg)
5529
5853
  // → 渲染器页面 ?pkgurl=<该 url>(浏览器直连带 cookie,过 DSH 鉴权)
5530
5854
  // → 渲染器 parsePkg 直接吃同族容器(RE-25 证实布局相同)→ 实时渲染。
5531
- // sceneRendererUrl 可在设置里覆盖(默认本机 8899)。
5855
+ // sceneRendererUrl 可在设置里覆盖(默认本机 8902 的 `/webloader/`;旧默认是 8899,见 ISSUE0924A 第 20 点)。
5532
5856
  // ① 渲染器可达性探测(结果缓存;未知时按可用处理,避免首次使用被误拦)
5533
5857
  const probeSceneRenderer = () => {
5534
5858
  try {
@@ -5558,7 +5882,12 @@ function mpwThumbFail(el, why) {
5558
5882
 
5559
5883
  const mpwSceneRendererBase = () => {
5560
5884
  try { const s = readSection(); if (s && s.sceneRendererUrl) return String(s.sceneRendererUrl) } catch {}
5561
- return "http://127.0.0.1:8899/";
5885
+ /* ①(2026-09-25 ISSUE0924A 第 20 点) 默认渲染器地址从 `http://127.0.0.1:8899/` 换成
5886
+ `http://127.0.0.1:8902/webloader/`:8902 是**一站式**测试台,它自己就直供渲染器面
5887
+ (`/webloader/`、`/bundle.js`、`/core/**`、`/pkg`、`/project`、`/diag-flags.json`,带 `X-Bench-Served: local`),
5888
+ 实测把 8899 进程**停掉**这些路由仍全部 200 ⇒ 不必再为了壁纸渲染单独启一个 8899。
5889
+ 尾斜杠**必须留**:下面的 `fetch(base + "diag-flags.json")` 是按字符串拼的。 */
5890
+ return "http://127.0.0.1:8902/webloader/";
5562
5891
  };
5563
5892
  const applySceneViaRenderer = (meta) => {
5564
5893
  // meta: { rawFile, folder, title, key }
@@ -6429,7 +6758,15 @@ function mpwThumbFail(el, why) {
6429
6758
  });
6430
6759
  } catch (e) {}
6431
6760
  } else {
6432
- try { const f = bgElements().frame; if (f) f.style.display = "none"; } catch {}
6761
+ try {
6762
+ const f = bgElements().frame
6763
+ if (f) {
6764
+ /* ①(2026-09-25 任务 ㉑ 追加) **总开关关掉 = 只隐藏不卸载** ⇒ 帧内音频必须停掉
6765
+ (旧写法只 `display:none`,隐藏 iframe 里的媒体照放)。 */
6766
+ try { sendRendererAudioPolicy(f, true, true, "wallpaper-disabled") } catch (e) {}
6767
+ f.style.display = "none"
6768
+ }
6769
+ } catch {}
6433
6770
  }
6434
6771
  // ①(修正) 刷新 buildCss(hasImage 已含 webUrl)→ .mpw-bgWrap.mpw-web iframe 可见
6435
6772
  try { const se = getStyleEl(); if (se) se.textContent = buildCss(section); } catch (err) { mpwErr("buildCss(web 路径)", err); }
@@ -7110,7 +7447,7 @@ function mpwThumbFail(el, why) {
7110
7447
  try {
7111
7448
  const d = ev && ev.data;
7112
7449
  if (!d || d.type !== "mpw-ln-mode") return;
7113
- // 只接受渲染器来源(:8899 或 strict 下的不透明源 "null")——contentWindow 精确匹配优先,
7450
+ // 只接受渲染器来源(**配置的那个 renderer base** 或 strict 下的不透明源 "null")——contentWindow 精确匹配优先,
7114
7451
  // 避免任意页面控制按键;①(B6) 改走统一校验(旧规则会拒掉 Origin:"null")。
7115
7452
  if (!mpwIsSceneFrameMsg(ev)) return;
7116
7453
  lnMode = !!d.on;
@@ -8267,6 +8604,19 @@ function mpwThumbFail(el, why) {
8267
8604
  /* ①(NP-3) 我们的播放器在放音时,web 壁纸帧内的那份声音要让位(同一首会被放两遍)。 */
8268
8605
  try { return !!(npAudio && !npAudio.paused && (npAudio.currentSrc || npAudio.getAttribute("src") || npAudio.src)) } catch (e) { return false }
8269
8606
  }
8607
+ /** ①(2026-09-25 任务 ㉑) **"我们真的在出声"**(比 `npAudioOwns()` 严一档):元素在放 **且**
8608
+ * 没被静音 **且** 音量 > 0。为什么单独有它:`npFrameSoundBlocked()` 只该在"帧内那份声音会与
8609
+ * **可听见的**我们叠在一起"时压帧内 —— 旧口径用 `npAudioOwns()`(只看 `paused`),
8610
+ * 于是"元素在放但被 muted/volume=0"也算占用 ⇒ 帧内被白白压住(用户报的"壁纸自己没声音")。
8611
+ * 反过来,用户报"静音开着仍有声音"那一条**不靠它**:设置项 `mute` 是独立的一项,
8612
+ * 下面 `npFrameSoundBlocked()` 里仍旧算数。 */
8613
+ function npOwnAudible() {
8614
+ try {
8615
+ if (!npAudioOwns()) return false
8616
+ if (npAudio.muted) return false
8617
+ return Number(npAudio.volume) > 0
8618
+ } catch (e) { return false }
8619
+ }
8270
8620
  /* ══════════════════════════════════════════════════════════════════════════════════
8271
8621
  ④(2026-09-20 真机修复)「卡片的静音/暂停必须**真的管住壁纸自带的音频**」
8272
8622
  ──────────────────────────────────────────────────────────────────────────────────
@@ -8313,8 +8663,11 @@ function mpwThumbFail(el, why) {
8313
8663
  const s = readSection();
8314
8664
  const on = s.npNowPlaying !== void 0 ? !!s.npNowPlaying : DEFAULT_NP_NOW_PLAYING;
8315
8665
  if (!on) return false;
8666
+ /* ①(2026-09-25 任务 ㉑) 口径对齐"**真的在放音**":元素在放但被 muted/音量 0 ⇒ 不占用,
8667
+ 帧内那份声音放行(旧口径只看 `paused` ⇒ 白白压住壁纸自带的 BGM)。
8668
+ ⚠ 这两行必须**紧邻**(`wallpaper-lifecycle-test` 的变异锚点钉的就是它们连在一起的字面量)。 */
8316
8669
  if (npCardPaused) return true;
8317
- return npAudioOwns();
8670
+ return npOwnAudible();
8318
8671
  } catch (e) { return false }
8319
8672
  }
8320
8673
  /** ④ 把"帧内音频现在归谁管"如实发布到 DOM/全局(探针与设置面板都读它,不猜)。 */
@@ -9194,7 +9547,20 @@ function mpwThumbFail(el, why) {
9194
9547
  // (旧写法恢复时会 play() **作者自己**停着的媒体 —— 省电档默认打开后这会变成常态问题)。
9195
9548
  // ①(2026-09-23) 再加一条前置:**这一下真的送达了**(shim 投出成功,或我们确实 pause 到了
9196
9549
  // 至少一个元素)才记账;失败路径不许把"没停成"记成"我们停的"。
9550
+ /* ①(2026-09-25 任务 ㉑ 追加) 跨源帧(场景渲染器 / 不透明源 web 帧):shim 到不了、
9551
+ `contentDocument` 为 null ⇒ 上面两条都等于没停。这时走 postMessage `park`(渲染器侧
9552
+ 真的 pause + suspend),并把"我们停过"如实记账(否则恢复时会去 play 作者自己停着的媒体)。 */
9553
+ /* `shimOk` 只等于"postMessage 投出没抛错" —— 对**不是** shim 的帧(场景渲染器)它同样返回 true,
9554
+ 那条 `{mpw:"mpw:web", op:"pause"}` 发过去根本没人收 ⇒ 必须按"**这个帧是不是 shim 帧**"分流:
9555
+ · shim 帧(`?mpwshim=1`):帧内 shim 自己按下暂停(既有语义,不动);
9556
+ · 非 shim 帧 / 跨源帧:走 `park`(渲染器侧真停源)。 */
9557
+ const isShimFrame = !!(frame && mpwWebShimUrl(frame.getAttribute("src")));
9558
+ let parked = false;
9559
+ if ((!isShimFrame || !shimOk) && n === 0) { try { parked = sendRendererAudioPolicy(frame, true, true, "pause-frame") } catch (e) { parked = false } }
9197
9560
  webFramePausedByUs = !!(shimOk || n > 0);
9561
+ /* ①(2026-09-25 任务 ㉑ 追加) `park` 也是"我们按下的暂停"(跨源帧那条)⇒ 记账,
9562
+ 恢复时才允许撤销(这一行**单独**写:`silent-failure-guards-test` 的变异锚点钉的是上面那行的字面量)。 */
9563
+ if (parked) webFramePausedByUs = true;
9198
9564
  return n;
9199
9565
  } catch { return 0 }
9200
9566
  }
@@ -9205,6 +9571,8 @@ function mpwThumbFail(el, why) {
9205
9571
  if (!frame) return;
9206
9572
  if (!webFramePausedByUs) return; // ⑤ 我们没停过 ⇒ 一个字节都不碰
9207
9573
  webFramePausedByUs = false;
9574
+ /* ①(2026-09-25 任务 ㉑ 追加) 撤销我们自己按下的"收起来"(跨源帧那条)。 */
9575
+ try { sendRendererAudioPolicy(frame, !!readSection().mute || npFrameSoundBlocked(), false, "resume-frame") } catch (e) {}
9208
9576
  webShimCallChecked(frame, { op: "pause", value: false }, "resumeWebFrame");
9209
9577
  const doc = frame.contentDocument;
9210
9578
  if (doc) {
@@ -11453,6 +11821,17 @@ function installAudioBus(win, opts = {}) {
11453
11821
  pauseWebFrame: () => { try { return pauseWebFrame() } catch (e) { return String(e && e.message || e) } },
11454
11822
  resumeWebFrame: () => { try { resumeWebFrame(); return true } catch (e) { return String(e && e.message || e) } },
11455
11823
  pauseWallpaperVideo: () => { try { pauseWallpaperVideo(); return true } catch (e) { return String(e && e.message || e) } },
11824
+ /* ①(2026-09-25 任务 ㉑ 追加) 场景看门狗兜底:真机"壁纸显示静态帧、声音却在响(且不是当前
11825
+ 壁纸的音频)"那条路径。跑的是**真实** `sceneFallbackActivate`;钩子只负责播种最小
11826
+ watchdog 状态(`__mpwSceneWd`),否则它自己会因 `!wd` 提前返回(判据就变成空转)。 */
11827
+ sceneFallback: (why) => {
11828
+ try {
11829
+ if (!__mpwSceneWd) __mpwSceneWd = { ident: "test-hook", info: null, url: "", armedAt: Date.now(), secs: 0, confirmed: false, fallback: false, lastPostAt: 0, checkTimer: 0, fallbackAt: 0 };
11830
+ sceneFallbackActivate(String(why || "test"));
11831
+ return { fallback: !!(__mpwSceneWd && __mpwSceneWd.fallback), park: window.__mpwSceneFallbackPark || null };
11832
+ } catch (e) { return String(e && e.message || e) }
11833
+ },
11834
+ sceneWd: () => { try { return __mpwSceneWd ? { ident: __mpwSceneWd.ident, fallback: !!__mpwSceneWd.fallback, confirmed: !!__mpwSceneWd.confirmed } : null } catch (e) { return null } },
11456
11835
  applyFrostInline: (sec) => { try { applyFrostInline(sec || readSection()); return true } catch (e) { return String(e && e.message || e) } },
11457
11836
  startFrostObserver: () => { try { startFrostObserver(); return true } catch (e) { return String(e && e.message || e) } },
11458
11837
  startCompatObserver: () => { try { startCompatObserver(); return true } catch (e) { return String(e && e.message || e) } },
@@ -17166,7 +17545,13 @@ body[data-mpw-modal] [role="dialog"] {
17166
17545
  }
17167
17546
  };
17168
17547
  const [url, setUrl] = react.useState("");
17169
- const [propsExpanded, setPropsExpanded] = react.useState(false);
17548
+ // ①(2026-09-24 用户 bug 修复) **默认展开**:旧默认 false ⇒ 面板只剩一个「展开全部(N)」
17549
+ // 按钮,用户视角就是"只显示了一项、展不开"。现在默认把官方属性**逐项摊开**。
17550
+ const [propsExpanded, setPropsExpanded] = react.useState(true);
17551
+ // ①(新) 分组折叠(按 group key 记;默认全展开)+ 文本框草稿(失焦/回车才落盘,避免逐字符重挂壁纸)
17552
+ const [propGroupsClosed, setPropGroupsClosed] = react.useState({});
17553
+ const [propDraft, setPropDraft] = react.useState({});
17554
+ const propTimer = react.useRef(null);
17170
17555
  const [conflictModal, setConflictModal] = react.useState(false);
17171
17556
  const [previewModal, setPreviewModal] = react.useState(false);
17172
17557
  const [errorModal, setErrorModal] = react.useState(false);
@@ -18850,12 +19235,14 @@ body[data-mpw-modal] [role="dialog"] {
18850
19235
 
18851
19236
 
18852
19237
  const propEdits = (section.propEdits && section.propEdits[section.mpkgKey]) || {};
18853
- const setProp = (key, value) => {
19238
+ /** ①(2026-09-24 用户 bug 修复) 可调参数**写入通道**(原 `setProp` 是死代码,全仓 0 处调用)。
19239
+ * `instant=false` 走 writeSection 的防抖路径(滑杆拖动/文本输入不每步重挂壁纸)。 */
19240
+ const setProp = (key, value, instant) => {
18854
19241
  const key2 = section.mpkgKey;
18855
19242
  if (!key2 || !safePropKey(key)) return;
18856
19243
  const edits = Object.assign({}, propEdits, { [key]: value });
18857
19244
  const propEditsAll = Object.assign({}, section.propEdits || {}, { [key2]: edits });
18858
- commit({ propEdits: propEditsAll }, true);
19245
+ commit({ propEdits: propEditsAll }, instant !== false);
18859
19246
  // ①(2026-09-16 I 项) 网页壁纸 shim 通道:编辑立刻下发到已挂载的帧(沙箱下父页读不到帧内 DOM,
18860
19247
  // 只能靠 postMessage;作者侧 applyUserProperties 会收到增量)。
18861
19248
  try {
@@ -18868,10 +19255,12 @@ body[data-mpw-modal] [role="dialog"] {
18868
19255
  const tvProp = allProps.find((p) => /随现实时间|timevarying|real time/.test(p.label + " " + p.key));
18869
19256
  const tvEnabled = tvProp ? (propEdits[tvProp.key] !== void 0 ? !!propEdits[tvProp.key] : !!tvProp.value) : true;
18870
19257
  const TIME_KEYS = new Set(["morningtime", "daytime", "dusktime", "nighttime", "display"]);
18871
- // ④(重做) 参数渲染暂不可用(浏览器显示的是预渲染素材,改了不生效):
18872
- // 默认全部折叠,只显示「展开全部」按钮;点击展开后只读展示全部参数。
18873
- const propsShown = tvEnabled ? allProps : allProps.filter((p) => !TIME_KEYS.has(p.key));
18874
- const propsToShow = propsExpanded ? propsShown : [];
19258
+ /* ①(2026-09-24 用户 bug「WE 自带选项只显示一项且展不开」修复) —— 面板行模型:
19259
+ 由 `mpwPropsModel` 按官方口径产出(分组头 + 子项 + condition 显隐 + 控件类型 + 记账),
19260
+ 面板**默认展开**(原来默认折叠 ⇒ 用户只看到一个「展开全部」按钮)。 */
19261
+ const propModel = mpwPropsModel(allProps, propEdits);
19262
+ const propRows = propModel.rows.filter((r) => r.visible && (tvEnabled || !TIME_KEYS.has(r.key)));
19263
+ const propsShownCount = propRows.filter((r) => r.kind !== "group").length;
18875
19264
  // ②(修正) web 壁纸选项项数(折叠按钮显示用):分辨率+语言+音量+开关
18876
19265
  const webCfgCount = (() => {
18877
19266
  if (!webCfg || !webCfg.model) return 0;
@@ -19794,37 +20183,160 @@ body[data-mpw-modal] [role="dialog"] {
19794
20183
  ]),
19795
20184
 
19796
20185
 
19797
- // 可调参数展示(④(重做):默认折叠成「展开全部」按钮。
19798
- // ②(修正) mpkg → 只读 + "暂不可用"(浏览器显示的是预渲染素材,改不了);
19799
- // web 壁纸(L2D 类 loadJson SettingModel)→ 同一折叠区,**可改**(分辨率/
19800
- // 语言/音量写入 iframe 同源 localStorage)。两者共用 propsExpanded 折叠。
20186
+ // 可调参数展示(①(2026-09-24 用户 bug「WE 自带选项只显示一项且展不开」修复):
20187
+ // · **默认展开**(旧默认折叠 ⇒ 面板只剩「展开全部」一个按钮 = 用户说的"只显示一项");
20188
+ // · 每个官方属性渲染**真控件**(bool 开关 / slider 量程滑杆 / combo 下拉 / color 取色 /
20189
+ // textinput 文本框),onChange 走 `setProp` **真的写进 propEdits 并下发**(此前是死代码);
20190
+ // · `type:"group"` = 分组头(可折叠,默认展开),子项按官方 order 跟随;
20191
+ // · `condition` 为假的项**不显示**(含传递闭包),白名单外的表达式按"未知"照显示并记账;
20192
+ // · file/directory/scenetexture 需要渲染器资源通道 ⇒ **只读如实展示 + 写明原因**(不猜);
20193
+ // · schemecolor/usershortcut 等不渲染的条目**逐条记账**(下方一行提示,不静默)。
20194
+ // ②(修正) mpkg → 控件可调但画面是否即时变化取决于壁纸类型(预渲染素材=下次生效);
20195
+ // web 壁纸(L2D 类 loadJson SettingModel)→ 同一折叠区,分辨率/语言/音量写入 iframe 同源 localStorage。
19801
20196
  ((mpkgMeta && mpkgMeta.info && section.fromMpkg && allProps.length) || webCfg) ? h("div", { className: "mpw_field" }, [
19802
20197
  h("label", { className: "mpw_label" },
19803
- `${t("props.title")}${webCfg ? "" : `(${t("props.unavailable")})`}`),
20198
+ `${t("props.title")}${webCfg ? "" : `(${propsShownCount})`}`),
19804
20199
  h("p", { className: "mpw_hint" }, webCfg ? t("webcfg.desc") : t("props.desc")),
19805
20200
  propsExpanded ? h("div", { className: "mpw_props" }, [
19806
- // ── mpkg 只读参数列表 ──
19807
- mpkgMeta && mpkgMeta.info && section.fromMpkg && allProps.length ? propsToShow.map((p) => {
19808
- // 纯展示性条目(作者信息/说明):不渲染输入框
19809
- if (p.displayOnly) {
19810
- return h("div", { className: "mpw_prop mpw_static", key: p.key },
19811
- h("span", { className: "mpw_propLabel" }, p.label));
20201
+ // ── 官方 user properties(分组 + 控件;写入 propEdits)──
20202
+ mpkgMeta && mpkgMeta.info && section.fromMpkg && allProps.length ? propRows.map((row) => {
20203
+ // 分组头:可折叠(默认展开),带子项数
20204
+ if (row.kind === "group") {
20205
+ const closed = !!propGroupsClosed[row.key];
20206
+ return h("div", { className: "mpw_prop mpw_propGroup", key: "g:" + row.key }, [
20207
+ h("button", {
20208
+ className: "mpw_reset mpw_moreBtn", type: "button", "data-mpw-propgroup": row.key,
20209
+ "aria-expanded": closed ? "false" : "true",
20210
+ onClick: () => setPropGroupsClosed(Object.assign({}, propGroupsClosed, { [row.key]: !closed }))
20211
+ }, (closed ? "▸ " : "▾ ") + (row.label || row.key) + `(${row.childCount})`)
20212
+ ]);
19812
20213
  }
19813
- const edited = propEdits[p.key] !== void 0 ? propEdits[p.key] : p.value;
19814
- const isBool = typeof p.value === "boolean";
20214
+ // 被折叠的分组 ⇒ 其子项不渲染
20215
+ if (row.group && propGroupsClosed[row.group]) return null;
20216
+ // 官方口径不渲染的条目(schemecolor / usershortcut / 未知类型):
20217
+ // **不渲染控件**(只在上面的"未渲染条目"一行里如实登记)
20218
+ if (row.kind === "skip") return null;
20219
+ const indent = row.group ? { marginLeft: 12 } : null;
20220
+ const shown = row.value;
19815
20221
  const fmt = (v) => {
19816
20222
  if (typeof v === "boolean") return v ? t("props.on") : t("props.off");
19817
- if (Array.isArray(p.options) && p.options.length) {
19818
- const o = p.options.find((x) => String(x.value) === String(v));
20223
+ if (Array.isArray(row.options) && row.options.length) {
20224
+ const o = row.options.find((x) => String(x.value) === String(v));
19819
20225
  return o ? cleanLabel(o.label) : String(v);
19820
20226
  }
19821
- return String(v);
20227
+ return String(v === void 0 || v === null ? "" : v);
19822
20228
  };
19823
- return h("div", { className: "mpw_prop", key: p.key }, [
19824
- h("div", { className: "mpw_propLabel" }, h("b", null, p.label)),
19825
- h("span", { className: "mpw_propValue" }, fmt(edited))
20229
+ // 说明行(type text/txt 或无 type 的作者文案):不渲染输入控件
20230
+ if (row.kind === "note") {
20231
+ return h("div", { className: "mpw_prop mpw_static", key: row.key, style: indent },
20232
+ h("span", { className: "mpw_propLabel" }, row.label || row.key));
20233
+ }
20234
+ const labelEl = h("div", { className: "mpw_propLabel" }, [
20235
+ h("b", null, row.label || row.key),
20236
+ row.edited ? h("span", { className: "mpw_hint" }, " · " + t("props.edited")) : null
20237
+ ]);
20238
+ // file/directory/scenetexture:官方语义需要渲染器资源通道 ⇒ 只读 + 写明原因
20239
+ if (row.kind === "readonly") {
20240
+ return h("div", { className: "mpw_prop", key: row.key, style: indent }, [
20241
+ labelEl,
20242
+ h("span", { className: "mpw_propValue" }, fmt(shown)),
20243
+ h("span", { className: "mpw_hint" }, row.kindWhy)
20244
+ ]);
20245
+ }
20246
+ // ── 真控件 ──
20247
+ if (row.kind === "control" && row.type === "bool") {
20248
+ return h("div", { className: "mpw_prop", key: row.key, style: indent }, [
20249
+ h("div", { className: "mpw_inline" }, [
20250
+ h(Toggle, { checked: !!shown, onChange: (v) => setProp(row.key, !!v) }),
20251
+ h("span", { className: "mpw_propLabel" }, row.label || row.key),
20252
+ row.edited ? h("span", { className: "mpw_hint" }, " · " + t("props.edited")) : null
20253
+ ])
20254
+ ]);
20255
+ }
20256
+ if (row.kind === "control" && row.type === "slider") {
20257
+ const min = typeof row.min === "number" ? row.min : 0;
20258
+ const max = typeof row.max === "number" ? row.max : 1;
20259
+ const step = typeof row.step === "number" && row.step > 0 ? row.step : ((max - min) / 100 || 0.01);
20260
+ const base = Number(shown);
20261
+ // ① 与插件既有滑杆同一口径:拖动时**即时显示**,落盘/重挂防抖 350ms
20262
+ // (逐个 input 事件都 commit → applyFromStorage 会把壁纸层反复重挂)
20263
+ const live = propDraft[row.key] !== void 0 ? Number(propDraft[row.key]) : (isFinite(base) ? base : min);
20264
+ const flush = (v) => {
20265
+ if (propTimer.current) { clearTimeout(propTimer.current); propTimer.current = null }
20266
+ setProp(row.key, v, false);
20267
+ };
20268
+ return h("div", { className: "mpw_prop", key: row.key, style: indent }, [
20269
+ labelEl,
20270
+ h("div", { className: "mpw_inline" }, [
20271
+ h("input", {
20272
+ className: "mpw_slider", type: "range", min, max, step,
20273
+ "data-mpw-prop": row.key,
20274
+ value: live,
20275
+ onChange: (ev) => {
20276
+ const v = Number(ev.target.value);
20277
+ setPropDraft(Object.assign({}, propDraft, { [row.key]: v }));
20278
+ if (propTimer.current) clearTimeout(propTimer.current);
20279
+ propTimer.current = setTimeout(() => { propTimer.current = null; flush(v) }, 350);
20280
+ },
20281
+ onMouseUp: (ev) => flush(Number(ev.target.value)),
20282
+ onTouchEnd: (ev) => flush(live),
20283
+ onBlur: (ev) => flush(Number(ev.target.value))
20284
+ }),
20285
+ h("span", { className: "mpw_value" }, fmt(live))
20286
+ ])
20287
+ ]);
20288
+ }
20289
+ if (row.kind === "control" && row.type === "combo") {
20290
+ const opts = (row.options || []).map((o) => ({ value: String(o.value), label: cleanLabel(o.label) }));
20291
+ return h("div", { className: "mpw_prop", key: row.key, style: indent }, [
20292
+ labelEl,
20293
+ h(SelectBox, {
20294
+ name: "prop:" + row.key, value: String(shown === void 0 ? "" : shown),
20295
+ placeholder: fmt(shown), options: opts,
20296
+ onChange: (v) => setProp(row.key, v)
20297
+ })
20298
+ ]);
20299
+ }
20300
+ if (row.kind === "control" && row.type === "color") {
20301
+ const hex = mpwPropColorToHex(shown);
20302
+ return h("div", { className: "mpw_prop", key: row.key, style: indent }, [
20303
+ labelEl,
20304
+ h("div", { className: "mpw_inline" }, [
20305
+ h("input", {
20306
+ className: "mpw_colorSwatch", type: "color", "data-mpw-prop": row.key,
20307
+ value: hex || "#000000",
20308
+ onChange: (ev) => setProp(row.key, mpwPropHexToColor(ev.target.value))
20309
+ }),
20310
+ h("span", { className: "mpw_hint" }, (hex || "") + " (" + fmt(shown) + ")")
20311
+ ])
20312
+ ]);
20313
+ }
20314
+ // textinput / 其余可编辑文本
20315
+ const draftKey = row.key;
20316
+ return h("div", { className: "mpw_prop", key: row.key, style: indent }, [
20317
+ labelEl,
20318
+ h("input", {
20319
+ className: "mpw_input", type: "text", "data-mpw-prop": row.key,
20320
+ value: propDraft[draftKey] !== void 0 ? propDraft[draftKey] : fmt(shown),
20321
+ onChange: (ev) => setPropDraft(Object.assign({}, propDraft, { [draftKey]: ev.target.value })),
20322
+ onBlur: () => {
20323
+ const v = propDraft[draftKey];
20324
+ if (v === void 0) return;
20325
+ setProp(draftKey, v);
20326
+ const next = Object.assign({}, propDraft); delete next[draftKey]; setPropDraft(next);
20327
+ },
20328
+ onKeyDown: (ev) => { if (ev.key === "Enter") { try { ev.currentTarget.blur() } catch (e) {} } }
20329
+ })
19826
20330
  ]);
19827
20331
  }) : null,
20332
+ // 记账:未渲染的官方条目(schemecolor / usershortcut / 未知类型)如实列出,不静默
20333
+ mpkgMeta && mpkgMeta.info && section.fromMpkg && propModel.counts.skipped
20334
+ ? h("p", { className: "mpw_hint" },
20335
+ t("props.skipped") + propModel.rows.filter((r) => r.kind === "skip").map((r) => r.key + ":" + r.kindWhy).join(";"))
20336
+ : null,
20337
+ mpkgMeta && mpkgMeta.info && section.fromMpkg && propModel.counts.hidden
20338
+ ? h("p", { className: "mpw_hint" }, t("props.hiddenByCond") + propModel.counts.hidden)
20339
+ : null,
19828
20340
  !tvEnabled ? h("p", { className: "mpw_hint" }, t("props.tvOff")) : null,
19829
20341
  // ── web 壁纸可改选项(分辨率/语言/音量,写入 iframe localStorage)──
19830
20342
  webCfg ? h("div", { className: "mpw_props" }, [
@@ -19871,13 +20383,15 @@ body[data-mpw-modal] [role="dialog"] {
19871
20383
  const next = Object.assign({}, s, { propEdits: pe });
19872
20384
  writeSection(next, true);
19873
20385
  setSection(next);
20386
+ // ① 恢复默认时把滑杆/文本框的本地草稿一并清掉(否则控件仍显示已改的值)
20387
+ setPropDraft({});
19874
20388
  setHint(t("props.resetDone"));
19875
20389
  } }, t("props.resetWallpaper"))
19876
20390
  ])
19877
20391
  ]) : null,
19878
20392
  h("div", { className: "mpw_inline" }, [
19879
20393
  h("button", { className: "mpw_reset mpw_moreBtn", type: "button", onClick: () => setPropsExpanded(!propsExpanded) },
19880
- propsExpanded ? t("props.collapse") : t("props.expand") + `(${webCfg ? webCfgCount : propsShown.length})`)
20394
+ propsExpanded ? t("props.collapse") : t("props.expand") + `(${webCfg ? webCfgCount : propsShownCount})`)
19881
20395
  ])
19882
20396
  ]) : null,
19883
20397
  // ①(新) 省电(从「其他」tab 挪到壁纸设置最下面):遮挡暂停三档(tab 内结尾)
@@ -20722,7 +21236,7 @@ body[data-mpw-modal] [role="dialog"] {
20722
21236
  "scnRender.extUrl": "渲染器扩展钩子地址(sceneExtUrl,可选)",
20723
21237
  "scnRender.report": "场景渲染上报(排障)",
20724
21238
  "scnRender.report.desc": "开启后壁纸每 10 秒把渲染器现场(截图 + 逐层绘制台账 + 纹理统计)写到渲染器的 reports/ 目录,用于定位“坐标/可见性”问题。仅排障时开,默认关(有磁盘写入)",
20725
- "scnRender.extUrl.ph": "http://127.0.0.1:8899/ext",
21239
+ "scnRender.extUrl.ph": "http://127.0.0.1:8902/webloader/",
20726
21240
  "scnRender.extUrl.hint": "留空=不启用。填写后按 extbase 拼进场景渲染 iframe(见渲染器仓文档 EXTENSION-HOOKS.md);点「测试」验证索引可达并显示钩子数量。",
20727
21241
  "scnRender.extUrl.save": "保存",
20728
21242
  "scnRender.extUrl.saved": "扩展钩子地址已保存,下次应用场景壁纸生效",
@@ -20754,7 +21268,7 @@ body[data-mpw-modal] [role="dialog"] {
20754
21268
  "scnRender.bigPkg": "该场景包体积较大,首帧可能较慢",
20755
21269
  // ①(MERGED-3 1.3/2.3) 诊断开关速查区(集合 == diag-flags.json common,panel-smoke 断言)
20756
21270
  "diag.sec": "诊断开关速查(渲染器)",
20757
- "diag.sec.desc": "拼到渲染器地址后面用,如 http://127.0.0.1:8899/?id=3719111841&audit=3;常用开关如下,点「复制」拿 URL 片段。",
21271
+ "diag.sec.desc": "拼到渲染器地址后面用,如 http://127.0.0.1:8902/webloader/?id=3719111841&audit=3;常用开关如下,点「复制」拿 URL 片段。",
20758
21272
  "diag.expand": "展开开关",
20759
21273
  "diag.fold": "收起开关",
20760
21274
  "diag.copy": "复制",
@@ -20919,11 +21433,14 @@ body[data-mpw-modal] [role="dialog"] {
20919
21433
  "webcfg.saveFail": "保存失败:",
20920
21434
  "webcfg.readFail": "读取壁纸自身设置失败(为避免覆盖你已改过的选项,本次**不写回**):",
20921
21435
  "webcfg.fail": "应用失败:",
20922
- "props.desc": "浏览器显示的是壁纸引擎预渲染的素材,修改参数不会改变画面。以下为壁纸自带的参数及当前值,供对照:如需修改,请在壁纸引擎 App 中调整",
21436
+ "props.desc": "以下为壁纸**自带**的官方参数(project.json → general.properties),逐项可调:改动会保存到本插件设置,并即时下发给已加载的网页壁纸;静态帧/视频类素材的画面要等下次应用(或由场景渲染器应用)才反映改动",
20923
21437
  "props.unavailable": "暂不可用",
20924
21438
  "props.on": "开",
20925
21439
  "props.off": "关",
20926
21440
  "props.expand": "展开全部",
21441
+ "props.edited": "已改",
21442
+ "props.skipped": "未渲染的条目(如实登记,不做猜测):",
21443
+ "props.hiddenByCond": "按壁纸自带 condition 隐藏的条目数:",
20927
21444
  "props.important": "★ 关键开关",
20928
21445
  "props.resetWallpaper": "重置壁纸参数",
20929
21446
  "props.resetDone": "壁纸参数已恢复默认",
@@ -21392,11 +21909,14 @@ body[data-mpw-modal] [role="dialog"] {
21392
21909
  "webcfg.saveFail": "Save failed: ",
21393
21910
  "webcfg.readFail": "Could not read the wallpaper's own settings (nothing was written back, so your existing options are not overwritten): ",
21394
21911
  "webcfg.fail": "Apply failed: ",
21395
- "props.desc": "The browser shows pre-rendered wallpaper assets, so editing these options cannot change the picture. Listed below are the wallpaper's own parameters and their current values for reference; change them in the Wallpaper Engine app instead",
21912
+ "props.desc": "These are the wallpaper's own official options (project.json → general.properties), adjustable per item: changes are saved to this plugin's settings and pushed live to a loaded web wallpaper; for still-frame/video assets the picture reflects them after the next apply (or when a scene renderer applies them)",
21396
21913
  "props.unavailable": "unavailable",
21397
21914
  "props.on": "On",
21398
21915
  "props.off": "Off",
21399
21916
  "props.expand": "Show all",
21917
+ "props.edited": "edited",
21918
+ "props.skipped": "Not rendered (recorded as-is, never guessed): ",
21919
+ "props.hiddenByCond": "Items hidden by the wallpaper's own condition: ",
21400
21920
  "props.important": "★ Key switch",
21401
21921
  "props.resetWallpaper": "Reset wallpaper options",
21402
21922
  "props.resetDone": "Wallpaper options restored to defaults",
@@ -21531,7 +22051,7 @@ body[data-mpw-modal] [role="dialog"] {
21531
22051
  "scnRender.extUrl": "Renderer extension hooks URL (sceneExtUrl, optional)",
21532
22052
  "scnRender.report": "Scene render reporting (debug)",
21533
22053
  "scnRender.report.desc": "When on, the wallpaper sends the renderer's live state (screenshot + per-layer draw ledger + texture stats) to the renderer's reports/ folder every 10s, to diagnose position/visibility bugs. Debug only; off by default (writes to disk)",
21534
- "scnRender.extUrl.ph": "http://127.0.0.1:8899/ext",
22054
+ "scnRender.extUrl.ph": "http://127.0.0.1:8902/webloader/",
21535
22055
  "scnRender.extUrl.hint": "Empty = disabled. When set, it is passed to the scene iframe as extbase (see the renderer repo docs EXTENSION-HOOKS.md); press Test to verify the index and show the hook count.",
21536
22056
  "scnRender.extUrl.save": "Save",
21537
22057
  "scnRender.extUrl.saved": "Extension hooks URL saved; takes effect the next time a scene wallpaper is applied",
@@ -21563,7 +22083,7 @@ body[data-mpw-modal] [role="dialog"] {
21563
22083
  "scnRender.bigPkg": "This scene package is large; the first frame may be slow",
21564
22084
  // ①(MERGED-3 1.3/2.3) Diagnostic flags quick reference (set == diag-flags.json common)
21565
22085
  "diag.sec": "Diagnostic flags (renderer)",
21566
- "diag.sec.desc": "Append to the renderer URL, e.g. http://127.0.0.1:8899/?id=3719111841&audit=3. Common flags below; press Copy to grab the URL fragment.",
22086
+ "diag.sec.desc": "Append to the renderer URL, e.g. http://127.0.0.1:8902/webloader/?id=3719111841&audit=3. Common flags below; press Copy to grab the URL fragment.",
21567
22087
  "diag.expand": "Show flags",
21568
22088
  "diag.fold": "Hide flags",
21569
22089
  "diag.copy": "Copy",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mpkg-wallpaper",
3
- "version": "3.13.6",
3
+ "version": "3.13.8",
4
4
  "description": "DSH Web 壁纸引擎 Wallpaper Engine mpkg 背景插件:浏览器内直接解析 .mpkg(preview.gif 动态背景/内嵌 mp4 视频/多时段切换),整屏统一虚化/对话框/弹层/遮罩独立虚化、镜头缩放平移、时钟、冲突检测。",
5
5
  "private": false,
6
6
  "type": "module",