姚章浩 eabe55b432 fix(release): wiki 校验与写入的归属口径不一致导致跨环境发布必然失败
端到端验收实测(2026-10-10):文件型文档(docx)跨环境发布时,写入侧按设计保留了
目标库外来源 wiki 页(日志 in_scope=0 foreign=8 untouched=1 degraded=true),
但校验侧 VerifyWiki 仍要求"生产页 slug 集合与包内快照完全相等",必然误判,项级
报 release_verification_failed,批次随后被判失败单项失败。

- verifier.go: 归属判定与写入侧 replaceWiki 共用 packageWikiOwners;
  校验改为"快照内页必须都存在 + 多出来的页只有在不属于本包时才放行",
  保留原有牙齿:本包拥有却缺失/残留的页仍判失败;撤权仍禁止任何页引用被撤权文档
- wiki_channel_test.go: 用例改为 外来源多余页放行 / 本包拥有残留页拒绝 / 缺页拒绝
- 实测:同一批次重排后完成(done=1/1 item=verified 生产可用),回滚 rolled_back
  这也是 wiki 归属门禁上线后第一次真实跨环境发布
2026-10-10 10:22:06 +08:00

知识同步治理平台

独立的知识治理控制平面。当前包含P1持久任务队列、P2统一来源与PP历史接管、P3本地/测试WeKnora编排验收、P4来源只读扫描、P5知识工程师审核与封批门禁、默认关闭的P6三通道生产发布与回滚Worker,以及P7持久快照ChangeSet、真实运行监控和每50项一批的发布进度。

安全边界

  • 本工程不修改 WeKnora、PP、AList、钉钉或 DobotAI 源码。
  • PP接管与核验只读取指定的本地账本、manifest和测试WeKnora;不请求PP写接口,不修改来源文件。
  • 普通Worker领取演示任务、test.upload/test.poll/test.validate和显式启用的source.scan.pp;只有普通Worker可挂载测试WeKnora Key和只读PP快照。
  • Release Worker默认RELEASE_WORKER_ENABLED=false,禁用时不读取release config/生产secret,不连接生产数据面或治理数据库。
  • P5封批只生成治理侧sealed记录,不创建生产发布Job;5条真实候选仍为待人工审核。
  • P6生产请求另受PRODUCTION_RELEASE_REQUESTS_ENABLED=false保护;开启时还必须有服务端固定actor和0600 Bearer token文件。当前未授权、未配置、未发布生产。
  • 凭据文件位于secrets/且被Git忽略;仓库只保留示例文件。

目录

cmd/api                 HTTP API
cmd/worker              普通任务Worker
cmd/release-worker      默认关闭的发布Worker
cmd/pp-ledger-import    PP历史账本一次性导入工具
internal/jobs           任务模型、租约和Worker循环
internal/governance     统一来源治理领域模型
internal/platform       PostgreSQL仓库与Migration
internal/releasebundle  v0.7.1发布bundle契约与摘要
internal/releaseadapter v0.7.1 PostgreSQL/ES/S3 Sidecar
internal/httpapi        HTTP路由
web                     React TypeScript状态页
deploy                   Nginx配置
docs                    验收计划

P2.1来源数据底座

Migration 2创建五张空表:

source_connections      来源连接配置,只保存secret引用名
source_items            来源内稳定对象身份与变化状态
content_objects         按完整SHA-256唯一的内容对象
source_item_versions    来源对象版本与内容对象关联
test_publications       测试知识库导入、处理、检索状态

source_change、ingest_state、processing_state和retrieval_state相互独立。只有同一来源连接内的stable_external_id唯一;只有完整小写SHA-256相同才能共享内容对象。

P2.1不会自动创建来源连接、读取PP账本或上传文档。完整验收步骤见docs/P2_SOURCE_FOUNDATION_ACCEPTANCE.md。

P2.2 PP历史账本接管

一次性工具将PP append-only账本折叠为当前终态:

1093 条历史事件
→ 559 个来源对象和版本
→ 530 个SHA内容对象
→ 530 条历史测试发布
→ 29 个content_alias共享canonical内容和Knowledge ID

