feat(electron): add Windows close-to-tray behavior

This commit is contained in:
digua committed 2026-08-07 20:29:37 +08:00
1 parent b7f59a75ea
commit 35880228b7
27 files changed
+496 -16

No files matched your search

+2
View File
@@ -57,6 +57,8 @@ win:
extraResources:
- from: native/better_sqlite3.node
to: native/better_sqlite3.node
- from: build/icon.ico
to: tray-icon.ico
# NSIS 安装器配置
nsis:
+11
View File
@@ -12,6 +12,17 @@ export default {
error: 'Error',
},
windowClose: {
title: 'Close ChatLab',
message: 'What should happen after the main window closes?',
detail: 'Run in background keeps ChatLab available from the system tray.',
background: 'Run in Background',
quitApp: 'Quit ChatLab',
cancel: 'Cancel',
remember: 'Remember my choice',
showApp: 'Show ChatLab',
},
// ===== P0: Update dialogs =====
update: {
newVersionTitle: 'New version v{{version}} available',
+11
View File
@@ -12,6 +12,17 @@ export default {
error: 'エラー',
},
windowClose: {
title: 'ChatLab を閉じる',
message: 'メインウィンドウを閉じた後の動作を選択してください。',
detail: 'バックグラウンドで実行すると、ChatLab はシステムトレイから開けます。',
background: 'バックグラウンドで実行',
quitApp: 'ChatLab を終了',
cancel: 'キャンセル',
remember: '選択を記憶する',
showApp: 'ChatLab を表示',
},
// ===== P0: アップデートダイアログ =====
update: {
newVersionTitle: '新バージョン v{{version}} が見つかりました',
+11
View File
@@ -11,6 +11,17 @@ export default {
error: '错误',
},
windowClose: {
title: '关闭 ChatLab',
message: '关闭主窗口后要执行什么操作?',
detail: '选择后台运行后,ChatLab 会保留在系统托盘中。',
background: '后台运行',
quitApp: '退出应用',
cancel: '取消',
remember: '记住我的选择',
showApp: '显示 ChatLab',
},
// ===== P0: 更新弹窗 =====
update: {
newVersionTitle: '发现新版本 v{{version}}',
+11
View File
@@ -12,6 +12,17 @@ export default {
error: '錯誤',
},
windowClose: {
title: '關閉 ChatLab',
message: '關閉主視窗後要執行什麼操作?',
detail: '選擇背景執行後,ChatLab 會保留在系統匣中。',
background: '背景執行',
quitApp: '結束應用程式',
cancel: '取消',
remember: '記住我的選擇',
showApp: '顯示 ChatLab',
},
// ===== P0: 更新彈窗 =====
update: {
newVersionTitle: '發現新版本 v{{version}}',
+2
View File
@@ -10,6 +10,7 @@ import { initAnalytics } from './analytics'
import { logger } from './logger'
import { prepareDesktopRuntime } from './app/bootstrap'
import { createMainWindow, markAppQuitting } from './window/main-window'
import { destroyWindowsTray } from './window/windows-tray'
import { initLockManager, cleanupLockManager } from './security/lock-manager'
class MainProcess {
@@ -148,6 +149,7 @@ class MainProcess {
})
app.on('will-quit', () => {
destroyWindowsTray()
stopInternalServer().catch(() => {})
cleanup()
cleanupLockManager()
+24 -7
View File
@@ -4,6 +4,8 @@
import { ipcMain, app, dialog, clipboard, shell, nativeTheme } from 'electron'
import * as fs from 'fs/promises'
import { loadConfig, setConfigField } from '@openchatlab/config'
import type { DesktopCloseBehavior } from '@openchatlab/shared-types'
import type { IpcContext } from './types'
import { simulateUpdateDialog, manualCheckForUpdates } from '../update/manager'
import { t } from '../i18n'
@@ -13,10 +15,8 @@ import {
resetCurrentTitleBarOverlayColor,
} from '../window/titlebar'
import { getDesktopAppVersion } from '../runtime/compat'
type AppWithQuitFlag = typeof app & { isQuiting?: boolean }
// 通过类型扩展记录应用退出意图,避免使用 @ts-ignore。
const appWithQuitFlag = app as AppWithQuitFlag
import { requestAppQuit } from '../window/main-window'
import { destroyWindowsTray } from '../window/windows-tray'
const REMOTE_CONFIG_ALLOWED_DOMAINS = ['chatlab.fun', '1app.top']
const REMOTE_CONFIG_TIMEOUT_MS = 8000
@@ -69,9 +69,7 @@ export function registerWindowHandlers(ctx: IpcContext): void {
})
ipcMain.on('window-close', () => {
win.close()
appWithQuitFlag.isQuiting = true
app.quit()
requestAppQuit()
})
ipcMain.on('window-resize', (_, data) => {
@@ -226,6 +224,25 @@ export function registerWindowHandlers(ctx: IpcContext): void {
}
})
// ==================== Windows 关闭行为 ====================
ipcMain.handle('app:getDesktopCloseBehavior', () => {
return loadConfig().desktop.close_behavior
})
ipcMain.handle('app:setDesktopCloseBehavior', (_, behavior: DesktopCloseBehavior) => {
if (behavior !== 'ask' && behavior !== 'background' && behavior !== 'quit') {
return { success: false, error: 'Unsupported desktop close behavior' }
}
try {
setConfigField('desktop.close_behavior', behavior)
if (behavior !== 'background') destroyWindowsTray()
return { success: true }
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) }
}
})
// ==================== 更新检查 ====================
ipcMain.on('check-update', () => {
// 手动检查更新(即使是预发布版本也会提示)
@@ -0,0 +1,83 @@
import assert from 'node:assert/strict'
import { describe, it } from 'node:test'
import type { DesktopCloseBehavior } from '@openchatlab/shared-types'
import { WindowsCloseController } from './close-behavior'
interface HarnessOptions {
preference?: DesktopCloseBehavior
promptResult?: { action: 'background' | 'quit' | 'cancel'; remember: boolean }
}
function createHarness(options: HarnessOptions = {}) {
const calls: string[] = []
let resolvePrompt: (() => void) | undefined
const promptGate = new Promise<void>((resolve) => {
resolvePrompt = resolve
})
let waitForPrompt = false
const controller = new WindowsCloseController({
readPreference: () => options.preference ?? 'ask',
savePreference: (preference) => calls.push(`save:${preference}`),
prompt: async () => {
calls.push('prompt')
if (waitForPrompt) await promptGate
return options.promptResult ?? { action: 'cancel', remember: false }
},
enterBackground: () => calls.push('background'),
quit: () => calls.push('quit'),
onError: (error) => calls.push(`error:${String(error)}`),
})
return {
calls,
controller,
holdPrompt: () => {
waitForPrompt = true
},
releasePrompt: () => resolvePrompt?.(),
}
}
describe('WindowsCloseController', () => {
it('applies remembered background and quit choices without prompting', async () => {
for (const preference of ['background', 'quit'] as const) {
const harness = createHarness({ preference })
await harness.controller.requestClose()
assert.deepEqual(harness.calls, [preference])
}
})
it('remembers the selected action only when the checkbox is enabled', async () => {
const remembered = createHarness({ promptResult: { action: 'background', remember: true } })
await remembered.controller.requestClose()
assert.deepEqual(remembered.calls, ['prompt', 'save:background', 'background'])
const oneTime = createHarness({ promptResult: { action: 'quit', remember: false } })
await oneTime.controller.requestClose()
assert.deepEqual(oneTime.calls, ['prompt', 'quit'])
})
it('keeps the window open when the prompt is cancelled', async () => {
const harness = createHarness({ promptResult: { action: 'cancel', remember: true } })
await harness.controller.requestClose()
assert.deepEqual(harness.calls, ['prompt'])
})
it('coalesces repeated close requests while the prompt is open', async () => {
const harness = createHarness({ promptResult: { action: 'background', remember: false } })
harness.holdPrompt()
const first = harness.controller.requestClose()
const second = harness.controller.requestClose()
assert.deepEqual(harness.calls, ['prompt'])
harness.releasePrompt()
await Promise.all([first, second])
assert.deepEqual(harness.calls, ['prompt', 'background'])
})
})
@@ -0,0 +1,82 @@
import type { DesktopCloseBehavior } from '@openchatlab/shared-types'
export type WindowsCloseAction = Exclude<DesktopCloseBehavior, 'ask'> | 'cancel'
export interface WindowsClosePromptResult {
action: WindowsCloseAction
remember: boolean
}
interface WindowsCloseControllerDependencies {
readPreference: () => DesktopCloseBehavior
savePreference: (preference: Exclude<DesktopCloseBehavior, 'ask'>) => void
prompt: () => Promise<WindowsClosePromptResult>
enterBackground: () => void
quit: () => void
onError: (error: unknown) => void
}
export class WindowsCloseController {
private handling = false
constructor(private readonly dependencies: WindowsCloseControllerDependencies) {}
async requestClose(): Promise<void> {
if (this.handling) return
this.handling = true
try {
const preference = this.readPreference()
const result = preference === 'ask' ? await this.prompt() : { action: preference, remember: false }
if (result.action === 'cancel') return
if (result.remember) {
try {
this.savePreference(result.action)
} catch (error) {
this.onError(error)
}
}
if (result.action === 'background') {
this.enterBackground()
} else {
this.quit()
}
} catch (error) {
this.onError(error)
} finally {
this.handling = false
}
}
private readPreference(): DesktopCloseBehavior {
try {
return this.dependencies.readPreference()
} catch (error) {
this.dependencies.onError(error)
return 'ask'
}
}
private prompt(): Promise<WindowsClosePromptResult> {
return this.dependencies.prompt()
}
private savePreference(preference: Exclude<DesktopCloseBehavior, 'ask'>): void {
this.dependencies.savePreference(preference)
}
private enterBackground(): void {
this.dependencies.enterBackground()
}
private quit(): void {
this.dependencies.quit()
}
private onError(error: unknown): void {
this.dependencies.onError(error)
}
}
+61 -1
View File
@@ -1,6 +1,11 @@
import { app, shell, BrowserWindow, nativeTheme } from 'electron'
import { app, shell, BrowserWindow, nativeTheme, dialog } from 'electron'
import { is, platform } from '@electron-toolkit/utils'
import { loadConfig, setConfigField } from '@openchatlab/config'
import { applyCurrentTitleBarOverlay, getTitleBarOverlayOptions, resetCurrentTitleBarOverlayColor } from './titlebar'
import { WindowsCloseController } from './close-behavior'
import { ensureWindowsTray } from './windows-tray'
import { t } from '../i18n'
import { logger } from '../logger'
type AppWithQuitFlag = typeof app & { isQuiting?: boolean }
@@ -75,7 +80,50 @@ export function markAppQuitting(): void {
appWithQuitFlag.isQuiting = true
}
export function requestAppQuit(): void {
markAppQuitting()
app.quit()
}
function createWindowsCloseController(win: BrowserWindow): WindowsCloseController {
return new WindowsCloseController({
readPreference: () => loadConfig().desktop.close_behavior,
savePreference: (preference) => {
setConfigField('desktop.close_behavior', preference)
},
prompt: async () => {
const result = await dialog.showMessageBox(win, {
type: 'question',
title: t('windowClose.title'),
message: t('windowClose.message'),
detail: t('windowClose.detail'),
buttons: [t('windowClose.background'), t('windowClose.quitApp'), t('windowClose.cancel')],
defaultId: 0,
cancelId: 2,
noLink: true,
checkboxLabel: t('windowClose.remember'),
checkboxChecked: false,
})
return {
action: result.response === 0 ? 'background' : result.response === 1 ? 'quit' : 'cancel',
remember: result.checkboxChecked,
}
},
enterBackground: () => {
ensureWindowsTray(win, requestAppQuit)
win.hide()
},
quit: requestAppQuit,
onError: (error) => {
logger.error(`Failed to handle Windows close request: ${error instanceof Error ? error.message : String(error)}`)
},
})
}
function registerMainWindowEvents(win: BrowserWindow): void {
const windowsCloseController = platform.isWindows ? createWindowsCloseController(win) : null
win.webContents.on('did-finish-load', () => {
setTimeout(() => {
currentMainWindow?.webContents.send('app-started')
@@ -90,10 +138,22 @@ function registerMainWindowEvents(win: BrowserWindow): void {
currentMainWindow?.webContents.send('windowState', false)
})
// Windows does not emit app.before-quit during system shutdown or restart.
// session-end only fires once the session can no longer be cancelled.
win.on('session-end', () => {
markAppQuitting()
})
win.on('close', (event) => {
if (platform.isMacOS && !appWithQuitFlag.isQuiting) {
event.preventDefault()
currentMainWindow?.hide()
return
}
if (platform.isWindows && !appWithQuitFlag.isQuiting) {
event.preventDefault()
void windowsCloseController?.requestClose()
}
})
}
+44
View File
@@ -0,0 +1,44 @@
import { app, BrowserWindow, Menu, Tray } from 'electron'
import { join } from 'path'
import { t } from '../i18n'
let tray: Tray | null = null
let targetWindow: BrowserWindow | null = null
let quitHandler: (() => void) | null = null
export function ensureWindowsTray(win: BrowserWindow, onQuit: () => void): void {
targetWindow = win
quitHandler = onQuit
if (!tray) {
tray = new Tray(resolveTrayIconPath())
tray.setToolTip('ChatLab')
tray.on('click', restoreMainWindow)
}
tray.setContextMenu(
Menu.buildFromTemplate([
{ label: t('windowClose.showApp'), click: restoreMainWindow },
{ type: 'separator' },
{ label: t('windowClose.quitApp'), click: () => quitHandler?.() },
])
)
}
export function destroyWindowsTray(): void {
tray?.destroy()
tray = null
targetWindow = null
quitHandler = null
}
function restoreMainWindow(): void {
if (!targetWindow || targetWindow.isDestroyed()) return
if (targetWindow.isMinimized()) targetWindow.restore()
targetWindow.show()
targetWindow.focus()
}
function resolveTrayIconPath(): string {
return app.isPackaged ? join(process.resourcesPath, 'tray-icon.ico') : join(__dirname, '../../build/icon.ico')
}
+7 -1
View File
@@ -2,7 +2,7 @@
* 核心 API - 基础 IPC 通信和系统功能
*/
import { ipcRenderer } from 'electron'
import type { AnalyticsEventName } from '@openchatlab/shared-types'
import type { AnalyticsEventName, DesktopCloseBehavior } from '@openchatlab/shared-types'
// Custom APIs for renderer
export const api = {
@@ -117,5 +117,11 @@ export const extendedApi = {
setOpenAtLogin: (enabled: boolean): Promise<{ success: boolean; error?: string }> => {
return ipcRenderer.invoke('app:setOpenAtLogin', enabled)
},
getDesktopCloseBehavior: (): Promise<DesktopCloseBehavior> => {
return ipcRenderer.invoke('app:getDesktopCloseBehavior')
},
setDesktopCloseBehavior: (behavior: DesktopCloseBehavior): Promise<{ success: boolean; error?: string }> => {
return ipcRenderer.invoke('app:setDesktopCloseBehavior', behavior)
},
},
}
+3 -1
View File
@@ -1,7 +1,7 @@
import { ElectronAPI } from '@electron-toolkit/preload'
import type { ImportProgress, ExportProgress } from '../../../src/types/base'
import type { TokenUsage, AgentRuntimeStatus, SerializedErrorInfo, SecurityApi } from '../shared/types'
import type { AnalyticsEventName, TimeFilter } from '@openchatlab/shared-types'
import type { AnalyticsEventName, DesktopCloseBehavior, TimeFilter } from '@openchatlab/shared-types'
// 迁移相关类型
interface MigrationInfo {
@@ -192,6 +192,8 @@ interface Api {
relaunch: () => Promise<void>
getOpenAtLogin: () => Promise<boolean>
setOpenAtLogin: (enabled: boolean) => Promise<{ success: boolean; error?: string }>
getDesktopCloseBehavior: () => Promise<DesktopCloseBehavior>
setDesktopCloseBehavior: (behavior: DesktopCloseBehavior) => Promise<{ success: boolean; error?: string }>
}
}
+11 -2
View File
@@ -7,8 +7,17 @@
export { loadConfig, getConfigPath, getConfigDir, writeConfigField } from './loader'
export { setConfigField, ConfigSetError } from './set-config-field'
export type { ConfigSetResult, ConfigSetErrorReason } from './set-config-field'
export { configSchema, DEFAULT_API_PORT } from './schema'
export type { ChatLabConfig, LlmConfig, DataConfig, ApiConfig, LocaleConfig, UiConfig, CliConfig } from './schema'
export { configSchema, desktopConfigSchema, DEFAULT_API_PORT } from './schema'
export type {
ChatLabConfig,
LlmConfig,
DataConfig,
ApiConfig,
LocaleConfig,
UiConfig,
CliConfig,
DesktopConfig,
} from './schema'
export {
loadAuthProfiles,
getApiKeyByProfile,
+17
View File
@@ -17,3 +17,20 @@ test('normalizes the released overview preference to insights', () => {
test('rejects unknown session landing tabs', () => {
assert.throws(() => configSchema.parse({ ui: { default_session_tab: 'unknown' } }))
})
test('defaults Windows close behavior to asking the user', () => {
const config = configSchema.parse({})
assert.equal(config.desktop.close_behavior, 'ask')
})
test('accepts supported Windows close behaviors and rejects invalid values', () => {
for (const closeBehavior of ['ask', 'background', 'quit']) {
assert.equal(
configSchema.parse({ desktop: { close_behavior: closeBehavior } }).desktop.close_behavior,
closeBehavior
)
}
assert.throws(() => configSchema.parse({ desktop: { close_behavior: 'hide-forever' } }))
})
+6
View File
@@ -46,6 +46,10 @@ export const cliConfigSchema = z.object({
allow_sql: z.boolean().default(true),
})
export const desktopConfigSchema = z.object({
close_behavior: z.enum(['ask', 'background', 'quit']).default('ask'),
})
export const configSchema = z.object({
llm: llmConfigSchema.default({}),
data: dataConfigSchema.default({}),
@@ -53,6 +57,7 @@ export const configSchema = z.object({
locale: localeConfigSchema.default({}),
ui: uiConfigSchema.default({}),
cli: cliConfigSchema.default({}),
desktop: desktopConfigSchema.default({}),
})
export type ChatLabConfig = z.infer<typeof configSchema>
@@ -62,3 +67,4 @@ export type ApiConfig = z.infer<typeof apiConfigSchema>
export type LocaleConfig = z.infer<typeof localeConfigSchema>
export type UiConfig = z.infer<typeof uiConfigSchema>
export type CliConfig = z.infer<typeof cliConfigSchema>
export type DesktopConfig = z.infer<typeof desktopConfigSchema>
@@ -45,6 +45,13 @@ describe('cli config section', () => {
})
describe('setConfigField', () => {
it('persists the Windows close behavior', () => {
const result = setConfigField('desktop.close_behavior', 'background')
assert.deepEqual(result, { section: 'desktop', key: 'close_behavior', value: 'background' })
assert.equal(loadConfig().desktop.close_behavior, 'background')
})
it('writes boolean value and round-trips via loadConfig', () => {
const result = setConfigField('cli.allow_raw', 'true')
assert.deepEqual(result, { section: 'cli', key: 'allow_raw', value: true })
+2
View File
@@ -666,6 +666,8 @@ export interface UiConfig {
summary_strategy?: 'brief' | 'standard'
}
export type DesktopCloseBehavior = 'ask' | 'background' | 'quit'
// ==================== 匿名使用统计 ====================
export type AnalyticsAppType = 'desktop' | 'cli' | 'cli_web' | 'web_wasm'
@@ -11,6 +11,7 @@ import UITabs from '@/components/UI/Tabs.vue'
import { usePlatformService } from '@/services'
import { IS_ELECTRON } from '@/utils/platform'
import { INSIGHT_CARD_THEMES, type InsightCardThemeId } from '@/utils/insight-card-theme'
import type { DesktopCloseBehavior } from '@openchatlab/shared-types'
const { t } = useI18n()
@@ -32,6 +33,10 @@ const { locale, defaultSessionTab, insightCardTheme } = storeToRefs(settingsStor
// Auto Launch
const openAtLogin = ref(false)
const isPackaged = ref(true)
const isWindowsDesktop =
IS_ELECTRON && typeof navigator !== 'undefined' && navigator.platform.toLowerCase().includes('win')
const desktopCloseBehavior = ref<DesktopCloseBehavior>('ask')
let savedDesktopCloseBehavior: DesktopCloseBehavior = 'ask'
onMounted(async () => {
if (!IS_ELECTRON) {
@@ -44,6 +49,15 @@ onMounted(async () => {
} catch {
isPackaged.value = false
}
if (isWindowsDesktop) {
try {
savedDesktopCloseBehavior = await usePlatformService().getDesktopCloseBehavior()
desktopCloseBehavior.value = savedDesktopCloseBehavior
} catch {
desktopCloseBehavior.value = 'ask'
}
}
})
async function handleAutoLaunchChange(enabled: boolean) {
@@ -55,6 +69,19 @@ async function handleAutoLaunchChange(enabled: boolean) {
}
}
async function handleDesktopCloseBehaviorChange(value: string | number) {
if (value !== 'ask' && value !== 'background' && value !== 'quit') return
const nextBehavior: DesktopCloseBehavior = value
desktopCloseBehavior.value = nextBehavior
const result = await usePlatformService().setDesktopCloseBehavior(nextBehavior)
if (result.success) {
savedDesktopCloseBehavior = nextBehavior
} else {
desktopCloseBehavior.value = savedDesktopCloseBehavior
}
}
// Color Mode
const colorMode = useColorMode({
emitAuto: true,
@@ -103,6 +130,11 @@ const toolsPanelPositionOptions = computed(() => [
{ label: t('settings.basic.toolsPanel.positionHeader'), value: 'header' },
{ label: t('settings.basic.toolsPanel.positionSide'), value: 'side' },
])
const desktopCloseBehaviorOptions = computed(() => [
{ label: t('settings.basic.closeBehavior.background'), value: 'background' },
{ label: t('settings.basic.closeBehavior.quit'), value: 'quit' },
])
</script>
<template>
@@ -141,6 +173,25 @@ const toolsPanelPositionOptions = computed(() => [
</div>
<USwitch v-model="openAtLogin" :disabled="!isPackaged" @update:model-value="handleAutoLaunchChange" />
</div>
<template v-if="isWindowsDesktop">
<div class="border-t border-gray-200 dark:border-gray-700"></div>
<div class="flex items-center justify-between p-4">
<div class="flex-1 pr-4">
<p class="text-sm font-medium text-gray-900 dark:text-white">
{{ t('settings.basic.closeBehavior.label') }}
</p>
</div>
<div class="w-64">
<UTabs
:model-value="desktopCloseBehavior === 'ask' ? undefined : desktopCloseBehavior"
size="sm"
class="gap-0"
:items="desktopCloseBehaviorOptions"
@update:model-value="handleDesktopCloseBehaviorChange"
></UTabs>
</div>
</div>
</template>
</template>
</div>
</div>
+5
View File
@@ -49,6 +49,11 @@
"openAtLoginDesc": "Automatically start ChatLab when the system boots",
"devModeHint": "Not available in dev mode"
},
"closeBehavior": {
"label": "When closing the main window",
"background": "Run in background",
"quit": "Quit app"
},
"toolsPanel": {
"positionLabel": "Tools Panel Position",
"positionDesc": "Choose where the quick tools panel appears",
+5
View File
@@ -49,6 +49,11 @@
"openAtLoginDesc": "システム起動時に ChatLab を自動的に実行します",
"devModeHint": "開発モードでは利用できません"
},
"closeBehavior": {
"label": "メインウィンドウを閉じるとき",
"background": "バックグラウンドで実行",
"quit": "アプリを終了"
},
"toolsPanel": {
"positionLabel": "ツールパネルの位置",
"positionDesc": "クイックツールパネルの表示位置を選択します",
+5
View File
@@ -49,6 +49,11 @@
"openAtLoginDesc": "系统启动时自动运行 ChatLab",
"devModeHint": "开发模式下不可用"
},
"closeBehavior": {
"label": "关闭主窗口时",
"background": "后台运行",
"quit": "退出应用"
},
"toolsPanel": {
"positionLabel": "快捷工具入口位置",
"positionDesc": "选择快捷工具面板的显示位置",
+5
View File
@@ -49,6 +49,11 @@
"openAtLoginDesc": "系統啟動時自動執行 ChatLab",
"devModeHint": "開發模式下不可用"
},
"closeBehavior": {
"label": "關閉主視窗時",
"background": "背景執行",
"quit": "結束應用程式"
},
"toolsPanel": {
"positionLabel": "快捷工具入口位置",
"positionDesc": "選擇快捷工具面板的顯示位置",
+9 -1
View File
@@ -6,7 +6,7 @@ import type {
PlatformAdapter,
RemoteConfigResult,
} from './types'
import type { AnalyticsEventName } from '@openchatlab/shared-types'
import type { AnalyticsEventName, DesktopCloseBehavior } from '@openchatlab/shared-types'
declare const __APP_VERSION__: string
@@ -41,6 +41,14 @@ export class BrowserPlatformAdapter implements PlatformAdapter {
return { success: false, error: 'Open at login is not available in Web WASM' }
}
async getDesktopCloseBehavior(): Promise<DesktopCloseBehavior> {
return 'ask'
}
async setDesktopCloseBehavior(_behavior: DesktopCloseBehavior): Promise<{ success: boolean; error?: string }> {
return { success: false, error: 'Desktop close behavior is not available in Web WASM' }
}
async getAnalyticsEnabled(): Promise<boolean> {
return false
}
+9 -1
View File
@@ -12,7 +12,7 @@ import type {
CheckUpdateResult,
PerformUpdateResult,
} from './types'
import type { AnalyticsEventName } from '@openchatlab/shared-types'
import type { AnalyticsEventName, DesktopCloseBehavior } from '@openchatlab/shared-types'
import { fetchWithAuth } from '../utils/http'
import { reportError } from '../log-report'
@@ -48,6 +48,14 @@ export class CliWebPlatformAdapter implements PlatformAdapter {
return { success: false, error: 'Not available in CLI Web' }
}
async getDesktopCloseBehavior(): Promise<DesktopCloseBehavior> {
return 'ask'
}
async setDesktopCloseBehavior(_behavior: DesktopCloseBehavior): Promise<{ success: boolean; error?: string }> {
return { success: false, error: 'Not available in CLI Web' }
}
async getAnalyticsEnabled(): Promise<boolean> {
try {
const resp = await fetchWithAuth('/_web/telemetry/enabled')
+9 -1
View File
@@ -2,7 +2,7 @@
* ElectronPlatformAdapter — wrap window.api
*/
import type { AnalyticsEventName } from '@openchatlab/shared-types'
import type { AnalyticsEventName, DesktopCloseBehavior } from '@openchatlab/shared-types'
import type { PlatformAdapter, OpenDialogOptions, OpenDialogResult, RemoteConfigResult } from './types'
export class ElectronPlatformAdapter implements PlatformAdapter {
@@ -26,6 +26,14 @@ export class ElectronPlatformAdapter implements PlatformAdapter {
return window.api.app.setOpenAtLogin(enabled)
}
getDesktopCloseBehavior(): Promise<DesktopCloseBehavior> {
return window.api.app.getDesktopCloseBehavior()
}
setDesktopCloseBehavior(behavior: DesktopCloseBehavior): Promise<{ success: boolean; error?: string }> {
return window.api.app.setDesktopCloseBehavior(behavior)
}
getAnalyticsEnabled(): Promise<boolean> {
return window.api.app.getAnalyticsEnabled()
}
+3 -1
View File
@@ -1,4 +1,4 @@
import type { AnalyticsEventName } from '@openchatlab/shared-types'
import type { AnalyticsEventName, DesktopCloseBehavior } from '@openchatlab/shared-types'
/**
* PlatformAdapter — 平台能力领域适配器接口
@@ -46,6 +46,8 @@ export interface PlatformAdapter {
getOpenAtLogin(): Promise<boolean>
setOpenAtLogin(enabled: boolean): Promise<{ success: boolean; error?: string }>
getDesktopCloseBehavior(): Promise<DesktopCloseBehavior>
setDesktopCloseBehavior(behavior: DesktopCloseBehavior): Promise<{ success: boolean; error?: string }>
getAnalyticsEnabled(): Promise<boolean>
setAnalyticsEnabled(enabled: boolean): Promise<{ success: boolean }>