beauticode-dsh 1.0.2 → 1.0.4

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.
@@ -90,6 +90,47 @@ export function isPidAlive(pid) {
90
90
  }
91
91
  }
92
92
 
93
+ let livenessModulePromise;
94
+
95
+ function loadCoreLiveness() {
96
+ if (!livenessModulePromise) {
97
+ livenessModulePromise = (async () => {
98
+ const candidates = [
99
+ "@beauticode/core",
100
+ new URL("./vendor/core/index.js", import.meta.url).href,
101
+ new URL("../../packages/core/dist/index.js", import.meta.url).href,
102
+ ];
103
+ for (const specifier of candidates) {
104
+ try {
105
+ const mod = await import(specifier);
106
+ if (typeof mod.isRecordedPidLive === "function") return mod;
107
+ } catch {
108
+ /* try the next resolution path */
109
+ }
110
+ }
111
+ return null;
112
+ })();
113
+ }
114
+ return livenessModulePromise;
115
+ }
116
+
117
+ async function isLiveRecordedPid(pid, startedAt, mtimeMs) {
118
+ if (!isPidAlive(pid)) return false;
119
+ const recorded =
120
+ typeof startedAt === "string" && startedAt
121
+ ? startedAt
122
+ : Number.isFinite(mtimeMs)
123
+ ? new Date(mtimeMs).toISOString()
124
+ : null;
125
+ try {
126
+ const core = await loadCoreLiveness();
127
+ if (core) return await core.isRecordedPidLive(pid, recorded);
128
+ } catch {
129
+ /* fall back to the cheap PID check */
130
+ }
131
+ return true;
132
+ }
133
+
93
134
  export function stripPathQuotes(value) {
94
135
  const text = String(value ?? "").trim();
95
136
  if (text.length >= 2) {
@@ -195,6 +236,7 @@ export async function writeDshControlFile(opts) {
195
236
  pid,
196
237
  url: new URL(url).origin,
197
238
  token,
239
+ startedAt: new Date().toISOString(),
198
240
  });
199
241
  }
200
242
 
@@ -219,6 +261,7 @@ export async function writeSessionHostFile(opts) {
219
261
  pid,
220
262
  url: new URL(url).origin,
221
263
  token,
264
+ startedAt: new Date().toISOString(),
222
265
  });
223
266
  }
224
267
 
