Browse Source

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

newdev_shunjiawei
fdp 1 year ago
parent
commit
6846f58bcc
  1. 2
      android/app/src/main/AndroidManifest.xml
  2. 9
      lib/core/translations/language/en_us.dart
  3. 6
      lib/core/translations/language/zh_cn.dart
  4. 68
      lib/core/widgets/intro_overlay_builder.dart
  5. 71
      lib/data/services/novice_guide_service.dart
  6. 61
      lib/modules/agent/controllers/agent_controller.dart
  7. 163
      lib/modules/agent/views/agent_view.dart
  8. 101
      lib/modules/agent/views/weather_card.dart
  9. 122
      lib/modules/login/controllers/login_controller.dart
  10. 16
      lib/modules/settings/controllers/settings_controller.dart
  11. 44
      lib/modules/settings/views/permissions_view.dart
  12. 76
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  13. 3
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  14. 8
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  15. 86
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  16. 3
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift
  17. 7
      local_plugins/agent_service/lib/agent_service.dart
  18. 57
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt
  19. 39
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt
  20. 175
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt
  21. 28
      local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt

2
android/app/src/main/AndroidManifest.xml

@ -50,6 +50,8 @@
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!--用于申请调用A-GPS模块,卫星定位加速-->
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
<!--用于申请调用精确闹钟权限-->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<!--允许写设备缓存,用于问题排查-->
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.VIBRATE"/>

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

@ -487,15 +487,19 @@ const Map<String, String> enUS = {
// 权限管理页面
'getPermissionStatusFailed': 'Failed to get permission status',
'communicationPermissions': 'Communication Permissions',
'alarmPermissions': 'Alarm Permissions',
'calendarPermissions': 'Calendar Permissions',
'locationPermissions': 'Location Permissions',
'phone': 'Phone',
'sms': 'SMS',
'alarm': 'Alarm',
'calendarWrite': 'Calendar Write',
'calendarFullAccess': 'Calendar Full Access',
'locationInfo': 'Location Info',
'phonePermissionDesc': 'Used for phone call functionality',
'smsPermissionDesc': 'Used for SMS functionality',
'alarmPermissionDesc':
'Used to set exact alarms to ensure reminders trigger on time',
'calendarWritePermissionDesc':
'Used for creating calendar events and reminders',
'calendarFullAccessPermissionDesc':
@ -512,6 +516,11 @@ const Map<String, String> enUS = {
'phonePermissionDetailDesc':
'Phone call functionality requires phone permission',
'smsPermissionDetailDesc': 'SMS functionality requires SMS permission',
'alarmPermissionDetailDesc':
'This permission is required to set exact alarms and ensure reminders trigger on time',
'contactsPermissionDesc': 'Used for accessing contact information',
'contactsPermissionDetailDesc':
'Phone call and SMS functionality requires contacts permission to select contacts',
'calendarWritePermissionDetailDesc':
'Creating calendar events requires calendar write permission',
'calendarFullAccessPermissionDetailDesc':

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

@ -455,15 +455,18 @@ const Map<String, String> zhCN = {
// 权限管理页面
'getPermissionStatusFailed': '获取权限状态失败',
'communicationPermissions': '通讯权限',
'alarmPermissions': '闹钟权限',
'calendarPermissions': '日历权限',
'locationPermissions': '位置权限',
'phone': '电话',
'sms': '短信',
'alarm': '闹钟',
'calendarWrite': '日历写入',
'calendarFullAccess': '日历完整访问',
'locationInfo': '位置信息',
'phonePermissionDesc': '用于拨打电话功能',
'smsPermissionDesc': '用于发送短信功能',
'alarmPermissionDesc': '用于设置精确闹钟以确保提醒按时触发',
'calendarWritePermissionDesc': '用于创建日历事件和提醒',
'calendarFullAccessPermissionDesc': '用于读取和管理日历事件及提醒',
'locationPermissionDesc': '用于获取当前位置信息',
@ -477,9 +480,12 @@ const Map<String, String> zhCN = {
'permissionRequestError': '请求权限时出错',
'phonePermissionDetailDesc': '拨打电话功能需要电话权限',
'smsPermissionDetailDesc': '发送短信功能需要短信权限',
'alarmPermissionDetailDesc': '设置精确闹钟需要此权限,以确保提醒按时触发',
'calendarWritePermissionDetailDesc': '创建日历事件需要日历写入权限',
'calendarFullAccessPermissionDetailDesc': '管理日历事件需要完整日历权限',
'locationPermissionDetailDesc': '获取位置信息需要位置权限',
'contactsPermissionDesc': '用于访问联系人信息',
'contactsPermissionDetailDesc': '拨打电话和发送短信功能需要联系人权限以选择联系人',
'appNeedsPermissionForFullFunction': '应用需要此权限以提供完整功能',
'permanentDenialText': '您已永久拒绝{permission}权限,请前往应用设置开启',
'goToSettings': '去设置',

68
lib/core/widgets/intro_overlay_builder.dart

@ -7,6 +7,7 @@ class IntroOverlayBuilder {
static Widget homeBuilder(
StepWidgetParams params,
bool isWakeup, {
String title = '对着麦克风说:“小言小言”',
Function? onNext,
}) {
return Column(
@ -21,7 +22,7 @@ class IntroOverlayBuilder {
Padding(
padding: EdgeInsets.only(bottom: 20.w),
child: Text(
isWakeup ? '我在呢~' : '对着麦克风说:“小言小言”',
isWakeup ? '我在呢~' : title,
style: TextStyle(
fontSize: 15.sp,
color: Colors.white,
@ -30,12 +31,27 @@ class IntroOverlayBuilder {
),
),
if (onNext != null)
_button(
'下一步',
() {
params.onNext!();
onNext();
},
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.only(right: 50.w),
child: _button(
'下一步',
() {
params.onNext!();
onNext();
},
),
),
_button(
'跳过',
() {
params.onFinish();
},
isSkip: true,
),
],
),
],
);
@ -82,17 +98,43 @@ class IntroOverlayBuilder {
onFinish();
},
),
if (onNext != null || onFinish != null) const Spacer(),
_button(
'跳过',
() {
params.onFinish();
},
isSkip: true,
),
],
),
],
);
}
static Widget _button(String text, Function() onTap) {
return _AnimatedBorderButton(
text: text,
onTap: onTap,
);
static Widget _button(String text, Function() onTap, {bool isSkip = false}) {
return isSkip
? GestureDetector(
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 5.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.r),
border: Border.all(color: Colors.white),
),
child: Text(
text,
style: TextStyle(
fontSize: 14.sp,
color: Colors.white,
),
),
),
)
: _AnimatedBorderButton(
text: text,
onTap: onTap,
);
}
}
@ -207,7 +249,7 @@ class _AnimatedBorderButtonState extends State<_AnimatedBorderButton>
borderRadius: 20.r,
),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w),
padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 5.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.r),
border: Border.all(color: Colors.transparent),

71
lib/data/services/novice_guide_service.dart

