封存入口闭环:实例级窗口接线 + 独立目标同步/恢复(0.1.180) #11

Open
zheromiku wants to merge 0 commits from feat/library-capture-instance-window into main
Owner

做了什么

封存入口接线(不再用库级窗口跑实例)

  • libraryctl capture/recover 改由 librarymaintenance.Manager 装配:入口闸门、后台静默检查、
    目标指纹、入口探针与发布链路同一套实现。SIGTERM exit=1 不再等于"请求被截断",
    排空证据来自闸门 drain 记录(校验没有放松,补的是缺的那层证据)。
  • 维护声明收敛到 internal/librarymaintenance/env.go(LIBRARY_TARGET_* 唯一解析处),
    缺项在开窗口前拒绝并点名;agent 启动如实打印接线状态;安装脚本强制 0600 agent.env。
  • 新增 -source-s3-endpoint:容器里的 minio 服务名宿主机解析不到,资源导出会失败。

接线后暴露并修掉的四个真缺陷

  1. POST /library/captures 队列作业 id 45 字符 > jobs.id varchar(36) → 一律 500;
  2. v0.7.1 的 app 写索引不带 tenant_id,只按 term 过滤 → 导出 0 条、候选校验报 legacy coverage;
  3. 全新实例的空索引({} 映射)被当成"索引被改坏" → 首次同步失败;
  4. 恢复作业 id 带 . → 维护窗口 invalid freeze request。
    另外:同步资源行时定位符里的桶要从来源桶换到目标桶(否则目标取不到图/附件),换桶变换写入与校验共用。

真实验收(本机专用实例,未动共享栈/远程生产)

  • 排空:先在途(闸门 conn:open active=1)→ 再发起封存 → 入口关闭 → 补完正文 → 应用 200;
    drain.json phase=drained active_at_close=1 served=126,quiesce.samples=18 stable_ms=1617。
  • 封存:快照 1196c518…(撤销后 KB:9 Wiki 页 / 1 文档 / 2 分块 / 1 资源),与源逐页比对一致。
  • 同步:独立实例 kgseal(自己的 PG/索引/桶)libsync-internal-1793226278273693890 succeeded + verify succeeded。
  • 恢复:人为偏离后 librestore-internal-1793226571284097366-retry succeeded,逐项读回与快照一致。
  • Neo4j 启用场景:真实 KB graph_enabled=true → CLI 预检 exit 1 Neo4j graph snapshot is not supported。

CI:最终 HEAD 之前每个代码提交都有 run;211 failure(密钥扫描误报,下一提交修)、215 failure(gofmt)、
218 cancelled(被后续提交取消)如实保留,其余 success。make verify 15 项全绿。

详见交付报告 release-capture-close-loop-result.md(本机 handoffs)。

## 做了什么 **封存入口接线(不再用库级窗口跑实例)** - `libraryctl capture/recover` 改由 `librarymaintenance.Manager` 装配:入口闸门、后台静默检查、 目标指纹、入口探针与发布链路同一套实现。SIGTERM `exit=1` 不再等于"请求被截断", 排空证据来自闸门 drain 记录(校验没有放松,补的是缺的那层证据)。 - 维护声明收敛到 `internal/librarymaintenance/env.go`(`LIBRARY_TARGET_*` 唯一解析处), 缺项在开窗口前拒绝并点名;agent 启动如实打印接线状态;安装脚本强制 0600 `agent.env`。 - 新增 `-source-s3-endpoint`:容器里的 minio 服务名宿主机解析不到,资源导出会失败。 **接线后暴露并修掉的四个真缺陷** 1. `POST /library/captures` 队列作业 id 45 字符 > `jobs.id varchar(36)` → 一律 500; 2. v0.7.1 的 app 写索引不带 `tenant_id`,只按 term 过滤 → 导出 0 条、候选校验报 legacy coverage; 3. 全新实例的空索引(`{}` 映射)被当成"索引被改坏" → 首次同步失败; 4. 恢复作业 id 带 `.` → 维护窗口 `invalid freeze request`。 另外:同步资源行时定位符里的桶要从来源桶换到目标桶(否则目标取不到图/附件),换桶变换写入与校验共用。 ## 真实验收(本机专用实例,未动共享栈/远程生产) - 排空:先在途(闸门 `conn:open active=1`)→ 再发起封存 → 入口关闭 → 补完正文 → 应用 200; `drain.json phase=drained active_at_close=1 served=126`,`quiesce.samples=18 stable_ms=1617`。 - 封存:快照 `1196c518…`(撤销后 KB:9 Wiki 页 / 1 文档 / 2 分块 / 1 资源),与源逐页比对一致。 - 同步:独立实例 kgseal(自己的 PG/索引/桶)`libsync-internal-1793226278273693890` succeeded + verify succeeded。 - 恢复:人为偏离后 `librestore-internal-1793226571284097366-retry` succeeded,逐项读回与快照一致。 - Neo4j 启用场景:真实 KB `graph_enabled=true` → CLI 预检 exit 1 `Neo4j graph snapshot is not supported`。 CI:最终 HEAD 之前每个代码提交都有 run;211 failure(密钥扫描误报,下一提交修)、215 failure(gofmt)、 218 cancelled(被后续提交取消)如实保留,其余 success。`make verify` 15 项全绿。 详见交付报告 `release-capture-close-loop-result.md`(本机 handoffs)。
zheromiku added 21 commits 2026-10-29 06:36:17 +08:00
为什么:删除一篇与别人共同贡献同一 Wiki 页的文档后,页面还在、名字也在,
只有那篇文档的正文与引用被摘掉。原来的页面级差异会把这种语义重算显示成
"无变化",操作员据此会误判撤销没有生效。

