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:34:17 +08:00
parent b40fc4bd30
commit b29c6bd83f
8 changed files with 557 additions and 26 deletions

View File

@@ -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,

View File

@@ -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',

View File

@@ -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.

View File

@@ -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: '設定資源下載偏好',

View File

@@ -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>

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>

View File

@@ -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"

View File

@@ -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"
/>