diff --git a/apps/tray/session-host.mjs b/apps/tray/session-host.mjs index e7bb49b..aac2f3c 100644 --- a/apps/tray/session-host.mjs +++ b/apps/tray/session-host.mjs @@ -9,10 +9,11 @@ * Protocol (JSON): * GET /health * GET /status - * POST /apply/image { "imagePath": "..." } - * POST /apply/video { "videoPath": "...", "imagePath"?: "...", "startAt"?: number } + * POST /apply/image { "imagePath": "...", "source"?: "managed"|"local" } + * POST /apply/video { "videoPath": "...", "imagePath"?: "...", "source"?: "managed"|"local", "startAt"?: number } * POST /apply/clear {} * POST /reapply {} // republish active background into live sessions + * POST /theme/apply { "name": "...", "input": ApplyInput } * POST /theme/save { "name": "..." } // keep current image/video * GET /theme/list * POST /theme/use { "id": "..." } @@ -51,6 +52,54 @@ function argValue(name) { return value && !value.startsWith("--") ? value : null; } +function parseImportMode(value) { + if (value == null) return undefined; + if (value === "managed" || value === "local") return value; + throw new Error("source 必须是 managed 或 local。"); +} + +function parseThemeApplyInput(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error("input 必须是图片或视频导入参数。"); + } + if (value.type === "image") { + if (typeof value.imagePath !== "string" || !value.imagePath) { + throw new Error("图片主题必须提供 imagePath。"); + } + const input = { + type: "image", + imagePath: path.resolve(value.imagePath), + source: parseImportMode(value.source), + }; + if (value.effects && typeof value.effects === "object") { + input.effects = value.effects; + } + return input; + } + if (value.type === "video") { + if (typeof value.videoPath !== "string" || !value.videoPath) { + throw new Error("视频主题必须提供 videoPath。"); + } + const input = { + type: "video", + videoPath: path.resolve(value.videoPath), + source: parseImportMode(value.source), + }; + if (typeof value.imagePath === "string" && value.imagePath) { + input.imagePath = path.resolve(value.imagePath); + } + if (value.startAt != null) { + const startAt = Number(value.startAt); + if (!Number.isFinite(startAt) || startAt < 0) { + throw new Error("startAt 必须是非负数字(秒)。"); + } + input.startAt = startAt; + } + return input; + } + throw new Error("input.type 必须是 image 或 video。"); +} + const hostKind = argValue("--host") ?? "codex"; if (hostKind !== "codex" && hostKind !== "dsh") { console.error("--host 必须是 codex 或 dsh。"); @@ -312,6 +361,7 @@ const server = http.createServer( type: "image", imagePath: path.resolve(body.imagePath), }; + imageInput.source = parseImportMode(body.source); if (body.effects && typeof body.effects === "object") { imageInput.effects = body.effects; } @@ -332,6 +382,7 @@ const server = http.createServer( type: "video", videoPath: path.resolve(body.videoPath), }; + videoInput.source = parseImportMode(body.source); if (typeof body.imagePath === "string" && body.imagePath) { videoInput.imagePath = path.resolve(body.imagePath); } @@ -357,6 +408,25 @@ const server = http.createServer( send(res, result.ok ? 200 : 422, result); return; } + if (req.method === "POST" && url === "/theme/apply") { + const body = await readBody(req); + if (typeof body.name !== "string" || !body.name.trim()) { + send(res, 400, { ok: false, error: "必须提供主题名称。" }); + return; + } + if (typeof session.applyAndSaveTheme !== "function") { + send(res, 501, { ok: false, error: "当前会话不支持应用并保存主题。" }); + return; + } + const input = parseThemeApplyInput(body.input); + const result = await session.applyAndSaveTheme(input, body.name.trim()); + send( + res, + result.ok ? 200 : 422, + result.ok ? { ...result, theme: publicTheme(result.theme) } : result, + ); + return; + } if (req.method === "POST" && url === "/theme/save") { const body = await readBody(req); if (typeof body.name !== "string" || !body.name.trim()) { diff --git a/design-demos/dsh-background-bar/design-spec.md b/design-demos/dsh-background-bar/design-spec.md new file mode 100644 index 0000000..a3a1526 --- /dev/null +++ b/design-demos/dsh-background-bar/design-spec.md @@ -0,0 +1,9 @@ +# DSH 背景栏视觉方向说明 + +本次不是重做 DSH,也不是把背景功能扩张成独立的素材管理器。目标是让侧边栏里的「背景」入口更像一个稳定、可信、长期使用的本地工具:用户能在几秒内完成图片导入、视频导入、声音控制、清除背景和已保存主题切换;正在处理时知道系统在做什么;发生错误或超时时按钮一定恢复;同时不把绝对路径、技术协议或多余统计暴露给普通用户。背景栏仍然依附 DSH 左侧栏,不能抢过会话、模型和输入区的主层级。 + +受众是主要在 Windows 本机使用 DSH 的用户。观看距离约为普通笔记本 60–90 厘米,面板宽度应控制在 224–272 像素,正文不低于 13 像素,辅助标签不低于 11 像素。视觉温度需要冷静、可靠、轻微有人味,避免常见 AI 产品的紫蓝渐变、发光描边、Bento 卡片堆叠、每个动作都配一个装饰图标,以及过度圆润的胶囊按钮。颜色从真实「画窗」背景里的雨蓝、雾灰和 DSH 现有深色侧栏采样,收敛为一组中性灰阶加一个低饱和蓝;错误只在发生时使用克制的红色。字体沿用 DSH 系统字体,不额外下载字体。 + +内容结构固定为四层:第一层是当前背景和来源状态;第二层是最常用的图片、视频导入;第三层是声音、清除、皮肤中心等次级操作;第四层是已保存主题。主题列表只使用现有名称、媒体类型、来源类型和选中状态,不新增缩略图接口,不把本地路径送到网页。忙碌状态必须局部化:当前操作显示进度文本,其余按钮暂时不可用,但面板仍可关闭;45 秒客户端上限后恢复交互并说明原背景保持不变。原生文件选择器是用户控制的系统窗口,不套用该操作超时。 + +三个方向共享以上功能,但构图互异。方向 A 用包豪斯式规则线和编号建立明确秩序,强调“工具盒”;方向 B 最大程度融入 DSH 本体,把状态和动作做成低噪声的原生侧栏;方向 C 借鉴纸质媒体清单,把导入动作和已保存主题放在一条连续的纵向轨道里,强调主题切换效率。三版都不依赖新增后端能力,选定后只改 `console.js` 的结构和样式,不动 local 导入、事务或媒体服务协议。 diff --git a/design-demos/dsh-background-bar/direction-approved.md b/design-demos/dsh-background-bar/direction-approved.md new file mode 100644 index 0000000..dc8f820 --- /dev/null +++ b/design-demos/dsh-background-bar/direction-approved.md @@ -0,0 +1,6 @@ +# DSH 背景栏方向确认 + +- 已评审:A「网格工具盒」、B「原生侧栏」、C「媒体清单」 +- 最终设计:`direction-c-media-ledger.html`、`direction-c-media-ledger.png` +- 用户选择原话:`C吧` +- 结论:正式背景栏采用 C「媒体清单」方向;保持现有 DSH 侧栏位置、local 零复制导入、主题切换和超时恢复语义。 diff --git a/design-demos/dsh-background-bar/direction-c-media-ledger.html b/design-demos/dsh-background-bar/direction-c-media-ledger.html new file mode 100644 index 0000000..6843793 --- /dev/null +++ b/design-demos/dsh-background-bar/direction-c-media-ledger.html @@ -0,0 +1,4 @@ + +方向 C · 媒体清单
探索未至之境
diff --git a/design-demos/dsh-background-bar/direction-c-media-ledger.png b/design-demos/dsh-background-bar/direction-c-media-ledger.png new file mode 100644 index 0000000..997755b Binary files /dev/null and b/design-demos/dsh-background-bar/direction-c-media-ledger.png differ diff --git a/integrations/deepseek-harness/README.zh-CN.md b/integrations/deepseek-harness/README.zh-CN.md index 6d33156..f64a7fb 100644 --- a/integrations/deepseek-harness/README.zh-CN.md +++ b/integrations/deepseek-harness/README.zh-CN.md @@ -2,7 +2,7 @@ 该 Cordis 插件向 DSH Web 注入 beautiCode 浏览器客户端,并提供本机鉴权接口。支持图片、MP4、播放位置、静音、摸鱼模式以及清除背景。 -装好插件并运行 `dsh web` 后,侧栏「设置」上方会出现「背景」:可从文件夹选图片或 MP4、清除、开关声音、切换已保存主题。网页控制台没有摸鱼;浅色/深色跟 DSH 自己的外观走。下次打开会恢复上次背景。 +装好插件并运行 `dsh web` 后,侧栏「设置」上方会出现「背景」:在 Windows 上点击图片或视频会打开系统文件选择器,选好后在页面内命名,背景立即应用并进入已保存主题;也可以清除、开关声音、切换或删除主题,以及打开「皮肤中心」安装已审核的社区皮肤。本地导入只在 Node 端保存绝对路径,并通过带 Range 支持的回环媒体服务读取原文件,不复制用户选择的图片或整段视频主文件。Windows 强制使用本地引用;原生选择器不可用时会明确报错,不会静默上传或复制媒体。非 Windows 环境保留兼容上传,并明确提示该模式会保存托管副本。网页控制台没有摸鱼;浅色/深色跟 DSH 自己的外观走。下次打开会恢复上次背景。设置 `BEAUTICODE_SKIN_CENTER` 或填写 `skin-center.json` 后,皮肤中心才会显示远程目录。 插件也会注册对话工具(`beauticode_*`)和斜杠命令(`/bg`、`/bg-theme`、`/bg-clear`)。不需要托盘;托盘若已在跑则复用它。 @@ -40,7 +40,7 @@ npx @deepseek-ai/dsh web 手动接入时: -1. 修改 `cordis.patch.example.yml` 中的 `file:///.../index.mjs`。 +1. 修改 `cordis.patch.yml` 中的插件接入项;优先使用上面的安装命令自动完成接线。 2. 确保 DSH 和 beautiCode 使用相同的 `BEAUTICODE_DATA_ROOT`。 3. 自己运行 `dsh web`(或带 `--patch`)。 4. 打开页面后用侧栏「背景」,或输入 `/bg <本机绝对路径>`,或直接跟 AI 说把某个视频/图片设为背景。 diff --git a/integrations/deepseek-harness/agent.mjs b/integrations/deepseek-harness/agent.mjs index f4b1810..7cb952e 100644 --- a/integrations/deepseek-harness/agent.mjs +++ b/integrations/deepseek-harness/agent.mjs @@ -1,3 +1,4 @@ +import path from "node:path"; import { callDshControl, formatStatusText, @@ -5,7 +6,7 @@ import { matchSavedTheme, stripPathQuotes, } from "./control-client.mjs"; -import { resolveApplyBackend, stopInProcessSession } from "./host-apply.mjs"; +import { loadAdapter, resolveApplyBackend, stopInProcessSession } from "./host-apply.mjs"; import { ATMOSPHERE_PRESETS, effectsForPreset, presetImagePath } from "./presets.mjs"; const APPLY_TIMEOUT_MS = 180_000; @@ -32,6 +33,60 @@ function fail(error) { throw error instanceof Error ? error : new Error(String(error)); } +export function themeNameFromFilePath(filePath, fallback = "主题") { + const base = path.basename(String(filePath ?? "").replaceAll("\\", "/")); + const ext = path.extname(base); + let name = (ext ? base.slice(0, -ext.length) : base).trim(); + name = name + .replace(/[<>:"/\\|?*]/g, " ") + .replace(/[\u0000-\u001f]/g, "") + .replace(/\s+/g, " ") + .trim(); + if (!name) name = fallback; + if (name.length > 80) name = name.slice(0, 80).trim(); + if (!name) name = fallback; + return name; +} + +async function chineseError(error) { + const raw = error instanceof Error ? error.message : String(error ?? ""); + try { + const adapter = await loadAdapter(); + if (typeof adapter.toChineseErrorMessage === "function") { + return adapter.toChineseErrorMessage(error); + } + } catch { + /* keep raw */ + } + return raw || "操作失败。"; +} + +async function unwrapApplyResult(result, fallbackMode, message) { + if (!result || result.ok === false) { + const failure = new Error(await chineseError(result?.error || "操作失败。")); + if (result?.sourceMode != null) failure.sourceMode = result.sourceMode; + if (result?.timings != null) failure.timings = result.timings; + throw failure; + } + return { + ok: true, + generation: result.generation ?? null, + mode: result.mode ?? fallbackMode, + sourceMode: result.sourceMode ?? null, + timings: result.timings ?? null, + message, + ...(result.theme + ? { + theme: { + id: result.theme.id, + name: result.theme.name, + type: result.theme.type ?? null, + }, + } + : {}), + }; +} + function commandResultFromError(error) { return { kind: "error", @@ -57,10 +112,20 @@ function unwrapApply(result, fallbackMode, message) { generation: result.generation ?? null, mode: result.mode ?? fallbackMode, message, + ...(result.theme + ? { + theme: { + id: result.theme.id, + name: result.theme.name, + type: result.theme.type ?? null, + }, + } + : {}), }; } function presentStatus(status) { + const background = status.manifest?.background ?? status.background ?? null; return { ok: true, hostReady: status.hostReady === true || status.sessions > 0, @@ -68,7 +133,13 @@ function presentStatus(status) { fish: status.fish === true, muted: status.muted !== false, tone: status.tone ?? "dark", - background: status.manifest?.background ?? status.background ?? null, + background, + sourceMode: background + ? background.source?.kind === "local" + ? "local" + : "managed" + : "clear", + themeId: typeof status.themeId === "string" && status.themeId ? status.themeId : null, message: formatStatusText(status), }; } @@ -106,24 +177,45 @@ export function createBeauticodeActions(dataRootOrOptions) { fail("beauticode_apply_image 只接受图片文件。"); } const effects = effectsForPreset(options?.effects?.preset) || options?.effects || null; - const input = { type: "image", imagePath: inspected.path }; + const persistTheme = options?.persistTheme !== false; + const themeName = + String(options?.themeName ?? "").trim() || + themeNameFromFilePath(inspected.path, "图片"); + const source = options?.source === "managed" ? "managed" : "local"; + const input = { type: "image", imagePath: inspected.path, source }; if (effects) input.effects = effects; const resolved = await backend(); if (resolved.kind === "tray") { - const body = { imagePath: inspected.path }; - if (effects) body.effects = effects; - return unwrapApply( - await request({ - method: "POST", - path: "/apply/image", - body, - signal, - }), + const result = await request({ + method: "POST", + path: persistTheme ? "/theme/apply" : "/apply/image", + body: persistTheme + ? { name: themeName, input } + : { + imagePath: inspected.path, + source, + ...(effects ? { effects } : {}), + }, + signal, + }); + return unwrapApplyResult( + result, "image", - "已将图片设为背景。", + persistTheme + ? `已将「${result.theme?.name || themeName}」设为背景。` + : "已将图片设为背景。", ); } - return unwrapApply(await resolved.session.apply(input), "image", "已将图片设为背景。"); + const applied = persistTheme + ? await resolved.session.applyAndSaveTheme(input, themeName) + : await resolved.session.apply(input); + return unwrapApplyResult( + applied, + "image", + persistTheme + ? `已将「${applied.theme?.name || themeName}」设为背景。` + : "已将图片设为背景。", + ); }, async applyPreset(id, signal) { @@ -132,6 +224,7 @@ export function createBeauticodeActions(dataRootOrOptions) { if (!preset || !imagePath) fail("未找到内置主题文件。"); const result = await this.applyImage(imagePath, signal, { effects: effectsForPreset(id), + persistTheme: false, }); try { await this.setTone(preset.tone, signal); @@ -167,8 +260,13 @@ export function createBeauticodeActions(dataRootOrOptions) { if (inspected.kind !== "video") { fail("beauticode_apply_video 只接受 .mp4 文件。"); } - const body = { videoPath: inspected.path }; - const localInput = { type: "video", videoPath: inspected.path }; + const persistTheme = input?.persistTheme !== false; + const themeName = + String(input?.themeName ?? "").trim() || + themeNameFromFilePath(inspected.path, "视频"); + const source = input?.source === "managed" ? "managed" : "local"; + const body = { videoPath: inspected.path, persistTheme, themeName, source }; + const localInput = { type: "video", videoPath: inspected.path, source }; if (typeof input.poster === "string" && input.poster.trim()) { const poster = await inspectLocalMedia(input.poster); if (!poster.ok) fail(poster.error); @@ -186,21 +284,29 @@ export function createBeauticodeActions(dataRootOrOptions) { } const resolved = await backend(); if (resolved.kind === "tray") { - return unwrapApply( - await request({ - method: "POST", - path: "/apply/video", - body, - signal, - }), + const result = await request({ + method: "POST", + path: persistTheme ? "/theme/apply" : "/apply/video", + body: persistTheme ? { name: themeName, input: localInput } : body, + signal, + }); + return unwrapApplyResult( + result, "video", - "已将视频设为背景。", + persistTheme + ? `已将「${result.theme?.name || themeName}」设为背景。` + : "已将视频设为背景。", ); } - return unwrapApply( - await resolved.session.apply(localInput), + const applied = persistTheme + ? await resolved.session.applyAndSaveTheme(localInput, themeName) + : await resolved.session.apply(localInput); + return unwrapApplyResult( + applied, "video", - "已将视频设为背景。", + persistTheme + ? `已将「${applied.theme?.name || themeName}」设为背景。` + : "已将视频设为背景。", ); }, @@ -240,6 +346,39 @@ export function createBeauticodeActions(dataRootOrOptions) { }); }, + async importTheme(input, signal) { + const name = String(input?.name ?? "").trim(); + const imagePath = String(input?.imagePath ?? "").trim(); + if (!name || !imagePath) fail("导入皮肤必须提供名称和图片。"); + const body = { + name, + imagePath, + }; + if (typeof input.videoPath === "string" && input.videoPath.trim()) { + body.videoPath = input.videoPath.trim(); + } + if (input.effects) body.effects = input.effects; + if (input.source) body.source = input.source; + const resolved = await backend(); + if (resolved.kind === "tray") { + const result = await request({ + method: "POST", + path: "/theme/import", + body, + signal, + timeoutMs: 30 * 60 * 1000, + }); + if (!result || result.ok === false) fail(result?.error || "导入皮肤失败。"); + return { + ok: true, + theme: result.theme, + message: `已保存皮肤「${result.theme.name}」。`, + }; + } + const theme = await resolved.session.importSavedTheme(body); + return { ok: true, theme, message: `已保存皮肤「${theme.name}」。` }; + }, + async listThemes(signal) { const resolved = await backend(); if (resolved.kind === "tray") { @@ -278,6 +417,32 @@ export function createBeauticodeActions(dataRootOrOptions) { }; }, + async deleteTheme(id, signal) { + const themeId = String(id ?? "").trim(); + if (!themeId) fail("必须提供主题。"); + const resolved = await backend(); + if (resolved.kind === "tray") { + const result = await request({ + method: "POST", + path: "/theme/delete", + body: { id: themeId }, + signal, + timeoutMs: QUICK_TIMEOUT_MS, + }); + if (!result || result.ok === false) { + fail(await chineseError(result?.error || "删除主题失败。")); + } + return { ok: true, deleted: true, message: "已删除主题。" }; + } + try { + const deleted = await resolved.session.deleteSavedTheme(themeId); + if (!deleted) fail("未找到已保存的主题。"); + return { ok: true, deleted: true, message: "已删除主题。" }; + } catch (error) { + fail(await chineseError(error)); + } + }, + async setFish(enabled, signal) { const resolved = await backend(); const want = Boolean(enabled); diff --git a/integrations/deepseek-harness/atmosphere.js b/integrations/deepseek-harness/atmosphere.js index 19338c7..ff311d7 100644 --- a/integrations/deepseek-harness/atmosphere.js +++ b/integrations/deepseek-harness/atmosphere.js @@ -92,8 +92,8 @@ html[data-bc-resolved-tone="light"][data-bc-gallery="true"] body{ } html[data-bc-gallery="true"] #root{position:relative;z-index:1;background:transparent!important} html[data-bc-gallery="true"] [class*="_fade"]{display:none!important} -html[data-bc-gallery="true"] #beauticode-bg-stage>img, -html[data-bc-gallery="true"] #beauticode-bg-stage>video{opacity:0!important} +html[data-bc-gallery="true"] #beauticode-bg-stage img, +html[data-bc-gallery="true"] #beauticode-bg-stage video{opacity:0!important} html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;pointer-events:none!important} `; return style; diff --git a/integrations/deepseek-harness/bin/beauticode-dsh b/integrations/deepseek-harness/bin/beauticode-dsh index e1cbe2c..d038d73 100644 --- a/integrations/deepseek-harness/bin/beauticode-dsh +++ b/integrations/deepseek-harness/bin/beauticode-dsh @@ -1,219 +1,7 @@ #!/usr/bin/env node -/** - * One-line installer: npx beauticode-dsh - * - * Copies this package to a stable local folder and writes the DSH patch so - * `dsh web` loads beautiCode. Does not start DeepSeek Harness. - */ -import fs from "node:fs"; -import fsp from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; +import { runCli } from "../cli.js"; -const here = path.dirname(fileURLToPath(import.meta.url)); -const pluginName = "@beauticode/dsh-plugin"; -const bridgeId = "beauticode-bridge"; - -function argValue(argv, name) { - const idx = argv.indexOf(name); - if (idx === -1) return null; - const value = argv[idx + 1]; - return value && !value.startsWith("--") ? value : null; -} - -function defaultDataRoot() { - if (process.env.BEAUTICODE_DATA_ROOT) return process.env.BEAUTICODE_DATA_ROOT; - if (process.env.LOCALAPPDATA) { - return path.join(process.env.LOCALAPPDATA, "beautiCode"); - } - return path.join(os.homedir(), ".beauticode"); -} - -function defaultDshHome() { - return process.env.DSH_HOME || path.join(os.homedir(), ".dsh"); -} - -function toFileUri(filePath) { - const full = path.resolve(filePath).replaceAll("\\", "/"); - if (/^[A-Za-z]:/.test(full)) return `file:///${full}`; - return pathToFileURL(full).href; -} - -function fileUriInsert(uri) { - return [ - "# beauticode-bridge (installer)", - "- insert:", - " - id: beauticode-bridge", - ` name: '${uri}'`, - " inject: [webServer]", - "", - ].join("\n"); -} - -function packageInsert() { - return [ - "# beauticode-bridge (installer)", - "- insert:", - " - id: beauticode-bridge", - ` name: '${pluginName}'`, - " inject: [webServer]", - "", - ].join("\n"); -} - -function hasBridge(text) { - return new RegExp(`^\\s*-\\s*id:\\s*${bridgeId}\\s*$`, "m").test(text); -} - -function stripBridge(text) { - const patterns = [ - /(?:^|\r?\n)# beauticode-bridge \(installer\)\r?\n- insert:\r?\n(?:[ \t]+.*\r?\n)*/g, - /(?:^|\r?\n)- insert:\r?\n(?:[ \t]+.*\r?\n)*?[ \t]+-\s*id:\s*beauticode-bridge\r?\n(?:[ \t]+.*\r?\n)*/g, - ]; - let next = text; - for (const pattern of patterns) next = next.replace(pattern, "\n"); - return next; -} - -async function writePatch(filePath, body) { - await fsp.mkdir(path.dirname(filePath), { recursive: true }); - if (!fs.existsSync(filePath)) { - await fsp.writeFile(filePath, body, "utf8"); - return; - } - const raw = await fsp.readFile(filePath, "utf8"); - if (hasBridge(raw)) { - const replaced = raw.replace( - /(?:# beauticode-bridge \(installer\)\r?\n)?- insert:\r?\n(?:[ \t]+.*\r?\n)*?[ \t]+-\s*id:\s*beauticode-bridge\r?\n(?:[ \t]+.*\r?\n)*/, - body, - ); - await fsp.writeFile(filePath, replaced.endsWith("\n") ? replaced : `${replaced}\n`, "utf8"); - return; - } - const stripped = raw.trim(); - if (stripped === "" || stripped === "[]") { - await fsp.writeFile(filePath, body, "utf8"); - return; - } - await fsp.writeFile(filePath, `${stripped}\n\n${body}`, "utf8"); -} - -async function removePatch(filePath) { - if (!fs.existsSync(filePath)) return false; - const raw = await fsp.readFile(filePath, "utf8"); - if (!hasBridge(raw)) return false; - const cleaned = stripBridge(raw).trim(); - if (cleaned === "" || cleaned === "[]") { - await fsp.writeFile( - filePath, - "# Your patch layer for this dsh profile.\n[]\n", - "utf8", - ); - return true; - } - await fsp.writeFile(filePath, `${cleaned}\n`, "utf8"); - return true; -} - -function shouldCopy(source) { - const relative = path.relative(here, source); - if (relative.startsWith("test") || relative.includes(`${path.sep}test${path.sep}`)) { - return false; - } - if (relative.endsWith(".test.mjs")) return false; - return true; -} - -async function copyPackage(dest) { - await fsp.rm(dest, { recursive: true, force: true }); - await fsp.cp(here, dest, { - recursive: true, - filter: (source) => shouldCopy(source), - }); -} - -async function ensureEngine(dest) { - const vendor = path.join(dest, "vendor", "adapter-dsh", "index.js"); - if (fs.existsSync(vendor)) return; - const packPath = path.resolve(here, "../../scripts/pack-dsh-plugin.mjs"); - if (!fs.existsSync(packPath)) { - throw new Error("插件包不完整:缺少本机导入引擎。请重新执行 npx beauticode-dsh。"); - } - const { stageEngineInto } = await import(pathToFileURL(packPath).href); - await stageEngineInto(dest); -} - -async function install(opts) { - const dest = path.resolve(opts.pluginHome); - const dshHome = path.resolve(opts.dshHome); - const webProfile = path.join(dshHome, "profiles", "web"); - const webPatch = path.join(webProfile, "cordis.patch.yml"); - const webPackage = path.join(webProfile, "package.json"); - const homePatch = path.join(dshHome, "cordis.patch.yml"); - - await fsp.mkdir(dest, { recursive: true }); - await copyPackage(dest); - await ensureEngine(dest); - const indexFile = path.join(dest, "index.mjs"); - if (!fs.existsSync(indexFile)) { - throw new Error(`缺少插件入口:${indexFile}`); - } - - if (fs.existsSync(webPackage)) { - await writePatch(webPatch, fileUriInsert(toFileUri(indexFile))); - if (fs.existsSync(homePatch)) { - const homeRaw = await fsp.readFile(homePatch, "utf8"); - if (hasBridge(homeRaw)) await removePatch(homePatch); - } - console.log(`已写入 ${webPatch}`); - console.log(`插件已复制到 ${dest}`); - console.log("请自己运行:npx @deepseek-ai/dsh web"); - return { dest, patch: webPatch }; - } - - await fsp.mkdir(dshHome, { recursive: true }); - await writePatch(homePatch, fileUriInsert(toFileUri(indexFile))); - console.log(`DSH web profile 还不存在,已写入 ${homePatch}`); - console.log(`插件已复制到 ${dest}`); - console.log("请自己运行:npx @deepseek-ai/dsh web"); - return { dest, patch: homePatch }; -} - -async function uninstall(opts) { - const dshHome = path.resolve(opts.dshHome); - const removed = []; - if (await removePatch(path.join(dshHome, "profiles", "web", "cordis.patch.yml"))) { - removed.push("web patch"); - } - if (await removePatch(path.join(dshHome, "cordis.patch.yml"))) { - removed.push("home patch"); - } - const dest = path.resolve(opts.pluginHome); - if (fs.existsSync(dest)) { - await fsp.rm(dest, { recursive: true, force: true }); - removed.push(dest); - } - console.log(removed.length ? `已移除:${removed.join("、")}` : "没有可移除的 beautiCode 插件接线。"); - return { removed }; -} - -export async function runCli(argv = process.argv.slice(2)) { - const dshHome = argValue(argv, "--dsh-home") || defaultDshHome(); - const pluginHome = - argValue(argv, "--plugin-home") || path.join(defaultDataRoot(), "plugin"); - const opts = { dshHome, pluginHome }; - if (argv.includes("--remove")) return uninstall(opts); - return install(opts); -} - -const launchedDirectly = - Boolean(process.argv[1]) && - pathToFileURL(path.resolve(process.argv[1])).href.toLowerCase() === - import.meta.url.toLowerCase(); -if (launchedDirectly) { - runCli().catch((error) => { - console.error(error instanceof Error ? error.message : String(error)); - process.exit(1); - }); -} +runCli().catch((error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); +}); diff --git a/integrations/deepseek-harness/cli.js b/integrations/deepseek-harness/cli.js index e1cbe2c..f95b548 100644 --- a/integrations/deepseek-harness/cli.js +++ b/integrations/deepseek-harness/cli.js @@ -11,8 +11,24 @@ import os from "node:os"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; -const here = path.dirname(fileURLToPath(import.meta.url)); -const pluginName = "@beauticode/dsh-plugin"; +function findPackageRoot(startDir) { + let dir = startDir; + for (let i = 0; i < 5; i += 1) { + if ( + fs.existsSync(path.join(dir, "package.json")) && + fs.existsSync(path.join(dir, "index.mjs")) + ) { + return dir; + } + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + return startDir; +} + +const here = findPackageRoot(path.dirname(fileURLToPath(import.meta.url))); +const pluginName = "beauticode-dsh"; const bridgeId = "beauticode-bridge"; function argValue(argv, name) { @@ -34,6 +50,14 @@ function defaultDshHome() { return process.env.DSH_HOME || path.join(os.homedir(), ".dsh"); } +function defaultPluginHome(dshHome) { + return path.join(path.resolve(dshHome), "plugins", pluginName); +} + +function legacyDefaultPluginHome() { + return path.join(defaultDataRoot(), "plugin"); +} + function toFileUri(filePath) { const full = path.resolve(filePath).replaceAll("\\", "/"); if (/^[A-Za-z]:/.test(full)) return `file:///${full}`; @@ -67,44 +91,110 @@ function hasBridge(text) { } function stripBridge(text) { - const patterns = [ - /(?:^|\r?\n)# beauticode-bridge \(installer\)\r?\n- insert:\r?\n(?:[ \t]+.*\r?\n)*/g, - /(?:^|\r?\n)- insert:\r?\n(?:[ \t]+.*\r?\n)*?[ \t]+-\s*id:\s*beauticode-bridge\r?\n(?:[ \t]+.*\r?\n)*/g, - ]; - let next = text; - for (const pattern of patterns) next = next.replace(pattern, "\n"); - return next; + const lines = text.split(/\r?\n/); + const removed = new Array(lines.length).fill(false); + const indentOf = (line) => line.match(/^[ \t]*/)?.[0].length ?? 0; + + for (let i = 0; i < lines.length; i += 1) { + const insert = lines[i].match(/^([ \t]*)-\s*insert:\s*(?:#.*)?$/); + if (!insert) continue; + const insertIndent = insert[1].length; + let blockEnd = i + 1; + while (blockEnd < lines.length) { + const line = lines[blockEnd]; + if (line.trim() && indentOf(line) <= insertIndent) break; + blockEnd += 1; + } + + let foundBridge = false; + for (let j = i + 1; j < blockEnd; ) { + const bridge = lines[j].match( + /^([ \t]*)-\s*id:\s*beauticode-bridge\s*(?:#.*)?$/, + ); + if (!bridge || bridge[1].length <= insertIndent) { + j += 1; + continue; + } + foundBridge = true; + const itemIndent = bridge[1].length; + let itemEnd = j + 1; + while (itemEnd < blockEnd) { + const line = lines[itemEnd]; + if (line.trim() && indentOf(line) <= itemIndent) break; + itemEnd += 1; + } + for (let k = j; k < itemEnd; k += 1) removed[k] = true; + j = itemEnd; + } + + if (!foundBridge) continue; + const marker = `${insert[1]}# beauticode-bridge (installer)`; + if (i > 0 && lines[i - 1].trimEnd() === marker) removed[i - 1] = true; + const hasSibling = lines + .slice(i + 1, blockEnd) + .some( + (line, offset) => + !removed[i + 1 + offset] && + line.trim() !== "" && + !line.trimStart().startsWith("#"), + ); + if (!hasSibling) { + for (let k = i; k < blockEnd; k += 1) removed[k] = true; + } + i = blockEnd - 1; + } + + return lines.filter((_, index) => !removed[index]).join("\n"); +} + +function overlayPayload(text) { + return text + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith("#")) + .join("\n"); +} + +function isEmptyOverlay(text) { + const payload = overlayPayload(text); + return payload === "" || payload === "[]"; +} + +// DSH seeds overlays as `# comment\n[]`. `[]` is already a complete YAML +// document, so appending `- insert:` makes js-yaml throw. +function keptOverlay(text) { + const withoutFlowEmpty = text + .split(/\r?\n/) + .filter((line) => line.trim() !== "[]") + .join("\n"); + if (isEmptyOverlay(withoutFlowEmpty)) return ""; + return withoutFlowEmpty.trim(); +} + +function withTrailingNewline(text) { + return text.endsWith("\n") ? text : `${text}\n`; } async function writePatch(filePath, body) { await fsp.mkdir(path.dirname(filePath), { recursive: true }); + const insert = withTrailingNewline(body); if (!fs.existsSync(filePath)) { - await fsp.writeFile(filePath, body, "utf8"); + await fsp.writeFile(filePath, insert, "utf8"); return; } const raw = await fsp.readFile(filePath, "utf8"); - if (hasBridge(raw)) { - const replaced = raw.replace( - /(?:# beauticode-bridge \(installer\)\r?\n)?- insert:\r?\n(?:[ \t]+.*\r?\n)*?[ \t]+-\s*id:\s*beauticode-bridge\r?\n(?:[ \t]+.*\r?\n)*/, - body, - ); - await fsp.writeFile(filePath, replaced.endsWith("\n") ? replaced : `${replaced}\n`, "utf8"); - return; - } - const stripped = raw.trim(); - if (stripped === "" || stripped === "[]") { - await fsp.writeFile(filePath, body, "utf8"); - return; - } - await fsp.writeFile(filePath, `${stripped}\n\n${body}`, "utf8"); + const remainder = hasBridge(raw) ? stripBridge(raw) : raw; + const kept = keptOverlay(remainder); + const next = kept ? `${kept}\n\n${insert}` : insert; + await fsp.writeFile(filePath, withTrailingNewline(next), "utf8"); } async function removePatch(filePath) { if (!fs.existsSync(filePath)) return false; const raw = await fsp.readFile(filePath, "utf8"); if (!hasBridge(raw)) return false; - const cleaned = stripBridge(raw).trim(); - if (cleaned === "" || cleaned === "[]") { + const kept = keptOverlay(stripBridge(raw)); + if (!kept) { await fsp.writeFile( filePath, "# Your patch layer for this dsh profile.\n[]\n", @@ -112,7 +202,7 @@ async function removePatch(filePath) { ); return true; } - await fsp.writeFile(filePath, `${cleaned}\n`, "utf8"); + await fsp.writeFile(filePath, withTrailingNewline(kept), "utf8"); return true; } @@ -125,12 +215,48 @@ function shouldCopy(source) { return true; } +async function sameInstalledVersion(dest) { + try { + const incoming = JSON.parse(await fsp.readFile(path.join(here, "package.json"), "utf8")); + const installed = JSON.parse(await fsp.readFile(path.join(dest, "package.json"), "utf8")); + return ( + incoming.name === installed.name && + incoming.version === installed.version && + fs.existsSync(path.join(dest, "index.mjs")) && + fs.existsSync(path.join(dest, "vendor", "adapter-dsh", "index.js")) + ); + } catch { + return false; + } +} + async function copyPackage(dest) { + if (await sameInstalledVersion(dest)) return false; await fsp.rm(dest, { recursive: true, force: true }); await fsp.cp(here, dest, { recursive: true, filter: (source) => shouldCopy(source), }); + return true; +} + +async function removeLegacyManagedPlugin(legacyHome, currentHome) { + if (!legacyHome) return false; + const legacy = path.resolve(legacyHome); + if (legacy === path.resolve(currentHome) || !fs.existsSync(legacy)) return false; + try { + const pkg = JSON.parse(await fsp.readFile(path.join(legacy, "package.json"), "utf8")); + if ( + ![pluginName, "@beauticode/dsh-plugin"].includes(pkg.name) || + !fs.existsSync(path.join(legacy, "index.mjs")) + ) { + return false; + } + } catch { + return false; + } + await fsp.rm(legacy, { recursive: true, force: true }); + return true; } async function ensureEngine(dest) { @@ -144,6 +270,83 @@ async function ensureEngine(dest) { await stageEngineInto(dest); } +function pluginLinkPath(webProfile) { + return path.join(webProfile, "node_modules", pluginName); +} + +function legacyPluginLinkPath(webProfile) { + return path.join(webProfile, "node_modules", "@beauticode", "dsh-plugin"); +} + +function linkSpecFor(pluginHome) { + return `link:${path.resolve(pluginHome).replaceAll("\\", "/")}`; +} + +async function sameLinkTarget(link, dest) { + try { + const stat = await fsp.lstat(link); + if (stat.isSymbolicLink()) { + const target = await fsp.readlink(link); + return path.resolve(path.dirname(link), target) === path.resolve(dest); + } + if (stat.isDirectory()) { + return path.resolve(link) === path.resolve(dest); + } + } catch { + return false; + } + return false; +} + +async function linkPluginIntoProfile(webProfile, dest) { + const link = pluginLinkPath(webProfile); + const legacy = legacyPluginLinkPath(webProfile); + if (fs.existsSync(legacy)) { + await fsp.rm(legacy, { recursive: true, force: true }); + } + await fsp.mkdir(path.dirname(link), { recursive: true }); + if (fs.existsSync(link)) { + if (await sameLinkTarget(link, dest)) return; + await fsp.rm(link, { recursive: true, force: true }); + } + const type = process.platform === "win32" ? "junction" : "dir"; + await fsp.symlink(path.resolve(dest), link, type); +} + +async function ensureWebPackageDep(webPackage, pluginHome) { + const raw = await fsp.readFile(webPackage, "utf8"); + const json = JSON.parse(raw); + if (!json.dependencies || typeof json.dependencies !== "object" || Array.isArray(json.dependencies)) { + json.dependencies = {}; + } + const spec = linkSpecFor(pluginHome); + const hadLegacy = Object.prototype.hasOwnProperty.call( + json.dependencies, + "@beauticode/dsh-plugin", + ); + delete json.dependencies["@beauticode/dsh-plugin"]; + const current = json.dependencies[pluginName]; + if (current === spec && !hadLegacy) return; + json.dependencies[pluginName] = spec; + await fsp.writeFile(webPackage, `${JSON.stringify(json, null, 2)}\n`, "utf8"); +} + +async function removeWebPackageDep(webPackage) { + if (!fs.existsSync(webPackage)) return false; + const json = JSON.parse(await fsp.readFile(webPackage, "utf8")); + if (!json.dependencies || typeof json.dependencies !== "object") return false; + let changed = false; + for (const name of [pluginName, "@beauticode/dsh-plugin"]) { + if (Object.prototype.hasOwnProperty.call(json.dependencies, name)) { + delete json.dependencies[name]; + changed = true; + } + } + if (!changed) return false; + await fsp.writeFile(webPackage, `${JSON.stringify(json, null, 2)}\n`, "utf8"); + return true; +} + async function install(opts) { const dest = path.resolve(opts.pluginHome); const dshHome = path.resolve(opts.dshHome); @@ -153,7 +356,7 @@ async function install(opts) { const homePatch = path.join(dshHome, "cordis.patch.yml"); await fsp.mkdir(dest, { recursive: true }); - await copyPackage(dest); + const copied = await copyPackage(dest); await ensureEngine(dest); const indexFile = path.join(dest, "index.mjs"); if (!fs.existsSync(indexFile)) { @@ -161,34 +364,50 @@ async function install(opts) { } if (fs.existsSync(webPackage)) { - await writePatch(webPatch, fileUriInsert(toFileUri(indexFile))); + await linkPluginIntoProfile(webProfile, dest); + await ensureWebPackageDep(webPackage, dest); + await writePatch(webPatch, packageInsert()); if (fs.existsSync(homePatch)) { const homeRaw = await fsp.readFile(homePatch, "utf8"); if (hasBridge(homeRaw)) await removePatch(homePatch); } + const migrated = await removeLegacyManagedPlugin(opts.legacyPluginHome, dest); console.log(`已写入 ${webPatch}`); - console.log(`插件已复制到 ${dest}`); + console.log(copied ? `插件已安装到 ${dest}` : `已复用已安装的插件 ${dest}`); + if (migrated) console.log("已迁移 1.0.5 的旧插件目录,已保留主题数据。"); console.log("请自己运行:npx @deepseek-ai/dsh web"); return { dest, patch: webPatch }; } await fsp.mkdir(dshHome, { recursive: true }); await writePatch(homePatch, fileUriInsert(toFileUri(indexFile))); + const migrated = await removeLegacyManagedPlugin(opts.legacyPluginHome, dest); console.log(`DSH web profile 还不存在,已写入 ${homePatch}`); - console.log(`插件已复制到 ${dest}`); + console.log(copied ? `插件已安装到 ${dest}` : `已复用已安装的插件 ${dest}`); + if (migrated) console.log("已迁移 1.0.5 的旧插件目录,已保留主题数据。"); console.log("请自己运行:npx @deepseek-ai/dsh web"); return { dest, patch: homePatch }; } async function uninstall(opts) { const dshHome = path.resolve(opts.dshHome); + const webProfile = path.join(dshHome, "profiles", "web"); const removed = []; - if (await removePatch(path.join(dshHome, "profiles", "web", "cordis.patch.yml"))) { + if (await removePatch(path.join(webProfile, "cordis.patch.yml"))) { removed.push("web patch"); } if (await removePatch(path.join(dshHome, "cordis.patch.yml"))) { removed.push("home patch"); } + if (await removeWebPackageDep(path.join(webProfile, "package.json"))) { + removed.push("web package.json"); + } + for (const link of [pluginLinkPath(webProfile), legacyPluginLinkPath(webProfile)]) { + if (fs.existsSync(link)) { + await fsp.rm(link, { recursive: true, force: true }); + removed.push(link); + } + } const dest = path.resolve(opts.pluginHome); if (fs.existsSync(dest)) { await fsp.rm(dest, { recursive: true, force: true }); @@ -200,9 +419,13 @@ async function uninstall(opts) { export async function runCli(argv = process.argv.slice(2)) { const dshHome = argValue(argv, "--dsh-home") || defaultDshHome(); - const pluginHome = - argValue(argv, "--plugin-home") || path.join(defaultDataRoot(), "plugin"); - const opts = { dshHome, pluginHome }; + const requestedPluginHome = argValue(argv, "--plugin-home"); + const pluginHome = requestedPluginHome || defaultPluginHome(dshHome); + const opts = { + dshHome, + pluginHome, + legacyPluginHome: requestedPluginHome ? null : legacyDefaultPluginHome(), + }; if (argv.includes("--remove")) return uninstall(opts); return install(opts); } diff --git a/integrations/deepseek-harness/client.js b/integrations/deepseek-harness/client.js index 359332f..9bcc253 100644 --- a/integrations/deepseek-harness/client.js +++ b/integrations/deepseek-harness/client.js @@ -8,9 +8,41 @@ globalThis.crypto?.randomUUID?.() || `bc-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`; const desiredModes = { fish: false, muted: true, tone: "auto" }; + // The DSH host owns a hard 10-second verification boundary. Keep the browser + // transaction inside it so failures remain phase-specific instead of turning + // into a generic host timeout. + const CLIENT_APPLY_DEADLINE_MS = 8_000; + const IMAGE_LOAD_TIMEOUT_MS = CLIENT_APPLY_DEADLINE_MS; + const IMAGE_ATTEMPT_TIMEOUT_MS = 3_000; + const IMAGE_MAX_ATTEMPTS = 2; + const VIDEO_STARTUP_TIMEOUT_MS = CLIENT_APPLY_DEADLINE_MS; + const VIDEO_PROBE_TIMEOUT_MS = 2_000; + // Phase-one playback acceptance window. A muted play() that is still pending + // after this only means the decoder is warming; the settle phase owns it. + const PLAY_ACCEPT_TIMEOUT_MS = 1_500; + // Phase-two budget for the committed video to present its first frame. It is + // deliberately far beyond any host deadline: a poster already committed, so + // slow first frames upgrade in place instead of failing the transaction. + const VIDEO_SETTLE_TIMEOUT_MS = 60_000; + const DSH_STRUCTURE_TIMEOUT_MS = CLIENT_APPLY_DEADLINE_MS; + const FRAME_FALLBACK_MS = 120; + const VIDEO_FIRST_FRAME_PROGRESS_SEC = 0.03; + const VIDEO_STABLE_FRAMES = 3; + const VIDEO_STABLE_PROGRESS_SEC = 0.18; + const CROSSFADE_MS = 180; + // A 1486-byte H.264 black frame (faststart). Playing it once on init makes + // Chromium build its media pipeline before the user's first real import. + const WARMUP_VIDEO_DATA_URI = `data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAMQbW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAAD6AAAAH0AAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAjp0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAAAH0AAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAEAAAABAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAAB9AAAAAAABAAAAAAGybWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAABAAAAACABVxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAABXW1pbmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAR1zdGJsAAAAuXN0c2QAAAAAAAAAAQAAAKlhdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAEAAQABIAAAASAAAAAAAAAABFUxhdmM2Mi4yOC4xMDIgbGlieDI2NAAAAAAAAAAAAAAAGP//AAAAL2F2Y0MBQsAN/+EAF2dCwA3ZBCbARAAAAwAEAAADAEA8UKkgAQAFaMuDyyAAAAAQcGFzcAAAAAEAAAABAAAAFGJ0cnQAAAAAAACjgAAAAAAAAAAYc3R0cwAAAAAAAAABAAAAAQAACAAAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAEAAAABAAAAFHN0c3oAAAAAAAACjgAAAAEAAAAUc3RjbwAAAAAAAAABAAADQAAAAGJ1ZHRhAAAAWm1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAALWlsc3QAAAAlqXRvbwAAAB1kYXRhAAAAAQAAAABMYXZmNjIuMTIuMTAyAAAACGZyZWUAAAKWbWRhdAAAAnAGBf//bNxF6b3m2Ui3lizYINkj7u94MjY0IC0gY29yZSAxNjUgcjMyMjMgMDQ4MGNiMCAtIEguMjY0L01QRUctNCBBVkMgY29kZWMgLSBDb3B5bGVmdCAyMDAzLTIwMjUgLSBodHRwOi8vd3d3LnZpZGVvbGFuLm9yZy94MjY0Lmh0bWwgLSBvcHRpb25zOiBjYWJhYz0wIHJlZj0zIGRlYmxvY2s9MTowOjAgYW5hbHl6ZT0weDE6MHgxMTEgbWU9aGV4IHN1Ym1lPTcgcHN5PTEgcHN5X3JkPTEuMDA6MC4wMCBtaXhlZF9yZWY9MSBtZV9yYW5nZT0xNiBjaHJvbWFfbWU9MSB0cmVsbGlzPTEgOHg4ZGN0PTAgY3FtPTAgZGVhZHpvbmU9MjEsMTEgZmFzdF9wc2tpcD0xIGNocm9tYV9xcF9vZmZzZXQ9LTIgdGhyZWFkcz0yIGxvb2thaGVhZF90aHJlYWRzPTEgc2xpY2VkX3RocmVhZHM9MCBucj0wIGRlY2ltYXRlPTEgaW50ZXJsYWNlPTAgYmx1cmF5X2NvbXBhdD0wIGNvbnN0cmFpbmVkX2ludHJhPTAgYmZyYW1lcz0wIHdlaWdodHA9MCBrZXlpbnQ9MjUwIGtleWludF9taW49OCBzY2VuZWN1dD00MCBpbnRyYV9yZWZyZXNoPTAgcmNfbG9va2FoZWFkPTQwIHJjPWNyZiBtYnRyZWU9MSBjcmY9MjMuMCBxY29tcD0wLjYwIHFwbWluPTAgcXBtYXg9NjkgcXBzdGVwPTQgaXBfcmF0aW89MS40MCBhcT0xOjEuMDBAIAAAAAWZYiEDvJigAC+/JycnXXXXXXXXXXXXg==`; let activePayload = null; + let committedPayload = null; + let renderPhase = "idle"; let playbackBlocked = false; + let currentSlot = null; + let applyController = null; + let videoSettleController = null; const systemDarkMedia = globalThis.matchMedia?.("(prefers-color-scheme: dark)") ?? null; + const reducedMotionMedia = + globalThis.matchMedia?.("(prefers-reduced-motion: reduce)") ?? null; let themeSyncQueued = false; const style = document.createElement("style"); @@ -48,22 +80,85 @@ html[data-bc-resolved-tone="light"][data-bc-active="true"]:has(#root [data-phase --dsw-specific-sidebar-fill:rgba(255,255,255,.78); } #beauticode-bg-stage{position:fixed;inset:0;z-index:0;overflow:hidden;pointer-events:none;background:#11141b} -#beauticode-bg-stage::after{content:"";position:absolute;inset:0;z-index:2;background:transparent;pointer-events:none} +#beauticode-bg-stage::after{content:"";position:absolute;inset:0;z-index:3;background:transparent;pointer-events:none} html[data-bc-resolved-tone="light"] #beauticode-bg-stage{background:#f8fafc} html[data-bc-active="true"]:has(#root [data-phase="active"]) #beauticode-bg-stage::after, html[data-bc-active="true"]:has(#root [data-phase="settling"]) #beauticode-bg-stage::after{background:rgba(0,0,0,.42)} html[data-bc-resolved-tone="light"][data-bc-active="true"]:has(#root [data-phase="active"]) #beauticode-bg-stage::after, html[data-bc-resolved-tone="light"][data-bc-active="true"]:has(#root [data-phase="settling"]) #beauticode-bg-stage::after{background:rgba(255,255,255,.22)} html[data-bc-fish="true"] #beauticode-bg-stage::after{background:transparent!important} -#beauticode-bg-stage img,#beauticode-bg-stage video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;pointer-events:none} -#beauticode-bg-stage img{z-index:0} -#beauticode-bg-stage video{z-index:1} +#beauticode-bg-stage .beauticode-media-slot{position:absolute;inset:0;z-index:0;opacity:1;overflow:hidden;pointer-events:none;transition:opacity ${CROSSFADE_MS}ms ease;will-change:opacity} +#beauticode-bg-stage .beauticode-media-slot[data-bc-role="current"]{z-index:1;opacity:1} +#beauticode-bg-stage .beauticode-media-slot[data-bc-role="candidate"]{z-index:2;opacity:1} +#beauticode-bg-stage[data-bc-empty="true"] .beauticode-media-slot[data-bc-role="candidate"]{z-index:1} +#beauticode-bg-stage[data-bc-transitioning="true"] .beauticode-media-slot[data-bc-role="current"]{opacity:0} +#beauticode-bg-stage[data-bc-transitioning="true"] .beauticode-media-slot[data-bc-role="candidate"]{opacity:1} +#beauticode-bg-stage .beauticode-media-slot img,#beauticode-bg-stage .beauticode-media-slot video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;pointer-events:none;transition:opacity 120ms ease} +#beauticode-bg-stage .beauticode-media-slot img{z-index:2;opacity:1} +/* Keep cold candidate video paintable. Chromium may defer decoding media that + is nearly transparent, which deadlocks the first-frame gate. The poster + covers it until data-bc-video-ready is committed. */ +#beauticode-bg-stage .beauticode-media-slot video{z-index:1;opacity:1} +#beauticode-bg-stage .beauticode-media-slot[data-bc-video-ready="true"] img{opacity:0} +#beauticode-bg-stage .beauticode-media-slot[data-bc-video-ready="true"] video{opacity:1} +@media (prefers-reduced-motion:reduce){#beauticode-bg-stage .beauticode-media-slot,#beauticode-bg-stage .beauticode-media-slot img,#beauticode-bg-stage .beauticode-media-slot video{transition:none!important}} html[data-bc-active="true"] #root{position:relative;z-index:1;background:transparent!important} html[data-bc-active="true"] [class*="_fade"]{display:none!important} html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;pointer-events:none!important} `; document.head.append(style); + // Chromium builds its media stack lazily; the first