fix(gateway): only sync configured channel plugins and cleanup unconfigured extensions (#898)

This commit is contained in:
paisley
2026-04-23 17:30:53 +08:00
committed by GitHub
parent 956e943072
commit 1b75fec71c
4 changed files with 130 additions and 87 deletions

View File

@@ -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<string, string> = {};
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);
}

View File

@@ -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<void> {
});
}
// 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.

View File

@@ -994,6 +994,22 @@ export async function deleteChannelConfig(channelType: string): Promise<void> {
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)) {

View File

@@ -1710,32 +1710,60 @@ export async function sanitizeOpenClawConfig(): Promise<void> {
|| 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<string, Record<string, unknown>> | 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<void> {
// ── 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<string, unknown>).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<string, unknown>).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<void> {
// 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)) {