dsh-folia 4.12.4 → 4.12.6
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 +6 -0
- package/lib/client.js +46 -20
- package/lib/routes/skins.js +171 -7
- package/package.json +2 -2
package/cordis.patch.yml
CHANGED
|
@@ -33,5 +33,11 @@
|
|
|
33
33
|
# ⇒ 自检去仓库 node_modules 里解析它,开发树里没有这个依赖就直接抛,整个 apply 失败。
|
|
34
34
|
# 所以这里用不撞前缀的 `skin-center`(也与 web-all 的 `web-ui-skin-center`、
|
|
35
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 再放开版本范围。
|
|
36
42
|
- id: skin-center
|
|
37
43
|
name: '@linxin666/dsh-client-ui-skin-center'
|
package/lib/client.js
CHANGED
|
@@ -18317,14 +18317,13 @@ 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
|
-
//
|
|
18322
|
-
//
|
|
18323
|
-
// 等于宿主 HTTP API 的源;拼错会得到一个不存在的地址,而它的 404 还会被误读成业务 404。
|
|
18324
|
-
// api-client 对 `/api/**` 原样放行(不补 /folia 前缀)。
|
|
18320
|
+
// 读"当前在用哪套皮肤":**先直连**(同源相对路径 `/api/**`,凭据天然带全),
|
|
18321
|
+
// 直连不通(页面的 origin 可能不等于宿主 HTTP 服务的 origin ⇒ 路由没到)再退回宿主代理
|
|
18322
|
+
// `/folia/skins/active` —— 代理会把浏览器这一侧的 cookie 与同源头转发过去。
|
|
18325
18323
|
if (doc.skinCenter === "present") {
|
|
18326
18324
|
try {
|
|
18327
|
-
|
|
18325
|
+
let a = await apiJson("/api/skin-center/v2/active", { parse: "always" });
|
|
18326
|
+
if (!(a && a.ok)) a = await apiJson("/skins/active", { parse: "always" });
|
|
18328
18327
|
if (a && a.ok) doc.active = (a.data && a.data.active) || "";
|
|
18329
18328
|
} catch { /* 读不到就显示"无使用中" */ }
|
|
18330
18329
|
}
|
|
@@ -18361,16 +18360,27 @@ window.__ModuleLoader__.load({
|
|
|
18361
18360
|
return;
|
|
18362
18361
|
}
|
|
18363
18362
|
}
|
|
18364
|
-
//
|
|
18365
|
-
//
|
|
18366
|
-
//
|
|
18367
|
-
//
|
|
18368
|
-
|
|
18363
|
+
// 写入有两条路,**先直连、再代理**:
|
|
18364
|
+
// ① 直连:相对 `/api/skin-center/v2/active`(同源、凭据天然带全)。
|
|
18365
|
+
// 前提是页面的 origin 就是宿主 HTTP 服务那个源;桌面端内嵌页面的 origin 未必是它 ⇒ 路由没到 ⇒ 404。
|
|
18366
|
+
// ② 代理:`/folia/skins/activate`,宿主把浏览器这侧的 cookie 与同源头转发给皮肤中心。
|
|
18367
|
+
// 这条在"origin 不一致"时仍然成立,所以作为兜底存在。
|
|
18368
|
+
// 两条都失败时把两边的状态码都显示出来 —— 下一次不用再猜是哪一段断的。
|
|
18369
|
+
const direct = await apiPostJson("/api/skin-center/v2/active", { active: wantId }, { parse: "always" });
|
|
18370
|
+
let resp = direct;
|
|
18369
18371
|
if (!(resp && resp.ok)) {
|
|
18370
|
-
|
|
18371
|
-
|
|
18372
|
-
|
|
18373
|
-
|
|
18372
|
+
resp = await apiPostJson("/skins/activate", { id: wantId || "" }, { parse: "always" });
|
|
18373
|
+
}
|
|
18374
|
+
if (!(resp && resp.ok)) {
|
|
18375
|
+
const up = (resp && resp.data && resp.data.upstream) || null;
|
|
18376
|
+
const det = (up && up.error) ? ":" + up.error
|
|
18377
|
+
: ((resp && resp.data && resp.data.error) ? ":" + resp.data.error : "");
|
|
18378
|
+
setNote({
|
|
18379
|
+
ok: false,
|
|
18380
|
+
text: weT("操作失败:{code}", {
|
|
18381
|
+
code: "direct=" + ((direct && direct.status) || 0) + " proxy=" + ((resp && resp.status) || 0) + det,
|
|
18382
|
+
}),
|
|
18383
|
+
});
|
|
18374
18384
|
return;
|
|
18375
18385
|
}
|
|
18376
18386
|
// 两条收敛路径:① 广播皮肤中心自己监听的 `dsh-skin-applied`(立即就地切换);
|
|
@@ -18436,21 +18446,33 @@ window.__ModuleLoader__.load({
|
|
|
18436
18446
|
// 不在 → 明说它是个**独立插件**(不在 dsh-web-all 里),给一条可复制的安装命令
|
|
18437
18447
|
// unknown(探测失败)→ 保守地按"在"讲,不吓唬用户
|
|
18438
18448
|
const skinCenter = (state && state.skinCenter) || "unknown";
|
|
18449
|
+
// `present` 是**唯一**允许点「应用」的状态:只有真的拿到 catalog JSON 才算它在跑。
|
|
18450
|
+
// `absent`=路由不存在(插件没装/未启用);`auth-gated`=鉴权在路由前面挡住(无法判定);
|
|
18451
|
+
// `unknown`=探测失败。后三种都如实告诉用户"现在点应用不会成功",而不是让他点了看 404。
|
|
18452
|
+
const canApply = skinCenter === "present";
|
|
18439
18453
|
if (skinCenter === "absent") {
|
|
18440
18454
|
children.push(React.createElement("div", {
|
|
18441
18455
|
key: "hint-absent",
|
|
18442
18456
|
className: "we-picker__hint",
|
|
18443
18457
|
style: { margin: "6px 0 4px", color: "#ffd08a", lineHeight: "1.6" },
|
|
18444
|
-
}, weT("
|
|
18458
|
+
}, weT("皮肤中心**没在运行**(它的 /api/skin-center/v2/* 路由不存在)。它是独立插件 @linxin666/dsh-client-ui-skin-center;常见原因是版本与当前桌面端不匹配 —— 0.4.9+ 要求 dsh ≥ 0.2.1-rc.3,而本机是 rc.2,市场会把这条行保持为 off。装/降级到 0.4.8 即可:")));
|
|
18445
18459
|
children.push(React.createElement("div", { key: "cmd", style: { display: "flex", gap: "8px", alignItems: "center", margin: "0 0 6px" } },
|
|
18446
18460
|
React.createElement("code", {
|
|
18447
18461
|
style: { fontSize: "11px", padding: "3px 8px", borderRadius: "6px", background: "rgba(255,255,255,0.08)" },
|
|
18448
|
-
}, "dsh plugin --profile web add @linxin666/dsh-client-ui-skin-center"),
|
|
18462
|
+
}, "dsh plugin --profile web add @linxin666/dsh-client-ui-skin-center@0.4.8"),
|
|
18449
18463
|
React.createElement("button", {
|
|
18450
18464
|
className: "we-picker__btn", type: "button",
|
|
18451
|
-
onClick: () => { try { navigator.clipboard.writeText("dsh plugin --profile web add @linxin666/dsh-client-ui-skin-center"); setNote({ ok: true, text: weT("已复制安装命令") }); } catch { setNote({ ok: false, text: weT("复制失败,请手动选中上面的命令") }); } },
|
|
18465
|
+
onClick: () => { try { navigator.clipboard.writeText("dsh plugin --profile web add @linxin666/dsh-client-ui-skin-center@0.4.8"); setNote({ ok: true, text: weT("已复制安装命令") }); } catch { setNote({ ok: false, text: weT("复制失败,请手动选中上面的命令") }); } },
|
|
18452
18466
|
}, weT("复制命令")),
|
|
18453
18467
|
));
|
|
18468
|
+
} else if (skinCenter === "auth-gated" || skinCenter === "unknown") {
|
|
18469
|
+
children.push(React.createElement("div", {
|
|
18470
|
+
key: "hint-unknown",
|
|
18471
|
+
className: "we-picker__hint",
|
|
18472
|
+
style: { margin: "6px 0 4px", color: "#ffd08a", lineHeight: "1.6" },
|
|
18473
|
+
}, skinCenter === "auth-gated"
|
|
18474
|
+
? weT("探测不到皮肤中心的状态(它的接口被会话鉴权挡住了,宿主拿不到会话)。先点一次「刷新」;若下面点「应用」仍报错,就按 0.4.8 版本重装它。")
|
|
18475
|
+
: weT("皮肤中心状态未知(探测超时/网络错误)。点「刷新」重试。")));
|
|
18454
18476
|
} else {
|
|
18455
18477
|
children.push(React.createElement("div", {
|
|
18456
18478
|
key: "hint",
|
|
@@ -18458,6 +18480,8 @@ window.__ModuleLoader__.load({
|
|
|
18458
18480
|
style: { margin: "6px 0 4px", opacity: 0.8 },
|
|
18459
18481
|
}, weT("本页签可以直接切换皮肤:装完点「应用」即可,无需打开设置页。皮肤包落在皮肤中心目录,两边读同一份。")));
|
|
18460
18482
|
}
|
|
18483
|
+
// 皮肤中心不在跑时,「应用」按钮直接禁用并给出原因 —— 比"点了报错"诚实。
|
|
18484
|
+
const applyBlockedReason = canApply ? "" : weT("皮肤中心没在运行,先按上面的命令装/降级到 0.4.8");
|
|
18461
18485
|
|
|
18462
18486
|
// 提示行常驻占位(与开机动画/桌宠页签同一条纪律:不因 note 出现/消失让面板跳高度)
|
|
18463
18487
|
children.push(React.createElement("div", {
|
|
@@ -18526,7 +18550,8 @@ window.__ModuleLoader__.load({
|
|
|
18526
18550
|
? React.createElement("span", { style: { ...SKIN_STYLE.badge, background: "rgba(60,160,90,0.55)" } }, weT("使用中"))
|
|
18527
18551
|
: React.createElement("button", {
|
|
18528
18552
|
className: "we-picker__btn", type: "button",
|
|
18529
|
-
disabled: Boolean(busy),
|
|
18553
|
+
disabled: Boolean(busy) || !canApply,
|
|
18554
|
+
title: applyBlockedReason || undefined,
|
|
18530
18555
|
onClick: () => act("activate", { id: s.id }, s.name),
|
|
18531
18556
|
}, busyThis ? weT("切换中…") : weT("应用")),
|
|
18532
18557
|
React.createElement("button", {
|
|
@@ -18572,7 +18597,8 @@ window.__ModuleLoader__.load({
|
|
|
18572
18597
|
: React.createElement("button", {
|
|
18573
18598
|
key: "apply",
|
|
18574
18599
|
className: "we-picker__btn", type: "button",
|
|
18575
|
-
disabled: Boolean(busy),
|
|
18600
|
+
disabled: Boolean(busy) || !canApply,
|
|
18601
|
+
title: applyBlockedReason || undefined,
|
|
18576
18602
|
onClick: () => act("activate", { id: localId }, s.title),
|
|
18577
18603
|
}, busy === "activate:" + localId ? weT("切换中…") : weT("应用")),
|
|
18578
18604
|
React.createElement("span", { key: "done", style: SKIN_STYLE.badge }, weT("已安装")),
|
package/lib/routes/skins.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { createHash } from 'node:crypto';
|
|
22
22
|
import { inflateRawSync } from 'node:zlib';
|
|
23
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
23
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
24
24
|
import { homedir } from 'node:os';
|
|
25
25
|
import { join } from 'node:path';
|
|
26
26
|
|
|
@@ -42,6 +42,32 @@ function dshHome() {
|
|
|
42
42
|
/** 皮肤根:与皮肤中心同一个目录(它读 `$DSH_HOME/skins`)。 */
|
|
43
43
|
function skinsRoot() { return join(dshHome(), 'skins'); }
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* 皮肤中心持久化"当前皮肤"的那个文件。
|
|
47
|
+
*
|
|
48
|
+
* 为什么我们会直接写它(兜底路径):切换皮肤要落到 `/api/skin-center/v2/active`,
|
|
49
|
+
* 而那条路径 ① 需要 DSH 会话;② 必须打到宿主 HTTP 服务那个源。实测这两点在桌面端内嵌页面里
|
|
50
|
+
* 都可能不成立(401 / 路由没到),于是"点应用"根本没有可靠的写入通道。
|
|
51
|
+
* 这个文件是**它自己的持久层**(`{ active, initialized, background }`),它每次 GET 都重新读它,
|
|
52
|
+
* 并且客户端每 2 秒轮询一次 GET 收敛 —— 所以写这个文件等价于"替它记下选择",可靠且即时。
|
|
53
|
+
* 语义上只改 `active` 一个键,其余原样保留。
|
|
54
|
+
*/
|
|
55
|
+
function activeStatePath() { return join(dshHome(), 'skin-center-active.json'); }
|
|
56
|
+
|
|
57
|
+
/** 把 `active` 写进皮肤中心的持久层(保留其它字段;原子替换,失败不破坏原文件)。 */
|
|
58
|
+
function writeActiveStateFile(id) {
|
|
59
|
+
const p = activeStatePath();
|
|
60
|
+
let doc = {};
|
|
61
|
+
try { doc = JSON.parse(readFileSync(p, 'utf8')) || {}; } catch { doc = {}; }
|
|
62
|
+
if (typeof doc !== 'object' || Array.isArray(doc)) doc = {};
|
|
63
|
+
doc.active = id === undefined ? null : id;
|
|
64
|
+
if (doc.initialized === undefined) doc.initialized = true;
|
|
65
|
+
const tmp = p + '.folia-tmp';
|
|
66
|
+
writeFileSync(tmp, JSON.stringify(doc, null, 2) + '\n', 'utf8');
|
|
67
|
+
renameSync(tmp, p);
|
|
68
|
+
return doc;
|
|
69
|
+
}
|
|
70
|
+
|
|
45
71
|
function siteBase() {
|
|
46
72
|
const raw = process.env.DSH_FOLIA_SITE;
|
|
47
73
|
if (typeof raw === 'string' && raw.trim().toLowerCase() === 'off') return null;
|
|
@@ -56,20 +82,43 @@ function siteBase() {
|
|
|
56
82
|
* 前端也能问,但宿主这一侧拿到的结果与"装没装"一一对应,而且不受浏览器缓存/跨源策略影响。
|
|
57
83
|
* 判据用**它自己的 catalog 路由**:该路由不需要鉴权(实测 200),404 就是没装。
|
|
58
84
|
*/
|
|
59
|
-
const skinCenterProbe = { at: 0, state: 'unknown' };
|
|
85
|
+
const skinCenterProbe = { at: 0, state: 'unknown', detail: '' };
|
|
60
86
|
async function probeSkinCenter(hostHeader, force) {
|
|
61
87
|
const now = Date.now();
|
|
62
88
|
if (!force && skinCenterProbe.at && now - skinCenterProbe.at < 30000) return skinCenterProbe.state;
|
|
63
89
|
let state = 'unknown';
|
|
90
|
+
let detail = '';
|
|
64
91
|
if (hostHeader) {
|
|
65
92
|
try {
|
|
66
93
|
const r = await fetch('http://' + hostHeader + '/api/skin-center/v2/catalog', { signal: AbortSignal.timeout(4000) });
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
94
|
+
if (r.ok) {
|
|
95
|
+
// 只有**真的拿到 catalog JSON** 才算在跑。这个判据以前写成"非 404 即 present",
|
|
96
|
+
// 而 401 也会落到那一支 —— 但宿主进程没有会话,拿到的 401 只代表"鉴权挡在前面",
|
|
97
|
+
// 完全可能是插件被禁用(路由根本没注册)。实测就被它骗过:面板显示"皮肤中心在",
|
|
98
|
+
// 点应用却永远 404(真实原因:插件被市场保持为 off)。
|
|
99
|
+
let doc = null;
|
|
100
|
+
try { doc = await r.json(); } catch { doc = null; }
|
|
101
|
+
state = doc && doc.ok ? 'present' : 'auth-gated';
|
|
102
|
+
detail = state === 'present' ? '' : 'catalog-not-json';
|
|
103
|
+
} else if (r.status === 401 || r.status === 403) {
|
|
104
|
+
// 鉴权挡在前面 ⇒ **无法判定**它到底有没有挂载;如实说"不确定",不要冒充 present。
|
|
105
|
+
state = 'auth-gated';
|
|
106
|
+
detail = 'http-' + r.status;
|
|
107
|
+
} else if (r.status === 404) {
|
|
108
|
+
state = 'absent';
|
|
109
|
+
detail = 'route-missing';
|
|
110
|
+
} else {
|
|
111
|
+
state = 'unknown';
|
|
112
|
+
detail = 'http-' + r.status;
|
|
113
|
+
}
|
|
114
|
+
} catch (e) {
|
|
115
|
+
state = 'unknown';
|
|
116
|
+
detail = (e && e.name) || 'network';
|
|
117
|
+
}
|
|
70
118
|
}
|
|
71
119
|
skinCenterProbe.at = now;
|
|
72
120
|
skinCenterProbe.state = state;
|
|
121
|
+
skinCenterProbe.detail = detail;
|
|
73
122
|
return state;
|
|
74
123
|
}
|
|
75
124
|
|
|
@@ -274,6 +323,13 @@ export function registerSkinsRoutes(webServer, c) {
|
|
|
274
323
|
sendJson(res, 200, {
|
|
275
324
|
ok: true,
|
|
276
325
|
skinCenter,
|
|
326
|
+
skinCenterDetail: skinCenterProbe.detail || '',
|
|
327
|
+
// 皮肤中心被"保持为 off"时(peer 版本不匹配等),用户需要知道去改什么。
|
|
328
|
+
// 这里不动它,只把"它为什么可能不在"说清楚:我们钉的是 0.4.8(peer dsh >= 0.2.1-rc.2),
|
|
329
|
+
// 0.4.9+ 要求 rc.3,装了更高版本就会被市场关掉。
|
|
330
|
+
skinCenterHint: skinCenter === 'present'
|
|
331
|
+
? ''
|
|
332
|
+
: 'skin-center-0.4.8',
|
|
277
333
|
siteBase: siteBase(),
|
|
278
334
|
siteError: site.error,
|
|
279
335
|
installed,
|
|
@@ -392,6 +448,114 @@ export function registerSkinsRoutes(webServer, c) {
|
|
|
392
448
|
},
|
|
393
449
|
}));
|
|
394
450
|
|
|
395
|
-
|
|
396
|
-
|
|
451
|
+
/* ④ 代理皮肤中心的 active 端点(**转发浏览器凭据**)。
|
|
452
|
+
*
|
|
453
|
+
* 为什么需要它:写入 `/api/skin-center/v2/active` 有两个前提 ——
|
|
454
|
+
* ① 会话(DSH 对这条路径做鉴权);② 请求打到**宿主 HTTP 服务**那个源。
|
|
455
|
+
* 浏览器直连是首选(同源相对路径,凭据天然带全);但当页面的 origin 与宿主 API 的 origin
|
|
456
|
+
* 不同(桌面端内嵌页面可能是自定义协议或另一个端口),直连会 404 —— 那是**路由没到**,
|
|
457
|
+
* 而它的 body 里没有 `error` 字段,正好和皮肤中心自己的 404 `skin-not-found` 区分开。
|
|
458
|
+
*
|
|
459
|
+
* 所以这里做**兜底**:把浏览器这一侧的 cookie 与同源头一起转发过去。
|
|
460
|
+
* 只转发必要的头,不转发 `authorization`(避免把别的凭据带进第三方插件)。
|
|
461
|
+
*/
|
|
462
|
+
const FORWARD_HEADERS = ['cookie', 'origin', 'referer', 'sec-fetch-site', 'sec-fetch-mode'];
|
|
463
|
+
function forwardedHeaders(req) {
|
|
464
|
+
const out = {};
|
|
465
|
+
const src = (req && req.headers) || {};
|
|
466
|
+
for (const k of FORWARD_HEADERS) {
|
|
467
|
+
const v = src[k];
|
|
468
|
+
if (typeof v === 'string' && v) out[k] = v;
|
|
469
|
+
}
|
|
470
|
+
// 同源栅栏看的是 Sec-Fetch-Site / Origin 与 Host 的一致性:我们代理出去时 Host 变了,
|
|
471
|
+
// 所以显式把它标成 same-origin(这正是事实:浏览器是发给"自己这台的宿主"的)。
|
|
472
|
+
out['sec-fetch-site'] = 'same-origin';
|
|
473
|
+
return out;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
disposers.push(webServer.register({
|
|
477
|
+
kind: 'exact',
|
|
478
|
+
path: `${base}/skins/activate`,
|
|
479
|
+
handler: async (req, res) => {
|
|
480
|
+
if ((req.method || 'GET').toUpperCase() !== 'POST') { sendJson(res, 405, { ok: false, error: 'method-not-allowed' }); return; }
|
|
481
|
+
const host = req.headers && req.headers.host;
|
|
482
|
+
if (!host) { sendJson(res, 400, { ok: false, error: 'no-host' }); return; }
|
|
483
|
+
const body = await readJsonBody(req);
|
|
484
|
+
const raw = body && body.id;
|
|
485
|
+
const requested = (raw === null || raw === undefined || String(raw) === '') ? null : safeSkinId(raw);
|
|
486
|
+
if (raw !== null && raw !== undefined && String(raw) !== '' && !requested) {
|
|
487
|
+
sendJson(res, 400, { ok: false, error: 'bad-id' });
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (requested && !existsSync(join(skinsRoot(), requested))) {
|
|
491
|
+
sendJson(res, 404, { ok: false, error: 'not-installed' });
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
let upstream = { status: 0, error: null };
|
|
495
|
+
try {
|
|
496
|
+
const r = await fetch('http://' + host + '/api/skin-center/v2/active', {
|
|
497
|
+
method: 'POST',
|
|
498
|
+
headers: { 'content-type': 'application/json', ...forwardedHeaders(req) },
|
|
499
|
+
body: JSON.stringify({ active: requested }),
|
|
500
|
+
signal: AbortSignal.timeout(6000),
|
|
501
|
+
});
|
|
502
|
+
const text = await r.text();
|
|
503
|
+
let doc = null;
|
|
504
|
+
try { doc = JSON.parse(text); } catch { /* 非 JSON(路由 miss 时常见) */ }
|
|
505
|
+
upstream = { status: r.status, error: (doc && doc.error) || null };
|
|
506
|
+
if (r.ok) {
|
|
507
|
+
sendJson(res, 200, { ok: true, active: requested, via: 'http', upstream });
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
} catch (e) {
|
|
511
|
+
upstream = { status: 0, error: e && e.name === 'TimeoutError' ? 'timeout' : 'network' };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ── 兜底:直接写它自己的持久层 ────────────────────────────────────────
|
|
515
|
+
// HTTP 写入不可用(无会话 / 源不一致 / 端点不在)时,写 `skin-center-active.json` 的
|
|
516
|
+
// `active` 字段:皮肤中心每次 GET 都重读这个文件,客户端又每 2 秒轮询一次 GET,
|
|
517
|
+
// 所以写文件等价于"替它记下选择",最迟 2 秒生效;我们同时还会广播 dsh-skin-applied 立即生效。
|
|
518
|
+
try {
|
|
519
|
+
writeActiveStateFile(requested);
|
|
520
|
+
sendJson(res, 200, { ok: true, active: requested, via: 'state-file', upstream });
|
|
521
|
+
} catch (e) {
|
|
522
|
+
sendJson(res, 502, {
|
|
523
|
+
ok: false,
|
|
524
|
+
error: 'activate-failed',
|
|
525
|
+
upstream,
|
|
526
|
+
detail: String((e && e.message) || e).slice(0, 160),
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
}));
|
|
531
|
+
|
|
532
|
+
disposers.push(webServer.register({
|
|
533
|
+
kind: 'exact',
|
|
534
|
+
path: `${base}/skins/active`,
|
|
535
|
+
handler: async (req, res) => {
|
|
536
|
+
const host = req.headers && req.headers.host;
|
|
537
|
+
// 先问皮肤中心自己(HTTP),问不到就**读它的持久层** —— 后者是写路径的兜底,
|
|
538
|
+
// 两边都必须能回答"现在用的是哪套",否则面板的"使用中"标记会随写入方式漂移。
|
|
539
|
+
if (host) {
|
|
540
|
+
try {
|
|
541
|
+
const r = await fetch('http://' + host + '/api/skin-center/v2/active', {
|
|
542
|
+
headers: { accept: 'application/json', ...forwardedHeaders(req) },
|
|
543
|
+
signal: AbortSignal.timeout(5000),
|
|
544
|
+
});
|
|
545
|
+
if (r.ok) {
|
|
546
|
+
const doc = await r.json().catch(() => null);
|
|
547
|
+
sendJson(res, 200, { ok: true, active: (doc && doc.active) || null, via: 'http' });
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
} catch { /* 落到文件读 */ }
|
|
551
|
+
}
|
|
552
|
+
try {
|
|
553
|
+
let doc = {};
|
|
554
|
+
try { doc = JSON.parse(readFileSync(activeStatePath(), 'utf8')) || {}; } catch { doc = {}; }
|
|
555
|
+
sendJson(res, 200, { ok: true, active: doc.active || null, via: 'state-file' });
|
|
556
|
+
} catch (e) {
|
|
557
|
+
sendJson(res, 502, { ok: false, error: 'read-failed', detail: String((e && e.message) || e).slice(0, 120) });
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
}));
|
|
397
561
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-folia",
|
|
3
|
-
"version": "4.12.
|
|
3
|
+
"version": "4.12.6",
|
|
4
4
|
"description": "Folia 壁纸引擎:把本机 Wallpaper Engine 壁纸搬进 DSH 界面(场景实时渲染 / 视频 / 网页),内置液态玻璃与 Folia 吉祥物,并可从自建站点拉取壁纸。单包形态(组件已合并)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -193,7 +193,7 @@
|
|
|
193
193
|
}
|
|
194
194
|
},
|
|
195
195
|
"dependencies": {
|
|
196
|
-
"@linxin666/dsh-client-ui-skin-center": "
|
|
196
|
+
"@linxin666/dsh-client-ui-skin-center": "0.4.8",
|
|
197
197
|
"webwallgl": "^3.0.0",
|
|
198
198
|
"x11": "^4.3.1"
|
|
199
199
|
},
|