FR-056.4 執行編排 + 結果轉證據 + 通知 + 歷史 Implementation Plan
For agentic workers / runner: REQUIRED SUB-SKILL: superpowers:subagent-driven-development / executing-plans.
- [ ]checkbox syntax.依賴:FR-056.1 / .2 / .3(收尾整合階段)。 母案設計見
design.md。
Goal: 把 FR-056.1~.3 串成完整可用流程:任務執行抽屜「開始執行」→ 派工 → Agent 掃描回報 → 報告自動存回 job_evidences(source=DETECTION_TOOL)+ detection_executions 落執行史 + 發 mail 通知負責人 → 依完成模式 auto 自動完成 / manual 留任務給人工。執行歷史可查、報告可下載(沿用既有下載端點)。
Architecture: 結果轉證據照抄 ImportDriveFileHandler(DRIVE_SYNC)三段式:報告存 Minio → 組 JobEvidenceEntity(source="DETECTION_TOOL", created_user="detection-agent@system") → domain service .add()。通知照 job_batch_complete_service 的 threading.Thread + send_mail_notification。完成模式不新增任何 JobStatus 狀態——auto 呼叫既有 complete_job,manual 什麼都不做(任務留 PROCESSING,人工用既有「完成任務」按鈕)。報告下載沿用既有 /file/download/<uid>,零新端點。
Tech Stack: 同前。
前置:關鍵決策(runner 必讀)
| 項目 | 定案 | 來源 |
|---|---|---|
| 完成模式語意 | 不是狀態。auto=系統掃完自動呼叫既有 complete_job;manual=系統不做事,任務留 PROCESSING,人工按既有「完成任務」 |
user 澄清 2026-07-26 |
| 不動 jedi_flow_engine | JobStatus(TODO/PROCESSING/COMPLETED/CANCEL)零新增,complete_job 前置檢查不改 |
user 澄清(原「待覆核新狀態」方案作廢) |
| 轉證據模板 | 抄 ImportDriveFileHandler.handle() 三段式,新系統帳號常數 detection-agent@system |
Explore |
| source 新值 | job_evidences.source CHECK constraint 加 DETECTION_TOOL(需 migration) |
Explore |
| evidence_type(D11 定案) | 用既有 FILE,不用 REPORT。REPORT 型別會牽動「其他判斷證據檔案的地方」,改動面大;第一版用 FILE 最安全,要區分檢測報告類證據未來另開需求 |
user 拍板 2026-07-26 |
| 通知模板 | job_batch_complete_service._send_batch_summary_notification(threading.Thread + send_mail_notification,收件人查 task_assignees/ParticipantRole) |
Explore |
| 報告下載 | 不新建端點,沿用 /file/download/<uid>(UploadFileDownloadRoute),report 存 upload_files 拿 uid 即可 |
Explore |
| 完成模式讀取來源 | FR-056.2 建的 config.job_execution_detection_tools.completion_mode |
FR-056.2 |
| 重掃 | PROCESSING 下既有「再執行」能力(再建一筆 agent_task),每次一筆 detection_executions 獨立紀錄 | design |
Task 分佈(對應 design T-4.x)
| Task | 對應 | 產出 | 依賴 | Repo |
|---|---|---|---|---|
| Task 1 | T-4.1 | job_evidences source 加 DETECTION_TOOL + detection_executions 表 + 轉證據 handler | FR-056.3 | BE |
| Task 2 | T-4.2 | 執行編排(開始執行串派工 / 完成模式分岔 / 通知) | Task 1 + FR-056.2 | BE |
| Task 3 | T-4.3 | FE 任務執行抽屜(開始執行 / 重掃 / 執行歷史 / 下載) | Task 2 | FE |
Task 1(T-4.1): source 加值 + detection_executions 表 + 轉證據 handler
Files:
- Create: scripts/sql/2026-07-26-fr056-4-job-evidences-detection-source.sql(改 CHECK constraint)
- Create: scripts/sql/2026-07-26-fr056-4-detection-executions.sql(執行史表)
- Create: infra/domain/app 全層 detection_execution 模組
- Create: app/detection_tools/service/detection_result_handler.py(轉證據,mirror ImportDriveFileHandler)
- Modify: infra/flow_engine/models/job_evidence.py(comment 更新 source 值)
- Test: test/test_detection_result_handler.py
- Step 1: migration — job_evidences source 加 DETECTION_TOOL
-- Date: 2026-07-26
-- FR-056.4 job_evidences.source 加 DETECTION_TOOL
ALTER TABLE compliance.job_evidences DROP CONSTRAINT IF EXISTS chk_job_evidences_source;
ALTER TABLE compliance.job_evidences ADD CONSTRAINT chk_job_evidences_source
CHECK (source IN ('SYSTEM_UPLOAD','DRIVE_SYNC','DETECTION_TOOL'));
INSERT INTO public.schema_migrations(filename, note) VALUES
('2026-07-26-fr056-4-job-evidences-detection-source.sql', 'FR-056.4 job_evidences source 加 DETECTION_TOOL') ON CONFLICT (filename) DO NOTHING;
- Step 2: migration — detection_executions 執行史表
-- Date: 2026-07-26
-- FR-056.4 檢測執行歷史(一次執行一筆,可查可下載)
CREATE TABLE compliance.detection_executions (
id BIGSERIAL PRIMARY KEY,
uid VARCHAR(36) NOT NULL UNIQUE,
tenant_id BIGINT NOT NULL,
agent_task_uid VARCHAR(36), -- soft-ref → compliance.agent_tasks.uid
job_execution_uid VARCHAR(50) NOT NULL, -- soft-ref → job_executions.uid
detection_tool_id BIGINT NOT NULL,
started_at TIMESTAMPTZ, finished_at TIMESTAMPTZ,
status VARCHAR(20) NOT NULL DEFAULT 'running', -- running/succeeded/failed
summary JSONB, -- 摘要(發現幾項等)
report_file_id BIGINT, -- soft-ref → upload_files.id(原始報告,供下載)
evidence_id BIGINT, -- soft-ref → job_evidences.id(轉出的證據)
error_message TEXT,
created_user VARCHAR(255), updated_user VARCHAR(255),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE compliance.detection_executions IS 'FR-056.4 檢測執行歷史(每次執行一筆,重掃不覆蓋)';
GRANT SELECT, INSERT, UPDATE, DELETE ON compliance.detection_executions TO cm_app;
GRANT USAGE, SELECT ON SEQUENCE compliance.detection_executions_id_seq TO cm_app;
ALTER TABLE compliance.detection_executions ENABLE ROW LEVEL SECURITY;
CREATE POLICY detection_executions_tenant_isolation ON compliance.detection_executions
USING (current_setting('app.is_super_admin', TRUE) = 'true'
OR tenant_id = ANY (string_to_array(current_setting('app.allowed_tenant_paths', TRUE), ',')::BIGINT[]));
INSERT INTO public.schema_migrations(filename, note) VALUES
('2026-07-26-fr056-4-detection-executions.sql', 'FR-056.4 檢測執行歷史表') ON CONFLICT (filename) DO NOTHING;
套進 DEV + psql 驗證。
-
[ ] Step 3: DDD 全層 detection_execution 模組(複製 remote_agent 骨架,tenant-scoped)。
-
[ ] Step 4: 寫 failing test(轉證據三段式 + source=DETECTION_TOOL)
test/test_detection_result_handler.py:mock file_upload + evidence domain service,驗證 handler 收到 agent 結果後:(a) 報告存 Minio;(b) JobEvidenceEntity 的 source="DETECTION_TOOL"、created_user="detection-agent@system";(c) detection_executions 落一筆 status=succeeded、report_file_id/evidence_id 有值。
- Step 5: 轉證據 handler(照抄 ImportDriveFileHandler 三段式)
app/detection_tools/service/detection_result_handler.py:
DETECTION_AGENT_USER = "detection-agent@system"
class DetectionResultHandler:
def __init__(self, file_upload_service, job_evidence_domain_service,
detection_execution_domain_service, job_execution_domain_service):
...
@transaction
def handle_result(self, agent_task, report_file_bytes, summary):
"""agent 回報成功後呼叫:報告存 Minio → 寫 job_evidences(DETECTION_TOOL) → detection_executions 落史。"""
job = self._job_exec.get_job_execution(JobExecutionQueryEntity(uid=agent_task.job_execution_uid))
# (1) 存 Minio(mirror ImportDriveFileHandler)
fs = FileStorage(stream=io.BytesIO(report_file_bytes), filename=f"detection_report_{agent_task.uid}.xml",
content_type="application/xml")
save_dir = f"JOB_EVIDENCES/{job.uid}"
uploaded = self._file_upload.upload_files([fs], DETECTION_AGENT_USER, save_dir)[0]
# (2) 寫 job_evidences(source=DETECTION_TOOL)
evidence = self._evidence.add(JobEvidenceEntity(
main_workflow_execution_id=job.main_workflow_execution_id,
workflow_execution_id=job.workflow_execution_id,
job_execution_id=job.id,
evidence_type="FILE", # D11 定案:用 FILE(REPORT 改動面大,未來另開需求)
source="DETECTION_TOOL",
file_id=uploaded.id,
content_hash=getattr(uploaded, "checksum", None), hash_algorithm="MD5",
description=f"[檢測工具] {agent_task.uid}",
created_user=DETECTION_AGENT_USER, updated_user=DETECTION_AGENT_USER,
))
# (3) detection_executions 落史
self._exec.write_success(agent_task.uid, job.uid, report_file_id=uploaded.id,
evidence_id=evidence.id, summary=summary, user=DETECTION_AGENT_USER)
return evidence
注意: 不改
job_evidence_service.add_job_evidence(那是 user 手動上傳路徑)——照 DRIVE_SYNC 慣例直接呼叫 domain service.add()自訂 source。
-
[ ] Step 6: 跑 test 通過 → PASS。
-
[ ] Step 7: Commit
git commit -m "feat(fr056): detection result → evidence handler + execution history (T-4.1)"
Task 2(T-4.2): 執行編排(開始執行 / 完成模式分岔 / 通知)
Files:
- Create/Modify: app/detection_tools/service/detection_orchestration_service.py(開始執行 + 完成模式分岔)
- Modify: app/remote_agent/service/remote_agent_service.py 的 receive_result(FR-056.3 留的 TODO 掛鉤點:成功時觸發 Task 1 handler + 本 Task 的完成/通知)
- Modify: api/detection_tools/routes/detection_tool_route.py(「開始執行」endpoint)
- Test: test/test_detection_orchestration.py
- Step 1: 寫 failing test(auto vs manual 分岔 + 通知)
驗證:(a) 開始執行 → 建 agent_task(pending) + detection_executions(running);(b) 結果成功 + completion_mode=auto → 呼叫 complete_job;(c) completion_mode=manual → 不呼叫 complete_job(任務留 PROCESSING);(d) 兩情況都發 mail 給負責人。
- Step 2: 「開始執行」app service
@transaction
def start_execution(self, job_uid, curr_user, tenant_id):
binding = self._jedt_domain.get_one(job_execution_uid=job_uid) # FR-056.2 綁定
if not binding:
raise BadRequestError(DetectionToolsErrorCode.DETECTION_TOOL_NOT_BOUND)
# 選一台有 detection_scan capability 的 agent(tenant 內)
agent = self._pick_agent(tenant_id)
# 解密該租戶工具憑證,隨派工下發(走 mTLS,見 FR-056.3 憑證下發決策)
creds = self._resolve_credentials(binding, tenant_id)
task = self._agent_task_domain.create(AgentTaskEntity(
uid=str(uuid.uuid4()), tenant_id=tenant_id, agent_id=agent.id,
job_execution_uid=job_uid, detection_tool_id=binding.detection_tool_id,
params={**binding.tool_params, "_credentials": creds}, status="pending",
created_user=curr_user, updated_user=curr_user))
self._exec_domain.create(DetectionExecutionEntity(
uid=str(uuid.uuid4()), tenant_id=tenant_id, agent_task_uid=task.uid,
job_execution_uid=job_uid, detection_tool_id=binding.detection_tool_id,
status="running", started_at=datetime.now(), created_user=curr_user))
return {"agent_task_uid": task.uid}
- Step 3: 完成模式分岔 + 通知(掛在 FR-056.3 的 receive_result 成功分支)
FR-056.3 Task 2 的 receive_result 成功時,於此觸發(把 TODO(FR-056.4) 換成實作):
def _on_scan_succeeded(self, agent_task, report_bytes, summary):
evidence = self._result_handler.handle_result(agent_task, report_bytes, summary) # Task 1
binding = self._jedt_domain.get_one(job_execution_uid=agent_task.job_execution_uid)
self._notify_owner(agent_task.job_execution_uid, evidence) # 發信
if binding.completion_mode == "auto":
self._workflow_execution_service.complete_job(...) # 既有 complete_job,PROCESSING→COMPLETED
# manual:什麼都不做,任務留 PROCESSING,等人工手動完成
通知照 job_batch_complete_service pattern:
def _notify_owner(self, job_uid, evidence):
targets = self._get_job_notify_targets(job_uid) # 查 task_assignees / ParticipantRole email
subject = _("detection_scan_completed_subject")
for info in targets:
if info["email"]:
content = _("detection_scan_completed_content") % {"nickname": info["nickname"]}
threading.Thread(target=notification_service.send_mail_notification,
kwargs={"to": info["email"], "subject": subject, "content": content, "is_html": True}).start()
掃描失敗(
receive_resultfailed 分支):detection_executions標 failed(Task 1 的 write_failure),任務留 PROCESSING(不 silent,FE 執行歷史看得到 failed),可重試。
- Step 4: 「開始執行」endpoint
POST /detection-tools/jobs/<job_uid>/execute(掛 @jwt_required() + 專案參與者守門;這是 GRC 專案資源,用軸③ project-role 或既有 job 操作守門,非軸④ capability——runner 對齊既有 job 操作 endpoint 的守門方式)。
- Step 5: 跑 test 通過 + 手測 auto/manual 兩路
手測:建 detection 任務綁 auto → 執行 → 掃完任務自動 COMPLETED + 收信 + 證據入庫;建另一任務綁 manual → 執行 → 掃完任務仍 PROCESSING + 收信 + 證據入庫,手動按完成才 COMPLETED。
- Step 6: Commit
git commit -m "feat(fr056): execution orchestration + completion mode + notify (T-4.2)
開始執行串派工;掃完轉證據 + 發信;completion_mode=auto 呼叫既有 complete_job,manual 留任務 PROCESSING 等人工。零新增 JobStatus。"
Task 3(T-4.3): FE 任務執行抽屜
FE repo。 任務執行抽屜(既有元件,runner 先定位 detection 任務執行時開的抽屜 Vue 檔)。
-
[ ] Step 1: 「開始執行」按鈕 → 打
POST /detection-tools/jobs/<uid>/execute,loading 狀態,成功 toast。 -
[ ] Step 2: 執行歷史列表 → 打
GET /detection-tools/jobs/<uid>/executions(Task 2 補此唯讀 endpoint),顯示每筆:時間、狀態(running/succeeded/failed)、摘要、報告下載連結(/file/download/<report_file.uid>,沿用既有下載)。 -
[ ] Step 3: 手動重新執行 → 執行歷史區「重新執行」按鈕 = 再打一次 execute(PROCESSING 下既有能力),新增一筆歷史,不覆蓋。
-
[ ] Step 4: manual 模式完成 → 不新增 UI。manual 任務掃完仍 PROCESSING,執行人員用抽屜既有的「完成任務」按鈕手動完成(跟一般任務一模一樣)。若證據不可用,可用既有「上傳證據」補一份、或重新執行,再按完成。
-
[ ] Step 5: i18n 補文案(start_execution、execution_history、re_execute、status_running/succeeded/failed 等,zh-tw+en)。
-
[ ] Step 6: 手測 → auto 任務執行後自動完成;manual 任務執行後停 PROCESSING、可看歷史、可重掃、可下載報告、可手動完成。
-
[ ] Step 7: Commit(FE repo)
git commit -m "feat(fr056): detection task execution drawer (T-4.3)"
完成後收尾(等 user 下令)
- FE error-code.json 同步(Task 1/2 新增的 error code,三語系 zh-tw/en/zh-cn)。
- STG/POC 套三支 migration(source constraint + detection_executions + 前階段的)。
- 更新任務執行頁 spec(
docs/specs/current/)。 - Notion FR-056.4 子卡標「修正待驗證」+ 回寫;母案 FR-056 標整體完成。
- e2e 測試計畫(test repo)——完整 auto/manual 兩路 happy path + 失敗重試。
決策狀態
- evidence_type → D11 定案:用
FILE(不用 REPORT)。REPORT 會牽動其他判斷證據檔案的地方、改動面大,第一版 FILE 最安全,未來要區分檢測報告類證據另開需求。 - 「開始執行」守門軸(runner 逕行):這是 GRC 專案內的 job 操作,對齊既有 job 操作(complete_job 等)的守門方式(專案參與者/角色),非工具管理的軸④ capability。runner 看既有 job route 守門照做——實作對齊,非阻塞決策。
附錄:範本來源
- 轉證據三段式:
app/cloud_integration/service/handlers/import_drive_file_handler.py::handle(DRIVE_SYNC) - job_evidences source constraint:
scripts/sql/2026-04-24-job-evidences-add-drive-source.sql - 通知:
app/grc/service/job_batch_complete_service.py::_send_batch_summary_notification+notification_service.send_mail_notification - 收件人查詢:
infra/grc/repository/job_batch_complete_query.py::get_batch_notification_data(task_assignees/ParticipantRole) - 既有完成任務:
app/flow_engine/service/workflow_execution_service.py::complete_job(不改,auto 模式直接呼叫) - 報告下載:
api/uploadfile/routes/uploadfile_route.py::UploadFileDownloadRoute(/file/download/<uid>,沿用) - DDD 九層:同 FR-056.1 附錄 A