怎么改:librarydiff 新增 CompareWiki,以 slug 为身份把正文/来源文档/引用分块/
出入边/目录分别取集合比较,输出 sources_removed、citations 变化与具体类别;
overview 的 diff.wiki 增加逐页明细(最多 50 页,超出置 pages_truncated),
说明文字明确"重算发生在源侧,本平台只如实比对与保留结果"。

页面:新增目标维护面板(实例级/库级、持有、入口开关、阶段、进度、恢复错误),
只有窗口仍持有且作业失败时才给"按预备份恢复";没有维护记录时写未接入 + fail closed。
变更页把 Wiki 逐页差异列出来源/引用怎么变。kind 补上 restore。

验过:go test ./internal/librarydiff ./internal/httpapi;web vitest 453 项;
make verify 全绿(15 项,无未运行项)。真实撤销语义验收见交付报告。
为什么:专用验收实例用 S3 兼容的 MinIO,而 CLI 的源发现只认 oss 环境变量,
`STORAGE_TYPE=minio` 直接失败;同时 CLI 只读 routing bindings,API 读
LIBRARY_PRODUCTION_KB_*,两边指向不同知识库时,"封存的是哪个库"就说不清。

怎么改:新增 objectStoreFromSourceEnv(oss/minio,端点/桶缺失即报错,不退回默认值),
scopeBindings 让环境变量优先、bindings 兜底。单测覆盖 minio http/https、oss 与未知类型。
上一提交只改了 status;capture/preflight 仍直接读 routing bindings,
所以 CLI 封存的还是 bindings 里的库,而不是 API 的目标——契约读取直接失败。
共享业务栈与专用验收实例共用同一个 WeKnora 镜像,原来只按镜像判"未声明写入者",
任何一台开着共享栈的机器都永远开不了窗。判据改为:同项目或无项目标签即冲突;
另一个项目只有数据库/队列目标与源相同才算冲突。单测覆盖六种组合。
`docker ps -q` 默认输出 12 位短 ID,与 declared 里的 64 位 ID 永不相等,
结果连已声明的写入者都被判成"未声明的并发写入者",封存永远开不了窗。
改用 --no-trunc 并按前缀双向匹配。
矩阵不再在顶部叠报告:已完成 / 缺代码 / 已实现未跑 / 需授权四段分开,
把本轮 Wiki 撤销真实验收(12 项检查)、贡献级差异、维护面板写进对应行,
并如实记下封存链路缺口(CLI 无入口排空证明、API 封存未接线)、
死信无解除契约、Neo4j 场景与排空探针未跑。

