fix(openclaw): revert update openclaw version (#944)

This commit is contained in:
Haze
2026-04-30 17:54:03 +08:00
committed by GitHub
parent 1fc1e20ec4
commit f88d6bc17b
23 changed files with 645 additions and 1298 deletions

View File

@@ -29,11 +29,9 @@ extraResources:
# Pre-bundled third-party skills (full directories, not only SKILL.md)
- from: build/preinstalled-skills/
to: resources/preinstalled-skills/
# Pre-bundled third-party OpenClaw plugin mirrors (DingTalk, Lark/Feishu, etc.).
# afterPack still has a fallback bundler, but the generated mirrors must be an
# explicit resource so installers consistently include them.
- from: build/openclaw-plugins/
to: openclaw-plugins/
# NOTE: OpenClaw plugin mirrors (dingtalk, etc.) are bundled by the
# afterPack hook (after-pack.cjs) directly from node_modules, so they
# don't need an extraResources entry here.
afterPack: ./scripts/after-pack.cjs

View File

@@ -18,7 +18,7 @@ function fsPath(filePath: string): string {
import { getAllSettings } from '../utils/store';
import { getApiKey, getDefaultProvider, getProvider } from '../utils/secure-storage';
import { getProviderEnvVar, getKeyableProviderTypes } from '../utils/provider-registry';
import { getOpenClawPluginStageDir, getOpenClawRuntimeDir, getOpenClawRuntimeEntryPath, isOpenClawPresent } from '../utils/paths';
import { getOpenClawDir, getOpenClawEntryPath, isOpenClawPresent } from '../utils/paths';
import { getUvMirrorEnv } from '../utils/uv-env';
import { cleanupDanglingWeChatPluginState, listConfiguredChannelsFromConfig, readOpenClawConfig } from '../utils/channel-config';
import { sanitizeOpenClawConfig, batchSyncConfigFields } from '../utils/openclaw-auth';
@@ -35,7 +35,6 @@ export interface GatewayLaunchContext {
appSettings: Awaited<ReturnType<typeof getAllSettings>>;
openclawDir: string;
entryScript: string;
pluginStageDir: string | null;
gatewayArgs: string[];
forkEnv: Record<string, string | undefined>;
mode: 'dev' | 'packaged';
@@ -87,25 +86,6 @@ function readPluginVersion(pkgJsonPath: string): string | null {
}
}
function collectPluginRuntimeDeps(pluginDir: string): string[] {
try {
const raw = readFileSync(fsPath(join(pluginDir, 'package.json')), 'utf-8');
const pkg = JSON.parse(raw) as { dependencies?: Record<string, string>; optionalDependencies?: Record<string, string> };
return Object.keys({
...pkg.dependencies,
...pkg.optionalDependencies,
});
} catch {
return [];
}
}
function hasPluginRuntimeDeps(pluginDir: string): boolean {
const deps = collectPluginRuntimeDeps(pluginDir);
if (deps.length === 0) return true;
return deps.every((depName) => existsSync(fsPath(join(pluginDir, 'node_modules', ...depName.split('/'), 'package.json'))));
}
function buildBundledPluginSources(pluginDirName: string): string[] {
return app.isPackaged
? [
@@ -142,7 +122,7 @@ function ensureConfiguredPluginsUpgraded(configuredChannels: string[]): void {
if (bundledDir) {
const sourceVersion = readPluginVersion(join(bundledDir, 'package.json'));
// Install or upgrade if version differs or plugin not installed
if (!isInstalled || !hasPluginRuntimeDeps(targetDir) || (sourceVersion && installedVersion && sourceVersion !== installedVersion)) {
if (!isInstalled || (sourceVersion && installedVersion && sourceVersion !== installedVersion)) {
logger.info(`[plugin] ${isInstalled ? 'Auto-upgrading' : 'Installing'} ${channelType} plugin${isInstalled ? `: ${installedVersion}${sourceVersion}` : `: ${sourceVersion}`} (bundled)`);
try {
mkdirSync(fsPath(join(homedir(), '.openclaw', 'extensions')), { recursive: true });
@@ -244,30 +224,9 @@ function ensureExtensionDepsResolvable(openclawDir: string): void {
const topNM = join(openclawDir, 'node_modules');
let linkedCount = 0;
// Use 'junction' on Windows (no admin required); on other platforms the
// type argument is ignored by Node so 'junction' is harmless.
const symlinkType = process.platform === 'win32' ? 'junction' : 'dir';
try {
if (!existsSync(extDir)) return;
// Create openclaw self-reference so built-in extension runtime files can
// resolve bare `import 'openclaw/plugin-sdk/...'` via native ESM.
// Extensions ship their own package.json (e.g. "@openclaw/codex"),
// creating a separate package scope that prevents Node's ESM
// package-self-reference from reaching the openclaw root package.
// Only needed in packaged mode — in dev pnpm already provides resolution.
if (app.isPackaged) {
const selfRef = join(topNM, 'openclaw');
if (!existsSync(selfRef)) {
try {
mkdirSync(topNM, { recursive: true });
symlinkSync(openclawDir, selfRef, symlinkType);
linkedCount++;
} catch { /* skip on error — non-fatal */ }
}
}
for (const ext of readdirSync(extDir, { withFileTypes: true })) {
if (!ext.isDirectory()) continue;
const extNM = join(extDir, ext.name, 'node_modules');
@@ -287,7 +246,7 @@ function ensureExtensionDepsResolvable(openclawDir: string): void {
if (existsSync(dest)) continue;
try {
mkdirSync(join(topNM, pkg.name), { recursive: true });
symlinkSync(join(scopeDir, sub.name), dest, symlinkType);
symlinkSync(join(scopeDir, sub.name), dest);
linkedCount++;
} catch { /* skip on error — non-fatal */ }
}
@@ -296,7 +255,7 @@ function ensureExtensionDepsResolvable(openclawDir: string): void {
if (existsSync(dest)) continue;
try {
mkdirSync(topNM, { recursive: true });
symlinkSync(join(extNM, pkg.name), dest, symlinkType);
symlinkSync(join(extNM, pkg.name), dest);
linkedCount++;
} catch { /* skip on error — non-fatal */ }
}
@@ -447,9 +406,8 @@ async function resolveChannelStartupPolicy(): Promise<{
}
export async function prepareGatewayLaunchContext(port: number): Promise<GatewayLaunchContext> {
const openclawDir = getOpenClawRuntimeDir();
const entryScript = getOpenClawRuntimeEntryPath();
const pluginStageDir = getOpenClawPluginStageDir(openclawDir);
const openclawDir = getOpenClawDir();
const entryScript = getOpenClawEntryPath();
if (!isOpenClawPresent()) {
throw new Error(`OpenClaw package not found at: ${openclawDir}`);
@@ -496,7 +454,6 @@ export async function prepareGatewayLaunchContext(port: number): Promise<Gateway
OPENCLAW_SKIP_CHANNELS: skipChannels ? '1' : '',
CLAWDBOT_SKIP_CHANNELS: skipChannels ? '1' : '',
OPENCLAW_NO_RESPAWN: '1',
...(pluginStageDir ? { OPENCLAW_PLUGIN_STAGE_DIR: pluginStageDir } : {}),
};
// Ensure extension-specific packages (e.g. grammy from the telegram
@@ -508,7 +465,6 @@ export async function prepareGatewayLaunchContext(port: number): Promise<Gateway
appSettings,
openclawDir,
entryScript,
pluginStageDir,
gatewayArgs,
forkEnv,
mode,

View File

@@ -102,7 +102,6 @@ export async function launchGatewayProcess(options: {
const {
openclawDir,
entryScript,
pluginStageDir,
gatewayArgs,
forkEnv,
mode,
@@ -113,9 +112,9 @@ export async function launchGatewayProcess(options: {
} = options.launchContext;
logger.info(
`Starting Gateway process (mode=${mode}, port=${options.port}, entry="${entryScript}", args="${options.sanitizeSpawnArgs(gatewayArgs).join(' ')}", cwd="${openclawDir}", stage="${pluginStageDir || '-'}", bundledBin=${binPathExists ? 'yes' : 'no'}, providerKeys=${loadedProviderKeyCount}, channels=${channelStartupSummary}, proxy=${proxySummary})`,
`Starting Gateway process (mode=${mode}, port=${options.port}, entry="${entryScript}", args="${options.sanitizeSpawnArgs(gatewayArgs).join(' ')}", cwd="${openclawDir}", bundledBin=${binPathExists ? 'yes' : 'no'}, providerKeys=${loadedProviderKeyCount}, channels=${channelStartupSummary}, proxy=${proxySummary})`,
);
const lastSpawnSummary = `mode=${mode}, entry="${entryScript}", args="${options.sanitizeSpawnArgs(gatewayArgs).join(' ')}", cwd="${openclawDir}", stage="${pluginStageDir || '-'}"`;
const lastSpawnSummary = `mode=${mode}, entry="${entryScript}", args="${options.sanitizeSpawnArgs(gatewayArgs).join(' ')}", cwd="${openclawDir}"`;
const runtimeEnv = { ...forkEnv };

View File

@@ -1,7 +1,7 @@
import { app, utilityProcess } from 'electron';
import path from 'path';
import { existsSync } from 'fs';
import { getOpenClawPluginStageDir, getOpenClawRuntimeDir, getOpenClawRuntimeEntryPath } from '../utils/paths';
import { getOpenClawDir, getOpenClawEntryPath } from '../utils/paths';
import { getUvMirrorEnv } from '../utils/uv-env';
import { isPythonReady, setupManagedPython } from '../utils/uv-setup';
import { logger } from '../utils/logger';
@@ -263,9 +263,8 @@ export async function findExistingGatewayProcess(options: {
}
export async function runOpenClawDoctorRepair(): Promise<boolean> {
const openclawDir = getOpenClawRuntimeDir();
const entryScript = getOpenClawRuntimeEntryPath();
const pluginStageDir = getOpenClawPluginStageDir(openclawDir);
const openclawDir = getOpenClawDir();
const entryScript = getOpenClawEntryPath();
if (!existsSync(entryScript)) {
logger.error(`Cannot run OpenClaw doctor repair: entry script not found at ${entryScript}`);
return false;
@@ -286,7 +285,7 @@ export async function runOpenClawDoctorRepair(): Promise<boolean> {
const uvEnv = await getUvMirrorEnv();
const doctorArgs = ['doctor', '--fix', '--yes', '--non-interactive'];
logger.info(
`Running OpenClaw doctor repair (entry="${entryScript}", args="${doctorArgs.join(' ')}", cwd="${openclawDir}", stage="${pluginStageDir || '-'}", bundledBin=${binPathExists ? 'yes' : 'no'})`,
`Running OpenClaw doctor repair (entry="${entryScript}", args="${doctorArgs.join(' ')}", cwd="${openclawDir}", bundledBin=${binPathExists ? 'yes' : 'no'})`,
);
return await new Promise<boolean>((resolve) => {
@@ -294,7 +293,6 @@ export async function runOpenClawDoctorRepair(): Promise<boolean> {
...baseEnvPatched,
...uvEnv,
OPENCLAW_NO_RESPAWN: '1',
...(pluginStageDir ? { OPENCLAW_PLUGIN_STAGE_DIR: pluginStageDir } : {}),
};
const child = utilityProcess.fork(entryScript, doctorArgs, {

View File

@@ -8,7 +8,7 @@ import { access, mkdir, readFile, writeFile, readdir, stat, rm } from 'fs/promis
import { constants } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { getOpenClawPluginStageDir, getOpenClawRuntimeDir } from './paths';
import { getOpenClawResolvedDir } from './paths';
import * as logger from './logger';
import { proxyAwareFetch } from './proxy-fetch';
import { withConfigLock } from './config-mutex';
@@ -562,7 +562,6 @@ function transformChannelConfig(
transformedConfig = { ...restConfig };
transformedConfig.groupPolicy = 'allowlist';
transformedConfig.dmPolicy = transformedConfig.dmPolicy ?? existingAccountConfig.dmPolicy ?? 'disabled';
transformedConfig.dm = { enabled: false };
transformedConfig.retry = {
attempts: 3,
@@ -606,21 +605,6 @@ function transformChannelConfig(
transformedConfig.allowFrom = users;
}
}
const allowFrom = Array.isArray(transformedConfig.allowFrom)
? transformedConfig.allowFrom
: Array.isArray(existingAccountConfig.allowFrom)
? existingAccountConfig.allowFrom
: [];
const hasWildcardAccess = allowFrom.includes('*');
transformedConfig.dmPolicy =
transformedConfig.dmPolicy
?? existingAccountConfig.dmPolicy
?? (hasWildcardAccess ? 'open' : 'allowlist');
transformedConfig.groupPolicy =
transformedConfig.groupPolicy
?? existingAccountConfig.groupPolicy
?? (hasWildcardAccess ? 'open' : 'allowlist');
}
if (channelType === 'feishu' || channelType === 'wecom') {
@@ -1528,8 +1512,7 @@ export async function validateChannelConfig(channelType: string): Promise<Valida
const result: ValidationResult = { valid: true, errors: [], warnings: [] };
try {
const openclawPath = getOpenClawRuntimeDir();
const pluginStageDir = getOpenClawPluginStageDir(openclawPath);
const openclawPath = getOpenClawResolvedDir();
// Run openclaw doctor command to validate config (async to avoid
// blocking the main thread).
@@ -1540,11 +1523,6 @@ export async function validateChannelConfig(channelType: string): Promise<Valida
{
cwd: openclawPath,
encoding: 'utf-8',
env: {
...process.env,
...(pluginStageDir ? { OPENCLAW_PLUGIN_STAGE_DIR: pluginStageDir } : {}),
OPENCLAW_NO_RESPAWN: '1',
},
timeout: 30000,
windowsHide: true,
},

View File

@@ -32,7 +32,6 @@ const AUTH_STORE_VERSION = 1;
const AUTH_PROFILE_FILENAME = 'auth-profiles.json';
const LEGACY_MINIMAX_OAUTH_PLUGIN_ID = 'minimax-portal-auth';
const MERGED_MINIMAX_PLUGIN_ID = 'minimax';
const PACKAGED_CONTROL_UI_ALLOWED_ORIGINS = ['file://', 'null'] as const;
interface BundledPluginManifest {
id: string;
@@ -68,24 +67,6 @@ function getOpenClawExtensionsRoots(): string[] {
];
}
function ensurePackagedControlUiAllowedOrigins(controlUi: Record<string, unknown>): Record<string, unknown> {
const allowedOrigins = Array.isArray(controlUi.allowedOrigins)
? (controlUi.allowedOrigins as unknown[]).filter((value): value is string => typeof value === 'string')
: [];
const nextAllowedOrigins = [...allowedOrigins];
for (const origin of PACKAGED_CONTROL_UI_ALLOWED_ORIGINS) {
if (!nextAllowedOrigins.includes(origin)) {
nextAllowedOrigins.push(origin);
}
}
return {
...controlUi,
allowedOrigins: nextAllowedOrigins,
};
}
function discoverBundledPluginManifests(): BundledPluginManifest[] {
if (_bundledPluginManifestCache) return _bundledPluginManifestCache;
@@ -1341,14 +1322,20 @@ export async function syncGatewayTokenToConfig(token: string): Promise<void> {
auth.token = token;
gateway.auth = auth;
// Packaged ClawX loads the renderer from file://. OpenClaw's URL origin
// normalization maps that to "null", so keep both forms allowlisted.
// Packaged ClawX loads the renderer from file://, so the gateway must allow
// that origin for the chat WebSocket handshake.
const controlUi = (
gateway.controlUi && typeof gateway.controlUi === 'object'
? { ...(gateway.controlUi as Record<string, unknown>) }
: {}
) as Record<string, unknown>;
gateway.controlUi = ensurePackagedControlUiAllowedOrigins(controlUi);
const allowedOrigins = Array.isArray(controlUi.allowedOrigins)
? (controlUi.allowedOrigins as unknown[]).filter((value): value is string => typeof value === 'string')
: [];
if (!allowedOrigins.includes('file://')) {
controlUi.allowedOrigins = [...allowedOrigins, 'file://'];
}
gateway.controlUi = controlUi;
if (!gateway.mode) gateway.mode = 'local';
config.gateway = gateway;
@@ -1475,7 +1462,13 @@ export async function batchSyncConfigFields(token: string): Promise<void> {
? { ...(gateway.controlUi as Record<string, unknown>) }
: {}
) as Record<string, unknown>;
gateway.controlUi = ensurePackagedControlUiAllowedOrigins(controlUi);
const allowedOrigins = Array.isArray(controlUi.allowedOrigins)
? (controlUi.allowedOrigins as unknown[]).filter((v): v is string => typeof v === 'string')
: [];
if (!allowedOrigins.includes('file://')) {
controlUi.allowedOrigins = [...allowedOrigins, 'file://'];
}
gateway.controlUi = controlUi;
if (!gateway.mode) gateway.mode = 'local';
config.gateway = gateway;

View File

@@ -1,7 +1,7 @@
import { app, utilityProcess } from 'electron';
import { existsSync } from 'node:fs';
import path from 'node:path';
import { getOpenClawPluginStageDir, getOpenClawRuntimeDir, getOpenClawRuntimeEntryPath } from './paths';
import { getOpenClawDir, getOpenClawEntryPath } from './paths';
import { logger } from './logger';
import { getUvMirrorEnv } from './uv-env';
@@ -69,9 +69,8 @@ async function runDoctorCommandWithArgs(
mode: OpenClawDoctorMode,
args: string[],
): Promise<OpenClawDoctorResult> {
const openclawDir = getOpenClawRuntimeDir();
const entryScript = getOpenClawRuntimeEntryPath();
const pluginStageDir = getOpenClawPluginStageDir(openclawDir);
const openclawDir = getOpenClawDir();
const entryScript = getOpenClawEntryPath();
const command = `openclaw ${args.join(' ')}`;
const startedAt = Date.now();
@@ -99,7 +98,7 @@ async function runDoctorCommandWithArgs(
const uvEnv = await getUvMirrorEnv();
logger.info(
`Running OpenClaw doctor (mode=${mode}, entry="${entryScript}", args="${args.join(' ')}", cwd="${openclawDir}", stage="${pluginStageDir || '-'}", bundledBin=${binPathExists ? 'yes' : 'no'})`,
`Running OpenClaw doctor (mode=${mode}, entry="${entryScript}", args="${args.join(' ')}", cwd="${openclawDir}", bundledBin=${binPathExists ? 'yes' : 'no'})`,
);
return await new Promise<OpenClawDoctorResult>((resolve) => {
@@ -111,7 +110,6 @@ async function runDoctorCommandWithArgs(
...uvEnv,
PATH: finalPath,
OPENCLAW_NO_RESPAWN: '1',
...(pluginStageDir ? { OPENCLAW_PLUGIN_STAGE_DIR: pluginStageDir } : {}),
} as NodeJS.ProcessEnv,
});

View File

@@ -1,10 +1,9 @@
/**
* Dynamic imports for openclaw plugin-sdk subpath exports.
*
* openclaw is NOT in the asar's node_modules — packaged builds stage it under
* resources/openclaw-runtime/<openclaw-versioned-dir>. Static
* `import ... from 'openclaw/plugin-sdk/...'` would produce a runtime require()
* that fails inside the asar.
* openclaw is NOT in the asar's node_modules — it lives at resources/openclaw/
* (extraResources). Static `import ... from 'openclaw/plugin-sdk/...'` would
* produce a runtime require() that fails inside the asar.
*
* Instead, we create a require context from the openclaw directory itself.
* Node.js package self-referencing allows a package to require its own exports

View File

@@ -3,30 +3,11 @@
* Cross-platform path resolution helpers
*/
import { createRequire } from 'node:module';
import { createHash } from 'node:crypto';
import { dirname, join } from 'path';
import { join } from 'path';
import { homedir } from 'os';
import {
cpSync,
accessSync,
constants,
existsSync,
mkdirSync,
readFileSync,
realpathSync,
readdirSync,
renameSync,
rmSync,
statSync,
writeFileSync,
} from 'fs';
import { existsSync, mkdirSync, readFileSync, realpathSync } from 'fs';
const require = createRequire(import.meta.url);
const OPENCLAW_RUNTIME_MANIFEST = 'clawx-runtime-deps.json';
const OPENCLAW_RUNTIME_READY_MARKER = '.clawx-runtime-ready.json';
const OPENCLAW_RUNTIME_KEEP_COUNT = 2;
let materializedOpenClawDir: string | null = null;
type ElectronAppLike = Pick<typeof import('electron').app, 'isPackaged' | 'getPath' | 'getAppPath'>;
@@ -110,185 +91,6 @@ export function ensureDir(dir: string): void {
}
}
function toFsPath(filePath: string): string {
if (process.platform !== 'win32') return filePath;
if (!filePath || filePath.startsWith('\\\\?\\')) return filePath;
const windowsPath = filePath.replace(/\//g, '\\');
if (!windowsPath.match(/^[a-zA-Z]:\\/u) && !windowsPath.startsWith('\\\\')) return windowsPath;
if (windowsPath.startsWith('\\\\')) return `\\\\?\\UNC\\${windowsPath.slice(2)}`;
return `\\\\?\\${windowsPath}`;
}
function readJsonFile<T extends Record<string, unknown>>(filePath: string): T | null {
try {
return JSON.parse(readFileSync(toFsPath(filePath), 'utf-8')) as T;
} catch {
return null;
}
}
function hashOpenClawRuntime(sourceDir: string): { key: string; version: string; manifestHash: string } {
const packageJsonPath = join(sourceDir, 'package.json');
const manifestPath = join(sourceDir, OPENCLAW_RUNTIME_MANIFEST);
const packageJsonRaw = existsSync(toFsPath(packageJsonPath)) ? readFileSync(toFsPath(packageJsonPath), 'utf-8') : '{}';
const manifestRaw = existsSync(toFsPath(manifestPath)) ? readFileSync(toFsPath(manifestPath), 'utf-8') : '';
const packageJson = JSON.parse(packageJsonRaw) as { version?: string };
const version = packageJson.version || 'unknown';
const manifestHash = createHash('sha256')
.update(packageJsonRaw)
.update('\0')
.update(manifestRaw)
.digest('hex')
.slice(0, 12);
const safeVersion = version.replace(/[^a-zA-Z0-9._-]/gu, '_');
return {
key: `openclaw-${safeVersion}-${manifestHash}`,
version,
manifestHash,
};
}
function isMaterializedRuntimeReady(targetDir: string, version: string, manifestHash: string): boolean {
const marker = readJsonFile<{ version?: unknown; manifestHash?: unknown }>(
join(targetDir, OPENCLAW_RUNTIME_READY_MARKER),
);
if (marker?.version !== version || marker?.manifestHash !== manifestHash) return false;
return existsSync(toFsPath(join(targetDir, 'openclaw.mjs')))
&& existsSync(toFsPath(join(targetDir, 'package.json')))
&& existsSync(toFsPath(join(targetDir, 'dist')))
&& existsSync(toFsPath(join(targetDir, 'node_modules')));
}
function isPackagedRuntimeReady(targetDir: string): boolean {
const marker = readJsonFile<{ version?: unknown; manifestHash?: unknown }>(
join(targetDir, OPENCLAW_RUNTIME_READY_MARKER),
);
if (typeof marker?.version !== 'string' || typeof marker?.manifestHash !== 'string') return false;
return existsSync(toFsPath(join(targetDir, 'openclaw.mjs')))
&& existsSync(toFsPath(join(targetDir, 'package.json')))
&& existsSync(toFsPath(join(targetDir, 'dist')))
&& existsSync(toFsPath(join(targetDir, 'node_modules')));
}
function isOpenClawRuntimeWritable(runtimeDir: string): boolean {
try {
accessSync(toFsPath(runtimeDir), constants.W_OK);
accessSync(toFsPath(join(runtimeDir, 'dist', 'extensions')), constants.W_OK);
return true;
} catch {
return false;
}
}
function cleanupOldOpenClawRuntimes(runtimeRoot: string, keepKey: string): void {
let entries;
try {
entries = readdirSync(toFsPath(runtimeRoot), { withFileTypes: true })
.filter((entry) => entry.isDirectory() && entry.name.startsWith('openclaw-'))
.map((entry) => {
const fullPath = join(runtimeRoot, entry.name);
let mtimeMs = 0;
try {
mtimeMs = statSync(toFsPath(fullPath)).mtimeMs;
} catch {
// Keep unknown entries until a later cleanup pass.
}
return { name: entry.name, fullPath, mtimeMs };
})
.sort((left, right) => right.mtimeMs - left.mtimeMs);
} catch {
return;
}
const keep = new Set(entries.slice(0, OPENCLAW_RUNTIME_KEEP_COUNT).map((entry) => entry.name));
keep.add(keepKey);
for (const entry of entries) {
if (keep.has(entry.name)) continue;
try {
rmSync(toFsPath(entry.fullPath), { recursive: true, force: true });
} catch {
// Cleanup is best-effort; stale runtimes are harmless.
}
}
}
function getPackagedOpenClawSourceDir(): string {
return join(process.resourcesPath, 'openclaw');
}
function getPackagedOpenClawRuntimeRoot(): string {
return join(process.resourcesPath, 'openclaw-runtime');
}
function findPackagedOpenClawRuntimeDir(): string | null {
const runtimeRoot = getPackagedOpenClawRuntimeRoot();
let entries;
try {
entries = readdirSync(toFsPath(runtimeRoot), { withFileTypes: true });
} catch {
return null;
}
const candidates = entries
.filter((entry) => entry.isDirectory() && entry.name.startsWith('openclaw-'))
.map((entry) => join(runtimeRoot, entry.name))
.filter((candidate) => isPackagedRuntimeReady(candidate))
.sort((left, right) => right.localeCompare(left));
return candidates[0] ?? null;
}
function getPackagedOpenClawFallbackSourceDir(): string {
return findPackagedOpenClawRuntimeDir() ?? getPackagedOpenClawSourceDir();
}
function materializePackagedOpenClawRuntime(): string {
if (materializedOpenClawDir) return materializedOpenClawDir;
const sourceDir = getPackagedOpenClawFallbackSourceDir();
if (!existsSync(toFsPath(sourceDir)) || !existsSync(toFsPath(join(sourceDir, 'package.json')))) {
return sourceDir;
}
const { key, version, manifestHash } = hashOpenClawRuntime(sourceDir);
const runtimeRoot = join(getElectronApp().getPath('userData'), 'openclaw-runtime');
const targetDir = join(runtimeRoot, key);
if (isMaterializedRuntimeReady(targetDir, version, manifestHash)) {
materializedOpenClawDir = targetDir;
cleanupOldOpenClawRuntimes(runtimeRoot, key);
return targetDir;
}
mkdirSync(toFsPath(runtimeRoot), { recursive: true });
const tempDir = join(runtimeRoot, `.tmp-${key}-${process.pid}-${Date.now()}`);
rmSync(toFsPath(tempDir), { recursive: true, force: true });
try {
cpSync(toFsPath(sourceDir), toFsPath(tempDir), {
recursive: true,
dereference: true,
force: true,
});
writeFileSync(
toFsPath(join(tempDir, OPENCLAW_RUNTIME_READY_MARKER)),
`${JSON.stringify({
version,
manifestHash,
source: sourceDir,
createdAt: new Date().toISOString(),
}, null, 2)}\n`,
'utf-8',
);
rmSync(toFsPath(targetDir), { recursive: true, force: true });
renameSync(toFsPath(tempDir), toFsPath(targetDir));
} catch (error) {
rmSync(toFsPath(tempDir), { recursive: true, force: true });
throw error;
}
materializedOpenClawDir = targetDir;
cleanupOldOpenClawRuntimes(runtimeRoot, key);
return targetDir;
}
/**
* Get resources directory (for bundled assets)
*/
@@ -308,35 +110,17 @@ export function getPreloadPath(): string {
/**
* Get OpenClaw package directory
* - Production (packaged): from resources/openclaw-runtime/openclaw-* (staged by afterPack)
* - Production (packaged): from resources/openclaw (copied by electron-builder extraResources)
* - Development: from node_modules/openclaw
*/
export function getOpenClawDir(): string {
if (getElectronApp().isPackaged) {
return getPackagedOpenClawFallbackSourceDir();
return join(process.resourcesPath, 'openclaw');
}
// Development: use node_modules/openclaw
return join(__dirname, '../../node_modules/openclaw');
}
/**
* Get the OpenClaw runtime directory used for spawned OpenClaw processes.
*
* In packaged builds, this prefers the afterPack-staged resources/openclaw-runtime
* directory. If the app was installed somewhere non-writable, it falls back to a
* userData copy so OpenClaw's runtime-deps checks can still create lock files.
*/
export function getOpenClawRuntimeDir(): string {
if (getElectronApp().isPackaged) {
const packagedRuntimeDir = findPackagedOpenClawRuntimeDir();
if (packagedRuntimeDir && isOpenClawRuntimeWritable(packagedRuntimeDir)) {
return packagedRuntimeDir;
}
return materializePackagedOpenClawRuntime();
}
return getOpenClawDir();
}
/**
* Get OpenClaw package directory resolved to a real path.
* Useful when consumers need deterministic module resolution under pnpm symlinks.
@@ -360,26 +144,6 @@ export function getOpenClawEntryPath(): string {
return join(getOpenClawDir(), 'openclaw.mjs');
}
/**
* Get OpenClaw entry script path for spawned OpenClaw processes.
*/
export function getOpenClawRuntimeEntryPath(): string {
return join(getOpenClawRuntimeDir(), 'openclaw.mjs');
}
/**
* Get the external runtime dependency staging directory for OpenClaw.
*
* In packaged mode this intentionally points at the parent of the materialized
* runtime directory. OpenClaw recognizes an existing openclaw-* package root
* under OPENCLAW_PLUGIN_STAGE_DIR and reuses its node_modules instead of
* running npm install into ~/.openclaw/plugin-runtime-deps.
*/
export function getOpenClawPluginStageDir(openclawDir = getOpenClawRuntimeDir()): string | null {
if (!getElectronApp().isPackaged) return null;
return dirname(openclawDir);
}
/**
* Get ClawHub CLI entry script path (clawdhub.js)
*/

View File

@@ -250,25 +250,6 @@ function readPluginVersion(pkgJsonPath: string): string | null {
}
}
function collectPluginRuntimeDeps(pluginDir: string): string[] {
try {
const raw = readFileSync(fsPath(join(pluginDir, 'package.json')), 'utf-8');
const pkg = JSON.parse(raw) as { dependencies?: Record<string, string>; optionalDependencies?: Record<string, string> };
return Object.keys({
...pkg.dependencies,
...pkg.optionalDependencies,
});
} catch {
return [];
}
}
function hasPluginRuntimeDeps(pluginDir: string): boolean {
const deps = collectPluginRuntimeDeps(pluginDir);
if (deps.length === 0) return true;
return deps.every((depName) => existsSync(fsPath(join(pluginDir, 'node_modules', ...depName.split('/'), 'package.json'))));
}
// ── pnpm-aware node_modules copy helpers ─────────────────────────────────────
/** Walk up from a path until we find a parent named node_modules. */
@@ -395,7 +376,7 @@ export function ensurePluginInstalled(
if (!sourceDir) return { installed: true }; // no bundled source to compare, keep existing
const installedVersion = readPluginVersion(targetPkgJson);
const sourceVersion = readPluginVersion(join(sourceDir, 'package.json'));
if ((!sourceVersion || !installedVersion || sourceVersion === installedVersion) && hasPluginRuntimeDeps(targetDir)) {
if (!sourceVersion || !installedVersion || sourceVersion === installedVersion) {
return { installed: true }; // same version or unable to compare
}
// Version differs — fall through to overwrite install

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 { getOpenClawDir, getOpenClawResolvedDir } from './paths';
import { getOpenClawResolvedDir } from './paths';
export const DEFAULT_WECHAT_BASE_URL = 'https://ilinkai.weixin.qq.com';
const DEFAULT_ILINK_BOT_TYPE = '3';
@@ -18,10 +18,6 @@ const WECHAT_STATE_DIR = join(OPENCLAW_DIR, 'openclaw-weixin');
const WECHAT_ACCOUNT_INDEX_FILE = join(WECHAT_STATE_DIR, 'accounts.json');
const WECHAT_ACCOUNTS_DIR = join(WECHAT_STATE_DIR, 'accounts');
const require = createRequire(import.meta.url);
const openclawPath = getOpenClawDir();
const openclawResolvedPath = getOpenClawResolvedDir();
const openclawRequire = createRequire(join(openclawResolvedPath, 'package.json'));
const projectRequire = createRequire(join(openclawPath, 'package.json'));
type QrCodeMatrix = {
addData(input: string): void;
@@ -42,29 +38,14 @@ type QrRenderDeps = {
let qrRenderDeps: QrRenderDeps | null = null;
function resolveOpenClawModule(specifier: string): string {
try {
return openclawRequire.resolve(specifier);
} catch { /* fall through */ }
try {
return projectRequire.resolve(specifier);
} catch (err) {
const reason = err instanceof Error ? err.message : String(err);
throw new Error(
`Failed to resolve "${specifier}" from OpenClaw context. ` +
`openclawPath=${openclawPath}, resolvedPath=${openclawResolvedPath}. ${reason}`,
{ cause: err },
);
}
}
function getQrRenderDeps(): QrRenderDeps {
if (qrRenderDeps) {
return qrRenderDeps;
}
const qrCodeModulePath = resolveOpenClawModule('qrcode-terminal/vendor/QRCode/index.js');
const qrErrorCorrectLevelPath = resolveOpenClawModule('qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js');
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');
qrRenderDeps = {
QRCode: require(qrCodeModulePath),
QRErrorCorrectLevel: require(qrErrorCorrectLevelPath),

View File

@@ -12,7 +12,7 @@ const require = createRequire(import.meta.url);
const openclawPath = getOpenClawDir();
const openclawResolvedPath = getOpenClawResolvedDir();
// Primary: resolves from openclaw's real (dereferenced) path in pnpm store.
// In packaged builds this is the staged resources/openclaw-runtime/<openclaw-versioned-dir>/node_modules.
// In packaged builds this is the flat `resources/openclaw/node_modules/`.
const openclawRequire = createRequire(join(openclawResolvedPath, 'package.json'));
// Fallback: resolves from the symlink path (`node_modules/openclaw`).
// In dev mode, Node walks UP from here to `<project>/node_modules/`, which
@@ -39,23 +39,9 @@ function resolveOpenClawPackageJson(packageName: string): string {
}
}
function resolveOpenClawModule(specifier: string): string {
try {
return openclawRequire.resolve(specifier);
} catch { /* fall through */ }
try {
return projectRequire.resolve(specifier);
} catch (err) {
const reason = err instanceof Error ? err.message : String(err);
throw new Error(
`Failed to resolve "${specifier}" from OpenClaw context. ` +
`openclawPath=${openclawPath}, resolvedPath=${openclawResolvedPath}. ${reason}`,
{ cause: err }
);
}
}
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 {
@@ -65,6 +51,10 @@ 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 };
@@ -78,41 +68,12 @@ 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 = resolveOpenClawModule('qrcode-terminal/vendor/QRCode/index.js');
const qrErrorCorrectLevelPath = resolveOpenClawModule('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

@@ -1,6 +1,6 @@
{
"name": "clawx",
"version": "0.3.12-beta.2",
"version": "0.3.11",
"pnpm": {
"onlyBuiltDependencies": [
"@discordjs/opus",
@@ -140,7 +140,7 @@
"jsdom": "^28.1.0",
"lucide-react": "^0.563.0",
"mpg123-decoder": "^1.0.3",
"openclaw": "2026.4.26",
"openclaw": "2026.4.23",
"opusscript": "^0.1.1",
"playwright-core": "1.59.1",
"png2icons": "^2.0.1",
@@ -169,4 +169,4 @@
"zx": "^8.8.5"
},
"packageManager": "pnpm@10.31.0+sha512.e3927388bfaa8078ceb79b748ffc1e8274e84d75163e67bc22e06c0d3aed43dd153151cbf11d7f8301ff4acb98c68bdc5cadf6989532801ffafe3b3e4a63c268"
}
}

897
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,30 +16,15 @@ if [ "$(uname)" = "Darwin" ]; then
# SCRIPT_DIR = .../Contents/Resources/cli
CONTENTS_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
ELECTRON="$CONTENTS_DIR/MacOS/ClawX"
RESOURCES_DIR="$CONTENTS_DIR/Resources"
CLI="$CONTENTS_DIR/Resources/openclaw/openclaw.mjs"
else
# Linux: /opt/ClawX/resources/cli/openclaw
# SCRIPT_DIR = .../resources/cli
INSTALL_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
ELECTRON="$INSTALL_DIR/clawx"
RESOURCES_DIR="$INSTALL_DIR/resources"
CLI="$INSTALL_DIR/resources/openclaw/openclaw.mjs"
fi
OPENCLAW_RUNTIME_ROOT="$RESOURCES_DIR/openclaw-runtime"
OPENCLAW_DIR=""
for dir in "$OPENCLAW_RUNTIME_ROOT"/openclaw-*; do
if [ -d "$dir" ]; then
OPENCLAW_DIR="$dir"
break
fi
done
if [ -z "$OPENCLAW_DIR" ]; then
OPENCLAW_DIR="$RESOURCES_DIR/openclaw"
else
export OPENCLAW_PLUGIN_STAGE_DIR="$OPENCLAW_RUNTIME_ROOT"
fi
CLI="$OPENCLAW_DIR/openclaw.mjs"
if [ ! -f "$ELECTRON" ]; then
echo "Error: ClawX executable not found at $ELECTRON" >&2
echo "Please reinstall ClawX or remove this script: $0" >&2

View File

@@ -15,20 +15,7 @@ esac
export OPENCLAW_EMBEDDED_IN="ClawX"
NODE_EXE="$INSTALL_DIR/resources/bin/node.exe"
OPENCLAW_RUNTIME_ROOT="$INSTALL_DIR/resources/openclaw-runtime"
OPENCLAW_DIR=""
for dir in "$OPENCLAW_RUNTIME_ROOT"/openclaw-*; do
if [ -d "$dir" ]; then
OPENCLAW_DIR="$dir"
break
fi
done
if [ -z "$OPENCLAW_DIR" ]; then
OPENCLAW_DIR="$INSTALL_DIR/resources/openclaw"
else
export OPENCLAW_PLUGIN_STAGE_DIR="$OPENCLAW_RUNTIME_ROOT"
fi
OPENCLAW_ENTRY="$OPENCLAW_DIR/openclaw.mjs"
OPENCLAW_ENTRY="$INSTALL_DIR/resources/openclaw/openclaw.mjs"
if [ -f "$NODE_EXE" ]; then
if "$NODE_EXE" -e 'const [maj,min]=process.versions.node.split(".").map(Number);process.exit((maj>22||maj===22&&min>=16)?0:1)' >/dev/null 2>&1; then

View File

@@ -17,12 +17,7 @@ chcp 65001 >nul 2>&1
set OPENCLAW_EMBEDDED_IN=ClawX
set "NODE_EXE=%~dp0..\bin\node.exe"
set "OPENCLAW_RUNTIME_ROOT=%~dp0..\openclaw-runtime"
set "OPENCLAW_DIR="
for /d %%d in ("%OPENCLAW_RUNTIME_ROOT%\openclaw-*") do if not defined OPENCLAW_DIR set "OPENCLAW_DIR=%%~fd"
if not defined OPENCLAW_DIR set "OPENCLAW_DIR=%~dp0..\openclaw"
set "OPENCLAW_ENTRY=%OPENCLAW_DIR%\openclaw.mjs"
if exist "%OPENCLAW_RUNTIME_ROOT%" set "OPENCLAW_PLUGIN_STAGE_DIR=%OPENCLAW_RUNTIME_ROOT%"
set "OPENCLAW_ENTRY=%~dp0..\openclaw\openclaw.mjs"
set "_USE_BUNDLED_NODE=0"
if exist "%NODE_EXE%" (

View File

@@ -19,13 +19,9 @@
* @mariozechner/clipboard).
*/
const { createHash } = require('crypto');
const { cpSync, existsSync, readdirSync, rmSync, statSync, mkdirSync, realpathSync, readFileSync, renameSync, writeFileSync } = require('fs');
const { cpSync, existsSync, readdirSync, rmSync, statSync, mkdirSync, realpathSync } = require('fs');
const { join, dirname, basename, relative } = require('path');
const RUNTIME_DEPS_MANIFEST = 'clawx-runtime-deps.json';
const OPENCLAW_RUNTIME_READY_MARKER = '.clawx-runtime-ready.json';
// 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
// registry key, but as a safety net we normalize paths to use the \\?\ prefix
@@ -463,19 +459,10 @@ function bundlePlugin(nodeModulesRoot, npmName, destDir) {
let realPluginPath;
try { realPluginPath = realpathSync(pkgPath); } catch { realPluginPath = pkgPath; }
function shouldCopyNodePackageEntry(src) {
const base = basename(src);
return base !== '.vscode' && base !== '.idea';
}
// Copy plugin package itself
if (existsSync(normWin(destDir))) rmSync(normWin(destDir), { recursive: true, force: true });
mkdirSync(normWin(destDir), { recursive: true });
cpSync(normWin(realPluginPath), normWin(destDir), {
recursive: true,
dereference: true,
filter: shouldCopyNodePackageEntry,
});
cpSync(normWin(realPluginPath), normWin(destDir), { recursive: true, dereference: true });
// Collect transitive deps via pnpm virtual store BFS
const collected = new Map();
@@ -528,11 +515,7 @@ function bundlePlugin(nodeModulesRoot, npmName, destDir) {
const d = join(destNM, pkgName);
try {
mkdirSync(normWin(dirname(d)), { recursive: true });
cpSync(normWin(rp), normWin(d), {
recursive: true,
dereference: true,
filter: shouldCopyNodePackageEntry,
});
cpSync(normWin(rp), normWin(d), { recursive: true, dereference: true });
count++;
} catch (e) {
console.warn(`[after-pack] Skipped dep ${pkgName}: ${e.message}`);
@@ -542,132 +525,6 @@ function bundlePlugin(nodeModulesRoot, npmName, destDir) {
return true;
}
function hasPluginRuntimeDeps(pluginDestDir) {
const pluginNM = join(pluginDestDir, 'node_modules');
if (!existsSync(normWin(pluginNM))) return false;
try {
return readdirSync(normWin(pluginNM)).some((entry) => entry !== '.bin');
} catch {
return false;
}
}
function collectPluginRuntimeDeps(pluginDestDir) {
try {
const pkg = JSON.parse(readFileSync(normWin(join(pluginDestDir, 'package.json')), 'utf8'));
return Object.keys({
...pkg.dependencies,
...pkg.optionalDependencies,
});
} catch {
return [];
}
}
function missingPluginRuntimeDeps(pluginDestDir) {
return collectPluginRuntimeDeps(pluginDestDir).filter((depName) => {
const depPackageJson = join(pluginDestDir, 'node_modules', ...depName.split('/'), 'package.json');
return !existsSync(normWin(depPackageJson));
});
}
function preparePackagedPluginMirror(nodeModulesRoot, npmName, pluginId, pluginDestDir, platform, arch) {
const manifestPath = join(pluginDestDir, 'openclaw.plugin.json');
if (!existsSync(normWin(manifestPath)) || !hasPluginRuntimeDeps(pluginDestDir) || missingPluginRuntimeDeps(pluginDestDir).length > 0) {
console.log(`[after-pack] Bundling plugin ${npmName} -> ${pluginDestDir}`);
const ok = bundlePlugin(nodeModulesRoot, npmName, pluginDestDir);
if (!ok) return;
} else {
console.log(`[after-pack] ✅ Plugin mirror already present: ${pluginDestDir}`);
}
const pluginNM = join(pluginDestDir, 'node_modules');
cleanupUnnecessaryFiles(pluginDestDir);
if (existsSync(normWin(pluginNM))) {
cleanupKoffi(pluginNM, platform, arch);
cleanupNativePlatformPackages(pluginNM, platform, arch);
}
patchPluginIds(pluginDestDir, pluginId);
}
function validateRuntimeDepsManifest(openclawRoot) {
const manifestPath = join(openclawRoot, RUNTIME_DEPS_MANIFEST);
if (!existsSync(normWin(manifestPath))) {
throw new Error(`[after-pack] Missing ${RUNTIME_DEPS_MANIFEST} in packaged OpenClaw resources`);
}
const manifest = JSON.parse(readFileSync(normWin(manifestPath), 'utf8'));
const plugins = manifest && typeof manifest === 'object' ? manifest.plugins : null;
if (!plugins || typeof plugins !== 'object' || Array.isArray(plugins)) {
throw new Error(`[after-pack] Invalid ${RUNTIME_DEPS_MANIFEST}: missing plugins object`);
}
const missing = [];
for (const [pluginId, deps] of Object.entries(plugins)) {
if (!Array.isArray(deps)) continue;
for (const dep of deps) {
if (!dep || typeof dep.name !== 'string') continue;
const depPackageJson = join(openclawRoot, 'node_modules', ...dep.name.split('/'), 'package.json');
if (!existsSync(normWin(depPackageJson))) {
missing.push(`${pluginId}:${dep.name}`);
}
}
}
if (missing.length > 0) {
throw new Error(`[after-pack] Missing packaged OpenClaw runtime deps: ${missing.join(', ')}`);
}
console.log(`[after-pack] ✅ Verified ${RUNTIME_DEPS_MANIFEST}.`);
}
function hashOpenClawRuntime(openclawRoot) {
const packageJsonPath = join(openclawRoot, 'package.json');
const manifestPath = join(openclawRoot, RUNTIME_DEPS_MANIFEST);
const packageJsonRaw = readFileSync(normWin(packageJsonPath), 'utf8');
const manifestRaw = existsSync(normWin(manifestPath)) ? readFileSync(normWin(manifestPath), 'utf8') : '';
const packageJson = JSON.parse(packageJsonRaw);
const version = packageJson.version || 'unknown';
const manifestHash = createHash('sha256')
.update(packageJsonRaw)
.update('\0')
.update(manifestRaw)
.digest('hex')
.slice(0, 12);
const safeVersion = String(version).replace(/[^a-zA-Z0-9._-]/g, '_');
return {
key: `openclaw-${safeVersion}-${manifestHash}`,
version,
manifestHash,
};
}
function stageOpenClawRuntimeForPackagedApp(resourcesDir, openclawRoot) {
if (!existsSync(normWin(openclawRoot))) {
throw new Error(`[after-pack] Cannot stage OpenClaw runtime; missing ${openclawRoot}`);
}
const { key, version, manifestHash } = hashOpenClawRuntime(openclawRoot);
const runtimeRoot = join(resourcesDir, 'openclaw-runtime');
const targetDir = join(runtimeRoot, key);
mkdirSync(normWin(runtimeRoot), { recursive: true });
rmSync(normWin(targetDir), { recursive: true, force: true });
renameSync(normWin(openclawRoot), normWin(targetDir));
writeFileSync(
normWin(join(targetDir, OPENCLAW_RUNTIME_READY_MARKER)),
`${JSON.stringify({
version,
manifestHash,
source: 'after-pack',
createdAt: new Date().toISOString(),
}, null, 2)}\n`,
'utf8',
);
validateRuntimeDepsManifest(targetDir);
console.log(`[after-pack] ✅ Staged OpenClaw runtime at ${targetDir}`);
return targetDir;
}
// ── Main hook ────────────────────────────────────────────────────────────────
exports.default = async function afterPack(context) {
@@ -705,7 +562,6 @@ exports.default = async function afterPack(context) {
console.log(`[after-pack] Copying ${depCount} openclaw dependencies to ${dest} ...`);
cpSync(src, dest, { recursive: true });
console.log('[after-pack] ✅ openclaw node_modules copied.');
validateRuntimeDepsManifest(openclawRoot);
// Patch broken modules whose CJS transpiled output sets module.exports = undefined,
// causing TypeError in Node.js 22+ ESM interop.
@@ -726,22 +582,29 @@ exports.default = async function afterPack(context) {
mkdirSync(pluginsDestRoot, { recursive: true });
for (const { npmName, pluginId } of BUNDLED_PLUGINS) {
const pluginDestDir = join(pluginsDestRoot, pluginId);
preparePackagedPluginMirror(nodeModulesRoot, npmName, pluginId, pluginDestDir, platform, arch);
const manifestPath = join(pluginDestDir, 'openclaw.plugin.json');
if (!existsSync(normWin(manifestPath))) {
throw new Error(`[after-pack] Missing packaged plugin mirror: ${pluginId} (${npmName})`);
}
const missingRuntimeDeps = missingPluginRuntimeDeps(pluginDestDir);
if (missingRuntimeDeps.length > 0) {
throw new Error(`[after-pack] Missing packaged plugin runtime deps for ${pluginId}: ${missingRuntimeDeps.join(', ')}`);
console.log(`[after-pack] Bundling plugin ${npmName} -> ${pluginDestDir}`);
const ok = bundlePlugin(nodeModulesRoot, npmName, pluginDestDir);
if (ok) {
const pluginNM = join(pluginDestDir, 'node_modules');
cleanupUnnecessaryFiles(pluginDestDir);
if (existsSync(pluginNM)) {
cleanupKoffi(pluginNM, platform, arch);
cleanupNativePlatformPackages(pluginNM, platform, arch);
}
// Fix hardcoded plugin ID mismatches in compiled JS
patchPluginIds(pluginDestDir, pluginId);
}
}
// 1.2 Legacy safety net for build/openclaw bundles that still contain nested
// built-in extension node_modules. The current bundle-openclaw.mjs skips
// these nested directories and merges their packages into the top-level
// OpenClaw node_modules instead, which is where shared dist chunks resolve
// bare imports from at runtime.
// 1.2 Copy built-in extension node_modules that electron-builder skipped.
// OpenClaw 3.31+ ships built-in extensions (discord, qqbot, etc.) under
// dist/extensions/<ext>/node_modules/. These are skipped by extraResources
// because .gitignore contains "node_modules/".
//
// Extension code is loaded via shared chunks in dist/ (e.g. outbound-*.js)
// which resolve modules from the top-level openclaw/node_modules/, NOT from
// the extension's own node_modules/. So we must merge extension deps into
// the top-level node_modules/ as well.
const buildExtDir = join(__dirname, '..', 'build', 'openclaw', 'dist', 'extensions');
const packExtDir = join(openclawRoot, 'dist', 'extensions');
if (existsSync(buildExtDir)) {
@@ -807,15 +670,6 @@ exports.default = async function afterPack(context) {
console.log(`[after-pack] ✅ Removed ${nativeRemoved} non-target native platform packages.`);
}
// 4.1 Move the finalized OpenClaw tree into the shape OpenClaw already
// recognizes as an external runtime root:
// resources/openclaw-runtime/openclaw-<version>-<hash>
//
// At runtime ClawX sets OPENCLAW_PLUGIN_STAGE_DIR to resources/openclaw-runtime
// and launches from the staged openclaw-* directory. This avoids a first-run
// copy while still preventing OpenClaw from running npm install.
stageOpenClawRuntimeForPackagedApp(resourcesDir, openclawRoot);
// 5. Patch lru-cache in app.asar.unpacked
//
// Production dependencies (electron-updater → semver → lru-cache@6,

View File

@@ -93,22 +93,13 @@ function bundleOnePlugin({ npmName, pluginId }) {
echo`📦 Bundling plugin ${npmName} -> ${outputDir}`;
function shouldCopyNodePackageEntry(src) {
const base = path.basename(src);
return base !== '.vscode' && base !== '.idea';
}
if (fs.existsSync(outputDir)) {
fs.rmSync(outputDir, { recursive: true, force: true });
}
fs.mkdirSync(outputDir, { recursive: true });
// 1) Copy plugin package itself
fs.cpSync(realPluginPath, outputDir, {
recursive: true,
dereference: true,
filter: shouldCopyNodePackageEntry,
});
fs.cpSync(realPluginPath, outputDir, { recursive: true, dereference: true });
// 2) Collect transitive deps from pnpm virtual store
const collected = new Map();
@@ -169,11 +160,7 @@ function bundleOnePlugin({ npmName, pluginId }) {
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,
filter: shouldCopyNodePackageEntry,
});
fs.cpSync(normWin(realPath), normWin(dest), { recursive: true, dereference: true });
copiedCount++;
} catch (err) {
echo` ⚠️ Skipped ${pkgName}: ${err.message}`;

View File

@@ -17,13 +17,10 @@
*/
import 'zx/globals';
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');
const RUNTIME_DEPS_MANIFEST = 'clawx-runtime-deps.json';
// On Windows, pnpm virtual store paths can exceed MAX_PATH (260 chars).
function normWin(p) {
@@ -43,27 +40,6 @@ if (!fs.existsSync(openclawLink)) {
const openclawReal = fs.realpathSync(openclawLink);
echo` openclaw resolved: ${openclawReal}`;
const extensionsDir = path.join(openclawReal, 'dist', 'extensions');
function shouldCopyOpenClawPackageEntry(src) {
const rel = path.relative(openclawReal, src);
if (!rel || rel.startsWith('..')) return true;
const parts = rel.split(path.sep);
if (parts[0] === 'dist' && parts[1] === 'extensions') {
const nodeModulesIndex = parts.indexOf('node_modules');
if (nodeModulesIndex >= 0) {
return false;
}
}
for (let i = 0; i < parts.length - 1; i++) {
if (parts[i] === 'node_modules' && parts[i + 1] === '.bin') {
return false;
}
}
return true;
}
// 2. Clean and create output directory
if (fs.existsSync(OUTPUT)) {
@@ -73,11 +49,7 @@ 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,
filter: shouldCopyOpenClawPackageEntry,
});
fs.cpSync(openclawReal, OUTPUT, { recursive: true, dereference: true });
// 4. Recursively collect ALL transitive dependencies via pnpm virtual store BFS
//
@@ -216,110 +188,9 @@ echo` Skipped ${skippedDevCount} dev-only package references`;
// 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)
'@larksuiteoapi/node-sdk', // Fallback for Feishu plugin setup/doctor module resolution
'qrcode-terminal', // QR rendering used by WhatsApp/WeChat login helpers
];
const BUNDLED_EXTENSION_RUNTIME_DEP_PLUGIN_IDS = [
'acpx',
'bonjour',
'browser',
'discord',
'memory-core',
'qqbot',
'telegram',
];
function readJsonFile(filePath) {
try {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch {
return null;
}
}
function collectBundledExtensionRuntimeDeps(extensionsRoot) {
const depsByPlugin = {};
for (const pluginId of BUNDLED_EXTENSION_RUNTIME_DEP_PLUGIN_IDS) {
const packageJson = readJsonFile(path.join(extensionsRoot, pluginId, 'package.json'));
if (!packageJson || typeof packageJson !== 'object') {
echo`❌ Bundled extension package.json not found for ${pluginId}`;
process.exit(1);
}
const depsByName = {};
for (const deps of [packageJson.dependencies, packageJson.optionalDependencies]) {
if (!deps || typeof deps !== 'object' || Array.isArray(deps)) continue;
for (const [name, version] of Object.entries(deps)) {
depsByName[name] = String(version);
}
}
depsByPlugin[pluginId] = Object.entries(depsByName)
.map(([name, version]) => ({ name, version }))
.sort((left, right) => left.name.localeCompare(right.name));
}
return depsByPlugin;
}
function flattenRuntimeDeps(depsByPlugin) {
return [...new Set(Object.values(depsByPlugin).flat().map(dep => dep.name))]
.sort((a, b) => a.localeCompare(b));
}
function readBundledPackageVersion(nodeModulesDir, pkgName) {
const packageJson = readJsonFile(path.join(nodeModulesDir, ...pkgName.split('/'), 'package.json'));
return typeof packageJson?.version === 'string' ? packageJson.version : null;
}
function writeRuntimeDepsManifest(outputDir, depsByPlugin) {
const nodeModulesDir = path.join(outputDir, 'node_modules');
const manifest = {
generatedBy: 'scripts/bundle-openclaw.mjs',
packageRoot: '.',
nodeModulesRoot: 'node_modules',
plugins: {},
};
const missing = [];
for (const [pluginId, deps] of Object.entries(depsByPlugin)) {
manifest.plugins[pluginId] = deps.map((dep) => {
const installedVersion = readBundledPackageVersion(nodeModulesDir, dep.name);
const present = Boolean(installedVersion);
if (!present) {
missing.push(`${pluginId}:${dep.name}@${dep.version}`);
}
return {
name: dep.name,
version: dep.version,
installedVersion,
present,
};
});
}
if (missing.length > 0) {
echo`❌ Missing bundled extension runtime deps: ${missing.join(', ')}`;
process.exit(1);
}
fs.writeFileSync(
path.join(outputDir, RUNTIME_DEPS_MANIFEST),
`${JSON.stringify(manifest, null, 2)}\n`,
'utf8',
);
echo` Wrote ${RUNTIME_DEPS_MANIFEST} for ${Object.keys(manifest.plugins).length} bundled extension(s)`;
}
const bundledExtensionRuntimeDeps = collectBundledExtensionRuntimeDeps(extensionsDir);
const bundledExtensionRuntimePackages = flattenRuntimeDeps(bundledExtensionRuntimeDeps);
for (const pkgName of bundledExtensionRuntimePackages) {
if (!EXTRA_BUNDLED_PACKAGES.includes(pkgName)) {
EXTRA_BUNDLED_PACKAGES.push(pkgName);
}
}
const preferredBundledPackages = new Set(EXTRA_BUNDLED_PACKAGES);
let extraCount = 0;
const preferredBundledPackageRealPaths = new Set();
for (const pkgName of EXTRA_BUNDLED_PACKAGES) {
const pkgLink = path.join(NODE_MODULES, ...pkgName.split('/'));
if (!fs.existsSync(pkgLink)) {
@@ -329,7 +200,6 @@ for (const pkgName of EXTRA_BUNDLED_PACKAGES) {
let pkgReal;
try { pkgReal = fs.realpathSync(pkgLink); } catch { continue; }
preferredBundledPackageRealPaths.add(pkgReal);
if (!collected.has(pkgReal)) {
collected.set(pkgReal, pkgName);
@@ -377,22 +247,8 @@ fs.mkdirSync(outputNodeModules, { recursive: true });
const copiedNames = new Set(); // Track package names already copied
let copiedCount = 0;
let skippedDupes = 0;
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 0;
return leftPreferred ? -1 : 1;
});
function shouldCopyNodePackageEntry(src) {
const base = path.basename(src);
return base !== '.vscode' && base !== '.idea';
}
for (const [realPath, pkgName] of collectedEntries) {
for (const [realPath, pkgName] of collected) {
if (copiedNames.has(pkgName)) {
skippedDupes++;
continue; // Keep the first version (closer to openclaw in dep tree)
@@ -403,11 +259,7 @@ for (const [realPath, pkgName] of collectedEntries) {
try {
fs.mkdirSync(normWin(path.dirname(dest)), { recursive: true });
fs.cpSync(normWin(realPath), normWin(dest), {
recursive: true,
dereference: true,
filter: shouldCopyNodePackageEntry,
});
fs.cpSync(normWin(realPath), normWin(dest), { recursive: true, dereference: true });
copiedCount++;
} catch (err) {
echo` ⚠️ Skipped ${pkgName}: ${err.message}`;
@@ -427,6 +279,7 @@ for (const [realPath, pkgName] of collectedEntries) {
// Fix: copy extension deps into the top-level node_modules/ so they are
// 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');
let mergedExtCount = 0;
if (fs.existsSync(extensionsDir)) {
for (const extEntry of fs.readdirSync(extensionsDir, { withFileTypes: true })) {
@@ -472,8 +325,6 @@ if (mergedExtCount > 0) {
echo` Merged ${mergedExtCount} extension packages into top-level node_modules`;
}
writeRuntimeDepsManifest(OUTPUT, bundledExtensionRuntimeDeps);
// 6. Clean up the bundle to reduce package size
//
// This removes platform-agnostic waste: dev artifacts, docs, source maps,
@@ -619,7 +470,7 @@ function cleanupBundle(outputDir) {
'node_modules/koffi/src',
'node_modules/koffi/vendor',
'node_modules/koffi/doc',
'dist/extensions/feishu', // Removed in favor of official @larksuite/openclaw-lark plugin
'extensions/feishu', // Removed in favor of official @larksuite/openclaw-lark plugin
];
for (const rel of LARGE_REMOVALS) {
if (rmSafe(path.join(outputDir, rel))) removedCount++;
@@ -861,20 +712,12 @@ function patchBundledRuntime(outputDir) {
for (const patch of replacePatches) {
const target = patch.target();
if (!target || !fs.existsSync(target)) {
if (patch.required) {
echo`❌ Required patch failed for ${patch.label}: target file not found`;
process.exit(1);
}
echo` ⚠️ Skipped patch for ${patch.label}: target file not found`;
continue;
}
const current = fs.readFileSync(target, 'utf8');
if (!current.includes(patch.search)) {
if (patch.required) {
echo`❌ Required patch failed for ${patch.label}: expected source snippet not found`;
process.exit(1);
}
echo` ⚠️ Skipped patch for ${patch.label}: expected source snippet not found`;
continue;
}

View File

@@ -139,81 +139,6 @@ describe('parseDoctorValidationOutput', () => {
});
});
describe('OpenClaw 4.24 bundled channel config defaults', () => {
beforeEach(async () => {
vi.resetAllMocks();
vi.resetModules();
await rm(testHome, { recursive: true, force: true });
await rm(testUserData, { recursive: true, force: true });
});
it('adds required Telegram access policies when saving token config', async () => {
const { saveChannelConfig } = await import('@electron/utils/channel-config');
await saveChannelConfig('telegram', {
botToken: 'telegram-token',
allowedUsers: '12345',
}, 'default');
const config = await readOpenClawJson();
const channels = config.channels as Record<string, {
dmPolicy?: string;
groupPolicy?: string;
accounts?: Record<string, { dmPolicy?: string; groupPolicy?: string; allowFrom?: string[] }>;
}>;
expect(channels.telegram.dmPolicy).toBe('allowlist');
expect(channels.telegram.groupPolicy).toBe('allowlist');
expect(channels.telegram.accounts?.default).toMatchObject({
allowFrom: ['12345'],
dmPolicy: 'allowlist',
groupPolicy: 'allowlist',
});
});
it('uses open Telegram policies for wildcard access', async () => {
const { saveChannelConfig } = await import('@electron/utils/channel-config');
await saveChannelConfig('telegram', {
botToken: 'telegram-token',
allowedUsers: '*',
}, 'default');
const config = await readOpenClawJson();
const channels = config.channels as Record<string, {
accounts?: Record<string, { dmPolicy?: string; groupPolicy?: string }>;
}>;
expect(channels.telegram.accounts?.default).toMatchObject({
dmPolicy: 'open',
groupPolicy: 'open',
});
});
it('adds Discord DM policy alongside required group policy', async () => {
const { saveChannelConfig } = await import('@electron/utils/channel-config');
await saveChannelConfig('discord', {
token: 'discord-token',
guildId: 'guild-1',
}, 'default');
const config = await readOpenClawJson();
const channels = config.channels as Record<string, {
dmPolicy?: string;
groupPolicy?: string;
accounts?: Record<string, { dmPolicy?: string; groupPolicy?: string }>;
}>;
expect(channels.discord.dmPolicy).toBe('disabled');
expect(channels.discord.groupPolicy).toBe('allowlist');
expect(channels.discord.accounts?.default).toMatchObject({
dmPolicy: 'disabled',
groupPolicy: 'allowlist',
});
});
});
describe('WeCom plugin configuration', () => {
beforeEach(async () => {
vi.resetAllMocks();

View File

@@ -709,39 +709,6 @@ describe('syncProviderConfigToOpenClaw', () => {
});
});
describe('batchSyncConfigFields', () => {
beforeEach(async () => {
vi.resetModules();
vi.restoreAllMocks();
await rm(testHome, { recursive: true, force: true });
await rm(testUserData, { recursive: true, force: true });
});
it('allowlists packaged file origins for the control UI websocket', async () => {
await writeOpenClawJson({
gateway: {
controlUi: {
allowedOrigins: ['http://localhost:18789', 'file://'],
},
},
});
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const { batchSyncConfigFields } = await import('@electron/utils/openclaw-auth');
await batchSyncConfigFields('test-token');
const result = await readOpenClawJson();
const gateway = result.gateway as Record<string, unknown>;
const controlUi = gateway.controlUi as Record<string, unknown>;
expect(controlUi.allowedOrigins).toEqual(['http://localhost:18789', 'file://', 'null']);
expect(gateway.auth).toEqual({ mode: 'token', token: 'test-token' });
logSpy.mockRestore();
});
});
describe('auth-backed provider discovery', () => {
beforeEach(async () => {
vi.resetModules();

View File

@@ -43,9 +43,6 @@ vi.mock('electron', () => ({
vi.mock('@electron/utils/paths', () => ({
getOpenClawDir: () => '/tmp/openclaw',
getOpenClawEntryPath: () => '/tmp/openclaw/openclaw-entry.js',
getOpenClawRuntimeDir: () => '/tmp/openclaw-runtime/openclaw-2026.4.26-test',
getOpenClawRuntimeEntryPath: () => '/tmp/openclaw-runtime/openclaw-2026.4.26-test/openclaw-entry.js',
getOpenClawPluginStageDir: () => '/tmp/openclaw-runtime',
}));
vi.mock('@electron/utils/uv-env', () => ({