docs: add bot platform support matrix - #4787
Open
ying-hua wants to merge 11 commits into
Open
Conversation
Document the eight IM bot channels registered in BOT_PROVIDERS: their transports, credential fields, capability parity, message limits, known limitations and security posture. Every claim is derived from the bridge implementations rather than from prose, and is cited by symbol name rather than line number so the references survive refactors. Setup instructions per platform are deliberately left as marked placeholders. The issue requires them to be tested against real developer accounts, and untested setup steps are worse than none. Refs apache#3894 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Walked the Feishu channel end-to-end against a real custom app and documented what the runtime actually requires: bot capability, the im:message.p2p_msg:readonly scope, im.message.receive_v1 delivered over a long connection rather than a webhook, and a released version whose availability range covers the intended users. Both directions were exercised: an outbound bot DM, and an inbound user reply received over the WebSocket long connection. Also corrects the Feishu row of the credential table. The bridge reads `appSecret` or, when empty, falls back to `token` — the two are interchangeable and only one is needed. The table previously implied Feishu takes three distinct credentials. Records the failure mode the walkthrough surfaced: the handshake succeeds on app credentials alone, so an unreleased app yields a channel that reports a healthy connection while no user can find the bot to message it. Refs apache#3894 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Walked the WeCom channel end-to-end against a real AI bot. Both directions were exercised with the same SDK version the runtime pins (@wecom/aibot-node-sdk 1.0.7), mirroring WeComBotBridge.start(): the WebSocket handshake authenticated, an inbound single-chat frame arrived, and an outbound markdown send returned a req_id. Records the decision that governs every other step: the channel is the 智能机器人 in API mode, not a custom app, not a group webhook, and not 普通模式 — the latter answers messages with WeCom's own hosted models, so nothing reaches the long connection. Documents the addressing consequence observed in the inbound frame. A single-chat frame carries no chatid at all, which is why the bridge falls back to the sender's userid, and why a WeCom conversation cannot be addressed until the bot has received a message in it. Also notes that private deployments cannot work, since the SDK endpoint is fixed and the bridge exposes no override. Clarifies globally that an empty matrix cell means Maka does not implement the capability, not that the platform lacks it: the WeCom SDK offers streaming replies, media upload and template cards that the bridge never wires up. Refs apache#3894 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Walked the QQ channel end-to-end against a real bot, mirroring QQBotBridge and the GatewayBridgeBase lifecycle it inherits. All three startup steps were exercised — app access token exchange, gateway URL fetch, and WebSocket IDENTIFY reaching READY — followed by an inbound C2C dispatch and an outbound send that returned a message id. Documents the failure taxonomy the code encodes but the console does not explain: 4014 and 4004 are fatal closes that stop the bridge, while every other code reconnects with backoff, so a channel that dies and stays dead is almost always a missing intent or bad credentials. Records the addressing scheme, since QQ is the only platform whose chat IDs carry a prefix. Four inbound dispatch types map to four prefixes and four REST routes, and an unprefixed chat ID is dropped before any network call. Notes three things the walkthrough surfaced: the console's Token field belongs to webhook mode and is never read; group and guild traffic only arrives when the bot is @-mentioned; and QQ supplies no display name for group or C2C senders, so the bridge uses the bot-scoped openid as the user name. Also flags that the API host is hardcoded to production, so a sandbox-confined bot cannot be exercised through this channel. Refs apache#3894 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Walked the Telegram channel end-to-end against a real BotFather bot: getMe identity, getUpdates long-poll receiving three inbound messages, sendMessage, and sendChatAction typing. Confirms the matrix's one Telegram-exclusive claim rather than assuming it. sendMessageDraft is reachable by an ordinary BotFather bot, so the native streaming-reply path genuinely works; it is not a method the public Bot API rejects. Also records the two limits around it — streams are refused outright for group targets, and a single draft failure latches the stream off so streaming stops silently rather than erroring. Documents the default that makes a working bot look broken in groups: getMe reports can_read_all_group_messages false, so privacy mode hides all group traffic except commands and @-mentions until it is disabled via BotFather and the bot is re-added. Also notes that the poll requests only 'message' updates, and that a bot cannot open a conversation, so a channel that has never been messaged first is indistinguishable from a broken one. Refs apache#3894 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Walked the Discord channel end-to-end against a real bot: REST /gateway/bot, gateway IDENTIFY reaching READY, an inbound MESSAGE_CREATE carrying non-empty content, an outbound send, and a typing indicator. Leads with the MESSAGE CONTENT INTENT because it has two failure modes and the second is the expensive one: either the gateway closes 4014, or messages arrive with every content field empty, leaving a bot that looks connected and awake while replying to nothing. Notes that Discord receives ordinary channel traffic with no @-mention, unlike QQ and Telegram groups, and that its chat IDs are bare channel IDs with none of QQ's prefixing. Records a proxy caveat honestly rather than as a confirmed defect. WsBridgeBase.createWebSocket() builds the socket without a dispatcher, so the channel's proxyUrl covers REST but not the gateway. Verification did not reproduce a failure — the unproxied socket connected — which is what a system-wide or TUN-mode proxy produces, so the gap is stated as conditional on the host rather than as an observed break. Refs apache#3894 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Walked the Slack channel end-to-end against a real workspace app: auth.test identity, Socket Mode connect, an inbound channel message and an inbound DM covering both branches of the isGroup derivation, and an outbound chat.postMessage. Documents the two-token split first, since the field named appSecret holds the app-level token rather than the signing secret, and a swap fails inside SocketModeClient.start() with an error that never names the cause. Records two setup gates found by walking it. A channel the bot has not been invited to rejects sends with not_in_channel, and direct messages stay disabled until App Home's Messages Tab allows them — until that box is checked Slack refuses DMs outright, message.im never fires, and nothing in the app's own configuration explains why. Notes that Slack is the only channel promoted to operational on connect rather than on first traffic, so a healthy-looking Slack channel proves less than a healthy-looking one elsewhere; that the mapper drops every event carrying a subtype or bot_id; and that Socket Mode has no replay, unlike Telegram's getUpdates offset. Also records that this channel ignores proxyUrl entirely, since neither WebClient nor SocketModeClient is given an agent. Refs apache#3894 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Walked the DingTalk channel against a live internal app. Setup and receive are verified: gettoken, gateway/connections/open, the Stream WebSocket, and an inbound direct message decoded from the /v1.0/im/bot/messages/get callback topic. Sending is not verified, because replying to a 1:1 conversation is broken. An inbound direct message yields conversationType "1", so isGroup is derived correctly, but chatId is stamped as conversationId and a 1:1 conversation ID also begins with "cid" — the exact prefix pickDingTalkSendRoute uses to detect a group. The reply is therefore posted to /v1.0/robot/groupMessages/send and rejected with resource.not.found. The comment above that helper describes the prefix as "cidp", which the implementation does not match. Routing to the 1:1 endpoint instead does not rescue it: that API wants staff user IDs, and the payload's senderId is an opaque $:LWCP_v1:$ token rejected with staffId.notExisted. DingTalkBotMessagePayload declares no field that endpoint accepts, so neither route can deliver a direct reply with what the bridge currently captures. Both failures were reproduced as a controlled pair against a live app, holding the robot code constant and varying only the chat ID. The section and the known-limitations list state the scope honestly: group replies were not exercised, and whether reusing appId as the robot code is valid remains unconfirmed. Refs apache#3894 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Captured the raw Stream payload and extended the send experiment to a third case, which closes the diagnosis. Holding the conversation and the robot code constant and varying only the target: conversationId (what the bridge stamps) is rejected by the group endpoint, senderId is rejected by the 1:1 endpoint, and senderStaffId is delivered by the 1:1 endpoint and returns a processQueryKey. So the defect is not merely a misrouted prefix check. The identifier the 1:1 endpoint requires, senderStaffId, is present on every inbound bot message and is absent from DingTalkBotMessagePayload, so no reachable value in the mapped event can address a direct conversation. The payload also carries a sessionWebhook with an explicit expiry, which needs neither robot code nor staff ID and covers both conversation kinds. Recorded as a second possible route the bridge does not use. Resolves one earlier caveat: the payload's robotCode equals the app's appId, so the bridge's reuse of appId as the robot code is correct. Group replies are still unexercised and stay marked unverified. Refs apache#3894 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
WeChat has no ordinary bot API, so the channel picks between an iLink route and a local-bridge route from the value of webhookUrl. Both are now documented, along with the fact that Maka ships no local bridge at all: it defines an HTTP contract and expects a third-party process to implement it. Wrote that contract down as a table — /health, the SSE receive loop and its since cursor, POST /send, and the QR endpoints — then exercised Maka's own WechatBridge against a mock implementing it. Driving the real class rather than a reimplementation is what makes the exercise mean anything: every mapping and error decision under test is the shipped code. All six inbound filter rules pass, including the group-without- mention drop that most looks like a broken bridge, plus bearer auth and both send paths, with the failure diagnostic surfacing as the status reason. Building the mock is what surfaced /health, which the streaming loop alone does not reveal: start() probes it first for identity and to gate sending, and a bridge missing it never reaches the receive loop. The verification note is explicit that this covers the contract and not WeChat itself, and that the iLink route stays unverified because its token only comes from the in-app QR flow. Also records that the settings UI collects Official Account App ID and Secret which no code path in the bridge reads. With this the setup guide has no placeholders left. Refs apache#3894 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rebased onto current main, which renamed the per-channel status reasons to snake_case. Updated the three the guide quotes: missing-slack-tokens to slack_tokens_missing, missing-feishu-credentials to feishu_credentials_missing, and the generic no-credentials to wecom_credentials_missing now that each channel carries its own code. Re-derived the capability matrix against the new tree; every row still maps to the same bridges, and the load-bearing constants are unchanged (Discord's 2000-character limit, Telegram's 4000 UTF-16 units and message-only allowed_updates, QQ's fatal 4004/4014 closes, Slack promoting to operational on connect). Points the DingTalk defect at apache#5112, which replaces the prefix guess with chat IDs stamped at receive time. That fix is not on main yet, so the section still documents the broken behaviour and says what to rewrite once it lands. Refs apache#3894 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ying-hua
force-pushed
the
doc/bot-platform-matrix
branch
from
September 9, 2026 14:52
88ec1b6 to
0c11b67
Compare
ying-hua
marked this pull request as ready for review
September 9, 2026 14:53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #3894.
Adds
docs/bot-platforms.mdcovering the eight IM bot channels registered inBOT_PROVIDERS, plus an index entry indocs/README.md.What's in it
feishuchannel and are separated by thedomainsetting rather than by provider.BotReadinessStateenum instead of inventing a parallel vocabulary.Claims are cited by symbol name rather than line number, so the references survive refactors.
Verification
Every setup section was walked against a real account rather than written from the code alone. For each platform the probe mirrored the bridge's own code path — same SDK version where one is used, same constructor options, same endpoints — so a passing probe means the shipped bridge would connect.
WeChat is the one channel with no real-account coverage. Maka ships no local bridge — it defines an HTTP contract and expects a third-party process to implement it — so the guide writes that contract down and verifies Maka's own
WechatBridgeclass against a mock implementing it. Driving the real class rather than a reimplementation is what makes it meaningful. Building the mock is also what surfacedGET /health, whichstart()probes before anything else and which reading the streaming loop alone does not reveal. The section states plainly that WeChat delivery itself is unverified, as is the iLink route.Things the walkthrough found that reading the code did not
message.imsimply never fires, and nothing in the app's configuration explains why.cidprefix, which every DingTalk conversation ID carries, so direct replies go to the group endpoint and are rejected. Reproduced as a three-way controlled send, which also showed the working target is the payload'ssenderStaffId— a fieldDingTalkBotMessagePayloadnever captures. Fixed by fix(runtime): route DingTalk 1:1 replies by staff id #5112; the section documentsmain's behaviour and says what to rewrite once that merges.contentempty, so the bot looks connected and awake while replying to nothing.getMereportscan_read_all_group_messages: false.Gaps worth separate issues
Flagging rather than fixing, since this PR is documentation only.
allowedUserIdsis enforced by three of eight bridges. Telegram, Feishu and WeCom check it. Discord, QQ, DingTalk, Slack and WeChat never read the field, so an allowlist configured there silently does nothing.SendCapableis text-only.proxyUrlreaches REST but not gateway sockets, and the Slack channel ignores it entirely. Not reproduced as a failure here — a system-level proxy masks it — so it is recorded conditionally.Checks
npm run check:asf-headerspasses.npm run format:check— the only failure is a pre-existing.claude/launch.jsonissue onmain, untouched by this branch.main; the capability matrix was re-derived against it and the renamed snake_case status reasons picked up.🤖 Generated with Claude Code