refactor(api): api.ts 拆分为 10 个域模块(Phase 4.22)

909 行的单文件 api.ts 按域拆为 10 个模块 + 共享 client;barrel
保持 import from '../api' 不变,调用点零改动。

- api/client.ts:axios 实例 + 拦截器 + authToken + setOnUnauthorized
  (唯一住处)。
- 10 个域模块:auth / targets / scenarios / modelConfigs / runs /
  reports / stats / campaigns / intelligentEvals / files,每个
  自包含类型声明与 API 方法。
- api/index.ts:barrel 再导出,调用点 import 路径全部不变。
- 原 api.ts 中的 Blob 导出仪式已在 Commit 4 下沉至
  utils/download.ts,本 commit 仅做拆分与搬运。
This commit is contained in:
sinohqb 2026-08-24 05:53:45 +08:00
parent f458897ab5
commit eca7ccebe2
13 changed files with 916 additions and 909 deletions

View File

@ -1,909 +0,0 @@
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 ExplorationSeeds {
personas: string[]
goals: string[]
}
export interface ExplorationBudgetConfig {
max_sessions?: number | null
max_turns?: number | null
min_interval_seconds?: number | null
}
export interface ExplorationIssueCount {
issue: string
count: number
}
export interface ExplorationJudgeReview {
reviewed_sessions: number
findings: { dimension: string; rating: string; comment: string }[]
summaries: string[]
}
export interface ExplorationSummary {
session_count: number
sessions_with_experience: number
goal_achieved_count: number
goal_achievement_rate: number | null
issues: ExplorationIssueCount[]
misled: ExplorationIssueCount[]
judge_review: ExplorationJudgeReview | null
}
export interface ExplorationExperience {
goal_achieved: boolean
blockers: string[]
misled: string[]
emotion: string
notes: string
}
export interface ExplorationSession {
id: string
campaign_id: string
target_id: string
persona: Record<string, unknown>
goal: string
seed_ref: Record<string, unknown> | null
status: string
triggered_by: string
experience: ExplorationExperience | null
judge_review: Record<string, unknown> | null
turn_count: number
error: string | null
created_at: string | null
closed_at: string | null
}
export interface ExplorationMessage {
id: string
session_id: string
round_index: number
role: string
content: string
latency_ms: number | null
created_at: string | null
}
export const explorationApi = {
listSessions: (campaignId: string) =>
api.get<{ sessions: ExplorationSession[] }>(`/exploration/campaigns/${campaignId}/sessions`),
listMessages: (sessionId: string) =>
api.get<{ messages: ExplorationMessage[] }>(`/exploration/sessions/${sessionId}/messages`),
}
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
exploration_seeds: ExplorationSeeds | null
exploration_budget: ExplorationBudgetConfig | 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[]
exploration?: ExplorationSummary | null
}
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 type CampaignAnalysisStatus = 'none' | 'queued' | 'generating' | 'completed' | 'failed'
export interface CampaignAnalysisProblem {
severity: string
title: string
description: string
scenario_ids: string[]
evidence_run_ids: string[]
}
export interface CampaignAnalysisResult {
overall: string
problems: CampaignAnalysisProblem[]
scenario_narratives: { scenario_id: string; narrative: string }[]
suggestions: { priority: number; text: string }[]
}
export interface CampaignAnalysis {
status: CampaignAnalysisStatus
result?: CampaignAnalysisResult | null
error?: string | null
model_config_id?: string | null
triggered_by?: string
updated_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
exploration_seeds?: ExplorationSeeds | null
exploration_budget?: ExplorationBudgetConfig | null
}
// ── Period comparison (v0.8) ────────────────────────────────────
export type ComparisonTrend = 'improving' | 'stable' | 'regressing'
export interface MetricDeltaPair {
baseline: number | null
current: number | null
delta: number | null
}
export interface MetricDiffScenario {
scenario_id: string
scenario_name: string
pass_rate: MetricDeltaPair
availability: MetricDeltaPair
avg_latency_ms: MetricDeltaPair
}
export interface MetricDiff {
overall: {
pass_rate: MetricDeltaPair
availability: MetricDeltaPair
avg_latency_ms: MetricDeltaPair
}
scenarios: MetricDiffScenario[]
}
export interface ComparisonBaselineInfo {
id: string
name: string
completed_at: string | null
}
export interface ComparisonProblemEvolution {
status: string
title: string
detail: string
scenario_ids: string[]
}
export interface ComparisonSuggestionTracking {
text: string
status: string
note: string
}
export interface ComparisonResult {
trend: ComparisonTrend
summary: string
problem_evolution: ComparisonProblemEvolution[]
suggestion_tracking: ComparisonSuggestionTracking[]
}
export interface CampaignComparison {
status: CampaignAnalysisStatus
auto_baseline: ComparisonBaselineInfo | null
metric_diff: MetricDiff | null
comparison: {
baseline_campaign_id: string
baseline: ComparisonBaselineInfo | null
result: ComparisonResult | null
error: string | null
model_config_id: string | null
triggered_by: string
updated_at: string | null
} | 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`),
getAnalysis: (id: string) => api.get<CampaignAnalysis>(`/campaigns/${id}/analysis`),
generateAnalysis: (id: string) => api.post<{ status: string }>(`/campaigns/${id}/analysis`),
getComparison: (id: string) => api.get<CampaignComparison>(`/campaigns/${id}/comparison`),
generateComparison: (id: string, baselineCampaignId?: string) =>
api.post<{ status: string }>(
`/campaigns/${id}/comparison`,
baselineCampaignId ? { baseline_campaign_id: baselineCampaignId } : {},
),
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)
},
}
// ── Intelligent Evaluation智能评估 v1.0 ────────────────────────
export type IntelligentEvalStatus =
| 'draft' | 'planning' | 'pending_approval'
| 'executing' | 'completed' | 'cancelled' | 'failed' | 'deleted'
export type IntelligentEvalSessionStatus = 'running' | 'completed' | 'failed' | 'expired'
export interface IntelligentEvalMessage {
id: string
session_id: string
role: string
content: string
latency_ms: number | null
created_at: string | null
}
export interface IntelligentEvalSession {
id: string
eval_id: string
target_id: string
persona: Record<string, unknown>
goal: string
dimension: string | null
status: IntelligentEvalSessionStatus
verdict: Record<string, unknown> | null
turn_count: number
created_at: string | null
closed_at: string | null
}
export interface IntelligentEvalVirtualUser {
persona: Record<string, unknown>
goal: string
}
export interface IntelligentEvalTimeSlot {
time_slot: string
sessions: number
scenario: string
}
export interface IntelligentEvalPlan {
dimensions: string[]
virtual_users: IntelligentEvalVirtualUser[]
time_distribution: IntelligentEvalTimeSlot[]
estimated_sessions: number
budget: { max_turns_per_session: number; total_max_turns: number }
completion_criteria: string
}
export interface ReportEvidence {
session_id?: string
turn_index?: number | null
user_said?: string
assistant_replied?: string
}
export interface ReportFinding {
issue: string
severity: string
dimension: string
evidence: ReportEvidence[]
suggestion?: string | null
related_sop?: string | null
}
export interface ReportHighlight {
description: string
dimension?: string | null
}
export interface IntelligentEvalReport {
summary: string
// 归一后的规范结构(后端 submit 边界归一,见 ADR-0011
scores: { overall: number | null; dimensions: Record<string, number> } | null
findings: ReportFinding[]
highlights: ReportHighlight[]
priority_recommendations: string[]
}
export interface IntelligentEval {
id: string
name: string
target_id: string
status: IntelligentEvalStatus
goal: string
seeds: Record<string, unknown>
intent: string
role_description: string
plan: IntelligentEvalPlan | null
plan_feedback: string | null
time_window_hours: number
report: IntelligentEvalReport | null
created_at: string | null
updated_at: string | null
started_at: string | null
completed_at: string | null
session_count: number
completed_sessions: number
sessions?: IntelligentEvalSession[]
}
export interface CreateIntelligentEvalPayload {
name: string
target_id: string
goal: string
seeds: Record<string, unknown>
intent: string
role_description: string
time_window_hours: number
}
export interface ConfigSnapshot {
id: string
eval_id: string
snapshot_type: 'created' | 'plan_submitted' | 'config_updated'
goal: string
seeds: Record<string, unknown>
intent: string
role_description: string
time_window_hours: number
plan: IntelligentEvalPlan | null
created_at: string | null
created_by: string
}
export interface ConfigSnapshotComparison {
snapshot_1: {
id: string
snapshot_type: string
created_at: string | null
}
snapshot_2: {
id: string
snapshot_type: string
created_at: string | null
}
differences: Record<string, { old: unknown; new: unknown }>
}
export interface DecisionLog {
id: string
eval_id: string
decision_type: 'execute_session' | 'wait' | 'start_analysis'
reason: string
context: Record<string, unknown>
cron_id: string
created_at: string | null
}
export type TaskQueueStatus = 'pending' | 'assigned' | 'completed' | 'failed'
export interface TaskQueueStats {
pending: number
assigned: number
completed: number
failed: number
unresolved: number
}
export interface TaskQueueItem {
id: string
eval_id: string
eval_name: string | null
eval_status: string | null
status: TaskQueueStatus
priority: number
reason: string
assigned_cron_id: string | null
assigned_at: string | null
completed_at: string | null
created_at: string | null
updated_at: string | null
error: string | null
}
export interface TaskQueueList {
tasks: TaskQueueItem[]
stats: TaskQueueStats
}
export interface ExecutionSlotProgress {
time_slot: string
planned: number
created: number
completed: number
is_current: boolean
is_past: boolean
}
export interface ExecutionProgress {
current_stage: 'planning' | 'approval' | 'executing' | 'done'
abnormal_outcome: 'cancelled' | 'failed' | null
blocker: string | null
next_action: string | null
slots: ExecutionSlotProgress[]
}
export const intelligentEvalsApi = {
list: (params?: { page?: number; page_size?: number; status?: string }) =>
api.get<{
intelligent_evals: IntelligentEval[]
total?: number
stats?: Record<string, number>
page?: number
page_size?: number
}>(
'/intelligent-evals',
{ params },
),
listTasks: (params?: { status?: TaskQueueStatus; limit?: number; eval_id?: string }) =>
api.get<TaskQueueList>('/intelligent-evals/tasks', { params }),
get: (id: string) => api.get<IntelligentEval>(`/intelligent-evals/${id}`),
getExecutionProgress: (id: string) =>
api.get<ExecutionProgress>(`/intelligent-evals/${id}/execution-progress`),
create: (data: CreateIntelligentEvalPayload) => api.post<IntelligentEval>('/intelligent-evals', data),
submitPlan: (id: string, plan: Record<string, unknown>) =>
api.put<IntelligentEval>(`/intelligent-evals/${id}/plan`, { plan }),
approve: (id: string) => api.post<IntelligentEval>(`/intelligent-evals/${id}/approve`),
reject: (id: string, feedback: string) =>
api.post<IntelligentEval>(`/intelligent-evals/${id}/reject`, { feedback }),
cancel: (id: string) => api.post<IntelligentEval>(`/intelligent-evals/${id}/cancel`),
remove: (id: string) => api.delete<{ ok: boolean }>(`/intelligent-evals/${id}`),
listSessions: (id: string) =>
api.get<{ sessions: IntelligentEvalSession[] }>(`/intelligent-evals/${id}/sessions`),
listMessages: (id: string, sessionId: string) =>
api.get<{ messages: IntelligentEvalMessage[] }>(`/intelligent-evals/${id}/sessions/${sessionId}/messages`),
getReport: (id: string) => api.get<IntelligentEvalReport>(`/intelligent-evals/${id}/report`),
downloadReportMarkdown: async (id: string) => {
const res = await api.get(`/intelligent-evals/${id}/report/markdown`, { responseType: 'blob' })
const url = URL.createObjectURL(res.data as Blob)
const a = document.createElement('a')
a.href = url
a.download = `intelligent-eval-report-${id.slice(0, 8)}.md`
a.click()
URL.revokeObjectURL(url)
},
// Config Snapshots
listConfigSnapshots: (id: string) =>
api.get<{ snapshots: ConfigSnapshot[] }>(`/intelligent-evals/${id}/config-snapshots`),
getConfigSnapshot: (id: string, snapshotId: string) =>
api.get<ConfigSnapshot>(`/intelligent-evals/${id}/config-snapshots/${snapshotId}`),
compareConfigSnapshots: (id: string, snapshotId1: string, snapshotId2: string) =>
api.post<ConfigSnapshotComparison>(`/intelligent-evals/${id}/config-snapshots/compare`, {
snapshot_id_1: snapshotId1,
snapshot_id_2: snapshotId2,
}),
// Decision Logs
listDecisionLogs: (id: string) =>
api.get<{ logs: DecisionLog[] }>(`/intelligent-evals/${id}/decision-logs`),
}
// ── 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}`),
}

