适用当前 PanelAgent 0.2.0。库存、仪器、染料、行内质量和 panel 历史现在共用一个实验室 SQLite 数据库。迁移脚本位于源码仓库的 scripts/migrate_quality_registry.py;它不是 pa 的子命令,使用 Python 标准库即可运行。
以下路径都是示例,执行前应替换为自己的备份或迁移工作副本。本文编写和验证只使用临时数据库,没有读取或修改真实实验室数据。
数据库选择顺序为 pa --db PATH、PANELAGENT_DB、~/.local/share/panelagent/panelagent.db。直接启动 FastAPI 时没有 CLI 参数,使用后两项。建议指定绝对路径,避免相对路径随启动目录变化。
- 后端启动只初始化选中的数据库;全部内核数据表为空时才导入包内 seed。已有 seed 不会被启动过程覆盖,库存不会从仓库 CSV 自动导入。
pa web向 Next、后端和 MCP 注入同一个数据库路径;新库可以自动初始化。pa serve当前要求数据库已存在且通过 schema 检查,随后启动后端。- 默认启动、库存查询和历史 API 不会搜索、读取、迁移或删除旧 admin 数据库和旧质量 JSON,也不会导入迁移脚本。没有迁移时,新历史页可能为空,旧历史仍留在原文件中。
- 旧历史库通常位于旧安装目录的
data/admin_console.sqlite3;旧版使用PANELAGENT_DATA_DIR时,位置为<PANELAGENT_DATA_DIR>/data/admin_console.sqlite3。新版不再把此变量作为后端数据库路径。 - 旧质量文件通常位于旧服务工作目录的
data/quality_registry/issues.json。同目录中的audit/和projections/保留归档用途,当前后端不再使用它们作为质量事实来源。 - 若主动把
--db或PANELAGENT_DB指向旧 admin 库,该文件就成为选中的运行库,启动会给它添加内核 schema/seed。这与默认自动发现不同;本指南建议使用独立迁移副本,保留原库归档。
当前质量视图来自 antibodies.quality_flag / quality_notes / quality_updated_at。旧 issue、审核、候选绑定、审计 API 返回 410;旧 admin 质量路由仍先检查管理员 session。公共库存读取与管理员编辑分别为:
GET /api/v1/inventory?library=Mouse&flag=warn
PATCH /api/v1/admin/antibodies/{id}/quality
{"flag":"warn","note":"需要重新滴定"}
PATCH 的 flag 为 good、warn、bad 或 null;note 为字符串或 null;两个字段都要提供。清空质量使用 {"flag":null,"note":null}。
先停止对源库和目标库的业务写入,并保留原始 CSV、质量 JSON、audit 目录及历史库。SQLite WAL 模式下,不能只复制正在使用的主 .db 文件而忽略尚未 checkpoint 的 WAL。可以用 SQLite backup API 生成独立快照;以下命令拒绝覆盖已有备份路径,也不会创建缺失的源数据库:
python3 - /srv/old/data/admin_console.sqlite3 /srv/backups/admin-before-migration.db <<'PY'
import sqlite3
import sys
from contextlib import closing
from pathlib import Path
source = Path(sys.argv[1]).expanduser().resolve()
backup = Path(sys.argv[2]).expanduser().resolve()
with closing(sqlite3.connect(source.as_uri() + "?mode=ro", uri=True)) as src:
backup.parent.mkdir(parents=True, exist_ok=True)
backup.touch(exist_ok=False)
with closing(sqlite3.connect(backup)) as dst:
src.backup(dst)
result = dst.execute("PRAGMA integrity_check").fetchone()[0]
if result != "ok":
raise RuntimeError(result)
print("Backup integrity: ok")
PY对已有目标实验室库也做一次同样的备份,指定另一个新文件名。备份失败时不要将产生的文件视为有效备份;检查错误后换新路径重新执行。JSON 和 audit 文件应在停止旧服务写入后一起归档。
迁移脚本的 dry-run 使用 SQLite mode=ro,不会修改业务行或执行 schema 写入;对于 WAL 数据库,只读连接仍可能需要共享内存/sidecar 文件。需要保留原文件现场时,对上面生成的离线快照进行检查和迁移,而不是直接使用原库。
脚本的 --db 必填,目标文件必须已经存在并含有 antibodies 表;脚本不会替你初始化内核,也不会从 PANELAGENT_DB 自动取值。
只在全新目标库上用当前 CLI 初始化和导入:
pa init --db /srv/panelagent/migration/lab.db \
--csv Mouse=/srv/backups/mouse.csv \
--csv Controls=/srv/backups/controls.csv
pa library list --db /srv/panelagent/migration/lab.db --json
pa instrument list --db /srv/panelagent/migration/lab.db --json
pa antibody list --db /srv/panelagent/migration/lab.db --library Mouse --json--csv 可重复,格式是 Library=PATH,库名不限定物种。pa init 还支持 --config-dir DIR 和 --inventory-dir DIR;后者按 CSV 文件名推断库名,识别不了的文件会警告并跳过。任意库优先使用显式 --csv。
注意:显式 pa init 每次都会执行 seed upsert,与后端“仅空库 seed”的启动策略不同。不要为了迁移旧质量/历史而在已有实验室库上反复执行 pa init,否则自定义 seed 值可能被包内值覆盖。已有目标库需要补库存时,可以通过 Web 上传 CSV/XLSX,Form 为 file 和 library;有效库名去掉首尾空白后必须为 1–128 字符,与 CLI/MCP 一致,超长返回 422。省略 library 时,文件名作为库名,也受该限制。重导相同库存身份更新原行,保留质量字段。XLSX 使用 openpyxl,旧 XLS 使用后端依赖 xlrd>=2.0.1。
迁移时,旧 species 对应新 library。请先确保目标库名一致;脚本没有 --library-map 参数。
在源码仓库根目录执行,或将脚本路径改为绝对路径:
python3 scripts/migrate_quality_registry.py --help
python3 scripts/migrate_quality_registry.py \
--db /srv/panelagent/migration/lab.db \
--issues /srv/backups/quality_registry/issues.json \
--history-db /srv/backups/admin-before-migration.db \
> /srv/panelagent/migration/dry-run.json当前脚本只有 --db、--issues、--history-db、--apply 和 --help。--issues 与 --history-db 至少提供一个;可只迁质量或只迁历史。不提供 --apply 就是 dry-run,没有单独的 --dry-run 参数。
正常生成的 JSON 包含:
| 字段 | 含义 |
|---|---|
mode |
dry-run 或 apply |
quality |
可迁移质量记录,含输入 index、原始 record、目标 antibody_id、flag、note |
history |
可导入的完整历史行 |
manual |
未自动处理的项目和原因;质量项目保留原始记录/候选 ID,历史项目保留历史 ID/原行 |
退出码 0 表示计划生成/执行完成且没有 manual 项;2 表示存在 manual 项。参数用法错误也返回 2,应结合 stderr 区分。输入文件、schema 或 SQLite 操作错误会报异常并以非零状态退出,不会伪装成完整成功报告。
报告可能包含实验记录和质量备注,应与数据库备份同等保管。
脚本按 library + target + clone 查找,只有唯一目标行才能迁移;即使 fluorochrome、brand 或 catalog 看起来能缩小范围,也不会擅自替你选择。
- 身份优先读取
entity_key,其次feedback_key,最后读取记录自身。 - library 支持旧
species,clone 支持clone_name/clone;库名和 clone 精确匹配。 - target 支持
target/marker/normalized_marker,匹配时转小写并去掉非英文字母数字。该规则不自动合并 CD8、CD8a、CD8b;旧规范化已丢失的信息需要人工核对。 - flag 优先使用
quality_flag,其次flag;没有显式 flag 的未 resolved issue 提议为warn,不会推断成good或bad。status=resolved且没有显式 flag 的记录转人工处理。 - note 优先使用
quality_notes,其次note,再其次issue_text。 - 缺少库名/target/clone、找不到抗体、匹配多行、无效 flag/note 均进入
manual。 - 多条旧记录指向同一抗体时,全部进入
manual,不自动去重、截断或覆盖。 - 目标行的任意质量字段(包括
quality_updated_at)已有值时,不覆盖该行。即使 flag/note 已清空,只要保留了更新时间,仍需人工确认。
人工处理应核对原始记录、library、clone 和候选 ID,使用有鉴权的质量 PATCH 明确编辑目标行;或编辑输入 JSON 的副本后重新 dry-run。不要为了消除报告而删除原始 issue/audit 文件。历史 issue 的审核和审计事件不会变成新的质量行记录,原始归档负责保留这些细节。
确认可迁移列表后,使用相同输入增加 --apply:
python3 scripts/migrate_quality_registry.py \
--db /srv/panelagent/migration/lab.db \
--issues /srv/backups/quality_registry/issues.json \
--history-db /srv/backups/admin-before-migration.db \
--apply > /srv/panelagent/migration/applied.json存在 manual 项不会阻止其他可迁移项写入。 因而 --apply 退出码为 2 时,目标库可能已经成功写入 quality/history 列表;不要将非零码等同于零写入。
应用时使用事务写入;如果计划生成后质量字段已变化,或历史 ID 插入冲突,会报错并回滚该次事务的数据写入。应在暂停业务写入的窗口执行,随后重新 dry-run 核对。
迁移后可以读取库存与历史确认结果:
pa antibody list --db /srv/panelagent/migration/lab.db --library Mouse --flag warn --json
pa db stats --db /srv/panelagent/migration/lab.db --json
pa serve --db /srv/panelagent/migration/lab.db --host 127.0.0.1 --port 8000后端起来后,另一个终端读取:
curl --fail http://127.0.0.1:8000/api/v1/panel-historyGET /panel-history 保持 {items,total},详情为 {item}。其中 total 延续旧行为,是本次分页返回的条数,不是整个表的总数。pa db stats 统计的是内核表,不包含后端 panel_history,不能用它核对历史数量。
--history-db 只读取旧库的 panel_history 表,不读取旧 llm_settings,不迁移凭据或运行配置。保留历史 ID、时间、species、inventory_file、markers、selected_panel、rationale、model_name、api_base;目标库不存在历史表时,在 apply 时创建。
同 ID 且字段相同的历史记录报告 already present;同 ID 内容不同报告 history ID conflict,两者都进入 manual,不自动覆盖。不同 ID 的同内容历史不会自动去重。
质量迁移重复执行时,已写入的行因质量字段非空而进入 manual,不重复覆盖;历史因已有 ID 而不重复插入。这是避免重复数据/覆盖的幂等性,不是“每次运行都退出 0”:重复执行通常会返回 2,需要阅读报告。
没有 --force、自动覆盖或自动删除源库选项。完成核对后仍保留原库、原 JSON/audit、备份和迁移报告。旧历史未迁移时只是当前 UI 不可见,不是被删除;显式导入后才能在新库历史 API 中查询。
上一轮完整回归为 302 passed。Git HEAD a3449fd 可复现收集 319 项;在该临时快照仅替换为当前 17 项的 tests/core/test_mcp.py 后,可复现收集 335 项(原文件 1 项,增加 16 项)。这说明 335 包含未提交 MCP 用例,不能直接与 Git HEAD 总数比较;此重建核对的是收集口径,不冒充原运行时源码快照。
按这套 335 收集口径,变动账目为:
| 类别 | 变化 | 原因 |
|---|---|---|
| 旧质量 API/持久化/投影/边界/LLM 测试重写 | -76 | 退休 issue 审核、绑定、审计文件、去重和 top-five 投影;改验行内质量、401/410、库隔离、清空和 bad 排除 |
| 其他后端新增/调整 | +17 | SQLite 集成 +12、迁移 +4、LLM env +3、仓库静态路径测试转 SQLite 后 -2 |
| CLI/contracts/Web runtime 用例 | +26 | 独立 root/CLI 交付的增量;不由后端修改 |
| 合计 | -33 | 335 → 302 |
质量重写净值来自:test_quality_registry 28→8、test_quality_registry_update 7→9、test_quality_registry_store 30→23、test_quality_projection 15→5、test_quality_edge_cases 41→8、test_quality_e2e_integration 4→1、test_panel_evaluate_quality 5→3、test_panel_recommend_quality 4→1。管理员边界 7 项、质量 schema 30 项、formatter 15 项保留;没有新增 skip/xfail 或通过排除文件来获得通过结果。
当前验收对应关系:
| 验收行为 | 测试 |
|---|---|
| flag/note/时间戳持久化、重建 store、空值、只改指定行、无效 flag | tests/test_quality_registry_store.py、tests/api/test_quality_registry_update.py |
| 库/marker 隔离、NULL target、修改与清空即时反映 | tests/test_quality_projection.py |
| 401 鉴权、session/TTL、退休 API 410、公开质量读取 | tests/api/test_admin_auth.py、tests/api/test_quality_admin_boundary.py、tests/api/test_quality_registry.py |
| 中文/emoji/控制字符、上下文预算、稳定排序、不同 reagent 保持独立 | tests/test_quality_edge_cases.py、tests/test_quality_context_formatter.py |
| SQLite 质量进入 LLM、没有备注时没有质量块、不串库 | tests/test_panel_evaluate_quality.py、tests/test_panel_recommend_quality.py |
| 默认排除 bad、include_bad 覆盖、清空后恢复 | tests/test_quality_e2e_integration.py |
| CSV/XLSX、任意库、分页、未知亮度、多仪器、WAL 并发、历史手动保存 | tests/api/test_kernel_integration.py |
| dry-run、歧义/重复记录、旧历史冲突、源文件保留、默认启动不访问旧库 | tests/test_quality_migration.py |
本轮核对又增加 5 项回归:默认/显式新库启动分别不访问旧库 2 项、无质量注释时 LLM 上下文为空 2 项、行上下文稳定排序与 reagent 区分 1 项;并加强已有 Unicode 断言及 health/OpenAPI 包版本一致性断言。旧 issue 的“文件读取失败时静默忽略质量”不再是验收语义:SQLite 是事实来源,不能把读取失败伪装成没有质量问题。
分发边界另加 3 项上传测试:Form 库名超长拒绝、默认文件名库名超长拒绝、去空白后恰好 128 字符可用。health 和 OpenAPI 版本都来自 Settings.VERSION 引用的 panelagent.__version__,不再独立硬编码。