Skip to content

feat: add telegram and whatsapp channels#554

Merged
Siri-Ray merged 10 commits intomainfrom
feat/telegram-whatsapp-channels
Mar 26, 2026
Merged

feat: add telegram and whatsapp channels#554
Siri-Ray merged 10 commits intomainfrom
feat/telegram-whatsapp-channels

Conversation

@Siri-Ray
Copy link
Copy Markdown
Contributor

@Siri-Ray Siri-Ray commented Mar 26, 2026

Relates to #439

Summary

  • add Telegram and WhatsApp channel onboarding to Nexu
  • stabilize WeChat and WhatsApp login flows and align Telegram/WhatsApp config defaults with OpenClaw requirements
  • wire desktop/controller/runtime updates needed for packaged local development

Testing

  • pnpm generate-types
  • pnpm typecheck
  • pnpm lint
  • pnpm test

Summary by CodeRabbit

  • New Features

    • Telegram bot support with in-app token setup and connect flow.
    • WhatsApp personal linking via QR: start, wait/poll, and finalize connect.
  • Enhanced Channel Management

    • Channel objects now include connection status, team/app/bot info and created/updated timestamps.
    • Channels/home UI and modals updated for Telegram/WhatsApp; "Open in Telegram"/WhatsApp links where applicable.
  • API & SDK

    • New Telegram and WhatsApp channel endpoints and matching client SDK calls.
  • Localization & Tests

    • English/Chinese strings added and new tests for WhatsApp plugin behavior.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Mar 26, 2026

Deploying nexu-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 6969442
Status: ✅  Deploy successful!
Preview URL: https://bed4906d.nexu-docs.pages.dev
Branch Preview URL: https://feat-telegram-whatsapp-chann.nexu-docs.pages.dev

View logs

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Telegram and WhatsApp channel support across the controller, gateway RPCs, config store, runtime plugins, SDK, web UI, and shared schemas — including Telegram bot-token connect and WhatsApp QR start/wait/connect flows, new ChannelService methods, and frontend setup components.

Changes

