From f10396e5bf4222e9a4e6fdd303c86fb767901f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Fri, 17 Apr 2026 20:21:15 +0800 Subject: [PATCH] feat(desktop): frontend API client + zustand store for haha OAuth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api/hahaOAuth.ts: start/status/logout 3 个方法,基于现有 api client - stores/hahaOAuthStore.ts: 状态管理 + 2s 间隔轮询 /status port 从 getBaseUrl() 动态 parse,避免 Tauri 每次启动端口变化时失效。 Co-Authored-By: Claude Opus 4.7 (1M context) --- desktop/src/api/hahaOAuth.ts | 38 ++++++++++++ desktop/src/stores/hahaOAuthStore.ts | 89 ++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 desktop/src/api/hahaOAuth.ts create mode 100644 desktop/src/stores/hahaOAuthStore.ts diff --git a/desktop/src/api/hahaOAuth.ts b/desktop/src/api/hahaOAuth.ts new file mode 100644 index 00000000..65b3de40 --- /dev/null +++ b/desktop/src/api/hahaOAuth.ts @@ -0,0 +1,38 @@ +// desktop/src/api/hahaOAuth.ts + +import { api, getBaseUrl } from './client' + +export type HahaOAuthStatus = + | { loggedIn: false } + | { + loggedIn: true + expiresAt: number | null + scopes: string[] + subscriptionType: 'pro' | 'max' | 'team' | 'enterprise' | null + } + +function currentServerPort(): number { + const port = new URL(getBaseUrl()).port + const parsed = Number.parseInt(port, 10) + if (!Number.isFinite(parsed) || parsed <= 0) { + throw new Error(`Cannot determine server port from baseUrl: ${getBaseUrl()}`) + } + return parsed +} + +export const hahaOAuthApi = { + start() { + return api.post<{ authorizeUrl: string; state: string }>( + '/api/haha-oauth/start', + { serverPort: currentServerPort() }, + ) + }, + + status() { + return api.get('/api/haha-oauth') + }, + + logout() { + return api.delete<{ ok: true }>('/api/haha-oauth') + }, +} diff --git a/desktop/src/stores/hahaOAuthStore.ts b/desktop/src/stores/hahaOAuthStore.ts new file mode 100644 index 00000000..d6f3e49a --- /dev/null +++ b/desktop/src/stores/hahaOAuthStore.ts @@ -0,0 +1,89 @@ +// desktop/src/stores/hahaOAuthStore.ts + +import { create } from 'zustand' +import { hahaOAuthApi, type HahaOAuthStatus } from '../api/hahaOAuth' + +const POLL_INTERVAL_MS = 2_000 + +type HahaOAuthState = { + status: HahaOAuthStatus | null + isPolling: boolean + isLoading: boolean + error: string | null + + fetchStatus: () => Promise + login: () => Promise<{ authorizeUrl: string }> + logout: () => Promise + startPolling: () => void + stopPolling: () => void +} + +export const useHahaOAuthStore = create((set, get) => { + let pollTimer: ReturnType | null = null + + return { + status: null, + isPolling: false, + isLoading: false, + error: null, + + fetchStatus: async () => { + try { + const status = await hahaOAuthApi.status() + set({ status, error: null }) + } catch (err) { + set({ error: err instanceof Error ? err.message : String(err) }) + } + }, + + login: async () => { + set({ isLoading: true, error: null }) + try { + const res = await hahaOAuthApi.start() + set({ isLoading: false }) + get().startPolling() + return { authorizeUrl: res.authorizeUrl } + } catch (err) { + set({ + isLoading: false, + error: err instanceof Error ? err.message : String(err), + }) + throw err + } + }, + + logout: async () => { + set({ isLoading: true }) + try { + await hahaOAuthApi.logout() + set({ status: { loggedIn: false }, isLoading: false }) + } catch (err) { + set({ + isLoading: false, + error: err instanceof Error ? err.message : String(err), + }) + throw err + } + }, + + startPolling: () => { + if (pollTimer) return + set({ isPolling: true }) + pollTimer = setInterval(async () => { + await get().fetchStatus() + const cur = get().status + if (cur && cur.loggedIn) { + get().stopPolling() + } + }, POLL_INTERVAL_MS) + }, + + stopPolling: () => { + if (pollTimer) { + clearInterval(pollTimer) + pollTimer = null + } + set({ isPolling: false }) + }, + } +})