建議 model:opus/effort:medium — 整案的流程主幹,串起 stage/容器/結果回寫,且背景執行緒租戶脈絡有已知坑。
本卡屬 FR-107(母卡 CM-1843)第 .3 棒(子需求卡 CM-1846),子任務 T-3.2:把「按下開始分類」到「分類跑完進審閱」整條串起來,並讓 BE 重啟後卡住的批次能自動變成失敗。
這是整案的流程主幹。前面幾張卡把零件做好了(批次表、插座、轉接頭、容器契約),這張卡把它們串成一條線:
按「開始分類」
→ 批次狀態改「分類中」(封口,之後加檔回 409)
→ 解析 AI 分類設定 + 取得檢查點清單
→ 把整批檔拉到 job 工作目錄,寫 catalog.json/manifest.json/prompt.json
→ docker run 起容器
→ 讀回結果 JSON
→ 寫進分類結果表(掛批次、存目標集快照)
→ 批次狀態改「審閱」
→ 刪掉工作目錄裡的 files/(不留第二份實體檔)
心跳機制(D4):分類跑在背景執行緒,如果 BE 中途被砍掉重啟,前端會永遠看到「分類中」永遠不會結束。改成執行緒每 60 秒在批次上蓋一個時間戳,BE 啟動時掃一遍:狀態是「分類中」但心跳超過 30 分鐘沒更新的,自動標成「失敗」,使用者可以按重跑(不必重傳檔案,檔還在批次裡)。
背景執行緒的租戶脈絡陷阱:背景執行緒沒有 HTTP 請求的脈絡,讀「檔案存到哪」的設定時會挑錯租戶。解法是在 HTTP 請求裡先把需要的東西全部解析好,當成參數傳進執行緒,執行緒裡不再讀任何 thread-local。
套件(走 path dependency 開發)
~/Projects/Jedicogy/module/jedi-python-package/jedi-evidence-classification/
.../app/service/evidence_batch_service.py
← 加 classify(batch_uid, user, model?, threshold?)
← ALLOWED_TRANSITIONS 補 classifying/review/failed 三列
.../app/service/evidence_classification_service.py
:144 trigger_classify(project_uid, ap_uid, ...) ← 舊簽名保留給舊 route,不動
← 加 run_batch(...)(stage → 寫三個 json → docker run → 讀回 → 寫 run → review)
.../app/service/job_registry.py:20 JobRegistry
← 🔴 只留「進度百分比」這種暫態,持久化狀態全部進批次表
.../infra/classifier_container_runner.py ← T-2.3 已改,這裡呼叫
.../plugin/register ← 加 BE 啟動 hook:掃心跳逾時
.../api/routing.py ← 加 POST /evidence-batches/<batch_uid>/classify
動作 允許的來源狀態 目標狀態 額外前置條件
開始分類 ready、uploading(自動先 ready)、 classifying ① 儲存設定已設(is_configured)
failed、review ② 目標集非空(否則 412
EC_NO_TARGET_CATALOG)
③ 同專案無其他 classifying 批次
(沿用 JobRegistry 單活性)
容器回結果 classifying review
失敗 classifying failed failure_reason 要填
加檔/刪檔 🔴 非 classifying classifying 時回 409 EC_BATCH_SEALED
心跳(D4)
job_started_at 分類開始時寫
job_heartbeat_at 執行緒每 60 秒 UPDATE(🔴 獨立短 transaction,不要卡在主 transaction 裡)
BE 啟動 hook:
UPDATE evidence_batches SET status='failed', failure_reason='heartbeat_timeout'
WHERE status='classifying' AND job_heartbeat_at < now() - interval '30 min'
🔴 心跳是「執行緒還活著」不是「跑完了」——只要執行緒活著就會一直蓋時間戳,所以超大批次真跑超過 30 分鐘不會被誤標失敗(design.md 風險 11)。30 分鐘這個常數放套件 config 可調。
classify 端點在 HTTP 請求裡先解析好:tenant_id、AI 分類設定(profile)、檢查點清單(catalog)、儲存 provider 設定,全部以參數傳進執行緒。IEvidenceStorage.stage_to_dir(tenant_id, ...) 由宿主轉接頭以傳進來的 tenant_id 明確解析 provider(走 managed_file_upload_service.py:324 upload_files_for_tenant 同一條解析路徑)。get_user_context()——這是驗收項(會 grep)。trigger_classify(:144)原樣保留給舊 route(D3 並存一版),新增 run_batch 走新路。不要改舊簽名。prompt.json 存在且格式對。files/,但 _container.log 與 _report-original.json 留著(7 天)供排查。