beauticode-dsh 1.0.0
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 +48 -0
- package/agent.mjs +595 -0
- package/atmosphere.js +306 -0
- package/bin/beauticode-dsh +219 -0
- package/cli.js +219 -0
- package/client.js +370 -0
- package/console.js +303 -0
- package/control-client.mjs +445 -0
- package/cordis.patch.yml +4 -0
- package/host-apply.mjs +163 -0
- package/index.mjs +570 -0
- package/package.json +56 -0
- package/presets.mjs +73 -0
- package/themes/internal-beyond/NOTICE.md +9 -0
- package/themes/internal-beyond/bg-canvas-4k.png +0 -0
- package/themes/internal-beyond/bg-canvas.png +0 -0
- package/themes/internal-beyond/bg-infernal.jpg +0 -0
- package/themes/internal-beyond/bg-internal.jpg +0 -0
- package/ui-host.mjs +292 -0
- package/vendor/adapter-dsh/bridge.js +215 -0
- package/vendor/adapter-dsh/host-descriptor.js +15 -0
- package/vendor/adapter-dsh/index.js +6 -0
- package/vendor/adapter-dsh/injector-lock.js +27 -0
- package/vendor/adapter-dsh/package.json +5 -0
- package/vendor/adapter-dsh/session.js +527 -0
- package/vendor/adapter-dsh/token.js +46 -0
- package/vendor/adapter-dsh/tray-handoff.js +54 -0
- package/vendor/core/apply-transaction.js +287 -0
- package/vendor/core/background-store.js +1116 -0
- package/vendor/core/bundled-gallery.js +53 -0
- package/vendor/core/constants.js +47 -0
- package/vendor/core/error-message.js +208 -0
- package/vendor/core/file-lock.js +151 -0
- package/vendor/core/host-session.js +2 -0
- package/vendor/core/index.js +12 -0
- package/vendor/core/media-server.js +544 -0
- package/vendor/core/media-validation.js +244 -0
- package/vendor/core/package.json +5 -0
- package/vendor/core/paths.js +168 -0
- package/vendor/core/types.js +15 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { effectsForPreset } from "./types.js";
|
|
4
|
+
export const BUNDLED_GALLERY_THEME_ID = "builtin-gallery";
|
|
5
|
+
export const BUNDLED_GALLERY_THEME_NAME = "画窗";
|
|
6
|
+
const GALLERY_FILES = ["bg-canvas-4k.png", "bg-canvas.png"];
|
|
7
|
+
export function isBundledThemeId(id) {
|
|
8
|
+
return id === BUNDLED_GALLERY_THEME_ID;
|
|
9
|
+
}
|
|
10
|
+
export function bundledGalleryThemeSpec(imagePath) {
|
|
11
|
+
return {
|
|
12
|
+
id: BUNDLED_GALLERY_THEME_ID,
|
|
13
|
+
name: BUNDLED_GALLERY_THEME_NAME,
|
|
14
|
+
imagePath,
|
|
15
|
+
effects: effectsForPreset("gallery"),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Locate the shipped 画窗 still. Prefer an explicit 4K file when present. */
|
|
19
|
+
export function resolveBundledGalleryImagePath(searchRoots = []) {
|
|
20
|
+
const env = process.env.BEAUTICODE_GALLERY_IMAGE?.trim();
|
|
21
|
+
const candidates = [];
|
|
22
|
+
if (env)
|
|
23
|
+
candidates.push(env);
|
|
24
|
+
for (const root of searchRoots) {
|
|
25
|
+
if (!root)
|
|
26
|
+
continue;
|
|
27
|
+
const resolved = path.resolve(root);
|
|
28
|
+
for (const file of GALLERY_FILES) {
|
|
29
|
+
candidates.push(path.join(resolved, file));
|
|
30
|
+
candidates.push(path.join(resolved, "themes", "internal-beyond", file));
|
|
31
|
+
candidates.push(path.join(resolved, "assets", "themes", "internal-beyond", file));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
for (const filePath of candidates) {
|
|
35
|
+
try {
|
|
36
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
37
|
+
return filePath;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
/* skip unreadable candidates */
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
export function resolveSessionBundledThemes(opts = {}) {
|
|
47
|
+
if (opts.enabled === false)
|
|
48
|
+
return [];
|
|
49
|
+
const explicit = opts.imagePath?.trim();
|
|
50
|
+
const imagePath = explicit || resolveBundledGalleryImagePath(opts.searchRoots ?? []);
|
|
51
|
+
return imagePath ? [bundledGalleryThemeSpec(imagePath)] : [];
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=bundled-gallery.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export const SCHEMA_ID = "beauticode.background/v1";
|
|
2
|
+
// Images are embedded into one CDP Runtime.evaluate payload. Keep validation
|
|
3
|
+
// and injection limits identical so an accepted import is always publishable.
|
|
4
|
+
export const MAX_IMAGE_BYTES = 18 * 1024 * 1024;
|
|
5
|
+
export const MAX_VIDEO_BYTES = 800 * 1024 * 1024;
|
|
6
|
+
/**
|
|
7
|
+
* Max raw media bytes embedded as a data: URL inside one CDP evaluate.
|
|
8
|
+
* Videos use CDP file-input/blob attachment instead of this limit.
|
|
9
|
+
* Keep this equal to MAX_IMAGE_BYTES so every accepted image can publish.
|
|
10
|
+
*/
|
|
11
|
+
export const MAX_INLINE_DATA_URL_BYTES = MAX_IMAGE_BYTES;
|
|
12
|
+
export const COMMIT_MARKER_NAME = ".beauticode-commit-in-progress";
|
|
13
|
+
export const COMMIT_MARKER_STALE_MS = 120_000;
|
|
14
|
+
export const MEDIA_TOKEN_HEADER = "x-beauticode-media-token";
|
|
15
|
+
export const MEDIA_TOKEN_HEADER_CANON = "X-BeautiCode-Media-Token";
|
|
16
|
+
/** Default origins accepted by the loopback media server (Electron-style). */
|
|
17
|
+
export const DEFAULT_TRUSTED_ORIGINS = Object.freeze([
|
|
18
|
+
"app://-",
|
|
19
|
+
"app://",
|
|
20
|
+
"null",
|
|
21
|
+
// Some Electron shells report a trailing slash or empty host variants.
|
|
22
|
+
"app://./",
|
|
23
|
+
"file://",
|
|
24
|
+
]);
|
|
25
|
+
/**
|
|
26
|
+
* Origins matching this pattern are also trusted for loopback media.
|
|
27
|
+
* Only safe because the media hub binds 127.0.0.1 exclusively.
|
|
28
|
+
*/
|
|
29
|
+
export const TRUSTED_ORIGIN_PREFIXES = Object.freeze(["app://"]);
|
|
30
|
+
export const IMAGE_EXTENSIONS = Object.freeze([
|
|
31
|
+
".jpg",
|
|
32
|
+
".jpeg",
|
|
33
|
+
".png",
|
|
34
|
+
".webp",
|
|
35
|
+
".avif",
|
|
36
|
+
]);
|
|
37
|
+
export const VIDEO_EXTENSION = ".mp4";
|
|
38
|
+
export const VIDEO_MIME = "video/mp4";
|
|
39
|
+
export const ACTIVE_DIR_NAME = "active";
|
|
40
|
+
export const STAGING_DIR_NAME = "staging";
|
|
41
|
+
export const SNAPSHOTS_DIR_NAME = "snapshots";
|
|
42
|
+
export const SAVED_DIR_NAME = "saved";
|
|
43
|
+
export const RUNTIME_MEDIA_DIR_NAME = "runtime-media";
|
|
44
|
+
export const MANIFEST_NAME = "background.json";
|
|
45
|
+
export const SAVED_META_NAME = "theme.json";
|
|
46
|
+
export const DEFAULT_VIDEO_BASENAME = "background.mp4";
|
|
47
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert application errors to concise Chinese text at user-facing
|
|
3
|
+
* boundaries. Internal error strings remain English so protocol matching and
|
|
4
|
+
* recovery logic keep their existing semantics.
|
|
5
|
+
*/
|
|
6
|
+
export function toChineseErrorMessage(value) {
|
|
7
|
+
const raw = value instanceof Error ? value.message : String(value ?? "");
|
|
8
|
+
const message = raw.trim();
|
|
9
|
+
if (!message)
|
|
10
|
+
return "未知错误。";
|
|
11
|
+
// Chromium/Node use both spellings for the same missing-CDP symptom.
|
|
12
|
+
if (/^(?:error:\s*)?(?:failed to fetch|fetch failed|fail fetch)$/i.test(message)) {
|
|
13
|
+
return "未发现注入CDP的Codex进程";
|
|
14
|
+
}
|
|
15
|
+
const rules = [
|
|
16
|
+
[
|
|
17
|
+
/DeepSeek Harness URL must be loopback HTTP\.?/i,
|
|
18
|
+
"DeepSeek Harness 地址必须使用本机回环 HTTP(127.0.0.1、localhost 或 ::1)。",
|
|
19
|
+
],
|
|
20
|
+
[
|
|
21
|
+
/DeepSeek Harness phase one supports image backgrounds only\.?/i,
|
|
22
|
+
"DeepSeek Harness 第一阶段仅支持图片背景、清除和重新应用;视频、摸鱼模式、声音与色调暂不支持。",
|
|
23
|
+
],
|
|
24
|
+
[
|
|
25
|
+
/DeepSeek Harness bridge token file is invalid\.?/i,
|
|
26
|
+
"DeepSeek Harness 桥接令牌文件无效,请删除该令牌文件后重新启动 beautiCode。",
|
|
27
|
+
],
|
|
28
|
+
[
|
|
29
|
+
/DeepSeek Harness bridge (?:is unavailable|request failed.*|request timed out)\.?/i,
|
|
30
|
+
"未连接到 DeepSeek Harness。请先用 beautiCode 补丁启动 dsh web,并打开浏览器页面。",
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
/No DeepSeek Harness browser client is connected\.?/i,
|
|
34
|
+
"DeepSeek Harness 页面尚未连接,请先在浏览器中打开 DSH Web。",
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
/DeepSeek Harness client (?:has not acknowledged this generation|failed to render the background)\.?/i,
|
|
38
|
+
"DeepSeek Harness 页面未确认背景已成功显示。",
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
/DeepSeek Harness image apply requires a loopback media URL\.?/i,
|
|
42
|
+
"无法为 DeepSeek Harness 创建安全的本机图片地址。",
|
|
43
|
+
],
|
|
44
|
+
[
|
|
45
|
+
/No healthy loopback Codex CDP endpoint found.*$/i,
|
|
46
|
+
"未发现健康的本机 Codex CDP 端点,请先打开 Codex Desktop。",
|
|
47
|
+
],
|
|
48
|
+
[/No CDP port configured\. Pass --port or enable auto-discover\.?/i, "未配置 CDP 端口,请传入 --port 或启用自动发现。"],
|
|
49
|
+
[/CDP is missing or unreachable \(fail closed\)\.?/i, "未发现可用的 CDP 连接。"],
|
|
50
|
+
[/ECONNREFUSED/i, "无法连接到目标服务(连接被拒绝)。"],
|
|
51
|
+
[/ECONNRESET/i, "与目标服务的连接已重置。"],
|
|
52
|
+
[/ETIMEDOUT|connection timed out/i, "操作超时。"],
|
|
53
|
+
[/aborted/i, "操作已中止。"],
|
|
54
|
+
[/No candidate page targets on CDP port/i, "CDP 端口上未发现可注入的页面。"],
|
|
55
|
+
[/No live CDP sessions to apply background/i, "没有可应用背景的活动 CDP 会话。"],
|
|
56
|
+
[/No live CDP sessions/i, "未发现活动的 CDP 会话。"],
|
|
57
|
+
[/Timed out waiting for a CDP page target/i, "等待 CDP 页面超时。"],
|
|
58
|
+
[/CDP target is missing webSocketDebuggerUrl/i, "CDP 目标缺少 WebSocket 调试地址。"],
|
|
59
|
+
[/CDP webSocketDebuggerUrl is not a valid URL/i, "CDP WebSocket 调试地址无效。"],
|
|
60
|
+
[/Rejected a CDP WebSocket URL outside the allowed loopback endpoint shape/i, "已拒绝非本机回环地址的 CDP WebSocket。"],
|
|
61
|
+
[/Rejected an invalid CDP browser identity URL/i, "CDP 浏览器身份地址无效。"],
|
|
62
|
+
[
|
|
63
|
+
/CDP HTTP (\d+) for (.+)/i,
|
|
64
|
+
(match) => `CDP 请求失败(HTTP ${match[1]}):${match[2]}`,
|
|
65
|
+
],
|
|
66
|
+
[/CDP response exceeded (.+) bytes(?: \(declared (.+)\))?\.?/i, (match) => `CDP 响应超过安全大小限制(${match[1]} 字节)。`],
|
|
67
|
+
[/CDP response had no readable body for (.+)/i, (match) => `CDP 响应没有可读取的内容:${match[1]}`],
|
|
68
|
+
[/CDP \/json\/version returned an unexpected shape\.?/i, "CDP 版本信息格式异常。"],
|
|
69
|
+
[/CDP \/json\/list is not an array/i, "CDP 页面列表格式异常。"],
|
|
70
|
+
[/CDP \/json\/list exceeded target count safety cap/i, "CDP 页面数量超过安全上限。"],
|
|
71
|
+
[/CDP browser identity changed from (.+) to (.+)/i, (match) => `CDP 浏览器身份已变化(${match[1] ?? "旧"} → ${match[2] ?? "新"})。`],
|
|
72
|
+
[/CDP WebSocket open timed out/i, "CDP WebSocket 连接超时。"],
|
|
73
|
+
[/CDP WebSocket open failed/i, "CDP WebSocket 连接失败。"],
|
|
74
|
+
[/CDP socket closed/i, "CDP 套接字已关闭。"],
|
|
75
|
+
[/CDP session is closed/i, "CDP 会话已关闭。"],
|
|
76
|
+
[/CDP command timed out:\s*(.*)/i, (match) => `CDP 命令执行超时:${match[1] ?? "未知命令"}`],
|
|
77
|
+
[/Renderer evaluation failed:\s*(.*)/i, (match) => `渲染器执行失败:${match[1] ?? "未知错误"}`],
|
|
78
|
+
[/CDP probe only allows loopback hosts\.?/i, "CDP 探测只允许本机回环地址。"],
|
|
79
|
+
[/CDP port must be an integer 1[–-]65535\.?/i, "CDP 端口必须是 1–65535 之间的整数。"],
|
|
80
|
+
[/Session already stopped/i, "会话已经停止。"],
|
|
81
|
+
[/Session is not started/i, "会话尚未启动。"],
|
|
82
|
+
[/Session already started/i, "会话已经启动。"],
|
|
83
|
+
[/Host is not connected/i, "尚未连接到 Codex 主机。"],
|
|
84
|
+
[/CodexHostApplier is closed/i, "Codex 注入器已经关闭。"],
|
|
85
|
+
[/Another beautiCode injector is running \(pid (\d+)\)\.?/i, (match) => `另一个 beautiCode 注入器正在运行(进程 ${match[1] ?? "未知"})。`],
|
|
86
|
+
[/CodexHostApplier port must be 1[–-]65535/i, "Codex 注入器端口必须是 1–65535 之间的整数。"],
|
|
87
|
+
[/Another (.+) is running \(pid (\d+)\)\.?/i, (match) => `另一个${match[1] ?? "操作"}正在运行(进程 ${match[2] ?? "未知"})。`],
|
|
88
|
+
[/Another (.+) may be starting; lock owner is not readable yet\.?/i, (match) => `另一个${match[1] ?? "操作"}可能正在启动,暂时无法读取锁的所有者。`],
|
|
89
|
+
[/Another background apply is already in progress\.?/i, "已有背景应用正在进行中,请等待当前操作完成。"],
|
|
90
|
+
[/No active background\. Apply an image or video first\.?/i, "当前没有背景,请先应用图片或视频。"],
|
|
91
|
+
[/Live verify did not pass \(([^)]+)\):\s*(.*)/i, (match) => `实时校验未通过(${translateVerifyStatus(match[1] ?? "")}):${translateReadinessReason(match[2] ?? "")}`],
|
|
92
|
+
[/Failed to inject background into any session:\s*(.*)/i, (match) => `未能向任何会话注入背景:${match[1]}`],
|
|
93
|
+
[/Could not attach the local MP4 through CDP:\s*(.*)/i, (match) => `无法通过 CDP 附加本地 MP4:${translateMediaDetail(match[1] ?? "")}`],
|
|
94
|
+
[/Could not toggle fish mode on any session/i, "无法在任何会话中切换摸鱼模式。"],
|
|
95
|
+
[/Could not set background tone on any session/i, "无法在任何会话中设置背景色调。"],
|
|
96
|
+
[/Could not toggle mute on any session/i, "无法在任何会话中切换视频声音。"],
|
|
97
|
+
[/runtime rejected fish mode \(no background\?\)/i, "运行时拒绝摸鱼模式:当前没有背景。"],
|
|
98
|
+
[/runtime rejected mute toggle/i, "运行时拒绝切换视频声音。"],
|
|
99
|
+
[/renderer does not support background tone/i, "当前渲染器不支持背景色调。"],
|
|
100
|
+
[/video decode\/playback failed/i, "视频解码或播放失败。"],
|
|
101
|
+
[/video node missing/i, "未找到视频节点。"],
|
|
102
|
+
[/video has no playable local source/i, "视频没有可播放的本地来源。"],
|
|
103
|
+
[/poster\/image decode failed/i, "海报图片解码失败。"],
|
|
104
|
+
[/poster\/image missing/i, "未找到海报图片。"],
|
|
105
|
+
[/background stage missing/i, "未找到背景渲染层。"],
|
|
106
|
+
[/clear expected but background still active/i, "预期已清除背景,但背景仍处于活动状态。"],
|
|
107
|
+
[/generation mismatch \(page=(.+), expected=(.+)\)/i, (match) => `背景版本不匹配(页面=${match[1]},预期=${match[2]})。`],
|
|
108
|
+
[/horizontal document overflow detected/i, "检测到页面出现横向溢出。"],
|
|
109
|
+
[/empty readiness snapshot/i, "未读取到渲染就绪状态。"],
|
|
110
|
+
[/snapshot missing generation/i, "渲染状态缺少背景版本号。"],
|
|
111
|
+
[/video expected but renderer reports (.+)/i, (match) => `预期显示视频,但渲染器报告:${match[1] ?? "未知状态"}。`],
|
|
112
|
+
[/stage pointer-events must be none \(got (.+)\)/i, (match) => `背景层 pointer-events 必须为 none(当前为 ${match[1]})。`],
|
|
113
|
+
[/Video background is missing a video basename\.?/i, "视频背景缺少视频文件名。"],
|
|
114
|
+
[/Video payload requires a detached runtime copy\.?/i, "视频载荷需要独立的运行时副本。"],
|
|
115
|
+
[/Unsupported media extension(?: for data URL)?:\s*(.*)/i, (match) => `不支持的媒体扩展名:${match[1]}`],
|
|
116
|
+
[/Could not acquire (.+) lock\.?/i, (match) => `无法获取${match[1]}锁。`],
|
|
117
|
+
[/Interrupted commit could not recover either active generation\.?/i, "中断的提交无法恢复任何一个活动版本。"],
|
|
118
|
+
[/Runtime media root must be a real directory\.?/i, "运行时媒体根目录必须是真实目录。"],
|
|
119
|
+
[/Runtime media session must be a real directory\.?/i, "运行时媒体会话目录必须是真实目录。"],
|
|
120
|
+
[/Snapshot must be a generated child of the snapshots directory\.?/i, "快照必须位于快照目录生成的子目录中。"],
|
|
121
|
+
[/Snapshot directory cannot be a link\.?/i, "快照目录不能是链接。"],
|
|
122
|
+
[/Snapshot manifest is invalid\.?/i, "快照清单无效。"],
|
|
123
|
+
[/Promoted active manifest is invalid\.?/i, "提升后的活动清单无效。"],
|
|
124
|
+
[/Theme name must be 1[–-]80 characters\.?/i, "主题名称长度必须为 1–80 个字符。"],
|
|
125
|
+
[/Theme name contains illegal characters\.?/i, "主题名称包含非法字符。"],
|
|
126
|
+
[/No active background to save\.?/i, "当前没有可保存的背景。"],
|
|
127
|
+
[/Saved theme limit reached \((\d+)\)\. Delete one before saving\.?/i, (match) => `已达到保存主题数量上限(${match[1]}),请先删除一个主题。`],
|
|
128
|
+
[/Saved theme storage limit exceeded \((\d+) bytes\)\. Delete a theme before saving\.?/i, (match) => `已超过保存主题的存储上限(${match[1]} 字节),请先删除一个主题。`],
|
|
129
|
+
[/Built-in theme cannot be deleted\.?/i, "内置主题不能删除。"],
|
|
130
|
+
[/Invalid saved theme id\.?/i, "已保存主题 ID 无效。"],
|
|
131
|
+
[/Saved theme not found\.?/i, "未找到已保存的主题。"],
|
|
132
|
+
[/Invalid video position\.?/i, "视频位置无效。"],
|
|
133
|
+
[/Theme meta unreadable\.?/i, "主题元数据无法读取。"],
|
|
134
|
+
[/Theme meta invalid\.?/i, "主题元数据无效。"],
|
|
135
|
+
[/Not a video theme\.?/i, "该主题不是视频主题。"],
|
|
136
|
+
[/Saved theme manifest is invalid\.?/i, "已保存主题清单无效。"],
|
|
137
|
+
[/Saved video theme has no video file\.?/i, "已保存的视频主题缺少视频文件。"],
|
|
138
|
+
[/Saved theme path escaped saved root\.?/i, "已保存主题路径越过了保存目录。"],
|
|
139
|
+
[/Saved theme directory cannot be a link\.?/i, "已保存主题目录不能是链接。"],
|
|
140
|
+
[/Saved theme real path escaped saved root\.?/i, "已保存主题真实路径越过了保存目录。"],
|
|
141
|
+
[/Staging transaction escaped staging root\.?/i, "暂存事务越过了暂存目录。"],
|
|
142
|
+
[/Invalid media commit payload\.?/i, "媒体提交载荷无效。"],
|
|
143
|
+
[/Media file ended while revalidating/i, "媒体文件在重新校验时提前结束。"],
|
|
144
|
+
[/Media hub is closed/i, "媒体服务已关闭。"],
|
|
145
|
+
[/Local media server did not expose a TCP port\.?/i, "本地媒体服务未提供 TCP 端口。"],
|
|
146
|
+
[/Media hub closed while listener was starting\.?/i, "媒体服务在监听器启动时已关闭。"],
|
|
147
|
+
[/Media path became a symbolic link/i, "媒体路径变成了符号链接。"],
|
|
148
|
+
[/Media file changed or exceeded the safety limit/i, "媒体文件已变化或超过安全限制。"],
|
|
149
|
+
[/Media file content changed after staging/i, "媒体文件在暂存后发生了内容变化。"],
|
|
150
|
+
[/Media file too large to embed as data URL \((\d+) > (\d+)\)\.?/i, (match) => `媒体文件过大,无法嵌入数据地址(${match[1]}/${match[2]} 字节)。`],
|
|
151
|
+
[/beautiCode data root must be a real directory\.?/i, "beautiCode 数据根目录必须是真实目录。"],
|
|
152
|
+
[/beautiCode data-root ownership marker is invalid\.?/i, "beautiCode 数据目录所有权标记无效。"],
|
|
153
|
+
[/Media file not found:\s*(.*)/i, (match) => `未找到媒体文件:${match[1]}`],
|
|
154
|
+
[/Media path must be a regular file\.?/i, "媒体路径必须指向普通文件。"],
|
|
155
|
+
[/Image must use one of:\s*(.*)/i, (match) => `图片必须使用以下扩展名之一:${match[1]}`],
|
|
156
|
+
[/Image must be a non-empty file no larger than (\d+) bytes\.?/i, (match) => `图片必须是非空文件,且不超过 ${match[1]} 字节。`],
|
|
157
|
+
[/Image content does not match a supported JPEG\/PNG\/WEBP\/AVIF signature\.?/i, "图片内容与支持的 JPEG/PNG/WEBP/AVIF 格式不匹配。"],
|
|
158
|
+
[/Video backgrounds must use an MP4 file\.?/i, "视频背景必须使用 MP4 文件。"],
|
|
159
|
+
[/Video background must be a non-empty MP4 no larger than (\d+) bytes\.?/i, (match) => `视频背景必须是非空 MP4 文件,且不超过 ${match[1]} 字节。`],
|
|
160
|
+
[/Video background is not a valid MP4 container\.?/i, "视频背景不是有效的 MP4 容器。"],
|
|
161
|
+
[/must be a basename string\.?/i, "必须是文件名字符串。"],
|
|
162
|
+
[/must be a basename only\.?/i, "只能是文件名,不能包含路径。"],
|
|
163
|
+
[/contains illegal path characters\.?/i, "包含非法路径字符。"],
|
|
164
|
+
[/contains characters invalid in Windows basenames\.?/i, "包含 Windows 文件名不允许的字符。"],
|
|
165
|
+
[/uses a reserved device name\.?/i, "使用了 Windows 保留设备名。"],
|
|
166
|
+
[/contains control characters\.?/i, "包含控制字符。"],
|
|
167
|
+
[/missing data url/i, "缺少数据地址。"],
|
|
168
|
+
[/data url fetch failed:\s*(\d+)/i, (match) => `数据地址加载失败(HTTP ${match[1]})。`],
|
|
169
|
+
[/malformed data url/i, "数据地址格式错误。"],
|
|
170
|
+
[/missing media url/i, "缺少媒体地址。"],
|
|
171
|
+
[/media fetch failed:\s*(\d+)/i, (match) => `媒体加载失败(HTTP ${match[1]})。`],
|
|
172
|
+
[/no video source/i, "没有视频来源。"],
|
|
173
|
+
[/Renderer did not create the video file input/i, "渲染器未创建视频文件输入框。"],
|
|
174
|
+
[/DOM\.getDocument returned no root nodeId/i, "DOM 文档没有返回根节点 ID。"],
|
|
175
|
+
[/Video file input is not attached to the renderer DOM/i, "视频文件输入框未附加到渲染器 DOM。"],
|
|
176
|
+
[/stale generation/i, "背景版本已过期,正在重新加载。"],
|
|
177
|
+
[/request body too large/i, "请求内容过大。"],
|
|
178
|
+
[/request body must be a JSON object/i, "请求内容必须是 JSON 对象。"],
|
|
179
|
+
[/unauthorized/i, "未授权的请求。"],
|
|
180
|
+
[/shutting down/i, "服务正在关闭。"],
|
|
181
|
+
[/not found/i, "未找到请求的资源。"],
|
|
182
|
+
[/Unknown flag:\s*(.*)/i, (match) => `未知参数:${match[1] ?? ""}`],
|
|
183
|
+
[/Refusing to adopt a non-empty directory without beautiCode data\. Choose an empty --data-root\.?/i, "拒绝接管不含 beautiCode 数据的非空目录,请选择空的 --data-root。"],
|
|
184
|
+
];
|
|
185
|
+
for (const [pattern, replacement] of rules) {
|
|
186
|
+
const match = pattern.exec(message);
|
|
187
|
+
if (!match)
|
|
188
|
+
continue;
|
|
189
|
+
return typeof replacement === "function"
|
|
190
|
+
? replacement(match)
|
|
191
|
+
: message.replace(pattern, replacement);
|
|
192
|
+
}
|
|
193
|
+
return message;
|
|
194
|
+
}
|
|
195
|
+
function translateReadinessReason(reason) {
|
|
196
|
+
return toChineseErrorMessage(reason);
|
|
197
|
+
}
|
|
198
|
+
function translateVerifyStatus(status) {
|
|
199
|
+
if (/^fail$/i.test(status))
|
|
200
|
+
return "失败";
|
|
201
|
+
if (/^inconclusive$/i.test(status))
|
|
202
|
+
return "无法确定";
|
|
203
|
+
return status;
|
|
204
|
+
}
|
|
205
|
+
function translateMediaDetail(detail) {
|
|
206
|
+
return toChineseErrorMessage(detail);
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=error-message.js.map
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
function isPidAlive(pid) {
|
|
5
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
6
|
+
return false;
|
|
7
|
+
try {
|
|
8
|
+
process.kill(pid, 0);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function parseOwner(raw) {
|
|
16
|
+
try {
|
|
17
|
+
const value = JSON.parse(raw);
|
|
18
|
+
if (!Number.isInteger(value.pid) ||
|
|
19
|
+
Number(value.pid) <= 0 ||
|
|
20
|
+
typeof value.nonce !== "string" ||
|
|
21
|
+
!/^[a-f0-9-]{16,80}$/i.test(value.nonce) ||
|
|
22
|
+
typeof value.startedAt !== "string") {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function parseOwnerPid(raw) {
|
|
32
|
+
const owner = parseOwner(raw);
|
|
33
|
+
if (owner)
|
|
34
|
+
return owner.pid;
|
|
35
|
+
// Migration guard for pre-nonce injector.lock files. They cannot be safely
|
|
36
|
+
// released by this implementation, but a live legacy pid must still block
|
|
37
|
+
// takeover instead of creating two injectors.
|
|
38
|
+
try {
|
|
39
|
+
const value = JSON.parse(raw);
|
|
40
|
+
const pid = Number(value?.pid);
|
|
41
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Cross-process lease acquired with O_EXCL (`wx`).
|
|
49
|
+
*
|
|
50
|
+
* The nonce prevents one same-pid owner from releasing another lock. Dead or
|
|
51
|
+
* stale malformed owners are quarantined before retrying, so takeover never
|
|
52
|
+
* uses a read-then-overwrite window.
|
|
53
|
+
*/
|
|
54
|
+
export async function acquireFileLock(filePath, opts = {}) {
|
|
55
|
+
const resolved = path.resolve(filePath);
|
|
56
|
+
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
|
57
|
+
const staleMs = Math.max(1_000, opts.staleMs ?? 120_000);
|
|
58
|
+
const owner = {
|
|
59
|
+
pid: process.pid,
|
|
60
|
+
nonce: crypto.randomUUID(),
|
|
61
|
+
startedAt: new Date().toISOString(),
|
|
62
|
+
...(opts.purpose ? { purpose: opts.purpose } : {}),
|
|
63
|
+
};
|
|
64
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
65
|
+
try {
|
|
66
|
+
const handle = await fs.open(resolved, "wx", 0o600);
|
|
67
|
+
try {
|
|
68
|
+
await handle.writeFile(`${JSON.stringify(owner, null, 2)}\n`, "utf8");
|
|
69
|
+
await handle.sync();
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
await handle.close().catch(() => { });
|
|
73
|
+
await fs.rm(resolved, { force: true }).catch(() => { });
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
let released = false;
|
|
77
|
+
return {
|
|
78
|
+
owner,
|
|
79
|
+
async release() {
|
|
80
|
+
if (released)
|
|
81
|
+
return;
|
|
82
|
+
released = true;
|
|
83
|
+
try {
|
|
84
|
+
const raw = await fs.readFile(resolved, "utf8");
|
|
85
|
+
const current = parseOwner(raw);
|
|
86
|
+
if (current?.nonce !== owner.nonce)
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
await handle.close().catch(() => { });
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const raw = await fs.readFile(resolved, "utf8");
|
|
97
|
+
if (parseOwner(raw)?.nonce === owner.nonce) {
|
|
98
|
+
await fs.rm(resolved, { force: true });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
/* already released/taken over */
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
const code = error && typeof error === "object" && "code" in error
|
|
109
|
+
? String(error.code)
|
|
110
|
+
: "";
|
|
111
|
+
if (code !== "EEXIST")
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
let raw = "";
|
|
115
|
+
let age = 0;
|
|
116
|
+
try {
|
|
117
|
+
const [text, stat] = await Promise.all([
|
|
118
|
+
fs.readFile(resolved, "utf8"),
|
|
119
|
+
fs.stat(resolved),
|
|
120
|
+
]);
|
|
121
|
+
raw = text;
|
|
122
|
+
age = Date.now() - stat.mtimeMs;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const existing = parseOwner(raw);
|
|
128
|
+
const existingPid = parseOwnerPid(raw);
|
|
129
|
+
if (existingPid != null && isPidAlive(existingPid)) {
|
|
130
|
+
throw new Error(`Another ${opts.purpose ?? "operation"} is running (pid ${existingPid}).`);
|
|
131
|
+
}
|
|
132
|
+
if (!existing && age <= staleMs) {
|
|
133
|
+
throw new Error(`Another ${opts.purpose ?? "operation"} may be starting; lock owner is not readable yet.`);
|
|
134
|
+
}
|
|
135
|
+
const quarantine = `${resolved}.stale-${process.pid}-${crypto.randomUUID()}`;
|
|
136
|
+
try {
|
|
137
|
+
await fs.rename(resolved, quarantine);
|
|
138
|
+
const movedRaw = await fs.readFile(quarantine, "utf8").catch(() => "");
|
|
139
|
+
if (movedRaw !== raw) {
|
|
140
|
+
await fs.rename(quarantine, resolved).catch(() => { });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
await fs.rm(quarantine, { force: true });
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
await fs.rm(quarantine, { force: true }).catch(() => { });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
throw new Error(`Could not acquire ${opts.purpose ?? "file"} lock.`);
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=file-lock.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./constants.js";
|
|
2
|
+
export * from "./types.js";
|
|
3
|
+
export * from "./host-session.js";
|
|
4
|
+
export * from "./media-validation.js";
|
|
5
|
+
export * from "./media-server.js";
|
|
6
|
+
export { defaultDataRoot, resolveDataPaths, isPathInsideRoot, } from "./paths.js";
|
|
7
|
+
export * from "./file-lock.js";
|
|
8
|
+
export * from "./error-message.js";
|
|
9
|
+
export * from "./background-store.js";
|
|
10
|
+
export * from "./bundled-gallery.js";
|
|
11
|
+
export * from "./apply-transaction.js";
|
|
12
|
+
//# sourceMappingURL=index.js.map
|