From f6de56fa788105e043f965e2e889a2fc3738a849 Mon Sep 17 00:00:00 2001 From: paisley <8197966+su8su@users.noreply.github.com> Date: Sat, 14 Mar 2026 14:16:21 +0800 Subject: [PATCH] build: upgrade feishu plugin to 2026.3.12 (#482) --- electron/api/routes/channels.ts | 186 ++++++++----------------- electron/gateway/config-sync.ts | 80 ++++++++++- electron/main/ipc-handlers.ts | 232 +++++++++++++------------------ electron/utils/channel-config.ts | 13 +- electron/utils/openclaw-auth.ts | 50 ++++++- package.json | 2 +- pnpm-lock.yaml | 10 +- 7 files changed, 294 insertions(+), 279 deletions(-) diff --git a/electron/api/routes/channels.ts b/electron/api/routes/channels.ts index 49d482956..7f1ce950a 100644 --- a/electron/api/routes/channels.ts +++ b/electron/api/routes/channels.ts @@ -1,6 +1,6 @@ import type { IncomingMessage, ServerResponse } from 'http'; import { app } from 'electron'; -import { existsSync, cpSync, mkdirSync, rmSync } from 'node:fs'; +import { existsSync, cpSync, mkdirSync, rmSync, readFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { join } from 'node:path'; import { @@ -25,72 +25,48 @@ function scheduleGatewayChannelRestart(ctx: HostApiContext, reason: string): voi void reason; } -async function ensureDingTalkPluginInstalled(): Promise<{ installed: boolean; warning?: string }> { - const targetDir = join(homedir(), '.openclaw', 'extensions', 'dingtalk'); - const targetManifest = join(targetDir, 'openclaw.plugin.json'); - - if (existsSync(targetManifest)) { - return { installed: true }; - } - - const candidateSources = app.isPackaged - ? [ - join(process.resourcesPath, 'openclaw-plugins', 'dingtalk'), - join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', 'dingtalk'), - join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', 'dingtalk'), - ] - : [ - join(app.getAppPath(), 'build', 'openclaw-plugins', 'dingtalk'), - join(process.cwd(), 'build', 'openclaw-plugins', 'dingtalk'), - join(__dirname, '../../../build/openclaw-plugins/dingtalk'), - ]; - - const sourceDir = candidateSources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json'))); - if (!sourceDir) { - return { - installed: false, - warning: `Bundled DingTalk plugin mirror not found. Checked: ${candidateSources.join(' | ')}`, - }; - } +// ── Generic plugin installer with version-aware upgrades ───────── +function readPluginVersion(pkgJsonPath: string): string | null { try { - mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true }); - rmSync(targetDir, { recursive: true, force: true }); - cpSync(sourceDir, targetDir, { recursive: true, dereference: true }); - if (!existsSync(targetManifest)) { - return { installed: false, warning: 'Failed to install DingTalk plugin mirror (manifest missing).' }; - } - return { installed: true }; + const raw = readFileSync(pkgJsonPath, 'utf-8'); + const parsed = JSON.parse(raw) as { version?: string }; + return parsed.version ?? null; } catch { - return { installed: false, warning: 'Failed to install bundled DingTalk plugin mirror' }; + return null; } } -async function ensureWeComPluginInstalled(): Promise<{ installed: boolean; warning?: string }> { - const targetDir = join(homedir(), '.openclaw', 'extensions', 'wecom'); +function ensurePluginInstalled( + pluginDirName: string, + candidateSources: string[], + pluginLabel: string, +): { installed: boolean; warning?: string } { + const targetDir = join(homedir(), '.openclaw', 'extensions', pluginDirName); const targetManifest = join(targetDir, 'openclaw.plugin.json'); - - if (existsSync(targetManifest)) { - return { installed: true }; - } - - const candidateSources = app.isPackaged - ? [ - join(process.resourcesPath, 'openclaw-plugins', 'wecom'), - join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', 'wecom'), - join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', 'wecom'), - ] - : [ - join(app.getAppPath(), 'build', 'openclaw-plugins', 'wecom'), - join(process.cwd(), 'build', 'openclaw-plugins', 'wecom'), - join(__dirname, '../../../build/openclaw-plugins/wecom'), - ]; + const targetPkgJson = join(targetDir, 'package.json'); const sourceDir = candidateSources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json'))); + + // If already installed, check whether an upgrade is available + if (existsSync(targetManifest)) { + if (!sourceDir) return { installed: true }; // no bundled source to compare, keep existing + const installedVersion = readPluginVersion(targetPkgJson); + const sourceVersion = readPluginVersion(join(sourceDir, 'package.json')); + if (!sourceVersion || !installedVersion || sourceVersion === installedVersion) { + return { installed: true }; // same version or unable to compare + } + // Version differs — fall through to overwrite install + console.log( + `[plugin] Upgrading ${pluginLabel} plugin: ${installedVersion} → ${sourceVersion}`, + ); + } + + // Fresh install or upgrade if (!sourceDir) { return { installed: false, - warning: `Bundled WeCom plugin mirror not found. Checked: ${candidateSources.join(' | ')}`, + warning: `Bundled ${pluginLabel} plugin mirror not found. Checked: ${candidateSources.join(' | ')}`, }; } @@ -98,95 +74,49 @@ async function ensureWeComPluginInstalled(): Promise<{ installed: boolean; warni mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true }); rmSync(targetDir, { recursive: true, force: true }); cpSync(sourceDir, targetDir, { recursive: true, dereference: true }); - if (!existsSync(targetManifest)) { - return { installed: false, warning: 'Failed to install WeCom plugin mirror (manifest missing).' }; + if (!existsSync(join(targetDir, 'openclaw.plugin.json'))) { + return { installed: false, warning: `Failed to install ${pluginLabel} plugin mirror (manifest missing).` }; } return { installed: true }; } catch { - return { installed: false, warning: 'Failed to install bundled WeCom plugin mirror' }; + return { installed: false, warning: `Failed to install bundled ${pluginLabel} plugin mirror` }; } } -async function ensureFeishuPluginInstalled(): Promise<{ installed: boolean; warning?: string }> { - const targetDir = join(homedir(), '.openclaw', 'extensions', 'feishu-openclaw-plugin'); - const targetManifest = join(targetDir, 'openclaw.plugin.json'); +// ── Per-channel plugin helpers (thin wrappers around ensurePluginInstalled) ── - if (existsSync(targetManifest)) { - return { installed: true }; - } - - const candidateSources = app.isPackaged +function buildCandidateSources(pluginDirName: string): string[] { + return app.isPackaged ? [ - join(process.resourcesPath, 'openclaw-plugins', 'feishu-openclaw-plugin'), - join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', 'feishu-openclaw-plugin'), - join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', 'feishu-openclaw-plugin'), + join(process.resourcesPath, 'openclaw-plugins', pluginDirName), + join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', pluginDirName), + join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', pluginDirName), ] : [ - join(app.getAppPath(), 'build', 'openclaw-plugins', 'feishu-openclaw-plugin'), - join(process.cwd(), 'build', 'openclaw-plugins', 'feishu-openclaw-plugin'), - join(__dirname, '../../../build/openclaw-plugins/feishu-openclaw-plugin'), + join(app.getAppPath(), 'build', 'openclaw-plugins', pluginDirName), + join(process.cwd(), 'build', 'openclaw-plugins', pluginDirName), + join(__dirname, '../../../build/openclaw-plugins', pluginDirName), ]; - - const sourceDir = candidateSources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json'))); - if (!sourceDir) { - return { - installed: false, - warning: `Bundled Feishu plugin mirror not found. Checked: ${candidateSources.join(' | ')}`, - }; - } - - try { - mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true }); - rmSync(targetDir, { recursive: true, force: true }); - cpSync(sourceDir, targetDir, { recursive: true, dereference: true }); - if (!existsSync(targetManifest)) { - return { installed: false, warning: 'Failed to install Feishu plugin mirror (manifest missing).' }; - } - return { installed: true }; - } catch { - return { installed: false, warning: 'Failed to install bundled Feishu plugin mirror' }; - } } -async function ensureQQBotPluginInstalled(): Promise<{ installed: boolean; warning?: string }> { - const targetDir = join(homedir(), '.openclaw', 'extensions', 'qqbot'); - const targetManifest = join(targetDir, 'openclaw.plugin.json'); +function ensureDingTalkPluginInstalled(): { installed: boolean; warning?: string } { + return ensurePluginInstalled('dingtalk', buildCandidateSources('dingtalk'), 'DingTalk'); +} - if (existsSync(targetManifest)) { - return { installed: true }; - } +function ensureWeComPluginInstalled(): { installed: boolean; warning?: string } { + return ensurePluginInstalled('wecom', buildCandidateSources('wecom'), 'WeCom'); +} - const candidateSources = app.isPackaged - ? [ - join(process.resourcesPath, 'openclaw-plugins', 'qqbot'), - join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', 'qqbot'), - join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', 'qqbot'), - ] - : [ - join(app.getAppPath(), 'build', 'openclaw-plugins', 'qqbot'), - join(process.cwd(), 'build', 'openclaw-plugins', 'qqbot'), - join(__dirname, '../../../build/openclaw-plugins/qqbot'), - ]; +function ensureFeishuPluginInstalled(): { installed: boolean; warning?: string } { + return ensurePluginInstalled( + 'feishu-openclaw-plugin', + buildCandidateSources('feishu-openclaw-plugin'), + 'Feishu', + ); +} - const sourceDir = candidateSources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json'))); - if (!sourceDir) { - return { - installed: false, - warning: `Bundled QQ Bot plugin mirror not found. Checked: ${candidateSources.join(' | ')}`, - }; - } - - try { - mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true }); - rmSync(targetDir, { recursive: true, force: true }); - cpSync(sourceDir, targetDir, { recursive: true, dereference: true }); - if (!existsSync(targetManifest)) { - return { installed: false, warning: 'Failed to install QQ Bot plugin mirror (manifest missing).' }; - } - return { installed: true }; - } catch { - return { installed: false, warning: 'Failed to install bundled QQ Bot plugin mirror' }; - } +function ensureQQBotPluginInstalled(): { installed: boolean; warning?: string } { + return ensurePluginInstalled('qqbot', buildCandidateSources('qqbot'), 'QQ Bot'); } export async function handleChannelRoutes( diff --git a/electron/gateway/config-sync.ts b/electron/gateway/config-sync.ts index 0e9ecab60..b4d47cca9 100644 --- a/electron/gateway/config-sync.ts +++ b/electron/gateway/config-sync.ts @@ -1,6 +1,8 @@ import { app } from 'electron'; import path from 'path'; -import { existsSync } from 'fs'; +import { existsSync, readFileSync, cpSync, mkdirSync, rmSync } from 'fs'; +import { homedir } from 'os'; +import { join } from 'path'; import { getAllSettings } from '../utils/store'; import { getApiKey, getDefaultProvider, getProvider } from '../utils/secure-storage'; import { getProviderEnvVar, getKeyableProviderTypes } from '../utils/provider-registry'; @@ -26,6 +28,73 @@ export interface GatewayLaunchContext { channelStartupSummary: string; } +// ── Auto-upgrade bundled plugins on startup ────────────────────── + +const CHANNEL_PLUGIN_MAP: Record = { + dingtalk: 'dingtalk', + wecom: 'wecom', + feishu: 'feishu-openclaw-plugin', + qqbot: 'qqbot', +}; + +function readPluginVersion(pkgJsonPath: string): string | null { + try { + const raw = readFileSync(pkgJsonPath, 'utf-8'); + const parsed = JSON.parse(raw) as { version?: string }; + return parsed.version ?? null; + } catch { + return null; + } +} + +function buildPluginCandidateSources(pluginDirName: string): string[] { + return app.isPackaged + ? [ + join(process.resourcesPath, 'openclaw-plugins', pluginDirName), + join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', pluginDirName), + join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', pluginDirName), + ] + : [ + join(app.getAppPath(), 'build', 'openclaw-plugins', pluginDirName), + join(process.cwd(), 'build', 'openclaw-plugins', pluginDirName), + ]; +} + +/** + * Auto-upgrade all configured channel plugins before Gateway start. + * Compares the installed version in ~/.openclaw/extensions/ with the + * bundled version and overwrites if the bundled version is newer. + */ +function ensureConfiguredPluginsUpgraded(configuredChannels: string[]): void { + for (const channelType of configuredChannels) { + const pluginDirName = CHANNEL_PLUGIN_MAP[channelType]; + if (!pluginDirName) continue; + + const targetDir = join(homedir(), '.openclaw', 'extensions', pluginDirName); + const targetManifest = join(targetDir, 'openclaw.plugin.json'); + if (!existsSync(targetManifest)) continue; // not installed, nothing to upgrade + + const sources = buildPluginCandidateSources(pluginDirName); + const sourceDir = sources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json'))); + if (!sourceDir) continue; // no bundled source available + + const installedVersion = readPluginVersion(join(targetDir, 'package.json')); + const sourceVersion = readPluginVersion(join(sourceDir, 'package.json')); + if (!sourceVersion || !installedVersion || sourceVersion === installedVersion) continue; + + logger.info(`[plugin] Auto-upgrading ${channelType} plugin: ${installedVersion} → ${sourceVersion}`); + try { + mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true }); + rmSync(targetDir, { recursive: true, force: true }); + cpSync(sourceDir, targetDir, { recursive: true, dereference: true }); + } catch (err) { + logger.warn(`[plugin] Failed to auto-upgrade ${channelType} plugin:`, err); + } + } +} + +// ── Pre-launch sync ────────────────────────────────────────────── + export async function syncGatewayConfigBeforeLaunch( appSettings: Awaited>, ): Promise { @@ -37,6 +106,15 @@ export async function syncGatewayConfigBeforeLaunch( logger.warn('Failed to sanitize openclaw.json:', err); } + // Auto-upgrade installed plugins before Gateway starts so that + // the plugin manifest ID matches what sanitize wrote to the config. + try { + const configuredChannels = await listConfiguredChannels(); + ensureConfiguredPluginsUpgraded(configuredChannels); + } catch (err) { + logger.warn('Failed to auto-upgrade plugins:', err); + } + try { await syncGatewayTokenToConfig(appSettings.gatewayToken); } catch (err) { diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index a7a102a42..dc855c583 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -3,7 +3,7 @@ * Registers all IPC handlers for main-renderer communication */ import { ipcMain, BrowserWindow, shell, dialog, app, nativeImage } from 'electron'; -import { existsSync, cpSync, mkdirSync, rmSync } from 'node:fs'; +import { existsSync, cpSync, mkdirSync, rmSync, readFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { join, extname, basename } from 'node:path'; import crypto from 'node:crypto'; @@ -1359,154 +1359,100 @@ function registerOpenClawHandlers(gatewayManager: GatewayManager): void { } }; - async function ensureDingTalkPluginInstalled(): Promise<{ installed: boolean; warning?: string }> { - const targetDir = join(homedir(), '.openclaw', 'extensions', 'dingtalk'); - const targetManifest = join(targetDir, 'openclaw.plugin.json'); + // ── Generic plugin installer with version-aware upgrades ───────── + function readPluginVersion(pkgJsonPath: string): string | null { + try { + const raw = readFileSync(pkgJsonPath, 'utf-8'); + const parsed = JSON.parse(raw) as { version?: string }; + return parsed.version ?? null; + } catch { + return null; + } + } + + function ensurePluginInstalled( + pluginDirName: string, + candidateSources: string[], + pluginLabel: string, + ): { installed: boolean; warning?: string } { + const targetDir = join(homedir(), '.openclaw', 'extensions', pluginDirName); + const targetManifest = join(targetDir, 'openclaw.plugin.json'); + const targetPkgJson = join(targetDir, 'package.json'); + + const sourceDir = candidateSources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json'))); + + // If already installed, check whether an upgrade is available if (existsSync(targetManifest)) { - logger.info('DingTalk plugin already installed from local mirror'); - return { installed: true }; + if (!sourceDir) return { installed: true }; + const installedVersion = readPluginVersion(targetPkgJson); + const sourceVersion = readPluginVersion(join(sourceDir, 'package.json')); + if (!sourceVersion || !installedVersion || sourceVersion === installedVersion) { + return { installed: true }; + } + logger.info(`[plugin] Upgrading ${pluginLabel} plugin: ${installedVersion} → ${sourceVersion}`); } - const candidateSources = app.isPackaged + if (!sourceDir) { + logger.warn(`Bundled ${pluginLabel} plugin mirror not found in candidate paths`, { candidateSources }); + return { + installed: false, + warning: `Bundled ${pluginLabel} plugin mirror not found. Checked: ${candidateSources.join(' | ')}`, + }; + } + + try { + mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true }); + rmSync(targetDir, { recursive: true, force: true }); + cpSync(sourceDir, targetDir, { recursive: true, dereference: true }); + + if (!existsSync(join(targetDir, 'openclaw.plugin.json'))) { + return { installed: false, warning: `Failed to install ${pluginLabel} plugin mirror (manifest missing).` }; + } + + logger.info(`Installed ${pluginLabel} plugin from bundled mirror: ${sourceDir}`); + return { installed: true }; + } catch (error) { + logger.warn(`Failed to install ${pluginLabel} plugin from bundled mirror:`, error); + return { + installed: false, + warning: `Failed to install bundled ${pluginLabel} plugin mirror`, + }; + } + } + + function buildCandidateSources(pluginDirName: string): string[] { + return app.isPackaged ? [ - join(process.resourcesPath, 'openclaw-plugins', 'dingtalk'), - join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', 'dingtalk'), - join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', 'dingtalk') + join(process.resourcesPath, 'openclaw-plugins', pluginDirName), + join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', pluginDirName), + join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', pluginDirName), ] : [ - join(app.getAppPath(), 'build', 'openclaw-plugins', 'dingtalk'), - join(process.cwd(), 'build', 'openclaw-plugins', 'dingtalk'), - join(__dirname, '../../build/openclaw-plugins/dingtalk'), + join(app.getAppPath(), 'build', 'openclaw-plugins', pluginDirName), + join(process.cwd(), 'build', 'openclaw-plugins', pluginDirName), + join(__dirname, '../../build/openclaw-plugins', pluginDirName), ]; - - const sourceDir = candidateSources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json'))); - if (!sourceDir) { - logger.warn('Bundled DingTalk plugin mirror not found in candidate paths', { candidateSources }); - return { - installed: false, - warning: `Bundled DingTalk plugin mirror not found. Checked: ${candidateSources.join(' | ')}`, - }; - } - - try { - mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true }); - rmSync(targetDir, { recursive: true, force: true }); - cpSync(sourceDir, targetDir, { recursive: true, dereference: true }); - - if (!existsSync(targetManifest)) { - return { installed: false, warning: 'Failed to install DingTalk plugin mirror (manifest missing).' }; - } - - logger.info(`Installed DingTalk plugin from bundled mirror: ${sourceDir}`); - return { installed: true }; - } catch (error) { - logger.warn('Failed to install DingTalk plugin from bundled mirror:', error); - return { - installed: false, - warning: 'Failed to install bundled DingTalk plugin mirror', - }; - } } - async function ensureWeComPluginInstalled(): Promise<{ installed: boolean; warning?: string }> { - const targetDir = join(homedir(), '.openclaw', 'extensions', 'wecom'); - const targetManifest = join(targetDir, 'openclaw.plugin.json'); - - if (existsSync(targetManifest)) { - logger.info('WeCom plugin already installed from local mirror'); - return { installed: true }; - } - - const candidateSources = app.isPackaged - ? [ - join(process.resourcesPath, 'openclaw-plugins', 'wecom'), - join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', 'wecom'), - join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', 'wecom') - ] - : [ - join(app.getAppPath(), 'build', 'openclaw-plugins', 'wecom'), - join(process.cwd(), 'build', 'openclaw-plugins', 'wecom'), - join(__dirname, '../../build/openclaw-plugins/wecom'), - ]; - - const sourceDir = candidateSources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json'))); - if (!sourceDir) { - logger.warn('Bundled WeCom plugin mirror not found in candidate paths', { candidateSources }); - return { - installed: false, - warning: `Bundled WeCom plugin mirror not found. Checked: ${candidateSources.join(' | ')}`, - }; - } - - try { - mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true }); - rmSync(targetDir, { recursive: true, force: true }); - cpSync(sourceDir, targetDir, { recursive: true, dereference: true }); - - if (!existsSync(targetManifest)) { - return { installed: false, warning: 'Failed to install WeCom plugin mirror (manifest missing).' }; - } - - logger.info(`Installed WeCom plugin from bundled mirror: ${sourceDir}`); - return { installed: true }; - } catch (error) { - logger.warn('Failed to install WeCom plugin from bundled mirror:', error); - return { - installed: false, - warning: 'Failed to install bundled WeCom plugin mirror', - }; - } + function ensureDingTalkPluginInstalled(): { installed: boolean; warning?: string } { + return ensurePluginInstalled('dingtalk', buildCandidateSources('dingtalk'), 'DingTalk'); } - async function ensureQQBotPluginInstalled(): Promise<{ installed: boolean; warning?: string }> { - const targetDir = join(homedir(), '.openclaw', 'extensions', 'qqbot'); - const targetManifest = join(targetDir, 'openclaw.plugin.json'); + function ensureWeComPluginInstalled(): { installed: boolean; warning?: string } { + return ensurePluginInstalled('wecom', buildCandidateSources('wecom'), 'WeCom'); + } - if (existsSync(targetManifest)) { - logger.info('QQ Bot plugin already installed from local mirror'); - return { installed: true }; - } + function ensureFeishuPluginInstalled(): { installed: boolean; warning?: string } { + return ensurePluginInstalled( + 'feishu-openclaw-plugin', + buildCandidateSources('feishu-openclaw-plugin'), + 'Feishu', + ); + } - const candidateSources = app.isPackaged - ? [ - join(process.resourcesPath, 'openclaw-plugins', 'qqbot'), - join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', 'qqbot'), - join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', 'qqbot') - ] - : [ - join(app.getAppPath(), 'build', 'openclaw-plugins', 'qqbot'), - join(process.cwd(), 'build', 'openclaw-plugins', 'qqbot'), - join(__dirname, '../../build/openclaw-plugins/qqbot'), - ]; - - const sourceDir = candidateSources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json'))); - if (!sourceDir) { - logger.warn('Bundled QQ Bot plugin mirror not found in candidate paths', { candidateSources }); - return { - installed: false, - warning: `Bundled QQ Bot plugin mirror not found. Checked: ${candidateSources.join(' | ')}`, - }; - } - - try { - mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true }); - rmSync(targetDir, { recursive: true, force: true }); - cpSync(sourceDir, targetDir, { recursive: true, dereference: true }); - - if (!existsSync(targetManifest)) { - return { installed: false, warning: 'Failed to install QQ Bot plugin mirror (manifest missing).' }; - } - - logger.info(`Installed QQ Bot plugin from bundled mirror: ${sourceDir}`); - return { installed: true }; - } catch (error) { - logger.warn('Failed to install QQ Bot plugin from bundled mirror:', error); - return { - installed: false, - warning: 'Failed to install bundled QQ Bot plugin mirror', - }; - } + function ensureQQBotPluginInstalled(): { installed: boolean; warning?: string } { + return ensurePluginInstalled('qqbot', buildCandidateSources('qqbot'), 'QQ Bot'); } // Get OpenClaw package status @@ -1615,6 +1561,22 @@ function registerOpenClawHandlers(gatewayManager: GatewayManager): void { warning: installResult.warning, }; } + if (channelType === 'feishu') { + const installResult = await ensureFeishuPluginInstalled(); + if (!installResult.installed) { + return { + success: false, + error: installResult.warning || 'Feishu plugin install failed', + }; + } + await saveChannelConfig(channelType, config); + scheduleGatewayChannelRestart(`channel:saveConfig (${channelType})`); + return { + success: true, + pluginInstalled: installResult.installed, + warning: installResult.warning, + }; + } await saveChannelConfig(channelType, config); scheduleGatewayChannelRestart(`channel:saveConfig (${channelType})`); return { success: true }; diff --git a/electron/utils/channel-config.ts b/electron/utils/channel-config.ts index 635f63189..63942b755 100644 --- a/electron/utils/channel-config.ts +++ b/electron/utils/channel-config.ts @@ -16,7 +16,8 @@ import { withConfigLock } from './config-mutex'; const OPENCLAW_DIR = join(homedir(), '.openclaw'); const CONFIG_FILE = join(OPENCLAW_DIR, 'openclaw.json'); const WECOM_PLUGIN_ID = 'wecom-openclaw-plugin'; -const FEISHU_PLUGIN_ID = 'feishu-openclaw-plugin'; +const FEISHU_PLUGIN_ID = 'openclaw-lark'; +const LEGACY_FEISHU_PLUGIN_ID = 'feishu-openclaw-plugin'; const DEFAULT_ACCOUNT_ID = 'default'; const CHANNEL_TOP_LEVEL_KEYS_TO_KEEP = new Set(['accounts', 'defaultAccount', 'enabled']); @@ -112,7 +113,10 @@ function ensurePluginAllowlist(currentConfig: OpenClawConfig, channelType: strin const allow: string[] = Array.isArray(currentConfig.plugins.allow) ? (currentConfig.plugins.allow as string[]) : []; - const normalizedAllow = allow.filter((pluginId) => pluginId !== 'feishu'); + // Remove legacy IDs: 'feishu' (built-in) and old 'feishu-openclaw-plugin' + const normalizedAllow = allow.filter( + (pluginId) => pluginId !== 'feishu' && pluginId !== LEGACY_FEISHU_PLUGIN_ID + ); if (!normalizedAllow.includes(FEISHU_PLUGIN_ID)) { currentConfig.plugins.allow = [...normalizedAllow, FEISHU_PLUGIN_ID]; } else if (normalizedAllow.length !== allow.length) { @@ -122,10 +126,9 @@ function ensurePluginAllowlist(currentConfig: OpenClawConfig, channelType: strin if (!currentConfig.plugins.entries) { currentConfig.plugins.entries = {}; } - // Remove legacy 'feishu' entry — the official plugin registers its - // channel AS 'feishu' via openclaw.plugin.json, so an explicit - // entries.feishu.enabled=false would block the official plugin's channel. + // Remove legacy entries that would conflict with the current plugin ID delete currentConfig.plugins.entries['feishu']; + delete currentConfig.plugins.entries[LEGACY_FEISHU_PLUGIN_ID]; if (!currentConfig.plugins.entries[FEISHU_PLUGIN_ID]) { currentConfig.plugins.entries[FEISHU_PLUGIN_ID] = {}; diff --git a/electron/utils/openclaw-auth.ts b/electron/utils/openclaw-auth.ts index 7c08c86f2..f8339cc92 100644 --- a/electron/utils/openclaw-auth.ts +++ b/electron/utils/openclaw-auth.ts @@ -1019,15 +1019,57 @@ export async function sanitizeOpenClawConfig(): Promise { // The official feishu plugin registers its channel AS 'feishu' via // openclaw.plugin.json. An explicit entries.feishu.enabled=false // (set by older ClawX to disable the legacy built-in) blocks the - // official plugin's channel from starting. Delete it. + // official plugin's channel from starting. Only clean up when the + // new openclaw-lark plugin is already configured (to avoid removing + // a legitimate old-style feishu plugin from users who haven't upgraded). if (typeof plugins === 'object' && !Array.isArray(plugins)) { const pluginsObj = plugins as Record; const pEntries = pluginsObj.entries as Record> | undefined; - if (pEntries?.feishu) { - console.log('[sanitize] Removing stale plugins.entries.feishu that blocks the official feishu plugin channel'); - delete pEntries.feishu; + + // ── feishu-openclaw-plugin → openclaw-lark migration ──────── + // Plugin @larksuite/openclaw-lark ≥2026.3.12 changed its manifest + // id from 'feishu-openclaw-plugin' to 'openclaw-lark'. Migrate + // both plugins.allow and plugins.entries so Gateway validation + // doesn't reject the config with "plugin not found". + const LEGACY_FEISHU_ID = 'feishu-openclaw-plugin'; + const NEW_FEISHU_ID = 'openclaw-lark'; + if (Array.isArray(pluginsObj.allow)) { + const allowArr = pluginsObj.allow as string[]; + const legacyIdx = allowArr.indexOf(LEGACY_FEISHU_ID); + if (legacyIdx !== -1) { + if (!allowArr.includes(NEW_FEISHU_ID)) { + allowArr[legacyIdx] = NEW_FEISHU_ID; + } else { + allowArr.splice(legacyIdx, 1); + } + console.log(`[sanitize] Migrated plugins.allow: ${LEGACY_FEISHU_ID} → ${NEW_FEISHU_ID}`); + modified = true; + } + } + if (pEntries?.[LEGACY_FEISHU_ID]) { + if (!pEntries[NEW_FEISHU_ID]) { + pEntries[NEW_FEISHU_ID] = pEntries[LEGACY_FEISHU_ID]; + } + delete pEntries[LEGACY_FEISHU_ID]; + console.log(`[sanitize] Migrated plugins.entries: ${LEGACY_FEISHU_ID} → ${NEW_FEISHU_ID}`); modified = true; } + + // ── Disable bare 'feishu' when openclaw-lark is present ──────── + // The Gateway binary automatically adds bare 'feishu' to plugins + // config because the openclaw-lark plugin registers the 'feishu' + // channel. We can't DELETE it (triggers config-change → restart → + // Gateway re-adds it → loop). Instead, disable it so it doesn't + // conflict with openclaw-lark. + const allowArr = Array.isArray(pluginsObj.allow) ? pluginsObj.allow as string[] : []; + const hasNewFeishu = allowArr.includes(NEW_FEISHU_ID) || !!pEntries?.[NEW_FEISHU_ID]; + if (hasNewFeishu && pEntries?.feishu) { + if (pEntries.feishu.enabled !== false) { + pEntries.feishu.enabled = false; + console.log(`[sanitize] Disabled bare plugins.entries.feishu (openclaw-lark is configured)`); + modified = true; + } + } } // ── channels default-account migration ───────────────────────── diff --git a/package.json b/package.json index 737c6ab6d..031e0ea88 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "@larksuite/openclaw-lark": "2026.3.10", + "@larksuite/openclaw-lark": "2026.3.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 87d585ded..73a845324 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,8 +37,8 @@ importers: specifier: ^10.0.1 version: 10.0.1(eslint@10.0.0(jiti@1.21.7)) '@larksuite/openclaw-lark': - specifier: 2026.3.10 - version: 2026.3.10 + specifier: 2026.3.12 + version: 2026.3.12 '@radix-ui/react-dialog': specifier: ^1.1.15 version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -1127,8 +1127,8 @@ packages: '@kwsites/promise-deferred@1.1.1': resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} - '@larksuite/openclaw-lark@2026.3.10': - resolution: {integrity: sha512-5XCPcEch2Bab2g284odCkxRCaRMerBezy9Qc6eUfnsO4HvLrqxR406DefTn1mr/ibPVNa3sGxKvjTg3rFXM4mg==} + '@larksuite/openclaw-lark@2026.3.12': + resolution: {integrity: sha512-MNcDrerQrO42I09w+M8q6dwnWHMKxOnXSCLG4qNwcekjGeDmA53lIuWJtGMpjTzvDjYkoWnN+8Zg78+FRCSV9w==} '@larksuiteoapi/node-sdk@1.59.0': resolution: {integrity: sha512-sBpkruTvZDOxnVtoTbepWKRX0j1Y1ZElQYu0x7+v088sI9pcpbVp6ZzCGn62dhrKPatzNyCJyzYCPXPYQWccrA==} @@ -7957,7 +7957,7 @@ snapshots: '@kwsites/promise-deferred@1.1.1': {} - '@larksuite/openclaw-lark@2026.3.10': + '@larksuite/openclaw-lark@2026.3.12': dependencies: '@larksuiteoapi/node-sdk': 1.59.0 '@sinclair/typebox': 0.34.48