feat: 接入运维真实数据与三角色权限链路

- 新增 model_deploy 与 model_operations 双库查询,支持模型列表、模型详情、单月监控结果和运维工作台真实接口。

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

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

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

- 新增架构实现基线、周目标完成情况和角色权限矩阵初始化 SQL 文档。
This commit is contained in:
郑龙捷
2026-09-02 15:02:47 +08:00
parent 6d044784ec
commit 74dd97428f
51 changed files with 2142 additions and 1062 deletions
+51 -44
View File
@@ -1,14 +1,12 @@
import {
MODELS,
monitoringRows,
type ModelCategoryId,
type ModelRecord,
type ModelStatus,
type MonitoringRow,
type RankingStatus,
import type {
ModelCategoryId,
ModelRecord,
ModelStatus,
MonitoringRow,
RankingStatus,
} from "~/features/operations/modelData";
export type OperationsApiMode = "mock" | "api";
export type OperationsApiMode = "api";
export type OperationsModelListParams = {
workspaceId?: string;
@@ -49,6 +47,41 @@ export type MonthlyMonitoringResultDto = {
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>;
@@ -66,8 +99,7 @@ export class OperationsApiError extends Error {
}
}
const configuredMode = import.meta.env.VITE_OPERATIONS_API_MODE;
export const operationsApiMode: OperationsApiMode = configuredMode === "api" ? "api" : "mock";
export const operationsApiMode: OperationsApiMode = "api";
const API_BASE = "/api/v1/operations";
function requireWorkspaceId(workspaceId?: string): string {
@@ -117,28 +149,7 @@ function toModelRecord(dto: OperationsModelDto): ModelRecord {
};
}
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);
@@ -155,12 +166,6 @@ export async function getOperationsModel(
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);
@@ -172,12 +177,6 @@ export async function getMonthlyMonitoringResult(
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,
@@ -197,3 +196,11 @@ export async function getMonthlyMonitoringResult(
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 });
}