Browse Source

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

weicu
tanlongsheng 1 year ago
parent
commit
533c74e13c
  1. 54
      lib/modules/agent/controllers/agent_controller.dart
  2. 126
      lib/modules/agent/views/agent_view.dart
  3. 190
      lib/modules/agent/widgets/full_screen_editor.dart
  4. 6
      lib/modules/profile/views/profile_view.dart
  5. 2
      lib/modules/settings/views/settings_view.dart
  6. 25
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  7. BIN
      local_plugins/agent_service/android/src/main/res/raw/await.mp3
  8. 1
      local_plugins/agent_service/ios/agent_service/Package.swift
  9. 68
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt
  10. 61
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt

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

@ -68,6 +68,13 @@ class AgentController extends GetxController {
final isImageInputActive = false.obs; // 是否处于图片输入模式 final isImageInputActive = false.obs; // 是否处于图片输入模式
final selectedImagePath = Rx<String?>(null); // 当前选择的图片路径 final selectedImagePath = Rx<String?>(null); // 当前选择的图片路径
// 输入状态控制 - 用于动态显示发送按钮
final hasTextInput = false.obs; // 是否有文本输入
// 全屏编辑相关状态
final isFullScreenEditMode = false.obs; // 是否处于全屏编辑模式
final shouldShowExpandButton = false.obs; // 是否显示展开按钮
// 流事件订阅 // 流事件订阅
StreamSubscription<AgentServiceEvent>? _eventSubscription; StreamSubscription<AgentServiceEvent>? _eventSubscription;
@ -107,6 +114,7 @@ class AgentController extends GetxController {
_subscribeToEvents(); //订阅代理服务事件 _subscribeToEvents(); //订阅代理服务事件
_initMusicStatus(); //初始化音乐状态 _initMusicStatus(); //初始化音乐状态
_setupMediaSessionCallbacks(); //设置媒体会话回调 _setupMediaSessionCallbacks(); //设置媒体会话回调
_setupTextInputListener(); //设置文本输入监听器
} }
// 初始化音乐播放状态 // 初始化音乐播放状态
@ -178,6 +186,52 @@ class AgentController extends GetxController {
} }
} }
// 设置文本输入监听器
void _setupTextInputListener() {
textController.addListener(() {
final text = textController.text.trim();
hasTextInput.value = text.isNotEmpty;
// 检查是否需要显示展开按钮
_checkShouldShowExpandButton();
});
}
// 检查是否需要显示展开按钮
void _checkShouldShowExpandButton() {
final text = textController.text;
if (text.isEmpty) {
shouldShowExpandButton.value = false;
return;
}
// 计算文本行数
final lines = text.split('\n');
final maxLines = isImageInputActive.value ? 3 : 5;
// 简单估算:如果换行符数量超过maxLines-1,或者单行文本过长,显示展开按钮
final hasMultipleLines = lines.length > maxLines;
final hasLongLine = lines.any((line) => line.length > 50); // 估算单行字符数
shouldShowExpandButton.value = hasMultipleLines || hasLongLine;
}
// 打开全屏编辑模式
void openFullScreenEdit() {
isFullScreenEditMode.value = true;
}
// 关闭全屏编辑模式
void closeFullScreenEdit() {
isFullScreenEditMode.value = false;
}
// 从全屏编辑模式发送消息
Future<void> sendMessageFromFullScreen() async {
await sendMessage();
closeFullScreenEdit();
}
// 加载聊天历史记录 // 加载聊天历史记录
Future<void> _loadChatHistory() async { Future<void> _loadChatHistory() async {
try { try {

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

@ -4,6 +4,7 @@ import 'package:get/get.dart';
import '../controllers/agent_controller.dart'; import '../controllers/agent_controller.dart';
import '../controllers/report_controller.dart'; import '../controllers/report_controller.dart';
import 'message_bubble.dart'; import 'message_bubble.dart';
import '../widgets/full_screen_editor.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
@ -287,8 +288,10 @@ class AgentView extends StatelessWidget {
} }
}), }),
// 输入控件行 // 输入控件行 - 使用IntrinsicHeight确保按钮与输入框底部对齐
Row( IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.end, // 所有元素底部对齐
children: [ children: [
// 键盘/语音切换按钮 // 键盘/语音切换按钮
Obx(() { Obx(() {
@ -325,10 +328,14 @@ class AgentView extends StatelessWidget {
final isTextMode = controller.isTextInputMode.value; final isTextMode = controller.isTextInputMode.value;
if (isTextMode) { if (isTextMode) {
// 文本输入模式 - 注意观察图片模式时的高度变化 // 文本输入模式 - 限制最大高度并只向上扩展
return Container( return Container(
//height: 44, constraints: const BoxConstraints(
padding: const EdgeInsets.symmetric(horizontal: 16), minHeight: 44, // 最小高度
maxHeight: 120, // 最大高度限制,防止挤压按钮
),
padding:
const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode color: isDarkMode
? Colors.grey[800] ? Colors.grey[800]
@ -343,6 +350,8 @@ class AgentView extends StatelessWidget {
), ),
), ),
child: Row( child: Row(
crossAxisAlignment:
CrossAxisAlignment.end, // 对齐到底部,实现向上扩展
children: [ children: [
Expanded( Expanded(
child: Obx(() { child: Obx(() {
@ -352,7 +361,8 @@ class AgentView extends StatelessWidget {
return TextField( return TextField(
controller: controller.textController, controller: controller.textController,
maxLines: isImageActive ? 2 : null, maxLines:
isImageActive ? 3 : 5, // 设置合理的最大行数
minLines: isImageActive ? 2 : 1, minLines: isImageActive ? 2 : 1,
keyboardType: keyboardType:
TextInputType.multiline, // 启用多行键盘 TextInputType.multiline, // 启用多行键盘
@ -383,24 +393,15 @@ class AgentView extends StatelessWidget {
), ),
), ),
contentPadding: EdgeInsets.symmetric( contentPadding: EdgeInsets.symmetric(
vertical: isImageActive ? 12 : 0), vertical: isImageActive
? 12
: 8), // 调整垂直内边距
), ),
onSubmitted: (_) => onSubmitted: (_) =>
controller.sendMessage(), controller.sendMessage(),
); );
}), }),
), ),
GestureDetector(
onTap: () => controller.sendMessage(),
child: Container(
padding: const EdgeInsets.all(6),
child: Icon(
Icons.send,
color: primaryColor,
size: 22,
),
),
),
], ],
), ),
); );
@ -482,19 +483,93 @@ class AgentView extends StatelessWidget {
const SizedBox(width: 8), const SizedBox(width: 8),
// 图片选择按钮 // 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮
Container( Obx(() {
final hasText = controller.hasTextInput.value;
final hasImage = controller.isImageInputActive.value;
final shouldShowSend = hasText || hasImage;
final shouldShowExpand =
controller.shouldShowExpandButton.value;
if (shouldShowSend) {
// 显示发送按钮(可能还有展开按钮)
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// 展开按钮(在发送按钮上方)
if (shouldShowExpand && hasText)
Padding(
padding: const EdgeInsets.only(bottom: 40),
child: GestureDetector(
onTap: () {
// 打开底部弹出编辑模式
controller.openFullScreenEdit();
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) =>
const FullScreenEditor(),
).then((_) {
// 弹出页关闭时的回调
controller.closeFullScreenEdit();
});
},
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[700]
: Colors.grey[300],
borderRadius: BorderRadius.circular(16),
),
child: Icon(
Icons.expand_less,
color: isDarkMode
? Colors.grey[300]
: Colors.grey[700],
size: 20,
),
),
),
),
// 发送按钮
GestureDetector(
onTap: () => controller.sendMessage(),
child: Container(
width: 44, width: 44,
height: 44, height: 44,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200], color: primaryColor,
borderRadius: BorderRadius.circular(22),
),
child: const Icon(
Icons.send,
color: Colors.white,
size: 22,
),
),
),
],
);
} else {
// 显示图片选择按钮
return Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[800]
: Colors.grey[200],
borderRadius: BorderRadius.circular(22), borderRadius: BorderRadius.circular(22),
), ),
child: PopupMenuButton<String>( child: PopupMenuButton<String>(
icon: Icon( icon: Icon(
Icons.add_photo_alternate_outlined, Icons.add_photo_alternate_outlined,
color: color: isDarkMode
isDarkMode ? Colors.grey[300] : Colors.grey[700], ? Colors.grey[300]
: Colors.grey[700],
size: 22, size: 22,
), ),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
@ -535,9 +610,12 @@ class AgentView extends StatelessWidget {
), ),
], ],
), ),
), );
}
}),
], ],
), ),
),
], ],
), ),
), ),

