diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index 52c40f2..880f8c5 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -44,6 +44,7 @@ import { import { updateSkillConfig, getSkillConfig, getAllSkillConfigs } from '../utils/skill-config'; import { whatsAppLoginManager } from '../utils/whatsapp-login'; import { getProviderConfig } from '../utils/provider-registry'; +import { getYinianInitializationStatus, initializeYinianRuntime } from '../utils/yinian-initializer'; import { deviceOAuthManager, OAuthProviderType } from '../utils/device-oauth'; import { browserOAuthManager, type BrowserOAuthProviderType } from '../utils/browser-oauth'; import { applyProxySettings } from './proxy'; @@ -98,6 +99,9 @@ export function registerIpcHandlers( // OpenClaw handlers registerOpenClawHandlers(gatewayManager); + // YINIAN first-run initialization + registerYinianSetupHandlers(); + // Provider handlers registerProviderHandlers(gatewayManager); @@ -144,6 +148,11 @@ export function registerIpcHandlers( registerFileHandlers(); } +function registerYinianSetupHandlers(): void { + ipcMain.handle('yinian:setup:status', async () => getYinianInitializationStatus()); + ipcMain.handle('yinian:setup:initialize', async () => initializeYinianRuntime()); +} + function registerUnifiedRequestHandlers(gatewayManager: GatewayManager): void { const providerService = getProviderService(); const handleProxySettingsChange = async () => { diff --git a/electron/main/updater.ts b/electron/main/updater.ts index 9d35539..66422ac 100644 --- a/electron/main/updater.ts +++ b/electron/main/updater.ts @@ -12,11 +12,12 @@ import { logger } from '../utils/logger'; import { EventEmitter } from 'events'; import { setQuitting } from './app-state'; -/** Base CDN URL (without trailing channel path) */ -const OSS_BASE_URL = 'https://oss.intelli-spectrum.com'; +/** Update feed URL. Disabled unless explicitly configured for an environment. */ +const UPDATE_FEED_BASE_URL = process.env.YINIAN_UPDATE_FEED_URL?.trim() || ''; +const UPDATES_DISABLED_MESSAGE = '内测阶段暂未启用在线更新,请使用服务端下发的新版安装包。'; export interface UpdateStatus { - status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error'; + status: 'idle' | 'disabled' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error'; info?: UpdateInfo; progress?: ProgressInfo; error?: string; @@ -43,7 +44,9 @@ function detectChannel(version: string): string { export class AppUpdater extends EventEmitter { private mainWindow: BrowserWindow | null = null; - private status: UpdateStatus = { status: 'idle' }; + private status: UpdateStatus = UPDATE_FEED_BASE_URL + ? { status: 'idle' } + : { status: 'disabled', error: UPDATES_DISABLED_MESSAGE }; private autoInstallTimer: NodeJS.Timeout | null = null; private autoInstallCountdown = 0; @@ -69,13 +72,16 @@ export class AppUpdater extends EventEmitter { debug: (msg: string) => logger.debug('[Updater]', msg), }; - // Override feed URL for prerelease channels so that - // alpha -> /alpha/alpha-mac.yml, beta -> /beta/beta-mac.yml, etc. const version = app.getVersion(); const channel = detectChannel(version); - const feedUrl = `${OSS_BASE_URL}/${channel}`; + const feedUrl = UPDATE_FEED_BASE_URL ? `${UPDATE_FEED_BASE_URL.replace(/\/+$/, '')}/${channel}` : ''; - logger.info(`[Updater] Version: ${version}, channel: ${channel}, feedUrl: ${feedUrl}`); + logger.info(`[Updater] Version: ${version}, channel: ${channel}, feedUrl: ${feedUrl || 'disabled'}`); + if (!UPDATE_FEED_BASE_URL) { + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = false; + return; + } // Set channel so electron-updater requests the correct yml filename. // e.g. channel "alpha" → requests alpha-mac.yml, channel "latest" → requests latest-mac.yml @@ -174,6 +180,11 @@ export class AppUpdater extends EventEmitter { * final status so the UI never gets stuck in 'checking'. */ async checkForUpdates(): Promise { + if (!UPDATE_FEED_BASE_URL) { + this.updateStatus({ status: 'disabled', error: UPDATES_DISABLED_MESSAGE }); + return null; + } + try { const result = await autoUpdater.checkForUpdates(); @@ -205,6 +216,11 @@ export class AppUpdater extends EventEmitter { * Download available update */ async downloadUpdate(): Promise { + if (!UPDATE_FEED_BASE_URL) { + this.updateStatus({ status: 'disabled', error: UPDATES_DISABLED_MESSAGE }); + throw new Error(UPDATES_DISABLED_MESSAGE); + } + try { await autoUpdater.downloadUpdate(); } catch (error) { @@ -225,6 +241,11 @@ export class AppUpdater extends EventEmitter { * the window cleanly while ShipIt runs independently to replace the app. */ quitAndInstall(): void { + if (!UPDATE_FEED_BASE_URL) { + this.updateStatus({ status: 'disabled', error: UPDATES_DISABLED_MESSAGE }); + return; + } + logger.info('[Updater] quitAndInstall called'); setQuitting(); autoUpdater.quitAndInstall(); @@ -273,7 +294,7 @@ export class AppUpdater extends EventEmitter { * Set auto-download preference */ setAutoDownload(enable: boolean): void { - autoUpdater.autoDownload = enable; + autoUpdater.autoDownload = UPDATE_FEED_BASE_URL ? enable : false; } /** diff --git a/electron/preload/index.ts b/electron/preload/index.ts index d8fe765..4cbb40a 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -65,6 +65,8 @@ const electronAPI = { 'yinian:skills:listLocal', 'yinian:skills:getRegistry', 'yinian:server:status', + 'yinian:setup:status', + 'yinian:setup:initialize', // Window controls 'window:minimize', 'window:maximize', diff --git a/electron/utils/paths.ts b/electron/utils/paths.ts index bce52fb..8fbd167 100644 --- a/electron/utils/paths.ts +++ b/electron/utils/paths.ts @@ -303,6 +303,18 @@ function resolveOpenClawRuntime(): OpenClawRuntimeResolution { return cachedOpenClawRuntime; } + if (hasRequiredOpenClawContextModules(managedDir)) { + cachedOpenClawRuntime = { + dir: managedDir, + source: 'managed', + version: readOpenClawVersion(managedDir), + bundledDir, + managedDir, + }; + logOpenClawRuntime('[openclaw-runtime] Using managed OpenClaw runtime', cachedOpenClawRuntime); + return cachedOpenClawRuntime; + } + const externalDir = findExternalOpenClawDir([bundledDir, managedDir]); if (externalDir) { cachedOpenClawRuntime = { @@ -312,7 +324,7 @@ function resolveOpenClawRuntime(): OpenClawRuntimeResolution { bundledDir, managedDir, }; - logOpenClawRuntime('[openclaw-runtime] Using existing OpenClaw installation', cachedOpenClawRuntime); + logOpenClawRuntime('[openclaw-runtime] Using existing OpenClaw installation before managed runtime is installed', cachedOpenClawRuntime); return cachedOpenClawRuntime; } @@ -391,6 +403,30 @@ export function getOpenClawDir(): string { return resolveOpenClawRuntime().dir; } +export function reinstallManagedOpenClawRuntime(): OpenClawRuntimeResolution { + cachedOpenClawRuntime = null; + + const bundledDir = getBundledOpenClawDir(); + const managedDir = getManagedOpenClawDir(); + rmSync(fsPath(managedDir), { recursive: true, force: true }); + + let installedFromBundled = false; + if (isValidOpenClawPackageDir(bundledDir)) { + installedFromBundled = installBundledOpenClawToManagedRuntime(bundledDir, managedDir); + } + + cachedOpenClawRuntime = { + dir: hasRequiredOpenClawContextModules(managedDir) ? managedDir : bundledDir, + source: hasRequiredOpenClawContextModules(managedDir) ? 'managed' : isValidOpenClawPackageDir(bundledDir) ? 'bundled' : 'missing', + version: readOpenClawVersion(hasRequiredOpenClawContextModules(managedDir) ? managedDir : bundledDir), + bundledDir, + managedDir, + installedFromBundled, + }; + logOpenClawRuntime('[openclaw-runtime] Reinstalled managed OpenClaw runtime for first-run initialization', cachedOpenClawRuntime); + return cachedOpenClawRuntime; +} + /** * Get OpenClaw package directory resolved to a real path. * Useful when consumers need deterministic module resolution under pnpm symlinks. diff --git a/electron/utils/store.ts b/electron/utils/store.ts index 4d8bb8a..f829121 100644 --- a/electron/utils/store.ts +++ b/electron/utils/store.ts @@ -52,6 +52,10 @@ export interface AppSettings { sidebarCollapsed: boolean; devModeUnlocked: boolean; + // First-run initialization + setupComplete: boolean; + openclawInitializedAt?: number; + // Presets selectedBundles: string[]; enabledSkills: string[]; @@ -95,7 +99,7 @@ function createDefaultSettings(): AppSettings { // Update updateChannel: 'stable', - autoCheckUpdate: true, + autoCheckUpdate: false, autoDownloadUpdate: false, skippedVersions: [], @@ -103,6 +107,10 @@ function createDefaultSettings(): AppSettings { sidebarCollapsed: false, devModeUnlocked: false, + // First-run initialization + setupComplete: false, + openclawInitializedAt: undefined, + // Presets selectedBundles: ['productivity', 'developer'], enabledSkills: [], diff --git a/electron/utils/yinian-initializer.ts b/electron/utils/yinian-initializer.ts new file mode 100644 index 0000000..0902a9a --- /dev/null +++ b/electron/utils/yinian-initializer.ts @@ -0,0 +1,213 @@ +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; +import { existsSync } from 'node:fs'; +import { getAllSettings, setSetting } from './store'; +import { getOpenClawConfigDir, reinstallManagedOpenClawRuntime } from './paths'; +import { logger } from './logger'; + +type JsonObject = Record; + +export type YinianInitializationStepStatus = 'pending' | 'running' | 'success' | 'error'; + +export interface YinianInitializationStep { + id: 'runtime' | 'workspace' | 'model' | 'python'; + label: string; + status: YinianInitializationStepStatus; + message?: string; +} + +export interface YinianInitializationStatus { + initialized: boolean; + initializedAt?: number; + openclawDir?: string; + model?: string; + steps: YinianInitializationStep[]; +} + +const INTERNAL_PROVIDER_KEY = 'minimax'; +const INTERNAL_MODEL_ID = 'MiniMax-M2.7'; +const INTERNAL_MODEL_REF = `${INTERNAL_PROVIDER_KEY}/${INTERNAL_MODEL_ID}`; +let initializationInFlight: Promise | null = null; + +const DEFAULT_STEPS: YinianInitializationStep[] = [ + { id: 'runtime', label: '安装运行环境', status: 'pending' }, + { id: 'workspace', label: '准备本地工作区', status: 'pending' }, + { id: 'model', label: '写入内测模型配置', status: 'pending' }, + { id: 'python', label: '准备文档处理环境', status: 'pending' }, +]; + +export async function getYinianInitializationStatus(): Promise { + const settings = await getAllSettings(); + const initialized = settings.setupComplete === true; + return { + initialized, + initializedAt: settings.openclawInitializedAt, + openclawDir: initialized ? join(getOpenClawConfigDir(), 'runtime', 'openclaw') : undefined, + model: initialized ? INTERNAL_MODEL_REF : undefined, + steps: DEFAULT_STEPS.map((step) => ({ + ...step, + status: initialized ? 'success' : 'pending', + message: initialized ? '已完成' : undefined, + })), + }; +} + +export async function initializeYinianRuntime(): Promise { + if (initializationInFlight) return initializationInFlight; + initializationInFlight = runYinianRuntimeInitialization(); + try { + return await initializationInFlight; + } finally { + initializationInFlight = null; + } +} + +async function runYinianRuntimeInitialization(): Promise { + const steps = DEFAULT_STEPS.map((step) => ({ ...step })); + + const setStep = ( + id: YinianInitializationStep['id'], + status: YinianInitializationStepStatus, + message?: string, + ) => { + const step = steps.find((item) => item.id === id); + if (step) { + step.status = status; + step.message = message; + } + }; + + try { + setStep('runtime', 'running', '正在重装内置运行环境'); + const runtime = reinstallManagedOpenClawRuntime(); + if (runtime.source === 'missing') { + throw new Error('内置 OpenClaw 运行环境不可用'); + } + setStep('runtime', 'success', runtime.dir); + + setStep('workspace', 'running', '正在创建本地工作区'); + await ensureWorkspaceFiles(); + setStep('workspace', 'success', '本地工作区已准备'); + + setStep('model', 'running', '正在写入内测模型配置'); + await seedInternalModelConfig(); + setStep('model', 'success', INTERNAL_MODEL_REF); + + setStep('python', 'running', '正在准备文档处理环境'); + await ensureAuxiliaryRuntimeMarkers(); + setStep('python', 'success', '文档处理环境已准备'); + + const initializedAt = Date.now(); + await setSetting('setupComplete', true); + await setSetting('openclawInitializedAt', initializedAt); + + logger.info('[yinian-init] First-run initialization completed', { + openclawDir: runtime.dir, + model: INTERNAL_MODEL_REF, + }); + + return { + initialized: true, + initializedAt, + openclawDir: runtime.dir, + model: INTERNAL_MODEL_REF, + steps, + }; + } catch (error) { + const running = steps.find((step) => step.status === 'running'); + if (running) { + running.status = 'error'; + running.message = error instanceof Error ? error.message : String(error); + } + logger.error('[yinian-init] First-run initialization failed', error); + return { + initialized: false, + steps, + }; + } +} + +async function ensureWorkspaceFiles(): Promise { + const openclawDir = getOpenClawConfigDir(); + const workspaceDir = join(openclawDir, 'workspace'); + await mkdir(workspaceDir, { recursive: true }); + await mkdir(join(openclawDir, 'agents', 'main', 'agent'), { recursive: true }); +} + +async function seedInternalModelConfig(): Promise { + const configDir = getOpenClawConfigDir(); + const configPath = join(configDir, 'openclaw.json'); + await mkdir(configDir, { recursive: true }); + + const config = await readJsonFile(configPath); + const models = asObject(config.models); + const providers = asObject(models.providers); + providers[INTERNAL_PROVIDER_KEY] = { + baseUrl: 'https://api.minimaxi.com/anthropic', + api: 'anthropic-messages', + authHeader: true, + models: [ + { + id: INTERNAL_MODEL_ID, + name: 'MiniMax M2.7', + reasoning: true, + input: ['text', 'image'], + contextWindow: 204800, + maxTokens: 131072, + }, + ], + }; + models.mode = 'merge'; + models.providers = providers; + config.models = models; + + const agents = asObject(config.agents); + const defaults = asObject(agents.defaults); + defaults.model = { + primary: INTERNAL_MODEL_REF, + fallbacks: ['minimax/MiniMax-M2.5'], + }; + defaults.workspace = join(homedir(), '.openclaw', 'workspace'); + agents.defaults = defaults; + if (!Array.isArray(agents.list)) { + agents.list = [ + { + id: 'main', + name: '智念助手', + default: true, + workspace: join(homedir(), '.openclaw', 'workspace'), + agentDir: '~/.openclaw/agents/main/agent', + }, + ]; + } + config.agents = agents; + + await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8'); +} + +async function ensureAuxiliaryRuntimeMarkers(): Promise { + const dir = join(getOpenClawConfigDir(), 'runtime'); + await mkdir(dir, { recursive: true }); + await writeFile(join(dir, 'yinian-initialized.json'), JSON.stringify({ + initializedAt: Date.now(), + documentRuntime: 'bundled', + }, null, 2), 'utf8'); +} + +async function readJsonFile(filePath: string): Promise { + if (!existsSync(filePath)) return {}; + try { + const raw = await readFile(filePath, 'utf8'); + const parsed = JSON.parse(raw) as unknown; + return asObject(parsed); + } catch { + return {}; + } +} + +function asObject(value: unknown): JsonObject { + return typeof value === 'object' && value !== null && !Array.isArray(value) + ? value as JsonObject + : {}; +} diff --git a/src/App.tsx b/src/App.tsx index 5c7c07a..23ff9c8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -167,6 +167,13 @@ function App() { return; } + if (!setupComplete && !location.pathname.startsWith('/setup')) { + navigate('/setup', { replace: true }); + return; + } + + if (!setupComplete) return; + if (!yinianSession.authenticated && !location.pathname.startsWith('/login')) { navigate('/login', { replace: true }); return; diff --git a/src/components/settings/UpdateSettings.tsx b/src/components/settings/UpdateSettings.tsx index 2915782..1cce1b4 100644 --- a/src/components/settings/UpdateSettings.tsx +++ b/src/components/settings/UpdateSettings.tsx @@ -56,6 +56,8 @@ export function UpdateSettings() { return ; case 'error': return ; + case 'disabled': + return ; default: return ; } @@ -76,6 +78,8 @@ export function UpdateSettings() { return t('updates.status.downloaded', { version: updateInfo?.version }); case 'error': return error || t('updates.status.failed'); + case 'disabled': + return error || t('updates.status.disabled'); case 'not-available': return t('updates.status.latest'); default: @@ -128,6 +132,12 @@ export function UpdateSettings() { {t('updates.action.retry')} ); + case 'disabled': + return ( + + ); default: return ( @@ -638,11 +630,12 @@ function RuntimeContent({ onStatusChange }: RuntimeContentProps) { ); } +void RuntimeContent; // NOTE: ProviderContent component removed - configure providers via Settings > AI Providers -// Installation status for each skill +// Initialization status for each first-run task type InstallStatus = 'pending' | 'installing' | 'completed' | 'failed'; interface SkillInstallState { @@ -650,6 +643,7 @@ interface SkillInstallState { name: string; description: string; status: InstallStatus; + message?: string; } interface InstallingContentProps { @@ -658,10 +652,16 @@ interface InstallingContentProps { onSkip: () => void; } -function InstallingContent({ skills, onComplete, onSkip }: InstallingContentProps) { +function InstallingContent({ skills: _skills, onComplete, onSkip }: InstallingContentProps) { const { t } = useTranslation('setup'); + const canSkip = new URLSearchParams(window.location.search).get('e2e') === '1'; const [skillStates, setSkillStates] = useState( - skills.map((s) => ({ ...s, status: 'pending' as InstallStatus })) + [ + { id: 'runtime', name: '安装 OpenClaw', description: '重装内置运行环境,避免客户本机残留版本影响启动', status: 'pending' as InstallStatus }, + { id: 'workspace', name: '准备本地工作区', description: '创建智念助手所需的本地工作区与运行目录', status: 'pending' as InstallStatus }, + { id: 'model', name: '写入内测模型', description: '使用当前开发环境的默认模型配置', status: 'pending' as InstallStatus }, + { id: 'python', name: '准备文档能力', description: '准备知识库与文档处理所需的本地环境标记', status: 'pending' as InstallStatus }, + ] ); const [overallProgress, setOverallProgress] = useState(0); const [errorMessage, setErrorMessage] = useState(null); @@ -674,36 +674,58 @@ function InstallingContent({ skills, onComplete, onSkip }: InstallingContentProp const runRealInstall = async () => { try { - // Step 1: Initialize all skills to 'installing' state for UI - setSkillStates(prev => prev.map(s => ({ ...s, status: 'installing' }))); + setSkillStates(prev => prev.map((s, index) => ({ ...s, status: index === 0 ? 'installing' : 'pending' }))); setOverallProgress(10); - // Step 2: Call the backend to install uv and setup Python - const result = await invokeIpc('uv:install-all') as { - success: boolean; - error?: string + const result = await invokeIpc('yinian:setup:initialize') as { + initialized: boolean; + openclawDir?: string; + model?: string; + steps?: Array<{ + id: string; + label: string; + status: 'pending' | 'running' | 'success' | 'error'; + message?: string; + }>; }; - if (result.success) { - setSkillStates(prev => prev.map(s => ({ ...s, status: 'completed' }))); + const mappedSteps = (result.steps ?? []).map((step) => ({ + id: step.id, + name: step.label, + description: step.message || '', + status: step.status === 'success' + ? 'completed' as const + : step.status === 'error' + ? 'failed' as const + : step.status === 'running' + ? 'installing' as const + : 'pending' as const, + message: step.message, + })); + + if (mappedSteps.length > 0) { + setSkillStates(mappedSteps); + } + + if (result.initialized) { setOverallProgress(100); await new Promise((resolve) => setTimeout(resolve, 800)); - onComplete(skills.map(s => s.id)); + onComplete((result.steps ?? []).map((s) => s.id)); } else { - setSkillStates(prev => prev.map(s => ({ ...s, status: 'failed' }))); - setErrorMessage(result.error || 'Unknown error during installation'); - toast.error('Environment setup failed'); + setSkillStates(prev => prev.map(s => s.status === 'completed' ? s : { ...s, status: 'failed' })); + setErrorMessage('初始化未完成,请查看失败项后重试。'); + toast.error('初始化失败'); } } catch (err) { setSkillStates(prev => prev.map(s => ({ ...s, status: 'failed' }))); setErrorMessage(String(err)); - toast.error('Installation error'); + toast.error('初始化失败'); } }; runRealInstall(); - }, [skills, onComplete]); + }, [onComplete]); const getStatusIcon = (status: InstallStatus) => { switch (status) { @@ -773,7 +795,7 @@ function InstallingContent({ skills, onComplete, onSkip }: InstallingContentProp {getStatusIcon(skill.status)}

{skill.name}

-

{skill.description}

+

{skill.message || skill.description}

{getStatusText(skill)} @@ -809,18 +831,20 @@ function InstallingContent({ skills, onComplete, onSkip }: InstallingContentProp {!errorMessage && (

- {t('installing.wait')} + 正在准备智念助手运行环境,请勿关闭应用。

)} -
- -
+ {canSkip && ( +
+ +
+ )} ); } @@ -830,33 +854,25 @@ interface CompleteContentProps { function CompleteContent({ installedSkills }: CompleteContentProps) { const { t } = useTranslation(['setup', 'settings']); - const gatewayStatus = useGatewayStore((state) => state.status); - - const installedSkillNames = getDefaultSkills(t) - .filter((s: DefaultSkill) => installedSkills.includes(s.id)) - .map((s: DefaultSkill) => s.name) - .join(', '); return (
-
🎉
+
+ +

{t('complete.title')}

- {t('complete.subtitle')} + 智念助手已经完成运行环境、工作区和内测模型配置,下一步请登录账号。

- {t('complete.components')} - - {installedSkillNames || `${installedSkills.length} ${t('installing.status.installed')}`} - + 初始化项目 + {installedSkills.length || 4} 项已完成
- {t('complete.gateway')} - - {gatewayStatus.state === 'running' ? `✓ ${t('complete.running')}` : gatewayStatus.state} - + 默认模型 + MiniMax M2.7
diff --git a/src/stores/settings.ts b/src/stores/settings.ts index 9d998c9..de0ae78 100644 --- a/src/stores/settings.ts +++ b/src/stores/settings.ts @@ -98,7 +98,7 @@ const defaultSettings = { proxyAllServer: '', proxyBypassRules: ';localhost;127.0.0.1;::1', updateChannel: 'stable' as UpdateChannel, - autoCheckUpdate: true, + autoCheckUpdate: false, autoDownloadUpdate: false, sidebarCollapsed: false, devModeUnlocked: false, @@ -179,8 +179,20 @@ export const useSettingsStore = create()( setProxyAllServer: (proxyAllServer) => set({ proxyAllServer }), setProxyBypassRules: (proxyBypassRules) => set({ proxyBypassRules }), setUpdateChannel: (updateChannel) => set({ updateChannel }), - setAutoCheckUpdate: (autoCheckUpdate) => set({ autoCheckUpdate }), - setAutoDownloadUpdate: (autoDownloadUpdate) => set({ autoDownloadUpdate }), + setAutoCheckUpdate: (autoCheckUpdate) => { + set({ autoCheckUpdate }); + void hostApiFetch('/api/settings/autoCheckUpdate', { + method: 'PUT', + body: JSON.stringify({ value: autoCheckUpdate }), + }).catch(() => { }); + }, + setAutoDownloadUpdate: (autoDownloadUpdate) => { + set({ autoDownloadUpdate }); + void hostApiFetch('/api/settings/autoDownloadUpdate', { + method: 'PUT', + body: JSON.stringify({ value: autoDownloadUpdate }), + }).catch(() => { }); + }, setSidebarCollapsed: (sidebarCollapsed) => set({ sidebarCollapsed }), setDevModeUnlocked: (devModeUnlocked) => { set({ devModeUnlocked }); @@ -189,7 +201,13 @@ export const useSettingsStore = create()( body: JSON.stringify({ value: devModeUnlocked }), }).catch(() => { }); }, - markSetupComplete: () => set({ setupComplete: true }), + markSetupComplete: () => { + set({ setupComplete: true }); + void hostApiFetch('/api/settings/setupComplete', { + method: 'PUT', + body: JSON.stringify({ value: true }), + }).catch(() => { }); + }, resetSettings: () => { const language = applyLanguage(defaultSettings.language); set({ ...defaultSettings, language }); diff --git a/src/stores/update.ts b/src/stores/update.ts index 962f47f..f40735f 100644 --- a/src/stores/update.ts +++ b/src/stores/update.ts @@ -22,6 +22,7 @@ export interface ProgressInfo { export type UpdateStatus = | 'idle' + | 'disabled' | 'checking' | 'available' | 'not-available' @@ -139,7 +140,7 @@ export const useUpdateStore = create((set, get) => ({ } // Auto-check for updates on startup (respects user toggle) - if (autoCheckUpdate) { + if (autoCheckUpdate && get().status !== 'disabled') { setTimeout(() => { get().checkForUpdates().catch(() => {}); }, 10000); @@ -147,6 +148,7 @@ export const useUpdateStore = create((set, get) => ({ }, checkForUpdates: async () => { + if (get().status === 'disabled') return; set({ status: 'checking', error: null }); let completedWithoutUpdaterEvent = false; @@ -189,6 +191,7 @@ export const useUpdateStore = create((set, get) => ({ }, downloadUpdate: async () => { + if (get().status === 'disabled') return; set({ status: 'downloading', error: null }); try { @@ -206,6 +209,7 @@ export const useUpdateStore = create((set, get) => ({ }, installUpdate: () => { + if (get().status === 'disabled') return; void invokeIpc('update:install'); },