建議 model:opus/effort:medium — 跨套件與主專案兩側、要定 port 契約、要接 DI,簽章對不上只在端點被打時才炸。

本卡屬 FR-107(母卡 CM-1843)第 .1 棒(子需求卡 CM-1844),子任務 T-1.3:做出建批次/列批次/批次詳情/上傳檔/刪檔/完成上傳的 API,定義「取檔放檔」插座,主專案接上現有儲存後端。

這張卡做什麼(白話)

有了資料表(T-1.2)之後,這張卡把使用者真的能用的 API 做出來:建一個批次、把檔傳進去、列出有哪些批次、看某個批次裡有什麼檔、刪掉其中一檔、按「完成上傳」。

同時定義 IEvidenceStorage 這個插座(port)——套件只說「我需要有人幫我存檔/取檔/刪檔」,實際存在主機硬碟、物件儲存還是客戶內網的遠端代理,由主專案插上去的**轉接頭(adapter)**決定。這張卡把插座定義好,並在主專案寫第一版轉接頭(接現有的 jedi-file-upload 的 IUploadFileProvider)。

兩個守門要一起做:① 防呆——系統還沒設定「檔案存到哪」時,建批次/上傳要回 412 並明確說「請先到系統設定完成儲存設定」,不能偷偷寫到 /tmp(重開機就不見)。② 權限——專案經理(manager)可以建/傳/刪,稽核員(auditor)只能看(D8)。

動哪些檔/表/套件

套件(走 path dependency 開發)
~/Projects/Jedicogy/module/jedi-python-package/jedi-evidence-classification/
  .../domain/ports.py:45 IProjectDirectory / :56 IProjectRoleGuard / :73 IEvidenceSource
      ← 加 IEvidenceStorage(見下方簽名)
      ← IProjectRoleGuard 加 is_project_participant(D8 auditor 只看)
  .../api/routing.py:42 起  ← 加批次系列 route
  .../app/service/evidence_batch_service.py   ← T-1.2 建的,這裡補 API 對應的 method
  .../error_code.py   ← 加四支 error code

主專案
core/plugins/evidence_classification.py
  :97  DriveEvidenceSourceAdapter        ← 不動(舊線)
  :127 LivingSspControlCatalogAdapter    ← 不動
  :170 build_adapters()                  ← 多一個參數
  ← 新增 UploadProviderEvidenceStorageAdapter
  ← ExactProjectManagerGuard 加 is_project_participant
di_containers/evidence_classification/evidence_classification_containers.py  ← 同步注入

參考
common/authz/project.py:16 assert_project_manager / :101 assert_project_participant
app/upload_file/service/managed_file_upload_service.py:324 upload_files_for_tenant()

插座簽名:IEvidenceStorage(design.md §6.3)

@dataclass(frozen=True)
class StagedFile:
    file_id: int          # 內部 int(upload_files.id)
    file_uid: str         # 對外 uid
    original_name: str
    local_path: Path      # 已拉到 job 目錄的實體路徑
    content_hash: str | None

class IEvidenceStorage(Protocol):
    """證據檔的取檔/放檔。主專案接 jedi-file-upload 的 IUploadFileProvider。
    套件與容器只認這個;後端是 local/minio/seaweedfs/remote_agent 由宿主決定。"""

    def is_configured(self, tenant_id: int) -> bool: ...
    # D6:未設 STORAGE_CONFIG 回 False,套件在批次上傳路徑拋 412 EC_STORAGE_NOT_CONFIGURED

    def save(self, tenant_id: int, owner_user_id: int, file: FileStorage) -> tuple[int, str, str | None]: ...
    # 存一份檔,回 (file_id, file_uid, sha256);宿主帶 tenant 脈絡寫 upload_files

    def get_bytes(self, tenant_id: int, file_uid: str) -> bytes: ...
    # 拉一份檔的內容(審閱頁預覽用)

    def stage_to_dir(self, tenant_id: int, file_uids: list[str], work_dir: Path) -> list[StagedFile]: ...
    # ⚠️ 本卡只定義簽名,實作留 T-2.2

    def delete(self, tenant_id: int, file_uid: str) -> bool: ...
    # D7 實刪;宿主走 IUploadFileProvider.delete_file

# IProjectRoleGuard 加一支(現有只有 is_project_manager / is_any_project_manager)
    def is_project_participant(self, project_id: int, user_id: int) -> bool: ...
    # 宿主 adapter 委派 common/authz/project.py:101 assert_project_participant

API 規格(套件 route,prefix /api/1.0,design.md §6.6)

方法    路徑                                          守門         說明
POST    /evidence-batches                             manager      body {round_uid} → 建批次(uploading)
                                                                    先 is_configured 否則 412
GET     /evidence-batches?round_uid=&status=          participant  列批次(分頁,繼承 RequestMetaSchema)
GET     /evidence-batches/<batch_uid>                 participant  批次詳情+檔案列表
POST    /evidence-batches/<batch_uid>/files           manager      multipart 單檔或多檔
                                                                    classifying 起回 409
DELETE  /evidence-batches/<batch_uid>/files/<file_uid> manager     刪檔(實刪+列標 removed)
POST    /evidence-batches/<batch_uid>/seal            manager      uploading → ready

新增 error code(套件 error_code.py,前綴沿用套件既有的 EC_):EC_STORAGE_NOT_CONFIGURED(412)、EC_BATCH_SEALED(409)、EC_BATCH_INVALID_TRANSITION(409)、EC_BATCH_NOT_FOUND(404)。BE 新增 error code 必須同步前端的 error-code.json(否則前端顯示空白訊息且不報錯)——這步併在 T-1.4 做,本卡回寫時要交代清楚新增了哪幾支。

怎麼做(逐步)