# FR-039 分散式檔案 Agent — 交接文件（冷接用，自包含）

> 2026-06-18 · branch `feature/FR-039-evidence-agent`（BE + evidence-agent）· 均未 push
> **接手前必讀**：① `design.md` ② `2026-06-18-FR039-progress.md`（進度表）③ 本檔 ④ `2026-06-18-FR039-next-prompt.md`

---

## 🧭 §0 先懂需求（硬 gate，別跳）

**WHY**：Guidant AI 是雲端 SaaS，但客戶的證據檔案是機敏資料，部分客戶**不接受上傳到我們雲端託管**。所以把「檔案儲存」拉出去，**落地在客戶自有設備**；雲端依不同客戶把檔案轉發到客戶端的 agent，**binary 不存我們雲端磁碟**。

**目標不是**「做一個檔案上傳服務」，**是**「讓檔案物理上待在客戶機房、雲端只持有指標 + 業務 metadata」。任何改動若讓 binary 回流雲端磁碟，就是違背初衷。

冷接自檢（答得出才開工）：
1. 為什麼 binary 不能存雲端？→ 客戶機敏資料、不託管。
2. 雲端怎麼知道要打哪台 agent？→ 該 tenant 的 `STORAGE_CONFIG`（per-tenant、走 RLS）。
3. 預覽轉檔在哪做、為什麼？→ 客戶端 agent（否則 source 要回流雲端，違背初衷）。

---

## §1 架構（誰做什麼）

```
雲端 BE（compliance-manager-be）                客戶 server（121 / 122）
─────────────────────────────────              ──────────────────────────
業務碼（不動）                                    evidence-agent（獨立 Flask + Docker）
 └ IUploadFileProvider（不動）                     └ jedi-file-upload 原廠（local/minio adapter + 轉檔）
     └ RemoteAgentAdapter（Phase 2 新）──HTTP──►       └ 既有 MinIO（bucket guidant-ai）
         · 寫雲端 upload_files 參照                     └ 本地 Postgres（upload_files：uid↔物件名）
         · 算 SHA-256 錨點（Phase 3）
STORAGE_CONFIG（per-tenant）：remote_agent
  · inline base_url，或
  · agent_uid → file_agents registry（方案 B）
```

**關鍵設計**：
- 雲端只存「參照 row」（cloud uid、檔名、tenant），`save_file_name` 記 agent 回傳的 uid 當跨邊界 key；binary 全在客戶端。
- agent 是「jedi-file-upload 包成獨立服務」，**完整重用、不 fork**；雲端側套件零改動。
- 路由沿用既有 per-tenant `STORAGE_CONFIG`（`system_configs` 走 RLS），不是另建 routing 機制。

---

## §2 現在到哪了（進度表見 progress.md）

- Phase 0/1/2/3 + registry 後端 + deploy 資產 **全完成且 commit**（hash 見 progress.md）。
- **Phase 4（demo 組裝）進行中**：121 agent 已跑、dev DB 已 seed、102 已指向 121；**就差「重啟 BE → 102 登入上傳新檔」走完整鏈**。

---

## §3 怎麼繼續（下一步具體動作）

### 3.1 完成 Phase 4 e2e（最優先）
1. **重啟本機 BE**（新 registry 模組要重啟才載入；服務由 user 自己起，別替他起）。
2. 用 **102 帳號**（`blsadmin / Billows@123!`）登入前端，**上傳一個新檔**。
3. 預期：檔案經 BE → 解析 `agent_uid=agent-cust-a-121` → registry 拿 `http://192.168.50.121:8080` → `RemoteAgentAdapter` → 121 agent → 落 **121 MinIO bucket `guidant-ai`**。
4. 下載 / 預覽該檔 → 應正常。
5. **測完還原 102 config**（102 是共用 dev tenant）：
   ```sql
   UPDATE public.system_configs SET value='{"minio_bucket":"guidant-ai-stg","storage_type":"minio","minio_endpoint":"192.168.50.171:9002","minio_access_key":"admin","minio_secret_key":"1qaz2wsx"}'
   WHERE "group"='STORAGE_CONFIG' AND key='CONFIG' AND tenant_id=102;
   ```
   ⚠️ 102 切成 remote_agent 期間，它**舊的 171-minio 檔讀不到**（read adapter 依當前 config，非每檔 storage_type）；測試只用新上傳檔。

### 3.2 部署 122（另一客戶）
同 121，`evidence-agent/deploy/`：把 `evidence-agent:0.1.0` image（amd64）`docker load` 到 122 → `.env` 的 `MINIO_ENDPOINT=192.168.50.122:9000` → `docker compose up -d`。要驗 122 就把 102 改 `{"agent_uid":"agent-cust-b-122"}`。

### 3.3 讓防竄改真生效（選做）
jedi-file-upload 的 `UploadFileMapper.to_entity` 漏 map `checksum`，已在**套件 source 補一行**（未 commit）。BE 現 pin 0.0.16 → 要 **dev path-dep 或發套件版** 才 live；未 live 時整性 fail-open（不驗、不誤擋）。

### 3.4 registry 管理 UI（後補）
FE 已有 `/system/storage-config`（`StorageConfigForm.vue`，支援 local/minio）。要加：storage_type 多 `remote_agent` 選項 + agent 下拉（讀 file_agents registry，需新增 BE 查詢 API）。

---

## §4 部署 runbook（客戶 server）

