



























@@ -33,7 +33,7 @@ final class DevicePairingApprovalPrompter {
3333let remoteIp: String?
3434}
353536-private struct PendingRequest: Codable, Equatable, Identifiable {
36+struct PendingRequest: Codable, Equatable, Identifiable {
3737let requestId: String
3838let deviceId: String
3939let publicKey: String
@@ -115,14 +115,16 @@ final class DevicePairingApprovalPrompter {
115115PairingAlertSupport.presentPairingAlert(
116116 request: req,
117117 requestId: req.requestId,
118- messageText: "Allow device to connect?",
119- informativeText: Self.describe(req),
118+ messageText: Self.alertTitle(for: req),
119+ informativeText: Self.alertSummary(for: req),
120+ buttonTitles: PairingAlertSupport.ButtonTitles(approve: Self.approveButtonTitle(for: req)),
121+ accessoryView: Self.buildAccessoryView(for: req),
120122 state: self.alertState,
121123 onResponse: self.handleAlertResponse)
122124}
123125124126private func handleAlertResponse(_ response: NSApplication.ModalResponse, request: PendingRequest) async {
125-var shouldRemove = response != .alertFirstButtonReturn
127+var shouldRemove = response != .alertSecondButtonReturn
126128defer {
127129if shouldRemove {
128130if self.queue.first == request {
@@ -144,14 +146,14 @@ final class DevicePairingApprovalPrompter {
144146145147switch response {
146148case .alertFirstButtonReturn:
149+ _ = await self.approve(requestId: request.requestId)
150+case .alertSecondButtonReturn:
147151 shouldRemove = false
148152if let idx = self.queue.firstIndex(of: request) {
149153self.queue.remove(at: idx)
150154}
151155self.queue.append(request)
152156return
153-case .alertSecondButtonReturn:
154- _ = await self.approve(requestId: request.requestId)
155157case .alertThirdButtonReturn:
156158await self.reject(requestId: request.requestId)
157159default:
@@ -233,24 +235,166 @@ final class DevicePairingApprovalPrompter {
233235self.updatePendingCounts()
234236}
235237236-private static func describe(_ req: PendingRequest) -> String {
237-var lines: [String] = []
238- lines.append("Device: \(req.displayName ?? req.deviceId)")
239-if let platform = req.platform {
240- lines.append("Platform: \(platform)")
238+static func alertTitle(for req: PendingRequest) -> String {
239+self.isMac(req.platform) ? "New Mac wants to connect" : "New device wants to connect"
240+}
241+242+static func alertSummary(for req: PendingRequest) -> String {
243+let subject = self.isMac(req.platform) ? "this Mac app" : "this device"
244+return "Approve \(subject) to control OpenClaw. Only approve if this is yours; you can remove it later in Settings."
245+}
246+247+static func approveButtonTitle(for req: PendingRequest) -> String {
248+self.isMac(req.platform) ? "Approve Mac" : "Approve Device"
249+}
250+251+static func buildAccessoryView(for req: PendingRequest) -> NSView {
252+let stack = NSStackView()
253+ stack.orientation = .vertical
254+ stack.alignment = .leading
255+ stack.spacing = 8
256+ stack.edgeInsets = NSEdgeInsets(top: 2, left: 0, bottom: 0, right: 0)
257+258+ stack.addArrangedSubview(self.makeValueRow(label: "Device", value: self.deviceName(for: req)))
259+if let platform = self.prettyPlatform(req.platform) {
260+ stack.addArrangedSubview(self.makeValueRow(label: "Platform", value: platform))
241261}
242-if let role = req.role {
243-lines.append("Role: \(role)")
262+if let role = self.prettyRole(req.role) {
263+stack.addArrangedSubview(self.makeValueRow(label: "Role", value: role))
244264}
245-if let scopes = req.scopes, !scopes.isEmpty {
246- lines.append("Scopes: \(scopes.joined(separator: ", "))")
265+let accessItems = self.friendlyScopeNames(req.scopes)
266+if !accessItems.isEmpty {
267+ stack.addArrangedSubview(self.makeSectionLabel("Access requested"))
268+for item in accessItems {
269+ stack.addArrangedSubview(self.makeBullet(item))
270+}
247271}
248-if let remoteIp = req.remoteIp {
249- lines.append("IP: \(remoteIp)")
272+ stack.addArrangedSubview(self.makeDetailLine(req))
273+274+let fitting = stack.fittingSize
275+ stack.frame = NSRect(x: 0, y: 0, width: 420, height: fitting.height)
276+return stack
277+}
278+279+static func deviceName(for req: PendingRequest) -> String {
280+let trimmedName = req.displayName?.trimmingCharacters(in: .whitespacesAndNewlines)
281+if let trimmedName, !trimmedName.isEmpty, trimmedName != req.deviceId {
282+return trimmedName
283+}
284+return self.isMac(req.platform) ? "OpenClaw Mac app" : "New device"
285+}
286+287+static func prettyPlatform(_ raw: String?) -> String? {
288+let platform = raw?.trimmingCharacters(in: .whitespacesAndNewlines)
289+guard let platform, !platform.isEmpty else { return nil }
290+switch platform.lowercased() {
291+case "macintel", "x86_64-apple-darwin":
292+return "Mac (Intel)"
293+case "macarm", "macarm64", "arm64-apple-darwin", "aarch64-apple-darwin":
294+return "Mac (Apple silicon)"
295+case "darwin":
296+return "Mac"
297+default:
298+if platform.lowercased().contains("mac") {
299+return "Mac"
300+}
301+return platform
302+}
303+}
304+305+static func prettyRole(_ raw: String?) -> String? {
306+let role = raw?.trimmingCharacters(in: .whitespacesAndNewlines)
307+guard let role, !role.isEmpty else { return nil }
308+return role == "operator" ? "Operator" : role
309+}
310+311+static func friendlyScopeNames(_ scopes: [String]?) -> [String] {
312+guard let scopes else { return [] }
313+var seen = Set<String>()
314+return scopes.compactMap { scope in
315+let normalized = scope.trimmingCharacters(in: .whitespacesAndNewlines)
316+guard !normalized.isEmpty, seen.insert(normalized).inserted else { return nil }
317+switch normalized {
318+case "operator.admin":
319+return "Admin access"
320+case "operator.read":
321+return "Read OpenClaw data"
322+case "operator.write":
323+return "Send messages and make changes"
324+case "operator.approvals":
325+return "Manage approvals"
326+case "operator.pairing":
327+return "Pair and repair devices"
328+case "operator.talk.secrets":
329+return "Use Talk credentials"
330+default:
331+return normalized
332+}
333+}
334+}
335+336+static func shortIdentifier(_ id: String) -> String {
337+let trimmed = id.trimmingCharacters(in: .whitespacesAndNewlines)
338+guard trimmed.count > 20 else { return trimmed }
339+return "\(trimmed.prefix(8))...\(trimmed.suffix(7))"
340+}
341+342+private static func isMac(_ platform: String?) -> Bool {
343+guard let platform else { return false }
344+let lower = platform.lowercased()
345+return lower.contains("mac") || lower.contains("darwin")
346+}
347+348+private static func makeValueRow(label: String, value: String) -> NSView {
349+let row = NSStackView()
350+ row.orientation = .horizontal
351+ row.alignment = .firstBaseline
352+ row.spacing = 8
353+354+let labelField = self.makeLabel("\(label):", font: .systemFont(ofSize: 12, weight: .semibold))
355+ labelField.textColor = .secondaryLabelColor
356+ labelField.setContentHuggingPriority(.required, for: .horizontal)
357+let valueField = self.makeLabel(value, font: .systemFont(ofSize: 12, weight: .regular))
358+ valueField.maximumNumberOfLines = 2
359+360+ row.addArrangedSubview(labelField)
361+ row.addArrangedSubview(valueField)
362+return row
363+}
364+365+private static func makeSectionLabel(_ text: String) -> NSTextField {
366+let label = self.makeLabel(text, font: .systemFont(ofSize: 12, weight: .semibold))
367+ label.textColor = .secondaryLabelColor
368+return label
369+}
370+371+private static func makeBullet(_ text: String) -> NSTextField {
372+let label = self.makeLabel("• \(text)", font: .systemFont(ofSize: 12, weight: .regular))
373+ label.maximumNumberOfLines = 2
374+return label
375+}
376+377+private static func makeDetailLine(_ req: PendingRequest) -> NSTextField {
378+var parts = ["ID \(self.shortIdentifier(req.deviceId))"]
379+if let remoteIp = req.remoteIp?.trimmingCharacters(in: .whitespacesAndNewlines), !remoteIp.isEmpty {
380+ parts.append("IP \(remoteIp.replacingOccurrences(of: "::ffff:", with: ""))")
250381}
251382if req.isRepair == true {
252-lines.append("Repair: yes")
383+parts.append("repair request")
253384}
254-return lines.joined(separator: "\n")
385+let label = self.makeLabel(
386+ parts.joined(separator: " · "),
387+ font: .monospacedSystemFont(ofSize: 11, weight: .regular))
388+ label.textColor = .tertiaryLabelColor
389+ label.maximumNumberOfLines = 2
390+return label
391+}
392+393+private static func makeLabel(_ text: String, font: NSFont) -> NSTextField {
394+let label = NSTextField(labelWithString: text)
395+ label.font = font
396+ label.lineBreakMode = .byWordWrapping
397+ label.textColor = .labelColor
398+return label
255399}
256400}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。