From fb6163e024defa4dcdc4121809e360277e344663 Mon Sep 17 00:00:00 2001 From: paisley <8197966+su8su@users.noreply.github.com> Date: Sat, 14 Mar 2026 16:01:22 +0800 Subject: [PATCH] fix upgrade openclaw --- electron/gateway/config-sync.ts | 177 +++++++++++++++++++++++++++----- scripts/after-pack.cjs | 28 +++++ 2 files changed, 180 insertions(+), 25 deletions(-) diff --git a/electron/gateway/config-sync.ts b/electron/gateway/config-sync.ts index b4d47cc..8ff07f8 100644 --- a/electron/gateway/config-sync.ts +++ b/electron/gateway/config-sync.ts @@ -1,6 +1,6 @@ import { app } from 'electron'; import path from 'path'; -import { existsSync, readFileSync, cpSync, mkdirSync, rmSync } from 'fs'; +import { existsSync, readFileSync, cpSync, mkdirSync, rmSync, readdirSync, realpathSync } from 'fs'; import { homedir } from 'os'; import { join } from 'path'; import { getAllSettings } from '../utils/store'; @@ -30,11 +30,11 @@ export interface GatewayLaunchContext { // ── Auto-upgrade bundled plugins on startup ────────────────────── -const CHANNEL_PLUGIN_MAP: Record = { - dingtalk: 'dingtalk', - wecom: 'wecom', - feishu: 'feishu-openclaw-plugin', - qqbot: 'qqbot', +const CHANNEL_PLUGIN_MAP: Record = { + dingtalk: { dirName: 'dingtalk', npmName: '@soimy/dingtalk' }, + wecom: { dirName: 'wecom', npmName: '@wecom/wecom-openclaw-plugin' }, + feishu: { dirName: 'feishu-openclaw-plugin', npmName: '@larksuite/openclaw-lark' }, + qqbot: { dirName: 'qqbot', npmName: '@sliverp/qqbot' }, }; function readPluginVersion(pkgJsonPath: string): string | null { @@ -47,7 +47,113 @@ function readPluginVersion(pkgJsonPath: string): string | null { } } -function buildPluginCandidateSources(pluginDirName: string): string[] { +/** 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(); // 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(); + 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[] { return app.isPackaged ? [ join(process.resourcesPath, 'openclaw-plugins', pluginDirName), @@ -62,33 +168,54 @@ function buildPluginCandidateSources(pluginDirName: string): string[] { /** * 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. + * - Packaged mode: uses bundled plugins from resources/ (includes deps) + * - Dev mode: falls back to node_modules/ with pnpm-aware dep collection */ function ensureConfiguredPluginsUpgraded(configuredChannels: string[]): void { for (const channelType of configuredChannels) { - const pluginDirName = CHANNEL_PLUGIN_MAP[channelType]; - if (!pluginDirName) continue; + const pluginInfo = CHANNEL_PLUGIN_MAP[channelType]; + if (!pluginInfo) continue; + const { dirName, npmName } = pluginInfo; - const targetDir = join(homedir(), '.openclaw', 'extensions', pluginDirName); + const targetDir = join(homedir(), '.openclaw', 'extensions', dirName); 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); + // Try bundled sources first (packaged mode or if bundle-plugins was run) + const bundledSources = buildBundledPluginSources(dirName); + const bundledDir = bundledSources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json'))); + + if (bundledDir) { + const sourceVersion = readPluginVersion(join(bundledDir, 'package.json')); + if (sourceVersion && installedVersion && sourceVersion !== installedVersion) { + logger.info(`[plugin] Auto-upgrading ${channelType} plugin: ${installedVersion} → ${sourceVersion} (bundled)`); + try { + mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true }); + rmSync(targetDir, { recursive: true, force: true }); + cpSync(bundledDir, targetDir, { recursive: true, dereference: true }); + } catch (err) { + logger.warn(`[plugin] Failed to auto-upgrade ${channelType} plugin:`, err); + } + } + continue; + } + + // Dev mode fallback: copy from node_modules/ with pnpm dep resolution + if (!app.isPackaged) { + const npmPkgPath = join(process.cwd(), 'node_modules', ...npmName.split('/')); + if (!existsSync(join(npmPkgPath, 'openclaw.plugin.json'))) continue; + const sourceVersion = readPluginVersion(join(npmPkgPath, 'package.json')); + if (!sourceVersion || !installedVersion || sourceVersion === installedVersion) continue; + + logger.info(`[plugin] Auto-upgrading ${channelType} plugin: ${installedVersion} → ${sourceVersion} (dev/node_modules)`); + try { + mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true }); + copyPluginFromNodeModules(npmPkgPath, targetDir, npmName); + } catch (err) { + logger.warn(`[plugin] Failed to auto-upgrade ${channelType} plugin from node_modules:`, err); + } } } } diff --git a/scripts/after-pack.cjs b/scripts/after-pack.cjs index 9286114..2eb91d1 100644 --- a/scripts/after-pack.cjs +++ b/scripts/after-pack.cjs @@ -171,6 +171,34 @@ function patchBrokenModules(nodeModulesDir) { count++; } } + + // https-proxy-agent@8.x only defines exports.import (ESM) with no CJS + // fallback. The openclaw Gateway loads it via require(), which triggers + // ERR_PACKAGE_PATH_NOT_EXPORTED. Patch exports to add CJS conditions. + const hpaPkgPath = join(nodeModulesDir, 'https-proxy-agent', 'package.json'); + if (existsSync(hpaPkgPath)) { + try { + const raw = readFileSync(hpaPkgPath, 'utf8'); + const pkg = JSON.parse(raw); + const exp = pkg.exports; + // Only patch if exports exists and lacks a CJS 'require' condition + if (exp && exp.import && !exp.require && !exp['.']) { + pkg.exports = { + '.': { + import: exp.import, + require: exp.import, // ESM dist works for CJS too via Node.js interop + default: typeof exp.import === 'string' ? exp.import : exp.import.default, + }, + }; + writeFileSync(hpaPkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8'); + count++; + console.log('[after-pack] 🩹 Patched https-proxy-agent exports for CJS compatibility'); + } + } catch (err) { + console.warn('[after-pack] ⚠️ Failed to patch https-proxy-agent:', err.message); + } + } + if (count > 0) { console.log(`[after-pack] 🩹 Patched ${count} broken module(s) in ${nodeModulesDir}`); }