beauticode-dsh 1.0.24 → 1.0.26

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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 beautiCode contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 beautiCode contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.zh-CN.md CHANGED
@@ -1,8 +1,22 @@
1
1
  # DeepSeek Harness 桥接插件
2
2
 
3
- 该 Cordis 插件向 DSH Web 注入 beautiCode 浏览器客户端,并提供本机鉴权接口。支持图片、MP4、播放位置、静音、摸鱼模式以及清除背景。
3
+ 该 Cordis 插件向 DSH Web 和官方 Desktop 注入 beautiCode 浏览器客户端,并提供本机鉴权接口。支持图片、MP4 / MOV、播放位置、静音、摸鱼模式以及清除背景。Web 和 Desktop 使用同一组结构化页面注入;Desktop 的页面请求经原生壳转发,回执由每次 Host 启动生成的页面密钥验证。
4
4
 
5
- 装好插件并运行 `dsh web` 后,侧栏「设置」上方会出现「背景」:在 Windows 上点击图片或视频会打开系统文件选择器,选好后在页面内命名,背景立即应用并进入已保存主题;也可以清除、开关声音、切换或删除主题,以及打开「皮肤中心」安装已审核的社区皮肤。本地导入只在 Node 端保存绝对路径,并通过带 Range 支持的回环媒体服务读取原文件,不复制用户选择的图片或整段视频主文件。Windows 强制使用本地引用;原生选择器不可用时会明确报错,不会静默上传或复制媒体。非 Windows 环境保留兼容上传,并明确提示该模式会保存托管副本。网页控制台没有摸鱼;浅色/深色跟 DSH 自己的外观走。下次打开会恢复上次背景。设置 `BEAUTICODE_SKIN_CENTER` 或填写 `skin-center.json` 后,皮肤中心才会显示远程目录。
5
+ ## 官方 Desktop
6
+
7
+ 先启动 Desktop 一次,让它创建 `desktop` profile,然后从系统托盘明确退出(关闭窗口通常只会隐藏)。可在 Desktop 的插件页面安装 `beauticode-dsh`,或使用 **Desktop 安装的** `dsh` 命令:
8
+
9
+ ```sh
10
+ dsh plugin --profile desktop add beauticode-dsh
11
+ ```
12
+
13
+ 要试当前仓库源码,可先在仓库根目录运行 `npm run plugin:pack`,再将上面命令的包名换成 `file:<仓库绝对路径>/artifacts/dsh-plugin`。
14
+
15
+ 重新打开 Desktop,在「设置」中使用「背景」。Desktop 默认 Host 端口是 `19387`;如果还使用 beautiCode 托盘,将托盘的 `-DshUrl` 指向 `http://127.0.0.1:19387`(若 Desktop profile 改过 `webserver.config.port`,请使用实际端口)。Desktop 插件由 Desktop 自己管理;下方的 `npx beauticode-dsh` 安装器针对 Web profile。
16
+
17
+ ## Web
18
+
19
+ 装好插件并运行 `dsh web` 后,打开「设置」,左侧导航里会多出一项「背景」:在 Windows 上点击图片或视频会打开系统文件选择器,选好后在页面内命名,背景立即应用并进入已保存主题;也可以清除、开关声音、切换或删除主题,进入全屏把浏览器的标签页和地址栏收起来,以及打开「皮肤中心」安装已审核的社区皮肤。本地导入只在 Node 端保存绝对路径,并通过带 Range 支持的回环媒体服务读取原文件,不复制用户选择的图片或整段视频主文件。Windows 强制使用本地引用;原生选择器不可用时会明确报错,不会静默上传或复制媒体。非 Windows 环境保留兼容上传,并明确提示该模式会保存托管副本。网页控制台没有摸鱼;浅色/深色跟 DSH 自己的外观走。下次打开会恢复上次背景。设置 `BEAUTICODE_SKIN_CENTER` 或填写 `skin-center.json` 后,皮肤中心才会显示远程目录。
6
20
 
7
21
  插件也会注册对话工具(`beauticode_*`)和斜杠命令(`/bg`、`/bg-theme`、`/bg-clear`)。不需要托盘;托盘若已在跑则复用它。
8
22
 
@@ -43,6 +57,6 @@ npx @deepseek-ai/dsh web
43
57
  1. 修改 `cordis.patch.yml` 中的插件接入项;优先使用上面的安装命令自动完成接线。
44
58
  2. 确保 DSH 和 beautiCode 使用相同的 `BEAUTICODE_DATA_ROOT`。
45
59
  3. 自己运行 `dsh web`(或带 `--patch`)。
