DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

lyingice /

lyingice/dsh-client-ui-mc-skin

Verified

This plugin has no description yet.

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

dsh-client-ui-mc-skin

给 DeepSeek Harness Web 客户端用的 Minecraft 风格主题:像素立体按钮、MC GUI 调色板、on/off 开关、可持久化的开关,以及可选的 Minecraft AE 像素字体。

PNG 素材已内联进 bundle(5 张 / 2552 字节);字体不内联——16 MB 的 assets/MinecraftAE-Pixel.ttf 由本包的 Host 半边按需发货,只在开关打开时才下载。所以本包现在两半都有:lib/index.js(Host,字体路由 + 设置命名空间)+ lib/client.js(浏览器)。

1.2.0 变更:① 移除「MC 字体颜色」十六色调色板(用不到),默认文字配色改为固定的四层递进; ② 两个开关接上 Host 用户设置文档,重启后保留;③ Host 半边新增设置命名空间注册。 从 1.1.x 升级 必须重启 dsh web —— Host 半边只在进程启动时 import 一次。


安装

前提:你已经跑起来过 dsh web,也就是 ~/.dsh/profiles/web/ 存在(Windows 是 C:\Users\<你>\.dsh\profiles\web\)。

1. 解压到你喜欢的任意目录,例如 D:\plugins\dsh-client-ui-mc-skin。

2. 装进 profile

dsh plugin --profile web add link:D:\plugins\dsh-client-ui-mc-skin

这条命令把参数转发给 profile 目录里的 pnpm,等价于在 ~/.dsh/profiles/web/ 下执行 pnpm add <你解压的路径>。

用 link: 前缀会把 profile 的 node_modules 指向你解压的目录本身——之后 node build.mjs 重新构建,重启即生效。想装成独立副本就去掉 link:(走 file: 语义,会拷一份)。

3. 登记进 bundles —— 已自动,无需手工

本包 package.json 里声明了:

"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }

并自带 cordis.patch.yml(一行 insert)。dsh plugin add 结束时会按"已装状态"重算 dsh.profile.bundles,看到这个声明就自动把包名追加进去,所以不用再手工编辑 package.json。

如果第 2 步没带 link:/没装成功,或者你手动把包解压到别处而不是走 dsh plugin add,那就还得自己把它加进 ~/.dsh/profiles/web/package.json 的 dsh.profile.bundles。profile 的 cordis.yml 是空数组,整棵插件树是由 bundles 里每一项逐层叠加出来的——只装上而不登记,插件不会被加载。

4. 重启 dsh web。

bundle 层是启动时读取的,所以第一次装完必须重启;之后改代码才走热重载。

这次重启不只是为了 bundle 层:字体路由是 Host 半边注册的,而 Host 半边的模块只在进程启动时 import 一次。1.1.0 起本包有了 Host 半边,所以从旧版升级上来也必须重启一次,否则浏览器那边一切正常、字体开关却会显示"加载失败"。


验证装好了

重启后:

  • 侧边栏左上角的标志变成 24×24 的像素图(minecraft24x.png),右边的 MINECRAFT 也是像素字体
  • 整个界面变成 MC 深色:工作区 #313233、对话区 #6B6B6B、按钮是 #C6C6C6 立体斜角、分页是绿板
  • 界面文字变成 Minecraft AE 像素字体
  • 设置 → 通用里多出两行:MC 主题 / MC 像素字体
  • 设置 → 模型 → 点「编辑」展开的面板是黑底 + 亮边框(见下文《浅色模式漏白底》)

想确认字体路由本身通了,可以直接打这条 URL:

curl -I http://127.0.0.1:3080/dsh-mc-skin/font.ttf
# 期望 200 + content-type: font/ttf + 一个 ETag

卸载

dsh plugin --profile web remove dsh-client-ui-mc-skin

这一步会顺带把它从 bundles 里摘掉(同样是自动 reconcile)。重启生效。

或者不卸载,直接去 设置 → 通用。两个开关完全独立:

  • MC 主题 关掉 → 配色、标志图、主题样式表还原成原生;像素字体如果开着,仍然继续生效
  • MC 像素字体 关掉 → 只还原字体,配色和标志图保持 MC 皮肤
  • 两个都关掉 → <style> 元素整个移除,和没装一样

