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 () => {