fix(agent99): add bilingual telegram incident cards

This commit is contained in:
ogt
2026-07-10 11:09:56 +08:00
parent e2063ad84d
commit 4e817dc21f
2 changed files with 89 additions and 78 deletions

View File

@@ -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

View File

@@ -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 loadmemory、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 thresholdcritical 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 cleanupreboot 不是第一反應。"
} 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/logssnapshot 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 radiusaffected host/servicesource log referencerepeated countallowlisted 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 stateevidence freshnessTelegram deliveryreboot 不是第一反應。"
} 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 }