dsh-folia 4.12.6 → 4.13.0

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/cordis.patch.yml CHANGED
@@ -1,23 +1,23 @@
1
1
  # dsh-plugin-folia bundle patch.
2
2
  #
3
- # Row set: this bundle + the skin centre it brings along.
3
+ # ⚠️ 这里**只有一条** row。皮肤中心由桌面端/市场自己那条 row 负责,我们不再插第二条(见下)。
4
4
  #
5
- # ── 为什么这里有第二条 row ──────────────────────────────────────────────────
6
- # 我们不再自己实现皮肤渲染:皮肤包(`skin.json` + `skin.css` + 可选 `hooks.mjs`)由
7
- # `@linxin666/dsh-client-ui-skin-center` 加载与启用,我们的「助手皮肤」页签只负责把站点 zip
8
- # 解到它读的那个目录(`$DSH_HOME/skins`)。所以它必须是**装得上的**:本包把它列为
9
- # `dependencies`,并在下面插一条 row —— 用户装 dsh-folia 就同时得到两件套,不用再单独装。
5
+ # ── 为什么不再声明 skin-center 的 row 与依赖(2026-10-30 实测)─────────────────
6
+ # 我们一厢情愿地"随身携带"皮肤中心(`dependencies` + 第二条 insert row),结果与桌面端自己
7
+ # 管理的同一条 row 撞了:`.dsh-market/log.ndjson` 每次启动都会写
10
8
  #
11
- # ── 重复 row 为什么不会打起来(实测依据,不是猜测)────────────────────────
12
- # 1) 宿主半边:它的 `apply` 走 `mountOnce('@linxin666/dsh-client-ui-skin-center', …)`,
13
- # 以 `globalThis[Symbol.for('dsh-web.mounted-plugins')]` 记账 ⇒ 第二次挂载是空操作,
14
- # 不会重复注册 webserver 路由。
15
- # 2) 客户端半边:它的设置分区注册包在 `try { ctx.slots.register(...) } catch { return () => {} }`,
16
- # 重复注册被吞掉 ⇒ 不会出现两张「皮肤」设置页。
17
- # 于是「用户已有 dsh-web-all(它也插了一条 row)」与「只用我们」两种情形都成立。
9
+ # "@linxin666/dsh-client-ui-skin-center -> off: fiber=true"
10
+ # "plugin kept off: @linxin666/dsh-client-ui-skin-center"
18
11
  #
19
- # 版本:依赖写 `^0.4.8`(本仓实测过的版本线);它若大改 v1alpha1 契约,我们跟着升级即可。
20
- # 判据:test/verify-skin-center-bundle.mjs 会同时钉住「声明了依赖」与「插了唯一 id 的 row」。
12
+ # 也就是**它被关掉了**,于是 `/api/skin-center/v2/*` 整族变成路由 miss(带会话实测:404),
13
+ # 「助手皮肤」页签里点「应用」必然失败。把依赖钉到 0.4.8 只在那一次启动里生效,桌面端重启后
14
+ # 会把 0.4.12 投影回来(engines 要求 `dsh >= 0.2.1-rc.3`,而当前桌面端是 rc.2)→ 又被关掉。
15
+ # 直接改 profile 的 package.json / overrides 会让桌面端"Profile 恢复未完成"进安全模式 —— 那条路
16
+ # 我们已经走过一次并回滚,不再重复。
17
+ #
18
+ # 结论:**不由我们替用户装这个插件**。它该由用户在自己的插件市场/桌面端里装(那里管版本与引擎
19
+ # 匹配),我们的页签只做两件事:把站点 zip 解到 `$DSH_HOME/skins`,以及在它确实在跑时写选择。
20
+ # 判据见 test/verify-skin-center-bundle.mjs:**只允许一条 row**,且不得再声明为依赖。
21
21
  #
22
22
  # A patch replaces the targeted row's whole `config`, so only additive `insert`
23
23
  # rows appear here (we never override an existing shipped row).
@@ -25,19 +25,3 @@
25
25
  - insert:
26
26
  - id: folia
27
27
  name: 'dsh-folia'
