Skip to content

插件生命周期的失败模式 ​

桌面 Agent 生命周期 · 第 2 / 2 篇
建议先读:第 1 篇:桌面 Agent 不是 Web 套壳。

系列第一篇描述了四层:profile、Host、Client、App。这一篇是一份“层与层之间会怎么出错”的现场手册。

真正有用的问题通常不是“重载能不能工作”,而是“哪一层还是旧的,用户在旧状态下会看到什么”。

一个小模型:期望状态与观察状态 ​

插件有一个期望状态:

text
installed: true | false
enabled:   true | false

每一层还有一个观察状态:

text
profile: installed | missing
host:    loaded | unloaded | failed
client:  mounted | unmounted | stale
app:     current | restart-required

大多数生命周期问题不是包管理器算错了,而是这些状态彼此不一致,或者界面只表达了其中一个。

七种失败模式 ​

1. 从 profile 删除了,但 Client 还挂着 ​

现象: 插件卡片消失了,侧栏入口或输入框按钮还在。

原因: 依赖和 patch 已删除,但渲染进程还持有旧的模块图。刷新页面可能修复,重启应用一定能。

设计回应: 返回 client-reload-required,而不是 ok。提供一个生产环境可用的 Client reload。不要在 Client 层确认之前宣称功能已经消失。

2. 原地更新了,但 Host 还跑着旧包 ​

现象: 新版本已经安装,工具或路由却还是旧行为。

原因: Host 在启动时解析了包。替换文件不会替换已加载的模块;包如果换了目录,甚至可能无法热重载。

设计回应: 区分 package-updated 和 host-applied。如果包移动了目录,要求 App restart,而不是假装 HMR 能跟上。

3. 工具调用进行中时被停用 ​

现象: 界面显示已停用,但运行中的任务还在使用工具;或者工具在一轮对话中途消失。

原因: Host 有活跃调用,Client 还持有旧工具列表。停用不是一个时间点事件。

设计回应: 明确停用是优雅还是立即。如果优雅,报告 pending 并排空调用;如果立即,显式取消并展示取消结果。

4. 应用运行时安装 ​

现象: 包已经在磁盘上,但直到重启都没有出现。

原因: profile 变了,但启动组合和 Client 模块图已经构建完成。

设计回应: 只在 profile 层变化时,不要说安装已经完成。应该说“已安装,需要重启”,或提供受控重载路径。

5. 重载执行到一半失败 ​

现象: 插件从列表消失了,但还在运行;或者还在运行,却不在列表里。

原因: 一个多步操作(卸载、删除、重写 patch、重载)在中间失败,系统进入没有按钮能描述的状态。

设计回应: 让操作可观察、可恢复。事务提交前保留上一状态。写出到达的层和错误回执。不要让用户猜哪一半成功了。

6. 两个生命周期操作重叠 ​

现象: 连续点击卸载,或在重启待处理时安装,产生未定义结果。

原因: 生命周期操作不一定是幂等的,第二个操作可能看到第一个操作执行到一半的状态。

设计回应: 按插件串行化。返回 busy 或排队结果。尽可能幂等,并在 UI 中显示当前操作。

7. 没有排空会话就重启 ​

现象: 重启修好了插件,但正在运行的一轮对话丢了,或会话日志不完整。

原因: App restart 是进程生命周期事件,不是插件重载。Host 可能在落盘前被杀掉。

设计回应: 走优雅关闭路径。任务运行中时拒绝或要求确认。重启后报告结果并重连页面。

能避免大部分问题的契约 ​

少量规则可以消除很多歧义:

  1. 一个 profile 只有一个所有者。 桌面外壳持有 profile 锁;CLI 操作要与它协调。
  2. 每个动作返回层。 saved、host-applied、client-reload-required、app-restart-required、failed。
  3. 用户可见层确认前,不报告成功。 写 profile 不等于完成卸载。
  4. 保留回执。 最近的操作、目标、层和错误应该能在页面刷新后看到。
  5. 把重启说清楚。 重启可以昂贵,但不应该意外。
  6. 测试状态,而不只是命令。 矩阵比退出码重要。

测试表 ​

场景ProfileHostClient期望的用户状态
应用运行中安装已更新旧旧需要重启
支持 HMR 的安装已更新已重载已重载已生效
删除时 Client 仍挂载已删除已卸载旧需要 Client reload
删除并重启应用已删除已卸载已卸载重启后生效
调用进行中停用不变排空中旧pending
更新且包移动了目录已更新旧旧需要 App restart
第一个操作未完成时再次操作不变busy不变busy

这张表故意很朴素。重点是每一行都有名字。

对插件作者意味着什么 ​

插件作者无法靠增加一个 dispose() 函数解决外壳的生命周期问题。但插件可以让契约更容易成立:

  • 避免留下比 unload 更长寿的后台资源;
  • 让 Host 和 Client 状态保持在独立模块;
  • 通过文档化的生命周期注册工具和 UI;
  • 不要假设“文件替换了”等于“代码重载了”;
  • 文档写清楚插件需要的最低重载级别。

生命周期仍由外壳拥有。插件不应该假装自己拥有它。

结尾 ​

桌面 Agent 不需要一个万能重载机制。它需要一套词汇,说明哪一层变了、哪一层还是旧的、用户接下来能做什么。

第一篇描述层,这一篇描述层之间如何不一致。下一步,是让这种不一致在界面中可见。

参考资料 ​

MIT 许可 · 使用 VitePress 构建