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

@@ -48,7 +48,8 @@ import { browserOAuthManager } from '../utils/browser-oauth';
import { whatsAppLoginManager } from '../utils/whatsapp-login';
import { syncAllProviderAuthToRuntime } from '../services/providers/provider-runtime-sync';
const WINDOWS_APP_USER_MODEL_ID = 'app.clawx.desktop';
const WINDOWS_APP_USER_MODEL_ID = 'app.zhinian.assistant';
const PRODUCT_NAME = '智念助手';
const isE2EMode = process.env.CLAWX_E2E === '1';
const requestedUserDataDir = process.env.CLAWX_USER_DATA_DIR?.trim();
@@ -56,6 +57,8 @@ if (isE2EMode && requestedUserDataDir) {
app.setPath('userData', requestedUserDataDir);
}
app.setName(PRODUCT_NAME);
// Disable GPU hardware acceleration globally for maximum stability across
// all GPU configurations (no GPU, integrated, discrete).
//
@@ -77,7 +80,7 @@ app.disableHardwareAcceleration();
// on X11 it supplements the StartupWMClass matching.
// Must be called before app.whenReady() / before any window is created.
if (process.platform === 'linux') {
app.setDesktopName('clawx.desktop');
app.setDesktopName('zhinian-assistant.desktop');
}
// Prevent multiple instances of the app from running simultaneously.
@@ -139,21 +142,29 @@ function getIconsDir(): string {
return join(__dirname, '../../resources/icons');
}
function getAppIconPath(): string {
const iconsDir = getIconsDir();
return process.platform === 'win32'
? join(iconsDir, 'icon.ico')
: join(iconsDir, 'icon.png');
}
/**
* Get the app icon for the current platform
*/
function getAppIcon(): Electron.NativeImage | undefined {
if (process.platform === 'darwin') return undefined; // macOS uses the app bundle icon
const iconsDir = getIconsDir();
const iconPath =
process.platform === 'win32'
? join(iconsDir, 'icon.ico')
: join(iconsDir, 'icon.png');
const icon = nativeImage.createFromPath(iconPath);
const icon = nativeImage.createFromPath(getAppIconPath());
return icon.isEmpty() ? undefined : icon;
}
function applyRuntimeAppIcon(): void {
if (process.platform !== 'darwin' || !app.dock) return;
const icon = nativeImage.createFromPath(join(getIconsDir(), 'icon.png'));
if (!icon.isEmpty()) {
app.dock.setIcon(icon);
}
}
/**
* Create the main application window
*/
@@ -162,6 +173,12 @@ function createWindow(): BrowserWindow {
const isWindows = process.platform === 'win32';
const useCustomTitleBar = isWindows;
const shouldSkipSetupForE2E = process.env.CLAWX_E2E_SKIP_SETUP === '1';
const e2eRendererQuery = isE2EMode
? {
e2e: '1',
...(shouldSkipSetupForE2E ? { e2eSkipSetup: '1' } : {}),
}
: undefined;
const win = new BrowserWindow({
width: 1280,
@@ -180,6 +197,7 @@ function createWindow(): BrowserWindow {
trafficLightPosition: isMac ? { x: 16, y: 16 } : undefined,
frame: isMac || !useCustomTitleBar,
show: false,
title: PRODUCT_NAME,
});
// Handle external links — only allow safe protocols to prevent arbitrary
@@ -201,18 +219,18 @@ function createWindow(): BrowserWindow {
// Load the app
if (process.env.VITE_DEV_SERVER_URL) {
const rendererUrl = new URL(process.env.VITE_DEV_SERVER_URL);
if (shouldSkipSetupForE2E) {
rendererUrl.searchParams.set('e2eSkipSetup', '1');
if (e2eRendererQuery) {
Object.entries(e2eRendererQuery).forEach(([key, value]) => {
rendererUrl.searchParams.set(key, value);
});
}
win.loadURL(rendererUrl.toString());
if (!isE2EMode) {
if (!isE2EMode && process.env.YINIAN_OPEN_DEVTOOLS === '1') {
win.webContents.openDevTools();
}
} else {
win.loadFile(join(__dirname, '../../dist/index.html'), {
query: shouldSkipSetupForE2E
? { e2eSkipSetup: '1' }
: undefined,
query: e2eRendererQuery,
});
}
@@ -281,7 +299,7 @@ function createMainWindow(): BrowserWindow {
async function initialize(): Promise<void> {
// Initialize logger first
logger.init();
logger.info('=== ClawX Application Starting ===');
logger.info('=== 智念助手 Application Starting ===');
logger.debug(
`Runtime: platform=${process.platform}/${process.arch}, electron=${process.versions.electron}, node=${process.versions.node}, packaged=${app.isPackaged}, pid=${process.pid}, ppid=${process.ppid}`
);
@@ -469,9 +487,12 @@ async function initialize(): Promise<void> {
hostEventBus.emit('channel:whatsapp-error', error);
});
// Start Gateway automatically (this seeds missing bootstrap files with full templates)
// YINIAN: do not start Gateway before a user has logged in and a workspace
// context has been loaded. Legacy ClawX auto-start can be restored for
// debugging with CLAWX_LEGACY_AUTOSTART=1.
const gatewayAutoStart = await getSetting('gatewayAutoStart');
if (!isE2EMode && gatewayAutoStart) {
const legacyAutoStart = process.env.CLAWX_LEGACY_AUTOSTART === '1';
if (!isE2EMode && gatewayAutoStart && legacyAutoStart) {
try {
await syncAllProviderAuthToRuntime();
logger.debug('Auto-starting Gateway...');
@@ -483,6 +504,8 @@ async function initialize(): Promise<void> {
}
} else if (isE2EMode) {
logger.info('Gateway auto-start skipped in E2E mode');
} else if (!legacyAutoStart) {
logger.info('Gateway auto-start deferred until YINIAN login');
} else {
logger.info('Gateway auto-start disabled in settings');
}
@@ -543,7 +566,7 @@ if (gotTheLock) {
// When a second instance is launched, focus the existing window instead.
app.on('second-instance', () => {
logger.info('Second ClawX instance detected; redirecting to the existing window');
logger.info('Second 智念助手 instance detected; redirecting to the existing window');
const focusRequest = requestSecondInstanceFocus(
mainWindowFocusState,
@@ -560,6 +583,7 @@ if (gotTheLock) {
// Application lifecycle
app.whenReady().then(() => {
applyRuntimeAppIcon();
void initialize().catch((error) => {
logger.error('Application initialization failed:', error);
});