From 19404fbe26159db85ff166840150d03d26dfe903 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Tue, 25 Nov 2025 19:19:17 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=B0=E7=9A=84=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/models/goods.dart | 13 +- lib/data/services/ble_manager.dart | 7 +- lib/data/services/pay_service.dart | 9 + .../agent/controllers/agent_controller.dart | 10 +- lib/modules/agent/views/agent_view.dart | 60 +- .../controllers/goodsvip_controller.dart | 47 +- lib/modules/goods/views/goodsvip_view.dart | 812 +++++++++++++++--- lib/modules/settings/views/settings_view.dart | 93 +- .../agent_service/AgentService.kt | 16 +- .../agent_service/AgentServicePlugin.kt | 2 +- .../agent_service/BleMediaService.kt | 6 +- .../agent_service/AgentServiceImpl.swift | 13 +- .../chat_api/ChatApiService.kt | 7 +- .../Sources/chat_api/ChatApiService.swift | 22 +- 14 files changed, 901 insertions(+), 216 deletions(-) diff --git a/lib/data/models/goods.dart b/lib/data/models/goods.dart index bda3a6c37..6218c65a7 100644 --- a/lib/data/models/goods.dart +++ b/lib/data/models/goods.dart @@ -10,7 +10,10 @@ enum UsageType { /// 会议 Meet, // 2 /// 智能体 - AI; // 3 + AI, // 3 + /// 智能体 + Vip; // 4 + /// Vip // 解析支持 int 或 string(大小写不敏感) static UsageType from(dynamic value) { @@ -24,6 +27,8 @@ enum UsageType { return UsageType.Meet; case 3: return UsageType.AI; + case 4: + return UsageType.Vip; default: return UsageType.UsageNull; } @@ -43,6 +48,8 @@ enum UsageType { return UsageType.Meet; case 'ai': return UsageType.AI; + case 'vip': + return UsageType.Vip; default: return UsageType.UsageNull; } @@ -60,6 +67,8 @@ enum UsageType { return 2; case UsageType.AI: return 3; + case UsageType.Vip: + return 4; } } @@ -73,6 +82,8 @@ enum UsageType { return 'Meet'; case UsageType.AI: return 'AI'; + case UsageType.Vip: + return 'Vip'; } } } diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index 94cca3036..5b9f3754c 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -9,7 +9,7 @@ import 'package:get/get.dart'; import 'package:ble_service/ble_service.dart'; import 'package:agent_service/agent_service.dart'; import '../../data/models/appconfig_model.dart'; - +import '../../data/models/user_Info.dart' as userInfo; import '../../data/models/appconfig.dart'; import '../../core/utils/logger.dart'; import 'bluetooth_manager.dart'; @@ -706,6 +706,9 @@ class BleManager extends GetxService { Logger.info('启动BLE服务,musicServiceType: ${musicServiceType}'); final isTtsInterruptEnabled = await _loadTtsInterrupe(); + final vipExpiryTime = userInfo.User.instance.vipexptime; + Logger.info('会员过期时间: $vipExpiryTime'); + // 根据APP语言动态配置语音识别语言 List supportedLanguages = []; try { @@ -776,7 +779,7 @@ class BleManager extends GetxService { 'musicServiceType': musicServiceType, "navigationMode": navigationMode, "isttsinterrupt": isTtsInterruptEnabled ? "true" : "false", - "aiintegral": 100, //ai积分 + "vipexptime": vipExpiryTime, //会员过期时间 "callingToolText": "callingTool".tr, "insufficientIntegralText": "insufficientIntegral".tr, }; diff --git a/lib/data/services/pay_service.dart b/lib/data/services/pay_service.dart index dedc2cae8..79ab577f9 100644 --- a/lib/data/services/pay_service.dart +++ b/lib/data/services/pay_service.dart @@ -2,6 +2,7 @@ import 'package:get/get.dart'; import 'dart:convert'; import 'dart:async'; import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:in_app_purchase_android/in_app_purchase_android.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -229,6 +230,14 @@ class PayService extends GetxService { } return ok; } catch (e) { + if (e is PlatformException && + (e.code.contains('purchase_cancelled') || + e.code.contains('storekit2_purchase_cancelled'))) { + onResult?.call(false, '已取消支付'); + print( + '[IAP] 用户取消支付(同步抛出) code=${e.code} message=${e.message} product=${product.id}'); + return false; + } onResult?.call(false, '发起内购失败: $e'); print('[IAP] 发起内购异常: $e'); return false; diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 2646dc84b..87a0d04a8 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -46,7 +46,7 @@ class AgentController extends GetxController with WidgetsBindingObserver { // 聊天消息列表 final messages = [].obs; final currsessionid = ''.obs; - final aichatintegral = 0.obs; + final vipExpiryTime = 0.obs; // UI控制器 final TextEditingController textController = TextEditingController(); final ScrollController scrollController = ScrollController(); @@ -166,7 +166,10 @@ class AgentController extends GetxController with WidgetsBindingObserver { @override void onInit() { super.onInit(); - aichatintegral.value = User.isLoggedIn() ? User.instance.aichatintegral : 0; + vipExpiryTime.value = User.isLoggedIn() ? User.instance.vipexptime : 0; + ever(User.refreshTick, (_) { + vipExpiryTime.value = User.instance.vipexptime; + }); _initAppDir(); _loadTtsPreference(); //加载TTS偏好设置 _loadTtsInterrupe(); //加载TTS播报打断偏好设置 @@ -837,7 +840,7 @@ class AgentController extends GetxController with WidgetsBindingObserver { ), onPressed: () { Get.back(); - Get.toNamed(Routes.goodsai); + Get.toNamed(Routes.goodsvip); }, child: const Text('去充值'), ), @@ -1084,7 +1087,6 @@ class AgentController extends GetxController with WidgetsBindingObserver { final sessionid = event.data['sessionid'] ?? ''; final integral = event.data['integral'] ?? 0; User.instance.aichatintegral = integral; - aichatintegral.value = integral; break; default: break; diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index 22662ad94..fc08482f9 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -13,6 +13,7 @@ import 'message_bubble.dart'; import '../widgets/full_screen_editor.dart'; import '../widgets/sound_bar_widget.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:intl/intl.dart'; class AgentView extends StatefulWidget { const AgentView({Key? key}) : super(key: key); @@ -119,14 +120,27 @@ class _AgentViewState extends State with WidgetsBindingObserver { ), ), SizedBox(width: 4.w), - Obx(() => Text( - '剩余积分: ${controller.aichatintegral.value}', - style: TextStyle( - fontSize: 12.sp, - color: Colors.green, - fontWeight: FontWeight.w500, - ), - )), + Obx(() { + final vipexptime = controller.vipExpiryTime.value; + DateTime? expiry; + if (vipexptime > 0) { + expiry = vipexptime > 1000000000000 + ? DateTime.fromMillisecondsSinceEpoch(vipexptime) + : DateTime.fromMillisecondsSinceEpoch( + vipexptime * 1000); + } + final text = expiry != null + ? '到期时间: ${DateFormat('yyyy-MM-dd').format(expiry)}' + : '未开启'; + return Text( + text, + style: TextStyle( + fontSize: 12.sp, + color: Colors.green, + fontWeight: FontWeight.w500, + ), + ); + }), ], ), ], @@ -326,18 +340,18 @@ class _AgentViewState extends State with WidgetsBindingObserver { getOverlayPosition: IntroOverlayBuilder.overlayPosition, overlayBuilder: (params) { - return IntroOverlayBuilder.aiBuilder( - params, - controller - .noviceGuideService - .aiStepsList[controller - .noviceGuideService.aiStep] - ['endKey'] - .tr, - onFinish: () async { - if (controller - .noviceGuideService.aiStep < - 5) { + return IntroOverlayBuilder.aiBuilder( + params, + controller + .noviceGuideService + .aiStepsList[controller + .noviceGuideService + .aiStep]['endKey'] + .tr, + onFinish: () async { + if (controller + .noviceGuideService.aiStep < + 5) { await Future.delayed(const Duration( milliseconds: 300)); controller @@ -627,8 +641,8 @@ class _AgentViewState extends State with WidgetsBindingObserver { controller .noviceGuideService .aiStepsList[controller - .noviceGuideService.aiStep] - ['startKey'] + .noviceGuideService + .aiStep]['startKey'] .tr, buttonText: 'confirmInput'.tr, onNext: () { @@ -877,7 +891,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { await controller .noviceGuideService.aiStepsList[ controller.noviceGuideService - .aiStep]['method'](); + .aiStep]['method'](); params.onNext!(); controller.sendMessage(); }, diff --git a/lib/modules/goods/controllers/goodsvip_controller.dart b/lib/modules/goods/controllers/goodsvip_controller.dart index 00be5dba8..98b18c030 100644 --- a/lib/modules/goods/controllers/goodsvip_controller.dart +++ b/lib/modules/goods/controllers/goodsvip_controller.dart @@ -1,5 +1,3 @@ -import 'package:get/get.dart'; -import '../../../routes/app_routes.dart'; import '../../../data/models/goods.dart'; import '../../../data/services/network/api.dart'; import 'goods_base_controller.dart'; @@ -7,23 +5,36 @@ import 'goods_base_controller.dart'; class GoodsvipController extends GoodsBaseController { @override Future> fetchProducts() async { - final resp = await Api.getGoods({'usagetype': UsageType.UsageNull.toInt}); - if (resp == null || resp is! Map) return []; - if (resp.containsKey('error') || resp.containsKey('msg')) { - final msg = resp['error'] ?? resp['msg'] ?? '服务器错误'; - throw Exception(msg); + final types = [ + UsageType.AI, + UsageType.Meet, + UsageType.Translation, + UsageType.Vip, + ]; + final Map all = {}; + for (final t in types) { + final resp = await Api.getGoods({'usagetype': t.toInt}); + if (resp == null || resp is! Map) continue; + if (resp.containsKey('error') || resp.containsKey('msg')) { + final msg = resp['error'] ?? resp['msg'] ?? '服务器错误'; + throw Exception(msg); + } + final container = + (resp is Map && resp['data'] is Map) ? resp['data'] : resp; + final data = (container is Map) + ? (container['goodss'] ?? container['goodes'] ?? container['goods']) + : null; + if (data is List) { + final list = data + .whereType>() + .map(DBGoods.fromJson) + .toList(); + for (final g in list) { + all[g.id] = g; + } + } } - final container = - (resp is Map && resp['data'] is Map) ? resp['data'] : resp; - final data = (container is Map) - ? (container['goodss'] ?? container['goodes'] ?? container['goods']) - : null; - if (data is List) { - final list = - data.whereType>().map(DBGoods.fromJson).toList(); - return list.where((g) => g.usagetype == UsageType.UsageNull).toList(); - } - return []; + return all.values.toList(); } // Future purchase(DBGoods item) async => super.purchase(item); diff --git a/lib/modules/goods/views/goodsvip_view.dart b/lib/modules/goods/views/goodsvip_view.dart index e4fbff0dd..6b541e319 100644 --- a/lib/modules/goods/views/goodsvip_view.dart +++ b/lib/modules/goods/views/goodsvip_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:voitrans/data/models/goods.dart'; +import '../../../data/models/user_Info.dart'; import '../controllers/goodsvip_controller.dart'; import '../utils/goods_i18n.dart'; @@ -15,14 +16,19 @@ class GoodsvipView extends GetView { return Scaffold( backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], appBar: AppBar( - backgroundColor: isDarkMode ? Colors.black : Colors.white, + backgroundColor: Colors.blue[600], + foregroundColor: Colors.white, elevation: 0.5, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new), + onPressed: () => Get.back(), + ), title: Text( - 'premiumMember'.tr, // 高级会员 + '购买', style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, + color: Colors.white, ), ), centerTitle: true, @@ -34,20 +40,38 @@ class GoodsvipView extends GetView { } else if (controller.error.isNotEmpty) { content = Center(child: Text(controller.error.value)); } else { - content = ListView.separated( + final Map> grouped = {}; + for (final g in controller.items) { + grouped.putIfAbsent(g.usagetype, () => []).add(g); + } + final order = [ + UsageType.Vip, + UsageType.Translation, + UsageType.Meet, + UsageType.AI, + ]; + final children = []; + children.add(_buildFeaturesSection(isDarkMode)); + children.add(SizedBox(height: 16.h)); + // children.add(_buildScenesSection(isDarkMode)); + // children.add(SizedBox(height: 16.h)); + for (int i = 0; i < order.length; i++) { + final t = order[i]; + final list = grouped[t]; + if (list == null || list.isEmpty) continue; + children.add(_buildTypeSection(t, list, isDarkMode)); + if (i != order.length - 1) { + children.add(SizedBox(height: 16.h)); + children.add( + Divider(color: isDarkMode ? Colors.white24 : Colors.black12)); + children.add(SizedBox(height: 16.h)); + } + } + + children.add(_buildFooterSection(isDarkMode)); + content = ListView( padding: EdgeInsets.all(16.w), - itemCount: controller.items.length + 2, - separatorBuilder: (_, __) => SizedBox(height: 12.h), - itemBuilder: (_, index) { - if (index < controller.items.length) { - final item = controller.items[index]; - return _buildGoodsCard(item, isDarkMode); - } - if (index == controller.items.length) { - return _buildFeaturesSection(isDarkMode); - } - return _buildFooterSection(isDarkMode); - }, + children: children, ); } return Stack( @@ -71,92 +95,6 @@ class GoodsvipView extends GetView { ); } - Widget _buildGoodsCard(DBGoods item, bool isDarkMode) { - return Container( - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[900] : Colors.white, - borderRadius: BorderRadius.circular(12.r), - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withOpacity(0.2) - : Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Row( - children: [ - Container( - width: 42.w, - height: 42.w, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.amber[900]!.withOpacity(0.3) - : Colors.amber[100]!, - borderRadius: BorderRadius.circular(21.r), - ), - child: Icon(Icons.workspace_premium, - color: isDarkMode ? Colors.amber[300]! : Colors.amber[600]!), - ), - SizedBox(width: 12.w), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(item.name, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - )), - SizedBox(height: 4.h), - Text(localizedGoodsDesc(item), - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white70 : Colors.grey[600], - )), - ], - ), - ), - SizedBox(width: 12.w), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text('¥${item.price.toStringAsFixed(2)}', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - )), - SizedBox(height: 8.h), - GestureDetector( - onTap: () => controller.purchase(item), - child: Container( - padding: - EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), - decoration: BoxDecoration( - color: Colors.blue[600], - borderRadius: BorderRadius.circular(20.r), - ), - child: Text( - 'purchase'.tr, - style: TextStyle( - fontSize: 12.sp, - color: Colors.white, - ), - ), - ), - ), - ], - ), - ], - ), - ); - } - Widget _buildFeaturesSection(bool isDarkMode) { final textColor = isDarkMode ? Colors.white : Colors.black; final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; @@ -317,4 +255,674 @@ class GoodsvipView extends GetView { ), ); } + + Widget _buildTypeHeader(UsageType type, bool isDarkMode) { + final titles = { + UsageType.AI: 'aiFunction'.tr, + UsageType.Meet: 'conferenceAssistant'.tr, + UsageType.Translation: 'simultaneousTranslation'.tr, + UsageType.Vip: 'premiumMember'.tr, + }; + final iconAndColor = _iconColorForType(type, isDarkMode); + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + String titleStr = titles[type] ?? type.name; + String? vipSubtitle; + if (type == UsageType.Vip) { + int viplv = 0; + int vipexptime = 0; + if (User.isLoggedIn()) { + final u = User.instance; + viplv = u.viplv; + vipexptime = u.vipexptime; + } + DateTime? expiry; + if (vipexptime > 0) { + expiry = vipexptime > 1000000000000 + ? DateTime.fromMillisecondsSinceEpoch(vipexptime) + : DateTime.fromMillisecondsSinceEpoch(vipexptime * 1000); + } + titleStr = 'Vip $viplv'; + vipSubtitle = expiry != null ? _fmtDate(expiry) : '未开启'; + } + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Container( + // width: 44.w, + // height: 44.w, + // decoration: BoxDecoration( + // color: iconAndColor.background, + // borderRadius: BorderRadius.circular(22.r), + // ), + // child: Icon(iconAndColor.icon, color: iconAndColor.foreground), + // ), + // SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + titleStr, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w700, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + SizedBox(height: 8.h), + if (type == UsageType.Vip && vipSubtitle != null) + Text( + vipSubtitle, + style: TextStyle(fontSize: 12.sp, color: subColor), + ) + else + _buildMetricsInline(type, isDarkMode), + ], + ), + ), + ], + ), + ); + } + + Widget _buildTypeSection( + UsageType type, List list, bool isDarkMode) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTypeHeader(type, isDarkMode), + SizedBox(height: 12.h), + _buildGoodsGrid(list, type, isDarkMode), + ], + ); + } + + Widget _buildMetricsInline(UsageType type, bool isDarkMode) { + final textColor = isDarkMode ? Colors.white : Colors.black; + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + if (type == UsageType.Vip) { + int viplv = 0; + int vipexptime = 0; + if (User.isLoggedIn()) { + final u = User.instance; + viplv = u.viplv; + vipexptime = u.vipexptime; + } + DateTime? expiry; + if (vipexptime > 0) { + expiry = vipexptime > 1000000000000 + ? DateTime.fromMillisecondsSinceEpoch(vipexptime) + : DateTime.fromMillisecondsSinceEpoch(vipexptime * 1000); + } + return Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Lv.$viplv', + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + SizedBox(height: 4.h), + Text( + expiry != null + ? '${'validityPeriod'.tr}${_fmtDate(expiry)}' + : 'notBound'.tr, + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + ], + ), + ), + // Container( + // padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.h), + // decoration: BoxDecoration( + // color: isDarkMode ? Colors.white10 : Colors.black12, + // borderRadius: BorderRadius.circular(16.r), + // ), + // child: Text( + // viplv > 0 ? 'enabled'.tr : 'disabled'.tr, + // style: TextStyle(fontSize: 11.sp, color: subColor), + // ), + // ), + ], + ); + } + + int total = 0; + int remain = 0; + if (User.isLoggedIn()) { + final u = User.instance; + if (type == UsageType.AI) { + total = u.aichattotalintegral; + remain = u.aichatintegral; + } else if (type == UsageType.Meet) { + total = u.meettotalintegral; + remain = u.meetintegral; + } else if (type == UsageType.Translation) { + total = u.tradetotalintegral; + remain = u.tradeintegral; + } + } + final progress = (total > 0) ? (remain / total).clamp(0.0, 1.0) : 0.0; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LinearProgressIndicator( + value: progress, + minHeight: 8.h, + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[300], + valueColor: AlwaysStoppedAnimation( + isDarkMode ? Colors.blue[300]! : Colors.blue[600]!), + ), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${'totalCount'.tr}:$total', + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + Text( + '${'remaining'.tr}:$remain', + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + ], + ), + ], + ); + } + + Widget _buildGoodsCardByType(DBGoods item, UsageType type, bool isDarkMode) { + final iconAndColor = _iconColorForType(type, isDarkMode); + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Container( + width: 42.w, + height: 42.w, + decoration: BoxDecoration( + color: iconAndColor.background, + borderRadius: BorderRadius.circular(21.r), + ), + child: Icon(iconAndColor.icon, color: iconAndColor.foreground), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(localizedGoodsName(item), + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + )), + SizedBox(height: 4.h), + Text(localizedGoodsDesc(item), + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[600], + )), + ], + ), + ), + SizedBox(width: 12.w), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text('¥${item.priceYuan.toStringAsFixed(2)}', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + )), + SizedBox(height: 8.h), + GestureDetector( + onTap: () => controller.purchase(item), + child: Container( + padding: + EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: Colors.blue[600], + borderRadius: BorderRadius.circular(20.r), + ), + child: Text( + 'purchase'.tr, + style: TextStyle( + fontSize: 12.sp, + color: Colors.white, + ), + ), + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildMetricsForType(UsageType type, bool isDarkMode) { + if (type == UsageType.Vip) { + return _buildVipMetricsCard(isDarkMode); + } + return _buildQuotaCard(type, isDarkMode); + } + + Widget _buildVipMetricsCard(bool isDarkMode) { + final textColor = isDarkMode ? Colors.white : Colors.black; + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + int viplv = 0; + int vipexptime = 0; + if (User.isLoggedIn()) { + final u = User.instance; + viplv = u.viplv; + vipexptime = u.vipexptime; + } + DateTime? expiry; + if (vipexptime > 0) { + expiry = vipexptime > 1000000000000 + ? DateTime.fromMillisecondsSinceEpoch(vipexptime) + : DateTime.fromMillisecondsSinceEpoch(vipexptime * 1000); + } + final iconColors = _iconColorForType(UsageType.Vip, isDarkMode); + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Container( + width: 42.w, + height: 42.w, + decoration: BoxDecoration( + color: iconColors.background, + borderRadius: BorderRadius.circular(21.r), + ), + child: Icon(iconColors.icon, color: iconColors.foreground), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Lv.$viplv', + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + SizedBox(height: 4.h), + Text( + expiry != null + ? '${'validityPeriod'.tr}${_fmtDate(expiry)}' + : 'notBound'.tr, + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + ], + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.h), + decoration: BoxDecoration( + color: isDarkMode ? Colors.white10 : Colors.black12, + borderRadius: BorderRadius.circular(16.r), + ), + child: Text( + viplv > 0 ? 'enabled'.tr : 'disabled'.tr, + style: TextStyle(fontSize: 11.sp, color: subColor), + ), + ), + ], + ), + ); + } + + Widget _buildQuotaCard(UsageType type, bool isDarkMode) { + int total = 0; + int remain = 0; + if (User.isLoggedIn()) { + final u = User.instance; + if (type == UsageType.AI) { + total = u.aichattotalintegral; + remain = u.aichatintegral; + } else if (type == UsageType.Meet) { + total = u.meettotalintegral; + remain = u.meetintegral; + } else if (type == UsageType.Translation) { + total = u.tradetotalintegral; + remain = u.tradeintegral; + } + } + final progress = (total > 0) ? (remain / total).clamp(0.0, 1.0) : 0.0; + final iconColors = _iconColorForType(type, isDarkMode); + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + final textColor = isDarkMode ? Colors.white : Colors.black; + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 36.w, + height: 36.w, + decoration: BoxDecoration( + color: iconColors.background, + borderRadius: BorderRadius.circular(18.r), + ), + child: Icon(iconColors.icon, color: iconColors.foreground), + ), + SizedBox(width: 8.w), + Text( + 'remainingQuota'.tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + ], + ), + SizedBox(height: 12.h), + LinearProgressIndicator( + value: progress, + minHeight: 8.h, + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[300], + valueColor: AlwaysStoppedAnimation( + isDarkMode ? Colors.blue[300]! : Colors.blue[600]!), + ), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${'totalCount'.tr}:$total', + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + Text( + '${'remaining'.tr}:$remain', + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + ], + ), + ], + ), + ); + } + + Widget _buildGoodsGrid(List list, UsageType type, bool isDarkMode) { + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 12.h, + crossAxisSpacing: 12.w, + childAspectRatio: 1.15, + ), + itemCount: list.length, + itemBuilder: (_, i) => _buildGridItemCard(list[i], type, isDarkMode), + ); + } + + Widget _buildGridItemCard(DBGoods item, UsageType type, bool isDarkMode) { + final iconColors = _iconColorForType(type, isDarkMode); + return Container( + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 34.w, + height: 34.w, + decoration: BoxDecoration( + color: iconColors.background, + borderRadius: BorderRadius.circular(17.r), + ), + child: Icon(iconColors.icon, color: iconColors.foreground), + ), + SizedBox(width: 8.w), + Expanded( + child: Text( + localizedGoodsName(item), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + ], + ), + SizedBox(height: 6.h), + Expanded( + child: Text( + localizedGoodsDesc(item), + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[700], + ), + ), + ), + SizedBox(height: 6.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '¥${item.priceYuan.toStringAsFixed(2)}', + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + GestureDetector( + onTap: () => controller.purchase(item), + child: Container( + padding: + EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.h), + decoration: BoxDecoration( + color: Colors.blue[600], + borderRadius: BorderRadius.circular(8.r), + ), + child: Text( + 'purchase'.tr, + style: TextStyle(fontSize: 12.sp, color: Colors.white), + ), + ), + ), + ], + ), + ], + ), + ); + } + + String _fmtDate(DateTime d) { + final y = d.year.toString().padLeft(4, '0'); + final m = d.month.toString().padLeft(2, '0'); + final day = d.day.toString().padLeft(2, '0'); + return '$y-$m-$day'; + } + + _IconColors _iconColorForType(UsageType type, bool isDarkMode) { + switch (type) { + case UsageType.AI: + return _IconColors( + icon: Icons.smart_toy, + background: isDarkMode + ? Colors.teal[900]!.withOpacity(0.3) + : Colors.teal[100]!, + foreground: isDarkMode ? Colors.teal[300]! : Colors.teal[600]!, + ); + case UsageType.Meet: + return _IconColors( + icon: Icons.people_alt, + background: isDarkMode + ? Colors.pink[900]!.withOpacity(0.3) + : Colors.pink[100]!, + foreground: isDarkMode ? Colors.pink[300]! : Colors.pink[600]!, + ); + case UsageType.Translation: + return _IconColors( + icon: Icons.translate, + background: isDarkMode + ? Colors.indigo[900]!.withOpacity(0.3) + : Colors.indigo[100]!, + foreground: isDarkMode ? Colors.indigo[300]! : Colors.indigo[600]!, + ); + case UsageType.Vip: + case UsageType.UsageNull: + return _IconColors( + icon: Icons.workspace_premium, + background: isDarkMode + ? Colors.amber[900]!.withOpacity(0.3) + : Colors.amber[100]!, + foreground: isDarkMode ? Colors.amber[300]! : Colors.amber[600]!, + ); + } + } + + Widget _buildScenesSection(bool isDarkMode) { + final textColor = isDarkMode ? Colors.white : Colors.black; + final subColor = isDarkMode ? Colors.white70 : Colors.grey[700]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'scene'.tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + SizedBox(height: 10.h), + Wrap( + spacing: 12.w, + runSpacing: 12.h, + children: [ + _buildFeatureCard( + isDarkMode, + icon: Icons.smart_toy, + title: 'aiFunction'.tr, + lines: ['aiHeaderDesc1'.tr, 'aiHeaderDesc2'.tr], + colorSeed: Colors.teal, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.people_alt, + title: 'conferenceAssistant'.tr, + lines: ['meetingHeaderDesc1'.tr, 'meetingHeaderDesc2'.tr], + colorSeed: Colors.pink, + ), + _buildFeatureCard( + isDarkMode, + icon: Icons.translate, + title: 'simultaneousTranslation'.tr, + lines: ['tradeHeaderDesc1'.tr, 'tradeHeaderDesc2'.tr], + colorSeed: Colors.indigo, + ), + ], + ), + SizedBox(height: 12.h), + Text( + 'vipDisclaimer'.tr, + style: TextStyle(fontSize: 12.sp, color: subColor), + ), + ], + ); + } +} + +class _IconColors { + final IconData icon; + final Color background; + final Color foreground; + const _IconColors({ + required this.icon, + required this.background, + required this.foreground, + }); } diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index c814ac4de..bf5a7f72d 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -243,46 +243,59 @@ class SettingsView extends GetView { ), ], ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _buildCategoryItem( - icon: Icons.workspace_premium, - label: 'premiumMember'.tr, - iconBg: isDarkMode - ? Colors.amber[900]!.withOpacity(0.3) - : Colors.amber[100]!, - iconColor: isDarkMode ? Colors.amber[300]! : Colors.amber[600]!, - onTap: () => Get.toNamed(Routes.goodsvip), - ), - _buildCategoryItem( - icon: Icons.translate, - label: 'interpretation'.tr, - iconBg: isDarkMode - ? Colors.indigo[900]!.withOpacity(0.3) - : Colors.indigo[100]!, - iconColor: isDarkMode ? Colors.indigo[300]! : Colors.indigo[600]!, - onTap: () => Get.toNamed(Routes.goodstrade), - ), - _buildCategoryItem( - icon: Icons.file_open, - label: 'conferenceAssistant'.tr, - iconBg: isDarkMode - ? Colors.pink[900]!.withOpacity(0.3) - : Colors.pink[100]!, - iconColor: isDarkMode ? Colors.pink[300]! : Colors.pink[600]!, - onTap: () => Get.toNamed(Routes.goodsmeet), - ), - _buildCategoryItem( - icon: Icons.smart_toy, - label: 'aiFunction'.tr, - iconBg: isDarkMode - ? Colors.teal[900]!.withOpacity(0.3) - : Colors.teal[100]!, - iconColor: isDarkMode ? Colors.teal[300]! : Colors.teal[600]!, - onTap: () => Get.toNamed(Routes.goodsai), - ), - ], + child: InkWell( + onTap: () => Get.toNamed(Routes.goodsvip), + borderRadius: BorderRadius.circular(16.r), + child: Row( + children: [ + Container( + width: 42.w, + height: 42.w, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.amber[900]!.withOpacity(0.3) + : Colors.amber[100]!, + borderRadius: BorderRadius.circular(21.r), + ), + child: Icon( + Icons.workspace_premium, + size: 20.w, + color: isDarkMode ? Colors.amber[300]! : Colors.amber[600]!, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'premiumMember'.tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + SizedBox(height: 4.h), + Text( + '${'higherQuota'.tr} · ${'fastResponse'.tr}', + style: TextStyle( + fontSize: 12.sp, + color: Colors.white70, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + Icon( + Icons.arrow_forward_ios, + size: 16.w, + color: Colors.white70, + ), + ], + ), ), ); } 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 709208fb7..3f3b6e49c 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 @@ -136,7 +136,7 @@ object AgentService : CoroutineScope { private var currentAiJob: Job? = null private var currsessionId: String = "" - private var aiintegral:Int = 0 + private var vipExptime:Int = 0 // 空闲检测相关 private var idleCheckJob: Job? = null @@ -207,8 +207,8 @@ object AgentService : CoroutineScope { usertoken = config["usertoken"]?.toString() ?: ""; callingToolText = config["callingToolText"]?.toString() ?: ""; insufficientIntegralText = config["insufficientIntegralText"]?.toString() ?: ""; - aiintegral = config["aiintegral"]?.toString()?.toIntOrNull() ?: 0; - + vipExptime = config["vipexptime"]?.toString()?.toIntOrNull() ?: 0; + Log.d(TAG, "Vip 过期时间 ${vipExptime}") // 初始化上下文和服务 this.context = appContext.applicationContext @@ -1013,7 +1013,7 @@ object AgentService : CoroutineScope { } chatApiService.sendMessageStream( sessionid, - aiintegral, + vipExptime, messagesWithSystemPrompt, object : StreamCallback { @@ -1033,7 +1033,7 @@ object AgentService : CoroutineScope { val key = "AICHAT_FUNCTION_" + funcName actions[key] = (actions[key] ?: 0) + 1 } - sendStatisticsEvent(actions) + // sendStatisticsEvent(actions) }catch (e: Exception) { Log.e(TAG, "liwei--------------- AI Call onUsage 异常", e) } @@ -2597,13 +2597,13 @@ object AgentService : CoroutineScope { } for ((k, v) in dataMap) { Log.d(TAG, "统计事件发送成功,响应: $k -> $v") - if(k == "3"){ //AI积分更新 - aiintegral = v + if(k == "4"){ //AI积分更新 + vipExptime = v // 广播一个细化事件,便于前端单独处理 AI 积分更新 sendEvent( "assistant_usages", mapOf( - "integral" to aiintegral + "vipxptime" to vipExptime ) ) } 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 5c0457fdc..c09205d16 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 @@ -80,7 +80,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream config["isttsinterrupt"]?.toString()?.let { intent.putExtra("isttsinterrupt", it) } config["callingToolText"]?.toString()?.let { intent.putExtra("callingToolText", it) } config["insufficientIntegralText"]?.toString()?.let { intent.putExtra("insufficientIntegralText", it) } - config["aiintegral"]?.toString()?.let { intent.putExtra("aiintegral", it) } + config["vipexptime"]?.toString()?.let { intent.putExtra("vipexptime", it) } // 添加 supportedLanguages 参数处理 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 219ba870b..a55ea8d1b 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 @@ -70,7 +70,7 @@ class BleMediaService : Service(), CoroutineScope { private const val APP_NAVIGATION_MODE = "navigationMode" private const val APP_CALLING_TOOL_TEXT = "callingToolText" // 工具调用提示 private const val APP_INSUFFICIENT_INTEGRAL_TEXT = "insufficientIntegralText" // 积分不足提示 - private const val KEY_AI_INTEGRAL = "aiIntegral" // 积分不足提示 + private const val KEY_VIP_EXPTIME = "vipexptime" // 会员到期时间 // 用于跟踪服务是否正在运行 private val isRunning = AtomicBoolean(false) @@ -582,7 +582,7 @@ private var companionManager: BleCompanionManager? = null it.getStringExtra(APP_NAVIGATION_MODE)?.let { value -> config[APP_NAVIGATION_MODE] = value } it.getStringExtra(APP_CALLING_TOOL_TEXT)?.let { value -> config[APP_CALLING_TOOL_TEXT] = value } it.getStringExtra(APP_INSUFFICIENT_INTEGRAL_TEXT)?.let { value -> config[APP_INSUFFICIENT_INTEGRAL_TEXT] = value } - it.getIntExtra(KEY_AI_INTEGRAL, config[KEY_AI_INTEGRAL] as? Int ?: 0) + it.getIntExtra(KEY_VIP_EXPTIME, config[KEY_VIP_EXPTIME] as? Int ?: 0) it.getStringExtra("isttsinterrupt")?.let { value -> config["isttsinterrupt"] = value } // 提取 supportedLanguages 参数 @@ -647,7 +647,7 @@ private var companionManager: BleCompanionManager? = null config[APP_NAVIGATION_MODE] = getConfigString(prefs, APP_NAVIGATION_MODE, "") config[APP_CALLING_TOOL_TEXT] = getConfigString(prefs, APP_CALLING_TOOL_TEXT, "") config[APP_INSUFFICIENT_INTEGRAL_TEXT] = getConfigString(prefs, APP_INSUFFICIENT_INTEGRAL_TEXT, "") - config[KEY_AI_INTEGRAL] = getConfigInt(prefs, KEY_AI_INTEGRAL, 0) + config[KEY_VIP_EXPTIME] = getConfigInt(prefs, KEY_VIP_EXPTIME, 0) // 添加 supportedLanguages 参数处理 val supportedLanguagesStr = getConfigString(prefs, KEY_SUPPORTED_LANGUAGES, "") diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index 30308b0c3..367f450ec 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -69,6 +69,8 @@ class AgentServiceImpl: NSObject { public var insufficientIntegralText = "" // 积分不足提示 private var navigationMode = "" + //会员过期时间 + public var vipexptime = 0 private var isInitialized: Bool = false private var isRecognizing: Bool = false @@ -183,7 +185,12 @@ class AgentServiceImpl: NSObject { if let callingToolText = config["callingToolText"] as? String { self.callingToolText = callingToolText } - + if let vipexptime = config["vipexptime"] as? Int { + self.vipexptime = vipexptime + } + //打印日志输出vipexptime + os_log("Vip 过期时间 %d", log: logger, type: .info, vipexptime) + if let insufficientIntegralText = config["insufficientIntegralText"] as? String { self.insufficientIntegralText = insufficientIntegralText } @@ -343,8 +350,6 @@ class AgentServiceImpl: NSObject { sendError("初始化ChatAPI服务失败", code: "CHATAPI_INIT_ERROR") return false } - - return true } @@ -719,7 +724,7 @@ class AgentServiceImpl: NSObject { azureTtsHelper?.startspeak(sessionid: sessionid) os_log("设置回调并调用sendMessageStream", log: logger, type: .info) chatApiService.setStreamCallback(sessionid,callback) - chatApiService.sendMessageStream(sessionid,messages: messages) + chatApiService.sendMessageStream(sessionid,vipexptime: vipexptime,messages: messages) audioPlayer?.playAwaitSound() os_log("sendMessageStream调用完成", log: logger, type: .info) } 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 bced9835b..7be026f4c 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 @@ -347,11 +347,12 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor /** * 发送消息(流式输出)- 兼容JSONArray格式 */ - fun sendMessageStream(sessionid: String,integral: Int, messages: org.json.JSONArray, callback: StreamCallback) { + fun sendMessageStream(sessionid: String,vipexptime: Int, messages: org.json.JSONArray, callback: StreamCallback) { // 1. 立即中止旧会话 abortCurrentSession() - if (integral <= 0) { - callback.onError(sessionid,2001,"积分不足") + // 判断时间是否过期:如果 vipexptime 小于当前时间戳(秒),则视为过期 + if (vipexptime <= (System.currentTimeMillis() / 1000)) { + callback.onError(sessionid,2001,"会员过期") return } currSessionId = sessionid diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift index c37e74bae..58a940cc1 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift @@ -271,12 +271,13 @@ public class ChatApiService: NSObject { throw ChatApiException("无效的响应格式") } - /// 发送消息(流式输出) - // MARK: - 流式请求实现 -public func sendMessageStream(_ sessionId: String, messages: [[String: Any]]) { +/// 发送消息(流式输出) + // MARK: - 流式请求实现 +public func sendMessageStream(_ sessionId: String,vipexptime: Int, messages: [[String: Any]]) { // 1. 立即中止旧会话 abortCurrentSession() + // 2. 创建新会话 currSessionId = sessionId @@ -289,6 +290,11 @@ public func sendMessageStream(_ sessionId: String, messages: [[String: Any]]) { getSessionCallback(sessionId)?.onError(sessionId,1000,"当前网络不可用") return } + let nowSec = Int(Date().timeIntervalSince1970) + if vipexptime <= 0 || nowSec >= vipexptime { + getSessionCallback(sessionId)?.onError(sessionId,2001,"会员已过期") + return + } // 保存当前消息和回调 currentMessages = messages @@ -400,7 +406,7 @@ public func sendMessageStream(_ sessionId: String, messages: [[String: Any]]) { self.getSessionCallback(sessionId)?.onUsage(sessionId, promptTokens, completionTokens, totalTokens) // 处理工具调用 - let hasToolCalls = await self.processToolCalls(sessionId) + let hasToolCalls = await self.processToolCalls(sessionId,vipexptime: vipexptime) if !hasToolCalls { self.getSessionCallback(sessionId)?.onComplete(sessionId) self.clearCurrentSession() @@ -444,7 +450,7 @@ private func roughTokenCount(text: String) -> Int { } /// 处理工具调用 -private func processToolCalls(_ sessionId:String) async -> Bool { +private func processToolCalls(_ sessionId:String,vipexptime: Int) async -> Bool { // 验证工具调用集合不为空 if toolCalls.isEmpty { print("[Session: \(sessionId)] 没有工具调用需要处理") @@ -563,6 +569,7 @@ private func processToolCalls(_ sessionId:String) async -> Bool { // 将结果发送回OpenAI继续对话 await self.sendFunctionCallResultInternal( sessionId: sessionId, + vipexptime: vipexptime, messages: self.currentMessages, functionCall: functionCall, functionResult: self.jsonToString(result) ?? "{}" @@ -575,6 +582,7 @@ private func processToolCalls(_ sessionId:String) async -> Bool { let errorMessage = "工具调用处理失败: \(error.localizedDescription)" await self.sendFunctionCallResultInternal( sessionId: sessionId, + vipexptime: vipexptime, messages: self.currentMessages, functionCall: functionCall, functionResult: errorMessage @@ -623,6 +631,7 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] { /// 发送函数调用结果(内部方法) private func sendFunctionCallResultInternal( sessionId:String, + vipexptime: Int, messages: [[String: Any]], functionCall: [String: Any], functionResult: String @@ -656,7 +665,7 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] { ]) // 重新发起对话,让AI基于工具调用结果生成回复 - sendMessageStream(sessionId,messages: fullMessages) + sendMessageStream(sessionId,vipexptime: vipexptime,messages: fullMessages) } /// 取消当前流式请求 @@ -1090,4 +1099,3 @@ private class LocalFunctionHandler: FunctionHandler { return "LOCAL_FUNCTION:\(functionName)" } } -