跳到主要内容

BioF3 系统健康审计(2026-06-07)

范围:全仓无死角扫描(r-server / py-server / platform-server / src / 部署 / 文档)。 方法:sub-agent 广度审计 + 主会话逐条验证(grep 确认行号、引用关系)。 触发:用户要求"系统无死角核对,看有什么需要优化整的"。

核心结论:项目结构整体清晰,但 Phase B(SQLite→PostgreSQL)迁移没收尾留下一类系统性隐患—— "写本地 SQLite / 读 PG" 的脑裂。我们上一轮修了 6 个 routes,但 agent-tools 全套 + py-server 数据桥 + task-manager 回写还没改,它们仍直写 Dell 本地 SQLite,与 PG 控制面数据分裂。

注意:当前 Dell 离线、生产 502,本报告只做记录与排期,任何修复都等 Dell 恢复 + 生产稳定后再动


P0 — 数据脑裂(必须修,影响数据正确性)

P0-A. agent-tools/* 全套绕过 PG(AI Agent 热路径)

  • 文件r-server/agent-tools/{act-tools,plan-ui-tools,learning,query-tools,verify-dispatch,index}.js
  • 现象:全部 const {db} = require('../db')(原始 better-sqlite3 句柄),读写 P2 表:copilot_plans / copilot_plan_steps / agent_audit / review_queue / user_progress / memory_vectors
  • 为什么是 P0:这些表在 Phase B 下由 routes(已用 getSqliteForLegacyModules(),PG shim)读写 PostgreSQL。但 AI Agent 实际执行动作走 dispatchTool → agent-tools/*,写的是 Dell 本地 SQLite。结果:Agent 建的 plan、写的 audit/undo token、限流计数、记忆、复习队列,在 PG 控制面读不到(反之亦然)server.js + routes/ai-agent.js 都接入这条热路径。
  • 修法:同上一轮 routes——模块级 const sqlite = dbMod.getSqliteForLegacyModules(),替换所有内部 {db}。注意 PG BOOLEAN(agent_audit.undone / copilot_settings.agent_paused)用 !! 包裹。
  • 验证:复用上轮的本地测试模式(注入假句柄 + .cjs 跑 PG 方言翻译)。

P0-B. py-server 直读 r-server 本地 biof3.db,无视 RDS

  • 文件py-server/r-server-db.js:14-31new Database('/opt/biof3-r-server/biof3.db') 硬编码本地路径)
  • 现象:写 tool_jobs(insert/update/delete)、读 users(配额/权限判断)、桥接写 datasets.meta_url,全走本地 SQLite。消费方:py-server/tools-router.jstask-queue.js
  • 为什么是 P0:Phase B 下 r-server 的 tool_jobs/users/datasets 已落 PostgreSQL。py-server 把同名 job 写本地副本 → /api/r/tools/*/api/py/tools/* 看到的状态/结果不一致;读 users过期快照(影响配额/权限)。py-server 已有 db-pg.js(PG Pool)却完全没用——明显是迁移遗漏。
  • 修法r-server-db.js 接入 db-pg.js,PG 模式下走 PG(与 r-server 对称)。这是 py-compute(空间/Python 工具)正确性的前提。

P1 — 正确性/可靠性/卫生(次优先)

P1-A. task-manager.js 回写 datasets 绕过 PG

  • r-server/task-manager.js:262-265_dbMod.db.prepare('UPDATE datasets...')
  • r-server/task-manager.js:458-465(更糟:new Database(BIOF3_DB_PATH) 重开连接直写本地)
  • 后果:worker 完成任务回写 meta_url/features_url 只改本地,PG 上 dataset 拿不到 → /single-cell/spatial 数据预览 tab 渲染不出 obs/var 表。

P1-B. routes/feedback.js 句柄退化

  • r-server/routes/feedback.js:14 const db = dbMod.db || dbMod(取原始 SQLite,非 PG shim)
  • feedback.js 在 P2_MODULES 内 → Phase B 下反馈落本地,admin 读 PG 看不到。

P1-C. r-pool-v2.js 孤儿文件

  • r-server/r-pool-v2.js(+ remote 镜像)全仓 0 引用(已 grep 确认)。
  • "开发完没接入主路径"的典型(健康度审计早标过)。确认后删除或归档,避免误导。

P1-D. ..r-server-remote/ 陈旧镜像已严重 drift

  • diff -rq r-server ..r-server-remote 有 102 处差异;remote 缺失整个 PG 层db-auth-pg.js/db-core-pg.js/db-p2*.js/db-pg.js 全无)。
  • 这是 Phase B 之前的版本。若某部署脚本指向它会回滚掉整个 PG 迁移
  • 处置:明确用途(它是 deploy.sh --pull 的同步镜像,用于"先 pull 再合并 server-side patch"流程),加 README 标注"deprecated 镜像,勿直接部署"。注意它已 gitignore,是本地工作流文件。

P1-E. 文件下载端点路径穿越(path traversal)

  • r-server/routes/tools.js:418-426py-server/tools-router.js:287-300 + :340-345(匿名 public-file,风险更高)
  • filename 来自 URL,未过滤 ..,直接 path.join + sendFile
  • 对比:py-server 上传时对文件名做了 replace(/[^a-zA-Z0-9._-]/g,'_'),下载侧没有等价防护。
  • 修法:path.basename(filename) 或校验 resolve 后仍在 output 目录内。

P1-F. OSS 上传失败被空 catch 吞掉

  • r-server/task-queue.js:230,241task-manager.js:421,433py-server/task-queue.js:208py-server/server.js:158
  • OSS put 失败时 results.files 少一个文件,任务仍标 done → 用户看到"成功"但下载不到图/对象,且日志无记录,排障难。至少补 logger.warn

P2 — 小问题(有空再清)

  • helpers/admin-gov.js:11helpers/site-snapshot.js:11 用原始 {db}(应统一 getSqliteForLegacyModules())。(注:helpers/methods-export.js 已正确用 isPg() 分支,是正面样板。)
  • routes/ai-memory.js 删除路径空 catch("清空记忆"可能静默部分失败)。
  • platform-server/server.js:7 端口 3020 vs 实际 3011(用户已知,生产靠 /etc/biof3.env 覆盖)。
  • py-server/r-server-db.js:16 path import 未使用。

正面项(审计确认做得对的,别动)

  • server.js / py-server/server.js 都注册了 unhandledRejection/uncaughtException
  • db-pg.js PG Pool 设了 connection/statement/idle 超时;LLM/summary 上游请求有 timeout+destroy。
  • py-server tools-router 各端点 jwtAuthRequired + owner/admin 校验完整。
  • 无发现"用户输入直拼 SQL"的真实注入点(均参数化)。
  • helpers/methods-export.js 的 PG/SQLite 双分支是正确写法的范本。

排期建议(与主线对齐)

这些全部属于"Phase B 迁移收尾",和我们正在做的"控制面迁回 ECS"是同一条线。建议顺序:

  1. 先恢复 Dell + 生产(当前 502,最高优先,见 DELL-RECOVERY-RUNBOOK.md
  2. 部署已修的 6 个 routes + ECS smoke(上一轮成果)
  3. P0-A agent-tools 脑裂(同模式批量改,是 Agent 正确性的关键)
  4. P0-B py-server 接 PG(py-compute 正确性前提)
  5. P1-A~B(task-manager / feedback 回写)
  6. P1-C~F(孤儿文件、remote 标注、path traversal、OSS 空 catch)
  7. P2 顺手清

统一元教训(已可写进 lessons):模块拆分 + 双后端迁移时,const {db} = require('../db') 是系统性反模式——它绕过 PG 切换层。应在 deploy-ops/architecture steering 里立一条强制规则:r-server 任何模块访问 P0/P2 表,必须用 getSqliteForLegacyModules()(或 async PG),禁止直接 require('../db').db。配一个 grep 检查(CI 或 hook)扫 require('../db') 的解构用法。

AI 陪学

让 AI 陪我学这一篇

AI 会读这篇文章后给你 3-5 步学习计划, 逐步陪你学完,最后出 1-3 道题验证你掌握得怎么样。 登录后 AI 才能记住你的进度。

静态文件

离线资料下载

手册 HTML / PDF 已在后台预生成,点击后直接下载网站静态资源。