feat: harden enterprise control plane
This commit is contained in:
@@ -4,27 +4,33 @@ 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";
|
||||
|
||||
const MENU_TREE = [
|
||||
{ key: "workbench", label: "工作台" },
|
||||
{ key: "tenant", label: "租户管理" },
|
||||
{ key: "user", label: "账号管理" },
|
||||
{ key: "role", label: "角色权限" },
|
||||
{
|
||||
key: "resource",
|
||||
label: "资源授权",
|
||||
children: [
|
||||
{ key: "resource.devices", label: "设备资源" },
|
||||
{ key: "resource.projects", label: "项目线程" },
|
||||
{ key: "resource.skills", label: "Skill 资源" },
|
||||
],
|
||||
},
|
||||
{ key: "skills", label: "Skill 中心" },
|
||||
{ key: "risk", label: "风险告警" },
|
||||
{ key: "audit", label: "审计日志" },
|
||||
{ key: "system", label: "系统设置" },
|
||||
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]));
|
||||
}
|
||||
@@ -34,6 +40,65 @@ function companyNameFor(state: BossState, companyId?: string) {
|
||||
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,
|
||||
@@ -153,13 +218,158 @@ function rolesContract() {
|
||||
};
|
||||
}
|
||||
|
||||
function buildBackofficePayload(state: BossState) {
|
||||
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 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: "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,
|
||||
},
|
||||
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,
|
||||
menuTree: MENU_TREE,
|
||||
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),
|
||||
@@ -210,10 +420,29 @@ export async function GET(request: NextRequest) {
|
||||
if (!session) {
|
||||
return jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
if (session.role !== "highest_admin") {
|
||||
return jsonNoStore({ ok: false, message: "FORBIDDEN" }, { status: 403 });
|
||||
}
|
||||
|
||||
const state = await readState();
|
||||
return jsonNoStore(buildBackofficePayload(state));
|
||||
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 }));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user