| 5 | const API_BASE = '/api'; |
| 6 | |
| 7 | async function request<T>(url: string, options: RequestInit = {}): Promise<T> { |
| 8 | const token = localStorage.getItem('token'); |
| 9 | const headers: Record<string, string> = { |
| 10 | 'Content-Type': 'application/json', |
| 11 | ...(token ? { Authorization: `Bearer ${token}` } : {}), |
| 12 | }; |
| 13 | |
| 14 | const res = await fetch(`${API_BASE}${url}`, { ...options, headers }); |
| 15 | |
| 16 | if (!res.ok) { |
| 17 | // Auto-logout on expired/invalid token (but not on auth endpoints — let them show errors) |
| 18 | const isAuthEndpoint = url.startsWith('/auth/login') |
| 19 | || url.startsWith('/auth/register') |
| 20 | || url.startsWith('/auth/verify-email') |
| 21 | || url.startsWith('/auth/resend-verification') |
| 22 | || url.startsWith('/auth/forgot-password') |
| 23 | || url.startsWith('/auth/reset-password'); |
| 24 | if (res.status === 401 && !isAuthEndpoint) { |
| 25 | localStorage.removeItem('token'); |
| 26 | localStorage.removeItem('user'); |
| 27 | window.location.href = '/login'; |
| 28 | throw new Error('Session expired'); |
| 29 | } |
| 30 | const bodyText = await res.text(); |
| 31 | let error: { detail?: unknown }; |
| 32 | try { |
| 33 | error = bodyText ? JSON.parse(bodyText) : {}; |
| 34 | } catch { |
| 35 | const snippet = bodyText.trim().slice(0, 280); |
| 36 | error = { |
| 37 | detail: snippet || `HTTP ${res.status} ${res.statusText || ''}`.trim(), |
| 38 | }; |
| 39 | } |
| 40 | // Pydantic validation errors return detail as an array of objects |
| 41 | const fieldLabels: Record<string, string> = { |
| 42 | name: '名称', |
| 43 | role_description: '角色描述', |
| 44 | agent_type: '智能体类型', |
| 45 | primary_model_id: '主模型', |
| 46 | max_tokens_per_day: '每日 Token 上限', |
| 47 | max_tokens_per_month: '每月 Token 上限', |
| 48 | }; |
| 49 | let message = ''; |
| 50 | if (Array.isArray(error.detail)) { |
| 51 | message = error.detail |
| 52 | .map((e: any) => { |
| 53 | const field = e.loc?.slice(-1)[0] || ''; |
| 54 | const label = fieldLabels[field] || field; |
| 55 | return label ? `${label}: ${e.msg}` : e.msg; |
| 56 | }) |
| 57 | .join('; '); |
| 58 | } else if (typeof error.detail === 'object' && error.detail !== null) { |
| 59 | // Structured error detail (e.g., NeedsVerificationResponse) |
| 60 | message = (error.detail as Record<string, any>).message || `HTTP ${res.status}`; |
| 61 | } else { |
| 62 | const d = error.detail; |
| 63 | if (typeof d === 'string') message = d; |
| 64 | else if (d != null && typeof d === 'object') message = JSON.stringify(d); |