dsh-folia 2.5.1 → 2.5.7

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.
@@ -0,0 +1,186 @@
1
+ /**
2
+ * routes/modules.js — 数据模块索引(站点侧声明"有哪些数据模块")。
3
+ *
4
+ * 为什么需要它:插件的能力是固定的(壁纸库、开机动画…),但**站点会一直加新东西**。
5
+ * 把"有哪些模块、每个模块的清单在哪、该用哪个 handler 处理"放进一份索引里,插件就不必
6
+ * 为每加一批素材发一次版 —— 它读索引,按 handler 决定自己认不认识。
7
+ *
8
+ * 解析顺序(**站点优先,R2 兜底,内置最后**):
9
+ *
10
+ * ① `<site>/data/modules.json` 站点自己维护的索引(你以后放进 Pages 即优先)
11
+ * ② `<assets>/data/modules.json` R2 桶上的索引(当前由我们维护)
12
+ * ③ 内置默认 两条都读不到时的最小集合
13
+ *
14
+ * 页面/助手只调 `/folia/modules` 这一个入口,具体从哪来的写在 `source` 里 —— 排查时
15
+ * 不用猜"到底读的哪份"。
16
+ */
17
+ import { sendJson } from '../json-response.js';
18
+
19
+ /** 站点基址(与 inventory 同一套环境变量,保持"换站只改一处"的手感)。 */
20
+ const DEFAULT_SITE = 'https://folia-5ve.pages.dev';
21
+ /** 素材桶基址:托管 <site> 上暂时没有的模块清单(例如 boot 素材)。 */
22
+ const DEFAULT_ASSETS = 'https://pub-1275321f146e479eaac54948f4cdad6f.r2.dev';
23
+
24
+ /**
25
+ * 索引可能落在哪。**按顺序试**,第一个返回 JSON 的胜出:
26
+ *
27
+ * /data/modules.json 站点自己的静态路径(你以后把文件放进 Pages 时用这条)
28
+ * /media/data/modules.json 站点的 /media 代理 —— 它把 /media/X 映射到 R2 的键 X,
29
+ * 所以这条能读到 R2 上的 data/modules.json(现在走这条)
30
+ *
31
+ * 为什么重要:站点的 /data/* 没有 Function,未知路径会回退 index.html;
32
+ * 而 /media/* 有代理。走 /media/ 让"全部数据都从站点域名来"成立,R2 只是背后的仓库。
33
+ */
34
+ const MODULES_PATHS = ['/data/modules.json', '/media/data/modules.json'];
35
+ const TTL_MS = 60000;
36
+ const TIMEOUT_MS = 8000;
37
+
38
+ function siteBase() {
39
+ const raw = process.env.DSH_FOLIA_SITE;
40
+ if (typeof raw === 'string' && raw.trim().toLowerCase() === 'off') return null;
41
+ const base = (typeof raw === 'string' && raw.trim()) ? raw.trim() : DEFAULT_SITE;
42
+ return base.replace(/\/+$/, '');
43
+ }
44
+
45
+ function assetsBase() {
46
+ const raw = process.env.DSH_FOLIA_ASSETS;
47
+ const base = (typeof raw === 'string' && raw.trim()) ? raw.trim() : DEFAULT_ASSETS;
48
+ return base.replace(/\/+$/, '');
49
+ }
50
+
51
+ /**
52
+ * 内置默认索引。**只在两条网络源都不可用时使用** —— 它同时也是一份"契约示例":
53
+ * 想加模块时照这个形状写,不必读代码。
54
+ */
55
+ const BUILTIN = {
56
+ version: 1,
57
+ modules: [
58
+ {
59
+ id: 'wallpapers',
60
+ name: '壁纸',
61
+ handler: 'wallpaper-library',
62
+ status: 'live',
63
+ manifest: '/data/catalog.json',
64
+ fallbackManifest: '/manifest.json',
65
+ },
66
+ {
67
+ id: 'boot',
68
+ name: '开机动画',
69
+ handler: 'boot-animation',
70
+ status: 'planned',
71
+ manifest: '/boot/manifest.json',
72
+ },
73
+ ],
74
+ };
75
+
76
+ /** @type {{ at: number, payload: any | null }} */
77
+ let cache = { at: 0, payload: null };
78
+
79
+ /** 形状校验:宁可判为无效(回落下一层),也不把半截数据当索引下发。 */
80
+ function normalize(doc, origin) {
81
+ if (!doc || typeof doc !== 'object') return null;
82
+ const list = Array.isArray(doc.modules) ? doc.modules : null;
83
+ if (!list) return null;
84
+ const modules = [];
85
+ for (const m of list) {
86
+ if (!m || typeof m !== 'object') continue;
87
+ const id = String(m.id || '').trim();
88
+ const handler = String(m.handler || '').trim();
89
+ if (!id || !handler) continue; // 没有 id 或 handler 的条目无法被使用
90
+ modules.push({
91
+ id,
92
+ name: String(m.name || id),
93
+ handler,
94
+ status: String(m.status || 'live'),
95
+ manifest: m.manifest ? String(m.manifest) : null,
96
+ fallbackManifest: m.fallbackManifest ? String(m.fallbackManifest) : null,
97
+ description: m.description ? String(m.description) : null,
98
+ requirements: m.requirements && typeof m.requirements === 'object' ? m.requirements : null,
99
+ });
100
+ }
101
+ return { version: doc.version || 1, updatedAt: doc.updatedAt || null, origin, modules };
102
+ }
103
+
104
+ async function fetchJson(url) {
105
+ // 网络抖动(境外 CDN 偶发 connect timeout)不该让索引退回内置集合 —— 重试一次。
106
+ // 只对 network/timeout 重试;`not-json` / `http-4xx` 是确定性结果,重试没意义。
107
+ let lastError = 'network';
108
+ for (let attempt = 0; attempt < 2; attempt++) {
109
+ try {
110
+ const res = await fetch(url, {
111
+ headers: { accept: 'application/json' },
112
+ signal: AbortSignal.timeout(TIMEOUT_MS),
113
+ });
114
+ if (!res.ok) return { doc: null, error: 'http-' + res.status };
115
+ const ct = String(res.headers.get('content-type') || '');
116
+ // 站点把未知路径回落到 index.html,这里必须挡住:HTML 不是索引。
117
+ if (ct && !/json/i.test(ct)) return { doc: null, error: 'not-json' };
118
+ return { doc: await res.json(), error: null };
119
+ } catch (e) {
120
+ const msg = String((e && e.message) || '');
121
+ lastError = /timeout|abort/i.test(msg) ? 'timeout' : 'network';
122
+ if (attempt === 0) await new Promise((r) => setTimeout(r, 600));
123
+ }
124
+ }
125
+ return { doc: null, error: lastError };
126
+ }
127
+
128
+ /**
129
+ * 解析模块索引。永不抛 —— 索引只是"能多发现一点东西",读不到就必须退回内置集合,
130
+ * 绝不能让面板因为一个索引文件而空白。
131
+ */
132
+ export async function resolveModules(force = false) {
133
+ if (!force && Date.now() - cache.at < TTL_MS && cache.payload) return cache.payload;
134
+
135
+ const tried = [];
136
+ const site = siteBase();
137
+ const assets = assetsBase();
138
+
139
+ // 站点先试(它优先),每个基址再按路径表试;R2 直接地址留作最后兜底。
140
+ const bases = [
141
+ ...(site ? [{ base: site, origin: 'site' }] : []),
142
+ { base: assets, origin: 'assets' },
143
+ ];
144
+ for (const { base, origin } of bases) {
145
+ for (const path of MODULES_PATHS) {
146
+ const url = base + path;
147
+ const { doc, error } = await fetchJson(url);
148
+ tried.push({ url, error });
149
+ if (doc) {
150
+ const norm = normalize(doc, origin);
151
+ if (norm) {
152
+ // 相对清单路径补成绝对 URL:调用方不必再关心基址是谁。
153
+ for (const m of norm.modules) {
154
+ if (m.manifest && m.manifest.startsWith('/')) m.manifest = base + m.manifest;
155
+ if (m.fallbackManifest && m.fallbackManifest.startsWith('/')) m.fallbackManifest = base + m.fallbackManifest;
156
+ }
157
+ const payload = { ok: true, source: norm.origin, url, updatedAt: norm.updatedAt, modules: norm.modules, tried };
158
+ cache = { at: Date.now(), payload };
159
+ return payload;
160
+ }
161
+ tried[tried.length - 1].error = 'bad-shape';
162
+ }
163
+ }
164
+ }
165
+
166
+ const payload = { ok: false, source: 'builtin', url: null, updatedAt: null, modules: BUILTIN.modules, tried };
167
+ cache = { at: Date.now(), payload };
168
+ return payload;
169
+ }
170
+
171
+ export function registerModulesRoutes(webServer, c) {
172
+ const { disposers, base: BASE } = c;
173
+
174
+ disposers.push(webServer.register({
175
+ kind: 'exact',
176
+ path: `${BASE}/modules`,
177
+ handler: async (req, res) => {
178
+ let force = false;
179
+ try {
180
+ force = new URL(req.url || '/', 'http://x').searchParams.get('refresh') === '1';
181
+ } catch { /* 解析失败就当没传 */ }
182
+ const payload = await resolveModules(force);
183
+ sendJson(res, 200, payload);
184
+ },
185
+ }));
186
+ }
@@ -596,6 +596,8 @@ const DEFAULTS = {
596
596
  contentRatingFilter: "everyone",
597
597
  // Wallpaper-type filter (all / video / web / image / scene). "all" disables it.
598
598
  typeFilter: "all",
599
+ // 壁纸来源:all / local(本机 Wallpaper Engine 库 + 上传)/ site(我们站点)
600
+ sourceFilter: "all",
599
601
  // Thumbnail-card style: "classic" (WE's original aspect-ratio 16/9 cards —
600
602
  // the CD-like look the author liked; can overlap in older browsers) or
601
603
  // "fixed" (fixed-height cards that never overlap). The vinyl
@@ -1040,6 +1042,7 @@ const KINDS = {
1040
1042
  objectFit: { kind: 'enum', values: OBJECT_FIT_VALUES },
1041
1043
  contentRatingFilter: { kind: 'enum', values: RATING_VALUES },
1042
1044
  typeFilter: { kind: 'enum', values: TYPE_VALUES },
1045
+ sourceFilter: { kind: 'enum', values: ['all', 'local', 'site'] },
1043
1046
  pickerLayout: { kind: 'enum', values: PICKER_LAYOUT_VALUES },
1044
1047
  edgeCompat: { kind: 'boolTrue' },
1045
1048
  accent: { kind: 'hex' },
@@ -1291,7 +1294,7 @@ const RESET_TABS = {
1291
1294
  library: [
1292
1295
  'switchTransition', 'switchTransitionDir', 'switchTransitionSpeed',
1293
1296
  'rotationEnabled', 'rotationGroupId', 'rotationGroups', 'rotationSeeded',
1294
- 'contentRatingFilter', 'typeFilter', 'pickerLayout',
1297
+ 'contentRatingFilter', 'typeFilter', 'sourceFilter', 'pickerLayout',
1295
1298
  ],
1296
1299
  // 外观:玻璃 / 侧栏与四角 / 标题栏 / 思考块 / 浮层 / 主题预设 / 字体 / UI 配色。
1297
1300
  appearance: [
@@ -1926,7 +1929,7 @@ const SETTINGS_NAMESPACES = {
1926
1929
  'id', 'hiddenIds', 'rotationEnabled', 'rotationGroupId', 'rotationGroups', 'rotationSeeded',
1927
1930
  'playbackRate', 'videoVolume', 'videoAudioEnabled', 'fpsCap', 'sceneLive', 'sceneLiveFps',
1928
1931
  'liveBootDelay', 'sceneLiveFailures', 'pauseOnHidden', 'pauseOnBlur', 'pauseOnBattery',
1929
- 'flip', 'objectFit', 'contentRatingFilter', 'typeFilter', 'pickerLayout', 'edgeCompat',
1932
+ 'flip', 'objectFit', 'contentRatingFilter', 'typeFilter', 'sourceFilter', 'pickerLayout', 'edgeCompat',
1930
1933
  'userProps', 'noticeSeen', 'layerPositionX', 'layerPositionY', 'layerScale',
1931
1934
  ],
1932
1935
  // 玻璃 / 材质 / 侧栏 / 过渡(`accent`、`glassMode` 与皮肤让路之外的整套玻璃登记表都在这里)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-folia",
3
- "version": "2.5.1",
3
+ "version": "2.5.7",
4
4
  "description": "Folia 壁纸引擎:把本机 Wallpaper Engine 壁纸搬进 DSH 界面(场景实时渲染 / 视频 / 网页),内置液态玻璃与 Folia 吉祥物,并可从自建站点拉取壁纸。单包形态(组件已合并)。",
5
5
  "type": "module",
6
6
  "engines": {
@@ -50,6 +50,7 @@
50
50
  "lib/kernel/config.js",
51
51
  "lib/kernel/host-kernel.js",
52
52
  "lib/kernel/host-session.js",
53
+ "lib/boot/",
53
54
  "lib/routes/",
54
55
  "lib/occlusion/",
55
56
  "lib/fontsets/",
@@ -188,7 +189,9 @@
188
189
  "verify:static": "node test/verify-static.mjs",
189
190
  "smoke": "node test/run-guards.mjs smoke",
190
191
  "verify:all": "node test/run-guards.mjs all",
191
- "prepack": "node scripts/prepare.mjs"
192
+ "prepack": "node scripts/prepare.mjs",
193
+ "test:boot": "node test/boot-http.mjs",
194
+ "gate": "node scripts/build-client.mjs \u0026\u0026 node test/run-guards.mjs smoke \u0026\u0026 node test/boot-http.mjs"
192
195
  },
193
196
  "peerDependencies": {
194
197
  "@deepseek-ai/cordis": "^4.0.4",