557 lines
23 KiB
TypeScript
557 lines
23 KiB
TypeScript
import { NextRequest } from "next/server";
|
||
import { jsonNoStore } from "@/lib/api-response";
|
||
import { requireRequestSession } from "@/lib/boss-auth";
|
||
import { BOSS_PERMISSION_TEMPLATES } from "@/lib/boss-access-templates";
|
||
import { buildAdminOverview } from "@/lib/boss-admin-overview";
|
||
import { readState, type BossState } from "@/lib/boss-data";
|
||
import { getBossStateBackupStatus, type BossStateBackupStatus } from "@/lib/boss-state-backups";
|
||
import { buildMasterAgentTaskSlaRows, type MasterAgentTaskSlaLevel } from "@/lib/master-agent-task-sla";
|
||
|
||
const PLATFORM_MENU_TREE = [
|
||
{ key: "platform-overview", label: "平台总览" },
|
||
{ key: "platform-provisioning", label: "企业开通" },
|
||
{ key: "platform-customer-plans", label: "客户与套餐" },
|
||
{ key: "platform-devices", label: "全局设备" },
|
||
{ key: "platform-risk", label: "全局风险" },
|
||
{ key: "platform-customer-success", label: "客户成功" },
|
||
{ key: "platform-audit", label: "系统审计" },
|
||
{ key: "platform-billing", label: "计费与授权" },
|
||
{ key: "platform-settings", label: "平台设置" },
|
||
] as const;
|
||
|
||
const ENTERPRISE_MENU_TREE = [
|
||
{ key: "enterprise-overview", label: "企业总览" },
|
||
{ key: "enterprise-members", label: "组织与成员" },
|
||
{ key: "enterprise-devices-agents", label: "设备与项目" },
|
||
{ key: "enterprise-agent-flows", label: "Agent 与流程" },
|
||
{ key: "enterprise-skill", label: "Skill 中心" },
|
||
{ key: "enterprise-risk-backup", label: "风险与审计" },
|
||
{ key: "enterprise-backup", label: "备份与回退" },
|
||
{ key: "enterprise-settings", label: "企业设置" },
|
||
] as const;
|
||
|
||
type BackofficeSurface = "platform" | "enterprise";
|
||
|
||
function companyNameMap(state: BossState) {
|
||
return new Map(state.adminCompanies.map((company) => [company.companyId, company.name]));
|
||
}
|
||
|
||
function companyNameFor(state: BossState, companyId?: string) {
|
||
if (!companyId || companyId === "default") return "默认公司";
|
||
return companyNameMap(state).get(companyId) ?? companyId;
|
||
}
|
||
|
||
function accountCompanyId(state: BossState, account?: string) {
|
||
if (!account) return undefined;
|
||
return state.authAccounts.find((item) => item.account === account)?.companyId ?? "default";
|
||
}
|
||
|
||
function resolvedDeviceCompanyId(state: BossState, device: { account?: string; companyId?: string }) {
|
||
return device.companyId ?? accountCompanyId(state, device.account) ?? "default";
|
||
}
|
||
|
||
function filteredStateForCompany(state: BossState, companyId: string): BossState {
|
||
const companyAccounts = new Set(
|
||
state.authAccounts.filter((account) => (account.companyId ?? "default") === companyId).map((account) => account.account),
|
||
);
|
||
const devices = state.devices.filter((device) => resolvedDeviceCompanyId(state, device) === companyId);
|
||
const deviceIds = new Set(devices.map((device) => device.id));
|
||
const projects = state.projects.filter(
|
||
(project) =>
|
||
project.deviceIds.some((deviceId) => deviceIds.has(deviceId)) ||
|
||
project.groupMembers.some((member) => deviceIds.has(member.deviceId)),
|
||
);
|
||
const projectIds = new Set(projects.map((project) => project.id));
|
||
|
||
return {
|
||
...state,
|
||
adminCompanies: state.adminCompanies.filter((company) => company.companyId === companyId),
|
||
authAccounts: state.authAccounts.filter((account) => companyAccounts.has(account.account)),
|
||
authSessions: state.authSessions.filter((session) => companyAccounts.has(session.account)),
|
||
devices,
|
||
projects,
|
||
deviceSkills: state.deviceSkills.filter((skill) => deviceIds.has(skill.deviceId)),
|
||
accountDeviceGrants: state.accountDeviceGrants.filter((grant) => companyAccounts.has(grant.account) && deviceIds.has(grant.deviceId)),
|
||
accountProjectGrants: state.accountProjectGrants.filter((grant) => companyAccounts.has(grant.account) && projectIds.has(grant.projectId)),
|
||
accountSkillGrants: state.accountSkillGrants.filter((grant) => companyAccounts.has(grant.account)),
|
||
opsFaults: state.opsFaults.filter(
|
||
(fault) => (fault.nodeId && deviceIds.has(fault.nodeId)) || (fault.projectId && projectIds.has(fault.projectId)),
|
||
),
|
||
threadContextAlerts: state.threadContextAlerts.filter((alert) => projectIds.has(alert.projectId)),
|
||
masterAgentTasks: state.masterAgentTasks.filter(
|
||
(task) => (task.deviceId && deviceIds.has(task.deviceId)) || (task.projectId && projectIds.has(task.projectId)),
|
||
),
|
||
permissionAuditLogs: state.permissionAuditLogs.filter(
|
||
(log) =>
|
||
companyAccounts.has(log.actorAccount) ||
|
||
Boolean(log.targetAccount && companyAccounts.has(log.targetAccount)) ||
|
||
Boolean(log.deviceId && deviceIds.has(log.deviceId)) ||
|
||
Boolean(log.projectId && projectIds.has(log.projectId)),
|
||
),
|
||
adminRiskTimeline: state.adminRiskTimeline.filter((event) => event.companyId === companyId),
|
||
adminNotifications: state.adminNotifications.filter((notification) => {
|
||
const companyScoped = notification as { companyId?: string; deviceId?: string; projectId?: string };
|
||
return (
|
||
companyScoped.companyId === companyId ||
|
||
Boolean(companyScoped.deviceId && deviceIds.has(companyScoped.deviceId)) ||
|
||
Boolean(companyScoped.projectId && projectIds.has(companyScoped.projectId))
|
||
);
|
||
}),
|
||
};
|
||
}
|
||
|
||
function safeUsers(state: BossState) {
|
||
return state.authAccounts.map((account) => ({
|
||
id: account.id,
|
||
account: account.account,
|
||
displayName: account.displayName,
|
||
role: account.role,
|
||
status: account.status ?? "active",
|
||
companyId: account.companyId ?? "default",
|
||
companyName: companyNameFor(state, account.companyId),
|
||
primaryDeviceId: account.primaryDeviceId,
|
||
codexNodeId: account.codexNodeId,
|
||
codexNodeLabel: account.codexNodeLabel,
|
||
lastLoginAt: account.lastLoginAt,
|
||
lastLoginMethod: account.lastLoginMethod,
|
||
mfaRequired: Boolean(account.mfaRequired),
|
||
createdAt: account.createdAt,
|
||
updatedAt: account.updatedAt,
|
||
}));
|
||
}
|
||
|
||
function skillResources(state: BossState) {
|
||
const byName = new Map<
|
||
string,
|
||
{
|
||
skillId: string;
|
||
name: string;
|
||
description: string;
|
||
category?: string;
|
||
invocation?: string;
|
||
sourceType: "device" | "catalog";
|
||
deviceCount: number;
|
||
devices: Array<{ deviceId: string; updatedAt: string }>;
|
||
updatedAt: string;
|
||
}
|
||
>();
|
||
|
||
for (const skill of state.deviceSkills) {
|
||
const existing = byName.get(skill.name);
|
||
if (existing) {
|
||
existing.deviceCount += existing.devices.some((device) => device.deviceId === skill.deviceId) ? 0 : 1;
|
||
existing.devices.push({ deviceId: skill.deviceId, updatedAt: skill.updatedAt });
|
||
if (skill.updatedAt.localeCompare(existing.updatedAt) > 0) {
|
||
existing.updatedAt = skill.updatedAt;
|
||
}
|
||
continue;
|
||
}
|
||
byName.set(skill.name, {
|
||
skillId: skill.skillId,
|
||
name: skill.name,
|
||
description: skill.description,
|
||
category: skill.category,
|
||
invocation: skill.invocation,
|
||
sourceType: "device",
|
||
deviceCount: 1,
|
||
devices: [{ deviceId: skill.deviceId, updatedAt: skill.updatedAt }],
|
||
updatedAt: skill.updatedAt,
|
||
});
|
||
}
|
||
|
||
for (const catalogItem of state.skillCatalog) {
|
||
if (byName.has(catalogItem.name)) continue;
|
||
byName.set(catalogItem.name, {
|
||
skillId: catalogItem.skillId,
|
||
name: catalogItem.name,
|
||
description: catalogItem.description,
|
||
category: catalogItem.category,
|
||
sourceType: "catalog",
|
||
deviceCount: 0,
|
||
devices: [],
|
||
updatedAt: catalogItem.updatedAt,
|
||
});
|
||
}
|
||
|
||
return [...byName.values()].sort(
|
||
(left, right) => right.deviceCount - left.deviceCount || left.name.localeCompare(right.name, "zh-CN"),
|
||
);
|
||
}
|
||
|
||
function projectResources(state: BossState) {
|
||
return state.projects.map((project) => ({
|
||
id: project.id,
|
||
name: project.name,
|
||
deviceIds: project.deviceIds,
|
||
deviceCount: project.deviceIds.length,
|
||
folderName: project.threadMeta.folderName,
|
||
threadId: project.threadMeta.threadId,
|
||
threadDisplayName: project.threadMeta.threadDisplayName,
|
||
isGroup: project.isGroup,
|
||
collaborationMode: project.collaborationMode,
|
||
unreadCount: project.unreadCount,
|
||
riskLevel: project.riskLevel,
|
||
updatedAt: project.updatedAt,
|
||
lastMessageAt: project.lastMessageAt,
|
||
}));
|
||
}
|
||
|
||
function rolesContract() {
|
||
return {
|
||
builtInRoles: [
|
||
{
|
||
role: "highest_admin",
|
||
label: "超级管理员",
|
||
description: "平台侧最高权限,可管理全部公司、账号、设备、项目、Skill、风险和审计。",
|
||
},
|
||
{
|
||
role: "admin",
|
||
label: "企业管理员",
|
||
description: "企业内管理员,按授权范围管理本公司资源。",
|
||
},
|
||
{
|
||
role: "member",
|
||
label: "成员账号",
|
||
description: "企业子账号,只能访问已分配的电脑、项目和 Skill。",
|
||
},
|
||
],
|
||
permissionTemplates: BOSS_PERMISSION_TEMPLATES,
|
||
};
|
||
}
|
||
|
||
function safePercent(value: number, total: number) {
|
||
if (total <= 0) return 0;
|
||
return Math.max(0, Math.min(100, Math.round((value / total) * 100)));
|
||
}
|
||
|
||
function healthScore(company: { accountCount: number; deviceCount: number; onlineDeviceCount: number; openRiskCount: number }) {
|
||
const onlineScore = company.deviceCount > 0 ? safePercent(company.onlineDeviceCount, company.deviceCount) : 80;
|
||
const riskPenalty = Math.min(45, company.openRiskCount * 12);
|
||
const activityBonus = Math.min(10, company.accountCount);
|
||
return Math.max(0, Math.min(100, onlineScore - riskPenalty + activityBonus));
|
||
}
|
||
|
||
function riskAggregateValue(risks: Array<{ kind: string; title: string }>, matcher: (risk: { kind: string; title: string }) => boolean) {
|
||
return risks.filter(matcher).length;
|
||
}
|
||
|
||
function minutesSince(value?: string): number | null {
|
||
if (!value) return null;
|
||
const timestamp = Date.parse(value);
|
||
if (!Number.isFinite(timestamp)) return null;
|
||
return Math.max(0, Math.floor((Date.now() - timestamp) / 60_000));
|
||
}
|
||
|
||
function buildDataSafetySummary(backupStatus: BossStateBackupStatus) {
|
||
const ageMinutes = minutesSince(backupStatus.lastBackupAt);
|
||
const healthLabel =
|
||
backupStatus.status === "error"
|
||
? "备份异常"
|
||
: backupStatus.status === "empty" || !backupStatus.lastBackupAt
|
||
? "暂无备份"
|
||
: ageMinutes !== null && ageMinutes > 24 * 60
|
||
? "备份过期"
|
||
: "备份正常";
|
||
const nextAction =
|
||
healthLabel === "备份异常"
|
||
? "检查备份目录与状态文件写入权限"
|
||
: healthLabel === "暂无备份"
|
||
? "立即创建首个状态快照"
|
||
: healthLabel === "备份过期"
|
||
? "补创建状态快照并检查自动备份任务"
|
||
: "保持自动快照并定期演练恢复";
|
||
|
||
return {
|
||
mode: backupStatus.mode,
|
||
status: backupStatus.status,
|
||
restorePointCount: backupStatus.restorePointCount,
|
||
lastBackupAt: backupStatus.lastBackupAt ?? "",
|
||
ageMinutes,
|
||
healthLabel,
|
||
rpoLabel: "文件 MVP:以最近成功快照为准",
|
||
rtoLabel: "文件 MVP:人工恢复目标 30-60 分钟",
|
||
nextAction,
|
||
};
|
||
}
|
||
|
||
function canRecoverActiveTask(task: BossState["masterAgentTasks"][number]) {
|
||
if (task.recoverable) return true;
|
||
if (task.phase === "recoverable_failed") return true;
|
||
if (task.phase === "turn_started" || task.phase === "awaiting_reply" || task.phase === "completing") {
|
||
return false;
|
||
}
|
||
const maxAttempts = task.maxAttempts ?? 1;
|
||
return (task.attemptCount ?? 0) < maxAttempts;
|
||
}
|
||
|
||
function buildTaskRiskSummary(state: BossState) {
|
||
const activeStatuses = new Set(["queued", "running", "needs_user_action"]);
|
||
const activeTasks = state.masterAgentTasks.filter((task) => activeStatuses.has(task.status));
|
||
const rows = buildMasterAgentTaskSlaRows(state)
|
||
.filter((row) => activeStatuses.has(row.status))
|
||
.map((row) => ({
|
||
taskId: row.taskId,
|
||
projectId: row.projectId,
|
||
deviceId: row.deviceId,
|
||
status: row.status,
|
||
phase: row.phase,
|
||
stale: row.stale,
|
||
recoverable: row.recoverable || canRecoverActiveTask(state.masterAgentTasks.find((task) => task.taskId === row.taskId)!),
|
||
lastProgressAt: row.lastProgressAt,
|
||
summary: row.summary,
|
||
slaLevel: row.slaLevel,
|
||
slaDueAt: row.slaDueAt,
|
||
recommendedAction: row.recommendedAction,
|
||
}))
|
||
.slice(0, 20);
|
||
|
||
return {
|
||
counts: {
|
||
active: activeTasks.length,
|
||
stale: rows.filter((row) => row.stale).length,
|
||
recoverable: activeTasks.filter(canRecoverActiveTask).length,
|
||
needsUserAction: activeTasks.filter((task) => task.status === "needs_user_action" || task.phase === "needs_user_action").length,
|
||
},
|
||
rows,
|
||
};
|
||
}
|
||
|
||
function buildTaskSlaPanel(state: BossState) {
|
||
const rows = buildMasterAgentTaskSlaRows(state).slice(0, 50);
|
||
const countByLevel = (level: MasterAgentTaskSlaLevel) => rows.filter((row) => row.slaLevel === level).length;
|
||
return {
|
||
generatedAt: new Date().toISOString(),
|
||
summary: {
|
||
total: rows.length,
|
||
active: rows.filter((row) => row.status === "queued" || row.status === "running" || row.status === "needs_user_action").length,
|
||
ok: countByLevel("ok"),
|
||
watch: countByLevel("watch"),
|
||
breached: countByLevel("breached"),
|
||
recoverable: countByLevel("recoverable"),
|
||
terminal: countByLevel("terminal"),
|
||
autoRecoverable: rows.filter((row) => row.autoRecoverable).length,
|
||
},
|
||
rows,
|
||
};
|
||
}
|
||
|
||
function buildBackofficeInsights(state: BossState, options: { surface: BackofficeSurface; backupStatus: BossStateBackupStatus }) {
|
||
const overview = buildAdminOverview(state);
|
||
const devices = state.devices;
|
||
const onlineDevices = devices.filter((device) => device.status === "online").length;
|
||
const guiReady = devices.filter((device) => Boolean(device.capabilities?.gui.connected)).length;
|
||
const cliReady = devices.filter((device) => Boolean(device.capabilities?.cli.connected)).length;
|
||
const computerUseReady = devices.filter((device) => Boolean(device.capabilities?.computerUse.connected)).length;
|
||
const browserReady = devices.filter((device) => Boolean(device.capabilities?.browserAutomation.connected)).length;
|
||
const codexHealthy = devices.length === 0 || (guiReady + cliReady) >= devices.length;
|
||
const computerUseHealthy = devices.length === 0 || computerUseReady >= Math.ceil(devices.length / 2);
|
||
|
||
return {
|
||
onboardingSteps: ["企业信息", "老板账号", "套餐授权", "设备与交付"],
|
||
serviceStatuses: [
|
||
{ label: "Boss API", value: "正常", tone: "green" },
|
||
{ label: "OTA", value: "正常", tone: "green" },
|
||
{ label: "Codex Provider", value: codexHealthy ? "正常" : "降级", tone: codexHealthy ? "green" : "orange" },
|
||
{ label: "Computer Use", value: computerUseHealthy ? "正常" : "降级", tone: computerUseHealthy ? "green" : "orange" },
|
||
{ label: "Skill Hub", value: state.deviceSkills.length + state.skillCatalog.length > 0 ? "正常" : "待配置", tone: "green" },
|
||
],
|
||
openingPreview: [
|
||
{ label: "默认套餐", value: "企业专业版" },
|
||
{ label: "推荐设备", value: `${Math.max(1, devices.length || 1)} 台起` },
|
||
{ label: "Codex Provider", value: "App Server 优先,CLI 兜底" },
|
||
{ label: "Computer Use", value: "macOS 桌面控制" },
|
||
],
|
||
deliveryChecklist: [
|
||
{ label: "API 可用", done: true },
|
||
{ label: "OTA 可用", done: true },
|
||
{ label: "boss-agent 安装包已生成", done: true },
|
||
{ label: "初始密码策略已设置", done: true },
|
||
{ label: "客户成功负责人已分配", done: overview.companies.some((company) => Boolean(company.successOwnerAccount)) },
|
||
],
|
||
recentCompanies: overview.companies.slice(0, 5).map((company) => ({
|
||
companyId: company.companyId,
|
||
label: company.name,
|
||
note: `${company.planTier ?? "enterprise"} · ${company.deviceCount} 台设备 · ${company.openRiskCount} 个风险`,
|
||
})),
|
||
customerHealthRows: overview.companies.map((company) => ({
|
||
companyId: company.companyId,
|
||
name: company.name,
|
||
healthScore: healthScore(company),
|
||
planTier: company.planTier ?? "enterprise",
|
||
onlineDevices: `${company.onlineDeviceCount}/${company.deviceCount}`,
|
||
openRiskCount: company.openRiskCount,
|
||
ownerAccount: company.successOwnerAccount || company.ownerAccount || "未指派",
|
||
})),
|
||
riskAggregates: [
|
||
{
|
||
label: "设备离线",
|
||
value: riskAggregateValue(overview.risks, (risk) => risk.kind === "device_offline"),
|
||
},
|
||
{
|
||
label: "主 Agent 执行失败",
|
||
value: riskAggregateValue(overview.risks, (risk) => risk.kind === "master_agent_task_failed"),
|
||
},
|
||
{
|
||
label: "任务 SLA 告警",
|
||
value: riskAggregateValue(overview.risks, (risk) => risk.kind === "master_agent_task_sla"),
|
||
},
|
||
{
|
||
label: "Computer Use 权限缺失",
|
||
value: riskAggregateValue(overview.risks, (risk) => /Computer Use|权限/.test(risk.title)),
|
||
},
|
||
{
|
||
label: "Skill 升级失败",
|
||
value: riskAggregateValue(overview.risks, (risk) => /Skill/.test(risk.title)),
|
||
},
|
||
{
|
||
label: "备份异常",
|
||
value: riskAggregateValue(overview.risks, (risk) => /备份|backup/i.test(risk.title)),
|
||
},
|
||
],
|
||
customerFollowups: overview.riskTimeline.slice(0, 5).map((event) => ({
|
||
eventId: event.eventId,
|
||
label: event.note || event.action,
|
||
meta: `${event.actorAccount} · ${event.createdAt}`,
|
||
})),
|
||
enterpriseGoals: state.projects.slice(0, 3).map((project) => ({
|
||
projectId: project.id,
|
||
label: project.name,
|
||
progress: Math.max(30, Math.min(96, 82 - (project.unreadCount ?? 0) * 3 - (project.riskLevel === "high" ? 18 : 0))),
|
||
})),
|
||
organizationUnits: ["销售部", "客服部", "研发部", "财务部", "行政部"],
|
||
departmentProgress: [
|
||
{ label: "销售部", note: "线索跟进正常", tone: "green" },
|
||
{ label: "客服部", note: `${overview.summary.openNotifications} 个通知待处理`, tone: overview.summary.openNotifications > 0 ? "orange" : "green" },
|
||
{ label: "研发部", note: `${state.projects.length} 个项目运行中`, tone: "green" },
|
||
{ label: "财务部", note: "账单与授权正常", tone: "green" },
|
||
],
|
||
masterAgentSummary: [
|
||
`当前企业有 ${state.projects.length} 个项目、${onlineDevices}/${devices.length} 台电脑在线。`,
|
||
overview.summary.openRisks > 0 ? `建议优先处理 ${overview.summary.openRisks} 个开放风险。` : "当前没有开放风险。",
|
||
],
|
||
permissionHighlights: ["device.view", "thread.chat", "master_agent.takeover", "computer.control", "skill.use"],
|
||
agentFlowSteps: ["主 Agent", "项目 Agent", "本地 Agent", "Codex / Computer Use / Skill"],
|
||
skillUsageAudit: state.permissionAuditLogs.slice(0, 5).map((log) => ({
|
||
auditId: log.auditId,
|
||
label: log.detail || log.action,
|
||
meta: `${log.actorAccount} · ${log.createdAt}`,
|
||
})),
|
||
recoveryActions: ["消息恢复", "项目目标恢复", "权限撤销", "Skill 回滚", "Codex checkpoint"],
|
||
backupStatus: {
|
||
lastBackupAt: options.backupStatus.lastBackupAt ?? "",
|
||
status:
|
||
options.backupStatus.status === "ready"
|
||
? "校验通过"
|
||
: options.backupStatus.status === "empty"
|
||
? "暂无快照"
|
||
: "备份异常",
|
||
restorePointCount: options.backupStatus.restorePointCount,
|
||
backupDir: options.backupStatus.backupDir,
|
||
detail: options.backupStatus.detail,
|
||
},
|
||
dataSafetySummary: buildDataSafetySummary(options.backupStatus),
|
||
taskRiskSummary: buildTaskRiskSummary(state),
|
||
taskSlaPanel: buildTaskSlaPanel(state),
|
||
capabilitySummary: {
|
||
guiReady,
|
||
cliReady,
|
||
computerUseReady,
|
||
browserReady,
|
||
},
|
||
surface: options.surface,
|
||
};
|
||
}
|
||
|
||
function buildBackofficePayload(
|
||
state: BossState,
|
||
options: { surface: BackofficeSurface; currentCompanyId?: string; backupStatus: BossStateBackupStatus },
|
||
) {
|
||
const overview = buildAdminOverview(state);
|
||
const skills = skillResources(state);
|
||
const currentCompany = options.currentCompanyId
|
||
? state.adminCompanies.find((company) => company.companyId === options.currentCompanyId) ?? null
|
||
: null;
|
||
|
||
return {
|
||
ok: true,
|
||
surface: options.surface,
|
||
currentCompany,
|
||
menuTree: options.surface === "platform" ? PLATFORM_MENU_TREE : ENTERPRISE_MENU_TREE,
|
||
insights: buildBackofficeInsights(state, { surface: options.surface, backupStatus: options.backupStatus }),
|
||
workbench: {
|
||
summary: overview.summary,
|
||
companies: overview.companies.slice(0, 10),
|
||
devices: overview.devices.slice(0, 20),
|
||
risks: overview.risks.slice(0, 20),
|
||
notifications: overview.notifications,
|
||
grantsSummary: overview.grantsSummary,
|
||
},
|
||
tenants: overview.companies.map((company) => ({
|
||
...company,
|
||
lifecycleStatus: company.status ?? "active",
|
||
})),
|
||
users: safeUsers(state),
|
||
roles: rolesContract(),
|
||
resourceGroups: {
|
||
devices: overview.devices,
|
||
projects: projectResources(state),
|
||
skills,
|
||
grants: {
|
||
devices: state.accountDeviceGrants,
|
||
projects: state.accountProjectGrants,
|
||
skills: state.accountSkillGrants,
|
||
},
|
||
},
|
||
audit: {
|
||
risks: overview.risks,
|
||
notifications: overview.notifications,
|
||
riskTimeline: overview.riskTimeline,
|
||
permissionLogs: state.permissionAuditLogs
|
||
.slice()
|
||
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))
|
||
.slice(0, 100),
|
||
},
|
||
yudaoMapping: {
|
||
tenant: "adminCompanies",
|
||
user: "authAccounts",
|
||
role: "BOSS_PERMISSION_TEMPLATES",
|
||
menu: "menuTree",
|
||
operateLog: "permissionAuditLogs",
|
||
resource: "devices/projects/deviceSkills",
|
||
risk: "opsFaults/threadContextAlerts/masterAgentTasks/adminNotifications",
|
||
},
|
||
};
|
||
}
|
||
|
||
export async function GET(request: NextRequest) {
|
||
const session = await requireRequestSession(request);
|
||
if (!session) {
|
||
return jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||
}
|
||
|
||
const state = await readState();
|
||
const backupStatus = await getBossStateBackupStatus();
|
||
const url = new URL(request.url);
|
||
const scope = url.searchParams.get("scope") === "enterprise" ? "enterprise" : "platform";
|
||
|
||
if (scope === "platform") {
|
||
if (session.role !== "highest_admin") {
|
||
return jsonNoStore({ ok: false, message: "FORBIDDEN" }, { status: 403 });
|
||
}
|
||
return jsonNoStore(buildBackofficePayload(state, { surface: "platform", backupStatus }));
|
||
}
|
||
|
||
if (session.role !== "admin" && session.role !== "highest_admin") {
|
||
return jsonNoStore({ ok: false, message: "FORBIDDEN" }, { status: 403 });
|
||
}
|
||
const requestedCompanyId = url.searchParams.get("companyId")?.trim();
|
||
const companyId = session.role === "highest_admin"
|
||
? requestedCompanyId || state.adminCompanies[0]?.companyId || "default"
|
||
: accountCompanyId(state, session.account);
|
||
if (!companyId) {
|
||
return jsonNoStore({ ok: false, message: "COMPANY_NOT_FOUND" }, { status: 404 });
|
||
}
|
||
const scopedState = filteredStateForCompany(state, companyId);
|
||
return jsonNoStore(buildBackofficePayload(scopedState, { surface: "enterprise", currentCompanyId: companyId, backupStatus }));
|
||
}
|