Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/abuaibobo-dev_xiaonaodai-android.json
T

1 line
492 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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<String> = _cozePat.asStateFlow()\n\n private val _cozeBotId = MutableStateFlow(\"\")\n val cozeBotId: StateFlow<String> = _cozeBotId.asStateFlow()\n\n private val _isUsingDefaultPat = MutableStateFlow(true)\n val isUsingDefaultPat: StateFlow<Boolean> = _isUsingDefaultPat.asStateFlow()\n\n private val _isUsingDefaultBotId = MutableStateFlow(true)\n val isUsingDefaultBotId: StateFlow<Boolean> = _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<Map<String, ChannelTokenStats>>(emptyMap())\n val channelTokenStats: StateFlow<Map<String, ChannelTokenStats>> = _channelTokenStats.asStateFlow()\n\n private val _totalTokens = MutableStateFlow(0)\n val totalTokens: StateFlow<Int> = _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<String> = _currentChannel.asStateFlow()\n\n private val _deepseekApiKey = MutableStateFlow(\"\")\n val deepseekApiKey: StateFlow<String> = _deepseekApiKey.asStateFlow()\n\n // ============ DeepSeek 模型选择 ============\n private val _deepseekModel = MutableStateFlow(\"deepseek-v4-flash\")\n val deepseekModel: StateFlow<String> = _deepseekModel.asStateFlow()\n\n // ============ DeepSeek 余额 ============\n private val _deepseekBalance = MutableStateFlow<String?>(null)\n val deepseekBalance: StateFlow<String?> = _deepseekBalance.asStateFlow()\n private val _isLoadingBalance = MutableStateFlow(false)\n val isLoadingBalance: StateFlow<Boolean> = _isLoadingBalance.asStateFlow()\n\n // ============ 自定义 API 通道 ============\n private val _customApiList = MutableStateFlow<List<CustomApiConfig>>(emptyList())\n val customApiList: StateFlow<List<CustomApiConfig>> = _customApiList.asStateFlow()\n\n private fun loadCustomApiList() {\n val json = securePrefs.getString(Constants.KEY_CUSTOM_API_LIST, \"[]\") ?: \"[]\"\n val type = object : TypeToken<List<CustomApiConfig>>() {}.type\n _customApiList.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n }\n\n private fun saveCustomApiList(list: List<CustomApiConfig>) {\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<String> = _githubToken.asStateFlow()\n\n private val _githubUser = MutableStateFlow<GitHubUser?>(null)\n val githubUser: StateFlow<GitHubUser?> = _githubUser.asStateFlow()\n\n private val _githubRepos = MutableStateFlow<List<GitHubRepo>>(emptyList())\n val githubRepos: StateFlow<List<GitHubRepo>> = _githubRepos.asStateFlow()\n\n private val _githubNotifications = MutableStateFlow<List<GitHubNotification>>(emptyList())\n val githubNotifications: StateFlow<List<GitHubNotification>> = _githubNotifications.asStateFlow()\n\n private val _selectedRepoCommits = MutableStateFlow<List<GitHubCommit>>(emptyList())\n val selectedRepoCommits: StateFlow<List<GitHubCommit>> = _selectedRepoCommits.asStateFlow()\n\n private val _isLoadingGitHub = MutableStateFlow(false)\n val isLoadingGitHub: StateFlow<Boolean> = _isLoadingGitHub.asStateFlow()\n\n private val _githubError = MutableStateFlow<String?>(null)\n val githubError: StateFlow<String?> = _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<String> = _hfToken.asStateFlow()\n\n private val _hfModels = MutableStateFlow<List<HfModel>>(emptyList())\n val hfModels: StateFlow<List<HfModel>> = _hfModels.asStateFlow()\n\n private val _isLoadingHf = MutableStateFlow(false)\n val isLoadingHf: StateFlow<Boolean> = _isLoadingHf.asStateFlow()\n\n private val _hfError = MutableStateFlow<String?>(null)\n val hfError: StateFlow<String?> = _hfError.asStateFlow()\n\n private val _hfTrendingTag = MutableStateFlow(\"text-generation\")\n val hfTrendingTag: StateFlow<String> = _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<String> = _serverchanKey.asStateFlow()\n\n private val _pushplusToken = MutableStateFlow(\"\")\n val pushplusToken: StateFlow<String> = _pushplusToken.asStateFlow()\n\n private val _pushTestResult = MutableStateFlow<String?>(null)\n val pushTestResult: StateFlow<String?> = _pushTestResult.asStateFlow()\n\n private val _isTestingPush = MutableStateFlow(false)\n val isTestingPush: StateFlow<Boolean> = _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<List<BalanceCheckService>>(emptyList())\n val balanceServices: StateFlow<List<BalanceCheckService>> = _balanceServices.asStateFlow()\n\n private val _balanceResults = MutableStateFlow<Map<String, String>>(emptyMap())\n val balanceResults: StateFlow<Map<String, String>> = _balanceResults.asStateFlow()\n\n private val _isLoadingBalanceCheck = MutableStateFlow(false)\n val isLoadingBalanceCheck: StateFlow<Boolean> = _isLoadingBalanceCheck.asStateFlow()\n\n fun loadBalanceServices() {\n val json = securePrefs.getString(Constants.KEY_BALANCE_CHECK_LIST, \"[]\") ?: \"[]\"\n val type = object : TypeToken<List<BalanceCheckService>>() {}.type\n _balanceServices.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n }\n\n private fun saveBalanceServices(list: List<BalanceCheckService>) {\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<List<AgentConfig>>(emptyList())\n val agentList: StateFlow<List<AgentConfig>> = _agentList.asStateFlow()\n\n private val _currentAgentId = MutableStateFlow(\"\")\n val currentAgentId: StateFlow<String> = _currentAgentId.asStateFlow()\n\n private val _isCreatingAgent = MutableStateFlow(false)\n val isCreatingAgent: StateFlow<Boolean> = _isCreatingAgent.asStateFlow()\n\n // ============ Toast ============\n private val _toastMessage = MutableStateFlow<String?>(null)\n val toastMessage: StateFlow<String?> = _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<String, ChannelTokenStats>()\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<List<AgentConfig>>() {}.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<AgentConfig>) {\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<String> = _cozePat.asStateFlow()\n\n private val _cozeBotId = MutableStateFlow(\"\")\n val cozeBotId: StateFlow<String> = _cozeBotId.asStateFlow()\n\n private val _isUsingDefaultPat = MutableStateFlow(true)\n val isUsingDefaultPat: StateFlow<Boolean> = _isUsingDefaultPat.asStateFlow()\n\n private val _isUsingDefaultBotId = MutableStateFlow(true)\n val isUsingDefaultBotId: StateFlow<Boolean> = _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<Map<String, ChannelTokenStats>>(emptyMap())\n val channelTokenStats: StateFlow<Map<String, ChannelTokenStats>> = _channelTokenStats.asStateFlow()\n\n private val _totalTokens = MutableStateFlow(0)\n val totalTokens: StateFlow<Int> = _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<String> = _currentChannel.asStateFlow()\n\n private val _deepseekApiKey = MutableStateFlow(\"\")\n val deepseekApiKey: StateFlow<String> = _deepseekApiKey.asStateFlow()\n\n // ============ DeepSeek 模型选择 ============\n private val _deepseekModel = MutableStateFlow(\"deepseek-v4-flash\")\n val deepseekModel: StateFlow<String> = _deepseekModel.asStateFlow()\n\n // ============ DeepSeek 余额 ============\n private val _deepseekBalance = MutableStateFlow<String?>(null)\n val deepseekBalance: StateFlow<String?> = _deepseekBalance.asStateFlow()\n private val _isLoadingBalance = MutableStateFlow(false)\n val isLoadingBalance: StateFlow<Boolean> = _isLoadingBalance.asStateFlow()\n\n // ============ 自定义 API 通道 ============\n private val _customApiList = MutableStateFlow<List<CustomApiConfig>>(emptyList())\n val customApiList: StateFlow<List<CustomApiConfig>> = _customApiList.asStateFlow()\n\n private fun loadCustomApiList() {\n val json = securePrefs.getString(Constants.KEY_CUSTOM_API_LIST, \"[]\") ?: \"[]\"\n val type = object : TypeToken<List<CustomApiConfig>>() {}.type\n _customApiList.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n }\n\n private fun saveCustomApiList(list: List<CustomApiConfig>) {\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<String> = _githubToken.asStateFlow()\n\n private val _githubUser = MutableStateFlow<GitHubUser?>(null)\n val githubUser: StateFlow<GitHubUser?> = _githubUser.asStateFlow()\n\n private val _githubRepos = MutableStateFlow<List<GitHubRepo>>(emptyList())\n val githubRepos: StateFlow<List<GitHubRepo>> = _githubRepos.asStateFlow()\n\n private val _githubNotifications = MutableStateFlow<List<GitHubNotification>>(emptyList())\n val githubNotifications: StateFlow<List<GitHubNotification>> = _githubNotifications.asStateFlow()\n\n private val _selectedRepoCommits = MutableStateFlow<List<GitHubCommit>>(emptyList())\n val selectedRepoCommits: StateFlow<List<GitHubCommit>> = _selectedRepoCommits.asStateFlow()\n\n private val _isLoadingGitHub = MutableStateFlow(false)\n val isLoadingGitHub: StateFlow<Boolean> = _isLoadingGitHub.asStateFlow()\n\n private val _githubError = MutableStateFlow<String?>(null)\n val githubError: StateFlow<String?> = _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<String> = _hfToken.asStateFlow()\n\n private val _hfModels = MutableStateFlow<List<HfModel>>(emptyList())\n val hfModels: StateFlow<List<HfModel>> = _hfModels.asStateFlow()\n\n private val _isLoadingHf = MutableStateFlow(false)\n val isLoadingHf: StateFlow<Boolean> = _isLoadingHf.asStateFlow()\n\n private val _hfError = MutableStateFlow<String?>(null)\n val hfError: StateFlow<String?> = _hfError.asStateFlow()\n\n private val _hfTrendingTag = MutableStateFlow(\"text-generation\")\n val hfTrendingTag: StateFlow<String> = _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<String> = _serverchanKey.asStateFlow()\n\n private val _pushplusToken = MutableStateFlow(\"\")\n val pushplusToken: StateFlow<String> = _pushplusToken.asStateFlow()\n\n private val _pushTestResult = MutableStateFlow<String?>(null)\n val pushTestResult: StateFlow<String?> = _pushTestResult.asStateFlow()\n\n private val _isTestingPush = MutableStateFlow(false)\n val isTestingPush: StateFlow<Boolean> = _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<List<BalanceCheckService>>(emptyList())\n val balanceServices: StateFlow<List<BalanceCheckService>> = _balanceServices.asStateFlow()\n\n private val _balanceResults = MutableStateFlow<Map<String, String>>(emptyMap())\n val balanceResults: StateFlow<Map<String, String>> = _balanceResults.asStateFlow()\n\n private val _isLoadingBalanceCheck = MutableStateFlow(false)\n val isLoadingBalanceCheck: StateFlow<Boolean> = _isLoadingBalanceCheck.asStateFlow()\n\n fun loadBalanceServices() {\n val json = securePrefs.getString(Constants.KEY_BALANCE_CHECK_LIST, \"[]\") ?: \"[]\"\n val type = object : TypeToken<List<BalanceCheckService>>() {}.type\n _balanceServices.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n }\n\n private fun saveBalanceServices(list: List<BalanceCheckService>) {\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<List<AgentConfig>>(emptyList())\n val agentList: StateFlow<List<AgentConfig>> = _agentList.asStateFlow()\n\n private val _currentAgentId = MutableStateFlow(\"\")\n val currentAgentId: StateFlow<String> = _currentAgentId.asStateFlow()\n\n private val _isCreatingAgent = MutableStateFlow(false)\n val isCreatingAgent: StateFlow<Boolean> = _isCreatingAgent.asStateFlow()\n\n // ============ Toast ============\n private val _toastMessage = MutableStateFlow<String?>(null)\n val toastMessage: StateFlow<String?> = _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<String, ChannelTokenStats>()\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<List<AgentConfig>>() {}.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<AgentConfig>) {\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<String> = _cozePat.asStateFlow()\n\n private val _cozeBotId = MutableStateFlow(\"\")\n val cozeBotId: StateFlow<String> = _cozeBotId.asStateFlow()\n\n private val _isUsingDefaultPat = MutableStateFlow(true)\n val isUsingDefaultPat: StateFlow<Boolean> = _isUsingDefaultPat.asStateFlow()\n\n private val _isUsingDefaultBotId = MutableStateFlow(true)\n val isUsingDefaultBotId: StateFlow<Boolean> = _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<Map<String, ChannelTokenStats>>(emptyMap())\n val channelTokenStats: StateFlow<Map<String, ChannelTokenStats>> = _channelTokenStats.asStateFlow()\n\n private val _totalTokens = MutableStateFlow(0)\n val totalTokens: StateFlow<Int> = _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<String> = _currentChannel.asStateFlow()\n\n private val _deepseekApiKey = MutableStateFlow(\"\")\n val deepseekApiKey: StateFlow<String> = _deepseekApiKey.asStateFlow()\n\n // ============ DeepSeek 模型选择 ============\n private val _deepseekModel = MutableStateFlow(\"deepseek-v4-flash\")\n val deepseekModel: StateFlow<String> = _deepseekModel.asStateFlow()\n\n // ============ DeepSeek 余额 ============\n private val _deepseekBalance = MutableStateFlow<String?>(null)\n val deepseekBalance: StateFlow<String?> = _deepseekBalance.asStateFlow()\n private val _isLoadingBalance = MutableStateFlow(false)\n val isLoadingBalance: StateFlow<Boolean> = _isLoadingBalance.asStateFlow()\n\n // ============ 自定义 API 通道 ============\n private val _customApiList = MutableStateFlow<List<CustomApiConfig>>(emptyList())\n val customApiList: StateFlow<List<CustomApiConfig>> = _customApiList.asStateFlow()\n\n private fun loadCustomApiList() {\n val json = securePrefs.getString(Constants.KEY_CUSTOM_API_LIST, \"[]\") ?: \"[]\"\n val type = object : TypeToken<List<CustomApiConfig>>() {}.type\n _customApiList.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n }\n\n private fun saveCustomApiList(list: List<CustomApiConfig>) {\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<String> = _githubToken.asStateFlow()\n\n private val _githubUser = MutableStateFlow<GitHubUser?>(null)\n val githubUser: StateFlow<GitHubUser?> = _githubUser.asStateFlow()\n\n private val _githubRepos = MutableStateFlow<List<GitHubRepo>>(emptyList())\n val githubRepos: StateFlow<List<GitHubRepo>> = _githubRepos.asStateFlow()\n\n private val _githubNotifications = MutableStateFlow<List<GitHubNotification>>(emptyList())\n val githubNotifications: StateFlow<List<GitHubNotification>> = _githubNotifications.asStateFlow()\n\n private val _selectedRepoCommits = MutableStateFlow<List<GitHubCommit>>(emptyList())\n val selectedRepoCommits: StateFlow<List<GitHubCommit>> = _selectedRepoCommits.asStateFlow()\n\n private val _isLoadingGitHub = MutableStateFlow(false)\n val isLoadingGitHub: StateFlow<Boolean> = _isLoadingGitHub.asStateFlow()\n\n private val _githubError = MutableStateFlow<String?>(null)\n val githubError: StateFlow<String?> = _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<String> = _hfToken.asStateFlow()\n\n private val _hfModels = MutableStateFlow<List<HfModel>>(emptyList())\n val hfModels: StateFlow<List<HfModel>> = _hfModels.asStateFlow()\n\n private val _isLoadingHf = MutableStateFlow(false)\n val isLoadingHf: StateFlow<Boolean> = _isLoadingHf.asStateFlow()\n\n private val _hfError = MutableStateFlow<String?>(null)\n val hfError: StateFlow<String?> = _hfError.asStateFlow()\n\n private val _hfTrendingTag = MutableStateFlow(\"text-generation\")\n val hfTrendingTag: StateFlow<String> = _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<String> = _serverchanKey.asStateFlow()\n\n private val _pushplusToken = MutableStateFlow(\"\")\n val pushplusToken: StateFlow<String> = _pushplusToken.asStateFlow()\n\n private val _pushTestResult = MutableStateFlow<String?>(null)\n val pushTestResult: StateFlow<String?> = _pushTestResult.asStateFlow()\n\n private val _isTestingPush = MutableStateFlow(false)\n val isTestingPush: StateFlow<Boolean> = _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<List<BalanceCheckService>>(emptyList())\n val balanceServices: StateFlow<List<BalanceCheckService>> = _balanceServices.asStateFlow()\n\n private val _balanceResults = MutableStateFlow<Map<String, String>>(emptyMap())\n val balanceResults: StateFlow<Map<String, String>> = _balanceResults.asStateFlow()\n\n private val _isLoadingBalanceCheck = MutableStateFlow(false)\n val isLoadingBalanceCheck: StateFlow<Boolean> = _isLoadingBalanceCheck.asStateFlow()\n\n fun loadBalanceServices() {\n val json = securePrefs.getString(Constants.KEY_BALANCE_CHECK_LIST, \"[]\") ?: \"[]\"\n val type = object : TypeToken<List<BalanceCheckService>>() {}.type\n _balanceServices.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n }\n\n private fun saveBalanceServices(list: List<BalanceCheckService>) {\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<List<AgentConfig>>(emptyList())\n val agentList: StateFlow<List<AgentConfig>> = _agentList.asStateFlow()\n\n private val _currentAgentId = MutableStateFlow(\"\")\n val currentAgentId: StateFlow<String> = _currentAgentId.asStateFlow()\n\n private val _isCreatingAgent = MutableStateFlow(false)\n val isCreatingAgent: StateFlow<Boolean> = _isCreatingAgent.asStateFlow()\n\n // ============ Toast ============\n private val _toastMessage = MutableStateFlow<String?>(null)\n val toastMessage: StateFlow<String?> = _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<String, ChannelTokenStats>()\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<List<AgentConfig>>() {}.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<AgentConfig>) {\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<String> = _cozePat.asStateFlow()\n\n private val _cozeBotId = MutableStateFlow(\"\")\n val cozeBotId: StateFlow<String> = _cozeBotId.asStateFlow()\n\n private val _isUsingDefaultPat = MutableStateFlow(true)\n val isUsingDefaultPat: StateFlow<Boolean> = _isUsingDefaultPat.asStateFlow()\n\n private val _isUsingDefaultBotId = MutableStateFlow(true)\n val isUsingDefaultBotId: StateFlow<Boolean> = _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<Map<String, ChannelTokenStats>>(emptyMap())\n val channelTokenStats: StateFlow<Map<String, ChannelTokenStats>> = _channelTokenStats.asStateFlow()\n\n private val _totalTokens = MutableStateFlow(0)\n val totalTokens: StateFlow<Int> = _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<String> = _currentChannel.asStateFlow()\n\n private val _deepseekApiKey = MutableStateFlow(\"\")\n val deepseekApiKey: StateFlow<String> = _deepseekApiKey.asStateFlow()\n\n // ============ DeepSeek 模型选择 ============\n private val _deepseekModel = MutableStateFlow(\"deepseek-v4-flash\")\n val deepseekModel: StateFlow<String> = _deepseekModel.asStateFlow()\n\n // ============ DeepSeek 余额 ============\n private val _deepseekBalance = MutableStateFlow<String?>(null)\n val deepseekBalance: StateFlow<String?> = _deepseekBalance.asStateFlow()\n private val _isLoadingBalance = MutableStateFlow(false)\n val isLoadingBalance: StateFlow<Boolean> = _isLoadingBalance.asStateFlow()\n\n // ============ 自定义 API 通道 ============\n private val _customApiList = MutableStateFlow<List<CustomApiConfig>>(emptyList())\n val customApiList: StateFlow<List<CustomApiConfig>> = _customApiList.asStateFlow()\n\n private fun loadCustomApiList() {\n val json = securePrefs.getString(Constants.KEY_CUSTOM_API_LIST, \"[]\") ?: \"[]\"\n val type = object : TypeToken<List<CustomApiConfig>>() {}.type\n _customApiList.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n }\n\n private fun saveCustomApiList(list: List<CustomApiConfig>) {\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<String> = _githubToken.asStateFlow()\n\n private val _githubUser = MutableStateFlow<GitHubUser?>(null)\n val githubUser: StateFlow<GitHubUser?> = _githubUser.asStateFlow()\n\n private val _githubRepos = MutableStateFlow<List<GitHubRepo>>(emptyList())\n val githubRepos: StateFlow<List<GitHubRepo>> = _githubRepos.asStateFlow()\n\n private val _githubNotifications = MutableStateFlow<List<GitHubNotification>>(emptyList())\n val githubNotifications: StateFlow<List<GitHubNotification>> = _githubNotifications.asStateFlow()\n\n private val _selectedRepoCommits = MutableStateFlow<List<GitHubCommit>>(emptyList())\n val selectedRepoCommits: StateFlow<List<GitHubCommit>> = _selectedRepoCommits.asStateFlow()\n\n private val _isLoadingGitHub = MutableStateFlow(false)\n val isLoadingGitHub: StateFlow<Boolean> = _isLoadingGitHub.asStateFlow()\n\n private val _githubError = MutableStateFlow<String?>(null)\n val githubError: StateFlow<String?> = _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<String> = _hfToken.asStateFlow()\n\n private val _hfModels = MutableStateFlow<List<HfModel>>(emptyList())\n val hfModels: StateFlow<List<HfModel>> = _hfModels.asStateFlow()\n\n private val _isLoadingHf = MutableStateFlow(false)\n val isLoadingHf: StateFlow<Boolean> = _isLoadingHf.asStateFlow()\n\n private val _hfError = MutableStateFlow<String?>(null)\n val hfError: StateFlow<String?> = _hfError.asStateFlow()\n\n private val _hfTrendingTag = MutableStateFlow(\"text-generation\")\n val hfTrendingTag: StateFlow<String> = _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<String> = _serverchanKey.asStateFlow()\n\n private val _pushplusToken = MutableStateFlow(\"\")\n val pushplusToken: StateFlow<String> = _pushplusToken.asStateFlow()\n\n private val _pushTestResult = MutableStateFlow<String?>(null)\n val pushTestResult: StateFlow<String?> = _pushTestResult.asStateFlow()\n\n private val _isTestingPush = MutableStateFlow(false)\n val isTestingPush: StateFlow<Boolean> = _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<List<BalanceCheckService>>(emptyList())\n val balanceServices: StateFlow<List<BalanceCheckService>> = _balanceServices.asStateFlow()\n\n private val _balanceResults = MutableStateFlow<Map<String, String>>(emptyMap())\n val balanceResults: StateFlow<Map<String, String>> = _balanceResults.asStateFlow()\n\n private val _isLoadingBalanceCheck = MutableStateFlow(false)\n val isLoadingBalanceCheck: StateFlow<Boolean> = _isLoadingBalanceCheck.asStateFlow()\n\n fun loadBalanceServices() {\n val json = securePrefs.getString(Constants.KEY_BALANCE_CHECK_LIST, \"[]\") ?: \"[]\"\n val type = object : TypeToken<List<BalanceCheckService>>() {}.type\n _balanceServices.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n }\n\n private fun saveBalanceServices(list: List<BalanceCheckService>) {\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<List<AgentConfig>>(emptyList())\n val agentList: StateFlow<List<AgentConfig>> = _agentList.asStateFlow()\n\n private val _currentAgentId = MutableStateFlow(\"\")\n val currentAgentId: StateFlow<String> = _currentAgentId.asStateFlow()\n\n private val _isCreatingAgent = MutableStateFlow(false)\n val isCreatingAgent: StateFlow<Boolean> = _isCreatingAgent.asStateFlow()\n\n // ============ Toast ============\n private val _toastMessage = MutableStateFlow<String?>(null)\n val toastMessage: StateFlow<String?> = _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<String, ChannelTokenStats>()\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<List<AgentConfig>>() {}.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<AgentConfig>) {\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<String> = _cozePat.asStateFlow()\n\n private val _cozeBotId = MutableStateFlow(\"\")\n val cozeBotId: StateFlow<String> = _cozeBotId.asStateFlow()\n\n private val _isUsingDefaultPat = MutableStateFlow(true)\n val isUsingDefaultPat: StateFlow<Boolean> = _isUsingDefaultPat.asStateFlow()\n\n private val _isUsingDefaultBotId = MutableStateFlow(true)\n val isUsingDefaultBotId: StateFlow<Boolean> = _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<Map<String, ChannelTokenStats>>(emptyMap())\n val channelTokenStats: StateFlow<Map<String, ChannelTokenStats>> = _channelTokenStats.asStateFlow()\n\n private val _totalTokens = MutableStateFlow(0)\n val totalTokens: StateFlow<Int> = _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<String> = _currentChannel.asStateFlow()\n\n private val _deepseekApiKey = MutableStateFlow(\"\")\n val deepseekApiKey: StateFlow<String> = _deepseekApiKey.asStateFlow()\n\n // ============ DeepSeek 模型选择 ============\n private val _deepseekModel = MutableStateFlow(\"deepseek-v4-flash\")\n val deepseekModel: StateFlow<String> = _deepseekModel.asStateFlow()\n\n // ============ DeepSeek 余额 ============\n private val _deepseekBalance = MutableStateFlow<String?>(null)\n val deepseekBalance: StateFlow<String?> = _deepseekBalance.asStateFlow()\n private val _isLoadingBalance = MutableStateFlow(false)\n val isLoadingBalance: StateFlow<Boolean> = _isLoadingBalance.asStateFlow()\n\n // ============ 自定义 API 通道 ============\n private val _customApiList = MutableStateFlow<List<CustomApiConfig>>(emptyList())\n val customApiList: StateFlow<List<CustomApiConfig>> = _customApiList.asStateFlow()\n\n private fun loadCustomApiList() {\n val json = securePrefs.getString(Constants.KEY_CUSTOM_API_LIST, \"[]\") ?: \"[]\"\n val type = object : TypeToken<List<CustomApiConfig>>() {}.type\n _customApiList.value = try { gson.fromJson(json, type) } catch (_: Exception) { emptyList() }\n }\n\n private fun saveCustomApiList(list: List<CustomApiConfig>) {\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<List<AgentConfig>>(emptyList())\n val agentList: StateFlow<List<AgentConfig>> = _agentList.asStateFlow()\n\n private val _currentAgentId = MutableStateFlow(\"\")\n val currentAgentId: StateFlow<String> = _currentAgentId.asStateFlow()\n\n private val _isCreatingAgent = MutableStateFlow(false)\n val isCreatingAgent: StateFlow<Boolean> = _isCreatingAgent.asStateFlow()\n\n // ============ Toast ============\n private val _toastMessage = MutableStateFlow<String?>(null)\n val toastMessage: StateFlow<String?> = _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<String, ChannelTokenStats>()\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<List<AgentConfig>>() {}.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<AgentConfig>) {\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<String> = _cozePat.asStateFlow()\n\n private val _cozeBotId = MutableStateFlow(\"\")\n val cozeBotId: StateFlow<String> = _cozeBotId.asStateFlow()\n\n private val _isUsingDefaultPat = MutableStateFlow(true)\n val isUsingDefaultPat: StateFlow<Boolean> = _isUsingDefaultPat.asStateFlow()\n\n private val _isUsingDefaultBotId = MutableStateFlow(true)\n val isUsingDefaultBotId: StateFlow<Boolean> = _isUsingDefaultBotId.asStateFlow()\n\n // ============ Token 消耗统计(分通道) ============\n // Coze 通道\n private val _cozeTokens = MutableStateFlow(0)\n val cozeTokens: StateFlow<Int> = _cozeTokens.asStateFlow()\n private val _cozeInputTokens = MutableStateFlow(0)\n val cozeInputTokens: StateFlow<Int> = _cozeInputTokens.asStateFlow()\n private val _cozeOutputTokens = MutableStateFlow(0)\n val cozeOutputTokens: StateFlow<Int> = _cozeOutputTokens.asStateFlow()\n private val _cozeMessages = MutableStateFlow(0)\n val cozeMessages: StateFlow<Int> = _cozeMessages.asStateFlow()\n\n // DeepSeek 通道\n private val _dsTokens = MutableStateFlow(0)\n val dsTokens: StateFlow<Int> = _dsTokens.asStateFlow()\n private val _dsInputTokens = MutableStateFlow(0)\n val dsInputTokens: StateFlow<Int> = _dsInputTokens.asStateFlow()\n private val _dsOutputTokens = MutableStateFlow(0)\n val dsOutputTokens: StateFlow<Int> = _dsOutputTokens.asStateFlow()\n private val _dsMessages = MutableStateFlow(0)\n val dsMessages: StateFlow<Int> = _dsMessages.asStateFlow()\n\n // 合计\n private val _totalTokens = MutableStateFlow(0)\n val totalTokens: StateFlow<Int> = _totalTokens.asStateFlow()\n\n // ============ AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow<String> = _currentChannel.asStateFlow()\n\n private val _deepseekApiKey = MutableStateFlow(\"\")\n val deepseekApiKey: StateFlow<String> = _deepseekApiKey.asStateFlow()\n\n // ============ DeepSeek 模型选择 ============\n private val _deepseekModel = MutableStateFlow(\"deepseek-v4-flash\")\n val deepseekModel: StateFlow<String> = _deepseekModel.asStateFlow()\n\n // ============ DeepSeek 余额 ============\n private val _deepseekBalance = MutableStateFlow<String?>(null)\n val deepseekBalance: StateFlow<String?> = _deepseekBalance.asStateFlow()\n private val _isLoadingBalance = MutableStateFlow(false)\n val isLoadingBalance: StateFlow<Boolean> = _isLoadingBalance.asStateFlow()\n\n // ============ Agent 管理 ============\n private val _agentList = MutableStateFlow<List<AgentConfig>>(emptyList())\n val agentList: StateFlow<List<AgentConfig>> = _agentList.asStateFlow()\n\n private val _currentAgentId = MutableStateFlow(\"\")\n val currentAgentId: StateFlow<String> = _currentAgentId.asStateFlow()\n\n private val _isCreatingAgent = MutableStateFlow(false)\n val isCreatingAgent: StateFlow<Boolean> = _isCreatingAgent.asStateFlow()\n\n // ============ Toast ============\n private val _toastMessage = MutableStateFlow<String?>(null)\n val toastMessage: StateFlow<String?> = _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<List<AgentConfig>>() {}.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<AgentConfig>) {\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<String> = _cozePat.asStateFlow()\n\n private val _cozeBotId = MutableStateFlow(\"\")\n val cozeBotId: StateFlow<String> = _cozeBotId.asStateFlow()\n\n private val _isUsingDefaultPat = MutableStateFlow(true)\n val isUsingDefaultPat: StateFlow<Boolean> = _isUsingDefaultPat.asStateFlow()\n\n private val _isUsingDefaultBotId = MutableStateFlow(true)\n val isUsingDefaultBotId: StateFlow<Boolean> = _isUsingDefaultBotId.asStateFlow()\n\n // ============ Token 消耗统计(分通道) ============\n // Coze 通道\n private val _cozeTokens = MutableStateFlow(0)\n val cozeTokens: StateFlow<Int> = _cozeTokens.asStateFlow()\n private val _cozeInputTokens = MutableStateFlow(0)\n val cozeInputTokens: StateFlow<Int> = _cozeInputTokens.asStateFlow()\n private val _cozeOutputTokens = MutableStateFlow(0)\n val cozeOutputTokens: StateFlow<Int> = _cozeOutputTokens.asStateFlow()\n private val _cozeMessages = MutableStateFlow(0)\n val cozeMessages: StateFlow<Int> = _cozeMessages.asStateFlow()\n\n // DeepSeek 通道\n private val _dsTokens = MutableStateFlow(0)\n val dsTokens: StateFlow<Int> = _dsTokens.asStateFlow()\n private val _dsInputTokens = MutableStateFlow(0)\n val dsInputTokens: StateFlow<Int> = _dsInputTokens.asStateFlow()\n private val _dsOutputTokens = MutableStateFlow(0)\n val dsOutputTokens: StateFlow<Int> = _dsOutputTokens.asStateFlow()\n private val _dsMessages = MutableStateFlow(0)\n val dsMessages: StateFlow<Int> = _dsMessages.asStateFlow()\n\n // 合计\n private val _totalTokens = MutableStateFlow(0)\n val totalTokens: StateFlow<Int> = _totalTokens.asStateFlow()\n\n // ============ AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow<String> = _currentChannel.asStateFlow()\n\n private val _deepseekApiKey = MutableStateFlow(\"\")\n val deepseekApiKey: StateFlow<String> = _deepseekApiKey.asStateFlow()\n\n // ============ DeepSeek 模型选择 ============\n private val _deepseekModel = MutableStateFlow(\"deepseek-v4-flash\")\n val deepseekModel: StateFlow<String> = _deepseekModel.asStateFlow()\n\n // ============ DeepSeek 余额 ============\n private val _deepseekBalance = MutableStateFlow<String?>(null)\n val deepseekBalance: StateFlow<String?> = _deepseekBalance.asStateFlow()\n private val _isLoadingBalance = MutableStateFlow(false)\n val isLoadingBalance: StateFlow<Boolean> = _isLoadingBalance.asStateFlow()\n\n // ============ Agent 管理 ============\n private val _agentList = MutableStateFlow<List<AgentConfig>>(emptyList())\n val agentList: StateFlow<List<AgentConfig>> = _agentList.asStateFlow()\n\n private val _currentAgentId = MutableStateFlow(\"\")\n val currentAgentId: StateFlow<String> = _currentAgentId.asStateFlow()\n\n private val _isCreatingAgent = MutableStateFlow(false)\n val isCreatingAgent: StateFlow<Boolean> = _isCreatingAgent.asStateFlow()\n\n // ============ Toast ============\n private val _toastMessage = MutableStateFlow<String?>(null)\n val toastMessage: StateFlow<String?> = _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<List<AgentConfig>>() {}.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<AgentConfig>) {\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<String> = _cozePat.asStateFlow()\n\n private val _cozeBotId = MutableStateFlow(\"\")\n val cozeBotId: StateFlow<String> = _cozeBotId.asStateFlow()\n\n private val _isUsingDefaultPat = MutableStateFlow(true)\n val isUsingDefaultPat: StateFlow<Boolean> = _isUsingDefaultPat.asStateFlow()\n\n private val _isUsingDefaultBotId = MutableStateFlow(true)\n val isUsingDefaultBotId: StateFlow<Boolean> = _isUsingDefaultBotId.asStateFlow()\n\n // ============ Token 消耗统计 ============\n private val _totalTokens = MutableStateFlow(0)\n val totalTokens: StateFlow<Int> = _totalTokens.asStateFlow()\n\n private val _totalInputTokens = MutableStateFlow(0)\n val totalInputTokens: StateFlow<Int> = _totalInputTokens.asStateFlow()\n\n private val _totalOutputTokens = MutableStateFlow(0)\n val totalOutputTokens: StateFlow<Int> = _totalOutputTokens.asStateFlow()\n\n private val _totalMessages = MutableStateFlow(0)\n val totalMessages: StateFlow<Int> = _totalMessages.asStateFlow()\n\n // ============ AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow<String> = _currentChannel.asStateFlow()\n\n private val _deepseekApiKey = MutableStateFlow(\"\")\n val deepseekApiKey: StateFlow<String> = _deepseekApiKey.asStateFlow()\n\n // ============ Agent 管理 ============\n private val _agentList = MutableStateFlow<List<AgentConfig>>(emptyList())\n val agentList: StateFlow<List<AgentConfig>> = _agentList.asStateFlow()\n\n private val _currentAgentId = MutableStateFlow(\"\")\n val currentAgentId: StateFlow<String> = _currentAgentId.asStateFlow()\n\n private val _isCreatingAgent = MutableStateFlow(false)\n val isCreatingAgent: StateFlow<Boolean> = _isCreatingAgent.asStateFlow()\n\n // ============ Toast ============\n private val _toastMessage = MutableStateFlow<String?>(null)\n val toastMessage: StateFlow<String?> = _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<List<AgentConfig>>() {}.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<AgentConfig>) {\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<String> = _cozePat.asStateFlow()\n\n private val _cozeBotId = MutableStateFlow(\"\")\n val cozeBotId: StateFlow<String> = _cozeBotId.asStateFlow()\n\n private val _isUsingDefaultPat = MutableStateFlow(true)\n val isUsingDefaultPat: StateFlow<Boolean> = _isUsingDefaultPat.asStateFlow()\n\n private val _isUsingDefaultBotId = MutableStateFlow(true)\n val isUsingDefaultBotId: StateFlow<Boolean> = _isUsingDefaultBotId.asStateFlow()()\n\n // ============ Token 消耗统计 ============\n private val _totalTokens = MutableStateFlow(0)\n val totalTokens: StateFlow<Int> = _totalTokens.asStateFlow()\n\n private val _totalInputTokens = MutableStateFlow(0)\n val totalInputTokens: StateFlow<Int> = _totalInputTokens.asStateFlow()\n\n private val _totalOutputTokens = MutableStateFlow(0)\n val totalOutputTokens: StateFlow<Int> = _totalOutputTokens.asStateFlow()\n\n private val _totalMessages = MutableStateFlow(0)\n val totalMessages: StateFlow<Int> = _totalMessages.asStateFlow()\n\n // ============ Toast ============\n private val _toastMessage = MutableStateFlow<String?>(null)\n val toastMessage: StateFlow<String?> = _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<List<ChatMessage>>(\n listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n )\n val messages: StateFlow<List<ChatMessage>> = _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<String> = _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<List<SessionSummary>>(emptyList())\n val sessionList: StateFlow<List<SessionSummary>> = _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<String> = _inputText.asStateFlow()\n\n fun setInputText(text: String) {\n _inputText.value = text\n }\n\n // ============ 附件(图片) ============\n private val _pendingImageUri = MutableStateFlow<String?>(null)\n val pendingImageUri: StateFlow<String?> = _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<Boolean> = _isLoading.asStateFlow()\n\n // ============ 引用回复 ============\n private val _replyToMessage = MutableStateFlow<ChatMessage?>(null)\n val replyToMessage: StateFlow<ChatMessage?> = _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<String?>(null)\n val statusMessage: StateFlow<String?> = _statusMessage.asStateFlow()\n\n // ============ 当前流式任务的 Job ============\n private var streamJob: Job? = null\n\n // ============ Coze 配置状态 ============\n private val _isCozeConfigured = MutableStateFlow(true)\n val isCozeConfigured: StateFlow<Boolean> = _isCozeConfigured.asStateFlow()\n\n // ============ 当前 AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow<String> = _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<String> {\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<List<com.meitu.generator.ui.settings.CustomApiConfig>>() {}.type\n val customList: List<com.meitu.generator.ui.settings.CustomApiConfig> = 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<List<com.meitu.generator.ui.settings.CustomApiConfig>>() {}.type\n val list: List<com.meitu.generator.ui.settings.CustomApiConfig> = 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<List<com.meitu.generator.ui.settings.CustomApiConfig>>() {}.type\n val list: List<com.meitu.generator.ui.settings.CustomApiConfig> = 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<Pair<String, String>> {\n val history = mutableListOf<Pair<String, String>>()\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<Application>()\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<List<ChatMessage>>(\n listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n )\n val messages: StateFlow<List<ChatMessage>> = _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<String> = _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<List<SessionSummary>>(emptyList())\n val sessionList: StateFlow<List<SessionSummary>> = _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<String> = _inputText.asStateFlow()\n\n fun setInputText(text: String) {\n _inputText.value = text\n }\n\n // ============ 附件(图片) ============\n private val _pendingImageUri = MutableStateFlow<String?>(null)\n val pendingImageUri: StateFlow<String?> = _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<Boolean> = _isLoading.asStateFlow()\n\n // ============ 引用回复 ============\n private val _replyToMessage = MutableStateFlow<ChatMessage?>(null)\n val replyToMessage: StateFlow<ChatMessage?> = _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<String?>(null)\n val statusMessage: StateFlow<String?> = _statusMessage.asStateFlow()\n\n // ============ 当前流式任务的 Job ============\n private var streamJob: Job? = null\n\n // ============ Coze 配置状态 ============\n private val _isCozeConfigured = MutableStateFlow(true)\n val isCozeConfigured: StateFlow<Boolean> = _isCozeConfigured.asStateFlow()\n\n // ============ 当前 AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow<String> = _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<String> {\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<List<com.meitu.generator.ui.settings.CustomApiConfig>>() {}.type\n val customList: List<com.meitu.generator.ui.settings.CustomApiConfig> = 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<List<com.meitu.generator.ui.settings.CustomApiConfig>>() {}.type\n val list: List<com.meitu.generator.ui.settings.CustomApiConfig> = 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<List<com.meitu.generator.ui.settings.CustomApiConfig>>() {}.type\n val list: List<com.meitu.generator.ui.settings.CustomApiConfig> = 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<Pair<String, String>> {\n val history = mutableListOf<Pair<String, String>>()\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<Application>()\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<List<ChatMessage>>(\n listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n )\n val messages: StateFlow<List<ChatMessage>> = _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<String> = _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<List<SessionSummary>>(emptyList())\n val sessionList: StateFlow<List<SessionSummary>> = _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<String> = _inputText.asStateFlow()\n\n fun setInputText(text: String) {\n _inputText.value = text\n }\n\n // ============ 附件(图片) ============\n private val _pendingImageUri = MutableStateFlow<String?>(null)\n val pendingImageUri: StateFlow<String?> = _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<Boolean> = _isLoading.asStateFlow()\n\n // ============ 引用回复 ============\n private val _replyToMessage = MutableStateFlow<ChatMessage?>(null)\n val replyToMessage: StateFlow<ChatMessage?> = _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<String?>(null)\n val statusMessage: StateFlow<String?> = _statusMessage.asStateFlow()\n\n // ============ 当前流式任务的 Job ============\n private var streamJob: Job? = null\n\n // ============ Coze 配置状态 ============\n private val _isCozeConfigured = MutableStateFlow(true)\n val isCozeConfigured: StateFlow<Boolean> = _isCozeConfigured.asStateFlow()\n\n // ============ 当前 AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow<String> = _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<String> {\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<List<com.meitu.generator.ui.settings.CustomApiConfig>>() {}.type\n val customList: List<com.meitu.generator.ui.settings.CustomApiConfig> = 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<List<com.meitu.generator.ui.settings.CustomApiConfig>>() {}.type\n val list: List<com.meitu.generator.ui.settings.CustomApiConfig> = 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<List<com.meitu.generator.ui.settings.CustomApiConfig>>() {}.type\n val list: List<com.meitu.generator.ui.settings.CustomApiConfig> = 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<Pair<String, String>> {\n val history = mutableListOf<Pair<String, String>>()\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<Application>()\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<List<ChatMessage>>(\n listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n )\n val messages: StateFlow<List<ChatMessage>> = _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<String> = _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<List<SessionSummary>>(emptyList())\n val sessionList: StateFlow<List<SessionSummary>> = _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<String> = _inputText.asStateFlow()\n\n fun setInputText(text: String) {\n _inputText.value = text\n }\n\n // ============ 附件(图片) ============\n private val _pendingImageUri = MutableStateFlow<String?>(null)\n val pendingImageUri: StateFlow<String?> = _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<Boolean> = _isLoading.asStateFlow()\n\n // ============ 状态 ============\n private val _statusMessage = MutableStateFlow<String?>(null)\n val statusMessage: StateFlow<String?> = _statusMessage.asStateFlow()\n\n // ============ 当前流式任务的 Job ============\n private var streamJob: Job? = null\n\n // ============ Coze 配置状态 ============\n private val _isCozeConfigured = MutableStateFlow(true)\n val isCozeConfigured: StateFlow<Boolean> = _isCozeConfigured.asStateFlow()\n\n // ============ 当前 AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow<String> = _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<String> {\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<List<com.meitu.generator.ui.settings.CustomApiConfig>>() {}.type\n val customList: List<com.meitu.generator.ui.settings.CustomApiConfig> = 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<List<com.meitu.generator.ui.settings.CustomApiConfig>>() {}.type\n val list: List<com.meitu.generator.ui.settings.CustomApiConfig> = 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<List<com.meitu.generator.ui.settings.CustomApiConfig>>() {}.type\n val list: List<com.meitu.generator.ui.settings.CustomApiConfig> = 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<Pair<String, String>> {\n val history = mutableListOf<Pair<String, String>>()\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<Application>()\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<List<ChatMessage>>(\n listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n )\n val messages: StateFlow<List<ChatMessage>> = _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<String> = _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<List<SessionSummary>>(emptyList())\n val sessionList: StateFlow<List<SessionSummary>> = _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<String> = _inputText.asStateFlow()\n\n fun setInputText(text: String) {\n _inputText.value = text\n }\n\n // ============ 附件(图片) ============\n private val _pendingImageUri = MutableStateFlow<String?>(null)\n val pendingImageUri: StateFlow<String?> = _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<Boolean> = _isLoading.asStateFlow()\n\n // ============ 状态 ============\n private val _statusMessage = MutableStateFlow<String?>(null)\n val statusMessage: StateFlow<String?> = _statusMessage.asStateFlow()\n\n // ============ 当前流式任务的 Job ============\n private var streamJob: Job? = null\n\n // ============ Coze 配置状态 ============\n private val _isCozeConfigured = MutableStateFlow(true)\n val isCozeConfigured: StateFlow<Boolean> = _isCozeConfigured.asStateFlow()\n\n // ============ 当前 AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow<String> = _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<Pair<String, String>> {\n val history = mutableListOf<Pair<String, String>>()\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<Application>()\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<List<ChatMessage>>(\n listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n )\n val messages: StateFlow<List<ChatMessage>> = _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<String> = _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<List<SessionSummary>>(emptyList())\n val sessionList: StateFlow<List<SessionSummary>> = _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<String> = _inputText.asStateFlow()\n\n fun setInputText(text: String) {\n _inputText.value = text\n }\n\n // ============ 附件(图片) ============\n private val _pendingImageUri = MutableStateFlow<String?>(null)\n val pendingImageUri: StateFlow<String?> = _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<Boolean> = _isLoading.asStateFlow()\n\n // ============ 状态 ============\n private val _statusMessage = MutableStateFlow<String?>(null)\n val statusMessage: StateFlow<String?> = _statusMessage.asStateFlow()\n\n // ============ 当前流式任务的 Job ============\n private var streamJob: Job? = null\n\n // ============ Coze 配置状态 ============\n private val _isCozeConfigured = MutableStateFlow(false)\n val isCozeConfigured: StateFlow<Boolean> = _isCozeConfigured.asStateFlow()\n\n // ============ 当前 AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow<String> = _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<Pair<String, String>> {\n val history = mutableListOf<Pair<String, String>>()\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<Application>()\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<List<ChatMessage>>(\n listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n )\n val messages: StateFlow<List<ChatMessage>> = _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<String> = _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<List<SessionSummary>>(emptyList())\n val sessionList: StateFlow<List<SessionSummary>> = _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<String> = _inputText.asStateFlow()\n\n fun setInputText(text: String) {\n _inputText.value = text\n }\n\n // ============ 附件(图片) ============\n private val _pendingImageUri = MutableStateFlow<String?>(null)\n val pendingImageUri: StateFlow<String?> = _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<Boolean> = _isLoading.asStateFlow()\n\n // ============ 状态 ============\n private val _statusMessage = MutableStateFlow<String?>(null)\n val statusMessage: StateFlow<String?> = _statusMessage.asStateFlow()\n\n // ============ 当前流式任务的 Job ============\n private var streamJob: Job? = null\n\n // ============ Coze 配置状态 ============\n private val _isCozeConfigured = MutableStateFlow(false)\n val isCozeConfigured: StateFlow<Boolean> = _isCozeConfigured.asStateFlow()\n\n // ============ 当前 AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow<String> = _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<Pair<String, String>> {\n val history = mutableListOf<Pair<String, String>>()\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<Application>()\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<List<ChatMessage>>(\n listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n )\n val messages: StateFlow<List<ChatMessage>> = _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<String> = _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<List<SessionSummary>>(emptyList())\n val sessionList: StateFlow<List<SessionSummary>> = _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<String> = _inputText.asStateFlow()\n\n fun setInputText(text: String) {\n _inputText.value = text\n }\n\n // ============ 附件(图片) ============\n private val _pendingImageUri = MutableStateFlow<String?>(null)\n val pendingImageUri: StateFlow<String?> = _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<Boolean> = _isLoading.asStateFlow()\n\n // ============ 状态 ============\n private val _statusMessage = MutableStateFlow<String?>(null)\n val statusMessage: StateFlow<String?> = _statusMessage.asStateFlow()\n\n // ============ 当前流式任务的 Job ============\n private var streamJob: Job? = null\n\n // ============ Coze 配置状态 ============\n private val _isCozeConfigured = MutableStateFlow(false)\n val isCozeConfigured: StateFlow<Boolean> = _isCozeConfigured.asStateFlow()\n\n // ============ 当前 AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow<String> = _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<Pair<String, String>> {\n val history = mutableListOf<Pair<String, String>>()\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<Application>()\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<List<ChatMessage>>(\n listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n )\n val messages: StateFlow<List<ChatMessage>> = _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<String> = _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<List<SessionSummary>>(emptyList())\n val sessionList: StateFlow<List<SessionSummary>> = _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<String> = _inputText.asStateFlow()\n\n fun setInputText(text: String) {\n _inputText.value = text\n }\n\n // ============ 附件(图片) ============\n private val _pendingImageUri = MutableStateFlow<String?>(null)\n val pendingImageUri: StateFlow<String?> = _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<Boolean> = _isLoading.asStateFlow()\n\n // ============ 状态 ============\n private val _statusMessage = MutableStateFlow<String?>(null)\n val statusMessage: StateFlow<String?> = _statusMessage.asStateFlow()\n\n // ============ 当前流式任务的 Job ============\n private var streamJob: Job? = null\n\n // ============ Coze 配置状态 ============\n private val _isCozeConfigured = MutableStateFlow(false)\n val isCozeConfigured: StateFlow<Boolean> = _isCozeConfigured.asStateFlow()\n\n // ============ 当前 AI 通道 ============\n private val _currentChannel = MutableStateFlow(Constants.CHANNEL_COZE)\n val currentChannel: StateFlow<String> = _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<Pair<String, String>> {\n val history = mutableListOf<Pair<String, String>>()\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<Application>()\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<List<ChatMessage>>(\n listOf(ChatMessage(\n text = \"你好!我是布老师,你的专属 AI 助手 🧠\\n\\n我可以陪你聊天、回答问题、分析图片。\\n\\n💡 有什么想法直接说,我来帮你\",\n isUser = false\n ))\n )\n val messages: StateFlow<List<ChatMessage>> = _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<String> = _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<List<SessionSummary>>(emptyList())\n val sessionList: StateFlow<List<SessionSummary>> = _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<String> = _inputText.asStateFlow()\n\n fun setInputText(text: String) {\n _inputText.value = text\n }\n\n // ============ 附件(图片) ============\n private val _pendingImageUri = MutableStateFlow<String?>(null)\n val pendingImageUri: StateFlow<String?> = _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<Boolean> = _isLoading.asStateFlow()\n\n // ============ 状态 ============\n private val _statusMessage = MutableStateFlow<String?>(null)\n val statusMessage: StateFlow<String?> = _statusMessage.asStateFlow()\n\n // ============ 当前流式任务的 Job ============\n private var streamJob: Job? = null\n\n // ============ Coze 配置状态 ============\n private val _isCozeConfigured = MutableStateFlow(false)\n val isCozeConfigured: StateFlow<Boolean> = _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<Application>()\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"}}}}