28
- # 皮肤中心:独立 npm 包(BSD-3-Clause),本包作为依赖携带。
29
- #
30
- # ⚠️ row id **不能**以 `folia-` 开头:`lib/subpackage-boot.js#subpackageSpecifiers` 把
31
- # 「id 以 `folia-` 开头的行」当作**我们自己的子包行**做启动自检(解析不到就抛,因为宿主
32
- # 对行级失败是静默的)。给第三方行起 `folia-skin-center` 这种 id 会被误判成"我们的子包"
33
- # ⇒ 自检去仓库 node_modules 里解析它,开发树里没有这个依赖就直接抛,整个 apply 失败。
34
- # 所以这里用不撞前缀的 `skin-center`(也与 web-all 的 `web-ui-skin-center`、
35
- # 官方包的 `ui-skin-center` 都不同名)。
36
- #
37
- # ⚠️ 版本**钉死 0.4.8**(package.json 里也是 exact):0.4.9+ 起的 peer 要求
38
- # `@deepseek-ai/dsh >= 0.2.1-rc.3`,而当前桌面端跑的是 0.2.1-rc.2 —— 于是市场把这条行
39
- # **保持为 off**(`.dsh-market/log.ndjson` 里写着 "plugin kept off: …skin-center"),
40
- # 皮肤中心的 /api/skin-center/v2/* 全部变成路由 miss(401/404),页签里点「应用」必失败。
41
- # 0.4.8 的 peer 是 `>=0.2.1-rc.2`,与现在的宿主匹配。等桌面端升到 rc.3 再放开版本范围。
42
- - id: skin-center
43
- name: '@linxin666/dsh-client-ui-skin-center'
package/lib/client.js CHANGED
@@ -18317,6 +18317,17 @@ window.__ModuleLoader__.load({
18317
18317
  const resp = await apiJson("/skins/list" + (force ? "?refresh=1" : ""), { parse: "always" });
18318
18318
  const doc = (resp && resp.data) || null;
18319
18319
  if (resp && resp.ok && doc && doc.ok) {
18320
+ // 皮肤中心在不在跑,**由浏览器判定**,不信宿主探测的结论。
18321
+ // 宿主 fetch 不带会话:`/api/**` 一旦被鉴权挡在前面就只会拿到 401,而 401 既可能是
18322
+ // "装了但需要会话"也可能是"路由没挂载",从宿主那一侧**无法区分**(实测踩过:
18323
+ // 报文纯 401 就把整个页签的「应用」按钮锁死)。浏览器同源请求天然带会话,
18324
+ // 拿得到就是真在跑,拿不到再退回宿主给的三态。
18325
+ if (doc.skinCenter !== "present") {
18326
+ try {
18327
+ const cat = await apiJson("/api/skin-center/v2/catalog", { parse: "always" });
18328
+ if (cat && cat.ok && cat.data && cat.data.ok) doc.skinCenter = "present";
18329
+ } catch { /* 仍旧用宿主给的状态 */ }
18330
+ }
18320
18331
  // 读"当前在用哪套皮肤":**先直连**(同源相对路径 `/api/**`,凭据天然带全),
18321
18332
  // 直连不通(页面的 origin 可能不等于宿主 HTTP 服务的 origin ⇒ 路由没到)再退回宿主代理
18322
18333
  // `/folia/skins/active` —— 代理会把浏览器这一侧的 cookie 与同源头转发过去。
@@ -18383,6 +18394,15 @@ window.__ModuleLoader__.load({
18383
18394
  });
18384
18395
  return;
18385
18396
  }
18397
+ // 关键:让**皮肤赢**。皮肤中心检测到 `body[data-we-wallpaper]` 时会**主动停画**
18398
+ // (它的 withhold 合约:壁纸在台时它不上台),所以"写成功但画面不变"完全合理 ——
18399
+ // 用户在这个页签点「应用」,意图就是让皮肤接管外观,因此这里主动进入「皮肤让路」态:
18400
+ // 清掉我们的壁纸层 + 摘下玻璃门控(client.js 里既有的 enterSkinYield,记忆会落盘,
18401
+ // 点「恢复默认外观」时按记忆放回壁纸)。
18402
+ try {
18403
+ if (wantId && typeof enterSkinYield === "function") enterSkinYield("folia-tab-apply");
18404
+ if (!wantId && typeof exitSkinYield === "function") exitSkinYield("folia-tab-clear");
18405
+ } catch { /* 让路是增强:失败也不影响皮肤中心的写入 */ }
18386
18406
  // 两条收敛路径:① 广播皮肤中心自己监听的 `dsh-skin-applied`(立即就地切换);
18387
18407
  // ② 它另外每 2 秒轮询 /api/skin-center/v2/active(事件丢了也会在 2 秒内跟上)。
18388
18408
  try {
@@ -18390,7 +18410,9 @@ window.__ModuleLoader__.load({
18390
18410
  } catch { /* 老环境没有 CustomEvent 也无所谓,②会兜住 */ }
18391
18411
  setNote({
18392
18412
  ok: true,
18393
- text: wantId ? weT("已切换到:{name}", { name: label || "" }) : weT("已恢复官方默认外观"),
18413
+ text: wantId
18414
+ ? weT("已切换到:{name}(本插件的壁纸与玻璃已让位)", { name: label || "" })
18415
+ : weT("已恢复官方默认外观(壁纸已按记忆放回)"),
18394
18416
  });
18395
18417
  await load(true);
18396
18418
  return;
@@ -18471,7 +18493,7 @@ window.__ModuleLoader__.load({
18471
18493
  className: "we-picker__hint",
18472
18494
  style: { margin: "6px 0 4px", color: "#ffd08a", lineHeight: "1.6" },
18473
18495
  }, skinCenter === "auth-gated"
18474
- ? weT("探测不到皮肤中心的状态(它的接口被会话鉴权挡住了,宿主拿不到会话)。先点一次「刷新」;若下面点「应用」仍报错,就按 0.4.8 版本重装它。")
18496
+ ? weT("读不到皮肤中心的清单(浏览器侧也被鉴权挡住)。先点一次「刷新」;仍不行就说明它没在运行 —— 去设置里确认皮肤中心已启用。")
18475
18497
  : weT("皮肤中心状态未知(探测超时/网络错误)。点「刷新」重试。")));
18476
18498
  } else {
18477
18499
  children.push(React.createElement("div", {
@@ -18481,7 +18503,7 @@ window.__ModuleLoader__.load({
18481
18503
  }, weT("本页签可以直接切换皮肤:装完点「应用」即可,无需打开设置页。皮肤包落在皮肤中心目录,两边读同一份。")));
18482
18504
  }
18483
18505
  // 皮肤中心不在跑时,「应用」按钮直接禁用并给出原因 —— 比"点了报错"诚实。
18484
- const applyBlockedReason = canApply ? "" : weT("皮肤中心没在运行,先按上面的命令装/降级到 0.4.8");
18506
+ const applyBlockedReason = canApply ? "" : weT("读不到皮肤中心的清单,先点「刷新」;仍不行说明它没在运行");
18485
18507
 
18486
18508
  // 提示行常驻占位(与开机动画/桌宠页签同一条纪律:不因 note 出现/消失让面板跳高度)
18487
18509
  children.push(React.createElement("div", {
@@ -18617,6 +18639,170 @@ window.__ModuleLoader__.load({
18617
18639
  return React.createElement("div", { className: "we-picker", style: { display: "flex", flexDirection: "column" } }, children);
18618
18640
  }
18619
18641
 
18642
+ // ── 内联模块:src/skin-compat.js —— 皮肤兼容层:把宿主槽位翻成分栏词表(data-pane / data-dsh-frame)—— 皮肤包写的是 web-all 的 compat 标记,没装 web-all 时否则整套选择器不命中(构建期注入;勿手改本段)──
18643
+ /**
18644
+ * 皮肤兼容层:把宿主(stock DSH)的**槽位词表**翻译成皮肤包认的**分栏词表**。
18645
+ *
18646
+ * 为什么需要它(实测,不是推测):
18647
+ * · 皮肤包的 CSS 大量使用 `[data-pane="sidebar"]` / `[data-pane="conversation"]` /
18648
+ * `[data-pane="details"]` / `[data-dsh-frame]`。样例计数(按行):
18649
+ * verdandi 60+、xp 36+、deep-current 12、cafe-roastery 10、ember-fall 6、blueprint 4 …
18650
+ * 全catalog 里 `data-pane=` 出现 250+ 次。
18651
+ * · 但**这些属性不是宿主写的**,是 `@linxin666/dsh-web-all` 的 compat shim 写的:
18652
+ * web-all/lib/client.js 的 `COLUMN_SHIMS = [["[class*=\"sidebarCol\"]","data-pane=\"sidebar\""], …]`
18653
+ * 而皮肤中心自己也写明:"the `data-pane=\"conversation\"` attribute is stamped by the
18654
+ * dsh-web-all compat shim"(skin-center/lib/client.js:4171 附近的注释)。
18655
+ * ⇒ 没装 web-all 时,这些选择器**一条都不命中**:皮肤不是"坏了",是它认的分栏标记不在。
18656
+ * 症状就是"皮肤显示不全"(布局/侧栏/输入区没被皮肤接管),而装了 web-all 就正常。
18657
+ *
18658
+ * 我们的做法:**只读宿主槽位,补写分栏属性**。
18659
+ * · `[data-slot="sidebar"]` → 它的父元素(`div.[class*=sidebarCol]`)标 `data-pane="sidebar"`
18660
+ * · `[data-slot="main"]` → 它的父元素(`div.[class*=centerCol]`)标 `data-pane="conversation"`
18661
+ * · `[data-slot="rightbar"]` → 它的父元素(`div.[class*=rightbarCol]`)标 `data-pane="details"`
18662
+ * · `#root` 下的画框元素 → `data-dsh-frame`
18663
+ *
18664
+ * ⚠️ **锚点必须取"列那一级"的槽名**(sidebar / main / rightbar),取深层槽名是无效的:
18665
+ * 宿主 `renderSlot` 的锚点是 `ANCHOR_STYLE = { display: "contents" }` 的**无布局包裹元素**
18666
+ * 与列容器之间的嵌套(app.asar 实证,JSX 顺序即 DOM 顺序):
18667
+ *
18668
+ * div.frame
18669
+ * ├ div.sidebarCol ← children: renderSlot("sidebar") ⇒ 锚点的父元素就是列
18670
+ * ├ div.centerCol ← children: renderSlot("main") ⇒ 同上
18671
+ * └ div.rightbarCol ← children: renderSlot("rightbar") ⇒ 同上
18672
+ *
18673
+ * 而 `main.conversation` 是 `main` 的子槽(`ConversationPanel` 里 renderSlot),
18674
+ * `rightbar.session` 更在 `div.session` 之内 ⇒ 它们的**父元素是另一个 display:contents 锚点
18675
+ * 或内层包裹 div**,把 data-pane 写在那里既没有布局效果、也不是方案认的那一列。
18676
+ * ⇒ 所以这里既用列级槽名,又让 `paneHost` **穿过 `[data-slot]` 锚点**再落笔;
18677
+ * 深层槽名只作退路(宿主哪天不再给列级槽名时仍能命中)。
18678
+ *
18679
+ * 三条纪律(与仓库既有的跨插件协议一致):
18680
+ * ① **只写自己的标记**:不碰皮肤中心的 `data-dsh-skin` / `data-dsh-surface` / 任何 `data-we-*`;
18681
+ * ② **幂等**:已存在同名属性就不改(别人(web-all)先写过就以它为准);
18682
+ * ③ **可失败**:整段包在 try 里,异常绝不影响壁纸/玻璃主路径;观察器只在 document 就绪后装。
18683
+ *
18684
+ * 属性本身是惰性的(宿主 CSS 不用 data-pane),所以可以在启动时就补,不必等皮肤上台。
18685
+ */
18686
+
18687
+ const PANE_ANCHORS = [
18688
+ // 列级槽名优先(实证:这三种锚点的父元素就是列容器)。
18689
+ { pane: 'sidebar', selectors: ['[data-slot="sidebar"]'] },
18690
+ { pane: 'conversation', selectors: ['[data-slot="main"]', '[data-slot="main.conversation"]'] },
18691
+ { pane: 'details', selectors: ['[data-slot="rightbar"]', '[data-slot="rightbar.session"]', '[data-sidebar-right-panel]'] },
18692
+ ];
18693
+
18694
+ /** 从锚点元素上溯(穿过 display:contents 的 `[data-slot]` 锚点),得到"那一列"的容器。 */
18695
+ function paneHost(anchor) {
18696
+ let el = anchor && anchor.parentElement;
18697
+ // 宿主的槽位锚点都是 display:contents 包裹元素(renderSlot 的 ANCHOR_STYLE),
18698
+ // 属性写在它身上没有布局效果 ⇒ 必须一直走到第一个"真盒子"。
18699
+ let guard = 0;
18700
+ while (el && el.hasAttribute && el.hasAttribute('data-slot') && guard < 12) {
18701
+ el = el.parentElement;
18702
+ guard += 1;
18703
+ }
18704
+ if (!el || el === document.body) return null;
18705
+ if (el.id === 'root') return null;
18706
+ if (el.hasAttribute('data-dsh-frame')) return null;
18707
+ return el;
18708
+ }
18709
+
18710
+ function stampPanes() {
18711
+ let stamped = 0;
18712
+ for (const rule of PANE_ANCHORS) {
18713
+ // 每条规则**各自**判断有没有命中:早先用累计计数会让后一条规则在第一条选择器空手时
18714
+ // 直接跳过退路选择器(conversation/details 的退路因此永远不被尝试)。
18715
+ let ruleStamped = 0;
18716
+ for (const selector of rule.selectors) {
18717
+ let nodes;
18718
+ try { nodes = document.querySelectorAll(selector); } catch { continue; }
18719
+ for (const node of nodes) {
18720
+ const host = paneHost(node);
18721
+ if (!host || host.hasAttribute('data-pane')) continue;
18722
+ host.setAttribute('data-pane', rule.pane);
18723
+ stamped += 1;
18724
+ ruleStamped += 1;
18725
+ }
18726
+ if (ruleStamped) break; // 同一 pane 命中一种锚点即可
18727
+ }
18728
+ }
18729
+ return stamped;
18730
+ }
18731
+
18732
+ function stampFrame() {
18733
+ try {
18734
+ if (document.querySelector('[data-dsh-frame]')) return 0;
18735
+ const root = document.getElementById('root') || document.body;
18736
+ if (!root) return 0;
18737
+ // 与 web-all 同口径:**列容器的父元素**就是画框(`sidebarCol.parentElement`);类名只作退路。
18738
+ const sidebarHost = document.querySelector('[data-pane="sidebar"]');
18739
+ const fromColumn = (sidebarHost && sidebarHost.parentElement) || null;
18740
+ const frame = (fromColumn && fromColumn !== document.body && fromColumn.id !== 'root' ? fromColumn : null)
18741
+ || root.querySelector('div[class*="_frame"]')
18742
+ || root.firstElementChild;
18743
+ if (!frame || frame.hasAttribute('data-dsh-frame')) return 0;
18744
+ if (frame === document.body || frame.id === 'root') return 0;
18745
+ frame.setAttribute('data-dsh-frame', '');
18746
+ return 1;
18747
+ } catch { return 0; }
18748
+ }
18749
+
18750
+ function stampAll() {
18751
+ try { return stampPanes() + stampFrame(); } catch { return 0; }
18752
+ }
18753
+
18754
+ /**
18755
+ * 定时器可缺省:**挂载点跑在不保证有定时的宿主里**。
18756
+ * 实测(2026-10-30,`test/verify-playback-controls.mjs` / `verify-transcode-state.mjs`):
18757
+ * 那几个守卫用只实现 `document` 的 fake DOM 直接 apply 客户端,**没有 `setTimeout`** ——
18758
+ * 这里裸调 `setTimeout` 会 `ReferenceError`,而 `ctx.effect(() => feat.mount())` 不做保护,
18759
+ * 异常会顺着 apply 链把**整个宿主插件**带崩(一次 17 条判据全红)。
18760
+ * ⇒ 有就用(优先 `window.setTimeout`,与仓库其它层同口径),没有就**当场做**,绝不抛。
18761
+ */
18762
+ function scheduleAfter(fn, ms) {
18763
+ try {
18764
+ const st = (typeof window !== 'undefined' && typeof window.setTimeout === 'function')
18765
+ ? window.setTimeout.bind(window)
18766
+ : (typeof setTimeout === 'function' ? setTimeout : null);
18767
+ if (!st) { fn(); return null; }
18768
+ return st(fn, ms);
18769
+ } catch { try { fn(); } catch { /* ignore */ } return null; }
18770
+ }
18771
+
18772
+ function cancelAfter(id) {
18773
+ try {
18774
+ if (id === null || id === undefined) return;
18775
+ if (typeof clearTimeout === 'function') clearTimeout(id);
18776
+ else if (typeof window !== 'undefined' && typeof window.clearTimeout === 'function') window.clearTimeout(id);
18777
+ } catch { /* ignore */ }
18778
+ }
18779
+
18780
+ /** 装上兼容层;返回幂等 teardown(与仓库其它 install* 同形)。整段可失败(异常不外溢)。 */
18781
+ function installSkinCompat() {
18782
+ if (typeof document === 'undefined') return () => {};
18783
+ try {
18784
+ stampAll();
18785
+ let observer = null;
18786
+ let timer = null;
18787
+ const schedule = () => {
18788
+ if (timer !== null) return;
18789
+ timer = scheduleAfter(() => { timer = null; stampAll(); }, 120);
18790
+ };
18791
+ try {
18792
+ observer = new MutationObserver(schedule);
18793
+ // 只观察结构性变化(子树增删);不观察属性,避免别人改属性时我们无限自激。
18794
+ observer.observe(document.body, { childList: true, subtree: true });
18795
+ } catch { observer = null; }
18796
+ // 宿主是 SPA:路由切换会整块换掉列容器,所以补一次延迟重扫。
18797
+ const late = scheduleAfter(stampAll, 1500);
18798
+ return () => {
18799
+ try { if (observer) observer.disconnect(); } catch { /* ignore */ }
18800
+ if (timer !== null) { cancelAfter(timer); timer = null; }
18801
+ cancelAfter(late);
18802
+ };
18803
+ } catch { return () => {}; }
18804
+ }
18805
+
18620
18806
  // ── 内联模块:src/features.js —— 客户端功能注册表:页签条 / 整区面板 / 常驻挂载层。装配点(quick-panel、client.js)只查表,不写功能名(构建期注入;勿手改本段)──
18621
18807
  /**
18622
18808
  * features.js(客户端)— **功能注册表**:页签声明、面板工厂、挂载钩子。
@@ -18665,6 +18851,9 @@ window.__ModuleLoader__.load({
18665
18851
  */
18666
18852
  const WE_FEATURE_MOUNTS = [
18667
18853
  { id: "pets", mount: mountPetLayer },
18854
+ // 皮肤兼容层:补写 `data-pane` / `data-dsh-frame`(皮肤包写的是 web-all 的 compat 词表)。
18855
+ // 它只写自己的惰性属性、幂等、可失败,所以随墙纸引擎常驻,不依赖皮肤是否在台上。
18856
+ { id: "skin-compat", mount: installSkinCompat },
18668
18857
  ];
18669
18858
 
18670
18859
  /** 页签 → 功能归属(宿主清单 lib/features.js 的客户端镜像,供守卫对账)。 */
@@ -27189,9 +27378,13 @@ window.__ModuleLoader__.load({
27189
27378
  * 才回收)。所以只认"过一拍还在"的出现:宽限结束时 DOM 仍带标记、且我方确实有要退的东西
27190
27379
  * (壁纸在台 ⇒ 清层;或**无壁纸**且设置已就位 ⇒ 只剩玻璃要让)⇒ 才是动作。
27191
27380
  * ⚠️ 450ms 必须显著短于对方的 1500ms 宽限,否则真实动作会在我们确认前被它回收。
27381
+ *
27382
+ * `opts.forced`:**标记值发生了变化**(换了一套皮肤)—— 那是用户刚做的一次显式动作,
27383
+ * 不存在"启动上妆"的竞态可言,所以连"我方是否有层"这条腿都不用再问(见 syncSkinYield)。
27192
27384
  */
27193
- function scheduleSkinYieldEnter(reason) {
27385
+ function scheduleSkinYieldEnter(reason, opts) {
27194
27386
  if (skinYieldEnterTimer !== null || skinYielded) return;
27387
+ const forced = !!(opts && opts.forced);
27195
27388
  const fire = () => {
27196
27389
  skinYieldEnterTimer = null;
27197
27390
  try {
@@ -27203,8 +27396,11 @@ window.__ModuleLoader__.load({
27203
27396
  // · 无壁纸腿:没有层可清、也没有竞态可防,但玻璃整族仍盖在皮肤上(玻璃与壁纸
27204
27397
  // 正交,data-we-glass-page 恒挂)⇒ 这条腿也要进,否则无壁纸用户中途试穿 /
27205
27398
  // 应用皮肤永远摘不掉玻璃(安装期的 adopt 只管启动那一拍)。
27206
- const wallMarkerOn = !!(document.body && document.body.hasAttribute("data-we-wallpaper"));
27207
- if (!wallMarkerOn && (selection.id || !selection.loaded)) return;
27399
+ // · forced(换皮肤 = 用户刚动作):跳过启动竞态那套判据,直接让路。
27400
+ if (!forced) {
27401
+ const wallMarkerOn = !!(document.body && document.body.hasAttribute("data-we-wallpaper"));
27402
+ if (!wallMarkerOn && (selection.id || !selection.loaded)) return;
27403
+ }
27208
27404
  enterSkinYield(reason + "/dom");
27209
27405
  } catch { /* ignore */ }
27210
27406
  };
@@ -27248,10 +27444,24 @@ window.__ModuleLoader__.load({
27248
27444
  } catch { fire(); }
27249
27445
  }
27250
27446
 
27251
- /** 合议当前状态:皮肤"要求上台"且我方确实在台上 ⇒ 退场;持续不在台上则排定复位。 */
27447
+ /** 上次看到的 `data-dsh-skin` 值(null = 还没看过;用于区分"启动上妆"与"用户换了一套")。 */
27448
+ let skinAttrSeen = null;
27449
+
27450
+ /**
27451
+ * 合议当前状态:皮肤"要求上台"且我方确实在台上 ⇒ 退场;持续不在台上则排定复位。
27452
+ *
27453
+ * **没有优先级,只有"谁最后动的"**(用户明确要求):
27454
+ * · 皮肤标记**值发生变化** = 用户刚换/刚应用了一套皮肤 ⇒ 我方立刻让路(哪怕壁纸正在画);
27455
+ * · 壁纸被选中 = `media-prep` 的钩子会 `exitSkinYield` ⇒ 皮肤侧 withhold、壁纸接管;
27456
+ * · 启动那一拍(值没变)= 不改判:我方有壁纸就让壁纸画(皮肤自己 withhold),
27457
+ * 我方无壁纸且在皮肤上 ⇒ 认领让路态(见 installSkinInterop 的 adoptOnLoaded)。
27458
+ */
27252
27459
  function syncSkinYield(reason) {
27253
27460
  try {
27254
- if (document.documentElement.hasAttribute("data-dsh-skin")) {
27461
+ const attr = document.documentElement.getAttribute("data-dsh-skin");
27462
+ const changed = attr !== skinAttrSeen;
27463
+ skinAttrSeen = attr;
27464
+ if (attr) {
27255
27465
  cancelSkinYieldExit();
27256
27466
  // ⚠️ 「我方在台上」有两条腿:壁纸在台(标记在场 ⇒ 有层要让)与无壁纸(设置已就位
27257
27467
  // 且 id 为空 ⇒ 只剩玻璃要让)。启动竞态只长在壁纸腿上——对方运行时会先于我们的
@@ -27260,7 +27470,8 @@ window.__ModuleLoader__.load({
27260
27470
  // 动作会清掉壁纸(实测报障:有壁纸时刷新掉壁纸)。正确的收场是:让我们的层建起来
27261
27471
  // → 标记挂上 → 对方 watcher 自行 withhold 撤掉皮肤。无壁纸腿没有竞态(没有层可清),
27262
27472
  // 随宽限进即可。不是立即退场:先排定一次"过一拍再确认"(见 scheduleSkinYieldEnter)。
27263
- scheduleSkinYieldEnter(reason);
27473
+ // 但 `changed`(换了一套皮肤)是用户的显式动作,不走那套竞态判据(forced)。
27474
+ scheduleSkinYieldEnter(reason, { forced: changed });
27264
27475
  return;
27265
27476
  }
27266
27477
  cancelSkinYieldEnter();
@@ -27271,6 +27482,9 @@ window.__ModuleLoader__.load({
27271
27482
  /** 装上互操作(单腿:DOM 观察);返回幂等 teardown。 */
27272
27483
  function installSkinInterop() {
27273
27484
  if (typeof document === "undefined" || !document.documentElement) return () => {};
27485
+ // 启动快照:把当前标记记为"已看过",这样**启动那一拍不会被当成一次换皮肤动作**
27486
+ // (否则每次刷新都会误判成用户刚应用皮肤,进而清掉壁纸)。
27487
+ try { skinAttrSeen = document.documentElement.getAttribute("data-dsh-skin"); } catch { skinAttrSeen = null; }
27274
27488
  let observer = null;
27275
27489
  try {
27276
27490
  observer = new MutationObserver(() => syncSkinYield("dom"));
@@ -27280,7 +27494,9 @@ window.__ModuleLoader__.load({
27280
27494
  // · 皮肤在台上(文档交付时 tap 已画)+ 我方**没有**选中的壁纸 ⇒ 本来就该在让路态
27281
27495
  // (玻璃退场)——认领,不清层;记忆从**盘上落的那份**拿(enter 写的
27282
27496
  // skinYieldRestoreId / skinYieldRestoreRotation),没落过盘才退回空记忆;
27283
- // · 皮肤在台上 + 我方有选中的壁纸 ⇒ 壁纸赢(对方的 withhold 会处理皮肤),不动;
27497
+ // · 皮肤在台上 + 我方有选中的壁纸 ⇒ **启动不改判**:保持现状(壁纸画、皮肤自己 withhold)。
27498
+ // 这不是"优先级",而是启动那一拍没有"谁最后动"的信息;运行期任一方动作都会立刻改判
27499
+ // (换皮肤 ⇒ syncSkinYield 的 forced 分支;选壁纸 ⇒ media-prep 的 exitSkinYield)。
27284
27500
  // · 皮肤**已不在**台上 + 我方没有壁纸 + 盘上留着让路记忆 ⇒ 启动复位:按记忆放回
27285
27501
  // (应用关闭期间皮肤被移除 / 卸载的那条路 —— 没有它,记忆落了盘也无人消费)。
27286
27502
  // ⚠️ 认领必须等**宿主设置到位**(`selection.loaded`):本地缓存可能过期(本窗口缓存空、
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-folia",
3
- "version": "4.12.6",
3
+ "version": "4.13.0",
4
4
  "description": "Folia 壁纸引擎:把本机 Wallpaper Engine 壁纸搬进 DSH 界面(场景实时渲染 / 视频 / 网页),内置液态玻璃与 Folia 吉祥物,并可从自建站点拉取壁纸。单包形态(组件已合并)。",
5
5
  "type": "module",
6
6
  "engines": {
@@ -193,7 +193,6 @@
193
193
  }
194
194
  },
195
195
  "dependencies": {
196
- "@linxin666/dsh-client-ui-skin-center": "0.4.8",
197
196
  "webwallgl": "^3.0.0",
198
197
  "x11": "^4.3.1"
199
198
  },