mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 03:10:50 +08:00
- Add log line parser to extract timestamp, level, status code, latency, IP, HTTP method, and path - Implement virtual scrolling with load-more on scroll-up to handle large log files efficiently - Replace monolithic pre block with structured grid layout for better readability - Add visual badges for log levels and HTTP status codes with color-coded severity - Add IconRefreshCw icon component - Update ToggleSwitch to accept ReactNode as label - Fix fetchConfig calls to use default parameters consistently - Add request deduplication in useConfigStore to prevent duplicate /config API calls - Add i18n keys for load_more_hint and hidden_lines
264 lines
7.2 KiB
TypeScript
264 lines
7.2 KiB
TypeScript
/**
|
|
* 配置状态管理
|
|
* 从原项目 src/core/config-service.js 迁移
|
|
*/
|
|
|
|
import { create } from 'zustand';
|
|
import type { Config } from '@/types';
|
|
import type { RawConfigSection } from '@/types/config';
|
|
import { configApi } from '@/services/api/config';
|
|
import { CACHE_EXPIRY_MS } from '@/utils/constants';
|
|
|
|
interface ConfigCache {
|
|
data: any;
|
|
timestamp: number;
|
|
}
|
|
|
|
interface ConfigState {
|
|
config: Config | null;
|
|
cache: Map<string, ConfigCache>;
|
|
loading: boolean;
|
|
error: string | null;
|
|
|
|
// 操作
|
|
fetchConfig: (section?: RawConfigSection, forceRefresh?: boolean) => Promise<Config | any>;
|
|
updateConfigValue: (section: RawConfigSection, value: any) => void;
|
|
clearCache: (section?: RawConfigSection) => void;
|
|
isCacheValid: (section?: RawConfigSection) => boolean;
|
|
}
|
|
|
|
let configRequestToken = 0;
|
|
let inFlightConfigRequest: { id: number; promise: Promise<Config> } | null = null;
|
|
|
|
const SECTION_KEYS: RawConfigSection[] = [
|
|
'debug',
|
|
'proxy-url',
|
|
'request-retry',
|
|
'quota-exceeded',
|
|
'usage-statistics-enabled',
|
|
'request-log',
|
|
'logging-to-file',
|
|
'ws-auth',
|
|
'api-keys',
|
|
'ampcode',
|
|
'gemini-api-key',
|
|
'codex-api-key',
|
|
'claude-api-key',
|
|
'openai-compatibility',
|
|
'oauth-excluded-models'
|
|
];
|
|
|
|
const extractSectionValue = (config: Config | null, section?: RawConfigSection) => {
|
|
if (!config) return undefined;
|
|
switch (section) {
|
|
case 'debug':
|
|
return config.debug;
|
|
case 'proxy-url':
|
|
return config.proxyUrl;
|
|
case 'request-retry':
|
|
return config.requestRetry;
|
|
case 'quota-exceeded':
|
|
return config.quotaExceeded;
|
|
case 'usage-statistics-enabled':
|
|
return config.usageStatisticsEnabled;
|
|
case 'request-log':
|
|
return config.requestLog;
|
|
case 'logging-to-file':
|
|
return config.loggingToFile;
|
|
case 'ws-auth':
|
|
return config.wsAuth;
|
|
case 'api-keys':
|
|
return config.apiKeys;
|
|
case 'ampcode':
|
|
return config.ampcode;
|
|
case 'gemini-api-key':
|
|
return config.geminiApiKeys;
|
|
case 'codex-api-key':
|
|
return config.codexApiKeys;
|
|
case 'claude-api-key':
|
|
return config.claudeApiKeys;
|
|
case 'openai-compatibility':
|
|
return config.openaiCompatibility;
|
|
case 'oauth-excluded-models':
|
|
return config.oauthExcludedModels;
|
|
default:
|
|
if (!section) return undefined;
|
|
return config.raw?.[section];
|
|
}
|
|
};
|
|
|
|
export const useConfigStore = create<ConfigState>((set, get) => ({
|
|
config: null,
|
|
cache: new Map(),
|
|
loading: false,
|
|
error: null,
|
|
|
|
fetchConfig: async (section, forceRefresh = false) => {
|
|
const { cache, isCacheValid } = get();
|
|
|
|
// 检查缓存
|
|
const cacheKey = section || '__full__';
|
|
if (!forceRefresh && isCacheValid(section)) {
|
|
const cached = cache.get(cacheKey);
|
|
if (cached) {
|
|
return cached.data;
|
|
}
|
|
}
|
|
|
|
// section 缓存未命中但 full 缓存可用时,直接复用已获取到的配置,避免重复 /config 请求
|
|
if (!forceRefresh && section && isCacheValid()) {
|
|
const fullCached = cache.get('__full__');
|
|
if (fullCached?.data) {
|
|
return extractSectionValue(fullCached.data as Config, section);
|
|
}
|
|
}
|
|
|
|
// 同一时刻合并多个 /config 请求(如 StrictMode 或多个页面同时触发)
|
|
if (inFlightConfigRequest) {
|
|
const data = await inFlightConfigRequest.promise;
|
|
return section ? extractSectionValue(data, section) : data;
|
|
}
|
|
|
|
// 获取新数据
|
|
set({ loading: true, error: null });
|
|
|
|
const requestId = (configRequestToken += 1);
|
|
try {
|
|
const requestPromise = configApi.getConfig();
|
|
inFlightConfigRequest = { id: requestId, promise: requestPromise };
|
|
const data = await requestPromise;
|
|
const now = Date.now();
|
|
|
|
// 如果在请求过程中连接已被切换/登出,则忽略旧请求的结果,避免覆盖新会话的状态
|
|
if (requestId !== configRequestToken) {
|
|
return section ? extractSectionValue(data, section) : data;
|
|
}
|
|
|
|
// 更新缓存
|
|
const newCache = new Map(cache);
|
|
newCache.set('__full__', { data, timestamp: now });
|
|
SECTION_KEYS.forEach((key) => {
|
|
const value = extractSectionValue(data, key);
|
|
if (value !== undefined) {
|
|
newCache.set(key, { data: value, timestamp: now });
|
|
}
|
|
});
|
|
|
|
set({
|
|
config: data,
|
|
cache: newCache,
|
|
loading: false
|
|
});
|
|
|
|
return section ? extractSectionValue(data, section) : data;
|
|
} catch (error: any) {
|
|
if (requestId === configRequestToken) {
|
|
set({
|
|
error: error.message || 'Failed to fetch config',
|
|
loading: false
|
|
});
|
|
}
|
|
throw error;
|
|
} finally {
|
|
if (inFlightConfigRequest?.id === requestId) {
|
|
inFlightConfigRequest = null;
|
|
}
|
|
}
|
|
},
|
|
|
|
updateConfigValue: (section, value) => {
|
|
set((state) => {
|
|
const raw = { ...(state.config?.raw || {}) };
|
|
raw[section] = value;
|
|
const nextConfig: Config = { ...(state.config || {}), raw };
|
|
|
|
switch (section) {
|
|
case 'debug':
|
|
nextConfig.debug = value;
|
|
break;
|
|
case 'proxy-url':
|
|
nextConfig.proxyUrl = value;
|
|
break;
|
|
case 'request-retry':
|
|
nextConfig.requestRetry = value;
|
|
break;
|
|
case 'quota-exceeded':
|
|
nextConfig.quotaExceeded = value;
|
|
break;
|
|
case 'usage-statistics-enabled':
|
|
nextConfig.usageStatisticsEnabled = value;
|
|
break;
|
|
case 'request-log':
|
|
nextConfig.requestLog = value;
|
|
break;
|
|
case 'logging-to-file':
|
|
nextConfig.loggingToFile = value;
|
|
break;
|
|
case 'ws-auth':
|
|
nextConfig.wsAuth = value;
|
|
break;
|
|
case 'api-keys':
|
|
nextConfig.apiKeys = value;
|
|
break;
|
|
case 'ampcode':
|
|
nextConfig.ampcode = value;
|
|
break;
|
|
case 'gemini-api-key':
|
|
nextConfig.geminiApiKeys = value;
|
|
break;
|
|
case 'codex-api-key':
|
|
nextConfig.codexApiKeys = value;
|
|
break;
|
|
case 'claude-api-key':
|
|
nextConfig.claudeApiKeys = value;
|
|
break;
|
|
case 'openai-compatibility':
|
|
nextConfig.openaiCompatibility = value;
|
|
break;
|
|
case 'oauth-excluded-models':
|
|
nextConfig.oauthExcludedModels = value;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return { config: nextConfig };
|
|
});
|
|
|
|
// 清除该 section 的缓存
|
|
get().clearCache(section);
|
|
},
|
|
|
|
clearCache: (section) => {
|
|
const { cache } = get();
|
|
const newCache = new Map(cache);
|
|
|
|
if (section) {
|
|
newCache.delete(section);
|
|
// 同时清除完整配置缓存
|
|
newCache.delete('__full__');
|
|
|
|
set({ cache: newCache });
|
|
return;
|
|
} else {
|
|
newCache.clear();
|
|
}
|
|
|
|
// 清除全部缓存一般代表“切换连接/登出/全量刷新”,需要让 in-flight 的旧请求失效
|
|
configRequestToken += 1;
|
|
inFlightConfigRequest = null;
|
|
|
|
set({ config: null, cache: newCache, loading: false, error: null });
|
|
},
|
|
|
|
isCacheValid: (section) => {
|
|
const { cache } = get();
|
|
const cacheKey = section || '__full__';
|
|
const cached = cache.get(cacheKey);
|
|
|
|
if (!cached) return false;
|
|
|
|
return Date.now() - cached.timestamp < CACHE_EXPIRY_MS;
|
|
}
|
|
}));
|