From 4e817dc21fbb4b1397c5c646844c2fa0a756f3d1 Mon Sep 17 00:00:00 2001 From: ogt Date: Fri, 10 Jul 2026 11:09:56 +0800 Subject: [PATCH] fix(agent99): add bilingual telegram incident cards --- HOST-REBOOT-AI-AUTOMATION-SOP.md | 10 ++ agent99-control-plane.ps1 | 157 ++++++++++++++++--------------- 2 files changed, 89 insertions(+), 78 deletions(-) diff --git a/HOST-REBOOT-AI-AUTOMATION-SOP.md b/HOST-REBOOT-AI-AUTOMATION-SOP.md index 75bc7378d..d97148b08 100644 --- a/HOST-REBOOT-AI-AUTOMATION-SOP.md +++ b/HOST-REBOOT-AI-AUTOMATION-SOP.md @@ -456,6 +456,7 @@ Recovery is complete only when all of the following are captured: - P0-5: route IWOOOS/Wazuh/SIEM alerts to `SecurityTriage`; collect source log reference, affected host/service, blast radius, repeated count, and allowlisted next action without arbitrary shell or secret exposure. - P0-6: route Gitea repository missing alerts to evidence-first `Status` until a dedicated Gitea recovery playbook is verified. - P0-7: run live TG-to-Agent99 end-to-end tests for each alert family before re-enabling the paused sensor schedules. + - P0-8: all Agent99 Telegram alert messages must include Traditional Chinese user-facing labels and human-readable context; professional terms such as `readback_failed`, `SecurityTriage`, `directReadback`, `KM/RAG`, and provider names may remain English when translation would reduce precision. - 2026-07-10 10:54 visual card verifier: - Runtime script deployed: `C:\Wooo\Agent99\bin\agent99-control-plane.ps1`. - Backup before deploy: `C:\Wooo\Agent99\bin\agent99-control-plane.ps1.bak-20260710-105439`. @@ -472,6 +473,15 @@ Recovery is complete only when all of the following are captured: - Result: `ok=True`, `caseCount=8`, `passedCount=8`, `failedCount=0`, `wroteAlerts=False`, `sentTelegram=False`. - Covered inputs: Agent99 echo suppression, provider freshness, host performance readback failure, backup health, Bitan AI config missing key, IWOOOS/Wazuh/SIEM security, Gitea repository missing, and public 502. - Severity correction: `readback_failed`, `missing_key`, locale guard, and AI support config failures stay `warning`; true `critical`, `P0`, `emergency`, `fatal`, or `down` signals remain `critical`. +- 2026-07-10 11:08 bilingual Traditional Chinese card verifier: + - Runtime script deployed with UTF-8 BOM to avoid Windows PowerShell 5 mojibake: `C:\Wooo\Agent99\bin\agent99-control-plane.ps1`. + - Backup before deploy: `C:\Wooo\Agent99\bin\agent99-control-plane.ps1.bak-20260710-110842`. + - Parser readback: `parse_ok`. + - Self-test evidence: `C:\Wooo\Agent99\evidence\agent99-TelegramCardSelfTest-20260710-110842.json`. + - Visual card: `C:\Wooo\Agent99\evidence\telegram-cards\agent99-card-performance_warning-20260710-110842.png`. + - Result: `ok=True`, `visualExists=True`, `sentTelegram=False`, `messageFormat=incident_card_zh_tw_v3`. + - UTF-8 readback confirmed local preview lines include: `Agent99 事件卡 / Incident Card`, `嚴重度 Severity`, `事件 Event`, `原因 Reason`, `影響 Impact`, `Agent99 動作 Action`, `目前狀態 Current`, and `時間 Time`. + - Local visual inspection: PNG is `1040x620`, non-blank, and Traditional Chinese text renders correctly. ## Agent99 Monitoring Alert Routing Self-Test Receipt diff --git a/agent99-control-plane.ps1 b/agent99-control-plane.ps1 index 62391b8a0..7f7d4847d 100644 --- a/agent99-control-plane.ps1 +++ b/agent99-control-plane.ps1 @@ -138,8 +138,8 @@ function Format-AgentTelegramText { default { $Severity } } $lines = @() - $lines += "Agent99 Incident Card" - $lines += "Severity: $severityLabel" + $lines += "Agent99 事件卡 / Incident Card" + $lines += "嚴重度 Severity: $severityLabel" if ($EventType -match "^performance_") { $hostName = Get-AgentObjectValue $Data "host" "unknown" @@ -147,49 +147,49 @@ function Format-AgentTelegramText { $disk = Get-AgentObjectValue $Data "diskUsedPercent" $null $load = Get-AgentObjectValue $Data "loadPerCore" $null $mem = Get-AgentObjectValue $Data "memAvailablePercent" $null - $lines += "Event: host performance" - $lines += "Host: $hostName" + $lines += "事件 Event: 主機效能 / host performance" + $lines += "主機 Host: $hostName" if ($reasons -contains "perf_readback_failed" -or ($null -eq $disk -and $null -eq $load -and $null -eq $mem)) { - $lines += "Reason: performance metric readback failed." - $lines += "Impact: load, memory, and disk values are not trustworthy yet." - $lines += "Agent99 action: keep noisy sensor paused; require verifier readback before remediation." + $lines += "原因 Reason: 效能指標 readback 失敗。" + $lines += "影響 Impact: CPU load、memory、disk 數值尚未可信,不能直接當成真故障。" + $lines += "Agent99 動作 Action: 維持噪音 sensor 暫停;修復前必須先補 verifier readback。" } elseif ($reasons -contains "disk_used_warning") { - $lines += "Reason: disk usage is $disk percent. Warning threshold reached; critical threshold is 95 percent." - $lines += "Agent99 action: run allowlisted disk remediation and suppress duplicates for 30 minutes." + $lines += "原因 Reason: 磁碟使用率 $disk percent,已達 warning threshold;critical threshold 是 95 percent。" + $lines += "Agent99 動作 Action: 執行 allowlisted disk remediation,並在 30 分鐘內 suppress duplicates。" } elseif ($reasons -contains "disk_used_high") { - $lines += "Reason: disk usage is $disk percent. Critical threshold reached." - $lines += "Agent99 action: run allowlisted cleanup first; reboot is not the first response." + $lines += "原因 Reason: 磁碟使用率 $disk percent,已達 critical threshold。" + $lines += "Agent99 動作 Action: 先執行 allowlisted cleanup;reboot 不是第一反應。" } elseif ($reasons -contains "load_per_core_warning" -or $reasons -contains "load_per_core_critical") { - $lines += "Reason: CPU load/core=$load." - $lines += "Agent99 action: use allowlisted load reduction only; no arbitrary service stop." + $lines += "原因 Reason: CPU load/core=$load。" + $lines += "Agent99 動作 Action: 僅使用 allowlisted load reduction;不得任意停止服務。" } elseif ($reasons -contains "memory_available_low") { - $lines += "Reason: available memory is $mem percent." - $lines += "Agent99 action: inspect memory pressure before allowlisted remediation." + $lines += "原因 Reason: 可用記憶體為 $mem percent。" + $lines += "Agent99 動作 Action: 先檢查 memory pressure,再進行 allowlisted remediation。" } else { - $lines += "Reason: $($reasons -join ', ')" - $lines += "Agent99 action: check Agent99 evidence for full readback." + $lines += "原因 Reason: $($reasons -join ', ')" + $lines += "Agent99 動作 Action: 檢查 Agent99 evidence,完成 full readback 後再處理。" } $metricParts = @() if ($null -ne $load -and [string]$load -ne "") { $metricParts += "load/core=$load" } if ($null -ne $mem -and [string]$mem -ne "") { $metricParts += "memAvail=$mem percent" } if ($null -ne $disk -and [string]$disk -ne "") { $metricParts += "disk=$disk percent" } if ($metricParts.Count -gt 0) { - $lines += "Current: $($metricParts -join ', ')." + $lines += "目前狀態 Current: $($metricParts -join ', ')." } else { - $lines += "Current: metrics unavailable; no blank metric alert is trusted." + $lines += "目前狀態 Current: metrics unavailable;空白指標不視為可信告警。" } } elseif ($EventType -match "^backup_") { $targetName = if ($Data -and $Data.PSObject.Properties["name"]) { $Data.name } else { "unknown" } $reason = if ($Data -and $Data.PSObject.Properties["reason"]) { $Data.reason } else { $EventType } $age = if ($Data -and $Data.PSObject.Properties["ageMinutes"]) { $Data.ageMinutes } else { $null } $maxAge = if ($Data -and $Data.PSObject.Properties["maxAgeMinutes"]) { $Data.maxAgeMinutes } else { $null } - $lines += "Event: backup health" - $lines += "Target: $targetName" - $lines += "Reason: $reason" + $lines += "事件 Event: 備份健康 / backup health" + $lines += "目標 Target: $targetName" + $lines += "原因 Reason: $reason" if ($age -ne $null -or $maxAge -ne $null) { - $lines += "Freshness: age=$age minutes / max=$maxAge minutes" + $lines += "新鮮度 Freshness: age=$age minutes / max=$maxAge minutes" } - $lines += "Agent99 action: inspect /backup/status, /backup/logs, and snapshot metadata only; do not read backup contents or secrets." + $lines += "Agent99 動作 Action: 只檢查 /backup/status、/backup/logs、snapshot metadata;不得讀取備份內容或 secrets。" } elseif ($EventType -eq "provider_freshness_triage") { $candidateId = if ($Data -and $Data.PSObject.Properties["candidateId"]) { $Data.candidateId } else { "unknown" } $targetName = if ($Data -and $Data.PSObject.Properties["target"]) { $Data.target } else { "provider_freshness_signal" } @@ -198,23 +198,23 @@ function Format-AgentTelegramText { $candidatePath = if ($Data -and $Data.PSObject.Properties["candidatePath"]) { $Data.candidatePath } else { $null } $readback = if ($Data -and $Data.PSObject.Properties["verifierReadback"]) { $Data.verifierReadback } else { $null } $status = if ($Data -and $Data.PSObject.Properties["status"]) { $Data.status } else { "unknown" } - $lines += "Event: provider freshness auto triage" - $lines += "Target: $targetName" - $lines += "Status: $status" - $lines += "Impact: dashboard green is not trusted until source freshness is proven." - $lines += "Evidence hits: $hitCount" + $lines += "事件 Event: Provider freshness 自動判讀 / provider freshness auto triage" + $lines += "目標 Target: $targetName" + $lines += "狀態 Status: $status" + $lines += "影響 Impact: source freshness 未證明前,dashboard green 不可視為健康。" + $lines += "證據命中 Evidence hits: $hitCount" if ($readback) { - $lines += "Verifier: directReadback ok=$($readback.ok), sourceEvents=$($readback.sourceEventTotal), latest=$($readback.latestReceivedAt)" + $lines += "驗證 Verifier: directReadback ok=$($readback.ok), sourceEvents=$($readback.sourceEventTotal), latest=$($readback.latestReceivedAt)" } if (@($missing).Count -gt 0) { - $lines += "Still missing: $($missing -join ', ')" + $lines += "仍缺欄位 Still missing: $($missing -join ', ')" } else { - $lines += "Missing: direct readback fields completed" + $lines += "缺漏狀態 Missing: direct readback fields completed" } - $lines += "Agent99 action: candidate and KM/RAG were written; duplicate same-state alerts are suppressed." - $lines += "Guard: no provider switch, paid model call, env change, or reload." + $lines += "Agent99 動作 Action: 已寫入 candidate 與 KM/RAG;相同狀態 duplicate alerts 會被 suppress。" + $lines += "限制 Guard: 不切 provider、不呼叫 paid model、不改 env、不 reload。" if ($candidatePath) { - $lines += "Evidence: $candidatePath" + $lines += "證據 Evidence: $candidatePath" } } elseif ($EventType -match "^load_shed_") { $actionName = if ($Data -and $Data.PSObject.Properties["name"]) { $Data.name } else { "unknown" } @@ -224,19 +224,19 @@ function Format-AgentTelegramText { $postVerifyOk = if ($Data -and $Data.PSObject.Properties["postVerifyOk"]) { $Data.postVerifyOk } else { $null } $exitCode = if ($Data -and $Data.PSObject.Properties["exitCode"]) { $Data.exitCode } else { $null } $rollback = if ($Data -and $Data.PSObject.Properties["rollback"]) { $Data.rollback } else { $null } - $lines += "Event: controlled remediation" - $lines += "Host: $hostName" - $lines += "Agent99 action: $actionName" + $lines += "事件 Event: 受控修復 / controlled remediation" + $lines += "主機 Host: $hostName" + $lines += "Agent99 動作 Action: $actionName" if ($ok -ne $null) { - $lines += "Result: ok=$ok exitCode=$exitCode" + $lines += "結果 Result: ok=$ok exitCode=$exitCode" if ($executionOk -ne $null -or $postVerifyOk -ne $null) { - $lines += "Verifier: executionOk=$executionOk postVerifyOk=$postVerifyOk" + $lines += "驗證 Verifier: executionOk=$executionOk postVerifyOk=$postVerifyOk" } } else { - $lines += "Result: check Agent99 evidence." + $lines += "結果 Result: 請查看 Agent99 evidence。" } if ($rollback) { - $lines += "Rollback: $rollback" + $lines += "回滾 Rollback: $rollback" } } elseif ($EventType -match "^operator_command_") { $commandId = if ($Data -and $Data.PSObject.Properties["id"]) { $Data.id } else { "unknown" } @@ -245,56 +245,56 @@ function Format-AgentTelegramText { $instruction = if ($Data -and $Data.PSObject.Properties["instruction"]) { $Data.instruction } else { $null } $exitCode = if ($Data -and $Data.PSObject.Properties["exitCode"]) { $Data.exitCode } else { $null } $evidencePath = if ($Data -and $Data.PSObject.Properties["evidence"]) { $Data.evidence } else { $null } - $lines += "Event: Agent99 automation result" - $lines += "Mode: $modeName" - $lines += "Source: $source" - $lines += "Result: exitCode=$exitCode" + $lines += "事件 Event: Agent99 自動化結果 / automation result" + $lines += "模式 Mode: $modeName" + $lines += "來源 Source: $source" + $lines += "結果 Result: exitCode=$exitCode" if ($Data -and $Data.PSObject.Properties["problem"]) { $problem = $Data.problem if ($problem -and $problem.PSObject.Properties["occurrences"]) { - $lines += "Problem count: occurrences=$($problem.occurrences), resolved=$($problem.resolved), failed=$($problem.failed)" + $lines += "問題計數 Problem count: occurrences=$($problem.occurrences), resolved=$($problem.resolved), failed=$($problem.failed)" } } if ($instruction -and $exitCode -ne 0) { - $lines += "Failure instruction: $instruction" + $lines += "失敗指令 Failure instruction: $instruction" } if ($evidencePath) { - $lines += "Evidence: $evidencePath" + $lines += "證據 Evidence: $evidencePath" } } elseif ($EventType -eq "agent_control_tick") { $dashboardPath = if ($Data -and $Data.PSObject.Properties["dashboardPath"]) { $Data.dashboardPath } else { $null } $processed = if ($Data -and $Data.PSObject.Properties["processedCount"]) { $Data.processedCount } else { 0 } $pending = if ($Data -and $Data.PSObject.Properties["pendingCount"]) { $Data.pendingCount } else { 0 } - $lines += "Event: Agent99 control loop" - $lines += "Dashboard: $dashboardPath" - $lines += "Queue: processed=$processed pending=$pending" + $lines += "事件 Event: Agent99 控制迴圈 / control loop" + $lines += "儀表板 Dashboard: $dashboardPath" + $lines += "佇列 Queue: processed=$processed pending=$pending" } elseif ($EventType -match "^ai_service_") { $serviceName = if ($Data -and $Data.PSObject.Properties["name"]) { $Data.name } else { "unknown" } - $lines += "Event: AI service health" - $lines += "Service: $serviceName" - $lines += "Agent99 action: inspect health endpoint and service/container state before controlled repair." + $lines += "事件 Event: AI 服務健康 / AI service health" + $lines += "服務 Service: $serviceName" + $lines += "Agent99 動作 Action: 先檢查 health endpoint 與 service/container state,再做 controlled repair。" } elseif ($EventType -eq "security_triage") { $status = if ($Data -and $Data.PSObject.Properties["status"]) { $Data.status } else { "unknown" } $hitCount = if ($Data -and $Data.PSObject.Properties["hitCount"]) { $Data.hitCount } else { 0 } $evidencePath = if ($Data -and $Data.PSObject.Properties["evidencePath"]) { $Data.evidencePath } else { $null } - $lines += "Event: security triage" - $lines += "Status: $status" - $lines += "Evidence hits: $hitCount" - $lines += "Agent99 action: classify blast radius, affected host/service, source log reference, repeated count, and allowlisted next action." - $lines += "Guard: no arbitrary shell, no data deletion, no disabling controls, no secret exposure." + $lines += "事件 Event: 資安判讀 / security triage" + $lines += "狀態 Status: $status" + $lines += "證據命中 Evidence hits: $hitCount" + $lines += "Agent99 動作 Action: 分類 blast radius、affected host/service、source log reference、repeated count、allowlisted next action。" + $lines += "限制 Guard: 不執行 arbitrary shell、不刪資料、不停用 security controls、不暴露 secrets。" if ($evidencePath) { - $lines += "Evidence: $evidencePath" + $lines += "證據 Evidence: $evidencePath" } } elseif ($EventType -eq "agent_selfcheck") { - $lines += "Event: Agent99 self health" - $lines += "Summary: $Message" - $lines += "Agent99 action: inspect task state, evidence freshness, and Telegram delivery; reboot is not the first response." + $lines += "事件 Event: Agent99 自我健康 / self health" + $lines += "摘要 Summary: $Message" + $lines += "Agent99 動作 Action: 檢查 task state、evidence freshness、Telegram delivery;reboot 不是第一反應。" } else { - $lines += "Event: $EventType" - $lines += "Summary: $Message" + $lines += "事件 Event: $EventType" + $lines += "摘要 Summary: $Message" } - $lines += "Time: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss zzz')" + $lines += "時間 Time: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss zzz')" $text = ($lines -join "`n") if ($text.Length -gt 3500) { $text = $text.Substring(0, 3500) + "`n...(truncated)" @@ -356,26 +356,27 @@ function New-AgentTelegramCardImage { [System.Drawing.Color]::FromArgb(37, 99, 235) } $accentBrush = New-Object System.Drawing.SolidBrush -ArgumentList $accentColor - $titleFont = New-Object System.Drawing.Font -ArgumentList "Segoe UI", 25, ([System.Drawing.FontStyle]::Bold) - $labelFont = New-Object System.Drawing.Font -ArgumentList "Segoe UI", 13, ([System.Drawing.FontStyle]::Bold) - $bodyFont = New-Object System.Drawing.Font -ArgumentList "Segoe UI", 16, ([System.Drawing.FontStyle]::Regular) - $smallFont = New-Object System.Drawing.Font -ArgumentList "Segoe UI", 11, ([System.Drawing.FontStyle]::Regular) + $fontFamily = "Microsoft JhengHei UI" + $titleFont = New-Object System.Drawing.Font -ArgumentList $fontFamily, 25, ([System.Drawing.FontStyle]::Bold) + $labelFont = New-Object System.Drawing.Font -ArgumentList $fontFamily, 13, ([System.Drawing.FontStyle]::Bold) + $bodyFont = New-Object System.Drawing.Font -ArgumentList $fontFamily, 16, ([System.Drawing.FontStyle]::Regular) + $smallFont = New-Object System.Drawing.Font -ArgumentList $fontFamily, 11, ([System.Drawing.FontStyle]::Regular) $graphics.FillRectangle($bgBrush, 0, 0, $width, $height) $graphics.FillRectangle($panelBrush, 36, 34, $width - 72, $height - 68) $graphics.FillRectangle($accentBrush, 36, 34, 14, $height - 68) - $graphics.DrawString("Agent99 Incident Card", $titleFont, $textBrush, 72, 58) - $graphics.DrawString(("Severity: " + $Severity.ToUpperInvariant()), $labelFont, $accentBrush, 72, 106) - $graphics.DrawString(("Event: " + (Limit-AgentTextLine $EventType 76)), $labelFont, $mutedBrush, 72, 134) + $graphics.DrawString("Agent99 事件卡 / Incident Card", $titleFont, $textBrush, 72, 58) + $graphics.DrawString(("嚴重度 Severity: " + $Severity.ToUpperInvariant()), $labelFont, $accentBrush, 72, 106) + $graphics.DrawString(("事件 Event: " + (Limit-AgentTextLine $EventType 76)), $labelFont, $mutedBrush, 72, 134) - $bodyLines = @($Text -split "`n" | Where-Object { $_ -and $_ -notmatch "^Agent99 Incident Card$" } | Select-Object -First 12) + $bodyLines = @($Text -split "`n" | Where-Object { $_ -and $_ -notmatch "^Agent99 事件卡 / Incident Card$" } | Select-Object -First 12) $y = 184 foreach ($line in $bodyLines) { $graphics.DrawString((Limit-AgentTextLine $line 96), $bodyFont, $textBrush, 72, $y) $y += 32 if ($y -gt 545) { break } } - $graphics.DrawString(("Generated: " + (Get-Date -Format "yyyy-MM-dd HH:mm:ss zzz")), $smallFont, $mutedBrush, 72, 570) + $graphics.DrawString(("產生時間 Generated: " + (Get-Date -Format "yyyy-MM-dd HH:mm:ss zzz")), $smallFont, $mutedBrush, 72, 570) $bitmap.Save($imagePath, [System.Drawing.Imaging.ImageFormat]::Png) $graphics.Dispose() @@ -447,7 +448,7 @@ function Invoke-AgentTelegramCardSelfTest { visualPath = $imagePath visualExists = $ok sentTelegram = $false - messageFormat = "incident_card_v2" + messageFormat = "incident_card_zh_tw_v3" } $selfTestPath = Join-Path $EvidenceDir "agent99-TelegramCardSelfTest-$stamp.json" $result | ConvertTo-Json -Depth 6 | Set-Content -Path $selfTestPath -Encoding UTF8 @@ -499,7 +500,7 @@ function Send-AgentTelegram { enabled = [bool]$enabled eventType = $EventType severity = $Severity - messageFormat = "incident_card_v2" + messageFormat = "incident_card_zh_tw_v3" configured = [bool]($token -and $chatId) tokenEnv = if ($tokenSecret) { $tokenSecret.name } else { $null } chatIdEnv = if ($chatSecret) { $chatSecret.name } else { $null }