@240xu/dsh-session-lazy-view
只读会话惰性查看器:仅 stat 列出会话,再只解压尾部 Zstandard 帧查看最近事件;独立 /lazyview 面板支持搜索、统计和导出。需 Node.js 24+。
安装
dsh plugin --profile web add @240xu/dsh-session-lazy-view
沙箱测试
这个插件还没测过。提交测试 →
README
@240xu/dsh-session-lazy-view
怎么打开:安装后访问 http://<dsh-web-地址>/lazyview 即是独立面板页(无需找侧边栏入口);
DSH 宿主侧边栏没有本插件入口是有意设计(纯只读工具,不污染主界面)。
装了 dsh-devkit 的话,Ctrl+K 命令面板里有「打开」入口(devkit 侧接线)。
DSH web 插件:会话惰性查看器(纯只读)。列出 ~/.dsh/sessions 下全部会话(只 stat 不解压),
并可只解压任意会话文件的最后 1–2 个 zstd 帧快速查看最近发生的事件,不碰整份文件。
不做删除、不做归档、不是 Trajectory 替代品;对 ~/.dsh/sessions 零写入。
原理
session.v3.jsonl.zstd 是连续多帧的独立 zstd 帧(魔数 0x28 B5 2F FD):
帧 0 恰好一行 session header JSON;之后每帧一批 newline-delimited JSON 事件,
最后一帧结束于 EOF。因此从文件尾部读一个窗口、扫帧魔数、只解压候选帧,
即可拿到最近事件,成本与文件总大小无关(旧格式 session.jsonl.zstd
为单帧,等效于解压整份,但同样走这条路径)。
安装
零 npm 依赖(只用 node:fs / node:zlib / node:path / node:url;
@deepseek-ai/schemastery 由 DSH profile 树自带,无需安装)。
# 方式 A(推荐):DSH 官方插件命令(内部走 pnpm,正确登记 lockfile 与 bundles) dsh plugin --profile web add @240xu/dsh-session-lazy-view # 方式 B:npm pack 直解(profile 是 pnpm workspace,不要在 profile 内直接 npm i —— # workspace 根的 link:/ 依赖会让 npm 报 EUNSUPPORTEDPROTOCOL) cd ~/.dsh/profiles/web/node_modules npm pack @240xu/dsh-session-lazy-view mkdir -p @240xu/dsh-session-lazy-view tar -xzf 240xu-dsh-session-lazy-view-*.tgz -C @240xu/dsh-session-lazy-view --strip-components=1 # 然后把 "@240xu/dsh-session-lazy-view" 加进 profile package.json 的 # dsh.profile.bundles 列表(lockfile/package-map 不一致时 pnpm install 会补齐) # 方式 C:从源码拷贝(开发) git clone https://github.com/240xu/dsh-session-lazy-view && cp -r dsh-session-lazy-view ~/.dsh/profiles/web/node_modules/
然后在 DSH web profile 的 cordis.patch.yml 里挂载(写法与
dsh-archived-sessions 完全一致)。若该文件已存在其它 - insert: 条目,
把下面 - id: 两行并入现有列表即可:
- insert:
- id: dsh-session-lazy-view
name: '@240xu/dsh-session-lazy-view'
重启 DSH web 实例后生效。
端点
前缀 /lazyview(受与 archived-sessions 相同的 loopback / same-origin 信任门保护):
端点 · 说明
GET /lazyview · HTML 面板:按项目分组列出会话,>10MB 标红,每行 tail 按钮展开最近 2 帧,支持“上一页”
GET /lazyview/api/list · JSON:{root, projects:[{project, sessions:[{sessionId, path, artifact, format, bytes, mtime, big}]}]},纯 stat
GET /lazyview/api/tail?path=<相对路径>&frames=2&skip=0 · JSON:从尾部解压 frames 帧;skip 为翻页偏移(跳过最新 skip 帧后再取 frames 帧)。帧内事件已文本化(role + 截断到 2000 字符的 text / tool-call / tool-result)
GET /lazyview/api/search?path=<相对路径>&q=<关键词>&max=20 · 在单个会话内全文检索;逐帧解压并在帧间让出事件循环,客户端断开即中止
GET /lazyview/api/stats?path=<相对路径>&fast=1 · 会话统计;fast=1 只数帧魔数和字节,默认解压全部帧统计事件类型
GET /lazyview/api/export?path=<相对路径>&frames=2 · 把最近 N 帧导出为 Markdown 下载;内存生成,不写会话目录
path 必须是 /api/list 返回的 path(相对 sessions 根,且必须以
session.v3.jsonl.zstd 或 session.jsonl.zstd 结尾);路径逃逸检查在服务端。
解析失败(坏帧、旧格式、窗口截断等)一律返回
{ok:false, error:{code, message}} 结构化错误,不会 500 崩面板。
已知限制
- 旧格式
session.jsonl.zstd单帧等于整份文件,tail 读取没有惰性收益。 - 尾部窗口默认 1MB,最多增长到 64MB;极端情况下(单帧超过 64MB)返回
truncatedScan: true 且可能少帧,不会死循环。
- 帧魔数理论上可能出现在压缩数据内部造成假候选;假候选解压失败后以
per-frame error 呈现,不影响其余帧。
- 事件文本化只覆盖已验证的形状(user/assistant/system message、tool call/result),
其余事件类型输出紧凑 JSON 摘要。
- 未做缓存:每次 tail 请求都重新读窗口,对调试场景足够。
离线验证
./verify.sh
用 node 直接 import ./lib/frames.js,对 ~/.dsh/sessions 下任一
.zstd 会话执行“读最后 2 帧”,打印事件条数与首条 role。
v0.2.0 新增(端点细节)
端点 · 说明
GET /lazyview/api/search?path=&q=&max=20 · 会话内全文搜索:流式逐帧解压做大小写不敏感子串匹配,命中返回 {frameIndex, seq, type, role, snippet}(命中点前后各 60 字符窗口)。达 max 截断;默认最多扫 500 帧,未扫完返回 partial:true。每帧之间 setImmediate 让出事件循环,客户端断开(AbortSignal)即中止扫描
GET /lazyview/api/stats?path=[&fast=1] · 会话统计:帧数、总事件数、按 type 分组计数、首/末事件时间、文件字节数、session header。fast=1 只流式扫描帧魔数(不解压、内存有界)给帧数/字节数
GET /lazyview/api/export?path=&frames=N · 把最近 N 帧(≤20)导出为 Markdown 文本下载(Content-Disposition: attachment)。文本在内存中生成直接响应,不写任何文件
面板集成:每个会话行新增「搜索」按钮(展开输入框 + 命中结果列表,命中
标注帧号/seq/type/片段,新搜索自动取消上一次);打开会话顶部新增「统计」
小节(先展示 fast 帧数/字节,按钮展开全量解压统计)与「导出 md」下载链接。
测试:npm test(即 node --test "test/*.test.js";零依赖,用 node:zlib 自造多帧 fixture,
覆盖搜索命中/截断/abort、stats 分组、export 渲染)。
性能取舍说明:
- 解压是同步的(
zstdDecompressSync),全量扫描在帧与帧之间用
setImmediate 让出事件循环(参照 dsh-src ZSTD_DECODE_YIELD_INTERVAL_MS
思路),单个大文件的搜索/统计不会长时间阻塞 web server;但**一次请求
内整份文件会被读进内存**(read-only,读完即释放),超大文件请优先
?fast=1 或改用 tail。
- 搜索只覆盖事件文本化后的可见文本(
describeEvent,单条截断到 2000
字符);无法文本化的行仍按原文参与匹配。
- 全量统计对大文件是秒级操作(每帧解压一次);面板默认只拉 fast 统计,
全量按需展开。
v0.2.1 评审修复
- 可发现性(P0):README 与面板页顶部均加显著说明——面板是
/lazyview
独立页,宿主侧边栏无入口是有意设计(纯只读工具);面板页新增
「复制面板链接」按钮;README 注明装了 dsh-devkit 可用 Ctrl+K 打开。
- S1(P1):360px 表格溢出修复——session id 列截断省略(
title悬停看全),
mtime 列在 ≤480px 隐藏(hide-sm)。
- S2(P2):全部按钮触控目标提升到 ≥44×44px。
- S3(P2):统计小节预留固定高度占位(fast→full 替换前锁定高度),防 CLS。
- S5(P3):
#status加role="status" aria-live="polite",读屏可闻。
v0.3.0 Timeline 视图
按 turn 分组的正序事件时间线(vscode Timeline 式阅读投影),设计文档见
timeline-design.md:
- 会话打开视图顶部新增「时间线」按钮,与帧流互斥切换;数据复用
/api/tail 翻页到文件头后按 seq 正序拼接(无需新端点)。
- 分组规则:有
turn/start事件时它开新组;历史格式以user/message
为组边界。组默认折叠一行摘要,展开显示全部事件;折叠状态存内存。
- Go to Message 深链:
/lazyview?session=<id>&seq=<n>自动打开会话、
进时间线、展开目标组并高亮首个 seq≥n 的事件。
- message-ops 遮蔽标注:事件自带
surfaceOp(op:"replace")时,被替换
区间内的事件在时间线上渲染为半透明 +「已遮蔽」徽标;纯本地计算,
不依赖 message-ops 端点。只读铁律不变。
- 纯函数(深链解析 / 遮蔽计算 / 分组)抽到
lib/timeline.js,一份两用:
ESM 供测试,服务端剥 export 内联进面板。
v0.3.2 v4 会话支持
- 用户实测发现:DSH 新会话写
session.v4.jsonl.zstd(帧 0 header
version:4),面板此前只探测 v3/legacy,新会话全部不可见。
- 修复:artifact 名单统一抽到
lib/artifact.js(session.vN.jsonl.zstd
形状正则),目录探测与 ?path= 校验共用;未来新版本号自动纳入,
frames.js 无版本硬校验(header.version 原样透传),无需再改。
- 面板 format 列区分
v4-multiframe/v3-multiframe/legacy-single-frame。
v0.3.3 可访问性补齐
- 时间线分组折叠头(.ghead)从纯点击 div 改为键盘可达:
role="button" tabindex="0"+ Enter/Space 触发 +aria-expanded与折叠 Map 同步。
0.3.4 · Bug 猎场修复
- [P1] 深链白屏:
?session=访问在sessions.map回调外引用回调参数s→
ReferenceError 被 catch 吞掉,面板一个会话都不渲染(0.3.0–0.3.3 全中)。改为在
project 的 sessions 内匹配 + 表格行定位打开按钮。
- [P1] 遮蔽标注失效:applySurfaceOps 只认
fromSeq/toSeq(message-ops 从没写过
这种拼写)——真实数据是 start/end(新引擎)与 startSeq/endSeq(leg
…(truncated)
以上内容来自 npm 上的 README,我们只做格式化,不改内容。
版本
npm 上共发布:12,最后更新 2026-10-21