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

feat(ai): 新增章节梗概二级配置菜单及任务参数预设功能,支持用户自定义章节梗概提示词模板、重置提示词,并可配置生成参数(Temperature 与 Max Output Tokens)。

lyc 4 месяцев назад
Родитель
Сommit
6e39df7c80

+ 59 - 2
app/src/main/java/io/legado/app/data/repository/AiProfileRepository.kt

@@ -22,6 +22,7 @@ import io.legado.app.domain.model.TranslationConstants
 import io.legado.app.utils.GSON
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.firstOrNull
 import kotlinx.coroutines.withContext
 import java.util.UUID
 
@@ -159,6 +160,62 @@ class AiProfileRepository(
         aiProfileDao.deleteModel(modelId)
     }
 
+    override suspend fun saveTaskPreset(
+        taskType: String,
+        promptTemplate: String,
+        temperature: Float,
+        maxOutputTokens: Int
+    ): AiTaskPresetConfig = withContext(Dispatchers.IO) {
+        runCatching {
+            val existingPreset = aiProfileDao.getDefaultPreset(taskType)
+            val presetId = existingPreset?.id ?: when (taskType) {
+                AiTaskType.TRANSLATE_CHAPTER -> DEFAULT_TRANSLATE_PRESET_ID
+                AiTaskType.SUMMARIZE_CHAPTER -> DEFAULT_SUMMARY_PRESET_ID
+                AiTaskType.CHAT -> DEFAULT_CHAT_PRESET_ID
+                else -> newId("preset")
+            }
+            val modelsList = aiProfileDao.observeModels().firstOrNull()
+            val modelProfileId = existingPreset?.modelProfileId
+                ?: modelsList?.firstOrNull()?.id
+                ?: ""
+            val currentParams = parseParams(existingPreset?.paramsJson)
+            val updatedParams = currentParams.copy(
+                temperature = temperature,
+                maxOutputTokens = maxOutputTokens
+            )
+            val now = System.currentTimeMillis()
+            val preset = AiTaskPreset(
+                id = presetId,
+                taskType = taskType,
+                name = existingPreset?.name ?: when (taskType) {
+                    AiTaskType.TRANSLATE_CHAPTER -> "Default Translation"
+                    AiTaskType.SUMMARIZE_CHAPTER -> "Default Chapter Summary"
+                    AiTaskType.CHAT -> "Default Chat"
+                    else -> "Default Preset"
+                },
+                modelProfileId = modelProfileId,
+                promptTemplate = promptTemplate.ifBlank {
+                    when (taskType) {
+                        AiTaskType.TRANSLATE_CHAPTER -> TranslationConstants.DEFAULT_PROMPT
+                        AiTaskType.SUMMARIZE_CHAPTER -> AiPromptTemplate.DEFAULT_CHAPTER_SUMMARY
+                        else -> "You are a helpful AI assistant."
+                    }
+                },
+                paramsJson = GSON.toJson(updatedParams),
+                chunkPolicyJson = existingPreset?.chunkPolicyJson,
+                enabled = existingPreset?.enabled ?: true,
+                isDefault = existingPreset?.isDefault ?: true,
+                sortNumber = existingPreset?.sortNumber ?: 0,
+                createdAt = existingPreset?.createdAt ?: now,
+                updatedAt = now
+            )
+            aiProfileDao.insertPreset(preset)
+            preset.toConfig() ?: error("Failed to save task preset")
+        }.getOrElse { error ->
+            throw IllegalStateException("保存 AI 预设配置失败: ${error.localizedMessage ?: "未知错误"}", error)
+        }
+    }
+
     override suspend fun saveDefaultChatProfile(draft: AiProfileDraft): AiTaskPresetConfig = withContext(Dispatchers.IO) {
         require(draft.baseUrl.isNotBlank()) { "Base URL is required" }
         require(draft.modelId.isNotBlank()) { "Model is required" }
@@ -253,8 +310,8 @@ class AiProfileRepository(
                 taskType = AiTaskType.SUMMARIZE_CHAPTER,
                 name = "Default Chapter Summary",
                 modelProfileId = modelProfileId,
-                promptTemplate = AiPromptTemplate.DEFAULT_CHAPTER_SUMMARY,
-                paramsJson = GSON.toJson(params),
+                promptTemplate = existingSummaryPreset?.promptTemplate ?: AiPromptTemplate.DEFAULT_CHAPTER_SUMMARY,
+                paramsJson = existingSummaryPreset?.paramsJson ?: GSON.toJson(params),
                 isDefault = true,
                 createdAt = existingSummaryPreset?.createdAt ?: now,
                 updatedAt = now

+ 2 - 0
app/src/main/java/io/legado/app/di/appModule.kt

@@ -139,6 +139,7 @@ import io.legado.app.ui.book.toc.rule.preview.TxtTocRulePreviewViewModel
 import io.legado.app.ui.config.ai.AiConfigViewModel
 import io.legado.app.ui.config.ai.AiModelEditViewModel
 import io.legado.app.ui.config.ai.AiProviderEditViewModel
+import io.legado.app.ui.config.ai.summary.AiSummaryConfigViewModel
 import io.legado.app.ui.config.backupConfig.BackupConfigViewModel
 import io.legado.app.ui.config.bookshelfConfig.BookshelfManageScreenConfig
 import io.legado.app.ui.config.coverConfig.CoverAlbumManageViewModel
@@ -316,6 +317,7 @@ val appModule = module {
     viewModelOf(::ThemeManageViewModel)
     viewModelOf(::BackupConfigViewModel)
     viewModelOf(::AiConfigViewModel)
+    viewModelOf(::AiSummaryConfigViewModel)
     viewModelOf(::AiChatViewModel)
     viewModel { (providerId: String?) ->
         AiProviderEditViewModel(

+ 6 - 0
app/src/main/java/io/legado/app/domain/gateway/AiProfileGateway.kt

@@ -23,6 +23,12 @@ interface AiProfileGateway {
     suspend fun importProviderModels(providerId: String, models: List<AiAvailableModel>): List<AiModelProfile>
     suspend fun setDefaultModel(modelProfileId: String): AiTaskPresetConfig
     suspend fun saveDefaultChatProfile(draft: AiProfileDraft): AiTaskPresetConfig
+    suspend fun saveTaskPreset(
+        taskType: String,
+        promptTemplate: String,
+        temperature: Float,
+        maxOutputTokens: Int
+    ): AiTaskPresetConfig
     suspend fun deleteProvider(providerId: String)
     suspend fun deleteModel(modelId: String)
 }

+ 6 - 3
app/src/main/java/io/legado/app/ui/config/ai/AiConfigScreen.kt

@@ -35,6 +35,7 @@ fun AiConfigRouteScreen(
     onNavigateToProviderEdit: (providerId: String?) -> Unit,
     onNavigateToModelEdit: (providerId: String?, modelProfileId: String?) -> Unit,
     onNavigateToTranslation: () -> Unit,
+    onNavigateToAiSummary: () -> Unit,
     viewModel: AiConfigViewModel = koinViewModel()
 ) {
     AiConfigScreen(
@@ -44,7 +45,8 @@ fun AiConfigRouteScreen(
         onBackClick = onBackClick,
         onNavigateToProviderEdit = onNavigateToProviderEdit,
         onNavigateToModelEdit = onNavigateToModelEdit,
-        onNavigateToTranslation = onNavigateToTranslation
+        onNavigateToTranslation = onNavigateToTranslation,
+        onNavigateToAiSummary = onNavigateToAiSummary
     )
 }
 
@@ -57,7 +59,8 @@ fun AiConfigScreen(
     onBackClick: () -> Unit,
     onNavigateToProviderEdit: (providerId: String?) -> Unit,
     onNavigateToModelEdit: (providerId: String?, modelProfileId: String?) -> Unit,
-    onNavigateToTranslation: () -> Unit
+    onNavigateToTranslation: () -> Unit,
+    onNavigateToAiSummary: () -> Unit
 ) {
     val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
     val snackbarHostState = remember { SnackbarHostState() }
@@ -131,7 +134,7 @@ fun AiConfigScreen(
                     )
                     ClickableSettingItem(
                         title = stringResource(R.string.ai_chapter_summary),
-                        onClick = {}
+                        onClick = onNavigateToAiSummary
                     )
                 }
             }

+ 38 - 0
app/src/main/java/io/legado/app/ui/config/ai/summary/AiSummaryConfigContract.kt

@@ -0,0 +1,38 @@
+package io.legado.app.ui.config.ai.summary
+
+import androidx.compose.runtime.Stable
+import io.legado.app.domain.model.AiPromptTemplate
+import io.legado.app.domain.model.TranslationConstants
+
+@Stable
+sealed interface AiSummaryConfigDialog {
+    @Stable
+    data class EditPrompt(val currentPrompt: String) : AiSummaryConfigDialog
+}
+
+@Stable
+data class AiSummaryConfigUiState(
+    val loading: Boolean = true,
+    val promptTemplate: String = AiPromptTemplate.DEFAULT_CHAPTER_SUMMARY,
+    val temperature: Float = TranslationConstants.DEFAULT_TEMPERATURE,
+    val maxOutputTokens: Int = 0,
+    val defaultPrompt: String = AiPromptTemplate.DEFAULT_CHAPTER_SUMMARY,
+    val initialized: Boolean = false,
+    val activeDialog: AiSummaryConfigDialog? = null
+)
+
+sealed interface AiSummaryConfigIntent {
+    data class UpdatePrompt(val prompt: String) : AiSummaryConfigIntent
+    data class OpenPromptDialog(val currentPrompt: String) : AiSummaryConfigIntent
+    data class UpdateDialogPrompt(val prompt: String) : AiSummaryConfigIntent
+    data object CloseDialog : AiSummaryConfigIntent
+    data class UpdateTemperature(val temperature: Float) : AiSummaryConfigIntent
+    data class UpdateMaxOutputTokens(val tokens: Int) : AiSummaryConfigIntent
+    data object ResetPrompt : AiSummaryConfigIntent
+    data object Save : AiSummaryConfigIntent
+}
+
+sealed interface AiSummaryConfigEffect {
+    data class ShowMessage(val message: String) : AiSummaryConfigEffect
+    data object NavigateBack : AiSummaryConfigEffect
+}

+ 187 - 0
app/src/main/java/io/legado/app/ui/config/ai/summary/AiSummaryConfigScreen.kt

@@ -0,0 +1,187 @@
+package io.legado.app.ui.config.ai.summary
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Save
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import io.legado.app.R
+import io.legado.app.domain.model.TranslationConstants
+import io.legado.app.ui.theme.adaptiveContentPadding
+import io.legado.app.ui.widget.components.AppFloatingActionButton
+import io.legado.app.ui.widget.components.AppScaffold
+import io.legado.app.ui.widget.components.AppTextField
+import io.legado.app.ui.widget.components.SplicedColumnGroup
+import io.legado.app.ui.widget.components.alert.AppAlertDialog
+import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
+import io.legado.app.ui.widget.components.settingItem.InputSettingItem
+import io.legado.app.ui.widget.components.settingItem.SliderSettingItem
+import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
+import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
+import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collectLatest
+import org.koin.androidx.compose.koinViewModel
+
+@Composable
+fun AiSummaryConfigRouteScreen(
+    onBackClick: () -> Unit,
+    viewModel: AiSummaryConfigViewModel = koinViewModel()
+) {
+    AiSummaryConfigScreen(
+        state = viewModel.uiState.collectAsStateWithLifecycle().value,
+        effects = viewModel.effects,
+        onIntent = viewModel::onIntent,
+        onBackClick = onBackClick
+    )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AiSummaryConfigScreen(
+    state: AiSummaryConfigUiState,
+    effects: Flow<AiSummaryConfigEffect>,
+    onIntent: (AiSummaryConfigIntent) -> Unit,
+    onBackClick: () -> Unit
+) {
+    val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
+    val snackbarHostState = remember { SnackbarHostState() }
+
+    LaunchedEffect(Unit) {
+        effects.collectLatest { effect ->
+            when (effect) {
+                is AiSummaryConfigEffect.ShowMessage -> {
+                    snackbarHostState.showSnackbar(effect.message)
+                }
+                is AiSummaryConfigEffect.NavigateBack -> {
+                    onBackClick()
+                }
+            }
+        }
+    }
+
+    AppScaffold(
+        modifier = Modifier
+            .fillMaxSize()
+            .nestedScroll(scrollBehavior.nestedScrollConnection),
+        topBar = {
+            GlassMediumFlexibleTopAppBar(
+                title = stringResource(R.string.ai_chapter_summary_config),
+                scrollBehavior = scrollBehavior,
+                navigationIcon = {
+                    TopBarNavigationButton(onClick = onBackClick)
+                }
+            )
+        },
+        floatingActionButton = {
+            AppFloatingActionButton(
+                onClick = { onIntent(AiSummaryConfigIntent.Save) },
+                icon = Icons.Default.Save,
+                tooltipText = stringResource(R.string.action_save)
+            )
+        },
+        snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
+    ) { paddingValues ->
+        LazyColumn(
+            modifier = Modifier.fillMaxSize(),
+            contentPadding = adaptiveContentPadding(
+                top = paddingValues.calculateTopPadding(),
+                bottom = 120.dp
+            )
+        ) {
+            item {
+                SplicedColumnGroup(title = stringResource(R.string.ai_prompt_setting)) {
+                    ClickableSettingItem(
+                        title = stringResource(R.string.ai_custom_prompt),
+                        description = stringResource(R.string.ai_custom_prompt_desc),
+                        onClick = {
+                            onIntent(AiSummaryConfigIntent.OpenPromptDialog(state.promptTemplate))
+                        }
+                    )
+                    ClickableSettingItem(
+                        title = stringResource(R.string.ai_reset_prompt),
+                        description = stringResource(R.string.ai_prompt_template),
+                        onClick = {
+                            onIntent(AiSummaryConfigIntent.ResetPrompt)
+                        }
+                    )
+                }
+            }
+            item {
+                SplicedColumnGroup(title = stringResource(R.string.ai_param_setting)) {
+                    SliderSettingItem(
+                        title = stringResource(R.string.ai_temperature),
+                        value = state.temperature,
+                        defaultValue = TranslationConstants.DEFAULT_TEMPERATURE,
+                        valueRange = TranslationConstants.MIN_TEMPERATURE..TranslationConstants.MAX_TEMPERATURE,
+                        steps = 19,
+                        description = state.temperature.toString(),
+                        onValueChange = { onIntent(AiSummaryConfigIntent.UpdateTemperature(it)) }
+                    )
+                    InputSettingItem(
+                        title = stringResource(R.string.ai_max_output_tokens),
+                        value = if (state.maxOutputTokens > 0) state.maxOutputTokens.toString() else "",
+                        defaultValue = "0",
+                        description = if (state.maxOutputTokens > 0) stringResource(R.string.ai_current_value, formatTokenLimit(state.maxOutputTokens)) else stringResource(R.string.ai_not_set),
+                        onConfirm = { input ->
+                            val tokens = input.trim().toIntOrNull() ?: 0
+                            onIntent(AiSummaryConfigIntent.UpdateMaxOutputTokens(tokens))
+                        }
+                    )
+                }
+            }
+        }
+    }
+
+    val activeDialog = state.activeDialog
+    val editPromptDialog = activeDialog as? AiSummaryConfigDialog.EditPrompt
+    AppAlertDialog(
+        show = editPromptDialog != null,
+        onDismissRequest = { onIntent(AiSummaryConfigIntent.CloseDialog) },
+        title = stringResource(R.string.ai_custom_prompt),
+        confirmText = stringResource(R.string.confirm),
+        onConfirm = {
+            if (editPromptDialog != null) {
+                onIntent(AiSummaryConfigIntent.UpdatePrompt(editPromptDialog.currentPrompt))
+            }
+        },
+        dismissText = stringResource(R.string.cancel),
+        onDismiss = { onIntent(AiSummaryConfigIntent.CloseDialog) },
+        content = {
+            if (editPromptDialog != null) {
+                AppTextField(
+                    value = editPromptDialog.currentPrompt,
+                    onValueChange = { onIntent(AiSummaryConfigIntent.UpdateDialogPrompt(it)) },
+                    singleLine = false,
+                    maxLines = 10,
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .heightIn(min = 160.dp, max = 320.dp)
+                        .imePadding()
+                )
+            }
+        }
+    )
+}
+
+private fun formatTokenLimit(value: Int): String {
+    return when {
+        value <= 0 -> "0"
+        value >= 1_000_000 && value % 1_000_000 == 0 -> "${value / 1_000_000}M"
+        value >= 1_000 && value % 1_000 == 0 -> "${value / 1_000}K"
+        else -> value.toString()
+    }
+}

+ 119 - 0
app/src/main/java/io/legado/app/ui/config/ai/summary/AiSummaryConfigViewModel.kt

@@ -0,0 +1,119 @@
+package io.legado.app.ui.config.ai.summary
+
+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.model.AiPromptTemplate
+import io.legado.app.domain.model.AiTaskType
+import io.legado.app.domain.model.TranslationConstants
+import io.legado.app.utils.toastOnUi
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import splitties.init.appCtx
+
+class AiSummaryConfigViewModel(
+    private val aiProfileGateway: AiProfileGateway
+) : ViewModel() {
+
+    private val _uiState = MutableStateFlow(AiSummaryConfigUiState())
+    val uiState = _uiState.asStateFlow()
+
+    private val _effects = MutableSharedFlow<AiSummaryConfigEffect>(extraBufferCapacity = 16)
+    val effects = _effects.asSharedFlow()
+
+    init {
+        viewModelScope.launch {
+            runCatching {
+                val config = aiProfileGateway.getTaskPreset(AiTaskType.SUMMARIZE_CHAPTER)
+                _uiState.update { current ->
+                    current.copy(
+                        loading = false,
+                        promptTemplate = config?.promptTemplate ?: AiPromptTemplate.DEFAULT_CHAPTER_SUMMARY,
+                        temperature = config?.params?.temperature ?: TranslationConstants.DEFAULT_TEMPERATURE,
+                        maxOutputTokens = config?.params?.maxOutputTokens ?: 0,
+                        defaultPrompt = AiPromptTemplate.DEFAULT_CHAPTER_SUMMARY,
+                        initialized = true
+                    )
+                }
+            }.onFailure { error ->
+                _uiState.update { current ->
+                    current.copy(
+                        loading = false,
+                        promptTemplate = AiPromptTemplate.DEFAULT_CHAPTER_SUMMARY,
+                        temperature = TranslationConstants.DEFAULT_TEMPERATURE,
+                        maxOutputTokens = 0,
+                        defaultPrompt = AiPromptTemplate.DEFAULT_CHAPTER_SUMMARY,
+                        initialized = true
+                    )
+                }
+                appCtx.toastOnUi("加载章节摘要配置失败: ${error.localizedMessage ?: "使用默认参数"}")
+            }
+        }
+    }
+
+    fun onIntent(intent: AiSummaryConfigIntent) {
+        when (intent) {
+            is AiSummaryConfigIntent.UpdatePrompt -> {
+                _uiState.update { it.copy(promptTemplate = intent.prompt, activeDialog = null) }
+            }
+            is AiSummaryConfigIntent.OpenPromptDialog -> {
+                _uiState.update { it.copy(activeDialog = AiSummaryConfigDialog.EditPrompt(intent.currentPrompt)) }
+            }
+            is AiSummaryConfigIntent.UpdateDialogPrompt -> {
+                val currentDialog = _uiState.value.activeDialog as? AiSummaryConfigDialog.EditPrompt
+                if (currentDialog != null) {
+                    _uiState.update { it.copy(activeDialog = currentDialog.copy(currentPrompt = intent.prompt)) }
+                }
+            }
+            is AiSummaryConfigIntent.CloseDialog -> {
+                _uiState.update { it.copy(activeDialog = null) }
+            }
+            is AiSummaryConfigIntent.UpdateTemperature -> {
+                _uiState.update { it.copy(temperature = intent.temperature) }
+            }
+            is AiSummaryConfigIntent.UpdateMaxOutputTokens -> {
+                _uiState.update { it.copy(maxOutputTokens = intent.tokens) }
+            }
+            is AiSummaryConfigIntent.ResetPrompt -> {
+                _uiState.update { it.copy(promptTemplate = AiPromptTemplate.DEFAULT_CHAPTER_SUMMARY) }
+                _effects.tryEmit(AiSummaryConfigEffect.ShowMessage(appCtx.getString(R.string.ai_prompt_reset_success)))
+            }
+            is AiSummaryConfigIntent.Save -> save()
+        }
+    }
+
+    private fun save() {
+        viewModelScope.launch {
+            runCatching {
+                val state = _uiState.value
+                val savedConfig = aiProfileGateway.saveTaskPreset(
+                    taskType = AiTaskType.SUMMARIZE_CHAPTER,
+                    promptTemplate = state.promptTemplate,
+                    temperature = state.temperature,
+                    maxOutputTokens = state.maxOutputTokens
+                )
+                _uiState.update { current ->
+                    current.copy(
+                        promptTemplate = savedConfig.promptTemplate,
+                        temperature = savedConfig.params.temperature ?: TranslationConstants.DEFAULT_TEMPERATURE,
+                        maxOutputTokens = savedConfig.params.maxOutputTokens ?: 0
+                    )
+                }
+            }.onSuccess {
+                appCtx.toastOnUi(R.string.ai_config_saved_success)
+                _effects.tryEmit(AiSummaryConfigEffect.NavigateBack)
+            }.onFailure { error ->
+                _effects.tryEmit(
+                    AiSummaryConfigEffect.ShowMessage(
+                        error.message ?: appCtx.getString(R.string.ai_config_save_failed)
+                    )
+                )
+            }
+        }
+    }
+}

+ 7 - 1
app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt

@@ -50,6 +50,7 @@ import io.legado.app.ui.config.ConfigNavScreen
 import io.legado.app.ui.config.ai.AiConfigRouteScreen
 import io.legado.app.ui.config.ai.AiModelEditRouteScreen
 import io.legado.app.ui.config.ai.AiProviderEditRouteScreen
+import io.legado.app.ui.config.ai.summary.AiSummaryConfigRouteScreen
 import io.legado.app.ui.config.backupConfig.BackupConfigScreen
 import io.legado.app.ui.config.coverConfig.CoverAlbumManageRouteScreen
 import io.legado.app.ui.config.coverConfig.CoverConfigScreen
@@ -247,10 +248,15 @@ fun MainActivity.mainEntryProvider(
                     )
                 )
             },
-            onNavigateToTranslation = { backStack.add(MainRouteSettingsTranslation) }
+            onNavigateToTranslation = { backStack.add(MainRouteSettingsTranslation) },
+            onNavigateToAiSummary = { backStack.add(MainRouteSettingsAiSummary) }
         )
     }
 
+    entry<MainRouteSettingsAiSummary> {
+        AiSummaryConfigRouteScreen(onBackClick = { onNavigateBack() })
+    }
+
     entry<MainRouteSettingsAiProviderEdit> { route ->
         AiProviderEditRouteScreen(
             providerId = route.providerId,

+ 4 - 0
app/src/main/java/io/legado/app/ui/main/MainNavKey.kt

@@ -47,6 +47,9 @@ data class MainRouteSettingsAiModelEdit(
     val modelProfileId: String? = null
 ) : MainRoute
 
+@Serializable
+data object MainRouteSettingsAiSummary : MainRoute
+
 @Serializable
 data object MainRouteSettingsCustomTheme : MainRoute
 
@@ -140,6 +143,7 @@ object MainRouteConst {
     const val ROUTE_SETTINGS_THEME = "settings/theme"
     const val ROUTE_SETTINGS_BACKUP = "settings/backup"
     const val ROUTE_SETTINGS_AI = "settings/ai"
+    const val ROUTE_SETTINGS_AI_SUMMARY = "settings/ai/summary"
     const val ROUTE_AI_CHAT = "ai/chat"
     const val ROUTE_SETTINGS_CUSTOM_THEME = "settings/custom_theme"
     const val ROUTE_SETTINGS_LAB_CONFIG = "settings/lab_config"

+ 1 - 0
app/src/main/java/io/legado/app/ui/main/MainNavigator.kt

@@ -59,6 +59,7 @@ object MainNavigator {
             MainRouteSettingsAi,
             is MainRouteSettingsAiProviderEdit,
             is MainRouteSettingsAiModelEdit,
+            MainRouteSettingsAiSummary,
             MainRouteSettingsCustomTheme,
             MainRouteSettingsThemeManage,
             MainRouteSettingsDownloadCache,

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

@@ -1926,6 +1926,16 @@
     <string name="ai_provider_database">供应商</string>
     <string name="ai_tasks">任务</string>
     <string name="ai_chapter_summary">章节梗概</string>
+    <string name="ai_chapter_summary_config">章节梗概配置</string>
+    <string name="ai_prompt_setting">提示词设置</string>
+    <string name="ai_param_setting">参数设置</string>
+    <string name="ai_custom_prompt">自定义提示词</string>
+    <string name="ai_custom_prompt_desc">点击修改或查看当前章节梗概提示词</string>
+    <string name="ai_prompt_template">提示词模板</string>
+    <string name="ai_reset_prompt">重置提示词</string>
+    <string name="ai_prompt_reset_success">提示词已重置为默认</string>
+    <string name="ai_config_saved_success">章节梗概配置已保存</string>
+    <string name="ai_config_save_failed">保存配置失败</string>
     <string name="ai_chapter_summary_loading">正在提炼当前章节梗概…</string>
     <string name="ai_chapter_changed">当前章节已变化,请重新操作。</string>
     <string name="ai_chapter_content_unavailable">无法读取当前章节正文。</string>

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

@@ -1856,6 +1856,16 @@
     <string name="ai_provider_database">Providers</string>
     <string name="ai_tasks">Tasks</string>
     <string name="ai_chapter_summary">Chapter Summary</string>
+    <string name="ai_chapter_summary_config">Chapter Summary Configuration</string>
+    <string name="ai_prompt_setting">Prompt Settings</string>
+    <string name="ai_param_setting">Parameter Settings</string>
+    <string name="ai_custom_prompt">Custom Prompt</string>
+    <string name="ai_custom_prompt_desc">Click to view or edit chapter summary prompt</string>
+    <string name="ai_prompt_template">Prompt Template</string>
+    <string name="ai_reset_prompt">Reset Prompt</string>
+    <string name="ai_prompt_reset_success">Prompt reset to default</string>
+    <string name="ai_config_saved_success">Chapter summary configuration saved</string>
+    <string name="ai_config_save_failed">Failed to save configuration</string>
     <string name="ai_chapter_summary_loading">Summarizing the current chapter…</string>
     <string name="ai_chapter_changed">The current chapter has changed. Please try again.</string>
     <string name="ai_chapter_content_unavailable">The chapter content is unavailable.</string>