Browse Source

add

newdev_shunjiawei
wolfplus 2 years ago
parent
commit
c9ca2b4b38
  1. 12
      .mcp.json
  2. 6
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt
  3. 5
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionHandler.kt
  4. 6
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionService.kt
  5. 2
      android/build.gradle.kts
  6. 24
      lib/data/services/open_ai_service_adapter.dart
  7. 32
      lib/data/services/voice_interaction_service.dart
  8. 105
      lib/modules/jieli_test/controllers/jieli_test_controller.dart
  9. 74
      lib/modules/jieli_test/views/jieli_test_view.dart
  10. 269
      local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt
  11. 25
      local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/JieliPlugin.kt
  12. 84
      local_plugins/jieli/lib/jieli.dart
  13. 5
      local_plugins/open_ai_service/android/build.gradle.kts
  14. 747
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt
  15. 10
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt
  16. 3
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt
  17. 357
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt
  18. 3
      local_plugins/open_ai_service/lib/open_ai_service.dart
  19. 1
      pubspec.yaml
  20. 14
      test.json

12
.mcp.json

@ -0,0 +1,12 @@
{
"mcpServers": {
"amap-amap-sse": {
"url": "https://mcp.amap.com/sse?key=e5fdc9605eabdeb5626f18f5721f343d"
},
"web-search": {
"url": "http://mcp.ideapsound.com:8000/sse"
}
}
}

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

