From 7c371ed6445a3853cec7e5dc750b53ebb9a369a5 Mon Sep 17 00:00:00 2001 From: AI Bot Date: Tue, 12 May 2026 17:04:40 +0800 Subject: [PATCH] feat: add mac boss-agent desktop app --- .gitignore | 1 + README.md | 16 +- .../boss-agent-mac/Sources/BossAgentApp.swift | 77 +++ local-agent/boss-agent-status.mjs | 579 ++++++++++++++++++ local-agent/server.mjs | 37 +- package-lock.json | 295 +++++++-- package.json | 6 +- scripts/build-boss-agent-mac-app.sh | 59 ++ tests/boss-agent-status.test.mjs | 122 ++++ 9 files changed, 1144 insertions(+), 48 deletions(-) create mode 100644 apps/boss-agent-mac/Sources/BossAgentApp.swift create mode 100644 local-agent/boss-agent-status.mjs create mode 100644 scripts/build-boss-agent-mac-app.sh create mode 100644 tests/boss-agent-status.test.mjs 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 `${cells.join("")}`; +} + +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 + ? `Boss APP 绑定二维码` + : renderPseudoQrSvg(status.binding.qrPayload) + }
`; + + return ` + + + + + boss-agent + + + +
+ +
+
+
+

boss-agent

+
企业电脑接入端
+
+
${escapeHtml(status.server.ok ? `已连接 ${status.server.endpoint}` : "服务器未连接")}
+
+ +
+
+ ${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 绑定二维码/); +});