Files
gongxue-base/apps/admin/src/pages/AiConfig/helpers.ts
wangziqi cc4f4dae4e fix: audit remediation — SSE user scoping, FK transactional safety, UI error handling
- H4: scoped SSE import progress to exact userId match; non-HTTP events excluded from all subscribers
- H2: moved PRAGMA foreign_key_check inside SQLite transaction before COMMIT; violations rollback preserving old tables
- M1: removed dead axios-style error branch from extractErrorMessage (interceptor already unwraps)
- M2: split handleSave try/catch — save errors vs reload errors shown distinctly
- M3: added provider field validation before AI config test request
- Added SSE scoping regression tests (import service + controller)
- Added FK check failure rollback test (database-migrations.spec)
- Updated controller spec expectations for userId parameter

Co-authored-by: Code Review <branch-review>
2026-07-12 22:59:03 +08:00

84 lines
2.8 KiB
TypeScript

// ---------------------------------------------------------------------------
// AiConfig helpers — pure functions, no React / DOM dependencies
// ---------------------------------------------------------------------------
export type AiProvider = 'OPENAI' | 'DEEPSEEK' | 'OPENAI_COMPATIBLE';
export const PROVIDER_OPTIONS: { value: AiProvider; label: string }[] = [
{ value: 'OPENAI', label: 'OpenAI' },
{ value: 'DEEPSEEK', label: 'DeepSeek' },
{ value: 'OPENAI_COMPATIBLE', label: 'OpenAI 兼容' },
];
export const PROVIDER_DEFAULTS: Record<AiProvider, string> = {
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;
}