Bundle
dsh-fonttune
Font plugin for the DeepSeek Harness Web GUI: the conversation gets its own font, size, line height and weight, the interface font and weight follow it, code keeps its own axis with ligature control, and whole setups save as presets.
- Source
- LyaxZ
- stars
- 2 stars
- License
- MIT
- Updated
- Updated 2 days ago
Readme
# dsh-fonttune
> [English](README.en.md) | **中文**
**DeepSeek Harness(DSH)字体插件**:**对话**(会话 Markdown 正文)拥有完整的排印控制——字体族、字号、行高、字重;**界面**(设置页、侧栏、工作区、按钮)提供字体与字重并**默认跟随对话**;**代码**独立成轴,另有连字与特性开关;整套配置可存为**预设方案**。配置入口是 **设置 → 插件 → 插件配置** 里的原生卡片(分区收起 + 摘要),改完即时生效。
## 功能
- **三套排印**:对话(会话 Markdown 正文、表格与标题)排在第一位、拥有全部轴;界面(设置页、侧栏、工作区与标题按钮)提供**字体与字重**、默认跟随对话(可关);代码独立成轴
- **界面 / 对话 / 代码字体** —— 各自独立的字体回退列表,拉丁字体在前、中文字体在后;留空表示完全沿用 DSH 的字体栈
- **字号偏移** —— **对话**字号 -3 ~ +6 px,加在 DSH 自己的「对话字号」之上(官方数值是基准),段落、标题与行高一起跟随;代码字号独立成轴,-3 ~ +6 px。**界面没有字号轴**:经实测 DSH 的设置页、侧栏与工作区把字号写死在上游 CSS 里,唯一官方字号接口就是「对话字号」
- **字重** —— **对话**、**界面**与**代码**各自 300 ~ 600 任意整数(未设置即 400,读数就显示 400)。界面字重是一条把对话 Markdown 整棵子树排除在外的 `body, body *` 规则:真机实测(活页面计算样式)它把界面的文本元素全部改到目标字重、对话 markdown 里的元素一个都没动,所以**改界面字重不会碰到对话**(对话的粗体、标题粗细也原样保留)
- **行高** —— **对话**行高用倍率(100% ~ 160%);代码行高用加法偏移(-4 ~ +8 px)。界面没有行高轴(同字号的原因)
- **代码连字** —— 三档开关(默认 / 开启 / 关闭,浏览器默认即开启,关闭可还原 `=>`、`!=` 这类连字),高级模式另有 `font-feature-settings` 特性值输入
- **拒绝合成样式** —— 禁用浏览器为中文伪造的斜体与粗体(中文字体没有斜体字形,浏览器会硬掰)
- **界面跟随对话开关** —— 界面分区顶部的「跟随对话设置」开关(默认开启):开启时界面沿用对话的**字体与字重**,界面自己的两个控件隐藏;关闭后界面用自己的字体与字重,与对话互不影响。字号与行高只属于对话(界面没有这两个接口)
- **预设方案** —— 编辑模式正下方的下拉选择框:五个默认配置开箱即用,点开选一个即应用;选中方案后所有改动自动存入该方案,右侧「改名 / 导入 / 导出」管理方案(导出 JSON 分享、导入合并)。剪贴板不可用时(非安全上下文、或没有剪贴板权限)导出不会只给一段被截断的提示,而是把完整 JSON 放进一个已选中、只读的文本框,按复制快捷键即可,复制后自动收起;任何**写设置失败**(宿主拒绝、只读部署等)都会在当前行显示出来,而不是让控件悄悄弹回旧值
- **西文 / 中文分栏(简单模式)** —— 编辑模式开关:简单模式每套字体只给「西文」「中文」两个单选格,高级模式是完整的字体栈编辑器;两种模式共用同一条栈,来回切换不改动已排好的顺序
- **选字体面板** —— 内置 等宽 / 中文(CJK)/ 拉丁 / 通用 四组预设,Chromium 下再补上「本机已安装」分组;四组预设**任何情况下都在**(包括搜索时,也包括枚举本机字体失败时),本机分组只列出预设分组没有的字体,所以同一个字体不会出现两次;搜索不到的名字可以直接「使用 xxx」新建。字体枚举被浏览器拒绝时会在一段时间后自动重试(用户在浏览器里授予字体权限后,重新打开面板即可看到本机字体)
- **拖拽排序** —— 已选字体是 chip 列表,可拖动调整回退顺序,同时保留前移 / 后移按钮(键盘与触屏可用)
- **分区预览** —— 对话 / 界面 / 代码分区展开后在分区末尾各有一个预览框,只预览该分区的部分
## 兼容性
**一份包同时支持 0.1.5-rc.x 与 0.1.7-alpha.x 两条宿主线**,安装方式与宿主线无关:两条线都验证过「设置卡片可用、样式真的作用到页面、卡片上的改动写得进去」。
| 插件版本 | 支持的 DSH 版本 |
| --- | --- |
| **0.2.4**(最新) | 0.1.5-rc.1 / rc.2 / rc.3、0.1.7-alpha.1 / alpha.2 |
| 0.2.3 | 0.1.5-rc.2 |
| 0.2.2 | 0.1.5-rc.2 |
| 0.2.1 | 0.1.5-rc.2 |
| 0.2.0 | 0.1.5-rc.2 |
`engines.dsh` 声明为 **`>=0.1.5-rc.1 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0`**(预发布版本必须显式留分支:node-semver 只在一个范围的某个比较符与该版本落在同一个 `major.minor.patch` 元组、且自身带预发布标签时才放行,写成 `>=0.1.5-rc.1` 匹配不到 `0.1.7-alpha.1`)。两条线的差别**全部在运行时二选一,不看版本号**:
- **设置服务**:0.1.5-rc.x 是 `settingsScope.bind({namespace})`(按命名空间),0.1.7-alpha.x 换成了 `configForms.get(<profile 条目 id>)`(按条目 id,id 由安装方的 profile patch 决定,所以插件靠「宿主服务的那份 schema 里有没有本插件自己的字段」来认领)。两者都是 `getSnapshot/subscribe/set/unset` 同一张脸,插件只写一套。
- **配置卡片座位**:rc 线是 设置 → 插件 → 插件配置 里的一个 keyed 单元(`settings.plugin.item`);alpha 线删掉了这个槽位,改成在「插件」页的官方插件列表里贡献一个条目(`plugins.item`,内置那些设置页就是这么挂的),并另外提供「已安装成 bundle 时」的每包页面(`plugins.bundle.config`)。三个座位都注册,宿主不声明的那个是惰性的。
- **宿主半的配置来源**:alpha 把 schema 标成 `.volatile()` 之后,宿主半拿到的配置字段是 cosmokit 的 volatile 引用(`{get(),[write]}`)而不是普通值,首帧样式行会因此变空;插件先把引用解出来再用。
- **`inject` 只声明两条线都有的服务**(`slots`、`locale`):声明一个宿主没有的服务会把整包 park 住——alpha 上声明 `settingsScope` 会让整个 Web 界面起不来。设置服务一律用 `ctx.get(name)` 现查。
- **alpha 的表单是「条目 id + 描述视图」**:宿主 `describe()` 还没答话时,插件不会去猜一个条目名(猜错的话每次写入都会被告主拒绝),而是先用一个等待态座位顶住,等宿主把视图发出来再接管并立刻重读。
市场条目同时声明这些兼容信息(`package.json` 的 `engines.dsh`、`dsh.compatibility.dshReleases` 与 `peerDependencies`),安装前可据此判断这一版需要的宿主版本。
## 安装
通过 DSH CLI 安装:
```
dsh plugin --profile web add dsh-fonttune
```
也可以从 GitHub 安装:
```
dsh plugin --profile web add github:LyaxZ/dsh-fonttune
```
或以本地目录安装:
```
dsh plugin --profile web add <插件目录路径>
```
安装后重启一次 DSH 并打开 Web UI;之后改设置不需要重启。
## 使用
- **打开设置** —— 设置 → 插件 → 插件配置 → 展开「字体增强」卡片
- **分区导航** —— 卡片展开后自上而下是:编辑模式、预设方案、**对话 → 界面 → 代码**三个收起的分区(对话在最前,因为它拥有全部轴),以及直接平铺在分区下面的「拒绝合成样式」;分区标题行右侧显示当前值摘要、箭头固定在最右,同一时刻最多展开一个分区
- **换字体** —— 简单模式下点「西文/中文」格子选字体;高级模式下用「添加字体」与 chips 编辑器维护完整回退列表
- **调整字号/字重/行高** —— 滑块就在各自分区里,拖动松手即生效(拖动时读数连同单位 % 一起显示);改过的字段标「已修改」并可单独重置,卡片底部有「全部重置」
- **对话优先** —— 对话分区可以设置字体、字号、行高、字重四项;界面分区默认只有「跟随对话设置」开关(默认开启),关掉后才出现界面自己的字体与字重
- **关掉跟随不会跳变** —— 关掉开关的那一刻,界面会把**正在显示的**字体与字重写成自己的值(跟随期间它本来就是这两项),所以画面零变化、滑块从当前值继续调;对话没设的那一项则清掉界面自己的字段、保持 DSH 默认。想回到 DSH 自己的粗细点该项的「重置」(读数会回到 400)
- **界面字重不会碰到对话** —— 界面字重是唯一必须铺满整页的轴(DSH 把小标签的粗细写死在上游 CSS 的类规则里,窄规则够不到它),实现上是一条把**对话 Markdown 整棵子树排除在外**的规则,所以对话的段落、标题、粗体都不受影响
- **字重读数** —— 未设置的读数显示 400(DSH 正文的就是 400,滑块也停在 400),不会出现「未设置」这类字眼
- **预设方案** —— 编辑模式下方的下拉选择框(默认配置 1 ~ 5,框内右侧有与卡片一致的下拉箭头),点开选择即切换并自动保存后续改动;「改名」重命名当前方案,「导出」复制 JSON,「导入」粘贴合并。操作结果(如「已复制到剪贴板」)显示在下面那行的**右侧**,约 2.6 秒后自动淡出,不会顶动任何布局
- **预览** —— 对话 / 界面 / 代码分区展开后,各自分区末尾有一个预览框,只渲染该分区的样式
- 设置保存在 DSH 的设置文档里(`settings.yaml`),跟随配置走
## 开发
- `src/shared.cjs` —— 纯函数核心:字体名消毒与解析、配置归一化、注入样式的生成
- `src/index.mjs` —— 宿主半:注册 `dsh-fonttune` 设置命名空间(schemastery schema,含长度与取值范围校验),并通过 `webserver/index-inject` 把一个打过标记的 `<style>` 随首页下发,首帧即是设置里的字体;那个元素随后由浏览器半**认领并接管**(页面里只有这一份样式表,规则可增可删)
- `src/client.js` —— 浏览器半:设置卡片、选字体面板、字体枚举与样式注入
- `build.mjs` —— 零依赖构建:内联共享核心、套上 `window.__ModuleLoader__.load({id, factory})` 外壳、拷贝宿主半,并校验客户端 bundle 只 require shell 预注入的模块
- `test/run.mjs` —— 离线检查(自建 DOM / cordis / 设置面替身、真实 schemastery schema、CSS 生成与注入、消毒对抗用例、双语文案键一致性、选字体面板的分组规则、令牌轮询的省电闸门);`test/render-card.mjs` 用 mini React hooks 运行时真实渲染卡片组件(含强制展开态与强制状态,覆盖 0.1.0 那类发布阻断崩溃),把**每个控件的回调都点一遍**(未接线的回调只有真点下去才会暴露),并**渲染第二次**覆盖「状态变化后 hook 顺序改变」这一类崩溃;`test/artifacts.mjs` 校验 `lib/` 与当前 `src/` 逐字节一致(`npm run verify` 跑全部三套)。devDependency 钉的是**最新的** schemastery(`^3.18.4`),也就是 alpha 那套方言——它带 `.volatile()`,解析出来的字段是**引用**而不是值,所以这几套检查都通过宿主半导出的 `plainConfigValue` 取值(和插件读配置同一条路);运行时用的是**宿主自带的那份**(rc.2/rc.3 是 3.18.2,没有该修饰符,那条形态由真机探针覆盖)
- 需要真实页面的检查走 `npm run verify:browser -- "<带 token 的地址>"`(先起一个受管实例:`dsh web --port 0 --no-open`,它会打印地址)。默认只跑**只读**的几支(`browser-probe` / `ui-walk` / `host-line-probe` / `style-verify`);加 `--writers` 再跑会**写设置命名空间**的几支(`slider-walk` / `split-walk` / `split-verify` / `weight-verify`,各自先快照用户层、退出前还原),`--only a,b` 挑子集;每支的退出码就是判定。`test/set-user-layer.mjs <url> '<json>'` 是在被中断的运行之后还原用户层的工具
- `test/host-line-probe.mjs` 是**与宿主线无关**的那一支:只断言两条线都必须成立的事——页面无报错、插件只维护**一份**样式表、设置真的作用到文档(`DFP_EXPECT_FAMILY` / `DFP_EXPECT_WEIGHT` 给了就顺带断言计算样式)、设置页里能找到并渲染出 `.dfp-card`(哪条线的座位都行)、以及从卡片上改一个值真的写进去。换宿主线(rc ↔ alpha)后先跑它;调试「alpha 上卡片写不进去」这类问题时设 `DFP_PROBE=1`,插件会往控制台打一行 `[dfp-probe]`(服务了哪些命名空间、认领了哪个、座位当前是什么状态)
- `test/weight-verify.mjs` 在真实页面里量三条字重轴(界面 / 对话 / 代码)的独立性与「样式表只有一份、不刷新关掉跟随立即回落」;`test/split-verify.mjs` 量对话/代码两条字号轴的独立性,外加**退役的界面字号轴什么都不动**;`test/slider-walk.mjs` 与 `test/split-walk.mjs` 驱动真实卡片(拖动不写、松手才写;简单模式两格与整条栈的往返);另有市场条目的维护脚本 `test/market-pr.mjs`(status / update / refresh / reopen / open / about)与诊断脚本 `test/market-inspect.mjs`(PR 状态、评论、CI 与分支差异)
- `docs/` —— 0.2.0 的[背景调研](docs/research-0.2.0.md)与[功能规格](docs/spec-0.2.0.md)(设计阶段的记录,含当时的取舍理由;与最终实现不同的地方以 README/CHANGELOG 为准,两份文件文首都有说明)。**不在 npm `files` 清单内,不随包发布**
- 客户端模块能 require 的只有 shell 静态表里的模块(`react`、`react/jsx-runtime`、`react-dom`、`@deepseek-ai/cordis`、`@deepseek-ai/dsh-client-*` 等);`dsh.client.inject` 只是加载顺序声明,不是 require 许可
- 改完源码要**先构建再刷新**:`node build.mjs`(或 `npm run watch`)把 `src/` 生成到 `lib/`,浏览器加载的是 `lib/`,只改 `src/client.js` 不构建的话页面看到的还是旧包;`npm test` 只检查功能,`node test/artifacts.mjs`(在 `npm run verify` 里)专门校验 `lib/` 与当前 `src/` 逐字节一致。改宿主半(`src/index.mjs`)或 `cordis.patch.yml` 需重启 DSH
## License
MIT © 2026 LyaxZ
Install
dsh plugin --profile web add github:LyaxZ/dsh-fonttune
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-fonttune from the hub
- This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.