feat: enhance after-pack script to copy OpenClaw runtime dependencies

- Added a new script `bundle-openclaw.mjs` to bundle OpenClaw runtime dependencies.
- Updated `after-pack.cjs` to copy bundled OpenClaw runtime and its node_modules.
- Improved cleanup of unnecessary development files in node_modules.
- Adjusted paths for resources in the packaging process.

style: update loading indicator styles in ChatHistoryPanel

- Changed the border radius and padding for the loading indicator in ChatHistoryPanel.

fix: improve ProvidersSection to handle provider account syncing

- Added logic to sync model configuration to provider accounts.
- Introduced error handling and loading states during the sync process.
- Enhanced vendor resolution and account management logic.

fix: fallback session handling in chat store

- Implemented fallback session logic in loadSessions to ensure a valid session is always available on error.
This commit is contained in:
duanshuwen
2026-04-22 21:56:37 +08:00
parent 2f675afe47
commit ea1fd18e6f
22 changed files with 8947 additions and 94 deletions

View File

@@ -7,7 +7,8 @@
* It handles:
* 1. Copying and bundling electron/scripts/ directory
* 2. Ensuring required dependencies (playwright, chromium-bidi, bytenode) are included
* 3. Cleaning up unnecessary development files to reduce package size
* 3. Copying bundled OpenClaw runtime dependencies that electron-builder may skip
* 4. Cleaning up unnecessary development files to reduce package size
*/
const fs = require('fs-extra');
@@ -75,15 +76,65 @@ function cleanupNativePlatformPackages(nodeModulesDir, platform, arch) {
return 0;
}
function resolveResourcesDir(context) {
if (context.electronPlatformName === 'darwin') {
if (context.appOutDir.endsWith('.app')) {
return path.join(context.appOutDir, 'Contents', 'Resources');
}
const appName = context.packager?.appInfo?.productFilename || 'NIANXX';
return path.join(context.appOutDir, `${appName}.app`, 'Contents', 'Resources');
}
return path.join(context.appOutDir, 'resources');
}
async function copyBundledOpenClaw(context) {
const srcRoot = path.join(__dirname, '..', 'build', 'openclaw');
if (!(await fs.pathExists(srcRoot))) {
console.warn('[after-pack] build/openclaw not found. Run bundle-openclaw first.');
return;
}
const resourcesDir = resolveResourcesDir(context);
const destRoot = path.join(resourcesDir, 'openclaw');
const srcNodeModules = path.join(srcRoot, 'node_modules');
const destNodeModules = path.join(destRoot, 'node_modules');
await fs.ensureDir(resourcesDir);
if (!(await fs.pathExists(destRoot))) {
await fs.copy(srcRoot, destRoot, {
filter: (src) => {
const relative = path.relative(srcRoot, src);
if (!relative) return true;
return relative.split(path.sep)[0] !== 'node_modules';
},
});
console.log('[after-pack] Copied bundled OpenClaw runtime root.');
}
if (!(await fs.pathExists(srcNodeModules))) {
console.warn('[after-pack] build/openclaw/node_modules not found. OpenClaw runtime will be incomplete.');
return;
}
if (!(await fs.pathExists(destNodeModules))) {
await fs.copy(srcNodeModules, destNodeModules);
console.log('[after-pack] Copied bundled OpenClaw node_modules.');
}
}
module.exports = async function afterPack(context) {
const { appOutDir, electronPlatformName: platform, arch } = context;
const resourcesDir = resolveResourcesDir(context);
console.log(`Running afterPack hook for ${platform}-${arch}`);
console.log(`App output directory: ${appOutDir}`);
// 1. Handle electron/scripts/ directory
const scriptsSrc = path.join(__dirname, '..', 'electron/scripts');
const scriptsDest = path.join(appOutDir, 'resources', 'scripts');
const scriptsDest = path.join(resourcesDir, 'scripts');
if (await fs.pathExists(scriptsSrc)) {
await fs.ensureDir(scriptsDest);
@@ -118,13 +169,15 @@ module.exports = async function afterPack(context) {
}
}
}
await copyBundledOpenClaw(context);
// 2. Ensure required dependencies are included
// electron-builder may exclude some dependencies due to .gitignore rules
const ensureDependency = async (depName) => {
const src = path.join(__dirname, '..', 'node_modules', depName);
const dest = path.join(appOutDir, 'node_modules', depName);
const dest = path.join(resourcesDir, 'node_modules', depName);
if (await fs.pathExists(src)) {
await fs.ensureDir(path.dirname(dest));
@@ -144,7 +197,7 @@ module.exports = async function afterPack(context) {
// 3. Clean up unnecessary development files from node_modules (skip if SKIP_AFTERPACK_CLEANUP is set)
if (!process.env.SKIP_AFTERPACK_CLEANUP) {
const nodeModulesDest = path.join(appOutDir, 'node_modules');
const nodeModulesDest = path.join(resourcesDir, 'node_modules');
if (await fs.pathExists(nodeModulesDest)) {
console.log('Cleaning up development files in node_modules...');
const removed = cleanupUnnecessaryFiles(nodeModulesDest);
@@ -165,4 +218,4 @@ module.exports = async function afterPack(context) {
// cleanupNativePlatformPackages(nodeModulesDest, platform, arch);
console.log('afterPack hook completed successfully');
};
};

304
scripts/bundle-openclaw.mjs Normal file
View File

@@ -0,0 +1,304 @@
#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const OUTPUT = path.join(ROOT, 'build', 'openclaw');
const NODE_MODULES = path.join(ROOT, 'node_modules');
function log(message) {
console.log(`[bundle-openclaw] ${message}`);
}
function normWin(filePath) {
if (process.platform !== 'win32') return filePath;
if (filePath.startsWith('\\\\?\\')) return filePath;
return `\\\\?\\${filePath.replace(/\//g, '\\')}`;
}
function getVirtualStoreNodeModules(realPkgPath) {
let current = realPkgPath;
while (current !== path.dirname(current)) {
if (path.basename(current) === 'node_modules') {
return current;
}
current = path.dirname(current);
}
return null;
}
function listPackages(nodeModulesDir) {
const results = [];
const targetDir = normWin(nodeModulesDir);
if (!fs.existsSync(targetDir)) return results;
for (const entry of fs.readdirSync(targetDir)) {
if (entry === '.bin') continue;
const entryPath = path.join(nodeModulesDir, entry);
if (entry.startsWith('@')) {
let scopedEntries = [];
try {
scopedEntries = fs.readdirSync(normWin(entryPath));
} catch {
continue;
}
for (const scoped of scopedEntries) {
results.push({
name: `${entry}/${scoped}`,
fullPath: path.join(entryPath, scoped),
});
}
continue;
}
results.push({ name: entry, fullPath: entryPath });
}
return results;
}
function rmSafe(target) {
try {
const stat = fs.lstatSync(target);
if (stat.isDirectory()) {
fs.rmSync(target, { recursive: true, force: true });
} else {
fs.rmSync(target, { force: true });
}
return true;
} catch {
return false;
}
}
function cleanupNodeModules(dir) {
let removedCount = 0;
const REMOVE_DIRS = new Set([
'test', 'tests', '__tests__', '.github', 'docs', 'examples', 'example',
]);
const REMOVE_FILE_EXTS = ['.d.ts', '.d.ts.map', '.js.map', '.mjs.map', '.ts.map', '.markdown'];
const REMOVE_FILE_NAMES = new Set([
'.DS_Store', 'README.md', 'CHANGELOG.md', 'LICENSE.md', 'CONTRIBUTING.md',
'tsconfig.json', '.npmignore', '.eslintrc', '.prettierrc', '.editorconfig',
]);
function walk(currentDir) {
let entries = [];
try {
entries = fs.readdirSync(currentDir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
if (REMOVE_DIRS.has(entry.name)) {
if (rmSafe(fullPath)) removedCount++;
continue;
}
walk(fullPath);
continue;
}
if (
REMOVE_FILE_NAMES.has(entry.name)
|| REMOVE_FILE_EXTS.some((ext) => entry.name.endsWith(ext))
) {
if (rmSafe(fullPath)) removedCount++;
}
}
}
if (fs.existsSync(dir)) {
walk(dir);
}
return removedCount;
}
function cleanupBundle(outputDir) {
let removedCount = 0;
for (const name of ['CHANGELOG.md', 'README.md']) {
if (rmSafe(path.join(outputDir, name))) removedCount++;
}
removedCount += cleanupNodeModules(path.join(outputDir, 'node_modules'));
const extensionsDir = path.join(outputDir, 'dist', 'extensions');
if (fs.existsSync(extensionsDir)) {
for (const entry of fs.readdirSync(extensionsDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
removedCount += cleanupNodeModules(path.join(extensionsDir, entry.name, 'node_modules'));
}
}
return removedCount;
}
const openclawLink = path.join(NODE_MODULES, 'openclaw');
if (!fs.existsSync(openclawLink)) {
console.error('[bundle-openclaw] node_modules/openclaw not found. Run pnpm install first.');
process.exit(1);
}
const openclawReal = fs.realpathSync(openclawLink);
log(`resolved openclaw package: ${openclawReal}`);
if (fs.existsSync(OUTPUT)) {
fs.rmSync(OUTPUT, { recursive: true, force: true });
}
fs.mkdirSync(OUTPUT, { recursive: true });
log('copying openclaw package root...');
fs.cpSync(normWin(openclawReal), normWin(OUTPUT), { recursive: true, dereference: true });
const openclawVirtualNodeModules = getVirtualStoreNodeModules(openclawReal);
if (!openclawVirtualNodeModules) {
console.error('[bundle-openclaw] could not determine pnpm virtual store for openclaw.');
process.exit(1);
}
const collected = new Map();
const queue = [{ nodeModulesDir: openclawVirtualNodeModules, skipPkg: 'openclaw' }];
const SKIP_PACKAGES = new Set([
'typescript',
'@playwright/test',
'@discordjs/opus',
]);
const SKIP_SCOPES = ['@cloudflare/', '@types/'];
while (queue.length > 0) {
const current = queue.shift();
if (!current) break;
for (const { name, fullPath } of listPackages(current.nodeModulesDir)) {
if (name === current.skipPkg) continue;
if (SKIP_PACKAGES.has(name) || SKIP_SCOPES.some((scope) => name.startsWith(scope))) {
continue;
}
let realPath;
try {
realPath = fs.realpathSync(fullPath);
} catch {
continue;
}
if (collected.has(realPath)) continue;
collected.set(realPath, name);
const depVirtualNodeModules = getVirtualStoreNodeModules(realPath);
if (depVirtualNodeModules && depVirtualNodeModules !== current.nodeModulesDir) {
queue.push({ nodeModulesDir: depVirtualNodeModules, skipPkg: name });
}
}
}
log(`resolved ${collected.size} transitive packages.`);
const outputNodeModules = path.join(OUTPUT, 'node_modules');
fs.mkdirSync(outputNodeModules, { recursive: true });
const copiedNames = new Set();
let copiedCount = 0;
let skippedDuplicates = 0;
for (const [realPath, pkgName] of collected) {
if (copiedNames.has(pkgName)) {
skippedDuplicates++;
continue;
}
copiedNames.add(pkgName);
const dest = path.join(outputNodeModules, pkgName);
try {
fs.mkdirSync(normWin(path.dirname(dest)), { recursive: true });
fs.cpSync(normWin(realPath), normWin(dest), { recursive: true, dereference: true });
copiedCount++;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
log(`skipped ${pkgName}: ${message}`);
}
}
log(`copied ${copiedCount} packages to bundled node_modules (${skippedDuplicates} duplicates skipped).`);
const extensionsDir = path.join(OUTPUT, 'dist', 'extensions');
let mergedExtensionPackages = 0;
if (fs.existsSync(extensionsDir)) {
for (const extEntry of fs.readdirSync(extensionsDir, { withFileTypes: true })) {
if (!extEntry.isDirectory()) continue;
const extNodeModules = path.join(extensionsDir, extEntry.name, 'node_modules');
if (!fs.existsSync(extNodeModules)) continue;
for (const pkgEntry of fs.readdirSync(extNodeModules, { withFileTypes: true })) {
if (!pkgEntry.isDirectory() || pkgEntry.name === '.bin') continue;
const srcPkg = path.join(extNodeModules, pkgEntry.name);
if (pkgEntry.name.startsWith('@')) {
let scopeEntries = [];
try {
scopeEntries = fs.readdirSync(srcPkg, { withFileTypes: true });
} catch {
continue;
}
for (const scopeEntry of scopeEntries) {
if (!scopeEntry.isDirectory()) continue;
const scopedName = `${pkgEntry.name}/${scopeEntry.name}`;
if (copiedNames.has(scopedName)) continue;
const srcScoped = path.join(srcPkg, scopeEntry.name);
const destScoped = path.join(outputNodeModules, pkgEntry.name, scopeEntry.name);
try {
fs.mkdirSync(normWin(path.dirname(destScoped)), { recursive: true });
fs.cpSync(normWin(srcScoped), normWin(destScoped), { recursive: true, dereference: true });
copiedNames.add(scopedName);
mergedExtensionPackages++;
} catch {
// Ignore extension copy failures and continue building the rest.
}
}
continue;
}
if (copiedNames.has(pkgEntry.name)) continue;
const destPkg = path.join(outputNodeModules, pkgEntry.name);
try {
fs.cpSync(normWin(srcPkg), normWin(destPkg), { recursive: true, dereference: true });
copiedNames.add(pkgEntry.name);
mergedExtensionPackages++;
} catch {
// Ignore extension copy failures and continue building the rest.
}
}
}
}
if (mergedExtensionPackages > 0) {
log(`merged ${mergedExtensionPackages} built-in extension packages into top-level node_modules.`);
}
const removedCount = cleanupBundle(OUTPUT);
log(`removed ${removedCount} development-only files from bundled runtime.`);
log(`OpenClaw bundle ready at ${OUTPUT}`);