dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Sidebar topic grouping plugin for DeepSeek Harness (DSH) - client-only, zero host-service dependency, with offline smoke tests.
给 DSH Web 侧边栏加一层按话题分组会话的能力。
只想先会用? 跳到 用户手册(装 / 开 / 关 / 恢复 / 卸载 / 升级)。 想接手改进代码? 读
docs/HANDOFF.md—— 面向下一个 agent 的交接文档 (代码地图、必须遵守的契约、已知问题与改进路线)。
DSH 官方的侧边栏只能按工作区分组,而工作区的成员资格是从每个会话记录的
cwd 推导出来的 —— 一个工作区只拥有 cwd 等于它自己路径的会话。所以同一个
目录下开出来的会话全挤在一组里,没法按主题拆开。
本插件加一条纯展示层的轴:话题文件夹。
话题是工作区作用域的 —— 一个话题只属于一个工作区,会话绝不会被挪到不属于 它的工作区里。这不是随手定的,有两条理由:
insertSessionBefore 的会话排序也是按工作区的。跨工作区的话题会同时破坏这两点。因此:跨工作区的拖拽会被直接拒绝(不建话题、不改归属)。这是有意为之。
+,在那个工作区里新建话题+ / ✎ / ×:新建子话题 / 重命名 / 删除MAX_TOPIC_DEPTH,改一个常数即可放开官方的拖拽手势是排序(拖到行的上/下半,host 持久化)。第一版把它挪用成 「拖到会话上就建话题」,等于抢走了用户本来就有的手势,还让排序和分组两个概念 再也分不清。
现在的边界是明确的:
| 你想干什么 | 怎么操作 |
|---|---|
| 调整顺序 | 拖到另一会话的上/下半(显示插入线) |
| 归入某个话题 | 拖到话题标题上 |
| 移出话题 | 拖到**「未归话题」标题**上 |
| 新建话题 | 工作区行右侧 +(或右键工作区) |
| 新建子话题 | 话题行悬停的 + |
本插件用 priority: -1 顶替官方 ui-workspace 注册的浏览器 —— 会话行从此由本插件
渲染。这意味着官方行上原有的每一个操作都属于本插件的责任。
第一版只实现了"打开会话",于是重命名 / 分叉 / 归档 / 工作区重命名删除全部静默消失。 保留会话是因为用户只看得见"少了个功能",看不见"是谁拿走的"。任何接管槽位的插件 都必须先盘点被替换方的全部能力,再逐条实现。
| 本插件 | 社区同类插件 | |
|---|---|---|
| host 半边 | 空壳,不 inject 任何服务 | 有的 inject storageDomain/workspaceRegistry |
| 持久化 | 浏览器本地(客户端 store) | 有的落 ~/.dsh 文件 |
| 启动风险 | 结构性为零(不依赖任何 host 服务) | 有挂起启动的先例 |
| 删除会话 | 不做 | 有的会写 .ps1 并 ExecutionPolicy Bypass 执行 |
| 官方数据 | 只读,从不写 | 有的改写/替换官方渲染器 |
| 官方 store key | 从不触碰(自有 dsh.session.topics.v2) | — |
明确不做的事:不删除会话、不调子进程、不生成脚本、不写任何 host 文件、不注册 任何面向模型的工具或提示词(零 token 成本)。
两条来自踩坑的硬约束:
workspaces/byId 去比对已经加载好的话题表 —— 于是每个话题
都被判定"所属工作区已不存在",每次刷新都把用户的分组整个删光。
修法是两层:数据没到齐(phase !== 'ready')就不跑;并且 store 层的
retain() 在比较集为空时直接返回(空的比较集意味着"还没加载",不是"全都没了")。dsh.session.topics.v2.bak;当主存储为空而备份有内容时,侧边栏顶部会出现一条
恢复条,一键还原。createTopicWithSessions),因为 store 实现可以不返回值(Redux 风格),
靠读返回值拿新 id 会静默失效。flex:1; min-height:0; flex-direction:column,内层列表 flex:1; overflow-y:auto。
不这么做,内容会被 overflow:hidden 的区域裁掉且无法滚动 —— 看起来像
"前端卡死",但 JS 线程其实完全健康。mousedown 做
"点外面关闭",结果连菜单内部的按下也被它先看到 —— 菜单在 click 触发前就被
卸载,所有菜单项变成摆设。stopPropagation 救不了(捕获早于冒泡),
必须显式判断 ref.current.contains(event.target)。parentId 是分叉血缘,不是子代理。 用它过滤会把每一个分叉出来的会话
藏起来 —— 用户点完"分叉"就找不到子会话了。唯一正确的过滤条件是
origin === 'subagent'。dsh plugin --profile web add D:/DSH/projects/dsh-session-topics
或按本 profile 已有的 link: 惯例,在 ~/.dsh/profiles/web/package.json 里加:
"dsh-session-topics": "link:D:/DSH/projects/dsh-session-topics"
并把它加进同一文件的 dsh.profile.bundles 数组。
改代码后只需刷新浏览器页面(客户端 bundle 每次加载都重新取),不必重启 dsh web。
node "<npm 全局目录>/node_modules/@deepseek-ai/dsh/lib/bin.js" --profile web --dump-config
node "<npm 全局目录>/node_modules/@deepseek-ai/dsh/lib/bin.js" plugin --profile web remove dsh-session-topics
本机
dsh.cmd/dsh.ps1在受限 shell 里会被执行策略拦掉(返回空输出,容易被误判成 "命令没生效"),直接node .../lib/bin.js更稳。
不需要浏览器,不需要装依赖:
node test/smoke.mjs
测试桩是故意做成迷你 React 的:函数组件会被真正调用、hook 状态跨渲染持久、 effect 尊重依赖数组,并且 store 的 action 故意不返回值。这三点都是踩过的坑 —— 早期版本的桩太松,把真 bug 放过去了。
覆盖:模块加载、slot 注册与 shadow 优先级、会话过滤(隐藏 subagent / 已归档 / 非当前空白会话)、按工作区建话题、空话题可见、真实 input→commit 重命名路径、 同工作区拖拽建话题、跨工作区拖拽被拒绝(含拖到标题上)、嵌套与层数上限、 删除父话题连带子树、点击打开会话、滚动契约 CSS、官方同款 14px 字号。
以及一组**"接管不能丢功能"**的回归:会话右键菜单存在且含重命名/分叉/归档、
重命名走内联编辑并提交到 host RPC、分叉与归档路由正确、工作区菜单含
新建话题/重命名/删除、拖到「未归话题」能把会话移出话题、跨工作区移出被拒绝、
retain() 会清掉已死话题/工作区的展开态 key。
还有一条菜单可点击性回归:菜单内部按下不能把它关掉(否则 click 永远不触发),
菜单项点击必须真的执行动作,菜单外部按下必须关闭。
package.json "." → 空壳 host;"./client" → 浏览器半边
cordis.patch.yml 插入 host 行(id: session-topics)
lib/index.js host 空壳,故意不 inject 任何服务
lib/client.js 全部功能(__ModuleLoader__ 包装的浏览器产物)
test/smoke.mjs 离线冒烟测试
MAX_TOPIC_DEPTH 可放开,但侧边栏宽度会先撑不住)。sidebar.workspaces 槽位(priority: -1 压过官方的 0)。这是加会话级
分组的前提 —— 会话行本来就是官方那个渲染器画的。官方将来更新该槽位时需人工跟进。sidebar.workspaces.directoryFlow
已由官方条目声明,本插件故意不重复声明)。需要新增工作区时用官方入口或 dsh CLI。| 我想… | 怎么做 |
|---|---|
| 装 | 插件市场 → 安装;或 dsh plugin --profile web add D:/DSH/projects/dsh-session-topics |
| 临时关掉 | 插件市场 → 已安装 → 本插件卡片的「启用中」开关关掉 → 按提示重启 DeepSeek Harness |
| 再打开 | 同一开关打开 → 重启 |
| 恢复出厂(重置分组) | 卡片上的「恢复」按钮 |
| 卸载 | 卡片上的「卸载」;或 dsh plugin --profile web remove dsh-session-topics |
| 升级本地代码 | 直接改 D:/DSH/projects/dsh-session-topics 里的文件 → 刷新浏览器页面即可(客户端 bundle 每次加载都重新取)。只有改了 package.json / cordis.patch.yml 才需要重启 dsh web |
| 确认它在跑 | 侧边栏是「话题」分组而不是官方的纯工作区分组 = 它在跑;node test/smoke.mjs 应输出 RESULT: all checks passed |
开关与「恢复 / 卸载」是插件市场(
dshmarket)的能力:它改的是 profile 的 patch 层, 所以开关之后要按提示重启才生效。本插件自身不实现开关。
| 症状 | 原因 | 处理 |
|---|---|---|
| 侧边栏变回官方按工作区分组 | 插件被关掉 / 没加载(priority: -1 没生效) | 看插件市场里是否「启用中」;重启 dsh web |
| 分组整个不见了 | 浏览器本地存储被清(换浏览器、清缓存) | 顶部若有恢复条点还原;否则查 localStorage["dsh.session.topics.v2.bak"] |
| 列表滚不动、像前端卡死 | 滚动契约被破坏(根节点的 flex 与列表 overflow) | 见 docs/HANDOFF.md §3.3 |
| 侧边栏冒出一批空对话 | 不是本插件的问题 | 见下方「已知问题」 |
| 右键菜单点不动 | 捕获阶段的监听器把菜单提前关掉了 | 见 docs/HANDOFF.md §3.7 |
| 冒烟测试不过 | 改了 UI / 过滤逻辑 | node test/smoke.mjs 看具体断言,改完必须全绿 |
blank 默认成 false
(dsh-api-session-controller 的 summarizeCold),于是"开起来就死"的空壳会话
在官方侧边栏和本插件里都会显示。根因、识别特征与三条缓解路径见
docs/HANDOFF.md §5.1。sidebar.workspaces 槽位演进时需要人工跟进(本插件顶替了官方浏览器)。.bak + 恢复条兜底)。main)
dsh plugin --profile web add github:WangXuexin24/dsh-session-topicslink: 安装,出现在插件市场「已安装」列表(带开关)lib/ 已随仓库提交(dsh 从 git 安装不跑构建)docs/market-submission.yml,随时可用;
另有「仓库创建满 1 天」的 CI 门槛。不收录不影响安装,只影响被搜到的概率。MIT