Skip to content

Commit fb6163e

Browse files
committed
fix upgrade openclaw
1 parent 67ffe09 commit fb6163e

File tree

2 files changed

+180
-25
lines changed

2 files changed

+180
-25
lines changed

electron/gateway/config-sync.ts

Lines changed: 152 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { app } from 'electron';
22
import path from 'path';
3-
import { existsSync, readFileSync, cpSync, mkdirSync, rmSync } from 'fs';
3+
import { existsSync, readFileSync, cpSync, mkdirSync, rmSync, readdirSync, realpathSync } from 'fs';
44
import { homedir } from 'os';
55
import { join } from 'path';
66
import { getAllSettings } from '../utils/store';
@@ -30,11 +30,11 @@ export interface GatewayLaunchContext {
3030

3131
// ── Auto-upgrade bundled plugins on startup ──────────────────────
3232

33-
const CHANNEL_PLUGIN_MAP: Record<string, string> = {
34-
dingtalk: 'dingtalk',
35-
wecom: 'wecom',
36-
feishu: 'feishu-openclaw-plugin',
37-
qqbot: 'qqbot',
33+
const CHANNEL_PLUGIN_MAP: Record<string, { dirName: string; npmName: string }> = {
34+
dingtalk: { dirName: 'dingtalk', npmName: '@soimy/dingtalk' },
35+
wecom: { dirName: 'wecom', npmName: '@wecom/wecom-openclaw-plugin' },
36+
feishu: { dirName: 'feishu-openclaw-plugin', npmName: '@larksuite/openclaw-lark' },
37+
qqbot: { dirName: 'qqbot', npmName: '@sliverp/qqbot' },
3838
};
3939

4040
function readPluginVersion(pkgJsonPath: string): string | null {
@@ -47,7 +47,113 @@ function readPluginVersion(pkgJsonPath: string): string | null {
4747
}
4848
}
4949

50-
function buildPluginCandidateSources(pluginDirName: string): string[] {
50+
/** Walk up from a path until we find a parent named node_modules. */
51+
function findParentNodeModules(startPath: string): string | null {
52+
let dir = startPath;
53+
while (dir !== path.dirname(dir)) {
54+
if (path.basename(dir) === 'node_modules') return dir;
55+
dir = path.dirname(dir);
56+
}
57+
return null;
58+
}
59+
60+
/** List packages inside a node_modules dir (handles @scoped packages). */
61+
function listPackagesInDir(nodeModulesDir: string): Array<{ name: string; fullPath: string }> {
62+
const result: Array<{ name: string; fullPath: string }> = [];
63+
if (!existsSync(nodeModulesDir)) return result;
64+
const SKIP = new Set(['.bin', '.package-lock.json', '.modules.yaml', '.pnpm']);
65+
for (const entry of readdirSync(nodeModulesDir, { withFileTypes: true })) {
66+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
67+
if (SKIP.has(entry.name)) continue;
68+
const entryPath = join(nodeModulesDir, entry.name);
69+
if (entry.name.startsWith('@')) {
70+
try {
71+
for (const sub of readdirSync(entryPath)) {
72+
result.push({ name: `${entry.name}/${sub}`, fullPath: join(entryPath, sub) });
73+
}
74+
} catch { /* ignore */ }
75+
} else {
76+
result.push({ name: entry.name, fullPath: entryPath });
77+
}
78+
}
79+
return result;
80+
}
81+
82+
/**
83+
* Copy a plugin from a pnpm node_modules location, including its
84+
* transitive runtime dependencies (replicates bundle-openclaw-plugins.mjs
85+
* logic).
86+
*/
87+
function copyPluginFromNodeModules(npmPkgPath: string, targetDir: string, npmName: string): void {
88+
let realPath: string;
89+
try {
90+
realPath = realpathSync(npmPkgPath);
91+
} catch {
92+
throw new Error(`Cannot resolve real path for ${npmPkgPath}`);
93+
}
94+
95+
// 1. Copy plugin package itself
96+
rmSync(targetDir, { recursive: true, force: true });
97+
mkdirSync(targetDir, { recursive: true });
98+
cpSync(realPath, targetDir, { recursive: true, dereference: true });
99+
100+
// 2. Collect transitive deps from pnpm virtual store
101+
const rootVirtualNM = findParentNodeModules(realPath);
102+
if (!rootVirtualNM) {
103+
logger.warn(`[plugin] Cannot find virtual store node_modules for ${npmName}, plugin may lack deps`);
104+
return;
105+
}
106+
107+
// Read peer deps to skip (they're provided by the host gateway)
108+
const SKIP_PACKAGES = new Set(['typescript', '@playwright/test']);
109+
try {
110+
const pluginPkg = JSON.parse(readFileSync(join(targetDir, 'package.json'), 'utf-8'));
111+
for (const peer of Object.keys(pluginPkg.peerDependencies || {})) {
112+
SKIP_PACKAGES.add(peer);
113+
}
114+
} catch { /* ignore */ }
115+
116+
const collected = new Map<string, string>(); // realPath → packageName
117+
const queue: Array<{ nodeModulesDir: string; skipPkg: string }> = [
118+
{ nodeModulesDir: rootVirtualNM, skipPkg: npmName },
119+
];
120+
121+
while (queue.length > 0) {
122+
const { nodeModulesDir, skipPkg } = queue.shift()!;
123+
for (const { name, fullPath } of listPackagesInDir(nodeModulesDir)) {
124+
if (name === skipPkg) continue;
125+
if (SKIP_PACKAGES.has(name) || name.startsWith('@types/')) continue;
126+
let depRealPath: string;
127+
try {
128+
depRealPath = realpathSync(fullPath);
129+
} catch { continue; }
130+
if (collected.has(depRealPath)) continue;
131+
collected.set(depRealPath, name);
132+
const depVirtualNM = findParentNodeModules(depRealPath);
133+
if (depVirtualNM && depVirtualNM !== nodeModulesDir) {
134+
queue.push({ nodeModulesDir: depVirtualNM, skipPkg: name });
135+
}
136+
}
137+
}
138+
139+
// 3. Copy flattened deps into targetDir/node_modules/
140+
const outputNM = join(targetDir, 'node_modules');
141+
mkdirSync(outputNM, { recursive: true });
142+
const copiedNames = new Set<string>();
143+
for (const [depRealPath, pkgName] of collected) {
144+
if (copiedNames.has(pkgName)) continue;
145+
copiedNames.add(pkgName);
146+
const dest = join(outputNM, pkgName);
147+
try {
148+
mkdirSync(path.dirname(dest), { recursive: true });
149+
cpSync(depRealPath, dest, { recursive: true, dereference: true });
150+
} catch { /* skip individual dep failures */ }
151+
}
152+
153+
logger.info(`[plugin] Copied ${copiedNames.size} deps for ${npmName}`);
154+
}
155+
156+
function buildBundledPluginSources(pluginDirName: string): string[] {
51157
return app.isPackaged
52158
? [
53159
join(process.resourcesPath, 'openclaw-plugins', pluginDirName),
@@ -62,33 +168,54 @@ function buildPluginCandidateSources(pluginDirName: string): string[] {
62168

63169
/**
64170
* Auto-upgrade all configured channel plugins before Gateway start.
65-
* Compares the installed version in ~/.openclaw/extensions/ with the
66-
* bundled version and overwrites if the bundled version is newer.
171+
* - Packaged mode: uses bundled plugins from resources/ (includes deps)
172+
* - Dev mode: falls back to node_modules/ with pnpm-aware dep collection
67173
*/
68174
function ensureConfiguredPluginsUpgraded(configuredChannels: string[]): void {
69175
for (const channelType of configuredChannels) {
70-
const pluginDirName = CHANNEL_PLUGIN_MAP[channelType];
71-
if (!pluginDirName) continue;
176+
const pluginInfo = CHANNEL_PLUGIN_MAP[channelType];
177+
if (!pluginInfo) continue;
178+
const { dirName, npmName } = pluginInfo;
72179

73-
const targetDir = join(homedir(), '.openclaw', 'extensions', pluginDirName);
180+
const targetDir = join(homedir(), '.openclaw', 'extensions', dirName);
74181
const targetManifest = join(targetDir, 'openclaw.plugin.json');
75182
if (!existsSync(targetManifest)) continue; // not installed, nothing to upgrade
76183

77-
const sources = buildPluginCandidateSources(pluginDirName);
78-
const sourceDir = sources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json')));
79-
if (!sourceDir) continue; // no bundled source available
80-
81184
const installedVersion = readPluginVersion(join(targetDir, 'package.json'));
82-
const sourceVersion = readPluginVersion(join(sourceDir, 'package.json'));
83-
if (!sourceVersion || !installedVersion || sourceVersion === installedVersion) continue;
84185

85-
logger.info(`[plugin] Auto-upgrading ${channelType} plugin: ${installedVersion}${sourceVersion}`);
86-
try {
87-
mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true });
88-
rmSync(targetDir, { recursive: true, force: true });
89-
cpSync(sourceDir, targetDir, { recursive: true, dereference: true });
90-
} catch (err) {
91-
logger.warn(`[plugin] Failed to auto-upgrade ${channelType} plugin:`, err);
186+
// Try bundled sources first (packaged mode or if bundle-plugins was run)
187+
const bundledSources = buildBundledPluginSources(dirName);
188+
const bundledDir = bundledSources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json')));
189+
190+
if (bundledDir) {
191+
const sourceVersion = readPluginVersion(join(bundledDir, 'package.json'));
192+
if (sourceVersion && installedVersion && sourceVersion !== installedVersion) {
193+
logger.info(`[plugin] Auto-upgrading ${channelType} plugin: ${installedVersion}${sourceVersion} (bundled)`);
194+
try {
195+
mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true });
196+
rmSync(targetDir, { recursive: true, force: true });
197+
cpSync(bundledDir, targetDir, { recursive: true, dereference: true });
198+
} catch (err) {
199+
logger.warn(`[plugin] Failed to auto-upgrade ${channelType} plugin:`, err);
200+
}
201+
}
202+
continue;
203+
}
204+
205+
// Dev mode fallback: copy from node_modules/ with pnpm dep resolution
206+
if (!app.isPackaged) {
207+
const npmPkgPath = join(process.cwd(), 'node_modules', ...npmName.split('/'));
208+
if (!existsSync(join(npmPkgPath, 'openclaw.plugin.json'))) continue;
209+
const sourceVersion = readPluginVersion(join(npmPkgPath, 'package.json'));
210+
if (!sourceVersion || !installedVersion || sourceVersion === installedVersion) continue;
211+
212+
logger.info(`[plugin] Auto-upgrading ${channelType} plugin: ${installedVersion}${sourceVersion} (dev/node_modules)`);
213+
try {
214+
mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true });
215+
copyPluginFromNodeModules(npmPkgPath, targetDir, npmName);
216+
} catch (err) {
217+
logger.warn(`[plugin] Failed to auto-upgrade ${channelType} plugin from node_modules:`, err);
218+
}
92219
}
93220
}
94221
}

scripts/after-pack.cjs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,34 @@ function patchBrokenModules(nodeModulesDir) {
171171
count++;
172172
}
173173
}
174+
175+
// https-proxy-agent@8.x only defines exports.import (ESM) with no CJS
176+
// fallback. The openclaw Gateway loads it via require(), which triggers
177+
// ERR_PACKAGE_PATH_NOT_EXPORTED. Patch exports to add CJS conditions.
178+
const hpaPkgPath = join(nodeModulesDir, 'https-proxy-agent', 'package.json');
179+
if (existsSync(hpaPkgPath)) {
180+
try {
181+
const raw = readFileSync(hpaPkgPath, 'utf8');
182+
const pkg = JSON.parse(raw);
183+
const exp = pkg.exports;
184+
// Only patch if exports exists and lacks a CJS 'require' condition
185+
if (exp && exp.import && !exp.require && !exp['.']) {
186+
pkg.exports = {
187+
'.': {
188+
import: exp.import,
189+
require: exp.import, // ESM dist works for CJS too via Node.js interop
190+
default: typeof exp.import === 'string' ? exp.import : exp.import.default,
191+
},
192+
};
193+
writeFileSync(hpaPkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
194+
count++;
195+
console.log('[after-pack] 🩹 Patched https-proxy-agent exports for CJS compatibility');
196+
}
197+
} catch (err) {
198+
console.warn('[after-pack] ⚠️ Failed to patch https-proxy-agent:', err.message);
199+
}
200+
}
201+
174202
if (count > 0) {
175203
console.log(`[after-pack] 🩹 Patched ${count} broken module(s) in ${nodeModulesDir}`);
176204
}

0 commit comments

Comments
 (0)