import React, { useState, useEffect, useRef, useMemo, useCallback, createContext, useContext, } from "react"; import { ConfigProvider, Spin, Button, Modal, notification, message, } from "antd"; import { ArrowLeftOutlined, ReloadOutlined } from "@ant-design/icons"; import { setupService, authService, Step1Config, DEFAULT_STEP1_CONFIG, } from "./services/core/setup"; import { syncConfigToServer, normalizeServerHost, loginAndRegister, logout, isLoggedIn as checkIsLoggedIn, } from "./services/core/auth"; import { AUTH_KEYS, normalizeAgentEngine } from "@shared/constants"; import type { QuickInitConfig } from "@shared/types/quickInit"; import { t, getCurrentLang } from "./services/core/i18n"; import SetupWizard from "./components/setup/SetupWizard"; import LoginPage from "./components/setup/LoginPage"; import SetupDependencies from "./components/setup/SetupDependencies"; import ClientPage from "./components/pages/ClientPage"; import SettingsPage from "./components/pages/SettingsPage"; import DependenciesPage from "./components/pages/DependenciesPage"; import AboutPage from "./components/pages/AboutPage"; import LogViewer from "./components/pages/LogViewer"; import PermissionsPage from "./components/pages/PermissionsPage"; import SessionsPage from "./components/pages/SessionsPage"; import MCPSettings from "./components/settings/MCPSettings"; import AppSidebar, { NavKey, LegacyTabKey, } from "./components/layout/AppSidebar"; import type { WebviewHeaderActions } from "./components/pages/SessionsPage"; import { createLogger } from "./services/utils/rendererLog"; import styles from "./styles/components/App.module.css"; import { lightTheme, darkTheme } from "./styles/theme"; import { FEATURES } from "@shared/featureFlags"; // 主题类型 export type ThemeMode = "light" | "dark" | "system"; // 主题 Context interface ThemeContextValue { themeMode: ThemeMode; isDarkMode: boolean; setThemeMode: (mode: ThemeMode) => void; } export const ThemeContext = createContext(null); // Hook to use theme context export function useTheme(): ThemeContextValue { const context = useContext(ThemeContext); if (!context) { throw new Error("useTheme must be used within App component"); } return context; } // i18n 语言 Context interface I18nContextValue { lang: string; updateLang: (lang: string) => void; } export const I18nContext = createContext(null); export function useI18nLang(): I18nContextValue { const context = useContext(I18nContext); if (!context) { throw new Error("useI18nLang must be used within App component"); } return context; } // Tab 类型定义(对齐 Tauri 客户端) type TabKey = | "client" | "sessions" | "mcp" | "settings" | "dependencies" | "permissions" | "logs" | "about"; // 服务状态接口(与 ClientPage 共享) export interface ServiceItem { key: string; label: string; description: string; running: boolean; pid?: number; port?: number; error?: string; } /** * 将 quick init 配置静默写入 DB(覆盖旧值) * 用于 setup 已完成时,每次启动优先使用配置文件/环境变量中的值 */ async function applyQuickInitToDb(config: QuickInitConfig): Promise { // 1. 更新 step1 配置 const step1: Step1Config = { ...DEFAULT_STEP1_CONFIG, serverHost: normalizeServerHost(config.serverHost), agentPort: config.agentPort, fileServerPort: config.fileServerPort, workspaceDir: config.workspaceDir, }; await setupService.saveStep1Config(step1); // 2. 更新 savedKey const domain = normalizeServerHost(config.serverHost); await window.electronAPI?.settings.set(AUTH_KEYS.SAVED_KEY, config.savedKey); if (config.username) { try { const domainKey = `${AUTH_KEYS.SAVED_KEYS_PREFIX}${new URL(domain).hostname}_${config.username}`; await window.electronAPI?.settings.set(domainKey, config.savedKey); } catch { // domain 解析失败时跳过域名级 savedKey 存储 } } // 3. 静默重新注册(更新服务端设备信息) try { await loginAndRegister(config.username, "", { suppressToast: true, domain, }); } catch (error) { // 注册失败不阻塞启动,已有的 auth 信息仍可用 console.warn("[App] Quick init silent registration failed:", error); } } function App() { // ============================================ // 初始化向导状态 // ============================================ const [isSetupComplete, setIsSetupComplete] = useState(null); const [isLoggedIn, setIsLoggedIn] = useState(null); const [loginStep1Config, setLoginStep1Config] = useState(DEFAULT_STEP1_CONFIG); const setupJustCompleted = useRef(false); // 内存变量:标记服务是否由登录流程启动(不持久化) const loginStartedRef = useRef(false); // ============================================ // 主题状态 // ============================================ const [themeMode, setThemeMode] = useState("system"); const [systemIsDark, setSystemIsDark] = useState(false); // 计算实际使用的主题 const isDarkMode = useMemo(() => { if (themeMode === "system") { return systemIsDark; } return themeMode === "dark"; }, [themeMode, systemIsDark]); const currentTheme = useMemo( () => (isDarkMode ? darkTheme : lightTheme), [isDarkMode], ); // ============================================ // i18n 语言状态(响应式,供 Context 下发) // ============================================ const [i18nLang, setI18nLang] = useState(getCurrentLang()); const handleI18nLangChange = useCallback((lang: string) => { setI18nLang(lang); }, []); // 监听系统主题变化 useEffect(() => { const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); setSystemIsDark(mediaQuery.matches); const handler = (e: MediaQueryListEvent) => setSystemIsDark(e.matches); mediaQuery.addEventListener("change", handler); return () => mediaQuery.removeEventListener("change", handler); }, []); // 加载保存的主题设置 useEffect(() => { const loadThemeSetting = async () => { try { const saved = (await window.electronAPI?.settings.get( "theme_mode", )) as ThemeMode | null; if (saved && ["light", "dark", "system"].includes(saved)) { setThemeMode(saved); } } catch (e) { console.warn("[App] Failed to load theme settings:", e); } }; loadThemeSetting(); }, []); // 保存主题设置 const handleSetThemeMode = useCallback(async (mode: ThemeMode) => { setThemeMode(mode); try { await window.electronAPI?.settings.set("theme_mode", mode); } catch (e) { console.warn("[App] Failed to save theme settings:", e); } }, []); // 应用主题到 body useEffect(() => { document.body.setAttribute("data-theme", isDarkMode ? "dark" : "light"); }, [isDarkMode]); /** * 主界面下「必需依赖未完全安装」时是否强制进入依赖安装流程。 * - null: 进入主界面后尚未完成检查 * - true: 存在 missing/error 的必需依赖,全屏显示依赖安装,完成后回到主界面 * - false: 必需依赖均已安装(含 outdated,以当前真实安装版本为准,不强制重装) */ const [needsRequiredDepsReinstall, setNeedsRequiredDepsReinstall] = useState< boolean | null >(null); /** 主进程初始化依赖同步是否仍在进行(客户端升级后后台安装新版本依赖) */ const [depsSyncInProgress, setDepsSyncInProgress] = useState(false); // 启动日志:便于快速确认渲染进程 feature flags 是否生效 useEffect(() => { console.info("[FeatureFlags][renderer]", FEATURES); window.electronAPI?.log .write("info", "[FeatureFlags][renderer]", FEATURES) .catch(() => {}); }, []); /** * 重启所有服务(使新安装的依赖/二进制生效)。 * restartAll 内部已包含停止逻辑,无需额外调用 stopAll。 * * 重启前先调 reg 接口,将本次返回的最新 serverHost/serverPort 写入配置, * 确保 lanproxy 使用最新服务端地址,而不是 SQLite 里的旧缓存值。 */ const restartAllServices = useCallback(async () => { try { // 先 reg 拿最新 serverHost/serverPort 写入配置,成功后再重启服务。 // reg 失败(网络不通/token 过期)时中止重启,并弹出通知让用户手动重试。 await syncConfigToServer({ suppressToast: true }); } catch (e) { console.error("[App] Reg sync failed, aborting service restart:", e); const notifKey = "restartRegFailed"; notification.error({ key: notifKey, message: t("Claw.App.ConfigSyncFailed"), description: t("Claw.App.ConfigSyncFailedDetail"), duration: 0, placement: "bottomRight", btn: ( ), }); return; } try { message.loading({ content: t("Claw.App.RestartingServices"), key: "restart-services", }); await window.electronAPI?.services.restartAll(); message.success({ content: t("Claw.App.RestartSuccess"), key: "restart-services", }); } catch (e) { console.error("[App] Failed to restart services:", e); message.error({ content: t("Claw.App.RestartFailed"), key: "restart-services", }); } }, []); // ============================================ // 核心状态 // ============================================ const [activeTab, setActiveTab] = useState("client"); const [activeNavKey, setActiveNavKey] = useState("overview"); const [sessionsAutoOpen, setSessionsAutoOpen] = useState(false); const [webviewActions, setWebviewActions] = useState(null); const [username, setUsername] = useState(""); const [userEmail, setUserEmail] = useState(""); const [onlineStatus, setOnlineStatus] = useState(null); const [services, setServices] = useState([]); const [servicesLoading, setServicesLoading] = useState(true); const [startingServices, setStartingServices] = useState>( new Set(), ); const servicesPollTimer = useRef | null>(null); /** 递增后通知 ClientPage 刷新账号状态(用户名等),与 reg 返回保持一致 */ const [authRefreshTrigger, setAuthRefreshTrigger] = useState(0); const getStartupServiceKeys = useCallback(async (): Promise => { const keys = ["mcpProxy", "agent", "fileServer", "lanproxy"]; if (!FEATURES.ENABLE_GUI_AGENT_SERVER) return keys; try { const guiEnabledRes = await window.electronAPI?.guiServer?.isEnabled(); if (guiEnabledRes?.enabled) { keys.splice(3, 0, "guiServer"); } } catch (e) { console.warn("[App] Failed to read GUI MCP enabled status:", e); } return keys; }, []); // ============================================ // 检查初始化向导状态(每次启动优先读取 quick init 配置) // ============================================ useEffect(() => { const log = createLogger("SetupCheck"); const checkSetup = async () => { try { const completed = await setupService.isSetupCompleted(); const step1 = await setupService.getStep1Config(); setLoginStep1Config(step1); const loggedIn = await checkIsLoggedIn(); setIsLoggedIn(loggedIn); // 每次启动优先读取 quick init 配置 if (completed) { try { const qiConfig = await window.electronAPI?.quickInit.getConfig(); if (qiConfig) { log.info("Applying quick init config"); await applyQuickInitToDb(qiConfig); } } catch (error) { log.warn("Failed to read quick init config:", error); } } log.info("isSetupComplete:", completed); setIsSetupComplete(completed); } catch (error) { log.error("Failed to check setup status:", error); setIsSetupComplete(true); setIsLoggedIn(false); } }; checkSetup(); }, []); // ============================================ // 初始化主界面(setup 完成后执行) // ============================================ useEffect(() => { if (isSetupComplete !== true) return; const init = async () => { // 加载用户信息 const user = await authService.getAuthUser(); if (user) { setUsername( user.displayName || user.username || t("Claw.App.defaultUsername"), ); setUserEmail(user.email || user.phone || user.username || ""); } // 加载在线状态 const online = await window.electronAPI?.settings.get("auth.online_status"); setOnlineStatus(online as boolean | null); }; init(); }, [isSetupComplete]); // ============================================ // 子组件登录/注销后刷新顶部栏用户名 // ============================================ const handleAuthChange = useCallback(async () => { const user = await authService.getAuthUser(); const loggedIn = await checkIsLoggedIn(); setIsLoggedIn(loggedIn); if (user) { setUsername( user.displayName || user.username || t("Claw.App.defaultUsername"), ); setUserEmail(user.email || user.phone || user.username || ""); } else { setUsername(""); setUserEmail(""); } }, []); // 标记服务由登录流程启动(内存变量,不持久化) const handleLoginStarted = useCallback(() => { loginStartedRef.current = true; }, []); const ensureDefaultLoginConfig = useCallback(async () => { const currentConfig = await setupService.getStep1Config(); const nextConfig: Step1Config = { ...DEFAULT_STEP1_CONFIG, ...currentConfig, serverHost: currentConfig.serverHost || DEFAULT_STEP1_CONFIG.serverHost, workspaceDir: currentConfig.workspaceDir || DEFAULT_STEP1_CONFIG.workspaceDir, }; await setupService.saveStep1Config(nextConfig); setLoginStep1Config(nextConfig); return nextConfig; }, []); const handleFullscreenLoginSuccess = useCallback(async () => { await setupService.completeStep2(); await setupService.completeSetup(); setupJustCompleted.current = true; setIsSetupComplete(true); await handleAuthChange(); setAuthRefreshTrigger((v) => v + 1); }, [handleAuthChange]); // ============================================ // 主界面下必需依赖检查:仅当存在「未安装」或「错误」时进入依赖安装 // 版本以当前真实安装为准,outdated 不触发(用户可在依赖 Tab 手动升级) // 同时检测主进程初始化依赖同步状态,避免服务启动时依赖尚未安装完成 // ============================================ useEffect(() => { if (isSetupComplete !== true) return; const log = createLogger("DepsCheck"); let cancelled = false; // 先注册事件监听,再做 checkAll,避免事件在 checkAll 返回前触发而丢失 const handleDepsSyncCompleted = () => { log.info("syncCompleted"); setDepsSyncInProgress(false); }; window.electronAPI?.on( "deps:syncCompleted", handleDepsSyncCompleted as any, ); const checkRequiredDeps = async () => { try { const result = await window.electronAPI?.dependencies.checkAll(); if (cancelled) return; const deps = result?.results ?? []; const hasMissingOrError = deps.some( (d: { status: string; required?: boolean }) => d.required !== false && (d.status === "missing" || d.status === "error"), ); const missingDeps = deps .filter( (d: { status: string; required?: boolean }) => d.required !== false && (d.status === "missing" || d.status === "error"), ) .map((d: { name: string; status: string }) => d.name); log.info("result:", { hasMissingOrError, missingDeps: missingDeps.length > 0 ? missingDeps : undefined, syncInProgress: result?.syncInProgress, }); setNeedsRequiredDepsReinstall(hasMissingOrError); // 记录主进程初始化依赖同步状态 if (result?.syncInProgress) { setDepsSyncInProgress(true); // 防竞态:checkAll 返回 syncInProgress=true 但事件可能已经在 checkAll IPC 期间触发过了, // 再次确认主进程当前真实状态,避免 depsSyncInProgress 永远卡在 true const recheck = await window.electronAPI?.dependencies.checkAll(); if (cancelled) return; if (!recheck?.syncInProgress) { setDepsSyncInProgress(false); } } } catch (error) { log.error("failed:", error); if (!cancelled) { setNeedsRequiredDepsReinstall(false); } } }; checkRequiredDeps(); return () => { cancelled = true; window.electronAPI?.off( "deps:syncCompleted", handleDepsSyncCompleted as any, ); }; }, [isSetupComplete]); // ============================================ // 服务状态轮询 // ============================================ const pollServicesStatus = useCallback(async () => { try { const items: ServiceItem[] = []; const [ fsStatus, lpStatus, agentSvcStatus, mcpStatus, csStatus, guiStatus, guiEnabledRes, ] = await Promise.all([ window.electronAPI?.fileServer.status(), window.electronAPI?.lanproxy.status(), window.electronAPI?.agent.serviceStatus(), window.electronAPI?.mcp.status(), window.electronAPI?.computerServer.status(), window.electronAPI?.guiServer?.status(), window.electronAPI?.guiServer?.isEnabled(), ]); const isGuiEnabled = FEATURES.ENABLE_GUI_AGENT_SERVER && (guiEnabledRes?.enabled ?? false); items.push({ key: "mcpProxy", label: t("Claw.Service.mcp"), description: t("Claw.Service.mcpDesc"), running: mcpStatus?.running ?? false, error: mcpStatus?.error, }); // ComputerServer 是 Agent 的 HTTP 接口,仅当 Agent 本身在运行时才检查其状态 const agentRunning = agentSvcStatus?.running ?? false; const csRunning = csStatus?.running ?? false; let agentError: string | undefined; if (agentRunning && !csRunning) { agentError = csStatus?.error ? t("Claw.App.agentInterfaceFailed", csStatus.error) : t("Claw.App.agentInterfaceNotRunning"); } items.push({ key: "agent", label: t("Claw.Service.agent"), description: t("Claw.Service.agentDesc"), running: agentRunning && csRunning, error: agentError, }); items.push({ key: "fileServer", label: t("Claw.Service.file"), description: t("Claw.Service.fileDesc"), running: fsStatus?.running ?? false, pid: fsStatus?.pid, error: fsStatus?.error, }); if (isGuiEnabled) { items.push({ key: "guiServer", label: t("Claw.Service.guiMcp"), description: t("Claw.Service.guiMcpDesc"), running: guiStatus?.running ?? false, pid: guiStatus?.pid, error: guiStatus?.error, }); } items.push({ key: "lanproxy", label: t("Claw.Service.proxy"), description: t("Claw.Service.proxyDesc"), running: lpStatus?.running ?? false, pid: lpStatus?.pid, error: lpStatus?.error, }); setServices(items); } catch (error) { console.error("[App] pollServicesStatus failed:", error); } finally { setServicesLoading(false); } }, []); // ============================================ // 逐个启动服务(实时更新状态) // ============================================ const startServicesSequentially = useCallback( async (serviceKeys: string[]) => { const log = createLogger("StartServices"); for (const key of serviceKeys) { setStartingServices((prev) => new Set(prev).add(key)); try { let result: { success: boolean; error?: string } | undefined; if (key === "agent") { const agentConfig = (await window.electronAPI?.settings.get( "agent_config", )) as any; const step1 = (await window.electronAPI?.settings.get( "step1_config", )) as { workspaceDir?: string } | null; result = await window.electronAPI?.agent.init({ engine: normalizeAgentEngine(agentConfig?.type), apiKey: agentConfig?.apiKey, baseUrl: agentConfig?.apiBaseUrl, model: agentConfig?.model, workspaceDir: step1?.workspaceDir || "", }); log.info( `agent: ${result?.success ? "ok" : "failed"}`, result?.error, ); // ComputerServer 是 Agent 的 HTTP 接口,随 Agent 一起启动 await window.electronAPI?.computerServer .start() .catch(() => undefined); } else if (key === "fileServer") { const step1 = (await window.electronAPI?.settings.get( "step1_config", )) as { fileServerPort?: number } | null; const port = step1?.fileServerPort ?? 60000; result = await window.electronAPI?.fileServer.start(port); log.info( `fileServer: ${result?.success ? "ok" : "failed"}`, result?.error, ); } else if (key === "guiServer") { result = await window.electronAPI?.guiServer?.start(); } else if (key === "lanproxy") { const clientKey = (await window.electronAPI?.settings.get( "auth.saved_key", )) as string | null; const lpConfig = (await window.electronAPI?.settings.get( "lanproxy_config", )) as any; const serverIp = lpConfig?.serverIp || ( (await window.electronAPI?.settings.get( "lanproxy.server_host", )) as string )?.replace(/^https?:\/\//, ""); const serverPort = lpConfig?.serverPort || (await window.electronAPI?.settings.get("lanproxy.server_port")); if (serverIp && clientKey && serverPort) { result = await window.electronAPI?.lanproxy.start({ serverIp, serverPort, clientKey, ssl: lpConfig?.ssl, }); log.info( `lanproxy: ${result?.success ? "ok" : "failed"}`, result?.error, ); } else { log.warn("lanproxy: skipped (missing config)"); } } else if (key === "mcpProxy") { result = await window.electronAPI?.mcp.start(); log.info( `mcpProxy: ${result?.success ? "ok" : "failed"}`, result?.error, ); } await pollServicesStatus(); } catch (e) { log.error(`${key} failed:`, e); } finally { setStartingServices((prev) => { const next = new Set(prev); next.delete(key); return next; }); } } log.info("completed"); }, [pollServicesStatus], ); // ============================================ // 自动重连(等待依赖检查及同步完成后再执行,避免竞态) // ============================================ useEffect(() => { if (isSetupComplete !== true) return; if (needsRequiredDepsReinstall !== false) return; if (depsSyncInProgress) return; const log = createLogger("AutoReconnect"); const autoReconnect = async () => { // 如果 ClientPage handleLogin 已经启动了服务,跳过自动重连 if (loginStartedRef.current) { loginStartedRef.current = false; log.info("skipped (login flow)"); return; } // 如果向导刚完成,启动所有服务 if (setupJustCompleted.current) { setupJustCompleted.current = false; log.info("setup completed, starting services"); await startServicesSequentially(await getStartupServiceKeys()); return; } try { const savedKey = await window.electronAPI?.settings.get("auth.saved_key"); if (savedKey) { // 用户已退出登录时(configKey 被清除),不自动重连 const configKey = await window.electronAPI?.settings.get("auth.config_key"); if (!configKey) { log.info("skipped (logged out)"); return; } const result = await syncConfigToServer({ suppressToast: true }); if (result) { log.info("reg ok, starting services"); setOnlineStatus(result.online); const user = await authService.getAuthUser(); if (user) { setUsername( user.displayName || user.username || t("Claw.App.defaultUsername"), ); } setAuthRefreshTrigger((v) => v + 1); await startServicesSequentially(await getStartupServiceKeys()); } else { log.warn("reg failed, using local config"); notification.info({ message: t("Claw.App.AutoReconnectFailed"), description: t("Claw.App.AutoReconnectFailedDetail"), duration: 8, placement: "bottomRight", }); await startServicesSequentially(await getStartupServiceKeys()); } } else { log.info("skipped (no savedKey)"); } } catch (error) { log.error("failed:", error); } }; autoReconnect(); }, [ isSetupComplete, needsRequiredDepsReinstall, depsSyncInProgress, startServicesSequentially, getStartupServiceKeys, ]); // 启动服务状态轮询 useEffect(() => { if (isSetupComplete !== true) return; // 立即执行一次 pollServicesStatus(); // 每 5 秒轮询一次 servicesPollTimer.current = setInterval(pollServicesStatus, 5000); return () => { if (servicesPollTimer.current) { clearInterval(servicesPollTimer.current); } }; }, [isSetupComplete]); // ============================================ // 监听托盘/菜单事件 // ============================================ useEffect(() => { if (!window.electronAPI) return; const cleanupHandlers: (() => void)[] = []; // 监听设置菜单 const handleSettings = () => { console.log("[App] Received menu:settings event"); setActiveTab("settings"); }; window.electronAPI.on("menu:settings", handleSettings); cleanupHandlers.push(() => window.electronAPI?.off("menu:settings", handleSettings), ); // 监听依赖管理菜单 const handleDependencies = () => { console.log("[App] Received menu:dependencies event"); setActiveTab("dependencies"); }; window.electronAPI.on("menu:dependencies", handleDependencies); cleanupHandlers.push(() => window.electronAPI?.off("menu:dependencies", handleDependencies), ); // 监听 MCP 设置菜单 const handleMcpSettings = () => { console.log("[App] Received menu:mcp-settings event"); setActiveTab("settings"); }; window.electronAPI.on("menu:mcp-settings", handleMcpSettings); cleanupHandlers.push(() => window.electronAPI?.off("menu:mcp-settings", handleMcpSettings), ); // 监听新建会话菜单 const handleNewSession = () => { console.log("[App] Received menu:new-session event"); setSessionsAutoOpen(true); setActiveTab("sessions"); }; window.electronAPI.on("menu:new-session", handleNewSession); cleanupHandlers.push(() => window.electronAPI?.off("menu:new-session", handleNewSession), ); // 监听 Admin Server 服务正在重启 const handleServicesRestarting = () => { console.log("[App] Received admin:servicesRestarting event"); message.loading({ content: t("Claw.App.ServicesRestarting"), key: "admin-restart", duration: 0, }); }; window.electronAPI.on("admin:servicesRestarting", handleServicesRestarting); cleanupHandlers.push(() => window.electronAPI?.off( "admin:servicesRestarting", handleServicesRestarting, ), ); // 监听 Admin Server 服务重启完成 const handleServicesRestarted = (data: { success: boolean; results: Record; }) => { console.log("[App] Received admin:servicesRestarted event", data); if (data.success) { message.success({ content: t("Claw.App.ServicesRestartSuccess"), key: "admin-restart", duration: 3, }); } else { const failed = Object.entries(data.results) .filter(([, v]) => !v.success) .map(([k]) => k) .join(", "); message.error({ content: t("Claw.App.serviceRestartFailed", failed), key: "admin-restart", duration: 5, }); } }; window.electronAPI.on( "admin:servicesRestarted", handleServicesRestarted as any, ); cleanupHandlers.push(() => window.electronAPI?.off( "admin:servicesRestarted", handleServicesRestarted as any, ), ); return () => { cleanupHandlers.forEach((fn) => fn()); }; }, []); // ============================================ // 向导完成回调 // ============================================ const handleSetupComplete = () => { setupJustCompleted.current = true; setIsSetupComplete(true); }; const stopServicesForLogout = useCallback(async () => { const toStop = services.filter((svc) => svc.running || !!svc.error); for (const svc of toStop) { try { if (svc.key === "agent") await window.electronAPI?.agent.destroy(); else if (svc.key === "fileServer") await window.electronAPI?.fileServer.stop(); else if (svc.key === "lanproxy") await window.electronAPI?.lanproxy.stop(); else if (svc.key === "mcpProxy") await window.electronAPI?.mcp.stop(); else if (svc.key === "guiServer") await window.electronAPI?.guiServer?.stop(); } catch (e) { console.error(`[App] Failed to stop ${svc.label}:`, e); } } await window.electronAPI?.computerServer.stop().catch((e: unknown) => { console.error("[App] Failed to stop computerServer:", e); }); }, [services]); const handleSidebarLogout = useCallback(() => { Modal.confirm({ title: t("Claw.Client.logoutConfirm"), content: t("Claw.Client.logoutConfirmDetail"), okText: t("Claw.Client.logout"), cancelText: t("Claw.Client.cancel"), okButtonProps: { danger: true }, onOk: async () => { try { await stopServicesForLogout(); await logout(); await handleAuthChange(); setIsLoggedIn(false); setActiveTab("client"); setActiveNavKey("overview"); setServices([]); setServicesLoading(false); setStartingServices(new Set()); setOnlineStatus(false); message.success(t("Claw.Auth.loggedOut")); } catch { message.error(t("Claw.Client.logoutFailed")); } }, }); }, [handleAuthChange, stopServicesForLogout]); const handleNavSelect = useCallback((key: NavKey) => { setActiveNavKey(key); if (key === "overview") { setActiveTab("client"); return; } if (key === "newTask") { setSessionsAutoOpen(true); setActiveTab("sessions"); return; } if (key === "aiChat") { setActiveTab("sessions"); return; } if (key === "mcpLibrary") { setActiveTab("mcp"); return; } if (key === "toolIntegration" || key === "systemSettings") { setActiveTab("settings"); return; } setActiveTab("client"); }, []); const handleLegacySelect = useCallback((key: LegacyTabKey) => { setActiveTab(key); const navByLegacyTab: Partial> = { client: "overview", sessions: "aiChat", mcp: "mcpLibrary", settings: "systemSettings", }; setActiveNavKey(navByLegacyTab[key] || "systemSettings"); }, []); // ============================================ // 平台检测 // ============================================ const isMacOS = navigator.platform.toUpperCase().includes("MAC"); // ============================================ // i18n Context value // ============================================ const i18nContextValue = useMemo( () => ({ lang: i18nLang, updateLang: handleI18nLangChange }), [i18nLang, handleI18nLangChange], ); // ============================================ // 渲染:加载中(含等待依赖检查完成) // ============================================ if ( isSetupComplete === null || isLoggedIn === null || (isLoggedIn && isSetupComplete && needsRequiredDepsReinstall === null) ) { return (
{t("Claw.App.Loading")}
); } if (!isLoggedIn) { return ( ); } // ============================================ // 渲染:初始化向导 // ============================================ if (!isSetupComplete) { return ( ); } // ============================================ // 渲染:主界面下必需依赖未满足 → 全屏依赖安装,完成后重启服务回到主界面 // ============================================ if (needsRequiredDepsReinstall === true) { return ( { // 先回到主界面,再在后台重启服务(使新安装的依赖生效) setNeedsRequiredDepsReinstall(false); await restartAllServices(); }} /> ); } // ============================================ // 渲染:主界面 // ============================================ return (
{webviewActions && (
)} {/* 主体部分 */}
{/* 左侧边栏 (hidden when webview is active) */} {!webviewActions && ( )} {/* 主内容区:flex 子撑满,便于日志等页占满高度 */}
{activeTab === "client" && ( { if (tab === "sessions") setSessionsAutoOpen(true); setActiveTab(tab); setActiveNavKey( tab === "sessions" ? "aiChat" : "overview", ); }} services={services} servicesLoading={servicesLoading} startingServices={startingServices} setStartingServices={setStartingServices} onRefreshServices={pollServicesStatus} authRefreshTrigger={authRefreshTrigger} onAuthChange={handleAuthChange} onLoginStarted={handleLoginStarted} /> )} {activeTab === "sessions" && ( setSessionsAutoOpen(false)} onWebviewChange={setWebviewActions} /> )} {activeTab === "mcp" && } {activeTab === "settings" && } {activeTab === "dependencies" && } {activeTab === "permissions" && } {activeTab === "logs" && } {activeTab === "about" && }
); } export default App;