dsh-viewtune
Manifest validDeepSeek Harness Visual Effects + Minor Feature Enhancement Plugin
dsh-viewtune
给 DeepSeek Harness 的阅读页签:一轮对话在进行时过程是看得见的——思考在写、工具在跑、到了第几步;一轮成功结束后过程收起来,只留最终回答。在此之上是一层可调的皮:一张壁纸、一层磨砂玻璃,以及一个把设置放在阅读页签里的面板。
上游原版的「对话 / 轨迹」页签、输入框、模型选择、工具与审批全部保留,本插件只增加一个阅读视图。
面向 DeepSeek Harness 0.2.0-rc.2。只改展示,不碰 Agent 执行、SDK 或模型凭据。Node.js ^22.19.0 || >=24。
平台版本:0.5.x 起对应 0.2.0-rc.2;0.4.x 那一条线对应 0.1.5-rc.2,两者不能互换。客户端半边是预编译产物,它在构建时就把平台包的导出写进了 bundle——0.2.0 把图标名从
IconXxx16/14换成IconXxxRegular/Medium、把useSessionPendingInteraction换成useSessionStatus、把TurnTailChatData.tokensPerSecond/ttftMs移走,所以一份产物只服务一个平台版本:装在 0.1.5 上会用到一个已不存在的导出,装在 0.2.0 上会读一个已删除的字段。要留在 0.1.5 就继续用 0.4.x 那一条线:tagv0.4.1与仓库里的dsh-viewtune-0.4.1.tgz(实测两者逐字节一致,main也停在同一个提交上)。
安装
需要 PATH 上有官方 dsh(没有就用 npx @deepseek-ai/dsh)与 pnpm —— dsh plugin add 会在 $DSH_HOME/profiles/web 里跑 pnpm。
# 从 GitHub 安装
dsh plugin --profile web add github:Farewish/dsh-viewtune
# 或从本地目录 / tarball 安装
dsh plugin --profile web add ./dsh-viewtune
dsh plugin --profile web add ./dsh-viewtune-0.5.4.tgz
同一台机器上装了多个 Harness 版本时,有两条会咬人的细节(本仓库在 0.1.5 与 0.2.0 并存的环境里装过一次):
DSH_HOME必须指向要装进去的那个实例。 如果 shell 里已经导出了别的实例的DSH_HOME(例如你正跑在 0.1.5 会话里),直接调用 0.2.0 的dsh会把插件装进正在运行的那个实例。装之前显式覆盖:$env:DSH_HOME='D:\DSH\homes\0.2.0-rc.2'(Windows PowerShell)。pnpm可能不在 PATH 上,而桌面启动器自带一份,把它加进PATH再跑即可(例如D:\DSH\in.dsh-plug.dsh-launcher\tools)。缺少时 CLI 只会说'pnpm' is not recognized,详细诊断写在<profile>\.plugin-manager\logs\operation-*\pnpm.log。
装完重启 Host 再刷新页面:dsh plugin add 只写 profile,不会热挂正在运行的进程。
卸载:
dsh plugin --profile web remove dsh-viewtune
三点容易踩的:
dsh.bundle是开机捕获的,不要再往 profile 的cordis.patch.yml手写同一条 insert,会重复挂载。- 本仓库已提交编译好的
lib/,安装时不需要prepare,也不需要给 profile 加allowBuilds。 - 与上游
dsh-better-display不要同时安装:两者的 bundle 补丁插入同一个入口 id,同时挂着会重复挂载。用上面的remove卸掉本插件后,再装回上游才是干净的。
一轮长什么样
一个「对话」在这里是一轮(提问 + 回复),阅读视图按这个单位组织:
-
过程可见:思考、工具调用、子代理与进度实时呈现,长轮次不会只剩一个转圈;一轮成功结束时过程自动折起、只留回答,运行中或未完成的轮次保持展开。
-
用户消息都在最前:一轮里可能以系统提示词开头并携带多条 user / steering 消息,它们按源顺序全部渲染在过程折叠开关之前。带与不带系统提示词的对话,顺序一致:
用户的话 → 折叠开关(用时 X 秒,可点击)→ 流程(系统提示词 / 思考卡 / 工具 / 回答正文) -
短思考与长思考同框:不再因为「没有溢出」就取消边框,短思考的标题与正文内边距与长思考一致。
-
等待时钟:模型正在思考或输出时,状态行右边显示「这一轮交给模型多久了」。前 3 秒完全不显示(等待通常比这更短,数字一冒出来只会把眼睛拉过去),过十秒挂一个「暂未响应」。锚点是最后一次交棒(工具返回、上下文注入、命令结束、你刚发言),不是轮次开始;工具还在跑时不计时——那时忙的是工具,不是模型。
-
步骤胶囊:回答末尾的动作行里显示该轮回答落在第几步 / 全轮共几步,点开是中文的过程记录,不是原始 JSON。
-
改动行计数:改了文件的工具行尾部显示
+N -M(增绿删红),点开是按文件分页的差异面板。计数优先读宿主已附在结果里的meta.diffs,没有才退回调用参数——而且只对真正会改文件的三种工具这么做(好几个不相干的工具都有叫content的字段);子调用改的文件也算在这一行里。 -
交互式 mcp-app 卡片:回答里的 ````mcp-app
代码块挂成交互卡片,跑在allow-same-origin;卡片可通过 JSON-RPC 把下一轮 prompt 填进输入框。技能包见 [skills/generative-mcpapps/`](https://github.com/Farewish/dsh-viewtune/blob/HEAD/skills/generative-mcpapps/)。 -
思考区的滚轮交给浏览器:卡片内原生滚动,滚到底后整格原生上链到会话。跨过底边那一格、卡片吃不下的余量会被丢掉(最多一格,一个手势只发生一次)——这是「一格都不从浏览器手里拿走」的代价;没有溢出的短思考完全不拦截滚轮。
工具栏与「收起」
- 一条吸顶泳道:「收起」+「viewtune ⚙」在阅读视图顶部,横跨整个阅读视图——分隔线与宿主的顶部栏一样从一头到另一头,而两个控件仍停在文字左边缘;它吸顶在顶部栏正下方,属于外层的横条而不是浮在内容上的东西,所以滚到任何深度都够得到。
- 一个控件,三个模式:收起与全部收起合并成一个按钮(阅读列左边缘只放得下一个控件)。两条条件都成立时,主按钮收起你正在看的这一轮、旁边的双箭头一次收完全部(
title与可访问名逐字就是「全部收起」);只有一条成立时按钮就是那个动作,没有箭头。设置里可以把它钉成单动作。 - 作用域只有当前这一轮:判定方式与阅读滚动选锚点用的是同一条谓词,视口横跨两轮时取上面那一轮。
- 快捷键 Alt+C / Alt+Shift+C,可在设置里改。收起之后如果按钮本身消失了,焦点会回到工具栏的另一个控件而不是页面
<body>。
设置面板
点「viewtune ⚙」打开,分三页(键盘用方向键 / Home / End 在页间切换):
- 视效:动效(被系统的「减少动态效果」覆盖时会写在行里说明)、磨砂玻璃(每个面一个框,框里是「透明度」与「模糊值」两条档)、壁纸。
- 功能:这个插件对 App 做了什么,分三组(组间一条分隔线):小功能——竖条滚轮、产物用右侧栏打开、自动收起更早流程、收起按钮(这个按钮做什么,三选一,第三项是「默认」);正文显示——逐词显现、逐词显现的模糊、正文更新节奏;流程展示设置——跟随到最新、思考卡自动跟随、自动滚动速度、焦点思考展开。只在别的设置下才有意义的项(模糊、速度)缩进排在它下面,并在不适用时置灰。
- 快捷键:本插件自己那两个绑定的录制(点键帽录制,Esc 取消,清除即不带快捷键;没有修饰键或已被浏览器占用的组合会被拒绝)。「收起按钮」是行为而不是绑定,所以它在功能页的「小功能」组里。
一条排版约定:加一个偏好就是加一行、加一类就是加一页,工具栏不会因此变宽;只有标签说不清的选项才带说明,那句说明挂在该行的 title 上(浏览器弹的原生框,和按钮、「收起」是同一套),平时不占版面。而被系统覆盖、录制中、组合被拒这类状态直接写在行里。
设置的出厂值就是本插件默认的那一套,而且是照读者自己调好的样子写的:皮肤开着、壁纸是自带那张、铺满整窗、正文节奏固定 60 次/秒、卡片与代码块是磨砂的、工具栏是清晰的(见下面每条档的初始值)。老记录里只保存读者改过的键,缺的键用出厂值。
磨砂玻璃
打开后工具栏、卡片、工具框与代码块都不再自带不透明底板,透出壁纸与宿主皮肤;路径 / 行数这类标签静止时透明,悬停或聚焦才显出轮廓。开关下面九个旋钮分别调各面的不透明度:
| 旋钮 | 管什么 | 模糊值(初始) |
|---|---|---|
| 工具栏 | 顶部那条泳道的底板、浮动在正文上的「回到最新」胶囊,以及对话页宿主自己那个「回到底部」按钮 | 0px(不磨砂;胶囊与按钮是它的 3/4,所以也是 0) |
| 用户气泡 | 你自己那条消息的底色 | 3px |
| 卡片与面板 | 推理卡、工具框、系统提示词、备忘框等面板 | 8px |
| 代码块 | 围栏代码块的底色 | 25px |
| 差异面板 | 差异面板与增减行的底色(含行号槽)、文件标签、工具结果里的行内差异,以及对话页那张「已编辑 x 个文件」卡(含它的卡头与悬停详情) | 25px |
| 滚动条槽位 | 最右侧那条滚动条的轨道 | —(槽位是滚动条伪元素,模糊在那上面无效,所以没有这条档) |
| 产物标签 | 一轮末尾那栏产物文件 | 5px |
| 用量与步骤胶囊 | 「用量 … tok / … 个步骤」两枚计数胶囊 | 2px |
| 输入框 | 宿主输入框的底板(阅读页与对话页都生效) | 10px |
0% 是「这个面什么都不画」,100% 是关掉皮肤时那块不透明底板。分组按「是什么」而不是「在哪」(差异纸无论画在面板里还是工具「结果」页里,都归「差异面板」),而且只给本来就有底板的面配旋钮——工具状态与行数计数在基础样式里本没有静止底板,所以不接任何旋钮。
每个面一个框,框里有两条档(这是读者定下的版面):第一行是这个面的名字,下面两行各自写着自己的名字——「透明度」决定这个面透出多少壁纸,「模糊值」决定透出来的是照片本身还是一层雾(0px 就是只有玻璃,往上才是磨砂)。表里那一列就是出厂值,而且是读者自己调好的那套:出厂时卡片、代码块、差异纸与输入框本来就是磨砂的,工具栏反而是清晰的(0px)——所以"磨砂玻璃"这个名字在你第一次打开时就成立,不必先去拨任何一条。模糊值旋到底(0px)时插件是把那条属性删掉而不是写 0px:backdrop-filter: blur(0px) 并不等于没有模糊,它会创建层叠上下文并把绘制放进合成器,代价是白付的。
两个遮罩(侧栏遮罩 / 顶栏遮罩)也是同样的框:名字一行,「透明度」与「模糊值」各一行。出厂值分别是侧栏 25% + 15px、顶栏 25% + 5px(两个面的磨砂是两个独立的数);同样在 0 时删掉属性,为的是不让这两个元素重新背上合成层(它们以前正是因为这个在新加载时"遮罩丢失",见 CHANGELOG)。
同一个皮肤可以再伸到对话页(独立开关):那里的代码块与用户气泡跟随上面这些旋钮,0.2.0 新加的那种「已编辑 x 个文件」卡片(连同鼠标悬停时弹出的改动详情)与宿主自己那个「回到底部」按钮也一样 —— 它们画在皮肤从没拨过的 token 上,所以以前一直不受影响;还有一个独立开关把对话页改成整页纯色底(主题底色 + 输入框上方一条淡出),与皮肤互不依赖。「回到底部」与阅读页的「回到最新」胶囊共用一个下限:旋钮到底也不会把它拨到看不见。
壁纸
插件自带一张默认壁纸(assets/sample-gradient.png),第一次启动时由宿主放进读者的壁纸文件夹——新装上去打开就是这个样子,不用先自己找图。
- 图片放在插件自己的文件夹里:点「打开文件夹」会建好并打开它,把图拖进去再点「刷新」就变成缩略图列表;点一张即生效,「清除」回到不设(这与「没有记录」是两件事:前者是你要无壁纸,后者才落到自带那张)。同名文件被替换后缩略图与背景都会更新。
- 浏览器不知道这个文件夹在哪:它只按名字要图,宿主那半只在那个文件夹里、且只对图片扩展名作答(不含 svg)。
- 压暗滑块把图混向主题底色,好让正文压在上面也看得清(出厂 50%);开着磨砂玻璃时卡片糊的正是这张图。
- 范围默认是整个窗口:侧栏与顶部栏也透出它(那两栏原本的底板被让开),输入框周围的底色跟着走;关掉「铺满整个窗口」就只覆盖阅读视图那一列,此时图片填满阅读页面(按「图片 ÷ 目标」的最大值取比例、居中裁掉溢出,小图会放大去填满)。整窗范围用
cover填满。 - 界面遮罩:整窗范围下,这两栏压在自己那份壁纸上的浓度——它们上面全是文字,
0%就是直接压在照片上。图只画一次、遮罩按窗口算一层,所以阅读区不会比旁边更暗。两个面的出厂值分别是侧栏 25% + 15px 磨砂、顶栏 25% + 5px 磨砂(两个面的磨砂是两个独立的数:它们面对的东西不同)。 - 输入框上方那条淡出带是把宿主的不透明渐变改成「遮罩 + 同一份底图」,所以内容照样平滑淡出,只是淡入的从一块底色变成你的图;阅读、对话、轨迹三页用的是同一个抬升量。
正文显现
流式正文"长出来"的过程有三项可调:
- 正文更新节奏(默认固定 60 次/秒):一次更新就是一次整个增长节点的重渲染 —— 尾部 markdown 重解析、词身份重算、挂上新的词元素,以及下面所有跟随器要量的布局。跟着屏幕刷新率走,意味着在这块 240Hz 屏上是四倍的工作量,而且节奏会跟着「上一帧花了多久」走。「跟随屏幕刷新」因此是显式选项,它的代价写在那一行自己的说明里;两种节奏驱动的是同一条显现轨迹(推进量是时间的函数),所以固定节奏只是把轨迹采样得稀一些。实测(同一台机器、同一页面、同样流式长文):3430 → 4435–4568 帧/20 秒(≈171 → 222–228fps)、掉帧 4 → 2、长任务 0,空闲时满帧 4800。
- 逐词显现的模糊(默认关):参考配方让每个词淡入的同时把 1px 模糊化开,而
filter不是合成器能独立动画的属性 ⇒ 每个正在显现的词每帧都要重绘自己那片区域;一个词的显现持续 350ms、而正文流里每秒约五十个词到达 ⇒ 任何时刻都有十几个这样的重绘重叠着跑。关掉后只剩淡入(观感差别是"清晰浮现"而不是"由糊变清")。 - 逐词显现(默认开):关掉后正文直接出现,连词身份和时间线都不再产生(省掉的不只是动画,还有分词与那本 birth 账,markdown 那条路也不再挂上显现钩子)。推进节奏仍来自流缓冲,所以看起来还是一点点写出来。
- 跟随到最新(默认直接贴底):每次增长只写一次滚动位置,滚动 spy、锚点补偿与各处测量都不会被每帧的写入唤醒。这是读者自己的选择,出厂如此;切到逐帧滑行则是另一种代价 —— 每帧写一次、每次都产生一个滚动事件,换来的是滑过去而不是跳过去。这个选择也管到卡片自身的长高:滑行时,「焦点思考展开」的卡片会缓入每一行新高度(是滑上去,不是跳一格),而且底边留在原处 —— 补偿逐帧追着那次缓动走;直接贴底(也就是出厂值)时,高度像以前一样瞬间到位(「动效」关掉或系统要求减少动效时同样如此)。
- 自动收起更早流程(默认开):开着时,只要有一轮在流式,只有正在长的那一轮的过程保持展开,其余轮次全部折起 —— 包括本该保持展开的"未完成"轮次(中断 / 出错),也包括你之前点开过的(新的一轮开始时那些选择会被清掉)。折起的过程会卸载内容而不是隐藏,所以开着它时,正在长的那一轮之外的过程不参与渲染与布局。你自己点开的那一轮仍然保持展开,直到下一轮开始。
- 思考卡自动跟随(默认跟随最新):思考卡内部一直是"每 840ms 前进 2 行、500ms 滑过去"(约 2.4 行/秒,大体是阅读速度,所以模型爆发时它故意落后)。三种模式:自动滚动就是这个节奏(840ms 的停顿,以及每步之后的休息);跟随最新不是——它冲着最新一行去,用的是 180ms 的短步长、且步与步之间不停,因为目标只在一步开始时重算,沿用那个长停顿就等于把延迟变成"一个停顿加一步"而不是一步;手动滚动卡片自己绝不动。三者的接管、边缘渐隐与跟随交接完全一致。
- 自动滚动速度(默认 3 行/秒):四档预设。节奏按整行量化,所以每档在 840ms 节拍下算出的步长会落在整行上(3 行/秒 ⇒ 每步约 2.5 行,量化后就是你看到的那一步);出厂这一档是读者自己选的速度,旧记录里没有这个键时落到标准档 2 行/秒(那是这张卡一直以来的步长)✓。
- 焦点思考展开(默认开):开着时,正在被写入的那张思考卡会长高来显示内容,直到「展开阅读」那一档的上限(
min(60vh, 560px))。判定发生在你正停在正文最下方、且某张卡正在被写入时。在卡片里往上翻=你在读这张卡:卡片保持展开、不会被折回小卡,这段时间里别的卡抢不走焦点,它自己的"空闲"也不会结束它 —— 这叫钉住,只有你自己能结束(把页面滚回最底,或点「回到最新」),或者点「展开阅读」、关掉这张卡。在卡片外往上翻=你离开底部去读别处:焦点与高度照旧保留,但页面那条"长行就把页面往下推"的补偿立刻停下(否则会和你的滚轮一行一行较劲),交接照旧 —— 新一张卡开始被写入时仍可接手 ✓。释放时跟随最新会把它折回小卡(另外两种模式保持高度),页面恢复贴底 ✓。高度按整行增长(布局次数从"每次发布"降到"每行一次"),上限留在样式表里;「逐帧滑行」时每一行是缓入的,而「直接贴底」时是瞬间到位(见上一条)✓。展开态本来就不画那两条遮罩渐隐 ✓。关掉它时三种跟随模式依然有效,只是作用在小卡上 —— 和现在一样 ✓。
页面跟随与「回到最新」
- 跟随只在有进行中的轮次时发生(判定与状态行、等待时钟读的是同一个
status === 'open')。没有轮次在跑时,内容再长也不会把你拉回底部 —— 那只是和你自己滚动较劲。 - 轮次结束后还会多跟约 1.5 秒:一轮并不是在回答写完时结束,产物栏、用量/步骤、操作行都是在那之后才排布的;不留这一拍,它们就会落在视口下方,你还得再滑一下。
- 三种"不动"是三件事:① 你接管(滚轮、触摸、PageUp/Home/向上键,或你自己往回滚)—— 跟随关掉,「回到最新」亮起 ✓;② 跟随被临时挂起(「焦点思考展开」期间页面本来就不该跟着卡片动)—— 页面只是暂时不动,别处长出来的高度不会被误读成"你滚开了",挂起一结束就自己接回去 ✓;③ 你在视图里输入(输入框 / 可编辑区)—— 跟随停下来等你,但不算你接管,也不会亮药丸 ✓。
- 「回到最新」胶囊只在①之后出现;点它 = 回到最新并恢复跟随。②那种情况不该亮药丸 —— 以前会(而且亮起后再也回不来 ✗),那是被修掉的 bug 之一 ✓。
竖条滚轮
阅读列两边那两条宽度把手属于外壳,位置在滚动容器旁边而不是里面,所以在那上面滚轮原本什么都不会发生。DSH 0.2.0 起,会话面板自己把把手上的滚轮转发给正文的滚动容器(一次即时的 scrollBy),于是这个开关决定的已经不是"滚不滚",而是怎么滚:开着(默认)用本插件的缓动,关掉就交给 App 自己那一跳。
- 一格一格的滑动由临界阻尼弹簧送出(单格与每次落地都是它),手感的两端可调:甩动与单格。
- 连续滚动(间隔 0.06–0.20 秒,约 5–16 格/秒)改为按轮子自己的速率匀速带走——速度取「当前这一格 ÷ 最近三个间隔的平均」。这是为了跟浏览器的手感对齐:同样手速下普通滚动不是匀速的,所以更慢或更快的滚动都仍是一格一格。
- 一次连续滚动要两个间隔才算成立(一个间隔不是节奏),停顿超过 0.2 秒就结束它。
- 开关关闭时这一格整个让出去:既不
preventDefault也不stopPropagation,交给 App 自己的即时滚动;开启时则整格归本插件(连传播一起挡住),否则 App 那一跳会和弹簧抢同一个滚动位置。
设置存在哪
- 浏览器里一份(
localStorage),宿主里一份(<instance home>/viewtune-settings.json)。宿主这份是真正跨重启的:GUI 跑在临时端口上,而localStorage按来源(scheme + host + port)分区,所以只靠浏览器那份,每次启动都是一个新的空来源——「每次退出 DSH,设置全没了」。 - 页面改动后会防抖写入宿主,并在离开时补一次;只有宿主接受的那次写入才会被广播给全 App 的外观(例如壁纸与滚动条槽位)。
已知限制(以及「说不出来时它怎么显示」)
- 被历史窗口截断的轮次不显示步骤数字。Harness 按消息条数分页,最上面那一轮可能只加载了一部分,它的步骤数是局部和、与过程记录里的绝对步数不可比——偏小的分母会误导,所以不显示;取而代之的是 「步骤记录」 标记,悬停提示「请完全加载该轮次记录后查看」。它只是说明:没有点击行为、没有 hover 高亮,也不会出现任何估算出来的数字。
- 用量胶囊偶尔不出现属于有意为之:宿主只在能精确证明该轮计费数据时才给出用量。本插件跟随这一取向,不猜、不估算,也不为它加标记——缺数字就是缺数字。
- 界面文案目前只有中文。宿主提供了语言座位,本插件也已经在借用它翻译宿主自己的两条文案(
message.contextRecall/message.contextInjection),但插件自身的文案没有走本地化——做成完整的 zh/en 需要先给插件加一个 locale provider 边(要重启 Host)。这是权衡后的取舍,不是遗漏。 - 设置是「整条覆盖写」,没有版本号也没有时间戳。客户端每次改动都 PUT 整份记录,宿主用「写临时文件再 rename」落盘。代价是明确的:两个窗口/标签同时改设置时,后写的静默覆盖先写的(没有合并、没有冲突提示)。之所以可以接受:这份记录是一个读者自己的偏好,一个实例里同时开两个窗口改同一项的概率很低;要修就得引入版本号与冲突策略,那比它保护的东西复杂。
- 读取回来的那几百毫秒里做的改动会被回滚。设置是启动时读一次(
loadHostSettings),在它返回之前:改动发不出去(写入器要等读取有个结果才armed),而且会被随后到达的记录覆盖。慢宿主上前几百毫秒的改动就是这样消失的。顺带一个可测的浪费:hydrate本身会改变 state、于是立刻把刚读到的记录又 PUT 一遍(字节相同)。两条都留着:要修得先把「这次改动是读者刚做的」与「这次改动来自记录」分开记账,那是一套新的状态机,而窗口只有几百毫秒。代价写在这里,是因为它确实存在。 - 写得出来、没有消费者的
data-*属性是有意保留的观测面:data-reasoning-mode/-from/-target/-began/-moving、若干data-ud-check与data-reader-*在插件自身代码里没有读者,它们是工作区里那批脚本的抓手(render-dump.mjs、feature-matrix.mjs、verify-patched-instance.mjs、probe-linked-host.mjs、marker-scan.mjs等都会读它们),也是产物层断言与"跑一次真实页面看状态机走到哪"的唯一观测点。
开发
构建
浏览器半边是预编译的 lib/client.js,Host 启动时直接加载它。仓库把编译结果一并提交,所以安装与分享都不需要构建:
npm ci --legacy-peer-deps # 按仓库里的 package-lock.json 装依赖(为什么必须带这个参数见下)
npm run build # src/ -> lib/client.js 与 lib/dsh-viewtune.js
npm run typecheck # tsc -p tsconfig.json --noEmit,对着真实声明检查
用锁文件而不是裸 npm install:tsdown / lightningcss / typescript 都写在 ^ 区间上,只有锁文件能钉住能复现当前 lib/ 的那套工具链——npm run guard 里的 verify-build 正是这么比的。只有动了依赖才用 npm install 回写锁文件。
--legacy-peer-deps 是上游包的 peer 声明造成的,不是本仓库能改掉的东西。0.2.0 这一代 @deepseek-ai/dsh-* 的开发依赖(以及它们互相之间的 peer)都钉在 0.2.0-rc.2,而其中若干包把 peer 写成区间(^0.2.0-rc.2)——在 0.1.5 那一代,注册表把这种区间解析到了 0.1.5-rc.3,于是裸 npm ci 报 ERESOLVE;现在解析器接受这组声明了,但换成另一种失败:peer 不在锁文件里(EUSAGE: Missing: @deepseek-ai/dsh-agent@0.2.0-rc.2 from lock file,随后是几十条同样的 transitive peer)。
两条命令都实测过,结论写在这里:npm ci --legacy-peer-deps --dry-run 通过 ✓,裸 npm ci --dry-run 失败(EUSAGE)✗。也就是说,本仓库安装的是锁文件里的那棵树:tsdown / lightningcss / typescript / react 与 13 个平台包,而 --legacy-peer-deps 让 npm 不去自动补装那批 peer。这棵树足够构建、类型检查与跑测试(三者都在 CI 里跑过),但它不是 npm install 的默认结果——把 peer 全部拉进来会装出一个更大、且在本仓库里从未被验证过的树。要复现当前 lib/,就用上面这条命令。
上游的 tsdown.config.ts 从一个不对外发布的 Harness 适配器取 externalClientBundle,所以在克隆出来的仓库里跑不起来。本仓库把它的真身(官方预设的 clientBundle(),Harness tag dsh-v0.1.5-rc.2)移植成了 scripts/client-bundle.mjs。产物契约不变,针对产物的断言依然有效——这一点在 0.2.0 上重新核对过:0.2.0 的客户端模块系统仍然以 window.__ModuleLoader__.load({ id, factory }) 注册、仍然用 data-plugin / data-plugin-css 认领自己注入的样式,与本仓库产物的形状逐项一致(0.2.0 的 dsh-client-modules 里读到的)。移植的预设没有因此改动,因为它决定的是产物形状,而形状没变。
改完记得同时提交 lib/:别人拿到的是这份产物,而不是让他在本地构建。
改代码
改动显示逻辑时,改 src/ 然后 npm run build。产物里的两处身份标记必须与 package.json 的 name 完全一致,否则整页会因为 loaded without registering "<id>" via __ModuleLoader__.load 而失败:
window.__ModuleLoader__.load({ id })的行 id(宿主由安装清单的包名派生);- 每个 CSS 模块的
tagId前缀与document.createElement("style")的data-plugin(HMR 按 plugin id 移除本插件的样式)。
tests/stock-install.test.ts 就是这两条的守卫;同一个测试还会检查本文档与 README.en.md 的安装命令与包名一致。
把克隆出来的仓库放在
node_modules之外。dsh plugin add会在 profile 目录里跑 pnpm,而 pnpm 会清理node_modules下未在package.json中声明的目录——仓库连同.git可能被一起删掉。
验证
分两层,因为它们回答的是不同的问题:
npm test # 源码层:Node 自带测试跑当前全部测试文件(35 个)
npm run guard # 产物层:23 条不变式,针对构建出来的 lib/client.js 与 lib/dsh-viewtune.js
npm run guard 检查的是产物:模块表注册 id 与 require() 集合、注入的 CSS 字面量是否完整、轮次渲染顺序、收起控件的形状与淡出、工具栏几何、设置面板的三页与滑块的宽度、自带壁纸的三处拼写与文件本身、Host 半边的几条路由决定(两个未鉴权请求体的上限、请求中断时不会留下永不落地的 promise、写盘失败必须回失败、壁纸路由的重新校验与 ETag)、产物从平台包里读的每一个名字是否真的存在(0.2.0-rc.2 换掉的那批图标名、标签字段、hook 名就是这一类:类型层会拦住源码,而产物是另一份字节,读到一个不存在的导出只会渲染出空白),以及**src/ 与产物的对应关系与「重新构建仍能复现契约形状」**。最后一条是这类仓库唯一能做的「产物与源码一致」证明——两边的字节永远不同(lib/client.js 里的 CSS 类名哈希由产物自身的绝对路径决定,实测同一份样式表在两个目录里得到 voucca_ 与 ED26sW_),所以它比对的是注册 id、require() 集合、导出、CSS 字面量与每一条样式表规则(名称差异先归一化),并在输出里把两个长度都列出来,不再用一句「复现」暗示字节相等。
这套做法有个前提值得说明:能解析(node --check 通过)不等于正确。本仓库的历史大量是直接改压缩产物,出现过语法完全合法、却因为一个引用被删掉而在运行时抛错的情况——那类错误只有「名字是否有声明」这一层检查能抓住,或者「重新构建一次」能暴露。两条都在 npm run guard 里,而且每条新断言都做过反向自检(喂一份动过手脚的产物,确认它真的会失败)。这份反向自检表就在仓库里,可以自己跑:
node scripts/guard/negcheck-shape-markers.mjs # 61 个用例:逐个把产物改回修复前的形状,要求对应断言全部失败
它不进 npm run guard:每个用例都要起一到三个新的 guard 进程,整表是分钟级,而 npm run guard 是每次构建后都要跑的快速闸门。表里出现 SKIP 会被算作失败——那说明产物已经没有被断言的那个形状(断言在空跑),或者已经有被改回去的形状(用例测的不是它说的东西)。
许可
本仓库按 MIT 发布,见 LICENSE。
它的起点是 aa2246740/dsh-better-display(MIT):阅读页签、流式动效与 Markdown 渲染来自那里。展示与 Markdown 部分源自 DeepSeek Harness(MIT)。动效参考 Transitions.dev。随包分发的默认壁纸是仓库里的 assets/sample-gradient.png,为本仓库自制,与代码同样按上面的 MIT 发布。
Comments
Loading…
Similar plugins
by rangdl
DSH(DeepSeek Harness)功能增强插件
★ 0
TypeScript
Sep 1, 2026
dsh plugin --profile web add dsh-all-enhanceby WEIHAOLEE
DeepSeek HARNESS 视觉代理插件
★ 0
MIT
JavaScript
Aug 17, 2026
dsh plugin --profile web add @local/dsh-vision-fallbackby Scorp1o117
Vision model for DeepSeek Harness | DeepSeek Harness 外置视觉模型插件
★ 8
↓ 220/wk
NOASSERTION
JavaScript
Sep 30, 2026
dsh plugin --profile web add dsh-tool-visionby Zydr114
A dsh(deepseek-harness) plugin to enhance markdown rendering.
★ 0
JavaScript
Aug 16, 2026
dsh plugin --profile web add dsh-client-ui-markdown-enhanceby xjackzenvey
Deepseek Harness 增强工具
★ 0
MIT
TypeScript
Aug 14, 2026
dsh plugin --profile web add dsh-ui-enhanceby nanbbb
DeepSeek Harness 增强插件:视觉/记忆设置面板 + 插件市场 + 本地模型 + 一键重启引擎
★ 0
MIT
JavaScript
Aug 15, 2026
dsh plugin --profile web add dsh-plus