mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-17 13:47:34 +08:00
feat: add AI agent configuration step and expand basic settings with OCR and recognition source options
This commit is contained in:
@@ -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<WizardData>({
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
recognizeSource: 'themoviedb',
|
||||
ocrHost: '',
|
||||
proxyHost: '',
|
||||
githubToken: '',
|
||||
},
|
||||
@@ -133,6 +160,22 @@ const wizardData = ref<WizardData>({
|
||||
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<ValidationErrorState>({
|
||||
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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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: '設定資源下載偏好',
|
||||
|
||||
@@ -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 () => {
|
||||
<NotificationSettingsStep />
|
||||
</VStepperWindowItem>
|
||||
|
||||
<!-- 步骤6:资源偏好 -->
|
||||
<!-- 步骤6:智能助手 -->
|
||||
<VStepperWindowItem :value="6">
|
||||
<AgentSettingsStep />
|
||||
</VStepperWindowItem>
|
||||
|
||||
<!-- 步骤7:资源偏好 -->
|
||||
<VStepperWindowItem :value="7">
|
||||
<PreferencesSettingsStep />
|
||||
</VStepperWindowItem>
|
||||
</VStepperWindow>
|
||||
|
||||
262
src/views/setup/AgentSettingsStep.vue
Normal file
262
src/views/setup/AgentSettingsStep.vue
Normal 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>
|
||||
@@ -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
|
||||
/>
|
||||
</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">
|
||||
<VTextField
|
||||
v-model="wizardData.basic.proxyHost"
|
||||
|
||||
@@ -345,6 +345,10 @@ const notificationTypes = [
|
||||
v-model="wizardData.notification.config.QQ_APP_ID"
|
||||
:label="t('notification.qqbot.appId')"
|
||||
:hint="t('notification.qqbot.appIdHint')"
|
||||
:error="validationErrors.notification.QQ_APP_ID"
|
||||
:error-messages="
|
||||
validationErrors.notification.QQ_APP_ID ? [t('notification.qqbot.appIdRequired')] : []
|
||||
"
|
||||
persistent-hint
|
||||
prepend-inner-icon="mdi-application"
|
||||
/>
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user