// --------------------------------------------------------------------------- // AiConfig helpers — pure functions, no React / DOM dependencies // --------------------------------------------------------------------------- export type AiProvider = 'OPENAI' | 'DEEPSEEK' | 'OPENAI_COMPATIBLE'; export const PROVIDER_OPTIONS: { value: AiProvider; label: string }[] = [ { value: 'DEEPSEEK', label: 'DeepSeek' }, { value: 'OPENAI', label: 'OpenAI' }, { value: 'OPENAI_COMPATIBLE', label: 'OpenAI 兼容' }, ]; export const PROVIDER_DEFAULTS: Record = { OPENAI: 'https://api.openai.com/v1', DEEPSEEK: 'https://api.deepseek.com', OPENAI_COMPATIBLE: '', } as const; export const FIXED_PROVIDERS: AiProvider[] = ['OPENAI', 'DEEPSEEK']; export function formatDateTime(iso: string | null): string { if (!iso) return '-'; return new Date(iso).toLocaleString('zh-CN'); } export function sourceLabel(source: string): string { switch (source) { case 'database': return '数据库'; case 'environment': return '环境变量'; default: return '未配置'; } } export function sourceColor(source: string): 'green' | 'blue' | 'default' { switch (source) { case 'database': return 'green'; case 'environment': return 'blue'; default: return 'default'; } } export function shouldAutoSwapBaseUrl( provider: AiProvider, currentBaseUrl: string, lastProvider: AiProvider | null, ): { baseUrl: string; shouldSwap: boolean } { if (!lastProvider) { return { baseUrl: PROVIDER_DEFAULTS[provider], shouldSwap: true }; } const prevDefault = PROVIDER_DEFAULTS[lastProvider]; if (!currentBaseUrl || currentBaseUrl === prevDefault) { return { baseUrl: PROVIDER_DEFAULTS[provider], shouldSwap: true }; } return { baseUrl: currentBaseUrl, shouldSwap: false }; } /** Extract a safe user-facing error message from any caught value. * * The Axios interceptor at `api/index.ts` unwraps errors before rejection: * `Promise.reject(err.response?.data || err)`. So server errors arrive as * `{ message: '...' }` (the unwrapped data) and network errors as the raw * `Error` object — never as a raw AxiosError with a `.response` property. */ export function extractErrorMessage(err: unknown, fallback: string = '操作失败'): string { let msg = ''; // standard Error or any object with a string message property if (err && typeof err === 'object' && 'message' in err && typeof err.message === 'string') { msg = err.message; } // sanitize: trim, collapse whitespace, strip newlines, truncate const trimmed = msg.trim(); if (!trimmed) return fallback; const singleLine = trimmed.replace(/[\n\r]+/g, ' ').replace(/ {2,}/g, ' '); return singleLine.length > 120 ? singleLine.slice(0, 120) + '\u2026' : singleLine; }