diff --git a/.gitignore b/.gitignore
index 809df85..a1b54b0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,7 @@
# production
/build
+/dist/
apps/boss-admin-web/dist/
apps/boss-admin-web/node_modules/
diff --git a/README.md b/README.md
index 2364bc3..53f08ef 100644
--- a/README.md
+++ b/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`
当前常驻默认值:
diff --git a/apps/boss-agent-mac/Sources/BossAgentApp.swift b/apps/boss-agent-mac/Sources/BossAgentApp.swift
new file mode 100644
index 0000000..5456a13
--- /dev/null
+++ b/apps/boss-agent-mac/Sources/BossAgentApp.swift
@@ -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 = """
+
+
+
+
+
+
+ boss-agent 未启动
+ 请先启动本机 local-agent 服务,然后重新打开 boss-agent。
+
+
+
+ """
+ webView?.loadHTMLString(html, baseURL: nil)
+ }
+}
+
+let app = NSApplication.shared
+let delegate = AppDelegate()
+app.delegate = delegate
+app.run()
diff --git a/local-agent/boss-agent-status.mjs b/local-agent/boss-agent-status.mjs
new file mode 100644
index 0000000..6c37b62
--- /dev/null
+++ b/local-agent/boss-agent-status.mjs
@@ -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(``);
+ }
+ }
+ }
+ return ``;
+}
+
+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 `
+
+
${escapeHtml(item.label)}
+
${escapeHtml(item.description)}
+
+
${escapeHtml(permissionText(item.status))}
+
`;
+ })
+ .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
+ ? `${escapeHtml(status.device.avatar)}
`
+ : `${
+ options.qrImageDataUrl
+ ? `
})
`
+ : renderPseudoQrSvg(status.binding.qrPayload)
+ }
`;
+
+ return `
+
+
+
+
+ boss-agent
+
+
+
+
+
+
+
+
+
+
+ ${qrBlock}
+
+
${escapeHtml(heroTitle)}
+
${escapeHtml(heroSubtitle)}
+
+
+ ${escapeHtml(bound ? `账号:${status.binding.account}` : status.binding.qrExpiresInLabel)}
+
+
+
+
+
绑定状态
+
+
账号${escapeHtml(status.binding.account)}
+
设备${escapeHtml(status.device.name)}
+
角色${escapeHtml(status.device.role)}
+
设备 ID${escapeHtml(status.device.id)}
+
+
+
+
+
+
+
账号登录状态
+
${escapeHtml(bound ? "已绑定" : "等待绑定")}
+
${escapeHtml(bound ? status.binding.account : "使用 Boss APP 扫码完成绑定")}
+
+
+
当前 API 使用情况
+
${escapeHtml(status.api.primary)}
+
备用 API:${escapeHtml(status.api.backup)}
+
+
+
服务器连接
+
${escapeHtml(status.server.ok ? "正常" : "异常")}
+
${escapeHtml(status.server.latencyLabel)}
+
+
+
正版授权
+
${escapeHtml(status.license.label)}
+
到期:${escapeHtml(status.license.expiresAtLabel)}
+
+
+
+
+
+
授权信息
+
+
企业${escapeHtml(status.license.enterpriseName)}
+
授权到期${escapeHtml(status.license.expiresAtLabel)}
+
权限范围${escapeHtml(status.license.scope)}
+
+
授权状态由 Boss 企业后台统一下发;未绑定时只显示本机预检结果。
+
+
+
本机电脑权限状态
+
${permissionRows(status)}
+
${escapeHtml(status.permissions.summary)}。如未授权,请在 macOS 系统设置里为 boss-agent / Node 运行时开启对应权限。
+
+
+
+
+
+`;
+}
+
+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",
+ };
+}
diff --git a/local-agent/server.mjs b/local-agent/server.mjs
index 2ba0f1e..a372d62 100755
--- a/local-agent/server.mjs
+++ b/local-agent/server.mjs
@@ -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 }));
diff --git a/package-lock.json b/package-lock.json
index 5f4ee3a..6f7a380 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 55775f4..c570059 100644
--- a/package.json
+++ b/package.json
@@ -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"
},
diff --git a/scripts/build-boss-agent-mac-app.sh b/scripts/build-boss-agent-mac-app.sh
new file mode 100644
index 0000000..c74b0a8
--- /dev/null
+++ b/scripts/build-boss-agent-mac-app.sh
@@ -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'
+
+
+
+
+ CFBundleDevelopmentRegion
+ zh_CN
+ CFBundleExecutable
+ boss-agent
+ CFBundleIdentifier
+ com.hyzq.boss.agent
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ boss-agent
+ CFBundleDisplayName
+ boss-agent
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 0.1.0
+ CFBundleVersion
+ 1
+ LSMinimumSystemVersion
+ 13.0
+ NSHighResolutionCapable
+
+
+
+PLIST
+
+plutil -lint "$CONTENTS_DIR/Info.plist" >/dev/null
+echo "$APP_DIR"
diff --git a/tests/boss-agent-status.test.mjs b/tests/boss-agent-status.test.mjs
new file mode 100644
index 0000000..816b43e
--- /dev/null
+++ b/tests/boss-agent-status.test.mjs
@@ -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 绑定二维码/);
+});