@@ -23,6 +23,95 @@ import { applyZoomToWindow } from './process/utils/zoom';
2323// @ts -expect-error - electron-squirrel-startup doesn't have types
2424import electronSquirrelStartup from 'electron-squirrel-startup' ;
2525
26+ // ============ Deep Link Protocol ============
27+ // Register aionui:// protocol scheme for external app integration (e.g., New API token quick-add)
28+ const PROTOCOL_SCHEME = 'aionui' ;
29+
30+ /**
31+ * Parse an aionui:// URL into action and params.
32+ * Supports two formats:
33+ * 1. aionui://add-provider?baseUrl=xxx&apiKey=xxx
34+ * 2. aionui://provider/add?v=1&data=<base64 JSON> (one-api / new-api style)
35+ */
36+ const parseDeepLinkUrl = ( url : string ) : { action : string ; params : Record < string , string > } | null => {
37+ try {
38+ const parsed = new URL ( url ) ;
39+ if ( parsed . protocol !== `${ PROTOCOL_SCHEME } :` ) return null ;
40+
41+ // Build action from hostname + pathname, e.g. "provider/add" or "add-provider"
42+ const hostname = parsed . hostname || '' ;
43+ const pathname = parsed . pathname . replace ( / ^ \/ + / , '' ) ;
44+ const action = pathname ? `${ hostname } /${ pathname } ` : hostname ;
45+
46+ const params : Record < string , string > = { } ;
47+ parsed . searchParams . forEach ( ( value , key ) => {
48+ params [ key ] = value ;
49+ } ) ;
50+
51+ // If data param exists, decode base64 JSON and merge into params
52+ if ( params . data ) {
53+ try {
54+ const json = JSON . parse ( Buffer . from ( params . data , 'base64' ) . toString ( 'utf-8' ) ) ;
55+ if ( json && typeof json === 'object' ) {
56+ Object . assign ( params , json ) ;
57+ }
58+ } catch {
59+ // Ignore decode errors
60+ }
61+ // Remove raw base64 blob so it isn't forwarded to the renderer
62+ delete params . data ;
63+ }
64+
65+ return { action, params } ;
66+ } catch {
67+ return null ;
68+ }
69+ } ;
70+
71+ /** Pending deep-link URL received before the window was ready */
72+ let pendingDeepLinkUrl : string | null = process . argv . find ( ( arg ) => arg . startsWith ( `${ PROTOCOL_SCHEME } ://` ) ) || null ;
73+
74+ /**
75+ * Send the deep-link payload to the renderer via IPC bridge.
76+ * If the window isn't ready yet, queue it.
77+ */
78+ const handleDeepLinkUrl = ( url : string ) => {
79+ const parsed = parseDeepLinkUrl ( url ) ;
80+ if ( ! parsed ) return ;
81+
82+ if ( ! mainWindow || mainWindow . isDestroyed ( ) ) {
83+ // Window not ready yet – last-write-wins: only the most recent deep link is kept,
84+ // which is intentional since the user can only act on one at a time.
85+ pendingDeepLinkUrl = url ;
86+ return ;
87+ }
88+
89+ ipcBridge . deepLink . received . emit ( parsed ) ;
90+ } ;
91+
92+ // ============ Single Instance Lock ============
93+ // Acquire lock early so the second instance quits before doing unnecessary work.
94+ // When a second instance starts (e.g. from protocol URL), it sends its data
95+ // to the first instance via second-instance event, then quits.
96+ const deepLinkFromArgv = process . argv . find ( ( arg ) => arg . startsWith ( `${ PROTOCOL_SCHEME } ://` ) ) ;
97+ const gotTheLock = app . requestSingleInstanceLock ( { deepLinkUrl : deepLinkFromArgv } ) ;
98+ if ( ! gotTheLock ) {
99+ app . quit ( ) ;
100+ } else {
101+ app . on ( 'second-instance' , ( _event , argv , _workingDirectory , additionalData ) => {
102+ // Prefer additionalData (reliable on all platforms), fallback to argv scan
103+ const deepLinkUrl = ( additionalData as { deepLinkUrl ?: string } ) ?. deepLinkUrl || argv . find ( ( arg ) => arg . startsWith ( `${ PROTOCOL_SCHEME } ://` ) ) ;
104+ if ( deepLinkUrl ) {
105+ handleDeepLinkUrl ( deepLinkUrl ) ;
106+ }
107+ // Focus existing window
108+ if ( mainWindow ) {
109+ if ( mainWindow . isMinimized ( ) ) mainWindow . restore ( ) ;
110+ mainWindow . focus ( ) ;
111+ }
112+ } ) ;
113+ }
114+
26115// Handle creating/removing shortcuts on Windows when installing/uninstalling.
27116// 修复 macOS 和 Linux 下 GUI 应用的 PATH 环境变量,使其与命令行一致
28117if ( process . platform === 'darwin' || process . platform === 'linux' ) {
@@ -197,6 +286,8 @@ const createWindow = (): void => {
197286 mainWindow = new BrowserWindow ( {
198287 width : windowWidth ,
199288 height : windowHeight ,
289+ show : false , // Hide until CSS is loaded to prevent FOUC
290+ backgroundColor : '#ffffff' ,
200291 autoHideMenuBar : true ,
201292 // Set icon for Windows/Linux in development mode
202293 ...( devIcon && process . platform !== 'darwin' ? { icon : devIcon } : { } ) ,
@@ -213,6 +304,18 @@ const createWindow = (): void => {
213304 } ,
214305 } ) ;
215306
307+ // Show window after page and CSS are fully loaded to prevent FOUC
308+ const showWindow = ( ) => {
309+ if ( ! mainWindow . isDestroyed ( ) && ! mainWindow . isVisible ( ) ) {
310+ mainWindow . show ( ) ;
311+ }
312+ } ;
313+ mainWindow . webContents . once ( 'did-finish-load' , ( ) => {
314+ setTimeout ( showWindow , 200 ) ;
315+ } ) ;
316+ // Fallback: show window after 3s even if did-finish-load doesn't fire
317+ setTimeout ( showWindow , 3000 ) ;
318+
216319 initMainAdapterWithWindow ( mainWindow ) ;
217320 setupApplicationMenu ( ) ;
218321 void applyZoomToWindow ( mainWindow ) ;
@@ -317,6 +420,16 @@ const handleAppReady = async (): Promise<void> => {
317420 await startWebServer ( resolvedPort , allowRemote ) ;
318421 } else {
319422 createWindow ( ) ;
423+
424+ // Flush pending deep-link URL (received before window was ready)
425+ if ( pendingDeepLinkUrl ) {
426+ const url = pendingDeepLinkUrl ;
427+ pendingDeepLinkUrl = null ;
428+ // Wait for renderer to be ready before sending
429+ mainWindow . webContents . once ( 'did-finish-load' , ( ) => {
430+ handleDeepLinkUrl ( url ) ;
431+ } ) ;
432+ }
320433 }
321434
322435 // 启动时初始化ACP检测器 (skip in --resetpass mode)
@@ -339,6 +452,27 @@ const handleAppReady = async (): Promise<void> => {
339452 } ) ;
340453} ;
341454
455+ // ============ Protocol Registration ============
456+ // Register aionui:// as the default protocol client
457+ if ( process . defaultApp ) {
458+ // Dev mode: need to pass execPath explicitly
459+ app . setAsDefaultProtocolClient ( PROTOCOL_SCHEME , process . execPath , [ path . resolve ( process . argv [ 1 ] ) ] ) ;
460+ } else {
461+ app . setAsDefaultProtocolClient ( PROTOCOL_SCHEME ) ;
462+ }
463+
464+ // macOS: handle aionui:// URLs via the open-url event
465+ app . on ( 'open-url' , ( event , url ) => {
466+ event . preventDefault ( ) ;
467+ handleDeepLinkUrl ( url ) ;
468+ // Focus existing window so user sees the result
469+ if ( mainWindow && ! mainWindow . isDestroyed ( ) ) {
470+ if ( mainWindow . isMinimized ( ) ) mainWindow . restore ( ) ;
471+ mainWindow . show ( ) ;
472+ mainWindow . focus ( ) ;
473+ }
474+ } ) ;
475+
342476// Ensure we don't miss the ready event when running in CLI/WebUI mode
343477void app
344478 . whenReady ( )
0 commit comments