From 837c4c5f1b0e9ab0133e0e2c55756b8ae80ef895 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Apr 2026 02:15:21 +0100 Subject: [PATCH] fix: respect external channel owners in doctor blockers --- CHANGELOG.md | 1 + .../shared/channel-plugin-blockers.test.ts | 101 ++++++++++++++++++ .../doctor/shared/channel-plugin-blockers.ts | 31 +++++- 3 files changed, 130 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e2f52c8945..ec4456d0610 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Cron/providers: preflight local Ollama and OpenAI-compatible provider endpoints before isolated cron agent turns, record unreachable local providers as skipped runs, and cache dead-endpoint probes so many jobs do not hammer the same stopped local server. Fixes #58584. Thanks @jpeghead. +- Doctor/channels: suppress disabled bundled-plugin blocker warnings when a trusted external plugin owns the configured channel, so Lark/Feishu installs no longer get Feishu repair noise after switching to `openclaw-lark`. Fixes #56794. Thanks @wuji-tech-dev. - CLI/status: show skipped fast-path memory checks as `not checked` and report active custom memory plugin runtime status from `status --json --all` without requiring built-in `agents.defaults.memorySearch`, so plugins such as memory-lancedb-pro and memory-cms no longer look unavailable when their own runtime is healthy. Fixes #56968. Thanks @Tony-ooo and @aderius. - Gateway/channels: record and log unexpected clean channel monitor exits so channels that return without throwing no longer appear stopped with no error. Fixes #73099. Thanks @balaji1968-kingler. - Channels/Telegram: centralize polling update tracking so accepted offsets remain durable across restarts, same-process handler failures can still retry, and slow offset writes cannot overwrite newer accepted watermarks. Refs #73115. Thanks @vdruts. diff --git a/src/commands/doctor/shared/channel-plugin-blockers.test.ts b/src/commands/doctor/shared/channel-plugin-blockers.test.ts index 61e899aaf3f..5f001be0867 100644 --- a/src/commands/doctor/shared/channel-plugin-blockers.test.ts +++ b/src/commands/doctor/shared/channel-plugin-blockers.test.ts @@ -106,4 +106,105 @@ describe("channel plugin blockers", () => { }, ]); }); + + it("does not report a disabled bundled owner when a configured external plugin owns the channel", () => { + vi.spyOn(manifestRegistry, "loadPluginManifestRegistry").mockReturnValue({ + plugins: [ + { + id: "feishu", + origin: "bundled", + channels: ["feishu"], + enabledByDefault: true, + }, + { + id: "openclaw-lark", + origin: "config", + channels: ["feishu"], + enabledByDefault: false, + channelConfigs: { + feishu: { + schema: { + type: "object", + }, + }, + }, + }, + ], + diagnostics: [], + } as unknown as ReturnType); + + const hits = scanConfiguredChannelPluginBlockers({ + plugins: { + entries: { + feishu: { + enabled: false, + }, + "openclaw-lark": { + enabled: true, + }, + }, + }, + channels: { + feishu: { + footer: { + model: false, + }, + }, + }, + }); + + expect(hits).toEqual([]); + }); + + it("still reports the disabled bundled owner when an external channel owner is not trusted", () => { + vi.spyOn(manifestRegistry, "loadPluginManifestRegistry").mockReturnValue({ + plugins: [ + { + id: "feishu", + origin: "bundled", + channels: ["feishu"], + enabledByDefault: true, + }, + { + id: "openclaw-lark", + origin: "config", + channels: ["feishu"], + enabledByDefault: false, + channelConfigs: { + feishu: { + schema: { + type: "object", + }, + }, + }, + }, + ], + diagnostics: [], + } as unknown as ReturnType); + + const hits = scanConfiguredChannelPluginBlockers({ + plugins: { + entries: { + feishu: { + enabled: false, + }, + }, + }, + channels: { + feishu: { + footer: { + model: false, + }, + }, + }, + }); + + expect(hits).toEqual([ + { + channelId: "feishu", + pluginId: "feishu", + reason: "disabled in config", + }, + ]); + }); }); diff --git a/src/commands/doctor/shared/channel-plugin-blockers.ts b/src/commands/doctor/shared/channel-plugin-blockers.ts index e046837a45f..c7279dae62f 100644 --- a/src/commands/doctor/shared/channel-plugin-blockers.ts +++ b/src/commands/doctor/shared/channel-plugin-blockers.ts @@ -1,10 +1,14 @@ import type { OpenClawConfig } from "../../../config/types.openclaw.js"; -import { listExplicitConfiguredChannelIdsForConfig } from "../../../plugins/channel-plugin-ids.js"; +import { + listExplicitConfiguredChannelIdsForConfig, + resolveConfiguredChannelPresencePolicy, +} from "../../../plugins/channel-plugin-ids.js"; import { normalizePluginsConfig, resolveEffectivePluginActivationState, } from "../../../plugins/config-state.js"; import { loadPluginManifestRegistryForPluginRegistry } from "../../../plugins/plugin-registry.js"; +import { normalizeOptionalLowercaseString } from "../../../shared/string-coerce.js"; import { sanitizeForLog } from "../../../terminal/ansi.js"; export type ChannelPluginBlockerHit = { @@ -39,7 +43,11 @@ export function scanConfiguredChannelPluginBlockers( if (!hasExplicitChannelPluginBlockerConfig(cfg)) { return []; } - const configuredChannelIds = new Set(listExplicitConfiguredChannelIdsForConfig(cfg)); + const configuredChannelIds = new Set( + listExplicitConfiguredChannelIdsForConfig(cfg) + .map((channelId) => normalizeOptionalLowercaseString(channelId)) + .filter((channelId): channelId is string => Boolean(channelId)), + ); if (configuredChannelIds.size === 0) { return []; } @@ -50,6 +58,16 @@ export function scanConfiguredChannelPluginBlockers( env, includeDisabled: true, }); + const activeConfiguredChannelIds = new Set( + resolveConfiguredChannelPresencePolicy({ + config: cfg, + env, + includePersistedAuthState: false, + manifestRecords: registry.plugins, + }) + .filter((entry) => entry.effective) + .map((entry) => entry.channelId), + ); const hits: ChannelPluginBlockerHit[] = []; for (const plugin of registry.plugins) { @@ -73,10 +91,17 @@ export function scanConfiguredChannelPluginBlockers( continue; } - for (const channelId of plugin.channels) { + for (const rawChannelId of plugin.channels) { + const channelId = normalizeOptionalLowercaseString(rawChannelId); + if (!channelId) { + continue; + } if (!configuredChannelIds.has(channelId)) { continue; } + if (activeConfiguredChannelIds.has(channelId)) { + continue; + } hits.push({ channelId, pluginId: plugin.id,