diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 65cab1c0e06..d63f8890cf5 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -195,6 +195,104 @@ describe("loadOpenClawPlugins", () => { expect(registry.channels.some((entry) => entry.plugin.id === "telegram")).toBe(true); }); + it("loads bundled channel plugins when channels..enabled=true", () => { + const bundledDir = makeTempDir(); + writePlugin({ + id: "telegram", + body: `export default { id: "telegram", register(api) { + api.registerChannel({ + plugin: { + id: "telegram", + meta: { + id: "telegram", + label: "Telegram", + selectionLabel: "Telegram", + docsPath: "/channels/telegram", + blurb: "telegram channel" + }, + capabilities: { chatTypes: ["direct"] }, + config: { + listAccountIds: () => [], + resolveAccount: () => ({ accountId: "default" }) + }, + outbound: { deliveryMode: "direct" } + } + }); + } };`, + dir: bundledDir, + filename: "telegram.js", + }); + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir; + + const registry = loadOpenClawPlugins({ + cache: false, + config: { + channels: { + telegram: { + enabled: true, + }, + }, + plugins: { + enabled: true, + }, + }, + }); + + const telegram = registry.plugins.find((entry) => entry.id === "telegram"); + expect(telegram?.status).toBe("loaded"); + expect(registry.channels.some((entry) => entry.plugin.id === "telegram")).toBe(true); + }); + + it("still respects explicit disable via plugins.entries for bundled channels", () => { + const bundledDir = makeTempDir(); + writePlugin({ + id: "telegram", + body: `export default { id: "telegram", register(api) { + api.registerChannel({ + plugin: { + id: "telegram", + meta: { + id: "telegram", + label: "Telegram", + selectionLabel: "Telegram", + docsPath: "/channels/telegram", + blurb: "telegram channel" + }, + capabilities: { chatTypes: ["direct"] }, + config: { + listAccountIds: () => [], + resolveAccount: () => ({ accountId: "default" }) + }, + outbound: { deliveryMode: "direct" } + } + }); + } };`, + dir: bundledDir, + filename: "telegram.js", + }); + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir; + + const registry = loadOpenClawPlugins({ + cache: false, + config: { + channels: { + telegram: { + enabled: true, + }, + }, + plugins: { + entries: { + telegram: { enabled: false }, + }, + }, + }, + }); + + const telegram = registry.plugins.find((entry) => entry.id === "telegram"); + expect(telegram?.status).toBe("disabled"); + expect(telegram?.error).toBe("disabled in config"); + }); + it("enables bundled memory plugin when selected by slot", () => { const registry = loadBundledMemoryPluginRegistry(); diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index be0e508faad..26491a41816 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { createJiti } from "jiti"; +import { normalizeChatChannelId } from "../channels/registry.js"; import type { OpenClawConfig } from "../config/config.js"; import type { GatewayRequestHandler } from "../gateway/server-methods/types.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; @@ -175,6 +176,19 @@ function createPluginRecord(params: { }; } +function isBundledChannelEnabledByChannelConfig(cfg: OpenClawConfig, pluginId: string): boolean { + const channelId = normalizeChatChannelId(pluginId); + if (!channelId) { + return false; + } + const channels = cfg.channels as Record | undefined; + const entry = channels?.[channelId]; + if (!entry || typeof entry !== "object" || Array.isArray(entry)) { + return false; + } + return (entry as Record).enabled === true; +} + function recordPluginError(params: { logger: PluginLogger; registry: PluginRegistry; @@ -472,7 +486,14 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi continue; } - const enableState = resolveEnableState(pluginId, candidate.origin, normalized); + let enableState = resolveEnableState(pluginId, candidate.origin, normalized); + if ( + !enableState.enabled && + enableState.reason === "bundled (disabled by default)" && + isBundledChannelEnabledByChannelConfig(cfg, pluginId) + ) { + enableState = { enabled: true }; + } const entry = normalized.entries[pluginId]; const record = createPluginRecord({ id: pluginId,