feat: add AI agent configuration step and expand basic settings with OCR and recognition source options

This commit is contained in:
jxxghp
2026-04-16 17:36:27 +08:00
parent b40fc4bd30
commit b29c6bd83f
8 changed files with 557 additions and 26 deletions
+216 -21
View File
@@ -13,6 +13,8 @@ export interface WizardData {
username: string username: string
password: string password: string
confirmPassword: string confirmPassword: string
recognizeSource: string
ocrHost: string
proxyHost: string proxyHost: string
githubToken: string githubToken: string
} }
@@ -41,6 +43,22 @@ export interface WizardData {
config: any config: any
switchs: 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: { preferences: {
quality: string quality: string
subtitle: string subtitle: string
@@ -85,11 +103,18 @@ export interface ValidationErrorState {
name: boolean name: boolean
[key: string]: boolean [key: string]: boolean
} }
agent: {
provider: boolean
apiKey: boolean
model: boolean
maxContextTokens: boolean
recommendMaxItems: boolean
}
} }
// 全局状态,所有组件共享 // 全局状态,所有组件共享
const currentStep = ref(1) const currentStep = ref(1)
const totalSteps = 6 const totalSteps = 7
// 加载状态 // 加载状态
const isLoading = ref(false) const isLoading = ref(false)
@@ -105,6 +130,8 @@ const wizardData = ref<WizardData>({
username: '', username: '',
password: '', password: '',
confirmPassword: '', confirmPassword: '',
recognizeSource: 'themoviedb',
ocrHost: '',
proxyHost: '', proxyHost: '',
githubToken: '', githubToken: '',
}, },
@@ -133,6 +160,22 @@ const wizardData = ref<WizardData>({
config: {}, config: {},
switchs: [], 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: { preferences: {
quality: '4K', quality: '4K',
subtitle: 'chinese', subtitle: 'chinese',
@@ -168,6 +211,13 @@ const validationErrors = ref<ValidationErrorState>({
notification: { notification: {
name: false, name: false,
}, },
agent: {
provider: false,
apiKey: false,
model: false,
maxContextTokens: false,
recommendMaxItems: false,
},
}) })
export function useSetupWizard() { export function useSetupWizard() {
@@ -181,6 +231,7 @@ export function useSetupWizard() {
downloader: { downloader: {
'qbittorrent': 'QbittorrentModule', 'qbittorrent': 'QbittorrentModule',
'transmission': 'TransmissionModule', 'transmission': 'TransmissionModule',
'rtorrent': 'RtorrentModule',
}, },
// 媒体服务器映射 // 媒体服务器映射
mediaServer: { mediaServer: {
@@ -196,6 +247,7 @@ export function useSetupWizard() {
'wechat': 'WechatModule', 'wechat': 'WechatModule',
'slack': 'SlackModule', 'slack': 'SlackModule',
'synologychat': 'SynologyChatModule', 'synologychat': 'SynologyChatModule',
'qqbot': 'QQBotModule',
'vocechat': 'VoceChatModule', 'vocechat': 'VoceChatModule',
'webpush': 'WebPushModule', 'webpush': 'WebPushModule',
}, },
@@ -208,6 +260,7 @@ export function useSetupWizard() {
t('setupWizard.downloader.title'), t('setupWizard.downloader.title'),
t('setupWizard.mediaServer.title'), t('setupWizard.mediaServer.title'),
t('setupWizard.notification.title'), t('setupWizard.notification.title'),
t('setupWizard.agent.title'),
t('setupWizard.preferences.title'), t('setupWizard.preferences.title'),
]) ])
@@ -218,6 +271,7 @@ export function useSetupWizard() {
t('setupWizard.downloader.description'), t('setupWizard.downloader.description'),
t('setupWizard.mediaServer.description'), t('setupWizard.mediaServer.description'),
t('setupWizard.notification.description'), t('setupWizard.notification.description'),
t('setupWizard.agent.description'),
t('setupWizard.preferences.description'), t('setupWizard.preferences.description'),
]) ])
@@ -341,6 +395,13 @@ export function useSetupWizard() {
validationErrors.value.notification = { validationErrors.value.notification = {
name: false, 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()) { if (!wizardData.value.downloader.config?.username?.trim()) {
errors.push(t('downloader.usernameRequired')) errors.push(t('downloader.usernameRequired'))
validationErrors.value.downloader.username = true validationErrors.value.downloader.username = true
@@ -487,6 +552,12 @@ export function useSetupWizard() {
validationErrors.value.notification.VOCECHAT_API_KEY = true validationErrors.value.notification.VOCECHAT_API_KEY = true
} }
break break
case 'webpush':
if (!config.WEBPUSH_USERNAME?.trim()) {
errors.push(t('notification.webpush.usernameRequired'))
validationErrors.value.notification.WEBPUSH_USERNAME = true
}
break
case 'qqbot': case 'qqbot':
if (!config.QQ_APP_ID?.trim()) { if (!config.QQ_APP_ID?.trim()) {
errors.push(t('notification.qqbot.appIdRequired')) 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[] } { function validateCurrentStep(): { isValid: boolean; errors: string[] } {
const errors: string[] = [] const errors: string[] = []
@@ -563,7 +677,14 @@ export function useSetupWizard() {
} }
break break
case 6: // 偏好设置 case 6: // 智能助手设置
if (wizardData.value.agent.enabled) {
const validation = validateAgentFields()
errors.push(...validation.errors)
}
break
case 7: // 偏好设置
// 偏好设置有默认值,不需要验证 // 偏好设置有默认值,不需要验证
break break
} }
@@ -794,18 +915,21 @@ export function useSetupWizard() {
validation.errors.forEach(error => { validation.errors.forEach(error => {
$toast.error(error) $toast.error(error)
}) })
return return false
} }
// 保存当前步骤的设置 // 保存当前步骤的设置
await saveCurrentStepSettings() const saved = await saveCurrentStepSettings()
if (!saved) {
return false
}
// 检查是否需要进行测试 // 检查是否需要进行测试
const needsTest = shouldPerformTest(currentStep.value) const needsTest = shouldPerformTest(currentStep.value)
if (needsTest) { if (needsTest) {
const testResult = await testConnectivity(currentStep.value) const testResult = await testConnectivity(currentStep.value)
if (!testResult) { if (!testResult) {
return return false
} }
} }
@@ -814,6 +938,8 @@ export function useSetupWizard() {
currentStep.value++ currentStep.value++
connectivityTest.value.showResult = false connectivityTest.value.showResult = false
} }
return true
} }
// 上一步 // 上一步
@@ -829,35 +955,36 @@ export function useSetupWizard() {
try { try {
switch (currentStep.value) { switch (currentStep.value) {
case 1: case 1:
await saveBasicSettings() return await saveBasicSettings()
break
case 2: case 2:
await saveStorageSettings() return await saveStorageSettings()
break
case 3: case 3:
await saveDownloaderSettings() return await saveDownloaderSettings()
break
case 4: case 4:
await saveMediaServerSettings() return await saveMediaServerSettings()
break
case 5: case 5:
await saveNotificationSettings() return await saveNotificationSettings()
break
case 6: case 6:
await savePreferenceSettings() return await saveAgentSettings()
break case 7:
return await savePreferenceSettings()
} }
} catch (error) { } catch (error) {
console.error('Save current step settings failed:', error) console.error('Save current step settings failed:', error)
$toast.error(t('setupWizard.saveStepFailed')) $toast.error(t('setupWizard.saveStepFailed'))
return false
} }
return true
} }
// 完成向导 // 完成向导
async function completeWizard() { async function completeWizard() {
try { try {
// 先处理下一步(保存当前步骤设置) // 先处理下一步(保存当前步骤设置)
await nextStep() const saved = await nextStep()
if (!saved) {
return
}
// 保存设置向导完成状态 // 保存设置向导完成状态
await saveSetupWizardState() await saveSetupWizardState()
@@ -910,6 +1037,8 @@ export function useSetupWizard() {
const basicSettings = { const basicSettings = {
APP_DOMAIN: wizardData.value.basic.appDomain, APP_DOMAIN: wizardData.value.basic.appDomain,
API_TOKEN: wizardData.value.basic.apiToken, API_TOKEN: wizardData.value.basic.apiToken,
RECOGNIZE_SOURCE: wizardData.value.basic.recognizeSource,
OCR_HOST: wizardData.value.basic.ocrHost,
PROXY_HOST: wizardData.value.basic.proxyHost, PROXY_HOST: wizardData.value.basic.proxyHost,
GITHUB_TOKEN: wizardData.value.basic.githubToken, GITHUB_TOKEN: wizardData.value.basic.githubToken,
} }
@@ -917,21 +1046,23 @@ export function useSetupWizard() {
// 保存基础设置 // 保存基础设置
const response: { [key: string]: any } = await api.post('system/env', basicSettings) const response: { [key: string]: any } = await api.post('system/env', basicSettings)
if (!response.success) { if (!response.success) {
return return false
} }
// 如果输入了密码,验证密码一致性 // 如果输入了密码,验证密码一致性
if (wizardData.value.basic.password) { if (wizardData.value.basic.password) {
if (wizardData.value.basic.password !== wizardData.value.basic.confirmPassword) { if (wizardData.value.basic.password !== wizardData.value.basic.confirmPassword) {
$toast.error(t('dialog.userAddEdit.passwordMismatch')) $toast.error(t('dialog.userAddEdit.passwordMismatch'))
return return false
} }
// 更新用户密码 // 更新用户密码
await updateUserPassword() await updateUserPassword()
} }
return true
} catch (error) { } catch (error) {
console.error('Save basic settings failed:', error) console.error('Save basic settings failed:', error)
$toast.error(t('setupWizard.saveBasicSettingsFailed')) $toast.error(t('setupWizard.saveBasicSettingsFailed'))
return false
} }
} }
@@ -970,9 +1101,11 @@ export function useSetupWizard() {
} }
await api.post('system/setting/Directories', [directory]) await api.post('system/setting/Directories', [directory])
return true
} catch (error) { } catch (error) {
console.error('Save storage settings failed:', error) console.error('Save storage settings failed:', error)
$toast.error(t('setupWizard.saveStorageSettingsFailed')) $toast.error(t('setupWizard.saveStorageSettingsFailed'))
return false
} }
} }
@@ -992,13 +1125,16 @@ export function useSetupWizard() {
} }
await api.post('system/setting/Downloaders', [downloader]) await api.post('system/setting/Downloaders', [downloader])
return true
} catch (error) { } catch (error) {
console.error('Save downloader settings failed:', error) console.error('Save downloader settings failed:', error)
$toast.error(t('setupWizard.saveDownloaderSettingsFailed')) $toast.error(t('setupWizard.saveDownloaderSettingsFailed'))
return false
} }
} else { } else {
// 没有选择下载器时,清空现有配置 // 没有选择下载器时,清空现有配置
console.log('No downloader selected, skipping save') console.log('No downloader selected, skipping save')
return true
} }
} }
@@ -1019,13 +1155,16 @@ export function useSetupWizard() {
} }
await api.post('system/setting/MediaServers', [mediaServer]) await api.post('system/setting/MediaServers', [mediaServer])
return true
} catch (error) { } catch (error) {
console.error('Save media server settings failed:', error) console.error('Save media server settings failed:', error)
$toast.error(t('setupWizard.saveMediaServerSettingsFailed')) $toast.error(t('setupWizard.saveMediaServerSettingsFailed'))
return false
} }
} else { } else {
// 没有选择媒体服务器时,清空现有配置 // 没有选择媒体服务器时,清空现有配置
console.log('No media server selected, skipping save') console.log('No media server selected, skipping save')
return true
} }
} }
@@ -1046,13 +1185,46 @@ export function useSetupWizard() {
} }
await api.post('system/setting/Notifications', [notification]) await api.post('system/setting/Notifications', [notification])
return true
} catch (error) { } catch (error) {
console.error('Save notification settings failed:', error) console.error('Save notification settings failed:', error)
$toast.error(t('setupWizard.saveNotificationSettingsFailed')) $toast.error(t('setupWizard.saveNotificationSettingsFailed'))
return false
} }
} else { } else {
// 没有选择通知时,清空现有配置 // 没有选择通知时,清空现有配置
console.log('No notification selected, skipping save') 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) console.error('Save rule sequences failed:', error)
} }
} }
return true
} catch (error) { } catch (error) {
console.error('Save preference settings failed:', error) console.error('Save preference settings failed:', error)
$toast.error(t('setupWizard.savePreferenceSettingsFailed')) $toast.error(t('setupWizard.savePreferenceSettingsFailed'))
return false
} }
} }
@@ -1115,12 +1289,32 @@ export function useSetupWizard() {
if (result.data.PROXY_HOST) { if (result.data.PROXY_HOST) {
wizardData.value.basic.proxyHost = 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) { if (result.data.GITHUB_TOKEN) {
wizardData.value.basic.githubToken = result.data.GITHUB_TOKEN wizardData.value.basic.githubToken = result.data.GITHUB_TOKEN
} }
if (result.data.SUPERUSER) { if (result.data.SUPERUSER) {
wizardData.value.basic.username = 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,则创建一个随机的 // 如果没有API Token,则创建一个随机的
if (!wizardData.value.basic.apiToken) { if (!wizardData.value.basic.apiToken) {
@@ -1234,6 +1428,7 @@ export function useSetupWizard() {
validateDownloaderFields, validateDownloaderFields,
validateMediaServerFields, validateMediaServerFields,
validateNotificationFields, validateNotificationFields,
validateAgentFields,
clearValidationErrors, clearValidationErrors,
testConnectivity, testConnectivity,
nextStep, nextStep,
+13
View File
@@ -3183,6 +3183,7 @@ export default {
saveMediaServerSettingsFailed: 'Failed to save media server settings', saveMediaServerSettingsFailed: 'Failed to save media server settings',
notificationSettingsSaved: 'Notification settings saved successfully', notificationSettingsSaved: 'Notification settings saved successfully',
saveNotificationSettingsFailed: 'Failed to save notification settings', saveNotificationSettingsFailed: 'Failed to save notification settings',
saveAgentSettingsFailed: 'Failed to save AI assistant settings',
preferenceSettingsSaved: 'Preference settings saved successfully', preferenceSettingsSaved: 'Preference settings saved successfully',
savePreferenceSettingsFailed: 'Failed to save preference settings', savePreferenceSettingsFailed: 'Failed to save preference settings',
passwordUpdateSuccess: 'Password updated successfully', passwordUpdateSuccess: 'Password updated successfully',
@@ -3268,6 +3269,18 @@ export default {
senderPassword: 'Sender Password', senderPassword: 'Sender Password',
receiverEmail: 'Receiver Email', 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: { preferences: {
title: 'Resource Preferences', title: 'Resource Preferences',
description: 'Set resource download preferences', description: 'Set resource download preferences',
+12 -4
View File
@@ -3147,6 +3147,7 @@ export default {
saveMediaServerSettingsFailed: '保存媒体服务器设置失败', saveMediaServerSettingsFailed: '保存媒体服务器设置失败',
notificationSettingsSaved: '通知设置保存成功', notificationSettingsSaved: '通知设置保存成功',
saveNotificationSettingsFailed: '保存通知设置失败', saveNotificationSettingsFailed: '保存通知设置失败',
saveAgentSettingsFailed: '保存智能助手设置失败',
preferenceSettingsSaved: '偏好设置保存成功', preferenceSettingsSaved: '偏好设置保存成功',
savePreferenceSettingsFailed: '保存偏好设置失败', savePreferenceSettingsFailed: '保存偏好设置失败',
passwordUpdateSuccess: '密码更新成功', passwordUpdateSuccess: '密码更新成功',
@@ -3231,6 +3232,17 @@ export default {
senderPassword: '发送密码', senderPassword: '发送密码',
receiverEmail: '接收邮箱', receiverEmail: '接收邮箱',
}, },
agent: {
title: '智能助手',
description: '配置 Agent 助手与 LLM 参数',
info: '智能助手配置说明',
infoDesc: '启用后可在消息会话中使用 Agent 能力,也可开启失败整理接管和智能推荐。',
providerRequired: 'LLM 提供商不能为空',
apiKeyRequired: 'LLM API 密钥不能为空',
modelRequired: 'LLM 模型名称不能为空',
maxContextTokensRequired: 'LLM 最大上下文 Token 数量必须大于 0',
recommendMaxItemsRequired: '智能推荐分析条目上限必须大于 0',
},
preferences: { preferences: {
title: '资源偏好', title: '资源偏好',
description: '设置资源下载偏好', 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.
+12
View File
@@ -3148,6 +3148,7 @@ export default {
saveMediaServerSettingsFailed: '保存媒體服務器設置失敗', saveMediaServerSettingsFailed: '保存媒體服務器設置失敗',
notificationSettingsSaved: '通知設置保存成功', notificationSettingsSaved: '通知設置保存成功',
saveNotificationSettingsFailed: '保存通知設置失敗', saveNotificationSettingsFailed: '保存通知設置失敗',
saveAgentSettingsFailed: '保存智能助手設置失敗',
preferenceSettingsSaved: '偏好設置保存成功', preferenceSettingsSaved: '偏好設置保存成功',
savePreferenceSettingsFailed: '保存偏好設置失敗', savePreferenceSettingsFailed: '保存偏好設置失敗',
passwordUpdateSuccess: '密碼更新成功', passwordUpdateSuccess: '密碼更新成功',
@@ -3232,6 +3233,17 @@ export default {
senderPassword: '發送密碼', senderPassword: '發送密碼',
receiverEmail: '接收信箱', receiverEmail: '接收信箱',
}, },
agent: {
title: '智能助手',
description: '配置 Agent 助手與 LLM 參數',
info: '智能助手配置說明',
infoDesc: '啟用後可在消息對話中使用 Agent 能力,也可開啟失敗整理接管與智能推薦。',
providerRequired: 'LLM 提供商不能為空',
apiKeyRequired: 'LLM API 密鑰不能為空',
modelRequired: 'LLM 模型名稱不能為空',
maxContextTokensRequired: 'LLM 最大上下文 Token 數量必須大於 0',
recommendMaxItemsRequired: '智能推薦分析條目上限必須大於 0',
},
preferences: { preferences: {
title: '資源偏好', title: '資源偏好',
description: '設定資源下載偏好', description: '設定資源下載偏好',
+7 -1
View File
@@ -8,6 +8,7 @@ import StorageSettingsStep from '@/views/setup/StorageSettingsStep.vue'
import DownloaderSettingsStep from '@/views/setup/DownloaderSettingsStep.vue' import DownloaderSettingsStep from '@/views/setup/DownloaderSettingsStep.vue'
import MediaServerSettingsStep from '@/views/setup/MediaServerSettingsStep.vue' import MediaServerSettingsStep from '@/views/setup/MediaServerSettingsStep.vue'
import NotificationSettingsStep from '@/views/setup/NotificationSettingsStep.vue' import NotificationSettingsStep from '@/views/setup/NotificationSettingsStep.vue'
import AgentSettingsStep from '@/views/setup/AgentSettingsStep.vue'
import PreferencesSettingsStep from '@/views/setup/PreferencesSettingsStep.vue' import PreferencesSettingsStep from '@/views/setup/PreferencesSettingsStep.vue'
import ConnectivityTest from '@/views/setup/ConnectivityTest.vue' import ConnectivityTest from '@/views/setup/ConnectivityTest.vue'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
@@ -121,8 +122,13 @@ onMounted(async () => {
<NotificationSettingsStep /> <NotificationSettingsStep />
</VStepperWindowItem> </VStepperWindowItem>
<!-- 步骤6资源偏好 --> <!-- 步骤6智能助手 -->
<VStepperWindowItem :value="6"> <VStepperWindowItem :value="6">
<AgentSettingsStep />
</VStepperWindowItem>
<!-- 步骤7资源偏好 -->
<VStepperWindowItem :value="7">
<PreferencesSettingsStep /> <PreferencesSettingsStep />
</VStepperWindowItem> </VStepperWindowItem>
</VStepperWindow> </VStepperWindow>
+262
View File
@@ -0,0 +1,262 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import api from '@/api'
import { useSetupWizard } from '@/composables/useSetupWizard'
const { t } = useI18n()
const { wizardData, validationErrors } = useSetupWizard()
const llmModels = ref<string[]>([])
const loadingModels = ref(false)
const providerItems = [
{ title: 'OpenAI', value: 'openai' },
{ title: 'Google', value: 'google' },
{ title: 'DeepSeek', value: 'deepseek' },
]
const jobIntervalItems = computed(() => [
{ title: t('setting.system.aiAgentJobIntervalDisabled'), value: 0 },
{ title: t('setting.system.aiAgentJobInterval1h'), value: 1 },
{ title: t('setting.system.aiAgentJobInterval3h'), value: 3 },
{ title: t('setting.system.aiAgentJobInterval6h'), value: 6 },
{ title: t('setting.system.aiAgentJobInterval12h'), value: 12 },
{ title: t('setting.system.aiAgentJobInterval24h'), value: 24 },
{ title: t('setting.system.aiAgentJobInterval1w'), value: 168 },
{ title: t('setting.system.aiAgentJobInterval1M'), value: 720 },
])
async function loadLlmModels() {
if (!wizardData.value.agent.provider || !wizardData.value.agent.apiKey) {
return
}
loadingModels.value = true
try {
const result: { [key: string]: any } = await api.get('system/llm-models', {
params: {
provider: wizardData.value.agent.provider,
api_key: wizardData.value.agent.apiKey,
base_url: wizardData.value.agent.baseUrl,
},
})
if (result.success) {
llmModels.value = result.data || []
if (!wizardData.value.agent.model && llmModels.value.length > 0) {
wizardData.value.agent.model = llmModels.value[0]
}
}
} catch (error) {
console.log('Load LLM models failed:', error)
} finally {
loadingModels.value = false
}
}
onMounted(() => {
if (wizardData.value.agent.enabled && wizardData.value.agent.apiKey) {
loadLlmModels()
}
})
</script>
<template>
<VCard variant="outlined">
<VCardText>
<div class="text-center mb-6">
<h3 class="text-h4 mb-2">{{ t('setupWizard.agent.title') }}</h3>
<p class="text-body-1 text-medium-emphasis">{{ t('setupWizard.agent.description') }}</p>
</div>
<VRow>
<VCol cols="12">
<VAlert type="info" variant="tonal" class="mb-4">
<VAlertTitle>{{ t('setupWizard.agent.info') }}</VAlertTitle>
{{ t('setupWizard.agent.infoDesc') }}
</VAlert>
</VCol>
<VCol cols="12">
<VSwitch
v-model="wizardData.agent.enabled"
:label="t('setting.system.aiAgentEnable')"
:hint="t('setting.system.aiAgentEnableHint')"
persistent-hint
color="primary"
/>
</VCol>
<template v-if="wizardData.agent.enabled">
<VCol cols="12" md="4">
<VSwitch
v-model="wizardData.agent.global"
:label="t('setting.system.aiAgentGlobal')"
:hint="t('setting.system.aiAgentGlobalHint')"
persistent-hint
color="primary"
/>
</VCol>
<VCol cols="12" md="4">
<VSwitch
v-model="wizardData.agent.verbose"
:label="t('setting.system.aiAgentVerbose')"
:hint="t('setting.system.aiAgentVerboseHint')"
persistent-hint
color="primary"
/>
</VCol>
<VCol cols="12" md="4">
<VSwitch
v-model="wizardData.agent.supportImageInput"
:label="t('setting.system.llmSupportImageInput')"
:hint="t('setting.system.llmSupportImageInputHint')"
persistent-hint
color="primary"
/>
</VCol>
<VCol cols="12" md="6">
<VSelect
v-model="wizardData.agent.provider"
:label="t('setting.system.llmProvider')"
:hint="t('setting.system.llmProviderHint')"
:items="providerItems"
:error="validationErrors.agent.provider"
:error-messages="validationErrors.agent.provider ? [t('setupWizard.agent.providerRequired')] : []"
persistent-hint
prepend-inner-icon="mdi-robot-outline"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="wizardData.agent.baseUrl"
:label="t('setting.system.llmBaseUrl')"
:hint="t('setting.system.llmBaseUrlHint')"
placeholder="https://api.deepseek.com"
persistent-hint
prepend-inner-icon="mdi-link-variant"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="wizardData.agent.apiKey"
:label="t('setting.system.llmApiKey')"
:hint="t('setting.system.llmApiKeyHint')"
:placeholder="t('setting.system.llmApiKeyPlaceholder')"
:error="validationErrors.agent.apiKey"
:error-messages="validationErrors.agent.apiKey ? [t('setupWizard.agent.apiKeyRequired')] : []"
persistent-hint
prepend-inner-icon="mdi-key-variant"
type="password"
/>
</VCol>
<VCol cols="12" md="6">
<VCombobox
v-model="wizardData.agent.model"
:label="t('setting.system.llmModel')"
:hint="t('setting.system.llmModelHint')"
:items="llmModels"
:loading="loadingModels"
:error="validationErrors.agent.model"
:error-messages="validationErrors.agent.model ? [t('setupWizard.agent.modelRequired')] : []"
persistent-hint
prepend-inner-icon="mdi-brain"
>
<template #append-inner>
<VBtn
variant="text"
icon="mdi-refresh"
size="small"
:disabled="!wizardData.agent.provider || !wizardData.agent.apiKey"
@click="loadLlmModels"
/>
</template>
</VCombobox>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model.number="wizardData.agent.maxContextTokens"
:label="t('setting.system.llmMaxContextTokens')"
:hint="t('setting.system.llmMaxContextTokensHint')"
:error="validationErrors.agent.maxContextTokens"
:error-messages="
validationErrors.agent.maxContextTokens ? [t('setupWizard.agent.maxContextTokensRequired')] : []
"
persistent-hint
prepend-inner-icon="mdi-counter"
type="number"
min="1"
/>
</VCol>
<VCol cols="12" md="6">
<VSelect
v-model="wizardData.agent.jobInterval"
:label="t('setting.system.aiAgentJobInterval')"
:hint="t('setting.system.aiAgentJobIntervalHint')"
:items="jobIntervalItems"
persistent-hint
prepend-inner-icon="mdi-timer-outline"
/>
</VCol>
<VCol cols="12">
<VSwitch
v-model="wizardData.agent.retryTransfer"
:label="t('setting.system.aiAgentRetryTransfer')"
:hint="t('setting.system.aiAgentRetryTransferHint')"
persistent-hint
color="primary"
/>
</VCol>
<VCol cols="12">
<VSwitch
v-model="wizardData.agent.recommendEnabled"
:label="t('setting.system.aiRecommendEnabled')"
:hint="t('setting.system.aiRecommendEnabledHint')"
persistent-hint
color="primary"
/>
</VCol>
<VCol v-if="wizardData.agent.recommendEnabled" cols="12" md="6">
<VTextarea
v-model="wizardData.agent.recommendUserPreference"
:label="t('setting.system.aiRecommendUserPreference')"
:hint="t('setting.system.aiRecommendUserPreferenceHint')"
persistent-hint
prepend-inner-icon="mdi-account-heart-outline"
rows="2"
auto-grow
/>
</VCol>
<VCol v-if="wizardData.agent.recommendEnabled" cols="12" md="6">
<VTextField
v-model.number="wizardData.agent.recommendMaxItems"
:label="t('setting.system.aiRecommendMaxItems')"
:hint="t('setting.system.aiRecommendMaxItemsHint')"
:error="validationErrors.agent.recommendMaxItems"
:error-messages="
validationErrors.agent.recommendMaxItems ? [t('setupWizard.agent.recommendMaxItemsRequired')] : []
"
persistent-hint
prepend-inner-icon="mdi-format-list-numbered"
type="number"
min="1"
/>
</VCol>
</template>
</VRow>
</VCardText>
</VCard>
</template>
+25
View File
@@ -37,6 +37,11 @@ const confirmPasswordErrorMessage = computed(() => {
return '' return ''
}) })
const recognizeSourceItems = [
{ title: 'TheMovieDb', value: 'themoviedb' },
{ title: '豆瓣', value: 'douban' },
]
// API Token验证 // API Token验证
const apiTokenError = computed(() => { const apiTokenError = computed(() => {
return !wizardData.value.basic.apiToken && hasErrors.value return !wizardData.value.basic.apiToken && hasErrors.value
@@ -119,6 +124,26 @@ const usernameErrorMessage = computed(() => {
clearable clearable
/> />
</VCol> </VCol>
<VCol cols="12" md="6">
<VSelect
v-model="wizardData.basic.recognizeSource"
:label="t('setupWizard.basic.recognizeSource')"
:hint="t('setupWizard.basic.recognizeSourceHint')"
:items="recognizeSourceItems"
persistent-hint
prepend-inner-icon="mdi-database-search"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="wizardData.basic.ocrHost"
:label="t('setting.system.ocrHost')"
:hint="t('setting.system.ocrHostHint')"
placeholder="https://movie-pilot.org"
persistent-hint
prepend-inner-icon="mdi-text-recognition"
/>
</VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<VTextField <VTextField
v-model="wizardData.basic.proxyHost" v-model="wizardData.basic.proxyHost"
@@ -345,6 +345,10 @@ const notificationTypes = [
v-model="wizardData.notification.config.QQ_APP_ID" v-model="wizardData.notification.config.QQ_APP_ID"
:label="t('notification.qqbot.appId')" :label="t('notification.qqbot.appId')"
:hint="t('notification.qqbot.appIdHint')" :hint="t('notification.qqbot.appIdHint')"
:error="validationErrors.notification.QQ_APP_ID"
:error-messages="
validationErrors.notification.QQ_APP_ID ? [t('notification.qqbot.appIdRequired')] : []
"
persistent-hint persistent-hint
prepend-inner-icon="mdi-application" prepend-inner-icon="mdi-application"
/> />
@@ -354,6 +358,12 @@ const notificationTypes = [
v-model="wizardData.notification.config.QQ_APP_SECRET" v-model="wizardData.notification.config.QQ_APP_SECRET"
:label="t('notification.qqbot.appSecret')" :label="t('notification.qqbot.appSecret')"
:hint="t('notification.qqbot.appSecretHint')" :hint="t('notification.qqbot.appSecretHint')"
:error="validationErrors.notification.QQ_APP_SECRET"
:error-messages="
validationErrors.notification.QQ_APP_SECRET
? [t('notification.qqbot.appSecretRequired')]
: []
"
persistent-hint persistent-hint
prepend-inner-icon="mdi-key" prepend-inner-icon="mdi-key"
/> />