From b3e66535036f4b71c4f46083a8e057560eb78f41 Mon Sep 17 00:00:00 2001 From: suko Date: Tue, 24 Feb 2026 21:56:49 +0100 Subject: [PATCH] fix(onboard): avoid false 'telegram plugin not available' block --- src/commands/onboard-channels.test.ts | 50 +++++++++++++++++++++++++++ src/commands/onboard-channels.ts | 14 ++++++++ src/commands/onboarding/registry.ts | 34 ++++++++++++++---- 3 files changed, 91 insertions(+), 7 deletions(-) diff --git a/src/commands/onboard-channels.test.ts b/src/commands/onboard-channels.test.ts index d263ff9c0b2..d6c0669e4fd 100644 --- a/src/commands/onboard-channels.test.ts +++ b/src/commands/onboard-channels.test.ts @@ -1,5 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; +import { createEmptyPluginRegistry } from "../plugins/registry.js"; +import { setActivePluginRegistry } from "../plugins/runtime.js"; import type { WizardPrompter } from "../wizard/prompts.js"; import { setDefaultChannelPluginRegistryForTests } from "./channel-test-helpers.js"; import { setupChannels } from "./onboard-channels.js"; @@ -42,6 +44,15 @@ vi.mock("./onboard-helpers.js", () => ({ detectBinary: vi.fn(async () => false), })); +vi.mock("./onboarding/plugin-install.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...(actual as Record), + // Allow tests to simulate an empty plugin registry during onboarding. + reloadOnboardingPluginRegistry: vi.fn(() => {}), + }; +}); + describe("setupChannels", () => { beforeEach(() => { setDefaultChannelPluginRegistryForTests(); @@ -81,6 +92,45 @@ describe("setupChannels", () => { expect(multiselect).not.toHaveBeenCalled(); }); + it("continues Telegram onboarding even when plugin registry is empty (avoids 'plugin not available' block)", async () => { + // Simulate missing registry entries (the scenario reported in #25545). + setActivePluginRegistry(createEmptyPluginRegistry()); + // Avoid accidental env-token configuration changing the prompt path. + process.env.TELEGRAM_BOT_TOKEN = ""; + + const note = vi.fn(async (_message?: string, _title?: string) => {}); + const select = vi.fn(async ({ message }: { message: string }) => { + if (message === "Select channel (QuickStart)") { + return "telegram"; + } + return "__done__"; + }); + const text = vi.fn(async () => "123:token"); + + const prompter = createPrompter({ + note, + select: select as unknown as WizardPrompter["select"], + text: text as unknown as WizardPrompter["text"], + }); + + const runtime = createExitThrowingRuntime(); + + await setupChannels({} as OpenClawConfig, runtime, prompter, { + skipConfirm: true, + quickstartDefaults: true, + }); + + // The new flow should not stop setup with a hard "plugin not available" note. + const sawHardStop = note.mock.calls.some((call) => { + const message = call[0]; + const title = call[1]; + return ( + title === "Channel setup" && String(message).trim() === "telegram plugin not available." + ); + }); + expect(sawHardStop).toBe(false); + }); + it("shows explicit dmScope config command in channel primer", async () => { const note = vi.fn(async (_message?: string, _title?: string) => {}); const select = vi.fn(async () => "__done__"); diff --git a/src/commands/onboard-channels.ts b/src/commands/onboard-channels.ts index 1ac763d9f01..32510c29f39 100644 --- a/src/commands/onboard-channels.ts +++ b/src/commands/onboard-channels.ts @@ -467,6 +467,20 @@ export async function setupChannels( workspaceDir, }); if (!getChannelPlugin(channel)) { + // Some installs/environments can fail to populate the plugin registry during onboarding, + // even for built-in channels. If the channel supports onboarding, proceed with config + // so setup isn't blocked; the gateway can still load plugins on startup. + const adapter = getChannelOnboardingAdapter(channel); + if (adapter) { + await prompter.note( + `${channel} plugin not available (continuing with onboarding). If the channel still doesn't work after setup, run \`${formatCliCommand( + "openclaw plugins list", + )}\` and \`${formatCliCommand("openclaw plugins enable " + channel)}\`, then restart the gateway.`, + "Channel setup", + ); + await refreshStatus(channel); + return true; + } await prompter.note(`${channel} plugin not available.`, "Channel setup"); return false; } diff --git a/src/commands/onboarding/registry.ts b/src/commands/onboarding/registry.ts index d3fdbef2ce9..814eab75ea2 100644 --- a/src/commands/onboarding/registry.ts +++ b/src/commands/onboarding/registry.ts @@ -1,16 +1,36 @@ import { listChannelPlugins } from "../../channels/plugins/index.js"; +import { discordOnboardingAdapter } from "../../channels/plugins/onboarding/discord.js"; +import { imessageOnboardingAdapter } from "../../channels/plugins/onboarding/imessage.js"; +import { signalOnboardingAdapter } from "../../channels/plugins/onboarding/signal.js"; +import { slackOnboardingAdapter } from "../../channels/plugins/onboarding/slack.js"; +import { telegramOnboardingAdapter } from "../../channels/plugins/onboarding/telegram.js"; +import { whatsappOnboardingAdapter } from "../../channels/plugins/onboarding/whatsapp.js"; import type { ChannelChoice } from "../onboard-types.js"; import type { ChannelOnboardingAdapter } from "./types.js"; -const CHANNEL_ONBOARDING_ADAPTERS = () => - new Map( - listChannelPlugins() - .map((plugin) => (plugin.onboarding ? ([plugin.id, plugin.onboarding] as const) : null)) - .filter((entry): entry is readonly [ChannelChoice, ChannelOnboardingAdapter] => - Boolean(entry), - ), +const BUILTIN_ONBOARDING_ADAPTERS: ChannelOnboardingAdapter[] = [ + telegramOnboardingAdapter, + whatsappOnboardingAdapter, + discordOnboardingAdapter, + slackOnboardingAdapter, + signalOnboardingAdapter, + imessageOnboardingAdapter, +]; + +const CHANNEL_ONBOARDING_ADAPTERS = () => { + const fromRegistry = listChannelPlugins() + .map((plugin) => (plugin.onboarding ? ([plugin.id, plugin.onboarding] as const) : null)) + .filter((entry): entry is readonly [ChannelChoice, ChannelOnboardingAdapter] => Boolean(entry)); + + // Fall back to built-in adapters to keep onboarding working even when the plugin registry + // fails to populate (see #25545). + const fromBuiltins = BUILTIN_ONBOARDING_ADAPTERS.map( + (adapter) => [adapter.channel, adapter] as const, ); + return new Map([...fromBuiltins, ...fromRegistry]); +}; + export function getChannelOnboardingAdapter( channel: ChannelChoice, ): ChannelOnboardingAdapter | undefined {