75条旧completed记录缺少SHA时,只允许从locator hash匹配的本地manifest恢复,并重新校验文件大小和实际SHA-256。单文件继续遵守旧连接器50MiB上限。连接器默认关闭,历史发布的retrieval_state保持unpublished,等待未来对目标WeKnora逐条只读核验。

工具只执行幂等INSERT,不执行UPDATE或DELETE。完整验收步骤见docs/P2_PP_LEDGER_TAKEOVER_ACCEPTANCE.md。

P2.3 PP完整清单与知识资产页

P2.3将manifest中未进入旧账本的54个超限来源纳入治理视图:

559 个历史终态 + 54 个over_limit阻断项 = 613 个来源对象
530 个内容对象
613 个来源版本
530 条历史测试发布
历史已同步内容计划重新上传数 = 0

超限版本没有虚假ContentObject或TestPublication,保持content_state=over_limit和阻断状态。GET /api/v1/source-items提供只读分页清单,支持state=completed|content_alias|over_limit、limit和offset;响应不包含来源URL、URI哈希、raw记录或Knowledge ID。Web“知识资产”模块读取该真实API。完整验收见docs/P2_PP_INVENTORY_ACCEPTANCE.md。

P3 WeKnora测试编排

普通Worker从只读Spool流式上传到测试WeKnora,持久保存远端身份,按单调poll_sequence轮询,并在完成后执行Knowledge/Chunk验收。API和Web只返回安全投影;人工重试只允许失败或停滞且仍有已验证制品的Publication。530条PP历史Knowledge已在测试服务器完成在线只读核验,没有重新上传。

本地、测试服务器、失败恢复、Worker重启和浏览器验收见docs/P3_LOCAL_AND_CROSS_ENV_ACCEPTANCE.md。

P4.1 来源连接只读工作台

GET /api/v1/source-connections返回来源ID、类型、名称、启用状态、是否配置调度、健康枚举、对象数和更新时间。查询与响应均不包含base_url、secret_ref或原始健康错误正文。Web“来源管理”模块展示真实PostgreSQL数据,保留左侧导航和顶部多标签。

P4.1完整验收见docs/P41_SOURCE_CONNECTIONS_ACCEPTANCE.md。

P4.2 PP只读扫描与变化入账

普通Worker在PP_SCAN_ENABLED=true且三个安全路径完整时注册source.scan.pp Handler。Compose仅给普通Worker只读挂载PP数据目录;API、Web和Release Worker均不挂载。Scanner拒绝越界、软链接、非普通文件和不完整manifest;54个历史over_limit项允许其制品未下载,但仍保持阻断。

Migration 5新增source_scans和source_scan_changes。触发Scan与创建Job原子执行,每个连接最多一个活动Scan;完成时聚合、变化与来源健康原子写入;失败只保存固定错误码。安全API支持触发、扫描历史和连接作用域详情,Web展示真实聚合与变化记录,不返回来源ID/revision/SHA/路径/URL或凭据。

真实PP快照只读扫描结果:

observed=613
unchanged=559
over_limit=54
added=0
updated=0
missing=0
planned_uploads=0

过期running租约已由常驻Worker真实重领并以attempt=2完成;并发第二次触发返回409。完整迁移、备份、恢复、API和Firefox验收见docs/P42_SOURCE_SCAN_ACCEPTANCE.md。

P5 知识工程师审核与封批

Migration 6新增追加式publication_reviews、release_batches和release_batch_items。审核决定绑定最新Acceptance run与Publication时间戳;批准仅允许完整结构门禁通过的候选,retrieval_hit=manual_required保持人工判断。创建draft和sealed封批均原子验证整个项目集合,任一项目失效则整批拒绝。

“发布管理”模块展示真实审核候选、固定门禁、决定可见性、草稿批次和封批记录,并明确显示“生产门禁保持关闭”“Release Worker保持关闭”“尚未发布”。当前535条Publication中,530条无逐条Acceptance的PP legacy不自动进入人工队列;5条manual合成验收项显示为ready/pending,持久库没有代替知识工程师创建Review或Batch。

