From a490969d586db26cd97122bc3f4ee925416891e7 Mon Sep 17 00:00:00 2001 From: clh02467605 Date: Tue, 2 Jun 2026 16:29:43 +0800 Subject: [PATCH] feat(auth): add retry logic and error handling for API key validation --- .../cli/src/commands/auth/login-console.ts | 9 +++- packages/cli/src/commands/auth/login.ts | 46 ++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/auth/login-console.ts b/packages/cli/src/commands/auth/login-console.ts index df13b01..f212325 100644 --- a/packages/cli/src/commands/auth/login-console.ts +++ b/packages/cli/src/commands/auth/login-console.ts @@ -281,6 +281,7 @@ export async function runConsoleLogin( opts?: { needApiKey?: boolean; onApiKey?: (key: string) => Promise }, ): Promise { const state = randomBytes(16).toString("hex"); + let callbackError: unknown; const server = http.createServer(async (req, res) => { try { if (req.method === "OPTIONS") { @@ -313,9 +314,11 @@ export async function runConsoleLogin( if (apiKey && opts?.onApiKey) { await opts.onApiKey(apiKey); } - } catch { + } catch (err: unknown) { + callbackError = err; res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" }); res.end("Failed to save credentials\n"); + server.close(); return; } } @@ -388,4 +391,8 @@ export async function runConsoleLogin( } }); }); + + if (callbackError) { + throw callbackError; + } } diff --git a/packages/cli/src/commands/auth/login.ts b/packages/cli/src/commands/auth/login.ts index 86cb7d1..0fe05ed 100644 --- a/packages/cli/src/commands/auth/login.ts +++ b/packages/cli/src/commands/auth/login.ts @@ -1,4 +1,6 @@ import { + BailianError, + ExitCode, chatEndpoint, defineCommand, getConfigPath, @@ -16,18 +18,58 @@ import { promptConfirm } from "../../output/prompt.ts"; import { printCurrentCommandHelp } from "../../utils/command-help.ts"; import { resolveConsoleOrigin, runConsoleLogin } from "./login-console.ts"; +const RETRY_DELAY_BASE_MS = 500; + +function canRetry(err: unknown): boolean { + if (err instanceof BailianError) { + if (err.exitCode === ExitCode.NETWORK || err.exitCode === ExitCode.TIMEOUT) { + return true; + } + const status = err.api?.httpStatus; + return status === 401 || (status !== undefined && status >= 500); + } + if (err instanceof Error) { + return ( + err.name === "AbortError" || + err.name === "TimeoutError" || + err.message.includes("timed out") || + err.message === "fetch failed" + ); + } + return false; +} + async function validateKeyAndPersist(config: Config, key: string): Promise { process.stderr.write("Testing key... "); const testConfig = { ...config, apiKey: key }; - await requestJson(testConfig, { + const requestOpts = { url: chatEndpoint(testConfig.baseUrl), method: "POST", + timeout: Math.min(config.timeout, 30), body: { model: "qwen3.7-max", messages: [{ role: "user", content: "hi" }], max_tokens: 1, }, - }); + }; + + for (let attempt = 1; attempt <= 3; attempt++) { + try { + await requestJson(testConfig, requestOpts); + break; + } catch (err) { + if (attempt >= 3 || !canRetry(err)) { + process.stderr.write("\n"); + throw new BailianError("API key validation failed", ExitCode.AUTH, "Invalid API key.", { + cause: err, + }); + } + // retry delay: 500ms, 1000ms, 2000ms + const delayMs = RETRY_DELAY_BASE_MS * 2 ** (attempt - 1); + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + } + process.stderr.write("Valid\n"); const existing = readConfigFile() as Record;