dsh-skin-aurora
Verifieddsh-skin-aurora · v1.0.1 · MIT · Web UI
Aurora skin for DeepSeek Harness: token-layer theme (4 palettes + custom colours), an uploadable global wallpaper with layer-aware translucency, a contrast dial for legible type over the wallpaper, its own settings page, and a live sidebar dashboard (bala
Install
dsh plugin add dsh-skin-aurora Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
dsh-skin-aurora
DeepSeek Harness 皮肤插件(installed client bundle)。它做四件事:通过 theme 服务声明
--dsw-* 值(token 层);可选地画一张全局背景图;注册自己的设置页(设置 →「皮肤」,
另外在 扩展管理 → 插件 那一行还有一个「配置」控件);侧栏底部一个实时仪表盘(余额 /
token 用量 / 上下文占用)。不改布局、不替换组件、不改 app 的 DOM 结构。
安装
前提:@deepseek-ai/dsh 在 >=0.2.0-rc.2 <0.3.0。
在 DSH 里对助手说一句(推荐,三件事一起做:装依赖 / 登记 bundles / 写 patch):
用 plugin_manager 的 install_bundle 装
dsh-skin-aurora@latest
然后完整重启 DSH。装完 设置 → 皮肤 出现「皮肤」页,侧栏出现仪表盘。
等价的手工路线(只做 pnpm 安装,需自己补 dsh.profile.bundles 与 cordis.patch.yml):
dsh plugin --profile desktop add dsh-skin-aurora
卸载:先用 设置 → 皮肤 → 「恢复默认」清掉配色,再 plugin_manager 卸载该 bundle,最后重启。
只停用也可以(token 层是 ctx.effect,停用即撤销;重新启用后要刷新页面)。
Four dials shape the colour result: 图片浓淡 (wallpaper strength), 界面透明 (how much wallpaper shows through the canvas), 侧栏校正 (sidebar/canvas parity) and 对比度 (type and panel legibility), plus a three-way 侧栏仪表盘 switch (完整 / 紧凑 / 隐藏).
- Host half (
index.js): emptyapply(), exists only so the bundle has a Loader row. - Client half (
lib/client.js): installs one token layer under the sourcedsh-skin-aurora, at most one self-created wallpaper node, the settings page, and the sidebar dashboard. Tearing the effect down removes all of them, stops the dashboard timer and drops every subscription it took.
配色方案
Four presets, each a full { light, dark } palette. Default is 极光.
| id | 名称 | 主色 light / dark | 底色 light / dark |
|---|---|---|---|
aurora |
极光 · 靛蓝 | #4f46e5 / #8b95ff |
#eef1ff / #0b0e22 |
celadon |
青瓷 · 松绿 | #0d8f7a / #46d3b4 |
#eef7f4 / #071713 |
sand |
暖砂 · 琥珀 | #b4621a / #e8a44a |
#fbf4e8 / #17120b |
ink |
墨玉 · 青灰 | #0e7c86 / #3fd0e0 |
#eef0f3 / #0a0c0f |
Every preset declares these 20 tokens (all { light, dark } pairs). The first 11 are picked by
hand per palette (SPEC_KEYS: base / layer1 / layer2 / overlay / b1 / b2 / brand / label1 /
label2 / success / side); the rest are derived by modeRow() so that changing the brand or
the background colour carries the whole set along instead of leaving half a palette behind:
| group | tokens |
|---|---|
| 面 | --dsw-alias-bg-base、-bg-layer-1、-bg-layer-2、-bg-layer-3、-bg-overlay、--dsw-specific-sidebar-fill |
| 弹层(v0.7.0,只在有背景图时写) | -bg-mask-1(遮罩)、-settings-card-fill(设置页卡片) |
| 按钮 / 交互 | -button-elevated-fill(不再是纯白 #fff)、-interactive-bg-hover、-interactive-bg-active(主色淡染) |
| 描边 | -border-l1、-border-l2、-border-l3(跟主色走,侧栏右界线即此) |
| 文字 / 主色 | -brand-primary、-label-primary、-label-secondary、-label-tertiary、-state-success-primary |
| 阴影 | --dsw-shadow-lv1、-lv2、-lv3(全部由主色染色) |
The chosen values are deliberately far from the native defaults (native light is bg #fff,
border #0000000a, label #0f1115, button-elevated-fill #fff, interactive-bg-hover a
neutral #2631480f) so the change is visible at a glance. --dsw-alias-interactive-bg-hover is
referenced 158 times across 38 client bundles (sidebar panelRow:hover / panelActive,
session rows, settings triggers) — it is the single switch for "interactive state" everywhere,
which is why hover now tints with the accent colour instead of a grey wash.
--dsw-alias-bg-layer-3 is referenced 13 times and only by floating menus (agent-preset,
plugin-manager, document preview), so it deliberately stays opaque: text must never sit on
top of the wallpaper.
It is palette-only: no --dsw-radius-*, so the global corner radius stays native. It also
does not change code-block syntax highlighting — this runtime has no syntax* token; that is
shiki plus the --dsw-font-markdown-code* family.
配置页
入口一:设置里的独立页(推荐)
The client half also registers into the settings.section slot, which is how built-in
pages (General / Plugins …) and plugin pages (宠物 / bili设置 …) get their own entry in
the settings navigation:
ctx.slots.inject('settings.section', () => ctx.slots.register({
name: 'settings.section',
id: 'skin-aurora',
order: 47,
label: () => '皮肤',
}, SkinSettingsPage));
Registration options are exactly id (required), order, label (string | () => string);
the component receives { close }. An id the shell does not style gets the default gear icon.
Order 47 seats it between 归档会话 (45) and 宠物 (50).
入口二:插件行上的「配置」
The client half registers into the plugins.row.config slot with the key
dsh-skin-aurora#dsh-skin-aurora:
- 四个方案:点一下即时生效;
- 主色 / 背景色:
<input type=color>加十六进制输入框,点「保存」生效。自定义主色的暗色值 由浅色向#fff混合 35% 得出,自定义背景色会一起带动底色、层级、侧栏与边框; - 恢复默认:回到极光方案(同时清掉背景图);
- 「实时检查」一行会读出本层写入的
--dsw-alias-bg-base与--dsw-specific-sidebar-fill、两者在body上的实测值、背景图状态、两侧的合成不透明度(主区 X% / 侧栏 Y%,齐平时标「齐平」)以及 当前对比度档位;侧栏 token 若被改成实心色会直接标⚠ 被覆盖, 用来判断配色层有没有真的写进 CSS、以及侧栏和主区到底还差多少。
Note: this control shows up on the built-in plugin inventory page (设置 → 插件), not on third-party market lists such as dsh-market's 已安装 tab.
侧栏仪表盘(v0.8.0;v0.9.0 增补:本机/平台分栏、赠送额、一键重载)
侧栏底部常驻一张读数卡(v0.9.0 起 四行),实时显示 余额 / 本机用量 / 上下文占用 / 平台账单。 它只读产品已经算好的数据,自己不做第二套估算:
| 读数 | 来源 | 与产品哪一处同源 |
|---|---|---|
| 余额(含「+赠送」次行) | ctx.remote.account.getState() / getBalance(client) / watch(signal),金额取 value[].balance,赠送取 bonusWallets |
设置 → 账号 |
| 本机用量 + 缓存命中 | ctx.sessions.list 快照行上的 projectionValues.tokenUsage |
聊天页顶部「用量 40.2M tok · 缓存命中 98%」 |
| 上下文占用 | 同一行的 projectionValues.contextPressure |
输入框右侧「5%」 |
| 平台账单(30 天) | 没有:本机没有这个接口 | platform.deepseek.com 的「用量信息」页 |
计费口径与产品逐字对齐:prompt 侧 = uncachedInputTokens + cacheReadTokens + cacheWriteTokens
(三个互不重叠的桶),"用量"再加 outputTokens;缓存命中率的分母只取 prompt 侧,不含
output,且没有计费桶时显示「—」而不是 0%。
三档显示(设置 → 皮肤 → 侧栏仪表盘):完整(四行)/ 紧凑(一行「余额 · 5%」)/
隐藏(不渲染,且一条余额请求都不发)。卡片右上角的 «/» 就地折叠,不用进设置页。
- 注入点:
sidebar.footer.action(list 槽,务必带id)常驻侧栏底部;侧栏收起时 Windows 桌面端整个footArea是display:none,所以另注册sidebar.toggle.badge(single 槽) 在「展开」按钮里放一个小圆点。不注册sidebar本体——那是整列替换,就是 codex-ui 那个坑。 - 余额口径(v0.9.0):
¥12,345.67—— 两位小数恒显、千分位手写、截断到分不四舍五入、 货币符号取currency(CNY→¥、USD→$、未知币种沿用¥);<0.01显示<¥0.01, 负数-¥3.00。bonusWallets里有同币种且 > 0 的余额时,主行下面多一行「+赠送 ¥x.xx」 (不同币种不相加;全是 0 就不显示,避免「+赠送 ¥0.00」这种噪声);取不到一律—。 - 本机 / 平台分栏(v0.9.0):第三行之前的用量行标签是 本机用量(悬停写明「当前会话投影,
不是账单」),最后一行的标签是 平台账单,值固定写 本机无接口(悬停写明「30 天消费/请求数/
tokens 只有平台算得出来,需在 platform.deepseek.com 查看」)。这一格永远不会出现数字——
本地拿不到的数据宁可说"没有",也不拿会话投影去冒充账单。平台窗口是主进程用
new URL(page === "usage" ? "/usage" : "/top_up", account.origin)开的独立 Electron 窗口, 插件层无法可靠触发,所以这里也不做跳转按钮。 - 降级:未登录显示「未登录」;登录态读不到 / 余额请求失败 / 金额解析不了,一律显示「—」。 余额层与 token 层解耦,余额任何一步失败都不影响 token 读数,也不抛错。
- 刷新:会话投影是订阅式的(近实时,不轮询);账号走
ctx.remote.$stream流;另有一个 60s 兜底轮询,只在流没起来或断了时才有实际作用;一个回合结束(running: true → false)时 补一次余额。卸载时clearInterval+ 退订 +stream.dispose()+ 清空监听集合,全部挂在 同一个ctx.effect的 disposer 上。 - 颜色:卡片、进度条、圆点全部走
var(--dsw-alias-*),不硬编码 hex,所以在任何背图上都跟着 对比度旋钮一起变。 - 点一下就进设置:先走
ctx.shortcuts.invoke('settings.open', …)打开设置弹层;弹层已经开着 时只做 DOM 导航(点中左侧那格「皮肤」),因为产品把"当前选中哪一页"存在 settings-general 自己的 store 里,第三方拿不到(见 REDME §15)。导航失败也只是停在设置首页,不会把弹层关掉。
一键重载(v0.9.0)
改完配色/背图/仪表盘档位后不需要退出应用:设置 → 皮肤最下面有「重新加载界面」按钮,
仪表盘右上角(折叠键 « 左边)还有一个小 ↻,两者走同一条通道。
- 做的动作只有一个:
location.reload()—— 页面级重载,等效于重跑一遍 client half; 所有设置都存在localStorage(dsh-skin-aurora:v1),重载后原样读回。 - 不是重启 DSH 进程:Host Service 目录里没有 relaunch / restart / lifecycle / app.quit 任何一个能力(全库 grep 0 命中),插件层做不到进程级重启,所以文案里也不这么写。
- 3 秒防连点:按下就把按钮置
disabled并记时间戳,3 秒内的后续点击直接返回(一次点击只发 一次location.reload());冷却结束后自动恢复可用,不是永久锁死。 - 静态验收 AC-14 钉死这条纪律:源码里
location.reload()恰好一处,且location.href/assign/replace/...与window.location均为 0。
全局背景图
Set from 设置 → 皮肤 → 「全局背景图」:
- 选择图片 → 读入
File,经createImageBitmap+OffscreenCanvas按[email protected] → [email protected] → [email protected] → [email protected] → [email protected]逐档降,每档用 编码后的 data URL 长度判定,落进 3 MB 就停;转成 data URL 存进localStorage。 写盘与读入共用MAX_WALL_BYTES = 5_000_000这一把尺——曾经一个按二进制字节、一个按 data URL 字符,base64 膨胀 4/3,一张 3.2 MB 的图能存进去却读不回来(预览正常、背景图"未设置"、颜色退回 hex,表现就是"皮肤直接没了")。全档都超 5 MB 才拒绝并提示换图; - 图片浓淡:图层自身的
opacity,0–100%,默认 100%; - 界面透明
g:0–90%,默认 50。滑块读的是背景透过率,不是每层 alpha。正文面在 Windows 上真实叠 3 层,所以要alpha = 1 - (g/100)^(1/3)才能让合成落回g:g=50 → 每层 0.206、g=60 → 0.157、g=0 → 1(全不透明)、g=90 → 0.035(几乎全透)。背景图开着时bg-base与sidebar-fill取同一个 alpha,其余面按ALPHA_LIFT逐级抬升 (layer-1 +0.06、overlay +0.10、button-elevated-fill +0.22),设置弹层那三件 (layer-2面板 /settings-card-fill卡片 /bg-mask-1遮罩)走单独的"吸血比例"模型, 文字、描边、bg-layer-3保持不透明; - 侧栏校正
sidebarBias:-40…+40,默认 0。0 时侧栏与主区合成严格相等;非 0 才按1 + bias/100缩放侧栏每层的 alpha(正=更实、负=更透),代价是交界处重新出现一条竖缝, 所以 UI 上直接标「会露出竖缝」。 - 对比度
contrast:0–100,默认 50。50 等于 v0.5.0 逐字节的结果,往两边才有变化, 所以加这个旋钮不会改动任何人现有的观感。
对比度怎么工作(v0.7.0)
它只解决"图上压字看不清",因此只动两处,都在 token 层:
- 文字:
label-primary/-secondary/-tertiary沿对比轴移动。k = (contrast-50)/50 ∈ [-1,1],k>0时按k × 30%推向「对比极」(亮色模式→黑、暗色模式→白),k<0时按|k| × 30%退回底色(更淡)。文字始终是不透明 hex,不受壁纸影响,所以它永远盖在图上而不是被图吃掉。 - 面类底:两套算法,因为两类面职责不同——
- 主区面(
layer-1/overlay/button-elevated-fill)仍是加法抬升+lift × (1+k):k=-1时抬升归零(与正文同 alpha,空间最通透),k=+1时抬升翻倍。这几处下面没有 正文,只有壁纸,透一点没关系。 - 设置弹层三件套是吸血比例(v0.7.0 改):
panel = 1 - (1 - body) × 0.25 × (1 - k),卡片card = 1 - (1 - panel) × 0.35, 遮罩scrim = min(0.78, 0.26 + 0.95 × (1 - panel))。语义是"面板吸走正文 75% 的透光",k只缩放残留那份透光:k=+1→ 面板全实、遮罩回基线 0.26;k=-1→ 残留翻倍、 遮罩自动加深。加法在这里会崩:g=90时正文 alpha 只有 0.035,+0.14的面板只剩 0.175,设置弹层会变成玻璃板、背后对话直接叠字;乘法给的是下界(g=90仍有 0.78), 滑块拉到头也不会塌。
- 主区面(
五个命名档位就是预设值:
| 对比度 | 档位 | 文字 × | 主区面板抬升 × | 弹层面板残留透光 × | 用在什么时候 |
|---|---|---|---|---|---|
| 10 | 极柔 | 0.88 | 0.60 | 1.80 | 图很淡、只想要氛围,字能读就行 |
| 30 | 柔和 | 0.94 | 0.80 | 1.40 | 低反差图片 |
| 50 | 标准 | 1.00 | 1.00 | 1.00 | 默认,与 v0.5.0 逐字节一致 |
| 70 | 清晰 | 1.06 | 1.40 | 0.60 | 背景图偏花、偏亮(推荐上限) |
| 90 | 锐利 | 1.12 | 1.80 | 0.20 | 深色图或长时间读长文 |
为什么和「界面透明」不打架:两根轴正交。界面透明改的是正文 alpha(bg-base、
sidebar-fill 的每层 alpha,决定空地上能看见多少图),对比度改的是压在图上的东西
(文字色 + 面类实度)。调前者不动后者,反之亦然;加法那几项被 withAlpha 夹在 [0,1],
乘法那几项本身就在 [0,1] 内,任何组合都不会产生非法值。
为什么不用 scrim(内阴影/压暗层):scrim 得画在壁纸那一层上,等于给壁纸节点再加一条
linear-gradient —— 它会和「图片浓淡」的 opacity 相乘,两个滑块互相抵消,而且全屏压暗
对深色模式是反效果。加实度 + 文字加深已经覆盖同一个需求,且没有第四个绘制者。
为什么侧栏会和主区不一致(v0.5.0 的根因与修法)
侧栏和主区不是同一个层数,这是所有"割裂感"的来源。源码依据(本机 0.2.0-rc.2 产物;0.2.1-rc.1 复核结构未变):
| 位置 | 规则 | 贡献 |
|---|---|---|
dsh-client-ui-layout AppFrame.module.css |
[data-windows-titlebar] .BynINW_frame{background:var(--dsw-specific-sidebar-fill)} |
整窗底 1 层 sidebar-fill |
| 同上 | .BynINW_sidebarCol{background:var(--dsw-specific-sidebar-fill)} |
侧栏列 +1 层 |
dsh-client-ui-sidebar |
._2H3hWW_root{background:var(--dsw-specific-sidebar-fill)} |
侧栏根 +1 层 |
| 同上 layout | [data-windows-titlebar] .BynINW_centerCol{background:var(--dsw-alias-bg-base)} |
主区列 +1 层 bg-base |
dsh-client-ui-conversation ConversationRoot.module.css |
.Dc7zOa_root{background:var(--dsw-alias-bg-base)} |
会话根 +1 层 bg-base |
即:侧栏正文 = 3 层 sidebar-fill;主区正文 = 1 层 sidebar-fill + 2 层 bg-base
(「对话」面板,也就是默认视图)。 两侧层数都是 3,所以只要
a(sidebar-fill) === a(bg-base),合成不透明度 1-(1-a)³ 就严格相等——不需要任何近似。
这正是 v0.5.0 的做法:两个 token 同 alpha,第三个滑块默认 0。
「侧栏是一块实心板」到底是谁画的(v0.7.0 修正:是 codex-ui)
是 @michengai/dsh-codex-ui v1.1.40 画的,而且不走 token。 v0.6.0 给出过"不是任何第三方插件"
的结论,理由是"没人写 --dsw-*"——这条推理漏了一种情况:插件根本不碰 token,而是整体替换侧栏
组件、自己用硬编码 hex 刷底。它的底色变量是自建的 --dcu-sidebar-background,值写死在样式表里,
和主题无关。(用户实证:停用 codex-ui 后"侧栏发白"立刻消失。)
证据(<profile>\node_modules\@michengai\dsh-codex-ui\lib\client.js,<profile> 指 ~/.dsh/profiles/<name>):
| 事实 | 位置 |
|---|---|
注册整列替换 sidebar slot(priority: -1,registrant: "michengai-codex-ui") |
:11836 ctx.slots.inject("sidebar", () => registerAfterOfficialWave(...));:11854–11859 ctx.slots.register({ name: "sidebar", priority: -1, registrant: "michengai-codex-ui", … }) |
自己渲染 <aside class="dcu-root"> 并内联一张样式表 |
:8050–8054 jsxs("aside", { className: 'dcu-root' + …, children: [jsx("style", { children: stylesheet$3 }), …] });:8413 CodexSidebar.displayName = "michengai-codex-ui" |
硬编码侧栏底色:亮色 #eef7f5、暗色 #1d2120 |
:7481 .dcu-root{--dcu-sidebar-background:#eef7f5; … background:var(--dcu-sidebar-background); …},同段 body[data-ds-dark-theme] .dcu-root{--dcu-sidebar-background:#1d2120; …} |
| 注释自述"只替换导航外观" | :7860 /* Codex 风格的 DSH 侧栏,只替换导航外观,项目浏览和设置仍由 DSH 官方组件提供。 */ |
| 只在 Desktop 给出系统材质(mica)时才放行透明 | :10249–10251 html[data-dsh-native-backdrop=mica] … :is(.dcu-root,.dcu-settings-nav){background:rgba(255,255,255,.18)};普通浏览器不走这条 |
| 自带的玻璃分支没有开启入口 | :7483 body[data-we-sidebar-glass] .dcu-root{position:relative;background:transparent} + ::before{…};全仓没有任何代码写 data-we-sidebar-glass 或 --we-sidebar-*,这条分支不可达 |
机制:codex-ui 把产品侧栏的整列内容换成自己的 aside.dcu-root,并在上面刷一层不透明 #eef7f5。
我们的 --dsw-specific-sidebar-fill 照写、照生效,只是被它整块盖住——所以诊断行里侧栏实测值仍是
我们的 rgba,眼睛看到的却是一块白板。
v0.6.0 那张排除表对其它插件仍然成立(也解释了为什么只 grep token 会得到 0 命中):
| 查什么 | 范围 | 结果 |
|---|---|---|
是否有人调 theme.overrideTokens |
node_modules\@michengai(683 个文件) |
0 处 |
是否有人引用 --dsw-specific-sidebar-fill |
better-sidebar / codex-ui / codex-pet / dshmarket / whale-widget | 0 处 |
| 是否有人碰产品侧栏的类名哈希 | 2H3hWW / BynINW / sidebarCol,6 个候选包 |
0 处 |
setProperty 写的变量名 |
全部 11 个 @michengai 包 + better-sidebar + dshmarket + whale-widget |
只有 --dsh-* / --dcu-* / --dshw-*,没有 --dsw-* |
| 全局选择器 + 背景的规则 | @michengai / whale-widget / billion-context |
0 条 |
| 全屏有背景的浮层(fixed/inset:0/100vh) | 8 个候选包 | 0 条 |
| 动态安装的包(绕过 profile 清单) | .dsh-market\、.plugin-manager\ |
只有日志与 discovery JSON,无代码 |
better-sidebar 只是右侧栏工作台(它接的是 sidebar.right.pane.tab 一类 slot,
src/client/layout.css 里全是间距/菜单方向规则,唯一带底色的是一条
div[role="menu"] button[aria-expanded="true"]{background:var(--dsw-alias-interactive-bg-hover)})。
archive-manager 那类"自己往 <head> 塞 <style>"的插件,塞的都是 .module.css 里的模块哈希类,
不覆盖左列。
所以在没装 codex-ui 的情况下,左列底色只有上面表里那三条 --dsw-specific-sidebar-fill,
而它整体由我们一个 token 决定。装了 codex-ui 时,最上面还压着它自己那层不透明 #eef7f5——
这一层在 token 层无法覆盖:它既不是 --dsw-*,又是 aside.dcu-root 自身的 background。
要让它透明只有三条路:让 codex-ui 退出(推荐,用户已验证)、改它的 DOM、或改它的包;
前两条都违反"只经 theme.overrideTokens"这条硬约束,所以本皮肤不碰,只在 §15 记下这个事实。
两条补充事实决定了"我们的值一定生效":
- presenter 在
dsh-client-ui-layout里(不是 theme 包里):lib/client.js:539起const body = document.body; …; for (const [name, value] of Object.entries(snapshot.active.tokens)) { body.style.setProperty(name, value); … }——逐条写进body的内联样式; theme服务的composeActive()把 override 层折进active.tokens时只按名字覆盖、不做白名单过滤 (tokens[name] = modes[active.colorScheme]),所以--dsw-specific-sidebar-fill一定在写入列表里。
于是默认状态下两侧应当是看不出差别的。按 aurora 亮色、glass=50、壁纸取中灰 #808080 手算:
| 逐层合成结果 | |
|---|---|
主区(sidebar-fill → bg-base → bg-base) |
rgb(182,184,192) |
侧栏(sidebar-fill × 3) |
rgb(179,181,192) |
差 3/255,肉眼不可辨。因此截图里"左边一整块实心"不可能来自 v0.5.0 的默认状态,只可能是:
① 存了非 0 的「侧栏校正」(侧栏每层被放大,s=1.3a 时合成 61%);② 页面跑的还是 v0.4.x 的
client bundle(那一版 sidebar-fill 是不透明的 #e6eaff,正好就是"浅色板");
③ 有插件在我们之后又写了一次这个 token(本机没有,但以后可能装)。
针对 ③,v0.6.0 加了一条兜底:订阅 theme/change,只在发现
--dsw-alias-bg-base / --dsw-specific-sidebar-fill 的实测值和本层写入值不同时,才在本层重铺一次
(相同则什么都不做,因此不会自激、也不会和别人的配色互相顶)。同时「实时检查」那行现在会分别
读出两侧的写入值与实测值,并在侧栏 token 变成非 rgba 时直接标
⚠ 侧栏 token 被覆盖成实心色 —— 这一行就是"到底是哪条规则让侧栏不透"的现场判据。
判据只剩层数:某个主区面板的根节点若不自铺 bg-base(不同面板写法不同),那块就只有 2 层,
会比侧栏透一档(a=0.206 时 37% vs 50%)。这是结构差异,token 层无法同时兼顾两种层数,
所以选择以默认视图(对话 = 3 层)为准。
v0.4.x 的两个错叠在一起:
- 滑块值被当成"每层" alpha。滑块写 50 时每层真的是
0.5,三层一叠就成了 87.5% 不透明 ——图只剩 12.5% 可见,"界面透明 50%"实际几乎不透; - 层数被记成 2(
1-(1-s)²对1-(1-s)(1-a)),于是得出"s=a 时两者齐平"这个错误结论。 真实层数是 3,s=a只是碰巧让两边的 3 层都是 87.5%;「侧栏浓度」往上一拉(用户为了看清 文字很自然会拉)就立刻分叉,s=0.8时侧栏合成 99.2%,肉眼就是一块白板。
再叠上侧栏内部的自身不透明块,割裂感就定住了:_2H3hWW_newSession 的底是
--dsw-alias-button-elevated-fill,原生值 = var(--dsw-static-neutral-bluish-00),即纯白
#fff、不透明,高 38px 通栏;选中/悬停行用的 --dsw-alias-interactive-bg-hover 是中性灰
#2631480f 直接压在白板上。主区(对话流)没有这类整块底,所以同一时刻"右边能透图、左边是白的"。
v0.5.0 三条一起改:滑块改指"背景透过率"并按 3 层反解(1-g^(1/3),50 → 每层 0.206,两侧合成
严格 50%)、把纯白 button-elevated-fill 换成"底色掺白 +0.22"的半透明面、悬停/选中改用主色淡染。
剩下两处结构性的、token 层无解的差异,只能记在这里:
- 全宽标题栏拖拽带
[data-windows-titlebar] .BynINW_frame:before只有 2 层 (frame+:before),恒比正文透一档。把它拉齐就意味着正文与侧栏分叉,所以选择让误差 落在这一条带上——它是横贯全宽的,不产生竖缝; - Windows 下
.BynINW_rightbarCol没有背景(只有[data-platform=darwin]才给bg-base), 右栏只有frame的 1 层,展开右栏时会比主区更透。
面类 token 不是一个数:侧栏、主区、设置弹层面板分别由
--dsw-specific-sidebar-fill、--dsw-alias-bg-base、--dsw-alias-bg-layer-2 上色
(设置面板的 wCInkW_panel 直接用 layer-2;同页卡片的 --dsw-alias-settings-card-fill
又在 theme 里被别名成 layer-2)。alpha 只要差 0.1,交界处就出现一条硬边("割裂感")。
v0.6.0 曾经让整屏同 alpha,结果弹层几乎不透、看起来像"根本没生效";现在的分工是:
bg-base与sidebar-fill取同一个 alpha(两侧齐平,不产生竖缝);layer-1在其上+0.06保留主区卡片的层次;- 设置弹层三件套单独一套模型(见下节),面板明显更实;
- 侧栏可用「侧栏校正」单独偏置,默认偏置为 0。
设置弹层为什么会和背后的对话叠字(v0.7.0 修法)
设置页不是我们的组件(我们只往 settings.section 插一段),它的壳是产品的
@deepseek-ai/dsh-client-ui-settings-general(lib/client.js 的 css$6 段):
| 层 | 规则(原文) | 用的 token | 原生值 |
|---|---|---|---|
.wCInkW_overlay |
position:fixed;inset:0;z-index:1000 |
— | 只负责定位 |
.wCInkW_mask |
background:var(--dsw-alias-bg-mask-1); backdrop-filter:var(--dsw-mask-blur) |
bg-mask-1 |
亮 #0000003d(24%)、暗 #00000080(50%);--dsw-mask-blur: none,即不模糊 |
.wCInkW_panel |
width:800px; border-radius:var(--dsw-radius-panel); background:var(--dsw-alias-bg-layer-2) |
bg-layer-2 |
不透明 hex |
| 页内卡片 | 各 settings-* 包里的 …_rowCard{background:var(--dsw-alias-settings-card-fill)} |
settings-card-fill |
theme 里 alias 成 var(--dsw-alias-bg-layer-2) |
v0.6.0 的漏洞:面板底跟着 layer-2 走,而 layer-2 当时是"正文 alpha + 0.14"。默认
glass=50 时正文每层 0.206 → 面板只有 0.346,遮罩还是原生 0.24 且不模糊;两层一乘,
背后正文还有 (1-0.346)×(1-0.24) = 49.7% 原样透上来——这就是「设置面板里能看到聊天文字」。
g=90 时更糟:正文 0.035 → 面板 0.175,透上率 62%。
v0.7.0 三条一起改,全部走 token,没碰任何产品 DOM:
- 面板不再用加法,改"吸血比例":
panel = 1 - (1-body) × 0.25 × (1-k),默认 → 0.802(原 0.346); - 卡片是独立 token,单独再抬一档:
card = 1 - (1-panel) × 0.35→ 默认 0.931; - 遮罩跟着面板的透光反向加深:
scrim = min(0.78, 0.26 + 0.95 × (1-panel))→ 默认 0.448(原 0.24)。
结果:默认设置下背后正文只剩 (1-0.802)×(1-0.449) = 10.9%(v0.6.0 是 49.7%),卡片里 3.8%;
把界面透明拉到 90 仍有 12.3%;对比度拉满时面板 1.0、遮罩回基线 0.26 → 0%。
为什么不给 --dsw-mask-blur 放白名单做真模糊:白名单正则只收 alias- / radius- /
shadow-lv / specific- 四族(TOKEN_NAME = /^--dsw-(?:(?:alias|radius|specific)-[a-z0-9-]+|shadow-lv[0-9]+(?:-[a-z0-9-]+)?)$/),
mask- 不在其中。放宽它等于允许这个插件写任意 CSS 值(FORBIDDEN_VALUE 只挡 url(、;、{} 这类),
而模糊对"叠字"的收益只是把背景抹花——可读性得靠不透明面拿,不是靠模糊。所以选择不动白名单:
加法换乘法 + 加深遮罩已经把透上率从 50% 压到 11%,代价为零。
代价与取舍:layer-2 是"设置面板 + 主区少数弹层卡(轨迹面板、文档预览、插件管理)"共用的一支
token,压到 0.80 会让那几处也变实——可它们的共同点正是"里面有字要读",本就不该透。反过来,
主区正文与首页用的是 bg-base / bg-layer-1(+0.06),透光一点没动:layer-2 在这 33 个产品包里
chat / conversation / layout 一处都没引用。
The image layer is a single position: fixed; inset: 0; z-index: -1; pointer-events: none node
with id dsh-skin-aurora-wall, created by the plugin and removed on teardown / 清除 / 关掉皮肤。
It carries aria-hidden="true" and never receives pointer events, so it cannot block the UI.
图片只存在本机浏览器(localStorage 的 dsh-skin-aurora:v1,与其他设置同一个键),不上传、
不写 profile。data:image/*;base64, 之外的一律拒收(含 http(s) 外链与其它 scheme),避免把
可执行内容塞进 url()。
配置写在 localStorage 的 dsh-skin-aurora:v1,只影响当前浏览器,不写 profile 配置。
注册失败(宿主没提供 slots、或 React 运行时取不到)只让配置页缺席,配色照常生效——两者
用 try/catch 隔离。
Install
From this workspace, with the package directory as an absolute path:
plugin_manager(action: "install_bundle", target: "<abs path to this directory>")
Then read the application field of the result:
applied— live now.restart-required— replacing an already-installed copy always needs a restart.overridden— a higher-priority layer covers the row; the skin is not in effect.failed— readwarningsand fix before assuming anything works.
Do not hand-edit the profile's package.json / cordis.patch.yml, and do not run pnpm
inside the profile directory: install_bundle already does that, and hand edits collide with
the patch it generates.
Row check (no approval needed):
Config.listConfigs(name: "dsh-skin-aurora")
Disable and roll back
set_plugin the row to disabled, or remove_bundle it. Either path tears the effect down,
the layer disappears, and the next theme snapshot is recomposed without it:
alias/specifictokens fall back to the base theme's own colours;shadowtokens fall back to the stylesheet definitions (:root/body).
No compensation code is needed, and the plugin never writes body / :root styles directly —
its only self-created node is the wallpaper layer, which is removed explicitly. That is exactly
why rollback is complete.
Measured in this runtime (2025, live profile): the row id for set_plugin is
include:dsh-skin-aurora — the package name dsh-skin-aurora is reported as unknown.
Disabling removes the layer from the live page with no refresh; re-enabling does not
re-create it (the Host row comes back, the client half only runs at page boot), so a page
refresh is required to get the skin back.
Self-check in the client
assertSkinTokens() runs inside apply before the layer is installed, and again on every
palette change. It rejects any token name outside --dsw-{alias,radius,shadow-lv,specific}-*,
any value that is not a non-empty { light, dark } string pair, and any value containing
url(), image-set(), expression(), javascript:, @import, ;{}, backslash, or a newline.
A failure shows up as a failing row in the plugin panel instead of a skin that installs and
changes nothing.
Offline checks
node --check lib/client.js
node ..\tmp\ac-skin2.mjs # 99 项:白名单/成对值/方案切换/自定义主色/背景图/四个滑块/对比度/弹层三件套/拒收/撤销/外部写入兜底/侧栏仪表盘/赠送额/平台账单格/一键重载
node ..\tmp\ac-static.mjs # 15 项:清单、token 基线、DOM 纪律、loader 契约、负向注入、注入点与降级契约、location 纪律
node ..\tmp\probe-contrast.mjs # 只读打印各对比度档位的 token 表
ac-skin2 里有两项专门钉住 v0.5.0 的根因(改坏了会立刻红):
「侧栏与主区用同一个 alpha(3 层 × 同一 alpha)」与
「两侧合成不透明度严格相等且 = 50%」;另有八项钉住对比度与兜底:50 必须与基线逐位相同、
100 时面板全实且遮罩回基线、0 时残留透光翻倍、无背景图时只推文字、
「恢复默认」后四个滑块读数必须归位、外部把侧栏 token 改成 #ffffff 后必须重铺顶回去且不回环、
theme/change 必须已订阅;还有四项钉住弹层(v0.7.0):面板吸血比例、卡片再实一档、
遮罩随面板透光加深且透上率 ≤15%、界面透明拉到 90% 时面板仍 ≥0.75。
钉住 v0.8.0 仪表盘的 19 项(ac-skin2 69 → 88),加上 ac-static 的口径变化与新增
(AC-11 从"零定时器"改成"恰好一对 setInterval/clearInterval",新增 AC-12 注入点、AC-13 降级与
清理契约,12 → 14 项):
- 注入点:
sidebar.footer.action必须带id、sidebar.toggle.badge必须注册、 绝不注册sidebar本体、数据泵订阅会话快照而不是自己数消息、全局只有一个 60s interval; - 读数口径:用量 42K tok(1000 + 39000 + 0 + 2000)与缓存命中 98%(39000/40000,分母不含
output)、上下文 5%(5000/100000)、余额
¥12,345.67(截断到分 + 千分位); - 降级:未登录 →「未登录」且一次余额请求都不发;余额返回
failed→「—」而 token 行照常; 隐藏档 → 整块不渲染且轮询不发余额请求; - 折叠:
wide=false退化成小圆点,紧凑档只留一行,切回完整档能重新渲染; - 清理:卸载后
clearInterval拿到的是同一个 token、会话快照退订一次、监听集合清空。
钉住 v0.9.0 的 11 项(ac-skin2 88 → 99,ac-static 新增 AC-14,14 → 15):
- 余额口径:主行仍是
¥12,345.67(两位小数 + 千分位 + 截断);bonusWallets有值时出现 「+赠送」次行且只累加同币种、> 0 的赠送额(喂8.5+0+ 一个 USD9.99→ 只显示¥8.50);没有赠送钱包时不渲染空次行;USD 样本必须出$1,234.50(符号取currency, 不是一律¥); - 平台账单格:行内必须有「平台账单」与「本机无接口」,且整行文本一个数字都没有
(
!/\d/,防止有人后来"顺手"填个 0 或拿会话用量冒充账单);第三行标签必须是「本机用量」, 与「平台账单」同时出现——分栏命名被钉死; - 一键重载:设置页与仪表盘各有按钮(仪表盘那个是
↻,aria-label同名);连点 3 次只调 一次location.reload();发起后按钮立刻disabled,冷却 3 秒后恢复可用(不是永久锁死), 仪表盘的↻走同一条通道、冷却结束后能再触发一次; ac-staticAC-14:去掉注释行后,location.reload()恰好 1 处,location.href/assign/replace/search/hash/pathname/origin与window.location都是 0, 并且必须带typeof location !== 'undefined'守卫(非浏览器宿主下静默降级)。
升级到 v0.9.0
不需要重置任何设置(SETTINGS_REV 仍是 5:v0.9.0 只加了两个模块级常量
RELOAD_COOLDOWN_MS / MONEY_SYMBOL,localStorage 的形状一个字节都没动)。
- 仪表盘从三行变四行:余额行下面可能多一行「+赠送」,最后多一行「平台账单 · 本机无接口」; 第三行标签从「用量」改成「本机用量」(悬停写明是当前会话投影,不是账单);
- 余额的货币符号与赠送额都在渲染时算,不落盘,所以已经存着的旧记录照常工作;
- 新增「一键重载」:设置页底部一个按钮 + 仪表盘右上角一个
↻,两者共用一个 3 秒冷却; ac-skin288 → 99 项,ac-static14 → 15 项(新增 AC-14 location 纪律)。
升级到 v0.8.0
不需要重置任何设置(SETTINGS_REV 仍是 5:只新增了 hud 一个键,旧记录里没有它就按
full 读入,不参与 legacy 重置)。
- 侧栏底部多一张仪表盘卡片(可切三档:完整 / 紧凑 / 隐藏),颜色全走
--dsw-alias-*, 跟着对比度旋钮和背图一起变; package.json的dsh.client.inject多了@deepseek-ai/dsh-client-ui-sidebar与@deepseek-ai/dsh-api-remotes(侧栏 slot 与ctx.remote.account的来源);- 客户端里的定时器纪律从"一个都不许有"改成"恰好一对
setInterval/clearInterval" (60s 兜底轮询,卸载时清掉),ac-static的 AC-11 已按新契约改; - 仪表盘拿不到
slots/sessions/remote时整块不渲染,配色和背景图不受影响。
升级到 v0.5.0 的副作用
SETTINGS_REV 4 → 5。旧记录里的 sidebarOpacity 键已废弃(语义从"每层 alpha"变成
"背景透过率"、侧栏浓度变成校正量,旧数值不再等价),所以升级后三个滑块会重置为默认
(图片浓淡 100%、界面透明 50%、侧栏校正 0),配色方案、自定义主色与背景图都保留。
不重置的话,旧记录会让侧栏再次和主区分叉——这正是要修的那个 bug。
升级到 v0.7.0
不需要重置任何设置(SETTINGS_REV 仍是 5:没有改任何键的语义,contrast 的含义也没变——
它依旧只缩放"面板残留多少透光",50 依然是 v0.5.0 的等价点)。变化全在 token 的算法上:
- 有背景图时会多写两个 token:
--dsw-alias-settings-card-fill(卡片,比面板再实一档)与--dsw-alias-bg-mask-1(弹层遮罩,随面板透光反向加深)。没设背景图时一个都不写, 全部沿用主题原值,所以默认观感与 v0.6.0 相同; --dsw-alias-bg-layer-2从"正文 +0.14"改成"吸走正文 75% 的透光"(默认 0.35 → 0.80)。 它会同时让设置面板、轨迹面板、文档预览、插件管理卡片变实——这几处本就是"要读字"的地方;- 设置页底部多一行说明(弹层为什么单独一套算法),实时检查那行多出
弹层面板 X% 卡片 Y% 遮罩 Z% → 背后正文透上 W%三个读数。
如果你的"侧栏发白"在 v0.7.0 依然存在,先去看 @michengai/dsh-codex-ui 是否启用:
它整列替换 sidebar slot 并自绘不透明 #eef7f5(依据见上节表格),token 层盖不住它。
升级到 v0.6.0
只新增一个 contrast 键,SETTINGS_REV 仍是 5(这次没有改任何旧键的语义)。
旧记录里没有 contrast 时按 50 读入,不会重置已有的浓淡/透明/校正值;
50 本身与 v0.5.0 的产出逐字节相同,所以升级后观感不变,除非用户自己去推这个新滑块。
Not verified
Confirmed by simulation only: the palette math (including the 3-layer composite equality), the
registration shape, every interaction in the settings page, the wallpaper node contract,
the rgba() surface tokens, the contrast mapping (0/50/100 and the no-wallpaper case), and the
settings-popup model (panel/card/scrim arithmetic), and the dashboard (injection shape, readout
arithmetic, sign-out/balance-failure degradation, teardown). Not yet confirmed on a live page: that
the shell paints wallpaper behind every opaque surface (deeply nested panels may still hide it),
how it looks next to native components, the two structural gaps listed above (2-layer titlebar
strip, 1-layer right bar) — those are read off the shipped CSS, not observed on screen — the
settings popup's actual legibility at glass=90 (0.78 panel + 0.49 scrim is arithmetic; whether
the leftover 12% reads as "ghost text" on a busy wallpaper needs eyes), whether the darker scrim
feels too heavy in dark mode (#000000 at 0.45 over a dark page), and whether the contrast stop a
given user likes is actually the stop that makes their wallpaper legible.
The pixel figures in「侧栏是一块实心板」and「设置弹层为什么叠字」are arithmetic on the shipped
alpha values and shipped CSS, not measured screenshots; the ⚠ 被覆盖 readout and the
弹层面板/卡片/遮罩/透上 readout are the on-machine tests that close that gap.
Dashboard-specific gaps, all of them "shape is right, numbers need real eyes":
- whether the real
projectionValues.tokenUsageon a live session matches what the chat pill shows for the same turn (the field names and the arithmetic are copied from the shipped client, but only a live session proves the two read the same object); - whether
ctx.remote.account.getState()returnsstatus: 'credential-stored'for the current signed-in official-API setup, and whethergetBalanceneeds the extra grant the settings page performs before it succeeds; - whether the settings popup can actually be navigated to「皮肤」by clicking the nav button — the
selector
[data-shortcut-modal="settings"]+nav buttonand thearia-currentcheck are read off the shipped client, not observed; if it misses, the dashboard still opens settings, just on whatever page it opened; - whether the 5px dot in
sidebar.toggle.badgeis visible enough on a collapsed sidebar; - (v0.9.0) whether
location.reload()in an Electron renderer actually re-runs the client half the same way a window reopen does — the simulation calls a stub, so the real path (skin re-applied, settings read back fromlocalStorage, dashboard repainted) is unverified on a live page; - (v0.9.0) whether
bonusWalletsreally carries{currency, balance}in the livegetBalancereply (the field name comes from the shipped account client, the element shape does not); if it differs, the second line simply never appears and the balance row is unaffected; - (v0.9.0) whether the settings page's reload button stays reachable at
glass=90+ a busy wallpaper (it is the last block on the page, so it scrolls, but nobody has looked at it on screen).