import Cocoa import WebKit import ApplicationServices import AVFoundation import IOKit.hid import Network import UserNotifications private let bossInputMonitoringTapCallback: CGEventTapCallBack = { _, _, event, _ in Unmanaged.passUnretained(event) } final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate { private var window: NSWindow? private var webView: WKWebView? private var inputMonitoringTap: CFMachPort? private var globalKeyMonitor: Any? private var inputMonitoringManager: IOHIDManager? private var localNetworkBrowser: NWBrowser? private var activeTab = "overview" func applicationDidFinishLaunching(_ notification: Notification) { NSApp.setActivationPolicy(.regular) NotificationCenter.default.addObserver( self, selector: #selector(handleApplicationDidBecomeActive), name: NSApplication.didBecomeActiveNotification, object: nil ) NSAppleEventManager.shared().setEventHandler( self, andSelector: #selector(handleGetUrlEvent(_:withReplyEvent:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL) ) 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(tab: activeTab) NSApp.activate(ignoringOtherApps: true) handleLaunchPermissionRequestIfNeeded() } private func loadAgentPanel(tab: String? = nil) { activeTab = normalizedTab(tab ?? activeTab) var components = URLComponents() components.scheme = "http" components.host = "127.0.0.1" components.port = 4317 components.path = "/boss-agent" components.queryItems = [URLQueryItem(name: "tab", value: activeTab)] + nativePermissionQueryItems() guard let url = components.url else { loadFallback() return } webView?.load(URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData)) } @objc private func handleApplicationDidBecomeActive() { loadAgentPanel(tab: activeTab) } private func handleLaunchPermissionRequestIfNeeded() { let arguments = CommandLine.arguments guard let targetIndex = arguments.firstIndex(of: "--request-permission"), arguments.indices.contains(targetIndex + 1) else { return } let target = arguments[targetIndex + 1] let returnTab = commandLineValue(after: "--return-tab", in: arguments) ?? "permissions" DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in self?.handlePermissionTarget(target, returnTab: returnTab) } } private func commandLineValue(after flag: String, in arguments: [String]) -> String? { guard let index = arguments.firstIndex(of: flag), arguments.indices.contains(index + 1) else { return nil } return arguments[index + 1] } @objc private func handleGetUrlEvent(_ event: NSAppleEventDescriptor, withReplyEvent replyEvent: NSAppleEventDescriptor) { guard let urlString = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue, let url = URL(string: urlString), isBossAgentDeepLink(url) else { return } handleBossAgentDeepLink(url) } func application(_ application: NSApplication, open urls: [URL]) { for url in urls where isBossAgentDeepLink(url) { handleBossAgentDeepLink(url) } } func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { loadFallback() } func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { loadFallback() } func webView( _ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void ) { guard let url = navigationAction.request.url else { decisionHandler(.allow) return } if isPermissionSetupUrl(url) { decisionHandler(.cancel) handlePermissionSetupNavigation(url) return } if isAgentPanelUrl(url) && !hasNativePermissionQuery(url) { decisionHandler(.cancel) loadAgentPanel(tab: queryValue("tab", in: url)) return } if isAgentPanelUrl(url), let tab = queryValue("tab", in: url) { activeTab = normalizedTab(tab) } decisionHandler(.allow) } private func isPermissionSetupUrl(_ url: URL) -> Bool { url.path == "/api/v1/boss-agent/permissions/open" } private func isBossAgentDeepLink(_ url: URL) -> Bool { url.scheme == "boss-agent" } private func handleBossAgentDeepLink(_ url: URL) { if url.host == "permissions" && url.path == "/open" { handlePermissionTarget( queryValue("target", in: url) ?? "all", returnTab: queryValue("returnTab", in: url) ?? "permissions" ) return } if url.host == "tab" { loadAgentPanel(tab: String(url.path.dropFirst())) } } private func isAgentPanelUrl(_ url: URL) -> Bool { let host = url.host ?? "" return (host == "127.0.0.1" || host == "localhost") && url.port == 4317 && (url.path == "/boss-agent" || url.path == "/") } private func hasNativePermissionQuery(_ url: URL) -> Bool { URLComponents(url: url, resolvingAgainstBaseURL: false)? .queryItems? .contains(where: { $0.name.hasPrefix("native") }) == true } private func queryValue(_ name: String, in url: URL) -> String? { URLComponents(url: url, resolvingAgainstBaseURL: false)? .queryItems? .first(where: { $0.name == name })? .value } private func normalizedTab(_ value: String?) -> String { switch value { case "permissions", "skills", "license", "logs", "overview": return value ?? "overview" default: return "overview" } } private func handlePermissionSetupNavigation(_ url: URL) { handlePermissionTarget( queryValue("target", in: url) ?? "all", returnTab: queryValue("returnTab", in: url) ?? activeTab ) } private func handlePermissionTarget(_ target: String, returnTab rawReturnTab: String) { let returnTab = normalizedTab(rawReturnTab) activeTab = returnTab UserDefaults.standard.set(target, forKey: "lastPermissionRequestTarget") UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "lastPermissionRequestAt") NSLog("boss-agent permission request target=%@ returnTab=%@", target, returnTab) NSApp.activate(ignoringOtherApps: true) requestNativePermission(for: target) DispatchQueue.main.asyncAfter(deadline: .now() + 0.45) { [weak self] in if let settingsUrl = self?.systemSettingsUrl(for: target) { NSWorkspace.shared.open(settingsUrl) } } DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { [weak self] in self?.loadAgentPanel(tab: returnTab) } } private func nativePermissionQueryItems() -> [URLQueryItem] { [ URLQueryItem(name: "nativeAccessibility", value: AXIsProcessTrusted() ? "granted" : "missing"), URLQueryItem(name: "nativeScreenRecording", value: screenRecordingStatus()), URLQueryItem(name: "nativeInputMonitoring", value: inputMonitoringStatus()), URLQueryItem(name: "nativeMicrophone", value: microphonePermissionStatus()), URLQueryItem(name: "nativeCamera", value: cameraPermissionStatus()), ] } private func screenRecordingStatus() -> String { if #available(macOS 10.15, *) { return CGPreflightScreenCaptureAccess() ? "granted" : "missing" } return "unknown" } private func microphonePermissionStatus() -> String { switch AVCaptureDevice.authorizationStatus(for: .audio) { case .authorized: return "granted" case .denied, .restricted: return "missing" case .notDetermined: return "unknown" @unknown default: return "unknown" } } private func cameraPermissionStatus() -> String { switch AVCaptureDevice.authorizationStatus(for: .video) { case .authorized: return "granted" case .denied, .restricted: return "missing" case .notDetermined: return "unknown" @unknown default: return "unknown" } } private func inputMonitoringStatus() -> String { if #available(macOS 10.15, *) { return CGPreflightListenEventAccess() ? "granted" : "missing" } switch IOHIDCheckAccess(kIOHIDRequestTypeListenEvent) { case kIOHIDAccessTypeGranted: return "granted" case kIOHIDAccessTypeDenied: return "missing" default: return "unknown" } } private func requestNativePermission(for target: String) { let targets: [String] if target == "core" { targets = ["accessibility", "screenRecording"] } else if target == "all" { targets = [ "accessibility", "screenRecording", "automation", "fullDiskAccess", "inputMonitoring", "notifications", "microphone", "camera", "localNetwork", ] } else { targets = [target] } for permission in targets { requestSingleNativePermission(permission) } } private func requestSingleNativePermission(_ permission: String) { switch permission { case "accessibility": requestAccessibilityPermission() case "screenRecording": requestScreenRecordingPermission() case "automation": requestAutomationPermission() case "fullDiskAccess": preflightFullDiskAccess() case "inputMonitoring": requestInputMonitoringPermission() case "notifications": requestNotificationPermission() case "microphone": requestMicrophonePermission() case "camera": requestCameraPermission() case "localNetwork": requestLocalNetworkPermission() default: break } } private func requestAccessibilityPermission() { let promptKey = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String let options = [promptKey: true] as CFDictionary _ = AXIsProcessTrustedWithOptions(options) } private func requestMicrophonePermission() { if AVCaptureDevice.authorizationStatus(for: .audio) == .notDetermined { AVCaptureDevice.requestAccess(for: .audio) { _ in } } } private func requestCameraPermission() { if AVCaptureDevice.authorizationStatus(for: .video) == .notDetermined { AVCaptureDevice.requestAccess(for: .video) { _ in } } } private func requestScreenRecordingPermission() { if #available(macOS 10.15, *) { _ = CGRequestScreenCaptureAccess() } else { _ = CGPreflightScreenCaptureAccess() } } private func requestAutomationPermission() { let script = NSAppleScript(source: "tell application \"Finder\" to get name") var error: NSDictionary? _ = script?.executeAndReturnError(&error) } private func preflightFullDiskAccess() { let candidatePaths = [ "\(NSHomeDirectory())/Library/Safari/Bookmarks.plist", "\(NSHomeDirectory())/Library/Mail", "/Library/Application Support/com.apple.TCC/TCC.db", ] for path in candidatePaths { if FileManager.default.fileExists(atPath: path) { _ = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) } } } private func requestInputMonitoringPermission() { var listenRequestResult = false if #available(macOS 10.15, *) { listenRequestResult = CGRequestListenEventAccess() } else { listenRequestResult = IOHIDRequestAccess(kIOHIDRequestTypeListenEvent) } NSLog("boss-agent input monitoring listen request result=%@", listenRequestResult ? "true" : "false") if globalKeyMonitor == nil { globalKeyMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.keyDown]) { _ in } } preflightKeyboardStateRead() primeInputMonitoringHidPath() let eventMask = CGEventMask(1 << CGEventType.keyDown.rawValue) inputMonitoringTap = CGEvent.tapCreate( tap: .cgSessionEventTap, place: .headInsertEventTap, options: .listenOnly, eventsOfInterest: eventMask, callback: bossInputMonitoringTapCallback, userInfo: nil ) if let tap = inputMonitoringTap { CFMachPortInvalidate(tap) inputMonitoringTap = nil } } private func preflightKeyboardStateRead() { for keyCode in [CGKeyCode(0), CGKeyCode(1), CGKeyCode(49), CGKeyCode(53)] { _ = CGEventSource.keyState(.combinedSessionState, key: keyCode) } } private func primeInputMonitoringHidPath() { let manager = IOHIDManagerCreate(kCFAllocatorDefault, IOOptionBits(kIOHIDOptionsTypeNone)) let keyboardMatching = [ kIOHIDDeviceUsagePageKey as String: kHIDPage_GenericDesktop, kIOHIDDeviceUsageKey as String: kHIDUsage_GD_Keyboard, ] as CFDictionary IOHIDManagerSetDeviceMatching(manager, keyboardMatching) let openResult = IOHIDManagerOpen(manager, IOOptionBits(kIOHIDOptionsTypeNone)) NSLog("boss-agent input monitoring hid open result=%d", openResult) inputMonitoringManager = manager } private func requestNotificationPermission() { UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { _, _ in } } private func requestLocalNetworkPermission() { let parameters = NWParameters.tcp parameters.includePeerToPeer = true let browser = NWBrowser(for: .bonjour(type: "_http._tcp", domain: nil), using: parameters) browser.stateUpdateHandler = { _ in } localNetworkBrowser = browser browser.start(queue: .main) DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { [weak self] in self?.localNetworkBrowser?.cancel() self?.localNetworkBrowser = nil } } private func systemSettingsUrl(for target: String) -> URL? { let mapping = [ "all": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Security", "core": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Accessibility", "accessibility": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Accessibility", "screenRecording": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_ScreenCapture", "automation": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Automation", "fullDiskAccess": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_AllFiles", "inputMonitoring": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_ListenEvent", "notifications": "x-apple.systempreferences:com.apple.Notifications-Settings.extension", "microphone": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Microphone", "camera": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Camera", "localNetwork": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?privacy-localnetwork", ] return URL(string: mapping[target] ?? mapping["all"]!) } private func loadFallback() { let html = """
请先启动本机 local-agent 服务,然后重新打开 boss-agent。