分享给别人

本包没有任何构建期依赖,lib/ 是提交进仓库的成品,所以别人拿到就能装,不需要在他们机器上跑构建。

包体积主要是字体:assets/MinecraftAE-Pixel.ttf 16 MB,zip 压完大约 8 MB。不带字体也能跑(字体开关会显示"加载失败",界面用系统字体)——只要把 assets/MinecraftAE-Pixel.ttf 删掉再打包即可,别的都不用改。

方式一:发 zip

对方解压后(路径随意,比如 D:\plugins\dsh-client-ui-mc-skin):

dsh plugin --profile web add D:\plugins\dsh-client-ui-mc-skin

这里有个要提前说清楚的差别(两种写法实测行为不同):

写法 pnpm 实际记录 解压目录删掉/挪走后
add <目录> link:(SymbolicLink 指向解压目录) 插件失效
add file:<目录> 真实副本(拷进 profile) 仍然可用
  • 想让对方自己改图重构建:用第一种(link:),解压目录就是生效的源,改完 node build.mjs 重启即生效。
  • 想发出去就不管了:用 add file:<目录>,装完对方可以把解压目录删掉。

两种写法都不需要联网,也都不需要对方装构建工具。装完重启 dsh web(bundle 层是启动时读的)。

方式二:GitHub

仓库根目录就是包根目录(package.json / cordis.patch.yml / lib/ 都在根上),然后:

dsh plugin --profile web add github:<用户>/<仓库>

生产上建议钉到 tag 或 commit,否则别人拿到的是默认分支的浮动最新版:

dsh plugin --profile web add github:<用户>/<仓库>#v1.0.0

上 GitHub 前的检查清单:

  • lib/client.js 和 lib/index.js 都必须提交,别写进 .gitignore——git 安装不会跑构建
  • assets/MinecraftAE-Pixel.ttf 也是,别被 *.ttf 之类的规则误伤(本包 .gitignore 只忽略 node_modules/ 和编辑器杂物)
  • cordis.patch.yml 在仓库根,否则 dsh 不会把它登记进 bundles
  • 不要加 prepare 脚本:pnpm 会把它当成需要审批的构建脚本(allowBuilds)拦下来。本包只有 build / prepack,git 安装时都不会触发
  • package.json 里补上 repository / homepage / bugs(本包目前没有填)
  • 发版时改 version 并打 tag

GitHub 单文件上限是 100 MB,16 MB 的字体没问题;但注意 git 历史是不可变的,以后换字体会让仓库持续变大。

三个坑

  • npm registry 装不了。 本包没有发布到 npm,dsh plugin add dsh-client-ui-mc-skin 这种裸包名会失败。只能用本地路径或 GitHub。
  • 依赖 DSH 内部结构。 槽位名、主题 token 名、CSS Module 局部类名都是跟着当前 DSH 版本走的,见下文《已知限制》。
  • 升级到 1.1.0 要重启。 1.1.0 新增了 Host 半边(字体路由),而 Host 模块只在进程启动时 import。

它改了什么

分五块,都通过 cordis 的标准接口,停止时会干净回滚:

动作 机制
覆盖主题颜色 theme.overrideTokens(包名, tokens),返回 disposer
注入样式表 document.head 插一个带 data-plugin-css 的 <style>,卸载时移除
三个槽位 slots.register({ name, priority: -1 }, 组件) 到 sidebar.brand.mark / sidebar.brand.name / conversation.hero.brand.mark
两行设置 slots.register 到 settings.general.item(列表槽位,order 13 / 14)
字体路由 Host 半边 webServer.register({ kind: 'exact', path: '/dsh-mc-skin/font.ttf', ... })
开关持久化 Host 半边 settings.register('ui-mc-skin', schema);客户端 ctx.settingsScope.bind({ namespace }) 读写

开关持久化

两个开关(主题、像素字体)现在存在 Host 的用户设置文档里 —— 也就是 $DSH_HOME/settings.yaml(Windows 是 C:\Users\<你>\.dsh\settings.yaml)。 关掉主题再重启 dsh web,它仍然是关着的。

写进去的形状就是普通一节 YAML:

ui-mc-skin:
  themeEnabled: false
  pixelFontEnabled: true

两边各一半,字段名必须对齐:

位置 作用
Host src/index.js settings.register('ui-mc-skin', schema) 声明命名空间
Client src/client.js ctx.settingsScope.bind({ namespace: 'ui-mc-skin' }) 读写同一节

常量在两边各有一份(SETTINGS_NAMESPACE / SETTINGS_FIELD_*), 改一处必须改另一处 —— 这是两个 bundle 共享不了模块的老问题, 和 FONT_ROUTE 一样。

几个刻意的取舍:

  • settingsScope 是可选依赖。 拿不到时(连接处于 memory 模式、或 ui-settings 没装配)自动退化成纯内存行为:开关照常能用,只是不跨会话保留。插件不会因为 持久化能力缺席而变砖。
  • 写失败不回滚界面。 设置文档只读、断连、revision 冲突都只记一条日志, 本次会话内开关仍按用户点的样子生效,下次启动回到上一次成功落盘的值。 让一次写入失败把用户刚点的那一下弹回去,比不持久化更糟。
  • 采纳时不回写。 启动读到 Host 的值只用来 set 本地 store,不再写回去, 否则会自己触发一次无意义的写入 —— 和 ui-theme 的 adopt() 一致。
  • 先采纳再渲染。 apply() 里 bind + 采纳排在 applySkin() 之前, 否则会出现"先按默认值渲染一帧、再跳到存的值"的闪烁。

schemastery 已经不再使用 —— 见下节。本包现在零运行时依赖。

⚠ Host 半边不要 import 任何外部包(link: 装法的致命细节)

Host 半边曾经 import z from '@deepseek-ai/schemastery'。在 web profile 上能跑, 在桌面端(tauri profile)上直接 ERR_MODULE_NOT_FOUND —— 整个 Host 半边加载失败, 字体路由和设置命名空间一起消失,而且 Host 侧只报一行模块错误,界面看不出所以然。

原因不是"没装",而是Node 解析软链时用的是真实路径:

profile/node_modules/dsh-client-ui-mc-skin  →  软链  →  D:\12501\Documents\deepseek\dsh-client-ui-mc-skin
                                                       └─ Node 从这里向上找 node_modules
                                                          只到 D:\12501\Documents\deepseek\node_modules
                                                          ← 够不到 dsh 自带的依赖树

两个 profile 的差异就在这里:

node_modules/@deepseek-ai/ 结果
web profile 摊着 schemastery、cosmokit 解析成功(侥幸)
tauri / 桌面端 profile 空目录,依赖全在桌面端自带的 dsh 树里 解析失败

web profile 能跑纯粹是因为 pnpm 恰好把 schemastery 提到了那一层。换个 profile、 换个 pnpm 版本、换个 nodeLinker 设置,就会静默炸掉。

所以本包的 Host 半边只 import Node 内置模块(node:fs/promises、node:url)。 需要 dsh 的能力(webServer、settings)一律通过 ctx 拿,那是运行时的服务查找, 不走 Node 的模块解析。

如果你要给自己写的插件加 Host 半边,且打算用 link: 分发: 先在一个和开发环境不同的 profile 上装一遍再发布。 判据很简单 —— lib/index.js 顶部的 import 只能是 node:*。

为什么要自己写 schema

dsh-settings 对 schema 的用法只有两处(见 dsh-settings/lib/index.js):

// resolve():把「composition base + 用户层」合并后喂进来,取回补过默认值的对象
const value = schema(mergeLayers(base, section))
// describe():给配置界面看的 JSON Schema
registration.schema.toJSON()

所以只要「可调用 + 有 toJSON」就够,不需要完整 schemastery。 src/index.js 里那 30 行自包含实现满足这个契约,且对脏数据安全:

输入 结果
{} {themeEnabled:true, pixelFontEnabled:true}
{themeEnabled:false} {themeEnabled:false, pixelFontEnabled:true}
{themeEnabled:'yes', pixelFontEnabled:0} 两个都回落默认(不做类型强转)
null 两个都回落默认

坏字段不会让整个命名空间解析失败 —— 那会导致插件加载不出来。

选择器核对:类名是猜的,token 是真的

主题里那批 [class*="_xxx"] 选择器当初是照截图猜的。拿 deepseek-harness-master 源码核对后(脚本 .dsh-tools/verify-selectors.mjs),58 条里有 37 条是死规则—— 类名在源码里根本不存在,规则一直静默失效,不报错。

原因是这些面板的类名跟猜的完全不是一个形状:

面板 真实实现 真实类名 当初猜的
代码块 ui-primitives/markdown/CodeBlock.module.css .block .body .banner _codeViewer _codeBlock
文件差异 ui-primitives/DiffBlock.module.css .block .line .add .del _diffHeader _lineAdd _diffDel
读取面板 ui-primitives/ReadBlock.module.css .block .gutter .line _codeViewer _lineNumber
终端输出 ui-primitives/TerminalBlock.module.css .block .output .prompt _terminal _logView

都是短名(.block、.line、.add),根本没有 _diff* 这种长前缀。 所以那 8 条规则从来没有命中过任何东西。

修法是不再猜类名,改走 token。这四个面板的底色只来自一个 token:

CodeBlock.module.css   :16  background: var(--dsl-code-block-background)
                            └─ --dsl-code-block-background: var(--dsw-alias-markdown-code-block)
DiffBlock.module.css   :8   background: var(--dsw-alias-markdown-code-block)
ReadBlock.module.css   :11  background: var(--dsw-alias-markdown-code-block)
TerminalBlock.module.css:21 background: var(--dsw-alias-markdown-code-block)

--dsw-alias-markdown-code-block 浅色模式 = bluish-50 = rgb(249,250,251)(近白), 文字早被主题染成浅色 ⇒ 白底白字。现在把这一对 token 钉成 MC 深色即可, 一处生效、四个面板同时跟随,不再依赖任何类名。

同时修掉的还有:

  • _tab_ / _tabActive_ —— 多写了个尾下划线。CSS Module 产出的是 _tab_<hash>, 尾字符是 hash 首字符而不是下划线,所以这两条也是死的。去掉尾下划线即可。
  • [class*="Close"] —— CSS Module 不会把类名首字母大写;真实类名是 ui-primitives/Modal.module.css 的 .close(全小写),编译成 _close_<hash>。
  • --dsw-alias-bg-layer-4 —— design-platform.css 里只有 layer-1..3, 没有 layer-4,覆盖它等于写一个没人读的变量。

改完复跑核对脚本:58 条 → 19 条,死规则 37 → 0(剩下 2 处是记录历史的注释文字); 24 个被覆盖的 token 全部真实存在。

保留的 [class*="_rowCard"] > [class*="_editor"] 是有效的,别误删: rowCard 在 ui-settings-models/ModelsSection.module.css:55 确实存在, 只是它以 styles['rowCard'] 下标形式引用。核对脚本必须同时认 styles.foo 和 styles['foo'] 两种写法,否则会把这条误判成死规则 (第一版脚本就犯了这个错)。

为什么品牌槽位必须写 priority: -1

sidebar.brand.mark / sidebar.brand.name 是 single 槽位:同一优先级只允许一条注册, 官方品牌插件(dsh-client-ui-brand-official)已经占在 priority: 0 上。规则是最低者渲染 (lowest renders),所以要么用更低的优先级遮蔽它,要么就直接抛错:

Error: single slot "sidebar.brand.mark" already has a registration at priority 0
       (registered by Ba) — register at a different priority to shadow it (lowest renders)

这个错误只出现在浏览器控制台,Host 侧毫无反应(插件照常加载、主题色照常生效), 表现为"主题变了但左上角还是鲸鱼"。删掉 priority: -1 就会复现。

为什么字体走 Host 半边,而不是像 PNG 那样内联

因为客户端插件没有静态资源路由——dsh-client-modules 只发 /plugins/<包名>/client.js(和它的 .map)。 要把字体送进浏览器只有两条路:

方案 代价
base64 内联进 lib/client.js 产物从 30 KB 变成 22 MB,每次打开界面都要下载并解析这 22 MB
Host 半边注册一条路由 字体留在磁盘上,只在开关打开时才下载,之后走 ETag 复验

所以选了后者。实测数据(loopback,Chrome headless):

