{"repo": "abuaibobo-dev/xiaonaodai-android", "files": {"Constants.kt": {"fe1783a63714dcef00096520050595d115ec92c8": {"t": 1785745710.5315666, "n": 1742, "h": "5e872b28261f390f", "c": "package com.meitu.generator.util\n\nobject Constants {\n // ============ Coze API 配置 ============\n const val COZE_API_BASE_URL = \"https://api.coze.cn\"\n const val KEY_COZE_PAT = \"coze_pat\"\n const val KEY_COZE_BOT_ID = \"coze_bot_id\"\n\n // 默认配置(开箱即用)\n const val DEFAULT_COZE_PAT = \"pat_UpyDNzHznqt05EuVxLR6HovSWc31SqOxRINUKSmrxMm31R91OCczSIxnFdPFacTD\"\n const val DEFAULT_COZE_BOT_ID = \"7667987502787461163\"\n\n // ============ DeepSeek API 配置 ============\n const val DEEPSEEK_API_BASE_URL = \"https://api.deepseek.com\"\n const val KEY_DEEPSEEK_API_KEY = \"deepseek_api_key\"\n const val KEY_DEEPSEEK_MODEL = \"deepseek_model\"\n val DEEPSEEK_MODELS = listOf(\"deepseek-v4-flash\", \"deepseek-v4-pro\")\n\n // ============ AI 通道类型 ============\n const val CHANNEL_COZE = \"coze\"\n const val CHANNEL_DEEPSEEK = \"deepseek\"\n const val CHANNEL_CUSTOM_PREFIX = \"custom:\"\n const val KEY_AI_CHANNEL = \"ai_channel\"\n const val KEY_CUSTOM_API_LIST = \"custom_api_list\"\n\n // 本地数据库名\n const val DB_NAME = \"meitu_database\"\n\n // ============ App 信息 ============\n const val APP_NAME = \"布老师\"\n const val APP_VERSION = \"6.2.7\"\n const val APP_VERSION_CODE = 68\n\n // ============ GitHub 配置 ============\n const val KEY_GITHUB_TOKEN = \"github_token\"\n const val GITHUB_API_BASE = \"https://api.github.com\"\n\n // ============ HuggingFace 配置 ============\n const val KEY_HF_TOKEN = \"hf_token\"\n const val HF_API_BASE = \"https://huggingface.co/api\"\n\n // ============ Server酱 / PushPlus 配置 ============\n const val KEY_SERVERCHAN_KEY = \"serverchan_key\"\n const val KEY_PUSHPLUS_TOKEN = \"pushplus_token\"\n\n // ============ 通用余额查询 ============\n const val KEY_BALANCE_CHECK_LIST = \"balance_check_list\"\n}"}, "d17a2688d5e089111350b34fe95eb2d0b8226bb9": {"t": 1785755784.0614455, "n": 1742, "h": "5e872b28261f390f", "c": "package com.meitu.generator.util\n\nobject Constants {\n // ============ Coze API 配置 ============\n const val COZE_API_BASE_URL = \"https://api.coze.cn\"\n const val KEY_COZE_PAT = \"coze_pat\"\n const val KEY_COZE_BOT_ID = \"coze_bot_id\"\n\n // 默认配置(开箱即用)\n const val DEFAULT_COZE_PAT = \"pat_UpyDNzHznqt05EuVxLR6HovSWc31SqOxRINUKSmrxMm31R91OCczSIxnFdPFacTD\"\n const val DEFAULT_COZE_BOT_ID = \"7667987502787461163\"\n\n // ============ DeepSeek API 配置 ============\n const val DEEPSEEK_API_BASE_URL = \"https://api.deepseek.com\"\n const val KEY_DEEPSEEK_API_KEY = \"deepseek_api_key\"\n const val KEY_DEEPSEEK_MODEL = \"deepseek_model\"\n val DEEPSEEK_MODELS = listOf(\"deepseek-v4-flash\", \"deepseek-v4-pro\")\n\n // ============ AI 通道类型 ============\n const val CHANNEL_COZE = \"coze\"\n const val CHANNEL_DEEPSEEK = \"deepseek\"\n const val CHANNEL_CUSTOM_PREFIX = \"custom:\"\n const val KEY_AI_CHANNEL = \"ai_channel\"\n const val KEY_CUSTOM_API_LIST = \"custom_api_list\"\n\n // 本地数据库名\n const val DB_NAME = \"meitu_database\"\n\n // ============ App 信息 ============\n const val APP_NAME = \"布老师\"\n const val APP_VERSION = \"6.2.7\"\n const val APP_VERSION_CODE = 68\n\n // ============ GitHub 配置 ============\n const val KEY_GITHUB_TOKEN = \"github_token\"\n const val GITHUB_API_BASE = \"https://api.github.com\"\n\n // ============ HuggingFace 配置 ============\n const val KEY_HF_TOKEN = \"hf_token\"\n const val HF_API_BASE = \"https://huggingface.co/api\"\n\n // ============ Server酱 / PushPlus 配置 ============\n const val KEY_SERVERCHAN_KEY = \"serverchan_key\"\n const val KEY_PUSHPLUS_TOKEN = \"pushplus_token\"\n\n // ============ 通用余额查询 ============\n const val KEY_BALANCE_CHECK_LIST = \"balance_check_list\"\n}"}}, "app/src/main/java/com/meitu/generator/util/Constants.kt": {"fe1783a63714dcef00096520050595d115ec92c8": {"t": 1785745713.6882572, "n": 1742, "h": "9b87d88917d81b0d", "c": "package com.meitu.generator.util\n\nobject Constants {\n // ============ Coze API 配置 ============\n const val COZE_API_BASE_URL = \"https://api.coze.cn\"\n const val KEY_COZE_PAT = \"coze_pat\"\n const val KEY_COZE_BOT_ID = \"coze_bot_id\"\n\n // 默认配置(开箱即用)\n const val DEFAULT_COZE_PAT = \"pat_UpyDNzHznqt05EuVxLR6HovSWc31SqOxRINUKSmrxMm31R91OCczSIxnFdPFacTD\"\n const val DEFAULT_COZE_BOT_ID = \"7667987502787461163\"\n\n // ============ DeepSeek API 配置 ============\n const val DEEPSEEK_API_BASE_URL = \"https://api.deepseek.com\"\n const val KEY_DEEPSEEK_API_KEY = \"deepseek_api_key\"\n const val KEY_DEEPSEEK_MODEL = \"deepseek_model\"\n val DEEPSEEK_MODELS = listOf(\"deepseek-v4-flash\", \"deepseek-v4-pro\")\n\n // ============ AI 通道类型 ============\n const val CHANNEL_COZE = \"coze\"\n const val CHANNEL_DEEPSEEK = \"deepseek\"\n const val CHANNEL_CUSTOM_PREFIX = \"custom:\"\n const val KEY_AI_CHANNEL = \"ai_channel\"\n const val KEY_CUSTOM_API_LIST = \"custom_api_list\"\n\n // 本地数据库名\n const val DB_NAME = \"meitu_database\"\n\n // ============ App 信息 ============\n const val APP_NAME = \"布老师\"\n const val APP_VERSION = \"6.2.6\"\n const val APP_VERSION_CODE = 67\n\n // ============ GitHub 配置 ============\n const val KEY_GITHUB_TOKEN = \"github_token\"\n const val GITHUB_API_BASE = \"https://api.github.com\"\n\n // ============ HuggingFace 配置 ============\n const val KEY_HF_TOKEN = \"hf_token\"\n const val HF_API_BASE = \"https://huggingface.co/api\"\n\n // ============ Server酱 / PushPlus 配置 ============\n const val KEY_SERVERCHAN_KEY = \"serverchan_key\"\n const val KEY_PUSHPLUS_TOKEN = \"pushplus_token\"\n\n // ============ 通用余额查询 ============\n const val KEY_BALANCE_CHECK_LIST = \"balance_check_list\"\n}"}, "958c6e62991a2f584de24c899acdff62bbf2dbb7": {"t": 1785755784.0935948, "n": 1742, "h": "9b87d88917d81b0d", "c": "package com.meitu.generator.util\n\nobject Constants {\n // ============ Coze API 配置 ============\n const val COZE_API_BASE_URL = \"https://api.coze.cn\"\n const val KEY_COZE_PAT = \"coze_pat\"\n const val KEY_COZE_BOT_ID = \"coze_bot_id\"\n\n // 默认配置(开箱即用)\n const val DEFAULT_COZE_PAT = \"pat_UpyDNzHznqt05EuVxLR6HovSWc31SqOxRINUKSmrxMm31R91OCczSIxnFdPFacTD\"\n const val DEFAULT_COZE_BOT_ID = \"7667987502787461163\"\n\n // ============ DeepSeek API 配置 ============\n const val DEEPSEEK_API_BASE_URL = \"https://api.deepseek.com\"\n const val KEY_DEEPSEEK_API_KEY = \"deepseek_api_key\"\n const val KEY_DEEPSEEK_MODEL = \"deepseek_model\"\n val DEEPSEEK_MODELS = listOf(\"deepseek-v4-flash\", \"deepseek-v4-pro\")\n\n // ============ AI 通道类型 ============\n const val CHANNEL_COZE = \"coze\"\n const val CHANNEL_DEEPSEEK = \"deepseek\"\n const val CHANNEL_CUSTOM_PREFIX = \"custom:\"\n const val KEY_AI_CHANNEL = \"ai_channel\"\n const val KEY_CUSTOM_API_LIST = \"custom_api_list\"\n\n // 本地数据库名\n const val DB_NAME = \"meitu_database\"\n\n // ============ App 信息 ============\n const val APP_NAME = \"布老师\"\n const val APP_VERSION = \"6.2.6\"\n const val APP_VERSION_CODE = 67\n\n // ============ GitHub 配置 ============\n const val KEY_GITHUB_TOKEN = \"github_token\"\n const val GITHUB_API_BASE = \"https://api.github.com\"\n\n // ============ HuggingFace 配置 ============\n const val KEY_HF_TOKEN = \"hf_token\"\n const val HF_API_BASE = \"https://huggingface.co/api\"\n\n // ============ Server酱 / PushPlus 配置 ============\n const val KEY_SERVERCHAN_KEY = \"serverchan_key\"\n const val KEY_PUSHPLUS_TOKEN = \"pushplus_token\"\n\n // ============ 通用余额查询 ============\n const val KEY_BALANCE_CHECK_LIST = \"balance_check_list\"\n}"}, "591efd6f9c0ccd5dc5e0a29fcb04915709f31c26": {"t": 1785755785.5272062, "n": 1742, "h": "26d38eed8caf9702", "c": "package com.meitu.generator.util\n\nobject Constants {\n // ============ Coze API 配置 ============\n const val COZE_API_BASE_URL = \"https://api.coze.cn\"\n const val KEY_COZE_PAT = \"coze_pat\"\n const val KEY_COZE_BOT_ID = \"coze_bot_id\"\n\n // 默认配置(开箱即用)\n const val DEFAULT_COZE_PAT = \"pat_UpyDNzHznqt05EuVxLR6HovSWc31SqOxRINUKSmrxMm31R91OCczSIxnFdPFacTD\"\n const val DEFAULT_COZE_BOT_ID = \"7667987502787461163\"\n\n // ============ DeepSeek API 配置 ============\n const val DEEPSEEK_API_BASE_URL = \"https://api.deepseek.com\"\n const val KEY_DEEPSEEK_API_KEY = \"deepseek_api_key\"\n const val KEY_DEEPSEEK_MODEL = \"deepseek_model\"\n val DEEPSEEK_MODELS = listOf(\"deepseek-v4-flash\", \"deepseek-v4-pro\")\n\n // ============ AI 通道类型 ============\n const val CHANNEL_COZE = \"coze\"\n const val CHANNEL_DEEPSEEK = \"deepseek\"\n const val CHANNEL_CUSTOM_PREFIX = \"custom:\"\n const val KEY_AI_CHANNEL = \"ai_channel\"\n const val KEY_CUSTOM_API_LIST = \"custom_api_list\"\n\n // 本地数据库名\n const val DB_NAME = \"meitu_database\"\n\n // ============ App 信息 ============\n const val APP_NAME = \"布老师\"\n const val APP_VERSION = \"6.2.3\"\n const val APP_VERSION_CODE = 65\n\n // ============ GitHub 配置 ============\n const val KEY_GITHUB_TOKEN = \"github_token\"\n const val GITHUB_API_BASE = \"https://api.github.com\"\n\n // ============ HuggingFace 配置 ============\n const val KEY_HF_TOKEN = \"hf_token\"\n const val HF_API_BASE = \"https://huggingface.co/api\"\n\n // ============ Server酱 / PushPlus 配置 ============\n const val KEY_SERVERCHAN_KEY = \"serverchan_key\"\n const val KEY_PUSHPLUS_TOKEN = \"pushplus_token\"\n\n // ============ 通用余额查询 ============\n const val KEY_BALANCE_CHECK_LIST = \"balance_check_list\"\n}"}, "1cb32dd08e5ba8d2c94c559e9a860a7e948481d9": {"t": 1785755786.8150811, "n": 1742, "h": "aa7595551e736d49", "c": "package com.meitu.generator.util\n\nobject Constants {\n // ============ Coze API 配置 ============\n const val COZE_API_BASE_URL = \"https://api.coze.cn\"\n const val KEY_COZE_PAT = \"coze_pat\"\n const val KEY_COZE_BOT_ID = \"coze_bot_id\"\n\n // 默认配置(开箱即用)\n const val DEFAULT_COZE_PAT = \"pat_UpyDNzHznqt05EuVxLR6HovSWc31SqOxRINUKSmrxMm31R91OCczSIxnFdPFacTD\"\n const val DEFAULT_COZE_BOT_ID = \"7667987502787461163\"\n\n // ============ DeepSeek API 配置 ============\n const val DEEPSEEK_API_BASE_URL = \"https://api.deepseek.com\"\n const val KEY_DEEPSEEK_API_KEY = \"deepseek_api_key\"\n const val KEY_DEEPSEEK_MODEL = \"deepseek_model\"\n val DEEPSEEK_MODELS = listOf(\"deepseek-v4-flash\", \"deepseek-v4-pro\")\n\n // ============ AI 通道类型 ============\n const val CHANNEL_COZE = \"coze\"\n const val CHANNEL_DEEPSEEK = \"deepseek\"\n const val CHANNEL_CUSTOM_PREFIX = \"custom:\"\n const val KEY_AI_CHANNEL = \"ai_channel\"\n const val KEY_CUSTOM_API_LIST = \"custom_api_list\"\n\n // 本地数据库名\n const val DB_NAME = \"meitu_database\"\n\n // ============ App 信息 ============\n const val APP_NAME = \"布老师\"\n const val APP_VERSION = \"6.2.2\"\n const val APP_VERSION_CODE = 64\n\n // ============ GitHub 配置 ============\n const val KEY_GITHUB_TOKEN = \"github_token\"\n const val GITHUB_API_BASE = \"https://api.github.com\"\n\n // ============ HuggingFace 配置 ============\n const val KEY_HF_TOKEN = \"hf_token\"\n const val HF_API_BASE = \"https://huggingface.co/api\"\n\n // ============ Server酱 / PushPlus 配置 ============\n const val KEY_SERVERCHAN_KEY = \"serverchan_key\"\n const val KEY_PUSHPLUS_TOKEN = \"pushplus_token\"\n\n // ============ 通用余额查询 ============\n const val KEY_BALANCE_CHECK_LIST = \"balance_check_list\"\n}"}, "499d7e4e953c35698abdd05ab892a6657f8cec85": {"t": 1785755788.3281124, "n": 1187, "h": "e1800def9df244f9", "c": "package com.meitu.generator.util\n\nobject Constants {\n // ============ Coze API 配置 ============\n const val COZE_API_BASE_URL = \"https://api.coze.cn\"\n const val KEY_COZE_PAT = \"coze_pat\"\n const val KEY_COZE_BOT_ID = \"coze_bot_id\"\n\n // 默认配置(开箱即用)\n const val DEFAULT_COZE_PAT = \"pat_UpyDNzHznqt05EuVxLR6HovSWc31SqOxRINUKSmrxMm31R91OCczSIxnFdPFacTD\"\n const val DEFAULT_COZE_BOT_ID = \"7667987502787461163\"\n\n // ============ DeepSeek API 配置 ============\n const val DEEPSEEK_API_BASE_URL = \"https://api.deepseek.com\"\n const val KEY_DEEPSEEK_API_KEY = \"deepseek_api_key\"\n const val KEY_DEEPSEEK_MODEL = \"deepseek_model\"\n val DEEPSEEK_MODELS = listOf(\"deepseek-v4-flash\", \"deepseek-v4-pro\")\n\n // ============ AI 通道类型 ============\n const val CHANNEL_COZE = \"coze\"\n const val CHANNEL_DEEPSEEK = \"deepseek\"\n const val CHANNEL_CUSTOM_PREFIX = \"custom:\"\n const val KEY_AI_CHANNEL = \"ai_channel\"\n const val KEY_CUSTOM_API_LIST = \"custom_api_list\"\n\n // 本地数据库名\n const val DB_NAME = \"meitu_database\"\n\n // ============ App 信息 ============\n const val APP_NAME = \"布老师\"\n const val APP_VERSION = \"6.2.1\"\n const val APP_VERSION_CODE = 63\n}\n"}, "a1e4a56b7b5136a3d441efc01c9b3034885341da": {"t": 1785755789.9424691, "n": 1085, "h": "f783833b7cac87bf", "c": "package com.meitu.generator.util\n\nobject Constants {\n // ============ Coze API 配置 ============\n const val COZE_API_BASE_URL = \"https://api.coze.cn\"\n const val KEY_COZE_PAT = \"coze_pat\"\n const val KEY_COZE_BOT_ID = \"coze_bot_id\"\n\n // 默认配置(开箱即用)\n const val DEFAULT_COZE_PAT = \"pat_UpyDNzHznqt05EuVxLR6HovSWc31SqOxRINUKSmrxMm31R91OCczSIxnFdPFacTD\"\n const val DEFAULT_COZE_BOT_ID = \"7667987502787461163\"\n\n // ============ DeepSeek API 配置 ============\n const val DEEPSEEK_API_BASE_URL = \"https://api.deepseek.com\"\n const val KEY_DEEPSEEK_API_KEY = \"deepseek_api_key\"\n const val KEY_DEEPSEEK_MODEL = \"deepseek_model\"\n val DEEPSEEK_MODELS = listOf(\"deepseek-v4-flash\", \"deepseek-v4-pro\")\n\n // ============ AI 通道类型 ============\n const val CHANNEL_COZE = \"coze\"\n const val CHANNEL_DEEPSEEK = \"deepseek\"\n const val KEY_AI_CHANNEL = \"ai_channel\"\n\n // 本地数据库名\n const val DB_NAME = \"meitu_database\"\n\n // ============ App 信息 ============\n const val APP_NAME = \"布老师\"\n const val APP_VERSION = \"6.2.1\"\n const val APP_VERSION_CODE = 63\n}\n"}, "74feddd5fc29af926811f49ebeb64d11fbed7ea3": {"t": 1785755791.6070716, "n": 1085, "h": "118912f7fee53764", "c": "package com.meitu.generator.util\n\nobject Constants {\n // ============ Coze API 配置 ============\n const val COZE_API_BASE_URL = \"https://api.coze.cn\"\n const val KEY_COZE_PAT = \"coze_pat\"\n const val KEY_COZE_BOT_ID = \"coze_bot_id\"\n\n // 默认配置(开箱即用)\n const val DEFAULT_COZE_PAT = \"pat_UpyDNzHznqt05EuVxLR6HovSWc31SqOxRINUKSmrxMm31R91OCczSIxnFdPFacTD\"\n const val DEFAULT_COZE_BOT_ID = \"7667987502787461163\"\n\n // ============ DeepSeek API 配置 ============\n const val DEEPSEEK_API_BASE_URL = \"https://api.deepseek.com\"\n const val KEY_DEEPSEEK_API_KEY = \"deepseek_api_key\"\n const val KEY_DEEPSEEK_MODEL = \"deepseek_model\"\n val DEEPSEEK_MODELS = listOf(\"deepseek-v4-flash\", \"deepseek-v4-pro\")\n\n // ============ AI 通道类型 ============\n const val CHANNEL_COZE = \"coze\"\n const val CHANNEL_DEEPSEEK = \"deepseek\"\n const val KEY_AI_CHANNEL = \"ai_channel\"\n\n // 本地数据库名\n const val DB_NAME = \"meitu_database\"\n\n // ============ App 信息 ============\n const val APP_NAME = \"布老师\"\n const val APP_VERSION = \"6.2.0\"\n const val APP_VERSION_CODE = 62\n}\n"}, "ef6307548c43782546795c7c69a66ddbc90b1558": {"t": 1785755793.6749084, "n": 1007, "h": "27fad7327598d3cf", "c": "package com.meitu.generator.util\n\nobject Constants {\n // ============ Coze API 配置 ============\n const val COZE_API_BASE_URL = \"https://api.coze.cn\"\n const val KEY_COZE_PAT = \"coze_pat\"\n const val KEY_COZE_BOT_ID = \"coze_bot_id\"\n\n // 默认配置(开箱即用)\n const val DEFAULT_COZE_PAT = \"pat_UpyDNzHznqt05EuVxLR6HovSWc31SqOxRINUKSmrxMm31R91OCczSIxnFdPFacTD\"\n const val DEFAULT_COZE_BOT_ID = \"7667987502787461163\"\n\n // ============ DeepSeek API 配置 ============\n const val DEEPSEEK_API_BASE_URL = \"https://api.deepseek.com\"\n const val KEY_DEEPSEEK_API_KEY = \"deepseek_api_key\"\n const val DEEPSEEK_MODEL = \"deepseek-chat\"\n\n // ============ AI 通道类型 ============\n const val CHANNEL_COZE = \"coze\"\n const val CHANNEL_DEEPSEEK = \"deepseek\"\n const val KEY_AI_CHANNEL = \"ai_channel\"\n\n // 本地数据库名\n const val DB_NAME = \"meitu_database\"\n\n // ============ App 信息 ============\n const val APP_NAME = \"布老师\"\n const val APP_VERSION = \"6.1.0\"\n const val APP_VERSION_CODE = 61\n}\n"}}, "app/src/main/java/com/meitu/generator/ui/settings/SettingsViewModel.kt": {"fe1783a63714dcef00096520050595d115ec92c8": {"t": 1785745726.531921, "n": 39977, "h": "2a40baa99e2c90b1", "c": "package com.meitu.generator.ui.settings\n\nimport android.content.SharedPreferences\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.meitu.generator.data.remote.CozeApiClient\nimport com.meitu.generator.repository.SettingsRepository\nimport com.meitu.generator.util.Constants\nimport com.google.gson.Gson\nimport com.google.gson.JsonParser\nimport com.google.gson.reflect.TypeToken\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport javax.inject.Inject\nimport javax.inject.Named\n\ndata class AgentConfig(\n val name: String,\n val botId: String,\n val prompt: String,\n val emoji: String = \"🤖\"\n)\n\n@HiltViewModel\nclass SettingsViewModel @Inject constructor(\n private val settingsRepo: SettingsRepository,\n private val cozeClient: CozeApiClient,\n @Named(\"securePrefs\") private val securePrefs: SharedPreferences\n) : ViewModel() {\n\n private val gson = Gson()\n\n // ============ Coze 配置 ============\n private val _cozePat = MutableStateFlow(\"\")\n val cozePat: StateFlow = _cozePat.asStateFlow()\n\n private val _cozeBotId = MutableStateFlow(\"\")\n val cozeBotId: StateFlow = _cozeBotId.asStateFlow()\n\n private val _isUsingDefaultPat = MutableStateFlow(true)\n val isUsingDefaultPat: StateFlow = _isUsingDefaultPat.asStateFlow()\n\n private val _isUsingDefaultBotId = MutableStateFlow(true)\n val isUsingDefaultBotId: StateFlow = _isUsingDefaultBotId.asStateFlow()\n\n // ============ Token 消耗统计(动态 Map) ============\n data class ChannelTokenStats(\n val total: Int = 0,\n val input: Int = 0,\n val output: Int = 0,\n val messages: Int = 0\n )\n\n private val _channelTokenStats = MutableStateFlow>(emptyMap())\n val channelTokenStats: StateFlow> = _channelTokenStats.asStateFlow()\n\n private val _totalTokens = MutableStateFlow(0)\n val totalTokens: StateFlow = _totalTokens.asStateFlow()\n\n /** 获取通道的显示名称 */\n fun getChannelDisplayName(channelKey: String): String {\n return when (channelKey) {\n \"coze\" -> \"🧠 Coze\"\n \"deepseek\" -> \"🔍 DeepSeek\"\n else -> {\n val id = channelKey.removePrefix(\"custom:\")\n _customApiList.value.find { it.id == id }?.let { \"${it.emoji} ${it.name}\" }\n ?: channelKey\n }\n }\n }\n\n // ============ AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow = _currentChannel.asStateFlow()\n\n private val _deepseekApiKey = MutableStateFlow(\"\")\n val deepseekApiKey: StateFlow = _deepseekApiKey.asStateFlow()\n\n // ============ DeepSeek 模型选择 ============\n private val _deepseekModel = MutableStateFlow(\"deepseek-v4-flash\")\n val deepseekModel: StateFlow = _deepseekModel.asStateFlow()\n\n // ============ DeepSeek 余额 ============\n private val _deepseekBalance = MutableStateFlow(null)\n val deepseekBalance: StateFlow = _deepseekBalance.asStateFlow()\n private val _isLoadingBalance = MutableStateFlow(false)\n val isLoadingBalance: StateFlow = _isLoadingBalance.asStateFlow()\n\n // ============ 自定义 API 通道 ============\n private val _customApiList = MutableStateFlow>(emptyList())\n val customApiList: StateFlow> = _customApiList.asStateFlow()\n\n private fun loadCustomApiList() {\n val json = securePrefs.getString(Constants.KEY_CUSTOM_API_LIST, \"[]\") ?: \"[]\"\n val type = object : TypeToken>() {}.type\n _customApiList.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n }\n\n private fun saveCustomApiList(list: List) {\n securePrefs.edit().putString(Constants.KEY_CUSTOM_API_LIST, gson.toJson(list)).apply()\n _customApiList.value = list\n }\n\n fun addCustomApi(config: CustomApiConfig) {\n val list = _customApiList.value.toMutableList()\n list.add(config)\n saveCustomApiList(list)\n _toastMessage.value = \"✅ 已添加自定义 API「${config.name}」\"\n }\n\n fun updateCustomApi(config: CustomApiConfig) {\n val list = _customApiList.value.toMutableList()\n val idx = list.indexOfFirst { it.id == config.id }\n if (idx >= 0) {\n list[idx] = config\n saveCustomApiList(list)\n _toastMessage.value = \"✅ 已更新「${config.name}」\"\n }\n }\n\n fun deleteCustomApi(id: String) {\n val list = _customApiList.value.toMutableList()\n list.removeAll { it.id == id }\n saveCustomApiList(list)\n val channel = _currentChannel.value\n if (channel == \"${Constants.CHANNEL_CUSTOM_PREFIX}$id\") {\n switchChannel(Constants.CHANNEL_COZE)\n }\n _toastMessage.value = \"✅ 已删除\"\n }\n\n fun getCustomApiById(id: String): CustomApiConfig? {\n return _customApiList.value.find { it.id == id }\n }\n\n // ============ GitHub 关联 ============\n data class GitHubUser(\n val login: String,\n val avatarUrl: String,\n val name: String,\n val publicRepos: Int,\n val followers: Int\n )\n\n data class GitHubRepo(\n val name: String,\n val fullName: String,\n val description: String,\n val language: String?,\n val stars: Int,\n val forks: Int,\n val updatedAt: String\n )\n\n data class GitHubCommit(\n val sha: String,\n val message: String,\n val author: String,\n val date: String\n )\n\n data class GitHubNotification(\n val title: String,\n val repoName: String,\n val type: String,\n val url: String,\n val updatedAt: String\n )\n\n private val _githubToken = MutableStateFlow(\"\")\n val githubToken: StateFlow = _githubToken.asStateFlow()\n\n private val _githubUser = MutableStateFlow(null)\n val githubUser: StateFlow = _githubUser.asStateFlow()\n\n private val _githubRepos = MutableStateFlow>(emptyList())\n val githubRepos: StateFlow> = _githubRepos.asStateFlow()\n\n private val _githubNotifications = MutableStateFlow>(emptyList())\n val githubNotifications: StateFlow> = _githubNotifications.asStateFlow()\n\n private val _selectedRepoCommits = MutableStateFlow>(emptyList())\n val selectedRepoCommits: StateFlow> = _selectedRepoCommits.asStateFlow()\n\n private val _isLoadingGitHub = MutableStateFlow(false)\n val isLoadingGitHub: StateFlow = _isLoadingGitHub.asStateFlow()\n\n private val _githubError = MutableStateFlow(null)\n val githubError: StateFlow = _githubError.asStateFlow()\n\n private fun loadGitHubToken() {\n _githubToken.value = securePrefs.getString(Constants.KEY_GITHUB_TOKEN, \"\") ?: \"\"\n }\n\n fun saveGitHubToken(token: String) {\n securePrefs.edit().putString(Constants.KEY_GITHUB_TOKEN, token).apply()\n _githubToken.value = token\n if (token.isNotBlank()) {\n refreshGitHubData()\n } else {\n _githubUser.value = null\n _githubRepos.value = emptyList()\n _githubNotifications.value = emptyList()\n }\n }\n\n fun refreshGitHubData() {\n val token = _githubToken.value\n if (token.isBlank()) return\n _isLoadingGitHub.value = true\n _githubError.value = null\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n // 获取用户信息\n val userResp = gitHubApiGet(\"/user\", token)\n val userJson = JsonParser.parseString(userResp).asJsonObject\n _githubUser.value = GitHubUser(\n login = userJson.get(\"login\")?.asString ?: \"\",\n avatarUrl = userJson.get(\"avatar_url\")?.asString ?: \"\",\n name = userJson.get(\"name\")?.asString ?: userJson.get(\"login\")?.asString ?: \"\",\n publicRepos = userJson.get(\"public_repos\")?.asInt ?: 0,\n followers = userJson.get(\"followers\")?.asInt ?: 0\n )\n\n // 获取仓库列表\n val reposResp = gitHubApiGet(\"/user/repos?sort=updated&per_page=20\", token)\n val reposArray = JsonParser.parseString(reposResp).asJsonArray\n _githubRepos.value = reposArray.map { repo ->\n val obj = repo.asJsonObject\n GitHubRepo(\n name = obj.get(\"name\")?.asString ?: \"\",\n fullName = obj.get(\"full_name\")?.asString ?: \"\",\n description = obj.get(\"description\")?.asString ?: \"\",\n language = obj.get(\"language\")?.asString,\n stars = obj.get(\"stargazers_count\")?.asInt ?: 0,\n forks = obj.get(\"forks_count\")?.asInt ?: 0,\n updatedAt = obj.get(\"updated_at\")?.asString?.take(10) ?: \"\"\n )\n }\n\n // 获取通知\n try {\n val notifResp = gitHubApiGet(\"/notifications?per_page=10\", token)\n val notifArray = JsonParser.parseString(notifResp).asJsonArray\n _githubNotifications.value = notifArray.map { n ->\n val obj = n.asJsonObject\n val subject = obj.getAsJsonObject(\"subject\")\n val repo = obj.getAsJsonObject(\"repository\")\n GitHubNotification(\n title = subject?.get(\"title\")?.asString ?: \"\",\n repoName = repo?.get(\"full_name\")?.asString ?: \"\",\n type = subject?.get(\"type\")?.asString ?: \"\",\n url = subject?.get(\"url\")?.asString ?: \"\",\n updatedAt = obj.get(\"updated_at\")?.asString?.take(10) ?: \"\"\n )\n }\n } catch (_: Exception) {\n _githubNotifications.value = emptyList()\n }\n }\n } catch (e: Exception) {\n _githubError.value = e.message ?: \"加载失败\"\n } finally {\n _isLoadingGitHub.value = false\n }\n }\n }\n\n fun loadRepoCommits(fullName: String) {\n val token = _githubToken.value\n if (token.isBlank()) return\n _isLoadingGitHub.value = true\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n val resp = gitHubApiGet(\"/repos/$fullName/commits?per_page=10\", token)\n val array = JsonParser.parseString(resp).asJsonArray\n _selectedRepoCommits.value = array.map { c ->\n val obj = c.asJsonObject\n val commit = obj.getAsJsonObject(\"commit\")\n val author = commit?.getAsJsonObject(\"author\")\n GitHubCommit(\n sha = obj.get(\"sha\")?.asString?.take(7) ?: \"\",\n message = commit?.get(\"message\")?.asString?.split(\"\\n\")?.firstOrNull() ?: \"\",\n author = author?.get(\"name\")?.asString ?: \"\",\n date = author?.get(\"date\")?.asString?.take(10) ?: \"\"\n )\n }\n }\n } catch (e: Exception) {\n _githubError.value = \"加载 commits 失败: ${e.message}\"\n } finally {\n _isLoadingGitHub.value = false\n }\n }\n }\n\n private fun gitHubApiGet(path: String, token: String): String {\n val url = java.net.URL(\"${Constants.GITHUB_API_BASE}$path\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"GET\"\n conn.setRequestProperty(\"Authorization\", \"Bearer $token\")\n conn.setRequestProperty(\"Accept\", \"application/vnd.github.v3+json\")\n conn.connectTimeout = 10000\n conn.readTimeout = 10000\n val code = conn.responseCode\n if (code != 200) {\n val err = conn.errorStream?.bufferedReader()?.readText() ?: \"HTTP $code\"\n throw Exception(\"GitHub API 错误 ($code): $err\")\n }\n return conn.inputStream.bufferedReader().readText()\n }\n\n // ============ HuggingFace ============\n data class HfModel(\n val modelId: String,\n val author: String,\n val pipelineTag: String,\n val downloads: Long,\n val likes: Int,\n val lastModified: String\n )\n\n private val _hfToken = MutableStateFlow(\"\")\n val hfToken: StateFlow = _hfToken.asStateFlow()\n\n private val _hfModels = MutableStateFlow>(emptyList())\n val hfModels: StateFlow> = _hfModels.asStateFlow()\n\n private val _isLoadingHf = MutableStateFlow(false)\n val isLoadingHf: StateFlow = _isLoadingHf.asStateFlow()\n\n private val _hfError = MutableStateFlow(null)\n val hfError: StateFlow = _hfError.asStateFlow()\n\n private val _hfTrendingTag = MutableStateFlow(\"text-generation\")\n val hfTrendingTag: StateFlow = _hfTrendingTag.asStateFlow()\n\n fun loadHfToken() {\n _hfToken.value = securePrefs.getString(Constants.KEY_HF_TOKEN, \"\") ?: \"\"\n }\n\n fun saveHfToken(token: String) {\n securePrefs.edit().putString(Constants.KEY_HF_TOKEN, token).apply()\n _hfToken.value = token\n _toastMessage.value = if (token.isNotBlank()) \"✅ HuggingFace Token 已保存\" else \"✅ 已清除\"\n }\n\n fun searchHfModels(query: String) {\n if (query.isBlank()) return\n _isLoadingHf.value = true\n _hfError.value = null\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n val encoded = java.net.URLEncoder.encode(query, \"UTF-8\")\n val resp = hfApiGet(\"/models?search=$encoded&sort=downloads&direction=-1&limit=20\")\n val array = JsonParser.parseString(resp).asJsonArray\n _hfModels.value = array.map { m ->\n val obj = m.asJsonObject\n HfModel(\n modelId = obj.get(\"modelId\")?.asString ?: obj.get(\"id\")?.asString ?: \"\",\n author = obj.get(\"author\")?.asString ?: obj.get(\"modelId\")?.asString?.split(\"/\")?.firstOrNull() ?: \"\",\n pipelineTag = obj.get(\"pipeline_tag\")?.asString ?: \"其他\",\n downloads = obj.get(\"downloads\")?.asLong ?: 0,\n likes = obj.get(\"likes\")?.asInt ?: 0,\n lastModified = obj.get(\"lastModified\")?.asString?.take(10) ?: \"\"\n )\n }\n }\n } catch (e: Exception) {\n _hfError.value = e.message ?: \"搜索失败\"\n _hfModels.value = emptyList()\n } finally {\n _isLoadingHf.value = false\n }\n }\n }\n\n fun loadHfTrending(tag: String? = null) {\n val pipelineTag = tag ?: _hfTrendingTag.value\n if (tag != null) _hfTrendingTag.value = tag\n _isLoadingHf.value = true\n _hfError.value = null\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n val resp = hfApiGet(\"/models?pipeline_tag=$pipelineTag&sort=downloads&direction=-1&limit=20\")\n val array = JsonParser.parseString(resp).asJsonArray\n _hfModels.value = array.map { m ->\n val obj = m.asJsonObject\n HfModel(\n modelId = obj.get(\"modelId\")?.asString ?: obj.get(\"id\")?.asString ?: \"\",\n author = obj.get(\"author\")?.asString ?: obj.get(\"modelId\")?.asString?.split(\"/\")?.firstOrNull() ?: \"\",\n pipelineTag = obj.get(\"pipeline_tag\")?.asString ?: \"其他\",\n downloads = obj.get(\"downloads\")?.asLong ?: 0,\n likes = obj.get(\"likes\")?.asInt ?: 0,\n lastModified = obj.get(\"lastModified\")?.asString?.take(10) ?: \"\"\n )\n }\n }\n } catch (e: Exception) {\n _hfError.value = e.message ?: \"加载失败\"\n _hfModels.value = emptyList()\n } finally {\n _isLoadingHf.value = false\n }\n }\n }\n\n private fun hfApiGet(path: String): String {\n val token = _hfToken.value\n val url = java.net.URL(\"${Constants.HF_API_BASE}$path\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"GET\"\n if (token.isNotBlank()) {\n conn.setRequestProperty(\"Authorization\", \"Bearer $token\")\n }\n conn.setRequestProperty(\"Accept\", \"application/json\")\n conn.connectTimeout = 10000\n conn.readTimeout = 10000\n val code = conn.responseCode\n if (code != 200) {\n val err = conn.errorStream?.bufferedReader()?.readText() ?: \"HTTP $code\"\n throw Exception(\"HuggingFace API 错误 ($code): $err\")\n }\n return conn.inputStream.bufferedReader().readText()\n }\n\n // ============ Server酱 / PushPlus ============\n private val _serverchanKey = MutableStateFlow(\"\")\n val serverchanKey: StateFlow = _serverchanKey.asStateFlow()\n\n private val _pushplusToken = MutableStateFlow(\"\")\n val pushplusToken: StateFlow = _pushplusToken.asStateFlow()\n\n private val _pushTestResult = MutableStateFlow(null)\n val pushTestResult: StateFlow = _pushTestResult.asStateFlow()\n\n private val _isTestingPush = MutableStateFlow(false)\n val isTestingPush: StateFlow = _isTestingPush.asStateFlow()\n\n fun loadPushConfig() {\n _serverchanKey.value = securePrefs.getString(Constants.KEY_SERVERCHAN_KEY, \"\") ?: \"\"\n _pushplusToken.value = securePrefs.getString(Constants.KEY_PUSHPLUS_TOKEN, \"\") ?: \"\"\n }\n\n fun saveServerchanKey(key: String) {\n securePrefs.edit().putString(Constants.KEY_SERVERCHAN_KEY, key).apply()\n _serverchanKey.value = key\n _toastMessage.value = if (key.isNotBlank()) \"✅ Server酱 Key 已保存\" else \"✅ 已清除\"\n }\n\n fun savePushplusToken(token: String) {\n securePrefs.edit().putString(Constants.KEY_PUSHPLUS_TOKEN, token).apply()\n _pushplusToken.value = token\n _toastMessage.value = if (token.isNotBlank()) \"✅ PushPlus Token 已保存\" else \"✅ 已清除\"\n }\n\n fun testServerchan() {\n val key = _serverchanKey.value\n if (key.isBlank()) { _toastMessage.value = \"请先配置 Server酱 SendKey\"; return }\n _isTestingPush.value = true; _pushTestResult.value = null\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n val url = java.net.URL(\"https://sctapi.ftqq.com/$key.send\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"POST\"\n conn.doOutput = true\n conn.setRequestProperty(\"Content-Type\", \"application/json; charset=utf-8\")\n conn.connectTimeout = 10000\n conn.readTimeout = 10000\n val body = \"\"\"{\"title\":\"布老师App 推送测试\",\"desp\":\"如果你看到这条消息,说明推送配置成功了 🎉\"}\"\"\"\n conn.outputStream.write(body.toByteArray(Charsets.UTF_8))\n val resp = conn.inputStream.bufferedReader().readText()\n conn.disconnect()\n val json = JsonParser.parseString(resp).asJsonObject\n val code = json.get(\"code\")?.asInt\n _pushTestResult.value = if (code == 0) \"✅ 测试成功!\" else \"❌ 推送失败: ${json.get(\"message\")?.asString ?: resp}\"\n }\n } catch (e: Exception) {\n _pushTestResult.value = \"❌ 测试失败: ${e.message}\"\n } finally {\n _isTestingPush.value = false\n }\n }\n }\n\n fun testPushplus() {\n val token = _pushplusToken.value\n if (token.isBlank()) { _toastMessage.value = \"请先配置 PushPlus Token\"; return }\n _isTestingPush.value = true; _pushTestResult.value = null\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n val url = java.net.URL(\"https://www.pushplus.plus/send\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"POST\"\n conn.doOutput = true\n conn.setRequestProperty(\"Content-Type\", \"application/json; charset=utf-8\")\n conn.connectTimeout = 10000\n conn.readTimeout = 10000\n val body = \"\"\"{\"token\":\"$token\",\"title\":\"布老师App 推送测试\",\"content\":\"如果你看到这条消息,说明推送配置成功了 🎉\"}\"\"\"\n conn.outputStream.write(body.toByteArray(Charsets.UTF_8))\n val resp = conn.inputStream.bufferedReader().readText()\n conn.disconnect()\n val json = JsonParser.parseString(resp).asJsonObject\n val code = json.get(\"code\")?.asInt\n _pushTestResult.value = if (code == 200) \"✅ 测试成功!\" else \"❌ 推送失败: ${json.get(\"msg\")?.asString ?: resp}\"\n }\n } catch (e: Exception) {\n _pushTestResult.value = \"❌ 测试失败: ${e.message}\"\n } finally {\n _isTestingPush.value = false\n }\n }\n }\n\n fun clearPushTestResult() { _pushTestResult.value = null }\n\n // ============ 通用余额查询 ============\n data class BalanceCheckService(\n val id: String,\n val name: String,\n val baseUrl: String,\n val apiKey: String\n )\n\n private val _balanceServices = MutableStateFlow>(emptyList())\n val balanceServices: StateFlow> = _balanceServices.asStateFlow()\n\n private val _balanceResults = MutableStateFlow>(emptyMap())\n val balanceResults: StateFlow> = _balanceResults.asStateFlow()\n\n private val _isLoadingBalanceCheck = MutableStateFlow(false)\n val isLoadingBalanceCheck: StateFlow = _isLoadingBalanceCheck.asStateFlow()\n\n fun loadBalanceServices() {\n val json = securePrefs.getString(Constants.KEY_BALANCE_CHECK_LIST, \"[]\") ?: \"[]\"\n val type = object : TypeToken>() {}.type\n _balanceServices.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n }\n\n private fun saveBalanceServices(list: List) {\n securePrefs.edit().putString(Constants.KEY_BALANCE_CHECK_LIST, gson.toJson(list)).apply()\n _balanceServices.value = list\n }\n\n fun addBalanceService(name: String, baseUrl: String, apiKey: String) {\n val service = BalanceCheckService(\n id = java.util.UUID.randomUUID().toString(),\n name = name.trim(),\n baseUrl = baseUrl.trim().trimEnd('/'),\n apiKey = apiKey.trim()\n )\n val list = _balanceServices.value.toMutableList()\n list.add(service)\n saveBalanceServices(list)\n _toastMessage.value = \"✅ 已添加余额查询「${service.name}」\"\n // 添加后自动查询\n queryBalance(service.id)\n }\n\n fun deleteBalanceService(id: String) {\n val list = _balanceServices.value.toMutableList()\n list.removeAll { it.id == id }\n saveBalanceServices(list)\n val results = _balanceResults.value.toMutableMap()\n results.remove(id)\n _balanceResults.value = results\n _toastMessage.value = \"✅ 已删除\"\n }\n\n fun queryBalance(serviceId: String) {\n val service = _balanceServices.value.find { it.id == serviceId } ?: return\n _isLoadingBalanceCheck.value = true\n viewModelScope.launch {\n try {\n val result = withContext(Dispatchers.IO) {\n // 尝试多个常见余额接口路径\n val paths = listOf(\"/user/balance\", \"/v1/user/balance\", \"/dashboard/billing/credit_grants\")\n var lastError: String? = null\n for (path in paths) {\n try {\n val url = java.net.URL(\"${service.baseUrl}$path\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"GET\"\n conn.setRequestProperty(\"Authorization\", \"Bearer ${service.apiKey}\")\n conn.setRequestProperty(\"Accept\", \"application/json\")\n conn.connectTimeout = 8000\n conn.readTimeout = 8000\n val code = conn.responseCode\n if (code == 200) {\n val text = conn.inputStream.bufferedReader().readText()\n conn.disconnect()\n // 尝试解析常见格式\n return@withContext try {\n val json = JsonParser.parseString(text).asJsonObject\n // 格式1: DeepSeek → balance_infos[0].total_balance\n val balanceInfos = json.getAsJsonArray(\"balance_infos\")\n if (balanceInfos != null && balanceInfos.size() > 0) {\n \"¥${balanceInfos[0].asJsonObject.get(\"total_balance\")?.asString ?: \"?\"}\"\n } else {\n val data = json.getAsJsonObject(\"data\")\n when {\n data != null && data.get(\"balance\") != null -> \"¥${data.get(\"balance\")?.asString ?: data.get(\"balance\")?.asNumber ?: \"?\"}\"\n json.get(\"balance\") != null -> \"¥${json.get(\"balance\")?.asString ?: json.get(\"balance\")?.asNumber ?: \"?\"}\"\n json.get(\"total_balance\") != null -> \"¥${json.get(\"total_balance\")?.asString ?: \"?\"}\"\n else -> text.take(200)\n }\n }\n } catch (_: Exception) {\n text.take(200)\n }\n }\n conn.disconnect()\n lastError = \"HTTP $code\"\n } catch (_: Exception) {\n lastError = \"未知错误\"\n }\n }\n \"❌ $lastError\"\n }\n val results = _balanceResults.value.toMutableMap()\n results[serviceId] = result\n _balanceResults.value = results\n } catch (e: Exception) {\n val results = _balanceResults.value.toMutableMap()\n results[serviceId] = \"❌ ${e.message}\"\n _balanceResults.value = results\n } finally {\n _isLoadingBalanceCheck.value = false\n }\n }\n }\n\n fun queryAllBalances() {\n _balanceServices.value.forEach { queryBalance(it.id) }\n }\n\n // ============ Agent 管理 ============\n private val _agentList = MutableStateFlow>(emptyList())\n val agentList: StateFlow> = _agentList.asStateFlow()\n\n private val _currentAgentId = MutableStateFlow(\"\")\n val currentAgentId: StateFlow = _currentAgentId.asStateFlow()\n\n private val _isCreatingAgent = MutableStateFlow(false)\n val isCreatingAgent: StateFlow = _isCreatingAgent.asStateFlow()\n\n // ============ Toast ============\n private val _toastMessage = MutableStateFlow(null)\n val toastMessage: StateFlow = _toastMessage.asStateFlow()\n\n companion object {\n private const val KEY_AGENTS = \"agent_list\"\n private const val KEY_CURRENT_AGENT = \"current_agent_id\"\n }\n\n init {\n loadConfig()\n loadTokenUsage()\n loadAgents()\n loadChannelAndDeepseek()\n loadCustomApiList()\n loadGitHubToken()\n loadHfToken()\n loadPushConfig()\n loadBalanceServices()\n }\n\n private fun loadChannelAndDeepseek() {\n val channel = securePrefs.getString(Constants.KEY_AI_CHANNEL, Constants.CHANNEL_COZE) ?: Constants.CHANNEL_COZE\n _currentChannel.value = channel\n val dsKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n _deepseekApiKey.value = dsKey\n val dsModel = securePrefs.getString(Constants.KEY_DEEPSEEK_MODEL, \"deepseek-v4-flash\") ?: \"deepseek-v4-flash\"\n _deepseekModel.value = dsModel\n }\n\n fun switchChannel(channel: String) {\n securePrefs.edit().putString(Constants.KEY_AI_CHANNEL, channel).apply()\n _currentChannel.value = channel\n\n val isCustom = channel.startsWith(Constants.CHANNEL_CUSTOM_PREFIX)\n if (isCustom) {\n cozeClient.currentBotId = \"\"\n val customId = channel.removePrefix(Constants.CHANNEL_CUSTOM_PREFIX)\n val config = getCustomApiById(customId)\n _toastMessage.value = \"✅ 已切换到「${config?.name ?: \"自定义\"}」\"\n } else {\n cozeClient.currentBotId = if (channel == Constants.CHANNEL_DEEPSEEK) {\n \"\"\n } else {\n val savedBotId = securePrefs.getString(\"current_agent_id\", \"\") ?: \"\"\n if (savedBotId.isNotBlank()) savedBotId\n else {\n viewModelScope.launch {\n val botId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n cozeClient.currentBotId = botId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n _cozeBotId.value = cozeClient.currentBotId\n }\n _cozeBotId.value\n }\n }\n _toastMessage.value = when (channel) {\n Constants.CHANNEL_DEEPSEEK -> \"✅ 已切换到 DeepSeek\"\n else -> \"✅ 已切换到 Coze\"\n }\n }\n }\n\n fun saveDeepseekApiKey(key: String) {\n securePrefs.edit().putString(Constants.KEY_DEEPSEEK_API_KEY, key).apply()\n _deepseekApiKey.value = key\n _toastMessage.value = if (key.isNotBlank()) \"✅ DeepSeek API Key 已保存\" else \"✅ 已清除 DeepSeek API Key\"\n }\n\n fun saveDeepseekModel(model: String) {\n securePrefs.edit().putString(Constants.KEY_DEEPSEEK_MODEL, model).apply()\n _deepseekModel.value = model\n _toastMessage.value = \"✅ 已切换模型: $model\"\n }\n\n fun queryDeepseekBalance() {\n val apiKey = _deepseekApiKey.value\n if (apiKey.isBlank()) {\n _toastMessage.value = \"请先配置 DeepSeek API Key\"\n return\n }\n _isLoadingBalance.value = true\n viewModelScope.launch {\n try {\n val result = withContext(Dispatchers.IO) {\n val url = java.net.URL(\"https://api.deepseek.com/user/balance\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"GET\"\n conn.setRequestProperty(\"Authorization\", \"Bearer $apiKey\")\n conn.setRequestProperty(\"Accept\", \"application/json\")\n conn.connectTimeout = 10000\n conn.readTimeout = 10000\n val response = conn.inputStream.bufferedReader().readText()\n conn.disconnect()\n val json = com.google.gson.JsonParser.parseString(response).asJsonObject\n val balanceInfos = json.getAsJsonArray(\"balance_infos\")\n val totalBalance = balanceInfos?.firstOrNull()?.asJsonObject?.get(\"total_balance\")?.asString\n if (totalBalance != null) \"¥$totalBalance\" else \"查询失败\"\n }\n _deepseekBalance.value = result\n } catch (e: Exception) {\n _deepseekBalance.value = \"查询失败: ${e.message ?: \"网络异常\"}\"\n } finally {\n _isLoadingBalance.value = false\n }\n }\n }\n\n private fun loadConfig() {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n if (savedPat.isNotBlank()) {\n _cozePat.value = savedPat\n _isUsingDefaultPat.value = false\n } else {\n _cozePat.value = Constants.DEFAULT_COZE_PAT\n _isUsingDefaultPat.value = true\n }\n viewModelScope.launch {\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n if (savedBotId.isNotBlank()) {\n _cozeBotId.value = savedBotId\n _isUsingDefaultBotId.value = false\n } else {\n _cozeBotId.value = Constants.DEFAULT_COZE_BOT_ID\n _isUsingDefaultBotId.value = true\n }\n }\n }\n\n private fun loadTokenUsage() {\n val map = mutableMapOf()\n var grandTotal = 0\n\n // 内置通道:Coze\n val cozeTotal = securePrefs.getInt(\"coze_total_tokens\", 0)\n map[\"coze\"] = ChannelTokenStats(\n total = cozeTotal,\n input = securePrefs.getInt(\"coze_total_input\", 0),\n output = securePrefs.getInt(\"coze_total_output\", 0),\n messages = securePrefs.getInt(\"coze_total_messages\", 0)\n )\n grandTotal += cozeTotal\n\n // 内置通道:DeepSeek\n val dsTotal = securePrefs.getInt(\"ds_total_tokens\", 0)\n map[\"deepseek\"] = ChannelTokenStats(\n total = dsTotal,\n input = securePrefs.getInt(\"ds_total_input\", 0),\n output = securePrefs.getInt(\"ds_total_output\", 0),\n messages = securePrefs.getInt(\"ds_total_messages\", 0)\n )\n grandTotal += dsTotal\n\n // 自定义 API 通道\n _customApiList.value.forEach { config ->\n val prefix = \"custom:${config.id}\"\n val total = securePrefs.getInt(\"${prefix}_total_tokens\", 0)\n if (total > 0 || securePrefs.contains(\"${prefix}_total_tokens\")) {\n map[prefix] = ChannelTokenStats(\n total = total,\n input = securePrefs.getInt(\"${prefix}_total_input\", 0),\n output = securePrefs.getInt(\"${prefix}_total_output\", 0),\n messages = securePrefs.getInt(\"${prefix}_total_messages\", 0)\n )\n grandTotal += total\n }\n }\n\n _channelTokenStats.value = map\n _totalTokens.value = grandTotal\n }\n\n fun refreshTokenUsage() {\n loadTokenUsage()\n }\n\n fun saveCozePat(pat: String) {\n securePrefs.edit().putString(Constants.KEY_COZE_PAT, pat).apply()\n _cozePat.value = pat\n _isUsingDefaultPat.value = false\n _toastMessage.value = if (pat.isNotBlank()) \"✅ PAT 已保存\" else \"✅ 已恢复默认 PAT\"\n if (pat.isBlank()) {\n _cozePat.value = Constants.DEFAULT_COZE_PAT\n _isUsingDefaultPat.value = true\n }\n }\n\n fun saveCozeBotId(botId: String) {\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, botId)\n _cozeBotId.value = botId\n _isUsingDefaultBotId.value = false\n _toastMessage.value = if (botId.isNotBlank()) \"✅ Bot ID 已保存\" else \"✅ 已恢复默认 Bot ID\"\n if (botId.isBlank()) {\n _cozeBotId.value = Constants.DEFAULT_COZE_BOT_ID\n _isUsingDefaultBotId.value = true\n }\n }\n }\n\n // ============ Agent 管理 ============\n\n private fun loadAgents() {\n val json = securePrefs.getString(KEY_AGENTS, \"[]\") ?: \"[]\"\n val type = object : TypeToken>() {}.type\n _agentList.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n _currentAgentId.value = securePrefs.getString(KEY_CURRENT_AGENT, \"\") ?: \"\"\n }\n\n private fun saveAgents(agents: List) {\n securePrefs.edit().putString(KEY_AGENTS, gson.toJson(agents)).apply()\n _agentList.value = agents\n }\n\n fun createAgent(name: String, prompt: String, emoji: String) {\n if (name.isBlank() || prompt.isBlank()) {\n _toastMessage.value = \"⚠️ 名称和提示词不能为空\"\n return\n }\n viewModelScope.launch {\n _isCreatingAgent.value = true\n _toastMessage.value = \"⏳ 正在创建 Agent...\"\n\n // 1. 通过 Coze API 创建 Bot\n val createResult = cozeClient.createBot(name, prompt)\n if (createResult.isFailure) {\n _toastMessage.value = \"❌ 创建失败: ${createResult.exceptionOrNull()?.message}\"\n _isCreatingAgent.value = false\n return@launch\n }\n val botId = createResult.getOrNull()!!\n\n // 2. 发布 Bot 到 API 渠道\n val publishResult = cozeClient.publishBot(botId)\n if (publishResult.isFailure) {\n _toastMessage.value = \"❌ 发布失败: ${publishResult.exceptionOrNull()?.message}\"\n _isCreatingAgent.value = false\n return@launch\n }\n\n // 3. 保存到本地\n val agent = AgentConfig(name = name, botId = botId, prompt = prompt, emoji = emoji)\n val currentList = _agentList.value.toMutableList()\n currentList.add(agent)\n saveAgents(currentList)\n\n // 4. 设为当前 Agent\n securePrefs.edit().putString(KEY_CURRENT_AGENT, botId).apply()\n _currentAgentId.value = botId\n cozeClient.currentBotId = botId\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, botId)\n _cozeBotId.value = botId\n _isUsingDefaultBotId.value = false\n\n _toastMessage.value = \"✅ Agent「$name」创建成功\"\n _isCreatingAgent.value = false\n }\n }\n\n fun switchAgent(agent: AgentConfig) {\n cozeClient.currentBotId = agent.botId\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, agent.botId)\n }\n securePrefs.edit().putString(KEY_CURRENT_AGENT, agent.botId).apply()\n _currentAgentId.value = agent.botId\n _cozeBotId.value = agent.botId\n _isUsingDefaultBotId.value = false\n _toastMessage.value = \"✅ 已切换到「${agent.name}」\"\n }\n\n fun deleteAgent(agent: AgentConfig) {\n val currentList = _agentList.value.toMutableList()\n currentList.removeAll { it.botId == agent.botId }\n saveAgents(currentList)\n\n // 如果删除的是当前Agent,切回默认\n if (_currentAgentId.value == agent.botId) {\n val defaultBotId = Constants.DEFAULT_COZE_BOT_ID\n cozeClient.currentBotId = defaultBotId\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, defaultBotId)\n }\n securePrefs.edit().putString(KEY_CURRENT_AGENT, \"\").apply()\n _currentAgentId.value = \"\"\n _cozeBotId.value = defaultBotId\n }\n _toastMessage.value = \"✅ 已删除「${agent.name}」\"\n }\n\n fun clearToast() { _toastMessage.value = null }\n}\n"}, "5c27068ceb27f93cbd70e94bed7f319c0706493c": {"t": 1785755783.2562878, "n": 39977, "h": "2a40baa99e2c90b1", "c": "package com.meitu.generator.ui.settings\n\nimport android.content.SharedPreferences\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.meitu.generator.data.remote.CozeApiClient\nimport com.meitu.generator.repository.SettingsRepository\nimport com.meitu.generator.util.Constants\nimport com.google.gson.Gson\nimport com.google.gson.JsonParser\nimport com.google.gson.reflect.TypeToken\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport javax.inject.Inject\nimport javax.inject.Named\n\ndata class AgentConfig(\n val name: String,\n val botId: String,\n val prompt: String,\n val emoji: String = \"🤖\"\n)\n\n@HiltViewModel\nclass SettingsViewModel @Inject constructor(\n private val settingsRepo: SettingsRepository,\n private val cozeClient: CozeApiClient,\n @Named(\"securePrefs\") private val securePrefs: SharedPreferences\n) : ViewModel() {\n\n private val gson = Gson()\n\n // ============ Coze 配置 ============\n private val _cozePat = MutableStateFlow(\"\")\n val cozePat: StateFlow = _cozePat.asStateFlow()\n\n private val _cozeBotId = MutableStateFlow(\"\")\n val cozeBotId: StateFlow = _cozeBotId.asStateFlow()\n\n private val _isUsingDefaultPat = MutableStateFlow(true)\n val isUsingDefaultPat: StateFlow = _isUsingDefaultPat.asStateFlow()\n\n private val _isUsingDefaultBotId = MutableStateFlow(true)\n val isUsingDefaultBotId: StateFlow = _isUsingDefaultBotId.asStateFlow()\n\n // ============ Token 消耗统计(动态 Map) ============\n data class ChannelTokenStats(\n val total: Int = 0,\n val input: Int = 0,\n val output: Int = 0,\n val messages: Int = 0\n )\n\n private val _channelTokenStats = MutableStateFlow>(emptyMap())\n val channelTokenStats: StateFlow> = _channelTokenStats.asStateFlow()\n\n private val _totalTokens = MutableStateFlow(0)\n val totalTokens: StateFlow = _totalTokens.asStateFlow()\n\n /** 获取通道的显示名称 */\n fun getChannelDisplayName(channelKey: String): String {\n return when (channelKey) {\n \"coze\" -> \"🧠 Coze\"\n \"deepseek\" -> \"🔍 DeepSeek\"\n else -> {\n val id = channelKey.removePrefix(\"custom:\")\n _customApiList.value.find { it.id == id }?.let { \"${it.emoji} ${it.name}\" }\n ?: channelKey\n }\n }\n }\n\n // ============ AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow = _currentChannel.asStateFlow()\n\n private val _deepseekApiKey = MutableStateFlow(\"\")\n val deepseekApiKey: StateFlow = _deepseekApiKey.asStateFlow()\n\n // ============ DeepSeek 模型选择 ============\n private val _deepseekModel = MutableStateFlow(\"deepseek-v4-flash\")\n val deepseekModel: StateFlow = _deepseekModel.asStateFlow()\n\n // ============ DeepSeek 余额 ============\n private val _deepseekBalance = MutableStateFlow(null)\n val deepseekBalance: StateFlow = _deepseekBalance.asStateFlow()\n private val _isLoadingBalance = MutableStateFlow(false)\n val isLoadingBalance: StateFlow = _isLoadingBalance.asStateFlow()\n\n // ============ 自定义 API 通道 ============\n private val _customApiList = MutableStateFlow>(emptyList())\n val customApiList: StateFlow> = _customApiList.asStateFlow()\n\n private fun loadCustomApiList() {\n val json = securePrefs.getString(Constants.KEY_CUSTOM_API_LIST, \"[]\") ?: \"[]\"\n val type = object : TypeToken>() {}.type\n _customApiList.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n }\n\n private fun saveCustomApiList(list: List) {\n securePrefs.edit().putString(Constants.KEY_CUSTOM_API_LIST, gson.toJson(list)).apply()\n _customApiList.value = list\n }\n\n fun addCustomApi(config: CustomApiConfig) {\n val list = _customApiList.value.toMutableList()\n list.add(config)\n saveCustomApiList(list)\n _toastMessage.value = \"✅ 已添加自定义 API「${config.name}」\"\n }\n\n fun updateCustomApi(config: CustomApiConfig) {\n val list = _customApiList.value.toMutableList()\n val idx = list.indexOfFirst { it.id == config.id }\n if (idx >= 0) {\n list[idx] = config\n saveCustomApiList(list)\n _toastMessage.value = \"✅ 已更新「${config.name}」\"\n }\n }\n\n fun deleteCustomApi(id: String) {\n val list = _customApiList.value.toMutableList()\n list.removeAll { it.id == id }\n saveCustomApiList(list)\n val channel = _currentChannel.value\n if (channel == \"${Constants.CHANNEL_CUSTOM_PREFIX}$id\") {\n switchChannel(Constants.CHANNEL_COZE)\n }\n _toastMessage.value = \"✅ 已删除\"\n }\n\n fun getCustomApiById(id: String): CustomApiConfig? {\n return _customApiList.value.find { it.id == id }\n }\n\n // ============ GitHub 关联 ============\n data class GitHubUser(\n val login: String,\n val avatarUrl: String,\n val name: String,\n val publicRepos: Int,\n val followers: Int\n )\n\n data class GitHubRepo(\n val name: String,\n val fullName: String,\n val description: String,\n val language: String?,\n val stars: Int,\n val forks: Int,\n val updatedAt: String\n )\n\n data class GitHubCommit(\n val sha: String,\n val message: String,\n val author: String,\n val date: String\n )\n\n data class GitHubNotification(\n val title: String,\n val repoName: String,\n val type: String,\n val url: String,\n val updatedAt: String\n )\n\n private val _githubToken = MutableStateFlow(\"\")\n val githubToken: StateFlow = _githubToken.asStateFlow()\n\n private val _githubUser = MutableStateFlow(null)\n val githubUser: StateFlow = _githubUser.asStateFlow()\n\n private val _githubRepos = MutableStateFlow>(emptyList())\n val githubRepos: StateFlow> = _githubRepos.asStateFlow()\n\n private val _githubNotifications = MutableStateFlow>(emptyList())\n val githubNotifications: StateFlow> = _githubNotifications.asStateFlow()\n\n private val _selectedRepoCommits = MutableStateFlow>(emptyList())\n val selectedRepoCommits: StateFlow> = _selectedRepoCommits.asStateFlow()\n\n private val _isLoadingGitHub = MutableStateFlow(false)\n val isLoadingGitHub: StateFlow = _isLoadingGitHub.asStateFlow()\n\n private val _githubError = MutableStateFlow(null)\n val githubError: StateFlow = _githubError.asStateFlow()\n\n private fun loadGitHubToken() {\n _githubToken.value = securePrefs.getString(Constants.KEY_GITHUB_TOKEN, \"\") ?: \"\"\n }\n\n fun saveGitHubToken(token: String) {\n securePrefs.edit().putString(Constants.KEY_GITHUB_TOKEN, token).apply()\n _githubToken.value = token\n if (token.isNotBlank()) {\n refreshGitHubData()\n } else {\n _githubUser.value = null\n _githubRepos.value = emptyList()\n _githubNotifications.value = emptyList()\n }\n }\n\n fun refreshGitHubData() {\n val token = _githubToken.value\n if (token.isBlank()) return\n _isLoadingGitHub.value = true\n _githubError.value = null\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n // 获取用户信息\n val userResp = gitHubApiGet(\"/user\", token)\n val userJson = JsonParser.parseString(userResp).asJsonObject\n _githubUser.value = GitHubUser(\n login = userJson.get(\"login\")?.asString ?: \"\",\n avatarUrl = userJson.get(\"avatar_url\")?.asString ?: \"\",\n name = userJson.get(\"name\")?.asString ?: userJson.get(\"login\")?.asString ?: \"\",\n publicRepos = userJson.get(\"public_repos\")?.asInt ?: 0,\n followers = userJson.get(\"followers\")?.asInt ?: 0\n )\n\n // 获取仓库列表\n val reposResp = gitHubApiGet(\"/user/repos?sort=updated&per_page=20\", token)\n val reposArray = JsonParser.parseString(reposResp).asJsonArray\n _githubRepos.value = reposArray.map { repo ->\n val obj = repo.asJsonObject\n GitHubRepo(\n name = obj.get(\"name\")?.asString ?: \"\",\n fullName = obj.get(\"full_name\")?.asString ?: \"\",\n description = obj.get(\"description\")?.asString ?: \"\",\n language = obj.get(\"language\")?.asString,\n stars = obj.get(\"stargazers_count\")?.asInt ?: 0,\n forks = obj.get(\"forks_count\")?.asInt ?: 0,\n updatedAt = obj.get(\"updated_at\")?.asString?.take(10) ?: \"\"\n )\n }\n\n // 获取通知\n try {\n val notifResp = gitHubApiGet(\"/notifications?per_page=10\", token)\n val notifArray = JsonParser.parseString(notifResp).asJsonArray\n _githubNotifications.value = notifArray.map { n ->\n val obj = n.asJsonObject\n val subject = obj.getAsJsonObject(\"subject\")\n val repo = obj.getAsJsonObject(\"repository\")\n GitHubNotification(\n title = subject?.get(\"title\")?.asString ?: \"\",\n repoName = repo?.get(\"full_name\")?.asString ?: \"\",\n type = subject?.get(\"type\")?.asString ?: \"\",\n url = subject?.get(\"url\")?.asString ?: \"\",\n updatedAt = obj.get(\"updated_at\")?.asString?.take(10) ?: \"\"\n )\n }\n } catch (_: Exception) {\n _githubNotifications.value = emptyList()\n }\n }\n } catch (e: Exception) {\n _githubError.value = e.message ?: \"加载失败\"\n } finally {\n _isLoadingGitHub.value = false\n }\n }\n }\n\n fun loadRepoCommits(fullName: String) {\n val token = _githubToken.value\n if (token.isBlank()) return\n _isLoadingGitHub.value = true\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n val resp = gitHubApiGet(\"/repos/$fullName/commits?per_page=10\", token)\n val array = JsonParser.parseString(resp).asJsonArray\n _selectedRepoCommits.value = array.map { c ->\n val obj = c.asJsonObject\n val commit = obj.getAsJsonObject(\"commit\")\n val author = commit?.getAsJsonObject(\"author\")\n GitHubCommit(\n sha = obj.get(\"sha\")?.asString?.take(7) ?: \"\",\n message = commit?.get(\"message\")?.asString?.split(\"\\n\")?.firstOrNull() ?: \"\",\n author = author?.get(\"name\")?.asString ?: \"\",\n date = author?.get(\"date\")?.asString?.take(10) ?: \"\"\n )\n }\n }\n } catch (e: Exception) {\n _githubError.value = \"加载 commits 失败: ${e.message}\"\n } finally {\n _isLoadingGitHub.value = false\n }\n }\n }\n\n private fun gitHubApiGet(path: String, token: String): String {\n val url = java.net.URL(\"${Constants.GITHUB_API_BASE}$path\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"GET\"\n conn.setRequestProperty(\"Authorization\", \"Bearer $token\")\n conn.setRequestProperty(\"Accept\", \"application/vnd.github.v3+json\")\n conn.connectTimeout = 10000\n conn.readTimeout = 10000\n val code = conn.responseCode\n if (code != 200) {\n val err = conn.errorStream?.bufferedReader()?.readText() ?: \"HTTP $code\"\n throw Exception(\"GitHub API 错误 ($code): $err\")\n }\n return conn.inputStream.bufferedReader().readText()\n }\n\n // ============ HuggingFace ============\n data class HfModel(\n val modelId: String,\n val author: String,\n val pipelineTag: String,\n val downloads: Long,\n val likes: Int,\n val lastModified: String\n )\n\n private val _hfToken = MutableStateFlow(\"\")\n val hfToken: StateFlow = _hfToken.asStateFlow()\n\n private val _hfModels = MutableStateFlow>(emptyList())\n val hfModels: StateFlow> = _hfModels.asStateFlow()\n\n private val _isLoadingHf = MutableStateFlow(false)\n val isLoadingHf: StateFlow = _isLoadingHf.asStateFlow()\n\n private val _hfError = MutableStateFlow(null)\n val hfError: StateFlow = _hfError.asStateFlow()\n\n private val _hfTrendingTag = MutableStateFlow(\"text-generation\")\n val hfTrendingTag: StateFlow = _hfTrendingTag.asStateFlow()\n\n fun loadHfToken() {\n _hfToken.value = securePrefs.getString(Constants.KEY_HF_TOKEN, \"\") ?: \"\"\n }\n\n fun saveHfToken(token: String) {\n securePrefs.edit().putString(Constants.KEY_HF_TOKEN, token).apply()\n _hfToken.value = token\n _toastMessage.value = if (token.isNotBlank()) \"✅ HuggingFace Token 已保存\" else \"✅ 已清除\"\n }\n\n fun searchHfModels(query: String) {\n if (query.isBlank()) return\n _isLoadingHf.value = true\n _hfError.value = null\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n val encoded = java.net.URLEncoder.encode(query, \"UTF-8\")\n val resp = hfApiGet(\"/models?search=$encoded&sort=downloads&direction=-1&limit=20\")\n val array = JsonParser.parseString(resp).asJsonArray\n _hfModels.value = array.map { m ->\n val obj = m.asJsonObject\n HfModel(\n modelId = obj.get(\"modelId\")?.asString ?: obj.get(\"id\")?.asString ?: \"\",\n author = obj.get(\"author\")?.asString ?: obj.get(\"modelId\")?.asString?.split(\"/\")?.firstOrNull() ?: \"\",\n pipelineTag = obj.get(\"pipeline_tag\")?.asString ?: \"其他\",\n downloads = obj.get(\"downloads\")?.asLong ?: 0,\n likes = obj.get(\"likes\")?.asInt ?: 0,\n lastModified = obj.get(\"lastModified\")?.asString?.take(10) ?: \"\"\n )\n }\n }\n } catch (e: Exception) {\n _hfError.value = e.message ?: \"搜索失败\"\n _hfModels.value = emptyList()\n } finally {\n _isLoadingHf.value = false\n }\n }\n }\n\n fun loadHfTrending(tag: String? = null) {\n val pipelineTag = tag ?: _hfTrendingTag.value\n if (tag != null) _hfTrendingTag.value = tag\n _isLoadingHf.value = true\n _hfError.value = null\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n val resp = hfApiGet(\"/models?pipeline_tag=$pipelineTag&sort=downloads&direction=-1&limit=20\")\n val array = JsonParser.parseString(resp).asJsonArray\n _hfModels.value = array.map { m ->\n val obj = m.asJsonObject\n HfModel(\n modelId = obj.get(\"modelId\")?.asString ?: obj.get(\"id\")?.asString ?: \"\",\n author = obj.get(\"author\")?.asString ?: obj.get(\"modelId\")?.asString?.split(\"/\")?.firstOrNull() ?: \"\",\n pipelineTag = obj.get(\"pipeline_tag\")?.asString ?: \"其他\",\n downloads = obj.get(\"downloads\")?.asLong ?: 0,\n likes = obj.get(\"likes\")?.asInt ?: 0,\n lastModified = obj.get(\"lastModified\")?.asString?.take(10) ?: \"\"\n )\n }\n }\n } catch (e: Exception) {\n _hfError.value = e.message ?: \"加载失败\"\n _hfModels.value = emptyList()\n } finally {\n _isLoadingHf.value = false\n }\n }\n }\n\n private fun hfApiGet(path: String): String {\n val token = _hfToken.value\n val url = java.net.URL(\"${Constants.HF_API_BASE}$path\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"GET\"\n if (token.isNotBlank()) {\n conn.setRequestProperty(\"Authorization\", \"Bearer $token\")\n }\n conn.setRequestProperty(\"Accept\", \"application/json\")\n conn.connectTimeout = 10000\n conn.readTimeout = 10000\n val code = conn.responseCode\n if (code != 200) {\n val err = conn.errorStream?.bufferedReader()?.readText() ?: \"HTTP $code\"\n throw Exception(\"HuggingFace API 错误 ($code): $err\")\n }\n return conn.inputStream.bufferedReader().readText()\n }\n\n // ============ Server酱 / PushPlus ============\n private val _serverchanKey = MutableStateFlow(\"\")\n val serverchanKey: StateFlow = _serverchanKey.asStateFlow()\n\n private val _pushplusToken = MutableStateFlow(\"\")\n val pushplusToken: StateFlow = _pushplusToken.asStateFlow()\n\n private val _pushTestResult = MutableStateFlow(null)\n val pushTestResult: StateFlow = _pushTestResult.asStateFlow()\n\n private val _isTestingPush = MutableStateFlow(false)\n val isTestingPush: StateFlow = _isTestingPush.asStateFlow()\n\n fun loadPushConfig() {\n _serverchanKey.value = securePrefs.getString(Constants.KEY_SERVERCHAN_KEY, \"\") ?: \"\"\n _pushplusToken.value = securePrefs.getString(Constants.KEY_PUSHPLUS_TOKEN, \"\") ?: \"\"\n }\n\n fun saveServerchanKey(key: String) {\n securePrefs.edit().putString(Constants.KEY_SERVERCHAN_KEY, key).apply()\n _serverchanKey.value = key\n _toastMessage.value = if (key.isNotBlank()) \"✅ Server酱 Key 已保存\" else \"✅ 已清除\"\n }\n\n fun savePushplusToken(token: String) {\n securePrefs.edit().putString(Constants.KEY_PUSHPLUS_TOKEN, token).apply()\n _pushplusToken.value = token\n _toastMessage.value = if (token.isNotBlank()) \"✅ PushPlus Token 已保存\" else \"✅ 已清除\"\n }\n\n fun testServerchan() {\n val key = _serverchanKey.value\n if (key.isBlank()) { _toastMessage.value = \"请先配置 Server酱 SendKey\"; return }\n _isTestingPush.value = true; _pushTestResult.value = null\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n val url = java.net.URL(\"https://sctapi.ftqq.com/$key.send\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"POST\"\n conn.doOutput = true\n conn.setRequestProperty(\"Content-Type\", \"application/json; charset=utf-8\")\n conn.connectTimeout = 10000\n conn.readTimeout = 10000\n val body = \"\"\"{\"title\":\"布老师App 推送测试\",\"desp\":\"如果你看到这条消息,说明推送配置成功了 🎉\"}\"\"\"\n conn.outputStream.write(body.toByteArray(Charsets.UTF_8))\n val resp = conn.inputStream.bufferedReader().readText()\n conn.disconnect()\n val json = JsonParser.parseString(resp).asJsonObject\n val code = json.get(\"code\")?.asInt\n _pushTestResult.value = if (code == 0) \"✅ 测试成功!\" else \"❌ 推送失败: ${json.get(\"message\")?.asString ?: resp}\"\n }\n } catch (e: Exception) {\n _pushTestResult.value = \"❌ 测试失败: ${e.message}\"\n } finally {\n _isTestingPush.value = false\n }\n }\n }\n\n fun testPushplus() {\n val token = _pushplusToken.value\n if (token.isBlank()) { _toastMessage.value = \"请先配置 PushPlus Token\"; return }\n _isTestingPush.value = true; _pushTestResult.value = null\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n val url = java.net.URL(\"https://www.pushplus.plus/send\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"POST\"\n conn.doOutput = true\n conn.setRequestProperty(\"Content-Type\", \"application/json; charset=utf-8\")\n conn.connectTimeout = 10000\n conn.readTimeout = 10000\n val body = \"\"\"{\"token\":\"$token\",\"title\":\"布老师App 推送测试\",\"content\":\"如果你看到这条消息,说明推送配置成功了 🎉\"}\"\"\"\n conn.outputStream.write(body.toByteArray(Charsets.UTF_8))\n val resp = conn.inputStream.bufferedReader().readText()\n conn.disconnect()\n val json = JsonParser.parseString(resp).asJsonObject\n val code = json.get(\"code\")?.asInt\n _pushTestResult.value = if (code == 200) \"✅ 测试成功!\" else \"❌ 推送失败: ${json.get(\"msg\")?.asString ?: resp}\"\n }\n } catch (e: Exception) {\n _pushTestResult.value = \"❌ 测试失败: ${e.message}\"\n } finally {\n _isTestingPush.value = false\n }\n }\n }\n\n fun clearPushTestResult() { _pushTestResult.value = null }\n\n // ============ 通用余额查询 ============\n data class BalanceCheckService(\n val id: String,\n val name: String,\n val baseUrl: String,\n val apiKey: String\n )\n\n private val _balanceServices = MutableStateFlow>(emptyList())\n val balanceServices: StateFlow> = _balanceServices.asStateFlow()\n\n private val _balanceResults = MutableStateFlow>(emptyMap())\n val balanceResults: StateFlow> = _balanceResults.asStateFlow()\n\n private val _isLoadingBalanceCheck = MutableStateFlow(false)\n val isLoadingBalanceCheck: StateFlow = _isLoadingBalanceCheck.asStateFlow()\n\n fun loadBalanceServices() {\n val json = securePrefs.getString(Constants.KEY_BALANCE_CHECK_LIST, \"[]\") ?: \"[]\"\n val type = object : TypeToken>() {}.type\n _balanceServices.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n }\n\n private fun saveBalanceServices(list: List) {\n securePrefs.edit().putString(Constants.KEY_BALANCE_CHECK_LIST, gson.toJson(list)).apply()\n _balanceServices.value = list\n }\n\n fun addBalanceService(name: String, baseUrl: String, apiKey: String) {\n val service = BalanceCheckService(\n id = java.util.UUID.randomUUID().toString(),\n name = name.trim(),\n baseUrl = baseUrl.trim().trimEnd('/'),\n apiKey = apiKey.trim()\n )\n val list = _balanceServices.value.toMutableList()\n list.add(service)\n saveBalanceServices(list)\n _toastMessage.value = \"✅ 已添加余额查询「${service.name}」\"\n // 添加后自动查询\n queryBalance(service.id)\n }\n\n fun deleteBalanceService(id: String) {\n val list = _balanceServices.value.toMutableList()\n list.removeAll { it.id == id }\n saveBalanceServices(list)\n val results = _balanceResults.value.toMutableMap()\n results.remove(id)\n _balanceResults.value = results\n _toastMessage.value = \"✅ 已删除\"\n }\n\n fun queryBalance(serviceId: String) {\n val service = _balanceServices.value.find { it.id == serviceId } ?: return\n _isLoadingBalanceCheck.value = true\n viewModelScope.launch {\n try {\n val result = withContext(Dispatchers.IO) {\n // 尝试多个常见余额接口路径\n val paths = listOf(\"/user/balance\", \"/v1/user/balance\", \"/dashboard/billing/credit_grants\")\n var lastError: String? = null\n for (path in paths) {\n try {\n val url = java.net.URL(\"${service.baseUrl}$path\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"GET\"\n conn.setRequestProperty(\"Authorization\", \"Bearer ${service.apiKey}\")\n conn.setRequestProperty(\"Accept\", \"application/json\")\n conn.connectTimeout = 8000\n conn.readTimeout = 8000\n val code = conn.responseCode\n if (code == 200) {\n val text = conn.inputStream.bufferedReader().readText()\n conn.disconnect()\n // 尝试解析常见格式\n return@withContext try {\n val json = JsonParser.parseString(text).asJsonObject\n // 格式1: DeepSeek → balance_infos[0].total_balance\n val balanceInfos = json.getAsJsonArray(\"balance_infos\")\n if (balanceInfos != null && balanceInfos.size() > 0) {\n \"¥${balanceInfos[0].asJsonObject.get(\"total_balance\")?.asString ?: \"?\"}\"\n } else {\n val data = json.getAsJsonObject(\"data\")\n when {\n data != null && data.get(\"balance\") != null -> \"¥${data.get(\"balance\")?.asString ?: data.get(\"balance\")?.asNumber ?: \"?\"}\"\n json.get(\"balance\") != null -> \"¥${json.get(\"balance\")?.asString ?: json.get(\"balance\")?.asNumber ?: \"?\"}\"\n json.get(\"total_balance\") != null -> \"¥${json.get(\"total_balance\")?.asString ?: \"?\"}\"\n else -> text.take(200)\n }\n }\n } catch (_: Exception) {\n text.take(200)\n }\n }\n conn.disconnect()\n lastError = \"HTTP $code\"\n } catch (_: Exception) {\n lastError = \"未知错误\"\n }\n }\n \"❌ $lastError\"\n }\n val results = _balanceResults.value.toMutableMap()\n results[serviceId] = result\n _balanceResults.value = results\n } catch (e: Exception) {\n val results = _balanceResults.value.toMutableMap()\n results[serviceId] = \"❌ ${e.message}\"\n _balanceResults.value = results\n } finally {\n _isLoadingBalanceCheck.value = false\n }\n }\n }\n\n fun queryAllBalances() {\n _balanceServices.value.forEach { queryBalance(it.id) }\n }\n\n // ============ Agent 管理 ============\n private val _agentList = MutableStateFlow>(emptyList())\n val agentList: StateFlow> = _agentList.asStateFlow()\n\n private val _currentAgentId = MutableStateFlow(\"\")\n val currentAgentId: StateFlow = _currentAgentId.asStateFlow()\n\n private val _isCreatingAgent = MutableStateFlow(false)\n val isCreatingAgent: StateFlow = _isCreatingAgent.asStateFlow()\n\n // ============ Toast ============\n private val _toastMessage = MutableStateFlow(null)\n val toastMessage: StateFlow = _toastMessage.asStateFlow()\n\n companion object {\n private const val KEY_AGENTS = \"agent_list\"\n private const val KEY_CURRENT_AGENT = \"current_agent_id\"\n }\n\n init {\n loadConfig()\n loadTokenUsage()\n loadAgents()\n loadChannelAndDeepseek()\n loadCustomApiList()\n loadGitHubToken()\n loadHfToken()\n loadPushConfig()\n loadBalanceServices()\n }\n\n private fun loadChannelAndDeepseek() {\n val channel = securePrefs.getString(Constants.KEY_AI_CHANNEL, Constants.CHANNEL_COZE) ?: Constants.CHANNEL_COZE\n _currentChannel.value = channel\n val dsKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n _deepseekApiKey.value = dsKey\n val dsModel = securePrefs.getString(Constants.KEY_DEEPSEEK_MODEL, \"deepseek-v4-flash\") ?: \"deepseek-v4-flash\"\n _deepseekModel.value = dsModel\n }\n\n fun switchChannel(channel: String) {\n securePrefs.edit().putString(Constants.KEY_AI_CHANNEL, channel).apply()\n _currentChannel.value = channel\n\n val isCustom = channel.startsWith(Constants.CHANNEL_CUSTOM_PREFIX)\n if (isCustom) {\n cozeClient.currentBotId = \"\"\n val customId = channel.removePrefix(Constants.CHANNEL_CUSTOM_PREFIX)\n val config = getCustomApiById(customId)\n _toastMessage.value = \"✅ 已切换到「${config?.name ?: \"自定义\"}」\"\n } else {\n cozeClient.currentBotId = if (channel == Constants.CHANNEL_DEEPSEEK) {\n \"\"\n } else {\n val savedBotId = securePrefs.getString(\"current_agent_id\", \"\") ?: \"\"\n if (savedBotId.isNotBlank()) savedBotId\n else {\n viewModelScope.launch {\n val botId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n cozeClient.currentBotId = botId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n _cozeBotId.value = cozeClient.currentBotId\n }\n _cozeBotId.value\n }\n }\n _toastMessage.value = when (channel) {\n Constants.CHANNEL_DEEPSEEK -> \"✅ 已切换到 DeepSeek\"\n else -> \"✅ 已切换到 Coze\"\n }\n }\n }\n\n fun saveDeepseekApiKey(key: String) {\n securePrefs.edit().putString(Constants.KEY_DEEPSEEK_API_KEY, key).apply()\n _deepseekApiKey.value = key\n _toastMessage.value = if (key.isNotBlank()) \"✅ DeepSeek API Key 已保存\" else \"✅ 已清除 DeepSeek API Key\"\n }\n\n fun saveDeepseekModel(model: String) {\n securePrefs.edit().putString(Constants.KEY_DEEPSEEK_MODEL, model).apply()\n _deepseekModel.value = model\n _toastMessage.value = \"✅ 已切换模型: $model\"\n }\n\n fun queryDeepseekBalance() {\n val apiKey = _deepseekApiKey.value\n if (apiKey.isBlank()) {\n _toastMessage.value = \"请先配置 DeepSeek API Key\"\n return\n }\n _isLoadingBalance.value = true\n viewModelScope.launch {\n try {\n val result = withContext(Dispatchers.IO) {\n val url = java.net.URL(\"https://api.deepseek.com/user/balance\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"GET\"\n conn.setRequestProperty(\"Authorization\", \"Bearer $apiKey\")\n conn.setRequestProperty(\"Accept\", \"application/json\")\n conn.connectTimeout = 10000\n conn.readTimeout = 10000\n val response = conn.inputStream.bufferedReader().readText()\n conn.disconnect()\n val json = com.google.gson.JsonParser.parseString(response).asJsonObject\n val balanceInfos = json.getAsJsonArray(\"balance_infos\")\n val totalBalance = balanceInfos?.firstOrNull()?.asJsonObject?.get(\"total_balance\")?.asString\n if (totalBalance != null) \"¥$totalBalance\" else \"查询失败\"\n }\n _deepseekBalance.value = result\n } catch (e: Exception) {\n _deepseekBalance.value = \"查询失败: ${e.message ?: \"网络异常\"}\"\n } finally {\n _isLoadingBalance.value = false\n }\n }\n }\n\n private fun loadConfig() {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n if (savedPat.isNotBlank()) {\n _cozePat.value = savedPat\n _isUsingDefaultPat.value = false\n } else {\n _cozePat.value = Constants.DEFAULT_COZE_PAT\n _isUsingDefaultPat.value = true\n }\n viewModelScope.launch {\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n if (savedBotId.isNotBlank()) {\n _cozeBotId.value = savedBotId\n _isUsingDefaultBotId.value = false\n } else {\n _cozeBotId.value = Constants.DEFAULT_COZE_BOT_ID\n _isUsingDefaultBotId.value = true\n }\n }\n }\n\n private fun loadTokenUsage() {\n val map = mutableMapOf()\n var grandTotal = 0\n\n // 内置通道:Coze\n val cozeTotal = securePrefs.getInt(\"coze_total_tokens\", 0)\n map[\"coze\"] = ChannelTokenStats(\n total = cozeTotal,\n input = securePrefs.getInt(\"coze_total_input\", 0),\n output = securePrefs.getInt(\"coze_total_output\", 0),\n messages = securePrefs.getInt(\"coze_total_messages\", 0)\n )\n grandTotal += cozeTotal\n\n // 内置通道:DeepSeek\n val dsTotal = securePrefs.getInt(\"ds_total_tokens\", 0)\n map[\"deepseek\"] = ChannelTokenStats(\n total = dsTotal,\n input = securePrefs.getInt(\"ds_total_input\", 0),\n output = securePrefs.getInt(\"ds_total_output\", 0),\n messages = securePrefs.getInt(\"ds_total_messages\", 0)\n )\n grandTotal += dsTotal\n\n // 自定义 API 通道\n _customApiList.value.forEach { config ->\n val prefix = \"custom:${config.id}\"\n val total = securePrefs.getInt(\"${prefix}_total_tokens\", 0)\n if (total > 0 || securePrefs.contains(\"${prefix}_total_tokens\")) {\n map[prefix] = ChannelTokenStats(\n total = total,\n input = securePrefs.getInt(\"${prefix}_total_input\", 0),\n output = securePrefs.getInt(\"${prefix}_total_output\", 0),\n messages = securePrefs.getInt(\"${prefix}_total_messages\", 0)\n )\n grandTotal += total\n }\n }\n\n _channelTokenStats.value = map\n _totalTokens.value = grandTotal\n }\n\n fun refreshTokenUsage() {\n loadTokenUsage()\n }\n\n fun saveCozePat(pat: String) {\n securePrefs.edit().putString(Constants.KEY_COZE_PAT, pat).apply()\n _cozePat.value = pat\n _isUsingDefaultPat.value = false\n _toastMessage.value = if (pat.isNotBlank()) \"✅ PAT 已保存\" else \"✅ 已恢复默认 PAT\"\n if (pat.isBlank()) {\n _cozePat.value = Constants.DEFAULT_COZE_PAT\n _isUsingDefaultPat.value = true\n }\n }\n\n fun saveCozeBotId(botId: String) {\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, botId)\n _cozeBotId.value = botId\n _isUsingDefaultBotId.value = false\n _toastMessage.value = if (botId.isNotBlank()) \"✅ Bot ID 已保存\" else \"✅ 已恢复默认 Bot ID\"\n if (botId.isBlank()) {\n _cozeBotId.value = Constants.DEFAULT_COZE_BOT_ID\n _isUsingDefaultBotId.value = true\n }\n }\n }\n\n // ============ Agent 管理 ============\n\n private fun loadAgents() {\n val json = securePrefs.getString(KEY_AGENTS, \"[]\") ?: \"[]\"\n val type = object : TypeToken>() {}.type\n _agentList.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n _currentAgentId.value = securePrefs.getString(KEY_CURRENT_AGENT, \"\") ?: \"\"\n }\n\n private fun saveAgents(agents: List) {\n securePrefs.edit().putString(KEY_AGENTS, gson.toJson(agents)).apply()\n _agentList.value = agents\n }\n\n fun createAgent(name: String, prompt: String, emoji: String) {\n if (name.isBlank() || prompt.isBlank()) {\n _toastMessage.value = \"⚠️ 名称和提示词不能为空\"\n return\n }\n viewModelScope.launch {\n _isCreatingAgent.value = true\n _toastMessage.value = \"⏳ 正在创建 Agent...\"\n\n // 1. 通过 Coze API 创建 Bot\n val createResult = cozeClient.createBot(name, prompt)\n if (createResult.isFailure) {\n _toastMessage.value = \"❌ 创建失败: ${createResult.exceptionOrNull()?.message}\"\n _isCreatingAgent.value = false\n return@launch\n }\n val botId = createResult.getOrNull()!!\n\n // 2. 发布 Bot 到 API 渠道\n val publishResult = cozeClient.publishBot(botId)\n if (publishResult.isFailure) {\n _toastMessage.value = \"❌ 发布失败: ${publishResult.exceptionOrNull()?.message}\"\n _isCreatingAgent.value = false\n return@launch\n }\n\n // 3. 保存到本地\n val agent = AgentConfig(name = name, botId = botId, prompt = prompt, emoji = emoji)\n val currentList = _agentList.value.toMutableList()\n currentList.add(agent)\n saveAgents(currentList)\n\n // 4. 设为当前 Agent\n securePrefs.edit().putString(KEY_CURRENT_AGENT, botId).apply()\n _currentAgentId.value = botId\n cozeClient.currentBotId = botId\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, botId)\n _cozeBotId.value = botId\n _isUsingDefaultBotId.value = false\n\n _toastMessage.value = \"✅ Agent「$name」创建成功\"\n _isCreatingAgent.value = false\n }\n }\n\n fun switchAgent(agent: AgentConfig) {\n cozeClient.currentBotId = agent.botId\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, agent.botId)\n }\n securePrefs.edit().putString(KEY_CURRENT_AGENT, agent.botId).apply()\n _currentAgentId.value = agent.botId\n _cozeBotId.value = agent.botId\n _isUsingDefaultBotId.value = false\n _toastMessage.value = \"✅ 已切换到「${agent.name}」\"\n }\n\n fun deleteAgent(agent: AgentConfig) {\n val currentList = _agentList.value.toMutableList()\n currentList.removeAll { it.botId == agent.botId }\n saveAgents(currentList)\n\n // 如果删除的是当前Agent,切回默认\n if (_currentAgentId.value == agent.botId) {\n val defaultBotId = Constants.DEFAULT_COZE_BOT_ID\n cozeClient.currentBotId = defaultBotId\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, defaultBotId)\n }\n securePrefs.edit().putString(KEY_CURRENT_AGENT, \"\").apply()\n _currentAgentId.value = \"\"\n _cozeBotId.value = defaultBotId\n }\n _toastMessage.value = \"✅ 已删除「${agent.name}」\"\n }\n\n fun clearToast() { _toastMessage.value = null }\n}\n"}, "4703c96fe20610589a4575a149ee011678be34ea": {"t": 1785755784.89379, "n": 39986, "h": "a37f3ea37910f516", "c": "package com.meitu.generator.ui.settings\n\nimport android.content.SharedPreferences\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.meitu.generator.data.remote.CozeApiClient\nimport com.meitu.generator.repository.SettingsRepository\nimport com.meitu.generator.util.Constants\nimport com.google.gson.Gson\nimport com.google.gson.JsonParser\nimport com.google.gson.reflect.TypeToken\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport javax.inject.Inject\nimport javax.inject.Named\n\ndata class AgentConfig(\n val name: String,\n val botId: String,\n val prompt: String,\n val emoji: String = \"🤖\"\n)\n\n@HiltViewModel\nclass SettingsViewModel @Inject constructor(\n private val settingsRepo: SettingsRepository,\n private val cozeClient: CozeApiClient,\n @Named(\"securePrefs\") private val securePrefs: SharedPreferences\n) : ViewModel() {\n\n private val gson = Gson()\n\n // ============ Coze 配置 ============\n private val _cozePat = MutableStateFlow(\"\")\n val cozePat: StateFlow = _cozePat.asStateFlow()\n\n private val _cozeBotId = MutableStateFlow(\"\")\n val cozeBotId: StateFlow = _cozeBotId.asStateFlow()\n\n private val _isUsingDefaultPat = MutableStateFlow(true)\n val isUsingDefaultPat: StateFlow = _isUsingDefaultPat.asStateFlow()\n\n private val _isUsingDefaultBotId = MutableStateFlow(true)\n val isUsingDefaultBotId: StateFlow = _isUsingDefaultBotId.asStateFlow()\n\n // ============ Token 消耗统计(动态 Map) ============\n data class ChannelTokenStats(\n val total: Int = 0,\n val input: Int = 0,\n val output: Int = 0,\n val messages: Int = 0\n )\n\n private val _channelTokenStats = MutableStateFlow>(emptyMap())\n val channelTokenStats: StateFlow> = _channelTokenStats.asStateFlow()\n\n private val _totalTokens = MutableStateFlow(0)\n val totalTokens: StateFlow = _totalTokens.asStateFlow()\n\n /** 获取通道的显示名称 */\n fun getChannelDisplayName(channelKey: String): String {\n return when (channelKey) {\n \"coze\" -> \"🧠 Coze\"\n \"deepseek\" -> \"🔍 DeepSeek\"\n else -> {\n val id = channelKey.removePrefix(\"custom:\")\n _customApiList.value.find { it.id == id }?.let { \"${it.emoji} ${it.name}\" }\n ?: channelKey\n }\n }\n }\n\n // ============ AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow = _currentChannel.asStateFlow()\n\n private val _deepseekApiKey = MutableStateFlow(\"\")\n val deepseekApiKey: StateFlow = _deepseekApiKey.asStateFlow()\n\n // ============ DeepSeek 模型选择 ============\n private val _deepseekModel = MutableStateFlow(\"deepseek-v4-flash\")\n val deepseekModel: StateFlow = _deepseekModel.asStateFlow()\n\n // ============ DeepSeek 余额 ============\n private val _deepseekBalance = MutableStateFlow(null)\n val deepseekBalance: StateFlow = _deepseekBalance.asStateFlow()\n private val _isLoadingBalance = MutableStateFlow(false)\n val isLoadingBalance: StateFlow = _isLoadingBalance.asStateFlow()\n\n // ============ 自定义 API 通道 ============\n private val _customApiList = MutableStateFlow>(emptyList())\n val customApiList: StateFlow> = _customApiList.asStateFlow()\n\n private fun loadCustomApiList() {\n val json = securePrefs.getString(Constants.KEY_CUSTOM_API_LIST, \"[]\") ?: \"[]\"\n val type = object : TypeToken>() {}.type\n _customApiList.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n }\n\n private fun saveCustomApiList(list: List) {\n securePrefs.edit().putString(Constants.KEY_CUSTOM_API_LIST, gson.toJson(list)).apply()\n _customApiList.value = list\n }\n\n fun addCustomApi(config: CustomApiConfig) {\n val list = _customApiList.value.toMutableList()\n list.add(config)\n saveCustomApiList(list)\n _toastMessage.value = \"✅ 已添加自定义 API「${config.name}」\"\n }\n\n fun updateCustomApi(config: CustomApiConfig) {\n val list = _customApiList.value.toMutableList()\n val idx = list.indexOfFirst { it.id == config.id }\n if (idx >= 0) {\n list[idx] = config\n saveCustomApiList(list)\n _toastMessage.value = \"✅ 已更新「${config.name}」\"\n }\n }\n\n fun deleteCustomApi(id: String) {\n val list = _customApiList.value.toMutableList()\n list.removeAll { it.id == id }\n saveCustomApiList(list)\n val channel = _currentChannel.value\n if (channel == \"${Constants.CHANNEL_CUSTOM_PREFIX}$id\") {\n switchChannel(Constants.CHANNEL_COZE)\n }\n _toastMessage.value = \"✅ 已删除\"\n }\n\n fun getCustomApiById(id: String): CustomApiConfig? {\n return _customApiList.value.find { it.id == id }\n }\n\n // ============ GitHub 关联 ============\n data class GitHubUser(\n val login: String,\n val avatarUrl: String,\n val name: String,\n val publicRepos: Int,\n val followers: Int\n )\n\n data class GitHubRepo(\n val name: String,\n val fullName: String,\n val description: String,\n val language: String?,\n val stars: Int,\n val forks: Int,\n val updatedAt: String\n )\n\n data class GitHubCommit(\n val sha: String,\n val message: String,\n val author: String,\n val date: String\n )\n\n data class GitHubNotification(\n val title: String,\n val repoName: String,\n val type: String,\n val url: String,\n val updatedAt: String\n )\n\n private val _githubToken = MutableStateFlow(\"\")\n val githubToken: StateFlow = _githubToken.asStateFlow()\n\n private val _githubUser = MutableStateFlow(null)\n val githubUser: StateFlow = _githubUser.asStateFlow()\n\n private val _githubRepos = MutableStateFlow>(emptyList())\n val githubRepos: StateFlow> = _githubRepos.asStateFlow()\n\n private val _githubNotifications = MutableStateFlow>(emptyList())\n val githubNotifications: StateFlow> = _githubNotifications.asStateFlow()\n\n private val _selectedRepoCommits = MutableStateFlow>(emptyList())\n val selectedRepoCommits: StateFlow> = _selectedRepoCommits.asStateFlow()\n\n private val _isLoadingGitHub = MutableStateFlow(false)\n val isLoadingGitHub: StateFlow = _isLoadingGitHub.asStateFlow()\n\n private val _githubError = MutableStateFlow(null)\n val githubError: StateFlow = _githubError.asStateFlow()\n\n private fun loadGitHubToken() {\n _githubToken.value = securePrefs.getString(Constants.KEY_GITHUB_TOKEN, \"\") ?: \"\"\n }\n\n fun saveGitHubToken(token: String) {\n securePrefs.edit().putString(Constants.KEY_GITHUB_TOKEN, token).apply()\n _githubToken.value = token\n if (token.isNotBlank()) {\n refreshGitHubData()\n } else {\n _githubUser.value = null\n _githubRepos.value = emptyList()\n _githubNotifications.value = emptyList()\n }\n }\n\n fun refreshGitHubData() {\n val token = _githubToken.value\n if (token.isBlank()) return\n _isLoadingGitHub.value = true\n _githubError.value = null\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n // 获取用户信息\n val userResp = gitHubApiGet(\"/user\", token)\n val userJson = JsonParser.parseString(userResp).asJsonObject\n _githubUser.value = GitHubUser(\n login = userJson.get(\"login\")?.asString ?: \"\",\n avatarUrl = userJson.get(\"avatar_url\")?.asString ?: \"\",\n name = userJson.get(\"name\")?.asString ?: userJson.get(\"login\")?.asString ?: \"\",\n publicRepos = userJson.get(\"public_repos\")?.asInt ?: 0,\n followers = userJson.get(\"followers\")?.asInt ?: 0\n )\n\n // 获取仓库列表\n val reposResp = gitHubApiGet(\"/user/repos?sort=updated&per_page=20\", token)\n val reposArray = JsonParser.parseString(reposResp).asJsonArray\n _githubRepos.value = reposArray.map { repo ->\n val obj = repo.asJsonObject\n GitHubRepo(\n name = obj.get(\"name\")?.asString ?: \"\",\n fullName = obj.get(\"full_name\")?.asString ?: \"\",\n description = obj.get(\"description\")?.asString ?: \"\",\n language = obj.get(\"language\")?.asString,\n stars = obj.get(\"stargazers_count\")?.asInt ?: 0,\n forks = obj.get(\"forks_count\")?.asInt ?: 0,\n updatedAt = obj.get(\"updated_at\")?.asString?.take(10) ?: \"\"\n )\n }\n\n // 获取通知\n try {\n val notifResp = gitHubApiGet(\"/notifications?per_page=10\", token)\n val notifArray = JsonParser.parseString(notifResp).asJsonArray\n _githubNotifications.value = notifArray.map { n ->\n val obj = n.asJsonObject\n val subject = obj.getAsJsonObject(\"subject\")\n val repo = obj.getAsJsonObject(\"repository\")\n GitHubNotification(\n title = subject?.get(\"title\")?.asString ?: \"\",\n repoName = repo?.get(\"full_name\")?.asString ?: \"\",\n type = subject?.get(\"type\")?.asString ?: \"\",\n url = subject?.get(\"url\")?.asString ?: \"\",\n updatedAt = obj.get(\"updated_at\")?.asString?.take(10) ?: \"\"\n )\n }\n } catch (_: Exception) {\n _githubNotifications.value = emptyList()\n }\n }\n } catch (e: Exception) {\n _githubError.value = e.message ?: \"加载失败\"\n } finally {\n _isLoadingGitHub.value = false\n }\n }\n }\n\n fun loadRepoCommits(fullName: String) {\n val token = _githubToken.value\n if (token.isBlank()) return\n _isLoadingGitHub.value = true\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n val resp = gitHubApiGet(\"/repos/$fullName/commits?per_page=10\", token)\n val array = JsonParser.parseString(resp).asJsonArray\n _selectedRepoCommits.value = array.map { c ->\n val obj = c.asJsonObject\n val commit = obj.getAsJsonObject(\"commit\")\n val author = commit?.getAsJsonObject(\"author\")\n GitHubCommit(\n sha = obj.get(\"sha\")?.asString?.take(7) ?: \"\",\n message = commit?.get(\"message\")?.asString?.split(\"\\n\")?.firstOrNull() ?: \"\",\n author = author?.get(\"name\")?.asString ?: \"\",\n date = author?.get(\"date\")?.asString?.take(10) ?: \"\"\n )\n }\n }\n } catch (e: Exception) {\n _githubError.value = \"加载 commits 失败: ${e.message}\"\n } finally {\n _isLoadingGitHub.value = false\n }\n }\n }\n\n private fun gitHubApiGet(path: String, token: String): String {\n val url = java.net.URL(\"${Constants.GITHUB_API_BASE}$path\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"GET\"\n conn.setRequestProperty(\"Authorization\", \"Bearer $token\")\n conn.setRequestProperty(\"Accept\", \"application/vnd.github.v3+json\")\n conn.connectTimeout = 10000\n conn.readTimeout = 10000\n val code = conn.responseCode\n if (code != 200) {\n val err = conn.errorStream?.bufferedReader()?.readText() ?: \"HTTP $code\"\n throw Exception(\"GitHub API 错误 ($code): $err\")\n }\n return conn.inputStream.bufferedReader().readText()\n }\n\n // ============ HuggingFace ============\n data class HfModel(\n val modelId: String,\n val author: String,\n val pipelineTag: String,\n val downloads: Long,\n val likes: Int,\n val lastModified: String\n )\n\n private val _hfToken = MutableStateFlow(\"\")\n val hfToken: StateFlow = _hfToken.asStateFlow()\n\n private val _hfModels = MutableStateFlow>(emptyList())\n val hfModels: StateFlow> = _hfModels.asStateFlow()\n\n private val _isLoadingHf = MutableStateFlow(false)\n val isLoadingHf: StateFlow = _isLoadingHf.asStateFlow()\n\n private val _hfError = MutableStateFlow(null)\n val hfError: StateFlow = _hfError.asStateFlow()\n\n private val _hfTrendingTag = MutableStateFlow(\"text-generation\")\n val hfTrendingTag: StateFlow = _hfTrendingTag.asStateFlow()\n\n fun loadHfToken() {\n _hfToken.value = securePrefs.getString(Constants.KEY_HF_TOKEN, \"\") ?: \"\"\n }\n\n fun saveHfToken(token: String) {\n securePrefs.edit().putString(Constants.KEY_HF_TOKEN, token).apply()\n _hfToken.value = token\n _toastMessage.value = if (token.isNotBlank()) \"✅ HuggingFace Token 已保存\" else \"✅ 已清除\"\n }\n\n fun searchHfModels(query: String) {\n if (query.isBlank()) return\n _isLoadingHf.value = true\n _hfError.value = null\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n val encoded = java.net.URLEncoder.encode(query, \"UTF-8\")\n val resp = hfApiGet(\"/models?search=$encoded&sort=downloads&direction=-1&limit=20\")\n val array = JsonParser.parseString(resp).asJsonArray\n _hfModels.value = array.map { m ->\n val obj = m.asJsonObject\n HfModel(\n modelId = obj.get(\"modelId\")?.asString ?: obj.get(\"id\")?.asString ?: \"\",\n author = obj.get(\"author\")?.asString ?: obj.get(\"modelId\")?.asString?.split(\"/\")?.firstOrNull() ?: \"\",\n pipelineTag = obj.get(\"pipeline_tag\")?.asString ?: \"其他\",\n downloads = obj.get(\"downloads\")?.asLong ?: 0,\n likes = obj.get(\"likes\")?.asInt ?: 0,\n lastModified = obj.get(\"lastModified\")?.asString?.take(10) ?: \"\"\n )\n }\n }\n } catch (e: Exception) {\n _hfError.value = e.message ?: \"搜索失败\"\n _hfModels.value = emptyList()\n } finally {\n _isLoadingHf.value = false\n }\n }\n }\n\n fun loadHfTrending(tag: String? = null) {\n val pipelineTag = tag ?: _hfTrendingTag.value\n if (tag != null) _hfTrendingTag.value = tag\n _isLoadingHf.value = true\n _hfError.value = null\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n val resp = hfApiGet(\"/models?pipeline_tag=$pipelineTag&sort=downloads&direction=-1&limit=20\")\n val array = JsonParser.parseString(resp).asJsonArray\n _hfModels.value = array.map { m ->\n val obj = m.asJsonObject\n HfModel(\n modelId = obj.get(\"modelId\")?.asString ?: obj.get(\"id\")?.asString ?: \"\",\n author = obj.get(\"author\")?.asString ?: obj.get(\"modelId\")?.asString?.split(\"/\")?.firstOrNull() ?: \"\",\n pipelineTag = obj.get(\"pipeline_tag\")?.asString ?: \"其他\",\n downloads = obj.get(\"downloads\")?.asLong ?: 0,\n likes = obj.get(\"likes\")?.asInt ?: 0,\n lastModified = obj.get(\"lastModified\")?.asString?.take(10) ?: \"\"\n )\n }\n }\n } catch (e: Exception) {\n _hfError.value = e.message ?: \"加载失败\"\n _hfModels.value = emptyList()\n } finally {\n _isLoadingHf.value = false\n }\n }\n }\n\n private fun hfApiGet(path: String): String {\n val token = _hfToken.value\n val url = java.net.URL(\"${Constants.HF_API_BASE}$path\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"GET\"\n if (token.isNotBlank()) {\n conn.setRequestProperty(\"Authorization\", \"Bearer $token\")\n }\n conn.setRequestProperty(\"Accept\", \"application/json\")\n conn.connectTimeout = 10000\n conn.readTimeout = 10000\n val code = conn.responseCode\n if (code != 200) {\n val err = conn.errorStream?.bufferedReader()?.readText() ?: \"HTTP $code\"\n throw Exception(\"HuggingFace API 错误 ($code): $err\")\n }\n return conn.inputStream.bufferedReader().readText()\n }\n\n // ============ Server酱 / PushPlus ============\n private val _serverchanKey = MutableStateFlow(\"\")\n val serverchanKey: StateFlow = _serverchanKey.asStateFlow()\n\n private val _pushplusToken = MutableStateFlow(\"\")\n val pushplusToken: StateFlow = _pushplusToken.asStateFlow()\n\n private val _pushTestResult = MutableStateFlow(null)\n val pushTestResult: StateFlow = _pushTestResult.asStateFlow()\n\n private val _isTestingPush = MutableStateFlow(false)\n val isTestingPush: StateFlow = _isTestingPush.asStateFlow()\n\n fun loadPushConfig() {\n _serverchanKey.value = securePrefs.getString(Constants.KEY_SERVERCHAN_KEY, \"\") ?: \"\"\n _pushplusToken.value = securePrefs.getString(Constants.KEY_PUSHPLUS_TOKEN, \"\") ?: \"\"\n }\n\n fun saveServerchanKey(key: String) {\n securePrefs.edit().putString(Constants.KEY_SERVERCHAN_KEY, key).apply()\n _serverchanKey.value = key\n _toastMessage.value = if (key.isNotBlank()) \"✅ Server酱 Key 已保存\" else \"✅ 已清除\"\n }\n\n fun savePushplusToken(token: String) {\n securePrefs.edit().putString(Constants.KEY_PUSHPLUS_TOKEN, token).apply()\n _pushplusToken.value = token\n _toastMessage.value = if (token.isNotBlank()) \"✅ PushPlus Token 已保存\" else \"✅ 已清除\"\n }\n\n fun testServerchan() {\n val key = _serverchanKey.value\n if (key.isBlank()) { _toastMessage.value = \"请先配置 Server酱 SendKey\"; return }\n _isTestingPush.value = true; _pushTestResult.value = null\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n val url = java.net.URL(\"https://sctapi.ftqq.com/$key.send\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"POST\"\n conn.doOutput = true\n conn.setRequestProperty(\"Content-Type\", \"application/json; charset=utf-8\")\n conn.connectTimeout = 10000\n conn.readTimeout = 10000\n val body = \"\"\"{\"title\":\"布老师App 推送测试\",\"desp\":\"如果你看到这条消息,说明推送配置成功了 🎉\"}\"\"\"\n conn.outputStream.write(body.toByteArray(Charsets.UTF_8))\n val resp = conn.inputStream.bufferedReader().readText()\n conn.disconnect()\n val json = JsonParser.parseString(resp).asJsonObject\n val code = json.get(\"code\")?.asInt\n _pushTestResult.value = if (code == 0) \"✅ 测试成功!\" else \"❌ 推送失败: ${json.get(\"message\")?.asString ?: resp}\"\n }\n } catch (e: Exception) {\n _pushTestResult.value = \"❌ 测试失败: ${e.message}\"\n } finally {\n _isTestingPush.value = false\n }\n }\n }\n\n fun testPushplus() {\n val token = _pushplusToken.value\n if (token.isBlank()) { _toastMessage.value = \"请先配置 PushPlus Token\"; return }\n _isTestingPush.value = true; _pushTestResult.value = null\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n val url = java.net.URL(\"https://www.pushplus.plus/send\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"POST\"\n conn.doOutput = true\n conn.setRequestProperty(\"Content-Type\", \"application/json; charset=utf-8\")\n conn.connectTimeout = 10000\n conn.readTimeout = 10000\n val body = \"\"\"{\"token\":\"$token\",\"title\":\"布老师App 推送测试\",\"content\":\"如果你看到这条消息,说明推送配置成功了 🎉\"}\"\"\"\n conn.outputStream.write(body.toByteArray(Charsets.UTF_8))\n val resp = conn.inputStream.bufferedReader().readText()\n conn.disconnect()\n val json = JsonParser.parseString(resp).asJsonObject\n val code = json.get(\"code\")?.asInt\n _pushTestResult.value = if (code == 200) \"✅ 测试成功!\" else \"❌ 推送失败: ${json.get(\"msg\")?.asString ?: resp}\"\n }\n } catch (e: Exception) {\n _pushTestResult.value = \"❌ 测试失败: ${e.message}\"\n } finally {\n _isTestingPush.value = false\n }\n }\n }\n\n fun clearPushTestResult() { _pushTestResult.value = null }\n\n // ============ 通用余额查询 ============\n data class BalanceCheckService(\n val id: String,\n val name: String,\n val baseUrl: String,\n val apiKey: String\n )\n\n private val _balanceServices = MutableStateFlow>(emptyList())\n val balanceServices: StateFlow> = _balanceServices.asStateFlow()\n\n private val _balanceResults = MutableStateFlow>(emptyMap())\n val balanceResults: StateFlow> = _balanceResults.asStateFlow()\n\n private val _isLoadingBalanceCheck = MutableStateFlow(false)\n val isLoadingBalanceCheck: StateFlow = _isLoadingBalanceCheck.asStateFlow()\n\n fun loadBalanceServices() {\n val json = securePrefs.getString(Constants.KEY_BALANCE_CHECK_LIST, \"[]\") ?: \"[]\"\n val type = object : TypeToken>() {}.type\n _balanceServices.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n }\n\n private fun saveBalanceServices(list: List) {\n securePrefs.edit().putString(Constants.KEY_BALANCE_CHECK_LIST, gson.toJson(list)).apply()\n _balanceServices.value = list\n }\n\n fun addBalanceService(name: String, baseUrl: String, apiKey: String) {\n val service = BalanceCheckService(\n id = java.util.UUID.randomUUID().toString(),\n name = name.trim(),\n baseUrl = baseUrl.trim().trimEnd('/'),\n apiKey = apiKey.trim()\n )\n val list = _balanceServices.value.toMutableList()\n list.add(service)\n saveBalanceServices(list)\n _toastMessage.value = \"✅ 已添加余额查询「${service.name}」\"\n // 添加后自动查询\n queryBalance(service.id)\n }\n\n fun deleteBalanceService(id: String) {\n val list = _balanceServices.value.toMutableList()\n list.removeAll { it.id == id }\n saveBalanceServices(list)\n val results = _balanceResults.value.toMutableMap()\n results.remove(id)\n _balanceResults.value = results\n _toastMessage.value = \"✅ 已删除\"\n }\n\n fun queryBalance(serviceId: String) {\n val service = _balanceServices.value.find { it.id == serviceId } ?: return\n _isLoadingBalanceCheck.value = true\n viewModelScope.launch {\n try {\n val result = withContext(Dispatchers.IO) {\n // 尝试多个常见余额接口路径\n val paths = listOf(\"/user/balance\", \"/v1/user/balance\", \"/dashboard/billing/credit_grants\")\n var lastError: String? = null\n for (path in paths) {\n try {\n val url = java.net.URL(\"${service.baseUrl}$path\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"GET\"\n conn.setRequestProperty(\"Authorization\", \"Bearer ${service.apiKey}\")\n conn.setRequestProperty(\"Accept\", \"application/json\")\n conn.connectTimeout = 8000\n conn.readTimeout = 8000\n val code = conn.responseCode\n if (code == 200) {\n val text = conn.inputStream.bufferedReader().readText()\n conn.disconnect()\n // 尝试解析常见格式\n return@withContext try {\n val json = JsonParser.parseString(text).asJsonObject\n // 格式1: DeepSeek → balance_infos[0].total_balance\n val balanceInfos = json.getAsJsonArray(\"balance_infos\")\n if (balanceInfos != null && balanceInfos.size() > 0) {\n \"¥${balanceInfos[0].asJsonObject.get(\"total_balance\")?.asString ?: \"?\"}\"\n } else {\n val data = json.getAsJsonObject(\"data\")\n when {\n data != null && data.get(\"balance\") != null -> \"¥${data.get(\"balance\")?.asString ?: data.get(\"balance\")?.asNumber ?: \"?\"}\"\n json.get(\"balance\") != null -> \"¥${json.get(\"balance\")?.asString ?: json.get(\"balance\")?.asNumber ?: \"?\"}\"\n json.get(\"total_balance\") != null -> \"¥${json.get(\"total_balance\")?.asString ?: \"?\"}\"\n else -> text.take(200)\n }\n }\n } catch (_: Exception) {\n text.take(200)\n }\n }\n conn.disconnect()\n lastError = \"HTTP $code\"\n } catch (_: Exception) {\n lastError = \"${_->message}\"\n }\n }\n \"❌ $lastError\"\n }\n val results = _balanceResults.value.toMutableMap()\n results[serviceId] = result\n _balanceResults.value = results\n } catch (e: Exception) {\n val results = _balanceResults.value.toMutableMap()\n results[serviceId] = \"❌ ${e.message}\"\n _balanceResults.value = results\n } finally {\n _isLoadingBalanceCheck.value = false\n }\n }\n }\n\n fun queryAllBalances() {\n _balanceServices.value.forEach { queryBalance(it.id) }\n }\n\n // ============ Agent 管理 ============\n private val _agentList = MutableStateFlow>(emptyList())\n val agentList: StateFlow> = _agentList.asStateFlow()\n\n private val _currentAgentId = MutableStateFlow(\"\")\n val currentAgentId: StateFlow = _currentAgentId.asStateFlow()\n\n private val _isCreatingAgent = MutableStateFlow(false)\n val isCreatingAgent: StateFlow = _isCreatingAgent.asStateFlow()\n\n // ============ Toast ============\n private val _toastMessage = MutableStateFlow(null)\n val toastMessage: StateFlow = _toastMessage.asStateFlow()\n\n companion object {\n private const val KEY_AGENTS = \"agent_list\"\n private const val KEY_CURRENT_AGENT = \"current_agent_id\"\n }\n\n init {\n loadConfig()\n loadTokenUsage()\n loadAgents()\n loadChannelAndDeepseek()\n loadCustomApiList()\n loadGitHubToken()\n loadHfToken()\n loadPushConfig()\n loadBalanceServices()\n }\n\n private fun loadChannelAndDeepseek() {\n val channel = securePrefs.getString(Constants.KEY_AI_CHANNEL, Constants.CHANNEL_COZE) ?: Constants.CHANNEL_COZE\n _currentChannel.value = channel\n val dsKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n _deepseekApiKey.value = dsKey\n val dsModel = securePrefs.getString(Constants.KEY_DEEPSEEK_MODEL, \"deepseek-v4-flash\") ?: \"deepseek-v4-flash\"\n _deepseekModel.value = dsModel\n }\n\n fun switchChannel(channel: String) {\n securePrefs.edit().putString(Constants.KEY_AI_CHANNEL, channel).apply()\n _currentChannel.value = channel\n\n val isCustom = channel.startsWith(Constants.CHANNEL_CUSTOM_PREFIX)\n if (isCustom) {\n cozeClient.currentBotId = \"\"\n val customId = channel.removePrefix(Constants.CHANNEL_CUSTOM_PREFIX)\n val config = getCustomApiById(customId)\n _toastMessage.value = \"✅ 已切换到「${config?.name ?: \"自定义\"}」\"\n } else {\n cozeClient.currentBotId = if (channel == Constants.CHANNEL_DEEPSEEK) {\n \"\"\n } else {\n val savedBotId = securePrefs.getString(\"current_agent_id\", \"\") ?: \"\"\n if (savedBotId.isNotBlank()) savedBotId\n else {\n viewModelScope.launch {\n val botId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n cozeClient.currentBotId = botId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n _cozeBotId.value = cozeClient.currentBotId\n }\n _cozeBotId.value\n }\n }\n _toastMessage.value = when (channel) {\n Constants.CHANNEL_DEEPSEEK -> \"✅ 已切换到 DeepSeek\"\n else -> \"✅ 已切换到 Coze\"\n }\n }\n }\n\n fun saveDeepseekApiKey(key: String) {\n securePrefs.edit().putString(Constants.KEY_DEEPSEEK_API_KEY, key).apply()\n _deepseekApiKey.value = key\n _toastMessage.value = if (key.isNotBlank()) \"✅ DeepSeek API Key 已保存\" else \"✅ 已清除 DeepSeek API Key\"\n }\n\n fun saveDeepseekModel(model: String) {\n securePrefs.edit().putString(Constants.KEY_DEEPSEEK_MODEL, model).apply()\n _deepseekModel.value = model\n _toastMessage.value = \"✅ 已切换模型: $model\"\n }\n\n fun queryDeepseekBalance() {\n val apiKey = _deepseekApiKey.value\n if (apiKey.isBlank()) {\n _toastMessage.value = \"请先配置 DeepSeek API Key\"\n return\n }\n _isLoadingBalance.value = true\n viewModelScope.launch {\n try {\n val result = withContext(Dispatchers.IO) {\n val url = java.net.URL(\"https://api.deepseek.com/user/balance\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"GET\"\n conn.setRequestProperty(\"Authorization\", \"Bearer $apiKey\")\n conn.setRequestProperty(\"Accept\", \"application/json\")\n conn.connectTimeout = 10000\n conn.readTimeout = 10000\n val response = conn.inputStream.bufferedReader().readText()\n conn.disconnect()\n val json = com.google.gson.JsonParser.parseString(response).asJsonObject\n val balanceInfos = json.getAsJsonArray(\"balance_infos\")\n val totalBalance = balanceInfos?.firstOrNull()?.asJsonObject?.get(\"total_balance\")?.asString\n if (totalBalance != null) \"¥$totalBalance\" else \"查询失败\"\n }\n _deepseekBalance.value = result\n } catch (e: Exception) {\n _deepseekBalance.value = \"查询失败: ${e.message ?: \"网络异常\"}\"\n } finally {\n _isLoadingBalance.value = false\n }\n }\n }\n\n private fun loadConfig() {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n if (savedPat.isNotBlank()) {\n _cozePat.value = savedPat\n _isUsingDefaultPat.value = false\n } else {\n _cozePat.value = Constants.DEFAULT_COZE_PAT\n _isUsingDefaultPat.value = true\n }\n viewModelScope.launch {\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n if (savedBotId.isNotBlank()) {\n _cozeBotId.value = savedBotId\n _isUsingDefaultBotId.value = false\n } else {\n _cozeBotId.value = Constants.DEFAULT_COZE_BOT_ID\n _isUsingDefaultBotId.value = true\n }\n }\n }\n\n private fun loadTokenUsage() {\n val map = mutableMapOf()\n var grandTotal = 0\n\n // 内置通道:Coze\n val cozeTotal = securePrefs.getInt(\"coze_total_tokens\", 0)\n map[\"coze\"] = ChannelTokenStats(\n total = cozeTotal,\n input = securePrefs.getInt(\"coze_total_input\", 0),\n output = securePrefs.getInt(\"coze_total_output\", 0),\n messages = securePrefs.getInt(\"coze_total_messages\", 0)\n )\n grandTotal += cozeTotal\n\n // 内置通道:DeepSeek\n val dsTotal = securePrefs.getInt(\"ds_total_tokens\", 0)\n map[\"deepseek\"] = ChannelTokenStats(\n total = dsTotal,\n input = securePrefs.getInt(\"ds_total_input\", 0),\n output = securePrefs.getInt(\"ds_total_output\", 0),\n messages = securePrefs.getInt(\"ds_total_messages\", 0)\n )\n grandTotal += dsTotal\n\n // 自定义 API 通道\n _customApiList.value.forEach { config ->\n val prefix = \"custom:${config.id}\"\n val total = securePrefs.getInt(\"${prefix}_total_tokens\", 0)\n if (total > 0 || securePrefs.contains(\"${prefix}_total_tokens\")) {\n map[prefix] = ChannelTokenStats(\n total = total,\n input = securePrefs.getInt(\"${prefix}_total_input\", 0),\n output = securePrefs.getInt(\"${prefix}_total_output\", 0),\n messages = securePrefs.getInt(\"${prefix}_total_messages\", 0)\n )\n grandTotal += total\n }\n }\n\n _channelTokenStats.value = map\n _totalTokens.value = grandTotal\n }\n\n fun refreshTokenUsage() {\n loadTokenUsage()\n }\n\n fun saveCozePat(pat: String) {\n securePrefs.edit().putString(Constants.KEY_COZE_PAT, pat).apply()\n _cozePat.value = pat\n _isUsingDefaultPat.value = false\n _toastMessage.value = if (pat.isNotBlank()) \"✅ PAT 已保存\" else \"✅ 已恢复默认 PAT\"\n if (pat.isBlank()) {\n _cozePat.value = Constants.DEFAULT_COZE_PAT\n _isUsingDefaultPat.value = true\n }\n }\n\n fun saveCozeBotId(botId: String) {\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, botId)\n _cozeBotId.value = botId\n _isUsingDefaultBotId.value = false\n _toastMessage.value = if (botId.isNotBlank()) \"✅ Bot ID 已保存\" else \"✅ 已恢复默认 Bot ID\"\n if (botId.isBlank()) {\n _cozeBotId.value = Constants.DEFAULT_COZE_BOT_ID\n _isUsingDefaultBotId.value = true\n }\n }\n }\n\n // ============ Agent 管理 ============\n\n private fun loadAgents() {\n val json = securePrefs.getString(KEY_AGENTS, \"[]\") ?: \"[]\"\n val type = object : TypeToken>() {}.type\n _agentList.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n _currentAgentId.value = securePrefs.getString(KEY_CURRENT_AGENT, \"\") ?: \"\"\n }\n\n private fun saveAgents(agents: List) {\n securePrefs.edit().putString(KEY_AGENTS, gson.toJson(agents)).apply()\n _agentList.value = agents\n }\n\n fun createAgent(name: String, prompt: String, emoji: String) {\n if (name.isBlank() || prompt.isBlank()) {\n _toastMessage.value = \"⚠️ 名称和提示词不能为空\"\n return\n }\n viewModelScope.launch {\n _isCreatingAgent.value = true\n _toastMessage.value = \"⏳ 正在创建 Agent...\"\n\n // 1. 通过 Coze API 创建 Bot\n val createResult = cozeClient.createBot(name, prompt)\n if (createResult.isFailure) {\n _toastMessage.value = \"❌ 创建失败: ${createResult.exceptionOrNull()?.message}\"\n _isCreatingAgent.value = false\n return@launch\n }\n val botId = createResult.getOrNull()!!\n\n // 2. 发布 Bot 到 API 渠道\n val publishResult = cozeClient.publishBot(botId)\n if (publishResult.isFailure) {\n _toastMessage.value = \"❌ 发布失败: ${publishResult.exceptionOrNull()?.message}\"\n _isCreatingAgent.value = false\n return@launch\n }\n\n // 3. 保存到本地\n val agent = AgentConfig(name = name, botId = botId, prompt = prompt, emoji = emoji)\n val currentList = _agentList.value.toMutableList()\n currentList.add(agent)\n saveAgents(currentList)\n\n // 4. 设为当前 Agent\n securePrefs.edit().putString(KEY_CURRENT_AGENT, botId).apply()\n _currentAgentId.value = botId\n cozeClient.currentBotId = botId\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, botId)\n _cozeBotId.value = botId\n _isUsingDefaultBotId.value = false\n\n _toastMessage.value = \"✅ Agent「$name」创建成功\"\n _isCreatingAgent.value = false\n }\n }\n\n fun switchAgent(agent: AgentConfig) {\n cozeClient.currentBotId = agent.botId\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, agent.botId)\n }\n securePrefs.edit().putString(KEY_CURRENT_AGENT, agent.botId).apply()\n _currentAgentId.value = agent.botId\n _cozeBotId.value = agent.botId\n _isUsingDefaultBotId.value = false\n _toastMessage.value = \"✅ 已切换到「${agent.name}」\"\n }\n\n fun deleteAgent(agent: AgentConfig) {\n val currentList = _agentList.value.toMutableList()\n currentList.removeAll { it.botId == agent.botId }\n saveAgents(currentList)\n\n // 如果删除的是当前Agent,切回默认\n if (_currentAgentId.value == agent.botId) {\n val defaultBotId = Constants.DEFAULT_COZE_BOT_ID\n cozeClient.currentBotId = defaultBotId\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, defaultBotId)\n }\n securePrefs.edit().putString(KEY_CURRENT_AGENT, \"\").apply()\n _currentAgentId.value = \"\"\n _cozeBotId.value = defaultBotId\n }\n _toastMessage.value = \"✅ 已删除「${agent.name}」\"\n }\n\n fun clearToast() { _toastMessage.value = null }\n}\n"}, "733b025309ac6826ee41372626804dda493a12db": {"t": 1785755786.2794178, "n": 40533, "h": "d1677f6ea79fec82", "c": "package com.meitu.generator.ui.settings\n\nimport android.content.SharedPreferences\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.meitu.generator.data.remote.CozeApiClient\nimport com.meitu.generator.repository.SettingsRepository\nimport com.meitu.generator.util.Constants\nimport com.google.gson.Gson\nimport com.google.gson.JsonParser\nimport com.google.gson.reflect.TypeToken\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport javax.inject.Inject\nimport javax.inject.Named\n\ndata class AgentConfig(\n val name: String,\n val botId: String,\n val prompt: String,\n val emoji: String = \"🤖\"\n)\n\n@HiltViewModel\nclass SettingsViewModel @Inject constructor(\n private val settingsRepo: SettingsRepository,\n private val cozeClient: CozeApiClient,\n @Named(\"securePrefs\") private val securePrefs: SharedPreferences\n) : ViewModel() {\n\n private val gson = Gson()\n\n // ============ Coze 配置 ============\n private val _cozePat = MutableStateFlow(\"\")\n val cozePat: StateFlow = _cozePat.asStateFlow()\n\n private val _cozeBotId = MutableStateFlow(\"\")\n val cozeBotId: StateFlow = _cozeBotId.asStateFlow()\n\n private val _isUsingDefaultPat = MutableStateFlow(true)\n val isUsingDefaultPat: StateFlow = _isUsingDefaultPat.asStateFlow()\n\n private val _isUsingDefaultBotId = MutableStateFlow(true)\n val isUsingDefaultBotId: StateFlow = _isUsingDefaultBotId.asStateFlow()\n\n // ============ Token 消耗统计(动态 Map) ============\n data class ChannelTokenStats(\n val total: Int = 0,\n val input: Int = 0,\n val output: Int = 0,\n val messages: Int = 0\n )\n\n private val _channelTokenStats = MutableStateFlow>(emptyMap())\n val channelTokenStats: StateFlow> = _channelTokenStats.asStateFlow()\n\n private val _totalTokens = MutableStateFlow(0)\n val totalTokens: StateFlow = _totalTokens.asStateFlow()\n\n /** 获取通道的显示名称 */\n fun getChannelDisplayName(channelKey: String): String {\n return when (channelKey) {\n \"coze\" -> \"🧠 Coze\"\n \"deepseek\" -> \"🔍 DeepSeek\"\n else -> {\n val id = channelKey.removePrefix(\"custom:\")\n _customApiList.value.find { it.id == id }?.let { \"${it.emoji} ${it.name}\" }\n ?: channelKey\n }\n }\n }\n\n // ============ AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow = _currentChannel.asStateFlow()\n\n private val _deepseekApiKey = MutableStateFlow(\"\")\n val deepseekApiKey: StateFlow = _deepseekApiKey.asStateFlow()\n\n // ============ DeepSeek 模型选择 ============\n private val _deepseekModel = MutableStateFlow(\"deepseek-v4-flash\")\n val deepseekModel: StateFlow = _deepseekModel.asStateFlow()\n\n // ============ DeepSeek 余额 ============\n private val _deepseekBalance = MutableStateFlow(null)\n val deepseekBalance: StateFlow = _deepseekBalance.asStateFlow()\n private val _isLoadingBalance = MutableStateFlow(false)\n val isLoadingBalance: StateFlow = _isLoadingBalance.asStateFlow()\n\n // ============ 自定义 API 通道 ============\n private val _customApiList = MutableStateFlow>(emptyList())\n val customApiList: StateFlow> = _customApiList.asStateFlow()\n\n private fun loadCustomApiList() {\n val json = securePrefs.getString(Constants.KEY_CUSTOM_API_LIST, \"[]\") ?: \"[]\"\n val type = object : TypeToken>() {}.type\n _customApiList.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n }\n\n private fun saveCustomApiList(list: List) {\n securePrefs.edit().putString(Constants.KEY_CUSTOM_API_LIST, gson.toJson(list)).apply()\n _customApiList.value = list\n }\n\n fun addCustomApi(config: CustomApiConfig) {\n val list = _customApiList.value.toMutableList()\n list.add(config)\n saveCustomApiList(list)\n _toastMessage.value = \"✅ 已添加自定义 API「${config.name}」\"\n }\n\n fun updateCustomApi(config: CustomApiConfig) {\n val list = _customApiList.value.toMutableList()\n val idx = list.indexOfFirst { it.id == config.id }\n if (idx >= 0) {\n list[idx] = config\n saveCustomApiList(list)\n _toastMessage.value = \"✅ 已更新「${config.name}」\"\n }\n }\n\n fun deleteCustomApi(id: String) {\n val list = _customApiList.value.toMutableList()\n list.removeAll { it.id == id }\n saveCustomApiList(list)\n val channel = _currentChannel.value\n if (channel == \"${Constants.CHANNEL_CUSTOM_PREFIX}$id\") {\n switchChannel(Constants.CHANNEL_COZE)\n }\n _toastMessage.value = \"✅ 已删除\"\n }\n\n fun getCustomApiById(id: String): CustomApiConfig? {\n return _customApiList.value.find { it.id == id }\n }\n\n // ============ GitHub 关联 ============\n data class GitHubUser(\n val login: String,\n val avatarUrl: String,\n val name: String,\n val publicRepos: Int,\n val followers: Int\n )\n\n data class GitHubRepo(\n val name: String,\n val fullName: String,\n val description: String,\n val language: String?,\n val stars: Int,\n val forks: Int,\n val updatedAt: String\n )\n\n data class GitHubCommit(\n val sha: String,\n val message: String,\n val author: String,\n val date: String\n )\n\n data class GitHubNotification(\n val title: String,\n val repoName: String,\n val type: String,\n val url: String,\n val updatedAt: String\n )\n\n private val _githubToken = MutableStateFlow(\"\")\n val githubToken: StateFlow = _githubToken.asStateFlow()\n\n private val _githubUser = MutableStateFlow(null)\n val githubUser: StateFlow = _githubUser.asStateFlow()\n\n private val _githubRepos = MutableStateFlow>(emptyList())\n val githubRepos: StateFlow> = _githubRepos.asStateFlow()\n\n private val _githubNotifications = MutableStateFlow>(emptyList())\n val githubNotifications: StateFlow> = _githubNotifications.asStateFlow()\n\n private val _selectedRepoCommits = MutableStateFlow>(emptyList())\n val selectedRepoCommits: StateFlow> = _selectedRepoCommits.asStateFlow()\n\n private val _isLoadingGitHub = MutableStateFlow(false)\n val isLoadingGitHub: StateFlow = _isLoadingGitHub.asStateFlow()\n\n private val _githubError = MutableStateFlow(null)\n val githubError: StateFlow = _githubError.asStateFlow()\n\n private fun loadGitHubToken() {\n _githubToken.value = securePrefs.getString(Constants.KEY_GITHUB_TOKEN, \"\") ?: \"\"\n }\n\n fun saveGitHubToken(token: String) {\n securePrefs.edit().putString(Constants.KEY_GITHUB_TOKEN, token).apply()\n _githubToken.value = token\n if (token.isNotBlank()) {\n refreshGitHubData()\n } else {\n _githubUser.value = null\n _githubRepos.value = emptyList()\n _githubNotifications.value = emptyList()\n }\n }\n\n fun refreshGitHubData() {\n val token = _githubToken.value\n if (token.isBlank()) return\n _isLoadingGitHub.value = true\n _githubError.value = null\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n // 获取用户信息\n val userResp = gitHubApiGet(\"/user\", token)\n val userJson = JsonParser.parseString(userResp).asJsonObject\n _githubUser.value = GitHubUser(\n login = userJson.get(\"login\")?.asString ?: \"\",\n avatarUrl = userJson.get(\"avatar_url\")?.asString ?: \"\",\n name = userJson.get(\"name\")?.asString ?: userJson.get(\"login\")?.asString ?: \"\",\n publicRepos = userJson.get(\"public_repos\")?.asInt ?: 0,\n followers = userJson.get(\"followers\")?.asInt ?: 0\n )\n\n // 获取仓库列表\n val reposResp = gitHubApiGet(\"/user/repos?sort=updated&per_page=20\", token)\n val reposArray = JsonParser.parseString(reposResp).asJsonArray\n _githubRepos.value = reposArray.map { repo ->\n val obj = repo.asJsonObject\n GitHubRepo(\n name = obj.get(\"name\")?.asString ?: \"\",\n fullName = obj.get(\"full_name\")?.asString ?: \"\",\n description = obj.get(\"description\")?.asString ?: \"\",\n language = obj.get(\"language\")?.asString,\n stars = obj.get(\"stargazers_count\")?.asInt ?: 0,\n forks = obj.get(\"forks_count\")?.asInt ?: 0,\n updatedAt = obj.get(\"updated_at\")?.asString?.take(10) ?: \"\"\n )\n }\n\n // 获取通知\n try {\n val notifResp = gitHubApiGet(\"/notifications?per_page=10\", token)\n val notifArray = JsonParser.parseString(notifResp).asJsonArray\n _githubNotifications.value = notifArray.map { n ->\n val obj = n.asJsonObject\n val subject = obj.getAsJsonObject(\"subject\")\n val repo = obj.getAsJsonObject(\"repository\")\n GitHubNotification(\n title = subject?.get(\"title\")?.asString ?: \"\",\n repoName = repo?.get(\"full_name\")?.asString ?: \"\",\n type = subject?.get(\"type\")?.asString ?: \"\",\n url = subject?.get(\"url\")?.asString ?: \"\",\n updatedAt = obj.get(\"updated_at\")?.asString?.take(10) ?: \"\"\n )\n }\n } catch (_: Exception) {\n _githubNotifications.value = emptyList()\n }\n }\n } catch (e: Exception) {\n _githubError.value = e.message ?: \"加载失败\"\n } finally {\n _isLoadingGitHub.value = false\n }\n }\n }\n\n fun loadRepoCommits(fullName: String) {\n val token = _githubToken.value\n if (token.isBlank()) return\n _isLoadingGitHub.value = true\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n val resp = gitHubApiGet(\"/repos/$fullName/commits?per_page=10\", token)\n val array = JsonParser.parseString(resp).asJsonArray\n _selectedRepoCommits.value = array.map { c ->\n val obj = c.asJsonObject\n val commit = obj.getAsJsonObject(\"commit\")\n val author = commit?.getAsJsonObject(\"author\")\n GitHubCommit(\n sha = obj.get(\"sha\")?.asString?.take(7) ?: \"\",\n message = commit?.get(\"message\")?.asString?.split(\"\\n\")?.firstOrNull() ?: \"\",\n author = author?.get(\"name\")?.asString ?: \"\",\n date = author?.get(\"date\")?.asString?.take(10) ?: \"\"\n )\n }\n }\n } catch (e: Exception) {\n _githubError.value = \"加载 commits 失败: ${e.message}\"\n } finally {\n _isLoadingGitHub.value = false\n }\n }\n }\n\n private fun gitHubApiGet(path: String, token: String): String {\n val url = java.net.URL(\"${Constants.GITHUB_API_BASE}$path\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"GET\"\n conn.setRequestProperty(\"Authorization\", \"Bearer $token\")\n conn.setRequestProperty(\"Accept\", \"application/vnd.github.v3+json\")\n conn.connectTimeout = 10000\n conn.readTimeout = 10000\n val code = conn.responseCode\n if (code != 200) {\n val err = conn.errorStream?.bufferedReader()?.readText() ?: \"HTTP $code\"\n throw Exception(\"GitHub API 错误 ($code): $err\")\n }\n return conn.inputStream.bufferedReader().readText()\n }\n\n // ============ HuggingFace ============\n data class HfModel(\n val modelId: String,\n val author: String,\n val pipelineTag: String,\n val downloads: Long,\n val likes: Int,\n val lastModified: String\n )\n\n private val _hfToken = MutableStateFlow(\"\")\n val hfToken: StateFlow = _hfToken.asStateFlow()\n\n private val _hfModels = MutableStateFlow>(emptyList())\n val hfModels: StateFlow> = _hfModels.asStateFlow()\n\n private val _isLoadingHf = MutableStateFlow(false)\n val isLoadingHf: StateFlow = _isLoadingHf.asStateFlow()\n\n private val _hfError = MutableStateFlow(null)\n val hfError: StateFlow = _hfError.asStateFlow()\n\n private val _hfTrendingTag = MutableStateFlow(\"text-generation\")\n val hfTrendingTag: StateFlow = _hfTrendingTag.asStateFlow()\n\n fun loadHfToken() {\n _hfToken.value = securePrefs.getString(Constants.KEY_HF_TOKEN, \"\") ?: \"\"\n }\n\n fun saveHfToken(token: String) {\n securePrefs.edit().putString(Constants.KEY_HF_TOKEN, token).apply()\n _hfToken.value = token\n _toastMessage.value = if (token.isNotBlank()) \"✅ HuggingFace Token 已保存\" else \"✅ 已清除\"\n }\n\n fun searchHfModels(query: String) {\n if (query.isBlank()) return\n _isLoadingHf.value = true\n _hfError.value = null\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n val encoded = java.net.URLEncoder.encode(query, \"UTF-8\")\n val resp = hfApiGet(\"/models?search=$encoded&sort=downloads&direction=-1&limit=20\")\n val array = JsonParser.parseString(resp).asJsonArray\n _hfModels.value = array.map { m ->\n val obj = m.asJsonObject\n HfModel(\n modelId = obj.get(\"modelId\")?.asString ?: obj.get(\"id\")?.asString ?: \"\",\n author = obj.get(\"author\")?.asString ?: obj.get(\"modelId\")?.asString?.split(\"/\")?.firstOrNull() ?: \"\",\n pipelineTag = obj.get(\"pipeline_tag\")?.asString ?: \"其他\",\n downloads = obj.get(\"downloads\")?.asLong ?: 0,\n likes = obj.get(\"likes\")?.asInt ?: 0,\n lastModified = obj.get(\"lastModified\")?.asString?.take(10) ?: \"\"\n )\n }\n }\n } catch (e: Exception) {\n _hfError.value = e.message ?: \"搜索失败\"\n _hfModels.value = emptyList()\n } finally {\n _isLoadingHf.value = false\n }\n }\n }\n\n fun loadHfTrending(tag: String? = null) {\n val pipelineTag = tag ?: _hfTrendingTag.value\n if (tag != null) _hfTrendingTag.value = tag\n _isLoadingHf.value = true\n _hfError.value = null\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n val resp = hfApiGet(\"/models?pipeline_tag=$pipelineTag&sort=downloads&direction=-1&limit=20\")\n val array = JsonParser.parseString(resp).asJsonArray\n _hfModels.value = array.map { m ->\n val obj = m.asJsonObject\n HfModel(\n modelId = obj.get(\"modelId\")?.asString ?: obj.get(\"id\")?.asString ?: \"\",\n author = obj.get(\"author\")?.asString ?: obj.get(\"modelId\")?.asString?.split(\"/\")?.firstOrNull() ?: \"\",\n pipelineTag = obj.get(\"pipeline_tag\")?.asString ?: \"其他\",\n downloads = obj.get(\"downloads\")?.asLong ?: 0,\n likes = obj.get(\"likes\")?.asInt ?: 0,\n lastModified = obj.get(\"lastModified\")?.asString?.take(10) ?: \"\"\n )\n }\n }\n } catch (e: Exception) {\n _hfError.value = e.message ?: \"加载失败\"\n _hfModels.value = emptyList()\n } finally {\n _isLoadingHf.value = false\n }\n }\n }\n\n private fun hfApiGet(path: String): String {\n val token = _hfToken.value\n val url = java.net.URL(\"${Constants.HF_API_BASE}$path\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"GET\"\n if (token.isNotBlank()) {\n conn.setRequestProperty(\"Authorization\", \"Bearer $token\")\n }\n conn.setRequestProperty(\"Accept\", \"application/json\")\n conn.connectTimeout = 10000\n conn.readTimeout = 10000\n val code = conn.responseCode\n if (code != 200) {\n val err = conn.errorStream?.bufferedReader()?.readText() ?: \"HTTP $code\"\n throw Exception(\"HuggingFace API 错误 ($code): $err\")\n }\n return conn.inputStream.bufferedReader().readText()\n }\n\n // ============ Server酱 / PushPlus ============\n private val _serverchanKey = MutableStateFlow(\"\")\n val serverchanKey: StateFlow = _serverchanKey.asStateFlow()\n\n private val _pushplusToken = MutableStateFlow(\"\")\n val pushplusToken: StateFlow = _pushplusToken.asStateFlow()\n\n private val _pushTestResult = MutableStateFlow(null)\n val pushTestResult: StateFlow = _pushTestResult.asStateFlow()\n\n private val _isTestingPush = MutableStateFlow(false)\n val isTestingPush: StateFlow = _isTestingPush.asStateFlow()\n\n fun loadPushConfig() {\n _serverchanKey.value = securePrefs.getString(Constants.KEY_SERVERCHAN_KEY, \"\") ?: \"\"\n _pushplusToken.value = securePrefs.getString(Constants.KEY_PUSHPLUS_TOKEN, \"\") ?: \"\"\n }\n\n fun saveServerchanKey(key: String) {\n securePrefs.edit().putString(Constants.KEY_SERVERCHAN_KEY, key).apply()\n _serverchanKey.value = key\n _toastMessage.value = if (key.isNotBlank()) \"✅ Server酱 Key 已保存\" else \"✅ 已清除\"\n }\n\n fun savePushplusToken(token: String) {\n securePrefs.edit().putString(Constants.KEY_PUSHPLUS_TOKEN, token).apply()\n _pushplusToken.value = token\n _toastMessage.value = if (token.isNotBlank()) \"✅ PushPlus Token 已保存\" else \"✅ 已清除\"\n }\n\n fun testServerchan() {\n val key = _serverchanKey.value\n if (key.isBlank()) { _toastMessage.value = \"请先配置 Server酱 SendKey\"; return }\n _isTestingPush.value = true; _pushTestResult.value = null\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n val url = java.net.URL(\"https://sctapi.ftqq.com/$key.send\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"POST\"\n conn.doOutput = true\n conn.setRequestProperty(\"Content-Type\", \"application/json; charset=utf-8\")\n conn.connectTimeout = 10000\n conn.readTimeout = 10000\n val body = \"\"\"{\"title\":\"布老师App 推送测试\",\"desp\":\"如果你看到这条消息,说明推送配置成功了 🎉\"}\"\"\"\n conn.outputStream.write(body.toByteArray(Charsets.UTF_8))\n val resp = conn.inputStream.bufferedReader().readText()\n conn.disconnect()\n val json = JsonParser.parseString(resp).asJsonObject\n val code = json.get(\"code\")?.asInt\n _pushTestResult.value = if (code == 0) \"✅ 测试成功!\" else \"❌ 推送失败: ${json.get(\"message\")?.asString ?: resp}\"\n }\n } catch (e: Exception) {\n _pushTestResult.value = \"❌ 测试失败: ${e.message}\"\n } finally {\n _isTestingPush.value = false\n }\n }\n }\n\n fun testPushplus() {\n val token = _pushplusToken.value\n if (token.isBlank()) { _toastMessage.value = \"请先配置 PushPlus Token\"; return }\n _isTestingPush.value = true; _pushTestResult.value = null\n viewModelScope.launch {\n try {\n withContext(Dispatchers.IO) {\n val url = java.net.URL(\"https://www.pushplus.plus/send\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"POST\"\n conn.doOutput = true\n conn.setRequestProperty(\"Content-Type\", \"application/json; charset=utf-8\")\n conn.connectTimeout = 10000\n conn.readTimeout = 10000\n val body = \"\"\"{\"token\":\"$token\",\"title\":\"布老师App 推送测试\",\"content\":\"如果你看到这条消息,说明推送配置成功了 🎉\"}\"\"\"\n conn.outputStream.write(body.toByteArray(Charsets.UTF_8))\n val resp = conn.inputStream.bufferedReader().readText()\n conn.disconnect()\n val json = JsonParser.parseString(resp).asJsonObject\n val code = json.get(\"code\")?.asInt\n _pushTestResult.value = if (code == 200) \"✅ 测试成功!\" else \"❌ 推送失败: ${json.get(\"msg\")?.asString ?: resp}\"\n }\n } catch (e: Exception) {\n _pushTestResult.value = \"❌ 测试失败: ${e.message}\"\n } finally {\n _isTestingPush.value = false\n }\n }\n }\n\n fun clearPushTestResult() { _pushTestResult.value = null }\n\n // ============ 通用余额查询 ============\n data class BalanceCheckService(\n val id: String,\n val name: String,\n val baseUrl: String,\n val apiKey: String\n )\n\n private val _balanceServices = MutableStateFlow>(emptyList())\n val balanceServices: StateFlow> = _balanceServices.asStateFlow()\n\n private val _balanceResults = MutableStateFlow>(emptyMap())\n val balanceResults: StateFlow> = _balanceResults.asStateFlow()\n\n private val _isLoadingBalanceCheck = MutableStateFlow(false)\n val isLoadingBalanceCheck: StateFlow = _isLoadingBalanceCheck.asStateFlow()\n\n fun loadBalanceServices() {\n val json = securePrefs.getString(Constants.KEY_BALANCE_CHECK_LIST, \"[]\") ?: \"[]\"\n val type = object : TypeToken>() {}.type\n _balanceServices.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n }\n\n private fun saveBalanceServices(list: List) {\n securePrefs.edit().putString(Constants.KEY_BALANCE_CHECK_LIST, gson.toJson(list)).apply()\n _balanceServices.value = list\n }\n\n fun addBalanceService(name: String, baseUrl: String, apiKey: String) {\n val service = BalanceCheckService(\n id = java.util.UUID.randomUUID().toString(),\n name = name.trim(),\n baseUrl = baseUrl.trim().trimEnd('/'),\n apiKey = apiKey.trim()\n )\n val list = _balanceServices.value.toMutableList()\n list.add(service)\n saveBalanceServices(list)\n _toastMessage.value = \"✅ 已添加余额查询「${service.name}」\"\n // 添加后自动查询\n queryBalance(service.id)\n }\n\n fun deleteBalanceService(id: String) {\n val list = _balanceServices.value.toMutableList()\n list.removeAll { it.id == id }\n saveBalanceServices(list)\n val results = _balanceResults.value.toMutableMap()\n results.remove(id)\n _balanceResults.value = results\n _toastMessage.value = \"✅ 已删除\"\n }\n\n fun queryBalance(serviceId: String) {\n val service = _balanceServices.value.find { it.id == serviceId } ?: return\n _isLoadingBalanceCheck.value = true\n viewModelScope.launch {\n try {\n val result = withContext(Dispatchers.IO) {\n // 尝试多个常见余额接口路径\n val paths = listOf(\"/user/balance\", \"/v1/user/balance\", \"/dashboard/billing/credit_grants\")\n var lastError: String? = null\n for (path in paths) {\n try {\n val url = java.net.URL(\"${service.baseUrl}$path\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"GET\"\n conn.setRequestProperty(\"Authorization\", \"Bearer ${service.apiKey}\")\n conn.setRequestProperty(\"Accept\", \"application/json\")\n conn.connectTimeout = 8000\n conn.readTimeout = 8000\n val code = conn.responseCode\n if (code == 200) {\n val text = conn.inputStream.bufferedReader().readText()\n conn.disconnect()\n // 尝试解析常见格式\n return@withContext try {\n val json = JsonParser.parseString(text).asJsonObject\n // 格式1: DeepSeek → balance_infos[0].total_balance\n val balanceInfos = json.getAsJsonArray(\"balance_infos\")\n if (balanceInfos != null && balanceInfos.size() > 0) {\n \"¥${balanceInfos[0].asJsonObject.get(\"total_balance\")?.asString ?: \"?\"}\"\n }\n // 格式2: 硅基流动 → data.balance\n else {\n val data = json.getAsJsonObject(\"data\")\n if (data != null && data.get(\"balance\") != null) {\n \"¥${data.get(\"balance\")?.asString ?: data.get(\"balance\")?.asNumber ?: \"?\"}\"\n }\n // 格式3: 直接返回 balance 字段\n else if (json.get(\"balance\") != null) {\n \"¥${json.get(\"balance\")?.asString ?: json.get(\"balance\")?.asNumber ?: \"?\"}\"\n }\n // 格式4: total_balance 在顶层\n else if (json.get(\"total_balance\") != null) {\n \"¥${json.get(\"total_balance\")?.asString ?: \"?\"}\"\n }\n // 格式5: 显示原始JSON\n else {\n text.take(200)\n }\n }\n } catch (_: Exception) {\n text.take(200)\n }\n }\n conn.disconnect()\n lastError = \"HTTP $code\"\n } catch (_: Exception) {\n lastError = \"${_->message}\"\n }\n }\n \"❌ $lastError\"\n }\n val results = _balanceResults.value.toMutableMap()\n results[serviceId] = result\n _balanceResults.value = results\n } catch (e: Exception) {\n val results = _balanceResults.value.toMutableMap()\n results[serviceId] = \"❌ ${e.message}\"\n _balanceResults.value = results\n } finally {\n _isLoadingBalanceCheck.value = false\n }\n }\n }\n\n fun queryAllBalances() {\n _balanceServices.value.forEach { queryBalance(it.id) }\n }\n\n // ============ Agent 管理 ============\n private val _agentList = MutableStateFlow>(emptyList())\n val agentList: StateFlow> = _agentList.asStateFlow()\n\n private val _currentAgentId = MutableStateFlow(\"\")\n val currentAgentId: StateFlow = _currentAgentId.asStateFlow()\n\n private val _isCreatingAgent = MutableStateFlow(false)\n val isCreatingAgent: StateFlow = _isCreatingAgent.asStateFlow()\n\n // ============ Toast ============\n private val _toastMessage = MutableStateFlow(null)\n val toastMessage: StateFlow = _toastMessage.asStateFlow()\n\n companion object {\n private const val KEY_AGENTS = \"agent_list\"\n private const val KEY_CURRENT_AGENT = \"current_agent_id\"\n }\n\n init {\n loadConfig()\n loadTokenUsage()\n loadAgents()\n loadChannelAndDeepseek()\n loadCustomApiList()\n loadGitHubToken()\n loadHfToken()\n loadPushConfig()\n loadBalanceServices()\n }\n\n private fun loadChannelAndDeepseek() {\n val channel = securePrefs.getString(Constants.KEY_AI_CHANNEL, Constants.CHANNEL_COZE) ?: Constants.CHANNEL_COZE\n _currentChannel.value = channel\n val dsKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n _deepseekApiKey.value = dsKey\n val dsModel = securePrefs.getString(Constants.KEY_DEEPSEEK_MODEL, \"deepseek-v4-flash\") ?: \"deepseek-v4-flash\"\n _deepseekModel.value = dsModel\n }\n\n fun switchChannel(channel: String) {\n securePrefs.edit().putString(Constants.KEY_AI_CHANNEL, channel).apply()\n _currentChannel.value = channel\n\n val isCustom = channel.startsWith(Constants.CHANNEL_CUSTOM_PREFIX)\n if (isCustom) {\n cozeClient.currentBotId = \"\"\n val customId = channel.removePrefix(Constants.CHANNEL_CUSTOM_PREFIX)\n val config = getCustomApiById(customId)\n _toastMessage.value = \"✅ 已切换到「${config?.name ?: \"自定义\"}」\"\n } else {\n cozeClient.currentBotId = if (channel == Constants.CHANNEL_DEEPSEEK) {\n \"\"\n } else {\n val savedBotId = securePrefs.getString(\"current_agent_id\", \"\") ?: \"\"\n if (savedBotId.isNotBlank()) savedBotId\n else {\n viewModelScope.launch {\n val botId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n cozeClient.currentBotId = botId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n _cozeBotId.value = cozeClient.currentBotId\n }\n _cozeBotId.value\n }\n }\n _toastMessage.value = when (channel) {\n Constants.CHANNEL_DEEPSEEK -> \"✅ 已切换到 DeepSeek\"\n else -> \"✅ 已切换到 Coze\"\n }\n }\n }\n\n fun saveDeepseekApiKey(key: String) {\n securePrefs.edit().putString(Constants.KEY_DEEPSEEK_API_KEY, key).apply()\n _deepseekApiKey.value = key\n _toastMessage.value = if (key.isNotBlank()) \"✅ DeepSeek API Key 已保存\" else \"✅ 已清除 DeepSeek API Key\"\n }\n\n fun saveDeepseekModel(model: String) {\n securePrefs.edit().putString(Constants.KEY_DEEPSEEK_MODEL, model).apply()\n _deepseekModel.value = model\n _toastMessage.value = \"✅ 已切换模型: $model\"\n }\n\n fun queryDeepseekBalance() {\n val apiKey = _deepseekApiKey.value\n if (apiKey.isBlank()) {\n _toastMessage.value = \"请先配置 DeepSeek API Key\"\n return\n }\n _isLoadingBalance.value = true\n viewModelScope.launch {\n try {\n val result = withContext(Dispatchers.IO) {\n val url = java.net.URL(\"https://api.deepseek.com/user/balance\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"GET\"\n conn.setRequestProperty(\"Authorization\", \"Bearer $apiKey\")\n conn.setRequestProperty(\"Accept\", \"application/json\")\n conn.connectTimeout = 10000\n conn.readTimeout = 10000\n val response = conn.inputStream.bufferedReader().readText()\n conn.disconnect()\n val json = com.google.gson.JsonParser.parseString(response).asJsonObject\n val balanceInfos = json.getAsJsonArray(\"balance_infos\")\n val totalBalance = balanceInfos?.firstOrNull()?.asJsonObject?.get(\"total_balance\")?.asString\n if (totalBalance != null) \"¥$totalBalance\" else \"查询失败\"\n }\n _deepseekBalance.value = result\n } catch (e: Exception) {\n _deepseekBalance.value = \"查询失败: ${e.message ?: \"网络异常\"}\"\n } finally {\n _isLoadingBalance.value = false\n }\n }\n }\n\n private fun loadConfig() {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n if (savedPat.isNotBlank()) {\n _cozePat.value = savedPat\n _isUsingDefaultPat.value = false\n } else {\n _cozePat.value = Constants.DEFAULT_COZE_PAT\n _isUsingDefaultPat.value = true\n }\n viewModelScope.launch {\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n if (savedBotId.isNotBlank()) {\n _cozeBotId.value = savedBotId\n _isUsingDefaultBotId.value = false\n } else {\n _cozeBotId.value = Constants.DEFAULT_COZE_BOT_ID\n _isUsingDefaultBotId.value = true\n }\n }\n }\n\n private fun loadTokenUsage() {\n val map = mutableMapOf()\n var grandTotal = 0\n\n // 内置通道:Coze\n val cozeTotal = securePrefs.getInt(\"coze_total_tokens\", 0)\n map[\"coze\"] = ChannelTokenStats(\n total = cozeTotal,\n input = securePrefs.getInt(\"coze_total_input\", 0),\n output = securePrefs.getInt(\"coze_total_output\", 0),\n messages = securePrefs.getInt(\"coze_total_messages\", 0)\n )\n grandTotal += cozeTotal\n\n // 内置通道:DeepSeek\n val dsTotal = securePrefs.getInt(\"ds_total_tokens\", 0)\n map[\"deepseek\"] = ChannelTokenStats(\n total = dsTotal,\n input = securePrefs.getInt(\"ds_total_input\", 0),\n output = securePrefs.getInt(\"ds_total_output\", 0),\n messages = securePrefs.getInt(\"ds_total_messages\", 0)\n )\n grandTotal += dsTotal\n\n // 自定义 API 通道\n _customApiList.value.forEach { config ->\n val prefix = \"custom:${config.id}\"\n val total = securePrefs.getInt(\"${prefix}_total_tokens\", 0)\n if (total > 0 || securePrefs.contains(\"${prefix}_total_tokens\")) {\n map[prefix] = ChannelTokenStats(\n total = total,\n input = securePrefs.getInt(\"${prefix}_total_input\", 0),\n output = securePrefs.getInt(\"${prefix}_total_output\", 0),\n messages = securePrefs.getInt(\"${prefix}_total_messages\", 0)\n )\n grandTotal += total\n }\n }\n\n _channelTokenStats.value = map\n _totalTokens.value = grandTotal\n }\n\n fun refreshTokenUsage() {\n loadTokenUsage()\n }\n\n fun saveCozePat(pat: String) {\n securePrefs.edit().putString(Constants.KEY_COZE_PAT, pat).apply()\n _cozePat.value = pat\n _isUsingDefaultPat.value = false\n _toastMessage.value = if (pat.isNotBlank()) \"✅ PAT 已保存\" else \"✅ 已恢复默认 PAT\"\n if (pat.isBlank()) {\n _cozePat.value = Constants.DEFAULT_COZE_PAT\n _isUsingDefaultPat.value = true\n }\n }\n\n fun saveCozeBotId(botId: String) {\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, botId)\n _cozeBotId.value = botId\n _isUsingDefaultBotId.value = false\n _toastMessage.value = if (botId.isNotBlank()) \"✅ Bot ID 已保存\" else \"✅ 已恢复默认 Bot ID\"\n if (botId.isBlank()) {\n _cozeBotId.value = Constants.DEFAULT_COZE_BOT_ID\n _isUsingDefaultBotId.value = true\n }\n }\n }\n\n // ============ Agent 管理 ============\n\n private fun loadAgents() {\n val json = securePrefs.getString(KEY_AGENTS, \"[]\") ?: \"[]\"\n val type = object : TypeToken>() {}.type\n _agentList.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n _currentAgentId.value = securePrefs.getString(KEY_CURRENT_AGENT, \"\") ?: \"\"\n }\n\n private fun saveAgents(agents: List) {\n securePrefs.edit().putString(KEY_AGENTS, gson.toJson(agents)).apply()\n _agentList.value = agents\n }\n\n fun createAgent(name: String, prompt: String, emoji: String) {\n if (name.isBlank() || prompt.isBlank()) {\n _toastMessage.value = \"⚠️ 名称和提示词不能为空\"\n return\n }\n viewModelScope.launch {\n _isCreatingAgent.value = true\n _toastMessage.value = \"⏳ 正在创建 Agent...\"\n\n // 1. 通过 Coze API 创建 Bot\n val createResult = cozeClient.createBot(name, prompt)\n if (createResult.isFailure) {\n _toastMessage.value = \"❌ 创建失败: ${createResult.exceptionOrNull()?.message}\"\n _isCreatingAgent.value = false\n return@launch\n }\n val botId = createResult.getOrNull()!!\n\n // 2. 发布 Bot 到 API 渠道\n val publishResult = cozeClient.publishBot(botId)\n if (publishResult.isFailure) {\n _toastMessage.value = \"❌ 发布失败: ${publishResult.exceptionOrNull()?.message}\"\n _isCreatingAgent.value = false\n return@launch\n }\n\n // 3. 保存到本地\n val agent = AgentConfig(name = name, botId = botId, prompt = prompt, emoji = emoji)\n val currentList = _agentList.value.toMutableList()\n currentList.add(agent)\n saveAgents(currentList)\n\n // 4. 设为当前 Agent\n securePrefs.edit().putString(KEY_CURRENT_AGENT, botId).apply()\n _currentAgentId.value = botId\n cozeClient.currentBotId = botId\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, botId)\n _cozeBotId.value = botId\n _isUsingDefaultBotId.value = false\n\n _toastMessage.value = \"✅ Agent「$name」创建成功\"\n _isCreatingAgent.value = false\n }\n }\n\n fun switchAgent(agent: AgentConfig) {\n cozeClient.currentBotId = agent.botId\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, agent.botId)\n }\n securePrefs.edit().putString(KEY_CURRENT_AGENT, agent.botId).apply()\n _currentAgentId.value = agent.botId\n _cozeBotId.value = agent.botId\n _isUsingDefaultBotId.value = false\n _toastMessage.value = \"✅ 已切换到「${agent.name}」\"\n }\n\n fun deleteAgent(agent: AgentConfig) {\n val currentList = _agentList.value.toMutableList()\n currentList.removeAll { it.botId == agent.botId }\n saveAgents(currentList)\n\n // 如果删除的是当前Agent,切回默认\n if (_currentAgentId.value == agent.botId) {\n val defaultBotId = Constants.DEFAULT_COZE_BOT_ID\n cozeClient.currentBotId = defaultBotId\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, defaultBotId)\n }\n securePrefs.edit().putString(KEY_CURRENT_AGENT, \"\").apply()\n _currentAgentId.value = \"\"\n _cozeBotId.value = defaultBotId\n }\n _toastMessage.value = \"✅ 已删除「${agent.name}」\"\n }\n\n fun clearToast() { _toastMessage.value = null }\n}\n"}, "3e56eb8b9b2355c143f61f3f1aeefb63704a8e2f": {"t": 1785755787.6409733, "n": 16995, "h": "5769b051104df34a", "c": "package com.meitu.generator.ui.settings\n\nimport android.content.SharedPreferences\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.meitu.generator.data.remote.CozeApiClient\nimport com.meitu.generator.repository.SettingsRepository\nimport com.meitu.generator.util.Constants\nimport com.google.gson.Gson\nimport com.google.gson.reflect.TypeToken\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport javax.inject.Inject\nimport javax.inject.Named\n\ndata class AgentConfig(\n val name: String,\n val botId: String,\n val prompt: String,\n val emoji: String = \"🤖\"\n)\n\n@HiltViewModel\nclass SettingsViewModel @Inject constructor(\n private val settingsRepo: SettingsRepository,\n private val cozeClient: CozeApiClient,\n @Named(\"securePrefs\") private val securePrefs: SharedPreferences\n) : ViewModel() {\n\n private val gson = Gson()\n\n // ============ Coze 配置 ============\n private val _cozePat = MutableStateFlow(\"\")\n val cozePat: StateFlow = _cozePat.asStateFlow()\n\n private val _cozeBotId = MutableStateFlow(\"\")\n val cozeBotId: StateFlow = _cozeBotId.asStateFlow()\n\n private val _isUsingDefaultPat = MutableStateFlow(true)\n val isUsingDefaultPat: StateFlow = _isUsingDefaultPat.asStateFlow()\n\n private val _isUsingDefaultBotId = MutableStateFlow(true)\n val isUsingDefaultBotId: StateFlow = _isUsingDefaultBotId.asStateFlow()\n\n // ============ Token 消耗统计(动态 Map) ============\n data class ChannelTokenStats(\n val total: Int = 0,\n val input: Int = 0,\n val output: Int = 0,\n val messages: Int = 0\n )\n\n private val _channelTokenStats = MutableStateFlow>(emptyMap())\n val channelTokenStats: StateFlow> = _channelTokenStats.asStateFlow()\n\n private val _totalTokens = MutableStateFlow(0)\n val totalTokens: StateFlow = _totalTokens.asStateFlow()\n\n /** 获取通道的显示名称 */\n fun getChannelDisplayName(channelKey: String): String {\n return when (channelKey) {\n \"coze\" -> \"🧠 Coze\"\n \"deepseek\" -> \"🔍 DeepSeek\"\n else -> {\n val id = channelKey.removePrefix(\"custom:\")\n _customApiList.value.find { it.id == id }?.let { \"${it.emoji} ${it.name}\" }\n ?: channelKey\n }\n }\n }\n\n // ============ AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow = _currentChannel.asStateFlow()\n\n private val _deepseekApiKey = MutableStateFlow(\"\")\n val deepseekApiKey: StateFlow = _deepseekApiKey.asStateFlow()\n\n // ============ DeepSeek 模型选择 ============\n private val _deepseekModel = MutableStateFlow(\"deepseek-v4-flash\")\n val deepseekModel: StateFlow = _deepseekModel.asStateFlow()\n\n // ============ DeepSeek 余额 ============\n private val _deepseekBalance = MutableStateFlow(null)\n val deepseekBalance: StateFlow = _deepseekBalance.asStateFlow()\n private val _isLoadingBalance = MutableStateFlow(false)\n val isLoadingBalance: StateFlow = _isLoadingBalance.asStateFlow()\n\n // ============ 自定义 API 通道 ============\n private val _customApiList = MutableStateFlow>(emptyList())\n val customApiList: StateFlow> = _customApiList.asStateFlow()\n\n private fun loadCustomApiList() {\n val json = securePrefs.getString(Constants.KEY_CUSTOM_API_LIST, \"[]\") ?: \"[]\"\n val type = object : TypeToken>() {}.type\n _customApiList.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n }\n\n private fun saveCustomApiList(list: List) {\n securePrefs.edit().putString(Constants.KEY_CUSTOM_API_LIST, gson.toJson(list)).apply()\n _customApiList.value = list\n }\n\n fun addCustomApi(config: CustomApiConfig) {\n val list = _customApiList.value.toMutableList()\n list.add(config)\n saveCustomApiList(list)\n _toastMessage.value = \"✅ 已添加自定义 API「${config.name}」\"\n }\n\n fun updateCustomApi(config: CustomApiConfig) {\n val list = _customApiList.value.toMutableList()\n val idx = list.indexOfFirst { it.id == config.id }\n if (idx >= 0) {\n list[idx] = config\n saveCustomApiList(list)\n _toastMessage.value = \"✅ 已更新「${config.name}」\"\n }\n }\n\n fun deleteCustomApi(id: String) {\n val list = _customApiList.value.toMutableList()\n list.removeAll { it.id == id }\n saveCustomApiList(list)\n val channel = _currentChannel.value\n if (channel == \"${Constants.CHANNEL_CUSTOM_PREFIX}$id\") {\n switchChannel(Constants.CHANNEL_COZE)\n }\n _toastMessage.value = \"✅ 已删除\"\n }\n\n fun getCustomApiById(id: String): CustomApiConfig? {\n return _customApiList.value.find { it.id == id }\n }\n\n // ============ Agent 管理 ============\n private val _agentList = MutableStateFlow>(emptyList())\n val agentList: StateFlow> = _agentList.asStateFlow()\n\n private val _currentAgentId = MutableStateFlow(\"\")\n val currentAgentId: StateFlow = _currentAgentId.asStateFlow()\n\n private val _isCreatingAgent = MutableStateFlow(false)\n val isCreatingAgent: StateFlow = _isCreatingAgent.asStateFlow()\n\n // ============ Toast ============\n private val _toastMessage = MutableStateFlow(null)\n val toastMessage: StateFlow = _toastMessage.asStateFlow()\n\n companion object {\n private const val KEY_AGENTS = \"agent_list\"\n private const val KEY_CURRENT_AGENT = \"current_agent_id\"\n }\n\n init {\n loadConfig()\n loadTokenUsage()\n loadAgents()\n loadChannelAndDeepseek()\n loadCustomApiList()\n }\n\n private fun loadChannelAndDeepseek() {\n val channel = securePrefs.getString(Constants.KEY_AI_CHANNEL, Constants.CHANNEL_COZE) ?: Constants.CHANNEL_COZE\n _currentChannel.value = channel\n val dsKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n _deepseekApiKey.value = dsKey\n val dsModel = securePrefs.getString(Constants.KEY_DEEPSEEK_MODEL, \"deepseek-v4-flash\") ?: \"deepseek-v4-flash\"\n _deepseekModel.value = dsModel\n }\n\n fun switchChannel(channel: String) {\n securePrefs.edit().putString(Constants.KEY_AI_CHANNEL, channel).apply()\n _currentChannel.value = channel\n\n val isCustom = channel.startsWith(Constants.CHANNEL_CUSTOM_PREFIX)\n if (isCustom) {\n cozeClient.currentBotId = \"\"\n val customId = channel.removePrefix(Constants.CHANNEL_CUSTOM_PREFIX)\n val config = getCustomApiById(customId)\n _toastMessage.value = \"✅ 已切换到「${config?.name ?: \"自定义\"}」\"\n } else {\n cozeClient.currentBotId = if (channel == Constants.CHANNEL_DEEPSEEK) {\n \"\"\n } else {\n val savedBotId = securePrefs.getString(\"current_agent_id\", \"\") ?: \"\"\n if (savedBotId.isNotBlank()) savedBotId\n else {\n viewModelScope.launch {\n val botId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n cozeClient.currentBotId = botId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n _cozeBotId.value = cozeClient.currentBotId\n }\n _cozeBotId.value\n }\n }\n _toastMessage.value = when (channel) {\n Constants.CHANNEL_DEEPSEEK -> \"✅ 已切换到 DeepSeek\"\n else -> \"✅ 已切换到 Coze\"\n }\n }\n }\n\n fun saveDeepseekApiKey(key: String) {\n securePrefs.edit().putString(Constants.KEY_DEEPSEEK_API_KEY, key).apply()\n _deepseekApiKey.value = key\n _toastMessage.value = if (key.isNotBlank()) \"✅ DeepSeek API Key 已保存\" else \"✅ 已清除 DeepSeek API Key\"\n }\n\n fun saveDeepseekModel(model: String) {\n securePrefs.edit().putString(Constants.KEY_DEEPSEEK_MODEL, model).apply()\n _deepseekModel.value = model\n _toastMessage.value = \"✅ 已切换模型: $model\"\n }\n\n fun queryDeepseekBalance() {\n val apiKey = _deepseekApiKey.value\n if (apiKey.isBlank()) {\n _toastMessage.value = \"请先配置 DeepSeek API Key\"\n return\n }\n _isLoadingBalance.value = true\n viewModelScope.launch {\n try {\n val result = withContext(Dispatchers.IO) {\n val url = java.net.URL(\"https://api.deepseek.com/user/balance\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"GET\"\n conn.setRequestProperty(\"Authorization\", \"Bearer $apiKey\")\n conn.setRequestProperty(\"Accept\", \"application/json\")\n conn.connectTimeout = 10000\n conn.readTimeout = 10000\n val response = conn.inputStream.bufferedReader().readText()\n conn.disconnect()\n val json = com.google.gson.JsonParser.parseString(response).asJsonObject\n val balanceInfos = json.getAsJsonArray(\"balance_infos\")\n val totalBalance = balanceInfos?.firstOrNull()?.asJsonObject?.get(\"total_balance\")?.asString\n if (totalBalance != null) \"¥$totalBalance\" else \"查询失败\"\n }\n _deepseekBalance.value = result\n } catch (e: Exception) {\n _deepseekBalance.value = \"查询失败: ${e.message ?: \"网络异常\"}\"\n } finally {\n _isLoadingBalance.value = false\n }\n }\n }\n\n private fun loadConfig() {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n if (savedPat.isNotBlank()) {\n _cozePat.value = savedPat\n _isUsingDefaultPat.value = false\n } else {\n _cozePat.value = Constants.DEFAULT_COZE_PAT\n _isUsingDefaultPat.value = true\n }\n viewModelScope.launch {\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n if (savedBotId.isNotBlank()) {\n _cozeBotId.value = savedBotId\n _isUsingDefaultBotId.value = false\n } else {\n _cozeBotId.value = Constants.DEFAULT_COZE_BOT_ID\n _isUsingDefaultBotId.value = true\n }\n }\n }\n\n private fun loadTokenUsage() {\n val map = mutableMapOf()\n var grandTotal = 0\n\n // 内置通道:Coze\n val cozeTotal = securePrefs.getInt(\"coze_total_tokens\", 0)\n map[\"coze\"] = ChannelTokenStats(\n total = cozeTotal,\n input = securePrefs.getInt(\"coze_total_input\", 0),\n output = securePrefs.getInt(\"coze_total_output\", 0),\n messages = securePrefs.getInt(\"coze_total_messages\", 0)\n )\n grandTotal += cozeTotal\n\n // 内置通道:DeepSeek\n val dsTotal = securePrefs.getInt(\"ds_total_tokens\", 0)\n map[\"deepseek\"] = ChannelTokenStats(\n total = dsTotal,\n input = securePrefs.getInt(\"ds_total_input\", 0),\n output = securePrefs.getInt(\"ds_total_output\", 0),\n messages = securePrefs.getInt(\"ds_total_messages\", 0)\n )\n grandTotal += dsTotal\n\n // 自定义 API 通道\n _customApiList.value.forEach { config ->\n val prefix = \"custom:${config.id}\"\n val total = securePrefs.getInt(\"${prefix}_total_tokens\", 0)\n if (total > 0 || securePrefs.contains(\"${prefix}_total_tokens\")) {\n map[prefix] = ChannelTokenStats(\n total = total,\n input = securePrefs.getInt(\"${prefix}_total_input\", 0),\n output = securePrefs.getInt(\"${prefix}_total_output\", 0),\n messages = securePrefs.getInt(\"${prefix}_total_messages\", 0)\n )\n grandTotal += total\n }\n }\n\n _channelTokenStats.value = map\n _totalTokens.value = grandTotal\n }\n\n fun refreshTokenUsage() {\n loadTokenUsage()\n }\n\n fun saveCozePat(pat: String) {\n securePrefs.edit().putString(Constants.KEY_COZE_PAT, pat).apply()\n _cozePat.value = pat\n _isUsingDefaultPat.value = false\n _toastMessage.value = if (pat.isNotBlank()) \"✅ PAT 已保存\" else \"✅ 已恢复默认 PAT\"\n if (pat.isBlank()) {\n _cozePat.value = Constants.DEFAULT_COZE_PAT\n _isUsingDefaultPat.value = true\n }\n }\n\n fun saveCozeBotId(botId: String) {\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, botId)\n _cozeBotId.value = botId\n _isUsingDefaultBotId.value = false\n _toastMessage.value = if (botId.isNotBlank()) \"✅ Bot ID 已保存\" else \"✅ 已恢复默认 Bot ID\"\n if (botId.isBlank()) {\n _cozeBotId.value = Constants.DEFAULT_COZE_BOT_ID\n _isUsingDefaultBotId.value = true\n }\n }\n }\n\n // ============ Agent 管理 ============\n\n private fun loadAgents() {\n val json = securePrefs.getString(KEY_AGENTS, \"[]\") ?: \"[]\"\n val type = object : TypeToken>() {}.type\n _agentList.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n _currentAgentId.value = securePrefs.getString(KEY_CURRENT_AGENT, \"\") ?: \"\"\n }\n\n private fun saveAgents(agents: List) {\n securePrefs.edit().putString(KEY_AGENTS, gson.toJson(agents)).apply()\n _agentList.value = agents\n }\n\n fun createAgent(name: String, prompt: String, emoji: String) {\n if (name.isBlank() || prompt.isBlank()) {\n _toastMessage.value = \"⚠️ 名称和提示词不能为空\"\n return\n }\n viewModelScope.launch {\n _isCreatingAgent.value = true\n _toastMessage.value = \"⏳ 正在创建 Agent...\"\n\n // 1. 通过 Coze API 创建 Bot\n val createResult = cozeClient.createBot(name, prompt)\n if (createResult.isFailure) {\n _toastMessage.value = \"❌ 创建失败: ${createResult.exceptionOrNull()?.message}\"\n _isCreatingAgent.value = false\n return@launch\n }\n val botId = createResult.getOrNull()!!\n\n // 2. 发布 Bot 到 API 渠道\n val publishResult = cozeClient.publishBot(botId)\n if (publishResult.isFailure) {\n _toastMessage.value = \"❌ 发布失败: ${publishResult.exceptionOrNull()?.message}\"\n _isCreatingAgent.value = false\n return@launch\n }\n\n // 3. 保存到本地\n val agent = AgentConfig(name = name, botId = botId, prompt = prompt, emoji = emoji)\n val currentList = _agentList.value.toMutableList()\n currentList.add(agent)\n saveAgents(currentList)\n\n // 4. 设为当前 Agent\n securePrefs.edit().putString(KEY_CURRENT_AGENT, botId).apply()\n _currentAgentId.value = botId\n cozeClient.currentBotId = botId\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, botId)\n _cozeBotId.value = botId\n _isUsingDefaultBotId.value = false\n\n _toastMessage.value = \"✅ Agent「$name」创建成功\"\n _isCreatingAgent.value = false\n }\n }\n\n fun switchAgent(agent: AgentConfig) {\n cozeClient.currentBotId = agent.botId\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, agent.botId)\n }\n securePrefs.edit().putString(KEY_CURRENT_AGENT, agent.botId).apply()\n _currentAgentId.value = agent.botId\n _cozeBotId.value = agent.botId\n _isUsingDefaultBotId.value = false\n _toastMessage.value = \"✅ 已切换到「${agent.name}」\"\n }\n\n fun deleteAgent(agent: AgentConfig) {\n val currentList = _agentList.value.toMutableList()\n currentList.removeAll { it.botId == agent.botId }\n saveAgents(currentList)\n\n // 如果删除的是当前Agent,切回默认\n if (_currentAgentId.value == agent.botId) {\n val defaultBotId = Constants.DEFAULT_COZE_BOT_ID\n cozeClient.currentBotId = defaultBotId\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, defaultBotId)\n }\n securePrefs.edit().putString(KEY_CURRENT_AGENT, \"\").apply()\n _currentAgentId.value = \"\"\n _cozeBotId.value = defaultBotId\n }\n _toastMessage.value = \"✅ 已删除「${agent.name}」\"\n }\n\n fun clearToast() { _toastMessage.value = null }\n}\n"}, "ab7c3387b8578ed3136d5cda3b513132f24613d8": {"t": 1785755789.0251095, "n": 14188, "h": "40f1ae46c048aaab", "c": "package com.meitu.generator.ui.settings\n\nimport android.content.SharedPreferences\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.meitu.generator.data.remote.CozeApiClient\nimport com.meitu.generator.repository.SettingsRepository\nimport com.meitu.generator.util.Constants\nimport com.google.gson.Gson\nimport com.google.gson.reflect.TypeToken\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport javax.inject.Inject\nimport javax.inject.Named\n\ndata class AgentConfig(\n val name: String,\n val botId: String,\n val prompt: String,\n val emoji: String = \"🤖\"\n)\n\n@HiltViewModel\nclass SettingsViewModel @Inject constructor(\n private val settingsRepo: SettingsRepository,\n private val cozeClient: CozeApiClient,\n @Named(\"securePrefs\") private val securePrefs: SharedPreferences\n) : ViewModel() {\n\n private val gson = Gson()\n\n // ============ Coze 配置 ============\n private val _cozePat = MutableStateFlow(\"\")\n val cozePat: StateFlow = _cozePat.asStateFlow()\n\n private val _cozeBotId = MutableStateFlow(\"\")\n val cozeBotId: StateFlow = _cozeBotId.asStateFlow()\n\n private val _isUsingDefaultPat = MutableStateFlow(true)\n val isUsingDefaultPat: StateFlow = _isUsingDefaultPat.asStateFlow()\n\n private val _isUsingDefaultBotId = MutableStateFlow(true)\n val isUsingDefaultBotId: StateFlow = _isUsingDefaultBotId.asStateFlow()\n\n // ============ Token 消耗统计(分通道) ============\n // Coze 通道\n private val _cozeTokens = MutableStateFlow(0)\n val cozeTokens: StateFlow = _cozeTokens.asStateFlow()\n private val _cozeInputTokens = MutableStateFlow(0)\n val cozeInputTokens: StateFlow = _cozeInputTokens.asStateFlow()\n private val _cozeOutputTokens = MutableStateFlow(0)\n val cozeOutputTokens: StateFlow = _cozeOutputTokens.asStateFlow()\n private val _cozeMessages = MutableStateFlow(0)\n val cozeMessages: StateFlow = _cozeMessages.asStateFlow()\n\n // DeepSeek 通道\n private val _dsTokens = MutableStateFlow(0)\n val dsTokens: StateFlow = _dsTokens.asStateFlow()\n private val _dsInputTokens = MutableStateFlow(0)\n val dsInputTokens: StateFlow = _dsInputTokens.asStateFlow()\n private val _dsOutputTokens = MutableStateFlow(0)\n val dsOutputTokens: StateFlow = _dsOutputTokens.asStateFlow()\n private val _dsMessages = MutableStateFlow(0)\n val dsMessages: StateFlow = _dsMessages.asStateFlow()\n\n // 合计\n private val _totalTokens = MutableStateFlow(0)\n val totalTokens: StateFlow = _totalTokens.asStateFlow()\n\n // ============ AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow = _currentChannel.asStateFlow()\n\n private val _deepseekApiKey = MutableStateFlow(\"\")\n val deepseekApiKey: StateFlow = _deepseekApiKey.asStateFlow()\n\n // ============ DeepSeek 模型选择 ============\n private val _deepseekModel = MutableStateFlow(\"deepseek-v4-flash\")\n val deepseekModel: StateFlow = _deepseekModel.asStateFlow()\n\n // ============ DeepSeek 余额 ============\n private val _deepseekBalance = MutableStateFlow(null)\n val deepseekBalance: StateFlow = _deepseekBalance.asStateFlow()\n private val _isLoadingBalance = MutableStateFlow(false)\n val isLoadingBalance: StateFlow = _isLoadingBalance.asStateFlow()\n\n // ============ Agent 管理 ============\n private val _agentList = MutableStateFlow>(emptyList())\n val agentList: StateFlow> = _agentList.asStateFlow()\n\n private val _currentAgentId = MutableStateFlow(\"\")\n val currentAgentId: StateFlow = _currentAgentId.asStateFlow()\n\n private val _isCreatingAgent = MutableStateFlow(false)\n val isCreatingAgent: StateFlow = _isCreatingAgent.asStateFlow()\n\n // ============ Toast ============\n private val _toastMessage = MutableStateFlow(null)\n val toastMessage: StateFlow = _toastMessage.asStateFlow()\n\n companion object {\n private const val KEY_AGENTS = \"agent_list\"\n private const val KEY_CURRENT_AGENT = \"current_agent_id\"\n }\n\n init {\n loadConfig()\n loadTokenUsage()\n loadAgents()\n loadChannelAndDeepseek()\n }\n\n private fun loadChannelAndDeepseek() {\n val channel = securePrefs.getString(Constants.KEY_AI_CHANNEL, Constants.CHANNEL_COZE) ?: Constants.CHANNEL_COZE\n _currentChannel.value = channel\n val dsKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n _deepseekApiKey.value = dsKey\n val dsModel = securePrefs.getString(Constants.KEY_DEEPSEEK_MODEL, \"deepseek-v4-flash\") ?: \"deepseek-v4-flash\"\n _deepseekModel.value = dsModel\n }\n\n fun switchChannel(channel: String) {\n securePrefs.edit().putString(Constants.KEY_AI_CHANNEL, channel).apply()\n _currentChannel.value = channel\n cozeClient.currentBotId = if (channel == Constants.CHANNEL_DEEPSEEK) {\n \"\" // DeepSeek 模式不需要 botId\n } else {\n // 恢复 Coze botId\n val savedBotId = securePrefs.getString(\"current_agent_id\", \"\") ?: \"\"\n if (savedBotId.isNotBlank()) savedBotId\n else {\n // 从 settingsRepo 异步读取\n viewModelScope.launch {\n val botId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n cozeClient.currentBotId = botId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n _cozeBotId.value = cozeClient.currentBotId\n }\n _cozeBotId.value\n }\n }\n _toastMessage.value = if (channel == Constants.CHANNEL_DEEPSEEK) \"✅ 已切换到 DeepSeek\" else \"✅ 已切换到 Coze\"\n }\n\n fun saveDeepseekApiKey(key: String) {\n securePrefs.edit().putString(Constants.KEY_DEEPSEEK_API_KEY, key).apply()\n _deepseekApiKey.value = key\n _toastMessage.value = if (key.isNotBlank()) \"✅ DeepSeek API Key 已保存\" else \"✅ 已清除 DeepSeek API Key\"\n }\n\n fun saveDeepseekModel(model: String) {\n securePrefs.edit().putString(Constants.KEY_DEEPSEEK_MODEL, model).apply()\n _deepseekModel.value = model\n _toastMessage.value = \"✅ 已切换模型: $model\"\n }\n\n fun queryDeepseekBalance() {\n val apiKey = _deepseekApiKey.value\n if (apiKey.isBlank()) {\n _toastMessage.value = \"请先配置 DeepSeek API Key\"\n return\n }\n _isLoadingBalance.value = true\n viewModelScope.launch {\n try {\n val result = withContext(Dispatchers.IO) {\n val url = java.net.URL(\"https://api.deepseek.com/user/balance\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"GET\"\n conn.setRequestProperty(\"Authorization\", \"Bearer $apiKey\")\n conn.setRequestProperty(\"Accept\", \"application/json\")\n conn.connectTimeout = 10000\n conn.readTimeout = 10000\n val response = conn.inputStream.bufferedReader().readText()\n conn.disconnect()\n val json = com.google.gson.JsonParser.parseString(response).asJsonObject\n val balance = json.get(\"balance\")?.asString\n if (balance != null) \"¥$balance\" else \"查询失败\"\n }\n _deepseekBalance.value = result\n } catch (e: Exception) {\n _deepseekBalance.value = \"查询失败: ${e.message ?: \"网络异常\"}\"\n } finally {\n _isLoadingBalance.value = false\n }\n }\n }\n\n private fun loadConfig() {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n if (savedPat.isNotBlank()) {\n _cozePat.value = savedPat\n _isUsingDefaultPat.value = false\n } else {\n _cozePat.value = Constants.DEFAULT_COZE_PAT\n _isUsingDefaultPat.value = true\n }\n viewModelScope.launch {\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n if (savedBotId.isNotBlank()) {\n _cozeBotId.value = savedBotId\n _isUsingDefaultBotId.value = false\n } else {\n _cozeBotId.value = Constants.DEFAULT_COZE_BOT_ID\n _isUsingDefaultBotId.value = true\n }\n }\n }\n\n private fun loadTokenUsage() {\n // Coze 通道\n val cozeTotal = securePrefs.getInt(\"coze_total_tokens\", 0)\n val cozeInput = securePrefs.getInt(\"coze_total_input\", 0)\n val cozeOutput = securePrefs.getInt(\"coze_total_output\", 0)\n val cozeMsgs = securePrefs.getInt(\"coze_total_messages\", 0)\n _cozeTokens.value = cozeTotal\n _cozeInputTokens.value = cozeInput\n _cozeOutputTokens.value = cozeOutput\n _cozeMessages.value = cozeMsgs\n\n // DeepSeek 通道\n val dsTotal = securePrefs.getInt(\"ds_total_tokens\", 0)\n val dsInput = securePrefs.getInt(\"ds_total_input\", 0)\n val dsOutput = securePrefs.getInt(\"ds_total_output\", 0)\n val dsMsgs = securePrefs.getInt(\"ds_total_messages\", 0)\n _dsTokens.value = dsTotal\n _dsInputTokens.value = dsInput\n _dsOutputTokens.value = dsOutput\n _dsMessages.value = dsMsgs\n\n // 合计\n _totalTokens.value = cozeTotal + dsTotal\n }\n\n fun refreshTokenUsage() {\n loadTokenUsage()\n }\n\n fun saveCozePat(pat: String) {\n securePrefs.edit().putString(Constants.KEY_COZE_PAT, pat).apply()\n _cozePat.value = pat\n _isUsingDefaultPat.value = false\n _toastMessage.value = if (pat.isNotBlank()) \"✅ PAT 已保存\" else \"✅ 已恢复默认 PAT\"\n if (pat.isBlank()) {\n _cozePat.value = Constants.DEFAULT_COZE_PAT\n _isUsingDefaultPat.value = true\n }\n }\n\n fun saveCozeBotId(botId: String) {\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, botId)\n _cozeBotId.value = botId\n _isUsingDefaultBotId.value = false\n _toastMessage.value = if (botId.isNotBlank()) \"✅ Bot ID 已保存\" else \"✅ 已恢复默认 Bot ID\"\n if (botId.isBlank()) {\n _cozeBotId.value = Constants.DEFAULT_COZE_BOT_ID\n _isUsingDefaultBotId.value = true\n }\n }\n }\n\n // ============ Agent 管理 ============\n\n private fun loadAgents() {\n val json = securePrefs.getString(KEY_AGENTS, \"[]\") ?: \"[]\"\n val type = object : TypeToken>() {}.type\n _agentList.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n _currentAgentId.value = securePrefs.getString(KEY_CURRENT_AGENT, \"\") ?: \"\"\n }\n\n private fun saveAgents(agents: List) {\n securePrefs.edit().putString(KEY_AGENTS, gson.toJson(agents)).apply()\n _agentList.value = agents\n }\n\n fun createAgent(name: String, prompt: String, emoji: String) {\n if (name.isBlank() || prompt.isBlank()) {\n _toastMessage.value = \"⚠️ 名称和提示词不能为空\"\n return\n }\n viewModelScope.launch {\n _isCreatingAgent.value = true\n _toastMessage.value = \"⏳ 正在创建 Agent...\"\n\n // 1. 通过 Coze API 创建 Bot\n val createResult = cozeClient.createBot(name, prompt)\n if (createResult.isFailure) {\n _toastMessage.value = \"❌ 创建失败: ${createResult.exceptionOrNull()?.message}\"\n _isCreatingAgent.value = false\n return@launch\n }\n val botId = createResult.getOrNull()!!\n\n // 2. 发布 Bot 到 API 渠道\n val publishResult = cozeClient.publishBot(botId)\n if (publishResult.isFailure) {\n _toastMessage.value = \"❌ 发布失败: ${publishResult.exceptionOrNull()?.message}\"\n _isCreatingAgent.value = false\n return@launch\n }\n\n // 3. 保存到本地\n val agent = AgentConfig(name = name, botId = botId, prompt = prompt, emoji = emoji)\n val currentList = _agentList.value.toMutableList()\n currentList.add(agent)\n saveAgents(currentList)\n\n // 4. 设为当前 Agent\n securePrefs.edit().putString(KEY_CURRENT_AGENT, botId).apply()\n _currentAgentId.value = botId\n cozeClient.currentBotId = botId\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, botId)\n _cozeBotId.value = botId\n _isUsingDefaultBotId.value = false\n\n _toastMessage.value = \"✅ Agent「$name」创建成功\"\n _isCreatingAgent.value = false\n }\n }\n\n fun switchAgent(agent: AgentConfig) {\n cozeClient.currentBotId = agent.botId\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, agent.botId)\n }\n securePrefs.edit().putString(KEY_CURRENT_AGENT, agent.botId).apply()\n _currentAgentId.value = agent.botId\n _cozeBotId.value = agent.botId\n _isUsingDefaultBotId.value = false\n _toastMessage.value = \"✅ 已切换到「${agent.name}」\"\n }\n\n fun deleteAgent(agent: AgentConfig) {\n val currentList = _agentList.value.toMutableList()\n currentList.removeAll { it.botId == agent.botId }\n saveAgents(currentList)\n\n // 如果删除的是当前Agent,切回默认\n if (_currentAgentId.value == agent.botId) {\n val defaultBotId = Constants.DEFAULT_COZE_BOT_ID\n cozeClient.currentBotId = defaultBotId\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, defaultBotId)\n }\n securePrefs.edit().putString(KEY_CURRENT_AGENT, \"\").apply()\n _currentAgentId.value = \"\"\n _cozeBotId.value = defaultBotId\n }\n _toastMessage.value = \"✅ 已删除「${agent.name}」\"\n }\n\n fun clearToast() { _toastMessage.value = null }\n}\n"}, "ed9ee5b7a66ce9b5a494c881d10570f1960f654a": {"t": 1785755790.8251088, "n": 14025, "h": "9ca0f96621b3ce6a", "c": "package com.meitu.generator.ui.settings\n\nimport android.content.SharedPreferences\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.meitu.generator.data.remote.CozeApiClient\nimport com.meitu.generator.repository.SettingsRepository\nimport com.meitu.generator.util.Constants\nimport com.google.gson.Gson\nimport com.google.gson.reflect.TypeToken\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\nimport javax.inject.Named\n\ndata class AgentConfig(\n val name: String,\n val botId: String,\n val prompt: String,\n val emoji: String = \"🤖\"\n)\n\n@HiltViewModel\nclass SettingsViewModel @Inject constructor(\n private val settingsRepo: SettingsRepository,\n private val cozeClient: CozeApiClient,\n @Named(\"securePrefs\") private val securePrefs: SharedPreferences\n) : ViewModel() {\n\n private val gson = Gson()\n\n // ============ Coze 配置 ============\n private val _cozePat = MutableStateFlow(\"\")\n val cozePat: StateFlow = _cozePat.asStateFlow()\n\n private val _cozeBotId = MutableStateFlow(\"\")\n val cozeBotId: StateFlow = _cozeBotId.asStateFlow()\n\n private val _isUsingDefaultPat = MutableStateFlow(true)\n val isUsingDefaultPat: StateFlow = _isUsingDefaultPat.asStateFlow()\n\n private val _isUsingDefaultBotId = MutableStateFlow(true)\n val isUsingDefaultBotId: StateFlow = _isUsingDefaultBotId.asStateFlow()\n\n // ============ Token 消耗统计(分通道) ============\n // Coze 通道\n private val _cozeTokens = MutableStateFlow(0)\n val cozeTokens: StateFlow = _cozeTokens.asStateFlow()\n private val _cozeInputTokens = MutableStateFlow(0)\n val cozeInputTokens: StateFlow = _cozeInputTokens.asStateFlow()\n private val _cozeOutputTokens = MutableStateFlow(0)\n val cozeOutputTokens: StateFlow = _cozeOutputTokens.asStateFlow()\n private val _cozeMessages = MutableStateFlow(0)\n val cozeMessages: StateFlow = _cozeMessages.asStateFlow()\n\n // DeepSeek 通道\n private val _dsTokens = MutableStateFlow(0)\n val dsTokens: StateFlow = _dsTokens.asStateFlow()\n private val _dsInputTokens = MutableStateFlow(0)\n val dsInputTokens: StateFlow = _dsInputTokens.asStateFlow()\n private val _dsOutputTokens = MutableStateFlow(0)\n val dsOutputTokens: StateFlow = _dsOutputTokens.asStateFlow()\n private val _dsMessages = MutableStateFlow(0)\n val dsMessages: StateFlow = _dsMessages.asStateFlow()\n\n // 合计\n private val _totalTokens = MutableStateFlow(0)\n val totalTokens: StateFlow = _totalTokens.asStateFlow()\n\n // ============ AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow = _currentChannel.asStateFlow()\n\n private val _deepseekApiKey = MutableStateFlow(\"\")\n val deepseekApiKey: StateFlow = _deepseekApiKey.asStateFlow()\n\n // ============ DeepSeek 模型选择 ============\n private val _deepseekModel = MutableStateFlow(\"deepseek-v4-flash\")\n val deepseekModel: StateFlow = _deepseekModel.asStateFlow()\n\n // ============ DeepSeek 余额 ============\n private val _deepseekBalance = MutableStateFlow(null)\n val deepseekBalance: StateFlow = _deepseekBalance.asStateFlow()\n private val _isLoadingBalance = MutableStateFlow(false)\n val isLoadingBalance: StateFlow = _isLoadingBalance.asStateFlow()\n\n // ============ Agent 管理 ============\n private val _agentList = MutableStateFlow>(emptyList())\n val agentList: StateFlow> = _agentList.asStateFlow()\n\n private val _currentAgentId = MutableStateFlow(\"\")\n val currentAgentId: StateFlow = _currentAgentId.asStateFlow()\n\n private val _isCreatingAgent = MutableStateFlow(false)\n val isCreatingAgent: StateFlow = _isCreatingAgent.asStateFlow()\n\n // ============ Toast ============\n private val _toastMessage = MutableStateFlow(null)\n val toastMessage: StateFlow = _toastMessage.asStateFlow()\n\n companion object {\n private const val KEY_AGENTS = \"agent_list\"\n private const val KEY_CURRENT_AGENT = \"current_agent_id\"\n }\n\n init {\n loadConfig()\n loadTokenUsage()\n loadAgents()\n loadChannelAndDeepseek()\n }\n\n private fun loadChannelAndDeepseek() {\n val channel = securePrefs.getString(Constants.KEY_AI_CHANNEL, Constants.CHANNEL_COZE) ?: Constants.CHANNEL_COZE\n _currentChannel.value = channel\n val dsKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n _deepseekApiKey.value = dsKey\n val dsModel = securePrefs.getString(Constants.KEY_DEEPSEEK_MODEL, \"deepseek-v4-flash\") ?: \"deepseek-v4-flash\"\n _deepseekModel.value = dsModel\n }\n\n fun switchChannel(channel: String) {\n securePrefs.edit().putString(Constants.KEY_AI_CHANNEL, channel).apply()\n _currentChannel.value = channel\n cozeClient.currentBotId = if (channel == Constants.CHANNEL_DEEPSEEK) {\n \"\" // DeepSeek 模式不需要 botId\n } else {\n // 恢复 Coze botId\n val savedBotId = securePrefs.getString(\"current_agent_id\", \"\") ?: \"\"\n if (savedBotId.isNotBlank()) savedBotId\n else {\n // 从 settingsRepo 异步读取\n viewModelScope.launch {\n val botId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n cozeClient.currentBotId = botId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n _cozeBotId.value = cozeClient.currentBotId\n }\n _cozeBotId.value\n }\n }\n _toastMessage.value = if (channel == Constants.CHANNEL_DEEPSEEK) \"✅ 已切换到 DeepSeek\" else \"✅ 已切换到 Coze\"\n }\n\n fun saveDeepseekApiKey(key: String) {\n securePrefs.edit().putString(Constants.KEY_DEEPSEEK_API_KEY, key).apply()\n _deepseekApiKey.value = key\n _toastMessage.value = if (key.isNotBlank()) \"✅ DeepSeek API Key 已保存\" else \"✅ 已清除 DeepSeek API Key\"\n }\n\n fun saveDeepseekModel(model: String) {\n securePrefs.edit().putString(Constants.KEY_DEEPSEEK_MODEL, model).apply()\n _deepseekModel.value = model\n _toastMessage.value = \"✅ 已切换模型: $model\"\n }\n\n fun queryDeepseekBalance() {\n val apiKey = _deepseekApiKey.value\n if (apiKey.isBlank()) {\n _toastMessage.value = \"请先配置 DeepSeek API Key\"\n return\n }\n _isLoadingBalance.value = true\n viewModelScope.launch {\n try {\n val url = java.net.URL(\"https://api.deepseek.com/user/balance\")\n val conn = url.openConnection() as java.net.HttpURLConnection\n conn.requestMethod = \"GET\"\n conn.setRequestProperty(\"Authorization\", \"Bearer $apiKey\")\n conn.setRequestProperty(\"Accept\", \"application/json\")\n conn.connectTimeout = 10000\n conn.readTimeout = 10000\n val response = conn.inputStream.bufferedReader().readText()\n conn.disconnect()\n val json = com.google.gson.JsonParser.parseString(response).asJsonObject\n val balance = json.get(\"balance\")?.asString\n val toppedUp = json.get(\"topped_up_balance\")?.asString\n _deepseekBalance.value = if (balance != null) \"¥$balance\" else \"查询失败\"\n } catch (e: Exception) {\n _deepseekBalance.value = \"查询失败: ${e.message}\"\n } finally {\n _isLoadingBalance.value = false\n }\n }\n }\n\n private fun loadConfig() {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n if (savedPat.isNotBlank()) {\n _cozePat.value = savedPat\n _isUsingDefaultPat.value = false\n } else {\n _cozePat.value = Constants.DEFAULT_COZE_PAT\n _isUsingDefaultPat.value = true\n }\n viewModelScope.launch {\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n if (savedBotId.isNotBlank()) {\n _cozeBotId.value = savedBotId\n _isUsingDefaultBotId.value = false\n } else {\n _cozeBotId.value = Constants.DEFAULT_COZE_BOT_ID\n _isUsingDefaultBotId.value = true\n }\n }\n }\n\n private fun loadTokenUsage() {\n // Coze 通道\n val cozeTotal = securePrefs.getInt(\"coze_total_tokens\", 0)\n val cozeInput = securePrefs.getInt(\"coze_total_input\", 0)\n val cozeOutput = securePrefs.getInt(\"coze_total_output\", 0)\n val cozeMsgs = securePrefs.getInt(\"coze_total_messages\", 0)\n _cozeTokens.value = cozeTotal\n _cozeInputTokens.value = cozeInput\n _cozeOutputTokens.value = cozeOutput\n _cozeMessages.value = cozeMsgs\n\n // DeepSeek 通道\n val dsTotal = securePrefs.getInt(\"ds_total_tokens\", 0)\n val dsInput = securePrefs.getInt(\"ds_total_input\", 0)\n val dsOutput = securePrefs.getInt(\"ds_total_output\", 0)\n val dsMsgs = securePrefs.getInt(\"ds_total_messages\", 0)\n _dsTokens.value = dsTotal\n _dsInputTokens.value = dsInput\n _dsOutputTokens.value = dsOutput\n _dsMessages.value = dsMsgs\n\n // 合计\n _totalTokens.value = cozeTotal + dsTotal\n }\n\n fun refreshTokenUsage() {\n loadTokenUsage()\n }\n\n fun saveCozePat(pat: String) {\n securePrefs.edit().putString(Constants.KEY_COZE_PAT, pat).apply()\n _cozePat.value = pat\n _isUsingDefaultPat.value = false\n _toastMessage.value = if (pat.isNotBlank()) \"✅ PAT 已保存\" else \"✅ 已恢复默认 PAT\"\n if (pat.isBlank()) {\n _cozePat.value = Constants.DEFAULT_COZE_PAT\n _isUsingDefaultPat.value = true\n }\n }\n\n fun saveCozeBotId(botId: String) {\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, botId)\n _cozeBotId.value = botId\n _isUsingDefaultBotId.value = false\n _toastMessage.value = if (botId.isNotBlank()) \"✅ Bot ID 已保存\" else \"✅ 已恢复默认 Bot ID\"\n if (botId.isBlank()) {\n _cozeBotId.value = Constants.DEFAULT_COZE_BOT_ID\n _isUsingDefaultBotId.value = true\n }\n }\n }\n\n // ============ Agent 管理 ============\n\n private fun loadAgents() {\n val json = securePrefs.getString(KEY_AGENTS, \"[]\") ?: \"[]\"\n val type = object : TypeToken>() {}.type\n _agentList.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n _currentAgentId.value = securePrefs.getString(KEY_CURRENT_AGENT, \"\") ?: \"\"\n }\n\n private fun saveAgents(agents: List) {\n securePrefs.edit().putString(KEY_AGENTS, gson.toJson(agents)).apply()\n _agentList.value = agents\n }\n\n fun createAgent(name: String, prompt: String, emoji: String) {\n if (name.isBlank() || prompt.isBlank()) {\n _toastMessage.value = \"⚠️ 名称和提示词不能为空\"\n return\n }\n viewModelScope.launch {\n _isCreatingAgent.value = true\n _toastMessage.value = \"⏳ 正在创建 Agent...\"\n\n // 1. 通过 Coze API 创建 Bot\n val createResult = cozeClient.createBot(name, prompt)\n if (createResult.isFailure) {\n _toastMessage.value = \"❌ 创建失败: ${createResult.exceptionOrNull()?.message}\"\n _isCreatingAgent.value = false\n return@launch\n }\n val botId = createResult.getOrNull()!!\n\n // 2. 发布 Bot 到 API 渠道\n val publishResult = cozeClient.publishBot(botId)\n if (publishResult.isFailure) {\n _toastMessage.value = \"❌ 发布失败: ${publishResult.exceptionOrNull()?.message}\"\n _isCreatingAgent.value = false\n return@launch\n }\n\n // 3. 保存到本地\n val agent = AgentConfig(name = name, botId = botId, prompt = prompt, emoji = emoji)\n val currentList = _agentList.value.toMutableList()\n currentList.add(agent)\n saveAgents(currentList)\n\n // 4. 设为当前 Agent\n securePrefs.edit().putString(KEY_CURRENT_AGENT, botId).apply()\n _currentAgentId.value = botId\n cozeClient.currentBotId = botId\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, botId)\n _cozeBotId.value = botId\n _isUsingDefaultBotId.value = false\n\n _toastMessage.value = \"✅ Agent「$name」创建成功\"\n _isCreatingAgent.value = false\n }\n }\n\n fun switchAgent(agent: AgentConfig) {\n cozeClient.currentBotId = agent.botId\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, agent.botId)\n }\n securePrefs.edit().putString(KEY_CURRENT_AGENT, agent.botId).apply()\n _currentAgentId.value = agent.botId\n _cozeBotId.value = agent.botId\n _isUsingDefaultBotId.value = false\n _toastMessage.value = \"✅ 已切换到「${agent.name}」\"\n }\n\n fun deleteAgent(agent: AgentConfig) {\n val currentList = _agentList.value.toMutableList()\n currentList.removeAll { it.botId == agent.botId }\n saveAgents(currentList)\n\n // 如果删除的是当前Agent,切回默认\n if (_currentAgentId.value == agent.botId) {\n val defaultBotId = Constants.DEFAULT_COZE_BOT_ID\n cozeClient.currentBotId = defaultBotId\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, defaultBotId)\n }\n securePrefs.edit().putString(KEY_CURRENT_AGENT, \"\").apply()\n _currentAgentId.value = \"\"\n _cozeBotId.value = defaultBotId\n }\n _toastMessage.value = \"✅ 已删除「${agent.name}」\"\n }\n\n fun clearToast() { _toastMessage.value = null }\n}\n"}, "f1331143705b5b237c9c313aeba0e71c79ed065e": {"t": 1785755792.1003082, "n": 10544, "h": "2a0a91c57e491367", "c": "package com.meitu.generator.ui.settings\n\nimport android.content.SharedPreferences\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.meitu.generator.data.remote.CozeApiClient\nimport com.meitu.generator.repository.SettingsRepository\nimport com.meitu.generator.util.Constants\nimport com.google.gson.Gson\nimport com.google.gson.reflect.TypeToken\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\nimport javax.inject.Named\n\ndata class AgentConfig(\n val name: String,\n val botId: String,\n val prompt: String,\n val emoji: String = \"🤖\"\n)\n\n@HiltViewModel\nclass SettingsViewModel @Inject constructor(\n private val settingsRepo: SettingsRepository,\n private val cozeClient: CozeApiClient,\n @Named(\"securePrefs\") private val securePrefs: SharedPreferences\n) : ViewModel() {\n\n private val gson = Gson()\n\n // ============ Coze 配置 ============\n private val _cozePat = MutableStateFlow(\"\")\n val cozePat: StateFlow = _cozePat.asStateFlow()\n\n private val _cozeBotId = MutableStateFlow(\"\")\n val cozeBotId: StateFlow = _cozeBotId.asStateFlow()\n\n private val _isUsingDefaultPat = MutableStateFlow(true)\n val isUsingDefaultPat: StateFlow = _isUsingDefaultPat.asStateFlow()\n\n private val _isUsingDefaultBotId = MutableStateFlow(true)\n val isUsingDefaultBotId: StateFlow = _isUsingDefaultBotId.asStateFlow()\n\n // ============ Token 消耗统计 ============\n private val _totalTokens = MutableStateFlow(0)\n val totalTokens: StateFlow = _totalTokens.asStateFlow()\n\n private val _totalInputTokens = MutableStateFlow(0)\n val totalInputTokens: StateFlow = _totalInputTokens.asStateFlow()\n\n private val _totalOutputTokens = MutableStateFlow(0)\n val totalOutputTokens: StateFlow = _totalOutputTokens.asStateFlow()\n\n private val _totalMessages = MutableStateFlow(0)\n val totalMessages: StateFlow = _totalMessages.asStateFlow()\n\n // ============ AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow = _currentChannel.asStateFlow()\n\n private val _deepseekApiKey = MutableStateFlow(\"\")\n val deepseekApiKey: StateFlow = _deepseekApiKey.asStateFlow()\n\n // ============ Agent 管理 ============\n private val _agentList = MutableStateFlow>(emptyList())\n val agentList: StateFlow> = _agentList.asStateFlow()\n\n private val _currentAgentId = MutableStateFlow(\"\")\n val currentAgentId: StateFlow = _currentAgentId.asStateFlow()\n\n private val _isCreatingAgent = MutableStateFlow(false)\n val isCreatingAgent: StateFlow = _isCreatingAgent.asStateFlow()\n\n // ============ Toast ============\n private val _toastMessage = MutableStateFlow(null)\n val toastMessage: StateFlow = _toastMessage.asStateFlow()\n\n companion object {\n private const val KEY_AGENTS = \"agent_list\"\n private const val KEY_CURRENT_AGENT = \"current_agent_id\"\n }\n\n init {\n loadConfig()\n loadTokenUsage()\n loadAgents()\n loadChannelAndDeepseek()\n }\n\n private fun loadChannelAndDeepseek() {\n val channel = securePrefs.getString(Constants.KEY_AI_CHANNEL, Constants.CHANNEL_COZE) ?: Constants.CHANNEL_COZE\n _currentChannel.value = channel\n val dsKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n _deepseekApiKey.value = dsKey\n }\n\n fun switchChannel(channel: String) {\n securePrefs.edit().putString(Constants.KEY_AI_CHANNEL, channel).apply()\n _currentChannel.value = channel\n cozeClient.currentBotId = if (channel == Constants.CHANNEL_DEEPSEEK) {\n \"\" // DeepSeek 模式不需要 botId\n } else {\n // 恢复 Coze botId\n val savedBotId = securePrefs.getString(\"current_agent_id\", \"\") ?: \"\"\n if (savedBotId.isNotBlank()) savedBotId\n else {\n // 从 settingsRepo 异步读取\n viewModelScope.launch {\n val botId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n cozeClient.currentBotId = botId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n _cozeBotId.value = cozeClient.currentBotId\n }\n _cozeBotId.value\n }\n }\n _toastMessage.value = if (channel == Constants.CHANNEL_DEEPSEEK) \"✅ 已切换到 DeepSeek\" else \"✅ 已切换到 Coze\"\n }\n\n fun saveDeepseekApiKey(key: String) {\n securePrefs.edit().putString(Constants.KEY_DEEPSEEK_API_KEY, key).apply()\n _deepseekApiKey.value = key\n _toastMessage.value = if (key.isNotBlank()) \"✅ DeepSeek API Key 已保存\" else \"✅ 已清除 DeepSeek API Key\"\n }\n\n private fun loadConfig() {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n if (savedPat.isNotBlank()) {\n _cozePat.value = savedPat\n _isUsingDefaultPat.value = false\n } else {\n _cozePat.value = Constants.DEFAULT_COZE_PAT\n _isUsingDefaultPat.value = true\n }\n viewModelScope.launch {\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n if (savedBotId.isNotBlank()) {\n _cozeBotId.value = savedBotId\n _isUsingDefaultBotId.value = false\n } else {\n _cozeBotId.value = Constants.DEFAULT_COZE_BOT_ID\n _isUsingDefaultBotId.value = true\n }\n }\n }\n\n private fun loadTokenUsage() {\n _totalTokens.value = securePrefs.getInt(\"total_tokens_consumed\", 0)\n _totalInputTokens.value = securePrefs.getInt(\"total_input_tokens\", 0)\n _totalOutputTokens.value = securePrefs.getInt(\"total_output_tokens\", 0)\n _totalMessages.value = securePrefs.getInt(\"total_ai_messages\", 0)\n }\n\n fun refreshTokenUsage() {\n loadTokenUsage()\n }\n\n fun saveCozePat(pat: String) {\n securePrefs.edit().putString(Constants.KEY_COZE_PAT, pat).apply()\n _cozePat.value = pat\n _isUsingDefaultPat.value = false\n _toastMessage.value = if (pat.isNotBlank()) \"✅ PAT 已保存\" else \"✅ 已恢复默认 PAT\"\n if (pat.isBlank()) {\n _cozePat.value = Constants.DEFAULT_COZE_PAT\n _isUsingDefaultPat.value = true\n }\n }\n\n fun saveCozeBotId(botId: String) {\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, botId)\n _cozeBotId.value = botId\n _isUsingDefaultBotId.value = false\n _toastMessage.value = if (botId.isNotBlank()) \"✅ Bot ID 已保存\" else \"✅ 已恢复默认 Bot ID\"\n if (botId.isBlank()) {\n _cozeBotId.value = Constants.DEFAULT_COZE_BOT_ID\n _isUsingDefaultBotId.value = true\n }\n }\n }\n\n // ============ Agent 管理 ============\n\n private fun loadAgents() {\n val json = securePrefs.getString(KEY_AGENTS, \"[]\") ?: \"[]\"\n val type = object : TypeToken>() {}.type\n _agentList.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n _currentAgentId.value = securePrefs.getString(KEY_CURRENT_AGENT, \"\") ?: \"\"\n }\n\n private fun saveAgents(agents: List) {\n securePrefs.edit().putString(KEY_AGENTS, gson.toJson(agents)).apply()\n _agentList.value = agents\n }\n\n fun createAgent(name: String, prompt: String, emoji: String) {\n if (name.isBlank() || prompt.isBlank()) {\n _toastMessage.value = \"⚠️ 名称和提示词不能为空\"\n return\n }\n viewModelScope.launch {\n _isCreatingAgent.value = true\n _toastMessage.value = \"⏳ 正在创建 Agent...\"\n\n // 1. 通过 Coze API 创建 Bot\n val createResult = cozeClient.createBot(name, prompt)\n if (createResult.isFailure) {\n _toastMessage.value = \"❌ 创建失败: ${createResult.exceptionOrNull()?.message}\"\n _isCreatingAgent.value = false\n return@launch\n }\n val botId = createResult.getOrNull()!!\n\n // 2. 发布 Bot 到 API 渠道\n val publishResult = cozeClient.publishBot(botId)\n if (publishResult.isFailure) {\n _toastMessage.value = \"❌ 发布失败: ${publishResult.exceptionOrNull()?.message}\"\n _isCreatingAgent.value = false\n return@launch\n }\n\n // 3. 保存到本地\n val agent = AgentConfig(name = name, botId = botId, prompt = prompt, emoji = emoji)\n val currentList = _agentList.value.toMutableList()\n currentList.add(agent)\n saveAgents(currentList)\n\n // 4. 设为当前 Agent\n securePrefs.edit().putString(KEY_CURRENT_AGENT, botId).apply()\n _currentAgentId.value = botId\n cozeClient.currentBotId = botId\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, botId)\n _cozeBotId.value = botId\n _isUsingDefaultBotId.value = false\n\n _toastMessage.value = \"✅ Agent「$name」创建成功\"\n _isCreatingAgent.value = false\n }\n }\n\n fun switchAgent(agent: AgentConfig) {\n cozeClient.currentBotId = agent.botId\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, agent.botId)\n }\n securePrefs.edit().putString(KEY_CURRENT_AGENT, agent.botId).apply()\n _currentAgentId.value = agent.botId\n _cozeBotId.value = agent.botId\n _isUsingDefaultBotId.value = false\n _toastMessage.value = \"✅ 已切换到「${agent.name}」\"\n }\n\n fun deleteAgent(agent: AgentConfig) {\n val currentList = _agentList.value.toMutableList()\n currentList.removeAll { it.botId == agent.botId }\n saveAgents(currentList)\n\n // 如果删除的是当前Agent,切回默认\n if (_currentAgentId.value == agent.botId) {\n val defaultBotId = Constants.DEFAULT_COZE_BOT_ID\n cozeClient.currentBotId = defaultBotId\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, defaultBotId)\n }\n securePrefs.edit().putString(KEY_CURRENT_AGENT, \"\").apply()\n _currentAgentId.value = \"\"\n _cozeBotId.value = defaultBotId\n }\n _toastMessage.value = \"✅ 已删除「${agent.name}」\"\n }\n\n fun clearToast() { _toastMessage.value = null }\n}\n"}, "2c99b685fbd01f44389fb1b021889e83cd7df9b9": {"t": 1785755793.7999613, "n": 4127, "h": "3f027a9e8db4f09d", "c": "package com.meitu.generator.ui.settings\n\nimport android.content.SharedPreferences\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.meitu.generator.repository.SettingsRepository\nimport com.meitu.generator.util.Constants\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\nimport javax.inject.Named\n\n@HiltViewModel\nclass SettingsViewModel @Inject constructor(\n private val settingsRepo: SettingsRepository,\n @Named(\"securePrefs\") private val securePrefs: SharedPreferences\n) : ViewModel() {\n\n // ============ Coze 配置 ============\n private val _cozePat = MutableStateFlow(\"\")\n val cozePat: StateFlow = _cozePat.asStateFlow()\n\n private val _cozeBotId = MutableStateFlow(\"\")\n val cozeBotId: StateFlow = _cozeBotId.asStateFlow()\n\n private val _isUsingDefaultPat = MutableStateFlow(true)\n val isUsingDefaultPat: StateFlow = _isUsingDefaultPat.asStateFlow()\n\n private val _isUsingDefaultBotId = MutableStateFlow(true)\n val isUsingDefaultBotId: StateFlow = _isUsingDefaultBotId.asStateFlow()()\n\n // ============ Token 消耗统计 ============\n private val _totalTokens = MutableStateFlow(0)\n val totalTokens: StateFlow = _totalTokens.asStateFlow()\n\n private val _totalInputTokens = MutableStateFlow(0)\n val totalInputTokens: StateFlow = _totalInputTokens.asStateFlow()\n\n private val _totalOutputTokens = MutableStateFlow(0)\n val totalOutputTokens: StateFlow = _totalOutputTokens.asStateFlow()\n\n private val _totalMessages = MutableStateFlow(0)\n val totalMessages: StateFlow = _totalMessages.asStateFlow()\n\n // ============ Toast ============\n private val _toastMessage = MutableStateFlow(null)\n val toastMessage: StateFlow = _toastMessage.asStateFlow()\n\n init {\n loadConfig()\n loadTokenUsage()\n }\n\n private fun loadConfig() {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n if (savedPat.isNotBlank()) {\n _cozePat.value = savedPat\n _isUsingDefaultPat.value = false\n } else {\n _cozePat.value = Constants.DEFAULT_COZE_PAT\n _isUsingDefaultPat.value = true\n }\n viewModelScope.launch {\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n if (savedBotId.isNotBlank()) {\n _cozeBotId.value = savedBotId\n _isUsingDefaultBotId.value = false\n } else {\n _cozeBotId.value = Constants.DEFAULT_COZE_BOT_ID\n _isUsingDefaultBotId.value = true\n }\n }\n }\n\n private fun loadTokenUsage() {\n _totalTokens.value = securePrefs.getInt(\"total_tokens_consumed\", 0)\n _totalInputTokens.value = securePrefs.getInt(\"total_input_tokens\", 0)\n _totalOutputTokens.value = securePrefs.getInt(\"total_output_tokens\", 0)\n _totalMessages.value = securePrefs.getInt(\"total_ai_messages\", 0)\n }\n\n fun refreshTokenUsage() {\n loadTokenUsage()\n }\n\n fun saveCozePat(pat: String) {\n securePrefs.edit().putString(Constants.KEY_COZE_PAT, pat).apply()\n _cozePat.value = pat\n _isUsingDefaultPat.value = false\n _toastMessage.value = if (pat.isNotBlank()) \"✅ PAT 已保存\" else \"✅ 已恢复默认 PAT\"\n if (pat.isBlank()) {\n _cozePat.value = Constants.DEFAULT_COZE_PAT\n _isUsingDefaultPat.value = true\n }\n }\n\n fun saveCozeBotId(botId: String) {\n viewModelScope.launch {\n settingsRepo.setString(Constants.KEY_COZE_BOT_ID, botId)\n _cozeBotId.value = botId\n _isUsingDefaultBotId.value = false\n _toastMessage.value = if (botId.isNotBlank()) \"✅ Bot ID 已保存\" else \"✅ 已恢复默认 Bot ID\"\n if (botId.isBlank()) {\n _cozeBotId.value = Constants.DEFAULT_COZE_BOT_ID\n _isUsingDefaultBotId.value = true\n }\n }\n }\n\n fun clearToast() { _toastMessage.value = null }\n}\n"}}, "app/src/main/java/com/meitu/generator/ui/assistant/AssistantViewModel.kt": {"fe1783a63714dcef00096520050595d115ec92c8": {"t": 1785745732.437219, "n": 29016, "h": "900aaafe167cf410", "c": "package com.meitu.generator.ui.assistant\n\nimport android.app.Application\nimport android.content.SharedPreferences\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.net.Uri\nimport android.util.Base64\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.meitu.generator.data.remote.CozeApiClient\nimport com.meitu.generator.data.remote.StreamEvent\nimport com.meitu.generator.data.local.dao.ChatMessageDao\nimport com.meitu.generator.data.local.dao.SessionSummary\nimport com.meitu.generator.data.local.entity.ChatMessageEntity\nimport com.meitu.generator.repository.SettingsRepository\nimport com.meitu.generator.util.Constants\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport java.io.ByteArrayOutputStream\nimport javax.inject.Inject\nimport javax.inject.Named\n\ndata class ChatMessage(\n val id: Long = System.nanoTime(),\n val text: String,\n val isUser: Boolean,\n val isSystem: Boolean = false,\n val imageUri: String? = null,\n val timestamp: Long = System.currentTimeMillis(),\n val replyToId: Long? = null\n)\n\n@HiltViewModel\nclass AssistantViewModel @Inject constructor(\n application: Application,\n private val settingsRepo: SettingsRepository,\n private val cozeClient: CozeApiClient,\n @Named(\"securePrefs\") private val securePrefs: SharedPreferences,\n private val chatMessageDao: ChatMessageDao\n) : AndroidViewModel(application) {\n\n // ============ Chat Messages ============\n private val _messages = MutableStateFlow>(\n listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n )\n val messages: StateFlow> = _messages.asStateFlow()\n\n // ============ 会话管理 ============\n private var currentSessionId: Long = System.currentTimeMillis()\n private var cozeConversationId: String? = null\n\n // ============ 对话名称 ============\n private val _conversationName = MutableStateFlow(\"新对话\")\n val conversationName: StateFlow = _conversationName.asStateFlow()\n\n fun setConversationName(name: String) {\n _conversationName.value = name\n }\n\n fun autoGenerateConversationName(firstMessage: String) {\n if (_conversationName.value == \"新对话\" && firstMessage.isNotBlank()) {\n val name = if (firstMessage.length > 15) firstMessage.take(15) + \"...\" else firstMessage\n _conversationName.value = name\n }\n }\n\n // ============ 历史对话列表 ============\n private val _sessionList = MutableStateFlow>(emptyList())\n val sessionList: StateFlow> = _sessionList.asStateFlow()\n\n fun loadSessionList() {\n viewModelScope.launch {\n withContext(Dispatchers.IO) {\n val sessions = chatMessageDao.getSessionSummaries()\n _sessionList.value = sessions\n }\n }\n }\n\n fun switchToSession(sessionId: Long) {\n currentSessionId = sessionId\n cozeConversationId = null // 切换会话时重置 Coze 对话 ID\n viewModelScope.launch {\n loadMessagesFromDb()\n // 恢复对话名称\n val messages = _messages.value\n val firstUserMsg = messages.firstOrNull { it.isUser }\n _conversationName.value = if (firstUserMsg != null) {\n if (firstUserMsg.text.length > 15) firstUserMsg.text.take(15) + \"...\" else firstUserMsg.text\n } else {\n \"历史对话\"\n }\n }\n }\n\n private val _inputText = MutableStateFlow(\"\")\n val inputText: StateFlow = _inputText.asStateFlow()\n\n fun setInputText(text: String) {\n _inputText.value = text\n }\n\n // ============ 附件(图片) ============\n private val _pendingImageUri = MutableStateFlow(null)\n val pendingImageUri: StateFlow = _pendingImageUri.asStateFlow()\n\n fun setPendingImageUri(uri: String?) {\n _pendingImageUri.value = uri\n }\n\n // ============ Loading ============\n private val _isLoading = MutableStateFlow(false)\n val isLoading: StateFlow = _isLoading.asStateFlow()\n\n // ============ 引用回复 ============\n private val _replyToMessage = MutableStateFlow(null)\n val replyToMessage: StateFlow = _replyToMessage.asStateFlow()\n\n fun setReplyToMessage(msg: ChatMessage?) {\n _replyToMessage.value = msg\n }\n\n fun clearReplyToMessage() {\n _replyToMessage.value = null\n }\n\n // ============ 状态 ============\n private val _statusMessage = MutableStateFlow(null)\n val statusMessage: StateFlow = _statusMessage.asStateFlow()\n\n // ============ 当前流式任务的 Job ============\n private var streamJob: Job? = null\n\n // ============ Coze 配置状态 ============\n private val _isCozeConfigured = MutableStateFlow(true)\n val isCozeConfigured: StateFlow = _isCozeConfigured.asStateFlow()\n\n // ============ 当前 AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow = _currentChannel.asStateFlow()\n\n init {\n checkCozeConfig()\n loadChannel()\n loadSessionList()\n }\n\n private fun loadChannel() {\n val channel = securePrefs.getString(Constants.KEY_AI_CHANNEL, Constants.CHANNEL_COZE) ?: Constants.CHANNEL_COZE\n _currentChannel.value = channel\n }\n\n /**\n * 获取所有可用通道列表,用于循环切换\n */\n private fun getAvailableChannels(): List {\n val channels = mutableListOf(Constants.CHANNEL_COZE, Constants.CHANNEL_DEEPSEEK)\n val customJson = securePrefs.getString(Constants.KEY_CUSTOM_API_LIST, \"[]\") ?: \"[]\"\n try {\n val type = object : com.google.gson.reflect.TypeToken>() {}.type\n val customList: List = com.google.gson.Gson().fromJson(customJson, type)\n for (c in customList) {\n channels.add(\"${Constants.CHANNEL_CUSTOM_PREFIX}${c.id}\")\n }\n } catch (_: Exception) {}\n return channels\n }\n\n /**\n * 获取当前通道的显示标签\n */\n fun getChannelLabel(): String {\n val ch = _currentChannel.value\n return when {\n ch == Constants.CHANNEL_COZE -> \"🧠 Coze\"\n ch == Constants.CHANNEL_DEEPSEEK -> \"DeepSeek\"\n ch.startsWith(Constants.CHANNEL_CUSTOM_PREFIX) -> {\n val id = ch.removePrefix(Constants.CHANNEL_CUSTOM_PREFIX)\n val customJson = securePrefs.getString(Constants.KEY_CUSTOM_API_LIST, \"[]\") ?: \"[]\"\n try {\n val type = object : com.google.gson.reflect.TypeToken>() {}.type\n val list: List = com.google.gson.Gson().fromJson(customJson, type)\n val config = list.find { it.id == id }\n \"${config?.emoji ?: \"🔌\"} ${config?.name ?: \"自定义\"}\"\n } catch (_: Exception) { \"🔌 自定义\" }\n }\n else -> \"🧠 Coze\"\n }\n }\n\n /**\n * 循环切换到下一个通道\n */\n fun cycleChannel() {\n val channels = getAvailableChannels()\n val currentIdx = channels.indexOf(_currentChannel.value)\n val nextIdx = if (currentIdx < 0 || currentIdx >= channels.size - 1) 0 else currentIdx + 1\n val nextChannel = channels[nextIdx]\n securePrefs.edit().putString(Constants.KEY_AI_CHANNEL, nextChannel).apply()\n _currentChannel.value = nextChannel\n }\n\n private fun checkCozeConfig() {\n viewModelScope.launch {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n // 使用默认值回退:用户未配置时自动使用内置的 PAT 和 Bot ID\n val effectivePat = savedPat.ifBlank { Constants.DEFAULT_COZE_PAT }\n val effectiveBotId = savedBotId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n _isCozeConfigured.value = effectivePat.isNotBlank() && effectiveBotId.isNotBlank()\n }\n }\n\n fun refreshCozeConfig() {\n checkCozeConfig()\n loadChannel()\n }\n\n // ============ 发送消息 ============\n fun sendMessage() {\n val text = _inputText.value.trim()\n val imageUri = _pendingImageUri.value\n\n if (text.isEmpty() && imageUri == null) return\n\n val channel = _currentChannel.value\n if (channel == Constants.CHANNEL_COZE && !_isCozeConfigured.value) {\n _statusMessage.value = \"请先在设置中配置 Coze PAT 和 Bot ID\"\n return\n }\n if (channel == Constants.CHANNEL_DEEPSEEK) {\n val apiKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n if (apiKey.isBlank()) {\n _statusMessage.value = \"请先在设置中配置 DeepSeek API Key\"\n return\n }\n }\n\n // 生成对话名称\n autoGenerateConversationName(text.ifEmpty { \"[图片]\" })\n\n // 添加用户消息到列表\n val replyMsg = _replyToMessage.value\n val replyPrefix = if (replyMsg != null) {\n val preview = replyMsg.text.take(50).replace(\"\\n\", \" \")\n \"[引用: ${preview}]\\n\\n\"\n } else \"\"\n val userMessage = ChatMessage(\n text = text.ifEmpty { \"[图片]\" },\n isUser = true,\n imageUri = imageUri,\n replyToId = replyMsg?.id\n )\n _messages.value = (_messages.value + userMessage).takeLast(100)\n _inputText.value = \"\"\n _pendingImageUri.value = null\n _replyToMessage.value = null\n _isLoading.value = true\n\n // 保存用户消息到数据库\n viewModelScope.launch(Dispatchers.IO) {\n try {\n chatMessageDao.insert(ChatMessageEntity(\n text = userMessage.text,\n isUser = true,\n imageUri = userMessage.imageUri,\n timestamp = userMessage.timestamp,\n sessionId = currentSessionId\n ))\n loadSessionList()\n } catch (_: Exception) {}\n }\n\n // 构建消息内容(如果有图片,先分析图片)\n viewModelScope.launch {\n var messageContent = replyPrefix + text\n if (imageUri != null) {\n _statusMessage.value = \"🖼️ 正在处理图片...\"\n val imageDescription = processImage(imageUri)\n messageContent = if (text.isNotEmpty()) {\n \"$text\\n\\n[用户发送了一张图片,图片内容描述: $imageDescription]\"\n } else {\n \"[用户发送了一张图片,图片内容描述: $imageDescription]\"\n }\n }\n\n // 根据通道选择调用\n when {\n channel == Constants.CHANNEL_DEEPSEEK -> streamChatFromDeepSeek(messageContent)\n channel.startsWith(Constants.CHANNEL_CUSTOM_PREFIX) -> {\n val customId = channel.removePrefix(Constants.CHANNEL_CUSTOM_PREFIX)\n val customJson = securePrefs.getString(Constants.KEY_CUSTOM_API_LIST, \"[]\") ?: \"[]\"\n try {\n val type = object : com.google.gson.reflect.TypeToken>() {}.type\n val list: List = com.google.gson.Gson().fromJson(customJson, type)\n val config = list.find { it.id == customId }\n if (config != null) {\n streamChatFromCustom(config, messageContent)\n } else {\n _statusMessage.value = \"❌ 自定义 API 配置不存在\"\n _isLoading.value = false\n }\n } catch (_: Exception) {\n _statusMessage.value = \"❌ 读取自定义配置失败\"\n _isLoading.value = false\n }\n }\n else -> streamChatFromCoze(messageContent)\n }\n }\n }\n\n // ============ DeepSeek 直连 ============\n private suspend fun streamChatFromDeepSeek(message: String) {\n var aiMessageId: Long? = null\n var fullResponse = StringBuilder()\n\n try {\n val apiKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n val systemPrompt = securePrefs.getString(\"deepseek_system_prompt\", \"\") ?: \"\"\n val dsModel = securePrefs.getString(Constants.KEY_DEEPSEEK_MODEL, \"deepseek-v4-flash\") ?: \"deepseek-v4-flash\"\n\n val history = buildHistoryFromMessages()\n\n cozeClient.streamDeepSeekChat(\n apiKey = apiKey,\n message = message,\n model = dsModel,\n systemPrompt = systemPrompt.ifBlank { null },\n history = history\n ).collect { event ->\n when (event) {\n is StreamEvent.Status -> {\n _statusMessage.value = event.message\n }\n is StreamEvent.Delta -> {\n if (aiMessageId == null) {\n aiMessageId = System.nanoTime()\n val newMsg = ChatMessage(id = aiMessageId!!, text = \"\", isUser = false)\n _messages.value = (_messages.value + newMsg).takeLast(100)\n }\n fullResponse.append(event.text)\n val id = aiMessageId!!\n _messages.value = _messages.value.map { msg ->\n if (msg.id == id) msg.copy(text = fullResponse.toString()) else msg\n }\n }\n is StreamEvent.Done -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else if (aiMessageId == null) {\n // 没有收到任何内容,创建一条提示\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ AI 未返回内容,请重试\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.Error -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ ${event.message}\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.TokenUsage -> {\n accumulateTokenUsage(event.total, event.input, event.output)\n }\n }\n }\n } catch (e: Exception) {\n _isLoading.value = false\n _statusMessage.value = null\n val errorMsg = \"❌ DeepSeek 连接失败: ${e.message ?: \"未知错误\"}\"\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = errorMsg, isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n }\n\n // ============ 自定义 API 流式对话 ============\n private suspend fun streamChatFromCustom(config: com.meitu.generator.ui.settings.CustomApiConfig, message: String) {\n var aiMessageId: Long? = null\n var fullResponse = StringBuilder()\n\n try {\n val systemPrompt = securePrefs.getString(\"deepseek_system_prompt\", \"\") ?: \"\"\n val history = buildHistoryFromMessages()\n\n cozeClient.streamCustomChat(\n apiKey = config.apiKey,\n message = message,// 确保传入实际用户消息\n model = config.model,\n baseUrl = config.baseUrl,\n systemPrompt = systemPrompt.ifBlank { null },\n history = history\n ).collect { event ->\n when (event) {\n is StreamEvent.Status -> {\n _statusMessage.value = event.message\n }\n is StreamEvent.Delta -> {\n if (aiMessageId == null) {\n aiMessageId = System.nanoTime()\n val newMsg = ChatMessage(id = aiMessageId!!, text = \"\", isUser = false)\n _messages.value = (_messages.value + newMsg).takeLast(100)\n }\n fullResponse.append(event.text)\n val id = aiMessageId!!\n _messages.value = _messages.value.map { msg ->\n if (msg.id == id) msg.copy(text = fullResponse.toString()) else msg\n }\n }\n is StreamEvent.Done -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else if (aiMessageId == null) {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ API 未返回内容,请检查配置\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.Error -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ ${event.message}\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.TokenUsage -> {\n accumulateTokenUsage(event.total, event.input, event.output)\n }\n }\n }\n } catch (e: Exception) {\n _isLoading.value = false\n _statusMessage.value = null\n val errorMsg = \"❌ 自定义 API 连接失败: ${e.message ?: \"未知错误\"}\"\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = errorMsg, isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n }\n\n // ============ Coze 流式对话 ============\n private suspend fun streamChatFromCoze(message: String) {\n var aiMessageId: Long? = null\n var fullResponse = StringBuilder()\n\n try {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n val pat = savedPat.ifBlank { Constants.DEFAULT_COZE_PAT }\n val botId = savedBotId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n\n val client = CozeApiClient(\n baseUrl = Constants.COZE_API_BASE_URL,\n pat = pat,\n botId = botId,\n httpClient = provideTempHttpClient()\n )\n\n client.streamChat(\n message = message,\n conversationId = cozeConversationId\n ).collect { event ->\n when (event) {\n is StreamEvent.Status -> {\n _statusMessage.value = event.message\n }\n is StreamEvent.Delta -> {\n if (aiMessageId == null) {\n aiMessageId = System.nanoTime()\n val newMsg = ChatMessage(id = aiMessageId!!, text = \"\", isUser = false)\n _messages.value = (_messages.value + newMsg).takeLast(100)\n }\n fullResponse.append(event.text)\n val id = aiMessageId!!\n _messages.value = _messages.value.map { msg ->\n if (msg.id == id) msg.copy(text = fullResponse.toString()) else msg\n }\n }\n is StreamEvent.Done -> {\n cozeConversationId = event.conversationId\n _statusMessage.value = null\n _isLoading.value = false\n\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else if (aiMessageId == null) {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ AI 未返回内容,请重试\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.Error -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ ${event.message}\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.TokenUsage -> {\n accumulateTokenUsage(event.total, event.input, event.output)\n }\n }\n }\n } catch (e: Exception) {\n _isLoading.value = false\n _statusMessage.value = null\n val errorMsg = \"❌ 连接失败: ${e.message ?: \"未知错误\"}\"\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = errorMsg, isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n }\n\n private fun provideTempHttpClient(): okhttp3.OkHttpClient {\n return okhttp3.OkHttpClient.Builder()\n .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)\n .readTimeout(120, java.util.concurrent.TimeUnit.SECONDS)\n .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS)\n .build()\n }\n\n private suspend fun saveAiMessage(text: String) {\n withContext(Dispatchers.IO) {\n try {\n chatMessageDao.insert(ChatMessageEntity(\n text = text,\n isUser = false,\n timestamp = System.currentTimeMillis(),\n sessionId = currentSessionId\n ))\n } catch (_: Exception) {}\n }\n }\n\n // ============ 构建历史对话 ============\n private fun buildHistoryFromMessages(): List> {\n val history = mutableListOf>()\n val chatMessages = _messages.value.filter { !it.isSystem }\n // 取最近的对话(排除空的AI占位消息)\n for (msg in chatMessages) {\n if (msg.text.isNotBlank()) {\n history.add(Pair(if (msg.isUser) \"user\" else \"assistant\", msg.text))\n }\n }\n return history\n }\n\n // ============ Token 消耗累计(分通道,支持自定义) ============\n companion object {\n // Coze 通道\n private const val KEY_COZE_TOTAL_TOKENS = \"coze_total_tokens\"\n private const val KEY_COZE_TOTAL_INPUT = \"coze_total_input\"\n private const val KEY_COZE_TOTAL_OUTPUT = \"coze_total_output\"\n private const val KEY_COZE_TOTAL_MESSAGES = \"coze_total_messages\"\n // DeepSeek 通道\n private const val KEY_DS_TOTAL_TOKENS = \"ds_total_tokens\"\n private const val KEY_DS_TOTAL_INPUT = \"ds_total_input\"\n private const val KEY_DS_TOTAL_OUTPUT = \"ds_total_output\"\n private const val KEY_DS_TOTAL_MESSAGES = \"ds_total_messages\"\n // 自定义通道前缀\n private const val KEY_CUSTOM_PREFIX = \"custom:\"\n }\n\n private fun accumulateTokenUsage(total: Int, input: Int, output: Int) {\n val channel = _currentChannel.value\n val editor = securePrefs.edit()\n\n val prefix = when {\n channel == Constants.CHANNEL_COZE -> \"coze\"\n channel == Constants.CHANNEL_DEEPSEEK -> \"ds\"\n channel.startsWith(KEY_CUSTOM_PREFIX) -> channel // custom:{id}\n else -> \"coze\"\n }\n\n editor.putInt(\"${prefix}_total_tokens\", securePrefs.getInt(\"${prefix}_total_tokens\", 0) + total)\n editor.putInt(\"${prefix}_total_input\", securePrefs.getInt(\"${prefix}_total_input\", 0) + input)\n editor.putInt(\"${prefix}_total_output\", securePrefs.getInt(\"${prefix}_total_output\", 0) + output)\n editor.putInt(\"${prefix}_total_messages\", securePrefs.getInt(\"${prefix}_total_messages\", 0) + 1)\n editor.apply()\n }\n\n private suspend fun processImage(uri: String): String {\n return withContext(Dispatchers.IO) {\n try {\n val context = getApplication()\n val inputStream = context.contentResolver.openInputStream(Uri.parse(uri))\n val bitmap = BitmapFactory.decodeStream(inputStream)\n inputStream?.close()\n\n if (bitmap != null) {\n val byteArrayOutputStream = ByteArrayOutputStream()\n bitmap.compress(Bitmap.CompressFormat.JPEG, 80, byteArrayOutputStream)\n val base64 = Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)\n \"图片已处理(${bitmap.width}x${bitmap.height})\"\n } else {\n \"图片无法解析\"\n }\n } catch (e: Exception) {\n \"图片处理失败: ${e.message}\"\n }\n }\n }\n\n // ============ 新建对话 ============\n fun newConversation() {\n streamJob?.cancel()\n currentSessionId = System.currentTimeMillis()\n cozeConversationId = null\n _conversationName.value = \"新对话\"\n _messages.value = listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n _isLoading.value = false\n _statusMessage.value = null\n loadSessionList()\n }\n\n // ============ 清空当前对话 ============\n fun clearCurrentChat() {\n streamJob?.cancel()\n _messages.value = listOf(ChatMessage(\n text = \"对话已清空,开始新的对话吧 💬\",\n isUser = false\n ))\n _isLoading.value = false\n _statusMessage.value = null\n\n viewModelScope.launch(Dispatchers.IO) {\n try {\n chatMessageDao.deleteBySession(currentSessionId)\n } catch (_: Exception) {}\n }\n }\n\n // ============ 从数据库加载消息 ============\n private fun loadMessagesFromDb() {\n viewModelScope.launch {\n withContext(Dispatchers.IO) {\n val entities = chatMessageDao.getMessagesBySession(currentSessionId)\n val chatMessages = entities.map { entity ->\n ChatMessage(\n text = entity.text,\n isUser = entity.isUser,\n imageUri = entity.imageUri,\n timestamp = entity.timestamp\n )\n }\n if (chatMessages.isNotEmpty()) {\n _messages.value = chatMessages\n }\n }\n }\n }\n\n override fun onCleared() {\n super.onCleared()\n streamJob?.cancel()\n }\n}\n"}, "ccebd3f58e4c037c5470c959540de1933e9b42d3": {"t": 1785755781.9267592, "n": 29016, "h": "900aaafe167cf410", "c": "package com.meitu.generator.ui.assistant\n\nimport android.app.Application\nimport android.content.SharedPreferences\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.net.Uri\nimport android.util.Base64\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.meitu.generator.data.remote.CozeApiClient\nimport com.meitu.generator.data.remote.StreamEvent\nimport com.meitu.generator.data.local.dao.ChatMessageDao\nimport com.meitu.generator.data.local.dao.SessionSummary\nimport com.meitu.generator.data.local.entity.ChatMessageEntity\nimport com.meitu.generator.repository.SettingsRepository\nimport com.meitu.generator.util.Constants\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport java.io.ByteArrayOutputStream\nimport javax.inject.Inject\nimport javax.inject.Named\n\ndata class ChatMessage(\n val id: Long = System.nanoTime(),\n val text: String,\n val isUser: Boolean,\n val isSystem: Boolean = false,\n val imageUri: String? = null,\n val timestamp: Long = System.currentTimeMillis(),\n val replyToId: Long? = null\n)\n\n@HiltViewModel\nclass AssistantViewModel @Inject constructor(\n application: Application,\n private val settingsRepo: SettingsRepository,\n private val cozeClient: CozeApiClient,\n @Named(\"securePrefs\") private val securePrefs: SharedPreferences,\n private val chatMessageDao: ChatMessageDao\n) : AndroidViewModel(application) {\n\n // ============ Chat Messages ============\n private val _messages = MutableStateFlow>(\n listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n )\n val messages: StateFlow> = _messages.asStateFlow()\n\n // ============ 会话管理 ============\n private var currentSessionId: Long = System.currentTimeMillis()\n private var cozeConversationId: String? = null\n\n // ============ 对话名称 ============\n private val _conversationName = MutableStateFlow(\"新对话\")\n val conversationName: StateFlow = _conversationName.asStateFlow()\n\n fun setConversationName(name: String) {\n _conversationName.value = name\n }\n\n fun autoGenerateConversationName(firstMessage: String) {\n if (_conversationName.value == \"新对话\" && firstMessage.isNotBlank()) {\n val name = if (firstMessage.length > 15) firstMessage.take(15) + \"...\" else firstMessage\n _conversationName.value = name\n }\n }\n\n // ============ 历史对话列表 ============\n private val _sessionList = MutableStateFlow>(emptyList())\n val sessionList: StateFlow> = _sessionList.asStateFlow()\n\n fun loadSessionList() {\n viewModelScope.launch {\n withContext(Dispatchers.IO) {\n val sessions = chatMessageDao.getSessionSummaries()\n _sessionList.value = sessions\n }\n }\n }\n\n fun switchToSession(sessionId: Long) {\n currentSessionId = sessionId\n cozeConversationId = null // 切换会话时重置 Coze 对话 ID\n viewModelScope.launch {\n loadMessagesFromDb()\n // 恢复对话名称\n val messages = _messages.value\n val firstUserMsg = messages.firstOrNull { it.isUser }\n _conversationName.value = if (firstUserMsg != null) {\n if (firstUserMsg.text.length > 15) firstUserMsg.text.take(15) + \"...\" else firstUserMsg.text\n } else {\n \"历史对话\"\n }\n }\n }\n\n private val _inputText = MutableStateFlow(\"\")\n val inputText: StateFlow = _inputText.asStateFlow()\n\n fun setInputText(text: String) {\n _inputText.value = text\n }\n\n // ============ 附件(图片) ============\n private val _pendingImageUri = MutableStateFlow(null)\n val pendingImageUri: StateFlow = _pendingImageUri.asStateFlow()\n\n fun setPendingImageUri(uri: String?) {\n _pendingImageUri.value = uri\n }\n\n // ============ Loading ============\n private val _isLoading = MutableStateFlow(false)\n val isLoading: StateFlow = _isLoading.asStateFlow()\n\n // ============ 引用回复 ============\n private val _replyToMessage = MutableStateFlow(null)\n val replyToMessage: StateFlow = _replyToMessage.asStateFlow()\n\n fun setReplyToMessage(msg: ChatMessage?) {\n _replyToMessage.value = msg\n }\n\n fun clearReplyToMessage() {\n _replyToMessage.value = null\n }\n\n // ============ 状态 ============\n private val _statusMessage = MutableStateFlow(null)\n val statusMessage: StateFlow = _statusMessage.asStateFlow()\n\n // ============ 当前流式任务的 Job ============\n private var streamJob: Job? = null\n\n // ============ Coze 配置状态 ============\n private val _isCozeConfigured = MutableStateFlow(true)\n val isCozeConfigured: StateFlow = _isCozeConfigured.asStateFlow()\n\n // ============ 当前 AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow = _currentChannel.asStateFlow()\n\n init {\n checkCozeConfig()\n loadChannel()\n loadSessionList()\n }\n\n private fun loadChannel() {\n val channel = securePrefs.getString(Constants.KEY_AI_CHANNEL, Constants.CHANNEL_COZE) ?: Constants.CHANNEL_COZE\n _currentChannel.value = channel\n }\n\n /**\n * 获取所有可用通道列表,用于循环切换\n */\n private fun getAvailableChannels(): List {\n val channels = mutableListOf(Constants.CHANNEL_COZE, Constants.CHANNEL_DEEPSEEK)\n val customJson = securePrefs.getString(Constants.KEY_CUSTOM_API_LIST, \"[]\") ?: \"[]\"\n try {\n val type = object : com.google.gson.reflect.TypeToken>() {}.type\n val customList: List = com.google.gson.Gson().fromJson(customJson, type)\n for (c in customList) {\n channels.add(\"${Constants.CHANNEL_CUSTOM_PREFIX}${c.id}\")\n }\n } catch (_: Exception) {}\n return channels\n }\n\n /**\n * 获取当前通道的显示标签\n */\n fun getChannelLabel(): String {\n val ch = _currentChannel.value\n return when {\n ch == Constants.CHANNEL_COZE -> \"🧠 Coze\"\n ch == Constants.CHANNEL_DEEPSEEK -> \"DeepSeek\"\n ch.startsWith(Constants.CHANNEL_CUSTOM_PREFIX) -> {\n val id = ch.removePrefix(Constants.CHANNEL_CUSTOM_PREFIX)\n val customJson = securePrefs.getString(Constants.KEY_CUSTOM_API_LIST, \"[]\") ?: \"[]\"\n try {\n val type = object : com.google.gson.reflect.TypeToken>() {}.type\n val list: List = com.google.gson.Gson().fromJson(customJson, type)\n val config = list.find { it.id == id }\n \"${config?.emoji ?: \"🔌\"} ${config?.name ?: \"自定义\"}\"\n } catch (_: Exception) { \"🔌 自定义\" }\n }\n else -> \"🧠 Coze\"\n }\n }\n\n /**\n * 循环切换到下一个通道\n */\n fun cycleChannel() {\n val channels = getAvailableChannels()\n val currentIdx = channels.indexOf(_currentChannel.value)\n val nextIdx = if (currentIdx < 0 || currentIdx >= channels.size - 1) 0 else currentIdx + 1\n val nextChannel = channels[nextIdx]\n securePrefs.edit().putString(Constants.KEY_AI_CHANNEL, nextChannel).apply()\n _currentChannel.value = nextChannel\n }\n\n private fun checkCozeConfig() {\n viewModelScope.launch {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n // 使用默认值回退:用户未配置时自动使用内置的 PAT 和 Bot ID\n val effectivePat = savedPat.ifBlank { Constants.DEFAULT_COZE_PAT }\n val effectiveBotId = savedBotId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n _isCozeConfigured.value = effectivePat.isNotBlank() && effectiveBotId.isNotBlank()\n }\n }\n\n fun refreshCozeConfig() {\n checkCozeConfig()\n loadChannel()\n }\n\n // ============ 发送消息 ============\n fun sendMessage() {\n val text = _inputText.value.trim()\n val imageUri = _pendingImageUri.value\n\n if (text.isEmpty() && imageUri == null) return\n\n val channel = _currentChannel.value\n if (channel == Constants.CHANNEL_COZE && !_isCozeConfigured.value) {\n _statusMessage.value = \"请先在设置中配置 Coze PAT 和 Bot ID\"\n return\n }\n if (channel == Constants.CHANNEL_DEEPSEEK) {\n val apiKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n if (apiKey.isBlank()) {\n _statusMessage.value = \"请先在设置中配置 DeepSeek API Key\"\n return\n }\n }\n\n // 生成对话名称\n autoGenerateConversationName(text.ifEmpty { \"[图片]\" })\n\n // 添加用户消息到列表\n val replyMsg = _replyToMessage.value\n val replyPrefix = if (replyMsg != null) {\n val preview = replyMsg.text.take(50).replace(\"\\n\", \" \")\n \"[引用: ${preview}]\\n\\n\"\n } else \"\"\n val userMessage = ChatMessage(\n text = text.ifEmpty { \"[图片]\" },\n isUser = true,\n imageUri = imageUri,\n replyToId = replyMsg?.id\n )\n _messages.value = (_messages.value + userMessage).takeLast(100)\n _inputText.value = \"\"\n _pendingImageUri.value = null\n _replyToMessage.value = null\n _isLoading.value = true\n\n // 保存用户消息到数据库\n viewModelScope.launch(Dispatchers.IO) {\n try {\n chatMessageDao.insert(ChatMessageEntity(\n text = userMessage.text,\n isUser = true,\n imageUri = userMessage.imageUri,\n timestamp = userMessage.timestamp,\n sessionId = currentSessionId\n ))\n loadSessionList()\n } catch (_: Exception) {}\n }\n\n // 构建消息内容(如果有图片,先分析图片)\n viewModelScope.launch {\n var messageContent = replyPrefix + text\n if (imageUri != null) {\n _statusMessage.value = \"🖼️ 正在处理图片...\"\n val imageDescription = processImage(imageUri)\n messageContent = if (text.isNotEmpty()) {\n \"$text\\n\\n[用户发送了一张图片,图片内容描述: $imageDescription]\"\n } else {\n \"[用户发送了一张图片,图片内容描述: $imageDescription]\"\n }\n }\n\n // 根据通道选择调用\n when {\n channel == Constants.CHANNEL_DEEPSEEK -> streamChatFromDeepSeek(messageContent)\n channel.startsWith(Constants.CHANNEL_CUSTOM_PREFIX) -> {\n val customId = channel.removePrefix(Constants.CHANNEL_CUSTOM_PREFIX)\n val customJson = securePrefs.getString(Constants.KEY_CUSTOM_API_LIST, \"[]\") ?: \"[]\"\n try {\n val type = object : com.google.gson.reflect.TypeToken>() {}.type\n val list: List = com.google.gson.Gson().fromJson(customJson, type)\n val config = list.find { it.id == customId }\n if (config != null) {\n streamChatFromCustom(config, messageContent)\n } else {\n _statusMessage.value = \"❌ 自定义 API 配置不存在\"\n _isLoading.value = false\n }\n } catch (_: Exception) {\n _statusMessage.value = \"❌ 读取自定义配置失败\"\n _isLoading.value = false\n }\n }\n else -> streamChatFromCoze(messageContent)\n }\n }\n }\n\n // ============ DeepSeek 直连 ============\n private suspend fun streamChatFromDeepSeek(message: String) {\n var aiMessageId: Long? = null\n var fullResponse = StringBuilder()\n\n try {\n val apiKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n val systemPrompt = securePrefs.getString(\"deepseek_system_prompt\", \"\") ?: \"\"\n val dsModel = securePrefs.getString(Constants.KEY_DEEPSEEK_MODEL, \"deepseek-v4-flash\") ?: \"deepseek-v4-flash\"\n\n val history = buildHistoryFromMessages()\n\n cozeClient.streamDeepSeekChat(\n apiKey = apiKey,\n message = message,\n model = dsModel,\n systemPrompt = systemPrompt.ifBlank { null },\n history = history\n ).collect { event ->\n when (event) {\n is StreamEvent.Status -> {\n _statusMessage.value = event.message\n }\n is StreamEvent.Delta -> {\n if (aiMessageId == null) {\n aiMessageId = System.nanoTime()\n val newMsg = ChatMessage(id = aiMessageId!!, text = \"\", isUser = false)\n _messages.value = (_messages.value + newMsg).takeLast(100)\n }\n fullResponse.append(event.text)\n val id = aiMessageId!!\n _messages.value = _messages.value.map { msg ->\n if (msg.id == id) msg.copy(text = fullResponse.toString()) else msg\n }\n }\n is StreamEvent.Done -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else if (aiMessageId == null) {\n // 没有收到任何内容,创建一条提示\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ AI 未返回内容,请重试\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.Error -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ ${event.message}\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.TokenUsage -> {\n accumulateTokenUsage(event.total, event.input, event.output)\n }\n }\n }\n } catch (e: Exception) {\n _isLoading.value = false\n _statusMessage.value = null\n val errorMsg = \"❌ DeepSeek 连接失败: ${e.message ?: \"未知错误\"}\"\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = errorMsg, isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n }\n\n // ============ 自定义 API 流式对话 ============\n private suspend fun streamChatFromCustom(config: com.meitu.generator.ui.settings.CustomApiConfig, message: String) {\n var aiMessageId: Long? = null\n var fullResponse = StringBuilder()\n\n try {\n val systemPrompt = securePrefs.getString(\"deepseek_system_prompt\", \"\") ?: \"\"\n val history = buildHistoryFromMessages()\n\n cozeClient.streamCustomChat(\n apiKey = config.apiKey,\n message = message,// 确保传入实际用户消息\n model = config.model,\n baseUrl = config.baseUrl,\n systemPrompt = systemPrompt.ifBlank { null },\n history = history\n ).collect { event ->\n when (event) {\n is StreamEvent.Status -> {\n _statusMessage.value = event.message\n }\n is StreamEvent.Delta -> {\n if (aiMessageId == null) {\n aiMessageId = System.nanoTime()\n val newMsg = ChatMessage(id = aiMessageId!!, text = \"\", isUser = false)\n _messages.value = (_messages.value + newMsg).takeLast(100)\n }\n fullResponse.append(event.text)\n val id = aiMessageId!!\n _messages.value = _messages.value.map { msg ->\n if (msg.id == id) msg.copy(text = fullResponse.toString()) else msg\n }\n }\n is StreamEvent.Done -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else if (aiMessageId == null) {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ API 未返回内容,请检查配置\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.Error -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ ${event.message}\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.TokenUsage -> {\n accumulateTokenUsage(event.total, event.input, event.output)\n }\n }\n }\n } catch (e: Exception) {\n _isLoading.value = false\n _statusMessage.value = null\n val errorMsg = \"❌ 自定义 API 连接失败: ${e.message ?: \"未知错误\"}\"\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = errorMsg, isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n }\n\n // ============ Coze 流式对话 ============\n private suspend fun streamChatFromCoze(message: String) {\n var aiMessageId: Long? = null\n var fullResponse = StringBuilder()\n\n try {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n val pat = savedPat.ifBlank { Constants.DEFAULT_COZE_PAT }\n val botId = savedBotId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n\n val client = CozeApiClient(\n baseUrl = Constants.COZE_API_BASE_URL,\n pat = pat,\n botId = botId,\n httpClient = provideTempHttpClient()\n )\n\n client.streamChat(\n message = message,\n conversationId = cozeConversationId\n ).collect { event ->\n when (event) {\n is StreamEvent.Status -> {\n _statusMessage.value = event.message\n }\n is StreamEvent.Delta -> {\n if (aiMessageId == null) {\n aiMessageId = System.nanoTime()\n val newMsg = ChatMessage(id = aiMessageId!!, text = \"\", isUser = false)\n _messages.value = (_messages.value + newMsg).takeLast(100)\n }\n fullResponse.append(event.text)\n val id = aiMessageId!!\n _messages.value = _messages.value.map { msg ->\n if (msg.id == id) msg.copy(text = fullResponse.toString()) else msg\n }\n }\n is StreamEvent.Done -> {\n cozeConversationId = event.conversationId\n _statusMessage.value = null\n _isLoading.value = false\n\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else if (aiMessageId == null) {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ AI 未返回内容,请重试\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.Error -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ ${event.message}\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.TokenUsage -> {\n accumulateTokenUsage(event.total, event.input, event.output)\n }\n }\n }\n } catch (e: Exception) {\n _isLoading.value = false\n _statusMessage.value = null\n val errorMsg = \"❌ 连接失败: ${e.message ?: \"未知错误\"}\"\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = errorMsg, isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n }\n\n private fun provideTempHttpClient(): okhttp3.OkHttpClient {\n return okhttp3.OkHttpClient.Builder()\n .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)\n .readTimeout(120, java.util.concurrent.TimeUnit.SECONDS)\n .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS)\n .build()\n }\n\n private suspend fun saveAiMessage(text: String) {\n withContext(Dispatchers.IO) {\n try {\n chatMessageDao.insert(ChatMessageEntity(\n text = text,\n isUser = false,\n timestamp = System.currentTimeMillis(),\n sessionId = currentSessionId\n ))\n } catch (_: Exception) {}\n }\n }\n\n // ============ 构建历史对话 ============\n private fun buildHistoryFromMessages(): List> {\n val history = mutableListOf>()\n val chatMessages = _messages.value.filter { !it.isSystem }\n // 取最近的对话(排除空的AI占位消息)\n for (msg in chatMessages) {\n if (msg.text.isNotBlank()) {\n history.add(Pair(if (msg.isUser) \"user\" else \"assistant\", msg.text))\n }\n }\n return history\n }\n\n // ============ Token 消耗累计(分通道,支持自定义) ============\n companion object {\n // Coze 通道\n private const val KEY_COZE_TOTAL_TOKENS = \"coze_total_tokens\"\n private const val KEY_COZE_TOTAL_INPUT = \"coze_total_input\"\n private const val KEY_COZE_TOTAL_OUTPUT = \"coze_total_output\"\n private const val KEY_COZE_TOTAL_MESSAGES = \"coze_total_messages\"\n // DeepSeek 通道\n private const val KEY_DS_TOTAL_TOKENS = \"ds_total_tokens\"\n private const val KEY_DS_TOTAL_INPUT = \"ds_total_input\"\n private const val KEY_DS_TOTAL_OUTPUT = \"ds_total_output\"\n private const val KEY_DS_TOTAL_MESSAGES = \"ds_total_messages\"\n // 自定义通道前缀\n private const val KEY_CUSTOM_PREFIX = \"custom:\"\n }\n\n private fun accumulateTokenUsage(total: Int, input: Int, output: Int) {\n val channel = _currentChannel.value\n val editor = securePrefs.edit()\n\n val prefix = when {\n channel == Constants.CHANNEL_COZE -> \"coze\"\n channel == Constants.CHANNEL_DEEPSEEK -> \"ds\"\n channel.startsWith(KEY_CUSTOM_PREFIX) -> channel // custom:{id}\n else -> \"coze\"\n }\n\n editor.putInt(\"${prefix}_total_tokens\", securePrefs.getInt(\"${prefix}_total_tokens\", 0) + total)\n editor.putInt(\"${prefix}_total_input\", securePrefs.getInt(\"${prefix}_total_input\", 0) + input)\n editor.putInt(\"${prefix}_total_output\", securePrefs.getInt(\"${prefix}_total_output\", 0) + output)\n editor.putInt(\"${prefix}_total_messages\", securePrefs.getInt(\"${prefix}_total_messages\", 0) + 1)\n editor.apply()\n }\n\n private suspend fun processImage(uri: String): String {\n return withContext(Dispatchers.IO) {\n try {\n val context = getApplication()\n val inputStream = context.contentResolver.openInputStream(Uri.parse(uri))\n val bitmap = BitmapFactory.decodeStream(inputStream)\n inputStream?.close()\n\n if (bitmap != null) {\n val byteArrayOutputStream = ByteArrayOutputStream()\n bitmap.compress(Bitmap.CompressFormat.JPEG, 80, byteArrayOutputStream)\n val base64 = Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)\n \"图片已处理(${bitmap.width}x${bitmap.height})\"\n } else {\n \"图片无法解析\"\n }\n } catch (e: Exception) {\n \"图片处理失败: ${e.message}\"\n }\n }\n }\n\n // ============ 新建对话 ============\n fun newConversation() {\n streamJob?.cancel()\n currentSessionId = System.currentTimeMillis()\n cozeConversationId = null\n _conversationName.value = \"新对话\"\n _messages.value = listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n _isLoading.value = false\n _statusMessage.value = null\n loadSessionList()\n }\n\n // ============ 清空当前对话 ============\n fun clearCurrentChat() {\n streamJob?.cancel()\n _messages.value = listOf(ChatMessage(\n text = \"对话已清空,开始新的对话吧 💬\",\n isUser = false\n ))\n _isLoading.value = false\n _statusMessage.value = null\n\n viewModelScope.launch(Dispatchers.IO) {\n try {\n chatMessageDao.deleteBySession(currentSessionId)\n } catch (_: Exception) {}\n }\n }\n\n // ============ 从数据库加载消息 ============\n private fun loadMessagesFromDb() {\n viewModelScope.launch {\n withContext(Dispatchers.IO) {\n val entities = chatMessageDao.getMessagesBySession(currentSessionId)\n val chatMessages = entities.map { entity ->\n ChatMessage(\n text = entity.text,\n isUser = entity.isUser,\n imageUri = entity.imageUri,\n timestamp = entity.timestamp\n )\n }\n if (chatMessages.isNotEmpty()) {\n _messages.value = chatMessages\n }\n }\n }\n }\n\n override fun onCleared() {\n super.onCleared()\n streamJob?.cancel()\n }\n}\n"}, "8fe69ce2c59395da611f57403b80e7efe6468114": {"t": 1785755783.4353476, "n": 29000, "h": "54155ab011836df1", "c": "package com.meitu.generator.ui.assistant\n\nimport android.app.Application\nimport android.content.SharedPreferences\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.net.Uri\nimport android.util.Base64\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.meitu.generator.data.remote.CozeApiClient\nimport com.meitu.generator.data.remote.StreamEvent\nimport com.meitu.generator.data.local.dao.ChatMessageDao\nimport com.meitu.generator.data.local.dao.SessionSummary\nimport com.meitu.generator.data.local.entity.ChatMessageEntity\nimport com.meitu.generator.repository.SettingsRepository\nimport com.meitu.generator.util.Constants\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport java.io.ByteArrayOutputStream\nimport javax.inject.Inject\nimport javax.inject.Named\n\ndata class ChatMessage(\n val id: Long = System.nanoTime(),\n val text: String,\n val isUser: Boolean,\n val isSystem: Boolean = false,\n val imageUri: String? = null,\n val timestamp: Long = System.currentTimeMillis(),\n val replyToId: Long? = null\n)\n\n@HiltViewModel\nclass AssistantViewModel @Inject constructor(\n application: Application,\n private val settingsRepo: SettingsRepository,\n private val cozeClient: CozeApiClient,\n @Named(\"securePrefs\") private val securePrefs: SharedPreferences,\n private val chatMessageDao: ChatMessageDao\n) : AndroidViewModel(application) {\n\n // ============ Chat Messages ============\n private val _messages = MutableStateFlow>(\n listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n )\n val messages: StateFlow> = _messages.asStateFlow()\n\n // ============ 会话管理 ============\n private var currentSessionId: Long = System.currentTimeMillis()\n private var cozeConversationId: String? = null\n\n // ============ 对话名称 ============\n private val _conversationName = MutableStateFlow(\"新对话\")\n val conversationName: StateFlow = _conversationName.asStateFlow()\n\n fun setConversationName(name: String) {\n _conversationName.value = name\n }\n\n fun autoGenerateConversationName(firstMessage: String) {\n if (_conversationName.value == \"新对话\" && firstMessage.isNotBlank()) {\n val name = if (firstMessage.length > 15) firstMessage.take(15) + \"...\" else firstMessage\n _conversationName.value = name\n }\n }\n\n // ============ 历史对话列表 ============\n private val _sessionList = MutableStateFlow>(emptyList())\n val sessionList: StateFlow> = _sessionList.asStateFlow()\n\n fun loadSessionList() {\n viewModelScope.launch {\n withContext(Dispatchers.IO) {\n val sessions = chatMessageDao.getSessionSummaries()\n _sessionList.value = sessions\n }\n }\n }\n\n fun switchToSession(sessionId: Long) {\n currentSessionId = sessionId\n cozeConversationId = null // 切换会话时重置 Coze 对话 ID\n viewModelScope.launch {\n loadMessagesFromDb()\n // 恢复对话名称\n val messages = _messages.value\n val firstUserMsg = messages.firstOrNull { it.isUser }\n _conversationName.value = if (firstUserMsg != null) {\n if (firstUserMsg.text.length > 15) firstUserMsg.text.take(15) + \"...\" else firstUserMsg.text\n } else {\n \"历史对话\"\n }\n }\n }\n\n private val _inputText = MutableStateFlow(\"\")\n val inputText: StateFlow = _inputText.asStateFlow()\n\n fun setInputText(text: String) {\n _inputText.value = text\n }\n\n // ============ 附件(图片) ============\n private val _pendingImageUri = MutableStateFlow(null)\n val pendingImageUri: StateFlow = _pendingImageUri.asStateFlow()\n\n fun setPendingImageUri(uri: String?) {\n _pendingImageUri.value = uri\n }\n\n // ============ Loading ============\n private val _isLoading = MutableStateFlow(false)\n val isLoading: StateFlow = _isLoading.asStateFlow()\n\n // ============ 引用回复 ============\n private val _replyToMessage = MutableStateFlow(null)\n val replyToMessage: StateFlow = _replyToMessage.asStateFlow()\n\n fun setReplyToMessage(msg: ChatMessage?) {\n _replyToMessage.value = msg\n }\n\n fun clearReplyToMessage() {\n _replyToMessage.value = null\n }\n\n // ============ 状态 ============\n private val _statusMessage = MutableStateFlow(null)\n val statusMessage: StateFlow = _statusMessage.asStateFlow()\n\n // ============ 当前流式任务的 Job ============\n private var streamJob: Job? = null\n\n // ============ Coze 配置状态 ============\n private val _isCozeConfigured = MutableStateFlow(true)\n val isCozeConfigured: StateFlow = _isCozeConfigured.asStateFlow()\n\n // ============ 当前 AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow = _currentChannel.asStateFlow()\n\n init {\n checkCozeConfig()\n loadChannel()\n loadSessionList()\n }\n\n private fun loadChannel() {\n val channel = securePrefs.getString(Constants.KEY_AI_CHANNEL, Constants.CHANNEL_COZE) ?: Constants.CHANNEL_COZE\n _currentChannel.value = channel\n }\n\n /**\n * 获取所有可用通道列表,用于循环切换\n */\n private fun getAvailableChannels(): List {\n val channels = mutableListOf(Constants.CHANNEL_COZE, Constants.CHANNEL_DEEPSEEK)\n val customJson = securePrefs.getString(Constants.KEY_CUSTOM_API_LIST, \"[]\") ?: \"[]\"\n try {\n val type = object : com.google.gson.reflect.TypeToken>() {}.type\n val customList: List = com.google.gson.Gson().fromJson(customJson, type)\n for (c in customList) {\n channels.add(\"${Constants.CHANNEL_CUSTOM_PREFIX}${c.id}\")\n }\n } catch (_: Exception) {}\n return channels\n }\n\n /**\n * 获取当前通道的显示标签\n */\n fun getChannelLabel(): String {\n val ch = _currentChannel.value\n return when {\n ch == Constants.CHANNEL_COZE -> \"🧠 Coze\"\n ch == Constants.CHANNEL_DEEPSEEK -> \"DeepSeek\"\n ch.startsWith(Constants.CHANNEL_CUSTOM_PREFIX) -> {\n val id = ch.removePrefix(Constants.CHANNEL_CUSTOM_PREFIX)\n val customJson = securePrefs.getString(Constants.KEY_CUSTOM_API_LIST, \"[]\") ?: \"[]\"\n try {\n val type = object : com.google.gson.reflect.TypeToken>() {}.type\n val list: List = com.google.gson.Gson().fromJson(customJson, type)\n val config = list.find { it.id == id }\n \"${config?.emoji ?: \"🔌\"} ${config?.name ?: \"自定义\"}\"\n } catch (_: Exception) { \"🔌 自定义\" }\n }\n else -> \"🧠 Coze\"\n }\n }\n\n /**\n * 循环切换到下一个通道\n */\n fun cycleChannel() {\n val channels = getAvailableChannels()\n val currentIdx = channels.indexOf(_currentChannel.value)\n val nextIdx = if (currentIdx < 0 || currentIdx >= channels.size - 1) 0 else currentIdx + 1\n val nextChannel = channels[nextIdx]\n securePrefs.edit().putString(Constants.KEY_AI_CHANNEL, nextChannel).apply()\n _currentChannel.value = nextChannel\n }\n\n private fun checkCozeConfig() {\n viewModelScope.launch {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n // 使用默认值回退:用户未配置时自动使用内置的 PAT 和 Bot ID\n val effectivePat = savedPat.ifBlank { Constants.DEFAULT_COZE_PAT }\n val effectiveBotId = savedBotId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n _isCozeConfigured.value = effectivePat.isNotBlank() && effectiveBotId.isNotBlank()\n }\n }\n\n fun refreshCozeConfig() {\n checkCozeConfig()\n loadChannel()\n }\n\n // ============ 发送消息 ============\n fun sendMessage() {\n val text = _inputText.value.trim()\n val imageUri = _pendingImageUri.value\n\n if (text.isEmpty() && imageUri == null) return\n\n val channel = _currentChannel.value\n if (channel == Constants.CHANNEL_COZE && !_isCozeConfigured.value) {\n _statusMessage.value = \"请先在设置中配置 Coze PAT 和 Bot ID\"\n return\n }\n if (channel == Constants.CHANNEL_DEEPSEEK) {\n val apiKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n if (apiKey.isBlank()) {\n _statusMessage.value = \"请先在设置中配置 DeepSeek API Key\"\n return\n }\n }\n\n // 生成对话名称\n autoGenerateConversationName(text.ifEmpty { \"[图片]\" })\n\n // 添加用户消息到列表\n val replyMsg = _replyToMessage.value\n val replyPrefix = if (replyMsg != null) {\n val preview = replyMsg.text.take(50).replace(\"\\n\", \" \")\n \"[引用: ${preview}]\\n\\n\"\n } else \"\"\n val userMessage = ChatMessage(\n text = text.ifEmpty { \"[图片]\" },\n isUser = true,\n imageUri = imageUri,\n replyToId = replyMsg?.id\n )\n _messages.value = (_messages.value + userMessage).takeLast(100)\n _inputText.value = \"\"\n _pendingImageUri.value = null\n _replyToMessage.value = null\n _isLoading.value = true\n\n // 保存用户消息到数据库\n viewModelScope.launch(Dispatchers.IO) {\n try {\n chatMessageDao.insert(ChatMessageEntity(\n text = userMessage.text,\n isUser = true,\n imageUri = userMessage.imageUri,\n timestamp = userMessage.timestamp,\n sessionId = currentSessionId\n ))\n loadSessionList()\n } catch (_: Exception) {}\n }\n\n // 构建消息内容(如果有图片,先分析图片)\n viewModelScope.launch {\n var messageContent = replyPrefix + text\n if (imageUri != null) {\n _statusMessage.value = \"🖼️ 正在处理图片...\"\n val imageDescription = processImage(imageUri)\n messageContent = if (text.isNotEmpty()) {\n \"$text\\n\\n[用户发送了一张图片,图片内容描述: $imageDescription]\"\n } else {\n \"[用户发送了一张图片,图片内容描述: $imageDescription]\"\n }\n }\n\n // 根据通道选择调用\n when {\n channel == Constants.CHANNEL_DEEPSEEK -> streamChatFromDeepSeek(messageContent)\n channel.startsWith(Constants.CHANNEL_CUSTOM_PREFIX) -> {\n val customId = channel.removePrefix(Constants.CHANNEL_CUSTOM_PREFIX)\n val customJson = securePrefs.getString(Constants.KEY_CUSTOM_API_LIST, \"[]\") ?: \"[]\"\n try {\n val type = object : com.google.gson.reflect.TypeToken>() {}.type\n val list: List = com.google.gson.Gson().fromJson(customJson, type)\n val config = list.find { it.id == customId }\n if (config != null) {\n streamChatFromCustom(config)\n } else {\n _statusMessage.value = \"❌ 自定义 API 配置不存在\"\n _isLoading.value = false\n }\n } catch (_: Exception) {\n _statusMessage.value = \"❌ 读取自定义配置失败\"\n _isLoading.value = false\n }\n }\n else -> streamChatFromCoze(messageContent)\n }\n }\n }\n\n // ============ DeepSeek 直连 ============\n private suspend fun streamChatFromDeepSeek(message: String) {\n var aiMessageId: Long? = null\n var fullResponse = StringBuilder()\n\n try {\n val apiKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n val systemPrompt = securePrefs.getString(\"deepseek_system_prompt\", \"\") ?: \"\"\n val dsModel = securePrefs.getString(Constants.KEY_DEEPSEEK_MODEL, \"deepseek-v4-flash\") ?: \"deepseek-v4-flash\"\n\n val history = buildHistoryFromMessages()\n\n cozeClient.streamDeepSeekChat(\n apiKey = apiKey,\n message = message,\n model = dsModel,\n systemPrompt = systemPrompt.ifBlank { null },\n history = history\n ).collect { event ->\n when (event) {\n is StreamEvent.Status -> {\n _statusMessage.value = event.message\n }\n is StreamEvent.Delta -> {\n if (aiMessageId == null) {\n aiMessageId = System.nanoTime()\n val newMsg = ChatMessage(id = aiMessageId!!, text = \"\", isUser = false)\n _messages.value = (_messages.value + newMsg).takeLast(100)\n }\n fullResponse.append(event.text)\n val id = aiMessageId!!\n _messages.value = _messages.value.map { msg ->\n if (msg.id == id) msg.copy(text = fullResponse.toString()) else msg\n }\n }\n is StreamEvent.Done -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else if (aiMessageId == null) {\n // 没有收到任何内容,创建一条提示\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ AI 未返回内容,请重试\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.Error -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ ${event.message}\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.TokenUsage -> {\n accumulateTokenUsage(event.total, event.input, event.output)\n }\n }\n }\n } catch (e: Exception) {\n _isLoading.value = false\n _statusMessage.value = null\n val errorMsg = \"❌ DeepSeek 连接失败: ${e.message ?: \"未知错误\"}\"\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = errorMsg, isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n }\n\n // ============ 自定义 API 流式对话 ============\n private suspend fun streamChatFromCustom(config: com.meitu.generator.ui.settings.CustomApiConfig, message: String) {\n var aiMessageId: Long? = null\n var fullResponse = StringBuilder()\n\n try {\n val systemPrompt = securePrefs.getString(\"deepseek_system_prompt\", \"\") ?: \"\"\n val history = buildHistoryFromMessages()\n\n cozeClient.streamCustomChat(\n apiKey = config.apiKey,\n message = message,// 确保传入实际用户消息\n model = config.model,\n baseUrl = config.baseUrl,\n systemPrompt = systemPrompt.ifBlank { null },\n history = history\n ).collect { event ->\n when (event) {\n is StreamEvent.Status -> {\n _statusMessage.value = event.message\n }\n is StreamEvent.Delta -> {\n if (aiMessageId == null) {\n aiMessageId = System.nanoTime()\n val newMsg = ChatMessage(id = aiMessageId!!, text = \"\", isUser = false)\n _messages.value = (_messages.value + newMsg).takeLast(100)\n }\n fullResponse.append(event.text)\n val id = aiMessageId!!\n _messages.value = _messages.value.map { msg ->\n if (msg.id == id) msg.copy(text = fullResponse.toString()) else msg\n }\n }\n is StreamEvent.Done -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else if (aiMessageId == null) {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ API 未返回内容,请检查配置\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.Error -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ ${event.message}\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.TokenUsage -> {\n accumulateTokenUsage(event.total, event.input, event.output)\n }\n }\n }\n } catch (e: Exception) {\n _isLoading.value = false\n _statusMessage.value = null\n val errorMsg = \"❌ 自定义 API 连接失败: ${e.message ?: \"未知错误\"}\"\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = errorMsg, isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n }\n\n // ============ Coze 流式对话 ============\n private suspend fun streamChatFromCoze(message: String) {\n var aiMessageId: Long? = null\n var fullResponse = StringBuilder()\n\n try {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n val pat = savedPat.ifBlank { Constants.DEFAULT_COZE_PAT }\n val botId = savedBotId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n\n val client = CozeApiClient(\n baseUrl = Constants.COZE_API_BASE_URL,\n pat = pat,\n botId = botId,\n httpClient = provideTempHttpClient()\n )\n\n client.streamChat(\n message = message,\n conversationId = cozeConversationId\n ).collect { event ->\n when (event) {\n is StreamEvent.Status -> {\n _statusMessage.value = event.message\n }\n is StreamEvent.Delta -> {\n if (aiMessageId == null) {\n aiMessageId = System.nanoTime()\n val newMsg = ChatMessage(id = aiMessageId!!, text = \"\", isUser = false)\n _messages.value = (_messages.value + newMsg).takeLast(100)\n }\n fullResponse.append(event.text)\n val id = aiMessageId!!\n _messages.value = _messages.value.map { msg ->\n if (msg.id == id) msg.copy(text = fullResponse.toString()) else msg\n }\n }\n is StreamEvent.Done -> {\n cozeConversationId = event.conversationId\n _statusMessage.value = null\n _isLoading.value = false\n\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else if (aiMessageId == null) {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ AI 未返回内容,请重试\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.Error -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ ${event.message}\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.TokenUsage -> {\n accumulateTokenUsage(event.total, event.input, event.output)\n }\n }\n }\n } catch (e: Exception) {\n _isLoading.value = false\n _statusMessage.value = null\n val errorMsg = \"❌ 连接失败: ${e.message ?: \"未知错误\"}\"\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = errorMsg, isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n }\n\n private fun provideTempHttpClient(): okhttp3.OkHttpClient {\n return okhttp3.OkHttpClient.Builder()\n .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)\n .readTimeout(120, java.util.concurrent.TimeUnit.SECONDS)\n .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS)\n .build()\n }\n\n private suspend fun saveAiMessage(text: String) {\n withContext(Dispatchers.IO) {\n try {\n chatMessageDao.insert(ChatMessageEntity(\n text = text,\n isUser = false,\n timestamp = System.currentTimeMillis(),\n sessionId = currentSessionId\n ))\n } catch (_: Exception) {}\n }\n }\n\n // ============ 构建历史对话 ============\n private fun buildHistoryFromMessages(): List> {\n val history = mutableListOf>()\n val chatMessages = _messages.value.filter { !it.isSystem }\n // 取最近的对话(排除空的AI占位消息)\n for (msg in chatMessages) {\n if (msg.text.isNotBlank()) {\n history.add(Pair(if (msg.isUser) \"user\" else \"assistant\", msg.text))\n }\n }\n return history\n }\n\n // ============ Token 消耗累计(分通道,支持自定义) ============\n companion object {\n // Coze 通道\n private const val KEY_COZE_TOTAL_TOKENS = \"coze_total_tokens\"\n private const val KEY_COZE_TOTAL_INPUT = \"coze_total_input\"\n private const val KEY_COZE_TOTAL_OUTPUT = \"coze_total_output\"\n private const val KEY_COZE_TOTAL_MESSAGES = \"coze_total_messages\"\n // DeepSeek 通道\n private const val KEY_DS_TOTAL_TOKENS = \"ds_total_tokens\"\n private const val KEY_DS_TOTAL_INPUT = \"ds_total_input\"\n private const val KEY_DS_TOTAL_OUTPUT = \"ds_total_output\"\n private const val KEY_DS_TOTAL_MESSAGES = \"ds_total_messages\"\n // 自定义通道前缀\n private const val KEY_CUSTOM_PREFIX = \"custom:\"\n }\n\n private fun accumulateTokenUsage(total: Int, input: Int, output: Int) {\n val channel = _currentChannel.value\n val editor = securePrefs.edit()\n\n val prefix = when {\n channel == Constants.CHANNEL_COZE -> \"coze\"\n channel == Constants.CHANNEL_DEEPSEEK -> \"ds\"\n channel.startsWith(KEY_CUSTOM_PREFIX) -> channel // custom:{id}\n else -> \"coze\"\n }\n\n editor.putInt(\"${prefix}_total_tokens\", securePrefs.getInt(\"${prefix}_total_tokens\", 0) + total)\n editor.putInt(\"${prefix}_total_input\", securePrefs.getInt(\"${prefix}_total_input\", 0) + input)\n editor.putInt(\"${prefix}_total_output\", securePrefs.getInt(\"${prefix}_total_output\", 0) + output)\n editor.putInt(\"${prefix}_total_messages\", securePrefs.getInt(\"${prefix}_total_messages\", 0) + 1)\n editor.apply()\n }\n\n private suspend fun processImage(uri: String): String {\n return withContext(Dispatchers.IO) {\n try {\n val context = getApplication()\n val inputStream = context.contentResolver.openInputStream(Uri.parse(uri))\n val bitmap = BitmapFactory.decodeStream(inputStream)\n inputStream?.close()\n\n if (bitmap != null) {\n val byteArrayOutputStream = ByteArrayOutputStream()\n bitmap.compress(Bitmap.CompressFormat.JPEG, 80, byteArrayOutputStream)\n val base64 = Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)\n \"图片已处理(${bitmap.width}x${bitmap.height})\"\n } else {\n \"图片无法解析\"\n }\n } catch (e: Exception) {\n \"图片处理失败: ${e.message}\"\n }\n }\n }\n\n // ============ 新建对话 ============\n fun newConversation() {\n streamJob?.cancel()\n currentSessionId = System.currentTimeMillis()\n cozeConversationId = null\n _conversationName.value = \"新对话\"\n _messages.value = listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n _isLoading.value = false\n _statusMessage.value = null\n loadSessionList()\n }\n\n // ============ 清空当前对话 ============\n fun clearCurrentChat() {\n streamJob?.cancel()\n _messages.value = listOf(ChatMessage(\n text = \"对话已清空,开始新的对话吧 💬\",\n isUser = false\n ))\n _isLoading.value = false\n _statusMessage.value = null\n\n viewModelScope.launch(Dispatchers.IO) {\n try {\n chatMessageDao.deleteBySession(currentSessionId)\n } catch (_: Exception) {}\n }\n }\n\n // ============ 从数据库加载消息 ============\n private fun loadMessagesFromDb() {\n viewModelScope.launch {\n withContext(Dispatchers.IO) {\n val entities = chatMessageDao.getMessagesBySession(currentSessionId)\n val chatMessages = entities.map { entity ->\n ChatMessage(\n text = entity.text,\n isUser = entity.isUser,\n imageUri = entity.imageUri,\n timestamp = entity.timestamp\n )\n }\n if (chatMessages.isNotEmpty()) {\n _messages.value = chatMessages\n }\n }\n }\n }\n\n override fun onCleared() {\n super.onCleared()\n streamJob?.cancel()\n }\n}\n"}, "a9598c8ea173ef4910307bdc33efe4184b99ed96": {"t": 1785755784.9637053, "n": 28306, "h": "1ad03e61d2f5e7fb", "c": "package com.meitu.generator.ui.assistant\n\nimport android.app.Application\nimport android.content.SharedPreferences\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.net.Uri\nimport android.util.Base64\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.meitu.generator.data.remote.CozeApiClient\nimport com.meitu.generator.data.remote.StreamEvent\nimport com.meitu.generator.data.local.dao.ChatMessageDao\nimport com.meitu.generator.data.local.dao.SessionSummary\nimport com.meitu.generator.data.local.entity.ChatMessageEntity\nimport com.meitu.generator.repository.SettingsRepository\nimport com.meitu.generator.util.Constants\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport java.io.ByteArrayOutputStream\nimport javax.inject.Inject\nimport javax.inject.Named\n\ndata class ChatMessage(\n val id: Long = System.nanoTime(),\n val text: String,\n val isUser: Boolean,\n val isSystem: Boolean = false,\n val imageUri: String? = null,\n val timestamp: Long = System.currentTimeMillis()\n)\n\n@HiltViewModel\nclass AssistantViewModel @Inject constructor(\n application: Application,\n private val settingsRepo: SettingsRepository,\n private val cozeClient: CozeApiClient,\n @Named(\"securePrefs\") private val securePrefs: SharedPreferences,\n private val chatMessageDao: ChatMessageDao\n) : AndroidViewModel(application) {\n\n // ============ Chat Messages ============\n private val _messages = MutableStateFlow>(\n listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n )\n val messages: StateFlow> = _messages.asStateFlow()\n\n // ============ 会话管理 ============\n private var currentSessionId: Long = System.currentTimeMillis()\n private var cozeConversationId: String? = null\n\n // ============ 对话名称 ============\n private val _conversationName = MutableStateFlow(\"新对话\")\n val conversationName: StateFlow = _conversationName.asStateFlow()\n\n fun setConversationName(name: String) {\n _conversationName.value = name\n }\n\n fun autoGenerateConversationName(firstMessage: String) {\n if (_conversationName.value == \"新对话\" && firstMessage.isNotBlank()) {\n val name = if (firstMessage.length > 15) firstMessage.take(15) + \"...\" else firstMessage\n _conversationName.value = name\n }\n }\n\n // ============ 历史对话列表 ============\n private val _sessionList = MutableStateFlow>(emptyList())\n val sessionList: StateFlow> = _sessionList.asStateFlow()\n\n fun loadSessionList() {\n viewModelScope.launch {\n withContext(Dispatchers.IO) {\n val sessions = chatMessageDao.getSessionSummaries()\n _sessionList.value = sessions\n }\n }\n }\n\n fun switchToSession(sessionId: Long) {\n currentSessionId = sessionId\n cozeConversationId = null // 切换会话时重置 Coze 对话 ID\n viewModelScope.launch {\n loadMessagesFromDb()\n // 恢复对话名称\n val messages = _messages.value\n val firstUserMsg = messages.firstOrNull { it.isUser }\n _conversationName.value = if (firstUserMsg != null) {\n if (firstUserMsg.text.length > 15) firstUserMsg.text.take(15) + \"...\" else firstUserMsg.text\n } else {\n \"历史对话\"\n }\n }\n }\n\n private val _inputText = MutableStateFlow(\"\")\n val inputText: StateFlow = _inputText.asStateFlow()\n\n fun setInputText(text: String) {\n _inputText.value = text\n }\n\n // ============ 附件(图片) ============\n private val _pendingImageUri = MutableStateFlow(null)\n val pendingImageUri: StateFlow = _pendingImageUri.asStateFlow()\n\n fun setPendingImageUri(uri: String?) {\n _pendingImageUri.value = uri\n }\n\n // ============ Loading ============\n private val _isLoading = MutableStateFlow(false)\n val isLoading: StateFlow = _isLoading.asStateFlow()\n\n // ============ 状态 ============\n private val _statusMessage = MutableStateFlow(null)\n val statusMessage: StateFlow = _statusMessage.asStateFlow()\n\n // ============ 当前流式任务的 Job ============\n private var streamJob: Job? = null\n\n // ============ Coze 配置状态 ============\n private val _isCozeConfigured = MutableStateFlow(true)\n val isCozeConfigured: StateFlow = _isCozeConfigured.asStateFlow()\n\n // ============ 当前 AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow = _currentChannel.asStateFlow()\n\n init {\n checkCozeConfig()\n loadChannel()\n loadSessionList()\n }\n\n private fun loadChannel() {\n val channel = securePrefs.getString(Constants.KEY_AI_CHANNEL, Constants.CHANNEL_COZE) ?: Constants.CHANNEL_COZE\n _currentChannel.value = channel\n }\n\n /**\n * 获取所有可用通道列表,用于循环切换\n */\n private fun getAvailableChannels(): List {\n val channels = mutableListOf(Constants.CHANNEL_COZE, Constants.CHANNEL_DEEPSEEK)\n val customJson = securePrefs.getString(Constants.KEY_CUSTOM_API_LIST, \"[]\") ?: \"[]\"\n try {\n val type = object : com.google.gson.reflect.TypeToken>() {}.type\n val customList: List = com.google.gson.Gson().fromJson(customJson, type)\n for (c in customList) {\n channels.add(\"${Constants.CHANNEL_CUSTOM_PREFIX}${c.id}\")\n }\n } catch (_: Exception) {}\n return channels\n }\n\n /**\n * 获取当前通道的显示标签\n */\n fun getChannelLabel(): String {\n val ch = _currentChannel.value\n return when {\n ch == Constants.CHANNEL_COZE -> \"🧠 Coze\"\n ch == Constants.CHANNEL_DEEPSEEK -> \"DeepSeek\"\n ch.startsWith(Constants.CHANNEL_CUSTOM_PREFIX) -> {\n val id = ch.removePrefix(Constants.CHANNEL_CUSTOM_PREFIX)\n val customJson = securePrefs.getString(Constants.KEY_CUSTOM_API_LIST, \"[]\") ?: \"[]\"\n try {\n val type = object : com.google.gson.reflect.TypeToken>() {}.type\n val list: List = com.google.gson.Gson().fromJson(customJson, type)\n val config = list.find { it.id == id }\n \"${config?.emoji ?: \"🔌\"} ${config?.name ?: \"自定义\"}\"\n } catch (_: Exception) { \"🔌 自定义\" }\n }\n else -> \"🧠 Coze\"\n }\n }\n\n /**\n * 循环切换到下一个通道\n */\n fun cycleChannel() {\n val channels = getAvailableChannels()\n val currentIdx = channels.indexOf(_currentChannel.value)\n val nextIdx = if (currentIdx < 0 || currentIdx >= channels.size - 1) 0 else currentIdx + 1\n val nextChannel = channels[nextIdx]\n securePrefs.edit().putString(Constants.KEY_AI_CHANNEL, nextChannel).apply()\n _currentChannel.value = nextChannel\n }\n\n private fun checkCozeConfig() {\n viewModelScope.launch {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n // 使用默认值回退:用户未配置时自动使用内置的 PAT 和 Bot ID\n val effectivePat = savedPat.ifBlank { Constants.DEFAULT_COZE_PAT }\n val effectiveBotId = savedBotId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n _isCozeConfigured.value = effectivePat.isNotBlank() && effectiveBotId.isNotBlank()\n }\n }\n\n fun refreshCozeConfig() {\n checkCozeConfig()\n loadChannel()\n }\n\n // ============ 发送消息 ============\n fun sendMessage() {\n val text = _inputText.value.trim()\n val imageUri = _pendingImageUri.value\n\n if (text.isEmpty() && imageUri == null) return\n\n val channel = _currentChannel.value\n if (channel == Constants.CHANNEL_COZE && !_isCozeConfigured.value) {\n _statusMessage.value = \"请先在设置中配置 Coze PAT 和 Bot ID\"\n return\n }\n if (channel == Constants.CHANNEL_DEEPSEEK) {\n val apiKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n if (apiKey.isBlank()) {\n _statusMessage.value = \"请先在设置中配置 DeepSeek API Key\"\n return\n }\n }\n\n // 生成对话名称\n autoGenerateConversationName(text.ifEmpty { \"[图片]\" })\n\n // 添加用户消息到列表\n val userMessage = ChatMessage(\n text = text.ifEmpty { \"[图片]\" },\n isUser = true,\n imageUri = imageUri\n )\n _messages.value = (_messages.value + userMessage).takeLast(100)\n _inputText.value = \"\"\n _pendingImageUri.value = null\n _isLoading.value = true\n\n // 保存用户消息到数据库\n viewModelScope.launch(Dispatchers.IO) {\n try {\n chatMessageDao.insert(ChatMessageEntity(\n text = userMessage.text,\n isUser = true,\n imageUri = userMessage.imageUri,\n timestamp = userMessage.timestamp,\n sessionId = currentSessionId\n ))\n loadSessionList()\n } catch (_: Exception) {}\n }\n\n // 构建消息内容(如果有图片,先分析图片)\n viewModelScope.launch {\n var messageContent = text\n if (imageUri != null) {\n _statusMessage.value = \"🖼️ 正在处理图片...\"\n val imageDescription = processImage(imageUri)\n messageContent = if (text.isNotEmpty()) {\n \"$text\\n\\n[用户发送了一张图片,图片内容描述: $imageDescription]\"\n } else {\n \"[用户发送了一张图片,图片内容描述: $imageDescription]\"\n }\n }\n\n // 根据通道选择调用\n when {\n channel == Constants.CHANNEL_DEEPSEEK -> streamChatFromDeepSeek(messageContent)\n channel.startsWith(Constants.CHANNEL_CUSTOM_PREFIX) -> {\n val customId = channel.removePrefix(Constants.CHANNEL_CUSTOM_PREFIX)\n val customJson = securePrefs.getString(Constants.KEY_CUSTOM_API_LIST, \"[]\") ?: \"[]\"\n try {\n val type = object : com.google.gson.reflect.TypeToken>() {}.type\n val list: List = com.google.gson.Gson().fromJson(customJson, type)\n val config = list.find { it.id == customId }\n if (config != null) {\n streamChatFromCustom(config)\n } else {\n _statusMessage.value = \"❌ 自定义 API 配置不存在\"\n _isLoading.value = false\n }\n } catch (_: Exception) {\n _statusMessage.value = \"❌ 读取自定义配置失败\"\n _isLoading.value = false\n }\n }\n else -> streamChatFromCoze(messageContent)\n }\n }\n }\n\n // ============ DeepSeek 直连 ============\n private suspend fun streamChatFromDeepSeek(message: String) {\n var aiMessageId: Long? = null\n var fullResponse = StringBuilder()\n\n try {\n val apiKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n val systemPrompt = securePrefs.getString(\"deepseek_system_prompt\", \"\") ?: \"\"\n val dsModel = securePrefs.getString(Constants.KEY_DEEPSEEK_MODEL, \"deepseek-v4-flash\") ?: \"deepseek-v4-flash\"\n\n val history = buildHistoryFromMessages()\n\n cozeClient.streamDeepSeekChat(\n apiKey = apiKey,\n message = message,\n model = dsModel,\n systemPrompt = systemPrompt.ifBlank { null },\n history = history\n ).collect { event ->\n when (event) {\n is StreamEvent.Status -> {\n _statusMessage.value = event.message\n }\n is StreamEvent.Delta -> {\n if (aiMessageId == null) {\n aiMessageId = System.nanoTime()\n val newMsg = ChatMessage(id = aiMessageId!!, text = \"\", isUser = false)\n _messages.value = (_messages.value + newMsg).takeLast(100)\n }\n fullResponse.append(event.text)\n val id = aiMessageId!!\n _messages.value = _messages.value.map { msg ->\n if (msg.id == id) msg.copy(text = fullResponse.toString()) else msg\n }\n }\n is StreamEvent.Done -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else if (aiMessageId == null) {\n // 没有收到任何内容,创建一条提示\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ AI 未返回内容,请重试\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.Error -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ ${event.message}\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.TokenUsage -> {\n accumulateTokenUsage(event.total, event.input, event.output)\n }\n }\n }\n } catch (e: Exception) {\n _isLoading.value = false\n _statusMessage.value = null\n val errorMsg = \"❌ DeepSeek 连接失败: ${e.message ?: \"未知错误\"}\"\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = errorMsg, isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n }\n\n // ============ 自定义 API 流式对话 ============\n private suspend fun streamChatFromCustom(config: com.meitu.generator.ui.settings.CustomApiConfig, message: String) {\n var aiMessageId: Long? = null\n var fullResponse = StringBuilder()\n\n try {\n val systemPrompt = securePrefs.getString(\"deepseek_system_prompt\", \"\") ?: \"\"\n val history = buildHistoryFromMessages()\n\n cozeClient.streamCustomChat(\n apiKey = config.apiKey,\n message = message,// 确保传入实际用户消息\n model = config.model,\n baseUrl = config.baseUrl,\n systemPrompt = systemPrompt.ifBlank { null },\n history = history\n ).collect { event ->\n when (event) {\n is StreamEvent.Status -> {\n _statusMessage.value = event.message\n }\n is StreamEvent.Delta -> {\n if (aiMessageId == null) {\n aiMessageId = System.nanoTime()\n val newMsg = ChatMessage(id = aiMessageId!!, text = \"\", isUser = false)\n _messages.value = (_messages.value + newMsg).takeLast(100)\n }\n fullResponse.append(event.text)\n val id = aiMessageId!!\n _messages.value = _messages.value.map { msg ->\n if (msg.id == id) msg.copy(text = fullResponse.toString()) else msg\n }\n }\n is StreamEvent.Done -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else if (aiMessageId == null) {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ API 未返回内容,请检查配置\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.Error -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ ${event.message}\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.TokenUsage -> {\n accumulateTokenUsage(event.total, event.input, event.output)\n }\n }\n }\n } catch (e: Exception) {\n _isLoading.value = false\n _statusMessage.value = null\n val errorMsg = \"❌ 自定义 API 连接失败: ${e.message ?: \"未知错误\"}\"\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = errorMsg, isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n }\n\n // ============ Coze 流式对话 ============\n private suspend fun streamChatFromCoze(message: String) {\n var aiMessageId: Long? = null\n var fullResponse = StringBuilder()\n\n try {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n val pat = savedPat.ifBlank { Constants.DEFAULT_COZE_PAT }\n val botId = savedBotId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n\n val client = CozeApiClient(\n baseUrl = Constants.COZE_API_BASE_URL,\n pat = pat,\n botId = botId,\n httpClient = provideTempHttpClient()\n )\n\n client.streamChat(\n message = message,\n conversationId = cozeConversationId\n ).collect { event ->\n when (event) {\n is StreamEvent.Status -> {\n _statusMessage.value = event.message\n }\n is StreamEvent.Delta -> {\n if (aiMessageId == null) {\n aiMessageId = System.nanoTime()\n val newMsg = ChatMessage(id = aiMessageId!!, text = \"\", isUser = false)\n _messages.value = (_messages.value + newMsg).takeLast(100)\n }\n fullResponse.append(event.text)\n val id = aiMessageId!!\n _messages.value = _messages.value.map { msg ->\n if (msg.id == id) msg.copy(text = fullResponse.toString()) else msg\n }\n }\n is StreamEvent.Done -> {\n cozeConversationId = event.conversationId\n _statusMessage.value = null\n _isLoading.value = false\n\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else if (aiMessageId == null) {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ AI 未返回内容,请重试\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.Error -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ ${event.message}\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.TokenUsage -> {\n accumulateTokenUsage(event.total, event.input, event.output)\n }\n }\n }\n } catch (e: Exception) {\n _isLoading.value = false\n _statusMessage.value = null\n val errorMsg = \"❌ 连接失败: ${e.message ?: \"未知错误\"}\"\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = errorMsg, isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n }\n\n private fun provideTempHttpClient(): okhttp3.OkHttpClient {\n return okhttp3.OkHttpClient.Builder()\n .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)\n .readTimeout(120, java.util.concurrent.TimeUnit.SECONDS)\n .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS)\n .build()\n }\n\n private suspend fun saveAiMessage(text: String) {\n withContext(Dispatchers.IO) {\n try {\n chatMessageDao.insert(ChatMessageEntity(\n text = text,\n isUser = false,\n timestamp = System.currentTimeMillis(),\n sessionId = currentSessionId\n ))\n } catch (_: Exception) {}\n }\n }\n\n // ============ 构建历史对话 ============\n private fun buildHistoryFromMessages(): List> {\n val history = mutableListOf>()\n val chatMessages = _messages.value.filter { !it.isSystem }\n // 取最近的对话(排除空的AI占位消息)\n for (msg in chatMessages) {\n if (msg.text.isNotBlank()) {\n history.add(Pair(if (msg.isUser) \"user\" else \"assistant\", msg.text))\n }\n }\n return history\n }\n\n // ============ Token 消耗累计(分通道,支持自定义) ============\n companion object {\n // Coze 通道\n private const val KEY_COZE_TOTAL_TOKENS = \"coze_total_tokens\"\n private const val KEY_COZE_TOTAL_INPUT = \"coze_total_input\"\n private const val KEY_COZE_TOTAL_OUTPUT = \"coze_total_output\"\n private const val KEY_COZE_TOTAL_MESSAGES = \"coze_total_messages\"\n // DeepSeek 通道\n private const val KEY_DS_TOTAL_TOKENS = \"ds_total_tokens\"\n private const val KEY_DS_TOTAL_INPUT = \"ds_total_input\"\n private const val KEY_DS_TOTAL_OUTPUT = \"ds_total_output\"\n private const val KEY_DS_TOTAL_MESSAGES = \"ds_total_messages\"\n // 自定义通道前缀\n private const val KEY_CUSTOM_PREFIX = \"custom:\"\n }\n\n private fun accumulateTokenUsage(total: Int, input: Int, output: Int) {\n val channel = _currentChannel.value\n val editor = securePrefs.edit()\n\n val prefix = when {\n channel == Constants.CHANNEL_COZE -> \"coze\"\n channel == Constants.CHANNEL_DEEPSEEK -> \"ds\"\n channel.startsWith(KEY_CUSTOM_PREFIX) -> channel // custom:{id}\n else -> \"coze\"\n }\n\n editor.putInt(\"${prefix}_total_tokens\", securePrefs.getInt(\"${prefix}_total_tokens\", 0) + total)\n editor.putInt(\"${prefix}_total_input\", securePrefs.getInt(\"${prefix}_total_input\", 0) + input)\n editor.putInt(\"${prefix}_total_output\", securePrefs.getInt(\"${prefix}_total_output\", 0) + output)\n editor.putInt(\"${prefix}_total_messages\", securePrefs.getInt(\"${prefix}_total_messages\", 0) + 1)\n editor.apply()\n }\n\n private suspend fun processImage(uri: String): String {\n return withContext(Dispatchers.IO) {\n try {\n val context = getApplication()\n val inputStream = context.contentResolver.openInputStream(Uri.parse(uri))\n val bitmap = BitmapFactory.decodeStream(inputStream)\n inputStream?.close()\n\n if (bitmap != null) {\n val byteArrayOutputStream = ByteArrayOutputStream()\n bitmap.compress(Bitmap.CompressFormat.JPEG, 80, byteArrayOutputStream)\n val base64 = Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)\n \"图片已处理(${bitmap.width}x${bitmap.height})\"\n } else {\n \"图片无法解析\"\n }\n } catch (e: Exception) {\n \"图片处理失败: ${e.message}\"\n }\n }\n }\n\n // ============ 新建对话 ============\n fun newConversation() {\n streamJob?.cancel()\n currentSessionId = System.currentTimeMillis()\n cozeConversationId = null\n _conversationName.value = \"新对话\"\n _messages.value = listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n _isLoading.value = false\n _statusMessage.value = null\n loadSessionList()\n }\n\n // ============ 清空当前对话 ============\n fun clearCurrentChat() {\n streamJob?.cancel()\n _messages.value = listOf(ChatMessage(\n text = \"对话已清空,开始新的对话吧 💬\",\n isUser = false\n ))\n _isLoading.value = false\n _statusMessage.value = null\n\n viewModelScope.launch(Dispatchers.IO) {\n try {\n chatMessageDao.deleteBySession(currentSessionId)\n } catch (_: Exception) {}\n }\n }\n\n // ============ 从数据库加载消息 ============\n private fun loadMessagesFromDb() {\n viewModelScope.launch {\n withContext(Dispatchers.IO) {\n val entities = chatMessageDao.getMessagesBySession(currentSessionId)\n val chatMessages = entities.map { entity ->\n ChatMessage(\n text = entity.text,\n isUser = entity.isUser,\n imageUri = entity.imageUri,\n timestamp = entity.timestamp\n )\n }\n if (chatMessages.isNotEmpty()) {\n _messages.value = chatMessages\n }\n }\n }\n }\n\n override fun onCleared() {\n super.onCleared()\n streamJob?.cancel()\n }\n}\n"}, "42329362ab3f6ec591ed1357b675a442f003791c": {"t": 1785755786.4878657, "n": 21484, "h": "21ac1f7c8ab4b2f5", "c": "package com.meitu.generator.ui.assistant\n\nimport android.app.Application\nimport android.content.SharedPreferences\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.net.Uri\nimport android.util.Base64\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.meitu.generator.data.remote.CozeApiClient\nimport com.meitu.generator.data.remote.StreamEvent\nimport com.meitu.generator.data.local.dao.ChatMessageDao\nimport com.meitu.generator.data.local.dao.SessionSummary\nimport com.meitu.generator.data.local.entity.ChatMessageEntity\nimport com.meitu.generator.repository.SettingsRepository\nimport com.meitu.generator.util.Constants\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport java.io.ByteArrayOutputStream\nimport javax.inject.Inject\nimport javax.inject.Named\n\ndata class ChatMessage(\n val id: Long = System.nanoTime(),\n val text: String,\n val isUser: Boolean,\n val isSystem: Boolean = false,\n val imageUri: String? = null,\n val timestamp: Long = System.currentTimeMillis()\n)\n\n@HiltViewModel\nclass AssistantViewModel @Inject constructor(\n application: Application,\n private val settingsRepo: SettingsRepository,\n private val cozeClient: CozeApiClient,\n @Named(\"securePrefs\") private val securePrefs: SharedPreferences,\n private val chatMessageDao: ChatMessageDao\n) : AndroidViewModel(application) {\n\n // ============ Chat Messages ============\n private val _messages = MutableStateFlow>(\n listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n )\n val messages: StateFlow> = _messages.asStateFlow()\n\n // ============ 会话管理 ============\n private var currentSessionId: Long = System.currentTimeMillis()\n private var cozeConversationId: String? = null\n\n // ============ 对话名称 ============\n private val _conversationName = MutableStateFlow(\"新对话\")\n val conversationName: StateFlow = _conversationName.asStateFlow()\n\n fun setConversationName(name: String) {\n _conversationName.value = name\n }\n\n fun autoGenerateConversationName(firstMessage: String) {\n if (_conversationName.value == \"新对话\" && firstMessage.isNotBlank()) {\n val name = if (firstMessage.length > 15) firstMessage.take(15) + \"...\" else firstMessage\n _conversationName.value = name\n }\n }\n\n // ============ 历史对话列表 ============\n private val _sessionList = MutableStateFlow>(emptyList())\n val sessionList: StateFlow> = _sessionList.asStateFlow()\n\n fun loadSessionList() {\n viewModelScope.launch {\n withContext(Dispatchers.IO) {\n val sessions = chatMessageDao.getSessionSummaries()\n _sessionList.value = sessions\n }\n }\n }\n\n fun switchToSession(sessionId: Long) {\n currentSessionId = sessionId\n cozeConversationId = null // 切换会话时重置 Coze 对话 ID\n viewModelScope.launch {\n loadMessagesFromDb()\n // 恢复对话名称\n val messages = _messages.value\n val firstUserMsg = messages.firstOrNull { it.isUser }\n _conversationName.value = if (firstUserMsg != null) {\n if (firstUserMsg.text.length > 15) firstUserMsg.text.take(15) + \"...\" else firstUserMsg.text\n } else {\n \"历史对话\"\n }\n }\n }\n\n private val _inputText = MutableStateFlow(\"\")\n val inputText: StateFlow = _inputText.asStateFlow()\n\n fun setInputText(text: String) {\n _inputText.value = text\n }\n\n // ============ 附件(图片) ============\n private val _pendingImageUri = MutableStateFlow(null)\n val pendingImageUri: StateFlow = _pendingImageUri.asStateFlow()\n\n fun setPendingImageUri(uri: String?) {\n _pendingImageUri.value = uri\n }\n\n // ============ Loading ============\n private val _isLoading = MutableStateFlow(false)\n val isLoading: StateFlow = _isLoading.asStateFlow()\n\n // ============ 状态 ============\n private val _statusMessage = MutableStateFlow(null)\n val statusMessage: StateFlow = _statusMessage.asStateFlow()\n\n // ============ 当前流式任务的 Job ============\n private var streamJob: Job? = null\n\n // ============ Coze 配置状态 ============\n private val _isCozeConfigured = MutableStateFlow(true)\n val isCozeConfigured: StateFlow = _isCozeConfigured.asStateFlow()\n\n // ============ 当前 AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow = _currentChannel.asStateFlow()\n\n init {\n checkCozeConfig()\n loadChannel()\n loadSessionList()\n }\n\n private fun loadChannel() {\n val channel = securePrefs.getString(Constants.KEY_AI_CHANNEL, Constants.CHANNEL_COZE) ?: Constants.CHANNEL_COZE\n _currentChannel.value = channel\n }\n\n private fun checkCozeConfig() {\n viewModelScope.launch {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n // 使用默认值回退:用户未配置时自动使用内置的 PAT 和 Bot ID\n val effectivePat = savedPat.ifBlank { Constants.DEFAULT_COZE_PAT }\n val effectiveBotId = savedBotId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n _isCozeConfigured.value = effectivePat.isNotBlank() && effectiveBotId.isNotBlank()\n }\n }\n\n fun refreshCozeConfig() {\n checkCozeConfig()\n loadChannel()\n }\n\n // ============ 发送消息 ============\n fun sendMessage() {\n val text = _inputText.value.trim()\n val imageUri = _pendingImageUri.value\n\n if (text.isEmpty() && imageUri == null) return\n\n val channel = _currentChannel.value\n if (channel == Constants.CHANNEL_COZE && !_isCozeConfigured.value) {\n _statusMessage.value = \"请先在设置中配置 Coze PAT 和 Bot ID\"\n return\n }\n if (channel == Constants.CHANNEL_DEEPSEEK) {\n val apiKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n if (apiKey.isBlank()) {\n _statusMessage.value = \"请先在设置中配置 DeepSeek API Key\"\n return\n }\n }\n\n // 生成对话名称\n autoGenerateConversationName(text.ifEmpty { \"[图片]\" })\n\n // 添加用户消息到列表\n val userMessage = ChatMessage(\n text = text.ifEmpty { \"[图片]\" },\n isUser = true,\n imageUri = imageUri\n )\n _messages.value = (_messages.value + userMessage).takeLast(100)\n _inputText.value = \"\"\n _pendingImageUri.value = null\n _isLoading.value = true\n\n // 保存用户消息到数据库\n viewModelScope.launch(Dispatchers.IO) {\n try {\n chatMessageDao.insert(ChatMessageEntity(\n text = userMessage.text,\n isUser = true,\n imageUri = userMessage.imageUri,\n timestamp = userMessage.timestamp,\n sessionId = currentSessionId\n ))\n loadSessionList()\n } catch (_: Exception) {}\n }\n\n // 构建消息内容(如果有图片,先分析图片)\n viewModelScope.launch {\n var messageContent = text\n if (imageUri != null) {\n _statusMessage.value = \"🖼️ 正在处理图片...\"\n val imageDescription = processImage(imageUri)\n messageContent = if (text.isNotEmpty()) {\n \"$text\\n\\n[用户发送了一张图片,图片内容描述: $imageDescription]\"\n } else {\n \"[用户发送了一张图片,图片内容描述: $imageDescription]\"\n }\n }\n\n // 根据通道选择调用\n when (channel) {\n Constants.CHANNEL_DEEPSEEK -> streamChatFromDeepSeek(messageContent)\n else -> streamChatFromCoze(messageContent)\n }\n }\n }\n\n // ============ DeepSeek 直连 ============\n private suspend fun streamChatFromDeepSeek(message: String) {\n var aiMessageId: Long? = null\n var fullResponse = StringBuilder()\n\n try {\n val apiKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n val systemPrompt = securePrefs.getString(\"deepseek_system_prompt\", \"\") ?: \"\"\n val dsModel = securePrefs.getString(Constants.KEY_DEEPSEEK_MODEL, \"deepseek-v4-flash\") ?: \"deepseek-v4-flash\"\n\n val history = buildHistoryFromMessages()\n\n cozeClient.streamDeepSeekChat(\n apiKey = apiKey,\n message = message,\n model = dsModel,\n systemPrompt = systemPrompt.ifBlank { null },\n history = history\n ).collect { event ->\n when (event) {\n is StreamEvent.Status -> {\n _statusMessage.value = event.message\n }\n is StreamEvent.Delta -> {\n if (aiMessageId == null) {\n aiMessageId = System.nanoTime()\n val newMsg = ChatMessage(id = aiMessageId!!, text = \"\", isUser = false)\n _messages.value = (_messages.value + newMsg).takeLast(100)\n }\n fullResponse.append(event.text)\n val id = aiMessageId!!\n _messages.value = _messages.value.map { msg ->\n if (msg.id == id) msg.copy(text = fullResponse.toString()) else msg\n }\n }\n is StreamEvent.Done -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else if (aiMessageId == null) {\n // 没有收到任何内容,创建一条提示\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ AI 未返回内容,请重试\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.Error -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ ${event.message}\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.TokenUsage -> {\n accumulateTokenUsage(event.total, event.input, event.output)\n }\n }\n }\n } catch (e: Exception) {\n _isLoading.value = false\n _statusMessage.value = null\n val errorMsg = \"❌ DeepSeek 连接失败: ${e.message ?: \"未知错误\"}\"\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = errorMsg, isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n }\n\n // ============ Coze 流式对话 ============\n private suspend fun streamChatFromCoze(message: String) {\n var aiMessageId: Long? = null\n var fullResponse = StringBuilder()\n\n try {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n val pat = savedPat.ifBlank { Constants.DEFAULT_COZE_PAT }\n val botId = savedBotId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n\n val client = CozeApiClient(\n baseUrl = Constants.COZE_API_BASE_URL,\n pat = pat,\n botId = botId,\n httpClient = provideTempHttpClient()\n )\n\n client.streamChat(\n message = message,\n conversationId = cozeConversationId\n ).collect { event ->\n when (event) {\n is StreamEvent.Status -> {\n _statusMessage.value = event.message\n }\n is StreamEvent.Delta -> {\n if (aiMessageId == null) {\n aiMessageId = System.nanoTime()\n val newMsg = ChatMessage(id = aiMessageId!!, text = \"\", isUser = false)\n _messages.value = (_messages.value + newMsg).takeLast(100)\n }\n fullResponse.append(event.text)\n val id = aiMessageId!!\n _messages.value = _messages.value.map { msg ->\n if (msg.id == id) msg.copy(text = fullResponse.toString()) else msg\n }\n }\n is StreamEvent.Done -> {\n cozeConversationId = event.conversationId\n _statusMessage.value = null\n _isLoading.value = false\n\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else if (aiMessageId == null) {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ AI 未返回内容,请重试\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.Error -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ ${event.message}\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.TokenUsage -> {\n accumulateTokenUsage(event.total, event.input, event.output)\n }\n }\n }\n } catch (e: Exception) {\n _isLoading.value = false\n _statusMessage.value = null\n val errorMsg = \"❌ 连接失败: ${e.message ?: \"未知错误\"}\"\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = errorMsg, isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n }\n\n private fun provideTempHttpClient(): okhttp3.OkHttpClient {\n return okhttp3.OkHttpClient.Builder()\n .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)\n .readTimeout(120, java.util.concurrent.TimeUnit.SECONDS)\n .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS)\n .build()\n }\n\n private suspend fun saveAiMessage(text: String) {\n withContext(Dispatchers.IO) {\n try {\n chatMessageDao.insert(ChatMessageEntity(\n text = text,\n isUser = false,\n timestamp = System.currentTimeMillis(),\n sessionId = currentSessionId\n ))\n } catch (_: Exception) {}\n }\n }\n\n // ============ 构建历史对话 ============\n private fun buildHistoryFromMessages(): List> {\n val history = mutableListOf>()\n val chatMessages = _messages.value.filter { !it.isSystem }\n // 取最近的对话(排除空的AI占位消息)\n for (msg in chatMessages) {\n if (msg.text.isNotBlank()) {\n history.add(Pair(if (msg.isUser) \"user\" else \"assistant\", msg.text))\n }\n }\n return history\n }\n\n // ============ Token 消耗累计(分通道) ============\n companion object {\n // Coze 通道\n private const val KEY_COZE_TOTAL_TOKENS = \"coze_total_tokens\"\n private const val KEY_COZE_TOTAL_INPUT = \"coze_total_input\"\n private const val KEY_COZE_TOTAL_OUTPUT = \"coze_total_output\"\n private const val KEY_COZE_TOTAL_MESSAGES = \"coze_total_messages\"\n // DeepSeek 通道\n private const val KEY_DS_TOTAL_TOKENS = \"ds_total_tokens\"\n private const val KEY_DS_TOTAL_INPUT = \"ds_total_input\"\n private const val KEY_DS_TOTAL_OUTPUT = \"ds_total_output\"\n private const val KEY_DS_TOTAL_MESSAGES = \"ds_total_messages\"\n }\n\n private fun accumulateTokenUsage(total: Int, input: Int, output: Int) {\n val isCoze = _currentChannel.value == Constants.CHANNEL_COZE\n val editor = securePrefs.edit()\n if (isCoze) {\n editor.putInt(KEY_COZE_TOTAL_TOKENS, securePrefs.getInt(KEY_COZE_TOTAL_TOKENS, 0) + total)\n editor.putInt(KEY_COZE_TOTAL_INPUT, securePrefs.getInt(KEY_COZE_TOTAL_INPUT, 0) + input)\n editor.putInt(KEY_COZE_TOTAL_OUTPUT, securePrefs.getInt(KEY_COZE_TOTAL_OUTPUT, 0) + output)\n editor.putInt(KEY_COZE_TOTAL_MESSAGES, securePrefs.getInt(KEY_COZE_TOTAL_MESSAGES, 0) + 1)\n } else {\n editor.putInt(KEY_DS_TOTAL_TOKENS, securePrefs.getInt(KEY_DS_TOTAL_TOKENS, 0) + total)\n editor.putInt(KEY_DS_TOTAL_INPUT, securePrefs.getInt(KEY_DS_TOTAL_INPUT, 0) + input)\n editor.putInt(KEY_DS_TOTAL_OUTPUT, securePrefs.getInt(KEY_DS_TOTAL_OUTPUT, 0) + output)\n editor.putInt(KEY_DS_TOTAL_MESSAGES, securePrefs.getInt(KEY_DS_TOTAL_MESSAGES, 0) + 1)\n }\n editor.apply()\n }\n\n private suspend fun processImage(uri: String): String {\n return withContext(Dispatchers.IO) {\n try {\n val context = getApplication()\n val inputStream = context.contentResolver.openInputStream(Uri.parse(uri))\n val bitmap = BitmapFactory.decodeStream(inputStream)\n inputStream?.close()\n\n if (bitmap != null) {\n val byteArrayOutputStream = ByteArrayOutputStream()\n bitmap.compress(Bitmap.CompressFormat.JPEG, 80, byteArrayOutputStream)\n val base64 = Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)\n \"图片已处理(${bitmap.width}x${bitmap.height})\"\n } else {\n \"图片无法解析\"\n }\n } catch (e: Exception) {\n \"图片处理失败: ${e.message}\"\n }\n }\n }\n\n // ============ 新建对话 ============\n fun newConversation() {\n streamJob?.cancel()\n currentSessionId = System.currentTimeMillis()\n cozeConversationId = null\n _conversationName.value = \"新对话\"\n _messages.value = listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n _isLoading.value = false\n _statusMessage.value = null\n loadSessionList()\n }\n\n // ============ 清空当前对话 ============\n fun clearCurrentChat() {\n streamJob?.cancel()\n _messages.value = listOf(ChatMessage(\n text = \"对话已清空,开始新的对话吧 💬\",\n isUser = false\n ))\n _isLoading.value = false\n _statusMessage.value = null\n\n viewModelScope.launch(Dispatchers.IO) {\n try {\n chatMessageDao.deleteBySession(currentSessionId)\n } catch (_: Exception) {}\n }\n }\n\n // ============ 从数据库加载消息 ============\n private fun loadMessagesFromDb() {\n viewModelScope.launch {\n withContext(Dispatchers.IO) {\n val entities = chatMessageDao.getMessagesBySession(currentSessionId)\n val chatMessages = entities.map { entity ->\n ChatMessage(\n text = entity.text,\n isUser = entity.isUser,\n imageUri = entity.imageUri,\n timestamp = entity.timestamp\n )\n }\n if (chatMessages.isNotEmpty()) {\n _messages.value = chatMessages\n }\n }\n }\n }\n\n override fun onCleared() {\n super.onCleared()\n streamJob?.cancel()\n }\n}\n"}, "2659a30ce3017ffb34149fa7741e416e0662349f": {"t": 1785755787.791973, "n": 21485, "h": "d57a0cbefed8e168", "c": "package com.meitu.generator.ui.assistant\n\nimport android.app.Application\nimport android.content.SharedPreferences\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.net.Uri\nimport android.util.Base64\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.meitu.generator.data.remote.CozeApiClient\nimport com.meitu.generator.data.remote.StreamEvent\nimport com.meitu.generator.data.local.dao.ChatMessageDao\nimport com.meitu.generator.data.local.dao.SessionSummary\nimport com.meitu.generator.data.local.entity.ChatMessageEntity\nimport com.meitu.generator.repository.SettingsRepository\nimport com.meitu.generator.util.Constants\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport java.io.ByteArrayOutputStream\nimport javax.inject.Inject\nimport javax.inject.Named\n\ndata class ChatMessage(\n val id: Long = System.nanoTime(),\n val text: String,\n val isUser: Boolean,\n val isSystem: Boolean = false,\n val imageUri: String? = null,\n val timestamp: Long = System.currentTimeMillis()\n)\n\n@HiltViewModel\nclass AssistantViewModel @Inject constructor(\n application: Application,\n private val settingsRepo: SettingsRepository,\n private val cozeClient: CozeApiClient,\n @Named(\"securePrefs\") private val securePrefs: SharedPreferences,\n private val chatMessageDao: ChatMessageDao\n) : AndroidViewModel(application) {\n\n // ============ Chat Messages ============\n private val _messages = MutableStateFlow>(\n listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n )\n val messages: StateFlow> = _messages.asStateFlow()\n\n // ============ 会话管理 ============\n private var currentSessionId: Long = System.currentTimeMillis()\n private var cozeConversationId: String? = null\n\n // ============ 对话名称 ============\n private val _conversationName = MutableStateFlow(\"新对话\")\n val conversationName: StateFlow = _conversationName.asStateFlow()\n\n fun setConversationName(name: String) {\n _conversationName.value = name\n }\n\n fun autoGenerateConversationName(firstMessage: String) {\n if (_conversationName.value == \"新对话\" && firstMessage.isNotBlank()) {\n val name = if (firstMessage.length > 15) firstMessage.take(15) + \"...\" else firstMessage\n _conversationName.value = name\n }\n }\n\n // ============ 历史对话列表 ============\n private val _sessionList = MutableStateFlow>(emptyList())\n val sessionList: StateFlow> = _sessionList.asStateFlow()\n\n fun loadSessionList() {\n viewModelScope.launch {\n withContext(Dispatchers.IO) {\n val sessions = chatMessageDao.getSessionSummaries()\n _sessionList.value = sessions\n }\n }\n }\n\n fun switchToSession(sessionId: Long) {\n currentSessionId = sessionId\n cozeConversationId = null // 切换会话时重置 Coze 对话 ID\n viewModelScope.launch {\n loadMessagesFromDb()\n // 恢复对话名称\n val messages = _messages.value\n val firstUserMsg = messages.firstOrNull { it.isUser }\n _conversationName.value = if (firstUserMsg != null) {\n if (firstUserMsg.text.length > 15) firstUserMsg.text.take(15) + \"...\" else firstUserMsg.text\n } else {\n \"历史对话\"\n }\n }\n }\n\n private val _inputText = MutableStateFlow(\"\")\n val inputText: StateFlow = _inputText.asStateFlow()\n\n fun setInputText(text: String) {\n _inputText.value = text\n }\n\n // ============ 附件(图片) ============\n private val _pendingImageUri = MutableStateFlow(null)\n val pendingImageUri: StateFlow = _pendingImageUri.asStateFlow()\n\n fun setPendingImageUri(uri: String?) {\n _pendingImageUri.value = uri\n }\n\n // ============ Loading ============\n private val _isLoading = MutableStateFlow(false)\n val isLoading: StateFlow = _isLoading.asStateFlow()\n\n // ============ 状态 ============\n private val _statusMessage = MutableStateFlow(null)\n val statusMessage: StateFlow = _statusMessage.asStateFlow()\n\n // ============ 当前流式任务的 Job ============\n private var streamJob: Job? = null\n\n // ============ Coze 配置状态 ============\n private val _isCozeConfigured = MutableStateFlow(false)\n val isCozeConfigured: StateFlow = _isCozeConfigured.asStateFlow()\n\n // ============ 当前 AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow = _currentChannel.asStateFlow()\n\n init {\n checkCozeConfig()\n loadChannel()\n loadSessionList()\n }\n\n private fun loadChannel() {\n val channel = securePrefs.getString(Constants.KEY_AI_CHANNEL, Constants.CHANNEL_COZE) ?: Constants.CHANNEL_COZE\n _currentChannel.value = channel\n }\n\n private fun checkCozeConfig() {\n viewModelScope.launch {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n // 使用默认值回退:用户未配置时自动使用内置的 PAT 和 Bot ID\n val effectivePat = savedPat.ifBlank { Constants.DEFAULT_COZE_PAT }\n val effectiveBotId = savedBotId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n _isCozeConfigured.value = effectivePat.isNotBlank() && effectiveBotId.isNotBlank()\n }\n }\n\n fun refreshCozeConfig() {\n checkCozeConfig()\n loadChannel()\n }\n\n // ============ 发送消息 ============\n fun sendMessage() {\n val text = _inputText.value.trim()\n val imageUri = _pendingImageUri.value\n\n if (text.isEmpty() && imageUri == null) return\n\n val channel = _currentChannel.value\n if (channel == Constants.CHANNEL_COZE && !_isCozeConfigured.value) {\n _statusMessage.value = \"请先在设置中配置 Coze PAT 和 Bot ID\"\n return\n }\n if (channel == Constants.CHANNEL_DEEPSEEK) {\n val apiKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n if (apiKey.isBlank()) {\n _statusMessage.value = \"请先在设置中配置 DeepSeek API Key\"\n return\n }\n }\n\n // 生成对话名称\n autoGenerateConversationName(text.ifEmpty { \"[图片]\" })\n\n // 添加用户消息到列表\n val userMessage = ChatMessage(\n text = text.ifEmpty { \"[图片]\" },\n isUser = true,\n imageUri = imageUri\n )\n _messages.value = (_messages.value + userMessage).takeLast(100)\n _inputText.value = \"\"\n _pendingImageUri.value = null\n _isLoading.value = true\n\n // 保存用户消息到数据库\n viewModelScope.launch(Dispatchers.IO) {\n try {\n chatMessageDao.insert(ChatMessageEntity(\n text = userMessage.text,\n isUser = true,\n imageUri = userMessage.imageUri,\n timestamp = userMessage.timestamp,\n sessionId = currentSessionId\n ))\n loadSessionList()\n } catch (_: Exception) {}\n }\n\n // 构建消息内容(如果有图片,先分析图片)\n viewModelScope.launch {\n var messageContent = text\n if (imageUri != null) {\n _statusMessage.value = \"🖼️ 正在处理图片...\"\n val imageDescription = processImage(imageUri)\n messageContent = if (text.isNotEmpty()) {\n \"$text\\n\\n[用户发送了一张图片,图片内容描述: $imageDescription]\"\n } else {\n \"[用户发送了一张图片,图片内容描述: $imageDescription]\"\n }\n }\n\n // 根据通道选择调用\n when (channel) {\n Constants.CHANNEL_DEEPSEEK -> streamChatFromDeepSeek(messageContent)\n else -> streamChatFromCoze(messageContent)\n }\n }\n }\n\n // ============ DeepSeek 直连 ============\n private suspend fun streamChatFromDeepSeek(message: String) {\n var aiMessageId: Long? = null\n var fullResponse = StringBuilder()\n\n try {\n val apiKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n val systemPrompt = securePrefs.getString(\"deepseek_system_prompt\", \"\") ?: \"\"\n val dsModel = securePrefs.getString(Constants.KEY_DEEPSEEK_MODEL, \"deepseek-v4-flash\") ?: \"deepseek-v4-flash\"\n\n val history = buildHistoryFromMessages()\n\n cozeClient.streamDeepSeekChat(\n apiKey = apiKey,\n message = message,\n model = dsModel,\n systemPrompt = systemPrompt.ifBlank { null },\n history = history\n ).collect { event ->\n when (event) {\n is StreamEvent.Status -> {\n _statusMessage.value = event.message\n }\n is StreamEvent.Delta -> {\n if (aiMessageId == null) {\n aiMessageId = System.nanoTime()\n val newMsg = ChatMessage(id = aiMessageId!!, text = \"\", isUser = false)\n _messages.value = (_messages.value + newMsg).takeLast(100)\n }\n fullResponse.append(event.text)\n val id = aiMessageId!!\n _messages.value = _messages.value.map { msg ->\n if (msg.id == id) msg.copy(text = fullResponse.toString()) else msg\n }\n }\n is StreamEvent.Done -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else if (aiMessageId == null) {\n // 没有收到任何内容,创建一条提示\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ AI 未返回内容,请重试\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.Error -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ ${event.message}\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.TokenUsage -> {\n accumulateTokenUsage(event.total, event.input, event.output)\n }\n }\n }\n } catch (e: Exception) {\n _isLoading.value = false\n _statusMessage.value = null\n val errorMsg = \"❌ DeepSeek 连接失败: ${e.message ?: \"未知错误\"}\"\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = errorMsg, isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n }\n\n // ============ Coze 流式对话 ============\n private suspend fun streamChatFromCoze(message: String) {\n var aiMessageId: Long? = null\n var fullResponse = StringBuilder()\n\n try {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n val pat = savedPat.ifBlank { Constants.DEFAULT_COZE_PAT }\n val botId = savedBotId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n\n val client = CozeApiClient(\n baseUrl = Constants.COZE_API_BASE_URL,\n pat = pat,\n botId = botId,\n httpClient = provideTempHttpClient()\n )\n\n client.streamChat(\n message = message,\n conversationId = cozeConversationId\n ).collect { event ->\n when (event) {\n is StreamEvent.Status -> {\n _statusMessage.value = event.message\n }\n is StreamEvent.Delta -> {\n if (aiMessageId == null) {\n aiMessageId = System.nanoTime()\n val newMsg = ChatMessage(id = aiMessageId!!, text = \"\", isUser = false)\n _messages.value = (_messages.value + newMsg).takeLast(100)\n }\n fullResponse.append(event.text)\n val id = aiMessageId!!\n _messages.value = _messages.value.map { msg ->\n if (msg.id == id) msg.copy(text = fullResponse.toString()) else msg\n }\n }\n is StreamEvent.Done -> {\n cozeConversationId = event.conversationId\n _statusMessage.value = null\n _isLoading.value = false\n\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else if (aiMessageId == null) {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ AI 未返回内容,请重试\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.Error -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ ${event.message}\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.TokenUsage -> {\n accumulateTokenUsage(event.total, event.input, event.output)\n }\n }\n }\n } catch (e: Exception) {\n _isLoading.value = false\n _statusMessage.value = null\n val errorMsg = \"❌ 连接失败: ${e.message ?: \"未知错误\"}\"\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = errorMsg, isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n }\n\n private fun provideTempHttpClient(): okhttp3.OkHttpClient {\n return okhttp3.OkHttpClient.Builder()\n .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)\n .readTimeout(120, java.util.concurrent.TimeUnit.SECONDS)\n .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS)\n .build()\n }\n\n private suspend fun saveAiMessage(text: String) {\n withContext(Dispatchers.IO) {\n try {\n chatMessageDao.insert(ChatMessageEntity(\n text = text,\n isUser = false,\n timestamp = System.currentTimeMillis(),\n sessionId = currentSessionId\n ))\n } catch (_: Exception) {}\n }\n }\n\n // ============ 构建历史对话 ============\n private fun buildHistoryFromMessages(): List> {\n val history = mutableListOf>()\n val chatMessages = _messages.value.filter { !it.isSystem }\n // 取最近的对话(排除空的AI占位消息)\n for (msg in chatMessages) {\n if (msg.text.isNotBlank()) {\n history.add(Pair(if (msg.isUser) \"user\" else \"assistant\", msg.text))\n }\n }\n return history\n }\n\n // ============ Token 消耗累计(分通道) ============\n companion object {\n // Coze 通道\n private const val KEY_COZE_TOTAL_TOKENS = \"coze_total_tokens\"\n private const val KEY_COZE_TOTAL_INPUT = \"coze_total_input\"\n private const val KEY_COZE_TOTAL_OUTPUT = \"coze_total_output\"\n private const val KEY_COZE_TOTAL_MESSAGES = \"coze_total_messages\"\n // DeepSeek 通道\n private const val KEY_DS_TOTAL_TOKENS = \"ds_total_tokens\"\n private const val KEY_DS_TOTAL_INPUT = \"ds_total_input\"\n private const val KEY_DS_TOTAL_OUTPUT = \"ds_total_output\"\n private const val KEY_DS_TOTAL_MESSAGES = \"ds_total_messages\"\n }\n\n private fun accumulateTokenUsage(total: Int, input: Int, output: Int) {\n val isCoze = _currentChannel.value == Constants.CHANNEL_COZE\n val editor = securePrefs.edit()\n if (isCoze) {\n editor.putInt(KEY_COZE_TOTAL_TOKENS, securePrefs.getInt(KEY_COZE_TOTAL_TOKENS, 0) + total)\n editor.putInt(KEY_COZE_TOTAL_INPUT, securePrefs.getInt(KEY_COZE_TOTAL_INPUT, 0) + input)\n editor.putInt(KEY_COZE_TOTAL_OUTPUT, securePrefs.getInt(KEY_COZE_TOTAL_OUTPUT, 0) + output)\n editor.putInt(KEY_COZE_TOTAL_MESSAGES, securePrefs.getInt(KEY_COZE_TOTAL_MESSAGES, 0) + 1)\n } else {\n editor.putInt(KEY_DS_TOTAL_TOKENS, securePrefs.getInt(KEY_DS_TOTAL_TOKENS, 0) + total)\n editor.putInt(KEY_DS_TOTAL_INPUT, securePrefs.getInt(KEY_DS_TOTAL_INPUT, 0) + input)\n editor.putInt(KEY_DS_TOTAL_OUTPUT, securePrefs.getInt(KEY_DS_TOTAL_OUTPUT, 0) + output)\n editor.putInt(KEY_DS_TOTAL_MESSAGES, securePrefs.getInt(KEY_DS_TOTAL_MESSAGES, 0) + 1)\n }\n editor.apply()\n }\n\n private suspend fun processImage(uri: String): String {\n return withContext(Dispatchers.IO) {\n try {\n val context = getApplication()\n val inputStream = context.contentResolver.openInputStream(Uri.parse(uri))\n val bitmap = BitmapFactory.decodeStream(inputStream)\n inputStream?.close()\n\n if (bitmap != null) {\n val byteArrayOutputStream = ByteArrayOutputStream()\n bitmap.compress(Bitmap.CompressFormat.JPEG, 80, byteArrayOutputStream)\n val base64 = Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)\n \"图片已处理(${bitmap.width}x${bitmap.height})\"\n } else {\n \"图片无法解析\"\n }\n } catch (e: Exception) {\n \"图片处理失败: ${e.message}\"\n }\n }\n }\n\n // ============ 新建对话 ============\n fun newConversation() {\n streamJob?.cancel()\n currentSessionId = System.currentTimeMillis()\n cozeConversationId = null\n _conversationName.value = \"新对话\"\n _messages.value = listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n _isLoading.value = false\n _statusMessage.value = null\n loadSessionList()\n }\n\n // ============ 清空当前对话 ============\n fun clearCurrentChat() {\n streamJob?.cancel()\n _messages.value = listOf(ChatMessage(\n text = \"对话已清空,开始新的对话吧 💬\",\n isUser = false\n ))\n _isLoading.value = false\n _statusMessage.value = null\n\n viewModelScope.launch(Dispatchers.IO) {\n try {\n chatMessageDao.deleteBySession(currentSessionId)\n } catch (_: Exception) {}\n }\n }\n\n // ============ 从数据库加载消息 ============\n private fun loadMessagesFromDb() {\n viewModelScope.launch {\n withContext(Dispatchers.IO) {\n val entities = chatMessageDao.getMessagesBySession(currentSessionId)\n val chatMessages = entities.map { entity ->\n ChatMessage(\n text = entity.text,\n isUser = entity.isUser,\n imageUri = entity.imageUri,\n timestamp = entity.timestamp\n )\n }\n if (chatMessages.isNotEmpty()) {\n _messages.value = chatMessages\n }\n }\n }\n }\n\n override fun onCleared() {\n super.onCleared()\n streamJob?.cancel()\n }\n}\n"}, "c7681304134ebb26fb18f0c9cea671700db02829": {"t": 1785755789.3682017, "n": 21535, "h": "70c276d4ed77d93b", "c": "package com.meitu.generator.ui.assistant\n\nimport android.app.Application\nimport android.content.SharedPreferences\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.net.Uri\nimport android.util.Base64\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.meitu.generator.data.remote.CozeApiClient\nimport com.meitu.generator.data.remote.StreamEvent\nimport com.meitu.generator.data.remote.TokenUsage\nimport com.meitu.generator.data.local.dao.ChatMessageDao\nimport com.meitu.generator.data.local.dao.SessionSummary\nimport com.meitu.generator.data.local.entity.ChatMessageEntity\nimport com.meitu.generator.repository.SettingsRepository\nimport com.meitu.generator.util.Constants\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport java.io.ByteArrayOutputStream\nimport javax.inject.Inject\nimport javax.inject.Named\n\ndata class ChatMessage(\n val id: Long = System.nanoTime(),\n val text: String,\n val isUser: Boolean,\n val isSystem: Boolean = false,\n val imageUri: String? = null,\n val timestamp: Long = System.currentTimeMillis()\n)\n\n@HiltViewModel\nclass AssistantViewModel @Inject constructor(\n application: Application,\n private val settingsRepo: SettingsRepository,\n private val cozeClient: CozeApiClient,\n @Named(\"securePrefs\") private val securePrefs: SharedPreferences,\n private val chatMessageDao: ChatMessageDao\n) : AndroidViewModel(application) {\n\n // ============ Chat Messages ============\n private val _messages = MutableStateFlow>(\n listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n )\n val messages: StateFlow> = _messages.asStateFlow()\n\n // ============ 会话管理 ============\n private var currentSessionId: Long = System.currentTimeMillis()\n private var cozeConversationId: String? = null\n\n // ============ 对话名称 ============\n private val _conversationName = MutableStateFlow(\"新对话\")\n val conversationName: StateFlow = _conversationName.asStateFlow()\n\n fun setConversationName(name: String) {\n _conversationName.value = name\n }\n\n fun autoGenerateConversationName(firstMessage: String) {\n if (_conversationName.value == \"新对话\" && firstMessage.isNotBlank()) {\n val name = if (firstMessage.length > 15) firstMessage.take(15) + \"...\" else firstMessage\n _conversationName.value = name\n }\n }\n\n // ============ 历史对话列表 ============\n private val _sessionList = MutableStateFlow>(emptyList())\n val sessionList: StateFlow> = _sessionList.asStateFlow()\n\n fun loadSessionList() {\n viewModelScope.launch {\n withContext(Dispatchers.IO) {\n val sessions = chatMessageDao.getSessionSummaries()\n _sessionList.value = sessions\n }\n }\n }\n\n fun switchToSession(sessionId: Long) {\n currentSessionId = sessionId\n cozeConversationId = null // 切换会话时重置 Coze 对话 ID\n viewModelScope.launch {\n loadMessagesFromDb()\n // 恢复对话名称\n val messages = _messages.value\n val firstUserMsg = messages.firstOrNull { it.isUser }\n _conversationName.value = if (firstUserMsg != null) {\n if (firstUserMsg.text.length > 15) firstUserMsg.text.take(15) + \"...\" else firstUserMsg.text\n } else {\n \"历史对话\"\n }\n }\n }\n\n private val _inputText = MutableStateFlow(\"\")\n val inputText: StateFlow = _inputText.asStateFlow()\n\n fun setInputText(text: String) {\n _inputText.value = text\n }\n\n // ============ 附件(图片) ============\n private val _pendingImageUri = MutableStateFlow(null)\n val pendingImageUri: StateFlow = _pendingImageUri.asStateFlow()\n\n fun setPendingImageUri(uri: String?) {\n _pendingImageUri.value = uri\n }\n\n // ============ Loading ============\n private val _isLoading = MutableStateFlow(false)\n val isLoading: StateFlow = _isLoading.asStateFlow()\n\n // ============ 状态 ============\n private val _statusMessage = MutableStateFlow(null)\n val statusMessage: StateFlow = _statusMessage.asStateFlow()\n\n // ============ 当前流式任务的 Job ============\n private var streamJob: Job? = null\n\n // ============ Coze 配置状态 ============\n private val _isCozeConfigured = MutableStateFlow(false)\n val isCozeConfigured: StateFlow = _isCozeConfigured.asStateFlow()\n\n // ============ 当前 AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow = _currentChannel.asStateFlow()\n\n init {\n checkCozeConfig()\n loadChannel()\n loadSessionList()\n }\n\n private fun loadChannel() {\n val channel = securePrefs.getString(Constants.KEY_AI_CHANNEL, Constants.CHANNEL_COZE) ?: Constants.CHANNEL_COZE\n _currentChannel.value = channel\n }\n\n private fun checkCozeConfig() {\n viewModelScope.launch {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n // 使用默认值回退:用户未配置时自动使用内置的 PAT 和 Bot ID\n val effectivePat = savedPat.ifBlank { Constants.DEFAULT_COZE_PAT }\n val effectiveBotId = savedBotId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n _isCozeConfigured.value = effectivePat.isNotBlank() && effectiveBotId.isNotBlank()\n }\n }\n\n fun refreshCozeConfig() {\n checkCozeConfig()\n loadChannel()\n }\n\n // ============ 发送消息 ============\n fun sendMessage() {\n val text = _inputText.value.trim()\n val imageUri = _pendingImageUri.value\n\n if (text.isEmpty() && imageUri == null) return\n\n val channel = _currentChannel.value\n if (channel == Constants.CHANNEL_COZE && !_isCozeConfigured.value) {\n _statusMessage.value = \"请先在设置中配置 Coze PAT 和 Bot ID\"\n return\n }\n if (channel == Constants.CHANNEL_DEEPSEEK) {\n val apiKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n if (apiKey.isBlank()) {\n _statusMessage.value = \"请先在设置中配置 DeepSeek API Key\"\n return\n }\n }\n\n // 生成对话名称\n autoGenerateConversationName(text.ifEmpty { \"[图片]\" })\n\n // 添加用户消息到列表\n val userMessage = ChatMessage(\n text = text.ifEmpty { \"[图片]\" },\n isUser = true,\n imageUri = imageUri\n )\n _messages.value = (_messages.value + userMessage).takeLast(100)\n _inputText.value = \"\"\n _pendingImageUri.value = null\n _isLoading.value = true\n\n // 保存用户消息到数据库\n viewModelScope.launch(Dispatchers.IO) {\n try {\n chatMessageDao.insert(ChatMessageEntity(\n text = userMessage.text,\n isUser = true,\n imageUri = userMessage.imageUri,\n timestamp = userMessage.timestamp,\n sessionId = currentSessionId\n ))\n loadSessionList()\n } catch (_: Exception) {}\n }\n\n // 构建消息内容(如果有图片,先分析图片)\n viewModelScope.launch {\n var messageContent = text\n if (imageUri != null) {\n _statusMessage.value = \"🖼️ 正在处理图片...\"\n val imageDescription = processImage(imageUri)\n messageContent = if (text.isNotEmpty()) {\n \"$text\\n\\n[用户发送了一张图片,图片内容描述: $imageDescription]\"\n } else {\n \"[用户发送了一张图片,图片内容描述: $imageDescription]\"\n }\n }\n\n // 根据通道选择调用\n when (channel) {\n Constants.CHANNEL_DEEPSEEK -> streamChatFromDeepSeek(messageContent)\n else -> streamChatFromCoze(messageContent)\n }\n }\n }\n\n // ============ DeepSeek 直连 ============\n private suspend fun streamChatFromDeepSeek(message: String) {\n var aiMessageId: Long? = null\n var fullResponse = StringBuilder()\n\n try {\n val apiKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n val systemPrompt = securePrefs.getString(\"deepseek_system_prompt\", \"\") ?: \"\"\n val dsModel = securePrefs.getString(Constants.KEY_DEEPSEEK_MODEL, \"deepseek-v4-flash\") ?: \"deepseek-v4-flash\"\n\n val history = buildHistoryFromMessages()\n\n cozeClient.streamDeepSeekChat(\n apiKey = apiKey,\n message = message,\n model = dsModel,\n systemPrompt = systemPrompt.ifBlank { null },\n history = history\n ).collect { event ->\n when (event) {\n is StreamEvent.Status -> {\n _statusMessage.value = event.message\n }\n is StreamEvent.Delta -> {\n if (aiMessageId == null) {\n aiMessageId = System.nanoTime()\n val newMsg = ChatMessage(id = aiMessageId!!, text = \"\", isUser = false)\n _messages.value = (_messages.value + newMsg).takeLast(100)\n }\n fullResponse.append(event.text)\n val id = aiMessageId!!\n _messages.value = _messages.value.map { msg ->\n if (msg.id == id) msg.copy(text = fullResponse.toString()) else msg\n }\n }\n is StreamEvent.Done -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else if (aiMessageId == null) {\n // 没有收到任何内容,创建一条提示\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ AI 未返回内容,请重试\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.Error -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ ${event.message}\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.TokenUsage -> {\n accumulateTokenUsage(event.total, event.input, event.output)\n }\n }\n }\n } catch (e: Exception) {\n _isLoading.value = false\n _statusMessage.value = null\n val errorMsg = \"❌ DeepSeek 连接失败: ${e.message ?: \"未知错误\"}\"\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = errorMsg, isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n }\n\n // ============ Coze 流式对话 ============\n private suspend fun streamChatFromCoze(message: String) {\n var aiMessageId: Long? = null\n var fullResponse = StringBuilder()\n\n try {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n val pat = savedPat.ifBlank { Constants.DEFAULT_COZE_PAT }\n val botId = savedBotId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n\n val client = CozeApiClient(\n baseUrl = Constants.COZE_API_BASE_URL,\n pat = pat,\n botId = botId,\n httpClient = provideTempHttpClient()\n )\n\n client.streamChat(\n message = message,\n conversationId = cozeConversationId\n ).collect { event ->\n when (event) {\n is StreamEvent.Status -> {\n _statusMessage.value = event.message\n }\n is StreamEvent.Delta -> {\n if (aiMessageId == null) {\n aiMessageId = System.nanoTime()\n val newMsg = ChatMessage(id = aiMessageId!!, text = \"\", isUser = false)\n _messages.value = (_messages.value + newMsg).takeLast(100)\n }\n fullResponse.append(event.text)\n val id = aiMessageId!!\n _messages.value = _messages.value.map { msg ->\n if (msg.id == id) msg.copy(text = fullResponse.toString()) else msg\n }\n }\n is StreamEvent.Done -> {\n cozeConversationId = event.conversationId\n _statusMessage.value = null\n _isLoading.value = false\n\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else if (aiMessageId == null) {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ AI 未返回内容,请重试\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.Error -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = \"❌ ${event.message}\", isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n is StreamEvent.TokenUsage -> {\n accumulateTokenUsage(event.total, event.input, event.output)\n }\n }\n }\n } catch (e: Exception) {\n _isLoading.value = false\n _statusMessage.value = null\n val errorMsg = \"❌ 连接失败: ${e.message ?: \"未知错误\"}\"\n if (aiMessageId != null && fullResponse.isNotEmpty()) {\n saveAiMessage(fullResponse.toString())\n } else {\n val id = System.nanoTime()\n val errMsg = ChatMessage(id = id, text = errorMsg, isUser = false)\n _messages.value = (_messages.value + errMsg).takeLast(100)\n }\n }\n }\n\n private fun provideTempHttpClient(): okhttp3.OkHttpClient {\n return okhttp3.OkHttpClient.Builder()\n .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)\n .readTimeout(120, java.util.concurrent.TimeUnit.SECONDS)\n .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS)\n .build()\n }\n\n private suspend fun saveAiMessage(text: String) {\n withContext(Dispatchers.IO) {\n try {\n chatMessageDao.insert(ChatMessageEntity(\n text = text,\n isUser = false,\n timestamp = System.currentTimeMillis(),\n sessionId = currentSessionId\n ))\n } catch (_: Exception) {}\n }\n }\n\n // ============ 构建历史对话 ============\n private fun buildHistoryFromMessages(): List> {\n val history = mutableListOf>()\n val chatMessages = _messages.value.filter { !it.isSystem }\n // 取最近的对话(排除空的AI占位消息)\n for (msg in chatMessages) {\n if (msg.text.isNotBlank()) {\n history.add(Pair(if (msg.isUser) \"user\" else \"assistant\", msg.text))\n }\n }\n return history\n }\n\n // ============ Token 消耗累计(分通道) ============\n companion object {\n // Coze 通道\n private const val KEY_COZE_TOTAL_TOKENS = \"coze_total_tokens\"\n private const val KEY_COZE_TOTAL_INPUT = \"coze_total_input\"\n private const val KEY_COZE_TOTAL_OUTPUT = \"coze_total_output\"\n private const val KEY_COZE_TOTAL_MESSAGES = \"coze_total_messages\"\n // DeepSeek 通道\n private const val KEY_DS_TOTAL_TOKENS = \"ds_total_tokens\"\n private const val KEY_DS_TOTAL_INPUT = \"ds_total_input\"\n private const val KEY_DS_TOTAL_OUTPUT = \"ds_total_output\"\n private const val KEY_DS_TOTAL_MESSAGES = \"ds_total_messages\"\n }\n\n private fun accumulateTokenUsage(total: Int, input: Int, output: Int) {\n val isCoze = _currentChannel.value == Constants.CHANNEL_COZE\n val editor = securePrefs.edit()\n if (isCoze) {\n editor.putInt(KEY_COZE_TOTAL_TOKENS, securePrefs.getInt(KEY_COZE_TOTAL_TOKENS, 0) + total)\n editor.putInt(KEY_COZE_TOTAL_INPUT, securePrefs.getInt(KEY_COZE_TOTAL_INPUT, 0) + input)\n editor.putInt(KEY_COZE_TOTAL_OUTPUT, securePrefs.getInt(KEY_COZE_TOTAL_OUTPUT, 0) + output)\n editor.putInt(KEY_COZE_TOTAL_MESSAGES, securePrefs.getInt(KEY_COZE_TOTAL_MESSAGES, 0) + 1)\n } else {\n editor.putInt(KEY_DS_TOTAL_TOKENS, securePrefs.getInt(KEY_DS_TOTAL_TOKENS, 0) + total)\n editor.putInt(KEY_DS_TOTAL_INPUT, securePrefs.getInt(KEY_DS_TOTAL_INPUT, 0) + input)\n editor.putInt(KEY_DS_TOTAL_OUTPUT, securePrefs.getInt(KEY_DS_TOTAL_OUTPUT, 0) + output)\n editor.putInt(KEY_DS_TOTAL_MESSAGES, securePrefs.getInt(KEY_DS_TOTAL_MESSAGES, 0) + 1)\n }\n editor.apply()\n }\n\n private suspend fun processImage(uri: String): String {\n return withContext(Dispatchers.IO) {\n try {\n val context = getApplication()\n val inputStream = context.contentResolver.openInputStream(Uri.parse(uri))\n val bitmap = BitmapFactory.decodeStream(inputStream)\n inputStream?.close()\n\n if (bitmap != null) {\n val byteArrayOutputStream = ByteArrayOutputStream()\n bitmap.compress(Bitmap.CompressFormat.JPEG, 80, byteArrayOutputStream)\n val base64 = Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)\n \"图片已处理(${bitmap.width}x${bitmap.height})\"\n } else {\n \"图片无法解析\"\n }\n } catch (e: Exception) {\n \"图片处理失败: ${e.message}\"\n }\n }\n }\n\n // ============ 新建对话 ============\n fun newConversation() {\n streamJob?.cancel()\n currentSessionId = System.currentTimeMillis()\n cozeConversationId = null\n _conversationName.value = \"新对话\"\n _messages.value = listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n _isLoading.value = false\n _statusMessage.value = null\n loadSessionList()\n }\n\n // ============ 清空当前对话 ============\n fun clearCurrentChat() {\n streamJob?.cancel()\n _messages.value = listOf(ChatMessage(\n text = \"对话已清空,开始新的对话吧 💬\",\n isUser = false\n ))\n _isLoading.value = false\n _statusMessage.value = null\n\n viewModelScope.launch(Dispatchers.IO) {\n try {\n chatMessageDao.deleteBySession(currentSessionId)\n } catch (_: Exception) {}\n }\n }\n\n // ============ 从数据库加载消息 ============\n private fun loadMessagesFromDb() {\n viewModelScope.launch {\n withContext(Dispatchers.IO) {\n val entities = chatMessageDao.getMessagesBySession(currentSessionId)\n val chatMessages = entities.map { entity ->\n ChatMessage(\n text = entity.text,\n isUser = entity.isUser,\n imageUri = entity.imageUri,\n timestamp = entity.timestamp\n )\n }\n if (chatMessages.isNotEmpty()) {\n _messages.value = chatMessages\n }\n }\n }\n }\n\n override fun onCleared() {\n super.onCleared()\n streamJob?.cancel()\n }\n}\n"}, "0df44538d6c6222efa067172758e4fd1f8102b4c": {"t": 1785755790.9587994, "n": 19151, "h": "fa574361b8c6df0f", "c": "package com.meitu.generator.ui.assistant\n\nimport android.app.Application\nimport android.content.SharedPreferences\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.net.Uri\nimport android.util.Base64\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.meitu.generator.data.remote.CozeApiClient\nimport com.meitu.generator.data.remote.StreamEvent\nimport com.meitu.generator.data.local.dao.ChatMessageDao\nimport com.meitu.generator.data.local.dao.SessionSummary\nimport com.meitu.generator.data.local.entity.ChatMessageEntity\nimport com.meitu.generator.repository.SettingsRepository\nimport com.meitu.generator.util.Constants\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport java.io.ByteArrayOutputStream\nimport javax.inject.Inject\nimport javax.inject.Named\n\ndata class ChatMessage(\n val id: Long = System.nanoTime(),\n val text: String,\n val isUser: Boolean,\n val isSystem: Boolean = false,\n val imageUri: String? = null,\n val timestamp: Long = System.currentTimeMillis()\n)\n\n@HiltViewModel\nclass AssistantViewModel @Inject constructor(\n application: Application,\n private val settingsRepo: SettingsRepository,\n private val cozeClient: CozeApiClient,\n @Named(\"securePrefs\") private val securePrefs: SharedPreferences,\n private val chatMessageDao: ChatMessageDao\n) : AndroidViewModel(application) {\n\n // ============ Chat Messages ============\n private val _messages = MutableStateFlow>(\n listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n )\n val messages: StateFlow> = _messages.asStateFlow()\n\n // ============ 会话管理 ============\n private var currentSessionId: Long = System.currentTimeMillis()\n private var cozeConversationId: String? = null\n\n // ============ 对话名称 ============\n private val _conversationName = MutableStateFlow(\"新对话\")\n val conversationName: StateFlow = _conversationName.asStateFlow()\n\n fun setConversationName(name: String) {\n _conversationName.value = name\n }\n\n fun autoGenerateConversationName(firstMessage: String) {\n if (_conversationName.value == \"新对话\" && firstMessage.isNotBlank()) {\n val name = if (firstMessage.length > 15) firstMessage.take(15) + \"...\" else firstMessage\n _conversationName.value = name\n }\n }\n\n // ============ 历史对话列表 ============\n private val _sessionList = MutableStateFlow>(emptyList())\n val sessionList: StateFlow> = _sessionList.asStateFlow()\n\n fun loadSessionList() {\n viewModelScope.launch {\n withContext(Dispatchers.IO) {\n val sessions = chatMessageDao.getSessionSummaries()\n _sessionList.value = sessions\n }\n }\n }\n\n fun switchToSession(sessionId: Long) {\n currentSessionId = sessionId\n cozeConversationId = null // 切换会话时重置 Coze 对话 ID\n viewModelScope.launch {\n loadMessagesFromDb()\n // 恢复对话名称\n val messages = _messages.value\n val firstUserMsg = messages.firstOrNull { it.isUser }\n _conversationName.value = if (firstUserMsg != null) {\n if (firstUserMsg.text.length > 15) firstUserMsg.text.take(15) + \"...\" else firstUserMsg.text\n } else {\n \"历史对话\"\n }\n }\n }\n\n private val _inputText = MutableStateFlow(\"\")\n val inputText: StateFlow = _inputText.asStateFlow()\n\n fun setInputText(text: String) {\n _inputText.value = text\n }\n\n // ============ 附件(图片) ============\n private val _pendingImageUri = MutableStateFlow(null)\n val pendingImageUri: StateFlow = _pendingImageUri.asStateFlow()\n\n fun setPendingImageUri(uri: String?) {\n _pendingImageUri.value = uri\n }\n\n // ============ Loading ============\n private val _isLoading = MutableStateFlow(false)\n val isLoading: StateFlow = _isLoading.asStateFlow()\n\n // ============ 状态 ============\n private val _statusMessage = MutableStateFlow(null)\n val statusMessage: StateFlow = _statusMessage.asStateFlow()\n\n // ============ 当前流式任务的 Job ============\n private var streamJob: Job? = null\n\n // ============ Coze 配置状态 ============\n private val _isCozeConfigured = MutableStateFlow(false)\n val isCozeConfigured: StateFlow = _isCozeConfigured.asStateFlow()\n\n // ============ 当前 AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow = _currentChannel.asStateFlow()\n\n init {\n checkCozeConfig()\n loadChannel()\n loadSessionList()\n }\n\n private fun loadChannel() {\n val channel = securePrefs.getString(Constants.KEY_AI_CHANNEL, Constants.CHANNEL_COZE) ?: Constants.CHANNEL_COZE\n _currentChannel.value = channel\n }\n\n private fun checkCozeConfig() {\n viewModelScope.launch {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n // 使用默认值回退:用户未配置时自动使用内置的 PAT 和 Bot ID\n val effectivePat = savedPat.ifBlank { Constants.DEFAULT_COZE_PAT }\n val effectiveBotId = savedBotId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n _isCozeConfigured.value = effectivePat.isNotBlank() && effectiveBotId.isNotBlank()\n }\n }\n\n fun refreshCozeConfig() {\n checkCozeConfig()\n loadChannel()\n }\n\n // ============ 发送消息 ============\n fun sendMessage() {\n val text = _inputText.value.trim()\n val imageUri = _pendingImageUri.value\n\n if (text.isEmpty() && imageUri == null) return\n\n val channel = _currentChannel.value\n if (channel == Constants.CHANNEL_COZE && !_isCozeConfigured.value) {\n _statusMessage.value = \"请先在设置中配置 Coze PAT 和 Bot ID\"\n return\n }\n if (channel == Constants.CHANNEL_DEEPSEEK) {\n val apiKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n if (apiKey.isBlank()) {\n _statusMessage.value = \"请先在设置中配置 DeepSeek API Key\"\n return\n }\n }\n\n // 生成对话名称\n autoGenerateConversationName(text.ifEmpty { \"[图片]\" })\n\n // 添加用户消息到列表\n val userMessage = ChatMessage(\n text = text.ifEmpty { \"[图片]\" },\n isUser = true,\n imageUri = imageUri\n )\n _messages.value = (_messages.value + userMessage).takeLast(100)\n _inputText.value = \"\"\n _pendingImageUri.value = null\n _isLoading.value = true\n\n // 保存用户消息到数据库\n viewModelScope.launch(Dispatchers.IO) {\n try {\n chatMessageDao.insert(ChatMessageEntity(\n text = userMessage.text,\n isUser = true,\n imageUri = userMessage.imageUri,\n timestamp = userMessage.timestamp,\n sessionId = currentSessionId\n ))\n loadSessionList()\n } catch (_: Exception) {}\n }\n\n // 构建消息内容(如果有图片,先分析图片)\n viewModelScope.launch {\n var messageContent = text\n if (imageUri != null) {\n _statusMessage.value = \"🖼️ 正在处理图片...\"\n val imageDescription = processImage(imageUri)\n messageContent = if (text.isNotEmpty()) {\n \"$text\\n\\n[用户发送了一张图片,图片内容描述: $imageDescription]\"\n } else {\n \"[用户发送了一张图片,图片内容描述: $imageDescription]\"\n }\n }\n\n // 根据通道选择调用\n when (channel) {\n Constants.CHANNEL_DEEPSEEK -> streamChatFromDeepSeek(messageContent)\n else -> streamChatFromCoze(messageContent)\n }\n }\n }\n\n // ============ DeepSeek 直连 ============\n private suspend fun streamChatFromDeepSeek(message: String) {\n val aiMessageId = System.nanoTime()\n val aiMessage = ChatMessage(id = aiMessageId, text = \"\", isUser = false)\n _messages.value = (_messages.value + aiMessage).takeLast(100)\n\n var fullResponse = StringBuilder()\n\n try {\n val apiKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n val systemPrompt = securePrefs.getString(\"deepseek_system_prompt\", \"\") ?: \"\"\n\n // 构建历史对话\n val history = buildHistoryFromMessages()\n\n cozeClient.streamDeepSeekChat(\n apiKey = apiKey,\n message = message,\n systemPrompt = systemPrompt.ifBlank { null },\n history = history\n ).collect { event ->\n when (event) {\n is StreamEvent.Status -> {\n _statusMessage.value = event.message\n }\n is StreamEvent.Delta -> {\n fullResponse.append(event.text)\n _messages.value = _messages.value.map { msg ->\n if (msg.id == aiMessageId) msg.copy(text = fullResponse.toString()) else msg\n }\n }\n is StreamEvent.Done -> {\n _statusMessage.value = null\n _isLoading.value = false\n saveAiMessage(fullResponse.toString())\n }\n is StreamEvent.Error -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (fullResponse.isEmpty()) {\n _messages.value = _messages.value.map { msg ->\n if (msg.id == aiMessageId) msg.copy(text = \"❌ ${event.message}\") else msg\n }\n } else {\n saveAiMessage(fullResponse.toString())\n }\n }\n is StreamEvent.TokenUsage -> {\n accumulateTokenUsage(event.total, event.input, event.output)\n }\n }\n }\n } catch (e: Exception) {\n _isLoading.value = false\n _statusMessage.value = null\n val errorMsg = \"❌ DeepSeek 连接失败: ${e.message ?: \"未知错误\"}\"\n if (fullResponse.isEmpty()) {\n _messages.value = _messages.value.map { msg ->\n if (msg.id == aiMessageId) msg.copy(text = errorMsg) else msg\n }\n } else {\n saveAiMessage(fullResponse.toString())\n }\n }\n }\n\n // ============ Coze 流式对话 ============\n private suspend fun streamChatFromCoze(message: String) {\n // 创建空的 AI 回复消息占位\n val aiMessageId = System.nanoTime()\n val aiMessage = ChatMessage(\n id = aiMessageId,\n text = \"\",\n isUser = false\n )\n _messages.value = (_messages.value + aiMessage).takeLast(100)\n\n var fullResponse = StringBuilder()\n\n try {\n // 更新 CozeApiClient 的配置(可能用户在设置中更改了)\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n val pat = savedPat.ifBlank { Constants.DEFAULT_COZE_PAT }\n val botId = savedBotId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n\n val client = CozeApiClient(\n baseUrl = Constants.COZE_API_BASE_URL,\n pat = pat,\n botId = botId,\n httpClient = provideTempHttpClient()\n )\n\n client.streamChat(\n message = message,\n conversationId = cozeConversationId\n ).collect { event ->\n when (event) {\n is StreamEvent.Status -> {\n _statusMessage.value = event.message\n }\n is StreamEvent.Delta -> {\n fullResponse.append(event.text)\n // 更新 AI 消息内容\n _messages.value = _messages.value.map { msg ->\n if (msg.id == aiMessageId) msg.copy(text = fullResponse.toString()) else msg\n }\n }\n is StreamEvent.Done -> {\n cozeConversationId = event.conversationId\n _statusMessage.value = null\n _isLoading.value = false\n\n // 保存到数据库\n saveAiMessage(fullResponse.toString())\n }\n is StreamEvent.Error -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (fullResponse.isEmpty()) {\n _messages.value = _messages.value.map { msg ->\n if (msg.id == aiMessageId) msg.copy(text = \"❌ ${event.message}\") else msg\n }\n } else {\n saveAiMessage(fullResponse.toString())\n }\n }\n is StreamEvent.TokenUsage -> {\n // 累计 token 消耗\n accumulateTokenUsage(event.total, event.input, event.output)\n }\n }\n }\n } catch (e: Exception) {\n _isLoading.value = false\n _statusMessage.value = null\n val errorMsg = \"❌ 连接失败: ${e.message ?: \"未知错误\"}\"\n if (fullResponse.isEmpty()) {\n _messages.value = _messages.value.map { msg ->\n if (msg.id == aiMessageId) msg.copy(text = errorMsg) else msg\n }\n } else {\n saveAiMessage(fullResponse.toString())\n }\n }\n }\n\n private fun provideTempHttpClient(): okhttp3.OkHttpClient {\n return okhttp3.OkHttpClient.Builder()\n .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)\n .readTimeout(120, java.util.concurrent.TimeUnit.SECONDS)\n .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS)\n .build()\n }\n\n private suspend fun saveAiMessage(text: String) {\n withContext(Dispatchers.IO) {\n try {\n chatMessageDao.insert(ChatMessageEntity(\n text = text,\n isUser = false,\n timestamp = System.currentTimeMillis(),\n sessionId = currentSessionId\n ))\n } catch (_: Exception) {}\n }\n }\n\n // ============ 构建历史对话 ============\n private fun buildHistoryFromMessages(): List> {\n val history = mutableListOf>()\n val chatMessages = _messages.value.filter { !it.isSystem }\n // 取最近的对话(排除空的AI占位消息)\n for (msg in chatMessages) {\n if (msg.text.isNotBlank()) {\n history.add(Pair(if (msg.isUser) \"user\" else \"assistant\", msg.text))\n }\n }\n return history\n }\n\n // ============ Token 消耗累计 ============\n companion object {\n private const val KEY_TOTAL_TOKENS = \"total_tokens_consumed\"\n private const val KEY_TOTAL_INPUT = \"total_input_tokens\"\n private const val KEY_TOTAL_OUTPUT = \"total_output_tokens\"\n private const val KEY_TOTAL_MESSAGES = \"total_ai_messages\"\n }\n\n private fun accumulateTokenUsage(total: Int, input: Int, output: Int) {\n val editor = securePrefs.edit()\n editor.putInt(KEY_TOTAL_TOKENS, securePrefs.getInt(KEY_TOTAL_TOKENS, 0) + total)\n editor.putInt(KEY_TOTAL_INPUT, securePrefs.getInt(KEY_TOTAL_INPUT, 0) + input)\n editor.putInt(KEY_TOTAL_OUTPUT, securePrefs.getInt(KEY_TOTAL_OUTPUT, 0) + output)\n editor.putInt(KEY_TOTAL_MESSAGES, securePrefs.getInt(KEY_TOTAL_MESSAGES, 0) + 1)\n editor.apply()\n }\n\n private suspend fun processImage(uri: String): String {\n return withContext(Dispatchers.IO) {\n try {\n val context = getApplication()\n val inputStream = context.contentResolver.openInputStream(Uri.parse(uri))\n val bitmap = BitmapFactory.decodeStream(inputStream)\n inputStream?.close()\n\n if (bitmap != null) {\n val byteArrayOutputStream = ByteArrayOutputStream()\n bitmap.compress(Bitmap.CompressFormat.JPEG, 80, byteArrayOutputStream)\n val base64 = Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)\n \"图片已处理(${bitmap.width}x${bitmap.height})\"\n } else {\n \"图片无法解析\"\n }\n } catch (e: Exception) {\n \"图片处理失败: ${e.message}\"\n }\n }\n }\n\n // ============ 新建对话 ============\n fun newConversation() {\n streamJob?.cancel()\n currentSessionId = System.currentTimeMillis()\n cozeConversationId = null\n _conversationName.value = \"新对话\"\n _messages.value = listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n _isLoading.value = false\n _statusMessage.value = null\n loadSessionList()\n }\n\n // ============ 清空当前对话 ============\n fun clearCurrentChat() {\n streamJob?.cancel()\n _messages.value = listOf(ChatMessage(\n text = \"对话已清空,开始新的对话吧 💬\",\n isUser = false\n ))\n _isLoading.value = false\n _statusMessage.value = null\n\n viewModelScope.launch(Dispatchers.IO) {\n try {\n chatMessageDao.deleteBySession(currentSessionId)\n } catch (_: Exception) {}\n }\n }\n\n // ============ 从数据库加载消息 ============\n private fun loadMessagesFromDb() {\n viewModelScope.launch {\n withContext(Dispatchers.IO) {\n val entities = chatMessageDao.getMessagesBySession(currentSessionId)\n val chatMessages = entities.map { entity ->\n ChatMessage(\n text = entity.text,\n isUser = entity.isUser,\n imageUri = entity.imageUri,\n timestamp = entity.timestamp\n )\n }\n if (chatMessages.isNotEmpty()) {\n _messages.value = chatMessages\n }\n }\n }\n }\n\n override fun onCleared() {\n super.onCleared()\n streamJob?.cancel()\n }\n}\n"}, "a019d8e678f6c2d3b3e137fccadebc65eada7d59": {"t": 1785755792.48937, "n": 19201, "h": "7b1efb69007970af", "c": "package com.meitu.generator.ui.assistant\n\nimport android.app.Application\nimport android.content.SharedPreferences\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.net.Uri\nimport android.util.Base64\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.meitu.generator.data.remote.CozeApiClient\nimport com.meitu.generator.data.remote.StreamEvent\nimport com.meitu.generator.data.remote.TokenUsage\nimport com.meitu.generator.data.local.dao.ChatMessageDao\nimport com.meitu.generator.data.local.dao.SessionSummary\nimport com.meitu.generator.data.local.entity.ChatMessageEntity\nimport com.meitu.generator.repository.SettingsRepository\nimport com.meitu.generator.util.Constants\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport java.io.ByteArrayOutputStream\nimport javax.inject.Inject\nimport javax.inject.Named\n\ndata class ChatMessage(\n val id: Long = System.nanoTime(),\n val text: String,\n val isUser: Boolean,\n val isSystem: Boolean = false,\n val imageUri: String? = null,\n val timestamp: Long = System.currentTimeMillis()\n)\n\n@HiltViewModel\nclass AssistantViewModel @Inject constructor(\n application: Application,\n private val settingsRepo: SettingsRepository,\n private val cozeClient: CozeApiClient,\n @Named(\"securePrefs\") private val securePrefs: SharedPreferences,\n private val chatMessageDao: ChatMessageDao\n) : AndroidViewModel(application) {\n\n // ============ Chat Messages ============\n private val _messages = MutableStateFlow>(\n listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n )\n val messages: StateFlow> = _messages.asStateFlow()\n\n // ============ 会话管理 ============\n private var currentSessionId: Long = System.currentTimeMillis()\n private var cozeConversationId: String? = null\n\n // ============ 对话名称 ============\n private val _conversationName = MutableStateFlow(\"新对话\")\n val conversationName: StateFlow = _conversationName.asStateFlow()\n\n fun setConversationName(name: String) {\n _conversationName.value = name\n }\n\n fun autoGenerateConversationName(firstMessage: String) {\n if (_conversationName.value == \"新对话\" && firstMessage.isNotBlank()) {\n val name = if (firstMessage.length > 15) firstMessage.take(15) + \"...\" else firstMessage\n _conversationName.value = name\n }\n }\n\n // ============ 历史对话列表 ============\n private val _sessionList = MutableStateFlow>(emptyList())\n val sessionList: StateFlow> = _sessionList.asStateFlow()\n\n fun loadSessionList() {\n viewModelScope.launch {\n withContext(Dispatchers.IO) {\n val sessions = chatMessageDao.getSessionSummaries()\n _sessionList.value = sessions\n }\n }\n }\n\n fun switchToSession(sessionId: Long) {\n currentSessionId = sessionId\n cozeConversationId = null // 切换会话时重置 Coze 对话 ID\n viewModelScope.launch {\n loadMessagesFromDb()\n // 恢复对话名称\n val messages = _messages.value\n val firstUserMsg = messages.firstOrNull { it.isUser }\n _conversationName.value = if (firstUserMsg != null) {\n if (firstUserMsg.text.length > 15) firstUserMsg.text.take(15) + \"...\" else firstUserMsg.text\n } else {\n \"历史对话\"\n }\n }\n }\n\n private val _inputText = MutableStateFlow(\"\")\n val inputText: StateFlow = _inputText.asStateFlow()\n\n fun setInputText(text: String) {\n _inputText.value = text\n }\n\n // ============ 附件(图片) ============\n private val _pendingImageUri = MutableStateFlow(null)\n val pendingImageUri: StateFlow = _pendingImageUri.asStateFlow()\n\n fun setPendingImageUri(uri: String?) {\n _pendingImageUri.value = uri\n }\n\n // ============ Loading ============\n private val _isLoading = MutableStateFlow(false)\n val isLoading: StateFlow = _isLoading.asStateFlow()\n\n // ============ 状态 ============\n private val _statusMessage = MutableStateFlow(null)\n val statusMessage: StateFlow = _statusMessage.asStateFlow()\n\n // ============ 当前流式任务的 Job ============\n private var streamJob: Job? = null\n\n // ============ Coze 配置状态 ============\n private val _isCozeConfigured = MutableStateFlow(false)\n val isCozeConfigured: StateFlow = _isCozeConfigured.asStateFlow()\n\n // ============ 当前 AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow = _currentChannel.asStateFlow()\n\n init {\n checkCozeConfig()\n loadChannel()\n loadSessionList()\n }\n\n private fun loadChannel() {\n val channel = securePrefs.getString(Constants.KEY_AI_CHANNEL, Constants.CHANNEL_COZE) ?: Constants.CHANNEL_COZE\n _currentChannel.value = channel\n }\n\n private fun checkCozeConfig() {\n viewModelScope.launch {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n // 使用默认值回退:用户未配置时自动使用内置的 PAT 和 Bot ID\n val effectivePat = savedPat.ifBlank { Constants.DEFAULT_COZE_PAT }\n val effectiveBotId = savedBotId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n _isCozeConfigured.value = effectivePat.isNotBlank() && effectiveBotId.isNotBlank()\n }\n }\n\n fun refreshCozeConfig() {\n checkCozeConfig()\n loadChannel()\n }\n\n // ============ 发送消息 ============\n fun sendMessage() {\n val text = _inputText.value.trim()\n val imageUri = _pendingImageUri.value\n\n if (text.isEmpty() && imageUri == null) return\n\n val channel = _currentChannel.value\n if (channel == Constants.CHANNEL_COZE && !_isCozeConfigured.value) {\n _statusMessage.value = \"请先在设置中配置 Coze PAT 和 Bot ID\"\n return\n }\n if (channel == Constants.CHANNEL_DEEPSEEK) {\n val apiKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n if (apiKey.isBlank()) {\n _statusMessage.value = \"请先在设置中配置 DeepSeek API Key\"\n return\n }\n }\n\n // 生成对话名称\n autoGenerateConversationName(text.ifEmpty { \"[图片]\" })\n\n // 添加用户消息到列表\n val userMessage = ChatMessage(\n text = text.ifEmpty { \"[图片]\" },\n isUser = true,\n imageUri = imageUri\n )\n _messages.value = (_messages.value + userMessage).takeLast(100)\n _inputText.value = \"\"\n _pendingImageUri.value = null\n _isLoading.value = true\n\n // 保存用户消息到数据库\n viewModelScope.launch(Dispatchers.IO) {\n try {\n chatMessageDao.insert(ChatMessageEntity(\n text = userMessage.text,\n isUser = true,\n imageUri = userMessage.imageUri,\n timestamp = userMessage.timestamp,\n sessionId = currentSessionId\n ))\n loadSessionList()\n } catch (_: Exception) {}\n }\n\n // 构建消息内容(如果有图片,先分析图片)\n viewModelScope.launch {\n var messageContent = text\n if (imageUri != null) {\n _statusMessage.value = \"🖼️ 正在处理图片...\"\n val imageDescription = processImage(imageUri)\n messageContent = if (text.isNotEmpty()) {\n \"$text\\n\\n[用户发送了一张图片,图片内容描述: $imageDescription]\"\n } else {\n \"[用户发送了一张图片,图片内容描述: $imageDescription]\"\n }\n }\n\n // 根据通道选择调用\n when (channel) {\n Constants.CHANNEL_DEEPSEEK -> streamChatFromDeepSeek(messageContent)\n else -> streamChatFromCoze(messageContent)\n }\n }\n }\n\n // ============ DeepSeek 直连 ============\n private suspend fun streamChatFromDeepSeek(message: String) {\n val aiMessageId = System.nanoTime()\n val aiMessage = ChatMessage(id = aiMessageId, text = \"\", isUser = false)\n _messages.value = (_messages.value + aiMessage).takeLast(100)\n\n var fullResponse = StringBuilder()\n\n try {\n val apiKey = securePrefs.getString(Constants.KEY_DEEPSEEK_API_KEY, \"\") ?: \"\"\n val systemPrompt = securePrefs.getString(\"deepseek_system_prompt\", \"\") ?: \"\"\n\n // 构建历史对话\n val history = buildHistoryFromMessages()\n\n cozeClient.streamDeepSeekChat(\n apiKey = apiKey,\n message = message,\n systemPrompt = systemPrompt.ifBlank { null },\n history = history\n ).collect { event ->\n when (event) {\n is StreamEvent.Status -> {\n _statusMessage.value = event.message\n }\n is StreamEvent.Delta -> {\n fullResponse.append(event.text)\n _messages.value = _messages.value.map { msg ->\n if (msg.id == aiMessageId) msg.copy(text = fullResponse.toString()) else msg\n }\n }\n is StreamEvent.Done -> {\n _statusMessage.value = null\n _isLoading.value = false\n saveAiMessage(fullResponse.toString())\n }\n is StreamEvent.Error -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (fullResponse.isEmpty()) {\n _messages.value = _messages.value.map { msg ->\n if (msg.id == aiMessageId) msg.copy(text = \"❌ ${event.message}\") else msg\n }\n } else {\n saveAiMessage(fullResponse.toString())\n }\n }\n is StreamEvent.TokenUsage -> {\n accumulateTokenUsage(event.total, event.input, event.output)\n }\n }\n }\n } catch (e: Exception) {\n _isLoading.value = false\n _statusMessage.value = null\n val errorMsg = \"❌ DeepSeek 连接失败: ${e.message ?: \"未知错误\"}\"\n if (fullResponse.isEmpty()) {\n _messages.value = _messages.value.map { msg ->\n if (msg.id == aiMessageId) msg.copy(text = errorMsg) else msg\n }\n } else {\n saveAiMessage(fullResponse.toString())\n }\n }\n }\n\n // ============ Coze 流式对话 ============\n private suspend fun streamChatFromCoze(message: String) {\n // 创建空的 AI 回复消息占位\n val aiMessageId = System.nanoTime()\n val aiMessage = ChatMessage(\n id = aiMessageId,\n text = \"\",\n isUser = false\n )\n _messages.value = (_messages.value + aiMessage).takeLast(100)\n\n var fullResponse = StringBuilder()\n\n try {\n // 更新 CozeApiClient 的配置(可能用户在设置中更改了)\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n val pat = savedPat.ifBlank { Constants.DEFAULT_COZE_PAT }\n val botId = savedBotId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n\n val client = CozeApiClient(\n baseUrl = Constants.COZE_API_BASE_URL,\n pat = pat,\n botId = botId,\n httpClient = provideTempHttpClient()\n )\n\n client.streamChat(\n message = message,\n conversationId = cozeConversationId\n ).collect { event ->\n when (event) {\n is StreamEvent.Status -> {\n _statusMessage.value = event.message\n }\n is StreamEvent.Delta -> {\n fullResponse.append(event.text)\n // 更新 AI 消息内容\n _messages.value = _messages.value.map { msg ->\n if (msg.id == aiMessageId) msg.copy(text = fullResponse.toString()) else msg\n }\n }\n is StreamEvent.Done -> {\n cozeConversationId = event.conversationId\n _statusMessage.value = null\n _isLoading.value = false\n\n // 保存到数据库\n saveAiMessage(fullResponse.toString())\n }\n is StreamEvent.Error -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (fullResponse.isEmpty()) {\n _messages.value = _messages.value.map { msg ->\n if (msg.id == aiMessageId) msg.copy(text = \"❌ ${event.message}\") else msg\n }\n } else {\n saveAiMessage(fullResponse.toString())\n }\n }\n is StreamEvent.TokenUsage -> {\n // 累计 token 消耗\n accumulateTokenUsage(event.total, event.input, event.output)\n }\n }\n }\n } catch (e: Exception) {\n _isLoading.value = false\n _statusMessage.value = null\n val errorMsg = \"❌ 连接失败: ${e.message ?: \"未知错误\"}\"\n if (fullResponse.isEmpty()) {\n _messages.value = _messages.value.map { msg ->\n if (msg.id == aiMessageId) msg.copy(text = errorMsg) else msg\n }\n } else {\n saveAiMessage(fullResponse.toString())\n }\n }\n }\n\n private fun provideTempHttpClient(): okhttp3.OkHttpClient {\n return okhttp3.OkHttpClient.Builder()\n .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)\n .readTimeout(120, java.util.concurrent.TimeUnit.SECONDS)\n .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS)\n .build()\n }\n\n private suspend fun saveAiMessage(text: String) {\n withContext(Dispatchers.IO) {\n try {\n chatMessageDao.insert(ChatMessageEntity(\n text = text,\n isUser = false,\n timestamp = System.currentTimeMillis(),\n sessionId = currentSessionId\n ))\n } catch (_: Exception) {}\n }\n }\n\n // ============ 构建历史对话 ============\n private fun buildHistoryFromMessages(): List> {\n val history = mutableListOf>()\n val chatMessages = _messages.value.filter { !it.isSystem }\n // 取最近的对话(排除空的AI占位消息)\n for (msg in chatMessages) {\n if (msg.text.isNotBlank()) {\n history.add(Pair(if (msg.isUser) \"user\" else \"assistant\", msg.text))\n }\n }\n return history\n }\n\n // ============ Token 消耗累计 ============\n companion object {\n private const val KEY_TOTAL_TOKENS = \"total_tokens_consumed\"\n private const val KEY_TOTAL_INPUT = \"total_input_tokens\"\n private const val KEY_TOTAL_OUTPUT = \"total_output_tokens\"\n private const val KEY_TOTAL_MESSAGES = \"total_ai_messages\"\n }\n\n private fun accumulateTokenUsage(total: Int, input: Int, output: Int) {\n val editor = securePrefs.edit()\n editor.putInt(KEY_TOTAL_TOKENS, securePrefs.getInt(KEY_TOTAL_TOKENS, 0) + total)\n editor.putInt(KEY_TOTAL_INPUT, securePrefs.getInt(KEY_TOTAL_INPUT, 0) + input)\n editor.putInt(KEY_TOTAL_OUTPUT, securePrefs.getInt(KEY_TOTAL_OUTPUT, 0) + output)\n editor.putInt(KEY_TOTAL_MESSAGES, securePrefs.getInt(KEY_TOTAL_MESSAGES, 0) + 1)\n editor.apply()\n }\n\n private suspend fun processImage(uri: String): String {\n return withContext(Dispatchers.IO) {\n try {\n val context = getApplication()\n val inputStream = context.contentResolver.openInputStream(Uri.parse(uri))\n val bitmap = BitmapFactory.decodeStream(inputStream)\n inputStream?.close()\n\n if (bitmap != null) {\n val byteArrayOutputStream = ByteArrayOutputStream()\n bitmap.compress(Bitmap.CompressFormat.JPEG, 80, byteArrayOutputStream)\n val base64 = Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)\n \"图片已处理(${bitmap.width}x${bitmap.height})\"\n } else {\n \"图片无法解析\"\n }\n } catch (e: Exception) {\n \"图片处理失败: ${e.message}\"\n }\n }\n }\n\n // ============ 新建对话 ============\n fun newConversation() {\n streamJob?.cancel()\n currentSessionId = System.currentTimeMillis()\n cozeConversationId = null\n _conversationName.value = \"新对话\"\n _messages.value = listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n _isLoading.value = false\n _statusMessage.value = null\n loadSessionList()\n }\n\n // ============ 清空当前对话 ============\n fun clearCurrentChat() {\n streamJob?.cancel()\n _messages.value = listOf(ChatMessage(\n text = \"对话已清空,开始新的对话吧 💬\",\n isUser = false\n ))\n _isLoading.value = false\n _statusMessage.value = null\n\n viewModelScope.launch(Dispatchers.IO) {\n try {\n chatMessageDao.deleteBySession(currentSessionId)\n } catch (_: Exception) {}\n }\n }\n\n // ============ 从数据库加载消息 ============\n private fun loadMessagesFromDb() {\n viewModelScope.launch {\n withContext(Dispatchers.IO) {\n val entities = chatMessageDao.getMessagesBySession(currentSessionId)\n val chatMessages = entities.map { entity ->\n ChatMessage(\n text = entity.text,\n isUser = entity.isUser,\n imageUri = entity.imageUri,\n timestamp = entity.timestamp\n )\n }\n if (chatMessages.isNotEmpty()) {\n _messages.value = chatMessages\n }\n }\n }\n }\n\n override fun onCleared() {\n super.onCleared()\n streamJob?.cancel()\n }\n}\n"}, "d37f4ae1948d01f521e597e35fc90e4b0359b727": {"t": 1785755794.0478723, "n": 14909, "h": "d612b5efe22aa882", "c": "package com.meitu.generator.ui.assistant\n\nimport android.app.Application\nimport android.content.SharedPreferences\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.net.Uri\nimport android.util.Base64\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.meitu.generator.data.remote.CozeApiClient\nimport com.meitu.generator.data.remote.StreamEvent\nimport com.meitu.generator.data.remote.TokenUsage\nimport com.meitu.generator.data.local.dao.ChatMessageDao\nimport com.meitu.generator.data.local.dao.SessionSummary\nimport com.meitu.generator.data.local.entity.ChatMessageEntity\nimport com.meitu.generator.repository.SettingsRepository\nimport com.meitu.generator.util.Constants\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport java.io.ByteArrayOutputStream\nimport javax.inject.Inject\nimport javax.inject.Named\n\ndata class ChatMessage(\n val id: Long = System.nanoTime(),\n val text: String,\n val isUser: Boolean,\n val isSystem: Boolean = false,\n val imageUri: String? = null,\n val timestamp: Long = System.currentTimeMillis()\n)\n\n@HiltViewModel\nclass AssistantViewModel @Inject constructor(\n application: Application,\n private val settingsRepo: SettingsRepository,\n private val cozeClient: CozeApiClient,\n @Named(\"securePrefs\") private val securePrefs: SharedPreferences,\n private val chatMessageDao: ChatMessageDao\n) : AndroidViewModel(application) {\n\n // ============ Chat Messages ============\n private val _messages = MutableStateFlow>(\n listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n )\n val messages: StateFlow> = _messages.asStateFlow()\n\n // ============ 会话管理 ============\n private var currentSessionId: Long = System.currentTimeMillis()\n private var cozeConversationId: String? = null\n\n // ============ 对话名称 ============\n private val _conversationName = MutableStateFlow(\"新对话\")\n val conversationName: StateFlow = _conversationName.asStateFlow()\n\n fun setConversationName(name: String) {\n _conversationName.value = name\n }\n\n fun autoGenerateConversationName(firstMessage: String) {\n if (_conversationName.value == \"新对话\" && firstMessage.isNotBlank()) {\n val name = if (firstMessage.length > 15) firstMessage.take(15) + \"...\" else firstMessage\n _conversationName.value = name\n }\n }\n\n // ============ 历史对话列表 ============\n private val _sessionList = MutableStateFlow>(emptyList())\n val sessionList: StateFlow> = _sessionList.asStateFlow()\n\n fun loadSessionList() {\n viewModelScope.launch {\n withContext(Dispatchers.IO) {\n val sessions = chatMessageDao.getSessionSummaries()\n _sessionList.value = sessions\n }\n }\n }\n\n fun switchToSession(sessionId: Long) {\n currentSessionId = sessionId\n cozeConversationId = null // 切换会话时重置 Coze 对话 ID\n viewModelScope.launch {\n loadMessagesFromDb()\n // 恢复对话名称\n val messages = _messages.value\n val firstUserMsg = messages.firstOrNull { it.isUser }\n _conversationName.value = if (firstUserMsg != null) {\n if (firstUserMsg.text.length > 15) firstUserMsg.text.take(15) + \"...\" else firstUserMsg.text\n } else {\n \"历史对话\"\n }\n }\n }\n\n private val _inputText = MutableStateFlow(\"\")\n val inputText: StateFlow = _inputText.asStateFlow()\n\n fun setInputText(text: String) {\n _inputText.value = text\n }\n\n // ============ 附件(图片) ============\n private val _pendingImageUri = MutableStateFlow(null)\n val pendingImageUri: StateFlow = _pendingImageUri.asStateFlow()\n\n fun setPendingImageUri(uri: String?) {\n _pendingImageUri.value = uri\n }\n\n // ============ Loading ============\n private val _isLoading = MutableStateFlow(false)\n val isLoading: StateFlow = _isLoading.asStateFlow()\n\n // ============ 状态 ============\n private val _statusMessage = MutableStateFlow(null)\n val statusMessage: StateFlow = _statusMessage.asStateFlow()\n\n // ============ 当前流式任务的 Job ============\n private var streamJob: Job? = null\n\n // ============ Coze 配置状态 ============\n private val _isCozeConfigured = MutableStateFlow(false)\n val isCozeConfigured: StateFlow = _isCozeConfigured.asStateFlow()\n\n init {\n checkCozeConfig()\n loadSessionList()\n }\n\n private fun checkCozeConfig() {\n viewModelScope.launch {\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n // 使用默认值回退:用户未配置时自动使用内置的 PAT 和 Bot ID\n val effectivePat = savedPat.ifBlank { Constants.DEFAULT_COZE_PAT }\n val effectiveBotId = savedBotId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n _isCozeConfigured.value = effectivePat.isNotBlank() && effectiveBotId.isNotBlank()\n }\n }\n\n fun refreshCozeConfig() {\n checkCozeConfig()\n }\n\n // ============ 发送消息 ============\n fun sendMessage() {\n val text = _inputText.value.trim()\n val imageUri = _pendingImageUri.value\n\n if (text.isEmpty() && imageUri == null) return\n if (!_isCozeConfigured.value) {\n _statusMessage.value = \"请先在设置中配置 Coze PAT 和 Bot ID\"\n return\n }\n\n // 生成对话名称\n autoGenerateConversationName(text.ifEmpty { \"[图片]\" })\n\n // 添加用户消息到列表\n val userMessage = ChatMessage(\n text = text.ifEmpty { \"[图片]\" },\n isUser = true,\n imageUri = imageUri\n )\n _messages.value = (_messages.value + userMessage).takeLast(100)\n _inputText.value = \"\"\n _pendingImageUri.value = null\n _isLoading.value = true\n\n // 保存用户消息到数据库\n viewModelScope.launch(Dispatchers.IO) {\n try {\n chatMessageDao.insert(ChatMessageEntity(\n text = userMessage.text,\n isUser = true,\n imageUri = userMessage.imageUri,\n timestamp = userMessage.timestamp,\n sessionId = currentSessionId\n ))\n loadSessionList()\n } catch (_: Exception) {}\n }\n\n // 构建消息内容(如果有图片,先分析图片)\n viewModelScope.launch {\n var messageContent = text\n if (imageUri != null) {\n _statusMessage.value = \"🖼️ 正在处理图片...\"\n val imageDescription = processImage(imageUri)\n messageContent = if (text.isNotEmpty()) {\n \"$text\\n\\n[用户发送了一张图片,图片内容描述: $imageDescription]\"\n } else {\n \"[用户发送了一张图片,图片内容描述: $imageDescription]\"\n }\n }\n\n // 流式调用 Coze API\n streamChatFromCoze(messageContent)\n }\n }\n\n private suspend fun streamChatFromCoze(message: String) {\n // 创建空的 AI 回复消息占位\n val aiMessageId = System.nanoTime()\n val aiMessage = ChatMessage(\n id = aiMessageId,\n text = \"\",\n isUser = false\n )\n _messages.value = (_messages.value + aiMessage).takeLast(100)\n\n var fullResponse = StringBuilder()\n\n try {\n // 更新 CozeApiClient 的配置(可能用户在设置中更改了)\n val savedPat = securePrefs.getString(Constants.KEY_COZE_PAT, \"\") ?: \"\"\n val savedBotId = settingsRepo.getString(Constants.KEY_COZE_BOT_ID, \"\")\n val pat = savedPat.ifBlank { Constants.DEFAULT_COZE_PAT }\n val botId = savedBotId.ifBlank { Constants.DEFAULT_COZE_BOT_ID }\n\n val client = CozeApiClient(\n baseUrl = Constants.COZE_API_BASE_URL,\n pat = pat,\n botId = botId,\n httpClient = provideTempHttpClient()\n )\n\n client.streamChat(\n message = message,\n conversationId = cozeConversationId\n ).collect { event ->\n when (event) {\n is StreamEvent.Status -> {\n _statusMessage.value = event.message\n }\n is StreamEvent.Delta -> {\n fullResponse.append(event.text)\n // 更新 AI 消息内容\n _messages.value = _messages.value.map { msg ->\n if (msg.id == aiMessageId) msg.copy(text = fullResponse.toString()) else msg\n }\n }\n is StreamEvent.Done -> {\n cozeConversationId = event.conversationId\n _statusMessage.value = null\n _isLoading.value = false\n\n // 保存到数据库\n saveAiMessage(fullResponse.toString())\n }\n is StreamEvent.Error -> {\n _statusMessage.value = null\n _isLoading.value = false\n if (fullResponse.isEmpty()) {\n _messages.value = _messages.value.map { msg ->\n if (msg.id == aiMessageId) msg.copy(text = \"❌ ${event.message}\") else msg\n }\n } else {\n saveAiMessage(fullResponse.toString())\n }\n }\n is StreamEvent.TokenUsage -> {\n // 累计 token 消耗\n accumulateTokenUsage(event.total, event.input, event.output)\n }\n }\n }\n } catch (e: Exception) {\n _isLoading.value = false\n _statusMessage.value = null\n val errorMsg = \"❌ 连接失败: ${e.message ?: \"未知错误\"}\"\n if (fullResponse.isEmpty()) {\n _messages.value = _messages.value.map { msg ->\n if (msg.id == aiMessageId) msg.copy(text = errorMsg) else msg\n }\n } else {\n saveAiMessage(fullResponse.toString())\n }\n }\n }\n\n private fun provideTempHttpClient(): okhttp3.OkHttpClient {\n return okhttp3.OkHttpClient.Builder()\n .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)\n .readTimeout(120, java.util.concurrent.TimeUnit.SECONDS)\n .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS)\n .build()\n }\n\n private suspend fun saveAiMessage(text: String) {\n withContext(Dispatchers.IO) {\n try {\n chatMessageDao.insert(ChatMessageEntity(\n text = text,\n isUser = false,\n timestamp = System.currentTimeMillis(),\n sessionId = currentSessionId\n ))\n } catch (_: Exception) {}\n }\n }\n\n // ============ Token 消耗累计 ============\n companion object {\n private const val KEY_TOTAL_TOKENS = \"total_tokens_consumed\"\n private const val KEY_TOTAL_INPUT = \"total_input_tokens\"\n private const val KEY_TOTAL_OUTPUT = \"total_output_tokens\"\n private const val KEY_TOTAL_MESSAGES = \"total_ai_messages\"\n }\n\n private fun accumulateTokenUsage(total: Int, input: Int, output: Int) {\n val editor = securePrefs.edit()\n editor.putInt(KEY_TOTAL_TOKENS, securePrefs.getInt(KEY_TOTAL_TOKENS, 0) + total)\n editor.putInt(KEY_TOTAL_INPUT, securePrefs.getInt(KEY_TOTAL_INPUT, 0) + input)\n editor.putInt(KEY_TOTAL_OUTPUT, securePrefs.getInt(KEY_TOTAL_OUTPUT, 0) + output)\n editor.putInt(KEY_TOTAL_MESSAGES, securePrefs.getInt(KEY_TOTAL_MESSAGES, 0) + 1)\n editor.apply()\n }\n\n private suspend fun processImage(uri: String): String {\n return withContext(Dispatchers.IO) {\n try {\n val context = getApplication()\n val inputStream = context.contentResolver.openInputStream(Uri.parse(uri))\n val bitmap = BitmapFactory.decodeStream(inputStream)\n inputStream?.close()\n\n if (bitmap != null) {\n val byteArrayOutputStream = ByteArrayOutputStream()\n bitmap.compress(Bitmap.CompressFormat.JPEG, 80, byteArrayOutputStream)\n val base64 = Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)\n \"图片已处理(${bitmap.width}x${bitmap.height})\"\n } else {\n \"图片无法解析\"\n }\n } catch (e: Exception) {\n \"图片处理失败: ${e.message}\"\n }\n }\n }\n\n // ============ 新建对话 ============\n fun newConversation() {\n streamJob?.cancel()\n currentSessionId = System.currentTimeMillis()\n cozeConversationId = null\n _conversationName.value = \"新对话\"\n _messages.value = listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n _isLoading.value = false\n _statusMessage.value = null\n loadSessionList()\n }\n\n // ============ 清空当前对话 ============\n fun clearCurrentChat() {\n streamJob?.cancel()\n _messages.value = listOf(ChatMessage(\n text = \"对话已清空,开始新的对话吧 💬\",\n isUser = false\n ))\n _isLoading.value = false\n _statusMessage.value = null\n\n viewModelScope.launch(Dispatchers.IO) {\n try {\n chatMessageDao.deleteBySession(currentSessionId)\n } catch (_: Exception) {}\n }\n }\n\n // ============ 从数据库加载消息 ============\n private fun loadMessagesFromDb() {\n viewModelScope.launch {\n withContext(Dispatchers.IO) {\n val entities = chatMessageDao.getMessagesBySession(currentSessionId)\n val chatMessages = entities.map { entity ->\n ChatMessage(\n text = entity.text,\n isUser = entity.isUser,\n imageUri = entity.imageUri,\n timestamp = entity.timestamp\n )\n }\n if (chatMessages.isNotEmpty()) {\n _messages.value = chatMessages\n }\n }\n }\n }\n\n override fun onCleared() {\n super.onCleared()\n streamJob?.cancel()\n }\n}\n"}}}}