feat: harden enterprise control plane

This commit is contained in:
AI Bot
2026-05-17 02:20:08 +08:00
parent 67511c31f4
commit e1aed590f8
112 changed files with 10977 additions and 2004 deletions

View File

@@ -12,6 +12,7 @@ import {
readState,
reclaimAuthAccountByAdmin,
resetAuthAccountPasswordByAdmin,
revokeDeviceByAdmin,
revokeAccessGrant,
saveAccountDeviceGrant,
saveAccountProjectGrant,
@@ -64,6 +65,9 @@ function publicAdminDevice(device: Device) {
lastSeenAt: device.lastSeenAt,
preferredExecutionMode: device.preferredExecutionMode,
capabilities: device.capabilities,
revokedAt: device.revokedAt,
revokedBy: device.revokedBy,
revokeReason: device.revokeReason,
};
}
@@ -383,6 +387,25 @@ export async function POST(request: NextRequest) {
}
}
if (action === "revoke_device") {
const deviceId = stringValue(body.deviceId);
if (!deviceId) {
return jsonNoStore({ ok: false, message: "DEVICE_ID_REQUIRED" }, { status: 400 });
}
try {
const device = await revokeDeviceByAdmin({
deviceId,
reason: stringValue(body.reason) || undefined,
actorAccount: auth.session.account,
auditMeta,
});
return jsonNoStore({ ok: true, device: publicAdminDevice(device) });
} catch (error) {
const message = error instanceof Error ? error.message : "UNKNOWN_ERROR";
return jsonNoStore({ ok: false, message }, { status: message === "DEVICE_NOT_FOUND" ? 404 : 400 });
}
}
if (action === "bulk_import_accounts") {
const companyId = stringValue(body.companyId);
const accounts = accountImportValues(body.accounts);

View File

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

View File

@@ -0,0 +1,79 @@
import { NextRequest } from "next/server";
import { jsonNoStore } from "@/lib/api-response";
import { requireRequestSession } from "@/lib/boss-auth";
import { requireCsrfSafeMutation } from "@/lib/boss-csrf";
import {
createBossStateBackup,
getBossStateBackupStatus,
listBossStateBackups,
restoreBossStateBackup,
} from "@/lib/boss-state-backups";
function forbidden() {
return jsonNoStore({ ok: false, message: "FORBIDDEN" }, { status: 403 });
}
function stringValue(value: unknown) {
return typeof value === "string" ? value.trim() : "";
}
async function requireHighestAdmin(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
return { response: jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 }) };
}
if (session.role !== "highest_admin") {
return { response: forbidden() };
}
return { session };
}
export async function GET(request: NextRequest) {
const auth = await requireHighestAdmin(request);
if (auth.response) return auth.response;
const status = await getBossStateBackupStatus();
const snapshots = await listBossStateBackups(50);
return jsonNoStore({ ok: true, status, snapshots });
}
export async function POST(request: NextRequest) {
const csrf = requireCsrfSafeMutation(request);
if (csrf) return csrf;
const auth = await requireHighestAdmin(request);
if (auth.response) return auth.response;
const body = (await request.json().catch(() => ({}))) as Record<string, unknown>;
const action = stringValue(body.action);
try {
if (action === "create_snapshot") {
const snapshot = await createBossStateBackup({
actorAccount: auth.session.account,
reason: stringValue(body.reason) || "manual",
});
const status = await getBossStateBackupStatus();
return jsonNoStore({ ok: true, action, snapshot, status });
}
if (action === "restore_snapshot") {
const snapshotId = stringValue(body.snapshotId);
if (!snapshotId) {
return jsonNoStore({ ok: false, message: "BACKUP_SNAPSHOT_ID_REQUIRED" }, { status: 400 });
}
const result = await restoreBossStateBackup({
snapshotId,
actorAccount: auth.session.account,
});
const status = await getBossStateBackupStatus();
return jsonNoStore({ ok: true, action, ...result, status });
}
} catch (error) {
const message = error instanceof Error ? error.message : "BACKUP_ACTION_FAILED";
const status = message === "BACKUP_SNAPSHOT_NOT_FOUND" || message === "ENOENT" ? 404 : 400;
return jsonNoStore({ ok: false, message }, { status });
}
return jsonNoStore({ ok: false, message: "BACKUP_ACTION_INVALID" }, { status: 400 });
}