// ============================================================ // HTTP API 客户端 // 职责:封装 REST API 请求(auth、conversations 等) // 内置 401 拦截 + 自动刷新 + 重试机制 // ============================================================ const API_BASE = "/api"; interface ApiResponse { data?: T; error?: { code: string; message: string }; status: number; } // ---- 认证回调(由 AuthProvider 注入,避免循环依赖) ---- interface AuthCallbacks { getAccessToken: () => string | null; getRefreshToken: () => string | null; onRefreshSuccess: (user: AuthUser, accessToken: string, refreshToken: string) => void; onRefreshFailed: () => void; } let authCallbacks: AuthCallbacks | null = null; let refreshPromise: Promise | null = null; /** 由 AuthProvider 在初始化时调用,注入认证回调。 */ export function setAuthCallbacks(callbacks: AuthCallbacks): void { authCallbacks = callbacks; } /** 不需要认证的公开路径。 */ const PUBLIC_PATHS = new Set([ "/auth/register", "/auth/login", "/auth/refresh", ]); function isPublicPath(path: string): boolean { return PUBLIC_PATHS.has(path); } /** 尝试用 refresh token 换取新的 access token。 */ async function doRefresh(): Promise { const rt = authCallbacks?.getRefreshToken(); if (!rt) return false; const res = await refreshTokenDirect(rt); if (res.data) { authCallbacks?.onRefreshSuccess( res.data.user, res.data.access_token, res.data.refresh_token ); return true; } authCallbacks?.onRefreshFailed(); return false; } /** 带并发保护的刷新:多个 401 只触发一次 refresh。 */ async function refreshWithLock(): Promise { if (!refreshPromise) { refreshPromise = doRefresh().finally(() => { refreshPromise = null; }); } return refreshPromise; } // ---- 核心请求函数 ---- async function request( path: string, options: RequestInit = {}, _retry = false ): Promise> { const url = `${API_BASE}${path}`; const headers: Record = { "Content-Type": "application/json", ...(options.headers as Record), }; // 对非公开路径自动附加 access token if (!isPublicPath(path) && !headers["Authorization"]) { const token = authCallbacks?.getAccessToken(); if (token) { headers["Authorization"] = `Bearer ${token}`; } } try { const res = await fetch(url, { ...options, headers }); const status = res.status; if (res.status === 204) { return { status }; } const body = await res.json(); // 401 拦截:尝试刷新 token 后重试(仅重试一次) if (res.status === 401 && !_retry && !isPublicPath(path) && authCallbacks) { const refreshed = await refreshWithLock(); if (refreshed) { return request(path, options, true); } } if (!res.ok) { return { error: { code: body.error || "UNKNOWN", message: body.message || "请求失败" }, status, }; } return { data: body as T, status }; } catch { return { error: { code: "NETWORK_ERROR", message: "网络连接失败,请检查网络" }, status: 0, }; } } function authHeaders(accessToken: string): Record { return { Authorization: `Bearer ${accessToken}` }; } // ---- Auth API ---- export interface AuthUser { id: string; username: string; created_at: string; } export interface AuthResponse { user: AuthUser; access_token: string; refresh_token: string; } export async function register( username: string, password: string ): Promise> { return request("/auth/register", { method: "POST", body: JSON.stringify({ username, password }), }); } export async function login( username: string, password: string ): Promise> { return request("/auth/login", { method: "POST", body: JSON.stringify({ username, password }), }); } /** 内部用的 refresh 请求,不经过 401 拦截(避免递归)。 */ async function refreshTokenDirect( refresh_token: string ): Promise> { const url = `${API_BASE}/auth/refresh`; try { const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh_token }), }); const body = await res.json(); if (!res.ok) { return { error: { code: body.error || "UNKNOWN", message: body.message || "请求失败" }, status: res.status, }; } return { data: body as AuthResponse, status: res.status }; } catch { return { error: { code: "NETWORK_ERROR", message: "网络连接失败" }, status: 0, }; } } export async function refreshToken( refresh_token: string ): Promise> { return request("/auth/refresh", { method: "POST", body: JSON.stringify({ refresh_token }), }); } export async function logout( accessToken: string, refreshTokenStr: string ): Promise> { return request<{ message: string }>("/auth/logout", { method: "POST", headers: authHeaders(accessToken), body: JSON.stringify({ refresh_token: refreshTokenStr }), }); } // ---- Conversation API ---- export interface ConversationListItem { id: string; title: string; last_message: string; message_count: number; created_at: string; updated_at: string; } export interface ConversationListResponse { conversations: ConversationListItem[]; total: number; page: number; size: number; } export interface CreateConversationResponse { id: string; title: string; created_at: string; updated_at: string; } export interface StoredMessage { id: number; role: string; content: string; tokens_used: number; created_at: string; } export interface MessagesResponse { messages: StoredMessage[]; total: number; } export async function listConversations( token: string, page = 1, size = 50 ): Promise> { return request( `/conversations?page=${page}&size=${size}`, { headers: authHeaders(token) } ); } export async function createConversation( token: string, config?: Record ): Promise> { return request("/conversations", { method: "POST", headers: authHeaders(token), body: JSON.stringify(config ? { config } : {}), }); } export async function deleteConversation( token: string, id: string ): Promise> { return request(`/conversations/${id}`, { method: "DELETE", headers: authHeaders(token), }); } export async function renameConversation( token: string, id: string, title: string ): Promise> { return request<{ message: string }>(`/conversations/${id}`, { method: "PATCH", headers: authHeaders(token), body: JSON.stringify({ title }), }); } export async function getConversationMessages( token: string, id: string, limit = 200 ): Promise> { return request( `/conversations/${id}/messages?limit=${limit}`, { headers: authHeaders(token) } ); }