feat: prepare Zhinian desktop client for pilot release

This commit is contained in:
inman
2026-04-29 10:23:20 +08:00
parent f9361e686a
commit 47b83b79fc
149 changed files with 15341 additions and 3590 deletions

View File

@@ -3,13 +3,25 @@
* Cross-platform path resolution helpers
*/
import { createRequire } from 'node:module';
import { join } from 'path';
import { dirname, isAbsolute, join, normalize } from 'path';
import { homedir } from 'os';
import { existsSync, mkdirSync, readFileSync, realpathSync } from 'fs';
import { cpSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync } from 'fs';
const require = createRequire(import.meta.url);
type ElectronAppLike = Pick<typeof import('electron').app, 'isPackaged' | 'getPath' | 'getAppPath'>;
type OpenClawRuntimeSource = 'dev' | 'external' | 'managed' | 'bundled' | 'missing';
type OpenClawRuntimeResolution = {
dir: string;
source: OpenClawRuntimeSource;
version?: string;
bundledDir?: string;
managedDir?: string;
installedFromBundled?: boolean;
};
let cachedOpenClawRuntime: OpenClawRuntimeResolution | null = null;
export {
quoteForCmd,
@@ -91,6 +103,218 @@ export function ensureDir(dir: string): void {
}
}
function fsPath(filePath: string): string {
if (process.platform !== 'win32') return filePath;
if (!filePath) return filePath;
if (filePath.startsWith('\\\\?\\')) return filePath;
const windowsPath = filePath.replace(/\//g, '\\');
if (!isAbsolute(windowsPath)) return windowsPath;
if (windowsPath.startsWith('\\\\')) {
return `\\\\?\\UNC\\${windowsPath.slice(2)}`;
}
return `\\\\?\\${windowsPath}`;
}
function normalizeCandidatePath(dir: string): string {
return normalize(expandPath(dir.trim()));
}
function readOpenClawVersion(dir: string): string | undefined {
try {
const pkgPath = join(dir, 'package.json');
if (existsSync(fsPath(pkgPath))) {
const pkg = JSON.parse(readFileSync(fsPath(pkgPath), 'utf-8')) as { version?: string };
return pkg.version;
}
} catch {
// Ignore version read errors.
}
return undefined;
}
function isValidOpenClawPackageDir(dir: string): boolean {
return Boolean(dir)
&& existsSync(fsPath(dir))
&& existsSync(fsPath(join(dir, 'package.json')))
&& existsSync(fsPath(join(dir, 'openclaw.mjs')));
}
function samePath(left: string, right: string): boolean {
try {
return realpathSync(fsPath(left)) === realpathSync(fsPath(right));
} catch {
return normalize(left) === normalize(right);
}
}
function logOpenClawRuntime(message: string, extra?: unknown): void {
try {
const { logger } = require('./logger') as typeof import('./logger');
if (extra === undefined) {
logger.info(message);
} else {
logger.info(message, extra);
}
} catch {
// Logger may be unavailable in unit tests or non-Electron contexts.
}
}
function getBundledOpenClawDir(): string {
if (getElectronApp().isPackaged) {
return join(process.resourcesPath, 'openclaw');
}
return join(__dirname, '../../node_modules/openclaw');
}
function getManagedOpenClawDir(): string {
return join(getOpenClawConfigDir(), 'runtime', 'openclaw');
}
function getEnvOpenClawCandidates(): string[] {
return [
process.env.YINIAN_OPENCLAW_DIR,
process.env.CLAWX_OPENCLAW_DIR,
process.env.OPENCLAW_DIR,
].filter((value): value is string => Boolean(value?.trim()));
}
function getStandardOpenClawCandidates(): string[] {
const home = homedir();
const candidates = [
join(home, '.openclaw', 'openclaw'),
join(home, '.openclaw', 'node_modules', 'openclaw'),
join(home, '.npm-global', 'lib', 'node_modules', 'openclaw'),
join(home, '.local', 'lib', 'node_modules', 'openclaw'),
];
if (process.platform === 'darwin') {
candidates.push(
'/usr/local/lib/node_modules/openclaw',
'/opt/homebrew/lib/node_modules/openclaw',
);
} else if (process.platform === 'linux') {
candidates.push(
'/usr/local/lib/node_modules/openclaw',
'/usr/lib/node_modules/openclaw',
);
} else if (process.platform === 'win32') {
const appData = process.env.APPDATA;
const programFiles = process.env.ProgramFiles;
const programFilesX86 = process.env['ProgramFiles(x86)'];
if (appData) candidates.push(join(appData, 'npm', 'node_modules', 'openclaw'));
if (programFiles) candidates.push(join(programFiles, 'nodejs', 'node_modules', 'openclaw'));
if (programFilesX86) candidates.push(join(programFilesX86, 'nodejs', 'node_modules', 'openclaw'));
}
return candidates;
}
function findExternalOpenClawDir(excludedDirs: string[]): string | null {
const seen = new Set<string>();
const candidates = [...getEnvOpenClawCandidates(), ...getStandardOpenClawCandidates()];
for (const rawCandidate of candidates) {
const candidate = normalizeCandidatePath(rawCandidate);
if (seen.has(candidate)) continue;
seen.add(candidate);
if (excludedDirs.some((excluded) => samePath(candidate, excluded))) continue;
if (isValidOpenClawPackageDir(candidate)) return candidate;
}
return null;
}
function installBundledOpenClawToManagedRuntime(bundledDir: string, managedDir: string): boolean {
if (!isValidOpenClawPackageDir(bundledDir)) return false;
const bundledVersion = readOpenClawVersion(bundledDir);
const managedVersion = isValidOpenClawPackageDir(managedDir)
? readOpenClawVersion(managedDir)
: undefined;
if (managedVersion && bundledVersion && managedVersion === bundledVersion) {
return false;
}
const tempDir = `${managedDir}.tmp-${Date.now()}`;
rmSync(fsPath(tempDir), { recursive: true, force: true });
mkdirSync(fsPath(dirname(tempDir)), { recursive: true });
cpSync(fsPath(bundledDir), fsPath(tempDir), { recursive: true, dereference: true });
rmSync(fsPath(managedDir), { recursive: true, force: true });
cpSync(fsPath(tempDir), fsPath(managedDir), { recursive: true, dereference: true });
rmSync(fsPath(tempDir), { recursive: true, force: true });
return true;
}
function resolveOpenClawRuntime(): OpenClawRuntimeResolution {
if (cachedOpenClawRuntime) return cachedOpenClawRuntime;
const app = getElectronApp();
const bundledDir = getBundledOpenClawDir();
const managedDir = getManagedOpenClawDir();
if (!app.isPackaged) {
cachedOpenClawRuntime = {
dir: bundledDir,
source: isValidOpenClawPackageDir(bundledDir) ? 'dev' : 'missing',
version: readOpenClawVersion(bundledDir),
bundledDir,
managedDir,
};
return cachedOpenClawRuntime;
}
const externalDir = findExternalOpenClawDir([bundledDir, managedDir]);
if (externalDir) {
cachedOpenClawRuntime = {
dir: externalDir,
source: 'external',
version: readOpenClawVersion(externalDir),
bundledDir,
managedDir,
};
logOpenClawRuntime('[openclaw-runtime] Using existing OpenClaw installation', cachedOpenClawRuntime);
return cachedOpenClawRuntime;
}
let installedFromBundled = false;
if (isValidOpenClawPackageDir(bundledDir)) {
try {
installedFromBundled = installBundledOpenClawToManagedRuntime(bundledDir, managedDir);
} catch (error) {
logOpenClawRuntime('[openclaw-runtime] Failed to install bundled OpenClaw runtime, falling back to bundled resources', error);
}
}
if (isValidOpenClawPackageDir(managedDir)) {
cachedOpenClawRuntime = {
dir: managedDir,
source: 'managed',
version: readOpenClawVersion(managedDir),
bundledDir,
managedDir,
installedFromBundled,
};
logOpenClawRuntime(
installedFromBundled
? '[openclaw-runtime] Installed bundled OpenClaw runtime'
: '[openclaw-runtime] Using managed OpenClaw runtime',
cachedOpenClawRuntime,
);
return cachedOpenClawRuntime;
}
cachedOpenClawRuntime = {
dir: bundledDir,
source: isValidOpenClawPackageDir(bundledDir) ? 'bundled' : 'missing',
version: readOpenClawVersion(bundledDir),
bundledDir,
managedDir,
};
return cachedOpenClawRuntime;
}
/**
* Get resources directory (for bundled assets)
*/
@@ -109,16 +333,17 @@ export function getPreloadPath(): string {
}
/**
* Get OpenClaw package directory
* - Production (packaged): from resources/openclaw (copied by electron-builder extraResources)
* - Development: from node_modules/openclaw
* Get OpenClaw package directory.
*
* Runtime resolution policy:
* 1. Development uses workspace node_modules/openclaw.
* 2. Packaged builds first adapt an existing user/system OpenClaw package.
* 3. If none exists, copy the bundled OpenClaw package into
* ~/.openclaw/runtime/openclaw and run from there.
* 4. If the managed install fails, fall back to packaged resources/openclaw.
*/
export function getOpenClawDir(): string {
if (getElectronApp().isPackaged) {
return join(process.resourcesPath, 'openclaw');
}
// Development: use node_modules/openclaw
return join(__dirname, '../../node_modules/openclaw');
return resolveOpenClawRuntime().dir;
}
/**
@@ -188,29 +413,26 @@ export interface OpenClawStatus {
entryPath: string;
dir: string;
version?: string;
source?: OpenClawRuntimeSource;
bundledDir?: string;
managedDir?: string;
installedFromBundled?: boolean;
}
export function getOpenClawStatus(): OpenClawStatus {
const dir = getOpenClawDir();
let version: string | undefined;
// Try to read version from package.json
try {
const pkgPath = join(dir, 'package.json');
if (existsSync(pkgPath)) {
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
version = pkg.version;
}
} catch {
// Ignore version read errors
}
const runtime = resolveOpenClawRuntime();
const dir = runtime.dir;
const status: OpenClawStatus = {
packageExists: isOpenClawPresent(),
isBuilt: isOpenClawBuilt(),
entryPath: getOpenClawEntryPath(),
dir,
version,
version: runtime.version,
source: runtime.source,
bundledDir: runtime.bundledDir,
managedDir: runtime.managedDir,
installedFromBundled: runtime.installedFromBundled,
};
try {