From 84d3c255f164b7a3a56c0ecc39decf43c1b567e2 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Fri, 20 Jun 2025 11:46:01 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=8B=A8=E6=89=93=E7=94=B5?= =?UTF-8?q?=E8=AF=9D=E5=92=8C=E6=8F=90=E9=86=92=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E8=AF=9D=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yunqiinnovation/chat_api/MCPSubClient.kt | 24 +- .../chat_api/SystemFunctionHandler.kt | 301 +++++++++++++++--- .../com/yunqiinnovation/chat_api/Utils.kt | 96 +++++- 3 files changed, 367 insertions(+), 54 deletions(-) diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt index b7b3adbe2..4ea8d1b44 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt @@ -373,18 +373,18 @@ class MCPSubClient( isConnected = false stopHeartbeat() - if (retryCount < maxRetryAttempts) { - retryCount++ - currentReconnectDelay = minOf(currentReconnectDelay * 2, maxReconnectDelay) - - Log.w(TAG, "连接失败,将在 ${currentReconnectDelay}ms 后尝试重连 (尝试 $retryCount/$maxRetryAttempts)") - - delay(currentReconnectDelay) - connect() - } else { - Log.e(TAG, "已达到最大重试次数($maxRetryAttempts),停止重连") - // 可以在这里添加通知或回调,告知上层连接彻底失败 - } +// if (retryCount < maxRetryAttempts) { +// retryCount++ +// currentReconnectDelay = minOf(currentReconnectDelay * 2, maxReconnectDelay) +// +// Log.w(TAG, "连接失败,将在 ${currentReconnectDelay}ms 后尝试重连 (尝试 $retryCount/$maxRetryAttempts)") +// +// delay(currentReconnectDelay) +// connect() +// } else { +// Log.e(TAG, "已达到最大重试次数($maxRetryAttempts),停止重连") +// // 可以在这里添加通知或回调,告知上层连接彻底失败 +// } } /** * 关闭连接 diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt index 80820bece..517356ff9 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt @@ -1,12 +1,17 @@ package com.yunqiinnovation.chat_api +import android.Manifest +import android.content.ContentUris +import android.content.ContentValues import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.location.LocationManager import android.net.Uri import android.os.Build +import android.provider.AlarmClock import android.provider.CalendarContract +import android.provider.ContactsContract import android.telephony.SmsManager import android.util.Log import androidx.core.content.ContextCompat @@ -295,28 +300,108 @@ private class SendTextMessageHandler(private val context: Context?) : FunctionHa * 拨打电话处理器 */ private class MakePhoneCallHandler(private val context: Context?) : FunctionHandler { + + /** + * 根据联系人名称获取电话号码 + * @param contact 联系人名称或电话号码 + * @return 电话号码 + */ + private fun getPhoneNumberForContact(contact: String): String { + // 如果输入的是电话号码(简单判断,包含数字和特殊符号) + if (contact.matches(Regex("^[+\\d\\s\\-()]*\$")) && contact.any { it.isDigit() }) { + // 移除非数字字符 + return contact.replace(Regex("[^+\\d]"), "") + } + + // 检查读取联系人权限 + if (!checkPermissionAndGuide(context!!, Manifest.permission.READ_CONTACTS, "读取联系人", "访问联系人")) { + return "" + } + + // 从联系人数据库查询 + try { + val contentResolver = context.contentResolver + val uri = Uri.withAppendedPath( + ContactsContract.Contacts.CONTENT_FILTER_URI, + Uri.encode(contact) + ) + + val projection = arrayOf( + ContactsContract.Contacts._ID, + ContactsContract.Contacts.DISPLAY_NAME, + ContactsContract.Contacts.HAS_PHONE_NUMBER + ) + + contentResolver.query(uri, projection, null, null, null)?.use { cursor -> + if (cursor.moveToFirst()) { + val hasPhoneNumber = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.HAS_PHONE_NUMBER)) + + if (hasPhoneNumber > 0) { + val contactId = cursor.getString(cursor.getColumnIndexOrThrow( + ContactsContract.Contacts._ID)) + val phoneUri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI + val phoneProjection = arrayOf(ContactsContract.CommonDataKinds.Phone.NUMBER) + val phoneSelection = "${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?" + val phoneSelectionArgs = arrayOf(contactId) + + contentResolver?.query(phoneUri, phoneProjection, phoneSelection, phoneSelectionArgs, null)?.use { phoneCursor -> + if (phoneCursor.moveToFirst()) { + return phoneCursor.getString(phoneCursor.getColumnIndexOrThrow( + ContactsContract.CommonDataKinds.Phone.NUMBER)) + } + } + } + } + } + } catch (e: Exception) { + Log.e("ChatApi", "查询联系人信息失败: ${e.message}", e) + } + + return "" + } + override suspend fun handle(arguments: Map): String { val contact = arguments["contact"] as? String if (contact == null) { return "{\"result\": \"缺少联系人参数\"}" } - return try { - val callIntent = Intent(Intent.ACTION_DIAL).apply { - data = Uri.parse("tel:$contact") - flags = Intent.FLAG_ACTIVITY_NEW_TASK + // 检查拨打电话权限 + if (!checkPermissionAndGuide(context!!,Manifest.permission.CALL_PHONE, "拨打电话", "拨打电话")) { + "{\"result\": \"需要电话权限才能拨打电话,请在权限管理中开启电话权限\"}" } - - if (context?.packageManager?.queryIntentActivities(callIntent, 0)?.isNotEmpty() == true) { - context.startActivity(callIntent) - "{\"result\": \"已发起电话呼叫\"}" - } else { - "{\"result\": \"无法拨打电话\"}" + + val phoneNumber = getPhoneNumberForContact(contact) + if (phoneNumber.isEmpty()) { + "{\"result\": \"未找到联系人信息: $contact\"}" } + + // 创建拨号意图 + val intent = Intent(Intent.ACTION_CALL) + intent.data = Uri.parse("tel:$phoneNumber") + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context?.startActivity(intent) + "{\"result\": \"正在拨打${contact}的电话\"}" } catch (e: Exception) { - "{\"result\": \"拨打电话失败:${e.message}\"}" + Log.e("ChatApi", "拨打电话失败: ${e.message}", e) + "{\"result\": \"权限不足或者为查询到联系人\"}" } +// return try { +// val callIntent = Intent(Intent.ACTION_DIAL).apply { +// data = Uri.parse("tel:$contact") +// flags = Intent.FLAG_ACTIVITY_NEW_TASK +// } +// +// if (context?.packageManager?.queryIntentActivities(callIntent, 0)?.isNotEmpty() == true) { +// context.startActivity(callIntent) +// "{\"result\": \"已发起电话呼叫\"}" +// } else { +// "{\"result\": \"无法拨打电话\"}" +// } +// } catch (e: Exception) { +// "{\"result\": \"拨打电话失败:${e.message}\"}" +// } } } @@ -324,6 +409,140 @@ private class MakePhoneCallHandler(private val context: Context?) : FunctionHand * 设置提醒处理器 */ private class SetReminderHandler(private val context: Context?) : FunctionHandler { + + /** + * 获取默认日历ID + */ + private fun getDefaultCalendarId(): Long { + val projection = arrayOf(CalendarContract.Calendars._ID) + var calendarId = -1L + + try { + val uri = CalendarContract.Calendars.CONTENT_URI + val selection = "(${CalendarContract.Calendars.VISIBLE} = 1) AND (${CalendarContract.Calendars.IS_PRIMARY} = 1)" + context?.contentResolver?.query(uri, projection, selection, null, null)?.use { cursor -> + if (cursor.moveToFirst()) { + calendarId = cursor.getLong(0) + } else { + // 如果没有找到主日历,尝试找任何可见的日历 + context.contentResolver.query(uri, projection, "${CalendarContract.Calendars.VISIBLE} = 1", null, null)?.use { c -> + if (c.moveToFirst()) { + calendarId = c.getLong(0) + } + } + } + } + } catch (e: Exception) { + Log.e("CahtApi", "获取默认日历ID失败: ${e.message}", e) + } + + return calendarId + } + + /** + * 尝试创建闹钟提醒(备选方案) + */ + private fun tryCreateAlarm(title: String, content: String, timeStr: String): String { + try { + // 解析小时和分钟 + val timeParts = timeStr.split(" ")[1].split(":") + val hour = timeParts[0].toInt() + val minute = timeParts[1].toInt() + + val alarmIntent = Intent(AlarmClock.ACTION_SET_ALARM) + alarmIntent.putExtra(AlarmClock.EXTRA_MESSAGE, "$title: $content") + alarmIntent.putExtra(AlarmClock.EXTRA_HOUR, hour) + alarmIntent.putExtra(AlarmClock.EXTRA_MINUTES, minute) + alarmIntent.putExtra(AlarmClock.EXTRA_SKIP_UI, true) // 尝试跳过UI + alarmIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + + if (alarmIntent.resolveActivity(context!!.packageManager) != null) { + context.startActivity(alarmIntent) + return "{\"result\": \"已创建闹钟提醒: $title, 时间: $timeStr\"}" + } + } catch (e: Exception) { + Log.e("CahtApi", "创建闹钟失败: ${e.message}", e) + } + return "{\"result\": \"未找到可用的日历或闹钟应用\"}" + } + + /** + * 设置提醒 + * @param title 提醒标题 + * @param content 提醒内容(可选) + * @param timeStr 提醒时间,格式为"yyyy-MM-dd HH:mm" + * @return 操作结果 + */ + fun setReminder(title: String, content: String, timeStr: String): String { + Log.d("CahtApi", "设置提醒: $title, 时间: $timeStr") + + try { + // 解析时间字符串 + val time:Long = try { + val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm", java.util.Locale.getDefault()) + dateFormat.parse(timeStr)?.time ?: System.currentTimeMillis() + } catch (e: Exception) { + Log.e("CahtApi", "解析时间失败: $timeStr, ${e.message}") + return "{\"result\": \"提醒时间格式无效,请使用'yyyy-MM-dd HH:mm'格式\"}" + } + + // 检查日历权限 + if (!checkPermissionAndGuide(context!!,Manifest.permission.WRITE_CALENDAR, "日历写入", "设置提醒")) { + return "{\"result\": \"需要日历权限才能创建提醒,请在权限管理中开启日历权限\"}" + } + + // 直接使用ContentProvider写入日历 + val endTime = time + 3600000 // 默认1小时后结束 + + // 获取默认日历ID + val calendarId = getDefaultCalendarId() + if (calendarId == -1L) { + Log.w("CahtApi", "未找到默认日历") + return tryCreateAlarm(title, content, timeStr) + } + + // 创建事件 + val eventValues = ContentValues().apply { + put(CalendarContract.Events.CALENDAR_ID, calendarId) + put(CalendarContract.Events.TITLE, title) + put(CalendarContract.Events.DESCRIPTION, content) + put(CalendarContract.Events.DTSTART, time) + put(CalendarContract.Events.DTEND, endTime) + put(CalendarContract.Events.HAS_ALARM, 1) + put(CalendarContract.Events.EVENT_TIMEZONE, java.util.TimeZone.getDefault().id) + } + + val eventUri = context.contentResolver.insert(CalendarContract.Events.CONTENT_URI, eventValues) + if (eventUri == null) { + Log.e("CahtApi", "插入日历事件失败") + return tryCreateAlarm(title, content, timeStr) + } + + // 获取事件ID + val eventId = ContentUris.parseId(eventUri) + + // 添加提醒(15分钟前提醒) + val reminderValues = ContentValues().apply { + put(CalendarContract.Reminders.EVENT_ID, eventId) + put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT) + put(CalendarContract.Reminders.MINUTES, 15) // 提前15分钟提醒 + } + + val reminderUri = context.contentResolver.insert(CalendarContract.Reminders.CONTENT_URI, reminderValues) + if (reminderUri != null) { + return "{\"result\": \"已成功创建提醒: $title, 时间: $timeStr\"}" + } else { + // 仍然创建了事件,但没有提醒 + return "{\"result\": \"已创建日历事件,但未设置提醒: $title\"}" + } + } catch (e: Exception) { + Log.e("CahtApi", "设置提醒失败: ${e.message}", e) + return "{\"result\": \"设置提醒失败: ${e.message}\"}" + } + } + + + override suspend fun handle(arguments: Map): String { val title = arguments["title"] as? String val content = arguments["content"] as? String ?: "" @@ -332,36 +551,36 @@ private class SetReminderHandler(private val context: Context?) : FunctionHandle if (title == null || timeString == null) { return "{\"result\": \"缺少必要参数\"}" } - - return try { - // 解析时间 - val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()) - val date = formatter.parse(timeString) - - if (date == null) { - return "{\"result\": \"时间格式错误\"}" - } - - // 创建日历事件 - val calendarIntent = Intent(Intent.ACTION_INSERT).apply { - data = CalendarContract.Events.CONTENT_URI - putExtra(CalendarContract.Events.TITLE, title) - putExtra(CalendarContract.Events.DESCRIPTION, content) - putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, date.time) - putExtra(CalendarContract.EXTRA_EVENT_END_TIME, date.time + 60 * 60 * 1000) // 默认1小时 - putExtra(CalendarContract.Events.HAS_ALARM, 1) - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - - if (context?.packageManager?.queryIntentActivities(calendarIntent, 0)?.isNotEmpty() == true) { - context.startActivity(calendarIntent) - "{\"result\": \"提醒设置成功\"}" - } else { - "{\"result\": \"无法打开日历应用\"}" - } - } catch (e: Exception) { - "{\"result\": \"设置提醒失败:${e.message}\"}" - } + return setReminder(title,content,timeString) +// return try { +// // 解析时间 +// val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()) +// val date = formatter.parse(timeString) +// +// if (date == null) { +// return "{\"result\": \"时间格式错误\"}" +// } +// +// // 创建日历事件 +// val calendarIntent = Intent(Intent.ACTION_INSERT).apply { +// data = CalendarContract.Events.CONTENT_URI +// putExtra(CalendarContract.Events.TITLE, title) +// putExtra(CalendarContract.Events.DESCRIPTION, content) +// putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, date.time) +// putExtra(CalendarContract.EXTRA_EVENT_END_TIME, date.time + 60 * 60 * 1000) // 默认1小时 +// putExtra(CalendarContract.Events.HAS_ALARM, 1) +// flags = Intent.FLAG_ACTIVITY_NEW_TASK +// } +// +// if (context?.packageManager?.queryIntentActivities(calendarIntent, 0)?.isNotEmpty() == true) { +// context.startActivity(calendarIntent) +// "{\"result\": \"提醒设置成功\"}" +// } else { +// "{\"result\": \"无法打开日历应用\"}" +// } +// } catch (e: Exception) { +// "{\"result\": \"设置提醒失败:${e.message}\"}" +// } } } diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt index 0d1b90153..31b7bba05 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt @@ -1,5 +1,13 @@ package com.yunqiinnovation.chat_api +import android.app.ActivityManager +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.provider.Settings +import android.util.Log +import androidx.core.content.ContextCompat import io.ktor.client.* import io.ktor.client.engine.okhttp.* import io.ktor.client.plugins.sse.* @@ -54,4 +62,90 @@ fun createSslTrustAllClient(): HttpClient { */ fun createMcpHttpClient(): HttpClient { return createSslTrustAllClient() -} \ No newline at end of file +} + +/** + * 检查应用是否在前台运行 + * @return 应用是否在前台 + */ +fun isAppInForeground(context: Context,): Boolean { + try { + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val appProcesses = activityManager.runningAppProcesses ?: return false + val packageName = context.packageName + + for (appProcess in appProcesses) { + if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND + && appProcess.processName == packageName) { + return true + } + } + } catch (e: Exception) { + Log.e("ChatApi", "检查应用状态失败: ${e.message}", e) + } + return false +} +/** + * 检查权限并根据应用状态采取不同行为 + * @param permission 需要检查的权限 + * @param permissionName 权限名称(用于显示给用户) + * @param functionName 功能名称(用于日志和提示) + * @return 是否拥有权限 + */ +fun checkPermissionAndGuide(context: Context, permission: String, permissionName: String, functionName: String): Boolean { + if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) { + Log.w("ChatApi", "缺少${permissionName}权限,无法执行${functionName}") + + // 检查应用是否在前台 + if (isAppInForeground(context)) { + // 应用在前台,尝试直接弹出权限申请框 + try { + // 首先尝试发送广播通知DeepSound应用弹出权限申请对话框 + val intent = Intent("com.yunqiinnovation.deepsound.action.REQUEST_PERMISSION").apply { + setPackage(context.packageName) + putExtra("permission", permission) + putExtra("permissionName", permissionName) + putExtra("functionName", functionName) + addFlags(Intent.FLAG_RECEIVER_FOREGROUND) + } + context.sendBroadcast(intent) + + // 由于插件无法直接请求权限(需要Activity),上面的广播会让主应用来处理权限请求 + // 这里设置一个短暂延迟,让用户有时间响应权限请求 + Thread.sleep(500) + + // 再次检查权限是否已授予(以防用户立即授予) + if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED) { + return true + } + } catch (e: Exception) { + Log.e("ChatApi", "请求权限失败: ${e.message}", e) + } + + // 如果权限仍未授予,引导用户到权限管理页面 + try { + // 尝试跳转到应用内的权限管理页面 + val intent = Intent("com.yunqiinnovation.deepsound.action.PERMISSIONS_SETTINGS").apply { + setPackage(context.packageName) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + + if (intent.resolveActivity(context.packageManager) != null) { + context.startActivity(intent) + } else { + // 如果应用内权限页面不可用,则跳转到系统应用设置页面 + val settingsIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", context.packageName, null) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(settingsIntent) + } + } catch (e: Exception) { + Log.e("ChatApi", "打开权限设置页面失败: ${e.message}", e) + } + } + // 应用在后台或权限请求失败,只返回文字提示 + return false + } + return true +} \ No newline at end of file