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: