DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

wupup /

wupup/dsh-custom-style

Verified

dsh web custom style plugin

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@ce67ec08

dsh-custom-style

CI License: MIT

DSH 网页端界面样式自定义插件。 在「设置」里获得一个完整的样式工作台:写全局 CSS、用鼠标点选界面元素直接改样式、覆盖设计令牌、微调界面密度,并把整套配置导出分享。

A DSH web GUI style-customization plugin. A live CSS editor, a click-to-style element picker, a design-token panel, density controls, and shareable preset packs — all in one settings page.

中文 · English

⚠️ 非官方项目。 这是一个社区插件,与 DeepSeek 官方无关,未获其背书或赞助。DSH 是 DeepSeek 的产品;本项目只针对其公开的插件机制开发。

Unofficial project. This is a community plugin. It is not affiliated with, endorsed by, or sponsored by DeepSeek. DSH is a DeepSeek product; this project only targets its documented plugin mechanism.


中文文档

功能

标签页 能做什么
全局 CSS 直接写任意 CSS,实时注入生效并自动保存。带撤销、大括号/引号/注释配对检查(写错会即时提示而不是静默失效)、以及一个「插入变量」菜单(点一下就把 var(--dsw-…) 插到光标处)。
点选元素 打开点选模式后,设置面板自动隐藏(它就是挡住页面的那一层),鼠标划过任意元素高亮边框,点击即可为它写样式规则;结束后设置面板自动恢复,选中结果已经在那里等你。顶部浮出提示条,随时可点「停止点选」或按 Esc 退出。自动生成稳定的 CSS 选择器(优先用语义化的 id / data-* 属性,跳过哈希类名),并显示当前计算值、匹配元素数量。
主题变量 列出 DSH 全部 163 个 --dsw-* 设计令牌(从已安装的 DSH 样式表里实际提取,不是猜的),按品牌色/文字/边框/背景/状态色等分组,可搜索。每个令牌都能分别设置浅色与深色两套值,改错了可以单项恢复。
界面密度 字体缩放、界面密度、全局圆角、内容宽度上限、气泡圆角五个滑块,实时预览。
预设与分享 7 套内置预设一键套用;把当前全部配置导出为 JSON 备份或分享;导入别人的配置;一键清空。

截图

待补充。欢迎在 Discussions 或 Issue 里贴出你的配置截图。

安装

插件装进 web profile(默认位置 $DSH_HOME/profiles/web,Windows 上通常是 C:\Users\<你>\.dsh\profiles\web)。

方式一:从 GitHub 安装(推荐)

dsh plugin --profile web add github:wupup/dsh-custom-style

也可以先克隆再本地 link:

git clone https://github.com/wupup/dsh-custom-style.git
cd dsh-custom-style
dsh plugin --profile web add .

方式二:手动安装

  1. 把仓库目录链接或复制到 $DSH_HOME/profiles/web/node_modules/dsh-custom-style
  2. 编辑 $DSH_HOME/profiles/web/package.json,在 dsh.profile.bundles 数组里加上 "dsh-custom-style"

方式三:pnpm link(dsh plugin 失败时)

cd $env:DSH_HOME\profiles\web
pnpm add link:<插件仓库的绝对路径>

然后手动把 "dsh-custom-style" 加进 package.json 的 dsh.profile.bundles(pnpm add 只改 dependencies,不认 dsh.profile 字段)。

让改动生效

改完 profile 后必须重启 dsh web 服务,只刷新浏览器没用:

# Ctrl+C 停掉当前服务,然后重新启动
dsh web

然后打开 http://127.0.0.1:3080 ,进入 设置 → 自定义样式(界面语言为英文时显示 Custom Style)。

为什么必须重启:DSH 在启动时扫描 profile、把每个插件的客户端 bundle 快照进内存,再通过 /plugins/<包名>/client.js 提供。服务启动之后才装的插件,这个路由根本不存在(直接 404),刷新浏览器也拿不到。

自查是否装上:

