Files
boss/local-agent/boss-agent-status.mjs
2026-05-12 23:39:13 +08:00

905 lines
35 KiB
JavaScript
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 { spawn } from "node:child_process";
import { rm, stat } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
const PERMISSION_DEFS = [
{
key: "accessibility",
label: "辅助功能",
description: "用于点击、输入和读取可访问控件",
tier: "core",
},
{
key: "screenRecording",
label: "屏幕录制",
description: "用于识别桌面画面和系统弹窗",
tier: "core",
},
{
key: "automation",
label: "自动化控制",
description: "用于控制 Finder、浏览器和企业软件",
tier: "core",
},
];
const EXTENDED_PERMISSION_DEFS = [
{
key: "fullDiskAccess",
label: "全磁盘访问",
description: "用于读取和写入企业授权目录、日志与开发资产",
tier: "extended",
},
{
key: "inputMonitoring",
label: "输入监控",
description: "用于低层热键、复杂输入和部分不可访问控件兜底",
tier: "extended",
},
{
key: "notifications",
label: "通知权限",
description: "用于后台任务、接管结果和风险告警提醒",
tier: "extended",
},
{
key: "microphone",
label: "麦克风",
description: "用于语音指令、会议和音频协作场景",
tier: "extended",
},
{
key: "camera",
label: "摄像头",
description: "用于视觉协作、会议和现场画面确认",
tier: "extended",
},
{
key: "localNetwork",
label: "本地网络",
description: "用于发现和连接局域网设备、开发板与企业内网服务",
tier: "extended",
},
];
const MACOS_PERMISSION_SETTINGS = {
all: "x-apple.systempreferences:com.apple.preference.security?Privacy",
accessibility: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility",
screenRecording: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture",
automation: "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation",
fullDiskAccess: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles",
inputMonitoring: "x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent",
notifications: "x-apple.systempreferences:com.apple.Notifications-Settings.extension",
microphone: "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone",
camera: "x-apple.systempreferences:com.apple.preference.security?Privacy_Camera",
localNetwork: "x-apple.systempreferences:com.apple.preference.security?Privacy_LocalNetwork",
};
const AUTO_PREFLIGHT_PERMISSION_KEYS = new Set(["accessibility", "screenRecording", "automation"]);
function nonEmpty(value) {
const text = String(value ?? "").trim();
return text || undefined;
}
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function maskValue(value) {
const text = nonEmpty(value);
if (!text) return "";
if (text.length <= 8) return "••••";
return `${text.slice(0, 4)}••••${text.slice(-4)}`;
}
function normalizePermissionStatus(value) {
return value === "granted" || value === "missing" || value === "unknown" ? value : "unknown";
}
function statusTone(status) {
if (status === "granted" || status === "valid" || status === "connected" || status === "bound") {
return "good";
}
if (status === "missing" || status === "expired" || status === "disconnected") {
return "bad";
}
return "warn";
}
function formatDate(value) {
const text = nonEmpty(value);
if (!text) return "绑定后显示";
const date = new Date(text);
if (Number.isNaN(date.getTime())) return text;
return new Intl.DateTimeFormat("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(date);
}
function buildBindingPayload(config) {
const controlPlaneUrl = nonEmpty(config.controlPlaneUrl) ?? "https://boss.hyzq.net";
const params = new URLSearchParams({
server: controlPlaneUrl,
deviceId: nonEmpty(config.deviceId) ?? "local-device",
name: nonEmpty(config.name) ?? "本机电脑",
});
const pairingCode = nonEmpty(config.pairingCode);
if (pairingCode) params.set("pairingCode", pairingCode);
return `boss://agent-bind?${params.toString()}`;
}
function resolveApiUsage(config) {
return {
primary:
nonEmpty(config.primaryApiLabel) ??
nonEmpty(config.apiUsage?.primary) ??
nonEmpty(config.masterAgentModel) ??
"由 Boss 后台统一配置",
backup:
nonEmpty(config.backupApiLabel) ??
nonEmpty(config.apiUsage?.backup) ??
"未启用",
status:
nonEmpty(config.apiUsage?.status) ??
(config.masterAgentEnabled === false ? "未启用主 Agent" : "可用"),
};
}
function resolveLicense(config, bound) {
const license = config.license && typeof config.license === "object" ? config.license : {};
if (!bound) {
return {
status: "pending_binding",
label: "未授权 · 绑定后校验",
enterpriseName: "绑定后显示",
expiresAt: nonEmpty(config.licenseExpiresAt) ?? "",
expiresAtLabel: "绑定后显示",
scope: "桌面控制 / 浏览器控制 / Skill 同步",
};
}
const status = nonEmpty(license.status) ?? "valid";
const expiresAt = nonEmpty(license.expiresAt) ?? nonEmpty(config.licenseExpiresAt) ?? "";
return {
status,
label: status === "valid" ? "正版授权正常" : status === "expired" ? "授权已过期" : "授权状态待确认",
enterpriseName: nonEmpty(license.enterpriseName) ?? nonEmpty(config.enterpriseName) ?? "默认公司",
expiresAt,
expiresAtLabel: formatDate(expiresAt),
scope: nonEmpty(license.scope) ?? "桌面控制 / 浏览器控制 / Skill 同步",
};
}
function permissionItems(defs, permissions) {
return defs.map((item) => ({
...item,
status: normalizePermissionStatus(permissions[item.key]),
}));
}
function resolvePermissionReadiness(coreItems, extendedItems) {
const coreGrantedCount = coreItems.filter((item) => item.status === "granted").length;
const extendedGrantedCount = extendedItems.filter((item) => item.status === "granted").length;
const coreReady = coreGrantedCount === coreItems.length;
const fullControlReady = coreReady && extendedGrantedCount === extendedItems.length;
const summary = fullControlReady
? "完整接管权限已具备"
: coreReady
? "核心桌面控制已具备,完整接管待补齐"
: "核心桌面控制待授权,完整接管不可用";
return {
coreReady,
fullControlReady,
coreGrantedCount,
coreTotal: coreItems.length,
extendedGrantedCount,
extendedTotal: extendedItems.length,
summary,
detail:
"辅助功能、屏幕录制、自动化控制是桌面点击输入与画面识别的核心权限;完整接管还需要按业务场景补齐全磁盘访问、输入监控、通知、麦克风、摄像头和本地网络。",
};
}
function buildPermissionSetupPlan(coreItems, extendedItems, readiness) {
const actions = [...coreItems, ...extendedItems].map((item) => ({
key: item.key,
label: item.label,
description: item.description,
tier: item.tier,
status: item.status,
requiredForSilentControl: true,
canPreflight: AUTO_PREFLIGHT_PERMISSION_KEYS.has(item.key),
settingsUrl: MACOS_PERMISSION_SETTINGS[item.key] ?? MACOS_PERMISSION_SETTINGS.all,
openUrl: `/api/v1/boss-agent/permissions/open?target=${encodeURIComponent(item.key)}`,
owner: "boss-agent.app",
}));
const missingActions = actions.filter((action) => action.status !== "granted");
return {
mode: "one_time_setup",
title: "一次完整授权",
goal: "首次把完整接管需要的权限集中配置好,后续控制过程中只做状态校验和静默使用。",
silentUseReady: missingActions.length === 0,
primaryAction: {
label: "打开完整授权向导",
href: "/api/v1/boss-agent/permissions/open?target=all",
settingsUrl: MACOS_PERMISSION_SETTINGS.all,
},
actions,
missingKeys: missingActions.map((action) => action.key),
summary: readiness.fullControlReady
? "完整授权已满足,后续可静默执行。"
: "仍有权限未确认,请在首次配置阶段一次性补齐,避免后续任务执行中断。",
persistenceNote:
"macOS 会把授权持久写入系统隐私数据库;除非用户撤销授权、重装应用或更换运行时签名,否则后续控制不需要重复申请。",
};
}
function resolveSkills(runtime) {
const rawSkills = Array.isArray(runtime.lastSkills) ? runtime.lastSkills : [];
const items = rawSkills
.map((item) => ({
name: nonEmpty(item?.name),
category: nonEmpty(item?.category) ?? "本机",
path: nonEmpty(item?.path) ?? "",
description: nonEmpty(item?.description) ?? "",
status: nonEmpty(item?.status) ?? "deployed",
}))
.filter((item) => item.name);
const syncOk = runtime.lastSkillSyncOk === true;
return {
total: items.length,
items: items.slice(0, 8),
syncOk,
syncStatus: syncOk ? "已同步" : items.length > 0 ? "待确认" : "未同步",
syncAt: nonEmpty(runtime.lastSkillSyncAt) ?? "",
summary: items.length > 0 ? `已部署 ${items.length} 个 Skill` : "本机暂无已同步 Skill",
};
}
export function buildBossAgentStatus(config = {}, runtime = {}, options = {}) {
const now = nonEmpty(options.now) ?? new Date().toISOString();
const token = nonEmpty(runtime.issuedToken) ?? nonEmpty(config.token);
const account = nonEmpty(config.account);
const bound = Boolean(token && account);
const permissions = options.permissions ?? {};
const serverOk = runtime.lastHeartbeatOk === true;
const qrPayload = bound ? "" : buildBindingPayload(config);
const corePermissionItems = permissionItems(PERMISSION_DEFS, permissions);
const extendedPermissionItems = permissionItems(EXTENDED_PERMISSION_DEFS, permissions);
const permissionReadiness = resolvePermissionReadiness(corePermissionItems, extendedPermissionItems);
const permissionSetup = buildPermissionSetupPlan(corePermissionItems, extendedPermissionItems, permissionReadiness);
return {
appName: "boss-agent",
generatedAt: now,
device: {
id: nonEmpty(config.deviceId) ?? "local-device",
name: nonEmpty(config.name) ?? os.hostname() ?? "本机电脑",
avatar: nonEmpty(config.avatar) ?? "B",
role: nonEmpty(config.deviceRole) ?? "企业被控节点",
endpoint: nonEmpty(config.endpoint) ?? "mac://local",
},
binding: {
bound,
status: bound ? "bound" : "unbound",
account: bound ? account : "未绑定",
tokenMasked: bound ? maskValue(token) : "",
pairingCode: bound ? "" : nonEmpty(config.pairingCode) ?? "",
qrPayload,
qrExpiresInLabel: bound ? "" : nonEmpty(config.qrExpiresInLabel) ?? "二维码 04:58 后失效",
},
server: {
ok: serverOk,
status: serverOk ? "connected" : "disconnected",
endpoint: nonEmpty(config.controlPlaneUrl) ?? "未配置服务器",
latencyLabel: nonEmpty(config.serverLatencyLabel) ?? (serverOk ? "延迟 28ms" : "连接异常"),
lastHeartbeatAt: nonEmpty(runtime.lastHeartbeatAt) ?? "",
lastHeartbeatStatus: runtime.lastHeartbeatStatus ?? null,
},
api: resolveApiUsage(config),
license: resolveLicense(config, bound),
permissions: {
summary: permissionReadiness.summary,
items: corePermissionItems,
extendedItems: extendedPermissionItems,
},
permissionReadiness,
permissionSetup,
skills: resolveSkills(runtime),
};
}
function renderPseudoQrSvg(payload) {
const size = 25;
let seed = 0;
for (const char of String(payload || "boss-agent")) {
seed = (seed * 31 + char.charCodeAt(0)) >>> 0;
}
const cells = [];
const finder = (x, y) => x < 7 && y < 7 || x > 17 && y < 7 || x < 7 && y > 17;
for (let y = 0; y < size; y += 1) {
for (let x = 0; x < size; x += 1) {
const finderCell = finder(x, y);
const edge = x === 0 || y === 0 || x === size - 1 || y === size - 1;
const value = finderCell
? !edge && (x % 6 === 1 || y % 6 === 1 || (x % 6 >= 2 && x % 6 <= 4 && y % 6 >= 2 && y % 6 <= 4))
: ((seed + x * 17 + y * 29 + x * y * 7) % 5) < 2;
if (value) {
cells.push(`<rect x="${x}" y="${y}" width="1" height="1" rx=".08"></rect>`);
}
}
}
return `<svg class="qr-svg" viewBox="0 0 ${size} ${size}" role="img" aria-label="Boss APP 绑定二维码">${cells.join("")}</svg>`;
}
function permissionText(status) {
if (status === "granted") return "已授权";
if (status === "missing") return "未授权";
return "待确认";
}
function setupActionRows(status) {
return status.permissionSetup.actions
.map((action) => {
const tone = statusTone(action.status);
const preflight = action.canPreflight ? "可预触发" : "需手动开启";
return `<div class="setup-action">
<div>
<div class="permission-name">${escapeHtml(action.label)}</div>
<div class="muted">${escapeHtml(action.description)} · ${escapeHtml(preflight)}</div>
</div>
<div class="setup-action-right">
<span class="pill ${tone}">${escapeHtml(permissionText(action.status))}</span>
<a class="text-link" href="${escapeHtml(action.openUrl)}">打开设置</a>
</div>
</div>`;
})
.join("");
}
function skillRows(status) {
const skills = status.skills?.items ?? [];
if (skills.length === 0) {
return `<div class="empty-state">本机暂无已同步 Skill。绑定账号后可由 Boss 后台下发或同步本机 Codex Skill。</div>`;
}
return skills
.map((skill) => {
const pathLabel = skill.path ? skill.path.replace(os.homedir(), "~") : "未记录路径";
return `<div class="skill-row">
<div>
<div class="skill-name">${escapeHtml(skill.name)}</div>
<div class="muted">${escapeHtml(skill.category)} · ${escapeHtml(pathLabel)}</div>
</div>
<span class="pill good">已部署</span>
</div>`;
})
.join("");
}
const BOSS_AGENT_TABS = new Set(["overview", "permissions", "skills", "license", "logs"]);
export function normalizeBossAgentTab(value = "overview") {
const tab = nonEmpty(value) ?? "overview";
return BOSS_AGENT_TABS.has(tab) ? tab : "overview";
}
export function renderBossAgentHtml(status, options = {}) {
return renderBossAgentHtmlBase(status, options);
}
export async function renderBossAgentHtmlWithQr(status, options = {}) {
let qrImageDataUrl = "";
if (!status.binding.bound && status.binding.qrPayload) {
try {
const qrModule = await import("qrcode");
const toDataURL = qrModule.toDataURL ?? qrModule.default?.toDataURL;
if (typeof toDataURL === "function") {
qrImageDataUrl = await toDataURL(status.binding.qrPayload, {
errorCorrectionLevel: "M",
margin: 1,
width: 288,
color: {
dark: "#18211a",
light: "#ffffff",
},
});
}
} catch {
qrImageDataUrl = "";
}
}
return renderBossAgentHtmlBase(status, { ...options, qrImageDataUrl });
}
function activeClass(activeTab, tab) {
return activeTab === tab ? ` class="active"` : "";
}
function renderBossAgentNav(status, activeTab) {
return `<nav class="nav">
<a${activeClass(activeTab, "overview")} href="/boss-agent?tab=overview"><span>概览</span>${activeTab === "overview" ? `<span class="nav-badge">当前</span>` : ""}</a>
<a${activeClass(activeTab, "permissions")} href="/boss-agent?tab=permissions"><span>本机权限获取</span><span class="nav-badge">${escapeHtml(status.permissionReadiness.coreReady ? "OK" : "待")}</span></a>
<a${activeClass(activeTab, "skills")} href="/boss-agent?tab=skills"><span>Skill</span><span class="nav-badge">${escapeHtml(status.skills.total)}</span></a>
<a${activeClass(activeTab, "license")} href="/boss-agent?tab=license"><span>绑定与授权</span></a>
<a${activeClass(activeTab, "logs")} href="/boss-agent?tab=logs"><span>日志</span></a>
</nav>`;
}
function renderTopbar(status, title = "boss-agent", subtitle = "企业电脑接入端") {
return `<header class="topbar">
<div>
<h1>${escapeHtml(title)}</h1>
<div class="subtitle">${escapeHtml(subtitle)}</div>
</div>
<div class="server-pill"><span class="dot ${status.server.ok ? "" : "off"}"></span>${escapeHtml(status.server.ok ? `已连接 ${status.server.endpoint}` : "服务器未连接")}</div>
</header>`;
}
function renderOverviewTab(status, { bound, heroTitle, heroSubtitle, qrBlock }) {
return `${renderTopbar(status)}
<section class="grid">
<div class="card hero">
${qrBlock}
<div>
<h2>${escapeHtml(heroTitle)}</h2>
<p>${escapeHtml(heroSubtitle)}</p>
<div class="button-row">
<button class="button">${escapeHtml(bound ? "查看绑定信息" : "刷新二维码")}</button>
<span class="timer">${escapeHtml(bound ? `账号:${status.binding.account}` : status.binding.qrExpiresInLabel)}</span>
</div>
</div>
</div>
<div class="card panel">
<h2>绑定状态</h2>
<div class="rows">
<div class="row"><span class="label">账号</span><span class="value">${escapeHtml(status.binding.account)}</span></div>
<div class="row"><span class="label">设备</span><span class="value">${escapeHtml(status.device.name)}</span></div>
<div class="row"><span class="label">角色</span><span class="value">${escapeHtml(status.device.role)}</span></div>
<div class="row"><span class="label">设备 ID</span><span class="value">${escapeHtml(status.device.id)}</span></div>
</div>
</div>
</section>
<section class="cards">
<div class="card metric">
<div class="metric-title">账号登录状态</div>
<div class="metric-value">${escapeHtml(bound ? "已绑定" : "等待绑定")}</div>
<div class="metric-detail">${escapeHtml(bound ? status.binding.account : "使用 Boss APP 扫码完成绑定")}</div>
</div>
<div class="card metric">
<div class="metric-title">当前 API 使用情况</div>
<div class="metric-value">${escapeHtml(status.api.primary)}</div>
<div class="metric-detail">备用 API${escapeHtml(status.api.backup)}</div>
</div>
<div class="card metric">
<div class="metric-title">服务器连接</div>
<div class="metric-value">${escapeHtml(status.server.ok ? "正常" : "异常")}</div>
<div class="metric-detail">${escapeHtml(status.server.latencyLabel)}</div>
</div>
<div class="card metric">
<div class="metric-title">正版授权</div>
<div class="metric-value">${escapeHtml(status.license.label)}</div>
<div class="metric-detail">到期:${escapeHtml(status.license.expiresAtLabel)}</div>
</div>
</section>
<section class="card panel">
<h2>授权信息</h2>
<div class="rows">
<div class="row"><span class="label">企业</span><span class="value">${escapeHtml(status.license.enterpriseName)}</span></div>
<div class="row"><span class="label">授权到期</span><span class="value">${escapeHtml(status.license.expiresAtLabel)}</span></div>
<div class="row"><span class="label">权限范围</span><span class="value">${escapeHtml(status.license.scope)}</span></div>
</div>
</section>`;
}
function renderPermissionsTab(status) {
return `${renderTopbar(status, "本机权限获取", "一次性完成本机接管权限配置")}
<section class="card panel setup-panel">
<div class="setup-head">
<div>
<h2>${escapeHtml(status.permissionSetup.title)}</h2>
<p>${escapeHtml(status.permissionSetup.goal)} 权限结论:${escapeHtml(status.permissions.summary)}。当前状态:${escapeHtml(status.permissionSetup.summary)} 后续静默使用依赖系统持久授权。</p>
</div>
<a class="button" href="${escapeHtml(status.permissionSetup.primaryAction.href)}">${escapeHtml(status.permissionSetup.primaryAction.label)}</a>
</div>
<div class="setup-actions">${setupActionRows(status)}</div>
<div class="hint">${escapeHtml(status.permissionSetup.persistenceNote)}</div>
</section>`;
}
function renderSkillsTab(status) {
return `${renderTopbar(status, "Skill", "部署在本机并可被 Boss 调用的能力")}
<section class="card panel">
<h2>Skill 部署情况</h2>
<div class="rows">${skillRows(status)}</div>
<div class="hint">Skill 用于把本机可复用能力分发给 Boss APP 和主 Agent后续企业后台可按账号、设备和权限策略下发。</div>
</section>`;
}
function renderLicenseTab(status) {
return `${renderTopbar(status, "绑定与授权", "账号、设备、服务器和正版授权状态")}
<section class="lower">
<div class="card panel">
<h2>绑定状态</h2>
<div class="rows">
<div class="row"><span class="label">账号</span><span class="value">${escapeHtml(status.binding.account)}</span></div>
<div class="row"><span class="label">设备</span><span class="value">${escapeHtml(status.device.name)}</span></div>
<div class="row"><span class="label">角色</span><span class="value">${escapeHtml(status.device.role)}</span></div>
<div class="row"><span class="label">设备 ID</span><span class="value">${escapeHtml(status.device.id)}</span></div>
</div>
</div>
<div class="card panel">
<h2>授权信息</h2>
<div class="rows">
<div class="row"><span class="label">企业</span><span class="value">${escapeHtml(status.license.enterpriseName)}</span></div>
<div class="row"><span class="label">授权到期</span><span class="value">${escapeHtml(status.license.expiresAtLabel)}</span></div>
<div class="row"><span class="label">权限范围</span><span class="value">${escapeHtml(status.license.scope)}</span></div>
</div>
<div class="hint">${escapeHtml(status.permissionReadiness.detail)}</div>
</div>
</section>`;
}
function renderLogsTab(status) {
return `${renderTopbar(status, "日志", "本机 agent 最近运行状态")}
<section class="card panel">
<h2>暂无日志面板</h2>
<div class="hint">当前版本先保留入口;运行日志仍写入本机 LaunchAgent 日志和 Boss 后台事件。</div>
</section>`;
}
function renderBossAgentTabContent(status, activeTab, viewModel) {
if (activeTab === "permissions") return renderPermissionsTab(status);
if (activeTab === "skills") return renderSkillsTab(status);
if (activeTab === "license") return renderLicenseTab(status);
if (activeTab === "logs") return renderLogsTab(status);
return renderOverviewTab(status, viewModel);
}
function renderBossAgentHtmlBase(status, options = {}) {
const activeTab = normalizeBossAgentTab(options.activeTab);
const bound = status.binding.bound;
const heroTitle = bound ? "这台电脑已接入 Boss" : "扫码绑定 Boss APP";
const heroSubtitle = bound
? "本机 agent 正在接收企业控制台调度,可用于桌面控制、浏览器控制和 Skill 同步。"
: "使用 Boss APP 扫码,将这台电脑加入企业账号。";
const qrBlock = bound
? `<div class="bound-mark"><span>${escapeHtml(status.device.avatar)}</span></div>`
: `<div class="qr-box">${
options.qrImageDataUrl
? `<img class="qr-img" src="${escapeHtml(options.qrImageDataUrl)}" alt="Boss APP 绑定二维码" />`
: renderPseudoQrSvg(status.binding.qrPayload)
}</div>`;
const viewModel = { bound, heroTitle, heroSubtitle, qrBlock };
return `<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>boss-agent</title>
<style>
:root {
color-scheme: light;
--green: #07c160;
--green-soft: #e9f8ef;
--ink: #111418;
--muted: #707982;
--line: #e8ece9;
--panel: #ffffff;
--bg: #f5f7f4;
--warn: #d98512;
--warn-soft: #fff6df;
--bad: #e54d42;
--bad-soft: #fff1ef;
--blue-soft: #eef6ff;
--blue: #2377d1;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
background:
radial-gradient(circle at 18% 10%, rgba(7, 193, 96, .08), transparent 28%),
linear-gradient(135deg, #fbfcfa 0%, var(--bg) 100%);
color: var(--ink);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
}
.window {
width: min(1180px, calc(100vw - 56px));
min-height: 760px;
display: grid;
grid-template-columns: 268px 1fr;
overflow: hidden;
border: 1px solid rgba(17, 20, 24, .08);
border-radius: 22px;
background: rgba(255, 255, 255, .92);
box-shadow: 0 32px 80px rgba(22, 38, 29, .14);
}
.sidebar {
padding: 18px 16px;
border-right: 1px solid var(--line);
background: rgba(248, 250, 247, .82);
}
.traffic { display: flex; gap: 8px; margin-bottom: 30px; }
.traffic span { width: 12px; height: 12px; border-radius: 999px; display: block; }
.red { background: #ff5f57; } .yellow { background: #febc2e; } .green-light { background: #28c840; }
.brand { display: flex; align-items: center; gap: 12px; margin-bottom: 24px; }
.brand-mark, .bound-mark {
width: 42px; height: 42px; border-radius: 16px;
display: grid; place-items: center;
background: var(--green-soft); color: var(--green);
font-weight: 800; font-size: 21px;
}
.brand-title { font-weight: 800; font-size: 18px; letter-spacing: -.02em; }
.device-name { margin-top: 4px; color: var(--muted); font-size: 12px; }
.nav { display: grid; gap: 6px; margin-bottom: 16px; }
.nav a {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
padding: 12px 12px;
border-radius: 14px;
color: #394139;
text-decoration: none;
font-size: 14px;
font-weight: 650;
}
.nav a.active { background: var(--green-soft); color: #058743; }
.nav-badge {
min-width: 22px;
padding: 3px 7px;
border-radius: 999px;
background: rgba(17, 20, 24, .06);
color: var(--muted);
text-align: center;
font-size: 11px;
font-weight: 800;
}
.nav a.active .nav-badge { background: rgba(7, 193, 96, .14); color: #058743; }
.content { padding: 26px 32px 32px; }
.topbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 28px; }
h1 { margin: 0; font-size: 28px; letter-spacing: -.04em; }
.subtitle { color: var(--muted); margin-top: 6px; font-size: 14px; }
.server-pill {
padding: 10px 14px;
border: 1px solid var(--line);
border-radius: 999px;
background: white;
color: #263128;
font-weight: 700;
font-size: 13px;
}
.dot {
width: 8px; height: 8px; border-radius: 999px; display: inline-block; margin-right: 8px;
background: var(--green);
}
.dot.off { background: var(--bad); }
.grid { display: grid; grid-template-columns: 1.1fr .9fr; gap: 18px; margin-bottom: 18px; }
.card {
border: 1px solid var(--line);
border-radius: 20px;
background: var(--panel);
box-shadow: 0 10px 34px rgba(21, 35, 27, .05);
}
.hero { padding: 24px; display: grid; grid-template-columns: 176px 1fr; gap: 24px; align-items: center; }
.qr-box {
width: 176px; height: 176px;
padding: 16px;
border: 1px solid var(--line);
border-radius: 22px;
background: linear-gradient(180deg, #fff, #f7faf7);
}
.qr-svg { width: 100%; height: 100%; fill: #18211a; image-rendering: pixelated; }
.qr-img { display: block; width: 100%; height: 100%; object-fit: contain; }
.hero h2, .panel h2 { margin: 0; font-size: 22px; letter-spacing: -.03em; }
.hero p { margin: 10px 0 18px; color: var(--muted); line-height: 1.65; }
.button-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.button {
border: 0;
border-radius: 14px;
padding: 12px 16px;
background: var(--green);
color: white;
font-weight: 800;
font-size: 14px;
}
.timer { color: var(--muted); font-size: 13px; }
.panel { padding: 22px; }
.rows { display: grid; gap: 14px; margin-top: 18px; }
.row, .permission-row, .skill-row, .setup-action { display: flex; justify-content: space-between; gap: 18px; align-items: center; }
.label, .muted { color: var(--muted); font-size: 13px; }
.value { font-weight: 750; text-align: right; }
.cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-bottom: 18px; }
.metric { padding: 18px; min-height: 126px; }
.metric-title { color: var(--muted); font-size: 13px; margin-bottom: 16px; }
.metric-value { font-size: 20px; font-weight: 850; letter-spacing: -.03em; line-height: 1.35; }
.metric-detail { color: var(--muted); margin-top: 8px; font-size: 12px; line-height: 1.5; }
.lower { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.pill {
border-radius: 999px;
padding: 6px 10px;
font-size: 12px;
font-weight: 750;
white-space: nowrap;
}
.pill.good { color: #058743; background: var(--green-soft); }
.pill.warn { color: var(--warn); background: var(--warn-soft); }
.pill.bad { color: var(--bad); background: var(--bad-soft); }
.permission-name { font-weight: 780; margin-bottom: 3px; }
.skill-name { font-weight: 780; margin-bottom: 3px; }
.setup-panel { margin-bottom: 18px; }
.setup-head { display: flex; justify-content: space-between; gap: 18px; align-items: flex-start; }
.setup-head p { margin: 8px 0 0; color: var(--muted); line-height: 1.65; font-size: 14px; }
.setup-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-top: 18px; }
.setup-action {
padding: 14px;
border: 1px solid var(--line);
border-radius: 16px;
background: #fbfcfb;
}
.setup-action-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.text-link {
color: #058743;
text-decoration: none;
font-size: 12px;
font-weight: 850;
white-space: nowrap;
}
.empty-state {
min-height: 120px;
display: grid;
place-items: center;
color: var(--muted);
line-height: 1.7;
text-align: center;
border: 1px dashed var(--line);
border-radius: 16px;
background: #fbfcfb;
}
.hint {
margin-top: 18px;
padding: 14px 16px;
border-radius: 16px;
color: var(--blue);
background: var(--blue-soft);
font-size: 13px;
line-height: 1.55;
}
@media (max-width: 980px) {
body { place-items: start; }
.window { width: 100vw; min-height: 100vh; border-radius: 0; grid-template-columns: 1fr; }
.sidebar { display: none; }
.grid, .lower { grid-template-columns: 1fr; }
.setup-actions { grid-template-columns: 1fr; }
.cards { grid-template-columns: repeat(2, 1fr); }
}
</style>
</head>
<body>
<main class="window">
<aside class="sidebar">
<div class="traffic"><span class="red"></span><span class="yellow"></span><span class="green-light"></span></div>
<div class="brand">
<div class="brand-mark">B</div>
<div>
<div class="brand-title">boss-agent</div>
<div class="device-name">${escapeHtml(status.device.name)}</div>
</div>
</div>
${renderBossAgentNav(status, activeTab)}
</aside>
<section class="content">
${renderBossAgentTabContent(status, activeTab, viewModel)}
</section>
</main>
</body>
</html>`;
}
function runCommand(command, args, timeoutMs = 2500) {
return new Promise((resolve) => {
const child = spawn(command, args, {
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
const timer = setTimeout(() => {
child.kill("SIGKILL");
}, timeoutMs);
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (chunk) => {
stdout += chunk;
});
child.stderr.on("data", (chunk) => {
stderr += chunk;
});
child.on("error", (error) => {
clearTimeout(timer);
resolve({ ok: false, stdout, stderr: error.message });
});
child.on("close", (code) => {
clearTimeout(timer);
resolve({ ok: code === 0, stdout: stdout.trim(), stderr: stderr.trim() });
});
});
}
export function resolveBossAgentPermissionSettingsUrl(target = "all") {
return MACOS_PERMISSION_SETTINGS[target] ?? MACOS_PERMISSION_SETTINGS.all;
}
export async function openBossAgentPermissionSettings(target = "all", platform = process.platform) {
const settingsUrl = resolveBossAgentPermissionSettingsUrl(target);
if (platform !== "darwin") {
return {
ok: false,
target,
settingsUrl,
message: "当前平台暂不支持自动打开系统隐私设置,请在系统设置中手动完成授权。",
};
}
const result = await runCommand("open", [settingsUrl], 2500);
return {
ok: result.ok,
target,
settingsUrl,
message: result.ok ? "已打开系统权限设置。" : result.stderr || result.stdout || "打开系统权限设置失败。",
};
}
export async function detectLocalComputerPermissions(platform = process.platform) {
if (platform !== "darwin") {
return {
accessibility: "unknown",
screenRecording: "unknown",
automation: "unknown",
};
}
const accessibility = await runCommand("osascript", [
"-e",
'tell application "System Events" to get UI elements enabled',
]);
const automation = await runCommand("osascript", ["-e", 'tell application "Finder" to get name']);
const screenshotPath = path.join(os.tmpdir(), `boss-agent-permission-${Date.now()}.png`);
const screen = await runCommand("screencapture", ["-x", "-t", "png", screenshotPath], 3500);
let screenRecording = "missing";
if (screen.ok) {
try {
const info = await stat(screenshotPath);
screenRecording = info.size > 1024 ? "granted" : "unknown";
} catch {
screenRecording = "unknown";
} finally {
await rm(screenshotPath, { force: true }).catch(() => {});
}
}
return {
accessibility: accessibility.ok && /true/i.test(accessibility.stdout) ? "granted" : "missing",
screenRecording,
automation: automation.ok ? "granted" : "missing",
};
}