dsh-mpkg-wallpaper 3.13.2 → 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 +232 -10
- package/lib/pkg-extract.js +116 -10
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -80,6 +80,198 @@ function mpwTrace(ev, data) {
|
|
|
80
80
|
return Math.max(1, Math.min(120, Number.isFinite(n) && n > 0 ? n : 5));
|
|
81
81
|
}
|
|
82
82
|
try { window.__mpwClampRotMin = mpwClampRotMin; } catch {}
|
|
83
|
+
/* ═══ MPW-BLOBRETRY-BEGIN ═══ */
|
|
84
|
+
/** blob 媒体「有界重试看门狗」(**唯一定义处**;上游 Firefox Bug 2056444 的页内兜底)。
|
|
85
|
+
* 症状(已定性,有实测证据):把**包内字节**做成 `blob:` URL 再喂 `<video>`/`<audio>`,在受影响的
|
|
86
|
+
* Firefox 构建上会**静默挂住** —— `readyState` 恒 0、`networkState` 恒 1、**不触发 error**、
|
|
87
|
+
* `play()` 的 Promise 永不 settle。
|
|
88
|
+
* 上游回归:Bug 2056444(由 Bug 2005247 引入,Firefox 152 起;官方已修:155 nightly → beta 154 →
|
|
89
|
+
* release 153(153.0.3)/ ESR153)。机制:blob 数据超过 **1MiB 的 IPC 内联上限**后改走 IPC 异步流,
|
|
90
|
+
* `CloneableWithRangeMediaResource` 在异步 IPC 流上做同步读 ⇒ 首次元数据读(MP4 的 `moov` 常在
|
|
91
|
+
* **尾部** = 远端 seek)挂住;`decodeAudioData` 同族也会挂。
|
|
92
|
+
* 证据(**仓外**姐妹仓库「渲染器仓」的两条探针,只读引用、**勿改**;本仓门禁禁止在 lib/** 里写跨仓路径):
|
|
93
|
+
* · `tests/mpkg-video-decode-probe.mjs` —— 同一段 22MB h264 字节:HTTP 直供 `rs=4`(约 0.4s);
|
|
94
|
+
* blob 恒 `rs=0`;14KB blob `rs=4`(⇒ 与 1MiB 上限相关);`new Blob([buf])`、`new File([buf])`、
|
|
95
|
+
* `new Response(buf).blob()`、`data:` URL 四种写法全部复现。
|
|
96
|
+
* · `tests/mpkg-videobase-recovery-probe.mjs` —— 真页面里元素卡在 `rs=0` 后,**只补一次 `load()`**
|
|
97
|
+
* 就恢复(`rs=4 / 1636x1156 / dur=30`,并且画面真的出来了)。
|
|
98
|
+
* ⇒ 页内可用的兜底 = **有界重试 `load()`**:最多 2 次、间隔 1500ms。
|
|
99
|
+
* **健康浏览器上永不触发**:`rs≥1` 早在 1.5s 内到达 ⇒ 首次检查就返回,一次 `load()` 都不会多调。
|
|
100
|
+
* 契约(`tools/blob-media-retry-test.mjs` S2 逐条钉住):①有界(2 次 / 1500ms);②`rs≥1` 或
|
|
101
|
+
* `el.error` 非空 ⇒ 立刻停(不再重试、不再计时);③每次重试**恰好一行**日志(`🔁` + label + 第几次
|
|
102
|
+
* + 缺陷号 2056444);④2 次用尽仍 `rs=0` ⇒ **恰好一行** `⚠`;⑤绝不抛异常(元素已摘 / `load()` 抛错 /
|
|
103
|
+
* 无 `window` 一律吞掉);⑥只兜 `blob:` 源(HTTP 直供是健康路径 —— 探针里 `rs=4` 的就是它 —— 不打扰)。
|
|
104
|
+
* 计数钩子 `window.__mpwBlobMediaRetry`(**仅当 `window` 存在时**写;语义如下):
|
|
105
|
+
* `calls` = 本函数被调用的次数(每处接线各算一次,**不代表真的重试过**);
|
|
106
|
+
* `retries` = 真的调了 `el.load()` 的次数;
|
|
107
|
+
* `gaveUp` = 2 次用尽仍 `rs=0` 的收尾次数;
|
|
108
|
+
* `byLabel` = 同口径按 `label` 分桶:`{ [label]: { calls, retries, gaveUp } }`。
|
|
109
|
+
* 纯诊断、无用户可见提示:恢复是自动的,正常用户不该被打扰(只在控制台留痕)。 */
|
|
110
|
+
function mpwBlobMediaRetry(el, label) {
|
|
111
|
+
const RETRY_MAX = 2, RETRY_GAP_MS = 1500, BUG = "2056444";
|
|
112
|
+
const tag = String(label == null ? "" : label);
|
|
113
|
+
/** 计数(**唯一**碰 `window` 的地方;任何一步失败都只吞掉,绝不影响兜底主流程)。 */
|
|
114
|
+
const count = (k) => {
|
|
115
|
+
try {
|
|
116
|
+
if (typeof window === "undefined" || !window) return;
|
|
117
|
+
const st = window.__mpwBlobMediaRetry || (window.__mpwBlobMediaRetry = { calls: 0, retries: 0, gaveUp: 0, byLabel: {} });
|
|
118
|
+
const bk = st.byLabel || (st.byLabel = {});
|
|
119
|
+
const b = bk[tag] || (bk[tag] = { calls: 0, retries: 0, gaveUp: 0 });
|
|
120
|
+
st[k] = (st[k] || 0) + 1;
|
|
121
|
+
b[k] = (b[k] || 0) + 1;
|
|
122
|
+
} catch (e) {}
|
|
123
|
+
};
|
|
124
|
+
/** 停的条件:已出元数据(`rs≥1`)或已报错(`error` 非空)。读失败按"停"处理(不拿读不准的状态去戳元素)。 */
|
|
125
|
+
const done = () => { try { return !!(el && (el.readyState >= 1 || el.error)) } catch (e) { return true } };
|
|
126
|
+
/** 只兜 `blob:` 源:先读 `src` 属性(`showVideoEl` 判等用的就是它,避免 `currentSrc` 还没换过来的窗口),
|
|
127
|
+
* 读不到再退回属性值;读不出 / 为空 ⇒ 放行(fail-open:宁可多兜一次,也不漏掉真挂住的那个)。 */
|
|
128
|
+
const blobSrc = () => {
|
|
129
|
+
try {
|
|
130
|
+
let u = "";
|
|
131
|
+
try { if (el && typeof el.getAttribute === "function") u = String(el.getAttribute("src") || "") } catch (e) {}
|
|
132
|
+
if (!u) { try { u = String((el && el.src) || "") } catch (e) {} }
|
|
133
|
+
return !u || /^blob:/i.test(u);
|
|
134
|
+
} catch (e) { return true }
|
|
135
|
+
};
|
|
136
|
+
let tries = 0, timer = null;
|
|
137
|
+
const tick = () => {
|
|
138
|
+
try {
|
|
139
|
+
timer = null;
|
|
140
|
+
if (done()) return; // 已出元数据 / 已报错 ⇒ 停(不再重试、不再计时)
|
|
141
|
+
if (tries >= RETRY_MAX) { // 2 次用尽仍 rs=0 ⇒ 恰好一行 ⚠ 收尾
|
|
142
|
+
count("gaveUp");
|
|
143
|
+
try { console.warn("[dsh-mpkg-wallpaper] ⚠ " + tag + " 始终未出元数据 rs=0,已重试 " + RETRY_MAX + " 次(上游 Firefox Bug " + BUG + ":blob>1MiB 走 IPC 异步流、同步读挂住;健康浏览器上永不触发)"); } catch (e) {}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
tries++; // 只数"真的调了 load()"的次数
|
|
147
|
+
count("retries");
|
|
148
|
+
try { console.warn("[dsh-mpkg-wallpaper] 🔁 " + tag + " rs=0 无元数据 → 第 " + tries + "/" + RETRY_MAX + " 次补 load()(上游 Firefox Bug " + BUG + ";依据渲染器仓探针 tests/mpkg-videobase-recovery-probe.mjs:只补一次 load() 即恢复)"); } catch (e) {}
|
|
149
|
+
try { el.load() } catch (e) {}
|
|
150
|
+
try { const p = el.play(); if (p && typeof p.catch === "function") p.catch(() => {}) } catch (e) {}
|
|
151
|
+
timer = setTimeout(tick, RETRY_GAP_MS);
|
|
152
|
+
} catch (e) {}
|
|
153
|
+
};
|
|
154
|
+
try {
|
|
155
|
+
count("calls");
|
|
156
|
+
if (!el) return; // 元素已摘 / 还没建:什么都不做
|
|
157
|
+
if (!blobSrc()) return; // HTTP/宿主直供:不打扰(探针里 rs=4 的就是这条健康路径)
|
|
158
|
+
if (done()) return; // 健康浏览器:一次 load() 都不调、也不计时
|
|
159
|
+
/* "立刻停"的另一半:真出元数据 / 真报错时把**待发的那一发**撤掉(不产生多余的 load())。
|
|
160
|
+
`{once:true}` + 元素长期存活 ⇒ 不累积;拿不到 addEventListener 就算了(tick 里仍会判)。 */
|
|
161
|
+
try {
|
|
162
|
+
if (typeof el.addEventListener === "function") {
|
|
163
|
+
const stop = () => { try { if (timer != null) clearTimeout(timer) } catch (e) {} timer = null };
|
|
164
|
+
el.addEventListener("loadedmetadata", stop, { once: true });
|
|
165
|
+
el.addEventListener("error", stop, { once: true });
|
|
166
|
+
}
|
|
167
|
+
} catch (e) {}
|
|
168
|
+
timer = setTimeout(tick, RETRY_GAP_MS);
|
|
169
|
+
} catch (e) {}
|
|
170
|
+
}
|
|
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 ═══ */
|
|
83
275
|
// ①(P-104 2026-09-17 用户发布纪律②) **插件侧 localStorage 也要有上限**:
|
|
84
276
|
// 本插件只用固定几个键(STORE_KEY / mpw_settings_backup / mpwdiag / mpwwatch),键数量天然有界;
|
|
85
277
|
// 真正的风险在**单值**——STORE_KEY 里含背景 data URL(本文件注释自述 1MB+)。这里给单值硬顶
|
|
@@ -263,7 +455,7 @@ function mpwTrace(ev, data) {
|
|
|
263
455
|
// ①(MERGED-3 1.3) 诊断开关速查区离线副本(渲染器离线时用)。
|
|
264
456
|
// 在线数据源 = <渲染器>/diag-flags.json(由 we-scene-demo/diag-flag-check.mjs 脚本生成,勿手改);
|
|
265
457
|
// tools/panel-smoke.mjs 断言本副本 == diag-flags.json 的 common 集合 → 两边同步,勿单独改这里。
|
|
266
|
-
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"}]';
|
|
267
459
|
const DEFAULT_THINK_BG = false; // Deep diving 背景方框:默认取消(透明)
|
|
268
460
|
const DEFAULT_ENABLED = true;
|
|
269
461
|
const DEFAULT_CLOCK = false;
|
|
@@ -2254,9 +2446,10 @@ function mpwThumbFail(el, why) {
|
|
|
2254
2446
|
const proj = projJson ? null : mpkg.entries.find((e) => e.name === "project.json");
|
|
2255
2447
|
if (proj) {
|
|
2256
2448
|
try {
|
|
2257
|
-
|
|
2449
|
+
// ①(P-177 同口径) 包内 project.json 走宽容解析(官方允许尾逗号;严格 parse 会整份丢掉)
|
|
2450
|
+
const pj = mpwParseWeJson(new TextDecoder().decode(await readEntry(proj)));
|
|
2258
2451
|
propsMap = (pj.general && pj.general.properties) || null;
|
|
2259
|
-
} catch {}
|
|
2452
|
+
} catch (e) { mpwWeJsonSwallowed("handleVideoTexes:project.json", e) } // 原来就吞;现在计数 + 一行诊断
|
|
2260
2453
|
}
|
|
2261
2454
|
const key2 = file.name + "|" + file.size;
|
|
2262
2455
|
const prevS = readSection();
|
|
@@ -2487,7 +2680,8 @@ function mpwThumbFail(el, why) {
|
|
|
2487
2680
|
/** 解析 project.json 文本,提取标题 + 可调参数信息。 */
|
|
2488
2681
|
function extractProjectInfo(jsonText) {
|
|
2489
2682
|
try {
|
|
2490
|
-
|
|
2683
|
+
// ①(P-177 同口径) 包内 project.json 走宽容解析(官方允许尾逗号);健康文件与 JSON.parse 逐位等价
|
|
2684
|
+
const json = mpwParseWeJson(jsonText);
|
|
2491
2685
|
const props = json.general && json.general.properties;
|
|
2492
2686
|
const list = [];
|
|
2493
2687
|
if (props) {
|
|
@@ -2512,7 +2706,8 @@ function mpwThumbFail(el, why) {
|
|
|
2512
2706
|
}
|
|
2513
2707
|
return { title: json.title || "", properties: list };
|
|
2514
2708
|
} catch (e) {
|
|
2515
|
-
|
|
2709
|
+
// 原来就吞(bad project.json ⇒ 无标题/无属性);现在记账:计数 + 一行诊断(不静默)
|
|
2710
|
+
return mpwWeJsonSwallowed("extractProjectInfo:project.json", e);
|
|
2516
2711
|
}
|
|
2517
2712
|
}
|
|
2518
2713
|
/** ③(重做) 异步版:project.json 按需读取(424MB 大包不整体进内存)。 */
|
|
@@ -5097,7 +5292,13 @@ function mpwThumbFail(el, why) {
|
|
|
5097
5292
|
const dirBase = HOST_BASE + (info.src === "library"
|
|
5098
5293
|
? "/library-web/" + encodeURIComponent(info.id) + "/"
|
|
5099
5294
|
: "/custom-folder/" + encodeURIComponent(info.id) + "/");
|
|
5100
|
-
|
|
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) => {
|
|
5101
5302
|
const wire = mpwWebPropsToWire(pj);
|
|
5102
5303
|
try {
|
|
5103
5304
|
const edits = (readSection().propEdits || {})[info.key] || null;
|
|
@@ -6596,6 +6797,9 @@ function mpwThumbFail(el, why) {
|
|
|
6596
6797
|
// ⑤(新) Edge 兼容:画到 canvas
|
|
6597
6798
|
if (IS_EDGE) { try { showVideoEdge(url, bgElements().video); } catch { showVideoEl(url); } }
|
|
6598
6799
|
else showVideoEl(url);
|
|
6800
|
+
/* ①(2026-09-23 上游 Firefox Bug 2056444) blob: 喂 `<video>` 的**唯一落点**(`showVideoEl` 只在这里
|
|
6801
|
+
拿到新建的 ObjectURL)⇒ 挂有界重试看门狗。健康浏览器 `rs≥1` ⇒ 一次 `load()` 都不会多调。 */
|
|
6802
|
+
mpwBlobMediaRetry(bgElements().video, "视频壁纸");
|
|
6599
6803
|
}).catch(() => {});
|
|
6600
6804
|
} else if (image === "idb:img") {
|
|
6601
6805
|
// ③(新) 大图片 Blob 路径:ObjectURL 显示,不走 dataURL(防内存膨胀)
|
|
@@ -8458,6 +8662,9 @@ function mpwThumbFail(el, why) {
|
|
|
8458
8662
|
npAudio.src = obj;
|
|
8459
8663
|
npBlobUrlSet(obj);
|
|
8460
8664
|
const p = npAudio.play(); if (p && p.catch) p.catch(() => {});
|
|
8665
|
+
/* ①(2026-09-23 上游 Firefox Bug 2056444) 同一族的静默挂起(blob 音轨 rs=0 / 无 error、
|
|
8666
|
+
play() 的 Promise 永不 settle;`decodeAudioData` 亦然)⇒ 同一个有界重试兜底。 */
|
|
8667
|
+
mpwBlobMediaRetry(npAudio, "音轨");
|
|
8461
8668
|
}).catch(() => {});
|
|
8462
8669
|
} catch (e) {}
|
|
8463
8670
|
}
|
|
@@ -17124,7 +17331,12 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
17124
17331
|
if (section.converted !== "web" || !section.mpkgKey || section.mpkgKey.indexOf("custom|") !== 0) return;
|
|
17125
17332
|
const name = section.mpkgKey.slice("custom|".length);
|
|
17126
17333
|
const url = HOST_BASE + "/custom-folder/" + encodeURIComponent(name) + "/loadJson.json";
|
|
17127
|
-
|
|
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) => {
|
|
17128
17340
|
if (dead || !d || !d.SettingModel) { setWebCfg(null); return; }
|
|
17129
17341
|
const langs = d.AnimationsTexts ? Object.keys(d.AnimationsTexts) : [];
|
|
17130
17342
|
// ①(修正) 缓存完整 loadJson(含 EventInfos/AnimationsTexts),
|
|
@@ -17843,7 +18055,12 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
17843
18055
|
let projJson = null;
|
|
17844
18056
|
try {
|
|
17845
18057
|
const pj = await fetch(HOST_BASE + "/custom-folder/" + encodeURIComponent(w.name) + "/project.json");
|
|
17846
|
-
|
|
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
|
+
}
|
|
17847
18064
|
} catch {}
|
|
17848
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 });
|
|
17849
18066
|
if (ok === "stale") { try { console.warn("[dsh-mpkg-wallpaper] 时段导入已过期(壁纸已切换),跳过后续链"); } catch {}; return; }
|
|
@@ -18326,10 +18543,11 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
18326
18543
|
let cfg = { enabled: true, morning: 4, day: 9, dusk: 17, night: 20 };
|
|
18327
18544
|
if (projIdx >= 0) {
|
|
18328
18545
|
try {
|
|
18329
|
-
|
|
18546
|
+
// ①(P-177 同口径) 宿主 /media 供来的包内 project.json 也走宽容解析
|
|
18547
|
+
const pj = mpwParseWeJson(new TextDecoder().decode(await readFull(projIdx)));
|
|
18330
18548
|
const pm = (pj.general && pj.general.properties) || null;
|
|
18331
18549
|
if (pm) cfg = timeConfigFromProps(pm, {});
|
|
18332
|
-
} catch {}
|
|
18550
|
+
} catch (e) { mpwWeJsonSwallowed("hostMedia:project.json(timeConfig)", e) } // 原来就吞;现在记账
|
|
18333
18551
|
}
|
|
18334
18552
|
const slot = slotForTime(cfg, new Date());
|
|
18335
18553
|
const active = vtex.find((v) => slotFromName(v.name) === slot) || vtex[0];
|
|
@@ -20530,6 +20748,8 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
20530
20748
|
"diagflag.align.d": "?align=0:复现旧对齐(origin 恒几何中心),用于对齐语义 A/B。",
|
|
20531
20749
|
"diagflag.parallax": "视差旧公式",
|
|
20532
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、粒子、轨迹、脚本指针全链生效。",
|
|
20533
20753
|
"diagflag.audio": "开启声音",
|
|
20534
20754
|
"diagflag.audio.d": "?audio=1:播放场景 sound 层(每层一条流;浏览器策略可能需要先点一下页面)。",
|
|
20535
20755
|
"diagflag.whitefallback": "缺纹理回退",
|
|
@@ -21337,6 +21557,8 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
21337
21557
|
"diagflag.align.d": "?align=0: legacy alignment (origin always geometric center) for A/B.",
|
|
21338
21558
|
"diagflag.parallax": "Legacy parallax",
|
|
21339
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.",
|
|
21340
21562
|
"diagflag.audio": "Enable audio",
|
|
21341
21563
|
"diagflag.audio.d": "?audio=1: play scene sound layers (one stream per layer; may need a user gesture).",
|
|
21342
21564
|
"diagflag.whitefallback": "Missing-texture fallback",
|
package/lib/pkg-extract.js
CHANGED
|
@@ -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
|
-
|
|
591
|
-
|
|
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
|
-
|
|
637
|
-
|
|
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
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
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
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
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