feat(desktop): frontend API client + zustand store for haha OAuth

- 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) <noreply@anthropic.com>
This commit is contained in:
程序员阿江(Relakkes) 2026-04-17 20:21:15 +08:00
parent 669be1fbc6
commit f10396e5bf
2 changed files with 127 additions and 0 deletions

View File

@ -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<HahaOAuthStatus>('/api/haha-oauth')
},
logout() {
return api.delete<{ ok: true }>('/api/haha-oauth')
},
}

View File

@ -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<void>
login: () => Promise<{ authorizeUrl: string }>
logout: () => Promise<void>
startPolling: () => void
stopPolling: () => void
}
export const useHahaOAuthStore = create<HahaOAuthState>((set, get) => {
let pollTimer: ReturnType<typeof setInterval> | 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 })
},
}
})