Skip to content

Commit 94c9ef2

Browse files
committed
chore: enforce release alignment and disable implicit runtime drift
1 parent 33430fa commit 94c9ef2

11 files changed

Lines changed: 315 additions & 34 deletions

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
88
PROJECT_ROOT=/app \
99
DATA_ROOT=/data \
1010
STATE_ROOT=/state \
11+
RUNTIME_DB_SYNC_MODE=disabled \
1112
PORT=8080 \
1213
HF_HOME=/state/cache/huggingface \
1314
HF_HUB_CACHE=/state/cache/huggingface/hub \

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@
4444

4545
- 涉及前端或页图行为的发布,必须从 clean release source 构建,不能直接从 VPS runtime repo 出包
4646
- 本机是唯一源头:代码更新先固化到 `release_manifest.json`,再推 GitHub;大文件同步到 VPS;页图同步到 R2;四端状态必须按同一轮 release manifest 对齐
47+
- 运行时大文件不经 GitHub 同步;`data/index/*.db``*.index`、runtime JSON 必须显式同步到 VPS,不能指望代码发布顺带带过去
48+
- R2 当前公开职责只保留页图与图片 CDN;生产容器默认禁用运行时主检索库自动拉取,避免启动时把 VPS 已验收数据盘悄悄改写
49+
- `textbook_mineru_fts.db` 含运行时日志表,文件级 sha 会随线上流量漂移;发布对账以稳定的运行时语料指纹为准,不再把 `search_logs` / `ai_chat_logs` 的增长误判成错版
4750
- 若发布涉及页图映射,镜像内必须包含 `frontend/assets/pages/book_map.json`;上线验收时需抽查 live 搜索结果返回非空 `page_url`
4851
- 手工发布前如需保留人工回滚点,应先按 running image digest 额外打 tag,不要直接把 `textbook-knowledge:latest` 当作回滚锚点
4952
-`README.md` / `docs/**` 这类说明文档更新不应触发生产部署;workflow 需继续显式忽略 docs-only push
@@ -401,9 +404,10 @@ RUNTIME_ROOT=/root/cross-subject-knowledge ./scripts/deploy_vps.sh
401404
3. 在 VPS 上创建临时的干净 release checkout,避免生产目录里历史热补丁或临时改动阻塞发布。
402405
4. 在 VPS 上先构建新镜像,再停旧容器,避免“构建失败直接打挂线上”。
403406
5. 部署脚本会同步补充教材索引与补充向量到运行时目录,并预热 `BAAI/bge-reranker-base`,避免首个精确查询才触发冷启动。
404-
6. 新容器通过 `/api/health` 健康检查后才算部署成功;当前健康闸门要求 DB、FAISS、补充教材索引可用,且在启用 reranker 时确认 `reranker.loaded=true`;失败则自动回滚到上一镜像。
405-
7. 运行时模型缓存保存在宿主机 `state/cache/`,避免每次发版都把 Hugging Face 缓存烘进镜像。
406-
8. 部署完成后自动清理悬空镜像,只保留最近几份 `pre-*` 回滚镜像,并删除历史 `build-*` tag。
407+
6. 生产容器默认以宿主机挂载的 `/data/index/` 为唯一运行时检索库来源;`backend/sync_db.py` 不再作为默认启动链路,只有显式设置 `RUNTIME_DB_SYNC_MODE=r2_textbook_mineru` 时才会执行应急拉库。
408+
7. 新容器通过 `/api/health` 健康检查后才算部署成功;当前健康闸门要求 DB、FAISS、补充教材索引可用,且在启用 reranker 时确认 `reranker.loaded=true`;失败则自动回滚到上一镜像。
409+
8. 运行时模型缓存保存在宿主机 `state/cache/`,避免每次发版都把 Hugging Face 缓存烘进镜像。
410+
9. 部署完成后自动清理悬空镜像,只保留最近几份 `pre-*` 回滚镜像,并删除历史 `build-*` tag。
407411

408412
手工紧急发布不应再从 VPS runtime repo 直接出包。当前统一做法是:
409413

backend/entrypoint.sh

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
#!/usr/bin/env sh
22
set -eu
33

4-
python /app/backend/sync_db.py
4+
RUNTIME_DB_SYNC_MODE="${RUNTIME_DB_SYNC_MODE:-disabled}"
5+
if [ "${RUNTIME_DB_SYNC_MODE}" != "disabled" ]; then
6+
python /app/backend/sync_db.py
7+
else
8+
echo "Runtime DB sync disabled; using mounted /data runtime assets."
9+
fi
510
python /app/backend/preflight.py
611

712
exec uvicorn backend.main:app --host 0.0.0.0 --port "${PORT:-8080}" --workers 1

backend/sync_db.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
import os, urllib.request, hashlib
1+
import os
2+
import sys
3+
import urllib.request
24
from pathlib import Path
35

46
DB_URL = "https://img.rdfzer.com/db-sync/textbook_mineru_fts.db"
7+
SYNC_MODE = os.getenv("RUNTIME_DB_SYNC_MODE", "disabled").strip().lower()
8+
SYNC_MODE_R2_TEXTBOOK = "r2_textbook_mineru"
59
PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT", Path(__file__).resolve().parents[1])).expanduser().resolve()
610
_DEFAULT_DATA_ROOT = PROJECT_ROOT / "data"
711
_ALT_DATA_ROOT = PROJECT_ROOT.parent / "data"
@@ -12,6 +16,21 @@
1216
DB_PATH_LEGACY = DATA_ROOT / "textbook_mineru_fts.db"
1317
DB_PATH = DB_PATH_PRIMARY if DB_PATH_PRIMARY.exists() or not DB_PATH_LEGACY.exists() else DB_PATH_LEGACY
1418

19+
def sync_enabled() -> bool:
20+
disabled_modes = {"", "0", "false", "off", "no", "disabled", "none"}
21+
if SYNC_MODE in disabled_modes:
22+
print("Runtime DB sync skipped: RUNTIME_DB_SYNC_MODE is disabled.")
23+
return False
24+
if SYNC_MODE != SYNC_MODE_R2_TEXTBOOK:
25+
print(
26+
f"Unsupported RUNTIME_DB_SYNC_MODE={SYNC_MODE!r}. "
27+
f"Expected {SYNC_MODE_R2_TEXTBOOK!r} or a disabled value.",
28+
file=sys.stderr,
29+
)
30+
raise SystemExit(2)
31+
return True
32+
33+
1534
def download_db():
1635
print(f"Checking for DB updates from {DB_URL}...")
1736
try:
@@ -37,4 +56,5 @@ def download_db():
3756
print(f"Failed to check/download DB update: {e}")
3857

3958
if __name__ == '__main__':
40-
download_db()
59+
if sync_enabled():
60+
download_db()

docs/data_layer_lineage_memory.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ Important DB note:
241241

242242
- `textbook_mineru_fts.db` is a mutable runtime file because local smoke tests and production traffic can append telemetry tables and log rows.
243243
- File-level SHA256 for this DB is therefore not a stable content identity signal by itself.
244-
- For release verification, pair the DB file SHA with a stable textbook-corpus fingerprint derived from `chunks WHERE source != 'gaokao'`.
244+
- For release verification, pair the DB file SHA with a stable runtime identity fingerprint derived from the search-critical content tables plus FTS shadow table counts, while excluding mutable telemetry tables such as `search_logs` and `ai_chat_logs`.
245245

246246
### Production runtime destinations
247247

@@ -634,19 +634,21 @@ Conditionally required runtime assets:
634634

635635
Startup behavior from [`platform/backend/entrypoint.sh`](../backend/entrypoint.sh):
636636

