FR-063 · Nuitka Packaging — 設計定案 · 2026-08-14

設計定案:單 binary 兩模式,六步管線,四棒拆分

本文件是 討論稿 的正式化——D1–D11 十一項決策全數定案,內容以討論稿為準。核心:統一入口 RUN_MODE 切模式、DI 靜態清單雙軌、resource_path() 資源收斂、config 三層重整、六步 build 管線、非 root production image、七項 smoke 探針。拆分為 .1 程式整理 → .2 Build 管線 → .3 Production image → .4 socketio 驗證四棒,執行順序與依賴關係見 §6。

設計定案待實作 D1–D11 全定案 範圍:Linux 容器版 Notion CM-1187

狀態:設計定案待實作(D1–D11 已拍板)|建立日期:2026-08-14|三階段戰線第一棒:FR-063 編譯 → FR-064 防竄改 → FR-065 Installer 討論稿(唯一素材來源,user 已審):discussion.html|Notion 母案:CM-1187

§1

變更紀錄

日期 變更 對應
2026-08-14 初版設計定案,D1–D10 全數拍板(D7–D10 為原待確認 Q1–Q4 升格) FR-063 母案 CM-1187
2026-08-14 新增 D11 config 三層重整+死項清理(子任務 .1g),§5.7 詳細設計 FR-063 母案 CM-1187
§2

需求背景與端到端流程

產品進入落地版(on-premise)交付階段——客戶主機上不可再出現一行可讀原始碼。本案把 BE 整包用 Nuitka standalone 編成機器碼 binary、包進 Linux Docker image 交付,與 FR-062 既有 license 執法構成雙保險。範圍限定 Linux 容器版;防竄改(FR-064)、Installer 與 Windows 支援(FR-065,傾向 Docker Desktop / WSL2 跑同一 Linux image)是後兩棒。

選型脈絡(前期已定):比較過 .pyc(compileall)/ Cython / PyArmor / Nuitka 四條路。Python 3.11 反編譯工具鏈已斷裂(uncompyle6 / decompyle3 停在 3.8 前後),純 .pyc 其實已有基礎保護力,但決策者拍板採 Nuitka standalone 取得最高保護等級,對齊 Grafana / HashiCorp 等 Go binary 陣營的交付形態(源碼可見陣營如 GitLab / Sentry 僅靠 license 執法)。

2026-08-13 夜間實測(STG 機 /opt/guidant_ai,Nuitka 4.1.3 / Python 3.11,4 core / 32GB):冷 build 1h27m、產物 main_app.dist 808MB、8,613 支 C 檔全編、pymupdf.mupdf.c 單檔 34 分鐘(平行化不可壓縮地板)。試跑 import 期即炸 SystemError: dynamic module 'dependency_injector.providers' not initialized properly from def——根因與修法已定案為 D2。

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
  SRC["原始碼樹<br/>(統一入口+<br/>resource_path 收斂後)"] --> PIPE["Build 管線六步<br/>(build 機 24c/32GB,<br/>全 script 化)"]
  PIPE --> BIN["Nuitka dist<br/>單 binary 兩模式<br/>+隨附資料檔"]
  BIN --> IMG["Production image<br/>非 root · gunicorn 內嵌<br/>fc-match 斷言"]
  IMG --> SMOKE["Smoke 七項探針"]
  SMOKE --> CUST["客戶主機<br/>容器內無 .py 業務原始碼<br/>+FR-062 license 雙保險"]
圖 1 — 端到端:源碼 → build 管線 → image → 客戶主機
§3

決策定案表 D1–D11

十一項均已定案(細節與完整理由見討論稿「決策定案 D1–D11」節):

