chore: checkpoint Boss app v2.5.11
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
createAdminBackup,
|
||||
fetchAdminBackups,
|
||||
fetchBossAdminBackoffice,
|
||||
fetchSkillLifecycleRequests,
|
||||
postAdminAccess,
|
||||
postDeviceCodexRemoteControl,
|
||||
postRiskAction,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
type BossAdminBackofficePayload,
|
||||
type BossAdminBackupSnapshot,
|
||||
type BossAdminBackupStatus,
|
||||
type BossAdminSkillLifecycleRequest,
|
||||
} from "./api/bossAdmin";
|
||||
|
||||
type AdminRecord = Record<string, unknown>;
|
||||
@@ -39,6 +41,8 @@ const backupLoading = ref(false);
|
||||
const backupSnapshots = ref<BossAdminBackupSnapshot[]>([]);
|
||||
const backupStatus = ref<BossAdminBackupStatus | null>(null);
|
||||
const backupReason = ref("manual");
|
||||
const skillLifecycleLoading = ref(false);
|
||||
const skillLifecycleRequests = ref<BossAdminSkillLifecycleRequest[]>([]);
|
||||
|
||||
const companyForm = reactive({
|
||||
companyId: "",
|
||||
@@ -105,6 +109,13 @@ const defaultBackofficeInsights: BossAdminBackofficePayload["insights"] = {
|
||||
skillUsageAudit: [],
|
||||
recoveryActions: [],
|
||||
backupStatus: {},
|
||||
dataSafetySummary: {},
|
||||
taskRiskSummary: {},
|
||||
taskSlaPanel: {
|
||||
generatedAt: "",
|
||||
summary: {},
|
||||
rows: [],
|
||||
},
|
||||
capabilitySummary: {},
|
||||
surface: "platform",
|
||||
};
|
||||
@@ -144,6 +155,23 @@ const notifications = computed(() => payload.value?.workbench.notifications ?? [
|
||||
const riskTimeline = computed(() => payload.value?.audit.riskTimeline ?? []);
|
||||
const auditLogs = computed(() => payload.value?.audit.permissionLogs ?? []);
|
||||
const grants = computed(() => payload.value?.resourceGroups.grants ?? { devices: [], projects: [], skills: [] });
|
||||
const taskSlaPanel = computed(() => insights.value.taskSlaPanel ?? defaultBackofficeInsights.taskSlaPanel);
|
||||
const taskSlaRows = computed(() => taskSlaPanel.value.rows ?? []);
|
||||
const taskSlaMetrics = computed(() => [
|
||||
{ label: "运行任务", value: numberValue(taskSlaPanel.value.summary?.active), tone: "black" },
|
||||
{ label: "SLA 超时", value: numberValue(taskSlaPanel.value.summary?.breached), tone: "red" },
|
||||
{ label: "可自动恢复", value: numberValue(taskSlaPanel.value.summary?.autoRecoverable), tone: "green" },
|
||||
{ label: "终态失败", value: numberValue(taskSlaPanel.value.summary?.terminal), tone: "orange" },
|
||||
]);
|
||||
const skillRequestMetrics = computed(() => [
|
||||
{ label: "待执行", value: skillLifecycleRequests.value.filter((item) => text(item.status, "").toLowerCase() === "queued").length, tone: "orange" },
|
||||
{
|
||||
label: "执行中",
|
||||
value: skillLifecycleRequests.value.filter((item) => ["claimed", "running", "processing"].includes(text(item.status, "").toLowerCase())).length,
|
||||
tone: "green",
|
||||
},
|
||||
{ label: "最近请求", value: skillLifecycleRequests.value.length, tone: "black" },
|
||||
]);
|
||||
|
||||
const currentSectionTitle = computed(() => menuTree.value.find((item) => item.key === activeKey.value)?.label ?? "总览");
|
||||
const currentCompanyName = computed(() => text(payload.value?.currentCompany?.name, "当前企业"));
|
||||
@@ -251,6 +279,9 @@ function selectMenu(key: string) {
|
||||
if (key === "enterprise-backup" || key === "enterprise-risk-backup") {
|
||||
void loadBackupSnapshots();
|
||||
}
|
||||
if (key === "enterprise-skill") {
|
||||
void loadSkillLifecycleRequests();
|
||||
}
|
||||
}
|
||||
|
||||
function menuIcon(key: string) {
|
||||
@@ -284,6 +315,32 @@ function riskColor(value: unknown) {
|
||||
return "blue";
|
||||
}
|
||||
|
||||
function slaColor(value: unknown) {
|
||||
const level = text(value, "").toLowerCase();
|
||||
if (level === "terminal" || level === "breached") return "red";
|
||||
if (level === "recoverable") return "green";
|
||||
if (level === "watch") return "orange";
|
||||
return "blue";
|
||||
}
|
||||
|
||||
function slaLabel(value: unknown) {
|
||||
const level = text(value, "").toLowerCase();
|
||||
if (level === "terminal") return "终态失败";
|
||||
if (level === "breached") return "SLA 超时";
|
||||
if (level === "recoverable") return "可恢复";
|
||||
if (level === "watch") return "观察中";
|
||||
return "正常";
|
||||
}
|
||||
|
||||
function formatDurationMs(value: unknown) {
|
||||
const ms = typeof value === "number" && Number.isFinite(value) ? value : 0;
|
||||
if (ms <= 0) return "-";
|
||||
const minutes = Math.floor(ms / 60_000);
|
||||
if (minutes < 60) return `${minutes} 分钟`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
return `${hours} 小时 ${minutes % 60} 分钟`;
|
||||
}
|
||||
|
||||
function formatBytes(value: unknown) {
|
||||
const bytes = typeof value === "number" && Number.isFinite(value) ? value : 0;
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
@@ -321,6 +378,9 @@ async function runMutation(label: string, task: () => Promise<unknown>) {
|
||||
hide();
|
||||
message.success(`${label}完成`);
|
||||
await loadBackoffice(adminSurface.value);
|
||||
if (activeKey.value === "enterprise-skill") {
|
||||
await loadSkillLifecycleRequests();
|
||||
}
|
||||
} catch (err) {
|
||||
hide();
|
||||
message.error(`${label}失败:${err instanceof Error ? err.message : "UNKNOWN_ERROR"}`);
|
||||
@@ -477,6 +537,61 @@ async function createSkillRequest() {
|
||||
);
|
||||
}
|
||||
|
||||
async function loadSkillLifecycleRequests() {
|
||||
skillLifecycleLoading.value = true;
|
||||
try {
|
||||
const result = await fetchSkillLifecycleRequests();
|
||||
skillLifecycleRequests.value = result.requests ?? [];
|
||||
} catch (err) {
|
||||
skillLifecycleRequests.value = [];
|
||||
if (adminSurface.value === "platform") {
|
||||
message.warning(`Skill 请求队列加载失败:${err instanceof Error ? err.message : "UNKNOWN_ERROR"}`);
|
||||
}
|
||||
} finally {
|
||||
skillLifecycleLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function fillSkillRequestForm(record: AdminRecord, action = "update") {
|
||||
skillRequestForm.action = action;
|
||||
skillRequestForm.skillId = text(record.skillId, "");
|
||||
if (!skillRequestForm.deviceId && devices.value.length > 0) {
|
||||
skillRequestForm.deviceId = text(devices.value[0].id, "");
|
||||
}
|
||||
skillRequestForm.sourceUrl = text(record.sourceUrl, "");
|
||||
skillRequestForm.targetVersion = text(record.version, "");
|
||||
}
|
||||
|
||||
async function quickSkillRequest(record: AdminRecord, action: "update" | "rollback" | "version_lock") {
|
||||
const skillId = text(record.skillId, "");
|
||||
const deviceId = skillRequestForm.deviceId || text(record.deviceId, "") || text(devices.value[0]?.id, "");
|
||||
if (!skillId || !deviceId) {
|
||||
message.warning("请先选择设备和 Skill");
|
||||
fillSkillRequestForm(record, action);
|
||||
return;
|
||||
}
|
||||
const payload: Record<string, unknown> = {
|
||||
action,
|
||||
deviceId,
|
||||
skillId,
|
||||
note: `quick-dispatch:${action}`,
|
||||
};
|
||||
if (action === "rollback") {
|
||||
const rollbackToVersion = window.prompt("请输入要回滚到的版本");
|
||||
if (!rollbackToVersion) return;
|
||||
payload.rollbackToVersion = rollbackToVersion;
|
||||
}
|
||||
if (action === "version_lock") {
|
||||
const lockedVersion = window.prompt("请输入要锁定的版本");
|
||||
if (!lockedVersion) return;
|
||||
payload.lockedVersion = lockedVersion;
|
||||
}
|
||||
await runMutation(
|
||||
action === "update" ? "更新下发" : action === "rollback" ? "回滚" : "版本锁定",
|
||||
() => postSkillLifecycleRequest(payload),
|
||||
);
|
||||
}
|
||||
|
||||
async function loadBackupSnapshots() {
|
||||
backupLoading.value = true;
|
||||
try {
|
||||
@@ -511,6 +626,9 @@ watch(activeKey, (key) => {
|
||||
if (key === "enterprise-backup" || key === "enterprise-risk-backup") {
|
||||
void loadBackupSnapshots();
|
||||
}
|
||||
if (key === "enterprise-skill") {
|
||||
void loadSkillLifecycleRequests();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -518,6 +636,9 @@ onMounted(async () => {
|
||||
if (activeKey.value === "enterprise-backup" || activeKey.value === "enterprise-risk-backup") {
|
||||
await loadBackupSnapshots();
|
||||
}
|
||||
if (activeKey.value === "enterprise-skill") {
|
||||
await loadSkillLifecycleRequests();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -894,6 +1015,40 @@ onMounted(async () => {
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</a-card>
|
||||
<a-card title="任务 SLA 面板" :bordered="false">
|
||||
<div class="boss-admin-metrics compact">
|
||||
<div
|
||||
v-for="item in taskSlaMetrics"
|
||||
:key="item.label"
|
||||
class="boss-admin-metric"
|
||||
:class="item.tone"
|
||||
>
|
||||
<span>{{ item.label }}</span>
|
||||
<strong>{{ item.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<a-table class="boss-admin-form-gap" :data-source="taskSlaRows" row-key="taskId">
|
||||
<a-table-column title="任务" data-index="taskType" />
|
||||
<a-table-column title="状态">
|
||||
<template #default="{ record }">
|
||||
<a-tag :color="slaColor(record.slaLevel)">{{ slaLabel(record.slaLevel) }}</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="阶段" data-index="phase" />
|
||||
<a-table-column title="设备" data-index="deviceId" />
|
||||
<a-table-column title="尝试" data-index="attemptLabel" />
|
||||
<a-table-column title="空闲">
|
||||
<template #default="{ record }">
|
||||
{{ formatDurationMs(record.idleMs) }}
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="建议动作">
|
||||
<template #default="{ record }">
|
||||
{{ text(record.recommendedAction) }}
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeKey === 'platform-audit'" class="boss-admin-section-grid">
|
||||
@@ -1147,7 +1302,23 @@ onMounted(async () => {
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeKey === 'enterprise-skill'" class="boss-admin-section-grid">
|
||||
<a-card title="创建 Skill 请求" :bordered="false">
|
||||
<a-card class="boss-admin-hero" :bordered="false">
|
||||
<p class="boss-admin-eyebrow">Skill 管理分发</p>
|
||||
<h3>统一管理 Skill 安装、更新、回滚、版本锁定和企业内权限分配</h3>
|
||||
<div class="boss-admin-metrics compact">
|
||||
<div
|
||||
v-for="metric in skillRequestMetrics"
|
||||
:key="metric.label"
|
||||
class="boss-admin-metric"
|
||||
:class="metric.tone"
|
||||
>
|
||||
<span>{{ metric.label }}</span>
|
||||
<strong>{{ metric.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-card title="快捷下发" :bordered="false">
|
||||
<a-form layout="vertical" class="boss-admin-form">
|
||||
<a-form-item label="动作">
|
||||
<a-select v-model:value="skillRequestForm.action">
|
||||
@@ -1186,7 +1357,10 @@ onMounted(async () => {
|
||||
<a-input v-model:value="skillRequestForm.checksum" placeholder="sha256 checksum" />
|
||||
<a-input v-model:value="skillRequestForm.note" class="boss-admin-form-gap" placeholder="备注" />
|
||||
</a-form-item>
|
||||
<a-button type="primary" block :loading="mutating" @click="createSkillRequest">创建 Skill 请求</a-button>
|
||||
<a-space wrap>
|
||||
<a-button type="primary" :loading="mutating" @click="createSkillRequest">创建 Skill 请求 / 安装远端 Skill</a-button>
|
||||
<a-button :loading="skillLifecycleLoading" @click="loadSkillLifecycleRequests">刷新请求队列</a-button>
|
||||
</a-space>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
@@ -1197,6 +1371,45 @@ onMounted(async () => {
|
||||
<a-table-column title="分类" data-index="category" />
|
||||
<a-table-column title="设备数" data-index="deviceCount" />
|
||||
<a-table-column title="更新时间" data-index="updatedAt" />
|
||||
<a-table-column title="操作">
|
||||
<template #default="{ record }">
|
||||
<a-space wrap>
|
||||
<a-button size="small" @click="fillSkillRequestForm(record, 'install')">安装远端 Skill</a-button>
|
||||
<a-button size="small" type="primary" @click="quickSkillRequest(record, 'update')">更新下发</a-button>
|
||||
<a-button size="small" @click="quickSkillRequest(record, 'rollback')">回滚</a-button>
|
||||
<a-button size="small" @click="quickSkillRequest(record, 'version_lock')">版本锁定</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</a-card>
|
||||
<a-card title="Skill 请求队列" :bordered="false">
|
||||
<a-table
|
||||
:loading="skillLifecycleLoading"
|
||||
:data-source="skillLifecycleRequests"
|
||||
row-key="requestId"
|
||||
:pagination="{ pageSize: 8 }"
|
||||
>
|
||||
<a-table-column title="请求" data-index="requestId" />
|
||||
<a-table-column title="动作" data-index="action" />
|
||||
<a-table-column title="状态">
|
||||
<template #default="{ record }">
|
||||
<a-tag :color="statusColor(record.status)">{{ text(record.status) }}</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="设备" data-index="deviceId" />
|
||||
<a-table-column title="Skill" data-index="skillId" />
|
||||
<a-table-column title="版本">
|
||||
<template #default="{ record }">
|
||||
{{ text(record.targetVersion || record.rollbackToVersion || record.lockedVersion) }}
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="结果">
|
||||
<template #default="{ record }">
|
||||
{{ text(record.resultSummary || record.error) }}
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="时间" data-index="requestedAt" />
|
||||
</a-table>
|
||||
</a-card>
|
||||
<a-card title="使用审计" :bordered="false">
|
||||
@@ -1234,6 +1447,35 @@ onMounted(async () => {
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</a-card>
|
||||
<a-card title="任务 SLA 面板" :bordered="false">
|
||||
<div class="boss-admin-metrics compact">
|
||||
<div
|
||||
v-for="item in taskSlaMetrics"
|
||||
:key="item.label"
|
||||
class="boss-admin-metric"
|
||||
:class="item.tone"
|
||||
>
|
||||
<span>{{ item.label }}</span>
|
||||
<strong>{{ item.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<a-table class="boss-admin-form-gap" :data-source="taskSlaRows" row-key="taskId">
|
||||
<a-table-column title="任务" data-index="taskType" />
|
||||
<a-table-column title="状态">
|
||||
<template #default="{ record }">
|
||||
<a-tag :color="slaColor(record.slaLevel)">{{ slaLabel(record.slaLevel) }}</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="阶段" data-index="phase" />
|
||||
<a-table-column title="设备" data-index="deviceId" />
|
||||
<a-table-column title="尝试" data-index="attemptLabel" />
|
||||
<a-table-column title="建议动作">
|
||||
<template #default="{ record }">
|
||||
{{ text(record.recommendedAction) }}
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</a-card>
|
||||
<a-card title="业务级回退" :bordered="false">
|
||||
<div class="boss-admin-recovery-grid">
|
||||
<div v-for="action in insights.recoveryActions" :key="action" class="boss-admin-recovery-card">
|
||||
@@ -1286,7 +1528,7 @@ onMounted(async () => {
|
||||
<a-button :loading="backupLoading" @click="loadBackupSnapshots">刷新快照</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
<a-card title="快照清单" :bordered="false">
|
||||
<a-card title="快照清单" class="boss-admin-wide-card" :bordered="false">
|
||||
<a-table :loading="backupLoading" :data-source="backupSnapshots" row-key="snapshotId">
|
||||
<a-table-column title="快照" data-index="snapshotId" />
|
||||
<a-table-column title="创建时间" data-index="createdAt" />
|
||||
|
||||
@@ -4,6 +4,32 @@ export interface BossAdminMenuItem {
|
||||
children?: BossAdminMenuItem[];
|
||||
}
|
||||
|
||||
export interface BossAdminTaskSlaRow extends Record<string, unknown> {
|
||||
taskId: string;
|
||||
riskId: string;
|
||||
projectId: string;
|
||||
deviceId: string;
|
||||
taskType: string;
|
||||
status: string;
|
||||
phase: string;
|
||||
summary: string;
|
||||
slaLevel: "ok" | "watch" | "breached" | "recoverable" | "terminal";
|
||||
severity: "info" | "warning" | "critical";
|
||||
slaDueAt: string;
|
||||
lastProgressAt: string;
|
||||
attemptLabel: string;
|
||||
stale: boolean;
|
||||
recoverable: boolean;
|
||||
autoRecoverable: boolean;
|
||||
recommendedAction: string;
|
||||
}
|
||||
|
||||
export interface BossAdminTaskSlaPanel {
|
||||
generatedAt: string;
|
||||
summary: Record<string, number>;
|
||||
rows: BossAdminTaskSlaRow[];
|
||||
}
|
||||
|
||||
export interface BossAdminBackofficePayload {
|
||||
ok: boolean;
|
||||
surface: "platform" | "enterprise";
|
||||
@@ -27,6 +53,9 @@ export interface BossAdminBackofficePayload {
|
||||
skillUsageAudit: Array<Record<string, unknown>>;
|
||||
recoveryActions: string[];
|
||||
backupStatus: Record<string, unknown>;
|
||||
dataSafetySummary: Record<string, unknown>;
|
||||
taskRiskSummary: Record<string, unknown>;
|
||||
taskSlaPanel: BossAdminTaskSlaPanel;
|
||||
capabilitySummary: Record<string, number>;
|
||||
surface: "platform" | "enterprise";
|
||||
};
|
||||
@@ -87,6 +116,27 @@ export interface BossAdminBackupsPayload {
|
||||
snapshots: BossAdminBackupSnapshot[];
|
||||
}
|
||||
|
||||
export interface BossAdminSkillLifecycleRequest extends Record<string, unknown> {
|
||||
requestId: string;
|
||||
action: string;
|
||||
status: string;
|
||||
deviceId: string;
|
||||
skillId?: string;
|
||||
sourceUrl?: string;
|
||||
targetVersion?: string;
|
||||
rollbackToVersion?: string;
|
||||
lockedVersion?: string;
|
||||
requestedAt?: string;
|
||||
completedAt?: string;
|
||||
resultSummary?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface BossAdminSkillLifecycleRequestsPayload {
|
||||
ok: boolean;
|
||||
requests: BossAdminSkillLifecycleRequest[];
|
||||
}
|
||||
|
||||
async function requestJson<T>(url: string, init: RequestInit = {}): Promise<T> {
|
||||
const response = await fetch(url, {
|
||||
credentials: "include",
|
||||
@@ -133,6 +183,12 @@ export async function postSkillLifecycleRequest(payload: Record<string, unknown>
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchSkillLifecycleRequests(): Promise<BossAdminSkillLifecycleRequestsPayload> {
|
||||
return requestJson<BossAdminSkillLifecycleRequestsPayload>("/api/v1/admin/skills/requests", {
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
export async function postDeviceCodexRemoteControl(
|
||||
deviceId: string,
|
||||
payload: { action: "start" | "stop"; reason?: string },
|
||||
|
||||
@@ -272,7 +272,7 @@ body {
|
||||
|
||||
.boss-admin-action-strip {
|
||||
display: grid;
|
||||
grid-template-columns: 220px 320px 1fr;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@@ -390,10 +390,37 @@ body {
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 22px;
|
||||
box-shadow: 0 18px 50px rgba(16, 32, 24, 0.07);
|
||||
}
|
||||
|
||||
.boss-admin-wide-card {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.ant-card .ant-card-body {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ant-table-wrapper {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-table {
|
||||
min-width: 720px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-table-cell {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.ant-table-wrapper .ant-btn,
|
||||
.boss-admin-action-strip .ant-btn {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user