app / src /utils /request.ts
AZILS's picture
Upload 323 files
a21c316 verified
// 探测环境
const isTauri = typeof window !== 'undefined' && (!!(window as any).__TAURI_INTERNALS__ || !!(window as any).__TAURI__);
// 命令到 API 的映射
const COMMAND_MAPPING: Record<string, { url: string; method: 'GET' | 'POST' | 'DELETE' | 'PATCH' }> = {
// Accounts
'list_accounts': { url: '/api/accounts', method: 'GET' },
'get_current_account': { url: '/api/accounts/current', method: 'GET' },
'switch_account': { url: '/api/accounts/switch', method: 'POST' },
'add_account': { url: '/api/accounts', method: 'POST' },
'delete_account': { url: '/api/accounts/:accountId', method: 'DELETE' },
'delete_accounts': { url: '/api/accounts/bulk-delete', method: 'POST' },
'fetch_account_quota': { url: '/api/accounts/:accountId/quota', method: 'GET' },
'refresh_account_quota': { url: '/api/accounts/:accountId/quota', method: 'GET' },
'refresh_all_quotas': { url: '/api/accounts/refresh', method: 'POST' },
'reorder_accounts': { url: '/api/accounts/reorder', method: 'POST' },
'toggle_proxy_status': { url: '/api/accounts/:accountId/toggle-proxy', method: 'POST' },
'warm_up_accounts': { url: '/api/accounts/warmup', method: 'POST' },
'warm_up_all_accounts': { url: '/api/accounts/warmup', method: 'POST' },
'warm_up_account': { url: '/api/accounts/:accountId/warmup', method: 'POST' },
'update_account_label': { url: '/api/accounts/:accountId/label', method: 'POST' },
'export_accounts': { url: '/api/accounts/export', method: 'POST' },
'bind_device_profile': { url: '/api/accounts/:accountId/bind-device', method: 'POST' },
'get_device_profiles': { url: '/api/accounts/:accountId/device-profiles', method: 'GET' },
'list_device_versions': { url: '/api/accounts/:accountId/device-versions', method: 'GET' },
'preview_generate_profile': { url: '/api/accounts/device-preview', method: 'POST' },
'bind_device_profile_with_profile': { url: '/api/accounts/:accountId/bind-device-profile', method: 'POST' },
'restore_original_device': { url: '/api/accounts/restore-original', method: 'POST' },
'restore_device_version': { url: '/api/accounts/:accountId/device-versions/:versionId/restore', method: 'POST' },
'delete_device_version': { url: '/api/accounts/:accountId/device-versions/:versionId', method: 'DELETE' },
'open_device_folder': { url: '/api/system/open-folder', method: 'POST' },
// Proxy Control & Status
'get_proxy_status': { url: '/api/proxy/status', method: 'GET' },
'start_proxy_service': { url: '/api/proxy/start', method: 'POST' },
'stop_proxy_service': { url: '/api/proxy/stop', method: 'POST' },
'update_model_mapping': { url: '/api/proxy/mapping', method: 'POST' },
'generate_api_key': { url: '/api/proxy/api-key/generate', method: 'POST' },
'clear_proxy_session_bindings': { url: '/api/proxy/session-bindings/clear', method: 'POST' },
'clear_proxy_rate_limit': { url: '/api/proxy/rate-limits/:accountId', method: 'DELETE' },
'clear_all_proxy_rate_limits': { url: '/api/proxy/rate-limits', method: 'DELETE' },
'check_proxy_health': { url: '/api/proxy/health-check/trigger', method: 'POST' },
'get_preferred_account': { url: '/api/proxy/preferred-account', method: 'GET' },
'set_preferred_account': { url: '/api/proxy/preferred-account', method: 'POST' },
'fetch_zai_models': { url: '/api/zai/models/fetch', method: 'POST' },
'load_config': { url: '/api/config', method: 'GET' },
'save_config': { url: '/api/config', method: 'POST' },
'get_proxy_stats': { url: '/api/proxy/stats', method: 'GET' },
'set_proxy_monitor_enabled': { url: '/api/proxy/monitor/toggle', method: 'POST' },
// Logs & Monitoring
'get_proxy_logs_filtered': { url: '/api/logs', method: 'GET' },
'get_proxy_logs_count_filtered': { url: '/api/logs/count', method: 'GET' },
'clear_proxy_logs': { url: '/api/logs/clear', method: 'POST' },
'get_proxy_log_detail': { url: '/api/logs/:logId', method: 'GET' },
// Debug Console
'enable_debug_console': { url: '/api/debug/enable', method: 'POST' },
'disable_debug_console': { url: '/api/debug/disable', method: 'POST' },
'is_debug_console_enabled': { url: '/api/debug/enabled', method: 'GET' },
'get_debug_console_logs': { url: '/api/debug/logs', method: 'GET' },
'clear_debug_console_logs': { url: '/api/debug/logs/clear', method: 'POST' },
// CLI Sync
'get_cli_sync_status': { url: '/api/proxy/cli/status', method: 'POST' },
'execute_cli_sync': { url: '/api/proxy/cli/sync', method: 'POST' },
'execute_cli_restore': { url: '/api/proxy/cli/restore', method: 'POST' },
'get_cli_config_content': { url: '/api/proxy/cli/config', method: 'POST' },
// OpenCode Sync
'get_opencode_sync_status': { url: '/api/proxy/opencode/status', method: 'POST' },
'execute_opencode_sync': { url: '/api/proxy/opencode/sync', method: 'POST' },
'execute_opencode_restore': { url: '/api/proxy/opencode/restore', method: 'POST' },
'execute_opencode_clear': { url: '/api/proxy/opencode/clear', method: 'POST' },
'get_opencode_config_content': { url: '/api/proxy/opencode/config', method: 'POST' },
// Stats
'get_token_stats_hourly': { url: '/api/stats/token/hourly', method: 'GET' },
'get_token_stats_daily': { url: '/api/stats/token/daily', method: 'GET' },
'get_token_stats_weekly': { url: '/api/stats/token/weekly', method: 'GET' },
'get_token_stats_by_account': { url: '/api/stats/token/by-account', method: 'GET' },
'get_token_stats_summary': { url: '/api/stats/token/summary', method: 'GET' },
'get_token_stats_by_model': { url: '/api/stats/token/by-model', method: 'GET' },
'get_token_stats_model_trend_hourly': { url: '/api/stats/token/model-trend/hourly', method: 'GET' },
'get_token_stats_model_trend_daily': { url: '/api/stats/token/model-trend/daily', method: 'GET' },
'get_token_stats_account_trend_hourly': { url: '/api/stats/token/account-trend/hourly', method: 'GET' },
'get_token_stats_account_trend_daily': { url: '/api/stats/token/account-trend/daily', method: 'GET' },
'clear_token_stats': { url: '/api/stats/token/clear', method: 'POST' },
// System
'get_data_dir_path': { url: '/api/system/data-dir', method: 'GET' },
'get_update_settings': { url: '/api/system/updates/settings', method: 'GET' },
'save_update_settings': { url: '/api/system/updates/save', method: 'POST' },
'is_auto_launch_enabled': { url: '/api/system/autostart/status', method: 'GET' },
'toggle_auto_launch': { url: '/api/system/autostart/toggle', method: 'POST' },
'get_http_api_settings': { url: '/api/system/http-api/settings', method: 'GET' },
'save_http_api_settings': { url: '/api/system/http-api/settings', method: 'POST' },
'get_antigravity_path': { url: '/api/system/antigravity/path', method: 'GET' },
'get_antigravity_args': { url: '/api/system/antigravity/args', method: 'GET' },
// Cloudflared
'cloudflared_install': { url: '/api/proxy/cloudflared/install', method: 'POST' },
'cloudflared_start': { url: '/api/proxy/cloudflared/start', method: 'POST' },
'cloudflared_stop': { url: '/api/proxy/cloudflared/stop', method: 'POST' },
'cloudflared_get_status': { url: '/api/proxy/cloudflared/status', method: 'GET' },
// Updates
'should_check_updates': { url: '/api/system/updates/check-status', method: 'GET' },
'check_for_updates': { url: '/api/system/updates/check', method: 'POST' },
'update_last_check_time': { url: '/api/system/updates/touch', method: 'POST' },
// OAuth
'prepare_oauth_url': { url: '/api/auth/url', method: 'GET' },
'start_oauth_login': { url: '/api/accounts/oauth/start', method: 'POST' },
'complete_oauth_login': { url: '/api/accounts/oauth/complete', method: 'POST' },
'cancel_oauth_login': { url: '/api/accounts/oauth/cancel', method: 'POST' },
'submit_oauth_code': { url: '/api/accounts/oauth/submit-code', method: 'POST' },
'list_oauth_clients': { url: '/api/accounts/oauth/clients', method: 'GET' },
'get_active_oauth_client': { url: '/api/accounts/oauth/client', method: 'GET' },
'set_active_oauth_client': { url: '/api/accounts/oauth/client', method: 'POST' },
// Import
'import_v1_accounts': { url: '/api/accounts/import/v1', method: 'POST' },
'import_from_db': { url: '/api/accounts/import/db', method: 'POST' },
'import_custom_db': { url: '/api/accounts/import/db-custom', method: 'POST' },
'sync_account_from_db': { url: '/api/accounts/sync/db', method: 'POST' },
// System Extra & Cache
'open_data_folder': { url: '/api/system/open-folder', method: 'POST' },
'clear_antigravity_cache': { url: '/api/system/cache/clear', method: 'POST' },
'get_antigravity_cache_paths': { url: '/api/system/cache/paths', method: 'GET' },
'clear_log_cache': { url: '/api/system/logs/clear-cache', method: 'POST' },
// Security / IP Management
'get_ip_access_logs': { url: '/api/security/logs', method: 'GET' },
'clear_ip_access_logs': { url: '/api/security/logs/clear', method: 'POST' },
'get_ip_stats': { url: '/api/security/stats', method: 'GET' },
'get_ip_token_stats': { url: '/api/security/token-stats', method: 'GET' },
'get_ip_blacklist': { url: '/api/security/blacklist', method: 'GET' },
'add_ip_to_blacklist': { url: '/api/security/blacklist', method: 'POST' },
'remove_ip_from_blacklist': { url: '/api/security/blacklist', method: 'DELETE' },
'clear_ip_blacklist': { url: '/api/security/blacklist/clear', method: 'POST' },
'check_ip_in_blacklist': { url: '/api/security/blacklist/check', method: 'GET' },
'get_ip_whitelist': { url: '/api/security/whitelist', method: 'GET' },
'add_ip_to_whitelist': { url: '/api/security/whitelist', method: 'POST' },
'remove_ip_from_whitelist': { url: '/api/security/whitelist', method: 'DELETE' },
'clear_ip_whitelist': { url: '/api/security/whitelist/clear', method: 'POST' },
'check_ip_in_whitelist': { url: '/api/security/whitelist/check', method: 'GET' },
'get_security_config': { url: '/api/security/config', method: 'GET' },
'update_security_config': { url: '/api/security/config', method: 'POST' },
// User Tokens
'list_user_tokens': { url: '/api/user-tokens', method: 'GET' },
'get_user_token_summary': { url: '/api/user-tokens/summary', method: 'GET' },
'create_user_token': { url: '/api/user-tokens', method: 'POST' },
'renew_user_token': { url: '/api/user-tokens/:id/renew', method: 'POST' },
'delete_user_token': { url: '/api/user-tokens/:id', method: 'DELETE' },
'update_user_token': { url: '/api/user-tokens/:id', method: 'PATCH' },
// Proxy Pool (Web Mode Fix)
'get_proxy_pool_config': { url: '/api/proxy/pool/config', method: 'GET' },
'get_all_account_bindings': { url: '/api/proxy/pool/bindings', method: 'GET' },
'bind_account_proxy': { url: '/api/proxy/pool/bind', method: 'POST' },
'unbind_account_proxy': { url: '/api/proxy/pool/unbind', method: 'POST' },
'get_account_proxy_binding': { url: '/api/proxy/pool/binding/:accountId', method: 'GET' },
};
export async function request<T>(cmd: string, args?: any): Promise<T> {
// 1. Tauri 环境:直接使用 invoke ...
if (isTauri) {
try {
const { invoke } = await import('@tauri-apps/api/core');
return await invoke<T>(cmd, args);
} catch (error) {
console.error(`Tauri Invoke Error [${cmd}]:`, error);
throw error;
}
}
// 2. Web 环境:映射到 HTTP API
const mapping = COMMAND_MAPPING[cmd];
if (!mapping) {
console.error(`Command [${cmd}] is not yet mapped for Web mode. Failing.`);
throw new Error(`Command [${cmd}] not supported in Web mode.`);
}
let url = mapping.url;
// [FIX] 创建 args 副本,用于移除已使用的路径参数
let bodyArgs = args ? { ...args } : undefined;
// 通用路径参数处理:替换 :key 为 args[key]
if (args) {
Object.keys(args).forEach(key => {
const placeholder = `:${key}`;
if (url.includes(placeholder)) {
url = url.replace(placeholder, encodeURIComponent(String(args[key])));
// [FIX] 从 body 参数中移除已用于路径的参数
if (bodyArgs) {
delete bodyArgs[key];
}
}
});
}
const apiKey = typeof window !== 'undefined' ? sessionStorage.getItem('abv_admin_api_key') : null;
const options: RequestInit = {
method: mapping.method,
headers: {
'Content-Type': 'application/json',
...(apiKey ? {
'Authorization': `Bearer ${apiKey}`,
'x-api-key': apiKey
} : {}),
},
};
if ((mapping.method === 'GET' || mapping.method === 'DELETE') && args) {
const params = new URLSearchParams();
Object.entries(args).forEach(([key, value]) => {
// [FIX] 跳过已用于路径替换的参数
if (url.includes(encodeURIComponent(String(value)))) return;
if (value !== undefined && value !== null) {
params.append(key, String(value));
}
});
const qs = params.toString();
if (qs) url += `?${qs}`;
} else if ((mapping.method === 'POST' || mapping.method === 'PATCH') && bodyArgs) {
// [FIX] 如果有 request 包装,提取其内容作为 body
const body = bodyArgs.request !== undefined ? bodyArgs.request : bodyArgs;
options.body = JSON.stringify(body);
}
try {
const response = await fetch(url, options);
if (!response.ok) {
if (!isTauri && response.status === 401) {
// [FIX #1163] 增加防抖锁,避免重复事件导致 UI 抖动
const now = Date.now();
const lastAuthError = (window as any)._lastAuthErrorTime || 0;
if (now - lastAuthError > 2000) {
(window as any)._lastAuthErrorTime = now;
window.dispatchEvent(new CustomEvent('abv-unauthorized'));
}
}
const errorData = await response.json().catch(() => ({}));
throw errorData.error || `HTTP Error ${response.status}`;
}
// 如果是 204 No Content,直接返回 null
if (response.status === 204) {
return null as unknown as T;
}
const text = await response.text();
if (!text) {
return null as unknown as T;
}
try {
return JSON.parse(text) as T;
} catch (e) {
console.warn(`Failed to parse JSON response for [${cmd}]:`, text);
return text as unknown as T; // Fallback for plain text responses
}
} catch (error) {
console.error(`Web Fetch Error [${cmd}]:`, error);
throw error;
}
}