200 lines
6.2 KiB
TypeScript
200 lines
6.2 KiB
TypeScript
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,
|
||
};
|
||
}
|