fix wecom channel (#530)
This commit is contained in:
committed by
GitHub
Unverified
parent
7e54aad9e6
commit
f1e2e9fa01
@@ -1,6 +1,6 @@
|
|||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { existsSync, readFileSync, cpSync, mkdirSync, rmSync, readdirSync, realpathSync } from 'fs';
|
import { existsSync, readFileSync, cpSync, mkdirSync, rmSync } from 'fs';
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { getAllSettings } from '../utils/store';
|
import { getAllSettings } from '../utils/store';
|
||||||
@@ -14,6 +14,7 @@ import { buildProxyEnv, resolveProxySettings } from '../utils/proxy';
|
|||||||
import { syncProxyConfigToOpenClaw } from '../utils/openclaw-proxy';
|
import { syncProxyConfigToOpenClaw } from '../utils/openclaw-proxy';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import { prependPathEntry } from '../utils/env-path';
|
import { prependPathEntry } from '../utils/env-path';
|
||||||
|
import { copyPluginFromNodeModules, fixupPluginManifest } from '../utils/plugin-install';
|
||||||
|
|
||||||
export interface GatewayLaunchContext {
|
export interface GatewayLaunchContext {
|
||||||
appSettings: Awaited<ReturnType<typeof getAllSettings>>;
|
appSettings: Awaited<ReturnType<typeof getAllSettings>>;
|
||||||
@@ -47,112 +48,6 @@ function readPluginVersion(pkgJsonPath: string): string | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Walk up from a path until we find a parent named node_modules. */
|
|
||||||
function findParentNodeModules(startPath: string): string | null {
|
|
||||||
let dir = startPath;
|
|
||||||
while (dir !== path.dirname(dir)) {
|
|
||||||
if (path.basename(dir) === 'node_modules') return dir;
|
|
||||||
dir = path.dirname(dir);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** List packages inside a node_modules dir (handles @scoped packages). */
|
|
||||||
function listPackagesInDir(nodeModulesDir: string): Array<{ name: string; fullPath: string }> {
|
|
||||||
const result: Array<{ name: string; fullPath: string }> = [];
|
|
||||||
if (!existsSync(nodeModulesDir)) return result;
|
|
||||||
const SKIP = new Set(['.bin', '.package-lock.json', '.modules.yaml', '.pnpm']);
|
|
||||||
for (const entry of readdirSync(nodeModulesDir, { withFileTypes: true })) {
|
|
||||||
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
||||||
if (SKIP.has(entry.name)) continue;
|
|
||||||
const entryPath = join(nodeModulesDir, entry.name);
|
|
||||||
if (entry.name.startsWith('@')) {
|
|
||||||
try {
|
|
||||||
for (const sub of readdirSync(entryPath)) {
|
|
||||||
result.push({ name: `${entry.name}/${sub}`, fullPath: join(entryPath, sub) });
|
|
||||||
}
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
} else {
|
|
||||||
result.push({ name: entry.name, fullPath: entryPath });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy a plugin from a pnpm node_modules location, including its
|
|
||||||
* transitive runtime dependencies (replicates bundle-openclaw-plugins.mjs
|
|
||||||
* logic).
|
|
||||||
*/
|
|
||||||
function copyPluginFromNodeModules(npmPkgPath: string, targetDir: string, npmName: string): void {
|
|
||||||
let realPath: string;
|
|
||||||
try {
|
|
||||||
realPath = realpathSync(npmPkgPath);
|
|
||||||
} catch {
|
|
||||||
throw new Error(`Cannot resolve real path for ${npmPkgPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Copy plugin package itself
|
|
||||||
rmSync(targetDir, { recursive: true, force: true });
|
|
||||||
mkdirSync(targetDir, { recursive: true });
|
|
||||||
cpSync(realPath, targetDir, { recursive: true, dereference: true });
|
|
||||||
|
|
||||||
// 2. Collect transitive deps from pnpm virtual store
|
|
||||||
const rootVirtualNM = findParentNodeModules(realPath);
|
|
||||||
if (!rootVirtualNM) {
|
|
||||||
logger.warn(`[plugin] Cannot find virtual store node_modules for ${npmName}, plugin may lack deps`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read peer deps to skip (they're provided by the host gateway)
|
|
||||||
const SKIP_PACKAGES = new Set(['typescript', '@playwright/test']);
|
|
||||||
try {
|
|
||||||
const pluginPkg = JSON.parse(readFileSync(join(targetDir, 'package.json'), 'utf-8'));
|
|
||||||
for (const peer of Object.keys(pluginPkg.peerDependencies || {})) {
|
|
||||||
SKIP_PACKAGES.add(peer);
|
|
||||||
}
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
|
|
||||||
const collected = new Map<string, string>(); // realPath → packageName
|
|
||||||
const queue: Array<{ nodeModulesDir: string; skipPkg: string }> = [
|
|
||||||
{ nodeModulesDir: rootVirtualNM, skipPkg: npmName },
|
|
||||||
];
|
|
||||||
|
|
||||||
while (queue.length > 0) {
|
|
||||||
const { nodeModulesDir, skipPkg } = queue.shift()!;
|
|
||||||
for (const { name, fullPath } of listPackagesInDir(nodeModulesDir)) {
|
|
||||||
if (name === skipPkg) continue;
|
|
||||||
if (SKIP_PACKAGES.has(name) || name.startsWith('@types/')) continue;
|
|
||||||
let depRealPath: string;
|
|
||||||
try {
|
|
||||||
depRealPath = realpathSync(fullPath);
|
|
||||||
} catch { continue; }
|
|
||||||
if (collected.has(depRealPath)) continue;
|
|
||||||
collected.set(depRealPath, name);
|
|
||||||
const depVirtualNM = findParentNodeModules(depRealPath);
|
|
||||||
if (depVirtualNM && depVirtualNM !== nodeModulesDir) {
|
|
||||||
queue.push({ nodeModulesDir: depVirtualNM, skipPkg: name });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Copy flattened deps into targetDir/node_modules/
|
|
||||||
const outputNM = join(targetDir, 'node_modules');
|
|
||||||
mkdirSync(outputNM, { recursive: true });
|
|
||||||
const copiedNames = new Set<string>();
|
|
||||||
for (const [depRealPath, pkgName] of collected) {
|
|
||||||
if (copiedNames.has(pkgName)) continue;
|
|
||||||
copiedNames.add(pkgName);
|
|
||||||
const dest = join(outputNM, pkgName);
|
|
||||||
try {
|
|
||||||
mkdirSync(path.dirname(dest), { recursive: true });
|
|
||||||
cpSync(depRealPath, dest, { recursive: true, dereference: true });
|
|
||||||
} catch { /* skip individual dep failures */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`[plugin] Copied ${copiedNames.size} deps for ${npmName}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildBundledPluginSources(pluginDirName: string): string[] {
|
function buildBundledPluginSources(pluginDirName: string): string[] {
|
||||||
return app.isPackaged
|
return app.isPackaged
|
||||||
? [
|
? [
|
||||||
@@ -179,9 +74,8 @@ function ensureConfiguredPluginsUpgraded(configuredChannels: string[]): void {
|
|||||||
|
|
||||||
const targetDir = join(homedir(), '.openclaw', 'extensions', dirName);
|
const targetDir = join(homedir(), '.openclaw', 'extensions', dirName);
|
||||||
const targetManifest = join(targetDir, 'openclaw.plugin.json');
|
const targetManifest = join(targetDir, 'openclaw.plugin.json');
|
||||||
if (!existsSync(targetManifest)) continue; // not installed, nothing to upgrade
|
const isInstalled = existsSync(targetManifest);
|
||||||
|
const installedVersion = isInstalled ? readPluginVersion(join(targetDir, 'package.json')) : null;
|
||||||
const installedVersion = readPluginVersion(join(targetDir, 'package.json'));
|
|
||||||
|
|
||||||
// Try bundled sources first (packaged mode or if bundle-plugins was run)
|
// Try bundled sources first (packaged mode or if bundle-plugins was run)
|
||||||
const bundledSources = buildBundledPluginSources(dirName);
|
const bundledSources = buildBundledPluginSources(dirName);
|
||||||
@@ -189,14 +83,16 @@ function ensureConfiguredPluginsUpgraded(configuredChannels: string[]): void {
|
|||||||
|
|
||||||
if (bundledDir) {
|
if (bundledDir) {
|
||||||
const sourceVersion = readPluginVersion(join(bundledDir, 'package.json'));
|
const sourceVersion = readPluginVersion(join(bundledDir, 'package.json'));
|
||||||
if (sourceVersion && installedVersion && sourceVersion !== installedVersion) {
|
// Install or upgrade if version differs or plugin not installed
|
||||||
logger.info(`[plugin] Auto-upgrading ${channelType} plugin: ${installedVersion} → ${sourceVersion} (bundled)`);
|
if (!isInstalled || (sourceVersion && installedVersion && sourceVersion !== installedVersion)) {
|
||||||
|
logger.info(`[plugin] ${isInstalled ? 'Auto-upgrading' : 'Installing'} ${channelType} plugin${isInstalled ? `: ${installedVersion} → ${sourceVersion}` : `: ${sourceVersion}`} (bundled)`);
|
||||||
try {
|
try {
|
||||||
mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true });
|
mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true });
|
||||||
rmSync(targetDir, { recursive: true, force: true });
|
rmSync(targetDir, { recursive: true, force: true });
|
||||||
cpSync(bundledDir, targetDir, { recursive: true, dereference: true });
|
cpSync(bundledDir, targetDir, { recursive: true, dereference: true });
|
||||||
|
fixupPluginManifest(targetDir);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(`[plugin] Failed to auto-upgrade ${channelType} plugin:`, err);
|
logger.warn(`[plugin] Failed to ${isInstalled ? 'auto-upgrade' : 'install'} ${channelType} plugin:`, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -207,14 +103,17 @@ function ensureConfiguredPluginsUpgraded(configuredChannels: string[]): void {
|
|||||||
const npmPkgPath = join(process.cwd(), 'node_modules', ...npmName.split('/'));
|
const npmPkgPath = join(process.cwd(), 'node_modules', ...npmName.split('/'));
|
||||||
if (!existsSync(join(npmPkgPath, 'openclaw.plugin.json'))) continue;
|
if (!existsSync(join(npmPkgPath, 'openclaw.plugin.json'))) continue;
|
||||||
const sourceVersion = readPluginVersion(join(npmPkgPath, 'package.json'));
|
const sourceVersion = readPluginVersion(join(npmPkgPath, 'package.json'));
|
||||||
if (!sourceVersion || !installedVersion || sourceVersion === installedVersion) continue;
|
if (!sourceVersion) continue;
|
||||||
|
// Skip only if installed AND same version
|
||||||
|
if (isInstalled && installedVersion && sourceVersion === installedVersion) continue;
|
||||||
|
|
||||||
logger.info(`[plugin] Auto-upgrading ${channelType} plugin: ${installedVersion} → ${sourceVersion} (dev/node_modules)`);
|
logger.info(`[plugin] ${isInstalled ? 'Auto-upgrading' : 'Installing'} ${channelType} plugin${isInstalled ? `: ${installedVersion} → ${sourceVersion}` : `: ${sourceVersion}`} (dev/node_modules)`);
|
||||||
try {
|
try {
|
||||||
mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true });
|
mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true });
|
||||||
copyPluginFromNodeModules(npmPkgPath, targetDir, npmName);
|
copyPluginFromNodeModules(npmPkgPath, targetDir, npmName);
|
||||||
|
fixupPluginManifest(targetDir);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(`[plugin] Failed to auto-upgrade ${channelType} plugin from node_modules:`, err);
|
logger.warn(`[plugin] Failed to ${isInstalled ? 'auto-upgrade' : 'install'} ${channelType} plugin from node_modules:`, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { withConfigLock } from './config-mutex';
|
|||||||
|
|
||||||
const OPENCLAW_DIR = join(homedir(), '.openclaw');
|
const OPENCLAW_DIR = join(homedir(), '.openclaw');
|
||||||
const CONFIG_FILE = join(OPENCLAW_DIR, 'openclaw.json');
|
const CONFIG_FILE = join(OPENCLAW_DIR, 'openclaw.json');
|
||||||
const WECOM_PLUGIN_ID = 'wecom-openclaw-plugin';
|
const WECOM_PLUGIN_ID = 'wecom';
|
||||||
const FEISHU_PLUGIN_ID = 'openclaw-lark';
|
const FEISHU_PLUGIN_ID = 'openclaw-lark';
|
||||||
const LEGACY_FEISHU_PLUGIN_ID = 'feishu-openclaw-plugin';
|
const LEGACY_FEISHU_PLUGIN_ID = 'feishu-openclaw-plugin';
|
||||||
const DEFAULT_ACCOUNT_ID = 'default';
|
const DEFAULT_ACCOUNT_ID = 'default';
|
||||||
@@ -176,7 +176,13 @@ function ensurePluginAllowlist(currentConfig: OpenClawConfig, channelType: strin
|
|||||||
|
|
||||||
if (channelType === 'wecom') {
|
if (channelType === 'wecom') {
|
||||||
if (!currentConfig.plugins) {
|
if (!currentConfig.plugins) {
|
||||||
currentConfig.plugins = { allow: [WECOM_PLUGIN_ID], enabled: true };
|
currentConfig.plugins = {
|
||||||
|
allow: [WECOM_PLUGIN_ID],
|
||||||
|
enabled: true,
|
||||||
|
entries: {
|
||||||
|
[WECOM_PLUGIN_ID]: { enabled: true }
|
||||||
|
}
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
currentConfig.plugins.enabled = true;
|
currentConfig.plugins.enabled = true;
|
||||||
const allow: string[] = Array.isArray(currentConfig.plugins.allow)
|
const allow: string[] = Array.isArray(currentConfig.plugins.allow)
|
||||||
@@ -188,6 +194,14 @@ function ensurePluginAllowlist(currentConfig: OpenClawConfig, channelType: strin
|
|||||||
} else if (normalizedAllow.length !== allow.length) {
|
} else if (normalizedAllow.length !== allow.length) {
|
||||||
currentConfig.plugins.allow = normalizedAllow;
|
currentConfig.plugins.allow = normalizedAllow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!currentConfig.plugins.entries) {
|
||||||
|
currentConfig.plugins.entries = {};
|
||||||
|
}
|
||||||
|
if (!currentConfig.plugins.entries[WECOM_PLUGIN_ID]) {
|
||||||
|
currentConfig.plugins.entries[WECOM_PLUGIN_ID] = {};
|
||||||
|
}
|
||||||
|
currentConfig.plugins.entries[WECOM_PLUGIN_ID].enabled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1055,6 +1055,31 @@ export async function sanitizeOpenClawConfig(): Promise<void> {
|
|||||||
modified = true;
|
modified = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── wecom-openclaw-plugin → wecom migration ────────────────
|
||||||
|
const LEGACY_WECOM_ID = 'wecom-openclaw-plugin';
|
||||||
|
const NEW_WECOM_ID = 'wecom';
|
||||||
|
if (Array.isArray(pluginsObj.allow)) {
|
||||||
|
const allowArr = pluginsObj.allow as string[];
|
||||||
|
const legacyIdx = allowArr.indexOf(LEGACY_WECOM_ID);
|
||||||
|
if (legacyIdx !== -1) {
|
||||||
|
if (!allowArr.includes(NEW_WECOM_ID)) {
|
||||||
|
allowArr[legacyIdx] = NEW_WECOM_ID;
|
||||||
|
} else {
|
||||||
|
allowArr.splice(legacyIdx, 1);
|
||||||
|
}
|
||||||
|
console.log(`[sanitize] Migrated plugins.allow: ${LEGACY_WECOM_ID} → ${NEW_WECOM_ID}`);
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pEntries?.[LEGACY_WECOM_ID]) {
|
||||||
|
if (!pEntries[NEW_WECOM_ID]) {
|
||||||
|
pEntries[NEW_WECOM_ID] = pEntries[LEGACY_WECOM_ID];
|
||||||
|
}
|
||||||
|
delete pEntries[LEGACY_WECOM_ID];
|
||||||
|
console.log(`[sanitize] Migrated plugins.entries: ${LEGACY_WECOM_ID} → ${NEW_WECOM_ID}`);
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Remove bare 'feishu' when openclaw-lark is present ─────────
|
// ── Remove bare 'feishu' when openclaw-lark is present ─────────
|
||||||
// The Gateway binary automatically adds bare 'feishu' to plugins.allow
|
// The Gateway binary automatically adds bare 'feishu' to plugins.allow
|
||||||
// because the openclaw-lark plugin registers the 'feishu' channel.
|
// because the openclaw-lark plugin registers the 'feishu' channel.
|
||||||
|
|||||||
@@ -6,11 +6,136 @@
|
|||||||
* stale plugins) and when a user configures a channel.
|
* stale plugins) and when a user configures a channel.
|
||||||
*/
|
*/
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import { existsSync, cpSync, mkdirSync, rmSync, readFileSync } from 'node:fs';
|
import path from 'node:path';
|
||||||
|
import { existsSync, cpSync, mkdirSync, rmSync, readFileSync, writeFileSync, readdirSync, realpathSync } from 'node:fs';
|
||||||
import { homedir } from 'node:os';
|
import { homedir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
|
|
||||||
|
// ── Known plugin-ID corrections ─────────────────────────────────────────────
|
||||||
|
// Some npm packages ship with an openclaw.plugin.json whose "id" field
|
||||||
|
// doesn't match the ID the plugin code actually exports. After copying we
|
||||||
|
// patch both the manifest AND the compiled JS so the Gateway accepts them.
|
||||||
|
const MANIFEST_ID_FIXES: Record<string, string> = {
|
||||||
|
'wecom-openclaw-plugin': 'wecom',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After a plugin has been copied to ~/.openclaw/extensions/<dir>, fix any
|
||||||
|
* known manifest-ID mismatches so the Gateway can load the plugin.
|
||||||
|
* Also patches package.json fields that the Gateway uses as "entry hints".
|
||||||
|
*/
|
||||||
|
export function fixupPluginManifest(targetDir: string): void {
|
||||||
|
// 1. Fix openclaw.plugin.json id
|
||||||
|
const manifestPath = join(targetDir, 'openclaw.plugin.json');
|
||||||
|
try {
|
||||||
|
const raw = readFileSync(manifestPath, 'utf-8');
|
||||||
|
const manifest = JSON.parse(raw);
|
||||||
|
const oldId = manifest.id as string | undefined;
|
||||||
|
if (oldId && MANIFEST_ID_FIXES[oldId]) {
|
||||||
|
const newId = MANIFEST_ID_FIXES[oldId];
|
||||||
|
manifest.id = newId;
|
||||||
|
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
|
||||||
|
logger.info(`[plugin] Fixed manifest ID: ${oldId} → ${newId}`);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// manifest may not exist yet — ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fix package.json fields that Gateway uses as "entry hints"
|
||||||
|
const pkgPath = join(targetDir, 'package.json');
|
||||||
|
try {
|
||||||
|
const raw = readFileSync(pkgPath, 'utf-8');
|
||||||
|
const pkg = JSON.parse(raw);
|
||||||
|
let modified = false;
|
||||||
|
|
||||||
|
// Check if the package name contains a legacy ID that needs fixing
|
||||||
|
for (const [oldId, newId] of Object.entries(MANIFEST_ID_FIXES)) {
|
||||||
|
if (typeof pkg.name === 'string' && pkg.name.includes(oldId)) {
|
||||||
|
pkg.name = pkg.name.replace(oldId, newId);
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
const install = pkg.openclaw?.install;
|
||||||
|
if (install) {
|
||||||
|
if (typeof install.npmSpec === 'string' && install.npmSpec.includes(oldId)) {
|
||||||
|
install.npmSpec = install.npmSpec.replace(oldId, newId);
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
if (typeof install.localPath === 'string' && install.localPath.includes(oldId)) {
|
||||||
|
install.localPath = install.localPath.replace(oldId, newId);
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modified) {
|
||||||
|
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
||||||
|
logger.info(`[plugin] Fixed package.json entry hints in ${targetDir}`);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fix hardcoded plugin IDs in compiled JS entry files.
|
||||||
|
// The Gateway validates that the JS export's `id` matches the manifest.
|
||||||
|
patchPluginEntryIds(targetDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch the compiled JS entry files so the hardcoded `id` field in the
|
||||||
|
* plugin export matches the manifest. Without this, the Gateway rejects
|
||||||
|
* the plugin with "plugin id mismatch".
|
||||||
|
*/
|
||||||
|
function patchPluginEntryIds(targetDir: string): void {
|
||||||
|
const pkgPath = join(targetDir, 'package.json');
|
||||||
|
let pkg: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryFiles = [pkg.main, pkg.module].filter(Boolean) as string[];
|
||||||
|
|
||||||
|
for (const entry of entryFiles) {
|
||||||
|
const entryPath = join(targetDir, entry);
|
||||||
|
if (!existsSync(entryPath)) continue;
|
||||||
|
|
||||||
|
let content: string;
|
||||||
|
try {
|
||||||
|
content = readFileSync(entryPath, 'utf-8');
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let patched = false;
|
||||||
|
for (const [wrongId, correctId] of Object.entries(MANIFEST_ID_FIXES)) {
|
||||||
|
// Match patterns like: id: "wecom-openclaw-plugin" or id: 'wecom-openclaw-plugin'
|
||||||
|
const escapedWrongId = wrongId.replace(/-/g, '\\-');
|
||||||
|
const pattern = new RegExp(`(\\bid\\s*:\\s*)(["'])${escapedWrongId}\\2`, 'g');
|
||||||
|
const replaced = content.replace(pattern, `$1$2${correctId}$2`);
|
||||||
|
if (replaced !== content) {
|
||||||
|
content = replaced;
|
||||||
|
patched = true;
|
||||||
|
logger.info(`[plugin] Patched plugin ID in ${entry}: "${wrongId}" → "${correctId}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patched) {
|
||||||
|
writeFileSync(entryPath, content, 'utf-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Plugin npm name mapping ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const PLUGIN_NPM_NAMES: Record<string, string> = {
|
||||||
|
dingtalk: '@soimy/dingtalk',
|
||||||
|
wecom: '@wecom/wecom-openclaw-plugin',
|
||||||
|
'feishu-openclaw-plugin': '@larksuite/openclaw-lark',
|
||||||
|
qqbot: '@sliverp/qqbot',
|
||||||
|
};
|
||||||
|
|
||||||
// ── Version helper ───────────────────────────────────────────────────────────
|
// ── Version helper ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function readPluginVersion(pkgJsonPath: string): string | null {
|
function readPluginVersion(pkgJsonPath: string): string | null {
|
||||||
@@ -23,6 +148,114 @@ function readPluginVersion(pkgJsonPath: string): string | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── pnpm-aware node_modules copy helpers ─────────────────────────────────────
|
||||||
|
|
||||||
|
/** Walk up from a path until we find a parent named node_modules. */
|
||||||
|
function findParentNodeModules(startPath: string): string | null {
|
||||||
|
let dir = startPath;
|
||||||
|
while (dir !== path.dirname(dir)) {
|
||||||
|
if (path.basename(dir) === 'node_modules') return dir;
|
||||||
|
dir = path.dirname(dir);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** List packages inside a node_modules dir (handles @scoped packages). */
|
||||||
|
function listPackagesInDir(nodeModulesDir: string): Array<{ name: string; fullPath: string }> {
|
||||||
|
const result: Array<{ name: string; fullPath: string }> = [];
|
||||||
|
if (!existsSync(nodeModulesDir)) return result;
|
||||||
|
const SKIP = new Set(['.bin', '.package-lock.json', '.modules.yaml', '.pnpm']);
|
||||||
|
for (const entry of readdirSync(nodeModulesDir, { withFileTypes: true })) {
|
||||||
|
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
||||||
|
if (SKIP.has(entry.name)) continue;
|
||||||
|
const entryPath = join(nodeModulesDir, entry.name);
|
||||||
|
if (entry.name.startsWith('@')) {
|
||||||
|
try {
|
||||||
|
for (const sub of readdirSync(entryPath)) {
|
||||||
|
result.push({ name: `${entry.name}/${sub}`, fullPath: join(entryPath, sub) });
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
} else {
|
||||||
|
result.push({ name: entry.name, fullPath: entryPath });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy a plugin from a pnpm node_modules location, including its
|
||||||
|
* transitive runtime dependencies (replicates bundle-openclaw-plugins.mjs
|
||||||
|
* logic).
|
||||||
|
*/
|
||||||
|
export function copyPluginFromNodeModules(npmPkgPath: string, targetDir: string, npmName: string): void {
|
||||||
|
let realPath: string;
|
||||||
|
try {
|
||||||
|
realPath = realpathSync(npmPkgPath);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Cannot resolve real path for ${npmPkgPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Copy plugin package itself
|
||||||
|
rmSync(targetDir, { recursive: true, force: true });
|
||||||
|
mkdirSync(targetDir, { recursive: true });
|
||||||
|
cpSync(realPath, targetDir, { recursive: true, dereference: true });
|
||||||
|
|
||||||
|
// 2. Collect transitive deps from pnpm virtual store
|
||||||
|
const rootVirtualNM = findParentNodeModules(realPath);
|
||||||
|
if (!rootVirtualNM) {
|
||||||
|
logger.warn(`[plugin] Cannot find virtual store node_modules for ${npmName}, plugin may lack deps`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read peer deps to skip (they're provided by the host gateway)
|
||||||
|
const SKIP_PACKAGES = new Set(['typescript', '@playwright/test']);
|
||||||
|
try {
|
||||||
|
const pluginPkg = JSON.parse(readFileSync(join(targetDir, 'package.json'), 'utf-8'));
|
||||||
|
for (const peer of Object.keys(pluginPkg.peerDependencies || {})) {
|
||||||
|
SKIP_PACKAGES.add(peer);
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
|
||||||
|
const collected = new Map<string, string>(); // realPath → packageName
|
||||||
|
const queue: Array<{ nodeModulesDir: string; skipPkg: string }> = [
|
||||||
|
{ nodeModulesDir: rootVirtualNM, skipPkg: npmName },
|
||||||
|
];
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const { nodeModulesDir, skipPkg } = queue.shift()!;
|
||||||
|
for (const { name, fullPath } of listPackagesInDir(nodeModulesDir)) {
|
||||||
|
if (name === skipPkg) continue;
|
||||||
|
if (SKIP_PACKAGES.has(name) || name.startsWith('@types/')) continue;
|
||||||
|
let depRealPath: string;
|
||||||
|
try {
|
||||||
|
depRealPath = realpathSync(fullPath);
|
||||||
|
} catch { continue; }
|
||||||
|
if (collected.has(depRealPath)) continue;
|
||||||
|
collected.set(depRealPath, name);
|
||||||
|
const depVirtualNM = findParentNodeModules(depRealPath);
|
||||||
|
if (depVirtualNM && depVirtualNM !== nodeModulesDir) {
|
||||||
|
queue.push({ nodeModulesDir: depVirtualNM, skipPkg: name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Copy flattened deps into targetDir/node_modules/
|
||||||
|
const outputNM = join(targetDir, 'node_modules');
|
||||||
|
mkdirSync(outputNM, { recursive: true });
|
||||||
|
const copiedNames = new Set<string>();
|
||||||
|
for (const [depRealPath, pkgName] of collected) {
|
||||||
|
if (copiedNames.has(pkgName)) continue;
|
||||||
|
copiedNames.add(pkgName);
|
||||||
|
const dest = join(outputNM, pkgName);
|
||||||
|
try {
|
||||||
|
mkdirSync(path.dirname(dest), { recursive: true });
|
||||||
|
cpSync(depRealPath, dest, { recursive: true, dereference: true });
|
||||||
|
} catch { /* skip individual dep failures */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`[plugin] Copied ${copiedNames.size} deps for ${npmName}`);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Core install / upgrade logic ─────────────────────────────────────────────
|
// ── Core install / upgrade logic ─────────────────────────────────────────────
|
||||||
|
|
||||||
export function ensurePluginInstalled(
|
export function ensurePluginInstalled(
|
||||||
@@ -50,14 +283,8 @@ export function ensurePluginInstalled(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fresh install or upgrade
|
// Fresh install or upgrade — try bundled/build sources first
|
||||||
if (!sourceDir) {
|
if (sourceDir) {
|
||||||
return {
|
|
||||||
installed: false,
|
|
||||||
warning: `Bundled ${pluginLabel} plugin mirror not found. Checked: ${candidateSources.join(' | ')}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true });
|
mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true });
|
||||||
rmSync(targetDir, { recursive: true, force: true });
|
rmSync(targetDir, { recursive: true, force: true });
|
||||||
@@ -65,6 +292,7 @@ export function ensurePluginInstalled(
|
|||||||
if (!existsSync(join(targetDir, 'openclaw.plugin.json'))) {
|
if (!existsSync(join(targetDir, 'openclaw.plugin.json'))) {
|
||||||
return { installed: false, warning: `Failed to install ${pluginLabel} plugin mirror (manifest missing).` };
|
return { installed: false, warning: `Failed to install ${pluginLabel} plugin mirror (manifest missing).` };
|
||||||
}
|
}
|
||||||
|
fixupPluginManifest(targetDir);
|
||||||
logger.info(`Installed ${pluginLabel} plugin from bundled mirror: ${sourceDir}`);
|
logger.info(`Installed ${pluginLabel} plugin from bundled mirror: ${sourceDir}`);
|
||||||
return { installed: true };
|
return { installed: true };
|
||||||
} catch {
|
} catch {
|
||||||
@@ -72,6 +300,42 @@ export function ensurePluginInstalled(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dev mode fallback: copy from node_modules with pnpm-aware dep resolution
|
||||||
|
if (!app.isPackaged) {
|
||||||
|
const npmName = PLUGIN_NPM_NAMES[pluginDirName];
|
||||||
|
if (npmName) {
|
||||||
|
const npmPkgPath = join(process.cwd(), 'node_modules', ...npmName.split('/'));
|
||||||
|
if (existsSync(join(npmPkgPath, 'openclaw.plugin.json'))) {
|
||||||
|
const installedVersion = existsSync(targetPkgJson) ? readPluginVersion(targetPkgJson) : null;
|
||||||
|
const sourceVersion = readPluginVersion(join(npmPkgPath, 'package.json'));
|
||||||
|
if (sourceVersion && (!installedVersion || sourceVersion !== installedVersion)) {
|
||||||
|
logger.info(
|
||||||
|
`[plugin] ${installedVersion ? 'Upgrading' : 'Installing'} ${pluginLabel} plugin` +
|
||||||
|
`${installedVersion ? `: ${installedVersion} → ${sourceVersion}` : `: ${sourceVersion}`} (dev/node_modules)`,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true });
|
||||||
|
copyPluginFromNodeModules(npmPkgPath, targetDir, npmName);
|
||||||
|
fixupPluginManifest(targetDir);
|
||||||
|
if (existsSync(join(targetDir, 'openclaw.plugin.json'))) {
|
||||||
|
return { installed: true };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`[plugin] Failed to install ${pluginLabel} plugin from node_modules:`, err);
|
||||||
|
}
|
||||||
|
} else if (existsSync(targetManifest)) {
|
||||||
|
return { installed: true }; // same version, already installed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
installed: false,
|
||||||
|
warning: `Bundled ${pluginLabel} plugin mirror not found. Checked: ${candidateSources.join(' | ')}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ── Candidate source path builder ────────────────────────────────────────────
|
// ── Candidate source path builder ────────────────────────────────────────────
|
||||||
|
|
||||||
export function buildCandidateSources(pluginDirName: string): string[] {
|
export function buildCandidateSources(pluginDirName: string): string[] {
|
||||||
|
|||||||
@@ -86,7 +86,7 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
||||||
"@typescript-eslint/parser": "^8.56.0",
|
"@typescript-eslint/parser": "^8.56.0",
|
||||||
"@vitejs/plugin-react": "^5.1.4",
|
"@vitejs/plugin-react": "^5.1.4",
|
||||||
"@wecom/wecom-openclaw-plugin": "^1.0.6",
|
"@wecom/wecom-openclaw-plugin": "^1.0.11",
|
||||||
"autoprefixer": "^10.4.24",
|
"autoprefixer": "^10.4.24",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|||||||
33
pnpm-lock.yaml
generated
33
pnpm-lock.yaml
generated
@@ -109,8 +109,8 @@ importers:
|
|||||||
specifier: ^5.1.4
|
specifier: ^5.1.4
|
||||||
version: 5.1.4(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(yaml@2.8.2))
|
version: 5.1.4(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(yaml@2.8.2))
|
||||||
'@wecom/wecom-openclaw-plugin':
|
'@wecom/wecom-openclaw-plugin':
|
||||||
specifier: ^1.0.6
|
specifier: ^1.0.11
|
||||||
version: 1.0.6
|
version: 1.0.11
|
||||||
autoprefixer:
|
autoprefixer:
|
||||||
specifier: ^10.4.24
|
specifier: ^10.4.24
|
||||||
version: 10.4.24(postcss@8.5.6)
|
version: 10.4.24(postcss@8.5.6)
|
||||||
@@ -3216,11 +3216,11 @@ packages:
|
|||||||
'@vitest/utils@4.0.18':
|
'@vitest/utils@4.0.18':
|
||||||
resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==}
|
resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==}
|
||||||
|
|
||||||
'@wecom/aibot-node-sdk@1.0.1':
|
'@wecom/aibot-node-sdk@1.0.2':
|
||||||
resolution: {integrity: sha512-c/sa1IvRKIP+4rZfRV2v70FaXB92+BJIh+vedZkPa8wZ1dwIUyvGg7ydkfYRIwFDzjO9IJZUX5V14EUQYVopAg==}
|
resolution: {integrity: sha512-azClUIMWWF5vs8K1YWBiNykTFUawej0Z1ooN0ZMGX/PlLB/BK0dQfwbLc1a5Wj3bLRLaFb8HuCTuBrxLnJKJ7g==}
|
||||||
|
|
||||||
'@wecom/wecom-openclaw-plugin@1.0.6':
|
'@wecom/wecom-openclaw-plugin@1.0.11':
|
||||||
resolution: {integrity: sha512-1yn6P3KGdEfKoTuGH0Ot4vuoHOFqZJ+qlVrEXYBzkPwtNHb7s2ja2YKizaffYWb0h2s464PEXKhmkQq/RRJwkg==}
|
resolution: {integrity: sha512-TqyWvi8AxPyii/fUZk/rVR4a5jXl6PHte6wuqgtbrWXFoOfYyKtnyjmtGJk3/kf1ZOjgHu2N6lfhz5fA6bTCyw==}
|
||||||
|
|
||||||
'@whiskeysockets/baileys@7.0.0-rc.9':
|
'@whiskeysockets/baileys@7.0.0-rc.9':
|
||||||
resolution: {integrity: sha512-YFm5gKXfDP9byCXCW3OPHKXLzrAKzolzgVUlRosHHgwbnf2YOO3XknkMm6J7+F0ns8OA0uuSBhgkRHTDtqkacw==}
|
resolution: {integrity: sha512-YFm5gKXfDP9byCXCW3OPHKXLzrAKzolzgVUlRosHHgwbnf2YOO3XknkMm6J7+F0ns8OA0uuSBhgkRHTDtqkacw==}
|
||||||
@@ -4295,10 +4295,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
file-type@21.3.0:
|
|
||||||
resolution: {integrity: sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==}
|
|
||||||
engines: {node: '>=20'}
|
|
||||||
|
|
||||||
file-type@21.3.2:
|
file-type@21.3.2:
|
||||||
resolution: {integrity: sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w==}
|
resolution: {integrity: sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
@@ -10885,7 +10881,7 @@ snapshots:
|
|||||||
'@vitest/pretty-format': 4.0.18
|
'@vitest/pretty-format': 4.0.18
|
||||||
tinyrainbow: 3.0.3
|
tinyrainbow: 3.0.3
|
||||||
|
|
||||||
'@wecom/aibot-node-sdk@1.0.1':
|
'@wecom/aibot-node-sdk@1.0.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
axios: 1.13.5(debug@4.4.3)
|
axios: 1.13.5(debug@4.4.3)
|
||||||
eventemitter3: 5.0.4
|
eventemitter3: 5.0.4
|
||||||
@@ -10895,10 +10891,10 @@ snapshots:
|
|||||||
- debug
|
- debug
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
'@wecom/wecom-openclaw-plugin@1.0.6':
|
'@wecom/wecom-openclaw-plugin@1.0.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@wecom/aibot-node-sdk': 1.0.1
|
'@wecom/aibot-node-sdk': 1.0.2
|
||||||
file-type: 21.3.0
|
file-type: 21.3.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
- debug
|
- debug
|
||||||
@@ -12239,15 +12235,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flat-cache: 4.0.1
|
flat-cache: 4.0.1
|
||||||
|
|
||||||
file-type@21.3.0:
|
|
||||||
dependencies:
|
|
||||||
'@tokenizer/inflate': 0.4.1
|
|
||||||
strtok3: 10.3.4
|
|
||||||
token-types: 6.1.2
|
|
||||||
uint8array-extras: 1.5.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
file-type@21.3.2:
|
file-type@21.3.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tokenizer/inflate': 0.4.1
|
'@tokenizer/inflate': 0.4.1
|
||||||
|
|||||||
@@ -204,6 +204,48 @@ function patchBrokenModules(nodeModulesDir) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Plugin ID mismatch patcher ───────────────────────────────────────────────
|
||||||
|
// Some plugins (e.g. wecom) have a compiled JS entry that hardcodes a different
|
||||||
|
// ID than what openclaw.plugin.json declares. The Gateway rejects mismatches,
|
||||||
|
// so we fix them after copying.
|
||||||
|
|
||||||
|
const PLUGIN_ID_FIXES = {
|
||||||
|
'wecom-openclaw-plugin': 'wecom',
|
||||||
|
};
|
||||||
|
|
||||||
|
function patchPluginIds(pluginDir, expectedId) {
|
||||||
|
const { readFileSync, writeFileSync } = require('fs');
|
||||||
|
|
||||||
|
const pkgJsonPath = join(pluginDir, 'package.json');
|
||||||
|
if (!existsSync(pkgJsonPath)) return;
|
||||||
|
|
||||||
|
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
|
||||||
|
const entryFiles = [pkg.main, pkg.module].filter(Boolean);
|
||||||
|
|
||||||
|
for (const entry of entryFiles) {
|
||||||
|
const entryPath = join(pluginDir, entry);
|
||||||
|
if (!existsSync(entryPath)) continue;
|
||||||
|
|
||||||
|
let content = readFileSync(entryPath, 'utf8');
|
||||||
|
let patched = false;
|
||||||
|
|
||||||
|
for (const [wrongId, correctId] of Object.entries(PLUGIN_ID_FIXES)) {
|
||||||
|
if (correctId !== expectedId) continue;
|
||||||
|
const pattern = new RegExp(`(\\bid\\s*:\\s*)(["'])${wrongId.replace(/-/g, '\\-')}\\2`, 'g');
|
||||||
|
const replaced = content.replace(pattern, `$1$2${correctId}$2`);
|
||||||
|
if (replaced !== content) {
|
||||||
|
content = replaced;
|
||||||
|
patched = true;
|
||||||
|
console.log(`[after-pack] 🩹 Patching plugin ID in ${entry}: "${wrongId}" → "${correctId}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patched) {
|
||||||
|
writeFileSync(entryPath, content, 'utf8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Plugin bundler ───────────────────────────────────────────────────────────
|
// ── Plugin bundler ───────────────────────────────────────────────────────────
|
||||||
// Bundles a single OpenClaw plugin (and its transitive deps) from node_modules
|
// Bundles a single OpenClaw plugin (and its transitive deps) from node_modules
|
||||||
// directly into the packaged resources directory. Mirrors the logic in
|
// directly into the packaged resources directory. Mirrors the logic in
|
||||||
@@ -383,6 +425,8 @@ exports.default = async function afterPack(context) {
|
|||||||
cleanupKoffi(pluginNM, platform, arch);
|
cleanupKoffi(pluginNM, platform, arch);
|
||||||
cleanupNativePlatformPackages(pluginNM, platform, arch);
|
cleanupNativePlatformPackages(pluginNM, platform, arch);
|
||||||
}
|
}
|
||||||
|
// Fix hardcoded plugin ID mismatches in compiled JS
|
||||||
|
patchPluginIds(pluginDestDir, pluginId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -171,9 +171,68 @@ function bundleOnePlugin({ npmName, pluginId }) {
|
|||||||
throw new Error(`Missing openclaw.plugin.json in bundled plugin output: ${pluginId}`);
|
throw new Error(`Missing openclaw.plugin.json in bundled plugin output: ${pluginId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4) Patch plugin ID mismatch: some npm packages hardcode a different ID in
|
||||||
|
// their JS output than what openclaw.plugin.json declares. The Gateway
|
||||||
|
// validates that these match, so we fix it post-copy.
|
||||||
|
patchPluginId(outputDir, pluginId);
|
||||||
|
|
||||||
echo` ✅ ${pluginId}: copied ${copiedCount} deps (skipped dupes: ${skippedDupes})`;
|
echo` ✅ ${pluginId}: copied ${copiedCount} deps (skipped dupes: ${skippedDupes})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch plugin entry JS files so the exported `id` matches openclaw.plugin.json.
|
||||||
|
* Some plugins (e.g. wecom) ship with a hardcoded ID in their compiled output
|
||||||
|
* that differs from the manifest, causing a Gateway "plugin id mismatch" error.
|
||||||
|
*/
|
||||||
|
function patchPluginId(pluginDir, expectedId) {
|
||||||
|
const manifestPath = path.join(pluginDir, 'openclaw.plugin.json');
|
||||||
|
if (!fs.existsSync(manifestPath)) return;
|
||||||
|
|
||||||
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||||
|
const manifestId = manifest.id;
|
||||||
|
if (manifestId !== expectedId) {
|
||||||
|
echo` ⚠️ Manifest ID "${manifestId}" doesn't match expected "${expectedId}", skipping patch`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the package.json to find the main entry point
|
||||||
|
const pkgJsonPath = path.join(pluginDir, 'package.json');
|
||||||
|
if (!fs.existsSync(pkgJsonPath)) return;
|
||||||
|
|
||||||
|
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
||||||
|
const entryFiles = [pkg.main, pkg.module].filter(Boolean);
|
||||||
|
|
||||||
|
// Known ID mismatches to patch. Keys are the wrong ID found in compiled JS,
|
||||||
|
// values are the correct ID (must match openclaw.plugin.json).
|
||||||
|
const ID_FIXES = {
|
||||||
|
'wecom-openclaw-plugin': 'wecom',
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const entry of entryFiles) {
|
||||||
|
const entryPath = path.join(pluginDir, entry);
|
||||||
|
if (!fs.existsSync(entryPath)) continue;
|
||||||
|
|
||||||
|
let content = fs.readFileSync(entryPath, 'utf8');
|
||||||
|
let patched = false;
|
||||||
|
|
||||||
|
for (const [wrongId, correctId] of Object.entries(ID_FIXES)) {
|
||||||
|
if (correctId !== expectedId) continue;
|
||||||
|
// Replace id: "wecom-openclaw-plugin" or id: 'wecom-openclaw-plugin'
|
||||||
|
const pattern = new RegExp(`(\\bid\\s*:\\s*)(["'])${wrongId.replace(/-/g, '\\-')}\\2`, 'g');
|
||||||
|
const replaced = content.replace(pattern, `$1$2${correctId}$2`);
|
||||||
|
if (replaced !== content) {
|
||||||
|
content = replaced;
|
||||||
|
patched = true;
|
||||||
|
echo` 🩹 Patching plugin ID in ${entry}: "${wrongId}" → "${correctId}"`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patched) {
|
||||||
|
fs.writeFileSync(entryPath, content, 'utf8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
echo`📦 Bundling OpenClaw plugin mirrors...`;
|
echo`📦 Bundling OpenClaw plugin mirrors...`;
|
||||||
fs.mkdirSync(OUTPUT_ROOT, { recursive: true });
|
fs.mkdirSync(OUTPUT_ROOT, { recursive: true });
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ describe('parseDoctorValidationOutput', () => {
|
|||||||
|
|
||||||
expect(out.undetermined).toBe(true);
|
expect(out.undetermined).toBe(true);
|
||||||
expect(out.errors).toEqual([]);
|
expect(out.errors).toEqual([]);
|
||||||
expect(out.warnings.some((w) => w.includes('falling back to local channel config checks'))).toBe(true);
|
expect(out.warnings.some((w: string) => w.includes('falling back to local channel config checks'))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls back with hint when output is empty', async () => {
|
it('falls back with hint when output is empty', async () => {
|
||||||
@@ -134,6 +134,27 @@ describe('parseDoctorValidationOutput', () => {
|
|||||||
|
|
||||||
expect(out.undetermined).toBe(true);
|
expect(out.undetermined).toBe(true);
|
||||||
expect(out.errors).toEqual([]);
|
expect(out.errors).toEqual([]);
|
||||||
expect(out.warnings.some((w) => w.includes('falling back to local channel config checks'))).toBe(true);
|
expect(out.warnings.some((w: string) => w.includes('falling back to local channel config checks'))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('WeCom plugin configuration', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
vi.resetModules();
|
||||||
|
await rm(testHome, { recursive: true, force: true });
|
||||||
|
await rm(testUserData, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets plugins.entries.wecom.enabled when saving wecom config', async () => {
|
||||||
|
const { saveChannelConfig } = await import('@electron/utils/channel-config');
|
||||||
|
|
||||||
|
await saveChannelConfig('wecom', { botId: 'test-bot', secret: 'test-secret' }, 'agent-a');
|
||||||
|
|
||||||
|
const config = await readOpenClawJson();
|
||||||
|
const plugins = config.plugins as { allow: string[], entries: Record<string, { enabled?: boolean }> };
|
||||||
|
|
||||||
|
expect(plugins.allow).toContain('wecom');
|
||||||
|
expect(plugins.entries['wecom'].enabled).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user