190
lib/modules/agent/widgets/full_screen_editor.dart

@ -0,0 +1,190 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../controllers/agent_controller.dart';
/// 底部弹出式文本编辑器组件
class FullScreenEditor extends StatefulWidget {
const FullScreenEditor({super.key});
@override
State<FullScreenEditor> createState() => _FullScreenEditorState();
}
class _FullScreenEditorState extends State<FullScreenEditor> {
late TextEditingController _textController;
late FocusNode _focusNode;
final AgentController controller = AgentController.to;
@override
void initState() {
super.initState();
// 使用与主界面相同的文本控制器,保持内容同步
_textController = controller.textController;
_focusNode = FocusNode();
// 延迟聚焦,确保界面完全加载后再聚焦
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNode.requestFocus();
});
}
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
final primaryColor = Theme.of(context).primaryColor;
final keyboardHeight = MediaQuery.of(context).viewInsets.bottom; // 获取键盘高度
return Container(
height: MediaQuery.of(context).size.height * 0.95, // 占屏幕高度的90%
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.r),
topRight: Radius.circular(20.r),
),
),
child: Stack(
children: [
// 主要内容区域
Column(
children: [
// 主要编辑区域
Expanded(
child: Container(
padding: EdgeInsets.fromLTRB(16.w, 60.w, 16.w, 16.w),
child: TextField(
controller: _textController,
focusNode: _focusNode,
maxLines: null,
expands: true,
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.newline,
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode ? Colors.white : Colors.black87,
height: 1.5,
),
decoration: InputDecoration(
hintText: '在这里输入你的消息...',
hintStyle: TextStyle(
color: isDarkMode ? Colors.white54 : Colors.grey[500],
fontSize: 16.sp,
),
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
),
),
),
// 底部操作区域
Container(
margin:
EdgeInsets.only(bottom: keyboardHeight), // 关键修改:添加键盘高度的边距
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[50],
border: Border(
top: BorderSide(
color: isDarkMode ? Colors.grey[700]! : Colors.grey[200]!,
width: 1,
),
),
),
child: Row(
children: [
// 字符计数
Expanded(
child: ValueListenableBuilder<TextEditingValue>(
valueListenable: _textController,
builder: (context, value, child) {
return Text(
'${value.text.length} 字符',
style: TextStyle(
color: isDarkMode
? Colors.white60
: Colors.grey[600],
fontSize: 14.sp,
),
);
},
),
),
// 发送按钮 - 保持与主界面一致的样式
Obx(() {
final hasText = controller.hasTextInput.value;
return GestureDetector(
onTap: hasText
? () async {
await controller.sendMessageFromFullScreen();
if (context.mounted) {
Navigator.of(context).pop();
}
}
: null,
child: Container(
width: 44.w,
height: 44.h,
decoration: BoxDecoration(
color: hasText
? primaryColor
: (isDarkMode
? Colors.grey[800]
: Colors.grey[300]),
borderRadius: BorderRadius.circular(22.r),
),
child: Icon(
Icons.send,
color: hasText
? Colors.white
: (isDarkMode
? Colors.grey[600]
: Colors.grey[500]),
size: 22.sp,
),
),
);
}),
],
),
),
],
),
// 右上角关闭按钮
Positioned(
top: 16.h,
right: 16.w,
child: GestureDetector(
onTap: () {
controller.closeFullScreenEdit();
Navigator.of(context).pop();
},
child: Container(
width: 32.w,
height: 32.h,
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
borderRadius: BorderRadius.circular(16.r),
),
child: Icon(
Icons.close,
color: isDarkMode ? Colors.white : Colors.black87,
size: 20.sp,
),
),
),
),
],
),
);
}
}

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

