dsh-mpkg-wallpaper 3.16.1 → 3.16.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 +252 -107
- package/lib/index.js +26 -1
- package/lib/pkg-extract.js +116 -6
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -6876,9 +6876,11 @@ function mpwThumbFail(el, why) {
|
|
|
6876
6876
|
/** ①(新) scene 内嵌视频探测(scene-video 快路径):探测 URL 按 key 路由。
|
|
6877
6877
|
* 返回 { has, mime };网络错误/未找到视为无视频(回退静态帧)。 */
|
|
6878
6878
|
function sceneVideoCheckUrl(key) {
|
|
6879
|
-
|
|
6879
|
+
// ①(2026-10-04) `has=1`:只要"有没有"(宿主走只读目录表+前缀的 verdict,不再为问一句提取整段视频);
|
|
6880
|
+
// 不带该参数时宿主保持旧行为(含 mime/size 与探测即落盘缓存)——旧路径判据因此逐位不变。
|
|
6881
|
+
if (key && key.indexOf("library|") === 0) return HOST_URL + "/library-scene-video-check?has=1<oken=" + encodeURIComponent(key.slice(8));
|
|
6880
6882
|
const name = key && key.indexOf("custom|") === 0 ? key.slice(7) : "";
|
|
6881
|
-
return HOST_URL + "/custom-scene-video-check?folder=" + encodeURIComponent(name);
|
|
6883
|
+
return HOST_URL + "/custom-scene-video-check?has=1&folder=" + encodeURIComponent(name);
|
|
6882
6884
|
}
|
|
6883
6885
|
function checkSceneVideo(key) {
|
|
6884
6886
|
// ①(修正) 探测加 4s 超时:host 端首查需同步读包/落盘(可达 1-3s),
|
|
@@ -9390,7 +9392,21 @@ function mpwThumbFail(el, why) {
|
|
|
9390
9392
|
宿主每帧都改 class/style,旧写法每帧同步跑一遍 applyFrostInline)。 */
|
|
9391
9393
|
let fraf = 0;
|
|
9392
9394
|
const frun = () => { fraf = 0; try { applyFrostInline(); } catch (e) {} };
|
|
9393
|
-
|
|
9395
|
+
/* ②(2026-10-05 真机采样定案:"刷新/切子代理会话时标题栏的模糊会闪一下消失")
|
|
9396
|
+
采样读数:`t=2028 frost=false attr=false`、下一帧又回来 —— 宿主切换视图时**重建了 header
|
|
9397
|
+
元素**,我们插在 header 里的磨砂层随旧节点被卸载,而重建走的是 rAF 合并的慢路
|
|
9398
|
+
⇒ 中间有 1~2 帧没有任何磨砂。修法:**新增节点**这一路同步重跑 `syncHeaderFrost()`
|
|
9399
|
+
(MutationObserver 回调是微任务,跑在绘制之前 ⇒ 那一帧就已经带上层),
|
|
9400
|
+
其余(class/style 变更)仍走 rAF 合并,避免动画期间每帧跑。 */
|
|
9401
|
+
frostObserver = new MutationObserver((records) => {
|
|
9402
|
+
let addedEl = false;
|
|
9403
|
+
try {
|
|
9404
|
+
for (const rec of (records || [])) {
|
|
9405
|
+
for (const n2 of (rec.addedNodes || [])) { if (n2 && n2.nodeType === 1) { addedEl = true; break } }
|
|
9406
|
+
if (addedEl) break;
|
|
9407
|
+
}
|
|
9408
|
+
} catch (e) { addedEl = true; }
|
|
9409
|
+
if (addedEl) { try { frun(); } catch (e) {} return; }
|
|
9394
9410
|
if (fraf) return;
|
|
9395
9411
|
try { fraf = (typeof requestAnimationFrame === "function") ? requestAnimationFrame(frun) : setTimeout(frun, 16); }
|
|
9396
9412
|
catch (e) { frun(); }
|
|
@@ -17991,24 +18007,24 @@ ${unifyTakeover ? `
|
|
|
17991
18007
|
现在接管真的做出来:这些表面与左栏**读同一套 token**(同色 + 同厚度 + 同半径),
|
|
17992
18008
|
于是"整屏虚化程度 + 左侧边栏/标题栏透明度"一改,右栏/dock 一起变。
|
|
17993
18009
|
位置在本块最后(与前文选择器同特异性时后者胜)⇒ 不需要额外的权重技巧。 */
|
|
17994
|
-
html body[data-mpw-unify] [data-
|
|
17995
|
-
html body[data-mpw-unify] [data-sidebar-right-float-host],
|
|
17996
|
-
html body[data-mpw-unify] [data-mpw-rs],
|
|
17997
|
-
html body[data-mpw-unify] [data-dockkit-pane],
|
|
17998
|
-
html body[data-mpw-unify] [data-dockkit-strip],
|
|
17999
|
-
html body[data-mpw-unify] [data-dockkit-surface],
|
|
18000
|
-
html body[data-mpw-unify] [data-dockkit-float] {
|
|
18010
|
+
html body[data-mpw-unify] [data-mpw-rs-surface],
|
|
18011
|
+
html body[data-mpw-unify] [data-mpw-rs-surface][data-sidebar-right-float-host],
|
|
18012
|
+
html body[data-mpw-unify] [data-mpw-rs-surface][data-mpw-rs],
|
|
18013
|
+
html body[data-mpw-unify] [data-mpw-rs-surface][data-dockkit-pane],
|
|
18014
|
+
html body[data-mpw-unify] [data-mpw-rs-surface][data-dockkit-strip],
|
|
18015
|
+
html body[data-mpw-unify] [data-mpw-rs-surface][data-dockkit-surface],
|
|
18016
|
+
html body[data-mpw-unify] [data-mpw-rs-surface][data-dockkit-float] {
|
|
18001
18017
|
-webkit-backdrop-filter: ${unifyBlurCss} !important;
|
|
18002
18018
|
backdrop-filter: ${unifyBlurCss} !important;
|
|
18003
18019
|
background-color: var(--mpw-unify-surface) !important;
|
|
18004
18020
|
}
|
|
18005
|
-
html body[data-ds-dark-theme][data-mpw-unify] [data-
|
|
18006
|
-
html body[data-ds-dark-theme][data-mpw-unify] [data-sidebar-right-float-host],
|
|
18007
|
-
html body[data-ds-dark-theme][data-mpw-unify] [data-mpw-rs],
|
|
18008
|
-
html body[data-ds-dark-theme][data-mpw-unify] [data-dockkit-pane],
|
|
18009
|
-
html body[data-ds-dark-theme][data-mpw-unify] [data-dockkit-strip],
|
|
18010
|
-
html body[data-ds-dark-theme][data-mpw-unify] [data-dockkit-surface],
|
|
18011
|
-
html body[data-ds-dark-theme][data-mpw-unify] [data-dockkit-float] {
|
|
18021
|
+
html body[data-ds-dark-theme][data-mpw-unify] [data-mpw-rs-surface],
|
|
18022
|
+
html body[data-ds-dark-theme][data-mpw-unify] [data-mpw-rs-surface][data-sidebar-right-float-host],
|
|
18023
|
+
html body[data-ds-dark-theme][data-mpw-unify] [data-mpw-rs-surface][data-mpw-rs],
|
|
18024
|
+
html body[data-ds-dark-theme][data-mpw-unify] [data-mpw-rs-surface][data-dockkit-pane],
|
|
18025
|
+
html body[data-ds-dark-theme][data-mpw-unify] [data-mpw-rs-surface][data-dockkit-strip],
|
|
18026
|
+
html body[data-ds-dark-theme][data-mpw-unify] [data-mpw-rs-surface][data-dockkit-surface],
|
|
18027
|
+
html body[data-ds-dark-theme][data-mpw-unify] [data-mpw-rs-surface][data-dockkit-float] {
|
|
18012
18028
|
background-color: var(--mpw-unify-surface-dark) !important;
|
|
18013
18029
|
}
|
|
18014
18030
|
/* better-sidebar 的面板 / 底部工作台:**只跟雾色与厚度,不给 backdrop-filter**。
|
|
@@ -18077,6 +18093,9 @@ html body[data-mpw-sblur-off] [data-mpw-rs] {
|
|
|
18077
18093
|
tok("--mpw-pop-surface", mpwSurfaceColor(surfacePlan.left.base, popFallbackAlpha).replace(/[\d.]+%/, `calc(var(--mpw-pop-surface-alpha, ${popFallbackAlpha.toFixed(2)}) * 100%)`));
|
|
18078
18094
|
tok("--mpw-pop-surface-alpha", popFallbackAlpha.toFixed(2));
|
|
18079
18095
|
tok("--mpw-pop-surface-dark", `rgba(18, 22, 30, var(--mpw-pop-surface-alpha, ${popFallbackAlpha.toFixed(2)}))`);
|
|
18096
|
+
/* ③(2026-10-05) C 类弹层(糊不到页面背后 ⇒ 做实在)专用的 88% 表面:**我们自己的 token**,
|
|
18097
|
+
不再借宿主 --dsw-specific-sidebar-fill(真机在侧栏作用域里它算出来是透明的 ⇒ 面板变透明)。 */
|
|
18098
|
+
tok("--mpw-pop-trunc-surface", mpwSurfaceColor(surfacePlan.left.base, 0.88));
|
|
18080
18099
|
tok("--mpw-surface-pop-dark", `rgba(18, 22, 30, var(--mpw-pop-alpha, ${(popSurfaceAlpha / 100).toFixed(2)}))`);
|
|
18081
18100
|
// 弹层**表面**选择器(背景 + 模糊共用一份,避免两处写法漂移)。
|
|
18082
18101
|
// ③(关键收窄 2026-09-13) 只用 [class*="_menu"] 会连**非表面**一起命中:
|
|
@@ -18085,13 +18104,17 @@ html body[data-mpw-sblur-off] [data-mpw-rs] {
|
|
|
18085
18104
|
// `._G5b-a_menuStatus`(说明文字)都含 "_menu" ——
|
|
18086
18105
|
// 给它们加 backdrop-filter 会在界面上糊出一块方块,加底色会糊出白块。
|
|
18087
18106
|
// 这里显式排除 anchor/icon/status/open/item/row/trigger/card 等非表面后缀。
|
|
18088
|
-
|
|
18107
|
+
/* ③(2026-10-05) 末尾追加 `:not([data-mpw-pop-untrunc])`:被我们"去截断"的祖先
|
|
18108
|
+
(见 lib 里的 mpwPopUntruncApply)不再吃这套弹层表面规则 —— 否则我们自己的
|
|
18109
|
+
blur 规则会跟去截断规则互相压制(真机:加号菜单容器的 blur 是**我们自己**给的,
|
|
18110
|
+
特异性还比注入的去截断规则高,摘不掉 ⇒ 内层弹层照旧被截断)。 */
|
|
18111
|
+
const POP_NOT = `:not([role="menuitem"]):not([role="option"]):not([class*="Anchor"]):not([class*="anchor"]):not([class*="Icon"]):not([class*="icon"]):not([class*="Status"]):not([class*="status"]):not([class*="Open"]):not([class*="open"]):not([class*="_item"]):not([class*="Item"]):not([class*="_row"]):not([class*="Row"]):not([class*="_trigger"]):not([class*="Trigger"]):not([class*="_card"]):not([class*="Card"]):not([class*="_groupTitle"]):not([class*="_viewport"]):not([data-mpw-pop-untrunc])`;
|
|
18089
18112
|
/* ①(2026-10-02 用户第 3 条⑥:上下文占用圆圈点开的框是**纯白**、没有玻璃感)
|
|
18090
18113
|
它的真实结构是 div.JObwrW_panel[role="dialog"][aria-label="上下文已用"] ⇒ 既不在
|
|
18091
18114
|
下面这串弹层选择器里(所以没有我们的底),又被"真弹层不做玻璃"那条按 role=dialog
|
|
18092
18115
|
清掉了模糊 ⇒ 纯白。把它(以及同类小面板)纳入弹层表面集合。 */
|
|
18093
18116
|
const popSurfaceSel = `[role="menu"]${POP_NOT}, [role="listbox"]${POP_NOT}, [role="combobox"]${POP_NOT}, [data-dsh-surface]${POP_NOT}, [class*="_menu"]${POP_NOT}, [class*="_popover"]${POP_NOT}, [class*="_dropdown"]${POP_NOT}, [class*="_denseList"]${POP_NOT}, [class*="JObwrW_panel"]${POP_NOT}`;
|
|
18094
|
-
const popTipSel = `[role="tooltip"], [role="alert"]`;
|
|
18117
|
+
const popTipSel = `[role="tooltip"]:not([data-mpw-pop-untrunc]), [role="alert"]:not([data-mpw-pop-untrunc])`;
|
|
18095
18118
|
/* ①(2026-10-02 用户第 3 条:「把这个好用的模糊效果独立成 token,因为还有几个地方也要实现」)
|
|
18096
18119
|
弹层表面 = **底色 + 模糊半径 + 圆角** 三件套,全部由这几枚 token 定义;任何弹层只要被
|
|
18097
18120
|
下面的选择器命中,就自动同一套效果(不再各处写死 rgba/blur)。
|
|
@@ -18133,11 +18156,25 @@ ${popSurfaceSel} {
|
|
|
18133
18156
|
html body [data-mpw-pop-bg] { background-color: var(--mpw-pop-surface) !important; }
|
|
18134
18157
|
/* ②(2026-10-05) 弹层处在一个"自己有底的玻璃祖先"里面(backdrop root 被截断 ⇒ 我们糊不到页面背后):
|
|
18135
18158
|
与其"半透明却没模糊"(真机被读成"只有白色滤镜"),不如**做实在**——表面不透明度抬到 88%,
|
|
18136
|
-
并把 backdrop-filter 撤掉(白算一遍)。注意:只动我们自己的标记属性,宿主元素一个都不碰。
|
|
18137
|
-
|
|
18138
|
-
|
|
18139
|
-
|
|
18140
|
-
html body [data-mpw-pop-
|
|
18159
|
+
并把 backdrop-filter 撤掉(白算一遍)。注意:只动我们自己的标记属性,宿主元素一个都不碰。
|
|
18160
|
+
③(2026-10-05) 底色改用**我们自己的** token(--mpw-pop-trunc-surface):原来借宿主
|
|
18161
|
+
--dsw-specific-sidebar-fill,真机在侧栏作用域里它被算成**透明**(color-mix 嵌套后 α=0)
|
|
18162
|
+
⇒ 面板整个变透明、白字直接看不见(提示气泡就是这么"没有底"的)。 */
|
|
18163
|
+
html body [data-mpw-pop-trunc] { background-color: var(--mpw-pop-trunc-surface, var(--mpw-pop-surface)) !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important; }
|
|
18164
|
+
/* ②(2026-10-05 v3 定案)「去截断 + 补霜」:弹层自己的 backdrop-filter 采样不到页面,是因为
|
|
18165
|
+
祖先链上有个创建 backdrop root 的元素(真机:输入框卡片 uV2eYG_card blur(14px))。
|
|
18166
|
+
B 类弹层开着期间给那个祖先打 data-mpw-pop-untrunc ⇒ 这条规则把它的 blur 摘掉(root 回到整页,
|
|
18167
|
+
弹层自己的 blur 立刻采样得到页面);丢掉的磨砂由 JS 在它内部补一层对齐霜层(data-mpw-pop-frost)。
|
|
18168
|
+
特异性 (0,2,2):压得过宿主按类名写的 !important 规则;而我们自己的弹层表面规则已经被
|
|
18169
|
+
POP_NOT 里的 :not([data-mpw-pop-untrunc]) 排除,不打架。 */
|
|
18170
|
+
html body [data-mpw-pop-untrunc][data-mpw-pop-untrunc] {
|
|
18171
|
+
backdrop-filter: none !important;
|
|
18172
|
+
-webkit-backdrop-filter: none !important;
|
|
18173
|
+
}
|
|
18174
|
+
/* ②(2026-10-05 v3) 旧「根级模糊层」机制已删除(真机结论:挂在卡片的层 root 被截断;
|
|
18175
|
+
挂 body 的层又会压住弹层——弹层在卡片的层叠上下文里)。data-mpw-pop-glass 现在只是"B 类、
|
|
18176
|
+
祖先截断已被我们摘掉"的**诊断标记**,没有任何 CSS 效果(弹层自己的 blur 照常工作)。 */
|
|
18177
|
+
html body [data-mpw-pop-frost] { background-color: transparent !important; }
|
|
18141
18178
|
html body[data-ds-dark-theme] [data-mpw-pop-bg] { background-color: var(--mpw-pop-surface-dark) !important; }
|
|
18142
18179
|
[role="menu"] [class*="card"],
|
|
18143
18180
|
[role="tooltip"] [class*="card"] {
|
|
@@ -24827,9 +24864,9 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
24827
24864
|
} catch { sblurObserver = null; }
|
|
24828
24865
|
// 窗口尺寸变化会让"近全屏"阈值与面板 rect 都变(父对话的真机判据就带视口切换)⇒ 一并重算
|
|
24829
24866
|
try { window.addEventListener("resize", recheck); } catch {}
|
|
24830
|
-
/*
|
|
24831
|
-
|
|
24832
|
-
|
|
24867
|
+
/* ③(2026-10-05 v3) 旧「根级模糊层」是 fixed + 按弹层 rect 对齐 ⇒ 那时才需要 scroll/resize 重贴;
|
|
24868
|
+
现在是"摘祖先 blur + 容器内 inset:0 的霜层",尺寸/位置全交给宿主自己的布局 ⇒ 两个监听删掉
|
|
24869
|
+
(顺带省掉滚动期的一路回调)。 */
|
|
24833
24870
|
check();
|
|
24834
24871
|
}
|
|
24835
24872
|
|
|
@@ -24939,10 +24976,22 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
24939
24976
|
} catch (e) { return 0 }
|
|
24940
24977
|
}
|
|
24941
24978
|
|
|
24979
|
+
/** 我们自己注入的节点(根级模糊层等):它们的突变**绝不能再喂回自己的快路**(否则死循环)。 */
|
|
24980
|
+
function mpwIsOurs(node) {
|
|
24981
|
+
try {
|
|
24982
|
+
if (!node || node.nodeType !== 1) return false;
|
|
24983
|
+
if (node.getAttribute && node.getAttribute("data-plugin") === "dsh-mpkg-wallpaper") return true;
|
|
24984
|
+
for (let p = node.parentElement, k = 0; p && k < 3; p = p.parentElement, k++) {
|
|
24985
|
+
if (p.getAttribute && p.getAttribute("data-plugin") === "dsh-mpkg-wallpaper") return true;
|
|
24986
|
+
}
|
|
24987
|
+
} catch (e) {}
|
|
24988
|
+
return false;
|
|
24989
|
+
}
|
|
24942
24990
|
function mpwTagPopoverFast(records) {
|
|
24943
24991
|
try {
|
|
24944
24992
|
let n = 0;
|
|
24945
24993
|
for (const rec of (records || [])) {
|
|
24994
|
+
if (mpwIsOurs(rec.target)) continue;
|
|
24946
24995
|
/* ②(2026-10-05 真机仍见白帧的根因) 弹层大多是**先插入(隐藏)→ 再靠 class/style 显形**:
|
|
24947
24996
|
只认 addedNodes 时,元素插入那一刻还是 display:none(判定不出"浮层面积",也看不见),
|
|
24948
24997
|
等它显形时走的是 **attributes 突变**,而旧观察器只监听 childList ⇒ 显形那一帧没人管,
|
|
@@ -24966,32 +25015,49 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
24966
25015
|
for (const el of cands) { if (mpwTagOnePopover(el)) n++; }
|
|
24967
25016
|
}
|
|
24968
25017
|
}
|
|
24969
|
-
|
|
24970
|
-
|
|
25018
|
+
/* ③(2026-10-05 会话里掉帧) 快路里**只留打标记**:mpwSideLiftSync 是全文档扫描
|
|
25019
|
+
(class* 子串匹配,代价随会话 DOM 线性涨),侧栏收起这类"宿主每帧改 DOM"的场景下
|
|
25020
|
+
每个突变批次都跑一次 ⇒ 会话里卡、空会话流畅。它挪去节流的重活路径(见 prun)。 */
|
|
25021
|
+
if (n) { try { mpwPopUntruncSync(); } catch (e) {} }
|
|
24971
25022
|
return n;
|
|
24972
25023
|
} catch (e) { return 0 }
|
|
24973
25024
|
}
|
|
24974
25025
|
/** 单个元素:该不该有我们的兜底底;返回是否发生了变化。 */
|
|
24975
25026
|
function mpwTagOnePopover(el) {
|
|
24976
25027
|
let need = false;
|
|
25028
|
+
let why = "";
|
|
24977
25029
|
try {
|
|
24978
25030
|
const cls = (typeof el.className === "string") ? el.className : "";
|
|
24979
|
-
|
|
25031
|
+
/* ③(2026-10-05 白字修复) **提示气泡(role=tooltip)不接管底色**:宿主的气泡是深底浅字
|
|
25032
|
+
(--dsw-alias-tooltip-bg = #2c2c2e 配 #fff 字),我们那层 58% 浅色表面压上去 ⇒
|
|
25033
|
+
白字看不清(真机:右栏右上角图标的气泡)。它自己的 blur 由 CSS 那条规则照常给。 */
|
|
25034
|
+
let isTip = false;
|
|
25035
|
+
try { isTip = el.getAttribute("role") === "tooltip" } catch (e) {}
|
|
25036
|
+
if (!MPW_POP_TAG_NOT.test(cls) && !isTip) {
|
|
24980
25037
|
const cs = getComputedStyle(el);
|
|
24981
25038
|
const blurred = !!(cs && /blur\(/.test(String(cs.backdropFilter || cs.webkitBackdropFilter || "")));
|
|
24982
25039
|
if (blurred) {
|
|
24983
|
-
|
|
24984
|
-
|
|
24985
|
-
|
|
24986
|
-
|
|
24987
|
-
|
|
24988
|
-
if (
|
|
24989
|
-
else if (
|
|
25040
|
+
/* ③(2026-10-05 闪烁修复) 已经带标记的:**只按当初那条结构性判据复核**,绝不重读底色。
|
|
25041
|
+
标记一生效,底色就变成我们自己画的 58%,重读会得到"宿主有半透明底"⇒ 撤标 ⇒
|
|
25042
|
+
宿主底回来 ⇒ 再打标 …… 逐帧自激(真机:"底色时有时无")。
|
|
25043
|
+
属性值就是当初的理由:empty = 宿主没画底;painted = 宿主画了不透明底。 */
|
|
25044
|
+
const reason = el.getAttribute("data-mpw-pop-bg");
|
|
25045
|
+
if (reason === "empty") { need = !mpwPopHasPaintedAncestor(el); why = "empty" }
|
|
25046
|
+
else if (reason === "painted") { need = mpwPopIsFloating(el); why = "painted" }
|
|
25047
|
+
else {
|
|
25048
|
+
const a = mpwColorAlpha(cs.backgroundColor);
|
|
25049
|
+
/* ②(2026-10-05) 两类分开判:
|
|
25050
|
+
· 宿主**没有底**(α<0.05)的弹层:**不要求此刻可见** —— 隐藏时打标无害,
|
|
25051
|
+
等它显形时表面已经在位(这正是"先插入后显形"那条路的白帧来源);
|
|
25052
|
+
· 宿主画了**不透明底**的小浮层:必须量得到面积(隐藏时 rect=0 ⇒ 留给下一次触发)。 */
|
|
25053
|
+
if (a < 0.05) { need = !mpwPopHasPaintedAncestor(el); why = "empty" }
|
|
25054
|
+
else if (a >= 0.999 && cs.display !== "none" && cs.visibility !== "hidden") { need = mpwPopIsFloating(el); why = "painted" }
|
|
25055
|
+
}
|
|
24990
25056
|
}
|
|
24991
25057
|
}
|
|
24992
25058
|
} catch (e) { need = false; }
|
|
24993
25059
|
try {
|
|
24994
|
-
if (need) { if (
|
|
25060
|
+
if (need) { if (el.getAttribute("data-mpw-pop-bg") !== why) { el.setAttribute("data-mpw-pop-bg", why); return true } }
|
|
24995
25061
|
else if (el.hasAttribute("data-mpw-pop-bg")) { el.removeAttribute("data-mpw-pop-bg"); return true }
|
|
24996
25062
|
} catch (e) {}
|
|
24997
25063
|
return false;
|
|
@@ -25003,7 +25069,8 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
25003
25069
|
for (let i = 0; i < list.length; i++) mpwTagOnePopover(list[i]);
|
|
25004
25070
|
}
|
|
25005
25071
|
|
|
25006
|
-
/* ①(P3 修复 2026-10-03
|
|
25072
|
+
/* ①(P3 修复 2026-10-03;历史:机制已被下面的 v3 取代,保留沿革与真机读数)
|
|
25073
|
+
弹层玻璃的 backdrop root 被截断 ⇒ 有半透明、没模糊。
|
|
25007
25074
|
读数与机制(docs/POPOVER-BLUR.md):挂在宿主输入框卡片(uV2eYG_card,backdrop-filter:
|
|
25008
25075
|
blur(14px))子树里的弹层,其 backdrop root 被卡片截断 ⇒ 弹层自己的 blur(11px) 只能采样
|
|
25009
25076
|
卡片内部 = 看不出模糊;挂在 body 下的模型选择器正常。实验定案:把表面移出祖先链即恢复
|
|
@@ -25014,64 +25081,100 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
25014
25081
|
(`[data-mpw-pop-untrunc] { backdrop-filter: none !important; }`)——只在弹层开着期间生效,
|
|
25015
25082
|
弹层关闭(无已标记表面)即摘属性、撤规则元素,宿主逐字还原。代价:属性表 + 一次
|
|
25016
25083
|
querySelectorAll,挂在 prun(已 rAF 合并 + mpwHeavyGate 闸门)里,不开新观察器。 */
|
|
25017
|
-
/* ②(2026-10-05 设计收口) 三类弹层,三条路(宿主元素一个都不碰):
|
|
25084
|
+
/* ②(2026-10-05 设计收口 v1) 三类弹层,三条路(宿主元素一个都不碰):
|
|
25018
25085
|
A. 没有模糊祖先(典型:挂在 body 下的模型/推理选择器)⇒ 我们自己的 58% 表面 + 自己的 blur;
|
|
25019
|
-
B.
|
|
25020
|
-
|
|
25021
|
-
|
|
25022
|
-
|
|
25023
|
-
|
|
25024
|
-
|
|
25025
|
-
|
|
25026
|
-
|
|
25027
|
-
|
|
25028
|
-
|
|
25029
|
-
|
|
25030
|
-
|
|
25031
|
-
|
|
25032
|
-
|
|
25033
|
-
|
|
25034
|
-
|
|
25035
|
-
|
|
25036
|
-
|
|
25037
|
-
|
|
25038
|
-
|
|
25039
|
-
|
|
25040
|
-
|
|
25041
|
-
|
|
25042
|
-
|
|
25043
|
-
|
|
25044
|
-
|
|
25045
|
-
|
|
25046
|
-
|
|
25047
|
-
|
|
25048
|
-
|
|
25049
|
-
|
|
25050
|
-
|
|
25051
|
-
|
|
25052
|
-
|
|
25053
|
-
|
|
25086
|
+
B. 祖先里有模糊、且那个祖先是**已知包装容器**(输入框卡片 / 菜单容器一类);
|
|
25087
|
+
C. 祖先里有模糊、但那不是包装容器(用户正在看的面板,例如子代理展开框)⇒ 88% 实底。 */
|
|
25088
|
+
/* ③(2026-10-05 v3 定案:**去截断 + 补霜**,取代 v2 的「根级模糊层」)
|
|
25089
|
+
为什么 v2 不行(真机读数 tools/host-popover-probe.mjs --groups glass-placement,结构性判据不用像素):
|
|
25090
|
+
· 层挂在**卡片子树里**(v2 最后那版紧挨弹层之前)⇒ 它的 backdrop root 被 uV2eYG_card
|
|
25091
|
+
(宿主自己的 backdrop-filter: blur(14px))截断,采样到的只是卡片内部已经糊过的底色
|
|
25092
|
+
⇒ 观感正好是用户报的"只剩一层白色半透明,模糊没了";
|
|
25093
|
+
· 层挂到 **body**(任何 z)⇒ root 确实干净了,但 `elementsFromPoint` 命中栈显示它**压在整个
|
|
25094
|
+
弹层之上**:弹层在卡片的层叠上下文里(卡片有 backdrop-filter ⇒ 自成层叠上下文),
|
|
25095
|
+
body 级定位元素整体画在它上面 ⇒ 弹层会被我们的层糊掉。两条路都堵死。
|
|
25096
|
+
所以只剩一条路:**把截断源头摘掉**——弹层开着期间给那个祖先打 `data-mpw-pop-untrunc`
|
|
25097
|
+
(静态规则 backdrop-filter:none !important ⇒ backdrop root 回到整页,弹层**自己的** blur
|
|
25098
|
+
立刻采样得到页面,连目标条一起糊),丢掉的磨砂由 `data-mpw-pop-frost` 补回来。
|
|
25099
|
+
真机验证(--groups glass-fix):卡片摘 bf + z-index:0(它本来就是 position:relative)后,
|
|
25100
|
+
弹层截断链**清空**、弹层矩形**一字不动**、补的霜层与卡片矩形完全重合、中心命中仍是弹层自己。
|
|
25101
|
+
代价:一次属性表 + 一次 querySelectorAll(挂在 prun,已 rAF 合并);层是 inset:0 的绝对定位,
|
|
25102
|
+
**不需要**随滚动/缩放重贴(v2 那两个 scroll/resize 监听因此删掉)。 */
|
|
25103
|
+
const popUntruncs = new Map(); // 祖先元素 -> { bf, radius, frost, frostKey, zInline }
|
|
25104
|
+
/** 该祖先能不能给"z-index:-1 的霜层"当层叠上下文:定位元素即可(静态元素我们不碰 ——
|
|
25105
|
+
给它 position:relative 会改包含块、把宿主弹层带跑,真机混账过)。 */
|
|
25106
|
+
function mpwPopCanCtx(el, cs) {
|
|
25107
|
+
try {
|
|
25108
|
+
if (cs && cs.position) return cs.position !== "static";
|
|
25109
|
+
return getComputedStyle(el).position !== "static";
|
|
25110
|
+
} catch (e) { return false }
|
|
25111
|
+
}
|
|
25112
|
+
/** 去截断 + 补霜(幂等:need 里没有的一律还原,宿主逐字回到原样)。 */
|
|
25113
|
+
function mpwPopUntruncApply(need) {
|
|
25114
|
+
let changed = 0;
|
|
25115
|
+
try {
|
|
25116
|
+
for (const el of Array.from(popUntruncs.keys())) {
|
|
25117
|
+
if (need && need.has(el) && el.isConnected !== false) continue;
|
|
25118
|
+
const rec = popUntruncs.get(el);
|
|
25119
|
+
if (rec && rec.frost && rec.frost.parentElement) { try { rec.frost.remove() } catch (e) {} }
|
|
25120
|
+
try { el.removeAttribute("data-mpw-pop-untrunc") } catch (e) {}
|
|
25121
|
+
/* 只还原我们动过的那一项(z-index 是我们为"造层叠上下文"补的) */
|
|
25122
|
+
if (rec && rec.zInline !== null) { try { el.style.zIndex = rec.zInline } catch (e) {} }
|
|
25123
|
+
popUntruncs.delete(el);
|
|
25124
|
+
changed++;
|
|
25054
25125
|
}
|
|
25055
|
-
|
|
25056
|
-
|
|
25057
|
-
|
|
25058
|
-
|
|
25059
|
-
|
|
25060
|
-
|
|
25061
|
-
|
|
25062
|
-
|
|
25063
|
-
|
|
25064
|
-
|
|
25126
|
+
if (!need) return changed;
|
|
25127
|
+
for (const el of need) {
|
|
25128
|
+
let rec = popUntruncs.get(el) || null;
|
|
25129
|
+
if (!rec) {
|
|
25130
|
+
let cs = null;
|
|
25131
|
+
try { cs = getComputedStyle(el) } catch (e) {}
|
|
25132
|
+
const bf = (cs && cs.backdropFilter && cs.backdropFilter !== "none") ? String(cs.backdropFilter) : "";
|
|
25133
|
+
rec = { bf: bf, radius: (cs && cs.borderTopLeftRadius) || "0px", frost: null, frostKey: "", zInline: null };
|
|
25134
|
+
popUntruncs.set(el, rec);
|
|
25135
|
+
try { el.setAttribute("data-mpw-pop-untrunc", "") } catch (e) {}
|
|
25136
|
+
/* 层叠上下文:定位元素且 z-index:auto 时补 z-index:0(auto 与 0 的绘制次序一致,
|
|
25137
|
+
不会把它挪到别的东西上下);已经是数值的不动(它本来就有层叠上下文)。 */
|
|
25138
|
+
try {
|
|
25139
|
+
const pcs = cs || getComputedStyle(el);
|
|
25140
|
+
if (pcs.position !== "static" && pcs.zIndex === "auto") { rec.zInline = el.style.zIndex || ""; el.style.zIndex = "0" }
|
|
25141
|
+
} catch (e) {}
|
|
25142
|
+
changed++;
|
|
25143
|
+
}
|
|
25144
|
+
const key = rec.bf + "|" + rec.radius;
|
|
25145
|
+
/* 存活判据用 parentElement + isConnected === false:桩/异常环境拿不到 isConnected 时
|
|
25146
|
+
也不会每轮重建霜层(重建 = 不可见的 DOM 抖动)。 */
|
|
25147
|
+
const frostAlive = !!(rec.frost && rec.frost.parentElement && rec.frost.isConnected !== false);
|
|
25148
|
+
if (rec.bf && rec.bf.indexOf("blur") >= 0 && (!frostAlive || rec.frostKey !== key)) {
|
|
25149
|
+
try { if (rec.frost && rec.frost.parentElement) rec.frost.remove() } catch (e) {}
|
|
25150
|
+
const frost = document.createElement("div");
|
|
25151
|
+
frost.setAttribute("data-plugin", "dsh-mpkg-wallpaper");
|
|
25152
|
+
frost.setAttribute("data-mpw-pop-frost", "");
|
|
25153
|
+
/* inset:0 + z-index:-1:按 CSS 绘制顺序画在**父元素背景之上、内容之下** ⇒
|
|
25154
|
+
容器自己的磨砂观感、圆角、内容层次全都不变(真机实测矩形与容器逐像素重合)。 */
|
|
25155
|
+
frost.style.cssText = "position:absolute;left:0;top:0;width:100%;height:100%;z-index:-1;pointer-events:none;"
|
|
25156
|
+
+ "border-radius:" + rec.radius + ";backdrop-filter:" + rec.bf + ";";
|
|
25157
|
+
rec.frost = frost;
|
|
25158
|
+
rec.frostKey = key;
|
|
25159
|
+
try { el.insertBefore(frost, el.firstChild) } catch (e) { try { el.appendChild(frost) } catch (e2) {} }
|
|
25160
|
+
changed++;
|
|
25161
|
+
}
|
|
25065
25162
|
}
|
|
25066
25163
|
} catch (e) {}
|
|
25164
|
+
return changed;
|
|
25067
25165
|
}
|
|
25068
25166
|
/* 判据出口(tools/popover-glass-test.mjs 直接用) */
|
|
25069
25167
|
try {
|
|
25070
25168
|
globalThis.__mpwPopUntruncTest = {
|
|
25071
25169
|
sync: () => mpwPopClassifySync(),
|
|
25072
|
-
/*
|
|
25073
|
-
glassCount: () =>
|
|
25074
|
-
|
|
25170
|
+
/* 旧名保留(旧判据引用过"根级模糊层"):v3 已经没有那层了,恒为 0 */
|
|
25171
|
+
glassCount: () => 0,
|
|
25172
|
+
/* 去截断的祖先数(每个都该配一层霜层:无 blur 可补的祖先不计霜层) */
|
|
25173
|
+
untruncCount: () => { try { return document.querySelectorAll("[data-mpw-pop-untrunc]").length } catch (e) { return 0 } },
|
|
25174
|
+
frostCount: () => { try { return document.querySelectorAll("[data-mpw-pop-frost]").length } catch (e) { return 0 } },
|
|
25175
|
+
truncCount: () => { try { return document.querySelectorAll("[data-mpw-pop-trunc]").length } catch (e) { return 0 } },
|
|
25176
|
+
/* 单个元素的兜底底判定(判据要能直接驱动它:闪烁是"打标/撤标"自激,必须单元素可复现) */
|
|
25177
|
+
tag: (el) => mpwTagOnePopover(el),
|
|
25075
25178
|
};
|
|
25076
25179
|
} catch (e) {}
|
|
25077
25180
|
|
|
@@ -25080,26 +25183,43 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
25080
25183
|
const surfaces = document.querySelectorAll("[data-mpw-pop-bg]");
|
|
25081
25184
|
const glasses = new Set();
|
|
25082
25185
|
const truncated = new Set();
|
|
25186
|
+
const neutralize = new Set();
|
|
25083
25187
|
for (let i = 0; i < surfaces.length; i++) {
|
|
25084
25188
|
const el = surfaces[i];
|
|
25085
25189
|
let cs = null;
|
|
25086
25190
|
try { cs = getComputedStyle(el) } catch (e) {}
|
|
25087
|
-
/* 硬前提:我们自己给了模糊才有"被截断"
|
|
25088
|
-
|
|
25191
|
+
/* 硬前提:我们自己给了模糊才有"被截断"这回事。
|
|
25192
|
+
③(2026-10-05 闪烁修复) **已带标记的表面必须放行**:C 类规则把表面的 backdrop-filter
|
|
25193
|
+
撤成 none,只认 computed blur 的话下一轮就会把刚打的标记摘掉 ⇒ 打标/撤标逐帧自激
|
|
25194
|
+
(真机实测:按钮提示气泡与几个面板底色"时有时无")。 */
|
|
25195
|
+
const marked = el.hasAttribute("data-mpw-pop-glass") || el.hasAttribute("data-mpw-pop-trunc");
|
|
25196
|
+
if (!marked && (!cs || !cs.backdropFilter || String(cs.backdropFilter).indexOf("blur") < 0)) continue;
|
|
25197
|
+
/* ③(2026-10-05) 提示气泡(role=tooltip)一律不接管:宿主的气泡是**深底浅字**
|
|
25198
|
+
(--dsw-alias-tooltip-bg 配 #fff 字),我们的表面/实底压上去 ⇒ 白字看不见。
|
|
25199
|
+
它自己的 blur 由 CSS 那条 [role="tooltip"] 规则照常给。 */
|
|
25200
|
+
try { if (el.getAttribute("role") === "tooltip") continue } catch (e) {}
|
|
25201
|
+
let path = "";
|
|
25202
|
+
const cands = []; // 这条链上"可去截断"的祖先(走完再决定用不用:C 路一个都不摘)
|
|
25089
25203
|
for (let p = el.parentElement, n = 0; p && p !== document.body && p !== document.documentElement && n < 12; p = p.parentElement, n++) {
|
|
25090
25204
|
let acs = null;
|
|
25091
25205
|
try { acs = getComputedStyle(p) } catch (e) { break }
|
|
25092
|
-
|
|
25206
|
+
/* ③(2026-10-05 反自激) 已经被我们"去截断"的祖先照样算截断祖先 —— 否则下一轮
|
|
25207
|
+
看不见它的 blur(被我们摘了)⇒ 判成 A ⇒ 撤标记 ⇒ 还原 blur ⇒ 再判 B … 逐帧自激。 */
|
|
25208
|
+
const declared = !!(p.hasAttribute && p.hasAttribute("data-mpw-pop-untrunc"));
|
|
25209
|
+
const ablur = declared || (acs && acs.backdropFilter && acs.backdropFilter !== "none" && String(acs.backdropFilter).indexOf("blur") >= 0);
|
|
25093
25210
|
if (!ablur) continue;
|
|
25094
|
-
let
|
|
25095
|
-
try {
|
|
25096
|
-
if (isSurface) { truncated.add(el); break } // C:用户正在看的面板 ⇒ 一个像素都不动
|
|
25211
|
+
let isOurDeco = false;
|
|
25212
|
+
try { isOurDeco = !declared && p.matches(MPW_POP_TAG_SEL) && !p.hasAttribute("data-mpw-pop-bg") } catch (e) {}
|
|
25097
25213
|
let wrap = false;
|
|
25098
|
-
try { wrap = /(_card|composer|overlayAnchor|_seat|_stack)/i.test(String(p.className || "")) } catch (e) {}
|
|
25099
|
-
|
|
25100
|
-
|
|
25214
|
+
try { wrap = declared || isOurDeco || /(_card|composer|overlayAnchor|_seat|_stack)/i.test(String(p.className || "")) } catch (e) {}
|
|
25215
|
+
/* B:能摘(它得是定位元素:霜层的 z-index:-1 要有层叠上下文兜住;真机两个截断祖先
|
|
25216
|
+
都是定位的)⇒ 记进待摘表;C:其余的(用户正在看的面板 / 未知祖先 / 静态元素)保守做实在。 */
|
|
25217
|
+
if (wrap && mpwPopCanCtx(p, acs)) { cands.push(p); continue }
|
|
25218
|
+
path = "C";
|
|
25101
25219
|
break;
|
|
25102
25220
|
}
|
|
25221
|
+
if (path === "C") truncated.add(el);
|
|
25222
|
+
else if (cands.length) { for (const p of cands) neutralize.add(p); glasses.add(el) }
|
|
25103
25223
|
}
|
|
25104
25224
|
const sync = (attr, want) => {
|
|
25105
25225
|
const stale = document.querySelectorAll("[" + attr + "]");
|
|
@@ -25108,7 +25228,7 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
25108
25228
|
};
|
|
25109
25229
|
sync("data-mpw-pop-glass", glasses);
|
|
25110
25230
|
sync("data-mpw-pop-trunc", truncated);
|
|
25111
|
-
|
|
25231
|
+
mpwPopUntruncApply(neutralize);
|
|
25112
25232
|
return glasses.size + truncated.size;
|
|
25113
25233
|
} catch (e) { return 0 }
|
|
25114
25234
|
}
|
|
@@ -25118,28 +25238,53 @@ body[data-mpw-modal] [role="dialog"] {
|
|
|
25118
25238
|
function setupPopTagWatch() {
|
|
25119
25239
|
if (popTagWatch !== null) return;
|
|
25120
25240
|
let praf = 0;
|
|
25241
|
+
/* ③(2026-10-05 会话里掉帧的根因) 下面这三个同步**每个都是全文档扫描**,而且选择器带
|
|
25242
|
+
`[class*=…]` 子串匹配 ⇒ 代价随会话 DOM 线性涨(真机现象正好是"空会话流畅、会话里卡":
|
|
25243
|
+
宿主在侧栏收起/展开动画期间每帧改 DOM ⇒ 每个突变批次都把这三趟全扫一遍)。
|
|
25244
|
+
改成**节流 + 尾随**:距上次 ≥150ms 才立刻跑;否则跳过并挂一个 180ms 的尾随定时器
|
|
25245
|
+
(突发结束一定还会跑一次,状态不会被饿死)。动画在跑时要求 600ms 才放行(优先保动画帧)。
|
|
25246
|
+
注意:标记本身(data-mpw-pop-bg 等)仍走快路,不受这里影响。 */
|
|
25247
|
+
let popHeavyLast = 0, popHeavyTimer = 0;
|
|
25248
|
+
const nowMs = () => { try { return (typeof performance !== "undefined" && performance.now) ? performance.now() : Date.now() } catch (e) { return Date.now() } };
|
|
25249
|
+
const popHeavySync = () => {
|
|
25250
|
+
popHeavyLast = nowMs();
|
|
25251
|
+
try { mpwTagSurfaceOuter(); } catch (e) {}
|
|
25252
|
+
try { mpwPopUntruncSync(); } catch (e) {}
|
|
25253
|
+
try { mpwSideLiftSync(); } catch (e) {}
|
|
25254
|
+
};
|
|
25255
|
+
const popHeavyTail = () => {
|
|
25256
|
+
if (popHeavyTimer) return;
|
|
25257
|
+
try {
|
|
25258
|
+
popHeavyTimer = setTimeout(() => { popHeavyTimer = 0; try { popHeavySync() } catch (e) {} }, 180);
|
|
25259
|
+
} catch (e) { popHeavyTimer = 0; }
|
|
25260
|
+
};
|
|
25121
25261
|
const prun = () => {
|
|
25122
25262
|
praf = 0;
|
|
25123
25263
|
try {
|
|
25124
25264
|
// 左栏收起/展开与拖拽期间宿主每帧改 DOM:跳过(收尾还会再来一次)
|
|
25125
25265
|
if (document.body && document.body.hasAttribute
|
|
25126
25266
|
&& (document.body.hasAttribute("data-dsh-sidebar-dragging") || document.body.hasAttribute("data-dragging"))) return;
|
|
25127
|
-
/*
|
|
25128
|
-
|
|
25129
|
-
|
|
25267
|
+
/* ②(2026-10-05 卡死 + 卡顿修复) 这一路**不再做全文档扫描**(旧写法每帧
|
|
25268
|
+
`querySelectorAll` 全文档,会话里元素多 ⇒ 侧栏动画就掉帧;空会话元素少 ⇒ 流畅,
|
|
25269
|
+
与真机观察完全一致)。全文档扫描只在 apply/初始那一次,突变一律走 fast path。 */
|
|
25130
25270
|
/* ②(2026-10-04) 右栏/dock 的"只留最外层"标记必须**跟着 DOM 走**:右栏面板是
|
|
25131
25271
|
用户打开后才挂载的,apply 那一刻它还不存在 ⇒ 只按一次会把 dock 错标成表面,
|
|
25132
25272
|
之后面板出现就变成两层(真机读数:dockPane/dockStrip 仍是 0.55)。
|
|
25133
|
-
|
|
25134
|
-
|
|
25135
|
-
|
|
25136
|
-
|
|
25137
|
-
/* ①(P3) 弹层开着 ⇒ 摘截断祖先的 blur;关了 ⇒ 还原(同一入口,随突变重算) */
|
|
25138
|
-
try { mpwPopUntruncSync(); } catch (e) {}
|
|
25273
|
+
这里与弹层标记共用同一个节流入口(要素很少时几乎立刻就跑)。 */
|
|
25274
|
+
const since = nowMs() - popHeavyLast;
|
|
25275
|
+
if (since >= 150 && (!mpwHeavyAnimRunning() || since >= 600)) popHeavySync();
|
|
25276
|
+
else popHeavyTail();
|
|
25139
25277
|
} catch (e) {}
|
|
25140
25278
|
};
|
|
25141
25279
|
try {
|
|
25142
25280
|
popTagWatch = new MutationObserver((records) => {
|
|
25281
|
+
/* ②(2026-10-05 卡死修复的第二道保险) 全是**我们自己**的写入(根级模糊层的 style 等)
|
|
25282
|
+
⇒ 直接返回:连 rAF 都不排,杜绝"写→观察→再写"的自激。 */
|
|
25283
|
+
let anyForeign = false;
|
|
25284
|
+
try {
|
|
25285
|
+
for (const rec of (records || [])) { if (!mpwIsOurs(rec.target)) { anyForeign = true; break } }
|
|
25286
|
+
} catch (e) { anyForeign = true; }
|
|
25287
|
+
if (!anyForeign) return;
|
|
25143
25288
|
/* 同步快路:新增的弹层当场判定打标(消灭"先白后变"的过渡态) */
|
|
25144
25289
|
try { mpwTagPopoverFast(records); } catch (e) {}
|
|
25145
25290
|
if (praf) return;
|
package/lib/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { execFileSync, spawn, spawnSync } 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, mpwParseWeJson, mpwWeJsonSwallowed } from './pkg-extract.js';
|
|
19
|
+
import { extractSceneMainImage, extractSceneMainImageFromDir, extractSceneManifest, extractSceneLayer, extractTexVideoMp4, collectSceneVideoFiles, parsePkg, readPkgEntry, scanSceneAudio, scanSceneVideo, clearSceneVideoScanCache, probeSceneVideoVerdict, 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)、宿主媒体音频控制
|
|
@@ -4229,6 +4229,31 @@ function apply(ctx) {
|
|
|
4229
4229
|
const probeSceneVideo = (req, res, dir) => {
|
|
4230
4230
|
const t0 = Date.now();
|
|
4231
4231
|
try {
|
|
4232
|
+
/* ①(2026-10-04) **只判不取**(**仅当调用方显式要 `?has=1` 时**):客户端"卡片/清单阶段"
|
|
4233
|
+
* 只问"这张场景有没有内嵌视频"(`lib/client.js` 只读 `d.has`),而 `ensureSceneVideo` 是
|
|
4234
|
+
* "判定 + 提取"一体的(会把整段内嵌视频读进内存并落盘;实测 792MB 的容器 = 791.6MB 读盘)。
|
|
4235
|
+
* `?has=1` 时先走只读目录表 + 候选前缀的 `probeSceneVideoVerdict`(实测 64KB/容器):能判定
|
|
4236
|
+
* 就直接答 `{ok,has}`,答不了(unknown/多独立视频/松散目录)回退提取路径。
|
|
4237
|
+
* ⚠ **不带该参数时行为与改前逐位相同**(含 `{has,mime,size}` 响应形状与"探测即落盘缓存"
|
|
4238
|
+
* 的既有契约)—— 既有判据 `tools/scene-video-test.mjs` 钉的正是那条默认路径。 */
|
|
4239
|
+
const wantHasOnly = (() => {
|
|
4240
|
+
try {
|
|
4241
|
+
const q = String((req && req.url) || '');
|
|
4242
|
+
return /[?&]has=1(?:&|$)/.test(q);
|
|
4243
|
+
} catch { return false; }
|
|
4244
|
+
})();
|
|
4245
|
+
try {
|
|
4246
|
+
const verdict = wantHasOnly ? probeSceneVideoVerdict(dir) : null;
|
|
4247
|
+
if (verdict && verdict.decided) {
|
|
4248
|
+
const ms0 = Date.now() - t0;
|
|
4249
|
+
if (ms0 > 500) {
|
|
4250
|
+
try { console.log('[dsh-mpkg-wallpaper][perf] scene 视频探测(verdict) ' + ms0 + 'ms:', dir); } catch {}
|
|
4251
|
+
}
|
|
4252
|
+
if (!verdict.has) { json(res, 200, { ok: true, has: false, verdict: true }); return; }
|
|
4253
|
+
json(res, 200, { ok: true, has: true, verdict: true });
|
|
4254
|
+
return;
|
|
4255
|
+
}
|
|
4256
|
+
} catch { /* verdict 出意外 → 回退提取路径(与改前同一答案) */ }
|
|
4232
4257
|
const v = ensureSceneVideo(dir);
|
|
4233
4258
|
const ms = Date.now() - t0;
|
|
4234
4259
|
if (ms > 500) {
|
package/lib/pkg-extract.js
CHANGED
|
@@ -427,6 +427,35 @@ function readPkgEntry(data, entry) {
|
|
|
427
427
|
return out;
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
+
/**
|
|
431
|
+
* ①(2026-10-04) **从"已读出的存储字节"还原一条目**:把"是否 LZ4 链"的判定交给**唯一口径**
|
|
432
|
+
* `probeCompressedEntry`(与 `parsePkg` 同一函数),而不是依赖调用方传进来的 `flags`。
|
|
433
|
+
*
|
|
434
|
+
* 为什么需要:`readPkgIndexFromFile` 只读目录表,条目内容不在已读头里 ⇒ 头解析出来的条目
|
|
435
|
+
* `flags` 恒为 0(`partial:true`)。索引先行的路径据此把**压缩条目**当 raw 用 ⇒ 视频/TEX
|
|
436
|
+
* 在"压缩存储"时会静默探测失败(legacy 整包口径却能成功)—— 这正是"两条路径判定分叉"的形态。
|
|
437
|
+
* 这里用全量存储字节做判定(信息最全),成功即解压;`flags & 1` 的快路保留(整包口径)。
|
|
438
|
+
*/
|
|
439
|
+
function decodeStoredEntry(stored, entry) {
|
|
440
|
+
const raw = stored instanceof Uint8Array ? stored : new Uint8Array(stored)
|
|
441
|
+
let original = null
|
|
442
|
+
if ((entry.flags & 1) === 0) {
|
|
443
|
+
// 头解析出来的条目 `size` 是**存储长度**(partial),不能拿它当原始长度:链头 u64 才是真值
|
|
444
|
+
try { original = probeCompressedEntry(raw, 0, raw.length) } catch { original = null }
|
|
445
|
+
if (original === null) return raw
|
|
446
|
+
}
|
|
447
|
+
if (original === null) {
|
|
448
|
+
// 整包口径(flags&1):`entry.size` 已是 probe 出来的原始长度;仍以链头为准做一致性兜底
|
|
449
|
+
try {
|
|
450
|
+
const view = new DataView(raw.buffer, raw.byteOffset, raw.byteLength)
|
|
451
|
+
const head = Number(view.getBigUint64(0, true))
|
|
452
|
+
original = head > 0 && head <= 2147483647 ? head : entry.size
|
|
453
|
+
} catch { original = entry.size }
|
|
454
|
+
}
|
|
455
|
+
const w = { path: entry.path, offset: 0, compressedSize: raw.length, size: original, flags: 1 }
|
|
456
|
+
return readPkgEntry(raw, w)
|
|
457
|
+
}
|
|
458
|
+
|
|
430
459
|
/* ═══════════════════════════════════════════════════════════════════════════
|
|
431
460
|
* ①(2026-09-15 用户第 1 条反馈「扫描音频的速度能否快些」) 惰性音频索引
|
|
432
461
|
*
|
|
@@ -2446,11 +2475,9 @@ function scanSceneVideo(dirOrPath, opts = {}) {
|
|
|
2446
2475
|
const got = readSync(fd, stored, 0, stored.length, Math.max(0, e.offset));
|
|
2447
2476
|
bytesRead += got; entriesRead++;
|
|
2448
2477
|
const buf = got === stored.length ? stored : stored.subarray(0, got);
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
}
|
|
2453
|
-
return new Uint8Array(buf);
|
|
2478
|
+
// ①(2026-10-04) 走 decodeStoredEntry:头解析的条目 flags 恒为 0(partial),
|
|
2479
|
+
// 压缩条目必须靠 probeCompressedEntry 判(全量口径),否则会被当 raw 用 ⇒ 静默漏判。
|
|
2480
|
+
return decodeStoredEntry(buf, e);
|
|
2454
2481
|
} catch { return null; }
|
|
2455
2482
|
};
|
|
2456
2483
|
// ① 独立视频条目:只读它们(旧路径读了全部 TEX 也会丢弃这些结果)
|
|
@@ -2510,7 +2537,90 @@ function scanSceneVideo(dirOrPath, opts = {}) {
|
|
|
2510
2537
|
return { video, source, cacheHit: false, indexEntries, entriesRead, bytesRead, tableBytes: source === 'pkg' ? undefined : 0, ms };
|
|
2511
2538
|
}
|
|
2512
2539
|
|
|
2513
|
-
|
|
2540
|
+
/* ═══════════════════════════════════════════════════════════════════════════
|
|
2541
|
+
* ①(2026-10-04) 「只判不取」的 scene 内嵌视频 verdict
|
|
2542
|
+
*
|
|
2543
|
+
* 动机(本机实测):`ensureSceneVideo` 是"判定 + 提取"一体的 —— 探一次就把整段内嵌视频读进
|
|
2544
|
+
* 内存并落盘(实测样例里 712 MB 的视频 = 712 MB 读盘),而客户端在**列表/卡片阶段**只问
|
|
2545
|
+
* `{has, mime}`(`lib/client.js` 的 `sceneVideoCheckUrl` 只读 `d.has`)。本函数只做判定:
|
|
2546
|
+
* **只读目录表 + 每条候选的前缀**(本机实测判定成本 ≈ 60 KB/包),绝不读视频载荷。
|
|
2547
|
+
*
|
|
2548
|
+
* 判定口径与 `scanSceneVideo` / legacy(`findSceneVideoInPkg`)**同源**:
|
|
2549
|
+
* · 有独立视频条目 ⇒ 恰好 1 条时可判定(取它);≥2 条时"谁最大"要看解压后长度 ⇒ 交回慢路径;
|
|
2550
|
+
* · 否则看 TEX:逐条前缀判定(`probeTexVideoDecision`)——
|
|
2551
|
+
* ≥2 条判为 video ⇒ `has:false`(与 legacy "多 TEX 视频 ⇒ null" 一致);
|
|
2552
|
+
* exactly 1 条 video 且无 unknown ⇒ `has:true`;0 条 video 且无 unknown ⇒ `has:false`;
|
|
2553
|
+
* 出现 unknown(前缀不足/异常布局/压缩解释歧义)⇒ `decided:false`,调用方回退 `ensureSceneVideo`。
|
|
2554
|
+
* · **松散目录**(`scene.json` + 平铺 `.tex`)一律 `decided:false`:那种场景没有大载荷,不值得为它加一条分支。
|
|
2555
|
+
*
|
|
2556
|
+
* 返回 `{ has, ref, tex, decided, source, reason, indexEntries, bytesRead, ms }`。
|
|
2557
|
+
* ⚠ 本函数**不写任何缓存**(判定结果由调用方决定是否复用;提取物缓存仍归 `ensureSceneVideo`)。
|
|
2558
|
+
* ═══════════════════════════════════════════════════════════════════════════ */
|
|
2559
|
+
function probeSceneVideoVerdict(dirOrPath, opts = {}) {
|
|
2560
|
+
const t0 = process.hrtime.bigint()
|
|
2561
|
+
const done = (o) => Object.assign({ has: null, ref: null, tex: null, decided: false, source: 'pkg', reason: null, indexEntries: 0, bytesRead: 0, texPeeked: 0, ms: Number(process.hrtime.bigint() - t0) / 1e6 }, o)
|
|
2562
|
+
let target = dirOrPath
|
|
2563
|
+
try {
|
|
2564
|
+
const st0 = statSync(dirOrPath)
|
|
2565
|
+
if (st0.isDirectory()) {
|
|
2566
|
+
const pkgPath = join(dirOrPath, 'scene.pkg')
|
|
2567
|
+
if (!existsSync(pkgPath)) return done({ source: 'dir', reason: 'loose-dir' })
|
|
2568
|
+
target = pkgPath
|
|
2569
|
+
}
|
|
2570
|
+
} catch (e) { return done({ source: 'unknown', reason: 'stat-failed' }) }
|
|
2571
|
+
let idx = null
|
|
2572
|
+
try { idx = readPkgIndexFromFile(target, opts) } catch (e) { return done({ reason: 'index-unreadable' }) }
|
|
2573
|
+
const idxEntries = idx.entries
|
|
2574
|
+
const sizeOf = (e) => (e.compressedSize == null ? e.size : e.compressedSize)
|
|
2575
|
+
const base = { indexEntries: idxEntries.length, bytesRead: idx.headBytes, texPeeked: 0 }
|
|
2576
|
+
const standalone = idxEntries.filter((e) => SCENE_VIDEO_EXT_RE.test(String(e.path || '').toLowerCase()) && sizeOf(e) > 0)
|
|
2577
|
+
if (standalone.length === 1) return done(Object.assign({ has: true, ref: standalone[0].path, tex: false, decided: true }, base))
|
|
2578
|
+
if (standalone.length > 1) return done(Object.assign({ reason: 'multi-standalone' }, base))
|
|
2579
|
+
const texEntries = idxEntries.filter((e) => String(e.path || '').toLowerCase().endsWith('.tex'))
|
|
2580
|
+
let bytesRead = idx.headBytes
|
|
2581
|
+
let texPeeked = 0
|
|
2582
|
+
const fd = openSync(target, 'r')
|
|
2583
|
+
try {
|
|
2584
|
+
let positives = 0, unknown = 0, firstPositive = null
|
|
2585
|
+
for (const e of texEntries) {
|
|
2586
|
+
const want = Math.min(sizeOf(e), TEX_PEEK_BYTES)
|
|
2587
|
+
if (want <= 0) { unknown++; continue }
|
|
2588
|
+
let head = null
|
|
2589
|
+
try {
|
|
2590
|
+
const buf = Buffer.allocUnsafe(want)
|
|
2591
|
+
const got = readSync(fd, buf, 0, want, Math.max(0, e.offset))
|
|
2592
|
+
bytesRead += got
|
|
2593
|
+
if (got > 0) texPeeked++
|
|
2594
|
+
head = got === want ? buf : buf.subarray(0, got)
|
|
2595
|
+
} catch { head = null }
|
|
2596
|
+
const decide = (bytes) => (bytes && bytes.length)
|
|
2597
|
+
? probeTexVideoDecision((off, len) => (off + len > bytes.length ? null : bytes.subarray(off, off + len)), bytes, e.path)
|
|
2598
|
+
: 'unknown'
|
|
2599
|
+
let decision = decide(head)
|
|
2600
|
+
// 头解析的条目 `flags` 未知:可能是 LZ4 链条目 ⇒ 再按"压缩解释"试一次(成功且判出结论才采用)
|
|
2601
|
+
if (decision === 'unknown') {
|
|
2602
|
+
try {
|
|
2603
|
+
const d = readPkgEntryHead(head, { path: e.path, offset: 0, compressedSize: head.length, size: e.size, flags: 1 }, TEX_PEEK_BYTES)
|
|
2604
|
+
const d2 = decide(d)
|
|
2605
|
+
if (d2 !== 'unknown') decision = d2
|
|
2606
|
+
} catch { /* 不是压缩条目(或前缀不足)→ 保持 unknown */ }
|
|
2607
|
+
}
|
|
2608
|
+
if (decision === 'none') continue
|
|
2609
|
+
if (decision === 'video') {
|
|
2610
|
+
positives++
|
|
2611
|
+
if (!firstPositive) firstPositive = e.path
|
|
2612
|
+
if (positives >= 2) break
|
|
2613
|
+
} else unknown++
|
|
2614
|
+
}
|
|
2615
|
+
const out = { indexEntries: idxEntries.length, bytesRead, texPeeked }
|
|
2616
|
+
if (positives >= 2) return done(Object.assign({ has: false, decided: true }, out))
|
|
2617
|
+
if (positives === 1 && unknown === 0) return done(Object.assign({ has: true, ref: firstPositive, tex: true, decided: true }, out))
|
|
2618
|
+
if (positives === 0 && unknown === 0) return done(Object.assign({ has: false, decided: true }, out))
|
|
2619
|
+
return done(Object.assign({ reason: 'unknown-tex-decision', positives, unknown }, out))
|
|
2620
|
+
} finally { closeSync(fd) }
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
export { extractSceneMainImage, extractSceneMainImageFromDir, extractSceneManifest, extractSceneLayer, extractTexVideoMp4, collectSceneVideoFiles, parsePkg, readPkgEntry, parseTex, PKG_MAGIC_RE, probeSceneVideoVerdict, decodeStoredEntry };
|
|
2514
2624
|
// ①(2026-09-15 音频扫描提速 / 2026-09-16 洁净室重写) 惰性音频索引:只读目录表 + 仅候选头 ≤16 字节。
|
|
2515
2625
|
// 名字与结构依据 docs/AUDIO-TRACK-SPEC.md(后缀判定 / 容器判定 / 枚举核心里外三层)。
|
|
2516
2626
|
export {
|
package/package.json
CHANGED