Browse Source

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

weicu
liwei1dao 1 year ago
parent
commit
1a8de6177b
  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',
'deleteAccountTips':
'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 错误信息
'networkConnectionFailed':

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

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

96
lib/core/widgets/intro_overlay_builder.dart

@ -1,8 +1,46 @@
import 'package:flutter/material.dart';
import 'package:flutter_intro/flutter_intro.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:lottie/lottie.dart';
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(
StepWidgetParams params,
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 序列化时返回数字值
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 {
final bool isUser; //标识信息是否来自用户还是AI
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 'package:agent_service/agent_service.dart';
import 'bluetooth_manager.dart';
import 'language_manager.dart';
// 重新导出BLE连接状态常量,让外部可以通过BleManager访问
export 'package:ble_service/ble_service.dart' show BleConnectionState;
@ -556,26 +557,44 @@ class BleManager extends GetxService {
// 根据APP语言动态配置语音识别语言
List<String> supportedLanguages = [];
try {
// 使用 GetX 获取 APP 当前语言环境 (Locale)
final appLocale = Get.locale;
if (appLocale != null) {
final lang = appLocale.languageCode; // e.g., 'en'
final country = appLocale.countryCode; // e.g., 'US'
// 构造 Azure SDK 需要的格式 (e.g., 'en-US')
if (country != null && country.isNotEmpty) {
supportedLanguages.add('$lang-$country');
} else {
supportedLanguages.add(lang); // fallback only language code
final storage = GetStorage();
final languageManager = Get.find<LanguageManager>();
// 从存储中读取用户设置的语言
String? savedLanguage = storage.read('language');
if (savedLanguage != null && savedLanguage != 'system') {
// 用户设置了特定语言
final parts = savedLanguage.split('_');
if (parts.length == 2) {
final asrCode = languageManager.getAsrCodeByShortCode(parts[0]);
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) {
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:get/get.dart';
import 'package:get_storage/get_storage.dart';
@ -5,9 +8,14 @@ import 'package:get_storage/get_storage.dart';
class NoviceGuideService extends GetxService {
final GetStorage _storage = GetStorage();
final BleService _bleService = BleService();
StreamSubscription<dynamic>? _statusSubscription;
bool isHomeGuide = true;
bool isAIGuide = true;
RxBool isWakeup = false.obs;
int aiStep = 0;
List aiStepsList = [
@ -56,6 +64,12 @@ class NoviceGuideService extends GetxService {
// isAIGuide = _storage.read('ai_guide') ?? false;
}
@override
void onClose() {
closeWakeupListen();
super.onClose();
}
void startHomeGuide() async {
if (!isHomeGuide) {
await Future.delayed(const Duration(milliseconds: 200));
@ -73,4 +87,18 @@ class NoviceGuideService extends GetxService {
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:logger/logger.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../models/channel_app_model.dart';
import 'network/api.dart';
import 'dart:math' as math;
@ -13,15 +14,62 @@ import 'dart:math' as math;
class VersionUpdateService extends GetxService {
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 {
try {
final packageInfo = await PackageInfo.fromPlatform();
_appVersion = packageInfo.version;
_buildNumber = packageInfo.buildNumber;
return '${packageInfo.version}+${packageInfo.buildNumber}';
} catch (e) {
_logger.e('获取当前版本信息失败: $e');
@ -31,14 +79,6 @@ class VersionUpdateService extends GetxService {
/// 获取当前平台信息
Future<PlatformInfo> getCurrentPlatformInfo() async {
if (kIsWeb) {
return const PlatformInfo(
platform: 'web',
storeType: AppStoreType.official,
channel: UserChannel.OFFICIAL,
);
}
if (Platform.isIOS) {
return const PlatformInfo(
platform: 'ios',
@ -66,10 +106,14 @@ class VersionUpdateService extends GetxService {
final androidInfo = await deviceInfo.androidInfo;
final brand = androidInfo.brand.toLowerCase();
return const PlatformInfo(
_deviceBrand = brand;
String channelFromEnv = dotenv.env['Channel'] ?? 'OFFICIAL';
_channel = UserChannel.fromName(channelFromEnv);
return PlatformInfo(
platform: 'android',
storeType: AppStoreType.official,
channel: UserChannel.OFFICIAL,
channel: _channel ?? UserChannel.OFFICIAL,
);
// switch (brand) {
@ -121,10 +165,27 @@ class VersionUpdateService extends GetxService {
/// 检查版本更新
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 {
final currentVersion = await getCurrentVersion();
final platformInfo = await getCurrentPlatformInfo();
_platform = platformInfo.platform;
_channel = platformInfo.channel;
_lastCheckTime = DateTime.now();
// 调用后端接口获取最新版本信息
final response =
await Api.getChannelApp({'channel': platformInfo.channel.value});
@ -144,9 +205,11 @@ class VersionUpdateService extends GetxService {
// 直接从response中获取app信息
final appInfo = AppInfo.fromJson(response['app']);
_logger.d('获取到的最新版本信息: ${appInfo.toJson()}');
_latestVersion = appInfo.version;
_updateUrl = appInfo.address;
// 设置游客登录显示状态
isShowTourists.value = appInfo.tourists;
_isShowTourists = appInfo.tourists;
_logger.d('从服务器获取游客登录配置: ${appInfo.tourists}');
// if (channelAppResponse.code != 0 || channelAppResponse.data == null) {
@ -160,18 +223,17 @@ class VersionUpdateService extends GetxService {
// }
// final appInfo = channelAppResponse.data!.app;
final hasUpdate = _compareVersions(currentVersion, appInfo.version);
_hasUpdate = _compareVersions(currentVersion, appInfo.version);
// 判断更新类型:如果主版本号不同则强制更新
UpdateType updateType = UpdateType.none;
if (hasUpdate) {
final isForceUpdate =
_shouldForceUpdate(currentVersion, appInfo.version);
updateType = isForceUpdate ? UpdateType.force : UpdateType.optional;
if (_hasUpdate!) {
_isForceUpdate = _shouldForceUpdate(currentVersion, appInfo.version);
updateType = _isForceUpdate! ? UpdateType.force : UpdateType.optional;
}
return VersionUpdateInfo(
hasUpdate: hasUpdate,
hasUpdate: _hasUpdate!,
currentVersion: currentVersion.split('+')[0],
latestVersion: appInfo.version.split('+')[0],
appInfo: appInfo,
@ -180,7 +242,7 @@ class VersionUpdateService extends GetxService {
} catch (e) {
_logger.e('检查版本更新失败: $e');
// 发生错误时,保持默认值 false
isShowTourists.value = false;
_isShowTourists = false;
final currentVersion = await getCurrentVersion();
return VersionUpdateInfo(
hasUpdate: false,
@ -270,84 +332,16 @@ class VersionUpdateService extends GetxService {
return false;
}
//final platformInfo = await getCurrentPlatformInfo();
try {
// switch (platformInfo.storeType) {
// 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!);
// }
return await _openDownloadUrl(updateInfo.appInfo!.address!);
} catch (e) {
_logger.e('执行更新失败: $e');
return false;
}
}
/// 打开iOS App Store
Future<bool> _openAppStore() 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 {
/// 打开下载链接
Future<bool> _openDownloadUrl(String downloadUrl) async {
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 'routes/app_pages.dart';
import 'routes/app_routes.dart';
import 'dart:io';
import 'package:window_size/window_size.dart';
import 'core/translations/app_translations.dart';
import 'core/themes/app_theme.dart';
import 'core/themes/theme_service.dart';
@ -32,28 +30,6 @@ void main() async {
// 初始化登录服务(包含Firebase初始化)
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());
}

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

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

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(
params,
'欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言',
onNext: () {},
onNext: () {
controller.noviceGuideService.startWakeupListen();
},
);
},
builder: (BuildContext context, GlobalKey key) => SizedBox(
@ -114,11 +116,26 @@ class HomeView extends GetView<HomeController> {
IntroStepBuilder(
group: 'home',
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) {
return IntroOverlayBuilder.aiBuilder(
params,
'对着麦克风说“小言小言”',
onNext: () {},
return Obx(
() => IntroOverlayBuilder.homeBuilder(
params,
controller.noviceGuideService.isWakeup.value,
onNext: () {
controller.noviceGuideService.closeWakeupListen();
},
),
);
},
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(
children: [
// 状态标题行
@ -277,7 +294,7 @@ class HomeView extends GetView<HomeController> {
if (!controller.isFullyConnectedRx.value) {
return _buildConnectButton();
} else {
return const SizedBox();
return _buildDeviceButton(isDarkMode);
}
}),
],
@ -400,12 +417,42 @@ class HomeView extends GetView<HomeController> {
),
SizedBox(height: 12.h),
_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) {
return Obx(() {
@ -505,6 +552,8 @@ class HomeView extends GetView<HomeController> {
batteryColor = Colors.red[600]!;
}
bool isBatteryZero = batteryLevel == 0;
return Expanded(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 6.h),
@ -523,32 +572,45 @@ class HomeView extends GetView<HomeController> {
),
SizedBox(height: 4.h),
// 电池指示器
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
batteryIcon,
size: 12.sp,
color: batteryColor,
),
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],
// 电池指示器 - 当电量为0时,显示横线
isBatteryZero
? Container(
width: 20.w, // 横线宽度
height: 12.sp, // 设置与图标相同的高度,确保对齐
alignment: Alignment.center, // 居中对齐
child: Container(
width: 10.w,
height: 1.5.h, // 实际横线高度
color: isDarkMode
? Colors.grey[600]
: Colors.grey[400], // 横线颜色
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
batteryIcon,
size: 12.sp,
color: batteryColor,
),
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:io';
import '../../../data/models/appconfig.dart';
import '../../../data/models/appconfig_model.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.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 'facebookLogin.dart';
import 'googleLogin.dart';
import 'weixinLogin.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/models/user_Info.dart';
import 'package:get_storage/get_storage.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'dart:io';
import 'package:package_info_plus/package_info_plus.dart';
import '../../../data/models/channel_app_model.dart';
import '../../../data/services/version_update_service.dart';
import '../../../modules/common/widgets/privacy_policy_dialog.dart';
@ -27,20 +27,20 @@ enum LoginViewState {
}
// 用户渠道枚举
enum UserChannel {
UNKNOWN(0), // 未知渠道
CHINA_HUAWEI(1), // 中国-华为
CHINA_XIAOMI(2), // 中国-小米
CHINA_OPPO(3), // 中国-OPPO
CHINA_VIVO(4), // 中国-VIVO
CHINA_HONOR(5), // 中国-荣耀
OFFICIAL(100), // 官方渠道
GLOBAL_APPLE(101), // 全球-苹果
GLOBAL_GOOGLE(102); // 全球-谷歌
const UserChannel(this.value);
final int value;
}
// enum UserChannel {
// UNKNOWN(0), // 未知渠道
// CHINA_HUAWEI(1), // 中国-华为
// CHINA_XIAOMI(2), // 中国-小米
// CHINA_OPPO(3), // 中国-OPPO
// CHINA_VIVO(4), // 中国-VIVO
// CHINA_HONOR(5), // 中国-荣耀
// OFFICIAL(100), // 官方渠道
// GLOBAL_APPLE(101), // 全球-苹果
// GLOBAL_GOOGLE(102); // 全球-谷歌
// const UserChannel(this.value);
// final int value;
// }
class LoginController extends GetxController {
static LoginController get to => Get.find();
@ -212,6 +212,7 @@ class LoginController extends GetxController {
String get contactValue => _contactValue.value;
String get displayContact => _displayContact.value;
List<String> get verificationCode => _verificationCode;
final versionService = VersionUpdateService.to;
// 文本控制器
TextEditingController contactController = TextEditingController();
@ -246,20 +247,9 @@ class LoginController extends GetxController {
// 检查游客登录配置
Future<void> _checkGuestLoginConfig() async {
try {
final versionService = VersionUpdateService.to;
// 先检查版本更新,这会同时设置 isShowTourists 的值
await versionService.checkForUpdate();
_isGuestLoginVisible.value = versionService.cachedShowTourists;
// 直接使用 VersionUpdateService 中的 isShowTourists 值
_isGuestLoginVisible.value = versionService.isShowTourists.value;
Logger().d('游客登录显示状态: ${_isGuestLoginVisible.value}');
// 监听 isShowTourists 的变化
versionService.isShowTourists.listen((value) {
_isGuestLoginVisible.value = value;
Logger().d('游客登录显示状态更新: $value');
});
} catch (e) {
Logger().e('检查游客登录配置失败: $e');
// 默认不显示游客登录
@ -301,9 +291,7 @@ class LoginController extends GetxController {
// 新增:初始化APP信息(版本号、渠道、国家代码)
Future<void> _initAppInfo() async {
try {
// 获取APP版本号
final packageInfo = await PackageInfo.fromPlatform();
_appVersion = '${packageInfo.version}+${packageInfo.buildNumber}';
_appVersion = versionService.cachedAppVersion;
Logger().d('appVersion: $_appVersion');
// 获取国家代码
@ -311,7 +299,7 @@ class LoginController extends GetxController {
Logger().d('countryCode: $_countryCode');
// 获取用户渠道(根据平台和设备信息确定)
_userChannel = UserChannel.OFFICIAL;
_userChannel = versionService.cachedChannel;
Logger().d('userChannel: ${_userChannel.value}');
} catch (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) {
case 0:
fileName.value = "现场录音";
_audioSourceType = true;
_audioSourceType = false;
break;
case 1:
fileName.value = "音、视频录音";
@ -271,7 +271,7 @@ class MeetingRecordController extends GetxController
void _startHardwareServices() {
switch (audioType.value) {
case 0:
_bleManager.openEncoder();
// _bleManager.openEncoder();
break;
case 1:
_bleManager.openDecoder();

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

@ -95,7 +95,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
unselectedLabelColor: Colors.grey,
tabs: const [
Tab(text: '音频'),
Tab(text: '文本'),
// Tab(text: '文本'),
],
),
Expanded(
@ -103,7 +103,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
controller: controller.tabController,
children: [
_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 '../../../core/utils/logger.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 {
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'] ??
currentSong['sgener'] ??
currentSong['artist'] ??
@ -192,7 +194,8 @@ class MusicPlaylistView extends StatelessWidget {
final id = item["id"] ?? 0;
final name = item["name"] ?? 'unknownSong'.tr; // 未知歌曲
final url = item["url"] ?? "";
final singer = item["singer"] ?? item["sgener"] ?? 'unknownArtist'.tr; // 未知歌手
final singer =
item["singer"] ?? item["sgener"] ?? 'unknownArtist'.tr; // 未知歌手
final image = item["image"] ?? "";
return _buildMusicItem(
@ -628,8 +631,8 @@ class MusicPlaylistView extends StatelessWidget {
}
// 播放选中的歌曲
Future<void> _playSelectedSong(AgentController controller, Map<String, dynamic> song,
List<Map<String, dynamic>> playlist) async {
Future<void> _playSelectedSong(AgentController controller,
Map<String, dynamic> song, List<Map<String, dynamic>> playlist) async {
// 防止重复点击导致的状态混乱
if (controller.isLoadingMusic.value) {
return;

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

@ -1,4 +1,5 @@
import 'package:classic_bluetooth/classic_bluetooth.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:voitrans/data/models/appconfig_model.dart';
import '/routes/app_routes.dart';
import 'package:logger/web.dart';
@ -214,10 +215,9 @@ class OtaUpgradeController extends GetxController {
updatepackageaddress = product!.updatepackageaddress!;
// 版本检查
bool isUpdate = _isNewVersionAvailable(product.version!);
bool hasUpdate = _isNewVersionAvailable(product.version!);
isUpdateAvailable.value = isUpdate;
isUpdateAvailable.value = hasUpdate;
} catch (e) {
// 统一错误处理
errorMessage.value = e.toString();
@ -238,22 +238,36 @@ class OtaUpgradeController extends GetxController {
}
}
// 版本比较方法优化
bool _isNewVersionAvailable(String latest) {
// 版本比较方法优化 - 支持升级和降级
bool _isNewVersionAvailable(String latestVersion) {
try {
List<int> currentParts = currentVersion.value
.split('.')
.map((e) => int.parse(e.trim()))
.toList();
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++) {
int current = (i < currentParts.length) ? currentParts[i] : 0;
if (latestParts[i] > current) return true;
if (latestParts[i] < current) return false;
while (currentParts.length < maxLength) {
currentParts.add(0);
}
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) {
Logger().d('版本比较错误: $e');
return false;
@ -654,7 +668,7 @@ class OtaUpgradeController extends GetxController {
color: Colors.green[600],
size: 28,
),
SizedBox(width: 12),
SizedBox(width: 12.w),
Text(
'升级成功',
style: TextStyle(

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

@ -740,72 +740,72 @@ class EditProfileView extends GetView<ProfileController> {
},
isDarkMode: isDarkMode,
),
// Divider(
// height: 1,
// color: isDarkMode
// ? Colors.white.withOpacity(0.1)
// : Colors.grey[200]),
// // 清理缓存
// _buildSimpleNavigationSetting(
// title: 'clean'.tr, // 清理缓存
// icon: Icons.logout,
// iconBgColor: isDarkMode
// ? Colors.red[900]!.withOpacity(0.3)
// : Colors.red[50]!,
// iconColor: isDarkMode ? Colors.red[300]! : Colors.red[600]!,
// titleColor:
// isDarkMode ? Colors.red[300]! : Colors.red[600]!,
// onTap: () {
// // 退出登录确认对话框
// Get.dialog(
// AlertDialog(
// backgroundColor:
// isDarkMode ? Colors.grey[900] : Colors.white,
// title: Text(
// 'clean'.tr, // 清理缓存
// style: TextStyle(
// color: isDarkMode ? Colors.white : Colors.black87,
// ),
// ),
// content: Text(
// 'cleanTips'.tr, // 确定要退出当前账号吗?
// style: TextStyle(
// color:
// isDarkMode ? Colors.white70 : Colors.black54,
// ),
// ),
// actions: [
// TextButton(
// onPressed: () => Get.back(),
// child: Text(
// 'cancel'.tr, // 取消
// style: TextStyle(
// color: isDarkMode
// ? Colors.white70
// : Colors.black54,
// ),
// ),
// ),
// TextButton(
// onPressed: () {
// Get.back();
// controller.clean();
// },
// child: Text(
// 'confirm'.tr, // 确定
// style: TextStyle(
// color: isDarkMode
// ? Colors.red[300]
// : Colors.red[600],
// ),
// ),
// ),
// ],
// ),
// );
// },
// isDarkMode: isDarkMode,
// ),
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// 清理缓存
_buildSimpleNavigationSetting(
title: 'clean'.tr, // 清理缓存
icon: Icons.cleaning_services,
iconBgColor: isDarkMode
? Colors.red[900]!.withOpacity(0.3)
: Colors.red[50]!,
iconColor: isDarkMode ? Colors.red[300]! : Colors.red[600]!,
titleColor:
isDarkMode ? Colors.red[300]! : Colors.red[600]!,
onTap: () {
// 退出登录确认对话框
Get.dialog(
AlertDialog(
backgroundColor:
isDarkMode ? Colors.grey[900] : Colors.white,
title: Text(
'clean'.tr, // 清除缓存
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
),
),
content: Text(
'cleanTips'.tr, // 确定要清除个人缓存吗?
style: TextStyle(
color:
isDarkMode ? Colors.white70 : Colors.black54,
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr, // 取消
style: TextStyle(
color: isDarkMode
? Colors.white70
: Colors.black54,
),
),
),
TextButton(
onPressed: () {
Get.back();
controller.clean();
},
child: Text(
'confirm'.tr, // 确定
style: TextStyle(
color: isDarkMode
? Colors.red[300]
: Colors.red[600],
),
),
),
],
),
);
},
isDarkMode: isDarkMode,
),
Divider(
height: 1,
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:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:get_storage/get_storage.dart';
import '../../../core/themes/theme_service.dart';
import '../../../data/services/speech_factory.dart';
import '../../../routes/app_routes.dart';
import '../models/language_model.dart';
import 'package:logger/logger.dart';
import '../../../data/services/ble_manager.dart';
import '../../../data/services/language_manager.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:permission_handler/permission_handler.dart';
@ -244,11 +247,9 @@ class SettingsController extends GetxController {
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
appVersion.value = packageInfo.version;
buildNumber.value = packageInfo.buildNumber;
print('版本信息: ${packageInfo.version}+${packageInfo.buildNumber}');
} catch (e) {
appVersion.value = '未知';
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') {
List languageList = data.value.split('_');
Get.updateLocale(Locale(languageList[0], languageList[1]));
} else {
Get.updateLocale(Get.deviceLocale!);
}
print("languageList: ${data.value}");
language.value = data;
_storage.write('language', data.value);
// 更新agent服务的语音识别语言设置
await _updateAgentLanguageSettings(data);
// 延迟一下确保语言更新完成,然后重新进入应用
await Future.delayed(const Duration(milliseconds: 500));
// 重新进入应用 - 回到启动页面重新初始化
Get.offAllNamed(Routes.splash);
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() {
try {

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

@ -16,13 +16,6 @@ import '../../profile/views/unified_avatar.dart';
class SettingsView extends GetView<SettingsController> {
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
Widget build(BuildContext context) {
// 直接使用 Theme.of(context) 获取当前主题
@ -425,32 +418,31 @@ class SettingsView extends GetView<SettingsController> {
},
isDarkMode: isDarkMode,
),
if (Platform.isAndroid) ...[
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// if (Platform.isAndroid) ...[
// Divider(
// height: 1,
// color: isDarkMode
// ? Colors.white.withOpacity(0.1)
// : Colors.grey[200]),
// 隐私设置
_buildSimpleNavigationSetting(
title: 'privacySettings'.tr, // 隐私设置
subtitle: 'privacySettingsInfo'.tr, // 管理数据收集和权限
icon: Icons.shield_outlined,
iconBgColor: isDarkMode
? Colors.purple[900]!.withOpacity(0.3)
: Colors.purple[100]!,
iconColor: isDarkMode
? Colors.purple[300]!
: Colors.purple[600]!,
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
onTap: () {
AppSettings.openAppSettings();
},
isDarkMode: isDarkMode,
)
],
// // 隐私设置
// _buildSimpleNavigationSetting(
// title: 'privacySettings'.tr, // 隐私设置
// subtitle: 'privacySettingsInfo'.tr, // 管理数据收集和权限
// icon: Icons.shield_outlined,
// iconBgColor: isDarkMode
// ? Colors.purple[900]!.withOpacity(0.3)
// : Colors.purple[100]!,
// iconColor:
// isDarkMode ? Colors.purple[300]! : Colors.purple[600]!,
// titleColor: isDarkMode ? Colors.white : null,
// subtitleColor: isDarkMode ? Colors.white70 : null,
// onTap: () {
// AppSettings.openAppSettings();
// },
// isDarkMode: isDarkMode,
// ),
// ],
Divider(
height: 1,
@ -599,32 +591,31 @@ class SettingsView extends GetView<SettingsController> {
onTap: () => Get.toNamed(Routes.OTA_UPGRADE),
isDarkMode: isDarkMode,
),
if (Platform.isAndroid) ...[
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// if (Platform.isAndroid) ...[
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// 添加权限管理设置项
_buildSimpleNavigationSetting(
title: 'permissionManagement'.tr, // 权限管理
subtitle: 'permissionManagementInfo'.tr, // 管理应用权限设置
icon: Icons.security,
iconBgColor: isDarkMode
? Colors.orange[900]!.withOpacity(0.3)
: Colors.orange[100]!,
iconColor: isDarkMode
? Colors.orange[300]!
: Colors.orange[600]!,
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
onTap: () {
Get.toNamed(Routes.permissions);
},
isDarkMode: isDarkMode,
),
]
// 权限管理设置项
_buildSimpleNavigationSetting(
title: 'permissionManagement'.tr, // 权限管理
subtitle: 'permissionManagementInfo'.tr, // 管理应用权限设置
icon: Icons.security,
iconBgColor: isDarkMode
? Colors.orange[900]!.withOpacity(0.3)
: Colors.orange[100]!,
iconColor:
isDarkMode ? Colors.orange[300]! : Colors.orange[600]!,
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
onTap: () {
Get.toNamed(Routes.permissions);
},
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:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/music_manager.dart';
import '../../../data/services/volcano_translation_service.dart';
import '../../../data/services/tts_service.dart';
@ -30,6 +31,8 @@ class TranslationController extends GetxController {
static const String TAG = 'TranslationController';
// 服务
final BluetoothManager _bluetoothManager = Get.find<BluetoothManager>();
final BleManager _bleManager = Get.find<BleManager>();
final AsrService _asrService = Get.find<AsrService>();
final VolcanoTranslationService _translationService =
Get.find<VolcanoTranslationService>();
@ -364,7 +367,7 @@ class TranslationController extends GetxController {
@override
void onClose() {
stopRecognition();
stopAll();
_recognitionSubscription?.cancel();
_recognitionSubscription = null;
_voiceInteractionSubscription?.cancel();
@ -483,7 +486,11 @@ class TranslationController extends GetxController {
if (currentMode.value == 'faceToFace') {
_asrService.stopContinuousRecognition();
} else {
await _ttsService.setAudioOutputDevice(0);
if (_bluetoothManager.currentDeviceRx.value != null) {
restoreOriginalAudioState();
} else {
disableBluetoothAudio();
}
await _asrService.startContinuousRecognition(_audioSourceType);
// 开始ASR活跃时长计时
_startAsrActiveTracking();

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

@ -662,13 +662,15 @@ class TranslationView extends GetView<TranslationController> {
// 显示语言选择对话框
void _showLanguageSelectionDialog(bool isSource, bool isDarkMode) {
Get.dialog(
Dialog(
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r), // 统一圆角尺寸为16
),
insetPadding: EdgeInsets.symmetric(horizontal: 20.w),
Get.dialog(Dialog(
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r), // 统一圆角尺寸为16
),
alignment: isSource ? Alignment.centerLeft : Alignment.centerRight,
insetPadding: EdgeInsets.symmetric(horizontal: 10.w),
child: SizedBox(
width: 0.4.sw,
child: Padding(
// 使用Padding替代Container的padding
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,
backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200],
colorText: Get.isDarkMode ? Colors.white : Colors.black,
duration: const Duration(seconds: 2),
duration: const Duration(milliseconds: 800),
);
} catch (e) {
// 显示删除失败提示
@ -1045,7 +1047,7 @@ class TranslationView extends GetView<TranslationController> {
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[400],
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 usertoken = ""
private var language = ""
// 历史聊天消息缓存
private val historyMessages = JSONArray()
@ -200,8 +201,8 @@ object AgentService : CoroutineScope {
//获取语音识别的语种
val supportedLanguagesConfig = config["supportedLanguages"] as? List<String>
val supportedLanguages = supportedLanguagesConfig?.toTypedArray()
?: arrayOf("zh-CN", "en-US") // 默认支持多语言
?: arrayOf("zh-CN") // 默认支持多语言
language = supportedLanguages[0]
val asrInitSuccess = initialize(
subscriptionKey = config["azureSpeechKey"]?.toString() ?: "",
region = config["azureSpeechRegion"]?.toString() ?: "",
@ -422,7 +423,7 @@ object AgentService : CoroutineScope {
try {
delay(maxIdleSeconds * 1000L)
if (isActive && isRecognitionActive && !hasSpeechDetected && !isTtsSpeaking && !isAiStreaming) {
if (isActive && isRecognitionActive && !isTtsSpeaking && !isAiStreaming) {
stopRecognition()
sendEvent(
"auto_stop",
@ -524,8 +525,8 @@ object AgentService : CoroutineScope {
override fun onRecognizing(recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()) {
// 检测到语音,更新状态
val previousHasSpeech = hasSpeechDetected
_hasSpeechDetected.set(true)
val previousHasSpeech = hasSpeechDetected
_hasSpeechDetected.set(true)
if (!previousHasSpeech) {
restartIdleCheck()
@ -829,7 +830,8 @@ object AgentService : CoroutineScope {
// 先添加系统提示词
if (systemPrompt.isNotEmpty()) {
val mapdata = UserPortrait.getAll()
val mapdata = UserPortrait.getAll().toMutableMap()
mapdata["language"] = language
// Log.d(TAG, "liwei--------- 用户画像: ${mapdata}")
val _systemPrompt = Utils.fillTemplate(systemPrompt, mapdata)
messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt))
@ -1120,7 +1122,7 @@ object AgentService : CoroutineScope {
}
/**
* 设置支持的语音识别语言
* 设置支持的语音识别语言,同时更新TTS语言设置
*/
fun setSupportedLanguages(languages: List<String>): Boolean {
if (!isInitialized) {
@ -1134,33 +1136,58 @@ object AgentService : CoroutineScope {
sendEvent("error", mapOf("code" to "EMPTY_LANGUAGES", "message" to "语言列表不能为空"))
return false
}
language = languages[0]
return try {
Log.i(TAG, "设置支持的语言: ${languages.joinToString(", ")}")
// 重新初始化ASR服务以应用新的语言设置
val success = azureAsrHelper?.initialize(
subscriptionKey = this.azureSpeechKey,
region = this.azureSpeechRegion,
supportedLanguages = languages.toTypedArray(),
audioSourceType = audioSourceType
) ?: false
if (success) {
Log.i(TAG, "语言设置更新成功")
val asrSuccess = AzureAsrHelper(context).apply {
// 使用协程处理异步初始化
CoroutineScope(Dispatchers.IO).launch {
//获取语音识别的语种
val supportedLanguagesConfig = config["supportedLanguages"] as? List<String>
val supportedLanguages = supportedLanguagesConfig?.toTypedArray()
?: arrayOf("zh-CN") // 默认支持多语言
language = supportedLanguages[0]
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(
"languages" to languages,
"status" to "success"
"status" to "success",
"asr_success" to asrSuccess,
"tts_success" to ttsSuccess
))
} else {
Log.e(TAG, "语言设置更新失败")
Log.e(TAG, "语言设置更新失败 - ASR: $asrSuccess, TTS: $ttsSuccess")
sendEvent("error", mapOf(
"code" to "LANGUAGE_UPDATE_FAILED",
"message" to "语言设置更新失败"
"message" to "语言设置更新失败 - ASR: $asrSuccess, TTS: $ttsSuccess"
))
}
success
overallSuccess
} catch (e: Exception) {
Log.e(TAG, "设置支持语言异常: ${e.message}")
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
var currsessionId: String = ""
var language: String = ""
private var idleTimer: DispatchSourceTimer?
private let maxIdleSeconds: TimeInterval = 10
@ -158,6 +159,9 @@ class AgentServiceImpl: NSObject {
if let openaivisionModel = config["openaivisionModel"] as? String {
self.visionModel = openaivisionModel
}
if let supportedLanguages = config["supportedLanguages"] as? [String] {
self.language = supportedLanguages[0]
}
if let customSystemPrompt = config["systemPrompt"] as? String, !customSystemPrompt.isEmpty {
self.systemPrompt = customSystemPrompt
} else {
@ -208,7 +212,7 @@ class AgentServiceImpl: NSObject {
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(
subscriptionKey: azureSpeechKey,
@ -287,8 +291,8 @@ class AgentServiceImpl: NSObject {
// 使用更安全的方式检查self是否存在
DispatchQueue.main.async { [weak self] in
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.sendEvent(name: "auto_stop", data: [
"reason": "idle_timeout",
@ -345,8 +349,14 @@ print("ai启动语音=\(audioSourceType)=\(useBle)")
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
}
@ -361,7 +371,12 @@ print("ai启动语音=\(audioSourceType)=\(useBle)")
return true
}
func disableBluetoothAudio() {
azureAsrHelper?.disableBluetoothAudio()
}
func restoreOriginalAudioState() {
azureAsrHelper?.restoreOriginalAudioState()
}
func pushAudioData(_ audioData: Data) -> Bool {
if !isRecognizing || !isInitialized {
return false
@ -454,8 +469,8 @@ audioStream.saveAudioDataTo(data: audioData)
processWithChatApiServiceInternal(userMessage: userMessage, displayText: text, speakResponse: speakResponse)
}
private func processWithChatApiServiceInternal(userMessage: [String: Any], displayText: String, speakResponse: Bool = true, hasImage: Bool = false) {
os_log("开始处理ChatAPI请求: displayText=%{public}@, speakResponse=%{public}@, hasImage=%{public}@",
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}@",
log: logger, type: .info, displayText, speakResponse ? "是" : "否", hasImage ? "是" : "否")
guard let chatApiService = chatApiService else {
@ -476,9 +491,16 @@ audioStream.saveAudioDataTo(data: audioData)
var messages: [[String: Any]] = []
if !systemPrompt.isEmpty {
let info: [String: String] = UserPortrait.shared.getAll()
var info = UserPortrait.shared.getAll()
info["language"] = self.language
// 添加调试日志
print("系统提示词模板: \(systemPrompt)")
print("用户画像数据: \(info)")
let _systemPrompt = fillTemplate(systemPrompt, with: info)
print("替换后的系统提示词: \(_systemPrompt)")
messages.append(chatApiService.createSystemMessage(content: _systemPrompt))
os_log("添加系统提示消息", log: logger, type: .info)
}
@ -495,7 +517,8 @@ audioStream.saveAudioDataTo(data: audioData)
agentService: self,
speakResponse: speakResponse,
displayText: displayText,
hasImage: hasImage
hasImage: hasImage,
imagePath: imagePath
)
currsessionId = UUID().uuidString
os_log("设置回调并调用sendMessageStream", log: logger, type: .info)
@ -552,7 +575,8 @@ audioStream.saveAudioDataTo(data: audioData)
userMessage: userMessage,
displayText: displayText,
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"]
* @return 是否设置成功
*/
func setSupportedLanguages(_ languages: [String]) -> Bool {
os_log("设置支持的语言: %{public}@", log: logger, type: .info, languages.joined(separator: ", "))
if !isInitialized {
os_log("服务未初始化,无法设置语言", log: logger, type: .error)
sendError("服务未初始化", code: "NOT_INITIALIZED")
return false
}
if languages.isEmpty {
os_log("语言列表为空,使用默认语言", log: logger, type: .info)
return false
}
self.language = languages[0]
// 如果正在识别,需要先停止
let wasRecognizing = isRecognizing
if wasRecognizing {
_ = stopRecognition()
}
// 重新初始化ASR引擎,使用新的语言设置
guard let success = azureAsrHelper?.initialize(
subscriptionKey: azureSpeechKey,
region: azureSpeechRegion,
guard let asrSuccess = azureAsrHelper?.initialize(
subscriptionKey: azureSpeechKey,
region: azureSpeechRegion,
supportedLanguages: languages,
audioSourceType: .microphone
), success else {
os_log("重新初始化ASR引擎失败", log: logger, type: .error)
sendError("重新初始化ASR引擎失败", code: "ASR_INIT_ERROR")
), asrSuccess else {
sendError("初始化语音识别服务失败", code: "ASR_INIT_ERROR")
return false
}
guard let asrsetupSuccess = azureAsrHelper?.setupEventListeners(
callback: self
), asrsetupSuccess else {
sendError("初始化语音识别服务失败", code: "ASR_INIT_ERROR")
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 {
_ = 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 {
print("ai释放资源")
if isRecognizing {
@ -967,19 +1139,22 @@ class ChatApiStreamCallback: StreamCallback {
private let speakResponse: Bool
private let displayText: String
private let hasImage: Bool
private let imagePath: String
private var responseBuilder = ""
private var metadata = ""
private var userMetadata:[String: Any] = [:]
private var reply = 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.speakResponse = speakResponse
self.displayText = displayText
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?){
@ -1418,6 +1593,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
hasSpeechDetected = true
if !previousHasSpeech {
print("onRecognizing 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":
impl.stopRecognition()
result(true)
case "disableBluetoothAudio":
impl.disableBluetoothAudio();
return
case "restoreOriginalAudioState":
impl.restoreOriginalAudioState();
return
case "processTextInput":
guard let arguments = call.arguments as? [String: Any],
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 {
try {
@ -385,6 +410,7 @@ class AgentService {
///
/// [imagePath] 图片文件路径
/// [text] 可选的文本描述或问题
/// [imageUrl] 图片url
/// [speakResponse] 是否朗读响应
/// 返回是否成功处理图片
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 {
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) {
if (audioSourceType == AudioSourceType.MICROPHONE) return
if (audioSourceType == AudioSourceType.MICROPHONE||!isContinuousRecognitionActive) return
// 放入队列,由写线程写入
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()
if isAutoDetectLanguage {
speechConfig?.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode")
speechConfig?.setPropertyTo("Continuous", by: SPXPropertyId.speechServiceConnectionLanguageIdMode)
// 不在这里设置语言,让SPXAutoDetectSourceLanguageConfiguration处理
os_log("启用语言检测模式,支持语言: %{public}@", log: log, type: .info, supportedLanguages.joined(separator: ","))
} else {
@ -131,7 +131,7 @@ public class AzureAsrHelper: NSObject {
*/
private func preInitializeAudioComponents() {
if audioStream == nil {
audioStream = AudioStream()
audioStream = AudioStream(parentHelper: self)
audioStream?.initAudioRecord()
audioStream?.onAudioData = { [weak self] data in
self?.recordfile?.saveAudioDataToWav(data)
@ -354,7 +354,7 @@ public class AzureAsrHelper: NSObject {
do {
// 【优化】检查是否已经初始化,避免重复创建
if audioStream == nil {
audioStream = AudioStream()
audioStream = AudioStream(parentHelper: self)
audioStream?.initAudioRecord()
audioStream?.onAudioData = { [weak self] data in
self?.recordfile?.saveAudioDataToWav(data)
@ -548,6 +548,7 @@ public class AzureAsrHelper: NSObject {
public func disableBluetoothAudio() {
do {
print("禁用蓝牙音频功能,切换回正常音频模式")
audioStream?.setAudioOutputRoute(.speaker)
} catch {
print("disableBluetoothAudio")
@ -559,6 +560,7 @@ public class AzureAsrHelper: NSObject {
public func restoreOriginalAudioState() {
do {
print("恢复原始音频设备状态(通常是重新启用蓝牙)")
audioStream?.setAudioOutputRoute(.receiver)
} catch {
print("restoreOriginalAudioState")
@ -632,9 +634,18 @@ public class AzureAsrHelper: NSObject {
private var isRunning = false
public var isWriting = false
private let bufferSize: Int = 4096
private var writeThread: DispatchQueue?
private var currentRoute: AudioOutputRoute?
public var onAudioData: ((Data) -> Void)?
private var writeThread: DispatchQueue?
private var currentRoute: AudioOutputRoute?
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")
startWriteThread()
self.setAudioOutputRoute(.receiver)
//self.setAudioOutputRoute(.receiver)
}
/// 获取最佳音频格式 (iOS 通常支持标准采样率)
@ -717,7 +728,8 @@ public class AzureAsrHelper: NSObject {
* @param data 音频数据字节数组
*/
public func saveAudioDataTo(data: Data) {
if audioSourceType != .external {
// 通过父类引用调用方法
if audioSourceType != .external || !(parentHelper?.isContinuousRecognitionActive() ?? false) {
return
}
// print("外部data=\(data)")
@ -879,8 +891,7 @@ private func runMicrophoneCapture() {
case bluetooth
}
public func setAudioOutputRoute(_ route: AudioOutputRoute) {
guard currentRoute != route else { return }
print("setAudioOutputRoute")
do {
//
if self.isWriting {
@ -890,14 +901,15 @@ private func runMicrophoneCapture() {
try audioSession.setActive(false)
}
print("setAudioOutputRoutecurrent,route=\(route)")
switch route {
case .speaker:
print("setAudioOutputRoutecurrent,speaker")
// 使用扬声器时必须用videoChat模式
try audioSession.setCategory(
.playAndRecord,
mode: .videoChat,
options: [.allowBluetooth, .mixWithOthers, .defaultToSpeaker]
options: [.defaultToSpeaker]
)
try audioSession.overrideOutputAudioPort(.speaker)
@ -906,7 +918,7 @@ private func runMicrophoneCapture() {
try audioSession.setCategory(
.playAndRecord,
mode: .voiceChat,
options: [.allowBluetooth, .mixWithOthers, .defaultToSpeaker]
options: [.allowBluetooth]
)
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!
)
result(success)
case "disableBluetoothAudio":
azureAsrHelper.disableBluetoothAudio();
return
case "restoreOriginalAudioState":
azureAsrHelper.restoreOriginalAudioState();
return
case "stopContinuousRecognition":
let success = azureAsrHelper.stopContinuousRecognition()
@ -224,7 +233,7 @@ import os.log
return
}
do {
print("音频文件名称为: \(filePath)")
print("音频文件名称为: (filePath)")
try azureAsrHelper.renameFile(filePath: filePath, newName: newName)
result(true)
} catch {
@ -239,7 +248,7 @@ import os.log
return
}
do {
print("音频文件名称为: \(sourcePath)")
print("音频文件名称为: (sourcePath)")
try azureAsrHelper.moveFile(sourcePath: sourcePath, destPath: destPath)
result(true)
} catch {
@ -253,7 +262,7 @@ import os.log
return
}
do {
print("音频文件名称为: \(filePath)")
print("音频文件名称为: (filePath)")
try azureAsrHelper.enableRecord(filePath: filePath)
result(true)
} catch {
@ -350,7 +359,7 @@ import os.log
let success = azureTtsHelper.flushStream()
result(success)
case "stop":
case "stopSpeaking":
let success = azureTtsHelper.stop()
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
// 标点符号列表 (与Android一致)
let punctuationMarks: [Character] = [".", "。", "!", "!", "?", "?", ";", ";", ",", ",", ":", ":", "\n"]
let punctuationMarks: [Character] = [".", "。", "!", "!","*", "?", "?", ";", ";", ",", ",", ":", ":", "\n"]
// 从后往前查找最后一个标点符号
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模式
var isEnterOta = false
// 是否需要唤醒
var isWakeup = true
// 是否回复指令
var isReply = true
@ -1515,7 +1518,13 @@ object BleService {
private fun notifyWakeupSignalReceived() {
for (callback in callbacks) {
try {
callback.onWakeupSignalReceived()
if (isWakeup) {
callback.onWakeupSignalReceived()
} else {
if (callback::class.java.simpleName == "BleServicePlugin") {
callback.onWakeupSignalReceived()
}
}
} catch (e: Exception) {
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()
result.success(connected)
}
"setWakeup" -> {
val wakeup = call.argument<Boolean>("wakeup")
if (wakeup != null) {
BleService.isWakeup = wakeup
}
result.success("")
}
"registerBackgroundScan" -> {
bleCompanionManager.registerBackgroundScan()
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 {
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 androidx.core.content.ContextCompat
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.engine.okhttp.*
import io.ktor.client.plugins.sse.*
@ -169,16 +171,42 @@ fun roughTokenCount(text: String): Int {
/**
* 估算消息列表的总 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 数
*/
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 ?: "")
var totalText = ""
for (message in messages) {
when (val content = message.messageContent) {
is TextContent? -> {
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 tokens
}
return roughTokenCount(totalText)
}
//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) {
print("[ChatApiService] 设置回调 for sessionId: \(sessionId)")
print("[ChatApiService] 回调对象: \(callback)")
// print("[ChatApiService] 设置回调 for sessionId: \(sessionId)")
// print("[ChatApiService] 回调对象: \(callback)")
self.sessionCallbacks[sessionId] = callback
print("[ChatApiService] 回调设置完成,当前回调数量: \(sessionCallbacks.count)")
}
public func getSessionCallback(_ sessionId: String) -> StreamCallback? {
let callback = self.sessionCallbacks[sessionId]
print("[ChatApiService] getSessionCallback for sessionId: \(sessionId)")
print("[ChatApiService] sessionCallbacks count: \(sessionCallbacks.count)")
print("[ChatApiService] sessionCallbacks keys: \(Array(sessionCallbacks.keys))")
print("[ChatApiService] callback exists: \(callback != nil)")
// print("[ChatApiService] getSessionCallback for sessionId: \(sessionId)")
// print("[ChatApiService] sessionCallbacks count: \(sessionCallbacks.count)")
// print("[ChatApiService] sessionCallbacks keys: \(Array(sessionCallbacks.keys))")
// print("[ChatApiService] callback exists: \(callback != nil)")
return callback
}
//移除回调函数
@ -311,7 +311,7 @@ public func sendMessageStream(_ sessionId: String, messages: [[String: Any]]) {
tools: chatTools
)
print("[ChatApiService] 发起AI问答 问题:\(messages[0]) 工具数量: \(chatTools.count)")
print("[ChatApiService] 发起AI问答 问题:\(messages) 工具数量: \(chatTools.count)")
} else {
query = ChatQuery(
messages: chatMessages,
@ -335,14 +335,14 @@ public func sendMessageStream(_ sessionId: String, messages: [[String: Any]]) {
// 处理文本内容
if let content = delta.content {
replyContext += content
print("[ChatApiService] 收到内容: \(content)")
print("[ChatApiService] 当前sessionId: \(sessionId)")
// print("[ChatApiService] 收到内容: \(content)")
// print("[ChatApiService] 当前sessionId: \(sessionId)")
let callback = self.getSessionCallback(sessionId)
if let callback = callback {
print("[ChatApiService] 找到回调,准备调用 onToken")
// print("[ChatApiService] 找到回调,准备调用 onToken")
callback.onToken(sessionId, content)
print("[ChatApiService] onToken 调用完成")
// print("[ChatApiService] onToken 调用完成")
} else {
print("[ChatApiService] 警告:未找到 sessionId \(sessionId) 的回调")
}
@ -761,7 +761,44 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] {
}
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格式
private func convertToChatMessages(_ messages: [[String: Any]]) throws -> [ChatQuery.ChatCompletionMessageParam] {
var chatMessages: [ChatQuery.ChatCompletionMessageParam] = []
@ -851,17 +888,24 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] {
var textContent = ""
for item in contentArray {
if let type = item["type"] as? String, type == "text",
let text = item["text"] as? String {
textContent += text
if let type = item["type"] as? String {
switch type {
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)
print("解析工具 \(name):\(schema)")
// print("解析工具 \(name):\(schema)")
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 {
print("[MCPClient] 🎉 MCP 连接完成,共连接 \(connectedCount) 个服务器")
print("[MCPClient] 📋 所有可用工具汇总:")
// print("[MCPClient] 📋 所有可用工具汇总:")
let allTools = getToolMaps()
for (index, toolMap) in allTools.enumerated() {
@ -490,9 +490,9 @@ public class MCPClient {
}
}
print("[MCPClient] 📊 工具总数: \(allTools.count)")
print("[MCPClient] 🔧 本地函数: \(localFunctionDefs.count) 个")
print("[MCPClient] 🌐 远程工具: \(allTools.count - localFunctionDefs.count) 个")
// print("[MCPClient] 📊 工具总数: \(allTools.count)")
// print("[MCPClient] 🔧 本地函数: \(localFunctionDefs.count) 个")
// print("[MCPClient] 🌐 远程工具: \(allTools.count - localFunctionDefs.count) 个")
}
return connectedCount > 0
@ -508,7 +508,7 @@ public class MCPClient {
parameters: Any,
handler: FunctionHandler
) -> Bool {
print("[MCPClient] 注册本地函数: \(name)")
// print("[MCPClient] 注册本地函数: \(name)")
localFunctions[name] = handler
let functionDef: [String: Any] = [
@ -544,7 +544,7 @@ public class MCPClient {
for client in subClients.values {
let tools = client.getToolMaps()
print("[MCPClient] 读取工具列表: \(tools)")
// print("[MCPClient] 读取工具列表: \(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"]
]
let handler = SetUserProfileFieldHandler()
let handler: SetUserProfileFieldHandler = SetUserProfileFieldHandler()
_ = client.registerLocalFunction(
name: "set_user_profile_field",
description: "设置或更新用户画像中的某一个字段",
@ -462,8 +462,7 @@ private class GetCurrentLocationHandler: FunctionHandler {
guard let latStr = locationInfo["user_lat"] as? String,
let lngStr = locationInfo["user_lng"] as? String,
let latitude = Double(latStr),
let longitude = Double(lngStr),
let address = locationInfo["user_location"] as? String else {
let longitude = Double(lngStr) else {
print("[GetCurrentLocationHandler] 位置数据格式错误")
continuation.resume(returning: self.createErrorResponse(message: "位置数据格式错误,请重试"))
return
@ -471,8 +470,8 @@ private class GetCurrentLocationHandler: FunctionHandler {
print("[GetCurrentLocationHandler] 位置获取成功: lat=\(latitude), lng=\(longitude)")
let message = "当前位置:\(address)(纬度 \(latitude),经度 \(longitude))"
let successResponse = self.createSuccessResponse(message: message, latitude: latitude, longitude: longitude, address: address)
// let message = "当前位置:\(address)(纬度 \(latitude),经度 \(longitude))"
let successResponse = self.createSuccessResponse(latitude: latitude, longitude: longitude)
print("[GetCurrentLocationHandler] 返回位置结果")
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] = [
"success": true,
"message": message,
"data": [
"latitude": latitude,
"longitude": longitude,
"address": address,
"timestamp": Date().timeIntervalSince1970 * 1000
]
]
@ -847,21 +844,19 @@ private class OpenRecorderHandler: FunctionHandler {
private class SetUserProfileFieldHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
guard let key = arguments["key"] as? String,
let value = arguments["value"] as? String else {
throw ChatApiException("缺少必要参数:key 和 value")
let value = arguments["value"] as? String else {
return "设置成功"
}
// 验证参数
if key.isEmpty {
throw ChatApiException("画像字段名不能为空")
return "设置成功"
}
if value.isEmpty {
throw ChatApiException("字段值不能为空")
return "设置成功"
}
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 {

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

@ -186,7 +186,7 @@ public class ChatStorageHelper {
"totalCount": totalCount,
"totalPages": totalPages
]
os_log("getMessages: 读取历史记录: %{public}@", log: logger, type: .error, messagesArray)
do {
let jsonData = try JSONSerialization.data(withJSONObject: result, options: [])
if let jsonString = String(data: jsonData, encoding: .utf8) {
@ -199,7 +199,7 @@ public class ChatStorageHelper {
let errmsg = String(cString: sqlite3_errmsg(db)!)
os_log("getMessages: SQL准备失败: %{public}@", log: logger, type: .error, errmsg)
}
// 返回空结果,但保持格式一致
let emptyResult: [String: Any] = [
"messages": [],
@ -370,4 +370,4 @@ public class ChatStorageHelper {
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 let latitude: Double
public let longitude: Double
public let address: String
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.longitude = longitude
self.address = address
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()
setupLocationManager()
}
private func setupLocationManager() {
DispatchQueue.main.async { [weak self] in
self?.locationManager = CLLocationManager()
@ -35,15 +34,26 @@ public class LocationService: NSObject {
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)")
// 不需要手动获取状态,delegate 方法会自动被调用
print("[LocationService] LocationManager 初始化完成,等待权限状态回调")
}
}
// 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) {
@ -190,11 +200,11 @@ public class LocationService: NSObject {
}
// 使用缓存的状态信息
print("[LocationService] 开始位置请求")
print("[LocationService] 缓存的权限状态: \(self.authorizationStatus.rawValue)")
print("[LocationService] 缓存的位置服务状态: \(self.isLocationServicesEnabled)")
print("[LocationService] 当前线程: \(Thread.current)")
print("[LocationService] 是否主线程: \(Thread.isMainThread)")
// print("[LocationService] 开始位置请求")
// print("[LocationService] 缓存的权限状态: \(self.authorizationStatus.rawValue)")
// print("[LocationService] 缓存的位置服务状态: \(self.isLocationServicesEnabled)")
// print("[LocationService] 当前线程: \(Thread.current)")
// print("[LocationService] 是否主线程: \(Thread.isMainThread)")
// 验证权限状态
if !self.checkLocationPermission() {
@ -238,7 +248,6 @@ public class LocationService: NSObject {
let locationData = LocationData(
latitude: latitude,
longitude: longitude,
address: "",
timestamp: Date().timeIntervalSince1970 * 1000
)
completion(.success(locationData))
@ -273,7 +282,6 @@ public class LocationService: NSObject {
return [
"user_lat": String(locationData.latitude),
"user_lng": String(locationData.longitude),
"user_location": locationData.address,
"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)
Log.d(TAG, "注册本地函数成功: $name")
// Log.d(TAG, "注册本地函数成功: $name")
return true
} catch (e: Exception) {
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) ?? ""
localFunctionDefs[name] = jsonString
print("\(tag) 成功注册本地函数: \(name)")
// print("\(tag) 成功注册本地函数: \(name)")
return true
} catch {
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) ?? ""
localFunctionDefs[name] = jsonString
print("\(tag) 成功注册本地函数: \(name)")
// print("\(tag) 成功注册本地函数: \(name)")
return true
} catch {
print("\(tag) 注册本地函数失败: \(error.localizedDescription)")

2
pubspec.yaml

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

Loading…
Cancel
Save