View File

@ -0,0 +1,11 @@
import api from './client'
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 }),
}

View File

@ -0,0 +1,291 @@
import api from './client'
import { downloadBlob } from '../utils/download'
export interface CampaignPlanEntry {
scenario_id: string
offset_seconds: number
count: number
}
export interface ExplorationSeeds {
personas: string[]
goals: string[]
}
export interface ExplorationBudgetConfig {
max_sessions?: number | null
max_turns?: number | null
min_interval_seconds?: number | null
}
export interface ExplorationIssueCount {
issue: string
count: number
}
export interface ExplorationJudgeReview {
reviewed_sessions: number
findings: { dimension: string; rating: string; comment: string }[]
summaries: string[]
}
export interface ExplorationSummary {
session_count: number
sessions_with_experience: number
goal_achieved_count: number
goal_achievement_rate: number | null
issues: ExplorationIssueCount[]
misled: ExplorationIssueCount[]
judge_review: ExplorationJudgeReview | null
}
export interface ExplorationExperience {
goal_achieved: boolean
blockers: string[]
misled: string[]
emotion: string
notes: string
}
export interface ExplorationSession {
id: string
campaign_id: string
target_id: string
persona: Record<string, unknown>
goal: string
seed_ref: Record<string, unknown> | null
status: string
triggered_by: string
experience: ExplorationExperience | null
judge_review: Record<string, unknown> | null
turn_count: number
error: string | null
created_at: string | null
closed_at: string | null
}
export interface ExplorationMessage {
id: string
session_id: string
round_index: number
role: string
content: string
latency_ms: number | null
created_at: string | null
}
export const explorationApi = {
listSessions: (campaignId: string) =>
api.get<{ sessions: ExplorationSession[] }>(`/exploration/campaigns/${campaignId}/sessions`),
listMessages: (sessionId: string) =>
api.get<{ messages: ExplorationMessage[] }>(`/exploration/sessions/${sessionId}/messages`),
}
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
exploration_seeds: ExplorationSeeds | null
exploration_budget: ExplorationBudgetConfig | 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[]
exploration?: ExplorationSummary | null
}
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 type CampaignAnalysisStatus = 'none' | 'queued' | 'generating' | 'completed' | 'failed'
export interface CampaignAnalysisProblem {
severity: string
title: string
description: string
scenario_ids: string[]
evidence_run_ids: string[]
}
export interface CampaignAnalysisResult {
overall: string
problems: CampaignAnalysisProblem[]
scenario_narratives: { scenario_id: string; narrative: string }[]
suggestions: { priority: number; text: string }[]
}
export interface CampaignAnalysis {
status: CampaignAnalysisStatus
result?: CampaignAnalysisResult | null
error?: string | null
model_config_id?: string | null
triggered_by?: string
updated_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
exploration_seeds?: ExplorationSeeds | null
exploration_budget?: ExplorationBudgetConfig | null
}
export type ComparisonTrend = 'improving' | 'stable' | 'regressing'
export interface MetricDeltaPair {
baseline: number | null
current: number | null
delta: number | null
}
export interface MetricDiffScenario {
scenario_id: string
scenario_name: string
pass_rate: MetricDeltaPair
availability: MetricDeltaPair
avg_latency_ms: MetricDeltaPair
}
export interface MetricDiff {
overall: {
pass_rate: MetricDeltaPair
availability: MetricDeltaPair
avg_latency_ms: MetricDeltaPair
}
scenarios: MetricDiffScenario[]
}
export interface ComparisonBaselineInfo {
id: string
name: string
completed_at: string | null
}
export interface ComparisonProblemEvolution {
status: string
title: string
detail: string
scenario_ids: string[]
}
export interface ComparisonSuggestionTracking {
text: string
status: string
note: string
}
export interface ComparisonResult {
trend: ComparisonTrend
summary: string
problem_evolution: ComparisonProblemEvolution[]
suggestion_tracking: ComparisonSuggestionTracking[]
}
export interface CampaignComparison {
status: CampaignAnalysisStatus
auto_baseline: ComparisonBaselineInfo | null
metric_diff: MetricDiff | null
comparison: {
baseline_campaign_id: string
baseline: ComparisonBaselineInfo | null
result: ComparisonResult | null
error: string | null
model_config_id: string | null
triggered_by: string
updated_at: string | null
} | 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`),
getAnalysis: (id: string) => api.get<CampaignAnalysis>(`/campaigns/${id}/analysis`),
generateAnalysis: (id: string) => api.post<{ status: string }>(`/campaigns/${id}/analysis`),
getComparison: (id: string) => api.get<CampaignComparison>(`/campaigns/${id}/comparison`),
generateComparison: (id: string, baselineCampaignId?: string) =>
api.post<{ status: string }>(
`/campaigns/${id}/comparison`,
baselineCampaignId ? { baseline_campaign_id: baselineCampaignId } : {},
),
downloadReport: async (id: string) => {
const res = await api.get(`/campaigns/${id}/report/markdown`, { responseType: 'blob' })
downloadBlob(res.data as Blob, `campaign-report-${id.slice(0, 8)}.md`)
},
}

View File

@ -0,0 +1,42 @@
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

View File

@ -0,0 +1,51 @@
import api from './client'
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}`),
}

View File

@ -0,0 +1,15 @@
/**
* API axios client.ts
* api/<domain>.ts barrel '../api'
*/
export { default, authToken, setOnUnauthorized } from './client'
export * from './auth'
export * from './targets'
export * from './scenarios'
export * from './modelConfigs'
export * from './runs'
export * from './reports'
export * from './stats'
export * from './campaigns'
export * from './intelligentEvals'
export * from './files'

View File

@ -0,0 +1,248 @@
import api from './client'
import { downloadBlob } from '../utils/download'
export type IntelligentEvalStatus =
| 'draft' | 'planning' | 'pending_approval'
| 'executing' | 'completed' | 'cancelled' | 'failed' | 'deleted'
export type IntelligentEvalSessionStatus = 'running' | 'completed' | 'failed' | 'expired'
export interface IntelligentEvalMessage {
id: string
session_id: string
role: string
content: string
latency_ms: number | null
created_at: string | null
}
export interface IntelligentEvalSession {
id: string
eval_id: string
target_id: string
persona: Record<string, unknown>
goal: string
dimension: string | null
status: IntelligentEvalSessionStatus
verdict: Record<string, unknown> | null
turn_count: number
created_at: string | null
closed_at: string | null
}
export interface IntelligentEvalVirtualUser {
persona: Record<string, unknown>
goal: string
}
export interface IntelligentEvalTimeSlot {
time_slot: string
sessions: number
scenario: string
}
export interface IntelligentEvalPlan {
dimensions: string[]
virtual_users: IntelligentEvalVirtualUser[]
time_distribution: IntelligentEvalTimeSlot[]
estimated_sessions: number
budget: { max_turns_per_session: number; total_max_turns: number }
completion_criteria: string
}
export interface ReportEvidence {
session_id?: string
turn_index?: number | null
user_said?: string
assistant_replied?: string
}
export interface ReportFinding {
issue: string
severity: string
dimension: string
evidence: ReportEvidence[]
suggestion?: string | null
related_sop?: string | null
}
export interface ReportHighlight {
description: string
dimension?: string | null
}
export interface IntelligentEvalReport {
summary: string
// 归一后的规范结构(后端 submit 边界归一,见 ADR-0011
scores: { overall: number | null; dimensions: Record<string, number> } | null
findings: ReportFinding[]
highlights: ReportHighlight[]
priority_recommendations: string[]
}
export interface IntelligentEval {
id: string
name: string
target_id: string
status: IntelligentEvalStatus
goal: string
seeds: Record<string, unknown>
intent: string
role_description: string
plan: IntelligentEvalPlan | null
plan_feedback: string | null
time_window_hours: number
report: IntelligentEvalReport | null
created_at: string | null
updated_at: string | null
started_at: string | null
completed_at: string | null
session_count: number
completed_sessions: number
sessions?: IntelligentEvalSession[]
}
export interface CreateIntelligentEvalPayload {
name: string
target_id: string
goal: string
seeds: Record<string, unknown>
intent: string
role_description: string
time_window_hours: number
}
export interface ConfigSnapshot {
id: string
eval_id: string
snapshot_type: 'created' | 'plan_submitted' | 'config_updated'
goal: string
seeds: Record<string, unknown>
intent: string
role_description: string
time_window_hours: number
plan: IntelligentEvalPlan | null
created_at: string | null
created_by: string
}
export interface ConfigSnapshotComparison {
snapshot_1: {
id: string
snapshot_type: string
created_at: string | null
}
snapshot_2: {
id: string
snapshot_type: string
created_at: string | null
}
differences: Record<string, { old: unknown; new: unknown }>
}
export interface DecisionLog {
id: string
eval_id: string
decision_type: 'execute_session' | 'wait' | 'start_analysis'
reason: string
context: Record<string, unknown>
cron_id: string
created_at: string | null
}
export type TaskQueueStatus = 'pending' | 'assigned' | 'completed' | 'failed'
export interface TaskQueueStats {
pending: number
assigned: number
completed: number
failed: number
unresolved: number
}
export interface TaskQueueItem {
id: string
eval_id: string
eval_name: string | null
eval_status: string | null
status: TaskQueueStatus
priority: number
reason: string
assigned_cron_id: string | null
assigned_at: string | null
completed_at: string | null
created_at: string | null
updated_at: string | null
error: string | null
}
export interface TaskQueueList {
tasks: TaskQueueItem[]
stats: TaskQueueStats
}
export interface ExecutionSlotProgress {
time_slot: string
planned: number
created: number
completed: number
is_current: boolean
is_past: boolean
}
export interface ExecutionProgress {
current_stage: 'planning' | 'approval' | 'executing' | 'done'
abnormal_outcome: 'cancelled' | 'failed' | null
blocker: string | null
next_action: string | null
slots: ExecutionSlotProgress[]
}
export const intelligentEvalsApi = {
list: (params?: { page?: number; page_size?: number; status?: string }) =>
api.get<{
intelligent_evals: IntelligentEval[]
total?: number
stats?: Record<string, number>
page?: number
page_size?: number
}>(
'/intelligent-evals',
{ params },
),
listTasks: (params?: { status?: TaskQueueStatus; limit?: number; eval_id?: string }) =>
api.get<TaskQueueList>('/intelligent-evals/tasks', { params }),
get: (id: string) => api.get<IntelligentEval>(`/intelligent-evals/${id}`),
getExecutionProgress: (id: string) =>
api.get<ExecutionProgress>(`/intelligent-evals/${id}/execution-progress`),
create: (data: CreateIntelligentEvalPayload) => api.post<IntelligentEval>('/intelligent-evals', data),
submitPlan: (id: string, plan: Record<string, unknown>) =>
api.put<IntelligentEval>(`/intelligent-evals/${id}/plan`, { plan }),
approve: (id: string) => api.post<IntelligentEval>(`/intelligent-evals/${id}/approve`),
reject: (id: string, feedback: string) =>
api.post<IntelligentEval>(`/intelligent-evals/${id}/reject`, { feedback }),
cancel: (id: string) => api.post<IntelligentEval>(`/intelligent-evals/${id}/cancel`),
remove: (id: string) => api.delete<{ ok: boolean }>(`/intelligent-evals/${id}`),
listSessions: (id: string) =>
api.get<{ sessions: IntelligentEvalSession[] }>(`/intelligent-evals/${id}/sessions`),
listMessages: (id: string, sessionId: string) =>
api.get<{ messages: IntelligentEvalMessage[] }>(`/intelligent-evals/${id}/sessions/${sessionId}/messages`),
getReport: (id: string) => api.get<IntelligentEvalReport>(`/intelligent-evals/${id}/report`),
downloadReportMarkdown: async (id: string) => {
const res = await api.get(`/intelligent-evals/${id}/report/markdown`, { responseType: 'blob' })
downloadBlob(res.data as Blob, `intelligent-eval-report-${id.slice(0, 8)}.md`)
},
// Config Snapshots
listConfigSnapshots: (id: string) =>
api.get<{ snapshots: ConfigSnapshot[] }>(`/intelligent-evals/${id}/config-snapshots`),
getConfigSnapshot: (id: string, snapshotId: string) =>
api.get<ConfigSnapshot>(`/intelligent-evals/${id}/config-snapshots/${snapshotId}`),
compareConfigSnapshots: (id: string, snapshotId1: string, snapshotId2: string) =>
api.post<ConfigSnapshotComparison>(`/intelligent-evals/${id}/config-snapshots/compare`, {
snapshot_id_1: snapshotId1,
snapshot_id_2: snapshotId2,
}),
// Decision Logs
listDecisionLogs: (id: string) =>
api.get<{ logs: DecisionLog[] }>(`/intelligent-evals/${id}/decision-logs`),
}

View File

@ -0,0 +1,74 @@
import api from './client'
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 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`),
}

View File

@ -0,0 +1,13 @@
import api from './client'
import { downloadBlob } from '../utils/download'
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' })
downloadBlob(res.data as Blob, `report-${runId.slice(0, 8)}.${ext}`)
},
compare: (run1: string, run2: string) =>
api.get(`/reports/compare`, { params: { run1, run2 } }),
}

View File

@ -0,0 +1,89 @@
import api from './client'
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 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 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`),
}

View File

@ -0,0 +1,24 @@
import api from './client'
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 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}`),
}

View File

@ -0,0 +1,34 @@
import api from './client'
import type { Run, RunTrigger } from './runs'
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 statsApi = {
dashboard: () => api.get<DashboardStats>('/stats/dashboard'),
trend: (days?: number) => api.get<TrendPoint[]>('/stats/trend', { params: { days } }),
}

View File

@ -0,0 +1,24 @@
import api from './client'
import type { Run } from './runs'
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 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`),
}