fix(openclaw): revert update openclaw version (#944)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
897
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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%" (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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', () => ({
|
||||
|
||||
Reference in New Issue
Block a user