From b588b5a2300794745900977d8bf5059898eb092c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 24 Apr 2026 07:20:32 +0100 Subject: [PATCH] fix(channels): accept setup aliases for add --- CHANGELOG.md | 1 + docs/channels/nextcloud-talk.md | 25 +++++ docs/channels/whatsapp.md | 7 ++ src/channels/plugins/types.core.ts | 3 + src/cli/channels-cli.ts | 4 + src/commands/channels.add.test.ts | 151 +++++++++++++++++++++++++++++ src/commands/channels/add.ts | 12 +++ 7 files changed, 203 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d08b95b248..56d6ede72b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai - Codex harness: route native `request_user_input` prompts back to the originating chat, preserve queued follow-up answers, and honor newer app-server command approval amendment decisions. - Codex status: report Codex CLI OAuth as `oauth (codex-cli)` for native `codex/*` sessions instead of showing unknown auth. Fixes #70688. Thanks @jb510. - Codex harness/context-engine: redact context-engine assembly failures before logging, so fallback warnings do not serialize raw error objects. (#70809) Thanks @jalehman. +- Channels/CLI: accept explicit shared-secret, base-URL, and auth-directory setup flags, and map legacy Nextcloud Talk `--url`/`--token` add commands to the bundled plugin setup input. Fixes #61759 and #61923. - WhatsApp/onboarding: keep first-run setup entry loading off the Baileys runtime dependency path, so packaged QuickStart installs can show WhatsApp setup before runtime deps are staged. Fixes #70932. - Block streaming: suppress final assembled text after partial block-delivery aborts when the already-sent text chunks exactly cover the final reply, preventing duplicate replies without dropping unrelated short messages. Fixes #70921. - Codex harness/Windows: resolve npm-installed `codex.cmd` shims through PATHEXT before starting the native app-server, so `codex/*` models work without a manual `.exe` shim. Fixes #70913. diff --git a/docs/channels/nextcloud-talk.md b/docs/channels/nextcloud-talk.md index fc6c5e5a2fd..44332172d6f 100644 --- a/docs/channels/nextcloud-talk.md +++ b/docs/channels/nextcloud-talk.md @@ -44,6 +44,31 @@ Details: [Plugins](/tools/plugin) 4. Configure OpenClaw: - Config: `channels.nextcloud-talk.baseUrl` + `channels.nextcloud-talk.botSecret` - Or env: `NEXTCLOUD_TALK_BOT_SECRET` (default account only) + + CLI setup: + + ```bash + openclaw channels add --channel nextcloud-talk \ + --url https://cloud.example.com \ + --token "" + ``` + + Equivalent explicit fields: + + ```bash + openclaw channels add --channel nextcloud-talk \ + --base-url https://cloud.example.com \ + --secret "" + ``` + + File-backed secret: + + ```bash + openclaw channels add --channel nextcloud-talk \ + --base-url https://cloud.example.com \ + --secret-file /path/to/nextcloud-talk-secret + ``` + 5. Restart the gateway (or finish setup). Minimal config: diff --git a/docs/channels/whatsapp.md b/docs/channels/whatsapp.md index a49c0f01259..22034b1be29 100644 --- a/docs/channels/whatsapp.md +++ b/docs/channels/whatsapp.md @@ -64,6 +64,13 @@ openclaw channels login --channel whatsapp ```bash openclaw channels login --channel whatsapp --account work +``` + + To attach an existing/custom WhatsApp Web auth directory before login: + +```bash +openclaw channels add --channel whatsapp --account work --auth-dir /path/to/wa-auth +openclaw channels login --channel whatsapp --account work ``` diff --git a/src/channels/plugins/types.core.ts b/src/channels/plugins/types.core.ts index 75ad434d8f7..b98840c7602 100644 --- a/src/channels/plugins/types.core.ts +++ b/src/channels/plugins/types.core.ts @@ -86,6 +86,8 @@ export type ChannelSetupInput = { token?: string; privateKey?: string; tokenFile?: string; + secret?: string; + secretFile?: string; botToken?: string; appToken?: string; signalNumber?: string; @@ -115,6 +117,7 @@ export type ChannelSetupInput = { initialSyncLimit?: number; ship?: string; url?: string; + baseUrl?: string; relayUrls?: string; code?: string; groupChannels?: string[]; diff --git a/src/cli/channels-cli.ts b/src/cli/channels-cli.ts index 890fac2e7a5..9d876374344 100644 --- a/src/cli/channels-cli.ts +++ b/src/cli/channels-cli.ts @@ -169,12 +169,16 @@ export function registerChannelsCli(program: Command) { .option("--name ", "Display name for this account") .option("--token ", "Channel token or credential payload") .option("--token-file ", "Read channel token or credential payload from file") + .option("--secret ", "Channel shared secret") + .option("--secret-file ", "Read channel shared secret from file") .option("--bot-token ", "Bot token") .option("--app-token ", "App token") .option("--password ", "Channel password or login secret") .option("--cli-path ", "Channel CLI path") .option("--url ", "Channel setup URL") + .option("--base-url ", "Channel base URL") .option("--http-url ", "Channel HTTP service URL") + .option("--auth-dir ", "Channel auth directory override") .option("--use-env", "Use env-backed credentials when supported", false), ).action(async (opts, command) => { await runChannelsCommand(async () => { diff --git a/src/commands/channels.add.test.ts b/src/commands/channels.add.test.ts index 8003ed0668c..6dc22fe8340 100644 --- a/src/commands/channels.add.test.ts +++ b/src/commands/channels.add.test.ts @@ -193,6 +193,9 @@ function registerExternalChatSetupPlugin(pluginId = "@vendor/external-chat-plugi type SignalAfterAccountConfigWritten = NonNullable< NonNullable["afterAccountConfigWritten"] >; +type ApplyAccountConfigParams = Parameters< + NonNullable["applyAccountConfig"]> +>[0]; function createSignalPlugin( afterAccountConfigWritten: SignalAfterAccountConfigWritten, @@ -306,6 +309,154 @@ describe("channelsAddCommand", () => { expect(lifecycleMocks.onAccountConfigChanged).not.toHaveBeenCalled(); }); + it("maps legacy Nextcloud Talk add flags to setup input fields", async () => { + const applyAccountConfig = vi.fn(({ cfg, input }) => ({ + ...cfg, + channels: { + ...cfg.channels, + "nextcloud-talk": { + enabled: true, + baseUrl: input.baseUrl, + botSecret: input.secret, + botSecretFile: input.secretFile, + }, + }, + })); + setActivePluginRegistry( + createTestRegistry([ + { + pluginId: "nextcloud-talk", + plugin: { + ...createChannelTestPluginBase({ + id: "nextcloud-talk", + label: "Nextcloud Talk", + }), + setup: { applyAccountConfig }, + }, + source: "test", + }, + ]), + ); + configMocks.readConfigFileSnapshot.mockResolvedValue({ ...baseConfigSnapshot }); + + await channelsAddCommand( + { + channel: "nextcloud-talk", + account: "default", + url: "https://cloud.example.com/", + token: "shared-secret", + }, + runtime, + { hasFlags: true }, + ); + + expect(applyAccountConfig).toHaveBeenCalledWith( + expect.objectContaining({ + input: expect.objectContaining({ + url: "https://cloud.example.com/", + token: "shared-secret", + baseUrl: "https://cloud.example.com/", + secret: "shared-secret", + }), + }), + ); + expect(configMocks.writeConfigFile).toHaveBeenCalledWith( + expect.objectContaining({ + channels: { + "nextcloud-talk": { + enabled: true, + baseUrl: "https://cloud.example.com/", + botSecret: "shared-secret", + botSecretFile: undefined, + }, + }, + }), + ); + + configMocks.writeConfigFile.mockClear(); + applyAccountConfig.mockClear(); + await channelsAddCommand( + { + channel: "nextcloud-talk", + account: "default", + url: "https://cloud.example.com", + tokenFile: "/tmp/nextcloud-secret", + }, + runtime, + { hasFlags: true }, + ); + + expect(applyAccountConfig).toHaveBeenCalledWith( + expect.objectContaining({ + input: expect.objectContaining({ + baseUrl: "https://cloud.example.com", + secretFile: "/tmp/nextcloud-secret", + }), + }), + ); + }); + + it("passes channel auth directory overrides through add setup input", async () => { + setActivePluginRegistry( + createTestRegistry([ + { + pluginId: "whatsapp", + plugin: { + ...createChannelTestPluginBase({ + id: "whatsapp", + label: "WhatsApp", + }), + setup: { + applyAccountConfig: (params: ApplyAccountConfigParams) => ({ + ...params.cfg, + channels: { + ...params.cfg.channels, + whatsapp: { + enabled: true, + accounts: { + [params.accountId]: { + enabled: true, + authDir: params.input.authDir, + }, + }, + }, + }, + }), + }, + }, + source: "test", + }, + ]), + ); + configMocks.readConfigFileSnapshot.mockResolvedValue({ ...baseConfigSnapshot }); + + await channelsAddCommand( + { + channel: "whatsapp", + account: "work", + authDir: "/tmp/openclaw-wa-auth", + }, + runtime, + { hasFlags: true }, + ); + + expect(configMocks.writeConfigFile).toHaveBeenCalledWith( + expect.objectContaining({ + channels: { + whatsapp: { + enabled: true, + accounts: { + work: { + enabled: true, + authDir: "/tmp/openclaw-wa-auth", + }, + }, + }, + }, + }), + ); + }); + it("loads external channel setup snapshots for newly installed and existing plugins", async () => { configMocks.readConfigFileSnapshot.mockResolvedValue({ ...baseConfigSnapshot }); setActivePluginRegistry(createTestRegistry()); diff --git a/src/commands/channels/add.ts b/src/commands/channels/add.ts index 0b41344bd92..f001f3c3a73 100644 --- a/src/commands/channels/add.ts +++ b/src/commands/channels/add.ts @@ -37,6 +37,7 @@ export type ChannelsAddOptions = { } & Record; const CHANNEL_ADD_CONTROL_OPTION_KEYS = new Set(["channel", "account"]); +const NEXTCLOUD_TALK_CLI_ALIASES = new Set(["nextcloud-talk", "nc-talk", "nc"]); async function resolveCatalogChannelEntry(raw: string, cfg: OpenClawConfig | null) { const trimmed = normalizeOptionalLowercaseString(raw); @@ -72,6 +73,10 @@ function parseOptionalDelimitedInput(value: unknown): string[] | undefined { return parseOptionalDelimitedEntries(typeof value === "string" ? value : undefined); } +function readOptionalString(value: unknown): string | undefined { + return typeof value === "string" && value.length > 0 ? value : undefined; +} + function buildChannelSetupInput(opts: ChannelsAddOptions): ChannelSetupInput { const input: Record = {}; for (const [key, value] of Object.entries(opts)) { @@ -81,6 +86,13 @@ function buildChannelSetupInput(opts: ChannelsAddOptions): ChannelSetupInput { input[key] = value; } + const rawChannel = readOptionalString(opts.channel)?.trim().toLowerCase(); + if (rawChannel && NEXTCLOUD_TALK_CLI_ALIASES.has(rawChannel)) { + input.baseUrl ??= readOptionalString(input.url); + input.secret ??= readOptionalString(input.token) ?? readOptionalString(input.password); + input.secretFile ??= readOptionalString(input.tokenFile); + } + input.initialSyncLimit = parseOptionalInt(opts.initialSyncLimit); input.groupChannels = parseOptionalDelimitedInput(opts.groupChannels); input.dmAllowlist = parseOptionalDelimitedInput(opts.dmAllowlist);