← 返回列表
未验证
一键把重要对话钉在侧栏分组顶部
尚未跑自动兼容性验证,可查看页面内的依赖与入口分析。 · 最近上游提交 2026/9/16 · 已提供中文文档
DSH「置顶对话」插件:会话头部与会话行内一键 📌,把重要对话钉在侧栏分组顶部。宿主端只存事实(原子写 JSON + 信任栅栏内的 HTTP 接口 + 配套 CLI),排序由客户端在 DOM 层完成——因为侧栏顺序由浏览器本地排序表优先决定,宿主端挪顺序在界面上无效。零运行时依赖、能力探测与熔断,面向跨版本升级设计。
综合分
30
GitHub 分
30
用户评分
—
★ Stars
0
周下载量
—
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add BPTumbleweed/dsh-pin-session该插件未发布到 npm,走 GitHub 源安装(pnpm 若拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑)
数据截至 2026/9/16(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动
README
dsh-pin-session —— DSH「置顶对话」插件
把重要的对话钉在 DSH 侧栏的最上面。会话头部一个 📌,侧栏会话行 hover 也一个 📌,点一下就行。
侧栏 会话头部
┌──────────────────────────┐ ┌───────────────────────────────┐
│ ▼ my-project │ │ 关于笔记发布流程 📌 ⋯ │
│ 📌 部署排查(置顶) │ └───────────────────────────────┘
│ 📌 课表整理(置顶) │ ↑ 点它置顶/取消置顶
│ nginx 配置查看 │
│ 随手一问 │ 侧栏会话行 hover
└──────────────────────────┘ ┌───────────────────────────────┐
│ 随手一问 刚刚 📌 ⋯ │
└───────────────────────────────┘
- 零运行时依赖:宿主端只用 node:fs/node:path,客户端只 require("react")。
- 不加模型上下文:置顶是人的界面行为,不产生 token、不影响 prompt cache。
- 坏了不影响 DSH:任何一步失败只丢本插件自己的效果,侧栏本体不受影响。
为什么是"宿主存事实 + 客户端排序"这个结构
动手前把 DSH 前端翻了一遍,两个事实决定了整个设计:
1. 宿主端挪顺序在界面上看不出来。
侧栏渲染顺序 = reconciledSessionOrder(宿主 sessionIds, 浏览器本地排序表),
本地表优先;而且默认 orderBy="updated" 还会按活跃度把刚动过的会话提到前面。
也就是说,就算在宿主端老老实实调 insertSessionBefore 把会话挪到第一,
界面上它还是待在原地。→ 排序只能放在客户端。
2. 侧栏会话行上没有 session id。
行是 div[role="treeitem"][aria-selected],里面只有 状态点 / 标题 / 时间 / 菜单,
没有任何 id 属性。→ id 得自己想办法拿(见下)。
所以本插件的分工是:
| 层 | 负责 | 失效后果 |
| --- | --- | --- |
| 宿主端 lib/index.js、store.js、routes.js | 存"哪些会话被置顶、顺序如何" + 自己的 HTTP 接口 | 置顶数据没了(有原子写与备份),界面照常 |
| 客户端 lib/client.js | 头部按钮(官方槽位)、行内按钮、把置顶行搬到分组顶部 + 📌 徽标 | 只是"钉不住",侧栏本体完全不受影响 |
session id 怎么拿(两级,都拿不到就什么都不做,绝不猜):
1. 读 React fiber:从行这个 DOM 节点上的 __reactFiber$… 往上走几层,取 memoizedProps.node.id。精确、无副作用。
2. 回退到标题反查:用 /api/list 返回的标题建索引,只在标题唯一时才认。
安装
前提:pnpm 在 PATH 上(DSH 自带的那个即可),核心版本 0.1.5-rc.1。
本地开发装(link):把 换成你 clone/开发的位置
dsh plugin --profile web add link:/path/to/dsh-pin-session
或从 github / npm 装
dsh plugin --profile web add github:BPTumbleweed/dsh-pin-session
dsh plugin --profile web add dsh-pin-session
装完要重启对应的 dsh-web- 服务才生效。注意:如果你(Agent)自己就跑在那个服务里,
直接 systemctl restart 会杀掉当前回合 —— 用它之外的方式重启(例如脱离会话 cgroup 的
systemd 瞬时单元)。重启前先 free -m 确认 available 够用。
改客户端代码不用重启:DSH 的宿主端 HMR 会监听 link: 插件的文件并重新哈希客户端 bundle,
刷新页面即可拿到新版本(实测:改完 lib/client.js 后 index.html 上该模块的 rev 立刻变了)。
只有动宿主端(lib/index.js 等)才需要重启。
停用 / 回滚
临时停用:给 profile patch 里这一项加 disabled: true,重启
彻底卸载:
dsh plugin --profile web remove dsh-pin-session
浏览器侧还有两个免重启的开关(写在 localStorage 里,刷新页面生效):
| 键 | 值 | 作用 |
| --- | --- | --- |
| dsh-pin:disable | "1" | 整个客户端效果关掉(含按钮与徽标) |
| dsh-pin:prefix | "/dsh-pin" | 改接口前缀(宿主端改了 routePrefix 时用) |
配置(profile patch 覆盖)
- id: dsh-pin-session
config:
storeRoot: /path/to/pin-data # 默认 $DSH_HOME/dsh-pin-session
routePrefix: /dsh-pin # 接口与面板前缀
maxPins: 100 # 置顶上限
breakerThreshold: 5 # 同一能力连续失败几次后熔断
allowUnfenced: false # 栅栏不可用时是否放行(默认 false = fail-closed)
数据文件
/pins.json,形如:
{
"schema": 1,
"revision": 5,
"pins": [
{ "sessionId": "session-…", "title": "部署排查", "workspaceId": "", "pinnedAt": 1789527147689 }
]
}
- 数组顺序就是显示顺序,pins[0] 在最上面;新置顶插到最前。
- 原子写:先写 pins.json.tmp- 再 rename,断电最多丢最后一次改动。
- 损坏自愈:读不动就把坏文件改名成 pins.json.corrupt- 留证,然后以空集合继续。
HTTP 接口
全部走 DSH 的浏览器信任栅栏 connection.requestRejection,栅栏缺失时默认 fail-closed
(置顶数据带会话标题,不能裸奔)。
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | /dsh-pin/api/list | 置顶集合(客户端排序引擎的数据源) |
| GET | /dsh-pin/api/status | 自检状态(能力、计数、最近错误) |
| POST | /dsh-pin/api/pin | {"sessionId":"…","title":"…"} |
| POST | /dsh-pin/api/unpin | {"sessionId":"…"} |
| POST | /dsh-pin/api/toggle | {"sessionId":"…","title":"…"} |
| POST | /dsh-pin/api/reorder | {"order":["id1","id2"]} |
| GET | /dsh-pin/panel | 人看的管理页(纯服务端渲染,无前端依赖) |
命令行
不走 HTTP,直接读写同一份 pins.json;给没有浏览器的场合用(SSH、脚本、人工救援)。
node bin/pin.mjs list [--json]
node bin/pin.mjs pin [title]
node bin/pin.mjs unpin
node bin/pin.mjs toggle [title]
node bin/pin.mjs move >
node bin/pin.mjs clear
node bin/pin.mjs path
目录解析顺序:--store/--file > $DSH_PIN_STORE > $DSH_HOME/dsh-pin-session > ~/.dsh/dsh-pin-session
自测
npm test # = node test/selftest.mjs && node test/dom-engine.test.mjs
npm run contract # 对着真实安装的 DSH 包核对本插件依赖的结构
- test/selftest.mjs(36 项):语法、存储语义(幂等/上限/控制字符清洗)、持久化与坏文件自愈、
路由(含 fail-closed 栅栏)、真 node:http 端到端、客户端静态检查。
- test/dom-engine.test.mjs(16 项):用一套极小假 DOM 把 lib/client.js 真跑起来。假 DOM 刻意
复刻真实结构(每个会话行外面还有 HoverCard 的一层 包装、分组 section 里还有 project 行),
验排序算法、幂等(防 MutationObserver 自激循环)、行内按钮点击、焦点策略、多棵 role=tree 时不认错树、
id 取不到时不动 DOM。
- test/contract-check.mjs(12 项):DSH 升级后先跑这个。核对槽位声明、行结构、
rowActions/title 容器、role="tree"、HoverCard 包装层、以及"本地排序表优先 + 默认 orderBy=updated"
这个设计前提是否还成立;红了就说明客户端那几处非公开依赖需要重新适配。
假 DOM 测试抓到过两个真 bug:
① 重排循环原本拿"行快照"当锚点,搬动之后锚点错位,顺序会排错([s3,s1,s2,s4] → [s2,s3,s1,s4])。
现在用 live 数组模拟 insertBefore 语义。
② 每行外面还有一层 HoverCard 包装,按"父容器里 ≥2 行"分组时行行都只有 1 个 → 整轮跳过,
表现就是"徽标出来了但位置不动"。现在按"可搬运单元"(自动识别包装层)来排。
兼容性与失效姿态
DSH 升级后可能被打断的地方,按"最先坏"排序:
| 环节 | 依赖的东西 | 坏了会怎样 |
| --- | --- | --- |
| 头部按钮 | 官方 slot conversation.session.header.actions | 按钮消失,侧栏置顶照常 |
| 行内按钮 | 行内 class 含 rowActions 的容器 | 按钮消失,头部按钮照常 |
| id 识别 | React fiber 的 __reactFiber$ + memoizedProps.node.id | 退到标题反查;仍不行就整体不动作 |
| 行识别 | div[role="tree"] + [role="treeitem"][aria-selected] | 找不到列表 → 什么都不做 |
| 搬运单元 | mountUnitOf:从行往上走到"装着 ≥2 行或含分组表头"的那层 | 判据不合身时退化成搬行本身(多半仍可用),最坏是不动作 |
| 排序 | 只改 DOM 顺序,不碰 React 状态 | 下一轮扫描会补回来(React 重排后最多 ~140ms 归位) |
熔断:DOM 引擎连续出错 5 次就整体拆掉(移除按钮、徽标、样式)并在 console 留一条警告,
绝不半死不活地反复报错。接口连续失败 5 次就停轮询,手动点击仍可再试。
已知限制
- 只在浏览器里生效:置顶是客户端 DOM 排序,不动宿主端的会话顺序。
所以换个浏览器/清掉本地状态后,置顶集合还在(服务端),效果照旧 —— 但用别的 DSH 客户端
(比如移动端)看不到置顶效果。
- 只在同一个工作区分组内置顶:DSH 侧栏按工作区分组,置顶行被提到“它自己那个分组”的顶部。
未归入任何工作区的会话(Ungrouped 桶)同样适用。
- 搜索态不干预:搜索结果的顺序由查询决定,此时不动 DOM。
- 重命名时不动 DOM:焦点在会话行内时跳过这一轮(搬节点会让输入框失焦),焦点离开后自动补上。
- 标题可能缺失:从 CLI 置顶的会话若没带 title,列表里只显示 sessionId(不影响排序,
排序靠 fiber 拿的 id)。
装完后的手工验收清单
自动化测试覆盖不到真实浏览器,装好后按这 7 条过一遍(约 2 分钟)。
注意第 0 条:客户端文件改动是宿主端 HMR 直接生效的,但页面必须 Ctrl+Shift+R 硬刷新
(普通 F5 常常还在用缓存的旧模块),重启过服务的话也顺带换掉旧的 gate token。
0. 硬刷新页面。
1. 侧栏会话行 hover → 出现 📌 → 点击 → 该行跳到所在分组顶部,标题前出现 📌 徽标。
2. 进入该会话 → 头部出现 📌,且是“已置顶”配色 → 点一下 → 取消置顶,行回到原来位置附近。
3. 再钉 2 个,确认“后钉的在最上面”。
4. 刷新页面(F5)→ 置顶仍然生效。
5. 随便发一条消息让另一个会话活跃起来 → 置顶行没有被挤下去(默认 orderBy=updated 会提升活跃会话)。
6. 在会话里正常打字(composer 有焦点)→ 置顶行依然在顶部,没有被“卡住不排序”。
7. 在侧栏里改一个会话名(改名输入框有焦点时)→ 置顶行暂时不动是正常的,焦点离开后会自动归位。
出问题时看 /dsh-pin/panel 与 /dsh-pin/api/status,浏览器 console 搜 dsh-pin-session。
变更日志
0.1.1 — 2026-09-16
- 修:置顶徽标出来了但行的位置不动。根因是 DSH 用 HoverCard 给每个会话行套了一层
包装(行是该 span 的唯一子元素),于是“按父容器分组、容器内 ≥2 行才重排”的判据
永远只有 1 行 → 整轮跳过。现在改为先算出每行的可搬运单元(从行往上走到“装着 ≥2 行或
含分组表头”的那一层),搬包装层而不是行本身。徽标/按钮本来就走另一条路径,所以之前只坏在排序。
- 修:页面上有多棵 role="tree" 时(分组列表 / 扁平列表 / 搜索结果树,别的插件也可能有树),
改为优先挑真的装着会话行的那棵(先认 class=sessionRow,再退到 [role=treeitem][aria-selected])。
- 假 DOM 测试夹具改成复刻真实结构(HoverCard 包装层 + project 行),并加了“包装层必须还在、
行不能被拽出来”“无包装层时也要能排”“多棵树不认错” 3 组回归。
- 契约检查新增“会话行仍然经由 HoverCard 渲染”一条。
- 客户端改动由宿主端 HMR 直接生效,不需要重启服务。
0.1.0 — 2026-09-16
- 首个版本。
- 宿主端:置顶集合持久化(原子写 + 损坏自愈 + 数量上限)、/dsh-pin/api/* 全套接口、
信任栅栏内面板页、能力探测与失败熔断。
- 客户端:会话头部 📌(官方 slot)、会话行 hover 📌(注入行内 rowActions)、
置顶行提到分组顶部 + 标题 📌 徽标、React fiber 取 id(带回退)、焦点策略、整体熔断。
- CLI dsh-pin:list / pin / unpin / toggle / move / clear / path。
- 测试:selftest.mjs 36 项 + dom-engine.test.mjs 13 项(假 DOM 跑真客户端代码)
+ contract-check.mjs 11 项(对照 DSH 0.1.5-rc.1 真实包)。
许可
MIT © 2026 Kan Zheng(202309068@uibe.edu.cn)同作者(BPTumbleweed)的其他插件
扫码进群