Files
boss/src/app/api/v1/admin/backoffice/route.ts
2026-06-08 12:22:50 +08:00

557 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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