FR-063 · Nuitka Packaging — 設計定案 · 2026-08-14
本文件是 討論稿 的正式化——D1–D11 十一項決策全數定案,內容以討論稿為準。核心:統一入口 RUN_MODE 切模式、DI 靜態清單雙軌、resource_path() 資源收斂、config 三層重整、六步 build 管線、非 root production image、七項 smoke 探針。拆分為 .1 程式整理 → .2 Build 管線 → .3 Production image → .4 socketio 驗證四棒,執行順序與依賴關係見 §6。
狀態:設計定案待實作(D1–D11 已拍板)|建立日期:2026-08-14|三階段戰線第一棒:FR-063 編譯 → FR-064 防竄改 → FR-065 Installer 討論稿(唯一素材來源,user 已審):
discussion.html|Notion 母案:CM-1187
| 日期 | 變更 | 對應 |
|---|---|---|
| 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 |
產品進入落地版(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 雙保險"]
十一項均已定案(細節與完整理由見討論稿「決策定案 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(第一批刻意保守) |
三路掃描(動態 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.json/cmmc_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_issue 期 makedirs) |
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:35、api/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)——見討論稿「✅ 掃過確認安全」。
新增統一入口(骨架以 main_app.py 邏輯為主),RUN_MODE 環境變數切換 api(預設)/ socketio;main.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) |
/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.monkey_patch() 收進 if mode == "socketio":,且必須在其他 import 之前——Python import 執行到才發生,Nuitka 保留模組執行順序語意,編譯後行為不變。BaseApplication API——在我們的程式內起 master/worker,入口仍是我們的 binary;worker 數走環境變數。開發模式保留 Flask dev server(開關切換,開發體驗不變)。RUN_MODE 不設或 =api 時 import eventlet 那行根本不執行、stdlib 零觸碰,debug 體驗與現行 main_app.py 完全等價;patch 收進條件塊後誤觸面比現狀更小。config/di_modules.py 的 rglob 掃描結果 dump 產 config/di_modules_static.py(build 期執行,Step 3)。%%{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
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 可能已經是壞的 |
順帶搬遷/清理項(同棒做掉):
docs/(release 產物不該讀 docs/)static/(解盤點#8 雙重身分衝突)module_frame_import_route.py:32 現在就壞的下載路徑)api/__init__.py 死碼 load_routes()| 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)。
chdir 固定工作目錄(解盤點#7 log 路徑)→ chown volume 目錄(解掛載 uid/gid 不合老問題)→ 起 binary。fonts-noto-cjk 等);WeasyPrint 的 .so 走 image apt 裝齊、不打進 binary(盤點#13)。build 期 fc-match 'Noto Sans CJK TC' 斷言——缺字型 PDF 回 200 但中文整段消失(盤點#5)。| 掛載點 | 用途 | 備註 |
|---|---|---|
上傳目錄(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_TENANT、DB_USERNAME 等,完整清單見討論稿盤點#14),落地版部署文件必列。「編得出來」≠「功能活著」——本案地雷多為靜默失效型,逐雷探針缺一不可(FR-064/065 復用):
| 探針 | 探的雷 |
|---|---|
| 登入 | 基本 API/DI wire 全量 |
| 任一分頁列表 | blueprint 載入完整 |
| i18n 中文回應 | .mo 檔有打包(盤點#4) |
| SSP docx 匯出 | 範本檔有打包(盤點#4) |
| 摘要報告 PDF 開檔驗中文 | WeasyPrint 系統庫+字型(盤點#5/#13) |
| 問卷上傳 | static/上傳目錄可寫(盤點#8/#9) |
| socketio 通知 | eventlet 編譯後可用(D1/D9) |
分層判準——「誰、多常、需要改它」:
| 層 | 判準 | 改動代價 | 例 |
|---|---|---|---|
| 編譯進 binary | 產品常數 | 改=出新版 | 上傳目錄常數、內建預設值 |
| 環境變數 | 每部署點不同 | 裝機定一次 | DB_HOST、CORS_ALLOWED_ORIGINS、LOG_DIR |
DB system_config |
營運中會調 | 線上改即生效 | 登入鎖定、密碼政策、JWT 效期、MFA |
第 1 刀:六套環境 class 塌成一套 Config
BaseConfig 為底);DevelopmentPremise / StagingPremise / StagingAwsBillowS / ProductionAwsBillowS / StagingAwsNics / ProductionAwsNics 六套子 class 全刪。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_URL、BaseConfig.DB_NAME="audit_manager" 一併清。.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 零改動)。json.loads 直接 traceback;json.loads 移進 loader,由統一入口(D9 / .1c)最早階段呼叫——loader 與統一入口同檔作業。DB_USERNAME / DB_PASSWORD 命名分歧一併對齊收斂(盤查發現 jedi 讀的變數名與主專案 .env 是兩套)。第 3 刀:營運參數遷移 DB system_config(內建預設+DB 覆寫)
LOGIN_MAX_LOCK_COUNT / LOGIN_USER_LOCK_TIME、CHANGE_PASSWORD_THRESHOLD_DAYS、LOGIN_INACTIVITY_THRESHOLD_DAYS、JWT_ACCESS/REFRESH_TOKEN_EXPIRES、MFA_REQUIRED。infra/system_config/system_config_root_reader.py 現成);編譯內建預設值,DB 有值覆寫,讀不到照預設跑(無啟動依賴)。本期不做 UI(root 管理介面留後續);migration 一支 seed(ON CONFLICT DO NOTHING),只套 DEV。死項清理(2026-08-14 盤查定案)
| 類別 | 項目 |
|---|---|
| config.py 直接刪 ×14 | TOKEN_TTL、USER_LOCK_COUNT、USER_PASSWORD_LIMIT_TIME(舊代,新代 LOGIN_* / CHANGE_PASSWORD_* 活著)、PROJECT_EXCEL_UPLOAD_DIR / PROJECT_EXCEL_SAMPLE_DIR / PROJECT_EXCEL_SAMPLE_FILENAME、USER_EXCEL_SAMPLE_DIR / USER_EXCEL_SAMPLE_FILENAME、ANSWER_SAMPLE_DIR / ANSWER_SAMPLE_FILENAME、LOGGING_FORMAT、FEEDBACK_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.test;NOTION_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_DIR、ANSWER_UPLOAD_DIR、REDIS_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 檔)。
🔴 排序要點(拆卡與派工必守)
%%{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
| 子任務 | 內容 | 對應 |
|---|---|---|
| .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。
| 子任務 | 內容 | 對應 |
|---|---|---|
| .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 全量)。
production Dockerfile:非 root 固定 uid(D8)/volume 佈局(盤點#10)/fc-match 斷言(盤點#5)/entrypoint chdir+chown(盤點#7/D8)/環境變數文件化(盤點#14 完整對照表)。
驗收:Smoke 七項探針全過(§5.6);重啟後上傳檔不丟;容器內找不到任何 .py 業務原始碼;process 以非 root uid 執行。
裁剪程式碼在 .1c 已寫掉;本棒主要是編譯後實測,是驗證棒不是開發棒:eventlet monkey-patch × Nuitka 相容性實測;卡關 → 啟動 D1 保底(換 gevent 等 async_mode,入口薄殼十幾行無祕密,業務模組仍全數受編譯保護)。
驗收:RUN_MODE=socketio 進程可建立連線並收發事件(socketio 通知端到端通)、不載 REST blueprint、scheduler 不雙跑;或保底方案落地且業務模組仍在編譯保護內。
.py 業務原始碼——客戶主機上找不到任何可讀業務原始碼,與 FR-062 license 執法構成雙保險。main_app.py 完全等價、動態 DI 掃描與資源定位行為不變(加檔即生效)。.env 原封不動起服務,行為零差異;新格式 .env 行為相同。