diff --git a/README.md b/README.md index 53f08ef..9baa764 100644 --- a/README.md +++ b/README.md @@ -245,7 +245,8 @@ open dist/boss-agent.app 说明: - `boss-agent.app` 是本机 `local-agent` 的 macOS WebView 外壳,默认打开 `http://127.0.0.1:4317/boss-agent` -- 未绑定账号时会显示可扫码的 Boss APP 绑定二维码;已绑定后显示账号、API、服务器、授权和本机电脑权限状态 +- 未绑定账号时会显示可扫码的 Boss APP 绑定二维码;已绑定后显示账号、API、服务器、授权、本机权限获取和本机 Skill 部署情况 +- 本机权限会区分两级判断:`辅助功能 / 屏幕录制 / 自动化控制` 只代表核心桌面控制能力;完整接管还需要按业务场景补齐全磁盘访问、输入监控、通知、麦克风、摄像头和本地网络等权限 - 本机状态 JSON 可通过 `GET http://127.0.0.1:4317/api/v1/boss-agent/status` 查看,不会返回设备 token 明文 device-agent 当前职责: diff --git a/local-agent/boss-agent-status.mjs b/local-agent/boss-agent-status.mjs index 6c37b62..fc52f7a 100644 --- a/local-agent/boss-agent-status.mjs +++ b/local-agent/boss-agent-status.mjs @@ -8,16 +8,58 @@ 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", }, ]; @@ -122,6 +164,60 @@ function resolveLicense(config, bound) { }; } +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 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); @@ -130,6 +226,9 @@ export function buildBossAgentStatus(config = {}, runtime = {}, options = {}) { 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); return { appName: "boss-agent", @@ -161,14 +260,12 @@ export function buildBossAgentStatus(config = {}, runtime = {}, options = {}) { api: resolveApiUsage(config), license: resolveLicense(config, bound), permissions: { - summary: PERMISSION_DEFS.every((item) => normalizePermissionStatus(permissions[item.key]) === "granted") - ? "本机权限正常" - : "本机权限待处理", - items: PERMISSION_DEFS.map((item) => ({ - ...item, - status: normalizePermissionStatus(permissions[item.key]), - })), + summary: permissionReadiness.summary, + items: corePermissionItems, + extendedItems: extendedPermissionItems, }, + permissionReadiness, + skills: resolveSkills(runtime), }; } @@ -201,8 +298,8 @@ function permissionText(status) { return "待确认"; } -function permissionRows(status) { - return status.permissions.items +function permissionRows(items) { + return items .map((item) => { const tone = statusTone(item.status); return `
@@ -216,6 +313,66 @@ function permissionRows(status) { .join(""); } +function sidebarPermissionBlock(status) { + const readiness = status.permissionReadiness; + const coreTone = readiness.coreReady ? "good" : "bad"; + const fullTone = readiness.fullControlReady ? "good" : "warn"; + const missingExtended = status.permissions.extendedItems + .filter((item) => item.status !== "granted") + .map((item) => item.label) + .join("、"); + return ``; +} + +function skillRows(status) { + const skills = status.skills?.items ?? []; + if (skills.length === 0) { + return `
本机暂无已同步 Skill。绑定账号后可由 Boss 后台下发或同步本机 Codex Skill。
`; + } + + return skills + .map((skill) => { + const pathLabel = skill.path ? skill.path.replace(os.homedir(), "~") : "未记录路径"; + return `
+
+
${escapeHtml(skill.name)}
+
${escapeHtml(skill.category)} · ${escapeHtml(pathLabel)}
+
+ 已部署 +
`; + }) + .join(""); +} + +function sidebarSkillBlock(status) { + const syncTone = status.skills?.syncOk ? "good" : "warn"; + return ``; +} + export function renderBossAgentHtml(status) { return renderBossAgentHtmlBase(status); } @@ -297,7 +454,7 @@ function renderBossAgentHtmlBase(status, options = {}) { width: min(1180px, calc(100vw - 56px)); min-height: 760px; display: grid; - grid-template-columns: 218px 1fr; + grid-template-columns: 268px 1fr; overflow: hidden; border: 1px solid rgba(17, 20, 24, .08); border-radius: 22px; @@ -312,7 +469,7 @@ function renderBossAgentHtmlBase(status, options = {}) { .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: 30px; } + .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; @@ -321,8 +478,12 @@ function renderBossAgentHtmlBase(status, options = {}) { } .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; } + .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; @@ -331,6 +492,44 @@ function renderBossAgentHtmlBase(status, options = {}) { 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; } + .sidebar-card { + display: grid; + gap: 10px; + margin-top: 12px; + padding: 14px; + border: 1px solid var(--line); + border-radius: 18px; + background: rgba(255, 255, 255, .82); + box-shadow: 0 8px 24px rgba(21, 35, 27, .04); + } + .sidebar-title { font-size: 14px; font-weight: 850; letter-spacing: -.02em; } + .sidebar-note { color: var(--muted); font-size: 12px; line-height: 1.5; } + .sidebar-kv { display: flex; justify-content: space-between; align-items: center; gap: 12px; color: var(--muted); font-size: 12px; } + .sidebar-kv b { color: var(--ink); font-size: 13px; } + .sidebar-kv b.good { color: #058743; } + .sidebar-kv b.warn { color: var(--warn); } + .sidebar-kv b.bad { color: var(--bad); } + .sidebar-mini-list { + display: grid; + gap: 8px; + padding-top: 4px; + border-top: 1px solid var(--line); + } + .sidebar-mini-list .permission-row { gap: 10px; align-items: flex-start; } + .sidebar-mini-list .permission-name { font-size: 12px; } + .sidebar-mini-list .muted { display: none; } + .sidebar-mini-list .pill { padding: 4px 8px; font-size: 11px; } .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; } @@ -381,7 +580,7 @@ function renderBossAgentHtmlBase(status, options = {}) { .timer { color: var(--muted); font-size: 13px; } .panel { padding: 22px; } .rows { display: grid; gap: 14px; margin-top: 18px; } - .row, .permission-row { display: flex; justify-content: space-between; gap: 18px; align-items: center; } + .row, .permission-row, .skill-row { 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; } @@ -401,6 +600,18 @@ function renderBossAgentHtmlBase(status, options = {}) { .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; } + .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; @@ -431,11 +642,14 @@ function renderBossAgentHtmlBase(status, options = {}) {
+ ${sidebarPermissionBlock(status)} + ${sidebarSkillBlock(status)}
@@ -500,12 +714,12 @@ function renderBossAgentHtmlBase(status, options = {}) {
授权到期${escapeHtml(status.license.expiresAtLabel)}
权限范围${escapeHtml(status.license.scope)}
-
授权状态由 Boss 企业后台统一下发;未绑定时只显示本机预检结果。
+
${escapeHtml(status.permissionReadiness.detail)}
-

本机电脑权限状态

-
${permissionRows(status)}
-
${escapeHtml(status.permissions.summary)}。如未授权,请在 macOS 系统设置里为 boss-agent / Node 运行时开启对应权限。
+

Skill 部署情况

+
${skillRows(status)}
+
Skill 用于把本机可复用能力分发给 Boss APP 和主 Agent;后续企业后台可按账号、设备和权限策略下发。
diff --git a/tests/boss-agent-status.test.mjs b/tests/boss-agent-status.test.mjs index 816b43e..a4b055b 100644 --- a/tests/boss-agent-status.test.mjs +++ b/tests/boss-agent-status.test.mjs @@ -25,6 +25,12 @@ test("boss-agent status exposes unbound QR binding and local permission states", lastHeartbeatOk: true, lastHeartbeatStatus: 200, lastHeartbeatAt: "2026-05-12T05:00:00.000Z", + lastSkills: [ + { name: "bb-browser", category: "MacBook Air", path: "/Users/jas/.codex/skills/bb-browser/SKILL.md" }, + { name: "skill-installer", category: ".system", path: "/Users/jas/.codex/skills/.system/skill-installer/SKILL.md" }, + ], + lastSkillSyncOk: true, + lastSkillSyncAt: "2026-05-12T05:30:00.000Z", }, { permissions: { @@ -43,6 +49,11 @@ test("boss-agent status exposes unbound QR binding and local permission states", assert.equal(status.api.primary, "DeepSeek V4"); assert.equal(status.api.backup, "未启用"); assert.equal(status.license.status, "pending_binding"); + assert.equal(status.skills.total, 2); + assert.equal(status.skills.syncOk, true); + assert.equal(status.permissionReadiness.coreReady, false); + assert.equal(status.permissionReadiness.fullControlReady, false); + assert.match(status.permissionReadiness.summary, /完整接管/); assert.deepEqual( status.permissions.items.map((item) => [item.key, item.status]), [ @@ -74,12 +85,19 @@ test("boss-agent status treats token-backed devices as bound and renders enterpr lastHeartbeatOk: true, lastHeartbeatStatus: 200, lastHeartbeatAt: "2026-05-12T05:00:00.000Z", + lastSkills: [ + { name: "bb-browser", category: "MacBook Air", path: "/Users/jas/.codex/skills/bb-browser/SKILL.md" }, + { name: "skill-installer", category: ".system", path: "/Users/jas/.codex/skills/.system/skill-installer/SKILL.md" }, + ], + lastSkillSyncOk: true, }, { permissions: { accessibility: "granted", screenRecording: "granted", automation: "granted", + fullDiskAccess: "missing", + inputMonitoring: "unknown", }, now: "2026-05-12T06:00:00.000Z", }, @@ -93,10 +111,14 @@ test("boss-agent status treats token-backed devices as bound and renders enterpr const html = renderBossAgentHtml(status); assert.match(html, /boss-agent/); assert.match(html, /企业电脑接入端/); - assert.match(html, /本机电脑权限状态/); + assert.match(html, /本机权限获取/); + assert.match(html, /完整接管待补齐/); + assert.match(html, /Skill/); + assert.match(html, /bb-browser/); assert.match(html, /DeepSeek V4/); assert.match(html, /默认公司/); assert.doesNotMatch(html, /boss-secret-token/); + assert.doesNotMatch(html, /

本机电脑权限状态<\/h2>/); }); test("boss-agent unbound HTML renders a real scannable QR image when qrcode is available", async () => {