dsh-folia 4.9.1 → 4.10.1

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 CHANGED
@@ -780,6 +780,34 @@ window.__ModuleLoader__.load({
780
780
  "…": "…",
781
781
  // 开机动画:已开启但无选片时的提示(4.8.2)
782
782
  "已开启,但还没有选片 —— 下载一条后点「使用」": "Enabled, but no clip is selected yet — download one, then press Use",
783
+
784
+ // ── 助手皮肤页签(4.10.0)────────────────────────────────────────────────
785
+ "助手皮肤": "Assistant skins",
786
+ "🎨 助手皮肤": "🎨 Assistant skins",
787
+ "皮肤包安装到皮肤中心目录;装完到「设置 → 皮肤中心」里点选即可启用。皮肤中心与本插件读同一个目录,无需重启。": "Skin packs are installed into the skin centre's directory; pick one in Settings → Skin centre to enable it. It reads the same folder, so no restart is needed.",
788
+ "本机已装({n})": "Installed ({n})",
789
+ "站点皮肤({n})": "Skins on the site ({n})",
790
+ "还没有安装任何皮肤 —— 从下面的站点清单里挑一个。": "No skins installed yet — pick one from the site list below.",
791
+ "站点上还没有皮肤。": "No skins on the site yet.",
792
+ "含动效钩子": "animated hooks",
793
+ "已安装": "Installed",
794
+ "已安装过:{name}": "Already installed: {name}",
795
+ "已安装:{name} —— 到「设置 → 皮肤中心」点它即可启用": "Installed: {name} — pick it in Settings → Skin centre to enable",
796
+ "卸载中…": "Uninstalling…",
797
+ "动态": "Animated",
798
+ "这个包里没有 skin.json,不是皮肤包": "No skin.json in this archive — not a skin pack",
799
+ "包里有非法路径(已整包拒绝)": "The archive contains an unsafe path (rejected entirely)",
800
+ "包太大了(上限 200 MB)": "Archive is too large (200 MB limit)",
801
+ "下载地址不在白名单里": "The download host is not allow-listed",
802
+ "站点被关闭了(DSH_FOLIA_SITE=off)": "The site is disabled (DSH_FOLIA_SITE=off)",
803
+ "这个文件不是 zip": "This file is not a zip",
804
+
805
+ // ── 助手皮肤:皮肤中心缺失时的引导(4.10.1)──────────────────────────
806
+ "皮肤中心没装:它是**独立插件** @linxin666/dsh-client-ui-skin-center(不在 dsh-web-all 里,那个只是聚合包)。装它才能启用皮肤:": "The skin centre is not installed: it is its own plugin, @linxin666/dsh-client-ui-skin-center (dsh-web-all is only an aggregate). Install it to enable skins:",
807
+ "已复制安装命令": "Install command copied",
808
+ "复制失败,请手动选中上面的命令": "Copy failed — select the command above manually",
809
+ "复制命令": "Copy command",
810
+ "先装皮肤中心,否则皮肤装好也没法启用": "Install the skin centre first, or the skin cannot be enabled",
783
811
  };
784
812
 
785
813
  // ── 内联模块:src/i18n-copy-picker.js —— 英文词表域:壁纸选择器与快捷面板(picker-* · quick-panel · sidebar-right · theme-follow)(构建期注入;勿手改本段)──
@@ -18213,6 +18241,209 @@ window.__ModuleLoader__.load({
18213
18241
 
18214
18242
  // 仅供单测驱动(构建期内联时会剥掉 export,不影响产物)。
18215
18243
 
18244
+ // ── 内联模块:src/panel-tabs-skins.js —— 「助手皮肤」页签:站点皮肤清单/安装/卸载 — 渲染由皮肤中心接管,我们只把 zip 解到 $DSH_HOME/skins(构建期注入;勿手改本段)──
18245
+ /**
18246
+ * panel-tabs-skins.js — 「助手皮肤」页签:站点皮肤清单 / 安装 / 卸载 / 去皮肤中心启用。
18247
+ *
18248
+ * 与「桌宠」页签同构(fetch → busy → note → 卡片列表)。差别在**安装之后谁负责渲染**:
18249
+ * 桌宠由本插件自己画(pet-layer.js),皮肤则由皮肤中心接管 —— 我们只把 zip 解到
18250
+ * `$DSH_HOME/skins/<id>/`,皮肤中心下一次 catalog 扫描就会认领(它的缓存键里带父目录 mtime)。
18251
+ * 所以这个页签的职责边界是:**装/卸 + 告诉你下一步去哪启用**,不重复实现渲染。
18252
+ */
18253
+
18254
+ const SKIN_STYLE = {
18255
+ row: { display: "flex", alignItems: "center", gap: "10px", flexWrap: "wrap" },
18256
+ card: {
18257
+ display: "flex", flexDirection: "column", gap: "6px", padding: "10px",
18258
+ borderRadius: "10px", border: "1px solid rgba(255,255,255,0.14)",
18259
+ background: "rgba(255,255,255,0.04)", width: "188px",
18260
+ },
18261
+ thumb: {
18262
+ width: "100%", aspectRatio: "16 / 9", borderRadius: "6px",
18263
+ backgroundSize: "cover", backgroundPosition: "center",
18264
+ backgroundColor: "rgba(0,0,0,0.25)",
18265
+ },
18266
+ title: { fontSize: "13px", fontWeight: 600, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" },
18267
+ meta: { fontSize: "11px", opacity: 0.72 },
18268
+ grid: { display: "flex", gap: "10px", flexWrap: "wrap", marginTop: "10px" },
18269
+ badge: {
18270
+ fontSize: "10px", padding: "1px 6px", borderRadius: "4px",
18271
+ background: "rgba(30,90,160,0.55)", alignSelf: "flex-start",
18272
+ },
18273
+ };
18274
+
18275
+ function skinFmtBytes(n) {
18276
+ const v = Number(n) || 0;
18277
+ if (!v) return "";
18278
+ if (v < 1024 * 1024) return Math.round(v / 1024) + " KB";
18279
+ return (v / 1048576).toFixed(1) + " MB";
18280
+ }
18281
+
18282
+ function SkinsTabPanel() {
18283
+ const [state, setState] = React.useState(null);
18284
+ const [busy, setBusy] = React.useState("");
18285
+ const [note, setNote] = React.useState(null);
18286
+
18287
+ const load = React.useCallback(async (force) => {
18288
+ try {
18289
+ const resp = await apiJson("/skins/list" + (force ? "?refresh=1" : ""), { parse: "always" });
18290
+ const doc = (resp && resp.data) || null;
18291
+ if (resp && resp.ok && doc && doc.ok) {
18292
+ setState(doc);
18293
+ if (doc.siteError && doc.siteError !== "site-off") {
18294
+ setNote({ ok: false, text: weT("站点清单读不到:{msg}", { msg: String(doc.siteError) }) });
18295
+ }
18296
+ } else {
18297
+ setNote({ ok: false, text: weT("读取失败:{msg}", { msg: (doc && doc.error) || "unknown" }) });
18298
+ }
18299
+ } catch (e) {
18300
+ setNote({ ok: false, text: weT("请求失败:{msg}", { msg: (e && e.message) || weT("网络错误") }) });
18301
+ }
18302
+ }, []);
18303
+
18304
+ React.useEffect(() => { load(false); }, [load]);
18305
+
18306
+ const act = async (kind, payload, label) => {
18307
+ const structural = kind === "install" || kind === "remove";
18308
+ if (structural) setBusy(kind + ":" + ((payload && payload.id) || ""));
18309
+ setNote(null);
18310
+ try {
18311
+ const resp = await apiPostJson("/skins/" + kind, payload || {}, { parse: "always" });
18312
+ const r = (resp && resp.data) || null;
18313
+ if (resp && resp.ok && r && r.ok) {
18314
+ if (kind === "install") {
18315
+ setNote({
18316
+ ok: true,
18317
+ text: r.exists
18318
+ ? weT("已安装过:{name}", { name: label || "" })
18319
+ : weT("已安装:{name} —— 到「设置 → 皮肤中心」点它即可启用", { name: label || "" }),
18320
+ });
18321
+ }
18322
+ if (kind === "remove") setNote({ ok: true, text: weT("已卸载:{name}", { name: label || "" }) });
18323
+ await load(true);
18324
+ } else {
18325
+ const code = (r && r.error) || "failed";
18326
+ const map = {
18327
+ 'no-skin-json': weT("这个包里没有 skin.json,不是皮肤包"),
18328
+ 'unsafe-entry': weT("包里有非法路径(已整包拒绝)"),
18329
+ 'too-large': weT("包太大了(上限 200 MB)"),
18330
+ 'host-not-allowed': weT("下载地址不在白名单里"),
18331
+ 'site-off': weT("站点被关闭了(DSH_FOLIA_SITE=off)"),
18332
+ 'not-a-zip': weT("这个文件不是 zip"),
18333
+ };
18334
+ setNote({ ok: false, text: map[code] || weT("操作失败:{code}", { code }) });
18335
+ }
18336
+ } catch (e) {
18337
+ setNote({ ok: false, text: weT("请求失败:{msg}", { msg: (e && e.message) || weT("网络错误") }) });
18338
+ } finally {
18339
+ setBusy("");
18340
+ }
18341
+ };
18342
+
18343
+ const children = [];
18344
+ children.push(React.createElement("div", { key: "head", style: SKIN_STYLE.row },
18345
+ React.createElement("div", { style: { fontSize: "13px", fontWeight: 600 } }, weT("🎨 助手皮肤")),
18346
+ React.createElement("button", {
18347
+ className: "we-picker__btn", type: "button",
18348
+ disabled: Boolean(busy),
18349
+ onClick: () => load(true),
18350
+ }, weT("刷新")),
18351
+ ));
18352
+
18353
+ // 皮肤中心在不在,决定这段提示说什么:
18354
+ // 在 → "装完去皮肤中心点它"(本页签只负责落盘)
18355
+ // 不在 → 明说它是个**独立插件**(不在 dsh-web-all 里),给一条可复制的安装命令
18356
+ // unknown(探测失败)→ 保守地按"在"讲,不吓唬用户
18357
+ const skinCenter = (state && state.skinCenter) || "unknown";
18358
+ if (skinCenter === "absent") {
18359
+ children.push(React.createElement("div", {
18360
+ key: "hint-absent",
18361
+ className: "we-picker__hint",
18362
+ style: { margin: "6px 0 4px", color: "#ffd08a", lineHeight: "1.6" },
18363
+ }, weT("皮肤中心没装:它是**独立插件** @linxin666/dsh-client-ui-skin-center(不在 dsh-web-all 里,那个只是聚合包)。装它才能启用皮肤:")));
18364
+ children.push(React.createElement("div", { key: "cmd", style: { display: "flex", gap: "8px", alignItems: "center", margin: "0 0 6px" } },
18365
+ React.createElement("code", {
18366
+ style: { fontSize: "11px", padding: "3px 8px", borderRadius: "6px", background: "rgba(255,255,255,0.08)" },
18367
+ }, "dsh plugin --profile web add @linxin666/dsh-client-ui-skin-center"),
18368
+ React.createElement("button", {
18369
+ className: "we-picker__btn", type: "button",
18370
+ 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("复制失败,请手动选中上面的命令") }); } },
18371
+ }, weT("复制命令")),
18372
+ ));
18373
+ } else {
18374
+ children.push(React.createElement("div", {
18375
+ key: "hint",
18376
+ className: "we-picker__hint",
18377
+ style: { margin: "6px 0 4px", opacity: 0.8 },
18378
+ }, weT("皮肤包安装到皮肤中心目录;装完到「设置 → 皮肤中心」里点选即可启用。皮肤中心与本插件读同一个目录,无需重启。")));
18379
+ }
18380
+
18381
+ // 提示行常驻占位(与开机动画/桌宠页签同一条纪律:不因 note 出现/消失让面板跳高度)
18382
+ children.push(React.createElement("div", {
18383
+ key: "note",
18384
+ className: "we-picker__hint",
18385
+ style: {
18386
+ color: note && !note.ok ? "#ff9a9a" : "#8ee6a8",
18387
+ margin: "2px 0 8px", minHeight: "18px", lineHeight: "18px",
18388
+ opacity: note ? 1 : 0, transition: "opacity 120ms linear",
18389
+ },
18390
+ }, note ? note.text : "\u00a0"));
18391
+
18392
+ const installed = (state && state.installed) || [];
18393
+ if (state) {
18394
+ children.push(React.createElement("div", { key: "installed-head", className: "we-picker__hint" },
18395
+ weT("本机已装({n})", { n: installed.length })));
18396
+ children.push(React.createElement("div", { key: "installed", style: SKIN_STYLE.grid },
18397
+ installed.length === 0
18398
+ ? [React.createElement("span", { key: "none", className: "we-picker__hint" }, weT("还没有安装任何皮肤 —— 从下面的站点清单里挑一个。"))]
18399
+ : installed.map((s) => React.createElement("div", { key: s.id, style: SKIN_STYLE.card },
18400
+ React.createElement("div", { style: { ...SKIN_STYLE.title, color: s.accent || undefined } }, s.name || s.id),
18401
+ React.createElement("div", { style: SKIN_STYLE.meta },
18402
+ [s.version ? "v" + s.version : "", skinFmtBytes(s.bytes), s.hasHooks ? weT("含动效钩子") : ""]
18403
+ .filter(Boolean).join(" · ")),
18404
+ React.createElement("div", { style: { display: "flex", gap: "6px", alignItems: "center" } },
18405
+ React.createElement("button", {
18406
+ className: "we-picker__btn", type: "button",
18407
+ disabled: Boolean(busy),
18408
+ onClick: () => act("remove", { id: s.id }, s.name),
18409
+ }, busy === "remove:" + s.id ? weT("卸载中…") : weT("卸载")),
18410
+ ),
18411
+ )),
18412
+ ));
18413
+ }
18414
+
18415
+ const site = (state && state.site) || [];
18416
+ children.push(React.createElement("div", { key: "site-head", className: "we-picker__hint", style: { marginTop: "10px" } },
18417
+ weT("站点皮肤({n})", { n: site.length })));
18418
+ children.push(React.createElement("div", { key: "site", style: SKIN_STYLE.grid },
18419
+ site.length === 0
18420
+ ? [React.createElement("span", { key: "none", className: "we-picker__hint" }, weT("站点上还没有皮肤。"))]
18421
+ : site.map((s) => {
18422
+ const busyThis = busy === "install:" + s.id;
18423
+ return React.createElement("div", { key: s.id, style: SKIN_STYLE.card },
18424
+ s.thumbUrl
18425
+ ? React.createElement("div", {
18426
+ style: { ...SKIN_STYLE.thumb, backgroundImage: "url(" + JSON.stringify(s.thumbUrl) + ")" },
18427
+ })
18428
+ : React.createElement("div", { style: SKIN_STYLE.thumb }),
18429
+ React.createElement("div", { style: SKIN_STYLE.title, title: s.title }, s.title),
18430
+ React.createElement("div", { style: SKIN_STYLE.meta },
18431
+ [skinFmtBytes(s.bytes), (s.tags || []).includes("动态") ? weT("动态") : ""].filter(Boolean).join(" · ")),
18432
+ s.installed
18433
+ ? React.createElement("div", { style: SKIN_STYLE.badge }, weT("已安装"))
18434
+ : React.createElement("button", {
18435
+ className: "we-picker__btn", type: "button",
18436
+ disabled: Boolean(busy),
18437
+ title: skinCenter === "absent" ? weT("先装皮肤中心,否则皮肤装好也没法启用") : undefined,
18438
+ onClick: () => act("install", { id: s.id, url: s.url }, s.title),
18439
+ }, busyThis ? weT("安装中…") : weT("安装")),
18440
+ );
18441
+ }),
18442
+ ));
18443
+
18444
+ return React.createElement("div", { className: "we-picker", style: { display: "flex", flexDirection: "column" } }, children);
18445
+ }
18446
+
18216
18447
  // ── 内联模块:src/features.js —— 客户端功能注册表:页签条 / 整区面板 / 常驻挂载层。装配点(quick-panel、client.js)只查表,不写功能名(构建期注入;勿手改本段)──