@ -4,6 +4,8 @@ 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';
import 'package:permission_handler/permission_handler.dart';
import '../../core/utils/permission_util.dart';
class NoviceGuideService extends GetxService {
final GetStorage _storage = GetStorage();
@ -24,44 +26,109 @@ class NoviceGuideService extends GetxService {
'input': '今天天气怎么样',
'send': '小言会为你实时为你播报天气',
'end': '小言能实时为你播报天气,试试其他指令吧',
'method': () async {
// 天气查询需要位置权限
await PermissionUtil.instance.requestPermission(
permissionType: Permission.location,
permissionName: 'locationInfo'.tr, // 位置信息
explanationText: 'locationPermissionDesc'.tr, // 用于获取当前位置信息
permanentDenialText:
'locationPermissionDetailDesc'.tr, // 获取位置信息需要位置权限
);
},
},
{
'start': '对小言说:“请播放一组摇滚乐”',
'input': '请播放一组摇滚乐',
'send': '小言会为你搜寻到一组音乐',
'end': '小言已成功为您搜寻到一组音乐,试试其他指令吧',
'method': () async {},
},
{
'start': '对小言说:“导航到深圳机场”',
'input': '导航到深圳机场',
'send': '小言会为你规划路线',
'end': '小言已成功为您导航到深圳机场,试试其他指令吧',
'method': () async {
// 导航功能需要位置权限
await PermissionUtil.instance.requestPermission(
permissionType: Permission.location,
permissionName: 'locationInfo'.tr, // 位置信息
explanationText: 'locationPermissionDesc'.tr, // 用于获取当前位置信息
permanentDenialText:
'locationPermissionDetailDesc'.tr, // 获取位置信息需要位置权限
);
},
},
{
'start': '对小言说:“明天下午3点创建会议提醒”',
'input': '明天下午3点创建会议提醒',
'send': '小言会为你创建会议提醒',
'end': '小言已成功为您创建会议提醒,试试其他指令吧',
'method': () async {
//创建会议提醒需要日历权限
await PermissionUtil.instance.requestPermission(
permissionType: Permission.calendarWriteOnly,
permissionName: 'calendarWrite'.tr, // 日历写入
explanationText: 'calendarWritePermissionDesc'.tr, // 用于创建日历事件和提醒
permanentDenialText:
'calendarWritePermissionDetailDesc'.tr, // 创建日历事件需要日历写入权限
);
},
},
{
'start': '对小言说:“拨打妈妈的电话”',
'input': '拨打妈妈的电话',
'send': '小言会为你拨打电话',
'end': '小言已成功为您拨打电话,试试其他指令吧',
'method': () async {
// 拨打电话需要电话权限
await PermissionUtil.instance.requestPermission(
permissionType: Permission.phone,
permissionName: 'phone'.tr, // 电话
explanationText: 'phonePermissionDesc'.tr, // 用于拨打电话功能
permanentDenialText: 'phonePermissionDetailDesc'.tr, // 拨打电话功能需要电话权限
);
//拨打电话还需要联系人权限
await PermissionUtil.instance.requestPermission(
permissionType: Permission.contacts,
permissionName: 'contacts'.tr, // 联系人
explanationText: 'contactsPermissionDesc'.tr, // 用于访问联系人信息
permanentDenialText:
'contactsPermissionDetailDesc'.tr, // 拨打电话功能需要联系人权限
);
},
},
{
'start': '对小言说:“给爸爸发短信说晚上回家吃饭’”',
'input': '给爸爸发短信说晚上回家吃饭’',
'send': '小言会为你发送短信',
'end': '短信已发送,语音操控更便捷',
'method': () async {
// 发送短信需要短信权限
await PermissionUtil.instance.requestPermission(
permissionType: Permission.sms,
permissionName: 'sms'.tr, // 短信
explanationText: 'smsPermissionDesc'.tr, // 用于发送短信功能
permanentDenialText: 'smsPermissionDetailDesc'.tr, // 发送短信功能需要短信权限
);
//发送短信还需要联系人权限
await PermissionUtil.instance.requestPermission(
permissionType: Permission.contacts,
permissionName: 'contacts'.tr, // 联系人
explanationText: 'contactsPermissionDesc'.tr, // 用于访问联系人信息
permanentDenialText:
'contactsPermissionDetailDesc'.tr, // 发送短信功能需要联系人权限
);
},
},
];
@override
void onInit() {
super.onInit();
// isHomeGuide = _storage.read('home_guide') ?? false;
// isAIGuide = _storage.read('ai_guide') ?? false;
isHomeGuide = _storage.read('home_guide') ?? false;
isAIGuide = _storage.read('ai_guide') ?? false;
}
@override

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

