For agentic workers: 本計畫採分階段。Phase 0 是 de-risk spike,未過不得往下展開逐行 TDD。 各 Task 用 checkbox 追蹤。
Goal: 把證據檔案的儲存從雲端拉出,落地到客戶端的「檔案 agent」;雲端依 per-tenant STORAGE_CONFIG 把檔案轉發給對應 agent,binary 不存雲端磁碟,並以 SHA-256 防竄改。
Architecture: agent = jedi-file-upload 包成的獨立極小 Flask 服務(自帶本地 Postgres,存 Local/MinIO);雲端新增 RemoteAgentAdapter(實作既有 IUploadFileProvider)依 tenant 呼叫 agent。雲端只留參照 metadata,agent 留真檔。
Tech Stack: Python 3.11 / Flask + Flask-RESTful / dependency-injector / SQLAlchemy 2.0 / psycopg3 / jedi-common(session/db)/ jedi-file-upload(檔案邏輯)/ MinIO client / LibreOffice headless / Docker Compose。
狀態(2026-06-18,v2): ✅ Phase 0/1/2/3 + ✅ file_agent registry 後端(方案 B,agent_uid 指向,無 UI)+ ✅ 部署資產(evidence-agent/deploy/)。⏳ Phase 4(雙客戶 demo 組裝:121 已部署、dev 已 seed,差 BE 重啟 + 102 上傳驗證)。
最新進度與接手 一律看
handoff/2026-06-18-FR039-progress.md(進度表)+handoff/2026-06-18-FR039-handoff.md(交接)+handoff/2026-06-18-FR039-next-prompt.md(prompt)。 套件相依:jedi-file-upload mapper 補checksum(未 commit,待發版/path-dep 才讓防竄改 live)。
| # | 決策 | 理由 |
|---|---|---|
| D-A | agent 是全新獨立專案,不是 fork 主專案砍模組 | 主專案 app factory 拖進 GRC config / JWT 多租戶 / Redis / RLS / DI auto-scan 全部包袱;agent 單租戶用不到 |
| D-B | agent 完整重用 jedi-file-upload 套件(不 fork 檔案/轉檔邏輯) |
維護成本最低(見 design.md §3) |
| D-C | agent storage 設定 走 env(用套件原廠 FileUploadService + config DTO),不搬 ManagedFileUploadService、不要 system_configs 表 |
agent 單租戶,per-tenant 查表多餘;env 即套件預設行為 |
| D-D | agent DB 只建 upload_files 一張表,本地 Postgres,不連雲端 DB |
自包含、隔離(design.md §3) |
| D-E | 雲端路由沿用 per-tenant STORAGE_CONFIG(加 remote_agent type),不建新表 |
STORAGE_CONFIG 已 per-tenant(system_configs 走 RLS) |
| D-F | uid 由 agent 產(jedi-file-upload 預設),雲端記下當跨邊界 key | 套件已生 uid,避免雙 uid |
結論:agent 不需 Redis、不需 JWT。 init_db(database_url) 後,@transaction 即可裸跑。
session_scope() 內的 get_user_context()(jedi-common/session/auth/auth_context.py:17)只讀一個 ContextVar,未設定回 None → 走 else 分支只下 SET LOCAL app.is_super_admin='t',不碰 Redis / Flask / JWT。FileUploadService.upload_file(無 user_context / 無 Flask app / 無 Redis)→ 成功寫檔 + 寫 upload_files row + get_upload_file 讀回。PostgreSQL 接受 SET LOCAL app.* 自訂 GUC。app_factory 不需 session 包裝、不需 Redis、不需 JWT;只要啟動時 init_db(本地 pg URL)。convert_to_pdf 需 LibreOffice headless(subprocess,不涉 transaction 風險)。雲端(主專案 compliance-manager-be) 客戶端(新專案 guidant-file-agent)
──────────────────────────────────── ──────────────────────────────────
業務碼(不動) Flask /blob API(新,薄)
└ IUploadFileProvider(不動) └ FileUploadService(jedi-file-upload 原廠)
└ RemoteAgentAdapter(新) ──HTTP──► └ Local / MinIO adapter(套件既有)
└ 寫雲端 upload_files 參照 └ 寫 agent 本地 upload_files
└ 算 SHA-256 錨點 └ binary 落地 + convert_to_pdf
STORAGE_CONFIG(per-tenant,加 remote_agent) 本地 Postgres(只有 upload_files)
RemoteAgentAdapter(新)— 實作 IUploadFileProviderFiles: Create infra/upload_file/remote_agent_adapter.py(或 app/upload_file/adapter/,依專案放 adapter 慣例)
實作介面(簽名對齊 jedi_file_upload/ports/upload_file_provider/upload_file_provider.py):
| method | 行為 |
|---|---|
save_file(file, user_name) -> UploadFileEntity |
① 串流 file → HTTP POST {base_url}/blob;② 邊串流邊算 SHA-256(錨點);③ agent 回 {uid, size, mimetype};④ 寫雲端 upload_files 參照(storage_type=REMOTE_AGENT, save_file_name=uid, sha256=錨點, tenant_id);⑤ 回 entity |
get_file(uid) -> BytesIO |
HTTP GET {base_url}/blob/{uid} 串流回 BytesIO;邊收邊算 SHA-256 → 比對雲端錨點,不符 raise + audit(見 2.5) |
delete_file(uid) -> bool |
HTTP DELETE {base_url}/blob/{uid} + 刪雲端參照 row |
convert_to_pdf(file_uid, output_folder, output_filename=None) -> BytesIO |
HTTP GET {base_url}/blob/{uid}/pdf 串流回(agent 已轉好 + 快取) |
base_url 由建構時注入(來自 STORAGE_CONFIG,見 2.2)。UploadFileRepoImpl(jedi_file_upload/infra/repository/upload_file_repo_impl.py)。httpx(串流上傳/下載)。ManagedFileUploadService 加 remote_agent 分支Files: Modify app/upload_file/service/managed_file_upload_service.py:33-56(_load_config)+ adapter 建構處
_load_config:storage_type == 'remote_agent' → 回新的 RemoteAgentConfigDTO(base_url=v['base_url'])。remote_agent 時 build RemoteAgentAdapter(不走 UploadProviderFactory,因該 factory 在套件內、不該認識雲端專屬 adapter)。在 service 層直接 instantiate。file_upload_service 是 providers.Factory(di_containers/upload_file/upload_file_containers.py:31),每 request 重讀 STORAGE_CONFIG,RLS 回本 tenant 那筆。RemoteAgentConfigDTO(新)Files: Create app/upload_file/dto/remote_agent_config_dto.py
@dataclass
class RemoteAgentConfigDTO(UploadConfigDTO): # 繼承 jedi_file_upload ports UploadConfigDTO
base_url: str = ""
# demo 後續:api_key、timeout、verify_tlsupload_files schema 變更(migration)Files: Create scripts/sql/2026-06-17-fr039-upload-files-remote-agent.sql(遵守 SQL migration 鐵則:日期註解 / schema_migrations / cmmgr 套用)
ALTER TABLE public.upload_files ADD COLUMN sha256 VARCHAR(64)(防竄改錨點,nullable,舊資料無值)ALTER TABLE public.upload_files ADD COLUMN tenant_id ...(參照表隔離;型別對齊專案 tenant_id 慣例)storage_type 新值 REMOTE_AGENT(VARCHAR(20) 容得下,無需改欄)sha256 同步加進 jedi_file_upload/infra/models/upload_file.py(套件異動,兩邊共用此 model;走 path-dependency dev 流程,發版時機見 CLAUDE.md)Files: Modify api/uploadfile/routes/uploadfile_route.py(download/preview route)或在 RemoteAgentAdapter.get_file
upload_files.sha256 錨點。logger.info(..., extra={'event_code': <新碼>})(沿用 common/util/audit_log.py pattern,event_code 參考 FR-033 規範新增一個「疑似竄改」碼)。system_config group=STORAGE_CONFIG key=CONFIG:
{ "storage_type": "remote_agent", "base_url": "https://<該客戶 agent>" }evidence-agent)專案已建立:
~/Projects/Billows/Audit-Manager/evidence-agent(poetry 2.1.1、PEP 621[project]、git main、已有 BE 同款.gitignore+ seedCLAUDE.md)。⚠️ 現為requires-python ^3.13,jedi-* baseline 是 3.11 → 動工前確認相容或對齊 3.11。
agent 自己沒有 domain 邏輯(上傳/下載/轉檔/寫 DB 全在 jedi-file-upload 套件內),所以:
core/(app factory)、api/<mod>/__init__ + Flask-RESTful route 風格、di_containers/ + dependency-injector、config/ config class + ENV 選擇、jedi-common error envelope/handler、poetry + Nexus 來源。domain/ infra/ app/ 三層(邏輯在套件內,硬建是 cargo-cult)。FileUploadService(= 套件的 app service 層),剛好就是「route 不碰 DB」的正確 pattern,不需再多包 pass-through。evidence-agent/
├── pyproject.toml # poetry 管理,Nexus 為 primary source(§3.2)
├── poetry.lock
├── .env.example # storage 設定 + DB 連線
├── main_app.py # 進入點(對齊主專案命名):create_app + 註冊 blob blueprint + run
├── core/
│ ├── app_factory.py # create_app(對齊主專案 core/app_factory.py,但極小,§3.3)
│ └── extensions.py # 需要的單例(可能只有 logger)
├── config/
│ ├── config.py # Config class(對齊主專案)
│ └── config_util.py # ENV 選 config + 組 storage DTO / DB URL(§3.5)
├── api/
│ └── blob/
│ ├── __init__.py # create_module() → blueprint(對齊主專案 api/<mod>/__init__)
│ └── routes/
│ └── blob_route.py # Flask-RESTful Resources,/blob 五支(§3.4)
├── di_containers/
│ └── containers.py # dependency-injector:FileUploadService + UploadProviderFactory
├── common/
│ └── code/
│ └── agent_error_code.py # 若有自有錯誤碼(最小;多數錯誤由套件拋)
├── Dockerfile # 含 LibreOffice(§3.7)
└── docker-compose.yml # file-agent + agent-db(postgres) + volume(§3.7)
注意:沒有
domain/infra/app/(見 §3.0)。不搬主專案的業務模組、GRC config、JWT 多租戶、RLS、APScheduler。
用 poetry(2.1.1)管理,PEP 621 [project] 格式(專案現況),Nexus 設為 primary source([[tool.poetry.source]])。對齊 jedi-file-upload 的 runtime 依賴(其 pyproject 已列),裝套件本身即帶入大部分:
[project]
name = "evidence-agent"
requires-python = ">=3.11,<4" # ⚠️ 現況 ^3.13,建議對齊 jedi-* baseline 3.11
dependencies = [
"jedi-file-upload>=0.0.16", # 檔案邏輯(連帶 jedi-common、minio、SQLAlchemy、psycopg)
"jedi-common>=0.0.23", # 顯式 pin:session_scope / init_db / BaseModel / @transaction
"flask",
"flask-restful",
"dependency-injector",
]sha256)走 poetry path-dependency(同主專案 dev 流程,CLAUDE.md),完成才發版改回 pin。poetry install、加套件 poetry add、更新 poetry update(不用 poetry lock)。app_factory.py(極小)職責(只做檔案服務跑得起來的最小集合):
Flask(__name__)jedi_common init_db(database_url)(agent 本地 Postgres URL)→ 讓 @transaction / get_session 可運作register_error_handlers(app)(jedi-common,例外轉 HTTP envelope)/blob routes(薄,對齊 design.md §7)| Method | Path | 呼叫 | 回傳 |
|---|---|---|---|
| POST | /blob |
file_upload_service.upload_file(file, user_name='cloud', save_dir=...) |
{uid, save_file_name, size, mimetype, sha256} |
| GET | /blob/{uid} |
file_upload_service.get_upload_file(uid) |
send_file(stream) |
| GET | /blob/{uid}/pdf |
file_upload_service.convert_to_pdf(uid, output_folder, f'{uid}.pdf') |
send_file(pdf, mimetype='application/pdf') |
| DELETE | /blob/{uid} |
file_upload_service.delete_file(uid) |
200 |
| GET | /health |
— | 200 |
FileUploadService(不是 ManagedFileUploadService)。AGENT_AUTH_MODE=full), 設計見 design-agent-auth.md、部署見 evidence-agent/deploy/README.md。none 僅測試用。config.py(env → config DTO)# storage:依 STORAGE_TYPE 組 DTO
STORAGE_TYPE = os.getenv("STORAGE_TYPE", "local") # local | minio
# local → LocalFileUploadConfigDTO(base_dir=os.getenv("BASE_DIR"))
# minio → MinioUploadConfigDTO(endpoint, access_key, secret_key, bucket_name, secure)
# DB:agent 本地 postgres
DATABASE_URL = os.getenv("DATABASE_URL") # postgresql+psycopg://...@agent-db:5432/fileagentupload_files(含新增的 sha256)。jedi_file_upload.infra.models.upload_file.UploadFile.metadata.create_all(engine)(agent 啟動時跑一次),或附一支簡單 SQL。不需要 system_configs、不需要 RLS。Dockerfile 要點:
python:3.11-slimapt-get install libreoffice(headless 轉檔;可裝 libreoffice-core + 需要的 filter 以縮 image)poetry install --no-root(Nexus 源;或 poetry export 成 requirements 再裝,二擇一)python main_app.pydocker-compose.yml:
services:
file-agent:
build: .
environment:
STORAGE_TYPE: local # 或 minio
BASE_DIR: /data/upload
DATABASE_URL: postgresql+psycopg://fileagent:***@agent-db:5432/fileagent
volumes: [ "./data:/data/upload" ]
ports: [ "8080:8000" ]
depends_on: [ agent-db ]
agent-db:
image: postgres:16
environment: { POSTGRES_DB: fileagent, POSTGRES_USER: fileagent, POSTGRES_PASSWORD: *** }
volumes: [ "agent-db-data:/var/lib/postgresql/data" ]
volumes: { agent-db-data: {} }(密碼一律走部署文件 / .env,不入版控。)
| 動作 | Request | Response |
|---|---|---|
| 上傳 | POST /blob multipart file |
200 {uid, save_file_name, size, mimetype, sha256} |
| 下載 | GET /blob/{uid} |
200 binary stream(Content-Type, Content-Disposition)/ 404 |
| 預覽 | GET /blob/{uid}/pdf |
200 PDF stream(必要時即時轉、快取)/ 404 / 415 不支援格式 |
| 刪除 | DELETE /blob/{uid} |
200 / 404 |
| 健康 | GET /health |
200 |
Authorization(per-tenant key)+ TLS。design-agent-auth.md / evidence-agent/deploy/README.md。見 design.md 流程圖:上傳(雲端寫參照 + 算錨點 → agent 存 + 回 uid)、下載(雲端 relay + 重算驗錨點)、預覽(agent 轉 PDF + 快取)。
Phase 1 ✅ 完成:agent 本機 dev / docker compose(含 Nexus BuildKit secret) / 預覽轉檔 / local + minio 兩種後端 / 正式模式錯誤 envelope 全綠。
Nexus 憑證策略(已採用):BuildKit secret。
.secrets/與data/已加 evidence-agent.gitignore。客戶出貨:image 在我們這邊 build 成自包含後docker save出貨(客戶端碰不到 Nexus),客戶端 compose 改用image:不 build。
checksum 回 null(jedi DTO 映射細節);sha256 於 Phase 3 補。upload_files.checksum 原本 write-only —— jedi-file-upload 的 UploadFileMapper.to_entity 漏 map checksum。已補一行(套件 source jedi_file_upload/infra/mapper/upload_file_mapper.py),免 migration(欄位本就存在)。但 BE 目前 pin 0.0.16,此修正要發版或 dev path-dep 才在 BE 生效;未生效時驗證 fail-open(讀不到錨點 → 略過,不會誤擋)。e2e 是臨時同步 venv copy 驗證、已還原。sha256 欄」;demo 改重用 checksum 欄避 migration,正式版再立獨立 sha256 欄(與 tenant_id/RLS 同一 migration)。/blob 五支寫 API 測試(local 模式用 tmp dir;minio 用 testcontainers,套件已依賴 testcontainers)。RemoteAgentAdapter 對 mock agent(httpx mock / responses)測 save/get/convert + SHA-256 比對;測試檔加 logger patch autouse fixture(見專案慣例)。compliance-manager-test(不在主專案內加)。upload_files(uid→落點)是關鍵狀態,掉了 binary 全孤兒。compose 已改 host bind-mount ./pgdata(survive down -v);正式部署要補備份策略(整包備 agent 目錄 / pg_dump 排程)。安全網:save_file_name 內嵌 uid → DB 全毀可從檔名重建(災難復原)。tenant_agents registry:等第二種 agent 出現再抽(initial 沿用 STORAGE_CONFIG)。