mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
refactor: dedupe android talk config parsing
This commit is contained in:
@@ -4,9 +4,16 @@ import ai.openclaw.app.normalizeMainKey
|
|||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import kotlinx.serialization.json.booleanOrNull
|
import kotlinx.serialization.json.booleanOrNull
|
||||||
import kotlinx.serialization.json.contentOrNull
|
import kotlinx.serialization.json.contentOrNull
|
||||||
|
|
||||||
|
internal data class TalkProviderConfigSelection(
|
||||||
|
val provider: String,
|
||||||
|
val config: JsonObject,
|
||||||
|
val normalizedPayload: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
internal data class TalkModeGatewayConfigState(
|
internal data class TalkModeGatewayConfigState(
|
||||||
val activeProvider: String,
|
val activeProvider: String,
|
||||||
val normalizedPayload: Boolean,
|
val normalizedPayload: Boolean,
|
||||||
@@ -22,6 +29,8 @@ internal data class TalkModeGatewayConfigState(
|
|||||||
)
|
)
|
||||||
|
|
||||||
internal object TalkModeGatewayConfigParser {
|
internal object TalkModeGatewayConfigParser {
|
||||||
|
private const val defaultTalkProvider = "elevenlabs"
|
||||||
|
|
||||||
fun parse(
|
fun parse(
|
||||||
config: JsonObject?,
|
config: JsonObject?,
|
||||||
defaultProvider: String,
|
defaultProvider: String,
|
||||||
@@ -32,7 +41,7 @@ internal object TalkModeGatewayConfigParser {
|
|||||||
envKey: String?,
|
envKey: String?,
|
||||||
): TalkModeGatewayConfigState {
|
): TalkModeGatewayConfigState {
|
||||||
val talk = config?.get("talk").asObjectOrNull()
|
val talk = config?.get("talk").asObjectOrNull()
|
||||||
val selection = TalkModeManager.selectTalkProviderConfig(talk)
|
val selection = selectTalkProviderConfig(talk)
|
||||||
val activeProvider = selection?.provider ?: defaultProvider
|
val activeProvider = selection?.provider ?: defaultProvider
|
||||||
val activeConfig = selection?.config
|
val activeConfig = selection?.config
|
||||||
val sessionCfg = config?.get("session").asObjectOrNull()
|
val sessionCfg = config?.get("session").asObjectOrNull()
|
||||||
@@ -48,7 +57,7 @@ internal object TalkModeGatewayConfigParser {
|
|||||||
activeConfig?.get("outputFormat")?.asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() }
|
activeConfig?.get("outputFormat")?.asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() }
|
||||||
val key = activeConfig?.get("apiKey")?.asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() }
|
val key = activeConfig?.get("apiKey")?.asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() }
|
||||||
val interrupt = talk?.get("interruptOnSpeech")?.asBooleanOrNull()
|
val interrupt = talk?.get("interruptOnSpeech")?.asBooleanOrNull()
|
||||||
val silenceTimeoutMs = TalkModeManager.resolvedSilenceTimeoutMs(talk)
|
val silenceTimeoutMs = resolvedSilenceTimeoutMs(talk)
|
||||||
|
|
||||||
return TalkModeGatewayConfigState(
|
return TalkModeGatewayConfigState(
|
||||||
activeProvider = activeProvider,
|
activeProvider = activeProvider,
|
||||||
@@ -91,6 +100,48 @@ internal object TalkModeGatewayConfigParser {
|
|||||||
interruptOnSpeech = null,
|
interruptOnSpeech = null,
|
||||||
silenceTimeoutMs = TalkDefaults.defaultSilenceTimeoutMs,
|
silenceTimeoutMs = TalkDefaults.defaultSilenceTimeoutMs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun selectTalkProviderConfig(talk: JsonObject?): TalkProviderConfigSelection? {
|
||||||
|
if (talk == null) return null
|
||||||
|
selectResolvedTalkProviderConfig(talk)?.let { return it }
|
||||||
|
val rawProvider = talk["provider"].asStringOrNull()
|
||||||
|
val rawProviders = talk["providers"].asObjectOrNull()
|
||||||
|
val hasNormalizedPayload = rawProvider != null || rawProviders != null
|
||||||
|
if (hasNormalizedPayload) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return TalkProviderConfigSelection(
|
||||||
|
provider = defaultTalkProvider,
|
||||||
|
config = talk,
|
||||||
|
normalizedPayload = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resolvedSilenceTimeoutMs(talk: JsonObject?): Long {
|
||||||
|
val fallback = TalkDefaults.defaultSilenceTimeoutMs
|
||||||
|
val primitive = talk?.get("silenceTimeoutMs") as? JsonPrimitive ?: return fallback
|
||||||
|
if (primitive.isString) return fallback
|
||||||
|
val timeout = primitive.content.toDoubleOrNull() ?: return fallback
|
||||||
|
if (timeout <= 0 || timeout % 1.0 != 0.0 || timeout > Long.MAX_VALUE.toDouble()) {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return timeout.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectResolvedTalkProviderConfig(talk: JsonObject): TalkProviderConfigSelection? {
|
||||||
|
val resolved = talk["resolved"].asObjectOrNull() ?: return null
|
||||||
|
val providerId = normalizeTalkProviderId(resolved["provider"].asStringOrNull()) ?: return null
|
||||||
|
return TalkProviderConfigSelection(
|
||||||
|
provider = providerId,
|
||||||
|
config = resolved["config"].asObjectOrNull() ?: buildJsonObject {},
|
||||||
|
normalizedPayload = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun normalizeTalkProviderId(raw: String?): String? {
|
||||||
|
val trimmed = raw?.trim()?.lowercase().orEmpty()
|
||||||
|
return trimmed.takeIf { it.isNotEmpty() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun normalizeTalkAliasKey(value: String): String =
|
private fun normalizeTalkAliasKey(value: String): String =
|
||||||
|
|||||||
@@ -64,54 +64,6 @@ class TalkModeManager(
|
|||||||
private const val chatFinalWaitWithSubscribeMs = 45_000L
|
private const val chatFinalWaitWithSubscribeMs = 45_000L
|
||||||
private const val chatFinalWaitWithoutSubscribeMs = 6_000L
|
private const val chatFinalWaitWithoutSubscribeMs = 6_000L
|
||||||
private const val maxCachedRunCompletions = 128
|
private const val maxCachedRunCompletions = 128
|
||||||
|
|
||||||
internal data class TalkProviderConfigSelection(
|
|
||||||
val provider: String,
|
|
||||||
val config: JsonObject,
|
|
||||||
val normalizedPayload: Boolean,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun normalizeTalkProviderId(raw: String?): String? {
|
|
||||||
val trimmed = raw?.trim()?.lowercase().orEmpty()
|
|
||||||
return trimmed.takeIf { it.isNotEmpty() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun selectResolvedTalkProviderConfig(talk: JsonObject): TalkProviderConfigSelection? {
|
|
||||||
val resolved = talk["resolved"].asObjectOrNull() ?: return null
|
|
||||||
val providerId = normalizeTalkProviderId(resolved["provider"].asStringOrNull()) ?: return null
|
|
||||||
return TalkProviderConfigSelection(
|
|
||||||
provider = providerId,
|
|
||||||
config = resolved["config"].asObjectOrNull() ?: buildJsonObject {},
|
|
||||||
normalizedPayload = true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun selectTalkProviderConfig(talk: JsonObject?): TalkProviderConfigSelection? {
|
|
||||||
if (talk == null) return null
|
|
||||||
selectResolvedTalkProviderConfig(talk)?.let { return it }
|
|
||||||
val rawProvider = talk["provider"].asStringOrNull()
|
|
||||||
val rawProviders = talk["providers"].asObjectOrNull()
|
|
||||||
val hasNormalizedPayload = rawProvider != null || rawProviders != null
|
|
||||||
if (hasNormalizedPayload) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return TalkProviderConfigSelection(
|
|
||||||
provider = defaultTalkProvider,
|
|
||||||
config = talk,
|
|
||||||
normalizedPayload = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun resolvedSilenceTimeoutMs(talk: JsonObject?): Long {
|
|
||||||
val fallback = TalkDefaults.defaultSilenceTimeoutMs
|
|
||||||
val primitive = talk?.get("silenceTimeoutMs") as? JsonPrimitive ?: return fallback
|
|
||||||
if (primitive.isString) return fallback
|
|
||||||
val timeout = primitive.content.toDoubleOrNull() ?: return fallback
|
|
||||||
if (timeout <= 0 || timeout % 1.0 != 0.0 || timeout > Long.MAX_VALUE.toDouble()) {
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
return timeout.toLong()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mainHandler = Handler(Looper.getMainLooper())
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class TalkModeConfigContractTest {
|
|||||||
@Test
|
@Test
|
||||||
fun selectionFixtures() {
|
fun selectionFixtures() {
|
||||||
for (fixture in loadFixtures().selectionCases) {
|
for (fixture in loadFixtures().selectionCases) {
|
||||||
val selection = TalkModeManager.selectTalkProviderConfig(fixture.talk)
|
val selection = TalkModeGatewayConfigParser.selectTalkProviderConfig(fixture.talk)
|
||||||
val expected = fixture.expectedSelection
|
val expected = fixture.expectedSelection
|
||||||
if (expected == null) {
|
if (expected == null) {
|
||||||
assertNull(fixture.id, selection)
|
assertNull(fixture.id, selection)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class TalkModeConfigParsingTest {
|
|||||||
)
|
)
|
||||||
.jsonObject
|
.jsonObject
|
||||||
|
|
||||||
val selection = TalkModeManager.selectTalkProviderConfig(talk)
|
val selection = TalkModeGatewayConfigParser.selectTalkProviderConfig(talk)
|
||||||
assertNotNull(selection)
|
assertNotNull(selection)
|
||||||
assertEquals("elevenlabs", selection?.provider)
|
assertEquals("elevenlabs", selection?.provider)
|
||||||
assertTrue(selection?.normalizedPayload == true)
|
assertTrue(selection?.normalizedPayload == true)
|
||||||
@@ -61,7 +61,7 @@ class TalkModeConfigParsingTest {
|
|||||||
)
|
)
|
||||||
.jsonObject
|
.jsonObject
|
||||||
|
|
||||||
val selection = TalkModeManager.selectTalkProviderConfig(talk)
|
val selection = TalkModeGatewayConfigParser.selectTalkProviderConfig(talk)
|
||||||
assertEquals(null, selection)
|
assertEquals(null, selection)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ class TalkModeConfigParsingTest {
|
|||||||
)
|
)
|
||||||
.jsonObject
|
.jsonObject
|
||||||
|
|
||||||
val selection = TalkModeManager.selectTalkProviderConfig(talk)
|
val selection = TalkModeGatewayConfigParser.selectTalkProviderConfig(talk)
|
||||||
assertEquals(null, selection)
|
assertEquals(null, selection)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ class TalkModeConfigParsingTest {
|
|||||||
)
|
)
|
||||||
.jsonObject
|
.jsonObject
|
||||||
|
|
||||||
val selection = TalkModeManager.selectTalkProviderConfig(talk)
|
val selection = TalkModeGatewayConfigParser.selectTalkProviderConfig(talk)
|
||||||
assertEquals(null, selection)
|
assertEquals(null, selection)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ class TalkModeConfigParsingTest {
|
|||||||
put("apiKey", legacyApiKey) // pragma: allowlist secret
|
put("apiKey", legacyApiKey) // pragma: allowlist secret
|
||||||
}
|
}
|
||||||
|
|
||||||
val selection = TalkModeManager.selectTalkProviderConfig(talk)
|
val selection = TalkModeGatewayConfigParser.selectTalkProviderConfig(talk)
|
||||||
assertNotNull(selection)
|
assertNotNull(selection)
|
||||||
assertEquals("elevenlabs", selection?.provider)
|
assertEquals("elevenlabs", selection?.provider)
|
||||||
assertTrue(selection?.normalizedPayload == false)
|
assertTrue(selection?.normalizedPayload == false)
|
||||||
@@ -130,25 +130,34 @@ class TalkModeConfigParsingTest {
|
|||||||
fun readsConfiguredSilenceTimeoutMs() {
|
fun readsConfiguredSilenceTimeoutMs() {
|
||||||
val talk = buildJsonObject { put("silenceTimeoutMs", 1500) }
|
val talk = buildJsonObject { put("silenceTimeoutMs", 1500) }
|
||||||
|
|
||||||
assertEquals(1500L, TalkModeManager.resolvedSilenceTimeoutMs(talk))
|
assertEquals(1500L, TalkModeGatewayConfigParser.resolvedSilenceTimeoutMs(talk))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun defaultsSilenceTimeoutMsWhenMissing() {
|
fun defaultsSilenceTimeoutMsWhenMissing() {
|
||||||
assertEquals(TalkDefaults.defaultSilenceTimeoutMs, TalkModeManager.resolvedSilenceTimeoutMs(null))
|
assertEquals(
|
||||||
|
TalkDefaults.defaultSilenceTimeoutMs,
|
||||||
|
TalkModeGatewayConfigParser.resolvedSilenceTimeoutMs(null),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun defaultsSilenceTimeoutMsWhenInvalid() {
|
fun defaultsSilenceTimeoutMsWhenInvalid() {
|
||||||
val talk = buildJsonObject { put("silenceTimeoutMs", 0) }
|
val talk = buildJsonObject { put("silenceTimeoutMs", 0) }
|
||||||
|
|
||||||
assertEquals(TalkDefaults.defaultSilenceTimeoutMs, TalkModeManager.resolvedSilenceTimeoutMs(talk))
|
assertEquals(
|
||||||
|
TalkDefaults.defaultSilenceTimeoutMs,
|
||||||
|
TalkModeGatewayConfigParser.resolvedSilenceTimeoutMs(talk),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun defaultsSilenceTimeoutMsWhenString() {
|
fun defaultsSilenceTimeoutMsWhenString() {
|
||||||
val talk = buildJsonObject { put("silenceTimeoutMs", "1500") }
|
val talk = buildJsonObject { put("silenceTimeoutMs", "1500") }
|
||||||
|
|
||||||
assertEquals(TalkDefaults.defaultSilenceTimeoutMs, TalkModeManager.resolvedSilenceTimeoutMs(talk))
|
assertEquals(
|
||||||
|
TalkDefaults.defaultSilenceTimeoutMs,
|
||||||
|
TalkModeGatewayConfigParser.resolvedSilenceTimeoutMs(talk),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user