新增正式 8088 待验收部署方案:迁移 067–072、loopback 配置差异、
缺配即拒绝的 fail-closed 规则、回滚与账号数据保留。只准备,未执行。
0.1.166(1faf513)是本轮验收代码 SHA;f82eb07 起的文档提交不改代码。
0.1.165 因后续提交被 CI 取消,按事实记为 cancelled,不改写成成功。
库级窗口永远拿不到在途排空证据:被代理的 WeKnora 收到 SIGTERM 就 exit(1),
写者退出码不能当作请求已排空。capture/recover 改由 librarymaintenance.Manager
装配入口闸门、后台静默检查、目标指纹与入口探针(与发布链路同一套实现),
排空证据来自闸门 drain 记录;缺 LIBRARY_TARGET_* 时在开窗口前拒绝并点名。

维护接线解析收敛到 internal/librarymaintenance/env.go,agent 与 worker 讲同一组
变量名;agent 启动如实打印接线状态;安装脚本强制 0600 agent.env。

试过:make verify 15 项全绿;新增 7 个用例(接线解析/身份绑定/缺闸门拒绝)。
真实 v0.7.1 的 app 自己写索引时不带 tenant_id,治理侧同步写入时才补齐。
原来只按 term tenant_id 过滤,等于把 app 写入的正文整库漏掉:真跑时索引里有
2 条文档,导出 0 条,候选校验报 legacy coverage,封存被拒。

改为"显式本租户 或 完全没写租户字段"(写了别的租户仍然排除),知识库身份
仍是独立 term,范围没有放宽。Count/PruneScope/整库导出共用同一写法。

试过:make verify 15 项全绿;新增 3 个过滤条件用例;真实 preflight 由
candidate-invalid(0 条) 变为 candidate-valid(2 条)。
真实请求 POST /api/v1/library/captures 一律 500:拼出来的
"library-capture-<scope>-<nano>" 是 45 个字符,而 jobs.id 是 varchar(36),
PG 直接拒绝(value too long)。此前页面一直停在"未接线",这条路径从没被
真实请求覆盖过,所以只有接上 agent 之后才暴露。

改为 libcap-<scope>-<nano>(35 字符),并用测试锁住列宽与形状。

试过:go test ./internal/httpapi -run LibraryCapture 全绿;修复前用真实
DATABASE_URL 复现 enqueue error: value too long for type character varying(36)。
容器 env 里的 MINIO_ENDPOINT 往往是服务名(langfuse-minio:9000),封存跑在宿主机上
解析不到,真实封存报 read library resource 并整体失败。
新增 -source-s3-endpoint:只换访问入口,桶与对象键不变;缺 scheme 直接拒绝;
status 会打印实际使用的 endpoint。

试过:make verify 15 项全绿;新增覆盖用例(生效 + 拒绝无 scheme)。
上一提交只给 libraryctl 加了 -source-s3-endpoint,agent 却不知道这个 flag,
systemd 起不来(exit 2)。端点属于"这台主机怎么访问这个源的存储",必须由 agent
自己钉住并随固定声明传给 libraryctl;请求仍然不能改。没配置时不凭空加 flag。

试过:make verify 15 项全绿;新增 agent 声明用例(有端点必传、无端点不多传)。
style(libraryagent): gofmt
build / build (push) Successful in 1m14s
84e8a0dfdc
上一个提交漏跑 gofmt,本地门禁 format 失败;只改空白。
全新 WeKnora 会用空映射({})先建好索引,范围字段等第一次写入才动态生成。
首次同步在读目标当前状态时就报 "missing release scope fields" 并失败,等于
"往空的生产实例做第一次同步"这条路根本走不通。

现在:"没有范围字段 + 索引确实 0 篇文档" = 空基线;有文档却看不出归属仍然报错
(绝不把无法归属的文档当空库覆盖)。Count 与整库导出共用同一判断。

