mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-18 02:30:51 +08:00
feat: initialize new React application structure with TypeScript, ESLint, and Prettier configurations, while removing legacy files and adding new components and pages for enhanced functionality
This commit is contained in:
19
src/services/api/apiKeys.ts
Normal file
19
src/services/api/apiKeys.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* API 密钥管理
|
||||
*/
|
||||
|
||||
import { apiClient } from './client';
|
||||
|
||||
export const apiKeysApi = {
|
||||
async list(): Promise<string[]> {
|
||||
const data = await apiClient.get('/api-keys');
|
||||
const keys = (data && (data['api-keys'] ?? data.apiKeys)) as unknown;
|
||||
return Array.isArray(keys) ? (keys as string[]) : [];
|
||||
},
|
||||
|
||||
replace: (keys: string[]) => apiClient.put('/api-keys', keys),
|
||||
|
||||
update: (index: number, value: string) => apiClient.patch('/api-keys', { index, value }),
|
||||
|
||||
delete: (index: number) => apiClient.delete(`/api-keys?index=${index}`)
|
||||
};
|
||||
29
src/services/api/authFiles.ts
Normal file
29
src/services/api/authFiles.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 认证文件与 OAuth 排除模型相关 API
|
||||
*/
|
||||
|
||||
import { apiClient } from './client';
|
||||
import type { AuthFilesResponse } from '@/types/authFile';
|
||||
|
||||
export const authFilesApi = {
|
||||
list: () => apiClient.get<AuthFilesResponse>('/auth-files'),
|
||||
|
||||
upload: (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file, file.name);
|
||||
return apiClient.postForm('/auth-files', formData);
|
||||
},
|
||||
|
||||
deleteFile: (name: string) => apiClient.delete(`/auth-files?name=${encodeURIComponent(name)}`),
|
||||
|
||||
deleteAll: () => apiClient.delete('/auth-files', { params: { all: true } }),
|
||||
|
||||
// OAuth 排除模型
|
||||
getOauthExcludedModels: () => apiClient.get('/oauth-excluded-models'),
|
||||
|
||||
saveOauthExcludedModels: (provider: string, models: string[]) =>
|
||||
apiClient.patch('/oauth-excluded-models', { provider, models }),
|
||||
|
||||
deleteOauthExcludedEntry: (provider: string) =>
|
||||
apiClient.delete(`/oauth-excluded-models?provider=${encodeURIComponent(provider)}`)
|
||||
};
|
||||
215
src/services/api/client.ts
Normal file
215
src/services/api/client.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* 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,
|
||||
MANAGEMENT_API_PREFIX,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
VERSION_HEADER_KEYS
|
||||
} from '@/utils/constants';
|
||||
|
||||
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 = this.normalizeApiBase(config.apiBase);
|
||||
this.managementKey = config.managementKey;
|
||||
|
||||
if (config.timeout) {
|
||||
this.instance.defaults.timeout = config.timeout;
|
||||
} else {
|
||||
this.instance.defaults.timeout = REQUEST_TIMEOUT_MS;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化 API Base URL
|
||||
*/
|
||||
private normalizeApiBase(base: string): string {
|
||||
let normalized = base.trim();
|
||||
|
||||
// 移除尾部的 /v0/management
|
||||
normalized = normalized.replace(/\/?v0\/management\/?$/i, '');
|
||||
|
||||
// 移除尾部斜杠
|
||||
normalized = normalized.replace(/\/+$/, '');
|
||||
|
||||
// 添加协议
|
||||
if (!/^https?:\/\//i.test(normalized)) {
|
||||
normalized = `http://${normalized}`;
|
||||
}
|
||||
|
||||
return `${normalized}${MANAGEMENT_API_PREFIX}`;
|
||||
}
|
||||
|
||||
private readHeader(headers: Record<string, any>, keys: string[]): string | null {
|
||||
const normalized = Object.fromEntries(
|
||||
Object.entries(headers || {}).map(([key, value]) => [key.toLowerCase(), value as string | undefined])
|
||||
);
|
||||
for (const key of keys) {
|
||||
const match = normalized[key.toLowerCase()];
|
||||
if (match) return match;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置请求/响应拦截器
|
||||
*/
|
||||
private setupInterceptors(): void {
|
||||
// 请求拦截器
|
||||
this.instance.interceptors.request.use(
|
||||
(config) => {
|
||||
// 设置 baseURL
|
||||
config.baseURL = this.apiBase;
|
||||
|
||||
// 添加认证头
|
||||
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: any): ApiError {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const responseData = error.response?.data as any;
|
||||
const message = responseData?.error || responseData?.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 fallback = new Error(error?.message || 'Unknown error occurred') as ApiError;
|
||||
fallback.name = 'ApiError';
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET 请求
|
||||
*/
|
||||
async get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||
const response = await this.instance.get<T>(url, config);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 请求
|
||||
*/
|
||||
async post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||
const response = await this.instance.post<T>(url, data, config);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT 请求
|
||||
*/
|
||||
async put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||
const response = await this.instance.put<T>(url, data, config);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH 请求
|
||||
*/
|
||||
async patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||
const response = await this.instance.patch<T>(url, data, config);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE 请求
|
||||
*/
|
||||
async delete<T = any>(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 = any>(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();
|
||||
80
src/services/api/config.ts
Normal file
80
src/services/api/config.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 配置相关 API
|
||||
*/
|
||||
|
||||
import { apiClient } from './client';
|
||||
import type { Config } from '@/types';
|
||||
import { normalizeConfigResponse } from './transformers';
|
||||
|
||||
export const configApi = {
|
||||
/**
|
||||
* 获取配置(会进行字段规范化)
|
||||
*/
|
||||
async getConfig(): Promise<Config> {
|
||||
const raw = await apiClient.get('/config');
|
||||
return normalizeConfigResponse(raw);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取原始配置(不做转换)
|
||||
*/
|
||||
getRawConfig: () => apiClient.get('/config'),
|
||||
|
||||
/**
|
||||
* 更新 Debug 模式
|
||||
*/
|
||||
updateDebug: (enabled: boolean) => apiClient.put('/debug', { value: enabled }),
|
||||
|
||||
/**
|
||||
* 更新代理 URL
|
||||
*/
|
||||
updateProxyUrl: (proxyUrl: string) => apiClient.put('/proxy-url', { value: proxyUrl }),
|
||||
|
||||
/**
|
||||
* 清除代理 URL
|
||||
*/
|
||||
clearProxyUrl: () => apiClient.delete('/proxy-url'),
|
||||
|
||||
/**
|
||||
* 更新重试次数
|
||||
*/
|
||||
updateRequestRetry: (retryCount: number) => apiClient.put('/request-retry', { value: retryCount }),
|
||||
|
||||
/**
|
||||
* 配额回退:切换项目
|
||||
*/
|
||||
updateSwitchProject: (enabled: boolean) =>
|
||||
apiClient.put('/quota-exceeded/switch-project', { value: enabled }),
|
||||
|
||||
/**
|
||||
* 配额回退:切换预览模型
|
||||
*/
|
||||
updateSwitchPreviewModel: (enabled: boolean) =>
|
||||
apiClient.put('/quota-exceeded/switch-preview-model', { value: enabled }),
|
||||
|
||||
/**
|
||||
* 使用统计开关
|
||||
*/
|
||||
updateUsageStatistics: (enabled: boolean) =>
|
||||
apiClient.put('/usage-statistics-enabled', { value: enabled }),
|
||||
|
||||
/**
|
||||
* 请求日志开关
|
||||
*/
|
||||
updateRequestLog: (enabled: boolean) => apiClient.put('/request-log', { value: enabled }),
|
||||
|
||||
/**
|
||||
* 写日志到文件开关
|
||||
*/
|
||||
updateLoggingToFile: (enabled: boolean) => apiClient.put('/logging-to-file', { value: enabled }),
|
||||
|
||||
/**
|
||||
* WebSocket 鉴权开关
|
||||
*/
|
||||
updateWsAuth: (enabled: boolean) => apiClient.put('/ws-auth', { value: enabled }),
|
||||
|
||||
/**
|
||||
* 重载配置
|
||||
*/
|
||||
reloadConfig: () => apiClient.post('/config/reload')
|
||||
};
|
||||
27
src/services/api/configFile.ts
Normal file
27
src/services/api/configFile.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 配置文件相关 API(/config.yaml)
|
||||
*/
|
||||
|
||||
import { apiClient } from './client';
|
||||
|
||||
export const configFileApi = {
|
||||
async fetchConfigYaml(): Promise<string> {
|
||||
const response = await apiClient.getRaw('/config.yaml', {
|
||||
responseType: 'text',
|
||||
headers: { Accept: 'application/yaml, text/yaml, text/plain' }
|
||||
});
|
||||
const data = response.data as any;
|
||||
if (typeof data === 'string') return data;
|
||||
if (data === undefined || data === null) return '';
|
||||
return String(data);
|
||||
},
|
||||
|
||||
async saveConfigYaml(content: string): Promise<void> {
|
||||
await apiClient.put('/config.yaml', content, {
|
||||
headers: {
|
||||
'Content-Type': 'application/yaml',
|
||||
Accept: 'application/json, text/plain, */*'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
12
src/services/api/index.ts
Normal file
12
src/services/api/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export * from './client';
|
||||
export * from './config';
|
||||
export * from './configFile';
|
||||
export * from './apiKeys';
|
||||
export * from './providers';
|
||||
export * from './authFiles';
|
||||
export * from './oauth';
|
||||
export * from './usage';
|
||||
export * from './logs';
|
||||
export * from './version';
|
||||
export * from './models';
|
||||
export * from './transformers';
|
||||
23
src/services/api/logs.ts
Normal file
23
src/services/api/logs.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 日志相关 API
|
||||
*/
|
||||
|
||||
import { apiClient } from './client';
|
||||
|
||||
export interface LogsQuery {
|
||||
after?: string | number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export const logsApi = {
|
||||
fetchLogs: (params: LogsQuery = {}) => apiClient.get('/logs', { params }),
|
||||
|
||||
clearLogs: () => apiClient.delete('/logs'),
|
||||
|
||||
fetchErrorLogs: () => apiClient.get('/request-error-logs'),
|
||||
|
||||
downloadErrorLog: (filename: string) =>
|
||||
apiClient.getRaw(`/request-error-logs/${encodeURIComponent(filename)}`, {
|
||||
responseType: 'blob'
|
||||
})
|
||||
};
|
||||
31
src/services/api/models.ts
Normal file
31
src/services/api/models.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 可用模型获取
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { normalizeModelList } from '@/utils/models';
|
||||
|
||||
const buildModelsEndpoint = (baseUrl: string): string => {
|
||||
if (!baseUrl) return '';
|
||||
const trimmed = String(baseUrl).trim().replace(/\/+$/g, '');
|
||||
if (!trimmed) return '';
|
||||
if (trimmed.endsWith('/v1')) {
|
||||
return `${trimmed}/models`;
|
||||
}
|
||||
return `${trimmed}/v1/models`;
|
||||
};
|
||||
|
||||
export const modelsApi = {
|
||||
async fetchModels(baseUrl: string, apiKey?: string) {
|
||||
const endpoint = buildModelsEndpoint(baseUrl);
|
||||
if (!endpoint) {
|
||||
throw new Error('Invalid base url');
|
||||
}
|
||||
|
||||
const response = await axios.get(endpoint, {
|
||||
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined
|
||||
});
|
||||
const payload = response.data?.data ?? response.data?.models ?? response.data;
|
||||
return normalizeModelList(payload, { dedupe: true });
|
||||
}
|
||||
};
|
||||
27
src/services/api/oauth.ts
Normal file
27
src/services/api/oauth.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* OAuth 与设备码登录相关 API
|
||||
*/
|
||||
|
||||
import { apiClient } from './client';
|
||||
|
||||
export type OAuthProvider =
|
||||
| 'codex'
|
||||
| 'anthropic'
|
||||
| 'antigravity'
|
||||
| 'gemini-cli'
|
||||
| 'qwen'
|
||||
| 'iflow';
|
||||
|
||||
export interface OAuthStartResponse {
|
||||
url: string;
|
||||
state?: string;
|
||||
}
|
||||
|
||||
export const oauthApi = {
|
||||
startAuth: (provider: OAuthProvider) => apiClient.get<OAuthStartResponse>(`/${provider}-auth-url`, { params: { is_webui: 1 } }),
|
||||
|
||||
getAuthStatus: (state: string) =>
|
||||
apiClient.get<{ status: 'ok' | 'wait' | 'error'; error?: string }>(`/get-auth-status`, {
|
||||
params: { state }
|
||||
})
|
||||
};
|
||||
152
src/services/api/providers.ts
Normal file
152
src/services/api/providers.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* AI 提供商相关 API
|
||||
*/
|
||||
|
||||
import { apiClient } from './client';
|
||||
import {
|
||||
normalizeGeminiKeyConfig,
|
||||
normalizeOpenAIProvider,
|
||||
normalizeProviderKeyConfig
|
||||
} from './transformers';
|
||||
import type {
|
||||
GeminiKeyConfig,
|
||||
OpenAIProviderConfig,
|
||||
ProviderKeyConfig,
|
||||
ApiKeyEntry,
|
||||
ModelAlias
|
||||
} from '@/types';
|
||||
|
||||
const serializeHeaders = (headers?: Record<string, string>) => (headers && Object.keys(headers).length ? headers : undefined);
|
||||
|
||||
const serializeModelAliases = (models?: ModelAlias[]) =>
|
||||
Array.isArray(models)
|
||||
? models
|
||||
.map((model) => {
|
||||
if (!model?.name) return null;
|
||||
const payload: Record<string, any> = { name: model.name };
|
||||
if (model.alias && model.alias !== model.name) {
|
||||
payload.alias = model.alias;
|
||||
}
|
||||
if (model.priority !== undefined) {
|
||||
payload.priority = model.priority;
|
||||
}
|
||||
if (model.testModel) {
|
||||
payload['test-model'] = model.testModel;
|
||||
}
|
||||
return payload;
|
||||
})
|
||||
.filter(Boolean)
|
||||
: undefined;
|
||||
|
||||
const serializeApiKeyEntry = (entry: ApiKeyEntry) => {
|
||||
const payload: Record<string, any> = { 'api-key': entry.apiKey };
|
||||
if (entry.proxyUrl) payload['proxy-url'] = entry.proxyUrl;
|
||||
const headers = serializeHeaders(entry.headers);
|
||||
if (headers) payload.headers = headers;
|
||||
return payload;
|
||||
};
|
||||
|
||||
const serializeProviderKey = (config: ProviderKeyConfig) => {
|
||||
const payload: Record<string, any> = { 'api-key': config.apiKey };
|
||||
if (config.baseUrl) payload['base-url'] = config.baseUrl;
|
||||
if (config.proxyUrl) payload['proxy-url'] = config.proxyUrl;
|
||||
const headers = serializeHeaders(config.headers);
|
||||
if (headers) payload.headers = headers;
|
||||
const models = serializeModelAliases(config.models);
|
||||
if (models && models.length) payload.models = models;
|
||||
return payload;
|
||||
};
|
||||
|
||||
const serializeGeminiKey = (config: GeminiKeyConfig) => {
|
||||
const payload: Record<string, any> = { 'api-key': config.apiKey };
|
||||
if (config.baseUrl) payload['base-url'] = config.baseUrl;
|
||||
const headers = serializeHeaders(config.headers);
|
||||
if (headers) payload.headers = headers;
|
||||
if (config.excludedModels && config.excludedModels.length) {
|
||||
payload['excluded-models'] = config.excludedModels;
|
||||
}
|
||||
return payload;
|
||||
};
|
||||
|
||||
const serializeOpenAIProvider = (provider: OpenAIProviderConfig) => {
|
||||
const payload: Record<string, any> = {
|
||||
name: provider.name,
|
||||
'base-url': provider.baseUrl,
|
||||
'api-key-entries': Array.isArray(provider.apiKeyEntries)
|
||||
? provider.apiKeyEntries.map((entry) => serializeApiKeyEntry(entry))
|
||||
: []
|
||||
};
|
||||
const headers = serializeHeaders(provider.headers);
|
||||
if (headers) payload.headers = headers;
|
||||
const models = serializeModelAliases(provider.models);
|
||||
if (models && models.length) payload.models = models;
|
||||
if (provider.priority !== undefined) payload.priority = provider.priority;
|
||||
if (provider.testModel) payload['test-model'] = provider.testModel;
|
||||
return payload;
|
||||
};
|
||||
|
||||
export const providersApi = {
|
||||
async getGeminiKeys(): Promise<GeminiKeyConfig[]> {
|
||||
const data = await apiClient.get('/gemini-api-key');
|
||||
const list = (data && (data['gemini-api-key'] ?? data.items ?? data)) as any;
|
||||
if (!Array.isArray(list)) return [];
|
||||
return list.map((item) => normalizeGeminiKeyConfig(item)).filter(Boolean) as GeminiKeyConfig[];
|
||||
},
|
||||
|
||||
saveGeminiKeys: (configs: GeminiKeyConfig[]) =>
|
||||
apiClient.put('/gemini-api-key', configs.map((item) => serializeGeminiKey(item))),
|
||||
|
||||
updateGeminiKey: (index: number, value: GeminiKeyConfig) =>
|
||||
apiClient.patch('/gemini-api-key', { index, value: serializeGeminiKey(value) }),
|
||||
|
||||
deleteGeminiKey: (apiKey: string) =>
|
||||
apiClient.delete(`/gemini-api-key?api-key=${encodeURIComponent(apiKey)}`),
|
||||
|
||||
async getCodexConfigs(): Promise<ProviderKeyConfig[]> {
|
||||
const data = await apiClient.get('/codex-api-key');
|
||||
const list = (data && (data['codex-api-key'] ?? data.items ?? data)) as any;
|
||||
if (!Array.isArray(list)) return [];
|
||||
return list.map((item) => normalizeProviderKeyConfig(item)).filter(Boolean) as ProviderKeyConfig[];
|
||||
},
|
||||
|
||||
saveCodexConfigs: (configs: ProviderKeyConfig[]) =>
|
||||
apiClient.put('/codex-api-key', configs.map((item) => serializeProviderKey(item))),
|
||||
|
||||
updateCodexConfig: (index: number, value: ProviderKeyConfig) =>
|
||||
apiClient.patch('/codex-api-key', { index, value: serializeProviderKey(value) }),
|
||||
|
||||
deleteCodexConfig: (apiKey: string) =>
|
||||
apiClient.delete(`/codex-api-key?api-key=${encodeURIComponent(apiKey)}`),
|
||||
|
||||
async getClaudeConfigs(): Promise<ProviderKeyConfig[]> {
|
||||
const data = await apiClient.get('/claude-api-key');
|
||||
const list = (data && (data['claude-api-key'] ?? data.items ?? data)) as any;
|
||||
if (!Array.isArray(list)) return [];
|
||||
return list.map((item) => normalizeProviderKeyConfig(item)).filter(Boolean) as ProviderKeyConfig[];
|
||||
},
|
||||
|
||||
saveClaudeConfigs: (configs: ProviderKeyConfig[]) =>
|
||||
apiClient.put('/claude-api-key', configs.map((item) => serializeProviderKey(item))),
|
||||
|
||||
updateClaudeConfig: (index: number, value: ProviderKeyConfig) =>
|
||||
apiClient.patch('/claude-api-key', { index, value: serializeProviderKey(value) }),
|
||||
|
||||
deleteClaudeConfig: (apiKey: string) =>
|
||||
apiClient.delete(`/claude-api-key?api-key=${encodeURIComponent(apiKey)}`),
|
||||
|
||||
async getOpenAIProviders(): Promise<OpenAIProviderConfig[]> {
|
||||
const data = await apiClient.get('/openai-compatibility');
|
||||
const list = (data && (data['openai-compatibility'] ?? data.items ?? data)) as any;
|
||||
if (!Array.isArray(list)) return [];
|
||||
return list.map((item) => normalizeOpenAIProvider(item)).filter(Boolean) as OpenAIProviderConfig[];
|
||||
},
|
||||
|
||||
saveOpenAIProviders: (providers: OpenAIProviderConfig[]) =>
|
||||
apiClient.put('/openai-compatibility', providers.map((item) => serializeOpenAIProvider(item))),
|
||||
|
||||
updateOpenAIProvider: (index: number, value: OpenAIProviderConfig) =>
|
||||
apiClient.patch('/openai-compatibility', { index, value: serializeOpenAIProvider(value) }),
|
||||
|
||||
deleteOpenAIProvider: (name: string) =>
|
||||
apiClient.delete(`/openai-compatibility?name=${encodeURIComponent(name)}`)
|
||||
};
|
||||
232
src/services/api/transformers.ts
Normal file
232
src/services/api/transformers.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import type {
|
||||
ApiKeyEntry,
|
||||
GeminiKeyConfig,
|
||||
ModelAlias,
|
||||
OpenAIProviderConfig,
|
||||
ProviderKeyConfig
|
||||
} from '@/types';
|
||||
import type { Config } from '@/types/config';
|
||||
import { buildHeaderObject } from '@/utils/headers';
|
||||
|
||||
const normalizeModelAliases = (models: any): ModelAlias[] => {
|
||||
if (!Array.isArray(models)) return [];
|
||||
return models
|
||||
.map((item) => {
|
||||
if (!item) return null;
|
||||
const name = item.name || item.id || item.model;
|
||||
if (!name) return null;
|
||||
const alias = item.alias || item.display_name || item.displayName;
|
||||
const priority = item.priority ?? item['priority'];
|
||||
const testModel = item['test-model'] ?? item.testModel;
|
||||
const entry: ModelAlias = { name: String(name) };
|
||||
if (alias && alias !== name) {
|
||||
entry.alias = String(alias);
|
||||
}
|
||||
if (priority !== undefined) {
|
||||
entry.priority = Number(priority);
|
||||
}
|
||||
if (testModel) {
|
||||
entry.testModel = String(testModel);
|
||||
}
|
||||
return entry;
|
||||
})
|
||||
.filter(Boolean) as ModelAlias[];
|
||||
};
|
||||
|
||||
const normalizeHeaders = (headers: any) => {
|
||||
if (!headers || typeof headers !== 'object') return undefined;
|
||||
const normalized = buildHeaderObject(headers as Record<string, string>);
|
||||
return Object.keys(normalized).length ? normalized : undefined;
|
||||
};
|
||||
|
||||
const normalizeExcludedModels = (input: any): string[] => {
|
||||
const rawList = Array.isArray(input) ? input : typeof input === 'string' ? input.split(/[\n,]/) : [];
|
||||
const seen = new Set<string>();
|
||||
const normalized: string[] = [];
|
||||
|
||||
rawList.forEach((item) => {
|
||||
const trimmed = String(item ?? '').trim();
|
||||
if (!trimmed) return;
|
||||
const key = trimmed.toLowerCase();
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
normalized.push(trimmed);
|
||||
});
|
||||
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const normalizeApiKeyEntry = (entry: any): ApiKeyEntry | null => {
|
||||
if (!entry) return null;
|
||||
const apiKey = entry['api-key'] ?? entry.apiKey ?? entry.key ?? (typeof entry === 'string' ? entry : '');
|
||||
const trimmed = String(apiKey || '').trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
const proxyUrl = entry['proxy-url'] ?? entry.proxyUrl;
|
||||
const headers = normalizeHeaders(entry.headers);
|
||||
|
||||
return {
|
||||
apiKey: trimmed,
|
||||
proxyUrl: proxyUrl ? String(proxyUrl) : undefined,
|
||||
headers
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeProviderKeyConfig = (item: any): ProviderKeyConfig | null => {
|
||||
if (!item) return null;
|
||||
const apiKey = item['api-key'] ?? item.apiKey ?? (typeof item === 'string' ? item : '');
|
||||
const trimmed = String(apiKey || '').trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
const config: ProviderKeyConfig = { apiKey: trimmed };
|
||||
const baseUrl = item['base-url'] ?? item.baseUrl;
|
||||
const proxyUrl = item['proxy-url'] ?? item.proxyUrl;
|
||||
if (baseUrl) config.baseUrl = String(baseUrl);
|
||||
if (proxyUrl) config.proxyUrl = String(proxyUrl);
|
||||
const headers = normalizeHeaders(item.headers);
|
||||
if (headers) config.headers = headers;
|
||||
const models = normalizeModelAliases(item.models);
|
||||
if (models.length) config.models = models;
|
||||
return config;
|
||||
};
|
||||
|
||||
const normalizeGeminiKeyConfig = (item: any): GeminiKeyConfig | null => {
|
||||
if (!item) return null;
|
||||
let apiKey = item['api-key'] ?? item.apiKey;
|
||||
if (!apiKey && typeof item === 'string') {
|
||||
apiKey = item;
|
||||
}
|
||||
const trimmed = String(apiKey || '').trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
const config: GeminiKeyConfig = { apiKey: trimmed };
|
||||
const baseUrl = item['base-url'] ?? item.baseUrl ?? item['base_url'];
|
||||
if (baseUrl) config.baseUrl = String(baseUrl);
|
||||
const headers = normalizeHeaders(item.headers);
|
||||
if (headers) config.headers = headers;
|
||||
const excludedModels = normalizeExcludedModels(item['excluded-models'] ?? item.excludedModels);
|
||||
if (excludedModels.length) config.excludedModels = excludedModels;
|
||||
return config;
|
||||
};
|
||||
|
||||
const normalizeOpenAIProvider = (provider: any): OpenAIProviderConfig | null => {
|
||||
if (!provider || typeof provider !== 'object') return null;
|
||||
const name = provider.name || provider.id;
|
||||
const baseUrl = provider['base-url'] ?? provider.baseUrl;
|
||||
if (!name || !baseUrl) return null;
|
||||
|
||||
let apiKeyEntries: ApiKeyEntry[] = [];
|
||||
if (Array.isArray(provider['api-key-entries'])) {
|
||||
apiKeyEntries = provider['api-key-entries']
|
||||
.map((entry: any) => normalizeApiKeyEntry(entry))
|
||||
.filter(Boolean) as ApiKeyEntry[];
|
||||
} else if (Array.isArray(provider['api-keys'])) {
|
||||
apiKeyEntries = provider['api-keys']
|
||||
.map((key: any) => normalizeApiKeyEntry({ 'api-key': key }))
|
||||
.filter(Boolean) as ApiKeyEntry[];
|
||||
}
|
||||
|
||||
const headers = normalizeHeaders(provider.headers);
|
||||
const models = normalizeModelAliases(provider.models);
|
||||
const priority = provider.priority ?? provider['priority'];
|
||||
const testModel = provider['test-model'] ?? provider.testModel;
|
||||
|
||||
const result: OpenAIProviderConfig = {
|
||||
name: String(name),
|
||||
baseUrl: String(baseUrl),
|
||||
apiKeyEntries
|
||||
};
|
||||
|
||||
if (headers) result.headers = headers;
|
||||
if (models.length) result.models = models;
|
||||
if (priority !== undefined) result.priority = Number(priority);
|
||||
if (testModel) result.testModel = String(testModel);
|
||||
return result;
|
||||
};
|
||||
|
||||
const normalizeOauthExcluded = (payload: any): Record<string, string[]> | undefined => {
|
||||
if (!payload || typeof payload !== 'object') return undefined;
|
||||
const source = payload['oauth-excluded-models'] ?? payload.items ?? payload;
|
||||
if (!source || typeof source !== 'object') return undefined;
|
||||
const map: Record<string, string[]> = {};
|
||||
Object.entries(source).forEach(([provider, models]) => {
|
||||
const key = String(provider || '').trim();
|
||||
if (!key) return;
|
||||
const normalized = normalizeExcludedModels(models);
|
||||
map[key.toLowerCase()] = normalized;
|
||||
});
|
||||
return map;
|
||||
};
|
||||
|
||||
/**
|
||||
* 规范化 /config 返回值
|
||||
*/
|
||||
export const normalizeConfigResponse = (raw: any): Config => {
|
||||
const config: Config = { raw: raw || {} };
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return config;
|
||||
}
|
||||
|
||||
config.debug = raw.debug;
|
||||
config.proxyUrl = raw['proxy-url'] ?? raw.proxyUrl;
|
||||
config.requestRetry = raw['request-retry'] ?? raw.requestRetry;
|
||||
|
||||
const quota = raw['quota-exceeded'] ?? raw.quotaExceeded;
|
||||
if (quota && typeof quota === 'object') {
|
||||
config.quotaExceeded = {
|
||||
switchProject: quota['switch-project'] ?? quota.switchProject,
|
||||
switchPreviewModel: quota['switch-preview-model'] ?? quota.switchPreviewModel
|
||||
};
|
||||
}
|
||||
|
||||
config.usageStatisticsEnabled = raw['usage-statistics-enabled'] ?? raw.usageStatisticsEnabled;
|
||||
config.requestLog = raw['request-log'] ?? raw.requestLog;
|
||||
config.loggingToFile = raw['logging-to-file'] ?? raw.loggingToFile;
|
||||
config.wsAuth = raw['ws-auth'] ?? raw.wsAuth;
|
||||
config.apiKeys = Array.isArray(raw['api-keys']) ? raw['api-keys'].slice() : raw.apiKeys;
|
||||
|
||||
const geminiList = raw['gemini-api-key'] ?? raw.geminiApiKey ?? raw.geminiApiKeys;
|
||||
if (Array.isArray(geminiList)) {
|
||||
config.geminiApiKeys = geminiList
|
||||
.map((item: any) => normalizeGeminiKeyConfig(item))
|
||||
.filter(Boolean) as GeminiKeyConfig[];
|
||||
}
|
||||
|
||||
const codexList = raw['codex-api-key'] ?? raw.codexApiKey ?? raw.codexApiKeys;
|
||||
if (Array.isArray(codexList)) {
|
||||
config.codexApiKeys = codexList
|
||||
.map((item: any) => normalizeProviderKeyConfig(item))
|
||||
.filter(Boolean) as ProviderKeyConfig[];
|
||||
}
|
||||
|
||||
const claudeList = raw['claude-api-key'] ?? raw.claudeApiKey ?? raw.claudeApiKeys;
|
||||
if (Array.isArray(claudeList)) {
|
||||
config.claudeApiKeys = claudeList
|
||||
.map((item: any) => normalizeProviderKeyConfig(item))
|
||||
.filter(Boolean) as ProviderKeyConfig[];
|
||||
}
|
||||
|
||||
const openaiList = raw['openai-compatibility'] ?? raw.openaiCompatibility ?? raw.openAICompatibility;
|
||||
if (Array.isArray(openaiList)) {
|
||||
config.openaiCompatibility = openaiList
|
||||
.map((item: any) => normalizeOpenAIProvider(item))
|
||||
.filter(Boolean) as OpenAIProviderConfig[];
|
||||
}
|
||||
|
||||
const oauthExcluded = normalizeOauthExcluded(raw['oauth-excluded-models'] ?? raw.oauthExcludedModels);
|
||||
if (oauthExcluded) {
|
||||
config.oauthExcludedModels = oauthExcluded;
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
export {
|
||||
normalizeApiKeyEntry,
|
||||
normalizeGeminiKeyConfig,
|
||||
normalizeModelAliases,
|
||||
normalizeOpenAIProvider,
|
||||
normalizeProviderKeyConfig,
|
||||
normalizeHeaders,
|
||||
normalizeExcludedModels
|
||||
};
|
||||
25
src/services/api/usage.ts
Normal file
25
src/services/api/usage.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 使用统计相关 API
|
||||
*/
|
||||
|
||||
import { apiClient } from './client';
|
||||
import { computeKeyStats, KeyStats } from '@/utils/usage';
|
||||
|
||||
export const usageApi = {
|
||||
/**
|
||||
* 获取使用统计原始数据
|
||||
*/
|
||||
getUsage: () => apiClient.get('/usage'),
|
||||
|
||||
/**
|
||||
* 计算密钥成功/失败统计,必要时会先获取 usage 数据
|
||||
*/
|
||||
async getKeyStats(usageData?: any): Promise<KeyStats> {
|
||||
let payload = usageData;
|
||||
if (!payload) {
|
||||
const response = await apiClient.get('/usage');
|
||||
payload = response?.usage ?? response;
|
||||
}
|
||||
return computeKeyStats(payload);
|
||||
}
|
||||
};
|
||||
9
src/services/api/version.ts
Normal file
9
src/services/api/version.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 版本相关 API
|
||||
*/
|
||||
|
||||
import { apiClient } from './client';
|
||||
|
||||
export const versionApi = {
|
||||
checkLatest: () => apiClient.get('/latest-version')
|
||||
};
|
||||
111
src/services/storage/secureStorage.ts
Normal file
111
src/services/storage/secureStorage.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* 安全存储服务
|
||||
* 基于原项目 src/utils/secure-storage.js
|
||||
*/
|
||||
|
||||
import { encryptData, decryptData } from '@/utils/encryption';
|
||||
|
||||
interface StorageOptions {
|
||||
encrypt?: boolean;
|
||||
}
|
||||
|
||||
class SecureStorageService {
|
||||
/**
|
||||
* 存储数据
|
||||
*/
|
||||
setItem(key: string, value: any, options: StorageOptions = {}): void {
|
||||
const { encrypt = true } = options;
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
this.removeItem(key);
|
||||
return;
|
||||
}
|
||||
|
||||
const stringValue = JSON.stringify(value);
|
||||
const storedValue = encrypt ? encryptData(stringValue) : stringValue;
|
||||
|
||||
localStorage.setItem(key, storedValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据
|
||||
*/
|
||||
getItem<T = any>(key: string, options: StorageOptions = {}): T | null {
|
||||
const { encrypt = true } = options;
|
||||
|
||||
const raw = localStorage.getItem(key);
|
||||
if (raw === null) return null;
|
||||
|
||||
try {
|
||||
const decrypted = encrypt ? decryptData(raw) : raw;
|
||||
return JSON.parse(decrypted) as T;
|
||||
} catch {
|
||||
// JSON解析失败,尝试兼容旧的纯字符串数据 (非JSON格式)
|
||||
try {
|
||||
// 如果是加密的,尝试解密后直接返回
|
||||
if (encrypt && raw.startsWith('enc::v1::')) {
|
||||
const decrypted = decryptData(raw);
|
||||
// 解密后如果还不是JSON,返回原始字符串
|
||||
return decrypted as T;
|
||||
}
|
||||
// 非加密的纯字符串,直接返回
|
||||
return raw as T;
|
||||
} catch {
|
||||
// 完全失败,静默返回null (避免控制台污染)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
*/
|
||||
removeItem(key: string): void {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有数据
|
||||
*/
|
||||
clear(): void {
|
||||
localStorage.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移旧的明文缓存为加密格式
|
||||
*/
|
||||
migratePlaintextKeys(keys: string[]): void {
|
||||
keys.forEach((key) => {
|
||||
const raw = localStorage.getItem(key);
|
||||
if (!raw) return;
|
||||
|
||||
// 如果已经是加密格式,跳过
|
||||
if (raw.startsWith('enc::v1::')) {
|
||||
return;
|
||||
}
|
||||
|
||||
let parsed: any = raw;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch {
|
||||
// 原值不是 JSON,直接使用字符串
|
||||
parsed = raw;
|
||||
}
|
||||
|
||||
try {
|
||||
this.setItem(key, parsed);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to migrate key "${key}":`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查键是否存在
|
||||
*/
|
||||
hasItem(key: string): boolean {
|
||||
return localStorage.getItem(key) !== null;
|
||||
}
|
||||
}
|
||||
|
||||
export const secureStorage = new SecureStorageService();
|
||||
Reference in New Issue
Block a user