feat(gateway): add WeCom (企业微信) channel adapter#769
feat(gateway): add WeCom (企业微信) channel adapter#769canyugs wants to merge 22 commits intoopenabdev:mainfrom
Conversation
Implement WeCom as a new gateway platform for receiving and sending messages via enterprise app callback API. Features: - AES-256-CBC message decryption (WeCom uses PKCS7 block_size=32) - SHA1 signature verification with constant-time comparison - Access token cache with auto-refresh and expiry margin - Message deduplication (30s TTL, 10k max entries) - Long message splitting at line boundaries (2048 byte limit) - GatewayResponse with message_id for OAB core integration Env vars: WECOM_CORP_ID, WECOM_SECRET, WECOM_TOKEN, WECOM_ENCODING_AES_KEY, WECOM_AGENT_ID, WECOM_WEBHOOK_PATH Note: Streaming (edit_message) is intentionally disabled — WeCom has no native message edit API; recall+resend shows disruptive notifications. Uses plain text msgtype since WeCom app message markdown is too limited (no code blocks, tables, or lists). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add comprehensive WeCom setup documentation covering: - Prerequisites and enterprise app creation - Callback URL configuration - Environment variables reference - Docker/Kubernetes deployment - Troubleshooting guide Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add handle_edit_message and recall_message methods to support OAB streaming. When OAB sends edit_message commands, the adapter recalls the previous message and re-sends updated content, tracking message ID changes via msg_id_map to handle the WeCom limitation of no native edit API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Download images from WeCom PicUrl, resize/compress via image crate, and forward as base64 attachment in gateway event. Also fix upstream googlechat test compilation (missing attachments field). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…r + debounce flush Instead of recalling and resending on every streaming update, send a single "⏳..." placeholder and buffer all edits. After 3 seconds of inactivity, recall the placeholder once and send the complete response. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Download files via WeCom media API and forward text-based files (code, config, data files) as text_file attachments to OAB. Also fix split_text tests to match renamed split_text_lines function. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add 5-minute max timeout to debounce flush task to prevent leaks - Make WECOM_AGENT_ID required (was defaulting to "0") - Align Helm chart: require token + encodingAesKey in $hasWecom condition - Fix README: mark WeCom env vars as required - Update docs/wecom.md feature matrix to reflect implemented features - Add WeCom vars to "no adapters configured" warning message Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ogging split_text_lines now handles single lines exceeding the limit by splitting at UTF-8 char boundaries. Previously long lines were sent as-is, causing WeCom to silently truncate messages. Also add detailed logging to flush_thinking for easier debugging. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new WeCom (企业微信) channel adapter to the openab-gateway service, including callback verification/decryption, token caching, message deduplication, and inbound media handling, plus documentation and Helm wiring so the adapter can be enabled via deployment config.
Changes:
- Implement WeCom adapter (webhook verify + message callback, AES-CBC decrypt, token cache, dedupe, reply sending + streaming placeholder flush).
- Wire the adapter into the gateway router/state and update gateway/environment documentation.
- Add Helm values/templates and a new setup guide (
docs/wecom.md) for configuring WeCom.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Adds WeCom entry pointing to the setup guide and gateway requirement. |
| gateway/src/main.rs | Registers WeCom adapter, routes, and reply dispatch plumbing. |
| gateway/src/adapters/wecom.rs | New WeCom adapter implementation (crypto, webhook handling, media download, streaming). |
| gateway/src/adapters/mod.rs | Exposes the new wecom adapter module. |
| gateway/src/adapters/googlechat.rs | Updates tests to include attachments field in Content. |
| gateway/README.md | Documents WeCom env vars and webhook endpoints. |
| gateway/Cargo.toml | Adds sha1 + quick-xml dependencies for WeCom integration. |
| gateway/Cargo.lock | Updates lockfile for new deps. |
| docs/wecom.md | New WeCom setup and usage documentation. |
| charts/openab/values.yaml | Adds WeCom gateway values stanza. |
| charts/openab/templates/gateway.yaml | Injects WeCom env vars into gateway deployment. |
| charts/openab/templates/gateway-secret.yaml | Stores WeCom secrets in the gateway secret when configured. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let pad_byte = *plaintext.last().ok_or_else(|| anyhow::anyhow!("empty plaintext"))? as usize; | ||
| if pad_byte == 0 || pad_byte > 32 || pad_byte > plaintext.len() { | ||
| anyhow::bail!("invalid wecom padding value: {pad_byte}"); | ||
| } |
There was a problem hiding this comment.
Fixed in 22b44a5 — now validates that all padding bytes equal the pad_byte value before stripping.
| if entries.len() >= DEDUPE_MAX_SIZE { | ||
| entries.retain(|_, t| now.duration_since(*t).as_secs() < DEDUPE_TTL_SECS); | ||
| } |
There was a problem hiding this comment.
Won't fix — the dedupe map has a 30s TTL and only triggers cleanup at 10,000 entries. WeCom enterprise app callbacks are rate-limited by the platform. Under realistic traffic this won't grow unbounded. Adding an LRU dependency for this edge case is over-engineering.
| "text" => { | ||
| if wecom.config.group_require_mention { | ||
| strip_bot_mention(&msg.content) | ||
| } else { | ||
| msg.content.clone() | ||
| } | ||
| } |
There was a problem hiding this comment.
Fixed in 22b44a5 — strip_bot_mention now returns Option<String> and messages without a mention are dropped when group_require_mention is enabled.
| let agent_id = self.config.agent_id.parse::<u64>().unwrap_or(0); | ||
| let body = serde_json::json!({ | ||
| "touser": to_user, | ||
| "msgtype": "text", | ||
| "agentid": agent_id, |
There was a problem hiding this comment.
Fixed in 22b44a5 — WECOM_AGENT_ID is now validated as numeric in from_env(). Invalid values disable the adapter at startup with a warning.
| | Text message receive/reply | ✅ | | ||
| | AES-256-CBC message decryption | ✅ | | ||
| | Message deduplication | ✅ | | ||
| | Auto-split long replies (2048 chars) | ✅ | |
There was a problem hiding this comment.
Fixed in 22b44a5 — updated to "2048 bytes".
| value: {{ ($cfg.gateway).googleChat.webhookPath | quote }} | ||
| {{- end }} | ||
| {{- end }} | ||
| {{- $hasWecom := and (($cfg.gateway).wecom).corpId (($cfg.gateway).wecom).secret (($cfg.gateway).wecom).token (($cfg.gateway).wecom).encodingAesKey }} |
There was a problem hiding this comment.
Fixed in 22b44a5 — agentId is now included in the $hasWecom condition in both gateway.yaml and gateway-secret.yaml. WECOM_AGENT_ID is always injected when WeCom is enabled.
| {{- if (($cfg.gateway).wecom).agentId }} | ||
| - name: WECOM_AGENT_ID | ||
| value: {{ ($cfg.gateway).wecom.agentId | quote }} | ||
| {{- end }} |
There was a problem hiding this comment.
Fixed in 22b44a5 — same fix as above, agentId now required in $hasWecom and always injected.
| secret: "" # App Secret → WECOM_SECRET (use --set-literal or external secret mgmt) | ||
| token: "" # Callback verification token → WECOM_TOKEN | ||
| encodingAesKey: "" # 43-char AES key → WECOM_ENCODING_AES_KEY | ||
| agentId: "" # Agent ID → WECOM_AGENT_ID (default: 0) |
There was a problem hiding this comment.
Fixed in 22b44a5 — updated comment to "(required)" and added agentId to $hasWecom condition.
- Validate PKCS#7 padding bytes match before stripping - Validate WECOM_AGENT_ID is numeric at startup (fail-fast) - Gate group messages: drop when no @mention present (group_require_mention) - Add agentId to Helm $hasWecom condition (required field) - Fix docs: "2048 chars" → "2048 bytes" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove standalone K8s manifest (Helm chart is the canonical way) - Keep docker run as env var quick-start example (matches other adapters) - Add note: group chat requires enterprise real-name verification Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
wangyuyan-agent
left a comment
There was a problem hiding this comment.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PR #769 Review: feat(gateway): add WeCom (企业微信) channel adapter
1. 解決什麼問題
為 OpenAB Gateway 新增企業微信(WeCom)channel adapter,讓用戶可以透過企業微信 App 與 OAB agent 對話。涵蓋:回調驗證、AES-256-CBC 消息解密、access token 自動刷新、長消息分片、群聊 @mention 過濾、streaming reply(debounce flush)。
2. 方案是否正確
整體架構正確——遵循現有 adapter 模式(feishu / telegram / googlechat):
- WecomConfig::from_env() 讀環境變數
- GET 處理回調驗證、POST 處理消息回調
- 加解密走 WeCom 官方規範(SHA1 簽名 + AES-CBC + PKCS#7)
- Helm chart / secret / values.yaml 同步更新
- 文檔完整(setup guide + troubleshooting)
依賴選擇合理:sha1 用於簽名驗證,quick-xml 解析 WeCom 的 XML payload,皆為成熟 crate。
3. 問題與建議
必修項(REQUEST_CHANGES):
| # | 問題 | 說明 |
|---|---|---|
| 1 | 缺少調研對比 | PR description 未包含 Discord / OpenClaw / Hermes Agent 對 WeCom adapter 的實現方式對比。按項目規範,PR 必須附調研對比、飛書/平台 API 事實核查、設計取捨。 |
| 2 | 缺少設計考量與取捨 | 為何選 XML 解析而非 JSON 模式?streaming reply 的 debounce 策略如何決定?長消息 2048 bytes 切分依據?這些決策需要記錄。 |
| 3 | Content struct 改動影響面 | attachments: vec![] 被加到所有 googlechat test fixtures,說明 Content struct 新增了 attachments 欄位——但 diff 中未見 Content 定義的改動,也未見其他 adapter(teams / telegram / line / feishu)的對應更新。需確認是否所有 adapter 都已適配,或是否有 breaking change。 |
| 4 | 版本跳躍 0.1.0 → 0.4.0 | 一個 PR 直接跳三個 minor version,缺乏 CHANGELOG 說明。若中間版本已在其他 PR 發佈則無妨,但需在 PR description 交代。 |
建議(非阻塞):
- WECOM_GROUP_REQUIRE_MENTION 未出現在 Helm values.yaml 中,生產部署時無法透過 Helm 配置。
- decode_aes_key 中 with_decode_allow_trailing_bits(true) 放寬了 base64 解碼——建議加註釋說明為何需要此設定(WeCom 的 key 格式特殊性)。
- 文檔中 "Option A: Zeabur" 作為 recommended 略顯主觀,建議改為 neutral 排列或標註 "for quick testing"。
4. Verdict
REQUEST_CHANGES
主要原因:缺少項目規範要求的調研對比與設計取捨文檔。代碼實現本身品質不錯,補齊 PR description 的三項必備內容(調研對比、API 事實核查、設計考量)後可 approve。
OpenAB PR ScreeningThis is auto-generated by the OpenAB project-screening flow for context collection and reviewer handoff.
Screening report## IntentPR #769 adds a WeCom / 企业微信 channel adapter to the OpenAB Custom Gateway so users can talk to OAB agents from WeCom enterprise app direct messages and group chats. The concrete operator-visible problem is that OpenAB currently lacks a first-class WeCom ingress and reply path. Teams using WeCom cannot route enterprise chat messages into agents without custom glue code, webhook handling, encryption verification, token management, and deployment configuration. FeatThis is a feature PR. It adds a new gateway adapter for WeCom callbacks and replies, including encrypted callback verification, XML message parsing, access token caching, message deduplication, image download/resize handling, long-response splitting, group mention gating, Helm configuration, and operator documentation. It also appears to include a small unrelated change in Who It ServesPrimary beneficiaries: WeCom enterprise users and deployers who want OpenAB agents available inside corporate WeCom workspaces. Secondary beneficiaries: gateway maintainers and reviewers, because the PR adds deployment docs, Helm values, and unit coverage for the new adapter. Rewritten PromptImplement a WeCom channel adapter for the OpenAB Custom Gateway. Add Add outbound reply support using WeCom’s send API, including cached access tokens, safe splitting for messages over WeCom’s 2048-byte text limit, and a streaming-compatible fallback that sends a temporary placeholder and flushes final text after debounce. Expose configuration through documented environment variables and Helm values. Add focused tests for crypto verification, XML parsing, deduplication, group mention behavior, token refresh, message splitting, and streaming flush behavior. Keep any unrelated adapter changes out of scope unless required by a shared gateway contract. Merge PitchThis is worth advancing because WeCom is a major enterprise chat surface, especially for organizations operating in China, and the PR fills a clear integration gap in the gateway layer. Risk profile: medium-high. The feature touches external crypto validation, inbound webhook security, media handling, outbound delivery, Helm secrets, and a large new adapter file. The main reviewer concern will likely be whether the implementation is too large to merge safely in one pass, whether WeCom protocol edge cases are covered, and whether the adapter follows existing gateway patterns closely enough. Best-Practice ComparisonRelevant OpenClaw principles: Gateway-owned scheduling is not directly relevant; this PR is event-driven webhook handling, not scheduled execution. Durable job persistence is partially relevant. WeCom callbacks and outbound replies are handled in process, and deduplication appears TTL-based. That may be acceptable for a first channel adapter, but durable delivery state would improve reliability during gateway restarts. Isolated executions are not central here unless media processing or streaming flush tasks can block shared gateway behavior. Review should confirm image processing is bounded and does not create unbounded memory or CPU pressure. Explicit delivery routing is highly relevant. The adapter should clearly map WeCom users, rooms, groups, and agent replies to gateway sessions so responses cannot leak across conversations. Retry/backoff and run logs are relevant for outbound sends, token refresh, media downloads, and placeholder recall. The PR should expose enough structured logging for operators to debug failed WeCom delivery. Relevant Hermes Agent principles: Gateway daemon tick model is not directly relevant because this is webhook-driven, not polling-driven. File locking to prevent overlap is not relevant. Atomic writes for persisted state is only relevant if token or dedupe state becomes persistent. The current in-memory approach avoids file consistency issues but loses state on restart. Fresh session per scheduled run is not relevant. Self-contained prompts for scheduled tasks is not relevant. The strongest applicable principles are explicit routing, bounded isolated processing, retry/backoff, structured logs, and careful persisted-state decisions if delivery durability is later added. Implementation OptionsConservative option: Merge a minimal WeCom text-only adapter first. Scope this to encrypted callback verification, text DM and group message ingestion, group mention gating, access token caching, text replies, Helm/env docs, and tests. Defer images, file handling, placeholder recall, and streaming debounce to follow-up PRs. Balanced option: Merge the current adapter after review tightening. Keep text, group messages, image receiving, long-message splitting, token caching, deduplication, Helm support, and docs. Require review cleanup around the large Ambitious option: Introduce a more general chat-adapter delivery framework. Use WeCom as the first implementation of a shared gateway abstraction for encrypted webhook verification, media fetch normalization, outbound delivery retries, dedupe stores, and channel-specific text splitting. This could improve future adapters but would expand the PR significantly and delay merge. Comparison Table
RecommendationAdvance with the balanced option, but ask Masami or Pahud to review it as a security-sensitive gateway adapter rather than a routine channel addition. Before merge discussion, split or justify the unrelated |
|
Good overall implementation — the AES-256-CBC decryption, double-checked locking on the token cache, and constant-time signature comparison are all handled correctly. 22 tests is a solid baseline. A few things worth addressing: Bugs / correctness
"image" => "Describe this image.".to_string(), // no mention check
"file" => format!("User sent a file: {}", msg.file_name), // no mention checkDangling "thinking…" placeholder on debounce timeout ( Err(_) => {
if !last_text.is_empty() { break; }
if started.elapsed() > max_idle { break; } // exits with empty last_text → recall skipped
}
Inconsistency between code and PR description
These are likely copy-paste leftovers from an earlier draft — the code values should be updated to match the described behaviour, or vice versa. Minor issues
Dedupe cache never purges until 10K entries ( Tests are not thread-safe ( Access token in URL ( |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Dismissing: verdict was REQUEST_CHANGES but incorrectly posted as approve due to a bug. Apologies.
Response to chaodu-agent third re-review (
|
This comment has been minimized.
This comment has been minimized.
Per chaodu-agent's 4th re-review:
F1: download_wecom_file used a token directly without retry. If the
cached token expired between get_token() and the GET, the file silently
failed. Added fetch_media_with_retry() which sniffs the response
Content-Type — WeCom's media API returns JSON {errcode:42001,...}
on token expiry instead of binary — and retries once after a forced
refresh. download_wecom_file now takes &WecomTokenCache and runs the
retry helper itself.
F2: TOCTOU in handle_reply's has_pending branch. The first has_pending
read happens under a lock that's then released; by the time we re-take
the lock to append, the debounce task may have removed the entry,
and we'd silently drop the chunk. Now: re-check inside the second
lock and, if the entry is gone, fall through to the direct-send path
so the chunk still reaches the user.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Response to chaodu-agent 4th re-review (
|
This comment has been minimized.
This comment has been minimized.
F2 (debounceSecs:0 silently ignored): document the minimum is 1 in docs/wecom.md and values.yaml. The Helm template's truthy check treats 0 as unset; since 0-second debounce defeats the buffer purpose anyway, documenting the floor is more honest than reshaping the truthy check to accept a value with no real use case. F3 (env::set_var in tests is parallel-unsafe): refactor from_env to delegate to from_reader, which takes a closure. Tests now build a HashMap-backed reader and never touch process-wide env vars, so cargo's parallel runner can't race them. Also fixes the wecom collapsible_match clippy warning while we're in there (XML parser nested if-in-match collapsed to match guards). F4 (no rate-limiting docs): added a "Production Hardening" section explaining that the timestamp-freshness check rejects stale replays cheaply but fresh-but-invalid requests still consume CPU, and pointing to edge / LB / reverse-proxy layer for IP-level rate limits, plus the WeCom Trusted IP allowlist as the strongest control. F1 (clippy CI gap) and F5 (mutex poison logging) intentionally not addressed — see the follow-up reply for rationale. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Response to chaodu-agent round-5 (
|
Restores the strict-clippy parity that the root crate has and that reviewers (chaodu round 4 and 5) flagged as a CI consistency gap. The mechanical fixes don't change behavior: - dead_code on serde-deserialize fields → #[allow(dead_code)] with fact-only "parsed by serde, not consumed in current code paths" (no speculation about future intent) - needless_range_loop in markdown rendering → buf.extend(slice.iter()) - manual_strip in fenced code block detection → strip_prefix - useless_conversion on tungstenite Message::Binary → drop the .into() - too_many_arguments on ws_connect_loop / handle_ws_message → #[allow(clippy::too_many_arguments)]; refactoring 11-arg async fn signatures is a larger change that doesn't belong here Any further warnings exposed by this strict job will be visible in the CI run and addressed in a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After merging upstream main and enabling strict clippy on the gateway job, 5 more warnings surfaced: - feishu.rs: collapsible_if — flatten outer + !(in_thread && bypass_mention_gating) into one if condition - feishu.rs: nonminimal_bool — !is_some_and(<) → is_none_or(>=) - wecom.rs: too_many_arguments on flush_thinking (8 args) → #[allow(clippy::too_many_arguments)] - wecom.rs: useless_vec on parts vec → use 4-element array, sort_unstable - main.rs: explicit_auto_deref on &*text → &text Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
…wn limits) F1 (corpsecret in URL): WeCom's gettoken API requires the secret as a query param; we cannot move it to a header. Added a comment in code clarifying the protocol constraint and a "Redact corpsecret from access logs" section in docs/wecom.md instructing operators to redact query strings at the proxy layer for /cgi-bin/gettoken outbound calls. F2 (byte-vs-char comment): added a doc comment to split_text_lines making explicit that the limit and all len() comparisons are in bytes (matching WeCom's server-side truncation), and that lines exceeding the limit are split at UTF-8 char boundaries. F3 (streaming task lifetime on shutdown): documented as known limitation. The fix would add a JoinSet/CancellationToken on the adapter; non-trivial scope, and impact is bounded since streaming defaults off. Recorded in the new "Known limitations" docs section. F4 (DedupeCache eviction is lazy): unchanged, documented under known limitations. ~500 KB max memory bound is acceptable; correctness (dedup window honored) is unaffected. Repeated finding from Copilot/chaodu earlier rounds; canyugs's prior "won't fix" rationale still applies. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Response to chaodu-agent round-6 (
|
|
LGTM ✅ — Well-engineered WeCom adapter that correctly implements the platform crypto spec, follows existing adapter patterns, and ships with thorough tests and documentation. What This PR DoesAdds WeCom (企业微信) enterprise app support to the Custom Gateway, enabling 1:1 direct message conversations between WeCom users and OAB agents via the self-built app callback API. How It WorksThe adapter handles the full WeCom callback lifecycle: AES-256-CBC message decryption (PKCS7 block_size=32, IV=key[..16]), SHA1 signature verification (constant-time via Findings
Finding Details🟢 F1: Correct WeCom crypto implementationThe adapter correctly handles WeCom's non-standard PKCS7 padding (block_size=32 instead of AES's native 16). It decrypts with 🟢 F2: Constant-time signature comparisonUses 🟢 F3: SSRF protection on image downloadRejects non-HTTPS 🟢 F4: Replay protectionThe 5-minute timestamp freshness check ( 🟢 F5: Streaming defaults offThe thinking-placeholder + recall pattern causes a brief client flicker. Defaulting to off is the right UX choice; operators can opt in with 🟢 F6: Token retry on 42001Both 🟢 F7: Secrets in K8s Secret
🟢 F8: Comprehensive test coverage21 tests covering: config parsing, AES key decode, signature verify/reject, encrypt-decrypt roundtrip, XML parsing (text/image/file), dedup cache, token refresh with wiremock, text splitting (including UTF-8 char boundaries), text file detection, and full webhook flow simulation. 🟢 F9: Production hardening docs
🟢 F10: CI coverage for gatewayAdding a dedicated Baseline Check
What's Good (🟢)
|
Summary
Adds WeCom (企业微信) support to the Custom Gateway. Users can chat with OAB agents via WeCom enterprise app 1:1 direct messages. Group chat is not supported by the WeCom self-built app callback API and is documented as a non-goal of this PR — see Not Yet Supported.
Prior Art & Industry Research
How comparable open-source agent gateways handle WeCom integration:
OpenClaw (repo) — delivers WeCom support as an out-of-tree community npm plugin:
@wecom/wecom-openclaw-plugin, maintained by the Tencent WeCom team. The plugin uses WeCom Bot WebSocket persistent connections (not HTTP callback mode), supporting DMs and group chats. OpenClaw's core never ships a callback-mode adapter — it relies on the plugin catalog model.Hermes Agent (repo) — ships two in-tree WeCom adapters:
gateway/platforms/wecom.py— bot/WebSocket mode via WeCom AI Bot gateway (introduced v0.6.0 / PR #3847).gateway/platforms/wecom_callback.py— self-built enterprise app callback mode (introduced v0.9.0 / PR #7943). This is the closest comparator to this PR.Hermes' callback adapter has a few notable choices that informed (or contrast with) this PR:
wecom_callback.pyWXBizMsgCrypthelper class wrapping AES-CBC + SHA1 sigwecom.rsapps: List[...], scoped bycorp_id:user_idMESSAGE_DEDUP_TTL_SECONDS)8645//wecom/callback8080//webhook/wecom(matches existing gateway adapters)chat_type="dm"(group routing only via separatewecom.pybot adapter)Neither project covers OpenAB's specific need — a single-binary Rust adapter inside the Custom Gateway that connects via WebSocket out to OAB pods. Hermes is Python/aiohttp; OpenClaw delegates to a separate Node.js plugin process.
Why This Approach
feishu.rs,telegram.rs,googlechat.rs).aes/cbcfrom RustCrypto keeps the dep surface minimal and auditable.feishu.rsuses incremental edits). The placeholder→debounce→recall pattern delivers streaming-feeling output without spamming users with N partial messages. Note: this causes a brief flicker on the WeCom client (recall + new message) — see chaodu-agent's UX feedback; tracked as a follow-up to make streaming opt-in.Alternatives Considered
wecom.py, OpenClaw plugin) — deferred to follow-up: AI Bot is generally available (no whitelist) but is a separate WeCom product type with a narrower feature surface than self-built apps. The protocol (aibot_subscribe/aibot_msg_callback/aibot_send_msg/ etc.) requires its own ~600-line implementation including reconnect/heartbeat/anti-kick logic. Worth doing as a second adapter — it natively supports group chats and streaming — but doubles the surface area of one PR. Tracked as separate work.wecom-rsand similar; all are unmaintained (>2 years no commits) or only cover the messaging-send side, not callback decryption. Not safe to depend on.Changes
gateway/src/adapters/wecom.rsgateway/src/adapters/mod.rspub mod wecomgateway/src/main.rsgateway/README.md/webhook/wecomendpoint docsgateway/Cargo.tomlaes,cbc,base64crypto dependenciesdocs/wecom.mdcharts/openab/values.yamlwecomconfig block undergatewaycharts/openab/templates/gateway.yamlcharts/openab/templates/gateway-secret.yamlwecom-secret,wecom-token,wecom-encoding-aes-keyto unified Secret.github/workflows/ci.ymlgateway/**and add agatewayjob runningcargo check+cargo testfor the gateway crate (was previously skipped)gateway/src/schema.rsDefaultderive toContentandAttachmentso future field additions don't require updating every adapter test fixtureKey Design Decisions
AES-256-CBC decryption — WeCom encrypts all callback payloads with a 43-char EncodingAESKey (base64-decoded to 32 bytes). The adapter decrypts in-process using
aes/cbccrates, validates Corp ID in the decrypted suffix, and strips PKCS#7 padding. No external crypto dependencies required.Streaming via thinking placeholder + debounce flush — WeCom has no edit-message API (unlike Feishu). Streaming uses a "thinking…" placeholder sent immediately, then a background task with a debounce timer (3s quiet period, 5min max wait). On flush: recall the placeholder, split the accumulated text, send final chunks. Uses
tokio::sync::watchfor efficient signaling.Message splitting at UTF-8 char boundaries — WeCom silently truncates messages exceeding 2048 bytes.
split_text_lines()splits at newline boundaries, and for single lines exceeding the limit, splits at safe UTF-8 char boundaries to prevent mid-character truncation.Access token caching — Tokens are cached with 7100s TTL (WeCom issues 7200s tokens). Auto-refresh on expiry. Thread-safe via
tokio::sync::RwLock.Image receiving with resize — Downloads media via WeCom's
/cgi-bin/media/getAPI, detects content type, compresses/resizes images > 1MB to fit within OAB's processing limits. Sends asimageattachment type in the gateway event schema.Text file receiving — Downloads files via media API, detects text files by extension (aligned with OAB's
src/media.rsallowlist:.txt,.md,.rs,.py,.js,.ts,.json,.yaml,.toml,.css,.html,.sh, etc.) and special filenames (Dockerfile,Makefile, etc.). Sends content astext_fileattachment type.DM-only scope — WeCom self-built enterprise app callbacks deliver only 1:1 member-to-app messages, not group chat messages. The adapter does not attempt group routing or
@mentiongating; group chat support belongs in a future WeCom AI Bot WS adapter (which natively supports it).Testing
.rs,.md,.json)cargo test— 21 wecom tests passed (now wired into PR-time CI)Configuration
Environment Variables
WECOM_CORP_IDWECOM_AGENT_IDWECOM_SECRETWECOM_TOKENWECOM_ENCODING_AES_KEYWECOM_WEBHOOK_PATH/webhook/wecom)Not Yet Supported
appchatAPI (group routing on send) plus group event subscription, or a separate WeCom AI Bot WebSocket adapter (which has native group support). Tracked as follow-up.Test plan
cargo testpasses (21 wecom-specific tests, now wired into PR-time CI)🤖 Generated with Claude Code
Discord Discussion URL
https://discord.com/channels/1491295327620169908/1501052008868745347
Closes #724