feat: add mac boss-agent desktop app
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,6 +19,7 @@
|
||||
|
||||
# production
|
||||
/build
|
||||
/dist/
|
||||
apps/boss-admin-web/dist/
|
||||
apps/boss-admin-web/node_modules/
|
||||
|
||||
|
||||
16
README.md
16
README.md
@@ -234,6 +234,20 @@ cd /Users/kris/code/boss
|
||||
./scripts/install-local-launchagent.sh /Users/kris/code/boss/local-agent/config.example.json
|
||||
```
|
||||
|
||||
构建 macOS 桌面状态应用 `boss-agent.app`:
|
||||
|
||||
```bash
|
||||
cd /Users/kris/code/boss
|
||||
npm run mac:agent
|
||||
open dist/boss-agent.app
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `boss-agent.app` 是本机 `local-agent` 的 macOS WebView 外壳,默认打开 `http://127.0.0.1:4317/boss-agent`
|
||||
- 未绑定账号时会显示可扫码的 Boss APP 绑定二维码;已绑定后显示账号、API、服务器、授权和本机电脑权限状态
|
||||
- 本机状态 JSON 可通过 `GET http://127.0.0.1:4317/api/v1/boss-agent/status` 查看,不会返回设备 token 明文
|
||||
|
||||
device-agent 当前职责:
|
||||
|
||||
- 上报设备状态、账号、5h/7d 额度和项目列表
|
||||
@@ -256,7 +270,7 @@ device-agent 当前职责:
|
||||
- Codex 项目/线程扫描当前已搬到 worker 线程执行,避免 `.codex/logs_1.sqlite` 和 `state_5.sqlite` 的同步扫描阻塞主线程 HTTP 响应
|
||||
- 如果某个历史群聊里已经没有真实线程成员,当前不会再表现成“发了没反应”,而是会在群里追加一条 `system_notice`,提示用户先重新整理群成员
|
||||
- 设备导入审核当前已经升级成 `local-agent -> codex exec -> complete` 的真实任务链;Web 和 Android 前台都会在 `pending_resolution` 阶段显示“主 Agent 审核中”并自动刷新,审核失败时保留当前勾选以便重新生成
|
||||
- 提供本地 `/health`、`/api/v1/device`、`/api/v1/skills`、`/api/v1/heartbeat`
|
||||
- 提供本地 `/boss-agent`、`/api/v1/boss-agent/status`、`/health`、`/api/v1/device`、`/api/v1/skills`、`/api/v1/heartbeat`
|
||||
|
||||
当前常驻默认值:
|
||||
|
||||
|
||||
77
apps/boss-agent-mac/Sources/BossAgentApp.swift
Normal file
77
apps/boss-agent-mac/Sources/BossAgentApp.swift
Normal file
@@ -0,0 +1,77 @@
|
||||
import Cocoa
|
||||
import WebKit
|
||||
|
||||
final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
|
||||
private var window: NSWindow?
|
||||
private var webView: WKWebView?
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
NSApp.setActivationPolicy(.regular)
|
||||
|
||||
let webConfiguration = WKWebViewConfiguration()
|
||||
let webView = WKWebView(frame: .zero, configuration: webConfiguration)
|
||||
webView.setValue(false, forKey: "drawsBackground")
|
||||
webView.navigationDelegate = self
|
||||
self.webView = webView
|
||||
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 1180, height: 780),
|
||||
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
window.title = "boss-agent"
|
||||
window.titlebarAppearsTransparent = true
|
||||
window.isMovableByWindowBackground = true
|
||||
window.contentView = webView
|
||||
window.center()
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
self.window = window
|
||||
|
||||
loadAgentPanel()
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
||||
private func loadAgentPanel() {
|
||||
guard let url = URL(string: "http://127.0.0.1:4317/boss-agent") else {
|
||||
loadFallback()
|
||||
return
|
||||
}
|
||||
webView?.load(URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData))
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
||||
loadFallback()
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
||||
loadFallback()
|
||||
}
|
||||
|
||||
private func loadFallback() {
|
||||
let html = """
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { margin:0; min-height:100vh; display:grid; place-items:center; background:#f6f8f5; font-family:-apple-system,BlinkMacSystemFont,'PingFang SC',sans-serif; color:#111418; }
|
||||
.card { width:520px; padding:32px; border-radius:24px; background:white; border:1px solid #e8ece9; box-shadow:0 24px 70px rgba(22,38,29,.12); }
|
||||
h1 { margin:0 0 10px; font-size:28px; letter-spacing:-.04em; }
|
||||
p { color:#707982; line-height:1.7; margin:0; }
|
||||
</style>
|
||||
<body>
|
||||
<section class="card">
|
||||
<h1>boss-agent 未启动</h1>
|
||||
<p>请先启动本机 local-agent 服务,然后重新打开 boss-agent。</p>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
webView?.loadHTMLString(html, baseURL: nil)
|
||||
}
|
||||
}
|
||||
|
||||
let app = NSApplication.shared
|
||||
let delegate = AppDelegate()
|
||||
app.delegate = delegate
|
||||
app.run()
|
||||
579
local-agent/boss-agent-status.mjs
Normal file
579
local-agent/boss-agent-status.mjs
Normal file
@@ -0,0 +1,579 @@
|
||||
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: "用于点击、输入和读取可访问控件",
|
||||
},
|
||||
{
|
||||
key: "screenRecording",
|
||||
label: "屏幕录制",
|
||||
description: "用于识别桌面画面和系统弹窗",
|
||||
},
|
||||
{
|
||||
key: "automation",
|
||||
label: "自动化控制",
|
||||
description: "用于控制 Finder、浏览器和企业软件",
|
||||
},
|
||||
];
|
||||
|
||||
function nonEmpty(value) {
|
||||
const text = String(value ?? "").trim();
|
||||
return text || undefined;
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
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 同步",
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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: PERMISSION_DEFS.every((item) => normalizePermissionStatus(permissions[item.key]) === "granted")
|
||||
? "本机权限正常"
|
||||
: "本机权限待处理",
|
||||
items: PERMISSION_DEFS.map((item) => ({
|
||||
...item,
|
||||
status: normalizePermissionStatus(permissions[item.key]),
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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 permissionRows(status) {
|
||||
return status.permissions.items
|
||||
.map((item) => {
|
||||
const tone = statusTone(item.status);
|
||||
return `<div class="permission-row">
|
||||
<div>
|
||||
<div class="permission-name">${escapeHtml(item.label)}</div>
|
||||
<div class="muted">${escapeHtml(item.description)}</div>
|
||||
</div>
|
||||
<span class="pill ${tone}">${escapeHtml(permissionText(item.status))}</span>
|
||||
</div>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
export function renderBossAgentHtml(status) {
|
||||
return renderBossAgentHtmlBase(status);
|
||||
}
|
||||
|
||||
export async function renderBossAgentHtmlWithQr(status) {
|
||||
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, { qrImageDataUrl });
|
||||
}
|
||||
|
||||
function renderBossAgentHtmlBase(status, options = {}) {
|
||||
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>`;
|
||||
|
||||
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: 218px 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: 30px; }
|
||||
.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; }
|
||||
.nav a {
|
||||
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; }
|
||||
.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 { 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; }
|
||||
.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; }
|
||||
.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>
|
||||
<nav class="nav">
|
||||
<a class="active" href="/">概览</a>
|
||||
<a href="/">绑定</a>
|
||||
<a href="/">授权</a>
|
||||
<a href="/">日志</a>
|
||||
</nav>
|
||||
</aside>
|
||||
<section class="content">
|
||||
<header class="topbar">
|
||||
<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="lower">
|
||||
<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">授权状态由 Boss 企业后台统一下发;未绑定时只显示本机预检结果。</div>
|
||||
</div>
|
||||
<div class="card panel">
|
||||
<h2>本机电脑权限状态</h2>
|
||||
<div class="rows">${permissionRows(status)}</div>
|
||||
<div class="hint">${escapeHtml(status.permissions.summary)}。如未授权,请在 macOS 系统设置里为 boss-agent / Node 运行时开启对应权限。</div>
|
||||
</div>
|
||||
</section>
|
||||
</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 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",
|
||||
};
|
||||
}
|
||||
@@ -38,6 +38,11 @@ import {
|
||||
resolveMasterAgentTaskTimeoutMs,
|
||||
runWithTaskTimeout,
|
||||
} from "./master-task-timeout.mjs";
|
||||
import {
|
||||
buildBossAgentStatus,
|
||||
detectLocalComputerPermissions,
|
||||
renderBossAgentHtmlWithQr,
|
||||
} from "./boss-agent-status.mjs";
|
||||
import {
|
||||
buildComputerUseCompletionPayload,
|
||||
buildMasterAgentTaskCompletionRequestBody,
|
||||
@@ -1000,7 +1005,31 @@ const skillLifecyclePoll = createSerializedRunner(async () => {
|
||||
});
|
||||
|
||||
const server = createServer(async (request, response) => {
|
||||
if (request.url === "/health") {
|
||||
const requestUrl = new URL(request.url || "/", `http://${config.bindHost || "127.0.0.1"}`);
|
||||
|
||||
if (requestUrl.pathname === "/" || requestUrl.pathname === "/boss-agent") {
|
||||
const permissions = await detectLocalComputerPermissions();
|
||||
const status = buildBossAgentStatus(config, runtime, { permissions });
|
||||
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
||||
response.end(await renderBossAgentHtmlWithQr(status));
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestUrl.pathname === "/favicon.ico") {
|
||||
response.writeHead(204);
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestUrl.pathname === "/api/v1/boss-agent/status") {
|
||||
const permissions = await detectLocalComputerPermissions();
|
||||
const status = buildBossAgentStatus(config, runtime, { permissions });
|
||||
response.writeHead(200, { "Content-Type": "application/json" });
|
||||
response.end(JSON.stringify({ ok: true, status }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestUrl.pathname === "/health") {
|
||||
response.writeHead(200, { "Content-Type": "application/json" });
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
@@ -1012,13 +1041,13 @@ const server = createServer(async (request, response) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.url === "/api/v1/device") {
|
||||
if (requestUrl.pathname === "/api/v1/device") {
|
||||
response.writeHead(200, { "Content-Type": "application/json" });
|
||||
response.end(JSON.stringify({ config, runtime }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.url === "/api/v1/skills") {
|
||||
if (requestUrl.pathname === "/api/v1/skills") {
|
||||
response.writeHead(200, { "Content-Type": "application/json" });
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
@@ -1036,7 +1065,7 @@ const server = createServer(async (request, response) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.url === "/api/v1/heartbeat" && request.method === "POST") {
|
||||
if (requestUrl.pathname === "/api/v1/heartbeat" && request.method === "POST") {
|
||||
await heartbeat();
|
||||
response.writeHead(200, { "Content-Type": "application/json" });
|
||||
response.end(JSON.stringify({ ok: runtime.lastHeartbeatOk, runtime }));
|
||||
|
||||
295
package-lock.json
generated
295
package-lock.json
generated
@@ -20,8 +20,9 @@
|
||||
"antd": "^5.29.3",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.20",
|
||||
"next": "16.2.4",
|
||||
"next": "16.2.6",
|
||||
"pg": "^8.20.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
},
|
||||
@@ -1837,9 +1838,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "16.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.4.tgz",
|
||||
"integrity": "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==",
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz",
|
||||
"integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next": {
|
||||
@@ -1853,9 +1854,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "16.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.4.tgz",
|
||||
"integrity": "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==",
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz",
|
||||
"integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1869,9 +1870,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "16.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.4.tgz",
|
||||
"integrity": "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==",
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz",
|
||||
"integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1885,9 +1886,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "16.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.4.tgz",
|
||||
"integrity": "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==",
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz",
|
||||
"integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1901,9 +1902,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "16.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.4.tgz",
|
||||
"integrity": "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==",
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz",
|
||||
"integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1917,9 +1918,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "16.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.4.tgz",
|
||||
"integrity": "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==",
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz",
|
||||
"integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1933,9 +1934,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "16.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.4.tgz",
|
||||
"integrity": "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==",
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz",
|
||||
"integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1949,9 +1950,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "16.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.4.tgz",
|
||||
"integrity": "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==",
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz",
|
||||
"integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1965,9 +1966,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "16.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.4.tgz",
|
||||
"integrity": "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==",
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz",
|
||||
"integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3869,6 +3870,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001781",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz",
|
||||
@@ -3927,6 +3937,31 @@
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
@@ -4096,6 +4131,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
@@ -4158,6 +4202,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||
@@ -5152,6 +5202,15 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
@@ -6644,12 +6703,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "16.2.4",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-16.2.4.tgz",
|
||||
"integrity": "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==",
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz",
|
||||
"integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "16.2.4",
|
||||
"@next/env": "16.2.6",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"baseline-browser-mapping": "^2.9.19",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
@@ -6663,14 +6722,14 @@
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "16.2.4",
|
||||
"@next/swc-darwin-x64": "16.2.4",
|
||||
"@next/swc-linux-arm64-gnu": "16.2.4",
|
||||
"@next/swc-linux-arm64-musl": "16.2.4",
|
||||
"@next/swc-linux-x64-gnu": "16.2.4",
|
||||
"@next/swc-linux-x64-musl": "16.2.4",
|
||||
"@next/swc-win32-arm64-msvc": "16.2.4",
|
||||
"@next/swc-win32-x64-msvc": "16.2.4",
|
||||
"@next/swc-darwin-arm64": "16.2.6",
|
||||
"@next/swc-darwin-x64": "16.2.6",
|
||||
"@next/swc-linux-arm64-gnu": "16.2.6",
|
||||
"@next/swc-linux-arm64-musl": "16.2.6",
|
||||
"@next/swc-linux-x64-gnu": "16.2.6",
|
||||
"@next/swc-linux-x64-musl": "16.2.6",
|
||||
"@next/swc-win32-arm64-msvc": "16.2.6",
|
||||
"@next/swc-win32-x64-msvc": "16.2.6",
|
||||
"sharp": "^0.34.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -6929,6 +6988,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
@@ -6958,7 +7026,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -7142,6 +7209,15 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/possible-typed-array-names": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||
@@ -7273,6 +7349,23 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dijkstrajs": "^1.0.1",
|
||||
"pngjs": "^5.0.0",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"qrcode": "bin/qrcode"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.15.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
|
||||
@@ -8007,6 +8100,21 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/resize-observer-polyfill": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||
@@ -8214,6 +8322,12 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
@@ -9273,6 +9387,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/which-typed-array": {
|
||||
"version": "1.1.20",
|
||||
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
|
||||
@@ -9362,6 +9482,12 @@
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
@@ -9369,6 +9495,93 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yauzl": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"test:master-agent-controls": "tsx --test tests/master-agent-chat-controls.test.ts",
|
||||
"apk:debug": "cd android && ./gradlew assembleDebug && cd .. && zsh ./scripts/publish-apk-to-public.sh",
|
||||
"apk:release": "zsh ./scripts/build-release-apk.sh",
|
||||
"aab:release": "zsh ./scripts/build-release-aab.sh"
|
||||
"aab:release": "zsh ./scripts/build-release-aab.sh",
|
||||
"mac:agent": "zsh ./scripts/build-boss-agent-mac-app.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.1.1",
|
||||
@@ -32,8 +33,9 @@
|
||||
"antd": "^5.29.3",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.20",
|
||||
"next": "16.2.4",
|
||||
"next": "16.2.6",
|
||||
"pg": "^8.20.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
},
|
||||
|
||||
59
scripts/build-boss-agent-mac-app.sh
Normal file
59
scripts/build-boss-agent-mac-app.sh
Normal file
@@ -0,0 +1,59 @@
|
||||
#!/bin/zsh
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
APP_DIR="$ROOT_DIR/dist/boss-agent.app"
|
||||
CONTENTS_DIR="$APP_DIR/Contents"
|
||||
MACOS_DIR="$CONTENTS_DIR/MacOS"
|
||||
RESOURCES_DIR="$CONTENTS_DIR/Resources"
|
||||
SOURCE_FILE="$ROOT_DIR/apps/boss-agent-mac/Sources/BossAgentApp.swift"
|
||||
BINARY_PATH="$MACOS_DIR/boss-agent"
|
||||
|
||||
if ! command -v swiftc >/dev/null 2>&1; then
|
||||
echo "swiftc not found. Install Xcode Command Line Tools first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf "$APP_DIR"
|
||||
mkdir -p "$MACOS_DIR" "$RESOURCES_DIR"
|
||||
|
||||
swiftc "$SOURCE_FILE" \
|
||||
-o "$BINARY_PATH" \
|
||||
-framework Cocoa \
|
||||
-framework WebKit
|
||||
|
||||
chmod +x "$BINARY_PATH"
|
||||
|
||||
cat > "$CONTENTS_DIR/Info.plist" <<'PLIST'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>zh_CN</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>boss-agent</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.hyzq.boss.agent</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>boss-agent</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>boss-agent</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>13.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
|
||||
plutil -lint "$CONTENTS_DIR/Info.plist" >/dev/null
|
||||
echo "$APP_DIR"
|
||||
122
tests/boss-agent-status.test.mjs
Normal file
122
tests/boss-agent-status.test.mjs
Normal file
@@ -0,0 +1,122 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
buildBossAgentStatus,
|
||||
renderBossAgentHtml,
|
||||
renderBossAgentHtmlWithQr,
|
||||
} from "../local-agent/boss-agent-status.mjs";
|
||||
|
||||
test("boss-agent status exposes unbound QR binding and local permission states", () => {
|
||||
const status = buildBossAgentStatus(
|
||||
{
|
||||
deviceId: "macbook-air",
|
||||
name: "MacBook Air",
|
||||
avatar: "A",
|
||||
account: "",
|
||||
controlPlaneUrl: "https://boss.hyzq.net",
|
||||
pairingCode: "482913",
|
||||
token: "",
|
||||
primaryApiLabel: "DeepSeek V4",
|
||||
backupApiLabel: "未启用",
|
||||
licenseExpiresAt: "2027-05-12T00:00:00.000Z",
|
||||
},
|
||||
{
|
||||
lastHeartbeatOk: true,
|
||||
lastHeartbeatStatus: 200,
|
||||
lastHeartbeatAt: "2026-05-12T05:00:00.000Z",
|
||||
},
|
||||
{
|
||||
permissions: {
|
||||
accessibility: "granted",
|
||||
screenRecording: "missing",
|
||||
automation: "unknown",
|
||||
},
|
||||
now: "2026-05-12T06:00:00.000Z",
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(status.appName, "boss-agent");
|
||||
assert.equal(status.binding.bound, false);
|
||||
assert.equal(status.binding.qrPayload.includes("pairingCode=482913"), true);
|
||||
assert.equal(status.server.ok, true);
|
||||
assert.equal(status.api.primary, "DeepSeek V4");
|
||||
assert.equal(status.api.backup, "未启用");
|
||||
assert.equal(status.license.status, "pending_binding");
|
||||
assert.deepEqual(
|
||||
status.permissions.items.map((item) => [item.key, item.status]),
|
||||
[
|
||||
["accessibility", "granted"],
|
||||
["screenRecording", "missing"],
|
||||
["automation", "unknown"],
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test("boss-agent status treats token-backed devices as bound and renders enterprise UI", () => {
|
||||
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",
|
||||
backupApiLabel: "OpenAI 备用",
|
||||
license: {
|
||||
enterpriseName: "默认公司",
|
||||
status: "valid",
|
||||
expiresAt: "2027-05-12T00:00:00.000Z",
|
||||
},
|
||||
},
|
||||
{
|
||||
lastHeartbeatOk: true,
|
||||
lastHeartbeatStatus: 200,
|
||||
lastHeartbeatAt: "2026-05-12T05:00:00.000Z",
|
||||
},
|
||||
{
|
||||
permissions: {
|
||||
accessibility: "granted",
|
||||
screenRecording: "granted",
|
||||
automation: "granted",
|
||||
},
|
||||
now: "2026-05-12T06:00:00.000Z",
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(status.binding.bound, true);
|
||||
assert.equal(status.binding.account, "krisolo");
|
||||
assert.equal(status.license.status, "valid");
|
||||
assert.equal(status.license.enterpriseName, "默认公司");
|
||||
|
||||
const html = renderBossAgentHtml(status);
|
||||
assert.match(html, /boss-agent/);
|
||||
assert.match(html, /企业电脑接入端/);
|
||||
assert.match(html, /本机电脑权限状态/);
|
||||
assert.match(html, /DeepSeek V4/);
|
||||
assert.match(html, /默认公司/);
|
||||
assert.doesNotMatch(html, /boss-secret-token/);
|
||||
});
|
||||
|
||||
test("boss-agent unbound HTML renders a real scannable QR image when qrcode is available", async () => {
|
||||
const status = buildBossAgentStatus(
|
||||
{
|
||||
deviceId: "macbook-air",
|
||||
name: "MacBook Air",
|
||||
controlPlaneUrl: "https://boss.hyzq.net",
|
||||
pairingCode: "482913",
|
||||
},
|
||||
{
|
||||
lastHeartbeatOk: true,
|
||||
lastHeartbeatStatus: 200,
|
||||
},
|
||||
{
|
||||
permissions: {},
|
||||
},
|
||||
);
|
||||
|
||||
const html = await renderBossAgentHtmlWithQr(status);
|
||||
assert.match(html, /data:image\/png;base64/);
|
||||
assert.match(html, /Boss APP 绑定二维码/);
|
||||
});
|
||||
Reference in New Issue
Block a user