Skip to content

Latest commit

 

History

History
114 lines (83 loc) · 9.58 KB

File metadata and controls

114 lines (83 loc) · 9.58 KB

DSH 回滚插件:踩过的雷(经验总结)

用途:后面重新做前端可视化 / 维护后端三件套时,避免重复踩坑。 每条雷包含【现象】【根因】【正确做法】。按"本次放弃的前端"→"已解决的后端"→"命令路由"三部分整理。


一、前端可视化(本次放弃,DOM 注入方案)

雷 1:渲染层选了 DOM 注入,而不是轨迹原生节点

  • 现象:锚点徽标钉在 section[data-turn] 上,React 一重渲染徽标就掉,得靠 MutationObserver 反复重钉("钉了掉、掉了钉");点击事件绑在随时被移除重建的节点上,时灵时不灵(用户实测:点小点点没反应)。
  • 根因:内置轨迹视图的节点 kind 分发是封闭 if 链if (node.kind === "user") … else if (node.kind === "assistant") …,无 default 分支),外部插件注册新 kind(如 rollback-anchor)会被所有 if 静默跳过、不渲染。于是绕去用 MutationObserver 往 React 管的 DOM 里塞节点。
  • 正确做法:改 trajectory 源码,在渲染器的 kind 分发链里加一个 rollback-anchor 分支,锚点成为轨迹的一等公民(继承布局、折叠、tooltip、分页)。数据层不用动。

雷 2:数据层其实写对了,别推倒重来

  • 现象conversationEvents.register + conversationViews.registerrollback/checkpointrollback/start|end 折成锚点快照,工作正常。
  • 正确做法:这两个是官方扩展点(trajectory 内置 8 个定义全走这套),src/client/anchors.ts 的 checkpoint/transaction 定义直接可复用。放弃的只是渲染层(overlay.ts 的 DOM 注入)。

雷 3:client 插件作为 bundle 挂载,破坏了 host 插件加载

  • 现象:用 dsh plugin add 把前端插件加为 profile 的第三个 bundle 后,command-rollback/rollback 命令不再注册(用户输入 /rollback 被降级成普通消息)。而 rollback-basic 正常(checkpoint 照写)。
  • 根因:bundle 加入改变了 profile 加载结构/时机,command-rollbackinject: ['commands','rollback'] 在加载窗口期没被正确满足 → 它的 apply() 没跑 → 命令没注册。前端插件 host 侧虽是无害空壳 apply(){},但"作为 bundle 挂载"这个动作本身是破坏源。
  • 教训:client 插件不要作为 profile bundle 挂载;要隔离加载(单独 patch 层 / 独立作用域),别让它改变 host 插件的加载顺序。
  • 已坐实(2026-08-15):从 profile 移除 ui-rollback-visual bundle 并重启后,/rollback 命令立即恢复(日志出现 command/run {name:"rollback"}),确认本雷即根因。

雷 4:childSessionId 拿不到(自动激活链路缺一环)

  • 现象commands.execute('rollback') 返回的 CommandResult 只有 { kind, text, sourceEventSeq },子会话 ID 埋在 text 字符串里("new child session xxx")。
  • 正确做法:给 command-rollback 的返回加一个结构化 childSessionId 字段,client 拿到后直接 sessions.open(childId)

二、后端三件套(已解决,但机制重要)

