import axios from 'axios' import { message } from 'antd' const AUTH_TOKEN_KEY = 'agenteval_auth_token' export const authToken = { get: () => sessionStorage.getItem(AUTH_TOKEN_KEY), set: (token: string) => sessionStorage.setItem(AUTH_TOKEN_KEY, token), clear: () => sessionStorage.removeItem(AUTH_TOKEN_KEY), } /** Registered by App to switch back to the login screen on 401. */ let onUnauthorized: (() => void) | null = null export const setOnUnauthorized = (handler: (() => void) | null) => { onUnauthorized = handler } const api = axios.create({ baseURL: '/api', timeout: 30000, }) api.interceptors.request.use((config) => { const token = authToken.get() if (token) config.headers['X-Auth-Token'] = token return config }) api.interceptors.response.use( (response) => response, (error) => { const isLoginCall = error.config?.url?.includes('/auth/login') if (error.response?.status === 401 && !isLoginCall && onUnauthorized) { authToken.clear() onUnauthorized() return Promise.reject(error) } const msg = error.response?.data?.detail || error.message || '请求失败' message.error(msg) return Promise.reject(error) }, ) export default api export interface AuthStatus { auth_required: boolean authenticated: boolean } export const authApi = { status: () => api.get('/auth/status'), login: (password: string) => api.post<{ token: string }>('/auth/login', { password }), } export interface Target { id: string name: string description: string platform: string channel_type: string channel_config: Record status: string created_at: string updated_at: string } export interface Scenario { id: string name: string description: string tags: string[] cases: any[] model_bindings: Record version: number created_at: string updated_at: string } export type ModelCapability = 'chat' | 'embedding' | 'moderation' export type ModelProtocol = 'openai_compatible' | 'anthropic' | 'google_gemini' | 'dashscope' export type ModelModality = 'text' | 'image' | 'audio' | 'video' export interface ModelConfig { id: string name: string provider: ModelProtocol capability: ModelCapability endpoint_url: string model_name: string | null vendor_name: string input_modalities: ModelModality[] output_modalities: ModelModality[] context_window: number | null max_output_tokens: number | null supports_streaming: boolean supports_tool_calling: boolean supports_structured_output: boolean supports_reasoning: boolean region: string documentation_url: string | null has_api_key: boolean enabled: boolean is_default: boolean is_analysis_default: boolean description: string created_at: string | null updated_at: string | null } export interface ModelConfigPayload { name: string provider: ModelProtocol capability: ModelCapability endpoint_url: string model_name?: string | null vendor_name: string input_modalities: ModelModality[] output_modalities: ModelModality[] context_window?: number | null max_output_tokens?: number | null supports_streaming: boolean supports_tool_calling: boolean supports_structured_output: boolean supports_reasoning: boolean region: string documentation_url?: string | null api_key?: string | null clear_api_key?: boolean enabled: boolean is_default: boolean is_analysis_default?: boolean description: string } export interface ModelConfigReference { scenario_id: string scenario_name: string purpose: string } export type RunTrigger = 'manual' | 'ai_assistant' | 'cli' | 'campaign' export interface RunError { code: string message: string } export interface RunSummary { total_cases: number passed_cases: number failed_cases: number total_rules: number passed_rules: number pass_rate: number | null avg_latency_ms: number | null case_outcomes: Record case_errors: Array> error: RunError | null } export interface Run { id: string target_id: string scenario_id: string scenario_version?: number campaign_id?: string | null status: string triggered_by?: RunTrigger scenario_name?: string | null target_name?: string | null started_at: string completed_at: string | null summary: RunSummary | null } export interface ScenarioStat { scenario_id: string scenario_name: string run_count: number avg_pass_rate: number | null last_run_at: string | null } export interface DashboardStats { targets_count: number scenarios_count: number runs_count: number model_configs_count: number today_runs: number running_count: number overall_pass_rate: number | null trigger_breakdown: Partial> scenario_stats: ScenarioStat[] recent_runs: Run[] } export interface TrendPoint { date: string pass_rate: number run_count: number } export const targetsApi = { list: () => api.get('/targets'), get: (id: string) => api.get(`/targets/${id}`), create: (data: Partial) => api.post('/targets', data), update: (id: string, data: Partial) => api.put(`/targets/${id}`, data), delete: (id: string) => api.delete(`/targets/${id}`), test: (id: string) => api.post<{ ok: boolean; message: string }>(`/targets/${id}/test`), runs: (id: string) => api.get(`/targets/${id}/runs`), } export const scenariosApi = { list: () => api.get('/scenarios'), get: (id: string) => api.get(`/scenarios/${id}`), create: (data: Partial) => api.post('/scenarios', data), update: (id: string, data: Partial) => api.put(`/scenarios/${id}`, data), delete: (id: string) => api.delete(`/scenarios/${id}`), validate: (data: any) => api.post<{ valid: boolean; errors: string[] }>('/scenarios/validate', data), listTemplates: () => api.get('/scenarios/templates'), getTemplate: (id: string) => api.get(`/scenarios/templates/${id}`), } export const modelConfigsApi = { list: (params?: { capability?: ModelCapability; enabled?: boolean }) => api.get('/model-configs', { params }), get: (id: string) => api.get(`/model-configs/${id}`), create: (data: ModelConfigPayload) => api.post('/model-configs', data), update: (id: string, data: ModelConfigPayload) => api.put(`/model-configs/${id}`, data), delete: (id: string) => api.delete(`/model-configs/${id}`), test: (id: string) => api.post<{ ok: boolean; message: string; tested_at: string }>(`/model-configs/${id}/test`), references: (id: string) => api.get(`/model-configs/${id}/references`), } export const runsApi = { list: () => api.get('/runs'), start: (target_id: string, scenario_id: string) => api.post('/runs', { target_id, scenario_id }), get: (id: string) => api.get(`/runs/${id}`), logs: (id: string) => api.get(`/runs/${id}/logs`), cancel: (id: string) => api.post(`/runs/${id}/cancel`), } export interface RunLogsTurn { id: string case_id: string round_index: number latency_ms: number | null sent_text: string reply_text: string sent_at: string | null received_at: string | null } export interface RunLogsResult { case_id: string rule_type: string passed: boolean score: number | null reason: string | null } export interface CaseSnapshotExpectations { intent?: string | null keywords_include: string[] keywords_exclude: string[] response_time_max_ms?: number | null coherence_min_score?: number | null } export interface CaseSnapshot { id: string type: string messages: string[] prompt?: string | null turns?: number | null expectations: CaseSnapshotExpectations eval_rules: { type: string; params: Record }[] } export interface RunLogsResponse { turns: RunLogsTurn[] results: RunLogsResult[] case_verdicts: Record scenario_snapshot: Record } export const reportsApi = { get: (runId: string) => api.get(`/reports/${runId}`), download: async (runId: string, format: 'html' | 'json' | 'markdown') => { const ext = format === 'markdown' ? 'md' : format const res = await api.get(`/reports/${runId}/${format}`, { responseType: 'blob' }) const url = URL.createObjectURL(res.data as Blob) const a = document.createElement('a') a.href = url a.download = `report-${runId.slice(0, 8)}.${ext}` a.click() URL.revokeObjectURL(url) }, compare: (run1: string, run2: string) => api.get(`/reports/compare`, { params: { run1, run2 } }), } export const statsApi = { dashboard: () => api.get('/stats/dashboard'), trend: (days?: number) => api.get('/stats/trend', { params: { days } }), } // ── Campaigns(评估活动) ────────────────────────────────────────── export interface CampaignPlanEntry { scenario_id: string offset_seconds: number count: number } export interface CampaignProgress { current_offset_seconds: number spawned_runs: number completed_runs: number } export interface CampaignListProgress { completed_runs: number planned_total: number overall_pass_rate: number | null } export interface Campaign { id: string name: string target_id: string window_seconds: number time_scale: number plan: CampaignPlanEntry[] status: string started_at: string | null completed_at: string | null summary: Record | null analysis_model_config_id: string | null progress?: CampaignProgress } export interface CampaignListItem extends Omit { progress: CampaignListProgress } export interface CampaignTrendBucket { bucket_index: number start_seconds: number end_seconds: number run_count: number pass_rate: number | null availability: number | null avg_latency_ms: number | null } export interface CampaignCapability { scenario_id: string scenario_name: string run_count: number pass_rate: number | null availability: number | null avg_latency_ms: number | null } export interface CampaignReport { campaign_id: string name: string target_id: string status: string window_seconds: number time_scale: number started_at: string | null completed_at: string | null summary: { total_runs: number completed_runs: number overall_pass_rate: number | null overall_availability: number | null avg_latency_ms: number | null } time_trend: CampaignTrendBucket[] capability_summary: CampaignCapability[] } export interface CampaignTimelineEntry { run_id: string scenario_id: string scenario_name: string offset_seconds: number status: string pass_rate: number | null avg_latency_ms: number | null started_at: string | null } export interface CreateCampaignPayload { name: string target_id: string window_seconds: number time_scale: number plan: CampaignPlanEntry[] analysis_model_config_id?: string | null } export const campaignsApi = { list: () => api.get('/campaigns'), get: (id: string) => api.get(`/campaigns/${id}`), create: (data: CreateCampaignPayload) => api.post('/campaigns', data), cancel: (id: string) => api.post(`/campaigns/${id}/cancel`), report: (id: string) => api.get(`/campaigns/${id}/report`), timeline: (id: string) => api.get<{ entries: CampaignTimelineEntry[] }>(`/campaigns/${id}/timeline`), downloadReport: async (id: string) => { const res = await api.get(`/campaigns/${id}/report/markdown`, { responseType: 'blob' }) const url = URL.createObjectURL(res.data as Blob) const a = document.createElement('a') a.href = url a.download = `campaign-report-${id.slice(0, 8)}.md` a.click() URL.revokeObjectURL(url) }, } // ── File Management ────────────────────────────────────────────── export interface FileCategory { id: string name: string parent_id: string | null children: FileCategory[] } export interface FileRecord { id: string original_name: string file_size: number mime_type: string file_ext: string category_id: string | null storage_directory: string created_at: string | null } export interface FileUploadConfig { allowed_extensions: string[] max_upload_size_mb: number } export const filesApi = { // Categories getConfig: () => api.get('/files/config'), listCategories: () => api.get('/files/categories'), createCategory: (data: { name: string; parent_id?: string | null }) => api.post('/files/categories', data), updateCategory: (id: string, data: { name: string }) => api.put(`/files/categories/${id}`, data), deleteCategory: (id: string) => api.delete(`/files/categories/${id}`), // Files list: (categoryId?: string | null) => api.get('/files', { params: categoryId ? { category_id: categoryId } : {} }), upload: (file: File, categoryId?: string | null) => { const form = new FormData() form.append('file', file) if (categoryId) { form.append('category_id', categoryId) } return api.post('/files/upload', form, { timeout: 120000, }) }, download: (id: string) => api.get(`/files/${id}/download`, { responseType: 'blob' }), delete: (id: string) => api.delete(`/files/${id}`), }