- 新增 model_deploy 与 model_operations 双库查询,支持模型列表、模型详情、单月监控结果和运维工作台真实接口。 - 关闭运维模块生产 Mock 数据,补充缺表/空数据降级、工作台空状态和首条纵向链路测试。 - 按业务团队、模型团队、管理员权限矩阵接入菜单、页面、操作按钮和后端接口权限校验,支持 business_team 角色。 - 更新工作台布局、深色欢迎卡片、全宽页面适配、顶部回退,以及权限分组展示。 - 新增架构实现基线、周目标完成情况和角色权限矩阵初始化 SQL 文档。
207 lines
5.7 KiB
TypeScript
207 lines
5.7 KiB
TypeScript
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 });
|
||
}
|