diff --git a/apps/api/src/api/v1/ai_governance.py b/apps/api/src/api/v1/ai_governance.py index 80068027..2620baa1 100644 --- a/apps/api/src/api/v1/ai_governance.py +++ b/apps/api/src/api/v1/ai_governance.py @@ -54,6 +54,7 @@ router = APIRouter() @router.get("/ai/governance/events", response_model=GovernanceEventsResponse) async def get_governance_events( + event_id: Annotated[list[str] | None, Query(alias="event_id")] = None, event_type: Annotated[list[str] | None, Query(alias="event_type")] = None, from_: Annotated[datetime | None, Query(alias="from")] = None, to: Annotated[datetime | None, Query(alias="to")] = None, @@ -66,6 +67,7 @@ async def get_governance_events( 查詢 AI 治理事件列表(分頁)。 - event_type: 多值過濾(可重複傳) + - event_id: 多值精準過濾(可重複傳),供 Telegram 詳情 / 歷史與 Work Items 錨點回看 - from / to: ISO 8601 時間範圍(URL 傳 from 參數) - status: resolved / unresolved - severity: critical / warning / info(由 event_type 映射決定) @@ -74,6 +76,7 @@ async def get_governance_events( """ logger.debug( "governance_events_request", + event_ids=event_id, event_types=event_type, from_=from_, to=to, @@ -83,6 +86,7 @@ async def get_governance_events( size=size, ) return await query_governance_events( + event_ids=event_id, event_types=event_type, from_dt=from_, to_dt=to, diff --git a/apps/api/src/services/governance_query_service.py b/apps/api/src/services/governance_query_service.py index 3794fe05..00cbbe35 100644 --- a/apps/api/src/services/governance_query_service.py +++ b/apps/api/src/services/governance_query_service.py @@ -195,6 +195,7 @@ async def _load_dispatch_ids_for_events(event_ids: list[str]) -> dict[str, list[ async def query_governance_events( *, + event_ids: list[str] | None = None, event_types: list[str] | None = None, from_dt: datetime | None = None, to_dt: datetime | None = None, @@ -212,6 +213,14 @@ async def query_governance_events( async with get_db_context() as db: stmt = select(AiGovernanceEvent) + normalized_event_ids = [ + event_id.strip() + for event_id in (event_ids or []) + if isinstance(event_id, str) and event_id.strip() + ] + if normalized_event_ids: + stmt = stmt.where(AiGovernanceEvent.id.in_(normalized_event_ids)) + if event_types: stmt = stmt.where(AiGovernanceEvent.event_type.in_(event_types)) diff --git a/apps/api/tests/test_ai_governance_endpoints.py b/apps/api/tests/test_ai_governance_endpoints.py index 56635b98..6346bd9f 100644 --- a/apps/api/tests/test_ai_governance_endpoints.py +++ b/apps/api/tests/test_ai_governance_endpoints.py @@ -176,6 +176,25 @@ class TestEventsEndpoint: assert r.status_code == 200 assert captured["severity"] == "critical" + def test_event_id_filter_passed(self, client): + """event_id query param 供 Telegram 詳情 / 歷史精準回看.""" + fake_response = GovernanceEventsResponse(items=[], total=0, page=1, size=20) + captured: dict = {} + + async def mock_query(**kwargs): + captured.update(kwargs) + return fake_response + + with patch("src.api.v1.ai_governance.query_governance_events", new=mock_query): + r = client.get( + "/api/v1/ai/governance/events" + "?event_id=evt-001&event_id=evt-002&status=unresolved" + ) + + assert r.status_code == 200 + assert captured["event_ids"] == ["evt-001", "evt-002"] + assert captured["status"] == "unresolved" + def test_invalid_severity_rejected(self, client): """非法 severity 值應被拒絕(422).""" r = client.get("/api/v1/ai/governance/events?severity=bad_value") diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index c045a55b..4cfd0c1f 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1526,6 +1526,8 @@ "dateRange": "Date Range", "status": "Status", "severity": "Severity", + "eventId": "Event ID", + "eventIdPlaceholder": "Paste governance_event_id", "clearAll": "Clear All", "allStatuses": "All Statuses", "resolved": "Resolved", @@ -1553,6 +1555,14 @@ "noDispatch": "No dispatch records" }, "eventType": { + "slo_violation": "SLO Violation", + "governance_slo_data_gap": "SLO Data Gap", + "knowledge_degradation": "KM Needs Update", + "kb_stale": "Stale KM", + "execution_blast_radius": "Execution Blast Radius", + "conservative_mode": "Conservative Mode", + "replay_degraded": "Replay Degraded", + "self_demotion": "AI Self-demotion", "slo_breach": "SLO Breach", "accuracy_drop": "Accuracy Drop", "km_stall": "KM Stall", @@ -1914,6 +1924,7 @@ "archiveProposal": "Archive candidates: {count} duplicate drafts", "ownerAction": "Owner action: {action}", "readOnlyPlan": "Writes on read: {writes}; archive blocked before review: {blocked}", + "openEventHistory": "Open Event History", "ownerActions": { "owner_review_canonical_then_archive_duplicates": "Review the canonical draft, then archive duplicates", "review_canonical_and_archive_duplicate_drafts": "Review canonical and archive duplicate drafts", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 33e58eec..a86f0a32 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -1527,6 +1527,8 @@ "dateRange": "時間範圍", "status": "狀態", "severity": "嚴重度", + "eventId": "事件 ID", + "eventIdPlaceholder": "貼上 governance_event_id", "clearAll": "清除全部", "allStatuses": "全部狀態", "resolved": "已解決", @@ -1554,6 +1556,14 @@ "noDispatch": "暫無派遣記錄" }, "eventType": { + "slo_violation": "SLO 違反", + "governance_slo_data_gap": "SLO 資料缺口", + "knowledge_degradation": "KM 需要更新", + "kb_stale": "KM 陳舊", + "execution_blast_radius": "執行爆炸半徑", + "conservative_mode": "保守模式", + "replay_degraded": "回放品質下降", + "self_demotion": "AI 自我降級", "slo_breach": "SLO 違反", "accuracy_drop": "準確率下降", "km_stall": "KM 停滯", @@ -1915,6 +1925,7 @@ "archiveProposal": "封存候選:{count} 份重複草稿", "ownerAction": "Owner 動作:{action}", "readOnlyPlan": "讀取不寫入:{writes};未審核不封存:{blocked}", + "openEventHistory": "開啟事件歷史", "ownerActions": { "owner_review_canonical_then_archive_duplicates": "審核 canonical 草稿後封存 duplicates", "review_canonical_and_archive_duplicate_drafts": "審核 canonical 並封存重複草稿", diff --git a/apps/web/src/app/[locale]/awooop/work-items/page.tsx b/apps/web/src/app/[locale]/awooop/work-items/page.tsx index ad531106..a490f9ac 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -856,6 +856,10 @@ function formatStaleRatio(value: number) { return `${(value * 100).toFixed(1)}%`; } +function governanceEventHistoryHref(eventId: string) { + return `/governance?tab=events&event_id=${encodeURIComponent(eventId)}`; +} + function buildWorkItems( telemetry: Telemetry, t: ReturnType @@ -1889,6 +1893,13 @@ function KnowledgeGovernancePanel({ )}
+ +