mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
refactor(native): deslop native apps (#163339)
Retire pre-July native client-ID and Talk credential paths, use shared transport owners, remove pure forwarding layers, and narrow Swift request completions to response frames. The remaining native production diff removes 190 net lines. A sibling's independently landed health-decoder cleanup is excluded; its useful clean-JSON assertion is retained. Supported July 2026+ route, cancellation, pairing and lifecycle behavior stays intact.
This commit is contained in:
parent
ee578bc732
commit
cfcee64d26
20 changed files with 108 additions and 288 deletions
|
|
@ -18,7 +18,6 @@ import ai.openclaw.app.chat.ChatTranscriptAnchorState
|
|||
import ai.openclaw.app.chat.ChatWidgetResource
|
||||
import ai.openclaw.app.chat.GatewayDefaultAgentOwner
|
||||
import ai.openclaw.app.chat.MessageSpeechState
|
||||
import ai.openclaw.app.chat.OutgoingAttachment
|
||||
import ai.openclaw.app.chat.SessionBranch
|
||||
import ai.openclaw.app.chat.SessionDiffSnapshot
|
||||
import ai.openclaw.app.chat.SessionForkResult
|
||||
|
|
@ -1182,7 +1181,7 @@ class MainViewModel private constructor(
|
|||
viewModelScope.launch {
|
||||
try {
|
||||
val accepted =
|
||||
sendChatForOwnerAwaitAcceptance(
|
||||
ensureRuntime().sendChatForOwnerAwaitAcceptance(
|
||||
owner = pending.owner,
|
||||
message = prompt,
|
||||
thinking = thinking,
|
||||
|
|
@ -2126,21 +2125,6 @@ class MainViewModel private constructor(
|
|||
ensureRuntime().chat.skipQuestion(prompt)
|
||||
}
|
||||
|
||||
internal suspend fun sendChatForOwnerAwaitAcceptance(
|
||||
owner: ChatComposerOwner,
|
||||
message: String,
|
||||
thinking: String,
|
||||
attachments: List<OutgoingAttachment>,
|
||||
idempotencyKey: String,
|
||||
): Boolean =
|
||||
ensureRuntime().sendChatForOwnerAwaitAcceptance(
|
||||
owner = owner,
|
||||
message = message,
|
||||
thinking = thinking,
|
||||
attachments = attachments,
|
||||
idempotencyKey = idempotencyKey,
|
||||
)
|
||||
|
||||
/** Admission outlives the composing Activity; accepted payloads clear by owner and snapshot. */
|
||||
internal fun beginChatComposerSend(
|
||||
owner: ChatComposerOwner,
|
||||
|
|
@ -2154,7 +2138,7 @@ class MainViewModel private constructor(
|
|||
var accepted: Boolean? = null
|
||||
try {
|
||||
accepted =
|
||||
sendChatForOwnerAwaitAcceptance(
|
||||
ensureRuntime().sendChatForOwnerAwaitAcceptance(
|
||||
owner = request.owner,
|
||||
message = request.message,
|
||||
thinking = thinking,
|
||||
|
|
|
|||
|
|
@ -68,10 +68,7 @@ internal data class ClawSpacing(
|
|||
val xxs: Dp = 8.dp,
|
||||
val xs: Dp = 12.dp,
|
||||
val sm: Dp = 16.dp,
|
||||
val md: Dp = 20.dp,
|
||||
val lg: Dp = 24.dp,
|
||||
val xl: Dp = 32.dp,
|
||||
val xxl: Dp = 40.dp,
|
||||
// Touch target and visible shape are separate: `touchTarget` is the minimum
|
||||
// hit area every control keeps, while `control`, `row`, `iconSlot`, and `icon`
|
||||
// describe the smaller painted geometry that sits inside it.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ OpenClaw iOS is the officially released iPhone app. It connects to an OpenClaw G
|
|||
|
||||
- UI and onboarding changes ship through normal app releases.
|
||||
- Upgrade migrations support data written by releases shipped on or after July 1, 2026. Earlier UserDefaults gateway locations are no longer imported; reconnect from Gateway settings if needed. Push registrations without a relay profile are refreshed through normal registration.
|
||||
- Connections use the current `openclaw-ios` client identity without pre-rename retries or undocumented legacy client-ID overrides. Talk credentials come from the Gateway's configuration; local provider keys whose writer retired before July are no longer read.
|
||||
- Some node commands require foreground access because of iOS platform limits.
|
||||
- Permissions, background behavior, and push delivery are documented below so release and support checks stay explicit.
|
||||
|
||||
|
|
|
|||
|
|
@ -153,43 +153,35 @@ final class ShareViewController: UIViewController {
|
|||
defer {
|
||||
Task { await gateway.disconnect() }
|
||||
}
|
||||
func connect(clientId: String) async throws {
|
||||
try await gateway.connect(
|
||||
url: url,
|
||||
credentials: GatewayNodeSessionCredentials(
|
||||
token: config.token,
|
||||
password: config.password),
|
||||
connectOptions: GatewayConnectOptions(
|
||||
role: "node",
|
||||
scopes: [],
|
||||
caps: [],
|
||||
commands: [],
|
||||
permissions: [:],
|
||||
clientId: clientId,
|
||||
clientMode: "node",
|
||||
clientDisplayName: "OpenClaw Share",
|
||||
deviceIdentityProfile: .shareExtension,
|
||||
includeDeviceIdentity: true,
|
||||
allowStoredDeviceAuth: config.gatewayStableID != nil,
|
||||
deviceAuthGatewayID: config.gatewayStableID),
|
||||
sessionBox: nil,
|
||||
onConnected: {},
|
||||
onDisconnected: { _ in },
|
||||
onInvoke: { req in
|
||||
BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: OpenClawNodeError(
|
||||
code: .invalidRequest,
|
||||
message: "share extension does not support node invoke"))
|
||||
})
|
||||
}
|
||||
do {
|
||||
try await connect(clientId: "openclaw-ios")
|
||||
} catch {
|
||||
guard self.shouldRetryWithLegacyClientId(error) else { throw error }
|
||||
try await connect(clientId: "moltbot-ios")
|
||||
}
|
||||
try await gateway.connect(
|
||||
url: url,
|
||||
credentials: GatewayNodeSessionCredentials(
|
||||
token: config.token,
|
||||
password: config.password),
|
||||
connectOptions: GatewayConnectOptions(
|
||||
role: "node",
|
||||
scopes: [],
|
||||
caps: [],
|
||||
commands: [],
|
||||
permissions: [:],
|
||||
clientId: "openclaw-ios",
|
||||
clientMode: "node",
|
||||
clientDisplayName: "OpenClaw Share",
|
||||
deviceIdentityProfile: .shareExtension,
|
||||
includeDeviceIdentity: true,
|
||||
allowStoredDeviceAuth: config.gatewayStableID != nil,
|
||||
deviceAuthGatewayID: config.gatewayStableID),
|
||||
sessionBox: nil,
|
||||
onConnected: {},
|
||||
onDisconnected: { _ in },
|
||||
onInvoke: { req in
|
||||
BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: OpenClawNodeError(
|
||||
code: .invalidRequest,
|
||||
message: "share extension does not support node invoke"))
|
||||
})
|
||||
|
||||
struct AgentRequestPayload: Codable {
|
||||
var message: String
|
||||
|
|
@ -228,27 +220,6 @@ final class ShareViewController: UIViewController {
|
|||
_ = try await gateway.request(method: "node.event", paramsJSON: nodeEventParams, timeoutSeconds: 25)
|
||||
}
|
||||
|
||||
private func shouldRetryWithLegacyClientId(_ error: Error) -> Bool {
|
||||
if let gatewayError = error as? GatewayResponseError {
|
||||
let code = gatewayError.code.lowercased()
|
||||
let message = gatewayError.message.lowercased()
|
||||
let pathValue = (gatewayError.details["path"]?.value as? String)?.lowercased() ?? ""
|
||||
let mentionsClientIdPath =
|
||||
message.contains("/client/id") || message.contains("client id")
|
||||
|| pathValue.contains("/client/id")
|
||||
let isInvalidConnectParams =
|
||||
(code.contains("invalid") && code.contains("connect"))
|
||||
|| message.contains("invalid connect params")
|
||||
if isInvalidConnectParams, mentionsClientIdPath {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
let text = error.localizedDescription.lowercased()
|
||||
return text.contains("invalid connect params")
|
||||
&& (text.contains("/client/id") || text.contains("client id"))
|
||||
}
|
||||
|
||||
private func extractSharedContent() async -> ExtractedShareContent {
|
||||
guard let items = self.extensionContext?.inputItems as? [NSExtensionItem] else {
|
||||
return ExtractedShareContent(
|
||||
|
|
|
|||
|
|
@ -458,14 +458,6 @@ struct IOSGatewayChatTransport: OpenClawChatGatewayTransport {
|
|||
unread: unread)
|
||||
}
|
||||
|
||||
func forkSession(parentKey: String) async throws -> String {
|
||||
try await self.forkSession(parentKey: parentKey, fromLastCompleted: false)
|
||||
}
|
||||
|
||||
func forkSession(parentKey: String, fromLastCompleted: Bool) async throws -> String {
|
||||
try await self.forkSession(parentKey: parentKey, fromLastCompleted: fromLastCompleted, agentID: nil)
|
||||
}
|
||||
|
||||
func forkSession(parentKey: String, fromLastCompleted: Bool, agentID: String?) async throws -> String {
|
||||
let target = self.sessionTarget(for: parentKey, overrideAgentID: agentID)
|
||||
let childAgentID = target.agentID ?? OpenClawChatSessionKey.agentID(from: target.sessionKey)
|
||||
|
|
@ -634,10 +626,6 @@ struct IOSGatewayChatTransport: OpenClawChatGatewayTransport {
|
|||
return error.message == "invalid chat.history params: at root: unexpected property 'inputRunIds'"
|
||||
}
|
||||
|
||||
var supportsSlashCommandCatalog: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
func waitForRunCompletion(
|
||||
runId rawRunId: String,
|
||||
timeoutMs: Int) async -> OpenClawChatRunObservation
|
||||
|
|
|
|||
|
|
@ -60,13 +60,11 @@ extension GatewayConnectionController {
|
|||
}
|
||||
|
||||
func makeConnectOptions(
|
||||
stableID: String?,
|
||||
deviceAuthGatewayID: String?,
|
||||
allowStoredDeviceAuth: Bool = true) async -> GatewayConnectOptions
|
||||
{
|
||||
let defaults = UserDefaults.standard
|
||||
let displayName = self.resolvedDisplayName(defaults: defaults)
|
||||
let resolvedClientId = self.resolvedClientId(defaults: defaults, stableID: stableID)
|
||||
let permissions = await self.currentPermissions()
|
||||
let caps = self.currentCaps()
|
||||
|
||||
|
|
@ -76,27 +74,13 @@ extension GatewayConnectionController {
|
|||
caps: caps,
|
||||
commands: Self.commands(for: caps),
|
||||
permissions: permissions,
|
||||
clientId: resolvedClientId,
|
||||
clientId: "openclaw-ios",
|
||||
clientMode: "node",
|
||||
clientDisplayName: displayName,
|
||||
allowStoredDeviceAuth: allowStoredDeviceAuth,
|
||||
deviceAuthGatewayID: GatewayStableIdentifier.exact(deviceAuthGatewayID))
|
||||
}
|
||||
|
||||
private func resolvedClientId(defaults: UserDefaults, stableID: String?) -> String {
|
||||
if let stableID,
|
||||
let override = GatewaySettingsStore.loadGatewayClientIdOverride(stableID: stableID)
|
||||
{
|
||||
return override
|
||||
}
|
||||
let manualClientId = defaults.string(forKey: "gateway.manual.clientId")?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if manualClientId?.isEmpty == false {
|
||||
return manualClientId!
|
||||
}
|
||||
return "openclaw-ios"
|
||||
}
|
||||
|
||||
private func resolvedDisplayName(defaults: UserDefaults) -> String {
|
||||
let key = "node.displayName"
|
||||
let existingRaw = defaults.string(forKey: key)
|
||||
|
|
|
|||
|
|
@ -656,7 +656,6 @@ final class GatewayConnectionController {
|
|||
GatewaySettingsStore.deleteGatewayCredentials(instanceId: instanceID, stableID: stableID)
|
||||
_ = GatewaySettingsStore.clearGatewayCustomHeaders(gatewayStableID: stableID)
|
||||
_ = GatewayTLSStore.clearFingerprint(stableID: stableID)
|
||||
GatewaySettingsStore.saveGatewayClientIdOverride(stableID: stableID, clientId: nil)
|
||||
GatewaySettingsStore.saveGatewaySelectedAgentId(stableID: stableID, agentId: nil)
|
||||
let shareRelayGatewayID = ShareGatewayRelaySettings.loadConfig()?.gatewayStableID
|
||||
if GatewayStableIdentifier.matches(shareRelayGatewayID, stableID) {
|
||||
|
|
@ -720,7 +719,6 @@ final class GatewayConnectionController {
|
|||
|
||||
var refreshedConfig = cfg
|
||||
refreshedConfig.nodeOptions = await self.makeConnectOptions(
|
||||
stableID: cfg.stableID,
|
||||
deviceAuthGatewayID: cfg.nodeOptions.deviceAuthGatewayID,
|
||||
allowStoredDeviceAuth: cfg.nodeOptions.allowStoredDeviceAuth)
|
||||
appModel.applyGatewayConnectConfig(refreshedConfig, expectedGeneration: generation)
|
||||
|
|
@ -1178,7 +1176,6 @@ extension GatewayConnectionController {
|
|||
guard !Task.isCancelled, generation == appModel.gatewayConnectGeneration else { return }
|
||||
}
|
||||
let nodeOptions = await self.makeConnectOptions(
|
||||
stableID: gatewayStableID,
|
||||
deviceAuthGatewayID: GatewaySettingsStore.authenticationOwnerID(routeStableID: gatewayStableID),
|
||||
allowStoredDeviceAuth: allowStoredDeviceAuth)
|
||||
// Permission reads above can suspend long enough for a model-owned reconnect reset
|
||||
|
|
@ -1295,7 +1292,6 @@ extension GatewayConnectionController {
|
|||
instanceId: GatewaySettingsStore.currentInstanceID(),
|
||||
gatewayStableID: stableID)
|
||||
let nodeOptions = await self.makeConnectOptions(
|
||||
stableID: stableID,
|
||||
deviceAuthGatewayID: GatewaySettingsStore.authenticationOwnerID(routeStableID: stableID),
|
||||
allowStoredDeviceAuth: !credentials.suppressStoredDeviceAuth)
|
||||
return GatewayConnectConfig(
|
||||
|
|
|
|||
|
|
@ -38,12 +38,10 @@ enum GatewaySettingsStore {
|
|||
}
|
||||
#endif
|
||||
private static let nodeService = "ai.openclawfoundation.app.node"
|
||||
private static let talkService = "ai.openclawfoundation.app.talk"
|
||||
|
||||
private static let instanceIdDefaultsKey = "node.instanceId"
|
||||
private static let preferredGatewayStableIDDefaultsKey = "gateway.preferredStableID"
|
||||
private static let lastDiscoveredGatewayStableIDDefaultsKey = "gateway.lastDiscoveredStableID"
|
||||
private static let clientIdOverrideDefaultsPrefix = "gateway.clientIdOverride."
|
||||
private static let selectedAgentDefaultsPrefix = "gateway.selectedAgentId."
|
||||
|
||||
private static let instanceIdAccount = "instanceId"
|
||||
|
|
@ -52,7 +50,6 @@ enum GatewaySettingsStore {
|
|||
private static let gatewayRegistryAccount = "gateway-registry"
|
||||
private static let lastGatewayConnectionAccount = "lastConnection"
|
||||
private static let gatewayCustomHeadersService = "ai.openclawfoundation.app.gateway.custom-headers"
|
||||
private static let talkProviderApiKeyAccountPrefix = "provider.apiKey." // pragma: allowlist secret
|
||||
|
||||
struct GatewayRegistryEntry: Codable, Equatable, Identifiable, Sendable {
|
||||
enum Kind: String, Codable, Sendable {
|
||||
|
|
@ -421,14 +418,6 @@ enum GatewaySettingsStore {
|
|||
var port: Int?
|
||||
}
|
||||
|
||||
static func loadTalkProviderApiKey(provider: String) -> String? {
|
||||
guard let providerId = self.normalizedTalkProviderID(provider) else { return nil }
|
||||
let account = self.talkProviderApiKeyAccount(providerId: providerId)
|
||||
return self.normalizedCredential(GenericPasswordKeychainStore.loadString(
|
||||
service: self.talkService,
|
||||
account: account))
|
||||
}
|
||||
|
||||
static func loadGatewayRegistry() -> GatewayRegistry {
|
||||
guard let json = GenericPasswordKeychainStore.loadString(
|
||||
service: self.gatewayService,
|
||||
|
|
@ -675,14 +664,6 @@ enum GatewaySettingsStore {
|
|||
return deletedAll
|
||||
}
|
||||
|
||||
static func loadGatewayClientIdOverride(stableID: String) -> String? {
|
||||
self.loadGatewayDefault(prefix: self.clientIdOverrideDefaultsPrefix, stableID: stableID)
|
||||
}
|
||||
|
||||
static func saveGatewayClientIdOverride(stableID: String, clientId: String?) {
|
||||
self.saveGatewayDefault(clientId, prefix: self.clientIdOverrideDefaultsPrefix, stableID: stableID)
|
||||
}
|
||||
|
||||
static func loadGatewaySelectedAgentId(stableID: String) -> String? {
|
||||
self.loadGatewayDefault(prefix: self.selectedAgentDefaultsPrefix, stableID: stableID)
|
||||
}
|
||||
|
|
@ -857,15 +838,6 @@ enum GatewaySettingsStore {
|
|||
account: "gateway-credential-metadata.\(instanceId)")
|
||||
}
|
||||
|
||||
private static func talkProviderApiKeyAccount(providerId: String) -> String {
|
||||
self.talkProviderApiKeyAccountPrefix + providerId
|
||||
}
|
||||
|
||||
private static func normalizedTalkProviderID(_ provider: String) -> String? {
|
||||
let trimmed = provider.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
return trimmed.isEmpty ? nil : trimmed
|
||||
}
|
||||
|
||||
private static func ensureStableInstanceID() {
|
||||
let defaults = UserDefaults.standard
|
||||
|
||||
|
|
|
|||
|
|
@ -311,7 +311,6 @@ final class NodeAppModel {
|
|||
private struct NodeGatewayLoopState: Sendable {
|
||||
var attempt = 0
|
||||
var options: GatewayConnectOptions
|
||||
var didFallbackClientID = false
|
||||
}
|
||||
|
||||
private enum NodeGatewayLoopStep: Sendable {
|
||||
|
|
@ -4387,12 +4386,10 @@ extension NodeAppModel {
|
|||
let reconnectOptions = self.currentGatewayReconnectOptions(
|
||||
stableID: stableID,
|
||||
fallback: config.nodeOptions)
|
||||
let effectiveClientId =
|
||||
GatewaySettingsStore.loadGatewayClientIdOverride(stableID: stableID) ?? reconnectOptions.clientId
|
||||
let talkPermissionUpgradeRequest = self.forceOperatorTalkPermissionUpgradeRequest
|
||||
let deviceAuthGatewayID = reconnectOptions.deviceAuthGatewayID ?? stableID
|
||||
let operatorOptions = self.makeOperatorConnectOptions(
|
||||
clientId: effectiveClientId,
|
||||
clientId: reconnectOptions.clientId,
|
||||
displayName: reconnectOptions.clientDisplayName,
|
||||
deviceAuthGatewayID: deviceAuthGatewayID,
|
||||
includeAdminScope: self.shouldRequestOperatorAdminScope(
|
||||
|
|
@ -4754,21 +4751,6 @@ extension NodeAppModel {
|
|||
stableID: context.stableID)
|
||||
else { return .stop }
|
||||
|
||||
if !state.didFallbackClientID,
|
||||
let fallbackClientID = self.legacyClientIdFallback(
|
||||
currentClientId: state.options.clientId,
|
||||
error: error)
|
||||
{
|
||||
var nextState = state
|
||||
nextState.didFallbackClientID = true
|
||||
nextState.options.clientId = fallbackClientID
|
||||
GatewaySettingsStore.saveGatewayClientIdOverride(
|
||||
stableID: context.stableID,
|
||||
clientId: fallbackClientID)
|
||||
self.gatewayStatusText = "Gateway rejected client id. Retrying…"
|
||||
return .retry(nextState)
|
||||
}
|
||||
|
||||
var nextState = state
|
||||
nextState.attempt += 1
|
||||
let problem = self.applyNodeGatewayConnectionError(
|
||||
|
|
@ -4959,16 +4941,6 @@ extension NodeAppModel {
|
|||
deviceAuthGatewayID: deviceAuthGatewayID)
|
||||
}
|
||||
|
||||
private func legacyClientIdFallback(currentClientId: String, error: Error) -> String? {
|
||||
let normalizedClientId = currentClientId.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
guard normalizedClientId == "openclaw-ios" else { return nil }
|
||||
let message = error.localizedDescription.lowercased()
|
||||
guard message.contains("invalid connect params"), message.contains("/client/id") else {
|
||||
return nil
|
||||
}
|
||||
return "moltbot-ios"
|
||||
}
|
||||
|
||||
func setOperatorConnected(_ connected: Bool) {
|
||||
let changed = self.operatorConnected != connected
|
||||
self.operatorConnected = connected
|
||||
|
|
|
|||
|
|
@ -4319,14 +4319,13 @@ extension TalkModeManager {
|
|||
}
|
||||
self.defaultOutputFormat = parsed.defaultOutputFormat
|
||||
|
||||
let credentialProvider = routing.route.usesRealtime
|
||||
? (routing.realtimeProvider ?? routing.activeProvider)
|
||||
: routing.activeProvider
|
||||
self.applyTalkConfigCredentials(
|
||||
parsed: parsed,
|
||||
activeProvider: routing.activeProvider,
|
||||
gatewayOwnsCredentials: routing.route.gatewayOwnsCredentials,
|
||||
credentialProvider: credentialProvider)
|
||||
self.apiKey = routing.route.gatewayOwnsCredentials ? nil : Self.normalizedTalkApiKey(parsed.rawConfigApiKey)
|
||||
if routing.route.gatewayOwnsCredentials {
|
||||
let credentialProvider = routing.route.usesRealtime
|
||||
? (routing.realtimeProvider ?? routing.activeProvider)
|
||||
: routing.activeProvider
|
||||
GatewayDiagnostics.log("talk provider '\(credentialProvider)' uses gateway-owned credentials")
|
||||
}
|
||||
self.applyTalkModeDescriptor(
|
||||
routing: routing,
|
||||
nativeModelId: routing.route == .localElevenLabs
|
||||
|
|
@ -4348,28 +4347,6 @@ extension TalkModeManager {
|
|||
}
|
||||
}
|
||||
|
||||
private func applyTalkConfigCredentials(
|
||||
parsed: TalkModeGatewayConfigState,
|
||||
activeProvider: String,
|
||||
gatewayOwnsCredentials: Bool,
|
||||
credentialProvider: String)
|
||||
{
|
||||
let rawConfigApiKey = parsed.rawConfigApiKey
|
||||
let configApiKey = Self.normalizedTalkApiKey(rawConfigApiKey)
|
||||
let localApiKey = Self.normalizedTalkApiKey(
|
||||
GatewaySettingsStore.loadTalkProviderApiKey(provider: activeProvider))
|
||||
if rawConfigApiKey == Self.redactedConfigSentinel {
|
||||
self.apiKey = localApiKey
|
||||
GatewayDiagnostics.log("talk config apiKey redacted; using local override if present")
|
||||
} else {
|
||||
self.apiKey = localApiKey ?? configApiKey
|
||||
}
|
||||
if gatewayOwnsCredentials {
|
||||
self.apiKey = nil
|
||||
GatewayDiagnostics.log("talk provider '\(credentialProvider)' uses gateway-owned credentials")
|
||||
}
|
||||
}
|
||||
|
||||
private func applyTalkModeDescriptor(
|
||||
routing: TalkModeResolvedRouting,
|
||||
nativeModelId: String?,
|
||||
|
|
|
|||
|
|
@ -289,7 +289,7 @@ private func waitUntil(
|
|||
]) {
|
||||
let appModel = NodeAppModel()
|
||||
let controller = GatewayConnectionController(appModel: appModel, startDiscovery: false)
|
||||
let options = await controller.makeConnectOptions(stableID: nil, deviceAuthGatewayID: nil)
|
||||
let options = await controller.makeConnectOptions(deviceAuthGatewayID: nil)
|
||||
let caps = Set(options.caps)
|
||||
|
||||
#expect(!caps.contains(OpenClawCapability.canvas.rawValue))
|
||||
|
|
@ -493,17 +493,17 @@ private func waitUntil(
|
|||
}
|
||||
|
||||
@Test func `stored device token scope gap uses gateway scope compatibility`() {
|
||||
#expect(!GatewayChannelActor._test_requestedScopesExceedStoredToken(
|
||||
#expect(!GatewayChannelActor.requestedScopesExceedStoredToken(
|
||||
role: "operator",
|
||||
requestedScopes: ["operator.read", "operator.write", "operator.talk.secrets"],
|
||||
storedToken: "stored-device-token",
|
||||
storedScopes: ["operator.admin"]))
|
||||
#expect(!GatewayChannelActor._test_requestedScopesExceedStoredToken(
|
||||
#expect(!GatewayChannelActor.requestedScopesExceedStoredToken(
|
||||
role: "operator",
|
||||
requestedScopes: ["operator.read"],
|
||||
storedToken: "stored-device-token",
|
||||
storedScopes: []))
|
||||
#expect(GatewayChannelActor._test_requestedScopesExceedStoredToken(
|
||||
#expect(GatewayChannelActor.requestedScopesExceedStoredToken(
|
||||
role: "operator",
|
||||
requestedScopes: ["operator.admin"],
|
||||
storedToken: "stored-device-token",
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ private func withLastGatewaySnapshot(_ body: () -> Void) {
|
|||
service: service)["X-Owner"] == "exact")
|
||||
}
|
||||
|
||||
@Test func `legacy gateway defaults cannot alias encoded owner keys`() {
|
||||
@Test func `legacy selected agent defaults cannot alias encoded owner keys`() {
|
||||
let exactOwner = "gateway-\(UUID().uuidString)"
|
||||
let component = Data(exactOwner.utf8).base64EncodedString()
|
||||
.replacingOccurrences(of: "+", with: "-")
|
||||
|
|
@ -204,20 +204,14 @@ private func withLastGatewaySnapshot(_ body: () -> Void) {
|
|||
.replacingOccurrences(of: "=", with: "")
|
||||
let collidingLegacyOwner = "v2.\(component)"
|
||||
defer {
|
||||
GatewaySettingsStore.saveGatewayClientIdOverride(stableID: exactOwner, clientId: nil)
|
||||
GatewaySettingsStore.saveGatewayClientIdOverride(stableID: collidingLegacyOwner, clientId: nil)
|
||||
GatewaySettingsStore.saveGatewaySelectedAgentId(stableID: exactOwner, agentId: nil)
|
||||
GatewaySettingsStore.saveGatewaySelectedAgentId(stableID: collidingLegacyOwner, agentId: nil)
|
||||
}
|
||||
|
||||
GatewaySettingsStore.saveGatewayClientIdOverride(stableID: exactOwner, clientId: "exact-client")
|
||||
GatewaySettingsStore.saveGatewaySelectedAgentId(stableID: exactOwner, agentId: "exact-agent")
|
||||
|
||||
#expect(GatewaySettingsStore.loadGatewayClientIdOverride(stableID: collidingLegacyOwner) == nil)
|
||||
#expect(GatewaySettingsStore.loadGatewaySelectedAgentId(stableID: collidingLegacyOwner) == nil)
|
||||
GatewaySettingsStore.saveGatewayClientIdOverride(stableID: collidingLegacyOwner, clientId: nil)
|
||||
GatewaySettingsStore.saveGatewaySelectedAgentId(stableID: collidingLegacyOwner, agentId: nil)
|
||||
#expect(GatewaySettingsStore.loadGatewayClientIdOverride(stableID: exactOwner) == "exact-client")
|
||||
#expect(GatewaySettingsStore.loadGatewaySelectedAgentId(stableID: exactOwner) == "exact-agent")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -105,22 +105,4 @@ extension MacGatewayChatTransport {
|
|||
request,
|
||||
ifCurrentServerLease: serverLease)
|
||||
}
|
||||
|
||||
func forkSession(parentKey: String) async throws -> String {
|
||||
try await self.forkSession(parentKey: parentKey, fromLastCompleted: false)
|
||||
}
|
||||
|
||||
func forkSession(parentKey: String, fromLastCompleted: Bool) async throws -> String {
|
||||
try await self.forkSession(parentKey: parentKey, fromLastCompleted: fromLastCompleted, agentID: nil)
|
||||
}
|
||||
|
||||
func forkSession(parentKey: String, fromLastCompleted: Bool, agentID: String?) async throws -> String {
|
||||
let target = self.sessionTarget(for: parentKey, overrideAgentID: agentID)
|
||||
let request = OpenClawChatGatewayRequests.forkSession(
|
||||
parentSessionKey: target.sessionKey,
|
||||
agentID: target.agentID,
|
||||
fromLastCompleted: fromLastCompleted)
|
||||
let data = try await self.requestChatSessionAction(request)
|
||||
return try JSONDecoder().decode(OpenClawChatCreateSessionResponse.self, from: data).key
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -652,10 +652,16 @@ struct MacGatewayChatTransport: OpenClawChatGatewayTransport {
|
|||
throw OpenClawChatTransportSendError.notDispatched
|
||||
}
|
||||
try await self.requireCurrentOutboxGateway()
|
||||
return try await MacChatMessageSpeechClient.synthesize(
|
||||
text: text,
|
||||
serverLease: serverLease,
|
||||
connection: self.connection)
|
||||
let encoded = try JSONEncoder().encode(TtsSpeakParams(text: text))
|
||||
guard let params = try JSONSerialization.jsonObject(with: encoded) as? [String: Any] else {
|
||||
throw MacChatMessageSpeechError.invalidRequest
|
||||
}
|
||||
let responseData = try await self.connection.request(
|
||||
method: "tts.speak",
|
||||
params: params.mapValues(AnyCodable.init),
|
||||
timeoutMs: 60000,
|
||||
ifCurrentServerLease: serverLease)
|
||||
return try OpenClawChatGatewayPayloadCodec.decodeSpeechClip(responseData)
|
||||
}
|
||||
|
||||
func loadSourceContext() async -> OpenClawChatSourceContext? {
|
||||
|
|
@ -687,10 +693,6 @@ struct MacGatewayChatTransport: OpenClawChatGatewayTransport {
|
|||
ifCurrentServerLease: serverLease)
|
||||
}
|
||||
|
||||
var supportsSlashCommandCatalog: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
func createSession(
|
||||
key: String,
|
||||
label: String?,
|
||||
|
|
@ -845,27 +847,6 @@ private enum MacChatMessageSpeechError: LocalizedError {
|
|||
}
|
||||
}
|
||||
|
||||
private enum MacChatMessageSpeechClient {
|
||||
private static let requestTimeoutMs: Double = 60000
|
||||
|
||||
static func synthesize(
|
||||
text: String,
|
||||
serverLease: GatewayConnection.ServerLease,
|
||||
connection: GatewayConnection) async throws -> OpenClawChatSpeechClip
|
||||
{
|
||||
let encoded = try JSONEncoder().encode(TtsSpeakParams(text: text))
|
||||
guard let params = try JSONSerialization.jsonObject(with: encoded) as? [String: Any] else {
|
||||
throw MacChatMessageSpeechError.invalidRequest
|
||||
}
|
||||
let responseData = try await connection.request(
|
||||
method: "tts.speak",
|
||||
params: params.mapValues(AnyCodable.init),
|
||||
timeoutMs: self.requestTimeoutMs,
|
||||
ifCurrentServerLease: serverLease)
|
||||
return try OpenClawChatGatewayPayloadCodec.decodeSpeechClip(responseData)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private struct MacChatSurface: View {
|
||||
let windowCommands: OpenClawChatWindowCommands
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ struct HealthDecodeTests {
|
|||
let snap = decodeHealthSnapshot(from: data)
|
||||
|
||||
#expect(snap?.channels["whatsapp"]?.linked == true)
|
||||
#expect(snap?.channels["telegram"]?.probe?.elapsedMs == 800)
|
||||
#expect(snap?.sessions.count == 1)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1915,10 +1915,10 @@ struct OnboardingAISetupTests {
|
|||
}
|
||||
|
||||
await startGate.waitUntilStarted()
|
||||
model.cancelProviderAuth()
|
||||
let cancellation = try #require(model.cancelProviderAuth())
|
||||
try #require(model.activeAuthOption != nil)
|
||||
do {
|
||||
_ = try await waitForAISetupRequests(harness.recorder, count: 3)
|
||||
await cancellation.value
|
||||
await startGate.release()
|
||||
try await TestWait.observed("settled provider auth") { model.activeAuthOption == nil }
|
||||
try #require(model.activeAuthOption == nil)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ public protocol OpenClawChatGatewayTransport: OpenClawChatTransport {
|
|||
}
|
||||
|
||||
extension OpenClawChatGatewayTransport {
|
||||
public var supportsSlashCommandCatalog: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
public func reactionsRouteLease(
|
||||
routeID: UUID,
|
||||
access: OpenClawChatReactionAccess,
|
||||
|
|
@ -162,6 +166,24 @@ extension OpenClawChatGatewayTransport {
|
|||
return try JSONDecoder().decode(OpenClawChatRewindResponse.self, from: data)
|
||||
}
|
||||
|
||||
public func forkSession(parentKey: String) async throws -> String {
|
||||
try await self.forkSession(parentKey: parentKey, fromLastCompleted: false)
|
||||
}
|
||||
|
||||
public func forkSession(parentKey: String, fromLastCompleted: Bool) async throws -> String {
|
||||
try await self.forkSession(parentKey: parentKey, fromLastCompleted: fromLastCompleted, agentID: nil)
|
||||
}
|
||||
|
||||
public func forkSession(parentKey: String, fromLastCompleted: Bool, agentID: String?) async throws -> String {
|
||||
let target = self.sessionTarget(for: parentKey, overrideAgentID: agentID)
|
||||
let request = OpenClawChatGatewayRequests.forkSession(
|
||||
parentSessionKey: target.sessionKey,
|
||||
agentID: target.agentID,
|
||||
fromLastCompleted: fromLastCompleted)
|
||||
let data = try await self.requestChatSessionAction(request)
|
||||
return try JSONDecoder().decode(OpenClawChatCreateSessionResponse.self, from: data).key
|
||||
}
|
||||
|
||||
public func forkSessionAtMessage(
|
||||
sessionKey: String,
|
||||
entryId: String) async throws -> OpenClawChatForkAtMessageResponse
|
||||
|
|
|
|||
|
|
@ -698,20 +698,7 @@ extension GatewayChannelActor {
|
|||
suppressedDeviceTokenRetry: suppressedDeviceTokenRetry)
|
||||
}
|
||||
|
||||
nonisolated static func _test_requestedScopesExceedStoredToken(
|
||||
role: String,
|
||||
requestedScopes: [String],
|
||||
storedToken: String?,
|
||||
storedScopes: [String]) -> Bool
|
||||
{
|
||||
self.requestedScopesExceedStoredToken(
|
||||
role: role,
|
||||
requestedScopes: requestedScopes,
|
||||
storedToken: storedToken,
|
||||
storedScopes: storedScopes)
|
||||
}
|
||||
|
||||
private nonisolated static func requestedScopesExceedStoredToken(
|
||||
nonisolated static func requestedScopesExceedStoredToken(
|
||||
role: String,
|
||||
requestedScopes: [String],
|
||||
storedToken: String?,
|
||||
|
|
@ -1101,7 +1088,7 @@ extension GatewayChannelActor {
|
|||
}
|
||||
switch frame {
|
||||
case let .res(res):
|
||||
self.finishRequest(id: res.id, result: .success(.res(res)))
|
||||
self.finishRequest(id: res.id, result: .success(res))
|
||||
case let .event(evt):
|
||||
if evt.event == "connect.challenge" { return }
|
||||
if let seq = evt.seq {
|
||||
|
|
@ -1428,11 +1415,11 @@ extension GatewayChannelActor {
|
|||
let effectiveTimeout = Self.resolveRequestTimeoutMs(timeoutMs, defaultMs: self.defaultRequestTimeoutMs)
|
||||
let payload = try self.encodeRequest(method: method, params: params, kind: "request")
|
||||
let cancellationGate = GatewayRequestCancellationGate()
|
||||
let response: GatewayFrame
|
||||
let response: ResponseFrame
|
||||
do {
|
||||
response = try await withTaskCancellationHandler {
|
||||
try Task.checkCancellation()
|
||||
return try await withCheckedThrowingContinuation { (cont: CheckedContinuation<GatewayFrame, Error>) in
|
||||
return try await withCheckedThrowingContinuation { (cont: CheckedContinuation<ResponseFrame, Error>) in
|
||||
guard !cancellationGate.isCancelled else {
|
||||
cont.resume(throwing: CancellationError())
|
||||
return
|
||||
|
|
@ -1494,16 +1481,13 @@ extension GatewayChannelActor {
|
|||
}
|
||||
#endif
|
||||
try Task.checkCancellation()
|
||||
guard case let .res(res) = response else {
|
||||
throw NSError(domain: "Gateway", code: 2, userInfo: [NSLocalizedDescriptionKey: "unexpected frame"])
|
||||
}
|
||||
if res.ok == false {
|
||||
let code = res.error?.code
|
||||
let msg = res.error?.message
|
||||
let details = gatewayErrorDetails(res.error)
|
||||
if response.ok == false {
|
||||
let code = response.error?.code
|
||||
let msg = response.error?.message
|
||||
let details = gatewayErrorDetails(response.error)
|
||||
throw GatewayResponseError(method: method, code: code, message: msg, details: details)
|
||||
}
|
||||
if let payload = res.payload {
|
||||
if let payload = response.payload {
|
||||
// Encode back to JSON with Swift's encoder to preserve types and avoid ObjC bridging exceptions.
|
||||
return try self.encoder.encode(payload)
|
||||
}
|
||||
|
|
@ -1621,7 +1605,7 @@ extension GatewayChannelActor {
|
|||
}
|
||||
}
|
||||
|
||||
private func finishRequest(id: String, result: Result<GatewayFrame, Error>) {
|
||||
private func finishRequest(id: String, result: Result<ResponseFrame, Error>) {
|
||||
guard let request = self.pending.removeValue(forKey: id) else { return }
|
||||
// A deadline belongs to its pending request, including after caller cancellation or disconnect.
|
||||
request.timeoutTask?.cancel()
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ final class GatewayRequestCancellationGate: @unchecked Sendable {
|
|||
|
||||
extension GatewayChannelActor {
|
||||
struct PendingRequest {
|
||||
let continuation: CheckedContinuation<GatewayFrame, Error>
|
||||
let continuation: CheckedContinuation<ResponseFrame, Error>
|
||||
var timeoutTask: Task<Void, Never>?
|
||||
let transportLifetime = WebSocketRequestLifetime()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,18 +94,32 @@ struct ChatGatewayTransportTests {
|
|||
if request.method == "sessions.rewind" {
|
||||
return Data(#"{"editorText":"restored draft"}"#.utf8)
|
||||
}
|
||||
if request.method == "sessions.create" {
|
||||
return Data(#"{"key":"agent:reviewer:child"}"#.utf8)
|
||||
}
|
||||
throw Failure.retiredRoute
|
||||
})
|
||||
|
||||
let response = try await transport.rewindSession(sessionKey: "global", entryId: "message-1")
|
||||
#expect(response.editorText == "restored draft")
|
||||
#expect(try await transport.forkSession(parentKey: "global") == "agent:reviewer:child")
|
||||
#expect(try await transport.forkSession(
|
||||
parentKey: "global", fromLastCompleted: true, agentID: "other") == "agent:reviewer:child")
|
||||
await #expect(throws: Failure.retiredRoute) {
|
||||
try await transport.switchSessionBranch(sessionKey: "global", agentID: "other", leafEntryId: "leaf-1")
|
||||
}
|
||||
let requests = await recorder.requests
|
||||
#expect(requests.map(\.method) == ["sessions.rewind", "sessions.branches.switch"])
|
||||
#expect(requests.map(\.method) == [
|
||||
"sessions.rewind", "sessions.create", "sessions.create", "sessions.branches.switch",
|
||||
])
|
||||
#expect(requests[0].params["agentId"]?.value as? String == "reviewer")
|
||||
#expect(requests[1].params["agentId"]?.value as? String == "other")
|
||||
#expect(requests[1].params["agentId"]?.value as? String == "reviewer")
|
||||
#expect(requests[1].params["parentSessionKey"]?.value as? String == "global")
|
||||
#expect(requests[1].params["fork"]?.value as? Bool == true)
|
||||
#expect(requests[1].params["forkFrom"] == nil)
|
||||
#expect(requests[2].params["agentId"]?.value as? String == "other")
|
||||
#expect(requests[2].params["forkFrom"]?.value as? String == "last-completed")
|
||||
#expect(requests[3].params["agentId"]?.value as? String == "other")
|
||||
}
|
||||
|
||||
private enum Failure: Error { case unexpectedRequest, retiredRoute }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue