From 1b75fec71cbd39d6267f33eb3ec09bfdf09cdf5a Mon Sep 17 00:00:00 2001 From: paisley <8197966+su8su@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:30:53 +0800 Subject: [PATCH] fix(gateway): only sync configured channel plugins and cleanup unconfigured extensions (#898) --- electron/gateway/config-sync.ts | 53 ++++++------ electron/main/index.ts | 14 ++-- electron/utils/channel-config.ts | 16 ++++ electron/utils/openclaw-auth.ts | 134 +++++++++++++++++++------------ 4 files changed, 130 insertions(+), 87 deletions(-) diff --git a/electron/gateway/config-sync.ts b/electron/gateway/config-sync.ts index e6bdc57..f7b0d2c 100644 --- a/electron/gateway/config-sync.ts +++ b/electron/gateway/config-sync.ts @@ -164,6 +164,31 @@ function ensureConfiguredPluginsUpgraded(configuredChannels: string[]): void { } } +/** + * Remove channel plugin extensions from ~/.openclaw/extensions/ when their + * corresponding channel is no longer configured. This prevents the Gateway + * from scanning residual plugin manifests that were installed by a previous + * configuration but are no longer needed. + */ +function cleanupUnconfiguredChannelPlugins(configuredChannels: string[]): void { + const configuredSet = new Set(configuredChannels); + + for (const [channelType, pluginInfo] of Object.entries(CHANNEL_PLUGIN_MAP)) { + if (configuredSet.has(channelType)) continue; + + const { dirName } = pluginInfo; + const targetDir = join(homedir(), '.openclaw', 'extensions', dirName); + if (!existsSync(fsPath(targetDir))) continue; + + logger.info(`[plugin] Removing unconfigured channel plugin: ${channelType} (${dirName})`); + try { + rmSync(fsPath(targetDir), { recursive: true, force: true }); + } catch (err) { + logger.warn(`[plugin] Failed to remove unconfigured channel plugin ${channelType}:`, err); + } + } +} + /** * Ensure extension-specific packages are resolvable from shared dist/ chunks. * @@ -280,36 +305,14 @@ export async function syncGatewayConfigBeforeLaunch( // Auto-upgrade installed plugins before Gateway starts so that // the plugin manifest ID matches what sanitize wrote to the config. - // Read config once and reuse for both listConfiguredChannels and plugins.allow. + // Only install/upgrade plugins for channels that are actually configured + // in openclaw.json — do NOT expand the list from plugins.allow. try { const rawCfg = await readOpenClawConfig(); const configuredChannels = await listConfiguredChannelsFromConfig(rawCfg); - // Also ensure plugins referenced in plugins.allow are installed even if - // they have no channels.X section yet (e.g. qqbot added via plugins.allow - // but never fully saved through ClawX UI). - try { - const allowList = Array.isArray(rawCfg.plugins?.allow) ? (rawCfg.plugins!.allow as string[]) : []; - const pluginIdToChannel: Record = {}; - for (const [channelType, info] of Object.entries(CHANNEL_PLUGIN_MAP)) { - pluginIdToChannel[info.dirName] = channelType; - } - - pluginIdToChannel['openclaw-lark'] = 'feishu'; - pluginIdToChannel['feishu-openclaw-plugin'] = 'feishu'; - - for (const pluginId of allowList) { - const channelType = pluginIdToChannel[pluginId] ?? pluginId; - if (CHANNEL_PLUGIN_MAP[channelType] && !configuredChannels.includes(channelType)) { - configuredChannels.push(channelType); - } - } - - } catch (err) { - logger.warn('[plugin] Failed to augment channel list from plugins.allow:', err); - } - ensureConfiguredPluginsUpgraded(configuredChannels); + cleanupUnconfiguredChannelPlugins(configuredChannels); } catch (err) { logger.warn('Failed to auto-upgrade plugins:', err); } diff --git a/electron/main/index.ts b/electron/main/index.ts index 299e647..326c8c6 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -40,7 +40,7 @@ import { createSignalQuitHandler } from './signal-quit'; import { acquireProcessInstanceFileLock } from './process-instance-lock'; import { getSetting } from '../utils/store'; import { ensureBuiltinSkillsInstalled, ensurePreinstalledSkillsInstalled } from '../utils/skill-config'; -import { ensureAllBundledPluginsInstalled } from '../utils/plugin-install'; + import { startHostApiServer } from '../api/server'; import { HostEventBus } from '../api/event-bus'; import { deviceOAuthManager } from '../utils/device-oauth'; @@ -389,14 +389,10 @@ async function initialize(): Promise { }); } - // Pre-deploy/upgrade bundled OpenClaw plugins (dingtalk, wecom, feishu, wechat) - // to ~/.openclaw/extensions/ so they are always up-to-date after an app update. - // Note: qqbot was moved to a built-in channel in OpenClaw 3.31. - if (!isE2EMode) { - void ensureAllBundledPluginsInstalled().catch((error) => { - logger.warn('Failed to install/upgrade bundled plugins:', error); - }); - } + // Plugin installation is now configuration-driven: + // - When a channel is added via UI: ensureXxxPluginInstalled() in IPC handlers + // - When Gateway starts: ensureConfiguredPluginsUpgraded() in config-sync.ts + // No need to pre-install all bundled plugins at app startup. // Bridge gateway and host-side events before any auto-start logic runs, so // renderer subscribers observe the full startup lifecycle. diff --git a/electron/utils/channel-config.ts b/electron/utils/channel-config.ts index 02001e0..0870783 100644 --- a/electron/utils/channel-config.ts +++ b/electron/utils/channel-config.ts @@ -994,6 +994,22 @@ export async function deleteChannelConfig(channelType: string): Promise { if (isWechatChannelType(resolvedChannelType)) { removePluginRegistration(currentConfig, WECHAT_PLUGIN_ID); } + // Clean up third-party plugin registrations when their channel is removed. + if (resolvedChannelType === 'feishu') { + for (const candidateId of FEISHU_PLUGIN_ID_CANDIDATES) { + removePluginRegistration(currentConfig, candidateId); + } + // Also remove the built-in feishu disable entry since it's no longer needed + if (currentConfig.plugins?.entries?.feishu) { + delete currentConfig.plugins.entries.feishu; + } + } + if (resolvedChannelType === 'dingtalk') { + removePluginRegistration(currentConfig, 'dingtalk'); + } + if (resolvedChannelType === 'wecom') { + removePluginRegistration(currentConfig, WECOM_PLUGIN_ID); + } syncBuiltinChannelsWithPluginAllowlist(currentConfig); await writeOpenClawConfig(currentConfig); if (isWechatChannelType(resolvedChannelType)) { diff --git a/electron/utils/openclaw-auth.ts b/electron/utils/openclaw-auth.ts index 5f135e0..5bfdb82 100644 --- a/electron/utils/openclaw-auth.ts +++ b/electron/utils/openclaw-auth.ts @@ -1710,32 +1710,60 @@ export async function sanitizeOpenClawConfig(): Promise { || FEISHU_PLUGIN_ID_CANDIDATES.find((id) => Boolean(pEntries[id])); const canonicalFeishuId = installedFeishuId || configuredFeishuId || FEISHU_PLUGIN_ID_CANDIDATES[0]; - const existingFeishuEntry = - FEISHU_PLUGIN_ID_CANDIDATES.map((id) => pEntries[id]).find(Boolean) - || pEntries.feishu; + // Only add feishu plugin to plugins.allow and plugins.entries when the + // feishu channel is actually configured. If not configured, remove all + // feishu-related entries so they don't linger in the config. + const feishuChannelSection = (config.channels as Record> | undefined)?.feishu; + const isFeishuConfigured = feishuChannelSection + && typeof feishuChannelSection === 'object' + && feishuChannelSection.enabled !== false + && Object.keys(feishuChannelSection).length > 0; - const normalizedAllow = allowArr.filter( - (id) => id !== 'feishu' && !FEISHU_PLUGIN_ID_CANDIDATES.includes(id as typeof FEISHU_PLUGIN_ID_CANDIDATES[number]), - ); - normalizedAllow.push(canonicalFeishuId); - if (JSON.stringify(normalizedAllow) !== JSON.stringify(allowArr)) { - pluginsObj.allow = normalizedAllow; - modified = true; - console.log(`[sanitize] Normalized plugins.allow for feishu -> ${canonicalFeishuId}`); - } + if (isFeishuConfigured) { + const existingFeishuEntry = + FEISHU_PLUGIN_ID_CANDIDATES.map((id) => pEntries[id]).find(Boolean) + || pEntries.feishu; - if (existingFeishuEntry || !pEntries[canonicalFeishuId]) { - pEntries[canonicalFeishuId] = { - ...(existingFeishuEntry || {}), - ...(pEntries[canonicalFeishuId] || {}), - enabled: true, - }; - modified = true; - } - for (const id of FEISHU_PLUGIN_ID_CANDIDATES) { - if (id !== canonicalFeishuId && pEntries[id]) { - delete pEntries[id]; + const normalizedAllow = allowArr.filter( + (id) => id !== 'feishu' && !FEISHU_PLUGIN_ID_CANDIDATES.includes(id as typeof FEISHU_PLUGIN_ID_CANDIDATES[number]), + ); + normalizedAllow.push(canonicalFeishuId); + if (JSON.stringify(normalizedAllow) !== JSON.stringify(allowArr)) { + pluginsObj.allow = normalizedAllow; modified = true; + console.log(`[sanitize] Normalized plugins.allow for feishu -> ${canonicalFeishuId}`); + } + + if (existingFeishuEntry || !pEntries[canonicalFeishuId]) { + pEntries[canonicalFeishuId] = { + ...(existingFeishuEntry || {}), + ...(pEntries[canonicalFeishuId] || {}), + enabled: true, + }; + modified = true; + } + for (const id of FEISHU_PLUGIN_ID_CANDIDATES) { + if (id !== canonicalFeishuId && pEntries[id]) { + delete pEntries[id]; + modified = true; + } + } + } else { + // Feishu channel not configured — remove all feishu plugin entries + const normalizedAllow = allowArr.filter( + (id) => id !== 'feishu' && !FEISHU_PLUGIN_ID_CANDIDATES.includes(id as typeof FEISHU_PLUGIN_ID_CANDIDATES[number]), + ); + if (normalizedAllow.length !== allowArr.length) { + pluginsObj.allow = normalizedAllow; + modified = true; + console.log('[sanitize] Removed unconfigured feishu plugin from plugins.allow'); + } + for (const id of [...FEISHU_PLUGIN_ID_CANDIDATES, 'feishu'] as const) { + if (pEntries[id]) { + delete pEntries[id]; + modified = true; + console.log(`[sanitize] Removed unconfigured feishu plugin entry: ${id}`); + } } } @@ -1821,31 +1849,31 @@ export async function sanitizeOpenClawConfig(): Promise { // ── Disable built-in 'feishu' when official openclaw-lark plugin is active ── // OpenClaw ships a built-in 'feishu' extension in dist/extensions/feishu/ // that conflicts with the official @larksuite/openclaw-lark plugin - // (id: 'openclaw-lark'). When the canonical feishu plugin is NOT the - // built-in 'feishu' itself, we must: - // 1. Remove bare 'feishu' from plugins.allow (already done above at line ~1648) - // 2. Delete plugins.entries.feishu entirely — keeping it with enabled:false - // causes the Gateway to report the feishu channel as "disabled". - // Since 'feishu' is not in plugins.allow, the built-in won't load. + // (id: 'openclaw-lark'). When the feishu channel IS configured and the + // canonical plugin is NOT the built-in 'feishu' itself, we must: + // 1. Remove bare 'feishu' from plugins.allow + // 2. Explicitly disable the built-in feishu extension const allowArr2 = Array.isArray(pluginsObj.allow) ? pluginsObj.allow as string[] : []; - const hasCanonicalFeishu = allowArr2.includes(canonicalFeishuId) || !!pEntries[canonicalFeishuId]; - if (hasCanonicalFeishu && canonicalFeishuId !== 'feishu') { - // Remove bare 'feishu' from plugins.allow - const bareFeishuIdx = allowArr2.indexOf('feishu'); - if (bareFeishuIdx !== -1) { - allowArr2.splice(bareFeishuIdx, 1); - console.log('[sanitize] Removed bare "feishu" from plugins.allow (openclaw-lark plugin is configured)'); - modified = true; - } - // Explicitly disable the built-in feishu extension so it doesn't - // conflict with the official openclaw-lark plugin at runtime. - // Simply deleting the entry is NOT sufficient — the built-in - // extension in dist/extensions/feishu/ (enabledByDefault: true) will - // still load unless explicitly marked as disabled. - if (!pEntries.feishu || (pEntries.feishu as Record).enabled !== false) { - pEntries.feishu = { enabled: false }; - console.log('[sanitize] Disabled built-in feishu plugin (openclaw-lark plugin is configured)'); - modified = true; + if (isFeishuConfigured) { + const hasCanonicalFeishu = allowArr2.includes(canonicalFeishuId) || !!pEntries[canonicalFeishuId]; + if (hasCanonicalFeishu && canonicalFeishuId !== 'feishu') { + // Remove bare 'feishu' from plugins.allow + const bareFeishuIdx = allowArr2.indexOf('feishu'); + if (bareFeishuIdx !== -1) { + allowArr2.splice(bareFeishuIdx, 1); + console.log('[sanitize] Removed bare "feishu" from plugins.allow (openclaw-lark plugin is configured)'); + modified = true; + } + // Explicitly disable the built-in feishu extension so it doesn't + // conflict with the official openclaw-lark plugin at runtime. + // Simply deleting the entry is NOT sufficient — the built-in + // extension in dist/extensions/feishu/ (enabledByDefault: true) will + // still load unless explicitly marked as disabled. + if (!pEntries.feishu || (pEntries.feishu as Record).enabled !== false) { + pEntries.feishu = { enabled: false }; + console.log('[sanitize] Disabled built-in feishu plugin (openclaw-lark plugin is configured)'); + modified = true; + } } } @@ -1899,12 +1927,12 @@ export async function sanitizeOpenClawConfig(): Promise { // allowlist because they were excluded from externalPluginIds above. if (nextAllow.length > 0) { for (const pluginId of bundled.enabledByDefault) { - // When the official openclaw-lark (or similar) plugin replaces the - // built-in 'feishu' extension, skip re-adding 'feishu' here — - // otherwise the enabledByDefault logic undoes the conflict - // resolution performed above and the built-in extension keeps - // reappearing in plugins.allow on every gateway restart. - if (pluginId === 'feishu' && canonicalFeishuId !== 'feishu') { + // When feishu is not configured at all, or the official + // openclaw-lark plugin replaces the built-in 'feishu' extension, + // skip re-adding 'feishu' here — otherwise the enabledByDefault + // logic undoes the cleanup performed above and the built-in + // extension keeps reappearing in plugins.allow. + if (pluginId === 'feishu' && (!isFeishuConfigured || canonicalFeishuId !== 'feishu')) { continue; } if (!nextAllow.includes(pluginId)) {