Files
Cli-Proxy-API-Management-Ce…/src/stores/useConfigStore.ts
Supra4E8C 4d898b3e20 feat(logs): redesign LogsPage with structured log parsing and virtual scrolling
- 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
2025-12-15 17:37:09 +08:00

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