雷 5:会话事件白名单机制(之前 resume 崩溃的真凶)

  • 现象:插件写了 rollback/* 自定义事件后,重启恢复会话直接崩("unknown event type, refusing to interpret the log")。
  • 根因KNOWN_SESSION_EVENT_TYPESpackages/core/session/src/known-event-types.ts)是生成物scripts/gen-persistence-catalog.ts),只收录"仓库内声明过"的 SessionEventMap 成员。持久化读取路径 assertEventsSupported 遇到白名单外、又没打 ignorable 标记的事件就拒绝。仓库外/未合入插件的事件天然不在列表。
  • 正确做法:插件用 declare module '@deepseek-ai/dsh-session/types' 声明事件后,跑 pnpm run gen-persistence-catalog 重新生成白名单,让事件进 KNOWN_SESSION_EVENT_TYPES

雷 6:Session.append 没有 ignorable 通道

  • 现象:想让事件"可被安全跳过",但 append(type, data, ...opts)opts 只对 surface 事件有意义,没有参数能设 ignorable
  • 正确做法:自定义事件要么进白名单(雷 5),要么接受"必选"语义。别指望运行时给 append 打 ignorable。

雷 7:Service Definition 不能单独加载

  • 现象:把 @deepseek-ai/dsh-timeline-rollback 也写进 cordis.patch.yml 加载 → service "rollback" has been registered 冲突。
  • 根因:timeline-rollback 是纯 Service Definition(抽象类 RollbackEngine + 事件词汇表),不是可加载插件。loader 把 default export 当插件实例化,new RollbackEngine(ctx) 注册了 ctx.rollback,随后 rollback-basic 的 BasicRollbackEngine 又注册一次 → 冲突。
  • 正确做法:只 insert Provider(rollback-basic)和 Consumer(command-rollback),Service Definition 作为它们的依赖自动引入,不要单独加载。

三、CLI 命令路由(最新发现)

雷 8:命令没注册时,输入被静默降级成消息,无任何报错

  • 现象/rollback 命令没注册时,用户输入 /rollback checkpoint 不会报"未知命令",而是当作普通消息发给模型,很难察觉。
  • 根因(GUI 路由链路):
    1. composer 检测到 / 前缀 → 进入「裁决」(adjudicate);
    2. matchEnter 解析 token,调 directory.resolve(sessionId, name)
    3. directory 缓存来自 commands.list(agent)(Remote RPC)→ host commands.list
    4. resolve 返回 undefined(命令不在列表)→ 裁决结果 void 0 → 降级成普通消息。
  • 排查方法:查会话日志里有没有 command/runname: "rollback")——0 次就是命令没注册;有 command/run 才是路由到了命令系统。
  • 教训:验证命令是否生效,先看日志里有没有 command/run,而不是只看"命令没报错"。

雷 9:带参数的命令必须声明 input(leadingInput),否则参数输入被降级成消息

  • 现象/rollback(无参数)能执行,但 /rollback checkpoint/rollback checkpoint:<turn>/rollback <seq>(带参数)全部被降级成普通消息。
  • 根因(GUI 裁决逻辑 matchEnter):命令输入解析后,bare(无空格)的命令走 runDetached 直接执行;但非 bare(带参数)的命令,只有 desc.input !== void 0 时才返回 claim(认作命令),否则 if (!bare) return void 0 → 降级成消息。而 CommandDefinitioninput 字段是 { hint: string }(leadingInput 声明),command-rollback 注册时没声明 input → 带参数一律降级。
  • 正确做法:注册命令时声明参数提示:
    ctx.commands.register({
      name: 'rollback',
      description: 'Fork a child session through a history boundary',
      input: { hint: '<seq | checkpoint | checkpoint:<turn>>' },  // ← 关键
      handler,
    })
  • 已修复(2026-08-15):command-rollback 加上了 input 声明(源码 38edcee3f7),/rollback <args> 系列命令恢复正常。
  • 参照:内置命令 hint 写法——input: { hint: "<text>" }(compact)、input: { hint: "[<objective>|clear|edit <objective>|pause|resume]" }(goal)。

雷 10:dsh 加载插件走 .dsh\profiles\node_modules,不是全局 dsh\node_modules

  • 现象:改了全局 C:\Users\<user>\AppData\Local\nvm\<ver>\node_modules\@deepseek-ai\dsh\node_modules\@deepseek-ai\dsh-command-rollback 的 lib 后,重启验证发现改动没生效(诊断 hasInput=false)。
  • 根因:dsh loader 解析 @deepseek-ai/dsh-* 包走的是 profile 的共享 node_modules(C:\Users\<user>\.dsh\profiles\node_modules),不是全局 dsh 的 node_modules。这个目录里:
    • dsh-session 等 session 相关包是 Junction(链接到全局)→ 改全局的 dsh-session 生效;
    • dsh-command-rollback / dsh-rollback-basic / dsh-timeline-rollback独立目录(当初 dsh plugin add / 手动复制进来的)→ 改全局的这三包不生效
  • 教训:改任何 @deepseek-ai/dsh-* 包的产物前,先确认它实际加载的副本在哪(Read-Item 看 LinkType:Junction=改全局源,独立目录=改这个目录本身)。用 Get-ChildItem <dir> -Recurse -Filter "xxx" 全盘搜同名包,别想当然。
  • 验证方法:改完后用动态插件在真实进程里查 commands.find(agent, name) 的实际返回(或查日志 command/run),确认改动真的进了运行态。

附:验证命令是否注册的最小脚本

# 在全局 dsh 的 node_modules 目录下模拟加载,确认代码本身没问题
cd <dsh>/node_modules/@deepseek-ai/dsh/node_modules
node --input-type=module -e "
import { Context } from '@deepseek-ai/cordis';
const { default: BasicRollbackEngine } = await import('@deepseek-ai/dsh-rollback-basic');
const ctx = new Context();
ctx.provide('sessions', { create: () => ({}), get: () => undefined, fork: async () => ({}), flush: async () => {} });
ctx.provide('commands', { register: (d) => { console.log('注册命令:', d.name); return () => {}; } });
new BasicRollbackEngine(ctx, { checkpoints: false });
console.log('rollback 服务已注册:', ctx.get('rollback') !== undefined);
const { apply } = await import('@deepseek-ai/dsh-command-rollback');
apply(ctx);
"

脚本能打印「注册命令: rollback」且「rollback 服务已注册: true」,说明代码正确,问题在加载环境(inject 时序 / bundle 结构),不在代码。