dsh-theme-endfield
Edge Intelligence Theme — 参考《明日方舟:终末地》(Arknights: Endfield)官网视觉风格制作的 DSH(DeepSeek Harness)Web 主题插件(动态 Cordis 插件,Client 半部)。
参考:https://endfield.hypergryph.com(国际版 https://endfield.gryphline.com)
设计语言
还原终末地官网的「工业编辑风」:
- 色板:奶油纸底
#f6f6f3/ 墨黑文字#101110/ 信号黄强调#fff500(暗色:纸底#101110、面板#181a18、文字#f5f5f0) - 字体:Arial / Helvetica Neue / PingFang SC / Microsoft YaHei,开启
tnum等宽数字 - 直角化:按钮、输入框、卡片、菜单、标签、气泡全部
border-radius: 0(状态圆点/头像/加载圈保留圆形) - 信号黄交互:悬停反色、焦点环、输入光标、滚动条、表格行、按钮、激活项、Markdown 标记
实现方式
client.js 通过三种机制作用于 DSH Web 界面:
theme.overrideTokens— 覆盖 13 个主题令牌(亮/暗双色),映射终末地官网色板;styles.insert— 注入全局样式:字体栈、直角化、信号黄强调、中和 DSH 内部 DeepSeek 品牌蓝、hover 反色、表格/按钮/徽章/头部动作黄化等;- 设置页开关 — 「终末地主题设置」提供背景水印、水印保持显示、启动加载动画、主题总开关、圆角模式五个开关,均由
localStorage持久化。
启动加载动画(默认关闭)
参考终末地官方主视觉:整屏黑底,左边缘 10px 信号黄进度轨自上而下填充。刻度 + 百分比 + 状态行跟随进度轨的填充端一起下移(参考图中读数就贴在黄条前端下方),并做了下边界钳制,接近 100% 时不会滑出屏幕。
播放时序(开启后每次刷新页面播放一次):
- 进度(约 1.75s,
easeOutCubic缓出)——黄条自上而下填充,读数随之下移; - 黄色向右铺满(520ms)——到 100% 后,进度轨从 10px 横向展开成整屏黄色,覆盖品牌块与读数;
- 整屏淡出(620ms)——黄色连同整块遮罩淡至透明,节点随即从 DOM 移除。
设置行提供「预览」按钮,可不刷新页面直接重播一次。
海报式排版
版式取自主视觉(参考图 1184×685 实测)。要点:品牌块内部左对齐(各行共用同一条左侧节奏线),但整体摆在画面右侧——这个组合才是参考图的观感;若改成右对齐(左边缘参差)则完全变味。
END/FIELD上下两行紧排堆叠,不含中文字标(参考图字标为纯拉丁);- 小字冒头
DEEPSEEK HARNESS在字标之上,双 V 形黄色箭标外突到左侧留白; - 说明行、方块指示条、底部标语依次收束在同一节奏线上。
定位改为「锚右边距」而非「锚左百分比」。 早前用 --edge-rail: 73% 定左边缘,实测暴露两个问题:左百分比只决定文字从哪开始,其宽度自由地向右伸展,于是「离右边缘多远」——也就是肉眼真正读到的那段留白——从来不是被控制的量;且纯 vh 取值在宽而矮的窗口里会把字标缩小。现在改为 right: var(--edge-gap),把右侧留白变成显式声明值,节奏线由最宽的一行自然决定。
实测对照(参考图为逐像素扫描;实现为 1440×900 渲染实测):
| 项目 | 参考图 | 修改前 | 修改后 |
|---|---|---|---|
| 字标 cap(占画面高) | 4.23% | 3.01%(偏小) | 3.72% |
| 右侧留白 | 7.4% | 10.5%(且不受控) | 12%(显式声明) |
| 行距 / cap | 1.10 | 1.10 | 1.10 |
| 右侧溢出 | — | 无 | 无 |
字标尺寸为 --edge-word: clamp(26px, min(5.2vh, 4.8vw), 64px):
min(5.2vh, 4.8vw)取较小轴,因此无论窗口「矮」还是「窄」,品牌块都不会挤进左侧进度轨;26px下限保证辅助小字在极小画面下仍可读;64px上限是实测结论——不加它时,2560×1440 下 cap 占比会掉到 2.69%(大屏上字标反而变小),加上后稳定在 3.2% 以上。
跨视口验证:520×900 到 2560×1440 共 13 种视口逐一实测(用精确尺寸的 iframe,vh/vw 与媒体查询按真实视口解析),全部无任何方向溢出,cap 占比维持 3.1–3.9%,与进度轨/读数的水平间距 ≥150px。字标仍刻意略小于参考图:参考图是整幅出血海报,而这里是一闪而过、盖在应用之上的遮罩。整块尺寸只由 --edge-word 一个变量控制(各行均为其比例)。
三处易错细节:
- 字标行
margin-left: -0.055em抵消 Arial 左侧字身空隙,让字形墨边(而非字盒)落在节奏线上; line-height取 0.80 而非 1.10——参考图的 1.10 是墨边间距,而line-height覆盖整个 em 盒(Arial-900 约为 cap 的 1.38 倍),直接写 1.10 会渲染成松散的 1.52;- 品牌块不能加
max-width:各行均为nowrap,宽度上限只会裁字而不换行;应改为移动--edge-gap。
两个实测踩坑,都是「只看代码看不出来、只有量像素才发现」的:
- 小字号必须有 px 下限。 各辅助行原先只写
em比例,缩小整块后实测只剩 3px 高、峰值亮度 94 的灰糊——肉眼是一道模糊而非文字。现在统一改为max(8~10px, …em);实测每行笔画分组数 16~37(模糊时仅个位数),确认为可读字形。 - 标语用 Arial Narrow 压缩字体。 早前判断「Arial 下标语无法满足参考比例」只对了一半:探测渲染器后发现 Arial Narrow 确实可用(同一探测串 245px vs Arial 299px);在缺少该字体的机器上会回退到 Arial,因尺寸是比例值而非固定 px,仍然不会溢出。
进度轨几何同样为实测值:轨宽 10px、刻度 4×16px、数字 cap-height 28px、状态行 #666。
工程要点:
- 进度由墙钟时间推导(非逐帧累加),不会漂移;
- 双时钟驱动:
requestAnimationFrame负责跟随刷新率的平滑更新,setInterval作为兜底——实测 rAF 在后台/被遮挡标签页与无头渲染器中会在首帧后停摆,仅靠 rAF 会让全屏遮罩永久留在屏幕上; - 收尾动画同样由 JS 驱动,不用 CSS transition。 这是实测结论而非偏好:最初「铺满 + 淡出」写成
transition: width …,在验证渲染器里完全不执行——transitionrun/transitionstart/transitionend一个都不触发,width过了时长仍停在 10px。做过对照实验(transition 写在状态选择器里 / 预先写在基础规则里,两种写法都不动),确认是渲染器不跑 transition,与写法无关。若照此发布,用户会看到左边一条 10px 黄条僵在原地。改为 JS 按墙钟时间逐帧写width/opacity后,实测同一渲染器下取到 17 个不同宽度、10px → 500px,并在真实速度截图中抓到中途帧(1184 宽画面上黄色已铺到 895px,整屏无缝隙、品牌块被完全覆盖); - 硬性保险丝:一个独立
setTimeout终点(时长长于 进度 + 铺满 + 淡出),即使所有时钟停摆也必定移除遮罩,应用不会被永久遮住;该定时器不随clearLoaderTimers()清除,因为收尾阶段本身要调用它来停掉进度时钟; prefers-reduced-motion:直接跳过铺满与淡出,立即移除遮罩;pointer-events: none:即使动画进行中也不拦截点击,下层应用始终可用;- 全部字形由 CSS
content绘制(整块无任何 DOM 文本节点)+translate="no"/notranslate/aria-hidden,防浏览器翻译改写品牌名; - 插件停止/卸载时,rAF、interval、timeout 与 DOM 节点全部随
ctx.effect拆除(含动画播放中途卸载); - CSS 注释必须成对:一次误把
*/提前收束,导致后续几行说明文字直接落进样式表,--edge-word未定义、整个品牌块塌回左上角——node --check查不出这类问题(文件照样能解析),故校验脚本专门检查「顶层是否漏进散文」;同理,CSS 模板字符串里不能出现反引号(写在注释里也会提前闭合模板串),校验脚本亦已覆盖。
回合状态标签「Deep diving...」
该标签由 @deepseek-ai/dsh-client-ui-conversation(类名 Md3f7G_turnStatus)绘制,是渐变文字而非普通着色文字:上游画了一层 linear-gradient 背景,再用 -webkit-text-fill-color: transparent + background-clip: text 把字「镂空」,并以 background-position 做流光动画。由此有两个结论:
- 写
color:完全无效——透明文字填充优先,字仍由渐变决定;改色必须改渐变本身。 - 不能去动
--dsw-static-deepseek-500/200这两个共享令牌。它们同时支撑--dsw-alias-button-info-fill、--dsw-alias-state-business-primary与--dsw-specific-bubble-highlight(已在dsh-client-ui-theme/styles/design-platform.css中核对),本主题刻意把它们映射成墨/纸色。因此只覆盖background-image,上游的background-size、background-position与流光动画保持不变。
配色是量出来的,不是挑出来的。 每个渐变色标都必须对该模式下两种可能底色(bg-base 与 bg-layer-1)都达到 WCAG AA 4.5:1——因为中间亮带会扫过字形,而在 prefers-reduced-motion 下上游把 background-size 钉成 100%,那条亮带会永久留在字里。实测:
| 颜色 | 亮色底 #e8e8e2 / #f2f2ec |
暗色底 #101110 / #181a18 |
|---|---|---|
#fff500(直觉上的「改成黄色」) |
1.02:1——完全不可见 | 15.26:1 ✓ |
#8f7c00 |
3.38 ✗ | 4.21 ✗ |
#7d6c00 |
4.25 ✗ | — |
#6b5d00 |
5.35 ✓(亮色下第一个达标的金) | — |
#a08a00 |
— | 5.11 ✓ |
#3f3600 |
9.82 ✓ | — |
因此流光在两种模式下都向暗处下沉,而非向亮处提亮:亮色底上没有比 #6b5d00 更亮的金能达标,而暗色底上「提亮」的浅色带会褪成近白、彻底失去黄意。亮色模式用深金而非信号黄,与水印/进度轨同理——在米色纸底上,#fff500 不是一种配色选择,而是一次抹除。
一个方法论提醒:视觉模型的文字描述不能作为颜色结论的依据。 期间它曾断言某候选在亮色面板下「金色更亮」,而两个候选的亮色值完全相同;靠逐像素取色才纠正。同样,第一版取色脚本误测到灰色的计时数字而非标签本身(计时器有自己的不透明颜色,在亮底上比浅黄更「显眼」),改为量「无计时器」状态才得到正确数据。
背景水印与「水印保持显示」
水印默认只在新建会话页(hero)显示,居中跟随标题。开启「水印保持显示」后,对话页等非新建会话页面也会显示水印:
- 水印挂载在会话列内部,以
z-index: -1位于正文之下(该列在水印挂载期间获得isolation: isolate与position: relative,卸载后自动还原为static/auto); pointer-events: none,不拦截点击与文本选择;- 非 hero 页透明度为
0.16(hero 页0.13)。该值经真实页面实测标定:0.07在深色下合成为#202120(对#101110仅 16/255,约 1.17:1)几乎不可见;0.22虽清晰但大字母边缘会与正文视觉打架;0.16约为深色 1.54:1 / 浅色 1.40:1。
该开关需先开启「背景水印」;未开启时按钮为禁用态。
提问卡片:「推荐」徽标与选项编号
提问卡片(@deepseek-ai/dsh-client-ui-user-questions)里有两处「字和自己的底色同色」,都是实测量像素才发现的:
- 「推荐」徽标在亮/暗两种模式下都完全不可见。 上游把
--dsw-alias-button-info-fill当前景色用(color),底色用--dsw-specific-sidebar-nav-item-active-accent;而本主题为了中和残留蓝色,把这两个令牌映射成了同一个值(亮#101110/ 暗#fff500),于是标签把自己画在自己的底色上。实测「有文字 / 无文字」两版渲染逐像素差为 0——DOM 里有字,画面上一个像素都没有。修复不去动这两个令牌(它们在别处确实被当作背景填充使用),而是只在选项行内把这对前景/背景显式钉死。 - 选项编号在暗色选中行上是黑底黑字。 既有的暗色选中行反色规则只把后代文字改黑,改不到后代自己的背景,所以编号仍保留
--dsw-alias-bg-overlay(#1c1e1c)的底色,实测对比度 1.25:1,编号消失。改为给编号加一层半透明墨色淡底(而非填死),让数字落在黄底上仍读得出,同时保留「小方块」的形态。
实测对照(逐像素,差异像素数 为「有文字」与「无文字」两版渲染之差,0 即完全不可见):
| 用例 | 修复前 | 修复后 |
|---|---|---|
| 「推荐」徽标 · 暗色 · 普通行 | 0 px(不可见) | 1275 px,16.50:1 |
| 「推荐」徽标 · 亮色 · 普通行 | 0 px(不可见) | 1275 px,16.50:1 |
| 「推荐」徽标 · 暗色 · 选中行 | 1274 px,18.31:1 | 1296 px,16.50:1(反色保边) |
| 选项编号 · 暗色 · 选中行 | 207 px,1.25:1 | 225 px,11.69:1 |
| 选项编号 · 暗色 · 普通行 | — | 230 px,4.98:1(未回归) |
| 选项编号 · 亮色 · 选中行 | — | 230 px,7.73:1(未回归) |
两个验证方法上的坑,记下来免得重犯:
- 不能靠「猜矩形 + 取色」判断可见性。 必须渲染「有文字 / 无文字」两版再做差:类名、层叠、几何完全相同,唯一变量是文字内容,差异像素数即字形本身。
- 给徽标做差时必须保留一个零宽空格(
\200B)。inline-block里没有任何在流内容时高度会塌成 0(line-height只作用于由内容产生的行盒),空文字那版会连背景都不画,差出来的是背景而不是字形——第一次就是这样量出「假可见」的。 :hover用同权重的类替换来验证。 截图无法触发:hover,而主题里已有一条[class*='badge' i]:hover{color:#000};把:hover换成.HOVERPROBE(类与伪类特异度同为 0,1,0,层叠结果不变)后实测:暗色选中行悬停时徽标为#fff500on#101110(16.50:1),没有被旧规则改成黑底黑字。
防浏览器误翻译
水印是品牌名,不应被 Chrome/Edge「翻译此页」、Google 翻译挂件或翻译插件改写。采取结构性 + 声明式双重防护:
- 结构性(主要手段):字形由 CSS
content(::before)绘制,元素本身没有任何 DOM 文本节点(实测textContent.length === 0、childNodes === 0),逐文本节点遍历的翻译器根本看不到它; - 声明式:
translate="no"(HTML5 标准 opt-out,Chrome/Edge 遵循)、class="notranslate"(Google 翻译自有钩子)、lang="en"(避免被判定为中文正文); aria-hidden="true":纯装饰元素不进入无障碍树。
实测:模拟翻译器改写会话列内 781 个文本节点后,水印内被改写的节点数为 0,字形仍为
ENDFIELD。
安装
作为 DSH 插件(推荐)
dsh plugin --profile web add github:ymh0000123/dsh-theme-endfield
dsh plugin 会转发给 profile 目录的 pnpm 完成安装,并根据包内的 cordis.patch.yml(dsh.bundle.patch)自动把插件行挂进 bundle 栈,同时把 exports["./client"](client.js)登记为浏览器端 client bundle。安装后重启(或重新加载)web profile 即生效。
本主题是纯 Client 半部插件:Host 半部(
index.js)为空实现,全部效果由浏览器端的 token 覆盖 + 样式注入完成。
作为动态 Cordis 插件(临时试用)
- 打开
client.js,复制apply(ctx) { ... }函数体; - 在 DSH 会话中用
cordis_define新建插件,将函数体粘贴为 Client 代码(return { apply(ctx) { ... } }), 在cordis_run中激活该 Package; - 刷新页面即生效;在 Run 卡片上停止插件即可完全卸载(token 层与样式层自动拆除)。
提示:
client.js依赖 DSH Client 运行时提供的theme服务、styles内建与ctx.effect,仅在 DSH Web 环境中可用,不能直接在普通浏览器中运行。
卸载
dsh plugin --profile web rm dsh-theme-endfield
使用
以动态 Cordis 插件的 Client 代码加载 client.js 的内容(apply 返回 Cordis Plugin),激活后刷新页面即生效;停止插件会自动拆除全部样式副作用。
特性清单
- 终末地官网色板(亮/暗)
- 信号黄
::selection、光标、焦点环 - 全局直角化
- 中和残留蓝色(状态点、气泡、信息按钮、侧栏激活项、偏蓝文字)
- 新建会话页背景光晕改信号黄(应用原本是 SVG 属性写死的
#6187D8,不走 token;亮 8% / 暗 5% 按亮度实测对齐原光晕强度) - hover 文字反色
- 表格行悬停实心黄底黑字
- 新建会话按钮黄底黑字
- Cordis 操作按钮(run/stop/approve)黄化
- 会话头部动作(agent preset 徽标 / 子代理 / 任务)黄化
- 背景 ENDFIELD 水印(hero 页跟随标题居中)
- 水印保持显示开关(非新建会话页面亦显示,置于正文之下)
- Cordis 审批按钮图标与自身底色对比(黄底黑勾 / 红底白叉)
- 提问卡片(
ask_user_question)「推荐」徽标与选项编号可见(见下) - 启动加载动画(左侧黄色进度轨 + 百分比 + ENDFIELD 字标,默认关闭,可预览)
- 加载屏海报式排版(END/FIELD 双行堆叠 + 品牌块居右 + 统一左侧节奏线)
- 读数跟随进度轨填充端下移(含下边界钳制,接近 100% 不出屏)
- 收尾动画:黄色向右铺满整屏 → 整体淡出移除(JS 驱动,不依赖 CSS transition)
- 回合状态标签「Deep diving...」改信号黄/深金(渐变文字,每个色标均实测达 AA,见上)
校验脚本
样式表是一整个 JavaScript 模板字符串,因此有几类改动会在「文件仍能解析」的情况下悄悄破坏效果。这些不变量已固化为脚本,而非依赖人工小心:
node check.js # 检查 client.js 的样式表不变量
node selftest.js # 反过来验证 check.js 真的能抓到那些 bug
check.js 检查项:
- 模板字符串内无反引号(写在 CSS 注释里也会提前闭合,导致整个 client bundle 解析失败);
- 无
${...}(在模板字符串里那是插值,不是 CSS); - CSS 注释成对、花括号配平(花括号在剥离注释后再统计);
- 顶层没有漏进散文——这是抓「注释提前闭合」的那一项:注释提前闭合后注释本身仍是配平的,真正的破坏是残留文字落到顶层、与下一条选择器黏在一起,从而整条规则被丢弃;
--edge-word/--edge-gap在实际生效的 CSS 里有定义(注释里提到不算);client.js能编译(用vm.Script在进程内解析、不执行;避免node --check子进程在沙箱下的 stdio 限制);- 回合状态标签仍通过
background-image改色(写成color:对渐变文字无效,属于「改了但没生效」的静默失败)。
selftest.js 会把上述每个真实 bug 注入 client.js 的副本并断言 check.js 确实失败——包括断言注入本身生效,避免「测试其实什么都没改」的空跑。一个从未被观察到失败过的校验,不能算证据。
顺带一个实测教训:「顶层漏进散文」的第一版实现用「含逗号或英文单词」做判据,在合法选择器上产生了 33 个误报(
:is([role='tab'], …)、input, textarea、tbody tr:hover)。可靠信号窄得多:CSS 选择器里不会出现跟在字母后的句点+空格,而散文会。
许可证
MIT
还没有评论,来写第一条。