# 決策 理由摘要 被排除方案
D1 api 模式先驗,socketio 模式隨後(單一 binary 兩模式,配 D9) api 路徑完全不碰 eventlet(已掃描確認),先驗它=90% 業務程式碼到位;兩模式是獨立 process(8000/8002)可分開驗。保底:eventlet 卡關則 socketio 換 async 承載(gevent 等,flask-socketio 支援換 async_mode),業務模組仍全數受編譯保護 兩支入口各自編譯(被 D9 統一入口取代)
D2 dependency-injector 私房 wheel,rebuild 納入管線前置步驟 官方 wheel 走 Limited API(abi3)→ 強制 PEP 489 多相初始化 → 與 Nuitka 靜態嵌入不相容。sdist(1MB,BSD)可現場 cythonize 重編:候選①不開 Limited API(預設即不開,15 分鐘可驗)②CFLAGS -DCYTHON_PEP489_MULTI_PHASE_INIT=0版本感知快取+sdist 留檔,升版自動重編零漂移 換掉 DI 套件(整個 DI 體系地基,不可行)
D3 resource_path() 統一資源定位入口 開發模式回原始碼樹、打包模式回 dist 根,所有資料檔讀取收斂一個入口;順帶清理 catalog seed 搬出 docs/、下載範本移出 static/、三處死 Blueprint static 宣告、load_routes() 死碼 各處各自判斷路徑(維持現狀,脆且散)
D4 證據分類 docker CLI 依賴:落地版功能開關降級停用 BE 自己容器化後跑 cmmc-classifier:latest 變成 DinD 問題,安裝複雜度暴增 docker.sock / 改分類器執行形態(留日後客戶真有需求再議)
D5 專用 build 機 24 core / 32GB(user 準備),100GB+ SSD 24 core 預估冷 build ~35–40 分(pymupdf 34 分是地板),ccache 熱 build 10 分內;流程全 script 化,build 機只是算力、搬家零成本 沿用 STG 機 4 core(1h27m 不可接受)
D6 版本號+git commit hash build 期 bake 進產物 /api/1.0/version 現讀 pyproject.toml,產物內無→回 unknown。改 build 期生成 version.py 常數檔;commit hash 一併 bake——編譯後 traceback 無行號,log 版本資訊必須足以唯一鎖定 commit(同版號可能 hotfix 重 build) 只 bake 版號不 bake commit(不足以定位)
D7 main_app 承載換 gunicorn BaseApplication API 內嵌(原 Q1) Flask dev server 不上 production;Nuitka 下不能走 gunicorn main:app CLI import(產物無可外部 import 的模組),改在程式內起 master/worker,gunicorn 作為依賴編入,worker 數走環境變數。開發模式保留 dev server(開關切換) gunicorn CLI 模式(binary 產物做不到)
D8 production image 非 root 執行(原 Q2) 合規產品客戶會掃 image,root 執行是標準紅字;亦降低被攻破後權限面。固定 uid 專用使用者+entrypoint chown volume+安裝文件寫明(FR-065 承接細節) root 執行(賣合規產品自己被掃紅字說不過去)
D9 統一入口+RUN_MODE 切模式+按模式裁剪載入,三支舊入口全退役(原 Q3) 骨架以 main_app.py 邏輯為主(較新、註解完整);main.py(會 create_all)/ main_app.py / main_socketio.py 全退役。單 binary build 一次、容器 command 只差環境變數,無縫接「一 image 兩 entrypoint」慣例;裁剪解掉 scheduler 雙跑、socketio 進程瘦身。PyCharm debug 不回歸(見 §5.1) 維持兩支入口各自編譯(build ×2、產物 ×2)
D10 jedi-file-upload 硬編 LibreOffice 路徑修正(原 Q4) 套件內 local_file_adapter.py / minio_adapter.py 硬編 platform 判斷、不吃 LIBREOFFICE_CMD,與主專案不一致。改同主專案邏輯(LIBREOFFICE_CMD env → PATH → mac fallback)。jedi-* 異動照規範:開發期 poetry path dependency、完成後 pin 版發佈 主專案側 workaround(違反套件異動規範,髒)
D11 Config 三層重整+死項清理(新增子任務 .1g) binary 化後「改 config=重編譯交付」,設定放錯層代價放大;六套環境 class 是雲端多環境時代設計,落地版每客戶都是新環境。三刀:①六套環境 class 塌成一套 Config、差異值 env 化、ENV 降級純顯示標籤 ②Secret 拆 JSON 包裝+config_loader.py 啟動期集中驗證(向後相容舊 .env)③營運參數(登入鎖定/密碼政策/JWT 效期/MFA)遷 DB system_config 內建預設+DB 覆寫。另附 2026-08-14 盤查定案的死項清理(config 14 項+SQLALCHEMY 系+APISPEC 系+.env 4 項)。詳見 §5.7 維持六套 class(為客戶加 class 要重編譯,不可行);一次大搬 DB(第一批刻意保守)
§4

現況盤點摘要

