chore: checkpoint Boss app v2.5.11

This commit is contained in:
AI Bot
2026-06-08 12:22:50 +08:00
parent bddbe8b5ba
commit 3b51641d99
78 changed files with 5706 additions and 954 deletions

View File

@@ -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" />

View File

@@ -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 },

View File

@@ -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;
}