FR-137 · 已上線/收尾 · 2026-10-14
資料模型、每 5 分鐘到期排程、掃描自動執行、SBOM 門檻通知與紀錄、支援期提醒
專案管理者在規劃頁替任務設週期;下次啟動時間一到,系統就在同一筆任務上再跑一輪——已完成的任務重開成進行中、寄信提醒執行人,檢測工具任務開了自動執行就直接開掃,證據與執行紀錄都進同一筆任務。SBOM 掃完若比這筆任務上一次成功掃描多出 high 以上的弱點,就寄給額外指定的人(例如 CRA 的通報聯絡人),每封都留紀錄可查;資訊系統版本支援期剩 90/30/7 天提醒 CRA 專案管理者,記在同一張紀錄表。本文件寫現在的規則、為什麼這樣定、踩到會怎樣。
狀態:已上線/收尾(2026-10-14,全部卡 Done)|收尾彙整:
handoff/2026-10-14-FR-137-SUMMARY.md|討論稿:discussion.md|雛形:prototype/index.html
本文件描述上線的現況與決策理由。雛形三頁是設計階段的畫面,週期列文字與 Dialog 內容以 §6.7 為準。檔案路徑會隨程式漂移,查不到時以函式名重新定位。路徑慣例:BE 路徑相對
compliance-manager-be/;pkg/代表~/Projects/Jedicogy/module/jedi-python-package/;FE 路徑相對compliance-manager-fe/。
| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-10-12 | v1:D1~D19 設計定案;資料模型、排程、自動執行、門檻通知、收件人、畫面、支援期提醒;五卡拆分與端到端驗收 | CM-2721 |
| 2026-10-14 | 收尾:改寫成上線現況——週期任務始終同一筆(run_count/last_run_at)、排程每 5 分鐘、週期單位含每小時、自動執行直呼派工入口、上一次掃描以同一筆任務為範圍;決策表、§6、拆分與驗收同步 |
CM-2714 |
稽核裡很多工作不是做一次就好——每月掃一次源碼弱點、每季填一次問卷、每年檢視一次程序書。本案讓任務帶一組週期設定:時間到了,系統在同一筆任務上再跑一輪並提醒執行人。對使用者來說一個週期任務就是一張卡,每一輪多一筆執行紀錄、多一批證據,都在同一筆任務裡。
掃描類任務更進一步:開了自動執行,時間到直接開掃,不用人按。檢測工具派給 agent,SBOM 原始碼掃描沿用上次上傳的源碼包。
CRA 要求製造商知悉高風險漏洞要及時處理,負責對外通報的人常常不是執行人。所以 SBOM 掃完時與這筆任務上一次成功的掃描比,新出現 high(含)以上的弱點時,多寄一封信給指定的人,每封留一筆紀錄,稽核時可拿來證明「知悉後有通知」。同一批老弱點不會每輪重寄。
資訊系統的版本支援期到期前 90/30/7 天提醒,走同一支排程與同一張紀錄表。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart TD
MF[資源庫 frequency] -.建專案時帶預設頻率.-> P[規劃頁任務週期設定<br/>頻率・間隔・下次啟動・自動執行・額外通知]
P --> T{{每 5 分鐘 tick<br/>撈 next_due_at 已到的任務}}
T --> S{輪次結案或<br/>專案結束?}
S -- 是 --> Z[清空 next_due_at,不再啟動]
S -- 否 --> U[條件式推進:next_due_at 往後推一格<br/>run_count+1・last_run_at]
U --> O[任務已完成就重開成進行中]
O --> K{檢測工具任務且<br/>自動執行開著?}
K -- 是 --> A[直呼派工入口開掃<br/>執行紀錄 created_user=system]
K -- 否 --> M
A --> M[寄信提醒執行人]
A --> D2[SBOM:沿用上次源碼包排背景掃]
D2 --> C[掃描完成回呼 on_scan_completed]
C --> F{與同一筆任務上一次成功掃描比<br/>有新出現 high 以上?}
F -- 否 --> X[不寄・不記]
F -- 是 --> R[寄給額外通知對象<br/>+寫通知紀錄]
整案一句話:任務上多一組週期設定;每 5 分鐘排程把到期的任務在原地重開、推進下次啟動時間並提醒執行人或直接開掃;SBOM 掃完比對同一筆任務上一次成功掃描,新出現 high 以上就寄給額外通知對象並記一筆。
| 卡 | 做什麼 | 產出 |
|---|---|---|
| ① 週期設定畫面 | 規劃頁任務表單多一行週期摘要+「設定」開週期 Dialog;任務頁籤有週期的加 ↻;頻率選項進 system_menus |
migration(任務週期欄、頻率 seed)、jedi-flow-engine ORM 帶欄、寫入驗證、FE 週期 Dialog 子元件、FE 四處頻率改讀 menu |
| ② 到期處理+提醒執行人 | 排程查到期任務,在同一筆上推進下次啟動、必要時重開,寄信給執行人;輪次結案或專案結束即停 | 排程 job、task_recurrence_app_service、任務抽屜週期列 |
| ③ 掃描自動執行 | 自動執行開著的檢測工具任務,到期直接開掃 | task_auto_execute_service、抽屜執行紀錄「自動執行」標記 |
| ④ 門檻通知+紀錄 | SBOM 掃完比上一次,新出現 high 以上就寄給額外通知對象,每封記一筆 | 通知紀錄表、jedi-source-scan 掃完回呼、BE 門檻判斷、專案內「通知紀錄」頁 |
| ⑤ 支援期提醒 | 版本支援期剩 90/30/7 天提醒 CRA 專案管理者,記進同一張紀錄表 | 排程尾端一段、版本→專案反查、紀錄表 object_type/stage |
| 項 | 現行規則 | 理由 | 被排除 |
|---|---|---|---|
| D1 重開語意 | 週期到了在同一筆任務上再跑一輪:任務已完成就重開成進行中,證據與執行紀錄都進同一筆;run_count 記排程已啟動幾次 |
對使用者來說就是同一個任務——設了週期掃描,時間到去掃,證據上傳到同一個任務內、多一個執行紀錄就好;複製出來的新任務使用者不認得,還要另管複製鏈與冪等 | 每輪複製成一筆新任務 |
| D2 屬哪一輪 | 任務不變,自然留在所屬的稽核輪次;不另立「週期輪」 | 輪次是稽核範圍的單位,週期是任務層的節奏 | 另立「週期輪」概念 |
| D3 週期設定存哪 | 任務層欄位(頻率、間隔、下次啟動、自動執行、額外通知對象);資源庫 module_frames.frequency 只在建專案時帶入預選頻率,不影響既有專案 |
一個評估目標底下會有多筆任務、各自頻率不同;config_schema 只到租戶層且平台不讀 |
放評估目標(AO);放任務型別的 config_schema |
| D4 到期誰判斷 | BE 排程每 5 分鐘 tick 一支 task_recurrence_tick 直接查任務表;不在 jedi-task-platform 加到期掛鉤 |
週期是產品業務規則,比照檢測派掃從平台搬出的原則;週期單位有每小時,檢查間隔要明顯短於最小單位 | 在任務平台申報表加到期/完成掛鉤 |
| D5 | 作廢。SBOM 重新執行本來就沿用上次上傳的源碼包,週期自動掃走同一條路 | — | — |
| D6 兩種掃描的派工 | 到期判斷都在同一支 tick;開掃直呼派工入口 detection_orchestration.start_execution,檢測工具建 agent_tasks、SBOM 走伺服器端派工沿用綁定上的源碼包 |
兩條派掃路都已存在且可靠,排程只負責「何時開始」;平台啟動掛鉤排除 upload 模式(SBOM 正是 upload),且只派沒有執行紀錄的任務,週期任務第二輪起一定有紀錄(§6.4) | 為兩種掃描各做排程;走平台啟動掛鉤 |
| D7 門檻等級 | SBOM 標準六級,high(含)以上=CRITICAL、HIGH;檢測工具結果沒有統一等級,不做 | 檢測工具單次結果只有各工具不同鍵的 summary,要先正規化才談得上門檻 | 連檢測工具一起做 |
| D8 何時通知 | 與同一筆任務本次之前最近一筆成功掃描比 fingerprint 差集,差集裡有 high 以上才通知 | 同一批老弱點每輪都寄等於沒寄;fingerprint 不含版本,升版仍受同一弱點影響不算新的 | 每次掃完有 high 以上就寄 |
| D9 通知紀錄範圍 | 紀錄表只記門檻通知與支援期提醒,放 BE 主專案,不動 jedi-notification | 決策者要的是「哪次掃描、什麼等級、何時通知誰」可查;指派/完成信納入要改所有既有寄信點 | 所有寄信都記;放進 jedi-notification |
| D10 語系時區 | 信件 zh_Hant_TW、時區 Asia/Taipei | 使用者與租戶都沒有語系/時區欄位,補上是獨立工程 | 本案補使用者/租戶語系時區 |
| D11 冪等 | 推進用條件式 UPDATE(WHERE next_due_at = 舊值),多副本同時 tick 只有一個推得動;門檻通知與支援期提醒靠紀錄表唯一索引先搶列才寄;承諾單副本,不加分散式鎖 |
既有排程 job 都靠冪等,沒有分散式鎖;條件式 UPDATE 不需要額外的表或鍵 | 分散式鎖 |
| D12 頻率選項與閒置欄 | 頻率值進 system_menus group task_frequency(hourly/daily/weekly/monthly/quarterly/semi_annually/annually 七個 key),FE 四處改讀 menu;module_frames.last_execute_date/next_execute_date 不啟用;TASK_TYPE 五 key 與申報型別對不上一事不處理 |
下拉選項不寫死是專案鐵則;閒置欄語意是範本的執行日,跟任務層週期是兩回事 | 啟用資源庫兩個閒置欄;順手對齊 TASK_TYPE |
| D13 問卷 | 週期問卷任務沿用同一份問卷快照,不建新快照;重開時不清上一輪答案(是否清空待決策者看畫面再裁);「填完問卷不自動完成任務」不處理 | 任務不複製就沒有新快照可掛;清不清答案牽涉「上一輪的回覆算不算證據」,要看實際畫面再定 | 每輪建空白快照 |
| 通知對象 | 預設=任務執行人員;檢測工具任務多一個「掃到新出現 high(含)以上時額外通知誰」,可選專案參與人員或系統責任角色(CRA 填通報聯絡人 vulnerability-contact) |
執行人是做事的人;對外通報的責任人常常不是執行人 | — |
| D14 動兩支套件 | jedi-flow-engine 的 job_executions 直接加週期欄;jedi-source-scan 在 SBOM 掃描完成處加 on_scan_completed 回呼,由 BE 註冊。已發版 jedi-flow-engine 1.4.3、jedi-source-scan 0.1.1 |
週期是任務本身的資料,放套件不另開 sidecar 表;回呼只影響 SBOM,與 D7 一致,範圍比改 jedi-detection 的匯合點小 | BE 另開一對一任務週期表(sidecar);改 jedi-detection 匯合點 _close_group_if_terminal |
| D15 支援期提醒記哪 | 通知紀錄表以 object_type/object_uid/stage 區分種類,支援期 90/30/7 天提醒記在同一張表,唯一索引含這幾欄 |
「寄過了沒」與「何時寄給誰」只有一處可查 | 版本列單欄戳記(三個階段要三欄,且查不到寄給誰) |
| D16 通報期限提醒 | 只做支援期提醒;24 小時預警/72 小時通報/最終報告提醒不做 | 起算點是通報案件的「知悉時間」,系統裡沒有這筆資料 | 一併做通報三階段提醒 |
| D17 首掃 | 沒有上一次可比的掃描,high 以上全部當新出現、照寄 | 第一次掃描就是第一次知道它們存在 | 首掃不寄 |
| D18 頻率 menu group 名 | system_menus group=task_frequency,任務與資源庫共用 |
與近期新增的小寫 group(ssp_party_role、risk_reference_standard)一致 |
— |
| D19 何時停止 | 任務所在輪次已結案(project_audit_rounds.status = closed)或專案已結束(projects.status 為 completed/archived)時,到期不再啟動並清空 next_due_at;週期開關關掉同樣清空。每輪按「完成」只代表這一輪做完,到期仍會重開 |
排程是系統動作、繞過規劃期守門,要另設停止條件;結案後再動任務會讓已結案的輪次被改動 | 按完成就整條停(每輪本來就會完成,週期會失效) |
上一輪還沒做完、下一次啟動時間又到了:照樣推進並提醒,任務維持進行中,不會疊出第二筆。
| 元件 | 位置 | 本案怎麼用 |
|---|---|---|
任務表 compliance.job_executions |
ORM 在 pkg/jedi-flow-engine/jedi_flow_engine/infra/models/job_execution.py(entity/mapper 同套件) |
加週期欄(§6.1);寫入只走套件的 update_recurrence,不經整筆蓋回的 update_job_execution |
| 週期寫入與驗證 | app/flow_control/service/job_recurrence_service.py、job_recurrence_validator.py;JobService.update_job 收 recurrence 物件一行轉交 |
規劃頁存檔(卡 ①) |
| 建專案帶預設 | app/flow_control/service/prep_job_generation_service.py 以 recur_unit_from_frequency 寫 recur_unit |
只預選頻率,next_due_at 留空 |
| 排程 | core/scheduler.py 的 task_recurrence_tick(@_job_scoped+system_context);只有 RUN_MODE=api 起 |
每 5 分鐘一輪(§6.2) |
| 到期處理 | app/flow_control/service/task_recurrence_app_service.py;SQL 在 infra/flow_control/repository/task_recurrence_query.py;時間推算 task_recurrence_schedule.py |
§6.2 |
| 自動執行 | app/flow_control/service/task_auto_execute_service.py → detection_orchestration.start_execution |
§6.4 |
| 任務提醒信 | notify_user_todo_job(app/flow_engine/service/workflow_execution_service.py,只在任務 PROCESSING 時寄,寄每位指派人) |
每輪沿用 |
| SBOM 掃完 | pkg/jedi-source-scan/jedi_source_scan/app/jobs/sbom_scan_job.py 的 _notify_completed;契約 plugin/contract.py 的 SourceScanAdapters.on_scan_completed |
門檻通知觸發點(§6.5) |
| 差異比對 | pkg/jedi-source-scan/jedi_source_scan/plugin/scan_alert_diff.py 的 new_findings(previous_scan_uid, scan_uid),配對鍵 fingerprint |
取新出現再濾 CRITICAL/HIGH |
| 門檻通知 | app/task_alert/service/scan_alert_app_service.py、scan_alert_recipients.py;讀模型 infra/readmodel/detection/scan_alert_context_query.py |
§6.5、§6.6 |
| 通知紀錄 | domain/task_alert/、infra/task_alert/、app/task_alert/service/task_alert_notification_app_service.py、api/task_alert/ |
§6.1 ②、§6.7 |
| 支援期提醒 | app/task_alert/service/support_end_reminder_service.py;讀模型 infra/readmodel/detection/support_end_query.py |
§6.8 |
| 寄信 | NotificationService.send_mail_notification;SMTP 未啟用回 False 不丟例外 |
回 False 記 failed |
| Party 與角色 | SSP parties,email 鍵 email_address;角色選單 ssp_party_role |
收件人解析(§6.6) |
| 頻率選項 | FE src/composables/useTaskFrequencyOptions.js;menuStore.js、ModuleFrameBasicInfoPanel.vue、SspDocxImportPage.vue、SheetPreviewBasic.vue 讀 menu |
D12、D18 |
| 規劃頁 | src/components/grc/planning/AoTaskSection.vue(頁籤 ↻)、AoTaskForm.vue 一行引用 TaskRecurrenceSummary.vue |
§6.7 |
| 任務抽屜 | JobExecutionDrawer.vue 引用 src/components/grc/job-drawer/JobRecurrenceRows.vue |
§6.7 |
| 專案概覽 | src/views/project/ProjectAuditorOverview.vue 頁首「通知紀錄」;新頁 ProjectAlertNotificationsView.vue |
§6.7 |
① compliance.job_executions 週期欄(jedi-flow-engine ORM/entity/mapper 三處帶欄;主線 migration 2026-10-12-fr137-job-recurrence.sql 與 2026-10-13-fr137-job-recurrence-same-task.sql,出貨基線已含最終結構):
| 欄 | 型別 | 用途 |
|---|---|---|
recur_unit |
varchar(32) 可空 | 週期單位,值=task_frequency 的 key;空=不重複 |
recur_interval |
int NOT NULL DEFAULT 1 | 每幾個單位一次(每 2 個月=monthly+2) |
next_due_at |
timestamptz 可空 | 下次啟動時間;排程只看這欄,空=停止 |
auto_execute |
boolean NOT NULL DEFAULT false | 到了啟動時間是否直接開掃;只有檢測工具任務可設 |
alert_recipients |
jsonb 可空 | 新出現 high 以上時額外通知誰:[{"type":"participant","user_id":12}, {"type":"party_role","role_id":"vulnerability-contact"}];只有檢測工具任務可設 |
run_count |
int NOT NULL DEFAULT 0 | 排程已在這筆任務上啟動過幾次;畫面顯示「第 run_count+1 輪」 |
last_run_at |
timestamptz 可空 | 排程最近一次啟動時間 |
CREATE INDEX IF NOT EXISTS ix_job_executions_next_due_at
ON compliance.job_executions (next_due_at) WHERE next_due_at IS NOT NULL;寫入驗證(job_recurrence_validator.py):recur_unit 必為 task_frequency 的 enable key;recur_interval ≥ 1;開重複時 next_due_at 必填;auto_execute/alert_recipients 只接受檢測工具任務,其他型別送來回 400;alert_recipients 每筆 type 限 participant(前端送 user_uid,必為本專案參與者,存成 user_id)或 party_role(role_id 必在 ssp_party_role menu)。錯誤碼 GRC_400141~GRC_400145(common/code/flow_control_error_code.py,GRC_TASK_RECUR_*)。
② 表 compliance.task_alert_notifications(主線 migration 2026-10-12-fr137-task-alert-notifications.sql、2026-10-13-fr137-task-alert-notifications-object.sql):
| 欄 | 型別 | 用途 |
|---|---|---|
id |
bigserial PK | |
uid |
varchar(36) NOT NULL UNIQUE | |
tenant_id |
int NOT NULL | FK → public.tenants ON DELETE CASCADE |
org_unit_id |
int | |
project_id |
bigint NOT NULL | 專案內頁依此列;FK → compliance.projects ON DELETE CASCADE |
object_type |
varchar(32) NOT NULL DEFAULT 'scan_alert' |
scan_alert(SBOM 門檻)/support_end(支援期提醒) |
object_uid |
varchar(36) | support_end:資訊系統版本 uid(soft-ref) |
stage |
varchar(16) | support_end:d90/d30/d7 |
job_execution_id |
bigint 可空 | 哪一筆任務(scan_alert 才有) |
scan_uid |
varchar(36) 可空 | 哪一次掃描(scan_alert 才有) |
vs_scan_uid |
varchar(36) 可空 | 跟哪一次比;首掃為空 |
new_critical_count/new_high_count |
int NOT NULL DEFAULT 0 | 新出現 CRITICAL/HIGH 筆數 |
recipient_email |
varchar(320) NOT NULL | 收件信箱(一人一列) |
recipient_label |
varchar(255) | 顯示用:參與者暱稱或 Party 名稱+角色 |
sent_at |
timestamptz | 寄出時間 |
result |
varchar(16) NOT NULL DEFAULT 'pending' |
pending/sent/failed |
error |
text | 失敗原因(例:郵件伺服器未啟用) |
created_at/updated_at |
timestamptz NOT NULL DEFAULT now() |
唯一索引 uq_task_alert_notifications_object_recipient (object_type, COALESCE(scan_uid, object_uid), COALESCE(stage, ''), recipient_email)(D11、D15);CHECK object_type IN ('scan_alert','support_end') AND (scan_uid IS NOT NULL OR object_uid IS NOT NULL)。RLS 比照 compliance.source_scans:ENABLE+FORCE、一條 FOR ALL policy、GRANT cm_app 含 sequence。
③ 內頁登記(2026-10-12-fr137-alert-notifications-route.sql):public.ui_routes 加 project-alert-notifications(/project/projects/:id/alert-notifications,pid 掛 project-auditor-overview,不上側欄),進入門檻 ALL grc.project.read。🔴 name 必須與 FE 路由 name 一字不差,沒登記就誰都進不去。
④ public.system_menus 頻率 seed(phase=seed,ON CONFLICT ("group", key) DO NOTHING;2026-10-12-fr137-task-frequency-menu.sql、2026-10-13-fr137-task-frequency-hourly.sql):
| group | key | sort |
|---|---|---|
task_frequency |
hourly |
5 |
task_frequency |
daily |
10 |
task_frequency |
weekly |
20 |
task_frequency |
monthly |
30 |
task_frequency |
quarterly |
40 |
task_frequency |
semi_annually |
50 |
task_frequency |
annually |
60 |
i18n 標籤走 lang.task_frequency.<key>(FE options.json,zh-tw/en)。資源庫的 none 不進 menu:任務上「不重複」由週期開關表示。
core/scheduler.py 的 task_recurrence_tick:cron 每 5 分鐘(minute="*/5",UTC),max_instances=1, coalesce=True,寫法照既有 @_job_scoped+app.app_context()+system_context。邏輯本體在 TaskRecurrenceAppService.run_once(),支援期提醒(§6.8)接在同一支 tick 尾端。
每輪:
system_context):next_due_at <= now() 的 USER 任務,每輪上限 500 筆。先套停止條件(D19):沒有所屬專案、輪次 closed、專案 completed/archived 的,直接清空 next_due_at 並記 info log。其餘依租戶分組。tenant_context,每筆各包一個 SAVEPOINT(單筆失敗不擋同批,失敗時 next_due_at 不動,下一輪再試):
next_due_at 依 recur_unit×recur_interval 往後推(不從 now() 算,排程延遲不會讓週期漂移);推完仍 ≤ now() 就繼續推到未來,停擺多格也只跑一輪。月以上單位以「原點+第 k 格」計算,31 號遇小月取月底,下個大月仍回到 31 號。UPDATE ... SET next_due_at = 新值, run_count = run_count + 1, last_run_at = now() WHERE id = :id AND next_due_at = :舊值;更新 0 列=另一副本已推過,本副本跳過。COMPLETED 就改回 PROCESSING、清 end_time;所屬流程實例已完成一併改回 PROCESSING。force_locale("zh_Hant_TW") 呼叫 notify_user_todo_job 寄信提醒執行人。開掃失敗不影響寄信,寄信失敗不影響已推進的任務。next_due_at 在畫面以 Asia/Taipei 顯示、存 UTC(D10),可選到時分。
週期任務始終是同一筆:不複製任務、不在流程圖掛新節點、問卷不建新快照。使用者眼中一個週期任務就是一張卡,每一輪的成果是「多一筆執行紀錄、多一批證據」,都在同一筆任務的抽屜裡、依時間排序(證據最新在上);第幾輪由 run_count 表示。複製的做法會長出使用者不認得的任務,還要另外處理複製鏈、唯一鍵、指派/部門/設備/檢測綁定的逐項複製,任一處沒對齊就靜默停掉週期。
TaskAutoExecuteService.start(job, tenant_id),caller 已在交易內且已進 tenant_context:
PROCESSING 就不開掃、記 warning——還在排隊(上游未完成)的任務不硬轉進行中,那會跳過流程順序。已完成的任務已由 §6.2 先重開。detection_orchestration.start_execution(job_uid, "system", tenant_id)。檢測工具建 agent_tasks;SBOM 走伺服器端派工,沿用綁定上上次上傳的源碼包排背景掃描。created_user = "system",抽屜據此標「自動執行」。結果與證據進同一筆任務。為什麼不走平台啟動掛鉤:掛鉤挑任務時刻意排除 upload 模式(防首次無檔 400 迴圈),SBOM 正是 upload 模式,走掛鉤永遠派不出去;且掛鉤只派「沒有執行紀錄」的任務,週期任務第二輪起一定有紀錄。直呼派工入口兩個問題都沒有。
派工失敗(例如從未上傳過源碼包)只記 warning,執行人仍收到提醒信,可從抽屜手動執行;Dialog 在檢測工具任務沒有源碼包時提示「需先手動執行一次」。
套件端(jedi-source-scan 0.1.1):
plugin/contract.py 的 SourceScanAdapters 有選填 on_scan_completed: Optional[Callable[[str], None]](參數=scan_uid)。SbomScanJob 在掃描完成並轉回檢測結果之後呼叫;回呼失敗只記 logger.exception,不會把已完成的掃描改成失敗。plugin/scan_alert_diff.py 的 new_findings(previous_scan_uid, scan_uid) 給宿主用:previous_scan_uid 為空=首掃,這次全部算新出現。BE 端:core/plugins/source_scan.py 的 build_adapters 註冊 on_scan_completed,轉交 ScanAlertAppService.handle(scan_uid)(worker 行程、以建單者身分、RLS 照常):
done、任務沒有 alert_recipients 或沒有所屬專案就結束。done、不是本次、完成時間早於本次的掃描中最近一筆;沒有=首掃(D17)。new_findings(上一次, 這一次) 濾 severity in ("CRITICAL", "HIGH")。為空就結束——不寄、不記。send_mail_notification(..., is_html=True);回 True 記 sent,回 False 或丟例外記 failed、error 寫原因。不重試、不自動補寄。不用 summary.delta:summary.delta 以掃描對象(target_uid)找上一次,任務發起的掃描不建掃描對象,target_uid 為空、delta 永遠是空的;照它判斷會每次都當首掃、整批老弱點每輪重寄,違反 D8。
信件:主旨與內文 msgid 在 config/translations/ zh_Hant_TW,排程與背景 job 沒有 request,以 force_locale("zh_Hant_TW") 取字串(D10)。內文列:專案、任務名、新出現 CRITICAL/HIGH 筆數、前五筆弱點編號與套件、掃描結果頁連結。使用者輸入的字串一律 html.escape。
app/task_alert/service/scan_alert_recipients.py:
type |
解析 |
|---|---|
participant |
user_id → 使用者 email;recipient_label=暱稱(無則登入名);已不是本專案參與者則略過 |
party_role |
專案 living_ssp_id → SSP parties,取該角色已填名字的 Party,每個 Party 的 email_address 各一位;recipient_label=<Party 名>(<角色顯示名>) |
排程與回呼裡沒有登入者,Party 走 domain service,不呼叫帶 require_participant 守門的 app service。同一信箱(不分大小寫)只寄一封。沒有任何可用信箱就記 warning 結束,不寫紀錄。
規劃頁(卡 ①):
AoTaskForm.vue「任務類型」下方一行引用 TaskRecurrenceSummary.vue:有週期時顯示 ↻ 每月 · 第 N 輪 · 下次啟動 2026-11-01 09:00 · 自動執行 · 高風險額外通知 2 個對象 [設定],沒設時顯示「不重複 [+ 設定週期]」。TaskRecurrenceDialog.vue:重複開關、頻率(讀 task_frequency)、間隔、下次啟動時間(可選到時分);檢測工具任務多「自動執行」與「掃到新出現的 high(含)以上時,額外通知」(專案參與人員/系統責任角色,可多筆);一般與問卷任務底部提示「到了啟動時間,這筆任務會重新開啟並通知指派人員」。沒有歷次列表。recurrence 物件。AoTaskSection.vue 頁籤列:有週期的任務名稱後加 ↻。frequency 不是 none 就寫 recur_unit,next_due_at 留空(只預選頻率),專案管理者在 Dialog 填下次啟動後才生效。任務抽屜(卡 ②③):
JobRecurrenceRows.vue:檢測工具任務放在檢測設定區、與工具/掃描模式/完成模式同一個 grid;一般與問卷任務放在描述與指南之後。週期列:每小時 · 第 N 輪 · 下次啟動 …(N=run_count+1);停掉後 已停止重複 · 第 N 輪。檢測工具任務另列自動執行(是/否)與高風險額外通知對象。created_user == "system" 的那筆標「自動執行」,提示「到了啟動時間系統自動開始,沿用上次上傳的源碼包」。通知紀錄(卡 ④⑤):
ProjectAuditorOverview.vue 頁首「通知紀錄」一顆(所有參與者可見)。ProjectAlertNotificationsView.vue,路由 name project-alert-notifications,有返回鍵;上方三個計數(全部、已寄出、寄送失敗),有失敗時提示郵件伺服器未啟用、失敗的不會自動補寄;篩選:結果、期間。POST /api/1.0/project/<project_uid>/alert-notifications(分頁,守門:專案參與者),回傳補任務名、掃描源碼包名、支援期列的系統名與版本。task_recurrence_tick 尾端呼叫 SupportEndReminderService.run_once()。撈 support_end_date - today(Asia/Taipei) 等於 90、30、7 天的版本;tick 一天跑很多次,同一天同一階段靠唯一索引只寄一次。infra/readmodel/detection/support_end_query.py):版本 → 資訊系統 uid → oscal.ssp_inventory_items.props 的 ref-id=該 uid → SSP → compliance.projects.living_ssp_id → 專案,濾框架 CRA_2024、未刪除、狀態不是 completed/archived → 該專案參與者中 role=manager 的 email。沒掛任何 CRA 專案的版本不寄。task_alert_notifications,object_type='support_end'、object_uid=版本 uid、stage=d90/d30/d7、project_id=收件人所屬專案;先搶列才寄,寫紀錄時逐專案切 tenant_context。| 卡 | 做什麼 | 動哪些 repo/檔 | migration | 套件 | 狀態 |
|---|---|---|---|---|---|
| ① 週期設定畫面 CM-2722 |
§6.1 ①④、§6.7 規劃頁;建專案帶資源庫頻率預設 | pkg:jedi-flow-engine model/entity/mapper、update_recurrence;BE:job_recurrence_service.py、job_recurrence_validator.py、prep_job_generation_service.py、error code;FE:TaskRecurrenceSummary.vue、TaskRecurrenceDialog.vue、AoTaskForm.vue 一行、AoTaskSection.vue ↻、四處頻率讀 menu、i18n |
fr137-job-recurrence(active)、fr137-task-frequency-menu(seed) |
jedi-flow-engine | ✅ Done |
| ② 到期處理+提醒執行人 CM-2723 |
§6.2、§6.7 抽屜 | BE:core/scheduler.py 一支 job、task_recurrence_app_service.py、task_recurrence_schedule.py、task_recurrence_query.py;FE:JobRecurrenceRows.vue、JobExecutionDrawer.vue 引用 |
— | — | ✅ Done |
| ③ 掃描自動執行 CM-2724 |
§6.4 | BE:task_auto_execute_service.py、DI;FE:抽屜執行紀錄「自動執行」 |
— | — | ✅ Done |
| ④ 門檻通知+紀錄 CM-2725 |
§6.1 ②③、§6.5、§6.6、§6.7 通知紀錄 | pkg:jedi-source-scan contract.py、sbom_scan_job.py、scan_alert_diff.py;BE:core/plugins/source_scan.py、app/task_alert/(新模組)、domain/task_alert/、infra/task_alert/、api/task_alert/、信件 msgid;FE:ProjectAuditorOverview.vue 一顆、ProjectAlertNotificationsView.vue、路由、i18n |
fr137-task-alert-notifications、fr137-alert-notifications-route(active) |
jedi-source-scan | ✅ Done |
| ⑤ 支援期提醒 CM-2726 |
§6.1 ②、§6.8 | BE:support_end_reminder_service.py、support_end_query.py、排程尾端一行;FE:通知紀錄頁支援期列 |
fr137-task-alert-notifications-object(active) |
— | ✅ Done |
| 小卡 CM-2730 | SSP Excel 範本分頁「參與人員」改名「責任人員」,舊名仍可匯入 | BE sheet_definitions、parser 別名表 |
— | — | ✅ Done |
| 小卡 CM-2731 | 週期單位加每小時、下次啟動可選到時分 | BE 排程間隔;FE Dialog | fr137-task-frequency-hourly(seed) |
— | ✅ Done |
| 改向 CM-2732 | 週期任務始終同一筆(run_count/last_run_at、條件式推進、原地重開) |
BE task_recurrence_*;FE 週期列;pkg jedi-flow-engine |
fr137-job-recurrence-same-task(active) |
jedi-flow-engine | ✅ Done |
另:2026-10-13-fn-purge-projects.sql(維運函式 compliance.purge_projects(ids, dry_run),硬刪專案整棵資料樹,預設試算)在本 FR 期間加入,隨同一批進出貨基線。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
C1[① 週期設定畫面] --> C2[② 到期處理<br/>+提醒執行人]
C2 --> C3[③ 掃描自動執行]
C1 --> C4[④ 門檻通知+紀錄]
C2 --> C5[⑤ 支援期提醒]
C4 --> C5
用一個有 SBOM 原始碼掃描任務的 CRA 專案(SSP 已填「漏洞通報聯絡人」Party 與信箱),郵件設定已啟用,批次跑完再一次回報:
↻ 每月 · 第 1 輪 · 下次啟動 …)與 Dialog 都還在;頁籤有 ↻。next_due_at 改成過去時間 → 5 分鐘內:同一筆任務(沒有新任務出現)回到進行中、執行人收到提醒信、抽屜週期列變「第 2 輪」、下次啟動往後推一個月、執行紀錄多一筆「自動執行」且沿用上次源碼包。next_due_at 改成過去 → 5 分鐘後任務沒有被重開,下次啟動已清空、週期列顯示「已停止重複」(D19)。🔴 多副本沒有分散式鎖,冪等靠條件式 UPDATE 與紀錄表唯一索引
排程只在 RUN_MODE=api 起,承諾單副本;多個 api 副本同時 tick 時,推進靠 WHERE next_due_at = 舊值 只讓一個副本成功,門檻信與支援期提醒靠唯一索引先搶列才寄。兩處都必須「先寫到才往下做」,不能先查再寫。
| 陷阱 | 後果 | 對策 |
|---|---|---|
| 排程同步跑派掃與寄信,跑在共用執行緒池,每輪上限 500 筆 | 到期任務一多,單輪變長、拖到其他排程 | 現量撐得住;「排程只入列、worker 執行」列為另案 |
notify_user_todo_job 只在任務 PROCESSING 時寄 |
還在排隊(上游未完成)的任務到期不會收到信 | 屬預期:排隊中的任務本來就還不能做;上游完成時既有流程會寄 |
| 排隊中的任務開了自動執行 | 本輪不開掃 | 刻意不硬轉進行中,避免跳過流程順序;warning 記 log |
任務發起的 SBOM 掃描沒有掃描對象,summary.delta 永遠為空 |
每次都被當首掃,老弱點每輪重寄 | 「上一次」以同一筆任務為範圍自己找(§6.5) |
next_due_at 從 now() 起算 |
排程延遲幾分鐘,週期會一路往後漂 | 從本次 next_due_at 往後推(§6.2) |
週期欄寫入走 update_job_execution |
會把狀態、時間等整筆欄位一起蓋回 | 只走套件的 update_recurrence |
| 從未上傳過源碼包的 SBOM 任務開了自動執行 | 派工失敗、畫面沒有新的執行紀錄 | 只記 warning、照寄提醒信;Dialog 提示需先手動執行一次 |
SMTP 未啟用時 send_mail_notification 回 False、不丟例外 |
誤記成已寄出 | 回 False 一律記 failed |
| 排程裡沒有 request | gettext 取不到語系、get_user_context 為空 |
信件字串以 force_locale 取;操作者寫 system;寫入包 tenant_context |
| 問卷任務重開時保留上一輪答案 | 執行人可能直接送出舊答案 | 是否清空待決策者看畫面再裁 |
AoTaskForm.vue、JobExecutionDrawer.vue、ProjectPlanningView.vue、ProjectAuditorOverview.vue、workflow_execution_service.py 超標或貼線 |
往裡加會被 reviewer 判阻斷 | 只准一行引用,新邏輯一律新開子元件/新檔 |
| 項目 | 去向 |
|---|---|
| 排程只入列、worker 執行(派掃與寄信移出排程執行緒) | 架構優化另案 |
core/scheduler.py 各 job 寫法重構 |
另開卡 |
| 問卷任務重開時是否清空上一輪答案 | 待決策者看畫面再裁 |
| 檢測工具結果的門檻通知(D7) | 檢測結果等級正規化後另案 |
| 使用者或租戶層級的語系、時區(D10) | 獨立工程 |
system_menus TASK_TYPE 與申報型別對齊(D12) |
另案 |
| 填完問卷自動完成任務(D13) | 另案;next_status_on_answer_complete 已定義、無呼叫端 |
| 指派、完成、專案啟動信納入通知紀錄(D9) | 另案 |
| 通知設定頁(「哪些事件通知誰」的全域設定);專案首頁的提醒卡 | 另案 |
| 通報案件三階段期限提醒(D16) | 恢復通報案件時再接同一套排程與紀錄表 |
| 多副本分散式鎖(D11) | 多副本部署成為需求時再評估 |
| 「已比對、無新增」也記一筆當證據 | 需要時加 result='no_new' |