import { MODELS, monitoringRows, type ModelCategoryId, type ModelRecord, type ModelStatus, type MonitoringRow, type RankingStatus, } from "~/features/operations/modelData"; export type OperationsApiMode = "mock" | "api"; export type OperationsModelListParams = { workspaceId?: string; bank?: string; category?: ModelCategoryId; status?: ModelStatus; keyword?: string; signal?: AbortSignal; }; export type OperationsModelDto = { model_instance_id: string; bank_name: string; model_category: ModelCategoryId; model_name: string; model_id: string; model_version: string; model_status: ModelStatus; last_iteration_date: string; ranking_result: RankingStatus; ks: number; psi: number; ks_mom_drop: number; secondary_hits_6m: number; is_wuji_bank: boolean; last_processed_at: string | null; previous_advice: string; common_model_name: string | null; }; export type MonthlyMonitoringResultDto = { model_instance_id: string; monitor_month: string; ranking_result: RankingStatus; ks: number; psi: number; ks_mom_drop: number; secondary_hits_6m: number; }; type ApiEnvelope = { data: T; meta?: Record; }; export class OperationsApiError extends Error { readonly status: number; readonly code?: string; constructor(message: string, status: number, code?: string) { super(message); this.name = "OperationsApiError"; this.status = status; this.code = code; } } const configuredMode = import.meta.env.VITE_OPERATIONS_API_MODE; export const operationsApiMode: OperationsApiMode = configuredMode === "api" ? "api" : "mock"; const API_BASE = "/api/v1/operations"; function requireWorkspaceId(workspaceId?: string): string { if (workspaceId) return workspaceId; throw new OperationsApiError("请先选择项目空间", 400, "WORKSPACE_REQUIRED"); } async function request(path: string, init: RequestInit = {}): Promise { const response = await fetch(`${API_BASE}${path}`, { ...init, credentials: "same-origin", headers: { Accept: "application/json", ...init.headers, }, }); const payload = await response.json().catch(() => ({})) as ApiEnvelope & { detail?: string | { code?: string; message?: string }; }; if (!response.ok) { const message = typeof payload.detail === "string" ? payload.detail : payload.detail?.message ?? `请求失败(HTTP ${response.status})`; throw new OperationsApiError(message, response.status, typeof payload.detail === "object" ? payload.detail.code : undefined); } return payload.data; } function toModelRecord(dto: OperationsModelDto): ModelRecord { return { bank: dto.bank_name, category: dto.model_category, name: dto.model_name, modelId: dto.model_id, version: dto.model_version, status: dto.model_status, iteratedAt: dto.last_iteration_date, ranking: dto.ranking_result, ks: dto.ks, psi: dto.psi, ksDrop: dto.ks_mom_drop, secondaryHits: dto.secondary_hits_6m, wuji: dto.is_wuji_bank, processedAt: dto.last_processed_at, previousAdvice: dto.previous_advice, commonModel: dto.common_model_name, }; } function mockDelay(signal?: AbortSignal): Promise { return new Promise((resolve, reject) => { const timer = window.setTimeout(resolve, 160); signal?.addEventListener("abort", () => { window.clearTimeout(timer); reject(new DOMException("Request aborted", "AbortError")); }, { once: true }); }); } export async function listOperationsModels(params: OperationsModelListParams = {}): Promise { if (operationsApiMode === "mock") { await mockDelay(params.signal); const keyword = params.keyword?.trim().toLowerCase(); return MODELS .filter((model) => !params.bank || model.bank === params.bank) .filter((model) => !params.category || model.category === params.category) .filter((model) => !params.status || model.status === params.status) .filter((model) => !keyword || `${model.bank} ${model.name} ${model.modelId} ${model.version}`.toLowerCase().includes(keyword)) .map((model) => ({ ...model })); } const query = new URLSearchParams(); query.set("workspace_id", requireWorkspaceId(params.workspaceId)); if (params.bank) query.set("bank", params.bank); if (params.category) query.set("category", params.category); if (params.status) query.set("status", params.status); if (params.keyword) query.set("keyword", params.keyword); const suffix = query.size ? `?${query.toString()}` : ""; const data = await request(`/models${suffix}`, { signal: params.signal }); return data.map(toModelRecord); } export async function getOperationsModel( modelId: string, workspaceId?: string, signal?: AbortSignal, ): Promise { if (operationsApiMode === "mock") { await mockDelay(signal); const model = MODELS.find((item) => item.modelId === modelId); if (!model) throw new OperationsApiError("模型不存在", 404, "MODEL_NOT_FOUND"); return { ...model }; } const query = new URLSearchParams({ workspace_id: requireWorkspaceId(workspaceId) }); const data = await request(`/models/${encodeURIComponent(modelId)}?${query.toString()}`, { signal }); return toModelRecord(data); } export async function getMonthlyMonitoringResult( modelId: string, month: string, workspaceId?: string, signal?: AbortSignal, ): Promise { if (operationsApiMode === "mock") { await mockDelay(signal); const result = monitoringRows(MODELS).find((item) => item.modelId === modelId && item.monitorMonth === month); if (!result) throw new OperationsApiError("该月份暂无监控结果", 404, "MONITOR_RESULT_NOT_FOUND"); return { ...result }; } const model = await getOperationsModel(modelId, workspaceId, signal); const query = new URLSearchParams({ month, workspace_id: requireWorkspaceId(workspaceId), }); const data = await request( `/models/${encodeURIComponent(modelId)}/monitor-results?${query.toString()}`, { signal }, ); return { ...model, monitorMonth: data.monitor_month, ranking: data.ranking_result, ks: data.ks, psi: data.psi, ksDrop: data.ks_mom_drop, secondaryHits: data.secondary_hits_6m, }; }