Files
model-platform/frontend/app/services/operationsApi.ts
T
郑龙捷 74dd97428f feat: 接入运维真实数据与三角色权限链路
- 新增 model_deploy 与 model_operations 双库查询,支持模型列表、模型详情、单月监控结果和运维工作台真实接口。

- 关闭运维模块生产 Mock 数据,补充缺表/空数据降级、工作台空状态和首条纵向链路测试。

- 按业务团队、模型团队、管理员权限矩阵接入菜单、页面、操作按钮和后端接口权限校验,支持 business_team 角色。

- 更新工作台布局、深色欢迎卡片、全宽页面适配、顶部回退,以及权限分组展示。

- 新增架构实现基线、周目标完成情况和角色权限矩阵初始化 SQL 文档。
2026-09-02 15:02:47 +08:00

207 lines
5.7 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 type {
ModelCategoryId,
ModelRecord,
ModelStatus,
MonitoringRow,
RankingStatus,
} from "~/features/operations/modelData";
export type OperationsApiMode = "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;
};
export type WorkbenchTone = "normal" | "warning" | "danger" | "primary";
export type OperationsWorkbenchKpi = {
key: string;
label: string;
value: number;
detail: string;
target: string;
tone: WorkbenchTone;
};
export type OperationsWorkbenchItem = {
id: string;
tone: WorkbenchTone;
title: string;
detail: string;
action_label: string;
target: string;
};
export type OperationsWorkbenchActivity = {
occurred_at: string;
actor: string;
text: string;
};
export type OperationsWorkbenchDto = {
role: "admin" | "model_team" | "business_team";
alerts: string[];
kpis: OperationsWorkbenchKpi[];
todos: OperationsWorkbenchItem[];
watches: OperationsWorkbenchItem[];
activities: OperationsWorkbenchActivity[];
};
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;
}
}
export const operationsApiMode: OperationsApiMode = "api";
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,
};
}
export async function listOperationsModels(params: OperationsModelListParams = {}): Promise<ModelRecord[]> {
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> {
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> {
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,
};
}
export async function getOperationsWorkbench(
workspaceId?: string,
signal?: AbortSignal,
): Promise<OperationsWorkbenchDto> {
const query = new URLSearchParams({ workspace_id: requireWorkspaceId(workspaceId) });
return request<OperationsWorkbenchDto>(`/workbench?${query.toString()}`, { signal });
}