用户没有企业微信,改用 Telegram Bot。同一台机器的 api.telegram.org 需要走本地代理 (实测 127.0.0.1:7897),所以发送逻辑要显式支持代理。 - notify.json 新增 telegram 段:token / chatId / proxy (也认环境变量 PI_NOTIFY_TELEGRAM_TOKEN、PI_NOTIFY_TELEGRAM_CHAT) - 未配置 proxy 时回落 HTTPS_PROXY 环境变量,再回落系统(IE)代理 - 文本超过 3900 字符截断(Telegram 上限 4096) - 自检 -Probe 同时报告 webhook 与 telegram 两个通道的结果,token 只显示前 10 位 - SKILL.md 更新通道配置说明,补上 notify.json 的完整示例与代理注意事项 实测:假 token 时返回 "(401) 未经授权",说明请求确实穿过了代理到达 api.telegram.org, 只是凭据无效;未配置时正确报告 not configured。真实推送待用户提供 bot token。
226 lines
8.5 KiB
PowerShell
226 lines
8.5 KiB
PowerShell
# notify_decision.ps1 - ping the user when the agent needs a decision.
|
|
#
|
|
# Usage:
|
|
# powershell -NoProfile -ExecutionPolicy Bypass -File tools\notify_decision.ps1 `
|
|
# -Title "Decision needed" -MessageFile C:\temp\q.txt [-Url <webhook>] [-NoToast]
|
|
#
|
|
# - Chinese text must go through -MessageFile (UTF-8) to avoid console codepage damage;
|
|
# -Message is fine for ASCII.
|
|
# - Every call appends to %USERPROFILE%\.pi\agent\decisions\pending.md so the question
|
|
# is not lost if the toast is missed.
|
|
# - Optional webhook: if -Url (or $env:PI_NOTIFY_WEBHOOK) is set, POST a text message.
|
|
# WeCom / DingTalk style payload is the default; Feishu/Lark URLs get their own shape.
|
|
# - Always exits 0: a failed notification must never break the agent turn.
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[string]$Title = 'Agent needs a decision',
|
|
[string]$Message = '',
|
|
[string]$MessageFile = '',
|
|
[string]$Url = '',
|
|
[switch]$NoToast,
|
|
[switch]$NoBeep,
|
|
[switch]$Probe
|
|
)
|
|
|
|
$ErrorActionPreference = 'Continue'
|
|
|
|
function Write-Pending {
|
|
param([string]$text)
|
|
try {
|
|
$dir = Join-Path $env:USERPROFILE '.pi\agent\decisions'
|
|
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
|
|
$log = Join-Path $dir 'pending.md'
|
|
$stamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
|
$cwd = (Get-Location).Path
|
|
$entry = "## $stamp`r`n`r`n- cwd: $cwd`r`n- title: $Title`r`n`r`n$text`r`n`r`n---`r`n"
|
|
[System.IO.File]::AppendAllText($log, $entry, (New-Object System.Text.UTF8Encoding $false))
|
|
} catch { }
|
|
}
|
|
|
|
function Send-Toast {
|
|
param([string]$t, [string]$m)
|
|
try {
|
|
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType=WindowsRuntime] | Out-Null
|
|
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType=WindowsRuntime] | Out-Null
|
|
|
|
$esc = { param($s) ($s -replace '&', '&' -replace '<', '<' -replace '>', '>') }
|
|
$body = (& $esc $m) -replace "`r?`n", '<text>'
|
|
# split the message into lines -> one <text> per line
|
|
$lines = $m -split "`r?`n" | ForEach-Object { '<text>' + (& $esc $_) + '</text>' }
|
|
$xmlText = @"
|
|
<toast scenario="reminder">
|
|
<visual>
|
|
<binding template="ToastGeneric">
|
|
<text>$(& $esc $t)</text>
|
|
$($lines -join "`r`n ")
|
|
</binding>
|
|
</visual>
|
|
<audio src="ms-winsoundevent:Notification.Reminder"/>
|
|
</toast>
|
|
"@
|
|
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
|
|
$xml.LoadXml($xmlText)
|
|
$toast = New-Object Windows.UI.Notifications.ToastNotification $xml
|
|
$appId = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe'
|
|
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($appId).Show($toast)
|
|
return $true
|
|
} catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Send-Popup {
|
|
# Toast can silently fail (session locked, notifications off, appId not registered).
|
|
# A popup is intrusive but guaranteed visible; it auto-closes after 120 s.
|
|
param([string]$t, [string]$m)
|
|
try {
|
|
$w = New-Object -ComObject Wscript.Shell
|
|
$w.Popup("$t`r`n`r`n$m", 120, 'pi agent', 64) | Out-Null
|
|
return $true
|
|
} catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
|
|
function Mask-Url {
|
|
param([string]$u)
|
|
if (-not $u) { return '(none)' }
|
|
if ($u.Length -le 48) { return $u }
|
|
return $u.Substring(0, 32) + '...' + $u.Substring($u.Length - 8)
|
|
}
|
|
|
|
function Mask-Token {
|
|
param([string]$t)
|
|
if (-not $t) { return '(none)' }
|
|
if ($t.Length -le 12) { return '***' }
|
|
return $t.Substring(0, 10) + '***'
|
|
}
|
|
|
|
function Get-NotifyConfig {
|
|
# ~/.pi/agent/notify.json (outside the repo on purpose: tokens are secrets)
|
|
# {
|
|
# "webhook": "https://qyapi.weixin.qq.com/...", // WeCom / DingTalk / Feishu
|
|
# "telegram": { "token": "123456:ABC...", "chatId": "123456789",
|
|
# "proxy": "http://127.0.0.1:7897" }
|
|
# }
|
|
$cfg = @{ webhook = ''; tgToken = ''; tgChat = ''; proxy = '' }
|
|
$path = Join-Path $env:USERPROFILE '.pi\agent\notify.json'
|
|
if (Test-Path $path) {
|
|
try {
|
|
$o = [System.IO.File]::ReadAllText($path, [System.Text.Encoding]::UTF8) | ConvertFrom-Json
|
|
if ($o.webhook) { $cfg.webhook = [string]$o.webhook }
|
|
if ($o.proxy) { $cfg.proxy = [string]$o.proxy }
|
|
if ($o.telegram) {
|
|
if ($o.telegram.token) { $cfg.tgToken = [string]$o.telegram.token }
|
|
if ($o.telegram.chatId) { $cfg.tgChat = [string]$o.telegram.chatId }
|
|
if ($o.telegram.proxy) { $cfg.proxy = [string]$o.telegram.proxy }
|
|
}
|
|
} catch { }
|
|
}
|
|
$txtHook = Join-Path $env:USERPROFILE '.pi\agent\notify_webhook.txt'
|
|
if (-not $cfg.webhook -and (Test-Path $txtHook)) {
|
|
$v = ([System.IO.File]::ReadAllText($txtHook, [System.Text.Encoding]::UTF8)).Trim()
|
|
if ($v) { $cfg.webhook = $v }
|
|
}
|
|
return $cfg
|
|
}
|
|
|
|
function Get-ProxyArg {
|
|
param([string]$p)
|
|
if (-not $p) { $p = $env:HTTPS_PROXY }
|
|
if (-not $p) { $p = $env:https_proxy }
|
|
return $p
|
|
}
|
|
|
|
function Send-Telegram {
|
|
# Telegram Bot API. In mainland China this needs the local proxy; Invoke-RestMethod
|
|
# otherwise falls back to the system (IE) proxy setting.
|
|
param([string]$token, [string]$chatId, [string]$t, [string]$m, [string]$proxy)
|
|
try {
|
|
if (-not $token -or -not $chatId) { return 'not configured' }
|
|
$txt = "$t`n$m"
|
|
if ($txt.Length -gt 3900) { $txt = $txt.Substring(0, 3900) + ' ...' }
|
|
$body = @{ chat_id = $chatId; text = $txt; disable_web_page_preview = $true } |
|
|
ConvertTo-Json -Compress
|
|
$bytes = [System.Text.Encoding]::UTF8.GetBytes($body)
|
|
$uri = "https://api.telegram.org/bot$token/sendMessage"
|
|
$args = @{
|
|
Uri = $uri; Method = 'Post'
|
|
ContentType = 'application/json; charset=utf-8'
|
|
Body = $bytes; TimeoutSec = 15
|
|
}
|
|
$px = Get-ProxyArg $proxy
|
|
if ($px) { $args['Proxy'] = $px }
|
|
$resp = Invoke-RestMethod @args
|
|
if ($resp.ok) { return 'ok' }
|
|
return "not ok: $($resp.description)"
|
|
} catch {
|
|
return "http: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
|
|
function Post-Webhook {
|
|
param([string]$u, [string]$t, [string]$m)
|
|
try {
|
|
$text = "$t`n$m"
|
|
if ($u -match 'feishu|larksuite|lark') {
|
|
$payload = @{ msg_type = 'text'; content = @{ text = $text } } | ConvertTo-Json -Depth 5 -Compress
|
|
} else {
|
|
# WeCom / DingTalk robot
|
|
$payload = @{ msgtype = 'text'; text = @{ content = $text } } | ConvertTo-Json -Depth 5 -Compress
|
|
}
|
|
$bytes = [System.Text.Encoding]::UTF8.GetBytes($payload)
|
|
$resp = Invoke-RestMethod -Uri $u -Method Post -ContentType 'application/json; charset=utf-8' -Body $bytes -TimeoutSec 10
|
|
# WeCom / DingTalk answer 200 with errcode; Feishu answers with code. 200 does NOT mean delivered.
|
|
if ($null -ne $resp.errcode -and $resp.errcode -ne 0) { return "errcode=$($resp.errcode) $($resp.errmsg)" }
|
|
if ($null -ne $resp.code -and $resp.code -ne 0) { return "code=$($resp.code) $($resp.msg)" }
|
|
return 'ok'
|
|
} catch {
|
|
return "http: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
# ---- resolve message -------------------------------------------------------
|
|
$text = $Message
|
|
if ($MessageFile -and (Test-Path $MessageFile)) {
|
|
$text = [System.IO.File]::ReadAllText($MessageFile, [System.Text.Encoding]::UTF8)
|
|
}
|
|
if (-not $text) { $text = '(empty message)' }
|
|
|
|
Write-Pending -text $text
|
|
|
|
if (-not $NoToast) {
|
|
$shown = Send-Toast -t $Title -m $text
|
|
if (-not $shown) { Send-Popup -t $Title -m $text | Out-Null }
|
|
}
|
|
if (-not $NoBeep) { try { [console]::Beep(880, 180); [console]::Beep(1175, 220) } catch { } }
|
|
|
|
$cfg = Get-NotifyConfig
|
|
$hook = $Url
|
|
if (-not $hook) { $hook = $env:PI_NOTIFY_WEBHOOK }
|
|
if (-not $hook) { $hook = $cfg.webhook }
|
|
|
|
$tgToken = $env:PI_NOTIFY_TELEGRAM_TOKEN
|
|
if (-not $tgToken) { $tgToken = $cfg.tgToken }
|
|
$tgChat = $env:PI_NOTIFY_TELEGRAM_CHAT
|
|
if (-not $tgChat) { $tgChat = $cfg.tgChat }
|
|
|
|
if ($hook) {
|
|
$st = Post-Webhook -u $hook -t $Title -m $text
|
|
if ($Probe) { Write-Output ("webhook -> " + (Mask-Url $hook) + " result: " + $st) }
|
|
} elseif ($Probe) {
|
|
Write-Output 'webhook not configured'
|
|
}
|
|
|
|
if ($tgToken -and $tgChat) {
|
|
$st = Send-Telegram -token $tgToken -chatId $tgChat -t $Title -m $text -proxy $cfg.proxy
|
|
if ($Probe) { Write-Output ("telegram -> token " + (Mask-Token $tgToken) + " chat " + $tgChat + " result: " + $st) }
|
|
} elseif ($Probe) {
|
|
Write-Output 'telegram not configured'
|
|
}
|
|
|
|
exit 0
|