dsh-glass-ranswork · 自定义主题
给 DeepSeek Harness Web 界面加上「半透明 + 背景模糊」的毛玻璃外观,并在
设置 → 自定义主题 里提供一个完整的调参页(启用 / 不透明度 / 面板不透明度 /
模糊 / 饱和度 / 氛围光 / 配色 / 自定义背景图片)。
出厂默认即用户要的 80% 半透明(opacity: 20,即表面 20% 不透明);
信息面板单独一档 panelOpacity: 85,保证弹层和文件预览里的字看得清。
快速开始
升级之前先看「升级」一节。 把 .../main.tar.gz 当安装地址时,pnpm 会把
那个 tarball 的 integrity 和 version 一起写进 lockfile —— main 会动,
你的依赖不会跟着动。重装多少次都还是当初装下的那个版本。
-
打开 DSH → 设置 → 插件 → 安装插件;
-
粘贴地址(推荐第一种):
npm(推荐)
dsh-glass-ranswork
codeload 固定版本(不想用 npm 时)
https://codeload.github.com/rans725/dsh-glass-ranswork/tar.gz/refs/tags/v1.1.1
本地目录:把仓库 zip(Code → Download ZIP)或 Release 的 .tgz 解压,
填含 package.json 的那层目录,例如 D:\Plugins\dsh-glass-ranswork。
-
装完按提示重启 DSH,打开 设置 → 自定义主题 即可调外观。
-
设置页顶部会显示当前加载的版本号(如 v1.1.1);有新版时多一行提示。
安装
本包是一个 DSH bundle(package.json 的 dsh.bundle.patch 指向
cordis.patch.yml,dsh.client 声明浏览器半侧),不需要构建、没有依赖。
方式一:npm(推荐)
在 profile 目录里直接装:
pnpm add dsh-glass-ranswork
为什么推荐 npm:装 npm 的包,插件市场能感知到新版本并推送更新提醒。
下面那些 URL 安装方式虽然能装,但永远收不到更新推送 —— 市场对 URL 安装
没有 registry 身份可比,宁可如实报「无更新」也不猜(dshmarket #768)。
从旧版 URL 方式升上来的用户:lockfile 里记的还是 URL,改 npm 后仍然收不到推送。
需要重装一次:pnpm remove dsh-glass-ranswork && pnpm add dsh-glass-ranswork
好处是版本可以写范围(^1.1.1),升级由 pnpm 和市场的更新提醒共同覆盖。
方式二:codeload 固定发行版(不想用 npm 时)
pnpm add https://codeload.github.com/rans725/dsh-glass-ranswork/tar.gz/refs/tags/v1.1.1
好处是版本可以写死在 tag 里,升级时换 tag 号即可;坏处是不能写成 ^1.1.1
这种范围(pnpm 无从解析),所以「有新版本」的提示靠设置页顶部那一行。
方式三:codeload 直链(固定版本,不需要 Git)
https://codeload.github.com/rans725/dsh-glass-ranswork/tar.gz/refs/tags/v1.1.1
为什么不写 https://github.com/.../archive/...:
github.com 只负责 302 跳到 codeload.github.com;主站被墙 / 不稳时,
pnpm 的 HEAD 请求会 ETIMEDOUT,而真正下发 tar 的 codeload 往往是通的;
- codeload 直链少一次跳转,受限网络下成功率明显更高,代码不用改一行。
版本号必须写进 URL。 refs/heads/main 看着“永远最新”,但 lockfile 一旦
记录了这个 spec 的 integrity,pnpm 不会为了同一个 spec 重新解析远端。
升级 = 换成新 tag 的 URL 再装一次(见「升级」)。
为什么不推荐 github: 前缀:pnpm 解析 github: 依赖会调用系统 git;
Windows 上没装 Git 就会报 git ls-remote failed。上面的地址由 pnpm 直接
下载解包,不需要 Git。
方式四:Release 安装包(固定版本)
到 Releases 下载
dsh-glass-ranswork-1.1.1.tgz,安装时填它的本地路径,或直接填资源地址:
https://github.com/rans725/dsh-glass-ranswork/releases/download/v1.1.1/dsh-glass-ranswork-1.1.1.tgz
资产地址同样先经过 github.com。主站不通时,改用上面的 codeload 直链。
方式五:本地目录 / zip
两种来源都可以:
- 仓库页 Code → Download ZIP(解压后是
dsh-glass-ranswork-main/);
- 或 Release 里的
dsh-glass-ranswork-1.1.1.tgz(解压后是 package/)。
把含 package.json 的那一层目录放到任意位置,在安装入口填它的绝对路径,例如:
D:\Plugins\dsh-glass-ranswork
安装器会把依赖写进当前 profile 的 package.json 并激活 bundle,
不需要手动改配置文件。
兼容性
- 在 DSH 0.2.1-rc.3(Windows 桌面版)上开发与验证(v1.1.1 起);
- 插件只用到公开客户端服务(
slots / locale / theme)和宿主 webServer,
没有私有 API、没有原生依赖;DSH 大版本升级后如果主题 token 改名,
退化结果是「某块颜色/模糊不生效」,不会崩。
- 每次宿主升级都会重编译客户端,CSS Modules 哈希会全变。插件只在
纯装饰性的卡片前缀上依赖哈希;凡是「排除某个结构容器」的规则一律用
稳定 data 属性或结构判据(详细的坑见 v1.1.1 更新日志)。
卸载
设置 → 插件 → 卸载。插件不会改 DSH 本体;设置文件在
$DSH_HOME/glass-theme.json,壁纸在 $DSH_HOME/glass-theme-bg.<ext>,
删掉即可彻底复位。
升级
先确认当前版本:设置 → 自定义主题 顶部有 v1.1.1(旧版宿主没有这行,
可以在 profile 的 node_modules/dsh-glass-ranswork/package.json 里看)。
怎么知道有新版本
设置页顶部那行:v1.1.1 后面如果出现「有新版本 v1.2.0」的链接,点它去 Release。
宿主半侧每 6 小时静默问一次 api.github.com 的最新 Release(失败 10 分钟后重试,
全程不打扰;断网时只显示版本号)。
tarball / Release 装的
三步:
- 把 profile
package.json 里的旧 spec 换成新 tag 的 URL;
- 删掉
pnpm-lock.yaml 里对应的 dsh-glass-ranswork@... 条目(或整份 lockfile);
pnpm install,重启 DSH。
或者一步到位:
pnpm add https://codeload.github.com/rans725/dsh-glass-ranswork/tar.gz/refs/tags/v1.1.1
为什么“重装一次”不升级
pnpm 对 tarball 依赖记录的是 spec + integrity + version。spec 没变(还是
.../heads/main.tar.gz)时,它认为这份依赖已经解析过,直接用 lockfile 里的
旧包,不会去远端重新 HEAD。main 更新了、tag 打了,都和你的 lockfile 无关。
只有下面两种情况会真正换包:
- spec 变了(新 tag 的 URL / 新版本号);
- 你显式让 pnpm 重新解析(删 lockfile 或
pnpm install --force)。
卡住时的自查
| 现象 | 原因 | 处理 |
|---|
| 重装后版本不变 | lockfile 钉住了旧 spec | 换成新 tag 的 URL |
HEAD https://github.com/.../main.tar.gz ... ETIMEDOUT | github.com 不通 | 改用 codeload 直链 |
安装返回 changed:false,日志只有 fetch failed | 旧依赖也在拉 github.com,一个失败整次回滚 | 先从 package.json 删掉旧条目再装 |
| 设置页没有版本行 | 宿主半侧还是旧版 | 重启 DSH(浏览器半侧热更新,宿主不会) |
安装器目前只会把 fetch failed 透出来,不会告诉你是哪个 URL 失败。
遇到时先看 profile 的 package.json 里还有没有 github.com/.../archive/...
的旧条目 —— 通常就是它。
设置
| 字段 | 默认 | 区间 | 说明 |
|---|
enabled | true | — | 关闭后完全恢复原生外观 |
opacity | 20 | 0–100 | 表面不透明度 %;20 = 80% 半透明,100 = 不透明 |
panelOpacity | 85 | 0–100 | 信息面板不透明度 %:弹层、文件预览、统计/费用面板、Modal 内容 |
blur | 32 | 0–80 | 三列骨架背后那层的模糊半径(px) |
overlayBlur | 32 | 0–80 | 浮层模糊半径(px):弹层、已归档面板、费用面板、Modal。运行时夹到 56px |
composerBlur | 24 | 0–80 | 发送框 / 任务卡片模糊半径(px)。运行时夹到 40px;设为 0 关闭这两处 |
previewOpacity | 85 | 0–100 | 文件预览 / 改动卡片不透明度 %:文件 diff、已编辑文件卡、文档预览 |
saturate | 180 | 100–200 | 背景饱和度 % |
ambient | true | — | 是否绘制氛围光背景 |
accent | aurora | aurora / ocean / sunset / mono | 氛围光配色 |
bgImage | '' | — | 背景图 token(空串 = 没设图)。只是缓存键,不是文件名 |
bgFit | cover | cover / contain / stretch / tile / center | 显示方式:填充 / 适应 / 拉伸 / 平铺 / 居中 |
bgCrop | {x:0,y:0,w:1,h:1} | 每项 0–1 | 自定义裁剪区域(占原图比例,整图 = 0,0,1,1) |
bgDim | 30 | 0–100 | 背景蒙层不透明度 %(浅色盖白、深色盖黑) |
bgGuard | true | — | 文字保护:有壁纸时给文字加光晕 + 加深次要文字(不加任何底色) |
bgAutoTone | true |
设置页常规是 7 个滑块:表面不透明度 / 面板不透明度 / 背景模糊 / 浮层模糊 /
发送框·任务模糊 / 文件预览·改动卡片 / 背景饱和度。设了背景图之后多一个
「背景蒙层」滑块、两个开关(文字保护 / 跟随图片明暗),并多出整行宽的
「自定义裁剪」区(设图前这些不显示,避免摆一个当前没有作用的控件)。
发送框与任务卡片的底色没有单独一档:它就是 --dsw-specific-input-major
(属 SURFACES),跟着「表面不透明度」走。曾经有过 composerOpacity
滑块,实现方式是给卡片刷一层 color-mix(白 N%) 的白底 —— 行内
background 会顶掉卡片自己的 token,拖「表面不透明度」时发送框纹丝不动,
所以已删除。挡住背后的字靠模糊,不靠颜色。
设置持久化在 $DSH_HOME/glass-theme.json(宿主半侧用「临时文件 + rename」原子写入;
坏文件按默认值处理,删掉即可复位)。范围校验以宿主半侧为准。
为什么分成两档不透明度
opacity 管的是装饰性骨架(frame / 侧栏 / 输入框 / 选区),透一点更好看;
panelOpacity 管的是要读字的地方。这两类不是一个可读性预算:把统计弹层、
文件预览也压到 20% 不透明,正文和表格会直接糊在氛围光上。
面板这一档覆盖的 token 见 client.js 的 PANELS:
| token | 谁在用 |
|---|
--dsw-specific-menu | 会话统计 / 逐轮用量弹层(宿主 Xt1eiG_panel)、所有下拉浮层 |
--dsw-alias-bg-document-preview | 文件预览里 PDF 的底色 |
--dsw-alias-bg-layer-2 / -3 | Modal 内容、Pill、轨迹列表、插件管理页、桌宠的费用弹层与花费看板(dsh-cost-panel / dsh-pet-dash-panel) |
基色不能用 --dsw-menu-surface-fill:那个值自带 alpha(#f8f9fa94),
再乘一次会变成 0.58 × 0.85 ≈ 49% 透明,比不改还透。所以 PANELS 用的是同色的
不透明静态 token(bluish-50 / bluish-750)。
宿主重启窗口期的保存
宿主半侧的 DEFAULTS 是模块加载时读进内存的,改它必须重启 DSH;
浏览器半侧走 HMR,刷新页面就生效。所以在「宿主还没重启」的窗口期里,
宿主不认识 panelOpacity:POST 上去会被旧键集丢掉,GET 回来也没有这个字段。
client.js 因此把设置镜像一份到 localStorage(键 dsh-glass-theme:settings),
只用来补齐宿主不认识的键;宿主认识的键永远以宿主为准。宿主重启后自然开始
持久化 panelOpacity,镜像里那份随即被覆盖成同一个值,不会分叉。
自定义背景图片
设置页可以选一张本机图片当壁纸,铺在所有半透明表面后面(不是盖在上面),
并像桌面壁纸设置一样有 填充 / 适应 / 拉伸 / 平铺 / 居中 五种显示方式,
外加一个自定义裁剪框和一个背景蒙层滑块。
图片存在哪:图片本体落盘成 $DSH_HOME/glass-theme-bg.<ext>,
设置里只存一个短 token(bgImage)。刻意不把图片转成 data URL 塞进设置 JSON:
glass-theme.json 每次拖滑块都会被整体重写,塞几 MB 的 base64 会让每次
保存都写一个大文件,localStorage 镜像也会跟着爆掉;
- 图片走独立路由,浏览器可以长缓存;token 一变 URL 就变,不会拿到旧图。
路由(宿主半侧 index.js):
| 方法 | 路径 | 作用 |
|---|
GET | /glass-theme/background/<token> | 原样回传图片(immutable 长缓存 + nosniff) |
POST | /glass-theme/background | 上传图片(body 是原始二进制,不是 multipart) |
DELETE | /glass-theme/background | 删除图片并把 bgImage 清空 |
三条共用一个 prefix 路由(kind: 'prefix')。GET 把 token 放在路径段里,
不放在 query 里 —— 这样不依赖「exact 路由会不会忽略 query」这种实现细节。
安全边界:文件名固定(glass-theme-bg.<ext>),扩展名只来自
content-type 白名单(webp / png / jpeg / gif / avif / bmp / svg),
不存在「往任意路径写任意文件」的通道;bgImage 只接受
[A-Za-z0-9_-]{1,64},非法值在 normalize() 里退回空串。写操作同样过
DSH connection 层的来源信任检查(和设置路由一致)。
上传前会压缩:浏览器半侧用 createImageBitmap + canvas 把长边压到 3840px
以内并转成 WebP(质量 0.92),顺便剥掉 EXIF(含拍摄地点)。请求体上限 24 MiB。
裁剪坐标是比例,不是像素:bgCrop = {x,y,w,h} 全部是「占原图的比例」,
所以换分辨率、换窗口大小都不需要重算;服务端与浏览器半侧用同一套规则夹取
(w/h ≥ 5%,越界时把左上角往回推而不是把框压扁)。
渲染时用 canvas 把裁剪区域真的重画成一张新位图再交给 background-size
——用百分比位移模拟只对「铺满」成立,平铺/居中会露出整张原图。
蒙层为什么默认 30:玻璃表面默认只有 20% 不透明,直接压一张照片上去正文
会读不了。蒙层是与明暗同向的雾(浅色盖白、深色盖黑),和宿主表面的明暗
逻辑一致;它和「氛围光」是两件事,设图后氛围光仍然在,会在适应/平铺的留白处
透出来(配色选择依旧有意义)。
文字保护(bgGuard)为什么只加光晕、不加底色:壁纸下最自然的诱惑是
「把表面调实一点」,但那等于替用户改掉了他调好的透明度 —— 有人就是要把
表面压到 5%、模糊调到 0 来看壁纸。所以文字保护做两件事,背景一个像素都不动:
- 用
text-shadow 在字形背后加一圈与明暗同向的光(浅色白晕、深色黑晕),
形成字幕式的局部对比;它可继承,挂在 body 上就能覆盖侧栏、顶栏、
发送框以及 portal 到 body 的浮层。
- 把次要文字加深/提亮一档:宿主的
--dsw-alias-label-secondary /
tertiary / caption 在浅色主题下是 bluish-700/600/400
(#61666b / #81858c / #a3a8b0)。这类浅灰字压在照片上,光晕反而帮倒忙
(灰字压在白晕上对比度更低),必须把字本身改深。按同一套调色板改成
secondary→bluish-750、tertiary/caption→bluish-700(深色主题反向:
200/300/400)。只改颜色明度,不碰字号字重,不会引起重排;内联 SVG 图标
用 currentColor,也会跟着一起变清楚。
不喜欢光晕可以关掉开关,或者把「背景蒙层」拉高。
跟随图片明暗(bgAutoTone)
为什么还需要它:文字颜色是跟主题模式走的(浅色主题深字、深色主题浅字),
而壁纸的明暗和模式无关。换一张暗色壁纸后,浅色主题的深色字压在暗图上,
光晕和加深 token 都救不了 —— 那是「方向错了」。
做法:把背景图(裁剪后的那块)画到 32×32 画布上,算平均相对亮度
(0.2126R + 0.7152G + 0.0722B):
| 平均亮度 | 动作 |
|---|
| < 0.42 | 切到 dark(字变浅、面板变暗、蒙层变黑) |
| > 0.58 | 切到 light |
| 0.42–0.58 | 不切(明暗参半的图来回切会晃眼,交给文字保护兜) |
亮度从哪来(两层):
- 首选:上传时就算好。 选图片时浏览器已经把图片画进了 canvas(压缩用),
顺手把 32×32 的亮度图存成
bgLumaGrid。之后无论怎么裁剪,都只是按比例
取子矩形求平均 —— 纯查表,不解码图片,也就不会被 canvas 跨源/污染卡住。
- 兜底:现场量。 早于这个功能上传的旧图没有亮度图,就现场把图片
fetch 成 blob: URL(同源、canvas 一定读得出来)再解码量一次;
量不到会退避重试 6 次,并在开关下面显示状态(测得亮度 0.38 → 已切深色主题
/ 还没量到图片亮度,会自动重试)。
- 切换走官方
ctx.theme.setTheme(),不是自己改 data-ds-dark-theme;
- 只动内置
light / dark / system:用户如果选了第三方主题 id,一律不碰
(setTheme('dark') 会把第三方主题顶掉);
- 切换前把用户原来的偏好写进
bgBaseTheme(落盘 + localStorage 镜像),
删图 / 关开关 / 卸载时还原 —— 重启后也认得;
- 用户中途自己改过明暗 → 还原时发现当前偏好已经不是我们切的那个,就只清基准、
不覆盖他的选择;
- 判断只在「图片 token / 裁剪区域 / 开关」变化时做一次,不是每帧都量。
原生窗口菜单和标题栏按钮是 Electron / Windows 画的(不在 DOM 里),
CSS 够不到;能改的只有它们下面那层底色(见下一节)。
右上角窗口按钮的「方块」
截图里三个系统按钮(最小化 / 最大化 / 关闭)底下有一块比标题栏亮的方块。
根因是 DSH 的 preload 用一个隐藏 probe 把「标题栏底色」
(--dsw-specific-sidebar-fill)算出来传给主进程,作为 Electron 原生
titleBarOverlay 的底色;而页面自己的标题条也铺了同一个半透明白。
同一层颜色被刷了两遍(opacity=5 时 0.05 叠 0.05 ≈ 0.0975),
右上角就比标题栏亮出一块。
修法:把那个 probe 的 background-color 改成透明(选择器只认
「body 直接子元素 + 无 class/id + 空元素 + 内联式里有 pointer-events 与
visibility」),主进程就会收到 rgba(0, 0, 0, 0),覆盖层不再画底,
露出下面同一条标题栏。按钮字形颜色不受影响(symbolColor 读的是 color)。
实现要点
三层结构(缺一不可):
-
氛围光背景 — html[data-dsh-glass] body::before 上一层固定定位渐变。
-
半透明表面 — 通过官方 ctx.theme.overrideTokens(source, tokens) 把
--dsw-alias-bg-* / --dsw-specific-* 等表面 token 改成
color-mix(in srgb, <原色> N%, transparent)。走官方 token 层,可撤销、
随主题切换自动重算明暗两套值。表面与面板两套表,各用各的百分比。
-
背景模糊 — 给主要列/面板加 backdrop-filter: blur() saturate(),
并把 --dsw-menu-backdrop-filter / --dsw-mask-blur 统一到同一档位。
-
浮层模糊 — 三列之上的浮层(已归档面板、桌宠费用弹层、宿主
_panel / _dialog / _float_、[role=dialog][aria-modal])单独挂
backdrop-filter,用 --dsh-glass-overlay-blur(由 paint() 按
overlayBlur 夹到 56px 上限)。原因:这些窗口原本多数没有模糊,
底色被 panelOpacity 压薄后背后正文会直接透出来,字压字看不清。
两个必须记住的排除项(删了会出问题):
OUqwTW_panel(右栏 dockkit 定位容器)必须排除:backdrop-filter
会为该元素建立「fixed 后代的包含块」,挂上去会让内部 position:fixed
的浮窗整体错位。
_panelRow / _panelActive / _panelButton / _panelHint 等行内小控件
也要排除:它们是子串匹配的误命中,背后没有要模糊的内容。
-
发送框 / 任务卡片模糊 — 发送框([data-composer-card])、审批卡、
提问卡、文件卡、账号提示卡,以及任务看板的内部卡片
(EBLgjq_task / EBLgjq_member)单独挂 backdrop-filter,
用 --dsh-glass-composer-blur(按 composerBlur 夹到 40px 上限)。
- 发送框用
[data-composer-card] 而不是哈希类名 .RlGAzG_card:
那是宿主自己写、并在 4 处内部逻辑里当锚点用的稳定 data 属性。
- 不要把模糊挂到
--dsw-specific-input-major token 上:它还被
ImageLightbox 的 .image / .close 消费,会把图片查看器里的图片本身
变成毛玻璃。
-
composer 铺底模糊(漏了就会「间隙透字」) —
宿主的 [data-composer-seat] 自己铺了一层渐变底,基色是
--dsw-alias-bg-base(骨架档,由 opacity 控制)。卡片之间那圈
间隙就是这层,没有任何元素给它挂模糊。所以:
html[data-dsh-glass] [data-composer-seat] {
backdrop-filter: blur(var(--dsh-glass-composer-blur, 24px)) saturate(...);
}
只加模糊,不加颜色。 曾经在这块写过 ,
把发送框的 顶掉了 —— 用户拖「表面不透明度」
时发送框不跟着变,看到的就是「发送框不半透明了」。 还会主动
清掉旧版本留下的行内 与
(HMR 只换 JS 不刷页面,行内样式会残留)。
最外层的 .frame 底色被清成透明(一处必需的 !important):.frame 与
.centerCol 都用 --dsw-alias-bg-base 铺底,两层叠加会让实际不透明度变成
1-(1-a)²(a=20% 时约 36%),滑块就对不上了。
右栏 / 文件预览的底色走局部变量重定义,而不是改 token:dockkit 的
_tabHost_ 与中央列共用 --dsw-alias-bg-base,全局改会把对话区一起变实。
所以只在右栏子树里把它指向 --dsh-glass-panel-light/dark(由 paint() 按
panelOpacity 写入):
html[data-dsh-glass] [class*="_rightbarCol"],
html[data-dsh-glass] [data-dockkit-pane],
html[data-dsh-glass] [class*="_tabHost_"],
html[data-dsh-glass] [class*="_emptyTabHost_"] {
--dsw-alias-bg-base: var(--dsh-glass-panel-light, transparent);
}
回退值必须写 transparent 而不是 var(--dsw-alias-bg-base):自定义属性自引用
会构成循环,整条声明会被丢弃。
更新日志
v1.1.1(2026-10-25)
修 0.2.1-rc.3 的「右半边毛玻璃遮挡」。
rc.3 重编译后右栏面板的 CSS Modules 哈希从 OUqwTW 变成 acoR2W,
插件里那条哈希排除失效;右栏的空壳容器(绝对定位、锚右缘、宽度 45%)
被 4b 段挂上 backdrop-filter,于是整片右半边被糊住。
- 4b 段改为跨构建稳定的排除:
[data-sidebar-right-panel] 及其后代,
外加 :not(:has([data-dockkit-surface]))(装着 dockkit 的容器一律不是浮层)
- 4b 里那条被哈希排除的写法保留作历史说明,并写清「别再用哈希做排除」
- 4c 段卡片前缀更新到 rc.3 实测值(VBK9GG / Zf6DNq / Nksazq / wnLwla /
EEnIVa / qpBK2a),旧前缀保留兼容旧客户端
- 新增一条守卫:任何
[class*="_panel"] 规则改动后,用工作区里的无头浏览器
脚本复核「面积 >5% 的元素不该有 backdrop-filter」
v1.1.0(2026-10-23)
发布到 npm,插件市场可以推送更新了。
v1.0.3 把更新检查改问 GitHub Releases,解决了「查 npm 永远 404」的问题,
但只覆盖了插件自己的设置页提示。插件市场那一侧仍然无法推送 ——
市场对 URL 安装没有 registry 身份可比,只能如实报「无更新」。
- npm 发布:
pnpm add dsh-glass-ranswork 现在可用,市场能感知新版本
- README:中英安装段改为以 npm 为首选,URL 方式降为备选并注明不再推送
version.json:新增 npm 字段,供自建更新检查在 GitHub 之外有第二来源
- 自建更新检查保留:
index.js 的 GitHub Releases 检查继续工作,
与市场推送互为补充(一个查 GitHub,一个查 npm registry)
老用户需重装一次:之前用 URL 装的,lockfile 里记的还是 URL,
改 npm 后仍然收不到推送。pnpm remove dsh-glass-ranswork && pnpm add dsh-glass-ranswork
功能代码(client.js / index.js 渲染部分)与 v1.0.3 一致,本版只改分发与文档。
v1.0.3(2026-10-20)
更新检查换了数据源:不再问 npm,改问 GitHub Releases。
本包不发 npm(作者不打算维护 npm 账号),registry 上永远是 404,所以
v1.0.2 的「有新版本」提示在事实上永远不会出现。
- 检查源:
index.js 的 UPDATE_REGISTRIES(npm 官方 + npmmirror)换成
UPDATE_SOURCES —— api.github.com/repos/.../releases/latest 的 tag_name
优先,回退到 GitHub 自己的静态文件
github.com/.../releases/latest/download/version.json(不占 API 限速);
- 缓存与退避不变:成功 6 小时(匿名 API 限速 60 次/小时/IP,不能去掉缓存),
失败 10 分钟退避,并发请求共用一次往返,单次 2.5 秒超时,失败静默;
- 请求头:带
user-agent(GitHub API 不带 UA 会 403);
- 文档:README 删掉 npm 安装方式与「发布到 npm」的说明,改为
codeload 固定 tag 直链为主,并写明「本包不发 npm」这个事实。
v1.0.2(2026-10-20)
安装 / 升级链路修复(对应 issue #1):
- 文档:删掉「重装一次即拉最新 main」。明确写出 lockfile 会钉住
tarball 的 integrity / version,升级必须换版本化地址;
- 安装源:README 改为 codeload 固定 tag 直链,绕开
github.com 的 302 跳转(受限网络下最常卡住的就是这一步);
- 发布:补
repository / publishConfig,新增
.github/workflows/release.yml(打 tag 自动建 Release + 传 tgz);
- 版本可见:设置页顶部显示当前加载的版本;宿主半侧新增
/glass-theme/version,静默比对最新版,有新版给一行链接,检查失败什么都不显示;
- 排错:新增「升级」「卡住时的自查」,覆盖版本不变、ETIMEDOUT、
changed:false 三类常见现象。
v1.0.1(2026-10-19)
发送框(composer)相关的四处修复,都围绕同一类根因:backdrop-filter
不只是「给元素加个模糊」,它还会改变元素的层叠与包含块语义。
- 修复:输入法有字时,鼠标移到发送箭头,发送框会闪一下
backdrop-filter 会让元素成为 position: fixed 后代的包含块。宿主的
Tooltip 气泡正是 fixed + 视口坐标、且默认不 portal,于是被祖先的模糊层
带偏;每次插入 / 重定位都触发整块毛玻璃重新采样。
现在模糊一律挂在 ::before 伪元素上(伪元素没有后代,不会成为包含块)。
- 修复:发送框颜色和背景图对不上(比周围更艳)
backdrop-filter 是逐层叠加的 —— 列 200% × 内层 200% 实际是 400%+。
现在饱和度只由最外层(三列 / 浮层)负责,内层只做模糊。
实测框内与框外的色差从 Δ=31 降到 Δ=3。
- 修复:发送框左右和下方多出一圈毛玻璃
模糊原先挂在整列宽的
composerSeat 上,而卡片受
--dsh-composer-side-clearance(16px)与底部 4px 内边距约束、比它窄一圈。
现在模糊只挂在卡片自身,形状随 border-radius 走,不再外溢。
- 修复:「发送框 / 任务模糊」滑块拖动无反应
上一版把卡片规则拆出来单独声明时漏了伪元素骨架(
content 等),
没有 content 的 ::before 不会生成,滑块的值没有消费者。
已补回骨架,并且调到 0 现在是真正关闭(backdrop-filter: none),
而不是留下一个仍然会建合成层的 blur(0px)。
v1.0.0(2026-10-17)
首个版本:半透明毛玻璃外观、三档不透明度、自定义背景图(五种摆放 + 裁剪)、
文字保护、跟随图片明暗。
已知边界(Windows)
Electron 主窗口在 Windows 上创建时没有 transparent / backgroundMaterial
(只有欢迎页用 acrylic;vibrancy 只在 macOS 主窗口)。所以:
- Windows:表面半透明后透出的是本插件画的氛围光,不是桌面壁纸。
- macOS:窗口本身透明,能真正透出桌面。
这是 Electron 窗口材质的边界,CSS 无法突破;宿主半侧跑在
ELECTRON_RUN_AS_NODE 子进程里,够不到 BrowserWindow API。
依赖的宿主 DOM 钩子
body::before / #root — 自有选择器,稳定。
[data-shortcut-modal] — 宿主写在组件上的 data 属性,稳定。
[data-dockkit-pane] — dockkit 的 pane 容器(右栏 / 文件预览的宿主)。
[class*="_frame"] / [class*="_sidebarCol"] / [class*="_centerCol"] /
[class*="_rightbarCol"] / [class*="_tabHost_"] / [class*="_emptyTabHost_"]
— DSH 用 CSS Modules,类名是 <哈希>_<局部名>,哈希随构建变、局部名稳定,
所以用子串匹配。这是全局主题唯一无法避免的耦合点;
万一哪天局部名改了,退化结果是「那一块没有模糊 / 底色回到骨架档」,
token 带来的半透明仍然生效。
- 浮层模糊另外用
[class*="_panel"] / [class*="_dialog"] /
[class*="_float_"] 与官方 [role="dialog"][aria-modal="true"]
(宿主 primitives 导出的 modalSelector)。前者带一长串 :not() 排除,
原因见上文「实现要点」第 4 条。
[data-sidebar-right-panel] — 右栏面板的容器(0.2.1-rc.3 起)。不要用
它的哈希类名做排除:rc.3 就把 OUqwTW_panel 重编译成了 acoR2W_panel,
哈希排除失效导致整片右半边被挂上 backdrop-filter(v1.1.1 修的就是这个)。
同理,判断「是否浮层」用 :not(:has([data-dockkit-surface])) 这种结构判据,
永远不要绑哈希。
[data-composer-card] — composer 输入卡片(见 4c 段)。
- 4c 段按「卡片」列出的哈希前缀(
VBK9GG_ / Zf6DNq_ / Nksazq_ 等)
是唯一保留哈希前缀的地方:它们是纯装饰性增强,失效只是「那张卡没模糊」;
旧前缀会一起留着以兼容旧客户端。
开发与验证
插件是两个手写的普通 JS 文件,没有构建步骤、没有依赖:
index.js —— 宿主半侧(Node):注册 /glass-theme/settings、
/glass-theme/background 与 /glass-theme/version 路由,把设置写到
$DSH_HOME/glass-theme.json;
client.js —— 浏览器半侧:window.__ModuleLoader__.load({ id, factory })
形态的 bundle,factory(require) 里拿 React,CSS 以字符串塞进 <style>。
改完只需要:
# 语法检查(用 DSH 自带的 node 或任意 Node 20+)
node --check index.js
node --check client.js
浏览器半侧走客户端 HMR:改完 client.js 刷新页面即可;宿主半侧的
index.js 改了路由/默认值时要重启 DSH。调试设置写入可以绕开 HTTP
直接 import('./index.js') 调 normalize()(DSH 的 connection 层会挡掉
命令行 POST,这是安全机制)。
作者工作区里有一套不随包发布的回归测试(node:test 风格、用假 DOM 跑
真实源码);如果你想贡献,改动请保持「无依赖、自带降级」的原则,
并在 PR 里说明验证方式。
发布(维护者)
-
改 package.json 的 version;
-
提交后打 tag 并推送:
git tag v1.1.1
git push origin v1.1.1
-
.github/workflows/release.yml 会:校验 tag 与 version 一致 → 语法检查 →
打包 → 创建 Release 并上传 .tgz。
本包不发 npm,所以流水线里的 npm publish 那一步在没配 NPM_TOKEN 时
自动跳过(那是默认状态,日志里有一行 ::warning::,可以忽略)。
用户侧的「有新版本」来自 GitHub Release 本身,与 npm 无关。