Browse Source

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

newdev_shunjiawei
tanlongsheng 1 year ago
parent
commit
6920d8a97c
  1. 2
      ios/Runner.xcodeproj/project.pbxproj
  2. 2
      ios/Runner/Info.plist
  3. 42
      lib/data/services/ble_manager.dart
  4. 14
      lib/data/services/network/dio_manager.dart
  5. 44
      lib/modules/agent/controllers/agent_controller.dart
  6. 3
      lib/modules/devices/controllers/devices_controller.dart
  7. 30
      lib/modules/pairing/controllers/pairing_controller.dart
  8. 13
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  9. 80
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt
  10. 7
      local_plugins/agent_service/ios/agent_service/Package.swift
  11. 34
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  12. 15
      local_plugins/agent_service/lib/agent_service.dart
  13. 156
      local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/UserPortrait.swift

2
ios/Runner.xcodeproj/project.pbxproj

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 60;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */

2
ios/Runner/Info.plist

@ -35,7 +35,7 @@
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>wx5bbb7be9324f3e2d</string>
<string>wx4b5ec8957ad5e425</string>
</array>
</dict>
<dict>

42
lib/data/services/ble_manager.dart

@ -519,7 +519,11 @@ class BleManager extends GetxService {
Future<bool> startAgentService() async {
try {
// 从.env文件直接读取环境变量
final serverurl = dotenv.env['SERVER_URL'] ?? '';
var serverurl = dotenv.env['SERVER_URL'] ?? '';
if (Get.locale?.countryCode != 'CN') {
serverurl = dotenv.env['HW_SERVER_URL'] ?? '';
}
var jsonString = _storage.read<String>("logintoken");
Logger.info('启动BLE服务,serverurl: ${serverurl}');
// final azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? 'eastasia';
@ -549,6 +553,40 @@ class BleManager extends GetxService {
// 从Flutter资源包中加载.mcp.json
final jsonData = AppConfig.mcpConfig();
// 根据APP语言动态配置语音识别语言
List<String> supportedLanguages = [];
try {
// 使用 GetX 获取 APP 当前语言环境 (Locale)
final appLocale = Get.locale;
if (appLocale != null) {
final lang = appLocale.languageCode; // e.g., 'en'
final country = appLocale.countryCode; // e.g., 'US'
// 构造 Azure SDK 需要的格式 (e.g., 'en-US')
if (country != null && country.isNotEmpty) {
supportedLanguages.add('$lang-$country');
} else {
supportedLanguages.add(lang); // fallback only language code
}
}
// 添加 en-US 作为备选
if (!supportedLanguages.contains('en-US')) {
supportedLanguages.add('en-US');
}
// 最终兜底
if (supportedLanguages.isEmpty) {
supportedLanguages.add('en-US');
}
Logger.info('动态配置语音识别语言: ${supportedLanguages.join(', ')}');
} catch (e) {
Logger.error('获取APP语言配置失败,使用默认语言: $e');
supportedLanguages = ['zh-CN', 'en-US'];
}
final config = <String, dynamic>{
// 语音代理配置参数
'azureSpeechKey': azureSpeechKey,
@ -563,7 +601,7 @@ class BleManager extends GetxService {
'volcanoToken': 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En',
'serverurl': serverurl,
'usertoken': jsonString,
'supportedLanguages': ['zh-CN'],
'supportedLanguages': supportedLanguages,
};
// 记录日志(不记录敏感信息)

14
lib/data/services/network/dio_manager.dart

@ -1,5 +1,6 @@
import 'package:dio/dio.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get/get.dart' hide Response;
import 'auth_interceptor.dart';
import 'nw_method.dart';
@ -9,7 +10,18 @@ class DioManager {
factory DioManager() => _shared;
late Dio dio;
final baseUrl = dotenv.env['SERVER_URL'] ?? "";
// 根据地区获取服务器URL
String get baseUrl {
final countryCode = Get.deviceLocale?.countryCode?.toUpperCase();
final isChinaMainland = countryCode == 'CN';
if (!isChinaMainland) {
return dotenv.env['HW_SERVER_URL'] ?? dotenv.env['SERVER_URL'] ?? '';
} else {
return dotenv.env['SERVER_URL'] ?? '';
}
}
DioManager._internal() {
BaseOptions options = BaseOptions(
baseUrl: baseUrl,

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

@ -153,7 +153,7 @@ class AgentController extends GetxController {
_setupMediaSessionCallbacks(); //设置媒体会话回调
_setupTextInputListener(); //设置文本输入监听器
_configureSpeechRecognitionLanguages(); //配置语音识别语言
startVoiceInput(); //开启语音输入
}
@ -1013,49 +1013,7 @@ class AgentController extends GetxController {
}
}
//根据APP语言配置语音识别语言
Future<void> _configureSpeechRecognitionLanguages() async {
try {
// 1. 使用 GetX 获取 APP 当前语言环境 (Locale)
final appLocale = Get.locale;
List<String> languagesToSupport = [];
if (appLocale != null) {
final lang = appLocale.languageCode; // e.g., 'en'
final country = appLocale.countryCode; // e.g., 'US'
// 2. 构造 Azure SDK 需要的格式 (e.g., 'en-US')
if (country != null && country.isNotEmpty) {
languagesToSupport.add('$lang-$country');
} else {
languagesToSupport.add(lang); // fallback only language code
}
}
// 3. 添加 en-US 作为备选
if (!languagesToSupport.contains('en-US')) {
languagesToSupport.add('en-US');
}
// 4. 最终兜底
if (languagesToSupport.isEmpty) {
languagesToSupport.add('en-US');
}
// 5. 调用原生设置
Logger.i(TAG, '支持的语言: ${languagesToSupport.join(', ')}');
AgentService.setSupportedLanguages(languagesToSupport).then((success) {
if (success) {
Logger.i(TAG, 'asr的语言已根据 APP 设置语言: ${languagesToSupport.join(', ')}');
} else {
Logger.w(TAG, '根据 APP 设置asr语言失败');
}
});
} catch (e) {
Logger.e(TAG, "获取 APP 语言并设置失败: $e");
AgentService.setSupportedLanguages(['zh-CN', 'en-US']);
}
}
// 开始语音输入
Future<void> startVoiceInput() async {

3
lib/modules/devices/controllers/devices_controller.dart

@ -28,9 +28,8 @@ class DevicesController extends GetxController {
for (var device in User.instance.devices) {
if (!devices.containsKey(device.id)) {
devices[device.devicetype] = [];
} else {
devices[device.devicetype]!.add(device);
}
devices[device.devicetype]!.add(device);
}
}

30
lib/modules/pairing/controllers/pairing_controller.dart

@ -613,8 +613,11 @@ class PairingController extends GetxController {
final names = AppConfig.allproductNames();
// 检查是否已有绑定的设备
print("device.address${device.address}");
final existingDevice = User.instance.devices.firstWhere(
(d) => d.devicetype == 1 && d.devicemac == device.address,
var existingDevice = User.instance.devices.firstWhere(
(d) =>
d.devicetype == 1 &&
d.devicename == device.name &&
d.devicemac == device.address,
orElse: () => UserDevice(
id: -1,
uid: '',
@ -627,7 +630,28 @@ class PairingController extends GetxController {
// 已经绑定
return 9;
}
if (Platform.isAndroid) {
// 检查是否已有BLE绑定的设备
existingDevice = User.instance.devices.firstWhere(
(d) =>
d.devicetype == 2 &&
d.devicename == device.name &&
d.devicemac == device.address,
orElse: () => UserDevice(
id: -1,
uid: '',
devicetype: -1,
devicename: '',
devicemac: '',
license: ''),
);
if (existingDevice.id != -1) {
_verifiedDeviceName = device.name;
_verifiedDeviceAddress = device.address;
// 已经绑定
return 10;
}
}
print(
'获取到 deviceName: $deviceName address: ${device.address} names:$names');
if (names.contains(deviceName)) {

13
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt

@ -74,6 +74,19 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
config["serverurl"]?.toString()?.let { intent.putExtra("serverurl", it) }
config["usertoken"]?.toString()?.let { intent.putExtra("usertoken", it) }
// 添加 supportedLanguages 参数处理
config["supportedLanguages"]?.let { languages ->
when (languages) {
is List<*> -> {
// 将 List<String> 转换为 ArrayList<String> 以便传递给 Intent
val languageList = languages.filterIsInstance<String>()
if (languageList.isNotEmpty()) {
intent.putStringArrayListExtra("supportedLanguages", ArrayList(languageList))
}
}
}
}
// 启动前台服务
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

80
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt

@ -51,6 +51,7 @@ class BleMediaService : Service(), CoroutineScope {
private const val KEY_SERVER_URL = "serverurl"
private const val KEY_USER_TOKEN = "usertoken"
private const val KEY_SUPPORTED_LANGUAGES = "supportedLanguages"
// 用于跟踪服务是否正在运行
private val isRunning = AtomicBoolean(false)
@ -130,7 +131,10 @@ class BleMediaService : Service(), CoroutineScope {
Log.d(TAG, "当前配置参数: $config")
// 检查必要的配置是否存在
if (config[KEY_AZURE_SPEECH_KEY].isNullOrEmpty() || config[KEY_OPENAI_API_KEY].isNullOrEmpty()) {
val azureSpeechKey = config[KEY_AZURE_SPEECH_KEY]?.toString() ?: ""
val openaiApiKey = config[KEY_OPENAI_API_KEY]?.toString() ?: ""
if (azureSpeechKey.isEmpty() || openaiApiKey.isEmpty()) {
Log.w(TAG, "语音代理初始化失败: API密钥未配置")
return
}
@ -159,8 +163,8 @@ class BleMediaService : Service(), CoroutineScope {
/**
* 从Intent中提取配置参数
*/
private fun extractConfigFromIntent(intent: Intent?): Map<String, String> {
val config = mutableMapOf<String, String>()
private fun extractConfigFromIntent(intent: Intent?): Map<String, Any> {
val config = mutableMapOf<String, Any>()
intent?.let {
// 提取可能存在的配置参数
it.getStringExtra(KEY_AZURE_SPEECH_KEY)?.let { value -> config[KEY_AZURE_SPEECH_KEY] = value }
@ -175,6 +179,13 @@ class BleMediaService : Service(), CoroutineScope {
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 }
// 提取 supportedLanguages 参数
it.getStringArrayListExtra(KEY_SUPPORTED_LANGUAGES)?.let { languages ->
if (languages.isNotEmpty()) {
config[KEY_SUPPORTED_LANGUAGES] = languages
}
}
}
Log.i(TAG, "从Intent中提取到配置参数: $config")
return config
@ -183,11 +194,25 @@ class BleMediaService : Service(), CoroutineScope {
/**
* 将配置参数保存到SharedPreferences
*/
private fun saveConfigToPreferences(config: Map<String, String>, prefs: SharedPreferences) {
private fun saveConfigToPreferences(config: Map<String, Any>, prefs: SharedPreferences) {
prefs.edit().apply {
config.forEach { (key, value) ->
if (value.isNotEmpty()) {
putString(key, value)
when (value) {
is String -> {
if (value.isNotEmpty()) {
putString(key, value)
}
}
is List<*> -> {
// 处理 supportedLanguages 列表
if (key == KEY_SUPPORTED_LANGUAGES) {
val languageList = value.filterIsInstance<String>()
if (languageList.isNotEmpty()) {
// 将语言列表转换为逗号分隔的字符串保存
putString(key, languageList.joinToString(","))
}
}
}
}
}
apply()
@ -197,21 +222,34 @@ class BleMediaService : Service(), CoroutineScope {
/**
* 从SharedPreferences获取配置
*/
private fun getConfigFromPreferences(prefs: SharedPreferences): Map<String, String> {
return mapOf(
KEY_AZURE_SPEECH_KEY to getConfigString(prefs, KEY_AZURE_SPEECH_KEY, ""),
KEY_AZURE_SPEECH_REGION to getConfigString(prefs, KEY_AZURE_SPEECH_REGION, "eastasia"),
KEY_OPENAI_API_KEY to getConfigString(prefs, KEY_OPENAI_API_KEY, ""),
KEY_OPENAI_BASE_URL to getConfigString(prefs, KEY_OPENAI_BASE_URL, "https://api.openai.com/v1"),
KEY_OPENAI_MODEL to getConfigString(prefs, KEY_OPENAI_MODEL, "gpt-3.5-turbo"),
KEY_OPENAI_VISION_MODEL to getConfigString(prefs, KEY_OPENAI_VISION_MODEL, "gpt-4-vision-preview"),
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_SERVER_URL to getConfigString(prefs, KEY_SERVER_URL, ""),
KEY_USER_TOKEN to getConfigString(prefs, KEY_USER_TOKEN, "")
)
private fun getConfigFromPreferences(prefs: SharedPreferences): Map<String, Any> {
val config = mutableMapOf<String, Any>()
// 添加字符串类型的配置参数
config[KEY_AZURE_SPEECH_KEY] = getConfigString(prefs, KEY_AZURE_SPEECH_KEY, "")
config[KEY_AZURE_SPEECH_REGION] = getConfigString(prefs, KEY_AZURE_SPEECH_REGION, "eastasia")
config[KEY_OPENAI_API_KEY] = getConfigString(prefs, KEY_OPENAI_API_KEY, "")
config[KEY_OPENAI_BASE_URL] = getConfigString(prefs, KEY_OPENAI_BASE_URL, "https://api.openai.com/v1")
config[KEY_OPENAI_MODEL] = getConfigString(prefs, KEY_OPENAI_MODEL, "gpt-3.5-turbo")
config[KEY_OPENAI_VISION_MODEL] = getConfigString(prefs, KEY_OPENAI_VISION_MODEL, "gpt-4-vision-preview")
config[KEY_SYSTEM_PROMPT] = getConfigString(prefs, KEY_SYSTEM_PROMPT, "gpt-3.5-turbo")
config[KEY_MCP_SERVER] = getConfigString(prefs, KEY_MCP_SERVER, "")
config[KEY_VOLCANO_APP_ID] = getConfigString(prefs, KEY_VOLCANO_APP_ID, "")
config[KEY_VOLCANO_TOKEN] = getConfigString(prefs, KEY_VOLCANO_TOKEN, "")
config[KEY_SERVER_URL] = getConfigString(prefs, KEY_SERVER_URL, "")
config[KEY_USER_TOKEN] = getConfigString(prefs, KEY_USER_TOKEN, "")
// 添加 supportedLanguages 参数处理
val supportedLanguagesStr = getConfigString(prefs, KEY_SUPPORTED_LANGUAGES, "")
if (supportedLanguagesStr.isNotEmpty()) {
// 将逗号分隔的字符串转换回语言列表
val languageList = supportedLanguagesStr.split(",").map { it.trim() }.filter { it.isNotEmpty() }
if (languageList.isNotEmpty()) {
config[KEY_SUPPORTED_LANGUAGES] = languageList
}
}
return config
}
/**

7
local_plugins/agent_service/ios/agent_service/Package.swift

@ -14,7 +14,9 @@ let package = Package(
.package(path: "../../../azure_speech/ios/azure_speech"),
.package(path: "../../../chat_api/ios/chat_api"),
.package(path: "../../../chat_storage/ios/chat_storage"),
.package(path: "../../../music_service/ios/music_service")
.package(path: "../../../music_service/ios/music_service"),
.package(path: "../../../music_service/ios/user_portrait")
],
targets: [
.target(
@ -26,7 +28,8 @@ let package = Package(
.product(name: "azure-speech", package: "azure_speech"),
.product(name: "chat-api", package: "chat_api"),
.product(name: "chat-storage", package: "chat_storage"),
.product(name: "music-service", package: "music_service")
.product(name: "music-service", package: "music_service"),
.product(name: "user-portrait", package: "user_portrait"),
],
path: "Sources/agent_service",
resources: [

34
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -8,6 +8,7 @@ import os.log
import chat_api
import chat_storage
import music_service
import user_portrait
protocol AgentServiceListener: AnyObject {
func onEvent(eventName: String, data: [String: Any])
@ -78,7 +79,7 @@ class AgentServiceImpl: NSObject {
chatApiService = ChatApiService()
// chatStorageHelper 现在是单例,不需要初始化
UserPortrait.shared.initialize()
BleService.shared.setDelegate(self)
}
@ -473,7 +474,7 @@ audioStream.saveAudioDataTo(data: audioData)
if !systemPrompt.isEmpty {
let info: [String: String] = self.locationInfo ?? ["user_location": "还未获取到位置信息", "user_lat": "未开启定位", "user_lng": "未开启定位"]
let info: [String: String] = UserPortrait.shared.getAll()
let _systemPrompt = fillTemplate(systemPrompt, with: info)
messages.append(chatApiService.createSystemMessage(content: _systemPrompt))
os_log("添加系统提示消息", log: logger, type: .info)
@ -825,11 +826,34 @@ private func processNavigationCard(_ cardNavigation: [String: Any]) {
}
}
}
func fillTemplate(_ template: String, with values: [String: String]) -> String {
/**
* 填充模板字符串,使用正则表达式匹配 {key} 格式的占位符
* 对应 Kotlin 版本的 fillTemplate 方法
*/
func fillTemplate(_ template: String, with data: [String: String]) -> String {
// 使用正则表达式匹配 {key} 格式的占位符
let regex = try! NSRegularExpression(pattern: "\\{([a-zA-Z0-9_]+)\\}", options: [])
let range = NSRange(location: 0, length: template.utf16.count)
var result = template
for (key, value) in values {
result = result.replacingOccurrences(of: "{\(key)}", with: value)
let matches = regex.matches(in: template, options: [], range: range).reversed()
// 从后往前替换,避免位置偏移问题
for match in matches {
if match.numberOfRanges >= 2 {
let keyRange = match.range(at: 1)
if let swiftKeyRange = Range(keyRange, in: template) {
let key = String(template[swiftKeyRange])
let value = data[key] ?? "未知"
let fullMatchRange = match.range(at: 0)
if let swiftFullRange = Range(fullMatchRange, in: result) {
result.replaceSubrange(swiftFullRange, with: value)
}
}
}
}
return result
}
func clearChatHistory() -> Bool {

15
local_plugins/agent_service/lib/agent_service.dart

@ -332,20 +332,7 @@ class AgentService {
}
}
/// 设置支持的语音识别语言
///
/// [languages] 支持的语言列表,如 ['zh-CN', 'en-US']
/// 返回是否设置成功
static Future<bool> setSupportedLanguages(List<String> languages) async {
try {
final bool result = await _channel.invokeMethod('setSupportedLanguages', {
'languages': languages,
});
return result;
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '设置支持语言失败', e.details);
}
}
/// 停止语音合成
static Future<bool> stopAiSteam() async {

156
local_plugins/user_portrait/ios/user_portrait/Sources/user_portrait/UserPortrait.swift

@ -0,0 +1,156 @@
import Foundation
import UIKit
public class UserPortrait: NSObject {
private static let TAG = "UserPortrait"
private static let PREF_NAME = "deep_voice_user"
// 单例实例
public static let shared = UserPortrait()
private var appContext: UIApplication?
private var userDefaults: UserDefaults?
private var portrait: [String: Any] = [:]
// 私有初始化方法,防止外部创建实例
private override init() {
super.init()
}
/**
* 初始化用户画像
*/
public func initialize() {
do {
// 注册通知监听器
NotificationCenter.default.addObserver(
self,
selector: #selector(handleUpdatePortrait(_:)),
name: NSNotification.Name("com.yunqiinnovation.user_portrait.UPDATE_PORTRAIT"),
object: nil
)
// 获取 UserDefaults
appContext = UIApplication.shared
userDefaults = UserDefaults(suiteName: UserPortrait.PREF_NAME) ?? UserDefaults.standard
// 加载已保存的画像数据
loadPortraitFromDefaults()
print("\(UserPortrait.TAG): 用户画像初始化成功")
} catch {
print("\(UserPortrait.TAG): 初始化用户画像时发生异常: \(error.localizedDescription)")
}
}
/**
* 处理更新画像通知
*/
@objc private func handleUpdatePortrait(_ notification: Notification) {
print("\(UserPortrait.TAG): 接收到通知事件 \(notification.name.rawValue)")
guard let userInfo = notification.userInfo,
let key = userInfo["key"] as? String,
let value = userInfo["value"] as? String else {
print("\(UserPortrait.TAG): 通知数据格式错误")
return
}
print("\(UserPortrait.TAG): 收到 UPDATE_PORTRAIT 通知 \(key):\(value)")
if !key.isEmpty {
print("\(UserPortrait.TAG): 接收到画像更新:\(key) = \(value)")
set(key: key, value: value)
}
}
/**
* 从 UserDefaults 加载画像数据
*/
private func loadPortraitFromDefaults() {
guard let defaults = userDefaults else { return }
// 获取所有保存的键值对
let allKeys = defaults.dictionaryRepresentation().keys
for key in allKeys {
if let value = defaults.object(forKey: key) {
portrait[key] = value
}
}
}
/**
* 获取所有画像数据
*/
public func getAll() -> [String: String] {
return portrait.compactMapValues { value in
return String(describing: value)
}
}
/**
* 设置画像字段
*/
public func set(key: String, value: String) {
userDefaults?.set(value, forKey: key)
userDefaults?.synchronize() // 立即同步到磁盘
// 更新内存中的画像数据
portrait[key] = value
print("\(UserPortrait.TAG): 设置画像字段 \(key) = \(value)")
}
/**
* 获取指定键的画像值
*/
public func get(key: String) -> String? {
return portrait[key] as? String
}
/**
* 清空所有画像数据
*/
public func clean() {
portrait.removeAll()
// 清空 UserDefaults 中的数据
if let defaults = userDefaults {
let allKeys = defaults.dictionaryRepresentation().keys
for key in allKeys {
defaults.removeObject(forKey: key)
}
defaults.synchronize()
}
print("\(UserPortrait.TAG): 已清空所有画像数据")
}
/**
* 发送更新画像通知(用于其他模块触发更新)
*/
public func sendUpdateNotification(key: String, value: String) {
let userInfo: [String: Any] = [
"key": key,
"value": value
]
NotificationCenter.default.post(
name: NSNotification.Name("com.yunqiinnovation.user_portrait.UPDATE_PORTRAIT"),
object: nil,
userInfo: userInfo
)
}
/**
* 移除通知监听器(在应用退出时调用)
*/
public func deinitialize() {
NotificationCenter.default.removeObserver(
self,
name: NSNotification.Name("com.yunqiinnovation.user_portrait.UPDATE_PORTRAIT"),
object: nil
)
print("\(UserPortrait.TAG): 用户画像已清理")
}
}
Loading…
Cancel
Save