Files
knowledge-governance/compose.yaml
T
姚章浩 acf053faf3 feat(release): 建批时冻结 + 验收通过自动批准 —— 把"每份文档 8 步"砍掉两步并去掉两次卡死
背景:发布管理把"逐条核验"的仪式强加给每一份文档。实测一份文档要 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 条通过。
2026-10-10 15:05:20 +08:00

219 lines
8.2 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: knowledge-governance-p1
services:
postgres:
image: postgres:17-alpine
environment:
POSTGRES_DB: governance
POSTGRES_USER: governance
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
secrets:
- postgres_password
volumes:
- governance-pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U governance -d governance"]
interval: 5s
timeout: 3s
retries: 20
restart: unless-stopped
api:
build:
context: .
dockerfile: Dockerfile
command: ["/app/api"]
user: "${APP_UID:-1000}:${APP_GID:-1000}"
environment:
<<: &app-environment
DATABASE_URL: postgres://governance@postgres:5432/governance?sslmode=disable
PGPASSFILE: /run/secrets/pgpass
APP_ADDR: :8080
WORKER_POLL_INTERVAL: 1s
WORKER_LEASE_DURATION: 30s
WORKER_RETRY_DELAY: 5s
TEST_WEKNORA_BASE_URL: "${TEST_WEKNORA_BASE_URL:-}"
# 验收通过后是否自动批准:off | internal(默认,仅对内;对外仍需人工)| all。
# 只影响"审核"这一步;生产发布的双闸、人工授权与发布前快照门禁不受影响。
TEST_AUTO_REVIEW_POLICY: "${TEST_AUTO_REVIEW_POLICY:-internal}"
DINGTALK_LOGIN_SIDECAR_URL: "http://dingtalk-login:8091"
MINERU_BASE_URL: "${MINERU_BASE_URL:-}"
TEST_INTERNAL_WEKNORA_KB_ID: "${TEST_INTERNAL_WEKNORA_KB_ID:-1b52c140-2e18-4676-afc5-4c7d8d190a8c}"
TEST_EXTERNAL_WEKNORA_KB_ID: "${TEST_EXTERNAL_WEKNORA_KB_ID:-fd7db6aa-763a-46f1-84d2-ecc7c952a5ea}"
# P10 readiness probe reads the routing KB with the same scoped key.
TEST_WEKNORA_API_KEY_FILE: /run/secrets/test_weknora_api_key
TEST_WEKNORA_ALLOW_DOCKER_HTTP: "${TEST_WEKNORA_ALLOW_DOCKER_HTTP:-false}"
WORKER_LIVENESS_STALE_AFTER: "${WORKER_LIVENESS_STALE_AFTER:-2m}"
PRODUCTION_RELEASE_REQUESTS_ENABLED: ${PRODUCTION_RELEASE_REQUESTS_ENABLED:-false}
PRODUCTION_BACKUP_RESTORE_VERIFIED: ${PRODUCTION_BACKUP_RESTORE_VERIFIED:-false}
# Read-only probe of the production WeKnora endpoint; publishing keeps
# reading its target from RELEASE_CONFIG_FILE.
PRODUCTION_WEKNORA_BASE_URL: "${PRODUCTION_WEKNORA_BASE_URL:-}"
PRODUCTION_WEKNORA_API_KEY_FILE: "${PRODUCTION_WEKNORA_API_KEY_FILE:-}"
PRODUCTION_WEKNORA_ALLOW_DOCKER_HTTP: "${PRODUCTION_WEKNORA_ALLOW_DOCKER_HTTP:-false}"
secrets:
- pgpass
- test_weknora_api_key
depends_on:
postgres:
condition: service_healthy
healthcheck:
# P10: liveness only — a third-party outage must never restart the API.
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/livez"]
interval: 5s
timeout: 3s
retries: 20
read_only: true
tmpfs:
- /tmp
networks:
- default
- weknora-local
restart: unless-stopped
worker:
build:
context: .
dockerfile: Dockerfile
command: ["/app/worker"]
user: "${APP_UID:-1000}:${APP_GID:-1000}"
environment:
<<: *app-environment
WORKER_ID: worker-1
PP_SCAN_ENABLED: "${PP_SCAN_ENABLED:-false}"
PP_SCAN_ALLOWED_ROOT: "${PP_SCAN_CONTAINER_DATA_ROOT:-/home/zheromiku/dobot-portal-sync/data}"
PP_SCAN_LEDGER_PATH: "${PP_SCAN_CONTAINER_DATA_ROOT:-/home/zheromiku/dobot-portal-sync/data}/public-weknora-sync/state.jsonl"
PP_SCAN_MANIFEST_PATH: "${PP_SCAN_CONTAINER_DATA_ROOT:-/home/zheromiku/dobot-portal-sync/data}/public-documents/manifest.json"
TEST_WEKNORA_ENABLED: "${TEST_WEKNORA_ENABLED:-false}"
TEST_WEKNORA_ALLOW_DOCKER_HTTP: "${TEST_WEKNORA_ALLOW_DOCKER_HTTP:-false}"
TEST_WEKNORA_KB_ID: "${TEST_WEKNORA_KB_ID:-}"
TEST_WEKNORA_API_KEY_FILE: /run/secrets/test_weknora_api_key
GOVERNANCE_SPOOL_ROOT: /var/lib/governance/spool
INGESTION_SPOOL_ROOT: /var/lib/governance/ingestion
TEST_WEKNORA_HTTP_TIMEOUT: "${TEST_WEKNORA_HTTP_TIMEOUT:-60s}"
TEST_WEKNORA_POLL_INITIAL: "${TEST_WEKNORA_POLL_INITIAL:-10s}"
TEST_WEKNORA_POLL_MAX: "${TEST_WEKNORA_POLL_MAX:-60s}"
TEST_WEKNORA_STALLED_AFTER: "${TEST_WEKNORA_STALLED_AFTER:-30m}"
TEST_WEKNORA_MAX_UPLOAD_BYTES: "${TEST_WEKNORA_MAX_UPLOAD_BYTES:-52428800}"
TEST_ACCEPTANCE_POLICY_VERSION: "${TEST_ACCEPTANCE_POLICY_VERSION:-structural-v1}"
# DingTalk 取件走 dws(默认优先 dws,可重试失败回落老路径):dws|auto|legacy
ARTIFACT_DINGTALK_FETCHER: "${ARTIFACT_DINGTALK_FETCHER:-auto}"
DWS_BIN: /usr/local/bin/dws
DWS_CONFIG_DIR: /data/dws
# dws 的文件型 keychain 在 $HOME/.local/share/dws-cli(XDG_DATA_HOME 不被识别),
# 仅给 dws 子进程换 HOME,不动整个 worker 的 HOME。
DWS_HOME: /data/home
DWS_DISABLE_KEYCHAIN: "1"
SSL_CERT_FILE: /etc/ssl/certs/ca-certificates.crt
secrets:
- pgpass
- test_weknora_api_key
volumes:
- ./spool:/var/lib/governance/spool:ro
- ./spool/ingestion:/var/lib/governance/ingestion
- "${PP_SCAN_HOST_DATA_ROOT:-/home/zheromiku/dobot-portal-sync/data}:${PP_SCAN_CONTAINER_DATA_ROOT:-/home/zheromiku/dobot-portal-sync/data}:ro"
- /home/zheromiku/dingtalk_kb_download:/sources/dingtalk:ro
# dws 凭据:与宿主共享同一份 state(refresh token 是轮换制,不能在两处各留一份)
- /home/zheromiku/.dws:/data/dws
# dws 的文件型 keychain:必须有 $HOME/.local/share/dws-cli,故挂到 DWS_HOME 下
- /home/zheromiku/.local/share/dws-cli:/data/home/.local/share/dws-cli
- /home/zheromiku/tmp/alist:/sources/alist:ro
- /home/zheromiku/dobot-portal-sync:/sources/pp:ro
- ./secrets/dingtalk_capture.json:/run/source-secrets/dingtalk_capture.json:ro
- ./secrets/alist_connections.xlsx:/run/source-secrets/alist_connections.xlsx:ro
- ./secrets/alist_api_token:/run/source-secrets/alist_api_token:ro
- ./secrets/pp_credentials.json:/run/source-secrets/pp_credentials.json:ro
networks:
- default
- weknora-local
depends_on:
postgres:
condition: service_healthy
read_only: true
tmpfs:
- /tmp
restart: unless-stopped
release-worker:
build:
context: .
dockerfile: Dockerfile
command: ["/app/release-worker"]
user: "${APP_UID:-1000}:${APP_GID:-1000}"
environment:
<<: *app-environment
WORKER_ID: release-worker-1
RELEASE_WORKER_ENABLED: "false"
RELEASE_CONFIG_FILE: ""
secrets:
- pgpass
depends_on:
postgres:
condition: service_healthy
read_only: true
tmpfs:
- /tmp
restart: unless-stopped
dingtalk-login:
# Chromium profile for DingTalk scan sessions. The bind-mounted secrets
# directory is rw ONLY here; the worker keeps reading the capture read-only.
build:
context: ./services/dingtalk-login
image: knowledge-governance-dingtalk-login:local
user: "${APP_UID:-1000}:${APP_GID:-1000}"
environment:
CAPTURE_FILE: /run/source-secrets/dingtalk_capture.json
DINGTALK_STATE_FILE: /sources/dingtalk/data/dingtalk-internal/inventory-state.json
VERIFY_SPACE: "${DINGTALK_VERIFY_SPACE:-9JOGO6332vlBnm4Q}"
QR_TTL_MS: "300000"
volumes:
- ./secrets:/run/source-secrets
- /home/zheromiku/dingtalk_kb_download:/sources/dingtalk
- dingtalk-chromium-profile:/profile
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:8091/healthz',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"]
interval: 10s
timeout: 5s
retries: 30
start_period: 15s
restart: unless-stopped
web:
build:
context: .
dockerfile: Dockerfile.web
ports:
- "8088:8080"
depends_on:
api:
condition: service_healthy
read_only: true
tmpfs:
- /tmp
- /var/cache/nginx
- /var/run
restart: unless-stopped
secrets:
postgres_password:
file: ./secrets/postgres_password
pgpass:
file: ./secrets/pgpass
test_weknora_api_key:
file: ./secrets/test_weknora_api_key
volumes:
governance-pgdata:
dingtalk-chromium-profile:
networks:
weknora-local:
external: true
name: weknora071full-network