@ -3,6 +3,7 @@ import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'package:amap_service/amap_service.dart';
import 'package:flutter_intro/flutter_intro.dart';
import '../../../data/services/ble_manager.dart';
import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/novice_guide_service.dart';
@ -123,6 +124,7 @@ class AgentController extends GetxController {
Timer? _progressTimer;
// 用于跟踪AI回复状态
bool _isNewAssistantResponse = true;
RxBool newAIReplyCompleted = true.obs;
String _currentResponseId = '';
final TAG = 'AgentController';
@ -158,6 +160,7 @@ class AgentController extends GetxController {
_setupScrollListener(); //设置滚动监听器
_loadChatHistory(); //加载聊天历史记录
_subscribeToEvents(); //订阅代理服务事件
// _subscribeToBleEvents(); //订阅BLE事件
_initMusicStatus(); //初始化音乐状态
_setupMediaSessionCallbacks(); //设置媒体会话回调
_setupTextInputListener(); //设置文本输入监听器
@ -207,6 +210,7 @@ class AgentController extends GetxController {
_eventSubscription?.cancel();
_amapeventSubscription?.cancel();
_bleEventSubscription?.cancel();
Intro.of(Get.context!).dispose();
super.onClose();
}
@ -635,6 +639,7 @@ class AgentController extends GetxController {
isImageProcessing.value = false;
// 标记当前回复完成
_isNewAssistantResponse = true;
newAIReplyCompleted.value = true;
break;
case AgentServiceEventType.error:
@ -892,6 +897,7 @@ class AgentController extends GetxController {
isProcessing.value = true;
// 标记为新的AI回复
_isNewAssistantResponse = true;
newAIReplyCompleted.value = false;
// 根据TTS状态决定是否朗读
await AgentService.processTextInput(text,
speakResponse: isTtsEnabled.value);
@ -1336,7 +1342,7 @@ class AgentController extends GetxController {
// 打电话模式
isTextInputMode.value = false;
Logger.i(TAG, '打电话模式已启动,开始语音输入');
await AgentService.startConversation();
await AgentService.startConversation(mode: 'phone_call');
}
// 结束打电话模式
@ -1388,7 +1394,7 @@ class AgentController extends GetxController {
} else {
// 当前未录音,开始录音
Logger.i(TAG, '开始打电话模式录音');
final result = await AgentService.startConversation();
final result = await AgentService.startConversation(mode: 'phone_call');
// 手动更新状态,确保UI同步
if (result) {
@ -1492,7 +1498,7 @@ class AgentController extends GetxController {
isPushToTalkActive.value = true;
//await AgentService.recognizeCallback();
// 开始语音识别
await AgentService.startConversation();
await AgentService.startConversation(mode: 'push_to_talk');
// await startVoiceInput();
}
@ -1756,21 +1762,52 @@ class AgentController extends GetxController {
// 打开图片查看器
void openImageViewer(BuildContext context, String imagePath) {
// 构建完整的图片路径
String fullImagePath;
if (imagePath.startsWith('/')) {
// 如果是相对路径(以/开头),需要拼接AppDir.value
fullImagePath = '${AppDir.value}$imagePath';
} else {
// 如果已经是完整路径,直接使用
fullImagePath = imagePath;
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(
backgroundColor: Colors.black,
iconTheme: IconThemeData(color: Colors.white),
),
// appBar: AppBar(
// backgroundColor: Colors.black,
// leading: IconButton(
// icon: Icon(Icons.arrow_back_ios, color: Colors.white),
// onPressed: () => Navigator.of(context).pop(),
// ),
// iconTheme: IconThemeData(color: Colors.white),
// //title: Text('图片预览', style: TextStyle(color: Colors.white)),
// ),
body: Container(
color: Colors.black,
child: PhotoView(
imageProvider: FileImage(File(imagePath)),
minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 2,
backgroundDecoration: BoxDecoration(color: Colors.black),
child: GestureDetector(
onTap: () => Navigator.of(context).pop(), // 点击退出
child: PhotoView(
imageProvider: FileImage(File(fullImagePath)),
minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 2,
backgroundDecoration: BoxDecoration(color: Colors.black),
// errorBuilder: (context, error, stackTrace) {
// return const Center(
// child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Icon(Icons.error, color: Colors.white, size: 64),
// SizedBox(height: 16),
// Text('图片加载失败', style: TextStyle(color: Colors.white)),
// ],
// ),
// );
// },
),
),
),
),

163
lib/modules/agent/views/agent_view.dart

@ -193,62 +193,108 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
),
body: Stack(
children: [
IntroStepBuilder(
group: 'ai',
order: 3,
getOverlayPosition: ({
required Offset offset,
required Size screenSize,
required Size size,
}) {
return OverlayPosition(
top: 200.h,
width: screenSize.width,
crossAxisAlignment: CrossAxisAlignment.end,
);
},
overlayBuilder: (params) {
return Obx(
() => IntroOverlayBuilder.homeBuilder(
params,
false,
title: controller.newAIReplyCompleted.value
? '小言已经完成了回答!'
: '小言正在思考中~',
onNext: controller.newAIReplyCompleted.value ? () {} : null,
),
);
},
builder: (BuildContext context, GlobalKey key) => SizedBox(
key: key,
),
),
// 主要聊天界面
Column(
children: [
// 聊天内容
Expanded(
child: Stack(
children: [
IntroStepBuilder(
group: 'ai',
order: 3,
overlayBuilder: (params) {
return IntroOverlayBuilder.aiBuilder(
params,
controller.noviceGuideService.aiStepsList[
controller.noviceGuideService.aiStep]['end'],
onFinish: () async {
if (controller.noviceGuideService.aiStep < 5) {
await Future.delayed(
const Duration(milliseconds: 300));
controller.noviceGuideService.aiStep++;
Intro.of(Get.context!)
.start(group: 'ai', reset: true);
}
},
);
},
builder: (BuildContext context, GlobalKey key) => Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
key: key,
width: double.infinity,
height: 430.w,
),
),
),
Obx(
() => Align(
alignment: Alignment.topCenter,
child: ListView.builder(
controller: controller.scrollController,
shrinkWrap: true,
reverse: true,
padding: EdgeInsets.symmetric(
vertical: 16.h, horizontal: 16.w),
itemCount: controller.messages.length +
(controller.hasMoreMessages.value ? 1 : 0),
itemBuilder: (context, index) {
// 如果是最后一项且有更多消息,显示加载指示器
if (index == controller.messages.length &&
controller.hasMoreMessages.value) {
return _buildLoadMoreIndicator();
}
child: Obx(() {
return Align(
alignment: Alignment.topCenter,
child: ListView.builder(
controller: controller.scrollController,
shrinkWrap: true,
reverse: true,
padding: EdgeInsets.symmetric(
vertical: 16.h, horizontal: 16.w),
itemCount: controller.messages.length +
(controller.hasMoreMessages.value ? 1 : 0),
itemBuilder: (context, index) {
// 如果是最后一项且有更多消息,显示加载指示器
if (index == controller.messages.length &&
controller.hasMoreMessages.value) {
return _buildLoadMoreIndicator();
}
final message = controller.messages[
controller.messages.length - 1 - index];
return Padding(
final message = controller
.messages[controller.messages.length - 1 - index];
return index == 0
? IntroStepBuilder(
group: 'ai',
order: 4,
overlayBuilder: (params) {
return IntroOverlayBuilder.aiBuilder(
params,
controller.noviceGuideService.aiStepsList[
controller
.noviceGuideService.aiStep]['end'],
onFinish: () async {
if (controller.noviceGuideService.aiStep <
5) {
await Future.delayed(
const Duration(milliseconds: 300));
controller.noviceGuideService.aiStep++;
Intro.of(Get.context!)
.start(group: 'ai', reset: true);
}
},
);
},
builder:
(BuildContext context, GlobalKey key) =>
Padding(
key: key,
padding: EdgeInsets.only(bottom: 8.h),
child: GestureDetector(
onLongPressStart:
(LongPressStartDetails details) {
messageActionController
.onLongPressMessage(context,
isDarkMode, details, message);
},
child: MessageBubble(
isUser: message.isUser,
message: message.text,
timestamp: message.timestamp,
isRecognizing: message.isRecognizing,
hasImage: message.hasImage,
imagePath: message.imagePath,
meta: message.meta,
),
),
),
)
: Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: GestureDetector(
onLongPressStart:
@ -265,13 +311,12 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
imagePath: message.imagePath,
meta: message.meta,
),
));
},
),
),
),
);
},
),
],
),
);
}),
),
// 输入区域
@ -685,7 +730,11 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
return IntroStepBuilder(
group: 'ai',
order: 2,
onHighlightWidgetTap: () {
onHighlightWidgetTap: () async {
await controller
.noviceGuideService.aiStepsList[
controller.noviceGuideService
.aiStep]['method']();
widgetParams!.onNext!();
controller.sendMessage();
},

101
lib/modules/agent/views/weather_card.dart

@ -25,17 +25,7 @@ Widget buildWeatherContent(BuildContext context, String city,
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
gradient: isDarkMode
? const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF1E40AF), Color(0xFF1E3A8A)],
)
: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF60A5FA), Color(0xFF3B82F6)],
),
gradient: _getWeatherGradient(now['dayweather'] ?? '', isDarkMode),
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
@ -63,7 +53,7 @@ Widget buildWeatherContent(BuildContext context, String city,
color: Colors.white,
),
),
SizedBox(height: 4),
SizedBox(height: 4.w),
Text(
_getWeatherDescriptionTranslation(now["dayweather"]),
style: TextStyle(
@ -483,3 +473,90 @@ IconData getWeatherIcon(String description) {
// 默认或未知天气
return Icons.help_outline;
}
/// 根据天气类型获取背景渐变颜色
LinearGradient _getWeatherGradient(String weather, bool isDarkMode) {
final weatherType = weather.trim();
// 晴天 - 温暖的橙黄色
if (['晴', '少云', '晴间多云'].any((sunny) => weatherType.contains(sunny))) {
return LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDarkMode
? [Color(0xFFFF8A00), Color(0xFFE65100)] // 深色模式:深橙色
: [Color(0xFFFFB74D), Color(0xFFFF9800)], // 浅色模式:亮橙色
);
}
// 多云/阴天 - 灰蓝色
if (['多云', '阴'].any((cloudy) => weatherType.contains(cloudy))) {
return LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDarkMode
? [Color(0xFF455A64), Color(0xFF37474F)] // 深色模式:深灰蓝
: [Color(0xFF78909C), Color(0xFF607D8B)], // 浅色模式:灰蓝色
);
}
// 雨天 - 深蓝色
if (weatherType.contains('雨') ||
['阵雨', '雷阵雨', '小雨', '中雨', '大雨', '暴雨']
.any((rain) => weatherType.contains(rain))) {
return LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDarkMode
? [Color(0xFF1565C0), Color(0xFF0D47A1)] // 深色模式:深蓝色
: [Color(0xFF42A5F5), Color(0xFF1976D2)], // 浅色模式:亮蓝色
);
}
// 雪天 - 冷色调蓝白色
if (weatherType.contains('雪') ||
['阵雪', '小雪', '中雪', '大雪', '暴雪']
.any((snow) => weatherType.contains(snow))) {
return LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDarkMode
? [Color(0xFF0277BD), Color(0xFF01579B)] // 深色模式:深冷蓝
: [Color(0xFF81D4FA), Color(0xFF29B6F6)], // 浅色模式:浅蓝色
);
}
// 雾霾天 - 暗黄灰色
if (weatherType.contains('雾') ||
weatherType.contains('霾') ||
['雾', '浓雾', '霾', '中度霾', '重度霾'].any((fog) => weatherType.contains(fog))) {
return LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDarkMode
? [Color(0xFF5D4037), Color(0xFF3E2723)] // 深色模式:深棕色
: [Color(0xFF8D6E63), Color(0xFF6D4C41)], // 浅色模式:棕灰色
);
}
// 大风天 - 青绿色
if (weatherType.contains('风') ||
['大风', '强风', '疾风', '烈风'].any((wind) => weatherType.contains(wind))) {
return LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDarkMode
? [Color(0xFF00695C), Color(0xFF004D40)] // 深色模式:深青色
: [Color(0xFF26A69A), Color(0xFF00796B)], // 浅色模式:青绿色
);
}
// 默认 - 原来的蓝色
return LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDarkMode
? [Color(0xFF1E40AF), Color(0xFF1E3A8A)]
: [Color(0xFF60A5FA), Color(0xFF3B82F6)],
);
}

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

@ -26,22 +26,6 @@ enum LoginViewState {
verification // 验证码页面
}
// 用户渠道枚举
// 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();
final GetStorage _storage = GetStorage();
@ -354,9 +338,32 @@ class LoginController extends GetxController {
// 获取验证码
void getVerificationCode() {
if (contactController.text.isEmpty) {
Get.snackbar(
'tip'.tr, //提示
'pleaseEnterContactInfo'.tr, //请输入手机号或邮箱
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
return;
}
// 根据登录类型验证格式
if (loginType == 0 && !isEmailContactController()) {
Get.snackbar(
'tip'.tr,
'pleaseEnterContactInfo'.tr,
'invalidEmailFormat'.tr,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
return;
}
if (loginType == 1 && !isPhoneContactController()) {
Get.snackbar(
'tip'.tr,
'invalidPhoneFormat'.tr,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
@ -371,40 +378,6 @@ class LoginController extends GetxController {
// 使用验证码直接登录
void loginWithVerificationCode() {
//测试期间暂时关闭验证码登录
// if (contactController.text.isEmpty) {
// Get.snackbar(
// '提示',
// '请输入手机号码或邮箱',
// snackPosition: SnackPosition.TOP,
// backgroundColor: Colors.red.withOpacity(0.1),
// colorText: Colors.red,
// );
// return;
// }
// if (verificationCodeController.text.isEmpty) {
// Get.snackbar(
// '提示',
// '请输入验证码',
// snackPosition: SnackPosition.TOP,
// backgroundColor: Colors.red.withOpacity(0.1),
// colorText: Colors.red,
// );
// return;
// }
// if (verificationCodeController.text.length < 6) {
// Get.snackbar(
// '提示',
// '请输入完整的验证码',
// snackPosition: SnackPosition.TOP,
// backgroundColor: Colors.red.withOpacity(0.1),
// colorText: Colors.red,
// );
// return;
// }
_isLoading.value = true;
// 模拟验证过程
@ -481,9 +454,8 @@ class LoginController extends GetxController {
// 发送验证码
Future<void> _sendVerificationCode() async {
// 这里应该是实际发送验证码的逻辑
// 为了演示,我们只是模拟发送
if (isEmialContactController() || isPhoneContactController()) {
// 实际发送验证码的逻辑
if (isEmailContactController() || isPhoneContactController()) {
_startCountdown();
Logger().d(contactController.text);
var response = await Api.getCode({
@ -497,8 +469,8 @@ class LoginController extends GetxController {
}
} else {
Get.snackbar(
'tip'.tr,
'pleaseEnterContactInfo'.tr,
'tip'.tr, //提示
'pleaseEnterContactInfo'.tr, //请输入手机号或邮箱
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
@ -530,19 +502,39 @@ class LoginController extends GetxController {
}
// 检查邮箱格式
bool isEmialContactController() {
bool isEmailContactController() {
// 检查每个元素是否为单个字母或数字字符
final text = contactController.text;
return (text.length > 5 &&
RegExp(r'^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$')
.hasMatch(text));
final text = contactController.text.trim();
if (text.length < 6) return false;
// 更严格的邮箱验证
final emailRegex =
RegExp(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$');
return emailRegex.hasMatch(text);
}
// 检查手机号码格式
// 检查手机号码格式
bool isPhoneContactController() {
// 检查每个元素是否为单个字母或数字字符
final text = contactController.text;
return RegExp(r'^[0-9]+$').hasMatch(text);
final text = contactController.text.trim();
if (text.isEmpty) return false;
// 根据国家代码验证手机号长度
// 长度检查
if (text.length < 7 || text.length > 15) return false;
// 必须是纯数字
if (!RegExp(r'^[0-9]+$').hasMatch(text)) return false;
// 特定国家的特殊验证
switch (_selectedCountryCode.value) {
case '+86': // 中国
return text.length == 11 && text.startsWith('1');
case '+1': // 美国/加拿大
return text.length == 10;
case '+81': // 日本
return text.length == 10 || text.length == 11;
default:
return true; // 其他国家只做基本验证
}
}
// 检查验证码是否完整
@ -570,7 +562,7 @@ class LoginController extends GetxController {
}
// 验证邮箱格式
if (loginType == 0 && !isEmialContactController()) {
if (loginType == 0 && !isEmailContactController()) {
Get.snackbar(
'error'.tr,
'invalidEmailFormat'.tr,

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

@ -192,7 +192,18 @@ class SettingsController extends GetxController {
},
{'type': Permission.bluetooth, 'name': '蓝牙', 'description': '用于连接蓝牙设备'}
]
}
},
{
'name': '闹钟权限',
'icon': Icons.alarm,
'permissions': [
{
'type': Permission.scheduleExactAlarm,
'name': '闹钟',
'description': '用于设置精确闹钟'
}
]
},
];
// 检查单个权限状态
@ -453,7 +464,8 @@ class SettingsController extends GetxController {
Logger().i('更新agent服务语音识别语言: ${supportedLanguages.join(', ')}');
// 调用agent服务更新语言设置
final success = await AgentService.setSupportedLanguages(supportedLanguages);
final success =
await AgentService.setSupportedLanguages(supportedLanguages);
if (success) {
Logger().i('agent服务语言设置更新成功');
} else {

44
lib/modules/settings/views/permissions_view.dart

@ -4,6 +4,8 @@ import 'package:permission_handler/permission_handler.dart';
import '../controllers/settings_controller.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:ui';
import 'dart:io' show Platform;
import '../../../core/utils/permission_util.dart'; // 引入PermissionUtil
class PermissionsView extends GetView<SettingsController> {
@ -37,7 +39,7 @@ class PermissionsView extends GetView<SettingsController> {
centerTitle: true,
),
body: FutureBuilder<Map<Permission, PermissionStatus>>(
future: controller.checkAllPermissions(),
future: controller.checkAllPermissions(), //检查权限当前状态
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
@ -45,8 +47,8 @@ class PermissionsView extends GetView<SettingsController> {
if (snapshot.hasError) {
return Center(
child: Text('getPermissionStatusFailed'.tr +
': ${snapshot.error}')); // 获取权限状态失败
child: Text(
'${'getPermissionStatusFailed'.tr}: ${snapshot.error}')); // 获取权限状态失败
}
final permissionStatuses = snapshot.data ?? {};
@ -54,15 +56,27 @@ class PermissionsView extends GetView<SettingsController> {
return ListView(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
children: [
// 通讯权限组
_buildPermissionSection(
'communicationPermissions'.tr, // 通讯权限
Icons.phone_android,
[Permission.phone, Permission.sms],
// 通讯权限组- iOS平台没有通讯权限
if (Platform.isAndroid) ...[
_buildPermissionSection(
'communicationPermissions'.tr, // 通讯权限
Icons.phone_android,
[Permission.phone, Permission.sms],
permissionStatuses,
isDarkMode),
SizedBox(height: 20.h),
],
// 闹钟权限组
if (Platform.isAndroid) ...[
_buildPermissionSection(
'alarmPermissions'.tr, // 闹钟权限
Icons.alarm,
[Permission.scheduleExactAlarm],
permissionStatuses,
isDarkMode),
SizedBox(height: 20.h),
isDarkMode,
),
SizedBox(height: 20.h),
],
// 日历权限组
_buildPermissionSection(
@ -162,6 +176,8 @@ class PermissionsView extends GetView<SettingsController> {
return 'calendarFullAccess'.tr; // 日历完整访问
case Permission.location:
return 'locationInfo'.tr; // 位置信息
case Permission.scheduleExactAlarm:
return 'alarm'.tr; // 闹钟
default:
return permission.toString();
}
@ -180,6 +196,8 @@ class PermissionsView extends GetView<SettingsController> {
return 'calendarFullAccessPermissionDesc'.tr; // 用于读取和管理日历事件及提醒
case Permission.location:
return 'locationPermissionDesc'.tr; // 用于获取当前位置信息
case Permission.scheduleExactAlarm:
return 'alarmPermissionDesc'.tr; // 设置精确闹钟需要此权限
default:
return 'appFunctionNeeds'.tr; // 应用功能需要
}
@ -355,6 +373,8 @@ class PermissionsView extends GetView<SettingsController> {
return 'calendarFullAccess'.tr; // 日历完整访问
case Permission.location:
return 'locationInfo'.tr; // 位置信息
case Permission.scheduleExactAlarm:
return 'alarm'.tr; // 闹钟
default:
return permission.toString();
}
@ -373,6 +393,8 @@ class PermissionsView extends GetView<SettingsController> {
return 'calendarFullAccessPermissionDetailDesc'.tr; // 管理日历事件需要完整日历权限
case Permission.location:
return 'locationPermissionDetailDesc'.tr; // 获取位置信息需要位置权限
case Permission.scheduleExactAlarm:
return 'alarmPermissionDetailDesc'.tr; // 设置精确闹钟需要此权限
default:
return 'appNeedsPermissionForFullFunction'.tr; // 应用需要此权限以提供完整功能
}

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

@ -117,7 +117,10 @@ object AgentService : CoroutineScope {
// 空闲检测相关
private var idleCheckJob: Job? = null
private val maxIdleSeconds = 10 // 最大空闲秒数
private val maxIdleSeconds = 10 // 最大空闲秒数(默认值)
// 语音识别模式
private var currentRecognitionMode = "normal" // 当前识别模式:normal, ble_wakeup, phone_call, push_to_talk
// 打断防抖相关
private var lastInterruptTime = 0L
@ -409,22 +412,63 @@ object AgentService : CoroutineScope {
listeners.clear()
}
/**
* 根据模式启动空闲检测
*/
private fun startIdleCheckForMode(mode: String) {
stopIdleCheck() // 先停止现有的检查
if (!isRecognitionActive) return
// 根据模式决定空闲检测策略
when (mode) {
"phone_call" -> {
// 通话模式:禁用空闲检测,保持持续激活
Log.d(TAG, "通话模式:禁用空闲检测")
return
}
"ble_wakeup" -> {
// BLE唤醒模式:使用默认的空闲检测
startIdleCheck(maxIdleSeconds)
}
"push_to_talk" -> {
// 按住说话模式:使用较长的空闲检测时间
startIdleCheck(maxIdleSeconds)
}
"normal" -> {
// 普通模式:使用默认的空闲检测
startIdleCheck(maxIdleSeconds)
}
else -> {
// 未知模式:使用默认的空闲检测
Log.w(TAG, "未知识别模式: $mode,使用默认空闲检测")
startIdleCheck(maxIdleSeconds)
}
}
}
/**
* 启动空闲检测
*/
private fun startIdleCheck() {
private fun startIdleCheck(idleSeconds: Int = maxIdleSeconds) {
stopIdleCheck() // 先停止现有的检查
if (!isRecognitionActive) return
Log.d(TAG, "启动空闲检测,超时时间: ${idleSeconds}秒,模式: $currentRecognitionMode")
idleCheckJob = launch {
try {
delay(maxIdleSeconds * 1000L)
delay(idleSeconds * 1000L)
if (isActive && isRecognitionActive && !isTtsSpeaking && !isAiStreaming) {
Log.d(TAG, "空闲检测触发自动停止,模式: $currentRecognitionMode")
stopRecognition()
sendEvent(
"auto_stop",
mapOf("reason" to "idle_timeout", "seconds" to maxIdleSeconds)
mapOf(
"reason" to "idle_timeout",
"seconds" to idleSeconds,
"mode" to currentRecognitionMode
)
)
}
} catch (e: CancellationException) {
@ -449,14 +493,16 @@ object AgentService : CoroutineScope {
*/
private fun restartIdleCheck() {
if (isRecognitionActive) {
startIdleCheck()
startIdleCheckForMode(currentRecognitionMode)
}
}
/**
* 开始语音识别
* @param isExternalActive 是否使用外部音频源(蓝牙)
* @param mode 识别模式:normal, ble_wakeup, phone_call, push_to_talk
*/
fun startRecognition(isExternalActive: Boolean): Boolean {
fun startRecognition(isExternalActive: Boolean, mode: String = "normal"): Boolean {
if (!isInitialized) {
Log.d(TAG, "服务未初始化")
return false
@ -488,6 +534,10 @@ object AgentService : CoroutineScope {
return false
}
// 设置当前识别模式
currentRecognitionMode = mode
Log.d(TAG, "设置语音识别模式: $mode")
_isRecognitionActive.set(true)
_hasSpeechDetected.set(false)
@ -500,6 +550,9 @@ object AgentService : CoroutineScope {
Log.d("TAG", "azureAsrHelper?.startContinuousRecognition")
azureAsrHelper?.startContinuousRecognition(audioSourceType)
// 根据模式启动相应的空闲检测
startIdleCheckForMode(mode)
return true
} catch (e: Exception) {
_isRecognitionActive.set(false)
@ -586,7 +639,8 @@ object AgentService : CoroutineScope {
override fun onSessionStarted() {
sendEvent("recognition_started", mapOf("status" to "started"))
startIdleCheck()
// 使用模式感知的空闲检测启动方法
startIdleCheckForMode(currentRecognitionMode)
audioPlayer?.playAudio(R.raw.start)
}
@ -644,16 +698,24 @@ object AgentService : CoroutineScope {
fun stopRecognition() {
if (!isRecognitionActive) return
Log.d(TAG, "停止语音识别,当前模式: $currentRecognitionMode")
try {
azureAsrHelper?.stopContinuousRecognition()
BleService.closeCodec()
_isRecognitionActive.set(false)
stopIdleCheck()
// 重置识别模式
currentRecognitionMode = "normal"
return
} catch (e: Exception) {
Log.e(TAG, "停止语音识别异常: ${e.message}")
_isRecognitionActive.set(false)
stopIdleCheck()
// 重置识别模式
currentRecognitionMode = "normal"
return
}
}

3
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt

@ -115,7 +115,8 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
"startConversation" -> {
try {
val isStarted = AgentService.startRecognition(false)
val mode = call.argument<String>("mode") ?: "normal"
val isStarted = AgentService.startRecognition(false, mode)
result.success(isStarted)
} catch (e: Exception) {
Log.e(TAG, "启动对话失败", e)

8
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt

@ -66,7 +66,7 @@ object BleAgent : BleService.Callback, AgentServiceListener {
return false
}
Log.d(TAG, "手动启动语音识别: ")
return AgentService.startRecognition(true)
return AgentService.startRecognition(true, "ble_wakeup")
}
/**
@ -148,11 +148,11 @@ Log.d(TAG, "手动启动语音识别: ")
// 停止当前TTS,避免冲突
AgentService.interruptCurrentResponse()
// AgentService.speakText("我在!")
// 启动语音识别
// 启动语音识别,使用BLE唤醒模式
// 如果已经在识别,则重新启动
//stopListening()
//stopListening()
startListening()

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

@ -65,6 +65,9 @@ class AgentServiceImpl: NSObject {
private var idleTimer: DispatchSourceTimer?
private let maxIdleSeconds: TimeInterval = 10
// 语音识别模式
private var currentRecognitionMode: String = "normal"
internal var audioPlayer: AudioPlayer?
// private var locationInfo:[String: String]?
@ -280,26 +283,56 @@ class AgentServiceImpl: NSObject {
return true
}
private func startIdleCheck() {
private func startIdleCheckForMode(mode: String) {
stopIdleCheck()
guard isRecognizing else { return }
// 根据模式决定空闲检测策略
switch mode {
case "phone_call":
// 通话模式:禁用空闲检测,保持持续激活
os_log("通话模式:禁用空闲检测", log: logger, type: .info)
return
case "ble_wakeup":
// BLE唤醒模式:使用默认的空闲检测
startIdleCheck(idleSeconds: maxIdleSeconds)
case "push_to_talk":
// 按住说话模式:使用默认的空闲检测
startIdleCheck(idleSeconds: maxIdleSeconds)
case "normal":
// 普通模式:使用默认的空闲检测
startIdleCheck(idleSeconds: maxIdleSeconds)
default:
// 未知模式:使用默认的空闲检测
os_log("未知识别模式: %{public}@,使用默认空闲检测", log: logger, type: .error, mode)
startIdleCheck(idleSeconds: maxIdleSeconds)
}
}
private func startIdleCheck(idleSeconds: TimeInterval = 10) {
stopIdleCheck()
guard isRecognizing else { return }
os_log("启动空闲检测,超时时间: %.0f秒,模式: %{public}@", log: logger, type: .info, idleSeconds, currentRecognitionMode)
// 使用全局队列而不是主队列,避免后台挂起问题
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .utility))
idleTimer = timer
timer.schedule(deadline: .now() + maxIdleSeconds)
timer.schedule(deadline: .now() + idleSeconds)
timer.setEventHandler { [weak self] in
// 使用更安全的方式检查self是否存在
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if self.isRecognizing && !self.isSpeaking && !self.isAiStreaming {
os_log("空闲检测触发自动停止,模式: %{public}@", log: self.logger, type: .info, self.currentRecognitionMode)
self.stopRecognition()
self.sendEvent(name: "auto_stop", data: [
"reason": "idle_timeout",
"seconds": self.maxIdleSeconds
"reason": "idle_timeout",
"seconds": idleSeconds,
"mode": self.currentRecognitionMode
])
}
}
@ -314,10 +347,10 @@ class AgentServiceImpl: NSObject {
private func restartIdleCheck() {
if isRecognizing {
startIdleCheck()
startIdleCheckForMode(mode: currentRecognitionMode)
}
}
func startRecognition(useBle: Bool = false) -> Bool {
func startRecognition(useBle: Bool = false, mode: String = "normal") -> Bool {
if !isInitialized {
sendError("服务未初始化", code: "NOT_INITIALIZED")
return false
@ -333,9 +366,13 @@ class AgentServiceImpl: NSObject {
if isRecognizing {
stopRecognition()
}
// 设置当前识别模式
currentRecognitionMode = mode
os_log("设置语音识别模式: %{public}@", log: logger, type: .info, mode)
let audioSourceType: AzureAsrHelper.AudioSourceType = useBle ? .external : .microphone
if useBle {
AudioSessionHub.shared.begin(.playback)
} else {
@ -349,7 +386,10 @@ print("ai启动语音=\(audioSourceType)=\(useBle)")
sendError("启动语音识别失败", code: "RECOGNITION_START_ERROR")
return false
}
// 根据模式启动相应的空闲检测
startIdleCheckForMode(mode: mode)
return true
}
func recognizeCallback() -> Bool {
@ -367,11 +407,18 @@ print("ai启动语音=\(audioSourceType)=\(useBle)")
if !isRecognizing {
return true
}
os_log("停止语音识别,当前模式: %{public}@", log: logger, type: .info, currentRecognitionMode)
guard let success = azureAsrHelper?.stopContinuousRecognition(), success else {
return false
}
stopIdleCheck()
// 重置识别模式
currentRecognitionMode = "normal"
return true
}
func disableBluetoothAudio() {
@ -1388,10 +1435,10 @@ extension AgentServiceImpl: BleService.Callback {
func onWakeupSignalReceived() {
stopTts()
stopRecognition()
startRecognition(useBle: true)
startRecognition(useBle: true, mode: "ble_wakeup")
}
func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) {
@ -1620,7 +1667,8 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback {
sendEvent(name: "recognition_started", data: ["status": "started"])
isRecognizing = true
hasSpeechDetected = false
startIdleCheck()
// 使用模式感知的空闲检测启动方法
startIdleCheckForMode(mode: currentRecognitionMode)
audioPlayer?.playStartSound()
}

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

@ -41,7 +41,8 @@ public class AgentServicePlugin: NSObject, FlutterPlugin {
result(success)
case "startConversation":
impl.startRecognition()
let mode = (call.arguments as? [String: Any])?["mode"] as? String ?? "normal"
impl.startRecognition(mode: mode)
result(true)
case "recognizeCallback":

7
local_plugins/agent_service/lib/agent_service.dart

@ -238,10 +238,13 @@ class AgentService {
/// 开始对话
///
/// 启动语音识别,开始监听用户语音输入
/// [mode] 语音识别模式:'ble_wakeup'(BLE唤醒), 'phone_call'(通话模式), 'push_to_talk'(按住说话), 'normal'(普通模式)
/// 返回是否成功开始对话
static Future<bool> startConversation() async {
static Future<bool> startConversation({String mode = 'normal'}) async {
try {
final bool result = await _channel.invokeMethod('startConversation');
final bool result = await _channel.invokeMethod('startConversation', {
'mode': mode,
});
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '启动对话失败', e.details);

57
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt

@ -22,7 +22,8 @@ import java.util.concurrent.atomic.AtomicBoolean
*/
class CustomSseClientTransport(
private val client: HttpClient,
private val urlString: String?,
private val serviceidString: String?,
public val urlString: String?,
private val reconnectionTime: Duration? = null,
private val requestBuilder: HttpRequestBuilder.() -> Unit = {},
private val onConnectionLost: (() -> Unit)? = null
@ -88,7 +89,7 @@ class CustomSseClientTransport(
Triple(hostUrl, path, params)
} catch (e: Exception) {
Log.e(TAG, "解析URL失败: $url, ${e.message}")
Log.e(TAG, "$serviceidString 解析URL失败: $url, ${e.message}")
Triple(url, "", emptyMap())
}
}
@ -102,9 +103,9 @@ class CustomSseClientTransport(
session.incoming.collect { event ->
when (event.event) {
"error" -> {
Log.e(TAG, "SSE错误: ${event.data}")
Log.e(TAG, "$serviceidString SSE错误: ${event.data}")
isConnected.set(false)
val exception = Exception("SSE Error: ${event.data}")
val exception = Exception("$serviceidString SSE Error: ${event.data}")
_onError(exception)
onConnectionLost?.invoke()
throw exception
@ -112,7 +113,7 @@ class CustomSseClientTransport(
"open" -> {
// SSE连接已打开
Log.d(TAG, "SSE连接已打开")
Log.d(TAG, "$serviceidString SSE连接已打开")
isConnected.set(true)
}
"ping" -> {
@ -146,7 +147,7 @@ class CustomSseClientTransport(
endpoint.complete(endpointWithParams)
} catch (e: Exception) {
Log.e(TAG, "处理endpoint事件失败: ${e.message}", e)
Log.e(TAG, "$serviceidString 处理endpoint事件失败: ${e.message}", e)
_onError(e)
close()
error(e)
@ -161,22 +162,22 @@ class CustomSseClientTransport(
val message = json.decodeFromString<JSONRPCMessage>(data)
_onMessage(message)
} catch (e: Exception) {
Log.e(TAG, "解析JSON-RPC消息失败: ${e.message}", e)
Log.e(TAG, "$serviceidString 解析JSON-RPC消息失败: ${e.message}", e)
_onError(e)
}
}
} catch (e: Exception) {
Log.e(TAG, "处理事件失败: ${e.message}", e)
Log.e(TAG, "$serviceidString 处理事件失败: ${e.message}", e)
_onError(e)
}
}
}
}
} catch (e: CancellationException) {
Log.d(TAG, "SSE事件收集被取消")
Log.d(TAG, "$serviceidString SSE事件收集被取消")
throw e
} catch (e: Exception) {
Log.e(TAG, "SSE连接异常断开: ${e.message}", e)
Log.e(TAG, "$serviceidString SSE连接异常断开: ${e.message}", e)
isConnected.set(false)
_onError(e)
onConnectionLost?.invoke()
@ -199,13 +200,13 @@ class CustomSseClientTransport(
// 检查session是否仍然活跃
if (session.coroutineContext[Job]?.isCancelled == true) {
Log.w(TAG, "检测到SSE会话已取消")
Log.w(TAG, "$serviceidString 检测到SSE会话已取消")
isConnected.set(false)
onConnectionLost?.invoke()
break
}
} catch (e: Exception) {
Log.e(TAG, "连接监控异常: ${e.message}", e)
Log.e(TAG, "$serviceidString 连接监控异常: ${e.message}", e)
isConnected.set(false)
onConnectionLost?.invoke()
break
@ -219,7 +220,7 @@ class CustomSseClientTransport(
*/
override suspend fun start() {
if (!initialized.compareAndSet(false, true)) {
Log.e(TAG, "传输层已经启动,不能重复启动")
Log.e(TAG, "$serviceidString 传输层已经启动,不能重复启动")
error("CustomSseClientTransport already started!")
}
@ -283,12 +284,30 @@ class CustomSseClientTransport(
setBody(jsonString)
}
if (!response.status.isSuccess()) {
val text = response.bodyAsText()
// Log.e(TAG, "发送消息失败:URL:${urlString} HTTP ${response.status}, $text")
// 优化处理百度API的202状态码
when {
response.status.isSuccess() -> {
// 2xx状态码都视为成功
Log.d(TAG, "$serviceidString 消息发送成功: HTTP ${response.status}")
}
response.status == HttpStatusCode.Accepted -> {
// 202 Accepted - 百度API异步处理中,这是正常状态
Log.d(TAG, "$serviceidString 消息已被接受,正在异步处理: HTTP ${response.status}")
}
else -> {
// 其他非成功状态码才记录为错误
val text = response.bodyAsText()
Log.w(TAG, "$serviceidString 发送消息收到非成功状态码: URL:${messageEndpoint} HTTP ${response.status}, $text")
// 根据具体状态码决定是否抛出异常
if (response.status.value >= 400) {
// 4xx和5xx错误才抛出异常
throw Exception("HTTP ${response.status}: $text")
}
}
}
} catch (e: Exception) {
Log.e(TAG, "发送消息异常: ${e.message}", e)
Log.e(TAG, "$serviceidString 发送消息异常: ${e.message}", e)
_onError(e)
throw e
}
@ -306,7 +325,7 @@ class CustomSseClientTransport(
*/
override suspend fun close() {
if (!initialized.get()) {
Log.e(TAG, "关闭失败: 传输层未初始化")
Log.e(TAG, "$serviceidString 关闭失败: 传输层未初始化")
error("CustomSseClientTransport is not initialized!")
}
@ -317,6 +336,6 @@ class CustomSseClientTransport(
job?.cancelAndJoin()
connectionMonitorJob?.cancelAndJoin()
Log.d(TAG, "CustomSseClientTransport已关闭")
Log.d(TAG, "$serviceidString CustomSseClientTransport已关闭")
}
}

39
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt

@ -67,45 +67,54 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
Log.w(TAG, "Failed to initialize system functions", e)
}
}
/**
* 连接到SSE服务器
* 直接接收完整的JSON配置字符串
*
*
* @param mcpConfigJson 包含mcpServers字段的JSON配置字符串
* @return 是否连接成功
*/
suspend fun connectToSSE(mcpConfigJson: String): Boolean {
// 清除现有连接
closeAllConnections()
return try {
val config = JSONObject(mcpConfigJson)
val mcpServers = config.optJSONObject("mcpServers") ?: return false
var connectedCount = 0
val serverIds = mcpServers.keys()
while (serverIds.hasNext()) {
val serverId = serverIds.next()
val serverConfig = mcpServers.optJSONObject(serverId) ?: continue
val url = serverConfig.optString("url", "")
val tools = serverConfig.optString("tools", "")
if (url.isEmpty()) continue
val subClient = MCPSubClient(serverId, url,tools, sharedHttpClient)
if (subClient.connect()) {
subClients[serverId] = subClient
connectedCount++
} else {
Log.w(TAG, "Failed to connect to MCP server: $serverId")
Log.d(TAG, "开始连接MCP服务器: $serverId")
val subClient = MCPSubClient(serverId, url, tools, sharedHttpClient)
// 使用协程并发连接,但每个服务器都会进行重试
try {
if (subClient.connect()) {
subClients[serverId] = subClient
connectedCount++
Log.d(TAG, "MCP服务器连接成功: $serverId")
} else {
Log.w(TAG, "MCP服务器连接失败: $serverId")
}
} catch (e: Exception) {
Log.e(TAG, "MCP服务器连接异常: $serverId, ${e.message}", e)
}
}
isConnectedFlag = connectedCount > 0
Log.d(TAG, "MCP连接完成,成功连接 $connectedCount 个服务器")
connectedCount > 0
} catch (e: Exception) {
Log.e(TAG, "Failed to connect to SSE", e)
false

175
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt

@ -33,7 +33,7 @@ class MCPSubClient(
private var heartbeatJob: Job? = null
private val heartbeatInterval = 30000L // 30秒
// 重连配置
private val maxRetryAttempts = 3
private val maxRetryAttempts = 10
private val initialReconnectDelay = 1000L // 初始重连延迟1秒
private val maxReconnectDelay = 30000L // 最大重连延迟30秒
private var currentReconnectDelay = initialReconnectDelay
@ -45,95 +45,122 @@ class MCPSubClient(
private var isConnected = false
private var availableTools = mutableListOf<Tool>()
private var transport: CustomSseClientTransport? = null
/**
* 连接到MCP服务器
*/
suspend fun connect(): Boolean = connectionMutex.withLock {
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 1")
if (isConnected) return true
Log.e(TAG, "[$serverId] 开始连接mcp服务器: $serverUrl")
return try {
// 创建MCP客户端实例
val client = Client(
clientInfo = Implementation(
name = "deep-voice-chat-api",
version = "1.0.0"
// 重试连接逻辑
for (attempt in 0 until maxRetryAttempts) {
try {
Log.d(TAG, "[$serverId] 连接尝试 ${attempt + 1}/$maxRetryAttempts")
// 创建MCP客户端实例
val client = Client(
clientInfo = Implementation(
name = "deep-voice-chat-api",
version = "1.0.0"
)
)
)
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 2")
// 根据URL类型选择传输方式
val newTransport = when {
serverUrl.startsWith("http://") || serverUrl.startsWith("https://") -> {
// SSE传输 - 使用自定义的CustomSseClientTransport
val mcpHttpClient = httpClient ?: createMcpHttpClient()
CustomSseClientTransport(
client = mcpHttpClient,
urlString = serverUrl,
onConnectionLost = {
// 连接断开回调
Log.w(TAG, "[$serverId] 检测到连接断开")
scope.launch {
handleConnectionLost()
// 根据URL类型选择传输方式
val newTransport = when {
serverUrl.startsWith("http://") || serverUrl.startsWith("https://") -> {
// SSE传输 - 使用自定义的CustomSseClientTransport
val mcpHttpClient = httpClient ?: createMcpHttpClient()
CustomSseClientTransport(
client = mcpHttpClient,
serviceidString = serverId,
urlString = serverUrl,
onConnectionLost = {
// 连接断开回调
Log.w(TAG, "[$serverId] 检测到连接断开")
scope.launch {
handleConnectionLost()
}
}
}
)
}
else -> {
Log.e(TAG, "[$serverId] 不支持的服务器URL格式: $serverUrl")
return false
)
}
else -> {
Log.e(TAG, "[$serverId] 不支持的服务器URL格式: $serverUrl")
return false
}
}
}
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 3")
transport = newTransport
try {
// 连接到服务器
withTimeout(10000) { // 10秒超时
client.connect(newTransport)
transport = newTransport
// 连接到服务器 - 增加超时时间
try {
Log.d(TAG, "[$serverId] 尝试建立连接 ${transport?.urlString}")
withTimeout(30000) { // 30秒超时
client.connect(newTransport)
}
Log.d(TAG, "[$serverId] 连接建立成功")
} catch (e: TimeoutCancellationException) {
Log.w(TAG, "[$serverId] 连接超时 (尝试 ${attempt + 1}/$maxRetryAttempts)")
if (attempt < maxRetryAttempts - 1) {
delay(currentReconnectDelay)
currentReconnectDelay = (currentReconnectDelay * 2).coerceAtMost(maxReconnectDelay)
continue // 继续下一次重试
} else {
Log.e(TAG, "[$serverId] 所有连接尝试都超时,连接失败")
return false
}
} catch (e: Exception) {
Log.w(TAG, "[$serverId] 连接异常 (尝试 ${attempt + 1}/$maxRetryAttempts): ${e.message}")
if (attempt < maxRetryAttempts - 1) {
delay(currentReconnectDelay)
currentReconnectDelay = (currentReconnectDelay * 2).coerceAtMost(maxReconnectDelay)
continue // 继续下一次重试
} else {
Log.e(TAG, "[$serverId] 所有连接尝试都失败: ${e.message}", e)
return false
}
}
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 4")
} catch (e: TimeoutCancellationException) {
Log.e(TAG, "liwei---------Mcp [$serverId] 连接超时")
return false
} catch (e: Exception) {
Log.e(TAG, "liwei---------Mcp [$serverId] 连接异常: ${e.message}", e)
return false
}
// 获取可用工具列表
try {
val toolsResult = client.listTools()
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 5")
if (toolsResult != null) {
availableTools.clear()
val filtered = toolsResult.tools.filter { tool ->
filterTools.isEmpty() || filterTools.contains(tool.name)
// 获取可用工具列表
try {
val toolsResult = client.listTools()
if (toolsResult != null) {
availableTools.clear()
val allToolNames = toolsResult.tools.map { it.name }
Log.d(TAG, "$serverId:所有工具名称列表: $allToolNames")
val filtered = toolsResult.tools.filter { tool ->
filterTools.isEmpty() || filterTools.contains(tool.name)
}
val filteredToolNames = filtered.map { it.name }
Log.d(TAG, "$serverId: 过滤后的工具: $filteredToolNames")
availableTools.addAll(filtered)
}
Log.w(TAG, "[$serverId] [${filterTools}] 获取工具列表: ${filtered} 原始列表:${toolsResult.tools}")
availableTools.addAll(filtered)
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 6")
} catch (e: Exception) {
Log.w(TAG, "[$serverId] 获取工具列表失败: ${e.message}")
// 即使获取工具失败,连接也可能是成功的
}
mcpClient = client
isConnected = true
retryCount = 0
currentReconnectDelay = initialReconnectDelay
Log.e(TAG, "[$serverId] 连接mcp服务器成功: $serverUrl")
return true
} catch (e: Exception) {
Log.w(TAG, "[$serverId] 获取工具列表失败: ${e.message}")
// 即使获取工具失败,连接也可能是成功的
Log.w(TAG, "[$serverId] 连接尝试 ${attempt + 1} 失败: ${e.message}")
if (attempt < maxRetryAttempts - 1) {
delay(currentReconnectDelay)
currentReconnectDelay = (currentReconnectDelay * 2).coerceAtMost(maxReconnectDelay)
}
}
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 7")
mcpClient = client
isConnected = true
retryCount = 0
currentReconnectDelay = initialReconnectDelay
// Log.e(TAG, "liwei---------Mcp [$serverId] 连接 8")
// 启动心跳检测
// startHeartbeat()
Log.e(TAG, "[$serverId] 连接mcp服务器成功: $serverUrl")
true
} catch (e: Exception) {
Log.e(TAG, "[$serverId] MCP连接失败: ${e.message}", e)
false
}
Log.e(TAG, "[$serverId] 所有重试尝试都失败,MCP连接失败")
return false
}
/**
* 检查是否包含指定工具
*/

28
local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt

@ -16,6 +16,7 @@ import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.json.JSONObject
object LocationService {
@ -115,6 +116,15 @@ object LocationService {
}
initialized = true
//首次请求一次定位
requestLocation(object : LocationCallback {
override fun onLocationSuccess(latitude: Double, longitude: Double) {
}
override fun onLocationError(errorCode: Int, errorMessage: String) {
}
})
Log.d(TAG, "定位服务初始化完成 - 优化高精度模式")
} catch (e: Exception) {
Log.e(TAG, "高德定位初始化失败: ${e.message}", e)
@ -173,20 +183,12 @@ object LocationService {
}
Log.d(TAG, "定位结果: 来源=${location.provider}, 精度=${location.accuracy}米, 坐标=(${location.latitude}, ${location.longitude})")
// 检查定位来源,只接受GPS定位
if (location.provider != "gps") {
Log.w(TAG, "拒绝非GPS定位,当前来源: ${location.provider}")
// 继续等待GPS定位,不回调
return
}
if (location.errorCode == AMapLocation.LOCATION_SUCCESS) {
val accuracy = location.accuracy
Log.d(TAG, "GPS定位成功: 精度=${accuracy}米")
// Log.d(TAG, "GPS定位成功: 精度=${accuracy}米")
// 只接受高精度GPS定位
if (accuracy <= HIGH_ACCURACY_THRESHOLD) {
// if (accuracy <= HIGH_ACCURACY_THRESHOLD) {
Log.d(TAG, "接受高精度GPS定位: ${accuracy}米")
// 停止超时计时器
@ -220,9 +222,9 @@ object LocationService {
}
// 清空服务回调列表(单次定位)
serviceCallbacks.clear()
} else {
Log.w(TAG, "GPS精度不足(${accuracy}米),继续等待更高精度定位")
}
// } else {
// Log.w(TAG, "GPS精度不足(${accuracy}米),继续等待更高精度定位")
// }
} else {
Log.e(TAG, "GPS定位失败: ${location.errorCode} - ${location.errorInfo}")
}

Loading…
Cancel
Save