496 lines
16 KiB
TypeScript
496 lines
16 KiB
TypeScript
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<string, any>
|
|
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<string, any>
|
|
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<UpstreamData[]>('/api/upstreams'),
|
|
create: (data: UpstreamForm) => api.post<UpstreamData>('/api/upstreams', data),
|
|
update: (id: number, data: Partial<UpstreamForm>) => api.put<UpstreamData>(`/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<GeneratedUpstreamKey[]>(`/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<any[]>(`/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<string, any>
|
|
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<string, any>
|
|
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<string, any>
|
|
}
|
|
|
|
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<Record<string, any>>
|
|
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<string, any>
|
|
}
|
|
|
|
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<string, any>
|
|
}
|
|
|
|
export const websitesApi = {
|
|
list: () => api.get<WebsiteData[]>('/api/websites'),
|
|
create: (data: WebsiteForm) => api.post<WebsiteData>('/api/websites', data),
|
|
update: (id: number, data: Partial<WebsiteForm>) => api.put<WebsiteData>(`/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<WebsiteGroup[]>(`/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<string, string>
|
|
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<GroupBindingData[]>('/api/group-bindings'),
|
|
createBinding: (data: GroupBindingForm) => api.post<GroupBindingData>('/api/group-bindings', data),
|
|
updateBinding: (id: number, data: Partial<GroupBindingForm>) => api.put<GroupBindingData>(`/api/group-bindings/${id}`, data),
|
|
deleteBinding: (id: number) => api.delete(`/api/group-bindings/${id}`),
|
|
syncNow: (id: number) => api.post<WebsiteSyncLog>(`/api/group-bindings/${id}/sync-now`),
|
|
logs: (params?: { website_id?: number; binding_id?: number; limit?: number; offset?: number }) =>
|
|
api.get<WebsiteSyncLog[]>('/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<WebhookData[]>('/api/webhooks'),
|
|
create: (data: WebhookForm) => api.post<WebhookData>('/api/webhooks', data),
|
|
update: (id: number, data: Partial<WebhookForm>) => api.put<WebhookData>(`/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<string, any>
|
|
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<LogData[]>('/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<CustomPageData[]>('/api/custom-pages'),
|
|
listPublic: () => axios.get<CustomPageData[]>('/api/custom-pages/public'),
|
|
create: (data: CustomPageForm) => api.post<CustomPageData>('/api/custom-pages', data),
|
|
update: (id: number, data: Partial<CustomPageForm>) => api.put<CustomPageData>(`/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<BrowserSessionData>('/api/browser-sessions', data),
|
|
get: (id: string) => api.get<BrowserSessionData>(`/api/browser-sessions/${id}`),
|
|
event: (id: string, data: BrowserEventPayload) =>
|
|
api.post<BrowserSessionData>(`/api/browser-sessions/${id}/events`, data),
|
|
activateTab: (id: string, tabId: string) =>
|
|
api.post<BrowserSessionData>(`/api/browser-sessions/${id}/tabs/${tabId}/activate`),
|
|
closeTab: (id: string, tabId: string) =>
|
|
api.delete<BrowserSessionData>(`/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<string, any>[]
|
|
storage: Record<string, string>
|
|
session_storage: Record<string, string>
|
|
auth_headers: Record<string, string>[]
|
|
candidates: AuthCaptureCandidate[]
|
|
}
|
|
|
|
export const authCaptureApi = {
|
|
createSession: (url: string, width?: number, height?: number) =>
|
|
api.post<AuthCaptureSession>('/api/auth-capture/sessions', { url, width, height }),
|
|
extract: (sessionId: string, options?: { includeRaw?: boolean }) =>
|
|
api.get<AuthCaptureResult>(`/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()}`
|
|
},
|
|
}
|