From baff51295a6d63e50f92439c74d177b23cb0e938 Mon Sep 17 00:00:00 2001 From: liangxu Date: Thu, 30 Apr 2026 22:00:01 +0800 Subject: [PATCH] feat(desktop-client): add settings window and switch default API to production - Open a dedicated settings BrowserWindow with general / login / diagnostics / about tabs - Persist openAtLogin and keepRunningInBackground via desktop-settings.json; hide main window instead of quitting when background mode is on - Default API base URL now points to api.shengxintui.com; LoginView and SettingsView share a single constant - Restore wangyihao session cookies on probe and silent refresh, and accept the legacy http://mp.163.com console origin - Drop the standalone diagnostics view in favor of the settings tab Co-Authored-By: Claude Opus 4.7 (1M context) --- .../desktop-client/src/main/account-binder.ts | 20 +- apps/desktop-client/src/main/app-settings.ts | 114 +++ apps/desktop-client/src/main/bootstrap.ts | 115 ++- apps/desktop-client/src/preload/bridge.ts | 11 + apps/desktop-client/src/renderer/App.vue | 21 +- .../src/renderer/components/DesktopShell.vue | 53 +- .../renderer/composables/useDesktopSession.ts | 13 +- apps/desktop-client/src/renderer/env.d.ts | 8 + apps/desktop-client/src/renderer/routes.ts | 7 +- .../src/renderer/views/DiagnosticsView.vue | 188 ----- .../src/renderer/views/LoginView.vue | 4 +- .../src/renderer/views/SettingsView.vue | 775 ++++++++++++++++++ .../src/renderer/views/SettingsWindow.vue | 7 + 13 files changed, 1110 insertions(+), 226 deletions(-) create mode 100644 apps/desktop-client/src/main/app-settings.ts delete mode 100644 apps/desktop-client/src/renderer/views/DiagnosticsView.vue create mode 100644 apps/desktop-client/src/renderer/views/SettingsView.vue create mode 100644 apps/desktop-client/src/renderer/views/SettingsWindow.vue diff --git a/apps/desktop-client/src/main/account-binder.ts b/apps/desktop-client/src/main/account-binder.ts index 3f63711..8805688 100644 --- a/apps/desktop-client/src/main/account-binder.ts +++ b/apps/desktop-client/src/main/account-binder.ts @@ -277,19 +277,22 @@ const ZOL_LOGIN_REDIRECT_URLS = [ "https://service.zol.com.cn/user/login", ]; const WANGYI_API_ORIGIN = "https://mp.163.com"; -const WANGYI_CONSOLE_ORIGIN = "http://mp.163.com"; +const WANGYI_CONSOLE_ORIGIN = "https://mp.163.com"; +const WANGYI_LEGACY_CONSOLE_ORIGIN = "http://mp.163.com"; const WANGYI_API_REFERER = `${WANGYI_API_ORIGIN}/subscribe_v4/index.html`; const WANGYI_INFO_URL = `${WANGYI_API_ORIGIN}/wemedia/info.do`; const WANGYI_CONSOLE_URL = `${WANGYI_CONSOLE_ORIGIN}/subscribe_v4/index.html`; const WANGYI_LOGIN_REDIRECT_URLS = [ `${WANGYI_API_ORIGIN}/login.html`, `${WANGYI_CONSOLE_ORIGIN}/login.html`, + `${WANGYI_LEGACY_CONSOLE_ORIGIN}/login.html`, "https://reg.163.com/", "https://dl.reg.163.com/", ]; const WANGYI_COOKIE_URLS = [ WANGYI_API_ORIGIN, WANGYI_CONSOLE_ORIGIN, + WANGYI_LEGACY_CONSOLE_ORIGIN, "https://reg.163.com/", "https://dl.reg.163.com/", ]; @@ -2092,6 +2095,10 @@ export async function probePublishAccountSession( }; } + if (account.platform === "wangyihao") { + await restoreWangyihaoSessionCookies(account.id, handle.session); + } + const definition = platformBindingDefinitions[account.platform]; if (!definition) { return { @@ -2460,6 +2467,10 @@ export async function silentRefreshPublishAccountSession( return false; } + if (account.platform === "wangyihao") { + await restoreWangyihaoSessionCookies(account.id, handle.session); + } + const definition = platformBindingDefinitions[account.platform]; if (!definition) { return false; @@ -2537,13 +2548,6 @@ export async function inspectPublishAccountLocalSession( const profile = await resolvePublishAccountProfileFromHandle(account, handle); if (!profile) { - if (account.platform === "wangyihao") { - return { - availability: "expired", - profile: null, - }; - } - const consoleReady = await verifyPublishAccountConsoleAccess(account, handle).catch(() => false); if (consoleReady) { return { diff --git a/apps/desktop-client/src/main/app-settings.ts b/apps/desktop-client/src/main/app-settings.ts new file mode 100644 index 0000000..284b9d5 --- /dev/null +++ b/apps/desktop-client/src/main/app-settings.ts @@ -0,0 +1,114 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +import { app } from "electron/main"; + +export interface DesktopAppSettings { + openAtLogin: boolean; + keepRunningInBackground: boolean; +} + +export type DesktopAppSettingKey = keyof DesktopAppSettings; + +const DEFAULT_DESKTOP_APP_SETTINGS: DesktopAppSettings = { + openAtLogin: false, + keepRunningInBackground: true, +}; + +let cachedSettings: DesktopAppSettings = { ...DEFAULT_DESKTOP_APP_SETTINGS }; + +function settingsPath(): string { + return join(app.getPath("userData"), "desktop-settings.json"); +} + +function normalizeSettings(input: unknown): DesktopAppSettings { + if (!input || typeof input !== "object") { + return { ...DEFAULT_DESKTOP_APP_SETTINGS }; + } + + const candidate = input as Partial>; + return { + openAtLogin: + typeof candidate.openAtLogin === "boolean" + ? candidate.openAtLogin + : DEFAULT_DESKTOP_APP_SETTINGS.openAtLogin, + keepRunningInBackground: + typeof candidate.keepRunningInBackground === "boolean" + ? candidate.keepRunningInBackground + : DEFAULT_DESKTOP_APP_SETTINGS.keepRunningInBackground, + }; +} + +function readPersistedSettings(): DesktopAppSettings { + try { + return normalizeSettings(JSON.parse(readFileSync(settingsPath(), "utf8"))); + } catch { + return { ...DEFAULT_DESKTOP_APP_SETTINGS }; + } +} + +function persistSettings(settings: DesktopAppSettings): void { + try { + writeFileSync(settingsPath(), JSON.stringify(settings), "utf8"); + } catch (error) { + console.warn("[desktop-settings] persist failed", error); + } +} + +function readSystemOpenAtLogin(fallback: boolean): boolean { + try { + return app.getLoginItemSettings().openAtLogin; + } catch (error) { + console.warn("[desktop-settings] read login item failed", error); + return fallback; + } +} + +function applyOpenAtLogin(openAtLogin: boolean): boolean { + try { + app.setLoginItemSettings({ + openAtLogin, + openAsHidden: false, + }); + return readSystemOpenAtLogin(openAtLogin); + } catch (error) { + console.warn("[desktop-settings] apply login item failed", error); + return openAtLogin; + } +} + +export function initDesktopAppSettings(): DesktopAppSettings { + cachedSettings = readPersistedSettings(); + cachedSettings = { + ...cachedSettings, + openAtLogin: applyOpenAtLogin(cachedSettings.openAtLogin), + }; + persistSettings(cachedSettings); + return getDesktopAppSettings(); +} + +export function getDesktopAppSettings(): DesktopAppSettings { + cachedSettings = { + ...cachedSettings, + openAtLogin: readSystemOpenAtLogin(cachedSettings.openAtLogin), + }; + return { ...cachedSettings }; +} + +export function setDesktopAppSetting( + key: DesktopAppSettingKey, + value: boolean, +): DesktopAppSettings { + const next: DesktopAppSettings = { + ...cachedSettings, + [key]: value, + }; + + if (key === "openAtLogin") { + next.openAtLogin = applyOpenAtLogin(value); + } + + cachedSettings = next; + persistSettings(cachedSettings); + return getDesktopAppSettings(); +} diff --git a/apps/desktop-client/src/main/bootstrap.ts b/apps/desktop-client/src/main/bootstrap.ts index f9f0f92..65cfb7f 100644 --- a/apps/desktop-client/src/main/bootstrap.ts +++ b/apps/desktop-client/src/main/bootstrap.ts @@ -16,6 +16,12 @@ import { startDesktopHealthIndicator, syncDesktopHealthIndicator, } from "./app-issue-indicator"; +import { + getDesktopAppSettings, + initDesktopAppSettings, + setDesktopAppSetting, + type DesktopAppSettingKey, +} from "./app-settings"; import { bindPublishAccount, openPublishAccountConsole } from "./account-binder"; import { installObservedGlobalFetch, registerObservedRequestRendererTarget } from "./network-observer"; import { initProcessMetricsSampler } from "./process-metrics"; @@ -67,10 +73,13 @@ process.on("uncaughtException", (error) => { let mainWindow: ElectronBrowserWindow | null = null; let loginWindow: ElectronBrowserWindow | null = null; +let settingsWindow: ElectronBrowserWindow | null = null; let mainRendererContents: ElectronWebContents | null = null; let quitReleaseInFlight = false; +const forceCloseWindows = new WeakSet(); type WindowMode = "login" | "main"; +type RendererWindowMode = WindowMode | "settings"; type WindowModeSource = "auth-state" | "login" | "logout"; interface WindowSizePreset { @@ -91,6 +100,14 @@ const WINDOW_SIZES: Record = { main: { width: 1320, height: 860, minWidth: 900, minHeight: 600, resizable: true }, }; +const SETTINGS_WINDOW_SIZE: WindowSizePreset = { + width: 920, + height: 640, + minWidth: 760, + minHeight: 520, + resizable: true, +}; + function windowModePath(): string { return join(app.getPath("userData"), "window-mode.json"); } @@ -155,11 +172,23 @@ function currentLoginWindow(): ElectronBrowserWindow | 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 closeWindowAfterIpcReturn(window: ElectronBrowserWindow): void { + forceCloseWindows.add(window); setTimeout(() => { if (!window.isDestroyed()) { window.close(); @@ -197,6 +226,16 @@ async function ensureLoginWindow(): Promise { return window; } +async function ensureSettingsWindow(): Promise { + const existing = currentSettingsWindow(); + if (existing) { + return existing; + } + const window = await createSettingsWindow(); + settingsWindow = window; + return window; +} + async function ensureWindow(mode: WindowMode): Promise { return mode === "main" ? ensureMainWindow() : ensureLoginWindow(); } @@ -238,6 +277,16 @@ async function switchWindowMode(mode: WindowMode, _request: WindowModeRequest = if (previous && previous !== target && !previous.isDestroyed()) { closeWindowAfterIpcReturn(previous); } + + const settings = currentSettingsWindow(); + if (mode === "login" && settings) { + closeWindowAfterIpcReturn(settings); + } +} + +async function openSettingsWindow(): Promise { + const window = await ensureSettingsWindow(); + await revealWindow(window); } function rendererURL(): string | null { @@ -248,7 +297,7 @@ function preloadPath(): string { return join(__dirname, "../preload/bridge.cjs"); } -async function loadRenderer(window: ElectronBrowserWindow, mode: WindowMode): Promise { +async function loadRenderer(window: ElectronBrowserWindow, mode: RendererWindowMode): Promise { const devServerURL = rendererURL(); if (devServerURL) { const url = new URL(devServerURL); @@ -265,7 +314,7 @@ async function loadRenderer(window: ElectronBrowserWindow, mode: WindowMode): Pr }); } -async function mountRendererWebContents(window: ElectronBrowserWindow, mode: WindowMode): Promise { +async function mountRendererWebContents(window: ElectronBrowserWindow, mode: RendererWindowMode): Promise { const contents = window.webContents; contents.setWindowOpenHandler(() => ({ action: "deny" })); @@ -321,6 +370,18 @@ async function createAppWindow(mode: WindowMode): Promise } }); + window.on("close", (event) => { + if ( + mode === "main" + && !quitReleaseInFlight + && !forceCloseWindows.has(window) + && getDesktopAppSettings().keepRunningInBackground + ) { + event.preventDefault(); + window.hide(); + } + }); + window.on("minimize", () => { console.warn("[desktop-main] main window minimize event", { at: new Date().toISOString(), @@ -345,6 +406,41 @@ async function createAppWindow(mode: WindowMode): Promise return window; } +async function createSettingsWindow(): Promise { + const initial = SETTINGS_WINDOW_SIZE; + const window = new BrowserWindow({ + width: initial.width, + height: initial.height, + minWidth: initial.minWidth, + minHeight: initial.minHeight, + show: false, + title: "GEO Rankly Desktop 设置", + icon: createDesktopHealthIcon("normal"), + titleBarStyle: "hiddenInset", + trafficLightPosition: { x: 14, y: 15 }, + resizable: initial.resizable, + maximizable: initial.resizable, + fullscreenable: false, + autoHideMenuBar: true, + backgroundColor: nativeTheme.shouldUseDarkColors ? "#15191a" : "#f2f4f7", + webPreferences: { + preload: preloadPath(), + sandbox: true, + contextIsolation: true, + nodeIntegration: false, + }, + }); + + 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"); @@ -450,6 +546,17 @@ function registerBridgeHandlers(): void { 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) => { @@ -479,6 +586,7 @@ if (!hasSingleInstanceLock) { } else { app.whenReady().then(async () => { currentWindowMode = readPersistedWindowMode(); + initDesktopAppSettings(); registerBridgeHandlers(); await initSessionRegistry(); initAccountHealth(); @@ -508,6 +616,9 @@ if (!hasSingleInstanceLock) { }); app.on("window-all-closed", () => { + if (getDesktopAppSettings().keepRunningInBackground) { + return; + } if (process.platform !== "darwin") { app.quit(); } diff --git a/apps/desktop-client/src/preload/bridge.ts b/apps/desktop-client/src/preload/bridge.ts index 12993a5..21b0dcc 100644 --- a/apps/desktop-client/src/preload/bridge.ts +++ b/apps/desktop-client/src/preload/bridge.ts @@ -27,6 +27,11 @@ interface WindowModeRequest { animate?: boolean; } +interface DesktopAppSettings { + openAtLogin: boolean; + keepRunningInBackground: boolean; +} + const rendererProxyRequestChannel = "desktop:renderer-devtools-proxy:request"; const rendererProxyResponseChannel = "desktop:renderer-devtools-proxy:response"; @@ -120,6 +125,12 @@ const desktopBridge = { ipcRenderer.invoke("desktop:retry-publish-task", taskId) as Promise, openExternalUrl: (url: string) => ipcRenderer.invoke("desktop:open-external-url", url) as Promise, + openSettingsWindow: () => + ipcRenderer.invoke("desktop:open-settings-window") as Promise, + getAppSettings: () => + ipcRenderer.invoke("desktop:get-app-settings") as Promise, + setAppSetting: (key: keyof DesktopAppSettings, value: boolean) => + ipcRenderer.invoke("desktop:set-app-setting", key, value) as Promise, syncRuntimeSession: (session: DesktopRuntimeSessionSyncRequest | null) => ipcRenderer.invoke("desktop:runtime-session-sync", session) as Promise, releaseRuntimeSession: (revoke?: boolean) => diff --git a/apps/desktop-client/src/renderer/App.vue b/apps/desktop-client/src/renderer/App.vue index 82ac63b..40ac20d 100644 --- a/apps/desktop-client/src/renderer/App.vue +++ b/apps/desktop-client/src/renderer/App.vue @@ -4,9 +4,10 @@ import { useDesktopSession } from "./composables/useDesktopSession"; const DesktopShell = defineAsyncComponent(() => import("./components/DesktopShell.vue")); const LoginView = defineAsyncComponent(() => import("./views/LoginView.vue")); +const SettingsWindow = defineAsyncComponent(() => import("./views/SettingsWindow.vue")); const { isAuthenticated, syncRuntimeSession } = useDesktopSession(); -type RendererWindowMode = "auto" | "login" | "main"; +type RendererWindowMode = "auto" | "login" | "main" | "settings"; interface WindowModeBridge { setWindowMode?: ( @@ -17,7 +18,7 @@ interface WindowModeBridge { function resolveRendererWindowMode(): RendererWindowMode { const mode = new URLSearchParams(window.location.search).get("window"); - return mode === "login" || mode === "main" ? mode : "auto"; + return mode === "login" || mode === "main" || mode === "settings" ? mode : "auto"; } function getModeBridge(): WindowModeBridge | undefined { @@ -31,7 +32,9 @@ function syncWindowMode(authenticated: boolean): void { } const rendererWindowMode = resolveRendererWindowMode(); +document.documentElement.dataset.windowMode = rendererWindowMode; const usesAutomaticWindowMode = rendererWindowMode === "auto"; +const showsSettingsSurface = rendererWindowMode === "settings"; const showsMainSurface = computed(() => rendererWindowMode === "main" || (rendererWindowMode === "auto" && isAuthenticated.value), ); @@ -63,6 +66,18 @@ onMounted(() => { + + diff --git a/apps/desktop-client/src/renderer/components/DesktopShell.vue b/apps/desktop-client/src/renderer/components/DesktopShell.vue index 445518c..f017438 100644 --- a/apps/desktop-client/src/renderer/components/DesktopShell.vue +++ b/apps/desktop-client/src/renderer/components/DesktopShell.vue @@ -6,8 +6,8 @@ import { SendOutlined, LinkOutlined, RobotOutlined, - ToolOutlined, LogoutOutlined, + SettingOutlined, } from "@ant-design/icons-vue"; import { useDesktopRuntime } from "../composables/useDesktopRuntime"; @@ -61,13 +61,6 @@ const navItems = computed(() => { count: accounts.filter((item) => monitoringIDs.has(item.platform)).length, icon: RobotOutlined, }, - { - to: "/diagnostics", - title: "诊断", - description: "vault、scheduler、transport 与原始快照", - count: data?.clients.length ?? 0, - icon: ToolOutlined, - }, ]; }); @@ -78,6 +71,10 @@ const liveClientLabel = computed(() => snapshot.value?.clients[0]?.deviceName ?? const clientLabel = computed( () => liveClientLabel.value ?? session.value?.desktopClient?.device_name ?? "当前设备尚未注册 client", ); + +function openSettingsWindow() { + void window.desktopBridge.app.openSettingsWindow(); +}