build: upgrade feishu plugin to 2026.3.12 (#482)
This commit is contained in:
committed by
GitHub
Unverified
parent
6988b2b5bf
commit
f6de56fa78
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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] = {};
|
||||
|
||||
@@ -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 ─────────────────────────
|
||||
|
||||
@@ -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
10
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user