fix(gateway): only sync configured channel plugins and cleanup unconfigured extensions (#898)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user