From e0b68ce33ccc401a940b979eb33bc21cc4ed4e78 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Thu, 10 Jul 2025 21:52:05 +0800 Subject: [PATCH 1/5] =?UTF-8?q?refactor(ui):=20=E4=BC=98=E5=8C=96=E9=80=9A?= =?UTF-8?q?=E8=AF=9D=E7=95=8C=E9=9D=A2=E7=9A=84=E5=B8=83=E5=B1=80=E5=92=8C?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E5=BC=8F=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将通话覆盖层改为透明,仅保留底部控制栏的背景 - 使用响应式单位(w/h/sp)替换固定尺寸 - 调整按钮的阴影和边框使用响应式单位 --- lib/modules/agent/views/agent_view.dart | 44 ++++++++++++--------- lib/modules/devices/views/devices_view.dart | 2 +- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index b93eb2fbf..ed13c720a 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -899,20 +899,28 @@ class _AgentViewState extends State with WidgetsBindingObserver { // 构建打电话模式覆盖层 Widget _buildPhoneCallOverlay(BuildContext context, bool isDarkMode) { return Container( - color: isDarkMode ? Colors.black : Colors.white, + color: Colors.transparent, // 设置整个覆盖层为透明 child: SafeArea( child: Column( children: [ - // 顶部标题栏 - _buildPhoneCallHeader(isDarkMode), + // 顶部标题栏 - 保持背景 + // Container( + // color: isDarkMode + // ? Colors.black.withOpacity(0.8) + // : Colors.white.withOpacity(0.8), + // child: _buildPhoneCallHeader(isDarkMode), + // ), - // 中间音频可视化区域 - Expanded( - child: _buildPhoneCallVisualization(isDarkMode), + // 中间音频可视化区域 - 完全透明 + const Expanded( + child: SizedBox(), // 空的占位符,完全透明 ), - // 底部控制按钮 - _buildPhoneCallControls(isDarkMode), + // 底部控制按钮 - 保持背景 + Container( + color: isDarkMode ? Colors.black : Colors.white, + child: _buildPhoneCallControls(isDarkMode), + ), ], ), ), @@ -1042,7 +1050,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { // 构建打电话模式控制按钮 Widget _buildPhoneCallControls(bool isDarkMode) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 24), + padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 24.h), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ @@ -1068,8 +1076,8 @@ class _AgentViewState extends State with WidgetsBindingObserver { // } // }, child: Container( - width: 80, - height: 80, + width: 80.w, + height: 80.h, decoration: BoxDecoration( shape: BoxShape.circle, color: isActive ? Colors.red : Colors.green, @@ -1077,15 +1085,15 @@ class _AgentViewState extends State with WidgetsBindingObserver { BoxShadow( color: (isActive ? Colors.red : Colors.green) .withOpacity(0.3), - blurRadius: 12, - spreadRadius: 2, + blurRadius: 12.r, + spreadRadius: 2.r, ), ], ), child: Icon( isActive ? Icons.call_end : Icons.call, color: Colors.white, - size: 36, + size: 36.sp, ), ), ); @@ -1115,20 +1123,20 @@ class _AgentViewState extends State with WidgetsBindingObserver { return GestureDetector( onTap: onTap, child: Container( - width: 56, - height: 56, + width: 56.w, + height: 56.h, decoration: BoxDecoration( shape: BoxShape.circle, color: backgroundColor ?? (isDarkMode ? Colors.grey[800] : Colors.grey[200]), - border: isActive ? Border.all(color: Colors.blue, width: 2) : null, + border: isActive ? Border.all(color: Colors.blue, width: 2.w) : null, ), child: Icon( icon, color: backgroundColor != null ? Colors.white : (isDarkMode ? Colors.white : Colors.black), - size: 24, + size: 24.sp, ), ), ); diff --git a/lib/modules/devices/views/devices_view.dart b/lib/modules/devices/views/devices_view.dart index cbabb4ba3..6b30d165b 100644 --- a/lib/modules/devices/views/devices_view.dart +++ b/lib/modules/devices/views/devices_view.dart @@ -1,6 +1,6 @@ import 'dart:ui'; -import 'package:deep_voice/modules/devices/controllers/devices_controller.dart'; +import '/modules/devices/controllers/devices_controller.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; From 85868dc0e88621b3de91b184872d8a0825b2be24 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Fri, 11 Jul 2025 10:23:35 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=94=BB=E5=83=8F=E4=BB=A5=E5=8F=8A=E5=AE=9A=E4=BD=8D=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/src/main/AndroidManifest.xml | 1 - lib/core/bindings/initial_binding.dart | 4 + lib/data/services/location_manager.dart | 2 +- lib/data/services/network/nw_api.dart | 2 +- lib/data/services/user_portrait.dart | 30 ++ .../controllers/profile_controller.dart | 9 +- lib/modules/profile/views/profile_view.dart | 66 +++ .../agent_service/android/build.gradle.kts | 1 + .../agent_service/AgentService.kt | 19 +- .../yunqiinnovation/agent_service/utils.kt | 11 +- .../chat_api/android/build.gradle.kts | 5 +- .../chat_api/SystemFunctionHandler.kt | 208 +++++++-- .../location_service/LocationService.kt | 434 +++++++++++++++--- .../location_service/LocationServicePlugin.kt | 39 +- .../lib/location_service.dart | 57 ++- local_plugins/user_portrait/.gitignore | 33 ++ local_plugins/user_portrait/.metadata | 33 ++ local_plugins/user_portrait/CHANGELOG.md | 3 + local_plugins/user_portrait/LICENSE | 1 + local_plugins/user_portrait/README.md | 15 + .../user_portrait/analysis_options.yaml | 4 + .../user_portrait/android/.gitignore | 9 + .../user_portrait/android/build.gradle | 66 +++ .../user_portrait/android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 3 + .../user_portrait/UserPortrait.kt | 90 ++++ .../user_portrait/UserPortraitPlugin.kt | 45 ++ local_plugins/user_portrait/ios/.gitignore | 38 ++ .../user_portrait/ios/user_portrait.podspec | 29 ++ .../ios/user_portrait/Package.swift | 32 ++ .../user_portrait/PrivacyInfo.xcprivacy | 14 + .../user_portrait/UserPortraitPlugin.swift | 19 + .../user_portrait/lib/user_portrait.dart | 26 ++ local_plugins/user_portrait/pubspec.yaml | 72 +++ .../user_portrait_method_channel_test.dart | 27 ++ .../test/user_portrait_test.dart | 29 ++ pubspec.yaml | 2 + 37 files changed, 1351 insertions(+), 128 deletions(-) create mode 100644 lib/data/services/user_portrait.dart create mode 100644 local_plugins/user_portrait/.gitignore create mode 100644 local_plugins/user_portrait/.metadata create mode 100644 local_plugins/user_portrait/CHANGELOG.md create mode 100644 local_plugins/user_portrait/LICENSE create mode 100644 local_plugins/user_portrait/README.md create mode 100644 local_plugins/user_portrait/analysis_options.yaml create mode 100644 local_plugins/user_portrait/android/.gitignore create mode 100644 local_plugins/user_portrait/android/build.gradle create mode 100644 local_plugins/user_portrait/android/settings.gradle create mode 100644 local_plugins/user_portrait/android/src/main/AndroidManifest.xml create mode 100644 local_plugins/user_portrait/android/src/main/kotlin/com/yunqiinnovation/user_portrait/UserPortrait.kt create mode 100644 local_plugins/user_portrait/android/src/main/kotlin/com/yunqiinnovation/user_portrait/UserPortraitPlugin.kt create mode 100644 local_plugins/user_portrait/ios/.gitignore create mode 100644 local_plugins/user_portrait/ios/user_portrait.podspec create mode 100644 local_plugins/user_portrait/ios/user_portrait/Package.swift create mode 100644 local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/PrivacyInfo.xcprivacy create mode 100644 local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/UserPortraitPlugin.swift create mode 100644 local_plugins/user_portrait/lib/user_portrait.dart create mode 100644 local_plugins/user_portrait/pubspec.yaml create mode 100644 local_plugins/user_portrait/test/user_portrait_method_channel_test.dart create mode 100644 local_plugins/user_portrait/test/user_portrait_test.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9b63b731d..b810dc7b4 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -112,7 +112,6 @@ diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index 231096b83..311a82feb 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -1,6 +1,7 @@ import 'package:deep_voice/data/services/location_manager.dart'; import 'package:deep_voice/data/services/music_manager.dart'; import 'package:deep_voice/data/services/navigation_manager.dart'; +import 'package:deep_voice/data/services/user_portrait.dart'; import 'package:get/get.dart'; import '../../data/services/meeting/meeting_upload_service.dart'; import '../../data/services/speech_factory.dart'; @@ -55,6 +56,9 @@ class InitialBinding extends Bindings { Get.lazyPut(() => MeetingUploadService(), fenix: true); + // 会议助手音频上传 + Get.lazyPut(() => UserportraitManager(), fenix: true); + // 杰理蓝牙服务 // // 语言管理器(需要最先初始化) diff --git a/lib/data/services/location_manager.dart b/lib/data/services/location_manager.dart index f72b4d588..cd63525a3 100644 --- a/lib/data/services/location_manager.dart +++ b/lib/data/services/location_manager.dart @@ -45,7 +45,7 @@ class LocationManager extends GetxService { } //定位 - Future> location() async { + Future> location() async { return await _musicService.location(); } } diff --git a/lib/data/services/network/nw_api.dart b/lib/data/services/network/nw_api.dart index 9a60f66f3..8feff1db5 100644 --- a/lib/data/services/network/nw_api.dart +++ b/lib/data/services/network/nw_api.dart @@ -1,5 +1,5 @@ class NWApi { // static const baseApi = "https://www.deapsound.com"; - static const baseApi = "https://api.ideapsound.com"; + static const baseApi = "https://api.deapsound.com"; // static const baseApi = "https://api.ideapsound.com"; } diff --git a/lib/data/services/user_portrait.dart b/lib/data/services/user_portrait.dart new file mode 100644 index 000000000..08372b638 --- /dev/null +++ b/lib/data/services/user_portrait.dart @@ -0,0 +1,30 @@ +import 'package:deep_voice/core/utils/logger.dart'; +import 'package:get/get.dart'; +import 'package:user_portrait/user_portrait.dart'; + +//音乐播放管理 +class UserportraitManager extends GetxService { + static UserportraitManager get to => Get.find(); + + // 音乐服务实例 + final UserPortrait _userPortrait = UserPortrait(); + + /// 初始化 + @override + void onInit() { + super.onInit(); + Logger.info('初始化权限请求'); + } + + /// 释放资源 + @override + void onClose() { + super.onClose(); + } + + //定位 + Future clean() async { + _userPortrait.clean(); + return; + } +} diff --git a/lib/modules/profile/controllers/profile_controller.dart b/lib/modules/profile/controllers/profile_controller.dart index 07ccad2d5..260b4ef73 100644 --- a/lib/modules/profile/controllers/profile_controller.dart +++ b/lib/modules/profile/controllers/profile_controller.dart @@ -1,3 +1,4 @@ +import 'package:deep_voice/data/services/user_portrait.dart'; import 'package:dio/dio.dart' as dio; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; @@ -30,7 +31,8 @@ class ProfileController extends GetxController { final RxBool hasEdits = false.obs; // 是否有编辑 final RxBool isUpdating = false.obs; // 是否正在更新用户信息 final RxBool isEditingNickname = false.obs; // 是否正在编辑昵称(内联编辑状态) - + //音乐播放管理 + final _userportraitManager = Get.find(); // 用于乐观更新的原始状态备份 String _originalName = ''; String _originalAvatar = ''; @@ -648,6 +650,11 @@ class ProfileController extends GetxController { } } + // 清理用户缓存 + Future clean() async { + await _userportraitManager.clean(); + } + // 退出登录 Future signOut() async { try { diff --git a/lib/modules/profile/views/profile_view.dart b/lib/modules/profile/views/profile_view.dart index a2ef560c4..dc61e98b4 100644 --- a/lib/modules/profile/views/profile_view.dart +++ b/lib/modules/profile/views/profile_view.dart @@ -730,6 +730,72 @@ class EditProfileView extends GetView { }, isDarkMode: isDarkMode, ), + Divider( + height: 1, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200]), + // 清理缓存 + _buildSimpleNavigationSetting( + title: 'clean'.tr, // 清理缓存 + icon: Icons.logout, + iconBgColor: isDarkMode + ? Colors.red[900]!.withOpacity(0.3) + : Colors.red[50]!, + iconColor: isDarkMode ? Colors.red[300]! : Colors.red[600]!, + titleColor: + isDarkMode ? Colors.red[300]! : Colors.red[600]!, + onTap: () { + // 退出登录确认对话框 + Get.dialog( + AlertDialog( + backgroundColor: + isDarkMode ? Colors.grey[900] : Colors.white, + title: Text( + 'clean'.tr, // 清理缓存 + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), + content: Text( + 'cleanTips'.tr, // 确定要退出当前账号吗? + style: TextStyle( + color: + isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + 'cancel'.tr, // 取消 + style: TextStyle( + color: isDarkMode + ? Colors.white70 + : Colors.black54, + ), + ), + ), + TextButton( + onPressed: () { + Get.back(); + controller.clean(); + }, + child: Text( + 'confirm'.tr, // 确定 + style: TextStyle( + color: isDarkMode + ? Colors.red[300] + : Colors.red[600], + ), + ), + ), + ], + ), + ); + }, + isDarkMode: isDarkMode, + ), Divider( height: 1, color: isDarkMode diff --git a/local_plugins/agent_service/android/build.gradle.kts b/local_plugins/agent_service/android/build.gradle.kts index d193bfbae..fc98a0bac 100644 --- a/local_plugins/agent_service/android/build.gradle.kts +++ b/local_plugins/agent_service/android/build.gradle.kts @@ -51,4 +51,5 @@ dependencies { add("compileOnly", project(":chat_api")) add("compileOnly", project(":location_service")) add("compileOnly", project(":bytedance_speech")) + add("compileOnly", project(":user_portrait")) } \ No newline at end of file diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index dcc5adb38..db2e75132 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -11,6 +11,7 @@ import com.yunqiinnovation.chat_api.StreamCallback import com.yunqiinnovation.chat_storage.ChatStorageHelper import com.yunqiinnovation.music_service.MusicServiceStarter import com.yunqiinnovation.amap_service.NavigationServiceHelper +import com.yunqiinnovation.user_portrait.UserPortrait import com.yunqiinnovation.location_service.LocationService import android.util.Log import kotlinx.coroutines.* @@ -166,6 +167,8 @@ object AgentService : CoroutineScope { this.chatApiService = ChatApiService(context) this.chatStorageHelper = ChatStorageHelper(context) this.audioPlayer = AudioPlayer(context) + //初始化用户画像 + UserPortrait.initialize(appContext); //初始化定位服务 LocationService.initialize(appContext) // 设置系统提示词 @@ -791,14 +794,16 @@ object AgentService : CoroutineScope { // 先添加系统提示词 if (systemPrompt.isNotEmpty()) { - val location = LocationService.getCachedLocation() ?: mapOf( - "user_location" to "定位未开启", - "user_lat" to "定位未开启", - "user_lng" to "定位未开启", - ) - val _systemPrompt = Utils.fillTemplate(systemPrompt, location) +// val location = LocationService.getCachedLocation() ?: mapOf( +// "user_location" to "定位未开启", +// "user_lat" to "定位未开启", +// "user_lng" to "定位未开启", +// ) + val mapdata = UserPortrait.getAll() + Log.d(TAG, "liwei--------- 用户画像: ${mapdata}") + val _systemPrompt = Utils.fillTemplate(systemPrompt, mapdata) messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt)) - // Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}") + Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}") } // 再添加历史消息 diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt index f9b3df85a..1d5d7fdf2 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt @@ -142,14 +142,11 @@ object Utils { } } - /** - * 替换字符串 变量 - * */ fun fillTemplate(tpl: String, data: Map): String { - var result = tpl - data.forEach { (k, v) -> - result = result.replace("{$k}", v) + val regex = Regex("\\{([a-zA-Z0-9_]+)\\}") + return regex.replace(tpl) { matchResult -> + val key = matchResult.groupValues[1] + data[key] ?: "未知" } - return result } } \ No newline at end of file diff --git a/local_plugins/chat_api/android/build.gradle.kts b/local_plugins/chat_api/android/build.gradle.kts index f9f8ddefc..d855ffd06 100644 --- a/local_plugins/chat_api/android/build.gradle.kts +++ b/local_plugins/chat_api/android/build.gradle.kts @@ -57,4 +57,7 @@ dependencies { // JSON处理 implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") implementation("com.google.code.gson:gson:2.10.1") -} \ No newline at end of file + + + add("compileOnly", project(":location_service")) +} \ No newline at end of file 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 984b48475..86ebda64e 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 @@ -18,6 +18,7 @@ import android.provider.Settings import android.telephony.SmsManager import android.util.Log import androidx.core.content.ContextCompat +import com.yunqiinnovation.location_service.LocationService import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED import org.json.JSONObject import java.text.SimpleDateFormat @@ -141,16 +142,16 @@ class SystemFunctionHandler(private val context: Context? = null) { ) // 注册获取当前位置函数 -// client.registerLocalFunction( -// name = "get_current_location", -// description = "获取当前地理位置", -// parameters = mapOf( -// "type" to "object", -// "properties" to emptyMap(), -// "required" to emptyList() -// ), -// handler = GetCurrentLocationHandler(context) -// ) + client.registerLocalFunction( + name = "get_current_location", + description = "获取当前地理位置", + parameters = mapOf( + "type" to "object", + "properties" to emptyMap(), + "required" to emptyList() + ), + handler = GetCurrentLocationHandler(context) + ) // 注册媒体播放功能 client.registerLocalFunction( @@ -269,6 +270,32 @@ class SystemFunctionHandler(private val context: Context? = null) { ), handler = MuteVolumeHandler(context) ) + // 设置最大音量 + client.registerLocalFunction( + name = "max_volume", + description = "设置最大音量", + parameters = mapOf( + "type" to "object", + "properties" to emptyMap(), + "required" to emptyList() + ), + handler = MaxVolumeHandler(context) + ) + + // 更新用户画像 + client.registerLocalFunction( + name = "set_user_profile_field", + description = "设置或更新用户画像中的某一个字段", + parameters = mapOf( + "type" to "object", + "properties" to mapOf( + "key" to mapOf("type" to "string","description" to "画像字段名,例如:age、tone、工作经历、device_preference"), + "value" to mapOf("type" to "string","description" to "字段对应的值"), + ), + "required" to listOf("key","value"), + ), + handler = UpdateUserPortraitHandler(context) + ) } /** @@ -1107,6 +1134,7 @@ private class GetCurrentTimeHandler : FunctionHandler { * 获取当前位置处理器 */ private class GetCurrentLocationHandler(private val context: Context?) : FunctionHandler { + override suspend fun handle(arguments: Map): String { if (context == null) { return JSONObject().apply { @@ -1116,9 +1144,9 @@ private class GetCurrentLocationHandler(private val context: Context?) : Functio }) }.toString() } - + val handler = context.let { SystemFunctionHandler(it) } - + // 检查位置权限 if (!handler.checkPermissionAndGuide(Manifest.permission.ACCESS_FINE_LOCATION, "位置信息", "获取位置")) { return JSONObject().apply { @@ -1128,36 +1156,84 @@ private class GetCurrentLocationHandler(private val context: Context?) : Functio }) }.toString() } + + // 获取缓存的位置数据 + val cachedLocation = LocationService.getCachedLocationData() - // 检查位置服务是否启用 - val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as? LocationManager - val isLocationEnabled = locationManager?.isProviderEnabled(LocationManager.GPS_PROVIDER) == true || - locationManager?.isProviderEnabled(LocationManager.NETWORK_PROVIDER) == true - - if (!isLocationEnabled) { - // 尝试引导用户打开位置服务 - try { - val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - context.startActivity(intent) - } catch (e: Exception) { - Log.e(SystemFunctionHandler.TAG, "打开位置设置失败: ${e.message}", e) + // 异步触发最新定位获取(不等待结果) + LocationService.requestLocationInBackground(object : LocationService.LocationCallback { + override fun onLocationSuccess(latitude: Double, longitude: Double, address: String) { + Log.d(SystemFunctionHandler.TAG, "后台定位成功,缓存已更新: $latitude, $longitude, $address") } + + override fun onLocationError(errorCode: Int, errorMessage: String) { + Log.w(SystemFunctionHandler.TAG, "后台定位失败: $errorCode - $errorMessage") + } + }) + + // 立即返回缓存数据(如果存在) + if (cachedLocation != null) { + val cacheAge = System.currentTimeMillis() - cachedLocation.timestamp + val cacheAgeMinutes = cacheAge / (60 * 1000) return JSONObject().apply { - put("error", JSONObject().apply { - put("code", "location_disabled") - put("message", "位置服务未启用,请打开GPS或网络定位") + put("result", JSONObject().apply { + put("latitude", cachedLocation.latitude) + put("longitude", cachedLocation.longitude) + put("address", cachedLocation.address) + put("source", "cache") + put("timestamp", cachedLocation.timestamp) + put("cacheAgeMinutes", cacheAgeMinutes) + put("note", "正在后台更新位置信息") }) }.toString() } - // 注意:这里简化了实现,实际应该获取真实位置 - // 在生产环境中应该使用 LocationManager 或 FusedLocationProviderClient 获取实际位置 - return JSONObject().apply { - put("result", "位置服务已启用,可以获取位置信息") - put("note", "需要实现实际的位置获取逻辑") - }.toString() + // 如果没有缓存,则同步获取一次 + return try { + kotlinx.coroutines.suspendCancellableCoroutine { continuation -> + LocationService.requestLocation(object : LocationService.LocationCallback { + override fun onLocationSuccess(latitude: Double, longitude: Double, address: String) { + Log.d(SystemFunctionHandler.TAG, "首次定位成功: $latitude, $longitude, $address") + + val result = JSONObject().apply { + put("result", JSONObject().apply { + put("latitude", latitude) + put("longitude", longitude) + put("address", address) + put("source", "gps") + put("timestamp", System.currentTimeMillis()) + put("note", "首次获取位置信息") + }) + }.toString() + + continuation.resumeWith(Result.success(result)) + } + + override fun onLocationError(errorCode: Int, errorMessage: String) { + Log.e(SystemFunctionHandler.TAG, "首次定位失败: $errorCode - $errorMessage") + + val result = JSONObject().apply { + put("error", JSONObject().apply { + put("code", "location_error") + put("message", "定位失败: $errorMessage") + put("errorCode", errorCode) + }) + }.toString() + + continuation.resumeWith(Result.success(result)) + } + }) + } + } catch (e: Exception) { + Log.e(SystemFunctionHandler.TAG, "获取位置时发生异常: ${e.message}", e) + JSONObject().apply { + put("error", JSONObject().apply { + put("code", "exception") + put("message", "获取位置时发生异常: ${e.message}") + }) + }.toString() + } } } @@ -1463,3 +1539,67 @@ private class MuteVolumeHandler(private val context: Context?) : FunctionHandler }.toString() } } + + +/** + * 添加音量处理类 + */ +private class MaxVolumeHandler(private val context: Context?) : FunctionHandler { + + //设置音量 + fun setMaxMusicVolume(context: Context) { + val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) + audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, maxVolume, AudioManager.FLAG_SHOW_UI) + } + + override suspend fun handle(arguments: Map): String { + if (context == null) { + return JSONObject().apply { + put("error", JSONObject().apply { + put("code", "context_error") + put("message", "上下文未初始化") + }) + }.toString() + } + + // 发送广播通知退出交互 + setMaxMusicVolume(context) + return JSONObject().apply { + put("result", "设置静音成功!") + put("broadcast", false) + }.toString() + } +} + + +/** + * 更新用户画像 + */ +private class UpdateUserPortraitHandler(private val context: Context?) : FunctionHandler { + override suspend fun handle(arguments: Map): String { + if (context == null) { + return JSONObject().apply { + put("error", JSONObject().apply { + put("code", "context_error") + put("message", "上下文未初始化") + }) + }.toString() + } + val key = arguments["key"]?.toString() ?: "未知" + val value = arguments["value"]?.toString() ?: "未知" + // 发送更新画像广播 + val intent = Intent("com.yunqiinnovation.user_portrait.UPDATE_PORTRAIT").apply { + putExtra("key", key) + putExtra("value", value) + setPackage(context.packageName) + } + context.sendBroadcast(intent) + // 发送广播通知退出交互 + return JSONObject().apply { + put("result", "更新成功!") + }.toString() + } +} + + diff --git a/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt index 8e25fdea2..73788b194 100644 --- a/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt +++ b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationService.kt @@ -1,32 +1,52 @@ package com.yunqiinnovation.location_service import android.Manifest +import android.app.Activity import android.app.ActivityManager import android.content.Context import android.content.pm.PackageManager -import android.location.Location import android.os.Build import android.os.Handler import android.os.Looper import android.util.Log +import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import com.amap.api.location.* import io.flutter.plugin.common.MethodChannel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch object LocationService { private const val TAG = "LocationService" - private var appcontext:Context? = null + private const val PERMISSION_REQUEST_CODE = 1001 + + private var appcontext: Context? = null private var locationClient: AMapLocationClient? = null - private var currentLocation: AMapLocation? = null private var initialized = false private var permissionRetryHandler: Handler? = null private var permissionRetryRunnable: Runnable? = null + + // 存储当前的定位请求回调(Flutter用) + private var currentLocationCallback: MethodChannel.Result? = null + // 存储权限请求回调(Flutter用) + private var permissionRequestCallback: MethodChannel.Result? = null + + // 存储服务间调用的回调 + private val serviceCallbacks = mutableListOf() + + /** + * 定位回调接口,供其他Android服务使用 + */ + interface LocationCallback { + fun onLocationSuccess(latitude: Double, longitude: Double, address: String) + fun onLocationError(errorCode: Int, errorMessage: String) + } /** * 检查应用是否在前台运行 - * @return 应用是否在前台 */ - fun isAppInForeground(context: Context,): Boolean { + fun isAppInForeground(context: Context): Boolean { try { val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager val appProcesses = activityManager.runningAppProcesses ?: return false @@ -39,53 +59,154 @@ object LocationService { } } } catch (e: Exception) { - Log.e("ChatApi", "检查应用状态失败: ${e.message}", e) + Log.e(TAG, "检查应用状态失败: ${e.message}", e) } return false } - fun initialize(context: Context) { if (initialized) return appcontext = context -// Log.e(TAG, "liwei--------- 初始化高德定位服务 1") + if (!checkPermissions(context)) { Log.w(TAG, "没有定位权限,等待用户授权中...") startPermissionMonitor() return } + try { - AMapLocationClient.updatePrivacyShow(context, true, true) AMapLocationClient.updatePrivacyAgree(context, true) -// Log.e(TAG, "liwei--------- 初始化高德定位服务 2") + locationClient = AMapLocationClient(context.applicationContext) val option = AMapLocationClientOption().apply { locationMode = AMapLocationClientOption.AMapLocationMode.Hight_Accuracy - isOnceLocation = false - interval = 30000L + isOnceLocation = true // 单次定位 + isNeedAddress = true // 需要地址信息 + httpTimeOut = 20000 // 设置超时时间 } -// Log.e(TAG, "liwei--------- 初始化高德定位服务 3") + locationClient?.setLocationOption(option) -// Log.e(TAG, "liwei--------- 初始化高德定位服务 4") + + // 设置定位监听器 locationClient?.setLocationListener { location -> - if (location != null && location.errorCode == 0) { - currentLocation = location - // Log.d(TAG, "liwei---------定位更新: ${location.latitude}, ${location.longitude}") - } else { - Log.w(TAG, "liwei---------定位失败: ${location?.errorCode} - ${location?.errorInfo}") - } + handleLocationResult(location) } -// Log.e(TAG, "liwei--------- 初始化高德定位服务 5") - locationClient?.startLocation() -// Log.e(TAG, "liwei--------- 初始化高德定位服务 6") + initialized = true + Log.d(TAG, "定位服务初始化完成") } catch (e: Exception) { Log.e(TAG, "高德定位初始化失败: ${e.message}", e) } } - + // 在LocationService object中添加缓存相关的属性和方法 + private var cachedLocationData: LocationData? = null + private const val CACHE_PREFS_NAME = "location_cache" + + /** + * 后台异步获取位置信息(不阻塞调用者) + * @param callback 定位结果回调 + */ + fun requestLocationInBackground(callback: LocationCallback) { + val ctx = appcontext + if (ctx == null) { + callback.onLocationError(-1, "定位服务未初始化") + return + } + + if (!checkPermissions(ctx)) { + callback.onLocationError(-2, "没有定位权限") + return + } + + if (!initialized) { + callback.onLocationError(-3, "定位服务未初始化") + return + } + + // 使用协程在后台执行 + CoroutineScope(Dispatchers.IO).launch { + try { + // 添加到回调列表 + serviceCallbacks.add(callback) + + // 如果没有正在进行的定位请求,开始定位 + if (currentLocationCallback == null && serviceCallbacks.size == 1) { + locationClient?.startLocation() + } + } catch (e: Exception) { + callback.onLocationError(-4, "启动后台定位失败: ${e.message}") + } + } + } + + // 修改handleLocationResult方法,添加缓存逻辑 + private fun handleLocationResult(location: AMapLocation?) { + // 处理Flutter回调 + val flutterCallback = currentLocationCallback + currentLocationCallback = null + + if (location != null && location.errorCode == 0) { + Log.d(TAG, "定位成功: ${location.latitude}, ${location.longitude}") + + // 缓存位置数据 + val locationData = LocationData( + latitude = location.latitude, + longitude = location.longitude, + address = location.address ?: "未知地址", + timestamp = System.currentTimeMillis() + ) + cachedLocationData = locationData + saveCachedLocationToPrefs(locationData) + + // 通知Flutter + if (flutterCallback != null) { + val result = mapOf( + "user_location" to (location.address ?: "未知地址"), + "user_lat" to location.latitude.toString(), + "user_lng" to location.longitude.toString() + ) + flutterCallback.success(result) + } + + // 通知其他Android服务 + serviceCallbacks.forEach { callback -> + try { + callback.onLocationSuccess( + location.latitude, + location.longitude, + location.address ?: "未知地址" + ) + } catch (e: Exception) { + Log.e(TAG, "服务回调失败: ${e.message}", e) + } + } + + } else { + Log.w(TAG, "定位失败: ${location?.errorCode} - ${location?.errorInfo}") + + // 通知Flutter + if (flutterCallback != null) { + flutterCallback.error("LOCATION_ERROR", "定位失败: ${location?.errorInfo ?: "未知错误"}", null) + } + + // 通知其他Android服务 + serviceCallbacks.forEach { callback -> + try { + callback.onLocationError( + location?.errorCode ?: -1, + location?.errorInfo ?: "未知错误" + ) + } catch (e: Exception) { + Log.e(TAG, "服务回调失败: ${e.message}", e) + } + } + } + + // 清空服务回调列表(单次定位) + serviceCallbacks.clear() + } private fun startPermissionMonitor() { if (permissionRetryHandler != null) return @@ -98,7 +219,7 @@ object LocationService { if (hasPermission && !initialized) { Log.d(TAG, "权限已授予,重新初始化定位服务") initialize(ctx) - stopPermissionMonitor() // 停止定时器 + stopPermissionMonitor() } else { permissionRetryHandler?.postDelayed(this, 5000) } @@ -114,10 +235,10 @@ object LocationService { } private fun checkPermissions(context: Context): Boolean { - if (isAppInForeground(context)) { - return hasForegroundLocationPermission(context) - }else{ - return hasBackgroundLocationPermission(context) + return if (isAppInForeground(context)) { + hasForegroundLocationPermission(context) + } else { + hasBackgroundLocationPermission(context) } } @@ -139,58 +260,237 @@ object LocationService { } } + /** + * 检查定位权限状态(Flutter用) + */ + fun checkLocationPermission(context: Context, result: MethodChannel.Result) { + val hasPermission = checkPermissions(context) + result.success(mapOf( + "hasPermission" to hasPermission, + "hasForegroundPermission" to hasForegroundLocationPermission(context), + "hasBackgroundPermission" to hasBackgroundLocationPermission(context) + )) + } - fun getCachedLocation(): Map? { -// // 定位成功,获取经纬度 -// val latitude = location.latitude -// val longitude = location.longitude -// -// // 获取结构化地址 -// val province = location.province // 省 -// val city = location.city // 市 -// val district = location.district // 区县 -// val street = location.street // 街道 -// val streetNum = location.streetNum // 门牌号 -// val adCode = location.adCode // 行政区划代码 -// val address = location.address // 详细地址拼接字符串 - if (!initialized){ - if (checkPermissions(appcontext!!)) { - return mapOf( - "user_location" to "当前定位权限为获取", - "user_lat" to "当前定位权限为获取", - "user_lng" to "当前定位权限为获取", - ) - }else{ - return mapOf( - "user_location" to "后台服务下,需要后台定位权限", - "user_lat" to "后台服务下,需要后台定位权限", - "user_lng" to "后台服务下,需要后台定位权限", - ) - } + /** + * 请求定位权限(Flutter用) + */ + fun requestLocationPermission(activity: Activity?, result: MethodChannel.Result) { + if (activity == null) { + result.error("NO_ACTIVITY", "无法获取Activity实例", null) + return + } + + val ctx = appcontext ?: activity + + // 如果已经有权限,直接返回成功 + if (checkPermissions(ctx)) { + result.success(mapOf("granted" to true)) + return } - return currentLocation?.let { - mapOf( - "user_location" to it.address, - "user_lat" to it.latitude.toString(), - "user_lng" to it.longitude.toString(), + // 保存回调 + permissionRequestCallback = result + + // 请求权限 + val permissions = mutableListOf() + + if (!hasForegroundLocationPermission(ctx)) { + permissions.add(Manifest.permission.ACCESS_FINE_LOCATION) + permissions.add(Manifest.permission.ACCESS_COARSE_LOCATION) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !hasBackgroundLocationPermission(ctx)) { + permissions.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION) + } + + if (permissions.isNotEmpty()) { + ActivityCompat.requestPermissions( + activity, + permissions.toTypedArray(), + PERMISSION_REQUEST_CODE ) + } else { + result.success(mapOf("granted" to true)) } } - fun getCurrentLocation(result: MethodChannel.Result) { - val cached = getCachedLocation() - if (cached != null) { - result.success(cached) + + /** + * 处理权限请求结果(Flutter用) + */ + fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ): Boolean { + if (requestCode != PERMISSION_REQUEST_CODE) { + return false + } + + val callback = permissionRequestCallback + permissionRequestCallback = null + + if (callback == null) { + return true + } + + val ctx = appcontext + if (ctx == null) { + callback.error("NO_CONTEXT", "上下文丢失", null) + return true + } + + val hasPermission = checkPermissions(ctx) + + if (hasPermission) { + // 权限获取成功,重新初始化定位服务 + if (!initialized) { + initialize(ctx) + } + callback.success(mapOf("granted" to true)) } else { - result.error("NO_LOCATION", "当前位置还未获取", null) + callback.success(mapOf("granted" to false)) } + + return true } + /** + * 按需获取当前位置(Flutter用) + */ + fun getCurrentLocation(result: MethodChannel.Result) { + val ctx = appcontext + if (ctx == null) { + result.error("NOT_INITIALIZED", "定位服务未初始化", null) + return + } + + if (!checkPermissions(ctx)) { + result.error("NO_PERMISSION", "需要定位权限", null) + return + } + + if (!initialized) { + result.error("NOT_INITIALIZED", "定位服务未初始化", null) + return + } + + if (currentLocationCallback != null) { + result.error("LOCATION_IN_PROGRESS", "正在定位中,请稍后再试", null) + return + } + + // 保存回调,开始定位 + currentLocationCallback = result + locationClient?.startLocation() + } + + // ========== 以下是供其他Android服务使用的接口 ========== + + /** + * 检查是否有定位权限(供其他服务使用) + */ + fun hasLocationPermission(): Boolean { + val ctx = appcontext ?: return false + return checkPermissions(ctx) + } + + /** + * 获取当前位置(供其他Android服务使用) + * @param callback 定位结果回调 + */ + fun requestLocation(callback: LocationCallback) { + val ctx = appcontext + if (ctx == null) { + callback.onLocationError(-1, "定位服务未初始化") + return + } + + if (!checkPermissions(ctx)) { + callback.onLocationError(-2, "没有定位权限") + return + } + + if (!initialized) { + callback.onLocationError(-3, "定位服务未初始化") + return + } + + // 添加到回调列表 + serviceCallbacks.add(callback) + + // 如果没有正在进行的定位请求,开始定位 + if (currentLocationCallback == null && serviceCallbacks.size == 1) { + locationClient?.startLocation() + } + } + + /** + * 同步获取缓存的位置信息(供其他服务使用) + * 注意:这个方法返回的可能是过期的位置信息 + */ + fun getCachedLocationData(): LocationData? { + // 优先返回内存中的缓存 + if (cachedLocationData != null) { + return cachedLocationData + } + + // 从SharedPreferences中读取缓存 + val ctx = appcontext ?: return null + return loadCachedLocationFromPrefs(ctx) + } + + private fun saveCachedLocationToPrefs(locationData: LocationData) { + val ctx = appcontext ?: return + try { + val prefs = ctx.getSharedPreferences(CACHE_PREFS_NAME, Context.MODE_PRIVATE) + prefs.edit().apply { + putString("latitude", locationData.latitude.toString()) + putString("longitude", locationData.longitude.toString()) + putString("address", locationData.address) + putLong("timestamp", locationData.timestamp) + apply() + } + } catch (e: Exception) { + Log.e(TAG, "保存位置缓存失败: ${e.message}", e) + } + } + + private fun loadCachedLocationFromPrefs(context: Context): LocationData? { + return try { + val prefs = context.getSharedPreferences(CACHE_PREFS_NAME, Context.MODE_PRIVATE) + val latitude = prefs.getString("latitude", null)?.toDoubleOrNull() ?: return null + val longitude = prefs.getString("longitude", null)?.toDoubleOrNull() ?: return null + val address = prefs.getString("address", null) ?: return null + val timestamp = prefs.getLong("timestamp", 0L) + + if (timestamp == 0L) return null + + LocationData(latitude, longitude, address, timestamp) + } catch (e: Exception) { + Log.e(TAG, "读取位置缓存失败: ${e.message}", e) + null + } + } + + /** + * 位置数据类 + */ + data class LocationData( + val latitude: Double, + val longitude: Double, + val address: String, + val timestamp: Long + ) + fun stop() { locationClient?.stopLocation() locationClient?.onDestroy() locationClient = null - currentLocation = null + currentLocationCallback = null + permissionRequestCallback = null + serviceCallbacks.clear() initialized = false + stopPermissionMonitor() } } diff --git a/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt index b34b48f24..4c23455b1 100644 --- a/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt +++ b/local_plugins/location_service/android/src/main/kotlin/com/yunqiinnovation/location_service/LocationServicePlugin.kt @@ -3,16 +3,20 @@ package com.yunqiinnovation.location_service import android.content.Context import android.util.Log import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result +import io.flutter.plugin.common.PluginRegistry /** LocationServicePlugin */ -class LocationServicePlugin: FlutterPlugin, MethodCallHandler { +class LocationServicePlugin: FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegistry.RequestPermissionsResultListener { private val TAG = "LocationServicePlugin" private lateinit var context: Context private lateinit var channel : MethodChannel + private var activityBinding: ActivityPluginBinding? = null override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { context = flutterPluginBinding.applicationContext @@ -24,9 +28,40 @@ class LocationServicePlugin: FlutterPlugin, MethodCallHandler { Log.d(TAG, "Method call: ${call.method}") when (call.method) { "location" -> LocationService.getCurrentLocation(result) + "requestPermission" -> LocationService.requestLocationPermission(activityBinding?.activity, result) + "checkPermission" -> LocationService.checkLocationPermission(context, result) else -> result.notImplemented() } } - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {} + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + activityBinding = binding + binding.addRequestPermissionsResultListener(this) + LocationService.initialize(context) + } + + override fun onDetachedFromActivityForConfigChanges() { + onDetachedFromActivity() + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + onAttachedToActivity(binding) + } + + override fun onDetachedFromActivity() { + activityBinding?.removeRequestPermissionsResultListener(this) + activityBinding = null + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ): Boolean { + return LocationService.onRequestPermissionsResult(requestCode, permissions, grantResults) + } } diff --git a/local_plugins/location_service/lib/location_service.dart b/local_plugins/location_service/lib/location_service.dart index a760675ce..1e83bc0d0 100644 --- a/local_plugins/location_service/lib/location_service.dart +++ b/local_plugins/location_service/lib/location_service.dart @@ -14,11 +14,56 @@ class LocationService { // 内部构造函数 LocationService._internal() {} - Future> location() async { - final result = await _methodChannel.invokeMethod('location'); - return { - 'latitude': result?['longitude'] ?? 0.0, - 'longitude': result?['longitude'] ?? 0.0, - }; + /// 检查定位权限 + Future> checkPermission() async { + try { + final result = await _methodChannel.invokeMethod('checkPermission'); + return { + 'hasPermission': result?['hasPermission'] ?? false, + 'hasForegroundPermission': result?['hasForegroundPermission'] ?? false, + 'hasBackgroundPermission': result?['hasBackgroundPermission'] ?? false, + }; + } catch (e) { + throw Exception('检查权限失败: $e'); + } + } + + /// 请求定位权限 + Future requestPermission() async { + try { + final result = await _methodChannel.invokeMethod( + 'requestPermission', + ); + return result?['granted'] ?? false; + } catch (e) { + throw Exception('请求权限失败: $e'); + } + } + + /// 获取当前位置(按需获取) + Future> location() async { + try { + // 先检查权限 + final permissionStatus = await checkPermission(); + if (!permissionStatus['hasPermission']!) { + // 请求权限 + final granted = await requestPermission(); + if (!granted) { + throw Exception('用户拒绝了定位权限'); + } + } + + final result = await _methodChannel.invokeMethod('location'); + if (result != null) { + return { + 'latitude': double.tryParse(result['user_lat'] ?? '0.0') ?? 0.0, + 'longitude': double.tryParse(result['user_lng'] ?? '0.0') ?? 0.0, + 'address': result['user_location'] ?? '未知地址', + }; + } + throw Exception('获取位置信息失败'); + } catch (e) { + throw Exception('定位失败: $e'); + } } } diff --git a/local_plugins/user_portrait/.gitignore b/local_plugins/user_portrait/.gitignore new file mode 100644 index 000000000..e7d347d9d --- /dev/null +++ b/local_plugins/user_portrait/.gitignore @@ -0,0 +1,33 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +build/ diff --git a/local_plugins/user_portrait/.metadata b/local_plugins/user_portrait/.metadata new file mode 100644 index 000000000..709f51d7e --- /dev/null +++ b/local_plugins/user_portrait/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "5c1433509f997e9059d230942bdf83f1b88a179e" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 5c1433509f997e9059d230942bdf83f1b88a179e + base_revision: 5c1433509f997e9059d230942bdf83f1b88a179e + - platform: android + create_revision: 5c1433509f997e9059d230942bdf83f1b88a179e + base_revision: 5c1433509f997e9059d230942bdf83f1b88a179e + - platform: ios + create_revision: 5c1433509f997e9059d230942bdf83f1b88a179e + base_revision: 5c1433509f997e9059d230942bdf83f1b88a179e + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/local_plugins/user_portrait/CHANGELOG.md b/local_plugins/user_portrait/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/local_plugins/user_portrait/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/local_plugins/user_portrait/LICENSE b/local_plugins/user_portrait/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/local_plugins/user_portrait/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/local_plugins/user_portrait/README.md b/local_plugins/user_portrait/README.md new file mode 100644 index 000000000..ba3867c74 --- /dev/null +++ b/local_plugins/user_portrait/README.md @@ -0,0 +1,15 @@ +# user_portrait + +用户画像 + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/to/develop-plugins), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/local_plugins/user_portrait/analysis_options.yaml b/local_plugins/user_portrait/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/local_plugins/user_portrait/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/local_plugins/user_portrait/android/.gitignore b/local_plugins/user_portrait/android/.gitignore new file mode 100644 index 000000000..161bdcdaf --- /dev/null +++ b/local_plugins/user_portrait/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/local_plugins/user_portrait/android/build.gradle b/local_plugins/user_portrait/android/build.gradle new file mode 100644 index 000000000..6930bad76 --- /dev/null +++ b/local_plugins/user_portrait/android/build.gradle @@ -0,0 +1,66 @@ +group = "com.yunqiinnovation.user_portrait" +version = "1.0-SNAPSHOT" + +buildscript { + ext.kotlin_version = "2.1.0" + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:8.7.3") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" + +android { + namespace = "com.yunqiinnovation.user_portrait" + + compileSdk = 35 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11 + } + + sourceSets { + main.java.srcDirs += "src/main/kotlin" + test.java.srcDirs += "src/test/kotlin" + } + + defaultConfig { + minSdk = 21 + } + + dependencies { + testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("org.mockito:mockito-core:5.0.0") + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/local_plugins/user_portrait/android/settings.gradle b/local_plugins/user_portrait/android/settings.gradle new file mode 100644 index 000000000..e79bd6825 --- /dev/null +++ b/local_plugins/user_portrait/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'user_portrait' diff --git a/local_plugins/user_portrait/android/src/main/AndroidManifest.xml b/local_plugins/user_portrait/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..11e49fdf6 --- /dev/null +++ b/local_plugins/user_portrait/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/local_plugins/user_portrait/android/src/main/kotlin/com/yunqiinnovation/user_portrait/UserPortrait.kt b/local_plugins/user_portrait/android/src/main/kotlin/com/yunqiinnovation/user_portrait/UserPortrait.kt new file mode 100644 index 000000000..7c69a3631 --- /dev/null +++ b/local_plugins/user_portrait/android/src/main/kotlin/com/yunqiinnovation/user_portrait/UserPortrait.kt @@ -0,0 +1,90 @@ +package com.yunqiinnovation.user_portrait + +import android.Manifest +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.SharedPreferences +import android.location.LocationManager +import android.os.Build +import android.provider.Settings +import android.util.Log +import kotlinx.coroutines.SupervisorJob +import org.json.JSONObject + +/** UserPortraitPlugin */ +object UserPortrait { + private const val TAG = "UserPortrait" + private const val PREF_NAME = "deep_voice_user" + private var appcontext:Context? = null + private var prefs:SharedPreferences? = null + private var portrait: Map = emptyMap() + + /** + * 初始化语音交互代理 + */ + fun initialize(context: Context) { + try { + // 注册广播监听器 + val filter = IntentFilter().apply { + addAction("com.yunqiinnovation.user_portrait.UPDATE_PORTRAIT") + } + + // Android 14+ 需要指定 flag + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.registerReceiver(controlReceiver, filter, Context.RECEIVER_NOT_EXPORTED) + } else { + context.registerReceiver(controlReceiver, filter) + } + // 获取SharedPreferences + appcontext = context; + prefs = appcontext?.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + portrait = prefs!!.all + + } catch (e: Exception) { + Log.e(TAG, "初始化语音交互代理时发生异常", e) + } + } + + private val controlReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + Log.i(TAG, "接受到通知事件 ${intent?.action}") + when (intent?.action) { + "com.yunqiinnovation.user_portrait.UPDATE_PORTRAIT" -> { + + val key = intent.getStringExtra("key").toString() + val value = intent.getStringExtra("value").toString() + Log.i(TAG, "收到 UPDATE_PORTRAIT 广播 $key:$value") + if (key.isNotEmpty()) { + Log.i(TAG ,"接收到画像更新:$key = $value") + set(key, value) + } + } + // 你也可以扩展支持更多指令 + } + } + } + + + fun getAll(): Map { + return portrait + .filterValues { it != null } // 可选,过滤掉 null + .mapValues { it.value.toString() } + } + + //设置画像字段 + fun set(key:String,value:String){ + val editor = prefs?.edit() + editor?.putString(key, value) + editor?.apply() // 异步提交(推荐) + portrait = portrait.toMutableMap().apply { + this[key] = value + } + } + + fun clean(){ + portrait = emptyMap(); + prefs?.edit()?.clear()?.apply() + } +} \ No newline at end of file diff --git a/local_plugins/user_portrait/android/src/main/kotlin/com/yunqiinnovation/user_portrait/UserPortraitPlugin.kt b/local_plugins/user_portrait/android/src/main/kotlin/com/yunqiinnovation/user_portrait/UserPortraitPlugin.kt new file mode 100644 index 000000000..391286196 --- /dev/null +++ b/local_plugins/user_portrait/android/src/main/kotlin/com/yunqiinnovation/user_portrait/UserPortraitPlugin.kt @@ -0,0 +1,45 @@ +package com.yunqiinnovation.user_portrait + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import android.util.Log +import androidx.core.content.ContextCompat.registerReceiver +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +/** UserPortraitPlugin */ +class UserPortraitPlugin: FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel : MethodChannel + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.user_portrait") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall(call: MethodCall, result: Result) { + if (call.method == "clean") { + UserPortrait.clean() + result.success(null) + } else { + result.notImplemented() + } + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } + + + + +} diff --git a/local_plugins/user_portrait/ios/.gitignore b/local_plugins/user_portrait/ios/.gitignore new file mode 100644 index 000000000..034771fc9 --- /dev/null +++ b/local_plugins/user_portrait/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh diff --git a/local_plugins/user_portrait/ios/user_portrait.podspec b/local_plugins/user_portrait/ios/user_portrait.podspec new file mode 100644 index 000000000..65cae4b79 --- /dev/null +++ b/local_plugins/user_portrait/ios/user_portrait.podspec @@ -0,0 +1,29 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint user_portrait.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'user_portrait' + s.version = '0.0.1' + s.summary = '用户画像' + s.description = <<-DESC +用户画像 + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'user_portrait/Sources/user_portrait/**/*' + s.dependency 'Flutter' + s.platform = :ios, '12.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' + + # If your plugin requires a privacy manifest, for example if it uses any + # required reason APIs, update the PrivacyInfo.xcprivacy file to describe your + # plugin's privacy impact, and then uncomment this line. For more information, + # see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files + # s.resource_bundles = {'user_portrait_privacy' => ['user_portrait/Sources/user_portrait/PrivacyInfo.xcprivacy']} +end diff --git a/local_plugins/user_portrait/ios/user_portrait/Package.swift b/local_plugins/user_portrait/ios/user_portrait/Package.swift new file mode 100644 index 000000000..d8d28ea06 --- /dev/null +++ b/local_plugins/user_portrait/ios/user_portrait/Package.swift @@ -0,0 +1,32 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "user_portrait", + platforms: [ + .iOS("12.0") + ], + products: [ + .library(name: "user-portrait", targets: ["user_portrait"]) + ], + dependencies: [], + targets: [ + .target( + name: "user_portrait", + dependencies: [], + resources: [ + // If your plugin requires a privacy manifest, for example if it uses any required + // reason APIs, update the PrivacyInfo.xcprivacy file to describe your plugin's + // privacy impact, and then uncomment these lines. For more information, see + // https://developer.apple.com/documentation/bundleresources/privacy_manifest_files + // .process("PrivacyInfo.xcprivacy"), + + // If you have other resources that need to be bundled with your plugin, refer to + // the following instructions to add them: + // https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package + ] + ) + ] +) diff --git a/local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/PrivacyInfo.xcprivacy b/local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/PrivacyInfo.xcprivacy new file mode 100644 index 000000000..a34b7e2e6 --- /dev/null +++ b/local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/UserPortraitPlugin.swift b/local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/UserPortraitPlugin.swift new file mode 100644 index 000000000..b8cdc8b21 --- /dev/null +++ b/local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/UserPortraitPlugin.swift @@ -0,0 +1,19 @@ +import Flutter +import UIKit + +public class UserPortraitPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "user_portrait", binaryMessenger: registrar.messenger()) + let instance = UserPortraitPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getPlatformVersion": + result("iOS " + UIDevice.current.systemVersion) + default: + result(FlutterMethodNotImplemented) + } + } +} diff --git a/local_plugins/user_portrait/lib/user_portrait.dart b/local_plugins/user_portrait/lib/user_portrait.dart new file mode 100644 index 000000000..5e371fe43 --- /dev/null +++ b/local_plugins/user_portrait/lib/user_portrait.dart @@ -0,0 +1,26 @@ +import 'package:flutter/services.dart'; + +class UserPortrait { + static final UserPortrait _instance = UserPortrait._internal(); + + /// 获取单例 + factory UserPortrait() => _instance; + + // 方法通道 + final MethodChannel _methodChannel = MethodChannel( + 'com.yunqiinnovation.user_portrait', + ); + + // 内部构造函数 + UserPortrait._internal() {} + + /// 请求定位权限 + Future clean() async { + try { + await _methodChannel.invokeMethod('clean'); + return; + } catch (e) { + throw Exception('请求权限失败: $e'); + } + } +} diff --git a/local_plugins/user_portrait/pubspec.yaml b/local_plugins/user_portrait/pubspec.yaml new file mode 100644 index 000000000..1e3f104cc --- /dev/null +++ b/local_plugins/user_portrait/pubspec.yaml @@ -0,0 +1,72 @@ +name: user_portrait +description: "用户画像" +version: 0.0.1 +homepage: + +environment: + sdk: ^3.8.1 + flutter: '>=3.3.0' + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: com.yunqiinnovation.user_portrait + pluginClass: UserPortraitPlugin + ios: + pluginClass: UserPortraitPlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/to/asset-from-package + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/to/font-from-package diff --git a/local_plugins/user_portrait/test/user_portrait_method_channel_test.dart b/local_plugins/user_portrait/test/user_portrait_method_channel_test.dart new file mode 100644 index 000000000..7a2dd7e32 --- /dev/null +++ b/local_plugins/user_portrait/test/user_portrait_method_channel_test.dart @@ -0,0 +1,27 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:user_portrait/user_portrait_method_channel.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + MethodChannelUserPortrait platform = MethodChannelUserPortrait(); + const MethodChannel channel = MethodChannel('user_portrait'); + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + channel, + (MethodCall methodCall) async { + return '42'; + }, + ); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null); + }); + + test('getPlatformVersion', () async { + expect(await platform.getPlatformVersion(), '42'); + }); +} diff --git a/local_plugins/user_portrait/test/user_portrait_test.dart b/local_plugins/user_portrait/test/user_portrait_test.dart new file mode 100644 index 000000000..02c2ecd8a --- /dev/null +++ b/local_plugins/user_portrait/test/user_portrait_test.dart @@ -0,0 +1,29 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:user_portrait/user_portrait.dart'; +import 'package:user_portrait/user_portrait_platform_interface.dart'; +import 'package:user_portrait/user_portrait_method_channel.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +class MockUserPortraitPlatform + with MockPlatformInterfaceMixin + implements UserPortraitPlatform { + + @override + Future getPlatformVersion() => Future.value('42'); +} + +void main() { + final UserPortraitPlatform initialPlatform = UserPortraitPlatform.instance; + + test('$MethodChannelUserPortrait is the default instance', () { + expect(initialPlatform, isInstanceOf()); + }); + + test('getPlatformVersion', () async { + UserPortrait userPortraitPlugin = UserPortrait(); + MockUserPortraitPlatform fakePlatform = MockUserPortraitPlatform(); + UserPortraitPlatform.instance = fakePlatform; + + expect(await userPortraitPlugin.getPlatformVersion(), '42'); + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index 32cdf99ff..f6ba51acb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -105,6 +105,8 @@ dependencies: path: local_plugins/realtime jl_opus: path: local_plugins/jl_opus + user_portrait: + path: local_plugins/user_portrait test: path: local_plugins/test From 8ff3eb1f588da38d667cf50eca53fbde0c950370 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Fri, 11 Jul 2025 10:42:27 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E9=A1=B5=E5=BC=82=E5=B8=B8=EF=BC=8C=E7=BF=BB=E8=AF=91=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E8=AF=AD=E8=A8=80=E4=B8=8D=E7=BF=BB=E8=AF=91=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modules/login/controllers/login_controller.dart | 4 ++-- .../translation/controllers/translation_controller.dart | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/modules/login/controllers/login_controller.dart b/lib/modules/login/controllers/login_controller.dart index becf93980..ab1c500ff 100644 --- a/lib/modules/login/controllers/login_controller.dart +++ b/lib/modules/login/controllers/login_controller.dart @@ -194,8 +194,8 @@ class LoginController extends GetxController { List get verificationCode => _verificationCode; // 文本控制器 - final contactController = TextEditingController(); - final verificationCodeController = TextEditingController(); + TextEditingController contactController = TextEditingController(); + TextEditingController verificationCodeController = TextEditingController(); @override void onInit() { diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 95b9c4953..703c87575 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -882,7 +882,12 @@ class TranslationController extends GetxController { await _asrService.initialize( supportedLanguages: asrSupportedLanguages, ); - + // 获取识别事件流 - 这启动了异步识别过程 + var recognitionStream = await _asrService.recognizeCallback(); + // 监听识别结果 + _recognitionSubscription?.cancel(); + _recognitionSubscription = + recognitionStream.listen(_handleRecognitionEvent); Logger.info('ASR服务重新初始化成功'); } catch (e) { Logger.error('重新初始化ASR服务失败: ${e.toString()}'); From 70b665a73369606c5e0d4ebbac79685fe5aefd6e Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Fri, 11 Jul 2025 15:57:49 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E9=87=8F=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/ble_manager.dart | 8 ++- .../agent_service/android/build.gradle.kts | 1 + .../agent_service/AgentService.kt | 63 ++++++++++++++++++- .../agent_service/AgentServicePlugin.kt | 3 + .../agent_service/BleMediaService.kt | 10 ++- .../chat_api/ChatApiService.kt | 16 +++++ .../com/yunqiinnovation/chat_api/Utils.kt | 33 ++++++++++ 7 files changed, 130 insertions(+), 4 deletions(-) diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index 3cbc033c3..175ca4d9e 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -1,4 +1,6 @@ import 'dart:async'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + import '../../data/models/appconfig.dart'; import 'package:get/get.dart'; import 'package:ble_service/ble_service.dart'; @@ -517,7 +519,9 @@ class BleManager extends GetxService { Future startAgentService() async { try { // 从.env文件直接读取环境变量 - // final azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; + final serverurl = dotenv.env['SERVER_URL'] ?? ''; + var jsonString = _storage.read("logintoken"); + Logger.info('启动BLE服务,serverurl: ${serverurl}'); // final azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? 'eastasia'; // final openaiApiKey = dotenv.env['OPENAI_API_KEY'] ?? ''; // final openaiBaseUrl = @@ -557,6 +561,8 @@ class BleManager extends GetxService { 'mcpServer': jsonEncode(jsonData), 'volcanoAppId': '2316081933', 'volcanoToken': 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En', + 'serverurl': serverurl, + 'usertoken': jsonString }; // 记录日志(不记录敏感信息) diff --git a/local_plugins/agent_service/android/build.gradle.kts b/local_plugins/agent_service/android/build.gradle.kts index fc98a0bac..82eec6b49 100644 --- a/local_plugins/agent_service/android/build.gradle.kts +++ b/local_plugins/agent_service/android/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { implementation("androidx.media:media:1.6.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") implementation("androidx.security:security-crypto:1.1.0-alpha03") + implementation("com.squareup.okhttp3:okhttp:4.12.0") // 依赖本地其他插件 add("compileOnly", project(":azure_speech")) add("compileOnly", project(":chat_storage")) diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index db2e75132..37ebe489e 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -28,6 +28,9 @@ import com.deep_voice.speech.tts.TtsEventType import com.deep_voice.bytedance_speech.BytedanceTTS import java.util.UUID import com.yunqiinnovation.agent_service.Utils +import okhttp3.* +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody /** * 代理服务事件监听接口 @@ -72,6 +75,9 @@ object AgentService : CoroutineScope { // 会话ID,用于区分不同聊天上下文 private val sessionId = "default_agent" + // 服务端连接 + private var serverurl = "" + private var usertoken = "" // 历史聊天消息缓存 private val historyMessages = JSONArray() @@ -210,7 +216,9 @@ object AgentService : CoroutineScope { config["openaivisionModel"]?.toString() ?: "", config["mcpServer"]?.toString() ?: "" ) - + serverurl = config["serverurl"]?.toString() ?: ""; + usertoken = config["usertoken"]?.toString() ?: ""; + Log.e(TAG, "liwei -----------读取服务地址和用户token: $serverurl $usertoken") // 加载最近的聊天记录 loadChatHistory() @@ -783,6 +791,7 @@ object AgentService : CoroutineScope { var userMetadata = JSONObject() var reply = true; //是否回复 var broadcast = true //是否播发 + // 如果有图片,添加图片信息到元数据 if (hasImage) { userMetadata.put("hasImage", true) @@ -844,6 +853,26 @@ object AgentService : CoroutineScope { currsessionId, messagesWithSystemPrompt, object : StreamCallback { + + override fun onUsage( + sessionid: String, + prompt_tokens: Int?, + completion_tokens: Int?, + total_tokens: Int? + ) { + try { + sendStatisticsEvent( + mapOf( + "CHAT_PROMPT_TOKENS" to (prompt_tokens ?: 0), + "CHAT_COMPLETION_TOKENS" to (completion_tokens ?: 0), + "CHAT_TOTAL_TOKENS" to (total_tokens ?: 0) + ) + ) + }catch (e: Exception) { + Log.e(TAG, "liwei--------------- AI Call onUsage 异常", e) + } + } + override fun onToken(sessionid: String, token: String) { try { if (sessionid != currsessionId) { @@ -1634,4 +1663,36 @@ object AgentService : CoroutineScope { Log.i(TAG, "启动导航服务失败") } } + + + /* + * 发送统计事件 + * */ + fun sendStatisticsEvent(actions:Map) { + val client = OkHttpClient() + + // 外层 JSON 对象 + val jsonObject = JSONObject() + val actionsObject = JSONObject() + + for ((key, value) in actions) { + actionsObject.put(key, value) + } + + jsonObject.put("actions", actionsObject) + + val json = jsonObject.toString() + val mediaType = "application/json".toMediaType() + val body = json.toRequestBody(mediaType) + + val request = Request.Builder() + .url("$serverurl/api/home/user_pushaction") + .addHeader("Authorization", usertoken) // 添加 Token + .post(body) + .build() + Thread { + val response = client.newCall(request).execute() + // println("sendStatisticsEvent Response: ${response.body?.string()}") + }.start() + } } \ No newline at end of file diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt index 819bd6ad7..1a5d06561 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt @@ -71,6 +71,9 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream config["mcpServer"]?.toString()?.let { intent.putExtra("mcpServer", it) } config["volcanoAppId"]?.toString()?.let { intent.putExtra("volcanoAppId", it) } config["volcanoToken"]?.toString()?.let { intent.putExtra("volcanoToken", it) } + config["serverurl"]?.toString()?.let { intent.putExtra("serverurl", it) } + config["usertoken"]?.toString()?.let { intent.putExtra("usertoken", it) } + // 启动前台服务 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt index c31528263..ff4771fa6 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt @@ -48,7 +48,9 @@ class BleMediaService : Service(), CoroutineScope { private const val KEY_MCP_SERVER = "mcpServer" private const val KEY_VOLCANO_APP_ID = "volcanoAppId" private const val KEY_VOLCANO_TOKEN = "volcanoToken" - + + private const val KEY_SERVER_URL = "serverurl" + private const val KEY_USER_TOKEN = "usertoken" // 用于跟踪服务是否正在运行 private val isRunning = AtomicBoolean(false) @@ -171,6 +173,8 @@ class BleMediaService : Service(), CoroutineScope { it.getStringExtra(KEY_MCP_SERVER)?.let { value -> config[KEY_MCP_SERVER] = value } it.getStringExtra(KEY_VOLCANO_APP_ID)?.let { value -> config[KEY_VOLCANO_APP_ID] = value } it.getStringExtra(KEY_VOLCANO_TOKEN)?.let { value -> config[KEY_VOLCANO_TOKEN] = value } + it.getStringExtra(KEY_SERVER_URL)?.let { value -> config[KEY_SERVER_URL] = value } + it.getStringExtra(KEY_USER_TOKEN)?.let { value -> config[KEY_USER_TOKEN] = value } } Log.i(TAG, "从Intent中提取到配置参数: $config") return config @@ -204,7 +208,9 @@ class BleMediaService : Service(), CoroutineScope { KEY_SYSTEM_PROMPT to getConfigString(prefs, KEY_SYSTEM_PROMPT, "gpt-3.5-turbo"), KEY_MCP_SERVER to getConfigString(prefs, KEY_MCP_SERVER, ""), KEY_VOLCANO_APP_ID to getConfigString(prefs, KEY_VOLCANO_APP_ID, ""), - KEY_VOLCANO_TOKEN to getConfigString(prefs, KEY_VOLCANO_TOKEN, "") + KEY_VOLCANO_TOKEN to getConfigString(prefs, KEY_VOLCANO_TOKEN, ""), + KEY_SERVER_URL to getConfigString(prefs, KEY_SERVER_URL, ""), + KEY_USER_TOKEN to getConfigString(prefs, KEY_USER_TOKEN, "") ) } diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt index 45d5a0caf..7d7d50bbe 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt @@ -36,6 +36,10 @@ class ChatApiException(message: String) : Exception("ChatApiException: $message" * 与 iOS 版本 StreamCallback 协议保持完全一致 */ interface StreamCallback { + /** + * AI使用量 + * */ + fun onUsage(sessionid: String,prompt_tokens: Int?,completion_tokens: Int?,total_tokens: Int?) /** * 接收到新的token */ @@ -407,8 +411,14 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor Log.e("ChatApiService", "创建ChatCompletionRequest或调用chatCompletions失败: ${e.message}", e) throw e } + + var replyContext = ""; // Log.d(TAG, "liwei-------------------------开始AI 对话 7") chatsFlow.collect { result -> +// val usage = result.usage +// if (usage != null){ //发现流式不返回这歌数据 +// getSessionCallback(sessionid)?.onUsage(sessionid,usage.promptTokens,usage.completionTokens,usage.totalTokens) +// } // 每次处理前检查是否仍是当前会话 if (sessionid != currSessionId) { // throw CancellationException("Session cancelled") @@ -430,6 +440,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor // 处理普通文本内容 delta.content?.let { content -> try { + replyContext += content getSessionCallback(sessionid)?.onToken(sessionid,content) } catch (e: Exception) { Log.e(TAG, "onToken回调异常: ${e.message}", e) @@ -454,6 +465,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor val functionName = function.name if (!functionName.isNullOrEmpty()) { toolCallInfo.name = functionName + replyContext += functionName } } catch (e: Exception) { // 在流式响应中,name 可能在后续的 delta 中为 null,这是正常的 @@ -465,6 +477,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor val functionArgs = function.arguments if (!functionArgs.isNullOrEmpty()) { toolCallInfo.arguments += functionArgs + replyContext += functionArgs } } catch (e: Exception) { // 在流式响应中,arguments 可能在后续的 delta 中为 null,这是正常的 @@ -483,6 +496,9 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor } } } + val promptTokens = estimatePromptTokens(chatMessages); //计算上传token + val completionTokens = roughTokenCount(replyContext); //计算上传token + getSessionCallback(sessionid)?.onUsage(sessionid,promptTokens,completionTokens,promptTokens+completionTokens) // Log.d(TAG, "liwei-------------------------开始AI 对话 8") // 完成处理 if (sessionid == currSessionId) { 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 31b7bba05..cd1ab77f7 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 @@ -8,6 +8,7 @@ import android.net.Uri import android.provider.Settings import android.util.Log import androidx.core.content.ContextCompat +import com.aallam.openai.api.chat.ChatMessage import io.ktor.client.* import io.ktor.client.engine.okhttp.* import io.ktor.client.plugins.sse.* @@ -148,4 +149,36 @@ fun checkPermissionAndGuide(context: Context, permission: String, permissionName return false } return true +} + + +/** + * 粗略计算一段文字的 token 数(每 4 字符 ≈ 1 token) + */ +fun roughTokenCount(text: String): Int { + val trimmed = text.trim() + if (trimmed.isEmpty()) return 0 + + // 中文字符(1字≈1token)+ 英文字符(4字符≈1token)混合估算 + val chineseCharCount = trimmed.count { it.code > 127 } + val englishCharCount = trimmed.length - chineseCharCount + + return chineseCharCount + (englishCharCount / 4) + 1 +} + + +/** + * 估算消息列表的总 token 数量(近似) + * @param messages List of message map: role: user/assistant, content: string + * @return 估算 token 数 + */ +fun estimatePromptTokens(messages: List): Int { + var tokens = 0 + for (msg in messages) { + tokens += 4 // 每条消息结构开销 + tokens += roughTokenCount(msg.role.role.lowercase()) // ChatRole 枚举转小写字符串 + tokens += roughTokenCount(msg.content ?: "") + } + tokens += 2 // 结尾 assistant 回复结构开销 + return tokens } \ No newline at end of file From 7e127516ad2c4e41c22165f841e1153c42d5be26 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Fri, 11 Jul 2025 16:06:50 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=9C=B0=E5=9D=80?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E7=8E=AF=E5=A2=83=E6=96=87=E4=BB=B6=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/network/dio_manager.dart | 6 +++--- lib/data/services/network/nw_api.dart | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/data/services/network/dio_manager.dart b/lib/data/services/network/dio_manager.dart index 7833ed582..abdb7850f 100644 --- a/lib/data/services/network/dio_manager.dart +++ b/lib/data/services/network/dio_manager.dart @@ -1,7 +1,7 @@ import 'package:dio/dio.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'auth_interceptor.dart'; -import 'nw_api.dart'; import 'nw_method.dart'; class DioManager { @@ -9,10 +9,10 @@ class DioManager { factory DioManager() => _shared; late Dio dio; - + final baseUrl = dotenv.env['SERVER_URL'] ?? ""; DioManager._internal() { BaseOptions options = BaseOptions( - baseUrl: NWApi.baseApi, + baseUrl: baseUrl, contentType: Headers.jsonContentType, responseType: ResponseType.json, connectTimeout: const Duration(seconds: 30), diff --git a/lib/data/services/network/nw_api.dart b/lib/data/services/network/nw_api.dart index 8feff1db5..9a7afbfd9 100644 --- a/lib/data/services/network/nw_api.dart +++ b/lib/data/services/network/nw_api.dart @@ -1,5 +1,5 @@ class NWApi { // static const baseApi = "https://www.deapsound.com"; - static const baseApi = "https://api.deapsound.com"; + // static const baseApi = "https://api.deapsound.com"; // static const baseApi = "https://api.ideapsound.com"; }