AgentEvalTool/frontend/web/src/api.ts
sinohqb c63236f671 feat(campaign): support per-campaign analysis model override
Campaigns can pin an analysis model config instead of following the
global analysis default. Creation validates the referenced config
exists (400 otherwise); the create form offers enabled chat configs
with the global default as the fallback option.
2026-08-03 01:53:13 +08:00

476 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<AuthStatus>('/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<string, unknown>
status: string
created_at: string
updated_at: string
}
export interface Scenario {
id: string
name: string
description: string
tags: string[]
cases: any[]
model_bindings: Record<string, string>
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<string, { passed: boolean; connectivity: boolean }>
case_errors: Array<Record<string, string>>
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<Record<RunTrigger, number>>
scenario_stats: ScenarioStat[]
recent_runs: Run[]
}
export interface TrendPoint {
date: string
pass_rate: number
run_count: number
}
export const targetsApi = {
list: () => api.get<Target[]>('/targets'),
get: (id: string) => api.get<Target>(`/targets/${id}`),
create: (data: Partial<Target>) => api.post<Target>('/targets', data),
update: (id: string, data: Partial<Target>) => api.put<Target>(`/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<Run[]>(`/targets/${id}/runs`),
}
export const scenariosApi = {
list: () => api.get<Scenario[]>('/scenarios'),
get: (id: string) => api.get<Scenario>(`/scenarios/${id}`),
create: (data: Partial<Scenario>) => api.post<Scenario>('/scenarios', data),
update: (id: string, data: Partial<Scenario>) => api.put<Scenario>(`/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<any[]>('/scenarios/templates'),
getTemplate: (id: string) => api.get<any>(`/scenarios/templates/${id}`),
}
export const modelConfigsApi = {
list: (params?: { capability?: ModelCapability; enabled?: boolean }) =>
api.get<ModelConfig[]>('/model-configs', { params }),
get: (id: string) => api.get<ModelConfig>(`/model-configs/${id}`),
create: (data: ModelConfigPayload) => api.post<ModelConfig>('/model-configs', data),
update: (id: string, data: ModelConfigPayload) => api.put<ModelConfig>(`/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<ModelConfigReference[]>(`/model-configs/${id}/references`),
}
export const runsApi = {
list: () => api.get<Run[]>('/runs'),
start: (target_id: string, scenario_id: string) =>
api.post<Run>('/runs', { target_id, scenario_id }),
get: (id: string) => api.get<Run>(`/runs/${id}`),
logs: (id: string) => api.get<RunLogsResponse>(`/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<string, unknown> }[]
}
export interface RunLogsResponse {
turns: RunLogsTurn[]
results: RunLogsResult[]
case_verdicts: Record<string, { passed: boolean; connectivity: boolean }>
scenario_snapshot: Record<string, CaseSnapshot>
}
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<DashboardStats>('/stats/dashboard'),
trend: (days?: number) => api.get<TrendPoint[]>('/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<string, unknown> | null
analysis_model_config_id: string | null
progress?: CampaignProgress
}
export interface CampaignListItem extends Omit<Campaign, 'progress'> {
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<CampaignListItem[]>('/campaigns'),
get: (id: string) => api.get<Campaign>(`/campaigns/${id}`),
create: (data: CreateCampaignPayload) => api.post<Campaign>('/campaigns', data),
cancel: (id: string) => api.post<Campaign>(`/campaigns/${id}/cancel`),
report: (id: string) => api.get<CampaignReport>(`/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<FileUploadConfig>('/files/config'),
listCategories: () => api.get<FileCategory[]>('/files/categories'),
createCategory: (data: { name: string; parent_id?: string | null }) =>
api.post<FileCategory>('/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<FileRecord[]>('/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<FileRecord>('/files/upload', form, {
timeout: 120000,
})
},
download: (id: string) => api.get<Blob>(`/files/${id}/download`, { responseType: 'blob' }),
delete: (id: string) => api.delete(`/files/${id}`),
}