import Cocoa import Darwin import FlutterMacOS @main class AppDelegate: FlutterAppDelegate { private let skillDirectoryChannelName = "plus.svc.xworkmate/skill_directory_access" private var directoryAccessSessions: [String: URL] = [:] private var skillDirectoryChannel: FlutterMethodChannel? private var skillDirectoryMessengerId: ObjectIdentifier? override func applicationDidFinishLaunching(_ notification: Notification) { super.applicationDidFinishLaunching(notification) guard let controller = mainFlutterWindow?.contentViewController as? FlutterViewController else { return } registerSkillDirectoryChannel(for: controller) } override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { return true } override func applicationWillTerminate(_ notification: Notification) { for (_, url) in directoryAccessSessions { url.stopAccessingSecurityScopedResource() } directoryAccessSessions.removeAll() super.applicationWillTerminate(notification) } func registerSkillDirectoryChannel(for controller: FlutterViewController) { let messengerObject = controller.engine.binaryMessenger as AnyObject let messengerId = ObjectIdentifier(messengerObject) if skillDirectoryMessengerId == messengerId { return } let channel = FlutterMethodChannel( name: skillDirectoryChannelName, binaryMessenger: controller.engine.binaryMessenger ) channel.setMethodCallHandler { [weak self] call, result in self?.handleSkillDirectoryCall(call, result: result) } skillDirectoryChannel = channel skillDirectoryMessengerId = messengerId } private func handleSkillDirectoryCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { case "resolveUserHomeDirectory": result(resolveUserHomeDirectoryPath()) case "authorizeDirectory": authorizeDirectory(call, result: result) case "authorizeDirectories": authorizeDirectories(call, result: result) case "startDirectoryAccess": startDirectoryAccess(call, result: result) case "stopDirectoryAccess": stopDirectoryAccess(call, result: result) default: result(FlutterMethodNotImplemented) } } private func authorizeDirectory(_ call: FlutterMethodCall, result: @escaping FlutterResult) { let arguments = call.arguments as? [String: Any] let suggestedPath = (arguments?["suggestedPath"] as? String ?? "").trimmingCharacters(in: .whitespacesAndNewlines) let panel = buildAuthorizationPanel( title: "授权技能目录", message: "请选择要授予 XWorkmate 只读访问权限的技能目录。", allowsMultipleSelection: false, suggestedPath: suggestedPath ) guard panel.runModal() == .OK, let selectedURL = panel.url else { result(nil) return } do { result(try authorizationPayload(for: selectedURL)) } catch { result( FlutterError( code: "bookmark_create_failed", message: error.localizedDescription, details: nil ) ) } } private func authorizeDirectories(_ call: FlutterMethodCall, result: @escaping FlutterResult) { let arguments = call.arguments as? [String: Any] let suggestedPaths = (arguments?["suggestedPaths"] as? [String] ?? []) .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } let panel = buildAuthorizationPanel( title: "批量授权技能目录", message: "请选择要授予 XWorkmate 只读访问权限的一个或多个技能目录。", allowsMultipleSelection: true, suggestedPath: suggestedPaths.first ?? "" ) guard panel.runModal() == .OK else { result([]) return } do { let payload = try panel.urls.map(authorizationPayload(for:)) result(payload) } catch { result( FlutterError( code: "bookmark_create_failed", message: error.localizedDescription, details: nil ) ) } } private func buildAuthorizationPanel( title: String, message: String, allowsMultipleSelection: Bool, suggestedPath: String ) -> NSOpenPanel { let panel = NSOpenPanel() panel.title = title panel.message = message panel.prompt = "授权" panel.canChooseDirectories = true panel.canChooseFiles = false panel.allowsMultipleSelection = allowsMultipleSelection panel.canCreateDirectories = false panel.resolvesAliases = true panel.showsHiddenFiles = true if let initialURL = initialDirectoryURL(for: suggestedPath) { panel.directoryURL = initialURL } return panel } private func authorizationPayload(for selectedURL: URL) throws -> [String: Any] { let resolvedURL = selectedURL.standardizedFileURL let bookmarkData = try resolvedURL.bookmarkData( options: [.withSecurityScope], includingResourceValuesForKeys: nil, relativeTo: nil ) return [ "path": resolvedURL.path, "bookmark": bookmarkData.base64EncodedString(), ] } private func startDirectoryAccess(_ call: FlutterMethodCall, result: @escaping FlutterResult) { let arguments = call.arguments as? [String: Any] let bookmark = (arguments?["bookmark"] as? String ?? "").trimmingCharacters(in: .whitespacesAndNewlines) guard !bookmark.isEmpty, let bookmarkData = Data(base64Encoded: bookmark) else { result( FlutterError( code: "invalid_bookmark", message: "Missing directory bookmark.", details: nil ) ) return } do { var isStale = false let url = try URL( resolvingBookmarkData: bookmarkData, options: [.withSecurityScope], relativeTo: nil, bookmarkDataIsStale: &isStale ) guard url.startAccessingSecurityScopedResource() else { result( FlutterError( code: "directory_access_denied", message: "Failed to start security-scoped access.", details: nil ) ) return } let accessId = UUID().uuidString directoryAccessSessions[accessId] = url var payload: [String: Any] = [ "accessId": accessId, "path": url.standardizedFileURL.path, ] if isStale, let refreshedBookmark = try? url.bookmarkData( options: [.withSecurityScope], includingResourceValuesForKeys: nil, relativeTo: nil ) { payload["bookmark"] = refreshedBookmark.base64EncodedString() } result(payload) } catch { result( FlutterError( code: "directory_access_failed", message: error.localizedDescription, details: nil ) ) } } private func stopDirectoryAccess(_ call: FlutterMethodCall, result: @escaping FlutterResult) { let arguments = call.arguments as? [String: Any] let accessId = (arguments?["accessId"] as? String ?? "").trimmingCharacters(in: .whitespacesAndNewlines) guard !accessId.isEmpty else { result(nil) return } if let url = directoryAccessSessions.removeValue(forKey: accessId) { url.stopAccessingSecurityScopedResource() } result(nil) } private func initialDirectoryURL(for suggestedPath: String) -> URL? { let trimmed = suggestedPath.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty { return URL(fileURLWithPath: resolveUserHomeDirectoryPath(), isDirectory: true) } var candidate = URL(fileURLWithPath: expandUserPath(trimmed)) var isDirectory: ObjCBool = false while true { if FileManager.default.fileExists(atPath: candidate.path, isDirectory: &isDirectory) { return isDirectory.boolValue ? candidate.deletingLastPathComponent() : candidate.deletingLastPathComponent() } let parent = candidate.deletingLastPathComponent() if parent.path == candidate.path || parent.path.isEmpty { break } candidate = parent } return URL(fileURLWithPath: resolveUserHomeDirectoryPath(), isDirectory: true) } private func expandUserPath(_ path: String) -> String { guard path.hasPrefix("~/") else { return path } let relative = String(path.dropFirst(2)) return (resolveUserHomeDirectoryPath() as NSString).appendingPathComponent(relative) } private func resolveUserHomeDirectoryPath() -> String { if let directoryPointer = getpwuid(getuid())?.pointee.pw_dir { return String(cString: directoryPointer) } return FileManager.default.homeDirectoryForCurrentUser.path } }