mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-23 11:18:08 +00:00
Prune unused iOS surfaces and regenerate the Xcode project. Add a scoped Periphery PR gate with hardened artifact handling and stale-status cleanup. Co-authored-by: Sash Zats <sash@zats.io>
668 lines
25 KiB
Swift
668 lines
25 KiB
Swift
import OpenClawKit
|
|
import OpenClawProtocol
|
|
import SwiftUI
|
|
|
|
struct SettingsChannelsDestination: View {
|
|
@Environment(NodeAppModel.self) private var appModel
|
|
@Environment(\.scenePhase) private var scenePhase
|
|
let showsSummaryCard: Bool
|
|
@State private var snapshot: ChannelsStatusResult?
|
|
@State private var isLoading = false
|
|
@State private var errorText: String?
|
|
@State private var busyOperation: SettingsChannelOperation?
|
|
|
|
init(showsSummaryCard: Bool = true) {
|
|
self.showsSummaryCard = showsSummaryCard
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 14) {
|
|
if self.showsSummaryCard {
|
|
self.summaryCard
|
|
}
|
|
self.channelsCard
|
|
}
|
|
.task(id: self.refreshID) {
|
|
await self.loadChannels(force: false)
|
|
}
|
|
.refreshable {
|
|
await self.loadChannels(force: true)
|
|
}
|
|
}
|
|
|
|
private var summaryCard: some View {
|
|
ProCard(radius: SettingsLayout.cardRadius) {
|
|
HStack(spacing: 12) {
|
|
ProIconBadge(systemName: "point.3.connected.trianglepath.dotted", color: self.summaryColor)
|
|
VStack(alignment: .leading, spacing: 3) {
|
|
Text("Channels / Integrations")
|
|
.font(.headline)
|
|
Text(self.summaryDetail)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
Spacer(minLength: 8)
|
|
ProValuePill(value: self.summaryValue, color: self.summaryColor)
|
|
}
|
|
}
|
|
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
|
}
|
|
|
|
private var channelsCard: some View {
|
|
ProCard(padding: 0, radius: SettingsLayout.cardRadius) {
|
|
VStack(spacing: 0) {
|
|
ProPanelHeader(
|
|
title: "Message Routing",
|
|
value: self.headerValue,
|
|
actionIcon: self.isLoading ? "hourglass" : "arrow.clockwise",
|
|
actionAccessibilityLabel: "Refresh Channels",
|
|
isActionDisabled: self.isLoading,
|
|
action: {
|
|
Task { await self.loadChannels(force: true) }
|
|
})
|
|
|
|
if let errorText {
|
|
ProStatusRow(
|
|
icon: "exclamationmark.triangle",
|
|
title: "Channel status unavailable",
|
|
detail: errorText,
|
|
value: "error",
|
|
color: OpenClawBrand.warn)
|
|
} else if !self.canRead {
|
|
ProStatusRow(
|
|
icon: "wifi.slash",
|
|
title: "Gateway offline",
|
|
detail: "Connect to the gateway to load installed channels, accounts, and routing status.",
|
|
value: "offline",
|
|
color: .secondary)
|
|
} else if self.isLoading, self.snapshot == nil {
|
|
ProStatusRow(
|
|
icon: "hourglass",
|
|
title: "Loading channels",
|
|
detail: "Fetching installed channels, accounts, and routing status from the gateway.",
|
|
value: "loading",
|
|
color: OpenClawBrand.accent)
|
|
} else if self.channelEntries.isEmpty {
|
|
ProStatusRow(
|
|
icon: "tray",
|
|
title: "No channel plugins reported",
|
|
detail: "Install or enable channel plugins on the gateway, then refresh.",
|
|
value: "empty",
|
|
color: .secondary)
|
|
} else {
|
|
ForEach(Array(self.channelEntries.enumerated()), id: \.element.id) { index, entry in
|
|
if index > 0 {
|
|
Divider().padding(.leading, 58)
|
|
}
|
|
SettingsChannelRow(
|
|
entry: entry,
|
|
canAdmin: self.canAdmin,
|
|
busyOperation: self.busyOperation,
|
|
start: { accountID in
|
|
Task { await self.run(.start, channelID: entry.id, accountID: accountID) }
|
|
},
|
|
stop: { accountID in
|
|
Task { await self.run(.stop, channelID: entry.id, accountID: accountID) }
|
|
},
|
|
logout: { accountID in
|
|
Task { await self.run(.logout, channelID: entry.id, accountID: accountID) }
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
|
}
|
|
|
|
private var refreshID: String {
|
|
[
|
|
self.canRead ? "connected" : "offline",
|
|
self.scenePhase == .active ? "active" : "inactive",
|
|
].joined(separator: ":")
|
|
}
|
|
|
|
private var canRead: Bool {
|
|
self.appModel.isOperatorGatewayConnected
|
|
}
|
|
|
|
private var canAdmin: Bool {
|
|
self.appModel.hasOperatorAdminScope
|
|
}
|
|
|
|
static func shouldEnableChannelOperation(canRead: Bool, hasOperatorAdminScope: Bool) -> Bool {
|
|
canRead && hasOperatorAdminScope
|
|
}
|
|
|
|
private var headerValue: String? {
|
|
if self.isLoading { return "Loading" }
|
|
guard self.canRead else { return "Offline" }
|
|
return "\(self.channelEntries.count)"
|
|
}
|
|
|
|
private var summaryDetail: String {
|
|
guard self.canRead else {
|
|
return "Connect to load channel integrations."
|
|
}
|
|
if let errorText {
|
|
return errorText
|
|
}
|
|
return "Installed channel clients, account state, and message-routing readiness."
|
|
}
|
|
|
|
private var summaryValue: String {
|
|
guard self.canRead else { return "offline" }
|
|
if self.isLoading { return "loading" }
|
|
if self.errorText != nil { return "error" }
|
|
let configured = self.channelEntries.count(where: { $0.configured })
|
|
return "\(configured)/\(self.channelEntries.count)"
|
|
}
|
|
|
|
private var summaryColor: Color {
|
|
guard self.canRead else { return .secondary }
|
|
if self.errorText != nil { return OpenClawBrand.warn }
|
|
return self.channelEntries.contains(where: { $0.running || $0.connected }) ? OpenClawBrand.ok : OpenClawBrand
|
|
.accent
|
|
}
|
|
|
|
private var channelEntries: [SettingsChannelEntry] {
|
|
guard let snapshot else { return [] }
|
|
let ids = snapshot.channelorder.isEmpty ? Array(snapshot.channels.keys).sorted() : snapshot.channelorder
|
|
return ids.map { self.entry(channelID: $0, snapshot: snapshot) }
|
|
}
|
|
|
|
private func entry(channelID: String, snapshot: ChannelsStatusResult) -> SettingsChannelEntry {
|
|
let summary = snapshot.channels[channelID]?.dictionaryValue ?? [:]
|
|
let accounts = self.accounts(channelID: channelID, snapshot: snapshot)
|
|
let configured = accounts.contains(where: \.configured) || summary["configured"]?.boolValue == true
|
|
let running = accounts.contains(where: \.running)
|
|
let connected = accounts.contains(where: \.connected)
|
|
let linked = accounts.contains(where: \.linked)
|
|
let label = snapshot.channellabels[channelID]?.stringValue ?? Self.fallbackLabel(channelID)
|
|
let detail = snapshot.channeldetaillabels?[channelID]?.stringValue ?? Self.fallbackDetail(channelID)
|
|
let systemImage = snapshot.channelsystemimages?[channelID]?.stringValue ?? Self.fallbackSystemImage(channelID)
|
|
let lastActivity = accounts.compactMap(\.lastActivityMs).max()
|
|
let lastError = accounts.compactMap(\.lastError).first ?? summary["lastError"]?.stringValue
|
|
return SettingsChannelEntry(
|
|
id: channelID,
|
|
label: label,
|
|
detail: detail,
|
|
systemImage: systemImage,
|
|
configured: configured,
|
|
running: running,
|
|
connected: connected,
|
|
linked: linked,
|
|
lastActivityText: lastActivity.map(Self.relativeTime),
|
|
lastError: lastError,
|
|
unavailableReason: configured ? nil : "Configure this channel on the gateway.",
|
|
accounts: accounts)
|
|
}
|
|
|
|
private func accounts(channelID: String, snapshot: ChannelsStatusResult) -> [SettingsChannelAccount] {
|
|
let rawAccounts = snapshot.channelaccounts[channelID]?.arrayValue ?? []
|
|
return rawAccounts.compactMap { raw in
|
|
guard let dict = raw.dictionaryValue else { return nil }
|
|
let accountID = dict["accountId"]?.stringValue ?? "default"
|
|
let name = dict["name"]?.stringValue
|
|
let lastActivity = [
|
|
dict["lastInboundAt"]?.intValue,
|
|
dict["lastOutboundAt"]?.intValue,
|
|
dict["lastTransportActivityAt"]?.intValue,
|
|
]
|
|
.compactMap(\.self)
|
|
.max()
|
|
return SettingsChannelAccount(
|
|
id: accountID,
|
|
name: name,
|
|
configured: dict["configured"]?.boolValue == true,
|
|
enabled: dict["enabled"]?.boolValue != false,
|
|
running: dict["running"]?.boolValue == true,
|
|
connected: dict["connected"]?.boolValue == true,
|
|
linked: dict["linked"]?.boolValue == true,
|
|
healthState: dict["healthState"]?.stringValue,
|
|
lastError: dict["lastError"]?.stringValue,
|
|
lastActivityMs: lastActivity)
|
|
}
|
|
}
|
|
|
|
private func loadChannels(force: Bool) async {
|
|
guard self.scenePhase == .active else { return }
|
|
guard self.canRead else {
|
|
self.snapshot = nil
|
|
self.errorText = nil
|
|
return
|
|
}
|
|
if self.isLoading { return }
|
|
|
|
self.isLoading = true
|
|
self.errorText = nil
|
|
defer { self.isLoading = false }
|
|
|
|
do {
|
|
let params = ChannelsStatusParams(probe: false, timeoutms: 10000, channel: nil)
|
|
let data = try await self.request(method: "channels.status", params: params, timeoutSeconds: 12)
|
|
self.snapshot = try JSONDecoder().decode(ChannelsStatusResult.self, from: data)
|
|
} catch {
|
|
if force || self.snapshot == nil {
|
|
self.errorText = Self.message(for: error)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func run(_ kind: SettingsChannelOperation.Kind, channelID: String, accountID: String?) async {
|
|
guard Self.shouldEnableChannelOperation(canRead: self.canRead, hasOperatorAdminScope: self.canAdmin),
|
|
self.busyOperation == nil
|
|
else {
|
|
return
|
|
}
|
|
self.busyOperation = SettingsChannelOperation(kind: kind, channelID: channelID, accountID: accountID)
|
|
self.errorText = nil
|
|
defer { self.busyOperation = nil }
|
|
|
|
do {
|
|
switch kind {
|
|
case .start:
|
|
let params = ChannelsStartParams(channel: channelID, accountid: accountID)
|
|
_ = try await self.request(method: "channels.start", params: params, timeoutSeconds: 20)
|
|
case .stop:
|
|
let params = ChannelsStopParams(channel: channelID, accountid: accountID)
|
|
_ = try await self.request(method: "channels.stop", params: params, timeoutSeconds: 20)
|
|
case .logout:
|
|
let params = ChannelsLogoutParams(channel: channelID, accountid: accountID)
|
|
_ = try await self.request(method: "channels.logout", params: params, timeoutSeconds: 20)
|
|
}
|
|
await self.loadChannels(force: true)
|
|
} catch {
|
|
self.errorText = Self.message(for: error)
|
|
}
|
|
}
|
|
|
|
private func request(method: String, params: some Encodable, timeoutSeconds: Int) async throws -> Data {
|
|
let data = try JSONEncoder().encode(params)
|
|
guard let json = String(data: data, encoding: .utf8) else {
|
|
throw SettingsChannelError.invalidPayload
|
|
}
|
|
return try await self.appModel.operatorSession.request(
|
|
method: method,
|
|
paramsJSON: json,
|
|
timeoutSeconds: timeoutSeconds)
|
|
}
|
|
|
|
static func fallbackLabel(_ id: String) -> String {
|
|
if let metadata = self.fallbackMetadata[id.lowercased()] {
|
|
return metadata.label
|
|
}
|
|
return id.replacingOccurrences(of: "-", with: " ")
|
|
.replacingOccurrences(of: "_", with: " ")
|
|
.split(separator: " ")
|
|
.map { $0.prefix(1).uppercased() + $0.dropFirst() }
|
|
.joined(separator: " ")
|
|
}
|
|
|
|
static func fallbackDetail(_ id: String) -> String {
|
|
self.fallbackMetadata[id.lowercased()]?.detail ?? "Channel integration"
|
|
}
|
|
|
|
static func fallbackSystemImage(_ id: String) -> String {
|
|
self.fallbackMetadata[id.lowercased()]?.systemImage ?? "bubble.left.and.text.bubble.right"
|
|
}
|
|
|
|
private static let fallbackMetadata: [String: SettingsChannelFallbackMetadata] = [
|
|
"clickclack": SettingsChannelFallbackMetadata(
|
|
label: "ClickClack",
|
|
detail: "Self-hosted chat bot routing.",
|
|
systemImage: "bubble.left.and.bubble.right"),
|
|
]
|
|
|
|
private static func relativeTime(_ milliseconds: Int) -> String {
|
|
let age = max(0, Int(Date().timeIntervalSince1970 * 1000) - milliseconds)
|
|
let minutes = age / 60000
|
|
if minutes < 1 { return "now" }
|
|
if minutes < 60 { return "\(minutes)m ago" }
|
|
let hours = minutes / 60
|
|
if hours < 24 { return "\(hours)h ago" }
|
|
return "\(hours / 24)d ago"
|
|
}
|
|
|
|
private static func message(for error: Error) -> String {
|
|
if let channelError = error as? SettingsChannelError {
|
|
return channelError.message
|
|
}
|
|
return error.localizedDescription
|
|
}
|
|
}
|
|
|
|
private struct SettingsChannelRow: View {
|
|
let entry: SettingsChannelEntry
|
|
let canAdmin: Bool
|
|
let busyOperation: SettingsChannelOperation?
|
|
let start: (String?) -> Void
|
|
let stop: (String?) -> Void
|
|
let logout: (String?) -> Void
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
HStack(alignment: .top, spacing: 12) {
|
|
ProIconBadge(systemName: self.entry.systemImage, color: self.entry.color)
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(self.entry.label)
|
|
.font(.subheadline.weight(.semibold))
|
|
Text(self.entry.detailText)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
if let lastError = self.entry.lastError {
|
|
Text(lastError)
|
|
.font(.caption2.weight(.medium))
|
|
.foregroundStyle(OpenClawBrand.warn)
|
|
.lineLimit(2)
|
|
}
|
|
}
|
|
Spacer(minLength: 8)
|
|
ProValuePill(value: self.entry.statusValue, color: self.entry.color)
|
|
}
|
|
|
|
if !self.entry.accounts.isEmpty {
|
|
VStack(spacing: 0) {
|
|
ForEach(Array(self.entry.accounts.enumerated()), id: \.element.id) { index, account in
|
|
if index > 0 {
|
|
Divider().padding(.leading, 38)
|
|
}
|
|
self.accountRow(account)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, 14)
|
|
.padding(.vertical, 10)
|
|
}
|
|
|
|
private func accountRow(_ account: SettingsChannelAccount) -> some View {
|
|
HStack(spacing: 10) {
|
|
Image(systemName: account.running || account.connected ? "checkmark.circle.fill" : "circle")
|
|
.foregroundStyle(account.color)
|
|
.frame(width: 28, height: 28)
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(account.displayName)
|
|
.font(.caption.weight(.semibold))
|
|
Text(account.detailText)
|
|
.font(.caption2)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(1)
|
|
}
|
|
Spacer(minLength: 8)
|
|
Menu {
|
|
if account.running {
|
|
Button("Stop") {
|
|
self.stop(account.id)
|
|
}
|
|
} else {
|
|
Button("Start") {
|
|
self.start(account.id)
|
|
}
|
|
.disabled(!account.configured || !account.enabled)
|
|
}
|
|
if account.linked {
|
|
Button("Logout", role: .destructive) {
|
|
self.logout(account.id)
|
|
}
|
|
}
|
|
} label: {
|
|
Image(systemName: self.actionMenuIcon(account))
|
|
}
|
|
.buttonStyle(.bordered)
|
|
.controlSize(.mini)
|
|
.disabled(!self.canAdmin || self.isBusy(account))
|
|
}
|
|
.padding(.vertical, 8)
|
|
}
|
|
|
|
private func actionMenuIcon(_ account: SettingsChannelAccount) -> String {
|
|
if self.isBusy(account) {
|
|
return "hourglass"
|
|
}
|
|
if !self.canAdmin {
|
|
return "lock.shield"
|
|
}
|
|
return "ellipsis.circle"
|
|
}
|
|
|
|
private func isBusy(_ account: SettingsChannelAccount) -> Bool {
|
|
self.busyOperation?.channelID == self.entry.id && self.busyOperation?.accountID == account.id
|
|
}
|
|
}
|
|
|
|
private struct SettingsChannelEntry: Identifiable {
|
|
let id: String
|
|
let label: String
|
|
let detail: String
|
|
let systemImage: String
|
|
let configured: Bool
|
|
let running: Bool
|
|
let connected: Bool
|
|
let linked: Bool
|
|
let lastActivityText: String?
|
|
let lastError: String?
|
|
let unavailableReason: String?
|
|
let accounts: [SettingsChannelAccount]
|
|
|
|
var color: Color {
|
|
if self.connected || self.running { return OpenClawBrand.ok }
|
|
if self.lastError != nil { return OpenClawBrand.warn }
|
|
return self.configured ? OpenClawBrand.accent : .secondary
|
|
}
|
|
|
|
var statusValue: String {
|
|
if self.connected { return "connected" }
|
|
if self.running { return "running" }
|
|
if self.linked { return "linked" }
|
|
if self.configured { return "configured" }
|
|
return "not set"
|
|
}
|
|
|
|
var detailText: String {
|
|
if let lastActivityText {
|
|
return "\(self.detail) • active \(lastActivityText)"
|
|
}
|
|
if let unavailableReason {
|
|
return unavailableReason
|
|
}
|
|
return self.detail
|
|
}
|
|
}
|
|
|
|
private struct SettingsChannelFallbackMetadata {
|
|
let label: String
|
|
let detail: String
|
|
let systemImage: String
|
|
}
|
|
|
|
private struct SettingsChannelAccount: Identifiable {
|
|
let id: String
|
|
let name: String?
|
|
let configured: Bool
|
|
let enabled: Bool
|
|
let running: Bool
|
|
let connected: Bool
|
|
let linked: Bool
|
|
let healthState: String?
|
|
let lastError: String?
|
|
let lastActivityMs: Int?
|
|
|
|
var displayName: String {
|
|
let trimmedName = self.name?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
return trimmedName.isEmpty ? self.id : "\(trimmedName) (\(self.id))"
|
|
}
|
|
|
|
var detailText: String {
|
|
let state = if self.connected {
|
|
"connected"
|
|
} else if self.running {
|
|
"running"
|
|
} else if self.linked {
|
|
"linked"
|
|
} else if self.configured {
|
|
"configured"
|
|
} else {
|
|
"not configured"
|
|
}
|
|
let enabledText = self.enabled ? "enabled" : "disabled"
|
|
if let healthState, !healthState.isEmpty {
|
|
return "\(state), \(enabledText), \(healthState)"
|
|
}
|
|
if let lastError, !lastError.isEmpty {
|
|
return "\(state), \(enabledText), error"
|
|
}
|
|
return "\(state), \(enabledText)"
|
|
}
|
|
|
|
var color: Color {
|
|
if self.connected || self.running { return OpenClawBrand.ok }
|
|
if self.lastError != nil { return OpenClawBrand.warn }
|
|
return self.configured ? OpenClawBrand.accent : .secondary
|
|
}
|
|
}
|
|
|
|
private struct SettingsChannelOperation: Equatable {
|
|
enum Kind {
|
|
case start
|
|
case stop
|
|
case logout
|
|
}
|
|
|
|
let kind: Kind
|
|
let channelID: String
|
|
let accountID: String?
|
|
}
|
|
|
|
private enum SettingsChannelError: Error {
|
|
case invalidPayload
|
|
|
|
var message: String {
|
|
switch self {
|
|
case .invalidPayload:
|
|
"Could not encode channel request."
|
|
}
|
|
}
|
|
}
|
|
|
|
#if DEBUG
|
|
#Preview("Channels states") {
|
|
SettingsChannelsStatesPreview()
|
|
}
|
|
|
|
private struct SettingsChannelsStatesPreview: View {
|
|
var body: some View {
|
|
ZStack {
|
|
OpenClawProBackground()
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
self.stateSection("Connected") {
|
|
SettingsChannelRow(
|
|
entry: Self.telegramEntry,
|
|
canAdmin: true,
|
|
busyOperation: nil,
|
|
start: { _ in },
|
|
stop: { _ in },
|
|
logout: { _ in })
|
|
}
|
|
|
|
self.stateSection("Loading") {
|
|
ProPanelHeader(
|
|
title: "Message Routing",
|
|
value: "Loading",
|
|
actionIcon: "hourglass",
|
|
actionAccessibilityLabel: "Refresh Channels",
|
|
isActionDisabled: true,
|
|
action: {})
|
|
ProStatusRow(
|
|
icon: "hourglass",
|
|
title: "Loading channel status",
|
|
detail: "Checking installed channel clients and account state.",
|
|
value: "loading",
|
|
color: OpenClawBrand.accent)
|
|
}
|
|
|
|
self.stateSection("Empty") {
|
|
ProPanelHeader(
|
|
title: "Message Routing",
|
|
value: "0",
|
|
actionIcon: "arrow.clockwise",
|
|
actionAccessibilityLabel: "Refresh Channels",
|
|
action: {})
|
|
ProStatusRow(
|
|
icon: "tray",
|
|
title: "No channel plugins reported",
|
|
detail: "Install or enable channel plugins on the gateway, then refresh.",
|
|
value: "empty",
|
|
color: .secondary)
|
|
}
|
|
|
|
self.stateSection("Error") {
|
|
ProStatusRow(
|
|
icon: "exclamationmark.triangle",
|
|
title: "Channel status unavailable",
|
|
detail: "Gateway returned an unexpected channel status response.",
|
|
value: "error",
|
|
color: OpenClawBrand.warn)
|
|
}
|
|
|
|
self.stateSection("Offline") {
|
|
ProStatusRow(
|
|
icon: "wifi.slash",
|
|
title: "Gateway offline",
|
|
detail: "Connect to the gateway to load installed channels, accounts, and routing status.",
|
|
value: "offline",
|
|
color: .secondary)
|
|
}
|
|
}
|
|
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
|
.padding(.vertical, 18)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func stateSection(
|
|
_ title: String,
|
|
@ViewBuilder content: () -> some View) -> some View
|
|
{
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text(title)
|
|
.font(.subheadline.weight(.semibold))
|
|
.foregroundStyle(.secondary)
|
|
ProCard(padding: 0, radius: SettingsLayout.cardRadius) {
|
|
VStack(spacing: 0) {
|
|
content()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static let telegramEntry = SettingsChannelEntry(
|
|
id: "telegram",
|
|
label: "Telegram",
|
|
detail: "Message routing client",
|
|
systemImage: "paperplane",
|
|
configured: true,
|
|
running: true,
|
|
connected: true,
|
|
linked: true,
|
|
lastActivityText: "4m ago",
|
|
lastError: nil,
|
|
unavailableReason: nil,
|
|
accounts: [
|
|
SettingsChannelAccount(
|
|
id: "main",
|
|
name: "OpenClaw Ops",
|
|
configured: true,
|
|
enabled: true,
|
|
running: true,
|
|
connected: true,
|
|
linked: true,
|
|
healthState: "healthy",
|
|
lastError: nil,
|
|
lastActivityMs: nil),
|
|
])
|
|
}
|
|
#endif
|