dsh-mpkg-wallpaper 3.13.3 → 3.13.5

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",
package/lib/index.js CHANGED
@@ -16,7 +16,7 @@ import { execFileSync, spawn } from 'node:child_process';
16
16
  // ①(2026-09-21 音频格式补全) 后三个导出是**唯一的**扩展名→MIME 表与嗅探入口,见 docs/AUDIO-TRACK-SPEC.md §1.2/§3:
17
17
  // 路由侧原先各自内联一份扩展名三元链 + 一份魔数嗅探,已与渲染器支持的后缀漂移(.oga/.m4a/.aac 缺失、ftyp 偏移写错)。
18
18
  // ⚠ 这条 import 必须**保持单行**:tools/bundle-equivalence-test.mjs 的具名导入解析器按行读(注释进括号会解析失败)。
19
- import { extractSceneMainImage, extractSceneMainImageFromDir, extractSceneManifest, extractSceneLayer, extractTexVideoMp4, collectSceneVideoFiles, parsePkg, readPkgEntry, scanSceneAudio, scanSceneVideo, clearSceneVideoScanCache, suffixAudioMime, audioMimeFor, AUDIO_SUFFIX_MIME } from './pkg-extract.js';
19
+ import { extractSceneMainImage, extractSceneMainImageFromDir, extractSceneManifest, extractSceneLayer, extractTexVideoMp4, collectSceneVideoFiles, parsePkg, readPkgEntry, scanSceneAudio, scanSceneVideo, clearSceneVideoScanCache, suffixAudioMime, audioMimeFor, AUDIO_SUFFIX_MIME, mpwParseWeJson, mpwWeJsonSwallowed } from './pkg-extract.js';
20
20
  // ①(新 2026-09-16 I 项) 网页(web)类壁纸:内容优先的类型判定 + WE API shim 注入 + 跨源策略
21
21
  // (MIT,本仓库自写;API 名单参考 oneincase/webwallgl,未复制其代码,见 ../docs/COPYING-RULES.md §4)
22
22
  // ①(WP-1 2026-09-19) 追加:帧内存储 facade 的持久化路由(/web-store)、宿主媒体音频控制
