Fix package issue (#952)

This commit is contained in:
paisley
2026-05-01 12:13:17 +08:00
committed by GitHub
parent 62d83b401f
commit cc7992e160
8 changed files with 308 additions and 49 deletions

View File

@@ -0,0 +1,59 @@
import { createRequire } from 'node:module';
import { join } from 'node:path';
import { getOpenClawDir, getOpenClawResolvedDir } from './paths';
export type RuntimeModuleResolver = {
label: string;
resolve(specifier: string): string;
};
export function resolveModulePathWithFallbacks(
specifier: string,
resolvers: RuntimeModuleResolver[],
): string {
const errors: string[] = [];
for (const resolver of resolvers) {
try {
return resolver.resolve(specifier);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
errors.push(`${resolver.label}: ${message}`);
}
}
throw new Error(
`Failed to resolve "${specifier}" from any runtime context. ${errors.join(' | ')}`,
);
}
function getRuntimeModuleResolvers(): RuntimeModuleResolver[] {
const candidates: Array<{ label: string; base: string | URL }> = [
{ label: 'openclaw-resolved', base: join(getOpenClawResolvedDir(), 'package.json') },
{ label: 'openclaw', base: join(getOpenClawDir(), 'package.json') },
{ label: 'app', base: import.meta.url },
];
const seen = new Set<string>();
const resolvers: RuntimeModuleResolver[] = [];
for (const candidate of candidates) {
const key = typeof candidate.base === 'string' ? candidate.base : candidate.base.toString();
if (seen.has(key)) {
continue;
}
seen.add(key);
const runtimeRequire = createRequire(candidate.base);
resolvers.push({
label: candidate.label,
resolve: runtimeRequire.resolve.bind(runtimeRequire),
});
}
return resolvers;
}
export function resolveOpenClawRuntimeModulePath(specifier: string): string {
return resolveModulePathWithFallbacks(specifier, getRuntimeModuleResolvers());
}

View File

@@ -6,7 +6,7 @@ import { homedir } from 'node:os';
import { join } from 'node:path';
import { deflateSync } from 'node:zlib';
import { normalizeOpenClawAccountId } from './channel-alias';
import { getOpenClawResolvedDir } from './paths';
import { resolveOpenClawRuntimeModulePath } from './runtime-package-resolution';
export const DEFAULT_WECHAT_BASE_URL = 'https://ilinkai.weixin.qq.com';
const DEFAULT_ILINK_BOT_TYPE = '3';
@@ -43,9 +43,10 @@ function getQrRenderDeps(): QrRenderDeps {
return qrRenderDeps;
}
const openclawRequire = createRequire(join(getOpenClawResolvedDir(), 'package.json'));
const qrCodeModulePath = openclawRequire.resolve('qrcode-terminal/vendor/QRCode/index.js');
const qrErrorCorrectLevelPath = openclawRequire.resolve('qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js');
const qrCodeModulePath = resolveOpenClawRuntimeModulePath('qrcode-terminal/vendor/QRCode/index.js');
const qrErrorCorrectLevelPath = resolveOpenClawRuntimeModulePath(
'qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js',
);
qrRenderDeps = {
QRCode: require(qrCodeModulePath),
QRErrorCorrectLevel: require(qrErrorCorrectLevelPath),

View File

@@ -5,6 +5,7 @@ import { EventEmitter } from 'events';
import { existsSync, mkdirSync, rmSync, readdirSync } from 'fs';
import { deflateSync } from 'zlib';
import { getOpenClawDir, getOpenClawResolvedDir } from './paths';
import { resolveOpenClawRuntimeModulePath } from './runtime-package-resolution';
const require = createRequire(import.meta.url);
@@ -40,8 +41,6 @@ function resolveOpenClawPackageJson(packageName: string): string {
}
const baileysPath = dirname(resolveOpenClawPackageJson('@whiskeysockets/baileys'));
const qrCodeModulePath = openclawRequire.resolve('qrcode-terminal/vendor/QRCode/index.js');
const qrErrorCorrectLevelPath = openclawRequire.resolve('qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js');
// Load Baileys dependencies dynamically
const {
@@ -51,10 +50,6 @@ const {
fetchLatestBaileysVersion
} = require(baileysPath);
// Load QRCode dependencies dynamically
const QRCodeModule = require(qrCodeModulePath);
const QRErrorCorrectLevelModule = require(qrErrorCorrectLevelPath);
// Types from Baileys (approximate since we don't have types for dynamic require)
interface BaileysError extends Error {
output?: { statusCode?: number };
@@ -68,12 +63,43 @@ type ConnectionState = {
qr?: string;
};
type QrCodeMatrix = {
addData(input: string): void;
make(): void;
getModuleCount(): number;
isDark(row: number, col: number): boolean;
};
type QrCodeConstructor = new (typeNumber: number, errorCorrectionLevel: unknown) => QrCodeMatrix;
type QrErrorCorrectLevelModule = {
L: unknown;
};
type QrRenderDeps = {
QRCode: QrCodeConstructor;
QRErrorCorrectLevel: QrErrorCorrectLevelModule;
};
let qrRenderDeps: QrRenderDeps | null = null;
function getQrRenderDeps(): QrRenderDeps {
if (qrRenderDeps) {
return qrRenderDeps;
}
const qrCodeModulePath = resolveOpenClawRuntimeModulePath('qrcode-terminal/vendor/QRCode/index.js');
const qrErrorCorrectLevelPath = resolveOpenClawRuntimeModulePath(
'qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js',
);
qrRenderDeps = {
QRCode: require(qrCodeModulePath),
QRErrorCorrectLevel: require(qrErrorCorrectLevelPath),
};
return qrRenderDeps;
}
// --- QR Generation Logic (Adapted from OpenClaw) ---
const QRCode = QRCodeModule;
const QRErrorCorrectLevel = QRErrorCorrectLevelModule;
function createQrMatrix(input: string) {
const { QRCode, QRErrorCorrectLevel } = getQrRenderDeps();
const qr = new QRCode(-1, QRErrorCorrectLevel.L);
qr.addData(input);
qr.make();

View File

@@ -615,11 +615,12 @@ exports.default = async function afterPack(context) {
const srcNM = join(buildExtDir, extEntry.name, 'node_modules');
if (!existsSync(srcNM)) continue;
// Copy to extension's own node_modules (for direct requires from extension code)
// Copy to extension's own node_modules (for direct requires from extension code).
// electron-builder may leave behind an empty destination directory when it
// skips node_modules content via .gitignore filtering, so always replace it.
const destExtNM = join(packExtDir, extEntry.name, 'node_modules');
if (!existsSync(destExtNM)) {
cpSync(srcNM, destExtNM, { recursive: true });
}
rmSync(destExtNM, { recursive: true, force: true });
cpSync(srcNM, destExtNM, { recursive: true });
extNMCount++;
// Merge into top-level openclaw/node_modules/ (for shared chunks in dist/)

View File

@@ -17,6 +17,7 @@
*/
import 'zx/globals';
import { EXTRA_BUNDLED_PACKAGES } from './openclaw-bundle-config.mjs';
const ROOT = path.resolve(__dirname, '..');
const OUTPUT = path.join(ROOT, 'build', 'openclaw');
@@ -41,6 +42,20 @@ if (!fs.existsSync(openclawLink)) {
const openclawReal = fs.realpathSync(openclawLink);
echo` openclaw resolved: ${openclawReal}`;
function shouldCopyOpenClawPackageEntry(src) {
const rel = path.relative(openclawReal, src);
if (!rel || rel.startsWith('..')) return true;
const parts = rel.split(path.sep);
for (let i = 0; i < parts.length - 1; i += 1) {
if (parts[i] === 'node_modules' && parts[i + 1] === '.bin') {
return false;
}
}
return true;
}
// 2. Clean and create output directory
if (fs.existsSync(OUTPUT)) {
fs.rmSync(OUTPUT, { recursive: true });
@@ -49,7 +64,11 @@ fs.mkdirSync(OUTPUT, { recursive: true });
// 3. Copy openclaw package itself to OUTPUT root
echo` Copying openclaw package...`;
fs.cpSync(openclawReal, OUTPUT, { recursive: true, dereference: true });
fs.cpSync(openclawReal, OUTPUT, {
recursive: true,
dereference: true,
filter: shouldCopyOpenClawPackageEntry,
});
// 4. Recursively collect ALL transitive dependencies via pnpm virtual store BFS
//
@@ -186,10 +205,6 @@ echo` Skipped ${skippedDevCount} dev-only package references`;
//
// 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('/'));
@@ -248,7 +263,31 @@ const copiedNames = new Set(); // Track package names already copied
let copiedCount = 0;
let skippedDupes = 0;
for (const [realPath, pkgName] of collected) {
const preferredBundledPackages = new Set(EXTRA_BUNDLED_PACKAGES);
const preferredBundledPackageRealPaths = new Set();
for (const pkgName of EXTRA_BUNDLED_PACKAGES) {
const pkgLink = path.join(NODE_MODULES, ...pkgName.split('/'));
if (!fs.existsSync(pkgLink)) continue;
try {
preferredBundledPackageRealPaths.add(fs.realpathSync(pkgLink));
} catch {
// ignore
}
}
const collectedEntries = [...collected].sort(([leftRealPath, leftName], [rightRealPath, rightName]) => {
const leftPreferredRealPath = preferredBundledPackageRealPaths.has(leftRealPath);
const rightPreferredRealPath = preferredBundledPackageRealPaths.has(rightRealPath);
if (leftPreferredRealPath !== rightPreferredRealPath) return leftPreferredRealPath ? -1 : 1;
const leftPreferred = preferredBundledPackages.has(leftName);
const rightPreferred = preferredBundledPackages.has(rightName);
if (leftPreferred !== rightPreferred) return leftPreferred ? -1 : 1;
return 0;
});
for (const [realPath, pkgName] of collectedEntries) {
if (copiedNames.has(pkgName)) {
skippedDupes++;
continue; // Keep the first version (closer to openclaw in dep tree)
@@ -280,50 +319,106 @@ for (const [realPath, pkgName] of collected) {
// resolvable from shared chunks. Skip-if-exists preserves version priority
// (openclaw's own deps take precedence over extension deps).
const extensionsDir = path.join(OUTPUT, 'dist', 'extensions');
function readJsonSafe(filePath) {
try {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch {
return null;
}
}
function listPackageDeps(pkgJson) {
return Object.keys({
...(pkgJson?.dependencies && typeof pkgJson.dependencies === 'object' ? pkgJson.dependencies : {}),
...(pkgJson?.optionalDependencies && typeof pkgJson.optionalDependencies === 'object' ? pkgJson.optionalDependencies : {}),
}).sort((a, b) => a.localeCompare(b));
}
let mergedExtCount = 0;
let mirroredExtRuntimeDeps = 0;
if (fs.existsSync(extensionsDir)) {
for (const extEntry of fs.readdirSync(extensionsDir, { withFileTypes: true })) {
if (!extEntry.isDirectory()) continue;
const extNM = path.join(extensionsDir, extEntry.name, 'node_modules');
if (!fs.existsSync(extNM)) continue;
const extRoot = path.join(extensionsDir, extEntry.name);
const extNM = path.join(extRoot, 'node_modules');
for (const pkgEntry of fs.readdirSync(extNM, { withFileTypes: true })) {
if (!pkgEntry.isDirectory() || pkgEntry.name === '.bin') continue;
const srcPkg = path.join(extNM, pkgEntry.name);
if (fs.existsSync(extNM)) {
for (const pkgEntry of fs.readdirSync(extNM, { withFileTypes: true })) {
if (!pkgEntry.isDirectory() || pkgEntry.name === '.bin') continue;
const srcPkg = path.join(extNM, pkgEntry.name);
if (pkgEntry.name.startsWith('@')) {
// Scoped package — iterate sub-entries
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);
if (pkgEntry.name.startsWith('@')) {
// Scoped package — iterate sub-entries
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);
mergedExtCount++;
} catch { /* skip on copy error */ }
}
} else {
if (copiedNames.has(pkgEntry.name)) continue;
const destPkg = path.join(outputNodeModules, pkgEntry.name);
try {
fs.mkdirSync(normWin(path.dirname(destScoped)), { recursive: true });
fs.cpSync(normWin(srcScoped), normWin(destScoped), { recursive: true, dereference: true });
copiedNames.add(scopedName);
fs.cpSync(normWin(srcPkg), normWin(destPkg), { recursive: true, dereference: true });
copiedNames.add(pkgEntry.name);
mergedExtCount++;
} catch { /* skip on copy error */ }
}
} else {
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);
mergedExtCount++;
} catch { /* skip on copy error */ }
}
}
const extPkg = readJsonSafe(path.join(extRoot, 'package.json'));
for (const depName of listPackageDeps(extPkg)) {
const srcPkg = path.join(outputNodeModules, ...depName.split('/'));
const destPkg = path.join(extNM, ...depName.split('/'));
if (!fs.existsSync(srcPkg) || fs.existsSync(destPkg)) continue;
try {
fs.mkdirSync(normWin(path.dirname(destPkg)), { recursive: true });
fs.cpSync(normWin(srcPkg), normWin(destPkg), { recursive: true, dereference: true });
mirroredExtRuntimeDeps++;
} catch { /* skip on copy error */ }
}
}
}
if (mergedExtCount > 0) {
echo` Merged ${mergedExtCount} extension packages into top-level node_modules`;
}
if (mirroredExtRuntimeDeps > 0) {
echo` Mirrored ${mirroredExtRuntimeDeps} extension runtime deps into dist/extensions/*/node_modules`;
}
function patchBundledExtensionPackageJsons(extensionsRoot) {
let patchedCount = 0;
const discordPkgPath = path.join(extensionsRoot, 'discord', 'package.json');
if (fs.existsSync(discordPkgPath)) {
try {
const pkg = JSON.parse(fs.readFileSync(discordPkgPath, 'utf8'));
if (pkg?.dependencies?.opusscript === '^0.0.8') {
pkg.dependencies.opusscript = '^0.1.1';
fs.writeFileSync(discordPkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
patchedCount++;
echo` 🩹 Patched discord bundled runtime dep range: opusscript ^0.0.8 -> ^0.1.1`;
}
} catch {
// ignore
}
}
return patchedCount;
}
patchBundledExtensionPackageJsons(extensionsDir);
// 6. Clean up the bundle to reduce package size
//

View File

@@ -0,0 +1,23 @@
export const EXTRA_BUNDLED_PACKAGES = [
'@whiskeysockets/baileys',
// Built-in channel/runtime extension deps that are not always pulled in by the
// OpenClaw package's own transitive dependency graph, but are required in
// packaged builds when dist/extensions/<channel>/*.js resolves bare imports
// from resources/openclaw/node_modules.
'@larksuiteoapi/node-sdk',
'@grammyjs/runner',
'@grammyjs/transformer-throttler',
'grammy',
'@buape/carbon',
'@discordjs/voice',
'discord-api-types',
'opusscript',
'@tencent-connect/qqbot-connector',
'mpg123-decoder',
'silk-wasm',
// Electron main process QR login flows resolve these files from the
// bundled OpenClaw runtime context in packaged builds.
'qrcode-terminal',
];

View File

@@ -0,0 +1,24 @@
// @vitest-environment node
import { describe, expect, it } from 'vitest';
describe('openclaw bundle config', () => {
it('includes Electron runtime-only packages needed in packaged builds', async () => {
const { EXTRA_BUNDLED_PACKAGES } = await import('../../scripts/openclaw-bundle-config.mjs');
expect(EXTRA_BUNDLED_PACKAGES).toEqual(expect.arrayContaining([
'@whiskeysockets/baileys',
'@larksuiteoapi/node-sdk',
'@grammyjs/runner',
'@grammyjs/transformer-throttler',
'grammy',
'@buape/carbon',
'@discordjs/voice',
'discord-api-types',
'opusscript',
'@tencent-connect/qqbot-connector',
'mpg123-decoder',
'silk-wasm',
'qrcode-terminal',
]));
});
});

View File

@@ -0,0 +1,30 @@
// @vitest-environment node
import { describe, expect, it, vi } from 'vitest';
import { resolveModulePathWithFallbacks } from '@electron/utils/runtime-package-resolution';
describe('runtime package resolution', () => {
it('returns the first successful resolver result', () => {
const openclawResolve = vi.fn(() => {
throw new Error('missing from openclaw');
});
const appResolve = vi.fn(() => '/tmp/app/node_modules/qrcode-terminal/vendor/QRCode/index.js');
const resolved = resolveModulePathWithFallbacks('qrcode-terminal/vendor/QRCode/index.js', [
{ label: 'openclaw', resolve: openclawResolve },
{ label: 'app', resolve: appResolve },
]);
expect(resolved).toBe('/tmp/app/node_modules/qrcode-terminal/vendor/QRCode/index.js');
expect(openclawResolve).toHaveBeenCalledOnce();
expect(appResolve).toHaveBeenCalledOnce();
});
it('surfaces all resolver failures in the final error', () => {
expect(() => resolveModulePathWithFallbacks('qrcode-terminal/vendor/QRCode/index.js', [
{ label: 'openclaw', resolve: () => { throw new Error('not bundled'); } },
{ label: 'app', resolve: () => { throw new Error('not packaged'); } },
])).toThrow(
'openclaw: not bundled | app: not packaged',
);
});
});