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:
Supra4E8C
2025-12-07 11:32:31 +08:00
parent 8e4132200d
commit 450964fb1a
144 changed files with 14223 additions and 21647 deletions

View 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}`)
};

View 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
View 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();

View 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')
};

View 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
View 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
View 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'
})
};

View 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
View 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 }
})
};

View 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)}`)
};

View 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
View 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);
}
};

View File

@@ -0,0 +1,9 @@
/**
* 版本相关 API
*/
import { apiClient } from './client';
export const versionApi = {
checkLatest: () => apiClient.get('/latest-version')
};

View 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();