为 DSH 会话补充可重放的消息编辑、重生成、重试与版本分支导航,历史保持不可变可回滚。
- 语言
- TypeScript
- 分支
- main
安装
$ dsh plugin --profile web add github:Moeblack/dsh-message-edit在终端中运行以上命令,通过 dsh CLI 安装此插件。可在右上角切换 Profile。 第一次用 dsh?看这篇新手教程
一句话定位
为 DeepSeek Harness 会话补一套可撤销的消息编辑、重生成、重试与分支版本导航能力。它把每次修改落成一个独立的会话版本,原会话不动不删,随时可以切回旧版。
核心能力
- 编辑已落定的用户正文、助手思考块、助手回复正文三类文本块
- 一键重生成最近一条已落定的助手回复,沿用原用户输入
- 在 Timeline 里选择任意历史回合重新执行(Retry)
- 支持两种级联策略:
truncate(默认,丢弃旧后续)和preserve(把后续用户输入依次带入新分支) - 会话标题栏提供撤销(←)与重做(→)按钮,按原子效果单位切换当前版本
- 新增 Timeline 标签页,展示完整分支树、操作时间、编辑前后内容、当前版本与可重试回合
技术实现
- 语言: TypeScript(ESM),构建产物为
index.mjs(Host)和client.js(Browser) - 关键依赖:
@deepseek-ai/dsh-agent、@deepseek-ai/dsh-session、@deepseek-ai/dsh-session-query、@deepseek-ai/dsh-workspace - 架构模式: Cordis 插件双端结构。Host 端注册 HTTP 路由
/message-edit提供时间线读取与编辑执行;Client 端通过slots注册conversation.view与conversation.session.header.actions两个槽位。事件溯源:每个分支追加message-edit/version事件,effect/inverse 配对存储,逆链由父版本自动组合 - 入口文件: Host 入口
src/index.ts(apply(ctx)注册 HTTP 路由),Client 入口src/client/index.ts(注册两个 UI 槽位)
适用场景
当你用 DSH 与模型长时间协作、对某条旧回复不满意、或想换个 prompt 试试同一处分支的不同走向时,这个插件让你不必从头开新会话,就能从历史任意回合分出新分支继续走。原会话始终保留,随时切回。适合需要反复打磨同一段对话结构、又不想丢失之前成果的重度用户。
前置依赖与兼容性
| 依赖 | 最低版本 | 说明 |
|---|---|---|
| DeepSeek Harness | 0.1.0-rc.6+ | 插件基于 @deepseek-ai/*@0.1.0-rc.6 类型构建,宿主需满足该版本契约 |
| Node.js | 未声明 | 仓库未在 engines 中声明 Node 版本要求 |
| 运行平台 | 跨平台 | 仓库未声明 os/cpu 限制;DSH web 平台本身跨 macOS/Windows/Linux |
| 原生模块 | 无 | 不依赖 node-pty、node:sqlite 等原生模块 |
安装方式
dsh plugin --profile web add github:Moeblack/dsh-message-edit
配置项
本插件无需额外配置。模型路由(provider、model、maxTokens)由插件从当前会话的请求头中自动读取,无需手动指定。
常见问题
Q: 编辑、重生成、重试三种操作分别会改写原会话吗?
A: 不会。插件从不原地改写 Session 事件,原会话作为 append-only 的不可变日志完整保留,每次操作都会从目标回合之前分支出一个新的会话版本。
Q: truncate 和 preserve 两种级联策略有什么差别?
A: truncate 只在分支后的新版本里重新执行目标输入,并丢弃旧的后续内容;preserve 会把目标之后尚未执行的用户输入也依次带入新分支,按顺序重新跑完后续回合的工具链。
Q: 撤销和重做会真的删除事件吗?
A: 不会。每次操作都会追加一个 effect/inverse 配对的事件,撤销只是沿着逆链切换到仍然存在的旧版本,被切走的分支依然保留在版本树里,可以再次切回。
Q: 支持编辑哪些内容?
A: 只支持已落定的文本块——用户消息正文、助手思考块、助手回复正文;图片、工具调用、附件等其它内容块不在可编辑范围内。
Q: 编辑一条消息会影响工作区文件或产物吗?
A: 不会。插件明确只动会话事件和派生 Agent,不联动恢复或修改工作区文件、命令的外部效果与既有产物。
Q: Timeline 标签页的位置在哪?
A: 注册在会话视图的 conversation.view 槽位,order 为 15,位于 Trajectory(10)与 Prompt Studio(20)之间。
上手难度
入门 — 编辑、重生成、撤销按钮都是可视化控件,会话期间无需写配置;理解 truncate/preserve 与版本分支需要花几分钟,但不影响日常使用。
已知问题与限制
- 不支持编辑图片、工具调用、附件等非文本块;只有用户正文、助手思考、助手回复三类文本块可改
- 不恢复或修改工作区文件、命令外部效果与既有产物,分支切换不会回滚这些副作用
- 不修改 DSH 引擎、apiproxy 或官方 UI 包;插件只通过 Host 公开服务与 Browser 槽位协作
- 当历史中找不到可定位的请求头配置(provider、model)时,分支操作会因无法解析模型路由而失败
- 旧版扁平版本事件(schemaVersion 缺失)仍可读取并在投影时规范化,但内部 schema 版本已升级到 2
dsh-message-edit(npm · GitHub)为 DeepSeek Harness 补充基于事件溯源的「消息编辑与重生成」能力。插件不改写历史事件,也不修改 DSH 引擎内部;每次编辑、重生成或重试都会从目标回合之前创建一个新会话版本,原会话始终保留并可随时切回。
dsh plugin --profile web add dsh-message-edit
功能
- 编辑消息:可编辑已落定的用户文本、
assistant.reasoning思考块与assistant.response回复文本。 - 重生成:从最后一条已落定助手回复所属回合之前分支,使用原用户输入重新生成。
- 重试任意回合:在 Timeline 中选择任意历史回合重新执行。
- 级联策略:
truncate(默认):只重新执行目标输入,删除该点之后的旧后续。preserve:保留后续用户输入,并在新分支中依次重新执行;助手输出与工具链全部重新生成。
- 版本切换:会话标题栏的
←撤销当前原子效果,→重施加最新直接子效果;Timeline 展示完整已知分支树、操作时间、编辑前后内容与当前版本。 - Timeline 标签页:注册到
conversation.view,order: 15,位于 Trajectory(10)与 Prompt Studio(20)之间。
设计
时间可组合性
插件把完整回合作为效果原子。目标回合的 turn/start、模型请求、工具调用、工具结果与 turn/end 不会被局部复制后拼接;新版本从该回合之前的闭合边界分支:
- 用户消息编辑、Reroll 与 Retry:回退整个目标回合,再把目标用户输入作为新回合交给 Agent。
- 助手块编辑:回退整个目标回合,以原用户输入和编辑后的助手内容构造一个新的完整闭合回合;原工具链不进入新版本。选择
preserve时,后续用户输入再依次交给 Agent,产生新的完整工具链。 - 每个版本都追加一个不可拆分的
message-edit/version效果对:effect记录正向效果,inverse记录恢复目标。父版本链自动导出组合逆;恢复不是删除事件,而是沿逆链切换到仍然存在的版本。 - 消息历史变换彼此不交换,因此撤销遵循 LIFO:一次只撤销当前原子效果并保留更早效果;各后继分支始终保留,可从父版本重新施加。
分支与 Agent 接线
旧实现先用短生命周期 Session 暂存分支、落盘、移除 live Session,再用 agents.resume() 重建 Agent。这个过程存在两个分离的生命周期边界:暂存日志已经持久化后,Agent 仍可能创建失败。现实现只使用 AgentRegistry.create() 已公开的 seed + meta 事务缝:
- 在来源 Agent 的 runMaintenance() 内,从已闭合边界取得不可变 seed;第一回合之前使用空 seed。
- 用本地等价的纯事件构造器把版本效果对与可选手工助手回合加入 seed,再调用
ctx.agents.create({ seed, meta })。Session 在 Agent 构造前一次性验证完整 seed;任何一步失败都会由 AgentFactory 的结构性逆撤销,外部观察者看不到半成品 Session,Agent 的回合计数也直接从完整历史初始化。 - 发布后调用
ctx.sessions.flush(),在 HTTP 操作成功前建立耐久性屏障。 - Workspace 挂接与 child Agent 生命周期分别返回原子逆;操作失败时按相反顺序组合恢复。随后通过
child.agent.followup()排入需要重新执行的用户输入。
此路径不接触 ReactLoopAgent、AgentLoop 私有方法或 apiproxy 的收窄 fork RPC;分支 seed 仍由同一 Session 公共事件契约验证。
空间可组合性
- Host 只依赖公开的
sessions、agents、sessionPersistence、sessionQuery、workspaceRegistry与webServer服务。 - Browser 只通过
slots、conversation、connection与 runtimesessions服务组合。 - Timeline 与标题栏共享一个按
sessionId建立的值级 Snapshot source;控制器反应式订阅当前 Session 的闭合回合值与 Session 列表中的谱系值,Session 身份替换时重新绑定,不缓存旧 Session 对象。 - 新版本导航等待 runtime Session 列表发布对应 ID 后再执行
ctx.sessions.open(),依赖可用性变化直接驱动导航。
数据模型
每个插件版本在自己的非继承后缀中包含一个 message-edit/version 事件:
interface MessageEditVersionEvent {
schemaVersion: 2
effect: {
id: string
operation: 'edit' | 'reroll' | 'retry'
cascade: 'truncate' | 'preserve'
targetTurn: number
targetEventSeq: number
targetBlockIndex?: number
blockKind?: 'user' | 'assistant.reasoning' | 'assistant.response'
before?: string
after?: string
}
inverse: {
kind: 'restore-version'
sessionId: string
}
}
会话头的 parentSession 构成版本树,且必须与事件中的 inverse.sessionId 一致;seedLength 区分当前版本自己的元数据与从祖先继承的同名事件。Timeline 通过 ctx.sessionQuery.traceSession() 和 readSession() 生成完整值级投影,并由原子逆链导出 undoStack 与直接 redoSessionIds。旧版平面事件仍可读取,并在投影时规范化为同一效果对。
UI
conversation.viewid: message-edit-timelineorder: 15label: Timeline
conversation.session.header.actionsid: message-edit-controls- 直接父效果撤销、直接子效果重施加、效果链计数、最后回复重生成
组件使用 CSS Modules 与 --dsw-* 语义 token,不引入 UI 库。所有产品文案为中文,代码注释为英文。
构建
npm install
npm run build
构建基于 npm 发布的 @deepseek-ai/*@0.1.0-rc.6 类型与本地工具链(typescript、tsdown、lightningcss),不再依赖 dsh 源码树。构建生成:
index.mjs:Host 插件client.js:Browser 插件client.js.map:Browser source map
安装
dsh plugin --profile web add dsh-message-edit
或本地开发:
dsh plugin --profile web add -w link:/path/to/dsh-message-edit
dsh plugin 是 pnpm 转发器:add 后会自动识别 dsh.bundle 声明并把插件收编进 profile 的 dsh.profile.bundles,重启 dsh 即生效。本地开发建议用 link:(符号链接),改动源码重构建后重启即更新。
HTTP 接口
GET /message-edit?sessionId=<id>:读取可编辑消息、可重试回合与完整版本树。POST /message-edit:执行edit、reroll或retry,返回已发布的新 Session ID。
范围边界
- 不原地改写 Session 事件;历史是 append-only、deep-frozen。
- 不联动恢复或修改工作区文件、命令外部效果与既有产物。
- 不修改 DSH 引擎、apiproxy 或官方 UI 包。