Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into liwei

newdev_shunjiawei
liwei1dao 1 year ago
parent
commit
4c629b2b1a
  1. 1
      assets/lottie/intro_foriday.json
  2. 2
      lib/core/translations/language/en_us.dart
  3. 2
      lib/core/translations/language/zh_cn.dart
  4. 96
      lib/core/widgets/intro_overlay_builder.dart
  5. 24
      lib/data/models/channel_app_model.dart
  6. 4
      lib/data/models/message_model.dart
  7. 53
      lib/data/services/ble_manager.dart
  8. 28
      lib/data/services/novice_guide_service.dart
  9. 178
      lib/data/services/version_update_service.dart
  10. 24
      lib/main.dart
  11. 60
      lib/modules/agent/controllers/agent_controller.dart
  12. 30
      lib/modules/agent/views/message_bubble.dart
  13. 2
      lib/modules/agent/views/music_card.dart
  14. 0
      lib/modules/agent/widgets/circular_progress_button.dart
  15. 130
      lib/modules/home/views/home_view.dart
  16. 62
      lib/modules/login/controllers/login_controller.dart
  17. 4
      lib/modules/meeting/controllers/meeting_record_controller.dart
  18. 4
      lib/modules/meeting/views/meeting_record_view.dart
  19. 13
      lib/modules/music/views/music_playlist_view.dart
  20. 38
      lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart
  21. 132
      lib/modules/profile/views/profile_view.dart
  22. 77
      lib/modules/settings/controllers/settings_controller.dart
  23. 105
      lib/modules/settings/views/settings_view.dart
  24. 11
      lib/modules/translation/controllers/translation_controller.dart
  25. 22
      lib/modules/translation/views/translation_view.dart
  26. 168
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  27. 236
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  28. 9
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift
  29. 45
      local_plugins/agent_service/lib/agent_service.dart
  30. 2
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  31. 40
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift
  32. 17
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift
  33. 2
      local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift
  34. 11
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  35. 7
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  36. 11
      local_plugins/ble_service/lib/ble_service.dart
  37. 46
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt
  38. 88
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift
  39. 12
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift
  40. 25
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift
  41. 6
      local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift
  42. 6
      local_plugins/location_service/ios/location_service/Sources/location_service/LocationData.swift
  43. 38
      local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift
  44. 2
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt
  45. 2
      local_plugins/open_ai_service/ios/Classes/MCPClient.swift
  46. 2
      local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/MCPClient.swift
  47. 2
      pubspec.yaml

1
assets/lottie/intro_foriday.json

File diff suppressed because one or more lines are too long

2
lib/core/translations/language/en_us.dart

@ -436,6 +436,8 @@ const Map<String, String> enUS = {
'deleteAccount': 'Delete Account', 'deleteAccount': 'Delete Account',
'deleteAccountTips': 'deleteAccountTips':
'Are you sure you want to permanently delete your account? This action cannot be undone.', 'Are you sure you want to permanently delete your account? This action cannot be undone.',
'clean': 'Clear Cache', // 清除缓存
'cleanTips': 'Are you sure you want to clear personal cache?', // 确定要清除个人缓存吗?
//Profile 错误信息 //Profile 错误信息
'networkConnectionFailed': 'networkConnectionFailed':

2
lib/core/translations/language/zh_cn.dart

@ -411,6 +411,8 @@ const Map<String, String> zhCN = {
"logoutTips": "确定要退出当前账号吗?", "logoutTips": "确定要退出当前账号吗?",
'deleteAccount': '注销账号', 'deleteAccount': '注销账号',
'deleteAccountTips': '确定要永久删除您的账号吗?此操作不可撤销', 'deleteAccountTips': '确定要永久删除您的账号吗?此操作不可撤销',
'clean': '清除缓存',
'cleanTips': '确定要清除个人缓存吗?',
// Profile 错误信息 // Profile 错误信息
'networkConnectionFailed': '网络连接失败,请检查网络设置后重试', 'networkConnectionFailed': '网络连接失败,请检查网络设置后重试',

96
lib/core/widgets/intro_overlay_builder.dart

@ -1,8 +1,46 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_intro/flutter_intro.dart'; import 'package:flutter_intro/flutter_intro.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:lottie/lottie.dart';
class IntroOverlayBuilder { class IntroOverlayBuilder {
static Widget homeBuilder(
StepWidgetParams params,
bool isWakeup, {
Function? onNext,
}) {
return Column(
children: [
isWakeup
? _AnimatedLottieWidget()
: SizedBox(
width: 200.w,
height: 200.w,
child: Lottie.asset('assets/lottie/intro_foriday.json'),
),
Padding(
padding: EdgeInsets.only(bottom: 20.w),
child: Text(
isWakeup ? '我在呢~' : '对着麦克风说:“小言小言”',
style: TextStyle(
fontSize: 15.sp,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
if (onNext != null)
_button(
'下一步',
() {
params.onNext!();
onNext();
},
),
],
);
}
static Widget aiBuilder( static Widget aiBuilder(
StepWidgetParams params, StepWidgetParams params,
String text, { String text, {
@ -70,3 +108,61 @@ class IntroOverlayBuilder {
); );
} }
} }
// 新增的动画Widget
class _AnimatedLottieWidget extends StatefulWidget {
@override
_AnimatedLottieWidgetState createState() => _AnimatedLottieWidgetState();
}
class _AnimatedLottieWidgetState extends State<_AnimatedLottieWidget>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
// 创建动画控制器,持续时间1.5秒
_animationController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
// 创建缩放动画,从0.8到1.2再回到1.0
_scaleAnimation = Tween<double>(
begin: 0.8,
end: 1.2,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.elasticInOut,
));
// 启动动画并设置为循环
_animationController.repeat(reverse: true);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: SizedBox(
width: 200.w,
height: 200.w,
child: Lottie.asset('assets/lottie/intro_foriday.json'),
),
);
},
);
}
}

24
lib/data/models/channel_app_model.dart

@ -177,6 +177,30 @@ enum UserChannel {
); );
} }
// 从字符串名称创建枚举
static UserChannel fromName(String name) {
switch (name.toUpperCase()) {
case 'CHINA_HUAWEI':
return UserChannel.CHINA_HUAWEI;
case 'CHINA_XIAOMI':
return UserChannel.CHINA_XIAOMI;
case 'CHINA_OPPO':
return UserChannel.CHINA_OPPO;
case 'CHINA_VIVO':
return UserChannel.CHINA_VIVO;
case 'CHINA_HONOR':
return UserChannel.CHINA_HONOR;
case 'OFFICIAL':
return UserChannel.OFFICIAL;
case 'GLOBAL_APPLE':
return UserChannel.GLOBAL_APPLE;
case 'GLOBAL_GOOGLE':
return UserChannel.GLOBAL_GOOGLE;
default:
return UserChannel.UNKNOWN;
}
}
// JSON 序列化时返回数字值 // JSON 序列化时返回数字值
int toJson() => value; int toJson() => value;

4
lib/data/models/message_model.dart

