用途:后面重新做前端可视化 / 维护后端三件套时,避免重复踩坑。 每条雷包含【现象】【根因】【正确做法】。按"本次放弃的前端"→"已解决的后端"→"命令路由"三部分整理。
- 现象:锚点徽标钉在
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、分页)。数据层不用动。
- 现象:
conversationEvents.register+conversationViews.register把rollback/checkpoint、rollback/start|end折成锚点快照,工作正常。 - 正确做法:这两个是官方扩展点(trajectory 内置 8 个定义全走这套),
src/client/anchors.ts的 checkpoint/transaction 定义直接可复用。放弃的只是渲染层(overlay.ts 的 DOM 注入)。
- 现象:用
dsh plugin add把前端插件加为 profile 的第三个 bundle 后,command-rollback的/rollback命令不再注册(用户输入/rollback被降级成普通消息)。而 rollback-basic 正常(checkpoint 照写)。 - 根因:bundle 加入改变了 profile 加载结构/时机,
command-rollback的inject: ['commands','rollback']在加载窗口期没被正确满足 → 它的apply()没跑 → 命令没注册。前端插件 host 侧虽是无害空壳apply(){},但"作为 bundle 挂载"这个动作本身是破坏源。 - 教训:client 插件不要作为 profile bundle 挂载;要隔离加载(单独 patch 层 / 独立作用域),别让它改变 host 插件的加载顺序。
- 已坐实(2026-08-15):从 profile 移除 ui-rollback-visual bundle 并重启后,
/rollback命令立即恢复(日志出现command/run {name:"rollback"}),确认本雷即根因。
- 现象:
commands.execute('rollback')返回的CommandResult只有{ kind, text, sourceEventSeq },子会话 ID 埋在 text 字符串里("new child session xxx")。 - 正确做法:给
command-rollback的返回加一个结构化childSessionId字段,client 拿到后直接sessions.open(childId)。
- 现象:插件写了
rollback/*自定义事件后,重启恢复会话直接崩("unknown event type, refusing to interpret the log")。 - 根因:
KNOWN_SESSION_EVENT_TYPES(packages/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。
- 现象:想让事件"可被安全跳过",但
append(type, data, ...opts)的opts只对 surface 事件有意义,没有参数能设ignorable。 - 正确做法:自定义事件要么进白名单(雷 5),要么接受"必选"语义。别指望运行时给 append 打 ignorable。
- 现象:把
@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 作为它们的依赖自动引入,不要单独加载。
- 现象:
/rollback命令没注册时,用户输入/rollback checkpoint不会报"未知命令",而是当作普通消息发给模型,很难察觉。 - 根因(GUI 路由链路):
- composer 检测到
/前缀 → 进入「裁决」(adjudicate); matchEnter解析 token,调directory.resolve(sessionId, name);- directory 缓存来自
commands.list(agent)(Remote RPC)→ hostcommands.list; resolve返回undefined(命令不在列表)→ 裁决结果void 0→ 降级成普通消息。
- composer 检测到
- 排查方法:查会话日志里有没有
command/run(name: "rollback")——0 次就是命令没注册;有command/run才是路由到了命令系统。 - 教训:验证命令是否生效,先看日志里有没有
command/run,而不是只看"命令没报错"。
- 现象:
/rollback(无参数)能执行,但/rollback checkpoint、/rollback checkpoint:<turn>、/rollback <seq>(带参数)全部被降级成普通消息。 - 根因(GUI 裁决逻辑
matchEnter):命令输入解析后,bare(无空格)的命令走runDetached直接执行;但非 bare(带参数)的命令,只有desc.input !== void 0时才返回 claim(认作命令),否则if (!bare) return void 0→ 降级成消息。而CommandDefinition的input字段是{ 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)。
- 现象:改了全局
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 结构),不在代码。