试过:make verify 15 项全绿;新增空/非空两种索引的用例;真实 sync 复现原错误。
同步把资源字节写进目标桶,但 resources.physical_path 是从快照原样搬的,
里面还写着来源桶(storage://…/minio://kgmaint-1028a/…)。目标实例之后按这一行
取图/附件就会指向测试环境的桶,或直接取不到——跨环境引用,必须在本轮修掉:
写入前换桶、键不变,定位符解析不出来就报错(不写指向别处的行)。

试过:make verify 15 项全绿;新增定位符用例(嵌套/单层/裸键/非法);
真实同步后目标桶里对象键与行内定位符一致。
fix(v071): 资源换桶的变换在校验时同用
build / build (push) Successful in 1m17s
b061a9686c
只改写入侧会让"行内容摘要"对不上:校验拿变换前的归档行与目标里的变换后行比较,
真实同步在 verify 阶段报"生产 resources 内容与快照不一致"并回滚。
把行变换抽成 rowTransform,写入与校验共用同一份。

试过:make verify 15 项全绿;真实同步复现该失败后修复(下一轮重跑验证)。
librestore-<scope>-<时间戳.微秒> 带 ".",冻结协调器的 job 白名单只收
[A-Za-z0-9_-],所以每次恢复都在开窗口时被拒(invalid freeze request),
作业随后被接管扫成失败。改纳秒整数,并加测试锁住该约束。

试过:make verify 15 项全绿;真实恢复复现该错误后修复。
- 矩阵:新增 S06/S06-a/S06-b/V03-c/Neo4j 拒绝行(都有真跑证据);把"封存入口未接线"、
  "CLI 库级窗口"从缺口里移走;排空时序旧结论保留但标注被确定性做法取代。
- 死信解除仍缺产品路径,写出最小外部治理契约(保留原始行、审计、验证真实处理成功后
  按精确项解除、未解除继续阻断)。
- 台账补 0.1.169–0.1.180 的 SHA 与 run(含 211/215 failure、218 cancelled,不涂改)。
- library-capture / 8088 部署方案同步成本轮真实结果。
zheromiku added 1 commit 2026-10-29 06:55:18 +08:00
上游 task_dead_letters 没有解决标记,purge/DELETE 会销毁"失败过"的唯一证据。
治理侧新增解除记录(迁移 073):绑定实时行逐字节指纹、重放引用、源侧观测摘要与
操作人;行被改过、重放后又出现新死信、源侧未结清都不命中,封存照旧阻断。
指纹先压紧空白再摘要,避免 PG to_jsonb::text 与 json.Marshal 的空白差异。

试过:go test ./internal/{librarydeadletter,libraryagent,httpapi,releaseadapter/v071,
platform/postgres},make verify 15 项全绿。
zheromiku added 1 commit 2026-10-29 07:04:47 +08:00
storage://<id> 是来源实例 storage_backends 的主键;只换桶不改 id 时,目标原版
应用以 storage backend not found 落回默认服务并拒绝整条路径(真实下载 500
code1007)。写入前按租户+目标桶解析目标后端,写入与校验共用同一份行变换;
解析不到就拒绝写入,不再产出目标读不出来的定位符。

试过:目标实例真实同步后 /api/v1/knowledge/{id}/download 读回 500 → 修后复测;
go test ./internal/releaseadapter/v071 与 make verify 15 项全绿。
zheromiku added 1 commit 2026-10-29 07:08:43 +08:00
原版应用先按 resources.storage_backend_id 找文件服务;只改定位符不改这一列,
目标仍报 storage backend not found 并拒绝整条路径(真实下载 500 code1007)。
行变换同时改写该列,写入与校验共用。

试过:目标同步后 /api/v1/knowledge/{id}/download 500 → 修后复测读回 507B 且
sha256 与源对象一致;go test ./internal/releaseadapter/v071 与 make verify 全绿。
zheromiku added 1 commit 2026-10-29 07:13:22 +08:00
同步目标与封存源常常不是同一个实例(kgseal vs kgmaint);用目标名去绑来源死信,
真实观测的 project 永远对不上,解除会被一律拒绝。新增 LIBRARY_SOURCE_INSTANCE,
留空回落到目标实例名,API 写记录与 worker 取指纹共用同一个来源实例名。

试过:真实 API GET/POST、go test ./internal/{httpapi,appconfig} 与 make verify 全绿。
zheromiku added 1 commit 2026-10-29 07:22:19 +08:00
按父审后重跑的真实结果改写验收矩阵:V03-a 单列"失败/不通过"(共享页正文仍带
撤销方独有事实);V05 并发阻断、B02 删源对象后目标应用读回、死信精确解除的
"已做/未演练"分开放;记 R03 实测耗时与正式栈 socket 风险;台账补 0.1.181–0.1.186
与 run 222–227。
Some checks are pending
build / build (push) Successful in 1m13s
This branch is already included in the target branch. There is nothing to merge.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/library-capture-instance-window:feat/library-capture-instance-window
git checkout feat/library-capture-instance-window
Sign in to join this conversation.