DeepSeek Harness 新会话草稿丢失:handOffDraft、localStorage 与复用
DeepSeek Harness 里字还在,是索引断了。 草稿按会话 id 存在浏览器本地,新建会话每次都换 id,旧记录就成了没人读的孤儿;旧 blank 会话同时被侧栏规则隐藏,所以连入口也一起消失(来源)。
DSH插件新会话草稿丢失的现象与复现
如果你的操作顺序与下面完全一致,基本不用再查别的原因——这是新会话路径的固定行为,不是偶发。
- 在工作区点「+」新建会话。预期得到一个空白 composer。
- 在输入框里打几个字,不要发送。预期文字留在输入框。
- 点侧栏里任意一个已有会话。预期正常切换。
- 再点「+」。预期输入框是空的,而且刚才那个 blank 会话在侧栏里再也找不到。
- 验证「字没丢」:回到第 3 步那个已有会话,它未发送的草稿仍然能恢复。预期只有新会话这一条路径丢失。
会话看起来是空的,还有另一类原因,见 DSH插件会话空白与侧栏隐藏排查。
为什么草稿会丢:DSH插件 0.2.1 的文档化变更
0.2.1 起,无参新建会话被有意改成「每次创建全新会话与 Agent」,目的是避免沿用旧空会话里残留的 preset 定义;复用逻辑没被删,只是不再默认走。
讨论里核对的结果是:startSession() 无选项时把 undefined 一路传下去,最终直接调用 sessions.create({ workspaceId });带显式 preset 的调用仍会走复用分支,按 cwd 找到同工作区的 blank 并把它的 sessionId 交给 Host(据讨论,社区核对源码)。
所以「把 undefined 改成 {} 恢复复用」这个一行补丁虽然能立刻见效,但它恢复的正是这次变更要退役的行为。真正缺的是:换 id 时没有迁移旧草稿,也没有给旧 blank 留入口。
草稿到底存在哪:一条记录三个字段
草稿不是存在会话文件里,而是存在浏览器 localStorage 里,键名带 sessionId;会话文件里那几百字节的空日志与草稿无关。
- 打开 DevTools,进入 Application → Local Storage。预期能看到
dsh.conversation.前缀的一批键。 - 逐条查看值,结构是
{ draft, view, viewRequest },draft.text里就是你的文字。预期能定位到含目标文字的那一条。 - 记下该键后缀的 sessionId。预期它对应那个已经不出现在侧栏的 blank 会话。
- 再看
dsh.sessions.current,它指向当前的会话 id。预期与第 3 步的 id 不同——这就是「键还在、指针走了」。
路径一:用 DSH插件 的 handOffDraft 补丁交接草稿
这是与上游方向一致的做法:仍然新建会话,但在选中新会话、发布之前,把工作区里被丢弃的 blank 草稿按「最新优先」交给新的输入框。
- 找到
ui-workspace里「保留新会话、尚未发布选中」的那个时点。预期此时新旧两个会话 id 都还拿得到。 - 把该工作区里未启动的 blank 会话列成候选,按最近使用排序。预期候选只包含没有发送过消息的会话。
- 对每个候选读取草稿:还挂载着就读实时快照,已卸载就读最后保存的草稿。预期不会读到空记录。
- 跳过没有文字、没有引用芯片、没有命令标记的候选。预期不会把空草稿搬过去。
- 把第一个有内容的草稿通过既有的结构化草稿编辑器导入目标会话,写入目标 sessionId 的记录,再删除源记录。预期新输入框出现原文,旧键消失。
- 反向保护要写全:目标已有文字或附件、目标正在提交、导航没有保留目标时,两边都不动。预期不会出现覆盖用户新输入的情况。
- 刷新页面重测,预期草稿仍在——说明写的是持久记录,不只是内存状态。
路径二:用 DSH插件 的 localStorage 手工抢救草稿
适合已经丢了草稿、又不想动构建产物的场景:把「当前会话指针」指回旧 blank,刷新后输入框会按 sessionId 重新读草稿。
- 在 DevTools 的 Console 里列出所有草稿键,找到
draft.text含目标文字的那一条,复制它的 sessionId。预期得到一串会话 id。 - 执行下面两行(把
<旧sessionId>换成刚复制的值):
localStorage.setItem('dsh.sessions.current', JSON.stringify({ sessionId: '<旧sessionId>' }))
location.reload()
- 刷新后确认输入框恢复原文、引用芯片也在。预期侧栏把那个 blank 会话重新显示为当前会话。
- 立刻把内容发送出去,或复制到别处保存。预期不再依赖这条脆弱的指针。
- 注意顺序:设置键后必须马上刷新,中间任何一次应用内导航都会把
dsh.sessions.current重写掉。预期操作间隔越短成功率越高。 - 桌面版打不开 DevTools 时,用 F12、Windows 的 Ctrl+Shift+I 或 macOS 的 ⌘⌥I,打包版同样可用。
- 恢复后把那条
dsh.conversation.*记录复制一份留档。预期下次再丢时不用重新翻找 sessionId。 - 如果 Console 里读到的
draft是旧版纯字符串而不是对象,说明记录来自更早版本,先升级再读,别按对象结构硬解析。
路径三:把 DSH插件 改回 blank 复用配置
如果你的团队更在意「草稿不丢」而不是「每次拿到干净的 preset」,可以让无参新建重新走复用分支,用回旧语义。
- 在接受这个取舍前先确认:复用会沿用旧空会话的 preset 定义,这是上游改动的出发点。预期你知道自己在换什么。
- 在无参分支传入复用预设(例如空对象而不是
undefined),让connectBlank进入按 cwd 复用的分支。预期「+」不再创建新 id。 - 重跑第 1 节的四步复现。预期草稿原地保留,侧栏也不再多出隐藏会话。
- 检查磁盘:之前每次按「+」都会留下一个约 350 字节的空会话日志,改回复用后不再新增。预期旧目录可以手工清理。
- 在团队内记录这处偏离上游的改动,升级时重新评估——上游若改了复用语义,这个补丁会失效。
- 验证带回车的边界:在 blank 里只输入空白字符也算有内容,预期不会被当成空草稿丢掉。
- 验证多工作区:在 A 工作区留下的 blank 不应被 B 工作区的「+」复用,预期复用只发生在同 cwd 内。
DSH插件 三条路径的边界与草稿存储限制
三条路径合起来才覆盖「已经丢的能救、以后不再丢」,单用任何一条都会在某个场景掉链子。
| 路径 | 解决什么 | 不解决什么 |
|---|---|---|
| handOffDraft 补丁 | 新会话仍新建,草稿自动跟着走 | 附件不迁移;one-shot 空草稿不搬 |
| localStorage 手工恢复 | 已经丢的历史草稿能救回来 | 要人工操作;附件同样没有 |
| 改回复用 | 从根上不换 id | 违背 0.2.1 的既定方向,升级易冲突 |
选型建议:还在丢草稿就先用路径二救数据,再决定长期走路径一还是路径三。
草稿存储还受什么影响
- 草稿只存在当前浏览器,换浏览器或清站点数据就真的没了。
- 附件与图片是运行时对象,从未进入草稿记录,任何路径都恢复不了。
- 手工改 localStorage 前先复制一份值到文本文件,避免写坏后无从回退。
- 补丁方案不改变持久化格式,旧记录仍可读;如果看到
{ draft, view, viewRequest }之外的形状,先确认版本。 - 以上机制与代码位置来自讨论中的社区核对源码,上游未修,按临时方案对待。
- 清站点数据、换浏览器或换端口(localhost 与 127.0.0.1 算不同来源)都会让草稿键读不到,排查前先确认访问地址没变。
常见问题
DeepSeek Harness 的草稿能找回:它并没有被删除,而是按 sessionId 存在 localStorage 的 dsh.conversation.* 记录里。切回来时新建了一个新 id,旧键就再也没人读,所以界面上看不到。
这是 DeepSeek Harness 0.2.1 的文档化变更,不是普通回归:同一批改动附带升级指南,明确写了 New Session 会创建全新会话与 Agent,不再复用工作区里的空会话。复用逻辑仍在,只是无参路径不再走它,所以问题变成「旧草稿没被迁移」。
可以,DeepSeek Harness 的草稿文字能只靠 localStorage 恢复。在 DevTools 里找到 dsh.conversation.* 中 draft.text 有内容的那条,把 dsh.sessions.current 写成该 sessionId,然后立刻刷新。文字与引用芯片会回来,附件和图片不会,它们本来就没进草稿存储。
DeepSeek Harness 的 handOffDraft 补丁不会搬附件:它只搬文字、引用芯片和命令芯片,附件属于原会话的浏览器对象,留在原处。补丁也不会删除旧会话,它只转移草稿记录。
DeepSeek Harness 改回 blank 复用确实有副作用:会重新沿用旧空会话的 preset 定义,这正是 0.2.1 改掉它的原因。好处是 id 不变、草稿原地保留;代价是升级时容易和上游行为冲突,需要自己维护这处偏离。
相关术语
- blank 会话
- blank 会话是尚未发送过任何消息的空会话,侧栏只在它是当前会话时才显示。它的 id 是草稿存储的键,因此换 id 等于换了一把打不开旧内容的锁。— https://github.com/deepseek-ai/deepseek-harness/discussions/10021
- dsh.conversation.*
- dsh.conversation.* 是 composer 草稿的 localStorage 键前缀,按 sessionId 分条保存文本、引用与命令标记。键里的会话 id 不存在时,这条草稿不会再被任何界面读取。— https://github.com/deepseek-ai/deepseek-harness/discussions/10021
- startSession()
- startSession 是客户端新建会话的入口。带 preset 参数时走复用分支,可以沿用工作区里已有的空会话;无参调用会直接创建新会话,因此每次按「+」都得到新 id。— https://github.com/deepseek-ai/deepseek-harness/discussions/10021
来源
- 0.2.1: unsent draft of a new session is gone after switching chats· deepseek-ai/deepseek-harness Discussions
- DeepSeek Harness 官方仓库· deepseek-ai