dsh-revive — 会话回放、复刻与录制
DSH 会话视图切换器里新增**「回放」**视图(与「对话」「轨迹」并列):
- 流式回放动效:⏮ ◀ ▶ ⏭ 游标控制 + 自动播放,内容按字符逐个"打"出来(带光标、视口自动跟随实时边缘),而不是整段 append;
- 一键复刻:把当前会话的全部原始用户请求按顺序重放到一个新会话(走官方
uiWorkspace.connectWorkspace+session.prompt),工具由新会话重新执行——真正的"重跑"; - 会话输出录屏:把回放画面(会话内容本身)逐帧绘制到 1280×720 canvas,用
MediaRecorder录成.webm(VP9),可自动下载;不依赖窗口/屏幕录制权限。
回放动效(0.1.3)
代价预算时钟(分类变速):一轮内所有文本被拍平成
segments(用户/思考/正文/工具调用/工具输出),共享一个「代价预算」游标;每类内容每个字符消耗的代价不同,于是同一时钟下快慢不同:内容 权重(代价/字符) 效果 用户输入 user/steer1.00 基准阅读速度(60cps ≈ 每秒 60 字) 助手正文 assistant1.00 基准阅读速度 思考 reasoning0.06 ≈16 倍速刷过 工具调用 toolCall0.04 ≈25 倍速 工具输出 toolResult0.12 ≈8 倍速 输入后停顿:用户/steer 内容播完后,时钟在边界上冻结 1 秒(期间显示「⋯ 思考中」),再开始出思考/正文——模拟真实的"发出去后等首字",避免输入一结束就立刻刷屏;
skim 封顶:思考/工具类内容即使超长也只占最多 3.5 秒/段(权重按当前速度自动抬高),避免一轮几万字的思考把观众晾在那里;而输入与正文永远保持完整阅读节奏。
视口跟随:内容不再撑高容器,而是沿用宿主的会话滚动容器;流式期间自动
scrollTop = scrollHeight,用户手动上滚超过 64px 即暂停跟随,滚回底部自动恢复。速度档:慢 25 / 标准 60 / 快 160 / 极速 400 字符/秒(localStorage 记忆)。
默认可见:打开视图即完整显示当前轮;按 ▶ 播放才从该轮开头重新流式播放并跨轮连续推进。
录制(0.1.2)
- 画布按「片段预排版」(换行切片保留字符偏移),每帧只画已揭示部分,实时边缘随内容下滚,底部有渐隐提示仍在输出;
- 时长档 30 / 60 / 120 秒 / 不限:选定时会自动抬高揭示速率把整段会话塞进目标时长(不限时按 240 秒硬上限加速),避免长会话录出十几分钟;
- 录制结束时自动生成 blob 并触发下载,按钮旁保留「⬇ 下载视频」可再次保存;录制中传输控件与两个下拉均锁定,避免画面被打断;
- 依赖
canvas.captureStream()+MediaRecorder(桌面版 rc.3 实测可用,video/webm;codecs=vp9)。
形态
纯 client 插件,但必须以 bundle 形式声明(见「安装」——这是 0.1.1 的关键修复):
- 无 Node 路由、无文件写入,
lib/index.js只有空apply(),仅作为 loader 行的挂载点; - 会话视图注册:
slots.inject("conversation.view", () => slots.register(def, Component))(与官方 ui-chat / ui-trajectory 同款); - 数据:
uiConversation.binding(sessionId).target("chat")投影 +session.loadOlder()补全历史; - 复刻:
uiWorkspace.connectWorkspace(workspaceId)+sessions.binding(sid).session.prompt([...], "queue")。
安装
profile 的 package.json:
"dependencies": { "dsh-revive": "link:/path/to/dsh-revive" },
"dsh": { "profile": { "bundles": [ /* ... */, "dsh-revive" ] } }
然后 pnpm install(link 依赖会建符号链接)+ 完全重启应用(boot 时才会重新组装 loader 行与 client 模块)。
也可以直接从仓库安装:
git clone https://github.com/zxsimple/dsh-revive.git
# 然后把上面的 link: 指向 clone 下来的目录
关键点:纯 client 插件也必须声明
dsh.bundle.patch并出现在dsh.profile.bundles里,否则宿主不会为它生成 loader 行(详见下一节)。
平台支持
纯 client 插件,运行在 DSH 的 Web 前端里,不含任何与操作系统相关的代码:没有 fs / path / child_process,没有 process / __dirname,没有绝对路径拼接,也没有构建脚本与原生依赖(pnpm install 对它只是建一个符号链接)。所以平台跟随 DSH 的 Web 客户端。
| 平台 | 状态 |
|---|---|
| macOS(DSH 桌面版 0.2.1-rc.3) | 已实测:视图注册、流式变速、输入后停顿、canvas 录制全部验证通过 |
| Windows | 预期可用(未实测)。DSH 随包提供 node.cmd 与 command-path.ps1,插件市场也有完整的 win32 分支(link: 依赖走 junction、npm.cmd / pnpm.cmd);插件侧没有需要移植的部分 |
| Linux | 同理,未实测 |
用到的浏览器能力都是 Chromium 内置的,Windows 上的 Electron / Chromium 同样具备:
MediaRecorder+canvas.captureStream()—— 录制走 VP9/VP8 WebM,Windows 的 Chromium 支持;产物用系统播放器或浏览器都能播;localStorage(速度与时长记忆)、navigator.language(中英文案);- 录制画布的字体栈包含
Microsoft YaHei,中文在 Windows 上不会掉字; - 滚动容器靠
getComputedStyle向上查找,不依赖任何平台布局常量。
Windows 上唯一的注意点:link: 依赖建的是 junction,如果手改过 profile 的 package.json,重启前先确认 node_modules/dsh-revive 指向正确。
如果你在 Windows 上跑出了问题(比如录制拿不到 video/webm,只剩 video/mp4),把控制台报错贴到 issue 里即可。
为什么必须是 bundle:0.1.0 的「没有按钮」根因
- package.json 缺
dsh.bundle.patch。插件目录里虽然有cordis.patch.yml,但没有声明它,宿主就永远不会把这条 insert 行并入 loader 树 → 没有 loader 行 → 没有 Node 半边 fiber →plugins/??里根本没有dsh-revive/client.js→ 视图切换器自然没有「回放」。dshmarket 自己的校验也会报同一句话:bundle declares no dsh.bundle.patch — the profile will fail to boot。 - 纯 client 插件(
dsh.client但无dsh.bundle)在 market 里只能热挂载一个 shim,重启即失效——所以它曾经"装上了"却重启后消失。 - 另外两处真实的运行时缺陷(0.1.0 即使加载成功也点不出内容):
require("react/jsx-runtime")返回的是命名空间对象,不能直接当函数调用(应取.jsx/.jsxs);- rc.3 的
chat快照不是{timeline, byId},而是{ order: string[], nodes: { get(key) }, timeline: { turnOrder, turns: Map } },节点形如{ key, kind, location, data };用户文本在data.content的分片里,助手文本在data.blocks里(kind:"text" | "reasoning" | "tool-call")。
兼容性(对照 DSH 0.2.1-rc.3 桌面版产物核对)
slots.register({name:"conversation.view",...}, Component)✓(ui-chat 的 id=chat order=0、ui-trajectory 的 id=trajectory order=10 同款)slots.entries("conversation.view")会收集所有注册项并渲染成切换器标签(label经resolveSlotLabel)✓uiConversation.binding(source)接受 sessionId 或 session binding ✓;.target("chat")→{ getSnapshot, subscribe }✓ctx.locale.register(ns, { zh, en })✓(值可以是函数,服务端不校验字符串)session.loadOlder()✓uiWorkspace.connectWorkspace(workspaceId)→Promise<SessionId>✓(服务名uiWorkspace)session.prompt([{type:"text",text}], "queue")→{ ok }✓- 运行期 cordis 注入用服务名:
["slots","sessions","uiSession","uiConversation","locale"](与 ui-trajectory 一致);workspaces/uiWorkspace用ctx.get(...)兜底读取。
No comments yet. Be the first to write one.