21 files changed
+1872
-7
No files matched your search
@@ -0,0 +1,114 @@
|
||||
package com.m57.hermescontrol.data.ws
|
||||
|
||||
import com.m57.hermescontrol.data.model.ConnectionEnvField
|
||||
import com.m57.hermescontrol.data.model.ConnectionOperationSnapshot
|
||||
import com.m57.hermescontrol.data.model.ConnectionOperationTarget
|
||||
import com.m57.hermescontrol.data.model.ConnectionTargetAction
|
||||
import com.m57.hermescontrol.data.model.ConnectionTargetKind
|
||||
import com.m57.hermescontrol.data.model.ConnectionTargetState
|
||||
|
||||
/** Tolerant parser for backend-owned connector operation snapshots. */
|
||||
object ConnectionOperationParser {
|
||||
fun parse(
|
||||
payload: Map<String, Any?>,
|
||||
sessionId: String? = null,
|
||||
): ConnectionOperationSnapshot? {
|
||||
val opId = (payload["op_id"] as? String)?.trim() ?: return null
|
||||
val seq = payload["seq"].toLongOrNull() ?: return null
|
||||
val deadlineAt = payload["deadline_at"].toDoubleOrNull() ?: return null
|
||||
val targets =
|
||||
(payload["targets"] as? List<*>)
|
||||
?.mapNotNull { parseTarget(it as? Map<*, *>) }
|
||||
?: return null
|
||||
if (opId.isBlank() || seq < 0L || !deadlineAt.isFinite() || deadlineAt <= 0.0 || targets.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
return ConnectionOperationSnapshot(
|
||||
sessionId = (sessionId ?: payload["session_id"] as? String)?.takeIf { it.isNotBlank() },
|
||||
opId = opId,
|
||||
seq = seq,
|
||||
deadlineAt = deadlineAt,
|
||||
timeoutSeconds =
|
||||
payload["timeout_seconds"]
|
||||
.toDoubleOrNull()
|
||||
?.takeIf { it.isFinite() && it > 0.0 },
|
||||
toolCallId = payload["tool_call_id"] as? String,
|
||||
settled = payload["settled"] as? Boolean ?: false,
|
||||
settledBy = payload["settled_by"] as? String,
|
||||
targets = targets,
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseTarget(raw: Map<*, *>?): ConnectionOperationTarget? {
|
||||
val map = raw ?: return null
|
||||
val name = (map["name"] as? String)?.trim() ?: return null
|
||||
if (name.isBlank()) return null
|
||||
return ConnectionOperationTarget(
|
||||
name = name,
|
||||
kind =
|
||||
enumValue(
|
||||
map["kind"] as? String,
|
||||
ConnectionTargetKind.values(),
|
||||
ConnectionTargetKind.UNKNOWN,
|
||||
),
|
||||
action =
|
||||
enumValue(
|
||||
map["action"] as? String,
|
||||
ConnectionTargetAction.values(),
|
||||
ConnectionTargetAction.UNKNOWN,
|
||||
),
|
||||
state =
|
||||
enumValue(
|
||||
map["state"] as? String,
|
||||
ConnectionTargetState.values(),
|
||||
ConnectionTargetState.UNKNOWN,
|
||||
),
|
||||
detail = map["detail"] as? String,
|
||||
instructions = map["instructions"] as? String,
|
||||
discoveryError = map["discovery_error"] as? String,
|
||||
connectUrl = map["connect_url"] as? String,
|
||||
connectionId = map["connection_id"] as? String,
|
||||
attempt = map["attempt"] as? String,
|
||||
requiredEnv =
|
||||
(map["required_env"] as? List<*>)
|
||||
?.mapNotNull { parseEnv(it as? Map<*, *>) }
|
||||
?: emptyList(),
|
||||
tools = (map["tools"] as? List<*>)?.filterIsInstance<String>() ?: emptyList(),
|
||||
hint = map["hint"] as? String,
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseEnv(raw: Map<*, *>?): ConnectionEnvField? {
|
||||
val map = raw ?: return null
|
||||
val name = (map["name"] as? String)?.trim() ?: return null
|
||||
if (name.isBlank()) return null
|
||||
val secret = map["secret"] as? Boolean ?: false
|
||||
return ConnectionEnvField(
|
||||
name = name,
|
||||
required = map["required"] as? Boolean ?: false,
|
||||
secret = secret,
|
||||
defaultValue = if (secret) null else map["default"] as? String,
|
||||
prompt = map["prompt"] as? String,
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun <reified T : Enum<T>> enumValue(
|
||||
raw: String?,
|
||||
values: Array<T>,
|
||||
fallback: T,
|
||||
): T = values.firstOrNull { it.name.equals(raw, ignoreCase = true) } ?: fallback
|
||||
|
||||
private fun Any?.toLongOrNull(): Long? =
|
||||
when (this) {
|
||||
is Number -> this.toLong().takeIf { this.toDouble() == it.toDouble() }
|
||||
is String -> this.toLongOrNull()
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun Any?.toDoubleOrNull(): Double? =
|
||||
when (this) {
|
||||
is Number -> this.toDouble()
|
||||
is String -> this.toDoubleOrNull()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,19 @@ object EventParser {
|
||||
val sessionId = params["session_id"] as? String ?: payload?.get("session_id") as? String
|
||||
|
||||
return when (eventType) {
|
||||
"connection.request", "connection.update" -> {
|
||||
val snapshot = payload?.let { ConnectionOperationParser.parse(it, sessionId) }
|
||||
// Operation payloads may carry credential defaults and OAuth URLs.
|
||||
// Never retain the raw frame when a malformed operation degrades to Unknown.
|
||||
if (snapshot == null) {
|
||||
WsEvent.Unknown("")
|
||||
} else if (eventType == "connection.request") {
|
||||
WsEvent.ConnectionRequest(snapshot)
|
||||
} else {
|
||||
WsEvent.ConnectionUpdate(snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
"gateway.ready" -> {
|
||||
WsEvent.GatewayReady(payload)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.m57.hermescontrol.data.ws
|
||||
|
||||
import com.m57.hermescontrol.data.model.ConnectionOperationSnapshot
|
||||
import com.m57.hermescontrol.ui.chat.TodoItem
|
||||
|
||||
/** Parsed WebSocket events emitted by [HermesWsClient]. */
|
||||
@@ -191,6 +192,14 @@ sealed class WsEvent {
|
||||
val sessionId: String? = null,
|
||||
) : WsEvent()
|
||||
|
||||
data class ConnectionRequest(
|
||||
val snapshot: ConnectionOperationSnapshot,
|
||||
) : WsEvent()
|
||||
|
||||
data class ConnectionUpdate(
|
||||
val snapshot: ConnectionOperationSnapshot,
|
||||
) : WsEvent()
|
||||
|
||||
// ── Status ───────────────────────────────────────────────────────────
|
||||
|
||||
data class StatusUpdate(
|
||||
|
||||
@@ -101,6 +101,9 @@ object WsMethods {
|
||||
|
||||
// ── Connectors (issue #1091) ───────────────────────────────────────────
|
||||
|
||||
const val CONNECTION_RESPOND = "connection.respond"
|
||||
const val CONNECTORS_OPERATION_WAKE = "connectors.operation.wake"
|
||||
|
||||
/** List available third-party connectors and authorization state for a session. */
|
||||
const val CONNECTORS_LIST = "connectors.list"
|
||||
|
||||
|
||||
Reference in new issue
Block a user