@@ -3830,7 +3830,18 @@ function apply(ctx) {
3830
3830
  const proj = join(p, 'project.json');
3831
3831
  if (!existsSync(proj)) continue;
3832
3832
  try {
3833
- const meta = JSON.parse(readFileSync(proj, 'utf8'));
3833
+ /* ①(2026-09-24 包内/包旁 JSON 收口线) **包旁** JSON:Steam 库里 `<壁纸目录>/project.json`
3834
+ (与 scene.pkg 同级,官方工坊布局)⇒ 走**宿主侧同一份**宽容解析(尾逗号/注释/BOM;
3835
+ 官方 `assets/effects/fluidsimulation/effect.json` 第 402 行自己就带尾逗号)。
3836
+ 语义与改动前一致:读不到/真坏 JSON ⇒ 仍然抛(下面的既有 catch 照旧跳过该壁纸并 console.warn)。
3837
+ 多出来的是**记账**:解析器计数(`mpwWeJsonStats().failed`)+ 下面 catch 里的
3838
+ `mpwWeJsonSwallowed`(计数 + 一行诊断;同 where+原因只吵一次)—— 原来只有一句 warn,
3839
+ "坏在哪一步/坏了几次"看不出来。**注意只在解析这一段**调用记账:`readdirSync`/探测
3840
+ 失败不该被记成"JSON 坏"(归因不能糊)。 */
3841
+ const meta = (() => {
3842
+ try { return mpwParseWeJson(readFileSync(proj, 'utf8')); }
3843
+ catch (e) { mpwWeJsonSwallowed('[steam-inventory] ' + proj, e); throw e; }
3844
+ })();
3834
3845
  const type = (meta.type || 'scene').toLowerCase();
3835
3846
  // ④(安全) 排除 application / exe 壁纸:这类已弃用的 .exe 格式可能被注入病毒,
3836
3847
  // 本插件绝不读取/执行任何 .exe。scene 只给预览图(无法渲染)。
@@ -52,6 +52,116 @@ 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
+
157
+ /* ①(2026-09-24 包内/包旁 JSON 收口线) **宿主侧复用出口**:`lib/web-wallpaper.js`(WE 目录的 `project.json`)
158
+ 与 `lib/index.js`(Steam 库壁纸目录的 `project.json`)都要用**同一份**宽容解析与记账 —— 导出面只加名字,
159
+ 解析逻辑、计数口径、`swallowed` 语义**一字未动**("包内 JSON 解析点都要记账"这条纪律由
160
+ tools/we-json-tolerance-test.mjs 的静态反查看住)。客户端侧 `lib/client.js` 的同名块是**浏览器产物**的
161
+ 内联副本(页面没有模块图),两边一致性由同一条门禁的切片对拍看着 —— 所以这里不是"第三份实现",
162
+ 而是把宿主这一份**接出来给宿主模块共用**。 */
163
+ export { mpwParseWeJson, mpwWeJsonStats, mpwWeJsonSwallowed };
164
+
55
165
  /** Wallpaper Engine texture format ids (TEXI0001 header), per RePKG/lwe. */
56
166
  const TexFormat = {
57
167
  RGBA8888: 0,
@@ -587,8 +697,9 @@ function enumerateAudioTracks(src) {
587
697
  if (sjEntry) {
588
698
  try {
589
699
  const bytes = readEntry(sjEntry);
590
- if (bytes && bytes.length) { sceneJson = JSON.parse(textDecoder.decode(bytes)); sceneJsonRead = true; }
591
- } catch { sceneJson = null; }
700
+ // ①(P-177 同口径) 包内 scene.json 走宽容解析(官方允许尾逗号);原来解析失败就 null(静默丢 refs)
701
+ if (bytes && bytes.length) { sceneJson = mpwParseWeJson(textDecoder.decode(bytes)); sceneJsonRead = true; }
702
+ } catch (e) { sceneJson = mpwWeJsonSwallowed('enumerateAudioTracks:scene.json(容器内)', e); }
592
703
  }
593
704
  }
594
705
  for (const { layer, path } of collectSoundLayerRefs(sceneJson)) note(path, layer, 0);
@@ -633,8 +744,9 @@ function collectDirAudioTracks(dir, opts = {}) {
633
744
  let sceneJson = null;
634
745
  try {
635
746
  const sj = join(dir, 'scene.json');
636
- if (existsSync(sj)) sceneJson = JSON.parse(readFileSync(sj, 'utf8'));
637
- } catch { sceneJson = null; }
747
+ // ①(P-177 同口径) 松散场景目录的 scene.json 也走宽容解析(原来解析失败静默 null)
748
+ if (existsSync(sj)) sceneJson = mpwParseWeJson(readFileSync(sj, 'utf8'));
749
+ } catch (e) { sceneJson = mpwWeJsonSwallowed('collectDirAudioTracks:' + join(dir, 'scene.json'), e); }
638
750
  const readHead = (entry, n) => {
639
751
  try {
640
752
  const len = Math.max(0, Math.min(n, entry.size));
@@ -1351,9 +1463,10 @@ function pkgSceneAccess(pkgData) {
1351
1463
  const file = readFile(path);
1352
1464
  if (!file) return null;
1353
1465
  try {
1354
- return JSON.parse(textDecoder.decode(file.bytes));
1355
- } catch {
1356
- return null;
1466
+ // ①(P-177 同口径) 容器内 scene.json / model.json / material.json 走宽容解析(官方允许尾逗号)
1467
+ return mpwParseWeJson(textDecoder.decode(file.bytes));
1468
+ } catch (e) {
1469
+ return mpwWeJsonSwallowed('pkgSceneAccess.readJson:' + path, e); // 原来就吞;现在记账
1357
1470
  }
1358
1471
  },
1359
1472
  readFile,
@@ -1459,9 +1572,10 @@ function dirSceneAccess(dir) {
1459
1572
  const file = readFile(path);
1460
1573
  if (!file) return null;
1461
1574
  try {
1462
- return JSON.parse(textDecoder.decode(file.bytes));
1463
- } catch {
1464
- return null;
1575
+ // ①(P-177 同口径) 松散场景目录里的 scene.json / model.json / material.json 走宽容解析
1576
+ return mpwParseWeJson(textDecoder.decode(file.bytes));
1577
+ } catch (e) {
1578
+ return mpwWeJsonSwallowed('dirSceneAccess.readJson:' + path, e); // 原来就吞;现在记账
1465
1579
  }
1466
1580
  },
1467
1581
  readFile,
@@ -31,6 +31,13 @@ import { createHash } from 'node:crypto';
31
31
  // 为什么要 import 而不是复制一份:触摸的协议形状(`op:'touch'` 的字段表)与父页整形体同在
32
32
  // lib/web-interaction.js,**唯一源**不能有两份(tools/web-interaction-test.mjs 会断言两侧一致)。
33
33
  import { WEB_TOUCH_FRAME_SOURCE } from './web-interaction.js';
34
+ // ①(2026-09-24 包内/包旁 JSON 收口线) 官方随包 JSON 的**宽容解析**:**复用宿主侧那一份**
35
+ // (`lib/pkg-extract.js` 的 MPW-WEJSON 块,P-177 同口径:先 JSON.parse → 字符串外尾逗号 → 再不行剥注释;
36
+ // BOM 吃掉;真坏 JSON 仍然抛)。为什么必须共享而不是在本文件再写一遍:本仓已有两份同语义实现
37
+ // (`lib/client.js` 浏览器内联副本 + `lib/pkg-extract.js` 宿主侧),tools/we-json-tolerance-test.mjs
38
+ // 把两处切片逐夹具对拍防漂移 —— 再加第三份就是三处漂移面。
39
+ // ⚠ 本模块是**宿主侧** ESM(Node),与 `lib/index.js` 同一个模块图(bundle 的模块表因此不变)。
40
+ import { mpwParseWeJson, mpwWeJsonSwallowed } from './pkg-extract.js';
34
41
 
35
42
  /** 类型判定结果(四态 + 排除态) */
36
43
  export const WEB_KIND = { WEB: 'web', SCENE: 'scene', VIDEO: 'video', UNKNOWN: 'unknown' };
@@ -316,15 +323,27 @@ export function listWallpaperFiles(root, opts = {}) {
316
323
  return out;
317
324
  }
318
325
 
319
- /** 读并解析目录里的 project.json(缺失/损坏 → null;损坏不抛,交给启发式兜底) */
326
+ /** 读并解析目录里的 project.json(缺失 → null;损坏 → 记账后 null,交给启发式兜底)。
327
+ * ①(2026-09-24 包内/包旁 JSON 收口线) 读的是 **WE 目录的 `project.json`**(**包旁**:官方工坊布局里与
328
+ * `scene.pkg` 同级;本模块的 `detectWallpaperDir` → `detectWebWallpaperKind` 用它做**内容优先**判定,
329
+ * `general.file` 是网页壁纸入口的第一顺位证据)⇒ 必须容忍官方允许的尾逗号/注释:官方随包发布的
330
+ * `assets/effects/fluidsimulation/effect.json` 第 402 行自己就带尾逗号(WE 用 jsoncpp 照用),
331
+ * 严格 `JSON.parse` 会把这份**包旁属性表/入口声明**整份丢掉 ⇒ 类型判定只剩文件名启发式、属性推不下去,
332
+ * 而且**日志一行都没有**(改前的 `catch { return null }` 就是这类静默)。
333
+ * 语义保持"读不到就是 null"(缺失 ⇒ null / 真坏 JSON ⇒ 记账后 null,照旧不抛 —— 调用方按启发式兜底);
334
+ * `mpwWeJsonSwallowed` 给计数 + 一行诊断(同 where+原因只吵一次)。 */
320
335
  export function readProjectJson(dir) {
336
+ const p = join(dir, 'project.json');
337
+ let text = null;
321
338
  try {
322
- const p = join(dir, 'project.json');
323
339
  if (!statSync(p).isFile()) return null;
324
- const obj = JSON.parse(readFileSync(p, 'utf8'));
340
+ text = readFileSync(p, 'utf8');
341
+ } catch { return null; } // 缺失/损坏的路径/IO 读不到 ⇒ null(原语义;**不是** JSON 问题,不记账成 JSON 坏)
342
+ try {
343
+ const obj = mpwParseWeJson(text);
325
344
  return obj && typeof obj === 'object' ? obj : null;
326
- } catch {
327
- return null;
345
+ } catch (e) {
346
+ return mpwWeJsonSwallowed('web-wallpaper:readProjectJson(' + p + ')', e);
328
347
  }
329
348
  }
330
349
 
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.5",
4
4
  "description": "DSH Web 壁纸引擎 Wallpaper Engine mpkg 背景插件:浏览器内直接解析 .mpkg(preview.gif 动态背景/内嵌 mp4 视频/多时段切换),整屏统一虚化/对话框/弹层/遮罩独立虚化、镜头缩放平移、时钟、冲突检测。",
5
5
  "private": false,
6
6
  "type": "module",