三路掃描(動態 import/__file__ 路徑/外部依賴)結果總表——完整明細(#4 四處資料檔、#10 volume 清單、#14 jedi- 環境變數清單)見討論稿「現況盤點」節,此處不重複貼*:

# 項目 位置 風險 對策 歸屬子需求
🔴 1 DI auto-scan rglob('*.py') config/di_modules.py:49,72 產物無 .py 掃空 → 所有 route @inject 失效,啟動不報錯、第一個 request 才炸 build 期 gen 靜態清單 config/di_modules_static.py,開發動態掃/release 用靜態(§5.2) .1a
🔴 2 dependency-injector abi3 套件本身 import 期 SystemError D2 私房 wheel rebuild .2b
🟠 3 blueprint 動態載入吞錯 main_app.py:26 等三處 except ImportError: warning 靜默——缺模組整組 API 消失 改 fail-fast+靜態 import 補強驗證 .1e
🟠 4 資料檔 ×4(__file__ 定位) translations .mo/SSP docx 範本/cmmc_l1_canon.jsoncmmc_l1_aos.json Nuitka 不自動打包非 .py 檔,四處全滅(canon.json 缺檔回 None 最陰) D3 resource_path()(§5.3) .1b
🟠 5 中文字型 production image 缺字型 PDF 回 200 但中文整段消失 Dockerfile 裝 fonts-noto-cjk+build 期 fc-match 斷言 .3
🟠 6 version 讀 pyproject.toml api/version/routes/version_route.py:17 產物內無 → 回 unknown D6 version+commit bake .1e / .2c
🟡 7 log/app.log cwd 相對路徑 jedi_common(import jedi_issuemakedirs cwd 不對就在奇怪位置長 log/ entrypoint 先 chdir 固定工作目錄 .3
🟡 8 static/ 雙重身分 上傳落地處 vs 範本下載處 volume 可寫 vs 隨版唯讀直接打架 範本移出 static(併 D3) .1b
🟡 9 上傳根目錄 fallback /tmp/upload/ STORAGE_CONFIG.base_dir 未設時 容器重啟即丟 落地版必須明確設定並掛載 .3
🟡 10 volume 清單 ×6 上傳/log/.cm-jobs/LibreOffice profile/TMPDIR/agent 憑證 未宣告即遺失或不可寫 Dockerfile / compose 明確宣告(§5.5) .3
🟡 11 死 static 宣告 ×3+壞下載 core/app_factory.py:35api/module_frame/__init__.py:83 module_frame_import_route.py:32 的下載現在就是壞的 D3 一併清理 .1b
🟣 12 外部 binary ×4 LibreOffice/cinc-auditor/docker CLI/tesseract 套件內 LibreOffice 路徑不吃 env(不一致);docker CLI 見 D4 D10 修套件;D4 功能開關停用;其餘 image 裝或降級 .1f / .3
🟣 13 WeasyPrint 系統庫 import 期 dlopen pango 等 Nuitka 不自動收 dlopen 的 .so image apt 裝齊(同現行做法),不打進 binary .3
🟣 14 環境變數 60+(含 jedi-* 16 個) DB_SECRET 等 import 期 json.loads 缺了直接炸 正式進入點不載 .env 全靠外部注入;jedi-* 16 個主專案 grep 不到 .1 收斂時產完整對照表,.3 文件化 .1 / .3
🟣 15 APScheduler 4 jobs 雙跑 兩進入點都起,靠 CAS 冪等擋 編譯面無風險,但雙跑是體質債 D9 裁剪收斂為 api 模式單一持有 .1c

掃過確認安全(getattr/registry 字串、inspect.getsource 零命中、BytesIO 串流匯出、__import__("uuid") 常量、runtime listdir)——見討論稿「✅ 掃過確認安全」。

§5

詳細設計

5.1 統一入口(D9 + D7 + D1)

新增統一入口(骨架以 main_app.py 邏輯為主),RUN_MODE 環境變數切換 api(預設)/ socketiomain.py / main_app.py / main_socketio.py 三支全退役。單一 binary、容器 command 只差環境變數。

模式裁剪載入表(現況兩入口幾乎孿生,socketio 背著全部 REST blueprint+4 個 scheduler job 雙跑):

載入項 api 模式 socketio 模式
REST blueprints(REGISTERED_APPS) 全載 不載(僅留 health check)
socketio init + Redis message queue 不載
APScheduler 4 jobs 載(唯一持有者) 不載——解掉雙跑
eventlet monkey_patch
DI container 全量 第一版照全量(求穩),瘦身留第二步
SocketIO namespaces 不註冊 註冊(集中清單,見下)
承載 gunicorn 內嵌(D7) socketio.run(eventlet)
  • SocketIO namespace 集中註冊(FR-063.4 補):兩個 namespace(/socket/notification/socket/fill-survey)原本註冊在各模組 create_module() 內,被本表「socketio 模式不載 REST blueprint」一併裁掉,導致即時功能全失效(api 模式有定義沒端點、socketio 模式有端點沒 namespace)。修法為抽離至 config/socketio_namespaces.py 集中清單,由 create_app(enable_socketio=True)DI wiring 之後統一註冊(handler 走 @inject,早於 wiring 建構會安靜拿到佔位物件)。該清單同時是 Nuitka 顯式 include 的來源,心智模型同 REGISTERED_APPS 與 DI 靜態清單。
  • eventlet 條件塊eventlet.monkey_patch() 收進 if mode == "socketio":,且必須在其他 import 之前——Python import 執行到才發生,Nuitka 保留模組執行順序語意,編譯後行為不變。
  • gunicorn 內嵌(D7):api 模式 production 承載採 gunicorn BaseApplication API——在我們的程式內起 master/worker,入口仍是我們的 binary;worker 數走環境變數。開發模式保留 Flask dev server(開關切換,開發體驗不變)。
  • PyCharm debug 保障(歷史脈絡:當初拆兩檔正是 eventlet monkey_patch × pydevd 衝突):RUN_MODE 不設或 =apiimport eventlet 那行根本不執行、stdlib 零觸碰,debug 體驗與現行 main_app.py 完全等價;patch 收進條件塊後誤觸面比現狀更小。
  • 工程注意:socketio 模式 DI 最小集邊界要實際盤(socket handler 依賴回追),第一版「不載 blueprint、不起 scheduler、DI 照全量」求穩——裁過頭會踩隱性依賴。
  • 紅利:socketio 進程瘦身(啟動快/記憶體省/8002 不再暴露整套 REST API)、scheduler 收斂單一持有、eventlet × Nuitka 實測覆蓋面縮小。

5.2 DI 靜態清單機制(盤點#1)

  • gen script:從現行 config/di_modules.py 的 rglob 掃描結果 dump 產 config/di_modules_static.py(build 期執行,Step 3)。
  • 雙軌:開發模式照舊動態掃(加檔即生效,開發體驗不變);release 模式吃 build 期凍結的靜態清單。
  • 一致性驗證:gen script 是兩軌之間唯一的橋——掃描結果 dump 下來就是靜態清單,不存在第二套手維護名單;同一份程式碼兩模式行為等價。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart TB
  subgraph DEV["開發模式(現況不變)"]
    D_SRC["原始碼樹 .py"] --> D_SCAN["di_modules.py<br/>rglob 動態掃"]
    D_SRC --> D_RES["resource_path()<br/>回原始碼樹"]
  end
  subgraph REL["release 模式(Nuitka 產物)"]
    R_BIN["統一入口 dist<br/>binary+隨附資料檔"] --> R_STATIC["di_modules_static.py<br/>build 期產的靜態清單"]
    R_BIN --> R_RES["resource_path()<br/>回 dist 根"]
  end
  D_SCAN -. "gen script<br/>build 期 dump" .-> R_STATIC
圖 2 — 開發模式 vs release 模式雙軌

5.3 resource_path() 設計(D3,盤點#4/#8/#11)

單一入口:開發模式回原始碼樹、打包模式回 dist 根。全部要收斂的讀取點

讀取點 現況壞法
config/config.py:46 TRANSLATIONS_DIR config/translations 下的 .mo i18n 全滅;.mo 是 build 產物不入版控,管線要先 pybabel compile
app/oscal/service/export/ssp_docx_generator.py:27-29 app/oscal/templates/ssp/ssp_cmmc_template.docx SSP 匯出壞;../../ 相對跳躍定位特別脆
app/evidence_classification/service/report/report_common.py:16 resources/cmmc_l1_canon.json 缺檔回 None 靜默降級,四處裡最陰
app/evidence_classification/service/catalog_builder.py:11 docs/reference/.../cmmc_l1_aos.json docs/ 下的檔,現行 image 可能已經是壞的

順帶搬遷/清理項(同棒做掉):

  1. catalog seed 搬出 docs/(release 產物不該讀 docs/)
  2. 下載範本移出 static/(解盤點#8 雙重身分衝突)
  3. 清掉三處指向不存在目錄的 Blueprint static 宣告(盤點#11,含 module_frame_import_route.py:32 現在就壞的下載路徑)
  4. api/__init__.py 死碼 load_routes()

5.4 Build 管線六步(D2 / D5 / D6)

Step 動作 備註
1 dependency-injector 私房 wheel rebuild D2:照 pyproject.toml 鎖定版本現場拉 sdist 重編;版本感知快取(wheel 檔名含版號,同版直接用快取)+ sdist tar.gz 留檔防 PyPI 不可用;升版自動觸發重編,零漂移
2 poetry install + 覆蓋私房 wheel
3 gen 靜態 DI 清單 + version/commit bake 盤點#1 / D6;另含 pybabel compile.mo
4 Nuitka 編譯統一入口 單 binary 兩模式(D9);api 模式先驗(D1)
5 production Dockerfile 打 image apt 依賴+fc-match 斷言(§5.5)
6 smoke 驗證 七項探針(§5.6)

全 script 化——build 機只是算力,搬家零成本(D5)。

5.5 Production image 規格(D8,盤點#5/#7/#9/#10/#13/#14)

  • 非 root 固定 uid(如 1000)專用使用者;entrypoint:先 chdir 固定工作目錄(解盤點#7 log 路徑)→ chown volume 目錄(解掛載 uid/gid 不合老問題)→ 起 binary。
  • OS 依賴:沿用現行 E2E Dockerfile apt 清單(LibreOffice / WeasyPrint 系統庫 pango·harfbuzz·gobject·fontconfig / fonts-noto-cjk 等);WeasyPrint 的 .so 走 image apt 裝齊、不打進 binary(盤點#13)。build 期 fc-match 'Noto Sans CJK TC' 斷言——缺字型 PDF 回 200 但中文整段消失(盤點#5)。
  • volume 佈局
掛載點 用途 備註
上傳目錄(STORAGE_CONFIG.base_dir 使用者上傳檔 必須明確設定,未設 fallback /tmp/upload/ 重啟即丟(盤點#9)
log/ app log 配合 entrypoint chdir
$HOME/.cm-jobs job 狀態
~/.config/libreoffice LibreOffice profile 首次轉檔會建
TMPDIR LibreOffice 轉檔中繼 容量要夠,大檔轉檔會吃
agent 憑證目錄 AGENT_* env 指的憑證檔 pki/ 在 dockerignore,檔案只能外部掛入
  • 環境變數注入:正式進入點不載 .env,全靠外部注入DB_SECRET / JWT_SECRET / REDIS_SECRET import 期就 json.loads——缺了 import 階段直接炸。主專案 60+ 變數以各 Config class 為準、.1 收斂時產完整對照表;jedi- 套件內另有 16 個主專案 grep 不到的變數*(ENABLE_MULTI_TENANTDB_USERNAME 等,完整清單見討論稿盤點#14),落地版部署文件必列。

5.6 Smoke 驗收清單(七項探針)

「編得出來」≠「功能活著」——本案地雷多為靜默失效型,逐雷探針缺一不可(FR-064/065 復用):

探針 探的雷
登入 基本 API/DI wire 全量
任一分頁列表 blueprint 載入完整
i18n 中文回應 .mo 檔有打包(盤點#4)
SSP docx 匯出 範本檔有打包(盤點#4)
摘要報告 PDF 開檔驗中文 WeasyPrint 系統庫+字型(盤點#5/#13)
問卷上傳 static/上傳目錄可寫(盤點#8/#9)
socketio 通知 eventlet 編譯後可用(D1/D9)

5.7 Config 三層重整(D11)

分層判準——「誰、多常、需要改它」:

判準 改動代價
編譯進 binary 產品常數 改=出新版 上傳目錄常數、內建預設值
環境變數 每部署點不同 裝機定一次 DB_HOSTCORS_ALLOWED_ORIGINSLOG_DIR
DB system_config 營運中會調 線上改即生效 登入鎖定、密碼政策、JWT 效期、MFA

第 1 刀:六套環境 class 塌成一套 Config

  • 保留單一 Config(以現 BaseConfig 為底);DevelopmentPremise / StagingPremise / StagingAwsBillowS / ProductionAwsBillowS / StagingAwsNics / ProductionAwsNics 六套子 class 全刪。
  • 差異值 env 化:WEBSITE_URL→刪(死項)、CORS_ALLOWED_ORIGINS→env 逗號分隔(預設 *)、DB_PORT→env(預設 5432)、LOGGING_LOCATION→env LOG_DIR(預設 ./log)、SQLALCHEMY_TRACK_MODIFICATIONS→死項刪、JWT 效期→第 3 刀進 DB。
  • config_util.py ENV→class 對照表退役;ENV 變數降級為純顯示標籤(log 印一行,不驅動行為),不破壞現有部署腳本。檔頭死值 AWD_DEFAULT_URLBaseConfig.DB_NAME="audit_manager" 一併清。
  • 遷移配套:產「六 class 差異→env 對照表」,三環境 .env 翻譯是機械操作;STG / POC 實際更新等部署放行(環境鐵律)。

第 2 刀:Secret 拆 JSON 包裝+啟動期集中驗證

  • 新增 config/config_loader.py:新格式平鋪變數(DB_USER / DB_PASSWORD / REDIS_USER / REDIS_PASSWORD / JWT_SECRET_KEY),向後相容舊格式(偵測 DB_SECRET 存在照舊 json.loads,現有 .env 零改動)。
  • 開機必填檢查,缺項一次列全(「缺少:DB_HOST, JWT_SECRET_KEY(或 JWT_SECRET)」),取代 import 期 json.loads 直接 traceback;json.loads 移進 loader,由統一入口(D9 / .1c)最早階段呼叫——loader 與統一入口同檔作業
  • 附帶效益:新格式命名與 jedi-* 期待的 DB_USERNAME / DB_PASSWORD 命名分歧一併對齊收斂(盤查發現 jedi 讀的變數名與主專案 .env 是兩套)。

第 3 刀:營運參數遷移 DB system_config(內建預設+DB 覆寫)

  • 第一批(刻意保守):LOGIN_MAX_LOCK_COUNT / LOGIN_USER_LOCK_TIMECHANGE_PASSWORD_THRESHOLD_DAYSLOGIN_INACTIVITY_THRESHOLD_DAYSJWT_ACCESS/REFRESH_TOKEN_EXPIRESMFA_REQUIRED
  • 機制:沿用既有 system_config root reader pattern(infra/system_config/system_config_root_reader.py 現成);編譯內建預設值,DB 有值覆寫,讀不到照預設跑(無啟動依賴)。本期不做 UI(root 管理介面留後續);migration 一支 seed(ON CONFLICT DO NOTHING),只套 DEV。
  • 風險註記:JWT 效期改 DB 後的快取生效時機,與既有 system_config reader 快取行為一致,不另發明。

死項清理(2026-08-14 盤查定案)

類別 項目
config.py 直接刪 ×14 TOKEN_TTLUSER_LOCK_COUNTUSER_PASSWORD_LIMIT_TIME(舊代,新代 LOGIN_* / CHANGE_PASSWORD_* 活著)、PROJECT_EXCEL_UPLOAD_DIR / PROJECT_EXCEL_SAMPLE_DIR / PROJECT_EXCEL_SAMPLE_FILENAMEUSER_EXCEL_SAMPLE_DIR / USER_EXCEL_SAMPLE_FILENAMEANSWER_SAMPLE_DIR / ANSWER_SAMPLE_FILENAMELOGGING_FORMATFEEDBACK_ALLOWED_EXTENSIONS(另兩處同名模組常數是別的東西不動)、SCHEDULER_API_ENABLED(裝的是純 apscheduler 非 flask-apscheduler)、WEBSITE_URL(唯一用途餵 CORS,inline 後刪)
SQLALCHEMY_* 四支 ×7 份全刪 未裝 flask_sqlalchemy、app_factory 自組 URI,SQLALCHEMY_DATABASE_URI / BINDS / POOL_RECYCLE / TRACK_MODIFICATIONS 全無消費者
APISPEC_* 三支刪 docs.init_app 被註解、Swagger 從未啟用;落地版不應對客戶暴露 Swagger。Swagger 補齊列 follow-up 不進本案
.env 死變數 ×4 REDIS_PORT(port 硬寫 6379)、UPLOAD_DIR / UPLOAD_FILE_DIR / UPLOAD_STATIC_DIR(僅 test fixture 用)
.env 整理 TEST_* / FR0*_SEED_* 六項測試帳密移 .env.testNOTION_API_TOKEN 留(工具鏈用)加註釋分區;LICENSE_ENFORCEMENT_ENABLED / LICENSE_READONLY_GATE_ENABLED 顯式寫入 .env(現靠預設全開,曾是 DEV 踩雷點)並列入安裝設定清單必填
確認活著不可刪 BUNDLE_ERRORS(flask_restful 隱式讀)、PROPAGATE_EXCEPTIONS(Flask core 讀)、CHANGE_PASSWORD_REQUEST_EXPIRATION_DAYS(餵 jedi-login DTO)、USER_EXCEL_UPLOAD_DIRANSWER_UPLOAD_DIRREDIS_URL(flask_redis 讀)

「讀寫分離未生效」事實註記:SQLALCHEMY read replica 綁定無人消費,DB_READ_HOST 設了未生效——讀寫分離從未實際運作。落地版單機 PG 無 replica 需求;真要讀寫分離是獨立 feature 不夾帶。DB_READ_HOST 保留 env 定義,文件標註「目前未生效」。jedi-* 套件不讀 Flask app.config(已驗證),只吃 env+DTO,死項判定無漏掃風險。

影響面:讀 app.config[...] 的呼叫點全不用動(key 名不變,值來源變);要動:config.py 大改、config_util.py 退役對照表、新增 config_loader.py、統一入口接線、一支 migration、密碼政策 / JWT 讀取點改走 system_config reader(約 3–5 個 service 檔)。

§6

拆分(4 子需求,執行順序與依賴)

🔴 排序要點(拆卡與派工必守)

  1. .1a / .1b 是編譯硬前置——DI 靜態清單與 resource_path 不到位,編出來的 binary 必死(盤點#1/#4),其他 .1 子項才是體質改善。
  2. .2b 提前風險驗證——build 機一到位最優先單獨跑 wheel rebuild,15 分鐘驗 dependency-injector 炸點解不解,提前排除最底層風險,免得 .1 做完才發現雷排不掉。
  3. .1 整包有獨立價值——統一入口/gunicorn/資源收斂/解 scheduler 雙跑是體質改善,即使 Nuitka 路線意外中止也不白費。
  4. 平行機會——.1f(jedi-file-upload)完全獨立可平行;.2 的 script 撰寫不必等 .1 完成;build 機環境準備(.2a)與 .1 開發時程重疊。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
  subgraph P1[".1 程式整理(開發環境即可驗證)"]
    A1["a DI 靜態清單<br/>🔴 編譯硬前置"]
    A2["b resource_path+搬遷<br/>🔴 編譯硬前置"]
    A3["c 統一入口 RUN_MODE<br/>+舊入口退役+裁剪"]
    A4["d gunicorn 內嵌<br/>(與 c 同檔一起做)"]
    A5["e fail-fast+死碼<br/>+version bake 接點"]
    A6["f jedi-file-upload D10<br/>(獨立可平行)"]
    A7["g config 三層重整 D11<br/>(.1b 後,與 c 平行;<br/>loader 與 c 同檔)"]
  end
  subgraph P2[".2 Build 管線(等 build 機;script 可先寫)"]
    B1["a build 機環境建置"]
    B2["b wheel rebuild script<br/>⚠️ build 機到位最優先跑<br/>15 分驗 D2 炸點"]
    B3["c Nuitka build script"]
  end
  P3[".3 Production image<br/>Smoke 七項探針"]
  P4[".4 socketio 模式驗證<br/>(驗證棒非開發棒)"]
  A1 --> B3
  A2 --> B3
  A2 --> A7
  A3 --> A4
  B1 --> B2
  B2 --> B3
  B3 --> P3
  P3 --> P4
圖 3 — 子需求依賴與執行順序

FR-063.1 程式整理 — 依賴:無(全部可在開發環境完成驗證,不需 build 機)

子任務 內容 對應
.1a DI 靜態清單機制(gen script+開發動態掃/release 靜態雙軌) 🔴 編譯硬前置|盤點#1|§5.2
.1b resource_path() 統一資源定位+資料檔搬遷(四讀取點收斂、catalog seed 搬出 docs/、範本移出 static/、清三處死 static 宣告) 🔴 編譯硬前置|D3|盤點#4/#8/#11|§5.3
.1c 統一入口(RUN_MODE)+三支舊入口退役(D9)+模式裁剪載入 §5.1
.1d gunicorn BaseApplication 內嵌(D7)——與 .1c 同檔作業一起做 §5.1
.1e blueprint fail-fast(盤點#3)+死碼清理(load_routes())+version bake 接點(D6) §5.1/§5.4
.1f jedi-file-upload 修 LIBREOFFICE_CMD(D10)——獨立可平行;涉套件異動走 path dependency 規範 盤點#12
.1g config 三層重整+死項清理(D11)——排序 .1b 之後、與 .1c 可平行;第 2 刀 loader 與統一入口(.1c)同檔作業。驗收:①三環境用舊 .env 原封不動起服務行為零差異(向後相容)②新格式 .env 起服務行為相同 ③故意漏設必填→啟動一次列全缺項非 traceback ④DB system_config 改鎖定次數生效、刪除回內建預設 ⑤既有測試全綠 D11|§5.7

收口 gate:開發模式行為零變化——既有測試綠、PyCharm debug 不變、RUN_MODE 兩模式開發環境都起得來;grep 全 codebase 無殘留 __file__ 相對資料檔定位;blueprint 缺模組時啟動即炸而非 warning。

FR-063.2 Build 管線 — 依賴:build 機到位(D5,user 準備);script 撰寫可與 .1 平行

子任務 內容 對應
.2a build 機環境建置(gcc/g++/patchelf/ccache/docker/100GB+ SSD) D5
.2b wheel rebuild script(D2 含版本感知快取+sdist 留檔)——⚠️ build 機一到位最優先單獨跑這個,15 分鐘驗 dependency-injector 炸點解不解,提前排除最底層風險,免得 .1 做完才發現雷排不掉 D2|盤點#2
.2c Nuitka build script(串 DI 清單生成+version bake+編譯) §5.4 Step 3–4

驗收:build 機一鍵跑完 Step 1–4 產出可啟動的統一入口 dist;私房 wheel 快取命中時跳過重編;binary 產出後 api 模式起得來、打得到 API(import 期不炸、DI wire 全量)。

FR-063.3 Production image — 依賴:.2 binary 產出

production Dockerfile:非 root 固定 uid(D8)/volume 佈局(盤點#10)/fc-match 斷言(盤點#5)/entrypoint chdir+chown(盤點#7/D8)/環境變數文件化(盤點#14 完整對照表)。

驗收:Smoke 七項探針全過(§5.6);重啟後上傳檔不丟;容器內找不到任何 .py 業務原始碼;process 以非 root uid 執行。

FR-063.4 socketio 模式驗證與裁剪 — 依賴:.3

裁剪程式碼在 .1c 已寫掉;本棒主要是編譯後實測,是驗證棒不是開發棒:eventlet monkey-patch × Nuitka 相容性實測;卡關 → 啟動 D1 保底(換 gevent 等 async_mode,入口薄殼十幾行無祕密,業務模組仍全數受編譯保護)。

驗收RUN_MODE=socketio 進程可建立連線並收發事件(socketio 通知端到端通)、不載 REST blueprint、scheduler 不雙跑;或保底方案落地且業務模組仍在編譯保護內。

§7

端到端驗收

  1. Smoke 七項探針全過(§5.6:登入/列表/i18n/SSP docx/PDF 中文/上傳/socketio)。
  2. 容器內無 .py 業務原始碼——客戶主機上找不到任何可讀業務原始碼,與 FR-062 license 執法構成雙保險。
  3. 開發模式零回歸——既有測試綠、PyCharm debug 體驗與現行 main_app.py 完全等價、動態 DI 掃描與資源定位行為不變(加檔即生效)。
  4. Config 向後相容(D11)——三環境用舊 .env 原封不動起服務,行為零差異;新格式 .env 行為相同。
  5. 其他逐棒驗收見 §6 各子需求(.1 收口 gate/.2 binary 可啟動/.3 image 規格/.4 socketio 通)。