wolfplus2048 1 year ago
parent
commit
f11e5f32bb
  1. 94
      .cursorrules
  2. 1
      .gitignore
  3. 15
      android/app/proguard-logging.pro
  4. 5
      android/app/src/main/kotlin/com/yunqiinnovation/deep_voice/MainActivity.kt
  5. 60
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt
  6. 4
      android/settings.gradle.kts
  7. 19
      lib/modules/agent/controllers/agent_controller.dart
  8. 6
      lib/modules/agent/views/music_card.dart
  9. 6
      lib/modules/meeting/controllers/meeting_controller.dart
  10. 122
      lib/modules/meeting/views/meeting_view.dart
  11. 86
      lib/modules/meeting/views/share_bottom_sheet.dart
  12. 24
      lib/modules/music/views/music_playlist_view.dart
  13. 9
      lib/modules/opus_test/bindings/opus_test_binding.dart
  14. 462
      lib/modules/opus_test/controllers/opus_test_controller.dart
  15. 518
      lib/modules/opus_test/views/opus_test_view.dart
  16. 176
      lib/modules/pairing/controllers/pairing_controller.dart
  17. 22
      lib/modules/settings/views/settings_view.dart
  18. 21
      lib/modules/translation/views/translation_view.dart
  19. 9
      lib/routes/app_pages.dart
  20. 79
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  21. 2
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt
  22. 33
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  23. 12
      local_plugins/bytedance_speech/lib/bytedance_speech.dart
  24. 256
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt
  25. 3
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt
  26. 5
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt
  27. 6
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt
  28. 748
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt
  29. 126
      local_plugins/jl_opus/README.md
  30. 62
      local_plugins/jl_opus/android/build.gradle.kts
  31. 1
      local_plugins/jl_opus/android/settings.gradle.kts
  32. 9
      local_plugins/jl_opus/android/src/main/AndroidManifest.xml
  33. 285
      local_plugins/jl_opus/android/src/main/kotlin/com/yunqiinnovation/jl_opus/JlOpusPlugin.kt
  34. BIN
      local_plugins/jl_opus/android/src/main/libs/jl_audio_decode_V2.0.0_20005.aar
  35. 214
      local_plugins/jl_opus/lib/jl_opus.dart
  36. 25
      local_plugins/jl_opus/pubspec.yaml
  37. 9
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt
  38. 4
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt
  39. 2
      local_plugins/music_service/lib/music_service.dart
  40. 2
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt
  41. 5
      pubspec.yaml

94
.cursorrules

@ -1,94 +0,0 @@
# 代码简明规范
## Dart / Flutter
1. 风格统一
• 严格遵循 Effective Dart 格式与命名惯例。
2. 日志与调试
• 禁止 print;仅在异常场景调用 logger.e(...)。
3. 业务逻辑
• 默认使用 GetX(状态、路由、依赖注入等),避免手写 StatefulWidget。
4. 异常处理
• 不写重试逻辑,失败即返回错误。
• 谨慎使用 try-catch;优先通过类型系统与空安全防御。
5. 代码演进
• 任何改动都应 “小步、安全”,杜绝一次性大改或过度设计。
## 原生层(Android / iOS)
1. 配置不可改
• 禁止修改 SDK、NDK、JDK 与 iOS 构建配置。
2. Kotlin DSL
• 原生实现一律采用 Kotlin + 协程,遵守 Kotlin 最佳实践。
• 不写 Java 代码。
## 通用要求
1. 不自动执行
• 生成脚本/代码不得包含 flutter run 等执行指令。
2. 并发优先级
• 并发场景首选 Kotlin 协程;仅在 Flutter 端按需使用 Dart Future / Stream。
3. 语言
• 与用户交流、注释及文档一律使用中文。
• 不要自己创建总结文档。
宗旨:保持极简,先跑通业务,再考虑优化。
# 项目架构概述
DeepSound是一个基于Flutter和GetX构建的多平台语音应用,具有语音合成、语音识别、语音聊天等功能。
## 总体架构
- 采用GetX状态管理框架
- 遵循模块化设计,各功能模块相对独立
- 使用依赖注入实现组件解耦
- 多语言支持,使用GetX国际化方案
## 主要目录结构
- lib/core: 核心组件、工具和通用功能
- lib/data: 数据模型、服务和提供者
- lib/modules: 功能模块,按业务划分
- lib/routes: 路由定义和管理
- local_plugins: 本地插件,提供原生功能支持
# 核心模块说明
## 语音处理
- 火山语音服务: 提供语音识别和合成核心能力
- Azure语音服务: 提供备选的语音识别和合成能力
- 离线TTS支持: 在线语音合成失败时的备选方案
## 聊天功能
- 基于OpenAI的对话功能
- 支持语音输入、文字输入和语音输出
- 支持多种角色和语音类型
## 用户系统
- 支持多种登录方式:Firebase、Google、Facebook
- 用户资料管理和设置
- 主题切换和语言设置
## 蓝牙配对
- 支持蓝牙设备发现和连接
- 提供简化的配对流程
- 设备管理和状态监控
## 音频处理
- 音频录制和播放
- 支持波形显示
- 音量控制和音频质量设置
# 技术栈
- Flutter: UI框架
- GetX: 状态管理和依赖注入
- Firebase: 用户认证和后端服务
- WebSocket: 实时通信
- SQLite: 本地数据存储
- 原生插件: 语音识别、合成和蓝牙功能
# 插件说明
- jieli: 杰理蓝牙设备连接和控制插件
- volcano_speech: 火山语音服务插件,提供语音识别和合成
- azure_speech: 微软Azure语音服务插件
- open_ai_service: OpenAI服务插件,提供AI对话能力
- speech_to_text: 通用语音识别插件
# 环境配置
- 环境变量通过.env文件管理
- 支持不同环境的配置切换
- 敏感信息不应直接硬编码在代码中

1
.gitignore

@ -14,6 +14,7 @@ doc/api/
# dotenv environment variables file # dotenv environment variables file
.env* .env*
.env.example .env.example
CLAUDE.md
# Avoid committing generated Javascript files: # Avoid committing generated Javascript files:
*.dart.js *.dart.js

15
android/app/proguard-logging.pro

@ -0,0 +1,15 @@
# 移除系统组件的日志调用
-assumenosideeffects class android.util.Log {
public static int v(...);
public static int d(...);
public static int i(...);
}
# 针对特定类的日志移除
-assumenosideeffects class android.media.AudioTrack {
private static void log*(...);
}
-assumenosideeffects class android.app.AppOpsManager {
private static void log*(...);
}

5
android/app/src/main/kotlin/com/yunqiinnovation/deep_voice/MainActivity.kt

@ -1,5 +0,0 @@
package com.yunqiinnovation.deep_voice
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

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

@ -39,6 +39,7 @@ class MainActivity: FlutterActivity() {
* 抑制系统级组件的日志输出 * 抑制系统级组件的日志输出
*/ */
private fun suppressSystemLogs() { private fun suppressSystemLogs() {
// 更全面的系统标签列表
val systemTags = arrayOf( val systemTags = arrayOf(
"MediaCodec", "MediaCodec",
"CCodec", "CCodec",
@ -49,11 +50,66 @@ class MainActivity: FlutterActivity() {
"AudioTrack", "AudioTrack",
"AudioManager", "AudioManager",
"AudioFlinger", "AudioFlinger",
"AudioService" "AudioService",
"AudioEffect",
"AppOpsManager",
"AudioRecord",
"OpenGLRenderer",
"Choreographer",
"InputMethodManager",
"ViewRootImpl",
"Surface",
"BufferQueueProducer",
"ActivityManager",
"WindowManager",
"SensorManager"
) )
// 尝试多种方式设置日志级别
systemTags.forEach { tag -> systemTags.forEach { tag ->
System.setProperty("log.tag.$tag", "WARN") try {
// 方式1:标准属性设置
System.setProperty("log.tag.$tag", "ERROR")
// 方式2:尝试使用反射设置(针对某些特殊标签)
setLogLevelViaReflection(tag, Log.ERROR)
} catch (e: Exception) {
// 忽略设置失败的情况
}
}
// 方式3:尝试设置全局日志级别
try {
// 设置更高的阈值,减少verbose和debug日志
System.setProperty("log.tag", "WARN")
} catch (e: Exception) {
// 忽略
}
}
/**
* 通过反射尝试设置日志级别
*/
private fun setLogLevelViaReflection(tag: String, level: Int) {
try {
val logClass = Class.forName("android.util.Log")
val isLoggableMethod = logClass.getDeclaredMethod(
"isLoggable",
String::class.java,
Int::class.javaPrimitiveType
)
isLoggableMethod.isAccessible = true
// 某些系统可能支持直接设置
val setLogLevelMethod = logClass.getDeclaredMethod(
"setLogLevel",
String::class.java,
Int::class.javaPrimitiveType
)
setLogLevelMethod.isAccessible = true
setLogLevelMethod.invoke(null, tag, level)
} catch (e: Exception) {
// 反射方法可能不存在,忽略
} }
} }
} }

4
android/settings.gradle.kts

@ -42,6 +42,7 @@ include(":speech")
include(":chat_api") include(":chat_api")
include(":bytedance_speech") include(":bytedance_speech")
include(":jl_opus")
// 设置azure_speech项目的路径 // 设置azure_speech项目的路径
project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android") project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android")
project(":chat_storage").projectDir = file("../local_plugins/chat_storage/android") project(":chat_storage").projectDir = file("../local_plugins/chat_storage/android")
@ -52,4 +53,5 @@ project(":classic_bluetooth").projectDir = file("../local_plugins/classic_blueto
project(":agent_service").projectDir = file("../local_plugins/agent_service/android") project(":agent_service").projectDir = file("../local_plugins/agent_service/android")
project(":speech").projectDir = file("../local_plugins/speech/android") project(":speech").projectDir = file("../local_plugins/speech/android")
project(":chat_api").projectDir = file("../local_plugins/chat_api/android") project(":chat_api").projectDir = file("../local_plugins/chat_api/android")
project(":bytedance_speech").projectDir = file("../local_plugins/bytedance_speech/android") project(":bytedance_speech").projectDir = file("../local_plugins/bytedance_speech/android")
project(":jl_opus").projectDir = file("../local_plugins/jl_opus/android")

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

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'package:amap_service/amap_service.dart'; import 'package:amap_service/amap_service.dart';
import 'package:deep_voice/core/utils/upload_oss.dart'; import 'package:deep_voice/core/utils/upload_oss.dart';
@ -35,7 +36,7 @@ class AgentController extends GetxController {
final _musiceManager = Get.find<MusicManager>(); final _musiceManager = Get.find<MusicManager>();
final _navigationManager = Get.find<NavigationManager>(); final _navigationManager = Get.find<NavigationManager>();
var isPlaying = false.obs; var isPlaying = false.obs;
var currentUrl = "".obs; var currentId = 0.obs;
// 播放进度相关状态 // 播放进度相关状态
var currentPosition = 0.obs; // 当前播放位置(毫秒) var currentPosition = 0.obs; // 当前播放位置(毫秒)
@ -310,7 +311,7 @@ class AgentController extends GetxController {
case AgentServiceEventType.ttsCanceled: case AgentServiceEventType.ttsCanceled:
isSpeaking.value = false; isSpeaking.value = false;
Logger.i(TAG, 'TTS停止播放'); Logger.i(TAG, 'TTS停止播放, $event.type');
break; break;
case AgentServiceEventType.imageProcessing: case AgentServiceEventType.imageProcessing:
@ -419,6 +420,16 @@ class AgentController extends GetxController {
Map<String, dynamic> metaResult = {}; Map<String, dynamic> metaResult = {};
metaResult = jsonDecode(metaStr) as Map<String, dynamic>; metaResult = jsonDecode(metaStr) as Map<String, dynamic>;
if (metaResult.isNotEmpty) { if (metaResult.isNotEmpty) {
var notreply = false;
if (metaResult.containsKey("notreply")) {
// 如果有导航卡片,直接添加
notreply = metaResult["notreply"] as bool;
}
if (notreply) {
Logger.i(TAG, 'Flutter 结束调用 不回复');
//不回复直接返回
break;
}
// 判断是否为新的回复或响应ID是否改变 // 判断是否为新的回复或响应ID是否改变
if (_isNewAssistantResponse || _currentResponseId != responseId) { if (_isNewAssistantResponse || _currentResponseId != responseId) {
// 添加新消息 // 添加新消息
@ -812,7 +823,7 @@ class AgentController extends GetxController {
var status = await _musiceManager.getStatus(); var status = await _musiceManager.getStatus();
// debugPrint('获取当前音乐播放状态 $status'); // debugPrint('获取当前音乐播放状态 $status');
isPlaying.value = status['isPlaying']; isPlaying.value = status['isPlaying'];
currentUrl.value = status['url']; currentId.value = status['id'] ?? 0;
} }
// 播放音乐 // 播放音乐
@ -844,7 +855,7 @@ class AgentController extends GetxController {
_musiceManager.play(song); _musiceManager.play(song);
isPlaying.value = true; isPlaying.value = true;
currentUrl.value = song['url'] ?? ''; currentId.value = song['id'] ?? 0;
_startProgressTimer(); _startProgressTimer();
} }

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

@ -82,7 +82,7 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url,
), ),
Obx(() { Obx(() {
final isThisPlaying = final isThisPlaying =
controller.isPlaying.value && controller.currentUrl.value == url; controller.isPlaying.value && controller.currentId.value == id;
// 只有当前播放的歌曲才显示进度 // 只有当前播放的歌曲才显示进度
final currentProgress = final currentProgress =
@ -97,13 +97,13 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url,
strokeWidth: 2.0, strokeWidth: 2.0,
onPlayPause: () { onPlayPause: () {
if (controller.isPlaying.value) { if (controller.isPlaying.value) {
if (controller.currentUrl.value == url) { if (controller.currentId.value == id) {
controller.musicPause(); controller.musicPause();
} else { } else {
controller.musicPlay(song); controller.musicPlay(song);
} }
} else { } else {
if (controller.currentUrl.value == url) { if (controller.currentId.value == id) {
controller.musicResume(); controller.musicResume();
} else { } else {
controller.musicPlay(song); controller.musicPlay(song);

6
lib/modules/meeting/controllers/meeting_controller.dart

@ -32,6 +32,7 @@ class MeetingController extends GetxController {
List<String> selected = []; List<String> selected = [];
final appDocDir = getApplicationDocumentsDirectory(); final appDocDir = getApplicationDocumentsDirectory();
//final appopus = getExternalStorageDirectories();
// final directory = Directory( // final directory = Directory(
// '/storage/emulated/0/Android/data/com.yunqiinnovation.deepsound/files'); // '/storage/emulated/0/Android/data/com.yunqiinnovation.deepsound/files');
List<FileSystemEntity> files = []; List<FileSystemEntity> files = [];
@ -129,8 +130,9 @@ class MeetingController extends GetxController {
} }
void loadTranslatopus() async { void loadTranslatopus() async {
final appDir = await appDocDir; // 先 await 获取目录 // final appDir = await appopus; // 先 await 获取目录
final dir = Directory("${appDir.path}/OpusAudio"); final dir =
Directory("/data/user/0/com.yunqiinnovation.deepsound/files/OpusAudio");
print("appDocDir=${dir?.path}"); print("appDocDir=${dir?.path}");

122
lib/modules/meeting/views/meeting_view.dart

@ -439,7 +439,7 @@ class MeetingView extends GetView<MeetingController> {
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.w), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.w),
child: Column( child: Column(
children: [ children: [
_importAudioItem( _importAudioItem1(
context, context,
isDarkMode, isDarkMode,
icon: Image.asset('assets/images/explore.png', icon: Image.asset('assets/images/explore.png',
@ -452,7 +452,7 @@ class MeetingView extends GetView<MeetingController> {
}, },
), ),
SizedBox(height: 8.w), SizedBox(height: 8.w),
_importAudioItem( _importAudioItem1(
context, context,
isDarkMode, isDarkMode,
icon: Image.asset('assets/images/explore.png', icon: Image.asset('assets/images/explore.png',
@ -465,7 +465,7 @@ class MeetingView extends GetView<MeetingController> {
}, },
), ),
SizedBox(height: 8.w), SizedBox(height: 8.w),
_importAudioItem( _importAudioItem1(
context, context,
isDarkMode, isDarkMode,
icon: Row( icon: Row(
@ -556,65 +556,64 @@ class MeetingView extends GetView<MeetingController> {
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.w), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.w),
child: Column( child: Column(
children: [ children: [
// 现场录音 - 始终可用
_importAudioItem( _importAudioItem(
context, context,
isDarkMode, isDarkMode,
icon: Image.asset('assets/images/explore.png', icon: Image.asset('assets/images/explore.png',
width: 32.w, height: 32.w), width: 32.w, height: 32.w),
title: '现场录音', title: '现场录音',
enabled: true, // 始终可用
onTap: () async { onTap: () async {
Navigator.of(context).pop(); Navigator.of(context).pop();
await Get.toNamed(Routes.meetingRecord, arguments: { await Get.toNamed(Routes.meetingRecord, arguments: {
'audioTypes': 0, 'audioTypes': 0,
}); });
controller.addMeetingAudio(); controller.addMeetingAudio();
// TODO: 实现从文件导入逻辑
}, },
), ),
SizedBox(height: 8.w), SizedBox(height: 8.w),
// 音、视频录音 - 根据控制器状态禁用
_importAudioItem( _importAudioItem(
context, context,
isDarkMode, isDarkMode,
icon: Image.asset('assets/images/explore.png', icon: Image.asset('assets/images/explore.png',
width: 32.w, height: 32.w), width: 32.w, height: 32.w),
title: '音、视频录音', title: '音、视频录音',
onTap: () async { enabled: false, // 依赖控制器状态
Navigator.of(context).pop(); onTap: false
await Get.toNamed(Routes.meetingRecord, arguments: { ? () async {
'audioTypes': 1, // 条件启用
}); Navigator.of(context).pop();
controller.addMeetingAudio(); await Get.toNamed(Routes.meetingRecord,
// TODO: 实现从相册导入逻辑 arguments: {
}, 'audioTypes': 1,
});
controller.addMeetingAudio();
}
: null, // 禁用时置空onTap
), ),
SizedBox(height: 8.w), SizedBox(height: 8.w),
// 通话录音 - 根据控制器状态禁用
_importAudioItem( _importAudioItem(
context, context,
isDarkMode, isDarkMode,
icon: Row( icon: Row(
mainAxisSize: MainAxisSize.min, // ... 图标组合保持不变
children: [ ),
Image.asset('assets/images/explore.png',
width: 20.w, height: 20.w),
SizedBox(width: 4.w),
Image.asset('assets/images/explore.png',
width: 20.w, height: 20.w),
SizedBox(width: 4.w),
Image.asset('assets/images/explore.png',
width: 20.w, height: 20.w),
],
),
title: '通话录音', title: '通话录音',
onTap: () async { enabled: false, // 依赖控制器状态
Navigator.of(context).pop(); onTap: false
? () async {
await Get.toNamed(Routes.meetingRecord, arguments: { // 条件启用
'audioTypes': 2, Navigator.of(context).pop();
}); await Get.toNamed(Routes.meetingRecord,
controller.addMeetingAudio(); arguments: {
'audioTypes': 2,
// TODO: 实现从其他APP导入逻辑 });
}, controller.addMeetingAudio();
}
: null, // 禁用时置空onTap
), ),
], ],
), ),
@ -643,8 +642,60 @@ class MeetingView extends GetView<MeetingController> {
); );
} }
// 选项框 // 选项框 (增加enabled参数)
Widget _importAudioItem( Widget _importAudioItem(
BuildContext context,
bool isDarkMode, {
required Widget icon,
required String title,
required bool enabled, // 新增启用状态参数
required VoidCallback? onTap, // 改为可空回调
}) {
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(12.w),
onTap: onTap, // 禁用时onTap为null
child: Container(
height: 52.w,
padding: EdgeInsets.symmetric(horizontal: 12.w),
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withOpacity(enabled ? 0.03 : 0.01) // 调整禁用透明度
: Colors.grey[50]!.withOpacity(enabled ? 1 : 0.5),
borderRadius: BorderRadius.circular(12.w),
),
child: Row(
children: [
// 图标添加透明度效果
Opacity(
opacity: enabled ? 1.0 : 0.5,
child: icon,
),
SizedBox(width: 14.w),
Expanded(
child: Text(
title,
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode
? Colors.white.withOpacity(enabled ? 1 : 0.5)
: Colors.black87.withOpacity(enabled ? 1 : 0.5),
),
),
),
Icon(Icons.arrow_forward_ios,
size: 18.w,
color: Colors.grey.withOpacity(enabled ? 1 : 0.5)),
],
),
),
),
);
}
// 选项框
Widget _importAudioItem1(
BuildContext context, BuildContext context,
bool isDarkMode, { bool isDarkMode, {
required Widget icon, required Widget icon,
@ -684,7 +735,6 @@ class MeetingView extends GetView<MeetingController> {
), ),
); );
} }
// 新增右侧弹窗 // 新增右侧弹窗
void _showRightDrawerDialog(BuildContext context, bool isDarkMode) { void _showRightDrawerDialog(BuildContext context, bool isDarkMode) {

86
lib/modules/meeting/views/share_bottom_sheet.dart

@ -1,7 +1,12 @@
import 'dart:io';
import 'package:common_utils/common_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import '../controllers/meeting_details_controller.dart'; import '../controllers/meeting_details_controller.dart';
@ -69,18 +74,44 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
_shareItem(Icons.link, '分享链接', isBorder: false), _shareItem(Icons.link, '分享链接', isBorder: false),
]), ]),
_box([ _box([
_shareItem(Icons.copy, '复制转写'), _shareItem(Icons.copy, '复制转写', onTap: () {
_shareItem(Icons.copy, '复制总结', isBorder: false), Get.back();
_copy(_getAllText());
}),
_shareItem(Icons.copy, '复制总结', isBorder: false, onTap: () {
Get.back();
_copy(_controller.meetingDetails.value.summary);
}),
]), ]),
_box([ _box([
_shareItem(Icons.multitrack_audio, '导出音频', onTap: () {
Get.back();
_share(_controller.meetingData.value.filepath, '分享会议音频');
}),
_shareItem(Icons.message, '导出转写', onTap: () async {
final tempDir = await getTemporaryDirectory();
final file = File(
'${tempDir.path}/${_controller.meetingData.value.title} speaker.txt');
String allText = _getAllText();
await file.writeAsString(allText);
Get.back();
_share(file.path, '分享会议转写');
}),
_shareItem(Icons.article, '导出总结', onTap: () async {
final tempDir = await getTemporaryDirectory();
final file = File(
'${tempDir.path}/${_controller.meetingData.value.title} summary.txt');
await file.writeAsString(
_controller.meetingDetails.value.summary);
Get.back();
_share(file.path, '分享会议总结');
}),
_shareItem( _shareItem(
Icons.multitrack_audio, Icons.lan,
'导出音频', '导出思维导图',
onTap: shareAllTranslatAudio, isBorder: false,
onTap: () {},
), ),
_shareItem(Icons.message, '导出转写'),
_shareItem(Icons.article, '导出总结'),
_shareItem(Icons.lan, '导出思维导图', isBorder: false),
]), ]),
], ],
), ),
@ -144,17 +175,42 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
); );
} }
void shareAllTranslatAudio() async { void _share(String filepath, String title) async {
if (_controller.meetingData.value.filepath.isEmpty) {
EasyLoading.showToast('没有可分享的音频文件');
return;
}
final result = await SharePlus.instance.share(ShareParams( final result = await SharePlus.instance.share(ShareParams(
text: '分享会议音频', text: title,
files: [XFile(_controller.meetingData.value.filepath)], files: [XFile(filepath)],
)); ));
if (result.status == ShareResultStatus.success) { if (result.status == ShareResultStatus.success) {
EasyLoading.showToast('分享完成!'); EasyLoading.showToast('分享完成!');
} }
} }
void _copy(String text) {
Clipboard.setData(ClipboardData(
text: text,
)).then(
(value) => Get.snackbar(
'success'.tr, // 成功
'复制成功',
backgroundColor: Colors.blue.withOpacity(0.8),
colorText: Colors.white,
),
);
}
String _getAllText() {
String text = '';
for (var element in _controller.textList) {
if (element['speaker'].isNotEmpty) {
text += '${element['speaker']} ';
}
text += '${DateUtil.formatDateMs(
element['starttime'],
format: 'HH:mm:ss',
isUtc: true,
)}\n';
text += '${element['content']}\n';
}
return text;
}
} }

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

@ -162,10 +162,11 @@ class MusicPlaylistView extends StatelessWidget {
// 播放按钮 // 播放按钮
Obx(() { Obx(() {
final isThisPlaying = final isThisPlaying =
controller.isPlaying.value && controller.currentUrl.value == url; controller.isPlaying.value && controller.currentId.value == url;
// 只有当前播放的歌曲才显示进度 // 只有当前播放的歌曲才显示进度
final currentProgress = isThisPlaying ? controller.progress.value : 0.0; final currentProgress =
isThisPlaying ? controller.progress.value : 0.0;
return CircularProgressButton( return CircularProgressButton(
isPlaying: isThisPlaying, isPlaying: isThisPlaying,
@ -176,24 +177,27 @@ class MusicPlaylistView extends StatelessWidget {
strokeWidth: 2.5, strokeWidth: 2.5,
onPlayPause: () { onPlayPause: () {
if (controller.isPlaying.value) { if (controller.isPlaying.value) {
if (controller.currentUrl.value == url) { if (controller.currentId.value == id) {
controller.musicPause(); controller.musicPause();
} else { } else {
controller.musicPlay(song); controller.musicPlay(song);
} }
} else { } else {
if (controller.currentUrl.value == url) { if (controller.currentId.value == id) {
controller.musicResume(); controller.musicResume();
} else { } else {
controller.musicPlay(song); controller.musicPlay(song);
} }
} }
}, },
onSeek: isThisPlaying ? (seekProgress) { onSeek: isThisPlaying
// 只有当前播放的歌曲才允许跳转 ? (seekProgress) {
final targetPosition = (controller.duration.value * seekProgress).round(); // 只有当前播放的歌曲才允许跳转
controller.seekToPosition(targetPosition); final targetPosition =
} : null, (controller.duration.value * seekProgress).round();
controller.seekToPosition(targetPosition);
}
: null,
); );
}), }),
], ],

9
lib/modules/opus_test/bindings/opus_test_binding.dart

@ -0,0 +1,9 @@
import 'package:get/get.dart';
import '../controllers/opus_test_controller.dart';
class OpusTestBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<OpusTestController>(() => OpusTestController());
}
}

462
lib/modules/opus_test/controllers/opus_test_controller.dart

@ -0,0 +1,462 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:path_provider/path_provider.dart';
import 'package:just_audio/just_audio.dart';
import 'package:jl_opus/jl_opus.dart';
import 'package:permission_handler/permission_handler.dart';
class OpusTestController extends GetxController {
// 选中的文件
final selectedFiles = <PlatformFile>[].obs;
// 扫描到的外部存储文件
final externalFiles = <PlatformFile>[].obs;
// 是否携带协议头
final hasHeader = false.obs;
// 数据包长度
final packetLength = '80'.obs;
// 采样率选项
final sampleRateOptions = [8000, 16000, 24000, 32000, 44100, 48000];
final selectedSampleRate = 16000.obs;
// 通道数量
final isMono = true.obs;
// 播放状态
final isPlaying = false.obs;
final isDecoding = false.obs;
final statusMessage = ''.obs;
// 播放器
final player = AudioPlayer();
// 临时文件路径
String? tempPcmPath;
// 杰理OPUS解码器
late JlOpus jlOpus;
StreamSubscription? _eventSubscription;
@override
void onInit() {
super.onInit();
requestPermissions();
_initOpusDecoder();
}
@override
void onClose() {
player.dispose();
_eventSubscription?.cancel();
jlOpus.dispose();
super.onClose();
}
// 初始化OPUS解码器
Future<void> _initOpusDecoder() async {
jlOpus = JlOpus();
// 监听解码器事件
_eventSubscription = jlOpus.eventStream.listen((event) {
switch (event.event) {
case 'onStart':
statusMessage.value = '开始${event.type == "file" ? "文件" : "流"}解码...';
break;
case 'onComplete':
if (event.type == 'file') {
statusMessage.value = '解码完成: ${event.filePath}';
} else {
statusMessage.value = '流解码完成';
}
break;
case 'onError':
statusMessage.value =
'解码错误: [${event.errorCode}] ${event.errorMessage}';
isDecoding.value = false;
break;
}
});
// 初始化OPUS解码器
final initialized = await jlOpus.initOpusDecoder();
if (!initialized) {
statusMessage.value = 'OPUS解码器初始化失败';
}
}
// 请求必要权限
Future<void> requestPermissions() async {
if (Platform.isAndroid) {
await Permission.storage.request();
}
}
// 加载外部存储文件
Future<void> loadExternalFiles() async {
try {
externalFiles.clear();
statusMessage.value = '正在扫描外部存储...';
// 获取外部存储目录
final directories = await getExternalStorageDirectories();
if (directories == null || directories.isEmpty) {
statusMessage.value = '未找到外部存储目录';
return;
}
// 遍历所有外部存储目录
for (final directory in directories) {
final dirPath = directory.path;
final dir = Directory(dirPath);
// 检查目录是否存在
if (await dir.exists()) {
// 递归列出所有文件
final files = await dir.list(recursive: true).toList();
for (var entity in files) {
if (entity is File) {
final path = entity.path.toLowerCase();
if (path.endsWith('.opus')) {
final fileSize = await entity.length();
externalFiles.add(PlatformFile(
name: entity.path.split('/').last,
path: entity.path,
size: fileSize,
));
}
}
}
}
}
statusMessage.value = '扫描完成,找到 ${externalFiles.length} 个Opus文件';
} catch (e) {
statusMessage.value = '扫描失败: $e';
}
}
// 选择文件
Future<void> pickFiles() async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['opus'],
allowMultiple: false,
);
if (result != null && result.files.isNotEmpty) {
selectedFiles.clear();
selectedFiles.add(result.files.first);
statusMessage.value = '已选择文件:${result.files.first.name}';
}
} catch (e) {
statusMessage.value = '选择文件失败: $e';
}
}
// 从外部文件列表中选择文件
void selectExternalFile(PlatformFile file) {
selectedFiles.clear();
selectedFiles.add(file);
statusMessage.value = '已选择文件: ${file.name}';
}
// 切换通道模式
void toggleChannel(bool isSingleChannel) {
isMono.value = isSingleChannel;
}
// 设置采样率
void setSampleRate(int rate) {
selectedSampleRate.value = rate;
}
// 设置数据包长度
void setPacketLength(String length) {
packetLength.value = length;
}
// 切换是否携带协议头
void toggleHeader(bool value) {
hasHeader.value = value;
}
// 播放音频
Future<void> playAudio() async {
if (isPlaying.value) {
await player.stop();
isPlaying.value = false;
statusMessage.value = '已停止播放';
return;
}
if (selectedFiles.isEmpty) {
statusMessage.value = '请先选择Opus文件';
return;
}
try {
isDecoding.value = true;
statusMessage.value = '正在解码...';
// 使用杰理OPUS库进行解码
final inPath = selectedFiles.first.path!;
// 创建WAV输出文件路径
final tempDir = await getTemporaryDirectory();
final wavFileName =
'decoded_opus_${DateTime.now().millisecondsSinceEpoch}.wav';
final outPath = '${tempDir.path}/$wavFileName';
// 创建解码参数
final option = OpusOption(
hasHeader: hasHeader.value,
channel: isMono.value ? 1 : 2,
sampleRate: selectedSampleRate.value,
packetSize: int.tryParse(packetLength.value) ?? 40,
);
// 解码文件
final pcmPath = await jlOpus.decodeOpusFile(inPath, outPath, option);
if (pcmPath == null) {
statusMessage.value = '解码失败';
isDecoding.value = false;
return;
}
tempPcmPath = pcmPath;
// 将PCM文件转换为WAV文件
final wavFile = await _convertPcmToWav(
pcmPath,
'${tempDir.path}/$wavFileName',
selectedSampleRate.value,
isMono.value ? 1 : 2);
statusMessage.value = '解码完成,准备播放';
// 播放WAV文件
await player.setFilePath(wavFile.path);
await player.play();
isPlaying.value = true;
statusMessage.value = '正在播放解码后的音频';
// 监听播放完成
player.playerStateStream.listen((state) {
if (state.processingState == ProcessingState.completed) {
isPlaying.value = false;
statusMessage.value = '播放完成';
}
});
} catch (e) {
statusMessage.value = '解码或播放失败: $e';
} finally {
isDecoding.value = false;
}
}
// 将PCM转换为WAV格式
Future<File> _convertPcmToWav(
String pcmPath, String wavPath, int sampleRate, int channels) async {
final pcmFile = File(pcmPath);
final pcmData = await pcmFile.readAsBytes();
// 创建WAV文件 (44字节头部 + PCM数据)
final wavSize = 44 + pcmData.length;
final wavData = ByteData(wavSize);
// 写入WAV头部
_writeWavHeader(wavData, pcmData.length, sampleRate, channels);
// 写入PCM数据
for (int i = 0; i < pcmData.length; i++) {
wavData.setUint8(44 + i, pcmData[i]);
}
// 保存为WAV文件
final wavFile = File(wavPath);
await wavFile.writeAsBytes(wavData.buffer.asUint8List());
return wavFile;
}
// 写入WAV头部到ByteData
void _writeWavHeader(
ByteData data, int pcmLength, int sampleRate, int channels) {
final bytesPerSample = 2; // 16位PCM
final byteRate = sampleRate * channels * bytesPerSample;
final blockAlign = channels * bytesPerSample;
// "RIFF"标识
data.setUint8(0, 0x52); // 'R'
data.setUint8(1, 0x49); // 'I'
data.setUint8(2, 0x46); // 'F'
data.setUint8(3, 0x46); // 'F'
// 文件大小 (减去8个字节的RIFF头部和大小字段)
data.setUint32(4, 36 + pcmLength, Endian.little);
// "WAVE"标识
data.setUint8(8, 0x57); // 'W'
data.setUint8(9, 0x41); // 'A'
data.setUint8(10, 0x56); // 'V'
data.setUint8(11, 0x45); // 'E'
// "fmt "子块
data.setUint8(12, 0x66); // 'f'
data.setUint8(13, 0x6D); // 'm'
data.setUint8(14, 0x74); // 't'
data.setUint8(15, 0x20); // ' '
// 子块大小 (16 for PCM)
data.setUint32(16, 16, Endian.little);
// 音频格式 (1 表示PCM)
data.setUint16(20, 1, Endian.little);
// 通道数量
data.setUint16(22, channels, Endian.little);
// 采样率
data.setUint32(24, sampleRate, Endian.little);
// 字节率 = 采样率 * 通道数 * 每样本字节数
data.setUint32(28, byteRate, Endian.little);
// 块对齐 = 通道数 * 每样本字节数
data.setUint16(32, blockAlign, Endian.little);
// 每个样本位数
data.setUint16(34, bytesPerSample * 8, Endian.little);
// "data"子块
data.setUint8(36, 0x64); // 'd'
data.setUint8(37, 0x61); // 'a'
data.setUint8(38, 0x74); // 't'
data.setUint8(39, 0x61); // 'a'
// 数据大小
data.setUint32(40, pcmLength, Endian.little);
}
}
// PCM音频源
class PCMAudioSource extends StreamAudioSource {
final Uint8List pcmData;
final int sampleRate;
final int channels;
PCMAudioSource({
required this.pcmData,
this.sampleRate = 16000,
this.channels = 1,
});
@override
Future<StreamAudioResponse> request([int? start, int? end]) async {
start = start ?? 0;
end = end ?? pcmData.length;
// 转换PCM到WAV格式
final wavHeader = _createWavHeader(pcmData.length, sampleRate, channels);
final wavData = Uint8List(wavHeader.length + pcmData.length);
wavData.setRange(0, wavHeader.length, wavHeader);
wavData.setRange(
wavHeader.length, wavHeader.length + pcmData.length, pcmData);
// 调整请求范围来适应WAV头部
int adjustedStart = start;
int adjustedEnd = end;
if (start < wavHeader.length) {
adjustedStart = start;
} else {
adjustedStart = start + wavHeader.length;
}
if (end <= wavHeader.length) {
adjustedEnd = end;
} else {
adjustedEnd = end + wavHeader.length;
adjustedEnd = adjustedEnd > wavData.length ? wavData.length : adjustedEnd;
}
return StreamAudioResponse(
sourceLength: wavData.length,
contentLength: adjustedEnd - adjustedStart,
offset: adjustedStart,
stream: Stream.value(wavData.sublist(adjustedStart, adjustedEnd)),
contentType: 'audio/wav',
);
}
// 创建WAV头部
Uint8List _createWavHeader(int dataLength, int sampleRate, int channels) {
final bytesPerSample = 2; // 16位PCM
final byteRate = sampleRate * channels * bytesPerSample;
final blockAlign = channels * bytesPerSample;
final header = ByteData(44); // WAV头部固定44字节
// "RIFF"标识
header.setUint8(0, 0x52); // 'R'
header.setUint8(1, 0x49); // 'I'
header.setUint8(2, 0x46); // 'F'
header.setUint8(3, 0x46); // 'F'
// 文件大小 (减去8个字节的RIFF头部和大小字段)
header.setUint32(4, 36 + dataLength, Endian.little);
// "WAVE"标识
header.setUint8(8, 0x57); // 'W'
header.setUint8(9, 0x41); // 'A'
header.setUint8(10, 0x56); // 'V'
header.setUint8(11, 0x45); // 'E'
// "fmt "子块
header.setUint8(12, 0x66); // 'f'
header.setUint8(13, 0x6D); // 'm'
header.setUint8(14, 0x74); // 't'
header.setUint8(15, 0x20); // ' '
// 子块大小 (16 for PCM)
header.setUint32(16, 16, Endian.little);
// 音频格式 (1 表示PCM)
header.setUint16(20, 1, Endian.little);
// 通道数量
header.setUint16(22, channels, Endian.little);
// 采样率
header.setUint32(24, sampleRate, Endian.little);
// 字节率 = 采样率 * 通道数 * 每样本字节数
header.setUint32(28, byteRate, Endian.little);
// 块对齐 = 通道数 * 每样本字节数
header.setUint16(32, blockAlign, Endian.little);
// 每个样本位数
header.setUint16(34, bytesPerSample * 8, Endian.little);
// "data"子块
header.setUint8(36, 0x64); // 'd'
header.setUint8(37, 0x61); // 'a'
header.setUint8(38, 0x74); // 't'
header.setUint8(39, 0x61); // 'a'
// 数据大小
header.setUint32(40, dataLength, Endian.little);
return header.buffer.asUint8List();
}
}

518
lib/modules/opus_test/views/opus_test_view.dart

@ -0,0 +1,518 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../controllers/opus_test_controller.dart';
class OpusTestView extends GetView<OpusTestController> {
const OpusTestView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// 获取主题模式
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return DefaultTabController(
length: 2,
child: Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
elevation: 0.5,
title: Text(
'OPUS解码',
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
centerTitle: true,
bottom: TabBar(
indicatorColor: Colors.blue,
indicatorWeight: 3,
labelColor: isDarkMode ? Colors.white : Colors.black,
unselectedLabelColor: isDarkMode ? Colors.grey : Colors.grey,
tabs: const [
Tab(text: "解码"),
Tab(text: "输出"),
],
),
),
body: TabBarView(
children: [
_buildDecodeTab(isDarkMode),
_buildEncodeTab(isDarkMode),
],
),
),
);
}
// 解码标签页
Widget _buildDecodeTab(bool isDarkMode) {
return ListView(
padding: EdgeInsets.all(16.w),
children: [
// 文件选择部分
_buildSection(
title: '选择文件',
isDarkMode: isDarkMode,
child: Column(
children: [
// 当前选中的文件
Obx(() => controller.selectedFiles.isEmpty
? const SizedBox.shrink()
: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: controller.selectedFiles.length,
itemBuilder: (context, index) {
final file = controller.selectedFiles[index];
return ListTile(
title: Text(
file.name,
style: TextStyle(
color:
isDarkMode ? Colors.white70 : Colors.black87,
fontSize: 14.sp,
),
),
subtitle: Text(
'${(file.size / 1024).toStringAsFixed(2)} Kb',
style: TextStyle(
color:
isDarkMode ? Colors.white38 : Colors.black54,
fontSize: 12.sp,
),
),
trailing: IconButton(
icon: Icon(Icons.clear, color: Colors.red),
onPressed: () {
controller.selectedFiles.removeAt(index);
},
),
);
},
)),
// 外部文件列表标题和刷新按钮
Padding(
padding: EdgeInsets.symmetric(vertical: 8.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'外部存储文件',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
IconButton(
icon: Icon(
Icons.refresh,
color: isDarkMode ? Colors.blue[300] : Colors.blue,
),
onPressed: controller.loadExternalFiles,
),
],
),
),
// 外部文件列表
Obx(() => controller.externalFiles.isEmpty
? Center(
child: Padding(
padding: EdgeInsets.all(16.h),
child: Text(
'未找到Opus文件',
style: TextStyle(
color: isDarkMode ? Colors.white54 : Colors.black54,
),
),
),
)
: Container(
height: 200.h,
decoration: BoxDecoration(
border: Border.all(
color: isDarkMode
? Colors.grey[700]!
: Colors.grey[300]!,
),
borderRadius: BorderRadius.circular(8.r),
),
child: ListView.builder(
itemCount: controller.externalFiles.length,
itemBuilder: (context, index) {
final file = controller.externalFiles[index];
return ListTile(
title: Text(
file.name,
style: TextStyle(
color: isDarkMode
? Colors.white70
: Colors.black87,
fontSize: 14.sp,
),
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
'${(file.size / 1024).toStringAsFixed(2)} Kb',
style: TextStyle(
color: isDarkMode
? Colors.white38
: Colors.black54,
fontSize: 12.sp,
),
),
onTap: () {
controller.selectExternalFile(file);
},
trailing: Icon(
Icons.arrow_forward,
color:
isDarkMode ? Colors.white54 : Colors.black54,
),
);
},
),
)),
],
),
),
SizedBox(height: 16.h),
// 解码参数部分
_buildSection(
title: '解码参数',
isDarkMode: isDarkMode,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Obx(() => Checkbox(
value: controller.hasHeader.value,
onChanged: (value) => controller.toggleHeader(value!),
activeColor: Colors.blue,
)),
Text(
'是否携带协议头',
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black87,
fontSize: 14.sp,
),
),
],
),
// 数据包长度
Row(
children: [
Text(
'数据包长度',
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black87,
fontSize: 14.sp,
),
),
SizedBox(width: 16.w),
Expanded(
child: Obx(() => TextField(
controller: TextEditingController(
text: controller.packetLength.value),
onChanged: (value) =>
controller.setPacketLength(value),
keyboardType: TextInputType.number,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black,
),
decoration: InputDecoration(
isDense: true,
suffix: Text(
'Byte',
style: TextStyle(
color: isDarkMode
? Colors.white60
: Colors.black54,
fontSize: 12.sp,
),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: isDarkMode
? Colors.white30
: Colors.black26,
),
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Colors.blue,
),
),
),
)),
),
],
),
SizedBox(height: 20.h),
// 采样率
Row(
children: [
Text(
'采样率',
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black87,
fontSize: 14.sp,
),
),
SizedBox(width: 16.w),
Expanded(
child: Obx(() => DropdownButton<int>(
value: controller.selectedSampleRate.value,
isExpanded: true,
dropdownColor:
isDarkMode ? Colors.grey[800] : Colors.white,
items: controller.sampleRateOptions.map((rate) {
return DropdownMenuItem<int>(
value: rate,
child: Text(
rate.toString(),
style: TextStyle(
color: isDarkMode
? Colors.white
: Colors.black87,
),
),
);
}).toList(),
onChanged: (value) =>
controller.setSampleRate(value!),
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black,
),
)),
),
],
),
SizedBox(height: 20.h),
// 通道数量
Text(
'通道数量',
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black87,
fontSize: 14.sp,
),
),
Row(
children: [
Expanded(
child: Row(
children: [
Obx(() => Radio<bool>(
value: true,
groupValue: controller.isMono.value,
onChanged: (value) =>
controller.toggleChannel(value!),
activeColor: Colors.blue,
)),
Text(
'单通道',
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black87,
fontSize: 14.sp,
),
),
],
),
),
Expanded(
child: Row(
children: [
Obx(() => Radio<bool>(
value: false,
groupValue: controller.isMono.value,
onChanged: (value) =>
controller.toggleChannel(value!),
activeColor: Colors.blue,
)),
Text(
'双通道',
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black87,
fontSize: 14.sp,
),
),
],
),
),
],
),
],
),
),
SizedBox(height: 16.h),
// 状态消息
Obx(() => controller.statusMessage.value.isNotEmpty
? Container(
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 16.w),
margin: EdgeInsets.only(bottom: 16.h),
decoration: BoxDecoration(
color: isDarkMode
? Colors.blue.withOpacity(0.2)
: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(4.r),
border: Border.all(
color: isDarkMode
? Colors.blue.withOpacity(0.3)
: Colors.blue.withOpacity(0.2),
width: 1,
),
),
child: Text(
controller.statusMessage.value,
style: TextStyle(
color: isDarkMode ? Colors.blue[300] : Colors.blue[700],
fontSize: 12.sp,
),
),
)
: const SizedBox.shrink()),
// 播放按钮
Container(
margin: EdgeInsets.symmetric(vertical: 8.h),
child: ElevatedButton.icon(
icon: Obx(() => Icon(
controller.isPlaying.value ? Icons.stop : Icons.play_arrow,
color: Colors.white,
)),
label: Obx(() => Text(
controller.isPlaying.value ? '停止播放' : '播放音频',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
),
)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
onPressed: controller.isDecoding.value
? null
: () {
controller.playAudio();
},
),
),
// 解码按钮
Container(
margin: EdgeInsets.symmetric(vertical: 8.h),
child: Obx(() => ElevatedButton(
onPressed: controller.selectedFiles.isEmpty ||
controller.isDecoding.value
? null
: () {
controller.pickFiles();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Text(
'开始解码',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
),
),
)),
),
],
);
}
// 编码标签页(暂未实现)
Widget _buildEncodeTab(bool isDarkMode) {
return Center(
child: Text(
'编码功能尚未实现',
style: TextStyle(
color: isDarkMode ? Colors.white54 : Colors.black54,
fontSize: 16.sp,
),
),
);
}
// 构建带标题的区段
Widget _buildSection({
required String title,
required Widget child,
required bool isDarkMode,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
SizedBox(width: 8.w),
Expanded(
child: Container(
height: 1,
color: isDarkMode ? Colors.white24 : Colors.black12,
),
),
if (title == '选择文件')
IconButton(
icon: Icon(
Icons.add_circle_outline,
color: isDarkMode ? Colors.blue[300] : Colors.blue,
),
onPressed: () {
controller.pickFiles();
},
),
],
),
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(8.r),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.03),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: child,
),
],
);
}
}

176
lib/modules/pairing/controllers/pairing_controller.dart

@ -11,27 +11,27 @@ import '../../../core/utils/permission_util.dart';
/// 配对状态枚举 - 简化版本 /// 配对状态枚举 - 简化版本
enum PairingState { enum PairingState {
idle, // 空闲/初始状态 idle, // 空闲/初始状态
checking, // 检查中(蓝牙、权限等) checking, // 检查中(蓝牙、权限等)
waitingClassic, // 等待经典蓝牙连接 waitingClassic, // 等待经典蓝牙连接
verifying, // 验证设备 verifying, // 验证设备
associating, // 正在进行BLE设备关联 associating, // 正在进行BLE设备关联
waitingBle, // 等待BLE连接 waitingBle, // 等待BLE连接
completed, // 配对完成 completed, // 配对完成
error, // 错误状态 error, // 错误状态
} }
/// 错误类型枚举 /// 错误类型枚举
enum ErrorType { enum ErrorType {
none, none,
bluetoothOff, // 蓝牙关闭 bluetoothOff, // 蓝牙关闭
noPermission, // 无权限 noPermission, // 无权限
noClassicDevice, // 无经典蓝牙设备 noClassicDevice, // 无经典蓝牙设备
deviceNotMatch, // 设备不匹配 deviceNotMatch, // 设备不匹配
deviceBound, // 设备已被绑定 deviceBound, // 设备已被绑定
associationFailed, // 关联失败 associationFailed, // 关联失败
bleTimeout, // BLE连接超时 bleTimeout, // BLE连接超时
networkError, // 网络错误 networkError, // 网络错误
} }
/// 配对控制器 - 基于简化状态机的实现 /// 配对控制器 - 基于简化状态机的实现
@ -43,7 +43,7 @@ class PairingController extends GetxController {
final _currentState = PairingState.idle.obs; final _currentState = PairingState.idle.obs;
final _errorType = ErrorType.none.obs; final _errorType = ErrorType.none.obs;
final _errorMessage = ''.obs; final _errorMessage = ''.obs;
PairingState get currentState => _currentState.value; PairingState get currentState => _currentState.value;
ErrorType get errorType => _errorType.value; ErrorType get errorType => _errorType.value;
String get errorMessage => _errorMessage.value; String get errorMessage => _errorMessage.value;
@ -68,7 +68,7 @@ class PairingController extends GetxController {
// ========== 当前设备信息 ========== // ========== 当前设备信息 ==========
BluetoothDevice? _currentClassicDevice; BluetoothDevice? _currentClassicDevice;
String? _verifiedDeviceAddress; String? _verifiedDeviceAddress;
// Public getter for view access // Public getter for view access
BluetoothDevice? get currentDevice => _currentClassicDevice; BluetoothDevice? get currentDevice => _currentClassicDevice;
@ -76,14 +76,14 @@ class PairingController extends GetxController {
void onInit() { void onInit() {
super.onInit(); super.onInit();
Logger.i(_tag, '配对控制器初始化'); Logger.i(_tag, '配对控制器初始化');
// 确保服务已初始化 // 确保服务已初始化
_bluetoothManager = Get.find<BluetoothManager>(); _bluetoothManager = Get.find<BluetoothManager>();
_bleManager = Get.find<BleManager>(); _bleManager = Get.find<BleManager>();
// 集中设置所有监听器 // 集中设置所有监听器
_setupAllListeners(); _setupAllListeners();
// 开始初始状态检查 // 开始初始状态检查
// 延迟稍长时间,确保BluetoothManager有足够时间获取已连接设备 // 延迟稍长时间,确保BluetoothManager有足够时间获取已连接设备
Future.delayed(const Duration(milliseconds: 300), () { Future.delayed(const Duration(milliseconds: 300), () {
@ -99,10 +99,10 @@ class PairingController extends GetxController {
worker.dispose(); worker.dispose();
} }
_workers.clear(); _workers.clear();
// 清理定时器 // 清理定时器
_bleTimeoutTimer?.cancel(); _bleTimeoutTimer?.cancel();
super.onClose(); super.onClose();
} }
@ -112,42 +112,37 @@ class PairingController extends GetxController {
void _setupAllListeners() { void _setupAllListeners() {
// 1. 监听蓝牙开关状态 // 1. 监听蓝牙开关状态
_setupBluetoothStateListener(); _setupBluetoothStateListener();
// 2. 监听经典蓝牙连接状态 // 2. 监听经典蓝牙连接状态
_setupClassicConnectionListener(); _setupClassicConnectionListener();
// 3. 监听BLE连接状态 // 3. 监听BLE连接状态
_setupBleConnectionListener(); _setupBleConnectionListener();
} }
/// 监听蓝牙开关状态 /// 监听蓝牙开关状态
void _setupBluetoothStateListener() { void _setupBluetoothStateListener() {
_workers.add( _workers.add(ever(_bluetoothManager.bluetoothStateRx, (String state) {
ever(_bluetoothManager.bluetoothStateRx, (String state) { Logger.d(_tag, '蓝牙状态变化: $state');
Logger.d(_tag, '蓝牙状态变化: $state'); _handleBluetoothStateChange(state);
_handleBluetoothStateChange(state); }));
})
);
} }
/// 监听经典蓝牙连接状态 /// 监听经典蓝牙连接状态
void _setupClassicConnectionListener() { void _setupClassicConnectionListener() {
_workers.add( _workers
ever(_bluetoothManager.currentDeviceRx, (BluetoothDevice? device) { .add(ever(_bluetoothManager.currentDeviceRx, (BluetoothDevice? device) {
Logger.d(_tag, '经典蓝牙设备变化: ${device?.name}'); Logger.d(_tag, '经典蓝牙设备变化: ${device?.name}');
_handleClassicDeviceChange(device); _handleClassicDeviceChange(device);
}) }));
);
} }
/// 监听BLE连接状态 /// 监听BLE连接状态
void _setupBleConnectionListener() { void _setupBleConnectionListener() {
_workers.add( _workers.add(ever(_bleManager.connectionStateRx, (int state) {
ever(_bleManager.connectionStateRx, (int state) { Logger.d(_tag, 'BLE连接状态变化: $state');
Logger.d(_tag, 'BLE连接状态变化: $state'); _handleBleStateChange(state);
_handleBleStateChange(state); }));
})
);
} }
// ==================== 状态处理 ==================== // ==================== 状态处理 ====================
@ -171,7 +166,7 @@ class PairingController extends GetxController {
/// 处理经典蓝牙设备变化 /// 处理经典蓝牙设备变化
void _handleClassicDeviceChange(BluetoothDevice? device) { void _handleClassicDeviceChange(BluetoothDevice? device) {
_currentClassicDevice = device; _currentClassicDevice = device;
switch (_currentState.value) { switch (_currentState.value) {
case PairingState.waitingClassic: case PairingState.waitingClassic:
if (device != null) { if (device != null) {
@ -181,7 +176,7 @@ class PairingController extends GetxController {
_verifyDevice(device); _verifyDevice(device);
} }
break; break;
case PairingState.error: case PairingState.error:
// 如果在错误状态下检测到设备连接,且错误是无设备连接,则自动恢复 // 如果在错误状态下检测到设备连接,且错误是无设备连接,则自动恢复
if (device != null && _errorType.value == ErrorType.noClassicDevice) { if (device != null && _errorType.value == ErrorType.noClassicDevice) {
@ -191,7 +186,7 @@ class PairingController extends GetxController {
_verifyDevice(device); _verifyDevice(device);
} }
break; break;
case PairingState.verifying: case PairingState.verifying:
case PairingState.waitingBle: case PairingState.waitingBle:
case PairingState.completed: case PairingState.completed:
@ -200,7 +195,7 @@ class PairingController extends GetxController {
_setError(ErrorType.noClassicDevice, '设备已断开'); _setError(ErrorType.noClassicDevice, '设备已断开');
} }
break; break;
default: default:
break; break;
} }
@ -209,12 +204,13 @@ class PairingController extends GetxController {
/// 处理BLE状态变化 /// 处理BLE状态变化
void _handleBleStateChange(int state) { void _handleBleStateChange(int state) {
if (_currentState.value == PairingState.waitingBle) { if (_currentState.value == PairingState.waitingBle) {
if (state == 2) { // BLE已连接 if (state == 2) {
// BLE已连接
_bleTimeoutTimer?.cancel(); _bleTimeoutTimer?.cancel();
_transition(PairingState.completed); _transition(PairingState.completed);
} }
} }
// 如果在完成状态下BLE断开,显示警告但不改变状态 // 如果在完成状态下BLE断开,显示警告但不改变状态
if (_currentState.value == PairingState.completed && state != 2) { if (_currentState.value == PairingState.completed && state != 2) {
Logger.w(_tag, 'BLE连接已断开,但配对流程已完成'); Logger.w(_tag, 'BLE连接已断开,但配对流程已完成');
@ -228,7 +224,7 @@ class PairingController extends GetxController {
final oldState = _currentState.value; final oldState = _currentState.value;
_currentState.value = newState; _currentState.value = newState;
Logger.i(_tag, '状态转换: $oldState -> $newState'); Logger.i(_tag, '状态转换: $oldState -> $newState');
// 根据新状态执行相应操作 // 根据新状态执行相应操作
_onStateChanged(newState); _onStateChanged(newState);
} }
@ -240,29 +236,29 @@ class PairingController extends GetxController {
showConnectButton.value = false; showConnectButton.value = false;
showRetryButton.value = false; showRetryButton.value = false;
showSkipButton.value = false; showSkipButton.value = false;
switch (state) { switch (state) {
case PairingState.idle: case PairingState.idle:
statusText.value = '准备开始配对'; statusText.value = '准备开始配对';
break; break;
case PairingState.checking: case PairingState.checking:
statusText.value = '正在检查环境...'; statusText.value = '正在检查环境...';
isLoading.value = true; isLoading.value = true;
break; break;
case PairingState.waitingClassic: case PairingState.waitingClassic:
statusText.value = '请确保耳机已开启并进入配对模式'; statusText.value = '请确保耳机已开启并进入配对模式';
showConnectButton.value = true; showConnectButton.value = true;
showSkipButton.value = true; showSkipButton.value = true;
connectButtonText.value = '前往蓝牙设置'; connectButtonText.value = '前往蓝牙设置';
break; break;
case PairingState.verifying: case PairingState.verifying:
statusText.value = '正在验证设备...'; statusText.value = '正在验证设备...';
isLoading.value = true; isLoading.value = true;
break; break;
case PairingState.associating: case PairingState.associating:
statusText.value = '正在关联设备...'; statusText.value = '正在关联设备...';
isLoading.value = true; isLoading.value = true;
@ -271,7 +267,7 @@ class PairingController extends GetxController {
_startDeviceAssociation(); _startDeviceAssociation();
}); });
break; break;
case PairingState.waitingBle: case PairingState.waitingBle:
statusText.value = '正在建立安全连接...'; statusText.value = '正在建立安全连接...';
isLoading.value = true; isLoading.value = true;
@ -280,18 +276,18 @@ class PairingController extends GetxController {
_startBleConnection(); _startBleConnection();
}); });
break; break;
case PairingState.completed: case PairingState.completed:
statusText.value = '配对成功!'; statusText.value = '配对成功!';
isLoading.value = false; isLoading.value = false;
_navigateToHome(); _navigateToHome();
break; break;
case PairingState.error: case PairingState.error:
statusText.value = _errorMessage.value; statusText.value = _errorMessage.value;
isLoading.value = false; isLoading.value = false;
showSkipButton.value = true; showSkipButton.value = true;
// 根据错误类型决定显示哪个按钮和按钮文本 // 根据错误类型决定显示哪个按钮和按钮文本
switch (_errorType.value) { switch (_errorType.value) {
case ErrorType.bluetoothOff: case ErrorType.bluetoothOff:
@ -350,28 +346,27 @@ class PairingController extends GetxController {
_setError(ErrorType.bluetoothOff, '请开启蓝牙'); _setError(ErrorType.bluetoothOff, '请开启蓝牙');
return; return;
} }
// 2. 检查权限 // 2. 检查权限
if (!await _checkPermissions()) { if (!await _checkPermissions()) {
_setError(ErrorType.noPermission, '请授予必要权限'); _setError(ErrorType.noPermission, '请授予必要权限');
return; return;
} }
// 3. 先刷新设备连接状态,确保获取最新的连接信息 // 3. 先刷新设备连接状态,确保获取最新的连接信息
await _bluetoothManager.refreshDeviceConnection(); await _bluetoothManager.refreshDeviceConnection();
// 4. 检查经典蓝牙连接 // 4. 检查经典蓝牙连接
final classicDevice = _bluetoothManager.currentDevice; final classicDevice = _bluetoothManager.currentDevice;
if (classicDevice == null) { if (classicDevice == null) {
_transition(PairingState.waitingClassic); _transition(PairingState.waitingClassic);
return; return;
} }
// 5. 如果有设备,进行验证 // 5. 如果有设备,进行验证
_currentClassicDevice = classicDevice; _currentClassicDevice = classicDevice;
_transition(PairingState.verifying); _transition(PairingState.verifying);
await _verifyDevice(classicDevice); await _verifyDevice(classicDevice);
} catch (e) { } catch (e) {
Logger.e(_tag, '状态检查失败: $e'); Logger.e(_tag, '状态检查失败: $e');
_setError(ErrorType.networkError, '检查失败,请重试'); _setError(ErrorType.networkError, '检查失败,请重试');
@ -386,10 +381,10 @@ class PairingController extends GetxController {
final bluetoothScan = await Permission.bluetoothScan.request(); final bluetoothScan = await Permission.bluetoothScan.request();
final bluetoothConnect = await Permission.bluetoothConnect.request(); final bluetoothConnect = await Permission.bluetoothConnect.request();
final location = await Permission.locationWhenInUse.request(); final location = await Permission.locationWhenInUse.request();
return bluetoothScan.isGranted && return bluetoothScan.isGranted &&
bluetoothConnect.isGranted && bluetoothConnect.isGranted &&
location.isGranted; location.isGranted;
} }
return true; return true;
} }
@ -398,28 +393,31 @@ class PairingController extends GetxController {
Future<void> _verifyDevice(BluetoothDevice device) async { Future<void> _verifyDevice(BluetoothDevice device) async {
try { try {
Logger.i(_tag, '开始验证设备: ${device.name} (${device.address})'); Logger.i(_tag, '开始验证设备: ${device.name} (${device.address})');
// TODO: 实现API验证逻辑 // TODO: 实现API验证逻辑
// 暂时模拟验证通过 // 暂时模拟验证通过
await Future.delayed(const Duration(seconds: 2)); await Future.delayed(const Duration(seconds: 2));
// 检查设备名称是否包含 DeepSound 或 DeapSound // 检查设备名称是否包含 DeepSound 或 DeapSound
final deviceName = device.name ?? ''; final deviceName = device.name ?? '';
if (deviceName.contains('DeepSound') || deviceName.contains('DeapSound')) { if (deviceName.contains('DeepSound') ||
deviceName.contains('DeapSound')) {
Logger.i(_tag, '设备验证通过'); Logger.i(_tag, '设备验证通过');
_verifiedDeviceAddress = device.address; _verifiedDeviceAddress = device.address;
// 检查是否已有BLE关联 // 检查是否已有BLE关联
final pairedMacAddress = await _bleManager.getPairedMacAddress(); final pairedMacAddress = await _bleManager.getPairedMacAddress();
Logger.i(_tag, '已关联的MAC地址: $pairedMacAddress'); Logger.i(_tag, '已关联的MAC地址: $pairedMacAddress');
if (pairedMacAddress != null && pairedMacAddress.toUpperCase() == device.address.toUpperCase()) { if (pairedMacAddress != null &&
pairedMacAddress.toUpperCase() == device.address.toUpperCase()) {
// 已有关联,检查BLE连接状态 // 已有关联,检查BLE连接状态
Logger.i(_tag, '设备已关联,检查BLE连接状态'); Logger.i(_tag, '设备已关联,检查BLE连接状态');
final currentBleState = await _bleManager.getConnectionState(); final currentBleState = await _bleManager.getConnectionState();
Logger.i(_tag, '验证时BLE状态: $currentBleState'); Logger.i(_tag, '验证时BLE状态: $currentBleState');
if (currentBleState == 2) { // BleConnectionState.connected if (currentBleState == 2) {
// BleConnectionState.connected
Logger.i(_tag, 'BLE已连接,直接完成配对'); Logger.i(_tag, 'BLE已连接,直接完成配对');
_transition(PairingState.completed); _transition(PairingState.completed);
} else { } else {
@ -447,15 +445,15 @@ class PairingController extends GetxController {
_setError(ErrorType.associationFailed, '设备地址无效'); _setError(ErrorType.associationFailed, '设备地址无效');
return; return;
} }
Logger.i(_tag, '开始BLE设备关联: $_verifiedDeviceAddress'); Logger.i(_tag, '开始BLE设备关联: $_verifiedDeviceAddress');
// 调用BLE管理器的关联方法 // 调用BLE管理器的关联方法
final success = await _bleManager.startDeviceAssociation( final success = await _bleManager.startDeviceAssociation(
deviceNamePattern: 'DeapSound', // 或者使用 _currentClassicDevice?.name deviceNamePattern: 'DeapSound', // 或者使用 _currentClassicDevice?.name
macAddress: _verifiedDeviceAddress!, macAddress: _verifiedDeviceAddress!,
); );
if (success) { if (success) {
Logger.i(_tag, 'BLE设备关联成功'); Logger.i(_tag, 'BLE设备关联成功');
// 关联成功后,转到等待BLE连接状态 // 关联成功后,转到等待BLE连接状态
@ -479,21 +477,23 @@ class PairingController extends GetxController {
_setError(ErrorType.bleTimeout, 'BLE连接超时,请重试'); _setError(ErrorType.bleTimeout, 'BLE连接超时,请重试');
} }
}); });
// 检查是否已连接 - 直接从原生层获取最新状态 // 检查是否已连接 - 直接从原生层获取最新状态
final currentBleState = await _bleManager.getConnectionState(); final currentBleState = await _bleManager.getConnectionState();
Logger.i(_tag, 'BLE当前连接状态: $currentBleState'); Logger.i(_tag, 'BLE当前连接状态: $currentBleState');
if (currentBleState == 2) { // BleConnectionState.connected if (currentBleState == 2) {
// BleConnectionState.connected
Logger.i(_tag, 'BLE已经连接,直接完成配对'); Logger.i(_tag, 'BLE已经连接,直接完成配对');
_bleTimeoutTimer?.cancel(); _bleTimeoutTimer?.cancel();
_transition(PairingState.completed); _transition(PairingState.completed);
return; return;
} }
// 注册后台扫描以开始BLE配对 // 注册后台扫描以开始BLE配对
if (_verifiedDeviceAddress != null) { if (_verifiedDeviceAddress != null) {
final success = await _bleManager.registerBackgroundScan(macAddress: _verifiedDeviceAddress); final success = await _bleManager.registerBackgroundScan(
macAddress: _verifiedDeviceAddress);
if (!success && _currentState.value == PairingState.waitingBle) { if (!success && _currentState.value == PairingState.waitingBle) {
_setError(ErrorType.bleTimeout, 'BLE配对启动失败'); _setError(ErrorType.bleTimeout, 'BLE配对启动失败');
} }
@ -524,7 +524,7 @@ class PairingController extends GetxController {
/// 用户点击连接按钮 /// 用户点击连接按钮
void onConnectTap() { void onConnectTap() {
Logger.i(_tag, '用户点击连接按钮'); Logger.i(_tag, '用户点击连接按钮');
// 根据当前状态或错误类型决定行为 // 根据当前状态或错误类型决定行为
if (_currentState.value == PairingState.error) { if (_currentState.value == PairingState.error) {
switch (_errorType.value) { switch (_errorType.value) {
@ -558,7 +558,7 @@ class PairingController extends GetxController {
/// 用户点击重试按钮 /// 用户点击重试按钮
void onRetryTap() { void onRetryTap() {
Logger.i(_tag, '用户点击重试按钮'); Logger.i(_tag, '用户点击重试按钮');
// 如果是权限问题,引导去设置 // 如果是权限问题,引导去设置
if (_errorType.value == ErrorType.noPermission) { if (_errorType.value == ErrorType.noPermission) {
Logger.i(_tag, '引导用户去应用设置授予权限'); Logger.i(_tag, '引导用户去应用设置授予权限');
@ -589,7 +589,7 @@ class PairingController extends GetxController {
void openBluetoothSettings() { void openBluetoothSettings() {
AppSettings.openAppSettings(type: AppSettingsType.bluetooth); AppSettings.openAppSettings(type: AppSettingsType.bluetooth);
} }
/// 清除设备关联(在需要重新配对时调用) /// 清除设备关联(在需要重新配对时调用)
Future<void> clearDeviceAssociation() async { Future<void> clearDeviceAssociation() async {
try { try {
@ -599,4 +599,4 @@ class PairingController extends GetxController {
Logger.e(_tag, '清除设备关联失败: $e'); Logger.e(_tag, '清除设备关联失败: $e');
} }
} }
} }

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

@ -543,6 +543,28 @@ class SettingsView extends GetView<SettingsController> {
}, },
isDarkMode: isDarkMode, isDarkMode: isDarkMode,
), ),
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// BLE测试
_buildSimpleNavigationSetting(
title: 'opus解码测试',
subtitle: '测试opus解码',
icon: Icons.bluetooth_searching,
iconBgColor: isDarkMode
? Colors.green[900]!.withOpacity(0.3)
: Colors.green[100]!,
iconColor:
isDarkMode ? Colors.green[300]! : Colors.green[600]!,
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
onTap: () {
Get.toNamed(Routes.opusTest);
},
isDarkMode: isDarkMode,
),
Divider( Divider(
height: 1, height: 1,
color: isDarkMode color: isDarkMode

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

@ -24,13 +24,18 @@ class TranslationView extends GetView<TranslationController> {
PreferredSizeWidget _buildAppBar(bool isDarkMode) { PreferredSizeWidget _buildAppBar(bool isDarkMode) {
// 在同一个类中添加辅助方法 // 在同一个类中添加辅助方法
PopupMenuItem<String> _buildPopupMenuItem(String value, String text) { PopupMenuItem<String> _buildPopupMenuItem(String value, String text,
{bool enabled = true // 默认启用
}) {
return PopupMenuItem( return PopupMenuItem(
value: value, value: value,
enabled: enabled, // 设置启用状态
child: Text( child: Text(
text, text,
style: TextStyle( style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black, color: enabled
? null // 使用默认颜色
: Colors.grey, // 禁用时显示灰色
), ),
), ),
); );
@ -59,8 +64,16 @@ class TranslationView extends GetView<TranslationController> {
itemBuilder: (BuildContext context) => [ itemBuilder: (BuildContext context) => [
_buildPopupMenuItem('simultaneous', 'simultaneousTranslation'.tr), _buildPopupMenuItem('simultaneous', 'simultaneousTranslation'.tr),
_buildPopupMenuItem('faceToFace', 'faceToFaceTranslation'.tr), _buildPopupMenuItem('faceToFace', 'faceToFaceTranslation'.tr),
_buildPopupMenuItem('audioVideo', 'audioVideoTranslation'.tr), _buildPopupMenuItem(
_buildPopupMenuItem('call', 'callTranslation'.tr), 'audioVideo',
'audioVideoTranslation'.tr,
enabled: true, // 根据连接状态启用/禁用
),
_buildPopupMenuItem(
'call',
'callTranslation'.tr,
enabled: true, // 根据连接状态启用/禁用
),
], ],
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, // 关键修改:让宽度自适应内容 mainAxisSize: MainAxisSize.min, // 关键修改:让宽度自适应内容

9
lib/routes/app_pages.dart

@ -11,6 +11,8 @@ import '../modules/meeting/views/meeting_details_view.dart';
import '../modules/meeting/bindings/meeting_record_binding.dart'; import '../modules/meeting/bindings/meeting_record_binding.dart';
import '../modules/meeting/views/meeting_record_view.dart'; import '../modules/meeting/views/meeting_record_view.dart';
import '../modules/meeting/views/meeting_view.dart'; import '../modules/meeting/views/meeting_view.dart';
import '../modules/opus_test/bindings/opus_test_binding.dart';
import '../modules/opus_test/views/opus_test_view.dart';
import '../modules/profile/views/profile_view.dart'; import '../modules/profile/views/profile_view.dart';
import '../modules/profile/bindings/profile_binding.dart'; import '../modules/profile/bindings/profile_binding.dart';
import '../modules/chat/bindings/chat_binding.dart'; import '../modules/chat/bindings/chat_binding.dart';
@ -189,6 +191,13 @@ abstract class AppPages {
transition: Transition.rightToLeft, transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250), transitionDuration: Duration(milliseconds: 250),
), ),
GetPage(
name: Routes.opusTest,
page: () => const OpusTestView(),
binding: OpusTestBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage( GetPage(
name: Routes.agent, name: Routes.agent,
page: () => const AgentView(), page: () => const AgentView(),

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

@ -90,6 +90,21 @@ object AgentService : CoroutineScope {
// 空闲检测相关 // 空闲检测相关
private var idleCheckJob: Job? = null private var idleCheckJob: Job? = null
private val maxIdleSeconds = 10 // 最大空闲秒数 private val maxIdleSeconds = 10 // 最大空闲秒数
// 打断防抖相关
private var lastInterruptTime = 0L
private val INTERRUPT_DEBOUNCE_MS = 300L
// 语气词过滤列表(中英文)
private val FILLER_WORDS = setOf(
// 中文语气词
"嗯", "啊", "呃", "哦", "唉", "哎", "哈", "呵", "嘿", "喂",
"哇", "呀", "哟", "嘛", "吧", "呢", "啦", "咦", "嘻", "嗨",
"额", "这个", "那个", "就是", "然后",
// 英文语气词
"um", "uh", "ah", "oh", "eh", "er", "hmm", "huh", "wow",
"well", "like", "you know", "i mean", "so", "actually"
)
/** /**
* 初始化系统提示词 * 初始化系统提示词
*/ */
@ -381,8 +396,28 @@ object AgentService : CoroutineScope {
"language" to detectedLanguage "language" to detectedLanguage
)) ))
// 如果TTS正在播放或AI正在生成,则触发打断 // 如果TTS正在播放或AI正在生成,检查是否需要打断
if (isTtsSpeaking || isAiStreaming) { if (isTtsSpeaking || isAiStreaming) {
val currentTime = System.currentTimeMillis()
// 防抖处理:避免过于频繁的打断
if (currentTime - lastInterruptTime < INTERRUPT_DEBOUNCE_MS) {
return
}
// 简单过滤:太短的内容可能是噪音
if (recognizing.trim().length < 2) {
return
}
// 过滤纯语气词
val trimmedText = recognizing.trim().lowercase()
if (FILLER_WORDS.contains(trimmedText)) {
return
}
// 执行打断
lastInterruptTime = currentTime
interruptCurrentResponse() interruptCurrentResponse()
} }
} }
@ -482,14 +517,21 @@ object AgentService : CoroutineScope {
*/ */
fun interruptCurrentResponse() { fun interruptCurrentResponse() {
if(isAiStreaming || isTtsSpeaking) { if(isAiStreaming || isTtsSpeaking) {
// 停止TTS播放 val startTime = System.currentTimeMillis()
stopTts()
// 停止AI流输出 // 并行执行停止操作,加快响应速度
stopAiStream() launch { stopTts() }
launch { stopAiStream() }
// 发送打断事件 // 记录打断耗时
sendEvent("response_interrupted", mapOf("status" to "interrupted")) launch {
delay(100) // 短暂延迟后计算耗时
val duration = System.currentTimeMillis() - startTime
sendEvent("response_interrupted", mapOf(
"status" to "interrupted",
"duration_ms" to duration
))
}
} }
} }
@ -739,13 +781,13 @@ object AgentService : CoroutineScope {
override fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) { override fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) {
audioPlayer?.stopAudio() audioPlayer?.stopAudio()
val name = functionCall.get("name") as String; val name = functionCall.get("name") as String;
val (metestr, _broadcast)= autoHandleFcunCallResult(name,functionCallResult); val resultmap = autoHandleFcunCallResult(name,functionCallResult);
aiMetadata = metestr; aiMetadata = resultmap["meta"] as String;
broadcast = _broadcast broadcast = resultmap["broadcast"] as Boolean;
sendEvent("function_call_result", mapOf( sendEvent("function_call_result", mapOf(
"function_call" to functionCall.toString(), "function_call" to functionCall.toString(),
"result" to functionCallResult.toString(), "result" to functionCallResult.toString(),
"meta" to metestr, "meta" to aiMetadata,
)) ))
} }
} }
@ -1116,7 +1158,7 @@ object AgentService : CoroutineScope {
/* /*
* 自动播放音乐 * 自动播放音乐
* */ * */
fun autoHandleFcunCallResult(toolname:String,functionCallResult: JSONObject): Pair<String, Boolean>{ fun autoHandleFcunCallResult(toolname:String,functionCallResult: JSONObject): Map<String,Any>{
println("协议工具返回数据 检查是否存在卡片! functionCallResult: ${functionCallResult}") println("协议工具返回数据 检查是否存在卡片! functionCallResult: ${functionCallResult}")
try { try {
val contentStr = functionCallResult.optString("context") val contentStr = functionCallResult.optString("context")
@ -1126,13 +1168,18 @@ object AgentService : CoroutineScope {
println("协议工具返回数据 检查是否存在卡片! textStr: ${textStr}") println("协议工具返回数据 检查是否存在卡片! textStr: ${textStr}")
val metadata: MutableMap<String, Any> = mutableMapOf() val metadata: MutableMap<String, Any> = mutableMapOf()
var broadcast = true; var broadcast = true;
var notreply = false;
if (textStr.isNotEmpty()) { if (textStr.isNotEmpty()) {
val meta = JSONObject(textStr) val meta = JSONObject(textStr)
metadata[toolname] = meta // 现在可以赋值
val metaStr = JSONObject(metadata as Map<*, *>).toString()
if (meta.has("broadcast")){ if (meta.has("broadcast")){
broadcast = meta["broadcast"] as Boolean broadcast = meta["broadcast"] as Boolean
} }
if (meta.has("notreply")){
notreply = meta["notreply"] as Boolean
}
metadata[toolname] = meta // 现在可以赋值
metadata["notreply"] = notreply // 现在可以赋值
val metaStr = JSONObject(metadata as Map<*, *>).toString()
// Log.d(TAG, "mcp调用结果: $metaStr") // Log.d(TAG, "mcp调用结果: $metaStr")
if (meta.has("card_music")) { //音乐卡片 if (meta.has("card_music")) { //音乐卡片
val cardMusic = meta.getJSONObject("card_music") val cardMusic = meta.getJSONObject("card_music")
@ -1190,13 +1237,13 @@ object AgentService : CoroutineScope {
val end = cardNavigation.optString("end", "") val end = cardNavigation.optString("end", "")
processNavigation(start, end); processNavigation(start, end);
} }
return Pair(metaStr, broadcast); return mapOf("meta" to metaStr, "broadcast" to broadcast,"notreply" to notreply);
} }
} catch (e: Exception) { } catch (e: Exception) {
// 捕获其他类型的异常(可选) // 捕获其他类型的异常(可选)
println("为解析到卡片数据!") println("为解析到卡片数据!")
} }
return Pair("", true); return mapOf("meta" to "", "broadcast" to true,"notreply" to false);
} }
/** /**

2
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt

@ -395,7 +395,7 @@ class BleCompanionManager(private val context: Context) {
// 构建过滤器 // 构建过滤器
val filter = ScanFilter.Builder() val filter = ScanFilter.Builder()
.setDeviceAddress(mac) .setDeviceAddress(mac)
// .setManufacturerData(0xEFAB, byteArrayOf(0xA0.toByte(), 0x01)) .setManufacturerData(0xEFAB, byteArrayOf(0xA0.toByte(), 0x01))
.build() .build()
// 扫描设置 // 扫描设置

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

@ -1084,8 +1084,6 @@ else if(codecStatus == BleConst.CODEC_CONTROL_CLOSE)
* 控制编解码 - 打开解码 * 控制编解码 - 打开解码
*/ */
fun openDecoder(): Boolean { fun openDecoder(): Boolean {
Log.i(TAG, "打开编码 (设备侧)...")
// val opusStarted = startOpusStreamDecoding( // val opusStarted = startOpusStreamDecoding(
// hasHeader = false, // BLE传输的Opus数据通常无文件头 // hasHeader = false, // BLE传输的Opus数据通常无文件头
// channel = 1, // 单声道 // channel = 1, // 单声道
@ -1095,9 +1093,9 @@ else if(codecStatus == BleConst.CODEC_CONTROL_CLOSE)
// // The library might expect this in a different unit or derive it. // // The library might expect this in a different unit or derive it.
// // Given JlOpusPlugin.kt, packetSize refers to Opus encoded frame duration in ms. // // Given JlOpusPlugin.kt, packetSize refers to Opus encoded frame duration in ms.
// ) // )
startOpusStreamDecoding(false, 1, 16000, 40)
Log.i(TAG, "打开解码0xA2")
Log.i(TAG, "打开解码...")
return sendCommand( return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf( BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(
BleConst.CODEC_CONTROL_DECODE_ON.toByte(), BleConst.CODEC_CONTROL_DECODE_ON.toByte(),
@ -1111,8 +1109,8 @@ else if(codecStatus == BleConst.CODEC_CONTROL_CLOSE)
* 控制编解码 - 打开解码 * 控制编解码 - 打开解码
*/ */
fun openA2DPDecoder(): Boolean { fun openA2DPDecoder(): Boolean {
Log.i(TAG, "打开编码 (设备侧)...") Log.i(TAG, "打开编码 0xA1")
startOpusStreamDecoding(false, 1, 16000, 80)
// Log.i(TAG, "打开解码...") // Log.i(TAG, "打开解码...")
return sendCommand( return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf( BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(
@ -1135,9 +1133,9 @@ else if(codecStatus == BleConst.CODEC_CONTROL_CLOSE)
* 控制编解码 - 打开编码 * 控制编解码 - 打开编码
*/ */
fun openEncoder(): Boolean { fun openEncoder(): Boolean {
Log.i(TAG, "打开编码 (设备侧)...") Log.i(TAG, "打开编码0xB1")
startOpusStreamDecoding(false, 1, 16000, 40)
// 目前仅发送命令通知设备开始编码。 // 目前仅发送命令通知设备开始编码。
return sendCommand( return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), BleConst.CMD_CONTROL_CODEC.toByte(),
@ -1410,11 +1408,8 @@ else if(codecStatus == BleConst.CODEC_CONTROL_CLOSE)
// 创建新的音频文件 // 创建新的音频文件
val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()) val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault())
val timestamp = dateFormat.format(Date()) val timestamp = dateFormat.format(Date())
val parentDir = File(context.filesDir, "OpusAudio")
if (!parentDir.exists()) { val filePath = File(context.getExternalFilesDir(null), "${fileName}_${timestamp}.opus").absolutePath
parentDir.mkdirs()
}
val filePath = File(parentDir, "${fileName}_${timestamp}.opus").absolutePath
currentAudioFile = File(filePath) currentAudioFile = File(filePath)
currentAudioFile?.createNewFile() currentAudioFile?.createNewFile()
// 追加音频数据到文件 // 追加音频数据到文件
@ -1445,7 +1440,17 @@ else if(codecStatus == BleConst.CODEC_CONTROL_CLOSE)
internal fun saveAudioDataToWav(buffer: ByteArray) { internal fun saveAudioDataToWav(buffer: ByteArray) {
if (fos == null || currentAudioFile == null) return if (fos == null || currentAudioFile == null) return
// 放入队列,由写线程写入 // 放入队列,由写线程写入
writeQueue.offer(buffer.copyOf()) // 1. 将字节数据转换为十六进制字符串
// val hexData = buildString {
// buffer.forEachIndexed { index, byte ->
// append("%02X".format(byte))
// }
// }
// 2. 写入十六进制字符串
writeQueue.offer(buffer)
totalBytesWritten += buffer.size totalBytesWritten += buffer.size
} }

12
local_plugins/bytedance_speech/lib/bytedance_speech.dart

@ -0,0 +1,12 @@
library bytedance_speech;
import 'package:flutter/services.dart';
class BytedanceSpeech {
static const MethodChannel _channel = MethodChannel('bytedance_speech');
static Future<String?> getPlatformVersion() async {
final String? version = await _channel.invokeMethod('getPlatformVersion');
return version;
}
}

256
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt

@ -24,6 +24,7 @@ import org.json.JSONObject
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import java.util.UUID import java.util.UUID
import java.util.Collections
/** /**
* ChatAPI服务异常 * ChatAPI服务异常
@ -70,8 +71,32 @@ private data class ToolCallInfo(
var arguments: String = "" var arguments: String = ""
) { ) {
fun isValid(): Boolean { fun isValid(): Boolean {
// 确保参数是完整的 JSON
return id.isNotEmpty() && name.isNotEmpty() &&
arguments.isNotEmpty() && isCompleteJson(arguments)
}
private fun isCompleteJson(json: String): Boolean {
return try { return try {
id.isNotEmpty() && name.isNotEmpty() // 检查 JSON 是否完整(括号匹配)
val trimmed = json.trim()
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
return false
}
// 简单的括号匹配检查
var braceCount = 0
for (char in trimmed) {
when (char) {
'{' -> braceCount++
'}' -> braceCount--
}
if (braceCount < 0) return false
}
if (braceCount != 0) return false
// 验证是否是有效的 JSON
JSONObject(json)
true
} catch (e: Exception) { } catch (e: Exception) {
false false
} }
@ -110,12 +135,10 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
// 流式请求相关 // 流式请求相关
private var currentStreamJob: Job? = null private var currentStreamJob: Job? = null
// private var streamCallback: StreamCallback? = null
private var currentMessages: List<Map<String, Any>> = emptyList() private var currentMessages: List<Map<String, Any>> = emptyList()
private var toolCalls: MutableMap<Int, ToolCallInfo> = mutableMapOf() private var toolCalls: MutableMap<Int, ToolCallInfo> = Collections.synchronizedMap(mutableMapOf())
private var sessions: MutableMap<String, StreamCallback?> = mutableMapOf() private var currSessionId = ""
private var currSessionId = "" // private var currSessionCallback: StreamCallback? = null
// private var isCanceled = false
@ -125,11 +148,33 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
// MARK: - 公共方法 // MARK: - 公共方法
/** /**
* 设置流式回调 * 中止当前会话
*/ */
// fun setStreamCallback(callback: StreamCallback) { private fun abortCurrentSession() {
// streamCallback = callback if (currSessionId.isNotEmpty()) {
// } // 1. 取消当前的协程任务
currentStreamJob?.cancel()
currentStreamJob = null
// 2. 通知旧会话被中止
currSessionCallback?.onError(ChatApiException("Session aborted by new request"))
// 3. 清理状态
currSessionId = ""
currSessionCallback = null
toolCalls.clear()
}
}
/**
* 清理当前会话
*/
private fun clearCurrentSession() {
currSessionId = ""
currSessionCallback = null
currentStreamJob = null
toolCalls.clear()
}
/** /**
* 初始化ChatAPI服务 * 初始化ChatAPI服务
@ -262,29 +307,34 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
* 发送消息(流式输出)- 兼容JSONArray格式 * 发送消息(流式输出)- 兼容JSONArray格式
*/ */
fun sendMessageStream(messages: org.json.JSONArray, callback: StreamCallback) { fun sendMessageStream(messages: org.json.JSONArray, callback: StreamCallback) {
// 设置回调 // 1. 立即中止旧会话
// setStreamCallback(callback) abortCurrentSession()
// 转换JSONArray为List<Map<String, Any>> // 2. 创建新会话
val messageList = convertJsonArrayToList(messages)
currSessionId = UUID.randomUUID().toString() currSessionId = UUID.randomUUID().toString()
sessions[currSessionId] = callback currSessionCallback = callback
// 调用原始方法
sendMessageStream(currSessionId,messageList) // 3. 转换消息并开始处理
val messageList = convertJsonArrayToList(messages)
sendMessageStream(currSessionId, messageList)
} }
/** /**
* 发送消息(流式输出) * 发送消息(流式输出)
*/ */
fun sendMessageStream(sessionid:String,messages: List<Map<String, Any>>) { fun sendMessageStream(sessionid:String,messages: List<Map<String, Any>>) {
// 验证是否是当前会话
if (sessionid != currSessionId) {
return // 忽略非当前会话的请求
}
if (!isInitialized || apiKey.isEmpty() || openAI == null) { if (!isInitialized || apiKey.isEmpty() || openAI == null) {
Log.e("ChatApiService", "ChatAPI服务未初始化,无法发送消息") Log.e("ChatApiService", "ChatAPI服务未初始化,无法发送消息")
sessions[sessionid]?.onError(ChatApiException("ChatAPI服务未初始化")) currSessionCallback?.onError(ChatApiException("ChatAPI服务未初始化"))
return return
} }
// 重置状态 // 重置状态
// isCanceled = false
currentMessages = messages currentMessages = messages
toolCalls.clear() toolCalls.clear()
// 检查是否包含图片,决定使用哪个模型 // 检查是否包含图片,决定使用哪个模型
@ -337,79 +387,93 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
} }
chatsFlow.collect { result -> chatsFlow.collect { result ->
if (currSessionId != sessionid) { // 每次处理前检查是否仍是当前会话
return@collect if (sessionid != currSessionId) {
throw CancellationException("Session cancelled")
} }
val choice = result.choices.firstOrNull() val choice = result.choices.firstOrNull()
if (choice == null) { if (choice == null) {
Log.e("ChatApiService", "choice为null") Log.w("ChatApiService", "[Session: $sessionid] choice为null")
return@collect return@collect
} }
val delta = choice.delta val delta = choice.delta
if (delta == null) { if (delta == null) {
Log.e("ChatApiService", "delta为null") Log.w("ChatApiService", "[Session: $sessionid] delta为null")
return@collect return@collect
} }
// 处理普通文本内容 // 处理普通文本内容
delta.content?.let { content -> delta.content?.let { content ->
sessions[sessionid]?.onToken(content) currSessionCallback?.onToken(content)
} }
// 收集工具调用信息 // 收集工具调用信息
delta.toolCalls?.forEach { toolCall -> delta.toolCalls?.forEach { toolCall ->
val index = toolCall.index
// 创建或获取现有的工具调用信息
val toolCallInfo = toolCalls.getOrPut(index) { ToolCallInfo() }
// 安全处理工具调用ID
try { try {
toolCall.id?.let { i -> val index = toolCall.index
toolCallInfo.id = i.toString() val toolCallInfo = toolCalls.getOrPut(index) { ToolCallInfo() }
// 使用安全调用链处理工具调用信息
toolCall.id?.toString()?.takeIf { it.isNotEmpty() }?.let {
toolCallInfo.id = it
} }
} catch (e: Exception) {
Log.e("ChatApiService", "处理工具调用ID异常: ${e.message}")
}
// 安全处理函数信息
try {
toolCall.function?.let { function -> toolCall.function?.let { function ->
// 安全处理函数名称 - 使用 try-catch 避免 null 属性访问
try { try {
function.name.let { name -> val functionName = function.name
toolCallInfo.name = name if (!functionName.isNullOrEmpty()) {
toolCallInfo.name = functionName
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ChatApiService", "处理工具调用函数名称异常! function:${toolCall.function} err:${e.message}") // 在流式响应中,name 可能在后续的 delta 中为 null,这是正常的
// 不记录日志,避免干扰
} }
// 安全处理函数参数 - 使用 try-catch 避免 null 属性访问
try { try {
function.arguments.let { args -> val functionArgs = function.arguments
toolCallInfo.arguments += args if (!functionArgs.isNullOrEmpty()) {
toolCallInfo.arguments += functionArgs
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ChatApiService", "处理工具调用参数异常: ${e.message}") // 在流式响应中,arguments 可能在后续的 delta 中为 null,这是正常的
// 不记录日志,避免干扰
} }
Log.e("ChatApiService", "AI发起工具调用:${toolCallInfo.name} ${toolCallInfo.arguments}")
// 不设置默认值,让参数自然累积
}
// 只在有完整有效数据时记录日志(避免流式响应过程中的重复日志)
if (toolCallInfo.isValid()) {
Log.d("ChatApiService", "[Session: $sessionid] AI发起工具调用: ${toolCallInfo.name}, 参数: ${toolCallInfo.arguments}")
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ChatApiService", "处理工具调用函数信息异常: ${e.message}") Log.w("ChatApiService", "[Session: $sessionid] 处理工具调用信息异常: ${e.message}", e)
} }
} }
} }
if (currSessionId == sessionid) { // 完成处理
if (sessionid == currSessionId) {
// 检查是否有工具调用需要处理 // 检查是否有工具调用需要处理
val hasToolCalls = processToolCalls(sessionid) val hasToolCalls = processToolCalls(sessionid)
if (!hasToolCalls) { if (!hasToolCalls) {
sessions[sessionid]?.onComplete() currSessionCallback?.onComplete()
clearCurrentSession()
} }
}else{
sessions[sessionid]?.onComplete()
} }
} catch (e: CancellationException) {
// 会话被取消,不需要处理
Log.d("ChatApiService", "Session $sessionid cancelled")
} catch (e: Exception) { } catch (e: Exception) {
// if (currSessionId == sessionid) { if (sessionid == currSessionId) {
sessions[sessionid]?.onError(ChatApiException("流式请求失败: ${e.message}")) currSessionCallback?.onError(ChatApiException("流式请求失败: ${e.message}"))
// } clearCurrentSession()
}
} }
} }
} }
@ -418,42 +482,54 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
* 处理工具调用 * 处理工具调用
*/ */
private suspend fun processToolCalls(sessionid: String): Boolean { private suspend fun processToolCalls(sessionid: String): Boolean {
val firstToolCall = try { // 验证工具调用集合不为空
toolCalls.values.firstOrNull { it.isValid() } if (toolCalls.isEmpty()) {
} catch (e: Exception) { Log.d("ChatApiService", "[Session: $sessionid] 没有工具调用需要处理")
Log.e("ChatApiService", "查找有效工具调用异常: ${e.message}")
null
}
if (firstToolCall == null) {
return false return false
} }
// 创建函数调用字典
val functionCall = try { // 输出所有收集到的工具调用信息(用于调试)
mapOf( Log.d("ChatApiService", "[Session: $sessionid] 收集到 ${toolCalls.size} 个工具调用")
"name" to (firstToolCall.name.takeIf { it.isNotEmpty() } ?: ""), toolCalls.forEach { (index, toolCall) ->
"arguments" to (firstToolCall.arguments.takeIf { it.isNotEmpty() } ?: "{}"), Log.d("ChatApiService", "[Session: $sessionid] 工具调用[$index]: name=${toolCall.name}, args.length=${toolCall.arguments.length}, args=${toolCall.arguments.take(100)}...")
"id" to (firstToolCall.id.takeIf { it.isNotEmpty() } ?: "") }
)
} catch (e: Exception) { // 只处理有效的工具调用
Log.e("ChatApiService", "创建函数调用字典异常: ${e.message}") val validToolCalls = toolCalls.values.filter { it.isValid() }
if (validToolCalls.isEmpty()) {
Log.w("ChatApiService", "[Session: $sessionid] 没有有效的工具调用(参数可能不完整)")
return false return false
} }
// 处理第一个有效的工具调用
val firstToolCall = validToolCalls.first()
Log.d("ChatApiService", "[Session: $sessionid] 处理工具调用: ${firstToolCall.name},参数: ${firstToolCall.arguments}")
// 创建函数调用字典
val functionCall = mapOf(
"name" to firstToolCall.name,
"arguments" to firstToolCall.arguments,
"id" to firstToolCall.id
)
// 通知上层工具调用事件 // 通知上层工具调用事件
sessions[sessionid]?.onFunctionCall(convertMapToJsonObject(functionCall)) currSessionCallback?.onFunctionCall(convertMapToJsonObject(functionCall))
// 在后台队列处理工具调用 // 在后台队列处理工具调用
launch { launch {
try { try {
if (currSessionId == sessionid) { if (sessionid == currSessionId) {
// 通过MCP客户端处理工具调用 // 通过MCP客户端处理工具调用
val functionName = firstToolCall.name val functionName = firstToolCall.name
val argumentsJson = firstToolCall.arguments val argumentsJson = firstToolCall.arguments
val result = if (_mcpClient?.hasToolWithName(functionName) == true) { val result = if (_mcpClient?.hasToolWithName(functionName) == true) {
// 解析参数 // 解析参数,如果参数为空JSON对象,使用空Map
val arguments = _mcpClient?.parseJsonArguments(argumentsJson) ?: emptyMap() val arguments = if (argumentsJson == "{}") {
emptyMap()
} else {
_mcpClient?.parseJsonArguments(argumentsJson) ?: emptyMap()
}
// 调用MCP工具 // 调用MCP工具
val toolResult = _mcpClient?.callTool(functionName, arguments) val toolResult = _mcpClient?.callTool(functionName, arguments)
Log.e("ChatApiService", "AI调用工具结果: $functionName}:$arguments} toolResult:${toolResult}") Log.d("ChatApiService", "[Session: $sessionid] AI调用工具结果: $functionName, 参数: $arguments, 结果: $toolResult")
// 处理结果 // 处理结果
if (toolResult != null) { if (toolResult != null) {
if (toolResult["isError"] == true) { if (toolResult["isError"] == true) {
@ -473,16 +549,16 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
mapOf("context" to text) mapOf("context" to text)
} }
} else { } else {
Log.e("ChatApiService", "MCP工具调用返回null") Log.w("ChatApiService", "[Session: $sessionid] MCP工具调用返回null")
mapOf("context" to "Tool call failed") mapOf("context" to "Tool call failed")
} }
} else { } else {
// 工具不存在 // 工具不存在
mapOf("context" to "Tool not found: $functionName") mapOf("context" to "Tool not found: $functionName")
} }
if (currSessionId == sessionid) { if (sessionid == currSessionId) {
// 处理结果 // 处理结果
sessions[sessionid]?.onFunctionCallResult( currSessionCallback?.onFunctionCallResult(
convertMapToJsonObject(functionCall), convertMapToJsonObject(functionCall),
convertMapToJsonObject(result) convertMapToJsonObject(result)
) )
@ -493,13 +569,11 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
functionCall = functionCall, functionCall = functionCall,
functionResult = gson.toJson(result) functionResult = gson.toJson(result)
) )
}else{
sessions[sessionid]?.onComplete()
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ChatApiService", "工具调用处理过程中出错: ${e.message}", e) Log.e("ChatApiService", "工具调用处理过程中出错: ${e.message}", e)
if (currSessionId == sessionid) { if (sessionid == currSessionId) {
val errorMessage = "工具调用处理失败: ${e.message}" val errorMessage = "工具调用处理失败: ${e.message}"
sendFunctionCallResultInternal( sendFunctionCallResultInternal(
sessionid= sessionid, sessionid= sessionid,
@ -507,8 +581,6 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
functionCall = functionCall, functionCall = functionCall,
functionResult = errorMessage functionResult = errorMessage
) )
}else{
sessions[sessionid]?.onComplete()
} }
} }
} }
@ -525,7 +597,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
functionCall: Map<String, Any>, functionCall: Map<String, Any>,
functionResult: String functionResult: String
) { ) {
if (currSessionId != sessionid) return if (sessionid != currSessionId) return
val fullMessages = messages.toMutableList() val fullMessages = messages.toMutableList()
@ -565,19 +637,19 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
* 取消当前流式请求 * 取消当前流式请求
*/ */
fun cancelCurrentStream(): Boolean { fun cancelCurrentStream(): Boolean {
sessions[currSessionId]?.onComplete() if (currSessionId.isNotEmpty()) {
sessions.remove(currSessionId) abortCurrentSession()
currSessionId = "" return true
currentStreamJob?.cancel() }
currentStreamJob = null return false
return true
} }
/** /**
* 取消所有操作并释放资源 (与 iOS 版本保持一致) * 取消所有操作并释放资源 (与 iOS 版本保持一致)
*/ */
fun cancelAll() { fun cancelAll() {
cancelCurrentStream() abortCurrentSession()
} }
/** /**
@ -706,7 +778,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
description = description, description = description,
parameters = parameters parameters = parameters
) )
Log.e("ChatApiService", "AI携带工具:${name}:${parameters}") Log.d("ChatApiService", "AI携带工具: $name, 参数定义: $parameters")
tools.add(tool) tools.add(tool)
} catch (e: Exception) { } catch (e: Exception) {
@ -796,9 +868,11 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
* 聊天完成流式接口 * 聊天完成流式接口
* 与iOS版本保持一致的接口 * 与iOS版本保持一致的接口
*/ */
fun chatCompletionStream(sessionid: String,messages: List<Map<String, Any>>, tool: Boolean = false) { fun chatCompletionStream(sessionid: String, messages: List<Map<String, Any>>, tool: Boolean = false) {
// 直接调用sendMessageStream,因为该方法已经处理了工具调用 // 验证是否是当前会话
sendMessageStream(sessionid,messages) if (sessionid == currSessionId) {
sendMessageStream(sessionid, messages)
}
} }
// MARK: - 工具方法 // MARK: - 工具方法

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

@ -236,8 +236,7 @@ class CustomSseClientTransport(
if (!response.status.isSuccess()) { if (!response.status.isSuccess()) {
val text = response.bodyAsText() val text = response.bodyAsText()
Log.e(TAG, "发送消息失败:URL:${urlString} HTTP ${response.status}, $text") // Log.e(TAG, "发送消息失败:URL:${urlString} HTTP ${response.status}, $text")
error("Error POSTing to endpoint (HTTP ${response.status}): $text")
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "发送消息异常: ${e.message}", e) Log.e(TAG, "发送消息异常: ${e.message}", e)

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

@ -237,7 +237,10 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
localFunctions[name]?.let { handler -> localFunctions[name]?.let { handler ->
return try { return try {
val result = handler.handle(arguments) val result = handler.handle(arguments)
mapOf("context" to result) mapOf("context" to mapOf(
"type" to "text",
"text" to result,
))
} catch (e: Exception) { } catch (e: Exception) {
mapOf( mapOf(
"content" to listOf(mapOf( "content" to listOf(mapOf(

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

@ -357,9 +357,9 @@ class MCPSubClient(
val response = mcpClient?.ping() val response = mcpClient?.ping()
val latency = System.currentTimeMillis() - startTime val latency = System.currentTimeMillis() - startTime
Log.d(TAG, "心跳检测成功,延迟: ${latency}ms") // Log.d(TAG, "心跳检测成功,延迟: ${latency}ms")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "心跳检测失败: ${e.message}") // Log.e(TAG, "心跳检测失败: ${e.message}")
handleConnectionError(e) handleConnectionError(e)
break break
} }
@ -377,7 +377,7 @@ class MCPSubClient(
retryCount++ retryCount++
currentReconnectDelay = minOf(currentReconnectDelay * 2, maxReconnectDelay) currentReconnectDelay = minOf(currentReconnectDelay * 2, maxReconnectDelay)
Log.w(TAG, "连接失败,将在 ${currentReconnectDelay}ms 后尝试重连 (尝试 $retryCount/$maxRetryAttempts)") // Log.w(TAG, "连接失败,将在 ${currentReconnectDelay}ms 后尝试重连 (尝试 $retryCount/$maxRetryAttempts)")
delay(currentReconnectDelay) delay(currentReconnectDelay)
connect() connect()

748
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt

@ -1,5 +1,9 @@
package com.yunqiinnovation.chat_api package com.yunqiinnovation.chat_api
import android.Manifest
import android.app.ActivityManager
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
@ -7,10 +11,13 @@ import android.location.LocationManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.CalendarContract import android.provider.CalendarContract
import android.provider.ContactsContract
import android.provider.Settings
import android.telephony.SmsManager import android.telephony.SmsManager
import android.util.Log import android.util.Log
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED
import org.json.JSONObject
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -19,6 +26,9 @@ import java.util.*
* 提供内置的系统函数,与 iOS 版本保持一致 * 提供内置的系统函数,与 iOS 版本保持一致
*/ */
class SystemFunctionHandler(private val context: Context? = null) { class SystemFunctionHandler(private val context: Context? = null) {
companion object {
const val TAG = "SystemFunctionHandler"
}
/** /**
* 注册所有系统函数 * 注册所有系统函数
@ -222,6 +232,171 @@ class SystemFunctionHandler(private val context: Context? = null) {
handler = ExitNavigationHandler(context) handler = ExitNavigationHandler(context)
) )
} }
/**
* 检查权限并根据应用状态采取不同行为
* @param permission 需要检查的权限
* @param permissionName 权限名称(用于显示给用户)
* @param functionName 功能名称(用于日志和提示)
* @return 是否拥有权限
*/
internal fun checkPermissionAndGuide(permission: String, permissionName: String, functionName: String): Boolean {
val ctx = context ?: return false
if (ContextCompat.checkSelfPermission(ctx, permission) != PackageManager.PERMISSION_GRANTED) {
Log.w(TAG, "缺少${permissionName}权限,无法执行${functionName}")
// 检查应用是否在前台
if (isAppInForeground()) {
// 应用在前台,尝试发送广播通知主应用弹出权限申请对话框
try {
val intent = Intent("com.yunqiinnovation.deepsound.action.REQUEST_PERMISSION").apply {
setPackage(ctx.packageName)
putExtra("permission", permission)
putExtra("permissionName", permissionName)
putExtra("functionName", functionName)
addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
}
ctx.sendBroadcast(intent)
// 设置一个短暂延迟,让用户有时间响应权限请求
Thread.sleep(500)
// 再次检查权限是否已授予
if (ContextCompat.checkSelfPermission(ctx, permission) == PackageManager.PERMISSION_GRANTED) {
return true
}
} catch (e: Exception) {
Log.e(TAG, "请求权限失败: ${e.message}", e)
}
// 如果权限仍未授予,引导用户到权限管理页面
try {
val settingsIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", ctx.packageName, null)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
ctx.startActivity(settingsIntent)
} catch (e: Exception) {
Log.e(TAG, "打开权限设置页面失败: ${e.message}", e)
}
}
return false
}
return true
}
/**
* 检查应用是否在前台运行
* @return 应用是否在前台
*/
internal fun isAppInForeground(): Boolean {
val ctx = context ?: return false
try {
val activityManager = ctx.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val appProcesses = activityManager.runningAppProcesses ?: return false
val packageName = ctx.packageName
for (appProcess in appProcesses) {
if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
&& appProcess.processName == packageName) {
return true
}
}
} catch (e: Exception) {
Log.e(TAG, "检查应用状态失败: ${e.message}", e)
}
return false
}
/**
* 根据联系人名称获取电话号码
* @param contact 联系人名称或电话号码
* @return 电话号码
*/
internal fun getPhoneNumberForContact(contact: String): String {
val ctx = context ?: return ""
// 如果输入的是电话号码(简单判断,包含数字和特殊符号)
if (contact.matches(Regex("^[+\\d\\s\\-()]*\$")) && contact.any { it.isDigit() }) {
// 移除非数字字符
return contact.replace(Regex("[^+\\d]"), "")
}
// 检查读取联系人权限
if (!checkPermissionAndGuide(Manifest.permission.READ_CONTACTS, "读取联系人", "访问联系人")) {
return ""
}
// 从联系人数据库查询
try {
val contentResolver = ctx.contentResolver
val uri = Uri.withAppendedPath(
ContactsContract.Contacts.CONTENT_FILTER_URI,
Uri.encode(contact)
)
val projection = arrayOf(
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.HAS_PHONE_NUMBER
)
contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val hasPhoneNumber = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.HAS_PHONE_NUMBER))
if (hasPhoneNumber > 0) {
val contactId = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID))
val phoneUri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI
val phoneProjection = arrayOf(ContactsContract.CommonDataKinds.Phone.NUMBER)
val phoneSelection = "${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?"
val phoneSelectionArgs = arrayOf(contactId)
contentResolver.query(phoneUri, phoneProjection, phoneSelection, phoneSelectionArgs, null)?.use { phoneCursor ->
if (phoneCursor.moveToFirst()) {
return phoneCursor.getString(phoneCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER))
}
}
}
}
}
} catch (e: Exception) {
Log.e(TAG, "查询联系人信息失败: ${e.message}", e)
}
return ""
}
/**
* 获取默认日历ID
*/
internal fun getDefaultCalendarId(): Long {
val ctx = context ?: return -1L
val projection = arrayOf(CalendarContract.Calendars._ID)
var calendarId = -1L
try {
val uri = CalendarContract.Calendars.CONTENT_URI
val selection = "(${CalendarContract.Calendars.VISIBLE} = 1) AND (${CalendarContract.Calendars.IS_PRIMARY} = 1)"
ctx.contentResolver.query(uri, projection, selection, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
calendarId = cursor.getLong(0)
} else {
// 如果没有找到主日历,尝试找任何可见的日历
ctx.contentResolver.query(uri, projection, "${CalendarContract.Calendars.VISIBLE} = 1", null, null)?.use { c ->
if (c.moveToFirst()) {
calendarId = c.getLong(0)
}
}
}
}
} catch (e: Exception) {
Log.e(TAG, "获取默认日历ID失败: ${e.message}", e)
}
return calendarId
}
} }
/** /**
@ -229,12 +404,25 @@ class SystemFunctionHandler(private val context: Context? = null) {
*/ */
private class ExitNavigationHandler(private val context: Context?) : FunctionHandler { private class ExitNavigationHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String { override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
// 发送广播通知退出交互 // 发送广播通知退出交互
val intent = Intent("com.yunqiinnovation.amap_service.EXIT_NAVIGATION").apply { val intent = Intent("com.yunqiinnovation.amap_service.EXIT_NAVIGATION").apply {
setPackage(context?.packageName) // 设置包名使其成为显式 Intent setPackage(context.packageName) // 设置包名使其成为显式 Intent
} }
context?.sendBroadcast(intent) context.sendBroadcast(intent)
return "{\"result\": \"已结束当前导航服务\"}"
return JSONObject().apply {
put("result", "已结束当前导航服务")
put("broadcast", false)
}.toString()
} }
} }
@ -243,9 +431,21 @@ private class ExitNavigationHandler(private val context: Context?) : FunctionHan
*/ */
private class ExitInteractionHandler(private val context: Context?) : FunctionHandler { private class ExitInteractionHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String { override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
// 发送广播通知退出交互 // 发送广播通知退出交互
context?.sendBroadcast(Intent("com.yunqiinnovation.deepsound.EXIT_INTERACTION")) context.sendBroadcast(Intent("com.yunqiinnovation.deepsound.EXIT_INTERACTION"))
return "{\"result\": \"已结束当前交互\"}"
return JSONObject().apply {
put("result", "已结束当前交互")
}.toString()
} }
} }
@ -254,9 +454,21 @@ private class ExitInteractionHandler(private val context: Context?) : FunctionHa
*/ */
private class TranslationModeHandler(private val context: Context?) : FunctionHandler { private class TranslationModeHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String { override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
// 发送广播通知进入翻译模式 // 发送广播通知进入翻译模式
context?.sendBroadcast(Intent("com.yunqiinnovation.deepsound.ENTER_TRANSLATION_MODE")) context.sendBroadcast(Intent("com.yunqiinnovation.deepsound.ENTER_TRANSLATION_MODE"))
return "{\"result\": \"已进入翻译模式\"}"
return JSONObject().apply {
put("result", "已进入翻译模式")
}.toString()
} }
} }
@ -269,24 +481,75 @@ private class SendTextMessageHandler(private val context: Context?) : FunctionHa
val message = arguments["message"] as? String val message = arguments["message"] as? String
if (contact == null || message == null) { if (contact == null || message == null) {
return "{\"result\": \"缺少必要参数\"}" return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "invalid_arguments")
put("message", "缺少必要参数:contact 或 message")
})
}.toString()
} }
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
val handler = context.let { SystemFunctionHandler(it) }
return try { return try {
val smsIntent = Intent(Intent.ACTION_SENDTO).apply { // 检查发送短信权限
data = Uri.parse("smsto:$contact") if (!handler.checkPermissionAndGuide(Manifest.permission.SEND_SMS, "发送短信", "发送短信")) {
putExtra("sms_body", message) return JSONObject().apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK put("error", JSONObject().apply {
put("code", "permission_denied")
put("message", "需要短信权限才能发送短信,请在权限管理中开启短信权限")
})
}.toString()
} }
if (context?.packageManager?.queryIntentActivities(smsIntent, 0)?.isNotEmpty() == true) { val phoneNumber = handler.getPhoneNumberForContact(contact)
context.startActivity(smsIntent) if (phoneNumber.isEmpty()) {
"{\"result\": \"已打开短信应用\"}" return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "contact_not_found")
put("message", "未找到联系人信息: $contact")
})
}.toString()
}
// 使用SmsManager发送短信
val smsManager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
context.getSystemService(SmsManager::class.java)
} else {
@Suppress("DEPRECATION")
SmsManager.getDefault()
}
// 处理长短信
val parts = smsManager.divideMessage(message)
if (parts.size == 1) {
smsManager.sendTextMessage(phoneNumber, null, message, null, null)
} else { } else {
"{\"result\": \"无法打开短信应用\"}" smsManager.sendMultipartTextMessage(phoneNumber, null, parts, null, null)
} }
JSONObject().apply {
put("result", "已成功向${contact}发送短信")
put("phone", phoneNumber)
put("message", message)
}.toString()
} catch (e: Exception) { } catch (e: Exception) {
"{\"result\": \"发送短信失败:${e.message}\"}" Log.e(SystemFunctionHandler.TAG, "发送短信失败: ${e.message}", e)
JSONObject().apply {
put("error", JSONObject().apply {
put("code", "send_failed")
put("message", "发送短信失败: ${e.message}")
})
}.toString()
} }
} }
} }
@ -299,23 +562,73 @@ private class MakePhoneCallHandler(private val context: Context?) : FunctionHand
val contact = arguments["contact"] as? String val contact = arguments["contact"] as? String
if (contact == null) { if (contact == null) {
return "{\"result\": \"缺少联系人参数\"}" return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "invalid_arguments")
put("message", "缺少必要参数:contact")
})
}.toString()
}
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
} }
val handler = context.let { SystemFunctionHandler(it) }
return try { return try {
val callIntent = Intent(Intent.ACTION_DIAL).apply { // 检查拨打电话权限
data = Uri.parse("tel:$contact") if (!handler.checkPermissionAndGuide(Manifest.permission.CALL_PHONE, "拨打电话", "拨打电话")) {
flags = Intent.FLAG_ACTIVITY_NEW_TASK return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "permission_denied")
put("message", "需要电话权限才能拨打电话,请在权限管理中开启电话权限")
})
}.toString()
} }
if (context?.packageManager?.queryIntentActivities(callIntent, 0)?.isNotEmpty() == true) { val phoneNumber = handler.getPhoneNumberForContact(contact)
context.startActivity(callIntent) if (phoneNumber.isEmpty()) {
"{\"result\": \"已发起电话呼叫\"}" return JSONObject().apply {
} else { put("error", JSONObject().apply {
"{\"result\": \"无法拨打电话\"}" put("code", "contact_not_found")
put("message", "未找到联系人信息: $contact")
})
}.toString()
} }
// 创建拨号意图(使用ACTION_CALL直接拨打)
val intent = Intent(Intent.ACTION_CALL).apply {
data = Uri.parse("tel:$phoneNumber")
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
context.startActivity(intent)
JSONObject().apply {
put("result", "正在拨打${contact}的电话")
put("phone", phoneNumber)
}.toString()
} catch (e: SecurityException) {
Log.e(SystemFunctionHandler.TAG, "拨打电话权限被拒绝: ${e.message}", e)
JSONObject().apply {
put("error", JSONObject().apply {
put("code", "permission_denied")
put("message", "权限被拒绝,无法拨打电话")
})
}.toString()
} catch (e: Exception) { } catch (e: Exception) {
"{\"result\": \"拨打电话失败:${e.message}\"}" Log.e(SystemFunctionHandler.TAG, "拨打电话失败: ${e.message}", e)
JSONObject().apply {
put("error", JSONObject().apply {
put("code", "call_failed")
put("message", "拨打电话失败: ${e.message}")
})
}.toString()
} }
} }
} }
@ -330,37 +643,148 @@ private class SetReminderHandler(private val context: Context?) : FunctionHandle
val timeString = arguments["time"] as? String val timeString = arguments["time"] as? String
if (title == null || timeString == null) { if (title == null || timeString == null) {
return "{\"result\": \"缺少必要参数\"}" return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "invalid_arguments")
put("message", "缺少必要参数:title 或 time")
})
}.toString()
}
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
} }
val handler = context.let { SystemFunctionHandler(it) }
return try { return try {
// 解析时间 // 解析时间
val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()) val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault())
val date = formatter.parse(timeString) val time = formatter.parse(timeString)?.time ?: run {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "invalid_time_format")
put("message", "提醒时间格式无效,请使用'yyyy-MM-dd HH:mm'格式")
})
}.toString()
}
// 检查日历权限
if (!handler.checkPermissionAndGuide(Manifest.permission.WRITE_CALENDAR, "日历写入", "设置提醒")) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "permission_denied")
put("message", "需要日历权限才能创建提醒,请在权限管理中开启日历权限")
})
}.toString()
}
// 直接使用ContentProvider写入日历
val endTime = time + 3600000 // 默认1小时后结束
// 获取默认日历ID
val calendarId = handler.getDefaultCalendarId()
if (calendarId == -1L) {
Log.w(SystemFunctionHandler.TAG, "未找到默认日历")
// 如果没有找到日历,尝试使用Intent创建
return tryCreateWithIntent(context, title, content, time)
}
if (date == null) { // 创建事件
return "{\"result\": \"时间格式错误\"}" val eventValues = ContentValues().apply {
put(CalendarContract.Events.CALENDAR_ID, calendarId)
put(CalendarContract.Events.TITLE, title)
put(CalendarContract.Events.DESCRIPTION, content)
put(CalendarContract.Events.DTSTART, time)
put(CalendarContract.Events.DTEND, endTime)
put(CalendarContract.Events.HAS_ALARM, 1)
put(CalendarContract.Events.EVENT_TIMEZONE, TimeZone.getDefault().id)
} }
// 创建日历事件 val eventUri = context.contentResolver.insert(CalendarContract.Events.CONTENT_URI, eventValues)
if (eventUri == null) {
Log.e(SystemFunctionHandler.TAG, "插入日历事件失败")
return tryCreateWithIntent(context, title, content, time)
}
// 获取事件ID
val eventId = ContentUris.parseId(eventUri)
// 添加提醒(15分钟前提醒)
val reminderValues = ContentValues().apply {
put(CalendarContract.Reminders.EVENT_ID, eventId)
put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT)
put(CalendarContract.Reminders.MINUTES, 15) // 提前15分钟提醒
}
val reminderUri = context.contentResolver.insert(CalendarContract.Reminders.CONTENT_URI, reminderValues)
if (reminderUri != null) {
return JSONObject().apply {
put("result", "已成功创建提醒: $title")
put("title", title)
put("content", content)
put("time", timeString)
put("eventId", eventId)
}.toString()
} else {
// 仍然创建了事件,但没有提醒
return JSONObject().apply {
put("result", "已创建日历事件,但未设置提醒: $title")
put("title", title)
put("time", timeString)
put("eventId", eventId)
}.toString()
}
} catch (e: Exception) {
Log.e(SystemFunctionHandler.TAG, "设置提醒失败: ${e.message}", e)
JSONObject().apply {
put("error", JSONObject().apply {
put("code", "reminder_failed")
put("message", "设置提醒失败: ${e.message}")
})
}.toString()
}
}
private fun tryCreateWithIntent(context: Context, title: String, content: String, time: Long): String {
return try {
val calendarIntent = Intent(Intent.ACTION_INSERT).apply { val calendarIntent = Intent(Intent.ACTION_INSERT).apply {
data = CalendarContract.Events.CONTENT_URI data = CalendarContract.Events.CONTENT_URI
putExtra(CalendarContract.Events.TITLE, title) putExtra(CalendarContract.Events.TITLE, title)
putExtra(CalendarContract.Events.DESCRIPTION, content) putExtra(CalendarContract.Events.DESCRIPTION, content)
putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, date.time) putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, time)
putExtra(CalendarContract.EXTRA_EVENT_END_TIME, date.time + 60 * 60 * 1000) // 默认1小时 putExtra(CalendarContract.EXTRA_EVENT_END_TIME, time + 3600000)
putExtra(CalendarContract.Events.HAS_ALARM, 1) putExtra(CalendarContract.Events.HAS_ALARM, 1)
flags = Intent.FLAG_ACTIVITY_NEW_TASK flags = Intent.FLAG_ACTIVITY_NEW_TASK
} }
if (context?.packageManager?.queryIntentActivities(calendarIntent, 0)?.isNotEmpty() == true) { if (context.packageManager?.queryIntentActivities(calendarIntent, 0)?.isNotEmpty() == true) {
context.startActivity(calendarIntent) context.startActivity(calendarIntent)
"{\"result\": \"提醒设置成功\"}" JSONObject().apply {
put("result", "已打开日历应用创建提醒")
put("title", title)
put("content", content)
}.toString()
} else { } else {
"{\"result\": \"无法打开日历应用\"}" JSONObject().apply {
put("error", JSONObject().apply {
put("code", "no_calendar_app")
put("message", "未找到可用的日历应用")
})
}.toString()
} }
} catch (e: Exception) { } catch (e: Exception) {
"{\"result\": \"设置提醒失败:${e.message}\"}" JSONObject().apply {
put("error", JSONObject().apply {
put("code", "intent_failed")
put("message", "创建日历事件失败: ${e.message}")
})
}.toString()
} }
} }
} }
@ -373,16 +797,61 @@ private class GetCurrentTimeHandler : FunctionHandler {
val format = arguments["format"] as? String val format = arguments["format"] as? String
return try { return try {
val formatter = if (!format.isNullOrEmpty()) { val calendar = Calendar.getInstance()
SimpleDateFormat(format, Locale.getDefault()) val date = calendar.time
} else {
SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss", Locale.CHINA) val timeStr = when (format?.lowercase(Locale.getDefault())) {
"short" -> {
SimpleDateFormat("HH:mm", Locale.getDefault()).format(date)
}
"date" -> {
SimpleDateFormat("yyyy年MM月dd日", Locale.getDefault()).format(date)
}
"time" -> {
SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(date)
}
"full" -> {
SimpleDateFormat("yyyy年MM月dd日 EEEE HH:mm:ss", Locale.getDefault()).format(date)
}
"iso" -> {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()).format(date)
}
else -> {
SimpleDateFormat("yyyy年MM月dd日 HH:mm", Locale.getDefault()).format(date)
}
}
// 获取星期几
val weekDay = when (calendar.get(Calendar.DAY_OF_WEEK)) {
Calendar.SUNDAY -> "星期日"
Calendar.MONDAY -> "星期一"
Calendar.TUESDAY -> "星期二"
Calendar.WEDNESDAY -> "星期三"
Calendar.THURSDAY -> "星期四"
Calendar.FRIDAY -> "星期五"
Calendar.SATURDAY -> "星期六"
else -> ""
} }
val currentTime = formatter.format(Date()) // 根据格式决定是否添加星期几
"{\"result\": \"$currentTime\", \"time\": \"$currentTime\"}" val resultTime = when {
format == "short" || format == "time" -> timeStr
format == "full" -> timeStr // full格式已经包含了星期
else -> "$timeStr $weekDay"
}
JSONObject().apply {
put("result", resultTime)
put("time", resultTime)
put("timestamp", date.time)
}.toString()
} catch (e: Exception) { } catch (e: Exception) {
"{\"result\": \"获取时间失败:${e.message}\"}" JSONObject().apply {
put("error", JSONObject().apply {
put("code", "time_error")
put("message", "获取时间失败: ${e.message}")
})
}.toString()
} }
} }
} }
@ -393,21 +862,24 @@ private class GetCurrentTimeHandler : FunctionHandler {
private class GetCurrentLocationHandler(private val context: Context?) : FunctionHandler { private class GetCurrentLocationHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String { override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) { if (context == null) {
return "{\"result\": \"上下文未初始化\", \"success\": false}" return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
} }
// 检查位置权限 val handler = context.let { SystemFunctionHandler(it) }
val hasPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ContextCompat.checkSelfPermission(
context,
android.Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
} else {
true
}
if (!hasPermission) { // 检查位置权限
return "{\"result\": \"位置权限被拒绝。请前往设置中启用位置权限\", \"success\": false}" if (!handler.checkPermissionAndGuide(Manifest.permission.ACCESS_FINE_LOCATION, "位置信息", "获取位置")) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "permission_denied")
put("message", "需要位置权限才能获取当前位置,请在权限管理中开启位置权限")
})
}.toString()
} }
// 检查位置服务是否启用 // 检查位置服务是否启用
@ -416,12 +888,29 @@ private class GetCurrentLocationHandler(private val context: Context?) : Functio
locationManager?.isProviderEnabled(LocationManager.NETWORK_PROVIDER) == true locationManager?.isProviderEnabled(LocationManager.NETWORK_PROVIDER) == true
if (!isLocationEnabled) { if (!isLocationEnabled) {
return "{\"result\": \"位置服务未启用。请前往设置中启用位置服务\", \"success\": false}" // 尝试引导用户打开位置服务
try {
val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
} catch (e: Exception) {
Log.e(SystemFunctionHandler.TAG, "打开位置设置失败: ${e.message}", e)
}
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "location_disabled")
put("message", "位置服务未启用,请打开GPS或网络定位")
})
}.toString()
} }
// 注意:实际的位置获取需要异步处理,这里只返回提示信息 // 注意:这里简化了实现,实际应该获取真实位置
// 在实际应用中应该使用 LocationCallback 或 Coroutines 来获取实时位置 // 在生产环境中应该使用 LocationManager 或 FusedLocationProviderClient 获取实际位置
return "{\"result\": \"需要通过位置服务获取当前位置\", \"success\": true}" return JSONObject().apply {
put("result", "位置服务已启用,可以获取位置信息")
put("note", "需要实现实际的位置获取逻辑")
}.toString()
} }
} }
@ -430,12 +919,26 @@ private class GetCurrentLocationHandler(private val context: Context?) : Functio
*/ */
private class MediaPlayHandler(private val context: Context?) : FunctionHandler { private class MediaPlayHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String { override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
// 发送媒体播放广播 // 发送媒体播放广播
val intent = Intent("com.yunqiinnovation.music_service.MEDIA_PLAY").apply { val intent = Intent("com.yunqiinnovation.music_service.MEDIA_PLAY").apply {
setPackage(context?.packageName) // 设置包名使其成为显式 Intent setPackage(context.packageName)
} }
context?.sendBroadcast(intent) context.sendBroadcast(intent)
return "{\"result\": \"已开始播放媒体\"}"
return JSONObject().apply {
put("result", "已开始播放媒体")
put("notreply", true)
put("broadcast", false)
}.toString()
} }
} }
@ -444,27 +947,55 @@ private class MediaPlayHandler(private val context: Context?) : FunctionHandler
*/ */
private class MediaPauseHandler(private val context: Context?) : FunctionHandler { private class MediaPauseHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String { override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
Log.d("MediaPauseHandler", "📤 正在发送广播 context = $context") Log.d("MediaPauseHandler", "📤 正在发送广播 context = $context")
// 发送媒体暂停广播 // 发送媒体暂停广播
val intent = Intent("com.yunqiinnovation.music_service.MEDIA_PAUSE").apply { val intent = Intent("com.yunqiinnovation.music_service.MEDIA_PAUSE").apply {
setPackage(context?.packageName) // 设置包名使其成为显式 Intent setPackage(context.packageName)
} }
context?.sendBroadcast(intent) context.sendBroadcast(intent)
return "{\"result\": \"已暂停媒体播放\"}"
return JSONObject().apply {
put("result", "已暂停媒体播放")
put("notreply", true)
put("broadcast", false)
}.toString()
} }
} }
/** /**
* 媒体暂停处理器 * 媒体关闭处理器
*/ */
private class MediaCloseHandler(private val context: Context?) : FunctionHandler { private class MediaCloseHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String { override suspend fun handle(arguments: Map<String, Any>): String {
Log.d("MediaPauseHandler", "📤 正在发送广播 context = $context") if (context == null) {
// 发送媒体暂停广播 return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
Log.d("MediaCloseHandler", "📤 正在发送广播 context = $context")
// 发送媒体关闭广播
val intent = Intent("com.yunqiinnovation.music_service.MEDIA_CLOSE").apply { val intent = Intent("com.yunqiinnovation.music_service.MEDIA_CLOSE").apply {
setPackage(context?.packageName) // 设置包名使其成为显式 Intent setPackage(context.packageName)
} }
context?.sendBroadcast(intent) context.sendBroadcast(intent)
return "{\"result\": \"已关闭媒体播放\"}"
return JSONObject().apply {
put("result", "已关闭媒体播放")
put("notreply", true)
put("broadcast", false)
}.toString()
} }
} }
/** /**
@ -472,13 +1003,26 @@ private class MediaCloseHandler(private val context: Context?) : FunctionHandler
*/ */
private class MediaPreviousHandler(private val context: Context?) : FunctionHandler { private class MediaPreviousHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String { override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
// 发送切换上一首广播 // 发送切换上一首广播
// context?.applicationContext?.sendBroadcast(Intent("com.yunqiinnovation.music_service.MEDIA_PREVIOUS"))
val intent = Intent("com.yunqiinnovation.music_service.MEDIA_PREVIOUS").apply { val intent = Intent("com.yunqiinnovation.music_service.MEDIA_PREVIOUS").apply {
setPackage(context?.packageName) // 设置包名使其成为显式 Intent setPackage(context.packageName)
} }
context?.sendBroadcast(intent) context.sendBroadcast(intent)
return "{\"result\": \"已切换到上一首\"}"
return JSONObject().apply {
put("result", "已切换到上一首")
put("notreply", true)
put("broadcast", false)
}.toString()
} }
} }
@ -487,13 +1031,26 @@ private class MediaPreviousHandler(private val context: Context?) : FunctionHand
*/ */
private class MediaNextHandler(private val context: Context?) : FunctionHandler { private class MediaNextHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String { override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
// 发送切换下一首广播 // 发送切换下一首广播
// context?.applicationContext?.sendBroadcast(Intent("com.yunqiinnovation.music_service.MEDIA_NEXT"))
val intent = Intent("com.yunqiinnovation.music_service.MEDIA_NEXT").apply { val intent = Intent("com.yunqiinnovation.music_service.MEDIA_NEXT").apply {
setPackage(context?.packageName) // 设置包名使其成为显式 Intent setPackage(context.packageName)
} }
context?.sendBroadcast(intent) context.sendBroadcast(intent)
return "{\"result\": \"已切换到下一首\"}"
return JSONObject().apply {
put("result", "已切换到下一首")
put("notreply", true)
put("broadcast", false)
}.toString()
} }
} }
@ -503,7 +1060,12 @@ private class MediaNextHandler(private val context: Context?) : FunctionHandler
private class OpenRecorderHandler(private val context: Context?) : FunctionHandler { private class OpenRecorderHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String { override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) { if (context == null) {
return "{\"result\": \"上下文未初始化\"}" return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
} }
return try { return try {
@ -520,18 +1082,32 @@ private class OpenRecorderHandler(private val context: Context?) : FunctionHandl
when { when {
context.packageManager?.queryIntentActivities(recordIntent, 0)?.isNotEmpty() == true -> { context.packageManager?.queryIntentActivities(recordIntent, 0)?.isNotEmpty() == true -> {
context.startActivity(recordIntent) context.startActivity(recordIntent)
"{\"result\": \"已打开录音机\"}" JSONObject().apply {
put("result", "已打开录音机")
}.toString()
} }
context.packageManager?.queryIntentActivities(recorderIntent, 0)?.isNotEmpty() == true -> { context.packageManager?.queryIntentActivities(recorderIntent, 0)?.isNotEmpty() == true -> {
context.startActivity(recorderIntent) context.startActivity(recorderIntent)
"{\"result\": \"已打开音频应用\"}" JSONObject().apply {
put("result", "已打开音频应用")
}.toString()
} }
else -> { else -> {
"{\"result\": \"无法打开录音机应用\"}" JSONObject().apply {
put("error", JSONObject().apply {
put("code", "no_recorder_app")
put("message", "无法打开录音机应用")
})
}.toString()
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
"{\"result\": \"打开录音机失败:${e.message}\"}" JSONObject().apply {
put("error", JSONObject().apply {
put("code", "recorder_failed")
put("message", "打开录音机失败: ${e.message}")
})
}.toString()
} }
} }
} }

126
local_plugins/jl_opus/README.md

@ -0,0 +1,126 @@
# JL OPUS Plugin
基于杰理音频编解码库的Flutter插件,提供OPUS音频格式的解码功能。
## 功能
- 支持OPUS文件解码为PCM格式
- 支持OPUS数据流解码
- 支持多种采样率和通道配置
- 支持带协议头/不带协议头的OPUS数据
## 引入方式
在pubspec.yaml中添加依赖:
```yaml
dependencies:
jl_opus:
path: ./local_plugins/jl_opus
```
## 使用方法
### 初始化插件
```dart
import 'package:jl_opus/jl_opus.dart';
final jlOpus = JlOpus();
```
### 解码OPUS文件
```dart
Future<void> decodeOpusFile() async {
final inPath = '/path/to/input.opus';
final outPath = '/path/to/output.pcm';
// 创建解码选项
final option = OpusOption(
hasHeader: false, // 是否有协议头
channel: 1, // 通道数:1=单声道,2=立体声
sampleRate: 16000, // 采样率
packetSize: 40, // 数据包大小(仅当hasHeader=false时生效)
);
try {
// 开始解码文件
final outFilePath = await jlOpus.decodeOpusFile(inPath, outPath, option);
print('解码成功: $outFilePath');
} catch (e) {
print('解码失败: $e');
}
}
```
### 解码OPUS数据流
```dart
Future<void> decodeOpusStream() async {
// 监听解码后的PCM数据
jlOpus.decodedDataStream.listen((pcmData) {
print('收到解码后的PCM数据: ${pcmData.length} 字节');
// 处理解码后的PCM数据
});
// 创建解码选项
final option = OpusOption(
hasHeader: false,
channel: 1,
sampleRate: 16000,
packetSize: 40,
);
// 开始解码数据流
final success = await jlOpus.startDecodeStream(option);
if (success) {
print('开始解码数据流');
// 写入OPUS数据进行解码
final opusData = Uint8List.fromList([/* OPUS数据 */]);
await jlOpus.writeAudioStream(opusData);
// 数据处理完成后停止解码
await jlOpus.stopDecodeStream();
}
}
```
### 监听事件
```dart
void listenToEvents() {
jlOpus.eventStream.listen((event) {
switch (event.event) {
case 'onStart':
print('开始${event.type == "file" ? "文件" : "流"}解码');
break;
case 'onComplete':
print('完成${event.type == "file" ? "文件" : "流"}解码');
if (event.filePath != null) {
print('输出文件: ${event.filePath}');
}
break;
case 'onError':
print('解码错误: [${event.errorCode}] ${event.errorMessage}');
break;
}
});
}
```
### 资源释放
```dart
Future<void> disposePlugin() async {
await jlOpus.dispose();
print('插件资源已释放');
}
```
## 注意事项
1. OPUS解码需要正确设置参数,特别是采样率和通道数必须与原始音频匹配
2. 使用前请确保jl_audio_decode_V2.0.0_20005.aar包已添加到libs目录
3. 本插件仅支持Android平台

62
local_plugins/jl_opus/android/build.gradle.kts

@ -0,0 +1,62 @@
plugins {
// Android Library 插件
id("com.android.library")
// Kotlin Android 插件
id("org.jetbrains.kotlin.android")
id("com.github.johnrengelman.shadow") version "8.1.1"
}
// 添加mavenLocal()仓库
repositories {
mavenLocal()
google()
mavenCentral()
}
android {
// 命名空间,对应你插件的包名(需与代码内包名保持一致)
namespace = "com.yunqiinnovation.jl_opus"
// 目标 SDK 版本
compileSdk = 33
defaultConfig {
// 最低 SDK 版本
minSdk = 21
targetSdk = 33
}
// Java 语言级别兼容配置
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
// Kotlin 语言级别
kotlinOptions {
jvmTarget = "11"
}
// 添加packaging配置,排除冲突文件
packaging {
resources {
excludes.add("META-INF/DEPENDENCIES")
excludes.add("META-INF/LICENSE")
excludes.add("META-INF/LICENSE.txt")
excludes.add("META-INF/license.txt")
excludes.add("META-INF/NOTICE")
excludes.add("META-INF/NOTICE.txt")
excludes.add("META-INF/notice.txt")
excludes.add("META-INF/*.kotlin_module")
}
}
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10")
implementation("androidx.annotation:annotation:1.5.0")
implementation("androidx.core:core-ktx:1.9.0")
// 使用从本地Maven仓库引用的AAR依赖
implementation("com.jieli.audio:jl_audio_decode:2.0.0")
}

1
local_plugins/jl_opus/android/settings.gradle.kts

@ -0,0 +1 @@
rootProject.name = "jl_opus"

9
local_plugins/jl_opus/android/src/main/AndroidManifest.xml

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.jl_opus">
<!-- 外部存储权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
</manifest>

285
local_plugins/jl_opus/android/src/main/kotlin/com/yunqiinnovation/jl_opus/JlOpusPlugin.kt

@ -0,0 +1,285 @@
package com.yunqiinnovation.jl_opus
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.annotation.NonNull
import com.jieli.jl_audio_decode.opus.model.OpusOption
import com.jieli.jl_audio_decode.callback.OnDecodeStreamCallback
import com.jieli.jl_audio_decode.callback.OnStateCallback
import com.jieli.jl_audio_decode.opus.OpusManager
import com.jieli.jl_audio_decode.exceptions.OpusException
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import java.io.File
/** JlOpusPlugin */
class JlOpusPlugin: FlutterPlugin, MethodCallHandler {
private lateinit var channel: MethodChannel
private lateinit var eventChannel: EventChannel
private lateinit var context: Context
private var opusManager: OpusManager? = null
private var streamSink: EventChannel.EventSink? = null
// 添加主线程Handler用于确保在主线程回调
private val mainHandler = Handler(Looper.getMainLooper())
companion object {
private const val TAG = "JlOpusPlugin"
private const val CHANNEL_NAME = "com.yunqiinnovation.jl_opus"
private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_opus/events"
}
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME)
channel.setMethodCallHandler(this)
eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, EVENT_CHANNEL_NAME)
eventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
streamSink = events
}
override fun onCancel(arguments: Any?) {
streamSink = null
}
})
}
// 在主线程上安全发送事件
private fun sendEventOnMainThread(event: Map<String, Any?>) {
if (streamSink == null) return
if (Looper.myLooper() == Looper.getMainLooper()) {
// 如果已经在主线程,直接发送
streamSink?.success(event)
} else {
// 否则切换到主线程
mainHandler.post {
streamSink?.success(event)
}
}
}
// 在主线程上安全返回结果
private fun sendResultOnMainThread(result: Result, value: Any?) {
if (Looper.myLooper() == Looper.getMainLooper()) {
// 如果已经在主线程,直接返回
result.success(value)
} else {
// 否则切换到主线程
mainHandler.post {
result.success(value)
}
}
}
// 在主线程上安全返回错误
private fun sendErrorOnMainThread(result: Result, errorCode: String, errorMessage: String, errorDetails: Any?) {
if (Looper.myLooper() == Looper.getMainLooper()) {
// 如果已经在主线程,直接返回
result.error(errorCode, errorMessage, errorDetails)
} else {
// 否则切换到主线程
mainHandler.post {
result.error(errorCode, errorMessage, errorDetails)
}
}
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
// if (opusManager == null) {
// result.error("OPUS_MANAGER_NULL", "OpusManager未初始化", null)
// return
// }
when (call.method) {
"initOpusDecoder" -> {
// 初始化OpusManager
opusManager = OpusManager()
Log.i("TAG", "OpusManager初始化成功")
result.success(true)
}
"decodeOpusFile" -> {
val inPath = call.argument<String>("inPath")
val outPath = call.argument<String>("outPath")
val hasHeader = call.argument<Boolean>("hasHeader") ?: false
val channel = call.argument<Int>("channel") ?: 1
val sampleRate = call.argument<Int>("sampleRate") ?: 16000
val packetSize = call.argument<Int>("packetSize") ?: 40
if (inPath == null || outPath == null) {
result.error("INVALID_ARGS", "输入或输出路径不能为空", null)
return
}
// 确保输入文件存在
val inFile = File(inPath)
if (!inFile.exists()) {
result.error("FILE_NOT_FOUND", "输入文件不存在: $inPath", null)
return
}
// 创建解码参数
val option = OpusOption()
.setHasHead(hasHeader)
.setChannel(channel)
.setSampleRate(sampleRate)
.setPacketSize(packetSize)
Log.d(TAG, "开始解码文件, 参数: $option, 输入: $inPath, 输出: $outPath")
opusManager?.decodeFile(inPath, outPath, option, object : OnStateCallback {
override fun onStart() {
Log.d(TAG, "文件解码开始")
sendEventOnMainThread(mapOf(
"event" to "onStart",
"type" to "file"
))
}
override fun onComplete(outFilePath: String?) {
Log.d(TAG, "文件解码完成: $outFilePath")
sendEventOnMainThread(mapOf(
"event" to "onComplete",
"type" to "file",
"filePath" to outFilePath
))
sendResultOnMainThread(result, outFilePath)
}
override fun onError(code: Int, message: String?) {
Log.e(TAG, "文件解码错误: [$code] $message")
sendEventOnMainThread(mapOf(
"event" to "onError",
"type" to "file",
"code" to code,
"message" to message
))
sendErrorOnMainThread(result, "DECODE_ERROR", "解码出错: [$code] $message", null)
}
})
}
"startDecodeStream" -> {
val hasHeader = call.argument<Boolean>("hasHeader") ?: false
val channel = call.argument<Int>("channel") ?: 1
val sampleRate = call.argument<Int>("sampleRate") ?: 16000
val packetSize = call.argument<Int>("packetSize") ?: 40
// 如果已经在解码流,先停止
if (opusManager?.isDecodeStream == true) {
opusManager?.stopDecodeStream()
}
// 创建解码参数
val option = OpusOption()
.setHasHead(hasHeader)
.setChannel(channel)
.setSampleRate(sampleRate)
.setPacketSize(packetSize)
Log.d(TAG, "开始解码数据流, 参数: $option")
opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback {
override fun onDecodeStream(data: ByteArray?) {
if (data != null) {
sendEventOnMainThread(mapOf(
"event" to "onDecodeStream",
"data" to data
))
}
}
override fun onStart() {
Log.d(TAG, "数据流解码开始")
sendEventOnMainThread(mapOf(
"event" to "onStart",
"type" to "stream"
))
sendResultOnMainThread(result, true)
}
override fun onComplete(outPath: String?) {
Log.d(TAG, "数据流解码完成: $outPath")
sendEventOnMainThread(mapOf(
"event" to "onComplete",
"type" to "stream"
))
}
override fun onError(code: Int, message: String?) {
Log.e(TAG, "数据流解码错误: [$code] $message")
sendEventOnMainThread(mapOf(
"event" to "onError",
"type" to "stream",
"code" to code,
"message" to message
))
sendErrorOnMainThread(result, "DECODE_STREAM_ERROR", "数据流解码出错: [$code] $message", null)
}
})
}
"stopDecodeStream" -> {
if (opusManager?.isDecodeStream == true) {
opusManager?.stopDecodeStream()
result.success(true)
} else {
result.success(false)
}
}
"writeAudioStream" -> {
val data = call.argument<ByteArray>("data")
if (data == null) {
result.error("INVALID_ARGS", "音频数据不能为空", null)
return
}
if (opusManager?.isDecodeStream == true) {
opusManager?.writeAudioStream(data)
result.success(true)
} else {
result.error("NOT_DECODING", "当前没有处于解码状态", null)
}
}
"isDecoding" -> {
result.success(opusManager?.isDecodeStream == true)
}
"dispose" -> {
opusManager?.let {
if (it.isDecodeStream) {
it.stopDecodeStream()
}
it.release()
}
opusManager = null
result.success(true)
}
else -> {
result.notImplemented()
}
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
eventChannel.setStreamHandler(null)
opusManager?.let {
if (it.isDecodeStream) {
it.stopDecodeStream()
}
it.release()
}
opusManager = null
}
}

BIN
local_plugins/jl_opus/android/src/main/libs/jl_audio_decode_V2.0.0_20005.aar

Binary file not shown.

214
local_plugins/jl_opus/lib/jl_opus.dart

@ -0,0 +1,214 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/services.dart';
/// OPUS解码参数选项
class OpusOption {
/// 是否具有协议头
final bool hasHeader;
/// 通道数。取值范围:[1, 2]
final int channel;
/// 采样率。参考值:{8000, 16000, 24000, 32000, 44100, 48000}
final int sampleRate;
/// 数据包长度。仅hasHeader = false时生效。
final int packetSize;
/// 构造函数
OpusOption({
this.hasHeader = false,
this.channel = 1,
this.sampleRate = 16000,
this.packetSize = 40,
});
/// 转换为参数Map
Map<String, dynamic> toMap() {
return {
'hasHeader': hasHeader,
'channel': channel,
'sampleRate': sampleRate,
'packetSize': packetSize,
};
}
}
/// 杰理OPUS编解码器事件
class OpusEvent {
/// 事件类型
final String event;
/// 数据类型 (file或stream)
final String? type;
/// 解码后的数据 (仅在流解码时有效)
final Uint8List? data;
/// 输出文件路径 (仅在文件解码完成时有效)
final String? filePath;
/// 错误码 (仅在发生错误时有效)
final int? errorCode;
/// 错误信息 (仅在发生错误时有效)
final String? errorMessage;
/// 构造函数
OpusEvent({
required this.event,
this.type,
this.data,
this.filePath,
this.errorCode,
this.errorMessage,
});
/// 从Map构造对象
factory OpusEvent.fromMap(Map<dynamic, dynamic> map) {
return OpusEvent(
event: map['event'],
type: map['type'],
data: map['data'],
filePath: map['filePath'],
errorCode: map['code'],
errorMessage: map['message'],
);
}
}
/// 杰理OPUS编解码插件
class JlOpus {
static const MethodChannel _methodChannel = MethodChannel('com.yunqiinnovation.jl_opus');
static const EventChannel _eventChannel = EventChannel('com.yunqiinnovation.jl_opus/events');
/// 事件流
Stream<OpusEvent>? _eventStream;
/// 获取事件流
Stream<OpusEvent> get eventStream {
_eventStream ??= _eventChannel.receiveBroadcastStream().map((event) {
if (event is Map) {
return OpusEvent.fromMap(event);
}
throw PlatformException(
code: 'INVALID_EVENT',
message: '无效的事件数据',
);
});
return _eventStream!;
}
/// 解码后的数据流
final StreamController<Uint8List> _decodedDataController = StreamController<Uint8List>.broadcast();
/// 获取解码数据流
Stream<Uint8List> get decodedDataStream => _decodedDataController.stream;
/// 构造函数,设置事件监听
JlOpus() {
_setupEventListener();
}
/// 设置事件监听
void _setupEventListener() {
eventStream.listen((event) {
if (event.event == 'onDecodeStream' && event.data != null) {
_decodedDataController.add(event.data!);
}
});
}
/// 初始化OPUS解码器
Future<bool> initOpusDecoder() async {
try {
final result = await _methodChannel.invokeMethod<bool>('initOpusDecoder');
return result ?? false;
} catch (e) {
return false;
}
}
/// 解码OPUS文件
///
/// [inPath] 输入文件路径
/// [outPath] 输出文件路径
/// [option] 解码参数
Future<String?> decodeOpusFile(String inPath, String outPath, OpusOption option) async {
try {
final result = await _methodChannel.invokeMethod<String>(
'decodeOpusFile',
{
'inPath': inPath,
'outPath': outPath,
...option.toMap(),
},
);
return result;
} catch (e) {
rethrow;
}
}
/// 开始解码OPUS数据流
///
/// [option] 解码参数
Future<bool> startDecodeStream(OpusOption option) async {
try {
final result = await _methodChannel.invokeMethod<bool>(
'startDecodeStream',
option.toMap(),
);
return result ?? false;
} catch (e) {
rethrow;
}
}
/// 停止解码OPUS数据流
Future<bool> stopDecodeStream() async {
try {
final result = await _methodChannel.invokeMethod<bool>('stopDecodeStream');
return result ?? false;
} catch (e) {
return false;
}
}
/// 写入OPUS音频数据进行解码
///
/// [data] OPUS音频数据
Future<bool> writeAudioStream(Uint8List data) async {
try {
final result = await _methodChannel.invokeMethod<bool>(
'writeAudioStream',
{'data': data},
);
return result ?? false;
} catch (e) {
rethrow;
}
}
/// 判断是否正在解码
Future<bool> isDecoding() async {
try {
final result = await _methodChannel.invokeMethod<bool>('isDecoding');
return result ?? false;
} catch (e) {
return false;
}
}
/// 释放资源
Future<bool> dispose() async {
try {
_decodedDataController.close();
final result = await _methodChannel.invokeMethod<bool>('dispose');
return result ?? false;
} catch (e) {
return false;
}
}
}

25
local_plugins/jl_opus/pubspec.yaml

@ -0,0 +1,25 @@
name: jl_opus
description: 杰理OPUS编解码库Flutter插件
version: 0.0.1
homepage:
environment:
sdk: ">=2.12.0 <3.0.0"
flutter: ">=2.0.0"
dependencies:
flutter:
sdk: flutter
path_provider: ^2.0.0
permission_handler: ^11.4.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
plugin:
platforms:
android:
package: com.yunqiinnovation.jl_opus
pluginClass: JlOpusPlugin

9
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt

@ -35,14 +35,14 @@ class MusicService : MediaBrowserServiceCompat() {
private const val DELETE_ACTION = "com.yunqiinnovation.music_service.DELETE_NOTIFICATION" private const val DELETE_ACTION = "com.yunqiinnovation.music_service.DELETE_NOTIFICATION"
private val isRunning = AtomicBoolean(false) private val isRunning = AtomicBoolean(false)
private var currentUrlInternal: String? = null private var currentIdInternal: Int? = null
private var currentStatus = false private var currentStatus = false
private var playlist = mutableListOf<Map<String,Any>>() private var playlist = mutableListOf<Map<String,Any>>()
private var currentIndex = 0 private var currentIndex = 0
fun isServiceRunning() = isRunning.get() fun isServiceRunning() = isRunning.get()
fun isPlaying() = currentStatus fun isPlaying() = currentStatus
fun getCurrentUrl(): String? = currentUrlInternal fun getCurrentId(): Int? = currentIdInternal
// 获取播放进度信息 // 获取播放进度信息
fun getProgress(): Map<String, Any> { fun getProgress(): Map<String, Any> {
@ -360,14 +360,15 @@ class MusicService : MediaBrowserServiceCompat() {
} }
private fun play(song: Map<String,Any>) { private fun play(song: Map<String,Any>) {
val url = song["url"] ?: return val url = song["url"] as? String ?: return
Log.i(TAG, "开始播放: $url") Log.i(TAG, "开始播放: $url")
val id = song["id"] as? Int ?: 0
val title = song["title"] as? String ?: "未知标题" val title = song["title"] as? String ?: "未知标题"
val artist = song["artist"] as? String ?: "未知歌手" val artist = song["artist"] as? String ?: "未知歌手"
val coverUrl = song["coverUrl"] as String val coverUrl = song["coverUrl"] as String
mediaPlayer?.release() mediaPlayer?.release()
currentStatus = true currentStatus = true
currentUrlInternal = url as String currentIdInternal = id
mediaPlayer = MediaPlayer().apply { mediaPlayer = MediaPlayer().apply {
setDataSource(url) setDataSource(url)
setAudioStreamType(AudioManager.STREAM_MUSIC) setAudioStreamType(AudioManager.STREAM_MUSIC)

4
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt

@ -63,10 +63,10 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler {
} }
"getStatus" -> { "getStatus" -> {
val isPlaying = MusicService.isPlaying() val isPlaying = MusicService.isPlaying()
val currentUrl = MusicService.getCurrentUrl() val currentId = MusicService.getCurrentId()
val status = mapOf( val status = mapOf(
"isPlaying" to isPlaying, "isPlaying" to isPlaying,
"url" to currentUrl "id" to currentId
) )
result.success(status) result.success(status)
} }

2
local_plugins/music_service/lib/music_service.dart

@ -56,7 +56,7 @@ class MusicService {
final result = await _methodChannel.invokeMethod<Map>('getStatus'); final result = await _methodChannel.invokeMethod<Map>('getStatus');
return { return {
'isPlaying': result?['isPlaying'] ?? false, 'isPlaying': result?['isPlaying'] ?? false,
'url': result?['url'] ?? "", 'id': result?['id'] ?? 0,
}; };
} }

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

@ -479,7 +479,7 @@ class MCPSubClient(
retryCount++ retryCount++
currentReconnectDelay = minOf(currentReconnectDelay * 2, maxReconnectDelay) currentReconnectDelay = minOf(currentReconnectDelay * 2, maxReconnectDelay)
Log.w(TAG, "连接失败,将在 ${currentReconnectDelay}ms 后尝试重连 (尝试 $retryCount/$maxRetryAttempts)") // Log.w(TAG, "连接失败,将在 ${currentReconnectDelay}ms 后尝试重连 (尝试 $retryCount/$maxRetryAttempts)")
delay(currentReconnectDelay) delay(currentReconnectDelay)
connect() connect()

5
pubspec.yaml

@ -93,9 +93,12 @@ dependencies:
path: local_plugins/amap_service path: local_plugins/amap_service
location_service: location_service:
path: local_plugins/location_service path: local_plugins/location_service
bytedance_speech:
path: local_plugins/bytedance_speech
realtime: realtime:
path: local_plugins/realtime path: local_plugins/realtime
jl_opus:
path: local_plugins/jl_opus
test: test:
path: local_plugins/test path: local_plugins/test

Loading…
Cancel
Save