feat(agent99): report sre alert completion cards

This commit is contained in:
ogt
2026-07-10 12:45:26 +08:00
parent cf5370b49e
commit a8080e13b9
5 changed files with 119 additions and 12 deletions

View File

@@ -463,6 +463,7 @@ Recovery is complete only when all of the following are captured:
- P0-12: slow SSH readback after reboot must not be collapsed into "host down". Performance and backup metadata checks must record `elapsedMs`, use configurable timeouts, and run a slow retry on timeout before raising `perf_readback_failed` or backup readback failure.
- P0-13: first-run evidence refresh after a noise freeze must use controlled `-SuppressAlerts` for Perf/Backup/SelfCheck so stale evidence is refreshed without Telegram noise; schedules are then re-enabled without suppression.
- P0-14: synthetic or `TEST_DO_NOT_PAGE` monitoring tests must still run the full Telegram -> SRE inbox -> ControlTick -> mode evidence path, but `suppressTelegram=true` and `SuppressReason=do_not_page` must propagate end to end so validation never pages the SRE Telegram group.
- P0-15: real SRE alert completion must send a human-readable Traditional Chinese automation result card back to Telegram with action taken, result, evidence, problem count, and KM path. Ordinary successful operator commands remain suppressed unless explicitly configured.
- 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`.
@@ -605,6 +606,23 @@ Recovery is complete only when all of the following are captured:
- ProviderFreshness evidence: `C:\Wooo\Agent99\evidence\agent99-ProviderFreshness-20260710-123128.json`, `status=candidate_created_waiting_direct_readback`, candidate `provider-freshness-20260710-123129`.
- ProviderFreshness Telegram evidence: both `agent_start` and `provider_freshness_triage` have `sent=False`, `suppressed=True`, `reason=do_not_page`.
- Problem/KM writeback: `problemKey=agent99-sre-alert-inbox_unknown_provider_freshness_signal`, `occurrences=6`, `resolved=5`, `failed=1`, `lastStatus=resolved`, KM path `C:\Wooo\Agent99\playbooks\agent99-incident-km.md`.
- 2026-07-10 12:42 SRE completion card v4 verifier:
- Runtime script deployed: `C:\Wooo\Agent99\bin\agent99-control-plane.ps1`.
- Backups before deploy: `C:\Wooo\Agent99\bin\agent99-control-plane.ps1.bak-20260710-124012` and `C:\Wooo\Agent99\bin\agent99-control-plane.ps1.bak-20260710-124137`.
- Runtime bootstrap deployed: `C:\Wooo\Agent99\bin\agent99-bootstrap.ps1`, backup `C:\Wooo\Agent99\bin\agent99-bootstrap.ps1.bak-20260710-124425`.
- Runtime config readback: `C:\Wooo\Agent99\config\agent99.config.json` has `telegram.alertSreCommandResults=True`; config backup `C:\Wooo\Agent99\config\agent99.config.json.bak-20260710-124351`.
- New behavior: real `source=agent99-sre-alert-inbox` command results with `alertId` are allowed to send completion cards even when ordinary successful operator commands are suppressed.
- Guard: `suppressTelegram=true` or `TEST_DO_NOT_PAGE` still suppresses the entire path with `reason=do_not_page`.
- Telegram card self-test evidence: `C:\Wooo\Agent99\evidence\agent99-TelegramCardSelfTest-20260710-124202.json`.
- Self-test result: `ok=True`, `messageFormat=incident_card_zh_tw_v4`, `operatorTextOk=True`, `operatorDecisionOk=True`, `operatorVisualExists=True`, `sentTelegram=False`.
- Operator completion card image: `C:\Wooo\Agent99\evidence\telegram-cards\agent99-card-operator_command_result-20260710-124202.png`.
- Do-not-page E2E alert id: `telegram-monitoring-1783687343`.
- Do-not-page E2E evidence:
- Telegram inbox: `C:\Wooo\Agent99\evidence\agent99-TelegramInbox-20260710-124224.json`, `alertedCount=1`, `suppressTelegram=True`.
- SRE inbox: `C:\Wooo\Agent99\evidence\agent99-SreAlertInbox-20260710-124224.json`, `queuedCount=1`, `controlExitCode=0`.
- ControlTick: `C:\Wooo\Agent99\evidence\agent99-ControlTick-20260710-124225.json`, `processedCount=1`, `pendingCount=0`, operator result `sent=False`, `suppressed=True`, `reason=do_not_page`.
- ProviderFreshness: `C:\Wooo\Agent99\evidence\agent99-ProviderFreshness-20260710-124226.json`, `status=direct_readback_ok_no_source_events`, candidate `provider-freshness-20260710-124226`.
- Problem/KM writeback after v4 E2E: `problemKey=agent99-sre-alert-inbox_unknown_provider_freshness_signal`, `occurrences=7`, `resolved=6`, `failed=1`, `lastStatus=resolved`.
## Agent99 Monitoring Alert Routing Self-Test Receipt

View File

@@ -73,6 +73,7 @@ if (-not (Test-Path $configPath)) {
"alertAllOperations": false,
"alertInfoEvents": false,
"alertOperatorCommands": true,
"alertSreCommandResults": true,
"dedupeMinutes": 30
},
"selfHealth": {

View File

@@ -350,22 +350,46 @@ 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 }
$resultLabel = if ($exitCode -eq 0 -or [string](Get-AgentObjectValue $Data "ok" "") -match "^(?i:true|1)$") { "已完成 / resolved" } else { "仍需處理 / failed" }
$actionText = switch ($modeName) {
"ProviderFreshness" { "建立 provider freshness candidate要求 provider window、last seen、direct reference、verifier readback不切 provider。" }
"BackupCheck" { "讀回備份 freshness、status/log、snapshot metadata、cron contract不讀備份內容或 secrets。" }
"Perf" { "讀回主機 CPU/記憶體/磁碟;必要時只做 allowlisted 降載。" }
"LoadShed" { "執行 allowlisted 降載並做 post-apply verifier。" }
"Recover" { "依序檢查 VM、SSH、Harbor/K3s/public route必要時執行受控恢復。" }
"StartVMs" { "檢查並啟動 allowlisted VMware VM。" }
"HarborRepair" { "檢查 Harbor core/redis/registry必要時執行受控修復。" }
"AwoooRepair" { "檢查 AWOOOI/K3s pod、deployment、image pull 狀態,必要時受控修復。" }
"SecurityTriage" { "建立資安 triage candidate分類 blast radius、affected host/service、source log reference不任意 shell、不暴露 secrets。" }
default { "執行 allowlisted Agent99 mode 並寫入 evidence/KM。" }
}
$lines += "事件 Event: Agent99 自動化結果 / automation result"
$lines += "模式 Mode: $modeName"
$lines += "來源 Source: $source"
$lines += "結果 Result: exitCode=$exitCode"
$lines += "結果 Result: $resultLabel, exitCode=$exitCode"
$lines += "Agent99 動作 Action: $actionText"
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)"
if ($problem.PSObject.Properties["kmPath"]) {
$lines += "KM: $($problem.kmPath)"
}
}
}
if ($instruction -and $exitCode -ne 0) {
$lines += "失敗指令 Failure instruction: $instruction"
} elseif ($instruction) {
$lines += "處理摘要 Summary: $(Limit-AgentTextLine $instruction 130)"
}
if ($evidencePath) {
$lines += "證據 Evidence: $evidencePath"
}
if ($exitCode -eq 0) {
$lines += "下一步 Next: 若相同問題再發生,依 problem count 追蹤重複率並回寫 PlayBook/KM。"
} else {
$lines += "下一步 Next: 保留 failed queue/evidence升級到對應 playbook不做破壞性動作。"
}
} 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 }
@@ -428,11 +452,24 @@ function Get-AgentTelegramVisualPath {
$null
}
function Test-AgentSreOperatorResultAlertEnabled {
param([object]$Data)
if (-not $Data) { return $false }
$source = [string](Get-AgentObjectValue $Data "source" "")
$alertId = [string](Get-AgentObjectValue $Data "alertId" "")
$enabled = $true
if ($Config.telegram -and $Config.telegram.PSObject.Properties["alertSreCommandResults"]) {
$enabled = Convert-AgentBool $Config.telegram.alertSreCommandResults
}
[bool]($enabled -and $source -eq "agent99-sre-alert-inbox" -and $alertId)
}
function New-AgentTelegramCardImage {
param(
[string]$Severity,
[string]$EventType,
[string]$Text
[string]$Text,
[object]$Data = $null
)
$visualCardsEnabled = $true
@@ -440,7 +477,8 @@ function New-AgentTelegramCardImage {
$visualCardsEnabled = [bool]$Config.telegram.visualCardsEnabled
}
if (-not $visualCardsEnabled) { return $null }
if ($Severity -notin @("warning", "critical")) { return $null }
$forceVisual = [bool]($EventType -match "^operator_command_" -and (Test-AgentSreOperatorResultAlertEnabled $Data))
if (($Severity -notin @("warning", "critical")) -and -not $forceVisual) { return $null }
$cardDir = Join-Path $EvidenceDir "telegram-cards"
New-Item -ItemType Directory -Force -Path $cardDir | Out-Null
@@ -553,14 +591,51 @@ function Invoke-AgentTelegramCardSelfTest {
$text = Format-AgentTelegramText "warning" "performance_warning" "selftest visual card" $sample
$imagePath = New-AgentTelegramCardImage "warning" "performance_warning" $text
$ok = [bool]($imagePath -and (Test-Path -LiteralPath $imagePath))
$operatorSample = [pscustomobject]@{
id = "selftest-sre-result"
mode = "ProviderFreshness"
source = "agent99-sre-alert-inbox"
reason = "sre_alert"
instruction = "Provider freshness candidate required; alertId=selftest-provider; require providerWindow,lastSeen,directReference,verifierReadback; do not switch provider."
alertId = "selftest-provider"
alertKind = "provider_freshness_signal"
alertSeverity = "warning"
alertService = "provider_freshness_signal"
ok = $true
exitCode = 0
evidence = $jsonPath
problem = [pscustomobject]@{
key = "selftest-provider-freshness"
occurrences = 2
resolved = 2
failed = 0
kmPath = (Join-Path $Config.agentRoot "playbooks\agent99-incident-km.md")
}
}
$operatorText = Format-AgentTelegramText "info" "operator_command_result" "selftest SRE completion card" $operatorSample
$operatorImagePath = New-AgentTelegramCardImage "info" "operator_command_result" $operatorText $operatorSample
$operatorDecisionOk = Test-AgentSreOperatorResultAlertEnabled $operatorSample
$operatorTextOk = [bool](
$operatorText -match "已完成 / resolved" -and
$operatorText -match "Agent99 動作 Action" -and
$operatorText -match "問題計數 Problem count" -and
$operatorText -match "KM:"
)
$operatorVisualOk = [bool]($operatorImagePath -and (Test-Path -LiteralPath $operatorImagePath))
$ok = [bool]($ok -and $operatorTextOk -and $operatorVisualOk -and $operatorDecisionOk)
$result = [pscustomobject]@{
timestamp = (Get-Date -Format o)
ok = $ok
textPreview = @($text -split "`n" | Select-Object -First 12)
visualPath = $imagePath
visualExists = $ok
operatorTextPreview = @($operatorText -split "`n" | Select-Object -First 14)
operatorVisualPath = $operatorImagePath
operatorVisualExists = $operatorVisualOk
operatorTextOk = $operatorTextOk
operatorDecisionOk = $operatorDecisionOk
sentTelegram = $false
messageFormat = "incident_card_zh_tw_v3"
messageFormat = "incident_card_zh_tw_v4"
}
$selfTestPath = Join-Path $EvidenceDir "agent99-TelegramCardSelfTest-$stamp.json"
$result | ConvertTo-Json -Depth 6 | Set-Content -Path $selfTestPath -Encoding UTF8
@@ -584,7 +659,7 @@ function Invoke-AgentTelegramDeliverySelfTest {
ok = $sent
testId = $testId
sentTelegram = $sent
messageFormat = "incident_card_zh_tw_v3"
messageFormat = "incident_card_zh_tw_v4"
attempts = $script:TelegramAttempts
}
$selfTestPath = Join-Path $EvidenceDir "agent99-TelegramDeliverySelfTest-$stamp.json"
@@ -739,12 +814,12 @@ function Send-AgentTelegram {
$attempt.error = "telegram_disabled"
$script:TelegramAttempts += $attempt
return
}
$text = Format-AgentTelegramText $Severity $EventType $Message $Data
$visualPath = Get-AgentTelegramVisualPath $Data
if (-not $visualPath) {
$visualPath = New-AgentTelegramCardImage $Severity $EventType $text
}
}
$text = Format-AgentTelegramText $Severity $EventType $Message $Data
$visualPath = Get-AgentTelegramVisualPath $Data
if (-not $visualPath) {
$visualPath = New-AgentTelegramCardImage $Severity $EventType $text $Data
}
$attempt.visualPath = $visualPath
if (-not ($token -and ($chatId -or $chatIdOverride))) {
$relayResult = Send-AgentTelegramRelay $text $telegram.relay $chatIdOverride $visualPath
@@ -859,20 +934,28 @@ function Record-AgentEvent {
$operatorOk = $false
$operatorMode = ""
$operatorId = ""
$operatorSource = ""
$operatorAlertId = ""
$operatorOkValue = $null
$operatorExitCode = $null
if ($Data -is [System.Collections.IDictionary]) {
if ($Data.Contains("mode")) { $operatorMode = [string]$Data["mode"] }
if ($Data.Contains("id")) { $operatorId = [string]$Data["id"] }
if ($Data.Contains("source")) { $operatorSource = [string]$Data["source"] }
if ($Data.Contains("alertId")) { $operatorAlertId = [string]$Data["alertId"] }
if ($Data.Contains("ok")) { $operatorOkValue = $Data["ok"] }
if ($Data.Contains("exitCode")) { $operatorExitCode = $Data["exitCode"] }
} else {
$modeProp = $Data.PSObject.Properties.Match("mode") | Select-Object -First 1
$idProp = $Data.PSObject.Properties.Match("id") | Select-Object -First 1
$sourceProp = $Data.PSObject.Properties.Match("source") | Select-Object -First 1
$alertIdProp = $Data.PSObject.Properties.Match("alertId") | Select-Object -First 1
$okProp = $Data.PSObject.Properties.Match("ok") | Select-Object -First 1
$exitCodeProp = $Data.PSObject.Properties.Match("exitCode") | Select-Object -First 1
if ($modeProp) { $operatorMode = [string]$modeProp.Value }
if ($idProp) { $operatorId = [string]$idProp.Value }
if ($sourceProp) { $operatorSource = [string]$sourceProp.Value }
if ($alertIdProp) { $operatorAlertId = [string]$alertIdProp.Value }
if ($okProp) { $operatorOkValue = $okProp.Value }
if ($exitCodeProp) { $operatorExitCode = $exitCodeProp.Value }
}
@@ -883,9 +966,12 @@ function Record-AgentEvent {
if ($Config.telegram -and $Config.telegram.PSObject.Properties["alertSuccessfulOperatorCommands"]) {
$alertSuccessfulOperatorCommands = [bool]$Config.telegram.alertSuccessfulOperatorCommands
}
if ($operatorOk -and -not $alertSuccessfulOperatorCommands) {
$operatorIsSreAlertResult = Test-AgentSreOperatorResultAlertEnabled $Data
if ($operatorOk -and -not $alertSuccessfulOperatorCommands -and -not $operatorIsSreAlertResult) {
Write-AgentLog "telegram_operator_success_suppressed type=$EventType mode=$operatorMode id=$operatorId"
$operatorCommandAlert = $false
} elseif ($operatorOk -and $operatorIsSreAlertResult) {
Write-AgentLog "telegram_sre_operator_success_enabled type=$EventType mode=$operatorMode id=$operatorId alertId=$operatorAlertId"
}
}
$shouldAlert = ($Severity -in @("warning", "critical")) -or $operatorCommandAlert -or ($Alert -and $Severity -ne "info") -or ($alertAll -and ($Severity -ne "info" -or $alertInfo))

View File

@@ -120,6 +120,7 @@
"alertAllOperations": false,
"alertInfoEvents": false,
"alertOperatorCommands": true,
"alertSreCommandResults": true,
"dedupeMinutes": 30
},
"performance": {

View File

@@ -107,6 +107,7 @@
"alertAllOperations": false,
"alertInfoEvents": false,
"alertOperatorCommands": true,
"alertSreCommandResults": true,
"dedupeMinutes": 30
},
"performance": {