18217
18448
  /**
18218
18449
  * features.js(客户端)— **功能注册表**:页签声明、面板工厂、挂载钩子。
@@ -18241,6 +18472,8 @@ window.__ModuleLoader__.load({
18241
18472
  { id: "playback", get label() { return weT("播放"); } },
18242
18473
  // 桌宠是"桌面上多出来的一只东西",与壁纸/开机动画并列。
18243
18474
  { id: "pets", get label() { return weT("桌宠"); } },
18475
+ // 助手皮肤:装的是皮肤中心的皮肤包。我们只管装/卸,渲染与启用归皮肤中心。
18476
+ { id: "skins", get label() { return weT("助手皮肤"); } },
18244
18477
  ];
18245
18478
 
18246
18479
  /**
@@ -18250,6 +18483,7 @@ window.__ModuleLoader__.load({
18250
18483
  const WE_FEATURE_PANELS = {
18251
18484
  boot: BootTabPanel,
18252
18485
  pets: PetsTabPanel,
18486
+ skins: SkinsTabPanel,
18253
18487
  };
18254
18488
 
18255
18489
  /**
package/lib/features.js CHANGED
@@ -126,12 +126,15 @@ export const FEATURES = [
126
126
  host: [
127
127
  'lib/routes/settings.js',
128
128
  'lib/routes/presets.js',
129
+ // 助手皮肤:站点皮肤包 → $DSH_HOME/skins(皮肤中心读同一目录,写盘即生效)。
130
+ 'lib/routes/skins.js',
129
131
  'lib/settings-schema.js',
130
132
  'lib/components/fonts/routes/fontsets.js',
131
133
  'lib/components/fonts/routes/system-fonts.js',
132
134
  ],
133
135
  client: [
134
136
  'src/panel-tabs-appearance.js',
137
+ 'src/panel-tabs-skins.js',
135
138
  'src/glass.js',
136
139
  'src/glass-panel.js',
137
140
  'src/glass-surfaces.js',
@@ -151,7 +154,7 @@ export const FEATURES = [
151
154
  'src/about-assets.js',
152
155
  'src/nav-icon.js',
153
156
  ],
154
- tab: ['appearance'],
157
+ tab: ['appearance', 'skins'],
155
158
  },
156
159
  {
157
160
  id: 'playback',
@@ -19,6 +19,7 @@ import { registerInventoryRoutes, warmInventorySiteCache } from './routes/invent
19
19
  import { registerModulesRoutes, warmModulesCache } from './routes/modules.js';
20
20
  import { registerBootRoutes } from './routes/boot.js';
21
21
  import { registerSiteThumbRoutes } from './routes/site-thumb.js';
22
+ import { registerSkinsRoutes } from './routes/skins.js';
22
23
  import { registerPetsRoutes } from './routes/pets.js';
23
24
 
24
25
  /**
@@ -71,6 +72,14 @@ export const FEATURE_ROUTE_FAMILIES = [
71
72
  register: registerPetsRoutes,
72
73
  deps: (c) => ({ disposers: c.disposers, base: c.base, log: c.log }),
73
74
  },
75
+ {
76
+ id: 'skins',
77
+ file: 'lib/routes/skins.js',
78
+ register: registerSkinsRoutes,
79
+ // 皮肤安装目标是 $DSH_HOME/skins(皮肤中心读同一个目录):由路由自己按 DSH_HOME 解析,
80
+ // 不额外引入注入面 —— 少一处注入,就少一种"改了目录却没生效"的可能。
81
+ deps: (c) => ({ disposers: c.disposers, base: c.base, log: c.log }),
82
+ },
74
83
  ];
75
84
 
76
85
  /**
@@ -0,0 +1,394 @@
1
+ /**
2
+ * routes/skins.js — **助手皮肤**页签的宿主半边:列站点皮肤、安装到皮肤中心目录、卸载。
3
+ *
4
+ * 为什么是这个形态(而不是另造一套渲染):皮肤中心扫的是 `$DSH_HOME/skins/<id>/`
5
+ * (`skin.json` + `skin.css` + 可选 `hooks.mjs` + `assets/`)。我们只要**把站点上的 zip 解到那个目录**,
6
+ * 皮肤中心下一次 catalog 扫描就会认领它 —— 它的缓存键里带父目录 `mtimeMs`,新增目录必然穿透缓存。
7
+ * 所以这里不"通知"皮肤中心,也不需要它配合:写盘即生效。
8
+ *
9
+ * 数据契约(站点一条 = 一个 zip;解出来必须是 v2 皮肤目录):
10
+ * <皮肤根>/<id>/skin.json 必填(id/name 从里面取,**不信任 URL 里的名字**)
11
+ * <皮肤根>/<id>/skin.css 必填(contributes.stylesheet 指向它)
12
+ * hooks.mjs 可选;带它时皮肤中心会要求 `dsh-market.provenance.json` 字节对得上才执行 ——
13
+ * 我们的 zip 是原样打的,所以 provenance 一起解出来,语义与市场安装一致。
14
+ *
15
+ * 纪律:
16
+ * · 只写 `$DSH_HOME/skins`,删也只删它下面(realpath 前缀校验,防符号链接逃逸);
17
+ * · 下载白名单 = 站点 host(挡 SSRF);只认 zip 魔数;单包上限 200 MB;
18
+ * · 解包拒路径穿越(整包拒绝,不是跳过);可执行后缀一律不落盘;
19
+ * · 失败清干净:绝不留下没有 skin.json 的残缺目录(否则皮肤中心会把它算进"坏皮肤")。
20
+ */
21
+ import { createHash } from 'node:crypto';
22
+ import { inflateRawSync } from 'node:zlib';
23
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
24
+ import { homedir } from 'node:os';
25
+ import { join } from 'node:path';
26
+
27
+ /** 注入契约(ADR-0013 D12)。 */
28
+ export { unzipSkinZip, safeSkinId };
29
+ export const DEPS = ['disposers', 'base', 'log'];
30
+
31
+ /** 站点分类 slug:助手皮肤。 */
32
+ const SKIN_CATEGORY = 'assistant-skin';
33
+ const MAX_ZIP_BYTES = 200 * 1024 * 1024;
34
+ const FETCH_TIMEOUT_MS = 60000;
35
+ const SKIN_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
36
+ /** 可执行/脚本后缀:皮肤包不需要,一律不写(防"皮肤包里塞个 exe")。 */
37
+ const SKIP_EXEC_RE = /\.(exe|dll|bat|cmd|com|scr|ps1|sh|msi|vbs|wsf|hta|lnk|jar)$/i;
38
+
39
+ function dshHome() {
40
+ return process.env.DSH_HOME || join(process.env.APPDATA || homedir(), 'dsh-desktop', 'harness');
41
+ }
42
+ /** 皮肤根:与皮肤中心同一个目录(它读 `$DSH_HOME/skins`)。 */
43
+ function skinsRoot() { return join(dshHome(), 'skins'); }
44
+
45
+ function siteBase() {
46
+ const raw = process.env.DSH_FOLIA_SITE;
47
+ if (typeof raw === 'string' && raw.trim().toLowerCase() === 'off') return null;
48
+ const base = (typeof raw === 'string' && raw.trim()) ? raw.trim() : 'https://folia-5ve.pages.dev';
49
+ return base.replace(/\/+$/, '');
50
+ }
51
+
52
+ /**
53
+ * 皮肤中心是否在线。
54
+ *
55
+ * 为什么由宿主探而不是前端探:两个插件跑在同一个 webServer 上,用请求自带的 Host 就能问到自己 ——
56
+ * 前端也能问,但宿主这一侧拿到的结果与"装没装"一一对应,而且不受浏览器缓存/跨源策略影响。
57
+ * 判据用**它自己的 catalog 路由**:该路由不需要鉴权(实测 200),404 就是没装。
58
+ */
59
+ const skinCenterProbe = { at: 0, state: 'unknown' };
60
+ async function probeSkinCenter(hostHeader, force) {
61
+ const now = Date.now();
62
+ if (!force && skinCenterProbe.at && now - skinCenterProbe.at < 30000) return skinCenterProbe.state;
63
+ let state = 'unknown';
64
+ if (hostHeader) {
65
+ try {
66
+ const r = await fetch('http://' + hostHeader + '/api/skin-center/v2/catalog', { signal: AbortSignal.timeout(4000) });
67
+ state = r.status === 404 ? 'absent' : (r.ok ? 'present' : 'present');
68
+ // 401/403 也说明插件在(只是该路由被别的版本加了鉴权);只有 404 才代表没装。
69
+ } catch { state = 'unknown'; }
70
+ }
71
+ skinCenterProbe.at = now;
72
+ skinCenterProbe.state = state;
73
+ return state;
74
+ }
75
+
76
+ /** 站点列表缓存(30 秒):面板打开与"刷新"共用,避免每次都打站点。 */
77
+ const siteCache = { at: 0, items: [], error: null };
78
+ const SITE_TTL_MS = 30000;
79
+
80
+ async function fetchSiteSkins(force) {
81
+ const now = Date.now();
82
+ if (!force && siteCache.at && now - siteCache.at < SITE_TTL_MS) return siteCache;
83
+ const base = siteBase();
84
+ if (!base) { siteCache.at = now; siteCache.items = []; siteCache.error = 'site-off'; return siteCache; }
85
+ try {
86
+ const url = base + '/api/public/wallpapers?category=' + encodeURIComponent(SKIN_CATEGORY) + '&limit=100';
87
+ const res = await fetch(url, { headers: { accept: 'application/json' }, signal: AbortSignal.timeout(20000) });
88
+ if (!res.ok) throw new Error('http-' + res.status);
89
+ const body = await res.json();
90
+ const items = (body && body.data && body.data.items) || [];
91
+ siteCache.items = items.map((it) => ({
92
+ id: String(it.slug || ''),
93
+ title: String(it.title || it.slug || ''),
94
+ bytes: Number(it.bytes) || 0,
95
+ mime: String(it.mime || ''),
96
+ url: String(it.url || ''),
97
+ thumbUrl: it.thumbUrl ? String(it.thumbUrl) : '',
98
+ tags: (() => { try { return JSON.parse(it.tags || '[]'); } catch { return []; } })(),
99
+ }));
100
+ siteCache.error = null;
101
+ } catch (e) {
102
+ siteCache.error = (e && e.message) || 'fetch-failed';
103
+ siteCache.items = siteCache.items || [];
104
+ }
105
+ siteCache.at = now;
106
+ return siteCache;
107
+ }
108
+
109
+ /** 本机已装皮肤(只读 `$DSH_HOME/skins`)。 */
110
+ function listInstalled() {
111
+ const root = skinsRoot();
112
+ let names = [];
113
+ try { names = readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name); } catch { return []; }
114
+ const out = [];
115
+ for (const dirId of names) {
116
+ const dir = join(root, dirId);
117
+ let manifest;
118
+ try { manifest = JSON.parse(readFileSync(join(dir, 'skin.json'), 'utf8')); } catch { continue; }
119
+ let bytes = 0;
120
+ try { bytes = dirBytes(dir); } catch { /* 忽略 */ }
121
+ out.push({
122
+ dirId,
123
+ id: String(manifest.id || dirId),
124
+ name: String(manifest.name || manifest.nameEn || manifest.id || dirId),
125
+ nameEn: manifest.nameEn ? String(manifest.nameEn) : '',
126
+ version: String(manifest.version || ''),
127
+ author: String(manifest.author || ''),
128
+ accent: manifest.accent ? String(manifest.accent) : '',
129
+ hasHooks: existsSync(join(dir, 'hooks.mjs')),
130
+ hasProvenance: existsSync(join(dir, 'dsh-market.provenance.json')),
131
+ bytes,
132
+ });
133
+ }
134
+ out.sort((a, b) => a.name.localeCompare(b.name, 'zh-Hans-CN'));
135
+ return out;
136
+ }
137
+
138
+ function dirBytes(dir) {
139
+ let total = 0;
140
+ for (const name of readdirSync(dir)) {
141
+ const abs = join(dir, name);
142
+ const st = statSync(abs);
143
+ total += st.isDirectory() ? dirBytes(abs) : st.size;
144
+ }
145
+ return total;
146
+ }
147
+
148
+ /* ── zip 读取(只读,够用即可;判据见 test/skins-http.mjs)────────────────── */
149
+ /**
150
+ * 解一个 zip 缓冲成条目数组。
151
+ *
152
+ * @param {Buffer} buf
153
+ * @param {{ maxEntries?: number, maxEntryBytes?: number, maxTotalBytes?: number }} [limits]
154
+ * @returns {{ name: string, data: Buffer, isDir: boolean }[]}
155
+ * @throws {Error} code: `not-a-zip` / `bad-zip` / `unsafe-entry` / `too-large`
156
+ */
157
+ function unzipSkinZip(buf, limits = {}) {
158
+ const maxEntries = limits.maxEntries || 4000;
159
+ const maxEntry = limits.maxEntryBytes || 256 * 1024 * 1024;
160
+ const maxTotal = limits.maxTotalBytes || 512 * 1024 * 1024;
161
+ if (!Buffer.isBuffer(buf) || buf.length < 22) throw Object.assign(new Error('not a zip'), { code: 'not-a-zip' });
162
+ const magic = buf.readUInt32LE(0);
163
+ if (magic !== 0x04034b50 && magic !== 0x06054b50) throw Object.assign(new Error('not a zip'), { code: 'not-a-zip' });
164
+ let eocd = -1;
165
+ for (let i = buf.length - 22; i >= Math.max(0, buf.length - 22 - 65535); i -= 1) {
166
+ if (buf.readUInt32LE(i) === 0x06054b50) { eocd = i; break; }
167
+ }
168
+ if (eocd < 0) throw Object.assign(new Error('no EOCD'), { code: 'bad-zip' });
169
+ const count = buf.readUInt16LE(eocd + 10);
170
+ const cdSize = buf.readUInt32LE(eocd + 12);
171
+ const cdOffset = buf.readUInt32LE(eocd + 16);
172
+ if (count > maxEntries) throw Object.assign(new Error('too many entries'), { code: 'too-large' });
173
+ if (cdOffset + cdSize > buf.length) throw Object.assign(new Error('CD out of range'), { code: 'bad-zip' });
174
+ const out = [];
175
+ let p = cdOffset;
176
+ let total = 0;
177
+ for (let i = 0; i < count; i += 1) {
178
+ if (p + 46 > buf.length || buf.readUInt32LE(p) !== 0x02014b50) throw Object.assign(new Error('bad CD'), { code: 'bad-zip' });
179
+ const method = buf.readUInt16LE(p + 10);
180
+ const compSize = buf.readUInt32LE(p + 20);
181
+ const uncompSize = buf.readUInt32LE(p + 24);
182
+ const nameLen = buf.readUInt16LE(p + 28);
183
+ const extraLen = buf.readUInt16LE(p + 30);
184
+ const commentLen = buf.readUInt16LE(p + 32);
185
+ const localOffset = buf.readUInt32LE(p + 42);
186
+ const name = buf.toString('utf8', p + 46, p + 46 + nameLen);
187
+ p += 46 + nameLen + extraLen + commentLen;
188
+ const norm = String(name || '').replace(/\\/g, '/');
189
+ if (!norm || norm.includes('\0') || norm.startsWith('/') || /^[a-zA-Z]:/.test(norm) || norm.split('/').some((s) => s === '..')) {
190
+ throw Object.assign(new Error('unsafe entry: ' + name), { code: 'unsafe-entry' });
191
+ }
192
+ if (norm.endsWith('/')) { out.push({ name: norm, data: Buffer.alloc(0), isDir: true }); continue; }
193
+ if (uncompSize > maxEntry) throw Object.assign(new Error('entry too large'), { code: 'too-large' });
194
+ total += uncompSize;
195
+ if (total > maxTotal) throw Object.assign(new Error('archive too large'), { code: 'too-large' });
196
+ if (localOffset + 30 > buf.length || buf.readUInt32LE(localOffset) !== 0x04034b50) throw Object.assign(new Error('bad local header'), { code: 'bad-zip' });
197
+ const lNameLen = buf.readUInt16LE(localOffset + 26);
198
+ const lExtraLen = buf.readUInt16LE(localOffset + 28);
199
+ const dataStart = localOffset + 30 + lNameLen + lExtraLen;
200
+ const dataEnd = dataStart + compSize;
201
+ if (dataEnd > buf.length) throw Object.assign(new Error('truncated'), { code: 'bad-zip' });
202
+ const raw = buf.subarray(dataStart, dataEnd);
203
+ let data;
204
+ if (method === 0) data = Buffer.from(raw);
205
+ else if (method === 8) {
206
+ try { data = inflateRawSync(raw); } catch (e) { throw Object.assign(new Error('inflate failed'), { code: 'bad-zip' }); }
207
+ } else throw Object.assign(new Error('unsupported method ' + method), { code: 'bad-zip' });
208
+ out.push({ name: norm, data, isDir: false });
209
+ }
210
+ return out;
211
+ }
212
+
213
+ /** 比对本机/站点 id 用的归一键:去掉可选的 `skin-` 前缀(站点 slug 为避撞名会加)。 */
214
+ function normSkinId(raw) {
215
+ const s = String(raw || '').trim().toLowerCase();
216
+ return s.startsWith('skin-') ? s.slice(5) : s;
217
+ }
218
+
219
+ /** 皮肤 id 只允许安全字符(要当目录名用)。 */
220
+ function safeSkinId(raw) {
221
+ const s = String(raw || '').trim();
222
+ return SKIN_ID_RE.test(s) ? s : '';
223
+ }
224
+
225
+ /** 校验"删/写这个目录"确实在皮肤根下面(realpath 前缀,挡符号链接逃逸)。 */
226
+ function resolveInside(root, id) {
227
+ const dir = join(root, id);
228
+ const realRoot = existsSync(root) ? realpathSync(root) : root;
229
+ const realDir = existsSync(dir) ? realpathSync(dir) : dir;
230
+ const withSep = realRoot.endsWith('\\') || realRoot.endsWith('/') ? realRoot : realRoot + (process.platform === 'win32' ? '\\' : '/');
231
+ return realDir === realRoot || realDir.startsWith(withSep) ? dir : '';
232
+ }
233
+
234
+ function sendJson(res, code, payload) {
235
+ res.statusCode = code;
236
+ res.setHeader('content-type', 'application/json; charset=utf-8');
237
+ res.end(JSON.stringify(payload));
238
+ }
239
+
240
+ function readJsonBody(req, limit = 64 * 1024) {
241
+ return new Promise((resolve) => {
242
+ let acc = '';
243
+ let done = false;
244
+ req.on('data', (d) => {
245
+ if (done) return;
246
+ acc += d;
247
+ if (acc.length > limit) { done = true; resolve(null); }
248
+ });
249
+ req.on('end', () => { if (!done) { done = true; try { resolve(JSON.parse(acc || '{}')); } catch { resolve(null); } } });
250
+ req.on('error', () => { if (!done) { done = true; resolve(null); } });
251
+ });
252
+ }
253
+
254
+ export function registerSkinsRoutes(webServer, c) {
255
+ const { disposers, base, log } = c;
256
+
257
+ /* ① 列表:站点可装的 + 本机已装的(同一份响应,面板一次拿全) */
258
+ disposers.push(webServer.register({
259
+ kind: 'exact',
260
+ path: `${base}/skins/list`,
261
+ handler: async (req, res) => {
262
+ const force = /[?&]refresh=1/.test(req.url || '');
263
+ const site = await fetchSiteSkins(force);
264
+ const installed = listInstalled();
265
+ const skinCenter = await probeSkinCenter(req.headers && req.headers.host, force);
266
+ // ⚠️ 站点 slug 与本机皮肤 id **不总是同一个字符串**:站点上 slug 是全站唯一的,
267
+ // 撞名时上传工具会加 `skin-` 前缀(实测 blue-throated-bee-eater 同时是桌宠 id)。
268
+ // 所以比对要归一:两边都去掉可选的 `skin-` 前缀再比,否则会显示成"未安装"。
269
+ const installedKeys = new Set();
270
+ for (const s of installed) {
271
+ installedKeys.add(normSkinId(s.id));
272
+ installedKeys.add(normSkinId(s.dirId));
273
+ }
274
+ sendJson(res, 200, {
275
+ ok: true,
276
+ skinCenter,
277
+ siteBase: siteBase(),
278
+ siteError: site.error,
279
+ installed,
280
+ site: site.items.map((it) => ({ ...it, installed: installedKeys.has(normSkinId(it.id)) })),
281
+ });
282
+ },
283
+ }));
284
+
285
+ /* ② 安装:下载 zip → 解到 $DSH_HOME/skins/<id> */
286
+ disposers.push(webServer.register({
287
+ kind: 'exact',
288
+ path: `${base}/skins/install`,
289
+ handler: async (req, res) => {
290
+ if ((req.method || 'GET').toUpperCase() !== 'POST') { sendJson(res, 405, { ok: false, error: 'method-not-allowed' }); return; }
291
+ const body = await readJsonBody(req);
292
+ if (!body) { sendJson(res, 400, { ok: false, error: 'bad-body' }); return; }
293
+ const base0 = siteBase();
294
+ if (!base0) { sendJson(res, 400, { ok: false, error: 'site-off' }); return; }
295
+ let url;
296
+ try { url = new URL(String(body.url || ''), base0); } catch { sendJson(res, 400, { ok: false, error: 'bad-url' }); return; }
297
+ const allowHost = new URL(base0).host.toLowerCase();
298
+ if (url.host.toLowerCase() !== allowHost) { sendJson(res, 400, { ok: false, error: 'host-not-allowed' }); return; }
299
+
300
+ const root = skinsRoot();
301
+ // 幂等:站点 id 与本机目录同名时直接返回(用户"再点一次"应该是立刻生效,而不是重下一遍)
302
+ const hinted = safeSkinId(body.id);
303
+ if (hinted && existsSync(join(root, hinted, 'skin.json'))) {
304
+ sendJson(res, 200, { ok: true, exists: true, id: hinted, dir: join(root, hinted) });
305
+ return;
306
+ }
307
+
308
+ let buf;
309
+ try {
310
+ const r = await fetch(url.toString(), { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
311
+ if (!r.ok) { sendJson(res, 502, { ok: false, error: 'http-' + r.status }); return; }
312
+ const len = Number(r.headers.get('content-length') || 0);
313
+ if (len && len > MAX_ZIP_BYTES) { sendJson(res, 413, { ok: false, error: 'too-large' }); return; }
314
+ buf = Buffer.from(await r.arrayBuffer());
315
+ if (buf.length > MAX_ZIP_BYTES) { sendJson(res, 413, { ok: false, error: 'too-large' }); return; }
316
+ } catch (e) {
317
+ sendJson(res, 502, { ok: false, error: e && e.name === 'TimeoutError' ? 'timeout' : 'network' });
318
+ return;
319
+ }
320
+
321
+ let entries;
322
+ try { entries = unzipSkinZip(buf); } catch (e) {
323
+ sendJson(res, 422, { ok: false, error: (e && e.code) || 'bad-zip' });
324
+ return;
325
+ }
326
+ const manifestEntry = entries.find((e) => !e.isDir && /^skin\.json$/i.test(e.name));
327
+ if (!manifestEntry) { sendJson(res, 422, { ok: false, error: 'no-skin-json' }); return; }
328
+ let manifest;
329
+ try { manifest = JSON.parse(manifestEntry.data.toString('utf8')); } catch { sendJson(res, 422, { ok: false, error: 'bad-skin-json' }); return; }
330
+ // 目录名以 skin.json 的 id 为准(不信任 URL/调用方给的名字)。
331
+ const skinId = safeSkinId(manifest && manifest.id) || hinted;
332
+ if (!skinId) { sendJson(res, 422, { ok: false, error: 'bad-skin-id' }); return; }
333
+ const dir = resolveInside(root, skinId);
334
+ if (!dir) { sendJson(res, 400, { ok: false, error: 'unsafe-id' }); return; }
335
+ if (existsSync(join(dir, 'skin.json'))) {
336
+ sendJson(res, 200, { ok: true, exists: true, id: skinId, dir });
337
+ return;
338
+ }
339
+
340
+ try {
341
+ rmSync(dir, { recursive: true, force: true });
342
+ mkdirSync(dir, { recursive: true });
343
+ let written = 0;
344
+ let skipped = 0;
345
+ for (const e of entries) {
346
+ if (e.isDir) continue;
347
+ if (SKIP_EXEC_RE.test(e.name)) { skipped += 1; continue; }
348
+ const target = join(dir, e.name);
349
+ const parent = join(target, '..');
350
+ mkdirSync(parent, { recursive: true });
351
+ writeFileSync(target, e.data);
352
+ written += 1;
353
+ }
354
+ const check = JSON.parse(readFileSync(join(dir, 'skin.json'), 'utf8'));
355
+ if (!check || !check.id) throw new Error('manifest lost after write');
356
+ sendJson(res, 200, {
357
+ ok: true,
358
+ id: skinId,
359
+ name: String(check.name || check.nameEn || skinId),
360
+ dir,
361
+ written,
362
+ skipped,
363
+ bytes: buf.length,
364
+ installedDir: root,
365
+ });
366
+ } catch (e) {
367
+ try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
368
+ try { log && log('skins install failed: ' + (e && e.message)); } catch { /* ignore */ }
369
+ sendJson(res, 500, { ok: false, error: 'extract-failed', detail: String(e && e.message).slice(0, 200) });
370
+ }
371
+ },
372
+ }));
373
+
374
+ /* ③ 卸载:只删皮肤根下的那个目录 */
375
+ disposers.push(webServer.register({
376
+ kind: 'exact',
377
+ path: `${base}/skins/remove`,
378
+ handler: async (req, res) => {
379
+ if ((req.method || 'GET').toUpperCase() !== 'POST') { sendJson(res, 405, { ok: false, error: 'method-not-allowed' }); return; }
380
+ const body = await readJsonBody(req);
381
+ const id = safeSkinId(body && body.id);
382
+ if (!id) { sendJson(res, 400, { ok: false, error: 'bad-id' }); return; }
383
+ const root = skinsRoot();
384
+ const dir = resolveInside(root, id);
385
+ if (!dir) { sendJson(res, 400, { ok: false, error: 'unsafe-id' }); return; }
386
+ if (!existsSync(dir)) { sendJson(res, 404, { ok: false, error: 'not-installed' }); return; }
387
+ try { rmSync(dir, { recursive: true, force: true }); } catch (e) {
388
+ sendJson(res, 500, { ok: false, error: 'remove-failed', detail: String(e && e.message).slice(0, 160) });
389
+ return;
390
+ }
391
+ sendJson(res, 200, { ok: true, id });
392
+ },
393
+ }));
394
+ }
package/package.json CHANGED
@@ -1,234 +1,234 @@
1
- {
2
- "name": "dsh-folia",
3
- "version": "4.9.1",
4
- "description": "Folia 壁纸引擎:把本机 Wallpaper Engine 壁纸搬进 DSH 界面(场景实时渲染 / 视频 / 网页),内置液态玻璃与 Folia 吉祥物,并可从自建站点拉取壁纸。单包形态(组件已合并)。",
5
- "type": "module",
6
- "engines": {
7
- "node": "\u003e=22.12.0",
8
- "dsh": "^0.2.0-rc.1"
9
- },
10
- "main": "lib/index.js",
11
- "types": "lib/types/index.d.ts",
12
- "icon": "./icon.png",
13
- "repository": {
14
- "type": "git",
15
- "url": "git+https://github.com/lehua-art/dsh-folia.git"
16
- },
17
- "keywords": [
18
- "dsh",
19
- "dsh-plugin",
20
- "deepseek-harness",
21
- "wallpaper",
22
- "folia",
23
- "theme",
24
- "background"
25
- ],
26
- "publishConfig": {
27
- "access": "public"
28
- },
29
- "exports": {
30
- ".": {
31
- "types": "./lib/types/index.d.ts",
32
- "default": "./lib/index.js"
33
- },
34
- "./client": {
35
- "types": "./lib/types/client.d.ts",
36
- "default": "./lib/client.js"
37
- },
38
- "./subpackage-boot.js": "./lib/subpackage-boot.js",
39
- "./cordis.patch.yml": "./cordis.patch.yml",
40
- "./package.json": "./package.json"
41
- },
42
- "files": [
43
- "icon.png",
44
- "lib/index.js",
45
- "lib/host-boot.js",
46
- "lib/log.js",
47
- "lib/notice.js",
48
- "lib/kernel/",
49
- "lib/kernel/data-dir.js",
50
- "lib/kernel/config.js",
51
- "lib/kernel/host-kernel.js",
52
- "lib/kernel/host-session.js",
53
- "lib/boot/",
54
- "lib/routes/",
55
- "lib/occlusion/",
56
- "lib/fontsets/",
57
- "lib/glass-presets/",
58
- "lib/about/",
59
- "lib/component-visibility.js",
60
- "lib/client.js",
61
- "lib/http-body.js",
62
- "lib/json-response.js",
63
- "lib/pkg-read.js",
64
- "lib/scene-manifest.js",
65
- "lib/settings-schema.js",
66
- "lib/subpackage-boot.js",
67
- "lib/media/",
68
- "lib/media/transcode.js",
69
- "lib/media/mp4-probe.js",
70
- "lib/media/scene-probe.js",
71
- "lib/media/video-preview.js",
72
- "lib/media/frame-store.js",
73
- "lib/media/media-dirs.js",
74
- "lib/media-origin.js",
75
- "lib/inventory.js",
76
- "lib/serve.js",
77
- "lib/faststart.js",
78
- "lib/cache-dir.js",
79
- "lib/upload-store.js",
80
- "lib/diag-trace.js",
81
- "lib/http-lifecycle.js",
82
- "lib/mp4-vfs.js",
83
- "lib/startup-sweep.js",
84
- "lib/fs-paths.js",
85
- "lib/upload-dir.js",
86
- "lib/wallpaper-scan.js",
87
- "lib/steam-discovery.js",
88
- "lib/webwallgl/",
89
- "lib/we-assets.js",
90
- "lib/shim-seed.js",
91
- "lib/we-props.js",
92
- "lib/we-focus-guard.js",
93
- "lib/types/index.d.ts",
94
- "lib/types/client.d.ts",
95
- "cordis.patch.yml",
96
- "assets/readme/hero-light.png",
97
- "assets/readme/hero-dark.png",
98
- "assets/readme/key-lang-zh-light.png",
99
- "assets/readme/key-lang-zh-dark.png",
100
- "assets/readme/key-lang-en-light.png",
101
- "assets/readme/key-lang-en-dark.png",
102
- "assets/readme/key-lang-start-light.png",
103
- "assets/readme/key-lang-start-dark.png",
104
- "assets/readme/key-get-light.png",
105
- "assets/readme/key-get-dark.png",
106
- "assets/readme/key-setup-light.png",
107
- "assets/readme/key-setup-dark.png",
108
- "assets/readme/key-docs-light.png",
109
- "assets/readme/key-docs-dark.png",
110
- "assets/readme/key-release-light.png",
111
- "assets/readme/key-release-dark.png",
112
- "assets/readme/stat-version-light.png",
113
- "assets/readme/stat-version-dark.png",
114
- "assets/readme/stat-formats-light.png",
115
- "assets/readme/stat-formats-dark.png",
116
- "assets/readme/stat-mascots-light.png",
117
- "assets/readme/stat-mascots-dark.png",
118
- "assets/readme/stat-modules-light.png",
119
- "assets/readme/stat-modules-dark.png",
120
- "assets/readme/section-intro-light.png",
121
- "assets/readme/section-intro-dark.png",
122
- "assets/readme/section-preview-light.png",
123
- "assets/readme/section-preview-dark.png",
124
- "assets/readme/section-start-light.png",
125
- "assets/readme/section-start-dark.png",
126
- "assets/readme/section-index-light.png",
127
- "assets/readme/section-index-dark.png",
128
- "assets/readme/section-features-light.png",
129
- "assets/readme/section-features-dark.png",
130
- "assets/readme/section-formats-light.png",
131
- "assets/readme/section-formats-dark.png",
132
- "assets/readme/section-install-light.png",
133
- "assets/readme/section-install-dark.png",
134
- "assets/readme/section-usage-light.png",
135
- "assets/readme/section-usage-dark.png",
136
- "assets/readme/section-pet-light.png",
137
- "assets/readme/section-pet-dark.png",
138
- "assets/readme/section-config-light.png",
139
- "assets/readme/section-config-dark.png",
140
- "assets/readme/section-compat-light.png",
141
- "assets/readme/section-compat-dark.png",
142
- "assets/readme/section-develop-light.png",
143
- "assets/readme/section-develop-dark.png",
144
- "assets/readme/section-community-light.png",
145
- "assets/readme/section-community-dark.png",
146
- "assets/readme/section-credits-light.png",
147
- "assets/readme/section-credits-dark.png",
148
- "assets/readme/section-support-light.png",
149
- "assets/readme/section-support-dark.png",
150
- "assets/readme/section-stars-light.png",
151
- "assets/readme/section-stars-dark.png",
152
- "assets/readme/SOURCES.md",
153
- "assets/readme/OFL-Jersey20.txt",
154
- "assets/readme/mascot-eating-rice-1280x1784.jpg",
155
- "README.md",
156
- "README.en.md",
157
- "README.beginner.md",
158
- "lib/components/",
159
- "lib/features.js",
160
- "lib/route-registry.js",
161
- "lib/settings/"
162
- ],
163
- "dsh": {
164
- "bundle": {
165
- "patch": "./cordis.patch.yml"
166
- },
167
- "client": {
168
- "inject": [
169
- "@deepseek-ai/dsh-client-runtime"
170
- ],
171
- "platform": "web",
172
- "immediately": true
173
- }
174
- },
175
- "license": "MIT",
176
- "scripts": {
177
- "build": "node scripts/build-client.mjs",
178
- "install:source": "node scripts/install-source.mjs",
179
- "install:release": "node scripts/install-release.mjs",
180
- "pack:release": "node scripts/pack-release.mjs",
181
- "verify:deployment": "node test/verify-deployment.mjs",
182
- "verify:pet-link": "node test/verify-pet-link.mjs",
183
- "verify:pet-link:integration": "node test/verify-pet-hotplug-integration.mjs",
184
- "verify:pet-link:browser": "node test/verify-pet-tuner-browser.mjs",
185
- "verify": "node test/run-guards.mjs verify",
186
- "verify:docs": "node test/run-guards.mjs docs",
187
- "verify:bridge": "node test/verify-bridge-ci.mjs --provision",
188
- "verify:bridge:strict": "node test/verify-media-bridge.mjs --provision",
189
- "verify:e2e": "node test/e2e-web-media-origin.mjs",
190
- "lint": "eslint .",
191
- "typecheck": "tsc --noEmit -p tsconfig.json",
192
- "verify:static": "node test/verify-static.mjs",
193
- "smoke": "node test/run-guards.mjs smoke",
194
- "verify:all": "node test/run-guards.mjs all",
195
- "prepack": "node scripts/prepare.mjs",
196
- "test:boot": "node test/boot-http.mjs",
197
- "test:pets": "node test/pets-http.mjs",
198
- "test:pets-layer": "node test/pets-layer.mjs",
199
- "test:features": "node test/verify-features.mjs",
200
- "audit": "node test/run-audit.mjs",
201
- "gate": "node scripts/build-client.mjs \u0026\u0026 node test/run-guards.mjs smoke \u0026\u0026 node test/verify-features.mjs \u0026\u0026 node test/verify-route-registry.mjs \u0026\u0026 node test/verify-settings-ownership.mjs \u0026\u0026 node test/boot-http.mjs \u0026\u0026 node test/pets-http.mjs \u0026\u0026 node test/pets-layer.mjs"
202
- },
203
- "peerDependencies": {
204
- "@deepseek-ai/cordis": "^4.0.4",
205
- "@deepseek-ai/dsh-client-runtime": "\u003e=0.1.5-rc.1",
206
- "@deepseek-ai/dsh-client-ui-slots": "\u003e=0.1.5-rc.1",
207
- "@deepseek-ai/dsh-host-webserver": "\u003e=0.1.5-rc.1",
208
- "react": "^18.2.0"
209
- },
210
- "peerDependenciesMeta": {
211
- "@deepseek-ai/dsh-client-runtime": {
212
- "optional": true
213
- },
214
- "@deepseek-ai/dsh-client-ui-slots": {
215
- "optional": true
216
- },
217
- "@deepseek-ai/dsh-host-webserver": {
218
- "optional": true
219
- }
220
- },
221
- "dependencies": {
222
- "webwallgl": "^3.0.0",
223
- "x11": "^4.3.1"
224
- },
225
- "overrides": {
226
- "js-yaml": "4.3.2"
227
- },
228
- "devDependencies": {
229
- "@eslint/js": "^10.0.1",
230
- "@types/node": "^26.6.5",
231
- "eslint": "^10.12.0",
232
- "typescript": "^7.0.2"
233
- }
1
+ {
2
+ "name": "dsh-folia",
3
+ "version": "4.10.1",
4
+ "description": "Folia 壁纸引擎:把本机 Wallpaper Engine 壁纸搬进 DSH 界面(场景实时渲染 / 视频 / 网页),内置液态玻璃与 Folia 吉祥物,并可从自建站点拉取壁纸。单包形态(组件已合并)。",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22.12.0",
8
+ "dsh": "^0.2.0-rc.1"
9
+ },
10
+ "main": "lib/index.js",
11
+ "types": "lib/types/index.d.ts",
12
+ "icon": "./icon.png",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/lehua-art/dsh-folia.git"
16
+ },
17
+ "keywords": [
18
+ "dsh",
19
+ "dsh-plugin",
20
+ "deepseek-harness",
21
+ "wallpaper",
22
+ "folia",
23
+ "theme",
24
+ "background"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "exports": {
30
+ ".": {
31
+ "types": "./lib/types/index.d.ts",
32
+ "default": "./lib/index.js"
33
+ },
34
+ "./client": {
35
+ "types": "./lib/types/client.d.ts",
36
+ "default": "./lib/client.js"
37
+ },
38
+ "./subpackage-boot.js": "./lib/subpackage-boot.js",
39
+ "./cordis.patch.yml": "./cordis.patch.yml",
40
+ "./package.json": "./package.json"
41
+ },
42
+ "files": [
43
+ "icon.png",
44
+ "lib/index.js",
45
+ "lib/host-boot.js",
46
+ "lib/log.js",
47
+ "lib/notice.js",
48
+ "lib/kernel/",
49
+ "lib/kernel/data-dir.js",
50
+ "lib/kernel/config.js",
51
+ "lib/kernel/host-kernel.js",
52
+ "lib/kernel/host-session.js",
53
+ "lib/boot/",
54
+ "lib/routes/",
55
+ "lib/occlusion/",
56
+ "lib/fontsets/",
57
+ "lib/glass-presets/",
58
+ "lib/about/",
59
+ "lib/component-visibility.js",
60
+ "lib/client.js",
61
+ "lib/http-body.js",
62
+ "lib/json-response.js",
63
+ "lib/pkg-read.js",
64
+ "lib/scene-manifest.js",
65
+ "lib/settings-schema.js",
66
+ "lib/subpackage-boot.js",
67
+ "lib/media/",
68
+ "lib/media/transcode.js",
69
+ "lib/media/mp4-probe.js",
70
+ "lib/media/scene-probe.js",
71
+ "lib/media/video-preview.js",
72
+ "lib/media/frame-store.js",
73
+ "lib/media/media-dirs.js",
74
+ "lib/media-origin.js",
75
+ "lib/inventory.js",
76
+ "lib/serve.js",
77
+ "lib/faststart.js",
78
+ "lib/cache-dir.js",
79
+ "lib/upload-store.js",
80
+ "lib/diag-trace.js",
81
+ "lib/http-lifecycle.js",
82
+ "lib/mp4-vfs.js",
83
+ "lib/startup-sweep.js",
84
+ "lib/fs-paths.js",
85
+ "lib/upload-dir.js",
86
+ "lib/wallpaper-scan.js",
87
+ "lib/steam-discovery.js",
88
+ "lib/webwallgl/",
89
+ "lib/we-assets.js",
90
+ "lib/shim-seed.js",
91
+ "lib/we-props.js",
92
+ "lib/we-focus-guard.js",
93
+ "lib/types/index.d.ts",
94
+ "lib/types/client.d.ts",
95
+ "cordis.patch.yml",
96
+ "assets/readme/hero-light.png",
97
+ "assets/readme/hero-dark.png",
98
+ "assets/readme/key-lang-zh-light.png",
99
+ "assets/readme/key-lang-zh-dark.png",
100
+ "assets/readme/key-lang-en-light.png",
101
+ "assets/readme/key-lang-en-dark.png",
102
+ "assets/readme/key-lang-start-light.png",
103
+ "assets/readme/key-lang-start-dark.png",
104
+ "assets/readme/key-get-light.png",
105
+ "assets/readme/key-get-dark.png",
106
+ "assets/readme/key-setup-light.png",
107
+ "assets/readme/key-setup-dark.png",
108
+ "assets/readme/key-docs-light.png",
109
+ "assets/readme/key-docs-dark.png",
110
+ "assets/readme/key-release-light.png",
111
+ "assets/readme/key-release-dark.png",
112
+ "assets/readme/stat-version-light.png",
113
+ "assets/readme/stat-version-dark.png",
114
+ "assets/readme/stat-formats-light.png",
115
+ "assets/readme/stat-formats-dark.png",
116
+ "assets/readme/stat-mascots-light.png",
117
+ "assets/readme/stat-mascots-dark.png",
118
+ "assets/readme/stat-modules-light.png",
119
+ "assets/readme/stat-modules-dark.png",
120
+ "assets/readme/section-intro-light.png",
121
+ "assets/readme/section-intro-dark.png",
122
+ "assets/readme/section-preview-light.png",
123
+ "assets/readme/section-preview-dark.png",
124
+ "assets/readme/section-start-light.png",
125
+ "assets/readme/section-start-dark.png",
126
+ "assets/readme/section-index-light.png",
127
+ "assets/readme/section-index-dark.png",
128
+ "assets/readme/section-features-light.png",
129
+ "assets/readme/section-features-dark.png",
130
+ "assets/readme/section-formats-light.png",
131
+ "assets/readme/section-formats-dark.png",
132
+ "assets/readme/section-install-light.png",
133
+ "assets/readme/section-install-dark.png",
134
+ "assets/readme/section-usage-light.png",
135
+ "assets/readme/section-usage-dark.png",
136
+ "assets/readme/section-pet-light.png",
137
+ "assets/readme/section-pet-dark.png",
138
+ "assets/readme/section-config-light.png",
139
+ "assets/readme/section-config-dark.png",
140
+ "assets/readme/section-compat-light.png",
141
+ "assets/readme/section-compat-dark.png",
142
+ "assets/readme/section-develop-light.png",
143
+ "assets/readme/section-develop-dark.png",
144
+ "assets/readme/section-community-light.png",
145
+ "assets/readme/section-community-dark.png",
146
+ "assets/readme/section-credits-light.png",
147
+ "assets/readme/section-credits-dark.png",
148
+ "assets/readme/section-support-light.png",
149
+ "assets/readme/section-support-dark.png",
150
+ "assets/readme/section-stars-light.png",
151
+ "assets/readme/section-stars-dark.png",
152
+ "assets/readme/SOURCES.md",
153
+ "assets/readme/OFL-Jersey20.txt",
154
+ "assets/readme/mascot-eating-rice-1280x1784.jpg",
155
+ "README.md",
156
+ "README.en.md",
157
+ "README.beginner.md",
158
+ "lib/components/",
159
+ "lib/features.js",
160
+ "lib/route-registry.js",
161
+ "lib/settings/",
162
+ "lib/routes/skins.js"
163
+ ],
164
+ "dsh": {
165
+ "bundle": {
166
+ "patch": "./cordis.patch.yml"
167
+ },
168
+ "client": {
169
+ "inject": [
170
+ "@deepseek-ai/dsh-client-runtime"
171
+ ],
172
+ "platform": "web",
173
+ "immediately": true
174
+ }
175
+ },
176
+ "license": "MIT",
177
+ "peerDependencies": {
178
+ "@deepseek-ai/cordis": "^4.0.4",
179
+ "@deepseek-ai/dsh-client-runtime": ">=0.1.5-rc.1",
180
+ "@deepseek-ai/dsh-client-ui-slots": ">=0.1.5-rc.1",
181
+ "@deepseek-ai/dsh-host-webserver": ">=0.1.5-rc.1",
182
+ "react": "^18.2.0"
183
+ },
184
+ "peerDependenciesMeta": {
185
+ "@deepseek-ai/dsh-client-runtime": {
186
+ "optional": true
187
+ },
188
+ "@deepseek-ai/dsh-client-ui-slots": {
189
+ "optional": true
190
+ },
191
+ "@deepseek-ai/dsh-host-webserver": {
192
+ "optional": true
193
+ }
194
+ },
195
+ "dependencies": {
196
+ "webwallgl": "^3.0.0",
197
+ "x11": "^4.3.1"
198
+ },
199
+ "overrides": {
200
+ "js-yaml": "4.3.2"
201
+ },
202
+ "devDependencies": {
203
+ "@eslint/js": "^10.0.1",
204
+ "@types/node": "^26.6.5",
205
+ "eslint": "^10.12.0",
206
+ "typescript": "^7.0.2"
207
+ },
208
+ "scripts": {
209
+ "build": "node scripts/build-client.mjs",
210
+ "install:source": "node scripts/install-source.mjs",
211
+ "install:release": "node scripts/install-release.mjs",
212
+ "pack:release": "node scripts/pack-release.mjs",
213
+ "verify:deployment": "node test/verify-deployment.mjs",
214
+ "verify:pet-link": "node test/verify-pet-link.mjs",
215
+ "verify:pet-link:integration": "node test/verify-pet-hotplug-integration.mjs",
216
+ "verify:pet-link:browser": "node test/verify-pet-tuner-browser.mjs",
217
+ "verify": "node test/run-guards.mjs verify",
218
+ "verify:docs": "node test/run-guards.mjs docs",
219
+ "verify:bridge": "node test/verify-bridge-ci.mjs --provision",
220
+ "verify:bridge:strict": "node test/verify-media-bridge.mjs --provision",
221
+ "verify:e2e": "node test/e2e-web-media-origin.mjs",
222
+ "lint": "eslint .",
223
+ "typecheck": "tsc --noEmit -p tsconfig.json",
224
+ "verify:static": "node test/verify-static.mjs",
225
+ "smoke": "node test/run-guards.mjs smoke",
226
+ "verify:all": "node test/run-guards.mjs all",
227
+ "test:boot": "node test/boot-http.mjs",
228
+ "test:pets": "node test/pets-http.mjs",
229
+ "test:pets-layer": "node test/pets-layer.mjs",
230
+ "test:features": "node test/verify-features.mjs",
231
+ "audit": "node test/run-audit.mjs",
232
+ "gate": "node scripts/build-client.mjs && node test/run-guards.mjs smoke && node test/verify-features.mjs && node test/verify-route-registry.mjs && node test/verify-settings-ownership.mjs && node test/boot-http.mjs && node test/pets-http.mjs && node test/pets-layer.mjs"
233
+ }
234
234
  }