diff --git a/lib/data/services/network/api.dart b/lib/data/services/network/api.dart index d5a1747ce..c83f11906 100644 --- a/lib/data/services/network/api.dart +++ b/lib/data/services/network/api.dart @@ -244,36 +244,37 @@ class Api { ); } - /// 资源使用上报(`/api/home/user_usages`) + /// 资源使用/用量上报接口(`/api/home/user_usages`) /// - /// 用途:在 AI 聊天等场景中,上报本次会话的消耗与功能调用次数,服务端据此统计并更新积分。 + /// 用途:上报翻译(Translation)和会议(Meet)的用量,服务端据此统计并回传最新余额/积分。 /// - /// 请求方式:`POST /api/home/user_usages` - /// - /// 认证:已通过全局拦截器自动携带 `Authorization`(见 `AuthInterceptor`)。 + /// 当前在 Dart 侧的使用场景: + /// - 会议助手 ASR:`UsageStatsService._reportMeetingUsageToServer` + /// - 翻译/同传 ASR:`UsageStatsService._reportAsrUsageToServer` /// /// 参数(`params`)结构: - /// - `usagetype`:`int` 类型,使用场景类型;Android 端 Agent 使用 `3` 表示 AICHAT。 + /// - `usagetype`:`int` 类型,使用场景类型(对应 `UsageType.toInt`),目前仅使用: + /// - `1`:翻译(Translation) + /// - `2`:会议(Meet) /// - `usages`:`Map`,各项指标的键值对,例如: - /// - `AICHAT_PROMPT_TOKENS`:本次对话的提示词 Token 数 - /// - `AICHAT_COMPLETION_TOKENS`:本次对话的生成 Token 数 - /// - `AICHAT_FUNCTION_{函数名}`:被调用的函数次数(按函数名累加) + /// - `MEETING_ASR_AUDIO_SECONDS`:会议识别音频时长(秒) + /// - `TRANSLATION_MODE_{mode}`:某翻译模式下累计有效识别秒数 + /// - `TRANSLATION_ASR_LANGUAGE_{language}`:某语言/语言对下的调用次数 /// - /// 返回:标准封装后 `code == 0` 时返回 `data`。其中 `data.usages` 可能包含键 `"3"`,表示最新的 AI 积分值。 + /// 返回: + /// - 经过 `AuthInterceptor` 处理后,本方法返回值即为服务端响应体中的 `data` 字段。 + /// - 若上报成功,`data['usages']` 为 `Map`,内部各键的含义与积分/余额更新规则由服务端约定。 /// - /// 使用示例: + /// 使用示例(Dart): /// ```dart /// final params = { - /// 'usagetype': 3, // AICHAT + /// 'usagetype': UsageType.Meet.toInt, /// 'usages': { - /// 'AICHAT_PROMPT_TOKENS': 120, - /// 'AICHAT_COMPLETION_TOKENS': 256, - /// 'AICHAT_FUNCTION_toolX': 1, + /// 'MEETING_ASR_AUDIO_SECONDS': 120, /// } /// }; /// final data = await Api.usagesResPoints(params); /// final usages = (data['usages'] as Map?)?.cast(); - /// final aiIntegral = usages?['3']; // 服务端返回的最新 AI 积分(如有) /// ``` static usagesResPoints([params]) { return DioManager().request( diff --git a/lib/data/services/version_update_service.dart b/lib/data/services/version_update_service.dart index 4c37e851f..b25f861b2 100644 --- a/lib/data/services/version_update_service.dart +++ b/lib/data/services/version_update_service.dart @@ -124,16 +124,15 @@ class VersionUpdateService extends GetxService { //_channel = platformInfo.channel; _lastCheckTime = DateTime.now(); - // 调用后端接口获取最新版本信息 - final response = - await Api.getChannelApp({'channel': platformInfo.channel.value}); - // final response = await Api.getChannelApps(); + // 获取所有渠道包信息 + final response = await Api.getChannelApps(); - _logger.d('lxm--获取渠道包信息:$response'); + _logger.d('lxm--获取所有渠道包信息:$response'); - if (response == null || !response.containsKey('app')) { - //[2]获取渠道包信息失败: 响应为空或不包含app字段 - _logger.w('lxm--获取渠道包信息失败: 响应为空或不包含app字段'); + final List apps = _parseAppsList(response); + + if (apps.isEmpty) { + _logger.w('lxm--获取所有渠道包信息失败或为空'); return VersionUpdateInfo( hasUpdate: false, currentVersion: currentVersion, @@ -142,24 +141,38 @@ class VersionUpdateService extends GetxService { ); } - //final channelAppResponse = ChannelAppResponse.fromJson(response); - // 直接从response中获取app信息 - final appInfo = AppInfo.fromJson(response['app']); - _appInfo = appInfo; - _logger.d('获取到的最新版本信息: ${appInfo.toJson()}'); - _latestVersion = appInfo.version; - _updateUrl = appInfo.address; + final Map appIndex = _buildChannelIndex(apps); - // 设置游客登录显示状态(先用当前渠道结果作为回退,再用.env渠道覆盖以保持一致) - _isShowTourists = appInfo.tourists; - _allowSkipDeviceBinding = appInfo.allowskipdevicebinding; + // 依据设备品牌渠道用于版本更新链接 + final brandChannel = platformInfo.channel; - //print('lxm--_isShowTourists: $_isShowTourists'); - if (Platform.isAndroid && _channel != platformInfo.channel) { - //[4]当前渠道与.env渠道不同,需要从.env渠道获取游客登录配置 - await _refreshTouristsFromEnv(); + // 环境变量渠道用于游客登录配置 + final envChannelName = dotenv.env['Channel'] ?? 'OFFICIAL'; + final envChannel = UserChannel.fromName(envChannelName); + final AppInfo? envAppInfo = appIndex[envChannel]; + + // 若不存在品牌渠道信息,降级为环境渠道;仍不存在则返回无更新 + final AppInfo? chosenAppInfo = + _getChannelAppInfo(appIndex, brandChannel, envChannel); + if (chosenAppInfo == null) { + _logger.w('lxm--未找到品牌或环境渠道的应用信息,返回无更新'); + return VersionUpdateInfo( + hasUpdate: false, + currentVersion: currentVersion, + latestVersion: currentVersion, + updateType: UpdateType.none, + ); } + _appInfo = chosenAppInfo; + _latestVersion = chosenAppInfo.version; + _updateUrl = chosenAppInfo.address; + + // 设置游客登录显示状态(先用当前渠道结果作为回退,再用.env渠道覆盖以保持一致) + _isShowTourists = (envAppInfo ?? chosenAppInfo).tourists; + _allowSkipDeviceBinding = + (envAppInfo ?? chosenAppInfo).allowskipdevicebinding; + _logger.d('lxm--从服务器获取游客登录配置(最终): ${_isShowTourists}'); // if (channelAppResponse.code != 0 || channelAppResponse.data == null) { @@ -173,20 +186,21 @@ class VersionUpdateService extends GetxService { // } // final appInfo = channelAppResponse.data!.app; - _hasUpdate = _compareVersions(currentVersion, appInfo.version); + _hasUpdate = _compareVersions(currentVersion, chosenAppInfo.version); // 判断更新类型:如果主版本号不同则强制更新 UpdateType updateType = UpdateType.none; if (_hasUpdate!) { - _isForceUpdate = _shouldForceUpdate(currentVersion, appInfo.version); + _isForceUpdate = + _shouldForceUpdate(currentVersion, chosenAppInfo.version); updateType = _isForceUpdate! ? UpdateType.force : UpdateType.optional; } //[3]返回版本更新信息 return VersionUpdateInfo( hasUpdate: _hasUpdate!, currentVersion: currentVersion.split('+')[0], - latestVersion: appInfo.version.split('+')[0], - appInfo: appInfo, + latestVersion: chosenAppInfo.version.split('+')[0], + appInfo: chosenAppInfo, updateType: updateType, ); } catch (e) { @@ -296,33 +310,6 @@ class VersionUpdateService extends GetxService { } } - /// 从.env文件刷新游客登录配置 - Future _refreshTouristsFromEnv() async { - try { - final envChannelName = dotenv.env['Channel']; - if (envChannelName == null || envChannelName.isEmpty) { - _logger.w('未配置.env Channel,沿用当前渠道的游客配置: ${_isShowTourists}'); - return; - } - final envChannel = UserChannel.fromName(envChannelName); - - final resp = await Api.getChannelApp({'channel': envChannel.value}); - // _logger.d('lxm--获取渠道包信息:$resp'); - if (resp != null && resp.containsKey('app')) { - final envAppInfo = AppInfo.fromJson(resp['app']); - // print('lxm--envAppInfo: ${envAppInfo.toJson()}'); - _isShowTourists = envAppInfo.tourists; - _allowSkipDeviceBinding = envAppInfo.allowskipdevicebinding; - _logger.d( - 'lxm--游客登录配置已按.env${envChannel.value}渠道同步: ${envAppInfo.tourists} (channel=$envChannelName),_isShowTourists: $_isShowTourists,_allowSkipDeviceBinding: $_allowSkipDeviceBinding'); - } else { - _logger.w('按.env渠道获取游客配置失败:响应为空或不含app字段'); - } - } catch (e) { - _logger.w('按.env渠道同步游客配置失败: $e'); - } - } - /// 获取iOS设备品牌信息 Future _getIosDeviceBrand() async { try { @@ -415,6 +402,45 @@ class VersionUpdateService extends GetxService { return latestVersions.length > currentVersions.length; } + /// 解析响应数据为AppInfo列表 + List _parseAppsList(dynamic response) { + try { + final appsJson = (response is Map) ? response['apps'] : null; + if (appsJson is List) { + return appsJson + .whereType>() + .map((e) => AppInfo.fromJson(e)) + .toList(); + } + } catch (e) { + _logger.e('解析渠道列表失败: $e'); + } + return []; + } + + /// 构建渠道索引 + Map _buildChannelIndex(List apps) { + final index = {}; + for (final app in apps) { + index[app.channel] = app; + } + return index; + } + + /// 获取渠道信息,带备选策略 + /// + /// 优先返回主渠道的信息,如果主渠道不存在则返回备选渠道的信息。 + /// 主要用于: + /// 1. 版本更新:优先使用设备品牌渠道,备选使用环境变量渠道 + /// 2. 功能配置:优先使用环境变量渠道,备选使用版本更新渠道 + AppInfo? _getChannelAppInfo( + Map index, + UserChannel primary, + UserChannel? fallback, + ) { + return index[primary] ?? (fallback != null ? index[fallback] : null); + } + /// 解析版本号字符串为数字数组 List _parseVersion(String version) { final parts = version.split('.');