dsh-mpkg-wallpaper 3.13.3 → 3.13.4

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/lib/client.js CHANGED
@@ -169,6 +169,109 @@ function mpwTrace(ev, data) {
169
169
  } catch (e) {}
170
170
  }
171
171
  /* ═══ MPW-BLOBRETRY-END ═══ */
172
+ /* ═══ MPW-WEJSON-BEGIN ═══ */
173
+ /* 官方随包 JSON 的**宽容解析**(WE 允许尾逗号;我们的 `JSON.parse` 不允许)。
174
+ *
175
+ * ── 为什么必须有(官方证据,2026-09-24 官方产物盘点;与渲染器仓同一轮定性)──────────────
176
+ * · 官方随包发布的 `wallpaper_engine/assets/effects/fluidsimulation/effect.json`(10,224 B)
177
+ * **自己就带尾逗号**:第 402 行 `"shaders/effects/fluidsimulation_normal.vert",` 后面紧跟 `}`
178
+ * ⇒ 标准 `JSON.parse` 报 `Expecting value: line 403 column 2`。WE 照发照用 ⇒ **官方容忍尾逗号**
179
+ * (引擎用 jsoncpp,二进制里带 `allowTrailingCommas`/`allowComments` 开关名)。
180
+ * · 后果是**静默**的:本仓凡是"读包内条目失败就 catch 成 null"的地方(project.json 属性表、
181
+ * web 壁纸的 general.properties…),遇到这种文件会**整份丢掉** —— 属性面板空白、`{user:…}`
182
+ * 绑定全部回落默认值,而日志里一行都不会有。
183
+ * · 渲染器仓的对应修复编号 **P-177**(`core/we-scene-bundle.js` 的 `export function parseWeJson`
184
+ * + `weJsonStats()`,同一轮把页面与 bundle 里"读容器条目文本"的解析点全部换过去)。本块与它
185
+ * **逐条同口径**(行为对照,不是代码搬运:渲染器仓是 GPL-3.0-or-later、本仓 MIT ⇒ 这里按同一
186
+ * 语义**独立实现**,`tools/we-json-tolerance-test.mjs` 拿同一组夹具把两边的读数逐条对拍)。
187
+ * 宿主侧 `lib/pkg-extract.js` 里的同名实现(Node 侧、同一语义)与本块在同一份判据里逐夹具对拍
188
+ * (防两处漂移)。
189
+ *
190
+ * ── 口径(只做"官方明确容忍"的那两件事,其余照旧抛)──────────────────────────────────
191
+ * ① 尾逗号:`,` 后面(跳过空白)紧跟 `}` / `]` ⇒ 丢掉这个逗号;**字符串内的逗号一律不动**
192
+ * (扫描器带 inString/escape 状态,不会碰 `"a,}"` 这种内容)。
193
+ * ② 注释:**只在**"去掉尾逗号后仍解析失败"时才尝试剥 `//` 与 `/* *\/`(同样字符串内不动),
194
+ * 并单独计数 —— 这样"到底有没有真用到"现场看得见,不是我们猜的。
195
+ * ③ BOM(U+FEFF)与官方一致地吃掉(`JSON.parse` 自己不认 BOM)。
196
+ * **健康文件逐位等价于 `JSON.parse`**:先走 `JSON.parse(text)`,成功就原样返回(plain 计数 +1);
197
+ * 只有失败才落到宽容路径。真坏 JSON **仍然抛** ⇒ **调用点的 try/catch 语义一个都不变**
198
+ * (原来抛的还抛、原来吞的还吞,只是吞的那几处现在要记账,见 `mpwWeJsonSwallowed`)。
199
+ * 计数:`mpwWeJsonStats()` 返回 `{ calls, plain, trailingComma, comments, failed, swallowed }`
200
+ * —— 前五项与渲染器 `weJsonStats()` 同名同义(两仓对拍用同一组读数);`swallowed` 是本仓
201
+ * 额外加的"被调用方吞掉"记账(渲染器那边是调用点自己记日志,没有这一项)。 */
202
+ const MPW_WEJSON = { calls: 0, plain: 0, trailingComma: 0, comments: 0, failed: 0, swallowed: 0 };
203
+ /** 只读快照(对外只给这个,避免调用方改计数)。 */
204
+ function mpwWeJsonStats() { return Object.assign({}, MPW_WEJSON) }
205
+ /** 文本整形:一趟扫描,按需去掉**字符串外**的尾逗号(`withComments=false`)或尾逗号 + 注释
206
+ * (`withComments=true`)。**字符串里的一律照抄**(`"a,}"` / `"//"` / `"/*"` 一个字都不动);
207
+ * 注释替换成等长空白,行号与列号都保持可读;尾逗号判定会**越过空白与注释**看下一个有效字符
208
+ * (所以 `{"a":1, /* c *\/ }` 与不带注释的写法同解)。 */
209
+ function mpwWeJsonRepair(src, withComments) {
210
+ let out = "", inStr = false, esc = false;
211
+ for (let i = 0; i < src.length; i++) {
212
+ const c = src[i], d = src[i + 1];
213
+ if (inStr) { // 字符串内:只跟 escape 状态,内容逐字照抄
214
+ out += c;
215
+ if (esc) esc = false;
216
+ else if (c === "\\") esc = true;
217
+ else if (c === '"') inStr = false;
218
+ continue;
219
+ }
220
+ if (c === '"') { inStr = true; out += c; continue }
221
+ if (withComments && c === "/" && d === "/") { // `//` 行注释 ⇒ 空白
222
+ while (i < src.length && src[i] !== "\n") { out += " "; i++ }
223
+ out += "\n";
224
+ continue;
225
+ }
226
+ if (withComments && c === "/" && d === "*") { // `/* */` 块注释 ⇒ 空白
227
+ out += " "; i += 2;
228
+ while (i < src.length && !(src[i] === "*" && src[i + 1] === "/")) { out += (src[i] === "\n" ? "\n" : " "); i++ }
229
+ out += " "; i++;
230
+ continue;
231
+ }
232
+ if (c === ",") { // 尾逗号:往后看下一个有效字符(可跨空白/注释)
233
+ let j = i + 1;
234
+ for (;;) {
235
+ while (j < src.length && (src[j] === " " || src[j] === "\t" || src[j] === "\n" || src[j] === "\r")) j++;
236
+ if (!withComments) break;
237
+ if (src[j] === "/" && src[j + 1] === "/") { while (j < src.length && src[j] !== "\n") j++; continue }
238
+ if (src[j] === "/" && src[j + 1] === "*") { j += 2; while (j < src.length && !(src[j] === "*" && src[j + 1] === "/")) j++; j += 2; continue }
239
+ break;
240
+ }
241
+ if (src[j] === "}" || src[j] === "]") continue; // 尾逗号 ⇒ 丢掉这个逗号
242
+ }
243
+ out += c;
244
+ }
245
+ return out;
246
+ }
247
+ /**
248
+ * 解析官方随包 JSON(project.json / scene.json / materials / effects / models / combo 定义…)。
249
+ * 与 `JSON.parse` 的差别只有两条:容忍尾逗号;再不行才容忍注释(外加吃掉 BOM)。
250
+ * **其余一律照旧抛**(调用方的 try/catch 语义不变)。
251
+ * @param {string} text
252
+ * @returns {any}
253
+ */
254
+ function mpwParseWeJson(text) {
255
+ MPW_WEJSON.calls++;
256
+ const s0 = (typeof text === "string") ? text : String(text == null ? "" : text);
257
+ const s = s0.charCodeAt(0) === 0xFEFF ? s0.slice(1) : s0; // ① BOM:官方产物会带,JSON.parse 自己不认
258
+ try { const v = JSON.parse(s); MPW_WEJSON.plain++; return v } catch (e) { /* 健康文件到此为止(逐位等价 JSON.parse) */ }
259
+ try { const v = JSON.parse(mpwWeJsonRepair(s, false)); MPW_WEJSON.trailingComma++; return v } catch (e) { /* ② 尾逗号也不够 ⇒ 再试注释 */ }
260
+ try { const v = JSON.parse(mpwWeJsonRepair(s, true)); MPW_WEJSON.comments++; return v } // ③ 尾逗号 + 注释
261
+ catch (e) { MPW_WEJSON.failed++; throw e } // 真坏 JSON:照旧抛
262
+ }
263
+ /** 包内 JSON 解析失败、调用方**按原有语义吞掉**时的记账:计数 + 至少一行诊断(不静默)。
264
+ * (调用点的 try/catch 结构不动:这里只负责"吞了要说一声",返回值恒 null 方便 `catch { x = ... }` 写法。) */
265
+ function mpwWeJsonSwallowed(where, e) {
266
+ try { MPW_WEJSON.swallowed++ } catch (e2) {}
267
+ try {
268
+ if (typeof mpwErr === "function") mpwErr("包内 JSON 解析失败(按原语义吞掉): " + where, e);
269
+ else console.warn("[dsh-mpkg-wallpaper] 包内 JSON 解析失败(按原语义吞掉): " + where, e);
270
+ } catch (e2) {}
271
+ return null;
272
+ }
273
+ try { window.__mpwWeJson = { parse: mpwParseWeJson, stats: mpwWeJsonStats, swallowed: mpwWeJsonSwallowed }; } catch (e) {}
274
+ /* ═══ MPW-WEJSON-END ═══ */
172
275
  // ①(P-104 2026-09-17 用户发布纪律②) **插件侧 localStorage 也要有上限**:
173
276
  // 本插件只用固定几个键(STORE_KEY / mpw_settings_backup / mpwdiag / mpwwatch),键数量天然有界;
174
277
  // 真正的风险在**单值**——STORE_KEY 里含背景 data URL(本文件注释自述 1MB+)。这里给单值硬顶
@@ -352,7 +455,7 @@ function mpwTrace(ev, data) {
352
455
  // ①(MERGED-3 1.3) 诊断开关速查区离线副本(渲染器离线时用)。
353
456
  // 在线数据源 = <渲染器>/diag-flags.json(由 we-scene-demo/diag-flag-check.mjs 脚本生成,勿手改);
354
457
  // tools/panel-smoke.mjs 断言本副本 == diag-flags.json 的 common 集合 → 两边同步,勿单独改这里。
355
- const MPW_DIAG_FLAGS_FALLBACK = '[{"name":"att","usage":"att=legacy"},{"name":"mcc","usage":"mcc=1"},{"name":"piv","usage":"piv=1"},{"name":"align","usage":"align=0"},{"name":"parallax","usage":"parallax=legacy"},{"name":"audio","usage":"audio=1"},{"name":"whitefallback","usage":"whitefallback=0"},{"name":"hier","usage":"hier=0"},{"name":"isolate","usage":"isolate=<层名>"},{"name":"audit","usage":"audit=3"}]';
458
+ const MPW_DIAG_FLAGS_FALLBACK = '[{"name":"att","usage":"att=legacy"},{"name":"mcc","usage":"mcc=1"},{"name":"piv","usage":"piv=1"},{"name":"align","usage":"align=0"},{"name":"parallax","usage":"parallax=legacy"},{"name":"gyro","usage":"gyro=1"},{"name":"audio","usage":"audio=1"},{"name":"whitefallback","usage":"whitefallback=0"},{"name":"hier","usage":"hier=0"},{"name":"isolate","usage":"isolate=<层名>"},{"name":"audit","usage":"audit=3"}]';
356
459
  const DEFAULT_THINK_BG = false; // Deep diving 背景方框:默认取消(透明)
357
460
  const DEFAULT_ENABLED = true;
358
461
  const DEFAULT_CLOCK = false;
@@ -2343,9 +2446,10 @@ function mpwThumbFail(el, why) {
2343
2446
  const proj = projJson ? null : mpkg.entries.find((e) => e.name === "project.json");
2344
2447
  if (proj) {
2345
2448
  try {
2346
- const pj = JSON.parse(new TextDecoder().decode(await readEntry(proj)));
2449
+ // ①(P-177 同口径) 包内 project.json 走宽容解析(官方允许尾逗号;严格 parse 会整份丢掉)
2450
+ const pj = mpwParseWeJson(new TextDecoder().decode(await readEntry(proj)));
2347
2451
  propsMap = (pj.general && pj.general.properties) || null;
2348
- } catch {}
2452
+ } catch (e) { mpwWeJsonSwallowed("handleVideoTexes:project.json", e) } // 原来就吞;现在计数 + 一行诊断
2349
2453
  }
2350
2454
  const key2 = file.name + "|" + file.size;
2351
2455
  const prevS = readSection();
@@ -2576,7 +2680,8 @@ function mpwThumbFail(el, why) {
2576
2680
  /** 解析 project.json 文本,提取标题 + 可调参数信息。 */
2577
2681
  function extractProjectInfo(jsonText) {
2578
2682
  try {
2579
- const json = JSON.parse(jsonText);
2683
+ // ①(P-177 同口径) 包内 project.json 走宽容解析(官方允许尾逗号);健康文件与 JSON.parse 逐位等价
2684
+ const json = mpwParseWeJson(jsonText);
2580
2685
  const props = json.general && json.general.properties;
2581
2686
  const list = [];
2582
2687
  if (props) {
@@ -2601,7 +2706,8 @@ function mpwThumbFail(el, why) {
2601
2706
  }
2602
2707
  return { title: json.title || "", properties: list };
2603
2708
  } catch (e) {
2604
- return null;
2709
+ // 原来就吞(bad project.json ⇒ 无标题/无属性);现在记账:计数 + 一行诊断(不静默)
2710
+ return mpwWeJsonSwallowed("extractProjectInfo:project.json", e);
2605
2711
  }
2606
2712
  }
2607
2713
  /** ③(重做) 异步版:project.json 按需读取(424MB 大包不整体进内存)。 */
@@ -5186,7 +5292,13 @@ function mpwThumbFail(el, why) {
5186
5292
  const dirBase = HOST_BASE + (info.src === "library"
5187
5293
  ? "/library-web/" + encodeURIComponent(info.id) + "/"
5188
5294
  : "/custom-folder/" + encodeURIComponent(info.id) + "/");
5189
- fetch(dirBase + "project.json").then((r) => (r.ok ? r.json() : null)).then((pj) => {
5295
+ // ①(P-177 同口径) 网页壁纸的包内 project.json:**先读文本再宽容解析**(原来是 r.json(),
5296
+ // 官方带尾逗号的 project.json 会在这里被静默丢掉 ⇒ general.properties 一个都推不下去)。
5297
+ // 原来的 `.catch(() => {})` 语义不变:解析失败仍吞,但走 mpwWeJsonSwallowed 记账。
5298
+ fetch(dirBase + "project.json")
5299
+ .then((r) => (r.ok ? r.text() : null))
5300
+ .then((txt) => { try { return txt ? mpwParseWeJson(txt) : null } catch (e) { return mpwWeJsonSwallowed("webShimPushProps:project.json", e) } })
5301
+ .then((pj) => {
5190
5302
  const wire = mpwWebPropsToWire(pj);
5191
5303
  try {
5192
5304
  const edits = (readSection().propEdits || {})[info.key] || null;
@@ -17219,7 +17331,12 @@ body[data-mpw-modal] [role="dialog"] {
17219
17331
  if (section.converted !== "web" || !section.mpkgKey || section.mpkgKey.indexOf("custom|") !== 0) return;
17220
17332
  const name = section.mpkgKey.slice("custom|".length);
17221
17333
  const url = HOST_BASE + "/custom-folder/" + encodeURIComponent(name) + "/loadJson.json";
17222
- fetch(url).then((r) => (r.ok ? r.json() : null)).then((d) => {
17334
+ // ①(P-177 同口径) 包内 loadJson.json(L2D 作者的 SettingModel):先读文本再宽容解析;
17335
+ // 原来的 `.catch(() => setWebCfg(null))` 语义不变,但解析失败要记账(不静默)。
17336
+ fetch(url)
17337
+ .then((r) => (r.ok ? r.text() : null))
17338
+ .then((txt) => { try { return txt ? mpwParseWeJson(txt) : null } catch (e) { return mpwWeJsonSwallowed("webCfg:loadJson.json", e) } })
17339
+ .then((d) => {
17223
17340
  if (dead || !d || !d.SettingModel) { setWebCfg(null); return; }
17224
17341
  const langs = d.AnimationsTexts ? Object.keys(d.AnimationsTexts) : [];
17225
17342
  // ①(修正) 缓存完整 loadJson(含 EventInfos/AnimationsTexts),
@@ -17938,7 +18055,12 @@ body[data-mpw-modal] [role="dialog"] {
17938
18055
  let projJson = null;
17939
18056
  try {
17940
18057
  const pj = await fetch(HOST_BASE + "/custom-folder/" + encodeURIComponent(w.name) + "/project.json");
17941
- if (pj.ok) projJson = await pj.json();
18058
+ // ①(P-177 同口径) 包内 project.json:**先读文本再宽容解析**(官方允许尾逗号;
18059
+ // 原来是 pj.json() ⇒ 带尾逗号的官方 project.json 在这里被静默丢掉、时段全走默认)。
18060
+ if (pj.ok) {
18061
+ const pjTxt = await pj.text();
18062
+ try { projJson = mpwParseWeJson(pjTxt) } catch (e) { mpwWeJsonSwallowed("customFolder:project.json(projJson)", e) }
18063
+ }
17942
18064
  } catch {}
17943
18065
  const ok = await handleVideoTexes({ entries: d.entries }, vtex, { name: w.title || w.name, size: 0 }, t, showError, readEntry, projJson, setHint, { kind: "host", token: d.token });
17944
18066
  if (ok === "stale") { try { console.warn("[dsh-mpkg-wallpaper] 时段导入已过期(壁纸已切换),跳过后续链"); } catch {}; return; }
@@ -18421,10 +18543,11 @@ body[data-mpw-modal] [role="dialog"] {
18421
18543
  let cfg = { enabled: true, morning: 4, day: 9, dusk: 17, night: 20 };
18422
18544
  if (projIdx >= 0) {
18423
18545
  try {
18424
- const pj = JSON.parse(new TextDecoder().decode(await readFull(projIdx)));
18546
+ // ①(P-177 同口径) 宿主 /media 供来的包内 project.json 也走宽容解析
18547
+ const pj = mpwParseWeJson(new TextDecoder().decode(await readFull(projIdx)));
18425
18548
  const pm = (pj.general && pj.general.properties) || null;
18426
18549
  if (pm) cfg = timeConfigFromProps(pm, {});
18427
- } catch {}
18550
+ } catch (e) { mpwWeJsonSwallowed("hostMedia:project.json(timeConfig)", e) } // 原来就吞;现在记账
18428
18551
  }
18429
18552
  const slot = slotForTime(cfg, new Date());
18430
18553
  const active = vtex.find((v) => slotFromName(v.name) === slot) || vtex[0];
@@ -20625,6 +20748,8 @@ body[data-mpw-modal] [role="dialog"] {
20625
20748
  "diagflag.align.d": "?align=0:复现旧对齐(origin 恒几何中心),用于对齐语义 A/B。",
20626
20749
  "diagflag.parallax": "视差旧公式",
20627
20750
  "diagflag.parallax.d": "?parallax=legacy:鼠标视差退回旧 lwe 近似式;默认官方公式。",
20751
+ "diagflag.gyro": "移动端姿态源",
20752
+ "diagflag.gyro.d": "?gyro=1 强制启用 / ?gyro=0 完全不监听(一个监听都不挂)/ 缺省 auto(有细指针不启用、触摸设备启用):deviceorientation 的 beta/gamma 按与鼠标同一套 u,v 喂进指针源 ⇒ 视差 g_ParallaxPosition、粒子、轨迹、脚本指针全链生效。",
20628
20753
  "diagflag.audio": "开启声音",
20629
20754
  "diagflag.audio.d": "?audio=1:播放场景 sound 层(每层一条流;浏览器策略可能需要先点一下页面)。",
20630
20755
  "diagflag.whitefallback": "缺纹理回退",
@@ -21432,6 +21557,8 @@ body[data-mpw-modal] [role="dialog"] {
21432
21557
  "diagflag.align.d": "?align=0: legacy alignment (origin always geometric center) for A/B.",
21433
21558
  "diagflag.parallax": "Legacy parallax",
21434
21559
  "diagflag.parallax.d": "?parallax=legacy: old lwe-approx mouse parallax; the official formula is default.",
21560
+ "diagflag.gyro": "Device-motion pointer source",
21561
+ "diagflag.gyro.d": "?gyro=1 force on / ?gyro=0 no deviceorientation listener at all / default auto (off when a fine pointer exists, on for touch devices): beta/gamma feed the same u,v pointer source as the mouse, so parallax g_ParallaxPosition, particles, trails and script pointers all follow.",
21435
21562
  "diagflag.audio": "Enable audio",
21436
21563
  "diagflag.audio.d": "?audio=1: play scene sound layers (one stream per layer; may need a user gesture).",
21437
21564
  "diagflag.whitefallback": "Missing-texture fallback",
@@ -52,6 +52,108 @@ import { readFileSync, readdirSync, statSync, openSync, readSync, closeSync, exi
52
52
  import { join, resolve, sep } from 'node:path';
53
53
  import { deflateSync, inflateSync } from 'node:zlib';
54
54
 
55
+ /* ═══ MPW-WEJSON-BEGIN ═══ */
56
+ /* 官方随包 JSON 的**宽容解析**(WE 允许尾逗号;`JSON.parse` 不允许)——**宿主侧**实现。
57
+ *
58
+ * ── 为什么必须有(官方证据,2026-09-24 官方产物盘点;与渲染器仓同一轮定性)──────────────
59
+ * · 官方随包发布的 `wallpaper_engine/assets/effects/fluidsimulation/effect.json`(10,224 B)
60
+ * **自己就带尾逗号**:第 402 行 `"shaders/effects/fluidsimulation_normal.vert",` 后面紧跟 `}`
61
+ * ⇒ 标准 `JSON.parse` 报 `Expecting value: line 403 column 2`。WE 照发照用 ⇒ **官方容忍尾逗号**
62
+ * (引擎用 jsoncpp,二进制里带 `allowTrailingCommas`/`allowComments` 开关名)。
63
+ * · 宿主侧同样会**静默丢整份文件**:`scene.json`(音频 sound 层引用 / 主图提取 / 图层清单)、
64
+ * `model.json`、`material.json`(`access.readJson`)解析失败就是 `null` —— 少一层、少一条音轨、
65
+ * 回退预览图,日志一行都没有。
66
+ * · 渲染器仓的对应修复编号 **P-177**(`core/we-scene-bundle.js` 的 `export function parseWeJson`
67
+ * + `weJsonStats()`);插件**客户端**侧同一实现是 `lib/client.js` 的 `mpwParseWeJson`
68
+ * (`/* ═══ MPW-WEJSON-BEGIN ═══ *\/` 块)。两处是**同一语义的独立实现**(渲染器仓 GPL-3.0-or-later、
69
+ * 本仓 MIT ⇒ 只做行为对照,不搬运代码),且 `tools/we-json-tolerance-test.mjs` 把两处切片后
70
+ * 逐夹具对拍(解析值 + 计数增量全等)⇒ 防漂移。
71
+ *
72
+ * ── 口径(只做"官方明确容忍"的那两件事,其余照旧抛)──────────────────────────────────
73
+ * ① 尾逗号:`,` 后面(跳过空白)紧跟 `}` / `]` ⇒ 丢掉这个逗号;**字符串内的逗号一律不动**。
74
+ * ② 注释:**只在**"去掉尾逗号后仍解析失败"时才尝试剥 `//` 与 `/* *\/`(同样字符串内不动),单独计数。
75
+ * ③ BOM(U+FEFF)与官方一致地吃掉。
76
+ * **健康文件逐位等价于 `JSON.parse`**:先走 `JSON.parse(text)`,成功原样返回(plain +1);
77
+ * 真坏 JSON **仍然抛** ⇒ 调用点的 try/catch 语义与读量(如 audio-scan 规格的 sceneJsonRead 预算)不变,
78
+ * 只是失败的那几处现在记账(计数 + 一行 console.warn,见 `mpwWeJsonSwallowed`),不再静默。
79
+ * 计数:`mpwWeJsonStats()` 返回 `{ calls, plain, trailingComma, comments, failed, swallowed }`
80
+ * —— 前五项与渲染器 `weJsonStats()` / 客户端 `mpwWeJsonStats()` 同名同义。 */
81
+ const MPW_WEJSON = { calls: 0, plain: 0, trailingComma: 0, comments: 0, failed: 0, swallowed: 0 };
82
+ const MPW_WEJSON_SEEN = new Map(); // where|msg → 次数(诊断去重:同一个坏文件只吵一行)
83
+ /** 只读快照(对外只给这个,避免调用方改计数)。 */
84
+ function mpwWeJsonStats() { return Object.assign({}, MPW_WEJSON) }
85
+ /** 文本整形:一趟扫描,按需去掉**字符串外**的尾逗号(`withComments=false`)或尾逗号 + 注释
86
+ * (`withComments=true`)。**字符串里的一律照抄**(`"a,}"` / `"//"` / `"/*"` 一个字都不动);
87
+ * 注释替换成等长空白,行号与列号都保持可读;尾逗号判定会**越过空白与注释**看下一个有效字符
88
+ * (所以 `{"a":1, /* c *\/ }` 与不带注释的写法同解)。与客户端 `lib/client.js` 的同名函数同语义。 */
89
+ function mpwWeJsonRepair(src, withComments) {
90
+ let out = '', inStr = false, esc = false;
91
+ for (let i = 0; i < src.length; i++) {
92
+ const c = src[i], d = src[i + 1];
93
+ if (inStr) { // 字符串内:只跟 escape 状态,内容逐字照抄
94
+ out += c;
95
+ if (esc) esc = false;
96
+ else if (c === '\\') esc = true;
97
+ else if (c === '"') inStr = false;
98
+ continue;
99
+ }
100
+ if (c === '"') { inStr = true; out += c; continue }
101
+ if (withComments && c === '/' && d === '/') { // `//` 行注释 ⇒ 空白
102
+ while (i < src.length && src[i] !== '\n') { out += ' '; i++ }
103
+ out += '\n';
104
+ continue;
105
+ }
106
+ if (withComments && c === '/' && d === '*') { // `/* */` 块注释 ⇒ 空白
107
+ out += ' '; i += 2;
108
+ while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) { out += (src[i] === '\n' ? '\n' : ' '); i++ }
109
+ out += ' '; i++;
110
+ continue;
111
+ }
112
+ if (c === ',') { // 尾逗号:往后看下一个有效字符(可跨空白/注释)
113
+ let j = i + 1;
114
+ for (;;) {
115
+ while (j < src.length && (src[j] === ' ' || src[j] === '\t' || src[j] === '\n' || src[j] === '\r')) j++;
116
+ if (!withComments) break;
117
+ if (src[j] === '/' && src[j + 1] === '/') { while (j < src.length && src[j] !== '\n') j++; continue }
118
+ if (src[j] === '/' && src[j + 1] === '*') { j += 2; while (j < src.length && !(src[j] === '*' && src[j + 1] === '/')) j++; j += 2; continue }
119
+ break;
120
+ }
121
+ if (src[j] === '}' || src[j] === ']') continue; // 尾逗号 ⇒ 丢掉这个逗号
122
+ }
123
+ out += c;
124
+ }
125
+ return out;
126
+ }
127
+ /**
128
+ * 解析官方随包 JSON(scene.json / project.json / materials / effects / models / combo 定义…)。
129
+ * 与 `JSON.parse` 的差别只有两条:容忍尾逗号;再不行才容忍注释(外加吃掉 BOM)。
130
+ * **其余一律照旧抛**(调用方的 try/catch 语义不变)。
131
+ * @param {string} text
132
+ * @returns {any}
133
+ */
134
+ function mpwParseWeJson(text) {
135
+ MPW_WEJSON.calls++;
136
+ const s0 = (typeof text === 'string') ? text : String(text == null ? '' : text);
137
+ const s = s0.charCodeAt(0) === 0xFEFF ? s0.slice(1) : s0; // ① BOM:官方产物会带,JSON.parse 自己不认
138
+ try { const v = JSON.parse(s); MPW_WEJSON.plain++; return v } catch (e) { /* 健康文件到此为止(逐位等价 JSON.parse) */ }
139
+ try { const v = JSON.parse(mpwWeJsonRepair(s, false)); MPW_WEJSON.trailingComma++; return v } catch (e) { /* ② 尾逗号也不够 ⇒ 再试注释 */ }
140
+ try { const v = JSON.parse(mpwWeJsonRepair(s, true)); MPW_WEJSON.comments++; return v } // ③ 尾逗号 + 注释
141
+ catch (e) { MPW_WEJSON.failed++; throw e } // 真坏 JSON:照旧抛
142
+ }
143
+ /** 包内 JSON 读取/解析失败、调用方**按原有语义吞掉**时的记账:计数 + 一行 console.warn(不静默)。
144
+ * (调用点的 try/catch 结构不动;返回值恒 null 方便 `catch { x = ... }` 写法。同 where+msg 只吵一次。) */
145
+ function mpwWeJsonSwallowed(where, e) {
146
+ try { MPW_WEJSON.swallowed++ } catch (e2) {}
147
+ try {
148
+ const key = where + '|' + String((e && e.message) || e);
149
+ const n = (MPW_WEJSON_SEEN.get(key) || 0) + 1;
150
+ MPW_WEJSON_SEEN.set(key, n);
151
+ if (n === 1) console.warn('[dsh-mpkg-wallpaper] 包内 JSON 读取/解析失败(按原语义吞掉): ' + where + ' — ' + String((e && e.message) || e));
152
+ } catch (e2) {}
153
+ return null;
154
+ }
155
+ /* ═══ MPW-WEJSON-END ═══ */
156
+
55
157
  /** Wallpaper Engine texture format ids (TEXI0001 header), per RePKG/lwe. */
56
158
  const TexFormat = {
57
159
  RGBA8888: 0,
@@ -587,8 +689,9 @@ function enumerateAudioTracks(src) {
587
689
  if (sjEntry) {
588
690
  try {
589
691
  const bytes = readEntry(sjEntry);
590
- if (bytes && bytes.length) { sceneJson = JSON.parse(textDecoder.decode(bytes)); sceneJsonRead = true; }
591
- } catch { sceneJson = null; }
692
+ // ①(P-177 同口径) 包内 scene.json 走宽容解析(官方允许尾逗号);原来解析失败就 null(静默丢 refs)
693
+ if (bytes && bytes.length) { sceneJson = mpwParseWeJson(textDecoder.decode(bytes)); sceneJsonRead = true; }
694
+ } catch (e) { sceneJson = mpwWeJsonSwallowed('enumerateAudioTracks:scene.json(容器内)', e); }
592
695
  }
593
696
  }
594
697
  for (const { layer, path } of collectSoundLayerRefs(sceneJson)) note(path, layer, 0);
@@ -633,8 +736,9 @@ function collectDirAudioTracks(dir, opts = {}) {
633
736
  let sceneJson = null;
634
737
  try {
635
738
  const sj = join(dir, 'scene.json');
636
- if (existsSync(sj)) sceneJson = JSON.parse(readFileSync(sj, 'utf8'));
637
- } catch { sceneJson = null; }
739
+ // ①(P-177 同口径) 松散场景目录的 scene.json 也走宽容解析(原来解析失败静默 null)
740
+ if (existsSync(sj)) sceneJson = mpwParseWeJson(readFileSync(sj, 'utf8'));
741
+ } catch (e) { sceneJson = mpwWeJsonSwallowed('collectDirAudioTracks:' + join(dir, 'scene.json'), e); }
638
742
  const readHead = (entry, n) => {
639
743
  try {
640
744
  const len = Math.max(0, Math.min(n, entry.size));
@@ -1351,9 +1455,10 @@ function pkgSceneAccess(pkgData) {
1351
1455
  const file = readFile(path);
1352
1456
  if (!file) return null;
1353
1457
  try {
1354
- return JSON.parse(textDecoder.decode(file.bytes));
1355
- } catch {
1356
- return null;
1458
+ // ①(P-177 同口径) 容器内 scene.json / model.json / material.json 走宽容解析(官方允许尾逗号)
1459
+ return mpwParseWeJson(textDecoder.decode(file.bytes));
1460
+ } catch (e) {
1461
+ return mpwWeJsonSwallowed('pkgSceneAccess.readJson:' + path, e); // 原来就吞;现在记账
1357
1462
  }
1358
1463
  },
1359
1464
  readFile,
@@ -1459,9 +1564,10 @@ function dirSceneAccess(dir) {
1459
1564
  const file = readFile(path);
1460
1565
  if (!file) return null;
1461
1566
  try {
1462
- return JSON.parse(textDecoder.decode(file.bytes));
1463
- } catch {
1464
- return null;
1567
+ // ①(P-177 同口径) 松散场景目录里的 scene.json / model.json / material.json 走宽容解析
1568
+ return mpwParseWeJson(textDecoder.decode(file.bytes));
1569
+ } catch (e) {
1570
+ return mpwWeJsonSwallowed('dirSceneAccess.readJson:' + path, e); // 原来就吞;现在记账
1465
1571
  }
1466
1572
  },
1467
1573
  readFile,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mpkg-wallpaper",
3
- "version": "3.13.3",
3
+ "version": "3.13.4",
4
4
  "description": "DSH Web 壁纸引擎 Wallpaper Engine mpkg 背景插件:浏览器内直接解析 .mpkg(preview.gif 动态背景/内嵌 mp4 视频/多时段切换),整屏统一虚化/对话框/弹层/遮罩独立虚化、镜头缩放平移、时钟、冲突检测。",
5
5
  "private": false,
6
6
  "type": "module",