@ -172,6 +172,7 @@ class MainActivity: FlutterActivity() {
var openaiModel: String? = null var openaiModel: String? = null
var volcanoSpeechAppId: String = "" var volcanoSpeechAppId: String = ""
var volcanoSpeechAppToken: String = "" var volcanoSpeechAppToken: String = ""
var mcpServer: String = ""
// 安全存储相关常量 // 安全存储相关常量
private const val SECURE_PREFS_FILENAME = "deep_voice_secure_prefs" private const val SECURE_PREFS_FILENAME = "deep_voice_secure_prefs"
@ -184,6 +185,7 @@ class MainActivity: FlutterActivity() {
private const val KEY_MCP_SERVER_ENDPOINT = "mcp_server_endpoint" private const val KEY_MCP_SERVER_ENDPOINT = "mcp_server_endpoint"
private const val KEY_VOLCANO_SPEECH_APP_ID = "volcano_speech_app_id" private const val KEY_VOLCANO_SPEECH_APP_ID = "volcano_speech_app_id"
private const val KEY_VOLCANO_SPEECH_APP_TOKEN = "volcano_speech_app_token" private const val KEY_VOLCANO_SPEECH_APP_TOKEN = "volcano_speech_app_token"
private const val KEY_MCP_SERVER = "mcp_server"
// 会话管理 // 会话管理
private const val KEY_SESSION_ID = "session_id" private const val KEY_SESSION_ID = "session_id"
@ -224,6 +226,7 @@ class MainActivity: FlutterActivity() {
.putString(KEY_OPENAI_MODEL, openaiModel) .putString(KEY_OPENAI_MODEL, openaiModel)
.putString(KEY_VOLCANO_SPEECH_APP_ID, volcanoSpeechAppId) .putString(KEY_VOLCANO_SPEECH_APP_ID, volcanoSpeechAppId)
.putString(KEY_VOLCANO_SPEECH_APP_TOKEN, volcanoSpeechAppToken) .putString(KEY_VOLCANO_SPEECH_APP_TOKEN, volcanoSpeechAppToken)
.putString(KEY_MCP_SERVER, mcpServer)
.apply() .apply()
FileLogger.d("MainActivity", "密钥已安全保存到加密存储中") FileLogger.d("MainActivity", "密钥已安全保存到加密存储中")
@ -272,6 +275,7 @@ class MainActivity: FlutterActivity() {
openaiModel = sharedPreferences.getString(KEY_OPENAI_MODEL, null) openaiModel = sharedPreferences.getString(KEY_OPENAI_MODEL, null)
volcanoSpeechAppId = sharedPreferences.getString(KEY_VOLCANO_SPEECH_APP_ID, "") ?: "" volcanoSpeechAppId = sharedPreferences.getString(KEY_VOLCANO_SPEECH_APP_ID, "") ?: ""
volcanoSpeechAppToken = sharedPreferences.getString(KEY_VOLCANO_SPEECH_APP_TOKEN, "") ?: "" volcanoSpeechAppToken = sharedPreferences.getString(KEY_VOLCANO_SPEECH_APP_TOKEN, "") ?: ""
mcpServer = sharedPreferences.getString(KEY_MCP_SERVER, "") ?: ""
FileLogger.d("MainActivity", "已从加密存储加载密钥") FileLogger.d("MainActivity", "已从加密存储加载密钥")
// 检查是否成功获取所有必要密钥 // 检查是否成功获取所有必要密钥
@ -520,7 +524,7 @@ class MainActivity: FlutterActivity() {
openaiModel = call.argument<String>("openai_model") openaiModel = call.argument<String>("openai_model")
volcanoSpeechAppId = call.argument<String>("volcano_speech_app_id") ?: "" volcanoSpeechAppId = call.argument<String>("volcano_speech_app_id") ?: ""
volcanoSpeechAppToken = call.argument<String>("volcano_speech_app_token") ?: "" volcanoSpeechAppToken = call.argument<String>("volcano_speech_app_token") ?: ""
mcpServer = call.argument<String>("mcp_config") ?: ""
// 保存密钥到安全存储 // 保存密钥到安全存储
saveKeysToSecureStorage(applicationContext) saveKeysToSecureStorage(applicationContext)

5
android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionHandler.kt

@ -25,7 +25,8 @@ class VoiceInteractionHandler(
private val openaiBaseUrl: String = "", private val openaiBaseUrl: String = "",
private val openaiModel: String = "", private val openaiModel: String = "",
private val volcanoSpeechAppId: String, private val volcanoSpeechAppId: String,
private val volcanoSpeechAppToken: String private val volcanoSpeechAppToken: String,
private val mcpServer: String
) { ) {
private val TAG = "VoiceInteractionHandler" private val TAG = "VoiceInteractionHandler"
@ -80,7 +81,7 @@ class VoiceInteractionHandler(
} }
// 初始化OpenAI服务 // 初始化OpenAI服务
openAIService.initialize(openaiApiKey, openaiBaseUrl, openaiModel) openAIService.initialize(openaiApiKey, openaiBaseUrl, openaiModel, mcpServer)
// 注册广播接收器 // 注册广播接收器
try { try {

6
android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionService.kt

@ -147,6 +147,7 @@ class VoiceInteractionService : Service() {
var openaiModel = MainActivity.openaiModel ?: "" // OpenAI模型 var openaiModel = MainActivity.openaiModel ?: "" // OpenAI模型
var volcanoSpeechAppId = MainActivity.volcanoSpeechAppId ?: "" var volcanoSpeechAppId = MainActivity.volcanoSpeechAppId ?: ""
var volcanoSpeechAppToken = MainActivity.volcanoSpeechAppToken ?: "" var volcanoSpeechAppToken = MainActivity.volcanoSpeechAppToken ?: ""
var mcpServer = MainActivity.mcpServer ?: ""
// 如果静态变量中没有配置,尝试从加密存储中加载 // 如果静态变量中没有配置,尝试从加密存储中加载
if (subscriptionKey.isEmpty() || serviceRegion.isEmpty() || openaiKey.isEmpty()) { if (subscriptionKey.isEmpty() || serviceRegion.isEmpty() || openaiKey.isEmpty()) {
@ -164,7 +165,7 @@ class VoiceInteractionService : Service() {
openaiModel = MainActivity.openaiModel ?: "" openaiModel = MainActivity.openaiModel ?: ""
volcanoSpeechAppId = MainActivity.volcanoSpeechAppId ?: "" volcanoSpeechAppId = MainActivity.volcanoSpeechAppId ?: ""
volcanoSpeechAppToken = MainActivity.volcanoSpeechAppToken ?: "" volcanoSpeechAppToken = MainActivity.volcanoSpeechAppToken ?: ""
mcpServer = MainActivity.mcpServer ?: ""
FileLogger.d(TAG, "已从加密存储加载配置信息") FileLogger.d(TAG, "已从加密存储加载配置信息")
} else { } else {
FileLogger.e(TAG, "无法从加密存储加载配置信息") FileLogger.e(TAG, "无法从加密存储加载配置信息")
@ -175,7 +176,8 @@ class VoiceInteractionService : Service() {
voiceInteractionHandler = VoiceInteractionHandler(applicationContext, voiceInteractionHandler = VoiceInteractionHandler(applicationContext,
subscriptionKey, serviceRegion, subscriptionKey, serviceRegion,
openaiKey, openaiBaseUrl, openaiModel, openaiKey, openaiBaseUrl, openaiModel,
volcanoSpeechAppId, volcanoSpeechAppToken) volcanoSpeechAppId, volcanoSpeechAppToken,
mcpServer)
// 初始化回调 // 初始化回调
voiceInteractionHandler.setCallback(object : VoiceInteractionHandler.InteractionCallback { voiceInteractionHandler.setCallback(object : VoiceInteractionHandler.InteractionCallback {

2
android/build.gradle.kts

@ -8,6 +8,8 @@ allprojects {
maven { maven {
url = uri("https://artifact.bytedance.com/repository/Volcengine/") url = uri("https://artifact.bytedance.com/repository/Volcengine/")
} }
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap") }
} }
} }

24
lib/data/services/open_ai_service_adapter.dart

@ -1,8 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'dart:convert';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:open_ai_service/open_ai_service.dart'; import 'package:open_ai_service/open_ai_service.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'ai_service.dart'; import 'ai_service.dart';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';
/// 定义流事件类型,用于区分不同类型的事件 /// 定义流事件类型,用于区分不同类型的事件
enum StreamEventType { token, complete, error } enum StreamEventType { token, complete, error }
@ -105,11 +109,31 @@ class OpenAIServiceAdapter implements AiService {
return false; return false;
} }
// 读取.mcp.json文件
String mcpConfig = '';
try {
// 从Flutter资源包中加载.mcp.json
mcpConfig = await rootBundle.loadString('.mcp.json');
printInfo(info: '成功从资源包加载.mcp.json配置文件');
// 验证JSON格式
final jsonData = jsonDecode(mcpConfig);
if (jsonData is Map && jsonData.containsKey('mcpServers')) {
printInfo(info: '解析到有效的mcpServers配置');
} else {
printInfo(info: '.mcp.json内容格式不正确,期望包含mcpServers字段');
}
} catch (e) {
printError(info: '加载.mcp.json资源文件时出错: $e');
}
// 初始化OpenAI服务 // 初始化OpenAI服务
final result = await _openAIService.initialize( final result = await _openAIService.initialize(
apiKey: apiKey, apiKey: apiKey,
baseUrl: baseUrl, baseUrl: baseUrl,
model: model, model: model,
mcpServer: mcpConfig,
); );
if (result) { if (result) {

32
lib/data/services/voice_interaction_service.dart

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
@ -44,6 +45,9 @@ class VoiceInteractionService extends GetxService {
late String _openaiModel; late String _openaiModel;
late String _volcanoSpeechAppId; late String _volcanoSpeechAppId;
late String _volcanoSpeechAppToken; late String _volcanoSpeechAppToken;
// MCP配置信息
String? _mcpConfig;
// 聊天历史服务 // 聊天历史服务
late final ChatHistoryService _chatHistoryService; late final ChatHistoryService _chatHistoryService;
@ -71,6 +75,21 @@ class VoiceInteractionService extends GetxService {
Logger.warning('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); Logger.warning('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION');
} }
// 加载MCP配置
_loadMcpConfig();
}
/// 加载MCP配置
void _loadMcpConfig() {
try {
rootBundle.loadString('.mcp.json').then((config) {
_mcpConfig = config;
Logger.info('成功加载MCP配置, 配置内容: $_mcpConfig');
});
} catch (e) {
Logger.warning('加载MCP配置失败: $e');
_mcpConfig = null;
}
} }
/// 处理来自原生层的事件 /// 处理来自原生层的事件
@ -212,8 +231,8 @@ class VoiceInteractionService extends GetxService {
try { try {
Logger.info('启动语音交互服务...'); Logger.info('启动语音交互服务...');
// 使用已加载的配置信息 // 构建参数
final result = await _channel.invokeMethod<bool>('startService', { final Map<String, dynamic> params = {
'azure_speech_key': _azureSpeechKey, 'azure_speech_key': _azureSpeechKey,
'azure_speech_region': _azureSpeechRegion, 'azure_speech_region': _azureSpeechRegion,
'openai_api_key': _openaiApiKey, 'openai_api_key': _openaiApiKey,
@ -221,7 +240,14 @@ class VoiceInteractionService extends GetxService {
'openai_model': _openaiModel, 'openai_model': _openaiModel,
'volcano_speech_app_id': _volcanoSpeechAppId, 'volcano_speech_app_id': _volcanoSpeechAppId,
'volcano_speech_app_token': _volcanoSpeechAppToken, 'volcano_speech_app_token': _volcanoSpeechAppToken,
}) ?? false; };
// 如果有MCP配置,添加到参数中
if (_mcpConfig != null) {
params['mcp_config'] = _mcpConfig;
}
final result = await _channel.invokeMethod<bool>('startService', params) ?? false;
if (result) { if (result) {
_isServiceRunning.value = true; _isServiceRunning.value = true;

105
lib/modules/jieli_test/controllers/jieli_test_controller.dart

@ -1,13 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart' hide ConnectionState;
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:logger/logger.dart';
import 'package:jieli/jieli.dart'; import 'package:jieli/jieli.dart';
/// 杰理测试控制器 /// 杰理测试控制器
class JieliTestController extends GetxController { class JieliTestController extends GetxController {
static final Logger logger = Logger();
// Jieli插件实例 // Jieli插件实例
late Jieli jieli; late Jieli jieli;
@ -21,23 +19,23 @@ class JieliTestController extends GetxController {
var audioDataSize = 0.obs; var audioDataSize = 0.obs;
var receivedFrames = 0.obs; var receivedFrames = 0.obs;
var audioDataReceiving = false.obs; var audioDataReceiving = false.obs;
var volumeCommandResult = ''.obs;
// 事件订阅 // 事件订阅
StreamSubscription? _scanSubscription; StreamSubscription? _scanSubscription;
StreamSubscription? _connectionSubscription; StreamSubscription? _connectionSubscription;
StreamSubscription? _wakeupSubscription; StreamSubscription? _wakeupSubscription;
StreamSubscription? _audioSubscription; StreamSubscription? _audioSubscription;
StreamSubscription? _commandResultSubscription;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
logger.d('JieliTestController 初始化');
_initializeJieli(); _initializeJieli();
} }
@override @override
void onClose() { void onClose() {
logger.d('JieliTestController 关闭');
_cancelAllSubscriptions(); _cancelAllSubscriptions();
_disposeJieli(); _disposeJieli();
super.onClose(); super.onClose();
@ -49,16 +47,16 @@ class JieliTestController extends GetxController {
_connectionSubscription?.cancel(); _connectionSubscription?.cancel();
_wakeupSubscription?.cancel(); _wakeupSubscription?.cancel();
_audioSubscription?.cancel(); _audioSubscription?.cancel();
_commandResultSubscription?.cancel();
} }
/// 初始化杰理SDK /// 初始化杰理SDK
void _initializeJieli() { void _initializeJieli() {
try { try {
logger.d('初始化Jieli插件');
jieli = Jieli(); jieli = Jieli();
_setupEventListeners(); _setupEventListeners();
} catch (e) { } catch (e) {
logger.e('初始化Jieli插件错误', error: e); // 初始化错误处理
} }
} }
@ -66,13 +64,11 @@ class JieliTestController extends GetxController {
void _setupEventListeners() { void _setupEventListeners() {
// 监听扫描结果 // 监听扫描结果
_scanSubscription = jieli.scanResults.listen((devices) { _scanSubscription = jieli.scanResults.listen((devices) {
// logger.d('扫描到设备: ${devices.length}个');
scannedDevices.assignAll(devices); scannedDevices.assignAll(devices);
}); });
// 监听连接状态 // 监听连接状态
_connectionSubscription = jieli.connectionState.listen((state) { _connectionSubscription = jieli.connectionState.listen((state) {
logger.d('连接状态变化: $state');
switch (state) { switch (state) {
case ConnectionState.connected: case ConnectionState.connected:
connectStatus.value = '已连接'; connectStatus.value = '已连接';
@ -95,7 +91,6 @@ class JieliTestController extends GetxController {
// 监听唤醒信号 // 监听唤醒信号
_wakeupSubscription = jieli.wakeupStream.listen((wakeup) { _wakeupSubscription = jieli.wakeupStream.listen((wakeup) {
logger.d('收到唤醒信号');
wakeupReceived.value = true; wakeupReceived.value = true;
Get.snackbar('收到唤醒信号', '耳机发送了唤醒信号'); Get.snackbar('收到唤醒信号', '耳机发送了唤醒信号');
}); });
@ -106,10 +101,21 @@ class JieliTestController extends GetxController {
audioDataSize.value += data.length; audioDataSize.value += data.length;
receivedFrames.value++; receivedFrames.value++;
audioDataReceiving.value = true; audioDataReceiving.value = true;
});
// 每100帧更新一次日志,避免日志过多
if (receivedFrames.value % 100 == 0) { // 监听命令结果
logger.d('已接收音频数据: ${audioDataSize.value} 字节, ${receivedFrames.value} 帧'); _commandResultSubscription = jieli.commandResultStream.listen((result) {
// 处理音量调整命令结果
if (result.commandType == Jieli.commandTypeVolume) {
volumeCommandResult.value = result.success ? '音量调整成功' : '音量调整失败';
Get.snackbar(
result.success ? '音量调整成功' : '音量调整失败',
result.success ? '设备已更新音量' : '设备未能更新音量',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: result.success ? Colors.green.withOpacity(0.7) : Colors.red.withOpacity(0.7),
colorText: Colors.white,
duration: Duration(seconds: 2),
);
} }
}); });
} }
@ -119,12 +125,10 @@ class JieliTestController extends GetxController {
try { try {
final isEnabled = await jieli.isBluetoothEnabled(); final isEnabled = await jieli.isBluetoothEnabled();
if (!isEnabled) { if (!isEnabled) {
logger.d('蓝牙未启用,请求启用蓝牙');
return await jieli.requestBluetoothEnable(); return await jieli.requestBluetoothEnable();
} }
return true; return true;
} catch (e) { } catch (e) {
logger.e('检查蓝牙状态错误', error: e);
return false; return false;
} }
} }
@ -140,7 +144,6 @@ class JieliTestController extends GetxController {
} }
try { try {
logger.d('开始扫描设备');
isScanning.value = true; isScanning.value = true;
// 清空之前的设备列表 // 清空之前的设备列表
@ -150,7 +153,6 @@ class JieliTestController extends GetxController {
final success = await jieli.startScan(scanTimeout: 10000); final success = await jieli.startScan(scanTimeout: 10000);
if (!success) { if (!success) {
logger.e('启动扫描失败');
isScanning.value = false; isScanning.value = false;
Get.snackbar('扫描失败', '启动扫描设备失败,请检查蓝牙权限'); Get.snackbar('扫描失败', '启动扫描设备失败,请检查蓝牙权限');
} }
@ -161,7 +163,6 @@ class JieliTestController extends GetxController {
}); });
} catch (e) { } catch (e) {
logger.e('扫描设备错误', error: e);
isScanning.value = false; isScanning.value = false;
Get.snackbar('扫描错误', e.toString()); Get.snackbar('扫描错误', e.toString());
} }
@ -172,11 +173,10 @@ class JieliTestController extends GetxController {
if (!isScanning.value) return; if (!isScanning.value) return;
try { try {
logger.d('停止扫描');
await jieli.stopScan(); await jieli.stopScan();
isScanning.value = false; isScanning.value = false;
} catch (e) { } catch (e) {
logger.e('停止扫描错误', error: e); // 停止扫描错误处理
} }
} }
@ -190,7 +190,6 @@ class JieliTestController extends GetxController {
// 更新UI状态 // 更新UI状态
connectStatus.value = '连接中...'; connectStatus.value = '连接中...';
logger.d('连接设备: ${device.name} (${device.id})');
// 重置音频和唤醒相关的状态 // 重置音频和唤醒相关的状态
wakeupReceived.value = false; wakeupReceived.value = false;
@ -205,14 +204,11 @@ class JieliTestController extends GetxController {
// 连接成功,更新当前设备 // 连接成功,更新当前设备
currentDevice.value = device; currentDevice.value = device;
deviceInfo.value = '设备名称: ${device.name}\n设备ID: ${device.id}\nRSSI: ${device.rssi} dBm'; deviceInfo.value = '设备名称: ${device.name}\n设备ID: ${device.id}\nRSSI: ${device.rssi} dBm';
logger.d('设备连接成功');
} else { } else {
deviceInfo.value = '连接失败'; deviceInfo.value = '连接失败';
logger.d('设备连接失败');
} }
} catch (e) { } catch (e) {
deviceInfo.value = '连接错误: ${e.toString()}'; deviceInfo.value = '连接错误: ${e.toString()}';
logger.e('连接设备错误', error: e);
} }
} }
@ -222,19 +218,15 @@ class JieliTestController extends GetxController {
try { try {
connectStatus.value = '断开中...'; connectStatus.value = '断开中...';
logger.d('断开设备连接');
final success = await jieli.disconnect(); final success = await jieli.disconnect();
if (success) { if (success) {
deviceInfo.value = '未连接设备'; deviceInfo.value = '未连接设备';
logger.d('设备断开连接成功');
} else {
logger.d('设备断开连接失败');
} }
} catch (e) { } catch (e) {
logger.e('断开设备错误', error: e); // 断开连接错误处理
} }
} }
@ -246,18 +238,14 @@ class JieliTestController extends GetxController {
} }
try { try {
logger.d('发送开始录音命令');
final success = await jieli.sendStartRecordingCommand(); final success = await jieli.sendStartRecordingCommand();
if (success) { if (success) {
Get.snackbar('命令已发送', '已发送开始录音命令'); Get.snackbar('命令已发送', '已发送开始录音命令');
logger.d('开始录音命令发送成功');
} else { } else {
Get.snackbar('命令发送失败', '发送开始录音命令失败'); Get.snackbar('命令发送失败', '发送开始录音命令失败');
logger.d('开始录音命令发送失败');
} }
} catch (e) { } catch (e) {
logger.e('发送开始录音命令错误', error: e);
Get.snackbar('命令错误', e.toString()); Get.snackbar('命令错误', e.toString());
} }
} }
@ -270,29 +258,66 @@ class JieliTestController extends GetxController {
} }
try { try {
logger.d('发送停止录音命令');
final success = await jieli.sendStopRecordingCommand(); final success = await jieli.sendStopRecordingCommand();
if (success) { if (success) {
Get.snackbar('命令已发送', '已发送停止录音命令'); Get.snackbar('命令已发送', '已发送停止录音命令');
logger.d('停止录音命令发送成功');
} else { } else {
Get.snackbar('命令发送失败', '发送停止录音命令失败'); Get.snackbar('命令发送失败', '发送停止录音命令失败');
logger.d('停止录音命令发送失败');
} }
} catch (e) { } catch (e) {
logger.e('发送停止录音命令错误', error: e);
Get.snackbar('命令错误', e.toString()); Get.snackbar('命令错误', e.toString());
} }
} }
/// 增加音量
Future<void> increaseVolume() async {
if (currentDevice.value == null) {
Get.snackbar('操作失败', '未连接设备');
return;
}
try {
volumeCommandResult.value = '发送中...';
final success = await jieli.increaseVolume();
if (!success) {
Get.snackbar('命令发送失败', '发送增加音量命令失败');
volumeCommandResult.value = '发送失败';
}
} catch (e) {
Get.snackbar('命令错误', e.toString());
volumeCommandResult.value = '发送出错';
}
}
/// 减小音量
Future<void> decreaseVolume() async {
if (currentDevice.value == null) {
Get.snackbar('操作失败', '未连接设备');
return;
}
try {
volumeCommandResult.value = '发送中...';
final success = await jieli.decreaseVolume();
if (!success) {
Get.snackbar('命令发送失败', '发送减小音量命令失败');
volumeCommandResult.value = '发送失败';
}
} catch (e) {
Get.snackbar('命令错误', e.toString());
volumeCommandResult.value = '发送出错';
}
}
/// 清理资源 /// 清理资源
void _disposeJieli() { void _disposeJieli() {
try { try {
logger.d('释放Jieli插件资源');
jieli.dispose(); jieli.dispose();
} catch (e) { } catch (e) {
logger.e('释放Jieli插件资源错误', error: e); // 释放资源错误处理
} }
} }
} }

74
lib/modules/jieli_test/views/jieli_test_view.dart

@ -260,6 +260,80 @@ class JieliTestView extends GetView<JieliTestController> {
), ),
], ],
), ),
SizedBox(height: 8.h),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: controller.increaseVolume,
icon: Icon(
Icons.volume_up,
size: 18.sp,
),
label: Text(
'增加音量',
style: TextStyle(fontSize: 14.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue[600],
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
),
),
SizedBox(width: 8.w),
Expanded(
child: ElevatedButton.icon(
onPressed: controller.decreaseVolume,
icon: Icon(
Icons.volume_down,
size: 18.sp,
),
label: Text(
'减小音量',
style: TextStyle(fontSize: 14.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange[600],
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
),
),
],
),
SizedBox(height: 8.h),
Obx(() => controller.volumeCommandResult.value.isNotEmpty
? Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 16.w),
decoration: BoxDecoration(
color: controller.volumeCommandResult.value.contains('成功')
? Colors.green[100]
: (controller.volumeCommandResult.value == '发送中...'
? Colors.blue[100]
: Colors.red[100]),
borderRadius: BorderRadius.circular(8.r),
),
child: Text(
'音量控制: ${controller.volumeCommandResult.value}',
style: TextStyle(
fontSize: 13.sp,
color: controller.volumeCommandResult.value.contains('成功')
? Colors.green[900]
: (controller.volumeCommandResult.value == '发送中...'
? Colors.blue[900]
: Colors.red[900]),
),
textAlign: TextAlign.center,
),
)
: SizedBox.shrink()
),
], ],
); );
} }