@ -1,3 +1,7 @@
import 'dart:io';
import 'package:path_provider/path_provider.dart';
class Message { class Message {
final bool isUser; //标识信息是否来自用户还是AI final bool isUser; //标识信息是否来自用户还是AI
final String text; //信息文本内容 final String text; //信息文本内容

53
lib/data/services/ble_manager.dart

@ -9,6 +9,7 @@ import 'package:get_storage/get_storage.dart';
import 'dart:convert'; import 'dart:convert';
import 'package:agent_service/agent_service.dart'; import 'package:agent_service/agent_service.dart';
import 'bluetooth_manager.dart'; import 'bluetooth_manager.dart';
import 'language_manager.dart';
// 重新导出BLE连接状态常量,让外部可以通过BleManager访问 // 重新导出BLE连接状态常量,让外部可以通过BleManager访问
export 'package:ble_service/ble_service.dart' show BleConnectionState; export 'package:ble_service/ble_service.dart' show BleConnectionState;
@ -556,26 +557,44 @@ class BleManager extends GetxService {
// 根据APP语言动态配置语音识别语言 // 根据APP语言动态配置语音识别语言
List<String> supportedLanguages = []; List<String> supportedLanguages = [];
try { try {
// 使用 GetX 获取 APP 当前语言环境 (Locale) final storage = GetStorage();
final appLocale = Get.locale; final languageManager = Get.find<LanguageManager>();
if (appLocale != null) { // 从存储中读取用户设置的语言
final lang = appLocale.languageCode; // e.g., 'en' String? savedLanguage = storage.read('language');
final country = appLocale.countryCode; // e.g., 'US'
if (savedLanguage != null && savedLanguage != 'system') {
// 构造 Azure SDK 需要的格式 (e.g., 'en-US') // 用户设置了特定语言
if (country != null && country.isNotEmpty) { final parts = savedLanguage.split('_');
supportedLanguages.add('$lang-$country'); if (parts.length == 2) {
} else { final asrCode = languageManager.getAsrCodeByShortCode(parts[0]);
supportedLanguages.add(lang); // fallback only language code if (asrCode != null) {
supportedLanguages.add(asrCode);
} else {
// 直接使用语言-国家格式
supportedLanguages.add('${parts[0]}-${parts[1]}');
}
}
} else {
// 跟随系统语言或未设置
final appLocale = Get.locale;
if (appLocale != null) {
final lang = appLocale.languageCode;
final country = appLocale.countryCode;
// 尝试从语言管理器获取对应的ASR代码
final asrCode = languageManager.getAsrCodeByShortCode(lang);
if (asrCode != null) {
supportedLanguages.add(asrCode);
} else if (country != null && country.isNotEmpty) {
// 构造 Azure SDK 需要的格式 (e.g., 'en-US')
supportedLanguages.add('$lang-$country');
} else {
supportedLanguages.add('en-US'); // 默认英语
}
} }
} }
// 添加 en-US 作为备选
// if (!supportedLanguages.contains('en-US')) {
// supportedLanguages.add('en-US');
// }
// 最终兜底 // 最终兜底
if (supportedLanguages.isEmpty) { if (supportedLanguages.isEmpty) {
supportedLanguages.add('en-US'); supportedLanguages.add('en-US');

28
lib/data/services/novice_guide_service.dart

@ -1,3 +1,6 @@
import 'dart:async';
import 'package:ble_service/ble_service.dart';
import 'package:flutter_intro/flutter_intro.dart'; import 'package:flutter_intro/flutter_intro.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';
@ -5,9 +8,14 @@ import 'package:get_storage/get_storage.dart';
class NoviceGuideService extends GetxService { class NoviceGuideService extends GetxService {
final GetStorage _storage = GetStorage(); final GetStorage _storage = GetStorage();
final BleService _bleService = BleService();
StreamSubscription<dynamic>? _statusSubscription;
bool isHomeGuide = true; bool isHomeGuide = true;
bool isAIGuide = true; bool isAIGuide = true;
RxBool isWakeup = false.obs;
int aiStep = 0; int aiStep = 0;
List aiStepsList = [ List aiStepsList = [
@ -56,6 +64,12 @@ class NoviceGuideService extends GetxService {
// isAIGuide = _storage.read('ai_guide') ?? false; // isAIGuide = _storage.read('ai_guide') ?? false;
} }
@override
void onClose() {
closeWakeupListen();
super.onClose();
}
void startHomeGuide() async { void startHomeGuide() async {
if (!isHomeGuide) { if (!isHomeGuide) {
await Future.delayed(const Duration(milliseconds: 200)); await Future.delayed(const Duration(milliseconds: 200));
@ -73,4 +87,18 @@ class NoviceGuideService extends GetxService {
Intro.of(Get.context!).start(group: 'ai'); Intro.of(Get.context!).start(group: 'ai');
} }
} }
void startWakeupListen() {
_bleService.setWakeup(false);
_statusSubscription = _bleService.onStatusUpdated.listen((event) {
if (event is Map && event['type'] == 'wakeup') {
isWakeup.value = true;
}
});
}
void closeWakeupListen() {
_bleService.setWakeup(true);
_statusSubscription?.cancel();
}
} }

178
lib/data/services/version_update_service.dart

@ -5,6 +5,7 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../models/channel_app_model.dart'; import '../models/channel_app_model.dart';
import 'network/api.dart'; import 'network/api.dart';
import 'dart:math' as math; import 'dart:math' as math;
@ -13,15 +14,62 @@ import 'dart:math' as math;
class VersionUpdateService extends GetxService { class VersionUpdateService extends GetxService {
static VersionUpdateService get to => Get.find(); static VersionUpdateService get to => Get.find();
final Logger _logger = Logger(); // 静态应用信息(内存缓存)
static String? _appVersion = ""; //应用版本号
static String? _buildNumber = ""; //构建号
static String? _platform = ""; //平台类型
static String? _deviceBrand = ""; //设备品牌
static UserChannel? _channel; //渠道
static String? _latestVersion = ""; //最新版本号+构建号
static bool? _hasUpdate = false; //是否有更新
static bool? _isForceUpdate = false; //是否强制更新
static String? _updateUrl = ""; //更新链接
static bool? _isShowTourists = false; //是否显示游客登录
static DateTime? _lastCheckTime; //上次检查时间
static const int _cacheIntervalMinutes = 10; // 可配置化
// 添加 getter 方法来访问缓存的静态数据
/// 获取缓存的应用版本
String get cachedAppVersion => _appVersion ?? '';
/// 获取缓存的构建号
String get cachedBuildNumber => _buildNumber ?? '';
/// 获取缓存的平台信息
String get cachedPlatform => _platform ?? '';
/// 获取缓存的设备品牌
String get cachedDeviceBrand => _deviceBrand ?? '';
/// 获取缓存的渠道信息
UserChannel get cachedChannel => _channel ?? UserChannel.UNKNOWN;
/// 获取缓存的最新版本
String get cachedLatestVersion => _latestVersion ?? '';
/// 获取缓存的是否有更新
bool get cachedHasUpdate => _hasUpdate ?? false;
/// 获取缓存的是否强制更新
bool get cachedIsForceUpdate => _isForceUpdate ?? false;
/// 获取缓存的更新地址
String get cachedUpdateUrl => _updateUrl ?? '';
/// 获取缓存的游客登录状态
bool get cachedShowTourists => _isShowTourists ?? false;
// 游客登录显示状态 /// 获取上次检查时间
final isShowTourists = false.obs; DateTime? get lastCheckTime => _lastCheckTime;
final Logger _logger = Logger();
/// 获取当前应用版本信息 /// 获取当前应用版本信息
Future<String> getCurrentVersion() async { Future<String> getCurrentVersion() async {
try { try {
final packageInfo = await PackageInfo.fromPlatform(); final packageInfo = await PackageInfo.fromPlatform();
_appVersion = packageInfo.version;
_buildNumber = packageInfo.buildNumber;
return '${packageInfo.version}+${packageInfo.buildNumber}'; return '${packageInfo.version}+${packageInfo.buildNumber}';
} catch (e) { } catch (e) {
_logger.e('获取当前版本信息失败: $e'); _logger.e('获取当前版本信息失败: $e');
@ -31,14 +79,6 @@ class VersionUpdateService extends GetxService {
/// 获取当前平台信息 /// 获取当前平台信息
Future<PlatformInfo> getCurrentPlatformInfo() async { Future<PlatformInfo> getCurrentPlatformInfo() async {
if (kIsWeb) {
return const PlatformInfo(
platform: 'web',
storeType: AppStoreType.official,
channel: UserChannel.OFFICIAL,
);
}
if (Platform.isIOS) { if (Platform.isIOS) {
return const PlatformInfo( return const PlatformInfo(
platform: 'ios', platform: 'ios',
@ -66,10 +106,14 @@ class VersionUpdateService extends GetxService {
final androidInfo = await deviceInfo.androidInfo; final androidInfo = await deviceInfo.androidInfo;
final brand = androidInfo.brand.toLowerCase(); final brand = androidInfo.brand.toLowerCase();
return const PlatformInfo( _deviceBrand = brand;
String channelFromEnv = dotenv.env['Channel'] ?? 'OFFICIAL';
_channel = UserChannel.fromName(channelFromEnv);
return PlatformInfo(
platform: 'android', platform: 'android',
storeType: AppStoreType.official, storeType: AppStoreType.official,
channel: UserChannel.OFFICIAL, channel: _channel ?? UserChannel.OFFICIAL,
); );
// switch (brand) { // switch (brand) {
@ -121,10 +165,27 @@ class VersionUpdateService extends GetxService {
/// 检查版本更新 /// 检查版本更新
Future<VersionUpdateInfo> checkForUpdate() async { Future<VersionUpdateInfo> checkForUpdate() async {
if (_lastCheckTime != null &&
DateTime.now().difference(_lastCheckTime!).inMinutes <
_cacheIntervalMinutes) {
return VersionUpdateInfo(
hasUpdate: _hasUpdate ?? false,
currentVersion: _appVersion ?? '1.0.0',
latestVersion: (_latestVersion ?? '1.0.0').split('+')[0],
updateType: _hasUpdate!
? (_isForceUpdate! ? UpdateType.force : UpdateType.optional)
: UpdateType.none,
);
}
try { try {
final currentVersion = await getCurrentVersion(); final currentVersion = await getCurrentVersion();
final platformInfo = await getCurrentPlatformInfo(); final platformInfo = await getCurrentPlatformInfo();
_platform = platformInfo.platform;
_channel = platformInfo.channel;
_lastCheckTime = DateTime.now();
// 调用后端接口获取最新版本信息 // 调用后端接口获取最新版本信息
final response = final response =
await Api.getChannelApp({'channel': platformInfo.channel.value}); await Api.getChannelApp({'channel': platformInfo.channel.value});
@ -144,9 +205,11 @@ class VersionUpdateService extends GetxService {
// 直接从response中获取app信息 // 直接从response中获取app信息
final appInfo = AppInfo.fromJson(response['app']); final appInfo = AppInfo.fromJson(response['app']);
_logger.d('获取到的最新版本信息: ${appInfo.toJson()}'); _logger.d('获取到的最新版本信息: ${appInfo.toJson()}');
_latestVersion = appInfo.version;
_updateUrl = appInfo.address;
// 设置游客登录显示状态 // 设置游客登录显示状态
isShowTourists.value = appInfo.tourists; _isShowTourists = appInfo.tourists;
_logger.d('从服务器获取游客登录配置: ${appInfo.tourists}'); _logger.d('从服务器获取游客登录配置: ${appInfo.tourists}');
// if (channelAppResponse.code != 0 || channelAppResponse.data == null) { // if (channelAppResponse.code != 0 || channelAppResponse.data == null) {
@ -160,18 +223,17 @@ class VersionUpdateService extends GetxService {
// } // }
// final appInfo = channelAppResponse.data!.app; // final appInfo = channelAppResponse.data!.app;
final hasUpdate = _compareVersions(currentVersion, appInfo.version); _hasUpdate = _compareVersions(currentVersion, appInfo.version);
// 判断更新类型:如果主版本号不同则强制更新 // 判断更新类型:如果主版本号不同则强制更新
UpdateType updateType = UpdateType.none; UpdateType updateType = UpdateType.none;
if (hasUpdate) { if (_hasUpdate!) {
final isForceUpdate = _isForceUpdate = _shouldForceUpdate(currentVersion, appInfo.version);
_shouldForceUpdate(currentVersion, appInfo.version); updateType = _isForceUpdate! ? UpdateType.force : UpdateType.optional;
updateType = isForceUpdate ? UpdateType.force : UpdateType.optional;
} }
return VersionUpdateInfo( return VersionUpdateInfo(
hasUpdate: hasUpdate, hasUpdate: _hasUpdate!,
currentVersion: currentVersion.split('+')[0], currentVersion: currentVersion.split('+')[0],
latestVersion: appInfo.version.split('+')[0], latestVersion: appInfo.version.split('+')[0],
appInfo: appInfo, appInfo: appInfo,
@ -180,7 +242,7 @@ class VersionUpdateService extends GetxService {
} catch (e) { } catch (e) {
_logger.e('检查版本更新失败: $e'); _logger.e('检查版本更新失败: $e');
// 发生错误时,保持默认值 false // 发生错误时,保持默认值 false
isShowTourists.value = false; _isShowTourists = false;
final currentVersion = await getCurrentVersion(); final currentVersion = await getCurrentVersion();
return VersionUpdateInfo( return VersionUpdateInfo(
hasUpdate: false, hasUpdate: false,
@ -270,84 +332,16 @@ class VersionUpdateService extends GetxService {
return false; return false;
} }
//final platformInfo = await getCurrentPlatformInfo();
try { try {
// switch (platformInfo.storeType) { return await _openDownloadUrl(updateInfo.appInfo!.address!);
// case AppStoreType.appStore:
// return await _openAppStore();
// case AppStoreType.googlePlay:
// return await _openGooglePlay();
// case AppStoreType.huawei:
// return await _openHuaweiStore();
// case AppStoreType.xiaomi:
// return await _openXiaomiStore();
// case AppStoreType.oppo:
// return await _openOppoStore();
// case AppStoreType.vivo:
// return await _openVivoStore();
// case AppStoreType.honor:
// return await _openHonorStore();
// case AppStoreType.official:
// return await _openOfficialDownload(updateInfo.appInfo!.address!);
// default:
return await _openOfficialDownload(updateInfo.appInfo!.address!);
// }
} catch (e) { } catch (e) {
_logger.e('执行更新失败: $e'); _logger.e('执行更新失败: $e');
return false; return false;
} }
} }
/// 打开iOS App Store /// 打开下载链接
Future<bool> _openAppStore() async { Future<bool> _openDownloadUrl(String downloadUrl) async {
// 这里需要替换为实际的App Store ID
const appStoreUrl = 'https://apps.apple.com/app/id你的应用ID';
return await _launchUrl(appStoreUrl);
}
/// 打开Google Play
Future<bool> _openGooglePlay() async {
// 这里需要替换为实际的包名
const packageName = 'com.yourcompany.deepvoice';
const playStoreUrl =
'https://play.google.com/store/apps/details?id=$packageName';
return await _launchUrl(playStoreUrl);
}
/// 打开华为应用市场
Future<bool> _openHuaweiStore() async {
const huaweiStoreUrl = 'appmarket://details?id=com.yourcompany.deepvoice';
return await _launchUrl(huaweiStoreUrl);
}
/// 打开小米应用商店
Future<bool> _openXiaomiStore() async {
const xiaomiStoreUrl = 'mimarket://details?id=com.yourcompany.deepvoice';
return await _launchUrl(xiaomiStoreUrl);
}
/// 打开OPPO软件商店
Future<bool> _openOppoStore() async {
const oppoStoreUrl =
'oppomarket://details?packagename=com.yourcompany.deepvoice';
return await _launchUrl(oppoStoreUrl);
}
/// 打开vivo应用商店
Future<bool> _openVivoStore() async {
const vivoStoreUrl = 'vivomarket://details?id=com.yourcompany.deepvoice';
return await _launchUrl(vivoStoreUrl);
}
/// 打开荣耀应用市场
Future<bool> _openHonorStore() async {
const honorStoreUrl = 'honormarket://details?id=com.yourcompany.deepvoice';
return await _launchUrl(honorStoreUrl);
}
/// 打开官网下载
Future<bool> _openOfficialDownload(String downloadUrl) async {
return await _launchUrl(downloadUrl); return await _launchUrl(downloadUrl);
} }

24
lib/main.dart

@ -10,8 +10,6 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'core/bindings/initial_binding.dart'; import 'core/bindings/initial_binding.dart';
import 'routes/app_pages.dart'; import 'routes/app_pages.dart';
import 'routes/app_routes.dart'; import 'routes/app_routes.dart';
import 'dart:io';
import 'package:window_size/window_size.dart';
import 'core/translations/app_translations.dart'; import 'core/translations/app_translations.dart';
import 'core/themes/app_theme.dart'; import 'core/themes/app_theme.dart';
import 'core/themes/theme_service.dart'; import 'core/themes/theme_service.dart';
@ -32,28 +30,6 @@ void main() async {
// 初始化登录服务(包含Firebase初始化) // 初始化登录服务(包含Firebase初始化)
LoginService(); LoginService();
// 设置窗口大小(仅在 macOS 下生效)
if (Platform.isMacOS) {
const double width = 375.0;
const double height = 812.0;
setWindowTitle('Voitrans');
setWindowMinSize(const Size(width, height));
setWindowMaxSize(const Size(width, height));
// 居中显示窗口
final screen = await getCurrentScreen();
if (screen != null) {
final screenFrame = screen.frame;
final windowFrame = Rect.fromCenter(
center: screenFrame.center,
width: width,
height: height,
);
setWindowFrame(windowFrame);
}
}
// 运行应用 // 运行应用
runApp(const MainApp()); runApp(const MainApp());
} }

60
lib/modules/agent/controllers/agent_controller.dart

@ -3,7 +3,6 @@ import 'dart:convert';
import 'dart:ffi'; import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'package:amap_service/amap_service.dart'; import 'package:amap_service/amap_service.dart';
import 'package:voitrans/data/services/qq_musoc.dart';
import '../../../data/services/novice_guide_service.dart'; import '../../../data/services/novice_guide_service.dart';
import '/core/utils/upload_oss.dart'; import '/core/utils/upload_oss.dart';
import '/data/models/user_Info.dart'; import '/data/models/user_Info.dart';
@ -42,7 +41,10 @@ class AgentController extends GetxController {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
final noviceGuideService = Get.find<NoviceGuideService>(); final noviceGuideService = Get.find<NoviceGuideService>();
// 确保服务已初始化
// ========== 依赖服务 ==========
final BluetoothManager _bluetoothManager = Get.find<BluetoothManager>();
final BleManager _bleManager = Get.find<BleManager>();
//音乐播放管理 //音乐播放管理
final _musiceManager = Get.find<MusicManager>(); final _musiceManager = Get.find<MusicManager>();
final _qqmusicManager = Get.find<QqMusocService>(); final _qqmusicManager = Get.find<QqMusocService>();
@ -122,7 +124,7 @@ class AgentController extends GetxController {
String _currentResponseId = ''; String _currentResponseId = '';
final TAG = 'AgentController'; final TAG = 'AgentController';
var AppDir = "".obs;
// 会话ID // 会话ID
final String sessionId = 'default_agent'; final String sessionId = 'default_agent';
@ -149,6 +151,7 @@ class AgentController extends GetxController {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
_initAppDir();
_loadTtsPreference(); //加载TTS偏好设置 _loadTtsPreference(); //加载TTS偏好设置
_setupScrollListener(); //设置滚动监听器 _setupScrollListener(); //设置滚动监听器
_loadChatHistory(); //加载聊天历史记录 _loadChatHistory(); //加载聊天历史记录
@ -162,6 +165,10 @@ class AgentController extends GetxController {
// _qqmusicManager.syncCurrentPlayInfo(); // _qqmusicManager.syncCurrentPlayInfo();
} }
Future<void> _initAppDir() async {
AppDir.value = (await getApplicationDocumentsDirectory()).path;
}
// 初始化音乐播放状态 // 初始化音乐播放状态
Future<void> _initMusicStatus() async { Future<void> _initMusicStatus() async {
try { try {
@ -289,7 +296,7 @@ class AgentController extends GetxController {
page: 1, //从第一页开始 page: 1, //从第一页开始
pageSize: pageSize, //每页20条记录 pageSize: pageSize, //每页20条记录
); );
Logger.i(TAG, '加载聊天历史记录: $result');
if (result.messages.isNotEmpty) { if (result.messages.isNotEmpty) {
// 转换并添加消息到列表 // 转换并添加消息到列表
final historyMessages = _convertChatMessagesToMessages(result.messages); final historyMessages = _convertChatMessagesToMessages(result.messages);
@ -301,7 +308,8 @@ class AgentController extends GetxController {
// 更新分页状态 // 更新分页状态
currentPage.value = 1; currentPage.value = 1;
hasMoreMessages.value = result.totalPages > 1; hasMoreMessages.value = result.totalPages > 1;
print(
'liwei-------------读取历史记录 ${AppDir.value}${historyMessages[0].imagePath}');
Logger.i(TAG, Logger.i(TAG,
'成功加载 ${historyMessages.length} 条聊天记录,总页数: ${result.totalPages}'); '成功加载 ${historyMessages.length} 条聊天记录,总页数: ${result.totalPages}');
_logMessageDetails(historyMessages); _logMessageDetails(historyMessages);
@ -419,7 +427,7 @@ class AgentController extends GetxController {
'meta: ${msg.meta != null ? "有" : "无"}' 'meta: ${msg.meta != null ? "有" : "无"}'
'}'); '}');
} }
Logger.d(TAG, '===== 历史消息详情结束 ====='); Logger.d(TAG, '===== 历史消息详情结束 ==lishi===');
} }
// 设置滚动监听器 // 设置滚动监听器
@ -561,16 +569,16 @@ class AgentController extends GetxController {
break; break;
case AgentServiceEventType.ttsStarted: case AgentServiceEventType.ttsStarted:
// isSpeaking.value = true; isSpeaking.value = true;
// Logger.i(TAG, 'TTS开始播放'); print('TTS开始播放');
break; break;
case AgentServiceEventType.ttsCompleted: case AgentServiceEventType.ttsCompleted:
case AgentServiceEventType.ttsStopped: case AgentServiceEventType.ttsStopped:
case AgentServiceEventType.ttsCanceled: case AgentServiceEventType.ttsCanceled:
// isSpeaking.value = false; isSpeaking.value = false;
// Logger.i(TAG, 'TTS停止播放, $event.type'); Logger.i(TAG, 'TTS停止播放, $event.type');
break; break;
case AgentServiceEventType.playbackStarted: case AgentServiceEventType.playbackStarted:
@ -909,10 +917,14 @@ class AgentController extends GetxController {
} }
// 复制图片到应用永久存储目录 // 复制图片到应用永久存储目录
final permanentImagePath = await _copyImageToPermanentStorage(imagePath); // 复制图片到应用永久存储目录
final imagePathResult = await _copyImageToPermanentStorage(imagePath);
final permanentImagePath = imagePathResult['fullPath']!;
final relativeImagePath = imagePathResult['relativePath']!;
var ImageUrl = await UploadOss.upload( var ImageUrl = await UploadOss.upload(
filepath: imagePath, rootDir: 'User/${User.instance.uid}/ChatImages'); filepath: permanentImagePath,
Logger.i(TAG, '图片上传: $imagePath URL: $ImageUrl'); rootDir: 'User/${User.instance.uid}/ChatImages');
Logger.i(TAG, '图片上传: $permanentImagePath URL: $ImageUrl');
// 显示的文本,如果没有提供则使用默认值 // 显示的文本,如果没有提供则使用默认值
final displayText = text?.isNotEmpty == true ? text! : '[图片]'; final displayText = text?.isNotEmpty == true ? text! : '[图片]';
@ -921,7 +933,7 @@ class AgentController extends GetxController {
isUser: true, isUser: true,
text: displayText, text: displayText,
hasImage: true, hasImage: true,
imagePath: permanentImagePath, imagePath: relativeImagePath,
); );
messages.add(message); messages.add(message);
@ -941,7 +953,7 @@ class AgentController extends GetxController {
// 调用Agent Service处理图片,考虑TTS状态 // 调用Agent Service处理图片,考虑TTS状态
await AgentService.processImageInput( await AgentService.processImageInput(
permanentImagePath, relativeImagePath,
ImageUrl, ImageUrl,
text: text ?? '', text: text ?? '',
speakResponse: isTtsEnabled.value, speakResponse: isTtsEnabled.value,
@ -954,12 +966,16 @@ class AgentController extends GetxController {
} }
// 将图片复制到应用的永久存储目录 // 将图片复制到应用的永久存储目录
Future<String> _copyImageToPermanentStorage(String sourcePath) async { Future<Map<String, String>> _copyImageToPermanentStorage(
String sourcePath) async {
try { try {
final File sourceFile = File(sourcePath); final File sourceFile = File(sourcePath);
if (!await sourceFile.exists()) { if (!await sourceFile.exists()) {
Logger.e(TAG, '源图片文件不存在: $sourcePath'); Logger.e(TAG, '源图片文件不存在: $sourcePath');
return sourcePath; // 如果源文件不存在,返回原路径 return {
'fullPath': sourcePath,
'relativePath': sourcePath
}; // 如果源文件不存在,返回原路径
} }
// 创建应用文档目录下的chatImages文件夹 // 创建应用文档目录下的chatImages文件夹
@ -973,15 +989,16 @@ class AgentController extends GetxController {
final fileName = final fileName =
'${DateTime.now().millisecondsSinceEpoch}_${sourcePath.split('/').last}'; '${DateTime.now().millisecondsSinceEpoch}_${sourcePath.split('/').last}';
final targetPath = '${imagesDir.path}/$fileName'; final targetPath = '${imagesDir.path}/$fileName';
final relativePath = '/chatImages/$fileName';
// 复制文件 // 复制文件
await sourceFile.copy(targetPath); await sourceFile.copy(targetPath);
Logger.i(TAG, '图片已复制到永久存储: $targetPath'); Logger.i(TAG, '图片已复制到永久存储: $targetPath');
return targetPath; return {'fullPath': targetPath, 'relativePath': relativePath};
} catch (e) { } catch (e) {
Logger.e(TAG, '复制图片到永久存储失败: $e'); Logger.e(TAG, '复制图片到永久存储失败: $e');
return sourcePath; // 出错时返回原路径 return {'fullPath': sourcePath, 'relativePath': sourcePath}; // 出错时返回原路径
} }
} }
@ -1046,6 +1063,11 @@ class AgentController extends GetxController {
try { try {
await AgentService.recognizeCallback(); await AgentService.recognizeCallback();
if (_bluetoothManager.currentDeviceRx.value != null) {
await AgentService.restoreOriginalAudioState();
} else {
await AgentService.disableBluetoothAudio();
}
} catch (e) { } catch (e) {
Logger.e(TAG, '启动识别器失败: $e'); Logger.e(TAG, '启动识别器失败: $e');
} }

30
lib/modules/agent/views/message_bubble.dart

@ -49,28 +49,20 @@ class MessageBubble extends GetView<AgentController> {
isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [ children: [
Container( Container(
padding: hasImage padding: (hasImage && message == '[图片]')
? (message == '[图片]' ? EdgeInsets.zero
? EdgeInsets.zero : const EdgeInsets.all(12),
: isUser
? const EdgeInsets.fromLTRB(
12, 12, 16, 12) // 用户消息右侧多一点内边距
: const EdgeInsets.fromLTRB(
16, 12, 12, 12)) // AI消息左侧多一点内边距
: isUser
? const EdgeInsets.fromLTRB(
12, 12, 16, 12) // 用户消息右侧多一点内边距
: const EdgeInsets.fromLTRB(
16, 12, 12, 12), // AI消息左侧多一点内边距
decoration: BoxDecoration( decoration: BoxDecoration(
color: _getBubbleColor(context), color: _getBubbleColor(context),
borderRadius: BorderRadius.circular(18).copyWith( borderRadius: BorderRadius.only(
topLeft: isUser topLeft: isUser
? const Radius.circular(18) ? const Radius.circular(18)
: const Radius.circular(4), : const Radius.circular(4),
topRight: !isUser topRight: isUser
? const Radius.circular(18) ? const Radius.circular(4)
: const Radius.circular(4), : const Radius.circular(18),
bottomLeft: const Radius.circular(18),
bottomRight: const Radius.circular(18),
), ),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
@ -180,7 +172,7 @@ class MessageBubble extends GetView<AgentController> {
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: Image.file( child: Image.file(
File(imagePath!), File('${controller.AppDir.value}$imagePath'),
width: 200, width: 200,
height: 200, height: 200,
fit: BoxFit.cover, fit: BoxFit.cover,
@ -213,7 +205,7 @@ class MessageBubble extends GetView<AgentController> {
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: Image.file( child: Image.file(
File(imagePath!), File('${controller.AppDir.value}$imagePath'),
width: 200, width: 200,
height: 200, height: 200,
fit: BoxFit.cover, fit: BoxFit.cover,

2
lib/modules/agent/views/music_card.dart

@ -3,7 +3,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
import '../controllers/agent_controller.dart'; import '../controllers/agent_controller.dart';
import 'circular_progress_button.dart'; import '../widgets/circular_progress_button.dart';
/// 构建音乐项目 /// 构建音乐项目
Widget buildMusicItem(BuildContext context, int id, String name, String url, Widget buildMusicItem(BuildContext context, int id, String name, String url,

0
lib/modules/agent/views/circular_progress_button.dart → lib/modules/agent/widgets/circular_progress_button.dart

130
lib/modules/home/views/home_view.dart

@ -100,7 +100,9 @@ class HomeView extends GetView<HomeController> {
return IntroOverlayBuilder.aiBuilder( return IntroOverlayBuilder.aiBuilder(
params, params,
'欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言', '欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言',
onNext: () {}, onNext: () {
controller.noviceGuideService.startWakeupListen();
},
); );
}, },
builder: (BuildContext context, GlobalKey key) => SizedBox( builder: (BuildContext context, GlobalKey key) => SizedBox(
@ -114,11 +116,26 @@ class HomeView extends GetView<HomeController> {
IntroStepBuilder( IntroStepBuilder(
group: 'home', group: 'home',
order: 2, order: 2,
getOverlayPosition: ({
required Offset offset,
required Size screenSize,
required Size size,
}) {
return OverlayPosition(
top: 200.h,
width: screenSize.width,
crossAxisAlignment: CrossAxisAlignment.end,
);
},
overlayBuilder: (params) { overlayBuilder: (params) {
return IntroOverlayBuilder.aiBuilder( return Obx(
params, () => IntroOverlayBuilder.homeBuilder(
'对着麦克风说“小言小言”', params,
onNext: () {}, controller.noviceGuideService.isWakeup.value,
onNext: () {
controller.noviceGuideService.closeWakeupListen();
},
),
); );
}, },
builder: (BuildContext context, GlobalKey key) => SizedBox( builder: (BuildContext context, GlobalKey key) => SizedBox(
@ -254,7 +271,7 @@ class HomeView extends GetView<HomeController> {
), ),
], ],
), ),
padding: EdgeInsets.all(16.w), padding: EdgeInsets.fromLTRB(16.w, 16.h, 16.w, 16.h),
child: Column( child: Column(
children: [ children: [
// 状态标题行 // 状态标题行
@ -277,7 +294,7 @@ class HomeView extends GetView<HomeController> {
if (!controller.isFullyConnectedRx.value) { if (!controller.isFullyConnectedRx.value) {
return _buildConnectButton(); return _buildConnectButton();
} else { } else {
return const SizedBox(); return _buildDeviceButton(isDarkMode);
} }
}), }),
], ],
@ -400,12 +417,42 @@ class HomeView extends GetView<HomeController> {
), ),
SizedBox(height: 12.h), SizedBox(height: 12.h),
_buildBatteryInfoGrid(isDarkMode), _buildBatteryInfoGrid(isDarkMode),
// SizedBox(height: 16.h),
// _buildDeviceDetailsCard(isDarkMode),
], ],
); );
} }
// 设备管理按钮
Widget _buildDeviceButton(bool isDarkMode) {
return ElevatedButton(
onPressed: () {
Get.toNamed(Routes.devices); // 跳转到设备管理页面
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
minimumSize: Size(double.infinity, 44.h),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.r),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.device_hub, size: 16.sp), // 设备图标
SizedBox(width: 8.w),
Text(
'deviceManagement'.tr,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
// 电池信息网格布局 // 电池信息网格布局
Widget _buildBatteryInfoGrid(bool isDarkMode) { Widget _buildBatteryInfoGrid(bool isDarkMode) {
return Obx(() { return Obx(() {
@ -505,6 +552,8 @@ class HomeView extends GetView<HomeController> {
batteryColor = Colors.red[600]!; batteryColor = Colors.red[600]!;
} }
bool isBatteryZero = batteryLevel == 0;
return Expanded( return Expanded(
child: Padding( child: Padding(
padding: EdgeInsets.symmetric(vertical: 6.h), padding: EdgeInsets.symmetric(vertical: 6.h),
@ -523,32 +572,45 @@ class HomeView extends GetView<HomeController> {
), ),
SizedBox(height: 4.h), SizedBox(height: 4.h),
// 电池指示器 // 电池指示器 - 当电量为0时,显示横线
Row( isBatteryZero
mainAxisAlignment: MainAxisAlignment.center, ? Container(
children: [ width: 20.w, // 横线宽度
Icon( height: 12.sp, // 设置与图标相同的高度,确保对齐
batteryIcon, alignment: Alignment.center, // 居中对齐
size: 12.sp, child: Container(
color: batteryColor, width: 10.w,
), height: 1.5.h, // 实际横线高度
SizedBox(width: 2.w), color: isDarkMode
Text( ? Colors.grey[600]
'$batteryLevel%', : Colors.grey[400], // 横线颜色
style: TextStyle( ),
fontSize: 11.sp, )
fontWeight: FontWeight.w500, : Row(
color: batteryColor, mainAxisAlignment: MainAxisAlignment.center,
), children: [
), Icon(
if (isCharging) batteryIcon,
Icon( size: 12.sp,
Icons.bolt, color: batteryColor,
size: 10.sp, ),
color: Colors.green[600], SizedBox(width: 2.w),
Text(
'$batteryLevel%',
style: TextStyle(
fontSize: 11.sp,
fontWeight: FontWeight.w500,
color: batteryColor,
),
),
if (isCharging)
Icon(
Icons.bolt,
size: 10.sp,
color: Colors.green[600],
),
],
), ),
],
),
], ],
), ),
), ),

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

@ -1,23 +1,23 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import '../../../data/models/appconfig.dart';
import '../../../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:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:logger/web.dart'; import 'package:logger/web.dart';
import '../../../routes/app_routes.dart'; import 'package:get_storage/get_storage.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'dart:convert'; import 'dart:convert';
import 'facebookLogin.dart'; import 'facebookLogin.dart';
import 'googleLogin.dart'; import 'googleLogin.dart';
import 'weixinLogin.dart'; import 'weixinLogin.dart';
import 'appleLogin.dart'; import 'appleLogin.dart';
import '../../../routes/app_routes.dart';
import '../../../data/models/appconfig.dart';
import '../../../data/models/appconfig_model.dart';
import '../../../data/services/network/api.dart'; import '../../../data/services/network/api.dart';
import '../../../../data/models/user_Info.dart'; import '../../../../data/models/user_Info.dart';
import 'package:get_storage/get_storage.dart'; import '../../../data/models/channel_app_model.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'dart:io';
import 'package:package_info_plus/package_info_plus.dart';
import '../../../data/services/version_update_service.dart'; import '../../../data/services/version_update_service.dart';
import '../../../modules/common/widgets/privacy_policy_dialog.dart'; import '../../../modules/common/widgets/privacy_policy_dialog.dart';
@ -27,20 +27,20 @@ enum LoginViewState {
} }
// 用户渠道枚举 // 用户渠道枚举
enum UserChannel { // enum UserChannel {
UNKNOWN(0), // 未知渠道 // UNKNOWN(0), // 未知渠道
CHINA_HUAWEI(1), // 中国-华为 // CHINA_HUAWEI(1), // 中国-华为
CHINA_XIAOMI(2), // 中国-小米 // CHINA_XIAOMI(2), // 中国-小米
CHINA_OPPO(3), // 中国-OPPO // CHINA_OPPO(3), // 中国-OPPO
CHINA_VIVO(4), // 中国-VIVO // CHINA_VIVO(4), // 中国-VIVO
CHINA_HONOR(5), // 中国-荣耀 // CHINA_HONOR(5), // 中国-荣耀
OFFICIAL(100), // 官方渠道 // OFFICIAL(100), // 官方渠道
GLOBAL_APPLE(101), // 全球-苹果 // GLOBAL_APPLE(101), // 全球-苹果
GLOBAL_GOOGLE(102); // 全球-谷歌 // GLOBAL_GOOGLE(102); // 全球-谷歌
const UserChannel(this.value); // const UserChannel(this.value);
final int value; // final int value;
} // }
class LoginController extends GetxController { class LoginController extends GetxController {
static LoginController get to => Get.find(); static LoginController get to => Get.find();
@ -212,6 +212,7 @@ class LoginController extends GetxController {
String get contactValue => _contactValue.value; String get contactValue => _contactValue.value;
String get displayContact => _displayContact.value; String get displayContact => _displayContact.value;
List<String> get verificationCode => _verificationCode; List<String> get verificationCode => _verificationCode;
final versionService = VersionUpdateService.to;
// 文本控制器 // 文本控制器
TextEditingController contactController = TextEditingController(); TextEditingController contactController = TextEditingController();
@ -246,20 +247,9 @@ class LoginController extends GetxController {
// 检查游客登录配置 // 检查游客登录配置
Future<void> _checkGuestLoginConfig() async { Future<void> _checkGuestLoginConfig() async {
try { try {
final versionService = VersionUpdateService.to; _isGuestLoginVisible.value = versionService.cachedShowTourists;
// 先检查版本更新,这会同时设置 isShowTourists 的值
await versionService.checkForUpdate();
// 直接使用 VersionUpdateService 中的 isShowTourists 值
_isGuestLoginVisible.value = versionService.isShowTourists.value;
Logger().d('游客登录显示状态: ${_isGuestLoginVisible.value}'); Logger().d('游客登录显示状态: ${_isGuestLoginVisible.value}');
// 监听 isShowTourists 的变化
versionService.isShowTourists.listen((value) {
_isGuestLoginVisible.value = value;
Logger().d('游客登录显示状态更新: $value');
});
} catch (e) { } catch (e) {
Logger().e('检查游客登录配置失败: $e'); Logger().e('检查游客登录配置失败: $e');
// 默认不显示游客登录 // 默认不显示游客登录
@ -301,9 +291,7 @@ class LoginController extends GetxController {
// 新增:初始化APP信息(版本号、渠道、国家代码) // 新增:初始化APP信息(版本号、渠道、国家代码)
Future<void> _initAppInfo() async { Future<void> _initAppInfo() async {
try { try {
// 获取APP版本号 _appVersion = versionService.cachedAppVersion;
final packageInfo = await PackageInfo.fromPlatform();
_appVersion = '${packageInfo.version}+${packageInfo.buildNumber}';
Logger().d('appVersion: $_appVersion'); Logger().d('appVersion: $_appVersion');
// 获取国家代码 // 获取国家代码
@ -311,7 +299,7 @@ class LoginController extends GetxController {
Logger().d('countryCode: $_countryCode'); Logger().d('countryCode: $_countryCode');
// 获取用户渠道(根据平台和设备信息确定) // 获取用户渠道(根据平台和设备信息确定)
_userChannel = UserChannel.OFFICIAL; _userChannel = versionService.cachedChannel;
Logger().d('userChannel: ${_userChannel.value}'); Logger().d('userChannel: ${_userChannel.value}');
} catch (e) { } catch (e) {
Logger().e('获取APP信息失败: $e'); Logger().e('获取APP信息失败: $e');

4
lib/modules/meeting/controllers/meeting_record_controller.dart

@ -138,7 +138,7 @@ class MeetingRecordController extends GetxController
switch (audioType.value) { switch (audioType.value) {
case 0: case 0:
fileName.value = "现场录音"; fileName.value = "现场录音";
_audioSourceType = true; _audioSourceType = false;
break; break;
case 1: case 1:
fileName.value = "音、视频录音"; fileName.value = "音、视频录音";
@ -271,7 +271,7 @@ class MeetingRecordController extends GetxController
void _startHardwareServices() { void _startHardwareServices() {
switch (audioType.value) { switch (audioType.value) {
case 0: case 0:
_bleManager.openEncoder(); // _bleManager.openEncoder();
break; break;
case 1: case 1:
_bleManager.openDecoder(); _bleManager.openDecoder();

4
lib/modules/meeting/views/meeting_record_view.dart

@ -95,7 +95,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
unselectedLabelColor: Colors.grey, unselectedLabelColor: Colors.grey,
tabs: const [ tabs: const [
Tab(text: '音频'), Tab(text: '音频'),
Tab(text: '文本'), // Tab(text: '文本'),
], ],
), ),
Expanded( Expanded(
@ -103,7 +103,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
controller: controller.tabController, controller: controller.tabController,
children: [ children: [
_buildAudioTab(context, isDarkMode), _buildAudioTab(context, isDarkMode),
_buildTextTab(context, isDarkMode), // _buildTextTab(context, isDarkMode),
], ],
), ),
), ),

13
lib/modules/music/views/music_playlist_view.dart

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import '../../agent/controllers/agent_controller.dart'; import '../../agent/controllers/agent_controller.dart';
import '../../agent/views/circular_progress_button.dart'; import '../../agent/widgets/circular_progress_button.dart';
class MusicPlaylistView extends StatelessWidget { class MusicPlaylistView extends StatelessWidget {
const MusicPlaylistView({Key? key}) : super(key: key); const MusicPlaylistView({Key? key}) : super(key: key);
@ -115,7 +115,9 @@ class MusicPlaylistView extends StatelessWidget {
); );
} }
final title = currentSong['name'] ?? currentSong['title'] ?? 'unknownSong'.tr; // 未知歌曲 final title = currentSong['name'] ??
currentSong['title'] ??
'unknownSong'.tr; // 未知歌曲
final artist = currentSong['singer'] ?? final artist = currentSong['singer'] ??
currentSong['sgener'] ?? currentSong['sgener'] ??
currentSong['artist'] ?? currentSong['artist'] ??
@ -192,7 +194,8 @@ class MusicPlaylistView extends StatelessWidget {
final id = item["id"] ?? 0; final id = item["id"] ?? 0;
final name = item["name"] ?? 'unknownSong'.tr; // 未知歌曲 final name = item["name"] ?? 'unknownSong'.tr; // 未知歌曲
final url = item["url"] ?? ""; final url = item["url"] ?? "";
final singer = item["singer"] ?? item["sgener"] ?? 'unknownArtist'.tr; // 未知歌手 final singer =
item["singer"] ?? item["sgener"] ?? 'unknownArtist'.tr; // 未知歌手
final image = item["image"] ?? ""; final image = item["image"] ?? "";
return _buildMusicItem( return _buildMusicItem(
@ -628,8 +631,8 @@ class MusicPlaylistView extends StatelessWidget {
} }
// 播放选中的歌曲 // 播放选中的歌曲
Future<void> _playSelectedSong(AgentController controller, Map<String, dynamic> song, Future<void> _playSelectedSong(AgentController controller,
List<Map<String, dynamic>> playlist) async { Map<String, dynamic> song, List<Map<String, dynamic>> playlist) async {
// 防止重复点击导致的状态混乱 // 防止重复点击导致的状态混乱
if (controller.isLoadingMusic.value) { if (controller.isLoadingMusic.value) {
return; return;

38
lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart

@ -1,4 +1,5 @@
import 'package:classic_bluetooth/classic_bluetooth.dart'; import 'package:classic_bluetooth/classic_bluetooth.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:voitrans/data/models/appconfig_model.dart'; import 'package:voitrans/data/models/appconfig_model.dart';
import '/routes/app_routes.dart'; import '/routes/app_routes.dart';
import 'package:logger/web.dart'; import 'package:logger/web.dart';
@ -214,10 +215,9 @@ class OtaUpgradeController extends GetxController {
updatepackageaddress = product!.updatepackageaddress!; updatepackageaddress = product!.updatepackageaddress!;
// 版本检查 // 版本检查
bool isUpdate = _isNewVersionAvailable(product.version!); bool hasUpdate = _isNewVersionAvailable(product.version!);
isUpdateAvailable.value = hasUpdate;
isUpdateAvailable.value = isUpdate;
} catch (e) { } catch (e) {
// 统一错误处理 // 统一错误处理
errorMessage.value = e.toString(); errorMessage.value = e.toString();
@ -238,22 +238,36 @@ class OtaUpgradeController extends GetxController {
} }
} }
// 版本比较方法优化 // 版本比较方法优化 - 支持升级和降级
bool _isNewVersionAvailable(String latest) { bool _isNewVersionAvailable(String latestVersion) {
try { try {
List<int> currentParts = currentVersion.value List<int> currentParts = currentVersion.value
.split('.') .split('.')
.map((e) => int.parse(e.trim())) .map((e) => int.parse(e.trim()))
.toList(); .toList();
List<int> latestParts = List<int> latestParts =
latest.split('.').map((e) => int.parse(e.trim())).toList(); latestVersion.split('.').map((e) => int.parse(e.trim())).toList();
// 确保两个版本号长度一致,短的用0补齐
int maxLength = currentParts.length > latestParts.length
? currentParts.length
: latestParts.length;
for (int i = 0; i < latestParts.length; i++) { while (currentParts.length < maxLength) {
int current = (i < currentParts.length) ? currentParts[i] : 0; currentParts.add(0);
if (latestParts[i] > current) return true;
if (latestParts[i] < current) return false;
} }
return false; while (latestParts.length < maxLength) {
latestParts.add(0);
}
// 比较每一段,只要有不同就返回true(支持升级和降级)
for (int i = 0; i < maxLength; i++) {
if (latestParts[i] != currentParts[i]) {
return true; // 版本不同,允许更新(升级或降级)
}
}
return false; // 版本完全相同,无需更新
} catch (e) { } catch (e) {
Logger().d('版本比较错误: $e'); Logger().d('版本比较错误: $e');
return false; return false;
@ -654,7 +668,7 @@ class OtaUpgradeController extends GetxController {
color: Colors.green[600], color: Colors.green[600],
size: 28, size: 28,
), ),
SizedBox(width: 12), SizedBox(width: 12.w),
Text( Text(
'升级成功', '升级成功',
style: TextStyle( style: TextStyle(

132
lib/modules/profile/views/profile_view.dart

@ -740,72 +740,72 @@ class EditProfileView extends GetView<ProfileController> {
}, },
isDarkMode: isDarkMode, isDarkMode: isDarkMode,
), ),
// Divider( Divider(
// height: 1, height: 1,
// color: isDarkMode color: isDarkMode
// ? Colors.white.withOpacity(0.1) ? Colors.white.withOpacity(0.1)
// : Colors.grey[200]), : Colors.grey[200]),
// // 清理缓存 // 清理缓存
// _buildSimpleNavigationSetting( _buildSimpleNavigationSetting(
// title: 'clean'.tr, // 清理缓存 title: 'clean'.tr, // 清理缓存
// icon: Icons.logout, icon: Icons.cleaning_services,
// iconBgColor: isDarkMode iconBgColor: isDarkMode
// ? Colors.red[900]!.withOpacity(0.3) ? Colors.red[900]!.withOpacity(0.3)
// : Colors.red[50]!, : Colors.red[50]!,
// iconColor: isDarkMode ? Colors.red[300]! : Colors.red[600]!, iconColor: isDarkMode ? Colors.red[300]! : Colors.red[600]!,
// titleColor: titleColor:
// isDarkMode ? Colors.red[300]! : Colors.red[600]!, isDarkMode ? Colors.red[300]! : Colors.red[600]!,
// onTap: () { onTap: () {
// // 退出登录确认对话框 // 退出登录确认对话框
// Get.dialog( Get.dialog(
// AlertDialog( AlertDialog(
// backgroundColor: backgroundColor:
// isDarkMode ? Colors.grey[900] : Colors.white, isDarkMode ? Colors.grey[900] : Colors.white,
// title: Text( title: Text(
// 'clean'.tr, // 清理缓存 'clean'.tr, // 清除缓存
// style: TextStyle( style: TextStyle(
// color: isDarkMode ? Colors.white : Colors.black87, color: isDarkMode ? Colors.white : Colors.black87,
// ), ),
// ), ),
// content: Text( content: Text(
// 'cleanTips'.tr, // 确定要退出当前账号吗? 'cleanTips'.tr, // 确定要清除个人缓存吗?
// style: TextStyle( style: TextStyle(
// color: color:
// isDarkMode ? Colors.white70 : Colors.black54, isDarkMode ? Colors.white70 : Colors.black54,
// ), ),
// ), ),
// actions: [ actions: [
// TextButton( TextButton(
// onPressed: () => Get.back(), onPressed: () => Get.back(),
// child: Text( child: Text(
// 'cancel'.tr, // 取消 'cancel'.tr, // 取消
// style: TextStyle( style: TextStyle(
// color: isDarkMode color: isDarkMode
// ? Colors.white70 ? Colors.white70
// : Colors.black54, : Colors.black54,
// ), ),
// ), ),
// ), ),
// TextButton( TextButton(
// onPressed: () { onPressed: () {
// Get.back(); Get.back();
// controller.clean(); controller.clean();
// }, },
// child: Text( child: Text(
// 'confirm'.tr, // 确定 'confirm'.tr, // 确定
// style: TextStyle( style: TextStyle(
// color: isDarkMode color: isDarkMode
// ? Colors.red[300] ? Colors.red[300]
// : Colors.red[600], : Colors.red[600],
// ), ),
// ), ),
// ), ),
// ], ],
// ), ),
// ); );
// }, },
// isDarkMode: isDarkMode, isDarkMode: isDarkMode,
// ), ),
Divider( Divider(
height: 1, height: 1,
color: isDarkMode color: isDarkMode

77
lib/modules/settings/controllers/settings_controller.dart

@ -1,12 +1,15 @@
import 'package:agent_service/agent_service.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import '../../../core/themes/theme_service.dart'; import '../../../core/themes/theme_service.dart';
import '../../../data/services/speech_factory.dart'; import '../../../data/services/speech_factory.dart';
import '../../../routes/app_routes.dart';
import '../models/language_model.dart'; import '../models/language_model.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import '../../../data/services/ble_manager.dart'; import '../../../data/services/ble_manager.dart';
import '../../../data/services/language_manager.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
@ -244,11 +247,9 @@ class SettingsController extends GetxController {
final PackageInfo packageInfo = await PackageInfo.fromPlatform(); final PackageInfo packageInfo = await PackageInfo.fromPlatform();
appVersion.value = packageInfo.version; appVersion.value = packageInfo.version;
buildNumber.value = packageInfo.buildNumber; buildNumber.value = packageInfo.buildNumber;
print('版本信息: ${packageInfo.version}+${packageInfo.buildNumber}');
} catch (e) { } catch (e) {
appVersion.value = '未知'; appVersion.value = '未知';
buildNumber.value = '0'; buildNumber.value = '0';
print('获取版本信息失败: $e');
} }
} }
@ -381,18 +382,88 @@ class SettingsController extends GetxController {
} }
} }
void setLanguage(LanguageModel data) { Future<void> setLanguage(LanguageModel data) async {
if (data.value != 'system') { if (data.value != 'system') {
List languageList = data.value.split('_'); List languageList = data.value.split('_');
Get.updateLocale(Locale(languageList[0], languageList[1])); Get.updateLocale(Locale(languageList[0], languageList[1]));
} else { } else {
Get.updateLocale(Get.deviceLocale!); Get.updateLocale(Get.deviceLocale!);
} }
print("languageList: ${data.value}");
language.value = data; language.value = data;
_storage.write('language', data.value); _storage.write('language', data.value);
// 更新agent服务的语音识别语言设置
await _updateAgentLanguageSettings(data);
// 延迟一下确保语言更新完成,然后重新进入应用
await Future.delayed(const Duration(milliseconds: 500));
// 重新进入应用 - 回到启动页面重新初始化
Get.offAllNamed(Routes.splash);
Get.back(); Get.back();
} }
/// 更新agent服务的语音识别语言设置
Future<void> _updateAgentLanguageSettings(LanguageModel languageModel) async {
try {
final languageManager = Get.find<LanguageManager>();
List<String> supportedLanguages = [];
// 根据选择的语言设置对应的ASR语言代码
if (languageModel.value == 'system') {
// 跟随系统语言
final deviceLocale = Get.deviceLocale;
if (deviceLocale != null) {
final lang = deviceLocale.languageCode;
final country = deviceLocale.countryCode;
if (country != null && country.isNotEmpty) {
supportedLanguages.add('$lang-$country');
} else {
// 根据语言代码映射到对应的ASR代码
final asrCode = languageManager.getAsrCodeByShortCode(lang);
if (asrCode != null) {
supportedLanguages.add(asrCode);
} else {
supportedLanguages.add('en-US'); // 默认英语
}
}
} else {
supportedLanguages.add('en-US'); // 默认英语
}
} else {
// 根据选择的语言值映射到ASR代码
final parts = languageModel.value.split('_');
if (parts.length == 2) {
final asrCode = languageManager.getAsrCodeByShortCode(parts[0]);
if (asrCode != null) {
supportedLanguages.add(asrCode);
} else {
// 直接使用语言-国家格式
supportedLanguages.add('${parts[0]}-${parts[1]}');
}
}
}
// 确保至少有一个语言
if (supportedLanguages.isEmpty) {
supportedLanguages.add('en-US');
}
Logger().i('更新agent服务语音识别语言: ${supportedLanguages.join(', ')}');
// 调用agent服务更新语言设置
final success = await AgentService.setSupportedLanguages(supportedLanguages);
if (success) {
Logger().i('agent服务语言设置更新成功');
} else {
Logger().w('agent服务语言设置更新失败');
}
} catch (e) {
Logger().e('更新agent服务语言设置时出错: $e');
}
}
// 加载当前语音服务类型 // 加载当前语音服务类型
void _loadCurrentSpeechServiceType() { void _loadCurrentSpeechServiceType() {
try { try {

105
lib/modules/settings/views/settings_view.dart

@ -16,13 +16,6 @@ import '../../profile/views/unified_avatar.dart';
class SettingsView extends GetView<SettingsController> { class SettingsView extends GetView<SettingsController> {
const SettingsView({Key? key}) : super(key: key); const SettingsView({Key? key}) : super(key: key);
// 正确获取用户名方式
// String get userName => User.getname();
// String get deviceInfo => User.getdeviceInfo();
// String get userId => User.getuserId();
// String get standing => User.getstanding();
// String get avatar => User.getnavatar();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// 直接使用 Theme.of(context) 获取当前主题 // 直接使用 Theme.of(context) 获取当前主题
@ -425,32 +418,31 @@ class SettingsView extends GetView<SettingsController> {
}, },
isDarkMode: isDarkMode, isDarkMode: isDarkMode,
), ),
if (Platform.isAndroid) ...[ // if (Platform.isAndroid) ...[
Divider( // Divider(
height: 1, // height: 1,
color: isDarkMode // color: isDarkMode
? Colors.white.withOpacity(0.1) // ? Colors.white.withOpacity(0.1)
: Colors.grey[200]), // : Colors.grey[200]),
// 隐私设置 // // 隐私设置
_buildSimpleNavigationSetting( // _buildSimpleNavigationSetting(
title: 'privacySettings'.tr, // 隐私设置 // title: 'privacySettings'.tr, // 隐私设置
subtitle: 'privacySettingsInfo'.tr, // 管理数据收集和权限 // subtitle: 'privacySettingsInfo'.tr, // 管理数据收集和权限
icon: Icons.shield_outlined, // icon: Icons.shield_outlined,
iconBgColor: isDarkMode // iconBgColor: isDarkMode
? Colors.purple[900]!.withOpacity(0.3) // ? Colors.purple[900]!.withOpacity(0.3)
: Colors.purple[100]!, // : Colors.purple[100]!,
iconColor: isDarkMode // iconColor:
? Colors.purple[300]! // isDarkMode ? Colors.purple[300]! : Colors.purple[600]!,
: Colors.purple[600]!, // titleColor: isDarkMode ? Colors.white : null,
titleColor: isDarkMode ? Colors.white : null, // subtitleColor: isDarkMode ? Colors.white70 : null,
subtitleColor: isDarkMode ? Colors.white70 : null, // onTap: () {
onTap: () { // AppSettings.openAppSettings();
AppSettings.openAppSettings(); // },
}, // isDarkMode: isDarkMode,
isDarkMode: isDarkMode, // ),
) // ],
],
Divider( Divider(
height: 1, height: 1,
@ -599,32 +591,31 @@ class SettingsView extends GetView<SettingsController> {
onTap: () => Get.toNamed(Routes.OTA_UPGRADE), onTap: () => Get.toNamed(Routes.OTA_UPGRADE),
isDarkMode: isDarkMode, isDarkMode: isDarkMode,
), ),
if (Platform.isAndroid) ...[ // if (Platform.isAndroid) ...[
Divider( Divider(
height: 1, height: 1,
color: isDarkMode color: isDarkMode
? Colors.white.withOpacity(0.1) ? Colors.white.withOpacity(0.1)
: Colors.grey[200]), : Colors.grey[200]),
// 添加权限管理设置项 // 权限管理设置项
_buildSimpleNavigationSetting( _buildSimpleNavigationSetting(
title: 'permissionManagement'.tr, // 权限管理 title: 'permissionManagement'.tr, // 权限管理
subtitle: 'permissionManagementInfo'.tr, // 管理应用权限设置 subtitle: 'permissionManagementInfo'.tr, // 管理应用权限设置
icon: Icons.security, icon: Icons.security,
iconBgColor: isDarkMode iconBgColor: isDarkMode
? Colors.orange[900]!.withOpacity(0.3) ? Colors.orange[900]!.withOpacity(0.3)
: Colors.orange[100]!, : Colors.orange[100]!,
iconColor: isDarkMode iconColor:
? Colors.orange[300]! isDarkMode ? Colors.orange[300]! : Colors.orange[600]!,
: Colors.orange[600]!, titleColor: isDarkMode ? Colors.white : null,
titleColor: isDarkMode ? Colors.white : null, subtitleColor: isDarkMode ? Colors.white70 : null,
subtitleColor: isDarkMode ? Colors.white70 : null, onTap: () {
onTap: () { Get.toNamed(Routes.permissions);
Get.toNamed(Routes.permissions); },
}, isDarkMode: isDarkMode,
isDarkMode: isDarkMode, ),
), // ]
]
], ],
), ),
), ),

11
lib/modules/translation/controllers/translation_controller.dart

@ -9,6 +9,7 @@ import 'package:get_storage/get_storage.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/music_manager.dart'; import '../../../data/services/music_manager.dart';
import '../../../data/services/volcano_translation_service.dart'; import '../../../data/services/volcano_translation_service.dart';
import '../../../data/services/tts_service.dart'; import '../../../data/services/tts_service.dart';
@ -30,6 +31,8 @@ class TranslationController extends GetxController {
static const String TAG = 'TranslationController'; static const String TAG = 'TranslationController';
// 服务 // 服务
final BluetoothManager _bluetoothManager = Get.find<BluetoothManager>();
final BleManager _bleManager = Get.find<BleManager>();
final AsrService _asrService = Get.find<AsrService>(); final AsrService _asrService = Get.find<AsrService>();
final VolcanoTranslationService _translationService = final VolcanoTranslationService _translationService =
Get.find<VolcanoTranslationService>(); Get.find<VolcanoTranslationService>();
@ -364,7 +367,7 @@ class TranslationController extends GetxController {
@override @override
void onClose() { void onClose() {
stopRecognition(); stopAll();
_recognitionSubscription?.cancel(); _recognitionSubscription?.cancel();
_recognitionSubscription = null; _recognitionSubscription = null;
_voiceInteractionSubscription?.cancel(); _voiceInteractionSubscription?.cancel();
@ -483,7 +486,11 @@ class TranslationController extends GetxController {
if (currentMode.value == 'faceToFace') { if (currentMode.value == 'faceToFace') {
_asrService.stopContinuousRecognition(); _asrService.stopContinuousRecognition();
} else { } else {
await _ttsService.setAudioOutputDevice(0); if (_bluetoothManager.currentDeviceRx.value != null) {
restoreOriginalAudioState();
} else {
disableBluetoothAudio();
}
await _asrService.startContinuousRecognition(_audioSourceType); await _asrService.startContinuousRecognition(_audioSourceType);
// 开始ASR活跃时长计时 // 开始ASR活跃时长计时
_startAsrActiveTracking(); _startAsrActiveTracking();

22
lib/modules/translation/views/translation_view.dart

@ -662,13 +662,15 @@ class TranslationView extends GetView<TranslationController> {
// 显示语言选择对话框 // 显示语言选择对话框
void _showLanguageSelectionDialog(bool isSource, bool isDarkMode) { void _showLanguageSelectionDialog(bool isSource, bool isDarkMode) {
Get.dialog( Get.dialog(Dialog(
Dialog( backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, shape: RoundedRectangleBorder(
shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16.r), // 统一圆角尺寸为16
borderRadius: BorderRadius.circular(16.r), // 统一圆角尺寸为16 ),
), alignment: isSource ? Alignment.centerLeft : Alignment.centerRight,
insetPadding: EdgeInsets.symmetric(horizontal: 20.w), insetPadding: EdgeInsets.symmetric(horizontal: 10.w),
child: SizedBox(
width: 0.4.sw,
child: Padding( child: Padding(
// 使用Padding替代Container的padding // 使用Padding替代Container的padding
padding: EdgeInsets.all(12.w), // 统一内边距 padding: EdgeInsets.all(12.w), // 统一内边距
@ -807,7 +809,7 @@ class TranslationView extends GetView<TranslationController> {
), ),
), ),
), ),
); ));
} }
// 构建加载更多指示器 // 构建加载更多指示器
@ -1035,7 +1037,7 @@ class TranslationView extends GetView<TranslationController> {
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200], backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200],
colorText: Get.isDarkMode ? Colors.white : Colors.black, colorText: Get.isDarkMode ? Colors.white : Colors.black,
duration: const Duration(seconds: 2), duration: const Duration(milliseconds: 800),
); );
} catch (e) { } catch (e) {
// 显示删除失败提示 // 显示删除失败提示
@ -1045,7 +1047,7 @@ class TranslationView extends GetView<TranslationController> {
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[400], backgroundColor: Colors.red[400],
colorText: Colors.white, colorText: Colors.white,
duration: const Duration(seconds: 3), duration: const Duration(milliseconds: 1500),
); );
} }
} }

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

@ -84,6 +84,7 @@ object AgentService : CoroutineScope {
// 服务端连接 // 服务端连接
private var serverurl = "" private var serverurl = ""
private var usertoken = "" private var usertoken = ""
private var language = ""
// 历史聊天消息缓存 // 历史聊天消息缓存
private val historyMessages = JSONArray() private val historyMessages = JSONArray()
@ -200,8 +201,8 @@ object AgentService : CoroutineScope {
//获取语音识别的语种 //获取语音识别的语种
val supportedLanguagesConfig = config["supportedLanguages"] as? List<String> val supportedLanguagesConfig = config["supportedLanguages"] as? List<String>
val supportedLanguages = supportedLanguagesConfig?.toTypedArray() val supportedLanguages = supportedLanguagesConfig?.toTypedArray()
?: arrayOf("zh-CN", "en-US") // 默认支持多语言 ?: arrayOf("zh-CN") // 默认支持多语言
language = supportedLanguages[0]
val asrInitSuccess = initialize( val asrInitSuccess = initialize(
subscriptionKey = config["azureSpeechKey"]?.toString() ?: "", subscriptionKey = config["azureSpeechKey"]?.toString() ?: "",
region = config["azureSpeechRegion"]?.toString() ?: "", region = config["azureSpeechRegion"]?.toString() ?: "",
@ -422,7 +423,7 @@ object AgentService : CoroutineScope {
try { try {
delay(maxIdleSeconds * 1000L) delay(maxIdleSeconds * 1000L)
if (isActive && isRecognitionActive && !hasSpeechDetected && !isTtsSpeaking && !isAiStreaming) { if (isActive && isRecognitionActive && !isTtsSpeaking && !isAiStreaming) {
stopRecognition() stopRecognition()
sendEvent( sendEvent(
"auto_stop", "auto_stop",
@ -524,8 +525,8 @@ object AgentService : CoroutineScope {
override fun onRecognizing(recognizing: String, detectedLanguage: String) { override fun onRecognizing(recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()) { if (recognizing.isNotEmpty()) {
// 检测到语音,更新状态 // 检测到语音,更新状态
val previousHasSpeech = hasSpeechDetected val previousHasSpeech = hasSpeechDetected
_hasSpeechDetected.set(true) _hasSpeechDetected.set(true)
if (!previousHasSpeech) { if (!previousHasSpeech) {
restartIdleCheck() restartIdleCheck()
@ -829,7 +830,8 @@ object AgentService : CoroutineScope {
// 先添加系统提示词 // 先添加系统提示词
if (systemPrompt.isNotEmpty()) { if (systemPrompt.isNotEmpty()) {
val mapdata = UserPortrait.getAll() val mapdata = UserPortrait.getAll().toMutableMap()
mapdata["language"] = language
// Log.d(TAG, "liwei--------- 用户画像: ${mapdata}") // Log.d(TAG, "liwei--------- 用户画像: ${mapdata}")
val _systemPrompt = Utils.fillTemplate(systemPrompt, mapdata) val _systemPrompt = Utils.fillTemplate(systemPrompt, mapdata)
messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt)) messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt))
@ -1120,7 +1122,7 @@ object AgentService : CoroutineScope {
} }
/** /**
* 设置支持的语音识别语言 * 设置支持的语音识别语言,同时更新TTS语言设置
*/ */
fun setSupportedLanguages(languages: List<String>): Boolean { fun setSupportedLanguages(languages: List<String>): Boolean {
if (!isInitialized) { if (!isInitialized) {
@ -1134,33 +1136,58 @@ object AgentService : CoroutineScope {
sendEvent("error", mapOf("code" to "EMPTY_LANGUAGES", "message" to "语言列表不能为空")) sendEvent("error", mapOf("code" to "EMPTY_LANGUAGES", "message" to "语言列表不能为空"))
return false return false
} }
language = languages[0]
return try { return try {
Log.i(TAG, "设置支持的语言: ${languages.joinToString(", ")}") Log.i(TAG, "设置支持的语言: ${languages.joinToString(", ")}")
// 重新初始化ASR服务以应用新的语言设置 // 重新初始化ASR服务以应用新的语言设置
val success = azureAsrHelper?.initialize( val asrSuccess = AzureAsrHelper(context).apply {
subscriptionKey = this.azureSpeechKey, // 使用协程处理异步初始化
region = this.azureSpeechRegion, CoroutineScope(Dispatchers.IO).launch {
supportedLanguages = languages.toTypedArray(), //获取语音识别的语种
audioSourceType = audioSourceType val supportedLanguagesConfig = config["supportedLanguages"] as? List<String>
) ?: false val supportedLanguages = supportedLanguagesConfig?.toTypedArray()
?: arrayOf("zh-CN") // 默认支持多语言
if (success) { language = supportedLanguages[0]
Log.i(TAG, "语言设置更新成功") val asrInitSuccess = initialize(
subscriptionKey = config["azureSpeechKey"]?.toString() ?: "",
region = config["azureSpeechRegion"]?.toString() ?: "",
supportedLanguages = supportedLanguages,
audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL
else AzureAsrHelper.AudioSourceType.MICROPHONE
)
if (asrInitSuccess) {
withContext(Dispatchers.Main) {
recognizeCallback()
}
} else {
Log.w(TAG, "Azure ASR初始化失败")
}
}
}
// 同时更新TTS语言设置
val ttsSuccess = updateTtsLanguage(languages.first()) // 使用第一个语言作为TTS语言
val overallSuccess = asrSuccess && ttsSuccess
if (overallSuccess) {
Log.i(TAG, "ASR和TTS语言设置更新成功")
sendEvent("languages_updated", mapOf( sendEvent("languages_updated", mapOf(
"languages" to languages, "languages" to languages,
"status" to "success" "status" to "success",
"asr_success" to asrSuccess,
"tts_success" to ttsSuccess
)) ))
} else { } else {
Log.e(TAG, "语言设置更新失败") Log.e(TAG, "语言设置更新失败 - ASR: $asrSuccess, TTS: $ttsSuccess")
sendEvent("error", mapOf( sendEvent("error", mapOf(
"code" to "LANGUAGE_UPDATE_FAILED", "code" to "LANGUAGE_UPDATE_FAILED",
"message" to "语言设置更新失败" "message" to "语言设置更新失败 - ASR: $asrSuccess, TTS: $ttsSuccess"
)) ))
} }
success overallSuccess
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "设置支持语言异常: ${e.message}") Log.e(TAG, "设置支持语言异常: ${e.message}")
sendEvent("error", mapOf( sendEvent("error", mapOf(
@ -1171,6 +1198,105 @@ object AgentService : CoroutineScope {
} }
} }
/**
* 更新TTS语言设置
*
* @param languageCode ASR语言代码,如 "zh-CN", "en-US"
* @return 是否更新成功
*/
private fun updateTtsLanguage(languageCode: String): Boolean {
return try {
Log.i(TAG, "更新TTS语言设置: $languageCode")
// 根据ASR语言代码映射到对应的TTS语音角色
val voiceName = getTtsVoiceNameByAsrCode(languageCode)
if (voiceName.isNotEmpty()) {
// 设置TTS语音角色
val success = ttsService?.setVoice(voiceName) ?: false
if (success) {
Log.i(TAG, "TTS语音角色设置成功: $voiceName")
} else {
Log.w(TAG, "TTS语音角色设置失败: $voiceName")
}
success
} else {
Log.w(TAG, "未找到语言代码 $languageCode 对应的TTS语音角色")
false
}
} catch (e: Exception) {
Log.e(TAG, "更新TTS语言设置异常: ${e.message}")
false
}
}
/**
* 根据ASR语言代码获取对应的TTS语音角色名称
* 与LanguageManager中的映射保持一致
*
* @param asrCode ASR语言代码,如 "zh-CN", "en-US"
* @return TTS语音角色名称
*/
private fun getTtsVoiceNameByAsrCode(asrCode: String): String {
return when (asrCode) {
// 中文相关
"zh-CN" -> "zh-CN-XiaoxiaoNeural"
"zh-TW" -> "zh-TW-HsiaoChenNeural"
"zh-HK" -> "zh-HK-HiuMaanNeural"
// 英语相关
"en-US" -> "en-US-AriaNeural"
// 亚洲语言
"ja-JP" -> "ja-JP-NanamiNeural"
"ko-KR" -> "ko-KR-SunHiNeural"
"th-TH" -> "th-TH-PremwadeeNeural"
"vi-VN" -> "vi-VN-HoaiMyNeural"
"id-ID" -> "id-ID-GadisNeural"
"hi-IN" -> "hi-IN-SwaraNeural"
"ms-MY" -> "ms-MY-YasminNeural"
"bn-IN" -> "bn-IN-TanishaaNeural"
"ta-IN" -> "ta-IN-PallaviNeural"
"te-IN" -> "te-IN-ShrutiNeural"
"mr-IN" -> "mr-IN-AarohiNeural"
"ur-IN" -> "ur-IN-GulNeural"
"tr-TR" -> "tr-TR-EmelNeural"
"fa-IR" -> "fa-IR-DilaraNeural"
// 欧洲语言
"fr-FR" -> "fr-FR-DeniseNeural"
"es-ES" -> "es-ES-ElviraNeural"
"pt-BR" -> "pt-BR-FranciscaNeural"
"it-IT" -> "it-IT-ElsaNeural"
"de-DE" -> "de-DE-KatjaNeural"
"ru-RU" -> "ru-RU-SvetlanaNeural"
"pl-PL" -> "pl-PL-AgnieszkaNeural"
"nl-NL" -> "nl-NL-ColetteNeural"
"sv-SE" -> "sv-SE-SofieNeural"
"cs-CZ" -> "cs-CZ-VlastaNeural"
"el-GR" -> "el-GR-AthinaNeural"
"ro-RO" -> "ro-RO-AlinaNeural"
"hu-HU" -> "hu-HU-NoemiNeural"
"uk-UA" -> "uk-UA-PolinaNeural"
"da-DK" -> "da-DK-ChristelNeural"
"fi-FI" -> "fi-FI-NooraNeural"
"nb-NO" -> "nb-NO-IselinNeural"
"hr-HR" -> "hr-HR-GabrijelaNeural"
"ca-ES" -> "ca-ES-JoanaNeural"
// 阿拉伯语和非洲语言
"ar-EG" -> "ar-EG-SalmaNeural"
"sw-KE" -> "sw-KE-ZuriNeural"
else -> {
Log.w(TAG, "未支持的语言代码: $asrCode,使用默认英语语音")
"en-US-AriaNeural"
}
}
}
/** /**
* 保存聊天消息 * 保存聊天消息
*/ */

236
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -59,6 +59,7 @@ class AgentServiceImpl: NSObject {
internal var isAiStreaming: Bool = false internal var isAiStreaming: Bool = false
var currsessionId: String = "" var currsessionId: String = ""
var language: String = ""
private var idleTimer: DispatchSourceTimer? private var idleTimer: DispatchSourceTimer?
private let maxIdleSeconds: TimeInterval = 10 private let maxIdleSeconds: TimeInterval = 10
@ -158,6 +159,9 @@ class AgentServiceImpl: NSObject {
if let openaivisionModel = config["openaivisionModel"] as? String { if let openaivisionModel = config["openaivisionModel"] as? String {
self.visionModel = openaivisionModel self.visionModel = openaivisionModel
} }
if let supportedLanguages = config["supportedLanguages"] as? [String] {
self.language = supportedLanguages[0]
}
if let customSystemPrompt = config["systemPrompt"] as? String, !customSystemPrompt.isEmpty { if let customSystemPrompt = config["systemPrompt"] as? String, !customSystemPrompt.isEmpty {
self.systemPrompt = customSystemPrompt self.systemPrompt = customSystemPrompt
} else { } else {
@ -208,7 +212,7 @@ class AgentServiceImpl: NSObject {
azureTtsHelper?.addListener(self) azureTtsHelper?.addListener(self)
// 获取支持的语言列表,支持从配置中读取 // 获取支持的语言列表,支持从配置中读取
let supportedLanguages = config["supportedLanguages"] as? [String] ?? ["zh-CN", "en-US"] let supportedLanguages = config["supportedLanguages"] as? [String] ?? ["zh-CN"]
guard let asrSuccess = azureAsrHelper?.initialize( guard let asrSuccess = azureAsrHelper?.initialize(
subscriptionKey: azureSpeechKey, subscriptionKey: azureSpeechKey,
@ -287,8 +291,8 @@ class AgentServiceImpl: NSObject {
// 使用更安全的方式检查self是否存在 // 使用更安全的方式检查self是否存在
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let self = self else { return } guard let self = self else { return }
if self.isRecognizing && !self.hasSpeechDetected && !self.isSpeaking && !self.isAiStreaming { if self.isRecognizing && !self.isSpeaking && !self.isAiStreaming {
self.stopRecognition() self.stopRecognition()
self.sendEvent(name: "auto_stop", data: [ self.sendEvent(name: "auto_stop", data: [
"reason": "idle_timeout", "reason": "idle_timeout",
@ -345,8 +349,14 @@ print("ai启动语音=\(audioSourceType)=\(useBle)")
return true return true
} }
func recognizeCallback(useBle: Bool = false) -> Bool { func recognizeCallback() -> Bool {
print("重新设置识别回调=")
guard let asrsetupSuccess = azureAsrHelper?.setupEventListeners(
callback: self
), asrsetupSuccess else {
sendError("初始化语音识别服务失败", code: "ASR_INIT_ERROR")
return false
}
return true return true
} }
@ -361,7 +371,12 @@ print("ai启动语音=\(audioSourceType)=\(useBle)")
return true return true
} }
func disableBluetoothAudio() {
azureAsrHelper?.disableBluetoothAudio()
}
func restoreOriginalAudioState() {
azureAsrHelper?.restoreOriginalAudioState()
}
func pushAudioData(_ audioData: Data) -> Bool { func pushAudioData(_ audioData: Data) -> Bool {
if !isRecognizing || !isInitialized { if !isRecognizing || !isInitialized {
return false return false
@ -454,8 +469,8 @@ audioStream.saveAudioDataTo(data: audioData)
processWithChatApiServiceInternal(userMessage: userMessage, displayText: text, speakResponse: speakResponse) processWithChatApiServiceInternal(userMessage: userMessage, displayText: text, speakResponse: speakResponse)
} }
private func processWithChatApiServiceInternal(userMessage: [String: Any], displayText: String, speakResponse: Bool = true, hasImage: Bool = false) { private func processWithChatApiServiceInternal(userMessage: [String: Any], displayText: String, speakResponse: Bool = true, hasImage: Bool = false,imagePath:String = "") {
os_log("开始处理ChatAPI请求: displayText=%{public}@, speakResponse=%{public}@, hasImage=%{public}@", os_log("开始处理ChatAPI请求: displayText=%{public}@, speakResponse=%{public}@, hasImage=%{public}@",
log: logger, type: .info, displayText, speakResponse ? "是" : "否", hasImage ? "是" : "否") log: logger, type: .info, displayText, speakResponse ? "是" : "否", hasImage ? "是" : "否")
guard let chatApiService = chatApiService else { guard let chatApiService = chatApiService else {
@ -476,9 +491,16 @@ audioStream.saveAudioDataTo(data: audioData)
var messages: [[String: Any]] = [] var messages: [[String: Any]] = []
if !systemPrompt.isEmpty { if !systemPrompt.isEmpty {
var info = UserPortrait.shared.getAll()
let info: [String: String] = UserPortrait.shared.getAll() info["language"] = self.language
// 添加调试日志
print("系统提示词模板: \(systemPrompt)")
print("用户画像数据: \(info)")
let _systemPrompt = fillTemplate(systemPrompt, with: info) let _systemPrompt = fillTemplate(systemPrompt, with: info)
print("替换后的系统提示词: \(_systemPrompt)")
messages.append(chatApiService.createSystemMessage(content: _systemPrompt)) messages.append(chatApiService.createSystemMessage(content: _systemPrompt))
os_log("添加系统提示消息", log: logger, type: .info) os_log("添加系统提示消息", log: logger, type: .info)
} }
@ -495,7 +517,8 @@ audioStream.saveAudioDataTo(data: audioData)
agentService: self, agentService: self,
speakResponse: speakResponse, speakResponse: speakResponse,
displayText: displayText, displayText: displayText,
hasImage: hasImage hasImage: hasImage,
imagePath: imagePath
) )
currsessionId = UUID().uuidString currsessionId = UUID().uuidString
os_log("设置回调并调用sendMessageStream", log: logger, type: .info) os_log("设置回调并调用sendMessageStream", log: logger, type: .info)
@ -552,7 +575,8 @@ audioStream.saveAudioDataTo(data: audioData)
userMessage: userMessage, userMessage: userMessage,
displayText: displayText, displayText: displayText,
speakResponse: speakResponse, speakResponse: speakResponse,
hasImage: true hasImage: true,
imagePath: imagePath
) )
} }
} }
@ -879,53 +903,201 @@ private func jsonToString(_ json: [String: Any]) -> String? {
} }
/** /**
* 设置支持的语音识别语言 * 设置支持的语音识别语言,同时更新TTS语言设置
* *
* @param languages 支持的语言列表,如 ["zh-CN", "en-US"] * @param languages 支持的语言列表,如 ["zh-CN", "en-US"]
* @return 是否设置成功 * @return 是否设置成功
*/ */
func setSupportedLanguages(_ languages: [String]) -> Bool { func setSupportedLanguages(_ languages: [String]) -> Bool {
os_log("设置支持的语言: %{public}@", log: logger, type: .info, languages.joined(separator: ", ")) os_log("设置支持的语言: %{public}@", log: logger, type: .info, languages.joined(separator: ", "))
if !isInitialized { if !isInitialized {
os_log("服务未初始化,无法设置语言", log: logger, type: .error) os_log("服务未初始化,无法设置语言", log: logger, type: .error)
sendError("服务未初始化", code: "NOT_INITIALIZED") sendError("服务未初始化", code: "NOT_INITIALIZED")
return false return false
} }
if languages.isEmpty { if languages.isEmpty {
os_log("语言列表为空,使用默认语言", log: logger, type: .info) os_log("语言列表为空,使用默认语言", log: logger, type: .info)
return false return false
} }
self.language = languages[0]
// 如果正在识别,需要先停止 // 如果正在识别,需要先停止
let wasRecognizing = isRecognizing let wasRecognizing = isRecognizing
if wasRecognizing { if wasRecognizing {
_ = stopRecognition() _ = stopRecognition()
} }
// 重新初始化ASR引擎,使用新的语言设置 // 重新初始化ASR引擎,使用新的语言设置
guard let success = azureAsrHelper?.initialize( guard let asrSuccess = azureAsrHelper?.initialize(
subscriptionKey: azureSpeechKey, subscriptionKey: azureSpeechKey,
region: azureSpeechRegion, region: azureSpeechRegion,
supportedLanguages: languages, supportedLanguages: languages,
audioSourceType: .microphone audioSourceType: .microphone
), success else { ), asrSuccess else {
os_log("重新初始化ASR引擎失败", log: logger, type: .error) sendError("初始化语音识别服务失败", code: "ASR_INIT_ERROR")
sendError("重新初始化ASR引擎失败", code: "ASR_INIT_ERROR") return false
}
guard let asrsetupSuccess = azureAsrHelper?.setupEventListeners(
callback: self
), asrsetupSuccess else {
sendError("初始化语音识别服务失败", code: "ASR_INIT_ERROR")
return false return false
} }
os_log("成功设置支持的语言: %{public}@", log: logger, type: .info, languages.joined(separator: ", ")) // 同时更新TTS语言设置
let ttsSuccess = updateTtsLanguage(languages.first ?? "zh-CN")
let overallSuccess = asrSuccess && ttsSuccess
if overallSuccess {
os_log("ASR和TTS语言设置更新成功: %{public}@", log: logger, type: .info, languages.joined(separator: ", "))
} else {
os_log("语言设置更新失败 - ASR: %{public}@, TTS: %{public}@", log: logger, type: .error, String(asrSuccess), String(ttsSuccess))
}
// 如果之前正在识别,重新开始 // 如果之前正在识别,重新开始
if wasRecognizing { if wasRecognizing {
_ = startRecognition() _ = startRecognition()
} }
return true return overallSuccess
} }
/**
* 更新TTS语言设置
*
* @param languageCode ASR语言代码,如 "zh-CN", "en-US"
* @return 是否更新成功
*/
private func updateTtsLanguage(_ languageCode: String) -> Bool {
os_log("更新TTS语言设置: %{public}@", log: logger, type: .info, languageCode)
// 根据ASR语言代码映射到对应的TTS语音角色
let voiceName = getTtsVoiceNameByAsrCode(languageCode)
if !voiceName.isEmpty {
// 设置TTS语音角色
let success = azureTtsHelper?.setVoice(voiceName) ?? false
if success {
os_log("TTS语音角色设置成功: %{public}@", log: logger, type: .info, voiceName)
} else {
os_log("TTS语音角色设置失败: %{public}@", log: logger, type: .error, voiceName)
}
return success
} else {
os_log("未找到语言代码 %{public}@ 对应的TTS语音角色", log: logger, type: .error, languageCode)
return false
}
}
/**
* 根据ASR语言代码获取对应的TTS语音角色名称
* 与LanguageManager中的映射保持一致
*
* @param asrCode ASR语言代码,如 "zh-CN", "en-US"
* @return TTS语音角色名称
*/
private func getTtsVoiceNameByAsrCode(_ asrCode: String) -> String {
switch asrCode {
// 中文相关
case "zh-CN":
return "zh-CN-XiaoxiaoNeural"
case "zh-TW":
return "zh-TW-HsiaoChenNeural"
case "zh-HK":
return "zh-HK-HiuMaanNeural"
// 英语相关
case "en-US":
return "en-US-AriaNeural"
// 亚洲语言
case "ja-JP":
return "ja-JP-NanamiNeural"
case "ko-KR":
return "ko-KR-SunHiNeural"
case "th-TH":
return "th-TH-PremwadeeNeural"
case "vi-VN":
return "vi-VN-HoaiMyNeural"
case "id-ID":
return "id-ID-GadisNeural"
case "hi-IN":
return "hi-IN-SwaraNeural"
case "ms-MY":
return "ms-MY-YasminNeural"
case "bn-IN":
return "bn-IN-TanishaaNeural"
case "ta-IN":
return "ta-IN-PallaviNeural"
case "te-IN":
return "te-IN-ShrutiNeural"
case "mr-IN":
return "mr-IN-AarohiNeural"
case "ur-IN":
return "ur-IN-GulNeural"
case "tr-TR":
return "tr-TR-EmelNeural"
case "fa-IR":
return "fa-IR-DilaraNeural"
// 欧洲语言
case "fr-FR":
return "fr-FR-DeniseNeural"
case "es-ES":
return "es-ES-ElviraNeural"
case "pt-BR":
return "pt-BR-FranciscaNeural"
case "it-IT":
return "it-IT-ElsaNeural"
case "de-DE":
return "de-DE-KatjaNeural"
case "ru-RU":
return "ru-RU-SvetlanaNeural"
case "pl-PL":
return "pl-PL-AgnieszkaNeural"
case "nl-NL":
return "nl-NL-ColetteNeural"
case "sv-SE":
return "sv-SE-SofieNeural"
case "cs-CZ":
return "cs-CZ-VlastaNeural"
case "el-GR":
return "el-GR-AthinaNeural"
case "ro-RO":
return "ro-RO-AlinaNeural"
case "hu-HU":
return "hu-HU-NoemiNeural"
case "uk-UA":
return "uk-UA-PolinaNeural"
case "da-DK":
return "da-DK-ChristelNeural"
case "fi-FI":
return "fi-FI-NooraNeural"
case "nb-NO":
return "nb-NO-IselinNeural"
case "hr-HR":
return "hr-HR-GabrijelaNeural"
case "ca-ES":
return "ca-ES-JoanaNeural"
// 阿拉伯语和非洲语言
case "ar-EG":
return "ar-EG-SalmaNeural"
case "sw-KE":
return "sw-KE-ZuriNeural"
default:
os_log("未支持的语言代码: %{public}@,使用默认英语语音", log: logger, type: .error, asrCode)
return "en-US-AriaNeural"
}
}
func dispose() -> Bool { func dispose() -> Bool {
print("ai释放资源") print("ai释放资源")
if isRecognizing { if isRecognizing {
@ -967,19 +1139,22 @@ class ChatApiStreamCallback: StreamCallback {
private let speakResponse: Bool private let speakResponse: Bool
private let displayText: String private let displayText: String
private let hasImage: Bool private let hasImage: Bool
private let imagePath: String
private var responseBuilder = "" private var responseBuilder = ""
private var metadata = "" private var metadata = ""
private var userMetadata:[String: Any] = [:] private var userMetadata:[String: Any] = [:]
private var reply = true; //是否回复 private var reply = true; //是否回复
private var broadcast = true //是否播发 private var broadcast = true //是否播发
init(agentService: AgentServiceImpl, speakResponse: Bool, displayText: String, hasImage: Bool) { init(agentService: AgentServiceImpl, speakResponse: Bool, displayText: String, hasImage: Bool,imagePath:String) {
self.agentService = agentService self.agentService = agentService
self.speakResponse = speakResponse self.speakResponse = speakResponse
self.displayText = displayText self.displayText = displayText
self.hasImage = hasImage self.hasImage = hasImage
self.imagePath = imagePath
self.userMetadata["hasImage"] = hasImage
self.userMetadata["imagePath"] = imagePath
} }
func onUsage(_ sessionid: String,_ prompt_tokens: Int?,_ completion_tokens: Int?,_ total_tokens: Int?){ func onUsage(_ sessionid: String,_ prompt_tokens: Int?,_ completion_tokens: Int?,_ total_tokens: Int?){
@ -1418,6 +1593,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
hasSpeechDetected = true hasSpeechDetected = true
if !previousHasSpeech { if !previousHasSpeech {
print("onRecognizing restartIdleCheck")
restartIdleCheck() restartIdleCheck()
} }

9
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift

@ -51,7 +51,14 @@ public class AgentServicePlugin: NSObject, FlutterPlugin {
case "stopConversation": case "stopConversation":
impl.stopRecognition() impl.stopRecognition()
result(true) result(true)
case "disableBluetoothAudio":
impl.disableBluetoothAudio();
return
case "restoreOriginalAudioState":
impl.restoreOriginalAudioState();
return
case "processTextInput": case "processTextInput":
guard let arguments = call.arguments as? [String: Any], guard let arguments = call.arguments as? [String: Any],
let text = arguments["text"] as? String else { let text = arguments["text"] as? String else {

45
local_plugins/agent_service/lib/agent_service.dart

@ -261,6 +261,33 @@ class AgentService {
} }
} }
/// 恢复原始音频状态
///
/// 恢复原始音频状态
/// 返回是否成功恢复原始音频状态
static Future<bool> restoreOriginalAudioState() async {
try {
final bool result =
await _channel.invokeMethod('restoreOriginalAudioState');
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '恢复原始音频状态失败', e.details);
}
}
/// 禁用蓝牙音频
///
/// 禁用蓝牙音频
/// 返回是否成功禁用蓝牙音频
static Future<bool> disableBluetoothAudio() async {
try {
final bool result = await _channel.invokeMethod('disableBluetoothAudio');
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '禁用蓝牙音频失败', e.details);
}
}
/// 停止对话 /// 停止对话
/// ///
/// 停止语音识别 /// 停止语音识别
@ -332,8 +359,6 @@ class AgentService {
} }
} }
/// 停止语音合成 /// 停止语音合成
static Future<bool> stopAiSteam() async { static Future<bool> stopAiSteam() async {
try { try {
@ -385,6 +410,7 @@ class AgentService {
/// ///
/// [imagePath] 图片文件路径 /// [imagePath] 图片文件路径
/// [text] 可选的文本描述或问题 /// [text] 可选的文本描述或问题
/// [imageUrl] 图片url
/// [speakResponse] 是否朗读响应 /// [speakResponse] 是否朗读响应
/// 返回是否成功处理图片 /// 返回是否成功处理图片
static Future<bool> processImageInput(String imagePath, String imageUrl, static Future<bool> processImageInput(String imagePath, String imageUrl,
@ -402,6 +428,21 @@ class AgentService {
} }
} }
/// 设置支持的语音识别语言
///
/// [languages] 支持的语言列表,如 ["zh-CN", "en-US"]
/// 返回是否设置成功
static Future<bool> setSupportedLanguages(List<String> languages) async {
try {
final bool result = await _channel.invokeMethod('setSupportedLanguages', {
'languages': languages,
});
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '设置支持语言失败', e.details);
}
}
/// 释放资源 /// 释放资源
static Future<bool> dispose() async { static Future<bool> dispose() async {
try { try {

2
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

@ -745,7 +745,7 @@ class AzureAsrHelper(private val context: Context) {
*/ */
fun saveAudioDataTo(buffer: ByteArray) { fun saveAudioDataTo(buffer: ByteArray) {
if (audioSourceType == AudioSourceType.MICROPHONE) return if (audioSourceType == AudioSourceType.MICROPHONE||!isContinuousRecognitionActive) return
// 放入队列,由写线程写入 // 放入队列,由写线程写入
writeQueue.offer(buffer.copyOf()) writeQueue.offer(buffer.copyOf())

40
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift

@ -98,11 +98,11 @@ public class AzureAsrHelper: NSObject {
} }
// 创建语音配置 // 创建语音配置
speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region) speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region)
// 针对翻译场景的性能优化 // 针对翻译场景的性能优化
// optimizeForTranslation() // optimizeForTranslation()
if isAutoDetectLanguage { if isAutoDetectLanguage {
speechConfig?.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode") speechConfig?.setPropertyTo("Continuous", by: SPXPropertyId.speechServiceConnectionLanguageIdMode)
// 不在这里设置语言,让SPXAutoDetectSourceLanguageConfiguration处理 // 不在这里设置语言,让SPXAutoDetectSourceLanguageConfiguration处理
os_log("启用语言检测模式,支持语言: %{public}@", log: log, type: .info, supportedLanguages.joined(separator: ",")) os_log("启用语言检测模式,支持语言: %{public}@", log: log, type: .info, supportedLanguages.joined(separator: ","))
} else { } else {
@ -131,7 +131,7 @@ public class AzureAsrHelper: NSObject {
*/ */
private func preInitializeAudioComponents() { private func preInitializeAudioComponents() {
if audioStream == nil { if audioStream == nil {
audioStream = AudioStream() audioStream = AudioStream(parentHelper: self)
audioStream?.initAudioRecord() audioStream?.initAudioRecord()
audioStream?.onAudioData = { [weak self] data in audioStream?.onAudioData = { [weak self] data in
self?.recordfile?.saveAudioDataToWav(data) self?.recordfile?.saveAudioDataToWav(data)
@ -354,7 +354,7 @@ public class AzureAsrHelper: NSObject {
do { do {
// 【优化】检查是否已经初始化,避免重复创建 // 【优化】检查是否已经初始化,避免重复创建
if audioStream == nil { if audioStream == nil {
audioStream = AudioStream() audioStream = AudioStream(parentHelper: self)
audioStream?.initAudioRecord() audioStream?.initAudioRecord()
audioStream?.onAudioData = { [weak self] data in audioStream?.onAudioData = { [weak self] data in
self?.recordfile?.saveAudioDataToWav(data) self?.recordfile?.saveAudioDataToWav(data)
@ -548,6 +548,7 @@ public class AzureAsrHelper: NSObject {
public func disableBluetoothAudio() { public func disableBluetoothAudio() {
do { do {
print("禁用蓝牙音频功能,切换回正常音频模式")
audioStream?.setAudioOutputRoute(.speaker) audioStream?.setAudioOutputRoute(.speaker)
} catch { } catch {
print("disableBluetoothAudio") print("disableBluetoothAudio")
@ -559,6 +560,7 @@ public class AzureAsrHelper: NSObject {
public func restoreOriginalAudioState() { public func restoreOriginalAudioState() {
do { do {
print("恢复原始音频设备状态(通常是重新启用蓝牙)")
audioStream?.setAudioOutputRoute(.receiver) audioStream?.setAudioOutputRoute(.receiver)
} catch { } catch {
print("restoreOriginalAudioState") print("restoreOriginalAudioState")
@ -632,9 +634,18 @@ public class AzureAsrHelper: NSObject {
private var isRunning = false private var isRunning = false
public var isWriting = false public var isWriting = false
private let bufferSize: Int = 4096 private let bufferSize: Int = 4096
private var writeThread: DispatchQueue? private var writeThread: DispatchQueue?
private var currentRoute: AudioOutputRoute? private var currentRoute: AudioOutputRoute?
public var onAudioData: ((Data) -> Void)? public var onAudioData: ((Data) -> Void)?
// 添加对外部类的弱引用
private weak var parentHelper: AzureAsrHelper?
// 添加初始化方法,接收外部类引用
init(parentHelper: AzureAsrHelper) {
self.parentHelper = parentHelper
super.init()
}
/** /**
* 初始化 * 初始化
*/ */
@ -654,7 +665,7 @@ public class AzureAsrHelper: NSObject {
// 创建新的写线程 // 创建新的写线程
writeThread = DispatchQueue(label: "audio.stream.writer") writeThread = DispatchQueue(label: "audio.stream.writer")
startWriteThread() startWriteThread()
self.setAudioOutputRoute(.receiver) //self.setAudioOutputRoute(.receiver)
} }
/// 获取最佳音频格式 (iOS 通常支持标准采样率) /// 获取最佳音频格式 (iOS 通常支持标准采样率)
@ -717,7 +728,8 @@ public class AzureAsrHelper: NSObject {
* @param data 音频数据字节数组 * @param data 音频数据字节数组
*/ */
public func saveAudioDataTo(data: Data) { public func saveAudioDataTo(data: Data) {
if audioSourceType != .external { // 通过父类引用调用方法
if audioSourceType != .external || !(parentHelper?.isContinuousRecognitionActive() ?? false) {
return return
} }
// print("外部data=\(data)") // print("外部data=\(data)")
@ -879,8 +891,7 @@ private func runMicrophoneCapture() {
case bluetooth case bluetooth
} }
public func setAudioOutputRoute(_ route: AudioOutputRoute) { public func setAudioOutputRoute(_ route: AudioOutputRoute) {
guard currentRoute != route else { return }
print("setAudioOutputRoute")
do { do {
// //
if self.isWriting { if self.isWriting {
@ -890,14 +901,15 @@ private func runMicrophoneCapture() {
try audioSession.setActive(false) try audioSession.setActive(false)
} }
print("setAudioOutputRoutecurrent,route=\(route)")
switch route { switch route {
case .speaker: case .speaker:
print("setAudioOutputRoutecurrent,speaker")
// 使用扬声器时必须用videoChat模式 // 使用扬声器时必须用videoChat模式
try audioSession.setCategory( try audioSession.setCategory(
.playAndRecord, .playAndRecord,
mode: .videoChat, mode: .videoChat,
options: [.allowBluetooth, .mixWithOthers, .defaultToSpeaker] options: [.defaultToSpeaker]
) )
try audioSession.overrideOutputAudioPort(.speaker) try audioSession.overrideOutputAudioPort(.speaker)
@ -906,7 +918,7 @@ private func runMicrophoneCapture() {
try audioSession.setCategory( try audioSession.setCategory(
.playAndRecord, .playAndRecord,
mode: .voiceChat, mode: .voiceChat,
options: [.allowBluetooth, .mixWithOthers, .defaultToSpeaker] options: [.allowBluetooth]
) )
try audioSession.overrideOutputAudioPort(.none) try audioSession.overrideOutputAudioPort(.none)

17
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift

@ -184,6 +184,15 @@ import os.log
callback: currentAsrCallback! callback: currentAsrCallback!
) )
result(success) result(success)
case "disableBluetoothAudio":
azureAsrHelper.disableBluetoothAudio();
return
case "restoreOriginalAudioState":
azureAsrHelper.restoreOriginalAudioState();
return
case "stopContinuousRecognition": case "stopContinuousRecognition":
let success = azureAsrHelper.stopContinuousRecognition() let success = azureAsrHelper.stopContinuousRecognition()
@ -224,7 +233,7 @@ import os.log
return return
} }
do { do {
print("音频文件名称为: \(filePath)") print("音频文件名称为: (filePath)")
try azureAsrHelper.renameFile(filePath: filePath, newName: newName) try azureAsrHelper.renameFile(filePath: filePath, newName: newName)
result(true) result(true)
} catch { } catch {
@ -239,7 +248,7 @@ import os.log
return return
} }
do { do {
print("音频文件名称为: \(sourcePath)") print("音频文件名称为: (sourcePath)")
try azureAsrHelper.moveFile(sourcePath: sourcePath, destPath: destPath) try azureAsrHelper.moveFile(sourcePath: sourcePath, destPath: destPath)
result(true) result(true)
} catch { } catch {
@ -253,7 +262,7 @@ import os.log
return return
} }
do { do {
print("音频文件名称为: \(filePath)") print("音频文件名称为: (filePath)")
try azureAsrHelper.enableRecord(filePath: filePath) try azureAsrHelper.enableRecord(filePath: filePath)
result(true) result(true)
} catch { } catch {
@ -350,7 +359,7 @@ import os.log
let success = azureTtsHelper.flushStream() let success = azureTtsHelper.flushStream()
result(success) result(success)
case "stop": case "stopSpeaking":
let success = azureTtsHelper.stop() let success = azureTtsHelper.stop()
result(success) result(success)

2
local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift

@ -166,7 +166,7 @@ public class AzureTtsHelper: NSObject, ITtsService {
let currentText = streamBuffer let currentText = streamBuffer
// 标点符号列表 (与Android一致) // 标点符号列表 (与Android一致)
let punctuationMarks: [Character] = [".", "。", "!", "!", "?", "?", ";", ";", ",", ",", ":", ":", "\n"] let punctuationMarks: [Character] = [".", "。", "!", "!","*", "?", "?", ";", ";", ",", ",", ":", ":", "\n"]
// 从后往前查找最后一个标点符号 // 从后往前查找最后一个标点符号
var lastPunctuationIndex = -1 var lastPunctuationIndex = -1

11
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

@ -87,6 +87,9 @@ object BleService {
// 是否是ota模式 // 是否是ota模式
var isEnterOta = false var isEnterOta = false
// 是否需要唤醒
var isWakeup = true
// 是否回复指令 // 是否回复指令
var isReply = true var isReply = true
@ -1515,7 +1518,13 @@ object BleService {
private fun notifyWakeupSignalReceived() { private fun notifyWakeupSignalReceived() {
for (callback in callbacks) { for (callback in callbacks) {
try { try {
callback.onWakeupSignalReceived() if (isWakeup) {
callback.onWakeupSignalReceived()
} else {
if (callback::class.java.simpleName == "BleServicePlugin") {
callback.onWakeupSignalReceived()
}
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "分发唤醒信号回调异常", e) Log.e(TAG, "分发唤醒信号回调异常", e)
} }

7
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt

@ -278,6 +278,13 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
val connected = BleService.isConnected() val connected = BleService.isConnected()
result.success(connected) result.success(connected)
} }
"setWakeup" -> {
val wakeup = call.argument<Boolean>("wakeup")
if (wakeup != null) {
BleService.isWakeup = wakeup
}
result.success("")
}
"registerBackgroundScan" -> { "registerBackgroundScan" -> {
bleCompanionManager.registerBackgroundScan() bleCompanionManager.registerBackgroundScan()
result.success(true) result.success(true)

11
local_plugins/ble_service/lib/ble_service.dart

@ -272,6 +272,17 @@ class BleService {
} }
} }
/// 设置是否是否需要唤醒
Future<void> setWakeup(bool wakeup) async {
try {
await _methodChannel.invokeMethod<String>('setWakeup', {
'wakeup': wakeup,
});
} catch (e) {
print('设置是否是否需要唤醒失败: $e');
}
}
/// 断开连接 /// 断开连接
Future<void> disconnect() async { Future<void> disconnect() async {
try { try {

46
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt

@ -9,6 +9,8 @@ import android.provider.Settings
import android.util.Log import android.util.Log
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.aallam.openai.api.chat.ChatMessage import com.aallam.openai.api.chat.ChatMessage
import com.aallam.openai.api.chat.TextContent
import com.aallam.openai.api.chat.ListContent
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.engine.okhttp.* import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.sse.* import io.ktor.client.plugins.sse.*
@ -169,16 +171,42 @@ fun roughTokenCount(text: String): Int {
/** /**
* 估算消息列表的总 token 数量(近似) * 估算消息列表的总 token 数量(近似)
* @param messages List of message map: role: user/assistant, content: string * @param messages List of message map: role: user/assistant, content: string or array
* @return 估算 token 数 * @return 估算 token 数
*/ */
fun estimatePromptTokens(messages: List<ChatMessage>): Int { fun estimatePromptTokens(messages: List<ChatMessage>): Int {
var tokens = 0 var totalText = ""
for (msg in messages) { for (message in messages) {
tokens += 4 // 每条消息结构开销 when (val content = message.messageContent) {
tokens += roughTokenCount(msg.role.role.lowercase()) // ChatRole 枚举转小写字符串 is TextContent? -> {
tokens += roughTokenCount(msg.content ?: "") totalText += content?.content + " "
}
is ListContent -> {
// 处理多模态内容数组
content.content.forEach { item ->
if (item is Map<*, *>) {
val type = item["type"] as? String
if (type == "text") {
val text = item["text"] as? String ?: ""
totalText += text + " "
}else if(type == "image_url"){
val text = item["image_url"] as? String ?: ""
totalText += text + " "
}
}
}
}
}
} }
tokens += 2 // 结尾 assistant 回复结构开销 return roughTokenCount(totalText)
return tokens }
} //fun estimatePromptTokens(messages: List<ChatMessage>): Int {
// var tokens = 0
// for (msg in messages) {
// tokens += 4 // 每条消息结构开销
// tokens += roughTokenCount(msg.role.role.lowercase()) // ChatRole 枚举转小写字符串
// tokens += roughTokenCount(msg.content ?: "")
// }
// tokens += 2 // 结尾 assistant 回复结构开销
// return tokens
//}

88
local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift

@ -68,18 +68,18 @@ public class ChatApiService: NSObject {
/// 设置流式回调 /// 设置流式回调
public func setStreamCallback(_ sessionId: String, _ callback: StreamCallback) { public func setStreamCallback(_ sessionId: String, _ callback: StreamCallback) {
print("[ChatApiService] 设置回调 for sessionId: \(sessionId)") // print("[ChatApiService] 设置回调 for sessionId: \(sessionId)")
print("[ChatApiService] 回调对象: \(callback)") // print("[ChatApiService] 回调对象: \(callback)")
self.sessionCallbacks[sessionId] = callback self.sessionCallbacks[sessionId] = callback
print("[ChatApiService] 回调设置完成,当前回调数量: \(sessionCallbacks.count)") print("[ChatApiService] 回调设置完成,当前回调数量: \(sessionCallbacks.count)")
} }
public func getSessionCallback(_ sessionId: String) -> StreamCallback? { public func getSessionCallback(_ sessionId: String) -> StreamCallback? {
let callback = self.sessionCallbacks[sessionId] let callback = self.sessionCallbacks[sessionId]
print("[ChatApiService] getSessionCallback for sessionId: \(sessionId)") // print("[ChatApiService] getSessionCallback for sessionId: \(sessionId)")
print("[ChatApiService] sessionCallbacks count: \(sessionCallbacks.count)") // print("[ChatApiService] sessionCallbacks count: \(sessionCallbacks.count)")
print("[ChatApiService] sessionCallbacks keys: \(Array(sessionCallbacks.keys))") // print("[ChatApiService] sessionCallbacks keys: \(Array(sessionCallbacks.keys))")
print("[ChatApiService] callback exists: \(callback != nil)") // print("[ChatApiService] callback exists: \(callback != nil)")
return callback return callback
} }
//移除回调函数 //移除回调函数
@ -311,7 +311,7 @@ public func sendMessageStream(_ sessionId: String, messages: [[String: Any]]) {
tools: chatTools tools: chatTools
) )
print("[ChatApiService] 发起AI问答 问题:\(messages[0]) 工具数量: \(chatTools.count)") print("[ChatApiService] 发起AI问答 问题:\(messages) 工具数量: \(chatTools.count)")
} else { } else {
query = ChatQuery( query = ChatQuery(
messages: chatMessages, messages: chatMessages,
@ -335,14 +335,14 @@ public func sendMessageStream(_ sessionId: String, messages: [[String: Any]]) {
// 处理文本内容 // 处理文本内容
if let content = delta.content { if let content = delta.content {
replyContext += content replyContext += content
print("[ChatApiService] 收到内容: \(content)") // print("[ChatApiService] 收到内容: \(content)")
print("[ChatApiService] 当前sessionId: \(sessionId)") // print("[ChatApiService] 当前sessionId: \(sessionId)")
let callback = self.getSessionCallback(sessionId) let callback = self.getSessionCallback(sessionId)
if let callback = callback { if let callback = callback {
print("[ChatApiService] 找到回调,准备调用 onToken") // print("[ChatApiService] 找到回调,准备调用 onToken")
callback.onToken(sessionId, content) callback.onToken(sessionId, content)
print("[ChatApiService] onToken 调用完成") // print("[ChatApiService] onToken 调用完成")
} else { } else {
print("[ChatApiService] 警告:未找到 sessionId \(sessionId) 的回调") print("[ChatApiService] 警告:未找到 sessionId \(sessionId) 的回调")
} }
@ -761,7 +761,44 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] {
} }
return false return false
} }
// 解析并构建正确的图片消息
func parseImageData(_ data: [String: Any]) -> ChatQuery.ChatCompletionMessageParam? {
// 1. 校验基础字段
guard let type = data["type"] as? String, type == "image_url",
let imageUrlDict = data["image_url"] as? [String: Any],
let imageUrlString = imageUrlDict["url"] as? String else {
print("数据结构错误:缺少type或image_url.url")
return nil
}
// 2. 补充默认的detail(原结构体必须,默认auto)
let detailString = imageUrlDict["detail"] as? String ?? "auto"
let imageDetail: ChatQuery.ChatCompletionMessageParam.UserMessageParam.Content.VisionContent.ChatCompletionContentPartImageParam.ImageURL.Detail
switch detailString.lowercased() {
case "low":
imageDetail = .low
case "high":
imageDetail = .high
default:
imageDetail = .auto
}
// 3. 逐层构建结构体(按原结构体要求映射)
let imageUrlModel = ChatQuery.ChatCompletionMessageParam.UserMessageParam.Content.VisionContent.ChatCompletionContentPartImageParam.ImageURL(
url: imageUrlString,
detail: imageDetail
)
let imageContentPart = ChatQuery.ChatCompletionMessageParam.UserMessageParam.Content.VisionContent.ChatCompletionContentPartImageParam(
imageUrl: imageUrlModel
)
let visionContent = ChatQuery.ChatCompletionMessageParam.UserMessageParam.Content.VisionContent.chatCompletionContentPartImageParam(imageContentPart)
let userContent = ChatQuery.ChatCompletionMessageParam.UserMessageParam.Content.vision([visionContent])
let userMessageParam = ChatQuery.ChatCompletionMessageParam.UserMessageParam(content: userContent)
return .user(userMessageParam)
}
/// 转换为OpenAI Message格式 /// 转换为OpenAI Message格式
private func convertToChatMessages(_ messages: [[String: Any]]) throws -> [ChatQuery.ChatCompletionMessageParam] { private func convertToChatMessages(_ messages: [[String: Any]]) throws -> [ChatQuery.ChatCompletionMessageParam] {
var chatMessages: [ChatQuery.ChatCompletionMessageParam] = [] var chatMessages: [ChatQuery.ChatCompletionMessageParam] = []
@ -851,17 +888,24 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] {
var textContent = "" var textContent = ""
for item in contentArray { for item in contentArray {
if let type = item["type"] as? String, type == "text", if let type = item["type"] as? String {
let text = item["text"] as? String { switch type {
textContent += text case "text":
if let text = item["text"] as? String {
if let chatMessage = ChatQuery.ChatCompletionMessageParam(role: messageRole, content: text) {
chatMessages.append(chatMessage)
}
}
case "image_url":
if let imageMessage = parseImageData(item) {
chatMessages.append(imageMessage)
print("图片消息解析成功")
}
default:
break
}
} }
} }
let finalContent = textContent.isEmpty ? "多媒体内容" : textContent
if let chatMessage = ChatQuery.ChatCompletionMessageParam(role: messageRole, content: finalContent) {
chatMessages.append(chatMessage)
}
} }
} }
@ -932,7 +976,7 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] {
// 创建工具参数 // 创建工具参数
let toolParam = ChatQuery.ChatCompletionToolParam(function: functionDefinition) let toolParam = ChatQuery.ChatCompletionToolParam(function: functionDefinition)
print("解析工具 \(name):\(schema)") // print("解析工具 \(name):\(schema)")
chatTools.append(toolParam) chatTools.append(toolParam)
} }

12
local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift

@ -479,7 +479,7 @@ public class MCPClient {
// 添加连接成功后的工具列表汇总输出 // 添加连接成功后的工具列表汇总输出
if connectedCount > 0 { if connectedCount > 0 {
print("[MCPClient] 🎉 MCP 连接完成,共连接 \(connectedCount) 个服务器") print("[MCPClient] 🎉 MCP 连接完成,共连接 \(connectedCount) 个服务器")
print("[MCPClient] 📋 所有可用工具汇总:") // print("[MCPClient] 📋 所有可用工具汇总:")
let allTools = getToolMaps() let allTools = getToolMaps()
for (index, toolMap) in allTools.enumerated() { for (index, toolMap) in allTools.enumerated() {
@ -490,9 +490,9 @@ public class MCPClient {
} }
} }
print("[MCPClient] 📊 工具总数: \(allTools.count)") // print("[MCPClient] 📊 工具总数: \(allTools.count)")
print("[MCPClient] 🔧 本地函数: \(localFunctionDefs.count) 个") // print("[MCPClient] 🔧 本地函数: \(localFunctionDefs.count) 个")
print("[MCPClient] 🌐 远程工具: \(allTools.count - localFunctionDefs.count) 个") // print("[MCPClient] 🌐 远程工具: \(allTools.count - localFunctionDefs.count) 个")
} }
return connectedCount > 0 return connectedCount > 0
@ -508,7 +508,7 @@ public class MCPClient {
parameters: Any, parameters: Any,
handler: FunctionHandler handler: FunctionHandler
) -> Bool { ) -> Bool {
print("[MCPClient] 注册本地函数: \(name)") // print("[MCPClient] 注册本地函数: \(name)")
localFunctions[name] = handler localFunctions[name] = handler
let functionDef: [String: Any] = [ let functionDef: [String: Any] = [
@ -544,7 +544,7 @@ public class MCPClient {
for client in subClients.values { for client in subClients.values {
let tools = client.getToolMaps() let tools = client.getToolMaps()
print("[MCPClient] 读取工具列表: \(tools)") // print("[MCPClient] 读取工具列表: \(tools)")
allToolMaps.append(contentsOf: tools) allToolMaps.append(contentsOf: tools)
} }

25
local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift

@ -419,7 +419,7 @@ public class SystemFunctionHandler: NSObject {
"required": ["key", "value"] "required": ["key", "value"]
] ]
let handler = SetUserProfileFieldHandler() let handler: SetUserProfileFieldHandler = SetUserProfileFieldHandler()
_ = client.registerLocalFunction( _ = client.registerLocalFunction(
name: "set_user_profile_field", name: "set_user_profile_field",
description: "设置或更新用户画像中的某一个字段", description: "设置或更新用户画像中的某一个字段",
@ -462,8 +462,7 @@ private class GetCurrentLocationHandler: FunctionHandler {
guard let latStr = locationInfo["user_lat"] as? String, guard let latStr = locationInfo["user_lat"] as? String,
let lngStr = locationInfo["user_lng"] as? String, let lngStr = locationInfo["user_lng"] as? String,
let latitude = Double(latStr), let latitude = Double(latStr),
let longitude = Double(lngStr), let longitude = Double(lngStr) else {
let address = locationInfo["user_location"] as? String else {
print("[GetCurrentLocationHandler] 位置数据格式错误") print("[GetCurrentLocationHandler] 位置数据格式错误")
continuation.resume(returning: self.createErrorResponse(message: "位置数据格式错误,请重试")) continuation.resume(returning: self.createErrorResponse(message: "位置数据格式错误,请重试"))
return return
@ -471,8 +470,8 @@ private class GetCurrentLocationHandler: FunctionHandler {
print("[GetCurrentLocationHandler] 位置获取成功: lat=\(latitude), lng=\(longitude)") print("[GetCurrentLocationHandler] 位置获取成功: lat=\(latitude), lng=\(longitude)")
let message = "当前位置:\(address)(纬度 \(latitude),经度 \(longitude))" // let message = "当前位置:\(address)(纬度 \(latitude),经度 \(longitude))"
let successResponse = self.createSuccessResponse(message: message, latitude: latitude, longitude: longitude, address: address) let successResponse = self.createSuccessResponse(latitude: latitude, longitude: longitude)
print("[GetCurrentLocationHandler] 返回位置结果") print("[GetCurrentLocationHandler] 返回位置结果")
continuation.resume(returning: successResponse) continuation.resume(returning: successResponse)
@ -486,14 +485,12 @@ private class GetCurrentLocationHandler: FunctionHandler {
} }
} }
private func createSuccessResponse(message: String, latitude: Double, longitude: Double, address: String) -> String { private func createSuccessResponse(latitude: Double, longitude: Double) -> String {
let result: [String: Any] = [ let result: [String: Any] = [
"success": true, "success": true,
"message": message,
"data": [ "data": [
"latitude": latitude, "latitude": latitude,
"longitude": longitude, "longitude": longitude,
"address": address,
"timestamp": Date().timeIntervalSince1970 * 1000 "timestamp": Date().timeIntervalSince1970 * 1000
] ]
] ]
@ -847,21 +844,19 @@ private class OpenRecorderHandler: FunctionHandler {
private class SetUserProfileFieldHandler: FunctionHandler { private class SetUserProfileFieldHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String { func handle(arguments: [String: Any]) async throws -> String {
guard let key = arguments["key"] as? String, guard let key = arguments["key"] as? String,
let value = arguments["value"] as? String else { let value = arguments["value"] as? String else {
throw ChatApiException("缺少必要参数:key 和 value") return "设置成功"
} }
// 验证参数 // 验证参数
if key.isEmpty { if key.isEmpty {
throw ChatApiException("画像字段名不能为空") return "设置成功"
} }
if value.isEmpty { if value.isEmpty {
throw ChatApiException("字段值不能为空") return "设置成功"
} }
let result = await setUserProfileField(key: key, value: value) let result = await setUserProfileField(key: key, value: value)
return "{\"result\": \"\(result.message)\", \"key\": \"\(key)\", \"value\": \"\(value)\"}\"}" return "设置成功"
} }
private func setUserProfileField(key: String, value: String) async -> Result { private func setUserProfileField(key: String, value: String) async -> Result {

6
local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift

@ -186,7 +186,7 @@ public class ChatStorageHelper {
"totalCount": totalCount, "totalCount": totalCount,
"totalPages": totalPages "totalPages": totalPages
] ]
os_log("getMessages: 读取历史记录: %{public}@", log: logger, type: .error, messagesArray)
do { do {
let jsonData = try JSONSerialization.data(withJSONObject: result, options: []) let jsonData = try JSONSerialization.data(withJSONObject: result, options: [])
if let jsonString = String(data: jsonData, encoding: .utf8) { if let jsonString = String(data: jsonData, encoding: .utf8) {
@ -199,7 +199,7 @@ public class ChatStorageHelper {
let errmsg = String(cString: sqlite3_errmsg(db)!) let errmsg = String(cString: sqlite3_errmsg(db)!)
os_log("getMessages: SQL准备失败: %{public}@", log: logger, type: .error, errmsg) os_log("getMessages: SQL准备失败: %{public}@", log: logger, type: .error, errmsg)
} }
// 返回空结果,但保持格式一致 // 返回空结果,但保持格式一致
let emptyResult: [String: Any] = [ let emptyResult: [String: Any] = [
"messages": [], "messages": [],
@ -370,4 +370,4 @@ public class ChatStorageHelper {
return messages return messages
} }
} }

6
local_plugins/location_service/ios/location_service/Sources/location_service/LocationData.swift

@ -4,13 +4,13 @@ import Foundation
public struct LocationData: Codable { public struct LocationData: Codable {
public let latitude: Double public let latitude: Double
public let longitude: Double public let longitude: Double
public let address: String
public let timestamp: TimeInterval public let timestamp: TimeInterval
public let latitudeandlongitude: String
public init(latitude: Double, longitude: Double, address: String, timestamp: TimeInterval) { public init(latitude: Double, longitude: Double,timestamp: TimeInterval) {
self.latitude = latitude self.latitude = latitude
self.longitude = longitude self.longitude = longitude
self.address = address
self.timestamp = timestamp self.timestamp = timestamp
self.latitudeandlongitude = "\(latitude),\(longitude)"
} }
} }

38
local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift

@ -27,7 +27,6 @@ public class LocationService: NSObject {
super.init() super.init()
setupLocationManager() setupLocationManager()
} }
private func setupLocationManager() { private func setupLocationManager() {
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
self?.locationManager = CLLocationManager() self?.locationManager = CLLocationManager()
@ -35,15 +34,26 @@ public class LocationService: NSObject {
self?.locationManager?.desiredAccuracy = kCLLocationAccuracyBest self?.locationManager?.desiredAccuracy = kCLLocationAccuracyBest
self?.locationManager?.distanceFilter = 10 self?.locationManager?.distanceFilter = 10
// 初始化时获取当前状态 // 不需要手动获取状态,delegate 方法会自动被调用
self?.authorizationStatus = CLLocationManager.authorizationStatus() print("[LocationService] LocationManager 初始化完成,等待权限状态回调")
self?.isLocationServicesEnabled = CLLocationManager.locationServicesEnabled()
print("[LocationService] LocationManager 初始化完成")
print("[LocationService] 初始权限状态: \(self?.authorizationStatus.rawValue ?? -1)")
print("[LocationService] 位置服务启用: \(self?.isLocationServicesEnabled ?? false)")
} }
} }
// private func setupLocationManager() {
// DispatchQueue.main.async { [weak self] in
// self?.locationManager = CLLocationManager()
// self?.locationManager?.delegate = self
// self?.locationManager?.desiredAccuracy = kCLLocationAccuracyBest
// self?.locationManager?.distanceFilter = 10
// // 初始化时获取当前状态
// self?.authorizationStatus = CLLocationManager.authorizationStatus()
// self?.isLocationServicesEnabled = CLLocationManager.locationServicesEnabled()
// print("[LocationService] LocationManager 初始化完成")
// print("[LocationService] 初始权限状态: \(self?.authorizationStatus.rawValue ?? -1)")
// print("[LocationService] 位置服务启用: \(self?.isLocationServicesEnabled ?? false)")
// }
// }
/// 获取当前位置 - 主要接口 /// 获取当前位置 - 主要接口
public func getCurrentLocation(completion: @escaping (Result<[String: Any], Error>) -> Void) { public func getCurrentLocation(completion: @escaping (Result<[String: Any], Error>) -> Void) {
@ -190,11 +200,11 @@ public class LocationService: NSObject {
} }
// 使用缓存的状态信息 // 使用缓存的状态信息
print("[LocationService] 开始位置请求") // print("[LocationService] 开始位置请求")
print("[LocationService] 缓存的权限状态: \(self.authorizationStatus.rawValue)") // print("[LocationService] 缓存的权限状态: \(self.authorizationStatus.rawValue)")
print("[LocationService] 缓存的位置服务状态: \(self.isLocationServicesEnabled)") // print("[LocationService] 缓存的位置服务状态: \(self.isLocationServicesEnabled)")
print("[LocationService] 当前线程: \(Thread.current)") // print("[LocationService] 当前线程: \(Thread.current)")
print("[LocationService] 是否主线程: \(Thread.isMainThread)") // print("[LocationService] 是否主线程: \(Thread.isMainThread)")
// 验证权限状态 // 验证权限状态
if !self.checkLocationPermission() { if !self.checkLocationPermission() {
@ -238,7 +248,6 @@ public class LocationService: NSObject {
let locationData = LocationData( let locationData = LocationData(
latitude: latitude, latitude: latitude,
longitude: longitude, longitude: longitude,
address: "",
timestamp: Date().timeIntervalSince1970 * 1000 timestamp: Date().timeIntervalSince1970 * 1000
) )
completion(.success(locationData)) completion(.success(locationData))
@ -273,7 +282,6 @@ public class LocationService: NSObject {
return [ return [
"user_lat": String(locationData.latitude), "user_lat": String(locationData.latitude),
"user_lng": String(locationData.longitude), "user_lng": String(locationData.longitude),
"user_location": locationData.address,
"timestamp": locationData.timestamp "timestamp": locationData.timestamp
] ]
} }

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

@ -768,7 +768,7 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
// 保存函数定义 // 保存函数定义
localFunctionDefs[name] = ObjectMapper().writeValueAsString(functionDef) localFunctionDefs[name] = ObjectMapper().writeValueAsString(functionDef)
Log.d(TAG, "注册本地函数成功: $name") // Log.d(TAG, "注册本地函数成功: $name")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "注册本地函数失败: ${e.message}", e) Log.e(TAG, "注册本地函数失败: ${e.message}", e)

2
local_plugins/open_ai_service/ios/Classes/MCPClient.swift

@ -312,7 +312,7 @@ public class MCPClient {
let jsonString = String(data: jsonData, encoding: .utf8) ?? "" let jsonString = String(data: jsonData, encoding: .utf8) ?? ""
localFunctionDefs[name] = jsonString localFunctionDefs[name] = jsonString
print("\(tag) 成功注册本地函数: \(name)") // print("\(tag) 成功注册本地函数: \(name)")
return true return true
} catch { } catch {
print("\(tag) 注册本地函数失败: \(error.localizedDescription)") print("\(tag) 注册本地函数失败: \(error.localizedDescription)")

2
local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/MCPClient.swift

@ -312,7 +312,7 @@ public class MCPClient {
let jsonString = String(data: jsonData, encoding: .utf8) ?? "" let jsonString = String(data: jsonData, encoding: .utf8) ?? ""
localFunctionDefs[name] = jsonString localFunctionDefs[name] = jsonString
print("\(tag) 成功注册本地函数: \(name)") // print("\(tag) 成功注册本地函数: \(name)")
return true return true
} catch { } catch {
print("\(tag) 注册本地函数失败: \(error.localizedDescription)") print("\(tag) 注册本地函数失败: \(error.localizedDescription)")

2
pubspec.yaml

@ -1,7 +1,7 @@
name: voitrans name: voitrans
description: "Voitrans - AI Voice Assistant." description: "Voitrans - AI Voice Assistant."
publish_to: "none" publish_to: "none"
version: 1.0.4+20 version: 1.0.6+20
environment: environment:
sdk: ">=3.3.0 <4.0.0" sdk: ">=3.3.0 <4.0.0"

Loading…
Cancel
Save