WeChat Work Provider¶
Opt-in feature flag
WeChat Work is not compiled into the default acteon-server build. Enable it with cargo build -p acteon-server --features wechat, or use --features extras-alerting to enable all opt-in messaging providers at once.
Acteon ships with a first-class WeChat Work (企业微信 / Enterprise WeChat) provider that sends messages via the Message Send API. Operators use it for any workflow that maps to an Acteon Action — on-call alerting, deployment notifications, approval callbacks inside chains, scheduled reminders, and anything else that fits the send-a-message shape of the WeChat Work API. It's the most architecturally involved of Acteon's native messaging providers because of three WeChat-specific quirks the provider handles transparently.
Three things that make WeChat different¶
- Access tokens expire every 7200 seconds. Every API call passes an
access_tokenquery parameter that must be refreshed by calling a separategettokenendpoint with the org'scorp_id+corp_secret. The provider caches tokens and refreshes lazily with a configurable buffer window (default 300s / 5 minutes) so a token right at the edge of its TTL does not race an in-flight dispatch. - Token revocation is in-band. If the server returns
errcode: 42001("access_token expired") or40014("invalid access_token") mid-send, the provider invalidates its cached token and retries the request exactly once with a fresh token. Operators don't need to restart anything when a token is revoked out of band. - Errors travel in a
{"errcode": 0, "errmsg": "ok", ...}envelope. HTTP 200 witherrcode != 0is the normal failure shape; the provider classifies non-zero errcodes into retryable / non-retryable buckets so the gateway's retry logic handles transient server-busy errors correctly.
Secret hygiene¶
Both corp_id and corp_secret live in SecretString, zeroized on drop and redacted in Debug output. Neither is logged in normal operation — the gettoken URL (which embeds both as query parameters) is deliberately excluded from debug/error log strings. The cached access token is also a SecretString.
Health check¶
Because get_access_token is both a connectivity check and a credential check (a bad corp_secret surfaces as errcode: 40001 from gettoken), the provider's health_check calls it directly. Bad credentials show up on the provider health dashboard as non-retryable Configuration errors, same as the Telegram provider. Network outages surface as retryable Connection.
TOML configuration¶
[[providers]]
name = "wechat-ops"
type = "wechat"
wechat.corp_id = "ENC[AES256_GCM,data:abc123...]"
wechat.corp_secret = "ENC[AES256_GCM,data:def456...]"
wechat.agent_id = 1000002
wechat.default_touser = "@all"
wechat.default_msgtype = "text" # default; also "markdown" or "textcard"
# wechat.default_toparty = "12|15" # department IDs
# wechat.default_totag = "oncall" # tag IDs
# wechat.safe = false # confidential (no forwarding / screenshots)
# wechat.enable_duplicate_check = true
# wechat.duplicate_check_interval = 1800 # seconds
# wechat.token_refresh_buffer_seconds = 300 # default 5 minutes
| Field | Required | Description |
|---|---|---|
name | Yes | Unique provider name used when dispatching actions |
type | Yes | Must be "wechat" |
wechat.corp_id | Yes | Corporation ID from the WeChat Work admin console. Supports ENC[...]. |
wechat.corp_secret | Yes | Per-app secret from the admin console. Supports ENC[...]. |
wechat.agent_id | Yes | Numeric agent ID — identifies which WeChat Work app is sending. |
wechat.default_touser | No* | Default \|-separated user IDs, or "@all" for everyone. |
wechat.default_toparty | No* | Default \|-separated department IDs. |
wechat.default_totag | No* | Default \|-separated tag IDs. |
wechat.default_msgtype | No | "text" (default), "markdown", or "textcard". |
wechat.safe | No | Mark outgoing messages as confidential (no forwarding / screenshots). Defaults to false. |
wechat.enable_duplicate_check | No | Enable server-side dedup. When true, duplicate_check_interval is required. |
wechat.duplicate_check_interval | No | Dedup window in seconds (max 1800). |
wechat.token_refresh_buffer_seconds | No | Refresh window before token expiry (default 300). |
wechat.api_base_url | No | Override base URL (testing only). |
* At least one of default_touser, default_toparty, or default_totag must be set either in the provider config or in every dispatch payload — WeChat rejects messages with no recipients.
Payload shape¶
WeChat has no lifecycle concept — the provider accepts one event_action ("send", also the default) and supports three message types:
msgtype | Required fields | Use case |
|---|---|---|
"text" | content | Plain text |
"markdown" | content | WeChat-flavored markdown (limited syntax — see API docs) |
"textcard" | title, description, url | Clickable card with title, body, and link (optional btntxt for the button label) |
Image, voice, video, file, news, taskcard, template_card, mpnews, and miniprogram_notice message types are not supported in v1 — they're for content delivery rather than alerting and have complex nested payload shapes. Add them as a follow-up if demand emerges.
Text message¶
Markdown¶
{
"toparty": "12|15",
"msgtype": "markdown",
"content": "### Latency spike on **checkout-api**\n> p95 above 2s for 5 minutes\n> [Open runbook](https://wiki.example.com/runbook/checkout-latency)"
}
Textcard¶
{
"totag": "oncall",
"msgtype": "textcard",
"title": "CRITICAL: checkout-api down",
"description": "5xx rate above 50% for 2 minutes. Oncall paged.",
"url": "https://wiki.example.com/runbook/checkout-5xx",
"btntxt": "Open runbook"
}
Recipient routing¶
Each of touser, toparty, and totag accepts a |-separated string of IDs. The provider resolves them in this order:
- Payload-supplied fields take precedence per field.
- Config defaults fill in any fields the payload omits.
- At least one of the three must end up populated, or the provider rejects the request with a non-retryable
Serializationerror before it ever hits the WeChat API.
Rule integration¶
Because WeChat is just another named provider, every routing primitive Acteon already has works with it:
- Reroute critical alerts to a WeChat department by matching on
action.payload.severity == "critical"with arerouterule. - Silence maintenance windows with silences — silences apply before the provider dispatch, so a WeChat message never leaves the gateway during an active silence.
- Quota-bound a WeChat agent via a per-provider tenant quota scoped to
provider: "wechat-ops". - Dedup noisy events with Acteon's deduplication using
Action.dedup_key. You can also enable WeChat's native server-sideduplicate_checkfor defense in depth.
Outcome body¶
On success the provider returns an Executed outcome whose body carries the WeChat response:
{
"errcode": 0,
"errmsg": "ok",
"msgid": "xxxx",
"invaliduser": "u3",
"invalidparty": null,
"invalidtag": null
}
The msgid is the server-assigned message ID. The invaliduser / invalidparty / invalidtag fields list any recipients the server couldn't reach — a partial delivery is still classified as success because the send itself succeeded, but operators can surface these in audit or alerting pipelines if they care about reachability.
Error mapping¶
| WeChat response | ProviderError | Retryable? |
|---|---|---|
HTTP 2xx, errcode: 0 | Executed (success) | — |
errcode: 42001 / 40014 | Internal retry (refresh token, send once more) | Invisible |
| 42001 / 40014 twice (after refresh) | Configuration | No |
errcode: 40001 / 40013 | Configuration | No — bad corp_id / corp_secret |
errcode: 45009 | RateLimited | Yes |
errcode: -1 | Connection (via Transient) | Yes — system busy |
Other non-zero errcode | ExecutionFailed | No |
| HTTP 401 / 403 | Configuration | No |
| HTTP 5xx / 408 | Connection (via Transient) | Yes |
| Transport failure | Connection | Yes |
Simulation example¶
A full demo — text broadcast, markdown alert, textcard, plus a rule-based severity reroute — is in crates/simulation/examples/wechat_simulation.rs:
The simulation uses a recording provider, so it runs offline with no real WeChat Work credentials.