beauticode-dsh 1.0.25 → 1.0.27

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 CHANGED
@@ -1,8 +1,24 @@
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
+ 「设置 → 背景」默认开启「功能面板不透明」,保证设置、弹层、菜单与消息编辑栏遮住后方内容。关闭后恢复原有透明效果;选择会保存在当前应用的本地偏好中,不受背景阴影、会话阶段或画窗模式影响。
18
+
19
+ ## Web
20
+
21
+ 装好插件并运行 `dsh web` 后,打开「设置」,左侧导航里会多出一项「背景」:在 Windows 上点击图片或视频会打开系统文件选择器,选好后在页面内命名,背景立即应用并进入已保存主题;也可以清除、开关声音、切换或删除主题,进入全屏把浏览器的标签页和地址栏收起来,以及打开「皮肤中心」安装已审核的社区皮肤。本地导入只在 Node 端保存绝对路径,并通过带 Range 支持的回环媒体服务读取原文件,不复制用户选择的图片或整段视频主文件。Windows 强制使用本地引用;原生选择器不可用时会明确报错,不会静默上传或复制媒体。非 Windows 环境保留兼容上传,并明确提示该模式会保存托管副本。网页控制台没有摸鱼;浅色/深色跟 DSH 自己的外观走。下次打开会恢复上次背景。设置 `BEAUTICODE_SKIN_CENTER` 或填写 `skin-center.json` 后,皮肤中心才会显示远程目录。
6
22
 
7
23
  插件也会注册对话工具(`beauticode_*`)和斜杠命令(`/bg`、`/bg-theme`、`/bg-clear`)。不需要托盘;托盘若已在跑则复用它。
8
24
 
@@ -43,6 +59,6 @@ npx @deepseek-ai/dsh web
43
59
  1. 修改 `cordis.patch.yml` 中的插件接入项;优先使用上面的安装命令自动完成接线。
44
60
  2. 确保 DSH 和 beautiCode 使用相同的 `BEAUTICODE_DATA_ROOT`。
45
61
  3. 自己运行 `dsh web`(或带 `--patch`)。
46
- 4. 打开页面后用侧栏「背景」,或输入 `/bg <本机绝对路径>`,或直接跟 AI 说把某个视频/图片设为背景。
62
+ 4. 打开页面后在「设置」里点「背景」,或输入 `/bg <本机绝对路径>`,或直接跟 AI 说把某个视频/图片设为背景。
47
63
 
48
64
  安全边界: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,22 +150,25 @@ 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)}
89
164
  html[data-bc-dim-user="true"][data-bc-active="true"]:has(#root [data-phase="active"]) body,
90
165
  html[data-bc-dim-user="true"][data-bc-active="true"]:has(#root [data-phase="settling"]) body{
91
166
  --dsw-alias-bg-base:rgba(17,20,27,.10);
92
167
  --dsw-alias-bg-layer-1:rgba(26,30,39,.28);
93
168
  --dsw-alias-bg-layer-2:rgba(35,40,51,.32);
94
169
  --dsw-alias-bg-overlay:rgba(17,20,27,.12);
95
- --dsw-specific-sidebar-fill:rgba(23,27,35,.28);
170
+ --bc-surface-mix:36%;
171
+ --bc-content-mix:36%;
96
172
  }
97
173
  html[data-bc-resolved-tone="light"][data-bc-dim-user="true"][data-bc-active="true"]:has(#root [data-phase="active"]) body,
98
174
  html[data-bc-resolved-tone="light"][data-bc-dim-user="true"][data-bc-active="true"]:has(#root [data-phase="settling"]) body{
@@ -100,7 +176,6 @@ html[data-bc-resolved-tone="light"][data-bc-dim-user="true"][data-bc-active="tru
100
176
  --dsw-alias-bg-layer-1:rgba(255,255,255,.28);
101
177
  --dsw-alias-bg-layer-2:rgba(248,250,252,.32);
102
178
  --dsw-alias-bg-overlay:rgba(255,255,255,.14);
103
- --dsw-specific-sidebar-fill:rgba(255,255,255,.28);
104
179
  }
105
180
  html[data-bc-dim-user="true"][data-bc-active="true"] #beauticode-bg-stage::after{background:rgba(0,0,0,var(--bc-dim))!important}
106
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}
@@ -119,6 +194,20 @@ html[data-bc-fish="true"] #beauticode-bg-stage::after{background:transparent!imp
119
194
  #beauticode-bg-stage .beauticode-media-slot video{z-index:1;opacity:1}
120
195
  #beauticode-bg-stage .beauticode-media-slot[data-bc-video-ready="true"] img{opacity:0}
121
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
+ }
122
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}}
123
212
  html[data-bc-active="true"] #root{position:relative;z-index:1;background:transparent!important}
124
213
  html[data-bc-active="true"] [class*="_fade"]{display:none!important}
@@ -195,6 +284,70 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
195
284
  clear: clearUserDim,
196
285
  };
197
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
+
198
351
  // Chromium builds its media stack lazily; the first <video> of a fresh
199
352
  // profile pays decoder/GPU/audio init inside the first import's verify
200
353
  // deadline. One muted playback of the bundled frame pays that cost early.
@@ -373,10 +526,20 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
373
526
  return video instanceof HTMLVideoElement ? video : null;
374
527
  }
375
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.
376
534
  async function postAck(body) {
377
535
  await fetch("/__beauticode/ack", {
378
536
  method: "POST",
379
- 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
+ },
380
543
  body: JSON.stringify({ clientId, ...body }),
381
544
  }).catch(() => {});
382
545
  }
@@ -777,7 +940,7 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
777
940
  // already-set error keeps the settle phase from waiting out its full
778
941
  // budget on media that has already failed.
779
942
  if (video.error) {
780
- return Promise.reject(new Error(describeVideoState(video, "MP4 加载或解码失败")));
943
+ return Promise.reject(new Error(describeVideoState(video, "视频加载或解码失败")));
781
944
  }
782
945
  return new Promise((resolve, reject) => {
783
946
  let settled = false;
@@ -786,7 +949,7 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
786
949
  if (video.readyState >= frameReadyState) done();
787
950
  };
788
951
  const failed = () => {
789
- finish(new Error(describeVideoState(video, "MP4 加载或解码失败")));
952
+ finish(new Error(describeVideoState(video, "视频加载或解码失败")));
790
953
  };
791
954
  const aborted = () => {
792
955
  finish(signal?.aborted ? abortError() : new Error(describeVideoState(video, "视频加载已中止")));
@@ -1640,18 +1803,22 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
1640
1803
  await acknowledgeMode();
1641
1804
  }
1642
1805
 
1643
- const events = new EventSource(
1644
- `/__beauticode/events?clientId=${encodeURIComponent(clientId)}`,
1645
- );
1646
- events.onmessage = (event) => {
1647
- try {
1648
- const payload = JSON.parse(event.data);
1649
- if (payload?.type === "mode") void applyModes(payload).catch(() => {});
1650
- else if (payload?.type === "apply") scheduleBackground(payload);
1651
- } catch {
1652
- /* EventSource will continue with the next valid frame. */
1653
- }
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
+ }
1654
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
+ }
1655
1822
 
1656
1823
  setInterval(() => {
1657
1824
  if (
@@ -1694,4 +1861,50 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
1694
1861
  }).catch(() => {});
1695
1862
  }
1696
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();
1697
1910
  })();