diff --git a/src/composables/useSetupWizard.ts b/src/composables/useSetupWizard.ts index 262c6789..842d86d4 100644 --- a/src/composables/useSetupWizard.ts +++ b/src/composables/useSetupWizard.ts @@ -13,6 +13,8 @@ export interface WizardData { username: string password: string confirmPassword: string + recognizeSource: string + ocrHost: string proxyHost: string githubToken: string } @@ -41,6 +43,22 @@ export interface WizardData { config: any switchs: any[] } + agent: { + enabled: boolean + global: boolean + verbose: boolean + provider: string + model: string + supportImageInput: boolean + apiKey: string + baseUrl: string + maxContextTokens: number + jobInterval: number + retryTransfer: boolean + recommendEnabled: boolean + recommendUserPreference: string + recommendMaxItems: number + } preferences: { quality: string subtitle: string @@ -85,11 +103,18 @@ export interface ValidationErrorState { name: boolean [key: string]: boolean } + agent: { + provider: boolean + apiKey: boolean + model: boolean + maxContextTokens: boolean + recommendMaxItems: boolean + } } // 全局状态,所有组件共享 const currentStep = ref(1) -const totalSteps = 6 +const totalSteps = 7 // 加载状态 const isLoading = ref(false) @@ -105,6 +130,8 @@ const wizardData = ref({ username: '', password: '', confirmPassword: '', + recognizeSource: 'themoviedb', + ocrHost: '', proxyHost: '', githubToken: '', }, @@ -133,6 +160,22 @@ const wizardData = ref({ config: {}, switchs: [], }, + agent: { + enabled: false, + global: false, + verbose: false, + provider: 'deepseek', + model: 'deepseek-chat', + supportImageInput: true, + apiKey: '', + baseUrl: 'https://api.deepseek.com', + maxContextTokens: 64, + jobInterval: 0, + retryTransfer: false, + recommendEnabled: false, + recommendUserPreference: '', + recommendMaxItems: 50, + }, preferences: { quality: '4K', subtitle: 'chinese', @@ -168,6 +211,13 @@ const validationErrors = ref({ notification: { name: false, }, + agent: { + provider: false, + apiKey: false, + model: false, + maxContextTokens: false, + recommendMaxItems: false, + }, }) export function useSetupWizard() { @@ -181,6 +231,7 @@ export function useSetupWizard() { downloader: { 'qbittorrent': 'QbittorrentModule', 'transmission': 'TransmissionModule', + 'rtorrent': 'RtorrentModule', }, // 媒体服务器映射 mediaServer: { @@ -196,6 +247,7 @@ export function useSetupWizard() { 'wechat': 'WechatModule', 'slack': 'SlackModule', 'synologychat': 'SynologyChatModule', + 'qqbot': 'QQBotModule', 'vocechat': 'VoceChatModule', 'webpush': 'WebPushModule', }, @@ -208,6 +260,7 @@ export function useSetupWizard() { t('setupWizard.downloader.title'), t('setupWizard.mediaServer.title'), t('setupWizard.notification.title'), + t('setupWizard.agent.title'), t('setupWizard.preferences.title'), ]) @@ -218,6 +271,7 @@ export function useSetupWizard() { t('setupWizard.downloader.description'), t('setupWizard.mediaServer.description'), t('setupWizard.notification.description'), + t('setupWizard.agent.description'), t('setupWizard.preferences.description'), ]) @@ -341,6 +395,13 @@ export function useSetupWizard() { validationErrors.value.notification = { name: false, } + validationErrors.value.agent = { + provider: false, + apiKey: false, + model: false, + maxContextTokens: false, + recommendMaxItems: false, + } } // 验证下载器字段 @@ -361,7 +422,11 @@ export function useSetupWizard() { } // 根据下载器类型验证其他必输项 - if (wizardData.value.downloader.type === 'qbittorrent' || wizardData.value.downloader.type === 'transmission') { + if ( + wizardData.value.downloader.type === 'qbittorrent' + || wizardData.value.downloader.type === 'transmission' + || wizardData.value.downloader.type === 'rtorrent' + ) { if (!wizardData.value.downloader.config?.username?.trim()) { errors.push(t('downloader.usernameRequired')) validationErrors.value.downloader.username = true @@ -487,6 +552,12 @@ export function useSetupWizard() { validationErrors.value.notification.VOCECHAT_API_KEY = true } break + case 'webpush': + if (!config.WEBPUSH_USERNAME?.trim()) { + errors.push(t('notification.webpush.usernameRequired')) + validationErrors.value.notification.WEBPUSH_USERNAME = true + } + break case 'qqbot': if (!config.QQ_APP_ID?.trim()) { errors.push(t('notification.qqbot.appIdRequired')) @@ -505,6 +576,49 @@ export function useSetupWizard() { } } + // 验证智能助手字段 + function validateAgentFields(): { isValid: boolean; errors: string[] } { + const errors: string[] = [] + clearValidationErrors() + + if (!wizardData.value.agent.enabled) { + return { + isValid: true, + errors, + } + } + + if (!wizardData.value.agent.provider?.trim()) { + errors.push(t('setupWizard.agent.providerRequired')) + validationErrors.value.agent.provider = true + } + + if (!wizardData.value.agent.apiKey?.trim()) { + errors.push(t('setupWizard.agent.apiKeyRequired')) + validationErrors.value.agent.apiKey = true + } + + if (!wizardData.value.agent.model?.trim()) { + errors.push(t('setupWizard.agent.modelRequired')) + validationErrors.value.agent.model = true + } + + if (!wizardData.value.agent.maxContextTokens || wizardData.value.agent.maxContextTokens < 1) { + errors.push(t('setupWizard.agent.maxContextTokensRequired')) + validationErrors.value.agent.maxContextTokens = true + } + + if (wizardData.value.agent.recommendEnabled && (!wizardData.value.agent.recommendMaxItems || wizardData.value.agent.recommendMaxItems < 1)) { + errors.push(t('setupWizard.agent.recommendMaxItemsRequired')) + validationErrors.value.agent.recommendMaxItems = true + } + + return { + isValid: errors.length === 0, + errors, + } + } + // 验证当前步骤的必输项 function validateCurrentStep(): { isValid: boolean; errors: string[] } { const errors: string[] = [] @@ -563,7 +677,14 @@ export function useSetupWizard() { } break - case 6: // 偏好设置 + case 6: // 智能助手设置 + if (wizardData.value.agent.enabled) { + const validation = validateAgentFields() + errors.push(...validation.errors) + } + break + + case 7: // 偏好设置 // 偏好设置有默认值,不需要验证 break } @@ -794,18 +915,21 @@ export function useSetupWizard() { validation.errors.forEach(error => { $toast.error(error) }) - return + return false } // 保存当前步骤的设置 - await saveCurrentStepSettings() + const saved = await saveCurrentStepSettings() + if (!saved) { + return false + } // 检查是否需要进行测试 const needsTest = shouldPerformTest(currentStep.value) if (needsTest) { const testResult = await testConnectivity(currentStep.value) if (!testResult) { - return + return false } } @@ -814,6 +938,8 @@ export function useSetupWizard() { currentStep.value++ connectivityTest.value.showResult = false } + + return true } // 上一步 @@ -829,35 +955,36 @@ export function useSetupWizard() { try { switch (currentStep.value) { case 1: - await saveBasicSettings() - break + return await saveBasicSettings() case 2: - await saveStorageSettings() - break + return await saveStorageSettings() case 3: - await saveDownloaderSettings() - break + return await saveDownloaderSettings() case 4: - await saveMediaServerSettings() - break + return await saveMediaServerSettings() case 5: - await saveNotificationSettings() - break + return await saveNotificationSettings() case 6: - await savePreferenceSettings() - break + return await saveAgentSettings() + case 7: + return await savePreferenceSettings() } } catch (error) { console.error('Save current step settings failed:', error) $toast.error(t('setupWizard.saveStepFailed')) + return false } + return true } // 完成向导 async function completeWizard() { try { // 先处理下一步(保存当前步骤设置) - await nextStep() + const saved = await nextStep() + if (!saved) { + return + } // 保存设置向导完成状态 await saveSetupWizardState() @@ -910,6 +1037,8 @@ export function useSetupWizard() { const basicSettings = { APP_DOMAIN: wizardData.value.basic.appDomain, API_TOKEN: wizardData.value.basic.apiToken, + RECOGNIZE_SOURCE: wizardData.value.basic.recognizeSource, + OCR_HOST: wizardData.value.basic.ocrHost, PROXY_HOST: wizardData.value.basic.proxyHost, GITHUB_TOKEN: wizardData.value.basic.githubToken, } @@ -917,21 +1046,23 @@ export function useSetupWizard() { // 保存基础设置 const response: { [key: string]: any } = await api.post('system/env', basicSettings) if (!response.success) { - return + return false } // 如果输入了密码,验证密码一致性 if (wizardData.value.basic.password) { if (wizardData.value.basic.password !== wizardData.value.basic.confirmPassword) { $toast.error(t('dialog.userAddEdit.passwordMismatch')) - return + return false } // 更新用户密码 await updateUserPassword() } + return true } catch (error) { console.error('Save basic settings failed:', error) $toast.error(t('setupWizard.saveBasicSettingsFailed')) + return false } } @@ -970,9 +1101,11 @@ export function useSetupWizard() { } await api.post('system/setting/Directories', [directory]) + return true } catch (error) { console.error('Save storage settings failed:', error) $toast.error(t('setupWizard.saveStorageSettingsFailed')) + return false } } @@ -992,13 +1125,16 @@ export function useSetupWizard() { } await api.post('system/setting/Downloaders', [downloader]) + return true } catch (error) { console.error('Save downloader settings failed:', error) $toast.error(t('setupWizard.saveDownloaderSettingsFailed')) + return false } } else { // 没有选择下载器时,清空现有配置 console.log('No downloader selected, skipping save') + return true } } @@ -1019,13 +1155,16 @@ export function useSetupWizard() { } await api.post('system/setting/MediaServers', [mediaServer]) + return true } catch (error) { console.error('Save media server settings failed:', error) $toast.error(t('setupWizard.saveMediaServerSettingsFailed')) + return false } } else { // 没有选择媒体服务器时,清空现有配置 console.log('No media server selected, skipping save') + return true } } @@ -1046,13 +1185,46 @@ export function useSetupWizard() { } await api.post('system/setting/Notifications', [notification]) + return true } catch (error) { console.error('Save notification settings failed:', error) $toast.error(t('setupWizard.saveNotificationSettingsFailed')) + return false } } else { // 没有选择通知时,清空现有配置 console.log('No notification selected, skipping save') + return true + } + } + + // 保存智能助手设置 + async function saveAgentSettings() { + try { + const agentSettings = { + AI_AGENT_ENABLE: wizardData.value.agent.enabled, + AI_AGENT_GLOBAL: wizardData.value.agent.enabled ? wizardData.value.agent.global : false, + AI_AGENT_VERBOSE: wizardData.value.agent.enabled ? wizardData.value.agent.verbose : false, + LLM_PROVIDER: wizardData.value.agent.provider, + LLM_MODEL: wizardData.value.agent.model, + LLM_SUPPORT_IMAGE_INPUT: wizardData.value.agent.supportImageInput, + LLM_API_KEY: wizardData.value.agent.apiKey, + LLM_BASE_URL: wizardData.value.agent.baseUrl || null, + LLM_MAX_CONTEXT_TOKENS: wizardData.value.agent.maxContextTokens, + AI_AGENT_JOB_INTERVAL: wizardData.value.agent.enabled ? wizardData.value.agent.jobInterval : 0, + AI_AGENT_RETRY_TRANSFER: wizardData.value.agent.enabled ? wizardData.value.agent.retryTransfer : false, + AI_RECOMMEND_ENABLED: + wizardData.value.agent.enabled && wizardData.value.agent.recommendEnabled, + AI_RECOMMEND_USER_PREFERENCE: wizardData.value.agent.recommendUserPreference, + AI_RECOMMEND_MAX_ITEMS: wizardData.value.agent.recommendMaxItems, + } + + await api.post('system/env', agentSettings) + return true + } catch (error) { + console.error('Save agent settings failed:', error) + $toast.error(t('setupWizard.saveAgentSettingsFailed')) + return false } } @@ -1081,9 +1253,11 @@ export function useSetupWizard() { console.error('Save rule sequences failed:', error) } } + return true } catch (error) { console.error('Save preference settings failed:', error) $toast.error(t('setupWizard.savePreferenceSettingsFailed')) + return false } } @@ -1115,12 +1289,32 @@ export function useSetupWizard() { if (result.data.PROXY_HOST) { wizardData.value.basic.proxyHost = result.data.PROXY_HOST } + if (result.data.RECOGNIZE_SOURCE) { + wizardData.value.basic.recognizeSource = result.data.RECOGNIZE_SOURCE + } + if (result.data.OCR_HOST) { + wizardData.value.basic.ocrHost = result.data.OCR_HOST + } if (result.data.GITHUB_TOKEN) { wizardData.value.basic.githubToken = result.data.GITHUB_TOKEN } if (result.data.SUPERUSER) { wizardData.value.basic.username = result.data.SUPERUSER } + wizardData.value.agent.enabled = Boolean(result.data.AI_AGENT_ENABLE) + wizardData.value.agent.global = Boolean(result.data.AI_AGENT_GLOBAL) + wizardData.value.agent.verbose = Boolean(result.data.AI_AGENT_VERBOSE) + wizardData.value.agent.provider = result.data.LLM_PROVIDER || 'deepseek' + wizardData.value.agent.model = result.data.LLM_MODEL || '' + wizardData.value.agent.supportImageInput = result.data.LLM_SUPPORT_IMAGE_INPUT ?? true + wizardData.value.agent.apiKey = result.data.LLM_API_KEY || '' + wizardData.value.agent.baseUrl = result.data.LLM_BASE_URL || '' + wizardData.value.agent.maxContextTokens = result.data.LLM_MAX_CONTEXT_TOKENS || 64 + wizardData.value.agent.jobInterval = result.data.AI_AGENT_JOB_INTERVAL || 0 + wizardData.value.agent.retryTransfer = Boolean(result.data.AI_AGENT_RETRY_TRANSFER) + wizardData.value.agent.recommendEnabled = Boolean(result.data.AI_RECOMMEND_ENABLED) + wizardData.value.agent.recommendUserPreference = result.data.AI_RECOMMEND_USER_PREFERENCE || '' + wizardData.value.agent.recommendMaxItems = result.data.AI_RECOMMEND_MAX_ITEMS || 50 // 如果没有API Token,则创建一个随机的 if (!wizardData.value.basic.apiToken) { @@ -1234,6 +1428,7 @@ export function useSetupWizard() { validateDownloaderFields, validateMediaServerFields, validateNotificationFields, + validateAgentFields, clearValidationErrors, testConnectivity, nextStep, diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index 28600122..d7ddb558 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -3183,6 +3183,7 @@ export default { saveMediaServerSettingsFailed: 'Failed to save media server settings', notificationSettingsSaved: 'Notification settings saved successfully', saveNotificationSettingsFailed: 'Failed to save notification settings', + saveAgentSettingsFailed: 'Failed to save AI assistant settings', preferenceSettingsSaved: 'Preference settings saved successfully', savePreferenceSettingsFailed: 'Failed to save preference settings', passwordUpdateSuccess: 'Password updated successfully', @@ -3268,6 +3269,18 @@ export default { senderPassword: 'Sender Password', receiverEmail: 'Receiver Email', }, + agent: { + title: 'AI Assistant', + description: 'Configure the Agent assistant and LLM parameters', + info: 'AI Assistant Configuration', + infoDesc: + 'After enabling it, you can use the Agent in message conversations and optionally turn on transfer-failure takeover and AI recommendations.', + providerRequired: 'LLM provider is required', + apiKeyRequired: 'LLM API key is required', + modelRequired: 'LLM model name is required', + maxContextTokensRequired: 'LLM max context tokens must be greater than 0', + recommendMaxItemsRequired: 'AI recommendation analysis limit must be greater than 0', + }, preferences: { title: 'Resource Preferences', description: 'Set resource download preferences', diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 64b52ac6..bdc512d5 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -3147,6 +3147,7 @@ export default { saveMediaServerSettingsFailed: '保存媒体服务器设置失败', notificationSettingsSaved: '通知设置保存成功', saveNotificationSettingsFailed: '保存通知设置失败', + saveAgentSettingsFailed: '保存智能助手设置失败', preferenceSettingsSaved: '偏好设置保存成功', savePreferenceSettingsFailed: '保存偏好设置失败', passwordUpdateSuccess: '密码更新成功', @@ -3231,6 +3232,17 @@ export default { senderPassword: '发送密码', receiverEmail: '接收邮箱', }, + agent: { + title: '智能助手', + description: '配置 Agent 助手与 LLM 参数', + info: '智能助手配置说明', + infoDesc: '启用后可在消息会话中使用 Agent 能力,也可开启失败整理接管和智能推荐。', + providerRequired: 'LLM 提供商不能为空', + apiKeyRequired: 'LLM API 密钥不能为空', + modelRequired: 'LLM 模型名称不能为空', + maxContextTokensRequired: 'LLM 最大上下文 Token 数量必须大于 0', + recommendMaxItemsRequired: '智能推荐分析条目上限必须大于 0', + }, preferences: { title: '资源偏好', description: '设置资源下载偏好', @@ -3273,7 +3285,3 @@ export default { }, }, } - -// Apply patch to add category strings -// This is a temporary placeholder command to show intent. -// I will use replace_file_content to actually edit the file safely. diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index 41d35d64..1691461c 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -3148,6 +3148,7 @@ export default { saveMediaServerSettingsFailed: '保存媒體服務器設置失敗', notificationSettingsSaved: '通知設置保存成功', saveNotificationSettingsFailed: '保存通知設置失敗', + saveAgentSettingsFailed: '保存智能助手設置失敗', preferenceSettingsSaved: '偏好設置保存成功', savePreferenceSettingsFailed: '保存偏好設置失敗', passwordUpdateSuccess: '密碼更新成功', @@ -3232,6 +3233,17 @@ export default { senderPassword: '發送密碼', receiverEmail: '接收信箱', }, + agent: { + title: '智能助手', + description: '配置 Agent 助手與 LLM 參數', + info: '智能助手配置說明', + infoDesc: '啟用後可在消息對話中使用 Agent 能力,也可開啟失敗整理接管與智能推薦。', + providerRequired: 'LLM 提供商不能為空', + apiKeyRequired: 'LLM API 密鑰不能為空', + modelRequired: 'LLM 模型名稱不能為空', + maxContextTokensRequired: 'LLM 最大上下文 Token 數量必須大於 0', + recommendMaxItemsRequired: '智能推薦分析條目上限必須大於 0', + }, preferences: { title: '資源偏好', description: '設定資源下載偏好', diff --git a/src/pages/setup.vue b/src/pages/setup.vue index d8251111..3c9f7e65 100644 --- a/src/pages/setup.vue +++ b/src/pages/setup.vue @@ -8,6 +8,7 @@ import StorageSettingsStep from '@/views/setup/StorageSettingsStep.vue' import DownloaderSettingsStep from '@/views/setup/DownloaderSettingsStep.vue' import MediaServerSettingsStep from '@/views/setup/MediaServerSettingsStep.vue' import NotificationSettingsStep from '@/views/setup/NotificationSettingsStep.vue' +import AgentSettingsStep from '@/views/setup/AgentSettingsStep.vue' import PreferencesSettingsStep from '@/views/setup/PreferencesSettingsStep.vue' import ConnectivityTest from '@/views/setup/ConnectivityTest.vue' import { useDisplay } from 'vuetify' @@ -121,8 +122,13 @@ onMounted(async () => { - + + + + + + diff --git a/src/views/setup/AgentSettingsStep.vue b/src/views/setup/AgentSettingsStep.vue new file mode 100644 index 00000000..80a81a62 --- /dev/null +++ b/src/views/setup/AgentSettingsStep.vue @@ -0,0 +1,262 @@ + + + diff --git a/src/views/setup/BasicSettingsStep.vue b/src/views/setup/BasicSettingsStep.vue index a2064423..6a89146b 100644 --- a/src/views/setup/BasicSettingsStep.vue +++ b/src/views/setup/BasicSettingsStep.vue @@ -37,6 +37,11 @@ const confirmPasswordErrorMessage = computed(() => { return '' }) +const recognizeSourceItems = [ + { title: 'TheMovieDb', value: 'themoviedb' }, + { title: '豆瓣', value: 'douban' }, +] + // API Token验证 const apiTokenError = computed(() => { return !wizardData.value.basic.apiToken && hasErrors.value @@ -119,6 +124,26 @@ const usernameErrorMessage = computed(() => { clearable /> + + + + + + @@ -354,6 +358,12 @@ const notificationTypes = [ v-model="wizardData.notification.config.QQ_APP_SECRET" :label="t('notification.qqbot.appSecret')" :hint="t('notification.qqbot.appSecretHint')" + :error="validationErrors.notification.QQ_APP_SECRET" + :error-messages=" + validationErrors.notification.QQ_APP_SECRET + ? [t('notification.qqbot.appSecretRequired')] + : [] + " persistent-hint prepend-inner-icon="mdi-key" />