269
local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt

@ -12,34 +12,6 @@ import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.util.* import java.util.*
/**
* BLE回调接口,统一处理蓝牙事件
*/
interface BleCallback {
/**
* 扫描结果回调
* @param devices 扫描到的设备列表
*/
fun onScanResult(devices: List<Map<String, Any>>)
/**
* 连接状态变化回调
* @param state 连接状态
*/
fun onConnectionStateChanged(state: Int)
/**
* 音频数据回调
* @param data 音频数据字节数组
*/
fun onAudioDataReceived(data: ByteArray)
/**
* 唤醒信号回调
*/
fun onWakeupSignalReceived()
}
/** /**
* BLE服务类,提供核心蓝牙功能 * BLE服务类,提供核心蓝牙功能
*/ */
@ -47,12 +19,11 @@ class BleService(private val context: Context) {
companion object { companion object {
private const val TAG = "JieliBleService" private const val TAG = "JieliBleService"
// 服务和特征UUID // 服务和特征UUID - 根据协议规范重新定义
private const val AUDIO_SERVICE_UUID = "0000A100-0000-1000-8000-00805F9B34FB" private const val AUDIO_SERVICE_UUID = "0000faa0-0000-1000-8000-00805f9b34fb" // 主服务UUID
private const val AUDIO_DATA_CHAR_UUID = "0000A101-0000-1000-8000-00805F9B34FB" private const val NOTIFY_CHAR_UUID = "0000faa2-0000-1000-8000-00805f9b34fb" // 用于接收数据的特征(notify)
private const val CONTROL_CHAR_UUID = "0000A102-0000-1000-8000-00805F9B34FB" private const val WRITE_CHAR_UUID = "0000faa1-0000-1000-8000-00805f9b34fb" // 用于发送命令的特征(write without response)
private const val CLIENT_CHAR_CONFIG_UUID = "00002902-0000-1000-8000-00805f9b34fb" private const val CLIENT_CHAR_CONFIG_UUID = "00002902-0000-1000-8000-00805f9b34fb" // 标准的客户端特征配置描述符
// 连接状态常量 // 连接状态常量
const val STATE_DISCONNECTED = 0 const val STATE_DISCONNECTED = 0
const val STATE_CONNECTING = 1 const val STATE_CONNECTING = 1
@ -62,6 +33,20 @@ class BleService(private val context: Context) {
// 扫描结果更新间隔(毫秒) // 扫描结果更新间隔(毫秒)
private const val SCAN_RESULT_UPDATE_INTERVAL = 500L private const val SCAN_RESULT_UPDATE_INTERVAL = 500L
// 命令类型常量
const val COMMAND_VOLUME = 1
}
/**
* 回调接口
*/
interface Callback {
fun onScanResult(devices: List<Map<String, Any>>)
fun onConnectionStateChanged(state: Int)
fun onAudioDataReceived(data: ByteArray)
fun onWakeupSignalReceived()
fun onCommandResult(commandType: Int, success: Boolean)
} }
// 蓝牙管理器和适配器 // 蓝牙管理器和适配器
@ -71,8 +56,8 @@ class BleService(private val context: Context) {
// GATT连接和特征 // GATT连接和特征
private var bluetoothGatt: BluetoothGatt? = null private var bluetoothGatt: BluetoothGatt? = null
private var audioDataChar: BluetoothGattCharacteristic? = null private var notifyChar: BluetoothGattCharacteristic? = null
private var controlChar: BluetoothGattCharacteristic? = null private var writeChar: BluetoothGattCharacteristic? = null
// 扫描相关 // 扫描相关
private val scanHandler = Handler(Looper.getMainLooper()) private val scanHandler = Handler(Looper.getMainLooper())
@ -88,7 +73,7 @@ class BleService(private val context: Context) {
val connectionState = MutableLiveData<Int>(STATE_DISCONNECTED) val connectionState = MutableLiveData<Int>(STATE_DISCONNECTED)
// 统一回调接口 // 统一回调接口
var callback: BleCallback? = null var callback: Callback? = null
/** /**
* 开始扫描BLE设备 * 开始扫描BLE设备
@ -252,8 +237,8 @@ class BleService(private val context: Context) {
// 清理资源 // 清理资源
gatt.close() gatt.close()
bluetoothGatt = null bluetoothGatt = null
audioDataChar = null notifyChar = null
controlChar = null writeChar = null
} else if (status != BluetoothGatt.GATT_SUCCESS) { } else if (status != BluetoothGatt.GATT_SUCCESS) {
Log.e(TAG, "连接失败,status=$status") Log.e(TAG, "连接失败,status=$status")
updateConnectionState(STATE_ERROR) updateConnectionState(STATE_ERROR)
@ -272,13 +257,16 @@ class BleService(private val context: Context) {
if (audioService != null) { if (audioService != null) {
Log.i(TAG, "找到音频服务") Log.i(TAG, "找到音频服务")
// 获取音频数据和控制特征 // 获取特征
audioDataChar = audioService.getCharacteristic(UUID.fromString(AUDIO_DATA_CHAR_UUID)) notifyChar = audioService.getCharacteristic(UUID.fromString(NOTIFY_CHAR_UUID))
controlChar = audioService.getCharacteristic(UUID.fromString(CONTROL_CHAR_UUID)) writeChar = audioService.getCharacteristic(UUID.fromString(WRITE_CHAR_UUID))
if (audioDataChar != null && controlChar != null) { if (notifyChar != null && writeChar != null) {
Log.i(TAG, "找到所需特征") Log.i(TAG, "找到所需特征")
// 设置特征写入类型为无响应模式
writeChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
// 注册特征通知 // 注册特征通知
setupNotifications(gatt) setupNotifications(gatt)
} else { } else {
@ -297,22 +285,28 @@ class BleService(private val context: Context) {
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
val characteristicUuid = characteristic.uuid.toString() val characteristicUuid = characteristic.uuid.toString()
Log.d(TAG, "收到特征变化通知: $characteristicUuid")
when (characteristicUuid) { when (characteristicUuid) {
AUDIO_DATA_CHAR_UUID -> { // NOTIFY_CHAR_UUID -> {
// 收到音频数据 // // 收到音频数据
val audioData = characteristic.value // val audioData = characteristic.value
Log.d(TAG, "收到音频数据帧,大小: ${audioData.size} 字节") // Log.d(TAG, "收到音频数据帧,大小: ${audioData.size} 字节")
callback?.onAudioDataReceived(audioData) // callback?.onAudioDataReceived(audioData)
} // }
CONTROL_CHAR_UUID -> { NOTIFY_CHAR_UUID -> {
// 收到唤醒信号 // 处理控制消息
val value = characteristic.value val value = characteristic.value
Log.i(TAG, "收到控制通知: ${value.contentToString()}") Log.i(TAG, "收到控制通知: ${value.contentToString()}")
// 判断是否是协议格式响应
if (value.isNotEmpty() && value[0].toInt() and 0xFF == 0xA5) {
// 处理设备返回的协议数据
processResponseData(value)
}
// 假设0x01表示唤醒信号 // 假设0x01表示唤醒信号
if (value.isNotEmpty() && value[0].toInt() == 0x01) { else if (value.isNotEmpty() && value[0].toInt() == 0x01) {
Log.i(TAG, "收到唤醒信号") Log.i(TAG, "收到唤醒信号")
callback?.onWakeupSignalReceived() callback?.onWakeupSignalReceived()
@ -325,7 +319,7 @@ class BleService(private val context: Context) {
} }
override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
if (characteristic.uuid.toString() == CONTROL_CHAR_UUID) { if (characteristic.uuid.toString() == WRITE_CHAR_UUID) {
if (status == BluetoothGatt.GATT_SUCCESS) { if (status == BluetoothGatt.GATT_SUCCESS) {
Log.i(TAG, "命令发送成功: ${characteristic.value.contentToString()}") Log.i(TAG, "命令发送成功: ${characteristic.value.contentToString()}")
} else { } else {
@ -337,13 +331,10 @@ class BleService(private val context: Context) {
override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) { override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) { if (status == BluetoothGatt.GATT_SUCCESS) {
Log.i(TAG, "描述符写入成功: ${descriptor.uuid}") Log.i(TAG, "描述符写入成功: ${descriptor.uuid}")
Log.i(TAG, "特征通知设置成功,设备就绪")
// 如果是音频数据特征的描述符,再订阅控制特征
if (descriptor.characteristic.uuid.toString() == AUDIO_DATA_CHAR_UUID) {
setupControlNotification(gatt)
}
} else { } else {
Log.e(TAG, "描述符写入失败,status=$status") Log.e(TAG, "描述符写入失败,status=$status")
updateConnectionState(STATE_ERROR)
} }
} }
} }
@ -352,57 +343,92 @@ class BleService(private val context: Context) {
* 设置特征通知 * 设置特征通知
*/ */
private fun setupNotifications(gatt: BluetoothGatt) { private fun setupNotifications(gatt: BluetoothGatt) {
// 先订阅音频数据通知 // 启用 notify 特征通知
if (audioDataChar?.properties?.and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) { if (notifyChar?.properties?.and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) {
gatt.setCharacteristicNotification(audioDataChar, true) gatt.setCharacteristicNotification(notifyChar, true)
val descriptor = audioDataChar?.getDescriptor(UUID.fromString(CLIENT_CHAR_CONFIG_UUID)) val descriptor = notifyChar?.getDescriptor(UUID.fromString(CLIENT_CHAR_CONFIG_UUID))
descriptor?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE descriptor?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
if (descriptor != null) { if (descriptor != null) {
gatt.writeDescriptor(descriptor) gatt.writeDescriptor(descriptor)
Log.i(TAG, "设置音频数据特征通知") Log.i(TAG, "设置数据通知特征通知")
} }
} else { } else {
Log.e(TAG, "音频数据特征不支持通知") Log.e(TAG, "数据通知特征不支持通知")
setupControlNotification(gatt)
} }
} }
/** /**
* 设置控制特征通知 * 处理设备返回的数据
* 协议格式:
* 协议头(0xA5) + 字段类型(1byte) + 数据长度(1byte) + 数据域(N byte) + 校验和(1byte)
*/ */
private fun setupControlNotification(gatt: BluetoothGatt) { private fun processResponseData(data: ByteArray) {
if (controlChar?.properties?.and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) { if (data.size < 4) {
gatt.setCharacteristicNotification(controlChar, true) Log.e(TAG, "返回数据格式错误,长度不足")
return
val descriptor = controlChar?.getDescriptor(UUID.fromString(CLIENT_CHAR_CONFIG_UUID)) }
descriptor?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
// 判断协议头
if (descriptor != null) { if (data[0].toInt() and 0xFF != 0xA5) {
gatt.writeDescriptor(descriptor) Log.e(TAG, "返回数据协议头错误: ${data[0].toInt() and 0xFF}")
Log.i(TAG, "设置控制特征通知") return
}
val fieldType = data[1].toInt() and 0xFF
val dataLen = data[2].toInt() and 0xFF
// 验证长度
if (data.size != dataLen + 4) { // 协议头+字段类型+数据长度+数据域+校验和
Log.e(TAG, "返回数据长度不匹配: 应为${dataLen + 4},实际为${data.size}")
return
}
// 计算校验和
var checksum = 0
for (i in 0 until data.size - 1) {
checksum += data[i].toInt() and 0xFF
}
checksum = checksum and 0xFF
val receivedChecksum = data[data.size - 1].toInt() and 0xFF
if (checksum != receivedChecksum) {
Log.e(TAG, "校验和错误: 计算值=$checksum, 接收值=$receivedChecksum")
return
}
// 解析字段类型
when (fieldType) {
0x01 -> {
// 音量调整命令的响应
val result = data[3].toInt() and 0xFF
val success = result == 0
Log.i(TAG, "音量调整命令响应: ${if (success) "成功" else "失败"}")
callback?.onCommandResult(COMMAND_VOLUME, success)
}
// 其他命令类型可在此处添加
else -> {
Log.d(TAG, "未知命令类型: ${String.format("0x%02X", fieldType)}")
} }
} else {
Log.e(TAG, "控制特征不支持通知")
} }
} }
/** /**
* 发送开始录音命令(向控制特征写入0x01) * 发送开始录音命令
*/ */
fun sendStartRecordingCommand(): Boolean { fun sendStartRecordingCommand(): Boolean {
if (bluetoothGatt == null || controlChar == null || connectionState.value != STATE_CONNECTED) { if (bluetoothGatt == null || writeChar == null || connectionState.value != STATE_CONNECTED) {
Log.e(TAG, "无法发送开始录音命令,设备未连接") Log.e(TAG, "无法发送开始录音命令,设备未连接")
return false return false
} }
try { try {
val commandData = byteArrayOf(0x01) val commandData = byteArrayOf(0x01)
controlChar?.value = commandData writeChar?.value = commandData
controlChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
val success = bluetoothGatt?.writeCharacteristic(controlChar) ?: false val success = bluetoothGatt?.writeCharacteristic(writeChar) ?: false
if (success) { if (success) {
Log.i(TAG, "开始录音命令已发送") Log.i(TAG, "开始录音命令已发送")
@ -418,20 +444,19 @@ class BleService(private val context: Context) {
} }
/** /**
* 发送停止录音命令(向控制特征写入0x02) * 发送停止录音命令
*/ */
fun sendStopRecordingCommand(): Boolean { fun sendStopRecordingCommand(): Boolean {
if (bluetoothGatt == null || controlChar == null || connectionState.value != STATE_CONNECTED) { if (bluetoothGatt == null || writeChar == null || connectionState.value != STATE_CONNECTED) {
Log.e(TAG, "无法发送停止录音命令,设备未连接") Log.e(TAG, "无法发送停止录音命令,设备未连接")
return false return false
} }
try { try {
val commandData = byteArrayOf(0x02) val commandData = byteArrayOf(0x02)
controlChar?.value = commandData writeChar?.value = commandData
controlChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
val success = bluetoothGatt?.writeCharacteristic(controlChar) ?: false val success = bluetoothGatt?.writeCharacteristic(writeChar) ?: false
if (success) { if (success) {
Log.i(TAG, "停止录音命令已发送") Log.i(TAG, "停止录音命令已发送")
@ -446,6 +471,70 @@ class BleService(private val context: Context) {
} }
} }
/**
* 发送音量调整命令
* 协议格式:
* 协议头(0xAA) + 字段类型(0x01) + 数据长度(0x01) + 音量值(0~16) + 校验和
*/
fun adjustVolume(volume: Int): Boolean {
if (bluetoothGatt == null || writeChar == null || connectionState.value != STATE_CONNECTED) {
Log.e(TAG, "无法发送音量调整命令,设备未连接")
return false
}
// 限制音量范围为0-16
val safeVolume = volume.coerceIn(0, 16)
try {
// 构建命令数据
val header: Byte = 0xAA.toByte()
val fieldType: Byte = 0x01
val dataLen: Byte = 0x01
val volumeData: Byte = safeVolume.toByte()
// 计算校验和
val checksum = ((header.toInt() and 0xFF) +
(fieldType.toInt() and 0xFF) +
(dataLen.toInt() and 0xFF) +
(volumeData.toInt() and 0xFF)) and 0xFF
// 创建完整命令
val commandData = byteArrayOf(header, fieldType, dataLen, volumeData, checksum.toByte())
// 写入特征
writeChar?.value = commandData
val success = bluetoothGatt?.writeCharacteristic(writeChar) ?: false
if (success) {
Log.i(TAG, "音量调整命令已发送: $safeVolume")
} else {
Log.e(TAG, "音量调整命令发送失败")
}
return success
} catch (e: Exception) {
Log.e(TAG, "发送音量调整命令时发生异常: ${e.message}")
return false
}
}
/**
* 增加音量
*/
fun increaseVolume(): Boolean {
// 假设当前音量保存在设备中,我们只需发送增加命令
return adjustVolume(16) // 发送最大音量命令
}
/**
* 减小音量
*/
fun decreaseVolume(): Boolean {
// 假设当前音量保存在设备中,我们只需发送减小命令
return adjustVolume(0) // 发送最小音量命令
}
/** /**
* 清理资源 * 清理资源
*/ */
@ -454,7 +543,7 @@ class BleService(private val context: Context) {
scanHandler.removeCallbacks(scanResultUpdateRunnable) scanHandler.removeCallbacks(scanResultUpdateRunnable)
bluetoothGatt?.close() bluetoothGatt?.close()
bluetoothGatt = null bluetoothGatt = null
audioDataChar = null notifyChar = null
controlChar = null writeChar = null
} }
} }

25
local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/JieliPlugin.kt

@ -44,6 +44,7 @@ class JieliPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, EventChannel
private const val EVENT_TYPE_CONNECTION_STATE = "connection_state" private const val EVENT_TYPE_CONNECTION_STATE = "connection_state"
private const val EVENT_TYPE_SCAN_RESULT = "scan_result" private const val EVENT_TYPE_SCAN_RESULT = "scan_result"
private const val EVENT_TYPE_WAKEUP_SIGNAL = "wakeup_signal" private const val EVENT_TYPE_WAKEUP_SIGNAL = "wakeup_signal"
private const val EVENT_TYPE_COMMAND_RESULT = "command_result"
} }
// 蓝牙相关请求码 // 蓝牙相关请求码
@ -75,7 +76,7 @@ class JieliPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, EventChannel
*/ */
private fun setupBleCallbacks() { private fun setupBleCallbacks() {
// 设置统一回调接口 // 设置统一回调接口
bleService.callback = object : BleCallback { bleService.callback = object : BleService.Callback {
override fun onScanResult(devices: List<Map<String, Any>>) { override fun onScanResult(devices: List<Map<String, Any>>) {
pluginScope.launch(Dispatchers.Main) { pluginScope.launch(Dispatchers.Main) {
sendEvent(EVENT_TYPE_SCAN_RESULT, devices) sendEvent(EVENT_TYPE_SCAN_RESULT, devices)
@ -99,6 +100,15 @@ class JieliPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, EventChannel
sendEvent(EVENT_TYPE_WAKEUP_SIGNAL, true) sendEvent(EVENT_TYPE_WAKEUP_SIGNAL, true)
} }
} }
override fun onCommandResult(commandType: Int, success: Boolean) {
pluginScope.launch(Dispatchers.Main) {
sendEvent(EVENT_TYPE_COMMAND_RESULT, mapOf(
"commandType" to commandType,
"success" to success
))
}
}
} }
} }
@ -187,6 +197,19 @@ class JieliPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, EventChannel
activity?.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) activity?.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
result.success(true) result.success(true)
} }
"adjustVolume" -> {
val volume = call.argument<Int>("volume") ?: 0
val success = bleService.adjustVolume(volume)
result.success(success)
}
"increaseVolume" -> {
val success = bleService.increaseVolume()
result.success(success)
}
"decreaseVolume" -> {
val success = bleService.decreaseVolume()
result.success(success)
}
else -> { else -> {
result.notImplemented() result.notImplemented()
} }

84
local_plugins/jieli/lib/jieli.dart

@ -46,6 +46,7 @@ class EventType {
static const String connectionState = 'connection_state'; static const String connectionState = 'connection_state';
static const String scanResult = 'scan_result'; static const String scanResult = 'scan_result';
static const String wakeupSignal = 'wakeup_signal'; static const String wakeupSignal = 'wakeup_signal';
static const String commandResult = 'command_result';
} }
/// 杰理BLE插件主类 /// 杰理BLE插件主类
@ -72,10 +73,21 @@ class Jieli {
static final StreamController<bool> _wakeupController = static final StreamController<bool> _wakeupController =
StreamController<bool>.broadcast(); StreamController<bool>.broadcast();
Stream<bool> get wakeupStream => _wakeupController.stream; Stream<bool> get wakeupStream => _wakeupController.stream;
/// 命令结果流
static final StreamController<CommandResult> _commandResultController =
StreamController<CommandResult>.broadcast();
Stream<CommandResult> get commandResultStream => _commandResultController.stream;
/// 原始事件流
Stream<Map<String, dynamic>>? _eventStream;
/// 事件流订阅 /// 事件流订阅
StreamSubscription? _eventSubscription; StreamSubscription? _eventSubscription;
/// 命令类型常量
static const int commandTypeVolume = 1;
/// 构造函数,初始化平台通道和事件监听 /// 构造函数,初始化平台通道和事件监听
Jieli() { Jieli() {
_setupEventListener(); _setupEventListener();
@ -118,6 +130,16 @@ class Jieli {
_wakeupController.add(data); _wakeupController.add(data);
} }
break; break;
case EventType.commandResult:
if (data is Map<dynamic, dynamic>) {
final commandType = data['commandType'] as int;
final success = data['success'] as bool;
_commandResultController.add(CommandResult(
commandType: commandType,
success: success,
));
}
break;
} }
} }
@ -172,6 +194,7 @@ class Jieli {
_connectionController.close(); _connectionController.close();
_audioController.close(); _audioController.close();
_wakeupController.close(); _wakeupController.close();
_commandResultController.close();
} }
/// 检查蓝牙是否已启用 /// 检查蓝牙是否已启用
@ -185,4 +208,65 @@ class Jieli {
final result = await _methodChannel.invokeMethod<bool>('requestBluetoothEnable'); final result = await _methodChannel.invokeMethod<bool>('requestBluetoothEnable');
return result ?? false; return result ?? false;
} }
/// 调整音量
/// [volume] 音量值,范围0-16
Future<bool> adjustVolume(int volume) async {
try {
final result = await _methodChannel.invokeMethod<bool>(
'adjustVolume',
{'volume': volume},
);
return result ?? false;
} catch (e) {
print('调整音量失败: $e');
return false;
}
}
/// 增加音量
Future<bool> increaseVolume() async {
try {
final result = await _methodChannel.invokeMethod<bool>('increaseVolume');
return result ?? false;
} catch (e) {
print('增加音量失败: $e');
return false;
}
}
/// 减小音量
Future<bool> decreaseVolume() async {
try {
final result = await _methodChannel.invokeMethod<bool>('decreaseVolume');
return result ?? false;
} catch (e) {
print('减小音量失败: $e');
return false;
}
}
/// 命令结果回调
Stream<CommandResult> get onCommandResult => commandResultStream
.where((event) => event.commandType == commandTypeVolume)
.map((event) => event);
}
/// 蓝牙设备模型
class BluetoothDevice {
// ... existing code ...
}
/// 命令结果
class CommandResult {
final int commandType;
final bool success;
CommandResult({
required this.commandType,
required this.success,
});
@override
String toString() => 'CommandResult{commandType: $commandType, success: $success}';
} }

