feat(release): 批量发布/建批即锁定 + 需下架逐条移除 + 数据源勾选改造(23 提交) #2
Reference in New Issue
Block a user
Delete Branch "feat/release-batch-and-monitor"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
范围
分支
feat/release-batch-and-monitor,23 个提交、80 个文件,来自本地 main 上尚未推送的开发成果。分组:
需下架判据用了没人写的状态(恒为 0);重建 api 会静默关掉生产门禁
docs/OPERATIONS_RUNBOOK.md、docs/USER_FLOWS_ACCEPTANCE.md及验收记录node_modules/.vite/.../results.json与 8 个历史scripts/__pycache__/*.pyc,并补上根级/node_modules/忽略规则安全与数据边界
db/migrations/变更)compose.yaml(+13 行)与scripts/deploy-api-web.sh有改动 ⇒ 按规范 §1.2 属部署相关,需异人评审提交信息闸门
已按 CI 同一条规则逐条预检本分支全部提交:全部合规(类型/长度/结尾句号)。
真跑过的测试
make verify未由我复跑;CI(go build/vet/test+npm ci && build)是本次的证据来源。尚未验证
internal/platform/postgres/*_integration_test.go等)在 runner 上是否依赖数据库,未确认明确声明
未合并、未部署、生产数据库未改动。 合并与部署是分开的动作,且由人决定。
CI 结果(回填,实测)
63db2b8d1e7c上的检查:build / build (push)= success(Successful in 24s)http://192.168.50.223:3000/Weknora/knowledge-governance/actions/runs/35/jobs/50mergeable=True表示已满足。go build ./.../go vet ./.../go test ./...与前端npm ci && npm run build在该 runner 上通过;此前担心的集成测试依赖数据库问题未发生(runner 上未因缺库而失败)。
界面验收实测(2026-10-10):把已发布到生产的 PDF 改为「禁止导入」后,发布通道既给不出 delete、也建不了 delete 批次。 根因:禁止导入只写 source_items.policy_state='blocked',而"期望策略"的解析只看 source_item_scope_overrides / directory_scope_rules / source_items.visibility—— 两处实现都不看 policy_state: 1) release_plan.go 的 source_truth:被禁止且生产侧存在的条目被算成 unchanged/present、 can_release=false → 永远不会出现 delete 动作; 2) release_batches.go 的 releaseDeleteReadyCTEs.source_truth:同样口径 → 计划即使给了 delete,创建封批仍被拒 422(实测原文「创建封批失败(HTTP 422)」)。 修复:两处都把 policy_state='blocked' 算进期望策略(各一行,注释写明成因)。 新增集成测试 release_plan_policy_state_integration_test.go:用 policy_state(真实路由的写法) 造"生产存在 + 已禁止"的条目,断言①计划给出 action=delete/can_release、②delete 批次可创建; 分别回退任一处修复,该测试即 FAIL(已实测两种回退都 FAIL)。 实测闭环:重新发布使生产存在(enable_status=enabled)→ 禁止导入 → 计划 delete(present/ can_release=true)→ v8 delete_count=1 → 批次 rb-8c1ad1c9…(sealed, action=delete) → 留档 20261010T033100Z + 门禁 PASS → 执行 rx-6c19232a… completed(项级 verified/delete、 production_revision=absent)→ 目标库该行 enable_status=disabled(行保留、检索停用、可恢复)。 附带:从未导入却被标为禁止的 82 条"等待测试"噪音不再占发布计划(195 → 113 项)。 证据与说明补进 docs/USER_FLOWS_ACCEPTANCE.md(C2b + F.3h)。背景:来源侧被判「禁止导入」后,已建好的测试库知识不会自动消失;此前只有变更集 删除路径会入队 test.remove,页面既列不出条目也没有按钮,运维无法闭环(车道只有按 库计数)。 新增: - GET /api/v1/test-publications?removal=required:条目级列表,判据与车道计数同源 (superseded_at IS NULL / 有 knowledge_id / completed / retrieval_state NOT IN (disabling,disabled) / policy_state='blocked'),过滤是叠加而不是替换。 - POST /api/v1/test-publications/{id}/removal:engineer 级写路由,要求 Idempotency-Key, 按条目幂等(operator-remove:<id>);置 disabling 并交既有 test.remove 工作流去删除 远端知识并读回校验。错误映射 404/409(已移除)/409(不可移除)/501(未接线)。 - 界面:需下架车道逐条显示条目 + 「下架(从测试库移除)」,页内二次确认,成功提示, 完成后计数归零。 修掉过程中踩到的两个真问题: 1. 二次确认的确认态原先经容器 props 传给视图,一次容器改动把三个 props 丢了 —— 视图永远渲染原按钮、确认步骤静默消失,而所有静态单元测试仍然通过。改为把确认态 放在视图内部;并把「取消」放在原按钮位置(真实指针连点两下只会落到取消)。 2. jobs 上有部分唯一索引 UNIQUE (business_change_set_id, business_version_id, business_target_kb_id, business_action) WHERE business_change_set_id IS NOT NULL。 本路径给这三列写空串(非 NULL),('','','','remove') 对所有条目是同一个键 —— 第一条之后每次 INSERT 都撞唯一索引、被 ON CONFLICT DO NOTHING 静默吞掉,留下 「状态已 disabling、没有任务」的永久卡死条目,而车道恰好隐藏 disabling,运维也 看不到。改为写 NULL(语义:无变更集身份)+ 冲突目标收敛到 idempotency_key + 校验 RowsAffected()(0 行回查既有任务;查不到就报错,不提交无任务的 disabling)。 卡死状态在重试时可自愈。 测试:internal/platform/postgres/test_publication_removal_integration_test.go(两条发布 各自入队、business_* 为 NULL、卡死自愈;把修复退回旧写法该测试即 FAIL,已实测)、 internal/httpapi/test_publication_removal_test.go(权限/幂等键/错误映射/未接线 501/列表 过滤)、前端 testPublications.test.ts + WeKnoraMonitor.test.tsx。 验收(纯界面,两条都走完):解析监控 → 需下架(2)→ 逐条「下架」+ 二次确认 → retrieval_state=disabled(PDF 03:57:03Z、md 04:08:55Z),jobs completed(attempt 2, 首次 test_remove_pending=远端仍在处理,重试通过;工作流以 GetKnowledge 读回 404 判 成功)→ 车道需下架(0)。核验「确认前不执行」:首次点击后仍 unpublished、移除任务数仍 1。写操作的服务端一直回 {"error": "<原因>"},但 getJSON/sendJSON 只把状态码拼进错误消息, 运维看到的是「创建封批失败(HTTP 422)」,无从下手。实测真实原因只有两种,且都有明确 下一步:所选知识不在已冻结的发布清单里(先「生成新版本」+「确认冻结」)、或所选知识当前 不满足发布条件。现在把原因带出,并对这两种已知原因直接给出下一步;其他原因原样展示。 背景(本次 422 的根因):ATOM群体广播控制方案使用方法.adoc 的批准时间 06:07:05Z 晚于 已冻结版本 v8 的冻结时间 03:23:48Z,v8 的 113 行里没有它的行,绑定校验因此不通过。 发布计划页显示的是实时计划(所以写着"可加入发布批次"),建批绑定的是已冻结版本——两者 不一致时必须先重新生成版本,这条顺带值得在产品上更醒目地提示。 测试:reviewRelease.test.ts 补 2 条(覆盖清单不足时给出下一步 / 未知原因原样带出)。背景:发布管理把"逐条核验"的仪式强加给每一份文档。实测一份文档要 9~11 次交互, 且有两处顺序陷阱(冻结后批准 → 建批 422;生成即固化 → 批准被永久钉住)。 P0-1 建批时冻结: - CreateReleaseBatch 在未指定版本时,自己把当前实时计划冻结成一份**已确认**版本并绑定 (同一事务;同摘要复用最近版本,planned 或 confirmed 都可复用,因此同一份计划不堆版本)。 - 语义不变:批一旦建立,清单/摘要/验收轮次/目标库绑定全部不可变、回滚按它走;变的只是 "谁在什么时候按固化键"——从操作员的手动两步变成建批这一下。 - 效果:去掉「生成新版本」「确认冻结」两步;消灭"批准晚于冻结 → 422"这类顺序陷阱; 前端不再传版本 id,也不再有"所选知识不在冻结清单里"的阻断。 - 计划→清单项的映射抽成 ChangeSetItemsFromPlan 供两处共用,否则两处算出的摘要不同, "同一计划复用同一版本"永远不命中。 P0-2 验收通过自动批准: - 验收三项结论达发布门槛(knowledge_exists/chunks_present=passed,retrieval_hit 与门槛同 口径)时,由 worker 写入 reviewer_ref='system:auto' 的批准记录,替代"每份文档点一次批准"。 - 策略 TEST_AUTO_REVIEW_POLICY = off | internal(默认,仅对内)| all;值不认识拒绝启动。 只影响审核这一步:生产授权、双闸与发布前快照门禁不受影响。界面上只读展示当前策略。 - 安全边界(写死在审核 SQL 里):系统只允许写"第一条"审核——人工一旦表过态,机器不得用一条 更晚的 approved 把人工结论翻回去;人工随时可再审。集成测试去掉该约束即 FAIL(已实测)。 - 自动批准自身的任何失败都不影响验收结论,只上报。 顺带(P1-1 前半,已被 P0-1 取代的部分已删除): - 写操作失败把服务端 {"error": ...} 的原因带出来,并对已知原因给出下一步(不再只给 HTTP 状态码)。 - 当前的"不在已冻结清单里"标注与就地修复按钮随 P0-1 一起移除(该失败模式已不存在)。 测试:bind_time_freeze_integration_test.go(绑定已确认版本/同计划复用/冻结后不可变/ 动作不符仍 NotEligible)、auto_review_integration_test.go(系统只写第一条、人工可再审)、 validate_test.go(对内自动/对外不自动/策略关闭/验收未过不批/自身失败不影响验收)、 auto_review_test.go、config_test 及前端能力解析测试。 实测耗时:本机 Go 全量 26 包通过、前端 178 条通过。用户的反馈是对的:上一轮只砍掉了"冻结/锁定"两个按钮,但一条文档从"已批准"到"发出"实际仍要 ~9 次交互(找条目→勾选→填名→建批→滚到清单→找批次→授权→输语句→提交),因为"批次仪式"本身 还留在路径上。 - 新端点 POST /api/v1/release-batches/publish-now:在同一操作里先建批(SealImmediately,冻结 即锁定)再授权生产发布,返回 {batch, execution}。 - 不放松任何约束:请求门禁(403)、备份恢复验证门禁(412)、管理员身份(401)、固定确认语句(400)、 发布前快照硬门禁(409)、建批门禁(422) 全部沿用既有实现,无旁路;名称可省(服务端固定回退名, 保证重放指纹一致)。 - 界面:审核行上直接「发布到生产」(管理员 + 门禁开启时显示),工具栏支持选中多条一次发布; 复用既有确认弹窗(固定语句)。批次名不再是必经步骤。 - 实测(面板):点行级按钮 → 输 `publish-to-production` → 提交 → 批次 rb-bee80550…(sealed) + 执行 rx-3d831129…(completed, 项级 verified) → 页面回滚 rx-43835fac…(rolled_back, absent)。 测试:publish_now_test.go(四道关卡 + 组合调用 + 名称回退)与 reviewRelease.test.ts(请求体/回传/错误原因); Go 26 包、前端 183 条全过。Pull request closed