mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
fix(protocol): preserve AnyCodable booleans from JSON bridge (#20220)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 1d86183e3b
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
This commit is contained in:
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- OpenClawKit/Protocol: preserve JSON boolean literals (`true`/`false`) when bridging through `AnyCodable` so Apple client RPC params no longer re-encode booleans as `1`/`0`. Thanks @mbelinky.
|
||||
- iOS/Onboarding: stabilize pairing and reconnect behavior by resetting stale pairing request state on manual retry, disconnecting both operator and node gateways on operator failure, and avoiding duplicate pairing loops from operator transport identity attachment. (#20056) Thanks @mbelinky.
|
||||
- Browser/Relay: reuse an already-running extension relay when the relay port is occupied by another OpenClaw process, while still failing on non-relay port collisions to avoid masking unrelated listeners. (#20035) Thanks @mbelinky.
|
||||
- Telegram/Cron/Heartbeat: honor explicit Telegram topic targets in cron and heartbeat delivery (`<chatId>:topic:<threadId>`) so scheduled sends land in the configured topic instead of the last active thread. (#19367) Thanks @Lukavyi.
|
||||
|
||||
@@ -6,13 +6,13 @@ import Foundation
|
||||
public struct AnyCodable: Codable, @unchecked Sendable, Hashable {
|
||||
public let value: Any
|
||||
|
||||
public init(_ value: Any) { self.value = value }
|
||||
public init(_ value: Any) { self.value = Self.normalize(value) }
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
if let boolVal = try? container.decode(Bool.self) { self.value = boolVal; return }
|
||||
if let intVal = try? container.decode(Int.self) { self.value = intVal; return }
|
||||
if let doubleVal = try? container.decode(Double.self) { self.value = doubleVal; return }
|
||||
if let boolVal = try? container.decode(Bool.self) { self.value = boolVal; return }
|
||||
if let stringVal = try? container.decode(String.self) { self.value = stringVal; return }
|
||||
if container.decodeNil() { self.value = NSNull(); return }
|
||||
if let dict = try? container.decode([String: AnyCodable].self) { self.value = dict; return }
|
||||
@@ -23,10 +23,12 @@ public struct AnyCodable: Codable, @unchecked Sendable, Hashable {
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
switch self.value {
|
||||
case let boolVal as Bool: try container.encode(boolVal)
|
||||
case let intVal as Int: try container.encode(intVal)
|
||||
case let doubleVal as Double: try container.encode(doubleVal)
|
||||
case let boolVal as Bool: try container.encode(boolVal)
|
||||
case let stringVal as String: try container.encode(stringVal)
|
||||
case let number as NSNumber where CFGetTypeID(number) == CFBooleanGetTypeID():
|
||||
try container.encode(number.boolValue)
|
||||
case is NSNull: try container.encodeNil()
|
||||
case let dict as [String: AnyCodable]: try container.encode(dict)
|
||||
case let array as [AnyCodable]: try container.encode(array)
|
||||
@@ -51,6 +53,13 @@ public struct AnyCodable: Codable, @unchecked Sendable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
private static func normalize(_ value: Any) -> Any {
|
||||
if let number = value as? NSNumber, CFGetTypeID(number) == CFBooleanGetTypeID() {
|
||||
return number.boolValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
public static func == (lhs: AnyCodable, rhs: AnyCodable) -> Bool {
|
||||
switch (lhs.value, rhs.value) {
|
||||
case let (l as Int, r as Int): l == r
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
import OpenClawProtocol
|
||||
|
||||
struct AnyCodableTests {
|
||||
@Test
|
||||
func encodesNSNumberBooleansAsJSONBooleans() throws {
|
||||
let trueData = try JSONEncoder().encode(AnyCodable(NSNumber(value: true)))
|
||||
let falseData = try JSONEncoder().encode(AnyCodable(NSNumber(value: false)))
|
||||
|
||||
#expect(String(data: trueData, encoding: .utf8) == "true")
|
||||
#expect(String(data: falseData, encoding: .utf8) == "false")
|
||||
}
|
||||
|
||||
@Test
|
||||
func preservesBooleanLiteralsFromJSONSerializationBridge() throws {
|
||||
let raw = try #require(
|
||||
JSONSerialization.jsonObject(with: Data(#"{"enabled":true,"nested":{"active":false}}"#.utf8))
|
||||
as? [String: Any]
|
||||
)
|
||||
let enabled = try #require(raw["enabled"])
|
||||
let nested = try #require(raw["nested"])
|
||||
|
||||
struct RequestEnvelope: Codable {
|
||||
let params: [String: AnyCodable]
|
||||
}
|
||||
|
||||
let envelope = RequestEnvelope(
|
||||
params: [
|
||||
"enabled": AnyCodable(enabled),
|
||||
"nested": AnyCodable(nested),
|
||||
]
|
||||
)
|
||||
let data = try JSONEncoder().encode(envelope)
|
||||
let json = try #require(String(data: data, encoding: .utf8))
|
||||
|
||||
#expect(json.contains(#""enabled":true"#))
|
||||
#expect(json.contains(#""active":false"#))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user