项目 值
字体文件 16 162 252 字节(63 449 个字形,覆盖整个 BMP,含中日韩)
首次传输(走了 webServer 的 gzip) 3 853 326 字节,约 0.43 s
第二次打开(If-None-Match) 304,0 字节
lib/client.js 产物 35 936 字节(和加字体前同一量级)

@font-face 的 src 指向 /dsh-mc-skin/font.ttf,这个常量在 src/index.js 和 src/client.js 里各有一份(两个 bundle 共享不了模块),改一处必须改另一处。

配套的 CSS 细节:

  • font-synthesis-weight:none —— 像素字体不能合成粗体,伪粗体会把 1px 笔画糊成 2px
  • html body,html body button,...{ font-family:"Minecraft AE",... !important } —— 这是给 那些自己不声明 font-family 的元素用的(下面这条说明它为什么不够)

行内 font-family 盖不住,加 !important 也没用

品牌名(MINECRAFT)一开始漏掉了像素字体,原因值得单独记一笔:

BrandName 这个 span 自带行内 font-family:"Courier New",...。样式表里那条 html body{font-family:"Minecraft AE" !important} 的主体是 body,span 只是 继承了 body 的值 —— 而继承是层叠里的最后一名,永远输给元素自己的任何声明, 哪怕那条声明不带 !important,也哪怕继承过来的那条带 !important。 两边根本不是同一条声明在竞争,所以堆多少个 !important 都没用。

要覆盖它只有两条路:让规则直接匹配到这个 span(如 html body span{...!important}), 或者让组件自己知道该用哪套字体。这里选了后者 —— BrandName 订阅字体开关,自己算 font stack。

字体要覆写变量,不是给元素设字体

同一个坑后来以更大的规模又踩了一次:品牌名修好之后,输入框里打的字、对话里的字 仍然不是像素字体。原因还是"元素自己的声明赢过继承",但这次没法靠匹配元素解决:

/* 应用自己的规则 */
body       {font-family:var(--dsw-font-family, -apple-system, …)}
.xxx_input {font-family:var(--dsw-font-family)}   /* ← 输入区,一个 contenteditable 的 div */

.xxx_input 是元素自己的声明,而 html body input{font-family:…!important} 匹配的是 html 和 body,对这个 div 只是继承 —— 继承永远输,!important 也救不了。 更要命的是输入区根本不是 <input>/<textarea>,是 contenteditable 的 <div> (打字后里面才生成 <p>),所以连 input 这个类型选择器都命中不了。

正确的做法是覆写变量本身,让应用自己那条声明去解析:

html:root body{--dsw-font-family:"Minecraft AE",… !important;
               --ds-font-family-code:"Minecraft AE",… !important;}
  • --dsw-font-family 被整套排版 token 引用(--dsw-font-base-16、--dsw-font-markdown-*、 --dsw-font-s-14 …共 40 多处),所以这一条就能把界面文字整体换掉。
  • --ds-font-family-code 是行内代码和代码块单独走的那条,不改的话代码区仍是 SF Mono。
  • 选择器用 html:root body 而不是 body:应用把 token 定义在 body 上, 而 html[data-ds-dark-theme] body 这类选择器比 html body 更具体,得抬一级才稳。

原来的 html body button,input,…{font-family:…} 保留成兜底(万一某个组件把字体写成字面量 而不是 var()),但真正起作用的是上面这条变量覆写。

透明度做层次,在浅底上会糊

文字层次原来是用半透明白递降做的:primary #FFFFFF / secondary .74 / tertiary .56 / caption .44。在侧边栏 #313233 上勉强能看,但对话区的底是 #6B6B6B 这种中灰, 半透明白压上去会直接糊掉:

层级 侧边栏 #313233 对话区 #6B6B6B
secondary .74 8.5:1 3.5:1
tertiary .56 5.2:1 2.4:1
caption .44 3.6:1 1.8:1

数字上"及格"不等于看着清楚 —— 用户报的「黑背景地方字体变灰看不清」就是这个。

1.2.0 起这套值按"最差底色"定。对比度算的是相对亮度,所以瓶颈不是最暗的 侧边栏,而是最亮的对话区 —— 基准取对话区,侧边栏自然满足。主色也从纯白 #FFFFFF 降到 #F2F2F2:纯白压在中灰底上会过曝,长文阅读发涩。

