diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 269ff6b20..8b3012b08 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -49,6 +49,7 @@ + initialize() async { if (!isInitialized) { - await getAccessToken(); - await connectToSpotifyRemote(); - isInitialized = true; final _env = _storage.read("ENV") as Map; SPOTIFY_CLIENT_ID = _env['SPOTIFY_CLIENT_ID'] ?? ''; SPOTIFY_REDIRECT_URL = _env['SPOTIFY_REDIRECT_URL'] ?? ''; - printInfo(info: "SpotifService----------- initialize"); - } + printInfo( + info: + "SpotifService----------- initialize SPOTIFY_CLIENT_ID:${SPOTIFY_CLIENT_ID} SPOTIFY_REDIRECT_URL:${SPOTIFY_REDIRECT_URL}"); + await getAccessToken(); + await connectToSpotifyRemote(); + isInitialized = true; + } return true; } diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart index 4ce3bf80b..7776de777 100644 --- a/lib/modules/chat/controllers/chat_controller.dart +++ b/lib/modules/chat/controllers/chat_controller.dart @@ -5,6 +5,7 @@ import 'package:deep_voice/data/services/spotify_service.dart'; import 'package:get/get.dart'; import 'package:flutter/widgets.dart'; import 'package:get_storage/get_storage.dart'; +import 'package:haptic_feedback/haptic_feedback.dart'; import 'package:just_audio/just_audio.dart'; import 'package:chat_storage/chat_storage.dart'; import 'package:path/path.dart'; @@ -112,8 +113,9 @@ class ChatController extends GetxController { // 获取Agent final Agent? foundAgent = AgentProvider.getAgentById(agentId); if (foundAgent == null) { - Logger.error('无法找到Agent: $agentId'); Get.back(); + Logger.error('无法找到Agent: $agentId'); + return; } @@ -402,18 +404,23 @@ class ChatController extends GetxController { messages: messageHistory, systemPrompt: agent.systemPrompt, ); - + Logger.info( + 'liwei----------- 发送用户提问 ${text}', + ); // 订阅响应流 _aiResponseSubscription = responseStream.listen( (chunk) { if (_shouldCancelAiResponse) return; Logger.info('chat ${chunk}'); if (chunk is Map) { + Logger.info( + 'liwei-----------收到卡片消息 ${chunk}', + ); _updateIntermediateMessage(meta: chunk); if (chunk['card_spotify'] != null) { play(chunk['card_spotify']); } - } else { + } else if (chunk is String) { fullResponse += chunk; // 更新中间状态的消息内容 @@ -421,11 +428,15 @@ class ChatController extends GetxController { // 如果TTS启用,使用流式处理发送到TTS if (isTtsEnabled.value) { - _ttsService.speakStream(chunk); + String str = chunk.replaceAll('*', ''); + _ttsService.speakStream(str); } } }, onDone: () { + Logger.info( + 'liwei-----------结束当前问题 ${text}', + ); if (_shouldCancelAiResponse) return; // 完成响应,将isIntermediate设为false @@ -458,6 +469,10 @@ class ChatController extends GetxController { final recentMessages = messages.where((m) => !m.isIntermediate).toList(); final startIndex = recentMessages.length > 10 ? recentMessages.length - 10 : 0; + messageHistory.add({ + 'role': 'user', + 'content': '我当前所在城市是$_city,我的经纬度是$_latLng', + }); for (int i = startIndex; i < recentMessages.length; i++) { final m = recentMessages[i]; @@ -465,12 +480,6 @@ class ChatController extends GetxController { 'role': m.role, 'content': m.content, }); - if (i == recentMessages.length - 2) { - messageHistory.add({ - 'role': 'user', - 'content': '当需要获取我的位置时,我当前在$_city,我的经纬度是$_latLng', - }); - } } return messageHistory; @@ -586,6 +595,8 @@ class ChatController extends GetxController { if (isHoldSpeak.value) return; _voiceInteractionService.pauseVoiceInteraction(); + // 中断当前响应 + _interruptCurrentResponse("发送新消息,中断当前响应"); try { final recognitionStream = await _asrService.startContinuousRecognition(); @@ -594,6 +605,7 @@ class ChatController extends GetxController { isHoldSpeak.value = true; recognizedText.value = ''; finalResult = ''; + Haptics.vibrate(HapticsType.success); // 监听识别事件 _recognitionSubscription = recognitionStream.listen( @@ -616,13 +628,16 @@ class ChatController extends GetxController { _recognitionSubscription?.cancel(); _recognitionSubscription = null; + String sendText = ''; + final intermediateIndex = messages.indexWhere((m) => m.isIntermediate && m.role == 'user'); if (intermediateIndex >= 0) { + sendText = messages[intermediateIndex].content; messages.removeAt(intermediateIndex); } - sendMessage(finalResult); + sendMessage(sendText); recognizedText.value = ''; finalResult = ''; diff --git a/lib/modules/chat/views/animated_gradient_background.dart b/lib/modules/chat/views/animated_gradient_background.dart index 181295cea..3a96b8bc5 100644 --- a/lib/modules/chat/views/animated_gradient_background.dart +++ b/lib/modules/chat/views/animated_gradient_background.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; class AnimatedGradientBackground extends StatefulWidget { - const AnimatedGradientBackground({super.key}); + final Widget child; + + const AnimatedGradientBackground(this.child, {super.key}); @override State createState() => @@ -58,8 +60,10 @@ class _AnimatedGradientBackgroundState extends State end: _endAnimation.value!, ), ), + child: child, ); }, + child: widget.child, ); } } diff --git a/lib/modules/chat/views/chat_view.dart b/lib/modules/chat/views/chat_view.dart index ed904d4e6..157536943 100644 --- a/lib/modules/chat/views/chat_view.dart +++ b/lib/modules/chat/views/chat_view.dart @@ -513,18 +513,23 @@ class ChatView extends GetView { // 构建语音输入面板 Widget _buildVoiceInputPanel(bool isDarkMode) { - return Container( - decoration: BoxDecoration( - color: isDarkMode ? AppColors.cardBackground : Colors.white, - ), - child: SafeArea( + return AnimatedGradientBackground( + SafeArea( child: Column( children: [ Row( children: [], ), Expanded( - child: AnimatedGradientBackground(), + child: Container( + width: 200.w, + height: 200.w, + decoration: BoxDecoration( + color: + isDarkMode ? Colors.white.withOpacity(0.1) : Colors.white, + shape: BoxShape.circle, + ), + ), ), SizedBox( height: 80.h, diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt index b43013e0a..e2796ee85 100644 --- a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt +++ b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt @@ -168,7 +168,7 @@ class MCPSubClient( private fun processToolsList(toolsResult: ListToolsResult?) { try { // 将MCP工具列表转换为ToolUnion列表 - tools = toolsResult?.tools?.filter { tool -> config.tools == null || config.tools.contains(tool.name) == true + tools = toolsResult?.tools?.filter { tool -> config.tools.isNullOrEmpty() || config.tools.contains(tool.name) }?.map { tool -> // 转换为ToolUnion格式 ToolUnion.ofTool( diff --git a/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock index fd40a1b74..a2d8b75c8 100644 Binary files a/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties b/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties index 8a9b74646..816b483f3 100644 --- a/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties +++ b/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties @@ -1,2 +1,2 @@ -#Tue Apr 15 09:36:50 IST 2025 -gradle.version=8.10 +#Thu Apr 24 10:52:53 CST 2025 +gradle.version=8.9 diff --git a/local_plugins/opus/example/lib/main.dart b/local_plugins/opus/example/lib/main.dart index 19730f7fe..a73c5a09a 100644 --- a/local_plugins/opus/example/lib/main.dart +++ b/local_plugins/opus/example/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'package:opus/opus.dart'; + +import '../../lib/opus.dart'; void main() { runApp(const OpusExampleApp()); @@ -92,13 +93,13 @@ class _OpusExamplePageState extends State { const int frequency = 440; // 440Hz - A4音符 const int durationMs = 20; // 20ms,按照常规frameSize=960 const int frameSize = 960; // 对应20ms@48kHz - + // 创建PCM数据缓冲区,16位有符号整数,每采样2字节,双声道 final int samplesToGenerate = (sampleRate * durationMs / 1000).round(); final int bufferSize = samplesToGenerate * channels * 2; // 2字节/样本 - + final byteBuffer = ByteData(bufferSize); - + // 生成正弦波 for (int i = 0; i < samplesToGenerate; i++) { // 计算当前时间点 @@ -107,18 +108,17 @@ class _OpusExamplePageState extends State { final double amplitude = sin(2 * 3.14159265359 * frequency * t); // 缩放到16位有符号整数范围 (-32768 to 32767) final int sample = (amplitude * 32767).round(); - + // 为所有声道写入相同的值 for (int channel = 0; channel < channels; channel++) { final int offset = (i * channels + channel) * 2; byteBuffer.setInt16(offset, sample, Endian.little); } } - + _samplePcmData = Uint8List.fromList( - List.generate(bufferSize, (index) => byteBuffer.getUint8(index)) - ); - + List.generate(bufferSize, (index) => byteBuffer.getUint8(index))); + setState(() { _statusMessage = '已创建${_samplePcmData!.length}字节测试PCM数据'; }); @@ -272,6 +272,9 @@ class _OpusExamplePageState extends State { String _formatBytes(Uint8List data) { final length = data.length > 16 ? 16 : data.length; - return data.sublist(0, length).map((e) => e.toRadixString(16).padLeft(2, '0')).join(' '); + return data + .sublist(0, length) + .map((e) => e.toRadixString(16).padLeft(2, '0')) + .join(' '); } -} \ No newline at end of file +} diff --git a/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkService.kt b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkHelper.kt similarity index 70% rename from local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkService.kt rename to local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkHelper.kt index 9c97e7545..7ae21f8fe 100644 --- a/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkService.kt +++ b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkHelper.kt @@ -2,7 +2,6 @@ package de.minimalme.spotify_sdk import android.app.Activity import android.content.Context -import android.content.Intent import android.util.Log import com.spotify.android.appremote.api.ConnectionParams import com.spotify.android.appremote.api.Connector @@ -20,46 +19,33 @@ import com.spotify.android.appremote.api.error.UserNotAuthorizedException import com.spotify.sdk.android.auth.AuthorizationClient import com.spotify.sdk.android.auth.AuthorizationRequest import com.spotify.sdk.android.auth.AuthorizationResponse -import de.minimalme.spotify_sdk.subscriptions.CapabilitiesChannel -import de.minimalme.spotify_sdk.subscriptions.ConnectionStatusChannel -import de.minimalme.spotify_sdk.subscriptions.PlayerContextChannel -import de.minimalme.spotify_sdk.subscriptions.PlayerStateChannel -import de.minimalme.spotify_sdk.subscriptions.UserStatusChannel -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.embedding.engine.plugins.activity.ActivityAware -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.PluginRegistry -import kotlinx.event.SetEvent -import kotlinx.event.event -class SpotifySdkService(private val context: Context? = null) { + +class SpotifySdkHelper(private val context: Context? = null) { // application context private var applicationContext : Context? = null private var applicationActivity : Activity? = null // method channel - private lateinit var methodChannel : MethodChannel - private val channelName = "spotify_sdk" - private val loggingTag = "spotify_sdk" + private val loggingTag = "spotify_sdk" - private val errorConnecting = "errorConnecting" - private val errorDisconnecting = "errorDisconnecting" private val errorConnection = "errorConnection" - private val errorAuthenticationToken = "authenticationTokenError" private val requestCodeAuthentication = 1337 private var spotifyAppRemote: SpotifyAppRemote? = null private var playerApi: PlayerApi? = null - //-- Method implementations - private fun connectToSpotify(clientId: String?, redirectUrl: String?, result: MethodChannel.Result) { + fun initialize(clientId: String, redirectUrl: String): Boolean { + getAccessToken(clientId,redirectUrl,""); + connectToSpotify(clientId,redirectUrl) + return true; + } + + public fun connectToSpotify(clientId: String?, redirectUrl: String?) { if (clientId.isNullOrBlank() || redirectUrl.isNullOrBlank()) { - result.error(errorConnecting, "client id or redirectUrl are not set or have invalid format", "") +// result.error(errorConnecting, "client id or redirectUrl are not set or have invalid format", "") } else { val connectionParams = ConnectionParams.Builder(clientId) .setRedirectUri(redirectUrl) @@ -75,7 +61,7 @@ class SpotifySdkService(private val context: Context? = null) { initiallyConnected = true Log.i(loggingTag, "App Remote successfully connected") - result.success(true) +// result.success(true) } override fun onFailure(throwable: Throwable) { @@ -155,25 +141,23 @@ class SpotifySdkService(private val context: Context? = null) { // ) } else { // throw exception as the connect method - result.error(errorCode, errorMessage, errorDetails) +// result.error(errorCode, errorMessage, errorDetails) } } }) } } - private fun getAccessToken(clientId: String?, redirectUrl: String?, scope: String?, result: MethodChannel.Result) { + public fun getAccessToken(clientId: String?, redirectUrl: String?, scope: String?) { if (applicationActivity == null) { throw IllegalStateException("getAccessToken needs a foreground activity") } if (clientId.isNullOrBlank() || redirectUrl.isNullOrBlank()) { - result.error(errorConnecting, "client id or redirectUrl are not set or have invalid format", "") +// result.error(errorConnecting, "client id or redirectUrl are not set or have invalid format", "") } else { //Convert String? scope to Array. Delimiter set as comma "," val scopeArray = scope?.split(",")?.toTypedArray() -// methodConnectToSpotify.checkAndSetPendingOperation(result) - val builder = AuthorizationRequest.Builder( clientId, AuthorizationResponse.Type.TOKEN, @@ -190,38 +174,18 @@ class SpotifySdkService(private val context: Context? = null) { } } - private fun disconnectFromSpotify(result: MethodChannel.Result) { + public fun disconnectFromSpotify() { if (spotifyAppRemote != null && spotifyAppRemote!!.isConnected) { SpotifyAppRemote.disconnect(spotifyAppRemote) - - // emit connection terminated event -// connStatusEventChannel( -// ConnectionStatusChannel.ConnectionEvent( -// false, -// "Successfully disconnected from Spotify.", -// null, -// null -// ) -// ) - // method success - result.success(true) +// result.success(true) } else if (!spotifyAppRemote!!.isConnected) { - result.error(errorDisconnecting, "could not disconnect spotify remote", "you are not connected, no need to disconnect") +// result.error(errorDisconnecting, "could not disconnect spotify remote", "you are not connected, no need to disconnect") } else { - result.error(errorDisconnecting, "could not disconnect spotify remote", "spotifyAppRemote is not set") +// result.error(errorDisconnecting, "could not disconnect spotify remote", "spotifyAppRemote is not set") } } - private fun play(spotifyUri: String?){ -// if (playerApi != null && !spotifyUri.isNullOrBlank()) { -// playerApi.play(spotifyUri) -// .setResultCallback { result.success(true) } -// .setErrorCallback { throwable -> result.error(errorPlay, "error when playing uri: $spotifyUri", throwable.toString()) } -// } else if (spotifyUri.isNullOrBlank()) { -// result.error(errorPlay, "spotifyUri has invalid format or is not set", "") -// } else { -// spotifyRemoteAppNotSetError() -// } + public fun play(spotifyUri: String?){ if (playerApi != null && !spotifyUri.isNullOrBlank()) { playerApi!!.play(spotifyUri) } else if (spotifyUri.isNullOrBlank()) { @@ -230,14 +194,8 @@ class SpotifySdkService(private val context: Context? = null) { } } - private fun pause(){ -// if (playerApi != null) { -// playerApi.pause() -// .setResultCallback { result.success(true) } -// .setErrorCallback { throwable -> result.error(errorPause, "error when pausing", throwable.toString()) } -// } else { -// spotifyRemoteAppNotSetError() -// } + + public fun pause(){ if (playerApi != null) { playerApi!!.pause() .setResultCallback {} diff --git a/pubspec.yaml b/pubspec.yaml index 8327fbc94..303d4b581 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,6 +35,7 @@ dependencies: flutter_easyloading: ^3.0.5 package_info_plus: ^8.3.0 file_picker: ^8.0.0+1 + haptic_feedback: ^0.5.1+1 window_size: git: