这一版没有新功能,全是「把自己绊了一跤的地方填平」——从一次线上事故的根因,到三个被门禁当场抓出来的问题。
启动卡在「步骤 4/8」:offline 怎么把主线程堵死的
本地部署时日志停在「步骤 4/8: 初始化规则引擎… 完成」永不继续,Web 端口从不上线。进程还活着、CPU 却只有 0.4%,采样栈显示主线程阻塞在一次文件读取上——它根本没在干活,在等磁盘。
真凶是一处看似合理的写法:
if background and not offline:走后台加载,else:走同步加载。
offline=True(用本地缓存、不联网,最常见的配置)恰好让整个条件为假,于是「后台加载」被跳过,主线程被按在地上读模型权重(实测超过 50 秒)。
问题出在把两件正交的事绑成了一个条件:background 管的是「要不要阻塞启动」,offline 管的是「要不要联网」。离线与否,跟能不能异步,毫无关系。现在后台加载与离线解耦:启动秒回,模型照旧在后台约 10 秒就绪,Web 不再被它挡住。
教训:两个语义独立的开关,绝不能用逻辑运算符绑成同一个分支。这不是编译器能拦的错误,只能靠人读代码时多问一句。
首页「决策追踪」停在旧消息
后端按时间正序返回决策,前端组件却直接取前 N 条——于是卡片上永远是最旧的两条。现在排序下沉到组件内部统一处理,最新消息始终在最上方,「意图 & 路由」页也一并受益。
顺带一个概念澄清:决策追踪记录的是「AI 对每条消息做了什么决策」,不是聊天记录。人工回复的消息本就不产生决策记录——某天没有决策,不代表哪里坏了。
安全:三件事
- 关闭鉴权不再裸奔:调试时关掉
auth_enabled且未配凭据,此前即使服务绑定了对外地址,写操作与配置导出仍会完全无认证放行。现在这种情况直接拒绝(fail-closed);仅本机 / 私网使用时维持原行为,不影响免鉴权调试与内网反代。 - 管理口令从明文升级为 PBKDF2 哈希:项目本已内置 PBKDF2 支持,只是存量配置从未迁移,现提供迁移工具一键升级。校验函数对明文与哈希双兼容,登录行为完全不变;迁移前自动备份,明文全程不落盘不打印。
- 消除 3 条依赖告警:
urllib3升级到 2.8.0,消掉 2 高危(分块解码无界内存耗尽、HTTPS 代理 TLS 配置被忽略)与 1 中危(分块解压死循环)。排查时发现一个容易误判的坑:requirements.lock早已是安全版本,但 CI 实际消费的是uv.lock里的漏洞版本——两份锁文件现已对齐。
把「静默吞噬」变成「可追溯」
项目内 640 处宽异常中,有 85 处既不记日志也不重新抛出——异常彻底消失,排障时看不到任何线索(上面那个启动卡死被另一条噪音告警淹没,就是活生生的例子)。现在这 85 处全部补上带完整堆栈的日志,并新增 AST 门禁把总数锁定为「只减不增」,防止回潮。
这里有个反直觉的结论:这 640 处宽异常,绝大多数是正确的防御性容错(配置解析失败即回退默认值;日志格式化失败绝不能崩)。所以本版本刻意没有做机械收窄——强行收窄反而会把「可容错」变成「会崩」。真正要治的不是数量,而是静默吞噬。
性能:每次回复最多省 400 次查询
少样本推荐原先对每条候选回复单独查一次「前一条用户消息」,候选多时每次回复产生最多 400 次查询。改为一批取回 + 二分定位后,N+1 降为单次查询,返回值完全不变。
生产事故复盘:索引损坏
此前一轮「持续报 database is locked」的根因既不是并发也不是参数,而是数据文件索引损坏:某分库的一个索引有 49 行缺失,致每次写入退化为全表扫描重建索引、写锁被长期占用,闸门与限流全部失效。现在启动时会检测并自动重建损坏索引(幂等、不动数据),上线后该错误归零。
由此定了一条硬规则:「代码改完还是坏」=数据库文件损坏的强信号,此时应立刻查 integrity_check,而不是继续在代码里找原因(注意 quick_check 对索引不一致会漏检)。
工程门禁
- ruff 覆盖范围从 38% 扩到全量:此前只扫
src/,web/、tests/、scripts/全在门外——一个真实的 Web 崩溃就是这么漏掉的。 - CI 补并发控制与超时:连续推送不再叠加完整作业;job 挂死不会永久占用 runner。
- 依赖一致性门禁首次真正生效:本次即拦下一次自动合并带来的 10 项版本漂移。
升级注意
无破坏性变更、无新增必填配置。数据库索引自愈与孤儿图片回收参数均自动生效,无需手工迁移。部署建议改用单进程模式:python scripts/run_linkora.py --single-process,Web 与后台轮询同驻一进程,从根上消除 SQLite 跨进程写锁争用(此前该问题曾表现为持续的 database is locked 与轮询丢会话)。