层级 值 对话区 #6B6B6B 侧边栏 #313233
primary #F2F2F2 7.4:1 15.1:1
secondary .90 5.4:1 11.0:1
tertiary .80 4.1:1 8.4:1
caption .72 3.3:1 6.8:1

层级之间靠 8~10 个百分点的透明度差拉开,而不是靠"调暗到快看不见"。 层次感要靠字号和字重。

浅色模式漏白底

DSH 的设计 token 分两套:body{...} 是浅色值,body[data-ds-dark-theme]{...} 是深色值。 本主题用 theme.overrideTokens 把值写成 body 的行内样式,所以两种模式下都是同一个值 —— 但前提是这个 token 得在覆盖表里。

一旦漏掉,浅色模式下那个 token 就还是近白色,而文字早被主题染成浅色 ⇒ 白底白字。 设置 → 模型 →「编辑」展开的面板踩的就是这个:组件写的是 .editor{background:var(--dsw-alias-bg-module-platform)},而 bg-module-platform 浅色值是 #f9fafb。

排查办法:把 DSH 的设计表整个解出来,逐 token 比对浅色值和深色值——凡是"浅色近白、 深色才深"的面板 token 就是可疑的。按用户实际报过的位置,目前钉了 10 个:

token 浅色时是 现在 用户在哪看到的
--dsw-alias-bg-module-platform #f9fafb #1B1C1D(黑底) 设置 → 模型 →「编辑」展开的面板
--dsw-specific-tip #f5f6f7 #353638 任务面板(展开的待办列表)
--dsw-alias-bg-multi-select #f9fafb #2A2B2C
--dsw-alias-bg-mask-drop #ffffffb3 #000000B3
--dsw-alias-button-elevated-fill #fff #3A3B3C
--dsw-alias-button-floating-fill #fff #3A3B3C
--dsw-alias-button-ghost-active-fill #ebeef2 #3A3B3C
--dsw-alias-interactive-bg-hover-solid #f1f3f5 #3A3B3C
--dsw-alias-scrollbar-bg-l1 #e5e5e5 #313233
--dsw-alias-scrollbar-bg-l2 #e5e5e5 #474848

--dsw-specific-tip 的深色值是 #353638,正好等于本主题的 --dsw-alias-bg-layer-3, 所以这不是自己配色,是把这个面钉回设计系统里它本该有的深色。

⚠ 范围纪律(踩过的坑)。 上面这个"逐 token 比对"的办法一旦用过头就会出事: 全套设计表里"浅色近白、深色才深"的 token 有 44 个。曾经照单全钉了 29 个, 结果把菜单、用户气泡、侧边栏选中态等用户根本没提过的颜色一起重刷了。

判定一个面要不要改,得同时满足三条:

  1. 它在浅色模式下确实近白;
  2. 它承载文字(纯装饰的底色改了也白改);
  3. 用户明确报过这个位置。

只满足第 1 条的,一律不动。枚举工具留在 .dsh-tools/token-leak-truth.mjs。

顺带一个反例:token 浅色近白 ≠ 就该深色化。有些 token 的语义是"反色文字" (--dsw-alias-label-primary-foreground 配 button-primary-fill、 --dsw-alias-label-primary-inverted 配 button-contrast-fill),它们的浅色值就是 #fff, 深色化会让"深底配深字",正好反过来。