# 200 = 服务已加载本插件;404 = 需要重启 dsh web
Invoke-WebRequest http://127.0.0.1:3080/plugins/dsh-custom-style/client.js -UseBasicParsing |
  Select-Object StatusCode

不需要构建。 仓库已提交构建产物 lib/client.js,并且插件没有任何运行时依赖(浏览器侧唯一需要的 react 由 DSH shell 的模块表提供)。只有你修改了 src/ 下的源码,才需要重新构建。

与其他插件共存

本插件不注册主题,也不碰 ctx.theme。它的全部输出是一层独立的 CSS 覆盖层 + 一层 --dsw-* 变量覆盖:

  • 你正在用的换肤插件(如 dsh-dream-skin)切换皮肤后,这里的自定义依然生效
  • 反之,这里的自定义不会锁死你的主题选择
  • 插件自带的界面配色全部读取 --dsw-* 令牌,所以在任何主题下都不会突兀

覆盖层被注入到 <head> 最末尾,选择器写在 body 层级(与 DSH 内置样式表同级),靠源码顺序取胜而不是靠 !important 互相加码。点选生成的规则才带 !important,因为 DSH 组件用 CSS Modules,类选择器优先级可能更高——这也是这层规则独立成层、可被总开关一键关掉的原因。

唯一的取舍:如果你在换肤插件里也改了同一个 --dsw-* 令牌,后加载的那个赢(两者选择器同级,靠 <head> 插入顺序决胜)。想彻底避免重叠,用「点选元素」写具体的组件样式,而不是在两个地方覆盖同一个令牌。

数据存放位置

位置 用途
$DSH_HOME/custom-style.json 权威副本。宿主半通过 /custom-style/api 读写,跨 origin、跨端口重启都不会丢。
浏览器 localStorage(dsh-custom-style:state) 首屏缓存,保证刷新后立刻是正确的样子,随后被宿主副本覆盖。

为什么需要宿主那一半:localStorage 按 origin 隔离,而 DSH Desktop 每次启动绑一个随机端口,GUI 的 origin 会变,缓存就「失忆」了。宿主副本让配置与访问地址无关。

卸载插件不会自动删除 custom-style.json,可以手动删。

安全说明

  • 持久化路由 /custom-style/api 带信任围栏:只接受 loopback(或部署时配置的可信主机)的 Host,拒绝 sec-fetch-site: cross-site,有 Origin 时比对 hostname(不比对端口,否则 DSH Desktop 的随机端口下每次写入都会 403)。这是防 DNS rebinding / 跨站请求的措施,不是认证。
  • 路由只承载你自己的视觉偏好,请求体上限 16 MB。
  • 写入只接受 string / null 值,且对「读改写」重新读取文件而不是用缓存,因此两个标签页同时保存不会互相覆盖。
  • 插件不收集、不上传任何数据,没有任何外部网络请求。

从源码构建

npm run build      # = tokens + client
npm run tokens     # 仅重新提取 --dsw-* 令牌目录
npm run client     # 仅重新打包 lib/client.js
npm test           # 运行行为测试

构建不压缩、不转译,产物是可读的普通 JS——你想确认「这个插件到底往页面注入了什么」时,打开就能看。

src/client.js      插件主体:状态模型、CSS 合成、持久化、点选引擎、设置页注册
src/panel.js       React 设置面板(用 React.createElement,不依赖 JSX 运行时)
lib/tokens.json    从已安装 DSH 提取的 --dsw-* 令牌目录(生成物)
        ↓ tools/build-client.mjs
lib/client.js      最终产物:单个 window.__ModuleLoader__.load({ id, factory }) 工厂

两个源文件会被拼进同一个工厂作用域(这样组件与插件主体共享 PLUGIN_ID、store、commit、点选引擎),并注入生成的令牌目录。lib/client.js 是生成文件,请改 src/ 下的源码。

DSH 升级后如果令牌有变化,重新跑 npm run tokens && npm run client 即可。

测试

npm test

