这一版没有新功能。它是一次全量缺陷审计的收口——把 Web 管理台、工具执行、技能系统、轮询主链路、平台适配、意图路由、LLM、记忆这些核心子系统逐个过了一遍,修了 10 类确凿缺陷。

有意思的是,这些缺陷大多是同一类形态:平时跑得好好的,特定条件下才发作。它们不会在开发时立刻报错,而是在生产环境累积到某个时刻突然爆掉。

本文要点

  • python -O 会把 assert 整条剥离,8 处类型收窄在生产静默失效
  • 3 个只读方法没关游标,累积到把写入拖垮——症状与真正的元凶完全无关
  • 工具卡住会拖慢所有并发会话,现补120 秒超时护栏
  • 注销令牌黑名单只增不减;frontmatter 切分实际有 3 处不一致

python -O 启动时,assert 会被整条剥离

代码里有 8 处用 assert 做类型收窄——写法本身没问题,运行时也确实有效。但生产环境常用 python -O 启动(跳过断言以省开销),而 -O 会把 assert 整条剥离。于是本该返回数据库记录 ID 的地方静默返回 None,调用方拿着 None 去拼 SQL,直接崩。

开发环境跑一年都不会发现,因为断言还在。这 8 处已全部改为运行时收窄——它在类型层面等价、但不会被 -O 剥离。

教训:assert 适合「开发期自检」,不适合承担「生产期的类型收窄」。后者一旦被优化掉,错误就从「明确」变成「静默」。

读游标没关干净,最后拖垮的是写入

高频轮询路径上有 3 个读方法没关游标。单看每一条都不严重——游标最终会被垃圾回收。可这个进程是永不退出的轮询进程,天长日久,WAL 读快照不断累积,最终触发写锁阻塞。

这类问题的阴险之处在于:报错的地方(database is locked)和出问题的地方(三个只读方法)隔着十万八千里。而且它和 v0.6.0 修的那次索引损坏事故症状一模一样——所以排查时很容易被带偏,去查锁参数、加限流,而真正的源头是三个忘了关的游标。

工具卡住会拖慢所有人

工具执行原先没有外层超时。一个工具卡住,调用它的线程就一直等——而工具是并发共享的,意味着一个卡死的工具会连带拖慢其它所有会话。

现在加了统一超时护栏,默认 120 秒。这个数字是算出来的:内部子进程的硬超时是 60 秒,留一倍余量,既能挡住真挂死,又不会误杀合法的慢工具。新增配置项 tools.max_tool_seconds,设为 0 可关闭护栏。

顺带补齐了参数校验:缺必填参数、参数类型不对,现在返回明确错误,而不是抛出让人摸不着头脑的内部异常。

注销之后,旧令牌仍然有效(且黑名单无限增长)

令牌黑名单是个只增不减的集合——每次注销往里塞一条,但过期的令牌从不清理。长时间运行会持续累积。更值得一提的是它的设计意图:黑名单本就是为了让「注销」这个动作真正生效,但它自己却需要人工介入才不会烂掉。

现在按令牌过期时间记录,校验时惰性清理。行为完全不变,但不再无限增长。

卡片里的图片路径,被当成了文字写进正文

处理图文混排消息时,「给正文加本地图片路径」的逻辑对所有消息类型都生效。但卡片消息里的图片字段内部是一段 JSON 映射,不是路径——于是被原样拼进正文,产生「本地图片 /xxx/yyy」这种指向不存在文件的假路径。

已把该逻辑限定为只对单图消息生效。修复前后的差别很直观:改动前后的消息正文里会多出一行莫名其妙的「本地图片」。

三处 frontmatter 切分方式不一致

技能文件(SKILL.md)的元信息写在文件头的 --- 分隔线之间。读取时用行级切分(只看「整行就是 ---」),但有 3 处写回逻辑用的是字符串查找——在文件里找下一个 --- 就完事。

两者在正常文件上结果一致,但只要 frontmatter 里某个值内部含 ---(说明文字里带代码片段、Markdown 分隔线之类),字符串查找就会先命中值里那个,切分错位,正文被吞进元信息。

三处已全部收敛到行级切分,和读取路径同一个真源。附带一个有意思的发现:三处都不是一处——最初只记了一处,实际扫出来三处(AI 意图写回、技能安装写平台、Web 端改元数据)。典型的「以为修完了,其实漏了两处」。

门禁

升级注意

无破坏性变更、无新增必填配置、无数据迁移。新增可选配置 tools.max_tool_seconds(默认 120 秒),一般无需调整。

延伸阅读