顺带扫了前端自带的 CSS:整个 dist 里只有一个浅色字面量 (body{background:var(--dsw-alias-bg-base,#fff)},而 bg-base 本来就被覆盖), 所以这类漏底只会来自 alias token,不会来自字面量。

那块面板另外加了一条规则补亮边框(就是"白框黑底"):

[class*="_rowCard"] > [class*="_editor"]{border:1px solid #C6C6C6 !important;}

限定在 _rowCard 里那一层是有原因的:组件在 addCard / setupCard 里复用了同一个 _editor 类,但故意写成 background:0 0。不加限定的话,那些地方也会被套上一圈框,出现双重边框。

调色板来自一组 16×16 的模板图(button_template / page_template / text_template / background_template_left / background_template_right / 滑轮模板),全部按像素复刻成 CSS。


换字体

字体放在 assets/MinecraftAE-Pixel.ttf,直接替换这个文件即可,不用重新构建——Host 半边按 大小 + mtime 算 ETag,换了文件下次请求就是新的。

要注意的是 CSS 里写的族名是 "Minecraft AE"(src/client.js 的 FONT_FAMILY), 所以换的字体要么内部 family 就叫这个,要么同步改 FONT_FAMILY 后 node build.mjs。

想换成别的路径、或者不打包字体而是用用户自己的文件,改 src/index.js 里的 FONT_PATH。


改图 / 改代码重新构建

assets/ 里放两类东西,处理方式不同:

文件 用途 是否内联
minecraft24x.png 侧边栏标志,24×24(1:1 渲染) 是
minecraft34x.png 新会话首屏标志,34×34(1:1 渲染) 是
on.png / off.png 开关两态,30×16 是
close.png 关闭按钮,15×15 是
MinecraftAE-Pixel.ttf 像素字体,16 MB 否,走 Host 路由

换掉图之后:

node build.mjs

lib/ 是纯产物目录,两个半边都由这一步生成:

  • src/client.js → lib/client.js:把 assets/*.png 转成 data URL 注入 __ASSETS__ 占位符
  • src/index.js → lib/index.js:Host 半边原样搬运(只加一条"勿手改"横幅)

不需要任何依赖,只用 Node 内置模块,也不联网。

渲染端会读 naturalWidth,取不大于目标尺寸的最大整数倍——所以 32×32 的图放进 34 的框里会按 32 渲染(1:1),而不是拉伸。


已知限制

  • 开关状态已持久化(1.2.0 起),但只在 Host 提供 settings 服务时。连接处于 memory 模式或 ui-settings 未装配时退化为内存态,重启回到默认。设置行末尾会自动据此显示或隐藏那句持久化说明。
  • 主题是深色单套。 浅色/深色模式目前是同一组值,切换不改变外观——因为模板图本身就是深色体系。本主题会把上表那 10 个面板 token 也钉成深色,所以即使应用处于浅色模式也不会漏白底(只限用户报过的那几个面,见《浅色模式漏白底》里的范围纪律)。
  • 第三方插件的自绘界面不在覆盖范围内。 本主题只负责 DSH 自己的 token 和类名;别的插件如果写死了浅色背景,那部分仍是它自己的样子。
  • 字体文件 16 MB。 首次开启要下 3.85 MB(gzip 后)。本机 loopback 无感,但如果哪天把 DSH 挂到网络上,这就是一笔真实流量。想变小只能做字体子集化——实测 ASCII+拉丁 602 个字形只占 60 KB,但中日韩统一表意区就要 6.2 MB,所以"只留常用字"可以做到 ~1 MB,代价是生僻字回落到系统字体。
  • 像素字体在小字号下是设计尺寸。 这份字体是 16×16 点阵风格,12–15 px 下最锐利;界面缩放(Ctrl +/-)到非整数倍时笔画会糊。
  • 依赖内部类名后缀。 按钮/分页/卡片等规则按 [class*="_xxx"] 匹配 DSH 的 CSS Module 局部名。DSH 若把这些名字改掉,对应规则会静默失效(不会报错,只是那部分回到原生样式)。别靠肉眼判断是否失效 —— 跑 node tools/verify-selectors.mjs,它会拿真实源码核对每一条。
  • 依赖内部槽位名。 sidebar.brand.mark / sidebar.brand.name / conversation.hero.brand.mark / settings.general.item 同理,改了就少渲染一部分。
  • 文字色不可调。 十六色调色板已移除;想改主色就改 src/client.js 的 TEXT_PRIMARY 再 node build.mjs。

关于"像应用程序那样的界面"

存在两类桌面端,别混淆 —— 它们的插件兼容性不同:

A. 官方桌面端(apps/desktop,Electron)

就在 DSH 源码树里(@deepseek-ai/dsh-desktop,Electron 44)。它不是把网页塞进浏览器窗口:

  • 自带上游 Node.js 子进程跑 dsh,不开监听端口
  • dsh-app:// 提供客户端资源,分帧字节管道承载 Fetch 与流式响应
  • 自带 dsh 生产依赖树 + pnpm;签名、自动更新、单实例锁齐全
  • Electron 独占 $DSH_HOME/profiles/desktop,与 CLI 共享 workspace/settings/credentials, 但不共享可执行包、插件激活状态、lockfile
pnpm run package:desktop:win:x64:unsigned   # 本机免签名测试包

产物在 apps/desktop/.desktop-build/targets/win-x64/unsigned-artifacts/。 正式签名需要 EV 证书 + SafeNet token,见 apps/desktop/README.md。

⚠ 官方 Electron 版不提供 webServer(它的 README 明确写了 "Desktop does not provide a webServer")。所以在它上面本包的字体路由不会注册,像素字体开关会 显示"加载失败";主题配色不受影响(走 theme.overrideTokens,不需要 HTTP)。 想在那里用像素字体,得把字体改成内联进 lib/client.js(代价见《为什么字体走 Host 半边》)。

B. 第三方 Tauri 桌面端(本机在用的那个)

D:\AAAai\Deepseek Harness Desktop(deepseek-harness-desktop,Rust + Tauri + WebView2)。 它和 A 完全不是一回事,实测差异:

A. 官方 Electron B. 第三方 Tauri(本机)
端口 不开端口 开 HTTP 端口(实测 3080)
webServer ❌ 无 ✅ 有 —— 所以字体路由正常工作
profile 目录 profiles/desktop(Electron 独占) 用 active_profile 指向任意 profile(本机是 web)
原生菜单栏 无 有(见下)

所以本包在 B 上两半都完全生效:配色、字体、持久化都正常。

B 的原生菜单栏改不了

B 的顶部有 文件 / 帮助 这类菜单,那是 Tauri 原生菜单,不在网页 DOM 里。 二进制里能直接找到编译进去的菜单项与 API:

文件 File · 帮助 Help · 关于 Desktop About Desktop · 退出 Quit
全屏幕 Full Screen · 运行日志 Run Logs · 检查更新 Check for Updates
plugin:menu|  ·  set_as_app_menu  ·  MenuItem  ·  Submenu

判定规则很简单:Windows 原生控件画的,CSS 够不到;theme.overrideTokens 也不管它。 插件跑在浏览器沙箱里(只能用 React / ctx / host.call / styles), 没有任何接口能重绘宿主窗口的菜单栏。

桌面端内置的 dsh-tauri-* 插件(dsh-tauri-rightclick 等)看起来能碰原生, 是因为桌面端为它们写死了 bridge 通道,只开放会话/工作区/宠物/右键菜单这些, 其中不含菜单栏定制。那条通道不是可复用的公开接口。

能改 / 不能改的分界:

区域 谁画的 本主题能改吗
侧边栏、对话区、输入框、按钮、分页 网页 DOM ✅
顶部 文件/配置/帮助 Windows 原生菜单 ❌
窗口标题栏、最小化/最大化/关闭 Windows 原生 ❌
宠物、右键菜单 桌面端插件的网页层 ⚠️ 能改,但那是别人的插件

安装到桌面端(B)时踩过的坑

  1. profile 不是 desktop —— 读 %APPDATA%\io.github.hairyf.deepseek-harness-desktop\.store.dat 里的 active_profile 才知道实际用的是哪个(本机先是 tauri,后手动切成了 web)。
  2. 桌面端会切到兜底 profile —— 装完插件后它新建过 safe profile 并切了过去 (safe 只含 9 个内置 dsh-tauri-* 插件,没有第三方插件), 表现是"插件装好了但界面毫无变化"。日志里会先出现:
    dsh-client-ui-mc-skin 的 bundle patch 含复杂结构,无法热挂载(需重启一次)
    Profile manifest created with the official web template: ...\profiles\safe\package.json
    
  3. Host 半边改动必须重启 —— 它只在进程启动时 import 一次。

许可

MIT。素材(assets/*.png)与字体(assets/MinecraftAE-Pixel.ttf)由使用者提供,字体版权归其原作者,本仓库只做打包与接线。

—/ 5

No ratings yet

Verified DSH bundle

Commit cf8f67c999f8

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