fix: split agent permission and skill tabs

This commit is contained in:
AI Bot
2026-05-12 23:39:13 +08:00
parent 29740f35c7
commit 8d3f68cebe
3 changed files with 216 additions and 102 deletions

View File

@@ -389,11 +389,18 @@ function skillRows(status) {
.join("");
}
export function renderBossAgentHtml(status) {
return renderBossAgentHtmlBase(status);
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 async function renderBossAgentHtmlWithQr(status) {
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 {
@@ -414,10 +421,158 @@ export async function renderBossAgentHtmlWithQr(status) {
qrImageDataUrl = "";
}
}
return renderBossAgentHtmlBase(status, { 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
@@ -430,6 +585,7 @@ function renderBossAgentHtmlBase(status, options = {}) {
? `<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">
@@ -649,97 +805,10 @@ function renderBossAgentHtmlBase(status, options = {}) {
<div class="device-name">${escapeHtml(status.device.name)}</div>
</div>
</div>
<nav class="nav">
<a class="active" href="#overview"><span>概览</span><span class="nav-badge">当前</span></a>
<a href="#permissions"><span>本机权限获取</span><span class="nav-badge">${escapeHtml(status.permissionReadiness.coreReady ? "OK" : "待")}</span></a>
<a href="#skills"><span>Skill</span><span class="nav-badge">${escapeHtml(status.skills.total)}</span></a>
<a href="#license"><span>绑定与授权</span></a>
<a href="#logs"><span>日志</span></a>
</nav>
${renderBossAgentNav(status, activeTab)}
</aside>
<section class="content">
<header class="topbar" id="overview">
<div>
<h1>boss-agent</h1>
<div class="subtitle">企业电脑接入端</div>
</div>
<div class="server-pill"><span class="dot ${status.server.ok ? "" : "off"}"></span>${escapeHtml(status.server.ok ? `已连接 ${status.server.endpoint}` : "服务器未连接")}</div>
</header>
<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 setup-panel" id="permissions">
<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>
<section class="lower">
<div class="card panel" id="license">
<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>
<div class="card panel" id="skills">
<h2>Skill 部署情况</h2>
<div class="rows">${skillRows(status)}</div>
<div class="hint">Skill 用于把本机可复用能力分发给 Boss APP 和主 Agent后续企业后台可按账号、设备和权限策略下发。</div>
</div>
</section>
${renderBossAgentTabContent(status, activeTab, viewModel)}
</section>
</main>
</body>

View File

@@ -41,6 +41,7 @@ import {
import {
buildBossAgentStatus,
detectLocalComputerPermissions,
normalizeBossAgentTab,
openBossAgentPermissionSettings,
renderBossAgentHtmlWithQr,
} from "./boss-agent-status.mjs";
@@ -1011,8 +1012,9 @@ const server = createServer(async (request, response) => {
if (requestUrl.pathname === "/" || requestUrl.pathname === "/boss-agent") {
const permissions = await detectLocalComputerPermissions();
const status = buildBossAgentStatus(config, runtime, { permissions });
const activeTab = normalizeBossAgentTab(requestUrl.searchParams.get("tab") ?? "overview");
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
response.end(await renderBossAgentHtmlWithQr(status));
response.end(await renderBossAgentHtmlWithQr(status, { activeTab }));
return;
}

View File

@@ -120,23 +120,66 @@ test("boss-agent status treats token-backed devices as bound and renders enterpr
assert.match(html, /boss-agent/);
assert.match(html, /企业电脑接入端/);
assert.match(html, /本机权限获取/);
assert.match(html, /完整接管待补齐/);
assert.match(html, /一次完整授权/);
assert.match(html, /后续静默使用/);
assert.match(html, /href="#permissions"><span>本机权限获取<\/span>/);
assert.match(html, /href="#skills"><span>Skill<\/span>/);
assert.match(html, /href="\/boss-agent\?tab=permissions"><span>本机权限获取<\/span>/);
assert.match(html, /href="\/boss-agent\?tab=skills"><span>Skill<\/span>/);
assert.match(html, /class="active" href="\/boss-agent\?tab=overview"/);
assert.doesNotMatch(html, /<section class="sidebar-card">/);
assert.doesNotMatch(html, /<div class="sidebar-title">本机权限获取<\/div>/);
assert.match(html, /api\/v1\/boss-agent\/permissions\/open\?target=all/);
assert.match(html, /api\/v1\/boss-agent\/permissions\/open\?target=fullDiskAccess/);
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>本机电脑权限状态<\/h2>/);
});
test("boss-agent permission and skill menu entries render as separate tab pages", () => {
const status = buildBossAgentStatus(
{
deviceId: "mac-studio",
name: "Mac Studio",
avatar: "M",
account: "krisolo",
controlPlaneUrl: "https://boss.hyzq.net",
token: "boss-secret-token",
primaryApiLabel: "DeepSeek V4",
license: {
enterpriseName: "默认公司",
status: "valid",
},
},
{
lastHeartbeatOk: true,
lastSkills: [
{ name: "bb-browser", category: "MacBook Air", path: "/Users/jas/.codex/skills/bb-browser/SKILL.md" },
],
lastSkillSyncOk: true,
},
{
permissions: {
accessibility: "granted",
screenRecording: "granted",
automation: "granted",
fullDiskAccess: "missing",
},
},
);
const permissionsHtml = renderBossAgentHtml(status, { activeTab: "permissions" });
assert.match(permissionsHtml, /class="active" href="\/boss-agent\?tab=permissions"/);
assert.match(permissionsHtml, /<h2>一次完整授权<\/h2>/);
assert.match(permissionsHtml, /完整接管待补齐/);
assert.match(permissionsHtml, /后续静默使用/);
assert.match(permissionsHtml, /api\/v1\/boss-agent\/permissions\/open\?target=all/);
assert.match(permissionsHtml, /api\/v1\/boss-agent\/permissions\/open\?target=fullDiskAccess/);
assert.doesNotMatch(permissionsHtml, /<h2>Skill 部署情况<\/h2>/);
const skillsHtml = renderBossAgentHtml(status, { activeTab: "skills" });
assert.match(skillsHtml, /class="active" href="\/boss-agent\?tab=skills"/);
assert.match(skillsHtml, /<h2>Skill 部署情况<\/h2>/);
assert.match(skillsHtml, /bb-browser/);
assert.doesNotMatch(skillsHtml, /<h2>一次完整授权<\/h2>/);
});
test("boss-agent unbound HTML renders a real scannable QR image when qrcode is available", async () => {
const status = buildBossAgentStatus(
{