tanlongsheng 1 year ago
parent
commit
8b64740198
  1. 8
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt
  2. 4
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionHandler.kt
  3. 6
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionService.kt
  4. 30
      lib/data/models/appconfig_model.dart
  5. 37
      lib/data/models/appconfig_model.g.dart
  6. 9
      lib/data/services/network/api.dart
  7. 45
      lib/data/services/open_ai_service_adapter.dart
  8. 132
      lib/data/services/speech_impl/azure_asr_service.dart
  9. 202
      lib/data/services/speech_impl/azure_tts_service.dart
  10. 16
      lib/data/services/spotify_service.dart
  11. 184
      lib/data/services/voice_interaction_service.dart
  12. 133
      lib/data/services/volcano_translation_service.dart
  13. 24
      lib/modules/login/controllers/login_controller.dart
  14. 23
      lib/modules/splash/controllers/splash_controller.dart
  15. 2
      local_plugins/open_ai_service/android/build.gradle.kts
  16. 63
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt
  17. 311
      local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkService.kt
  18. 3
      pubspec.yaml

8
android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt

@ -177,7 +177,9 @@ class MainActivity: FlutterActivity() {
var volcanoSpeechAppId: String = ""
var volcanoSpeechAppToken: String = ""
var mcpServer: String = ""
var spotifyClientId: String = ""
var spotifyRedirectUrl: String = ""
// 安全存储相关常量
private const val SECURE_PREFS_FILENAME = "deep_voice_secure_prefs"
private const val KEY_AZURE_SPEECH_KEY = "azure_speech_key"
@ -190,6 +192,8 @@ class MainActivity: FlutterActivity() {
private const val KEY_VOLCANO_SPEECH_APP_ID = "volcano_speech_app_id"
private const val KEY_VOLCANO_SPEECH_APP_TOKEN = "volcano_speech_app_token"
private const val KEY_MCP_SERVER = "mcp_server"
private const val KEY_SPOTIFY_CLIENT_ID = "spotify_client_id"
private const val KEY_SPOTIFY_REDIRECT_URL = "spotify_redirect_url"
// 会话管理
private const val KEY_SESSION_ID = "session_id"
@ -231,6 +235,8 @@ class MainActivity: FlutterActivity() {
.putString(KEY_VOLCANO_SPEECH_APP_ID, volcanoSpeechAppId)
.putString(KEY_VOLCANO_SPEECH_APP_TOKEN, volcanoSpeechAppToken)
.putString(KEY_MCP_SERVER, mcpServer)
.putString(KEY_SPOTIFY_CLIENT_ID, spotifyClientId)
.putString(KEY_SPOTIFY_REDIRECT_URL, spotifyRedirectUrl)
.apply()
FileLogger.d("MainActivity", "密钥已安全保存到加密存储中")

4
android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionHandler.kt

@ -26,7 +26,9 @@ class VoiceInteractionHandler(
private val openaiModel: String = "",
private val volcanoSpeechAppId: String,
private val volcanoSpeechAppToken: String,
private val mcpServer: String
private val mcpServer: String,
private val spotifyclientid: String,
private val spotifyRedirectUrl: String
) {
private val TAG = "VoiceInteractionHandler"

6
android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionService.kt

@ -149,6 +149,8 @@ class VoiceInteractionService : Service() {
var volcanoSpeechAppId = MainActivity.volcanoSpeechAppId ?: ""
var volcanoSpeechAppToken = MainActivity.volcanoSpeechAppToken ?: ""
var mcpServer = MainActivity.mcpServer ?: ""
var spotifyclientid = MainActivity.spotifyClientId ?: ""
var spotifyRedirectUrl = MainActivity.spotifyRedirectUrl ?: ""
// 如果静态变量中没有配置,尝试从加密存储中加载
if (subscriptionKey.isEmpty() || serviceRegion.isEmpty() || openaiKey.isEmpty()) {
@ -167,6 +169,8 @@ class VoiceInteractionService : Service() {
volcanoSpeechAppId = MainActivity.volcanoSpeechAppId ?: ""
volcanoSpeechAppToken = MainActivity.volcanoSpeechAppToken ?: ""
mcpServer = MainActivity.mcpServer ?: ""
spotifyclientid = MainActivity.spotifyClientId ?: ""
spotifyRedirectUrl = MainActivity.spotifyRedirectUrl ?: ""
FileLogger.d(TAG, "已从加密存储加载配置信息")
} else {
FileLogger.e(TAG, "无法从加密存储加载配置信息")
@ -178,7 +182,7 @@ class VoiceInteractionService : Service() {
subscriptionKey, serviceRegion,
openaiKey, openaiBaseUrl, openaiModel,
volcanoSpeechAppId, volcanoSpeechAppToken,
mcpServer)
mcpServer,spotifyclientid,spotifyRedirectUrl)
// 初始化回调
voiceInteractionHandler.setCallback(object : VoiceInteractionHandler.InteractionCallback {

30
lib/data/models/appconfig_model.dart

@ -0,0 +1,30 @@
import 'package:json_annotation/json_annotation.dart';
part 'appconfig_model.g.dart'; // 生成的文件名
@JsonSerializable()
class UserGetAppConfigResp {
final Map<String, String> env;
final Map<String, DBMCPServer> mcps;
UserGetAppConfigResp({required this.env, required this.mcps});
factory UserGetAppConfigResp.fromJson(Map<String, dynamic> json) =>
_$UserGetAppConfigRespFromJson(json);
Map<String, dynamic> toJson() => _$UserGetAppConfigRespToJson(this);
}
@JsonSerializable()
class DBMCPServer {
final String servername;
final String url;
final List<String>? tools;
DBMCPServer({
required this.servername,
required this.url,
required this.tools,
});
factory DBMCPServer.fromJson(Map<String, dynamic> json) =>
_$DBMCPServerFromJson(json);
Map<String, dynamic> toJson() => _$DBMCPServerToJson(this);
}

37
lib/data/models/appconfig_model.g.dart

@ -0,0 +1,37 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'appconfig_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
UserGetAppConfigResp _$UserGetAppConfigRespFromJson(
Map<String, dynamic> json) =>
UserGetAppConfigResp(
env: Map<String, String>.from(json['env'] as Map),
mcps: (json['mcps'] as Map<String, dynamic>).map(
(k, e) => MapEntry(k, DBMCPServer.fromJson(e as Map<String, dynamic>)),
),
);
Map<String, dynamic> _$UserGetAppConfigRespToJson(
UserGetAppConfigResp instance) =>
<String, dynamic>{
'env': instance.env,
'mcps': instance.mcps,
};
DBMCPServer _$DBMCPServerFromJson(Map<String, dynamic> json) => DBMCPServer(
servername: json['servername'] as String,
url: json['url'] as String,
tools:
(json['tools'] as List<dynamic>?)?.map((e) => e as String).toList(),
);
Map<String, dynamic> _$DBMCPServerToJson(DBMCPServer instance) =>
<String, dynamic>{
'servername': instance.servername,
'url': instance.url,
'tools': instance.tools,
};

9
lib/data/services/network/api.dart

@ -64,4 +64,13 @@ class Api {
params: params,
);
}
//app配置
static getappconfig([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/user_getappconfig',
params: params,
);
}
}

45
lib/data/services/open_ai_service_adapter.dart

@ -1,7 +1,9 @@
import 'dart:async';
import 'dart:io';
import 'dart:convert';
import 'package:deep_voice/data/models/appconfig_model.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart';
import 'package:open_ai_service/open_ai_service.dart';
import 'package:get/get.dart';
import 'ai_service.dart';
@ -26,6 +28,7 @@ class OpenAIServiceAdapter implements AiService {
StreamSubscription<OpenAIEvent>? _eventSubscription;
final StreamController<StreamEvent> _tokenStreamController =
StreamController<StreamEvent>.broadcast();
final GetStorage _storage = GetStorage();
/// 构造函数
OpenAIServiceAdapter() {
@ -97,9 +100,13 @@ class OpenAIServiceAdapter implements AiService {
Future<bool> initialize() async {
try {
// 从.env文件中读取配置
final apiKey = dotenv.env['OPENAI_API_KEY'] ?? '';
final baseUrl = dotenv.env['OPENAI_BASE_URL'] ?? '';
final model = dotenv.env['OPENAI_MODEL'] ?? '';
// final apiKey = dotenv.env['OPENAI_API_KEY'] ?? '';
// final baseUrl = dotenv.env['OPENAI_BASE_URL'] ?? '';
// final model = dotenv.env['OPENAI_MODEL'] ?? '';
final _env = _storage.read("ENV") as Map<String, String>;
final apiKey = _env['OPENAI_API_KEY'] ?? '';
final baseUrl = _env['OPENAI_BASE_URL'] ?? '';
final model = _env['OPENAI_MODEL'] ?? '';
printInfo(info: '从.env读取OpenAI配置');
printInfo(info: '基础URL: $baseUrl');
@ -109,24 +116,24 @@ class OpenAIServiceAdapter implements AiService {
printError(info: '错误: OpenAI API密钥未配置,请在.env文件中设置OPENAI_API_KEY');
return false;
}
final _mcps = _storage.read("MCPS") as Map<String, DBMCPServer>;
// 读取.mcp.json文件
String mcpConfig = '';
try {
// 从Flutter资源包中加载.mcp.json
mcpConfig = await rootBundle.loadString('.mcp.json');
printInfo(info: '成功从资源包加载.mcp.json配置文件');
String mcpConfig = jsonEncode(_mcps);
// try {
// // 从Flutter资源包中加载.mcp.json
// mcpConfig = await rootBundle.loadString('.mcp.json');
// printInfo(info: '成功从资源包加载.mcp.json配置文件');
// 验证JSON格式
final jsonData = jsonDecode(mcpConfig);
if (jsonData is Map && jsonData.containsKey('mcpServers')) {
printInfo(info: '解析到有效的mcpServers配置');
} else {
printInfo(info: '.mcp.json内容格式不正确,期望包含mcpServers字段');
}
} catch (e) {
printError(info: '加载.mcp.json资源文件时出错: $e');
}
// // 验证JSON格式
// final jsonData = jsonDecode(mcpConfig);
// if (jsonData is Map && jsonData.containsKey('mcpServers')) {
// printInfo(info: '解析到有效的mcpServers配置');
// } else {
// printInfo(info: '.mcp.json内容格式不正确,期望包含mcpServers字段');
// }
// } catch (e) {
// printError(info: '加载.mcp.json资源文件时出错: $e');
// }
// 初始化OpenAI服务
final result = await _openAIService.initialize(

132
lib/data/services/speech_impl/azure_asr_service.dart

@ -1,76 +1,79 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart';
import '../../../core/utils/logger.dart';
import 'package:get/get.dart';
import '../asr_service.dart';
/// Azure 语音识别服务
///
///
/// 该服务提供了通过平台通道与 Android 上的 Microsoft Speech SDK 交互的接口
class AzureAsrService extends GetxService implements AsrService {
static final AzureAsrService to = Get.put(AzureAsrService());
static const MethodChannel _channel = MethodChannel('azure_speech/asr');
static const EventChannel _eventChannel = EventChannel('azure_speech/asr_events');
static const EventChannel _eventChannel =
EventChannel('azure_speech/asr_events');
final GetStorage _storage = GetStorage();
bool _isInitialized = false;
late final String _subscriptionKey;
late final String _serviceRegion;
final List<String> _defaultSupportedLanguages = ['zh-CN'];
@override
List<String> get supportedLanguages => _defaultSupportedLanguages;
// 连续识别相关
bool _isContinuousRecognitionActive = false;
StreamController<RecognitionEvent>? _eventStreamController;
StreamSubscription? _eventSubscription;
// 最新的识别结果
String _latestRecognizedText = '';
String get latestRecognizedText => _latestRecognizedText;
// 最新检测到的语言
String _latestDetectedLanguage = '';
String get latestDetectedLanguage => _latestDetectedLanguage;
AzureAsrService() {
_loadConfig();
}
/// 从环境变量加载配置
void _loadConfig() {
_subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? '';
_serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? '';
final _env = _storage.read("ENV") as Map<String, String>;
_subscriptionKey = _env['AZURE_SPEECH_KEY'] ?? '';
_serviceRegion = _env['AZURE_SPEECH_REGION'] ?? '';
if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) {
throw Exception('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION');
throw Exception(
'未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION');
}
}
/// 设置事件通道
void _setupEventChannel() {
_eventSubscription = _eventChannel
.receiveBroadcastStream()
.listen((event) {
if (event is Map) {
_handleRecognitionEvent(event);
}
}, onError: _handleRecognitionError);
_eventSubscription = _eventChannel.receiveBroadcastStream().listen((event) {
if (event is Map) {
_handleRecognitionEvent(event);
}
}, onError: _handleRecognitionError);
}
@override
Future<bool> initialize({
List<String>? supportedLanguages,
}) async {
try {
final List<String> languages = supportedLanguages ?? _defaultSupportedLanguages;
final List<String> languages =
supportedLanguages ?? _defaultSupportedLanguages;
// 检查是否需要重新初始化
if (_isInitialized) {
await dispose();
}
final bool result = await _channel.invokeMethod('initialize', {
'subscriptionKey': _subscriptionKey,
'region': _serviceRegion,
@ -87,22 +90,23 @@ class AzureAsrService extends GetxService implements AsrService {
rethrow;
}
}
@override
Future<RecognitionEvent> recognizeOnce() async {
if (!_isInitialized) {
await initialize();
}
try {
final result = await _channel.invokeMethod('recognizeOnce');
if (result is Map) {
final String text = result['text'] as String? ?? '';
final String detectedLanguage = result['detectedLanguage'] as String? ?? '';
final String detectedLanguage =
result['detectedLanguage'] as String? ?? '';
_latestRecognizedText = text;
_latestDetectedLanguage = detectedLanguage;
return RecognitionEvent.finalResult(
text: text,
detectedLanguage: detectedLanguage,
@ -112,35 +116,36 @@ class AzureAsrService extends GetxService implements AsrService {
_latestRecognizedText = result;
return RecognitionEvent.finalResult(text: result);
}
throw Exception('无效的识别结果格式');
} catch (e) {
Logger.error('语音识别失败: $e');
return RecognitionEvent.error(e.toString());
}
}
@override
Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
if (!_isInitialized) {
await initialize();
}
if (_isContinuousRecognitionActive) {
await stopContinuousRecognition();
}
try {
_eventStreamController = StreamController<RecognitionEvent>.broadcast();
// 开始连续识别
final bool result = await _channel.invokeMethod('startContinuousRecognition');
final bool result =
await _channel.invokeMethod('startContinuousRecognition');
if (!result) {
_cleanupEventStream();
throw Exception('启动连续识别失败');
}
_isContinuousRecognitionActive = true;
return _eventStreamController!.stream;
} catch (e) {
@ -149,15 +154,16 @@ class AzureAsrService extends GetxService implements AsrService {
rethrow;
}
}
@override
Future<bool> stopContinuousRecognition() async {
if (!_isInitialized || !_isContinuousRecognitionActive) {
return true;
}
try {
final bool result = await _channel.invokeMethod('stopContinuousRecognition');
final bool result =
await _channel.invokeMethod('stopContinuousRecognition');
_cleanupEventStream();
return result;
} catch (e) {
@ -166,26 +172,27 @@ class AzureAsrService extends GetxService implements AsrService {
rethrow;
}
}
@override
bool isContinuousRecognitionActive() {
return _isContinuousRecognitionActive;
}
/// 处理来自原生端的识别事件
void _handleRecognitionEvent(dynamic event) {
if (event is! Map || _eventStreamController == null) return;
final Map<dynamic, dynamic> eventMap = event;
final String eventType = eventMap['type'] as String? ?? '';
// 添加日志帮助调试
Logger.debug('收到语音识别事件: $eventType, 数据: $eventMap');
switch (eventType) {
case 'result':
final String text = eventMap['text'] as String? ?? '';
final String detectedLanguage = eventMap['detectedLanguage'] as String? ?? '';
final String detectedLanguage =
eventMap['detectedLanguage'] as String? ?? '';
_latestRecognizedText = text;
_latestDetectedLanguage = detectedLanguage;
_eventStreamController?.add(RecognitionEvent(
@ -194,45 +201,46 @@ class AzureAsrService extends GetxService implements AsrService {
detectedLanguage: detectedLanguage,
));
break;
case 'recognizing':
final String text = eventMap['text'] as String? ?? '';
final String detectedLanguage = eventMap['detectedLanguage'] as String? ?? '';
final String detectedLanguage =
eventMap['detectedLanguage'] as String? ?? '';
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.intermediateResult,
text: text,
detectedLanguage: detectedLanguage,
));
break;
case 'sessionStarted':
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.sessionStarted,
));
break;
case 'sessionStopped':
_isContinuousRecognitionActive = false;
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.sessionStopped,
));
break;
case 'canceled':
_isContinuousRecognitionActive = false;
final String reason = eventMap['reason'] as String? ?? '';
final String errorDetails = eventMap['errorDetails'] as String? ?? '';
if (reason.isNotEmpty || errorDetails.isNotEmpty) {
Logger.error('识别取消: $reason - $errorDetails');
}
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.canceled,
error: '$reason: $errorDetails',
));
break;
case 'error':
final String error = eventMap['message'] as String? ?? '';
Logger.error('识别错误: $error');
@ -243,36 +251,36 @@ class AzureAsrService extends GetxService implements AsrService {
break;
}
}
/// 处理识别事件流错误
void _handleRecognitionError(Object error) {
Logger.error('识别事件流错误: $error');
_eventStreamController?.addError(error);
_cleanupEventStream();
}
/// 清理事件流资源
void _cleanupEventStream() {
_eventStreamController?.close();
_eventStreamController = null;
_isContinuousRecognitionActive = false;
}
@override
Future<void> dispose() async {
try {
if (_isContinuousRecognitionActive) {
await stopContinuousRecognition();
}
// 取消事件订阅
await _eventSubscription?.cancel();
_eventSubscription = null;
// 通知原生端释放资源
await _channel.invokeMethod('dispose');
_isInitialized = false;
Logger.info('Azure 语音识别资源已释放');
} catch (e) {
Logger.error('释放语音识别资源失败: $e');
@ -280,4 +288,4 @@ class AzureAsrService extends GetxService implements AsrService {
_isInitialized = false;
}
}
}
}

202
lib/data/services/speech_impl/azure_tts_service.dart

@ -3,27 +3,28 @@ import 'dart:io';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart';
import '../../../core/utils/logger.dart';
import '../tts_service.dart';
/// 微软 Text-to-Speech 服务
///
///
/// 该服务通过平台通道与原生 Microsoft Speech SDK 交互,
/// 提供文本转语音功能。
class AzureTtsService extends GetxService implements TtsService {
static final AzureTtsService to = Get.put(AzureTtsService());
static const MethodChannel _channel = MethodChannel('azure_speech/tts');
final GetStorage _storage = GetStorage();
bool _isInitialized = false;
late final String _subscriptionKey;
late final String _serviceRegion;
// 当前设置
String _currentVoice = 'zh-CN-XiaoxiaoNeural';
int _currentRate = 0;
int _currentPitch = 0;
int _currentVolume = 100;
// 支持的语音列表
final List<String> _supportedVoices = [
'zh-CN-XiaoxiaoNeural',
@ -32,103 +33,98 @@ class AzureTtsService extends GetxService implements TtsService {
'en-US-JennyNeural',
'en-US-GuyNeural'
];
// 语音合成队列
final List<_SpeechItem> _textQueue = [];
bool _isProcessingQueue = false;
// 可观察状态
final isEnabled = true.obs;
// 流式文本缓冲区
String _streamBuffer = '';
/// 缓存的播放状态
bool _speakingCache = false;
DateTime _lastSpeakingCheck = DateTime.now();
@override
String get currentVoice => _currentVoice;
@override
bool get isSpeaking {
// 如果未初始化,直接返回false
if (!_isInitialized) return false;
// 如果距离上次检查不到50ms,则使用缓存值
if (DateTime.now().difference(_lastSpeakingCheck).inMilliseconds < 50) {
return _speakingCache;
}
// 否则异步更新缓存(但仍返回当前缓存值)
_checkSpeakingStatus();
return _speakingCache;
}
@override
Stream<TtsEvent> get onEvent => const Stream.empty();
@override
List<String> get supportedVoices => _supportedVoices;
AzureTtsService() {
_loadConfig();
}
/// 从环境变量加载配置
void _loadConfig() {
_subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? '';
_serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? '';
final _env = _storage.read("ENV") as Map<String, String>;
_subscriptionKey = _env['AZURE_SPEECH_KEY'] ?? '';
_serviceRegion = _env['AZURE_SPEECH_REGION'] ?? '';
}
@override
Future<bool> initialize({
List<String>? supportedLanguages,
}) async {
// 检查是否需要重新初始化
if (_isInitialized) {
await dispose();
}
// 检查是否需要重新初始化
if (_isInitialized) {
await dispose();
}
try {
if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) {
Logger.error('未找到Azure语音服务配置');
return false;
}
// 更新支持的语音列表(如果提供)
final String language = supportedLanguages?.first ?? 'zh-CN';
final result = await _channel.invokeMethod('initialize', {
'subscriptionKey': _subscriptionKey,
'region': _serviceRegion,
'language': language,
});
_isInitialized = true;
return result;
final result = await _channel.invokeMethod('initialize', {
'subscriptionKey': _subscriptionKey,
'region': _serviceRegion,
'language': language,
});
_isInitialized = true;
return result;
} catch (e) {
Logger.error('初始化失败: $e');
return false;
}
}
@override
Future<bool> setVoice(String voiceName) async {
if (!_isInitialized) await initialize();
if (voiceName == _currentVoice) return true;
try {
final result = await _channel.invokeMethod('setVoice', {
'voiceName': voiceName,
});
if (result) _currentVoice = voiceName;
return result;
} catch (e) {
@ -136,45 +132,46 @@ class AzureTtsService extends GetxService implements TtsService {
return false;
}
}
/// 设置语音参数
Future<bool> setSpeechParams({int rate = 0, int pitch = 0, int volume = 100}) async {
Future<bool> setSpeechParams(
{int rate = 0, int pitch = 0, int volume = 100}) async {
if (!_isInitialized) await initialize();
try {
final result = await _channel.invokeMethod('setSpeechParams', {
'rate': rate,
'pitch': pitch,
'volume': volume,
});
if (result) {
_currentRate = rate;
_currentPitch = pitch;
_currentVolume = volume;
}
return result;
} catch (e) {
Logger.error('设置语音参数失败: $e');
return false;
}
}
@override
Future<bool> speakOnce(String text) async {
if (!_isInitialized) await initialize();
if (!isEnabled.value || text.isEmpty) return false;
try {
// 主动设置缓存状态为true
_speakingCache = true;
final result = await _channel.invokeMethod('speakText', {'text': text});
// 调用完成后主动检查状态
_checkSpeakingStatus();
return result == "OK";
} catch (e) {
// 发生错误时重置状态
@ -183,21 +180,21 @@ class AzureTtsService extends GetxService implements TtsService {
return false;
}
}
/// 播放SSML
Future<bool> speakSsmlOnce(String ssml) async {
if (!_isInitialized) await initialize();
if (!isEnabled.value || ssml.isEmpty) return false;
try {
// 主动设置缓存状态为true
_speakingCache = true;
final result = await _channel.invokeMethod('speakSsml', {'ssml': ssml});
// 调用完成后主动检查状态
_checkSpeakingStatus();
return result == "OK";
} catch (e) {
// 发生错误时重置状态
@ -206,15 +203,16 @@ class AzureTtsService extends GetxService implements TtsService {
return false;
}
}
/// 等待语音播放完成
Future<void> waitForSpeechCompletion({Duration timeout = const Duration(seconds: 30)}) async {
Future<void> waitForSpeechCompletion(
{Duration timeout = const Duration(seconds: 30)}) async {
// 首先检查当前状态
_checkSpeakingStatus();
if (!_speakingCache) return;
final stopwatch = Stopwatch()..start();
while (_speakingCache && stopwatch.elapsed < timeout) {
// 短暂等待后再次检查
await Future.delayed(const Duration(milliseconds: 100));
@ -227,31 +225,30 @@ class AzureTtsService extends GetxService implements TtsService {
});
}
}
@override
Future<bool> speakStream(String text) async {
if (!isEnabled.value || text.isEmpty) return false;
try {
// 添加文本到缓冲区
_streamBuffer += text;
// 如果缓冲区为空,直接返回
if (_streamBuffer.isEmpty) return true;
// 使用正则表达式匹配句子,包括结束符号
// 匹配任意字符,直到遇到句子结束符号
final sentenceRegex = RegExp(
r'([^。.!!??;;:\n\r]+[。.!!??;;::\n\r]|[^。.!!??;;:\n\r]+(?:\.{3,}|…)|[^。.!!??;;:\n\r]+["」』])'
);
r'([^。.!!??;;:\n\r]+[。.!!??;;::\n\r]|[^。.!!??;;:\n\r]+(?:\.{3,}|…)|[^。.!!??;;:\n\r]+["」』])');
bool hasProcessed = false;
// 查找所有完整句子
final matches = sentenceRegex.allMatches(_streamBuffer);
final List<String> sentences = [];
int lastMatchEnd = 0;
for (final match in matches) {
// 提取完整句子(包含结束符号)
final sentence = match.group(1)?.trim();
@ -260,7 +257,7 @@ class AzureTtsService extends GetxService implements TtsService {
lastMatchEnd = match.end;
}
}
// 处理找到的句子
for (final sentence in sentences) {
_textQueue.add(_SpeechItem(
@ -271,32 +268,32 @@ class AzureTtsService extends GetxService implements TtsService {
));
hasProcessed = true;
}
// 更新缓冲区,只保留未完成的部分
if (lastMatchEnd > 0) {
_streamBuffer = _streamBuffer.substring(lastMatchEnd);
}
// 如果处理了文本并且队列未在处理中,开始处理队列
if (hasProcessed && !_isProcessingQueue) {
_processQueue();
}
return true;
} catch (e) {
Logger.error('处理流式文本失败: $e');
return false;
}
}
@override
Future<bool> flushStream() async {
if (_textQueue.isEmpty && _streamBuffer.isEmpty) return true;
try {
// 处理缓冲区中可能的完整句子
await speakStream('');
// 如果缓冲区仍有剩余文本,将其添加到播放队列
if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) {
_textQueue.add(_SpeechItem(
@ -305,25 +302,25 @@ class AzureTtsService extends GetxService implements TtsService {
pitch: _currentPitch,
volume: _currentVolume,
));
// 清空缓冲区
_streamBuffer = '';
// 如果队列未在处理中,开始处理队列
if (!_isProcessingQueue) {
_processQueue();
}
}
Logger.info('等待TTS队列播放完成,剩余${_textQueue.length}条');
// 等待队列处理完成
await _checkSpeakingStatus();
while (_isProcessingQueue || _speakingCache) {
await Future.delayed(const Duration(milliseconds: 100));
await _checkSpeakingStatus();
}
Logger.info('TTS队列播放完成');
return true;
} catch (e) {
@ -331,33 +328,35 @@ class AzureTtsService extends GetxService implements TtsService {
return false;
}
}
/// 处理语音合成队列
Future<void> _processQueue() async {
if (_textQueue.isEmpty || _isProcessingQueue) return;
_isProcessingQueue = true;
try {
while (_textQueue.isNotEmpty) {
if (!isEnabled.value) {
_textQueue.clear();
break;
}
final item = _textQueue.removeAt(0);
if (item.rate != _currentRate || item.pitch != _currentPitch || item.volume != _currentVolume) {
if (item.rate != _currentRate ||
item.pitch != _currentPitch ||
item.volume != _currentVolume) {
await setSpeechParams(
rate: item.rate,
pitch: item.pitch,
volume: item.volume,
);
}
// 使用speakOnce播放当前项
await speakOnce(item.text);
// 等待播放完成
await waitForSpeechCompletion();
}
@ -367,26 +366,26 @@ class AzureTtsService extends GetxService implements TtsService {
_isProcessingQueue = false;
}
}
@override
Future<bool> stop() async {
try {
_textQueue.clear();
if (_speakingCache) {
final result = await _channel.invokeMethod('stopSpeaking');
// 重置缓存状态
_speakingCache = false;
return result;
}
return true;
} catch (e) {
Logger.error('停止语音合成出错: $e');
return false;
}
}
/// 生成SSML文本
String generateSsml({
required String text,
@ -399,11 +398,15 @@ class AzureTtsService extends GetxService implements TtsService {
final rateValue = (rate ?? _currentRate).clamp(-100, 100);
final pitchValue = (pitch ?? _currentPitch).clamp(-100, 100);
final volumeValue = (volume ?? _currentVolume).clamp(0, 100);
final rateStr = rateValue == 0 ? '0%' : rateValue < 0 ? '${(rateValue * 0.9).round()}%' : '$rateValue%';
final rateStr = rateValue == 0
? '0%'
: rateValue < 0
? '${(rateValue * 0.9).round()}%'
: '$rateValue%';
final pitchStr = pitchValue == 0 ? '0%' : '${(pitchValue * 0.5).round()}%';
final volumeStr = "$volumeValue%";
return '''
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="zh-CN">
<voice name="$voice">
@ -414,17 +417,17 @@ class AzureTtsService extends GetxService implements TtsService {
</speak>
''';
}
/// 切换启用状态
void toggleEnabled() {
isEnabled.toggle();
if (!isEnabled.value) stop();
}
@override
Future<void> dispose() async {
if (!_isInitialized) return;
try {
await stop();
await _channel.invokeMethod('dispose');
@ -448,7 +451,6 @@ class AzureTtsService extends GetxService implements TtsService {
return _speakingCache;
}
}
}
/// 语音合成项目
@ -457,11 +459,11 @@ class _SpeechItem {
final int rate;
final int pitch;
final int volume;
_SpeechItem({
required this.text,
this.rate = 0,
this.pitch = 0,
this.volume = 100,
});
}
}

16
lib/data/services/spotify_service.dart

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'dart:convert';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart';
import 'package:spotify_sdk/spotify_sdk.dart';
import 'package:get/get.dart';
import 'ai_service.dart';
@ -11,6 +12,10 @@ import 'package:path_provider/path_provider.dart';
/// OpenAI服务适配器 - 连接AiService接口与OpenAIService插件
class SpotifService {
bool isInitialized = false;
final GetStorage _storage = GetStorage();
late String SPOTIFY_CLIENT_ID;
late String SPOTIFY_REDIRECT_URL;
/// 初始化OpenAI服务
Future<bool> initialize() async {
@ -18,6 +23,9 @@ class SpotifService {
await getAccessToken();
await connectToSpotifyRemote();
isInitialized = true;
final _env = _storage.read("ENV") as Map<String, String>;
SPOTIFY_CLIENT_ID = _env['SPOTIFY_CLIENT_ID'] ?? '';
SPOTIFY_REDIRECT_URL = _env['SPOTIFY_REDIRECT_URL'] ?? '';
printInfo(info: "SpotifService----------- initialize");
}
@ -28,8 +36,8 @@ class SpotifService {
Future<void> connectToSpotifyRemote() async {
try {
var result = await SpotifySdk.connectToSpotifyRemote(
clientId: dotenv.env['SPOTIFY_CLIENT_ID'].toString(),
redirectUrl: dotenv.env['SPOTIFY_REDIRECT_URL'].toString(),
clientId: SPOTIFY_CLIENT_ID,
redirectUrl: SPOTIFY_REDIRECT_URL,
scope:
'user-read-private user-read-email streaming app-remote-control user-modify-playback-state',
);
@ -46,8 +54,8 @@ class SpotifService {
Future<String> getAccessToken() async {
try {
var authenticationToken = await SpotifySdk.getAccessToken(
clientId: dotenv.env['SPOTIFY_CLIENT_ID'].toString(),
redirectUrl: dotenv.env['SPOTIFY_REDIRECT_URL'].toString(),
clientId: SPOTIFY_CLIENT_ID,
redirectUrl: SPOTIFY_REDIRECT_URL,
scope: 'app-remote-control, '
'user-modify-playback-state, '
'playlist-read-private, '

184
lib/data/services/voice_interaction_service.dart

@ -1,8 +1,10 @@
import 'dart:async';
import 'dart:convert';
import 'package:deep_voice/data/models/appconfig_model.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart';
import '../models/events/voice_interaction_event.dart';
import '../../core/utils/logger.dart';
import '../../modules/chat/models/message_model.dart';
@ -14,29 +16,33 @@ import 'chat_history_service.dart';
/// 管理与平台原生语音交互服务的通信,提供统一的接口供应用使用
class VoiceInteractionService extends GetxService {
static VoiceInteractionService get to => Get.find<VoiceInteractionService>();
final GetStorage _storage = GetStorage();
// 方法通道
static const MethodChannel _channel = MethodChannel('com.deep_voice.voice_interaction');
static const MethodChannel _channel =
MethodChannel('com.deep_voice.voice_interaction');
// 事件通道
static const EventChannel _eventChannel = EventChannel('com.deep_voice.voice_interaction_events');
static const EventChannel _eventChannel =
EventChannel('com.deep_voice.voice_interaction_events');
// 服务状态
final _isServiceRunning = false.obs;
bool get isServiceRunning => _isServiceRunning.value;
// 流控制器
final _eventStreamController = StreamController<VoiceInteractionEvent>.broadcast();
final _eventStreamController =
StreamController<VoiceInteractionEvent>.broadcast();
// 事件流
Stream<VoiceInteractionEvent> get eventStream => _eventStreamController.stream;
Stream<VoiceInteractionEvent> get eventStream =>
_eventStreamController.stream;
// 事件通道订阅
StreamSubscription? _eventSubscription;
// 标记是否初始化
bool _isInitialized = false;
// 配置信息
late String _azureSpeechKey;
late String _azureSpeechRegion;
@ -47,61 +53,73 @@ class VoiceInteractionService extends GetxService {
late String _volcanoSpeechAppToken;
// MCP配置信息
String? _mcpConfig;
// 聊天历史服务
late final ChatHistoryService _chatHistoryService;
/// 设置事件通道
void _setupEventChannel() {
_eventSubscription = _eventChannel
.receiveBroadcastStream()
.listen(_handleVoiceInteractionEvent, onError: (error) {
Logger.error('语音交互事件通道错误: $error');
});
Logger.error('语音交互事件通道错误: $error');
});
}
/// 从环境变量加载配置
void _loadConfig() {
_azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? '';
_azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? '';
_openaiApiKey = dotenv.env['OPENAI_API_KEY'] ?? '';
_openaiBaseUrl = dotenv.env['OPENAI_BASE_URL'] ?? '';
_openaiModel = dotenv.env['OPENAI_MODEL'] ?? '';
_volcanoSpeechAppId = dotenv.env['VOLCANO_SPEECH_APP_ID'] ?? '';
_volcanoSpeechAppToken = dotenv.env['VOLCANO_SPEECH_APP_TOKEN'] ?? '';
// _azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? '';
// _azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? '';
// _openaiApiKey = dotenv.env['OPENAI_API_KEY'] ?? '';
// _openaiBaseUrl = dotenv.env['OPENAI_BASE_URL'] ?? '';
// _openaiModel = dotenv.env['OPENAI_MODEL'] ?? '';
// _volcanoSpeechAppId = dotenv.env['VOLCANO_SPEECH_APP_ID'] ?? '';
// _volcanoSpeechAppToken = dotenv.env['VOLCANO_SPEECH_APP_TOKEN'] ?? '';
final _env = _storage.read("ENV") as Map<String, String>;
_azureSpeechKey = _env['AZURE_SPEECH_KEY'] ?? '';
_azureSpeechRegion = _env['AZURE_SPEECH_REGION'] ?? '';
_openaiApiKey = _env['OPENAI_API_KEY'] ?? '';
_openaiBaseUrl = _env['OPENAI_BASE_URL'] ?? '';
_openaiModel = _env['OPENAI_MODEL'] ?? '';
_volcanoSpeechAppId = _env['VOLCANO_SPEECH_APP_ID'] ?? '';
_volcanoSpeechAppToken = _env['VOLCANO_SPEECH_APP_TOKEN'] ?? '';
if (_azureSpeechKey.isEmpty || _azureSpeechRegion.isEmpty) {
Logger.warning('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION');
Logger.warning(
'未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION');
}
// 加载MCP配置
_loadMcpConfig();
}
/// 加载MCP配置
void _loadMcpConfig() {
try {
rootBundle.loadString('.mcp.json').then((config) {
_mcpConfig = config;
Logger.info('成功加载MCP配置, 配置内容: $_mcpConfig');
});
final mcps = _storage.read("MCPS") as Map<String, DBMCPServer>;
String config = jsonEncode(mcps);
_mcpConfig = config;
Logger.info('成功加载MCP配置, 配置内容: $_mcpConfig');
// rootBundle.loadString('.mcp.json').then((config) {
// _mcpConfig = config;
// Logger.info('成功加载MCP配置, 配置内容: $_mcpConfig');
// });
} catch (e) {
Logger.warning('加载MCP配置失败: $e');
_mcpConfig = null;
}
}
/// 处理来自原生层的事件
void _handleVoiceInteractionEvent(dynamic event) {
if (event is! Map) return;
final eventMap = event as Map<dynamic, dynamic>;
final String eventType = eventMap['type'] as String? ?? '';
// final int timestamp = eventMap['timestamp'] as int? ?? DateTime.now().millisecondsSinceEpoch;
Logger.info('收到语音交互事件: $eventType');
switch (eventType) {
case 'recognition_started':
// 语音识别开始事件
@ -110,13 +128,14 @@ class VoiceInteractionService extends GetxService {
);
_eventStreamController.add(recognitionEvent);
break;
case 'chat_history_updated':
// 聊天历史更新事件
final String agentId = eventMap['agentId'] as String? ?? '';
final String userMessage = eventMap['userMessage'] as String? ?? '';
final String assistantMessage = eventMap['assistantMessage'] as String? ?? '';
final String assistantMessage =
eventMap['assistantMessage'] as String? ?? '';
final chatHistoryEvent = ChatHistoryEvent(
agentId: agentId,
userMessage: userMessage,
@ -124,72 +143,74 @@ class VoiceInteractionService extends GetxService {
timestamp: DateTime.now().millisecondsSinceEpoch,
);
_eventStreamController.add(chatHistoryEvent);
// 保存聊天历史到ChatHistoryService
_saveChatHistory(agentId, userMessage, assistantMessage);
break;
case 'enter_translation_mode':
// 进入翻译模式事件
Logger.info('收到进入翻译模式事件,正在导航到翻译界面');
_navigateToTranslation();
break;
}
}
/// 导航到翻译界面
void _navigateToTranslation() {
try {
// 使用GetX导航到翻译页面
Get.offNamedUntil(Routes.translation, (route) => route.settings.name == Routes.home);
Get.offNamedUntil(
Routes.translation, (route) => route.settings.name == Routes.home);
Logger.info('已导航到翻译界面');
} catch (e) {
Logger.error('导航到翻译界面失败: $e');
}
}
/// 保存聊天历史
Future<void> _saveChatHistory(String agentId, String userMessage, String assistantMessage) async {
Future<void> _saveChatHistory(
String agentId, String userMessage, String assistantMessage) async {
try {
final timestamp = DateTime.now().millisecondsSinceEpoch;
// 创建用户消息和助手消息
final userMsg = Message(
role: 'user',
content: userMessage,
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp),
);
final assistantMsg = Message(
role: 'assistant',
content: assistantMessage,
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp + 1), // 确保助手消息时间戳晚于用户消息
timestamp: DateTime.fromMillisecondsSinceEpoch(
timestamp + 1), // 确保助手消息时间戳晚于用户消息
);
// 加载现有历史记录
final existingMessages = await _chatHistoryService.loadHistory(agentId);
// 添加新消息
existingMessages.addAll([userMsg, assistantMsg]);
// 保存更新后的历史记录
await _chatHistoryService.saveHistory(agentId, existingMessages);
Logger.info('已保存聊天历史: agentId=$agentId');
} catch (e) {
Logger.error('保存聊天历史失败: $e');
}
}
/// 初始化服务
Future<bool> initialize() async {
if (_isInitialized) return true;
try {
Logger.info('正在初始化语音交互服务...');
// 获取聊天历史服务
try {
_chatHistoryService = Get.find<ChatHistoryService>();
@ -197,16 +218,16 @@ class VoiceInteractionService extends GetxService {
Logger.warning('获取ChatHistoryService失败,将创建新实例');
_chatHistoryService = Get.put(ChatHistoryService());
}
// 设置事件通道
_setupEventChannel();
// 加载配置
_loadConfig();
// 检查服务是否已在运行
final bool running = await checkServiceStatus();
// 如果服务未运行,启动服务
if (!running) {
// 启动语音交互服务
@ -216,7 +237,7 @@ class VoiceInteractionService extends GetxService {
return false;
}
}
_isInitialized = true;
Logger.info('语音交互服务初始化完成');
return true;
@ -225,12 +246,12 @@ class VoiceInteractionService extends GetxService {
return false;
}
}
/// 启动语音交互服务
Future<bool> startService() async {
try {
Logger.info('启动语音交互服务...');
// 构建参数
final Map<String, dynamic> params = {
'azure_speech_key': _azureSpeechKey,
@ -241,53 +262,55 @@ class VoiceInteractionService extends GetxService {
'volcano_speech_app_id': _volcanoSpeechAppId,
'volcano_speech_app_token': _volcanoSpeechAppToken,
};
// 如果有MCP配置,添加到参数中
if (_mcpConfig != null) {
params['mcp_config'] = _mcpConfig;
}
final result = await _channel.invokeMethod<bool>('startService', params) ?? false;
final result =
await _channel.invokeMethod<bool>('startService', params) ?? false;
if (result) {
_isServiceRunning.value = true;
Logger.info('语音交互服务已启动');
} else {
Logger.error('启动语音交互服务失败');
}
return result;
} catch (e) {
Logger.error('启动语音交互服务时发生错误: $e');
return false;
}
}
/// 停止语音交互服务
Future<bool> stopService() async {
try {
Logger.info('停止语音交互服务...');
final result = await _channel.invokeMethod<bool>('stopService') ?? false;
if (result) {
_isServiceRunning.value = false;
Logger.info('语音交互服务已停止');
} else {
Logger.error('停止语音交互服务失败');
}
return result;
} catch (e) {
Logger.error('停止语音交互服务时发生错误: $e');
return false;
}
}
/// 检查服务是否运行
Future<bool> checkServiceStatus() async {
try {
final bool result = await _channel.invokeMethod<bool>('isServiceRunning') ?? false;
final bool result =
await _channel.invokeMethod<bool>('isServiceRunning') ?? false;
_isServiceRunning.value = result;
return result;
} catch (e) {
@ -295,27 +318,28 @@ class VoiceInteractionService extends GetxService {
return false;
}
}
/// 暂停语音交互
Future<bool> pauseVoiceInteraction() async {
try {
Logger.info('暂停后台语音交互...');
final result = await _channel.invokeMethod<bool>('pauseVoiceInteraction') ?? false;
final result =
await _channel.invokeMethod<bool>('pauseVoiceInteraction') ?? false;
if (result) {
Logger.info('语音交互已暂停');
} else {
Logger.error('暂停语音交互失败');
}
return result;
} catch (e) {
Logger.error('暂停语音交互时发生错误: $e');
return false;
}
}
/// 资源释放
@override
void onClose() {
@ -323,4 +347,4 @@ class VoiceInteractionService extends GetxService {
_eventStreamController.close();
super.onClose();
}
}
}

133
lib/data/services/volcano_translation_service.dart

@ -1,5 +1,6 @@
import 'dart:convert';
import 'dart:math';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart' as http;
import 'package:crypto/crypto.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
@ -10,7 +11,7 @@ import 'language_manager.dart';
class VolcanoTranslationService extends GetxService {
// API 配置
late String _baseUrl;
final String _endpoint = '/'; // 修改为根路径
final String _endpoint = '/'; // 修改为根路径
late final String _accessKey;
late final String _secretKey;
late final String _region;
@ -21,7 +22,7 @@ class VolcanoTranslationService extends GetxService {
// 语言管理器
final LanguageManager _languageManager = Get.find<LanguageManager>();
final GetStorage _storage = GetStorage();
// 构造函数
VolcanoTranslationService();
@ -32,9 +33,10 @@ class VolcanoTranslationService extends GetxService {
String? region,
}) async {
try {
_accessKey = accessKey ?? dotenv.env['VOLCANO_TRANSLATION_ACCESS_KEY'] ?? '';
_secretKey = secretKey ?? dotenv.env['VOLCANO_TRANSLATION_SECRET_KEY'] ?? '';
_region = region ?? dotenv.env['VOLCANO_TRANSLATION_REGION'] ?? 'cn-north-1';
final _env = _storage.read("ENV") as Map<String, String>;
_accessKey = accessKey ?? _env['VOLCANO_TRANSLATION_ACCESS_KEY'] ?? '';
_secretKey = secretKey ?? _env['VOLCANO_TRANSLATION_SECRET_KEY'] ?? '';
_region = region ?? _env['VOLCANO_TRANSLATION_REGION'] ?? 'cn-north-1';
_service = 'translate';
_baseUrl = 'https://translate.volcengineapi.com';
@ -50,14 +52,21 @@ class VolcanoTranslationService extends GetxService {
}
// 生成签名
Map<String, String> _generateSignature(String method, Map<String, dynamic> requestBody, Map<String, String> queryParams) {
Map<String, String> _generateSignature(String method,
Map<String, dynamic> requestBody, Map<String, String> queryParams) {
// 1. 准备时间相关参数
final now = DateTime.now().toUtc();
final date = now.toIso8601String().split('T')[0].replaceAll('-', '');
final timestamp = now.toIso8601String().replaceAll(':', '').replaceAll('-', '').split('.')[0] + 'Z';
final timestamp = now
.toIso8601String()
.replaceAll(':', '')
.replaceAll('-', '')
.split('.')[0] +
'Z';
// 2. 构建规范查询字符串 - 按照参数名ASCII排序
final sortedParams = Map.fromEntries(queryParams.entries.toList()..sort((a, b) => a.key.compareTo(b.key)));
final sortedParams = Map.fromEntries(
queryParams.entries.toList()..sort((a, b) => a.key.compareTo(b.key)));
final List<String> queryParts = [];
sortedParams.forEach((key, value) {
// 使用RFC 3986编码,确保特殊字符被正确处理
@ -66,48 +75,53 @@ class VolcanoTranslationService extends GetxService {
queryParts.add('$encodedKey=$encodedValue');
});
final canonicalQueryString = queryParts.join('&');
// 3. 创建规范请求
final contentType = 'application/json';
final payloadHash = sha256.convert(utf8.encode(json.encode(requestBody))).toString();
final payloadHash =
sha256.convert(utf8.encode(json.encode(requestBody))).toString();
final host = 'translate.volcengineapi.com';
// 确保头部按照字典序排序,且键名小写
// 注意:只包含host和x-date头部,与示例保持一致
final canonicalHeaders = 'host:$host\nx-date:$timestamp\n';
final signedHeaders = 'host;x-date';
// 构建规范请求,格式必须与示例完全一致
final canonicalRequest = '$method\n$_endpoint\n$canonicalQueryString\n$canonicalHeaders\n$signedHeaders\n$payloadHash';
final canonicalRequest =
'$method\n$_endpoint\n$canonicalQueryString\n$canonicalHeaders\n$signedHeaders\n$payloadHash';
// 4. 创建待签字符串
final algorithm = 'HMAC-SHA256';
final credentialScope = '$date/$_region/$_service/request';
final stringToSign = '$algorithm\n$timestamp\n$credentialScope\n${sha256.convert(utf8.encode(canonicalRequest)).toString()}';
final stringToSign =
'$algorithm\n$timestamp\n$credentialScope\n${sha256.convert(utf8.encode(canonicalRequest)).toString()}';
// 5. 计算签名 - 按照文档正确派生密钥
// 注意:这里是关键修复点,确保密钥派生过程完全符合文档
final kSecret = utf8.encode(_secretKey); // 直接使用原始密钥,不添加前缀
final kSecret = utf8.encode(_secretKey); // 直接使用原始密钥,不添加前缀
// kDate = HMAC(kSecret, Date)
var kDate = Hmac(sha256, kSecret).convert(utf8.encode(date)).bytes;
// kRegion = HMAC(kDate, Region)
var kRegion = Hmac(sha256, kDate).convert(utf8.encode(_region)).bytes;
// kService = HMAC(kRegion, Service)
var kService = Hmac(sha256, kRegion).convert(utf8.encode(_service)).bytes;
// kSigning = HMAC(kService, "request")
var kSigning = Hmac(sha256, kService).convert(utf8.encode('request')).bytes;
// 使用派生的签名密钥计算最终签名
final signature = Hmac(sha256, kSigning).convert(utf8.encode(stringToSign)).toString();
final signature =
Hmac(sha256, kSigning).convert(utf8.encode(stringToSign)).toString();
// 6. 添加签名到请求头 - 格式必须与示例完全一致
final authorization = '$algorithm Credential=$_accessKey/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature';
final authorization =
'$algorithm Credential=$_accessKey/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature';
// 只返回必要的头部,与示例保持一致
return {
'Content-Type': contentType,
@ -131,27 +145,29 @@ class VolcanoTranslationService extends GetxService {
// 获取源语言和目标语言的短代码
String sourceCode = '';
String targetCode = 'zh';
// 从 ASR 代码获取短代码
if (sourceLanguageCode.isNotEmpty) {
final sourceShortCode = _languageManager.getShortCodeByAsrCode(sourceLanguageCode);
final sourceShortCode =
_languageManager.getShortCodeByAsrCode(sourceLanguageCode);
if (sourceShortCode != null) {
sourceCode = sourceShortCode;
}
}
final targetShortCode = _languageManager.getShortCodeByAsrCode(targetLanguageCode);
final targetShortCode =
_languageManager.getShortCodeByAsrCode(targetLanguageCode);
if (targetShortCode != null) {
targetCode = targetShortCode;
}
// 构建请求体
final requestBody = {
'SourceLanguage': sourceCode,
'TargetLanguage': targetCode,
'TextList': [text],
};
// 添加必要的查询参数 - 确保包含所有必需参数
final queryParams = {
'Action': 'TranslateText',
@ -159,60 +175,61 @@ class VolcanoTranslationService extends GetxService {
'Region': _region,
'Service': _service,
};
// 生成包含查询参数的签名
final headers = _generateSignature('POST', requestBody, queryParams);
// 构建带有查询参数的URL
final queryString = Uri(queryParameters: queryParams).query;
final url = '$_baseUrl/?$queryString';
final response = await http.post(
Uri.parse(url),
headers: headers,
body: json.encode(requestBody),
);
if (response.statusCode == 200) {
final responseData = json.decode(response.body);
// 尝试从不同的响应结构中提取翻译结果
String? translation;
// 1. 标准响应结构
if (responseData['TranslationList'] != null &&
responseData['TranslationList'] is List &&
if (responseData['TranslationList'] != null &&
responseData['TranslationList'] is List &&
responseData['TranslationList'].isNotEmpty) {
final translationItem = responseData['TranslationList'][0];
if (translationItem is Map && translationItem.containsKey('Translation')) {
if (translationItem is Map &&
translationItem.containsKey('Translation')) {
translation = translationItem['Translation'];
}
}
// 2. 检查是否有错误响应
if (translation == null &&
responseData['ResponseMetadata'] != null &&
responseData['ResponseMetadata'] is Map &&
if (translation == null &&
responseData['ResponseMetadata'] != null &&
responseData['ResponseMetadata'] is Map &&
responseData['ResponseMetadata']['Error'] != null) {
return null;
}
// 3. 尝试其他可能的响应结构
if (translation == null) {
if (responseData.containsKey('Translation')) {
translation = responseData['Translation'];
} else if (responseData.containsKey('Result') &&
responseData['Result'] is Map &&
responseData['Result'].containsKey('Translation')) {
} else if (responseData.containsKey('Result') &&
responseData['Result'] is Map &&
responseData['Result'].containsKey('Translation')) {
translation = responseData['Result']['Translation'];
} else if (responseData.containsKey('Data') &&
responseData['Data'] is Map) {
} else if (responseData.containsKey('Data') &&
responseData['Data'] is Map) {
final data = responseData['Data'];
if (data.containsKey('Translation')) {
translation = data['Translation'];
} else if (data.containsKey('TranslationList') &&
data['TranslationList'] is List &&
data['TranslationList'].isNotEmpty) {
} else if (data.containsKey('TranslationList') &&
data['TranslationList'] is List &&
data['TranslationList'].isNotEmpty) {
final item = data['TranslationList'][0];
if (item is Map && item.containsKey('Translation')) {
translation = item['Translation'];
@ -220,14 +237,14 @@ class VolcanoTranslationService extends GetxService {
}
}
}
// 返回提取到的翻译结果
return translation;
}
return null;
} catch (e) {
return null;
}
}
}
}

24
lib/modules/login/controllers/login_controller.dart

@ -1,3 +1,4 @@
import 'package:deep_voice/data/models/appconfig_model.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:logger/web.dart';
@ -695,7 +696,8 @@ class LoginController extends GetxController {
//final prefs = await GetStorage();
await _storage.write(
'user', jsonEncode(user.toJson())); // 使用 user.toJson()
//获取app的配置信息
await getAppConfig();
Get.offAllNamed(Routes.pairing);
// } else {
// throw FormatException('API 响应缺少 user 字段');
@ -725,4 +727,24 @@ class LoginController extends GetxController {
colorText: Colors.blue,
);
}
Future<bool> getAppConfig() async {
try {
print('getAppConfig:进来了');
//if (User.getname() != "") {
final response = await Api.getappconfig();
print('getAppConfig: $response');
if (response == null) return false;
print('getAppConfig:进来了');
// 根据API设计调整
final resp = UserGetAppConfigResp.fromJson(response);
// 存储用户信息(示例)
_storage.write('ENV', resp.env);
_storage.write('MCPS', resp.mcps);
return true;
} catch (e) {
print('getAppConfig 取失败: $e');
return false;
}
}
}

23
lib/modules/splash/controllers/splash_controller.dart

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:amap_flutter/amap_flutter.dart';
import 'package:deep_voice/data/models/appconfig_model.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
@ -88,6 +89,7 @@ class SplashController extends GetxController {
isLoggedIn = await getLoginToken();
}
if (isLoggedIn) {
await getAppConfig();
Get.offAllNamed(Routes.pairing);
} else {
Get.offAllNamed(Routes.login); // 跳转到登录页
@ -118,4 +120,25 @@ class SplashController extends GetxController {
return false;
}
}
Future<bool> getAppConfig() async {
try {
print('getAppConfig:进来了');
//if (User.getname() != "") {
final response = await Api.getappconfig();
print('getAppConfig: $response');
if (response == null) return false;
// 根据API设计调整
final resp = UserGetAppConfigResp.fromJson(response);
print('getAppConfig:进来了');
// 存储用户信息(示例)
_storage.write('ENV', resp.env);
_storage.write('MCPS', resp.mcps);
return true;
} catch (e) {
print('getAppConfig 取失败: $e');
return false;
}
}
}

2
local_plugins/open_ai_service/android/build.gradle.kts

@ -4,6 +4,7 @@ plugins {
// Kotlin Android 插件
id("org.jetbrains.kotlin.android")
id("com.github.johnrengelman.shadow") version "8.1.1"
kotlin("plugin.serialization") version "1.9.24"
}
android {
@ -58,4 +59,5 @@ dependencies {
implementation("io.ktor:ktor-client-okhttp:3.1.2")
implementation("io.ktor:ktor-client-logging:3.1.2")
implementation("com.google.code.gson:gson:2.10.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
}

63
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt

@ -35,7 +35,17 @@ import io.ktor.http.*
import com.yunqiinnovation.open_ai_service.mcp.CustomSseClientTransport
import io.ktor.util.valuesOf
import okio.Buffer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
@Serializable
data class ServerConfig(
val servername: String,
val url: String,
val tools: List<String>? = null
)
/**
* 工具类型枚举
*/
@ -62,13 +72,13 @@ interface FunctionHandler {
*/
class MCPSubClient(
private val serverId: String,
serverUrl: String,
private val config: ServerConfig,
private val customClient: HttpClient? = null
) : AutoCloseable {
private val TAG = "MCPSubClient[$serverId]"
// 保存服务器URL
private val serverUrl: String = serverUrl
private val serverUrl: String = config.url
init {
Log.d(TAG, "MCPSubClient初始化,URL: $serverUrl")
@ -81,7 +91,6 @@ class MCPSubClient(
version = "1.0.0"
)
)
// 服务器提供的工具列表
private var tools: List<ToolUnion> = emptyList()
@ -157,26 +166,12 @@ class MCPSubClient(
* 处理工具列表
*/
private fun processToolsList(toolsResult: ListToolsResult?) {
try {
// 将MCP工具列表转换为ToolUnion列表
tools = toolsResult?.tools?.map { tool ->
// 转换为ToolUnion格式
ToolUnion.ofTool(
Tool.builder()
.name(tool.name)
.description(tool.description ?: "")
.inputSchema(
Tool.InputSchema.builder()
.type(JsonValue.from(tool.inputSchema?.type ?: "object"))
.properties(if (tool.inputSchema?.properties is JsonObject)
(tool.inputSchema.properties as JsonObject).toJsonValue()
else JsonValue.from(emptyMap<String, String>()))
.putAdditionalProperty("required", JsonValue.from(tool.inputSchema?.required ?: emptyList<String>()))
.build()
)
.build()
)
} ?: emptyList()
tools = toolsResult?.tools?.filter { tool -> config.tools == null || config.tools.contains(tool.name) == true
}?.map { tool ->
// 确保输入模式有效
val inputType = tool.inputSchema?.type ?: "object"
val inputProperties = tool.inputSchema?.properties ?: emptyMap()
val inputRequired = tool.inputSchema?.required ?: emptyList<String>()
// 更新工具映射
toolMapping.clear()
@ -488,7 +483,7 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
// 清除现有连接
closeAllConnections()
val serverMap = mutableMapOf<String, String>()
val serverMap = mutableMapOf<String, ServerConfig>()
var configLoaded = false
// 尝试解析传入的JSON配置
@ -496,17 +491,19 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
if (mcpConfigJson.isNotEmpty()) {
val mapper = ObjectMapper()
try {
val config = mapper.readValue(mcpConfigJson, Map::class.java)
Log.d(TAG, "传入JSON:${mcpConfigJson}")
val configMap: Map<String, ServerConfig> = Json.decodeFromString(mcpConfigJson)
// val config = mapper.readValue(mcpConfigJson, Map::class.java)
// 处理mcpServers格式
@Suppress("UNCHECKED_CAST")
val mcpServers = config["mcpServers"] as? Map<String, Map<String, String>>
if (mcpServers != null && mcpServers.isNotEmpty()) {
// val mcpServers = config["mcpServers"] as? Map<String, Map<String, String>>
if (configMap.isNotEmpty()) {
// 从服务器配置中提取URL
mcpServers.forEach { (serverId, serverConfig) ->
val url = serverConfig["url"]
configMap.forEach { (serverId, serverConfig) ->
val url = serverConfig.url
if (!url.isNullOrEmpty()) {
serverMap[serverId] = url
serverMap[serverId] = serverConfig
Log.d(TAG, "从配置中添加服务器 [$serverId]: $url")
}
}
@ -525,10 +522,10 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
// 创建并连接所有子客户端
val connectionResults = mutableMapOf<String, Boolean>()
for ((serverId, serverUrl) in serverMap) {
for ((serverId, serverConfig) in serverMap) {
try {
Log.d(TAG, "为服务器 [$serverId],[$serverUrl] 创建子客户端")
val subClient = MCPSubClient(serverId, serverUrl, sharedHttpClient)
Log.d(TAG, "为服务器 [$serverId],[${serverConfig.url}] 创建子客户端")
val subClient = MCPSubClient(serverId, serverConfig, sharedHttpClient)
val connected = subClient.connect()
if (connected) {

311
local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkService.kt

@ -0,0 +1,311 @@
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
import com.spotify.android.appremote.api.PlayerApi
import com.spotify.android.appremote.api.SpotifyAppRemote
import com.spotify.android.appremote.api.error.AuthenticationFailedException
import com.spotify.android.appremote.api.error.CouldNotFindSpotifyApp
import com.spotify.android.appremote.api.error.NotLoggedInException
import com.spotify.android.appremote.api.error.OfflineModeException
import com.spotify.android.appremote.api.error.SpotifyConnectionTerminatedException
import com.spotify.android.appremote.api.error.SpotifyDisconnectedException
import com.spotify.android.appremote.api.error.SpotifyRemoteServiceException
import com.spotify.android.appremote.api.error.UnsupportedFeatureVersionException
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) {
// 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"
// event channels
private var playerContextChannel : EventChannel? = null
private var playerStateChannel : EventChannel? = null
private var capabilitiesChannel : EventChannel? = null
private var userStatusChannel : EventChannel? = null
private var connectionStatusChannel : EventChannel? = null
private val playerContextSubscription = "player_context_subscription"
private val playerStateSubscription = "player_state_subscription"
private val capabilitiesSubscription = "capabilities_subscription"
private val userStatusSubscription = "user_status_subscription"
private val connectionStatusSubscription = "connection_status_subscription"
//connecting
private val methodConnectToSpotify = "connectToSpotify"
private val methodGetAccessToken = "getAccessToken"
private val methodDisconnectFromSpotify = "disconnectFromSpotify"
// connectApi
private val methodSwitchToLocalDevice = "switchToLocalDevice"
//playerApi
private val methodGetCrossfadeState = "getCrossfadeState"
private val methodGetPlayerState = "getPlayerState"
private val methodPlay = "play"
private val methodPause = "pause"
private val methodQueueTrack = "queueTrack"
private val methodResume = "resume"
private val methodSeekToRelativePosition = "seekToRelativePosition"
private val methodSetPodcastPlaybackSpeed = "setPodcastPlaybackSpeed"
private val methodSkipNext = "skipNext"
private val methodSkipPrevious = "skipPrevious"
private val methodSkipToIndex = "skipToIndex"
private val methodSeekTo = "seekTo"
private val methodToggleRepeat = "toggleRepeat"
private val methodToggleShuffle = "toggleShuffle"
private val methodSetShuffle = "setShuffle"
private val methodSetRepeatMode = "setRepeatMode"
//userApi
private val methodAddToLibrary = "addToLibrary"
private val methodRemoveFromLibrary = "removeFromLibrary"
private val methodGetCapabilities = "getCapabilities"
private val methodGetLibraryState = "getLibraryState"
//imagesApi
private val methodGetImage = "getImage"
private val paramClientId = "clientId"
private val paramRedirectUrl = "redirectUrl"
private val paramScope = "scope"
private val paramSpotifyUri = "spotifyUri"
private val paramImageUri = "imageUri"
private val paramImageDimension = "imageDimension"
private val paramPositionedMilliseconds = "positionedMilliseconds"
private val paramRelativeMilliseconds = "relativeMilliseconds"
private val paramPodcastPlaybackSpeed = "podcastPlaybackSpeed"
private val paramTrackIndex = "trackIndex"
private val paramRepeatMode = "repeatMode"
private val paramShuffle = "shuffle"
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) {
if (clientId.isNullOrBlank() || redirectUrl.isNullOrBlank()) {
result.error(errorConnecting, "client id or redirectUrl are not set or have invalid format", "")
} else {
val connectionParams = ConnectionParams.Builder(clientId)
.setRedirectUri(redirectUrl)
.showAuthView(true)
.build()
SpotifyAppRemote.disconnect(spotifyAppRemote)
var initiallyConnected = false
SpotifyAppRemote.connect(applicationContext, connectionParams,
object : Connector.ConnectionListener {
override fun onConnected(spotifyAppRemoteValue: SpotifyAppRemote) {
spotifyAppRemote = spotifyAppRemoteValue
playerApi = spotifyAppRemote?.playerApi
initiallyConnected = true
Log.i(loggingTag, "App Remote successfully connected")
result.success(true)
}
override fun onFailure(throwable: Throwable) {
val errorDetails = throwable.toString()
// determine the error
val errorMessage: String
val errorCode: String
var connected = false
when (throwable) {
is SpotifyDisconnectedException, is SpotifyConnectionTerminatedException -> {
// The Spotify app was/is disconnected by the Spotify app.
// This indicates typically that the Spotify app was closed by the user or for other reasons.
// You need to reconnect to continue using Spotify App Remote.
errorMessage =
"The Spotify app was/is disconnected by the Spotify app.Reconnect necessary"
errorCode = "SpotifyDisconnectedException"
}
is CouldNotFindSpotifyApp -> {
errorMessage = "The Spotify app is not installed on the device"
errorCode = "CouldNotFindSpotifyApp"
}
is AuthenticationFailedException -> {
errorMessage =
"Partner app failed to authenticate with Spotify. Check client credentials and make sure your app is registered correctly at developer.spotify.com"
errorCode = "AuthenticationFailedException"
}
is UserNotAuthorizedException -> {
errorMessage =
"Indicates the user did not authorize this client of App Remote to use Spotify on the users behalf."
errorCode = "UserNotAuthorizedException"
}
is UnsupportedFeatureVersionException -> {
errorMessage =
"Spotify app can't support requested features. User should update Spotify app."
errorCode = "UnsupportedFeatureVersionException"
connected = true
}
is OfflineModeException -> {
errorMessage =
"Spotify user has set their Spotify app to be in offline mode"
errorCode = "OfflineModeException"
connected = true
}
is NotLoggedInException -> {
errorMessage = "User has logged out from Spotify."
errorCode = "NotLoggedInException"
}
is SpotifyRemoteServiceException -> {
errorMessage =
"Encapsulates possible SecurityException and IllegalStateException errors."
errorCode = "SpotifyRemoteServiceException"
}
else -> {
errorMessage = "Something went wrong connecting spotify remote"
errorCode = errorConnection
}
}
Log.e(loggingTag, errorMessage)
// notify plugin
if (initiallyConnected) {
// emit connection error event
// connStatusEventChannel(
// ConnectionStatusChannel.ConnectionEvent(
// connected,
// errorMessage,
// errorCode,
// errorDetails
// )
// )
} else {
// throw exception as the connect method
result.error(errorCode, errorMessage, errorDetails)
}
}
})
}
}
private fun getAccessToken(clientId: String?, redirectUrl: String?, scope: String?, result: MethodChannel.Result) {
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", "")
} 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,
redirectUrl
)
builder.setScopes(scopeArray)
val request = builder.build()
AuthorizationClient.openLoginActivity(
applicationActivity,
requestCodeAuthentication,
request
)
}
}
private fun disconnectFromSpotify(result: MethodChannel.Result) {
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)
} else if (!spotifyAppRemote!!.isConnected) {
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")
}
}
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()
// }
if (playerApi != null && !spotifyUri.isNullOrBlank()) {
playerApi!!.play(spotifyUri)
} else if (spotifyUri.isNullOrBlank()) {
} else {
}
}
private fun pause(){
// if (playerApi != null) {
// playerApi.pause()
// .setResultCallback { result.success(true) }
// .setErrorCallback { throwable -> result.error(errorPause, "error when pausing", throwable.toString()) }
// } else {
// spotifyRemoteAppNotSetError()
// }
if (playerApi != null) {
playerApi!!.pause()
.setResultCallback {}
.setErrorCallback { }
} else {
}
}
}

3
pubspec.yaml

@ -13,6 +13,7 @@ dependencies:
sdk: flutter
cupertino_icons: ^1.0.6
get: ^4.6.6
json_annotation: ^4.9.0
get_storage: ^2.1.1
http: ^1.3.0
flutter_screenutil: ^5.9.0
@ -84,6 +85,8 @@ dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
build_runner: ^2.3.3 # 用于生成代码 flutter pub run build_runner build
json_serializable: ^6.6.0 # 用于自动生成代码
flutter:
uses-material-design: true

Loading…
Cancel
Save