upgrade openclaw to 3.23 (#652)

Co-authored-by: Felix <24791380+vcfgv@users.noreply.github.com>
This commit is contained in:
paisley
2026-03-26 16:58:04 +08:00
committed by GitHub
Unverified
parent b786b773f1
commit ba5947e2cb
22 changed files with 2927 additions and 4739 deletions

View File

@@ -20,7 +20,7 @@
*/
const { cpSync, existsSync, readdirSync, rmSync, statSync, mkdirSync, realpathSync } = require('fs');
const { join, dirname, basename } = require('path');
const { join, dirname, basename, relative } = require('path');
// On Windows, paths in pnpm's virtual store can exceed the default MAX_PATH
// limit (260 chars). Node.js 18.17+ respects the system LongPathsEnabled
@@ -287,6 +287,84 @@ function patchBrokenModules(nodeModulesDir) {
}
}
// lru-cache CJS/ESM interop fix (recursive):
// Multiple versions of lru-cache may exist in the output tree — not just
// at node_modules/lru-cache/ but also nested inside other packages.
// Older CJS versions (v5, v6) export the class via `module.exports = LRUCache`
// without a named `LRUCache` property, so `import { LRUCache } from 'lru-cache'`
// fails in Node.js 22+ ESM interop (used by Electron 40+).
// We recursively scan the entire output for ALL lru-cache installations and
// patch each CJS entry to ensure `exports.LRUCache` always exists.
function patchAllLruCacheInstances(rootDir) {
let lruCount = 0;
const stack = [rootDir];
while (stack.length > 0) {
const dir = stack.pop();
let entries;
try { entries = readdirSync(normWin(dir), { withFileTypes: true }); } catch { continue; }
for (const entry of entries) {
const fullPath = join(dir, entry.name);
let isDirectory = entry.isDirectory();
if (!isDirectory) {
// pnpm layout may contain symlink/junction directories on Windows.
try { isDirectory = statSync(normWin(fullPath)).isDirectory(); } catch { isDirectory = false; }
}
if (!isDirectory) continue;
if (entry.name === 'lru-cache') {
const pkgPath = join(fullPath, 'package.json');
if (!existsSync(normWin(pkgPath))) { stack.push(fullPath); continue; }
try {
const pkg = JSON.parse(readFileSync(normWin(pkgPath), 'utf8'));
if (pkg.type === 'module') continue; // ESM version — already has named exports
const mainFile = pkg.main || 'index.js';
const entryFile = join(fullPath, mainFile);
if (!existsSync(normWin(entryFile))) continue;
const original = readFileSync(normWin(entryFile), 'utf8');
if (!original.includes('exports.LRUCache')) {
const patched = [
original,
'',
'// ClawX patch: add LRUCache named export for Node.js 22+ ESM interop',
'if (typeof module.exports === "function" && !module.exports.LRUCache) {',
' module.exports.LRUCache = module.exports;',
'}',
'',
].join('\n');
writeFileSync(normWin(entryFile), patched, 'utf8');
lruCount++;
console.log(`[after-pack] 🩹 Patched lru-cache CJS (v${pkg.version}) at ${relative(rootDir, fullPath)}`);
}
// lru-cache v7 ESM entry exports default only; add named export.
const moduleFile = typeof pkg.module === 'string' ? pkg.module : null;
if (moduleFile) {
const esmEntry = join(fullPath, moduleFile);
if (existsSync(normWin(esmEntry))) {
const esmOriginal = readFileSync(normWin(esmEntry), 'utf8');
if (
esmOriginal.includes('export default LRUCache') &&
!esmOriginal.includes('export { LRUCache')
) {
const esmPatched = [esmOriginal, '', 'export { LRUCache }', ''].join('\n');
writeFileSync(normWin(esmEntry), esmPatched, 'utf8');
lruCount++;
console.log(`[after-pack] 🩹 Patched lru-cache ESM (v${pkg.version}) at ${relative(rootDir, fullPath)}`);
}
}
}
} catch (err) {
console.warn(`[after-pack] ⚠️ Failed to patch lru-cache at ${fullPath}:`, err.message);
}
} else {
stack.push(fullPath);
}
}
}
return lruCount;
}
const lruPatched = patchAllLruCacheInstances(nodeModulesDir);
count += lruPatched;
if (count > 0) {
console.log(`[after-pack] 🩹 Patched ${count} broken module(s) in ${nodeModulesDir}`);
}
@@ -497,7 +575,7 @@ exports.default = async function afterPack(context) {
const BUNDLED_PLUGINS = [
{ npmName: '@soimy/dingtalk', pluginId: 'dingtalk' },
{ npmName: '@wecom/wecom-openclaw-plugin', pluginId: 'wecom' },
{ npmName: '@sliverp/qqbot', pluginId: 'qqbot' },
{ npmName: '@tencent-connect/openclaw-qqbot', pluginId: 'qqbot' },
{ npmName: '@larksuite/openclaw-lark', pluginId: 'feishu-openclaw-plugin' },
{ npmName: '@tencent-weixin/openclaw-weixin', pluginId: 'openclaw-weixin' },
];
@@ -535,4 +613,86 @@ exports.default = async function afterPack(context) {
if (nativeRemoved > 0) {
console.log(`[after-pack] ✅ Removed ${nativeRemoved} non-target native platform packages.`);
}
// 5. Patch lru-cache in app.asar.unpacked
//
// Production dependencies (electron-updater → semver → lru-cache@6,
// posthog-node → proxy agents → lru-cache@7, etc.) end up inside app.asar.
// Older CJS versions lack the `LRUCache` named export, breaking
// `import { LRUCache }` in Electron 40+ (Node.js 22+ ESM interop).
//
// electron-builder.yml lists `**/node_modules/lru-cache/**` in asarUnpack,
// which extracts those files to app.asar.unpacked/. We patch them here so
// Electron's transparent asar fs layer serves the fixed version at runtime.
const asarUnpackedDir = join(resourcesDir, 'app.asar.unpacked');
if (existsSync(asarUnpackedDir)) {
const { readFileSync: readFS, writeFileSync: writeFS } = require('fs');
let asarLruCount = 0;
const lruStack = [asarUnpackedDir];
while (lruStack.length > 0) {
const dir = lruStack.pop();
let entries;
try { entries = readdirSync(normWin(dir), { withFileTypes: true }); } catch { continue; }
for (const entry of entries) {
const fullPath = join(dir, entry.name);
let isDirectory = entry.isDirectory();
if (!isDirectory) {
// pnpm layout may contain symlink/junction directories on Windows.
try { isDirectory = statSync(normWin(fullPath)).isDirectory(); } catch { isDirectory = false; }
}
if (!isDirectory) continue;
if (entry.name === 'lru-cache') {
const pkgPath = join(fullPath, 'package.json');
if (!existsSync(normWin(pkgPath))) { lruStack.push(fullPath); continue; }
try {
const pkg = JSON.parse(readFS(normWin(pkgPath), 'utf8'));
if (pkg.type === 'module') continue; // ESM — already exports LRUCache
const mainFile = pkg.main || 'index.js';
const entryFile = join(fullPath, mainFile);
if (!existsSync(normWin(entryFile))) continue;
const original = readFS(normWin(entryFile), 'utf8');
if (!original.includes('exports.LRUCache')) {
const patched = [
original,
'',
'// ClawX patch: add LRUCache named export for Node.js 22+ ESM interop',
'if (typeof module.exports === "function" && !module.exports.LRUCache) {',
' module.exports.LRUCache = module.exports;',
'}',
'',
].join('\n');
writeFS(normWin(entryFile), patched, 'utf8');
asarLruCount++;
console.log(`[after-pack] 🩹 Patched lru-cache CJS (v${pkg.version}) in app.asar.unpacked at ${relative(asarUnpackedDir, fullPath)}`);
}
// lru-cache v7 ESM entry exports default only; add named export.
const moduleFile = typeof pkg.module === 'string' ? pkg.module : null;
if (moduleFile) {
const esmEntry = join(fullPath, moduleFile);
if (existsSync(normWin(esmEntry))) {
const esmOriginal = readFS(normWin(esmEntry), 'utf8');
if (
esmOriginal.includes('export default LRUCache') &&
!esmOriginal.includes('export { LRUCache')
) {
const esmPatched = [esmOriginal, '', 'export { LRUCache }', ''].join('\n');
writeFS(normWin(esmEntry), esmPatched, 'utf8');
asarLruCount++;
console.log(`[after-pack] 🩹 Patched lru-cache ESM (v${pkg.version}) in app.asar.unpacked at ${relative(asarUnpackedDir, fullPath)}`);
}
}
}
} catch (err) {
console.warn(`[after-pack] ⚠️ Failed to patch lru-cache in asar.unpacked at ${fullPath}:`, err.message);
}
} else {
lruStack.push(fullPath);
}
}
}
if (asarLruCount > 0) {
console.log(`[after-pack] 🩹 Patched ${asarLruCount} lru-cache instance(s) in app.asar.unpacked`);
}
}
};

View File

@@ -38,7 +38,7 @@ function normWin(p) {
const PLUGINS = [
{ npmName: '@soimy/dingtalk', pluginId: 'dingtalk' },
{ npmName: '@wecom/wecom-openclaw-plugin', pluginId: 'wecom' },
{ npmName: '@sliverp/qqbot', pluginId: 'qqbot' },
{ npmName: '@tencent-connect/openclaw-qqbot', pluginId: 'qqbot' },
{ npmName: '@larksuite/openclaw-lark', pluginId: 'feishu-openclaw-plugin' },
{ npmName: '@tencent-weixin/openclaw-weixin', pluginId: 'openclaw-weixin' },
];

View File

@@ -179,6 +179,61 @@ while (queue.length > 0) {
echo` Found ${collected.size} total packages (direct + transitive)`;
echo` Skipped ${skippedDevCount} dev-only package references`;
// 4b. Collect extra packages required by ClawX's Electron main process that are
// NOT deps of openclaw. These are resolved from openclaw's context at runtime
// (via createRequire from the openclaw directory) so they must live in the
// bundled openclaw/node_modules/.
//
// For each package we resolve it from the workspace's own node_modules,
// then BFS its transitive deps exactly like we did for openclaw above.
const EXTRA_BUNDLED_PACKAGES = [
'@whiskeysockets/baileys', // WhatsApp channel (was a dep of old clawdbot, not openclaw)
];
let extraCount = 0;
for (const pkgName of EXTRA_BUNDLED_PACKAGES) {
const pkgLink = path.join(NODE_MODULES, ...pkgName.split('/'));
if (!fs.existsSync(pkgLink)) {
echo` ⚠️ Extra package ${pkgName} not found in workspace node_modules, skipping.`;
continue;
}
let pkgReal;
try { pkgReal = fs.realpathSync(pkgLink); } catch { continue; }
if (!collected.has(pkgReal)) {
collected.set(pkgReal, pkgName);
extraCount++;
// BFS this package's own transitive deps
const depVirtualNM = getVirtualStoreNodeModules(pkgReal);
if (depVirtualNM) {
const extraQueue = [{ nodeModulesDir: depVirtualNM, skipPkg: pkgName }];
while (extraQueue.length > 0) {
const { nodeModulesDir, skipPkg } = extraQueue.shift();
const packages = listPackages(nodeModulesDir);
for (const { name, fullPath } of packages) {
if (name === skipPkg) continue;
if (SKIP_PACKAGES.has(name) || SKIP_SCOPES.some(s => name.startsWith(s))) continue;
let realPath;
try { realPath = fs.realpathSync(fullPath); } catch { continue; }
if (collected.has(realPath)) continue;
collected.set(realPath, name);
extraCount++;
const innerVirtualNM = getVirtualStoreNodeModules(realPath);
if (innerVirtualNM && innerVirtualNM !== nodeModulesDir) {
extraQueue.push({ nodeModulesDir: innerVirtualNM, skipPkg: name });
}
}
}
}
}
}
if (extraCount > 0) {
echo` Added ${extraCount} extra packages (+ transitive deps) for Electron main process`;
}
// 5. Copy all collected packages into OUTPUT/node_modules/ (flat structure)
//
// IMPORTANT: BFS guarantees direct deps are encountered before transitive deps.
@@ -457,6 +512,84 @@ function patchBrokenModules(nodeModulesDir) {
count++;
}
}
// lru-cache CJS/ESM interop fix (recursive):
// Multiple versions of lru-cache may exist in the output tree — not just
// at node_modules/lru-cache/ but also nested inside other packages.
// Older CJS versions (v5, v6) export the class via `module.exports = LRUCache`
// without a named `LRUCache` property, so `import { LRUCache } from 'lru-cache'`
// fails in Node.js 22+ ESM interop (used by Electron 40+).
// We recursively scan the entire output for ALL lru-cache installations and
// patch each CJS entry to ensure `exports.LRUCache` always exists.
function patchAllLruCacheInstances(rootDir) {
let lruCount = 0;
const stack = [rootDir];
while (stack.length > 0) {
const dir = stack.pop();
let entries;
try { entries = fs.readdirSync(normWin(dir), { withFileTypes: true }); } catch { continue; }
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
let isDirectory = entry.isDirectory();
if (!isDirectory) {
// pnpm layout may contain symlink/junction directories on Windows.
try { isDirectory = fs.statSync(normWin(fullPath)).isDirectory(); } catch { isDirectory = false; }
}
if (!isDirectory) continue;
if (entry.name === 'lru-cache') {
const pkgPath = path.join(fullPath, 'package.json');
if (!fs.existsSync(normWin(pkgPath))) { stack.push(fullPath); continue; }
try {
const pkg = JSON.parse(fs.readFileSync(normWin(pkgPath), 'utf8'));
if (pkg.type === 'module') continue; // ESM version — already has named exports
const mainFile = pkg.main || 'index.js';
const entryFile = path.join(fullPath, mainFile);
if (!fs.existsSync(normWin(entryFile))) continue;
const original = fs.readFileSync(normWin(entryFile), 'utf8');
if (!original.includes('exports.LRUCache')) {
const patched = [
original,
'',
'// ClawX patch: add LRUCache named export for Node.js 22+ ESM interop',
'if (typeof module.exports === "function" && !module.exports.LRUCache) {',
' module.exports.LRUCache = module.exports;',
'}',
'',
].join('\n');
fs.writeFileSync(normWin(entryFile), patched, 'utf8');
lruCount++;
echo` 🩹 Patched lru-cache CJS (v${pkg.version}) at ${path.relative(rootDir, fullPath)}`;
}
// lru-cache v7 ESM entry exports default only; add named export.
const moduleFile = typeof pkg.module === 'string' ? pkg.module : null;
if (moduleFile) {
const esmEntry = path.join(fullPath, moduleFile);
if (fs.existsSync(normWin(esmEntry))) {
const esmOriginal = fs.readFileSync(normWin(esmEntry), 'utf8');
if (
esmOriginal.includes('export default LRUCache') &&
!esmOriginal.includes('export { LRUCache')
) {
const esmPatched = [esmOriginal, '', 'export { LRUCache }', ''].join('\n');
fs.writeFileSync(normWin(esmEntry), esmPatched, 'utf8');
lruCount++;
echo` 🩹 Patched lru-cache ESM (v${pkg.version}) at ${path.relative(rootDir, fullPath)}`;
}
}
}
} catch (err) {
echo` ⚠️ Failed to patch lru-cache at ${fullPath}: ${err.message}`;
}
} else {
stack.push(fullPath);
}
}
}
return lruCount;
}
const lruPatched = patchAllLruCacheInstances(nodeModulesDir);
count += lruPatched;
if (count > 0) {
echo` 🩹 Patched ${count} broken module(s) in node_modules`;
}