build: upgrade feishu plugin to 2026.3.12 (#482)

This commit is contained in:
paisley
2026-03-14 14:16:21 +08:00
committed by GitHub
Unverified
parent 6988b2b5bf
commit f6de56fa78
7 changed files with 294 additions and 279 deletions

View File

@@ -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(

View File

@@ -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<string, string> = {
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<ReturnType<typeof getAllSettings>>,
): Promise<void> {
@@ -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) {

View File

@@ -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 };

View File

@@ -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] = {};

View File

@@ -1019,15 +1019,57 @@ export async function sanitizeOpenClawConfig(): Promise<void> {
// 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<string, unknown>;
const pEntries = pluginsObj.entries as Record<string, Record<string, unknown>> | 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 ─────────────────────────

View File

@@ -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",

10
pnpm-lock.yaml generated
View File

@@ -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