46
- 4. 打开页面后用侧栏「背景」,或输入 `/bg <本机绝对路径>`,或直接跟 AI 说把某个视频/图片设为背景。
60
+ 4. 打开页面后在「设置」里点「背景」,或输入 `/bg <本机绝对路径>`,或直接跟 AI 说把某个视频/图片设为背景。
47
61
 
48
62
  安全边界:DSH Web 必须绑定本机回环地址;控制端点需要随机令牌;媒体 URL 仅允许带令牌的回环 HTTP 地址;浏览器回执只接受同源请求。
package/agent.mjs CHANGED
@@ -275,7 +275,7 @@ export function createBeauticodeActions(dataRootOrOptions) {
275
275
  const inspected = await inspectLocalMedia(input.path);
276
276
  if (!inspected.ok) fail(inspected.error);
277
277
  if (inspected.kind !== "video") {
278
- fail("beauticode_apply_video 只接受 .mp4 文件。");
278
+ fail("beauticode_apply_video 只接受 .mp4 / .mov 文件。");
279
279
  }
280
280
  const persistTheme = input?.persistTheme !== false;
281
281
  const themeName =
package/atmosphere.js CHANGED
@@ -82,14 +82,14 @@ html[data-bc-gallery="true"] body{
82
82
  --dsw-alias-bg-layer-1:rgba(26,30,39,.28);
83
83
  --dsw-alias-bg-layer-2:rgba(35,40,51,.32);
84
84
  --dsw-alias-bg-overlay:rgba(17,20,27,.12);
85
- --dsw-specific-sidebar-fill:rgba(23,27,35,.28);
85
+ --dsw-specific-sidebar-fill:rgba(23,27,35,.36);
86
86
  }
87
87
  html[data-bc-resolved-tone="light"][data-bc-gallery="true"] body{
88
88
  --dsw-alias-bg-base:rgba(248,250,252,.12);
89
89
  --dsw-alias-bg-layer-1:rgba(255,255,255,.28);
90
90
  --dsw-alias-bg-layer-2:rgba(248,250,252,.32);
91
91
  --dsw-alias-bg-overlay:rgba(255,255,255,.14);
92
- --dsw-specific-sidebar-fill:rgba(255,255,255,.28);
92
+ --dsw-specific-sidebar-fill:rgba(255,255,255,.36);
93
93
  }
94
94
  html[data-bc-gallery="true"] #root{position:relative;z-index:1;background:transparent!important}
95
95
  html[data-bc-gallery="true"] [class*="_fade"]{display:none!important}
@@ -0,0 +1,31 @@
1
+ import crypto from "node:crypto";
2
+
3
+ const SCRIPT_PATHS = [
4
+ "/__beauticode/transport.js",
5
+ "/__beauticode/atmosphere.js",
6
+ "/__beauticode/client.js",
7
+ "/__beauticode/console.js",
8
+ "/__beauticode/gallery.js",
9
+ ];
10
+
11
+ /** The desktop shell forwards requests after removing browser Origin headers. */
12
+ export function createBrowserInjection() {
13
+ const desktopKey = crypto.randomBytes(32).toString("hex");
14
+
15
+ return {
16
+ contribute(table) {
17
+ table.push({ kind: "global", name: "__BEAUTICODE_DESKTOP_KEY__", value: desktopKey });
18
+ for (const src of SCRIPT_PATHS) {
19
+ table.push({ kind: "script-src", placement: "body", src });
20
+ }
21
+ },
22
+ isSameOrigin(req) {
23
+ const origin = req.headers.origin;
24
+ if (typeof origin === "string") return origin === `http://${req.headers.host}`;
25
+ if (req.headers["sec-fetch-site"] === "same-origin") return true;
26
+ const supplied = req.headers["x-beauticode-desktop-key"];
27
+ if (typeof supplied !== "string" || !/^[a-f0-9]{64}$/.test(supplied)) return false;
28
+ return crypto.timingSafeEqual(Buffer.from(supplied), Buffer.from(desktopKey));
29
+ },
30
+ };
31
+ }
package/client.js CHANGED
@@ -1,6 +1,9 @@
1
1
  (() => {
2
2
  "use strict";
3
3
  if (window.__beauticodeBridgeLoaded) return;
4
+ if (globalThis.location?.protocol === "dsh-app:" && !globalThis.__beauticodeTransport) {
5
+ throw new Error("beautiCode Desktop transport did not load.");
6
+ }
4
7
  window.__beauticodeBridgeLoaded = true;
5
8
  window.__beauticodeBridgeVersion = 4;
6
9
 
@@ -49,27 +52,97 @@
49
52
  style.dataset.beauticodeBridge = "true";
50
53
  style.textContent = `
51
54
  html[data-bc-active="true"],html[data-bc-active="true"] body{background:transparent!important}
55
+ /* Every surface DSH ships opaque is re-expressed as its own static palette
56
+ color at an alpha tier, so the whole UI is one translucent material instead
57
+ of a few translucent panels around solid blocks. Two knobs, both percentages:
58
+ --bc-surface-mix for chrome (columns, composer card, dropdowns, popovers,
59
+ chips) and --bc-content-mix for transcript content (user bubble, the colored
60
+ highlight blocks, code and diff blocks). A token declared here recomputes
61
+ whenever a phase / dim block below re-aliases its tier, so nothing has to be
62
+ repeated per state. */
52
63
  html[data-bc-active="true"] body{
64
+ --bc-surface-mix:36%;
65
+ --bc-content-mix:36%;
53
66
  --dsw-alias-bg-base:rgba(17,20,27,.10);
54
67
  --dsw-alias-bg-layer-1:rgba(26,30,39,.28);
55
68
  --dsw-alias-bg-layer-2:rgba(35,40,51,.32);
56
69
  --dsw-alias-bg-overlay:rgba(17,20,27,.12);
57
- --dsw-specific-sidebar-fill:rgba(23,27,35,.28);
70
+ --dsw-specific-sidebar-fill:color-mix(in srgb,var(--dsw-static-neutral-bluish-900) var(--bc-surface-mix),transparent);
71
+ --dsw-specific-input-major:var(--dsw-specific-sidebar-fill);
72
+ --dsw-alias-bg-layer-3:color-mix(in srgb,var(--dsw-static-neutral-bluish-800) var(--bc-surface-mix),transparent);
73
+ --dsw-specific-tip:color-mix(in srgb,var(--dsw-static-neutral-bluish-800) var(--bc-surface-mix),transparent);
74
+ --dsw-alias-bg-module-platform:color-mix(in srgb,var(--dsw-static-neutral-bluish-800) var(--bc-surface-mix),transparent);
75
+ --dsw-specific-selector:color-mix(in srgb,var(--dsw-static-neutral-bluish-800) var(--bc-surface-mix),transparent);
76
+ --dsw-alias-interactive-bg-hover-solid:color-mix(in srgb,var(--dsw-static-neutral-bluish-800) var(--bc-surface-mix),transparent);
77
+ --dsw-alias-tooltip-bg:color-mix(in srgb,var(--dsw-static-neutral-bluish-750) var(--bc-surface-mix),transparent);
78
+ --dsw-alias-toast-bg:color-mix(in srgb,var(--dsw-static-neutral-bluish-750) var(--bc-surface-mix),transparent);
79
+ --dsw-alias-button-floating-fill:color-mix(in srgb,var(--dsw-static-neutral-bluish-850) var(--bc-surface-mix),transparent);
80
+ --dsw-specific-bubble:color-mix(in srgb,var(--dsw-static-neutral-bluish-850) var(--bc-content-mix),transparent);
81
+ --dsw-specific-bubble-highlight:color-mix(in srgb,var(--dsw-static-neutral-bluish-750) var(--bc-content-mix),transparent);
82
+ --dsw-alias-state-warn-tertiary:color-mix(in srgb,var(--dsw-static-amber-900) var(--bc-content-mix),transparent);
83
+ --dsw-alias-state-success-tertiary:color-mix(in srgb,var(--dsw-static-green-900) var(--bc-content-mix),transparent);
84
+ --dsw-alias-state-business-tertiary:color-mix(in srgb,var(--dsw-static-deepseek-800) var(--bc-content-mix),transparent);
85
+ --dsw-alias-markdown-code-block:color-mix(in srgb,var(--dsw-static-neutral-bluish-900) var(--bc-content-mix),transparent);
86
+ --dsw-alias-markdown-code-block-banner:color-mix(in srgb,var(--dsw-static-neutral-bluish-850) var(--bc-content-mix),transparent);
87
+ --dsw-alias-markdown-inline-code:color-mix(in srgb,var(--dsw-static-neutral-800) var(--bc-content-mix),transparent);
88
+ --dsw-alias-markdown-tag:color-mix(in srgb,var(--dsw-static-neutral-bluish-850) var(--bc-content-mix),transparent);
89
+ --dsw-alias-markdown-citation:color-mix(in srgb,var(--dsw-static-neutral-bluish-800) var(--bc-content-mix),transparent);
90
+ --dsw-alias-markdown-placeholder:color-mix(in srgb,var(--dsw-static-neutral-bluish-850) var(--bc-content-mix),transparent);
91
+ --dsw-alias-markdown-code-segment-selected:color-mix(in srgb,var(--dsw-static-neutral-bluish-800) var(--bc-content-mix),transparent);
92
+ --dsw-alias-markdown-code-segment-unselected:color-mix(in srgb,var(--dsw-static-neutral-bluish-900) var(--bc-content-mix),transparent);
58
93
  }
59
94
  html[data-bc-resolved-tone="light"][data-bc-active="true"] body{
60
95
  --dsw-alias-bg-base:rgba(248,250,252,.12);
61
96
  --dsw-alias-bg-layer-1:rgba(255,255,255,.28);
62
97
  --dsw-alias-bg-layer-2:rgba(248,250,252,.32);
63
98
  --dsw-alias-bg-overlay:rgba(255,255,255,.14);
64
- --dsw-specific-sidebar-fill:rgba(255,255,255,.28);
99
+ --dsw-specific-sidebar-fill:color-mix(in srgb,var(--dsw-static-neutral-bluish-50) var(--bc-surface-mix),transparent);
100
+ --dsw-alias-bg-layer-3:color-mix(in srgb,var(--dsw-static-neutral-bluish-00) var(--bc-surface-mix),transparent);
101
+ --dsw-specific-tip:color-mix(in srgb,var(--dsw-static-neutral-bluish-60) var(--bc-surface-mix),transparent);
102
+ --dsw-alias-bg-module-platform:color-mix(in srgb,var(--dsw-static-neutral-bluish-60) var(--bc-surface-mix),transparent);
103
+ --dsw-specific-selector:color-mix(in srgb,var(--dsw-static-neutral-bluish-60) var(--bc-surface-mix),transparent);
104
+ --dsw-alias-interactive-bg-hover-solid:color-mix(in srgb,var(--dsw-static-neutral-bluish-75) var(--bc-surface-mix),transparent);
105
+ --dsw-alias-tooltip-bg:color-mix(in srgb,var(--dsw-static-neutral-bluish-850) var(--bc-surface-mix),transparent);
106
+ --dsw-alias-toast-bg:color-mix(in srgb,var(--dsw-static-neutral-bluish-800) var(--bc-surface-mix),transparent);
107
+ --dsw-alias-button-floating-fill:color-mix(in srgb,var(--dsw-static-neutral-bluish-00) var(--bc-surface-mix),transparent);
108
+ --dsw-specific-bubble:color-mix(in srgb,var(--dsw-static-deepseek-50) var(--bc-content-mix),transparent);
109
+ --dsw-specific-bubble-highlight:color-mix(in srgb,var(--dsw-static-deepseek-200) var(--bc-content-mix),transparent);
110
+ --dsw-alias-state-warn-tertiary:color-mix(in srgb,var(--dsw-static-amber-100) var(--bc-content-mix),transparent);
111
+ --dsw-alias-state-success-tertiary:color-mix(in srgb,var(--dsw-static-green-100) var(--bc-content-mix),transparent);
112
+ --dsw-alias-state-business-tertiary:color-mix(in srgb,var(--dsw-static-deepseek-100) var(--bc-content-mix),transparent);
113
+ --dsw-alias-markdown-code-block:color-mix(in srgb,var(--dsw-static-neutral-bluish-50) var(--bc-content-mix),transparent);
114
+ --dsw-alias-markdown-code-block-banner:color-mix(in srgb,var(--dsw-static-neutral-bluish-50) var(--bc-content-mix),transparent);
115
+ --dsw-alias-markdown-inline-code:color-mix(in srgb,var(--dsw-static-neutral-50) var(--bc-content-mix),transparent);
116
+ --dsw-alias-markdown-tag:color-mix(in srgb,var(--dsw-static-neutral-bluish-75) var(--bc-content-mix),transparent);
117
+ --dsw-alias-markdown-citation:color-mix(in srgb,var(--dsw-static-neutral-bluish-100) var(--bc-content-mix),transparent);
118
+ --dsw-alias-markdown-placeholder:color-mix(in srgb,var(--dsw-static-neutral-bluish-60) var(--bc-content-mix),transparent);
119
+ --dsw-alias-markdown-code-segment-selected:color-mix(in srgb,var(--dsw-static-neutral-bluish-00) var(--bc-content-mix),transparent);
120
+ --dsw-alias-markdown-code-segment-unselected:color-mix(in srgb,var(--dsw-static-neutral-bluish-75) var(--bc-content-mix),transparent);
121
+ }
122
+ /* The cards listed under 本轮文件改动 (the deliverables grid) do not read an
123
+ alias token: the module root derives its own --deliverable-fill /
124
+ --deliverable-hover pair from the static palette, so no body-level override
125
+ reaches them. Declaring the pair on the card element itself wins over the
126
+ value it would otherwise inherit from that root. */
127
+ html[data-bc-active="true"] [data-presented-file]{
128
+ --deliverable-fill:color-mix(in srgb,var(--dsw-static-neutral-850) var(--bc-content-mix),transparent);
129
+ --deliverable-hover:color-mix(in srgb,var(--dsw-static-neutral-800) var(--bc-content-mix),transparent);
130
+ }
131
+ html[data-bc-resolved-tone="light"][data-bc-active="true"] [data-presented-file]{
132
+ --deliverable-fill:color-mix(in srgb,var(--dsw-static-neutral-50) var(--bc-content-mix),transparent);
133
+ --deliverable-hover:color-mix(in srgb,var(--dsw-static-neutral-100) var(--bc-content-mix),transparent);
65
134
  }
135
+ /* DSH renders the dropdown menus through --dsw-specific-menu, which it defines
136
+ as var(--dsw-alias-bg-layer-3), so the layer override above already carries
137
+ them onto the same tier. */
66
138
  html[data-bc-active="true"]:has(#root [data-phase="active"]) body,
67
139
  html[data-bc-active="true"]:has(#root [data-phase="settling"]) body{
140
+ --bc-surface-mix:86%;
141
+ --bc-content-mix:72%;
68
142
  --dsw-alias-bg-base:rgba(17,20,27,.42);
69
143
  --dsw-alias-bg-layer-1:rgba(26,30,39,.72);
70
144
  --dsw-alias-bg-layer-2:rgba(35,40,51,.80);
71
145
  --dsw-alias-bg-overlay:rgba(17,20,27,.86);
72
- --dsw-specific-sidebar-fill:rgba(23,27,35,.78);
73
146
  }
74
147
  html[data-bc-resolved-tone="light"][data-bc-active="true"]:has(#root [data-phase="active"]) body,
75
148
  html[data-bc-resolved-tone="light"][data-bc-active="true"]:has(#root [data-phase="settling"]) body{
@@ -77,15 +150,35 @@ html[data-bc-resolved-tone="light"][data-bc-active="true"]:has(#root [data-phase
77
150
  --dsw-alias-bg-layer-1:rgba(255,255,255,.74);
78
151
  --dsw-alias-bg-layer-2:rgba(248,250,252,.82);
79
152
  --dsw-alias-bg-overlay:rgba(255,255,255,.86);
80
- --dsw-specific-sidebar-fill:rgba(255,255,255,.78);
81
153
  }
82
154
  #beauticode-bg-stage{position:fixed;inset:0;z-index:0;overflow:hidden;pointer-events:none;background:#11141b}
83
155
  #beauticode-bg-stage::after{content:"";position:absolute;inset:0;z-index:3;background:transparent;pointer-events:none}
84
156
  html[data-bc-resolved-tone="light"] #beauticode-bg-stage{background:#f8fafc}
157
+ /* The background shadow defaults to zero, so the wallpaper keeps its own
158
+ brightness until someone moves the 背景阴影 slider — that writes --bc-dim and
159
+ takes over through the data-bc-dim-user rules below. */
85
160
  html[data-bc-active="true"]:has(#root [data-phase="active"]) #beauticode-bg-stage::after,
86
- html[data-bc-active="true"]:has(#root [data-phase="settling"]) #beauticode-bg-stage::after{background:rgba(0,0,0,.42)}
161
+ html[data-bc-active="true"]:has(#root [data-phase="settling"]) #beauticode-bg-stage::after{background:rgba(0,0,0,0)}
87
162
  html[data-bc-resolved-tone="light"][data-bc-active="true"]:has(#root [data-phase="active"]) #beauticode-bg-stage::after,
88
- 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)}
163
+ html[data-bc-resolved-tone="light"][data-bc-active="true"]:has(#root [data-phase="settling"]) #beauticode-bg-stage::after{background:rgba(255,255,255,0)}
164
+ html[data-bc-dim-user="true"][data-bc-active="true"]:has(#root [data-phase="active"]) body,
165
+ html[data-bc-dim-user="true"][data-bc-active="true"]:has(#root [data-phase="settling"]) body{
166
+ --dsw-alias-bg-base:rgba(17,20,27,.10);
167
+ --dsw-alias-bg-layer-1:rgba(26,30,39,.28);
168
+ --dsw-alias-bg-layer-2:rgba(35,40,51,.32);
169
+ --dsw-alias-bg-overlay:rgba(17,20,27,.12);
170
+ --bc-surface-mix:36%;
171
+ --bc-content-mix:36%;
172
+ }
173
+ html[data-bc-resolved-tone="light"][data-bc-dim-user="true"][data-bc-active="true"]:has(#root [data-phase="active"]) body,
174
+ html[data-bc-resolved-tone="light"][data-bc-dim-user="true"][data-bc-active="true"]:has(#root [data-phase="settling"]) body{
175
+ --dsw-alias-bg-base:rgba(248,250,252,.12);
176
+ --dsw-alias-bg-layer-1:rgba(255,255,255,.28);
177
+ --dsw-alias-bg-layer-2:rgba(248,250,252,.32);
178
+ --dsw-alias-bg-overlay:rgba(255,255,255,.14);
179
+ }
180
+ html[data-bc-dim-user="true"][data-bc-active="true"] #beauticode-bg-stage::after{background:rgba(0,0,0,var(--bc-dim))!important}
181
+ html[data-bc-resolved-tone="light"][data-bc-dim-user="true"][data-bc-active="true"] #beauticode-bg-stage::after{background:rgba(255,255,255,var(--bc-dim))!important}
89
182
  html[data-bc-fish="true"] #beauticode-bg-stage::after{background:transparent!important}
90
183
  #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}
91
184
  #beauticode-bg-stage .beauticode-media-slot[data-bc-role="current"]{z-index:1;opacity:1}
@@ -101,6 +194,20 @@ html[data-bc-fish="true"] #beauticode-bg-stage::after{background:transparent!imp
101
194
  #beauticode-bg-stage .beauticode-media-slot video{z-index:1;opacity:1}
102
195
  #beauticode-bg-stage .beauticode-media-slot[data-bc-video-ready="true"] img{opacity:0}
103
196
  #beauticode-bg-stage .beauticode-media-slot[data-bc-video-ready="true"] video{opacity:1}
197
+ /* Background blur (磨砂). Applied to the background media element itself with a
198
+ plain CSS filter, never to the stage or the app:
199
+
200
+ - no backdrop-filter -> no containing block, so settings dialog and every
201
+ fixed/absolute control keeps its position
202
+ - no extra DOM node -> nothing to stack, nothing to isolate
203
+
204
+ The media is NOT scaled: an earlier revision grew it slightly to hide the soft
205
+ border a blur leaves at the viewport edge, but a growing picture is a worse
206
+ artefact than the soft edge, so the picture keeps its framing. */
207
+ html[data-bc-bg-blur="true"] #beauticode-bg-stage .beauticode-media-slot img,
208
+ html[data-bc-bg-blur="true"] #beauticode-bg-stage .beauticode-media-slot video{
209
+ filter:blur(var(--bc-bg-blur,0px));
210
+ }
104
211
  @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}}
105
212
  html[data-bc-active="true"] #root{position:relative;z-index:1;background:transparent!important}
106
213
  html[data-bc-active="true"] [class*="_fade"]{display:none!important}
@@ -108,6 +215,139 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
108
215
  `;
109
216
  document.head.append(style);
110
217
 
218
+ const DIM_STORAGE_KEY = "beauticode-dim";
219
+ let userDim = null;
220
+
221
+ function clampDim(value) {
222
+ const n = Number(value);
223
+ if (!Number.isFinite(n) || n < 0 || n > 1) return null;
224
+ return n;
225
+ }
226
+
227
+ function readStoredDim() {
228
+ try {
229
+ const raw = globalThis.localStorage?.getItem(DIM_STORAGE_KEY);
230
+ if (raw == null || raw === "") return null;
231
+ return clampDim(raw);
232
+ } catch {
233
+ return null;
234
+ }
235
+ }
236
+
237
+ function writeStoredDim(value) {
238
+ try {
239
+ if (value == null) globalThis.localStorage?.removeItem(DIM_STORAGE_KEY);
240
+ else globalThis.localStorage?.setItem(DIM_STORAGE_KEY, String(value));
241
+ } catch {
242
+ /* private mode / quota */
243
+ }
244
+ }
245
+
246
+ function applyUserDim(value) {
247
+ const root = document.documentElement;
248
+ if (value == null) {
249
+ delete root.dataset.bcDimUser;
250
+ root.removeAttribute("data-bc-dim-user");
251
+ root.style.removeProperty?.("--bc-dim");
252
+ return null;
253
+ }
254
+ root.dataset.bcDimUser = "true";
255
+ root.style.setProperty?.("--bc-dim", String(value));
256
+ return value;
257
+ }
258
+
259
+ function getUserDim() {
260
+ return userDim;
261
+ }
262
+
263
+ function setUserDim(value) {
264
+ const next = clampDim(value);
265
+ if (next == null) return userDim;
266
+ userDim = next;
267
+ writeStoredDim(next);
268
+ applyUserDim(next);
269
+ return next;
270
+ }
271
+
272
+ function clearUserDim() {
273
+ userDim = null;
274
+ writeStoredDim(null);
275
+ applyUserDim(null);
276
+ return null;
277
+ }
278
+
279
+ userDim = readStoredDim();
280
+ if (userDim != null) applyUserDim(userDim);
281
+ globalThis.BeauticodeBackgroundDim = {
282
+ get: getUserDim,
283
+ set: setUserDim,
284
+ clear: clearUserDim,
285
+ };
286
+
287
+
288
+ // ---------------------------------------------------------------------------
289
+ // Background blur (磨砂程度, 0-100%).
290
+ //
291
+ // Publishes a percentage and writes two things on <html>: the --bc-bg-blur
292
+ // length the stylesheet applies to the media, and a data attribute so the rule
293
+ // only matches while the value is above zero. 0% therefore means "no rule
294
+ // matches at all", which is exactly the previous appearance.
295
+ // ---------------------------------------------------------------------------
296
+ const BG_BLUR_KEY = "beauticode-bg-blur";
297
+ // 100% on the slider maps to this radius. Chosen from the rendered result:
298
+ // the previous 30px cap was already heavy at 30%, so 9px is the new ceiling.
299
+ const BG_BLUR_MAX_PX = 9;
300
+ let bgBlurPercent = 0;
301
+
302
+ function clampBlurPercent(value) {
303
+ if (value == null || value === "") return null;
304
+ const n = Number(value);
305
+ if (!Number.isFinite(n)) return null;
306
+ return Math.min(100, Math.max(0, Math.round(n)));
307
+ }
308
+
309
+ function applyBgBlur() {
310
+ const root = document.documentElement;
311
+ if (!root?.dataset || !root.style) return;
312
+ const setVar = typeof root.style.setProperty === "function"
313
+ ? root.style.setProperty.bind(root.style)
314
+ : null;
315
+ const px = (bgBlurPercent / 100) * BG_BLUR_MAX_PX;
316
+ setVar?.("--bc-bg-blur", `${px.toFixed(2)}px`);
317
+ if (bgBlurPercent > 0) root.dataset.bcBgBlur = "true";
318
+ else if (root.dataset) delete root.dataset.bcBgBlur;
319
+ }
320
+
321
+ function getBgBlur() {
322
+ return bgBlurPercent;
323
+ }
324
+
325
+ function setBgBlur(value) {
326
+ const percent = clampBlurPercent(value);
327
+ if (percent == null) return bgBlurPercent;
328
+ bgBlurPercent = percent;
329
+ try {
330
+ globalThis.localStorage?.setItem(BG_BLUR_KEY, String(percent));
331
+ } catch {
332
+ /* private mode / quota */
333
+ }
334
+ applyBgBlur();
335
+ return percent;
336
+ }
337
+
338
+ try {
339
+ const stored = clampBlurPercent(globalThis.localStorage?.getItem(BG_BLUR_KEY));
340
+ if (stored != null) bgBlurPercent = stored;
341
+ } catch {
342
+ /* private mode */
343
+ }
344
+ globalThis.BeauticodeBackgroundBlur = { get: getBgBlur, set: setBgBlur };
345
+ try {
346
+ applyBgBlur();
347
+ } catch {
348
+ /* a cosmetic extra must never take the background down with it */
349
+ }
350
+
111
351
  // Chromium builds its media stack lazily; the first <video> of a fresh
112
352
  // profile pays decoder/GPU/audio init inside the first import's verify
113
353
  // deadline. One muted playback of the bundled frame pays that cost early.
@@ -286,10 +526,20 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
286
526
  return video instanceof HTMLVideoElement ? video : null;
287
527
  }
288
528
 
529
+ // Same-origin receipt only — NOT an authenticated control API. clientId is
530
+ // a client-generated correlation id (see top of file), not a credential.
531
+ // The server enforces same-origin and binds this to a live SSE session for
532
+ // the clientId; it intentionally does not use authorized()/tokenFile,
533
+ // which is reserved for /apply, /mode, /status.
289
534
  async function postAck(body) {
290
535
  await fetch("/__beauticode/ack", {
291
536
  method: "POST",
292
- headers: { "content-type": "application/json" },
537
+ mode: "same-origin",
538
+ credentials: "same-origin",
539
+ headers: {
540
+ "content-type": "application/json",
541
+ ...(globalThis.__beauticodeTransport?.headers() ?? {}),
542
+ },
293
543
  body: JSON.stringify({ clientId, ...body }),
294
544
  }).catch(() => {});
295
545
  }
@@ -690,7 +940,7 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
690
940
  // already-set error keeps the settle phase from waiting out its full
691
941
  // budget on media that has already failed.
692
942
  if (video.error) {
693
- return Promise.reject(new Error(describeVideoState(video, "MP4 加载或解码失败")));
943
+ return Promise.reject(new Error(describeVideoState(video, "视频加载或解码失败")));
694
944
  }
695
945
  return new Promise((resolve, reject) => {
696
946
  let settled = false;
@@ -699,7 +949,7 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
699
949
  if (video.readyState >= frameReadyState) done();
700
950
  };
701
951
  const failed = () => {
702
- finish(new Error(describeVideoState(video, "MP4 加载或解码失败")));
952
+ finish(new Error(describeVideoState(video, "视频加载或解码失败")));
703
953
  };
704
954
  const aborted = () => {
705
955
  finish(signal?.aborted ? abortError() : new Error(describeVideoState(video, "视频加载已中止")));
@@ -1553,18 +1803,22 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
1553
1803
  await acknowledgeMode();
1554
1804
  }
1555
1805
 
1556
- const events = new EventSource(
1557
- `/__beauticode/events?clientId=${encodeURIComponent(clientId)}`,
1558
- );
1559
- events.onmessage = (event) => {
1560
- try {
1561
- const payload = JSON.parse(event.data);
1562
- if (payload?.type === "mode") void applyModes(payload).catch(() => {});
1563
- else if (payload?.type === "apply") scheduleBackground(payload);
1564
- } catch {
1565
- /* EventSource will continue with the next valid frame. */
1566
- }
1806
+ const onEvent = (event) => {
1807
+ try {
1808
+ const payload = JSON.parse(event.data);
1809
+ if (payload?.type === "mode") void applyModes(payload).catch(() => {});
1810
+ else if (payload?.type === "apply") scheduleBackground(payload);
1811
+ } catch {
1812
+ /* The stream continues with the next valid frame. */
1813
+ }
1567
1814
  };
1815
+ const eventsUrl = `/__beauticode/events?clientId=${encodeURIComponent(clientId)}`;
1816
+ if (globalThis.__beauticodeTransport) {
1817
+ globalThis.__beauticodeTransport.events(eventsUrl, onEvent);
1818
+ } else {
1819
+ const events = new EventSource(eventsUrl);
1820
+ events.onmessage = onEvent;
1821
+ }
1568
1822
 
1569
1823
  setInterval(() => {
1570
1824
  if (
@@ -1607,4 +1861,50 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
1607
1861
  }).catch(() => {});
1608
1862
  }
1609
1863
  }, 1_000);
1864
+
1865
+ // Fullscreen by default. A browser only grants fullscreen from inside a user
1866
+ // gesture, so a page cannot open in it; the closest honest reading of
1867
+ // "starts fullscreen" is the first click after load. Typing in the composer
1868
+ // must not count: DSH's main action is a keydown. Escape or the settings row
1869
+ // still exits. Stay armed until the request actually succeeds — a rejected
1870
+ // first gesture used to spend the listener and leave the page windowed.
1871
+ function armFullscreenDefault() {
1872
+ const root = document.documentElement;
1873
+ if (typeof root?.requestFullscreen !== "function" &&
1874
+ typeof root?.webkitRequestFullscreen !== "function") {
1875
+ return;
1876
+ }
1877
+ let spent = false;
1878
+ const enter = () => {
1879
+ if (spent) return;
1880
+ if (document.fullscreenElement || document.webkitFullscreenElement) {
1881
+ spent = true;
1882
+ document.removeEventListener?.("pointerdown", enter, true);
1883
+ return;
1884
+ }
1885
+ for (const name of ["requestFullscreen", "webkitRequestFullscreen"]) {
1886
+ const request = root?.[name];
1887
+ if (typeof request !== "function") continue;
1888
+ let started;
1889
+ try {
1890
+ started = request.call(root, { navigationUI: "hide" });
1891
+ } catch {
1892
+ continue;
1893
+ }
1894
+ Promise.resolve(started).then(
1895
+ () => {
1896
+ spent = true;
1897
+ document.removeEventListener?.("pointerdown", enter, true);
1898
+ },
1899
+ () => {
1900
+ /* gesture rejected — stay armed for the next click */
1901
+ },
1902
+ );
1903
+ return;
1904
+ }
1905
+ };
1906
+ document.addEventListener?.("pointerdown", enter, { capture: true });
1907
+ }
1908
+
1909
+ armFullscreenDefault();
1610
1910
  })();