P5 Store与HTTP写链路已在disposable PostgreSQL和临时API中验证pending→approved→draft→sealed,完成后临时数据库与临时密钥文件均清理;持久环境只应用空Migration 6。完整验收见docs/P5_REVIEW_AND_SEAL_ACCEPTANCE.md。

P6 生产发布Worker与回滚

Migration 7新增release_executions与release_item_executions。只有受保护的生产授权请求才能把P5 sealed batch原子转换为release.publish Job;rollback只允许针对完整成功的publish execution。终态失败可在同一Bearer门禁下重排,仅重置failed items并重新验证sealed snapshot。API和Release Worker使用两个独立默认关闭的门禁,UI只展示执行审计,不提供一键生产授权按钮。

Release Worker按Knowledge Base精确路由,固定WeKnora v0.7.1 / migration 74 / elasticsearch_v8 / 1024维契约;同一release package协调复制以下内容且不触发生产解析/Embedding:

PostgreSQL: knowledges/chunks/knowledge_tags/relations/resources/bindings
对象存储:  referenced assets,staging→SHA校验→相同relative key
Elasticsearch: chunk text+vectors,先upsert新文档再清理同Knowledge旧_id

每个release revision写入0700目录中的0600 bundle并重验整包SHA。失败恢复旧bundle;首次发布无旧revision时禁用Knowledge/Chunk/ES,不删除行、文档或对象。生产WeKnora官方API最终验证Knowledge、Chunk数和knowledge-scoped检索。

本地真实验收使用完整v0.7.1 75个migration的source/target PostgreSQL、真实MinIO、状态化ES协议服务,已通过publish v1 → publish v2 → rollback v1以及PG disable、S3 Open/Put/Copy、Worker受限配置装配。模板见config/release.v071.example.json,完整边界与结果见docs/P6_RELEASE_WORKER_ACCEPTANCE.md。

当前生产状态保持:

PRODUCTION_RELEASE_REQUESTS_ENABLED=false
RELEASE_WORKER_ENABLED=false
release executions/items/jobs=0/0/0

服务配置在哪配(四项)

「系统设置 → 服务配置」只保留四项,其他都不在这页:

项 含义 校验 谁来写
测试地址 控制台探活的测试 WeKnora 地址 裸 http(s) 源地址 仅管理员
生产地址 控制台探活的生产 WeKnora 地址 裸 http(s) 源地址 仅管理员
生产 ID 发布契约里的租户 ID 十进制整数(空 = 未配置);保存时真查它是否就是该知识库的租户 仅管理员
知识库 ID 生产知识库标识 不含空格、/、?、#、\(空 = 未配置);保存时必须在该地址上真实存在 仅管理员

