Browse Source

上传获取服务器的配置信息

weicu
liwei1dao 2 years ago
parent
commit
676cabd4f0
  1. 30
      lib/data/models/appconfig_model.dart
  2. 37
      lib/data/models/appconfig_model.g.dart
  3. 9
      lib/data/services/network/api.dart
  4. 45
      lib/data/services/open_ai_service_adapter.dart
  5. 132
      lib/data/services/speech_impl/azure_asr_service.dart
  6. 202
      lib/data/services/speech_impl/azure_tts_service.dart
  7. 184
      lib/data/services/voice_interaction_service.dart
  8. 24
      lib/modules/login/controllers/login_controller.dart
  9. 23
      lib/modules/splash/controllers/splash_controller.dart
  10. 2
      local_plugins/open_ai_service/android/build.gradle.kts
  11. 22
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt
  12. 3
      pubspec.yaml

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

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

@ -1,76 +1,79 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../asr_service.dart'; import '../asr_service.dart';
/// Azure 语音识别服务 /// Azure 语音识别服务
/// ///
/// 该服务提供了通过平台通道与 Android 上的 Microsoft Speech SDK 交互的接口 /// 该服务提供了通过平台通道与 Android 上的 Microsoft Speech SDK 交互的接口
class AzureAsrService extends GetxService implements AsrService { class AzureAsrService extends GetxService implements AsrService {
static final AzureAsrService to = Get.put(AzureAsrService()); static final AzureAsrService to = Get.put(AzureAsrService());
static const MethodChannel _channel = MethodChannel('azure_speech/asr'); 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; bool _isInitialized = false;
late final String _subscriptionKey; late final String _subscriptionKey;
late final String _serviceRegion; late final String _serviceRegion;
final List<String> _defaultSupportedLanguages = ['zh-CN']; final List<String> _defaultSupportedLanguages = ['zh-CN'];
@override @override
List<String> get supportedLanguages => _defaultSupportedLanguages; List<String> get supportedLanguages => _defaultSupportedLanguages;
// 连续识别相关 // 连续识别相关
bool _isContinuousRecognitionActive = false; bool _isContinuousRecognitionActive = false;
StreamController<RecognitionEvent>? _eventStreamController; StreamController<RecognitionEvent>? _eventStreamController;
StreamSubscription? _eventSubscription; StreamSubscription? _eventSubscription;
// 最新的识别结果 // 最新的识别结果
String _latestRecognizedText = ''; String _latestRecognizedText = '';
String get latestRecognizedText => _latestRecognizedText; String get latestRecognizedText => _latestRecognizedText;
// 最新检测到的语言 // 最新检测到的语言
String _latestDetectedLanguage = ''; String _latestDetectedLanguage = '';
String get latestDetectedLanguage => _latestDetectedLanguage; String get latestDetectedLanguage => _latestDetectedLanguage;
AzureAsrService() { AzureAsrService() {
_loadConfig(); _loadConfig();
} }
/// 从环境变量加载配置 /// 从环境变量加载配置
void _loadConfig() { void _loadConfig() {
_subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; final _env = _storage.read("ENV") as Map<String, String>;
_serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; _subscriptionKey = _env['AZURE_SPEECH_KEY'] ?? '';
_serviceRegion = _env['AZURE_SPEECH_REGION'] ?? '';
if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) { 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() { void _setupEventChannel() {
_eventSubscription = _eventChannel _eventSubscription = _eventChannel.receiveBroadcastStream().listen((event) {
.receiveBroadcastStream() if (event is Map) {
.listen((event) { _handleRecognitionEvent(event);
if (event is Map) { }
_handleRecognitionEvent(event); }, onError: _handleRecognitionError);
}
}, onError: _handleRecognitionError);
} }
@override @override
Future<bool> initialize({ Future<bool> initialize({
List<String>? supportedLanguages, List<String>? supportedLanguages,
}) async { }) async {
try { try {
final List<String> languages = supportedLanguages ?? _defaultSupportedLanguages; final List<String> languages =
supportedLanguages ?? _defaultSupportedLanguages;
// 检查是否需要重新初始化 // 检查是否需要重新初始化
if (_isInitialized) { if (_isInitialized) {
await dispose(); await dispose();
} }
final bool result = await _channel.invokeMethod('initialize', { final bool result = await _channel.invokeMethod('initialize', {
'subscriptionKey': _subscriptionKey, 'subscriptionKey': _subscriptionKey,
'region': _serviceRegion, 'region': _serviceRegion,
@ -87,22 +90,23 @@ class AzureAsrService extends GetxService implements AsrService {
rethrow; rethrow;
} }
} }
@override @override
Future<RecognitionEvent> recognizeOnce() async { Future<RecognitionEvent> recognizeOnce() async {
if (!_isInitialized) { if (!_isInitialized) {
await initialize(); await initialize();
} }
try { try {
final result = await _channel.invokeMethod('recognizeOnce'); final result = await _channel.invokeMethod('recognizeOnce');
if (result is Map) { if (result is Map) {
final String text = result['text'] as String? ?? ''; final String text = result['text'] as String? ?? '';
final String detectedLanguage = result['detectedLanguage'] as String? ?? ''; final String detectedLanguage =
result['detectedLanguage'] as String? ?? '';
_latestRecognizedText = text; _latestRecognizedText = text;
_latestDetectedLanguage = detectedLanguage; _latestDetectedLanguage = detectedLanguage;
return RecognitionEvent.finalResult( return RecognitionEvent.finalResult(
text: text, text: text,
detectedLanguage: detectedLanguage, detectedLanguage: detectedLanguage,
@ -112,35 +116,36 @@ class AzureAsrService extends GetxService implements AsrService {
_latestRecognizedText = result; _latestRecognizedText = result;
return RecognitionEvent.finalResult(text: result); return RecognitionEvent.finalResult(text: result);
} }
throw Exception('无效的识别结果格式'); throw Exception('无效的识别结果格式');
} catch (e) { } catch (e) {
Logger.error('语音识别失败: $e'); Logger.error('语音识别失败: $e');
return RecognitionEvent.error(e.toString()); return RecognitionEvent.error(e.toString());
} }
} }
@override @override
Future<Stream<RecognitionEvent>> startContinuousRecognition() async { Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
if (!_isInitialized) { if (!_isInitialized) {
await initialize(); await initialize();
} }
if (_isContinuousRecognitionActive) { if (_isContinuousRecognitionActive) {
await stopContinuousRecognition(); await stopContinuousRecognition();
} }
try { try {
_eventStreamController = StreamController<RecognitionEvent>.broadcast(); _eventStreamController = StreamController<RecognitionEvent>.broadcast();
// 开始连续识别 // 开始连续识别
final bool result = await _channel.invokeMethod('startContinuousRecognition'); final bool result =
await _channel.invokeMethod('startContinuousRecognition');
if (!result) { if (!result) {
_cleanupEventStream(); _cleanupEventStream();
throw Exception('启动连续识别失败'); throw Exception('启动连续识别失败');
} }
_isContinuousRecognitionActive = true; _isContinuousRecognitionActive = true;
return _eventStreamController!.stream; return _eventStreamController!.stream;
} catch (e) { } catch (e) {
@ -149,15 +154,16 @@ class AzureAsrService extends GetxService implements AsrService {
rethrow; rethrow;
} }
} }
@override @override
Future<bool> stopContinuousRecognition() async { Future<bool> stopContinuousRecognition() async {
if (!_isInitialized || !_isContinuousRecognitionActive) { if (!_isInitialized || !_isContinuousRecognitionActive) {
return true; return true;
} }
try { try {
final bool result = await _channel.invokeMethod('stopContinuousRecognition'); final bool result =
await _channel.invokeMethod('stopContinuousRecognition');
_cleanupEventStream(); _cleanupEventStream();
return result; return result;
} catch (e) { } catch (e) {
@ -166,26 +172,27 @@ class AzureAsrService extends GetxService implements AsrService {
rethrow; rethrow;
} }
} }
@override @override
bool isContinuousRecognitionActive() { bool isContinuousRecognitionActive() {
return _isContinuousRecognitionActive; return _isContinuousRecognitionActive;
} }
/// 处理来自原生端的识别事件 /// 处理来自原生端的识别事件
void _handleRecognitionEvent(dynamic event) { void _handleRecognitionEvent(dynamic event) {
if (event is! Map || _eventStreamController == null) return; if (event is! Map || _eventStreamController == null) return;
final Map<dynamic, dynamic> eventMap = event; final Map<dynamic, dynamic> eventMap = event;
final String eventType = eventMap['type'] as String? ?? ''; final String eventType = eventMap['type'] as String? ?? '';
// 添加日志帮助调试 // 添加日志帮助调试
Logger.debug('收到语音识别事件: $eventType, 数据: $eventMap'); Logger.debug('收到语音识别事件: $eventType, 数据: $eventMap');
switch (eventType) { switch (eventType) {
case 'result': case 'result':
final String text = eventMap['text'] as String? ?? ''; final String text = eventMap['text'] as String? ?? '';
final String detectedLanguage = eventMap['detectedLanguage'] as String? ?? ''; final String detectedLanguage =
eventMap['detectedLanguage'] as String? ?? '';
_latestRecognizedText = text; _latestRecognizedText = text;
_latestDetectedLanguage = detectedLanguage; _latestDetectedLanguage = detectedLanguage;
_eventStreamController?.add(RecognitionEvent( _eventStreamController?.add(RecognitionEvent(
@ -194,45 +201,46 @@ class AzureAsrService extends GetxService implements AsrService {
detectedLanguage: detectedLanguage, detectedLanguage: detectedLanguage,
)); ));
break; break;
case 'recognizing': case 'recognizing':
final String text = eventMap['text'] as String? ?? ''; final String text = eventMap['text'] as String? ?? '';
final String detectedLanguage = eventMap['detectedLanguage'] as String? ?? ''; final String detectedLanguage =
eventMap['detectedLanguage'] as String? ?? '';
_eventStreamController?.add(RecognitionEvent( _eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.intermediateResult, type: RecognitionEventType.intermediateResult,
text: text, text: text,
detectedLanguage: detectedLanguage, detectedLanguage: detectedLanguage,
)); ));
break; break;
case 'sessionStarted': case 'sessionStarted':
_eventStreamController?.add(RecognitionEvent( _eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.sessionStarted, type: RecognitionEventType.sessionStarted,
)); ));
break; break;
case 'sessionStopped': case 'sessionStopped':
_isContinuousRecognitionActive = false; _isContinuousRecognitionActive = false;
_eventStreamController?.add(RecognitionEvent( _eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.sessionStopped, type: RecognitionEventType.sessionStopped,
)); ));
break; break;
case 'canceled': case 'canceled':
_isContinuousRecognitionActive = false; _isContinuousRecognitionActive = false;
final String reason = eventMap['reason'] as String? ?? ''; final String reason = eventMap['reason'] as String? ?? '';
final String errorDetails = eventMap['errorDetails'] as String? ?? ''; final String errorDetails = eventMap['errorDetails'] as String? ?? '';
if (reason.isNotEmpty || errorDetails.isNotEmpty) { if (reason.isNotEmpty || errorDetails.isNotEmpty) {
Logger.error('识别取消: $reason - $errorDetails'); Logger.error('识别取消: $reason - $errorDetails');
} }
_eventStreamController?.add(RecognitionEvent( _eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.canceled, type: RecognitionEventType.canceled,
error: '$reason: $errorDetails', error: '$reason: $errorDetails',
)); ));
break; break;
case 'error': case 'error':
final String error = eventMap['message'] as String? ?? ''; final String error = eventMap['message'] as String? ?? '';
Logger.error('识别错误: $error'); Logger.error('识别错误: $error');
@ -243,36 +251,36 @@ class AzureAsrService extends GetxService implements AsrService {
break; break;
} }
} }
/// 处理识别事件流错误 /// 处理识别事件流错误
void _handleRecognitionError(Object error) { void _handleRecognitionError(Object error) {
Logger.error('识别事件流错误: $error'); Logger.error('识别事件流错误: $error');
_eventStreamController?.addError(error); _eventStreamController?.addError(error);
_cleanupEventStream(); _cleanupEventStream();
} }
/// 清理事件流资源 /// 清理事件流资源
void _cleanupEventStream() { void _cleanupEventStream() {
_eventStreamController?.close(); _eventStreamController?.close();
_eventStreamController = null; _eventStreamController = null;
_isContinuousRecognitionActive = false; _isContinuousRecognitionActive = false;
} }
@override @override
Future<void> dispose() async { Future<void> dispose() async {
try { try {
if (_isContinuousRecognitionActive) { if (_isContinuousRecognitionActive) {
await stopContinuousRecognition(); await stopContinuousRecognition();
} }
// 取消事件订阅 // 取消事件订阅
await _eventSubscription?.cancel(); await _eventSubscription?.cancel();
_eventSubscription = null; _eventSubscription = null;
// 通知原生端释放资源 // 通知原生端释放资源
await _channel.invokeMethod('dispose'); await _channel.invokeMethod('dispose');
_isInitialized = false; _isInitialized = false;
Logger.info('Azure 语音识别资源已释放'); Logger.info('Azure 语音识别资源已释放');
} catch (e) { } catch (e) {
Logger.error('释放语音识别资源失败: $e'); Logger.error('释放语音识别资源失败: $e');
@ -280,4 +288,4 @@ class AzureAsrService extends GetxService implements AsrService {
_isInitialized = false; _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:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import '../tts_service.dart'; import '../tts_service.dart';
/// 微软 Text-to-Speech 服务 /// 微软 Text-to-Speech 服务
/// ///
/// 该服务通过平台通道与原生 Microsoft Speech SDK 交互, /// 该服务通过平台通道与原生 Microsoft Speech SDK 交互,
/// 提供文本转语音功能。 /// 提供文本转语音功能。
class AzureTtsService extends GetxService implements TtsService { class AzureTtsService extends GetxService implements TtsService {
static final AzureTtsService to = Get.put(AzureTtsService()); static final AzureTtsService to = Get.put(AzureTtsService());
static const MethodChannel _channel = MethodChannel('azure_speech/tts'); static const MethodChannel _channel = MethodChannel('azure_speech/tts');
final GetStorage _storage = GetStorage();
bool _isInitialized = false; bool _isInitialized = false;
late final String _subscriptionKey; late final String _subscriptionKey;
late final String _serviceRegion; late final String _serviceRegion;
// 当前设置 // 当前设置
String _currentVoice = 'zh-CN-XiaoxiaoNeural'; String _currentVoice = 'zh-CN-XiaoxiaoNeural';
int _currentRate = 0; int _currentRate = 0;
int _currentPitch = 0; int _currentPitch = 0;
int _currentVolume = 100; int _currentVolume = 100;
// 支持的语音列表 // 支持的语音列表
final List<String> _supportedVoices = [ final List<String> _supportedVoices = [
'zh-CN-XiaoxiaoNeural', 'zh-CN-XiaoxiaoNeural',
@ -32,103 +33,98 @@ class AzureTtsService extends GetxService implements TtsService {
'en-US-JennyNeural', 'en-US-JennyNeural',
'en-US-GuyNeural' 'en-US-GuyNeural'
]; ];
// 语音合成队列 // 语音合成队列
final List<_SpeechItem> _textQueue = []; final List<_SpeechItem> _textQueue = [];
bool _isProcessingQueue = false; bool _isProcessingQueue = false;
// 可观察状态 // 可观察状态
final isEnabled = true.obs; final isEnabled = true.obs;
// 流式文本缓冲区 // 流式文本缓冲区
String _streamBuffer = ''; String _streamBuffer = '';
/// 缓存的播放状态 /// 缓存的播放状态
bool _speakingCache = false; bool _speakingCache = false;
DateTime _lastSpeakingCheck = DateTime.now(); DateTime _lastSpeakingCheck = DateTime.now();
@override @override
String get currentVoice => _currentVoice; String get currentVoice => _currentVoice;
@override @override
bool get isSpeaking { bool get isSpeaking {
// 如果未初始化,直接返回false // 如果未初始化,直接返回false
if (!_isInitialized) return false; if (!_isInitialized) return false;
// 如果距离上次检查不到50ms,则使用缓存值 // 如果距离上次检查不到50ms,则使用缓存值
if (DateTime.now().difference(_lastSpeakingCheck).inMilliseconds < 50) { if (DateTime.now().difference(_lastSpeakingCheck).inMilliseconds < 50) {
return _speakingCache; return _speakingCache;
} }
// 否则异步更新缓存(但仍返回当前缓存值) // 否则异步更新缓存(但仍返回当前缓存值)
_checkSpeakingStatus(); _checkSpeakingStatus();
return _speakingCache; return _speakingCache;
} }
@override @override
Stream<TtsEvent> get onEvent => const Stream.empty(); Stream<TtsEvent> get onEvent => const Stream.empty();
@override @override
List<String> get supportedVoices => _supportedVoices; List<String> get supportedVoices => _supportedVoices;
AzureTtsService() { AzureTtsService() {
_loadConfig(); _loadConfig();
} }
/// 从环境变量加载配置 /// 从环境变量加载配置
void _loadConfig() { void _loadConfig() {
_subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; final _env = _storage.read("ENV") as Map<String, String>;
_serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; _subscriptionKey = _env['AZURE_SPEECH_KEY'] ?? '';
_serviceRegion = _env['AZURE_SPEECH_REGION'] ?? '';
} }
@override @override
Future<bool> initialize({ Future<bool> initialize({
List<String>? supportedLanguages, List<String>? supportedLanguages,
}) async { }) async {
// 检查是否需要重新初始化 // 检查是否需要重新初始化
if (_isInitialized) { if (_isInitialized) {
await dispose(); await dispose();
} }
try { try {
if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) { if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) {
Logger.error('未找到Azure语音服务配置'); Logger.error('未找到Azure语音服务配置');
return false; return false;
} }
// 更新支持的语音列表(如果提供) // 更新支持的语音列表(如果提供)
final String language = supportedLanguages?.first ?? 'zh-CN'; final String language = supportedLanguages?.first ?? 'zh-CN';
final result = await _channel.invokeMethod('initialize', {
final result = await _channel.invokeMethod('initialize', { 'subscriptionKey': _subscriptionKey,
'subscriptionKey': _subscriptionKey, 'region': _serviceRegion,
'region': _serviceRegion, 'language': language,
'language': language, });
});
_isInitialized = true;
return result;
_isInitialized = true;
return result;
} catch (e) { } catch (e) {
Logger.error('初始化失败: $e'); Logger.error('初始化失败: $e');
return false; return false;
} }
} }
@override @override
Future<bool> setVoice(String voiceName) async { Future<bool> setVoice(String voiceName) async {
if (!_isInitialized) await initialize(); if (!_isInitialized) await initialize();
if (voiceName == _currentVoice) return true; if (voiceName == _currentVoice) return true;
try { try {
final result = await _channel.invokeMethod('setVoice', { final result = await _channel.invokeMethod('setVoice', {
'voiceName': voiceName, 'voiceName': voiceName,
}); });
if (result) _currentVoice = voiceName; if (result) _currentVoice = voiceName;
return result; return result;
} catch (e) { } catch (e) {
@ -136,45 +132,46 @@ class AzureTtsService extends GetxService implements TtsService {
return false; 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(); if (!_isInitialized) await initialize();
try { try {
final result = await _channel.invokeMethod('setSpeechParams', { final result = await _channel.invokeMethod('setSpeechParams', {
'rate': rate, 'rate': rate,
'pitch': pitch, 'pitch': pitch,
'volume': volume, 'volume': volume,
}); });
if (result) { if (result) {
_currentRate = rate; _currentRate = rate;
_currentPitch = pitch; _currentPitch = pitch;
_currentVolume = volume; _currentVolume = volume;
} }
return result; return result;
} catch (e) { } catch (e) {
Logger.error('设置语音参数失败: $e'); Logger.error('设置语音参数失败: $e');
return false; return false;
} }
} }
@override @override
Future<bool> speakOnce(String text) async { Future<bool> speakOnce(String text) async {
if (!_isInitialized) await initialize(); if (!_isInitialized) await initialize();
if (!isEnabled.value || text.isEmpty) return false; if (!isEnabled.value || text.isEmpty) return false;
try { try {
// 主动设置缓存状态为true // 主动设置缓存状态为true
_speakingCache = true; _speakingCache = true;
final result = await _channel.invokeMethod('speakText', {'text': text}); final result = await _channel.invokeMethod('speakText', {'text': text});
// 调用完成后主动检查状态 // 调用完成后主动检查状态
_checkSpeakingStatus(); _checkSpeakingStatus();
return result == "OK"; return result == "OK";
} catch (e) { } catch (e) {
// 发生错误时重置状态 // 发生错误时重置状态
@ -183,21 +180,21 @@ class AzureTtsService extends GetxService implements TtsService {
return false; return false;
} }
} }
/// 播放SSML /// 播放SSML
Future<bool> speakSsmlOnce(String ssml) async { Future<bool> speakSsmlOnce(String ssml) async {
if (!_isInitialized) await initialize(); if (!_isInitialized) await initialize();
if (!isEnabled.value || ssml.isEmpty) return false; if (!isEnabled.value || ssml.isEmpty) return false;
try { try {
// 主动设置缓存状态为true // 主动设置缓存状态为true
_speakingCache = true; _speakingCache = true;
final result = await _channel.invokeMethod('speakSsml', {'ssml': ssml}); final result = await _channel.invokeMethod('speakSsml', {'ssml': ssml});
// 调用完成后主动检查状态 // 调用完成后主动检查状态
_checkSpeakingStatus(); _checkSpeakingStatus();
return result == "OK"; return result == "OK";
} catch (e) { } catch (e) {
// 发生错误时重置状态 // 发生错误时重置状态
@ -206,15 +203,16 @@ class AzureTtsService extends GetxService implements TtsService {
return false; return false;
} }
} }
/// 等待语音播放完成 /// 等待语音播放完成
Future<void> waitForSpeechCompletion({Duration timeout = const Duration(seconds: 30)}) async { Future<void> waitForSpeechCompletion(
{Duration timeout = const Duration(seconds: 30)}) async {
// 首先检查当前状态 // 首先检查当前状态
_checkSpeakingStatus(); _checkSpeakingStatus();
if (!_speakingCache) return; if (!_speakingCache) return;
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
while (_speakingCache && stopwatch.elapsed < timeout) { while (_speakingCache && stopwatch.elapsed < timeout) {
// 短暂等待后再次检查 // 短暂等待后再次检查
await Future.delayed(const Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
@ -227,31 +225,30 @@ class AzureTtsService extends GetxService implements TtsService {
}); });
} }
} }
@override @override
Future<bool> speakStream(String text) async { Future<bool> speakStream(String text) async {
if (!isEnabled.value || text.isEmpty) return false; if (!isEnabled.value || text.isEmpty) return false;
try { try {
// 添加文本到缓冲区 // 添加文本到缓冲区
_streamBuffer += text; _streamBuffer += text;
// 如果缓冲区为空,直接返回 // 如果缓冲区为空,直接返回
if (_streamBuffer.isEmpty) return true; if (_streamBuffer.isEmpty) return true;
// 使用正则表达式匹配句子,包括结束符号 // 使用正则表达式匹配句子,包括结束符号
// 匹配任意字符,直到遇到句子结束符号 // 匹配任意字符,直到遇到句子结束符号
final sentenceRegex = RegExp( 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; bool hasProcessed = false;
// 查找所有完整句子 // 查找所有完整句子
final matches = sentenceRegex.allMatches(_streamBuffer); final matches = sentenceRegex.allMatches(_streamBuffer);
final List<String> sentences = []; final List<String> sentences = [];
int lastMatchEnd = 0; int lastMatchEnd = 0;
for (final match in matches) { for (final match in matches) {
// 提取完整句子(包含结束符号) // 提取完整句子(包含结束符号)
final sentence = match.group(1)?.trim(); final sentence = match.group(1)?.trim();
@ -260,7 +257,7 @@ class AzureTtsService extends GetxService implements TtsService {
lastMatchEnd = match.end; lastMatchEnd = match.end;
} }
} }
// 处理找到的句子 // 处理找到的句子
for (final sentence in sentences) { for (final sentence in sentences) {
_textQueue.add(_SpeechItem( _textQueue.add(_SpeechItem(
@ -271,32 +268,32 @@ class AzureTtsService extends GetxService implements TtsService {
)); ));
hasProcessed = true; hasProcessed = true;
} }
// 更新缓冲区,只保留未完成的部分 // 更新缓冲区,只保留未完成的部分
if (lastMatchEnd > 0) { if (lastMatchEnd > 0) {
_streamBuffer = _streamBuffer.substring(lastMatchEnd); _streamBuffer = _streamBuffer.substring(lastMatchEnd);
} }
// 如果处理了文本并且队列未在处理中,开始处理队列 // 如果处理了文本并且队列未在处理中,开始处理队列
if (hasProcessed && !_isProcessingQueue) { if (hasProcessed && !_isProcessingQueue) {
_processQueue(); _processQueue();
} }
return true; return true;
} catch (e) { } catch (e) {
Logger.error('处理流式文本失败: $e'); Logger.error('处理流式文本失败: $e');
return false; return false;
} }
} }
@override @override
Future<bool> flushStream() async { Future<bool> flushStream() async {
if (_textQueue.isEmpty && _streamBuffer.isEmpty) return true; if (_textQueue.isEmpty && _streamBuffer.isEmpty) return true;
try { try {
// 处理缓冲区中可能的完整句子 // 处理缓冲区中可能的完整句子
await speakStream(''); await speakStream('');
// 如果缓冲区仍有剩余文本,将其添加到播放队列 // 如果缓冲区仍有剩余文本,将其添加到播放队列
if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) { if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) {
_textQueue.add(_SpeechItem( _textQueue.add(_SpeechItem(
@ -305,25 +302,25 @@ class AzureTtsService extends GetxService implements TtsService {
pitch: _currentPitch, pitch: _currentPitch,
volume: _currentVolume, volume: _currentVolume,
)); ));
// 清空缓冲区 // 清空缓冲区
_streamBuffer = ''; _streamBuffer = '';
// 如果队列未在处理中,开始处理队列 // 如果队列未在处理中,开始处理队列
if (!_isProcessingQueue) { if (!_isProcessingQueue) {
_processQueue(); _processQueue();
} }
} }
Logger.info('等待TTS队列播放完成,剩余${_textQueue.length}条'); Logger.info('等待TTS队列播放完成,剩余${_textQueue.length}条');
// 等待队列处理完成 // 等待队列处理完成
await _checkSpeakingStatus(); await _checkSpeakingStatus();
while (_isProcessingQueue || _speakingCache) { while (_isProcessingQueue || _speakingCache) {
await Future.delayed(const Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
await _checkSpeakingStatus(); await _checkSpeakingStatus();
} }
Logger.info('TTS队列播放完成'); Logger.info('TTS队列播放完成');
return true; return true;
} catch (e) { } catch (e) {
@ -331,33 +328,35 @@ class AzureTtsService extends GetxService implements TtsService {
return false; return false;
} }
} }
/// 处理语音合成队列 /// 处理语音合成队列
Future<void> _processQueue() async { Future<void> _processQueue() async {
if (_textQueue.isEmpty || _isProcessingQueue) return; if (_textQueue.isEmpty || _isProcessingQueue) return;
_isProcessingQueue = true; _isProcessingQueue = true;
try { try {
while (_textQueue.isNotEmpty) { while (_textQueue.isNotEmpty) {
if (!isEnabled.value) { if (!isEnabled.value) {
_textQueue.clear(); _textQueue.clear();
break; break;
} }
final item = _textQueue.removeAt(0); 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( await setSpeechParams(
rate: item.rate, rate: item.rate,
pitch: item.pitch, pitch: item.pitch,
volume: item.volume, volume: item.volume,
); );
} }
// 使用speakOnce播放当前项 // 使用speakOnce播放当前项
await speakOnce(item.text); await speakOnce(item.text);
// 等待播放完成 // 等待播放完成
await waitForSpeechCompletion(); await waitForSpeechCompletion();
} }
@ -367,26 +366,26 @@ class AzureTtsService extends GetxService implements TtsService {
_isProcessingQueue = false; _isProcessingQueue = false;
} }
} }
@override @override
Future<bool> stop() async { Future<bool> stop() async {
try { try {
_textQueue.clear(); _textQueue.clear();
if (_speakingCache) { if (_speakingCache) {
final result = await _channel.invokeMethod('stopSpeaking'); final result = await _channel.invokeMethod('stopSpeaking');
// 重置缓存状态 // 重置缓存状态
_speakingCache = false; _speakingCache = false;
return result; return result;
} }
return true; return true;
} catch (e) { } catch (e) {
Logger.error('停止语音合成出错: $e'); Logger.error('停止语音合成出错: $e');
return false; return false;
} }
} }
/// 生成SSML文本 /// 生成SSML文本
String generateSsml({ String generateSsml({
required String text, required String text,
@ -399,11 +398,15 @@ class AzureTtsService extends GetxService implements TtsService {
final rateValue = (rate ?? _currentRate).clamp(-100, 100); final rateValue = (rate ?? _currentRate).clamp(-100, 100);
final pitchValue = (pitch ?? _currentPitch).clamp(-100, 100); final pitchValue = (pitch ?? _currentPitch).clamp(-100, 100);
final volumeValue = (volume ?? _currentVolume).clamp(0, 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 pitchStr = pitchValue == 0 ? '0%' : '${(pitchValue * 0.5).round()}%';
final volumeStr = "$volumeValue%"; final volumeStr = "$volumeValue%";
return ''' 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"> <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"> <voice name="$voice">
@ -414,17 +417,17 @@ class AzureTtsService extends GetxService implements TtsService {
</speak> </speak>
'''; ''';
} }
/// 切换启用状态 /// 切换启用状态
void toggleEnabled() { void toggleEnabled() {
isEnabled.toggle(); isEnabled.toggle();
if (!isEnabled.value) stop(); if (!isEnabled.value) stop();
} }
@override @override
Future<void> dispose() async { Future<void> dispose() async {
if (!_isInitialized) return; if (!_isInitialized) return;
try { try {
await stop(); await stop();
await _channel.invokeMethod('dispose'); await _channel.invokeMethod('dispose');
@ -448,7 +451,6 @@ class AzureTtsService extends GetxService implements TtsService {
return _speakingCache; return _speakingCache;
} }
} }
} }
/// 语音合成项目 /// 语音合成项目
@ -457,11 +459,11 @@ class _SpeechItem {
final int rate; final int rate;
final int pitch; final int pitch;
final int volume; final int volume;
_SpeechItem({ _SpeechItem({
required this.text, required this.text,
this.rate = 0, this.rate = 0,
this.pitch = 0, this.pitch = 0,
this.volume = 100, this.volume = 100,
}); });
} }

184
lib/data/services/voice_interaction_service.dart

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

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:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:logger/web.dart'; import 'package:logger/web.dart';
@ -692,7 +693,8 @@ class LoginController extends GetxController {
//final prefs = await GetStorage(); //final prefs = await GetStorage();
await _storage.write( await _storage.write(
'user', jsonEncode(user.toJson())); // 使用 user.toJson() 'user', jsonEncode(user.toJson())); // 使用 user.toJson()
//获取app的配置信息
await getAppConfig();
Get.offAllNamed(Routes.pairing); Get.offAllNamed(Routes.pairing);
// } else { // } else {
// throw FormatException('API 响应缺少 user 字段'); // throw FormatException('API 响应缺少 user 字段');
@ -722,4 +724,24 @@ class LoginController extends GetxController {
colorText: Colors.blue, 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 'dart:async';
import 'package:amap_flutter/amap_flutter.dart'; import 'package:amap_flutter/amap_flutter.dart';
import 'package:deep_voice/data/models/appconfig_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
@ -88,6 +89,7 @@ class SplashController extends GetxController {
isLoggedIn = await getLoginToken(); isLoggedIn = await getLoginToken();
} }
if (isLoggedIn) { if (isLoggedIn) {
await getAppConfig();
Get.offAllNamed(Routes.pairing); Get.offAllNamed(Routes.pairing);
} else { } else {
Get.offAllNamed(Routes.login); // 跳转到登录页 Get.offAllNamed(Routes.login); // 跳转到登录页
@ -118,4 +120,25 @@ class SplashController extends GetxController {
return false; 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 插件 // Kotlin Android 插件
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
id("com.github.johnrengelman.shadow") version "8.1.1" id("com.github.johnrengelman.shadow") version "8.1.1"
kotlin("plugin.serialization") version "1.9.24"
} }
android { android {
@ -58,4 +59,5 @@ dependencies {
implementation("io.ktor:ktor-client-okhttp:3.1.2") implementation("io.ktor:ktor-client-okhttp:3.1.2")
implementation("io.ktor:ktor-client-logging:3.1.2") implementation("io.ktor:ktor-client-logging:3.1.2")
implementation("com.google.code.gson:gson:2.10.1") implementation("com.google.code.gson:gson:2.10.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
} }

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

@ -34,7 +34,17 @@ import io.ktor.client.request.*
import io.ktor.http.* import io.ktor.http.*
import com.yunqiinnovation.open_ai_service.mcp.CustomSseClientTransport import com.yunqiinnovation.open_ai_service.mcp.CustomSseClientTransport
import io.ktor.util.valuesOf import io.ktor.util.valuesOf
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
)
/** /**
* 工具类型枚举 * 工具类型枚举
*/ */
@ -484,15 +494,17 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
if (mcpConfigJson.isNotEmpty()) { if (mcpConfigJson.isNotEmpty()) {
val mapper = ObjectMapper() val mapper = ObjectMapper()
try { 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格式 // 处理mcpServers格式
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val mcpServers = config["mcpServers"] as? Map<String, Map<String, String>> // val mcpServers = config["mcpServers"] as? Map<String, Map<String, String>>
if (mcpServers != null && mcpServers.isNotEmpty()) { if (configMap.isNotEmpty()) {
// 从服务器配置中提取URL // 从服务器配置中提取URL
mcpServers.forEach { (serverId, serverConfig) -> configMap.forEach { (serverId, serverConfig) ->
val url = serverConfig["url"] val url = serverConfig.url
if (!url.isNullOrEmpty()) { if (!url.isNullOrEmpty()) {
serverMap[serverId] = url serverMap[serverId] = url
Log.d(TAG, "从配置中添加服务器 [$serverId]: $url") Log.d(TAG, "从配置中添加服务器 [$serverId]: $url")

3
pubspec.yaml

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

Loading…
Cancel
Save