Files
geo/apps/desktop-client/src/main/bootstrap.ts
T
root 94c6b5d5a5
Desktop Client Build / Resolve Build Metadata (push) Successful in 29s
Frontend CI / Frontend (push) Successful in 4m48s
Desktop Client Build / Build Desktop Client (push) Successful in 26m5s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 42s
fix(desktop): stop forcing login window on business API 401
Business calls (heartbeat, account sync, lease/pull/resume tasks,
preempt monitoring lease) used to short-circuit any 401 into
handleAuthExpired, which tore down the runtime and switched the
window to login. That stole the screen on transient or per-request
auth glitches and conflicted with the renderer-side proactive token
renewal.

Now business 401s fall through to the existing warn/danger activity
log paths and runtime keeps running. The login redirect is owned
solely by the renderer's renewAuthenticatedSession → on refresh /
rotate failure → forceLogoutAfterRenewFailure path. Server-side
revocation via error code 40991 is unaffected.

Removed the now-orphan plumbing: handleAuthExpired,
isApiClientError helper, emitRuntimeAuthExpired /
onRuntimeAuthExpired events, and forceLoginWindowForAuthExpired
listener registration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:55:10 +08:00

1055 lines
30 KiB
TypeScript

import { readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import type {
DesktopAccountInfo,
DesktopClientRotateResponse,
DesktopRuntimeSessionSyncRequest,
} from '@geo/shared-types'
import { isAIPlatformId } from '@geo/shared-types'
import { shell } from 'electron'
import type {
BrowserWindow as ElectronBrowserWindow,
WebContents as ElectronWebContents,
IpcMainInvokeEvent,
} from 'electron/main'
import { BrowserWindow, Menu, app, ipcMain, nativeTheme } from 'electron/main'
import { bindPublishAccount, openPublishAccountConsole } from './account-binder'
import { initAccountHealth } from './account-health'
import {
applyDesktopHealthIndicatorFromSnapshot,
createDesktopHealthIcon,
startDesktopHealthIndicator,
syncDesktopHealthIndicator,
} from './app-issue-indicator'
import {
getDesktopAppSettings,
hasShownBackgroundBalloon,
initDesktopAppSettings,
markBackgroundBalloonShown,
setDesktopAppSetting,
type DesktopAppSettingKey,
} from './app-settings'
import { resolveDesktopClientID, resolveDesktopDeviceInfo } from './device-id'
import {
installObservedGlobalFetch,
registerObservedRequestRendererTarget,
} from './network-observer'
import { getPlaywrightCDPPort, startHiddenPlaywrightReaper } from './playwright-cdp'
import { initProcessMetricsSampler } from './process-metrics'
import { registerRendererDevtoolsProxyTarget } from './renderer-devtools-proxy'
import {
noteRuntimeAccountBound,
refreshRuntimeAccounts,
getRuntimeControllerSnapshot,
releaseRuntimeSession,
requestRuntimeAccountProbe,
syncRuntimeSession,
unbindRuntimeAccount,
} from './runtime-controller'
import { onRuntimeInvalidated } from './runtime-events'
import { createRuntimeAccountSnapshot, createRuntimeSnapshot } from './runtime-snapshot'
import { initScheduler } from './scheduler'
import { initSessionRegistry } from './session-registry'
import { initSingleInstance } from './single-instance'
import {
initTransport,
listDesktopPublishTasks,
retryDesktopPublishTask,
rotateDesktopClient,
} from './transport/api-client'
import { initTray, showTrayBalloon } from './tray'
import { STANDARD_USER_AGENT } from './user-agent'
import { startHotViewReaper } from './view-pool'
app.commandLine.appendSwitch(
'disable-features',
'PostQuantumKyber,EncryptedClientHello,UseDnsHttpsSvcb,UseDnsHttpsSvcbAlpn',
)
app.commandLine.appendSwitch('disable-quic')
app.commandLine.appendSwitch('disable-background-networking')
// CDP is required by the hidden Playwright execution pool in packaged builds.
app.commandLine.appendSwitch('remote-debugging-port', String(getPlaywrightCDPPort()))
app.userAgentFallback = STANDARD_USER_AGENT
// Windows 10+ routes tray.displayBalloon through the Toast Notification API,
// which requires a registered AppUserModelID. electron-builder writes this on
// the Start Menu shortcut at install time, but we set it explicitly so dev
// runs and edge-case installs also fire the balloon reliably.
if (process.platform === 'win32') {
app.setAppUserModelId('com.geo.rankly.desktop')
}
const isDevelopmentRuntime = !app.isPackaged
const desktopDeepLinkScheme = 'shengxintui'
function silencePackagedConsole(): void {
if (isDevelopmentRuntime) {
return
}
const noop = () => undefined
console.log = noop
console.info = noop
console.warn = noop
console.error = noop
console.debug = noop
}
silencePackagedConsole()
if (isDevelopmentRuntime) {
installObservedGlobalFetch()
}
console.info('[desktop-main] boot', {
electron: process.versions.electron,
chrome: process.versions.chrome,
node: process.versions.node,
ua: STANDARD_USER_AGENT,
})
process.on('unhandledRejection', (reason) => {
console.error('[desktop-main] unhandled rejection', reason)
})
process.on('uncaughtException', (error) => {
console.error('[desktop-main] uncaught exception', error)
})
let mainWindow: ElectronBrowserWindow | null = null
let loginWindow: ElectronBrowserWindow | null = null
let settingsWindow: ElectronBrowserWindow | null = null
let mainRendererContents: ElectronWebContents | null = null
let quitReleaseInFlight = false
let mainWindowCreatePromise: Promise<ElectronBrowserWindow> | null = null
let loginWindowCreatePromise: Promise<ElectronBrowserWindow> | null = null
let settingsWindowCreatePromise: Promise<ElectronBrowserWindow> | null = null
let windowModeSwitchQueue: Promise<void> = Promise.resolve()
let clientTokenRotatePromise: Promise<DesktopClientRotateResponse> | null = null
let canHandleDesktopDeepLinks = false
const pendingDesktopDeepLinks: string[] = []
const forceCloseWindows = new WeakSet<ElectronBrowserWindow>()
const appWindowModes = new WeakMap<ElectronBrowserWindow, RendererWindowMode>()
type WindowMode = 'login' | 'main'
type RendererWindowMode = WindowMode | 'settings'
type WindowModeSource = 'auth-state' | 'login' | 'logout'
interface WindowSizePreset {
width: number
height: number
minWidth: number
minHeight: number
resizable: boolean
}
interface WindowModeRequest {
source?: WindowModeSource
animate?: boolean
}
const WINDOW_READY_TIMEOUT_MS = 1200
const WINDOW_SIZES: Record<WindowMode, WindowSizePreset> = {
login: { width: 340, height: 540, minWidth: 340, minHeight: 540, resizable: false },
main: { width: 1420, height: 960, minWidth: 900, minHeight: 600, resizable: true },
}
const SETTINGS_WINDOW_SIZE: WindowSizePreset = {
width: 920,
height: 640,
minWidth: 760,
minHeight: 520,
resizable: true,
}
const TITLE_BAR_OVERLAY_HEIGHT = 24
function appTitleBarStyle(): 'hidden' | 'hiddenInset' {
return process.platform === 'darwin' ? 'hiddenInset' : 'hidden'
}
function appTitleBarOverlay(
color: string,
): { color: string; symbolColor: string; height: number } | undefined {
if (process.platform === 'darwin') {
return undefined
}
return {
color,
symbolColor: nativeTheme.shouldUseDarkColors ? '#f8fafc' : '#111827',
height: TITLE_BAR_OVERLAY_HEIGHT,
}
}
function suppressNativeWindowMenu(window?: ElectronBrowserWindow): void {
if (process.platform === 'darwin') {
return
}
Menu.setApplicationMenu(null)
window?.setMenu(null)
window?.setMenuBarVisibility(false)
}
function windowModePath(): string {
return join(app.getPath('userData'), 'window-mode.json')
}
function readPersistedWindowMode(): WindowMode {
try {
const raw = readFileSync(windowModePath(), 'utf8')
const data = JSON.parse(raw) as { mode?: unknown }
if (data?.mode === 'main' || data?.mode === 'login') {
return data.mode
}
} catch {
// First run, missing file, or corrupt — fall through to default.
}
return 'login'
}
function persistWindowMode(mode: WindowMode): void {
try {
writeFileSync(windowModePath(), JSON.stringify({ mode }), 'utf8')
} catch (err) {
console.warn('[desktop-main] persist window mode failed', err)
}
}
let currentWindowMode: WindowMode = 'login'
function normalizeWindowModeRequest(input: unknown): WindowModeRequest {
if (!input || typeof input !== 'object') {
return {}
}
const candidate = input as { source?: unknown; animate?: unknown }
return {
source:
candidate.source === 'login' ||
candidate.source === 'logout' ||
candidate.source === 'auth-state'
? candidate.source
: undefined,
animate: candidate.animate === true,
}
}
function currentMainWindow(): ElectronBrowserWindow | null {
if (!mainWindow) {
return null
}
if (mainWindow.isDestroyed()) {
mainWindow = null
return null
}
return mainWindow
}
function currentLoginWindow(): ElectronBrowserWindow | null {
if (!loginWindow) {
return null
}
if (loginWindow.isDestroyed()) {
loginWindow = null
return null
}
return loginWindow
}
function currentSettingsWindow(): ElectronBrowserWindow | null {
if (!settingsWindow) {
return null
}
if (settingsWindow.isDestroyed()) {
settingsWindow = null
return null
}
return settingsWindow
}
function currentActiveWindow(): ElectronBrowserWindow | null {
return currentMainWindow() ?? currentLoginWindow()
}
function rendererModeFromWindow(window: ElectronBrowserWindow): RendererWindowMode | null {
if (window.isDestroyed()) {
return null
}
const knownMode = appWindowModes.get(window)
if (knownMode) {
return knownMode
}
try {
const url = new URL(window.webContents.getURL())
const mode = url.searchParams.get('window')
return mode === 'login' || mode === 'main' || mode === 'settings' ? mode : null
} catch {
return null
}
}
function retireWindowAfterIpcReturn(window: ElectronBrowserWindow): void {
if (window.isDestroyed()) {
return
}
forceCloseWindows.add(window)
if (window.isVisible()) {
window.hide()
}
setTimeout(() => {
if (!window.isDestroyed()) {
window.destroy()
}
}, 50)
}
function retireInactiveAppWindows(mode: WindowMode, target: ElectronBrowserWindow): void {
for (const candidate of BrowserWindow.getAllWindows()) {
if (candidate === target || candidate.isDestroyed()) {
continue
}
const candidateMode = rendererModeFromWindow(candidate)
if (
(mode === 'main' && (candidateMode === 'login' || candidateMode === 'main')) ||
(mode === 'login' &&
(candidateMode === 'login' || candidateMode === 'main' || candidateMode === 'settings'))
) {
retireWindowAfterIpcReturn(candidate)
}
}
}
async function waitForWindowReady(window: ElectronBrowserWindow): Promise<void> {
if (window.isDestroyed() || !window.webContents.isLoading()) {
return
}
await new Promise<void>((resolve) => {
let settled = false
let timeout: ReturnType<typeof setTimeout> | null = null
const finish = () => {
if (settled) {
return
}
settled = true
if (timeout) {
clearTimeout(timeout)
}
window.off('ready-to-show', finish)
window.webContents.off('did-finish-load', finish)
window.webContents.off('did-fail-load', finish)
resolve()
}
timeout = setTimeout(finish, WINDOW_READY_TIMEOUT_MS)
window.once('ready-to-show', finish)
window.webContents.once('did-finish-load', finish)
window.webContents.once('did-fail-load', finish)
})
}
function captureRuntimeSnapshot() {
const snapshot = createRuntimeSnapshot()
applyDesktopHealthIndicatorFromSnapshot(snapshot, currentMainWindow())
return snapshot
}
function captureRuntimeAccountSnapshot(accountId: string) {
return createRuntimeAccountSnapshot(accountId)
}
async function clearRendererDesktopSession(window: ElectronBrowserWindow | null): Promise<void> {
if (!window || window.isDestroyed()) {
return
}
await window.webContents
.executeJavaScript("window.localStorage.removeItem('geo.desktop.session.v1')", true)
.catch((error) => {
console.warn('[desktop-main] clear renderer desktop session failed', {
message: error instanceof Error ? error.message : String(error),
})
})
}
async function clearRendererDesktopSessions(): Promise<void> {
await Promise.all(
BrowserWindow.getAllWindows().map((window) =>
clearRendererDesktopSession(window).catch((error) => {
console.warn('[desktop-main] clear window desktop session failed', {
message: error instanceof Error ? error.message : String(error),
})
}),
),
)
}
async function ensureMainWindow(): Promise<ElectronBrowserWindow> {
const existing = currentMainWindow()
if (existing) {
return existing
}
if (!mainWindowCreatePromise) {
mainWindowCreatePromise = createAppWindow('main')
.then((window) => {
mainWindow = window
return window
})
.finally(() => {
mainWindowCreatePromise = null
})
}
return mainWindowCreatePromise
}
async function ensureLoginWindow(): Promise<ElectronBrowserWindow> {
const existing = currentLoginWindow()
if (existing) {
return existing
}
if (!loginWindowCreatePromise) {
loginWindowCreatePromise = createAppWindow('login')
.then((window) => {
loginWindow = window
return window
})
.finally(() => {
loginWindowCreatePromise = null
})
}
return loginWindowCreatePromise
}
async function ensureSettingsWindow(): Promise<ElectronBrowserWindow> {
const existing = currentSettingsWindow()
if (existing) {
return existing
}
if (!settingsWindowCreatePromise) {
settingsWindowCreatePromise = createSettingsWindow()
.then((window) => {
settingsWindow = window
return window
})
.finally(() => {
settingsWindowCreatePromise = null
})
}
return settingsWindowCreatePromise
}
async function ensureWindow(mode: WindowMode): Promise<ElectronBrowserWindow> {
return mode === 'main' ? ensureMainWindow() : ensureLoginWindow()
}
async function revealWindow(window: ElectronBrowserWindow): Promise<void> {
await waitForWindowReady(window)
if (window.isMinimized()) {
window.restore()
}
if (!window.isVisible()) {
window.show()
}
window.focus()
app.focus({ steal: true })
}
async function revealActiveWindow(): Promise<void> {
const window = currentActiveWindow() ?? (await ensureWindow(currentWindowMode))
await revealWindow(window)
}
function revealActiveWindowSafely(source: string): void {
void revealActiveWindow().catch((error) => {
console.error('[desktop-main] reveal active window failed', { source, error })
})
}
function windowFromIpcEvent(event: IpcMainInvokeEvent): ElectronBrowserWindow | null {
const window = BrowserWindow.fromWebContents(event.sender)
if (!window || window.isDestroyed()) {
return null
}
return window
}
async function switchWindowMode(
mode: WindowMode,
_request: WindowModeRequest = {},
sourceWindow: ElectronBrowserWindow | null = null,
): Promise<void> {
currentWindowMode = mode
persistWindowMode(mode)
const target = await ensureWindow(mode)
const previous =
sourceWindow && sourceWindow !== target
? sourceWindow
: mode === 'main'
? currentLoginWindow()
: currentMainWindow()
if (!target.isVisible()) {
target.center()
}
await revealWindow(target)
if (previous && previous !== target && !previous.isDestroyed()) {
retireWindowAfterIpcReturn(previous)
}
const settings = currentSettingsWindow()
if (mode === 'login' && settings) {
retireWindowAfterIpcReturn(settings)
}
retireInactiveAppWindows(mode, target)
}
function queueWindowModeSwitch(
mode: WindowMode,
request: WindowModeRequest = {},
sourceWindow: ElectronBrowserWindow | null = null,
): Promise<void> {
const nextSwitch = windowModeSwitchQueue
.catch(() => undefined)
.then(() => switchWindowMode(mode, request, sourceWindow))
windowModeSwitchQueue = nextSwitch.catch(() => undefined)
return nextSwitch
}
async function openSettingsWindow(): Promise<void> {
const window = await ensureSettingsWindow()
const parent = settingsWindowParent()
if (parent && window.getParentWindow() !== parent) {
window.setParentWindow(parent)
}
await revealWindow(window)
}
function settingsWindowParent(): ElectronBrowserWindow | undefined {
if (process.platform !== 'win32') {
return undefined
}
return currentMainWindow() ?? undefined
}
function rendererURL(): string | null {
return process.env.ELECTRON_RENDERER_URL ?? null
}
function preloadPath(): string {
return join(__dirname, '../preload/bridge.cjs')
}
async function loadRenderer(
window: ElectronBrowserWindow,
mode: RendererWindowMode,
): Promise<void> {
const devServerURL = rendererURL()
if (devServerURL) {
const url = new URL(devServerURL)
url.searchParams.set('window', mode)
await window.webContents.loadURL(url.toString())
if (mode === 'main' && isDevelopmentRuntime) {
window.webContents.openDevTools({ mode: 'detach' })
}
return
}
await window.webContents.loadFile(join(__dirname, '../renderer/index.html'), {
query: { window: mode },
})
}
async function mountRendererWebContents(
window: ElectronBrowserWindow,
mode: RendererWindowMode,
): Promise<void> {
const contents = window.webContents
contents.setWindowOpenHandler(() => ({ action: 'deny' }))
if (mode === 'main') {
if (isDevelopmentRuntime) {
registerRendererDevtoolsProxyTarget(contents)
registerObservedRequestRendererTarget(contents)
}
mainRendererContents = contents
window.on('closed', () => {
if (mainRendererContents === contents) {
mainRendererContents = null
}
})
}
await loadRenderer(window, mode)
}
async function createAppWindow(mode: WindowMode): Promise<ElectronBrowserWindow> {
const initial = WINDOW_SIZES[mode]
const backgroundColor =
mode === 'login' ? '#eaf2ff' : nativeTheme.shouldUseDarkColors ? '#15191a' : '#f0f2f5'
const window = new BrowserWindow({
width: initial.width,
height: initial.height,
minWidth: initial.minWidth,
minHeight: initial.minHeight,
show: false,
title: '省心推',
icon: createDesktopHealthIcon('normal'),
titleBarStyle: appTitleBarStyle(),
titleBarOverlay: appTitleBarOverlay('#00000000'),
trafficLightPosition: { x: 12, y: 14 },
resizable: initial.resizable,
minimizable: mode !== 'login',
maximizable: initial.resizable,
fullscreenable: initial.resizable,
autoHideMenuBar: true,
backgroundColor,
webPreferences: {
preload: preloadPath(),
sandbox: true,
contextIsolation: true,
nodeIntegration: false,
},
})
appWindowModes.set(window, mode)
suppressNativeWindowMenu(window)
if (!initial.resizable) {
window.setMinimumSize(initial.minWidth, initial.minHeight)
window.setMaximumSize(initial.width, initial.height)
}
window.once('closed', () => {
if (mode === 'main' && mainWindow === window) {
mainWindow = null
}
if (mode === 'login' && loginWindow === window) {
loginWindow = null
}
})
window.on('close', (event) => {
if (
mode === 'main' &&
!quitReleaseInFlight &&
!forceCloseWindows.has(window) &&
getDesktopAppSettings().keepRunningInBackground
) {
event.preventDefault()
window.hide()
// Windows: notify users once that the app is still running in the tray.
// displayBalloon is a no-op on macOS/Linux so the guard is in tray.ts.
if (process.platform === 'win32' && !hasShownBackgroundBalloon()) {
showTrayBalloon('省心推已最小化到托盘', '应用仍在后台运行,右键托盘图标可退出。')
markBackgroundBalloonShown()
}
}
})
window.on('minimize', () => {
console.warn('[desktop-main] main window minimize event', {
at: new Date().toISOString(),
stack: new Error('trace').stack,
})
})
window.on('hide', () => {
console.warn('[desktop-main] main window hide event', {
at: new Date().toISOString(),
stack: new Error('trace').stack,
})
})
window.on('blur', () => {
console.warn('[desktop-main] main window blur event', {
at: new Date().toISOString(),
visible: window.isVisible(),
minimized: window.isMinimized(),
})
})
await mountRendererWebContents(window, mode)
return window
}
async function createSettingsWindow(): Promise<ElectronBrowserWindow> {
const initial = SETTINGS_WINDOW_SIZE
const backgroundColor = nativeTheme.shouldUseDarkColors ? '#15191a' : '#f2f4f7'
const window = new BrowserWindow({
parent: settingsWindowParent(),
width: initial.width,
height: initial.height,
minWidth: initial.minWidth,
minHeight: initial.minHeight,
show: false,
title: '省心推设置',
icon: createDesktopHealthIcon('normal'),
titleBarStyle: appTitleBarStyle(),
titleBarOverlay: appTitleBarOverlay(backgroundColor),
trafficLightPosition: { x: 14, y: 15 },
resizable: initial.resizable,
maximizable: initial.resizable,
fullscreenable: false,
autoHideMenuBar: true,
backgroundColor,
webPreferences: {
preload: preloadPath(),
sandbox: true,
contextIsolation: true,
nodeIntegration: false,
},
})
appWindowModes.set(window, 'settings')
suppressNativeWindowMenu(window)
window.once('closed', () => {
if (settingsWindow === window) {
settingsWindow = null
}
})
await mountRendererWebContents(window, 'settings')
return window
}
function flattenError(error: unknown): Error {
if (error instanceof Error) {
const copy = new Error(error.message || 'ipc_handler_failed')
copy.name = error.name
return copy
}
return new Error(typeof error === 'string' ? error : 'ipc_handler_failed')
}
function safeHandle<Args extends unknown[], Result>(
channel: string,
handler: (...args: Args) => Promise<Result> | Result,
): void {
ipcMain.handle(channel, async (...args) => {
try {
return await handler(...(args as unknown as Args))
} catch (error) {
throw flattenError(error)
}
})
}
function isSafeExternalUrl(url: string): boolean {
try {
const parsed = new URL(url)
return parsed.protocol === 'http:' || parsed.protocol === 'https:'
} catch {
return false
}
}
function registerDesktopProtocolClient(): void {
if (app.isDefaultProtocolClient(desktopDeepLinkScheme)) {
return
}
if (process.defaultApp && process.execPath) {
app.setAsDefaultProtocolClient(desktopDeepLinkScheme, process.execPath, [
process.argv[1] ?? '',
])
return
}
app.setAsDefaultProtocolClient(desktopDeepLinkScheme)
}
function extractDesktopDeepLink(argv: string[]): string | null {
return argv.find((item) => item.startsWith(`${desktopDeepLinkScheme}://`)) ?? null
}
function queueDesktopDeepLink(rawUrl: string | null | undefined): void {
if (!rawUrl) {
return
}
if (!canHandleDesktopDeepLinks) {
pendingDesktopDeepLinks.push(rawUrl)
return
}
handleDesktopDeepLink(rawUrl)
}
function flushPendingDesktopDeepLinks(): void {
canHandleDesktopDeepLinks = true
const links = pendingDesktopDeepLinks.splice(0)
for (const link of links) {
handleDesktopDeepLink(link)
}
}
function handleDesktopDeepLink(rawUrl: string): void {
let parsed: URL
try {
parsed = new URL(rawUrl)
} catch {
return
}
if (parsed.protocol !== `${desktopDeepLinkScheme}:` || parsed.hostname !== 'desktop') {
return
}
if (
parsed.pathname === '/workbench' ||
parsed.searchParams.get('action') === 'open-publish-account-console'
) {
const account = {
id: parsed.searchParams.get('account_id') ?? '',
platform: parsed.searchParams.get('platform') ?? '',
platformUid: parsed.searchParams.get('platform_uid') ?? '',
displayName: parsed.searchParams.get('display_name') ?? '',
}
if (!account.id || !account.platform) {
return
}
void openPublishAccountConsole(account).catch((error) => {
console.warn('[desktop-deeplink] open publish account console failed', {
accountId: account.id,
platform: account.platform,
message: error instanceof Error ? error.message : String(error),
})
})
}
}
function registerBridgeHandlers(): void {
ipcMain.handle('desktop:ping', () => 'pong')
ipcMain.handle('desktop:device-info', () => resolveDesktopDeviceInfo())
safeHandle(
'desktop:client-id',
(_event, scope: { tenant_id?: unknown; workspace_id?: unknown; user_id?: unknown }) =>
resolveDesktopClientID({
tenant_id: typeof scope?.tenant_id === 'number' ? scope.tenant_id : 0,
workspace_id: typeof scope?.workspace_id === 'number' ? scope.workspace_id : 0,
user_id: typeof scope?.user_id === 'number' ? scope.user_id : 0,
}),
)
ipcMain.handle('desktop:runtime-snapshot', () => captureRuntimeSnapshot())
ipcMain.handle('desktop:runtime-account-snapshot', (_event, accountId: string) =>
captureRuntimeAccountSnapshot(accountId),
)
safeHandle('desktop:bind-publish-account', async (_event, platformId: string) => {
const account = (await bindPublishAccount(platformId)) as DesktopAccountInfo
noteRuntimeAccountBound(account)
void refreshRuntimeAccounts().catch((error) => {
console.warn('[desktop-bind] background account refresh failed', {
message: error instanceof Error ? error.message : String(error),
})
})
return account
})
safeHandle('desktop:refresh-runtime-accounts', async () => {
await refreshRuntimeAccounts()
return null
})
safeHandle('desktop:probe-runtime-account', async (_event, accountId: string) => {
await requestRuntimeAccountProbe(accountId)
return captureRuntimeAccountSnapshot(accountId)
})
safeHandle(
'desktop:open-publish-account-console',
async (
_event,
account: { id: string; platform: string; platformUid: string; displayName: string },
) => {
await openPublishAccountConsole(account)
if (isAIPlatformId(account.platform)) {
return null
}
void requestRuntimeAccountProbe(account.id, {
account,
force: true,
}).catch((error) => {
console.warn('[desktop-console] background account probe failed', {
accountId: account.id,
platform: account.platform,
message: error instanceof Error ? error.message : String(error),
})
})
return null
},
)
safeHandle(
'desktop:unbind-publish-account',
async (_event, accountId: string, syncVersion: number) => {
await unbindRuntimeAccount(accountId, syncVersion)
return null
},
)
safeHandle(
'desktop:list-publish-tasks',
async (_event, params?: { page?: number; page_size?: number; title?: string }) => {
return listDesktopPublishTasks(params)
},
)
safeHandle('desktop:retry-publish-task', async (_event, taskId: string) => {
return retryDesktopPublishTask(taskId)
})
safeHandle('desktop:open-external-url', async (_event, url: string) => {
if (!isSafeExternalUrl(url)) {
throw new Error('unsupported_external_url')
}
await shell.openExternal(url)
return null
})
safeHandle('desktop:open-settings-window', async () => {
await openSettingsWindow()
return null
})
ipcMain.handle('desktop:get-app-settings', () => getDesktopAppSettings())
safeHandle(
'desktop:set-app-setting',
async (_event, key: DesktopAppSettingKey, value: boolean) => {
if (key !== 'openAtLogin' && key !== 'keepRunningInBackground') {
throw new Error('unsupported_app_setting')
}
return setDesktopAppSetting(key, value)
},
)
ipcMain.handle(
'desktop:runtime-session-sync',
(_event, session: DesktopRuntimeSessionSyncRequest | null) => {
syncRuntimeSession(session)
return null
},
)
safeHandle('desktop:runtime-session-rotate-client-token', async () => {
if (!clientTokenRotatePromise) {
clientTokenRotatePromise = rotateDesktopClient()
.then((rotated) => {
const currentSession = getRuntimeControllerSnapshot().session
if (
currentSession?.mode === 'authenticated' &&
currentSession.desktop_client?.id === rotated.client.id
) {
syncRuntimeSession({
...currentSession,
client_token: rotated.client_token,
desktop_client: rotated.client,
})
}
return rotated
})
.finally(() => {
clientTokenRotatePromise = null
})
}
return clientTokenRotatePromise
})
safeHandle('desktop:runtime-session-release', async (_event, revoke?: boolean) => {
if (revoke) {
currentWindowMode = 'login'
persistWindowMode('login')
await clearRendererDesktopSessions()
}
await releaseRuntimeSession({ revoke: Boolean(revoke) })
return null
})
ipcMain.handle('desktop:set-window-mode', async (event, mode: WindowMode, request?: unknown) => {
if (mode === 'login' || mode === 'main') {
await queueWindowModeSwitch(
mode,
normalizeWindowModeRequest(request),
windowFromIpcEvent(event),
)
}
return null
})
}
const hasSingleInstanceLock = initSingleInstance((argv) => {
revealActiveWindowSafely('single-instance')
queueDesktopDeepLink(extractDesktopDeepLink(argv))
})
if (!hasSingleInstanceLock) {
console.info('[desktop-main] another instance is already running; exiting')
app.exit(0)
} else {
app
.whenReady()
.then(async () => {
suppressNativeWindowMenu()
registerDesktopProtocolClient()
currentWindowMode = 'login'
persistWindowMode('login')
initDesktopAppSettings()
registerBridgeHandlers()
await initSessionRegistry()
initAccountHealth()
initTransport()
initScheduler()
initProcessMetricsSampler()
startHotViewReaper()
startHiddenPlaywrightReaper()
onRuntimeInvalidated((event) => {
syncDesktopHealthIndicator(currentMainWindow())
if (!mainRendererContents || mainRendererContents.isDestroyed()) {
return
}
mainRendererContents.send('desktop:runtime-invalidated', event)
})
await queueWindowModeSwitch(currentWindowMode)
initTray(() => {
revealActiveWindowSafely('tray')
})
startDesktopHealthIndicator(() => currentMainWindow())
queueDesktopDeepLink(extractDesktopDeepLink(process.argv))
flushPendingDesktopDeepLinks()
})
.catch((error) => {
console.error('[desktop-main] app.whenReady failed', error)
})
app.on('activate', () => {
revealActiveWindowSafely('activate')
})
app.on('open-url', (event, url) => {
event.preventDefault()
revealActiveWindowSafely('deep-link')
queueDesktopDeepLink(url)
})
app.on('window-all-closed', () => {
if (getDesktopAppSettings().keepRunningInBackground) {
return
}
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('before-quit', (event) => {
if (quitReleaseInFlight) {
return
}
quitReleaseInFlight = true
event.preventDefault()
currentWindowMode = 'login'
persistWindowMode('login')
void Promise.race([
(async () => {
await clearRendererDesktopSessions()
await releaseRuntimeSession()
})(),
new Promise<void>((resolve) => setTimeout(resolve, 1500)),
]).finally(() => {
app.quit()
})
})
}