dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
English | 中文
DeepSeek Harness 网页界面的 Frutiger Aero 皮肤 —— 玻璃、水、天空与气泡,电脑端和手机端都顺手。
一个可移植的 dsh profile 插件。它通过产品自身的 token 体系给整个 Web 界面换肤,
并且重做了窄屏布局 —— 让手机不再是被压扁的桌面端,而是一个真正的 App。
require 任何模块。安装就是复制文件;没有构建步骤,
运行时也不访问网络。
一行命令。不用包管理器、不用 registry 账号、不用构建。
macOS / Linux:
curl -fsSL https://raw.githubusercontent.com/Hotsteel2901/dsh-frutiger-aero/main/install.sh | sh
Windows(PowerShell):
irm https://raw.githubusercontent.com/Hotsteel2901/dsh-frutiger-aero/main/install.ps1 | iex
两者都会下载本仓库的一份快照,放进目标 profile 的 node_modules,并把它加入该 profile 的 bundle 列表。
除了 Node 24 或更新版本 什么都不需要 —— 不用包管理器、不连 registry、不用构建。
想装到 frutiger 以外的 profile:
curl -fsSL .../install.sh | sh -s -- --profile aero
为什么要求 Node 24,而不是以前写的 20。 Harness CLI 的入口是
if (import.meta.main),这个属性在 Node 24 之前根本没有实现。在 20 和 22 上这个判断恒为假:dsh什么都不打印,然后以 0 退出。这和"安装坏了"完全无法区分,也是绝大多数人认为这个插件装不上的真正原因。 现在安装脚本会直接拒绝在旧 Node 上运行,并一句话说清原因,而不是留给你一个静默无反应的命令。
安装脚本默认拉取默认分支,而不是 releases/latest。早期版本正好相反,由此产生了第二个更隐蔽的问题:
发布标签是在某一刻从分支上切下来的,而 package.json 一直在报告分支的版本号,
于是标签 1.0.3 和 main 都自称 version: 1.1.0,内容却不同。
结果就是重新安装永远只会拿到同一份快照,而且无从分辨。想固定到某个 ref 时:
curl -fsSL .../install.sh | sh -s -- --ref main
两个安装脚本每次运行都会重新拉取该 ref,所以想升级到最新版本,就是重新跑一遍那行命令 —— 没有任何缓存,也没有需要手动改的版本号。每次安装都会打印本次写入的版本号和构建指纹, 这就是"我装的是不是最新"的答案。
git clone https://github.com/Hotsteel2901/dsh-frutiger-aero
cd dsh-frutiger-aero
node install.mjs # 装进 `--profile frutiger`
node install.mjs --link # 或改为软链接,重新构建即时生效
然后:
dsh --profile frutiger --port 3099 --no-open
打开它打印出来的地址 —— 地址里带一次性 token,浏览器会因此拿到已认证的会话 cookie。
| 参数 | 作用 |
|---|---|
--profile <name> | 要创建或更新的 profile(默认 frutiger) |
--home <dir> | 要操作的 Harness home(默认 $DSH_HOME,其次 ~/.dsh) |
--link | 用软链接代替复制 —— 方便改插件本身 |
--print | 只解析、不写任何文件 |
--uninstall | 删除包与 bundle 条目,保留 profile 及其会话 |
--doctor | 体检现有安装,逐条列出问题与对应修法 —— 只读,不写任何文件 |
--repair | 在原地重新拷贝一份,保留 profile 及其会话 |
--json | 输出机器可读结果 |
两个安装脚本通过环境变量接受同一套设置 —— 在 irm … | iex 下这是唯一可行的入口,
因为 Invoke-Expression 根本没有 -Args 参数,那条命令行上没地方放标志位。
install.sh --help 会打印出本次运行实际会使用的取值。
| 环境变量 | 作用 |
|---|---|
DSH_FRUTIGER_REPO | 拉取的 owner/repo(默认 Hotsteel2901/dsh-frutiger-aero) |
DSH_FRUTIGER_REF | 固定到某个 tag 或 commit(默认:安装时解析出的默认分支) |
DSH_FRUTIGER_PROFILE | 要创建或更新的 profile(默认 frutiger) |
DSH_FRUTIGER_HOME | 操作的 Harness home(默认 $DSH_HOME,再退到 ~/.dsh) |
$env:DSH_FRUTIGER_PROFILE = 'aero'
irm https://raw.githubusercontent.com/Hotsteel2901/dsh-frutiger-aero/main/install.ps1 | iex
落地页会按你的系统自动选好上面这两条命令中的一条,正常情况下不需要手打。
不要删掉 profile 重装 —— 那既有破坏性,而且在真正的原因是 Node 根本跑不起 CLI 时,删了重装必然没用。 应该这样问:
node install.mjs --profile frutiger --doctor
它会告诉你到底哪里不对、为什么不对,以及修好它需要的那一条命令;只有真的有问题时才会以非零状态退出。 每次安装都会打印它写进去的 构建指纹,于是"我装的是最新版吗"有了一个不必依赖版本号的答案:
dsh-frutiger-aero: installed profile "frutiger" (copy)
version 1.1.0
build 20bfbdb4cf57
这个指纹是构建所依据源码的内容哈希,而构建是可复现的,所以它能精确标识一次构建 ——
哪怕只差一个字节的 CSS,指纹也不同。正因如此,--doctor 才能判断出你手上那份是旧的,
而不是靠一个两份不同快照恰好都在用的版本号去猜。
按浏览器调,不动安装。 插件暴露了一个小的控制面:
window.__FRUTIGER__.setEffects('lite') // 'full' | 'lite' | 'off'
window.__FRUTIGER__.setScene(false) // 只关壁纸
window.__FRUTIGER__.tier() // 当前生效档位
window.__FRUTIGER__.bubbles() // { step, steps, count }
window.__FRUTIGER__.bubbles('calm') // 'calm' | 'normal' | 'lively'
壁纸密度。 档位会按设备选一个合理的默认气泡数,bubbles() 让你在不放弃整张壁纸的前提下
否决它:
| 档位 | 效果 | 最高档设备 | 轻量档设备 |
|---|---|---|---|
calm | 约一半 | 10 | 5 |
normal | 档位默认 | 22 | 10 |
lively | 接近两倍 | 40 | 18 |
倍率是乘在档位基线上的,不是替换它,所以轻量档设备在任何密度下都还是轻量档。数量被限制在
3–48 之间。改密度会重建场景但不重排构图 —— 气泡位置来自固定种子,你选中的壁纸就是你留下
的那一张。偏好按浏览器保存,刷新后仍在;在 normal 上再点一次会清空存储值,而不是把今天
的默认值钉死。
或者直接用地址栏参数:
http://127.0.0.1:3099/?frutiger=off # 完全不要装饰
http://127.0.0.1:3099/?frutiger=lite # 保留配色与布局,关模糊,10 个气泡
http://127.0.0.1:3099/?frutiger=full # 强制最高档,跳过帧率治理
http://127.0.0.1:3099/?bubbles=calm # 安静一点的壁纸,不动已保存的设置
地址参数是在 token 校验之后读取的,所以请收藏干净的地址,
而不是 dsh 打印出来的那一条。地址参数永远优先于已保存的偏好,所以需要精确数量的截图或
问题复现,用 ?bubbles= 最合适。
只禁用这一行,保留安装。 在 <profile>/cordis.patch.yml 里:
- id: frutiger-aero
disabled: true
profile 的 patch 文件是实时重载的,不需要重启。
彻底移除。 node install.mjs --uninstall,或者在安装脚本副本上跑 install.sh --uninstall。
profile 和会话都会保留。
原生窄屏布局是挤压式的:窄于 1024px 时侧栏仍占着网格轨道, 于是 390px 手机上图标栏拿 56px,正在读的内容只剩 334px —— 而且抽屉一打开, 聊天记录只剩 108px 宽。这个插件改的是布局本身,不只是涂装。
| ≥ 1024px | 641–1023px | ≤ 640px | |
|---|---|---|---|
| 布局 | 原生三栏 | 单栏,侧栏覆盖在内容之上 | 单栏,图标栏移出画布 |
| 导航 | 侧栏 | 56px 图标栏 + 覆盖式抽屉 | 底部浮动 dock |
| 阅读宽度 | 产品默认 | 整列,上限 680px | 整列 |
在此之上还有:
visualViewport 把被遮挡的高度发布成自定义属性,
在布局视口不缩放的 iOS 上把输入框精确抬起那么多。touch-action: manipulation、惯性滚动、overscroll 收敛、去掉点击高亮、按下轻震动。viewport-fit=cover 配合抽屉、dock、输入框上的
env(safe-area-inset-*),并用 100dvh 取代 100vh。另外修掉了两个产品级缺陷(而不是绕开),因为不修的话,皮肤只是在给一个坏掉的界面换皮:
clamp(680px, …) —— 680px 下限比手机还宽,
会把每一轮消息裁成左边一条。手机上的全屏右侧面板(文件预览、diff)会保留 dock —— 阅读时切换会话仍然有用 —— 但面板会在 dock 之上收边,文件最后几行不会被浮动条挡住。
什么都没少,这层皮肤是纯增量的:
interact.mjs 每次运行都会断言这些 —— 见验证方式。
bundle patch 只插入一行,宿主端是一个空的 apply()。这一行存在的意义在于它的清单声明:
dsh.client 让 @deepseek-ai/dsh-client-modules 把 lib/client.js 作为浏览器插件
编进 window.__DSH_BOOT__。宿主端什么都没加 —— 没有服务、没有工具、没有配置、不碰会话状态。
浏览器端只负责绘制。它装的每一个钩子,要么是样式表,要么是自己创建的节点,
要么是被动监听器,要么是产品自己节点上的属性;每一项都通过 ctx.effect 释放。
把这一行设为 disabled: true 就完全恢复原样。
产品的颜色分两层 —— --dsw-static-* 原始色阶和 --dsw-alias-* 语义角色 ——
所有表面、边框和文字都走语义层。覆盖语义层就一次性给整个应用换色,
不需要写任何组件级规则;而且因为 ui-layout 正是把这些 alias 值写到 <body> 上,
它也是唯一能和内置明暗切换协作而不是打架的层。
调色板故意写两遍:
body[data-ds-dark-theme] 上的 !important 样式表 ——
级联里唯一能压过内联自定义属性的东西,这样皮肤永远不会因为加载顺序的意外而输掉优先级;theme.overrideTokens(),让运行时快照、theme-color 元数据和外观预览方块
与实际画面保持一致。src/client/palette.js 是这两者的唯一真相源。原始色阶两边共用;
只有语义别名按方案区分,因为 Aero 是一种明亮材质(明亮天空之上的玻璃),
而它的夜间形态是一种幽暗材质(深海之上的生物光)。
CSS-module 的类名是按构建哈希的(.pI_x6G_sidebarCol),拿它当选择器根本活不过一次升级。
产品真正稳定的是它自己样式和测试都依赖的语义化 data-* 词汇。
这层皮肤就是照着它写的 —— [data-rightbar-col]、[data-shell-overlay]、
[data-conversation-scroll]、[data-composer-card]、[data-chat-flow-kind]、
[data-files-row] —— 再加上 ARIA role 与元素语义。
剩下的靠一个 rAF 合并的小标注器补齐:它以 [data-rightbar-col] 的父节点作为 frame,
给每一列打上 data-fa-col,并从聊天记录往上找到第一个真正画了背景的祖先作为画布。
它只监听 childList 和三个表现属性,不监听消息列表,也永远不会看到自己写的属性。
九层图层装在一个 contain: strict 的 fixed 元素里,
所以它不参与布局、不能被滚动、也永远不会出现在命中测试里。两条规则让它足够便宜:
transform 和 opacity —— 合成器能不唤醒主线程就完成动画的两个属性;backdrop-filter 背后不放动画。场景由大面积羽化渐变构成 ——
本身就是模糊会得到的样子 —— 所以气泡移动时不会有任何滤镜被重算。整个插件里没有任何 rAF 循环,也没有 will-change 提示:
22 个被提升的图层比壁纸本身还贵,而带动画的 transform 引擎自己就会提升。
在一个不属于自己的应用里加动效,只有在开销有上限时才站得住脚,所以插件运行一套档位治理。
| 档位 | 触发条件 | 壁纸 | 模糊 | 动画 |
|---|---|---|---|---|
full | 精细指针、≥ 3 核、≥ 4 GB | 22 气泡、焦散、光束、颗粒 | 开 | 全部 |
lite | 粗指针或小屏、≤ 4 核、≤ 3 GB、省流量、减少动效 | 10 气泡 | 关 | 仅入场 |
off | 显式 ?frutiger=off | 无 | 关 | 无 |
分级只读取零成本信号(pointer、hardwareConcurrency、deviceMemory、saveData、
prefers-reduced-motion);随后插件会在启动后采样约一秒真实帧率,不一致就只降一次 ——
治理只会往下走,所以恢复性能的设备不会在「华丽」和「朴素」之间来回抖。
显式指定档位会完全关闭治理:用户要 full,那就是 full。
整个设计里唯一真正昂贵的是 backdrop-filter,它被门控两次:
低于 full 关掉,以及只用在比视口小的面板上。第二道门控是关键 ——
早期版本把它加在右侧列上,那一列在桌面是零宽轨道,但一旦框架塌成单列就铺满整个屏幕,
于是在手机上它把整个应用都模糊并降饱和了。
prefers-reduced-motion 被理解为「不要动」,而不是「不要主题」:壁纸留着,冻住。
标签页切到后台,一切暂停。
dsh --profile web,或任何基于 @deepseek-ai/dsh-base +
@deepseek-ai/dsh-web-app 构建的 profile)。内嵌 dsh Web UI 的桌面客户端会得到同一层皮肤,
因为插件针对的是产品的 token 与钩子,而不是某一个客户端的 DOM。backdrop-filter、:has()、dvh 都是优雅降级而不是直接坏掉 ——
lite 档存在的意义就是让负担不起玻璃的设备仍然拿到配色与布局。--dsw-* token 的界面都会跟着一起换色。
但如果某个插件把颜色写死,或者在整屏上铺 backdrop-filter,仍可能冲突 ——
这是真实存在的限制,不是假设。宿主端是空的 apply() —— 没有服务、没有工具、不碰会话状态。浏览器端只负责绘制。
每个副作用都通过 ctx.effect 释放,禁用这一行即可完全恢复原样。
node install.mjs --uninstall,或在 profile 的 cordis.patch.yml 里写
- id: frutiger-aero 加 disabled: true。两种方式都会保留 profile、会话和设置。
因为原生窄屏布局坏在颜色修不了的地方:390px 手机上聊天记录只有 108px 宽。 对这一点视而不见的皮肤,只是给一个坏掉的界面换了层皮。
看上面的档位表。简短版:full 档有模糊输入框、模糊弹窗和 22 个气泡的壁纸,
全部只走合成器;lite 档拿到配色和布局,完全不用滤镜。
屏幕外和后台标签页里没有任何动画在跑。
目前还没有设置界面。src/client/palette.js 是唯一真相源,
node build.mjs 从它同时生成样式表和主题服务覆盖。加一个设置行是合理的下一步 ——
见 CHANGELOG.md。
.devtools/ 用 Playwright 驱动真实页面。它不是交付物的一部分,安装时完全用不到,
但这份文档里的每一条结论都是用它量出来的。
| 脚本 | 回答什么问题 |
|---|---|
interact.mjs | 真实输入 —— CDP 触摸滑动、点击、键入、滚轮、拖拽、选择:每次运行 29 条断言,两种语言各跑一遍 |
docktest.mjs | 手机端底栏在中文与英文下:每次 24 条断言 |
trajcheck.mjs | 轨迹界面在手机端与电脑端:33 条断言(其中电脑端部分是一组反向校验,确认手机端专属处理没有渗入宽布局) |
installcheck.mjs | 不依赖浏览器的安装路径验证 —— Node 版本下限、可重复安装、--doctor、--repair、构建可复现:15 条检查 |
settingscheck.mjs | 从底栏打开设置:两种语言、手机与电脑端,23 条断言 |
aligndiff.mjs | 与原生 profile 对比,找出由皮肤引入的控件错位 |
final.mjs | 各视口下的布局、抽屉、dock、计算样式、控制台报错与截图 |
tiers.mjs | 同一页面在 full / lite / off 三档下、各视口的表现 |
lum.mjs | 渲染对比度测量,用来找只存在于像素里的问题 |
landing.mjs | GitHub Pages 落地页:桌面、移动端、减少动效下共 19 条检查 |
hit.mjs | 视口各点上的点击究竟落在哪个元素上 |
用例覆盖桌面、笔记本、平板、手机竖屏、小屏手机与手机横屏,明暗两套方案,全部档位。 当前状态:交互 29/29(两种语言各一遍)、底栏 24/24、轨迹 11/11、落地页 19/19,控制台全程干净。
install.mjs 可移植的 profile 安装/卸载器
install.sh, install.ps1 GitHub 一行安装脚本
packages/frutiger-aero/ 插件本体
package.json dsh.bundle + dsh.client 声明
cordis.patch.yml 这个 bundle 插入的唯一一行
build.mjs 把 src/css/*.css 内联进 lib/client.js
src/host.js 宿主端(故意留空)
src/client/palette.js 整套配色,唯一真相源
src/client/scenery.js 壁纸的 DOM,种子化且可复现
src/client/runtime.js 分级、标注、移动层、dock、控制面
src/css/*.css base、scenery、material、mobile、effects
lib/ 构建产物(已提交 —— 安装无需构建)
docs/ GitHub Pages 落地页
submission/ 可直接提 PR 的收录条目
.devtools/ 验证工具链
改了 src/ 下任何东西之后,重新构建浏览器端:
node packages/frutiger-aero/build.mjs
欢迎提 issue 和 PR —— 见 CONTRIBUTING.md。简短版:
先读实现原理,守住两条规则(只绘制;每个副作用都通过 ctx.effect 释放),
动了移动层就在开 PR 前跑一遍 interact.mjs。
MIT —— 见 LICENSE。
非官方社区插件,与 DeepSeek 无隶属或背书关系。