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 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 } }), } // ——— 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 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`), 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' | 'remote_browser' 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}`), refreshAuth: (id: number) => api.post<{ success: boolean; message: string }>(`/api/custom-pages/${id}/refresh-auth`), } // ——— Remote browser sessions ——— export interface BrowserTabData { id: string title: string url: string created_at: number } export interface BrowserSessionData { id: string custom_page_id: number url: string title: string active_tab_id?: string tabs?: BrowserTabData[] tab_revision?: number } export type BrowserEventPayload = | { type: 'click' | 'dblclick' | 'mousemove' | 'mousedown' | 'mouseup'; x: number; y: number; button?: 'left' | 'right' | 'middle' } | { type: 'type'; text: string } | { type: 'key'; key: string } | { type: 'scroll'; delta_x: number; delta_y: number; x?: number; y?: number } | { type: 'reload' | 'back' | 'forward' } | { type: 'resize'; width: number; height: number } export const browserSessionsApi = { create: (data: { custom_page_id: number; width: number; height: number }) => api.post('/api/browser-sessions', data), get: (id: string) => api.get(`/api/browser-sessions/${id}`), event: (id: string, data: BrowserEventPayload) => api.post(`/api/browser-sessions/${id}/events`, data), activateTab: (id: string, tabId: string) => api.post(`/api/browser-sessions/${id}/tabs/${tabId}/activate`), closeTab: (id: string, tabId: string) => api.delete(`/api/browser-sessions/${id}/tabs/${tabId}`), selection: (id: string) => api.get<{ text: string }>(`/api/browser-sessions/${id}/selection`), clipboard: (id: string) => api.get<{ text?: string; error?: string }>(`/api/browser-sessions/${id}/clipboard`), close: (id: string) => api.delete(`/api/browser-sessions/${id}`), autofillLogin: (id: string) => api.post<{ success: boolean; message: string }>(`/api/browser-sessions/${id}/autofill-login`), clearProfile: (customPageId: number) => api.delete(`/api/browser-sessions/profiles/${customPageId}`), screenshotUrl: (id: string, token?: string) => { const params = new URLSearchParams({ t: String(Date.now()) }) if (token) params.set('token', token) return `/api/browser-sessions/${id}/screenshot?${params.toString()}` }, /** Build a WebSocket URL for the streaming endpoint. */ wsUrl: (id: string, token?: string) => { const proto = location.protocol === 'https:' ? 'wss:' : 'ws:' const params = new URLSearchParams() if (token) params.set('token', token) return `${proto}//${location.host}/api/browser-sessions/${id}/ws?${params.toString()}` }, } // ——— Auth Capture ——— export interface AuthCaptureSession { session_id: string ws_url: string } export interface AuthCaptureCandidate { type: 'bearer_token' | 'cookie' | 'credential' | 'api_key' source: string preview: string label: string confidence: number value?: string cookie_name?: string cookie_value?: string new_api_user?: string } export interface AuthCaptureResult { cookies: Record[] storage: Record session_storage: Record auth_headers: Record[] candidates: AuthCaptureCandidate[] } export const authCaptureApi = { createSession: (url: string, width?: number, height?: number) => api.post('/api/auth-capture/sessions', { url, width, height }), extract: (sessionId: string, options?: { includeRaw?: boolean }) => api.get(`/api/auth-capture/sessions/${sessionId}/extract`, { params: options?.includeRaw ? { include_raw: true } : undefined, }), closeSession: (sessionId: string) => api.delete(`/api/auth-capture/sessions/${sessionId}`), wsUrl: (sessionId: string, token?: string) => { const proto = location.protocol === 'https:' ? 'wss:' : 'ws:' const params = new URLSearchParams() if (token) params.set('token', token) return `${proto}//${location.host}/api/browser-sessions/${sessionId}/ws?${params.toString()}` }, }