feat: prepare Zhinian desktop client for pilot release
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
@@ -63,6 +63,7 @@ import {
|
||||
import { validateApiKeyWithProvider } from '../services/providers/provider-validation';
|
||||
import { appUpdater } from './updater';
|
||||
import { registerHostApiProxyHandlers } from './ipc/host-api-proxy';
|
||||
import { registerYinianHandlers } from './ipc/yinian';
|
||||
import {
|
||||
isLaunchAtStartupKey,
|
||||
isProxyKey,
|
||||
@@ -85,6 +86,9 @@ export function registerIpcHandlers(
|
||||
// Host API proxy handlers
|
||||
registerHostApiProxyHandlers();
|
||||
|
||||
// YINIAN hotel control-plane handlers
|
||||
registerYinianHandlers();
|
||||
|
||||
// Gateway handlers
|
||||
registerGatewayHandlers(gatewayManager, mainWindow);
|
||||
|
||||
|
||||
60
electron/main/ipc/yinian.ts
Normal file
60
electron/main/ipc/yinian.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import { getYinianControlPlane } from '../../yinian/control-plane';
|
||||
import { getYinianStorage } from '../../yinian/storage';
|
||||
import type { YinianLoginWithPasswordInput, YinianLoginWithSmsInput, YinianSavedCredentials } from '../../../shared/yinian';
|
||||
|
||||
export function registerYinianHandlers(): void {
|
||||
ipcMain.handle('yinian:server:status', async () => getYinianControlPlane().getServerStatus());
|
||||
|
||||
ipcMain.handle('yinian:auth:createImageCaptcha', async (_, randomStr?: string) => {
|
||||
return getYinianControlPlane().createImageCaptcha(randomStr);
|
||||
});
|
||||
|
||||
ipcMain.handle('yinian:auth:restoreSession', async () => getYinianControlPlane().restoreSession());
|
||||
|
||||
ipcMain.handle('yinian:auth:getSessionState', async () => getYinianControlPlane().getSessionState());
|
||||
|
||||
ipcMain.handle('yinian:auth:loginWithSms', async (_, input: YinianLoginWithSmsInput) => {
|
||||
return getYinianControlPlane().loginWithSms(input);
|
||||
});
|
||||
|
||||
ipcMain.handle('yinian:auth:loginWithPassword', async (_, input: YinianLoginWithPasswordInput) => {
|
||||
return getYinianControlPlane().loginWithPassword(input);
|
||||
});
|
||||
|
||||
ipcMain.handle('yinian:auth:logout', async () => getYinianControlPlane().logout());
|
||||
|
||||
ipcMain.handle('yinian:auth:getSavedCredentials', async () => {
|
||||
return getYinianStorage().getSavedCredentials();
|
||||
});
|
||||
|
||||
ipcMain.handle('yinian:auth:saveCredentials', async (_, input: Pick<YinianSavedCredentials, 'account' | 'password' | 'rememberPassword'>) => {
|
||||
const account = typeof input.account === 'string' ? input.account.trim() : '';
|
||||
if (!account) {
|
||||
await getYinianStorage().clearSavedCredentials();
|
||||
return undefined;
|
||||
}
|
||||
const credentials: YinianSavedCredentials = {
|
||||
account,
|
||||
password: input.rememberPassword ? input.password ?? '' : undefined,
|
||||
rememberPassword: Boolean(input.rememberPassword),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
await getYinianStorage().setSavedCredentials(credentials);
|
||||
return credentials;
|
||||
});
|
||||
|
||||
ipcMain.handle('yinian:auth:clearSavedCredentials', async () => {
|
||||
await getYinianStorage().clearSavedCredentials();
|
||||
});
|
||||
|
||||
ipcMain.handle('yinian:config:get', async () => getYinianControlPlane().getConfigSnapshot());
|
||||
|
||||
ipcMain.handle('yinian:hotel:switch', async (_, hotelId: string) => getYinianControlPlane().switchHotel(hotelId));
|
||||
|
||||
ipcMain.handle('yinian:skills:sync', async () => getYinianControlPlane().syncSkills());
|
||||
|
||||
ipcMain.handle('yinian:skills:listLocal', async () => getYinianControlPlane().listLocalSkills());
|
||||
|
||||
ipcMain.handle('yinian:skills:getRegistry', async (_, hotelId?: string) => getYinianControlPlane().getSkillRegistry(hotelId));
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { dirname, join } from 'node:path';
|
||||
import { logger } from '../utils/logger';
|
||||
import { getSetting } from '../utils/store';
|
||||
|
||||
const LINUX_AUTOSTART_FILE = join('.config', 'autostart', 'clawx.desktop');
|
||||
const LINUX_AUTOSTART_FILE = join('.config', 'autostart', 'zhinian-assistant.desktop');
|
||||
|
||||
function quoteDesktopArg(value: string): string {
|
||||
if (!value) return '""';
|
||||
@@ -30,8 +30,8 @@ function getLinuxDesktopEntry(): string {
|
||||
'[Desktop Entry]',
|
||||
'Type=Application',
|
||||
'Version=1.0',
|
||||
'Name=ClawX',
|
||||
'Comment=ClawX - AI Assistant',
|
||||
'Name=智念助手',
|
||||
'Comment=B 端 AI Agent 桌面助手',
|
||||
`Exec=${getLinuxExecCommand()}`,
|
||||
'Terminal=false',
|
||||
'Categories=Utility;',
|
||||
|
||||
@@ -57,7 +57,7 @@ export function createTray(mainWindow: BrowserWindow): Tray {
|
||||
tray = new Tray(icon);
|
||||
|
||||
// Set tooltip
|
||||
tray.setToolTip('ClawX - AI Assistant');
|
||||
tray.setToolTip('智念助手 - B 端 AI Agent');
|
||||
|
||||
const showWindow = () => {
|
||||
if (mainWindow.isDestroyed()) return;
|
||||
@@ -68,7 +68,7 @@ export function createTray(mainWindow: BrowserWindow): Tray {
|
||||
// Create context menu
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Show ClawX',
|
||||
label: '显示智念助手',
|
||||
click: showWindow,
|
||||
},
|
||||
{
|
||||
@@ -91,7 +91,7 @@ export function createTray(mainWindow: BrowserWindow): Tray {
|
||||
label: 'Quick Actions',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Open Chat',
|
||||
label: '打开对话',
|
||||
click: () => {
|
||||
if (mainWindow.isDestroyed()) return;
|
||||
mainWindow.show();
|
||||
@@ -99,7 +99,7 @@ export function createTray(mainWindow: BrowserWindow): Tray {
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Open Settings',
|
||||
label: '打开设置',
|
||||
click: () => {
|
||||
if (mainWindow.isDestroyed()) return;
|
||||
mainWindow.show();
|
||||
@@ -112,7 +112,7 @@ export function createTray(mainWindow: BrowserWindow): Tray {
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
label: 'Check for Updates...',
|
||||
label: '检查更新...',
|
||||
click: () => {
|
||||
if (mainWindow.isDestroyed()) return;
|
||||
mainWindow.webContents.send('update:check');
|
||||
@@ -122,7 +122,7 @@ export function createTray(mainWindow: BrowserWindow): Tray {
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
label: 'Quit ClawX',
|
||||
label: '退出智念助手',
|
||||
click: () => {
|
||||
app.quit();
|
||||
},
|
||||
@@ -157,7 +157,7 @@ export function createTray(mainWindow: BrowserWindow): Tray {
|
||||
*/
|
||||
export function updateTrayStatus(status: string): void {
|
||||
if (tray) {
|
||||
tray.setToolTip(`ClawX - ${status}`);
|
||||
tray.setToolTip(`智念助手 - ${status}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user