DSH Plugins Marketplace

DSH Plugins

Plugins

/

dsh-endfield-theme

L

dsh-endfield-theme

Manifest valid

Arknights: Endfield theme for the DeepSeek Harness Web GUI — charcoal + hazard-yellow industrial HUD, first-paint boot splash, conversation backdrop.

hasBundlePatch

dsh-endfield-theme · 《明日方舟:终末地》主题

npm license

为 DeepSeek Harness Web GUI 做的一套终末地主题:炭黑工业 HUD + 警戒黄强调色, 带开屏动画与对话区背景。它是一个普通的 DSH 客户端插件(host 半区发一条路由 + 四条 index 注入行,浏览器半区是纯 CSS/JS),不依赖皮肤中心,也不需要构建步骤。

桌面端与浏览器端渲染的是同一份前端,主题两边都生效。差别只在桌面端多一层窗口装饰: Windows Controls Overlay 那 40 px、以及由 preload 打上的 data-windows-titlebar 标记。

验证覆盖并不对称,这点直说,免得读者按同一份信任去读两件事:

形态验证到什么程度
浏览器(http://127.0.0.1:PORT/)preview/ 里每一张图都是无头 Edge 打在这个真实地址上拍的。注入行与开屏首帧、背景层、HUD 取景框、品牌位、标题栏条带,全部在这条路径上量过
桌面端(Electron 窗口)注入行、WCO 那 40 px、caption 按钮的取色,来自读 DSH 源码 + 在浏览器里复刻 preload 的标记,并在真实窗口里核对过。有一处已知差别:桌面端在开屏出现之前,会有几帧应用自己的 boot 卡可见

"首帧即开屏"只在浏览器形态成立。 浏览器那条路径把注入行直接渲染进返回的 HTML, 所以它们必然早于任何脚本;桌面端的 index.html 是包装 dist 里一个 825 字节的静态文件, 由 dsh-app:// 直出、不经过 web 服务器,注入行要等 dshDesktopBoot.ready() 的一次 IPC 往返之后才落地——那时 boot 卡已经画完了。

这个缺口插件补不上,原因和取舍写在 NOTES.md: 前端的行解释器里 script-preload 是空实现、preload-app.cjs 也不认识它,所以不存在 "首帧之前"的通道;窗口的 backgroundColor 与 show:false 时机属于主进程。 注入行在桌面端的价值是"在插件能触及的最早时刻把 boot 卡换成开屏",不是消除闪烁。

English: an Arknights: Endfield themed client plugin for the DeepSeek Harness Web GUI — charcoal industrial HUD, a single hazard-yellow accent, a boot splash and a conversation backdrop. The host half serves one asset route and four index-injection rows; the browser half is plain CSS/JS. No build step, no skin center required.

The desktop app and the browser render the same front end, so the theme applies to both; the desktop only adds window chrome (the 40px Windows Controls Overlay and the data-windows-titlebar marker the preload sets). Verification is lopsided and worth stating: every image under preview/ was captured in headless Edge against the served web address, while the desktop-only behaviour was reasoned from DSH's source and by reproducing the preload's markers in the browser, not by attaching to the Electron renderer.

开屏动画

深色(推荐)浅色(雪原 daylight)
darklight

它做了什么

  • 开屏动画 —— 每次文档加载跑一次,约 2.9 s,用的是终末地字标素材(不是排出来的字, 出处见 NOTICE.md): 黑色底 + 46 px 测量网格 + 上下警戒带拉开 → 原始徽记整张图(冠部 + 终末地 + ENDFIELD INDUSTRIES + 倒三角,未做任何处理)被一道带亮边的横向擦除从左往右写出来, 写出的进度与下面的读条同步 → 细线、BOOT 读数条、左下角三行终端自检、右下角英文 小字标 → 整屏像 CRT 一样纵向收拢消失。 同时播放一条开屏音效(endfield-boot.mp3,约 2.9 s 后淡出)。随时点击/按键即跳过; prefers-reduced-motion 下完全不播。

    开屏时间轴

    (0.4 / 0.9 / 1.5 / 2.1 s 四帧。底纹用的是原始徽记本身——中间那句 终末地 就是图里的字, 不是另外画上去的一层,所以擦除揭示的就是它,也就不存在"底纹与字标重影"的问题。 进度条上的百分比由 theme.js 从同一个时钟动画算出,因此数字与填充永远一致;擦除与 读条共用同一条时间窗(1.45 s → 2.6 s),读条走到哪里,字就写到哪里。)

    动画只用合成层属性:transform / opacity(以及擦除用的 clip-path)。上一版那条 从上而下的扫描光带动画的是 top,每帧触发重排,是当时不流畅的主因,已移除;亮边那道线 原本也动画 left,同样改成 translateX。

    音效只在桌面客户端响:Electron 的 autoplayPolicy 默认是 no-user-gesture-required,而浏览器要求用户先有交互——被拒时脚本静默继续, 开屏照常播完,不报错也不中断。

    它现在是首帧而不是脚本搭出来的:原版客户端没有独立开屏环节,启动时看到的其实是官方 boot 底色(浅色 #fff / 深色 #151517)加外壳第一帧,所以开屏改由 index 注入行下发。 细节见下面「开屏为什么是注入行」与「右上角那三个按键」两节。

  • 对话区背景 —— 背景艺术是一张烤过的主视觉:深色模式用炭黑版(亮度 ×0.30、去饱和、 加暗角),浅色模式用雪原版。它被放在 #ef-backdrop(z-index:-1 的固定层)里, 由 CSS 把外壳自己的白底/面板底改成透明 —— 于是画面成为"地面",而不是白房间里挂的一张画。 对话里有消息时(html[data-ef-conversation="on"],由 MutationObserver 判定)画面自动退一档, 正文另加一层极淡的文字光晕,保证可读性。

  • HUD 取景框 —— 四角括线 + 右缘刻度锚在对话列上,不是锚在窗口上:窗口级括线会正好 压在侧边栏品牌行上,看起来像第二个画歪了的"收起侧栏"按钮叠在 logo 上。现在由 theme.js 量 [class*="centerCol"] 的矩形写进 .ef-frame,ResizeObserver + 窗口 resize + MutationObserver 三路跟随,侧栏收起/展开都会重新贴合。

  • 整体色调 —— --dsw-* 设计 token 重映射成终末地那套语言:炭黑面板、1 px 发丝结构线、 近直角(半径 2–10 px)、等宽读数、警戒黄只给状态(新会话按钮、当前行左轨、链接、 选中文本、焦点环、进度)。深色是设计本体,浅色是「雪原」变体。

  • 屏幕细纹 —— 一层 repeating-linear-gradient(1 px 亮、2 px 暗,约 5% 平均压暗), 纯静态、零动画,所以不占每帧预算。它是唯一一层画在应用之上的图层(#ef-crt, z-index 略低于开屏的 2147483000)——扫线如果被面板盖住,那是墙纸不是屏幕。开屏自己也 有一层(稍重一点,因为开屏是亮场)。pointer-events: none 是硬要求:它铺满整个视口, 否则整个应用都点不动。浅色模式减半。想关掉加 ?dsh-endfield-crt=0。

  • 轮次索引 —— 对话列左侧每个消息项一个小的警戒黄轮次号。纯 CSS:聊天流每一项本来 就带 data-chat-turn,所以序号由 attr() 取得,不需要脚本遍历,流式输出时也没有东西需要 同步。序号在条目内部(靠 padding-left: 20px 留位),不是负偏移——实测消息左缘就在 栏目边界(280 px 处,侧栏 277 px),负偏移会把序号推进侧栏。只作用于非 turn-process 项,因为那类项嵌在别的项里,双重缩进会叠加。

  • 电力储备(余额可视化) —— 侧栏底部把账户余额画成一块电量计:刻度格 + ¥ 数字 + 状态词(NOMINAL / OUTPUT LOW / DEPLETED),满量程 = 预警阈值 ×4,所以预警点落在 1/4 处。 做成电量计是因为你提供的两句警告音是电站播报 —— 余额恰好是这套语言里最贴切的东西: 一份会耗尽、会告警、会断电的储备。

    数值来自宿主半区的 GET /dsh-endfield/balance,而宿主只是问 DSH 自己的账号服务 (deepseekAccount 服务,方法 getBalance(client)):插件从不接触凭证、也不直连 Platform。 宿主对返回值不做结构假设,直接透传给浏览器;浏览器只读它认得的字段,认不出就显示 NO SIGNAL 而不是猜一个数字。缓存 30 秒,免得开着的面板每分钟打一次账号接口。

    账号服务是可选依赖,所以它和 webServer 放在两个独立的 inject 里:放进同一个的话, 在没有账号 provider 的 profile 上 inject 永不触发,整个主题都不会注册。

    余额低于阈值播 endfield-low-power.mp3,小于等于 0 播 endfield-no-power.mp3 —— 边沿触发, 不是电平触发:每分钟轮询一次,电平触发会每分钟播一遍。余额回升后重置,下次再跌会再报。

  • 设置页 —— 设置对话框里多一个「终末地主题」分页,13 项设置,存在 localStorage, 改完即时生效(开屏 / 品牌位 / 开屏音效在重启客户端后生效)。

    这个插件是 host-only(cordis.patch.yml 里没有 dsh.client facet),注册不了官方设置页, 所以它扩展设置对话框自己的 DOM。两个锚点都取自实测而不是猜类名:导航项取 nav button 的最后一个(结构定位,因为标签栏没有 data-slot),内容区取 [data-slot="settings.section"] 的父节点 —— data-slot 是外壳有意设置的稳定属性, 周围那圈 wCInkW_* 是构建哈希,不能依赖。分页切换由主题自己处理:外壳不知道一个它没渲染过的 标签存在,所以点我们的标签就藏起它的内容,点它的标签就藏起我们的。

    面板插在那个父节点「里面」、紧挨着被替换的 section 之后 —— 因为实测那正是滚动容器 (overflow-y: auto,从面板到对话框之间唯一可滚的一层)。第一版把它插在容器外面, 于是多出来的设置项挂在折线以下、没有任何东西能滚到它们。同理,切换时藏的是 那一节 section 本身,不是容器 —— 藏容器会把我们自己的页面一起藏掉。

    标签的盒子样式直接复用外壳自己的 nav-cell 类名(运行时复制),只额外加一条警戒黄左轨 —— 这样尺寸、内边距、悬停行为都跟着外壳走,不会因为外壳改了样式而错位。

    开关是真的 checkbox(appearance: none 重绘),滑杆是真的 input[type=range] —— 键盘操作、无障碍语义都保留。

    排版照客户端自己的设置行来,而且是量出来的不是挑出来的:标签 14px/22px、说明 12px/18px、 行高 44px、字体走 var(--ef-sans)(它解析成和外壳同一套 "Endfield Sans SC" + 平台回退)。 早先那版把整页设成主题的等宽字体、9–11px —— 读起来像塞进设置里的另一个应用,而不是设置里的 又一页。

    目前 13 项设置 + 一行试听按钮。强关联的项收成一组,起决定作用的那个当主项:

    开屏动画 ─┬─ 开屏音效
    替换侧栏品牌位
    屏幕细纹 ─── 细纹强度
    轮次索引
    对话区背景
    侧栏电力储备 ─┬─ 余额刷新间隔
                 ├─ 余额语音播报 ── 播报音量
                 └─ 预警阈值 ────── 电量计满量程
    

    这些都是真的依赖,不是为了好看才分层:开屏关掉就没有音效可听;电力储备关掉就不轮询, 于是播报也一起停;阈值为 0 时「满量程 = 阈值 × N」没有意义。判据因此只有一条 —— 主项的值是否为真,它同时覆盖了两种主项:关掉的开关,和等于 0 的数字。

    主项关掉时,从属项变灰并且真的 disabled(不是 pointer-events 障眼法 —— 键盘也不该够到 一个没有作用的控件)。变灰而不是隐藏,这样来回切主项时页面不会在手底下跳动。

    依赖关系同时决定了缩进和竖向导轨(data-depth + 每行一条绝对定位的 1px 线,相邻子项连成 一整条),所以分组读起来是结构而不是一段说明文字。

    有些项光存值不够,所以滑杆与数字控件带一个「存完之后跑」的回调:音量会当场试听新音量、 满量程会重画电量计、刷新间隔会先清掉旧定时器再起新的(两个定时器会变成双倍请求频率, 而且都不是用户要的那个)。

    只有开关行用 <label>。滑杆和数字行是 <div> —— <label> 会把点击转发给内部控件, 于是拖动变成跳变、控件旁边的空白什么都不做,滑杆摸起来就是坏的。开关是唯一「点哪都能切」 才符合预期的控件。

    需要重启才生效的项(开屏动画 / 开屏音效 / 替换品牌位)在说明里直接写「修改后重启客户端生效」—— 写「刷新」是错的:桌面客户端里没有刷新按钮,用户能照做的动作只有重启。

    「试听警告音」一行两个按钮,按一下直接播对应那条 —— 这正是那个调试 URL 想解决的问题, 只是换成了不需要记 URL 的形式,而且走的就是真实播报那条代码路径。

    配置 一行三个按钮:重置为默认 / 导出 / 导入。导出把设置写进剪贴板(失败时退回一个 显示 JSON 的 prompt —— 剪贴板是这里唯一可能失败的一环),导入粘贴 JSON。导入只接受这个 版本认识的键,且类型必须一致 —— 粘进来的对象不能引入主题永远不会校验的状态,一个落在布尔 位上的数字也不该被当成「开」。

    清除数据 是自卸载:确认之后,宿主半区把本插件从 profile 的配置里移除(只删它自己的 两条 —— package.json 的依赖与 bundles 条目、cordis.patch.yml 的那条 insert),浏览器半区 清空它写过的全部本地数据(那两个 localStorage 键就是全部,没有 cookie、没有文件),并把自己 的 DOM 从页面上撤掉 —— 所以界面立刻就像没装过一样,重启客户端后彻底消失。

    写配置前两个文件都先备份(*.ef-uninstall-backup)—— 这是插件里唯一会写用户配置的 代码路径,备份就是「修一下」和「重装」的区别。删除逻辑是纯函数(stripSelfFromProfile), 由夹具测试验证:合成一个还含有别的插件条目的 profile,断言只删自己的、其余逐字节不变、 注释保留、第二次运行什么都不做、无关 profile 完全不动。

    它不动 node_modules —— 那不是「插件是否会加载」的原因,而且对着一个客户端正开着的 profile 跑包管理器是另一回事。留在 node_modules 里的那份在没人引用之后是惰性的,下次安装会清掉。

    测出来的一个细节:撤掉 DOM 之后必须让重建停下来。这个脚本处处幂等自愈(rAF 泵、 MutationObserver、防抖的对话 tick),这恰恰意味着它会把自己建回来。所以撤除时置一个总闸, 所有重建入口先查它 —— 反正脚本无法卸载自己,诚实的做法是停止重建、交给重启。

    早先一版只清数据、然后提示用户自己去跑卸载命令,那不算「一键」;现在是真的一键。

    对话框里外壳自己的控件也一起换了样式(开关、下拉、数字框),否则设置里只有主题那一页 长得像主题。定位用 role="switch" 和稳定的类名后缀(_selector),不用完整类名 —— _switch_1ik0f_5 这种是 CSS Module 哈希,换个构建就变。全部作用域限定在 [role="dialog"] 内。外壳的开关是 <button role="switch"> 里套一个圆形 <span> 滑块:滑块隐藏,状态改从 aria-checked 画在外层按钮上 —— 那既是语义状态也是稳定钩子,于是得到和主题自己那页 一样的方形格子。

    分页为什么「隔一下才出现」(已修):面板原先挂在对话的 MutationObserver 上,而那个观察者 为了扛住流式输出带 200 ms 防抖,于是对话框打开后要等防抖窗口过去才装上。现在打开设置是 一次点击,那就用这次点击当信号:click 捕获阶段唤醒一个 rAF 泵,最多跑 1.5 秒、装上即停。 观察者那条路保留作兜底(键盘等方式打开的对话框)。

  • 今日变化 —— 电力储备面板底部多一行 今日 −¥x.xx。账号服务只报一个余额,所以「今天花了 多少」只能靠自己记基线再相减。两种情形会重置基线:跨过自然日,以及余额上升了 —— 充值不是负的消费,带着昨天的基线过充值会报出一个荒唐的数字。基线存在 localStorage, 刷新不会把测量从错误的数字重新开始。显示用带符号的差值(充值显示 +¥),平时是安静的次级灰, 只有充值才用强调色 —— 花钱是账户的常态,把它涂成告警色,告警色就不再意味着什么了。

  • 余额调试开关 —— ?dsh-endfield-fakeBalance=0 让读数相信账户是空的,于是 「电力储备耗尽」那条语音、以及电量计的红色告警态,都可以在一个离空还很远的账户上当场看到。 每次轮询都会重新读,改 URL 刷新即可。解析不出来时返回 null —— 一个畸形的值绝不能 被当成真的零。

  • 外壳调整 —— 品牌行居中并与窗口按钮同高、对话区顶栏不再把会话名重复念一遍、 侧边栏折成 55 px 轨道时展开按钮点亮成警戒黄(一个看不出能点的按钮等于没有)。 动机与实测见 NOTES.md。

深入

有四处做法从代码上看不出动机,读源码时容易被当成没来由的 hack。它们连同证据、 实测数据都写在 NOTES.md:

做法一句话
开屏走 index 注入行,而不是 theme.js 事后搭原版没有独立开屏环节,要成为首帧就必须在外壳渲染之前进 DOM
盖掉 DSH 的 WCO 探针、重切顶部 40 px 条带那 40 px 上压了三层,主题要做的不是给③上色,而是让它停笔、由①按下方界面对齐
侧边栏品牌行居中、行高收到 40 px、去掉 backdrop-filter每一条都对应一个具体问题(品牌行压在收起按钮上、轨道看不出能点、看着像侧边栏消失了)
输入卡的切角画在 ::before 上,且必须画在内容之下外壳把「+」的添加菜单渲染在输入卡内部:对卡片加 clip-path 会让菜单布局正常但完全画不出来;把背景层放到内容之上又会盖住图标

那个「+」按钮为什么会失灵(1.3.2 修复)

这是本主题修过的最隐蔽的一处,而且踩了方向相反的两个坑:第一个让菜单画不出来, 第二个让图标被盖住。两个坑都记在下面。

症状:点输入框左下角的「+」,光标会变成手指、elementFromPoint 在按钮中心返回按钮自己、 element.click() 也能触发 —— 但用户什么也看不到,菜单根本没出现。

原因:外壳把「+」的添加菜单渲染在输入卡内部(它的容器 ._surface_ri079_1 是卡片的后代), 而菜单布局在卡片上方约 340 px 处。主题给卡片加的 clip-path 用来切角 —— clip-path 只把盒子从绘制中移除,不动它的布局盒,于是菜单被完整裁掉:

menuRect                     [465, 101, 739, 335]      ← 布局在这里
[data-composer-card] rect    [460, 443, 749, 116]      ← 卡片只有这么高
elementsFromPoint(菜单中心)   [scrollBody, body, root]  ← 画出来的不是菜单 ✗

为什么难查:

  • getBoundingClientRect() 照常返回布局盒,所以"菜单存在、位置也对"看起来毫无问题;
  • elementFromPoint 在按钮上是干净的,因为遮挡发生在绘制阶段而不是命中测试阶段;
  • 用 element.click() 测试会绕过命中测试,所以"程序化点击能弹出菜单"是假阳性;
  • 只有在菜单自己的中心调 elementsFromPoint 才能看出真相。

第二半:图标被盖住(1.3.2)

第一半的修法是把缺口、背景、描边、阴影挪到 [data-composer-card]::before —— 伪元素是叶子节点, 裁不到卡片的任何子元素。但伪元素当时放在 z-index: 0,于是踩了反方向的第二个坑:

定位元素(z-index: 0)绘制在普通流内联内容之上(CSS 绘制顺序:背景 → 负 z-index → 块级 → 内联内容 → z-index 0 的定位元素)。而「+」的图标正是普通流内容。

结果:菜单出来了,图标却被不透明的面板整个盖住 —— 按钮能用,但看上去是空的。

正确做法是让这一层画在内容之下:伪元素用 z-index: -1,卡片加 isolation: isolate 把这个 -1 关在卡片内部(否则它会掉到某个祖先的背景后面去)。

1.3.1 里我刻意避开 isolation,担心它会把外壳 .surface 的 z-index: 100 关进卡片的层叠 上下文 —— 这个担心是多余的,而且现在有实测:.surface 本来就是卡片的后代,把它的 z-index 限定在卡片里并不改变它能盖住什么。

验证:一次量两件事

修复前  paintedAtPlusCentre: [面板…]                                ✗ 图标被盖
        paintedAtMenuCentre: [scrollBody, body, root, …]            ✗ 菜单被裁

修复后  paintedAtPlusCentre: [path, path, svg, BUTTON.RlGAzG_add]   ✓ 图标画出来了
        paintedAtMenuCentre: [DIV.Z9Jnlq_viewport, …]               ✓ 菜单画出来了
        卡片已不在 clippingAncestors 里                               ✓

给别的主题作者的两句话:

  1. 外壳会把浮层渲染进你的容器里,所以不要在容器上用 clip-path / overflow: hidden / mask 来做造型 —— 挪到伪元素或内层元素上去。
  2. 给容器加背景层时,一定要让它画在内容之下(z-index: -1 + 让容器成为层叠上下文), 否则会盖住容器自己的图标和文字 ——「按钮能用但看着是空的」就是这一条的典型症状。

品牌位

深色(炭黑侧栏)浅色(雪原侧栏)
深色品牌位浅色品牌位

这是本主题里唯一会改动产品标识的一处:侧边栏左上原本是 DeepSeek Harness 的产品标识 (鲸标 + deepseek + HARNESS 徽章),启用后换成「终末地工业」。窗口标题、对话框与插件页 里的产品名不受影响,仍然显示 DSH。

品牌呈现是 DSH 设计上可替换的一环——官方在自己的 @deepseek-ai/dsh-client-ui-brand-official 里写明「自有身份的部署……组合另一个占据侧栏 slot 的包,占据 slot 是唯一的组合路径」 (原文见 NOTICE.md 第 3 节)。所以这一处走的是外壳提供的扩展点, 不是绕开机制去改它。

两件需要讲明的事:

  • 「终末地工业 / ENDFIELD INDUSTRIES」是鹰角的商标,本仓库不主张它。品牌素材的出处、 版权归属与分发条件见 NOTICE.md;fork 或再打包时,那份同人非商业的义务 随文件一起走。
  • 若权利人认为本仓库这种分发方式不妥,联系作者即可移除。

不想要这一处、保留其余部分:在 DSH Web 地址后加 ?dsh-endfield-brand=0。

做法:

  • 图标就是终末地 lockup 素材本身(冠部 + 终末地 / ENDFIELD INDUSTRIES + 倒三角), 没有重画、没有描摹,只是缩到 28px。这个尺寸下里面的字标读不出来,也不指望读出来—— 它承担的是"剪影",名字由旁边的「终末地工业」写清楚。

  • 两份素材缺一不可,且极易用反。原图是双色的:不透明像素里 49% 纯黑、46% 纯白, 而白色那半才是主角——冠部的等高线、终末地 字标、ENDFIELD INDUSTRIES 那条细线 全是白颜料。所以原图是给深色底画的:

    侧栏用哪份做法
    深色(炭黑)endfield-lockup-dark-bg.png原图原样
    浅色(雪原)endfield-lockup-light-bg.png黑白对调,镂空保持镂空

    用反了不会报错、也不会崩:图形照样渲染、轮廓照样看得见,只有字标悄悄沉进背景里—— 只能靠量才查得出来。tools/check_brand_lockup.py(在维护者工作区,不随仓库分发) 就是干这个的:取字标那一段,数深色与浅色像素,要求浅底那份是深字、深底那份是浅字。

  • 文案是「终末地工业」+ 一枚 ENDFIELD 徽章,位置与原来 deepseek + HARNESS 一一对应; 徽章浅色下是炭黑底黄字、深色下反过来。

  • 原标识是 React 拥有的节点,所以是隐藏(html[data-ef-brand='on'] 下的两条 display: none)而不是删除,替换节点追加在旁边。万一某次重渲染把它冲掉, start() 里的 rAF 泵与那个 subtree observer 会补回来——data-ef-brand 闸门保证幂等, 不会和 React 打乒乓。

  • 按钮自己的 aria-label(新建会话)没动,整个标识区本来就是 aria-hidden, 所以这个改动不影响这个按钮"是什么、干什么"。

字体

客户端全局换成两套随插件发布的可变字体(都做了子集化,SIL OFL 1.1,许可证在 client/assets/fonts/):

字体来源子集用途
Endfield Sans SCNoto Sans SC(可变,wght 100–900)拉丁 + GB2312 + CJK 标点,7 465 个码位,2.0 MB全局 UI:正文、按钮、会话名、对话框
Endfield TechSaira(可变,wdth + wght)拉丁/数字/标点,223 个码位,105 KiBHUD 读数、大写标签、标题、模型名一类"仪器字"

为什么是这两套:终末地工业的味道来自方正的重黑加窄体技术字——中文用 800/900 的思源黑 配紧字距,英文数字走 Saira 的窄体轴。原来的 --dsw-font-family 会把中文交给系统里碰巧装了的 微软雅黑,那是这套主题最"不像"的一环。

两处细节值得记下来:--ef-din 的栈尾必须接 var(--ef-sans)——Saira 一个汉字都没有, 落到 sans-serif 就会退回微软雅黑,所有显示体里的中文标签会当场破功。以及字体 URL 全部写在 theme.css 里(不写在 JS 里),这样整张表可搬迁、可离线注入测试。

配色来源

不是另配的,是从发行主视觉里量出来的(工作区里的 tools/bake_assets.py,Pillow 采样; 背景图也由同一个脚本烘出来):

角色色值来源
警戒黄#F2F013海报最亮的饱和色,实测 #F9F60C,压暗一档以适配炭黑
面板高亮#FBFA6A同色提亮,用于 hover
炭黑 1000#050606画面四周最深的水
炭黑 900–500#0A0C0C → #2E3331暗部层次的 #242820 / #383E36 连成阶梯
正文#EDF0EA银白,微暖以免和警戒黄打架
次要 / 三级 / 注脚#A6AEA8 / #79827C / #5D6661画面里的灰阶

浅色模式把品牌色换成炭黑(浅底上按钮用墨黑 + 白字),警戒黄降为 #6B6A00 一类的强调色 —— 白底上直接铺 #F2F013 是读不了的。

安装

从 npm 装(推荐) —— 走 registry,不需要 git,国内也能直连(npmmirror 已同步):

dsh plugin --profile desktop add dsh-endfield-theme
dsh plugin --profile desktop remove dsh-endfield-theme     # 卸载

包页面在 https://www.npmjs.com/package/dsh-endfield-theme。从 npm 装会跳过 allowBuilds 授权提示,一条命令装好。

从 GitHub 装 —— 等价,但需要能连上 github.com:

dsh plugin --profile desktop add github:Longxiangjunlin/dsh-endfield-theme

连不上 github.com 时,也可以直接下压缩包 —— 把下面地址里的 <版本> 换成要装的版本 (在包页面上能看到当前版本):

https://registry.npmmirror.com/dsh-endfield-theme/-/dsh-endfield-theme-<版本>.tgz

装到的不是最新版?去插件市场的「已安装」里点更新。 按包名直接重装会受 pnpm 11 的 「新包冷静期」影响,而市场的更新按钮正是为了绕开它而设计的 —— 下面把两条路都写清楚。

pnpm 11 默认带 minimumReleaseAge(DSH 的 profile 里写的是 1440 分钟,也就是 24 小时): 发布不满 24 小时的版本一律不合格。关键在于 pnpm 遇到这种情况不是拒绝安装,而是退回 到唯一通过冷静期的版本,所以一个刚连发过几个版本的包,用户重装时拿到的会是最老的那个合格版本, 并且输出里还会写着 + dsh-endfield-theme 1.0.0 (1.3.0 is available) —— 它知道有新版,只是不能装。

更新请走市场(设置 → 插件 → 已安装 → 更新)。它自己从 npm 查 latest,把这个版本按名字 装下去并带上 --config.minimum-release-age=0,再把这一版记进 profile 的 minimumReleaseAgeExclude,所以后续的普通安装也不会再被拦。这套行为在 dshmarket 的 lib/install.js(RELEASE_AGE_OVERRIDE)和 lib/routes.js 里都能看到。

命令行用户急着要新版也可以自己放宽(代价是放弃这道供应链防护,自己判断):

pnpm install --config.minimum-release-age=0

(注意是 minimum-release-age,带连字符 —— 不带连字符的写法在部分 pnpm 版本上不生效。) 对维护者的教训:同一个包不要在 24 小时内连发多个版本,否则每个新版本都要等满一天 才会被普通安装取到。

本地改代码用 link 装,改 client/ 下的文件刷新页面即生效:

dsh plugin --profile desktop add link:<本仓库绝对路径>

哪一半改动需要重启:client/ 下的文件按请求读盘,改完刷新即可;lib/index.js 是宿主 半区,桌面端的注入表在宿主启动时采集一次——所以改注入行要重启一次客户端, 浏览器形态则是下一次请求就重新采集。

发布新版本:改 package.json 里的 version,然后 npm publish。同一个版本号不能发第二次。

文件

dsh-endfield-theme/
├─ package.json                 dsh.bundle.patch 指向下面的 patch
├─ cordis.patch.yml             一条 insert 行
├─ lib/index.js                 host 半区:prefix 路由 + 四条 index 注入行 + tapIndex
├─ test/host.test.mjs           host 半区测试(npm test)
├─ client/
│  ├─ splash.css                开屏:自给自足的首帧样式表(+ 兜底自动隐藏 + 按键配色覆盖)
│  ├─ splash.html               开屏 DOM(与 splash.css 合成一条 html 注入行)
│  ├─ theme.css                 @font-face + L1 token 重映射 + L2 外壳面重切 + HUD 取景框
│  ├─ theme.js                  浏览器半区:样式表、背景层、取景框、开屏时序、品牌位
│  └─ assets/
│     ├─ endfield-field.jpg            深色背景(1920×1080,烘过的主视觉)
│     ├─ endfield-field-day.jpg        浅色背景
│     ├─ endfield-lockup-dark-bg.png   完整 lockup,深色侧栏用(原图)
│     ├─ endfield-lockup-light-bg.png  同一 lockup,浅色侧栏用(黑白对调)
│     ├─ endfield-mark-zh.png          终末地 + ENDFIELD INDUSTRIES 字标(白)
│     ├─ endfield-mark-en.png          纯英文 ENDFIELD INDUSTRIES 字标(白)
│     ├─ endfield-emblem.webp          原始徽记整张图,除文字外颜色对调,开屏底纹
│     ├─ endfield-boot.mp3             开屏音效(约 5.9 s,开屏结束后淡出)
│     ├─ endfield-low-power.mp3        余额预警音(低于阈值播一次)
│     ├─ endfield-no-power.mp3         余额耗尽音(≤ 0 播一次)
│     ├─ endfield-icon.svg             站点图标
│     └─ fonts/
│        ├─ endfield-sans-sc.woff2     Noto Sans SC 子集(2.0 MB)
│        ├─ endfield-tech.woff2        Saira 子集(105 KiB)
│        └─ OFL-*.txt                  两份 SIL OFL 1.1 许可证
├─ contrib/
│  ├─ awesome-dsh-plugin.yml        提交到插件目录用的条目模板(注释不随提交走)
│  └─ README.md                     投稿步骤与提交前清单,不随提交分发
├─ LICENSE                      MIT,含对 client/assets/ 素材的排除声明
├─ NOTICE.md                    第三方素材出处与授权
├─ NOTES.md                     设计笔记:几处做法的动机、证据与实测
└─ preview/                     dark / light / splash / splash-strip /
                                brand-dark / brand-light / titlebar-band / firstpaint-*

路由只有一条:/dsh-endfield/*(theme.css、theme.js、splash.css、splash.html、assets/*)。 它只接受同源 GET/HEAD,路径逃逸 fail-closed,文件按 mtime 缓存(改完刷新即生效)。

开关与调试

以下开关都通过 URL 查询参数生效,加在 DSH Web 地址后面:

  • ?dsh-endfield-splash=0 —— 不播开屏
  • ?dsh-endfield-brand=0 —— 保留 DSH 原品牌位,主题其余部分照常
  • 控制台重播开屏:__dshEndfieldTheme.replay()
  • 侧边栏收起/展开:点轨道里那枚高亮的侧栏按钮
  • 系统「减少动态效果」开启时:不播开屏,背景动效全部关掉,主题照常生效
  • 推荐配置:设置 → 通用 → 外观 切到深色(浅色是雪原变体,深色才是终末地的语言)

怎么验的

验证分两层。仓库里带一份 host 半区测试,不需要跑起整个 harness:

npm test        # = node test/host.test.mjs

test/host.test.mjs 把一个假的 cordis root 喂给插件,然后断言注入行的种类、顺序与内容, 路由的 content-type、目录重定向、HEAD、405/403/404、路径逃逸 fail-closed,以及 tap 幂等与卸载。 设 DSH_ENDFIELD_PKG 可以把它指向已安装或已解包的副本——npm pack 会遵循 files 字段, 所以工作区里能过的测试,未必能保证发出去的包是完整的。

其余验证是维护者在工作区里跑的一次性链路(不进仓库,因为都依赖本机的 profile 与凭据):

工具作用
bake_assets.py采样主视觉调色板 + 烘背景图(与本文表格同源)
prepare_logos.py把字标素材抠成白/黑两套透明 PNG 并裁掉透明边
subset_fonts.py按 GB2312 + 拉丁子集化两套可变字体并转 woff2
dsh-cookie.mjs用 profile 里的签名密钥签出 web 会话 cookie,好让无头浏览器能打开真实 GUI
cdp.mjs无头 Edge + CDP:打开真实 GUI、注入脚本、按时间轴冻结动画、截图、读计算结果
firstpaint-ab.mjs拿服务端真实 index 生成"插行 / 不插行"两份文档,同一时刻抓帧对比首帧
grab-splash.mjs在真实 GUI 里 replay 开屏并把动画停到指定毫秒,抓一帧;preview/ 的两张开屏图都由它出
check-readme.mjs检查文档里的相对链接能否解析、图片是否已被 git 跟踪、每张表格的列数是否一致

preview/ 的图都是从真实 GUI 里拍的,窗口统一 1920×1010、成品缩到 1600×842: splash.jpg 是插件真身注入后按时间轴冻结抓的一帧,splash-strip.jpg 是同一条时间线上的 四帧拼成 2×2 —— 都靠 __dshEndfieldTheme.replay() 重新挂载开屏、再用 Web Animations 的 currentTime 把动画停在绝对时刻,所以每一帧都可复现而不是碰运气。 dark.jpg / light.jpg 是同一个窗口切深浅两套配色后的空对话页。 firstpaint-with.png / firstpaint-without.png 是首帧 A/B:同一份服务端 HTML, 一份插入注入行、一份不插,在 load 事件后立刻抓帧。

授权

代码 MIT(LICENSE)。但仓库里包含第三方素材,详见 NOTICE.md:

  • 字体:Noto Sans SC 与 Saira,均为 SIL OFL 1.1,子集化后随插件分发,许可证全文在 client/assets/fonts/。
  • 终末地美术素材(标志、锁标、背景)由作者取自 B 站用户 SealedManx41527 发布的 这篇图文,他自己那一份也来自 《明日方舟:终末地》官方素材。版权与商标始终属于 © Hypergryph / GRYPHLINE, 按同人非商业用途分发,不在 MIT 范围内;商用请自行替换 client/assets/endfield-lockup-*.png、endfield-mark-*.png 与 endfield-field*.jpg。
  • 本插件是 DSH 的第三方插件,不是 DeepSeek 官方项目。

Comments

Loading…

Similar plugins

dph-endfield-theme

by thjyy

Unofficial Endfield-inspired theme and animated mascot for DeepSeek Harness Web

Development & InfrastructureManifest valid

★ 4

NOASSERTION

JavaScript

Aug 19, 2026

dsh plugin --profile web add dph-endfield-theme

by Theflowyears

Arknights theme pack for the DeepSeek Harness Web GUI: wallpapers with saturation-driven edge blur, an acrylic sidebar, and a one-click wallpaper picker.

Development & InfrastructureManifest valid

★ 1

NOASSERTION

JavaScript

Sep 28, 2026

dsh plugin --profile web add dsh-arknights-theme

✦

No screenshots

by ymh0000123

终末地官网风格的 DSH Web 主题:奶油纸底、墨黑文字、信号黄强调、全直角工业编辑风。

Themes & SkinsManifest valid

★ 124

MIT

JavaScript

Oct 4, 2026

dsh plugin --profile web add dsh-theme-endfield
dsh-style-hub3

No screenshots

Theme studio for the DeepSeek Harness web GUI: eight switchable preset styles, accent and panel-translucency fine-tuning, host-side wallpapers painted on the first frame, and a one-click fix when the

Themes & SkinsManifest valid

★ 0

dsh plugin --profile web add dsh-style-hub
dsh-theme-eink-retro2

No screenshots

by exoticknight

A paper-and-ink client-side theme for the DeepSeek Harness Web UI: Balanced preserves semantic status colors, and Immersive maps supported surfaces to monochrome.

Themes & SkinsUI & ExperienceTerminal & ClientsManifest valid

★ 1

Apache-2.0

CSS

Sep 24, 2026

dsh plugin --profile web add dsh-theme-eink-retro

✦

No screenshots

by ash-qw

Unofficial personal non-commercial Arknights P.R.T.S. fan UI for DeepSeek Harness

Themes & SkinsTerminal & ClientsManifest valid

★ 3

↓ 1.3k/wk

JavaScript

Oct 1, 2026

dsh plugin --profile web add @ash-qw/dsh-theme-prts