5
local_plugins/open_ai_service/android/build.gradle.kts

@ -50,6 +50,11 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2") implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2")
implementation("com.squareup.okhttp3:okhttp:4.9.3") implementation("com.squareup.okhttp3:okhttp:4.9.3")
implementation("com.squareup.okhttp3:logging-interceptor:4.9.3")
implementation("com.anthropic:anthropic-java:0.8.0") implementation("com.anthropic:anthropic-java:0.8.0")
implementation("io.modelcontextprotocol:kotlin-sdk:0.4.0") implementation("io.modelcontextprotocol:kotlin-sdk:0.4.0")
implementation("io.ktor:ktor-client-core:3.1.2")
implementation("io.ktor:ktor-client-cio:3.1.2")
implementation("io.ktor:ktor-client-okhttp:3.1.2")
implementation("io.ktor:ktor-client-logging:3.1.2")
} }

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

@ -9,14 +9,30 @@ import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.plugins.sse.*
import io.modelcontextprotocol.kotlin.sdk.Implementation import io.modelcontextprotocol.kotlin.sdk.Implementation
import io.modelcontextprotocol.kotlin.sdk.ListToolsResult
import io.modelcontextprotocol.kotlin.sdk.TextContent import io.modelcontextprotocol.kotlin.sdk.TextContent
import io.modelcontextprotocol.kotlin.sdk.client.Client import io.modelcontextprotocol.kotlin.sdk.client.Client
import io.modelcontextprotocol.kotlin.sdk.client.WebSocketClientTransport import io.modelcontextprotocol.kotlin.sdk.client.SseClientTransport
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import org.json.JSONObject import org.json.JSONObject
import kotlin.jvm.optionals.getOrNull import kotlin.jvm.optionals.getOrNull
import java.io.File
import io.ktor.client.engine.okhttp.*
import okhttp3.OkHttpClient
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
import java.security.SecureRandom
import java.util.concurrent.TimeUnit
import okhttp3.logging.HttpLoggingInterceptor
import okhttp3.Interceptor
import io.ktor.client.request.*
import io.ktor.http.*
import com.yunqiinnovation.open_ai_service.mcp.CustomSseClientTransport
/** /**
* 工具类型枚举 * 工具类型枚举
@ -39,55 +55,45 @@ interface FunctionHandler {
} }
/** /**
* MCP客户端,用于连接Model Context Protocol服务器和管理本地函数 * 单个MCP子客户端
* 负责连接到一个具体的MCP服务器并管理其工具
*/ */
class MCPClient(private val context: Context? = null) : AutoCloseable { class MCPSubClient(
private val TAG = "MCPClient" private val serverId: String,
serverUrl: String,
private val customClient: HttpClient? = null
) : AutoCloseable {
private val TAG = "MCPSubClient[$serverId]"
// 保存服务器URL
private val serverUrl: String = serverUrl
init {
Log.d(TAG, "MCPSubClient初始化,URL: $serverUrl")
}
// 初始化MCP客户端 // 初始化MCP客户端
private val mcp: Client = Client( private val mcp: Client = Client(
clientInfo = Implementation( clientInfo = Implementation(
name = "deep-voice-mcp-client", name = "deep-voice-mcp-client-$serverId",
version = "1.0.0" version = "1.0.0"
) )
) )
// 服务器提供的工具列表 // 服务器提供的工具列表
private var tools: List<ToolUnion> = emptyList() private var tools: List<ToolUnion> = emptyList()
// 工具Map列表,用于OpenAI请求 // 工具Map列表,用于OpenAI请求
private val toolMaps = mutableListOf<Map<String, Any>>() private val toolMaps = mutableListOf<Map<String, Any>>()
// 本地函数Map,函数名 -> 处理器
private val localFunctions = mutableMapOf<String, FunctionHandler>()
// 本地函数定义Map,函数名 -> 定义
private val localFunctionDefs = mutableMapOf<String, String>()
// 是否已连接 // 是否已连接
private var isConnected = false private var isConnected = false
// 系统功能处理器 // SSE传输层,使用自定义的传输层
private val systemFunctionHandler = SystemFunctionHandler(context!!) private var sseTransport: CustomSseClientTransport? = null
init {
// 如果提供了Context,自动注册语音功能处理器
context?.let { initializeVoiceFunctions(it) }
}
/** // 工具名称到工具的映射,用于快速查找
* 初始化语音功能处理器 private val toolMapping = mutableMapOf<String, ToolUnion>()
*/
private fun initializeVoiceFunctions(context: Context) {
try {
// 使用SystemFunctionHandler注册所有功能
systemFunctionHandler.registerAllFunctions(this)
Log.d(TAG, "语音功能初始化完成")
} catch (e: Exception) {
Log.e(TAG, "语音功能初始化失败: ${e.message}", e)
}
}
/** /**
* 将JsonObject转换为JsonValue * 将JsonObject转换为JsonValue
@ -97,20 +103,35 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
val node = mapper.readTree(this.toString()) val node = mapper.readTree(this.toString())
return JsonValue.fromJsonNode(node) return JsonValue.fromJsonNode(node)
} }
/** /**
* 连接到MCP服务器(通过WebSocket) * 连接到MCP服务器(通过SSE)
* @return 是否连接成功
*/ */
suspend fun connectToWebSocket(serverUrl: String): Boolean { suspend fun connect(): Boolean {
try { try {
// 创建WebSocket传输层 Log.d(TAG, "开始连接到服务器: $serverUrl")
val transport = WebSocketClientTransport(
client = HttpClient(), // 使用提供的客户端或创建带SSL配置的客户端
urlString = serverUrl val client = customClient ?: createSslTrustAllClient()
// 使用自定义传输层,直接传递完整URL
sseTransport = CustomSseClientTransport(
client = client,
urlString = serverUrl,
requestBuilder = {
// 添加基本请求头
headers {
append(HttpHeaders.Accept, "text/event-stream")
append(HttpHeaders.CacheControl, "no-cache")
}
}
) )
Log.d(TAG, "创建自定义SSE传输层成功")
// 连接MCP客户端到服务器 // 连接MCP客户端到服务器
mcp.connect(transport) mcp.connect(sseTransport!!)
// 获取服务器提供的工具列表 // 获取服务器提供的工具列表
val toolsResult = mcp.listTools() val toolsResult = mcp.listTools()
@ -120,10 +141,12 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
processToolsList(toolsResult) processToolsList(toolsResult)
isConnected = true isConnected = true
Log.d(TAG, "已连接到WebSocket服务器,可用工具: ${tools.map { it.tool().get().name() }.joinToString(", ")}") Log.d(TAG, "成功连接到MCP服务器,可用工具: ${tools.map { it.tool().get().name() }.joinToString(", ")}")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "连接WebSocket服务器失败: ${e.message}", e) Log.e(TAG, "连接服务器失败: ${e.message}", e)
// 记录详细的错误堆栈
Log.e(TAG, "详细错误: ${e.stackTraceToString()}")
return false return false
} }
} }
@ -131,7 +154,7 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
/** /**
* 处理工具列表 * 处理工具列表
*/ */
private fun processToolsList(toolsResult: io.modelcontextprotocol.kotlin.sdk.ListToolsResult?) { private fun processToolsList(toolsResult: ListToolsResult?) {
tools = toolsResult?.tools?.map { tool -> tools = toolsResult?.tools?.map { tool ->
// 确保输入模式有效 // 确保输入模式有效
val inputType = tool.inputSchema?.type ?: "object" val inputType = tool.inputSchema?.type ?: "object"
@ -153,10 +176,415 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
) )
} ?: emptyList() } ?: emptyList()
// 更新用于OpenAI的工具映射 // 更新工具映射
tools.forEach { toolUnion ->
val tool = toolUnion.tool().get()
toolMapping[tool.name()] = toolUnion
}
// 更新工具Map列表
updateToolMaps() updateToolMaps()
} }
/**
* 更新工具Map列表,用于OpenAI请求
*/
private fun updateToolMaps() {
toolMaps.clear()
// 添加MCP工具
tools.forEach { toolUnion ->
try {
val toolOptional = toolUnion.tool()
if (!toolOptional.isPresent) {
return@forEach
}
val tool = toolOptional.get()
// 创建基本参数结构
val parametersMap = mutableMapOf<String, Any>()
parametersMap["type"] = "object" // 默认值
parametersMap["properties"] = mutableMapOf<String, Any>()
parametersMap["required"] = emptyList<String>()
// 从工具的输入模式中提取信息
try {
// 获取输入模式并转换为字符串
val schema = tool.inputSchema()
val schemaStr = schema.toString()
Log.d(TAG, "解析工具Schema: $schemaStr")
// 如果toString()提供了足够的信息,就使用正则表达式解析
// 这是最可靠的方法,因为它不依赖于API的细节变化
// 解析类型
val typePattern = "type=([^,}]+)".toRegex()
val typeMatch = typePattern.find(schemaStr)
if (typeMatch != null) {
parametersMap["type"] = typeMatch.groupValues[1].trim()
}
// 尝试从字符串中解析完整的JSON结构
try {
// 直接从Schema字符串中提取属性结构,手动构建properties映射
val propertiesMap = mutableMapOf<String, Any>()
// 匹配属性定义: 形如 fieldName={...}
val propertiesPattern = "properties=\\{([^}]+)\\}".toRegex()
val propertiesMatch = propertiesPattern.find(schemaStr)
if (propertiesMatch != null) {
// 匹配属性定义: 形如 fieldName={...}
val fieldPattern = "([\\w]+)=\\{([^{}]+(?:\\{[^{}]*\\}[^{}]*)*)\\}".toRegex()
val fieldMatches = fieldPattern.findAll(propertiesMatch.groupValues[1])
fieldMatches.forEach { fieldMatch ->
val fieldName = fieldMatch.groupValues[1]
val fieldContent = fieldMatch.groupValues[2]
// 为属性创建一个Map
val fieldMap = mutableMapOf<String, Any>()
// 解析字段内容中的键值对
val keyValuePattern = "(\\w+)=([^,}]+)".toRegex()
val keyValueMatches = keyValuePattern.findAll(fieldContent)
keyValueMatches.forEach { kvMatch ->
val key = kvMatch.groupValues[1]
val value = kvMatch.groupValues[2].trim()
// 根据值的类型进行适当处理
val processedValue: Any = when {
value == "true" -> true
value == "false" -> false
value.matches("\\d+".toRegex()) -> value.toIntOrNull() ?: value
value.matches("\\d+\\.\\d+".toRegex()) -> value.toDoubleOrNull() ?: value
else -> value // 保持为字符串
}
fieldMap[key] = processedValue
}
propertiesMap[fieldName] = fieldMap
}
parametersMap["properties"] = propertiesMap
Log.d(TAG, "成功解析属性: $propertiesMap")
} else {
parametersMap["properties"] = emptyMap<String, Any>()
}
// 提取required部分
val requiredPattern = "required=\\[([^\\]]+)\\]".toRegex()
val requiredMatch = requiredPattern.find(schemaStr)
if (requiredMatch != null) {
val requiredStr = requiredMatch.groupValues[1]
val requiredList = requiredStr.split(",").map { it.trim() }
parametersMap["required"] = requiredList
} else {
parametersMap["required"] = emptyList<String>()
}
} catch (e: Exception) {
Log.e(TAG, "从字符串解析schema细节失败: ${e.message}")
// 使用默认值
parametersMap["properties"] = emptyMap<String, Any>()
parametersMap["required"] = emptyList<String>()
}
} catch (e: Exception) {
Log.e(TAG, "解析工具输入模式失败: ${e.message}", e)
// 使用默认值继续
}
// 创建工具Map
toolMaps.add(mapOf(
"type" to "function",
"function" to mapOf(
"name" to tool.name(),
"description" to (tool.description() ?: ""),
"parameters" to parametersMap
)
))
Log.d(TAG, "工具 ${tool.name()} 成功添加到工具映射")
} catch (e: Exception) {
Log.e(TAG, "转换工具到Map失败: ${e.message}")
}
}
Log.d(TAG, "更新工具映射完成,共 ${toolMaps.size} 个工具")
}
/**
* 将JsonNode转换为Map
*/
private fun convertJsonNodeToMap(node: JsonNode): Any {
return when {
node.isTextual() -> node.asText()
node.isNumber() -> node.asDouble()
node.isBoolean() -> node.asBoolean()
node.isObject() -> {
val result = mutableMapOf<String, Any>()
node.fieldNames().forEach { fieldName ->
result[fieldName] = convertJsonNodeToMap(node.get(fieldName))
}
result
}
node.isArray() -> {
val result = mutableListOf<Any>()
for (i in 0 until node.size()) {
result.add(convertJsonNodeToMap(node.get(i)))
}
result
}
node.isNull() -> "null"
else -> node.toString()
}
}
/**
* 获取工具Map列表,用于OpenAI请求
*/
fun getToolMaps(): List<Map<String, Any>> {
return toolMaps
}
/**
* 获取可用的工具列表
*/
fun getTools(): List<ToolUnion> {
return tools
}
/**
* 检查是否包含指定工具
*/
fun containsTool(name: String): Boolean {
return toolMapping.containsKey(name)
}
/**
* 调用工具
*/
suspend fun callTool(name: String, arguments: Map<String, Any>): String? {
if (!isConnected) {
Log.e(TAG, "未连接到MCP服务器", null)
return "未连接到MCP服务器"
}
if (!containsTool(name)) {
Log.w(TAG, "此客户端不包含工具: $name")
return null
}
return try {
val result = mcp.callTool(
name = name,
arguments = arguments
)
// 处理工具调用结果
result?.content?.joinToString("\n") { content ->
when (content) {
is TextContent -> content.text ?: ""
else -> ""
}
}
} catch (e: Exception) {
Log.e(TAG, "调用MCP工具失败: ${e.message}", e)
"调用MCP工具失败: ${e.message}"
}
}
/**
* 检查是否已连接
*/
fun isConnected(): Boolean {
return isConnected
}
/**
* 关闭连接
*/
override fun close() {
runBlocking {
try {
mcp.close()
sseTransport?.close()
isConnected = false
Log.d(TAG, "已关闭MCP服务器连接")
} catch (e: Exception) {
Log.e(TAG, "关闭MCP服务器连接失败: ${e.message}", e)
}
}
}
}
/**
* MCP客户端,用于连接Model Context Protocol服务器和管理本地函数
*/
class MCPClient(private val context: Context? = null) : AutoCloseable {
private val TAG = "MCPClient"
// 本地函数Map,函数名 -> 处理器
private val localFunctions = mutableMapOf<String, FunctionHandler>()
// 本地函数定义Map,函数名 -> 定义
private val localFunctionDefs = mutableMapOf<String, String>()
// 子客户端列表,每个连接一个MCP服务器
private val subClients = mutableMapOf<String, MCPSubClient>()
// 是否已连接
private var isConnected = false
// 系统功能处理器
private val systemFunctionHandler = SystemFunctionHandler(context!!)
// 共享的HttpClient,用于所有子客户端
private val sharedHttpClient by lazy { createSslTrustAllClient() }
init {
// 如果提供了Context,自动注册语音功能处理器
context?.let { initializeVoiceFunctions(it) }
}
/**
* 初始化语音功能处理器
*/
private fun initializeVoiceFunctions(context: Context) {
try {
// 使用SystemFunctionHandler注册所有功能
systemFunctionHandler.registerAllFunctions(this)
Log.d(TAG, "语音功能初始化完成")
} catch (e: Exception) {
Log.e(TAG, "语音功能初始化失败: ${e.message}", e)
}
}
/**
* 连接到MCP服务器(通过SSE)
* 直接接收完整的JSON配置字符串
*
* @param mcpConfigJson 包含mcpServers字段的JSON配置字符串
* @return 是否连接成功
*/
suspend fun connectToSSE(mcpConfigJson: String): Boolean {
// 清除现有连接
closeAllConnections()
val serverMap = mutableMapOf<String, String>()
var configLoaded = false
// 尝试解析传入的JSON配置
try {
if (mcpConfigJson.isNotEmpty()) {
val mapper = ObjectMapper()
try {
val config = mapper.readValue(mcpConfigJson, Map::class.java)
// 处理mcpServers格式
@Suppress("UNCHECKED_CAST")
val mcpServers = config["mcpServers"] as? Map<String, Map<String, String>>
if (mcpServers != null && mcpServers.isNotEmpty()) {
// 从服务器配置中提取URL
mcpServers.forEach { (serverId, serverConfig) ->
val url = serverConfig["url"]
if (!url.isNullOrEmpty()) {
serverMap[serverId] = url
Log.d(TAG, "从配置中添加服务器 [$serverId]: $url")
}
}
configLoaded = serverMap.isNotEmpty()
Log.d(TAG, "从传入的JSON配置加载了 ${serverMap.size} 个服务器")
}
} catch (e: Exception) {
Log.e(TAG, "解析传入的JSON配置失败: ${e.message}")
}
}
} catch (e: Exception) {
Log.e(TAG, "处理传入的配置失败: ${e.message}")
}
// 创建并连接所有子客户端
val connectionResults = mutableMapOf<String, Boolean>()
for ((serverId, serverUrl) in serverMap) {
try {
Log.d(TAG, "为服务器 [$serverId],[$serverUrl] 创建子客户端")
val subClient = MCPSubClient(serverId, serverUrl, sharedHttpClient)
val connected = subClient.connect()
if (connected) {
subClients[serverId] = subClient
connectionResults[serverId] = true
Log.d(TAG, "子客户端 [$serverId] 连接成功")
} else {
subClient.close()
connectionResults[serverId] = false
Log.w(TAG, "子客户端 [$serverId] 连接失败")
}
} catch (e: Exception) {
Log.e(TAG, "创建子客户端 [$serverId] 失败: ${e.message}", e)
connectionResults[serverId] = false
}
}
// 检查是否有任何客户端连接成功
val successCount = connectionResults.count { it.value }
isConnected = successCount > 0
if (isConnected) {
Log.d(TAG, "已成功连接 $successCount/${serverMap.size} 个MCP服务器")
// 记录所有可用工具
val allToolNames = getAllToolNames()
Log.d(TAG, "所有可用工具: ${allToolNames.joinToString(", ")}")
return true
} else {
Log.e(TAG, "所有MCP服务器连接均失败")
return false
}
}
/**
* 关闭所有连接
*/
private fun closeAllConnections() {
subClients.forEach { (serverId, client) ->
try {
client.close()
Log.d(TAG, "已关闭子客户端 [$serverId]")
} catch (e: Exception) {
Log.e(TAG, "关闭子客户端 [$serverId] 失败: ${e.message}")
}
}
subClients.clear()
}
/**
* 获取所有工具名称
*/
private fun getAllToolNames(): List<String> {
val toolNames = mutableSetOf<String>()
// 添加本地函数
toolNames.addAll(localFunctions.keys)
// 添加所有子客户端的工具
subClients.forEach { (_, client) ->
client.getTools().forEach { toolUnion ->
val tool = toolUnion.tool().get()
toolNames.add(tool.name())
}
}
return toolNames.toList()
}
/** /**
* 注册本地函数 * 注册本地函数
@ -211,9 +639,6 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
// 保存函数定义 // 保存函数定义
localFunctionDefs[name] = ObjectMapper().writeValueAsString(functionDef) localFunctionDefs[name] = ObjectMapper().writeValueAsString(functionDef)
// 更新工具映射
updateToolMaps()
Log.d(TAG, "注册本地函数成功: $name") Log.d(TAG, "注册本地函数成功: $name")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
@ -231,128 +656,39 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
val removed = localFunctions.remove(name) != null val removed = localFunctions.remove(name) != null
if (removed) { if (removed) {
localFunctionDefs.remove(name) localFunctionDefs.remove(name)
updateToolMaps()
Log.d(TAG, "取消注册本地函数成功: $name") Log.d(TAG, "取消注册本地函数成功: $name")
} }
return removed return removed
} }
/** /**
* 更新工具Map列表,用于OpenAI请求 * 获取工具Map列表,用于OpenAI请求
*/ */
private fun updateToolMaps() { fun getToolMaps(): List<Map<String, Any>> {
toolMaps.clear() val allToolMaps = mutableListOf<Map<String, Any>>()
// 首先添加本地函数 // 首先添加本地函数
localFunctionDefs.forEach { (name, functionDefJson) -> localFunctionDefs.forEach { (name, functionDefJson) ->
try { try {
val mapper = ObjectMapper() val mapper = ObjectMapper()
val functionDef = mapper.readValue(functionDefJson, object : TypeReference<Map<String, Any>>() {}) val functionDef = mapper.readValue(functionDefJson, object : TypeReference<Map<String, Any>>() {})
val toolMap = mapOf( allToolMaps.add(mapOf(
"type" to "function", "type" to "function",
"function" to functionDef "function" to functionDef
) ))
toolMaps.add(toolMap)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "解析本地函数定义失败: $name, ${e.message}", e) Log.e(TAG, "解析本地函数定义失败: $name, ${e.message}")
} }
} }
// 然后添加MCP工具 // 然后添加所有子客户端的工具
tools.forEach { toolUnion -> subClients.forEach { (serverId, client) ->
try { val clientTools = client.getToolMaps()
val tool = toolUnion.tool().get() allToolMaps.addAll(clientTools)
val parametersMap = mutableMapOf<String, Any>() Log.d(TAG, "添加子客户端 [$serverId] 的 ${clientTools.size} 个工具")
// 提取工具类型及参数
try {
val inputSchema = tool.inputSchema()
// 解析JsonValue
val mapper = ObjectMapper()
val inputSchemaStr = inputSchema.toString()
val inputSchemaJson = mapper.readTree(inputSchemaStr)
val typeValue = inputSchemaJson.path("type")
parametersMap["type"] = if (typeValue.isTextual) typeValue.asText() else "object"
// 提取properties字段
val propertiesMap = mutableMapOf<String, Any>()
val propertiesNode = inputSchemaJson.path("properties")
if (propertiesNode.isObject) {
propertiesNode.fieldNames().forEach { fieldName ->
val fieldNode = propertiesNode.get(fieldName)
propertiesMap[fieldName] = convertJsonNodeToMap(fieldNode)
}
}
parametersMap["properties"] = propertiesMap
// 提取required字段
val requiredList = mutableListOf<String>()
val requiredNode = inputSchemaJson.path("required")
if (requiredNode.isArray) {
for (i in 0 until requiredNode.size()) {
if (requiredNode.get(i).isTextual) {
requiredList.add(requiredNode.get(i).asText())
}
}
}
parametersMap["required"] = requiredList
} catch (e: Exception) {
Log.e(TAG, "解析工具参数失败: ${e.message}", e)
// 设置默认参数
parametersMap["type"] = "object"
parametersMap["properties"] = emptyMap<String, Any>()
parametersMap["required"] = emptyList<String>()
}
// 创建工具Map
val toolMap = mapOf(
"type" to "function",
"function" to mapOf(
"name" to tool.name(),
"description" to (tool.description() ?: ""),
"parameters" to parametersMap
)
)
toolMaps.add(toolMap)
} catch (e: Exception) {
Log.e(TAG, "转换工具到Map失败: ${e.message}", e)
}
}
}
/**
* 将JsonNode转换为Map
*/
private fun convertJsonNodeToMap(node: JsonNode): Any {
return when {
node.isTextual() -> node.asText()
node.isNumber() -> node.asDouble()
node.isBoolean() -> node.asBoolean()
node.isObject() -> {
val result = mutableMapOf<String, Any>()
node.fieldNames().forEach { fieldName ->
result[fieldName] = convertJsonNodeToMap(node.get(fieldName))
}
result
}
node.isArray() -> {
val result = mutableListOf<Any>()
for (i in 0 until node.size()) {
result.add(convertJsonNodeToMap(node.get(i)))
}
result
}
node.isNull() -> "null"
else -> node.toString()
} }
}
return allToolMaps
/**
* 获取工具Map列表,用于OpenAI请求
*/
fun getToolMaps(): List<Map<String, Any>> {
return toolMaps
} }
/** /**
@ -365,7 +701,7 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
return ToolType.LOCAL_FUNCTION return ToolType.LOCAL_FUNCTION
} }
if (tools.any { it.tool().get().name() == name }) { if (subClients.any { it.value.containsTool(name) }) {
return ToolType.MCP_TOOL return ToolType.MCP_TOOL
} }
@ -422,40 +758,40 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
return "未连接到MCP服务器" return "未连接到MCP服务器"
} }
return try { // 查找包含此工具的子客户端
val result = mcp.callTool( val clientEntry = subClients.entries.find { it.value.containsTool(name) }
name = name,
arguments = arguments if (clientEntry != null) {
) val (serverId, client) = clientEntry
Log.d(TAG, "通过子客户端 [$serverId] 调用工具: $name")
// 处理工具调用结果 return client.callTool(name, arguments)
result?.content?.joinToString("\n") { content ->
when (content) {
is TextContent -> content.text ?: ""
else -> ""
}
}
} catch (e: Exception) {
Log.e(TAG, "调用MCP工具失败: ${e.message}", e)
"调用MCP工具失败: ${e.message}"
} }
// 没有找到包含此工具的客户端
Log.e(TAG, "没有子客户端包含工具: $name")
return "没有可用的服务器提供工具: $name"
} }
/** /**
* 获取可用的工具列表 * 获取可用的工具列表
*/ */
fun getTools(): List<ToolUnion> { fun getTools(): List<ToolUnion> {
return tools val allTools = mutableListOf<ToolUnion>()
// 收集所有子客户端的工具
subClients.forEach { (_, client) ->
allTools.addAll(client.getTools())
}
return allTools
} }
/** /**
* 检查工具是否存在 * 检查工具是否存在
*/ */
fun hasToolWithName(name: String): Boolean { fun hasToolWithName(name: String): Boolean {
return localFunctions.containsKey(name) || toolMaps.any { return localFunctions.containsKey(name) ||
val functionMap = it["function"] as? Map<String, Any> subClients.any { it.value.containsTool(name) }
functionMap?.get("name") == name
}
} }
/** /**
@ -483,14 +819,71 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
override fun close() { override fun close() {
runBlocking { runBlocking {
try { try {
mcp.close() closeAllConnections()
isConnected = false isConnected = false
tools = emptyList() Log.d(TAG, "已关闭所有MCP服务器连接")
toolMaps.clear()
Log.d(TAG, "已关闭MCP服务器连接", null)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "关闭MCP服务器连接失败: ${e.message}", e) Log.e(TAG, "关闭MCP服务器连接失败: ${e.message}", e)
} }
} }
} }
}
/**
* 创建一个信任所有SSL证书的HttpClient
*/
private fun createSslTrustAllClient(): HttpClient {
// 创建信任所有证书的TrustManager
val trustAllCerts = arrayOf<TrustManager>(
object : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
}
)
// 创建SSL上下文并初始化它
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, trustAllCerts, SecureRandom())
// 创建HTTP日志拦截器
val loggingInterceptor = HttpLoggingInterceptor { message ->
Log.d("OkHttp", message)
}.apply {
level = HttpLoggingInterceptor.Level.HEADERS
}
// 创建自定义拦截器显示实际请求URL
val urlLoggingInterceptor = Interceptor { chain ->
val request = chain.request()
Log.d("OkHttp-URL", "实际请求URL: ${request.url}")
Log.d("OkHttp-Headers", "请求头: ${request.headers}")
val response = chain.proceed(request)
Log.d("OkHttp-Response", "状态码: ${response.code} - ${response.message}")
Log.d("OkHttp-Response", "响应头: ${response.headers}")
response
}
// 创建OkHttpClient并配置信任所有证书
val okHttpClient = OkHttpClient.Builder()
.sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager)
.hostnameVerifier { _, _ -> true }
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(loggingInterceptor) // 添加HTTP日志拦截器
.addInterceptor(urlLoggingInterceptor) // 添加URL日志拦截器
.build()
// 创建使用OkHttp引擎的HttpClient
return HttpClient(OkHttp) {
engine {
preconfigured = okHttpClient
}
// 安装SSE插件
install(SSE)
}
} }