89 个测试,覆盖:模块契约、apply() 的注册与销毁、状态规范化(信任边界)、持久化编解码往返、CSS 合成、单位补全、大括号配对、选择器转义与生成、颜色归一化、预设行为、设置页的真实渲染输出、样式标签生命周期、点选引擎的启动/停止/回调/一次性模式、点选时设置弹层的隐藏与精确恢复,以及「刷新后从缓存完整还原」这条端到端路径。

测试跑的是构建产物 lib/client.js,并通过 window.__ModuleLoader__.load 这一真实契约加载——所以测试通过意味着用户实际安装的那个文件就是这个行为,而不是「源码如果按另一种方式拼装大概会这样」。

npm test 走的是 node test/run.mjs(单进程 import),而不是 node --test test/*.test.mjs。后者会为每个测试文件 spawn 子进程,在禁止打开命名管道的沙箱里会以 EPERM 失败。两种方式跑的是同一套 node:test 用例。

已知边界

  • 不猜 DSH 的私有类名。 插件不依赖任何压缩后的组件类名,因此密度滑块作用于字号、行高和 .markdown 这类语义选择器。想精细控制某个具体组件,请用「点选元素」——那正是为此设计的路径。
  • 令牌面板列出的是别名层。 改动 --dsw-alias-*(以及色板层 --dsw-static-*)会覆盖内置值,但不会重算派生令牌(阴影、高度等仍按原样计算)。
  • 点选时只隐藏设置弹层本身。 插件从自己的 DOM 位置向上找最近的 role="dialog"(不用类名——DSH 的类名是每次构建都变的 CSS Module 哈希),再沿「只属于这个弹窗」的祖先链取最外层:判定条件是兄弟节点要么是 aria-hidden 的遮罩,要么已经在链里。这既能把遮罩一起藏掉(只藏 dialog 会留下一层挡住页面的遮罩),又不会多爬一层把侧栏按钮甚至整个应用一起藏掉。找不到 role="dialog" 时退化为「不隐藏」而不是报错——点选仍可用,手动关掉设置面板再点即可。
  • 自定义 CSS 写坏了不会崩。 大括号/引号/注释不配对会即时提示,注入的是纯文本,浏览器最多忽略无效声明。

贡献

见 CONTRIBUTING.md。特别欢迎:新的预设样式包、更多语言的界面文案、以及针对新版 DSH 的适配。

致谢与出处

本项目是独立实现,没有复制任何第三方代码,但它在两个意义上站在别人的工作之上:

  • dsh-dream-skin(MIT)——一个已经成熟的手写 bundle 插件。本项目的浏览器半采用与它相同的形态(plain-CJS 工厂 + 种子探测降级 + data-plugin 标记的样式标签)。该形态最初由它实践并记录,是本项目能快速做对的原因。
  • dsh-better-sidebar——宿主半的 /sidebar 路由展示了信任围栏的写法;lib/index.js 的围栏与「比对 hostname 而非 host」这一决定沿用了它的做法。
  • DSH 官方客户端包(@deepseek-ai/dsh-client-ui-*,MIT)——api-notes.md 对 slot / store / locale / 主题令牌契约的梳理,来自阅读这些包的代码与文档。

api-notes.md 记录了 DSH 客户端插件机制的逐条契约,每条都标注了来源文件与行号。它不包含任何第三方源代码,可以自由复用。

许可

MIT


English

Features

Tab What it does
Global CSS Write any CSS; it is injected live and saved automatically. Undo, brace/quote/comment balance checking (a broken sheet tells you instead of silently failing), and an "insert token" menu that drops var(--dsw-…) at the caret.
Element picker Enter picking mode and the settings panel hides itself (it is the layer covering the page). Hover any element to see its bounds; click to author style rules for it. The panel returns the moment you finish, with the selection waiting. A floating bar offers Esc or an explicit stop. Generates stable selectors (prefers semantic ids / data-* attributes, skips hashed class names) and shows computed values plus match counts.
Tokens Lists all 163 --dsw-* design tokens (extracted from your installed DSH's actual stylesheets, not guessed), grouped by brand/text/border/background/state and searchable. Every token takes separate light and dark values, and each override is individually reversible.
Density Five live sliders: type scale, density, corner radius, content width cap, bubble radius.
Presets Seven built-in packs applied in one click; export everything to JSON for backup or sharing; import someone else's configuration; one-click clear.

Installation

The plugin is installed into your web profile ($DSH_HOME/profiles/web).

dsh plugin --profile web add github:wupup/dsh-custom-style

Or clone and link locally:

git clone https://github.com/wupup/dsh-custom-style.git
cd dsh-custom-style
dsh plugin --profile web add .

Then restart the web server — a browser refresh is not enough:

dsh web

Open http://127.0.0.1:3080 and go to Settings → Custom Style.

Why the restart matters: DSH scans the profile at startup and snapshots each plugin's client bundle in memory before serving it at /plugins/<package-name>/client.js. A plugin installed after startup has no route at all (a plain 404), and refreshing the browser cannot change that.

Verify it loaded:

Invoke-WebRequest http://127.0.0.1:3080/plugins/dsh-custom-style/client.js -UseBasicParsing |
  Select-Object StatusCode

No build step required. The built bundle lib/client.js is committed, and the plugin has no runtime dependencies — the only module it needs (react) comes from the DSH shell's frozen module table. Rebuilding is only necessary if you edit src/.

Coexistence with other plugins

This plugin registers no theme and never touches ctx.theme. Everything it emits is an independent CSS layer plus --dsw-* variable overrides, so it composes with a theming plugin (such as dsh-dream-skin) rather than competing with it.

Its own UI reads --dsw-* tokens throughout, so it looks native under any theme.

The only trade-off: if you override the same token in a theming plugin, the one loaded later wins (both use equal-specificity selectors, decided by <head> insertion order). To avoid the overlap entirely, use the element picker to style the specific component instead of overriding the same token twice.

Where data lives

Location Purpose
$DSH_HOME/custom-style.json The authoritative copy, read and written by the host half over /custom-style/api. Survives origin and port changes.
Browser localStorage (dsh-custom-style:state) First-paint cache, so a refresh is styled immediately; superseded by the host copy.

Security

  • The /custom-style/api route is fenced: loopback Host only (or a trusted authority configured at deploy time), sec-fetch-site: cross-site refused, and Origin compared by hostname rather than host — a port-sensitive check would 403 every write under DSH Desktop's OS-assigned port. This is a DNS-rebinding / cross-site defense, not authentication.
  • The route carries only your own visual preferences, capped at a 16 MB body.
  • Writes accept only string/null values and re-read the file before merging, so two tabs saving concurrently cannot clobber each other.
  • The plugin collects nothing, uploads nothing, and makes no external network requests.

Building from source

npm run build      # tokens + client
npm run tokens     # regenerate the --dsw-* token catalog only
npm run client     # rebuild lib/client.js only
npm test           # run the behavior suite

The build does no minification and no transpilation: the output is readable plain JS, so you can open it and see exactly what the plugin injects.

Testing

89 tests run against the built bundle (lib/client.js), loaded through the real window.__ModuleLoader__.load({ id, factory }) contract the DSH web shell uses — so a passing suite means the artifact users install behaves this way, not that the sources would if assembled differently.

Known limitations

  • No guessing at DSH's private class names. The density sliders act on type scale, line height, and semantic selectors like .markdown. For fine-grained control of a specific component, use the element picker — that is the path designed for it.
  • The token panel covers the alias layer. Overriding --dsw-alias-* (or the raw --dsw-static-* palette) replaces the shipped value but does not recompute derived tokens such as shadows and elevations.
  • Picking hides the settings modal and only that. The layer is resolved from the plugin's own DOM position by ARIA contract (role="dialog"), never by class name, then climbed along the ancestors that belong solely to the modal. If a future DSH build renders the panel without role="dialog", the plugin degrades to "not hidden" rather than erroring — picking still works, you just close the panel manually first.

Contributing

See CONTRIBUTING.md. Especially welcome: new preset packs, more UI languages, and adaptations to newer DSH releases.

License

MIT

—/ 5

No ratings yet

Verified DSH bundle

Commit ce67ec08af23

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout