feat: add CASL authorization and AI configuration
This commit is contained in:
89
apps/admin/src/pages/AiConfig/helpers.ts
Normal file
89
apps/admin/src/pages/AiConfig/helpers.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 */
|
||||
export function extractErrorMessage(err: unknown, fallback: string = '操作失败'): string {
|
||||
let msg = '';
|
||||
|
||||
// axios-style error: { response: { data: { message: string } } }
|
||||
if (err && typeof err === 'object' && 'response' in err) {
|
||||
const resp: unknown = err.response;
|
||||
if (resp && typeof resp === 'object' && 'data' in resp) {
|
||||
const data: unknown = resp.data;
|
||||
if (data && typeof data === 'object' && 'message' in data && typeof data.message === 'string') {
|
||||
msg = data.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// standard Error or any object with a string message property
|
||||
if (!msg && 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;
|
||||
}
|
||||
Reference in New Issue
Block a user