dsh-plugin-headless-sessions-panel
Verifieddsh-plugin-headless-sessions-panel · v1.0.1 · MIT · Web UI
侧栏「无头会话」入口:列出本机正在运行的无头 DSH 会话,并可导出/预览其转录
Install
dsh plugin add dsh-plugin-headless-sessions-panel Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
dsh-plugin-headless-sessions-panel
在 DSH 侧栏加一个「无头会话」入口:点开后列出本机正在运行的无头会话(cli.js headless … 拉起的那些)。
每条可以:
- 看 —— 点行,右侧把该会话导出成可读 Markdown 预览(只读快照,来自
dsh-session-forensics/out/); - 进 —— 会话在本 GUI 的会话目录里时显示「打开会话」,走
uiWorkspace.openSession(sessionId)把它放进主视图。不在目录里时不会给一个点了就报错的按钮,而是按状态给不同出路(见下表)。
| 会话状态 | 面板右侧 |
|---|---|
| 已归档(优先判断,哪怕它同时也在目录里) | 「取消归档并打开」:workspaces.unarchiveSession(id) → 刷新目录 → openSession(id) |
| 在客户端目录里、未归档 | 「打开会话」 |
| 目录里还没有(外部进程刚建) | 「暂时只读」+ 「刷新目录并打开」:先让客户端重拉 session/list,宿主认识了就打开 |
2026-10-30 修的归档 bug:归档判断必须排在最前。宿主
session/list不过滤归档会话 (只有 GUI 的分组界面才隐藏它们),所以已归档的会话几乎总是同时"known"。旧代码先判断known, 于是归档会话被渲染成普通的「打开会话」:点下去看起来打开了,但 id 仍留在archivedSessionIds里, 客户端ui-workspace的clearArchivedCurrent()(dsh-client-ui-workspace/lib/client.js)在下一个 Workspace 帧到达时把主视图清掉 —— 表现就是「打开成功,几十秒后自己消失」。 本机实测:156 条归档会话里 77 条同时还在工作区sessionIds里,这 77 条全都会踩到。 修法:classifySession先判归档;openSession增加兜底重定向(除afterUnarchive重试外一律先取消归档)。
行标题(2026-10-30):无头会话的文件名全是 session-<uuid>,只看 id 无法分辨。扫描器现在
总是解析首部,输出 title / titleSource / firstText / displayTitle:
displayTitle= 真标题(用户改名、模型生成);DSH 自己那个source.kind === 'fallback'的标题 是按字节截断的前缀(fallbackSessionTitle,中文没空格等于硬截,例如「用无头会话插件打开一个已归」), 所以遇到 fallback 就改用用户首句话。两者都没有才回落到短 id。- 首句取自第一条
user/message(source.kind === 'user'),跳过注入的<system-reminder>/Current runtime context之类上下文。
归档集合读的是 workspaces.model.archivedSessionIds(不是服务本身的属性)——2026-10-08 修过一次:
读错属性会恒为 undefined,导致归档会话也被显示成"只读"且没有取消归档入口。
目录里为什么"还没有"(2026-10-08 实测):宿主在启动时扫一遍会话目录建立目录,之后别的进程
(21:30 的日报无头任务)新建的会话,要等客户端重新拉一次 remote.session.list({}) 才进目录 ——
这是暂时状态,不是"永久打不开"。实测 21:32:17 全机 73 条会话里只有 21:30:06 那条无头会话
session-989ade16… 不在目录里;21:36:04 它已进目录(sessionCount: 74,且排在列表第一位)。
面板因此会在轮询时自动重拉(最多 15 秒一次),这种行会自己变成可点的「打开会话」。
归档集合读的是 workspaces.model.archivedSessionIds(不是服务本身的属性)——2026-10-08 修过一次:
读错属性会恒为 undefined,导致归档会话也被显示成"只读"且没有取消归档入口。
为什么无头会话打不开(2026-10-07 实测结论)
DSH 客户端只肯 retain 自己目录里的会话,别人进程创建的会话会被拒绝:
| 证据 | 位置 |
|---|---|
throw new Error('sessions.retain: unknown session ' + id):id 既不在活着的会话里、也不在 summaries 里就拒绝 |
@deepseek-ai/dsh-api-session-controller/lib/client.js:2385 |
客户端的 summaries 由 remote.session.list({}) 全量刷新而来 |
同上 :2603 |
| 宿主的目录来自 workspace registry + 本进程活着的会话 | @deepseek-ai/dsh-api-session-controller/lib/index.js(workspaceRegistry.list() / ctx.sessions.list()) |
磁盘对照:~/.dsh/storages/workspace.json 里有本 GUI 会话 session-a74e300b…,没有无头会话 session-82101393…、session-8615d74a… |
本机实测 |
session/openWorkspacePath 只是让桌面打开路径(资源管理器 / 外部程序),不会把会话收编进目录 |
…/dsh-api-session-controller/lib/index.js:3031 |
根因:cli.js headless 是另一个进程(通常还是另一个 profile:~/.dsh/profiles/headless)拉起的,
它的会话从来不会写进本 profile 的 workspace registry,所以本 GUI 既列不出来、也 retain 不了。
想要"能进",得让那个会话进入同一个 profile 的 registry(例如改掉无头任务的启动方式,让它走桌面 profile 的 会话控制器),或者等 DSH 提供"导入/接管外部会话"的 API——现在没有。
它由两半组成
| 半 | 文件 | 做什么 |
|---|---|---|
| 宿主半 | lib/host.mjs |
在 DSH 自己的 HTTP 服务上注册两条路由;调用同工作区的取证脚本 dsh-session-forensics/find-headless.mjs(现成的、已核对的工具) |
| 浏览器半 | lib/client.js |
注册 sidebar.panellist(侧栏行)与 main(面板);轮询 status、渲染列表与转录;「打开会话」调 ctx.get('uiWorkspace').openSession(服务不存在时自动退化为只读) |
路由:
GET /headless-sessions/status[?all=1] 正在运行的无头会话 + 最近 5 分钟内活动过的会话(?all=1 = 按更新时间前 50 条)
GET /headless-sessions/dump?id=<id前缀> 把某个会话导出成 Markdown(取自 dsh-session-forensics/out/)
浏览器半用同源 fetch 取这两条路由——这正是官方客户端插件用的通道,且只有已通过会话 HTTP 鉴权的页面能读。
「正在运行」是怎么判定的
判定逻辑不在本插件里,而在 dsh-session-forensics/find-headless.mjs(其 README 有完整说明):
- 会话文件
~/.dsh/sessions/<工作区编码>/session-<uuid>/session.v4.jsonl.zstd是多帧追加 zstd; - 无头进程 = 命令行匹配
cli\.js"?\s+headless\b的DeepSeek Harness.exe; - 进程启动时间与会话首行
createdAt对齐(20s 容差)后,running: true才是真·正在跑的无头会话。
宿主半优先用捆绑 node 跑脚本(脚本需要 zlib.zstd*,Electron 宿主进程不保证有);
可用 DSH_HEADLESS_FORENSICS 覆盖脚本路径、DSH_BUNDLED_NODE 覆盖解释器。
诊断戳(排障用)
宿主半会在 ~/.dsh/headless-sessions/ 写两个小文件:
host-stamp.json—— 每次宿主半加载时写一次(含hostNode/electron/ 脚本路径),用来确认"新代码有没有真的加载";last-request.json—— 路由被访问时写(1 秒节流),用来确认"页面有没有真的连上宿主"。
安装(本机 desktop profile)
# 用插件管理器装(等价于 pnpm add + 选入 dsh.profile.bundles)
# plugin_manager install_bundle target=dsh-plugin-headless-sessions-panel
装完检查:
Get-Content "$env:USERPROFILE\.dsh\profiles\desktop\package.json" -Raw # dependencies + dsh.profile.bundles
Get-Content "$env:USERPROFILE\.dsh\headless-sessions\host-stamp.json" -Raw
本机踩到的两条加载器限制(2026-10-07 实测)
- profile 安装的包,子路径 specifier 解析不了:
name: 'dsh-plugin-headless-sessions-panel/host'→failed to import(同一个 specifier 用普通 node 能解析;官方包能用的/startup、/tools等子路径在这里不行)。 → 所以宿主代码挂在package.json#main上,入口只用裸包名。 - 同一 specifier 的模块 URL 不会重跑:改完
lib/host.mjs后,禁用/启用 bundle、甚至pnpm remove+ 重装, 还是加载旧模块(package.json解析结果与 ESM 模块都在进程内缓存)。 → 改宿主半要么重启 DSH,要么换一个新的包名(本插件名字里带-panel就是这个原因)。 客户端半不受影响:它走 HTTP 里的 bundle 版本号,改完自动热更新。
测试
# 零依赖:注册、轮询、列表排序、转录预览、错误分支、Markdown 解析
node test/smoke.mjs
package.json 只声明 dsh.bundle 与 dsh.client,没有依赖;lib/index.mjs 是历史兼容 shim。