一次点「保存并启用服务配置」四项一起提交:服务端先格式校验,再写版本化的活动设置(候选 → 激活 → 旧版本保留审计),网页只录裸地址、绝不夹带凭据。四项里任何一项相对活动版本发生变化都要管理员角色, 普通工程师提交会被 403(只改刷新参数不受影响)。留空表示未配置,界面如实显示「未配置」。

  • 地址探活:后端按地址真打 /health,显示「端点可达 / 不可达 · 未做接单验证」;测试地址留空时回落到 容器 env TEST_WEKNORA_BASE_URL,生产地址回落到 PRODUCTION_WEKNORA_BASE_URL(可选 PRODUCTION_WEKNORA_API_KEY_FILE、PRODUCTION_WEKNORA_ALLOW_DOCKER_HTTP=false)。探活只打 /health,从不读写知识库、从不参与发布,所以生产端点抖动不会阻断测试流水线。
  • 两个 ID 的真校验(保存时执行):保存/激活服务配置时,后端拿「生产地址 + 生产 API Key 文件」 调 WeKnora 列库接口(GET /api/v1/knowledge-bases/{id},回落到 GET /api/v1/knowledge-bases 列租户)真查一次:
    • 查到 → 通过,界面显示知识库名与观测到的租户(verification.knowledge_base_name / observed_tenant_id,服务端只读,从不写回配置);
    • 端点明确回答「没有这个知识库」或租户与该库不符 → 拒绝保存(HTTP 400 + 原话),一行都不写;
    • 端点抖动、超时、401/403、未配置生产凭据 → 只警告不拦(响应里带 verification.status = unreachable / unauthorized / cannot_verify),保存照常落地——运维不能因为端点故障就改不了地址。 真校验需要 api 容器能读到生产凭据:PRODUCTION_WEKNORA_API_KEY_FILE 指向 0600 文件(值永不进网页、 永不进响应);留空时该项显示「未核验:没有可用的生产 API Key 文件」,其余行为不变。 激活前会再查一次(候选可以躺几天,库可能已被删);字段与线上一致的回放不重复核验,幂等重试不受端点影响。 覆盖率要点:租户校验只在「该库的 tenant_id」这一条路径上成立;只填生产 ID 时按列库结果的租户去比对。
  • 排列组合之外的东西(依赖探活矩阵、对内/对外知识库绑定下拉)已从这页摘掉;依赖状态在总览页, 绑定数据与 API 仍在(internal/httpapi/routing_bindings.go,迁移 000034)。

发布目标与凭据仍然是另一件事,刻意不进网页:

  1. 发布通道的目标(唯一真源,Release Worker 读它):RELEASE_CONFIG_FILE 指向的 0600 JSON 里 routes[].target.weknora.base_url(同级 api_key_file),模板见 config/release.v071.example.json。 硬约束:绝对路径、0600 普通文件、严格 JSON(多字段即报错)、source/target 不能是同一库或同一桶。 凭据与发布目标刻意不进网页、不回读——它只在管理员受限的运行时配置里。

所以:网页上配的地址管「控制台看到的真实状态」,真正发单时的目标仍由发布运行时配置决定。改了一处却 忘了另一处时,控制台会显示旧地址的真实状态——这是提示,不是故障。

本地验证

要求Go 1.26、Node.js 22和npm。

make test
make vet
make build
make web-test
make web-typecheck
make web-build

Docker Compose(由操作者执行)

先创建仅限本机的密钥文件:

cp secrets/postgres_password.example secrets/postgres_password
cp secrets/pgpass.example secrets/pgpass
# 将两个文件中的CHANGE_ME_LOCAL_ONLY替换为同一随机本地密码
chmod 600 secrets/postgres_password secrets/pgpass
docker compose up --build -d

Compose默认让三个Go进程以1000:1000运行,以读取宿主机Owner为1000且权限为600的密钥文件。若操作者UID/GID不同,启动前设置APP_UID和APP_GID为密钥文件Owner。

访问:

  • 状态页:http://127.0.0.1:8088/
  • 同源健康检查:http://127.0.0.1:8088/health

停止时保留数据库:

docker compose down

不要执行docker compose down -v,除非明确决定删除本地数据库数据。

P1 API

创建普通演示任务:

curl -fsS -X POST http://127.0.0.1:8088/api/v1/jobs \
  -H 'Content-Type: application/json' \
  -d '{"type":"demo","payload":{},"idempotency_key":"demo:001"}'

创建可用于租约恢复验收的30秒任务:

curl -fsS -X POST http://127.0.0.1:8088/api/v1/jobs \
  -H 'Content-Type: application/json' \
  -d '{"type":"demo.sleep","payload":{"sleep_ms":30000},"idempotency_key":"demo:sleep:001"}'

查看最近任务:

curl -fsS 'http://127.0.0.1:8088/api/v1/jobs?limit=50'

完整人工步骤见docs/P1_ACCEPTANCE_TEST_PLAN.md。

S
Description
No description provided
Readme 34 MiB
0.1.0 Latest
2026-10-13 16:50:11 +08:00
Languages
Go 65.7%
TypeScript 21%
Python 7.7%
CSS 2.2%
Shell 2%
Other 1.2%