mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix: finish channels rename sweep
This commit is contained in:
@@ -187,7 +187,7 @@ actor BridgeServer {
|
|||||||
thinking: "low",
|
thinking: "low",
|
||||||
deliver: false,
|
deliver: false,
|
||||||
to: nil,
|
to: nil,
|
||||||
provider: .last))
|
channel: .last))
|
||||||
|
|
||||||
case "agent.request":
|
case "agent.request":
|
||||||
guard let json = evt.payloadJSON, let data = json.data(using: .utf8) else {
|
guard let json = evt.payloadJSON, let data = json.data(using: .utf8) else {
|
||||||
@@ -205,7 +205,7 @@ actor BridgeServer {
|
|||||||
?? "node-\(nodeId)"
|
?? "node-\(nodeId)"
|
||||||
let thinking = link.thinking?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty
|
let thinking = link.thinking?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty
|
||||||
let to = link.to?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty
|
let to = link.to?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty
|
||||||
let provider = GatewayAgentProvider(raw: link.channel)
|
let channel = GatewayAgentChannel(raw: link.channel)
|
||||||
|
|
||||||
_ = await GatewayConnection.shared.sendAgent(GatewayAgentInvocation(
|
_ = await GatewayConnection.shared.sendAgent(GatewayAgentInvocation(
|
||||||
message: message,
|
message: message,
|
||||||
@@ -213,7 +213,7 @@ actor BridgeServer {
|
|||||||
thinking: thinking,
|
thinking: thinking,
|
||||||
deliver: link.deliver,
|
deliver: link.deliver,
|
||||||
to: to,
|
to: to,
|
||||||
provider: provider))
|
channel: channel))
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ extension ConnectionsSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func providerHeaderActions(_ provider: ConnectionProvider) -> some View {
|
func channelHeaderActions(_ channel: ConnectionChannel) -> some View {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
if provider == .whatsapp {
|
if channel == .whatsapp {
|
||||||
Button("Logout") {
|
Button("Logout") {
|
||||||
Task { await self.store.logoutWhatsApp() }
|
Task { await self.store.logoutWhatsApp() }
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ extension ConnectionsSettings {
|
|||||||
.disabled(self.store.whatsappBusy)
|
.disabled(self.store.whatsappBusy)
|
||||||
}
|
}
|
||||||
|
|
||||||
if provider == .telegram {
|
if channel == .telegram {
|
||||||
Button("Logout") {
|
Button("Logout") {
|
||||||
Task { await self.store.logoutTelegram() }
|
Task { await self.store.logoutTelegram() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
extension ConnectionsSettings {
|
extension ConnectionsSettings {
|
||||||
private func providerStatus<T: Decodable>(
|
private func channelStatus<T: Decodable>(
|
||||||
_ id: String,
|
_ id: String,
|
||||||
as type: T.Type) -> T?
|
as type: T.Type) -> T?
|
||||||
{
|
{
|
||||||
self.store.snapshot?.decodeProvider(id, as: type)
|
self.store.snapshot?.decodeChannel(id, as: type)
|
||||||
}
|
}
|
||||||
|
|
||||||
var whatsAppTint: Color {
|
var whatsAppTint: Color {
|
||||||
guard let status = self.providerStatus("whatsapp", as: ProvidersStatusSnapshot.WhatsAppStatus.self)
|
guard let status = self.channelStatus("whatsapp", as: ChannelsStatusSnapshot.WhatsAppStatus.self)
|
||||||
else { return .secondary }
|
else { return .secondary }
|
||||||
if !status.configured { return .secondary }
|
if !status.configured { return .secondary }
|
||||||
if !status.linked { return .red }
|
if !status.linked { return .red }
|
||||||
@@ -20,7 +20,7 @@ extension ConnectionsSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var telegramTint: Color {
|
var telegramTint: Color {
|
||||||
guard let status = self.providerStatus("telegram", as: ProvidersStatusSnapshot.TelegramStatus.self)
|
guard let status = self.channelStatus("telegram", as: ChannelsStatusSnapshot.TelegramStatus.self)
|
||||||
else { return .secondary }
|
else { return .secondary }
|
||||||
if !status.configured { return .secondary }
|
if !status.configured { return .secondary }
|
||||||
if status.lastError != nil { return .orange }
|
if status.lastError != nil { return .orange }
|
||||||
@@ -30,7 +30,7 @@ extension ConnectionsSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var discordTint: Color {
|
var discordTint: Color {
|
||||||
guard let status = self.providerStatus("discord", as: ProvidersStatusSnapshot.DiscordStatus.self)
|
guard let status = self.channelStatus("discord", as: ChannelsStatusSnapshot.DiscordStatus.self)
|
||||||
else { return .secondary }
|
else { return .secondary }
|
||||||
if !status.configured { return .secondary }
|
if !status.configured { return .secondary }
|
||||||
if status.lastError != nil { return .orange }
|
if status.lastError != nil { return .orange }
|
||||||
@@ -40,7 +40,7 @@ extension ConnectionsSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var signalTint: Color {
|
var signalTint: Color {
|
||||||
guard let status = self.providerStatus("signal", as: ProvidersStatusSnapshot.SignalStatus.self)
|
guard let status = self.channelStatus("signal", as: ChannelsStatusSnapshot.SignalStatus.self)
|
||||||
else { return .secondary }
|
else { return .secondary }
|
||||||
if !status.configured { return .secondary }
|
if !status.configured { return .secondary }
|
||||||
if status.lastError != nil { return .orange }
|
if status.lastError != nil { return .orange }
|
||||||
@@ -50,7 +50,7 @@ extension ConnectionsSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var imessageTint: Color {
|
var imessageTint: Color {
|
||||||
guard let status = self.providerStatus("imessage", as: ProvidersStatusSnapshot.IMessageStatus.self)
|
guard let status = self.channelStatus("imessage", as: ChannelsStatusSnapshot.IMessageStatus.self)
|
||||||
else { return .secondary }
|
else { return .secondary }
|
||||||
if !status.configured { return .secondary }
|
if !status.configured { return .secondary }
|
||||||
if status.lastError != nil { return .orange }
|
if status.lastError != nil { return .orange }
|
||||||
@@ -60,7 +60,7 @@ extension ConnectionsSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var whatsAppSummary: String {
|
var whatsAppSummary: String {
|
||||||
guard let status = self.providerStatus("whatsapp", as: ProvidersStatusSnapshot.WhatsAppStatus.self)
|
guard let status = self.channelStatus("whatsapp", as: ChannelsStatusSnapshot.WhatsAppStatus.self)
|
||||||
else { return "Checking…" }
|
else { return "Checking…" }
|
||||||
if !status.linked { return "Not linked" }
|
if !status.linked { return "Not linked" }
|
||||||
if status.connected { return "Connected" }
|
if status.connected { return "Connected" }
|
||||||
@@ -69,7 +69,7 @@ extension ConnectionsSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var telegramSummary: String {
|
var telegramSummary: String {
|
||||||
guard let status = self.providerStatus("telegram", as: ProvidersStatusSnapshot.TelegramStatus.self)
|
guard let status = self.channelStatus("telegram", as: ChannelsStatusSnapshot.TelegramStatus.self)
|
||||||
else { return "Checking…" }
|
else { return "Checking…" }
|
||||||
if !status.configured { return "Not configured" }
|
if !status.configured { return "Not configured" }
|
||||||
if status.running { return "Running" }
|
if status.running { return "Running" }
|
||||||
@@ -77,7 +77,7 @@ extension ConnectionsSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var discordSummary: String {
|
var discordSummary: String {
|
||||||
guard let status = self.providerStatus("discord", as: ProvidersStatusSnapshot.DiscordStatus.self)
|
guard let status = self.channelStatus("discord", as: ChannelsStatusSnapshot.DiscordStatus.self)
|
||||||
else { return "Checking…" }
|
else { return "Checking…" }
|
||||||
if !status.configured { return "Not configured" }
|
if !status.configured { return "Not configured" }
|
||||||
if status.running { return "Running" }
|
if status.running { return "Running" }
|
||||||
@@ -85,7 +85,7 @@ extension ConnectionsSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var signalSummary: String {
|
var signalSummary: String {
|
||||||
guard let status = self.providerStatus("signal", as: ProvidersStatusSnapshot.SignalStatus.self)
|
guard let status = self.channelStatus("signal", as: ChannelsStatusSnapshot.SignalStatus.self)
|
||||||
else { return "Checking…" }
|
else { return "Checking…" }
|
||||||
if !status.configured { return "Not configured" }
|
if !status.configured { return "Not configured" }
|
||||||
if status.running { return "Running" }
|
if status.running { return "Running" }
|
||||||
@@ -93,7 +93,7 @@ extension ConnectionsSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var imessageSummary: String {
|
var imessageSummary: String {
|
||||||
guard let status = self.providerStatus("imessage", as: ProvidersStatusSnapshot.IMessageStatus.self)
|
guard let status = self.channelStatus("imessage", as: ChannelsStatusSnapshot.IMessageStatus.self)
|
||||||
else { return "Checking…" }
|
else { return "Checking…" }
|
||||||
if !status.configured { return "Not configured" }
|
if !status.configured { return "Not configured" }
|
||||||
if status.running { return "Running" }
|
if status.running { return "Running" }
|
||||||
@@ -101,7 +101,7 @@ extension ConnectionsSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var whatsAppDetails: String? {
|
var whatsAppDetails: String? {
|
||||||
guard let status = self.providerStatus("whatsapp", as: ProvidersStatusSnapshot.WhatsAppStatus.self)
|
guard let status = self.channelStatus("whatsapp", as: ChannelsStatusSnapshot.WhatsAppStatus.self)
|
||||||
else { return nil }
|
else { return nil }
|
||||||
var lines: [String] = []
|
var lines: [String] = []
|
||||||
if let e164 = status.`self`?.e164 ?? status.`self`?.jid {
|
if let e164 = status.`self`?.e164 ?? status.`self`?.jid {
|
||||||
@@ -132,7 +132,7 @@ extension ConnectionsSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var telegramDetails: String? {
|
var telegramDetails: String? {
|
||||||
guard let status = self.providerStatus("telegram", as: ProvidersStatusSnapshot.TelegramStatus.self)
|
guard let status = self.channelStatus("telegram", as: ChannelsStatusSnapshot.TelegramStatus.self)
|
||||||
else { return nil }
|
else { return nil }
|
||||||
var lines: [String] = []
|
var lines: [String] = []
|
||||||
if let source = status.tokenSource {
|
if let source = status.tokenSource {
|
||||||
@@ -164,7 +164,7 @@ extension ConnectionsSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var discordDetails: String? {
|
var discordDetails: String? {
|
||||||
guard let status = self.providerStatus("discord", as: ProvidersStatusSnapshot.DiscordStatus.self)
|
guard let status = self.channelStatus("discord", as: ChannelsStatusSnapshot.DiscordStatus.self)
|
||||||
else { return nil }
|
else { return nil }
|
||||||
var lines: [String] = []
|
var lines: [String] = []
|
||||||
if let source = status.tokenSource {
|
if let source = status.tokenSource {
|
||||||
@@ -193,7 +193,7 @@ extension ConnectionsSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var signalDetails: String? {
|
var signalDetails: String? {
|
||||||
guard let status = self.providerStatus("signal", as: ProvidersStatusSnapshot.SignalStatus.self)
|
guard let status = self.channelStatus("signal", as: ChannelsStatusSnapshot.SignalStatus.self)
|
||||||
else { return nil }
|
else { return nil }
|
||||||
var lines: [String] = []
|
var lines: [String] = []
|
||||||
lines.append("Base URL: \(status.baseUrl)")
|
lines.append("Base URL: \(status.baseUrl)")
|
||||||
@@ -220,7 +220,7 @@ extension ConnectionsSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var imessageDetails: String? {
|
var imessageDetails: String? {
|
||||||
guard let status = self.providerStatus("imessage", as: ProvidersStatusSnapshot.IMessageStatus.self)
|
guard let status = self.channelStatus("imessage", as: ChannelsStatusSnapshot.IMessageStatus.self)
|
||||||
else { return nil }
|
else { return nil }
|
||||||
var lines: [String] = []
|
var lines: [String] = []
|
||||||
if let cliPath = status.cliPath, !cliPath.isEmpty {
|
if let cliPath = status.cliPath, !cliPath.isEmpty {
|
||||||
@@ -243,68 +243,68 @@ extension ConnectionsSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var isTelegramTokenLocked: Bool {
|
var isTelegramTokenLocked: Bool {
|
||||||
self.providerStatus("telegram", as: ProvidersStatusSnapshot.TelegramStatus.self)?.tokenSource == "env"
|
self.channelStatus("telegram", as: ChannelsStatusSnapshot.TelegramStatus.self)?.tokenSource == "env"
|
||||||
}
|
}
|
||||||
|
|
||||||
var isDiscordTokenLocked: Bool {
|
var isDiscordTokenLocked: Bool {
|
||||||
self.providerStatus("discord", as: ProvidersStatusSnapshot.DiscordStatus.self)?.tokenSource == "env"
|
self.channelStatus("discord", as: ChannelsStatusSnapshot.DiscordStatus.self)?.tokenSource == "env"
|
||||||
}
|
}
|
||||||
|
|
||||||
var orderedProviders: [ConnectionProvider] {
|
var orderedChannels: [ConnectionChannel] {
|
||||||
ConnectionProvider.allCases.sorted { lhs, rhs in
|
ConnectionChannel.allCases.sorted { lhs, rhs in
|
||||||
let lhsEnabled = self.providerEnabled(lhs)
|
let lhsEnabled = self.channelEnabled(lhs)
|
||||||
let rhsEnabled = self.providerEnabled(rhs)
|
let rhsEnabled = self.channelEnabled(rhs)
|
||||||
if lhsEnabled != rhsEnabled { return lhsEnabled && !rhsEnabled }
|
if lhsEnabled != rhsEnabled { return lhsEnabled && !rhsEnabled }
|
||||||
return lhs.sortOrder < rhs.sortOrder
|
return lhs.sortOrder < rhs.sortOrder
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var enabledProviders: [ConnectionProvider] {
|
var enabledChannels: [ConnectionChannel] {
|
||||||
self.orderedProviders.filter { self.providerEnabled($0) }
|
self.orderedChannels.filter { self.channelEnabled($0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
var availableProviders: [ConnectionProvider] {
|
var availableChannels: [ConnectionChannel] {
|
||||||
self.orderedProviders.filter { !self.providerEnabled($0) }
|
self.orderedChannels.filter { !self.channelEnabled($0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureSelection() {
|
func ensureSelection() {
|
||||||
guard let selected = self.selectedProvider else {
|
guard let selected = self.selectedChannel else {
|
||||||
self.selectedProvider = self.orderedProviders.first
|
self.selectedChannel = self.orderedChannels.first
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !self.orderedProviders.contains(selected) {
|
if !self.orderedChannels.contains(selected) {
|
||||||
self.selectedProvider = self.orderedProviders.first
|
self.selectedChannel = self.orderedChannels.first
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func providerEnabled(_ provider: ConnectionProvider) -> Bool {
|
func channelEnabled(_ channel: ConnectionChannel) -> Bool {
|
||||||
switch provider {
|
switch channel {
|
||||||
case .whatsapp:
|
case .whatsapp:
|
||||||
guard let status = self.providerStatus("whatsapp", as: ProvidersStatusSnapshot.WhatsAppStatus.self)
|
guard let status = self.channelStatus("whatsapp", as: ChannelsStatusSnapshot.WhatsAppStatus.self)
|
||||||
else { return false }
|
else { return false }
|
||||||
return status.configured || status.linked || status.running
|
return status.configured || status.linked || status.running
|
||||||
case .telegram:
|
case .telegram:
|
||||||
guard let status = self.providerStatus("telegram", as: ProvidersStatusSnapshot.TelegramStatus.self)
|
guard let status = self.channelStatus("telegram", as: ChannelsStatusSnapshot.TelegramStatus.self)
|
||||||
else { return false }
|
else { return false }
|
||||||
return status.configured || status.running
|
return status.configured || status.running
|
||||||
case .discord:
|
case .discord:
|
||||||
guard let status = self.providerStatus("discord", as: ProvidersStatusSnapshot.DiscordStatus.self)
|
guard let status = self.channelStatus("discord", as: ChannelsStatusSnapshot.DiscordStatus.self)
|
||||||
else { return false }
|
else { return false }
|
||||||
return status.configured || status.running
|
return status.configured || status.running
|
||||||
case .signal:
|
case .signal:
|
||||||
guard let status = self.providerStatus("signal", as: ProvidersStatusSnapshot.SignalStatus.self)
|
guard let status = self.channelStatus("signal", as: ChannelsStatusSnapshot.SignalStatus.self)
|
||||||
else { return false }
|
else { return false }
|
||||||
return status.configured || status.running
|
return status.configured || status.running
|
||||||
case .imessage:
|
case .imessage:
|
||||||
guard let status = self.providerStatus("imessage", as: ProvidersStatusSnapshot.IMessageStatus.self)
|
guard let status = self.channelStatus("imessage", as: ChannelsStatusSnapshot.IMessageStatus.self)
|
||||||
else { return false }
|
else { return false }
|
||||||
return status.configured || status.running
|
return status.configured || status.running
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func providerSection(_ provider: ConnectionProvider) -> some View {
|
func channelSection(_ channel: ConnectionChannel) -> some View {
|
||||||
switch provider {
|
switch channel {
|
||||||
case .whatsapp:
|
case .whatsapp:
|
||||||
self.whatsAppSection
|
self.whatsAppSection
|
||||||
case .telegram:
|
case .telegram:
|
||||||
@@ -318,8 +318,8 @@ extension ConnectionsSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func providerTint(_ provider: ConnectionProvider) -> Color {
|
func channelTint(_ channel: ConnectionChannel) -> Color {
|
||||||
switch provider {
|
switch channel {
|
||||||
case .whatsapp:
|
case .whatsapp:
|
||||||
self.whatsAppTint
|
self.whatsAppTint
|
||||||
case .telegram:
|
case .telegram:
|
||||||
@@ -333,8 +333,8 @@ extension ConnectionsSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func providerSummary(_ provider: ConnectionProvider) -> String {
|
func channelSummary(_ channel: ConnectionChannel) -> String {
|
||||||
switch provider {
|
switch channel {
|
||||||
case .whatsapp:
|
case .whatsapp:
|
||||||
self.whatsAppSummary
|
self.whatsAppSummary
|
||||||
case .telegram:
|
case .telegram:
|
||||||
@@ -348,8 +348,8 @@ extension ConnectionsSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func providerDetails(_ provider: ConnectionProvider) -> String? {
|
func channelDetails(_ channel: ConnectionChannel) -> String? {
|
||||||
switch provider {
|
switch channel {
|
||||||
case .whatsapp:
|
case .whatsapp:
|
||||||
self.whatsAppDetails
|
self.whatsAppDetails
|
||||||
case .telegram:
|
case .telegram:
|
||||||
@@ -363,55 +363,55 @@ extension ConnectionsSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func providerLastCheckText(_ provider: ConnectionProvider) -> String {
|
func channelLastCheckText(_ channel: ConnectionChannel) -> String {
|
||||||
guard let date = self.providerLastCheck(provider) else { return "never" }
|
guard let date = self.channelLastCheck(channel) else { return "never" }
|
||||||
return relativeAge(from: date)
|
return relativeAge(from: date)
|
||||||
}
|
}
|
||||||
|
|
||||||
func providerLastCheck(_ provider: ConnectionProvider) -> Date? {
|
func channelLastCheck(_ channel: ConnectionChannel) -> Date? {
|
||||||
switch provider {
|
switch channel {
|
||||||
case .whatsapp:
|
case .whatsapp:
|
||||||
guard let status = self.providerStatus("whatsapp", as: ProvidersStatusSnapshot.WhatsAppStatus.self)
|
guard let status = self.channelStatus("whatsapp", as: ChannelsStatusSnapshot.WhatsAppStatus.self)
|
||||||
else { return nil }
|
else { return nil }
|
||||||
return self.date(fromMs: status.lastEventAt ?? status.lastMessageAt ?? status.lastConnectedAt)
|
return self.date(fromMs: status.lastEventAt ?? status.lastMessageAt ?? status.lastConnectedAt)
|
||||||
case .telegram:
|
case .telegram:
|
||||||
return self
|
return self
|
||||||
.date(fromMs: self.providerStatus("telegram", as: ProvidersStatusSnapshot.TelegramStatus.self)?
|
.date(fromMs: self.channelStatus("telegram", as: ChannelsStatusSnapshot.TelegramStatus.self)?
|
||||||
.lastProbeAt)
|
.lastProbeAt)
|
||||||
case .discord:
|
case .discord:
|
||||||
return self
|
return self
|
||||||
.date(fromMs: self.providerStatus("discord", as: ProvidersStatusSnapshot.DiscordStatus.self)?
|
.date(fromMs: self.channelStatus("discord", as: ChannelsStatusSnapshot.DiscordStatus.self)?
|
||||||
.lastProbeAt)
|
.lastProbeAt)
|
||||||
case .signal:
|
case .signal:
|
||||||
return self
|
return self
|
||||||
.date(fromMs: self.providerStatus("signal", as: ProvidersStatusSnapshot.SignalStatus.self)?.lastProbeAt)
|
.date(fromMs: self.channelStatus("signal", as: ChannelsStatusSnapshot.SignalStatus.self)?.lastProbeAt)
|
||||||
case .imessage:
|
case .imessage:
|
||||||
return self
|
return self
|
||||||
.date(fromMs: self.providerStatus("imessage", as: ProvidersStatusSnapshot.IMessageStatus.self)?
|
.date(fromMs: self.channelStatus("imessage", as: ChannelsStatusSnapshot.IMessageStatus.self)?
|
||||||
.lastProbeAt)
|
.lastProbeAt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func providerHasError(_ provider: ConnectionProvider) -> Bool {
|
func channelHasError(_ channel: ConnectionChannel) -> Bool {
|
||||||
switch provider {
|
switch channel {
|
||||||
case .whatsapp:
|
case .whatsapp:
|
||||||
guard let status = self.providerStatus("whatsapp", as: ProvidersStatusSnapshot.WhatsAppStatus.self)
|
guard let status = self.channelStatus("whatsapp", as: ChannelsStatusSnapshot.WhatsAppStatus.self)
|
||||||
else { return false }
|
else { return false }
|
||||||
return status.lastError?.isEmpty == false || status.lastDisconnect?.loggedOut == true
|
return status.lastError?.isEmpty == false || status.lastDisconnect?.loggedOut == true
|
||||||
case .telegram:
|
case .telegram:
|
||||||
guard let status = self.providerStatus("telegram", as: ProvidersStatusSnapshot.TelegramStatus.self)
|
guard let status = self.channelStatus("telegram", as: ChannelsStatusSnapshot.TelegramStatus.self)
|
||||||
else { return false }
|
else { return false }
|
||||||
return status.lastError?.isEmpty == false || status.probe?.ok == false
|
return status.lastError?.isEmpty == false || status.probe?.ok == false
|
||||||
case .discord:
|
case .discord:
|
||||||
guard let status = self.providerStatus("discord", as: ProvidersStatusSnapshot.DiscordStatus.self)
|
guard let status = self.channelStatus("discord", as: ChannelsStatusSnapshot.DiscordStatus.self)
|
||||||
else { return false }
|
else { return false }
|
||||||
return status.lastError?.isEmpty == false || status.probe?.ok == false
|
return status.lastError?.isEmpty == false || status.probe?.ok == false
|
||||||
case .signal:
|
case .signal:
|
||||||
guard let status = self.providerStatus("signal", as: ProvidersStatusSnapshot.SignalStatus.self)
|
guard let status = self.channelStatus("signal", as: ChannelsStatusSnapshot.SignalStatus.self)
|
||||||
else { return false }
|
else { return false }
|
||||||
return status.lastError?.isEmpty == false || status.probe?.ok == false
|
return status.lastError?.isEmpty == false || status.probe?.ok == false
|
||||||
case .imessage:
|
case .imessage:
|
||||||
guard let status = self.providerStatus("imessage", as: ProvidersStatusSnapshot.IMessageStatus.self)
|
guard let status = self.channelStatus("imessage", as: ChannelsStatusSnapshot.IMessageStatus.self)
|
||||||
else { return false }
|
else { return false }
|
||||||
return status.lastError?.isEmpty == false || status.probe?.ok == false
|
return status.lastError?.isEmpty == false || status.probe?.ok == false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ extension ConnectionsSettings {
|
|||||||
self.store.start()
|
self.store.start()
|
||||||
self.ensureSelection()
|
self.ensureSelection()
|
||||||
}
|
}
|
||||||
.onChange(of: self.orderedProviders) { _, _ in
|
.onChange(of: self.orderedChannels) { _, _ in
|
||||||
self.ensureSelection()
|
self.ensureSelection()
|
||||||
}
|
}
|
||||||
.onDisappear { self.store.stop() }
|
.onDisappear { self.store.stop() }
|
||||||
@@ -20,17 +20,17 @@ extension ConnectionsSettings {
|
|||||||
private var sidebar: some View {
|
private var sidebar: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack(alignment: .leading, spacing: 8) {
|
LazyVStack(alignment: .leading, spacing: 8) {
|
||||||
if !self.enabledProviders.isEmpty {
|
if !self.enabledChannels.isEmpty {
|
||||||
self.sidebarSectionHeader("Configured")
|
self.sidebarSectionHeader("Configured")
|
||||||
ForEach(self.enabledProviders) { provider in
|
ForEach(self.enabledChannels) { channel in
|
||||||
self.sidebarRow(provider)
|
self.sidebarRow(channel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.availableProviders.isEmpty {
|
if !self.availableChannels.isEmpty {
|
||||||
self.sidebarSectionHeader("Available")
|
self.sidebarSectionHeader("Available")
|
||||||
ForEach(self.availableProviders) { provider in
|
ForEach(self.availableChannels) { channel in
|
||||||
self.sidebarRow(provider)
|
self.sidebarRow(channel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,8 +46,8 @@ extension ConnectionsSettings {
|
|||||||
|
|
||||||
private var detail: some View {
|
private var detail: some View {
|
||||||
Group {
|
Group {
|
||||||
if let provider = self.selectedProvider {
|
if let channel = self.selectedChannel {
|
||||||
self.providerDetail(provider)
|
self.channelDetail(channel)
|
||||||
} else {
|
} else {
|
||||||
self.emptyDetail
|
self.emptyDetail
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,7 @@ extension ConnectionsSettings {
|
|||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text("Connections")
|
Text("Connections")
|
||||||
.font(.title3.weight(.semibold))
|
.font(.title3.weight(.semibold))
|
||||||
Text("Select a provider to view status and settings.")
|
Text("Select a channel to view status and settings.")
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
@@ -67,12 +67,12 @@ extension ConnectionsSettings {
|
|||||||
.padding(.vertical, 18)
|
.padding(.vertical, 18)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func providerDetail(_ provider: ConnectionProvider) -> some View {
|
private func channelDetail(_ channel: ConnectionChannel) -> some View {
|
||||||
ScrollView(.vertical) {
|
ScrollView(.vertical) {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
self.detailHeader(for: provider)
|
self.detailHeader(for: channel)
|
||||||
Divider()
|
Divider()
|
||||||
self.providerSection(provider)
|
self.channelSection(channel)
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
@@ -81,18 +81,18 @@ extension ConnectionsSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sidebarRow(_ provider: ConnectionProvider) -> some View {
|
private func sidebarRow(_ channel: ConnectionChannel) -> some View {
|
||||||
let isSelected = self.selectedProvider == provider
|
let isSelected = self.selectedChannel == channel
|
||||||
return Button {
|
return Button {
|
||||||
self.selectedProvider = provider
|
self.selectedChannel = channel
|
||||||
} label: {
|
} label: {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Circle()
|
Circle()
|
||||||
.fill(self.providerTint(provider))
|
.fill(self.channelTint(channel))
|
||||||
.frame(width: 8, height: 8)
|
.frame(width: 8, height: 8)
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(provider.title)
|
Text(channel.title)
|
||||||
Text(self.providerSummary(provider))
|
Text(self.channelSummary(channel))
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
@@ -119,23 +119,23 @@ extension ConnectionsSettings {
|
|||||||
.padding(.top, 2)
|
.padding(.top, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func detailHeader(for provider: ConnectionProvider) -> some View {
|
private func detailHeader(for channel: ConnectionChannel) -> some View {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
HStack(alignment: .firstTextBaseline, spacing: 10) {
|
HStack(alignment: .firstTextBaseline, spacing: 10) {
|
||||||
Label(provider.detailTitle, systemImage: provider.systemImage)
|
Label(channel.detailTitle, systemImage: channel.systemImage)
|
||||||
.font(.title3.weight(.semibold))
|
.font(.title3.weight(.semibold))
|
||||||
self.statusBadge(
|
self.statusBadge(
|
||||||
self.providerSummary(provider),
|
self.channelSummary(channel),
|
||||||
color: self.providerTint(provider))
|
color: self.channelTint(channel))
|
||||||
Spacer()
|
Spacer()
|
||||||
self.providerHeaderActions(provider)
|
self.channelHeaderActions(channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
Text("Last check \(self.providerLastCheckText(provider))")
|
Text("Last check \(self.channelLastCheckText(channel))")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
if self.providerHasError(provider) {
|
if self.channelHasError(channel) {
|
||||||
Text("Error")
|
Text("Error")
|
||||||
.font(.caption2.weight(.semibold))
|
.font(.caption2.weight(.semibold))
|
||||||
.padding(.horizontal, 6)
|
.padding(.horizontal, 6)
|
||||||
@@ -146,7 +146,7 @@ extension ConnectionsSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let details = self.providerDetails(provider) {
|
if let details = self.channelDetails(channel) {
|
||||||
Text(details)
|
Text(details)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import AppKit
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ConnectionsSettings: View {
|
struct ConnectionsSettings: View {
|
||||||
enum ConnectionProvider: String, CaseIterable, Identifiable, Hashable {
|
enum ConnectionChannel: String, CaseIterable, Identifiable, Hashable {
|
||||||
case whatsapp
|
case whatsapp
|
||||||
case telegram
|
case telegram
|
||||||
case discord
|
case discord
|
||||||
@@ -53,7 +53,7 @@ struct ConnectionsSettings: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bindable var store: ConnectionsStore
|
@Bindable var store: ConnectionsStore
|
||||||
@State var selectedProvider: ConnectionProvider?
|
@State var selectedChannel: ConnectionChannel?
|
||||||
@State var showTelegramToken = false
|
@State var showTelegramToken = false
|
||||||
@State var showDiscordToken = false
|
@State var showDiscordToken = false
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ extension ConnectionsStore {
|
|||||||
"probe": AnyCodable(probe),
|
"probe": AnyCodable(probe),
|
||||||
"timeoutMs": AnyCodable(8000),
|
"timeoutMs": AnyCodable(8000),
|
||||||
]
|
]
|
||||||
let snap: ProvidersStatusSnapshot = try await GatewayConnection.shared.requestDecoded(
|
let snap: ChannelsStatusSnapshot = try await GatewayConnection.shared.requestDecoded(
|
||||||
method: .providersStatus,
|
method: .channelsStatus,
|
||||||
params: params,
|
params: params,
|
||||||
timeoutMs: 12000)
|
timeoutMs: 12000)
|
||||||
self.snapshot = snap
|
self.snapshot = snap
|
||||||
@@ -101,10 +101,10 @@ extension ConnectionsStore {
|
|||||||
defer { self.whatsappBusy = false }
|
defer { self.whatsappBusy = false }
|
||||||
do {
|
do {
|
||||||
let params: [String: AnyCodable] = [
|
let params: [String: AnyCodable] = [
|
||||||
"provider": AnyCodable("whatsapp"),
|
"channel": AnyCodable("whatsapp"),
|
||||||
]
|
]
|
||||||
let result: ProviderLogoutResult = try await GatewayConnection.shared.requestDecoded(
|
let result: ChannelLogoutResult = try await GatewayConnection.shared.requestDecoded(
|
||||||
method: .providersLogout,
|
method: .channelsLogout,
|
||||||
params: params,
|
params: params,
|
||||||
timeoutMs: 15000)
|
timeoutMs: 15000)
|
||||||
self.whatsappLoginMessage = result.cleared
|
self.whatsappLoginMessage = result.cleared
|
||||||
@@ -123,10 +123,10 @@ extension ConnectionsStore {
|
|||||||
defer { self.telegramBusy = false }
|
defer { self.telegramBusy = false }
|
||||||
do {
|
do {
|
||||||
let params: [String: AnyCodable] = [
|
let params: [String: AnyCodable] = [
|
||||||
"provider": AnyCodable("telegram"),
|
"channel": AnyCodable("telegram"),
|
||||||
]
|
]
|
||||||
let result: ProviderLogoutResult = try await GatewayConnection.shared.requestDecoded(
|
let result: ChannelLogoutResult = try await GatewayConnection.shared.requestDecoded(
|
||||||
method: .providersLogout,
|
method: .channelsLogout,
|
||||||
params: params,
|
params: params,
|
||||||
timeoutMs: 15000)
|
timeoutMs: 15000)
|
||||||
if result.envToken == true {
|
if result.envToken == true {
|
||||||
@@ -154,8 +154,8 @@ private struct WhatsAppLoginWaitResult: Codable {
|
|||||||
let message: String
|
let message: String
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct ProviderLogoutResult: Codable {
|
private struct ChannelLogoutResult: Codable {
|
||||||
let provider: String?
|
let channel: String?
|
||||||
let accountId: String?
|
let accountId: String?
|
||||||
let cleared: Bool
|
let cleared: Bool
|
||||||
let envToken: Bool?
|
let envToken: Bool?
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import ClawdbotProtocol
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Observation
|
import Observation
|
||||||
|
|
||||||
struct ProvidersStatusSnapshot: Codable {
|
struct ChannelsStatusSnapshot: Codable {
|
||||||
struct WhatsAppSelf: Codable {
|
struct WhatsAppSelf: Codable {
|
||||||
let e164: String?
|
let e164: String?
|
||||||
let jid: String?
|
let jid: String?
|
||||||
@@ -121,7 +121,7 @@ struct ProvidersStatusSnapshot: Codable {
|
|||||||
let lastProbeAt: Double?
|
let lastProbeAt: Double?
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ProviderAccountSnapshot: Codable {
|
struct ChannelAccountSnapshot: Codable {
|
||||||
let accountId: String
|
let accountId: String
|
||||||
let name: String?
|
let name: String?
|
||||||
let enabled: Bool?
|
let enabled: Bool?
|
||||||
@@ -154,14 +154,14 @@ struct ProvidersStatusSnapshot: Codable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let ts: Double
|
let ts: Double
|
||||||
let providerOrder: [String]
|
let channelOrder: [String]
|
||||||
let providerLabels: [String: String]
|
let channelLabels: [String: String]
|
||||||
let providers: [String: AnyCodable]
|
let channels: [String: AnyCodable]
|
||||||
let providerAccounts: [String: [ProviderAccountSnapshot]]
|
let channelAccounts: [String: [ChannelAccountSnapshot]]
|
||||||
let providerDefaultAccountId: [String: String]
|
let channelDefaultAccountId: [String: String]
|
||||||
|
|
||||||
func decodeProvider<T: Decodable>(_ id: String, as type: T.Type) -> T? {
|
func decodeChannel<T: Decodable>(_ id: String, as type: T.Type) -> T? {
|
||||||
guard let value = self.providers[id] else { return nil }
|
guard let value = self.channels[id] else { return nil }
|
||||||
do {
|
do {
|
||||||
let data = try JSONEncoder().encode(value)
|
let data = try JSONEncoder().encode(value)
|
||||||
return try JSONDecoder().decode(type, from: data)
|
return try JSONDecoder().decode(type, from: data)
|
||||||
@@ -230,7 +230,7 @@ struct DiscordGuildForm: Identifiable {
|
|||||||
final class ConnectionsStore {
|
final class ConnectionsStore {
|
||||||
static let shared = ConnectionsStore()
|
static let shared = ConnectionsStore()
|
||||||
|
|
||||||
var snapshot: ProvidersStatusSnapshot?
|
var snapshot: ChannelsStatusSnapshot?
|
||||||
var lastError: String?
|
var lastError: String?
|
||||||
var lastSuccess: Date?
|
var lastSuccess: Date?
|
||||||
var isRefreshing = false
|
var isRefreshing = false
|
||||||
|
|||||||
@@ -36,13 +36,13 @@ extension CronJobEditor {
|
|||||||
case let .systemEvent(text):
|
case let .systemEvent(text):
|
||||||
self.payloadKind = .systemEvent
|
self.payloadKind = .systemEvent
|
||||||
self.systemEventText = text
|
self.systemEventText = text
|
||||||
case let .agentTurn(message, thinking, timeoutSeconds, deliver, provider, to, bestEffortDeliver):
|
case let .agentTurn(message, thinking, timeoutSeconds, deliver, channel, to, bestEffortDeliver):
|
||||||
self.payloadKind = .agentTurn
|
self.payloadKind = .agentTurn
|
||||||
self.agentMessage = message
|
self.agentMessage = message
|
||||||
self.thinking = thinking ?? ""
|
self.thinking = thinking ?? ""
|
||||||
self.timeoutSeconds = timeoutSeconds.map(String.init) ?? ""
|
self.timeoutSeconds = timeoutSeconds.map(String.init) ?? ""
|
||||||
self.deliver = deliver ?? false
|
self.deliver = deliver ?? false
|
||||||
self.provider = GatewayAgentProvider(raw: provider)
|
self.channel = GatewayAgentChannel(raw: channel)
|
||||||
self.to = to ?? ""
|
self.to = to ?? ""
|
||||||
self.bestEffortDeliver = bestEffortDeliver ?? false
|
self.bestEffortDeliver = bestEffortDeliver ?? false
|
||||||
}
|
}
|
||||||
@@ -204,7 +204,7 @@ extension CronJobEditor {
|
|||||||
if let n = Int(self.timeoutSeconds), n > 0 { payload["timeoutSeconds"] = n }
|
if let n = Int(self.timeoutSeconds), n > 0 { payload["timeoutSeconds"] = n }
|
||||||
payload["deliver"] = self.deliver
|
payload["deliver"] = self.deliver
|
||||||
if self.deliver {
|
if self.deliver {
|
||||||
payload["provider"] = self.provider.rawValue
|
payload["channel"] = self.channel.rawValue
|
||||||
let to = self.to.trimmingCharacters(in: .whitespacesAndNewlines)
|
let to = self.to.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
if !to.isEmpty { payload["to"] = to }
|
if !to.isEmpty { payload["to"] = to }
|
||||||
payload["bestEffortDeliver"] = self.bestEffortDeliver
|
payload["bestEffortDeliver"] = self.bestEffortDeliver
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ extension CronJobEditor {
|
|||||||
self.payloadKind = .agentTurn
|
self.payloadKind = .agentTurn
|
||||||
self.agentMessage = "Run diagnostic"
|
self.agentMessage = "Run diagnostic"
|
||||||
self.deliver = true
|
self.deliver = true
|
||||||
self.provider = .last
|
self.channel = .last
|
||||||
self.to = "+15551230000"
|
self.to = "+15551230000"
|
||||||
self.thinking = "low"
|
self.thinking = "low"
|
||||||
self.timeoutSeconds = "90"
|
self.timeoutSeconds = "90"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ struct CronJobEditor: View {
|
|||||||
static let scheduleKindNote =
|
static let scheduleKindNote =
|
||||||
"“At” runs once, “Every” repeats with a duration, “Cron” uses a 5-field Unix expression."
|
"“At” runs once, “Every” repeats with a duration, “Cron” uses a 5-field Unix expression."
|
||||||
static let isolatedPayloadNote =
|
static let isolatedPayloadNote =
|
||||||
"Isolated jobs always run an agent turn. The result can be delivered to a provider, "
|
"Isolated jobs always run an agent turn. The result can be delivered to a channel, "
|
||||||
+ "and a short summary is posted back to your main chat."
|
+ "and a short summary is posted back to your main chat."
|
||||||
static let mainPayloadNote =
|
static let mainPayloadNote =
|
||||||
"System events are injected into the current main session. Agent turns require an isolated session target."
|
"System events are injected into the current main session. Agent turns require an isolated session target."
|
||||||
@@ -45,7 +45,7 @@ struct CronJobEditor: View {
|
|||||||
@State var systemEventText: String = ""
|
@State var systemEventText: String = ""
|
||||||
@State var agentMessage: String = ""
|
@State var agentMessage: String = ""
|
||||||
@State var deliver: Bool = false
|
@State var deliver: Bool = false
|
||||||
@State var provider: GatewayAgentProvider = .last
|
@State var channel: GatewayAgentChannel = .last
|
||||||
@State var to: String = ""
|
@State var to: String = ""
|
||||||
@State var thinking: String = ""
|
@State var thinking: String = ""
|
||||||
@State var timeoutSeconds: String = ""
|
@State var timeoutSeconds: String = ""
|
||||||
@@ -323,7 +323,7 @@ struct CronJobEditor: View {
|
|||||||
}
|
}
|
||||||
GridRow {
|
GridRow {
|
||||||
self.gridLabel("Deliver")
|
self.gridLabel("Deliver")
|
||||||
Toggle("Deliver result to a provider", isOn: self.$deliver)
|
Toggle("Deliver result to a channel", isOn: self.$deliver)
|
||||||
.toggleStyle(.switch)
|
.toggleStyle(.switch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -331,15 +331,15 @@ struct CronJobEditor: View {
|
|||||||
if self.deliver {
|
if self.deliver {
|
||||||
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 14, verticalSpacing: 10) {
|
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 14, verticalSpacing: 10) {
|
||||||
GridRow {
|
GridRow {
|
||||||
self.gridLabel("Provider")
|
self.gridLabel("Channel")
|
||||||
Picker("", selection: self.$provider) {
|
Picker("", selection: self.$channel) {
|
||||||
Text("last").tag(GatewayAgentProvider.last)
|
Text("last").tag(GatewayAgentChannel.last)
|
||||||
Text("whatsapp").tag(GatewayAgentProvider.whatsapp)
|
Text("whatsapp").tag(GatewayAgentChannel.whatsapp)
|
||||||
Text("telegram").tag(GatewayAgentProvider.telegram)
|
Text("telegram").tag(GatewayAgentChannel.telegram)
|
||||||
Text("discord").tag(GatewayAgentProvider.discord)
|
Text("discord").tag(GatewayAgentChannel.discord)
|
||||||
Text("slack").tag(GatewayAgentProvider.slack)
|
Text("slack").tag(GatewayAgentChannel.slack)
|
||||||
Text("signal").tag(GatewayAgentProvider.signal)
|
Text("signal").tag(GatewayAgentChannel.signal)
|
||||||
Text("imessage").tag(GatewayAgentProvider.imessage)
|
Text("imessage").tag(GatewayAgentChannel.imessage)
|
||||||
}
|
}
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
.pickerStyle(.segmented)
|
.pickerStyle(.segmented)
|
||||||
|
|||||||
@@ -67,20 +67,20 @@ enum CronSchedule: Codable, Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CronPayload: Codable, Equatable {
|
enum CronPayload: Codable, Equatable {
|
||||||
case systemEvent(text: String)
|
case systemEvent(text: String)
|
||||||
case agentTurn(
|
case agentTurn(
|
||||||
message: String,
|
message: String,
|
||||||
thinking: String?,
|
thinking: String?,
|
||||||
timeoutSeconds: Int?,
|
timeoutSeconds: Int?,
|
||||||
deliver: Bool?,
|
deliver: Bool?,
|
||||||
provider: String?,
|
channel: String?,
|
||||||
to: String?,
|
to: String?,
|
||||||
bestEffortDeliver: Bool?)
|
bestEffortDeliver: Bool?)
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case kind, text, message, thinking, timeoutSeconds, deliver, provider, to, bestEffortDeliver
|
case kind, text, message, thinking, timeoutSeconds, deliver, channel, provider, to, bestEffortDeliver
|
||||||
}
|
}
|
||||||
|
|
||||||
var kind: String {
|
var kind: String {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -95,15 +95,16 @@ enum CronPayload: Codable, Equatable {
|
|||||||
switch kind {
|
switch kind {
|
||||||
case "systemEvent":
|
case "systemEvent":
|
||||||
self = try .systemEvent(text: container.decode(String.self, forKey: .text))
|
self = try .systemEvent(text: container.decode(String.self, forKey: .text))
|
||||||
case "agentTurn":
|
case "agentTurn":
|
||||||
self = try .agentTurn(
|
self = try .agentTurn(
|
||||||
message: container.decode(String.self, forKey: .message),
|
message: container.decode(String.self, forKey: .message),
|
||||||
thinking: container.decodeIfPresent(String.self, forKey: .thinking),
|
thinking: container.decodeIfPresent(String.self, forKey: .thinking),
|
||||||
timeoutSeconds: container.decodeIfPresent(Int.self, forKey: .timeoutSeconds),
|
timeoutSeconds: container.decodeIfPresent(Int.self, forKey: .timeoutSeconds),
|
||||||
deliver: container.decodeIfPresent(Bool.self, forKey: .deliver),
|
deliver: container.decodeIfPresent(Bool.self, forKey: .deliver),
|
||||||
provider: container.decodeIfPresent(String.self, forKey: .provider),
|
channel: container.decodeIfPresent(String.self, forKey: .channel)
|
||||||
to: container.decodeIfPresent(String.self, forKey: .to),
|
?? container.decodeIfPresent(String.self, forKey: .provider),
|
||||||
bestEffortDeliver: container.decodeIfPresent(Bool.self, forKey: .bestEffortDeliver))
|
to: container.decodeIfPresent(String.self, forKey: .to),
|
||||||
|
bestEffortDeliver: container.decodeIfPresent(Bool.self, forKey: .bestEffortDeliver))
|
||||||
default:
|
default:
|
||||||
throw DecodingError.dataCorruptedError(
|
throw DecodingError.dataCorruptedError(
|
||||||
forKey: .kind,
|
forKey: .kind,
|
||||||
@@ -118,17 +119,17 @@ enum CronPayload: Codable, Equatable {
|
|||||||
switch self {
|
switch self {
|
||||||
case let .systemEvent(text):
|
case let .systemEvent(text):
|
||||||
try container.encode(text, forKey: .text)
|
try container.encode(text, forKey: .text)
|
||||||
case let .agentTurn(message, thinking, timeoutSeconds, deliver, provider, to, bestEffortDeliver):
|
case let .agentTurn(message, thinking, timeoutSeconds, deliver, channel, to, bestEffortDeliver):
|
||||||
try container.encode(message, forKey: .message)
|
try container.encode(message, forKey: .message)
|
||||||
try container.encodeIfPresent(thinking, forKey: .thinking)
|
try container.encodeIfPresent(thinking, forKey: .thinking)
|
||||||
try container.encodeIfPresent(timeoutSeconds, forKey: .timeoutSeconds)
|
try container.encodeIfPresent(timeoutSeconds, forKey: .timeoutSeconds)
|
||||||
try container.encodeIfPresent(deliver, forKey: .deliver)
|
try container.encodeIfPresent(deliver, forKey: .deliver)
|
||||||
try container.encodeIfPresent(provider, forKey: .provider)
|
try container.encodeIfPresent(channel, forKey: .channel)
|
||||||
try container.encodeIfPresent(to, forKey: .to)
|
try container.encodeIfPresent(to, forKey: .to)
|
||||||
try container.encodeIfPresent(bestEffortDeliver, forKey: .bestEffortDeliver)
|
try container.encodeIfPresent(bestEffortDeliver, forKey: .bestEffortDeliver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CronIsolation: Codable, Equatable {
|
struct CronIsolation: Codable, Equatable {
|
||||||
var postToMainPrefix: String?
|
var postToMainPrefix: String?
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ final class DeepLinkHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let provider = GatewayAgentProvider(raw: link.channel)
|
let channel = GatewayAgentChannel(raw: link.channel)
|
||||||
let explicitSessionKey = link.sessionKey?
|
let explicitSessionKey = link.sessionKey?
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
.nonEmpty
|
.nonEmpty
|
||||||
@@ -72,9 +72,9 @@ final class DeepLinkHandler {
|
|||||||
message: messagePreview,
|
message: messagePreview,
|
||||||
sessionKey: resolvedSessionKey,
|
sessionKey: resolvedSessionKey,
|
||||||
thinking: link.thinking?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty,
|
thinking: link.thinking?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty,
|
||||||
deliver: provider.shouldDeliver(link.deliver),
|
deliver: channel.shouldDeliver(link.deliver),
|
||||||
to: link.to?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty,
|
to: link.to?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty,
|
||||||
provider: provider,
|
channel: channel,
|
||||||
timeoutSeconds: link.timeoutSeconds,
|
timeoutSeconds: link.timeoutSeconds,
|
||||||
idempotencyKey: UUID().uuidString)
|
idempotencyKey: UUID().uuidString)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import OSLog
|
|||||||
|
|
||||||
private let gatewayConnectionLogger = Logger(subsystem: "com.clawdbot", category: "gateway.connection")
|
private let gatewayConnectionLogger = Logger(subsystem: "com.clawdbot", category: "gateway.connection")
|
||||||
|
|
||||||
enum GatewayAgentProvider: String, Codable, CaseIterable, Sendable {
|
enum GatewayAgentChannel: String, Codable, CaseIterable, Sendable {
|
||||||
case last
|
case last
|
||||||
case whatsapp
|
case whatsapp
|
||||||
case telegram
|
case telegram
|
||||||
@@ -18,7 +18,7 @@ enum GatewayAgentProvider: String, Codable, CaseIterable, Sendable {
|
|||||||
|
|
||||||
init(raw: String?) {
|
init(raw: String?) {
|
||||||
let normalized = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
let normalized = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
self = GatewayAgentProvider(rawValue: normalized) ?? .last
|
self = GatewayAgentChannel(rawValue: normalized) ?? .last
|
||||||
}
|
}
|
||||||
|
|
||||||
var isDeliverable: Bool { self != .webchat }
|
var isDeliverable: Bool { self != .webchat }
|
||||||
@@ -32,7 +32,7 @@ struct GatewayAgentInvocation: Sendable {
|
|||||||
var thinking: String?
|
var thinking: String?
|
||||||
var deliver: Bool = false
|
var deliver: Bool = false
|
||||||
var to: String?
|
var to: String?
|
||||||
var provider: GatewayAgentProvider = .last
|
var channel: GatewayAgentChannel = .last
|
||||||
var timeoutSeconds: Int?
|
var timeoutSeconds: Int?
|
||||||
var idempotencyKey: String = UUID().uuidString
|
var idempotencyKey: String = UUID().uuidString
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ actor GatewayConnection {
|
|||||||
case setHeartbeats = "set-heartbeats"
|
case setHeartbeats = "set-heartbeats"
|
||||||
case systemEvent = "system-event"
|
case systemEvent = "system-event"
|
||||||
case health
|
case health
|
||||||
case providersStatus = "providers.status"
|
case channelsStatus = "channels.status"
|
||||||
case configGet = "config.get"
|
case configGet = "config.get"
|
||||||
case configSet = "config.set"
|
case configSet = "config.set"
|
||||||
case wizardStart = "wizard.start"
|
case wizardStart = "wizard.start"
|
||||||
@@ -62,7 +62,7 @@ actor GatewayConnection {
|
|||||||
case talkMode = "talk.mode"
|
case talkMode = "talk.mode"
|
||||||
case webLoginStart = "web.login.start"
|
case webLoginStart = "web.login.start"
|
||||||
case webLoginWait = "web.login.wait"
|
case webLoginWait = "web.login.wait"
|
||||||
case providersLogout = "providers.logout"
|
case channelsLogout = "channels.logout"
|
||||||
case modelsList = "models.list"
|
case modelsList = "models.list"
|
||||||
case chatHistory = "chat.history"
|
case chatHistory = "chat.history"
|
||||||
case chatSend = "chat.send"
|
case chatSend = "chat.send"
|
||||||
@@ -368,7 +368,7 @@ extension GatewayConnection {
|
|||||||
"thinking": AnyCodable(invocation.thinking ?? "default"),
|
"thinking": AnyCodable(invocation.thinking ?? "default"),
|
||||||
"deliver": AnyCodable(invocation.deliver),
|
"deliver": AnyCodable(invocation.deliver),
|
||||||
"to": AnyCodable(invocation.to ?? ""),
|
"to": AnyCodable(invocation.to ?? ""),
|
||||||
"provider": AnyCodable(invocation.provider.rawValue),
|
"channel": AnyCodable(invocation.channel.rawValue),
|
||||||
"idempotencyKey": AnyCodable(invocation.idempotencyKey),
|
"idempotencyKey": AnyCodable(invocation.idempotencyKey),
|
||||||
]
|
]
|
||||||
if let timeout = invocation.timeoutSeconds {
|
if let timeout = invocation.timeoutSeconds {
|
||||||
@@ -389,7 +389,7 @@ extension GatewayConnection {
|
|||||||
sessionKey: String,
|
sessionKey: String,
|
||||||
deliver: Bool,
|
deliver: Bool,
|
||||||
to: String?,
|
to: String?,
|
||||||
provider: GatewayAgentProvider = .last,
|
channel: GatewayAgentChannel = .last,
|
||||||
timeoutSeconds: Int? = nil,
|
timeoutSeconds: Int? = nil,
|
||||||
idempotencyKey: String = UUID().uuidString) async -> (ok: Bool, error: String?)
|
idempotencyKey: String = UUID().uuidString) async -> (ok: Bool, error: String?)
|
||||||
{
|
{
|
||||||
@@ -399,7 +399,7 @@ extension GatewayConnection {
|
|||||||
thinking: thinking,
|
thinking: thinking,
|
||||||
deliver: deliver,
|
deliver: deliver,
|
||||||
to: to,
|
to: to,
|
||||||
provider: provider,
|
channel: channel,
|
||||||
timeoutSeconds: timeoutSeconds,
|
timeoutSeconds: timeoutSeconds,
|
||||||
idempotencyKey: idempotencyKey))
|
idempotencyKey: idempotencyKey))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,19 +211,19 @@ final class GatewayProcessManager {
|
|||||||
private func describe(details instance: String?, port: Int, snap: HealthSnapshot?) -> String {
|
private func describe(details instance: String?, port: Int, snap: HealthSnapshot?) -> String {
|
||||||
let instanceText = instance ?? "pid unknown"
|
let instanceText = instance ?? "pid unknown"
|
||||||
if let snap {
|
if let snap {
|
||||||
let linkId = snap.providerOrder?.first(where: {
|
let linkId = snap.channelOrder?.first(where: {
|
||||||
if let summary = snap.providers[$0] { return summary.linked != nil }
|
if let summary = snap.channels[$0] { return summary.linked != nil }
|
||||||
return false
|
return false
|
||||||
}) ?? snap.providers.keys.first(where: {
|
}) ?? snap.channels.keys.first(where: {
|
||||||
if let summary = snap.providers[$0] { return summary.linked != nil }
|
if let summary = snap.channels[$0] { return summary.linked != nil }
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
let linked = linkId.flatMap { snap.providers[$0]?.linked } ?? false
|
let linked = linkId.flatMap { snap.channels[$0]?.linked } ?? false
|
||||||
let authAge = linkId.flatMap { snap.providers[$0]?.authAgeMs }.flatMap(msToAge) ?? "unknown age"
|
let authAge = linkId.flatMap { snap.channels[$0]?.authAgeMs }.flatMap(msToAge) ?? "unknown age"
|
||||||
let label =
|
let label =
|
||||||
linkId.flatMap { snap.providerLabels?[$0] } ??
|
linkId.flatMap { snap.channelLabels?[$0] } ??
|
||||||
linkId?.capitalized ??
|
linkId?.capitalized ??
|
||||||
"provider"
|
"channel"
|
||||||
let linkText = linked ? "linked" : "not linked"
|
let linkText = linked ? "linked" : "not linked"
|
||||||
return "port \(port), \(label) \(linkText), auth \(authAge), \(instanceText)"
|
return "port \(port), \(label) \(linkText), auth \(authAge), \(instanceText)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -496,18 +496,18 @@ struct GeneralSettings: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let snap = snapshot {
|
if let snap = snapshot {
|
||||||
let linkId = snap.providerOrder?.first(where: {
|
let linkId = snap.channelOrder?.first(where: {
|
||||||
if let summary = snap.providers[$0] { return summary.linked != nil }
|
if let summary = snap.channels[$0] { return summary.linked != nil }
|
||||||
return false
|
return false
|
||||||
}) ?? snap.providers.keys.first(where: {
|
}) ?? snap.channels.keys.first(where: {
|
||||||
if let summary = snap.providers[$0] { return summary.linked != nil }
|
if let summary = snap.channels[$0] { return summary.linked != nil }
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
let linkLabel =
|
let linkLabel =
|
||||||
linkId.flatMap { snap.providerLabels?[$0] } ??
|
linkId.flatMap { snap.channelLabels?[$0] } ??
|
||||||
linkId?.capitalized ??
|
linkId?.capitalized ??
|
||||||
"Link provider"
|
"Link channel"
|
||||||
let linkAge = linkId.flatMap { snap.providers[$0]?.authAgeMs }
|
let linkAge = linkId.flatMap { snap.channels[$0]?.authAgeMs }
|
||||||
Text("\(linkLabel) auth age: \(healthAgeString(linkAge))")
|
Text("\(linkLabel) auth age: \(healthAgeString(linkAge))")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Observation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct HealthSnapshot: Codable, Sendable {
|
struct HealthSnapshot: Codable, Sendable {
|
||||||
struct ProviderSummary: Codable, Sendable {
|
struct ChannelSummary: Codable, Sendable {
|
||||||
struct Probe: Codable, Sendable {
|
struct Probe: Codable, Sendable {
|
||||||
struct Bot: Codable, Sendable {
|
struct Bot: Codable, Sendable {
|
||||||
let username: String?
|
let username: String?
|
||||||
@@ -44,9 +44,9 @@ struct HealthSnapshot: Codable, Sendable {
|
|||||||
let ok: Bool?
|
let ok: Bool?
|
||||||
let ts: Double
|
let ts: Double
|
||||||
let durationMs: Double
|
let durationMs: Double
|
||||||
let providers: [String: ProviderSummary]
|
let channels: [String: ChannelSummary]
|
||||||
let providerOrder: [String]?
|
let channelOrder: [String]?
|
||||||
let providerLabels: [String: String]?
|
let channelLabels: [String: String]?
|
||||||
let heartbeatSeconds: Int?
|
let heartbeatSeconds: Int?
|
||||||
let sessions: Sessions
|
let sessions: Sessions
|
||||||
}
|
}
|
||||||
@@ -144,13 +144,13 @@ final class HealthStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func isProviderHealthy(_ summary: HealthSnapshot.ProviderSummary) -> Bool {
|
private static func isChannelHealthy(_ summary: HealthSnapshot.ChannelSummary) -> Bool {
|
||||||
guard summary.configured == true else { return false }
|
guard summary.configured == true else { return false }
|
||||||
// If probe is missing, treat it as "configured but unknown health" (not a hard fail).
|
// If probe is missing, treat it as "configured but unknown health" (not a hard fail).
|
||||||
return summary.probe?.ok ?? true
|
return summary.probe?.ok ?? true
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func describeProbeFailure(_ probe: HealthSnapshot.ProviderSummary.Probe) -> String {
|
private static func describeProbeFailure(_ probe: HealthSnapshot.ChannelSummary.Probe) -> String {
|
||||||
let elapsed = probe.elapsedMs.map { "\(Int($0))ms" }
|
let elapsed = probe.elapsedMs.map { "\(Int($0))ms" }
|
||||||
if let error = probe.error, error.lowercased().contains("timeout") || probe.status == nil {
|
if let error = probe.error, error.lowercased().contains("timeout") || probe.status == nil {
|
||||||
if let elapsed { return "Health check timed out (\(elapsed))" }
|
if let elapsed { return "Health check timed out (\(elapsed))" }
|
||||||
@@ -162,28 +162,28 @@ final class HealthStore {
|
|||||||
return "\(reason) (\(code))"
|
return "\(reason) (\(code))"
|
||||||
}
|
}
|
||||||
|
|
||||||
private func resolveLinkProvider(
|
private func resolveLinkChannel(
|
||||||
_ snap: HealthSnapshot) -> (id: String, summary: HealthSnapshot.ProviderSummary)?
|
_ snap: HealthSnapshot) -> (id: String, summary: HealthSnapshot.ChannelSummary)?
|
||||||
{
|
{
|
||||||
let order = snap.providerOrder ?? Array(snap.providers.keys)
|
let order = snap.channelOrder ?? Array(snap.channels.keys)
|
||||||
for id in order {
|
for id in order {
|
||||||
if let summary = snap.providers[id], summary.linked != nil {
|
if let summary = snap.channels[id], summary.linked != nil {
|
||||||
return (id: id, summary: summary)
|
return (id: id, summary: summary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func resolveFallbackProvider(
|
private func resolveFallbackChannel(
|
||||||
_ snap: HealthSnapshot,
|
_ snap: HealthSnapshot,
|
||||||
excluding id: String?) -> (id: String, summary: HealthSnapshot.ProviderSummary)?
|
excluding id: String?) -> (id: String, summary: HealthSnapshot.ChannelSummary)?
|
||||||
{
|
{
|
||||||
let order = snap.providerOrder ?? Array(snap.providers.keys)
|
let order = snap.channelOrder ?? Array(snap.channels.keys)
|
||||||
for providerId in order {
|
for channelId in order {
|
||||||
if providerId == id { continue }
|
if channelId == id { continue }
|
||||||
guard let summary = snap.providers[providerId] else { continue }
|
guard let summary = snap.channels[channelId] else { continue }
|
||||||
if Self.isProviderHealthy(summary) {
|
if Self.isChannelHealthy(summary) {
|
||||||
return (id: providerId, summary: summary)
|
return (id: channelId, summary: summary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -194,13 +194,13 @@ final class HealthStore {
|
|||||||
return .degraded(error)
|
return .degraded(error)
|
||||||
}
|
}
|
||||||
guard let snap = self.snapshot else { return .unknown }
|
guard let snap = self.snapshot else { return .unknown }
|
||||||
guard let link = self.resolveLinkProvider(snap) else { return .unknown }
|
guard let link = self.resolveLinkChannel(snap) else { return .unknown }
|
||||||
if link.summary.linked != true {
|
if link.summary.linked != true {
|
||||||
// Linking is optional if any other provider is healthy; don't paint the whole app red.
|
// Linking is optional if any other channel is healthy; don't paint the whole app red.
|
||||||
let fallback = self.resolveFallbackProvider(snap, excluding: link.id)
|
let fallback = self.resolveFallbackChannel(snap, excluding: link.id)
|
||||||
return fallback != nil ? .degraded("Not linked") : .linkingNeeded
|
return fallback != nil ? .degraded("Not linked") : .linkingNeeded
|
||||||
}
|
}
|
||||||
// A provider can be "linked" but still unhealthy (failed probe / cannot connect).
|
// A channel can be "linked" but still unhealthy (failed probe / cannot connect).
|
||||||
if let probe = link.summary.probe, probe.ok == false {
|
if let probe = link.summary.probe, probe.ok == false {
|
||||||
return .degraded(Self.describeProbeFailure(probe))
|
return .degraded(Self.describeProbeFailure(probe))
|
||||||
}
|
}
|
||||||
@@ -211,10 +211,10 @@ final class HealthStore {
|
|||||||
if self.isRefreshing { return "Health check running…" }
|
if self.isRefreshing { return "Health check running…" }
|
||||||
if let error = self.lastError { return "Health check failed: \(error)" }
|
if let error = self.lastError { return "Health check failed: \(error)" }
|
||||||
guard let snap = self.snapshot else { return "Health check pending" }
|
guard let snap = self.snapshot else { return "Health check pending" }
|
||||||
guard let link = self.resolveLinkProvider(snap) else { return "Health check pending" }
|
guard let link = self.resolveLinkChannel(snap) else { return "Health check pending" }
|
||||||
if link.summary.linked != true {
|
if link.summary.linked != true {
|
||||||
if let fallback = self.resolveFallbackProvider(snap, excluding: link.id) {
|
if let fallback = self.resolveFallbackChannel(snap, excluding: link.id) {
|
||||||
let fallbackLabel = snap.providerLabels?[fallback.id] ?? fallback.id.capitalized
|
let fallbackLabel = snap.channelLabels?[fallback.id] ?? fallback.id.capitalized
|
||||||
let fallbackState = (fallback.summary.probe?.ok ?? true) ? "ok" : "degraded"
|
let fallbackState = (fallback.summary.probe?.ok ?? true) ? "ok" : "degraded"
|
||||||
return "\(fallbackLabel) \(fallbackState) · Not linked — run clawdbot login"
|
return "\(fallbackLabel) \(fallbackState) · Not linked — run clawdbot login"
|
||||||
}
|
}
|
||||||
@@ -247,10 +247,10 @@ final class HealthStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func describeFailure(from snap: HealthSnapshot, fallback: String?) -> String {
|
func describeFailure(from snap: HealthSnapshot, fallback: String?) -> String {
|
||||||
if let link = self.resolveLinkProvider(snap), link.summary.linked != true {
|
if let link = self.resolveLinkChannel(snap), link.summary.linked != true {
|
||||||
return "Not linked — run clawdbot login"
|
return "Not linked — run clawdbot login"
|
||||||
}
|
}
|
||||||
if let link = self.resolveLinkProvider(snap), let probe = link.summary.probe, probe.ok == false {
|
if let link = self.resolveLinkChannel(snap), let probe = link.summary.probe, probe.ok == false {
|
||||||
return Self.describeProbeFailure(probe)
|
return Self.describeProbeFailure(probe)
|
||||||
}
|
}
|
||||||
if let fallback, !fallback.isEmpty {
|
if let fallback, !fallback.isEmpty {
|
||||||
|
|||||||
@@ -242,18 +242,18 @@ final class InstancesStore {
|
|||||||
do {
|
do {
|
||||||
let data = try await ControlChannel.shared.health(timeout: 8)
|
let data = try await ControlChannel.shared.health(timeout: 8)
|
||||||
guard let snap = decodeHealthSnapshot(from: data) else { return }
|
guard let snap = decodeHealthSnapshot(from: data) else { return }
|
||||||
let linkId = snap.providerOrder?.first(where: {
|
let linkId = snap.channelOrder?.first(where: {
|
||||||
if let summary = snap.providers[$0] { return summary.linked != nil }
|
if let summary = snap.channels[$0] { return summary.linked != nil }
|
||||||
return false
|
return false
|
||||||
}) ?? snap.providers.keys.first(where: {
|
}) ?? snap.channels.keys.first(where: {
|
||||||
if let summary = snap.providers[$0] { return summary.linked != nil }
|
if let summary = snap.channels[$0] { return summary.linked != nil }
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
let linked = linkId.flatMap { snap.providers[$0]?.linked } ?? false
|
let linked = linkId.flatMap { snap.channels[$0]?.linked } ?? false
|
||||||
let linkLabel =
|
let linkLabel =
|
||||||
linkId.flatMap { snap.providerLabels?[$0] } ??
|
linkId.flatMap { snap.channelLabels?[$0] } ??
|
||||||
linkId?.capitalized ??
|
linkId?.capitalized ??
|
||||||
"provider"
|
"channel"
|
||||||
let entry = InstanceInfo(
|
let entry = InstanceInfo(
|
||||||
id: "health-\(snap.ts)",
|
id: "health-\(snap.ts)",
|
||||||
host: "gateway (health)",
|
host: "gateway (health)",
|
||||||
|
|||||||
@@ -694,7 +694,7 @@ extension OnboardingView {
|
|||||||
systemImage: "bubble.left.and.bubble.right")
|
systemImage: "bubble.left.and.bubble.right")
|
||||||
self.featureActionRow(
|
self.featureActionRow(
|
||||||
title: "Connect WhatsApp or Telegram",
|
title: "Connect WhatsApp or Telegram",
|
||||||
subtitle: "Open Settings → Connections to link providers and monitor status.",
|
subtitle: "Open Settings → Connections to link channels and monitor status.",
|
||||||
systemImage: "link")
|
systemImage: "link")
|
||||||
{
|
{
|
||||||
self.openSettings(tab: .connections)
|
self.openSettings(tab: .connections)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ enum VoiceWakeForwarder {
|
|||||||
var thinking: String = "low"
|
var thinking: String = "low"
|
||||||
var deliver: Bool = true
|
var deliver: Bool = true
|
||||||
var to: String?
|
var to: String?
|
||||||
var provider: GatewayAgentProvider = .last
|
var channel: GatewayAgentChannel = .last
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
@@ -46,14 +46,14 @@ enum VoiceWakeForwarder {
|
|||||||
options: ForwardOptions = ForwardOptions()) async -> Result<Void, VoiceWakeForwardError>
|
options: ForwardOptions = ForwardOptions()) async -> Result<Void, VoiceWakeForwardError>
|
||||||
{
|
{
|
||||||
let payload = Self.prefixedTranscript(transcript)
|
let payload = Self.prefixedTranscript(transcript)
|
||||||
let deliver = options.provider.shouldDeliver(options.deliver)
|
let deliver = options.channel.shouldDeliver(options.deliver)
|
||||||
let result = await GatewayConnection.shared.sendAgent(GatewayAgentInvocation(
|
let result = await GatewayConnection.shared.sendAgent(GatewayAgentInvocation(
|
||||||
message: payload,
|
message: payload,
|
||||||
sessionKey: options.sessionKey,
|
sessionKey: options.sessionKey,
|
||||||
thinking: options.thinking,
|
thinking: options.thinking,
|
||||||
deliver: deliver,
|
deliver: deliver,
|
||||||
to: options.to,
|
to: options.to,
|
||||||
provider: options.provider))
|
channel: options.channel))
|
||||||
|
|
||||||
if result.ok {
|
if result.ok {
|
||||||
self.logger.info("voice wake forward ok")
|
self.logger.info("voice wake forward ok")
|
||||||
|
|||||||
@@ -4,22 +4,22 @@ import Testing
|
|||||||
|
|
||||||
@Suite(.serialized)
|
@Suite(.serialized)
|
||||||
@MainActor
|
@MainActor
|
||||||
struct ConnectionsSettingsSmokeTests {
|
struct ConnectionsSettingsSmokeTests {
|
||||||
@Test func connectionsSettingsBuildsBodyWithSnapshot() {
|
@Test func connectionsSettingsBuildsBodyWithSnapshot() {
|
||||||
let store = ConnectionsStore(isPreview: true)
|
let store = ConnectionsStore(isPreview: true)
|
||||||
store.snapshot = ProvidersStatusSnapshot(
|
store.snapshot = ChannelsStatusSnapshot(
|
||||||
ts: 1_700_000_000_000,
|
ts: 1_700_000_000_000,
|
||||||
providerOrder: ["whatsapp", "telegram", "signal", "imessage"],
|
channelOrder: ["whatsapp", "telegram", "signal", "imessage"],
|
||||||
providerLabels: [
|
channelLabels: [
|
||||||
"whatsapp": "WhatsApp",
|
"whatsapp": "WhatsApp",
|
||||||
"telegram": "Telegram",
|
"telegram": "Telegram",
|
||||||
"signal": "Signal",
|
"signal": "Signal",
|
||||||
"imessage": "iMessage",
|
"imessage": "iMessage",
|
||||||
],
|
],
|
||||||
providers: [
|
channels: [
|
||||||
"whatsapp": AnyCodable([
|
"whatsapp": AnyCodable([
|
||||||
"configured": true,
|
"configured": true,
|
||||||
"linked": true,
|
"linked": true,
|
||||||
"authAgeMs": 86_400_000,
|
"authAgeMs": 86_400_000,
|
||||||
"self": ["e164": "+15551234567"],
|
"self": ["e164": "+15551234567"],
|
||||||
"running": true,
|
"running": true,
|
||||||
@@ -70,13 +70,13 @@ struct ConnectionsSettingsSmokeTests {
|
|||||||
"lastError": "not configured",
|
"lastError": "not configured",
|
||||||
"probe": ["ok": false, "error": "imsg not found (imsg)"],
|
"probe": ["ok": false, "error": "imsg not found (imsg)"],
|
||||||
"lastProbeAt": 1_700_000_050_000,
|
"lastProbeAt": 1_700_000_050_000,
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
providerAccounts: [:],
|
channelAccounts: [:],
|
||||||
providerDefaultAccountId: [
|
channelDefaultAccountId: [
|
||||||
"whatsapp": "default",
|
"whatsapp": "default",
|
||||||
"telegram": "default",
|
"telegram": "default",
|
||||||
"signal": "default",
|
"signal": "default",
|
||||||
"imessage": "default",
|
"imessage": "default",
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -93,23 +93,23 @@ struct ConnectionsSettingsSmokeTests {
|
|||||||
|
|
||||||
let view = ConnectionsSettings(store: store)
|
let view = ConnectionsSettings(store: store)
|
||||||
_ = view.body
|
_ = view.body
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func connectionsSettingsBuildsBodyWithoutSnapshot() {
|
@Test func connectionsSettingsBuildsBodyWithoutSnapshot() {
|
||||||
let store = ConnectionsStore(isPreview: true)
|
let store = ConnectionsStore(isPreview: true)
|
||||||
store.snapshot = ProvidersStatusSnapshot(
|
store.snapshot = ChannelsStatusSnapshot(
|
||||||
ts: 1_700_000_000_000,
|
ts: 1_700_000_000_000,
|
||||||
providerOrder: ["whatsapp", "telegram", "signal", "imessage"],
|
channelOrder: ["whatsapp", "telegram", "signal", "imessage"],
|
||||||
providerLabels: [
|
channelLabels: [
|
||||||
"whatsapp": "WhatsApp",
|
"whatsapp": "WhatsApp",
|
||||||
"telegram": "Telegram",
|
"telegram": "Telegram",
|
||||||
"signal": "Signal",
|
"signal": "Signal",
|
||||||
"imessage": "iMessage",
|
"imessage": "iMessage",
|
||||||
],
|
],
|
||||||
providers: [
|
channels: [
|
||||||
"whatsapp": AnyCodable([
|
"whatsapp": AnyCodable([
|
||||||
"configured": false,
|
"configured": false,
|
||||||
"linked": false,
|
"linked": false,
|
||||||
"running": false,
|
"running": false,
|
||||||
"connected": false,
|
"connected": false,
|
||||||
"reconnectAttempts": 0,
|
"reconnectAttempts": 0,
|
||||||
@@ -146,13 +146,13 @@ struct ConnectionsSettingsSmokeTests {
|
|||||||
"cliPath": "imsg",
|
"cliPath": "imsg",
|
||||||
"probe": ["ok": false, "error": "imsg not found (imsg)"],
|
"probe": ["ok": false, "error": "imsg not found (imsg)"],
|
||||||
"lastProbeAt": 1_700_000_200_000,
|
"lastProbeAt": 1_700_000_200_000,
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
providerAccounts: [:],
|
channelAccounts: [:],
|
||||||
providerDefaultAccountId: [
|
channelDefaultAccountId: [
|
||||||
"whatsapp": "default",
|
"whatsapp": "default",
|
||||||
"telegram": "default",
|
"telegram": "default",
|
||||||
"signal": "default",
|
"signal": "default",
|
||||||
"imessage": "default",
|
"imessage": "default",
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,17 @@ import Foundation
|
|||||||
import Testing
|
import Testing
|
||||||
@testable import Clawdbot
|
@testable import Clawdbot
|
||||||
|
|
||||||
@Suite struct HealthDecodeTests {
|
@Suite struct HealthDecodeTests {
|
||||||
private let sampleJSON: String = // minimal but complete payload
|
private let sampleJSON: String = // minimal but complete payload
|
||||||
"""
|
"""
|
||||||
{"ts":1733622000,"durationMs":420,"providers":{"whatsapp":{"linked":true,"authAgeMs":120000},"telegram":{"configured":true,"probe":{"ok":true,"elapsedMs":800}}},"providerOrder":["whatsapp","telegram"],"heartbeatSeconds":60,"sessions":{"path":"/tmp/sessions.json","count":1,"recent":[{"key":"abc","updatedAt":1733621900,"age":120000}]}}
|
{"ts":1733622000,"durationMs":420,"channels":{"whatsapp":{"linked":true,"authAgeMs":120000},"telegram":{"configured":true,"probe":{"ok":true,"elapsedMs":800}}},"channelOrder":["whatsapp","telegram"],"heartbeatSeconds":60,"sessions":{"path":"/tmp/sessions.json","count":1,"recent":[{"key":"abc","updatedAt":1733621900,"age":120000}]}}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@Test func decodesCleanJSON() async throws {
|
@Test func decodesCleanJSON() async throws {
|
||||||
let data = Data(sampleJSON.utf8)
|
let data = Data(sampleJSON.utf8)
|
||||||
let snap = decodeHealthSnapshot(from: data)
|
let snap = decodeHealthSnapshot(from: data)
|
||||||
|
|
||||||
#expect(snap?.providers["whatsapp"]?.linked == true)
|
#expect(snap?.channels["whatsapp"]?.linked == true)
|
||||||
#expect(snap?.sessions.count == 1)
|
#expect(snap?.sessions.count == 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ import Testing
|
|||||||
let noisy = "debug: something logged\n" + self.sampleJSON + "\ntrailer"
|
let noisy = "debug: something logged\n" + self.sampleJSON + "\ntrailer"
|
||||||
let snap = decodeHealthSnapshot(from: Data(noisy.utf8))
|
let snap = decodeHealthSnapshot(from: Data(noisy.utf8))
|
||||||
|
|
||||||
#expect(snap?.providers["telegram"]?.probe?.elapsedMs == 800)
|
#expect(snap?.channels["telegram"]?.probe?.elapsedMs == 800)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func failsWithoutBraces() async throws {
|
@Test func failsWithoutBraces() async throws {
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import Testing
|
|||||||
@testable import Clawdbot
|
@testable import Clawdbot
|
||||||
|
|
||||||
@Suite struct HealthStoreStateTests {
|
@Suite struct HealthStoreStateTests {
|
||||||
@Test @MainActor func linkedProviderProbeFailureDegradesState() async throws {
|
@Test @MainActor func linkedChannelProbeFailureDegradesState() async throws {
|
||||||
let snap = HealthSnapshot(
|
let snap = HealthSnapshot(
|
||||||
ok: true,
|
ok: true,
|
||||||
ts: 0,
|
ts: 0,
|
||||||
durationMs: 1,
|
durationMs: 1,
|
||||||
providers: [
|
channels: [
|
||||||
"whatsapp": .init(
|
"whatsapp": .init(
|
||||||
configured: true,
|
configured: true,
|
||||||
linked: true,
|
linked: true,
|
||||||
@@ -22,8 +22,8 @@ import Testing
|
|||||||
webhook: nil),
|
webhook: nil),
|
||||||
lastProbeAt: 0),
|
lastProbeAt: 0),
|
||||||
],
|
],
|
||||||
providerOrder: ["whatsapp"],
|
channelOrder: ["whatsapp"],
|
||||||
providerLabels: ["whatsapp": "WhatsApp"],
|
channelLabels: ["whatsapp": "WhatsApp"],
|
||||||
heartbeatSeconds: 60,
|
heartbeatSeconds: 60,
|
||||||
sessions: .init(path: "/tmp/sessions.json", count: 0, recent: []))
|
sessions: .init(path: "/tmp/sessions.json", count: 0, recent: []))
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ import Testing
|
|||||||
case let .degraded(message):
|
case let .degraded(message):
|
||||||
#expect(!message.isEmpty)
|
#expect(!message.isEmpty)
|
||||||
default:
|
default:
|
||||||
Issue.record("Expected degraded state when probe fails for linked provider")
|
Issue.record("Expected degraded state when probe fails for linked channel")
|
||||||
}
|
}
|
||||||
|
|
||||||
#expect(store.summaryLine.contains("probe degraded"))
|
#expect(store.summaryLine.contains("probe degraded"))
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ Short, exact flow of one agent run.
|
|||||||
- `assistant`: streamed deltas from pi-agent-core
|
- `assistant`: streamed deltas from pi-agent-core
|
||||||
- `tool`: streamed tool events from pi-agent-core
|
- `tool`: streamed tool events from pi-agent-core
|
||||||
|
|
||||||
## Chat provider handling
|
## Chat channel handling
|
||||||
- Assistant deltas are buffered into chat `delta` messages.
|
- Assistant deltas are buffered into chat `delta` messages.
|
||||||
- A chat `final` is emitted on **lifecycle end/error**.
|
- A chat `final` is emitted on **lifecycle end/error**.
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ read_when:
|
|||||||
---
|
---
|
||||||
# Model providers
|
# Model providers
|
||||||
|
|
||||||
This page covers **LLM/model providers** (not chat providers like WhatsApp/Telegram).
|
This page covers **LLM/model providers** (not chat channels like WhatsApp/Telegram).
|
||||||
For model selection rules, see [/concepts/models](/concepts/models).
|
For model selection rules, see [/concepts/models](/concepts/models).
|
||||||
|
|
||||||
## Quick rules
|
## Quick rules
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ incompatible, update the global CLI to match the app version.
|
|||||||
```bash
|
```bash
|
||||||
clawdbot --version
|
clawdbot --version
|
||||||
|
|
||||||
CLAWDBOT_SKIP_PROVIDERS=1 \
|
CLAWDBOT_SKIP_CHANNELS=1 \
|
||||||
CLAWDBOT_SKIP_CANVAS_HOST=1 \
|
CLAWDBOT_SKIP_CANVAS_HOST=1 \
|
||||||
clawdbot gateway --port 18999 --bind loopback
|
clawdbot gateway --port 18999 --bind loopback
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -22,15 +22,15 @@ echo "Creating Docker network..."
|
|||||||
docker network create "$NET_NAME" >/dev/null
|
docker network create "$NET_NAME" >/dev/null
|
||||||
|
|
||||||
echo "Starting gateway container..."
|
echo "Starting gateway container..."
|
||||||
docker run --rm -d \
|
docker run --rm -d \
|
||||||
--name "$GW_NAME" \
|
--name "$GW_NAME" \
|
||||||
--network "$NET_NAME" \
|
--network "$NET_NAME" \
|
||||||
-e "CLAWDBOT_GATEWAY_TOKEN=$TOKEN" \
|
-e "CLAWDBOT_GATEWAY_TOKEN=$TOKEN" \
|
||||||
-e "CLAWDBOT_SKIP_PROVIDERS=1" \
|
-e "CLAWDBOT_SKIP_CHANNELS=1" \
|
||||||
-e "CLAWDBOT_SKIP_GMAIL_WATCHER=1" \
|
-e "CLAWDBOT_SKIP_GMAIL_WATCHER=1" \
|
||||||
-e "CLAWDBOT_SKIP_CRON=1" \
|
-e "CLAWDBOT_SKIP_CRON=1" \
|
||||||
-e "CLAWDBOT_SKIP_CANVAS_HOST=1" \
|
-e "CLAWDBOT_SKIP_CANVAS_HOST=1" \
|
||||||
"$IMAGE_NAME" \
|
"$IMAGE_NAME" \
|
||||||
bash -lc "node dist/index.js gateway --port $PORT --bind lan --allow-unconfigured > /tmp/gateway-net-e2e.log 2>&1"
|
bash -lc "node dist/index.js gateway --port $PORT --bind lan --allow-unconfigured > /tmp/gateway-net-e2e.log 2>&1"
|
||||||
|
|
||||||
echo "Waiting for gateway to come up..."
|
echo "Waiting for gateway to come up..."
|
||||||
|
|||||||
@@ -445,11 +445,11 @@ function buildCollectPrompt(items: FollowupRun[], summary?: string): string {
|
|||||||
/**
|
/**
|
||||||
* Checks if queued items have different routable originating channels.
|
* Checks if queued items have different routable originating channels.
|
||||||
*
|
*
|
||||||
* Returns true if messages come from different providers (e.g., Slack + Telegram),
|
* Returns true if messages come from different channels (e.g., Slack + Telegram),
|
||||||
* meaning they cannot be safely collected into one prompt without losing routing.
|
* meaning they cannot be safely collected into one prompt without losing routing.
|
||||||
* Also returns true for a mix of routable and non-routable channels.
|
* Also returns true for a mix of routable and non-routable channels.
|
||||||
*/
|
*/
|
||||||
function hasCrossProviderItems(items: FollowupRun[]): boolean {
|
function hasCrossChannelItems(items: FollowupRun[]): boolean {
|
||||||
const keys = new Set<string>();
|
const keys = new Set<string>();
|
||||||
let hasUnkeyed = false;
|
let hasUnkeyed = false;
|
||||||
|
|
||||||
@@ -499,33 +499,33 @@ export function scheduleFollowupDrain(
|
|||||||
if (forceIndividualCollect) {
|
if (forceIndividualCollect) {
|
||||||
const next = queue.items.shift();
|
const next = queue.items.shift();
|
||||||
if (!next) break;
|
if (!next) break;
|
||||||
await runFollowup(next);
|
await runFollowup(next);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if messages span multiple providers.
|
// Check if messages span multiple channels.
|
||||||
// If so, process individually to preserve per-message routing.
|
// If so, process individually to preserve per-message routing.
|
||||||
const isCrossProvider = hasCrossProviderItems(queue.items);
|
const isCrossChannel = hasCrossChannelItems(queue.items);
|
||||||
|
|
||||||
if (isCrossProvider) {
|
if (isCrossChannel) {
|
||||||
forceIndividualCollect = true;
|
forceIndividualCollect = true;
|
||||||
// Process one at a time to preserve per-message routing info.
|
// Process one at a time to preserve per-message routing info.
|
||||||
const next = queue.items.shift();
|
const next = queue.items.shift();
|
||||||
if (!next) break;
|
if (!next) break;
|
||||||
await runFollowup(next);
|
await runFollowup(next);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Same-provider messages can be safely collected.
|
// Same-channel messages can be safely collected.
|
||||||
const items = queue.items.splice(0, queue.items.length);
|
const items = queue.items.splice(0, queue.items.length);
|
||||||
const summary = buildSummaryPrompt(queue);
|
const summary = buildSummaryPrompt(queue);
|
||||||
const run = items.at(-1)?.run ?? queue.lastRun;
|
const run = items.at(-1)?.run ?? queue.lastRun;
|
||||||
if (!run) break;
|
if (!run) break;
|
||||||
|
|
||||||
// Preserve originating channel from items when collecting same-provider.
|
// Preserve originating channel from items when collecting same-channel.
|
||||||
const originatingChannel = items.find(
|
const originatingChannel = items.find(
|
||||||
(i) => i.originatingChannel,
|
(i) => i.originatingChannel,
|
||||||
)?.originatingChannel;
|
)?.originatingChannel;
|
||||||
const originatingTo = items.find(
|
const originatingTo = items.find(
|
||||||
(i) => i.originatingTo,
|
(i) => i.originatingTo,
|
||||||
)?.originatingTo;
|
)?.originatingTo;
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ describe("gateway SIGTERM", () => {
|
|||||||
...process.env,
|
...process.env,
|
||||||
CLAWDBOT_STATE_DIR: stateDir,
|
CLAWDBOT_STATE_DIR: stateDir,
|
||||||
CLAWDBOT_CONFIG_PATH: configPath,
|
CLAWDBOT_CONFIG_PATH: configPath,
|
||||||
CLAWDBOT_SKIP_PROVIDERS: "1",
|
CLAWDBOT_SKIP_CHANNELS: "1",
|
||||||
CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER: "1",
|
CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER: "1",
|
||||||
CLAWDBOT_SKIP_CANVAS_HOST: "1",
|
CLAWDBOT_SKIP_CANVAS_HOST: "1",
|
||||||
// Avoid port collisions with other test processes that may also start a bridge server.
|
// Avoid port collisions with other test processes that may also start a bridge server.
|
||||||
|
|||||||
@@ -96,21 +96,21 @@ async function connectReq(params: { url: string; token?: string }) {
|
|||||||
|
|
||||||
describe("onboard (non-interactive): gateway auth", () => {
|
describe("onboard (non-interactive): gateway auth", () => {
|
||||||
it("writes gateway token auth into config and gateway enforces it", async () => {
|
it("writes gateway token auth into config and gateway enforces it", async () => {
|
||||||
const prev = {
|
const prev = {
|
||||||
home: process.env.HOME,
|
home: process.env.HOME,
|
||||||
stateDir: process.env.CLAWDBOT_STATE_DIR,
|
stateDir: process.env.CLAWDBOT_STATE_DIR,
|
||||||
configPath: process.env.CLAWDBOT_CONFIG_PATH,
|
configPath: process.env.CLAWDBOT_CONFIG_PATH,
|
||||||
skipProviders: process.env.CLAWDBOT_SKIP_PROVIDERS,
|
skipChannels: process.env.CLAWDBOT_SKIP_CHANNELS,
|
||||||
skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER,
|
skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER,
|
||||||
skipCron: process.env.CLAWDBOT_SKIP_CRON,
|
skipCron: process.env.CLAWDBOT_SKIP_CRON,
|
||||||
skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST,
|
skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST,
|
||||||
token: process.env.CLAWDBOT_GATEWAY_TOKEN,
|
token: process.env.CLAWDBOT_GATEWAY_TOKEN,
|
||||||
};
|
};
|
||||||
|
|
||||||
process.env.CLAWDBOT_SKIP_PROVIDERS = "1";
|
process.env.CLAWDBOT_SKIP_CHANNELS = "1";
|
||||||
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1";
|
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1";
|
||||||
process.env.CLAWDBOT_SKIP_CRON = "1";
|
process.env.CLAWDBOT_SKIP_CRON = "1";
|
||||||
process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1";
|
process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1";
|
||||||
delete process.env.CLAWDBOT_GATEWAY_TOKEN;
|
delete process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||||
|
|
||||||
const tempHome = await fs.mkdtemp(
|
const tempHome = await fs.mkdtemp(
|
||||||
@@ -186,7 +186,7 @@ describe("onboard (non-interactive): gateway auth", () => {
|
|||||||
process.env.HOME = prev.home;
|
process.env.HOME = prev.home;
|
||||||
process.env.CLAWDBOT_STATE_DIR = prev.stateDir;
|
process.env.CLAWDBOT_STATE_DIR = prev.stateDir;
|
||||||
process.env.CLAWDBOT_CONFIG_PATH = prev.configPath;
|
process.env.CLAWDBOT_CONFIG_PATH = prev.configPath;
|
||||||
process.env.CLAWDBOT_SKIP_PROVIDERS = prev.skipProviders;
|
process.env.CLAWDBOT_SKIP_CHANNELS = prev.skipChannels;
|
||||||
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = prev.skipGmail;
|
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = prev.skipGmail;
|
||||||
process.env.CLAWDBOT_SKIP_CRON = prev.skipCron;
|
process.env.CLAWDBOT_SKIP_CRON = prev.skipCron;
|
||||||
process.env.CLAWDBOT_SKIP_CANVAS_HOST = prev.skipCanvas;
|
process.env.CLAWDBOT_SKIP_CANVAS_HOST = prev.skipCanvas;
|
||||||
|
|||||||
@@ -106,21 +106,21 @@ describe("onboard (non-interactive): lan bind auto-token", () => {
|
|||||||
// Windows runner occasionally drops the temp config write in this flow; skip to keep CI green.
|
// Windows runner occasionally drops the temp config write in this flow; skip to keep CI green.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const prev = {
|
const prev = {
|
||||||
home: process.env.HOME,
|
home: process.env.HOME,
|
||||||
stateDir: process.env.CLAWDBOT_STATE_DIR,
|
stateDir: process.env.CLAWDBOT_STATE_DIR,
|
||||||
configPath: process.env.CLAWDBOT_CONFIG_PATH,
|
configPath: process.env.CLAWDBOT_CONFIG_PATH,
|
||||||
skipProviders: process.env.CLAWDBOT_SKIP_PROVIDERS,
|
skipChannels: process.env.CLAWDBOT_SKIP_CHANNELS,
|
||||||
skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER,
|
skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER,
|
||||||
skipCron: process.env.CLAWDBOT_SKIP_CRON,
|
skipCron: process.env.CLAWDBOT_SKIP_CRON,
|
||||||
skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST,
|
skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST,
|
||||||
token: process.env.CLAWDBOT_GATEWAY_TOKEN,
|
token: process.env.CLAWDBOT_GATEWAY_TOKEN,
|
||||||
};
|
};
|
||||||
|
|
||||||
process.env.CLAWDBOT_SKIP_PROVIDERS = "1";
|
process.env.CLAWDBOT_SKIP_CHANNELS = "1";
|
||||||
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1";
|
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1";
|
||||||
process.env.CLAWDBOT_SKIP_CRON = "1";
|
process.env.CLAWDBOT_SKIP_CRON = "1";
|
||||||
process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1";
|
process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1";
|
||||||
delete process.env.CLAWDBOT_GATEWAY_TOKEN;
|
delete process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||||
|
|
||||||
const tempHome = await fs.mkdtemp(
|
const tempHome = await fs.mkdtemp(
|
||||||
@@ -215,7 +215,7 @@ describe("onboard (non-interactive): lan bind auto-token", () => {
|
|||||||
process.env.HOME = prev.home;
|
process.env.HOME = prev.home;
|
||||||
process.env.CLAWDBOT_STATE_DIR = prev.stateDir;
|
process.env.CLAWDBOT_STATE_DIR = prev.stateDir;
|
||||||
process.env.CLAWDBOT_CONFIG_PATH = prev.configPath;
|
process.env.CLAWDBOT_CONFIG_PATH = prev.configPath;
|
||||||
process.env.CLAWDBOT_SKIP_PROVIDERS = prev.skipProviders;
|
process.env.CLAWDBOT_SKIP_CHANNELS = prev.skipChannels;
|
||||||
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = prev.skipGmail;
|
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = prev.skipGmail;
|
||||||
process.env.CLAWDBOT_SKIP_CRON = prev.skipCron;
|
process.env.CLAWDBOT_SKIP_CRON = prev.skipCron;
|
||||||
process.env.CLAWDBOT_SKIP_CANVAS_HOST = prev.skipCanvas;
|
process.env.CLAWDBOT_SKIP_CANVAS_HOST = prev.skipCanvas;
|
||||||
|
|||||||
@@ -27,22 +27,22 @@ async function getFreePort(): Promise<number> {
|
|||||||
|
|
||||||
describe("onboard (non-interactive): remote gateway config", () => {
|
describe("onboard (non-interactive): remote gateway config", () => {
|
||||||
it("writes gateway.remote url/token and callGateway uses them", async () => {
|
it("writes gateway.remote url/token and callGateway uses them", async () => {
|
||||||
const prev = {
|
const prev = {
|
||||||
home: process.env.HOME,
|
home: process.env.HOME,
|
||||||
stateDir: process.env.CLAWDBOT_STATE_DIR,
|
stateDir: process.env.CLAWDBOT_STATE_DIR,
|
||||||
configPath: process.env.CLAWDBOT_CONFIG_PATH,
|
configPath: process.env.CLAWDBOT_CONFIG_PATH,
|
||||||
skipProviders: process.env.CLAWDBOT_SKIP_PROVIDERS,
|
skipChannels: process.env.CLAWDBOT_SKIP_CHANNELS,
|
||||||
skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER,
|
skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER,
|
||||||
skipCron: process.env.CLAWDBOT_SKIP_CRON,
|
skipCron: process.env.CLAWDBOT_SKIP_CRON,
|
||||||
skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST,
|
skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST,
|
||||||
token: process.env.CLAWDBOT_GATEWAY_TOKEN,
|
token: process.env.CLAWDBOT_GATEWAY_TOKEN,
|
||||||
password: process.env.CLAWDBOT_GATEWAY_PASSWORD,
|
password: process.env.CLAWDBOT_GATEWAY_PASSWORD,
|
||||||
};
|
};
|
||||||
|
|
||||||
process.env.CLAWDBOT_SKIP_PROVIDERS = "1";
|
process.env.CLAWDBOT_SKIP_CHANNELS = "1";
|
||||||
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1";
|
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1";
|
||||||
process.env.CLAWDBOT_SKIP_CRON = "1";
|
process.env.CLAWDBOT_SKIP_CRON = "1";
|
||||||
process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1";
|
process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1";
|
||||||
delete process.env.CLAWDBOT_GATEWAY_TOKEN;
|
delete process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||||
delete process.env.CLAWDBOT_GATEWAY_PASSWORD;
|
delete process.env.CLAWDBOT_GATEWAY_PASSWORD;
|
||||||
|
|
||||||
@@ -104,16 +104,16 @@ describe("onboard (non-interactive): remote gateway config", () => {
|
|||||||
expect(health?.ok).toBe(true);
|
expect(health?.ok).toBe(true);
|
||||||
} finally {
|
} finally {
|
||||||
await server.close({ reason: "non-interactive remote test complete" });
|
await server.close({ reason: "non-interactive remote test complete" });
|
||||||
await fs.rm(tempHome, { recursive: true, force: true });
|
await fs.rm(tempHome, { recursive: true, force: true });
|
||||||
process.env.HOME = prev.home;
|
process.env.HOME = prev.home;
|
||||||
process.env.CLAWDBOT_STATE_DIR = prev.stateDir;
|
process.env.CLAWDBOT_STATE_DIR = prev.stateDir;
|
||||||
process.env.CLAWDBOT_CONFIG_PATH = prev.configPath;
|
process.env.CLAWDBOT_CONFIG_PATH = prev.configPath;
|
||||||
process.env.CLAWDBOT_SKIP_PROVIDERS = prev.skipProviders;
|
process.env.CLAWDBOT_SKIP_CHANNELS = prev.skipChannels;
|
||||||
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = prev.skipGmail;
|
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = prev.skipGmail;
|
||||||
process.env.CLAWDBOT_SKIP_CRON = prev.skipCron;
|
process.env.CLAWDBOT_SKIP_CRON = prev.skipCron;
|
||||||
process.env.CLAWDBOT_SKIP_CANVAS_HOST = prev.skipCanvas;
|
process.env.CLAWDBOT_SKIP_CANVAS_HOST = prev.skipCanvas;
|
||||||
process.env.CLAWDBOT_GATEWAY_TOKEN = prev.token;
|
process.env.CLAWDBOT_GATEWAY_TOKEN = prev.token;
|
||||||
process.env.CLAWDBOT_GATEWAY_PASSWORD = prev.password;
|
process.env.CLAWDBOT_GATEWAY_PASSWORD = prev.password;
|
||||||
}
|
}
|
||||||
}, 60_000);
|
}, 60_000);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ async function auditGatewayRuntime(
|
|||||||
issues.push({
|
issues.push({
|
||||||
code: SERVICE_AUDIT_CODES.gatewayRuntimeBun,
|
code: SERVICE_AUDIT_CODES.gatewayRuntimeBun,
|
||||||
message:
|
message:
|
||||||
"Gateway service uses Bun; Bun is incompatible with WhatsApp + Telegram providers.",
|
"Gateway service uses Bun; Bun is incompatible with WhatsApp + Telegram channels.",
|
||||||
detail: execPath,
|
detail: execPath,
|
||||||
level: "recommended",
|
level: "recommended",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -199,21 +199,21 @@ async function connectClient(params: { url: string; token: string }) {
|
|||||||
|
|
||||||
describeLive("gateway live (cli backend)", () => {
|
describeLive("gateway live (cli backend)", () => {
|
||||||
it("runs the agent pipeline against the local CLI backend", async () => {
|
it("runs the agent pipeline against the local CLI backend", async () => {
|
||||||
const previous = {
|
const previous = {
|
||||||
configPath: process.env.CLAWDBOT_CONFIG_PATH,
|
configPath: process.env.CLAWDBOT_CONFIG_PATH,
|
||||||
token: process.env.CLAWDBOT_GATEWAY_TOKEN,
|
token: process.env.CLAWDBOT_GATEWAY_TOKEN,
|
||||||
skipProviders: process.env.CLAWDBOT_SKIP_PROVIDERS,
|
skipChannels: process.env.CLAWDBOT_SKIP_CHANNELS,
|
||||||
skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER,
|
skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER,
|
||||||
skipCron: process.env.CLAWDBOT_SKIP_CRON,
|
skipCron: process.env.CLAWDBOT_SKIP_CRON,
|
||||||
skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST,
|
skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST,
|
||||||
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
|
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
|
||||||
anthropicApiKeyOld: process.env.ANTHROPIC_API_KEY_OLD,
|
anthropicApiKeyOld: process.env.ANTHROPIC_API_KEY_OLD,
|
||||||
};
|
};
|
||||||
|
|
||||||
process.env.CLAWDBOT_SKIP_PROVIDERS = "1";
|
process.env.CLAWDBOT_SKIP_CHANNELS = "1";
|
||||||
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1";
|
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1";
|
||||||
process.env.CLAWDBOT_SKIP_CRON = "1";
|
process.env.CLAWDBOT_SKIP_CRON = "1";
|
||||||
process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1";
|
process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1";
|
||||||
delete process.env.ANTHROPIC_API_KEY;
|
delete process.env.ANTHROPIC_API_KEY;
|
||||||
delete process.env.ANTHROPIC_API_KEY_OLD;
|
delete process.env.ANTHROPIC_API_KEY_OLD;
|
||||||
|
|
||||||
@@ -444,9 +444,9 @@ describeLive("gateway live (cli backend)", () => {
|
|||||||
if (previous.token === undefined)
|
if (previous.token === undefined)
|
||||||
delete process.env.CLAWDBOT_GATEWAY_TOKEN;
|
delete process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||||
else process.env.CLAWDBOT_GATEWAY_TOKEN = previous.token;
|
else process.env.CLAWDBOT_GATEWAY_TOKEN = previous.token;
|
||||||
if (previous.skipProviders === undefined)
|
if (previous.skipChannels === undefined)
|
||||||
delete process.env.CLAWDBOT_SKIP_PROVIDERS;
|
delete process.env.CLAWDBOT_SKIP_CHANNELS;
|
||||||
else process.env.CLAWDBOT_SKIP_PROVIDERS = previous.skipProviders;
|
else process.env.CLAWDBOT_SKIP_CHANNELS = previous.skipChannels;
|
||||||
if (previous.skipGmail === undefined)
|
if (previous.skipGmail === undefined)
|
||||||
delete process.env.CLAWDBOT_SKIP_GMAIL_WATCHER;
|
delete process.env.CLAWDBOT_SKIP_GMAIL_WATCHER;
|
||||||
else process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = previous.skipGmail;
|
else process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = previous.skipGmail;
|
||||||
|
|||||||
@@ -352,7 +352,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
|
|||||||
const previous = {
|
const previous = {
|
||||||
configPath: process.env.CLAWDBOT_CONFIG_PATH,
|
configPath: process.env.CLAWDBOT_CONFIG_PATH,
|
||||||
token: process.env.CLAWDBOT_GATEWAY_TOKEN,
|
token: process.env.CLAWDBOT_GATEWAY_TOKEN,
|
||||||
skipProviders: process.env.CLAWDBOT_SKIP_PROVIDERS,
|
skipChannels: process.env.CLAWDBOT_SKIP_CHANNELS,
|
||||||
skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER,
|
skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER,
|
||||||
skipCron: process.env.CLAWDBOT_SKIP_CRON,
|
skipCron: process.env.CLAWDBOT_SKIP_CRON,
|
||||||
skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST,
|
skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST,
|
||||||
@@ -363,7 +363,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
|
|||||||
let tempAgentDir: string | undefined;
|
let tempAgentDir: string | undefined;
|
||||||
let tempStateDir: string | undefined;
|
let tempStateDir: string | undefined;
|
||||||
|
|
||||||
process.env.CLAWDBOT_SKIP_PROVIDERS = "1";
|
process.env.CLAWDBOT_SKIP_CHANNELS = "1";
|
||||||
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1";
|
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1";
|
||||||
process.env.CLAWDBOT_SKIP_CRON = "1";
|
process.env.CLAWDBOT_SKIP_CRON = "1";
|
||||||
process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1";
|
process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1";
|
||||||
@@ -776,7 +776,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
|
|||||||
|
|
||||||
process.env.CLAWDBOT_CONFIG_PATH = previous.configPath;
|
process.env.CLAWDBOT_CONFIG_PATH = previous.configPath;
|
||||||
process.env.CLAWDBOT_GATEWAY_TOKEN = previous.token;
|
process.env.CLAWDBOT_GATEWAY_TOKEN = previous.token;
|
||||||
process.env.CLAWDBOT_SKIP_PROVIDERS = previous.skipProviders;
|
process.env.CLAWDBOT_SKIP_CHANNELS = previous.skipChannels;
|
||||||
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = previous.skipGmail;
|
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = previous.skipGmail;
|
||||||
process.env.CLAWDBOT_SKIP_CRON = previous.skipCron;
|
process.env.CLAWDBOT_SKIP_CRON = previous.skipCron;
|
||||||
process.env.CLAWDBOT_SKIP_CANVAS_HOST = previous.skipCanvas;
|
process.env.CLAWDBOT_SKIP_CANVAS_HOST = previous.skipCanvas;
|
||||||
@@ -895,13 +895,13 @@ describeLive("gateway live (dev agent, profile keys)", () => {
|
|||||||
const previous = {
|
const previous = {
|
||||||
configPath: process.env.CLAWDBOT_CONFIG_PATH,
|
configPath: process.env.CLAWDBOT_CONFIG_PATH,
|
||||||
token: process.env.CLAWDBOT_GATEWAY_TOKEN,
|
token: process.env.CLAWDBOT_GATEWAY_TOKEN,
|
||||||
skipProviders: process.env.CLAWDBOT_SKIP_PROVIDERS,
|
skipChannels: process.env.CLAWDBOT_SKIP_CHANNELS,
|
||||||
skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER,
|
skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER,
|
||||||
skipCron: process.env.CLAWDBOT_SKIP_CRON,
|
skipCron: process.env.CLAWDBOT_SKIP_CRON,
|
||||||
skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST,
|
skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST,
|
||||||
};
|
};
|
||||||
|
|
||||||
process.env.CLAWDBOT_SKIP_PROVIDERS = "1";
|
process.env.CLAWDBOT_SKIP_CHANNELS = "1";
|
||||||
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1";
|
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1";
|
||||||
process.env.CLAWDBOT_SKIP_CRON = "1";
|
process.env.CLAWDBOT_SKIP_CRON = "1";
|
||||||
process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1";
|
process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1";
|
||||||
@@ -1035,7 +1035,7 @@ describeLive("gateway live (dev agent, profile keys)", () => {
|
|||||||
|
|
||||||
process.env.CLAWDBOT_CONFIG_PATH = previous.configPath;
|
process.env.CLAWDBOT_CONFIG_PATH = previous.configPath;
|
||||||
process.env.CLAWDBOT_GATEWAY_TOKEN = previous.token;
|
process.env.CLAWDBOT_GATEWAY_TOKEN = previous.token;
|
||||||
process.env.CLAWDBOT_SKIP_PROVIDERS = previous.skipProviders;
|
process.env.CLAWDBOT_SKIP_CHANNELS = previous.skipChannels;
|
||||||
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = previous.skipGmail;
|
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = previous.skipGmail;
|
||||||
process.env.CLAWDBOT_SKIP_CRON = previous.skipCron;
|
process.env.CLAWDBOT_SKIP_CRON = previous.skipCron;
|
||||||
process.env.CLAWDBOT_SKIP_CANVAS_HOST = previous.skipCanvas;
|
process.env.CLAWDBOT_SKIP_CANVAS_HOST = previous.skipCanvas;
|
||||||
|
|||||||
@@ -271,15 +271,15 @@ async function connectClient(params: { url: string; token: string }) {
|
|||||||
|
|
||||||
describe("gateway (mock openai): tool calling", () => {
|
describe("gateway (mock openai): tool calling", () => {
|
||||||
it("runs a Read tool call end-to-end via gateway agent loop", async () => {
|
it("runs a Read tool call end-to-end via gateway agent loop", async () => {
|
||||||
const prev = {
|
const prev = {
|
||||||
home: process.env.HOME,
|
home: process.env.HOME,
|
||||||
configPath: process.env.CLAWDBOT_CONFIG_PATH,
|
configPath: process.env.CLAWDBOT_CONFIG_PATH,
|
||||||
token: process.env.CLAWDBOT_GATEWAY_TOKEN,
|
token: process.env.CLAWDBOT_GATEWAY_TOKEN,
|
||||||
skipProviders: process.env.CLAWDBOT_SKIP_PROVIDERS,
|
skipChannels: process.env.CLAWDBOT_SKIP_CHANNELS,
|
||||||
skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER,
|
skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER,
|
||||||
skipCron: process.env.CLAWDBOT_SKIP_CRON,
|
skipCron: process.env.CLAWDBOT_SKIP_CRON,
|
||||||
skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST,
|
skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST,
|
||||||
};
|
};
|
||||||
|
|
||||||
const originalFetch = globalThis.fetch;
|
const originalFetch = globalThis.fetch;
|
||||||
const openaiResponsesUrl = "https://api.openai.com/v1/responses";
|
const openaiResponsesUrl = "https://api.openai.com/v1/responses";
|
||||||
@@ -321,14 +321,14 @@ describe("gateway (mock openai): tool calling", () => {
|
|||||||
// TypeScript: Bun's fetch typing includes extra properties; keep this test portable.
|
// TypeScript: Bun's fetch typing includes extra properties; keep this test portable.
|
||||||
(globalThis as unknown as { fetch: unknown }).fetch = fetchImpl;
|
(globalThis as unknown as { fetch: unknown }).fetch = fetchImpl;
|
||||||
|
|
||||||
const tempHome = await fs.mkdtemp(
|
const tempHome = await fs.mkdtemp(
|
||||||
path.join(os.tmpdir(), "clawdbot-gw-mock-home-"),
|
path.join(os.tmpdir(), "clawdbot-gw-mock-home-"),
|
||||||
);
|
);
|
||||||
process.env.HOME = tempHome;
|
process.env.HOME = tempHome;
|
||||||
process.env.CLAWDBOT_SKIP_PROVIDERS = "1";
|
process.env.CLAWDBOT_SKIP_CHANNELS = "1";
|
||||||
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1";
|
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1";
|
||||||
process.env.CLAWDBOT_SKIP_CRON = "1";
|
process.env.CLAWDBOT_SKIP_CRON = "1";
|
||||||
process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1";
|
process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1";
|
||||||
|
|
||||||
const token = `test-${randomUUID()}`;
|
const token = `test-${randomUUID()}`;
|
||||||
process.env.CLAWDBOT_GATEWAY_TOKEN = token;
|
process.env.CLAWDBOT_GATEWAY_TOKEN = token;
|
||||||
@@ -424,13 +424,13 @@ describe("gateway (mock openai): tool calling", () => {
|
|||||||
await server.close({ reason: "mock openai test complete" });
|
await server.close({ reason: "mock openai test complete" });
|
||||||
await fs.rm(tempHome, { recursive: true, force: true });
|
await fs.rm(tempHome, { recursive: true, force: true });
|
||||||
(globalThis as unknown as { fetch: unknown }).fetch = originalFetch;
|
(globalThis as unknown as { fetch: unknown }).fetch = originalFetch;
|
||||||
process.env.HOME = prev.home;
|
process.env.HOME = prev.home;
|
||||||
process.env.CLAWDBOT_CONFIG_PATH = prev.configPath;
|
process.env.CLAWDBOT_CONFIG_PATH = prev.configPath;
|
||||||
process.env.CLAWDBOT_GATEWAY_TOKEN = prev.token;
|
process.env.CLAWDBOT_GATEWAY_TOKEN = prev.token;
|
||||||
process.env.CLAWDBOT_SKIP_PROVIDERS = prev.skipProviders;
|
process.env.CLAWDBOT_SKIP_CHANNELS = prev.skipChannels;
|
||||||
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = prev.skipGmail;
|
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = prev.skipGmail;
|
||||||
process.env.CLAWDBOT_SKIP_CRON = prev.skipCron;
|
process.env.CLAWDBOT_SKIP_CRON = prev.skipCron;
|
||||||
process.env.CLAWDBOT_SKIP_CANVAS_HOST = prev.skipCanvas;
|
process.env.CLAWDBOT_SKIP_CANVAS_HOST = prev.skipCanvas;
|
||||||
}
|
}
|
||||||
}, 30_000);
|
}, 30_000);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -172,21 +172,21 @@ type WizardNextPayload = {
|
|||||||
|
|
||||||
describe("gateway wizard (e2e)", () => {
|
describe("gateway wizard (e2e)", () => {
|
||||||
it("runs wizard over ws and writes auth token config", async () => {
|
it("runs wizard over ws and writes auth token config", async () => {
|
||||||
const prev = {
|
const prev = {
|
||||||
home: process.env.HOME,
|
home: process.env.HOME,
|
||||||
stateDir: process.env.CLAWDBOT_STATE_DIR,
|
stateDir: process.env.CLAWDBOT_STATE_DIR,
|
||||||
configPath: process.env.CLAWDBOT_CONFIG_PATH,
|
configPath: process.env.CLAWDBOT_CONFIG_PATH,
|
||||||
token: process.env.CLAWDBOT_GATEWAY_TOKEN,
|
token: process.env.CLAWDBOT_GATEWAY_TOKEN,
|
||||||
skipProviders: process.env.CLAWDBOT_SKIP_PROVIDERS,
|
skipChannels: process.env.CLAWDBOT_SKIP_CHANNELS,
|
||||||
skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER,
|
skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER,
|
||||||
skipCron: process.env.CLAWDBOT_SKIP_CRON,
|
skipCron: process.env.CLAWDBOT_SKIP_CRON,
|
||||||
skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST,
|
skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST,
|
||||||
};
|
};
|
||||||
|
|
||||||
process.env.CLAWDBOT_SKIP_PROVIDERS = "1";
|
process.env.CLAWDBOT_SKIP_CHANNELS = "1";
|
||||||
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1";
|
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1";
|
||||||
process.env.CLAWDBOT_SKIP_CRON = "1";
|
process.env.CLAWDBOT_SKIP_CRON = "1";
|
||||||
process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1";
|
process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1";
|
||||||
delete process.env.CLAWDBOT_GATEWAY_TOKEN;
|
delete process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||||
|
|
||||||
const tempHome = await fs.mkdtemp(
|
const tempHome = await fs.mkdtemp(
|
||||||
@@ -282,7 +282,7 @@ describe("gateway wizard (e2e)", () => {
|
|||||||
process.env.CLAWDBOT_STATE_DIR = prev.stateDir;
|
process.env.CLAWDBOT_STATE_DIR = prev.stateDir;
|
||||||
process.env.CLAWDBOT_CONFIG_PATH = prev.configPath;
|
process.env.CLAWDBOT_CONFIG_PATH = prev.configPath;
|
||||||
process.env.CLAWDBOT_GATEWAY_TOKEN = prev.token;
|
process.env.CLAWDBOT_GATEWAY_TOKEN = prev.token;
|
||||||
process.env.CLAWDBOT_SKIP_PROVIDERS = prev.skipProviders;
|
process.env.CLAWDBOT_SKIP_CHANNELS = prev.skipChannels;
|
||||||
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = prev.skipGmail;
|
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = prev.skipGmail;
|
||||||
process.env.CLAWDBOT_SKIP_CRON = prev.skipCron;
|
process.env.CLAWDBOT_SKIP_CRON = prev.skipCron;
|
||||||
process.env.CLAWDBOT_SKIP_CANVAS_HOST = prev.skipCanvas;
|
process.env.CLAWDBOT_SKIP_CANVAS_HOST = prev.skipCanvas;
|
||||||
|
|||||||
@@ -166,21 +166,21 @@ vi.mock("./config-reload.js", () => ({
|
|||||||
installGatewayTestHooks();
|
installGatewayTestHooks();
|
||||||
|
|
||||||
describe("gateway hot reload", () => {
|
describe("gateway hot reload", () => {
|
||||||
let prevSkipProviders: string | undefined;
|
let prevSkipChannels: string | undefined;
|
||||||
let prevSkipGmail: string | undefined;
|
let prevSkipGmail: string | undefined;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
prevSkipProviders = process.env.CLAWDBOT_SKIP_PROVIDERS;
|
prevSkipChannels = process.env.CLAWDBOT_SKIP_CHANNELS;
|
||||||
prevSkipGmail = process.env.CLAWDBOT_SKIP_GMAIL_WATCHER;
|
prevSkipGmail = process.env.CLAWDBOT_SKIP_GMAIL_WATCHER;
|
||||||
process.env.CLAWDBOT_SKIP_PROVIDERS = "0";
|
process.env.CLAWDBOT_SKIP_CHANNELS = "0";
|
||||||
delete process.env.CLAWDBOT_SKIP_GMAIL_WATCHER;
|
delete process.env.CLAWDBOT_SKIP_GMAIL_WATCHER;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
if (prevSkipProviders === undefined) {
|
if (prevSkipChannels === undefined) {
|
||||||
delete process.env.CLAWDBOT_SKIP_PROVIDERS;
|
delete process.env.CLAWDBOT_SKIP_CHANNELS;
|
||||||
} else {
|
} else {
|
||||||
process.env.CLAWDBOT_SKIP_PROVIDERS = prevSkipProviders;
|
process.env.CLAWDBOT_SKIP_CHANNELS = prevSkipChannels;
|
||||||
}
|
}
|
||||||
if (prevSkipGmail === undefined) {
|
if (prevSkipGmail === undefined) {
|
||||||
delete process.env.CLAWDBOT_SKIP_GMAIL_WATCHER;
|
delete process.env.CLAWDBOT_SKIP_GMAIL_WATCHER;
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ vi.mock("../commands/agent.js", () => ({
|
|||||||
agentCommand,
|
agentCommand,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
process.env.CLAWDBOT_SKIP_PROVIDERS = "1";
|
process.env.CLAWDBOT_SKIP_CHANNELS = "1";
|
||||||
|
|
||||||
let previousHome: string | undefined;
|
let previousHome: string | undefined;
|
||||||
let tempHome: string | undefined;
|
let tempHome: string | undefined;
|
||||||
|
|||||||
@@ -430,9 +430,9 @@ const SUBSYSTEM_COLOR_OVERRIDES: Record<
|
|||||||
> = {
|
> = {
|
||||||
"gmail-watcher": "blue",
|
"gmail-watcher": "blue",
|
||||||
};
|
};
|
||||||
const SUBSYSTEM_PREFIXES_TO_DROP = ["gateway", "providers"] as const;
|
const SUBSYSTEM_PREFIXES_TO_DROP = ["gateway", "channels", "providers"] as const;
|
||||||
const SUBSYSTEM_MAX_SEGMENTS = 2;
|
const SUBSYSTEM_MAX_SEGMENTS = 2;
|
||||||
const PROVIDER_SUBSYSTEM_PREFIXES = new Set<string>(CHAT_CHANNEL_ORDER);
|
const CHANNEL_SUBSYSTEM_PREFIXES = new Set<string>(CHAT_CHANNEL_ORDER);
|
||||||
|
|
||||||
function pickSubsystemColor(
|
function pickSubsystemColor(
|
||||||
color: ChalkInstance,
|
color: ChalkInstance,
|
||||||
@@ -461,7 +461,7 @@ function formatSubsystemForConsole(subsystem: string): string {
|
|||||||
parts.shift();
|
parts.shift();
|
||||||
}
|
}
|
||||||
if (parts.length === 0) return original;
|
if (parts.length === 0) return original;
|
||||||
if (PROVIDER_SUBSYSTEM_PREFIXES.has(parts[0])) {
|
if (CHANNEL_SUBSYSTEM_PREFIXES.has(parts[0])) {
|
||||||
return parts[0];
|
return parts[0];
|
||||||
}
|
}
|
||||||
if (parts.length > SUBSYSTEM_MAX_SEGMENTS) {
|
if (parts.length > SUBSYSTEM_MAX_SEGMENTS) {
|
||||||
|
|||||||
@@ -249,14 +249,14 @@ export async function runOnboardingWizard(
|
|||||||
`Tailscale exposure: ${formatTailscale(
|
`Tailscale exposure: ${formatTailscale(
|
||||||
quickstartGateway.tailscaleMode,
|
quickstartGateway.tailscaleMode,
|
||||||
)}`,
|
)}`,
|
||||||
"Direct to chat providers.",
|
"Direct to chat channels.",
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
`Gateway port: ${DEFAULT_GATEWAY_PORT}`,
|
`Gateway port: ${DEFAULT_GATEWAY_PORT}`,
|
||||||
"Gateway bind: Loopback (127.0.0.1)",
|
"Gateway bind: Loopback (127.0.0.1)",
|
||||||
"Gateway auth: Token (default)",
|
"Gateway auth: Token (default)",
|
||||||
"Tailscale exposure: Off",
|
"Tailscale exposure: Off",
|
||||||
"Direct to chat providers.",
|
"Direct to chat channels.",
|
||||||
];
|
];
|
||||||
await prompter.note(quickstartLines.join("\n"), "QuickStart");
|
await prompter.note(quickstartLines.join("\n"), "QuickStart");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ const spawnGatewayInstance = async (name: string): Promise<GatewayInstance> => {
|
|||||||
CLAWDBOT_STATE_DIR: stateDir,
|
CLAWDBOT_STATE_DIR: stateDir,
|
||||||
CLAWDBOT_GATEWAY_TOKEN: "",
|
CLAWDBOT_GATEWAY_TOKEN: "",
|
||||||
CLAWDBOT_GATEWAY_PASSWORD: "",
|
CLAWDBOT_GATEWAY_PASSWORD: "",
|
||||||
CLAWDBOT_SKIP_PROVIDERS: "1",
|
CLAWDBOT_SKIP_CHANNELS: "1",
|
||||||
CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER: "1",
|
CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER: "1",
|
||||||
CLAWDBOT_SKIP_CANVAS_HOST: "1",
|
CLAWDBOT_SKIP_CANVAS_HOST: "1",
|
||||||
CLAWDBOT_ENABLE_BRIDGE_IN_TESTS: "1",
|
CLAWDBOT_ENABLE_BRIDGE_IN_TESTS: "1",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import type {
|
|||||||
LogEntry,
|
LogEntry,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
PresenceEntry,
|
PresenceEntry,
|
||||||
ProvidersStatusSnapshot,
|
ChannelsStatusSnapshot,
|
||||||
SessionsListResult,
|
SessionsListResult,
|
||||||
SkillStatusReport,
|
SkillStatusReport,
|
||||||
StatusSummary,
|
StatusSummary,
|
||||||
@@ -47,7 +47,7 @@ import { renderOverview } from "./views/overview";
|
|||||||
import { renderSessions } from "./views/sessions";
|
import { renderSessions } from "./views/sessions";
|
||||||
import { renderSkills } from "./views/skills";
|
import { renderSkills } from "./views/skills";
|
||||||
import {
|
import {
|
||||||
loadProviders,
|
loadChannels,
|
||||||
updateDiscordForm,
|
updateDiscordForm,
|
||||||
updateIMessageForm,
|
updateIMessageForm,
|
||||||
updateSlackForm,
|
updateSlackForm,
|
||||||
@@ -119,10 +119,10 @@ export type AppViewState = {
|
|||||||
configUiHints: Record<string, unknown>;
|
configUiHints: Record<string, unknown>;
|
||||||
configForm: Record<string, unknown> | null;
|
configForm: Record<string, unknown> | null;
|
||||||
configFormMode: "form" | "raw";
|
configFormMode: "form" | "raw";
|
||||||
providersLoading: boolean;
|
channelsLoading: boolean;
|
||||||
providersSnapshot: ProvidersStatusSnapshot | null;
|
channelsSnapshot: ChannelsStatusSnapshot | null;
|
||||||
providersError: string | null;
|
channelsError: string | null;
|
||||||
providersLastSuccess: number | null;
|
channelsLastSuccess: number | null;
|
||||||
whatsappLoginMessage: string | null;
|
whatsappLoginMessage: string | null;
|
||||||
whatsappLoginQrDataUrl: string | null;
|
whatsappLoginQrDataUrl: string | null;
|
||||||
whatsappLoginConnected: boolean | null;
|
whatsappLoginConnected: boolean | null;
|
||||||
@@ -299,7 +299,7 @@ export function renderApp(state: AppViewState) {
|
|||||||
sessionsCount,
|
sessionsCount,
|
||||||
cronEnabled: state.cronStatus?.enabled ?? null,
|
cronEnabled: state.cronStatus?.enabled ?? null,
|
||||||
cronNext,
|
cronNext,
|
||||||
lastProvidersRefresh: state.providersLastSuccess,
|
lastChannelsRefresh: state.channelsLastSuccess,
|
||||||
onSettingsChange: (next) => state.applySettings(next),
|
onSettingsChange: (next) => state.applySettings(next),
|
||||||
onPasswordChange: (next) => (state.password = next),
|
onPasswordChange: (next) => (state.password = next),
|
||||||
onSessionKeyChange: (next) => {
|
onSessionKeyChange: (next) => {
|
||||||
@@ -320,10 +320,10 @@ export function renderApp(state: AppViewState) {
|
|||||||
${state.tab === "connections"
|
${state.tab === "connections"
|
||||||
? renderConnections({
|
? renderConnections({
|
||||||
connected: state.connected,
|
connected: state.connected,
|
||||||
loading: state.providersLoading,
|
loading: state.channelsLoading,
|
||||||
snapshot: state.providersSnapshot,
|
snapshot: state.channelsSnapshot,
|
||||||
lastError: state.providersError,
|
lastError: state.channelsError,
|
||||||
lastSuccessAt: state.providersLastSuccess,
|
lastSuccessAt: state.channelsLastSuccess,
|
||||||
whatsappMessage: state.whatsappLoginMessage,
|
whatsappMessage: state.whatsappLoginMessage,
|
||||||
whatsappQrDataUrl: state.whatsappLoginQrDataUrl,
|
whatsappQrDataUrl: state.whatsappLoginQrDataUrl,
|
||||||
whatsappConnected: state.whatsappLoginConnected,
|
whatsappConnected: state.whatsappLoginConnected,
|
||||||
@@ -347,7 +347,7 @@ export function renderApp(state: AppViewState) {
|
|||||||
imessageForm: state.imessageForm,
|
imessageForm: state.imessageForm,
|
||||||
imessageSaving: state.imessageSaving,
|
imessageSaving: state.imessageSaving,
|
||||||
imessageStatus: state.imessageConfigStatus,
|
imessageStatus: state.imessageConfigStatus,
|
||||||
onRefresh: (probe) => loadProviders(state, probe),
|
onRefresh: (probe) => loadChannels(state, probe),
|
||||||
onWhatsAppStart: (force) => state.handleWhatsAppStart(force),
|
onWhatsAppStart: (force) => state.handleWhatsAppStart(force),
|
||||||
onWhatsAppWait: () => state.handleWhatsAppWait(),
|
onWhatsAppWait: () => state.handleWhatsAppWait(),
|
||||||
onWhatsAppLogout: () => state.handleWhatsAppLogout(),
|
onWhatsAppLogout: () => state.handleWhatsAppLogout(),
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import type {
|
|||||||
LogEntry,
|
LogEntry,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
PresenceEntry,
|
PresenceEntry,
|
||||||
ProvidersStatusSnapshot,
|
ChannelsStatusSnapshot,
|
||||||
SessionsListResult,
|
SessionsListResult,
|
||||||
SkillStatusReport,
|
SkillStatusReport,
|
||||||
StatusSummary,
|
StatusSummary,
|
||||||
@@ -63,7 +63,7 @@ import {
|
|||||||
updateConfigFormValue,
|
updateConfigFormValue,
|
||||||
} from "./controllers/config";
|
} from "./controllers/config";
|
||||||
import {
|
import {
|
||||||
loadProviders,
|
loadChannels,
|
||||||
logoutWhatsApp,
|
logoutWhatsApp,
|
||||||
saveDiscordConfig,
|
saveDiscordConfig,
|
||||||
saveIMessageConfig,
|
saveIMessageConfig,
|
||||||
@@ -188,7 +188,7 @@ const DEFAULT_CRON_FORM: CronFormState = {
|
|||||||
payloadKind: "systemEvent",
|
payloadKind: "systemEvent",
|
||||||
payloadText: "",
|
payloadText: "",
|
||||||
deliver: false,
|
deliver: false,
|
||||||
provider: "last",
|
channel: "last",
|
||||||
to: "",
|
to: "",
|
||||||
timeoutSeconds: "",
|
timeoutSeconds: "",
|
||||||
postToMainPrefix: "",
|
postToMainPrefix: "",
|
||||||
@@ -247,10 +247,10 @@ export class ClawdbotApp extends LitElement {
|
|||||||
@state() configFormDirty = false;
|
@state() configFormDirty = false;
|
||||||
@state() configFormMode: "form" | "raw" = "form";
|
@state() configFormMode: "form" | "raw" = "form";
|
||||||
|
|
||||||
@state() providersLoading = false;
|
@state() channelsLoading = false;
|
||||||
@state() providersSnapshot: ProvidersStatusSnapshot | null = null;
|
@state() channelsSnapshot: ChannelsStatusSnapshot | null = null;
|
||||||
@state() providersError: string | null = null;
|
@state() channelsError: string | null = null;
|
||||||
@state() providersLastSuccess: number | null = null;
|
@state() channelsLastSuccess: number | null = null;
|
||||||
@state() whatsappLoginMessage: string | null = null;
|
@state() whatsappLoginMessage: string | null = null;
|
||||||
@state() whatsappLoginQrDataUrl: string | null = null;
|
@state() whatsappLoginQrDataUrl: string | null = null;
|
||||||
@state() whatsappLoginConnected: boolean | null = null;
|
@state() whatsappLoginConnected: boolean | null = null;
|
||||||
@@ -1026,7 +1026,7 @@ export class ClawdbotApp extends LitElement {
|
|||||||
|
|
||||||
async loadOverview() {
|
async loadOverview() {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
loadProviders(this, false),
|
loadChannels(this, false),
|
||||||
loadPresence(this),
|
loadPresence(this),
|
||||||
loadSessions(this),
|
loadSessions(this),
|
||||||
loadCronStatus(this),
|
loadCronStatus(this),
|
||||||
@@ -1035,7 +1035,7 @@ export class ClawdbotApp extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async loadConnections() {
|
private async loadConnections() {
|
||||||
await Promise.all([loadProviders(this, true), loadConfig(this)]);
|
await Promise.all([loadChannels(this, true), loadConfig(this)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadCron() {
|
async loadCron() {
|
||||||
@@ -1147,47 +1147,47 @@ export class ClawdbotApp extends LitElement {
|
|||||||
|
|
||||||
async handleWhatsAppStart(force: boolean) {
|
async handleWhatsAppStart(force: boolean) {
|
||||||
await startWhatsAppLogin(this, force);
|
await startWhatsAppLogin(this, force);
|
||||||
await loadProviders(this, true);
|
await loadChannels(this, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleWhatsAppWait() {
|
async handleWhatsAppWait() {
|
||||||
await waitWhatsAppLogin(this);
|
await waitWhatsAppLogin(this);
|
||||||
await loadProviders(this, true);
|
await loadChannels(this, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleWhatsAppLogout() {
|
async handleWhatsAppLogout() {
|
||||||
await logoutWhatsApp(this);
|
await logoutWhatsApp(this);
|
||||||
await loadProviders(this, true);
|
await loadChannels(this, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleTelegramSave() {
|
async handleTelegramSave() {
|
||||||
await saveTelegramConfig(this);
|
await saveTelegramConfig(this);
|
||||||
await loadConfig(this);
|
await loadConfig(this);
|
||||||
await loadProviders(this, true);
|
await loadChannels(this, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleDiscordSave() {
|
async handleDiscordSave() {
|
||||||
await saveDiscordConfig(this);
|
await saveDiscordConfig(this);
|
||||||
await loadConfig(this);
|
await loadConfig(this);
|
||||||
await loadProviders(this, true);
|
await loadChannels(this, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleSlackSave() {
|
async handleSlackSave() {
|
||||||
await saveSlackConfig(this);
|
await saveSlackConfig(this);
|
||||||
await loadConfig(this);
|
await loadConfig(this);
|
||||||
await loadProviders(this, true);
|
await loadChannels(this, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleSignalSave() {
|
async handleSignalSave() {
|
||||||
await saveSignalConfig(this);
|
await saveSignalConfig(this);
|
||||||
await loadConfig(this);
|
await loadConfig(this);
|
||||||
await loadProviders(this, true);
|
await loadChannels(this, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleIMessageSave() {
|
async handleIMessageSave() {
|
||||||
await saveIMessageConfig(this);
|
await saveIMessageConfig(this);
|
||||||
await loadConfig(this);
|
await loadConfig(this);
|
||||||
await loadProviders(this, true);
|
await loadChannels(this, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sidebar handlers for tool output viewing
|
// Sidebar handlers for tool output viewing
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { GatewayBrowserClient } from "../gateway";
|
import type { GatewayBrowserClient } from "../gateway";
|
||||||
import { parseList } from "../format";
|
import { parseList } from "../format";
|
||||||
import type { ConfigSnapshot, ProvidersStatusSnapshot } from "../types";
|
import type { ChannelsStatusSnapshot, ConfigSnapshot } from "../types";
|
||||||
import {
|
import {
|
||||||
defaultDiscordActions,
|
defaultDiscordActions,
|
||||||
defaultSlackActions,
|
defaultSlackActions,
|
||||||
@@ -18,10 +18,10 @@ import {
|
|||||||
export type ConnectionsState = {
|
export type ConnectionsState = {
|
||||||
client: GatewayBrowserClient | null;
|
client: GatewayBrowserClient | null;
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
providersLoading: boolean;
|
channelsLoading: boolean;
|
||||||
providersSnapshot: ProvidersStatusSnapshot | null;
|
channelsSnapshot: ChannelsStatusSnapshot | null;
|
||||||
providersError: string | null;
|
channelsError: string | null;
|
||||||
providersLastSuccess: number | null;
|
channelsLastSuccess: number | null;
|
||||||
whatsappLoginMessage: string | null;
|
whatsappLoginMessage: string | null;
|
||||||
whatsappLoginQrDataUrl: string | null;
|
whatsappLoginQrDataUrl: string | null;
|
||||||
whatsappLoginConnected: boolean | null;
|
whatsappLoginConnected: boolean | null;
|
||||||
@@ -48,22 +48,22 @@ export type ConnectionsState = {
|
|||||||
configSnapshot: ConfigSnapshot | null;
|
configSnapshot: ConfigSnapshot | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function loadProviders(state: ConnectionsState, probe: boolean) {
|
export async function loadChannels(state: ConnectionsState, probe: boolean) {
|
||||||
if (!state.client || !state.connected) return;
|
if (!state.client || !state.connected) return;
|
||||||
if (state.providersLoading) return;
|
if (state.channelsLoading) return;
|
||||||
state.providersLoading = true;
|
state.channelsLoading = true;
|
||||||
state.providersError = null;
|
state.channelsError = null;
|
||||||
try {
|
try {
|
||||||
const res = (await state.client.request("providers.status", {
|
const res = (await state.client.request("channels.status", {
|
||||||
probe,
|
probe,
|
||||||
timeoutMs: 8000,
|
timeoutMs: 8000,
|
||||||
})) as ProvidersStatusSnapshot;
|
})) as ChannelsStatusSnapshot;
|
||||||
state.providersSnapshot = res;
|
state.channelsSnapshot = res;
|
||||||
state.providersLastSuccess = Date.now();
|
state.channelsLastSuccess = Date.now();
|
||||||
const providers = res.providers as Record<string, unknown>;
|
const channels = res.channels as Record<string, unknown>;
|
||||||
const telegram = providers.telegram as { tokenSource?: string | null };
|
const telegram = channels.telegram as { tokenSource?: string | null };
|
||||||
const discord = providers.discord as { tokenSource?: string | null } | null;
|
const discord = channels.discord as { tokenSource?: string | null } | null;
|
||||||
const slack = providers.slack as
|
const slack = channels.slack as
|
||||||
| { botTokenSource?: string | null; appTokenSource?: string | null }
|
| { botTokenSource?: string | null; appTokenSource?: string | null }
|
||||||
| null;
|
| null;
|
||||||
state.telegramTokenLocked = telegram?.tokenSource === "env";
|
state.telegramTokenLocked = telegram?.tokenSource === "env";
|
||||||
@@ -71,9 +71,9 @@ export async function loadProviders(state: ConnectionsState, probe: boolean) {
|
|||||||
state.slackTokenLocked = slack?.botTokenSource === "env";
|
state.slackTokenLocked = slack?.botTokenSource === "env";
|
||||||
state.slackAppTokenLocked = slack?.appTokenSource === "env";
|
state.slackAppTokenLocked = slack?.appTokenSource === "env";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
state.providersError = String(err);
|
state.channelsError = String(err);
|
||||||
} finally {
|
} finally {
|
||||||
state.providersLoading = false;
|
state.channelsLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ export async function logoutWhatsApp(state: ConnectionsState) {
|
|||||||
if (!state.client || !state.connected || state.whatsappBusy) return;
|
if (!state.client || !state.connected || state.whatsappBusy) return;
|
||||||
state.whatsappBusy = true;
|
state.whatsappBusy = true;
|
||||||
try {
|
try {
|
||||||
await state.client.request("providers.logout", { provider: "whatsapp" });
|
await state.client.request("channels.logout", { channel: "whatsapp" });
|
||||||
state.whatsappLoginMessage = "Logged out.";
|
state.whatsappLoginMessage = "Logged out.";
|
||||||
state.whatsappLoginQrDataUrl = null;
|
state.whatsappLoginQrDataUrl = null;
|
||||||
state.whatsappLoginConnected = null;
|
state.whatsappLoginConnected = null;
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export function buildCronPayload(form: CronFormState) {
|
|||||||
kind: "agentTurn";
|
kind: "agentTurn";
|
||||||
message: string;
|
message: string;
|
||||||
deliver?: boolean;
|
deliver?: boolean;
|
||||||
provider?:
|
channel?:
|
||||||
| "last"
|
| "last"
|
||||||
| "whatsapp"
|
| "whatsapp"
|
||||||
| "telegram"
|
| "telegram"
|
||||||
@@ -85,7 +85,7 @@ export function buildCronPayload(form: CronFormState) {
|
|||||||
timeoutSeconds?: number;
|
timeoutSeconds?: number;
|
||||||
} = { kind: "agentTurn", message };
|
} = { kind: "agentTurn", message };
|
||||||
if (form.deliver) payload.deliver = true;
|
if (form.deliver) payload.deliver = true;
|
||||||
if (form.provider) payload.provider = form.provider;
|
if (form.channel) payload.channel = form.channel;
|
||||||
if (form.to.trim()) payload.to = form.to.trim();
|
if (form.to.trim()) payload.to = form.to.trim();
|
||||||
const timeoutSeconds = toNumber(form.timeoutSeconds, 0);
|
const timeoutSeconds = toNumber(form.timeoutSeconds, 0);
|
||||||
if (timeoutSeconds > 0) payload.timeoutSeconds = timeoutSeconds;
|
if (timeoutSeconds > 0) payload.timeoutSeconds = timeoutSeconds;
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ export function subtitleForTab(tab: Tab) {
|
|||||||
case "overview":
|
case "overview":
|
||||||
return "Gateway status, entry points, and a fast health read.";
|
return "Gateway status, entry points, and a fast health read.";
|
||||||
case "connections":
|
case "connections":
|
||||||
return "Link providers and keep transport settings in sync.";
|
return "Link channels and keep transport settings in sync.";
|
||||||
case "instances":
|
case "instances":
|
||||||
return "Presence beacons from connected clients and nodes.";
|
return "Presence beacons from connected clients and nodes.";
|
||||||
case "sessions":
|
case "sessions":
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
export type ProvidersStatusSnapshot = {
|
export type ChannelsStatusSnapshot = {
|
||||||
ts: number;
|
ts: number;
|
||||||
providerOrder: string[];
|
channelOrder: string[];
|
||||||
providerLabels: Record<string, string>;
|
channelLabels: Record<string, string>;
|
||||||
providers: Record<string, unknown>;
|
channels: Record<string, unknown>;
|
||||||
providerAccounts: Record<string, ProviderAccountSnapshot[]>;
|
channelAccounts: Record<string, ChannelAccountSnapshot[]>;
|
||||||
providerDefaultAccountId: Record<string, string>;
|
channelDefaultAccountId: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProviderAccountSnapshot = {
|
export type ChannelAccountSnapshot = {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
enabled?: boolean | null;
|
enabled?: boolean | null;
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ export type CronFormState = {
|
|||||||
payloadKind: "systemEvent" | "agentTurn";
|
payloadKind: "systemEvent" | "agentTurn";
|
||||||
payloadText: string;
|
payloadText: string;
|
||||||
deliver: boolean;
|
deliver: boolean;
|
||||||
provider:
|
channel:
|
||||||
| "last"
|
| "last"
|
||||||
| "whatsapp"
|
| "whatsapp"
|
||||||
| "telegram"
|
| "telegram"
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { html, nothing } from "lit";
|
|||||||
|
|
||||||
import { formatAgo } from "../format";
|
import { formatAgo } from "../format";
|
||||||
import type {
|
import type {
|
||||||
|
ChannelAccountSnapshot,
|
||||||
|
ChannelsStatusSnapshot,
|
||||||
DiscordStatus,
|
DiscordStatus,
|
||||||
IMessageStatus,
|
IMessageStatus,
|
||||||
ProviderAccountSnapshot,
|
|
||||||
ProvidersStatusSnapshot,
|
|
||||||
SignalStatus,
|
SignalStatus,
|
||||||
SlackStatus,
|
SlackStatus,
|
||||||
TelegramStatus,
|
TelegramStatus,
|
||||||
@@ -50,7 +50,7 @@ const slackActionOptions = [
|
|||||||
export type ConnectionsProps = {
|
export type ConnectionsProps = {
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
snapshot: ProvidersStatusSnapshot | null;
|
snapshot: ChannelsStatusSnapshot | null;
|
||||||
lastError: string | null;
|
lastError: string | null;
|
||||||
lastSuccessAt: number | null;
|
lastSuccessAt: number | null;
|
||||||
whatsappMessage: string | null;
|
whatsappMessage: string | null;
|
||||||
@@ -93,18 +93,18 @@ export type ConnectionsProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function renderConnections(props: ConnectionsProps) {
|
export function renderConnections(props: ConnectionsProps) {
|
||||||
const providers = props.snapshot?.providers as Record<string, unknown> | null;
|
const channels = props.snapshot?.channels as Record<string, unknown> | null;
|
||||||
const whatsapp = (providers?.whatsapp ?? undefined) as
|
const whatsapp = (channels?.whatsapp ?? undefined) as
|
||||||
| WhatsAppStatus
|
| WhatsAppStatus
|
||||||
| undefined;
|
| undefined;
|
||||||
const telegram = (providers?.telegram ?? undefined) as
|
const telegram = (channels?.telegram ?? undefined) as
|
||||||
| TelegramStatus
|
| TelegramStatus
|
||||||
| undefined;
|
| undefined;
|
||||||
const discord = (providers?.discord ?? null) as DiscordStatus | null;
|
const discord = (channels?.discord ?? null) as DiscordStatus | null;
|
||||||
const slack = (providers?.slack ?? null) as SlackStatus | null;
|
const slack = (channels?.slack ?? null) as SlackStatus | null;
|
||||||
const signal = (providers?.signal ?? null) as SignalStatus | null;
|
const signal = (channels?.signal ?? null) as SignalStatus | null;
|
||||||
const imessage = (providers?.imessage ?? null) as IMessageStatus | null;
|
const imessage = (channels?.imessage ?? null) as IMessageStatus | null;
|
||||||
const providerOrder: ProviderKey[] = [
|
const channelOrder: ChannelKey[] = [
|
||||||
"whatsapp",
|
"whatsapp",
|
||||||
"telegram",
|
"telegram",
|
||||||
"discord",
|
"discord",
|
||||||
@@ -112,10 +112,10 @@ export function renderConnections(props: ConnectionsProps) {
|
|||||||
"signal",
|
"signal",
|
||||||
"imessage",
|
"imessage",
|
||||||
];
|
];
|
||||||
const orderedProviders = providerOrder
|
const orderedChannels = channelOrder
|
||||||
.map((key, index) => ({
|
.map((key, index) => ({
|
||||||
key,
|
key,
|
||||||
enabled: providerEnabled(key, props),
|
enabled: channelEnabled(key, props),
|
||||||
order: index,
|
order: index,
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
@@ -125,15 +125,15 @@ export function renderConnections(props: ConnectionsProps) {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<section class="grid grid-cols-2">
|
<section class="grid grid-cols-2">
|
||||||
${orderedProviders.map((provider) =>
|
${orderedChannels.map((channel) =>
|
||||||
renderProvider(provider.key, props, {
|
renderChannel(channel.key, props, {
|
||||||
whatsapp,
|
whatsapp,
|
||||||
telegram,
|
telegram,
|
||||||
discord,
|
discord,
|
||||||
slack,
|
slack,
|
||||||
signal,
|
signal,
|
||||||
imessage,
|
imessage,
|
||||||
providerAccounts: props.snapshot?.providerAccounts ?? null,
|
channelAccounts: props.snapshot?.channelAccounts ?? null,
|
||||||
}),
|
}),
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
@@ -142,7 +142,7 @@ export function renderConnections(props: ConnectionsProps) {
|
|||||||
<div class="row" style="justify-content: space-between;">
|
<div class="row" style="justify-content: space-between;">
|
||||||
<div>
|
<div>
|
||||||
<div class="card-title">Connection health</div>
|
<div class="card-title">Connection health</div>
|
||||||
<div class="card-sub">Provider status snapshots from the gateway.</div>
|
<div class="card-sub">Channel status snapshots from the gateway.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="muted">${props.lastSuccessAt ? formatAgo(props.lastSuccessAt) : "n/a"}</div>
|
<div class="muted">${props.lastSuccessAt ? formatAgo(props.lastSuccessAt) : "n/a"}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,7 +168,7 @@ function formatDuration(ms?: number | null) {
|
|||||||
return `${hr}h`;
|
return `${hr}h`;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProviderKey =
|
type ChannelKey =
|
||||||
| "whatsapp"
|
| "whatsapp"
|
||||||
| "telegram"
|
| "telegram"
|
||||||
| "discord"
|
| "discord"
|
||||||
@@ -176,16 +176,16 @@ type ProviderKey =
|
|||||||
| "signal"
|
| "signal"
|
||||||
| "imessage";
|
| "imessage";
|
||||||
|
|
||||||
function providerEnabled(key: ProviderKey, props: ConnectionsProps) {
|
function channelEnabled(key: ChannelKey, props: ConnectionsProps) {
|
||||||
const snapshot = props.snapshot;
|
const snapshot = props.snapshot;
|
||||||
const providers = snapshot?.providers as Record<string, unknown> | null;
|
const channels = snapshot?.channels as Record<string, unknown> | null;
|
||||||
if (!snapshot || !providers) return false;
|
if (!snapshot || !channels) return false;
|
||||||
const whatsapp = providers.whatsapp as WhatsAppStatus | undefined;
|
const whatsapp = channels.whatsapp as WhatsAppStatus | undefined;
|
||||||
const telegram = providers.telegram as TelegramStatus | undefined;
|
const telegram = channels.telegram as TelegramStatus | undefined;
|
||||||
const discord = (providers.discord ?? null) as DiscordStatus | null;
|
const discord = (channels.discord ?? null) as DiscordStatus | null;
|
||||||
const slack = (providers.slack ?? null) as SlackStatus | null;
|
const slack = (channels.slack ?? null) as SlackStatus | null;
|
||||||
const signal = (providers.signal ?? null) as SignalStatus | null;
|
const signal = (channels.signal ?? null) as SignalStatus | null;
|
||||||
const imessage = (providers.imessage ?? null) as IMessageStatus | null;
|
const imessage = (channels.imessage ?? null) as IMessageStatus | null;
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "whatsapp":
|
case "whatsapp":
|
||||||
return (
|
return (
|
||||||
@@ -208,24 +208,24 @@ function providerEnabled(key: ProviderKey, props: ConnectionsProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProviderAccountCount(
|
function getChannelAccountCount(
|
||||||
key: ProviderKey,
|
key: ChannelKey,
|
||||||
providerAccounts?: Record<string, ProviderAccountSnapshot[]> | null,
|
channelAccounts?: Record<string, ChannelAccountSnapshot[]> | null,
|
||||||
): number {
|
): number {
|
||||||
return providerAccounts?.[key]?.length ?? 0;
|
return channelAccounts?.[key]?.length ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderProviderAccountCount(
|
function renderChannelAccountCount(
|
||||||
key: ProviderKey,
|
key: ChannelKey,
|
||||||
providerAccounts?: Record<string, ProviderAccountSnapshot[]> | null,
|
channelAccounts?: Record<string, ChannelAccountSnapshot[]> | null,
|
||||||
) {
|
) {
|
||||||
const count = getProviderAccountCount(key, providerAccounts);
|
const count = getChannelAccountCount(key, channelAccounts);
|
||||||
if (count < 2) return nothing;
|
if (count < 2) return nothing;
|
||||||
return html`<div class="account-count">Accounts (${count})</div>`;
|
return html`<div class="account-count">Accounts (${count})</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderProvider(
|
function renderChannel(
|
||||||
key: ProviderKey,
|
key: ChannelKey,
|
||||||
props: ConnectionsProps,
|
props: ConnectionsProps,
|
||||||
data: {
|
data: {
|
||||||
whatsapp?: WhatsAppStatus;
|
whatsapp?: WhatsAppStatus;
|
||||||
@@ -234,12 +234,12 @@ function renderProvider(
|
|||||||
slack?: SlackStatus | null;
|
slack?: SlackStatus | null;
|
||||||
signal?: SignalStatus | null;
|
signal?: SignalStatus | null;
|
||||||
imessage?: IMessageStatus | null;
|
imessage?: IMessageStatus | null;
|
||||||
providerAccounts?: Record<string, ProviderAccountSnapshot[]> | null;
|
channelAccounts?: Record<string, ChannelAccountSnapshot[]> | null;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const accountCountLabel = renderProviderAccountCount(
|
const accountCountLabel = renderChannelAccountCount(
|
||||||
key,
|
key,
|
||||||
data.providerAccounts,
|
data.channelAccounts,
|
||||||
);
|
);
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "whatsapp": {
|
case "whatsapp": {
|
||||||
@@ -345,10 +345,10 @@ function renderProvider(
|
|||||||
}
|
}
|
||||||
case "telegram": {
|
case "telegram": {
|
||||||
const telegram = data.telegram;
|
const telegram = data.telegram;
|
||||||
const telegramAccounts = data.providerAccounts?.telegram ?? [];
|
const telegramAccounts = data.channelAccounts?.telegram ?? [];
|
||||||
const hasMultipleAccounts = telegramAccounts.length > 1;
|
const hasMultipleAccounts = telegramAccounts.length > 1;
|
||||||
|
|
||||||
const renderAccountCard = (account: ProviderAccountSnapshot) => {
|
const renderAccountCard = (account: ChannelAccountSnapshot) => {
|
||||||
const probe = account.probe as { bot?: { username?: string } } | undefined;
|
const probe = account.probe as { bot?: { username?: string } } | undefined;
|
||||||
const botUsername = probe?.bot?.username;
|
const botUsername = probe?.bot?.username;
|
||||||
const label = account.name || account.accountId;
|
const label = account.name || account.accountId;
|
||||||
|
|||||||
@@ -168,9 +168,9 @@ export function renderCron(props: CronProps) {
|
|||||||
rows="4"
|
rows="4"
|
||||||
></textarea>
|
></textarea>
|
||||||
</label>
|
</label>
|
||||||
${props.form.payloadKind === "agentTurn"
|
${props.form.payloadKind === "agentTurn"
|
||||||
? html`
|
? html`
|
||||||
<div class="form-grid" style="margin-top: 12px;">
|
<div class="form-grid" style="margin-top: 12px;">
|
||||||
<label class="field checkbox">
|
<label class="field checkbox">
|
||||||
<span>Deliver</span>
|
<span>Deliver</span>
|
||||||
<input
|
<input
|
||||||
@@ -181,17 +181,17 @@ export function renderCron(props: CronProps) {
|
|||||||
deliver: (e.target as HTMLInputElement).checked,
|
deliver: (e.target as HTMLInputElement).checked,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Provider</span>
|
<span>Channel</span>
|
||||||
<select
|
<select
|
||||||
.value=${props.form.provider}
|
.value=${props.form.channel}
|
||||||
@change=${(e: Event) =>
|
@change=${(e: Event) =>
|
||||||
props.onFormChange({
|
props.onFormChange({
|
||||||
provider: (e.target as HTMLSelectElement).value as CronFormState["provider"],
|
channel: (e.target as HTMLSelectElement).value as CronFormState["channel"],
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<option value="last">Last</option>
|
<option value="last">Last</option>
|
||||||
<option value="whatsapp">WhatsApp</option>
|
<option value="whatsapp">WhatsApp</option>
|
||||||
<option value="telegram">Telegram</option>
|
<option value="telegram">Telegram</option>
|
||||||
<option value="discord">Discord</option>
|
<option value="discord">Discord</option>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export type OverviewProps = {
|
|||||||
sessionsCount: number | null;
|
sessionsCount: number | null;
|
||||||
cronEnabled: boolean | null;
|
cronEnabled: boolean | null;
|
||||||
cronNext: number | null;
|
cronNext: number | null;
|
||||||
lastProvidersRefresh: number | null;
|
lastChannelsRefresh: number | null;
|
||||||
onSettingsChange: (next: UiSettings) => void;
|
onSettingsChange: (next: UiSettings) => void;
|
||||||
onPasswordChange: (next: string) => void;
|
onPasswordChange: (next: string) => void;
|
||||||
onSessionKeyChange: (next: string) => void;
|
onSessionKeyChange: (next: string) => void;
|
||||||
@@ -109,10 +109,10 @@ export function renderOverview(props: OverviewProps) {
|
|||||||
<div class="stat-value">${tick}</div>
|
<div class="stat-value">${tick}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-label">Last Providers Refresh</div>
|
<div class="stat-label">Last Channels Refresh</div>
|
||||||
<div class="stat-value">
|
<div class="stat-value">
|
||||||
${props.lastProvidersRefresh
|
${props.lastChannelsRefresh
|
||||||
? formatAgo(props.lastProvidersRefresh)
|
? formatAgo(props.lastChannelsRefresh)
|
||||||
: "n/a"}
|
: "n/a"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export default defineConfig({
|
|||||||
// Gateway server integration surfaces are intentionally validated via manual/e2e runs.
|
// Gateway server integration surfaces are intentionally validated via manual/e2e runs.
|
||||||
"src/gateway/control-ui.ts",
|
"src/gateway/control-ui.ts",
|
||||||
"src/gateway/server-bridge.ts",
|
"src/gateway/server-bridge.ts",
|
||||||
"src/gateway/server-providers.ts",
|
"src/gateway/server-channels.ts",
|
||||||
"src/gateway/server-methods/config.ts",
|
"src/gateway/server-methods/config.ts",
|
||||||
"src/gateway/server-methods/send.ts",
|
"src/gateway/server-methods/send.ts",
|
||||||
"src/gateway/server-methods/skills.ts",
|
"src/gateway/server-methods/skills.ts",
|
||||||
@@ -62,13 +62,13 @@ export default defineConfig({
|
|||||||
// Interactive UIs/flows are intentionally validated via manual/e2e runs.
|
// Interactive UIs/flows are intentionally validated via manual/e2e runs.
|
||||||
"src/tui/**",
|
"src/tui/**",
|
||||||
"src/wizard/**",
|
"src/wizard/**",
|
||||||
// Provider surfaces are largely integration-tested (or manually validated).
|
// Channel surfaces are largely integration-tested (or manually validated).
|
||||||
"src/discord/**",
|
"src/discord/**",
|
||||||
"src/imessage/**",
|
"src/imessage/**",
|
||||||
"src/signal/**",
|
"src/signal/**",
|
||||||
"src/slack/**",
|
"src/slack/**",
|
||||||
"src/browser/**",
|
"src/browser/**",
|
||||||
"src/providers/web/**",
|
"src/channels/web/**",
|
||||||
"src/telegram/index.ts",
|
"src/telegram/index.ts",
|
||||||
"src/telegram/proxy.ts",
|
"src/telegram/proxy.ts",
|
||||||
"src/telegram/webhook-set.ts",
|
"src/telegram/webhook-set.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user