scripts/restore_drill.py:把留档 PG 行按原文 \copy FROM 回填进目标库,核对每张表行数与清单、 抽样页面正文/标题逐字段比对,再用临时跟踪表 _drill_restored 精确删除并断言残留为 0。 实测(本地测试库 weknora_full,留档 20261009T041913Z-0b217e8a):13,539 行全部回填成功、 8 张表行数与清单逐一致、冲突 0、抽样页正文一致(388 字符)、残留 0。 同时记录一个格式事实:留档 JSONL 是 psql \copy text 格式(转义),按行 json.loads 会失败 (knowledges 3830/3830、wiki_pages 218/1982),回填/程序化读取须先按 text 格式反转义。
20 KiB
P6 生产发布 Worker 与回滚验收
结论
P6 的发布控制面、v0.7.1 三通道 Sidecar、Release Worker、归档、补偿和回滚代码已经完成,并通过本地真实依赖 E2E。
生产发布仍未授权、未配置、未启动、未执行:
PRODUCTION_RELEASE_REQUESTS_ENABLED=false
RELEASE_WORKER_ENABLED=false
persistent release_executions=0
persistent release_item_executions=0
production release jobs=0
因此本文的“通过”表示实现和隔离验收通过,不表示生产已发布。
范围
P5 sealed batch
→ 受保护的生产授权请求
→ ReleaseExecution + release.publish Job 原子入账
→ Release Worker 按 KB 路由
→ 从测试 WeKnora 导出经过验收的同一 Knowledge
→ PostgreSQL / Elasticsearch / 对象存储三通道发布
→ 生产 WeKnora 官方 API 验收
→ 成功记录 production revision
回滚:
已完成的 publish execution
→ 受保护的 rollback 授权
→ release.rollback Job
→ 载入旧 0600 release bundle
→ 三通道恢复
→ 官方 API 验收
首次发布没有旧 revision 时,失败补偿和回滚采用禁用而不是删除:
PostgreSQL Knowledge enable_status=disabled
PostgreSQL Chunks is_enabled=false
Elasticsearch 文档保留但 is_enabled=false
对象保留
官方 knowledge-scoped Search 必须零命中
人工授权与双门禁
发布请求门禁:
PRODUCTION_RELEASE_REQUESTS_ENABLED=false
执行门禁:
RELEASE_WORKER_ENABLED=false
两个门禁默认都关闭且职责不同:
- 请求门禁关闭时,授权 HTTP API 返回 403,Store 不会被调用。
- 执行门禁关闭时,Release Worker 不加载 release config、不读取生产 secret、不连接生产数据面。
- P5 的
sealed仍不创建 Job。 - 只有受保护的生产授权请求才会原子创建 ReleaseExecution 和 Job。
授权 HTTP API 不接受调用方提交的审核人身份。审核人来自服务端固定配置:
PRODUCTION_RELEASE_AUTHORIZER_REF
门禁开启时还必须提供普通、非软链接、权限 0600 的 Bearer token 文件:
PRODUCTION_RELEASE_AUTH_TOKEN_FILE
缺少固定 actor 或 token 文件时 API 拒绝启动。两条授权通道(2026-10-08 起):
- 运维通道:
Authorization: Bearer <token>,审计记PRODUCTION_RELEASE_AUTHORIZER_REF; - 页面通道:管理员会话 + 请求体里的固定确认语句(发布管理「授权发布 / 重排 / 回滚」按钮),审计记该管理员用户名。
UI 仍然不录入、不读取、不显示 token,也没有"一键生产发布":每次动作都要在页面逐字输入确认语句。请求门禁、备份恢复门禁与执行门禁三者不受通道选择影响——放宽"谁能调"不等于放宽"能不能跑"。
固定确认语句:
publish-to-production
rollback-production-release
retry-production-release
API
POST /api/v1/release-batches/{batch_id}/authorize-production
POST /api/v1/release-executions/{execution_id}/authorize-rollback
POST /api/v1/release-executions/{execution_id}/retry
GET /api/v1/release-capabilities
GET /api/v1/release-executions
GET /api/v1/release-executions/{execution_id}
授权响应和只读审计响应不返回:
- authorization token
- authorization_code
- bundle SHA
- WeKnora API key
- Elasticsearch / PostgreSQL / 对象存储凭据或 URL
- 测试 KB 内部连接信息
审计响应显示:
- publish / rollback
- pending / running / completed / failed
- 固定授权人
- 总项数、成功数、失败数
- 每项固定错误码
- production revision / previous production revision
- 开始和完成时间
Migration 7
新增:
release_executions
release_item_executions
约束:
- operation 只允许
publish/rollback。 - target_environment 固定为
production。 - state 使用固定枚举。
- publish execution 每个 sealed batch 唯一。
- rollback execution 每个 prior publish execution 唯一。
- Job 外键、逐项 Publication 外键和计数约束保留审计关联。
- Migration 7 不回填历史 release execution,也不自动创建 Job。
往返验收:
up 7 → down 6 → up 7
Migration 7 应用前备份:
tmp/backups/governance-pre-migration7-20260906T133742+0800.dump
bytes=263001
mode=0600
pg_restore catalog=valid
Release Worker 状态机
批次 Job 在 Worker 中按每批最多50项展开,并持久记录当前批次/总批次;批内逐项执行:
pending execution
→ running
→ 为 batch items 幂等建立 release_item_executions
→ 每个 item 执行 publish 或 rollback
→ verified / rolled_back / failed
→ execution completed 或 failed
行为:
- 一项永久失败会立即停止本项之后的同批项目与全部后续批次;未执行项记录固定
release_stopped_after_failure,随后仅对已验证成功项安排自动回滚。 - 可重试故障在重试次数耗尽前保留重试语义,不把尚未终态的项目误标为永久失败。
- 可重试失败在 Job 还有 attempt 时返回固定
release_retryable。 - 最后一次 attempt 仍失败时,逐项写固定错误码并收口,不留下永远 running 的 Execution。
- 外部响应正文、对象 key、路径、凭据和错误正文不写入治理库。
- Worker 重启后跳过已经 verified / rolled_back 的项。
- 终态failed execution可由同一Bearer门禁和固定
retry-production-release确认重排;只重置failed items,verified / rolled_back项保持完成,Job从completed/failed回pending并从attempt 0重新开始。 - publish重排前重新验证P5 sealed snapshot;过期审核或Publication变化时返回冲突。
- 未知 KB 不会落入默认适配器,而是
release_contract_mismatch。
v0.7.1 release bundle 契约
固定:
format weknora-release-bundle/v1
WeKnora v0.7.1
migration 74
retriever elasticsearch_v8
embedding dimension 1024
每个 bundle 绑定:
- ReleaseExecution ID
- Publication ID
- SourceItemVersion ID
- source revision
- parse revision(parser generation + index generation)
- release revision(封批时锁定的 review ID)
- tenant / KB / Knowledge ID
- embedding model identity
- Elasticsearch mapping SHA-256
- 各 PostgreSQL 表行数和 canonical SHA-256
- ES 文档数和 canonical SHA-256
- 资产数量、总字节和 canonical SHA-256
- 整包 SHA-256
任何 revision、模型、维度、KB、tenant、mapping 或 bundle digest 不一致都在目标写入前 fail-closed。
PostgreSQL 通道
只允许以下 v0.7.1 内容表:
knowledges
chunks
knowledge_tags
knowledge_tag_relations
resources
resource_bindings
没有读取或复制:
- users / sessions / messages
- tenants / memberships / permissions
- API keys / model provider credentials
- vector store connection credentials
- Jobs / audit logs / chat history
导出范围同时绑定:
tenant_id + knowledge_base_id + knowledge_id
目标导入:
- 必须是 migration 74 且 dirty=false。
- 使用 per-Knowledge PostgreSQL advisory transaction lock。
- 单 Knowledge 单事务替换。
- ID 在其他 tenant / KB 已存在时拒绝。
- Tag 定义和 relation 一起复制;共享 Tag 不删除。
- Resource stable handle 保留。
- production storage_backend_id、provider、storage URI 和 location_hash 按目标配置重写。
- Chunk seq_id 冲突时整个事务失败,不静默改 ID。
- 不调用 WeKnora 解析和 Embedding。
wiki 通道的归属门禁(2026-10-08 起)
WeKnora 的 wiki 图谱与页面同表(wiki_pages,节点/边靠 slug + in_links/out_links 表达),
所以"库级快照整库替换"在共享真实知识库上会把其他文档的页面和 agent 手写页一起抹掉。
导入端现在先判定归属,再决定写入范围:
- 归属来自页面
source_refs(形如<knowledge_id>|<doc_title>;chunk_refs只有 chunk UUID, 不带知识 id)。一页引用的知识全部属于本包才算InScope;引用包外知识的是外来页;完全没有 引用的页(index/log、agent 手写页)永不触碰。 - 目标库没有外来页 → 镜像库对原件的前提成立,保持整库替换语义(行为与旧实现一致,仍不删 "无引用且不在包内"的页)。
- 目标库存在外来页 → 自动降级为"只替换包内页集":按 slug 删除重建包内页、只删包内 folder id、
外来页上仅摘掉本包知识的引用(正文不动)、包内库级页(
index/log)不写以免毁掉目标库自己的聚合。 作用域以一行日志落盘:release wiki replace scope in_scope=… foreign=… foreign_ref_stripped=… untouched=… skipped_library=… degraded=…。 - 包内会写入的页与外来页同 slug → 拒绝发布(fail-closed),事务不落任何改动,错误信息点名该 slug。
- 撤权(
delete)与回滚走同一套判定(ReplaceWiki),不再整库清空。 - 已知取舍:降级路径上共享页的正文仍由目标库的库级合成结果决定,无法在不重算(不额外烧 token)的
前提下与本包一致;这类页只做引用级差集,并在作用域日志里计入
foreign_ref_stripped。
发布前备份(2026-10-08 起)
发布链本身不做发布前备份:DocumentTarget.Prepare 只归档本包、解析 previousRevision
(首次发布为 absent,此时回滚只能停用、无法恢复),PRODUCTION_BACKUP_RESTORE_VERIFIED
只是"备份/恢复能力已验证"的人工开关。目标库现场必须显式留档:
bash scripts/target_kb_snapshot.sh # 用 production_kb_bindings 里绑定的目标库
bash scripts/target_kb_snapshot.sh <kb_id> [<kb_id>] # 指定目标库
INCLUDE_CHUNKS=0 bash scripts/target_kb_snapshot.sh # 不导 chunks(快照很小,适合每次发布前)
输出 $KB_BACKUP_ROOT/target-kb-snapshots/<UTC>-<kb前8位>/(契约目录见
docs/PRODUCTION_BACKUP_RECOVERY.md;云内执行时先在云主机产出,再把小表与 ES 导出拉回本地):
- KB 维度行级 JSONL(
to_jsonb(row),含软删行,8 张表); es_documents.jsonl.gz+es_documents.jsonl.gz.manifest.json:该库在 ES 里的全部文档 (_id+_source,只读 scroll 导出,按_id可重建);MANIFEST.json(行数/字节/sha256/覆盖范围/每库 ES 文档数)+export.sql(原始查询)+README.md(恢复步骤)。
全过程只有 SELECT / _count / _search,不修改目标库与索引;backups/ 已在 .gitignore。
覆盖范围与边界:
- 覆盖:
knowledge_bases、knowledges、chunks(可选)、knowledge_tags、knowledge_tag_relations、resources、resource_bindings、wiki_pages、wiki_folders(PG 行), 以及 Elasticsearch 里该库的文档(含向量字段,按_id可重建索引)。 - 不覆盖:OSS 对象本体(
resources.jsonl只给路径/大小/哈希清单)、ES 索引的 settings/mappings、 列出的知识库之外的行、表结构。整库级恢复用阿里云 RDS 快照/按时间点恢复(PITR), 自管整库逻辑备份用scripts/logical-backup.sh。
约定:对共享真实知识库发布、撤权、回滚前必须先跑一次快照,并把快照目录与
MANIFEST.json 的 sha256 记进发布单;previous_production_revision=absent 时必须写明
"无前版可回退,只有这份快照"。
硬门禁(写目标库之前)
"必须有快照"已从人工约定升级为发布前置硬门禁(internal/releasesnapshot):
-
Worker 配置新增:
"target_snapshot": { "root": "/var/lib/knowledge-governance/target-kb-snapshots", "max_age_hours": 24, "policy": "mapped" }root由compose.release-sim.yaml以只读方式挂载快照目录;policy=mapped(默认) 只对源库 → 目标库映射的项强制(同 ID 目标的本地演练不受影响),always则一律强制, 且此时root缺失直接判定配置非法。 -
Publish/Delete/Rollback在任何写目标库的动作之前检查:存在覆盖目标库、 未过期(max_age_hours,默认 24h)、含wiki_pages.jsonl及其 sha256、**且含该库的 ES 文档留档 (es_documents.jsonl.gz+ 计数 > 0)**的快照;本地文件会再按哈希逐份校验 ("可恢复"的最低证明)。只留 PG 行、或 ES 计数为 0 的留档一律拒绝。任一条不满足—— 包括门禁本身未接线——都以release_target_snapshot_missing(不可重试)拒绝发布, 且不触碰目标库一行。页面把该码显示为"缺少目标知识库的发布前快照留档,已拒绝发布"。 -
回填演练:
python3 scripts/restore_drill.py --snapshot-dir <留档目录> --target-url <本地测试库>实测把留档 13,539 行按原文回填进测试库(8 张表行数与清单逐一致、冲突 0、抽样页面正文与标题 逐字段相同),核对后精确删除、残留 0。注意留档 JSONL 是 psql text 格式(\copy转义), 回填要用对称的\copy FROM,按行json.loads会失败。 -
云内执行:
bash scripts/target_kb_snapshot.sh默认在与 DB 同 VPC 的云主机上跑 (凭据取自已部署的 WeKnora 容器,不经家用带宽),PG 8 张表 44 秒;ES 导出走云主机直连 ES 容器(只读 scroll),两个库共 131,122 条文档、gzip 后 692 MB;全流程实测 2 分 52 秒(ES 字节留云主机),拉回本机则 19 分 40 秒(家用上行 ~0.6 MB/s)—— 故PULL_ES默认 0,清单以location=cloud:<主机>:<路径>记字节位置。 隧道模式保留为EXECUTION=tunnel兜底。
Elasticsearch 通道
源导出:
- tenant / KB / Knowledge 三重 term filter。
- 原样保存
_id、chunk文本、向量和其他_source字段。 - mapping 只计算
mappingscanonical SHA,不把索引名混入指纹。 - 导出后按
_idcanonical排序。 - 单 Knowledge 超过 10000 个 ES 文档时 fail-closed,不截断。
目标替换:
- 先 bulk upsert 新文档,
refresh=true。 - 再只删除同 tenant / KB / Knowledge 下、不属于新 release 的旧
_id。 - 读回数量和 canonical SHA。
- 失败时自动恢复 prior SearchSnapshot。
检索禁用使用 knowledge-scoped _update_by_query 将 is_enabled=false,不删除 ES 文档,并读回确认文档仍在且全部禁用。
对象存储通道
支持 S3-compatible MinIO / S3 / OSS:
- secret 仅从 0600 文件读取。
- 不在 URL 中携带 token。
- 支持 v0.7.1
storage://<backend-id>/<provider-path>。 - 拒绝 traversal、query、fragment、控制字符和越界 locator。
- 导出时核对 resource size / content SHA。
- staging:
.governance-release-stage/<release_revision>/<relative-key>。 - staging 写入后做完整流式 SHA-256。
- Copy 到 final 相同 relative key 后再次完整流式 SHA-256。
- ObjectStore 接口没有 Delete 方法。
- 回滚重传旧 bundle 中的资产;不删除新资产。
Release bundle 归档
每个 KB 使用独立目录:
<archive_root>/<knowledge_base_id>/
要求:
- archive root 和 KB 子目录是真实 0700 目录。
- 不允许目录或文件软链接。
- bundle 文件为普通 0600 文件。
- 原子写、文件 fsync、目录 fsync。
- 相同 release revision + 相同 SHA 可幂等重写。
- 相同 revision + 不同 SHA 为碰撞并拒绝。
- 加载时重验 manifest、PG、ES、资产和整包 SHA。
- 默认每 bundle 最大 1 GiB。
Bundle payload 可能包含对象 locator 和资产内容,因此不能进入 Web/API/普通日志;只有摘要进入治理库。
Release Worker 配置
模板:
config/release.v071.example.json
Runtime 配置文件必须:
- 使用绝对路径;
- 是普通文件;
- 不是软链接;
- 权限为 0600;
- JSON 不允许未知字段或尾随数据;
- 每个 KB 只允许一个 route;
- source 与 target 的数据库值、ES endpoint、对象 endpoint+bucket 必须不同;
- 所有数据库URL、ES token、对象AK/SK、WeKnora API key 只通过各自 0600 文件引用。
模板不能直接启用生产。启用前必须根据真实数据面生成独立受限配置和 Compose override,并由人工授权。
本地真实 E2E
精确 schema
使用:
ParadeDB PostgreSQL 17
WeKnora v0.7.1 migrations 000000..000074
source DB + target DB
结果:
source migration=74
目标 migration=74
合成 source:
knowledges=1
chunks=1
knowledge_tags=1
knowledge_tag_relations=1
resources=2
resource_bindings=1
通过:
TestPostgresExporterV071Integration
TestPostgresImporterV071ReplacesAndRestoresDocument
覆盖:
old → new → restore old
disable keeps 1 Knowledge + 1 Chunk
Knowledge=disabled
chunk enabled count=0
对象存储
使用本机已有的 disposable MinIO,无卷、loopback随机端口、合成凭据和合成bucket。
通过:
TestS3ObjectStoreIntegration
覆盖:
Open storage:// source
Put staging
streaming SHA Stat
Copy final
streaming SHA Stat
三通道发布与回滚
通过:
TestThreeLaneReleaseAndRollbackV071Integration
链路:
publish review-release-1
→ publish review-release-2
→ rollback review-release-1
→ target PG chunks=1
→ target ES has es-release
→ target two assets non-empty
说明:
- PostgreSQL 和 MinIO 为真实 disposable 服务。
- Elasticsearch 为状态化 HTTP contract server,真实执行 mapping/search/bulk/delete-by-query/update-by-query 协议和 digest 回读。
- WeKnora verifier 使用现有官方 client 接口 contract stub,真实执行 GetKnowledge/ListChunks/SearchKnowledge 的判断逻辑;没有连接生产 WeKnora。
- 所有 disposable 容器最终均已停止并由
--rm删除。
Worker装配
通过:
TestBuildReleaseRuntimeWithRestrictedFilesIntegration
验证:
两个真实 v0.7.1 DB连接
source/target secret文件0600
独立ES endpoint
独立对象bucket
独立官方WeKnora endpoint
KB archive 0700
RoutingPublisher构建
关闭时两个DB pool均释放
Web验收
Firefox Selenium只读验收:
review_candidates=5
release_batches=0
release_executions=0
production_request_gate=false
unauthorized_release_controls=0
截图:
tmp/P6_RELEASE_AUDIT_FIREFOX.png
可见:
- 左侧发布管理导航和顶部标签。
- P5审核候选与封批记录。
- P6生产执行审计。
- 生产请求门禁关闭。
- Release Worker保持关闭。
- 当前无生产执行记录。
- 页面没有生产授权按钮。
当前持久治理库
schema migrations=1,2,3,4,5,6,7
publication_reviews=0
release_batches=0
release_batch_items=0
release_executions=0
release_item_executions=0
release publish/rollback jobs=0
现有 5 条候选仍为知识工程师业务待审,未由测试代替人工批准。
生产前仍需人工完成
以下不属于本地自动验收,也未执行:
- 明确批准生产发布窗口。
- 对测试和生产各选一份同ID真实Knowledge,做只读PG/ES/OSS依赖盘点。
- 确认两端均是WeKnora v0.7.1 migration 74、1024维、同模型和同mapping。
- 为source创建只读数据面凭据。
- 为target创建仅允许约定KB内容表、ES索引和对象bucket的最小写权限凭据。
- 创建0600 release config和secret文件,并让Release Worker独占挂载。
- 先做1份无敏感canary。
- 官方API读取、Chunks、检索、资产和审计全部通过后再扩批。
- 保留Release Worker关闭状态,直到上述材料由人工核对。
不得因为本文完成而自动执行生产发布、创建生产Job、连接生产数据面、删除任何WeKnora Knowledge或对象。