dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Reverses the conversation reading layout for DeepSeek Harness: the composer pins to the top of an active conversation and the transcript plus turn navigator render newest-first.
0.7.0 更名:原名dsh-composer-top。旧名只写了「输入框置顶」,新名字覆盖插件做的两件事——「对话栏倒序阅读」+「输入框置顶」。内部包名 / Loader id / 开关属性(data-dsh-reversed-chat)同步更名,功能不变。
自实现的极简 DSH 客户端插件(参考 dsh-layout-control 的思路)。为「程序员职业病(颈椎)」定制阅读布局:
宿主用相属性控制输入框位置(@deepseek-ai/dsh-client-ui-conversation / ConversationRoot.module.css):
hero → justify-content: center → 居中(不碰)active → [data-composer-seat] { position: sticky; bottom: 0 } → 贴底(覆盖为贴顶)apply(ctx) 做六件事:
打开关属性 data-dsh-reversed-chat`(停用即消失,布局零残留);[data-content-phase] / [data-composer-seat] / [data-slot=…] / [data-chat-flow],不碰 CSS Module 哈希类名);data-dsh-top-main 标记 —— 子代理面板、右栏会话等嵌入对话永不误伤;data-dsh-to-bottom-slot 标记并隐藏(唯一子元素是纯图标 ` 的浮动容器,且有 flow/composer/nav/tooltip 四道排除)——该槽在库存 DOM 里没有稳定属性、只有哈希类名,所以这里用结构识别而不是 [class*=…]`;DOM 若变化,最坏结果是该按钮重新出现(纯装饰);scrollTop / scrollTo:宿主所有「跳到底部(最新)」的写入(打开会话、跟随流式输出)目标是 metrics.floor(最大滚动值),一律改写为跳到顶部(倒序后最新在 scrollTop 0)。其余程序化滚动(导航条跳转、锚点恢复)按几何原样放行。readVisibleTurn)执行期间遮蔽 flow 行的 getBoundingClientRect,让倒序布局下探测落在阅读线所在回合(默认最新),详见「0.6.0 修复」。@tanstack/react-virtual 只用于右侧 turn 导航条);flow 项就是 [data-slot="conversation.chat.flow"] 的直接子元素,flex-direction: column-reverse 只翻转视觉顺序,DOM 顺序与宿主全部测量逻辑原样。scrollTop 0 即时可见,overflow-anchor: none 防止滚动锚定把阅读位置拖走。导航条是 TanStack 虚拟化的点列表(transform 定位),不能动它的滚动数学,所以在 nav.frame 级做整帧镜像:transform: translateY(-50%) scaleY(-1) —— 库存的 translateY(-50%)(居中)留在镜像外层,帧的位置分毫不动;点、悬停浮泡随帧一起镜像。稳定锚点:nav:has(button[data-index])、[role="tooltip"],零哈希类名。
两个坑(0.5.1 修复):CSS scale 属性是独立变换,组合在 transform 之后,所以
scale: 1 -1 直接镜像帧时,帧自己的 translateY(-50%) 也被翻转 → 整帧下沉自身高度(最多 420px,半截出屏);button[data-index] { scale: 1 -1 } 时,点的定位就是它自己的行内 transform: translate3d(0, start, 0),被翻转后每个点落到槽位的镜像一侧(滚动口的 overflow 裁剪之外)→ 12 个回合的画面里只剩 1 个点可见,即「用户发了多条对话,导航条只有 1 个跳转点」。现在的做法:帧用 transform: translateY(-50%) scaleY(-1) 镜像;点不做反向翻转(点的字形是 20×2 的水平条 .mark::before,上下对称,本来不需要翻正);只有浮泡里的文字子元素反向翻转保持可读(两行上下次序对调,纯装饰取舍)。
置顶后卡片若直接贴着 tab 栏下沿,会同时有两个观感问题:太挤,以及卡片自带的柔和阴影(--dsw-elevation-soft)被滚动口在顶部裁掉,看起来像“贴边”。0.7.1 的处理:
padding-top,卡片浮在横线下方,阴影有呼吸空间;留白区用页面底色(--dsw-alias-bg-base)铺满,转写内容不会从缝里透出来;--dsh-reversed-chat-top-gap(默认 12px)、--dsh-reversed-chat-bottom-gap(8px)、--dsh-reversed-chat-fade(20px)。只在 active 相生效;hero(新对话居中)与 settling(切换过渡)保持库存行为。
package.json dsh.client 声明(platform web, inject [])+ dsh.bundle.patch 声明
+ locale / icon 的 exports(管理器显示元数据)
cordis.patch.yml - insert: 一条 Loader 条目 → 管理器列表/开关的数据源(唯一插入处)
locale/en.json 英文显示元数据:meta.title / meta.description
locale/zh.json 中文显示元数据
icon.svg 插件图标(exports["./icon"],≤256 KiB)
lib/index.js 宿主半边占位(空 apply,供 Loader 条目挂载)
lib/client.js 浏览器半边:window.__ModuleLoader__.load({ id, factory }),导出 { inject, apply }插件管理器的列表读的是 Loader(cordis 加载树)条目(dsh-host-plugin-inventory 的 readPluginInventory() 遍历 ctx.loader.entries()),而 dsh.client 声明只驱动客户端扫描器加载 lib/client.js —— 两条通道互不相干。没有 - insert 行 = 功能生效但管理器里没有名字和开关。dsh.bundle.patch 声明让 dsh plugin add 在安装时自动应用该补丁。
insert 只能存在一处。 bundle 通道下(dsh.profile.bundles 含本包)这一行由插件自带的 cordis.patch.yml 提供;profile 的 cordis.patch.yml 只允许写 id 覆盖行(例如默认关的 disabled: true)。若 profile 再镜像一条同样的 insert,合成结果会出现两行同 id:管理器的 listPlugins() 找不到"唯一"补丁行,该行变成只读(界面提示「当前 profile 的 patch 无法唯一定位这一项」),开关失效;同时 Loader 按 id 后写覆盖,默认开关状态还会随两行先后顺序翻转(disable 行在 insert 之前就会变成默认开启)。dsh-better-sidebar 的 bundle patch 注释里也警告过 manual mount line 必须删除。
从 npm 安装(推荐)
dsh plugin --profile desktop add "dsh-reversed-chat"从 GitHub 安装(公开仓库)
dsh plugin --profile desktop add "github:lingr7/dsh-reversed-chat"本地开发(link 安装)
先完全退出 DSH,然后:
:: 首次
dsh plugin --profile desktop add "link:<plugin-dir>"
:: 更新(link 安装指向本目录,拉新代码后重启 DSH 即可;若被复制安装则先 remove 再 add)启动 DSH → Ctrl+F5 → 验收:
disabled: true 覆盖行 → 默认不生效;到「插件管理」打开 dsh-reversed-chat,然后 Ctrl+F5,以下效果才出现。开关切换后都需要 Ctrl+F5(客户端模块在页面启动时装配)。该覆盖行就是 profile 层唯一允许的条目,不要再写 insert(见上一节);turnOutline 投影,与已加载窗口无关;0.5.1 修),最新回合的点在最上,点击跳转正确,悬停预览跟随点位且文字可读;宿主硬性要求:「bundle patch 含配置/表达式, 热挂载仅支持纯 insert; 重启后由 bundle 层生效」。因此:
cordis.patch.yml 永远是纯 insert(只有 id + name,无配置字段、无 !!js 表达式)→ dsh plugin add 可以热挂载,安装即时生效(启用状态下);这也是该行唯一的插入处;- id: dsh-reversed-chat + disabled: true,与 better-sidebar 禁用行同形态)。客户端扫描器跳过被禁用的 Loader 条目(processOne() 的 entry.disabled 检查),lib/client.js 不装配,布局零改动;unaddressable(「当前 profile 的 patch 无法唯一定位这一项」)而无法开关。排查/修复口径见上一节;setPluginEnabled → writePluginEnabled 增删/改写 profile 层的 id 覆盖行并热重载 Loader 条目(composeEntries 统一合成有效状态)→ Ctrl+F5 后客户端装配清单随之变化;- id: dsh-reversed-chat + disabled: true。现象:会话有多个回合,右侧导航条却只显示 1 个点;回合越多越明显。
根因(0.3 引入):scale: 1 -1 是独立变换属性,按规范组合在 transform 之后。
nav.frame)自己的 transform: translateY(-50%) 被翻转 → 帧下沉自身高度;button[data-index])的定位就是它自己的行内 transform: translate3d(0, start, 0),被 scale: 1 -1 翻转后点落到槽位的镜像一侧,超出滚动口(.scroller 的 overflow-y: auto)的裁剪范围;结果只有最靠近裁剪边界的那 1 个点可见。DOM 里点一个不少——TanStack 的 count/getVirtualItems 完全正常,丢的是渲染位置。修法:镜像折进帧自己的 transform(translateY(-50%) scaleY(-1),居中项留在镜像外层);点不再反向翻转(字形是水平条,对称);浮泡文字子元素保留反向翻转。只改 2 条 CSS / 1 条 transform,不动虚拟化、不碰点击与滚动数学。
验证(Chromium + 真机同款客户端模块,非模拟):12 回合会话 12 个点全部可见、最新(turn 12)在最上;2 / 6 回合同理;1 回合会话仍不显示导航条(库存 items.length < 2 逻辑);悬停浮泡中心与点位对齐;点击第 7 个点落到 turn 7 与 turn 6 的视觉分界(上方 turn 7、下方 turn 6,符合倒序阅读)。
现象:会话有多个回合时,右侧导航条的「当前回合」实心长条(.markActive)总是落在最早一回合(画面最下方那个点);倒序阅读下视线所在的是最新回合(最上方),预期应是最新回合实心。
根因(宿主 @deepseek-ai/dsh-client-ui-chat 的 readVisibleTurn()):宿主用二分查找求「阅读线所在回合」,前提是 DOM 顺序 = 视觉上下顺序(row.getBoundingClientRect().top 随 DOM 索引单调递增)。flex-direction: column-reverse 只翻转视觉顺序、DOM 仍旧→新,于是:
top 随 DOM 索引递减,二分判据方向相反;reading 保持初值 turns[0] —— 最早回合;activeTurn 恒为最早回合 → 轨道实心高亮、自动居中跟随(scrollToIndex(activeIndex))、aria-current 一起错。修法(不改宿主、不动 DOM 顺序):只在宿主探测调用期间补偿几何。插件在 flow 行元素实例上遮蔽 getBoundingClientRect,用调用栈识别当前是否为 readVisibleTurn 在问(栈帧含 readVisibleTurn),是则:
scrollport.top + min(96, clientHeight * 0.2);top 不大于阅读线的行里 top 最大者;若没有(阅读线在转写之上)取最上面的行;top = line - 1、之后的返回 top = line + 2 —— 二分查看到单调递增的假顺序,精确落回该行;align())一律拿到原样 rect,几何跳转分毫不动。行元素增删由 MutationObserver 即时补挂;切换会话时随主滚动口重挂;插件停用时全部还原(删除实例属性,原型行为恢复)。
验证:6 回合会话默认最新回合为实心长条、位于导航条最上;向下滚动读史时长条跟随阅读线;点击跳转、悬停预览、导航条居中跟随照常;aria-current 同步正确。
readVisibleTurn() 的二分查找假设「DOM 靠后 = 视觉靠下」:turn 高亮已在 0.6 用「探测期几何补偿」修正(依赖宿主函数名 readVisibleTurn;若未来宿主改名/内联导致栈帧缺失,补偿静默失效,回退为 0.5 的「恒最早回合」表现,不会崩溃)。阅读位置捕获/恢复仍按原假设工作,加载更早消息后落点可能轻微偏移。纯体验损耗,不影响功能。data-dsh-to-bottom-slot,不依赖哈希类名);回到最新 = 滚回顶部。若库存 DOM 结构调整导致识别失效,唯一后果是该按钮重新出现(纯装饰)。nth-last-child 判定按 DOM 序,反转后出现位置可能不符设计(纯装饰)。dsh-layout-control 的「停靠四角」功能重叠会打架:二选一,或其输入栏保持「完整」。locale/.json 的 meta.title / meta.description(本包提供 locale/en.json、locale/zh.json,经 exports["./locale/*.json"] 暴露);exports["./icon"] → icon.svg(宿主支持 SVG/PNG/JPEG/WebP,≤256 KiB,data: 内联后交给界面);package.json 的 files 必须同时包含 lib、cordis.patch.yml、locale/*.json、icon.svg:漏掉 `cordis.patch.yml` 时打包产物会缺 bundle patch,dsh plugin add 直接失败(本地 link: 安装不受影响);dsh/exports 约定,不读运行时状态,因此管理器不激活插件也能显示。dsh-layout-control(思路来源:overlay + 稳定锚点 + 属性开关):%USERPROFILE%.dsh\profiles\desktop\node_modules\dsh-layout-control,GitHubdsh-better-sidebar(client 插件写法范本):GitHubdsh/node_modules/@deepseek-ai/dsh-client-ui-chat/lib/client.js(ChatView、ScrollFollow、flow slot 注册)与 dsh-client-ui-conversation/lib/client.js(ConversationContent、conversationPhase())