Files
model-platform/frontend/app/services/operationsApi.ts
T

200 lines
6.2 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 {
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<T> = {
data: T;
meta?: Record<string, unknown>;
};
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<T>(path: string, init: RequestInit = {}): Promise<T> {
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<T> & {
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<void> {
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<ModelRecord[]> {
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<OperationsModelDto[]>(`/models${suffix}`, { signal: params.signal });
return data.map(toModelRecord);
}
export async function getOperationsModel(
modelId: string,
workspaceId?: string,
signal?: AbortSignal,
): Promise<ModelRecord> {
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<OperationsModelDto>(`/models/${encodeURIComponent(modelId)}?${query.toString()}`, { signal });
return toModelRecord(data);
}
export async function getMonthlyMonitoringResult(
modelId: string,
month: string,
workspaceId?: string,
signal?: AbortSignal,
): Promise<MonitoringRow> {
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<MonthlyMonitoringResultDto>(
`/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,
};
}