beauticode-dsh 1.0.2 → 1.0.3
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/README.zh-CN.md +1 -1
- package/agent.mjs +33 -0
- package/bin/beauticode-dsh +7 -252
- package/cli.js +37 -18
- package/console.js +15 -0
- package/control-client.mjs +75 -6
- package/cordis.patch.yml +1 -1
- package/gallery-host.mjs +253 -0
- package/gallery.js +178 -0
- package/index.mjs +34 -1
- package/package.json +4 -1
- package/skin-center.json +3 -0
- package/ui-host.mjs +17 -0
- package/vendor/adapter-dsh/session.js +3 -0
- package/vendor/adapter-dsh/tray-handoff.js +11 -13
- package/vendor/core/background-store.js +104 -1
- package/vendor/core/error-message.js +6 -0
- package/vendor/core/file-lock.js +34 -13
- package/vendor/core/index.js +2 -0
- package/vendor/core/process-liveness.js +205 -0
- package/vendor/core/skin-pack.js +64 -0
package/gallery-host.mjs
ADDED
|
@@ -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("&", "&")
|
|
54
|
+
.replaceAll("<", "<")
|
|
55
|
+
.replaceAll(">", ">");
|
|
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.
|
|
3
|
+
"version": "1.0.3",
|
|
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",
|
package/skin-center.json
ADDED
package/ui-host.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { pipeline } from "node:stream/promises";
|
|
|
6
6
|
import { Transform } from "node:stream";
|
|
7
7
|
import { createBeauticodeActions } from "./agent.mjs";
|
|
8
8
|
import { callDshControl } from "./control-client.mjs";
|
|
9
|
+
import { createGalleryHandlers, resolveConfiguredSkinCenterUrl } from "./gallery-host.mjs";
|
|
9
10
|
import { hasLiveTray, resolveApplyBackend, stopInProcessSession } from "./host-apply.mjs";
|
|
10
11
|
|
|
11
12
|
const IMAGE_EXTENSIONS = new Set([".jpg", ".jpeg", ".png", ".webp", ".avif"]);
|
|
@@ -58,6 +59,7 @@ function publicThemes(list) {
|
|
|
58
59
|
name: theme.name,
|
|
59
60
|
type: theme.type ?? null,
|
|
60
61
|
...(theme.bundled ? { bundled: true } : {}),
|
|
62
|
+
...(theme.source ? { source: theme.source } : {}),
|
|
61
63
|
}));
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -86,6 +88,7 @@ export function createBeauticodeUi({ dataRoot, baseUrl, getBaseUrl, sendJson, is
|
|
|
86
88
|
},
|
|
87
89
|
};
|
|
88
90
|
const actions = createBeauticodeActions(options);
|
|
91
|
+
const gallery = createGalleryHandlers({ dataRoot, actions });
|
|
89
92
|
let restoreStarted = false;
|
|
90
93
|
|
|
91
94
|
async function restoreOnce() {
|
|
@@ -137,6 +140,7 @@ export function createBeauticodeUi({ dataRoot, baseUrl, getBaseUrl, sendJson, is
|
|
|
137
140
|
const status = await actions.status();
|
|
138
141
|
const listed = await actions.listThemes();
|
|
139
142
|
const background = status.background ?? null;
|
|
143
|
+
const center = await resolveConfiguredSkinCenterUrl();
|
|
140
144
|
sendJson(res, 200, {
|
|
141
145
|
ok: true,
|
|
142
146
|
media: background?.type ?? null,
|
|
@@ -144,6 +148,7 @@ export function createBeauticodeUi({ dataRoot, baseUrl, getBaseUrl, sendJson, is
|
|
|
144
148
|
atmosphere: background?.effects?.preset ?? null,
|
|
145
149
|
themes: publicThemes(listed.themes),
|
|
146
150
|
message: status.message,
|
|
151
|
+
skinCenter: { url: center, enabled: Boolean(center) },
|
|
147
152
|
});
|
|
148
153
|
} catch (error) {
|
|
149
154
|
sendJson(res, 200, {
|
|
@@ -265,6 +270,18 @@ export function createBeauticodeUi({ dataRoot, baseUrl, getBaseUrl, sendJson, is
|
|
|
265
270
|
}
|
|
266
271
|
},
|
|
267
272
|
|
|
273
|
+
async galleryConfig(req, res) {
|
|
274
|
+
return gallery.config(req, res, sendJson, isSameOrigin);
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
async galleryCatalog(req, res) {
|
|
278
|
+
return gallery.catalog(req, res, sendJson, isSameOrigin);
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
async galleryInstall(req, res) {
|
|
282
|
+
return gallery.install(req, res, sendJson, isSameOrigin, readJson);
|
|
283
|
+
},
|
|
284
|
+
|
|
268
285
|
async applyPreset(req, res) {
|
|
269
286
|
if (req.method !== "POST") {
|
|
270
287
|
res.writeHead(405).end();
|
|
@@ -1,26 +1,19 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { isRecordedPidLive } from "../core/index.js";
|
|
3
4
|
export const TRAY_CLAIM_FILE = "tray-claim.json";
|
|
4
5
|
export const TRAY_CLAIM_SCHEMA = "beauticode.tray-claim/v1";
|
|
5
6
|
export const SESSION_HOST_FILE = "session-host.json";
|
|
6
7
|
export const SESSION_HOST_SCHEMA = "beauticode.session-host/v1";
|
|
7
8
|
export const DSH_CONTROL_FILE = "dsh-control.json";
|
|
8
9
|
export const DSH_CONTROL_SCHEMA = "beauticode.dsh-control/v1";
|
|
9
|
-
function isPidAlive(pid) {
|
|
10
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
11
|
-
return false;
|
|
12
|
-
try {
|
|
13
|
-
process.kill(pid, 0);
|
|
14
|
-
return true;
|
|
15
|
-
}
|
|
16
|
-
catch (error) {
|
|
17
|
-
return Boolean(error && typeof error === "object" && "code" in error && error.code === "EPERM");
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
10
|
async function readLivePid(filePath, schema) {
|
|
21
11
|
let raw;
|
|
12
|
+
let mtimeMs = 0;
|
|
22
13
|
try {
|
|
23
|
-
|
|
14
|
+
const [text, stat] = await Promise.all([fs.readFile(filePath, "utf8"), fs.stat(filePath)]);
|
|
15
|
+
raw = text;
|
|
16
|
+
mtimeMs = stat.mtimeMs;
|
|
24
17
|
}
|
|
25
18
|
catch {
|
|
26
19
|
return null;
|
|
@@ -30,7 +23,12 @@ async function readLivePid(filePath, schema) {
|
|
|
30
23
|
if (schema && parsed.schema !== schema)
|
|
31
24
|
return null;
|
|
32
25
|
const pid = Number(parsed.pid);
|
|
33
|
-
if (!Number.isInteger(pid) || pid <= 0
|
|
26
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
27
|
+
return null;
|
|
28
|
+
const startedAt = typeof parsed.startedAt === "string" && parsed.startedAt
|
|
29
|
+
? parsed.startedAt
|
|
30
|
+
: new Date(mtimeMs).toISOString();
|
|
31
|
+
if (!(await isRecordedPidLive(pid, startedAt)))
|
|
34
32
|
return null;
|
|
35
33
|
return pid;
|
|
36
34
|
}
|