cc-haha/desktop/src/stores/hahaOpenAIOAuthStore.ts
程序员阿江(Relakkes) 83ee4c09ec Add desktop ChatGPT OAuth controls
The desktop app now has a dedicated OpenAI OAuth API client and store mirroring the existing Claude Official flow. The component uses the existing browser-open plus polling pattern and never exposes token material to the UI.

Constraint: OAuth status responses must not return token bodies

Rejected: Reuse Claude OAuth store | the status shape and endpoint differ

Confidence: high

Scope-risk: narrow

Tested: cd desktop && bun run test -- src/stores/hahaOpenAIOAuthStore.test.ts

Tested: git diff --check
2026-05-18 23:53:11 +08:00

102 lines
2.4 KiB
TypeScript

// desktop/src/stores/hahaOpenAIOAuthStore.ts
import { create } from 'zustand'
import {
hahaOpenAIOAuthApi,
type HahaOpenAIOAuthStatus,
} from '../api/hahaOpenAIOAuth'
const POLL_INTERVAL_MS = 2_000
type HahaOpenAIOAuthState = {
status: HahaOpenAIOAuthStatus | null
isPolling: boolean
isLoading: boolean
error: string | null
fetchStatus: () => Promise<void>
login: () => Promise<{ authorizeUrl: string }>
logout: () => Promise<void>
startPolling: () => void
stopPolling: () => void
}
export const useHahaOpenAIOAuthStore = create<HahaOpenAIOAuthState>((set, get) => {
let pollTimer: ReturnType<typeof setTimeout> | null = null
return {
status: null,
isPolling: false,
isLoading: false,
error: null,
fetchStatus: async () => {
try {
const status = await hahaOpenAIOAuthApi.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 hahaOpenAIOAuthApi.start()
set({ isLoading: false })
return { authorizeUrl: res.authorizeUrl }
} catch (err) {
set({
isLoading: false,
error: err instanceof Error ? err.message : String(err),
})
throw err
}
},
logout: async () => {
get().stopPolling()
set({ isLoading: true, error: null })
try {
await hahaOpenAIOAuthApi.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 })
const scheduleNext = () => {
pollTimer = setTimeout(async () => {
pollTimer = null
await get().fetchStatus()
const cur = get().status
if (cur && cur.loggedIn) {
get().stopPolling()
return
}
if (get().isPolling) {
scheduleNext()
}
}, POLL_INTERVAL_MS)
}
scheduleNext()
},
stopPolling: () => {
if (pollTimer) {
clearTimeout(pollTimer)
pollTimer = null
}
set({ isPolling: false })
},
}
})