10
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt

@ -94,7 +94,7 @@ class OpenAIService(private val context: Context? = null) {
/** /**
* 初始化OpenAI服务 * 初始化OpenAI服务
*/ */
fun initialize(apiKey: String, baseUrl: String, model: String): Boolean { fun initialize(apiKey: String, baseUrl: String, model: String, mcpServer: String): Boolean {
this.apiKey = apiKey this.apiKey = apiKey
if (baseUrl.isNotEmpty()) { if (baseUrl.isNotEmpty()) {
this.baseUrl = baseUrl this.baseUrl = baseUrl
@ -107,7 +107,7 @@ class OpenAIService(private val context: Context? = null) {
if (mcpClient == null) { if (mcpClient == null) {
mcpClient = MCPClient(context) mcpClient = MCPClient(context)
} }
initializeMcpClient(mcpServer)
isInitialized = apiKey.isNotEmpty() isInitialized = apiKey.isNotEmpty()
return isInitialized return isInitialized
} }
@ -129,9 +129,9 @@ class OpenAIService(private val context: Context? = null) {
} }
/** /**
* 初始化MCP客户端(WebSocket方式) * 初始化MCP客户端
*/ */
fun initializeMcpClient(serverUrl: String): Boolean { fun initializeMcpClient(mcpServer: String): Boolean {
if (mcpClient != null) { if (mcpClient != null) {
mcpClient?.close() mcpClient?.close()
} }
@ -144,7 +144,7 @@ class OpenAIService(private val context: Context? = null) {
var result = false var result = false
scope.launch { scope.launch {
try { try {
result = mcpClient?.connectToWebSocket(serverUrl) ?: false result = mcpClient?.connectToSSE(mcpServer) ?: false
isMcpInitialized = result isMcpInitialized = result
Log.d(TAG, "MCP客户端初始化${if (result) "成功" else "失败"}") Log.d(TAG, "MCP客户端初始化${if (result) "成功" else "失败"}")
} catch (e: Exception) { } catch (e: Exception) {

3
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt

@ -73,8 +73,9 @@ class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
val apiKey = call.argument<String>("apiKey") ?: "" val apiKey = call.argument<String>("apiKey") ?: ""
val baseUrl = call.argument<String>("baseUrl") ?: "" val baseUrl = call.argument<String>("baseUrl") ?: ""
val model = call.argument<String>("model") ?: "" val model = call.argument<String>("model") ?: ""
val mcpServer = call.argument<String>("mcpServer") ?: ""
val initialized = openAIService.initialize(apiKey, baseUrl, model) val initialized = openAIService.initialize(apiKey, baseUrl, model, mcpServer)
result.success(initialized) result.success(initialized)
} }

357
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt

@ -0,0 +1,357 @@
package com.yunqiinnovation.open_ai_service.mcp
import android.util.Log
import io.ktor.client.*
import io.ktor.client.plugins.sse.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport
import kotlinx.coroutines.*
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.decodeFromString
import kotlin.properties.Delegates
import kotlin.time.Duration
import java.util.concurrent.atomic.AtomicBoolean
import org.json.JSONObject
/**
* 自定义SSE客户端传输层,修复原始SseClientTransport中的URL拼接问题
* 解决URL查询参数与路径拼接错误的问题,确保消息端点URL格式正确
*/
class CustomSseClientTransport(
private val client: HttpClient,
private val urlString: String?,
private val reconnectionTime: Duration? = null,
private val requestBuilder: HttpRequestBuilder.() -> Unit = {},
) : AbstractTransport() {
private val TAG = "CustomSseClientTransport"
private val scope by lazy {
CoroutineScope(session.coroutineContext + SupervisorJob())
}
// 使用Java标准库的AtomicBoolean替代kotlinx.atomicfu
private val initialized = AtomicBoolean(false)
private var session: ClientSSESession by Delegates.notNull()
private val endpoint = CompletableDeferred<String>()
private var job: Job? = null
// 创建JSON解析器,增强灵活性设置
private val json = Json {
ignoreUnknownKeys = true // 忽略未知字段
isLenient = true // 宽松解析模式
coerceInputValues = true // 尝试强制转换类型
encodeDefaults = true // 编码默认值
explicitNulls = false // 不要求显式null值
}
// 保存基础URL(不包含查询参数)和查询参数
private var baseUrlWithoutParams: String? = null
private var queryParams: Map<String, String> = emptyMap()
private var hostPart: String = "" // 添加类级别变量
private var pathPart: String = "" // 添加类级别变量
/**
* 解析URL,分离基础URL、路径和查询参数
* 返回三元组: (主机部分URL, 路径部分, 查询参数Map)
*/
private fun parseUrl(url: String): Triple<String, String, Map<String, String>> {
return try {
val params = mutableMapOf<String, String>()
// 确保URL有协议部分
var processedUrl = url.trim()
if (!processedUrl.startsWith("http://") && !processedUrl.startsWith("https://")) {
processedUrl = "https://$processedUrl"
Log.d(TAG, "添加默认协议: $processedUrl")
}
val urlObj = java.net.URL(processedUrl)
// 解析查询参数
if (urlObj.query != null) {
urlObj.query.split("&").forEach { param ->
val parts = param.split("=", limit = 2)
if (parts.size == 2) {
params[parts[0]] = parts[1]
}
}
}
// 构建主机部分URL(协议+主机+端口)
val port = if (urlObj.port == -1) "" else ":${urlObj.port}"
val hostUrl = "${urlObj.protocol}://${urlObj.host}$port"
// 路径部分
val path = urlObj.path
Triple(hostUrl, path, params)
} catch (e: Exception) {
Log.e(TAG, "解析URL失败: $url, ${e.message}")
Triple(url, "", emptyMap())
}
}
/**
* 收集SSE事件
*/
private suspend fun collectEvents() {
job = scope.launch(CoroutineName("CustomSseMcpClientTransport.collect#${hashCode()}")) {
session.incoming.collect { event ->
when (event.event) {
"error" -> {
val e = IllegalStateException("SSE error: ${event.data}")
Log.e(TAG, "SSE错误: ${event.data}")
_onError(e)
throw e
}
"open" -> {
Log.d(TAG, "SSE连接已打开")
// 连接已打开,等待endpoint事件
}
"endpoint" -> {
try {
val eventData = event.data ?: ""
Log.d(TAG, "收到endpoint事件: $eventData")
// 使用主机部分构建endpoint
val fullEndpoint = if (eventData.startsWith("/")) {
"$hostPart$eventData"
} else {
"$hostPart/$eventData"
}
Log.d(TAG, "构建的endpoint路径(不含参数): $fullEndpoint")
// 添加查询参数到endpoint
val endpointWithParams = if (queryParams.isNotEmpty()) {
// 检查endpoint是否已有查询参数
if (fullEndpoint.contains("?")) {
// 已有查询参数,添加&并附加其他参数
val queryString = queryParams.entries.joinToString("&") { "${it.key}=${it.value}" }
"$fullEndpoint&$queryString"
} else {
// 没有查询参数,添加?并附加参数
val queryString = queryParams.entries.joinToString("&") { "${it.key}=${it.value}" }
"$fullEndpoint?$queryString"
}
} else {
fullEndpoint
}
Log.d(TAG, "最终消息端点: $endpointWithParams")
endpoint.complete(endpointWithParams)
} catch (e: Exception) {
Log.e(TAG, "处理endpoint事件失败: ${e.message}", e)
_onError(e)
close()
error(e)
}
}
else -> {
try {
// 解析JSON-RPC消息
val data = event.data
if (data != null) {
Log.d(TAG, "收到事件数据: $data")
try {
// 尝试安全地解析JSON消息
safeParseMessage(data)
} catch (e: Exception) {
Log.e(TAG, "解析JSON-RPC消息失败: ${e.message}", e)
// 错误已记录,但不中断连接,只发送错误通知
_onError(e)
}
}
} catch (e: Exception) {
Log.e(TAG, "处理事件失败: ${e.message}", e)
_onError(e)
}
}
}
}
}
}
/**
* 安全解析JSON-RPC消息
*/
private suspend fun safeParseMessage(data: String) {
try {
// 先尝试使用标准解析
val message = json.decodeFromString<JSONRPCMessage>(data)
_onMessage(message)
} catch (e: Exception) {
// 如果标准解析失败,记录错误并尝试使用备用解析方式
Log.w(TAG, "标准解析失败,尝试备用解析: ${e.message}")
try {
// 尝试修复nextCursor缺失问题
if (e.message?.contains("nextCursor") == true) {
// 尝试手动添加缺失的nextCursor字段
val jsonObj = JSONObject(data)
// 只有在解析ListToolsResult时处理
if (data.contains("\"tools\"")) {
Log.d(TAG, "尝试修复ListToolsResult缺少nextCursor字段的问题")
// 手动解析result部分并添加nextCursor
val resultJson = try {
if (jsonObj.has("result")) {
val resultObj = jsonObj.getJSONObject("result")
if (!resultObj.has("nextCursor")) {
resultObj.put("nextCursor", "")
jsonObj.put("result", resultObj)
}
jsonObj.toString()
} else {
// 如果没有result字段,可能是其他类型的消息
data
}
} catch (ex: Exception) {
Log.e(TAG, "手动修复JSON失败: ${ex.message}")
data
}
// 重新尝试解析修复后的JSON
val fixedMessage = json.decodeFromString<JSONRPCMessage>(resultJson)
_onMessage(fixedMessage)
return
}
}
// 通用错误处理
Log.e(TAG, "无法解析消息,跳过: $data")
} catch (ex: Exception) {
Log.e(TAG, "备用解析也失败: ${ex.message}", ex)
// 不抛出异常,只记录错误
_onError(e)
}
}
}
/**
* 启动传输层
*/
override suspend fun start() {
if (!initialized.compareAndSet(false, true)) {
Log.e(TAG, "传输层已经启动,不能重复启动")
error("CustomSseClientTransport already started!")
}
// 解析URL和参数
if (urlString != null) {
// 解析URL,提取主机部分、路径部分和查询参数
val urlInfo = parseUrl(urlString)
hostPart = urlInfo.first
pathPart = urlInfo.second
queryParams = urlInfo.third
// 存储不带查询参数的基础URL(主机+路径)
baseUrlWithoutParams = hostPart + pathPart
Log.d(TAG, "原始URL: $urlString")
Log.d(TAG, "主机部分: $hostPart")
Log.d(TAG, "路径部分: $pathPart")
Log.d(TAG, "查询参数: $queryParams")
Log.d(TAG, "完整基础URL: $baseUrlWithoutParams")
}
// 创建SSE会话 - 直接使用原始URL,不添加/sse后缀
session = urlString?.let {
// 完整的SSE连接URL(主机部分+原始路径+查询参数)
val sseConnectUrl = if (queryParams.isNotEmpty()) {
// 如果路径已包含查询参数,就不再添加
if (pathPart.contains("?")) {
"$hostPart$pathPart"
} else {
val queryString = queryParams.entries.joinToString("&") { "${it.key}=${it.value}" }
"$hostPart$pathPart?$queryString"
}
} else {
"$hostPart$pathPart"
}
Log.d(TAG, "SSE连接URL: $sseConnectUrl")
client.sseSession(
urlString = sseConnectUrl,
reconnectionTime = reconnectionTime,
block = requestBuilder,
)
} ?: client.sseSession(
reconnectionTime = reconnectionTime,
block = requestBuilder,
)
// 收集SSE事件
collectEvents()
// 等待endpoint就绪
endpoint.await()
Log.d(TAG, "传输层启动完成,消息端点已就绪")
}
/**
* 发送消息
*/
@OptIn(ExperimentalCoroutinesApi::class)
override suspend fun send(message: JSONRPCMessage) {
if (!endpoint.isCompleted) {
Log.e(TAG, "发送失败: 未连接")
error("Not connected")
}
try {
val messageEndpoint = endpoint.getCompleted()
Log.d(TAG, "发送消息到: $messageEndpoint")
// 序列化消息
val jsonString = json.encodeToString(message)
val response = client.post(messageEndpoint) {
headers.append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
setBody(jsonString)
}
if (!response.status.isSuccess()) {
val text = response.bodyAsText()
Log.e(TAG, "发送消息失败: HTTP ${response.status}, $text")
error("Error POSTing to endpoint (HTTP ${response.status}): $text")
}
} catch (e: Exception) {
Log.e(TAG, "发送消息异常: ${e.message}", e)
_onError(e)
throw e
}
}
/**
* 关闭传输层
*/
override suspend fun close() {
if (!initialized.get()) {
Log.e(TAG, "关闭失败: 传输层未初始化")
error("CustomSseClientTransport is not initialized!")
}
session.cancel()
_onClose()
job?.cancelAndJoin()
Log.d(TAG, "传输层已关闭")
}
/**
* 检查传输层是否已初始化
*/
fun isInitialized(): Boolean {
return initialized.get()
}
}

3
local_plugins/open_ai_service/lib/open_ai_service.dart

@ -94,10 +94,12 @@ class OpenAIService {
/// [apiKey] OpenAI API密钥 /// [apiKey] OpenAI API密钥
/// [baseUrl] 可选,自定义API基础URL /// [baseUrl] 可选,自定义API基础URL
/// [model] 可选,自定义使用的模型 /// [model] 可选,自定义使用的模型
/// [mcpServer] 可选,MCP服务器地址配置
Future<bool> initialize({ Future<bool> initialize({
required String apiKey, required String apiKey,
String baseUrl = '', String baseUrl = '',
String model = '', String model = '',
String mcpServer = '',
}) async { }) async {
try { try {
final result = await _channel.invokeMethod<bool>( final result = await _channel.invokeMethod<bool>(
@ -106,6 +108,7 @@ class OpenAIService {
'apiKey': apiKey, 'apiKey': apiKey,
'baseUrl': baseUrl, 'baseUrl': baseUrl,
'model': model, 'model': model,
'mcpServer': mcpServer,
}, },
); );

1
pubspec.yaml

@ -91,3 +91,4 @@ flutter:
- assets/images/explore2.jpg - assets/images/explore2.jpg
- assets/lottie/ - assets/lottie/
- .env - .env
- .mcp.json

14
test.json

@ -16,9 +16,19 @@ curl 'https://ark.cn-beijing.volces.com/api/v3/bots/chat/completions' \
} }
] ]
}' }'
"map-qq": {
"url": "https://mcp.map.qq.com/sse?key=E5ABZ-RIACZ-DX2XC-7DEU2-AXR2Q-V7BWQ"
},
"amap-amap-sse": {
"url": "https://mcp.amap.com/sse?key=66b3f6199e3561f874a5a9cd9e9ed7f2"
},
// "map-qq": {
// "url": "https://mcp.map.qq.com?key=E5ABZ-RIACZ-DX2XC-7DEU2-AXR2Q-V7BWQ"
// },
"web-search": {
"url": "http://mcp.ideapsound.com:8000"
},
curl 'https://ark.cn-beijing.volces.com/api/v3/bots/chat/completions' \ curl 'https://ark.cn-beijing.volces.com/api/v3/bots/chat/completions' \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \

Loading…
Cancel
Save