ACECode can talk to you through instant-messaging apps. QQ (official bot, API v2), WeChat (Tencent iLink bot), Feishu / Lark, DingTalk, Telegram (Bot API), Discord and LINE (Messaging API) are built into the daemon and configured in Settings > Integrations > Messaging channels in Desktop or the local Web UI. WhatsApp is configured in a terminal; see WhatsApp.
All seven channels are native C++: transports in src/adapters/im/<platform>/,
routing, access and storage in src/host/channels/core/. The platform list,
credential fields and identity rules live in one table
(src/host/channels/core/platforms.cpp). Every platform except LINE connects
outward from this computer (WebSocket or long polling), so no public address,
webhook server or Node/Python runtime is needed. LINE only delivers to a public
HTTPS address; see below. All HTTP and WebSocket traffic follows ACECode's proxy
settings.
The page shows the platforms as cards in two columns. Each card has one button:
Connect while the platform is off, Disconnect while it is on. Both take
effect immediately; there is no separate Save. A card shows only a one-line
status (connecting, connected with the bot's name, reconnecting, failed, or held by
another ACECode process) and any pending approval requests. Details such as
failure reasons, group privacy mode, held QQ outputs, bindings and send counts are
written to the daemon log (<data dir>/logs/daemon-<date>.log, lines tagged
[channels/<platform>]) instead of the page. A failed card
offers Fix, which reopens the connect step. A configured card offers
Manage to change the bot or revoke the owner and approved contacts.
Connect on a platform that has no credentials or no owner yet opens a three-step dialog: set up the bot, connect, and set the owner. When credentials and owner both exist, Connect connects right away without the dialog.
QQ. The first step shows a QR code as soon as it opens; scan it with mobile
QQ. ACECode requests a bind task from Tencent's bot binding service (the flow
used by the official connector, with source acecode), shows the bind page as a
QR code, refreshes expired codes automatically and gives up after ten minutes. On
success the AppID and the AppSecret, decrypted locally with a one-time AES-256-GCM
key, are saved and QQ is switched on. The person who scanned becomes the owner
when the service reports them; otherwise the first person to message the bot
privately within ten minutes becomes the owner, and after that window approving
the first private request does. Cancelling or failing leaves the existing
configuration unchanged.
Enter AppID / AppSecret manually in the same step accepts the credentials of a bot from QQ Open Platform. They are verified by exchanging an access token before saving. Bots without personal verification can only be used by their administrator and can only join groups the administrator owns; the dialog says so when it finishes.
Telegram. The first step explains how to create a bot with @BotFather; paste
its token and it is verified with getMe before saving. The owner step creates a
one-time https://t.me/<bot>?start=<code> link valid for ten minutes and shows it
as a QR code. Whoever opens it and taps Start becomes the owner. Scan the QR
code with the phone's camera app; Telegram's own scanner only links devices and
cannot read it. On a computer where Telegram is signed in, click the link or use
Copy link instead. With privacy mode on (the BotFather default) groups deliver
only @mentions, replies to the bot and commands, which matches the mention-only
group behavior; with it off the bot sees every group message but still handles
only @mentions. If the bot has a webhook, polling is blocked; the connect step
offers Remove webhook and connect, after explaining that the service
currently receiving the webhook stops getting messages.
WeChat. The first step shows a QR code from Tencent's iLink bot service; scan it with WeChat and confirm on the phone. The WeChat account that scanned becomes the owner automatically and is the only one who can talk to the bot; WeChat bots have private chats only. The bot token is saved locally. When the service reports the login has expired, the card shows a failure and the dialog asks you to scan again. Media goes through Tencent's CDN, encrypted with AES-128-ECB.
Feishu / Lark. Create a custom app on the Feishu Open Platform
(open.feishu.cn/app, or open.larksuite.com/app for Lark), add the Bot
capability, grant im:message, im:message:send_as_bot,
im:message.p2p_msg:readonly, im:message.group_at_msg:readonly and
im:resource, then enter the App ID and App Secret (and pick Feishu or Lark).
They are verified with a tenant access token before saving. ACECode then opens
the long connection; only while it is online does Feishu let you save Receive
events through persistent connection in Events & Callbacks, so the connect
step lists what to finish there: save that mode, add the event
im.message.receive_v1 and publish a version that includes you.
DingTalk. Create an internal app on the DingTalk Open Platform
(open-dev.dingtalk.com), add the Robot capability with Stream message
mode, grant the internal robot send-message permission, publish it, and enter the
Client ID and Client Secret. They are verified with an access token. Replies go to
the reply address carried by each incoming message while it is valid, and through
the robot API afterwards, so replies typed in Desktop or sent after a restart are
still delivered.
Discord. Create an application in the Discord Developer Portal, copy the bot
token from the Bot page and turn on Message Content Intent there. The
token is verified with /users/@me and /applications/@me; the connect step then
shows an invite link with the permissions the bot needs. Direct messages and
@mentions (or replies to the bot) in server channels and threads are handled.
LINE. Create an official account in LINE Official Account Manager, enable the
Messaging API, then copy the Channel ID and Channel secret from LINE
Developers; ACECode issues short-lived access tokens from them (a long-lived token
can be pasted instead). In the console turn on Use webhook and turn off
auto-reply and greeting messages. LINE can only deliver to a public HTTPS address,
so the daemon starts a separate listener bound to 127.0.0.1 that serves only
the webhook, a health check and outgoing media; every request is checked against
the channel secret signature. Leave Public address empty to let ACECode start
a Cloudflare quick tunnel (cloudflared, install with
winget install --id Cloudflare.cloudflared) or enter your own HTTPS address that
forwards to that listener. On every connect ACECode sets LINE's webhook URL to the
current address. Never point a tunnel at the daemon's own port: local requests
skip token checks there.
Feishu, DingTalk, Discord and LINE owners. After connecting, the owner step shows a 6-digit code. Send exactly those digits to the bot in a private chat; the sender becomes the owner and the message is not passed to a session. Codes expire after ten minutes and work once; ten wrong guesses while a code is outstanding invalidate it.
Everyone is denied by default. Each platform has at most one owner.
- An unknown private sender gets a pairing notice. The request appears on the platform's card, Desktop shows a system notification when its window is not focused, and the request expires after ten minutes. Approval applies from the contact's next message. Approving a private user while there is no owner makes them the owner.
- A group must be approved first. In an approved group only messages that @mention the bot are handled (on Telegram and Discord a reply to the bot also counts), and each member is approved separately. QQ member identities are scoped per group. On Discord each server channel or thread counts as a group; WeChat has no groups.
- Revoking access stops processing that contact immediately and aborts the running turn of their sessions.
- Switching to a different bot clears the owner and approvals on QQ, WeChat, Feishu, DingTalk and LINE, because their user ids differ per bot or app; on Telegram and Discord user ids are global and are kept.
Each private chat, and each member within a group, is bound to exactly one
ACECode session. The first accepted message creates a no-workspace session with
default permissions; it never inherits the daemon's dangerous or
skip-confirmation mode. Bindings survive restarts. A session is bound to at most
one chat: resuming it from another chat, even on the other platform, moves the
binding and tells the previous chat. If a bound session was permanently deleted,
the bot says so and suggests /new or /sessions instead of silently starting a
new one.
| Command | Effect |
|---|---|
/help |
Commands available to the sender |
/status |
Platform, session, location, model, permission mode, reasoning effort, busy state, pending approvals and questions |
/stop |
Stop the current turn |
/new |
New session in the bound session's workspace, or a no-workspace session |
/sessions, /sessions more, /sessions search <text> |
Numbered list of sessions to switch to |
/resume <number or session id> |
Switch the binding; numbers refer to the latest list |
/model [name] |
List saved models, or switch the bound session's model from the next turn |
/mode [default|auto|yolo] |
Show or change the bound session's permission mode: default, automatic, or full access (accept-edits is an alias for auto) |
/yolo |
Switch directly to full access, skipping tool permission prompts and allowing pending permission requests |
/effort [level|default] |
Show current reasoning effort and supported levels, set a supported level, or restore the model preset with default |
/approve <id>, /deny <id> |
Decide a permission request once |
/aq ... |
Answer an AskUserQuestion, with the same syntax as /rc |
Commands are never sent to the model. While a session is busy /new and
/resume are refused; other commands run at once and plain messages queue as
usual. The owner can switch to any normal, unarchived session, including other
workspaces and no-workspace sessions. Other contacts only see sessions created
from their own chat. Archived and subagent sessions are never listed. Text that
starts with / is expanded as an opencode or skill command exactly like Web
input. Plain text sent while a question is pending is handed to that question as
an interjection.
/mode, /yolo, and /effort apply only to the bound session and work while
it is busy. Permission changes follow the same rules as Desktop. Effort changes
take effect on the next model request, including later requests in the current
turn; the in-flight request is not interrupted. Use /effort to see the current
model's supported levels. Unsupported levels leave the setting unchanged. These
commands require an existing session (/new or a normal message creates one)
and do not change global defaults.
The assistant text of each turn and turn errors are sent; tool progress is not.
Long text is split below each platform's limit. QQ merges one turn's text into as
few messages as possible and uses Markdown with a plain-text fallback. Telegram
converts Markdown to Telegram HTML and shows "typing" while the session is busy.
Replies to input typed in Desktop are sent to the bound chat as well. Permission
requests list /approve and /deny ids and questions use /aq; the first
answer, from the chat or Desktop, wins and the other side is told it was handled.
Send failures are counted per binding and written to the daemon log. A send
whose outcome is unknown is never retried automatically.
QQ passive replies quote the incoming message (private chats: 4 replies within 60 minutes; groups: 5 within 5 minutes). Beyond that window ACECode sends an active message; if that is refused too, the output is held (up to 20 per chat, in memory) and delivered before the contact's next message is handled.
Feishu sends Markdown as rich text (post) and falls back to plain text;
DingTalk sends Markdown; Discord sends Markdown as is, below 2000 characters per
message, never pinging @everyone; WeChat and LINE send plain text. LINE uses the
free reply token while it is fresh and push messages otherwise; when the push
quota is used up, outputs are held until the contact's next message, like QQ.
Images and files up to 25 MiB, and within the platform download limit (20 MB on Telegram), are imported into the session's attachments. QQ voice messages use the platform's recognized text. Video, stickers and voice without text get a clear "not supported" reply. Images and files produced by the session are sent as native media: on Telegram images over 10 MB go as files and files over 50 MB are not sent; on QQ files over 20 MiB are replaced by a text notice. LINE bots cannot send files at all, so a text notice is sent instead; LINE images are served to LINE through the listener's media address.
Only the daemon hosts these channels: Desktop's shared daemon or a standalone
acecode daemon. The TUI and headless modes never do. Whether channels stay
online after Desktop quits follows Settings > General > Continue running the
background process after exiting ACECode. Each platform account has its own OS
file lock (owner.lock); a second process stays in standby, shows that the bot
is held by another ACECode on the card (the owning PID goes to the log) and takes
over after that process exits.
Data lives under <data dir>/channels/<platform>/ (qq, weixin, feishu,
dingtalk, telegram, discord, line).
config.json holds the on/off state, credentials, owner and approvals; it has
owner-only permissions and is written only through the settings API. state.json
holds bindings, the sessions created per chat, the last 4096 message receipts and
the Telegram update offset and the WeChat polling cursor. A corrupt file blocks that platform with an error
instead of being reset. Credentials and QR payloads are never logged; the API
returns only their last four characters. The settings API and its WebSocket events
accept direct local clients only (docs/daemon-api.md, section 19).
QQ guild channels, Telegram webhooks, hosted bots, buttons, streamed message editing and voice replies are not supported. Feishu and DingTalk apps are set up by hand (their scan-to-create flows are not used). Cloudflare quick tunnels have no uptime guarantee and get a new address on every start; LINE may lose messages while its webhook URL switches. WeChat logins expire on Tencent's side and must be scanned again. Feishu and DingTalk hand each event to one random connection of the app, so running the same app in another ACECode, hermes or OpenClaw silently takes part of the messages; use one app per runner. LINE shows "connecting" until Use webhook is on in the console, and LINE file messages other than images become a text notice. Discord forum channels and Feishu card buttons are not handled. QQ "typing" is not sent because it may consume the passive reply budget. Held QQ outputs are lost if the daemon restarts; the session transcript still has them. Chunked QQ uploads are not implemented. So far the channels have been verified on Windows only.
cmake --build <build-dir> --target acecode acecode_unit_tests
acecode_unit_tests --gtest_filter='WebSocketClient*:AesGcm*:Im*:Qq*:Tg*:Telegram*:Channel*:ImChannelsEndToEnd*'
pnpm --dir web testThe transports run end to end against local fake QQ (token, gateway WebSocket,
messages, uploads, bind portal) and Telegram (Bot API, long polling, multipart)
servers; the core uses an in-memory session client. ImChannelsEndToEnd* runs the daemon's
production wiring with a real session registry and a scripted model against those fakes, covering
owner binding, private and group chats, approvals, session switching, media and QQ resend. These
tests never contact
Tencent or Telegram. Real-account acceptance is tracked in the change's
verification.md.
WhatsApp uses a personal account linked by QR. C++ routing lives
in src/host/channels; a small, pinned Baileys/Node.js process handles WhatsApp device
authentication and encryption. This is an unofficial WhatsApp Web integration,
not the Business Cloud API. Account-based end-to-end validation requires pairing
your own device. Never expose the account's auth directory or pairing QR.
Run acecode channels in an interactive terminal. acecode channels setup
is an explicit alias. This command only configures the account; it does not
start a daemon or leave a channel service running. There is no /channels
command or channel page inside the main TUI.
The standalone acecode channels command displays a content-sized wizard after
the shell command, without clearing existing terminal output or switching to a
full-screen buffer. Exiting returns below the wizard.
- Choose Just myself (the default), or Myself and selected contacts.
- For selected contacts, enter phone numbers with country codes, separated by commas. No WhatsApp JIDs are needed. Your own linked account is added automatically.
- Choose Save configuration. An already linked account is reused without connecting or starting a process. First-time linking checks Node.js 22+ and installs pinned bridge dependencies automatically. It never starts a daemon. If Node.js/npm is missing or too old, use Download Node.js, install it, reopen ACECode if PATH changed, and retry. The wizard does not install Node.js or modify system settings itself.
- Scan the live QR from WhatsApp Settings > Linked devices > Link a device. Expired codes refresh automatically; a saved login skips this step entirely. Enlarge the terminal if requested so the complete QR fits on screen.
- After WhatsApp confirms the linked account, the wizard saves credentials, access and the auto-connect preference, then closes the temporary bridge. The completion screen only confirms that configuration is saved.
The saved configuration is read when acecode daemon or ACECode Desktop
next starts. Already-running instances are left alone, without checking their
status, applying changes to them, or asking the user to close or restart them.
Once connected, WhatsApp's Message yourself conversation can be used normally.
Back, Cancel, and Retry are available within the setup flow. Installation and pairing run off the UI thread. Cancel stops pending installation/pairing; a first-time configuration stays disabled until completion. Reconfiguration retains existing access lists, credentials, conversation bindings, and histories. Pairing uses a temporary configuration-only bridge that cannot send or deliver messages to an agent. Success, cancellation and failure all stop that temporary process. New pairing uses a unique credential profile and never opens another instance's active credentials. Saving settings uses a separate, short configuration lock; the runtime account lock does not block the wizard. Pairing QR codes and setup progress never enter an agent conversation.
Dependencies and credentials live in the user data directory, outside Program
Files or a signed .app. No manual npm command is needed for normal first-time setup.
All other contacts are denied by default. With daemon/Desktop running, allow a known contact locally:
acecode channels allow 15551234567@s.whatsapp.net
Or let an unknown contact send a DM, inspect acecode channels pending, and approve
their ten-minute pairing code with acecode channels approve <code>. Approval is only
available to the local operator; sending the code back over WhatsApp does not
grant access. The contact must send a new message after approval. Self-chat is
already authorized by the wizard. Bot output IDs are persisted to avoid self-chat
echo loops. Existing contacts remain authorized when the wizard is rerun; revoke
unwanted contacts explicitly with acecode channels revoke <jid>.
The first authorized message creates a regular no-workspace session, with
default tool permissions. It does not inherit a local skip-confirmations mode,
does not run inside the current terminal project, and is not a subagent. The
same contact resumes the same persisted session after restart. A group must be
allowlisted as well as its participant, and every input must explicitly mention
the linked account. Each group participant has their own session.
| Terminal command | Operation |
|---|---|
acecode channels [setup] |
Configure and link; do not start a service |
acecode channels status |
Live status, or saved configuration when no host is running |
acecode channels on, acecode channels reconnect |
Enable/restart in an already running host |
acecode channels off |
Disconnect, retain credentials and histories |
acecode channels qr |
Current pairing QR, without printing the raw login payload |
acecode channels pending, acecode channels approve <code> |
Inspect/approve incoming pairing requests |
acecode channels allow <jid>, acecode channels revoke <jid> |
Manage contact/group access |
acecode channels sessions |
Bound conversations and failed/dropped delivery counters |
acecode channels show <session-id> |
Recent persisted transcript and pending requests |
acecode channels send <session-id> <text> |
Submit input to that independent session |
acecode channels file <session-id> <path> |
Explicitly send a local file to its WhatsApp chat |
acecode channels stop <session-id> |
Stop its active turn |
Only configuration and status work without a running host. Other commands attach to the current owner and report that daemon/Desktop must be started when absent; none of these commands auto-start a background process.
Quote paths containing spaces. Local file is an explicit operator action and
can select a file outside the session; automatic outgoing files must resolve to
that session's stored attachments. Arbitrary Markdown file paths are not sent.
WhatsApp controls bypass the ordinary message queue, even while a tool waits:
/status
/stop
/aq 1
/aq --status
/aq --cancel
/approve <permission-request-id>
/deny <permission-request-id>
/aq is the existing RC question interface, including multiple choice and free
text. The terminal can answer the same requests with acecode channels send <session-id> /aq ... or /approve .... Only the originating authorized participant or the
local operator can answer; the underlying question/permission prompter retains
first-wins semantics. Remote permission approval is one-time, not a persistent
grant. Other sessions' request IDs cannot be used.
Text, images and documents are accepted. Attachments are limited to 25 MiB and imported into the existing session attachment store. Model support still applies to image/document understanding. Native quoted replies preserve the incoming message reference while it remains in the bounded bridge cache. Long text is split using the existing RC helper. Unsupported media and delivery failures do not terminate other conversations.
Only the daemon worker hosts the gateway, using its existing SessionRegistry and
LocalSessionClient. Starting acecode daemon connects configured channels.
Desktop starts/reuses the same worker and connects without requiring a separately
started daemon. The main TUI does not host channels. Closing Desktop obeys its
existing background/keep-alive settings; a user-started daemon retains normal
daemon lifetime. acecode channels off stops communication without shutting
down unrelated sessions or deleting history. Configuration creates no dedicated
daemon, listener, agent session or channels/whatsapp/run runtime directory.
Each runtime reads its configuration once at startup. Disabled instances do not
claim account ownership or start a channel listener. An OS file lock is claimed
when the first enabled daemon/Desktop channel runtime starts.
Later instances remain standby and do not start another bridge, reconnect the
account, or consume its messages. They can take over only after the owner exits
and closes its bridge. A standby retains its startup configuration on takeover;
only conversation bindings and receipts are refreshed. Setup never acquires this lock.
The private owner.json descriptor publishes an
authenticated loopback endpoint only after readiness. This endpoint is separate
from the public daemon API and rejects browser-origin requests.
Settings and access live under ~/.acecode/channels/whatsapp/config.json.
Conversation bindings and the last 4096 incoming receipts remain in state.json.
Legacy settings in state.json are read only if config.json does not exist, so
old running binaries cannot overwrite newly saved configuration. New credentials,
dependencies and media live under the selected profiles/<id>/ directory;
legacy auth/, bridge/ and media/ directories remain usable without migration
or modification during setup. Pairing requests are memory-only and expire after
ten minutes. State corruption fails closed. A missing historical session is an
error, not an excuse to silently replace the binding with a new conversation.
Messages are recorded before dispatch to avoid repeated tool execution. A crash
between recording and enqueue may require the contact to send a new message.
Outbound delivery uses the bounded RC FIFO; it is not a durable outbox. Unknown
send outcomes are not automatically retried. Inspect acecode channels sessions for
failed/dropped sends and the session transcript before explicitly retrying.
No voice, scheduled/cross-channel delivery, other platforms, Matrix encryption,
Yuanbao, Signal, BlueBubbles or Photon support is included in this phase. Existing
/rc activation and bindings are unchanged.
acecode builds stage the bridge next to the executable. cmake --install build --config Release --prefix <package> --component whatsapp_channel stages the
bridge assets for a flat package. macOS desktop builds include the same assets
under Contents/Resources/channels/whatsapp. npm dependencies are installed in
the user's private runtime directory on first setup, not bundled with the app.
cmake --build build --config Release --target acecode acecode_unit_tests
ctest --test-dir build -C Release --output-on-failure -R 'Channel'
npm ci --prefix assets/channels/whatsapp
npm test --prefix assets/channels/whatsappLocal tests exercise mock transport/process failure, session routing, access,
attachments, questions, permissions, owner takeover, setup ordering, cancellation,
access preservation, QR rendering and wizard navigation. Set
ACECODE_TEST_CHANNEL_INSTALL=1 to run the optional real npm installation test in
a temporary directory. It never starts WhatsApp or touches an account's credentials.
The real bridge smoke
test loads Baileys and checks the disconnected protocol without contacting an
account. These are not a substitute for scanning a QR and exchanging real
messages from the intended account.
After pairing, verify the following with the intended account before relying on unattended operation:
- Fresh setup installs dependencies, refreshes the QR and saves only after linking succeeds. Completion leaves no bridge or background host, and messages are not processed until daemon/Desktop starts. Cancel before completion leaves it disabled. Rerunning setup retains existing contact access and conversations.
- An unknown contact gets only a pairing code. Local approval permits a new message; revocation prevents further input and stops its active turn.
- Two contacts get different no-workspace sessions. Self-chat does not feed bot replies back into the model. The current TUI session stays independent.
- An allowed group ignores messages without a mention or from unapproved participants. Approved participants keep separate sessions.
- Text, an image, a document and quoted replies arrive in the correct chat. Unsupported media returns a clear error.
/aq,/approve,/denyand/stopwork while the session is busy. An answer from one surface closes the same pending request on the other surface.- Reconnect and restart retain the same session history. Opening Desktop alone reconnects an enabled account, and closing it follows the existing background preference. Starting another daemon/Desktop leaves the first connection alone; a standby takes over only after the owner exits.