feat(设置页): 添加 Agent 过滤配置字段

- 在设置页添加 Agent ID 输入框,用于过滤流式播报消息
- 新增字符串资源 label_agent_filter 和 hint_agent_id
- 在 MQTT 管理器中处理 soul2user 主题消息,根据配置的 demp_id 进行过滤
- 更新 README 文档结构,将详细技术说明移至 technique.md 文件
This commit is contained in:
2026-03-16 10:58:19 +08:00
parent d8e875793d
commit 1e7a45f19c
7 changed files with 282 additions and 104 deletions

View File

@@ -28,6 +28,8 @@ class MqttManager(
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job)
private var reconnectJob: Job? = null
private val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
private val agentDempIdKey = "agent_demp_id"
// Streaming text buffer
private val speechBuffer = StringBuilder()
@@ -42,6 +44,8 @@ class MqttManager(
private var interruptedLanguage: TtsRequest.Language? = null
private var lastStreamLangCode: String? = null
private val ttsLanguageMap = mutableMapOf<TtsRequest, TtsRequest.Language>()
private var currentStreamSessionId: String? = null
private var currentStreamMessageId: String? = null
init {
try {
@@ -50,6 +54,7 @@ class MqttManager(
override fun connectComplete(reconnect: Boolean, serverURI: String?) {
Log.i(TAG, "MQTT connection complete. Reconnect: $reconnect")
subscribeTopic("robot/cmd")
subscribeTopic("soul2user")
updateConnectionStatus(true)
}
@@ -192,7 +197,11 @@ class MqttManager(
}
try {
val obj = JSONObject(trimmed)
handleJsonCommand(obj)
if (topic == "soul2user") {
handleSoulStream(obj)
} else {
handleJsonCommand(obj)
}
} catch (e: Exception) {
Log.e(TAG, "Invalid JSON payload: $payload", e)
val ttsRequest = TtsRequest.create("指令格式错误,请检查指令格式", false, language = TtsRequest.Language.ZH_CN)
@@ -200,6 +209,63 @@ class MqttManager(
}
}
private fun handleSoulStream(obj: JSONObject) {
val configuredDempId = prefs.getString(agentDempIdKey, "").orEmpty().trim()
val dempId = obj.optString("demp_id", "").trim()
if (configuredDempId.isNotEmpty() && configuredDempId != dempId) {
Log.i(TAG, "Stream ignored: demp_id mismatch ($dempId)")
return
}
val sessionId = obj.optString("session_id", "").trim()
val messageId = obj.optString("message_id", "").trim()
ensureStreamContext(sessionId, messageId)
val text = obj.optString("text", "")
val isFinal = obj.optBoolean("is_final", false)
val lang = obj.optString("lang", "").trim()
scope.launch(Dispatchers.Main) {
(context as? MainActivity)?.markSpeechTaskActive()
}
processStreamText(text, lang)
if (isFinal) {
flushStreamRemainder(lang)
currentStreamSessionId = null
currentStreamMessageId = null
}
}
private fun ensureStreamContext(sessionId: String, messageId: String) {
if (sessionId.isEmpty() && messageId.isEmpty()) {
return
}
val sessionChanged = currentStreamSessionId != null && sessionId.isNotEmpty() && sessionId != currentStreamSessionId
val messageChanged = currentStreamMessageId != null && messageId.isNotEmpty() && messageId != currentStreamMessageId
if (currentStreamSessionId == null && currentStreamMessageId == null) {
currentStreamSessionId = sessionId.ifEmpty { null }
currentStreamMessageId = messageId.ifEmpty { null }
return
}
if (sessionChanged || messageChanged) {
stopTts()
currentStreamSessionId = sessionId.ifEmpty { null }
currentStreamMessageId = messageId.ifEmpty { null }
return
}
if (currentStreamSessionId == null && sessionId.isNotEmpty()) {
currentStreamSessionId = sessionId
}
if (currentStreamMessageId == null && messageId.isNotEmpty()) {
currentStreamMessageId = messageId
}
}
private fun flushStreamRemainder(langCode: String?) {
val remaining = speechBuffer.toString().trim()
if (remaining.isNotEmpty()) {
speechBuffer.setLength(0)
speak(remaining, langCode)
}
}
private fun handleJsonCommand(obj: JSONObject) {
val action = obj.optString("action", obj.optString("cmd", obj.optString("type", ""))).lowercase()
when (action) {

View File

@@ -33,6 +33,7 @@ class SettingsActivity : AppCompatActivity() {
private val liveKitRoomKey = "livekit_room"
private val liveKitTokenKey = "livekit_token"
private val liveKitEnabledKey = "livekit_enabled"
private val agentDempIdKey = "agent_demp_id"
private val liveKitUrlDefault = "ws://192.168.2.236:7880"
private val liveKitRoomDefault = "temi-room"
@@ -47,6 +48,8 @@ class SettingsActivity : AppCompatActivity() {
prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
val savedIp = prefs.getString("network_ip", "")
binding.etIpAddress.setText(savedIp)
val savedDempId = prefs.getString(agentDempIdKey, "")
binding.etAgentDempId.setText(savedDempId)
val savedLiveKitUrl = prefs.getString(liveKitUrlKey, resolveLiveKitUrl())
val savedLiveKitRoom = prefs.getString(liveKitRoomKey, liveKitRoomDefault)
val savedLiveKitToken = prefs.getString(liveKitTokenKey, "")
@@ -66,8 +69,12 @@ class SettingsActivity : AppCompatActivity() {
binding.btnSave.setOnClickListener {
hideKeyboard()
val ip = binding.etIpAddress.text.toString().trim()
val dempId = binding.etAgentDempId.text?.toString()?.trim().orEmpty()
if (ip.isNotEmpty()) {
prefs.edit().putString("network_ip", ip).apply()
prefs.edit()
.putString("network_ip", ip)
.putString(agentDempIdKey, dempId)
.apply()
Toast.makeText(this, getString(R.string.msg_ip_saved, ip), Toast.LENGTH_SHORT).show()
finish()
} else {

View File

@@ -102,6 +102,22 @@
android:textColor="@color/text_primary" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.App.TextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:hint="@string/hint_agent_id">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etAgentDempId"
android:layout_width="match_parent"
android:layout_height="72dp"
android:inputType="text"
android:textSize="18sp"
android:textColor="@color/text_primary" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btnSave"
style="@style/Widget.App.Button"

View File

@@ -93,6 +93,22 @@
android:textColor="@color/text_primary" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.App.TextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:hint="@string/hint_agent_id">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etAgentDempId"
android:layout_width="match_parent"
android:layout_height="72dp"
android:inputType="text"
android:textSize="18sp"
android:textColor="@color/text_primary" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btnSave"
style="@style/Widget.App.Button"

View File

@@ -16,6 +16,8 @@
<string name="btn_restart_app">长按重启应用</string>
<string name="label_location_config">当前位置</string>
<string name="hint_current_location">请选择当前地点</string>
<string name="label_agent_filter">Agent 过滤</string>
<string name="hint_agent_id">请输入 demp_id留空接收全部</string>
<string name="location_unknown">未知</string>
<string name="label_livekit_config">LiveKit 配置</string>
<string name="hint_livekit_url">请输入 LiveKit 地址 (wss://your-host)</string>