mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
refactor(macos): share pairing and ui dedupe utilities
This commit is contained in:
@@ -25,22 +25,11 @@ extension CanvasWindowController {
|
||||
}
|
||||
|
||||
static func _testParseIPv4(_ host: String) -> (UInt8, UInt8, UInt8, UInt8)? {
|
||||
let parts = host.split(separator: ".", omittingEmptySubsequences: false)
|
||||
guard parts.count == 4 else { return nil }
|
||||
let bytes: [UInt8] = parts.compactMap { UInt8($0) }
|
||||
guard bytes.count == 4 else { return nil }
|
||||
return (bytes[0], bytes[1], bytes[2], bytes[3])
|
||||
LoopbackHost.parseIPv4(host)
|
||||
}
|
||||
|
||||
static func _testIsLocalNetworkIPv4(_ ip: (UInt8, UInt8, UInt8, UInt8)) -> Bool {
|
||||
let (a, b, _, _) = ip
|
||||
if a == 10 { return true }
|
||||
if a == 172, (16...31).contains(Int(b)) { return true }
|
||||
if a == 192, b == 168 { return true }
|
||||
if a == 127 { return true }
|
||||
if a == 169, b == 254 { return true }
|
||||
if a == 100, (64...127).contains(Int(b)) { return true }
|
||||
return false
|
||||
LoopbackHost.isLocalNetworkIPv4(ip)
|
||||
}
|
||||
|
||||
static func _testIsLocalNetworkCanvasURL(_ url: URL) -> Bool {
|
||||
|
||||
@@ -17,9 +17,7 @@ final class DevicePairingApprovalPrompter {
|
||||
private var queue: [PendingRequest] = []
|
||||
var pendingCount: Int = 0
|
||||
var pendingRepairCount: Int = 0
|
||||
private var activeAlert: NSAlert?
|
||||
private var activeRequestId: String?
|
||||
private var alertHostWindow: NSWindow?
|
||||
private let alertState = PairingAlertState()
|
||||
private var resolvedByRequestId: Set<String> = []
|
||||
|
||||
private struct PairingList: Codable {
|
||||
@@ -78,12 +76,10 @@ final class DevicePairingApprovalPrompter {
|
||||
private func stopPushTask() {
|
||||
PairingAlertSupport.stopPairingPrompter(
|
||||
isStopping: &self.isStopping,
|
||||
activeAlert: &self.activeAlert,
|
||||
activeRequestId: &self.activeRequestId,
|
||||
task: &self.task,
|
||||
queue: &self.queue,
|
||||
isPresenting: &self.isPresenting,
|
||||
alertHostWindow: &self.alertHostWindow)
|
||||
state: self.alertState)
|
||||
}
|
||||
|
||||
private func loadPendingRequestsFromGateway() async {
|
||||
@@ -121,20 +117,10 @@ final class DevicePairingApprovalPrompter {
|
||||
requestId: req.requestId,
|
||||
messageText: "Allow device to connect?",
|
||||
informativeText: Self.describe(req),
|
||||
activeAlert: &self.activeAlert,
|
||||
activeRequestId: &self.activeRequestId,
|
||||
alertHostWindow: &self.alertHostWindow,
|
||||
clearActive: self.clearActiveAlert(hostWindow:),
|
||||
state: self.alertState,
|
||||
onResponse: self.handleAlertResponse)
|
||||
}
|
||||
|
||||
private func clearActiveAlert(hostWindow: NSWindow) {
|
||||
PairingAlertSupport.clearActivePairingAlert(
|
||||
activeAlert: &self.activeAlert,
|
||||
activeRequestId: &self.activeRequestId,
|
||||
hostWindow: hostWindow)
|
||||
}
|
||||
|
||||
private func handleAlertResponse(_ response: NSApplication.ModalResponse, request: PendingRequest) async {
|
||||
var shouldRemove = response != .alertFirstButtonReturn
|
||||
defer {
|
||||
@@ -194,7 +180,7 @@ final class DevicePairingApprovalPrompter {
|
||||
}
|
||||
|
||||
private func endActiveAlert() {
|
||||
PairingAlertSupport.endActiveAlert(activeAlert: &self.activeAlert, activeRequestId: &self.activeRequestId)
|
||||
PairingAlertSupport.endActiveAlert(state: self.alertState)
|
||||
}
|
||||
|
||||
private func handle(push: GatewayPush) {
|
||||
@@ -234,7 +220,7 @@ final class DevicePairingApprovalPrompter {
|
||||
let resolution = resolved.decision == PairingAlertSupport.PairingResolution.approved.rawValue
|
||||
? PairingAlertSupport.PairingResolution.approved
|
||||
: PairingAlertSupport.PairingResolution.rejected
|
||||
if let activeRequestId, activeRequestId == resolved.requestId {
|
||||
if let activeRequestId = self.alertState.activeRequestId, activeRequestId == resolved.requestId {
|
||||
self.resolvedByRequestId.insert(resolved.requestId)
|
||||
self.endActiveAlert()
|
||||
let decision = resolution.rawValue
|
||||
|
||||
@@ -48,27 +48,11 @@ struct GatewayDiscoveryInlineList: View {
|
||||
.truncationMode(.middle)
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
if selected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(Color.accentColor)
|
||||
} else {
|
||||
Image(systemName: "arrow.right.circle")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
SelectionStateIndicator(selected: selected)
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 8)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(self.rowBackground(
|
||||
selected: selected,
|
||||
hovered: self.hoveredGatewayID == gateway.id)))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.strokeBorder(
|
||||
selected ? Color.accentColor.opacity(0.45) : Color.clear,
|
||||
lineWidth: 1))
|
||||
.openClawSelectableRowChrome(
|
||||
selected: selected,
|
||||
hovered: self.hoveredGatewayID == gateway.id)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -106,12 +90,6 @@ struct GatewayDiscoveryInlineList: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func rowBackground(selected: Bool, hovered: Bool) -> Color {
|
||||
if selected { return Color.accentColor.opacity(0.12) }
|
||||
if hovered { return Color.secondary.opacity(0.08) }
|
||||
return Color.clear
|
||||
}
|
||||
|
||||
private func trimmed(_ value: String?) -> String {
|
||||
value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
}
|
||||
|
||||
@@ -347,21 +347,8 @@ actor GatewayEndpointStore {
|
||||
|
||||
/// Explicit action: ensure the remote control tunnel is established and publish the resolved endpoint.
|
||||
func ensureRemoteControlTunnel() async throws -> UInt16 {
|
||||
let mode = await self.deps.mode()
|
||||
guard mode == .remote else {
|
||||
throw NSError(
|
||||
domain: "RemoteTunnel",
|
||||
code: 1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Remote mode is not enabled"])
|
||||
}
|
||||
let root = OpenClawConfigFile.loadDict()
|
||||
if GatewayRemoteConfig.resolveTransport(root: root) == .direct {
|
||||
guard let url = GatewayRemoteConfig.resolveGatewayUrl(root: root) else {
|
||||
throw NSError(
|
||||
domain: "GatewayEndpoint",
|
||||
code: 1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "gateway.remote.url missing or invalid"])
|
||||
}
|
||||
try await self.requireRemoteMode()
|
||||
if let url = try self.resolveDirectRemoteURL() {
|
||||
guard let port = GatewayRemoteConfig.defaultPort(for: url),
|
||||
let portInt = UInt16(exactly: port)
|
||||
else {
|
||||
@@ -425,22 +412,9 @@ actor GatewayEndpointStore {
|
||||
}
|
||||
|
||||
private func ensureRemoteConfig(detail: String) async throws -> GatewayConnection.Config {
|
||||
let mode = await self.deps.mode()
|
||||
guard mode == .remote else {
|
||||
throw NSError(
|
||||
domain: "RemoteTunnel",
|
||||
code: 1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Remote mode is not enabled"])
|
||||
}
|
||||
try await self.requireRemoteMode()
|
||||
|
||||
let root = OpenClawConfigFile.loadDict()
|
||||
if GatewayRemoteConfig.resolveTransport(root: root) == .direct {
|
||||
guard let url = GatewayRemoteConfig.resolveGatewayUrl(root: root) else {
|
||||
throw NSError(
|
||||
domain: "GatewayEndpoint",
|
||||
code: 1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "gateway.remote.url missing or invalid"])
|
||||
}
|
||||
if let url = try self.resolveDirectRemoteURL() {
|
||||
let token = self.deps.token()
|
||||
let password = self.deps.password()
|
||||
self.cancelRemoteEnsure()
|
||||
@@ -491,6 +465,27 @@ actor GatewayEndpointStore {
|
||||
}
|
||||
}
|
||||
|
||||
private func requireRemoteMode() async throws {
|
||||
guard await self.deps.mode() == .remote else {
|
||||
throw NSError(
|
||||
domain: "RemoteTunnel",
|
||||
code: 1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Remote mode is not enabled"])
|
||||
}
|
||||
}
|
||||
|
||||
private func resolveDirectRemoteURL() throws -> URL? {
|
||||
let root = OpenClawConfigFile.loadDict()
|
||||
guard GatewayRemoteConfig.resolveTransport(root: root) == .direct else { return nil }
|
||||
guard let url = GatewayRemoteConfig.resolveGatewayUrl(root: root) else {
|
||||
throw NSError(
|
||||
domain: "GatewayEndpoint",
|
||||
code: 1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "gateway.remote.url missing or invalid"])
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
private func removeSubscriber(_ id: UUID) {
|
||||
self.subscribers[id] = nil
|
||||
}
|
||||
|
||||
@@ -32,9 +32,7 @@ final class NodePairingApprovalPrompter {
|
||||
private var queue: [PendingRequest] = []
|
||||
var pendingCount: Int = 0
|
||||
var pendingRepairCount: Int = 0
|
||||
private var activeAlert: NSAlert?
|
||||
private var activeRequestId: String?
|
||||
private var alertHostWindow: NSWindow?
|
||||
private let alertState = PairingAlertState()
|
||||
private var remoteResolutionsByRequestId: [String: PairingResolution] = [:]
|
||||
private var autoApproveAttempts: Set<String> = []
|
||||
|
||||
@@ -99,12 +97,10 @@ final class NodePairingApprovalPrompter {
|
||||
private func stopPushTask() {
|
||||
PairingAlertSupport.stopPairingPrompter(
|
||||
isStopping: &self.isStopping,
|
||||
activeAlert: &self.activeAlert,
|
||||
activeRequestId: &self.activeRequestId,
|
||||
task: &self.task,
|
||||
queue: &self.queue,
|
||||
isPresenting: &self.isPresenting,
|
||||
alertHostWindow: &self.alertHostWindow)
|
||||
state: self.alertState)
|
||||
}
|
||||
|
||||
private func loadPendingRequestsFromGateway() async {
|
||||
@@ -180,7 +176,7 @@ final class NodePairingApprovalPrompter {
|
||||
if pendingById[req.requestId] != nil { continue }
|
||||
let resolution = self.inferResolution(for: req, list: list)
|
||||
|
||||
if self.activeRequestId == req.requestId, self.activeAlert != nil {
|
||||
if self.alertState.activeRequestId == req.requestId, self.alertState.activeAlert != nil {
|
||||
self.remoteResolutionsByRequestId[req.requestId] = resolution
|
||||
self.logger.info(
|
||||
"""
|
||||
@@ -222,7 +218,7 @@ final class NodePairingApprovalPrompter {
|
||||
}
|
||||
|
||||
private func endActiveAlert() {
|
||||
PairingAlertSupport.endActiveAlert(activeAlert: &self.activeAlert, activeRequestId: &self.activeRequestId)
|
||||
PairingAlertSupport.endActiveAlert(state: self.alertState)
|
||||
}
|
||||
|
||||
private func handle(push: GatewayPush) {
|
||||
@@ -284,20 +280,10 @@ final class NodePairingApprovalPrompter {
|
||||
requestId: req.requestId,
|
||||
messageText: "Allow node to connect?",
|
||||
informativeText: Self.describe(req),
|
||||
activeAlert: &self.activeAlert,
|
||||
activeRequestId: &self.activeRequestId,
|
||||
alertHostWindow: &self.alertHostWindow,
|
||||
clearActive: self.clearActiveAlert(hostWindow:),
|
||||
state: self.alertState,
|
||||
onResponse: self.handleAlertResponse)
|
||||
}
|
||||
|
||||
private func clearActiveAlert(hostWindow: NSWindow) {
|
||||
PairingAlertSupport.clearActivePairingAlert(
|
||||
activeAlert: &self.activeAlert,
|
||||
activeRequestId: &self.activeRequestId,
|
||||
hostWindow: hostWindow)
|
||||
}
|
||||
|
||||
private func handleAlertResponse(_ response: NSApplication.ModalResponse, request: PendingRequest) async {
|
||||
defer {
|
||||
if self.queue.first == request {
|
||||
@@ -575,7 +561,7 @@ final class NodePairingApprovalPrompter {
|
||||
let resolution: PairingResolution =
|
||||
resolved.decision == PairingResolution.approved.rawValue ? .approved : .rejected
|
||||
|
||||
if self.activeRequestId == resolved.requestId, self.activeAlert != nil {
|
||||
if self.alertState.activeRequestId == resolved.requestId, self.alertState.activeAlert != nil {
|
||||
self.remoteResolutionsByRequestId[resolved.requestId] = resolution
|
||||
self.logger.info(
|
||||
"""
|
||||
|
||||
@@ -311,29 +311,13 @@ extension OnboardingView {
|
||||
.font(.caption.monospaced())
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
.truncationMode(.middle)
|
||||
}
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
if selected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(Color.accentColor)
|
||||
} else {
|
||||
Image(systemName: "arrow.right.circle")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
SelectionStateIndicator(selected: selected)
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 8)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(selected ? Color.accentColor.opacity(0.12) : Color.clear))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.strokeBorder(
|
||||
selected ? Color.accentColor.opacity(0.45) : Color.clear,
|
||||
lineWidth: 1))
|
||||
.openClawSelectableRowChrome(selected: selected)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,13 @@ final class PairingAlertHostWindow: NSWindow {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class PairingAlertState {
|
||||
var activeAlert: NSAlert?
|
||||
var activeRequestId: String?
|
||||
var alertHostWindow: NSWindow?
|
||||
}
|
||||
|
||||
@MainActor
|
||||
enum PairingAlertSupport {
|
||||
enum PairingResolution: String {
|
||||
@@ -34,6 +41,10 @@ enum PairingAlertSupport {
|
||||
activeRequestId = nil
|
||||
}
|
||||
|
||||
static func endActiveAlert(state: PairingAlertState) {
|
||||
self.endActiveAlert(activeAlert: &state.activeAlert, activeRequestId: &state.activeRequestId)
|
||||
}
|
||||
|
||||
static func requireAlertHostWindow(alertHostWindow: inout NSWindow?) -> NSWindow {
|
||||
if let alertHostWindow {
|
||||
return alertHostWindow
|
||||
@@ -179,6 +190,30 @@ enum PairingAlertSupport {
|
||||
}
|
||||
}
|
||||
|
||||
static func presentPairingAlert<Request>(
|
||||
request: Request,
|
||||
requestId: String,
|
||||
messageText: String,
|
||||
informativeText: String,
|
||||
state: PairingAlertState,
|
||||
onResponse: @escaping @MainActor (NSApplication.ModalResponse, Request) async -> Void)
|
||||
{
|
||||
self.presentPairingAlert(
|
||||
request: request,
|
||||
requestId: requestId,
|
||||
messageText: messageText,
|
||||
informativeText: informativeText,
|
||||
activeAlert: &state.activeAlert,
|
||||
activeRequestId: &state.activeRequestId,
|
||||
alertHostWindow: &state.alertHostWindow)
|
||||
{ response, hostWindow in
|
||||
Task { @MainActor in
|
||||
self.clearActivePairingAlert(state: state, hostWindow: hostWindow)
|
||||
await onResponse(response, request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func clearActivePairingAlert(
|
||||
activeAlert: inout NSAlert?,
|
||||
activeRequestId: inout String?,
|
||||
@@ -189,6 +224,13 @@ enum PairingAlertSupport {
|
||||
hostWindow.orderOut(nil)
|
||||
}
|
||||
|
||||
static func clearActivePairingAlert(state: PairingAlertState, hostWindow: NSWindow) {
|
||||
self.clearActivePairingAlert(
|
||||
activeAlert: &state.activeAlert,
|
||||
activeRequestId: &state.activeRequestId,
|
||||
hostWindow: hostWindow)
|
||||
}
|
||||
|
||||
static func stopPairingPrompter<Request>(
|
||||
isStopping: inout Bool,
|
||||
activeAlert: inout NSAlert?,
|
||||
@@ -210,6 +252,23 @@ enum PairingAlertSupport {
|
||||
alertHostWindow = nil
|
||||
}
|
||||
|
||||
static func stopPairingPrompter<Request>(
|
||||
isStopping: inout Bool,
|
||||
task: inout Task<Void, Never>?,
|
||||
queue: inout [Request],
|
||||
isPresenting: inout Bool,
|
||||
state: PairingAlertState)
|
||||
{
|
||||
self.stopPairingPrompter(
|
||||
isStopping: &isStopping,
|
||||
activeAlert: &state.activeAlert,
|
||||
activeRequestId: &state.activeRequestId,
|
||||
task: &task,
|
||||
queue: &queue,
|
||||
isPresenting: &isPresenting,
|
||||
alertHostWindow: &state.alertHostWindow)
|
||||
}
|
||||
|
||||
static func approveRequest(
|
||||
requestId: String,
|
||||
kind: String,
|
||||
|
||||
40
apps/macos/Sources/OpenClaw/SelectableRow.swift
Normal file
40
apps/macos/Sources/OpenClaw/SelectableRow.swift
Normal file
@@ -0,0 +1,40 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SelectionStateIndicator: View {
|
||||
let selected: Bool
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if self.selected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(Color.accentColor)
|
||||
} else {
|
||||
Image(systemName: "arrow.right.circle")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func openClawSelectableRowChrome(selected: Bool, hovered: Bool = false) -> some View {
|
||||
self
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 8)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(self.openClawRowBackground(selected: selected, hovered: hovered)))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.strokeBorder(
|
||||
selected ? Color.accentColor.opacity(0.45) : Color.clear,
|
||||
lineWidth: 1))
|
||||
}
|
||||
|
||||
private func openClawRowBackground(selected: Bool, hovered: Bool) -> Color {
|
||||
if selected { return Color.accentColor.opacity(0.12) }
|
||||
if hovered { return Color.secondary.opacity(0.08) }
|
||||
return Color.clear
|
||||
}
|
||||
}
|
||||
@@ -285,16 +285,12 @@ actor GatewayWizardClient {
|
||||
nonce: connectNonce,
|
||||
platform: platform,
|
||||
deviceFamily: "Mac")
|
||||
if let signature = DeviceIdentityStore.signPayload(payload, identity: identity),
|
||||
let publicKey = DeviceIdentityStore.publicKeyBase64Url(identity)
|
||||
if let device = GatewayDeviceAuthPayload.signedDeviceDictionary(
|
||||
payload: payload,
|
||||
identity: identity,
|
||||
signedAtMs: signedAtMs,
|
||||
nonce: connectNonce)
|
||||
{
|
||||
let device: [String: ProtoAnyCodable] = [
|
||||
"id": ProtoAnyCodable(identity.deviceId),
|
||||
"publicKey": ProtoAnyCodable(publicKey),
|
||||
"signature": ProtoAnyCodable(signature),
|
||||
"signedAt": ProtoAnyCodable(signedAtMs),
|
||||
"nonce": ProtoAnyCodable(connectNonce),
|
||||
]
|
||||
params["device"] = ProtoAnyCodable(device)
|
||||
}
|
||||
|
||||
|
||||
@@ -70,13 +70,7 @@ private struct ChatBubbleShape: InsettableShape {
|
||||
to: baseBottom,
|
||||
control1: CGPoint(x: bubbleMaxX + self.tailWidth * 0.95, y: midY + baseH * 0.15),
|
||||
control2: CGPoint(x: bubbleMaxX + self.tailWidth * 0.2, y: baseBottomY - baseH * 0.05))
|
||||
path.addQuadCurve(
|
||||
to: CGPoint(x: bubbleMaxX - r, y: bubbleMaxY),
|
||||
control: CGPoint(x: bubbleMaxX, y: bubbleMaxY))
|
||||
path.addLine(to: CGPoint(x: bubbleMinX + r, y: bubbleMaxY))
|
||||
path.addQuadCurve(
|
||||
to: CGPoint(x: bubbleMinX, y: bubbleMaxY - r),
|
||||
control: CGPoint(x: bubbleMinX, y: bubbleMaxY))
|
||||
self.addBottomEdge(path: &path, bubbleMinX: bubbleMinX, bubbleMaxX: bubbleMaxX, bubbleMaxY: bubbleMaxY, radius: r)
|
||||
path.addLine(to: CGPoint(x: bubbleMinX, y: bubbleMinY + r))
|
||||
path.addQuadCurve(
|
||||
to: CGPoint(x: bubbleMinX + r, y: bubbleMinY),
|
||||
@@ -108,13 +102,7 @@ private struct ChatBubbleShape: InsettableShape {
|
||||
to: CGPoint(x: bubbleMaxX, y: bubbleMinY + r),
|
||||
control: CGPoint(x: bubbleMaxX, y: bubbleMinY))
|
||||
path.addLine(to: CGPoint(x: bubbleMaxX, y: bubbleMaxY - r))
|
||||
path.addQuadCurve(
|
||||
to: CGPoint(x: bubbleMaxX - r, y: bubbleMaxY),
|
||||
control: CGPoint(x: bubbleMaxX, y: bubbleMaxY))
|
||||
path.addLine(to: CGPoint(x: bubbleMinX + r, y: bubbleMaxY))
|
||||
path.addQuadCurve(
|
||||
to: CGPoint(x: bubbleMinX, y: bubbleMaxY - r),
|
||||
control: CGPoint(x: bubbleMinX, y: bubbleMaxY))
|
||||
self.addBottomEdge(path: &path, bubbleMinX: bubbleMinX, bubbleMaxX: bubbleMaxX, bubbleMaxY: bubbleMaxY, radius: r)
|
||||
path.addLine(to: baseBottom)
|
||||
path.addCurve(
|
||||
to: tip,
|
||||
@@ -131,6 +119,22 @@ private struct ChatBubbleShape: InsettableShape {
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
private func addBottomEdge(
|
||||
path: inout Path,
|
||||
bubbleMinX: CGFloat,
|
||||
bubbleMaxX: CGFloat,
|
||||
bubbleMaxY: CGFloat,
|
||||
radius: CGFloat)
|
||||
{
|
||||
path.addQuadCurve(
|
||||
to: CGPoint(x: bubbleMaxX - radius, y: bubbleMaxY),
|
||||
control: CGPoint(x: bubbleMaxX, y: bubbleMaxY))
|
||||
path.addLine(to: CGPoint(x: bubbleMinX + radius, y: bubbleMaxY))
|
||||
path.addQuadCurve(
|
||||
to: CGPoint(x: bubbleMinX, y: bubbleMaxY - radius),
|
||||
control: CGPoint(x: bubbleMinX, y: bubbleMaxY))
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import OpenClawProtocol
|
||||
|
||||
public enum GatewayDeviceAuthPayload {
|
||||
public static func buildV3(
|
||||
@@ -52,4 +53,24 @@ public enum GatewayDeviceAuthPayload {
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
public static func signedDeviceDictionary(
|
||||
payload: String,
|
||||
identity: DeviceIdentity,
|
||||
signedAtMs: Int,
|
||||
nonce: String) -> [String: OpenClawProtocol.AnyCodable]?
|
||||
{
|
||||
guard let signature = DeviceIdentityStore.signPayload(payload, identity: identity),
|
||||
let publicKey = DeviceIdentityStore.publicKeyBase64Url(identity)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return [
|
||||
"id": OpenClawProtocol.AnyCodable(identity.deviceId),
|
||||
"publicKey": OpenClawProtocol.AnyCodable(publicKey),
|
||||
"signature": OpenClawProtocol.AnyCodable(signature),
|
||||
"signedAt": OpenClawProtocol.AnyCodable(signedAtMs),
|
||||
"nonce": OpenClawProtocol.AnyCodable(nonce),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,15 +406,12 @@ public actor GatewayChannelActor {
|
||||
nonce: connectNonce,
|
||||
platform: platform,
|
||||
deviceFamily: InstanceIdentity.deviceFamily)
|
||||
if let signature = DeviceIdentityStore.signPayload(payload, identity: identity),
|
||||
let publicKey = DeviceIdentityStore.publicKeyBase64Url(identity) {
|
||||
let device: [String: ProtoAnyCodable] = [
|
||||
"id": ProtoAnyCodable(identity.deviceId),
|
||||
"publicKey": ProtoAnyCodable(publicKey),
|
||||
"signature": ProtoAnyCodable(signature),
|
||||
"signedAt": ProtoAnyCodable(signedAtMs),
|
||||
"nonce": ProtoAnyCodable(connectNonce),
|
||||
]
|
||||
if let device = GatewayDeviceAuthPayload.signedDeviceDictionary(
|
||||
payload: payload,
|
||||
identity: identity,
|
||||
signedAtMs: signedAtMs,
|
||||
nonce: connectNonce)
|
||||
{
|
||||
params["device"] = ProtoAnyCodable(device)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ public enum LoopbackHost {
|
||||
return self.isLocalNetworkIPv4(ipv4)
|
||||
}
|
||||
|
||||
private static func parseIPv4(_ host: String) -> (UInt8, UInt8, UInt8, UInt8)? {
|
||||
static func parseIPv4(_ host: String) -> (UInt8, UInt8, UInt8, UInt8)? {
|
||||
let parts = host.split(separator: ".", omittingEmptySubsequences: false)
|
||||
guard parts.count == 4 else { return nil }
|
||||
let bytes: [UInt8] = parts.compactMap { UInt8($0) }
|
||||
@@ -61,7 +61,7 @@ public enum LoopbackHost {
|
||||
return (bytes[0], bytes[1], bytes[2], bytes[3])
|
||||
}
|
||||
|
||||
private static func isLocalNetworkIPv4(_ ip: (UInt8, UInt8, UInt8, UInt8)) -> Bool {
|
||||
static func isLocalNetworkIPv4(_ ip: (UInt8, UInt8, UInt8, UInt8)) -> Bool {
|
||||
let (a, b, _, _) = ip
|
||||
// 10.0.0.0/8
|
||||
if a == 10 { return true }
|
||||
|
||||
Reference in New Issue
Block a user