From a1b2530fab10737a838bbdb88d398be4569154d1 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 19:55:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(desktop):=20cleanup=20HahaOAuthService=20?= =?UTF-8?q?=E2=80=94=20=E5=88=A0=E9=99=A4=20dead=20code=E3=80=81=E5=8A=A0?= =?UTF-8?q?=20fetch=20timeout=E3=80=81log=20refresh=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code review follow-ups on b0ca5861: - 删除 dead code exchangeFn/setExchangeFn (completeSession 总走 exchangeWithCustomCallback) - exchangeWithCustomCallback fetch 加 15s AbortController timeout (parity with client.ts) - ensureFreshAccessToken refresh 失败时 console.error,不再静默吞异常 - test 删除未用的 mock import - getOauthConfig 改为 static import - saveTokens 加注释说明 atomic write 意图 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../__tests__/haha-oauth-service.test.ts | 2 +- src/server/services/hahaOAuthService.ts | 41 ++++++++++--------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/server/__tests__/haha-oauth-service.test.ts b/src/server/__tests__/haha-oauth-service.test.ts index 8ce99b57..90c9e055 100644 --- a/src/server/__tests__/haha-oauth-service.test.ts +++ b/src/server/__tests__/haha-oauth-service.test.ts @@ -2,7 +2,7 @@ * Unit tests for HahaOAuthService — haha 自管 OAuth 的核心 service 层。 */ -import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test' +import { describe, test, expect, beforeEach, afterEach } from 'bun:test' import * as fs from 'fs/promises' import * as path from 'path' import * as os from 'os' diff --git a/src/server/services/hahaOAuthService.ts b/src/server/services/hahaOAuthService.ts index 79020794..227f59fd 100644 --- a/src/server/services/hahaOAuthService.ts +++ b/src/server/services/hahaOAuthService.ts @@ -19,7 +19,6 @@ import { } from '../../services/oauth/crypto.js' import { buildAuthUrl, - exchangeCodeForTokens, refreshOAuthToken, isOAuthTokenExpired, parseScopes, @@ -29,6 +28,7 @@ import type { OAuthTokenExchangeResponse, SubscriptionType, } from '../../services/oauth/types.js' +import { getOauthConfig } from '../../constants/oauth.js' export type StoredOAuthTokens = { accessToken: string @@ -47,29 +47,17 @@ export type OAuthSession = { } type RefreshFn = (refreshToken: string, opts?: { scopes?: string[] }) => Promise -type ExchangeFn = ( - code: string, - state: string, - verifier: string, - port: number, -) => Promise const SESSION_TTL_MS = 5 * 60 * 1000 export class HahaOAuthService { private sessions = new Map() private refreshFn: RefreshFn = refreshOAuthToken - private exchangeFn: ExchangeFn = (code, state, verifier, port) => - exchangeCodeForTokens(code, state, verifier, port, false) setRefreshFn(fn: RefreshFn): void { this.refreshFn = fn } - setExchangeFn(fn: ExchangeFn): void { - this.exchangeFn = fn - } - private getOAuthFilePath(): string { const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude') @@ -89,6 +77,8 @@ export class HahaOAuthService { async saveTokens(tokens: StoredOAuthTokens): Promise { const filePath = this.getOAuthFilePath() await fs.mkdir(path.dirname(filePath), { recursive: true }) + // 写临时文件再 rename,防止写到一半被其他读者读到残缺 JSON。 + // 单进程 desktop 下 pid 后缀足够隔离。 const tmp = `${filePath}.tmp.${process.pid}` await fs.writeFile(tmp, JSON.stringify(tokens, null, 2), { mode: 0o600 }) await fs.rename(tmp, filePath) @@ -194,7 +184,6 @@ export class HahaOAuthService { verifier: string, port: number, ): Promise { - const { getOauthConfig } = await import('../../constants/oauth.js') const requestBody = { grant_type: 'authorization_code', code, @@ -204,11 +193,19 @@ export class HahaOAuthService { state, } - const res = await fetch(getOauthConfig().TOKEN_URL, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(requestBody), - }) + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), 15_000) + let res: Response + try { + res = await fetch(getOauthConfig().TOKEN_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(requestBody), + signal: controller.signal, + }) + } finally { + clearTimeout(timeoutId) + } if (!res.ok) { throw new Error( `Token exchange failed (${res.status}): ${await res.text()}`, @@ -240,7 +237,11 @@ export class HahaOAuthService { } await this.saveTokens(updated) return updated.accessToken - } catch { + } catch (err) { + console.error( + '[HahaOAuthService] token refresh failed:', + err instanceof Error ? err.message : err, + ) return null } }