@ -327,7 +327,7 @@ class EditProfileView extends GetView<ProfileController> {
Row( Row(
children: [ children: [
Obx(() => CircleAvatar( Obx(() => CircleAvatar(
radius: 30.r, // 调整头像大小以适应行高 radius: 26.r, // 调整头像大小以适应行高
backgroundImage: controller.localAvatar.value.isNotEmpty backgroundImage: controller.localAvatar.value.isNotEmpty
? FileImage(File(controller.localAvatar.value)) ? FileImage(File(controller.localAvatar.value))
: (controller.userInfo.value.avatar.isNotEmpty : (controller.userInfo.value.avatar.isNotEmpty
@ -444,7 +444,7 @@ class EditProfileView extends GetView<ProfileController> {
borderRadius: BorderRadius.circular(16.r), borderRadius: BorderRadius.circular(16.r),
), ),
child: Icon( child: Icon(
Icons.edit, Icons.chevron_right,
size: 16.w, size: 16.w,
color: isDarkMode ? Colors.white70 : Colors.grey[500], color: isDarkMode ? Colors.white70 : Colors.grey[500],
), ),
@ -504,7 +504,7 @@ class EditProfileView extends GetView<ProfileController> {
child: TextField( child: TextField(
controller: nameController, controller: nameController,
autofocus: true, autofocus: true,
textAlign: TextAlign.right, // 右对齐,与原昵称显示保持一致 textAlign: TextAlign.center, // 右对齐,与原昵称显示保持一致
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: isDarkMode ? Colors.white70 : Colors.grey[600], color: isDarkMode ? Colors.white70 : Colors.grey[600],

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

@ -977,7 +977,7 @@ class SettingsView extends GetView<SettingsController> {
//------------测试使用标记 start---------------// //------------测试使用标记 start---------------//
SizedBox(height: 4.h), SizedBox(height: 4.h),
Text( Text(
'${Platform.operatingSystem}-test-${DateTime.now().month.toString().padLeft(2, '0')}${DateTime.now().day.toString().padLeft(2, '0')}', '${Platform.operatingSystem}-test-0626',
style: TextStyle( style: TextStyle(
fontSize: 12.sp, fontSize: 12.sp,
color: const Color.fromARGB(255, 143, 94, 94), color: const Color.fromARGB(255, 143, 94, 94),

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

@ -668,7 +668,7 @@ object AgentService : CoroutineScope {
try { try {
// 设置状态为正在流式输出 // 设置状态为正在流式输出
_isAiStreaming.set(true) _isAiStreaming.set(true)
audioPlayer?.playAudio(R.raw.await, true,0.3f)
// 使用历史记录作为上下文发送到OpenAI // 使用历史记录作为上下文发送到OpenAI
val responseBuilder = StringBuilder() val responseBuilder = StringBuilder()
var aiMetadata:String = "" var aiMetadata:String = ""
@ -736,6 +736,9 @@ object AgentService : CoroutineScope {
responseBuilder.append(token) responseBuilder.append(token)
if (speakResponse && broadcast) { if (speakResponse && broadcast) {
ttsService?.speakStream(token) ttsService?.speakStream(token)
if (token.length > 0){
audioPlayer?.stopAudio()
}
} }
if (broadcast){ if (broadcast){
// 发送流式回复token // 发送流式回复token
@ -783,7 +786,7 @@ object AgentService : CoroutineScope {
override fun onFunctionCall(call: JSONObject) { override fun onFunctionCall(call: JSONObject) {
try { try {
audioPlayer?.playAudio(R.raw.calling, true) // audioPlayer?.playAudio(R.raw.calling, true)
val name = call.getString("name") val name = call.getString("name")
sendEvent("function_call", mapOf( sendEvent("function_call", mapOf(
"name" to name, "name" to name,
@ -1120,8 +1123,10 @@ object AgentService : CoroutineScope {
/** /**
* 播放音频资源 * 播放音频资源
* @param resId 资源ID * @param resId 资源ID
* @param isLooping 是否循环播放
* @param volume 音量大小,范围0.0-1.0,默认1.0
*/ */
fun playAudio(resId: Int, isLooping: Boolean = false) { fun playAudio(resId: Int, isLooping: Boolean = false, volume: Float = 1.0f) {
try { try {
// 释放之前的资源 // 释放之前的资源
release() release()
@ -1133,21 +1138,17 @@ object AgentService : CoroutineScope {
setDataSource(fd.fileDescriptor, fd.startOffset, fd.length) setDataSource(fd.fileDescriptor, fd.startOffset, fd.length)
} }
// 播放完成后自动释放资源 this.isLooping = isLooping // 设置循环属性
setVolume(volume, volume) // 设置音量(左声道,右声道)
setOnCompletionListener { setOnCompletionListener {
if (!isLooping) {
release() release()
} }
}
// 准备并播放
prepare() prepare()
if (isLooping) {
start() start()
} else {
start()
setOnCompletionListener {
release()
}
}
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "播放音频资源异常: ${e.message}", e) Log.e(TAG, "播放音频资源异常: ${e.message}", e)

BIN
local_plugins/agent_service/android/src/main/res/raw/await.mp3

Binary file not shown.

1
local_plugins/agent_service/ios/agent_service/Package.swift

@ -31,6 +31,7 @@ let package = Package(
path: "Sources/agent_service", path: "Sources/agent_service",
resources: [ resources: [
.copy("Resources/calling.mp3"), .copy("Resources/calling.mp3"),
.copy("Resources/await.mp3"),
.copy("Resources/start.mp3"), .copy("Resources/start.mp3"),
.copy("Resources/stop.mp3") .copy("Resources/stop.mp3")
] ]

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

@ -25,6 +25,7 @@ class CustomSseClientTransport(
private val urlString: String?, private val urlString: String?,
private val reconnectionTime: Duration? = null, private val reconnectionTime: Duration? = null,
private val requestBuilder: HttpRequestBuilder.() -> Unit = {}, private val requestBuilder: HttpRequestBuilder.() -> Unit = {},
private val onConnectionLost: (() -> Unit)? = null
) : AbstractTransport() { ) : AbstractTransport() {
private val TAG = "CustomSseClientTransport" private val TAG = "CustomSseClientTransport"
@ -35,8 +36,10 @@ class CustomSseClientTransport(
private val initialized = AtomicBoolean(false) private val initialized = AtomicBoolean(false)
private var session: ClientSSESession by Delegates.notNull() private var session: ClientSSESession by Delegates.notNull()
private val endpoint = CompletableDeferred<String>() private val endpoint = CompletableDeferred<String>()
private val isConnected = AtomicBoolean(false)
private var job: Job? = null private var job: Job? = null
private var connectionMonitorJob: Job? = null
// 创建JSON解析器 // 创建JSON解析器
private val json = Json { private val json = Json {
@ -95,17 +98,22 @@ class CustomSseClientTransport(
*/ */
private suspend fun collectEvents() { private suspend fun collectEvents() {
job = scope.launch(CoroutineName("CustomSseMcpClientTransport.collect#${hashCode()}")) { job = scope.launch(CoroutineName("CustomSseMcpClientTransport.collect#${hashCode()}")) {
try {
session.incoming.collect { event -> session.incoming.collect { event ->
when (event.event) { when (event.event) {
"error" -> { "error" -> {
val e = IllegalStateException("SSE error: ${event.data}")
Log.e(TAG, "SSE错误: ${event.data}") Log.e(TAG, "SSE错误: ${event.data}")
_onError(e) isConnected.set(false)
throw e val exception = Exception("SSE Error: ${event.data}")
_onError(exception)
onConnectionLost?.invoke()
throw exception
} }
"open" -> { "open" -> {
// SSE连接已打开 // SSE连接已打开
Log.d(TAG, "SSE连接已打开")
isConnected.set(true)
} }
"ping" -> { "ping" -> {
// 心跳 // 心跳
@ -164,6 +172,45 @@ class CustomSseClientTransport(
} }
} }
} }
} catch (e: CancellationException) {
Log.d(TAG, "SSE事件收集被取消")
throw e
} catch (e: Exception) {
Log.e(TAG, "SSE连接异常断开: ${e.message}", e)
isConnected.set(false)
_onError(e)
onConnectionLost?.invoke()
throw e
}
}
// 启动连接监控
startConnectionMonitor()
}
/**
* 启动连接监控,定期检查连接状态
*/
private fun startConnectionMonitor() {
connectionMonitorJob = scope.launch {
while (isActive && isConnected.get()) {
try {
delay(10000) // 每10秒检查一次
// 检查session是否仍然活跃
if (session.coroutineContext[Job]?.isCancelled == true) {
Log.w(TAG, "检测到SSE会话已取消")
isConnected.set(false)
onConnectionLost?.invoke()
break
}
} catch (e: Exception) {
Log.e(TAG, "连接监控异常: ${e.message}", e)
isConnected.set(false)
onConnectionLost?.invoke()
break
}
}
} }
} }
@ -212,6 +259,8 @@ class CustomSseClientTransport(
// 等待endpoint就绪 // 等待endpoint就绪
endpoint.await() endpoint.await()
Log.d(TAG, "CustomSseClientTransport启动完成")
} }
/** /**
@ -245,6 +294,13 @@ class CustomSseClientTransport(
} }
} }
/**
* 检查连接状态
*/
fun isConnectionActive(): Boolean {
return isConnected.get() && session.coroutineContext[Job]?.isActive == true
}
/** /**
* 关闭传输层 * 关闭传输层
*/ */
@ -253,8 +309,14 @@ class CustomSseClientTransport(
Log.e(TAG, "关闭失败: 传输层未初始化") Log.e(TAG, "关闭失败: 传输层未初始化")
error("CustomSseClientTransport is not initialized!") error("CustomSseClientTransport is not initialized!")
} }
isConnected.set(false)
connectionMonitorJob?.cancel()
session.cancel() session.cancel()
_onClose() _onClose()
job?.cancelAndJoin() job?.cancelAndJoin()
connectionMonitorJob?.cancelAndJoin()
Log.d(TAG, "CustomSseClientTransport已关闭")
} }
} }

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

@ -44,13 +44,14 @@ class MCPSubClient(
private var mcpClient: Client? = null private var mcpClient: Client? = null
private var isConnected = false private var isConnected = false
private var availableTools = mutableListOf<Tool>() private var availableTools = mutableListOf<Tool>()
private var transport: CustomSseClientTransport? = null
/** /**
* 连接到MCP服务器 * 连接到MCP服务器
*/ */
suspend fun connect(): Boolean = connectionMutex.withLock { suspend fun connect(): Boolean = connectionMutex.withLock {
if (isConnected) return true if (isConnected) return true
Log.e(TAG, "[$serverId] 开始连接mcp服务器: $serverUrl")
return try { return try {
// 创建MCP客户端实例 // 创建MCP客户端实例
val client = Client( val client = Client(
@ -61,13 +62,20 @@ class MCPSubClient(
) )
// 根据URL类型选择传输方式 // 根据URL类型选择传输方式
val transport = when { val newTransport = when {
serverUrl.startsWith("http://") || serverUrl.startsWith("https://") -> { serverUrl.startsWith("http://") || serverUrl.startsWith("https://") -> {
// SSE传输 - 使用自定义的CustomSseClientTransport // SSE传输 - 使用自定义的CustomSseClientTransport
val mcpHttpClient = httpClient ?: createMcpHttpClient() val mcpHttpClient = httpClient ?: createMcpHttpClient()
CustomSseClientTransport( CustomSseClientTransport(
client = mcpHttpClient, client = mcpHttpClient,
urlString = serverUrl urlString = serverUrl,
onConnectionLost = {
// 连接断开回调
Log.w(TAG, "[$serverId] 检测到连接断开")
scope.launch {
handleConnectionLost()
}
}
) )
} }
else -> { else -> {
@ -76,8 +84,10 @@ class MCPSubClient(
} }
} }
transport = newTransport
// 连接到服务器 // 连接到服务器
client.connect(transport) client.connect(newTransport)
// 获取可用工具列表 // 获取可用工具列表
try { try {
@ -101,9 +111,9 @@ class MCPSubClient(
retryCount = 0 retryCount = 0
currentReconnectDelay = initialReconnectDelay currentReconnectDelay = initialReconnectDelay
// 启动心跳检测 // 启动心跳检测
startHeartbeat() // startHeartbeat()
Log.e(TAG, "[$serverId] 连接mcp服务器成功: $serverUrl")
true true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "[$serverId] MCP连接失败: ${e.message}", e) Log.e(TAG, "[$serverId] MCP连接失败: ${e.message}", e)
false false
@ -321,7 +331,15 @@ class MCPSubClient(
*/ */
suspend fun checkConnection(): Boolean { suspend fun checkConnection(): Boolean {
if (!isConnected) { if (!isConnected) {
Log.d(TAG, "当前未连接,尝试重新连接...") Log.d(TAG, "[$serverId] 当前未连接,尝试重新连接...")
return connect()
}
// 检查传输层连接状态
val transportActive = transport?.isConnectionActive() ?: false
if (!transportActive) {
Log.w(TAG, "[$serverId] 传输层连接已断开")
isConnected = false
return connect() return connect()
} }
@ -330,11 +348,34 @@ class MCPSubClient(
mcpClient?.ping() mcpClient?.ping()
return true return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "连接检查失败: ${e.message}") Log.e(TAG, "[$serverId] 连接检查失败: ${e.message}")
isConnected = false isConnected = false
return false return false
} }
} }
/**
* 处理连接断开事件
*/
private suspend fun handleConnectionLost() {
connectionMutex.withLock {
if (isConnected) {
Log.w(TAG, "[$serverId] 连接已断开,更新状态")
isConnected = false
stopHeartbeat()
// 可以在这里添加自动重连逻辑
// 或者通知上层应用连接已断开
}
}
}
/**
* 获取连接状态
*/
fun getConnectionStatus(): Boolean {
return isConnected && (transport?.isConnectionActive() ?: false)
}
/** /**
* 停止心跳检测 * 停止心跳检测
*/ */
@ -393,10 +434,14 @@ class MCPSubClient(
scope.launch { scope.launch {
connectionMutex.withLock { connectionMutex.withLock {
try { try {
stopHeartbeat()
mcpClient?.close() mcpClient?.close()
transport?.close()
mcpClient = null mcpClient = null
transport = null
isConnected = false isConnected = false
availableTools.clear() availableTools.clear()
Log.d(TAG, "[$serverId] MCP连接已关闭")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "[$serverId] 关闭MCP连接时出错: ${e.message}", e) Log.e(TAG, "[$serverId] 关闭MCP连接时出错: ${e.message}", e)
} }

Loading…
Cancel
Save