142 lines
5.8 KiB
TypeScript
142 lines
5.8 KiB
TypeScript
/**
|
|
* Shared OpenClaw Plugin Install Utilities
|
|
*
|
|
* Provides version-aware install/upgrade logic for bundled OpenClaw plugins
|
|
* (DingTalk, WeCom, QQBot, Feishu). Used both at app startup (to auto-upgrade
|
|
* stale plugins) and when a user configures a channel.
|
|
*/
|
|
import { app } from 'electron';
|
|
import { existsSync, cpSync, mkdirSync, rmSync, readFileSync } from 'node:fs';
|
|
import { homedir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { logger } from './logger';
|
|
|
|
// ── Version helper ───────────────────────────────────────────────────────────
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// ── Core install / upgrade logic ─────────────────────────────────────────────
|
|
|
|
export 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
|
|
logger.info(
|
|
`[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).` };
|
|
}
|
|
logger.info(`Installed ${pluginLabel} plugin from bundled mirror: ${sourceDir}`);
|
|
return { installed: true };
|
|
} catch {
|
|
return { installed: false, warning: `Failed to install bundled ${pluginLabel} plugin mirror` };
|
|
}
|
|
}
|
|
|
|
// ── Candidate source path builder ────────────────────────────────────────────
|
|
|
|
export 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),
|
|
];
|
|
}
|
|
|
|
// ── Per-channel plugin helpers ───────────────────────────────────────────────
|
|
|
|
export function ensureDingTalkPluginInstalled(): { installed: boolean; warning?: string } {
|
|
return ensurePluginInstalled('dingtalk', buildCandidateSources('dingtalk'), 'DingTalk');
|
|
}
|
|
|
|
export function ensureWeComPluginInstalled(): { installed: boolean; warning?: string } {
|
|
return ensurePluginInstalled('wecom', buildCandidateSources('wecom'), 'WeCom');
|
|
}
|
|
|
|
export function ensureFeishuPluginInstalled(): { installed: boolean; warning?: string } {
|
|
return ensurePluginInstalled(
|
|
'feishu-openclaw-plugin',
|
|
buildCandidateSources('feishu-openclaw-plugin'),
|
|
'Feishu',
|
|
);
|
|
}
|
|
|
|
export function ensureQQBotPluginInstalled(): { installed: boolean; warning?: string } {
|
|
return ensurePluginInstalled('qqbot', buildCandidateSources('qqbot'), 'QQ Bot');
|
|
}
|
|
|
|
// ── Bulk startup installer ───────────────────────────────────────────────────
|
|
|
|
/**
|
|
* All bundled plugins, in the same order as after-pack.cjs BUNDLED_PLUGINS.
|
|
*/
|
|
const ALL_BUNDLED_PLUGINS = [
|
|
{ fn: ensureDingTalkPluginInstalled, label: 'DingTalk' },
|
|
{ fn: ensureWeComPluginInstalled, label: 'WeCom' },
|
|
{ fn: ensureQQBotPluginInstalled, label: 'QQ Bot' },
|
|
{ fn: ensureFeishuPluginInstalled, label: 'Feishu' },
|
|
] as const;
|
|
|
|
/**
|
|
* Ensure all bundled OpenClaw plugins are installed/upgraded in
|
|
* `~/.openclaw/extensions/`. Designed to be called once at app startup
|
|
* as a fire-and-forget task — errors are logged but never thrown.
|
|
*/
|
|
export async function ensureAllBundledPluginsInstalled(): Promise<void> {
|
|
for (const { fn, label } of ALL_BUNDLED_PLUGINS) {
|
|
try {
|
|
const result = fn();
|
|
if (result.warning) {
|
|
logger.warn(`[plugin] ${label}: ${result.warning}`);
|
|
}
|
|
} catch (error) {
|
|
logger.warn(`[plugin] Failed to install/upgrade ${label} plugin:`, error);
|
|
}
|
|
}
|
|
}
|