dsh-warm-ui
一个 DSH(DeepSeek Harness) Web GUI 客户端插件包: 暖色主题 + 液态玻璃材质 + 输入栏工具行整形 + 动态壁纸 + 品牌彩蛋。
给 DSH Web GUI 换一套暖色调,并把输入栏工具行里那簇第三方控件压扁、对齐、去发光。
这是一个单包多插件的 bundle:暖色系列的全部 4 个插件都随 dsh-warm-ui 一起发布、一起挂载。
- 暖色:整站色阶从冷蓝灰换成暖褐灰,强调色从品牌蓝换成暖琥珀;明暗两套主题同时生效。
- 整形:输入栏里
prompt-optimizer的两个滑杆 + 档位胶囊 + 模型胶囊 + 帮助圆钮,改成 24px 高、 无发光、无胶囊底色的一组安静控件,并在它与左侧 DSH 原生「访问模式」之间加一条发丝分隔线。 - 动效:输入框里是弹簧光标(打字常亮、停手 500ms 才闪、横移是滑过去而不是瞬移)+ 框内与正文的选区高亮(位置/宽度弹簧驱动);颜色从当前主题的强调色派生,浓度一个变量可调, 外观面板里有「动效 关/开」。尊重系统「减少动态效果」,关掉即完全回到浏览器原生。
- 玻璃:输入卡片/菜单的折射、边缘光、表面反射(Chromium 独有;Firefox 平台不支持 SVG 滤镜
作为 backdrop,那里只有 blur/tint。见
contract/ui-contract.md第 6 节)。
环境要求
| 项 | 要求 |
|---|---|
| DSH | >=0.1.7-rc.2 <0.3.0(实测于 0.1.7-rc.2 与 0.2.1-rc.3) |
| profile | web(本插件只在 Web GUI 里有意义) |
| 浏览器 | Chromium 系(玻璃效果);Firefox 可用但无折射层 |
| 运行时依赖 | dsh-warm-shared 已作为子包随本包发布,不需要单独安装 |
peerDependencies 里声明了两个宿主框架包作为能力来源(不是硬性安装要求,DSH 自带):
| peer | 用途 |
|---|---|
@deepseek-ai/dsh-client-ui-theme |
ctx.theme.overrideTokens —— 暖色 token 的写入通道 |
@deepseek-ai/dsh-client-ui-slots |
sidebar.footer.action —— 左下角「外观」换档面板 |
只有以
@deepseek-ai/dsh/@deepseek-ai/dsh-*开头的 peer 会被 DSH 做版本校验 (见dsh-app-boot的evaluatePluginCompatibility),所以上面这两条是声明意图 + 让 版本不匹配时 DSH 能提前告警,不会挡住安装。
它导出什么
| 入口 | 文件 | 说明 |
|---|---|---|
. |
lib/index.js |
host 半边:空插件,只为了让 loader 有一条 entry(dsh-client-modules 按 loader entry 扫描 dsh.client 声明,没有它就不会有客户端模块) |
./client |
lib/client.js |
浏览器半边:全部实际行为都在这里 |
./cordis.patch.yml |
cordis.patch.yml |
包自带的组合补丁层,声明暖色系列 4 个 loader entry |
./package.json |
package.json |
dsh.bundle.patch 指向包内的 cordis.patch.yml,这是 DSH 标准插件包的挂载方式
(与 @gbthui/dsh-auto-review、@dsh-external/dsh-prompt-optimizer 同一写法)——
装完由 DSH 自己把包名写进 profile 的 bundles,用户不需要手改配置。
dsh-warm-ui 不只是主题本体:它是一个单包多插件 bundle。package.json 的 files
同时发布 preview/shared(共享模块)、preview/wallpaper(动态壁纸)、
preview/brandgag(品牌彩蛋);cordis.patch.yml 用相对路径 entry 把这几个子包
一起插进组合树。所以安装/卸载永远只面对一个包名:dsh-warm-ui。
为什么没有构建步骤
源码就是产物。 这个仓库没有 tsconfig / rollup / vite / esbuild / tsup 任何构建配置,
也不需要 —— DSH 的客户端 bundle 是按请求逐字节读盘的(实测:从 lib/client.js 取的原文
片段在服务器返回的 bundle 里 100% 命中),格式本身就是平台要求的
window.__ModuleLoader__.load({ id, factory }) 工厂式 CJS。
所以:clone 下来即可用,不必 build。改动 lib/client.js → 刷新浏览器即生效。
它依赖什么
dsh-warm-shared(同仓库 preview/shared/)是必需的运行时依赖,它只有 97 行、导出 2 个函数
(createStyleInstaller / cssPropertyName),被收拢在这里的理由是一个真实线上 bug:
样式表必须打上 data-plugin / data-plugin-css 标记,漏掉会导致热重载删不掉旧表,
旧表把玻璃滤镜 svg 撑出 255px,用户看到的就是「壁纸跑到页面下面」。实现只有一份,
消费方就不能各抄一遍。它现在作为 dsh-warm-ui 的包内子目录一起发布,不再要求用户
额外安装一条 #path:preview/shared spec。
一条依赖要在三处同时成立(node tools/check.mjs 的「共享依赖」一条会核对):
- 代码:工厂体里
require("dsh-warm-shared"); - 清单:本包
package.json的dsh.client.external(宿主靠它把依赖行排在本包之前); - 契约:
contract/ui-contract.json的requires。
它还改了什么(技术细节)
液态玻璃的折射与"流动层"(玻璃底下的内容持续轻微游动)写在
contract/ui-contract.md第 6 节:数字、三道门、以及为什么 Firefox 没有这一层(实测)。
1. 暖色:叠一层 theme token
DSH 的配色是两级变量:--dsw-static-*(色阶)→ --dsw-alias-*(语义别名)。别名基本都写成
var(--dsw-static-…),所以只替换三条色阶就能带动整站:
| 覆盖的色阶 | 原值 | 现值 |
|---|---|---|
--dsw-static-neutral-bluish-* |
冷蓝灰(#151517 / #2c2c2e …) |
暖褐灰(#17130e / #2d261e …) |
--dsw-static-neutral-* |
纯灰(行内代码、滚动条) | 暖灰 |
--dsw-static-deepseek-* |
品牌蓝(发送按钮、滑杆、链接) | 暖琥珀 / 赤陶 |
--shiki-token-*(样式表内) |
冷色语法高亮 | Gruvbox 取向的暖色语法高亮 |
写入走官方扩展点 ctx.theme.overrideTokens(source, tokens)(theme 服务会把值写成 body
上的内联样式,稳定压过基础样式表,并与明暗主题切换自动重组)。DSH 升级后这套接口仍在,插件不会失效。
2. 输入栏整形:一张作用域受限的样式表
只作用于输入栏里的 .dpo-controls(即 prompt-optimizer 在工具行里渲染的那一簇),
插件自己的浮层 / 弹窗不受影响。用双写类名(.dpo-controls.dpo-controls)抬特异性,
稳压插件自带的媒体查询断点。
窄窗口下两个滑杆原本会被挤扁、滑块越界压到相邻按钮,所以宽屏(≥1241px)把「档位 / 权限」 收进一颗 24px 的**「优化」胶囊**,点开是一个向上展开的小弹层(内部点击仍转发给插件原本的 刻度按钮,状态机与落盘完全照旧);选完一项弹层立即收起,点空白处或按 Esc 也会收起。 ≤1240px 时胶囊让位给插件自带的窄屏形态,同样没有滑杆可被挤扁。 万一插件的 DOM 结构变了、探测不到刻度按钮,插件会自动退回原样(滑杆回来、胶囊隐藏), 不会留下一个点不动的空壳。
3. 动效层:弹簧光标 + 选区高亮
一张独立样式表(MOTION_STYLESHEET,标记 data-dsh-warm-motion)+ 一个独立引擎 (createMotionApi),挂在 body[data-wta-motion] 下。关掉开关时引擎整个销毁: 图层移除、两个属性摘掉,原生光标与原生选中色立刻回来。
做什么
| 场景 | 行为 |
|---|---|
| 光标 | 自绘 2px 竖条,横向位置由弹簧驱动(response 0.3s、临界阻尼不过冲);打字期间常亮,停手 500ms 后按 1.06s 周期闪 |
| 框内选区 | 位置与宽度弹簧(0.2s,拖选 0.1s)、透明度 0.12s 淡入;跨行时每行各自一块 |
| 正文选区 | 同上,覆盖整页;被包住的嵌套矩形会去掉,上限 200 块 |
范围是全站所有可编辑区:textarea、单行 input、[contenteditable](DSH 的输入框是 Lexical contenteditable)。单行/多行输入框走「隐形镜面」量取(同字体同内边距的隐藏 div + Range 矩形), contenteditable 直接量 Selection。
为什么高亮不会把字染色
自绘层画在文字上面(不重排宿主 DOM),靠 mix-blend-mode 把它变成"垫在下面"的观感: 亮色主题 multiply(白底压成暖米色,深字几乎不变)、暗色 screen(暗底提亮,亮字几乎不变)。 3082 实测:选中前后文字色只差 +2~3 级,而原生蓝色选中会让文字 +20 级。
颜色从哪来
--wta-motion-sel-fill = color-mix(in srgb, var(--dsw-alias-state-business-primary) var(--wta-motion-sel-strength), transparent)。强调色由暖色 token 提供(暖琥珀),所以换暖度档位、 换明暗主题都会自动跟随。浓度只有 --wta-motion-sel-strength 一个旋钮(默认 34%,是在真实暖色 对话框上对比 18 / 26 / 34 / 42 四档定的)。
改这张表/这段代码前必须知道的两件事(都踩过)
- 图层必须是 display: contents。position: fixed 的包裹层会被浏览器单独合成,内层 mix-blend-mode 的"背景"变成透明的自己 —— 高亮退化成一块盖住文字的不透明色块 (实测:整行只剩一个 RGB 值,字全被吃掉)。
- ::selection 的开关属性(
data-wta-motion-sel)挂在"这次要画的那块滚动容器"上, 不是<html>(2026-11-07 改的,原因见下面「为什么属性不能挂在 上」)。 规则写成[data-wta-motion-sel] ::selection—— 挂在谁身上就管谁。
自检口
window.__dshWarmMotion.get() // { enabled, layer, engine }
window.__dshWarmMotion.diagnostics() // { mode, boxes, paintedRects, caret, nativeSel, error }
window.__dshWarmMotion.set(0) // 等价于点外观面板里的「关」
原生蓝色不能一闪而过(2026-10-31 用户报"选中有蓝色一闪")
原生选中色由一条属性控制:[data-wta-motion-sel] ::selection { background: transparent }。
它曾经是"选区出现后"才写的 —— 而浏览器可能在派发 selectionchange 之前就把蓝色画上屏,
于是拖选首帧必露一次蓝。现在改成结构性做法:
- 按下去那一刻就把属性挂上(pointerdown 早于 selectionchange),键盘扩选同理; 之后只在"确实画不出来"时还原(块数超限、量不到矩形、引擎停用、销毁、paint 内部异常);
- 正常路径(光标分支、空闲分支、失焦)一律不去动它 —— 那些地方一还原就会在 "按下鼠标、选区还没形成"的那一帧留下空窗;
pointerdown/ 键盘扩选(Shift+方向、Ctrl+A)也各补一次,用于属性被外部摘掉的极端情况;syncNativeSel()自己记着"属性挂在哪个元素上"(nativeSelHost),换容器就搬过去、还原时清干净; 不再靠读document.documentElement猜状态。
为什么属性不能挂在 上(2026-11-07 用户报「小助手选中文字卡顿」)
::selection 的样式重算是按子树算的:属性挂在 <html> 上 = 整棵文档都在这条规则的作用域里,
于是每一次选区变化(哪怕选区在小助手浮窗里、根本不归我们画)浏览器都要给每个元素重算一次
::selection。3082 真机实测(2593 个节点):单次重算 24ms,拖选时每帧两次 ≈ 46ms/帧,
p95 100ms、28 个长任务。把属性挪到浮窗自己的滚动容器上之后同一段拖选:样式重算 1546ms → 1ms、
帧间隔 66.6 → 16.7ms、长任务 28 → 0。
所以现在:
nativeSelScopeFor(node)找"这次选区归谁管"(最近的、真的能滚的祖先);- 落在别人的地盘(
SKIP_CLOSEST:.ea-field、.dsh-explain-assistant-overlay)里时 一个属性都不挂 —— 那边的选区由小助手自己的高亮层负责; - 挂载时不再抢占式打开属性(旧写法就是在这里把
<html>长期占住的)。
逐帧取证(每个 rAF 记录"选区已存在但原生色未关"的帧数,目标 0):
| 场景 | 修复前 | 修复后 |
|---|---|---|
| 正文拖选(冷启动 ×3) | 1 帧 | 0 / 0 / 0 |
| 输入框内拖选 ×2 | 1 帧 | 0 / 0 |
| 键盘 Shift+方向扩选 | 0 帧 | 0 |
| 双击选词 | — | 0 |
| 关掉动效(对照组) | — | 属性不存在,原生蓝正常回来 |
混排不能叠层(2026-10-31 用户报"不同字体处重叠")
一行里混排不同字体时,Range.getClientRects() 给的是每个行内盒一个矩形:正文一段、
行内代码一段、加粗一段,彼此 top/height 还不一样(实测同一行给出 7 个矩形,高度 24 与 13 两种,
还带完全重复的)。逐块画就是半透明叠半透明 —— 34% 叠 34% ≈ 56%,看着比别处深一块。
现在画之前过两道:
mergeLineRects():按行心距离(≤ 0.6 行高)归行,行内按间隙(≤ 0.8 行高)切段合并, 每段取"段内最宽的矩形"的高度当基准 —— 于是每行一条、高度不随字体跳变;unionOverlaps():对所有已外扩的矩形做相交检测,相交就取并集,循环到两两不相交。 这是结构性保证,不靠阈值 —— 只要送进图层的矩形不相交,就绝不可能叠出更深的颜色。
验收(3081 真机,跨 12 段 136 个块的选区):矩形两两相交数 49 → 0; 单行混排 7 个矩形 → 1 条。
几个会被误认为"坏了"的正常行为:窗口没有焦点时不画光标(浏览器在这种情况下也不显示
光标,paint() 里直接判 document.hasFocus());宿主换掉输入框节点(切换会话、React 重渲染)
时引擎会重新认领当前焦点,另外每秒对账一次,发现"焦点在可编辑区但没被接管"就补上;
编辑器里选中了 chip(DSH 自己把原生光标藏起来)时不画。
降级:prefers-reduced-motion: reduce 时引擎不接管(CSS 也把图层 display:none), 两个方向都监听 change;任何一步抛错都会摘掉属性退回原生,不会出现"没有光标"的输入框。
定稿的默认值(2025-09-13 评审结果)
配色是一轮轮在隔离预览里挑出来的,最终烧死成下面这几个默认值;每一档都能用
localStorage 临时改(键名见表),不用重新构建:
| 维度 | 默认 | localStorage 键 | 说明 |
|---|---|---|---|
| 面亮 | 0 夜间 | dsh-warm-surface |
白天把底色整体抬亮,见下 |
| 暖度 | 3 中性面 + 暖点缀 | dsh-warm-level |
面用中性暖灰,只有强调色留暖琥珀 |
| 字亮 | 3 中暗 −20% | dsh-warm-text |
只压最亮的几档文字,层级不乱 |
| 字色 | 0 冷(贴近 DSH 默认灰) | dsh-warm-tint |
看着眼熟、最不容易累;暖色文字会发涩 |
| 加粗 | 1 暖·极弱 | dsh-warm-bold |
见下 |
| 标题 | 4 中性纯灰·更暗 −20% | dsh-warm-heading |
见下 |
面亮:白天 / 夜间
深色主题的底色是很暗的 #141414。环境光一强,屏幕比周围暗一大截,眼睛要在两者之间来回
适应,看久了发闷。面亮档不是"把亮度乘个系数"——近黑处乘 1.3 几乎看不出来(#141414 →
#1b1b1b),而是在线性空间往一个暖灰里混,每档的绝对提亮量肉眼可辨:
| 档 | 底色 | 面板色 | 正文对比 |
|---|---|---|---|
| 0 夜间 | #141414 |
#212121 |
14.08:1 |
| 1 微亮 | #201f1d |
#282726 |
12.59:1 |
| 2 亮(白天常规) | #292724 |
#2f2d2a |
11.38:1 |
| 3 更亮 | #312e2a |
#35332f |
10.32:1 |
| 4 最亮 | #393631 |
#3c3934 |
9.19:1 |
只作用于"面",不动文字;提亮后若觉得字发灰,把「字亮」调小(数字越小越亮)。
换档入口:侧栏底部的「外观」
DSH 的侧栏底部有一个官方插槽 sidebar.footer.action,插件在那里注册了一行
「外观 · 当前档」按钮(React 组件,不依赖任何 CSS Module 哈希类名)。点开是一个向上
展开的面板,六个维度各一行小圆钮,点一下立即生效、写进 localStorage,刷新与重启都还在。
面板点空白处或按 Esc 收起;拿不到插槽/React 时该面板自动不挂,主题本身不受影响。
选择只存在这台浏览器里;要让所有设备开箱就是这个档,把对应 *_LEVELS 表里的默认值
改成选中的档即可(currentXxx() 的第二个参数)。
加粗为什么改成"同亮度换色相"
深色底上笔画一变粗就发亮发胀(光晕),单纯压暗又会把强调感一起压没。所以加粗保留字重、
把色相挪到暖侧(≈40°)、亮度贴住正文(实测 0.98~1.03 倍),只靠彩度分级控制强调强弱:
色相对比走的是另一条感知通路(红-绿/蓝-黄对手通道),可以在不变亮的前提下制造强调。
默认第 1 档(#e1dfda)几乎只差一点点暖,安静但足够区分。
标题为什么要单独压暗
标题(h1~h6)在 DOM 里的颜色和正文完全一样,它显亮是因为 19px + 700 字重铺成了一整块
"亮面"。所以标题档位只做减法:压亮度为主。默认第 4 档是「中性纯灰、比正文暗 20%」
(#cccccc,对底 ≈11.4:1,仍远高于 AA)。
不推荐用暖色标题:同一屏里正文冷、标题暖会让视线来回时睫状肌跟着色差反复微调,
实测会发涩——暖色那两档只作对照保留。
安装
只装一个包:
dsh plugin add --profile web 'git+https://github.com/yunliya-cuter/dsh-warm-ui.git'
从 fork 安装时把 yunliya-cuter 换成你自己的账号即可。装完 不用手改任何配置:
包自带 dsh.bundle.patch,DSH 会在安装后自动把 dsh-warm-ui 写进 profile 的
dsh.profile.bundles(dsh-app-boot 的 reconcileProfilePlugins:
「New bundle dependencies activate automatically」),并在进程内重新装配。浏览器按一次 F5 即可。
本包的 cordis.patch.yml 会一次插入 4 个 entry:
| entry id | 实际插件 |
|---|---|
dsh-warm-shared |
共享样式表注入模块 |
dsh-warm-ui |
暖色主题 + 玻璃 + 输入栏整形 |
dsh-warm-brandgag |
品牌文字彩蛋 |
dsh-warm-wallpaper |
动态壁纸(纸 / 摩尔纹) |
从旧的多包版本升级:如果你之前分别装过
dsh-warm-shared、dsh-warm-wallpaper、dsh-warm-brandgag,先把它们从 profile 里卸掉,再装本版;否则同名 entry 会被重复插入。dsh plugin remove --profile web dsh-warm-shared dsh-warm-wallpaper dsh-warm-brandgag dsh plugin add --profile web 'git+https://github.com/yunliya-cuter/dsh-warm-ui.git'
验证装好了
打开页面后,左下角应出现 「外观 · <当前档>」 一行(本插件注册到 sidebar.footer.action);
点开「外观」面板,里面应能看到 「壁纸」 分组;侧栏品牌徽章应显示 「遛狗绳」;
整站强调色变成暖琥珀、输入卡片带玻璃质感即为生效。控制台可查:
window.__DSH_BOOT__.entries.filter((e) => e.id.startsWith("dsh-warm-")).map((e) => e.id)
// => ["dsh-warm-shared", "dsh-warm-ui", "dsh-warm-brandgag", "dsh-warm-wallpaper"]
window.__dshWarmExtra.glass // => { alive: true, generation: 1 }
window.__dshWarmAppearance.ids() // => ["dsh-warm-wallpaper"](壁纸分组已并进外观面板)
卸载
dsh plugin remove --profile web dsh-warm-ui
bundles 与 node_modules 会被自动清理,刷新页面即回出厂配色。如果 profile 里还留着旧版
分开安装的三个子包,再补一条:
dsh plugin remove --profile web dsh-warm-shared dsh-warm-wallpaper dsh-warm-brandgag
从本地工作区开发(改代码即时生效)
不想走 git 安装、就在本仓库上改代码时,直接把本地路径作为 spec 装进来:
dsh plugin add --profile web "$(pwd)"
dsh plugin add 接受本地绝对路径(会做成 link: 依赖),preview/shared、preview/wallpaper、
preview/brandgag 都通过主包的相对路径 entry 加载。改完 lib/client.js(或其他子包的
lib/client.js)刷新浏览器即可生效(客户端 bundle 由 webserver 按请求读盘,不用重启服务)。
注意:改了包的
dsh.client清单(例如动external)需要重启 dsh 才会被重新读取 —— 宿主把包元数据缓存在内存里,换 patch 不会让它重读package.json。
想自己调
改 lib/client.js 里的几张表即可(都在文件开头,带中文注释):
WARM_NEUTRAL:暖中性色阶。想更浓就往橙红方向推色相,想更淡就把彩度往灰收。WARM_GRAY:行内代码 / 滚动条用的暖灰。WARM_ACCENT:强调色(发送按钮、滑杆、链接)。WARMTH_LEVELS/TEXT_LEVELS/TINT_LEVELS/SURFACE_LEVELS:暖度、字亮、字色、面亮的档位定义。BOLD_LEVELS/HEADING_LEVELS:加粗与标题的档位定义。GLASS_MATERIAL/GLASS_CALM:玻璃的材质(斜面、厚度、压暗、回光、阅读态系数)。GLASS_WARP:玻璃的流动配方(幅度amp、周期period、帧率fps、两条摆动)。 想更明显就加amp,想更慢就加period;ampSwing/freqSwing给 0 = 只呼吸或只变形。 这一层在 Firefox 上不生效(平台不支持,见上),改了只在 Chromium 看得到。STYLESHEET里的.dpo-controls段落:输入栏控件的高度、滑杆宽度、字号等。
日常换档不用改代码:侧栏底部的「外观」面板点一下即可(记在浏览器里)。
临时试档也可以在控制台执行 localStorage['dsh-warm-surface']='2' 之后刷新
(键名见上表;清掉键就回到烧死的默认值)。
改完不用重装:直接刷新浏览器页面就会重新拉取 lib/client.js(客户端 bundle 由
webserver 按请求读盘,服务端只在 HMR 里按内容变更推送)。
预览(不实装也能看)
/mnt/d/projects/_warmtheme_shots/ 下有一套无头 Edge + CDP 的小工具,可以在不改 profile
的前提下,把这份插件的真实代码注入到真实页面里预览:
# 空态整页(暖色)
"/mnt/c/Program Files/nodejs/node.exe" cdp.mjs shot out.png --plugin "D:\\projects\\dsh-warm-ui\\lib\\client.js"
# 浅色方案
... --scheme light
# 局部放大 3 倍:--clip "x,y,w,h" --scale 3
# 点某个按钮再截图:--click "会话默认" --after 2500
# 冷色残留扫描(0 = 没有漏掉的蓝色元素)
... eval cool-audit.js --plugin ...
改完怎么确认没改坏
node tools/check.mjs # 静态:语法 / 样式归属 / 客户端插件形状 / 注入登记 / 挂载入口 /
# 变量闭包 / 契约数字 / 共享依赖 / 探针可解析 / 玻璃无动效
node tools/check.mjs --browser # 追加真机断言(需要 Windows 侧 Firefox + Marionette,见 tools/README.md)
玻璃不做动效(2026-09-24 按用户要求删掉了「流动层」,见 contract/ui-contract.md 第 6 节):
现在只有静止的折射 + 边缘光 + 表面反射。想确认
「这台引擎到底吃不吃 backdrop 里的 SVG 滤镜」,跑 tools/probes/engine-glass.js
(Firefox 不吃,所以那边只有 blur/tint),命令见 tools/README.md 第 5 节。
「UI 应该长什么样」写在一份契约里(contract/ui-contract.json 机器可读 +
contract/ui-contract.md 人读版):工具行三态的外观、行高、注入节点的登记表、
全局不变量(文档不可滚、每张样式表一份、每个节点一个)。改设计先改契约再改代码,
静态检查与真机断言会跟着契约走 —— 不用再去别处找回"原来是什么样"。
注入节点与样式表的三条规矩(都由检查脚本守)
- 每张样式表自己打
data-plugin/data-plugin-css(照 DSH 官方客户端包的写法)。 不打标的话,热重载删不掉它 —— 旧表会在<head>里越积越多,把玻璃滤镜 svg 撑出 高度,页面就能往下滚、"壁纸跑到页面下面"。 - 每个注入节点带
data-dsh-warm-owner(值包名:节点名),登记在各自的NODE_OWNERS表里;同类节点在挂载前先清扫上一代,永远只有一个。 - 页面外层的节点只能经
mountDetached/mountLayer/installStyleSheet入文档, 定位一律写内联(内联永远赢样式表,热更残留也赢不了)。
输入框在 plan 模式下变宽是怎么回事
不是本插件干的。卡片宽度由 DSH 自己的会话宽度偏好决定:
--dsh-chat-user-width = clamp(偏好, 640, 列宽 - 176) # 有偏好
= clamp(列宽 * 0.64, 680, 920) # 没有偏好
卡片宽度 = --dsh-chat-user-width + 32
偏好键是 localStorage['dsh.conversation.contentWidth'](拖会话宽度手柄才会写)。
进 plan 模式后可用列变宽,同一份偏好不再被列宽夹住,于是卡片跟着变宽。
回到默认宽度:
localStorage.removeItem('dsh.conversation.contentWidth'); location.reload();
实测数据(真 Firefox,2026-09-19):无偏好 1752 窗口 → 952px;偏好 1200、列宽 1472 → 1232px;同一份偏好、列宽 920 → 776px。
修复记录:高亮块层不能留在滚动内容里(2026-11-07)
用户报「用滚轮在主界面往下滑,会滑到没有内容的地方」。
根因不在滚动条,而在自绘选区高亮层:为了让橙框跟着滚动不掉帧,块层会被挂进
对话滚动容器(data-mode="content",见 2026-11-01 那几次提交)。一旦挂进去,块就是
滚动容器里的绝对定位后代,用的是内容坐标 —— 浏览器会把这些坐标算进
scrollHeight。而块淡出后只是 opacity:0,坐标还留在原处;只要它比「当前内容」
更靠下(选中的是很长的消息,之后消息折叠 / 切会话 / 压缩让内容变短),滚动区末尾
就凭空多出一段空白:滚轮能一直往下滚,滚到的却是一片空。
修法(lib/client.js 的 retireBoxes()):没有要画的块时,把层收回 body
(fixed 模式不产生可滚动溢出,与 NODE_OWNERS.glassSvg 同一条平台事实),并把每块
的 transform 归零;有选区时照旧挂回滚动内容,橙框跟随滚动不受影响。
验收(1920×827,真实会话):把正文藏掉模拟"内容变短",scrollHeight
修前 8,485px(被 10 个旧块顶住)→ 修后 751px(=容器高度);
探针 tools/probes/motion-retire.js 三条判据全过(层不在滚动内容里 / 藏掉正文后
不再溢出 / 有选区时仍回内容模式)。
已知依赖
ctx.theme.overrideTokens(@deepseek-ai/dsh-client-ui-theme的公开扩展点)。prompt-optimizer的.dpo-*类名(整形目标)。该类名若改名,整形段失效但暖色不受影响。.uV2eYG_tools(conversation 插件的 CSS Module 哈希类名),只用于把工具行间距收 12px → 10px; 哈希变了也只是这一条失效。- 平台约定(踩过坑,别忘):客户端半边必须导出
apply/inject—— 浏览器侧的dsh-cordis-client-runner会把每个客户端模块的导出当插件 apply,缺apply会拦住整个宿主启动 (页面只剩「Failed to load plugins」)。工具型包(preview/shared)也要写一对空实现。 - 平台约定:
cordis.patch.yml是热加载的(dsh.profile.patchReload: "live"), 加/删条目刷新页面即可;但改包的dsh.client清单(如external)要重启才会被重新读取。 - 平台能力(实测):Firefox 完全不认
backdrop-filter里的url(#…)(三种配方位移全为 0), Chromium 认。所以"玻璃底下的内容被扭 / 在动"这件事只在 Chromium 上看得见 —— 见contract/ui-contract.md第 6 节与tools/probes/engine-glass.js。
包结构(单包多插件)
以下 4 个包都随 dsh-warm-ui 一起发布;其中前三个是主包的子目录,由主包的
cordis.patch.yml 相对路径 entry 一起挂载,只有 dsh-warm-ui 需要被安装:
| 包 | 包内位置 | 说明 |
|---|---|---|
dsh-warm-ui |
仓库根 | 暖色 token 层 + 输入栏工具行整形(界面主体) |
dsh-warm-wallpaper |
preview/wallpaper |
动态壁纸(关 / 纸 / 摩尔纹) |
dsh-warm-shared |
preview/shared |
共享客户端模块:样式表注入三件套 + camelCase→kebab。被上面两个包依赖 |
dsh-warm-brandgag |
preview/brandgag |
文字彩蛋(独立,不依赖共享包) |
dsh-warm-dynbg |
preview/dynbg |
已停用:什么都不画的空壳,2026-09-19 起已从两份 patch 里摘除,只作历史保留;private: true,不会进包 |
依赖方向是单向的:warm-ui / wallpaper → shared。要声明一条依赖,三处同时改,
少一处都是「看起来能跑、换个时机就崩」:
- 代码:工厂体里
require("dsh-warm-shared")(浏览器侧靠它拿实现); - 本包
package.json的dsh.client.external(宿主侧靠它把依赖行排在本包之前); - 契约
contract/ui-contract.json的requires(静态检查靠它核对上面两处,并禁止依赖方再抄一份实现)。
node tools/check.mjs 的「共享依赖」一条就是核对这三处的;注入三件套的写法也只在共享包里核对一次。
还没做的
没有压着的结构改动了。要做新功能之前,先读 STATE.md(当前状态 + 实测证据 + 硬约束)
与 contract/ui-contract.md(UI 应该长什么样)。
No comments yet. Be the first to write one.