637-
1. run `sync_db.py`
637+
1. optionally run `sync_db.py` only when `RUNTIME_DB_SYNC_MODE` is explicitly enabled
638638
2. run `preflight.py`
639639
3. start `uvicorn`
640640

641641
The runtime does not rebuild FAISS or supplemental assets on the VPS.
642642

643643
### DB drift rule
644644

645-
[`platform/backend/sync_db.py`](../backend/sync_db.py) auto-syncs only:
645+
[`platform/backend/sync_db.py`](../backend/sync_db.py) is now an explicit emergency path only. When manually enabled, it can sync only:
646646

647647
- `textbook_mineru_fts.db`
648648

649-
It does not auto-sync:
649+
By default, production startup keeps `RUNTIME_DB_SYNC_MODE=disabled`, so it does not auto-sync any runtime DB.
650+
651+
It never auto-syncs:
650652

651653
- `textbook_chunks.index`
652654
- `textbook_chunks.manifest.json`

docs/release_maintenance_design.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,44 @@
4747

4848
这样,手工紧急修复不再依赖“自己记得复制哪些文件”。
4949

50+
### 2.5. 运行时主检索库不再允许隐式同步
51+
52+
这次排查还暴露了另一条真正会制造状态漂移的链路:
53+
54+
- 容器启动时自动运行 `backend/sync_db.py`
55+
- `sync_db.py` 会尝试从 R2 `db-sync/` 路径下载 `textbook_mineru_fts.db`
56+
57+
这条链路直接破坏了四端对齐模型:
58+
59+
- 本机、GitHub、VPS、R2 可以在没有显式发布动作的情况下继续分叉
60+
- 网站有效并不能证明 VPS `data/index/` 仍等于本轮验收版本
61+
- 代码发布会夹带运行时数据改写,回滚与验收都失去边界
62+
63+
因此本轮的固定约束是:
64+
65+
- 生产容器默认 `RUNTIME_DB_SYNC_MODE=disabled`
66+
- `deploy_vps.sh` 强制要求 `RUNTIME_DB_SYNC_MODE=disabled`
67+
- `sync_db.py` 只保留为显式应急工具,必须手动设置 `RUNTIME_DB_SYNC_MODE=r2_textbook_mineru` 才会执行
68+
69+
未来运行时主检索库的唯一正常更新方式是:
70+
71+
1. 本机生成或确认工件
72+
2. 显式同步到 VPS `data/index/`
73+
3. 再部署或重启容器
74+
75+
而不是让容器在启动时自行改写数据盘。
76+
77+
另一个必须同时固定下来的点是 `textbook_mineru_fts.db` 的“活文件”属性:
78+
79+
- 线上流量会继续写 `search_logs` / `ai_chat_logs`
80+
- 因此整库 SHA 不能长期作为唯一发布对账依据
81+
82+
本轮已把 manifest / verify 规则收成:
83+
84+
- 保留整库 SHA 作为信息字段
85+
- 对真正的发布校验,改用稳定的 `runtime_identity`
86+
- `runtime_identity` 只覆盖检索和知识运行所需的核心内容表与 FTS 影子表计数,不把运行时日志表计入错版判断
87+
5088
### 3. 部署脚本必须拒绝坏包
5189

5290
`platform/scripts/deploy_vps.sh` 现在新增硬性拦截:

docs/runtime_operations_overview.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Before any data rebuild, deploy, rollback, or search-debugging pass, read [data_
99
- Local development machine: code editing, data inspection, lightweight validation
1010
- Offline processing/build machine: OCR, FAISS rebuild, batch backfill, and large model downloads
1111
- Production VPS: serves search, graph, gaokao linkage, and AI chat APIs; does not run OCR or FAISS rebuild
12-
- Cloudflare R2: image and page asset delivery
12+
- Cloudflare R2: image and page asset delivery only; production runtime DBs should not mutate from R2 during normal startup
1313
- Cloudflare Worker custom domain `ai.bdfz.net`: external AI gateway bound to Worker service `apis` / `production`
1414

1515
## Runtime assets
@@ -29,6 +29,12 @@ Production runtime depends on host-mounted directories instead of baking data in
2929

3030
The repository also keeps bundled fallback copies of the supplemental index under `backend/`, but production should rely on the synced `data/index/` copies and their manifest.
3131

32+
Runtime DB rule:
33+
34+
- `/data/index/*.db` and `/data/index/*.index` are the authoritative runtime assets once they are synced to the VPS
35+
- production startup must not implicitly replace them from R2 or any other remote source
36+
- if a runtime DB needs emergency replacement, do it as an explicit file sync step before deploy or restart
37+
3238
Current page-image boundary:
3339

3440
- `book_map.json` and the CDN page-image product now cover `96` books: the original `69` primary books plus `27` supported supplemental books

release_manifest.json

Lines changed: 75 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"schema_version": 1,
3-
"generated_at": "2026-03-11T14:36:55.640411+00:00",
4-
"git_head": "32c69d3b69cb2ad5d31b7c9abe4ec65420a2328d",
3+
"generated_at": "2026-03-11T15:26:41.781604+00:00",
4+
"git_head": "33430fa149ae546feab64aae1a1c45e8a67a1f2c",
55
"frontend_version": "2026.03.11-r30",
66
"source_of_truth": {
77
"local_workspace": "authoritative",
@@ -13,8 +13,8 @@
1313
{
1414
"kind": "source",
1515
"logical_path": "Dockerfile",
16-
"size": 1355,
17-
"sha256": "59de729b4c698c9c522eacdea60257bfbc75ed19512f76cad0fa17e2805add75"
16+
"size": 1391,
17+
"sha256": "81e182f30187904c62899480ddb4dfa61012dcd63d43e708b29e12d44f57051b"
1818
},
1919
{
2020
"kind": "source",
@@ -37,8 +37,8 @@
3737
{
3838
"kind": "source",
3939
"logical_path": "backend/entrypoint.sh",
40-
"size": 172,
41-
"sha256": "4cc0d23dbf99899c4467553caaa318a5cd4033c46c017b04dc6dec09e6f11418"
40+
"size": 363,
41+
"sha256": "07c195d0bf29569d11cda2b5c2928b08edec6bed90174731021b2c88bed0fa0b"
4242
},
4343
{
4444
"kind": "source",
@@ -49,8 +49,8 @@
4949
{
5050
"kind": "source",
5151
"logical_path": "backend/sync_db.py",
52-
"size": 1904,
53-
"sha256": "55f2b3119d1852036180cd3a6cf397a4f7c808d38c9e05f1e8a02f02d0fe5a53"
52+
"size": 2571,
53+
"sha256": "55dcd301704bc1d80bf0b10e86a321e208a26e505f12898481ab23aeeb9b448e"
5454
},
5555
{
5656
"kind": "source",
@@ -133,8 +133,8 @@
133133
{
134134
"kind": "source",
135135
"logical_path": "scripts/deploy_vps.sh",
136-
"size": 13040,
137-
"sha256": "af5c476eced7e12625fcef41bb79f88d9c634bd373b0a82f6c71c734dcd0534e"
136+
"size": 13432,
137+
"sha256": "d62f2a4d23a7f1be3d60069d1d6bfaadc184d9724f49753b398896eb203d92f2"
138138
},
139139
{
140140
"kind": "source",
@@ -145,23 +145,82 @@
145145
{
146146
"kind": "source",
147147
"logical_path": "scripts/build_release_manifest.py",
148-
"size": 6868,
149-
"sha256": "709c5782b39f84a756bf7d00d8d14f22996e24dbda0579b43afeed28790a992f"
148+
"size": 9282,
149+
"sha256": "161a834da14ee82dd1c61a1b5fe71b9c890fb5188228a242b3872e377c5d582e"
150150
},
151151
{
152152
"kind": "source",
153153
"logical_path": "scripts/verify_release_manifest.py",
154-
"size": 3798,
155-
"sha256": "be6b63e7606867e840154cc939dd44cdadecc0836a9ccfd508b1310ba6e1c78d"
154+
"size": 6547,
155+
"sha256": "c294a574a27f7c80a260c854cf041f02ed55d1d8bc60044b4f787b7d4cdeecbf"
156156
}
157157
],
158158
"runtime_assets": [
159159
{
160160
"kind": "runtime",
161161
"logical_path": "data/index/textbook_mineru_fts.db",
162162
"size": 58892288,
163-
"sha256": "766e9878195a0ceb9c385f21884017bc86d51c6066427e542c361a05b05d5f1d",
164-
"source_path": "data/index/textbook_mineru_fts.db"
163+
"sha256": "b3814c96092ebe90e42675420aa0066a957c51a0be837ea63d67e907726a22e0",
164+
"source_path": "data/index/textbook_mineru_fts.db",
165+
"runtime_identity": {
166+
"type": "sqlite_textbook_runtime_identity_v1",
167+
"mutable_tables": [
168+
"ai_batch_ingest",
169+
"ai_batch_jobs",
170+
"ai_chat_logs",
171+
"search_logs",
172+
"sqlite_sequence"
173+
],
174+
"content_tables": {
175+
"chunks": {
176+
"row_count": 21925,
177+
"sha256": "d73e6c4bed9bccce6388ef8411270fdc9aeb67cdbef7d6ebfb52b367567988aa"
178+
},
179+
"ai_summaries": {
180+
"row_count": 17506,
181+
"sha256": "9024b21c7da9386f4ed347cd49ac2a1a2946b5f3f292a891d0ea39524c0c10e2"
182+
},
183+
"ai_explanations": {
184+
"row_count": 126,
185+
"sha256": "3411be218b600d53652d170c8c8ee8cbb6d40ee430eaf92d65978fcf4c0c3ebf"
186+
},
187+
"ai_synonyms": {
188+
"row_count": 788,
189+
"sha256": "427b888ce49c265cd10ec1c6342fc2032c467edb37393549055b2a0b0d57d770"
190+
},
191+
"concept_map": {
192+
"row_count": 1102,
193+
"sha256": "dbc7bcc6269e9dc9843391a5868ce87af100fc36f014d5f22fede990713b3563"
194+
},
195+
"cross_subject_map": {
196+
"row_count": 83,
197+
"sha256": "aecd745f59f35d5b087a090845b2c9b1c363905d49d2cf575ed38d331a724ea9"
198+
},
199+
"curated_keywords": {
200+
"row_count": 515,
201+
"sha256": "0d56877e6f7bf542b1381b1775d301fb576d2eab98e7b3fa048d1216531bead0"
202+
},
203+
"keyword_counts": {
204+
"row_count": 1773,
205+
"sha256": "0a1181df496039944a619406c5e1e9f984ad09d5281f29a3d3aee57cc8de70b6"
206+
},
207+
"ai_gaokao_links": {
208+
"row_count": 4029,
209+
"sha256": "a1f3beaedba27de965da2c5bd36b5683bbee4d16d38ae2acc6706acd2d4a2a8a"
210+
},
211+
"ai_relations": {
212+
"row_count": 597,
213+
"sha256": "b403d9353b08c20e0f0900c825cb002c8d2c5d44cbb3860fc7e02369fe905f3b"
214+
}
215+
},
216+
"fts_shadow_counts": {
217+
"chunks_fts": 21925,
218+
"chunks_fts_data": 4254,
219+
"chunks_fts_idx": 4134,
220+
"chunks_fts_docsize": 21925
221+
},
222+
"integrity_check": "ok"
223+
}
165224
},
166225
{
167226
"kind": "runtime",

0 commit comments

Comments
 (0)