image **必須 build 成 `linux/amd64`**（客戶 server 是 x86_64，Mac 是 arm64）：
```bash
# 我們這邊（Mac）
cd ~/Projects/Billows/Audit-Manager/evidence-agent
docker build --platform linux/amd64 --secret id=nexus_auth,src=.secrets/nexus.env -t evidence-agent:0.1.0 .
docker save evidence-agent:0.1.0 | gzip > /tmp/evidence-agent-0.1.0.tar.gz   # 傳到 121/122

# 客戶 server（121/122，x86_64）
docker load < evidence-agent-0.1.0.tar.gz
# 放 deploy/docker-compose.yml + .env（填 MINIO_ENDPOINT=本機IP:9000 / keys / bucket / DB_PASSWORD）
docker compose up -d && curl http://localhost:8080/health
```
- `.secrets/nexus.env`（Nexus 帳密 admin/1qaz2wsx）只在 build 用、gitignored。
- agent ↔ 既有 minio：分開 compose = 不同 network → 用 **server LAN IP**（`192.168.50.121:9000`），非 `minio:9000`。
- agent ↔ agent-db：同 compose → service 名 `agent-db`。

---

## §5 踩過的雷（這 arc 學到的，別重踩）

| 雷 | 解 |
|----|----|
| `exec format error` | image 架構不對：Mac=arm64、server=amd64 → build 要 `--platform linux/amd64` |
| 分開 compose 連不到 minio | 不同 compose 不同 network → 用 host LAN IP，非 service 名 |
| `DB_PASSWORD` 含 `@` 打爆 DATABASE_URL | compose 傳 `DB_PASSWORD` 各段、agent `config.py` `quote_plus` 組 URL（已改）|
| `container name already in use` | 移除 `container_name`（已改）；或 `docker rm -f` 舊 container |
| Postgres 密碼改了沒生效 | Postgres 只在 pgdata **首次初始化**套密碼；第一次就設好別後改 |
| 整性驗證被靜默跳過 | jedi-file-upload mapper `to_entity` 漏 map `checksum`（write-only）→ 補一行才讀得回 |
| 切 tenant 到 remote_agent 後舊檔讀不到 | read adapter 依「當前 STORAGE_CONFIG」非每檔 storage_type；mixed backend 不支援 |
| `flask-restful` 非 debug 吞 500 | app_factory 設 `PROPAGATE_EXCEPTIONS=True`（evidence-agent 已設）|
| `tenant_id` 欄沒出現 | `TenantScopedMixinModel` 看 `ENABLE_MULTI_TENANT=true` env 才加；產 DDL 要帶此 env |
| `source .env` 漏讀後段 | DB_SECRET JSON 那行會中斷 source；取值用 python parse .env |
| minio 預覽 500 `os.replace EXDEV` | `PDF_CACHE_DIR` 不可指 bind-mount（/data）；要容器內 /tmp（同 os.replace 目標裝置）。套件已 `os.replace`→`shutil.move` 根治（待發版）|
| 轉檔後中文變空白/豆腐字 | LibreOffice 缺 CJK 字型 → Dockerfile 加 `fonts-noto-cjk`，**要重 build image** |
| 刪程序書留 minio 孤兒 | 各刪除路徑要經 `file_upload_service.delete_file`→adapter；已修 `delete_from_pool`，其他刪除路徑（如 job_evidence）若有同問題比照 |

---

## §6 關鍵檔案地圖

**BE（雲端）**
- `infra/upload_file/remote_agent_adapter.py` — RemoteAgentAdapter（HTTP 轉發 + 寫參照 row + SHA-256）
- `infra/upload_file/remote_agent_config_dto.py` / `common/code/file_storage_type.py`（REMOTE_AGENT 常數）
- `app/upload_file/service/managed_file_upload_service.py` — `_load_config` / `_build_adapter` / `_resolve_agent_base_url`
- `domain/file_agent/` + `infra/file_agent/` + `di_containers/file_agent/` — registry 模組
- `api/uploadfile/routes/uploadfile_route.py` — download/preview 的 REMOTE_AGENT 分支
- `scripts/sql/2026-06-18-fr039-file-agents-table.sql` — registry 表 migration

**evidence-agent（客戶端）**
- `core/app_factory.py`（極小）/ `config/config.py`（env→DTO）/ `api/blob/`（5 endpoint）/ `di_containers/containers.py`
- `deploy/docker-compose.yml` + `.env.example`（客戶部署）/ `Dockerfile`（含 LibreOffice）
- `CLAUDE.md`（專案規範與硬規則）

---

## §7 測試座標

| 項 | 值 |
|----|----|
| dev DB | `192.168.50.188:25432` / `guidant_ai_dev` / cmmgr（密碼查 `.env DB_SECRET`）|
| 測試 tenant | 102（登入 `blsadmin / Billows@123!`）|
| 121 agent | `http://192.168.50.121:8080`，minio bucket `guidant-ai` |
| registry uids | `agent-cust-a-121`（→121）、`agent-cust-b-122`（→122）|

---

## §8 不變式（改動別違反）

- 業務碼 / `IUploadFileProvider` / 既有 local/minio adapter **不得改**。
- 雲端任何路徑**不得把 binary 寫進雲端磁碟**（relay 用串流）。
- agent **不連雲端 DB**；雲端↔agent 只走 REST。
- agent **完整重用 jedi-file-upload，不得 fork**。
- 整性信任根是**雲端算的 sha256**；驗證一律**對實際 bytes 重算**。
- agent DB（pgdata）是關鍵狀態，bind-mount、勿刪。
