fix(channels): accept setup aliases for add

This commit is contained in:
Peter Steinberger
2026-04-24 07:20:32 +01:00
parent d29eaeafc1
commit b588b5a230
7 changed files with 203 additions and 0 deletions

View File

@@ -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.

View File

@@ -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 "<shared-secret>"
```
Equivalent explicit fields:
```bash
openclaw channels add --channel nextcloud-talk \
--base-url https://cloud.example.com \
--secret "<shared-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:

View File

@@ -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
```
</Step>

View File

@@ -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[];

View File

@@ -169,12 +169,16 @@ export function registerChannelsCli(program: Command) {
.option("--name <name>", "Display name for this account")
.option("--token <token>", "Channel token or credential payload")
.option("--token-file <path>", "Read channel token or credential payload from file")
.option("--secret <secret>", "Channel shared secret")
.option("--secret-file <path>", "Read channel shared secret from file")
.option("--bot-token <token>", "Bot token")
.option("--app-token <token>", "App token")
.option("--password <password>", "Channel password or login secret")
.option("--cli-path <path>", "Channel CLI path")
.option("--url <url>", "Channel setup URL")
.option("--base-url <url>", "Channel base URL")
.option("--http-url <url>", "Channel HTTP service URL")
.option("--auth-dir <path>", "Channel auth directory override")
.option("--use-env", "Use env-backed credentials when supported", false),
).action(async (opts, command) => {
await runChannelsCommand(async () => {

View File

@@ -193,6 +193,9 @@ function registerExternalChatSetupPlugin(pluginId = "@vendor/external-chat-plugi
type SignalAfterAccountConfigWritten = NonNullable<
NonNullable<ChannelPlugin["setup"]>["afterAccountConfigWritten"]
>;
type ApplyAccountConfigParams = Parameters<
NonNullable<NonNullable<ChannelPlugin["setup"]>["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());

View File

@@ -37,6 +37,7 @@ export type ChannelsAddOptions = {
} & Record<string, unknown>;
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<string, unknown> = {};
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);