← Back to home@tuibe

dsh-deepseek-theme-studio

DSH Web GUI client theme plugin — frosted glass / fluid / quantised effects in DeepSeek's visual language. 100 adjustable parameters.

Stars
0
Language
JavaScript
Created
Oct 6, 2026
Updated
Oct 6, 2026
GitHub repo

Introduction

dsh-deepseek-theme-studio

DeepSeek 官网视觉语言的 DSH Web 客户端主题插件:毛玻璃 / 液化(流体)/ 量子化(Q1–Q4)三类效果, 全部参数可调、可持久化、可程序化调用。

  • 宿主实测版本:0.2.0-rc.2
  • 目标 profile:web($env:USERPROFILE\.dsh\profiles\web)
  • 安装后外观与 DSH 原生完全一致,直到你在设置里显式启用效果。
  • 复用与许可台账见 REUSE.md,第三方许可见 LICENSES/。

1. 交付物

路径说明
package.jsonmanifest(dsh.bundle.patch / dsh.client / exports)
cordis.patch.ymlloader 条目(dsh.bundle.patch 指向它)
lib/index.js宿主半(Node):JSON 参数文件 + 围栏 HTTP 路由 + 热加载
lib/client.js浏览器 bundle(scripts/build.mjs 由 src/ 生成,勿手改)
src/**可读源码(参数表、存储、各效果引擎、设置行、程序化 API)
scripts/build.mjs零依赖构建(--check 做陈旧检查)
scripts/smoke-factory.mjs在 Node 里跑一遍 bundle 工厂,抓语法/导出错误
scripts/verify-tokens.mjs令牌漂移自检(失败非零退出)
scripts/png-diff.mjs纯 Node PNG 双图差异报告(含 --dump-region diff)
REUSE.md复用与许可台账(Phase 0 闸门产物:逐行来源与许可判定)
ACCEPTANCE.md9 项验收的实测证据
icon.svg, LICENSE, LICENSES/图标 / 许可证 / 第三方声明

2. 安装 / 升级 / 卸载 / 还原

dsh 不在 PATH 上,下面用绝对路径调用。

# 本仓库克隆到哪,就填哪
$repo = "$env:USERPROFILE\src\dsh-deepseek-theme-studio"
$dsh  = "$env:LOCALAPPDATA\Programs\DeepSeek Harness\resources\runtime\cli\bin\dsh.cmd"

2.1 安装(本地路径)

$env:COREPACK_ENABLE_PROJECT_SPEC = "0"     # 见 §6 坑 1
& $dsh plugin --profile web add $repo

换成别的 profile 只改 --profile 的值,其余完全一样:

profile你在哪用它命令
desktop桌面版 DSH(托盘常驻的那个,Web GUI 通常绑定随机端口)& $dsh plugin --profile desktop add $repo
webdsh --profile web 起的纯 Web 实例& $dsh plugin --profile web add $repo

两者是独立的两份 profile(各自有自己的 package.json / node_modules / 设置),装一个不影响另一个; 想两边都有就各装一次。重启也只重启你要用的那个。

桌面版重启:完全退出 DeepSeek Harness(含托盘图标)再打开。这份 profile 没有 patchReload: live, 所以热更新不适用于"新增插件"这种改动。

预期输出(本次实测):

dependencies:
+ dsh-deepseek-theme-studio link:$repo
Done in 3s using pnpm v11.7.0

安装做三件事,缺一不可:

  1. profiles/web/package.json → dependencies 增加 "dsh-deepseek-theme-studio": "link:$repo";
  2. 同文件 dsh.profile.bundles 追加 "dsh-deepseek-theme-studio";
  3. profiles/web/node_modules/dsh-deepseek-theme-studio 建 Junction 指向源码目录。

loader 条目由本包自己的 cordis.patch.yml 提供(dsh.bundle.patch 机制),因此不需要手改 profiles/web/cordis.patch.yml。本包内容:

- insert:
    - id: deepseek-theme-studio
      name: 'dsh-deepseek-theme-studio'

2.2 生效:重启 profile

客户端插件没有自动发现机制 —— 只把包装进 node_modules 不会生效,必须重启 profile 让 boot manifest 重新组合。 重启前可先跑 node scripts/preflight-profile.mjs 体检(见 §7)。

web profile:

# 停掉当前实例后:
$exe = "$env:LOCALAPPDATA\Programs\DeepSeek Harness\DeepSeek Harness.exe"
$bin = "$env:LOCALAPPDATA\Programs\DeepSeek Harness\resources\app.asar\dsh\node_modules\@deepseek-ai\dsh\lib\bin.js"
$env:ELECTRON_RUN_AS_NODE = "1"
Remove-Item Env:DSH_PROFILE, Env:DSH_PROFILE_DIR, Env:DSH_SHELL -ErrorAction SilentlyContinue
& $exe --expose-internals $bin --profile web --port 19399 --no-open
# 输出形如: dsh web: http://127.0.0.1:19399/?token=<TOKEN>

然后浏览器打开该 URL 并 Ctrl+Shift+R 硬刷新。

为什么必须清那几个环境变量:dsh.cmd 走的是 desktop host CLI(manageDesktopProfile: true), 会强行注入 --profile desktop,于是 --profile web 变成"profile 指定了两次"而报错; 且 DSH_PROFILE_DIR 会把 profile 目录钉死在 desktop。直连内层 bin.js 并清掉这三个变量即可。

desktop profile: 完全退出 DeepSeek Harness(含托盘图标)再打开即可 —— 桌面版由它自己拉起, 不需要手敲命令。重启后到设置 → 通用 看那一行在不在;不在就按 §6 坑 2 逐条查, 或先跑 node scripts/preflight-profile.mjs --profile desktop。

2.3 升级

本地 link 安装:改完 src/ 后 node scripts/build.mjs(或用 pnpm run build),再重启 profile 即可; 不需要重新 add。npm 安装:& $dsh plugin --profile web add dsh-deepseek-theme-studio@<新版本> 后重启。

2.4 卸载

& $dsh plugin --profile web remove dsh-deepseek-theme-studio

实测会把 dependencies 与 dsh.profile.bundles 中的条目一并移除(刷新页面不够,必须重启 profile)。 node_modules 下可能残留一个失效 Junction,可手动删除:

Remove-Item "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-deepseek-theme-studio" -Force -Recurse

2.5 还原原生外观(不卸载)

设置 → 通用 → DeepSeek 官网主题工作台 → 点 一键还原原生;或:

window.dshThemeStudio.reset()

还原后本插件不写任何令牌、不挂任何 canvas、不设 data-dts 根属性,只剩设置行本身(关掉设置即不可见)。


3. 三条控制通道

3.1 设置页 UI

注册进宿主自己的 settings.general.item 槽位(order: 20,紧随内置「外观」行 order: 10 与字号行 order: 11), 不是独立悬浮面板。分组折叠、每组控件由参数表自动生成。

3.2 程序化 API

同一个对象挂两处:window.dshThemeStudio 与 ctx.provide('themeStudio', api)。

方法签名说明
list()() => ParamInfo[]每个参数的 key / 标签 / 分组 / 类型 / 范围 / 步长 / 默认值 / 当前值
get(key)(string) => any取一个参数(未知 key 抛错)
getAll()() => object取全部当前值
set(key, value)(string, any) => any设一个参数,按参数表强制类型与范围
setMany(patch)(object) => object批量设置
reset()() => object还原原生
exportPreset()() => string导出 JSON(含 $schema)
importPreset(text|object)(any) => {applied, unknown}导入
presets() / applyPreset(id)内置预设:native / official-dark / official-light
subscribe(fn)(fn) => () => void订阅变更
selfCheck() / formatSelfCheck()令牌 + DOM 钩子漂移报告
diagnostics() / diagnosticsText()全量诊断(含每个效果的运行状态)
window.dshThemeStudio.applyPreset('official-dark')
window.dshThemeStudio.set('glass.blur', 32)
window.dshThemeStudio.formatSelfCheck()

3.3 JSON 参数文件(可手工编辑 + 热加载)

// $env:USERPROFILE\.dsh\theme-studio.json
{
  "appearance.mode": "dark",
  "appearance.backgroundMode": "fluid",
  "glass.enabled": true,
  "glass.blur": 24,
  "quantum.q1Spacing": 90,
  "quantum.q2Levels": 6
}
  • 文件名/路径:$DSH_HOME/theme-studio.json(默认 $env:USERPROFILE\.dsh\theme-studio.json),原子写(tmp+rename)。
  • HTTP 接口(前缀路由,带 loopback + 同源信任围栏):
    • GET /deepseek-theme-studio/api → { ok, value, fileError, revision, path }
    • POST /deepseek-theme-studio/api { "method": "get" }
    • POST /deepseek-theme-studio/api { "method": "set", "patch": { "glass.blur": 30 } }(浅合并,null 删键)
    • POST /deepseek-theme-studio/api { "method": "replace", "value": { ... } }
  • 客户端每 3 秒(页面可见时)检查一次 revision,文件被手工改动即热加载;fileError 会在设置行显示, 不会被当成空配置静默吞掉。
  • 文件必须是 UTF-8 无 BOM 的 JSON 对象;带 BOM 会被容忍,语法错误会被报告。

安全说明:该路由只做 DNS-rebinding / 跨站防护(Host 必须是 loopback 或 webRuntime.trustedHosts, 且 sec-fetch-site 不是 cross-site),不是鉴权。本机任意进程都能读写,与生态内同类插件的取舍一致。


4. 参数完整说明

持久化 key = 参数名本身(JSON 文件里的键;localStorage 里加前缀 dsh-deepseek-theme-studio:)。 「默认」列是安装后的初始值 —— 全部等于"什么都不做"。

4.1 外观模式 / 背景

设置项键控件范围 / 步长默认
外观模式appearance.mode三态system/light/darksystem
背景模式appearance.backgroundMode分段solid/gradient/fluidsolid
底色appearance.bgColor取色器 + 文本HEX(空 = 跟随宿主)""
强调色appearance.accentColor取色器 + 文本HEX(空 = 跟随宿主)""
流体调色板 1–5appearance.palette1..5取色器 ×5HEX#000000/#1a3870/#204a7e/#eed8aa/#000000
装饰层不透明度appearance.opacity滑杆0–100%,步长 1%60%
渐变角度appearance.gradientAngle滑杆0–360°,步长 1180

4.2 液化(流体)

设置项键范围 / 步长默认
启用流体背景fluid.enabled开关关
流速fluid.speed0–2×,步长 0.050.28
噪声波长(形状宽窄)fluid.scale0.2–4,步长 0.011.77
指针扰动半径fluid.radius0–300 px,步长 1140
域扭曲强度fluid.distort0–6,步长 0.052.2
卷曲强度fluid.swirl0–3,步长 0.050.8
颗粒噪点fluid.grain0–0.05,步长 0.0010.005
指针光晕fluid.glow0–0.6,步长 0.010.13
暗角fluid.vignette0–1,步长 0.010.38
流体柔化模糊fluid.blur0–24 px,步长 10
指针流场扰动fluid.flowmap开关开

4.3 毛玻璃

设置项键范围 / 步长默认
启用毛玻璃glass.enabled开关关
模糊半径glass.blur0–40 px,步长 124
深色 tint / 浓度glass.tintDark / glass.tintDarkAlphaHEX / 0–1 步长 0.01#000000 / 0.20
浅色 tint / 浓度glass.tintLight / glass.tintLightAlphaHEX / 0–1 步长 0.01#ffffff / 0.55
描边透明度glass.borderAlpha0–1,步长 0.010.08
饱和度glass.saturation0–2×,步长 0.051
四个白名单槽位glass.slot.composer / .bubble / .sidebar / .sessionlog开关前三开、第四关
槽位选择器(可改)glass.slot.<名>.selector文本见 §4.3.1

4.3.1 白名单默认选择器与漂移说明

槽位默认选择器0.2.0-rc.2 实测命中
输入卡[data-composer-card]1
用户消息气泡[class*="_userStack"] [class*="_bubble"]有消息时命中
侧栏按钮[class*="_newSession"]3
会话日志控件[data-slot="settings.general.item"] [role="switch"]仅设置页打开时命中

任务书里的第四项"会话日志按钮"在 0.2.0-rc.2 不存在为按钮:该版本把 session log 做成设置页 General 区的一行 (@deepseek-ai/dsh-client-ui-settings-session-log),行内是 Switch 原语;dsh-unknown-theme 针对的 .nL4_yW_sessionLogButton 在本版宿主中查无此类(已抽取全部 51 个宿主 client bundle 的 class-map 佐证)。 因此默认指向真实控件,且随时可在设置里改选择器。

同屏 backdrop-filter 元素硬上限 6(GLASS_MAX_TARGETS),超出者被标记跳过; 会形成 containing block 而困住 position: fixed 浮层的祖先(_frame、_sidebarCol、[data-slot] 列、 role=dialog/menu 等)永不加滤镜。

4.4 量子化

编号设置项键范围 / 步长默认
Q1空间量子化(点阵)quantum.q1Enabled开关关
Q1栅格间距quantum.q1Spacing24–160 px,步长 190
Q1斥力半径quantum.q1Radius0–300 px,步长 1140
Q1弹簧回弹 / 阻尼quantum.q1Spring / quantum.q1Damping0.005–0.3 / 0.5–0.990.05 / 0.85
Q1网格线 / 节点不透明度quantum.q1LineOpacity / quantum.q1PointOpacity0–0.60.08 / 0.08
Q1命中节点半径quantum.q1ActivePoint1–8 px,步长 0.12.2
Q1点阵颜色quantum.q1ColorHEX#ffffff
Q2颜色量子化quantum.q2Enabled开关关
Q2色阶数quantum.q2Levels2–16 阶,步长 16
Q2Bayer 抖动强度quantum.q2Dither0–1,步长 0.051
Q3形态量子化(粒子)quantum.q3Enabled开关关
Q3粒子总数quantum.q3Count200–6000,步长 501400
Q3成形时间quantum.q3Assemble300–4000 ms,步长 501200
Q3打散后回正时间quantum.q3Scatter4–60 s,步长 122
Q3粒子半径 / 颜色quantum.q3Size / quantum.q3Color0.5–4 px / HEX1.4 / #ffffff
Q3指针斥力半径quantum.q3Repel0–120 px,步长 119
Q3装饰图形quantum.q3Shapewhale/ring/wavewhale
Q4状态量子化quantum.q4Enabled开关关
Q4档位数quantum.q4Steps2–16 档,步长 18
Q4量化对象quantum.q4Targetpointer/ambient/scrollpointer

4.5 聚光灯 / 动画

设置项键范围 / 步长默认
标题聚光灯spotlight.enabled开关关
光斑直径 / 颜色spotlight.size / spotlight.color16–200 px / HEX64 / #ffffff
触发元素选择器spotlight.selector文本见下
动画总开关motion.enabled开关开
统一时长motion.duration80–400 ms,步长 10160
缓动曲线motion.easing4 条(均无过冲)cubic-bezier(0.2, 0, 0, 1)

聚光灯默认触发选择器:[data-conversation-header] h1, [data-conversation-header] h2, [class*="_headline"] (只在标题文字/图标上触发,容器空白区不触发)。


5. 已知取舍(明说,不静默)

  1. motion.duration 只作用于本插件自己的表面(效果层、玻璃过渡、设置行)。宿主组件动画不受影响 —— 任务 §3.4 禁止大段全局覆盖宿主样式,两者冲突时以此为准。
  2. appearance.opacity 允许 0–100%(任务 §3.2 的范围要求),但内置预设与默认值都不超过 60% (任务 §2.1 的简洁预算);超过 60% 时设置行会给出提示。
  3. 单元数据里 dsh.compatibility.dshReleases 宿主不读取(app.asar 全量 grep 无消费点); 字段按要求照写,实际兼容性由 dsh.bundle.patch / dsh.client / exports 三条契约保证。
  4. 鲸鱼轮廓是 DeepSeek 品牌资产,不属于本包 MIT 授权范围;quantum.q3Shape 可切换为 ring/wave 或直接关闭 Q3。

6. 排障(含三个已知坑)

坑 1:corepack EPERM ... package.json

Error: EPERM: operation not permitted, open '...\profiles\web\package.json.lock'

先设环境变量再重试:

$env:COREPACK_ENABLE_PROJECT_SPEC = "0"

若仍失败:确认目标 profile 没有被另一个 dsh 实例占用(关闭正在运行的 web 实例),并把整个命令放在同一个 shell 会话里执行。

坑 2:插件装了但页面上什么都没有

按顺序查这四件事:

  1. profile 里有没有 bundle 条目 —— profiles/web/package.json 的 dsh.profile.bundles 必须含 dsh-deepseek-theme-studio;只有 node_modules 里有个包是不够的(客户端插件没有自动发现)。
  2. 重启过 profile 没有 —— 刷新页面不够,boot manifest 在 profile 启动时组合。
  3. boot manifest 里有没有你的条目 —— 浏览器控制台执行:
    window.__DSH_BOOT__.entries.find(e => e.id === 'dsh-deepseek-theme-studio')
    
    有条目但页面报 1 entry did not activate → 打开控制台看 [theme-studio] 前缀的错误。
  4. bundle 的 id 是否等于包名 —— window.__ModuleLoader__.load({ id }) 的 id 必须是包名 (dsh-deepseek-theme-studio),否则报 loaded without registering "<包名>" via __ModuleLoader__.load。

坑 3:重启时机

  • 改了 cordis.patch.yml / package.json / lib/index.js → 必须重启 profile。
  • 只改了 lib/client.js(由 src/ 构建)→ 刷新页面即可,宿主按内容版本号重新拉取 bundle; 必要时 Ctrl+Shift+R 硬刷新。
  • 改完源码记得 node scripts/build.mjs,否则跑的还是旧 bundle:node scripts/build.mjs --check 会告诉你是否陈旧。

其他常见问题

症状原因 / 处理
设置行里显示"参数文件解析失败"theme-studio.json 不是合法 JSON(常见:编辑器写了 BOM、尾逗号)。修好后 3 秒内自动恢复
设置行显示"未连接参数文件"宿主半没挂载(webServer 服务不可用)或 URL 前缀被改。此时仍可用,只是仅存 localStorage
重启后参数"丢了"桌面端每次启动换端口 → localStorage 按 origin 隔离;正是为此把 JSON 文件做成权威来源。检查 $DSH_HOME/theme-studio.json
流体效果变成静态渐变当前环境没有 WebGL2(控制台会有 [theme-studio] WebGL2 unavailable 警告)。这是设计好的降级,不会白屏
粒子/网格在小窗口不见了视口宽度 < 768 px 时刻意关闭(任务 §4.2),属预期
dsh plugin 命令报"profile 指定了两次"你用的是 dsh.cmd(desktop host CLI,会强行注入 --profile desktop)。见 §2.2 直连 bin.js 的做法

7. 自检与验证脚本

cd $repo
node scripts/preflight-profile.mjs      # 重启前体检:loader 到底会不会收这个包(全部 profile)
node scripts/preflight-profile.mjs --profile desktop
node scripts/build.mjs --check          # bundle 是否与 src 同步(非零退出 = 陈旧)
node scripts/smoke-factory.mjs          # 在 Node 里跑 bundle 工厂,抓语法/导出错误
node scripts/verify-tokens.mjs          # 令牌漂移自检(默认扫已解包的宿主树)
node scripts/verify-tokens.mjs --live http://127.0.0.1:19399   # 对运行实例复检
node scripts/png-diff.mjs a.png b.png --json out.json --dump-region diff

preflight-profile.mjs 复刻了宿主 @deepseek-ai/dsh-client-modules 在 profile 启动时做的每一项检查 (bundles 条目 / 包可解析 / dsh.client.platform / exports["./client"] 与文件存在 / exports["."] 与 服务端入口存在 / 补丁层含 insert 且 name 指向本包),这样配置错误在重启之前就能发现, 而不是重启后对着一个空白设置页猜。

运行时自检(浏览器):

window.dshThemeStudio.formatSelfCheck()

verify-tokens.mjs 刻意比参考实现更严格:抓不到宿主 CSS 或运行实例时非零退出, 不会像 mux9056-bot/dsh-theme 的 verify:live 那样在抓取失败后仍然打印"校验通过"。


发布(维护者)

发布不需要任何 token、不需要验证码。npm 侧的凭据是通过 trusted publishing(OIDC) 建立的: .github/workflows/publish.yml 用一次性的、限定到本工作流的 OIDC 令牌换发布权,所以没有长期凭据可泄露、也没有东西需要轮换。

流程就三步:

# 1. 改版本号(构建会把版本号注入 lib/client.js,所以必须重新构建)
node -e "const f='package.json',p=require('./'+f);p.version='0.1.2';require('fs').writeFileSync(f,JSON.stringify(p,null,2)+'\n')"
node scripts/build.mjs

# 2. 提交
git add -A; git commit -m "Release 0.1.2"

# 3. 打附注 tag 并推送(工作流由 tag 触发)
git tag -a v0.1.2 -m "v0.1.2"
git push origin main; git push origin v0.1.2

然后工作流会自动:校验 bundle 与 src/ 同步 → 工厂冒烟测试 → 校验 tag 与 package.json 版本一致 → 发布并生成 SLSA provenance。

两个容易踩的坑(都实际踩过):

  • 必须重新构建再提交。 scripts/build.mjs 会把版本号写进 bundle(头部注释和 createPluginBody({ version })),只改 package.json 不动 bundle 会导致工作流第一步就失败 —— 这是门禁在起作用,不是 bug。
  • 必须用附注 tag(git tag -a),且显式推送 tag。 轻量 tag 配 git push --follow-tags 不会被推上去,工作流根本不会触发。

首次发布为什么更麻烦

新包没有包设置页,所以配不了 trusted publisher —— 得先有一次带 2FA 的发布把包建出来。当时用的是 npm stage publish + 网页批准(分阶段发布对尚未存在的包会先发一个 0.0.0-stage 占位版本)。 包一旦存在,就可以用下面这条命令配置 trusted publisher:

npm trust github <package> --file publish.yml --repository <owner>/<repo> --allow-publish -y

这条命令需要交互式 2FA:它会在浏览器里打开验证页(用安全密钥也行),必须在真实终端里跑 —— 管道或后台任务里没有 TTY,npm 会直接抛 EOTP 而不打开浏览器。

配置好后用 npm trust list <package> 查看(同样需要 2FA)。