Files
boss/local-agent/boss-agent-status.mjs
2026-06-04 15:18:52 +08:00

1267 lines
48 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";
import { DatabaseSync } from "node:sqlite";
const PERMISSION_DEFS = [
{
key: "accessibility",
label: "辅助功能",
description: "用于点击、输入和读取可访问控件",
tier: "core",
},
{
key: "screenRecording",
label: "屏幕录制",
description: "用于识别桌面画面和系统弹窗",
tier: "core",
},
];
const MACOS_PERMISSION_SETTINGS = {
core: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Accessibility",
accessibility: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Accessibility",
screenRecording: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_ScreenCapture",
};
const AUTO_PREFLIGHT_PERMISSION_KEYS = new Set(["accessibility", "screenRecording"]);
const NATIVE_PERMISSION_QUERY_PARAMS = {
accessibility: "nativeAccessibility",
screenRecording: "nativeScreenRecording",
};
const BOSS_AGENT_BUNDLE_ID = "com.hyzq.boss.agent";
const BOSS_COMPUTER_USE_HELPER_BUNDLE_ID = "site.hyzq.boss.computer-use-helper";
const BOSS_COMPUTER_USE_HELPER_APP_CANDIDATES = [
path.join(os.homedir(), "Applications/BossComputerUseHelper.app"),
"/Applications/BossComputerUseHelper.app",
];
const HELPER_SCREEN_RECORDING_CACHE_TTL_MS = 30_000;
let helperScreenRecordingCache = {
status: "unknown",
expiresAt: 0,
};
const BOSS_AGENT_DEFAULTS_DOMAIN = "com.hyzq.boss.agent";
const TCC_PERMISSION_SERVICES = {
kTCCServiceAccessibility: "accessibility",
kTCCServiceScreenCapture: "screenRecording",
};
const TCC_PERMISSION_CLIENTS = {
kTCCServiceAccessibility: [BOSS_AGENT_BUNDLE_ID],
kTCCServiceScreenCapture: [BOSS_AGENT_BUNDLE_ID, BOSS_COMPUTER_USE_HELPER_BUNDLE_ID],
};
const TCC_PERMISSION_DATABASES = [
"/Library/Application Support/com.apple.TCC/TCC.db",
path.join(os.homedir(), "Library/Application Support/com.apple.TCC/TCC.db"),
];
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 sqlQuote(value) {
return `'${String(value ?? "").replaceAll("'", "''")}'`;
}
function normalizePermissionStatus(value) {
return value === "granted" || value === "missing" || value === "unknown" ? value : "unknown";
}
function isPermissionStatus(value) {
return value === "granted" || value === "missing" || 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 normalizeCommandArgs(value, fallback = []) {
if (!Array.isArray(value)) return [...fallback];
return value.map((item) => nonEmpty(item)).filter(Boolean);
}
function resolveBooleanWithDefault(value, defaultValue = false) {
if (value === undefined || value === null || value === "") return defaultValue;
if (value === false || value === "false" || value === "0" || value === 0) return false;
return true;
}
function resolveCodexRemoteControl(config, appServerEnabled) {
const enabled = resolveBooleanWithDefault(config.codexRemoteControlEnabled, appServerEnabled);
const command = nonEmpty(config.codexRemoteControlCommand) ?? nonEmpty(config.codexAppServerCommand) ?? "codex";
const args = normalizeCommandArgs(config.codexRemoteControlArgs, ["remote-control", "start", "--json"]);
const startCommandLabel = [command, ...args].join(" ");
return {
enabled,
mode: enabled ? "managed_daemon" : "disabled",
command,
args,
startCommandLabel,
statusLabel: enabled ? "可托管启动" : "未启用",
summary: enabled
? "Codex Remote Control 会通过 App Server daemon 提供官方远控入口;当前状态页只展示能力,不在刷新时自动启动。"
: "Codex Remote Control daemon 未启用;远程控制会继续使用当前 App Server / Computer Use 配置。",
};
}
function resolveCodexBinding(config) {
const appServerEnabled = config.codexAppServerEnabled === true;
const codexComputerUseEnabled = config.codexComputerUseEnabled === true;
const command = nonEmpty(config.codexAppServerCommand) ?? "codex";
const defaultDesktopProvider = codexComputerUseEnabled
? "codex-computer-use"
: "cua-driver-computer-use";
const bindingStatus = appServerEnabled || codexComputerUseEnabled ? "connected" : "not_configured";
return {
bindingStatus,
statusLabel: bindingStatus === "connected" ? "已默认绑定" : "未默认绑定",
command,
appServerEnabled,
computerUseEnabled: codexComputerUseEnabled,
defaultDesktopProvider,
desktopProviderLabel:
defaultDesktopProvider === "codex-computer-use"
? "Codex Computer Use"
: "Boss CUA Driver",
fallbackProvider: "cua-driver-computer-use",
fallbackLabel: "Boss CUA Driver",
remoteControl: resolveCodexRemoteControl(config, appServerEnabled),
summary:
defaultDesktopProvider === "codex-computer-use"
? "远程控制默认走 Codex Computer Use失败后回退 Boss CUA Driver。"
: "远程控制默认走 Boss CUA Driver。",
};
}
function resolveAgentOta(config, runtime) {
const enabledValue = config.bossAgentOtaEnabled;
const enabled = enabledValue === undefined ? true : enabledValue !== false && enabledValue !== "false";
const currentVersion = nonEmpty(config.bossAgentVersion) ?? "dev";
const lastStatus = runtime.lastBossAgentOtaStatus && typeof runtime.lastBossAgentOtaStatus === "object"
? runtime.lastBossAgentOtaStatus
: {};
const lastApply = runtime.lastBossAgentOtaApply && typeof runtime.lastBossAgentOtaApply === "object"
? runtime.lastBossAgentOtaApply
: {};
const latest = lastStatus.latest && typeof lastStatus.latest === "object" ? lastStatus.latest : null;
const hasUpdate = enabled && lastStatus.hasUpdate === true && Boolean(latest);
const latestVersion = nonEmpty(latest?.version) ?? "";
const applyStatus = nonEmpty(lastApply.status) ?? "";
return {
enabled,
currentVersion,
hasUpdate,
latestVersion,
latestFileName: nonEmpty(latest?.fileName) ?? "",
latestUpdatedAt: nonEmpty(latest?.updatedAt) ?? "",
lastCheckedAt: nonEmpty(lastStatus.checkedAt) ?? "",
lastApplyStatus: applyStatus,
lastApplyAt: nonEmpty(lastApply.completedAt) ?? "",
statusLabel: !enabled
? "未启用"
: hasUpdate
? "发现新版本"
: lastStatus.error
? "检查失败"
: "当前版本",
detail: !enabled
? "boss-agent OTA 已关闭"
: hasUpdate
? `最新:${latestVersion || "未知版本"}`
: lastStatus.error
? String(lastStatus.error)
: `当前:${currentVersion}`,
};
}
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 = coreReady ? "基础桌面控制已可用" : "基础桌面控制待授权,桌面接管不可用";
return {
coreReady,
fullControlReady,
coreGrantedCount,
coreTotal: coreItems.length,
extendedGrantedCount,
extendedTotal: extendedItems.length,
summary,
detail:
"参考 Codex Computer Use 的最小权限模型boss-agent 只要求辅助功能和屏幕录制:辅助功能负责点击输入,屏幕录制可由 Boss Computer Use Helper 提供画面识别。",
};
}
function buildPermissionSetupPlan(coreItems, readiness) {
const actions = coreItems.map((item) => ({
key: item.key,
label: item.label,
description: item.description,
tier: item.tier,
status: item.status,
requiredForSilentControl: item.tier === "core",
canPreflight: AUTO_PREFLIGHT_PERMISSION_KEYS.has(item.key),
settingsUrl: MACOS_PERMISSION_SETTINGS[item.key] ?? MACOS_PERMISSION_SETTINGS.core,
openUrl: `/api/v1/boss-agent/permissions/open?target=${encodeURIComponent(item.key)}&returnTab=permissions`,
owner: "boss-agent.app",
}));
const missingRequiredActions = actions.filter(
(action) => action.requiredForSilentControl && action.status !== "granted",
);
return {
mode: "minimal_computer_use",
title: "基础桌面控制授权",
goal: "按 Codex Computer Use 的思路,只申请辅助功能和屏幕录制两项最小权限。",
silentUseReady: missingRequiredActions.length === 0,
primaryAction: {
label: "打开基础授权",
href: "/api/v1/boss-agent/permissions/open?target=core&returnTab=permissions",
settingsUrl: MACOS_PERMISSION_SETTINGS.core,
},
actions,
missingKeys: missingRequiredActions.map((action) => action.key),
missingRequiredKeys: missingRequiredActions.map((action) => action.key),
optionalMissingKeys: [],
summary: readiness.coreReady
? "基础桌面控制已可用;后续控制只校验这两项权限。"
: "仍缺少基础桌面控制权限,请先授权辅助功能和屏幕录制。",
persistenceNote:
"macOS 会把授权持久写入系统隐私数据库;稳定签名后,后续更新不会因为二进制哈希变化反复丢失授权。",
};
}
export function mergeBossAgentNativePermissionOverrides(permissions = {}, queryParams = {}) {
const getQueryValue = (name) => {
if (typeof queryParams.get === "function") return queryParams.get(name);
return queryParams[name];
};
const merged = {};
for (const permissionKey of Object.keys(NATIVE_PERMISSION_QUERY_PARAMS)) {
if (isPermissionStatus(permissions[permissionKey])) {
merged[permissionKey] = permissions[permissionKey];
}
}
for (const [permissionKey, queryKey] of Object.entries(NATIVE_PERMISSION_QUERY_PARAMS)) {
const value = getQueryValue(queryKey);
if (isPermissionStatus(value)) {
if (merged[permissionKey] === "granted" && value !== "granted") {
continue;
}
merged[permissionKey] = value;
}
}
return merged;
}
export function mergeBossAgentStoredNativePermissions(permissions = {}, storedPermissions = {}) {
const merged = { ...permissions };
for (const permissionKey of Object.keys(NATIVE_PERMISSION_QUERY_PARAMS)) {
if (storedPermissions[permissionKey] === "granted") {
merged[permissionKey] = "granted";
}
}
return merged;
}
export function mergeBossComputerUseHelperScreenRecordingPermission(permissions = {}, helperStatus = "unknown") {
if (helperStatus !== "granted") return { ...permissions };
return {
...permissions,
screenRecording: "granted",
};
}
export function mergeBossAgentAppTccPermissions(permissions = {}, tccRows = "") {
const merged = { ...permissions };
const granted = new Set();
for (const rawLine of String(tccRows ?? "").split(/\r?\n/)) {
const line = rawLine.trim();
if (!line) continue;
const parts = line.split("|");
const [service] = parts;
const authValue = parts.length >= 3 ? parts[2] : parts[1];
const client = parts.length >= 3 ? parts[1] : BOSS_AGENT_BUNDLE_ID;
const permissionKey = TCC_PERMISSION_SERVICES[service];
if (!permissionKey) continue;
const allowedClients = TCC_PERMISSION_CLIENTS[service] ?? [];
if (client && allowedClients.length > 0 && !allowedClients.includes(client)) continue;
if (authValue === "2") {
merged[permissionKey] = "granted";
granted.add(permissionKey);
} else if (!granted.has(permissionKey) && authValue === "0") {
merged[permissionKey] = "missing";
}
}
return merged;
}
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 = [];
const permissionReadiness = resolvePermissionReadiness(corePermissionItems, extendedPermissionItems);
const permissionSetup = buildPermissionSetupPlan(corePermissionItems, 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),
codex: resolveCodexBinding(config),
agentOta: resolveAgentOta(config, runtime),
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.requiredForSilentControl
? "基础必需"
: 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">Codex 默认接管</div>
<div class="metric-value">${escapeHtml(status.codex.desktopProviderLabel)}</div>
<div class="metric-detail">${escapeHtml(status.codex.summary)}</div>
</div>
<div class="card metric">
<div class="metric-title">Codex Remote Control</div>
<div class="metric-value">${escapeHtml(status.codex.remoteControl.statusLabel)}</div>
<div class="metric-detail">${escapeHtml(status.codex.remoteControl.startCommandLabel)}</div>
</div>
<div class="card metric">
<div class="metric-title">boss-agent OTA</div>
<div class="metric-value">${escapeHtml(status.agentOta.statusLabel)}</div>
<div class="metric-detail">${escapeHtml(status.agentOta.detail)}</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>
<section class="card panel">
<h2>boss-agent OTA</h2>
<div class="rows">
<div class="row"><span class="label">当前版本</span><span class="value">${escapeHtml(status.agentOta.currentVersion)}</span></div>
<div class="row"><span class="label">最新版本</span><span class="value">${escapeHtml(status.agentOta.latestVersion || "未发现新版本")}</span></div>
<div class="row"><span class="label">升级状态</span><span class="value">${escapeHtml(status.agentOta.statusLabel)}</span></div>
<div class="row"><span class="label">最近安装</span><span class="value">${escapeHtml(status.agentOta.lastApplyStatus || "暂无")}</span></div>
</div>
<div class="button-row ota-actions">
<form action="/api/v1/boss-agent/ota/check" method="get"><button class="button secondary" type="submit">检查更新</button></form>
<form action="/api/v1/boss-agent/ota/apply" method="post"><button class="button" type="submit">下载并安装</button></form>
</div>
<div class="hint">Mac 端 OTA 会先下载并校验安装包,再拉起本机安装器;原配置会被保留,失败不会覆盖当前运行版本。</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;
}
.button.secondary { background: #eef3ef; color: #263128; }
form { margin: 0; }
.ota-actions { margin-top: 18px; }
.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(6, 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() });
});
});
}
async function findBossComputerUseHelperApp() {
for (const appPath of BOSS_COMPUTER_USE_HELPER_APP_CANDIDATES) {
try {
const info = await stat(appPath);
if (info.isDirectory()) return appPath;
} catch {
// Try the next installation location.
}
}
return "";
}
async function detectBossComputerUseHelperScreenRecording() {
const now = Date.now();
if (helperScreenRecordingCache.expiresAt > now) {
return helperScreenRecordingCache.status;
}
const helperApp = await findBossComputerUseHelperApp();
if (!helperApp) {
helperScreenRecordingCache = {
status: "unknown",
expiresAt: now + HELPER_SCREEN_RECORDING_CACHE_TTL_MS,
};
return helperScreenRecordingCache.status;
}
const screenshotPath = path.join(os.tmpdir(), `boss-helper-screen-permission-${now}.png`);
try {
const result = await runCommand(
"open",
["-W", "-na", helperApp, "--args", "screenshot", "--path", screenshotPath],
5000,
);
if (result.ok) {
const info = await stat(screenshotPath).catch(() => null);
if (info?.size > 1024) {
helperScreenRecordingCache = {
status: "granted",
expiresAt: now + HELPER_SCREEN_RECORDING_CACHE_TTL_MS,
};
return helperScreenRecordingCache.status;
}
}
} finally {
await rm(screenshotPath, { force: true }).catch(() => {});
}
helperScreenRecordingCache = {
status: "missing",
expiresAt: now + HELPER_SCREEN_RECORDING_CACHE_TTL_MS,
};
return helperScreenRecordingCache.status;
}
function normalizePermissionTarget(target = "core") {
return Object.hasOwn(MACOS_PERMISSION_SETTINGS, target) ? target : "core";
}
export function resolveBossAgentPermissionSettingsUrl(target = "core") {
return MACOS_PERMISSION_SETTINGS[normalizePermissionTarget(target)];
}
export async function openBossAgentPermissionSettings(target = "core", platform = process.platform) {
const normalizedTarget = normalizePermissionTarget(target);
const settingsUrl = resolveBossAgentPermissionSettingsUrl(normalizedTarget);
if (platform !== "darwin") {
return {
ok: false,
target: normalizedTarget,
settingsUrl,
message: "当前平台暂不支持自动打开系统隐私设置,请在系统设置中手动完成授权。",
};
}
const nativeUrl = `boss-agent://permissions/open?target=${encodeURIComponent(normalizedTarget)}&returnTab=permissions`;
const nativeLaunch = await runCommand(
"open",
[
"-na",
"/Applications/boss-agent.app",
"--args",
"--request-permission",
normalizedTarget,
"--return-tab",
"permissions",
],
2500,
);
if (nativeLaunch.ok) {
return {
ok: true,
target: normalizedTarget,
settingsUrl,
message: "已通过 boss-agent 发起系统权限申请。",
nativeRequest: true,
nativeUrl,
};
}
const nativeDeepLink = await runCommand("open", ["-b", "com.hyzq.boss.agent", nativeUrl], 2500);
if (nativeDeepLink.ok) {
return {
ok: true,
target: normalizedTarget,
settingsUrl,
message: "已通过 boss-agent 发起系统权限申请。",
nativeRequest: true,
nativeUrl,
};
}
const result = await runCommand("open", [settingsUrl], 2500);
return {
ok: result.ok,
target: normalizedTarget,
settingsUrl,
message: result.ok
? "已打开系统权限设置。"
: nativeLaunch.stderr
|| nativeLaunch.stdout
|| nativeDeepLink.stderr
|| nativeDeepLink.stdout
|| result.stderr
|| result.stdout
|| "打开系统权限设置失败。",
nativeRequest: false,
nativeUrl,
};
}
export async function detectLocalComputerPermissions(platform = process.platform) {
if (platform !== "darwin") {
return {
accessibility: "unknown",
screenRecording: "unknown",
};
}
const accessibility = await runCommand("osascript", [
"-e",
'tell application "System Events" to get UI elements enabled',
]);
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(() => {});
}
}
const localProcessPermissions = {
accessibility: accessibility.ok && /true/i.test(accessibility.stdout) ? "granted" : "missing",
screenRecording,
};
const helperPermissions = mergeBossComputerUseHelperScreenRecordingPermission(
localProcessPermissions,
screenRecording === "granted" ? "unknown" : await detectBossComputerUseHelperScreenRecording(),
);
const appTccPermissions = mergeBossAgentAppTccPermissions(helperPermissions, await readBossAgentAppTccRows());
return mergeBossAgentStoredNativePermissions(appTccPermissions, await readBossAgentStoredNativePermissions());
}
async function readBossAgentAppTccRows() {
const outputs = [];
for (const dbPath of TCC_PERMISSION_DATABASES) {
try {
await stat(dbPath);
} catch {
continue;
}
for (const service of Object.keys(TCC_PERMISSION_SERVICES)) {
const clients = TCC_PERMISSION_CLIENTS[service] ?? [BOSS_AGENT_BUNDLE_ID];
const clientList = clients.map(sqlQuote).join(",");
const query = [
"select service,client,auth_value from access",
`where client in (${clientList})`,
`and service='${service}'`,
"order by auth_value desc",
].join(" ");
try {
const db = new DatabaseSync(dbPath, { readonly: true });
try {
const rows = db.prepare(query).all();
if (rows.length > 0) {
outputs.push(rows.map((row) => `${row.service}|${row.client}|${row.auth_value}`).join("\n"));
}
} finally {
db.close();
}
} catch {
// Keep the command-line reader below as a second source of truth.
}
const result = await runCommand("/usr/bin/sqlite3", [dbPath, `${query};`], 2500);
if (result.ok && result.stdout) {
outputs.push(result.stdout);
}
}
}
return outputs.join("\n");
}
async function readBossAgentStoredNativePermissions() {
const entries = await Promise.all(
Object.keys(NATIVE_PERMISSION_QUERY_PARAMS).map(async (permissionKey) => {
const result = await runCommand(
"/usr/bin/defaults",
["read", BOSS_AGENT_DEFAULTS_DOMAIN, `native.${permissionKey}`],
1500,
);
return [permissionKey, result.ok ? normalizePermissionStatus(result.stdout) : "unknown"];
}),
);
return Object.fromEntries(entries);
}