Browse Source

修复移动聊天界面 的细节问题

newdev_shunjiawei
liwei1dao 7 months ago
parent
commit
ac69a14296
  1. 35
      lib/modules/mobile_elf/controllers/mobile_elf_controller.dart
  2. 15
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  3. 11
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/StsAgent.kt

35
lib/modules/mobile_elf/controllers/mobile_elf_controller.dart

@ -57,6 +57,8 @@ class MobileElfController extends GetxController {
// BLE/AI 唤醒触发的会话(非通话、非PTT模式下的唤醒) // BLE/AI 唤醒触发的会话(非通话、非PTT模式下的唤醒)
bool _bleWakeupActive = false; bool _bleWakeupActive = false;
final _storage = GetStorage(); final _storage = GetStorage();
// 滚动:layout 变化前是否已在底部
bool _atBottom = true;
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
// 生命周期 // 生命周期
@ -172,8 +174,8 @@ class MobileElfController extends GetxController {
case AgentServiceEventType.ttsCompleted: case AgentServiceEventType.ttsCompleted:
if (!sessionActive) break; if (!sessionActive) break;
_pttResultPending = false; // PTT 一轮结束 _pttResultPending = false;
_bleWakeupActive = false; // BLE 唤醒一轮结束 _endBleWakeup(); // BLE 唤醒一轮结束,清理 native PTT 状态
if (_inCallMode) { if (_inCallMode) {
status.value = MobileElfStatus.recording; status.value = MobileElfStatus.recording;
micAlive.value = true; micAlive.value = true;
@ -186,16 +188,16 @@ class MobileElfController extends GetxController {
case AgentServiceEventType.ttsCanceled: case AgentServiceEventType.ttsCanceled:
case AgentServiceEventType.ttsStopped: case AgentServiceEventType.ttsStopped:
if (!sessionActive) break; if (!sessionActive) break;
_pttResultPending = false; // TTS 被打断也视为结束 _pttResultPending = false;
_bleWakeupActive = false; _endBleWakeup();
status.value = _inCallMode ? MobileElfStatus.recording : MobileElfStatus.idle; status.value = _inCallMode ? MobileElfStatus.recording : MobileElfStatus.idle;
micAlive.value = _inCallMode; micAlive.value = _inCallMode;
break; break;
case AgentServiceEventType.error: case AgentServiceEventType.error:
if (!sessionActive) break; if (!sessionActive) break;
_pttResultPending = false; // 出错时结束等待 _pttResultPending = false;
_bleWakeupActive = false; _endBleWakeup();
final msg = event.data['message']?.toString() ?? '未知错误'; final msg = event.data['message']?.toString() ?? '未知错误';
status.value = MobileElfStatus.error; status.value = MobileElfStatus.error;
errorMsg.value = msg; errorMsg.value = msg;
@ -305,6 +307,13 @@ class MobileElfController extends GetxController {
if (pttRecording.value || _pttStarting) return; if (pttRecording.value || _pttStarting) return;
_pttStarting = true; _pttStarting = true;
_checkHeartbeat(); _checkHeartbeat();
// BLE 唤醒/PTT 等待结果期间用户按下 PTT,先中断当前会话(同 startCall 逻辑)
if (_bleWakeupActive || _pttResultPending) {
_pttResultPending = false;
_bleWakeupActive = false;
await AgentService.interruptCurrentResponse();
await AgentService.stopConversation();
}
final granted = await PermissionUtil.instance.requestPermission( final granted = await PermissionUtil.instance.requestPermission(
permissionType: Permission.microphone, permissionType: Permission.microphone,
permissionName: '麦克风', permissionName: '麦克风',
@ -386,9 +395,21 @@ class MobileElfController extends GetxController {
_scrollToBottom(); _scrollToBottom();
} }
/// BLE 唤醒会话标志清除
/// Native 状态(pttRecording 等)由 StsAgent.startPtt() 的重入逻辑懒清理,
/// 不在此调用 stopConversation(),避免与新会话产生竞态。
void _endBleWakeup() {
_bleWakeupActive = false;
}
void _scrollToBottom() { void _scrollToBottom() {
// layout 变化前捕获是否在底部,避免新内容撑开后差值变大误判为"已滚离底部"
if (scrollController.hasClients) {
final pos = scrollController.position;
_atBottom = pos.maxScrollExtent - pos.pixels <= 80;
}
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (scrollController.hasClients) { if (scrollController.hasClients && _atBottom) {
scrollController.animateTo( scrollController.animateTo(
scrollController.position.maxScrollExtent, scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),

15
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -225,6 +225,7 @@ object AgentService : CoroutineScope {
// STS 模式:跳过 Azure/OpenAI 初始化,直接启动 StsAgent // STS 模式:跳过 Azure/OpenAI 初始化,直接启动 StsAgent
if (agentMode == "sts") { if (agentMode == "sts") {
this.context = appContext.applicationContext this.context = appContext.applicationContext
audioPlayer = AudioPlayer(context) // STS 模式也需要音频播放器(提示音等)
val stsConfig = StsConfig( val stsConfig = StsConfig(
productId = config["stsProductId"]?.toString() ?: "", productId = config["stsProductId"]?.toString() ?: "",
productKey = config["stsProductKey"]?.toString() ?: "", productKey = config["stsProductKey"]?.toString() ?: "",
@ -236,7 +237,13 @@ object AgentService : CoroutineScope {
stsAgent = StsAgent( stsAgent = StsAgent(
context = context, context = context,
config = stsConfig, config = stsConfig,
onEvent = { name, data -> sendEvent(name, data) }, onEvent = { name, data ->
// BLE 唤醒:AI 开始回复时关闭 BLE 编解码器(说明用户已说完)
if (currentRecognitionMode == "ble_wakeup" && name == "assistant_response") {
BleService.closeCodec()
}
sendEvent(name, data)
},
onCredentialsFetched = { deviceId, deviceSecret, deviceNo -> onCredentialsFetched = { deviceId, deviceSecret, deviceNo ->
sendEvent("sts_credentials", mapOf( sendEvent("sts_credentials", mapOf(
"deviceId" to deviceId, "deviceId" to deviceId,
@ -685,10 +692,14 @@ object AgentService : CoroutineScope {
*/ */
fun startRecognition(isExternalActive: Boolean, mode: String = "normal"): Boolean { fun startRecognition(isExternalActive: Boolean, mode: String = "normal"): Boolean {
if (agentMode == "sts") { if (agentMode == "sts") {
currentRecognitionMode = mode // 记录模式供 onEvent 回调使用
when (mode) { when (mode) {
"push_to_talk" -> stsAgent?.startPtt() "push_to_talk" -> stsAgent?.startPtt()
"phone_call" -> stsAgent?.startCall() "phone_call" -> stsAgent?.startCall()
"ble_wakeup" -> stsAgent?.startCall() // BLE 唤醒:进入通话模式(VAD 自动检测) "ble_wakeup" -> {
audioPlayer?.playAudio(R.raw.start) // 立即播提示音,不等 WS 连接完成
stsAgent?.startPtt()
}
else -> stsAgent?.startPtt() // 普通模式默认 PTT else -> stsAgent?.startPtt() // 普通模式默认 PTT
} }
return true return true

11
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/StsAgent.kt

@ -289,7 +289,16 @@ class StsAgent(
/** PTT 模式:立即连 WS 并开始录音 */ /** PTT 模式:立即连 WS 并开始录音 */
fun startPtt() { fun startPtt() {
if (pttRecording) return if (pttRecording) {
if (forwardingAudio) return // 真正在录音中,忽略重复调用
// forwardingAudio=false:WS 已关闭,正处于 TTS 播报等待阶段(如 BLE 唤醒等待 AI 回复期间)
// 调用方(BleAgent)已通过 interruptCurrentResponse() 中断了 TTS,
// 此处只做静默状态重置,不再重复发出 tts_canceled 事件
Log.i(TAG, "[YIDONG] startPtt: 静默重置旧会话,重新启动")
stopMicHard()
sessionEnded = false
pttRecording = false
}
if (ttsPlaying) interruptTts() if (ttsPlaying) interruptTts()
if (!credentialReady) { emitError("移动精灵凭证未就绪"); return } if (!credentialReady) { emitError("移动精灵凭证未就绪"); return }
Log.i(TAG, "[YIDONG] startPtt: 启动PTT模式 deviceNo=${config.deviceNo} agentId=${config.agentId}") Log.i(TAG, "[YIDONG] startPtt: 启动PTT模式 deviceNo=${config.deviceNo} agentId=${config.agentId}")

Loading…
Cancel
Save