fix: sync native agent permission states

This commit is contained in:
AI Bot
2026-05-13 00:18:19 +08:00
parent 2ff75087b3
commit 73327be8b0
4 changed files with 191 additions and 16 deletions

View File

@@ -14,9 +14,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
private var webView: WKWebView?
private var inputMonitoringTap: CFMachPort?
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
)
let webConfiguration = WKWebViewConfiguration()
let webView = WKWebView(frame: .zero, configuration: webConfiguration)
@@ -38,18 +45,29 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
window.makeKeyAndOrderFront(nil)
self.window = window
loadAgentPanel()
loadAgentPanel(tab: activeTab)
NSApp.activate(ignoringOtherApps: true)
}
private func loadAgentPanel() {
guard let url = URL(string: "http://127.0.0.1:4317/boss-agent") else {
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)
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
loadFallback()
}
@@ -74,6 +92,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
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)
}
@@ -81,12 +109,38 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
url.path == "/api/v1/boss-agent/permissions/open"
}
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) {
let target =
URLComponents(url: url, resolvingAgainstBaseURL: false)?
.queryItems?
.first(where: { $0.name == "target" })?
.value ?? "all"
queryValue("target", in: url) ?? "all"
let returnTab = normalizedTab(queryValue("returnTab", in: url) ?? activeTab)
activeTab = returnTab
requestNativePermission(for: target)
if let settingsUrl = systemSettingsUrl(for: target) {
@@ -94,7 +148,49 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { [weak self] in
self?.loadAgentPanel()
self?.loadAgentPanel(tab: returnTab)
}
}
private func nativePermissionQueryItems() -> [URLQueryItem] {
[
URLQueryItem(name: "nativeAccessibility", value: AXIsProcessTrusted() ? "granted" : "missing"),
URLQueryItem(name: "nativeScreenRecording", value: screenRecordingStatus()),
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"
}
}
@@ -133,9 +229,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
case "notifications":
requestNotificationPermission()
case "microphone":
AVCaptureDevice.requestAccess(for: .audio) { _ in }
requestMicrophonePermission()
case "camera":
AVCaptureDevice.requestAccess(for: .video) { _ in }
requestCameraPermission()
case "localNetwork":
requestLocalNetworkPermission()
default:
@@ -149,6 +245,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate {
_ = 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()