Просмотр исходного кода

Merge pull request #1702 from lalala-12138/feat/ai-test-connection

feat(ai): 支持 AI 供应商连接测试功能
Kudomaga 4 месяцев назад
Родитель
Сommit
36dac7bb7a

+ 2 - 0
app/src/main/java/io/legado/app/ui/config/ai/AiProviderEditContract.kt

@@ -18,6 +18,7 @@ data class AiProviderEditUiState(
     val apiKey: String = "",
     val providerModels: ImmutableList<AiProviderModelUi> = persistentListOf(),
     val editingModel: AiProviderModelEditorUi? = null,
+    val isTesting: Boolean = false,
     val isSaving: Boolean = false,
     val isFetchingModels: Boolean = false,
     val initialized: Boolean = false
@@ -79,6 +80,7 @@ sealed interface AiProviderEditIntent {
     data class UpdateEditingMaxOutputTokens(val value: String) : AiProviderEditIntent
     data class UpdateEditingTemperature(val value: String) : AiProviderEditIntent
     data object SaveEditingModel : AiProviderEditIntent
+    data object TestConnection : AiProviderEditIntent
     data object SaveProvider : AiProviderEditIntent
     data object SyncModels : AiProviderEditIntent
     data object DeleteProvider : AiProviderEditIntent

+ 8 - 0
app/src/main/java/io/legado/app/ui/config/ai/AiProviderEditScreen.kt

@@ -168,6 +168,14 @@ fun AiProviderEditScreen(
                             showApiKeyDialog = true
                         }
                     )
+                    ClickableSettingItem(
+                        title = if (state.isTesting) "${stringResource(R.string.ai_test_connection)}..." else stringResource(R.string.ai_test_connection),
+                        onClick = {
+                            if (!state.isTesting && !state.isSaving && !state.isFetchingModels) {
+                                onIntent(AiProviderEditIntent.TestConnection)
+                            }
+                        }
+                    )
                 }
             }
 

+ 41 - 10
app/src/main/java/io/legado/app/ui/config/ai/AiProviderEditViewModel.kt

@@ -2,6 +2,7 @@ package io.legado.app.ui.config.ai
 
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
+import io.legado.app.R
 import io.legado.app.domain.gateway.AiProfileGateway
 import io.legado.app.domain.gateway.AiTextGateway
 import io.legado.app.domain.model.AiAvailableModel
@@ -13,6 +14,7 @@ import io.legado.app.domain.model.AiProviderPresets
 import io.legado.app.domain.model.TranslationConstants
 import io.legado.app.utils.GSON
 import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asSharedFlow
