chore: format swift/ts and fix gateway lint
This commit is contained in:
@@ -26,11 +26,14 @@ final class AppState: ObservableObject {
|
||||
}
|
||||
|
||||
@Published var onboardingSeen: Bool {
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.onboardingSeen, forKey: "clawdis.onboardingSeen") } }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.onboardingSeen, forKey: "clawdis.onboardingSeen") }
|
||||
}
|
||||
}
|
||||
|
||||
@Published var debugPaneEnabled: Bool {
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.debugPaneEnabled, forKey: "clawdis.debugPaneEnabled") } }
|
||||
didSet {
|
||||
self.ifNotPreview { UserDefaults.standard.set(self.debugPaneEnabled, forKey: "clawdis.debugPaneEnabled") }
|
||||
}
|
||||
}
|
||||
|
||||
@Published var swabbleEnabled: Bool {
|
||||
@@ -63,7 +66,9 @@ final class AppState: ObservableObject {
|
||||
}
|
||||
|
||||
@Published var iconAnimationsEnabled: Bool {
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.iconAnimationsEnabled, forKey: iconAnimationsEnabledKey) } }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(
|
||||
self.iconAnimationsEnabled,
|
||||
forKey: iconAnimationsEnabledKey) } }
|
||||
}
|
||||
|
||||
@Published var showDockIcon: Bool {
|
||||
@@ -98,19 +103,27 @@ final class AppState: ObservableObject {
|
||||
}
|
||||
|
||||
@Published var voiceWakeAdditionalLocaleIDs: [String] {
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.voiceWakeAdditionalLocaleIDs, forKey: voiceWakeAdditionalLocalesKey) } }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(
|
||||
self.voiceWakeAdditionalLocaleIDs,
|
||||
forKey: voiceWakeAdditionalLocalesKey) } }
|
||||
}
|
||||
|
||||
@Published var voiceWakeForwardEnabled: Bool {
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.voiceWakeForwardEnabled, forKey: voiceWakeForwardEnabledKey) } }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(
|
||||
self.voiceWakeForwardEnabled,
|
||||
forKey: voiceWakeForwardEnabledKey) } }
|
||||
}
|
||||
|
||||
@Published var voiceWakeForwardCommand: String {
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.voiceWakeForwardCommand, forKey: voiceWakeForwardCommandKey) } }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(
|
||||
self.voiceWakeForwardCommand,
|
||||
forKey: voiceWakeForwardCommandKey) } }
|
||||
}
|
||||
|
||||
@Published var voicePushToTalkEnabled: Bool {
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.voicePushToTalkEnabled, forKey: voicePushToTalkEnabledKey) } }
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(
|
||||
self.voicePushToTalkEnabled,
|
||||
forKey: voicePushToTalkEnabledKey) } }
|
||||
}
|
||||
|
||||
@Published var iconOverride: IconOverrideSelection {
|
||||
@@ -131,7 +144,9 @@ final class AppState: ObservableObject {
|
||||
}
|
||||
|
||||
@Published var connectionMode: ConnectionMode {
|
||||
didSet { self.ifNotPreview { UserDefaults.standard.set(self.connectionMode.rawValue, forKey: connectionModeKey) } }
|
||||
didSet {
|
||||
self.ifNotPreview { UserDefaults.standard.set(self.connectionMode.rawValue, forKey: connectionModeKey) }
|
||||
}
|
||||
}
|
||||
|
||||
@Published var webChatEnabled: Bool {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ClawdisProtocol
|
||||
import Foundation
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
import ClawdisProtocol
|
||||
|
||||
struct ControlHeartbeatEvent: Codable {
|
||||
let ts: Double
|
||||
@@ -14,7 +14,7 @@ struct ControlHeartbeatEvent: Codable {
|
||||
}
|
||||
|
||||
struct ControlAgentEvent: Codable, Sendable, Identifiable {
|
||||
var id: String { "\(runId)-\(seq)" }
|
||||
var id: String { "\(self.runId)-\(self.seq)" }
|
||||
let runId: String
|
||||
let seq: Int
|
||||
let stream: String
|
||||
@@ -173,7 +173,8 @@ final class ControlChannel: ObservableObject {
|
||||
if let data = evt.payload?.value,
|
||||
JSONSerialization.isValidJSONObject(data),
|
||||
let blob = try? JSONSerialization.data(withJSONObject: data),
|
||||
let agent = try? JSONDecoder().decode(AgentEvent.self, from: blob) {
|
||||
let agent = try? JSONDecoder().decode(AgentEvent.self, from: blob)
|
||||
{
|
||||
Task { @MainActor in
|
||||
AgentEventStore.shared.append(ControlAgentEvent(
|
||||
runId: agent.runid,
|
||||
|
||||
@@ -108,7 +108,8 @@ enum DebugActionError: LocalizedError {
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case let .message(text): text
|
||||
case let .message(text):
|
||||
text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import ClawdisProtocol
|
||||
import Foundation
|
||||
import OSLog
|
||||
import ClawdisProtocol
|
||||
|
||||
struct GatewayEvent: Codable {
|
||||
let type: String
|
||||
@@ -27,7 +27,7 @@ private actor GatewayChannelActor {
|
||||
private var shouldReconnect = true
|
||||
private var lastSeq: Int?
|
||||
private var lastTick: Date?
|
||||
private var tickIntervalMs: Double = 30_000
|
||||
private var tickIntervalMs: Double = 30000
|
||||
private let decoder = JSONDecoder()
|
||||
private let encoder = JSONEncoder()
|
||||
|
||||
@@ -88,7 +88,8 @@ private actor GatewayChannelActor {
|
||||
let type = obj["type"] as? String else { return false }
|
||||
if type == "hello-ok" {
|
||||
if let policy = obj["policy"] as? [String: Any],
|
||||
let tick = policy["tickIntervalMs"] as? Double {
|
||||
let tick = policy["tickIntervalMs"] as? Double
|
||||
{
|
||||
self.tickIntervalMs = tick
|
||||
}
|
||||
self.lastTick = Date()
|
||||
|
||||
@@ -296,7 +296,9 @@ struct GeneralSettings: View {
|
||||
.disabled(self.relayInstalling)
|
||||
}
|
||||
|
||||
Text(self.relayInstallMessage ?? "Installs the global \"clawdis\" package and expects the gateway on port 18789.")
|
||||
Text(self
|
||||
.relayInstallMessage ??
|
||||
"Installs the global \"clawdis\" package and expects the gateway on port 18789.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
|
||||
@@ -151,15 +151,15 @@ final class HealthStore: ObservableObject {
|
||||
|
||||
/// Short, human-friendly detail for the last failure, used in the UI.
|
||||
var detailLine: String? {
|
||||
if let error = self.lastError, !error.isEmpty {
|
||||
let lower = error.lowercased()
|
||||
if lower.contains("connection refused") {
|
||||
return "The gateway control port (127.0.0.1:18789) isn’t listening — restart Clawdis to bring it back."
|
||||
}
|
||||
if lower.contains("timeout") {
|
||||
return "Timed out waiting for the control server; the relay may be crashed or still starting."
|
||||
}
|
||||
return error
|
||||
if let error = self.lastError, !error.isEmpty {
|
||||
let lower = error.lowercased()
|
||||
if lower.contains("connection refused") {
|
||||
return "The gateway control port (127.0.0.1:18789) isn’t listening — restart Clawdis to bring it back."
|
||||
}
|
||||
if lower.contains("timeout") {
|
||||
return "Timed out waiting for the control server; the relay may be crashed or still starting."
|
||||
}
|
||||
return error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ClawdisProtocol
|
||||
import Cocoa
|
||||
import Foundation
|
||||
import OSLog
|
||||
import ClawdisProtocol
|
||||
|
||||
struct InstanceInfo: Identifiable, Codable {
|
||||
let id: String
|
||||
@@ -79,7 +79,8 @@ final class InstancesStore: ObservableObject {
|
||||
case let .event(evt) where evt.event == "presence":
|
||||
if let payload = evt.payload?.value as? [String: Any],
|
||||
let presence = payload["presence"],
|
||||
let presenceData = try? JSONSerialization.data(withJSONObject: presence) {
|
||||
let presenceData = try? JSONSerialization.data(withJSONObject: presence)
|
||||
{
|
||||
Task { @MainActor [weak self] in self?.decodeAndApplyPresenceData(presenceData) }
|
||||
}
|
||||
default:
|
||||
@@ -104,7 +105,8 @@ final class InstancesStore: ObservableObject {
|
||||
switch frame {
|
||||
case let .helloOk(hello):
|
||||
if JSONSerialization.isValidJSONObject(hello.snapshot.presence),
|
||||
let data = try? JSONEncoder().encode(hello.snapshot.presence) {
|
||||
let data = try? JSONEncoder().encode(hello.snapshot.presence)
|
||||
{
|
||||
Task { @MainActor [weak self] in self?.decodeAndApplyPresenceData(data) }
|
||||
}
|
||||
default:
|
||||
@@ -323,7 +325,7 @@ extension InstancesStore {
|
||||
mode: "remote",
|
||||
reason: "preview",
|
||||
text: "Relay node · tunnel ok",
|
||||
ts: Date().timeIntervalSince1970 * 1000 - 45_000),
|
||||
ts: Date().timeIntervalSince1970 * 1000 - 45000),
|
||||
]) -> InstancesStore {
|
||||
let store = InstancesStore(isPreview: true)
|
||||
store.instances = instances
|
||||
|
||||
@@ -181,7 +181,8 @@ struct OnboardingView: View {
|
||||
self.onboardingPage {
|
||||
Text("Install the relay")
|
||||
.font(.largeTitle.weight(.semibold))
|
||||
Text("Clawdis now runs the WebSocket gateway from the global \"clawdis\" package. Install/update it here and we’ll check Node for you.")
|
||||
Text(
|
||||
"Clawdis now runs the WebSocket gateway from the global \"clawdis\" package. Install/update it here and we’ll check Node for you.")
|
||||
.font(.body)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
@@ -132,9 +132,8 @@ struct PermissionsSettings_Previews: PreviewProvider {
|
||||
.speechRecognition: false,
|
||||
],
|
||||
refresh: {},
|
||||
showOnboarding: {}
|
||||
)
|
||||
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
|
||||
showOnboarding: {})
|
||||
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Foundation
|
||||
import ClawdisIPC
|
||||
import Foundation
|
||||
|
||||
// Lightweight SemVer helper (major.minor.patch only) for relay compatibility checks.
|
||||
struct Semver: Comparable, CustomStringConvertible, Sendable {
|
||||
|
||||
@@ -126,7 +126,7 @@ extension SessionRow {
|
||||
id: "global",
|
||||
key: "global",
|
||||
kind: .global,
|
||||
updatedAt: Date().addingTimeInterval(-86_400),
|
||||
updatedAt: Date().addingTimeInterval(-86400),
|
||||
sessionId: nil,
|
||||
thinkingLevel: nil,
|
||||
verboseLevel: nil,
|
||||
|
||||
@@ -101,7 +101,14 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate {
|
||||
|
||||
private func loadWebChat(baseEndpoint: URL) {
|
||||
var comps = URLComponents(url: baseEndpoint.appendingPathComponent("webchat/"), resolvingAgainstBaseURL: false)
|
||||
comps?.queryItems = [URLQueryItem(name: "session", value: self.sessionKey)]
|
||||
var items = [URLQueryItem(name: "session", value: self.sessionKey)]
|
||||
if let hostName = Host.current().localizedName ?? Host.current().name {
|
||||
items.append(URLQueryItem(name: "host", value: hostName))
|
||||
}
|
||||
if let ip = Self.primaryIPv4Address() {
|
||||
items.append(URLQueryItem(name: "ip", value: ip))
|
||||
}
|
||||
comps?.queryItems = items
|
||||
guard let url = comps?.url else {
|
||||
self.showError("invalid webchat url")
|
||||
return
|
||||
@@ -197,6 +204,37 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
extension WebChatWindowController {
|
||||
/// Returns the first non-loopback IPv4 address, skipping link-local (169.254.x.x).
|
||||
fileprivate static func primaryIPv4Address() -> String? {
|
||||
var ifaddr: UnsafeMutablePointer<ifaddrs>?
|
||||
guard getifaddrs(&ifaddr) == 0, let first = ifaddr else { return nil }
|
||||
defer { freeifaddrs(ifaddr) }
|
||||
|
||||
for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) {
|
||||
let flags = Int32(ptr.pointee.ifa_flags)
|
||||
let addrFamily = ptr.pointee.ifa_addr.pointee.sa_family
|
||||
if (flags & IFF_UP) == 0 || (flags & IFF_LOOPBACK) != 0 { continue }
|
||||
if addrFamily == UInt8(AF_INET) {
|
||||
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
||||
if getnameinfo(
|
||||
ptr.pointee.ifa_addr,
|
||||
socklen_t(ptr.pointee.ifa_addr.pointee.sa_len),
|
||||
&hostname,
|
||||
socklen_t(hostname.count),
|
||||
nil,
|
||||
0,
|
||||
NI_NUMERICHOST) == 0
|
||||
{
|
||||
let ip = String(cString: hostname)
|
||||
if !ip.hasPrefix("169.254") { return ip }
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Manager
|
||||
|
||||
@MainActor
|
||||
|
||||
@@ -28,8 +28,8 @@ public struct Hello: Codable {
|
||||
caps: [String]?,
|
||||
auth: [String: AnyCodable]?,
|
||||
locale: String?,
|
||||
useragent: String?
|
||||
) {
|
||||
useragent: String?)
|
||||
{
|
||||
self.type = type
|
||||
self.minprotocol = minprotocol
|
||||
self.maxprotocol = maxprotocol
|
||||
@@ -39,6 +39,7 @@ public struct Hello: Codable {
|
||||
self.locale = locale
|
||||
self.useragent = useragent
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case minprotocol = "minProtocol"
|
||||
@@ -65,8 +66,8 @@ public struct HelloOk: Codable {
|
||||
server: [String: AnyCodable],
|
||||
features: [String: AnyCodable],
|
||||
snapshot: Snapshot,
|
||||
policy: [String: AnyCodable]
|
||||
) {
|
||||
policy: [String: AnyCodable])
|
||||
{
|
||||
self.type = type
|
||||
self._protocol = _protocol
|
||||
self.server = server
|
||||
@@ -74,6 +75,7 @@ public struct HelloOk: Codable {
|
||||
self.snapshot = snapshot
|
||||
self.policy = policy
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case _protocol = "protocol"
|
||||
@@ -94,13 +96,14 @@ public struct HelloError: Codable {
|
||||
type: String,
|
||||
reason: String,
|
||||
expectedprotocol: Int?,
|
||||
minclient: String?
|
||||
) {
|
||||
minclient: String?)
|
||||
{
|
||||
self.type = type
|
||||
self.reason = reason
|
||||
self.expectedprotocol = expectedprotocol
|
||||
self.minclient = minclient
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case reason
|
||||
@@ -119,13 +122,14 @@ public struct RequestFrame: Codable {
|
||||
type: String,
|
||||
id: String,
|
||||
method: String,
|
||||
params: AnyCodable?
|
||||
) {
|
||||
params: AnyCodable?)
|
||||
{
|
||||
self.type = type
|
||||
self.id = id
|
||||
self.method = method
|
||||
self.params = params
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case id
|
||||
@@ -146,14 +150,15 @@ public struct ResponseFrame: Codable {
|
||||
id: String,
|
||||
ok: Bool,
|
||||
payload: AnyCodable?,
|
||||
error: [String: AnyCodable]?
|
||||
) {
|
||||
error: [String: AnyCodable]?)
|
||||
{
|
||||
self.type = type
|
||||
self.id = id
|
||||
self.ok = ok
|
||||
self.payload = payload
|
||||
self.error = error
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case id
|
||||
@@ -175,14 +180,15 @@ public struct EventFrame: Codable {
|
||||
event: String,
|
||||
payload: AnyCodable?,
|
||||
seq: Int?,
|
||||
stateversion: [String: AnyCodable]?
|
||||
) {
|
||||
stateversion: [String: AnyCodable]?)
|
||||
{
|
||||
self.type = type
|
||||
self.event = event
|
||||
self.payload = payload
|
||||
self.seq = seq
|
||||
self.stateversion = stateversion
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case event
|
||||
@@ -214,8 +220,8 @@ public struct PresenceEntry: Codable {
|
||||
tags: [String]?,
|
||||
text: String?,
|
||||
ts: Int,
|
||||
instanceid: String?
|
||||
) {
|
||||
instanceid: String?)
|
||||
{
|
||||
self.host = host
|
||||
self.ip = ip
|
||||
self.version = version
|
||||
@@ -227,6 +233,7 @@ public struct PresenceEntry: Codable {
|
||||
self.ts = ts
|
||||
self.instanceid = instanceid
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case host
|
||||
case ip
|
||||
@@ -247,11 +254,12 @@ public struct StateVersion: Codable {
|
||||
|
||||
public init(
|
||||
presence: Int,
|
||||
health: Int
|
||||
) {
|
||||
health: Int)
|
||||
{
|
||||
self.presence = presence
|
||||
self.health = health
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case presence
|
||||
case health
|
||||
@@ -268,13 +276,14 @@ public struct Snapshot: Codable {
|
||||
presence: [PresenceEntry],
|
||||
health: AnyCodable,
|
||||
stateversion: StateVersion,
|
||||
uptimems: Int
|
||||
) {
|
||||
uptimems: Int)
|
||||
{
|
||||
self.presence = presence
|
||||
self.health = health
|
||||
self.stateversion = stateversion
|
||||
self.uptimems = uptimems
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case presence
|
||||
case health
|
||||
@@ -295,14 +304,15 @@ public struct ErrorShape: Codable {
|
||||
message: String,
|
||||
details: AnyCodable?,
|
||||
retryable: Bool?,
|
||||
retryafterms: Int?
|
||||
) {
|
||||
retryafterms: Int?)
|
||||
{
|
||||
self.code = code
|
||||
self.message = message
|
||||
self.details = details
|
||||
self.retryable = retryable
|
||||
self.retryafterms = retryafterms
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case code
|
||||
case message
|
||||
@@ -324,14 +334,15 @@ public struct AgentEvent: Codable {
|
||||
seq: Int,
|
||||
stream: String,
|
||||
ts: Int,
|
||||
data: [String: AnyCodable]
|
||||
) {
|
||||
data: [String: AnyCodable])
|
||||
{
|
||||
self.runid = runid
|
||||
self.seq = seq
|
||||
self.stream = stream
|
||||
self.ts = ts
|
||||
self.data = data
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case runid = "runId"
|
||||
case seq
|
||||
@@ -353,14 +364,15 @@ public struct SendParams: Codable {
|
||||
message: String,
|
||||
mediaurl: String?,
|
||||
provider: String?,
|
||||
idempotencykey: String
|
||||
) {
|
||||
idempotencykey: String)
|
||||
{
|
||||
self.to = to
|
||||
self.message = message
|
||||
self.mediaurl = mediaurl
|
||||
self.provider = provider
|
||||
self.idempotencykey = idempotencykey
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case to
|
||||
case message
|
||||
@@ -386,8 +398,8 @@ public struct AgentParams: Codable {
|
||||
thinking: String?,
|
||||
deliver: Bool?,
|
||||
timeout: Int?,
|
||||
idempotencykey: String
|
||||
) {
|
||||
idempotencykey: String)
|
||||
{
|
||||
self.message = message
|
||||
self.to = to
|
||||
self.sessionid = sessionid
|
||||
@@ -396,6 +408,7 @@ public struct AgentParams: Codable {
|
||||
self.timeout = timeout
|
||||
self.idempotencykey = idempotencykey
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case message
|
||||
case to
|
||||
@@ -411,10 +424,11 @@ public struct TickEvent: Codable {
|
||||
public let ts: Int
|
||||
|
||||
public init(
|
||||
ts: Int
|
||||
) {
|
||||
ts: Int)
|
||||
{
|
||||
self.ts = ts
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case ts
|
||||
}
|
||||
@@ -426,11 +440,12 @@ public struct ShutdownEvent: Codable {
|
||||
|
||||
public init(
|
||||
reason: String,
|
||||
restartexpectedms: Int?
|
||||
) {
|
||||
restartexpectedms: Int?)
|
||||
{
|
||||
self.reason = reason
|
||||
self.restartexpectedms = restartexpectedms
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case reason
|
||||
case restartexpectedms = "restartExpectedMs"
|
||||
@@ -454,17 +469,17 @@ public enum GatewayFrame: Codable {
|
||||
}
|
||||
switch type {
|
||||
case "hello":
|
||||
self = .hello(try Self.decodePayload(Hello.self, from: raw))
|
||||
self = try .hello(Self.decodePayload(Hello.self, from: raw))
|
||||
case "hello-ok":
|
||||
self = .helloOk(try Self.decodePayload(HelloOk.self, from: raw))
|
||||
self = try .helloOk(Self.decodePayload(HelloOk.self, from: raw))
|
||||
case "hello-error":
|
||||
self = .helloError(try Self.decodePayload(HelloError.self, from: raw))
|
||||
self = try .helloError(Self.decodePayload(HelloError.self, from: raw))
|
||||
case "req":
|
||||
self = .req(try Self.decodePayload(RequestFrame.self, from: raw))
|
||||
self = try .req(Self.decodePayload(RequestFrame.self, from: raw))
|
||||
case "res":
|
||||
self = .res(try Self.decodePayload(ResponseFrame.self, from: raw))
|
||||
self = try .res(Self.decodePayload(ResponseFrame.self, from: raw))
|
||||
case "event":
|
||||
self = .event(try Self.decodePayload(EventFrame.self, from: raw))
|
||||
self = try .event(Self.decodePayload(EventFrame.self, from: raw))
|
||||
default:
|
||||
self = .unknown(type: type, raw: raw)
|
||||
}
|
||||
@@ -472,23 +487,21 @@ public enum GatewayFrame: Codable {
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
switch self {
|
||||
case .hello(let v): try v.encode(to: encoder)
|
||||
case .helloOk(let v): try v.encode(to: encoder)
|
||||
case .helloError(let v): try v.encode(to: encoder)
|
||||
case .req(let v): try v.encode(to: encoder)
|
||||
case .res(let v): try v.encode(to: encoder)
|
||||
case .event(let v): try v.encode(to: encoder)
|
||||
case .unknown(_, let raw):
|
||||
case let .hello(v): try v.encode(to: encoder)
|
||||
case let .helloOk(v): try v.encode(to: encoder)
|
||||
case let .helloError(v): try v.encode(to: encoder)
|
||||
case let .req(v): try v.encode(to: encoder)
|
||||
case let .res(v): try v.encode(to: encoder)
|
||||
case let .event(v): try v.encode(to: encoder)
|
||||
case let .unknown(_, raw):
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(raw)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static func decodePayload<T: Decodable>(_ type: T.Type, from raw: [String: AnyCodable]) throws -> T {
|
||||
let data = try JSONSerialization.data(withJSONObject: raw)
|
||||
let decoder = JSONDecoder()
|
||||
return try decoder.decode(T.self, from: data)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -624,7 +624,7 @@ export async function getReplyFromConfig(
|
||||
systemLines.push(...queued);
|
||||
if (isNewSession) {
|
||||
const summary = await buildProviderSummary(cfg);
|
||||
if (summary) systemLines.unshift(summary);
|
||||
if (summary.length > 0) systemLines.unshift(...summary);
|
||||
}
|
||||
if (systemLines.length > 0) {
|
||||
const block = systemLines.map((l) => `System: ${l}`).join("\n");
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("node:child_process", async () => {
|
||||
const actual = await vi.importActual<typeof import("node:child_process")>(
|
||||
"node:child_process",
|
||||
);
|
||||
const actual =
|
||||
await vi.importActual<typeof import("node:child_process")>(
|
||||
"node:child_process",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
execFileSync: vi.fn(),
|
||||
@@ -14,8 +15,8 @@ import { execFileSync } from "node:child_process";
|
||||
import {
|
||||
forceFreePort,
|
||||
listPortListeners,
|
||||
parseLsofOutput,
|
||||
type PortProcess,
|
||||
parseLsofOutput,
|
||||
} from "./program.js";
|
||||
|
||||
describe("gateway --force helpers", () => {
|
||||
|
||||
@@ -116,10 +116,7 @@ export function buildProgram() {
|
||||
'clawdis send --to +15555550123 --message "Hi" --json',
|
||||
"Send via your web session and print JSON result.",
|
||||
],
|
||||
[
|
||||
"clawdis gateway --port 18789",
|
||||
"Run the WebSocket Gateway locally.",
|
||||
],
|
||||
["clawdis gateway --port 18789", "Run the WebSocket Gateway locally."],
|
||||
[
|
||||
"clawdis gateway --force",
|
||||
"Kill anything bound to the default gateway port, then start it.",
|
||||
|
||||
@@ -15,11 +15,11 @@ import {
|
||||
} from "../infra/system-presence.js";
|
||||
import { logError } from "../logger.js";
|
||||
import { getResolvedLoggerSettings } from "../logging.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { monitorWebProvider, webAuthExists } from "../providers/web/index.js";
|
||||
import { sendMessageWhatsApp } from "../web/outbound.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { monitorTelegramProvider } from "../telegram/monitor.js";
|
||||
import { sendMessageTelegram } from "../telegram/send.js";
|
||||
import { sendMessageWhatsApp } from "../web/outbound.js";
|
||||
import {
|
||||
ErrorCodes,
|
||||
type ErrorShape,
|
||||
@@ -450,32 +450,41 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||
const message = params.message.trim();
|
||||
const provider = (params.provider ?? "whatsapp").toLowerCase();
|
||||
try {
|
||||
const result =
|
||||
provider === "telegram"
|
||||
? await sendMessageTelegram(to, message, {
|
||||
mediaUrl: params.mediaUrl,
|
||||
verbose: isVerbose(),
|
||||
})
|
||||
: await sendMessageWhatsApp(to, message, {
|
||||
mediaUrl: params.mediaUrl,
|
||||
verbose: isVerbose(),
|
||||
});
|
||||
const payload =
|
||||
provider === "telegram"
|
||||
? {
|
||||
runId: idem,
|
||||
messageId: result.messageId,
|
||||
chatId: result.chatId,
|
||||
provider,
|
||||
}
|
||||
: {
|
||||
runId: idem,
|
||||
messageId: result.messageId,
|
||||
toJid: result.toJid ?? `${to}@s.whatsapp.net`,
|
||||
provider,
|
||||
};
|
||||
dedupe.set(`send:${idem}`, { ts: Date.now(), ok: true, payload });
|
||||
respond(true, payload, undefined);
|
||||
if (provider === "telegram") {
|
||||
const result = await sendMessageTelegram(to, message, {
|
||||
mediaUrl: params.mediaUrl,
|
||||
verbose: isVerbose(),
|
||||
});
|
||||
const payload = {
|
||||
runId: idem,
|
||||
messageId: result.messageId,
|
||||
chatId: result.chatId,
|
||||
provider,
|
||||
};
|
||||
dedupe.set(`send:${idem}`, {
|
||||
ts: Date.now(),
|
||||
ok: true,
|
||||
payload,
|
||||
});
|
||||
respond(true, payload, undefined);
|
||||
} else {
|
||||
const result = await sendMessageWhatsApp(to, message, {
|
||||
mediaUrl: params.mediaUrl,
|
||||
verbose: isVerbose(),
|
||||
});
|
||||
const payload = {
|
||||
runId: idem,
|
||||
messageId: result.messageId,
|
||||
toJid: result.toJid ?? `${to}@s.whatsapp.net`,
|
||||
provider,
|
||||
};
|
||||
dedupe.set(`send:${idem}`, {
|
||||
ts: Date.now(),
|
||||
ok: true,
|
||||
payload,
|
||||
});
|
||||
respond(true, payload, undefined);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = errorShape(ErrorCodes.UNAVAILABLE, String(err));
|
||||
dedupe.set(`send:${idem}`, { ts: Date.now(), ok: false, error });
|
||||
@@ -594,7 +603,9 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||
if (process.env.CLAWDIS_SKIP_PROVIDERS !== "1") {
|
||||
void startProviders();
|
||||
} else {
|
||||
defaultRuntime.log("gateway: skipping provider start (CLAWDIS_SKIP_PROVIDERS=1)");
|
||||
defaultRuntime.log(
|
||||
"gateway: skipping provider start (CLAWDIS_SKIP_PROVIDERS=1)",
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -21,7 +21,9 @@ export async function buildProviderSummary(
|
||||
const { e164 } = readWebSelfId();
|
||||
lines.push(
|
||||
webLinked
|
||||
? chalk.green(`WhatsApp: linked${e164 ? ` as ${e164}` : ""} (auth ${authAge})`)
|
||||
? chalk.green(
|
||||
`WhatsApp: linked${e164 ? ` as ${e164}` : ""} (auth ${authAge})`,
|
||||
)
|
||||
: chalk.red("WhatsApp: not linked"),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user