接手這個 session 請直接從「待辦事項」開始。branch = feature/ssp-import-export-phase2。本次延續上一份 handoff(2026-05-22-docx-format-handoff.md — B6 docx template 重寫),不重複 B6 內容。
GET /api/1.0/module-frame/<uid>/ssp-export?format=pdf(或 format=odt)回 500,log:
FileNotFoundError: [Errno 2] No such file or directory: 'libreoffice'
app/oscal/service/export/ssp_libreoffice_converter.py:61 subprocess.run(...)
DOCX 不受影響(不走 LibreOffice)。
SspLibreOfficeConverter.__init__ 預設 libreoffice_cmd="libreoffice",DI 也沒覆寫。但 LibreOffice 在不同平台 binary 名稱不同:
| 平台 | 執行檔 |
|---|---|
| Linux | libreoffice |
| macOS Homebrew | /opt/homebrew/bin/soffice |
| macOS app bundle | /Applications/LibreOffice.app/Contents/MacOS/soffice |
開發機只有 soffice,因此 subprocess 找不到 libreoffice binary。
app/oscal/service/export/ssp_libreoffice_converter.py
_resolve_libreoffice_cmd() 自動偵測:env var LIBREOFFICE_CMD → PATH 內 libreoffice / soffice → macOS app bundle 絕對路徑 → fallback "libreoffice"None,會觸發自動偵測;顯式傳 string 可 override(測試或客製化部署用)在本機驗證 _resolve_libreoffice_cmd() 回傳 soffice ✓。
使用者反映 DOCX 匯出的 Hardware Inventory section 沒資料。
3699fc01-3398-40af-be20-ef7913451a27 透過 oscal_profile_uid=68882112-… 對到 profile_id=122,有 3 個 SSP(id 250/251/252),三個 SSP 各自都有 hardware items(3/4/3 筆)module_frame_ssp_resources_service.list_resources():回 {devices: 3, info_systems: 3} ✓MfSspContentLoader.load():model.inventory_items 3 筆 ✓SspDocxGenerator._build_context():context dict 的 inventory_items 3 筆 ✓gen.generate(model):產出 DOCX 的 Hardware Inventory table 正確含 3 rows(Windows server 2025 ERP System / D365 DB Server / Windows Server AD Service)HEAD code (4fde105) 與 working tree(B6 改版)兩個版本都跑過,都正確產 3 列。
Code path 沒問題。空白是因為 BE process 尚未重啟,跑的是上一個 session 之前的舊版 code(可能更舊,舊到 inventory loader 尚未實作完整)。重啟即解決。
| 檔案 | 類型 | 說明 |
|---|---|---|
app/oscal/service/export/ssp_libreoffice_converter.py |
小改 | 加 _resolve_libreoffice_cmd() binary 自動偵測 |
docs/changelog/2026-05-22-fix-ssp-export-libreoffice-binary-detect.md |
新增 | changelog |
上一個 session(B6)的未 commit 改動仍在,本 session 沒 commit 任何東西。完整未 commit 清單(含 B6 + 本 session):
M app/oscal/service/export/ssp_docx_generator.py # B6
M app/oscal/service/export/ssp_export_app_service.py # B6
M app/oscal/service/export/ssp_export_model.py # B6
M app/oscal/service/export/ssp_libreoffice_converter.py # 本 session
M app/oscal/service/export/ssp_mf_content_loader.py # B6
M app/oscal/service/export/ssp_version_content_loader.py # B6
M app/oscal/templates/ssp/ssp_cmmc_template.docx # B6
M di_containers/oscal/oscal_containers.py # B6
M scripts/generate_ssp_docx_template.py # B6
?? docs/changelog/2026-05-22-feat-ssp-docx-template-rebuild.md # B6
?? docs/changelog/2026-05-22-fix-ssp-export-libreoffice-binary-detect.md # 本 session
?? docs/features/FR-011.2-2605-ssp-import-export-phase2/handoff/2026-05-22-docx-format-handoff.md # B6
?? docs/features/FR-011.2-2605-ssp-import-export-phase2/handoff/2026-05-22-export-libreoffice-and-inventory-handoff.md # 本 session
?? docs/features/FR-011.2-2605-ssp-import-export-phase2/implementation-plan-docx-template-rebuild.md # B6
M pyproject.toml # dev-only path dep, NOT to commit
重啟 BE(最關鍵 — 兩個本 session 修的問題都需要重啟才生效,B6 改動也還沒生效)
lsof -ti:8000 | xargs kill -9
nohup python main_socketio.py > /dev/null 2>&1 &Smoke test 三種格式(每種開啟確認):
GET /api/1.0/module-frame/3699fc01-3398-40af-be20-ef7913451a27/ssp-export?format=docxGET /api/1.0/module-frame/3699fc01-3398-40af-be20-ef7913451a27/ssp-export?format=pdfGET /api/1.0/module-frame/3699fc01-3398-40af-be20-ef7913451a27/ssp-export?format=odt驗收點:
Windows server 2025 ERP System / D365 DB Server / Windows Server AD Service)ERP SYSTEM / Jira / AlienVault)Commit 所有異動(smoke test 通過後)
建議分兩個 commit(兩個獨立主題):
Commit A — B6 docx template 重寫:
git add app/oscal/service/export/ssp_docx_generator.py \
app/oscal/service/export/ssp_export_app_service.py \
app/oscal/service/export/ssp_export_model.py \
app/oscal/service/export/ssp_mf_content_loader.py \
app/oscal/service/export/ssp_version_content_loader.py \
app/oscal/templates/ssp/ssp_cmmc_template.docx \
di_containers/oscal/oscal_containers.py \
scripts/generate_ssp_docx_template.py \
docs/changelog/2026-05-22-feat-ssp-docx-template-rebuild.md \
docs/features/FR-011.2-2605-ssp-import-export-phase2/implementation-plan-docx-template-rebuild.md \
docs/features/FR-011.2-2605-ssp-import-export-phase2/handoff/2026-05-22-docx-format-handoff.mdCommit B — libreoffice binary detect:
git add app/oscal/service/export/ssp_libreoffice_converter.py \
docs/changelog/2026-05-22-fix-ssp-export-libreoffice-binary-detect.md \
docs/features/FR-011.2-2605-ssp-import-export-phase2/handoff/2026-05-22-export-libreoffice-and-inventory-handoff.md注意:pyproject.toml 的 path dep 不 commit(dev-only)。
LIBREOFFICE_CMD env var 可指定絕對路徑(部署環境若 binary 不在 PATH 內可用)providers.Singleton(SspLibreOfficeConverter) 不變MF uid
→ ModuleFrameService.get_module_frame() [RLS 過後可讀]
→ ModuleFrameSspResourcesService.list_resources() 回 {devices, info_systems}
→ MfSspContentLoader._build_inventory_items(devices) 回 SspInventoryItemExportItem[]
→ SspDocxGenerator._build_context(model) 內 "inventory_items": [{name, device_type, ip_address, os, status}]
→ DocxTemplate.render(context) Jinja {%tr for item in inventory_items %}
→ Document(rendered_buf) + _append_control_sections + _append_reference_docs
→ BytesIO docx
注意:profile_id=122 對應 3 SSPs(250/251/252,SSP versioning 產生的版本鏈)。_resolve_ssp_id 用 get_one(profile_id=122) 取任意一個 SSP(PostgreSQL 沒 ORDER BY,順序不確定)。三個 SSP 都有 hw items 所以實務上看不出差異,但這是個 latent issue — 未來如果只有某個 SSP 版本有 items 而其他沒有,會 flaky。Follow-up:考慮明確指定哪個 SSP 版本(latest by updated_at?)。
本次用 standalone script bootstrap 完整 DI 容器追資料流。pattern:
load_dotenv(.env) + 補 third-party stub env(GITLAB_*, GITHUB_*, DRIVE_TOKEN_ENCRYPTION_KEY)sys.setrecursionlimit(5000)create_app(enable_socketio=False) + 從 app.extensions['di_container'] 拿已配置好的 container(不要 Containers() 開新的,config 沒注入)set_user_context(UserContextDTO(...)) — allowed_tenant_paths 要傳 tenant path(如 /1/102/),不是 tenant idwith session_scope(): 包住所有 service 呼叫本次跑出的證據顯示 working tree + HEAD code 都正確產 inventory,問題純粹是 BE 沒重啟。
ao_name(B6 知曉)network_architecture 和 data_flow 欄位目前 MF / SSP version loader 都回傳空字串(尚無對應 DB 欄位)_resolve_ssp_id 三 SSP 撞 profile_id 時挑「任意一個」— 應改為明確挑 latest versionpyproject.toml 有 jedi-oscal path dep(dev 用),不要 commit