import axios from 'axios' import axiosRetry from 'axios-retry' import router from '@/router' /** 标记是否正在处理 401,防多个并发 */ let isHandlingUnauthorized = false /** 统一的 401 处理:清登录态 + 提示 + 跳转 /login */ async function handleUnauthorized() { if (isHandlingUnauthorized) return isHandlingUnauthorized = true try { const { useAuthStore } = await import('@/stores/auth') useAuthStore().clear() const { ElMessage } = await import('element-plus') ElMessage.warning('登录已过期,请重新登录') if (router.currentRoute.value.path !== '/login') { await router.replace('/login') } } finally { isHandlingUnauthorized = false } } export const api = axios.create({ baseURL: '/', timeout: 30000, }) axiosRetry(api, { retries: 3, retryDelay: axiosRetry.exponentialDelay, onRetry: (_retryCount, _err, _requestConfig) => { // no-op — could log in dev }, // Only retry idempotent methods — never retry POST/PUT/PATCH/DELETE retryCondition: (err) => { const method = (err.config?.method ?? '').toUpperCase() if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) return false // Retry on network errors or 5xx, but never on 401/403/404/4xx if (!err.response) return true return err.response.status >= 500 && err.response.status < 600 }, }) api.interceptors.response.use( (r) => r, (err) => { // 跳过登录接口的 401(密码错误等正常登录失败场景) const requestPath = new URL(err.config?.url || '', window.location.origin).pathname if (err.response?.status === 401 && requestPath === '/api/auth/login') { return Promise.reject(err) } if (err.response?.status === 401) { void handleUnauthorized() } return Promise.reject(err) } ) // ——— Auth ——— export const authApi = { login: (email: string, password: string) => api.post<{ access_token: string }>('/api/auth/login', { email, password }), me: () => api.get<{ email: string }>('/api/auth/me'), } // ——— Upstreams ——— export interface UpstreamData { id: number name: string base_url: string api_prefix: string auth_type: string auth_config_masked: Record rate_endpoint: string groups_endpoint: string enabled: boolean check_interval_seconds: number timeout_seconds: number last_status: string last_checked_at: string | null last_error: string | null balance: number | null balance_updated_at: string | null balance_endpoint: string balance_response_path: string balance_divisor: number balance_alert_threshold: number | null created_at: string updated_at: string } export interface UpstreamForm { name: string base_url: string api_prefix: string auth_type: string auth_config: Record rate_endpoint: string groups_endpoint: string enabled: boolean check_interval_seconds: number timeout_seconds: number balance_endpoint: string balance_response_path: string balance_divisor: number balance_alert_threshold: number | null } export interface GeneratedUpstreamKey { id: number | null upstream_id: number group_id: string group_name: string key_id: string | null key_name: string key_value: string | null masked_key: string status: string error: string | null imported_website_id: number | null imported_account_id: string | null imported_at: string | null created_at: string | null has_key_value: boolean } export interface GenerateKeysByGroupsForm { group_ids: string[] name_prefix: string quota: number expires_in_days?: number | null rate_limit_5h: number rate_limit_1d: number rate_limit_7d: number endpoint: string } export interface UpstreamBatchActionItem { upstream_id: number upstream_name: string status: 'success' | 'failed' | 'skipped' message: string detail?: string | null } export interface UpstreamBatchActionSummary { total: number success: number failed: number skipped: number } export interface UpstreamBatchActionResponse { success: boolean message: string summary: UpstreamBatchActionSummary items: UpstreamBatchActionItem[] } export const upstreamsApi = { list: () => api.get('/api/upstreams'), create: (data: UpstreamForm) => api.post('/api/upstreams', data), update: (id: number, data: Partial) => api.put(`/api/upstreams/${id}`, data), delete: (id: number) => api.delete(`/api/upstreams/${id}`), test: (id: number) => api.post<{ success: boolean; message: string; detail?: string }>(`/api/upstreams/${id}/test`), checkNow: (id: number) => api.post<{ success: boolean; message: string }>(`/api/upstreams/${id}/check-now`), generatedKeys: (id: number) => api.get(`/api/upstreams/${id}/generated-keys`), generateKeysByGroups: (id: number, data: GenerateKeysByGroupsForm) => api.post<{ success: boolean; message: string; items: GeneratedUpstreamKey[] }>(`/api/upstreams/${id}/keys/generate-by-groups`, data), latestSnapshot: (id: number) => api.get(`/api/upstreams/${id}/snapshots/latest`), listSnapshots: (id: number, limit = 20, offset = 0) => api.get(`/api/upstreams/${id}/snapshots`, { params: { limit, offset } }), testAll: () => api.post('/api/upstreams/test-all'), checkNowAll: () => api.post('/api/upstreams/check-now-all'), } // ——— Websites ——— export interface WebsiteData { id: number name: string site_type: string base_url: string api_prefix: string auth_type: string auth_config_masked: Record groups_endpoint: string group_update_endpoint: string enabled: boolean auto_sync_enabled: boolean timeout_seconds: number last_status: string last_checked_at: string | null last_error: string | null created_at: string updated_at: string } export interface WebsiteForm { name: string site_type: string base_url: string api_prefix: string auth_type: string auth_config: Record groups_endpoint: string group_update_endpoint: string enabled: boolean auto_sync_enabled: boolean timeout_seconds: number } export interface WebsiteGroup { id: string name: string rate_multiplier: string | null raw: Record } export interface BindingSourceGroup { upstream_id: number group_id: string upstream_name: string group_name: string } export interface GroupBindingData { id: number website_id: number website_name: string target_group_id: string target_group_name: string source_groups: BindingSourceGroup[] percent: number algorithm: string enabled: boolean created_at: string updated_at: string } export interface GroupBindingForm { website_id: number target_group_id: string target_group_name: string source_groups: BindingSourceGroup[] percent: number algorithm: string enabled: boolean } export interface WebsiteSyncLog { id: number website_id: number binding_id: number | null target_group_id: string target_group_name: string algorithm: string percent: number source_rates: Array> old_rate: string | null new_rate: string | null status: string message: string created_at: string } export interface ImportGroupItem { source_group_id: string source_group_name: string target_group_id: string | null target_group_name: string status: string message: string raw: Record } export interface ImportAccountItem { upstream_key_id: number source_group_id: string source_group_name: string target_group_id: string | null account_id: string | null account_name: string platform: string upstream_base_url: string status: string message: string raw: Record } export interface WebsiteBatchSyncResponse { total: number success: number failed: number skipped: number message: string logs: WebsiteSyncLog[] } export const websitesApi = { list: () => api.get('/api/websites'), create: (data: WebsiteForm) => api.post('/api/websites', data), update: (id: number, data: Partial) => api.put(`/api/websites/${id}`, data), delete: (id: number) => api.delete(`/api/websites/${id}`), test: (id: number) => api.post<{ success: boolean; message: string; detail?: string }>(`/api/websites/${id}/test`), groups: (id: number) => api.get(`/api/websites/${id}/groups`), importGroupsFromUpstream: (id: number, upstreamId: number, data: { group_ids: string[]; name_prefix: string }) => api.post<{ success: boolean; message: string; items: ImportGroupItem[] }>(`/api/websites/${id}/groups/import-from-upstream/${upstreamId}`, data), syncImportedUpstreamKeys: (id: number, data: { upstream_id: number }) => api.post<{ success: boolean; message: string; items: ImportAccountItem[] }>(`/api/websites/${id}/accounts/sync-imported-upstream-keys`, data), importAccountsFromUpstreamKeys: (id: number, data: { upstream_key_ids: number[] target_group_map: Record account_name_prefix: string default_platform: string platform_mode?: string concurrency?: number priority?: number auto_priority_by_rate?: boolean }) => api.post<{ success: boolean; message: string; items: ImportAccountItem[] }>(`/api/websites/${id}/accounts/import-upstream-keys`, data), listBindings: () => api.get('/api/group-bindings'), createBinding: (data: GroupBindingForm) => api.post('/api/group-bindings', data), updateBinding: (id: number, data: Partial) => api.put(`/api/group-bindings/${id}`, data), deleteBinding: (id: number) => api.delete(`/api/group-bindings/${id}`), syncNow: (id: number) => api.post(`/api/group-bindings/${id}/sync-now`), syncWebsiteBindings: (id: number) => api.post(`/api/websites/${id}/group-bindings/sync-now`), logs: (params?: { website_id?: number; binding_id?: number; limit?: number; offset?: number }) => api.get('/api/website-sync-logs', { params }), } // ——— Webhooks ——— export interface WebhookData { id: number name: string type: string url: string secret_masked: string enabled: boolean events: string[] created_at: string updated_at: string } export interface WebhookForm { name: string type: string url: string secret: string enabled: boolean events: string[] } export const webhooksApi = { list: () => api.get('/api/webhooks'), create: (data: WebhookForm) => api.post('/api/webhooks', data), update: (id: number, data: Partial) => api.put(`/api/webhooks/${id}`, data), delete: (id: number) => api.delete(`/api/webhooks/${id}`), test: (id: number) => api.post<{ success: boolean; message: string }>(`/api/webhooks/${id}/test`), } // ——— Logs ——— export interface LogData { id: number webhook_config_id: number webhook_name: string event_type: string payload: Record status: string response_text: string | null created_at: string } export const logsApi = { list: (params?: { status?: string; event_type?: string; limit?: number; offset?: number }) => api.get('/api/notification-logs', { params }), } // ——— Custom Pages ——— export type CustomPageAccessMode = 'direct' | 'proxy' export interface CustomPageData { id: number name: string url: string icon: string sort_order: number enabled: boolean use_proxy: boolean access_mode: CustomPageAccessMode description: string | null login_username: string | null login_username_selector: string | null login_password_selector: string | null login_submit_selector: string | null login_autofill_enabled: boolean login_password_configured: boolean linked_upstream_id: number | null created_at: string updated_at: string } export interface CustomPageForm { name: string url: string icon: string sort_order: number enabled: boolean use_proxy?: boolean access_mode?: CustomPageAccessMode description?: string login_username?: string login_password?: string login_username_selector?: string login_password_selector?: string login_submit_selector?: string login_autofill_enabled?: boolean login_password_clear?: boolean linked_upstream_id?: number | null } export const customPagesApi = { list: () => api.get('/api/custom-pages'), listPublic: () => axios.get('/api/custom-pages/public'), create: (data: CustomPageForm) => api.post('/api/custom-pages', data), update: (id: number, data: Partial) => api.put(`/api/custom-pages/${id}`, data), delete: (id: number) => api.delete(`/api/custom-pages/${id}`), } // ——— Auth Capture ——— export interface BrowserImportSession { session_id: string secret: string expires_in_seconds: number } export interface AuthCaptureCandidate { type: 'bearer_token' | 'cookie' | 'cookie_bundle' | 'credential' | 'api_key' source: string preview: string label: string confidence: number value?: string cookie_name?: string cookie_value?: string cookie_count?: number cookie_names?: string[] new_api_user?: string } export interface AuthCaptureResult { cookies: Record[] storage: Record session_storage: Record auth_headers: Record[] candidates: AuthCaptureCandidate[] } export interface BrowserImportStatus { session_id: string ready: boolean expires_at: number result: AuthCaptureResult | null } export const authCaptureApi = { createImportSession: (targetUrl: string) => api.post('/api/auth-capture/import-sessions', { target_url: targetUrl }), importSessionStatus: (sessionId: string, options?: { includeRaw?: boolean }) => api.get(`/api/auth-capture/import-sessions/${sessionId}`, { params: options?.includeRaw ? { include_raw: true } : undefined, }), }