dsh-folia 2.7.0 → 2.9.2
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 +210 -50
- package/lib/routes/skins.js +63 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -16231,38 +16231,80 @@ window.__ModuleLoader__.load({
|
|
|
16231
16231
|
}
|
|
16232
16232
|
|
|
16233
16233
|
/**
|
|
16234
|
-
*
|
|
16234
|
+
* 取一套皮肤**最终该用的 CSS**。
|
|
16235
|
+
*
|
|
16236
|
+
* 关键:皮肤中心不是把皮肤目录里的 CSS 原样发出去的 ——
|
|
16237
|
+
* `GET /api/skin-center/v2/skins/<id>/stylesheet` → `transformSkinCss()`:**强制作用域化**
|
|
16238
|
+
* `GET /api/skin-center/v2/skins/<id>/patches` → 同上(无则 404)
|
|
16239
|
+
* 作用域化 = 把每条规则包进 `html[data-dsh-skin="<id>"]`。这一步直接决定成败:
|
|
16240
|
+
* **包了作用域的规则特异性更高**,才压得过应用自身的面板/令牌规则。我们早先注入的是
|
|
16241
|
+
* 目录里的裸 CSS,特异性与应用同级 ⇒ 大量规则输掉 ⇒ 配色变了一点、面板依旧不透明。
|
|
16242
|
+
*
|
|
16243
|
+
* 所以优先**直接用它加工好的那份**(同源,浏览器自带凭证);只有拿不到才退回我们自己的
|
|
16244
|
+
* `/folia/skins/css`(未作用域化,能变配色但压不住应用规则)。
|
|
16235
16245
|
*
|
|
16236
|
-
*
|
|
16237
|
-
|
|
16238
|
-
|
|
16239
|
-
|
|
16240
|
-
|
|
16246
|
+
* @returns {Promise<{ css: string, source: string }>}
|
|
16247
|
+
*/
|
|
16248
|
+
async function fetchSkinCss(id) {
|
|
16249
|
+
const enc = encodeURIComponent(id);
|
|
16250
|
+
const parts = [];
|
|
16251
|
+
let source = "";
|
|
16252
|
+
|
|
16253
|
+
// ① 宿主端代理由它自己去问皮肤中心(带上本次请求的 Cookie 与同源头)。
|
|
16254
|
+
// 前端直连 `/api/skin-center/...` 会被 DSH 网关挡掉,实测一直落到"未作用域化"。
|
|
16255
|
+
try {
|
|
16256
|
+
const viaHost = await apiFetch("/skins/scoped-css?id=" + enc, { parse: false, cache: "no-store" });
|
|
16257
|
+
if (viaHost && viaHost.ok && viaHost.response) {
|
|
16258
|
+
const text = await viaHost.response.text();
|
|
16259
|
+
if (text && text.trim()) return { css: text, source: "skin-center" };
|
|
16260
|
+
}
|
|
16261
|
+
} catch { /* 代理不可用 ⇒ 继续走下面两条 */ }
|
|
16262
|
+
|
|
16263
|
+
// ② 前端直连(皮肤中心同源路由,能通的话最省一次往返)
|
|
16264
|
+
for (const part of ["stylesheet", "patches"]) {
|
|
16265
|
+
try {
|
|
16266
|
+
// 根相对路径 + 原生请求:桌面壳可能跑在 `dsh-app://` 下(`location.origin` 是 "null"),
|
|
16267
|
+
// 拼绝对 URL 会作废;`/api/...` 交给浏览器按文档解析才可靠
|
|
16268
|
+
//(皮肤中心自己的客户端用的也是这种写法)。
|
|
16269
|
+
const r = await apiRawResponse("/api/skin-center/v2/skins/" + enc + "/" + part);
|
|
16270
|
+
if (r && r.ok) {
|
|
16271
|
+
const text = await r.text();
|
|
16272
|
+
if (text && text.trim()) {
|
|
16273
|
+
parts.push(text);
|
|
16274
|
+
source = "skin-center";
|
|
16275
|
+
}
|
|
16276
|
+
}
|
|
16277
|
+
} catch { /* 拿不到就试下一段 / 退回自己的路由 */ }
|
|
16278
|
+
}
|
|
16279
|
+
if (parts.length > 0) return { css: parts.join("\n/* ---- patches ---- */\n"), source };
|
|
16280
|
+
const r2 = await apiFetch("/skins/css?id=" + enc + "&t=" + Date.now(), { parse: false, cache: "no-store" });
|
|
16281
|
+
if (!r2 || !r2.ok || !r2.response) throw new Error("css-http-" + ((r2 && r2.status) || 0));
|
|
16282
|
+
const text = await r2.response.text();
|
|
16283
|
+
if (!text || !text.trim()) throw new Error("css-empty");
|
|
16284
|
+
return { css: text, source: "folia-raw" };
|
|
16285
|
+
}
|
|
16286
|
+
|
|
16287
|
+
/**
|
|
16288
|
+
* 把一套皮肤的样式**当场**挂进文档。
|
|
16241
16289
|
*
|
|
16242
|
-
*
|
|
16290
|
+
* 用 `<style>` 而不是 `<link>`:`<style>` 一赋值就参与样式计算,且追加到 `<body>` 末尾
|
|
16291
|
+
* ⇒ 文档顺序最后、同等特异性下后写的赢;`<link>` 是异步的,顺序不由我们定。
|
|
16243
16292
|
*
|
|
16244
16293
|
* @param {string} id 皮肤 id
|
|
16245
|
-
* @returns {Promise<{ bytes: number, rules: number }>}
|
|
16294
|
+
* @returns {Promise<{ bytes: number, rules: number, source: string }>}
|
|
16246
16295
|
*/
|
|
16247
16296
|
async function applySkinCss(id) {
|
|
16248
|
-
const
|
|
16249
|
-
parse: false,
|
|
16250
|
-
cache: "no-store",
|
|
16251
|
-
});
|
|
16252
|
-
if (!resp || !resp.ok || !resp.response) throw new Error("css-http-" + ((resp && resp.status) || 0));
|
|
16253
|
-
const css = await resp.response.text();
|
|
16254
|
-
if (!css || !css.trim()) throw new Error("css-empty");
|
|
16297
|
+
const { css, source } = await fetchSkinCss(id);
|
|
16255
16298
|
let el = document.getElementById("folia-skin-hot");
|
|
16256
16299
|
if (!el) {
|
|
16257
16300
|
el = document.createElement("style");
|
|
16258
16301
|
el.id = "folia-skin-hot";
|
|
16259
|
-
// 挂 body 末尾(不是 head):文档顺序最后 ⇒ 同等特异性下覆盖先前的规则。
|
|
16260
16302
|
(document.body || document.head).appendChild(el);
|
|
16261
16303
|
}
|
|
16262
16304
|
el.textContent = css;
|
|
16263
16305
|
let rules = 0;
|
|
16264
16306
|
try { rules = (el.sheet && el.sheet.cssRules && el.sheet.cssRules.length) || 0; } catch { /* 读不到就报 0 */ }
|
|
16265
|
-
return { bytes: css.length, rules };
|
|
16307
|
+
return { bytes: css.length, rules, source };
|
|
16266
16308
|
}
|
|
16267
16309
|
|
|
16268
16310
|
/**
|
|
@@ -16320,12 +16362,13 @@ window.__ModuleLoader__.load({
|
|
|
16320
16362
|
}
|
|
16321
16363
|
el.src = pick.url;
|
|
16322
16364
|
// 皮肤 CSS 通过 `> img` / `> video` 选中它 ⇒ 必须是**直接子元素**。
|
|
16365
|
+
el.setAttribute("aria-hidden", "true");
|
|
16323
16366
|
el.style.cssText = "position:absolute;inset:0;width:100%;height:100%;object-fit:cover;";
|
|
16324
16367
|
layer.appendChild(el);
|
|
16325
16368
|
|
|
16326
16369
|
if (pick.scrim) {
|
|
16327
16370
|
const scrim = document.createElement("div");
|
|
16328
|
-
scrim.style.cssText = "position:absolute;inset:0;pointer-events:none;background
|
|
16371
|
+
scrim.style.cssText = "position:absolute;inset:0;pointer-events:none;background:" + pick.scrim + ";";
|
|
16329
16372
|
layer.appendChild(scrim);
|
|
16330
16373
|
}
|
|
16331
16374
|
|
|
@@ -16352,6 +16395,24 @@ window.__ModuleLoader__.load({
|
|
|
16352
16395
|
*/
|
|
16353
16396
|
|
|
16354
16397
|
const SKIN_LAYER_NAMES = ["background", "ambient", "bottom", "sidebar", "top", "foreground"];
|
|
16398
|
+
/**
|
|
16399
|
+
* 六层的**精确**样式:逐字对齐皮肤中心 `runtime/decoration-layers.ts` 的 `LAYER_STYLE`。
|
|
16400
|
+
*
|
|
16401
|
+
* 不能自己拍脑袋定层级 —— 它的注释写明了为什么:
|
|
16402
|
+
* · background 用 **-2**:负 z-index 元素画在 html/body 背景**之上**(皮肤自己的
|
|
16403
|
+
* 不透明底色因此在画后面、艺术图在它前面),但**在所有面板之下**;
|
|
16404
|
+
* · ambient 30、top/bottom/sidebar 40、foreground 41:压在面板之上、官方浮层
|
|
16405
|
+
* (>=1000)之下。之前我把非背景层留成 auto ⇒ 装饰条落进文档流最底层,
|
|
16406
|
+
* 表现正是"上面被面板盖住、只有下面露出背景"。
|
|
16407
|
+
*/
|
|
16408
|
+
const SKIN_LAYER_STYLE = {
|
|
16409
|
+
background: "position:fixed;top:0;right:0;bottom:0;left:0;z-index:-2;pointer-events:none;will-change:transform;",
|
|
16410
|
+
ambient: "position:fixed;top:0;right:0;bottom:0;left:0;z-index:30;pointer-events:none;",
|
|
16411
|
+
top: "position:fixed;top:0;left:0;right:0;z-index:40;pointer-events:none;",
|
|
16412
|
+
bottom: "position:fixed;bottom:0;left:0;right:0;z-index:40;pointer-events:none;",
|
|
16413
|
+
sidebar: "position:fixed;top:0;bottom:0;left:0;z-index:40;pointer-events:none;",
|
|
16414
|
+
foreground: "position:fixed;top:0;right:0;bottom:0;left:0;z-index:41;pointer-events:none;",
|
|
16415
|
+
};
|
|
16355
16416
|
/** 本次激活注册的清理函数(切换皮肤时逐个跑掉,幂等)。 */
|
|
16356
16417
|
let skinHotCleanups = [];
|
|
16357
16418
|
|
|
@@ -16373,14 +16434,17 @@ window.__ModuleLoader__.load({
|
|
|
16373
16434
|
if (!el) {
|
|
16374
16435
|
el = document.createElement("div");
|
|
16375
16436
|
el.setAttribute("data-dsh-skin-layer", name);
|
|
16376
|
-
|
|
16377
|
-
//(`background` 是契约里明写 z-index -2 的那层,其余留 auto)。
|
|
16378
|
-
el.style.cssText = "position:fixed;inset:0;pointer-events:none;"
|
|
16379
|
-
+ (name === "background" ? "z-index:-2;" : "");
|
|
16437
|
+
el.setAttribute("aria-hidden", "true");
|
|
16380
16438
|
host.appendChild(el);
|
|
16381
16439
|
}
|
|
16440
|
+
// 与它的 ensureOne 同口径:**每次都对已有层重写样式** —— 皮肤中心的层可能带着
|
|
16441
|
+
// 上一次激活留下的内联覆盖,只建不改会继承脏值。
|
|
16442
|
+
el.style.cssText = SKIN_LAYER_STYLE[name];
|
|
16382
16443
|
out[name] = el;
|
|
16383
16444
|
}
|
|
16445
|
+
// 皮肤中心自己会给 body 盖 `data-dsh-skin-center`(它的若干基础样式挂在这个前缀下),
|
|
16446
|
+
// 缺了它,面板/列表那些"表面透明"的规则不生效 → 大片面板仍然不透明。
|
|
16447
|
+
try { document.body.setAttribute("data-dsh-skin-center", ""); } catch { /* ignore */ }
|
|
16384
16448
|
return out;
|
|
16385
16449
|
}
|
|
16386
16450
|
|
|
@@ -16458,6 +16522,84 @@ window.__ModuleLoader__.load({
|
|
|
16458
16522
|
} catch { /* ignore */ }
|
|
16459
16523
|
}
|
|
16460
16524
|
|
|
16525
|
+
/**
|
|
16526
|
+
* 把一套皮肤**完整**应用上去(作用域 + 六层 + 样式 + 背景 + hooks)。
|
|
16527
|
+
* 按钮和"壁纸撤下后自动恢复"走的是**同一个**函数 —— 两条路径不能各写一遍。
|
|
16528
|
+
*/
|
|
16529
|
+
async function applySkinHot(id) {
|
|
16530
|
+
clearSkinHot();
|
|
16531
|
+
try { document.documentElement.setAttribute("data-dsh-skin", id); } catch { /* ignore */ }
|
|
16532
|
+
ensureSkinLayers();
|
|
16533
|
+
const info = await applySkinCss(id);
|
|
16534
|
+
const bg = await applySkinBackground(id);
|
|
16535
|
+
let hooksInfo = { ran: false, reason: "skipped" };
|
|
16536
|
+
try {
|
|
16537
|
+
hooksInfo = await applySkinHooks(id);
|
|
16538
|
+
} catch (e) {
|
|
16539
|
+
hooksInfo = { ran: false, reason: String((e && e.message) || e) };
|
|
16540
|
+
}
|
|
16541
|
+
return { info, bg, hooks: hooksInfo };
|
|
16542
|
+
}
|
|
16543
|
+
|
|
16544
|
+
/** 把皮肤**整个撤掉**,回到应用原生外观(壁纸接管背景时走这条)。 */
|
|
16545
|
+
function teardownSkin() {
|
|
16546
|
+
clearSkinHot();
|
|
16547
|
+
try { document.documentElement.removeAttribute("data-dsh-skin"); } catch { /* ignore */ }
|
|
16548
|
+
try { document.body.removeAttribute("data-dsh-skin-center"); } catch { /* ignore */ }
|
|
16549
|
+
for (const name of SKIN_LAYER_NAMES) {
|
|
16550
|
+
try {
|
|
16551
|
+
const el = document.querySelector('[data-dsh-skin-layer="' + name + '"]');
|
|
16552
|
+
if (el) el.innerHTML = "";
|
|
16553
|
+
} catch { /* ignore */ }
|
|
16554
|
+
}
|
|
16555
|
+
}
|
|
16556
|
+
|
|
16557
|
+
/** 向宿主问"当前选中的皮肤是哪个"(没选中返回空串)。 */
|
|
16558
|
+
async function activeSkinIdFromHost() {
|
|
16559
|
+
try {
|
|
16560
|
+
const resp = await apiJson("/skins");
|
|
16561
|
+
const doc = (resp && resp.data) || null;
|
|
16562
|
+
return (doc && typeof doc.active === "string") ? doc.active : "";
|
|
16563
|
+
} catch {
|
|
16564
|
+
return "";
|
|
16565
|
+
}
|
|
16566
|
+
}
|
|
16567
|
+
|
|
16568
|
+
let skinArbiterStarted = false;
|
|
16569
|
+
/**
|
|
16570
|
+
* **皮肤与壁纸互斥的仲裁器**(两个方向都要管)。
|
|
16571
|
+
*
|
|
16572
|
+
* 现状:点「使用」时我们会主动清掉壁纸(一个方向);但用户**反过来**点壁纸时,
|
|
16573
|
+
* 没人撤掉我们热注入的皮肤 —— 于是皮肤那层不透明样式压在壁纸上面,壁纸看起来被"遮挡"。
|
|
16574
|
+
*
|
|
16575
|
+
* 这里盯 `body[data-we-wallpaper]` 这个标记(**壁纸插件自己在挂/摘**,是双方共同的事实源):
|
|
16576
|
+
* · 标记出现 ⇒ 撤掉皮肤(teardownSkin)
|
|
16577
|
+
* · 标记消失 ⇒ 若选中过皮肤,重新完整应用(applySkinHot)
|
|
16578
|
+
* 这样无论用户先点哪边,最后都是"只有一个在台"。
|
|
16579
|
+
*/
|
|
16580
|
+
function startSkinArbiter() {
|
|
16581
|
+
if (skinArbiterStarted) return;
|
|
16582
|
+
const body = (typeof document !== "undefined") ? document.body : null;
|
|
16583
|
+
if (!body) { setTimeout(startSkinArbiter, 400); return; }
|
|
16584
|
+
skinArbiterStarted = true;
|
|
16585
|
+
let seq = 0;
|
|
16586
|
+
const sync = async () => {
|
|
16587
|
+
const mine = ++seq;
|
|
16588
|
+
const wallpaperOn = body.hasAttribute("data-we-wallpaper");
|
|
16589
|
+
if (wallpaperOn) { teardownSkin(); return; }
|
|
16590
|
+
const id = await activeSkinIdFromHost();
|
|
16591
|
+
if (mine !== seq) return; // 期间又变了,放弃这次
|
|
16592
|
+
if (!id) return;
|
|
16593
|
+
try { await applySkinHot(id); } catch { /* 失败就保持原生外观 */ }
|
|
16594
|
+
};
|
|
16595
|
+
try {
|
|
16596
|
+
new MutationObserver(() => { sync().catch(() => {}); })
|
|
16597
|
+
.observe(body, { attributes: true, attributeFilter: ["data-we-wallpaper"] });
|
|
16598
|
+
// 启动时就跑一次:处理"上次退出时壁纸在台、皮肤被让位"这类残留状态
|
|
16599
|
+
sync().catch(() => {});
|
|
16600
|
+
} catch { /* 环境不支持观察就只保留按钮路径 */ }
|
|
16601
|
+
}
|
|
16602
|
+
|
|
16461
16603
|
function SkinsTabPanel(props) {
|
|
16462
16604
|
// 壁纸让位:挂载点(设置页 / 首页抽屉)各自把"清掉当前壁纸"的回调传进来 ——
|
|
16463
16605
|
// 皮肤和壁纸都在铺背景,必须二选一。本组件不直接碰设置,只调用传进来的入口。
|
|
@@ -16523,51 +16665,36 @@ window.__ModuleLoader__.load({
|
|
|
16523
16665
|
setNote({ kind: "ok", text: weT("已安装:") + label });
|
|
16524
16666
|
await load(true);
|
|
16525
16667
|
} else if (kind === "activate") {
|
|
16526
|
-
//
|
|
16527
|
-
|
|
16528
|
-
// 这一行就是判断"是没注入"还是"注入了但被压住"的依据,不用再猜。
|
|
16668
|
+
// **当场换肤**:作用域 + 六层 + 样式 + 背景 + hooks,全在 applySkinHot 里
|
|
16669
|
+
//(与"壁纸撤下后自动恢复"共用同一实现,两条路径不会漂移)。
|
|
16529
16670
|
let info = null;
|
|
16530
16671
|
let applyErr = "";
|
|
16531
16672
|
let bgKind = "none";
|
|
16532
16673
|
let hooksInfo = { ran: false, reason: "skipped" };
|
|
16533
16674
|
try {
|
|
16534
|
-
// ①
|
|
16535
|
-
|
|
16536
|
-
|
|
16537
|
-
|
|
16538
|
-
try { document.documentElement.setAttribute("data-dsh-skin", id); } catch { /* ignore */ }
|
|
16539
|
-
// ③ 六个装饰层(hooks 与部分皮肤 CSS 都要用)
|
|
16540
|
-
ensureSkinLayers();
|
|
16541
|
-
// ④ 样式、⑤ 背景、⑥ hooks
|
|
16542
|
-
info = await applySkinCss(id);
|
|
16543
|
-
const bg = await applySkinBackground(id);
|
|
16544
|
-
bgKind = bg.kind;
|
|
16545
|
-
try {
|
|
16546
|
-
hooksInfo = await applySkinHooks(id);
|
|
16547
|
-
} catch (e) {
|
|
16548
|
-
hooksInfo = { ran: false, reason: String((e && e.message) || e) };
|
|
16675
|
+
// ① 壁纸先让位:清掉 `id` 之后壁纸层撤下、`data-we-wallpaper` 消失,
|
|
16676
|
+
// 皮肤中心也不会再扣住皮肤。(反方向由 startSkinArbiter 负责。)
|
|
16677
|
+
if (typeof onClearWallpaper === "function") {
|
|
16678
|
+
try { await Promise.resolve(onClearWallpaper()); } catch { /* 清不掉也继续换肤 */ }
|
|
16549
16679
|
}
|
|
16680
|
+
// ② 应用皮肤
|
|
16681
|
+
const applied = await applySkinHot(id);
|
|
16682
|
+
info = applied.info;
|
|
16683
|
+
bgKind = applied.bg.kind;
|
|
16684
|
+
hooksInfo = applied.hooks;
|
|
16550
16685
|
} catch (e) {
|
|
16551
16686
|
applyErr = String((e && e.message) || e);
|
|
16552
16687
|
}
|
|
16553
|
-
// ⑦ 壁纸让位:皮肤和壁纸都在铺背景,必须互斥。清掉 `id` 之后壁纸层会自己撤下
|
|
16554
|
-
// (同时 `data-we-wallpaper` 标记消失,皮肤中心也不会再扣住皮肤)。
|
|
16555
|
-
let wallpaperCleared = false;
|
|
16556
|
-
try {
|
|
16557
|
-
if (typeof onClearWallpaper === "function") {
|
|
16558
|
-
await Promise.resolve(onClearWallpaper());
|
|
16559
|
-
wallpaperCleared = true;
|
|
16560
|
-
}
|
|
16561
|
-
} catch { /* 清壁纸失败不影响换肤本身 */ }
|
|
16562
16688
|
setNote({
|
|
16563
16689
|
kind: applyErr ? "err" : "ok",
|
|
16564
16690
|
text: applyErr
|
|
16565
16691
|
? weT("已切换为「") + label + weT("」,但样式注入失败:") + applyErr
|
|
16566
16692
|
: weT("已切换到「") + label + weT("」—— 样式 ")
|
|
16567
16693
|
+ Math.round(info.bytes / 1024) + " KB / " + info.rules + weT(" 条规则")
|
|
16694
|
+
+ (info.source === "skin-center" ? weT("(已作用域化)") : weT("(未作用域化,可能显示不全)"))
|
|
16568
16695
|
+ (bgKind === "none" ? weT(",无背景媒体") : weT(",背景:") + bgKind)
|
|
16569
16696
|
+ (hooksInfo.ran ? weT(",动效已启动") : (hooksInfo.reason === "no-hooks" ? weT(",无动效") : weT(",动效未启动(") + hooksInfo.reason + ")"))
|
|
16570
|
-
+ (
|
|
16697
|
+
+ (typeof onClearWallpaper === "function" ? weT(",壁纸已关闭") : ""),
|
|
16571
16698
|
// hooks 已经跑过了;这里只作为"整页重载"的可选入口(极少数皮肤依赖启动期布局)
|
|
16572
16699
|
reload: true,
|
|
16573
16700
|
});
|
|
@@ -16792,6 +16919,11 @@ window.__ModuleLoader__.load({
|
|
|
16792
16919
|
return React.createElement(SkinsTabPanel, props || null);
|
|
16793
16920
|
}
|
|
16794
16921
|
|
|
16922
|
+
// 模块加载即启动互斥仲裁(不依赖用户是否打开过皮肤包页签):
|
|
16923
|
+
// 壁纸在台 ⇒ 撤掉皮肤;壁纸撤下 ⇒ 恢复选中的皮肤。
|
|
16924
|
+
// 放在模块作用域而不是 React effect 里,是因为它必须在**任何面板渲染之前**就守住这个不变量。
|
|
16925
|
+
try { startSkinArbiter(); } catch { /* 环境异常时退化为纯按钮路径 */ }
|
|
16926
|
+
|
|
16795
16927
|
// ── 内联模块:src/picker-model.js —— 选择器模型层:过滤判定 + 派生数据 + 分页切片(纯函数 / 显式入参 / 零 DOM)(构建期注入;勿手改本段)──
|
|
16796
16928
|
/**
|
|
16797
16929
|
* picker-model.js — 壁纸选择器的**模型层**:过滤判定 + 派生数据 + 分页切片。
|
|
@@ -22765,6 +22897,34 @@ window.__ModuleLoader__.load({
|
|
|
22765
22897
|
/** DELETE(多数接口不返回体;需要时传 parse: true)。 */
|
|
22766
22898
|
const apiDelete = (path, options) => apiFetch(path, Object.assign({ method: 'DELETE', parse: false }, options));
|
|
22767
22899
|
|
|
22900
|
+
/**
|
|
22901
|
+
* 根相对路径的**原生**请求(**不补 BASE**),返回原始 `Response`(失败给 null)。
|
|
22902
|
+
*
|
|
22903
|
+
* 用途:访问**同源其它插件**的路由 —— 典型是皮肤中心的 `/api/skin-center/v2/...`
|
|
22904
|
+
*(它把皮肤 CSS 做了强制作用域化,只有那条路由给的版本才能压过应用自身规则)。
|
|
22905
|
+
*
|
|
22906
|
+
* 为什么必须是**根相对路径**而不是 `location.origin + path`:桌面壳里文档可能跑在
|
|
22907
|
+
* 自定义协议下(`dsh-app://…`),此时 `location.origin` 是 `"null"`,拼出来的绝对 URL
|
|
22908
|
+
* 直接作废。`/api/...` 交给浏览器按文档解析,两种协议下都成立 —— 皮肤中心自己的客户端
|
|
22909
|
+
* 用的也正是这种写法。
|
|
22910
|
+
*
|
|
22911
|
+
* @param {string} path 以 `/` 开头的同源路径
|
|
22912
|
+
* @param {{ method?: string, cache?: string, headers?: object, fetch?: Function }} [options]
|
|
22913
|
+
* @returns {Promise<Response|null>}
|
|
22914
|
+
*/
|
|
22915
|
+
async function apiRawResponse(path, options) {
|
|
22916
|
+
const o = options || {};
|
|
22917
|
+
const doFetch = pickFetch(o.fetch);
|
|
22918
|
+
if (!doFetch) return null;
|
|
22919
|
+
const init = { method: o.method || 'GET', cache: o.cache || 'no-store' };
|
|
22920
|
+
if (o.headers) init.headers = o.headers;
|
|
22921
|
+
try {
|
|
22922
|
+
return await doFetch(String(path), init);
|
|
22923
|
+
} catch {
|
|
22924
|
+
return null;
|
|
22925
|
+
}
|
|
22926
|
+
}
|
|
22927
|
+
|
|
22768
22928
|
// ── 内联模块:src/occlusion.js —— 遮挡投递:/occlusion 轮询 + 缩放 + postMessage(1Hz,含退避与可见性暂停)(构建期注入;勿手改本段)──
|
|
22769
22929
|
/**
|
|
22770
22930
|
* occlusion.js — Linux 壳窗遮挡的**客户端轮询器**:宿主 `/occlusion` 快照 → 画布局部
|
package/lib/routes/skins.js
CHANGED
|
@@ -680,6 +680,69 @@ export function registerSkinRoutes(webServer, c) {
|
|
|
680
680
|
},
|
|
681
681
|
}));
|
|
682
682
|
|
|
683
|
+
/* ── 皮肤中心「加工后 CSS」的宿主端代理 ──────────────────────────────────────
|
|
684
|
+
* 皮肤中心的 `/skins/<id>/stylesheet|patches` 是 `transformSkinCss()` 的产物:
|
|
685
|
+
* **每条规则被强制包进 `html[data-dsh-skin="<id>"]`**,特异性高于应用自身规则 ——
|
|
686
|
+
* 只有那份 CSS 才能真正把面板压透。皮肤目录里的原始 CSS 压不住,于是"显示不全"。
|
|
687
|
+
*
|
|
688
|
+
* 为什么需要代理:浏览器直接请求 `/api/skin-center/v2/...` 被 DSH 网关挡掉
|
|
689
|
+
*(实测前端一直落到"未作用域化"的降级分支)。宿主端在本机回环上再发一次、并**带上
|
|
690
|
+
* 浏览器这次请求的 Cookie 与同源头**,就能拿到那份产物。
|
|
691
|
+
*
|
|
692
|
+
* 拿不到(皮肤中心没装 / 该皮肤不在它的 catalog 里)一律 404,客户端据此回退到原始 CSS。
|
|
693
|
+
*/
|
|
694
|
+
disposers.push(webServer.register({
|
|
695
|
+
kind: 'exact',
|
|
696
|
+
path: `${base}/skins/scoped-css`,
|
|
697
|
+
handler: async (req, res) => {
|
|
698
|
+
const url = new URL(req.url || '/', 'http://x');
|
|
699
|
+
const id = url.searchParams.get('id') || '';
|
|
700
|
+
if (!ID_RE.test(id)) { res.statusCode = 400; res.end(); return; }
|
|
701
|
+
const host = req && req.headers && req.headers.host;
|
|
702
|
+
if (!host) { res.statusCode = 500; res.end(); return; }
|
|
703
|
+
const origin = 'http://' + host;
|
|
704
|
+
// 透传**全部**入站头(网关认的是这次会话的凭证,可能是 cookie,也可能是某个自定义
|
|
705
|
+
// 头;只挑几个字段必漏),再去掉逐跳头并补上同源语义。
|
|
706
|
+
// 实测:裸请求与"只带 sec-fetch-*"都回 401 ⇒ 凭证不在这几个头里。
|
|
707
|
+
const HOP_BY_HOP = new Set([
|
|
708
|
+
'host', 'connection', 'keep-alive', 'transfer-encoding', 'upgrade', 'te', 'trailer',
|
|
709
|
+
'proxy-authorization', 'proxy-authenticate', 'content-length', 'accept-encoding',
|
|
710
|
+
]);
|
|
711
|
+
const fwdHeaders = {};
|
|
712
|
+
for (const [k, v] of Object.entries(req.headers || {})) {
|
|
713
|
+
const key = k.toLowerCase();
|
|
714
|
+
if (HOP_BY_HOP.has(key)) continue;
|
|
715
|
+
if (typeof v === 'string') fwdHeaders[key] = v;
|
|
716
|
+
else if (Array.isArray(v)) fwdHeaders[key] = v.join(', ');
|
|
717
|
+
}
|
|
718
|
+
fwdHeaders.origin = origin;
|
|
719
|
+
fwdHeaders.referer = origin + '/';
|
|
720
|
+
fwdHeaders['sec-fetch-site'] = 'same-origin';
|
|
721
|
+
fwdHeaders['sec-fetch-mode'] = 'cors';
|
|
722
|
+
fwdHeaders['sec-fetch-dest'] = 'empty';
|
|
723
|
+
fwdHeaders.accept = 'text/css,*/*;q=0.1';
|
|
724
|
+
|
|
725
|
+
for (const part of ['stylesheet', 'patches']) {
|
|
726
|
+
try {
|
|
727
|
+
const r = await fetch(origin + '/api/skin-center/v2/skins/' + encodeURIComponent(id) + '/' + part, {
|
|
728
|
+
headers: fwdHeaders,
|
|
729
|
+
signal: AbortSignal.timeout(SKINS_TIMEOUT_MS),
|
|
730
|
+
});
|
|
731
|
+
if (!r.ok) continue;
|
|
732
|
+
const text = await r.text();
|
|
733
|
+
if (!text || !text.trim()) continue;
|
|
734
|
+
res.setHeader('Content-Type', 'text/css; charset=utf-8');
|
|
735
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
736
|
+
res.setHeader('X-Folia-Scoped', '1');
|
|
737
|
+
res.end(text);
|
|
738
|
+
return;
|
|
739
|
+
} catch { /* 这一段失败就试下一段 */ }
|
|
740
|
+
}
|
|
741
|
+
res.statusCode = 404;
|
|
742
|
+
res.end();
|
|
743
|
+
},
|
|
744
|
+
}));
|
|
745
|
+
|
|
683
746
|
disposers.push(webServer.register({
|
|
684
747
|
kind: 'exact',
|
|
685
748
|
path: `${base}/skins/asset`,
|