From 6b45e3ba73f9a2371cac1ae9e061a7f2c7589db1 Mon Sep 17 00:00:00 2001 From: "Ariznawl@163.com" Date: Tue, 12 May 2026 16:57:48 +0800 Subject: [PATCH 1/2] feat(gateway): webhook delivery for WeCom cron scheduled messages WeCom AI Bot can only respond to messages in group chats (requires reply_token from an inbound @mention). Cron-triggered tasks have no reply_token, so their results are silently dropped in groups. Add optional `webhook_url` to WeComConfig. When set, the cron scheduler sends results to the group via the WeCom webhook bot API instead of the AI Bot respond path. This enables proactive scheduled reports (nightly regression summaries, alerts) in group chats. Also fixes pre-existing rustfmt diffs in commands.rs. Co-Authored-By: Claude Opus 4.6 --- crates/astra-gateway/src/commands.rs | 15 ++++------ crates/astra-gateway/src/config.rs | 5 ++++ crates/astra-gateway/src/scheduler.rs | 43 ++++++++++++++++++++++++++- 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/crates/astra-gateway/src/commands.rs b/crates/astra-gateway/src/commands.rs index 0516a9f..922e57c 100644 --- a/crates/astra-gateway/src/commands.rs +++ b/crates/astra-gateway/src/commands.rs @@ -404,10 +404,7 @@ pub async fn handle_command(ctx: &CommandContext<'_>, text: &str) -> Option HarnessSnapshot { @@ -2473,10 +2469,7 @@ mod tests { ); // Numeric index: 1=้ป˜่ฎค, 2=Sonnet, 3=Haiku, 4=Opus 4.7, 5=Opus 4.6 assert!(matches!(resolve_model_input("1"), ResolvedModel::Default)); - assert_eq!( - id(resolve_model_input("4")), - "us.anthropic.claude-opus-4-7" - ); + assert_eq!(id(resolve_model_input("4")), "us.anthropic.claude-opus-4-7"); // Default assert!(matches!( resolve_model_input("้ป˜่ฎค"), @@ -2516,7 +2509,9 @@ mod tests { ); // Extended whitelist: older Bedrock ids not in the menu still accepted assert_eq!( - id(resolve_model_input("us.anthropic.claude-opus-4-5-20251101-v1:0")), + id(resolve_model_input( + "us.anthropic.claude-opus-4-5-20251101-v1:0" + )), "us.anthropic.claude-opus-4-5-20251101-v1:0" ); // Anything else rejected (no passthrough) diff --git a/crates/astra-gateway/src/config.rs b/crates/astra-gateway/src/config.rs index 0476fe6..35ea822 100644 --- a/crates/astra-gateway/src/config.rs +++ b/crates/astra-gateway/src/config.rs @@ -143,6 +143,11 @@ pub struct WeComConfig { pub secret: String, #[serde(default = "default_wecom_ws_url")] pub websocket_url: String, + /// Webhook URL for proactive group messages (cron results, alerts). + /// WeCom AI Bot cannot send proactive messages to groups โ€” only respond. + /// A separate webhook bot in the same group enables scheduled reports. + #[serde(default)] + pub webhook_url: Option, } impl std::fmt::Debug for WeComConfig { diff --git a/crates/astra-gateway/src/scheduler.rs b/crates/astra-gateway/src/scheduler.rs index 27676c3..07151a8 100644 --- a/crates/astra-gateway/src/scheduler.rs +++ b/crates/astra-gateway/src/scheduler.rs @@ -12,6 +12,7 @@ use std::time::Duration; use tokio::sync::mpsc; const POLL_INTERVAL: Duration = Duration::from_secs(60); +const WEBHOOK_MAX_LEN: usize = 4000; #[cfg(test)] const CLI_TIMEOUT: Duration = Duration::from_secs(300); @@ -168,7 +169,20 @@ impl CronScheduler { let prefix = format!("โฐ **ๅฎšๆ—ถไปปๅŠก `{}`**\n\n", &job_id[..8.min(job_id.len())]); let body = format!("{prefix}{response}"); - if let Some(writer) = trace.as_ref() { + + // WeCom groups cannot receive proactive messages via AI Bot โ€” use webhook if configured. + if platform == "wecom" + && let Some(ref webhook_url) = self + .config + .platforms + .wecom + .as_ref() + .and_then(|w| w.webhook_url.clone()) + { + if let Err(e) = send_webhook(webhook_url, &body).await { + tracing::warn!(error = %e, "scheduler: webhook send failed"); + } + } else if let Some(writer) = trace.as_ref() { match writer.enqueue_outbox(platform, chat_id, None, &body).await { Ok(outbox_id) => { if let Err(e) = self @@ -306,6 +320,33 @@ impl CronScheduler { } } +/// Send a message to a WeCom group via webhook bot. +async fn send_webhook(url: &str, text: &str) -> Result<(), String> { + let truncated = if text.len() > WEBHOOK_MAX_LEN { + format!("{}โ€ฆ\n\n_(truncated)_", &text[..WEBHOOK_MAX_LEN]) + } else { + text.to_string() + }; + let payload = serde_json::json!({ + "msgtype": "markdown", + "markdown": { "content": truncated } + }); + let client = reqwest::Client::new(); + let resp = client + .post(url) + .json(&payload) + .send() + .await + .map_err(|e| e.to_string())?; + if !resp.status().is_success() { + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + return Err(format!("webhook returned {status}: {body}")); + } + tracing::info!("cron result sent via webhook"); + Ok(()) +} + #[cfg(test)] mod tests { use super::*; From 1930bb992c66fd91801add11fb458b76e59dc0cf Mon Sep 17 00:00:00 2001 From: "Ariznawl@163.com" Date: Tue, 12 May 2026 17:10:29 +0800 Subject: [PATCH 2/2] docs(gateway): add WeCom webhook cron delivery guide Co-Authored-By: Claude Opus 4.6 --- .../astra-gateway/docs/wecom-webhook-cron.md | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 crates/astra-gateway/docs/wecom-webhook-cron.md diff --git a/crates/astra-gateway/docs/wecom-webhook-cron.md b/crates/astra-gateway/docs/wecom-webhook-cron.md new file mode 100644 index 0000000..222cdb6 --- /dev/null +++ b/crates/astra-gateway/docs/wecom-webhook-cron.md @@ -0,0 +1,93 @@ +# WeCom Webhook for Cron Scheduled Messages + +## Background + +WeCom AI Bot (`aibot_respond_msg`) can only **respond** to messages in group chats โ€” it requires a `reply_token` from an inbound @mention. Cron-triggered tasks have no `reply_token`, so their results cannot be delivered to groups via the AI Bot respond path. + +By configuring `webhook_url`, cron task results are delivered to the group via a WeCom Webhook Bot instead. + +## Setup + +### 1. Create a Webhook Bot in Your WeCom Group + +1. Open the target group chat โ†’ Group Settings (top-right corner) +2. Group Bots โ†’ Add Bot +3. Enter a name (e.g. "Nightly Report") +4. Copy the Webhook URL after creation + +### 2. Add webhook_url to gateway.yaml + +```yaml +platforms: + wecom: + enabled: true + bot_id: "your-bot-id" + secret: "your-secret" + webhook_url: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY" +``` + +`webhook_url` is optional. When not configured, cron results fall back to `aibot_send_msg`. + +### 3. Create a Cron Job + +@mention the AI Bot in the group to create a scheduled task: + +> @BisectBot add a cron job: every day at 9am, analyze nightly regression results and generate a report + +The gateway will parse this and create a cron job. When triggered, the result is posted to the group via the webhook bot. + +## How It Works + +``` +Cron timer fires + โ†’ CronScheduler invokes Claude CLI with the task message + โ†’ Gets analysis result + โ†’ Detects platform == "wecom" && webhook_url is configured + โ†’ HTTP POST to webhook URL (markdown format) + โ†’ Message appears in group (as the webhook bot identity) +``` + +## Message Format + +Messages are sent as WeCom Webhook markdown: + +```json +{ + "msgtype": "markdown", + "markdown": { + "content": "โฐ **Scheduled task `abc12345`**\n\n" + } +} +``` + +Messages exceeding 4000 characters are automatically truncated. + +## Mentioning Users + +Use `<@userid>` syntax in the message content to @mention specific users: + +```json +{ + "msgtype": "markdown", + "markdown": { + "content": "<@WeiLu><@SunYuZe> Nightly regression report:\n\n..." + } +} +``` + +## Comparison: AI Bot vs Webhook Bot + +| Capability | AI Bot (respond) | AI Bot (send) | Webhook Bot | +|---|---|---|---| +| Group reply (reactive) | Yes (needs reply_token) | - | - | +| Group message (proactive) | No | Yes, but cannot @mention | Yes, supports @mention | +| DM (proactive) | - | Yes | No | +| Cron result to group | No | Possible | Recommended | + +Both can coexist in the same group โ€” AI Bot handles interactive conversations, Webhook Bot handles scheduled push notifications. + +## Fallback Behavior + +- **webhook_url configured**: cron results go through webhook (reliable, supports @mention) +- **webhook_url not configured**: cron results attempt `aibot_send_msg` (works but cannot @mention users) +- **Non-wecom platforms**: always use the normal outbound delivery path