@@ -253,8 +296,11 @@ export async function removeDshControlFile(opts) {
253
296
  export async function readDshControlFile(dataRoot, opts = {}) {
254
297
  const file = controlFilePath(dataRoot);
255
298
  let raw;
299
+ let mtimeMs = 0;
256
300
  try {
257
- raw = await fs.readFile(file, "utf8");
301
+ const [text, stat] = await Promise.all([fs.readFile(file, "utf8"), fs.stat(file)]);
302
+ raw = text;
303
+ mtimeMs = stat.mtimeMs;
258
304
  } catch (error) {
259
305
  if (error && typeof error === "object" && error.code === "ENOENT") return null;
260
306
  throw error;
@@ -277,13 +323,19 @@ export async function readDshControlFile(dataRoot, opts = {}) {
277
323
  ) {
278
324
  return null;
279
325
  }
280
- if (!opts.allowDead && !isPidAlive(parsed.pid)) return null;
326
+ if (
327
+ !opts.allowDead &&
328
+ !(await isLiveRecordedPid(parsed.pid, parsed.startedAt, mtimeMs))
329
+ ) {
330
+ return null;
331
+ }
281
332
  return {
282
333
  schema: CONTROL_SCHEMA,
283
334
  host: "dsh",
284
335
  pid: parsed.pid,
285
336
  url: new URL(parsed.url).origin,
286
337
  token: parsed.token,
338
+ startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : null,
287
339
  };
288
340
  }
289
341
 
@@ -305,8 +357,11 @@ export async function removeSessionHostFile(opts) {
305
357
  export async function readSessionHostFile(dataRoot, opts = {}) {
306
358
  const file = sessionHostFilePath(dataRoot);
307
359
  let raw;
360
+ let mtimeMs = 0;
308
361
  try {
309
- raw = await fs.readFile(file, "utf8");
362
+ const [text, stat] = await Promise.all([fs.readFile(file, "utf8"), fs.stat(file)]);
363
+ raw = text;
364
+ mtimeMs = stat.mtimeMs;
310
365
  } catch (error) {
311
366
  if (error && typeof error === "object" && error.code === "ENOENT") return null;
312
367
  throw error;
@@ -329,21 +384,30 @@ export async function readSessionHostFile(dataRoot, opts = {}) {
329
384
  ) {
330
385
  return null;
331
386
  }
332
- if (!opts.allowDead && !isPidAlive(parsed.pid)) return null;
387
+ if (
388
+ !opts.allowDead &&
389
+ !(await isLiveRecordedPid(parsed.pid, parsed.startedAt, mtimeMs))
390
+ ) {
391
+ return null;
392
+ }
333
393
  return {
334
394
  schema: SESSION_HOST_SCHEMA,
335
395
  host: parsed.host,
336
396
  pid: parsed.pid,
337
397
  url: new URL(parsed.url).origin,
338
398
  token: parsed.token,
399
+ startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : null,
339
400
  };
340
401
  }
341
402
 
342
403
  export async function readTrayClaim(dataRoot, opts = {}) {
343
404
  const file = trayClaimFilePath(dataRoot);
344
405
  let raw;
406
+ let mtimeMs = 0;
345
407
  try {
346
- raw = await fs.readFile(file, "utf8");
408
+ const [text, stat] = await Promise.all([fs.readFile(file, "utf8"), fs.stat(file)]);
409
+ raw = text;
410
+ mtimeMs = stat.mtimeMs;
347
411
  } catch (error) {
348
412
  if (error && typeof error === "object" && error.code === "ENOENT") return null;
349
413
  throw error;
@@ -362,7 +426,12 @@ export async function readTrayClaim(dataRoot, opts = {}) {
362
426
  ) {
363
427
  return null;
364
428
  }
365
- if (!opts.allowDead && !isPidAlive(parsed.pid)) return null;
429
+ if (
430
+ !opts.allowDead &&
431
+ !(await isLiveRecordedPid(parsed.pid, parsed.startedAt, mtimeMs))
432
+ ) {
433
+ return null;
434
+ }
366
435
  return {
367
436
  schema: TRAY_CLAIM_SCHEMA,
368
437
  pid: parsed.pid,
package/cordis.patch.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  - insert:
2
2
  - id: beauticode-bridge
3
- name: 'beauticode-dsh'
3
+ name: beauticode-dsh
4
4
  inject: [webServer]
@@ -0,0 +1,253 @@
1
+ import fs from "node:fs";
2
+ import fsp from "node:fs/promises";
3
+ import path from "node:path";
4
+ import crypto from "node:crypto";
5
+ import { Readable } from "node:stream";
6
+ import { Transform } from "node:stream";
7
+ import { pipeline } from "node:stream/promises";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ const here = path.dirname(fileURLToPath(import.meta.url));
11
+ const SKIN_ID = /^skin-[a-z0-9]{8,40}$/;
12
+ const MAX_IMAGE_BYTES = 18 * 1024 * 1024;
13
+ const MAX_VIDEO_BYTES = 800 * 1024 * 1024;
14
+ const INSTALL_TIMEOUT_MS = 30 * 60 * 1000;
15
+ const LOOPBACK = new Set(["127.0.0.1", "localhost", "::1"]);
16
+
17
+ export function isSafeSkinId(id) {
18
+ return typeof id === "string" && SKIN_ID.test(id);
19
+ }
20
+
21
+ export function normalizeSkinCenterUrl(value) {
22
+ if (typeof value !== "string") return null;
23
+ const trimmed = value.trim();
24
+ if (!trimmed) return null;
25
+ try {
26
+ const url = new URL(trimmed);
27
+ if (url.username || url.password || url.hash) return null;
28
+ if (url.protocol !== "https:" && url.protocol !== "http:") return null;
29
+ if (url.protocol === "http:" && !LOOPBACK.has(url.hostname.toLowerCase())) return null;
30
+ const pathname = url.pathname === "/" ? "" : url.pathname.replace(/\/+$/, "");
31
+ return `${url.origin}${pathname}`;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ export async function readBundledSkinCenterUrl() {
38
+ try {
39
+ const raw = JSON.parse(await fsp.readFile(path.join(here, "skin-center.json"), "utf8"));
40
+ return normalizeSkinCenterUrl(raw.url);
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ export async function resolveConfiguredSkinCenterUrl() {
47
+ return (
48
+ normalizeSkinCenterUrl(process.env.BEAUTICODE_SKIN_CENTER) ??
49
+ (await readBundledSkinCenterUrl())
50
+ );
51
+ }
52
+
53
+ export function skinUrl(center, id, part = "") {
54
+ const origin = normalizeSkinCenterUrl(center);
55
+ if (!origin || !isSafeSkinId(id)) {
56
+ throw new Error("Skin center is not configured.");
57
+ }
58
+ return part ? `${origin}/api/skins/${id}/${part}` : `${origin}/api/skins/${id}`;
59
+ }
60
+
61
+ export async function downloadToFile(url, dest, { maxBytes, expectedOrigin, onProgress } = {}) {
62
+ const expected = new URL(url);
63
+ if (expectedOrigin && expected.origin !== expectedOrigin) {
64
+ throw new Error("Skin media download host mismatch.");
65
+ }
66
+ const response = await fetch(url, { redirect: "follow" });
67
+ if (!response.ok || !response.body) {
68
+ throw new Error("Skin media download failed.");
69
+ }
70
+ const finalUrl = new URL(response.url);
71
+ if (finalUrl.origin !== expected.origin) {
72
+ throw new Error("Skin media download host mismatch.");
73
+ }
74
+ const length = Number(response.headers.get("content-length"));
75
+ if (Number.isFinite(length) && length > maxBytes) {
76
+ throw new Error("Skin media download exceeded the size limit.");
77
+ }
78
+ await fsp.mkdir(path.dirname(dest), { recursive: true });
79
+ let size = 0;
80
+ const limiter = new Transform({
81
+ transform(chunk, _enc, callback) {
82
+ size += chunk.length;
83
+ if (size > maxBytes) {
84
+ callback(new Error("Skin media download exceeded the size limit."));
85
+ return;
86
+ }
87
+ onProgress?.(size, Number.isFinite(length) ? length : 0);
88
+ callback(null, chunk);
89
+ },
90
+ });
91
+ await pipeline(Readable.fromWeb(response.body), limiter, fs.createWriteStream(dest));
92
+ return { bytes: size };
93
+ }
94
+
95
+ function extensionOf(url, fallback) {
96
+ try {
97
+ const ext = path.extname(new URL(url).pathname).toLowerCase();
98
+ if (ext) return ext;
99
+ } catch {
100
+ /* use fallback */
101
+ }
102
+ return fallback;
103
+ }
104
+
105
+ export function createGalleryHandlers({ dataRoot, actions }) {
106
+ async function importTheme(input) {
107
+ if (typeof actions.importTheme === "function") {
108
+ return actions.importTheme(input);
109
+ }
110
+ throw new Error("当前引擎不支持导入皮肤。");
111
+ }
112
+
113
+ return {
114
+ async config(req, res, sendJson, isSameOrigin) {
115
+ if (req.method !== "GET") {
116
+ res.writeHead(405).end();
117
+ return;
118
+ }
119
+ if (!isSameOrigin(req)) {
120
+ res.writeHead(403).end();
121
+ return;
122
+ }
123
+ const url = await resolveConfiguredSkinCenterUrl();
124
+ sendJson(res, 200, { ok: true, url, enabled: Boolean(url) });
125
+ },
126
+
127
+ async catalog(req, res, sendJson, isSameOrigin) {
128
+ if (req.method !== "GET") {
129
+ res.writeHead(405).end();
130
+ return;
131
+ }
132
+ if (!isSameOrigin(req)) {
133
+ res.writeHead(403).end();
134
+ return;
135
+ }
136
+ const center = await resolveConfiguredSkinCenterUrl();
137
+ if (!center) {
138
+ sendJson(res, 200, {
139
+ ok: false,
140
+ error: "尚未配置皮肤中心地址。",
141
+ skins: [],
142
+ });
143
+ return;
144
+ }
145
+ const incoming = new URL(req.url || "/", "http://127.0.0.1");
146
+ const target = new URL("/api/catalog", `${center}/`);
147
+ target.search = incoming.search;
148
+ const response = await fetch(target, { headers: { accept: "application/json" } });
149
+ const body = await response.json().catch(() => null);
150
+ if (!response.ok || !body || body.ok === false) {
151
+ sendJson(res, 422, {
152
+ ok: false,
153
+ error: body?.error || "无法读取皮肤目录。",
154
+ skins: [],
155
+ });
156
+ return;
157
+ }
158
+ sendJson(res, 200, {
159
+ ok: true,
160
+ skins: Array.isArray(body.skins) ? body.skins : [],
161
+ nextCursor: body.nextCursor ?? null,
162
+ url: center,
163
+ });
164
+ },
165
+
166
+ async install(req, res, sendJson, isSameOrigin, readJson) {
167
+ if (req.method !== "POST") {
168
+ res.writeHead(405).end();
169
+ return;
170
+ }
171
+ if (!isSameOrigin(req)) {
172
+ res.writeHead(403).end();
173
+ return;
174
+ }
175
+ const body = await readJson(req);
176
+ const id = String(body.id ?? "").trim();
177
+ if (!isSafeSkinId(id)) {
178
+ sendJson(res, 400, { ok: false, error: "皮肤 ID 无效。" });
179
+ return;
180
+ }
181
+ const center = await resolveConfiguredSkinCenterUrl();
182
+ if (!center) {
183
+ sendJson(res, 422, { ok: false, error: "尚未配置皮肤中心地址。" });
184
+ return;
185
+ }
186
+ const origin = new URL(center).origin;
187
+ res.writeHead(200, {
188
+ "content-type": "application/x-ndjson; charset=utf-8",
189
+ "cache-control": "no-store",
190
+ });
191
+ const write = (payload) => {
192
+ if (!res.writableEnded) res.write(`${JSON.stringify(payload)}\n`);
193
+ };
194
+ const tmpDir = path.join(dataRoot, "tmp", "gallery", `${Date.now()}-${crypto.randomBytes(4).toString("hex")}`);
195
+ try {
196
+ write({ phase: "fetch" });
197
+ const metaRes = await fetch(skinUrl(center, id), { headers: { accept: "application/json" } });
198
+ const meta = await metaRes.json().catch(() => null);
199
+ const skin = meta?.skin;
200
+ if (!metaRes.ok || !skin || skin.status && skin.status !== "approved") {
201
+ throw new Error("Skin is not available for download.");
202
+ }
203
+ await fsp.mkdir(tmpDir, { recursive: true });
204
+ const imageUrl = skinUrl(center, id, "image");
205
+ const imagePath = path.join(tmpDir, `image${extensionOf(imageUrl, ".png")}`);
206
+ write({ phase: "download", part: "image" });
207
+ await downloadToFile(imageUrl, imagePath, {
208
+ maxBytes: MAX_IMAGE_BYTES,
209
+ expectedOrigin: origin,
210
+ onProgress: (done, total) => write({ phase: "download", part: "image", done, total }),
211
+ });
212
+ let videoPath;
213
+ if (skin.type === "video") {
214
+ const videoUrl = skinUrl(center, id, "video");
215
+ videoPath = path.join(tmpDir, "background.mp4");
216
+ write({ phase: "download", part: "video" });
217
+ await downloadToFile(videoUrl, videoPath, {
218
+ maxBytes: MAX_VIDEO_BYTES,
219
+ expectedOrigin: origin,
220
+ onProgress: (done, total) => write({ phase: "download", part: "video", done, total }),
221
+ });
222
+ }
223
+ write({ phase: "import" });
224
+ const imported = await importTheme({
225
+ name: String(skin.name || id).slice(0, 80),
226
+ imagePath,
227
+ ...(videoPath ? { videoPath } : {}),
228
+ ...(skin.effects ? { effects: skin.effects } : {}),
229
+ source: { kind: "skin-center", skinId: id, centerUrl: center },
230
+ });
231
+ write({ phase: "apply" });
232
+ const applied = await actions.useTheme(imported.theme?.id || imported.id, undefined);
233
+ fetch(skinUrl(center, id, "download"), { method: "POST" }).catch(() => {});
234
+ write({
235
+ ok: true,
236
+ phase: "done",
237
+ theme: imported.theme || imported,
238
+ message: applied.message || `已安装并应用「${skin.name}」。`,
239
+ });
240
+ } catch (error) {
241
+ write({
242
+ ok: false,
243
+ error: error instanceof Error ? error.message : String(error),
244
+ });
245
+ } finally {
246
+ await fsp.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
247
+ res.end();
248
+ }
249
+ },
250
+ };
251
+ }
252
+
253
+ export const GALLERY_INSTALL_TIMEOUT_MS = INSTALL_TIMEOUT_MS;
package/gallery.js ADDED
@@ -0,0 +1,178 @@
1
+ (() => {
2
+ "use strict";
3
+ if (window.__beauticodeGalleryLoaded) return;
4
+ window.__beauticodeGalleryLoaded = true;
5
+
6
+ const style = document.createElement("style");
7
+ style.textContent = `
8
+ #beauticode-gallery{position:fixed;inset:0;z-index:3000;display:flex;align-items:center;justify-content:center;background:rgba(11,13,18,.62)}
9
+ #beauticode-gallery[hidden]{display:none}
10
+ #beauticode-gallery .bcg-panel{width:min(880px,calc(100vw - 32px));height:min(640px,calc(100vh - 32px));display:flex;flex-direction:column;border:1px solid rgba(255,255,255,.08);border-radius:18px;background:#2c323c;color:#e8eaed;box-shadow:0 16px 48px rgba(0,0,0,.4);overflow:hidden}
11
+ body:not([data-ds-dark-theme]) #beauticode-gallery .bcg-panel{background:#fff;color:#1b1f24;border-color:rgba(0,0,0,.08)}
12
+ #beauticode-gallery .bcg-head{display:flex;align-items:center;gap:10px;padding:12px 14px;border-bottom:1px solid rgba(255,255,255,.08)}
13
+ #beauticode-gallery .bcg-head h2{margin:0;font-size:15px;font-weight:600}
14
+ #beauticode-gallery .bcg-head input,#beauticode-gallery .bcg-head select{height:32px;border:1px solid rgba(255,255,255,.1);border-radius:10px;background:rgba(255,255,255,.06);color:inherit;padding:0 8px}
15
+ #beauticode-gallery .bcg-grid{flex:1;overflow:auto;padding:12px;display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px;align-content:start}
16
+ #beauticode-gallery .bcg-card{display:block;border:none;padding:0;border-radius:12px;overflow:hidden;background:#232830;color:inherit;text-align:left;cursor:pointer}
17
+ #beauticode-gallery .bcg-card img{width:100%;aspect-ratio:16/10;object-fit:cover;display:block;background:#111}
18
+ #beauticode-gallery .bcg-card span{display:block;padding:8px 10px;font-size:13px}
19
+ #beauticode-gallery .bcg-msg,#beauticode-gallery .bcg-foot{padding:0 14px 12px;color:#9aa3ad;font-size:12px}
20
+ #beauticode-gallery .bcg-close{margin-left:auto}
21
+ #beauticode-gallery .bcg-btn{height:32px;padding:0 10px;border:1px solid rgba(255,255,255,.1);border-radius:10px;background:rgba(255,255,255,.06);color:inherit;cursor:pointer}
22
+ #beauticode-gallery .bcg-btn.primary{background:#4d6bfe;border-color:transparent}
23
+ `;
24
+ document.head.append(style);
25
+
26
+ const host = document.createElement("div");
27
+ host.id = "beauticode-gallery";
28
+ host.hidden = true;
29
+ host.innerHTML =
30
+ '<div class="bcg-panel" role="dialog" aria-modal="true" aria-labelledby="bcg-title">' +
31
+ '<div class="bcg-head">' +
32
+ '<h2 id="bcg-title">皮肤中心</h2>' +
33
+ '<input class="bcg-q" placeholder="搜索" />' +
34
+ '<select class="bcg-type"><option value="">全部</option><option value="image">图片</option><option value="video">视频</option></select>' +
35
+ '<button type="button" class="bcg-btn bcg-close">关闭</button>' +
36
+ "</div>" +
37
+ '<div class="bcg-grid"></div>' +
38
+ '<p class="bcg-msg"></p>' +
39
+ '<p class="bcg-foot"></p>' +
40
+ "</div>";
41
+ document.body.append(host);
42
+
43
+ const grid = host.querySelector(".bcg-grid");
44
+ const msg = host.querySelector(".bcg-msg");
45
+ const foot = host.querySelector(".bcg-foot");
46
+ const queryInput = host.querySelector(".bcg-q");
47
+ const typeSelect = host.querySelector(".bcg-type");
48
+ let centerUrl = "";
49
+ let busy = false;
50
+
51
+ function escapeText(value) {
52
+ return String(value ?? "")
53
+ .replaceAll("&", "&amp;")
54
+ .replaceAll("<", "&lt;")
55
+ .replaceAll(">", "&gt;");
56
+ }
57
+
58
+ async function request(path, init) {
59
+ const response = await fetch(path, init);
60
+ if ((response.headers.get("content-type") || "").includes("ndjson")) {
61
+ return readNdjson(response);
62
+ }
63
+ const body = await response.json().catch(() => null);
64
+ if (!response.ok || body?.ok === false) {
65
+ throw new Error(body?.error || `请求失败(${response.status})`);
66
+ }
67
+ return body;
68
+ }
69
+
70
+ async function readNdjson(response) {
71
+ const reader = response.body.getReader();
72
+ const decoder = new TextDecoder();
73
+ let buffer = "";
74
+ let last = null;
75
+ while (true) {
76
+ const { value, done } = await reader.read();
77
+ buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
78
+ const lines = buffer.split("\n");
79
+ buffer = lines.pop() || "";
80
+ for (const line of lines) {
81
+ if (!line.trim()) continue;
82
+ last = JSON.parse(line);
83
+ if (last.phase === "download" && last.total) {
84
+ const pct = Math.round((last.done / last.total) * 100);
85
+ msg.textContent = `正在下载${last.part === "video" ? "视频" : "图片"} ${pct}%`;
86
+ } else if (last.phase === "import") {
87
+ msg.textContent = "正在写入本机主题…";
88
+ } else if (last.phase === "apply") {
89
+ msg.textContent = "正在应用到当前窗口…";
90
+ }
91
+ if (last.ok === false) throw new Error(last.error || "安装失败。");
92
+ }
93
+ if (done) break;
94
+ }
95
+ if (last?.ok) return last;
96
+ throw new Error(last?.error || "安装失败。");
97
+ }
98
+
99
+ async function load() {
100
+ msg.textContent = "正在读取目录…";
101
+ const params = new URLSearchParams();
102
+ if (queryInput.value.trim()) params.set("q", queryInput.value.trim());
103
+ if (typeSelect.value) params.set("type", typeSelect.value);
104
+ const data = await request(`/__beauticode/ui/gallery/catalog?${params}`);
105
+ centerUrl = data.url || centerUrl;
106
+ grid.innerHTML = (data.skins || [])
107
+ .map(
108
+ (skin) =>
109
+ `<button type="button" class="bcg-card" data-id="${escapeText(skin.id)}">` +
110
+ `<img alt="" src="${escapeText(centerUrl)}/api/skins/${escapeText(skin.id)}/card">` +
111
+ `<span>${escapeText(skin.name)}${skin.type === "video" ? " · 视频" : ""}</span></button>`,
112
+ )
113
+ .join("");
114
+ msg.textContent = data.skins?.length ? "" : "目录是空的。";
115
+ foot.innerHTML = centerUrl
116
+ ? `上传与审核在 <a href="${escapeText(centerUrl)}" target="_blank" rel="noreferrer">皮肤中心网站</a>。安装会下载到本机后再应用。`
117
+ : "未配置皮肤中心地址。在插件的 skin-center.json 或环境变量 BEAUTICODE_SKIN_CENTER 里填入你的域名。";
118
+ }
119
+
120
+ async function open() {
121
+ host.hidden = false;
122
+ const config = await request("/__beauticode/ui/gallery/config");
123
+ centerUrl = config.url || "";
124
+ if (!config.enabled) {
125
+ grid.innerHTML = "";
126
+ msg.textContent = "尚未配置皮肤中心地址。";
127
+ foot.textContent = "设置 BEAUTICODE_SKIN_CENTER,或在 skin-center.json 填写站点 URL。";
128
+ return;
129
+ }
130
+ await load();
131
+ }
132
+
133
+ function close() {
134
+ host.hidden = true;
135
+ }
136
+
137
+ host.querySelector(".bcg-close").addEventListener("click", close);
138
+ host.addEventListener("click", (event) => {
139
+ if (event.target === host) close();
140
+ });
141
+ queryInput.addEventListener("keydown", (event) => {
142
+ if (event.key === "Enter") {
143
+ event.preventDefault();
144
+ void load();
145
+ }
146
+ });
147
+ typeSelect.addEventListener("change", () => void load());
148
+ grid.addEventListener("click", (event) => {
149
+ const card = event.target.closest("[data-id]");
150
+ if (!card || busy) return;
151
+ const id = card.getAttribute("data-id");
152
+ busy = true;
153
+ msg.textContent = "开始安装…";
154
+ request("/__beauticode/ui/gallery/install", {
155
+ method: "POST",
156
+ headers: { "content-type": "application/json" },
157
+ body: JSON.stringify({ id }),
158
+ })
159
+ .then((result) => {
160
+ msg.textContent = result.message || "已安装。";
161
+ document.dispatchEvent(new CustomEvent("beauticode-gallery-installed"));
162
+ })
163
+ .catch((error) => {
164
+ msg.textContent = error instanceof Error ? error.message : String(error);
165
+ })
166
+ .finally(() => {
167
+ busy = false;
168
+ });
169
+ });
170
+ document.addEventListener("keydown", (event) => {
171
+ if (event.key === "Escape" && !host.hidden) {
172
+ event.stopPropagation();
173
+ close();
174
+ }
175
+ });
176
+
177
+ window.BeauticodeGallery = { open, close };
178
+ })();
package/index.mjs CHANGED
@@ -215,7 +215,8 @@ export function apply(ctx, config = {}) {
215
215
  const script =
216
216
  '<script defer src="/__beauticode/atmosphere.js"></script>' +
217
217
  '<script defer src="/__beauticode/client.js" data-beauticode-bridge></script>' +
218
- '<script defer src="/__beauticode/console.js"></script>';
218
+ '<script defer src="/__beauticode/console.js"></script>' +
219
+ '<script defer src="/__beauticode/gallery.js"></script>';
219
220
  return html.includes("</body>")
220
221
  ? html.replace("</body>", `${script}</body>`)
221
222
  : `${html}${script}`;
@@ -316,6 +317,23 @@ export function apply(ctx, config = {}) {
316
317
  res.end(req.method === "HEAD" ? undefined : source);
317
318
  },
318
319
  }),
320
+ ctx.webServer.register({
321
+ kind: "exact",
322
+ path: "/__beauticode/gallery.js",
323
+ handler: async (req, res) => {
324
+ if (req.method !== "GET" && req.method !== "HEAD") {
325
+ res.writeHead(405).end();
326
+ return;
327
+ }
328
+ const source = await fs.readFile(path.join(here, "gallery.js"));
329
+ res.writeHead(200, {
330
+ "content-type": "text/javascript; charset=utf-8",
331
+ "cache-control": "no-store",
332
+ "content-length": source.length,
333
+ });
334
+ res.end(req.method === "HEAD" ? undefined : source);
335
+ },
336
+ }),
319
337
  ctx.webServer.register({
320
338
  kind: "exact",
321
339
  path: "/__beauticode/console.js",
@@ -363,6 +381,21 @@ export function apply(ctx, config = {}) {
363
381
  path: "/__beauticode/ui/preset",
364
382
  handler: (req, res) => ui.applyPreset(req, res),
365
383
  }),
384
+ ctx.webServer.register({
385
+ kind: "exact",
386
+ path: "/__beauticode/ui/gallery/config",
387
+ handler: (req, res) => ui.galleryConfig(req, res),
388
+ }),
389
+ ctx.webServer.register({
390
+ kind: "exact",
391
+ path: "/__beauticode/ui/gallery/catalog",
392
+ handler: (req, res) => ui.galleryCatalog(req, res),
393
+ }),
394
+ ctx.webServer.register({
395
+ kind: "exact",
396
+ path: "/__beauticode/ui/gallery/install",
397
+ handler: (req, res) => ui.galleryInstall(req, res),
398
+ }),
366
399
  ctx.webServer.register({
367
400
  kind: "exact",
368
401
  path: "/__beauticode/events",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beauticode-dsh",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Cordis plugin: image/video backgrounds for DeepSeek Harness web.",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
@@ -16,6 +16,9 @@
16
16
  "index.mjs",
17
17
  "client.js",
18
18
  "console.js",
19
+ "gallery.js",
20
+ "gallery-host.mjs",
21
+ "skin-center.json",
19
22
  "atmosphere.js",
20
23
  "presets.mjs",
21
24
  "agent.mjs",
@@ -0,0 +1,3 @@
1
+ {
2
+ "url": "https://hnnulwh.cn"
3
+ }