DeepSeek Harness 删掉最后一个市场源,列表却又回来:在途刷新回写的竞态与修法

故障排查发布于 2026-10-30作者: DeepSeek Plugin 插件市场
DeepSeek HarnessDSH插件市场竞态market-store刷新异步回写
DeepSeek Harness 插件市场里删掉最后一个源,列表会先清空,过一会儿又自己回来。按时间线拆开看:在途刷新没有被作废,回写时不校验来源,于是覆盖了用户操作。

删掉最后一个市场源,列表立刻清空,界面显示「添加第一个源」——几秒后,那个源的插件列表又整整齐齐地回来了。 来源配置其实没变,indexes 仍是空数组、selected 仍是 null,只有 listing 被覆盖了。把它按时间线拆成三段,就能看清这是一次「在途请求覆盖用户操作」的经典竞态(#11103)。

DeepSeek Harness 发生前:一次刷新带着它的「代号」出发

每次刷新开始时,客户端会记下当前的读取代号;请求返回时对比代号,不一致就应丢弃。

  1. 配置一个索引源 A,正常加载插件市场。
  2. 触发一次刷新,让 pluginMarket.search() 的响应保持挂起。
  3. 此时这次读取已经持有它出发时的代号。
  4. 正常情况下,只要代号被后续操作递增,这次返回就会被判为过期。
  5. 关键就在这里:它是否过期,取决于有没有人递增那个计数器。

DSH插件 发生时:清空列表,却没有作废在途读取

loadIndexes() 在选中项为空时把 listing 清空,但这条分支不递增代号,于是那份在途读取仍然「有效」。

  1. 删除源 A,setIndexes([]) 与随后的 listIndexes() 完成。
  2. 此刻状态确实是 indexes: []、selected: null、listing: null。
  3. 但清空动作只改了 listing,没有让在途读取失效。
  4. 对比另一条路径:当 selected !== 当前 listing.url 时会新起一次 load(),那次会递增代号,旧的读取自然作废。
  5. 缺口只有一条分支宽:只有 selected === null 这条清空路径不做失效。

DeepSeek Harness 发生后:返回结果无条件写回

settle() 用它出发时记下的 url 直接写回状态,不检查这个来源是否还在配置里。

  1. 在途请求返回后走到 settle(),后者调用 patch({ listing, indexes })。
  2. patch 是合并式更新,所以 indexes: [] 与 selected: null 被保留。
  3. 只有 listing 被换成旧来源的数据,url 又变回 A。
  4. 成功时状态是 status: 'ready';失败时同样会把旧列表写回,只是 status: 'error'。
  5. 受控环境里成功与失败各复现 10/10,说明这不是偶发。

DeepSeek插件 为什么用户在界面上能踩到

源管理菜单在列表加载期间仍然可用,删除按钮只在「保存来源」进行时被禁用,所以刷新→管理源→删除这条顺序是可达的。

  1. 界面按 listing === null 选择空状态,否则渲染浏览视图。
  2. 恢复后的列表不会被隐藏,因为 selected 与 indexes 都是空的。
  3. 于是用户看到的就是「我明明删了,它又回来了」。
  4. 需要注意边界:这份报告没有声称源被加回配置,也没有声称插件被自动安装。
  5. 它只描述显示层被旧数据覆盖。

DeepSeek Harness 恢复到真正的空状态

重新读一次源列表,或构造新控制器;再点一次 ensure() 没用。

  1. 调用 reload() 重新读取源列表,列表回到空。
  2. 或者销毁并重建控制器,效果相同。
  3. 再点一次 ensure() 不会清除,它沿用被恢复的那份列表。
  4. 再刷新一次会报 plugin-market/unknown-index,但旧列表依旧保留。
  5. 这些是 API 层验证,未覆盖点击陈旧插件的安装按钮这类交互。

DSH插件 修法:在清空的同一个同步块里递增代号

社区给出的最小修法是在 loadIndexes() 的清空分支里先递增 listingRead 再清空 listing;位置必须在同一个同步块内。

  1. 在 selected === null 分支里执行 this.listingRead++,让在途读取立刻过期。
  2. 紧接着 this.patch({ listing: null })。
  3. 顺序不能反,也不能放在任何 await 之后——晚一步就等于没做。
  4. 可选的双保险:在 settle() 里校验 url 是否仍在配置中。
  5. 这种递增写法与另一处 indexRead 守卫 loadIndexes 的做法一致,风格统一。
  6. 配套测试可以沿现有 helpers 写:先让刷新挂起,删除源并断言为空,再 resolve 那个挂起的结果,最后用 setTimeout(resolve, 0) 等微任务队列结算再断言仍为空。

一分钟自查:这次回来属于哪一种

按「删除与回来之间的时间差」分三类:零延迟是配置写回,有等待是在途回写,跨进程是持久化。

现象时间差判断
删除后立刻回来0 秒设置被写回磁盘
删除后几秒回来有等待在途刷新回写
重启后仍在跨进程配置或缓存持久化
  1. 先记下删除时刻,再看列表回来的时刻。
  2. 有等待的,去查这段时间里是否有刷新在进行。
  3. 重启后仍在的,去查设置文件而不是内存状态。

注意事项

  1. 别把「列表回来」当成「源被加回来」。配置层一直是空的。
  2. 别忽略失败分支。失败同样会写回旧列表,只是状态标成 error。
  3. 别用重复刷新当恢复手段。它只会报 unknown-index 并保留旧列表。
  4. 修复要注意时序位置。放在 await 之后等于没修。
  5. 记录版本与提交。社区在 rc.3 与 master 上都复现过,路径一致。

如果你的列表在删除后立刻回来、中间没有等待,那不是这条竞态,去查设置是否被写回磁盘。相关排查见 dsh 插件市场打不开、插件列表加载不出来?DSH Plugin Hub 网络、代理与证书排查。

常见问题

插件市场删掉最后一个源后,列表为什么又会在 DeepSeek Harness 里自己回来?

因为 DeepSeek Harness 清空列表时没有作废在途的刷新。loadIndexes() 在 selected === null 时把 listing 清空,但没递增读取代号;刷新返回后仍然会 settle() 写回旧来源的列表。

是配置里的源被加回来了,才让 DeepSeek Harness 的列表恢复吗?

没有,DeepSeek Harness 的配置里源并没有被加回来。写回用的是 patch 合并式更新,indexes: [] 与 selected: null 都保留了下来,只有 listing 回来了。所以这是显示层被覆盖,不是配置被改。

即使刷新失败,DeepSeek Harness 也会把删掉的列表恢复吗?

会,即使刷新失败,DeepSeek Harness 也会把旧列表写回。社区在受控环境里测了 10/10 成功和 10/10 失败,两种结果都会把旧列表写回,只是 status 分别是 ready 与 error。

怎么操作才能让 DeepSeek Harness 的市场列表回到真正的空状态?

在 DeepSeek Harness 里重新读一次源列表(reload()),或构造一个新的控制器,才能让列表回到真正的空状态。再点一次 ensure() 不行,它沿用被恢复的那份列表;再刷新一次会报 plugin-market/unknown-index,但旧列表仍在。

DeepSeek Harness 这个列表回写竞态的修法是什么,具体怎么排查?

修法是在 DeepSeek Harness 的 loadIndexes() 清空列表的同一个同步块里递增 listingRead,让在途读取失效;也可以在 settle() 里加一道来源校验作为双保险。关键是不能放在任何 await 之后。

相关术语

listingRead
listingRead 是插件市场客户端里用于标记列表读取代号的计数器。刷新开始时记下当前值,回来时对比,不一致就说明这次读取已过期。— https://github.com/deepseek-ai/deepseek-harness/discussions/11103
settle()
settle() 是把一次刷新的结果写回状态的函数,当前实现直接用发起时的 url 写回,不检查该来源是否仍被配置。— https://github.com/deepseek-ai/deepseek-harness/discussions/11103
loadIndexes()
loadIndexes() 是读取已配置市场源列表的函数。它在选中项为空时清空 listing,但在这条分支上不作废正在进行中的列表读取。— https://github.com/deepseek-ai/deepseek-harness/discussions/11103
in-flight(在途)
in-flight(在途)是已经发出但尚未返回的异步请求。用户在此期间修改设置时,返回结果可能已经不再对应当前界面状态。— https://github.com/deepseek-ai/deepseek-harness/discussions/11103

来源