@@ -20,6 +22,7 @@ import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
+import splitties.init.appCtx
 
 class AiProviderEditViewModel(
     private val initialProviderId: String?,
@@ -114,6 +117,7 @@ class AiProviderEditViewModel(
             is AiProviderEditIntent.UpdateEditingMaxOutputTokens -> updateEditingModel { copy(maxOutputTokens = intent.value) }
             is AiProviderEditIntent.UpdateEditingTemperature -> updateEditingModel { copy(temperature = intent.value) }
             AiProviderEditIntent.SaveEditingModel -> saveEditingModel()
+            AiProviderEditIntent.TestConnection -> testConnection()
             AiProviderEditIntent.SaveProvider -> saveProvider()
             AiProviderEditIntent.SyncModels -> syncModels()
             AiProviderEditIntent.DeleteProvider -> deleteProvider()
@@ -206,6 +210,31 @@ class AiProviderEditViewModel(
         }
     }
 
+    private fun testConnection() {
+        if (_uiState.value.isTesting || _uiState.value.isSaving || _uiState.value.isFetchingModels) return
+        viewModelScope.launch {
+            _uiState.update { it.copy(isTesting = true) }
+            try {
+                val models = aiTextGateway.fetchModels(_uiState.value.toProviderConfig()).getOrThrow()
+                val count = models.size
+                val message = if (count == 0) {
+                    appCtx.getString(R.string.ai_test_success_no_models)
+                } else {
+                    appCtx.getString(R.string.ai_test_success_with_models, count)
+                }
+                _effects.tryEmit(AiProviderEditEffect.ShowMessage(message))
+            } catch (error: Throwable) {
+                if (error is CancellationException) throw error
+                val errorMsg = error.message
+                val failMsg = appCtx.getString(R.string.ai_test_failed)
+                val message = if (errorMsg.isNullOrBlank()) failMsg else "$failMsg: $errorMsg"
+                _effects.tryEmit(AiProviderEditEffect.ShowMessage(message))
+            } finally {
+                _uiState.update { it.copy(isTesting = false) }
+            }
+        }
+    }
+
     private fun syncModels() {
         viewModelScope.launch {
             _uiState.update { it.copy(isSaving = true, isFetchingModels = true) }
@@ -215,16 +244,7 @@ class AiProviderEditViewModel(
                 _uiState.update { it.copy(providerId = provider.id) }
                 val state = _uiState.value
                 runCatching {
-                    aiTextGateway.fetchModels(
-                        AiProviderConfig(
-                            id = provider.id,
-                            name = state.providerName,
-                            protocol = state.protocol,
-                            baseUrl = state.baseUrl,
-                            apiKey = state.apiKey,
-                            modelsUrl = state.modelsUrl.ifBlank { null }
-                        )
-                    ).getOrThrow()
+                    aiTextGateway.fetchModels(state.toProviderConfig(provider.id)).getOrThrow()
                 }.onSuccess { models ->
                     aiProfileGateway.importProviderModels(provider.id, models)
                     applyFetchedModels(provider.id, models)
@@ -303,6 +323,17 @@ class AiProviderEditViewModel(
         )
     }
 
+    private fun AiProviderEditUiState.toProviderConfig(id: String = providerId ?: "test_connection_id"): AiProviderConfig {
+        return AiProviderConfig(
+            id = id,
+            name = providerName,
+            protocol = protocol,
+            baseUrl = baseUrl,
+            apiKey = apiKey,
+            modelsUrl = modelsUrl.ifBlank { null }
+        )
+    }
+
     private fun parseParams(json: String?): AiGenerationParams {
         if (json.isNullOrBlank()) return AiGenerationParams()
         return runCatching {

+ 3 - 0
app/src/main/res/values-zh-rCN/strings.xml

@@ -2070,6 +2070,9 @@
     <string name="ai_profile_status">档案</string>
     <string name="ai_save_profile">保存默认档案</string>
     <string name="ai_test_connection">测试连接</string>
+    <string name="ai_test_success_no_models">AI 连接测试成功,但未找到任何模型</string>
+    <string name="ai_test_success_with_models">AI 连接测试成功(共找到 %1$d 个模型)</string>
+    <string name="ai_test_failed">AI 连接测试失败</string>
     <string name="ai_profile_counts">供应商:%1$d,模型:%2$d,预设:%3$d</string>
     <string name="read_menu_bg_color_summary">阅读顶栏和底栏背景色</string>
     <string name="read_menu_color_source">色板来源</string>

+ 3 - 0
app/src/main/res/values-zh-rHK/strings.xml

@@ -1494,6 +1494,9 @@
     <string name="ai_tasks">任務</string>
     <string name="ai_temperature">Temperature</string>
     <string name="ai_test_connection">測試連線</string>
+    <string name="ai_test_success_no_models">AI 連線測試成功,但未找到任何模型</string>
+    <string name="ai_test_success_with_models">AI 連線測試成功(共找到 %1$d 個模型)</string>
+    <string name="ai_test_failed">AI 連線測試失敗</string>
     <string name="ai_thinking_auto">自動</string>
     <string name="ai_thinking_collapse">收起</string>
     <string name="ai_thinking_deep">深度</string>

+ 3 - 0
app/src/main/res/values-zh-rTW/strings.xml

@@ -2014,6 +2014,9 @@
     <string name="ai_profile_status">設定檔</string>
     <string name="ai_save_profile">儲存預設設定檔</string>
     <string name="ai_test_connection">測試連線</string>
+    <string name="ai_test_success_no_models">AI 連線測試成功,但未找到任何模型</string>
+    <string name="ai_test_success_with_models">AI 連線測試成功(共找到 %1$d 個模型)</string>
+    <string name="ai_test_failed">AI 連線測試失敗</string>
     <string name="ai_profile_counts">供應商:%1$d,模型:%2$d,預設:%3$d</string>
     <string name="homepage_select_from_kinds">從分類中選擇</string>
     <string name="homepage_select_multiple_kinds">選擇多個分類</string>

+ 3 - 0
app/src/main/res/values/strings.xml

@@ -2000,6 +2000,9 @@
     <string name="ai_profile_status">Profile</string>
     <string name="ai_save_profile">Save Default Profile</string>
     <string name="ai_test_connection">Test Connection</string>
+    <string name="ai_test_success_no_models">AI connection test succeeded, but no models found</string>
+    <string name="ai_test_success_with_models">AI connection test succeeded (%1$d models found)</string>
+    <string name="ai_test_failed">AI connection test failed</string>
     <string name="ai_profile_counts">Providers: %1$d, models: %2$d, presets: %3$d</string>
     <string name="homepage_load_more_failed">Failed to load more: %1$s</string>
     <string name="homepage_module_duplicate_infinite">This group already has an infinite loading module</string>