feat(electron): auto-upgrade bundled OpenClaw plugins at app startup (#521)

This commit is contained in:
paisley
2026-03-16 13:19:06 +08:00
committed by GitHub
Unverified
parent 02d38d15db
commit 9ec23174c0
6 changed files with 172 additions and 292 deletions

View File

@@ -1,8 +1,4 @@
import type { IncomingMessage, ServerResponse } from 'http';
import { app } from 'electron';
import { existsSync, cpSync, mkdirSync, rmSync, readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
import {
deleteChannelConfig,
getChannelFormValues,
@@ -12,6 +8,12 @@ import {
validateChannelConfig,
validateChannelCredentials,
} from '../../utils/channel-config';
import {
ensureDingTalkPluginInstalled,
ensureFeishuPluginInstalled,
ensureQQBotPluginInstalled,
ensureWeComPluginInstalled,
} from '../../utils/plugin-install';
import { assignChannelToAgent, clearAllBindingsForChannel } from '../../utils/agent-config';
import { whatsAppLoginManager } from '../../utils/whatsapp-login';
import type { HostApiContext } from '../context';
@@ -46,100 +48,6 @@ function scheduleGatewayChannelSaveRefresh(
void reason;
}
// ── 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)) {
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 ${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).` };
}
return { installed: true };
} catch {
return { installed: false, warning: `Failed to install bundled ${pluginLabel} plugin mirror` };
}
}
// ── Per-channel plugin helpers (thin wrappers around ensurePluginInstalled) ──
function buildCandidateSources(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),
join(__dirname, '../../../build/openclaw-plugins', pluginDirName),
];
}
function ensureDingTalkPluginInstalled(): { installed: boolean; warning?: string } {
return ensurePluginInstalled('dingtalk', buildCandidateSources('dingtalk'), 'DingTalk');
}
function ensureWeComPluginInstalled(): { installed: boolean; warning?: string } {
return ensurePluginInstalled('wecom', buildCandidateSources('wecom'), 'WeCom');
}
function ensureFeishuPluginInstalled(): { installed: boolean; warning?: string } {
return ensurePluginInstalled(
'feishu-openclaw-plugin',
buildCandidateSources('feishu-openclaw-plugin'),
'Feishu',
);
}
function ensureQQBotPluginInstalled(): { installed: boolean; warning?: string } {
return ensurePluginInstalled('qqbot', buildCandidateSources('qqbot'), 'QQ Bot');
}
function toComparableConfig(input: Record<string, unknown>): Record<string, string> {
const next: Record<string, string> = {};
for (const [key, value] of Object.entries(input)) {