安装
在 DeepSeek Harness 里通过 dsh-market 安装
dsh plugin --profile web add dshmarket
或使用命令行
dsh plugin --profile web add github:Yinxe/deepseek-harness-plugins#path:/plugins/web-style
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络。请先审阅源码,并尽量锁定 commit(github:owner/repo#sha)。
截图
README
DeepSeek Harness(DSH)Web 外观定制套件:8 套主题画廊(每套 133 个 --dsw-* token 全量映射)+ 3 个可交互背景效果(官网同款字符点阵字标 / 纯 CSS 极光辉光 / 程序化流场)+ 设置页一键切换并持久化;另有 壁纸取色(Material You / MD3) 一键生成整套动态配色(可导出 MD3 令牌)与 全局圆角 三档。
设计原则:与官方外观系统同轨,不做对抗。不注册自定义主题 id,而是把每套主题作为官方
overrideTokens覆盖层叠加在官方亮/暗之上——主题自身 scheme 分支填主题值,对侧分支填官方原样值,因此官方「外观」行(浅色/深色/跟随系统)永远合法可用,「回到官方」= 撤销覆盖层。主题 token 单源src/host/themes/,由 Host 经同源路由下发,client 只存卡片 meta;运行时零依赖。
安装
方式一(推荐):克隆 monorepo 安装 —— 更新只需 git pull,旧版 DSH 可 checkout tag
# 1. 克隆 monorepo(lib/ 构建产物已提交,clone 下来就能用,无需 build)
git clone git@github.com:Yinxe/deepseek-harness-plugins.git
cd deepseek-harness-plugins
pnpm install
# 2. 本地安装到 profile(路径按你执行命令时的 cwd 解析)
dsh plugin --profile web add ./plugins/web-style
# 3. 重启生效
dsh web
- 更新:仓库内
git pull+dsh web(最快——lib 已提交,未改 src 无需 build);兼容旧版 DSH:按compat.json的 taggit checkout <tag>后重跑上面的 add。
dsh plugin 会把包写进 profile 的 dsh.profile.bundles —— 无需手动改配置文件。
从旧包
@dshp-inx/custom-ui换过来:先移除旧挂载再装新包,否则两个插件会注册同一个设置节(WEB_DUPLICATE_PROVIDER):dsh plugin --profile web remove "@dshp-inx/custom-ui" # 旧包(dsh-custom-ui) dsh plugin --profile web add ./plugins/web-style dsh web配置不自动迁移:本插件只读 profile 条目
dshp-web-style的config:。要把旧包的选择带过来,手工把dshp-inx-custom-ui段改名即可(字段同名,缩进块原样搬运):# 改名前(旧包,不再被读取) 改之后(本插件读取) dshp-inx-custom-ui: dshp-web-style: themeId: "supabase-dark" themeId: "supabase-dark"
改源码后:src/host 或 src/client 改完跑 pnpm --filter @dshp/web-style build 重新打出 lib/host.js + lib/client.js,然后 dsh web 重启(client 半强刷页面即可)。
验证:设置 → 外观定制 能看到主题画廊;点任意卡片应立即换色并显示「正在使用」。
一键 AI 安装:把下面这段发给你的 DSH AI 即可:
帮我安装 Web 外观定制插件(monorepo Yinxe/deepseek-harness-plugins,子目录 plugins/web-style,包名 @dshp/web-style):
1. git clone monorepo 并 pnpm install
2. dsh plugin --profile web remove "@dshp-inx/custom-ui"(旧包 dsh-custom-ui,装着才需要;旧 settings 段请手工改名为 dshp-web-style)
3. dsh plugin --profile web add ./plugins/web-style
4. dsh web 重启,确认无报错,设置页「外观定制」出现主题画廊即成功
方式二:从 Release 安装(无需 clone;更新需手动重跑命令)
latest 滚动版(跟随 main 最新构建;滚动更新 = 重跑同一条 add 命令):
dsh plugin --profile web add \
https://github.com/Yinxe/deepseek-harness-plugins/releases/download/latest/dshp-web-style-latest.tgz
dsh web # 重启生效
历史版本(兼容旧版 DSH)
main 永远跟随最新 DSH。老版本 DSH 用户装静态历史版本:按 compat.json 记录的兼容 tag,到 Releases 找对应版本 Release(v* tag 触发,静态存档、永不滚动),资产名 = dshp-web-style-<tag>.tgz:
# 以 v0.1.5-rc.1 为例
dsh plugin --profile web add \
https://github.com/Yinxe/deepseek-harness-plugins/releases/download/v0.1.5-rc.1/dshp-web-style-v0.1.5-rc.1.tgz
dsh web # 重启生效
更新
三种安装方式对应的更新方式
| 安装方式 | 更新命令 | 说明 |
|---|---|---|
| 方式一 clone(推荐) | 仓库内 git pull + dsh web |
最快;未改 src 免 build;旧版 DSH 用 git checkout <tag> |
| 方式二 Release | 重跑同一条 add 命令 + dsh web |
URL 直装对 update 免疫;两个 pnpm 坑见根 README |
| 方式三 git 依赖 | dsh plugin --profile web update @dshp/\<pkg\> + dsh web |
一条命令;git 解析约 35s/插件(实测) |
功能
| 部分 | 内容 |
|---|---|
Host(src/host/ → lib/host.js) |
用官方 ctx.settings + schemastery 持久化到 profile 条目 dshp-web-style 的 config:(0.1.7 条目存储:themeId / backgroundId / photoPalette / radius.global);主题目录全量 token 下发(单源 src/host/themes/,白名单从目录动态派生 + 虚拟 photo:custom);配置只认 dshp-web-style(不读历史 key、不迁移);退役字段 wallpaper.* / glass.* 仅不透明透传保留。 |
Client(src/client/ → lib/client.js) |
「设置 → 外观定制」一张页面:顶部背景效果四选一 + 圆角三档 + 回到官方;中部壁纸取色(上传 → seed → 5 组 ref 调色板 × 亮/暗角色预览 → 启用/复制 MD3/清除);底部主题列表(当前态条 + 关键词搜索 + 亮/暗过滤 + 自适应 grid 色卡,点击即切)。启动时读 Host 快照恢复覆盖层、背景效果与圆角;theme/change 事件驱动「使用中」徽标与壁纸渐变实时跟随,canvas 类背景同时重挂一次(配色是 mount 时烘进 JS 的,不重挂官方亮/暗一切就陈旧)。背景效果登记在 background.ts 的 BACKGROUNDS 表里,与主题正交(见背景效果层)。UI 全部使用官方 dsw-alias-* token。 |
| 同源路由 | GET /ext/dshp-web-style/state(偏好快照)、GET /ext/dshp-web-style/themes(目录全量 token)、POST /ext/dshp-web-style/theme(切主题)、POST /ext/dshp-web-style/config(背景效果 / 圆角 / 取色补丁),全部先过同源校验(Origin 与 Host 一致或缺失才放行),no-store,body 上限 1MB。 |
| 工具 | 无(纯 UI 插件,不给模型注册任何工具)。 |
主题清单
label / desc / swatch 以 src/host/themes/*.ts 的 meta 为准(token 同源此处,client 只存 meta)。
| 主题 id | 模式 | 风格 | 来源 |
|---|---|---|---|
claude-parchment-light |
纯亮 | 羊皮纸 #f5f4ed + 赤陶 #c96442,ring 型深度 | claude |
xiaohongshu-light |
纯亮 | 米灰 #f5f5f5 + 种草红 #ff2442 | xiaohongshu |
levels-light |
纯亮 | 米纸 #fbf7ef + 代谢绿 #2f8f46 | levels |
arc-light |
纯亮 | 蜜桃 #fdf3ec + 珊瑚 #ef4a4a | arc |
sakura-light |
纯亮 | 樱白 #fff9fa + 樱粉 #e75480 | 原创 |
luxury-dark |
纯暗 | 曜石 #080706 + 鎏金 #c6a15b | luxury |
supabase-dark |
纯暗 | 墨黑 #171717 + 翡翠绿 #3ecf8e | supabase |
harness-office |
纯暗 | 曜黑 #0a0a0a + 官网蓝 #6799fe · 默认点阵字标 | deepseek |
harness-office 是目录里唯一带默认背景效果的主题(点阵字标)。背景效果与主题正交:
任意主题都能在设置页单独挂 aurora / flow,harness-office 也可以选「不挂」——见背景效果层。
另有一套虚拟主题 photo:custom:壁纸取色(MD3 运行时生成,不在目录内,但属于合法持久化值,Host 白名单放行)。
每套 134 个 --dsw-* token 全量映射:背景 / 边框 / 品牌 / 按钮 / 交互态 / 文字 / 语义色 /
diff 视图 / 文档预览 / Markdown / 滚动条 / 侧栏 / 气泡 / 浮层 / 阴影 / 字体栈。
(diff 与文档预览三组 11 个键来自 0.1.7-rc.1;toast 文字色 1 个键来自 0.2.0-rc.2。
官方其余新增键有意不覆盖——多数是 var(--dsw-alias-*) 别名,会继承本主题已覆盖的值自动生效。
派生规则与逐条取舍见 src/host/themes/shared.ts 头注释。)
scripts/check-themes.mjs 强制全部主题覆盖同一套 token 名(多写少写都报错),
并校验 official.ts 与 DSH 实际值一致 —— 后者是覆盖层的「对侧 scheme 无操作原值」,
漂移会导致选浅色主题再切深色时把 DSH 的旧配色涂回去。基线由
node scripts/sync-official.mjs 从已安装的 DSH 重新生成(快照落在
scripts/dsw-alias-baseline.json,CI 离线比对)。
侧栏选中行跟随品牌色
--dsw-specific-sidebar-nav-item-active 是侧栏选中行的胶囊底(DSH 的 .navCell.active,
画在 --dsw-specific-sidebar-fill 之上)。约定值固化为:
rgba(<该主题 --dsw-alias-brand-primary 的 RGB>, 0.22)
原来 33 套里 21 套与 nav-item-hover 的感知色差 ΔE < 5(其中 8 套完全相同、
3 套与侧栏底色几乎无差),选中态基本看不出来。改成品牌色叠色后:
| 指标 | 改前 | 改后 |
|---|---|---|
| ΔE(active, hover) < 5 的主题 | 21 / 33 | 0 / 33 |
| active 与 hover 完全相同 | 8 / 33 | 0 / 33 |
| 最小 ΔE(active, hover) | 0.0 | 6.6 |
| 最小 ΔE(active, 侧栏底色) | 0.0 | 11.3 |
最小 label-primary 对比度 |
8.94 | 8.20(仍远超 AA 4.5) |
check-themes.mjs 固定住这条派生关系;壁纸取色主题(src/client/md3.ts)同样走
withAlpha(brand, 0.22)。改主题品牌色后必须同步这个值,否则 pnpm test 会失败。
背景效果层
配色之外的一层可交互背景。三个效果登记在 src/client/background.ts 的 BACKGROUNDS 表里,
设置页顶部可单选,持久化字段是 backgroundId:
| 效果 id | kind |
铺法 | 代价 |
|---|---|---|---|
harness-dots |
canvas | 顶部字标带(0.56 视口高) | 逐点采样 + 30fps 逐帧绘制 |
aurora |
dom | 铺满视口 | 零(不建画布、不起帧) |
flow |
canvas | 铺满视口 | 720 游子 × 每帧约 3 次三角 |
kind 就是能力位:dom 类效果根本不创建 canvas、不绑 mousemove/resize、不起 rAF,
零帧成本是结构性的而不是优化出来的。
为什么动效不进 token 层:token 契约要求每套主题覆盖同一批 133 个键(check-themes.mjs 拿
THEME_CATALOG[0] 比对),给单套主题加「动效开关」会直接破坏契约——src/client/themes.ts 的画廊
meta 也被同一脚本用严格正则咬着字段集合与顺序,加不了字段。所以配色层仍由 Host 全量下发,
背景层完全活在客户端:backgroundId 是空串时按主题 id 的默认映射 THEME_DEFAULT_EFFECT
(当前只有 harness-office → harness-dots)解析,显式选过就一直跟着选——切主题不会重置背景,
选背景也不会碰 token 覆盖层。
挂载位置:body 末尾一个 position:fixed; z-index:0; pointer-events:none 的舞台,#root 抬到
z-index:1。但光这样什么都看不见——DSH 从 #root 往下还有若干级实心壳面挡在舞台上面。
所以挂载时按 computed style 认壳面,只看几何、两类:
| 档 | 判据 | 抓的是谁 |
|---|---|---|
shell |
宽、高各 ≥ 40% 视口 | 外层 frame、主视图 root(都吃 --dsw-alias-bg-base) |
rail |
贴左缘(left ≤ 16px)+ 高 ≥ 40% 视口 + 宽 ≤ max(380px, 30% 视口宽) |
侧边栏那一列,展开与收起成 56px 轨道都命中 |
命中就把它的背景压到 SHELL_KEEP = 0.6:只改 alpha、颜色取该元素自己的计算值,不引入任何新色,
官方亮/暗切换时它自己跟着走;比 0.6 更实心才夹,本来就比它透的(主题把侧栏画成
rgba(...,0.55))不加浓。侧栏单独立一档是因为它够不着 shell 的宽度门槛(240 ÷ 1500 ≈ 16%),
漏掉它就是「主区透了、左侧一整条仍然实心」。气泡 / 卡片 / 代码块这类小面刻意不动(那些是要读字的实心面)。
两级叠乘后舞台只剩 0.4 × 0.4 ≈ 16% 通透度——这个数字是下面整套标定的前提。
MutationObserver 只观察 childList,跟着 React 重建补压,自己写的 inline 样式不会回流自激。
颜色全部从当前 token 现读(--dsw-alias-brand-primary 经 color-mix 分档、字体栈做字标),不写死色值
——这正是背景能与主题正交的前提。
职责边界:预算在舞台、铺法在效果。通透度是舞台属性(正文对比度是三个效果共享的代价,
不能让单个效果擅自调深),节拍/30fps 上限/dpr 上限/停帧/去抖/压壳/收回也全在舞台,
效果只实现 mount + 可选 draw,只管自己的几何与配色。由此得出一条硬约束:
画布永远不许铺满底色——舞台不清屏是给 flow 留拖尾的自由度,谁铺了底色谁就把别人的层盖住了。
harness-dots:官网点阵的移植与标定
官网首屏那块跟随鼠标的点阵背景(HeroDigitile:React Three Fiber + 自定义 GLSL 的 instanced 点阵),
本插件移植成 src/client/background-dots.ts,逐粒子数学逐段照抄,但降级为 Canvas 2D——仓库红线是
运行时零依赖,不能为背景引入 three/r3f。四处等价改写:instanced Box → fillRect +
globalCompositeOperation='lighter'(= 站点 AdditiveBlending)、透视投影 → 相机距离 18 的一阶除法、
组旋转只留 x/y 微量视差(去掉 rotation.z 自转)、丢弃滚动散开(应用内自行滚动,window.scrollY 没有语义)。
亮度预算怎么花:16% 通透度意味着站点原样的点阵在这里几乎读不出来,所以这层的每一档都按
「背景层」重标:极光收进顶部字标带(铺满视口会把整页底色抬起来,实测正文区抬 11 级、
反差点阵被自己抹平);画布遮罩在字标高度内保持全通(线性淡出会把整个词标吃掉一半);
点宽从站点的 1/3 格放到 0.7 格、逐点随机缩放从 0.5..1.5 收到 0.85..1.15(字标只有 9 行点,
疏密差一大笔画就断成麻点);光照只乘进颜色一次(原先 alpha 里还乘了一遍 min(vLight,1),
等于双重暗档衰减,左半行直接掉出可读区间);光源从站点打立体块的右上斜光收到中上并抬 z
放缓衰减。合起来舞台出光量约 5.4×,而无气泡正文的对比度只掉 5%(16.2:1 → 15.4:1,
压在点阵上的那一段 5.34:1 → 5.30:1)。嫌弱先调 STAGE_GAIN(当前 1.4,已接近饱和),
嫌整层太透再调 SHELL_KEEP——后者同时决定正文还剩多少对比度,别越过 0.55。
「极光收进字标带」和画布遮罩是这个效果自己的 <style>:它们是同一条可读性论证的两半,
不住在舞台里。独立那个 aurora 铺满视口,因为它底下没有 competing 的点阵,抬 11 级那条不适用。
与站点的有意偏差(都写在各常量的注释里):网格 60 → 90(源是文字不是立体块,
一行七个字母要落在「字高约 9 行点、笔画约 3 行点」的 LED 屏比例上才读得出字)、
点宽 1/3 格 → 0.7 格、逐点缩放 0.5..1.5 → 0.85..1.15、光照从右上斜射改成中上并放缓衰减
(斜射光会把字标左半压暗)、uLight 只进颜色不进 alpha、极光收进字标带、
鼠标 distort 5 → 1.1 rad 与 strength 0.8 → 0.5(实心图形打散后还是那团,字标会被整个甩飞)、
光照暗端 shadeMin 0.28 → 0.7(背景层要留得住字形)。
aurora:零帧成本那一档
kind: 'dom'——三层 radial-gradient blob + @keyframes 漂移 + mix-blend-mode: screen,
铺满视口,颜色从 --dsw-alias-brand-primary 经 color-mix 分三档现读。不建画布、不绑监听、
不起 rAF,所以它是「想要背景但一点帧都不想花」的那一档;代价转给了合成器。
prefers-reduced-motion 下动画名置空,退化成三层静态辉光。
flow:720 条流场游子,不是粒子系统
速度场是解析正弦叠加(curl-of-sines:sin(x·k₁+t·s₁) + cos(y·k₂−t·s₂) + 0.5·sin((x+y)·k₃+t·s₃),
每子每帧约 3 次三角),积分后环面取模回绕:无生灭、无对象池、无数组增删 ⇒ 点数逐帧恒定、无 GC 抖动。
鼠标是每子 O(1) 的切向涡流,不做邻域查询。拖尾不 clearRect,帧首用 destination-out 洗掉已有像素
FADE = 0.042(≈ 0.8 s 余晖)的 alpha——只降透明度、不铺颜色,画布该透明的地方继续透明
(铺一层实色等于在两级壳下面再加第三张壳,见上面那条硬约束)。按 6 个颜色分桶各发一条批量
stroke,位置存 Float32Array 双缓冲(当前 + 上一帧)。
刻意不做真粒子系统(变点数 spawn/kill、Verlet、斥力):点数波动 = 成本尖峰;需要历史才长成样子,
reduced 单帧会画出一坨出生点、得专门写暖机;生灭簿记不产生观感。真粒子的收益(涌现)在背景层
用途上是负资产。而流场是确定性函数——场对 t 统计稳态 + 回绕有界 ⇒ 任意单帧都是代表帧。
代价与可控性(全在舞台,效果改不了):30fps 上限(站点同款节拍修正)、dpr 上限 1.5、
页面隐藏即停帧、resize 去抖只重算映射;prefers-reduced-motion: reduce 下 canvas 类只画一帧
装配完成的静态成品,不挂监听、不起 rAF,dom 类把动画名置空。
可逆性:mountBackground() 内部收回上一层、再挂新的一层,每个效果的 disposer 由舞台按
严格反序调用:停帧解绑 → 效果自己的收回 → 还原壳面 → 摘画布/舞台/自注入样式 → 还原 #root。
#root 与每一块壳面的 inline 背景逐项还原成首次记录的原值(首次记录、重复扫描不覆盖,
所以第二次之后不会把改过的值当成原值存下来)。切背景效果、切主题、点「回到官方」、插件停止,
四条路径都走同一个 disposer。
跟 DSH 的耦合面(客户端更新后会不会失效):这层只认三样东西——挂载点 id #root、
两个 token 名(--dsw-alias-brand-primary 取色、--dsw-font-base-16-font-family 取字标字体栈)、
以及「alias token 会落到某块实心壳上」这个事实。刻意不认 CSS Module 的哈希类名
(pI_x6G_frame 这类每版都变),壳面识别只吃 computed style 的 backgroundColor + 视口占比(侧栏那一档再加
「贴左缘 + 一条窄列」的几何判据),探针从命中点向上走到 #root 为止,中间隔几级、顺序怎么变都不影响。所以:
- DSH 换构建 / 改类名 / 加一层壳 → 照常生效(识别与层数无关)。
- 改这两个 token 名 → 不报错,只是失色:品牌色读不到就退成中性近白端点,字体栈读不到就退成
system-ui,字标仍在。 - 把壳面从
background-color换成background-image/backdrop-filter→ 识别不到,症状是 「极光和点阵整个不见了」。自查两条命令:!!document.getElementById('dshp-ws-ambient')与 那两级壳面的backgroundColor;前者 true 后者实心,就是这里要补探针或SHELL_PROBES。 - 官方 token 键集变了 → 不会静默错位:
scripts/check-themes.mjs拿 DSH 快照比对 133 键契约,scripts/sync-official.mjs负责把官方基线同步进来,红就是红。 - 注意安装形态:web profile 的
node_modules/@dshp/web-style是指向仓库的 symlink(改完刷新即生效), 桌面端 profile 里是拷贝安装的一份。仓库更新后桌面端不会自己跟上,得重装该 profile 的插件依赖。
衡量口径:check-themes.mjs 除 token 契约外还咬一条——Host 的 BACKGROUND_IDS 白名单里每个 id
都必须出现在 lib/client.js 产物里(登记表漏 import、或 id 在两端拼写不一致,都会红)。
配置项
配置全部落在 profile 条目 dshp-web-style 的 config:(0.1.7 契约:Host 导出条目 Config + volatile 字段,设置页经 settings.update 写入即热更新;手工编辑条目 config 重启生效):
dshp-web-style:
themeId: supabase-dark # 空串 = 完全跟随官方「外观」(system/light/dark)
backgroundId: flow # 空串 = 跟随主题默认;harness-dots / aurora / flow
radius:
global: 0 # -1 跟随主题 / 0 全锐角 / 1–24 统一圆角(px)
# 壁纸取色:未取色为 null;accent 恒等于 seed(旧数据的 seed 字段也认)
photoPalette:
accent: '#8b5cf6'
companionA: '#5cf6c8'
companionB: '#4a3a8b'
- 切换主题只需改
themeId(白名单外的 id 一律视为空串 = 回官方默认)。 - 背景效果
backgroundId与themeId各存各的:改主题不会动它,改它不会动主题。 白名单外的值同样消毒成空串 = 跟随主题默认(harness-office→ 点阵,其余 → 无)。 - 删除持久化:把
themeId置空串即完全跟随官方;再清photoPalette即丢掉取色数据。 wallpaper.*/glass.*是已退役特性的历史字段,插件只原样透传、不再读写,可手动删除。
代码结构
web-style/
├── package.json # @dshp/web-style,dsh.bundle.patch + dsh.client.platform=web
├── tsup.config.ts # host ESM + client CJS 双 bundle(loader 壳由 shared/tsup.preset.ts 拼出;运行时零依赖)
├── cordis.patch.yml # bundle 挂载行(id: dshp-web-style)
├── src/
│ ├── host/ # Node 半:settings 持久化 + 同源路由 + 主题目录
│ │ ├── index.ts # apply 装配:Config 导出 → configure → 4 条路由
│ │ ├── types.ts # StyleConfig / PhotoPalette / RadiusConfig / 补丁类型
│ │ ├── config.ts # NS / DEFAULT_CONFIG / ConfigSchema / sanitize*(不做迁移)
│ │ ├── http.ts # sameOrigin / json / readBody / settingsNamespace
│ │ └── themes/ # 主题 token 单源
│ │ ├── index.ts # THEME_CATALOG / THEME_IDS(expand)
│ │ ├── shared.ts # 字体栈 / fillFontTokens / FLAT_SHADOWS / 类型
│ │ └── <8 个主题模块> # 每文件导出 { dark?, light?, meta }
│ └── client/ # 浏览器半:设置节 UI + 覆盖层 + 背景效果层
│ ├── index.tsx # 只导出 inject / apply(loader 壳由构建预设拼出)+ apply 装配
│ ├── GallerySection.tsx # 外观定制页(背景效果 / 圆角 / 壁纸取色 / 画廊,普通函数组件 + JSX)
│ ├── apply-theme.ts # 覆盖层:buildPair / applyThemeChoice / applyBackgroundChoice / 启动恢复 / 卸载
│ ├── background.ts # 背景效果舞台 + 登记表 BACKGROUNDS(预算/收回在这层)
│ ├── background-dots.ts # harness-dots:官网同款字符点阵字标(canvas)
│ ├── background-aurora.ts # 极光层工厂 + aurora 效果本体(dom,零帧成本)
│ ├── background-flow.ts # flow:720 条流场游子(canvas)
│ ├── md3.ts # Material You 引擎(seed → 调色板 → sys → --dsw-*,含 MD3 导出)
│ ├── official.ts # 官方 alias token 原值(对侧「无操作覆盖」用)
│ ├── api.ts # /ext/dshp-web-style/* fetch 封装(themes 缓存一次)
│ ├── themes.ts # 画廊 meta(8 条,与 Host 目录逐项一致)
│ ├── state.ts # 模块级会话状态
│ ├── radius.ts # 全局圆角三档(语义属性选择器)
│ ├── clipboard.ts # 复制 MD3(Clipboard API + execCommand 回落)
│ ├── types.ts # 路由协议 / 服务类型
│ └── styles.module.css # 画廊 CSS(CSS Modules,只走 --dsw-* token)
├── scripts/check-themes.mjs # 目录 / client meta / 构建产物一致性校验(pnpm test 内跑)
└── lib/ # 构建产物(已提交):host.js + client.js
新增一套主题(三步)
src/host/themes/加一个模块(照抄现有文件:export const dark/light = fillFontTokens({...}, FONT)+export const meta: ThemeMetaMap = {...});src/host/themes/index.ts加import * as x+...expand(x)(顺序 = 画廊展示顺序);src/client/themes.ts加一条 meta(id / colorScheme / label / desc / swatch)。
然后 pnpm --filter @dshp/web-style build && pnpm --filter @dshp/web-style test:scripts/check-themes.mjs 会校验两处 meta 逐项一致、id 唯一、client bundle 未内联 token,不一致直接非零退出。
新主题不需要动背景那套:
backgroundId与主题正交,不写进src/client/themes.ts的 meta (那里字段集合被check-themes.mjs用严格正则咬着,加字段直接 FAIL)。只有当一套主题确实该 默认带某个效果时,才在background.ts的THEME_DEFAULT_EFFECT里加一条themeId → effectId。 加效果本身是四步:写一个background-<id>.ts实现BackgroundEffect→BACKGROUNDS注册 →src/host/config.ts的BACKGROUND_IDS加同名白名单 →build+pnpm test(脚本会两端比对 id)。
常见问题
- 与其他主题类插件并存报
WEB_DUPLICATE_PROVIDER:动态插件版(会话内cordis_define)与静态挂载版、或旧包@dshp-inx/custom-ui与新包同时挂载都会撞。只保留一个:cordis_undefine动态版,或dsh plugin --profile web remove旧包。 - 切换后重启回退到官方配色:说明
POST /theme保存失败(Host 未起来 / settings 服务不可用)。页面顶部会给出红字提示;检查 profile 条目dshp-web-style段落、以及dsh web日志里有无[dshp-web-style]报错。 - 壁纸取色重启后丢失:取色数据只存 seed(
photoPalette.accent)。配色生效但保存失败时会提示「重启后会丢失取色」。 - 点主题没换色:静态主题 token 需从 Host
GET /themes拉取;Host 半没起来时控制台会打印「主题 token 下发失败」。 - 圆角没生效:圆角只改「语义面」选择器(
[class*="_card"]/_panel/_bubble+ 通用控件),圆/胶囊类元素刻意保留。 - 背景看不到:先在 Console 里确认两件事 ——
!!document.getElementById('dshp-ws-ambient')(舞台在不在)和getComputedStyle(document.querySelector('#root > div div')).backgroundColor是不是带 alpha 的(壳面有没有被压薄)。选harness-office走「跟随主题」时它默认是点阵,其它主题默认是无——不是坏了,去设置页顶部显式选一个效果。另外harness-office是纯暗主题,官方「外观」行设成浅色时覆盖层走的是对侧官方原值,浅底上自然没有对比——设成深色即可。系统开了prefers-reduced-motion时 canvas 类效果是一张静态成品图(不动、不跟鼠标),这是刻意的。 - 主区透了、左侧边栏还是一整条实心:同一套压壳机制,侧栏走的是
rail档(贴左缘 + 纵向铺满 + 窄列),收起成 56px 轨道也算。它只按几何认列,所以把侧栏拖到 380px 以上(且不到视口 40% 宽)就会被判成「不是侧栏」——那是刻意留的误伤护栏。 - 换了主题背景就没了:不会——
backgroundId与themeId各存各的(条目config:里两个平级字段)。真丢了的话先确认选的是「跟随主题」还是某个具体效果:前者在切到无默认映射的主题时本来就该消失,后者会一路跟着。 - 舞台在、极光是透的,但字标糊成一团麻点:几乎一定是
GRID与DOT_RATIO被改过——这两个常数要成对调(点宽 ÷ 格宽 ≈ 0.7 才连得成笔画),单独加密网格会把笔画打断。整行偏暗改STAGE_GAIN。 - 觉得背景碍事 / 想省电:设置页顶部选「跟随主题」(非 harness-office 主题即无背景)或直接把效果换掉,整层会精确收回(舞台节点、自注入样式、监听、rAF 全部撤销,
#root与被压薄的壳面 inline 背景逐项还原)。插件停止同样走这条路径。想零帧成本就选aurora(kind: 'dom',结构上不建画布不起循环)。强弱有两个旋钮:SHELL_KEEP(整层通透度,同时决定正文对比度,别越过0.55)与STAGE_GAIN(只调点阵亮度)。
卸载
dsh plugin --profile web remove "@dshp/web-style"
dsh web
再删掉 profile 条目dshp-web-style 段(可选),并把官方「外观」行设回你想要的状态。插件不写任何自有文件、不装运行时依赖,卸载即干净。
移植说明
本插件由独立仓库插件 @dshp-inx/custom-ui(~/.dsh/plugins/dsh-custom-ui,纯 JS) 移植而来,按本 monorepo 规范重写为 TypeScript ESM,并更名为 @dshp/web-style。
| 原 JS | 新 TS | 说明 |
|---|---|---|
lib/index.js(269 行) |
src/host/index.ts + types.ts + config.ts + http.ts |
Host 逻辑逐行等价;配置只认 dshp-web-style,未保留旧命名空间迁移 |
lib/themes/index.js |
src/host/themes/index.ts + shared.ts |
expand() / 顺序 / meta 全等,补类型(ThemeModule / ThemeCatalogEntry) |
lib/themes/<name>.js × 21 |
src/host/themes/<name>.ts × 21 |
脚本机械转换,token 值逐字未改;补 ThemeMetaMap 类型 |
lib/themes/photo.js(382 行,Host 从未引用) |
src/client/md3.ts |
双份手工同步的 MD3 引擎合并为一份:Host 半不再打包 photo 引擎(原来就是死代码),client 侧以 client.js 的实时实现为准 |
client.js(997 行) |
src/client/*.ts(x) + styles.module.css |
画廊 / 覆盖层 / 圆角 / MD3 / 官方原值按层拆分,行为逐行对齐;.tsx + JSX,样式是 CSS Modules |
scripts/check-themes.mjs |
同名脚本 | 校验点升级:目录自洽 + client meta 逐项一致 + 构建产物(id 存在、token 未内联、Host 路由与白名单齐全) |
有意的差异(其余为纯机械等价):
- 命名空间:
dshp-inx-custom-ui→dshp-web-style(settings NS / 路由前缀 / 设置节 id / 覆盖层 source 同名);不写迁移代码,旧段由用户手工改名(本插件首个版本,无历史用户)。 - 内部 token 改名:
--dshp-cu-body-gradient→--dshp-ws-body-gradient(仅插件内部使用,不影响用户配置)。 - MD3 导出注释:
exported by dshp-inx-custom-ui→exported by dshp-web-style。 - 删除死代码:client 画廊 meta 里从未使用的
group字段、CSS 里从未引用的.tg-photoSwatch、findTheme()中 photo 分支(调用点早已被PHOTO_ID分支拦掉)。 - lint 修正:
img.onload/onerror→addEventListener;new Array(12).fill(null).map()→Array.from({length:12});neobrutalism 未使用的FLAT_SHADOWSimport 移除。行为不变。 - client 形态对齐仓库新规范(机制重构,行为逐字节不变):
index.ts的手写 loader 壳删除,改由shared/tsup.preset.ts的 banner / footer 拼出;GallerySection.ts的React.createElement→GallerySection.tsx+ JSX(createGallery(React, P)工厂取消,改为普通函数组件);styles.ts的 CSS 字符串 + 手插<style>→styles.module.css(构建期内联,类名前缀tg-去掉,局部名改 camelCase)。
等价性验证(移植时执行,脚本已删):以原 lib/themes/* 与 client.js 为基准逐项比对——23 套目录条目(含全部 token)全等、10 个 seed × {palettes, light/dark scheme, buildPhotoTokens, buildM3ExportCss} 全等、hexToHsl/hslToHex/withAlpha/extractDominant/extractPalette/buildWallpaperTheme 全等、23 条画廊 meta 全等、88 + 88 个 OFFICIAL_* token 全等。
注:移植期这份手抄的
OFFICIAL_DARK后来被查出漂移了 65/88 项(值仍停留在 DSH 0.0.x 的旧暗色调色板), 会导致「选浅色主题 → 切深色」时整片回退成旧配色。现已改为由scripts/sync-official.mjs从真实安装重新生成(101 + 101 项,0 漂移),并由check-themes.mjs常驻校验。
主题集精简
曾扩展到 33 套,但实测大多数主题的视觉区分度不足 —— 基本是「同一套布局换个色相」,
而维护成本随数量线性增长(每套 133 个 token,任何全局约定都要同步几十处)。
现精简为 7 套:claude-parchment-light / xiaohongshu-light / levels-light / arc-light /
sakura-light / luxury-dark / supabase-dark。此后追加的第 8 套 harness-office 不属于这轮取舍——
它换的是信息维度(官网同款的动效背景),不是又一个换个色相的布局。
⚠️ 明暗分布:7 套里 5 套浅色、只有 2 套深色(
luxury-dark/supabase-dark), 加上harness-office后为 5 浅 / 3 深。日常用深色模式的话,实际只有这 3 套可选。
同期修复(来自 token 审计)
OFFICIAL_LIGHT/DARK重新生成 —— 暗色分支此前漂移 65/88 项(值仍停留在 DSH 0.0.x 的旧暗色调色板), 会让「选浅色主题 → 切深色」整片回退成旧配色。现由scripts/sync-official.mjs从真实 DSH 生成 (0.2.0-rc.2 基线为 118 + 118,0 漂移),check-themes.mjs常驻校验。- 补
--dsw-alias-link—— 此前基线与主题都没有它,markdown 链接与文件提及永远是 DeepSeek 蓝。 - 补
--dsw-alias-state-business-primary/tertiary—— 此前官方 DeepSeek 蓝会渗进每套主题。 - 删死 token
--dsw-alias-line-secondary—— 全 DSH 零引用。 --dsw-alias-markdown-tag交给官方默认 —— DSH 里它是「活动标签页底色」(面不是前景), 主题按前景语义填中灰实色会把活跃标签压成灰块,故主题一律不覆盖(check-themes.mjs守着这条)。- 画廊按钮文字改为按亮度择优 —— 原先写死
#fff,在保留的这 7 套上一套都不达标 (最差 Supabase 绿仅 1.91:1);改为按 accent 相对亮度在白/近黑间择优后 8/8 达标 (后加的harness-office官网蓝上取近黑,6.8:1)。 .tg-search::placeholder换用--dsw-alias-label-tertiary—— 原用的label-quaternaryDSH 并未定义。fillFontTokens支持自定义等宽栈 —— 传字符串时 markdown 代码块与--dsw-font-mono同源。
0.5.0:跟 DSH 0.2.0-rc.2 基线
- 官方基线重新生成 ——
official.ts/dsw-alias-baseline.json由 0.1.7-rc.1 的 101 + 101 项重生成到 0.2.0-rc.2 的 118 + 118 项(node scripts/sync-official.mjs,本机多版本共存时用DSH_THEME_BUNDLE=<...>/dsh-client-ui-theme/lib/client.js指准版本,否则脚本会挑到 profile 里的旧 DSH)。 不重生成的话,主题覆盖层的「对侧 scheme」分支会继续下发旧官方值。 - 补
--dsw-alias-toast-label(契约 133 → 134) —— 0.2.0-rc.2 才把 toast 文字色抽成 token, 官方默认是静态白(--dsw-static-neutral-bluish-00),而官方注释假定 toast 面「两档都深」; 本套主题的浅色档把toast-bg定成浅面(白 / 象牙 / 米色),此前是白底白字。 现在每套主题显式取自己的label-primary(壁纸主题取 M3 的inverseOnSurface),check-themes.mjs新增「必须定义 + toast 面/字 ≥4.5:1」双重守卫。 - 其余 16 个新增键有意不覆盖 —— 分三类:a) 官方值本身是
var(--dsw-alias-*)别名 (settings-card-fill/-stroke、turn-trigger-bg(-hover)、tooltip-key-bg),CSS 自定义属性惰性求值, 主题覆盖层一改它们自动跟随,再写一遍只会多出第二份真相;b) 语义上不该被主题改写的静态色 (bg-document-selection固定 40% blue-500,用于保留文档原色的 PDF 选区;onboarding-*属桌面首启引导); c) 扫光/工作态(label-shimmer、label-deep-diving(-shimmer)),官方是中性色叠 alpha,叠在任意主题上都成立。 逐条理由写在src/host/themes/shared.ts头注释。 bg-document-preview亮档语义变化(本主题有意不跟) —— 官方 0.2.0-rc.2 把亮档文档预览画布 由深改浅(bluish-100 + bluish-700 状态字),暗档不变。本套主题保留「预览面板恒为深色画布」, 属于设计取舍而非基线漂移。
免责声明
主题配色参考各品牌公开的设计系统(Claude / 小红书 / Levels / Arc / Supabase / Luxury / DeepSeek Harness 官网 等)的公开色值,
用于本地界面外观定制;商标与品牌归属各自所有者,本项目与上述公司无任何关联。原创主题(Sakura)为本项目贡献。
harness-office 的背景效果移植的是官网首屏的算法(逐粒子数学),点阵字标由页面自己的字体栈在本地画布上现算,
不含官网的任何图片、字体或模型资源。三个效果都是程序化绘制——本插件不打包、不下载任何图片或视频素材。
本插件只改 CSS 变量与圆角,另在选定背景效果时挂一层自绘舞台(canvas 或纯 CSS,换选 / 收回即整层撤销); 不修改官方代码、不拦截或改写官方交互与数据流、不收集任何数据。背景层的鼠标坐标只在当前帧内参与点阵与流场排布, 不上传、不落盘。壁纸取色在浏览器本地 48×48 采样,图片不上传。
评论
评论存放在 GitHub Discussions。用 GitHub 账号登录后可发表评论或点表情。