FR-065 · On-premise Installer — 需求討論稿 · 2026-08-16(v1 · D 項大致定案)

讓客戶自己把 Guidant AI 裝起來

FR-063 把 BE 編成了不帶原始碼的 Nuitka image,FR-064 給了它防竄改與授權鎖定。但今天要在一台客戶主機上從零裝出一套可用的系統,仍然只有一份 deployment-env.md 四步手動流程——沒有 DB 初始化、沒有 FE 交付形態、沒有 image 傳遞通路、沒有升級與回滾,每一步都要原廠工程師在場。本案要把「出貨 bundle → 客戶主機 → 可登入的系統 → 後續換版」整條路標準化成客戶可自助執行的 installer。

D 項大致定案(待轉 design.md) D1–D10/D12 已定案 2026-08-16 D11 seed 裁決 8/10 已定案 Web 設定精靈 已定案 待決策:D11.3/D11.5 保留+D13 待公司討論 母案 CM-1189 前作:FR-063 打包 · FR-064 防竄改 盤點依據:CM-1207 DB init 基線
§1

需求背景與目標

  • 母案:CM-1189(FR-065 Installer
  • 前作:FR-063(Nuitka 落地版打包,2026-08-15 收口)→ FR-064(防竄改偵測與授權鎖定,2026-08-16 收口)
  • 盤點輸入:CM-1207《DB 初始化基線盤點》(本資料夾 db-init-inventory.md,其 D1–D10 待裁決清單整合進本稿 §D11)

FR-063/064 兩案已把「跑起來之後」的問題解完:image 不帶原始碼、產物唯讀化、防竄改鎖定、license 綁機。還沒解的是「怎麼跑起來」

  1. 客戶側安裝資產現況=零。唯一的安裝素材是 docker/production/docker-compose.ymldeployment-env.md §0 的四步手動流程——那是寫給「會 docker、看得懂我們文件的自己人」的,不是給客戶的。
  2. DB 從零建庫沒有機制。186 張表、107 條 RLS policy、5 個 schema、一票必備 seed——現況全靠 DEV 這顆活了兩年的庫,客戶主機上沒有任何工具能重建它(CM-1207 已盤點完整清單與方案草案)。
  3. FE 沒有落地交付形態。正式部署至今是 zip dist scp 上主機手工擺放;有 Dockerfile 但只有 E2E 環境在用容器形態。
  4. image 沒有交付通路。build_image.sh 不含 push/save;Harbor 是內部 registry,air-gapped 客戶碰不到。
  5. 升級/回滾整段空白。換版指令只有「改 GUIDANT_VERSION 重跑 up -d」一句,無 migration 套用、無備份、無回滾語意。

目標(從 CM-1189 出發):讓客戶落地安裝標準化——一份出貨 bundle + 一支 installer,客戶自己(或一般 IT 人員照手冊)完成安裝、開通、日後升級,不依賴原廠工程師到場。硬前提是 FR-064 design 明文的部署假設:客戶環境可能完全 air-gapped,一切須離線自洽

§2

端到端旅程

2.1 首次安裝+開通旅程

安裝與開通有一個「雞生蛋」結構:license 綁機器指紋(sha256(/etc/machine-id)),但取指紋的唯一現成介面 GET /license/activation/machine-code 需要 JWT 登入——而無照態下系統雖起得來、license 軸以外全 403(/license//login 豁免,是設計好的逃生門)。所以旅程必須在裝機當下就把指紋交到客戶手上——D10 精靈定案後主要展示面是精靈頁,installer fingerprint 為保底(D6)。

%%{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'}}}%%
sequenceDiagram
    participant V as 原廠(build+LC 簽發站)
    participant C as 客戶安裝者
    participant H as 客戶主機
    participant S as Guidant AI 服務

    V->>C: 出貨 bundle(install.sh+image tars+bundle manifest+SQL/seed+手冊)
    C->>H: 解開 bundle,執行 install.sh
    H->>H: 環境檢查(docker/compose 版本、/etc/machine-id 非空、磁碟/記憶體)
    H->>H: docker load 各 image + 比對 bundle manifest digest(D3)
    H->>H: 產生基礎設施密碼組(cmmgr/cm_app)+ 寫 guidant.env
    H->>H: DB init(one-shot init 容器跑分段腳本:schema+字典 seed,不建 admin,D4)
    H->>S: docker compose up -d(PG/Redis/BE api/BE socketio/FE,D1/D2)
    S-->>H: healthcheck 全綠
    H-->>C: 印出:「請開 http://<主機>/ 完成設定」+ 一次性 setup token(D10)
    C->>S: 開站進 Web 設定精靈:貼 setup token → 建 admin 帳號(密碼自設)
    S-->>C: 精靈顯示機器指紋(sha256 machine-id,D6)+ license 匯入引導
    C->>V: 抄指紋(離線通路:mail/電話)申請簽照
    V->>V: LC 簽發站簽 license(綁指紋)
    V-->>C: 回傳 .license 檔(PEM armor)
    C->>S: 在精靈匯入 license(復用 FR-062 開通頁)→ 完成安裝進登入頁
    S-->>C: 開通完成,全功能可用
圖 1 — 首次安裝與開通端到端時序(含原廠簽照往返)

2.2 升級旅程

%%{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'}}}%%
sequenceDiagram
    participant V as 原廠
    participant C as 客戶安裝者
    participant H as 客戶主機
    participant D as PostgreSQL

    V->>C: 升級 bundle(新版 image tars+增量 migration+manifest)
    C->>H: 執行 install.sh --upgrade
    H->>D: ① pg_dump 全量備份(落地到指定備份目錄)
    H->>H: ② docker load 新 image + digest 驗證
    H->>D: ③ one-shot init 容器套增量 migration(依 schema_migrations 判斷起點,align_migrations 演算法移植)
    H->>H: ④ 改 GUIDANT_VERSION → docker compose up -d(自動 recreate)
    H-->>C: healthcheck 全綠,升級完成
    Note over C,D: 回滾=restore 備份 + GUIDANT_VERSION 回舊 tag 重跑 up -d
圖 2 — 換版升級時序(含備份與回滾語意,D5)
§3

現況盤點

以下是撰稿前實際查證的現況(來源:FR-063 deployment-env.mddocker/production/、CM-1207 盤點、FE repo 實檔、FR-064 SUMMARY §9/§10)。

元件 現況 本案動作
BE image guidant-ai-be:1.14.0:ubuntu:22.04 基底(glibc 對齊 build 機)、中文字型/LibreOffice/WeasyPrint 原生庫已進 image、產物 root:root + entrypoint setpriv 降權 uid 1000、不帶 python/原始碼 直接沿用,不動
BE build 管線 scripts/build/:build_all.sh → smoke → build_image.sh;integrity manifest 簽章在 build_release.sh Step 4 尾端送 LC 簽 擴充:FE image build(D2)+ bundle 打包段(D3)
compose 定式 docker/production/docker-compose.yml:僅 guidant-api(8000)+guidant-socketio(8002) 兩服務;read_only+tmpfs;7 個掛載(static/log/home/tmp/pki/machine-id/agent 憑證);無 DB/Redis/MinIO/FE;檔頭明文警語:獨立 compose project、up -d 必帶 service 名以免 recreate 同 project 的 DB 擴充為完整 stack(D1/D2),並處理 recreate 風險
FE 交付 有 Dockerfile(node build → nginx:alpine,ARG NGINX_CONF 可換 conf);build:PRD 走相對路徑(VITE_API_URL=/api/1.0、WS 空字串→同源)故不需 per-customer rebuildnginx.e2e.conf 是現成同源反代範本(/api/1.0→be:8000、/socket.io→be-socketio:8002 含 WS upgrade),但檔頭明標「限 E2E」;正式部署現況=zip dist scp 手工擺放;build-time 綁死的只剩 VITE_TURNSTILE_SITE_KEYVITE_DEV_MODE(PRD 現值 true,落地要檢討) 新增 production 容器形態(D2)
DB 初始化 零。DEV 實況:5 schema/186 表/107 條 RLS policy/156 sequence;cmmgr/cm_app 帳號建立紀錄不在 repo;CM-1207 已產出完整基線盤點+方案 A/B/C 草案(傾向 B 分段腳本+C one-shot init 容器載體) 本案核心交付(D4)
migration/升級 scripts/sql/manifest.tsv active 138 筆(127 筆 envs=*、11 筆環境限定不屬基線);align_migrations.sh 的 diff+--apply 演算法現成,但 target 寫死內部 IP、密碼讀 repo .env——客戶端要重做連線層並把 SQL+manifest 打進交付物;換版現況只有「改 GUIDANT_VERSION 重跑 up -d」,無備份/回滾/相容性檢查 升級流程(D5)
license/指紋 指紋=sha256(/etc/machine-id)common/license/machine_fingerprint.py);取指紋唯一介面需 JWT 登入;無照態服務照起但 license 軸外全 403(/license//login 豁免);license 檔 PEM armor(.license)貼 FE 頁匯入,BE 不落檔;unlock.token 落 /opt/guidant/pki 開通旅程(D6);宿主前提 /etc/machine-id 非空(CM-1242 實測)
簽章公鑰 common/license/public_keys.py 硬編進 image,現有 DEV/STG/POC 三把,無正式 PROD 鑰——FR-064 遺留待辦,出貨前必須新增鑰+重 build image D7
外網依賴 Turnstile captcha 內網會因連不到 Cloudflare 登入失敗(deployment-env.md §3 標「需評估」未定案);LC 回報是 best-effort 已離線自洽 D8
image 交付通路 完全沒寫。build_image.sh 無 push/save;Harbor 是內部 registry;docs 內 docker save 先例只有 FR-058 agent image D3
FR-064 出貨遺留 未進版(現標 1.14.0);image 未推 Harbor(本機 tag 已打);K8s liveness 下鎖定殼會 CrashLoop(compose 不受影響) D12 歸屬裁決
客戶側安裝資產 零(唯一素材=compose + deployment-env.md §0 四步手動流程) 本案主體
§4

e2e-env 借鑑分析

test repo compliance-manager-test/e2e-env/ 已有一套跑得起來的 8-service 全套 compose(postgres/redis/mailpit/minio/minio-init/be/be-socketio/fe)——它每天在驗證「這組服務能在一個 compose 內良好共處」。user 指示:分析借用、不整抄。逐項拆成可移植與不可抄兩表:

可移植(借拓撲與機制)

# 項目 為什麼要借
1 PG service 形態:postgres:16+顯式 POSTGRES_INITDB_ARGS--encoding=UTF8 --locale=en_US.utf8)+named volume+pg_isready healthcheck 不顯式指定 encoding/locale,客戶主機的預設 locale 會滲進 initdb,跨機不可重現
2 Redis 必須 ACL user 而非 requirepass BE config 組的是 redis://<user>:<pass>@host 形式,requirepass 只認 default user 會連不上;--user "$REDIS_USER" on ... "+@all" 段必抄+appendonly no+healthcheck 用 ACL user 實測 ping
3 同源 nginx 反代拓撲/api/1.0→be:8000、/socket.io→be-socketio:8002(必須分開兩 service)、SPA try_files build:PRD 相對路徑產物正好互補,單一入口不需 per-customer rebuild
4 nginx resolver 動態解析resolver 127.0.0.11 valid=10s+變數 upstream 不寫的話 BE restart 換 IP 後 nginx 快取舊 IP、全站 502——落地客戶自己重啟服務是常態,必抄
5 反代調校client_max_body_size 0、API read_timeout 600s+buffering off、socket.io 3600s+WS upgrade 大檔上傳與長工時匯出(SSP docx/PDF)會踩預設 timeout
6 depends_on condition: service_healthy 啟動順序 DB/Redis ready 才起 BE、BE healthy 才起 FE——installer「up 完即可用」的前提
7 一 image 兩服務(RUN_MODE)+YAML anchor 產品 compose 已有同形態,e2e-env 驗證了與 DB/FE 同 compose 時也成立
8 MinIO 選配段形態:minio+mc init job(--ignore-existing 冪等) D1 已定 MinIO 選配,bucket init 的冪等 job 形態直接可用
9 零硬編憑證+.env.example 規範(只列 key 與用途) 與本案「密碼由 installer 產生、憑證不入版控」一致
10 BE 必要 env 踩坑清單ENABLE_MULTI_TENANT 必須顯式 "true"(缺了 RLS 靜默擋、查無資料無錯誤);DRIVE_TOKEN_ENCRYPTION_KEY 啟動即檢;DETECTION_TOOL_ENCRYPTION_KEY 是另一支(錯誤訊息會誤導);DB_PORT 顯式 5432(BaseConfig 硬編 25432) 每一條都是 e2e-env 實踩過的啟動失敗/靜默故障,.env 範本照單全收
11 容器內 vs 對外 port 分離(BE 連 service:5432,對外映射另計) 客戶主機 port 衝突時只改對外映射,服務互連不動
12 HTTPS 是功能必要不是加分:FE aiChatStore 用 crypto.randomUUID(),非 secure context 下為 undefined 且錯誤被吞→登入卡死零訊息;443+憑證掛載(bind ro、私鑰不進 image)是必要設計 不是資安加分項——不上 HTTPS(或 localhost 之外裸 HTTP)產品直接壞

不可抄(測試專用)

# 項目 為什麼不抄
1 golden dump→TEMPLATE clone 整套 產品是空庫 init(CM-1207),不是 restore 測試基準
2 pg_dumpall --roles-only 搬 role(連 SCRAM 雜湊) 產品 init 新建 role+安裝時生成密碼,不搬既有雜湊
3 sanitize-golden 全套 空庫沒有「洗掉開發資料」的問題
4 mailpit 假 SMTP 產品接客戶真 SMTP
5 BE E2E image(帶原始碼跑 root dev server) 產品用 docker/production/Dockerfile Nuitka 產物(不帶原始碼、降權)
6 ENV=DEVELOP_PREMISE 產品走正式 config
7 fe-image 兩層疊 image 產品單層乾淨 build
8 測試帳號/auth-state/排他鎖/污染偵測/奇數 port/e2e-postgres 寫死 service 名 全是測試編排產物;產品 service 名用中性名(如 guidant-db

結論:e2e-env 驗證了「這組服務能在一個 compose 內良好共處」——借拓撲與機制三條線(服務組成/互連/啟動順序),不借資料來源、image 血統、測試編排三條線。

§5

決策清單 D1–D12

2026-08-16 批次拍板:D1–D10/D12 全數定案,D11 十項落裁決(8 定案、2 保留——D11.3 與 PM 討論、D11.5 .2 棒前定)。

✅ D1 — 依賴服務形狀(已定案 2026-08-16):PG/Redis compose 全包為預設

現行 compose 定式只有 BE 兩服務,DB/Redis/MinIO 都假設外部已存在。落地客戶(尤其 air-gap 單機)多半沒有現成 PG 16/Redis,要求自備等於把最難的一段推回給客戶——故拍板全包為預設

  • PG/Redis image 隨 bundle 出貨,compose 全包(air-gap 單機客戶是主場景)。
  • 客戶已有自備服務時,由客戶自行修改 docker-compose 內容切外部——compose 內寫註解引導:改 DB_HOSTREDIS_HOST 環境變數、移除對應 service 段。installer 不做 --external-db 類參數;參數化外接模式列為未來可選加分項。
  • MinIO 選配不預設(storage 預設 local)維持。
  • recreate 風險防護照原建議進實作設計:DB 與應用同 compose project 時 up -d 不帶 service 名會 recreate 所有容器(含 DB,CLAUDE.md 有案)——傾向 DB/Redis 獨立成另一個 compose project(或至少檔內大字警語+installer 包裝所有 up 指令、永遠帶 service 名),不讓客戶裸打 up -d

✅ D2 — FE production image 與反向代理(已定案 2026-08-16):nginx.onprem.conf+FE production image+單一入口 port

定案:照原建議採納——新增 nginx.onprem.conf+FE production image,進 build 管線、tag 與 BE 同版號;單一入口 port(80/443)由 FE nginx 統一對外。VITE_DEV_MODEVITE_TURNSTILE_SITE_KEY 在實作棒查清語意後定值。

FE 已具備容器化的全部零件:Dockerfile 支援 ARG NGINX_CONF 換 conf、build:PRD 產物走相對路徑(同源即可用,不需 per-customer rebuild)、nginx.e2e.conf 是現成的同源反代範本(含 /socket.io WS upgrade 與長工時 timeout)。缺的只是「產品版」的定案:一支正式 nginx conf(以 e2e 版為底、upstream 指向 compose service 名)+ FE image 進 build 管線與 BE 版號對齊。單一入口 port(80/443)由 FE nginx 統一對外,BE 8000/8002 收進內網。

連帶要檢討兩個 build-time 值:VITE_DEV_MODE(PRD 現值 true——語意與影響面待實作棒查清,落地版直覺上不該開)與 VITE_TURNSTILE_SITE_KEY(現值是 Cloudflare 測試 key,與 D8 連動)。

✅ D3 — image 交付通路與完整性驗證(已定案 2026-08-16):docker save tar bundle 為主+manifest digest 比對必做

定案:照原建議——tar bundle 為主(air-gap 必然);bundle manifest digest 比對進 installer 必做;Harbor pull 文件化為有網客戶的替代路徑;cosign 評估後另議。

air-gap 客戶拉不到 Harbor,通路只剩實體傳遞。選項:(a) docker save tar bundle——BE/FE/PG/Redis 全部 image 打進一份出貨包;(b) Harbor pull(僅限有網路且信任外連的客戶)。

FR-064 D7 已把「image 層驗證」劃入本案:bundle 內附 manifest 列各 tar 的 sha256 digest,installer docker load 後比對,確保客戶手上的 image 與原廠出貨一致(傳遞途中未被替換)。cosign 之類的簽章方案列為評估項,不在 v1 硬性範圍。

✅ D4 — DB init 載體(已定案 2026-08-16):分段 SQL 腳本(B)+one-shot init 容器(C 載體),照 CM-1207 §5.4

定案:照原建議——B+C 載體。DB init=分段 SQL 腳本(基線真相取 DEV pg_dump 整理,參數化注入密碼/命名)+one-shot init 容器為執行載體(跑完即棄,最高權限憑證只存在建庫當下)。init 容器與升級(D5)的 migration 容器共用同一形態。

CM-1207 §5 已完整比較三案:A(單一 3 萬行 init.sql,不建議)、B(分段腳本組 scripts/init/00-cluster … 99-stamp+init.sh)、C(entrypoint 自動 init,服務容器得持有 cmmgr 憑證、違反最小權限)。§5.4 取捨結論:B 為主、C 的 one-shot init-job 容器作為執行載體——installer 產參數(兩組 DB 密碼、admin 初始密碼)→ 起 init 容器跑 init.sh → 成功後才起服務容器(服務只拿 cm_app 憑證)。基線真相取 DEV pg_dump(非 224 支 migration 重放);權限不逐表 GRANT 而是複製 default privileges+event trigger 機制;收尾 stamp schema_migrations baseline marker 讓後續增量 migration 無縫接軌。

✅ D5 — 升級流程與回滾語意(已定案 2026-08-16):四步包成 install.sh --upgrade+回滾=災難逃生

定案:照原建議——升級定式化為四步、包成 install.sh --upgrade;回滾=restore 備份+回舊 tag(明文:災難逃生非常規操作);相容性檢查 v1 最簡版(bundle manifest 標適用起始版號區間,不符即拒跑)。

四步:① pg_dump 全量備份 → ② load 新 image(含 digest 驗證)→ ③ init 容器套增量 migration(align_migrations.sh 的 diff+apply 演算法移植到客戶端形態:連線參數外部注入、SQL+manifest.tsv 隨 bundle 出貨、依 schema_migrations 判斷起點)→ ④ up -d recreate。回滾語意=restore 備份+GUIDANT_VERSION 回舊 tag(明文告知:回滾會丟升級後產生的資料,屬災難逃生不是常規操作)。

✅ D6 — 首次開通的「雞生蛋」(已定案 2026-08-16):指紋主展示面=Web 設定精靈頁+installer fingerprint 保底

定案:指紋主要展示面=Web 設定精靈頁(顯示+複製鈕);installer fingerprint 子命令為保底查詢工具(裝機尾端自動印+可隨時重跑,服務起不來時可用),演算法與 machine_fingerprint.py 一字不差;實作棒需加「兩邊算出同值」的驗證步驟。

取機器指紋的唯一現成介面需 JWT 登入,但簽照又需要指紋——雖然無照態 /login 有豁免(登入後可打 machine-code API),流程仍繞。installer 在裝機尾端直接算指紋(與 BE 同演算法:sha256(/etc/machine-id),宿主上一行 shell 即可),客戶抄指紋走離線通路給原廠簽照,拿到 .license 後進系統貼照開通。

✅ D7 — 正式簽章鑰(PROD kid)(已定案 2026-08-16):劃入本案 .2 棒

定案:PROD 簽章鑰劃入本案 scope(.2 棒,隨 bundle 打包段)——LC 端生成 PROD 鑰對 → public_keys.py 新增 PROD kid → 重 build image;出貨 bundle 一律以含 PROD 鑰的 image 產出。

public_keys.py 現有 DEV/STG/POC 三把,無 PROD 鑰。這是出貨的硬前置:客戶拿到的 image 必須能驗「正式簽發鑰」簽出的 license/unlock token/integrity manifest,否則出貨即「未知的 kid」([[feedback_multi_env_signing_key_pairs_with_public_key_commit]] 四步一組的教訓適用)。

✅ D8 — 外網依賴停用(已定案 2026-08-16):落地版預設停用 Turnstile

定案:落地版預設停用 Turnstile(BE/FE 一對開關,實作棒查清現況後補齊缺的開關);bundle 手冊附「本產品可能外連清單與停用法」。

Turnstile captcha 在內網連不到 Cloudflare 會直接讓登入失敗,deployment-env.md 標「需評估」至今未定案。BE 端 TURNSTILE_SECRET_KEY、FE 端 VITE_TURNSTILE_SITE_KEY 各自的「停用開關」現況(不填是否即停用?有無顯式 flag?)待實作棒查清。其他外網呼叫盤點:LC 在線回報已是 best-effort 離線自洽;AI API/Google Drive 屬功能模組、客戶不啟用即不外連——仍應在實作棒做一次完整外連盤點收進手冊。

✅ D9 — Windows 支援(已定案 2026-08-16):僅出評估文件,不實作不承諾

定案:本案僅出評估文件+前提清單(WSL2 machine-id 行為、Docker Desktop licensing、效能界線),不實作不承諾支援;有真實客戶需求時另開案。

母案卡內文傾向:不做原生 Windows 版,引導客戶裝 Docker Desktop/WSL2 跑同一套 Linux image。技術上可行但有未驗證變數:WSL2 的 /etc/machine-id 穩定性(指紋錨點)、volume 效能、Docker Desktop 授權條款(企業要付費)。

✅ D10 — installer 形式(已定案 2026-08-16,兩次拍板):純 bash(不做 TUI)+首次啟動 Web 設定精靈

user 兩次拍板:第一次定「純 bash install.sh,不做 TUI」;同日續拍採混合式——bash 管基礎設施段+「首次啟動 Web 設定精靈」管應用設定段(業界主流模式:Portainer/GitLab/Nextcloud/Jenkins 式)。

分工定式:

  1. install.sh(bash 純文字):環境檢查 → load image+digest 比對 → DB init(one-shot init 容器:schema+字典 seed,不建 admin)→ 起服務 → 尾端印「請開 http://<主機>/ 完成設定」+一次性 setup token(防搶注,Jenkins 式——避免精靈開放期間被非安裝者搶先建帳號)
  2. Web 設定精靈(首次開站偵測「無 admin」自動進入):貼 setup token → 建管理員帳號(密碼客戶自設,不經終端留痕)→ 顯示機器指紋+license 匯入引導(復用 FR-062 開通頁)→ 完成安裝、進正常登入頁
  3. SMTP/storage 等裝完後台設定頁慢慢設(不進精靈必填)

代價明文:精靈是新的 FE+BE 開發(未初始化態偵測、setup token 驗證、建 admin 的特殊通道——繞過既有權限體系的一次性 API,用後即封),約多一棒工作量;user 拍板值得(體驗優先)。TUI 維持不做;systemd 維持不另立 unit(installer 確保 systemctl enable docker 即可)。

被排除方案:純 shell 印生成密碼+首登強制改密(機制現成但體驗差一階,列 fallback)。

D11 — seed 出貨範圍裁決(承接 CM-1207 §3.4)

CM-1207 盤點把「拿不準的 seed/schema 項目」列成十項待裁決,原編號 D1–D10 為避免與本稿撞號改編為 D11.1–D11.10。字典類必備 seed(operations/system_menus/ui_routes/capabilities/root tenant+org/Administrator 角色/RUNTIME_CONFIG 等,CM-1207 §3.1)已明確屬出貨集,不在此重列;十項已於 2026-08-16 逐項落 user 裁決——8 項定案、2 項保留待議(D11.3 與 PM 討論/D11.5 .2 棒開工前定)

✅ D11.1 — CMMC 框架 catalog 整族(已定案 2026-08-16):出貨框架只有 CMMC 2.0,走應用層匯入

定案:出貨框架清單只有 CMMC 2.0;初始化資料由 user(決策者)準備。出貨形式採「應用層匯入」——出廠帶 OSCAL JSON 檔、服務起來後走既有匯入流程灌入(與 D11.4 同一模式),DB init 不 seed catalog 資料列。

背景:DEV 的 79 個 catalog 混雜「內建 CMMC」與「開發測試匯入的」,schema 上分不出來——正因如此不走 seed dump 直灌。

✅ D11.2 — 內建 workflow_templates(已定案 2026-08-16,範圍待核對):只帶「預設內建」最小集

定案:只帶「預設內建」的最小集——DEV 的 191 筆(provider=Billows-Official)屬混雜舊資料,非全部出貨;snapshot 類專案運行產物與 tenant 1 的 59 筆不帶。實作棒需與決策者核對確切出貨清單後落地。

⏸ D11.3 — 樣板 SSP/MF defaults 家族(保留——與 PM 討論後另定)

裁示(2026-08-16)先留空——出貨基線不帶樣板 SSP/MF defaults,與 PM 討論後另定範圍。

背景:新客戶初始該有哪些「內建模組框架+樣板 SSP」?DEV 現況分不出產品內建 vs 開發試作。

✅ D11.4 — 檢測 Profile 庫(已定案 2026-08-16):隨 bundle 帶檔+既有匯入機制灌入

定案:採「隨 bundle 帶檔案+服務起來後走既有匯入機制灌入」——DB init 不 seed 資料列,與 D11.1 CMMC 初始化同一模式(單一維護面;避免 seed 列與 storage 檔案脫鉤的壞連結)。

背景:資料列是產品資產(dev-sec 基準+TWGCB 8 支),但 profile 版本的檔案實體在 storage(MinIO/local)——DB seed 灌了列、檔案不在就是壞連結,正是選匯入路線的原因。

⏸ D11.5 — public.labels 35 筆(保留——.2 棒開工前定)

裁示(2026-08-16):先不裁,.2 棒開工前定。傾向:預設只帶前段系統字典(系統回饋/系統操作/選單分類),後段 textbook-/resource- 不帶。

✅ D11.6 — ISSUE_INTEGRATE_CONFIG(已定案 2026-08-16):出廠帶 disabled 空殼

定案:功能對落地客戶開放——出廠帶 disabled 空殼,客戶後台自行啟用接自己的 GitHub/GitLab。

✅ D11.7 — public.alembic_version(已定案 2026-08-16):暫留

定案:此表係舊版 flask-migrate 遺留(user 證實,來源已明)——暫留,基線帶此表、不清除。

✅ D11.8 — root tenant/org/admin 出廠命名(已定案 2026-08-16):由客戶命名

定案:root tenant 顯示名由客戶命名——在 Web 設定精靈輸入(D10 流程內);root id=1 是硬約定不可變,這裡只裁顯示名。DEV 值 Guidant.AIGuidant.AI Manageadmin 不沿用為出廠固定值。

連動註:若 D13 採納系統殼方案,客戶命名對象改為第一個業務租戶、root 顯示名固定(如 System)。

✅ D11.9 — debug view 與 cm_app_group(已定案 2026-08-16,同日追加裁示):debug view 確認零引用後連 DEV 一併刪除、空群組保留

定案(追加裁示後):debug view 不只「不出貨」——debug_org_unitsv_tenant_parent_debug 兩支 確認沒程式在用後,連 DEV 也一併刪掉。實作棒動作:grep 全 codebase 確認零引用 → 開 migration drop 兩支 view(DEV 先套,走既有 sql-migration 流程與 DROP 前安全查核鐵則)→ 出貨基線自然不含。cm_app_group 空群組保留(利未來多 app 帳號,照 CM-1207 傾向)。

✅ D11.10 — 落地版 DB 名(已定案 2026-08-16):統一 guidant_ai

定案:落地版 DB 名統一 guidant_ai——採 2026-08-08 DB 命名新制(<系統>_{dev|stg}、prod 不帶後綴,落地新客戶屬新建置適用新制);installer 的 INIT_DB_NAME 預設值定此。

✅ D12 — FR-064 出貨遺留歸屬(已定案 2026-08-16):①②劃入本案,③不實作但設計預留

FR-064 收口時明文留下三件:① 進版(現標 1.14.0,未走 version-bump);② image 推 Harbor(本機 tag 已打);③ K8s liveness 下鎖定殼會 CrashLoop(compose 落地不受影響,上 K8s 才需把鎖定態排除在 liveness 外)。

定案:①進版(version-bump)與 ②image 推 Harbor 劃入本案出貨動作(出貨 bundle 必然要定版,推 Harbor 是原廠內部歸檔)。③ 不在本案實作,但設計預留——user 明示「要預留 K8s」:design.md 需把「鎖定態排除在 liveness 之外的設計要點」與 cluster 化前提備忘一起記為正式預留節(作為未來 K8s 案的起點),compose 服務契約保持可翻譯性(服務名/port/env 介面清楚)。見文末〈cluster 化前提備忘〉同步標註。

🏢 D13 — root tenant 定位:系統管理殼 vs 可做業務(待公司討論)

議題:落地版 root tenant(id=1)要不要給客戶做業務,還是只當 super admin 系統層?

市面對照(皆為「系統層=管理殼、業務在下層」同構):

  • Keycloak:master realm 只管其他 realm,官方明文建議業務絕不放 master realm
  • GitLab self-managed:Admin Area 獨立系統介面,業務在 Group/Project 層
  • Atlassian DC:系統管理員與空間/專案管理分層
  • Payload CMS:無 root tenant 概念,super admin 是「無租戶歸屬的旗標」跨租戶管理(架構與我們 B-2 相反——我們系統資源必掛 root tenant 避免 NULL 孤兒,不可學它的形狀,但「系統身分不做業務」的體驗邊界值得抄)

我方架構現況:B-2 模型下 root tenant 本來就是系統資源掛載點(平台 capability/系統設定/Administrator 角色),DEV 大家拿 root admin 直接做業務是開發便利非設計意圖。

建議方案:root tenant=super admin 的家,只做系統管理(license 匯入/SMTP/storage/建租戶/系統日誌);業務一律走第一層租戶。精靈流程配合改為:建 super admin(root 層)→ 引導建第一個業務租戶(D11.8 客戶命名的就是它,root 顯示名可固定如 System)→ 建租戶管理員 → 日常用租戶管理員登入。

強制程度兩檔(待公司裁):

  • (a) 軟邊界(v1 便宜):精靈強制引導+手冊明文,root 下技術上仍可按到業務功能
  • (b) 硬邊界(多工):root 層介面實際收斂藏掉業務功能——現況距離多遠需實作棒盤點

首腦建議:v1 軟邊界+精靈強制引導;硬邊界視盤點與 PM 意見排 v2。

連動註記:此裁決影響 .3 棒精靈流程與 D11.8 的落法(客戶命名對象從 root tenant 改為第一個業務租戶)。

§6

階段拆分草案

粗略切棒,轉 design.md 時細拆。階段重切(user 2026-08-16 指示):先切一個「docker compose 起整個服務」可獨立驗收的地基棒當 .1;原 .1 的 bundle 打包與 PROD 簽章鑰移到 .2。

階段 內容 驗收
**FR-065.1 full-stack compose(地基棒) 產品 compose 全 stack:PG+Redis+BE api/socketio+FE production image(nginx 同源反代單一入口)+MinIO 選配段(D1/D2);FE image build 定案(nginx.onprem.confbuild:PRD);.env 範本;自備 DB/Redis 的註解引導。DB 先用暫行手段(如手動 restore)驗通拓撲,正式空庫 init 屬 .2** 乾淨機器上 docker compose up -d 全組 healthy、FE 反代登入通
FR-065.2 DB init+installer 殼 分段 init 腳本+one-shot init 容器(CM-1207 方案,D4;schema+字典 seed,不建 admin);install.sh(環境檢查/load image/產基礎設施密碼/起 stack/尾端印 setup token 而非 admin 密碼,D10);bundle 打包(docker save+digest manifest,D3);PROD 簽章鑰(D7 定案:LC 生 PROD 鑰對+public_keys.py 新增 kid+重 build image);seed 依 D11 裁決落地(D11.2 清單開工前與決策者核對、D11.5 labels 開工前定;CMMC/檢測 Profile 走應用層匯入不 seed) 空機一鍵裝到「服務全綠、待精靈設定」
FR-065.3 開通精靈與升級 Web 設定精靈(FE 精靈頁+BE setup token/建 admin 通道,D10);installer fingerprint 子命令+開通流程整合(D6);--upgrade 四步+回滾(D5);Turnstile 停用落地(D8) 空機裝完 → 瀏覽器精靈建帳號 → 匯照開通 → 升級 → 回滾端到端
FR-065.4 文件與驗收 客戶安裝手冊;乾淨主機端到端真機驗收;Windows 評估文件(D9) 全鏈真機走一遍

依賴關係相應調整:.1 依 D1/D2(均已定案)即可開工;.2 依 D3/D4/D7/D10/D11(均已定案或裁定,餘 D11.2 清單核對與 D11.5 於開工前補定)+.1 完成;.3(含 Web 設定精靈,依 D10 定案)依 .2;.4 收全鏈。D12 的進版與推 Harbor 屬出貨動作,掛在 .2 bundle 打包段前後執行。

§7

附:宿主前提清單草案

installer 環境檢查段的初稿,門檻數值待實作棒定案:

  • OS:Linux x86_64(glibc ≥ 2.35——BE binary 在 Ubuntu 22.04 編出,基底鏡像已自帶 glibc,宿主只需能跑 docker;此條實際約束的是 docker 本身)
  • Docker:Docker Engine(最低版本待定,需支援 compose plugin v2)+ docker compose plugin;systemctl enable docker(開機自起前提)
  • /etc/machine-id 非空test -s /etc/machine-id 必過(CM-1242 實測:缺檔時 docker 會建成目錄、指紋退回 MAC 且每次重啟漂移;systemd 系開機安裝即有,極精簡系統用 systemd-machine-id-setup 補)。⚠️ machine-id 一旦重生成,既有綁機憑證全部失效——只在「本來就沒有」時補
  • 磁碟:門檻待定(image tars+DB volume+上傳檔+log+pg_dump 備份空間;bundle 本身估數 GB 級)
  • 記憶體/CPU:門檻待定(PG+Redis+BE gunicorn 4 workers+socketio+nginx 全包時的最低規格,實作棒實測定)
  • 時區:主機 TZ 與部署一致(容器 image 內建 Asia/Taipei,跨時區覆寫但不可各自為政——deployment-env.md §4 時區段)
  • 網路:無外網前提可運作(air-gap 自洽);需開放的對內 port:單一入口 80/443(D2 已定案)
§8

附:cluster 化前提備忘

user 問過未來 cluster(K8s/多節點)支援。compose→K8s 的翻譯本身是機械工,真門檻在應用層假設,先記錄不動:

  1. 機器指紋綁單機sha256(/etc/machine-id))——多節點下每台指紋不同,license 模型要改(改綁 cluster identity 或浮動授權)。
  2. 鎖定殼 vs liveness:FR-064 已知,K8s liveness 下鎖定態會 CrashLoop,需把鎖定態排除在 liveness 判定外。
  3. tamper 偵測的 FS 落點/opt/guidant/pki、unlock.token)——多副本共享或各自持有需重新設計。
  4. storage 需切 MinIO/S3:local storage 綁單機磁碟,多副本必然外部化。
  5. socketio 多副本需 sticky session(或改 message-queue adapter)。

D12 裁示同步(2026-08-16):user 明示「要預留 K8s」——本案不做 cluster 實作,但 design.md 需把本清單+「鎖定態排除在 liveness 之外的設計要點」(上列第 2 項)記為正式預留節,作為未來 K8s 案的起點;compose 寫成乾淨的服務契約(服務名/port/env 介面清楚)保持可翻譯性。未來有真實需求另開 FR。