Cohort / File(s) Summary
OpenAPI & API Types
apps/controller/openapi.json, apps/web/lib/api/types.gen.ts, apps/web/lib/api/sdk.gen.ts
Extended channelType with telegram/whatsapp; added channel response fields (status, teamName, appId, botUserId, createdAt, updatedAt); added Telegram connect and WhatsApp QR/connect endpoints/types; made WeChat QR endpoints require request bodies.
Shared Schemas & Config
packages/shared/src/schemas/channel.ts, packages/shared/src/schemas/openclaw-config.ts
Added telegram/whatsapp to channelTypeSchema; new schemas/types for Telegram/WhatsApp connect and WhatsApp QR flows; added Telegram/WhatsApp account/channel schemas and exported account config types.
Controller Routes
apps/controller/src/routes/channel-routes.ts
Added POST /api/v1/channels/telegram/connect and WhatsApp QR/connect endpoints; made WeChat handlers use ChannelService and tightened request validation.
Channel Service & Gateway
apps/controller/src/services/channel-service.ts, apps/controller/src/services/openclaw-gateway-service.ts
Implemented Telegram connect and WhatsApp QR lifecycle (start/wait/connect), in-memory WhatsApp login tracking, auth-dir handling, gateway RPCs for WhatsApp QR/login and logout; ChannelService now injected with env and gatewayService.
Config Store
apps/controller/src/store/nexu-config-store.ts
Added connectTelegram and connectWhatsapp methods to persist channel records, deduplicate existing channels, and store channel-specific secrets (botToken, authDir).
Runtime Plugins (WhatsApp)
apps/controller/static/runtime-plugins/whatsapp/*
Added WhatsApp runtime plugin (channel implementation, runtime wrapper, manifest, package.json), tests, outbound/media/poll support, actions (react), account management, status/gateway hooks.
Runtime writer / bindings
apps/controller/src/runtime/openclaw-runtime-plugin-writer.ts, apps/controller/src/lib/channel-binding-compiler.ts
Runtime writer skips materializing builtin plugins when present; channel-binding compiler emits Telegram and WhatsApp accounts into OpenClaw config.
Sessions & Analytics
apps/controller/src/runtime/sessions-runtime.ts, apps/controller/src/services/analytics-service.ts
Added WhatsApp/Telegram detection in session hint parsing; extended analytics channel enum/mapping to include telegram and whatsapp.
Env & Container wiring
apps/controller/src/app/env.ts, apps/controller/src/app/container.ts
Added OPENCLAW_EXTENSIONS_DIRenv.openclawBuiltinExtensionsDir; container now constructs ChannelService with env and gatewayService.
Frontend: Pages & Setup UIs
apps/web/src/components/channel-setup/telegram-setup-view.tsx, apps/web/src/components/channel-setup/whatsapp-setup-view.tsx, apps/web/src/pages/channels.tsx, apps/web/src/pages/home.tsx
Added Telegram bot-token setup UI and multi-phase WhatsApp QR setup UI; integrated into Channels and Home pages with modals, icons, platform entries, and "Open in Telegram" link.
Frontend SDK & Links
apps/web/lib/api/sdk.gen.ts, apps/web/src/lib/channel-links.ts
Generated SDK functions for Telegram/WhatsApp endpoints; added channel link resolution for Telegram (t.me/<botUserId> fallback) and WhatsApp (https://web.whatsapp.com/).
Runtime plugin tweak
apps/controller/static/runtime-plugins/openclaw-weixin/src/channel.ts
Removed unused gatewayMethods declaration from WeChat plugin.
Desktop bootstrap
apps/desktop/main/services/launchd-bootstrap.ts, apps/desktop/main/index.ts
Added openclawBinPath and openclawExtensionsDir to resolved launchd paths; updated openclaw path derivation to use centralized paths.*.
Localization
apps/web/src/i18n/locales/en.ts, apps/web/src/i18n/locales/zh-CN.ts
Added Telegram and WhatsApp UI strings and setup copy; removed Telegram from "coming soon" lists.
Tests
apps/controller/tests/openclaw-runtime-plugin-writer.test.ts, apps/controller/static/runtime-plugins/whatsapp/src/*test.ts
Added tests verifying builtin plugin skip and WhatsApp plugin resolve/send behaviors.

Sequence Diagram(s)

sequenceDiagram
    participant User as Web Client
    participant API as Controller API
    participant ChannelSvc as ChannelService
    participant Store as NexuConfigStore

    User->>API: POST /api/v1/channels/telegram/connect { botToken, telegramBotId, ... }
    API->>ChannelSvc: connectTelegram(input)
    ChannelSvc->>Store: connectTelegram(input)
    Store-->>ChannelSvc: ChannelResponse (status: connected, id, accountId, ...)
    ChannelSvc-->>API: ChannelResponse
    API-->>User: 200 ChannelResponse
Loading
sequenceDiagram
    participant User as Web Client
    participant API as Controller API
    participant ChannelSvc as ChannelService
    participant Gateway as OpenClawGatewayService
    participant Store as NexuConfigStore

    User->>API: POST /api/v1/channels/whatsapp/qr-start
    API->>ChannelSvc: whatsappQrStart()
    ChannelSvc->>Gateway: whatsappQrStart(accountId?)
    Gateway-->>ChannelSvc: { qrDataUrl, accountId }
    ChannelSvc-->>API: { qrDataUrl, accountId? }
    API-->>User: 200 { qrDataUrl }

    loop Polling
      User->>API: POST /api/v1/channels/whatsapp/qr-wait { accountId }
      API->>ChannelSvc: whatsappQrWait(accountId)
      ChannelSvc->>Gateway: whatsappQrWait(accountId)
      Gateway-->>ChannelSvc: { connected: false | true }
      ChannelSvc-->>API: { connected, message }
      API-->>User: 200 { connected }
    end

    User->>API: POST /api/v1/channels/whatsapp/connect { accountId }
    API->>ChannelSvc: connectWhatsapp({ accountId })
    ChannelSvc->>Store: connectWhatsapp({ accountId })
    Store-->>ChannelSvc: ChannelResponse (status: connected, id, ...)
    ChannelSvc-->>API: ChannelResponse
    API-->>User: 200 ChannelResponse
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

  • nexu-io/nexu#468: Implements Telegram and WhatsApp channel support across controller, plugins, SDK, and UI — directly aligns with this PR.

Possibly related PRs

Suggested reviewers

  • lefarcen
  • nettee
  • PerishCode
  • anthhub

Poem

🐰 I hopped through schemas, QR and token tunes,

Wired bots and sockets under silver moons.
Plugins sprout wings, the channels hum,
QR scans blink — connections come.
Carrots for code, a hop and boom!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description provides a clear summary section explaining what was added and why (adding channels and stabilizing flows), includes a testing section confirming key verification steps were performed, but does not follow the provided template structure with 'What', 'Why', 'How', 'Affected areas', or 'Checklist' sections. Consider restructuring the description to follow the template format: add 'What' (one-liner), 'Why' (motivation), 'How' (design decisions), explicitly check the 'Affected areas', and mark all checklist items as complete.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the primary change: adding support for Telegram and WhatsApp channel integration to Nexu. It is concise, specific, and clearly conveys the main objective without unnecessary detail.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/telegram-whatsapp-channels

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 33bbb44008

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/controller/src/services/channel-service.ts
Comment thread apps/controller/src/services/channel-service.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

🧹 Nitpick comments (3)
apps/controller/src/store/nexu-config-store.ts (1)

839-840: Use cuid2 for the new channel ids.

These ids are exposed through ChannelResponse, so the new Telegram/WhatsApp connect paths should follow the repo’s public-id rule instead of minting UUIDs here.

As per coding guidelines, Public IDs: use cuid2 (@paralleldrive/cuid2), never expose pk.

Also applies to: 879-880

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/src/store/nexu-config-store.ts` around lines 839 - 840, The
code creates new channel IDs using crypto.randomUUID() for the ChannelResponse
objects (variable channel) — replace those calls with the cuid2 generator from
`@paralleldrive/cuid2` (e.g., createId()) so the exposed ChannelResponse IDs
follow the repo’s public-id rule; add the import for createId and update both
occurrences (the channel construction at the current snippet and the other
occurrence around lines 879-880) to use createId() instead of
crypto.randomUUID().
apps/controller/static/runtime-plugins/whatsapp/src/channel.ts (1)

439-441: Use structured fields for the startup log.

Interpolating the account metadata into one string makes this harder to query and parse in log sinks. Emit fields and a short message instead.

🪵 Suggested refactor
-      ctx.log?.info(`[${account.accountId}] starting provider (${identity})`);
+      ctx.log?.info(
+        { channel: "whatsapp", accountId: account.accountId, identity },
+        "starting provider",
+      );
As per coding guidelines, "Logging: structured (pino or console JSON), never log credentials".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/static/runtime-plugins/whatsapp/src/channel.ts` around lines
439 - 441, Current startup log interpolates account metadata into a single
string; change it to emit structured fields and a short message instead. In the
block using getWhatsAppRuntime().channel.whatsapp.readWebSelfId(account.authDir)
and ctx.log?.info, build a log call that passes an object with accountId, e164,
jid (omit sensitive values if they are credentials) and a concise message like
"starting provider" rather than embedding them in the message string; update the
call site where account.accountId, e164, and jid are used (and ensure you never
log secrets) so downstream log consumers can query fields instead of parsing the
interpolated string.
apps/web/lib/api/types.gen.ts (1)

1-2: Note: Auto-generated file has style deviations from coding guidelines.

This file uses 4-space indentation and single quotes, while the coding guidelines specify 2-space indentation and double quotes. Since this is auto-generated by @hey-api/openapi-ts, these style choices should be addressed in the generator configuration rather than manually edited in the generated output.

As per coding guidelines, TypeScript files should use 2-space indent and double quotes. Consider updating the generator config to align with project standards.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/api/types.gen.ts` around lines 1 - 2, The generated types file
produced by `@hey-api/openapi-ts` uses 4-space indentation and single quotes;
update the generator configuration for `@hey-api/openapi-ts` to emit double quotes
and a 2-space indent (e.g., set quote style to double and tabWidth/indentation
to 2 in the generator’s options or template/prettier settings), then regenerate
the output (the header comment "// This file is auto-generated by
`@hey-api/openapi-ts`" identifies the generated artifact to target).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/controller/openapi.json`:
- Around line 3359-3376: The OpenAPI request bodies for the new onboarding POST
endpoints currently omit requestBody.required making the entire body optional;
update the OpenAPI generation (the POST route specs that include a requestBody
with properties like "botToken" and "accountId") to set requestBody.required:
true so the body itself is required while keeping the inner property
"botToken"/"accountId" required as-is; apply the same change for the other two
occurrences referenced (the blocks around the "botToken" schema and the ones at
the other two ranges) so all POST onboarding routes enforce a required request
body.

In `@apps/controller/src/services/channel-service.ts`:
- Around line 469-485: fetchWechatQrCode uses a bare fetch so a stalled upstream
can hang wechatQrStart; add an AbortController-based timeout: create an
AbortController inside fetchWechatQrCode, start a timer (e.g., 10s or use a
configurable constant) that calls controller.abort(), pass controller.signal to
fetch(url.toString(), { signal }), and clear the timer after fetch completes;
ensure the function throws a clear timeout/abort error so callers (like
wechatQrStart) can handle it.
- Around line 681-690: The polling loop in channel-service.ts that calls
pollWechatQrStatus (using DEFAULT_WECHAT_BASE_URL and activeLogin.qrcode) spins
with no delay when status is "wait" or "scaned"; add a short awaitable backoff
inside that branch (e.g., await sleep(500ms) with optional exponential/backoff
capped at ~5s and jitter) so the loop yields between requests and reduces
hammering the WeChat API, leaving the existing deadline logic intact; implement
a small helper like sleep(ms) and apply it whenever status is "wait" or "scaned"
before continuing.

In `@apps/controller/src/store/nexu-config-store.ts`:
- Around line 852-868: The update block in this.store.update that replaces
channels by channelType+accountId leaves orphaned secrets keyed by the previous
channel.id (e.g., channel:<oldId>:botToken or channel:<oldId>:authDir) causing
unreachable secrets after reconnect; modify the update logic in the
channels-replacement blocks (the this.store.update call that builds channels and
secrets using channel and input) to detect any previous channel with the same
channelType+accountId, and remove its secrets from the secrets map (delete keys
like `channel:${oldId}:botToken` and `channel:${oldId}:authDir`) while adding
the new `channel:${channel.id}:...` entries; apply the same pruning change to
the second occurrence of this pattern (the other this.store.update block around
the 892-910 region).

In `@apps/controller/static/runtime-plugins/whatsapp/package.json`:
- Around line 7-10: The package.json "openclaw.extensions" currently points to
"./index.ts" but the plugin isn't compiled, so Node will fail to load it; add a
build step to transpile TypeScript to JavaScript (e.g., add a "build" script
using tsc or an equivalent bundler), ensure the compiled output includes an
"index.js" at the same path, and update package.json or the
"openclaw.extensions" entry if you change the output path; specifically modify
the plugin's package.json to add a "scripts.build" (and run it in CI) and ensure
the source entry referenced by openclaw (the "extensions" array) points to the
produced index.js rather than index.ts.

In `@apps/controller/static/runtime-plugins/whatsapp/src/channel.ts`:
- Around line 246-264: listActions currently may add "poll" to the returned set
via createActionGate(cfg.channels.whatsapp.actions) but supportsAction and
handleAction only accept "react", causing a mismatch; either remove "poll" from
listActions or implement poll support. Fix by updating listActions in the
whatsapp channel to not add "poll" (remove gate("poll") / actions.add("poll"))
if you do not intend to support polls, or by extending supportsAction to return
true for "poll" and adding a poll handling branch in handleAction (similar to
the existing react branch) that validates params, performs the poll action
against the provider, and throws a clear error including meta.id on unsupported
action. Ensure consistency between listActions, supportsAction, and handleAction
for ChannelMessageActionName values so "poll" is only present if fully
supported.
- Around line 407-427: buildAccountSnapshot currently forces configured: true
which contradicts the linked check and makes resolveAccountState report "linked"
for unlinked accounts; change buildAccountSnapshot (the function that calls
getWhatsAppRuntime().channel.whatsapp.webAuthExists and sets linked) to set
configured to the actual link status (e.g., configured: !!linked or configured:
linked) or otherwise derive it from account.authDir/webAuthExists instead of
hardcoding true so resolveAccountState correctly reflects the account's real
auth state.

In `@apps/desktop/main/services/launchd-bootstrap.ts`:
- Around line 686-702: Currently openclawBinPath and openclawExtensionsDir point
at .tmp/sidecars/openclaw while openclawPath still points at
openclaw-runtime/node_modules, causing mismatched installs; unify by deriving
all OpenClaw locations from a single sidecar root variable (e.g., const
openclawSidecarRoot = path.join(repoRoot, ".tmp", "sidecars", "openclaw")) and
then set openclawBinPath, openclawExtensionsDir and openclawPath from that root
(e.g., openclawPath -> path.join(openclawSidecarRoot, "node_modules",
"openclaw", "openclaw.mjs")), and replace existing hardcoded paths in the return
object so controller and gateway use the same OpenClaw install.
- Around line 656-662: resolveLaunchdPaths() is computing openclawSidecarRoot
from ~/.nexu which differs from the runtime manifest resolver; update
resolveLaunchdPaths to accept (or derive) the same runtime/userData root used by
the manifest resolver (the same runtimeRoot value) and build openclawBinPath and
openclawExtensionsDir from that shared root instead of openclawSidecarRoot
derived from ~/.nexu; adjust the function signature/call sites to pass through
the runtimeRoot so openclawBinPath and openclawExtensionsDir reference the exact
packaged sidecar the manifests.ts resolver uses.

In `@apps/web/src/components/channel-setup/telegram-setup-view.tsx`:
- Around line 22-23: The hard-coded English strings in telegram-setup-view.tsx
(all toast messages, headings, helper text, step descriptions, labels, and
button text used in the Telegram setup UI and in functions that call
toast.error) must be moved to i18n translation keys: wrap the component with
react-i18next's useTranslation and replace every literal string (including the
toast.error("Telegram bot token is required") call and all UI text in the
component) with t('channelSetup.telegram.<descriptive_key>') lookups; add
corresponding keys/values to the appropriate locale resource files, and ensure
the toast calls use those translated strings (e.g.,
toast.error(t('channelSetup.telegram.tokenRequired'))), so the entire onboarding
experience is localized.
- Around line 32-37: The toast currently displays raw error.message from the
Telegram connect flow (in telegram-setup-view.tsx around the if (error || !data)
block), which can leak bot tokens or upstream URLs; change this to always show a
generic user-facing message (e.g. "Failed to connect to Telegram") or map only
from a strict allowlist of safe error keys before touching the toast, and ensure
any full error details are only logged to a secure developer log (not shown to
the user) or sent to server-side telemetry instead; update the logic that
computes message (the variable currently derived from error.message) and the
toast.error call accordingly while keeping any sensitive error content out of
the UI.

In `@apps/web/src/components/channel-setup/whatsapp-setup-view.tsx`:
- Around line 44-73: finalizeConnect currently treats a non-terminal `/qr-wait`
response as an immediate error and lets its async work continue after the modal
is closed; update finalizeConnect (and the related polling flow around
postApiV1ChannelsWhatsappConnect) to treat responses where connected === false
as a cancellable polling step (do not setPhase("error") or drop QR prematurely),
implement cancellation via an AbortController or by checking mountedRef.current
before any state mutations, and ensure any pending promise early-returns when
cancelled; specifically, modify the code path that inspects the API
error/response from postApiV1ChannelsWhatsappConnect to continue polling on
qr-wait responses, only transition to "error" on real terminal errors, and guard
all setPhase/setQrUrl/onConnected calls with mountedRef.current or abort checks
to prevent background updates after the modal is closed.

In `@apps/web/src/pages/channels.tsx`:
- Around line 452-475: The Telegram card renders several hard-coded English
strings; update the JSX block that checks platform === "telegram" and
telegramBotUrl to use the i18n translator (t(...)) for all user-facing text
(title "Open in Telegram", description lines, button label "Open Bot") instead
of raw strings, keeping the same structure and components (ExternalLink, anchor
props, classNames, etc.), and add corresponding translation keys to the locale
files used by the app so the strings are available in other locales.
- Around line 334-337: The inline Telegram URL construction using
channel.botUserId should be replaced with the shared helper getChannelChatUrl to
preserve the existing Telegram Web fallback and handle missing botUserId; find
the telegramBotUrl declaration (currently using platform === "telegram" &&
channel.botUserId) and set telegramBotUrl = getChannelChatUrl(...) using the
helper's expected parameters (pass platform and/or channel object as the helper
requires) so the helper centralizes the URL logic for Telegram channels.

In `@apps/web/src/pages/home.tsx`:
- Around line 1120-1178: TelegramModal and WhatsappModal are missing the
accessibility contract that WechatQrModal uses; update both components to behave
like real dialogs by adding role="dialog" and aria-modal="true" on the outer
dialog container, give the title element a unique id and reference it with
aria-labelledby, add an accessible label (e.g. aria-label="Close dialog") to the
icon-only close button, and implement Escape key and backdrop-click handlers to
call onClose (and ensure focus is moved into and trapped within the modal on
open and restored on close similar to WechatQrModal). Use the same identifying
symbols (TelegramModal, WhatsappModal, the close button and the title div /
TelegramSetupView / WhatsappSetupView) so the changes mirror the existing
WechatQrModal accessibility behavior.

---

Nitpick comments:
In `@apps/controller/src/store/nexu-config-store.ts`:
- Around line 839-840: The code creates new channel IDs using
crypto.randomUUID() for the ChannelResponse objects (variable channel) — replace
those calls with the cuid2 generator from `@paralleldrive/cuid2` (e.g.,
createId()) so the exposed ChannelResponse IDs follow the repo’s public-id rule;
add the import for createId and update both occurrences (the channel
construction at the current snippet and the other occurrence around lines
879-880) to use createId() instead of crypto.randomUUID().

In `@apps/controller/static/runtime-plugins/whatsapp/src/channel.ts`:
- Around line 439-441: Current startup log interpolates account metadata into a
single string; change it to emit structured fields and a short message instead.
In the block using
getWhatsAppRuntime().channel.whatsapp.readWebSelfId(account.authDir) and
ctx.log?.info, build a log call that passes an object with accountId, e164, jid
(omit sensitive values if they are credentials) and a concise message like
"starting provider" rather than embedding them in the message string; update the
call site where account.accountId, e164, and jid are used (and ensure you never
log secrets) so downstream log consumers can query fields instead of parsing the
interpolated string.

In `@apps/web/lib/api/types.gen.ts`:
- Around line 1-2: The generated types file produced by `@hey-api/openapi-ts` uses
4-space indentation and single quotes; update the generator configuration for
`@hey-api/openapi-ts` to emit double quotes and a 2-space indent (e.g., set quote
style to double and tabWidth/indentation to 2 in the generator’s options or
template/prettier settings), then regenerate the output (the header comment "//
This file is auto-generated by `@hey-api/openapi-ts`" identifies the generated
artifact to target).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f8c5a64a-7061-4846-abc4-79f48521016b

📥 Commits

Reviewing files that changed from the base of the PR and between fc31260 and 33bbb44.

📒 Files selected for processing (35)
  • apps/controller/openapi.json
  • apps/controller/src/app/container.ts
  • apps/controller/src/app/env.ts
  • apps/controller/src/lib/channel-binding-compiler.ts
  • apps/controller/src/routes/channel-routes.ts
  • apps/controller/src/runtime/openclaw-runtime-plugin-writer.ts
  • apps/controller/src/runtime/sessions-runtime.ts
  • apps/controller/src/services/analytics-service.ts
  • apps/controller/src/services/channel-service.ts
  • apps/controller/src/services/openclaw-gateway-service.ts
  • apps/controller/src/store/nexu-config-store.ts
  • apps/controller/static/runtime-plugins/openclaw-weixin/src/channel.ts
  • apps/controller/static/runtime-plugins/whatsapp/index.ts
  • apps/controller/static/runtime-plugins/whatsapp/openclaw.plugin.json
  • apps/controller/static/runtime-plugins/whatsapp/package.json
  • apps/controller/static/runtime-plugins/whatsapp/src/channel.outbound.test.ts
  • apps/controller/static/runtime-plugins/whatsapp/src/channel.test.ts
  • apps/controller/static/runtime-plugins/whatsapp/src/channel.ts
  • apps/controller/static/runtime-plugins/whatsapp/src/resolve-target.test.ts
  • apps/controller/static/runtime-plugins/whatsapp/src/runtime.ts
  • apps/controller/tests/openclaw-runtime-plugin-writer.test.ts
  • apps/desktop/main/services/launchd-bootstrap.ts
  • apps/desktop/main/services/plist-generator.ts
  • apps/web/lib/api/sdk.gen.ts
  • apps/web/lib/api/types.gen.ts
  • apps/web/src/components/channel-setup/telegram-setup-view.tsx
  • apps/web/src/components/channel-setup/wechat-setup-view.tsx
  • apps/web/src/components/channel-setup/whatsapp-setup-view.tsx
  • apps/web/src/i18n/locales/en.ts
  • apps/web/src/i18n/locales/zh-CN.ts
  • apps/web/src/lib/channel-links.ts
  • apps/web/src/pages/channels.tsx
  • apps/web/src/pages/home.tsx
  • packages/shared/src/schemas/channel.ts
  • packages/shared/src/schemas/openclaw-config.ts
💤 Files with no reviewable changes (1)
  • apps/controller/static/runtime-plugins/openclaw-weixin/src/channel.ts

Comment thread apps/controller/openapi.json
Comment thread apps/controller/src/services/channel-service.ts
Comment thread apps/controller/src/services/channel-service.ts
Comment thread apps/controller/src/store/nexu-config-store.ts
Comment thread apps/controller/static/runtime-plugins/whatsapp/package.json
Comment thread apps/web/src/components/channel-setup/telegram-setup-view.tsx Outdated
Comment thread apps/web/src/components/channel-setup/whatsapp-setup-view.tsx
Comment thread apps/web/src/pages/channels.tsx
Comment thread apps/web/src/pages/channels.tsx
Comment thread apps/web/src/pages/home.tsx
…p-channels

# Conflicts:
#	apps/desktop/main/services/launchd-bootstrap.ts
#	apps/desktop/main/services/plist-generator.ts
#	apps/web/lib/api/sdk.gen.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/controller/openapi.json (1)

2840-2847: ⚠️ Potential issue | 🟠 Major

Keep the shared channelType schema in sync with this contract.

This spec now publishes telegram and whatsapp, but packages/shared/src/schemas/channel.ts:1-12 and packages/shared/src/schemas/channel.ts:82-104 still constrain channelType to slack | discord | feishu | wechat. Any path still validating or persisting through that shared schema will reject the new channel types.

Representative source fix
 export const channelTypeSchema = z.enum([
   "slack",
   "discord",
   "feishu",
   "wechat",
+  "telegram",
+  "whatsapp",
 ]);

After that, regenerate the OpenAPI and SDK artifacts so the contract and shared validators match again.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/openapi.json` around lines 2840 - 2847, The shared
channelType schema is out of sync with the OpenAPI contract — update the
channelType enum in packages/shared/src/schemas/channel.ts (both the schema
definition and the exported TypeScript union/type locations referencing
channelType) to include "telegram" and "whatsapp" alongside the existing values,
ensure any validators/types that reference channelType are updated accordingly,
then regenerate the OpenAPI and SDK artifacts so the spec and shared
validators/types match.
♻️ Duplicate comments (3)
apps/controller/static/runtime-plugins/whatsapp/src/channel.ts (2)

407-427: ⚠️ Potential issue | 🟠 Major

Use the real link status for configured.

buildAccountSnapshot() computes linked and then hardcodes configured: true. That makes resolveAccountState() report "linked" for accounts whose auth state is actually missing.

Representative fix
-      const linked = await getWhatsAppRuntime().channel.whatsapp.webAuthExists(account.authDir);
+      const linked = account.authDir
+        ? await getWhatsAppRuntime().channel.whatsapp.webAuthExists(account.authDir)
+        : false;
       return {
         accountId: account.accountId,
         name: account.name,
         enabled: account.enabled,
-        configured: true,
+        configured: linked,
         linked,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/static/runtime-plugins/whatsapp/src/channel.ts` around lines
407 - 427, In buildAccountSnapshot, configured is hardcoded to true which
misleads resolveAccountState; change configured to reflect the actual link
status (use the computed linked value or an explicit check like webAuthExists
result) so buildAccountSnapshot returns configured: linked (or equivalent) and
let resolveAccountState correctly return "linked" or "not linked".

246-264: ⚠️ Potential issue | 🟡 Minor

Don't advertise poll as a message action yet.

listActions() can return "poll", but supportsAction() and handleAction() still reject anything except "react". That leaves callers with an action the plugin immediately errors on.

Representative fix
       if (gate("reactions")) {
         actions.add("react");
       }
-      if (gate("polls")) {
-        actions.add("poll");
-      }
       return Array.from(actions);

If poll is meant to be a real message action here, supportsAction() and handleAction() need the matching branch instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/static/runtime-plugins/whatsapp/src/channel.ts` around lines
246 - 264, listActions returns "poll" but supportsAction and handleAction only
accept "react", causing callers to receive an advertised action that will throw;
either stop advertising "poll" or implement poll handling. Update either
listActions (in the same channel.ts code) to omit adding "poll" from the actions
Set when gate("polls") is true, or modify supportsAction to return action ===
"react" || action === "poll" and extend handleAction to branch on action ===
"poll" (or delegate to a new handlePoll function) and avoid throwing for poll;
reference the functions listActions, supportsAction, handleAction and the
meta.id error message to keep behavior consistent.
apps/controller/openapi.json (1)

3359-3376: ⚠️ Potential issue | 🟠 Major

Make these new POST bodies required in the spec.

Omitting requestBody.required here makes the entire JSON body optional, even though botToken / accountId inside it are mandatory. Please fix the source that generates openapi.json, then regenerate.

Representative contract fix
         "requestBody": {
+          "required": true,
           "content": {
In OpenAPI 3.1, what is the default value of `requestBody.required`, and does omitting it make the entire request body optional?

Also applies to: 3703-3720, 3774-3791

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/openapi.json` around lines 3359 - 3376, The OpenAPI fragment
omits requestBody.required which leaves the entire JSON body optional despite
properties like "botToken" and "accountId" being required; update the OpenAPI
generation logic to set requestBody.required = true for the affected POST
operations (the requestBody objects that contain "botToken" and "accountId"
schemas), regenerate openapi.json, and ensure the regenerated spec includes
requestBody.required: true for the entries around the duplicated ranges (the
ones shown with "botToken" and the other POST bodies at the noted ranges).
🧹 Nitpick comments (1)
apps/controller/src/store/nexu-config-store.ts (1)

844-844: Consider using cuid2 for public channel IDs.

The coding guidelines specify using @paralleldrive/cuid2 for public IDs. Both new methods use crypto.randomUUID(), which is consistent with all other connect methods in this file but technically violates the guideline. If alignment with guidelines is desired, this would be a file-wide refactor affecting all ID generation.

As per coding guidelines: "Public IDs: use cuid2 (@paralleldrive/cuid2), never expose pk".

Also applies to: 894-894

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/src/store/nexu-config-store.ts` at line 844, The code is
using crypto.randomUUID() for public channel IDs (e.g. the object property "id:
crypto.randomUUID()") which conflicts with the guideline to use
`@paralleldrive/cuid2` for public IDs; replace the crypto.randomUUID() calls used
to generate public IDs with the cuid2 generator (import { createId } from
'@paralleldrive/cuid2') and use createId() wherever "id: crypto.randomUUID()"
appears (and the other similar occurrences around the second instance) so public
channel IDs are generated with cuid2; ensure imports are added and remove or
keep crypto import only if still needed for private/internal IDs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/controller/static/runtime-plugins/whatsapp/src/channel.ts`:
- Around line 437-448: The startup log in startAccount leaks WhatsApp
identifiers by logging e164 and jid returned from
getWhatsAppRuntime().channel.whatsapp.readWebSelfId(account.authDir); update the
ctx.log?.info invocation in startAccount to omit e164 and jid and log only
non-identifying context (e.g., channel: "whatsapp" and accountId:
account.accountId) so self IDs are not written to controller logs.

---

Outside diff comments:
In `@apps/controller/openapi.json`:
- Around line 2840-2847: The shared channelType schema is out of sync with the
OpenAPI contract — update the channelType enum in
packages/shared/src/schemas/channel.ts (both the schema definition and the
exported TypeScript union/type locations referencing channelType) to include
"telegram" and "whatsapp" alongside the existing values, ensure any
validators/types that reference channelType are updated accordingly, then
regenerate the OpenAPI and SDK artifacts so the spec and shared validators/types
match.

---

Duplicate comments:
In `@apps/controller/openapi.json`:
- Around line 3359-3376: The OpenAPI fragment omits requestBody.required which
leaves the entire JSON body optional despite properties like "botToken" and
"accountId" being required; update the OpenAPI generation logic to set
requestBody.required = true for the affected POST operations (the requestBody
objects that contain "botToken" and "accountId" schemas), regenerate
openapi.json, and ensure the regenerated spec includes requestBody.required:
true for the entries around the duplicated ranges (the ones shown with
"botToken" and the other POST bodies at the noted ranges).

In `@apps/controller/static/runtime-plugins/whatsapp/src/channel.ts`:
- Around line 407-427: In buildAccountSnapshot, configured is hardcoded to true
which misleads resolveAccountState; change configured to reflect the actual link
status (use the computed linked value or an explicit check like webAuthExists
result) so buildAccountSnapshot returns configured: linked (or equivalent) and
let resolveAccountState correctly return "linked" or "not linked".
- Around line 246-264: listActions returns "poll" but supportsAction and
handleAction only accept "react", causing callers to receive an advertised
action that will throw; either stop advertising "poll" or implement poll
handling. Update either listActions (in the same channel.ts code) to omit adding
"poll" from the actions Set when gate("polls") is true, or modify supportsAction
to return action === "react" || action === "poll" and extend handleAction to
branch on action === "poll" (or delegate to a new handlePoll function) and avoid
throwing for poll; reference the functions listActions, supportsAction,
handleAction and the meta.id error message to keep behavior consistent.

---

Nitpick comments:
In `@apps/controller/src/store/nexu-config-store.ts`:
- Line 844: The code is using crypto.randomUUID() for public channel IDs (e.g.
the object property "id: crypto.randomUUID()") which conflicts with the
guideline to use `@paralleldrive/cuid2` for public IDs; replace the
crypto.randomUUID() calls used to generate public IDs with the cuid2 generator
(import { createId } from '@paralleldrive/cuid2') and use createId() wherever
"id: crypto.randomUUID()" appears (and the other similar occurrences around the
second instance) so public channel IDs are generated with cuid2; ensure imports
are added and remove or keep crypto import only if still needed for
private/internal IDs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fca8a926-439f-4532-b697-b1b297920fa1

📥 Commits

Reviewing files that changed from the base of the PR and between 33bbb44 and 6969442.

📒 Files selected for processing (9)
  • apps/controller/openapi.json
  • apps/controller/src/app/container.ts
  • apps/controller/src/store/nexu-config-store.ts
  • apps/controller/static/runtime-plugins/whatsapp/src/channel.ts
  • apps/desktop/main/services/launchd-bootstrap.ts
  • apps/web/lib/api/sdk.gen.ts
  • apps/web/lib/api/types.gen.ts
  • apps/web/src/i18n/locales/en.ts
  • apps/web/src/i18n/locales/zh-CN.ts
✅ Files skipped from review due to trivial changes (2)
  • apps/web/src/i18n/locales/en.ts
  • apps/web/lib/api/types.gen.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/main/services/launchd-bootstrap.ts

Comment thread apps/controller/static/runtime-plugins/whatsapp/src/channel.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
apps/controller/src/services/channel-service.ts (2)

889-960: Minor: while (true) loop relies on internal exits.

The loop has proper timeout handling via Promise.race with a deadline, and all paths eventually return. The logic correctly handles 401 (logged out) and 515 (retry once) error codes. The structure is sound, though while (Date.now() < deadline) would make the exit condition more explicit.

♻️ Optional: explicit loop condition
-    while (true) {
+    while (Date.now() < deadline) {
       const remaining = deadline - Date.now();
-      if (remaining <= 0) {
-        return {
-          connected: false,
-          message:
-            "Still waiting for the QR scan. Let me know when you've scanned it.",
-          accountId,
-        };
-      }

Then add a fallback return after the loop for clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/src/services/channel-service.ts` around lines 889 - 960, The
infinite loop uses while (true) and relies on internal returns; change it to an
explicit time-bound loop like while (Date.now() < deadline) around the block
that awaits Promise.race so the exit condition is clear, keep the existing
Promise.race/timeout logic and all existing handling for login.error (including
checks against WHATSAPP_LOGGED_OUT_STATUS, calls to rmSync(login.authDir...),
resetActiveWhatsappLogin(accountId, ...), restartWhatsappLoginSocket(login,
runtime) and isWhatsappLoginFresh(login)), preserve the login.connected branch
(setting login.preserveAuthDirOnReset), and add a single fallback return after
the loop that returns { connected: false, message: "Still waiting for the QR
scan. Let me know when you've scanned it.", accountId } to cover any
fallthrough.

778-866: Add a code comment explaining why auth is reset on every QR start.

The logging at line 1078–1081 is properly structured, but the code lacks a comment explaining the design rationale. The method unconditionally calls resetWhatsAppDefaultLoginState which deletes the auth directory if it exists, forcing a fresh QR scan. Consider adding a comment clarifying whether this is intentional (e.g., to handle stale/corrupt sessions) or if users should reconnect without re-scanning.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/src/services/channel-service.ts` around lines 778 - 866, The
method whatsappQrStart currently calls resetWhatsAppDefaultLoginState(...)
unconditionally which deletes existing auth and forces a fresh QR; add a concise
code comment above that call (in whatsappQrStart) explaining the rationale
(e.g., to avoid stale or corrupted credential state, ensure clean authDir before
creating a new login session, and prevent mismatched socket/auth state) and note
any user-facing consequence (users must re-scan) or future TODO if you intend to
allow reuse; reference resetWhatsAppDefaultLoginState,
DEFAULT_WHATSAPP_ACCOUNT_ID, and the authDir/login session logic so reviewers
can see why the reset is necessary.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/controller/src/routes/channel-routes.ts`:
- Around line 324-358: The leading comment "WeChat QR login flow" above the
WhatsApp endpoint is stale and misleading; update or replace it to accurately
describe the code that follows (e.g., "WhatsApp QR login flow") where the
app.openapi(createRoute(...)) defines the POST route
"/api/v1/channels/whatsapp/qr-start" (and any adjacent WhatsApp routes),
ensuring nearby comments reflect that the WeChat flow actually begins later
around the WeChat handlers.

In `@apps/controller/src/services/channel-service.ts`:
- Around line 443-468: The loadWhatsappRuntimeModules function currently assumes
minified export names (sessionModule.t/i/r/n) exist; add runtime validation
after importing sessionModule to verify each expected export exists and is of
the correct type (e.g., typeof sessionModule.t === "function" for
createWaSocket, likewise for i, r, n) and if any check fails throw a clear,
descriptive error that includes the actual keys present on the imported module
and which expected export(s) were missing or invalid; reference sessionModule
and the mapping to createWaSocket/waitForWaConnection/getStatusCode/formatError
so reviewers can locate and update the validation and error message.

---

Nitpick comments:
In `@apps/controller/src/services/channel-service.ts`:
- Around line 889-960: The infinite loop uses while (true) and relies on
internal returns; change it to an explicit time-bound loop like while
(Date.now() < deadline) around the block that awaits Promise.race so the exit
condition is clear, keep the existing Promise.race/timeout logic and all
existing handling for login.error (including checks against
WHATSAPP_LOGGED_OUT_STATUS, calls to rmSync(login.authDir...),
resetActiveWhatsappLogin(accountId, ...), restartWhatsappLoginSocket(login,
runtime) and isWhatsappLoginFresh(login)), preserve the login.connected branch
(setting login.preserveAuthDirOnReset), and add a single fallback return after
the loop that returns { connected: false, message: "Still waiting for the QR
scan. Let me know when you've scanned it.", accountId } to cover any
fallthrough.
- Around line 778-866: The method whatsappQrStart currently calls
resetWhatsAppDefaultLoginState(...) unconditionally which deletes existing auth
and forces a fresh QR; add a concise code comment above that call (in
whatsappQrStart) explaining the rationale (e.g., to avoid stale or corrupted
credential state, ensure clean authDir before creating a new login session, and
prevent mismatched socket/auth state) and note any user-facing consequence
(users must re-scan) or future TODO if you intend to allow reuse; reference
resetWhatsAppDefaultLoginState, DEFAULT_WHATSAPP_ACCOUNT_ID, and the
authDir/login session logic so reviewers can see why the reset is necessary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 19fb15a1-ec2f-4e48-b0f5-435646358671

📥 Commits

Reviewing files that changed from the base of the PR and between 6969442 and b4db2a9.

📒 Files selected for processing (12)
  • apps/controller/openapi.json
  • apps/controller/src/routes/channel-routes.ts
  • apps/controller/src/services/channel-service.ts
  • apps/controller/static/runtime-plugins/whatsapp/src/channel.ts
  • apps/web/lib/api/sdk.gen.ts
  • apps/web/lib/api/types.gen.ts
  • apps/web/src/components/channel-setup/telegram-setup-view.tsx
  • apps/web/src/components/channel-setup/whatsapp-setup-view.tsx
  • apps/web/src/i18n/locales/en.ts
  • apps/web/src/i18n/locales/zh-CN.ts
  • apps/web/src/pages/channels.tsx
  • apps/web/src/pages/home.tsx
🚧 Files skipped from review as they are similar to previous changes (9)
  • apps/web/src/components/channel-setup/telegram-setup-view.tsx
  • apps/web/src/pages/channels.tsx
  • apps/web/src/pages/home.tsx
  • apps/web/src/i18n/locales/en.ts
  • apps/web/src/components/channel-setup/whatsapp-setup-view.tsx
  • apps/web/lib/api/sdk.gen.ts
  • apps/controller/openapi.json
  • apps/controller/static/runtime-plugins/whatsapp/src/channel.ts
  • apps/web/lib/api/types.gen.ts

Comment thread apps/controller/src/routes/channel-routes.ts Outdated
Comment thread apps/controller/src/services/channel-service.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
apps/controller/src/services/channel-service.ts (3)

1066-1090: Consider extracting magic numbers to constants.

Lines 1067 and 1083 use inline numeric values. For consistency with the other timeout constants defined at the top of the file, consider extracting these:

const WHATSAPP_READINESS_TIMEOUT_MS = 45_000;
const WHATSAPP_READINESS_POLL_INTERVAL_MS = 1_500;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/src/services/channel-service.ts` around lines 1066 - 1090,
Extract the magic numbers in waitForWhatsappReady into named constants: replace
45_000 with WHATSAPP_READINESS_TIMEOUT_MS and 1_500 with
WHATSAPP_READINESS_POLL_INTERVAL_MS (define them near the other timeout
constants at top of the file), then update the function (method
waitForWhatsappReady) to use these constants for deadline calculation and the
sleep(...) call to keep consistency with existing timeout configuration.

911-911: Consider caching WhatsApp runtime modules.

loadWhatsappRuntimeModules involves file system operations and dynamic imports. If whatsappQrWait is called multiple times (e.g., client polling), this redundant I/O could impact performance. Consider caching the loaded modules at the service instance level.

Suggested approach
// Add private field
private whatsappRuntime: WhatsappRuntimeModules | null = null;

private async getWhatsappRuntime(): Promise<WhatsappRuntimeModules> {
  if (!this.whatsappRuntime) {
    this.whatsappRuntime = await loadWhatsappRuntimeModules(this.env);
  }
  return this.whatsappRuntime;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/src/services/channel-service.ts` at line 911, The call to
loadWhatsappRuntimeModules in whatsappQrWait causes repeated FS and dynamic
import work; add an instance-level cache and accessor to avoid reloading: add a
private field (e.g., whatsappRuntime: WhatsappRuntimeModules | null = null) on
the service, implement a private async getWhatsappRuntime() that sets
whatsappRuntime = await loadWhatsappRuntimeModules(this.env) the first time and
returns the cached object thereafter, and update whatsappQrWait (and any other
places calling loadWhatsappRuntimeModules) to call getWhatsappRuntime() instead
of loadWhatsappRuntimeModules(this.env).

555-561: Add tracing annotations for methods with monitoring value.

Per coding guidelines, methods with monitoring value should use @Trace/@Span annotations. The channel connection methods (connectTelegram, connectWhatsapp, whatsappQrStart, whatsappQrWait, etc.) involve external API calls and would benefit from distributed tracing for debugging and performance monitoring.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/src/services/channel-service.ts` around lines 555 - 561, The
channel connection methods lack tracing annotations; add `@Trace` or `@Span`
decorators to the public methods that perform external calls (e.g.,
connectTelegram, connectWhatsapp, whatsappQrStart, whatsappQrWait and any other
channel connect methods in ChannelService) so they emit distributed-tracing
spans; import the tracing decorators from the project's tracing/tracer module,
place an appropriate `@Trace/`@Span on each method, include contextual attributes
(channel type, channelId or phone) and ensure exceptions are recorded on the
span so errors are visible in monitoring.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/controller/src/services/channel-service.ts`:
- Line 716: The polling deadline currently uses a hardcoded 500_000ms (deadline
= Date.now() + 500_000) which exceeds the QR code TTL; change the deadline
computation to use the WECHAT_LOGIN_TTL_MS constant (e.g., deadline = Date.now()
+ WECHAT_LOGIN_TTL_MS) in the function in channel-service.ts so polling stops
when the QR TTL elapses (or use Math.min if you need an upper bound), ensuring
the loop’s timeout aligns with WECHAT_LOGIN_TTL_MS.

---

Nitpick comments:
In `@apps/controller/src/services/channel-service.ts`:
- Around line 1066-1090: Extract the magic numbers in waitForWhatsappReady into
named constants: replace 45_000 with WHATSAPP_READINESS_TIMEOUT_MS and 1_500
with WHATSAPP_READINESS_POLL_INTERVAL_MS (define them near the other timeout
constants at top of the file), then update the function (method
waitForWhatsappReady) to use these constants for deadline calculation and the
sleep(...) call to keep consistency with existing timeout configuration.
- Line 911: The call to loadWhatsappRuntimeModules in whatsappQrWait causes
repeated FS and dynamic import work; add an instance-level cache and accessor to
avoid reloading: add a private field (e.g., whatsappRuntime:
WhatsappRuntimeModules | null = null) on the service, implement a private async
getWhatsappRuntime() that sets whatsappRuntime = await
loadWhatsappRuntimeModules(this.env) the first time and returns the cached
object thereafter, and update whatsappQrWait (and any other places calling
loadWhatsappRuntimeModules) to call getWhatsappRuntime() instead of
loadWhatsappRuntimeModules(this.env).
- Around line 555-561: The channel connection methods lack tracing annotations;
add `@Trace` or `@Span` decorators to the public methods that perform external calls
(e.g., connectTelegram, connectWhatsapp, whatsappQrStart, whatsappQrWait and any
other channel connect methods in ChannelService) so they emit
distributed-tracing spans; import the tracing decorators from the project's
tracing/tracer module, place an appropriate `@Trace/`@Span on each method, include
contextual attributes (channel type, channelId or phone) and ensure exceptions
are recorded on the span so errors are visible in monitoring.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b8235dcc-4807-4c2c-ba13-c0f8b493e76a

📥 Commits

Reviewing files that changed from the base of the PR and between b4db2a9 and 5f73919.

📒 Files selected for processing (2)
  • apps/controller/src/routes/channel-routes.ts
  • apps/controller/src/services/channel-service.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/controller/src/routes/channel-routes.ts

Comment thread apps/controller/src/services/channel-service.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
apps/controller/src/services/channel-service.ts (1)

708-717: ⚠️ Potential issue | 🟡 Minor

Cap WeChat polling to the QR's actual TTL.

This starts a fresh 500s deadline even when the session is already partway through its 5-minute lifetime, so wechatQrWait() can keep polling after the QR should already be expired. Use activeLogin.startedAt + WECHAT_LOGIN_TTL_MS instead.

🕒 Proposed fix
-    const deadline = Date.now() + 500_000;
+    const deadline = activeLogin.startedAt + WECHAT_LOGIN_TTL_MS;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/src/services/channel-service.ts` around lines 708 - 717, The
polling deadline currently resets to Date.now() + 500_000 which can extend
polling past the QR TTL; change the deadline calculation in the wechat QR wait
loop to use the QR's actual expiry (activeLogin.startedAt + WECHAT_LOGIN_TTL_MS)
and ensure you clamp it to at least the current time (so you don't loop if
already expired). Update the variable referenced as deadline (used in the while
loop that polls) to compare against activeLogin.startedAt + WECHAT_LOGIN_TTL_MS
instead of Date.now() + 500_000, keeping use of
activeWechatLogins.delete(sessionKey) and the existing early-return logic
intact.
🧹 Nitpick comments (1)
apps/controller/src/services/channel-service.ts (1)

676-695: Use cuid2 for the public WeChat session key.

sessionKey is returned to clients and is part of the controller's public ID surface. While randomUUID() is cryptographically sound, it diverges from the repo's established convention of using cuid2 (@paralleldrive/cuid2) for public IDs in this package.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/src/services/channel-service.ts` around lines 676 - 695, The
public sessionKey in wechatQrStart is generated with randomUUID(), but this repo
standard uses cuid2 for public IDs; replace randomUUID() with the cuid2
generator (import createId from '@paralleldrive/cuid2') and use createId() to
produce sessionKey in the wechatQrStart function, update the import list at the
top of the file to include createId, and keep the sessionKey variable name and
usage unchanged so activeWechatLogins and the returned object continue to work.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/controller/src/services/channel-service.ts`:
- Around line 828-834: The code creates a temporary login session directory via
resolveWhatsAppLoginSessionDir and then uses its nested authDir (authDir,
loginSessionId, mkdirSync) as the channel's persistent auth state, which leaves
orphaned credentials when replaced; change this to persist a stable per-account
auth path (e.g., derive a deterministic auth path using the channel/account id
instead of the transient loginSessionId) or, if you must replace the stored
authDir in connectWhatsapp(), delete the previous authDir recursively before
assigning the new one (and update resetWhatsAppDefaultLoginState() to clear the
whole stored tree), referencing resolveWhatsAppLoginSessionDir,
connectWhatsapp(), resetWhatsAppDefaultLoginState(), authDir and loginSessionId
to locate the affected code.
- Around line 971-972: connectWhatsapp() must take responsibility for preserving
and rolling back auth-dir state: when preparing to store login.authDir
(including when login.connected is already true), set
login.preserveAuthDirOnReset = true before touching authDir and ensure you clear
it and call resetActiveWhatsappLogin() on failure; wrap the critical section
(including waitForWhatsappReady()) in try/catch/finally so any thrown error
triggers the rollback (unset preserveAuthDirOnReset and
resetActiveWhatsappLogin()), and remove reliance on whatsappQrWait() having set
preserveAuthDirOnReset first. Ensure the same change is applied to the other
block around lines 988-1008.
- Around line 736-748: The code uses status.ilink_bot_id without verifying
normalizeAccountId(status.ilink_bot_id) returns a non-empty string, so values
like "---" produce an empty normalizedAccountId and create a bad file; before
calling writeWeChatAccount and registerWeChatAccount, compute
normalizedAccountId = normalizeAccountId(status.ilink_bot_id) and skip (or
return/log) if normalizedAccountId is empty (e.g., === "" or falsy), ensuring
you only write/register when normalizedAccountId is a valid non-empty string.

---

Duplicate comments:
In `@apps/controller/src/services/channel-service.ts`:
- Around line 708-717: The polling deadline currently resets to Date.now() +
500_000 which can extend polling past the QR TTL; change the deadline
calculation in the wechat QR wait loop to use the QR's actual expiry
(activeLogin.startedAt + WECHAT_LOGIN_TTL_MS) and ensure you clamp it to at
least the current time (so you don't loop if already expired). Update the
variable referenced as deadline (used in the while loop that polls) to compare
against activeLogin.startedAt + WECHAT_LOGIN_TTL_MS instead of Date.now() +
500_000, keeping use of activeWechatLogins.delete(sessionKey) and the existing
early-return logic intact.

---

Nitpick comments:
In `@apps/controller/src/services/channel-service.ts`:
- Around line 676-695: The public sessionKey in wechatQrStart is generated with
randomUUID(), but this repo standard uses cuid2 for public IDs; replace
randomUUID() with the cuid2 generator (import createId from
'@paralleldrive/cuid2') and use createId() to produce sessionKey in the
wechatQrStart function, update the import list at the top of the file to include
createId, and keep the sessionKey variable name and usage unchanged so
activeWechatLogins and the returned object continue to work.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 180e5f39-fe3d-4af9-815b-0262a2717677

📥 Commits

Reviewing files that changed from the base of the PR and between 5f73919 and 46131d7.

📒 Files selected for processing (4)
  • apps/controller/src/runtime/openclaw-runtime-plugin-writer.ts
  • apps/controller/src/services/channel-service.ts
  • apps/controller/tests/openclaw-runtime-plugin-writer.test.ts
  • apps/desktop/main/index.ts

Comment thread apps/controller/src/services/channel-service.ts
Comment thread apps/controller/src/services/channel-service.ts
Comment thread apps/controller/src/services/channel-service.ts
Copy link
Copy Markdown
Collaborator

@lefarcen lefarcen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

P0 — Must fix

1. Accidental openapi.json title change

"Nexu Controller API""nexu Controller API" — looks like an unintended side-effect of pnpm generate-types. Please revert.

2. WhatsApp login management complexity in channel-service.ts

The controller gained ~900 lines of low-level WhatsApp baileys socket management (activeWhatsappLogins Map, QR polling, disconnect/reconnect, session directories). This turns the stateless controller into a stateful connection manager.

The WhatsApp runtime plugin already declares gatewayMethods: ["web.login.start", "web.login.wait"] — which means the OpenClaw gateway exposes these operations over its websocket RPC. The controller should call gatewayService.request("web.login.start", ...) / gatewayService.request("web.login.wait", ...) instead of reimplementing baileys connection logic.

Current approach:

  • Breaks the controller's stateless design (global Map of active sockets)
  • Duplicates logic already in the OpenClaw WhatsApp plugin
  • Creates two competing connection managers if the gateway is also running WhatsApp

3. WeChat QR flow moved from gatewayService to channelService with hardcoded iLink API

const DEFAULT_WECHAT_BASE_URL = "https://ilinkai.weixin.qq.com";

The route handler was changed from container.gatewayService.wechatQrStart() to container.channelService.wechatQrStart(), bypassing the openclaw-weixin plugin entirely. This leaks iLink API implementation details (iLink-App-ClientVersion header, QR polling protocol) into the controller. What motivated this change? If the gateway was unreliable, a fallback with clear comments would be better than a full reimplementation.

4. isQrImageSource in whatsapp-setup-view.tsx allows arbitrary HTTP URLs as <img src>

trimmed.startsWith("http://") || trimmed.startsWith("https://")

The WeChat version only allows data:image/, which is correct. Allowing arbitrary URLs opens an XSS/tracking vector if the QR data is ever influenced by external input. Restrict to data:image/ only, or validate against a known origin.

5. Telegram botToken in request URL — verify logging

https://api.telegram.org/bot${token}/getMe is the standard Telegram pattern, but make sure the controller's HTTP request logging (pino, fetch interceptors) does not log the full URL, or the token will appear in plaintext logs.


P1 — Strongly recommended

6. 500-second HTTP request timeout for QR wait endpoints

Both whatsappQrWait (timeoutMs: 500_000) and wechatQrWait (deadline = Date.now() + 500_000) hold an HTTP connection open for ~8 minutes. Most reverse proxies and browsers time out at 30–120s. Either shorten to ~60s or switch to a short-polling pattern (frontend polls every few seconds).

7. No Telegram runtime plugin

WhatsApp has a full static/runtime-plugins/whatsapp/ plugin, but Telegram has none. The channel-binding-compiler generates a telegram channel config block, but does OpenClaw have a built-in Telegram extension to consume it? If not, connecting Telegram will appear to succeed but the bot won't actually respond. Please clarify.

8. connectWhatsapp store method doesn't persist authDir

configStore.connectWhatsapp({ accountId }) stores no authDir, but channel-binding-compiler reads secret("authDir"). Confirm the final version writes authDir into secrets — otherwise WhatsApp config will have authDir: undefined and the runtime won't find the session.


P2 — Suggestions

9. Platform order change — WhatsApp is now first

PLATFORMS array puts WhatsApp before WeChat. Given the primary user base is Chinese, WeChat probably should remain the default selection.

10. Telegram and WhatsApp setup views have no i18n

All copy is hardcoded English ("Connect Telegram Bot", "Scan WhatsApp QR", etc.). The project has a full i18n system with en.ts / zh-CN.ts — these views should use useTranslation() and locale keys like the other channel setup views.

11. TelegramModal / WhatsappModal in home.tsx duplicate WechatQrModal

All three modals share the same shell (overlay + close button + title). Extract a generic ChannelSetupModal wrapper to reduce repetition.

12. Session channel detection relies on fragile string matching

combined.includes("@g.us")

@g.us is very short and could false-match in non-WhatsApp contexts. Consider a more specific check (e.g., require the string to end with @g.us or be preceded by digits).

@Siri-Ray
Copy link
Copy Markdown
Contributor Author

Thanks, this is helpful. Point-by-point:

  1. openapi.json title change
    Accepted. This is an unintended generated diff and I will revert it.

  2. WhatsApp login management complexity in channel-service.ts
    I agree with the long-term direction: reusing OpenClaws gateway login surface would be preferable. We did try routing WhatsApp through web.login.start / web.login.wait, but in the current runtime combination that path was not stable enough to complete the QR -> 515 -> listener handoff in Nexus desktop workflow. The current controller-side logic is a compatibility layer around the existing OpenClaw runtime, not a protocol reimplementation. I do not plan to re-architect this PR back to the generic gateway path, but I agree this should be revisited once that runtime surface is stable enough to use directly.

  3. WeChat QR flow moved from gatewayService to channelService
    This was intentional. WeChat and WhatsApp were colliding on the shared generic web-login route, which made the two channels unstable when enabled together. Moving WeChat QR handling into ChannelService isolated that flow and removed the provider conflict. I agree it exposes more provider detail in the controller than ideal; if the underlying gateway path becomes reliable and provider-specific later, we should consolidate again. For this PR, the change is deliberate.

  4. isQrImageSource allows arbitrary HTTP URLs
    Accepted. This is a fair hardening suggestion. I will restrict the <img> path to data:image/ only and let URL-like values continue through QR rendering instead of remote image loading.

  5. Telegram botToken in request URL / logging
    Agreed on the concern. We are using Telegrams standard bot<token>/getMe endpoint format, but the intent is that token material never appears in logs. I will verify this path does not emit the full URL.

  6. 500-second HTTP request timeout for QR wait endpoints
    Valid design concern, but I do not consider it a blocker for this PR. The current implementation is optimized for the desktop-local workflow, where controller and web are co-located. A shorter poll window with client-side re-polling would be a cleaner follow-up, but changing the contract now would expand the surface area significantly. I would treat this as follow-up optimization rather than must-fix.

  7. No Telegram runtime plugin
    This does not appear to be an issue in the current setup. Telegram is replying successfully in the integrated runtime, which confirms the generated Telegram config is being consumed by existing OpenClaw runtime support. So this is not a missing-functionality issue for the current PR.

  8. connectWhatsapp store method doesnt persist authDir
    This was a valid risk in earlier revisions, but the current branch does persist and use authDir correctly in the final successful path. We have now verified successful QR link, listener handoff, reply flow, disconnect, and reconnect. So this comment is stale relative to the current code.

  9. Platform order change
    This is product preference rather than correctness. There is a reasonable argument for keeping WeChat ahead of WhatsApp for the current user base, but I would treat that as a UX decision, not a blocking code issue.

  10. Telegram and WhatsApp setup views have no i18n
    This has already been addressed in later commits. The setup views and related copy are now wired into the locale files.

  11. TelegramModal / WhatsappModal duplicate WechatQrModal
    Reasonable refactor suggestion, but not something I would fold into this PR unless we specifically want to do a follow-up cleanup pass. Functionally this is working; extracting a shared wrapper would be code-shape cleanup, not a bug fix.

  12. Session channel detection relies on fragile string matching
    Fair point as a hardening suggestion. That said, this has not shown up as a functional regression in the current flow, so I would classify it as a follow-up improvement rather than a blocker for merging the channel support work.

I will follow up on (1) and (4), and verify (5).

@Siri-Ray Siri-Ray merged commit 6c66f84 into main Mar 26, 2026
9 of 10 checks passed
This was referenced Mar 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants