diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 44c9dd0dc..20d5ead25 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -5,7 +5,6 @@ repositories {
mavenCentral()
flatDir {
dirs(project(":spotify_sdk").projectDir.resolve("libs"))
- dirs(project(":jl_ota").projectDir.resolve("libs"))
}
}
diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts
index fa5282d77..cf7591c68 100644
--- a/android/settings.gradle.kts
+++ b/android/settings.gradle.kts
@@ -39,7 +39,7 @@ include(":ble_service")
include(":classic_bluetooth")
include(":agent_service")
include(":speech")
-include(":open_ai_service")
+include(":chat_api")
// 设置azure_speech项目的路径
project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android")
@@ -50,4 +50,4 @@ project(":ble_service").projectDir = file("../local_plugins/ble_service/android"
project(":classic_bluetooth").projectDir = file("../local_plugins/classic_bluetooth/android")
project(":agent_service").projectDir = file("../local_plugins/agent_service/android")
project(":speech").projectDir = file("../local_plugins/speech/android")
-project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/android")
\ No newline at end of file
+project(":chat_api").projectDir = file("../local_plugins/chat_api/android")
\ No newline at end of file
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 9a7b93fa1..b7e7fc98f 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -123,9 +123,9 @@
NSBluetoothPeripheralUsageDescription
需要蓝牙权限用于连接和管理音频设备
NSLocationAlwaysAndWhenInUseUsageDescription
- 需要位置权限用于搜索附近的蓝牙设备
+ 语音助手需要位置权限来为您提供基于位置的信息和服务
NSLocationWhenInUseUsageDescription
- 需要位置权限用于搜索附近的蓝牙设备
+ 语音助手需要位置权限来为您提供基于位置的信息和服务
NSMicrophoneUsageDescription
需要麦克风权限用于语音识别和录音功能
NSCalendarsUsageDescription
diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart
index 1b472e8ab..4fd99d91e 100644
--- a/lib/data/services/ble_manager.dart
+++ b/lib/data/services/ble_manager.dart
@@ -159,7 +159,7 @@ class BleManager extends GetxService {
final state = event['state'] as int;
final oldState = _connectionState.value;
_connectionState.value = state;
-
+
Logger.d('BleManager', '🔄 BLE状态更新: $oldState → $state');
Logger.d('BleManager', '📡 通知监听器状态变化: ${_connectionState.value}');
@@ -179,6 +179,19 @@ class BleManager extends GetxService {
state == BleConnectionState.error) {
Logger.info('BLE设备已断开或发生错误');
_isConnected.value = false;
+ // 更新设备信息
+ _updateDeviceInfoIfNeeded(
+ batteryLevel: 0,
+ leftEarBattery: 0,
+ rightEarBattery: 0,
+ caseBattery: 0,
+ leftEarCharging: false,
+ rightEarCharging: false,
+ caseCharging: false,
+ firmwareVersion: '',
+ firmwareProductId: '',
+ firmwareProductColor: '',
+ );
}
}
@@ -418,7 +431,8 @@ class BleManager extends GetxService {
} else {
Logger.warning('电池信息获取指令发送失败');
}
-
+ // 添加延迟
+ await Future.delayed(const Duration(seconds: 1));
// 获取设备版本信息
final versionResult = await _bleService.getDeviceVersion();
if (versionResult) {
@@ -426,7 +440,8 @@ class BleManager extends GetxService {
} else {
Logger.warning('版本信息获取指令发送失败');
}
-
+ // 添加延迟
+ await Future.delayed(const Duration(seconds: 1));
// 获取产品id
final productIdResult = await _bleService.getProductId();
if (productIdResult) {
@@ -655,7 +670,7 @@ class BleManager extends GetxService {
}
// ==================== 配对相关方法 ====================
-
+
/// 检查设备是否支持Companion Device Manager
Future isCompanionSupported() async {
try {
@@ -732,6 +747,16 @@ class BleManager extends GetxService {
}
}
+ /// 清除BLE配对设备
+ Future clearPairedDevices() async {
+ try {
+ return await _bleService.clearAssociations();
+ } catch (e) {
+ Logger.error('清除BLE配对设备失败: ${e.toString()}');
+ return false;
+ }
+ }
+
/// 注册服务
static Future init() async {
if (!Get.isRegistered()) {
diff --git a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart
index c9c827551..8d0f3653b 100644
--- a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart
+++ b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart
@@ -7,82 +7,148 @@ import '../model/ota_upgrade_model.dart';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';
-import 'package:jl_ota/jl_ota.dart';
import 'package:ota/ota.dart';
import '../../../data/services/ble_manager.dart';
class OtaUpgradeController extends GetxController {
+ static const int NOT_STARTED = -1; // 未开始
+ static const int VALIDATION_FILE = 0; // 验证文件
+ static const int ONUPGRADE = 1; // 升级中
+ static const int START_UPGRADING = 2; // 开始升级
+ static const int UPGRADE_SUCCESS = 3; // 升级成功
+ static const int UPGRADE_FAILED = 4; // 升级失败
+ static const int CONNECTION_DISCONNECTION = 5; // 连接断开
+ static const int CONNECTION_SUCCESS = 6; // 连接成功
+ static const int DOWNLOADING_UP_FILES = 7; // 下载升级文件
+
+// 错误信息,用于存储错误提示
final RxString errorMessage = ''.obs;
- final RxBool isFetchingDeviceInfo = false.obs;
- final Dio _dio = Dio();
+
+// 最新版本号
final RxString latestVersion = ''.obs;
+
+// 当前设备固件版本号
final RxString currentVersion = ''.obs;
+
+// 更新包地址,用于下载更新文件
String updatepackageaddress = '';
+
+// 下载进度,范围为 0.0 到 1.0
final RxDouble downloadProgress = 0.0.obs;
- final RxBool isUpdating = false.obs;
+
+// 是否有可用更新的状态
final isUpdateAvailable = true.obs;
- final RxDouble progress = 0.0.obs;
- var jlota = Ota();
- // 蓝牙服务
+// 当前升级进度,范围为 0.0 到 1.0
+ // final RxDouble progress = 0.0.obs;
+
+// OTA 单例实例,用于处理升级相关逻辑
+ final jlota = Ota.instance;
+
+// 是否正在下载更新包的状态
+ final RxBool isDownloading = false.obs;
+
+// 蓝牙管理服务,用于获取设备信息和通信
final _bleManager = Get.find();
+ final maxRetryAttempts = 3; // 最大重试次数
+ int currentRetryCount = 0;
+
+ final RxBool isFetchingDeviceInfo = true.obs;
+
+ final RxDouble progress = 0.0.obs; // 控制器自己的可观察属性
+ final RxInt type = OtaEvent.NOT_STARTED.obs;
+ final RxBool isUpgrade = false.obs;
@override
void onInit() {
super.onInit();
_getCurrentVersion();
}
+ @override
+ void onClose() {
+ jlota.onOtaClose();
+ isUpgrade.value = false;
+ super.onClose();
+ }
+
// 添加重试方法
void retryConnection() {
errorMessage.value = '';
_getCurrentVersion();
}
+ String getStatusMessage(int type) {
+ switch (type) {
+ case NOT_STARTED: // -1
+ return "未开始".tr;
+ case VALIDATION_FILE: // 0
+ return "验证文件中".tr;
+ case ONUPGRADE: // 1
+ return "升级中".tr;
+ case START_UPGRADING: // 2
+ return "开始升级".tr;
+ case UPGRADE_SUCCESS: // 3
+ return "升级成功".tr;
+ case UPGRADE_FAILED: // 4
+ return "升级失败".tr;
+ case CONNECTION_DISCONNECTION: // 5
+ return "连接断开".tr;
+ case CONNECTION_SUCCESS: // 6
+ return "连接成功".tr;
+ case DOWNLOADING_UP_FILES: // 7 (新增)
+ return "下载升级文件".tr;
+ default:
+ return "未知状态".tr; // 更安全的默认值
+ }
+ }
+
// 添加更新方法
Future startUpdate() async {
try {
- isUpdating.value = true;
checkUpdate();
// 实现更新逻辑...
} catch (e) {
errorMessage.value = e.toString();
Get.snackbar('更新失败', e.toString());
- } finally {
- isUpdating.value = false;
}
}
Future _getCurrentVersion() async {
+ _listenToEvents();
try {
- jlota.getPlatformVersion();
+ isFetchingDeviceInfo.value = true;
+ errorMessage.value = '';
// 添加获取设备信息的重试机制
DeviceInfo? deviceInfo = _bleManager.deviceInfo.value;
- // 第一次检查设备信息
- if (deviceInfo == null) {
- // 显示加载提示
- isFetchingDeviceInfo.value = true;
+ // // 第一次检查设备信息
+ // if (deviceInfo == null) {
+ // // 尝试重新获取设备信息
+ // await _bleManager.fetchDeviceInfo();
+ // }
- // 尝试重新获取设备信息
- await _bleManager.fetchDeviceInfo();
-
- // 等待后再次获取设备信息
+ while (currentRetryCount < maxRetryAttempts) {
deviceInfo = _bleManager.deviceInfo.value;
- isFetchingDeviceInfo.value = false;
- // 仍然为空则抛出异常
- if (deviceInfo == null) {
- throw const FormatException('无法获取设备信息,请检查设备连接');
+ // 检查信息有效性
+ if (deviceInfo != null &&
+ deviceInfo.firmwareVersion.isNotEmpty &&
+ deviceInfo.firmwareProductId.isNotEmpty) {
+ break;
}
- }
- // 检查固件信息有效性
- if (deviceInfo.firmwareVersion.isEmpty ||
+ // 获取设备信息
+ await _bleManager.fetchDeviceInfo();
+ await Future.delayed(const Duration(seconds: 5));
+ currentRetryCount++;
+ }
+ // 最终验证
+ if (deviceInfo == null ||
+ deviceInfo.firmwareVersion.isEmpty ||
deviceInfo.firmwareProductId.isEmpty) {
- throw const FormatException('设备固件信息不完整');
+ throw "获取设备信息失败,请检查连接";
}
-
// 正常流程
currentVersion.value = deviceInfo.firmwareVersion;
print('getequipmentinfo,id: ${deviceInfo.firmwareProductId}');
@@ -98,7 +164,8 @@ class OtaUpgradeController extends GetxController {
final resp = OtaConfigResp.fromJson(response);
print('getequipmentinfo,resp.systemversion: ${resp.systemversion}');
- latestVersion.value = "1";
+ latestVersion.value = resp.systemversion!;
+
updatepackageaddress = resp.updatepackageaddress!;
// 版本检查
@@ -141,9 +208,9 @@ class OtaUpgradeController extends GetxController {
// 版本更新
Future checkUpdate() async {
try {
- if (latestVersion.value != "")
+ if (latestVersion.value != "") {
_showUpdateDialog(latestVersion.value);
- else {
+ } else {
Get.snackbar('错误', '获取最新版本信息失败');
}
} catch (e) {
@@ -151,20 +218,28 @@ class OtaUpgradeController extends GetxController {
}
}
- // 显示更新对话框
- void _showUpdateDialog(String latest) {
+ void _showUpdateDialog(String latest) async {
+ print('显示更新对话框');
Get.dialog(
AlertDialog(
title: Text('发现新版本 $latest'),
content: Text('是否立即更新?'),
actions: [
TextButton(
- onPressed: () => Get.back(),
+ onPressed: () async {
+ print('取消按钮被点击');
+
+ Get.back();
+ Future.delayed(Duration(milliseconds: 300));
+ },
child: Text('取消'),
),
TextButton(
- onPressed: () {
+ onPressed: () async {
+ print('更新按钮被点击');
+
Get.back();
+ Future.delayed(Duration(milliseconds: 300));
startDownload();
},
child: Text('更新'),
@@ -176,7 +251,7 @@ class OtaUpgradeController extends GetxController {
// 修改后的startDownload方法
Future startDownload() async {
- isUpdating.value = true;
+ isDownloading.value = true;
int lastIndex = updatepackageaddress.lastIndexOf('/');
String fileName = updatepackageaddress.substring(lastIndex + 1);
String directoryName = "ota_upgrades";
@@ -188,8 +263,7 @@ class OtaUpgradeController extends GetxController {
headers: {'Accept-Encoding': 'gzip'},
));
- Get.snackbar('升级命令已发送', '开始下载...');
-
+ type.value = DOWNLOADING_UP_FILES;
try {
final Directory? downloadsDir = await getDownloadsDirectory();
if (downloadsDir == null) {
@@ -218,14 +292,14 @@ class OtaUpgradeController extends GetxController {
}
} catch (e) {
Get.snackbar('错误', '检查本地文件失败: $e');
- isUpdating.value = false;
+
return;
}
if (existingSameFile) {
Get.snackbar('文件已存在', '跳过下载,直接更新');
- isUpdating.value = false;
- _installApp(savePath);
+
+ startUpgrading(savePath);
return;
}
@@ -239,7 +313,7 @@ class OtaUpgradeController extends GetxController {
}
} catch (e) {
Get.snackbar('错误', '删除旧文件失败: $e');
- isUpdating.value = false;
+ //isUpgrade.value = false;
return;
}
@@ -250,59 +324,80 @@ class OtaUpgradeController extends GetxController {
followRedirects: true,
maxRedirects: 5,
);
-
- DateTime lastUpdate = DateTime.now();
+ // 修改下载部分
await dio.download(
updatepackageaddress,
savePath,
options: options,
onReceiveProgress: (received, total) {
- final now = DateTime.now();
- if (now.difference(lastUpdate).inMilliseconds > 1000) {
- lastUpdate = now;
- String progressMsg = total != -1
- ? '${(received / total * 100).toStringAsFixed(1)}%'
- : '已下载:${(received / 1024 / 1024).toStringAsFixed(1)}MB';
- Get.snackbar(
- '升级进度',
- progressMsg,
- duration: const Duration(seconds: 1),
- );
+ if (total != -1) {
+ downloadProgress.value = received / total;
}
},
);
-
File file = File(savePath);
if (await file.exists()) {
Get.snackbar('下载完成', '文件已保存至: $savePath');
} else {
Get.snackbar('错误', '文件未找到,请检查写入权限');
}
- isUpdating.value = false;
- _installApp(savePath);
+ isDownloading.value = false;
+ startUpgrading(savePath);
+ return;
} on DioException catch (e) {
Get.snackbar('下载失败', '错误: ${e.message}');
- isUpdating.value = false;
} on FileSystemException catch (e) {
Get.snackbar('存储失败', '无法写入文件: ${e.message}');
- isUpdating.value = false;
} catch (e) {
Get.snackbar('未知错误', e.toString());
- isUpdating.value = false;
}
}
// 开始升级
- void _installApp(String path) async {
- jlota.searchDevices();
- jlota.startUpgrade();
- // JlOtaPlugin()
- // final File file = File(path);
- // if (await file.exists()) {
- // // OpenResult result = await OpenFile.open(path);
- // // if (result.type != ResultType.done) {
- // // Get.snackbar('错误', '安装失败: ${result.message}');
- // // }
- // }
+ void startUpgrading(String path) async {
+ jlota.startUpgrading(path);
+ return;
+ // isUpgrade.value = false;
+ }
+
+ // 停止升级
+ void stopUpgrading() async {
+ jlota.onOtaClose();
+ return;
+ // isUpgrade.value = false;
+ }
+
+ void _listenToEvents() {
+ Ota.instance.eventStream.listen((event) {
+ if (event.event == 'onProgress' &&
+ event.type != null &&
+ event.progress != null) {
+ progress.value = event.progress!;
+ type.value = event.type!;
+ isUpgrade.value = true; // 开始升级
+ }
+ if (event.event == 'onStartOTA') {
+ type.value = OtaEvent.START_UPGRADING;
+ isUpgrade.value = true; // 开始升级
+ }
+ if (event.event == 'onStopOTA') {
+ type.value = OtaEvent.UPGRADE_SUCCESS;
+ isUpgrade.value = false;
+ Get.snackbar('升级成功', '请退出重新连接设备');
+ _getCurrentVersion();
+ }
+ if (event.event == 'onError') {
+ type.value = OtaEvent.UPGRADE_FAILED;
+ isUpgrade.value = false;
+ }
+ if (event.event == 'NOCONNECTION') {
+ type.value = OtaEvent.CONNECTION_DISCONNECTION;
+ isUpgrade.value = false;
+ }
+ if (event.event == 'CONNECTION_SUCCESS') {
+ type.value = OtaEvent.CONNECTION_SUCCESS;
+ isUpgrade.value = true; // 开始升级
+ }
+ });
}
}
diff --git a/lib/modules/ota_upgrade/views/ota_upgrade_view.dart b/lib/modules/ota_upgrade/views/ota_upgrade_view.dart
index 4555f63c8..1bb1629bc 100644
--- a/lib/modules/ota_upgrade/views/ota_upgrade_view.dart
+++ b/lib/modules/ota_upgrade/views/ota_upgrade_view.dart
@@ -10,70 +10,125 @@ class OtaUpgradeView extends GetView {
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
- return Scaffold(
- backgroundColor: isDarkMode ? Colors.black : Colors.grey[100],
- appBar: AppBar(
- automaticallyImplyLeading: false,
- centerTitle: true,
- elevation: 0,
- backgroundColor: isDarkMode ? Colors.black : Colors.white,
- leading: IconButton(
- icon: Icon(
- Icons.arrow_back_ios,
- size: 20.sp,
- color: isDarkMode ? Colors.white : Colors.black,
+ return WillPopScope(
+ onWillPop: () async {
+ if (controller.isUpgrade.value) {
+ // 显示确认弹窗
+ await Get.dialog(
+ AlertDialog(
+ title: Text('提示'),
+ content: Text('当前正在升级,退出将中断升级进程,确定要退出吗?'),
+ actions: [
+ TextButton(
+ onPressed: () => Get.back(), // 关闭弹窗
+ child: Text('取消'),
+ ),
+ TextButton(
+ onPressed: () {
+ controller.stopUpgrading();
+ Get.back(); // 关闭弹窗
+ Get.back(); // 返回上一页
+ },
+ child: Text('确定'),
+ ),
+ ],
+ ),
+ );
+ return false; // 阻止默认返回
+ }
+ return true; // 允许返回
+ },
+ child: Scaffold(
+ backgroundColor: isDarkMode ? Colors.black : Colors.grey[100],
+ appBar: AppBar(
+ automaticallyImplyLeading: false,
+ centerTitle: true,
+ elevation: 0,
+ backgroundColor: isDarkMode ? Colors.black : Colors.white,
+ leading: IconButton(
+ icon: Icon(
+ Icons.arrow_back_ios,
+ size: 20.sp,
+ color: isDarkMode ? Colors.white : Colors.black,
+ ),
+ onPressed: () {
+ if (controller.isUpgrade.value) {
+ // 显示确认弹窗
+ Get.dialog(
+ AlertDialog(
+ title: Text('提示'),
+ content: Text('当前正在升级,退出将中断升级进程,确定要退出吗?'),
+ actions: [
+ TextButton(
+ onPressed: () => Get.back(), // 关闭弹窗
+ child: Text('取消'),
+ ),
+ TextButton(
+ onPressed: () {
+ controller.stopUpgrading();
+ Get.back(); // 关闭弹窗
+ Get.back(); // 返回上一页
+ },
+ child: Text('确定'),
+ ),
+ ],
+ ),
+ );
+ } else {
+ Get.back(); // 正常返回
+ }
+ },
),
- onPressed: () => Get.back(),
- ),
- title: Text(
- '固件升级'.tr,
- style: TextStyle(
- fontSize: 20.sp,
- color: isDarkMode ? Colors.white : Colors.black,
+ title: Text(
+ '固件升级'.tr,
+ style: TextStyle(
+ fontSize: 20.sp,
+ color: isDarkMode ? Colors.white : Colors.black,
+ ),
),
),
- ),
- body: SingleChildScrollView(
- child: Padding(
- padding: EdgeInsets.all(20),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- // 设备信息卡片
- Card(
- elevation: 2,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(16),
- ),
- color: isDarkMode ? Colors.grey[900] : Colors.white,
- child: Padding(
- padding: EdgeInsets.all(20),
- child: Column(
- children: [
- _buildDeviceIcon(isDarkMode),
- SizedBox(height: 20),
- _buildVersionInfo(isDarkMode),
- ],
+ body: SingleChildScrollView(
+ child: Padding(
+ padding: EdgeInsets.all(20),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ // 设备信息卡片
+ Card(
+ elevation: 2,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16),
+ ),
+ color: isDarkMode ? Colors.grey[900] : Colors.white,
+ child: Padding(
+ padding: EdgeInsets.all(20),
+ child: Column(
+ children: [
+ _buildDeviceIcon(isDarkMode),
+ SizedBox(height: 20),
+ _buildVersionInfo(isDarkMode),
+ ],
+ ),
),
),
- ),
- SizedBox(height: 20),
+ SizedBox(height: 20),
- // 更新说明
- _buildUpdateNotes(isDarkMode),
- SizedBox(height: 20),
+ // 更新说明
+ _buildUpdateNotes(isDarkMode),
+ SizedBox(height: 20),
- // 状态显示区域
- Obx(() {
- if (controller.isFetchingDeviceInfo.value) {
- return _buildLoadingIndicator(isDarkMode);
- }
- if (controller.errorMessage.value.isNotEmpty) {
- return _buildErrorInfo(isDarkMode);
- }
- return _buildUpdateButton(isDarkMode);
- }),
- ],
+ // 状态显示区域
+ Obx(() {
+ if (controller.isFetchingDeviceInfo.value) {
+ return _buildLoadingIndicator(isDarkMode);
+ }
+ if (controller.errorMessage.value.isNotEmpty) {
+ return _buildErrorInfo(isDarkMode);
+ }
+ return _buildUpdateButton(isDarkMode);
+ }),
+ ],
+ ),
),
),
),
@@ -101,7 +156,7 @@ class OtaUpgradeView extends GetView {
fontSize: 18,
color: isDarkMode ? Colors.grey[300] : Colors.black)),
SizedBox(width: 8), // 改为width
- Obx(() => Text('v${controller.currentVersion}',
+ Obx(() => Text('V ${controller.currentVersion}',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.normal,
@@ -117,11 +172,11 @@ class OtaUpgradeView extends GetView {
fontSize: 18,
color: isDarkMode ? Colors.grey[300] : Colors.black)),
SizedBox(width: 8), // 改为width
- Text('v${controller.latestVersion}',
+ Obx(() => Text('V ${controller.latestVersion}',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.normal,
- color: isDarkMode ? Colors.green[300] : Colors.green)),
+ color: isDarkMode ? Colors.blue[300] : Colors.blue))),
],
),
],
@@ -182,7 +237,7 @@ class OtaUpgradeView extends GetView {
isDarkMode ? Colors.blue[300] : Colors.blue),
),
SizedBox(height: 20),
- Text('正在连接设备...',
+ Text('正在获取设备信息...',
style:
TextStyle(color: isDarkMode ? Colors.grey[300] : Colors.grey))
],
@@ -227,7 +282,7 @@ class OtaUpgradeView extends GetView {
);
Widget _buildUpdateButton(bool isDarkMode) {
- return controller.isUpdating.value
+ return controller.isUpgrade.value
? _buildProgressIndicator(isDarkMode)
: Container(
width: double.infinity,
@@ -262,7 +317,7 @@ class OtaUpgradeView extends GetView {
if (controller.isUpdateAvailable.value) ...[
SizedBox(height: 8),
Text(
- '建议连接WiFi并保持充电状态'.tr,
+ '取出双耳机,并保证电量均为30%以上的状态'.tr,
style: TextStyle(
fontSize: 14,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
@@ -274,61 +329,67 @@ class OtaUpgradeView extends GetView {
);
}
- Widget _buildProgressIndicator(bool isDarkMode) => Container(
- padding: EdgeInsets.all(20),
- decoration: BoxDecoration(
- color:
- isDarkMode ? Colors.blue[900]!.withOpacity(0.2) : Colors.blue[50],
- borderRadius: BorderRadius.circular(16),
- ),
+ Widget _buildProgressIndicator(bool isDarkMode) {
+ return Obx(() {
+ final progress = controller.progress.value;
+ final type = controller.type.value;
+
+ // // 处理升级失败状态:弹出 snackbar 并显示错误提示
+ // if (type == OtaUpgradeController.UPGRADE_FAILED ||
+ // type == OtaUpgradeController.CONNECTION_DISCONNECTION) {
+ // // 使用延迟确保在 build 完成后执行
+ // Future.delayed(Duration.zero, () {
+ // Get.snackbar('升级失败', '重新连接设备后重试');
+ // });
+
+ // return Container(
+ // width: double.infinity,
+ // padding: const EdgeInsets.symmetric(vertical: 10),
+ // child: Text(
+ // '升级失败,请重新连接设备',
+ // style: TextStyle(
+ // fontSize: 18,
+ // fontWeight: FontWeight.bold,
+ // color: Colors.red[700],
+ // ),
+ // ),
+ // );
+ // }
+
+ // 正常升级状态
+ return Container(
+ width: double.infinity,
+ padding: const EdgeInsets.symmetric(vertical: 10),
child: Column(
children: [
- Text('正在下载更新...'.tr,
- style: TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.bold,
- color: isDarkMode ? Colors.blue[300] : Colors.blue[700])),
- SizedBox(height: 16),
- //SizedBox(
- //width: 240,
- Container(
- decoration: BoxDecoration(
- border: Border.all(
- color: isDarkMode ? Colors.blue[400]! : Colors.blue[300]!,
- width: 1.5,
- ),
- borderRadius: BorderRadius.circular(12),
+ Text(
+ controller.getStatusMessage(type),
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ color: isDarkMode ? Colors.blue[300] : Colors.blue[700],
),
- child: LinearProgressIndicator(
- value: controller.downloadProgress.value,
- backgroundColor:
- isDarkMode ? Colors.grey[800] : Colors.grey[200],
- valueColor: AlwaysStoppedAnimation(
- isDarkMode ? Colors.blue[300] : Colors.blue),
- minHeight: 12,
- borderRadius: BorderRadius.circular(10),
+ ),
+ const SizedBox(height: 16),
+ LinearProgressIndicator(
+ value: progress / 100.0, // 转换为 0.0-1.0
+ backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[200],
+ valueColor: AlwaysStoppedAnimation(
+ isDarkMode ? Colors.blue[300]! : Colors.blue,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ '${progress.toStringAsFixed(1)}%',
+ style: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ color: isDarkMode ? Colors.blue[300] : Colors.blue[700],
),
),
- SizedBox(height: 16),
- Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Icon(
- Icons.downloading,
- color: isDarkMode ? Colors.blue[300] : Colors.blue,
- size: 20,
- ),
- SizedBox(width: 8),
- Text(
- '${(controller.downloadProgress.value * 100).toStringAsFixed(1)}%',
- style: TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.bold,
- color:
- isDarkMode ? Colors.blue[300] : Colors.blue[700])),
- ],
- )
],
),
);
+ });
+ }
}
diff --git a/lib/modules/settings/controllers/settings_controller.dart b/lib/modules/settings/controllers/settings_controller.dart
index 984376632..aa8ac4d54 100644
--- a/lib/modules/settings/controllers/settings_controller.dart
+++ b/lib/modules/settings/controllers/settings_controller.dart
@@ -8,6 +8,7 @@ import '../../../data/services/speech_factory.dart';
import '../models/language_model.dart';
import 'package:logger/logger.dart';
import '../../../data/models/user_Info.dart';
+import '../../../data/services/ble_manager.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
@@ -402,4 +403,42 @@ class SettingsController extends GetxController {
sourceLanguage.value = targetLanguage.value;
targetLanguage.value = temp;
}
+
+ /// 清除BLE配对设备
+ Future clearBleDevices() async {
+ // 显示确认对话框
+ final confirmed = await Get.dialog(
+ AlertDialog(
+ title: const Text('清除配对设备'),
+ content: const Text('确定要清除所有BLE配对设备吗?此操作不可撤销。'),
+ actions: [
+ TextButton(
+ onPressed: () => Get.back(result: false),
+ child: Text('cancel'.tr),
+ ),
+ TextButton(
+ onPressed: () => Get.back(result: true),
+ style: TextButton.styleFrom(
+ foregroundColor: Colors.red,
+ ),
+ child: const Text('确定'),
+ ),
+ ],
+ ),
+ );
+
+ if (confirmed == true) {
+ try {
+ final bleManager = Get.find();
+ final result = await bleManager.clearPairedDevices();
+ if (result) {
+ Get.snackbar('成功', 'BLE配对设备已清除');
+ } else {
+ Get.snackbar('失败', '清除BLE配对设备失败');
+ }
+ } catch (e) {
+ Get.snackbar('错误', '清除BLE配对设备时发生错误');
+ }
+ }
+ }
}
diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart
index ce889eb10..9fe16c6dc 100644
--- a/lib/modules/settings/views/settings_view.dart
+++ b/lib/modules/settings/views/settings_view.dart
@@ -481,6 +481,30 @@ class SettingsView extends GetView {
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
+
+ // 清除配对设备
+ _buildSimpleNavigationSetting(
+ title: '清除配对设备',
+ subtitle: '清除所有BLE配对设备记录',
+ icon: Icons.bluetooth_disabled,
+ iconBgColor: isDarkMode
+ ? Colors.red[900]!.withOpacity(0.3)
+ : Colors.red[100]!,
+ iconColor:
+ isDarkMode ? Colors.red[300]! : Colors.red[600]!,
+ titleColor: isDarkMode ? Colors.white : null,
+ subtitleColor: isDarkMode ? Colors.white70 : null,
+ onTap: () {
+ controller.clearBleDevices();
+ },
+ isDarkMode: isDarkMode,
+ ),
+ Divider(
+ height: 1,
+ color: isDarkMode
+ ? Colors.white.withOpacity(0.1)
+ : Colors.grey[200]),
+
// 杰理测试
_buildSimpleNavigationSetting(
title: '杰理蓝牙测试',
diff --git a/local_plugins/agent_service/android/build.gradle.kts b/local_plugins/agent_service/android/build.gradle.kts
index 64a0b9518..1fd600fb7 100644
--- a/local_plugins/agent_service/android/build.gradle.kts
+++ b/local_plugins/agent_service/android/build.gradle.kts
@@ -48,5 +48,5 @@ dependencies {
add("compileOnly", project(":music_service"))
add("compileOnly", project(":amap_service"))
add("compileOnly", project(":speech"))
- add("compileOnly", project(":open_ai_service"))
+ add("compileOnly", project(":chat_api"))
}
\ No newline at end of file
diff --git a/local_plugins/agent_service/android/src/main/AndroidManifest.xml b/local_plugins/agent_service/android/src/main/AndroidManifest.xml
index ac04b7dca..b9e7e226b 100644
--- a/local_plugins/agent_service/android/src/main/AndroidManifest.xml
+++ b/local_plugins/agent_service/android/src/main/AndroidManifest.xml
@@ -13,7 +13,7 @@
android:name=".BleMediaService"
android:exported="true"
android:enabled="true"
- android:foregroundServiceType="mediaPlayback">
+ android:foregroundServiceType="connectedDevice">
diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
index 94eae0890..d9f358d34 100644
--- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
+++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
@@ -4,7 +4,8 @@ import android.content.Context
import org.json.JSONArray
import org.json.JSONObject
import com.yunqiinnovation.azure_speech.AzureAsrHelper
-import com.yunqiinnovation.open_ai_service.OpenAIService
+import com.yunqiinnovation.chat_api.ChatApiService
+import com.yunqiinnovation.chat_api.StreamCallback
import com.yunqiinnovation.chat_storage.ChatStorageHelper
import com.yunqiinnovation.music_service.MusicServiceStarter
import com.yunqiinnovation.amap_service.NavigationServiceHelper
@@ -53,8 +54,8 @@ object AgentService : CoroutineScope {
// 使用AzureTtsHelper作为唯一的TTS实现
private var ttsService: AzureTtsHelper? = null
- // OpenAI服务 - 修改为使用OpenAIService
- private lateinit var openAIService: OpenAIService
+ // ChatAPI服务 - 使用新的ChatApiService
+ private lateinit var chatApiService: ChatApiService
// 聊天存储服务
private lateinit var chatStorageHelper: ChatStorageHelper
@@ -121,7 +122,7 @@ object AgentService : CoroutineScope {
try {
// 初始化上下文和服务
this.context = appContext.applicationContext
- this.openAIService = OpenAIService(context)
+ this.chatApiService = ChatApiService(context)
this.chatStorageHelper = ChatStorageHelper(context)
this.audioPlayer = AudioPlayer(context)
@@ -146,8 +147,8 @@ object AgentService : CoroutineScope {
// 初始化TTS服务
initializeTtsEngine(config)
- // 初始化OpenAI服务
- openAIService.initialize(
+ // 初始化ChatAPI服务
+ chatApiService.initialize(
config["openaiApiKey"]?.toString() ?: "",
config["openaiBaseUrl"]?.toString() ?: "",
config["openaiModel"]?.toString() ?: "",
@@ -181,9 +182,9 @@ object AgentService : CoroutineScope {
azureAsrHelper?.dispose()
- // 释放OpenAI服务
- if (::openAIService.isInitialized) {
- openAIService.cancelAll()
+ // 释放ChatAPI服务
+ if (::chatApiService.isInitialized) {
+ chatApiService.cancelCurrentStream()
}
// 释放TTS服务
@@ -401,7 +402,7 @@ object AgentService : CoroutineScope {
"language" to detectedLanguage
))
- processWithOpenAI(result)
+ processWithChatApi(result)
}
// 重置状态,继续识别
@@ -513,8 +514,8 @@ object AgentService : CoroutineScope {
currentAiJob?.cancel()
currentAiJob = null
- // 通知OpenAI服务终止当前流式请求
- openAIService.cancelCurrentStream()
+ // 通知ChatAPI服务终止当前流式请求
+ chatApiService.cancelCurrentStream()
// 记录日志
Log.d(TAG, "AI流输出已停止")
@@ -543,43 +544,43 @@ object AgentService : CoroutineScope {
return false
}
- // 使用OpenAI处理文本
- processWithOpenAI(text, speakResponse)
+ // 使用ChatAPI处理文本
+ processWithChatApi(text, speakResponse)
return true
}
/**
- * 使用OpenAI处理语音识别结果
+ * 使用ChatAPI处理语音识别结果
*/
- private fun processWithOpenAI(text: String) {
- processWithOpenAI(text, true)
+ private fun processWithChatApi(text: String) {
+ processWithChatApi(text, true)
}
/**
- * 使用OpenAI处理文本消息
+ * 使用ChatAPI处理文本消息
*
* @param text 用户输入文本
* @param speakResponse 是否使用TTS朗读回复
*/
- private fun processWithOpenAI(
+ private fun processWithChatApi(
text: String,
speakResponse: Boolean = true
) {
Log.d(TAG, "用户问题: $text")
// 创建用户文本消息并处理
- val userMessage = openAIService.createUserMessage(text)
- processWithOpenAIInternal(userMessage, text, speakResponse)
+ val userMessage = createUserMessage(text)
+ processWithChatApiInternal(userMessage, text, speakResponse)
}
/**
- * 使用OpenAI处理图片
+ * 使用ChatAPI处理图片
*
* @param imageBase64 Base64编码的图片数据
* @param text 可选的文本描述或问题
* @param speakResponse 是否朗读回复
*/
- private fun processImageWithOpenAI(
+ private fun processImageWithChatApi(
imageBase64: String,
text: String = "",
speakResponse: Boolean = false
@@ -587,14 +588,14 @@ object AgentService : CoroutineScope {
Log.d(TAG, "处理图片输入: ${if (text.isEmpty()) "无附加文本" else "附带文本: $text"}")
// 创建带图片的用户消息并处理
- val userMessage = openAIService.createUserMessageWithImage(text, imageBase64)
+ val userMessage = createUserMessageWithImage(text, imageBase64)
// 图片描述用于存储
val displayText = text.ifEmpty { "[图片]" }
- processWithOpenAIInternal(userMessage, displayText, speakResponse, true)
+ processWithChatApiInternal(userMessage, displayText, speakResponse, true)
}
/**
- * 内部方法:通用的OpenAI处理逻辑
+ * 内部方法:通用的ChatAPI处理逻辑
*
* @param userMessage 用户消息(可以是文本或图片格式)
* @param displayText 用于显示和存储的文本
@@ -602,7 +603,7 @@ object AgentService : CoroutineScope {
* @param hasImage 是否包含图片
* @param imagePath 图片路径(如果有)
*/
- private fun processWithOpenAIInternal(
+ private fun processWithChatApiInternal(
userMessage: JSONObject,
displayText: String,
speakResponse: Boolean = true,
@@ -634,7 +635,7 @@ object AgentService : CoroutineScope {
// 先添加系统提示词
if (systemPrompt.isNotEmpty()) {
- messagesWithSystemPrompt.put(openAIService.createSystemMessage(systemPrompt))
+ messagesWithSystemPrompt.put(createSystemMessage(systemPrompt))
// Log.d(TAG, "系统提示词: ${systemPrompt}")
}
@@ -672,9 +673,9 @@ object AgentService : CoroutineScope {
addToHistoryMessages(userMessage)
}
- openAIService.sendMessageStream(
+ chatApiService.sendMessageStream(
messagesWithSystemPrompt,
- object : OpenAIService.StreamCallback {
+ object : StreamCallback {
override fun onToken(token: String) {
responseBuilder.append(token)
if (speakResponse && !nobroadcast) {
@@ -704,7 +705,7 @@ object AgentService : CoroutineScope {
sendEvent("assistant_response", responseData)
// 添加AI回复到历史记录
- addToHistoryMessages(openAIService.createAssistantMessage(response))
+ addToHistoryMessages(createAssistantMessage(response))
// 保存聊天记录
saveChatMessage(displayText, response,aiMetadata,userMetadata.toString())
@@ -796,9 +797,9 @@ object AgentService : CoroutineScope {
val content = message["message"] as String
if (sender == "user") {
- addToHistoryMessages(openAIService.createUserMessage(content))
+ addToHistoryMessages(createUserMessage(content))
} else if (sender == "assistant") {
- addToHistoryMessages(openAIService.createAssistantMessage(content))
+ addToHistoryMessages(createAssistantMessage(content))
}
}
@@ -945,6 +946,62 @@ object AgentService : CoroutineScope {
return result == android.content.pm.PackageManager.PERMISSION_GRANTED
}
+ /**
+ * 创建用户消息
+ */
+ private fun createUserMessage(content: String): JSONObject {
+ return JSONObject().apply {
+ put("role", "user")
+ put("content", content)
+ }
+ }
+
+ /**
+ * 创建系统消息
+ */
+ private fun createSystemMessage(content: String): JSONObject {
+ return JSONObject().apply {
+ put("role", "system")
+ put("content", content)
+ }
+ }
+
+ /**
+ * 创建助手消息
+ */
+ private fun createAssistantMessage(content: String): JSONObject {
+ return JSONObject().apply {
+ put("role", "assistant")
+ put("content", content)
+ }
+ }
+
+ /**
+ * 创建带图片的用户消息
+ */
+ private fun createUserMessageWithImage(text: String, imageBase64: String): JSONObject {
+ val content = JSONArray().apply {
+ if (text.isNotEmpty()) {
+ put(JSONObject().apply {
+ put("type", "text")
+ put("text", text)
+ })
+ }
+ put(JSONObject().apply {
+ put("type", "image_url")
+ put("image_url", JSONObject().apply {
+ put("url", "data:image/jpeg;base64,$imageBase64")
+ put("detail", "auto")
+ })
+ })
+ }
+
+ return JSONObject().apply {
+ put("role", "user")
+ put("content", content)
+ }
+ }
+
/**
* 处理图片输入
*/
@@ -971,7 +1028,7 @@ object AgentService : CoroutineScope {
launch {
try {
// 将图片转换为Base64格式
- val imageBase64 = openAIService.fileToBase64(imagePath) ?: run {
+ val imageBase64 = chatApiService.fileToBase64(imagePath) ?: run {
sendEvent("error", mapOf(
"code" to "IMAGE_CONVERSION_FAILED",
"message" to "图片转换失败"
@@ -980,9 +1037,9 @@ object AgentService : CoroutineScope {
}
// 创建带图片的用户消息并处理
- val userMessage = openAIService.createUserMessageWithImage(text, imageBase64)
- // 图片描述用于存储
- val displayText = text.ifEmpty { "[图片]" }
+ val userMessage = createUserMessageWithImage(text, imageBase64)
+ // 图片描述用于存储
+ val displayText = text.ifEmpty { "[图片]" }
// 通知图片准备完成
sendEvent("image_ready", mapOf(
@@ -991,7 +1048,7 @@ object AgentService : CoroutineScope {
))
// 处理消息并传入存储路径
- processWithOpenAIInternal(userMessage, displayText, speakResponse, true, imagePath)
+ processWithChatApiInternal(userMessage, displayText, speakResponse, true, imagePath)
} catch (e: Exception) {
Log.e(TAG, "处理图片失败: ${e.message}")
sendEvent("error", mapOf(
diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt
index 0a811f33a..e7452eb45 100644
--- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt
+++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt
@@ -5,46 +5,35 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
-import android.bluetooth.BluetoothManager
-import android.bluetooth.le.ScanFilter
-import android.bluetooth.le.ScanSettings
-import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
-import android.media.AudioManager
-import android.media.MediaPlayer
import android.os.Build
import android.os.IBinder
-import android.support.v4.media.session.MediaSessionCompat
-import android.support.v4.media.session.PlaybackStateCompat
import androidx.core.app.NotificationCompat
-import androidx.media.session.MediaButtonReceiver
-import com.yunqiinnovation.agent_service.R
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean
-import android.view.KeyEvent
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
import com.yunqiinnovation.ble_service.BleConst
import com.yunqiinnovation.ble_service.BleService
+
/**
- * 语音助手媒体服务
+ * BLE代理服务
* 职责:
- * 1. 接收并处理媒体按钮事件(播放/暂停等)
- * 2. 维持媒体会话,使系统可以通过媒体按钮唤醒应用
- * 3. 最小化实现,不直接对接Flutter层
+ * 1. 语音代理初始化和管理
+ * 2. BLE设备连接管理
+ * 3. 后台扫描结果处理
*/
class BleMediaService : Service(), CoroutineScope {
companion object {
- private const val TAG = "BleMediaService"
- private const val CHANNEL_ID = "ble_media_service_channel"
+ private const val TAG = "BleAgentService"
+ private const val CHANNEL_ID = "ble_agent_service_channel"
private const val NOTIFICATION_ID = 1002
private const val PREF_NAME = "deep_voice_config"
@@ -70,18 +59,10 @@ class BleMediaService : Service(), CoroutineScope {
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
-
- private lateinit var mediaSession: MediaSessionCompat
- private lateinit var audioManager: AudioManager
-
- // 媒体操作防抖
- private var lastActionTime = 0L
+ // BLE扫描防抖
private var lastScanTime = 0L
- private val ACTION_DEBOUNCE_MS = 800L // 媒体操作防抖时间(毫秒)
-
- // 添加音频播放器
- private var audioPlayer: MiniMediaPlayer? = null
+ private val SCAN_DEBOUNCE_MS = 800L // BLE扫描防抖时间(毫秒)
// 语音代理是否已初始化
private var isChatAgentInitialized = false
@@ -91,106 +72,85 @@ class BleMediaService : Service(), CoroutineScope {
override fun onCreate() {
super.onCreate()
- Log.i(TAG, "BleMediaService正在创建...")
+ Log.i(TAG, "BLE代理服务正在创建...")
// 设置服务状态
isRunning.set(true)
startForeground(NOTIFICATION_ID, createNotification())
- // 初始化音频管理器
- audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
-
- // 创建并启动媒体会话
- setupMediaSession()
- // 注册媒体按钮接收器
- // registerMediaButtonReceiver()
- // // 注册后台扫描
- // registerBackgroundScan()
-
- // 创建音频播放器,并播放静音文件
- audioPlayer = MiniMediaPlayer(this)
- audioPlayer?.play(R.raw.silence, 0f)
- setPlaybackState(PlaybackStateCompat.STATE_PAUSED)
-
- Log.i(TAG, "媒体服务已启动")
+ Log.i(TAG, "BLE代理服务已启动")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- Log.i(TAG, "BleMediaService.onStartCommand: ${intent?.action}")
+ Log.i(TAG, "BleAgentService.onStartCommand: ${intent?.action}")
// 保存当前Intent,用于后续初始化
currentIntent = intent
- // 初始化语音代理
+
+ // 初始化语音代理
if (!isChatAgentInitialized) {
initializeChatAgent()
}
- // 如果是媒体按钮事件,交给MediaButtonReceiver处理
- if (Intent.ACTION_MEDIA_BUTTON == intent?.action) {
- MediaButtonReceiver.handleIntent(mediaSession, intent)
- return START_STICKY
- }
// 处理蓝牙扫描结果事件
if (intent?.action == BleConst.ACTION_SCAN_RESULT) {
- val currentTime = System.currentTimeMillis()
- if (currentTime - lastScanTime >= ACTION_DEBOUNCE_MS) {
- lastScanTime = currentTime
- checkBleConnection()
- }
-
+ val currentTime = System.currentTimeMillis()
+ if (currentTime - lastScanTime >= SCAN_DEBOUNCE_MS) {
+ lastScanTime = currentTime
+ checkBleConnection()
+ }
return START_STICKY
}
-
-
return START_STICKY
}
-
/**
* 初始化语音交互代理
*/
private fun initializeChatAgent() {
- try {
- // 获取SharedPreferences
- val prefs = applicationContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
-
- // 尝试从Intent中获取参数
- val configFromIntent = extractConfigFromIntent(currentIntent)
-
- // 如果Intent中有参数,则保存到SharedPreferences
- if (configFromIntent.isNotEmpty()) {
- Log.d(TAG, "从Intent中获取到配置参数,保存到SharedPreferences")
- saveConfigToPreferences(configFromIntent, prefs)
- }
-
- // 合并Intent和SharedPreferences中的参数
- val config = getConfigFromPreferences(prefs)
- Log.d(TAG, "当前配置参数: $config")
- // 检查必要的配置是否存在
- if (config[KEY_AZURE_SPEECH_KEY].isNullOrEmpty() || config[KEY_OPENAI_API_KEY].isNullOrEmpty()) {
- Log.w(TAG, "语音代理初始化失败: API密钥未配置")
- return
- }
-
- if (isChatAgentInitialized) {
- Log.d(TAG, "ChatAgent已经初始化,跳过")
- return
- }
- if(config.isNotEmpty()) {
- // 初始化ChatAgent
- Log.i(TAG, "开始初始化ChatAgent")
- val success = BleAgent.initialize(applicationContext, config)
- if (success) {
- isChatAgentInitialized = true
- Log.i(TAG, "语音交互代理初始化成功")
- } else {
- Log.e(TAG, "语音交互代理初始化失败")
- }
+ try {
+ // 获取SharedPreferences
+ val prefs = applicationContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
+
+ // 尝试从Intent中获取参数
+ val configFromIntent = extractConfigFromIntent(currentIntent)
+
+ // 如果Intent中有参数,则保存到SharedPreferences
+ if (configFromIntent.isNotEmpty()) {
+ Log.d(TAG, "从Intent中获取到配置参数,保存到SharedPreferences")
+ saveConfigToPreferences(configFromIntent, prefs)
+ }
+
+ // 合并Intent和SharedPreferences中的参数
+ val config = getConfigFromPreferences(prefs)
+ Log.d(TAG, "当前配置参数: $config")
+
+ // 检查必要的配置是否存在
+ if (config[KEY_AZURE_SPEECH_KEY].isNullOrEmpty() || config[KEY_OPENAI_API_KEY].isNullOrEmpty()) {
+ Log.w(TAG, "语音代理初始化失败: API密钥未配置")
+ return
+ }
+
+ if (isChatAgentInitialized) {
+ Log.d(TAG, "ChatAgent已经初始化,跳过")
+ return
+ }
+
+ if(config.isNotEmpty()) {
+ // 初始化ChatAgent
+ Log.i(TAG, "开始初始化ChatAgent")
+ val success = BleAgent.initialize(applicationContext, config)
+ if (success) {
+ isChatAgentInitialized = true
+ Log.i(TAG, "语音交互代理初始化成功")
+ } else {
+ Log.e(TAG, "语音交互代理初始化失败")
}
- } catch (e: Exception) {
- Log.e(TAG, "初始化语音交互代理时发生异常", e)
}
+ } catch (e: Exception) {
+ Log.e(TAG, "初始化语音交互代理时发生异常", e)
+ }
}
/**
@@ -255,23 +215,7 @@ class BleMediaService : Service(), CoroutineScope {
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
- Log.i(TAG, "BleMediaService正在销毁...")
-
- // 取消注册媒体按钮接收器
- try {
- val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name)
- audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver)
- Log.i(TAG, "媒体按钮接收器已取消注册")
- } catch (e: Exception) {
- Log.e(TAG, "取消注册媒体按钮接收器失败: ${e.message}", e)
- }
-
- // 释放媒体会话
- mediaSession.release()
-
- // 停止并释放音频播放器
- audioPlayer?.release()
- audioPlayer = null
+ Log.i(TAG, "BLE代理服务正在销毁...")
// 取消所有协程
cancel()
@@ -281,62 +225,12 @@ class BleMediaService : Service(), CoroutineScope {
super.onDestroy()
}
-
- /**
- * 设置媒体会话,处理媒体按钮事件
- */
- private fun setupMediaSession() {
- mediaSession = MediaSessionCompat(this, "BleMediaSession").apply {
- setFlags(
- MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or
- MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
- )
- // 设置回调,处理媒体按钮事件
- setCallback(object : MediaSessionCompat.Callback() {
-
- override fun onMediaButtonEvent(mediaButtonIntent: Intent): Boolean {
-
- // 使用系统默认的媒体按钮处理
- return super.onMediaButtonEvent(mediaButtonIntent)
- }
- })
-
- // 设置初始播放状态
- val stateBuilder = PlaybackStateCompat.Builder()
- .setActions(
- PlaybackStateCompat.ACTION_PLAY or
- PlaybackStateCompat.ACTION_PAUSE or
- PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
- PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
- )
- .setState(PlaybackStateCompat.STATE_STOPPED, 0, 1.0f)
- setPlaybackState(stateBuilder.build())
-
- // 激活会话,使其可以接收媒体按钮事件
- isActive = true
- }
- }
-
- /**
- * 注册媒体按钮接收器
- */
- private fun registerMediaButtonReceiver() {
- try {
- val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name)
- audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver)
- Log.i(TAG, "媒体按钮接收器注册成功: $mediaButtonReceiver")
- } catch (e: Exception) {
- Log.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e)
- }
- }
-
private fun checkBleConnection() {
-
Log.i(TAG, "checkBleConnection收到连接请求")
try {
- // 示例:使用协程异步处理连接
+ // 使用协程异步处理连接
launch {
if(!BleService.isConnected()) {
BleService.connect()
@@ -347,44 +241,6 @@ class BleMediaService : Service(), CoroutineScope {
}
}
- /**
- * 更新通知内容
- */
- private fun updateNotification() {
- try {
- val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- notificationManager.notify(NOTIFICATION_ID, createNotification())
- } catch (e: Exception) {
- Log.e(TAG, "更新通知异常: ${e.message}")
- }
- }
-
- /**
- * 设置播放状态
- */
- private fun setPlaybackState(state: Int) {
- val playbackStateBuilder = PlaybackStateCompat.Builder()
- .setActions(
- PlaybackStateCompat.ACTION_PLAY or
- PlaybackStateCompat.ACTION_PAUSE or
- PlaybackStateCompat.ACTION_PLAY_PAUSE or
- PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
- PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
- PlaybackStateCompat.ACTION_STOP or
- PlaybackStateCompat.ACTION_SEEK_TO or
- PlaybackStateCompat.ACTION_FAST_FORWARD or
- PlaybackStateCompat.ACTION_REWIND
- )
- // 使用更高的播放速度,让系统认为是真正的播放
- .setState(state, 0, 1.0f, System.currentTimeMillis())
- mediaSession.setPlaybackState(playbackStateBuilder.build())
-
- // 确保MediaSession激活
- if (!mediaSession.isActive) {
- mediaSession.isActive = true
- }
- }
-
/**
* 创建前台服务通知
*/
@@ -395,7 +251,7 @@ class BleMediaService : Service(), CoroutineScope {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
- "语音助手媒体服务",
+ "语音助手代理服务",
NotificationManager.IMPORTANCE_LOW
)
manager.createNotificationChannel(channel)
@@ -419,77 +275,12 @@ class BleMediaService : Service(), CoroutineScope {
// 构建通知
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("DeepSound")
- // .setContentText(if (isPlaying) "正在播放" else "已暂停")
- .setSmallIcon(android.R.drawable.ic_media_play)
+ .setContentText("语音助手代理服务运行中")
+ .setSmallIcon(android.R.drawable.ic_dialog_info)
// 设置点击意图
pendingIntent?.let { builder.setContentIntent(it) }
return builder.build()
}
-
- /**
- * 简易媒体播放器,用于播放静音资源
- */
- inner class MiniMediaPlayer(private val context: Context) {
- private var mediaPlayer: MediaPlayer? = null
-
- /**
- * 播放资源文件
- * @param resId 资源ID
- * @param volume 音量 (0.0-1.0)
- */
- fun play(resId: Int, volume: Float = 0f) {
- try {
- // 释放之前的资源
- release()
-
- // 创建播放器并设置资源
- mediaPlayer = MediaPlayer().apply {
- // 设置资源
- context.resources.openRawResourceFd(resId)?.use { fd ->
- setDataSource(fd.fileDescriptor, fd.startOffset, fd.length)
- }
-
- // 设置音量
- setVolume(volume, volume)
-
- // // 播放完成后自动循环播放,保持媒体会话活跃
- // setOnCompletionListener {
- // Log.i(TAG, "静音资源播放完成,再次播放保持会话活跃")
- // play(resId, volume)
- // }
-
- // 准备并播放
- prepare()
- start()
- }
-
- Log.i(TAG, "开始播放静音资源")
- } catch (e: Exception) {
- Log.e(TAG, "播放静音资源异常: ${e.message}", e)
- release()
- }
- }
-
- /**
- * 释放资源
- */
- fun release() {
- try {
- mediaPlayer?.apply {
- if (isPlaying) stop()
- release()
- }
- mediaPlayer = null
- } catch (e: Exception) {
- // 忽略异常
- }
- }
-
- /**
- * 是否正在播放
- */
- fun isPlaying() = mediaPlayer?.isPlaying == true
- }
}
\ No newline at end of file
diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
index b3810ea94..c000e1faa 100644
--- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
+++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
@@ -30,7 +30,6 @@ 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 com.yunqiinnovation.ble_service.BleCompanionManager
-import com.yunqiinnovation.ble_service.OTAManager
import com.jieli.jl_bt_ota.model.BluetoothOTAConfigure;
import com.jieli.jl_bt_ota.constant.ErrorCode;
import com.jieli.jl_bt_ota.constant.StateCode;
@@ -89,7 +88,8 @@ object BleService {
private lateinit var scanHandler: Handler
private val scanResults = ArrayList()
private var isScanning = false
-
+// 是否是ota模式
+ var isEnterOta = false
// 连接状态
val connectionState = MutableLiveData(BleConst.STATE_DISCONNECTED)
@@ -354,10 +354,14 @@ object BleService {
return
}
- Log.i(TAG, "检测到已配对设备,MAC: $pairedMac,尝试连接")
-
+
+ if(!isEnterOta)
+ {
+ Log.i(TAG, "检测到已配对设备,MAC: $pairedMac,尝试连接")
// 调用现有的 connect 方法
connect(pairedMac)
+ }
+
} catch (e: Exception) {
Log.e(TAG, "自动连接已配对设备失败: ${e.message}", e)
updateConnectionState(BleConst.STATE_ERROR)
@@ -428,44 +432,6 @@ object BleService {
}
}
- // fun startOTA() {
- // try {
- // otaManager?.startOTA(CustomUpdateCallback(context, null))
- // Log.i(TAG, "OTA 升级已启动")
- // } catch (e: IllegalStateException) {
- // Log.e(TAG, "OTA 启动失败: ${e.message}")
- // disconnectGatt()
- // }
- // }
-
- // // 配置OTA
- // fun configureOTA() {
- // try {
- // val firmwarePath =
- // context.getExternalFilesDir("downloads/ota_upgrades")?.path + "/1.ufw"
- // val file = File(firmwarePath)
- // if (!file.exists()) {
- // // throw IOException("Firmware file not found")
- // Log.i(TAG, "Firmware file not found")
- // }
-
- // val bluetoothOption = BluetoothOTAConfigure().apply {
- // priority = BluetoothOTAConfigure.PREFER_BLE
- // isUseReconnect = true
- // isUseAuthDevice = true
- // mtu = 512
- // isNeedChangeMtu = false // 确保MTU请求生效
- // isUseJLServer = false
- // firmwareFilePath = firmwarePath
- // }
- // otaManager?.configure(bluetoothOption) ?: run {
- // Log.e(TAG, "OTA Manager未初始化")
- // }
- // } catch (e: Exception) {
- // Log.e(TAG, "OTA配置失败: ${e.message}")
- // // 处理错误
- // }
- // }
/**
* 更新连接状态并通知回调
diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OTAManager.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OTAManager.kt
deleted file mode 100644
index b28149983..000000000
--- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OTAManager.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-package com.yunqiinnovation.ble_service
-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 android.bluetooth.BluetoothDevice
-import android.bluetooth.BluetoothGatt
-import android.bluetooth.BluetoothProfile
-import android.content.Context
-import com.jieli.jl_bt_ota.constant.ErrorCode;
-import com.jieli.jl_bt_ota.constant.StateCode;
-import com.jieli.jl_bt_ota.impl.BluetoothOTAManager;
-import com.jieli.jl_bt_ota.interfaces.BtEventCallback;
-import com.jieli.jl_bt_ota.interfaces.IActionCallback;
-import com.jieli.jl_bt_ota.interfaces.IUpgradeCallback;
-import com.jieli.jl_bt_ota.model.BluetoothOTAConfigure;
-import com.jieli.jl_bt_ota.model.base.BaseError;
-import com.jieli.jl_bt_ota.model.response.TargetInfoResponse;
-import android.util.Log
-import java.util.*
- class CustomUpdateCallback(
- val context: Context,
- val device: BluetoothDevice?
-) : IUpgradeCallback {
- companion object {
- private const val TAG = "JlOtaPlugin"
- private const val CHANNEL_NAME = "com.yunqiinnovation.jl_ota"
- private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_ota/events"
-
- }
-
- override fun onStartOTA() {
- Log.d(TAG, "onStartOTA开始升级")
- // viewModel.otaStateMLD.value = OTAStart(device)
- }
-
- override fun onNeedReconnect(addr: String?, isNewReconnectWay: Boolean) {
- Log.d(TAG, "onNeedReconnect需要重新链接")
-
- }
-
- override fun onProgress(type: Int, progress: Float) {
- Log.d(TAG, "onProgress正在进展")
- //viewModel.otaStateMLD.value = OTAWorking(device, type, progress)
- }
-
- override fun onStopOTA() {
- Log.d(TAG, "onStartOTA停止升级")
-
- }
-
- override fun onCancelOTA() {
- Log.d(TAG, "onCancelOTA取消升级")
-
- }
-
- override fun onError(error: BaseError?) {
- val errorMessage = error?.message ?: "Unknown error"
- Log.e(TAG, "升级失败: ${error?.message ?: "未知错误"}")
-
- // 如果需要打印完整错误对象信息(假设BaseError有重写toString())
- Log.d(TAG, "完整错误对象: ${error ?: "null"}")
- }
-}
-
-class OTAManager(context: Context) : BluetoothOTAManager(context) {
-
-
- // 定义回调接口
- interface DataCallback {
- fun onDataReceived(device: BluetoothDevice?, data: ByteArray?)
- }
- // 保存回调的实例
- private var dataCallback: DataCallback? = null
-
- // 设置回调的方法
- fun setDataCallback(callback: DataCallback) {
- this.dataCallback = callback
- }
- override fun getConnectedDevice(): BluetoothDevice? {
- // 这里可以添加空实现的注释说明
- Log.i(TAG, "ota数据发送getConnectedDevice")
- return null
- }
-
- override fun getConnectedBluetoothGatt(): BluetoothGatt? {
- // 这里可以添加空实现的注释说明
- Log.i(TAG, "ota数据发送getConnectedBluetoothGatt")
- return null
- }
-
- override fun connectBluetoothDevice(bluetoothDevice: BluetoothDevice?) {
- // 这里可以添加空实现的注释说明
- Log.i(TAG, "ota数据发送connectBluetoothDevice")
- }
-
- override fun disconnectBluetoothDevice(bluetoothDevice: BluetoothDevice?) {
- // 这里可以添加空实现的注释说明
- Log.i(TAG, "ota数据发送disconnectBluetoothDevice")
- }
-
- /** 数据发送方法(协议无关) */
- override fun sendDataToDevice(
- bluetoothDevice: BluetoothDevice?,
- bytes: ByteArray?
- ): Boolean {
- Log.i(TAG, "ota数据发送${bluetoothDevice},${bytes?.size}")
- // 触发回调,将数据回传给调用者
- dataCallback?.onDataReceived(bluetoothDevice, bytes)
- // context.onReceiveDeviceData(device, data);
- return true
- }
-}
-
diff --git a/local_plugins/chat_api/README.md b/local_plugins/chat_api/README.md
index be47e00d0..463e1c3d6 100644
--- a/local_plugins/chat_api/README.md
+++ b/local_plugins/chat_api/README.md
@@ -15,6 +15,7 @@
## 平台支持
- iOS 16.0+
+- Android (API 21+)
## MCP 功能
@@ -138,6 +139,7 @@ chatApi.eventStream.listen((event) {
## 架构
+### iOS
```
ChatApiService (Flutter层)
↓
@@ -150,3 +152,14 @@ ChatApiService (iOS原生服务)
└── SystemFunctionHandler (内置系统功能)
```
+### Android
+```
+ChatApiService (Flutter层)
+ ↓
+ChatApiPlugin (Android插件桥接)
+ ↓
+ChatApiService (Android原生服务)
+ ├── OpenAI客户端 (aallam/openai-kotlin 4.0.1)
+ └── MCP功能 (暂时留空,接口预留)
+```
+
diff --git a/local_plugins/chat_api/android/README.md b/local_plugins/chat_api/android/README.md
new file mode 100644
index 000000000..99c528b5f
--- /dev/null
+++ b/local_plugins/chat_api/android/README.md
@@ -0,0 +1,70 @@
+# ChatAPI Android 实现
+
+基于 [openai-kotlin 4.0.1](https://github.com/aallam/openai-kotlin/tree/4.0.1) 的 ChatAPI 插件 Android 实现。
+
+## 特性
+
+- ✅ 基于 openai-kotlin 4.0.1 的 OpenAI API 客户端
+- ✅ 与 iOS 版本接口完全一致
+- ✅ 支持流式和非流式对话
+- ✅ 支持多模态内容(文本和图像识别)
+- ✅ 工具调用架构预留(MCP 功能暂时留空)
+- ✅ 协程支持和异步处理
+- ✅ 完整的错误处理和取消机制
+
+## 依赖
+
+- openai-kotlin 4.0.1
+- ktor-client-okhttp 2.3.2
+- kotlinx-coroutines
+- gson
+
+## 主要文件
+
+- `ChatApiPlugin.kt` - Flutter 插件桥接层
+- `ChatApiService.kt` - 核心 ChatAPI 服务实现
+- `StreamCallback.kt` - 流式回调接口
+
+## 与 iOS 版本的一致性
+
+Android 实现完全遵循 iOS 版本的接口定义:
+
+1. **初始化方法**: `initialize(apiKey, baseUrl, model, mcpServer)`
+2. **消息创建**: `createUserMessage()`, `createAssistantMessage()`
+3. **消息发送**: `sendMessage()` (非流式), `sendMessageStream()` (流式)
+4. **函数注册**: `registerFunction()`
+5. **MCP 相关**: 接口保留,功能暂时留空
+6. **流式事件**: 完全一致的事件类型和回调
+
+## MCP 功能状态
+
+MCP (Model Context Protocol) 相关功能在 Android 版本中暂时留空,包括:
+
+- `initializeMcpClient()` - 返回 false
+- `isMcpInitialized()` - 返回 false
+- `handleMcpToolCall()` - 返回占位符消息
+- 工具调用会通知上层但不会实际执行
+
+## 使用示例
+
+```kotlin
+// 初始化
+chatApiService.initialize(
+ apiKey = "your-api-key",
+ baseUrl = "https://api.openai.com/v1/",
+ model = "gpt-3.5-turbo",
+ mcpServer = ""
+)
+
+// 流式对话
+chatApiService.sendMessageStream(listOf(
+ mapOf("role" to "user", "content" to "Hello!")
+))
+```
+
+## 注意事项
+
+1. 需要网络权限 `INTERNET` 和 `ACCESS_NETWORK_STATE`
+2. 所有网络请求在后台线程执行
+3. 流式回调在主线程触发
+4. 支持取消正在进行的请求
\ No newline at end of file
diff --git a/local_plugins/chat_api/android/build.gradle.kts b/local_plugins/chat_api/android/build.gradle.kts
new file mode 100644
index 000000000..1eb929ecf
--- /dev/null
+++ b/local_plugins/chat_api/android/build.gradle.kts
@@ -0,0 +1,50 @@
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+ kotlin("plugin.serialization") version "1.9.24"
+}
+
+android {
+ namespace = "com.yunqiinnovation.chat_api"
+ compileSdk = 35
+
+ defaultConfig {
+ minSdk = 21
+ targetSdk = 33
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+
+ 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.kotlinx:kotlinx-coroutines-android:1.6.4")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
+
+ // OpenAI Kotlin 4.0.1
+ implementation("com.aallam.openai:openai-client:4.0.1")
+ implementation("io.ktor:ktor-client-okhttp:2.3.2")
+
+ // JSON处理
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
+ implementation("com.google.code.gson:gson:2.10.1")
+}
\ No newline at end of file
diff --git a/local_plugins/chat_api/android/settings.gradle.kts b/local_plugins/chat_api/android/settings.gradle.kts
new file mode 100644
index 000000000..c953e09ff
--- /dev/null
+++ b/local_plugins/chat_api/android/settings.gradle.kts
@@ -0,0 +1 @@
+rootProject.name = "chat_api"
\ No newline at end of file
diff --git a/local_plugins/chat_api/android/src/main/AndroidManifest.xml b/local_plugins/chat_api/android/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..11db23350
--- /dev/null
+++ b/local_plugins/chat_api/android/src/main/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt
new file mode 100644
index 000000000..bc7801951
--- /dev/null
+++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiPlugin.kt
@@ -0,0 +1,252 @@
+package com.yunqiinnovation.chat_api
+
+import android.os.Handler
+import android.os.Looper
+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 kotlinx.coroutines.*
+
+/**
+ * ChatApiPlugin
+ *
+ * 基于 openai-kotlin 4.0.1 的 ChatAPI 插件 Android 实现
+ * 与 iOS 版本接口完全一致
+ */
+class ChatApiPlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler {
+ private lateinit var methodChannel: MethodChannel
+ private lateinit var eventChannel: EventChannel
+ private var eventSink: EventChannel.EventSink? = null
+
+ private val chatApiService = ChatApiService()
+ private val mainHandler = Handler(Looper.getMainLooper())
+
+ // 协程作用域
+ private val pluginScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
+
+ override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
+ methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.chat_api/methods")
+ methodChannel.setMethodCallHandler(this)
+
+ eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.chat_api/events")
+ eventChannel.setStreamHandler(this)
+
+ // 设置流式回调
+ chatApiService.setStreamCallback(object : com.yunqiinnovation.chat_api.StreamCallback {
+ override fun onToken(token: String) {
+ sendEvent("token", token)
+ }
+
+ override fun onComplete() {
+ sendEvent("complete", null)
+ }
+
+ override fun onError(error: Exception) {
+ sendEvent("error", error.message ?: "未知错误")
+ }
+
+ override fun onFunctionCall(functionCall: org.json.JSONObject) {
+ try {
+ val jsonString = functionCall.toString()
+ sendEvent("functionCall", jsonString)
+ } catch (e: Exception) {
+ sendEvent("error", "Failed to serialize function call: ${e.message}")
+ }
+ }
+
+ override fun onFunctionCallResult(functionCall: org.json.JSONObject, functionCallResult: org.json.JSONObject) {
+ try {
+ val meta = org.json.JSONObject().apply {
+ put("functionCall", functionCall)
+ put("functionCallResult", functionCallResult)
+ }
+ val metaString = meta.toString()
+ val context = functionCallResult.optString("context", "")
+
+ sendEvent("functionCall", context, metaString)
+ } catch (e: Exception) {
+ sendEvent("error", "Failed to serialize function call result: ${e.message}")
+ }
+ }
+ })
+ }
+
+ override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
+ methodChannel.setMethodCallHandler(null)
+ eventChannel.setStreamHandler(null)
+ pluginScope.cancel()
+ }
+
+ override fun onMethodCall(call: MethodCall, result: Result) {
+ when (call.method) {
+ "initialize" -> handleInitialize(call, result)
+ "createUserMessage" -> handleCreateUserMessage(call, result)
+ "createAssistantMessage" -> handleCreateAssistantMessage(call, result)
+ "sendMessage" -> handleSendMessage(call, result)
+ "sendMessageStream" -> handleSendMessageStream(call, result)
+ "sendFunctionCallResult" -> handleSendFunctionCallResult(call, result)
+ "cancelCurrentStream" -> handleCancelCurrentStream(result)
+ "registerFunction" -> handleRegisterFunction(call, result)
+ "initializeMcpClient" -> handleInitializeMcpClient(call, result)
+ "isMcpInitialized" -> handleIsMcpInitialized(result)
+ "closeMcpClient" -> handleCloseMcpClient(result)
+ "handleMcpToolCall" -> handleMcpToolCall(call, result)
+ else -> result.notImplemented()
+ }
+ }
+
+ // MARK: - Method Handlers
+
+ private fun handleInitialize(call: MethodCall, result: Result) {
+ val apiKey = call.argument("apiKey")
+ if (apiKey.isNullOrEmpty()) {
+ result.error("INVALID_ARGS", "缺少必要参数", null)
+ return
+ }
+
+ val baseUrl = call.argument("baseUrl") ?: ""
+ val model = call.argument("model") ?: ""
+ val mcpServer = call.argument("mcpServer") ?: ""
+
+ val success = chatApiService.initialize(apiKey, baseUrl, model, mcpServer)
+ result.success(success)
+ }
+
+ private fun handleCreateUserMessage(call: MethodCall, result: Result) {
+ val content = call.argument("content")
+ if (content.isNullOrEmpty()) {
+ result.error("INVALID_ARGS", "缺少必要参数", null)
+ return
+ }
+
+ val message = chatApiService.createUserMessage(content)
+ result.success(message)
+ }
+
+ private fun handleCreateAssistantMessage(call: MethodCall, result: Result) {
+ val content = call.argument("content")
+ if (content.isNullOrEmpty()) {
+ result.error("INVALID_ARGS", "缺少必要参数", null)
+ return
+ }
+
+ val message = chatApiService.createAssistantMessage(content)
+ result.success(message)
+ }
+
+ private fun handleSendMessage(call: MethodCall, result: Result) {
+ @Suppress("UNCHECKED_CAST")
+ val messages = call.argument>>("messages")
+ if (messages == null) {
+ result.error("INVALID_ARGS", "缺少必要参数", null)
+ return
+ }
+
+ pluginScope.launch {
+ try {
+ val response = chatApiService.sendMessage(messages)
+ mainHandler.post {
+ result.success(response)
+ }
+ } catch (e: Exception) {
+ mainHandler.post {
+ result.error("SEND_ERROR", e.message, null)
+ }
+ }
+ }
+ }
+
+ private fun handleSendMessageStream(call: MethodCall, result: Result) {
+ @Suppress("UNCHECKED_CAST")
+ val messages = call.argument>>("messages")
+ if (messages == null) {
+ result.error("INVALID_ARGS", "缺少必要参数", null)
+ return
+ }
+
+ chatApiService.sendMessageStream(messages)
+ result.success(true)
+ }
+
+ private fun handleSendFunctionCallResult(call: MethodCall, result: Result) {
+ result.error("DEPRECATED", "sendFunctionCallResult已废弃,工具调用结果现在自动处理", null)
+ }
+
+ private fun handleCancelCurrentStream(result: Result) {
+ val success = chatApiService.cancelCurrentStream()
+ result.success(success)
+ }
+
+ private fun handleRegisterFunction(call: MethodCall, result: Result) {
+ val name = call.argument("name")
+ val description = call.argument("description")
+ @Suppress("UNCHECKED_CAST")
+ val parameters = call.argument