Files
Cli-Proxy-API-Management-Ce…/src/services/api/client.ts
2026-02-14 00:16:14 +08:00

246 lines
7.0 KiB
TypeScript

/**
* Axios API 客户端
* 替代原项目 src/core/api-client.js
*/
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import type { ApiClientConfig, ApiError } from '@/types';
import {
BUILD_DATE_HEADER_KEYS,
REQUEST_TIMEOUT_MS,
VERSION_HEADER_KEYS
} from '@/utils/constants';
import { computeApiUrl } from '@/utils/connection';
class ApiClient {
private instance: AxiosInstance;
private apiBase: string = '';
private managementKey: string = '';
constructor() {
this.instance = axios.create({
timeout: REQUEST_TIMEOUT_MS,
headers: {
'Content-Type': 'application/json'
}
});
this.setupInterceptors();
}
/**
* 设置 API 配置
*/
setConfig(config: ApiClientConfig): void {
this.apiBase = computeApiUrl(config.apiBase);
this.managementKey = config.managementKey;
if (config.timeout) {
this.instance.defaults.timeout = config.timeout;
} else {
this.instance.defaults.timeout = REQUEST_TIMEOUT_MS;
}
}
private readHeader(
headers: Record<string, unknown> | undefined,
keys: string[]
): string | null {
if (!headers) return null;
const normalizeValue = (value: unknown): string | null => {
if (value === undefined || value === null) return null;
if (Array.isArray(value)) {
const first = value.find((entry) => entry !== undefined && entry !== null && String(entry).trim());
return first !== undefined ? String(first) : null;
}
const text = String(value);
return text ? text : null;
};
const headerGetter = (headers as { get?: (name: string) => unknown }).get;
if (typeof headerGetter === 'function') {
for (const key of keys) {
const match = normalizeValue(headerGetter.call(headers, key));
if (match) return match;
}
}
const entries =
typeof (headers as { entries?: () => Iterable<[string, unknown]> }).entries === 'function'
? Array.from((headers as { entries: () => Iterable<[string, unknown]> }).entries())
: Object.entries(headers);
const normalized = Object.fromEntries(
entries.map(([key, value]) => [String(key).toLowerCase(), value])
);
for (const key of keys) {
const match = normalizeValue(normalized[key.toLowerCase()]);
if (match) return match;
}
return null;
}
/**
* 设置请求/响应拦截器
*/
private setupInterceptors(): void {
// 请求拦截器
this.instance.interceptors.request.use(
(config) => {
// 设置 baseURL
config.baseURL = this.apiBase;
if (config.url) {
// Normalize deprecated Gemini endpoint to the current path.
config.url = config.url.replace(/\/generative-language-api-key\b/g, '/gemini-api-key');
}
// 添加认证头
if (this.managementKey) {
config.headers.Authorization = `Bearer ${this.managementKey}`;
}
return config;
},
(error) => Promise.reject(this.handleError(error))
);
// 响应拦截器
this.instance.interceptors.response.use(
(response) => {
const headers = response.headers as Record<string, string | undefined>;
const version = this.readHeader(headers, VERSION_HEADER_KEYS);
const buildDate = this.readHeader(headers, BUILD_DATE_HEADER_KEYS);
// 触发版本更新事件(后续通过 store 处理)
if (version || buildDate) {
window.dispatchEvent(
new CustomEvent('server-version-update', {
detail: { version: version || null, buildDate: buildDate || null }
})
);
}
return response;
},
(error) => Promise.reject(this.handleError(error))
);
}
/**
* 错误处理
*/
private handleError(error: unknown): ApiError {
const isRecord = (value: unknown): value is Record<string, unknown> =>
value !== null && typeof value === 'object';
if (axios.isAxiosError(error)) {
const responseData: unknown = error.response?.data;
const responseRecord = isRecord(responseData) ? responseData : null;
const errorValue = responseRecord?.error;
const message =
typeof errorValue === 'string'
? errorValue
: isRecord(errorValue) && typeof errorValue.message === 'string'
? errorValue.message
: typeof responseRecord?.message === 'string'
? responseRecord.message
: error.message || 'Request failed';
const apiError = new Error(message) as ApiError;
apiError.name = 'ApiError';
apiError.status = error.response?.status;
apiError.code = error.code;
apiError.details = responseData;
apiError.data = responseData;
// 401 未授权 - 触发登出事件
if (error.response?.status === 401) {
window.dispatchEvent(new Event('unauthorized'));
}
return apiError;
}
const fallbackMessage =
error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error occurred';
const fallback = new Error(fallbackMessage) as ApiError;
fallback.name = 'ApiError';
return fallback;
}
/**
* GET 请求
*/
async get<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.instance.get<T>(url, config);
return response.data;
}
/**
* POST 请求
*/
async post<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
const response = await this.instance.post<T>(url, data, config);
return response.data;
}
/**
* PUT 请求
*/
async put<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
const response = await this.instance.put<T>(url, data, config);
return response.data;
}
/**
* PATCH 请求
*/
async patch<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {
const response = await this.instance.patch<T>(url, data, config);
return response.data;
}
/**
* DELETE 请求
*/
async delete<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.instance.delete<T>(url, config);
return response.data;
}
/**
* 获取原始响应(用于下载等场景)
*/
async getRaw(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse> {
return this.instance.get(url, config);
}
/**
* 发送 FormData
*/
async postForm<T = unknown>(
url: string,
formData: FormData,
config?: AxiosRequestConfig
): Promise<T> {
const response = await this.instance.post<T>(url, formData, {
...config,
headers: {
...(config?.headers || {}),
'Content-Type': 'multipart/form-data'
}
});
return response.data;
}
/**
* 保留对 axios.request 的访问,便于下载等场景
*/
async requestRaw(config: AxiosRequestConfig): Promise<AxiosResponse> {
return this.instance.request(config);
}
}
// 导出单例
export const apiClient = new ApiClient();