Files
MoviePilot-Frontend/src/views/setting/AccountSettingSystem.vue
T

2841 lines
112 KiB
Vue

<!-- eslint-disable sonarjs/no-duplicate-string -->
<script lang="ts" setup>
import { useToast } from 'vue-toastification'
import api from '@/api'
import { manageLlmProvider } from '@/api/manage'
import { useGlobalSettingsStore } from '@/stores'
import { DownloaderConf, MediaServerConf } from '@/api/types'
import DownloaderCard from '@/components/cards/DownloaderCard.vue'
import MediaServerCard from '@/components/cards/MediaServerCard.vue'
import { copyToClipboard } from '@/@core/utils/navigator'
import { useI18n } from 'vue-i18n'
import { downloaderOptions, mediaServerOptions } from '@/api/constants'
import { useDisplay, useTheme } from 'vuetify'
import { useLlmProviderDirectory } from '@/composables/useLlmProviderDirectory'
import { useSilentSettingRefresh } from '@/composables/useSilentSettingRefresh'
import { openSharedDialog } from '@/composables/useSharedDialog'
const display = useDisplay()
const theme = useTheme()
const isTransparentTheme = computed(() => theme.name.value === 'transparent')
const globalSettingsStore = useGlobalSettingsStore()
// 国际化
const { t } = useI18n()
const props = defineProps({
active: {
type: Boolean,
default: true,
},
})
// 下载器/媒体服务器排序按需加载,降低系统设置页入口解析量。
const Draggable = defineAsyncComponent(() => import('vuedraggable').then(module => module.default))
const LlmProviderAuthDialog = defineAsyncComponent(() => import('@/components/dialog/LlmProviderAuthDialog.vue'))
const AgentMcpSettingsDialog = defineAsyncComponent(() => import('@/components/dialog/AgentMcpSettingsDialog.vue'))
// 系统设置项
const SystemSettings = ref<any>({
// 基础设置
Basic: {
DB_TYPE: 'sqlite',
APP_DOMAIN: null,
API_TOKEN: null,
WALLPAPER: 'tmdb',
GITHUB_TOKEN: null,
CUSTOMIZE_WALLPAPER_API_URL: null,
AI_AGENT_ENABLE: false,
AI_AGENT_GLOBAL: false,
AI_AGENT_HIDE_ENTRY: false,
AI_AGENT_VERBOSE: false,
AI_AGENT_JOB_INTERVAL: 24,
LLM_PROVIDER: 'deepseek',
LLM_MODEL: 'deepseek-chat',
LLM_THINKING_LEVEL: 'off',
LLM_API_PROTOCOL: 'auto',
LLM_WEB_SEARCH_MODE: 'local',
LLM_SUPPORT_IMAGE_INPUT: false,
LLM_SUPPORT_AUDIO_INPUT: false,
LLM_SUPPORT_AUDIO_OUTPUT: false,
LLM_API_KEY: null,
LLM_BASE_URL: 'https://api.deepseek.com',
LLM_USE_PROXY: true,
LLM_BASE_URL_PRESET: null,
LLM_MAX_CONTEXT_TOKENS: 128,
LLM_USER_AGENT: null,
LLM_TEMPERATURE: 0.3,
AUDIO_INPUT_PROVIDER: 'openai',
AUDIO_INPUT_API_KEY: null,
AUDIO_INPUT_BASE_URL: null,
AUDIO_INPUT_MODEL: 'gpt-4o-mini-transcribe',
AUDIO_INPUT_LANGUAGE: 'zh',
AUDIO_OUTPUT_PROVIDER: 'openai',
AUDIO_OUTPUT_API_KEY: null,
AUDIO_OUTPUT_BASE_URL: null,
AUDIO_OUTPUT_MODEL: 'gpt-4o-mini-tts',
AUDIO_OUTPUT_VOICE: 'alloy',
AUDIO_OUTPUT_INCLUDE_TEXT: false,
AI_AGENT_RETRY_TRANSFER: false,
AI_RECOMMEND_ENABLED: false,
AI_RECOMMEND_USER_PREFERENCE: null,
AI_RECOMMEND_MAX_ITEMS: 50,
},
// 高级系统设置
Advanced: {
// 全局
AUXILIARY_AUTH_ENABLE: false,
GLOBAL_IMAGE_CACHE: false,
SUBSCRIBE_STATISTIC_SHARE: true,
PLUGIN_STATISTIC_SHARE: true,
USAGE_STATISTIC_SHARE: true,
WORKFLOW_STATISTIC_SHARE: true,
BIG_MEMORY_MODE: false,
DB_WAL_ENABLE: false,
AUTO_UPDATE_RESOURCE: true,
MOVIEPILOT_AUTO_UPDATE: false,
DATA_CLEANUP_ENABLE: false,
DATA_CLEANUP_MESSAGE_DAYS: 90,
DATA_CLEANUP_DOWNLOAD_HISTORY_DAYS: 180,
DATA_CLEANUP_SITE_USERDATA_DAYS: 180,
DATA_CLEANUP_TRANSFER_HISTORY_DAYS: 365 * 3,
// 本地数据库备份策略同时适用于 SQLite 与 PostgreSQL。
DB_BACKUP_ENABLE: false,
DB_BACKUP_CRON: '0 3 * * *',
DB_BACKUP_PATH: null,
DB_BACKUP_RETENTION_DAYS: 30,
DB_BACKUP_MAX_COUNT: 30,
DB_BACKUP_ON_UPGRADE: true,
// 媒体
RECOGNIZE_PLUGIN_FIRST: false,
MEDIA_RECOGNIZE_SHARE: true,
TMDB_API_DOMAIN: null,
TMDB_API_KEY: null,
ACOUSTID_API_KEY: null,
MUSIC_METADATA_TO_SIMPLIFIED: true,
TMDB_IMAGE_DOMAIN: null,
MUSIC_COVER_PROXY: null,
TMDB_LOCALE: null,
META_CACHE_EXPIRE: 0,
SCRAP_FOLLOW_TMDB: true,
FANART_ENABLE: false,
FANART_LANG: 'zh,en',
TMDB_SCRAP_ORIGINAL_IMAGE: null,
// 网络
PROXY_HOST: null,
GITHUB_PROXY: null,
PIP_PROXY: null,
DOH_ENABLE: false,
DOH_RESOLVERS: null,
DOH_DOMAINS: null,
SECURITY_IMAGE_DOMAINS: [],
IMAGE_PROXY_ALLOWED_PRIVATE_RANGES: [],
// 日志
DEBUG: false,
LOG_LEVEL: 'INFO',
LOG_MAX_FILE_SIZE: '5',
LOG_BACKUP_COUNT: '3',
LOG_FILE_FORMAT: '【%(levelname)s】%(asctime)s - %(message)s',
// 实验室
PLUGIN_AUTO_RELOAD: false,
PLUGIN_LOCAL_REPO_PATHS: '',
RUST_ACCEL: false,
ENCODING_DETECTION_PERFORMANCE_MODE: true,
TRANSFER_THREADS: 1,
MONITOR_NETWORK_FAST_MODE: false,
TRANSFER_MAX_FAILED_RETRIES: 3,
FS_PROXY_ENABLED: true,
FS_PROXY_TIMEOUT: 30,
FS_PROXY_STALL_TIMEOUT: 120,
},
})
const audioProviderItems = computed(() => [
{ title: t('setting.system.audioProviderOpenAiAudio'), value: 'openai' },
{ title: t('setting.system.audioProviderChatAudio'), value: 'openai_chat_audio' },
{ title: t('setting.system.audioProviderMimo'), value: 'mimo' },
{ title: t('setting.system.audioProviderMinimax'), value: 'minimax' },
])
// 刮削配置
const scrapingConfig = [
{
section: 'movie',
items: [
{ key: 'movie_nfo', label: 'setting.system.movieNfo' },
{ key: 'movie_poster', label: 'setting.system.moviePoster' },
{ key: 'movie_backdrop', label: 'setting.system.movieBackdrop' },
{ key: 'movie_logo', label: 'setting.system.movieLogo' },
{ key: 'movie_disc', label: 'setting.system.movieDisc' },
{ key: 'movie_banner', label: 'setting.system.movieBanner' },
{ key: 'movie_thumb', label: 'setting.system.movieThumb' },
],
},
{
section: 'tv',
items: [
{ key: 'tv_nfo', label: 'setting.system.tvNfo' },
{ key: 'tv_poster', label: 'setting.system.tvPoster' },
{ key: 'tv_backdrop', label: 'setting.system.tvBackdrop' },
{ key: 'tv_banner', label: 'setting.system.tvBanner' },
{ key: 'tv_logo', label: 'setting.system.tvLogo' },
{ key: 'tv_thumb', label: 'setting.system.tvThumb' },
],
},
{
section: 'season',
items: [
{ key: 'season_nfo', label: 'setting.system.seasonNfo' },
{ key: 'season_poster', label: 'setting.system.seasonPoster' },
{ key: 'season_banner', label: 'setting.system.seasonBanner' },
{ key: 'season_thumb', label: 'setting.system.seasonThumb' },
],
},
{
section: 'episode',
items: [
{ key: 'episode_nfo', label: 'setting.system.episodeNfo' },
{ key: 'episode_thumb', label: 'setting.system.episodeThumb' },
],
},
{
section: 'music',
items: [
{ key: 'music_nfo', label: 'setting.system.musicNfo' },
{ key: 'music_poster', label: 'setting.system.musicPoster' },
{ key: 'music_lyrics', label: 'setting.system.musicLyrics' },
],
},
]
// 刮削策略设置
type ScrapingPolicy = 'skip' | 'missingOnly' | 'overwrite'
const ScrapingPolicies = ref<Record<string, ScrapingPolicy>>(
Object.fromEntries(scrapingConfig.flatMap(section => section.items.map(item => [item.key, 'missingOnly']))),
)
// 是否发送请求的总开关
const isRequest = ref(true)
// 选中的媒体服务器
const mediaServers = ref<MediaServerConf[]>([])
// 旧版全局媒体服务器同步间隔,仅用于未单独设置时的默认值提示
const legacyMediaServerSyncInterval = ref<number | null>(null)
// 下载器
const downloaders = ref<DownloaderConf[]>([])
// 提示框
const $toast = useToast()
// 高级设置对话框
const advancedDialog = ref(false)
const savingBasic = ref(false)
const testingLlm = ref(false)
const rustAccelAvailable = ref(false)
const agentMcpDialog = ref(false)
const agentMcpServers = ref<AgentMcpServer[]>([])
const loadingAgentMcpServers = ref(false)
// 智能助手配置项较多,默认收起以降低基础设置页的视觉占用。
const aiAgentSettingsCollapsed = ref(true)
type LlmSettingsSnapshot = {
AI_AGENT_ENABLE: boolean
LLM_PROVIDER: string
LLM_MODEL: string
LLM_THINKING_LEVEL: string
LLM_API_PROTOCOL: string
LLM_WEB_SEARCH_MODE: string
LLM_API_KEY: string
LLM_BASE_URL: string
LLM_USE_PROXY: boolean
LLM_BASE_URL_PRESET: string
LLM_USER_AGENT: string
LLM_TEMPERATURE: number
}
type AgentMcpTransport = 'stdio' | 'sse' | 'http' | 'streamable_http'
interface AgentMcpServer {
id: string
name: string
enabled: boolean
transport: AgentMcpTransport
description?: string | null
command?: string | null
args: string[]
env: Record<string, string>
url?: string | null
headers: Record<string, string>
timeout: number
tool_prefix?: string | null
require_admin: boolean
}
let llmTestRequestId = 0
let llmTestAbortController: AbortController | null = null
const llmProviderRef = computed({
get: () => String(SystemSettings.value.Basic.LLM_PROVIDER ?? ''),
set: value => {
SystemSettings.value.Basic.LLM_PROVIDER = value || ''
},
})
const llmApiKeyRef = computed({
get: () => String(SystemSettings.value.Basic.LLM_API_KEY ?? ''),
set: value => {
SystemSettings.value.Basic.LLM_API_KEY = value || ''
},
})
const llmBaseUrlRef = computed({
get: () => String(SystemSettings.value.Basic.LLM_BASE_URL ?? ''),
set: value => {
SystemSettings.value.Basic.LLM_BASE_URL = value || ''
},
})
const llmBaseUrlPresetRef = computed({
get: () => String(SystemSettings.value.Basic.LLM_BASE_URL_PRESET ?? ''),
set: value => {
SystemSettings.value.Basic.LLM_BASE_URL_PRESET = value || ''
},
})
const llmUseProxyRef = computed({
get: () => Boolean(SystemSettings.value.Basic.LLM_USE_PROXY),
set: value => {
SystemSettings.value.Basic.LLM_USE_PROXY = Boolean(value)
},
})
const llmUserAgentRef = computed({
get: () => String(SystemSettings.value.Basic.LLM_USER_AGENT ?? ''),
set: value => {
SystemSettings.value.Basic.LLM_USER_AGENT = value || ''
},
})
const llmApiProtocolRef = computed({
get: () => String(SystemSettings.value.Basic.LLM_API_PROTOCOL ?? 'auto'),
set: value => {
SystemSettings.value.Basic.LLM_API_PROTOCOL = value || 'auto'
},
})
const llmModelRef = computed({
get: () => String(SystemSettings.value.Basic.LLM_MODEL ?? ''),
set: value => {
SystemSettings.value.Basic.LLM_MODEL = value || ''
},
})
const llmMaxContextRef = computed({
get: () => Number(SystemSettings.value.Basic.LLM_MAX_CONTEXT_TOKENS ?? 0),
set: value => {
SystemSettings.value.Basic.LLM_MAX_CONTEXT_TOKENS = value || 0
},
})
const {
providerItems: llmProviderItems,
baseUrlPresetItems: llmBaseUrlPresetItems,
models: llmModels,
selectedProvider: selectedLlmProvider,
selectedModel: selectedLlmModel,
loadingProviders: loadingLlmProviders,
loadingModels,
providerConnected,
showBaseUrlField,
showApiKeyField,
showApiProtocolField: showLlmApiProtocolField,
supportsBuiltinWebSearch,
canRefreshModels,
setBaseUrlPreset,
authDialogVisible,
authPolling,
authPopupBlocked,
authSession,
handleProviderSelection,
applyModelMetadata,
loadProviders: loadLlmProviders,
loadModels: loadLlmModels,
openAuthPage,
startAuth: startLlmProviderAuth,
pollAuthSession,
disconnectAuth: disconnectLlmProviderAuth,
closeAuthDialog,
} = useLlmProviderDirectory({
provider: llmProviderRef,
apiKey: llmApiKeyRef,
baseUrl: llmBaseUrlRef,
baseUrlPreset: llmBaseUrlPresetRef,
useProxy: llmUseProxyRef,
userAgent: llmUserAgentRef,
apiProtocol: llmApiProtocolRef,
model: llmModelRef,
maxContextTokens: llmMaxContextRef,
})
let authDialogController: ReturnType<typeof openSharedDialog> | null = null
// 生成 LLM 授权共享弹窗所需的最新状态。
function getProviderAuthDialogProps() {
return {
authSession: authSession.value,
polling: authPolling.value,
popupBlocked: authPopupBlocked.value,
}
}
// 打开或刷新 LLM 授权共享弹窗。
function openProviderAuthDialog() {
const dialogProps = getProviderAuthDialogProps()
if (authDialogController) {
authDialogController.updateProps(dialogProps)
return
}
authDialogController = openSharedDialog(
LlmProviderAuthDialog,
dialogProps,
{
close: () => {
closeAuthDialog()
authDialogController = null
},
openAuthPage,
poll: () => {
void pollAuthSession()
},
'update:modelValue': (value: boolean) => {
if (!value) {
closeAuthDialog()
authDialogController = null
}
},
},
{ closeOn: ['close', 'update:modelValue'] },
)
}
// 关闭 LLM 授权共享弹窗控制器。
function closeProviderAuthDialog() {
authDialogController?.close()
authDialogController = null
}
function buildLlmSnapshot(): LlmSettingsSnapshot {
return {
AI_AGENT_ENABLE: Boolean(SystemSettings.value.Basic.AI_AGENT_ENABLE),
LLM_PROVIDER: String(SystemSettings.value.Basic.LLM_PROVIDER ?? ''),
LLM_MODEL: String(SystemSettings.value.Basic.LLM_MODEL ?? ''),
LLM_THINKING_LEVEL: String(SystemSettings.value.Basic.LLM_THINKING_LEVEL ?? 'off'),
LLM_API_PROTOCOL: String(SystemSettings.value.Basic.LLM_API_PROTOCOL ?? 'auto'),
LLM_WEB_SEARCH_MODE: String(SystemSettings.value.Basic.LLM_WEB_SEARCH_MODE ?? 'local'),
LLM_API_KEY: String(SystemSettings.value.Basic.LLM_API_KEY ?? ''),
LLM_BASE_URL: String(SystemSettings.value.Basic.LLM_BASE_URL ?? ''),
LLM_USE_PROXY: Boolean(SystemSettings.value.Basic.LLM_USE_PROXY),
LLM_BASE_URL_PRESET: String(SystemSettings.value.Basic.LLM_BASE_URL_PRESET ?? ''),
LLM_USER_AGENT: String(SystemSettings.value.Basic.LLM_USER_AGENT ?? ''),
LLM_TEMPERATURE: Number(SystemSettings.value.Basic.LLM_TEMPERATURE ?? 0.3),
}
}
function buildLlmSnapshotKey(snapshot: LlmSettingsSnapshot) {
return JSON.stringify(snapshot)
}
function buildLlmTestPayload(snapshot: LlmSettingsSnapshot) {
return {
enabled: snapshot.AI_AGENT_ENABLE,
provider: snapshot.LLM_PROVIDER.trim(),
model: snapshot.LLM_MODEL.trim(),
thinking_level: snapshot.LLM_THINKING_LEVEL.trim(),
api_protocol: snapshot.LLM_API_PROTOCOL.trim() || 'auto',
web_search_mode: snapshot.LLM_WEB_SEARCH_MODE.trim() || 'local',
api_key: snapshot.LLM_API_KEY.trim(),
base_url: snapshot.LLM_BASE_URL.trim(),
use_proxy: snapshot.LLM_USE_PROXY,
base_url_preset: snapshot.LLM_BASE_URL_PRESET.trim(),
user_agent: snapshot.LLM_USER_AGENT.trim(),
temperature: Number.isFinite(snapshot.LLM_TEMPERATURE) ? snapshot.LLM_TEMPERATURE : 0.3,
}
}
function normalizeThinkingLevelValue(value?: unknown) {
const normalized = String(value ?? '')
.trim()
.toLowerCase()
if (!normalized) return ''
const aliasMap: Record<string, string> = {
none: 'off',
disabled: 'off',
disable: 'off',
enabled: 'auto',
enable: 'auto',
default: 'auto',
dynamic: 'auto',
}
return aliasMap[normalized] || normalized
}
function resolveThinkingLevelValue(data?: Record<string, unknown>) {
const explicit = normalizeThinkingLevelValue(data?.LLM_THINKING_LEVEL)
if (explicit) return explicit
const legacyEffort = normalizeThinkingLevelValue(data?.LLM_REASONING_EFFORT)
if (data?.LLM_DISABLE_THINKING === true) return 'off'
if (data?.LLM_DISABLE_THINKING === false) return legacyEffort || 'auto'
return legacyEffort || 'off'
}
function showLlmTestFailedToast(message?: string) {
const normalizedMessage = String(message ?? '').trim()
if (normalizedMessage) {
$toast.error(t('setting.system.llmTestFailedToastWithMessage', { message: normalizedMessage }))
return
}
$toast.error(t('setting.system.llmTestFailedToast'))
}
function invalidateLlmTestState() {
llmTestRequestId += 1
if (llmTestAbortController) {
llmTestAbortController.abort()
llmTestAbortController = null
}
testingLlm.value = false
}
const currentLlmSnapshot = computed(() => buildLlmSnapshot())
const currentLlmSnapshotKey = computed(() => buildLlmSnapshotKey(currentLlmSnapshot.value))
const llmProviderAuthMethods = computed(() => selectedLlmProvider.value?.oauth_methods || [])
const llmProviderAuthLabel = computed(() => selectedLlmProvider.value?.auth_status?.label || '')
const selectedLlmModelInfo = computed(() => {
if (!selectedLlmModel.value?.context_tokens_k) return ''
return t('setting.system.llmModelResolvedHint', {
context: selectedLlmModel.value.context_tokens_k,
source: selectedLlmModel.value.source || 'models.dev',
})
})
const agentMcpEnabledCount = computed(() => agentMcpServers.value.filter(server => server.enabled).length)
const agentMcpServerPreview = computed(() => agentMcpServers.value.slice(0, 3))
const canTestLlm = computed(() => {
const snapshot = currentLlmSnapshot.value
return (
snapshot.AI_AGENT_ENABLE &&
Boolean(snapshot.LLM_PROVIDER.trim()) &&
(Boolean(snapshot.LLM_API_KEY.trim()) || providerConnected.value) &&
Boolean(snapshot.LLM_MODEL.trim()) &&
!savingBasic.value &&
!testingLlm.value
)
})
const rustAccelHint = computed(() =>
rustAccelAvailable.value ? t('setting.system.rustAccelHint') : t('setting.system.rustAccelUnavailableHint'),
)
const thinkingLevelItems = computed(() => [
{ title: t('setting.system.llmThinkingLevelOff'), value: 'off' },
{ title: t('setting.system.llmThinkingLevelAuto'), value: 'auto' },
{ title: t('setting.system.llmThinkingLevelMinimal'), value: 'minimal' },
{ title: t('setting.system.llmThinkingLevelLow'), value: 'low' },
{ title: t('setting.system.llmThinkingLevelMedium'), value: 'medium' },
{ title: t('setting.system.llmThinkingLevelHigh'), value: 'high' },
{ title: t('setting.system.llmThinkingLevelMax'), value: 'max' },
{ title: t('setting.system.llmThinkingLevelXhigh'), value: 'xhigh' },
])
const apiProtocolItems = computed(() => [
{ title: t('setting.system.llmApiProtocolAuto'), value: 'auto' },
{ title: t('setting.system.llmApiProtocolChatCompletions'), value: 'chat_completions' },
{ title: t('setting.system.llmApiProtocolResponses'), value: 'responses' },
])
const webSearchModeItems = computed(() => [
{ title: t('setting.system.llmWebSearchModeLocal'), value: 'local' },
{
title: t('setting.system.llmWebSearchModeBuiltin'),
value: 'builtin',
disabled: !supportsBuiltinWebSearch.value,
},
{ title: t('setting.system.llmWebSearchModeAuto'), value: 'auto' },
{ title: t('setting.system.llmWebSearchModeDisabled'), value: 'disabled' },
])
const webSearchModeHint = computed(() =>
supportsBuiltinWebSearch.value
? t('setting.system.llmWebSearchModeBuiltinSupportedHint')
: t('setting.system.llmWebSearchModeHint'),
)
const activeTab = ref('system')
// 元数据语言
const tmdbLanguageItems = [
{ title: t('setting.system.tmdbLanguage.zhCN'), value: 'zh' },
{ title: t('setting.system.tmdbLanguage.zhTW'), value: 'zh-TW' },
{ title: t('setting.system.tmdbLanguage.en'), value: 'en' },
]
// Fanart语言选项
const fanartLanguageItems = [
{ title: t('setting.system.fanartLanguage.zh'), value: 'zh' },
{ title: t('setting.system.fanartLanguage.en'), value: 'en' },
{ title: t('setting.system.fanartLanguage.ja'), value: 'ja' },
{ title: t('setting.system.fanartLanguage.ko'), value: 'ko' },
{ title: t('setting.system.fanartLanguage.de'), value: 'de' },
{ title: t('setting.system.fanartLanguage.fr'), value: 'fr' },
{ title: t('setting.system.fanartLanguage.es'), value: 'es' },
{ title: t('setting.system.fanartLanguage.it'), value: 'it' },
{ title: t('setting.system.fanartLanguage.pt'), value: 'pt' },
{ title: t('setting.system.fanartLanguage.ru'), value: 'ru' },
]
// 日志等级
const logLevelItems = [
{ title: t('setting.system.logLevelItems.debug'), value: 'DEBUG' },
{ title: t('setting.system.logLevelItems.info'), value: 'INFO' },
{ title: t('setting.system.logLevelItems.warning'), value: 'WARNING' },
{ title: t('setting.system.logLevelItems.error'), value: 'ERROR' },
{ title: t('setting.system.logLevelItems.critical'), value: 'CRITICAL' },
]
const dataCleanupFieldRules = [
(value: unknown) => value === 0 || !!value || t('setting.system.dataCleanupDaysRequired'),
(value: unknown) => Number(value) >= 0 || t('setting.system.dataCleanupDaysMin'),
]
const dbBackupPath = computed({
get: () => String(SystemSettings.value.Advanced.DB_BACKUP_PATH ?? ''),
set: (value: string) => {
SystemSettings.value.Advanced.DB_BACKUP_PATH = value
},
})
function hasValidCronFieldCount(value: string) {
return value.trim().split(/\s+/).length === 5
}
function isNonNegativeInteger(value: unknown) {
if (value === '' || value === null || value === undefined) return false
const numberValue = Number(value)
return Number.isInteger(numberValue) && numberValue >= 0
}
const dbBackupCronRules = [
(value: unknown) => {
const cron = String(value ?? '').trim()
return !cron || hasValidCronFieldCount(cron) || t('setting.system.dbBackupCronInvalid')
},
]
const dbBackupRetentionRules = [
(value: unknown) => isNonNegativeInteger(value) || t('setting.system.dbBackupRetentionDaysInvalid'),
]
const dbBackupMaxCountRules = [
(value: unknown) => isNonNegativeInteger(value) || t('setting.system.dbBackupMaxCountInvalid'),
]
// 安全域名添加变量
const newSecurityDomain = ref('')
// 图片代理允许非公网网段添加变量
const newImageProxyAllowedPrivateRange = ref('')
// 加载 LLM 模型列表与 provider 目录
async function refreshLlmModels(forceRefresh = true) {
try {
await loadLlmModels(forceRefresh)
} catch (error) {
$toast.error(error instanceof Error ? error.message : String(error))
console.log(error)
}
}
async function handleLlmProviderChanged() {
handleProviderSelection(true)
if (canRefreshModels.value) {
await refreshLlmModels(false)
}
}
function handleLlmModelChanged() {
applyModelMetadata()
}
async function startProviderAuth(methodId: string) {
try {
await startLlmProviderAuth(methodId)
} catch (error) {
$toast.error(error instanceof Error ? error.message : String(error))
}
}
async function disconnectProviderAuth() {
try {
await disconnectLlmProviderAuth()
$toast.success(t('setting.system.llmProviderDisconnected'))
} catch (error) {
$toast.error(error instanceof Error ? error.message : String(error))
}
}
// 添加安全域名
function addSecurityDomain() {
if (
newSecurityDomain.value &&
!SystemSettings.value.Advanced.SECURITY_IMAGE_DOMAINS.includes(newSecurityDomain.value)
) {
SystemSettings.value.Advanced.SECURITY_IMAGE_DOMAINS.push(newSecurityDomain.value)
newSecurityDomain.value = ''
}
}
// 添加图片代理允许访问的非公网网段
function addImageProxyAllowedPrivateRange() {
if (
newImageProxyAllowedPrivateRange.value &&
!SystemSettings.value.Advanced.IMAGE_PROXY_ALLOWED_PRIVATE_RANGES.includes(newImageProxyAllowedPrivateRange.value)
) {
SystemSettings.value.Advanced.IMAGE_PROXY_ALLOWED_PRIVATE_RANGES.push(newImageProxyAllowedPrivateRange.value)
newImageProxyAllowedPrivateRange.value = ''
}
}
// 调用API查询下载器设置
async function loadDownloaderSetting() {
try {
const result = await api.get<{ value?: DownloaderConf[] }>('system/setting/Downloaders')
downloaders.value = result.value ?? []
} catch (error) {
console.log(error)
}
}
// 调用API保存下载器设置
async function saveDownloaderSetting() {
try {
// 提取启用的下载器
const enabledDownloaders = downloaders.value.filter(item => item.enabled)
// 有启动的下载器时
if (enabledDownloaders.length > 0) {
downloaders.value = handleDefaultDownloaders(enabledDownloaders, downloaders.value)
}
await api.post('system/setting/Downloaders', downloaders.value, { feedback: 'silent' })
$toast.success(t('setting.system.downloaderSaveSuccess'))
await loadDownloaderSetting()
} catch (error) {
console.log(error)
$toast.error(t('setting.system.downloaderSaveFailed'))
}
}
// 处理默认下载器状态
function handleDefaultDownloaders(enabledDownloaders: DownloaderConf[], currentDownloaders: DownloaderConf[]) {
const enabledDefaultDownloader = enabledDownloaders.find(item => item.default)
if (enabledDownloaders.length > 0 && !enabledDefaultDownloader) {
return currentDownloaders.map(item => {
if (item === enabledDownloaders[0]) {
$toast.info(t('setting.system.defaultDownloaderNotice', { name: item.name }))
return { ...item, default: true }
}
// 清除其他下载器的默认下载器状态
return { ...item, default: false }
})
}
return currentDownloaders
}
// 调用API查询媒体服务器设置
async function loadMediaServerSetting() {
try {
const result = await api.get<{ value?: MediaServerConf[] }>('system/setting/MediaServers')
mediaServers.value = result.value ?? []
} catch (error) {
console.log(error)
}
}
// 调用API保存媒体服务器设置
async function saveMediaServerSetting() {
try {
await api.post('system/setting/MediaServers', mediaServers.value, { feedback: 'silent' })
$toast.success(t('setting.system.mediaServerSaveSuccess'))
await loadMediaServerSetting()
} catch (error) {
console.log(error)
$toast.error(t('setting.system.mediaServerSaveFailed'))
}
}
// 加载系统设置
async function loadSystemSettings() {
invalidateLlmTestState()
try {
const result = await api.get<Record<string, unknown>>('system/env')
const defaultSyncInterval = Number(result.MEDIASERVER_SYNC_INTERVAL ?? Number.NaN)
legacyMediaServerSyncInterval.value = Number.isFinite(defaultSyncInterval) ? defaultSyncInterval : null
// 将API返回的值赋值给SystemSettings
for (const sectionKey of Object.keys(SystemSettings.value) as Array<keyof typeof SystemSettings.value>) {
Object.keys(SystemSettings.value[sectionKey]).forEach((key: string) => {
if (Object.prototype.hasOwnProperty.call(result, key))
(SystemSettings.value[sectionKey] as Record<string, unknown>)[key] = result[key]
})
}
const accelAvailable = Boolean(result.RUST_ACCEL_AVAILABLE ?? result.RUST_ACCEL_ENABLED)
rustAccelAvailable.value = accelAvailable
if (!accelAvailable) SystemSettings.value.Advanced.RUST_ACCEL = false
SystemSettings.value.Basic.LLM_THINKING_LEVEL = resolveThinkingLevelValue(result)
await loadLlmProviders()
} catch (error) {
console.log(error)
}
await loadAgentMcpServers()
}
async function loadAgentMcpServers() {
loadingAgentMcpServers.value = true
try {
const result = await api.get<{ servers?: AgentMcpServer[] }>('message/agent/mcp/servers')
agentMcpServers.value = result.servers || []
} catch (error) {
console.log(error)
} finally {
loadingAgentMcpServers.value = false
}
}
function handleAgentMcpSaved(servers: AgentMcpServer[]) {
agentMcpServers.value = servers
}
// 调用API保存设置
async function saveSystemSetting(value: Record<string, unknown>) {
try {
await api.post('system/env', value, { feedback: 'silent' })
return true
} catch (error) {
console.log(error)
}
return false
}
// 保存基础设置
async function saveBasicSettings() {
savingBasic.value = true
try {
const llmTemperature = Number(SystemSettings.value.Basic.LLM_TEMPERATURE ?? 0.3)
SystemSettings.value.Basic.LLM_TEMPERATURE = Number.isFinite(llmTemperature) ? llmTemperature : 0.3
if (await saveSystemSetting(SystemSettings.value.Basic)) {
// 更新全局设置store,使Web Agent图标实时生效
globalSettingsStore.setData({ ...globalSettingsStore.getData, ...SystemSettings.value.Basic })
$toast.success(t('setting.system.basicSaveSuccess'))
} else {
$toast.error(t('setting.system.saveFailed', { message: t('common.apiRequestFailed') }))
}
} finally {
savingBasic.value = false
}
}
async function testLlmConnection() {
if (!canTestLlm.value) return
const snapshot = buildLlmSnapshot()
const snapshotKey = buildLlmSnapshotKey(snapshot)
const { provider: testProvider, ...testParams } = buildLlmTestPayload(snapshot)
const requestId = ++llmTestRequestId
if (llmTestAbortController) llmTestAbortController.abort()
const abortController = new AbortController()
llmTestAbortController = abortController
testingLlm.value = true
try {
await manageLlmProvider(testProvider, 'test', testParams, {
feedback: 'silent',
signal: abortController.signal,
})
if (
requestId !== llmTestRequestId ||
abortController.signal.aborted ||
currentLlmSnapshotKey.value !== snapshotKey
) {
return
}
$toast.success(t('setting.system.llmTestSuccessToast'))
} catch (error) {
if (
requestId !== llmTestRequestId ||
abortController.signal.aborted ||
currentLlmSnapshotKey.value !== snapshotKey
) {
return
}
showLlmTestFailedToast(error instanceof Error ? error.message : String(error))
console.log(error)
} finally {
if (requestId === llmTestRequestId) {
if (llmTestAbortController === abortController) llmTestAbortController = null
testingLlm.value = false
}
}
}
// 保存高级设置
async function saveAdvancedSettings() {
if (!normalizeDbBackupSettings()) return
if (!rustAccelAvailable.value) SystemSettings.value.Advanced.RUST_ACCEL = false
cleanEmptyFields(SystemSettings.value.Advanced, ['LOG_FILE_FORMAT'])
// 同时保存高级设置和刮削开关设置
const advancedResult = await saveSystemSetting(SystemSettings.value.Advanced)
const scrapingResult = await saveScrapingSwitchs()
if (!advancedResult) {
$toast.error(t('setting.system.saveFailed', { message: t('common.apiRequestFailed') }))
}
if (advancedResult && scrapingResult) {
advancedDialog.value = false
$toast.success(t('setting.system.advancedSaveSuccess'))
}
}
/** 规范化备份策略,并在请求前阻止后端无法执行的配置。 */
function normalizeDbBackupSettings() {
const settings = SystemSettings.value.Advanced
const path = String(settings.DB_BACKUP_PATH ?? '').trim()
settings.DB_BACKUP_PATH = path || null
const cron = String(settings.DB_BACKUP_CRON ?? '').trim()
const retentionDays = Number(settings.DB_BACKUP_RETENTION_DAYS)
const maxCount = Number(settings.DB_BACKUP_MAX_COUNT)
if (cron && !hasValidCronFieldCount(cron)) {
$toast.error(t('setting.system.dbBackupCronInvalid'))
return false
}
if (!isNonNegativeInteger(settings.DB_BACKUP_RETENTION_DAYS)) {
$toast.error(t('setting.system.dbBackupRetentionDaysInvalid'))
return false
}
if (!isNonNegativeInteger(settings.DB_BACKUP_MAX_COUNT)) {
$toast.error(t('setting.system.dbBackupMaxCountInvalid'))
return false
}
settings.DB_BACKUP_CRON = cron
settings.DB_BACKUP_RETENTION_DAYS = retentionDays
settings.DB_BACKUP_MAX_COUNT = maxCount
return true
}
// 当字段为空时,将其设置为 null 提交,以便后端恢复为默认值
function cleanEmptyFields(settings: Record<string, unknown>, fields: string[]) {
fields.forEach(field => {
const value = settings[field]
if (typeof value === 'string' && value.trim() === '') {
settings[field] = null
}
})
}
// 快捷复制到剪贴板
async function copyValue(value: string) {
try {
const success = copyToClipboard(value)
if (await success) $toast.success(t('setting.system.copySuccess'))
else $toast.error(t('setting.system.copyFailed'))
} catch (error) {
$toast.error(t('setting.system.copyError'))
console.log(error)
}
}
// 登录首页壁纸来源
const wallpaperItems = [
{ title: t('setting.system.wallpaperItems.tmdb'), value: 'tmdb' },
{ title: t('setting.system.wallpaperItems.bing'), value: 'bing' },
{ title: t('setting.system.wallpaperItems.mediaserver'), value: 'mediaserver' },
{ title: t('setting.system.wallpaperItems.customize'), value: 'customize' },
{ title: t('setting.system.wallpaperItems.none'), value: '' },
]
// 预设部分Github加速站
const githubMirrorsItems: string[] = [
// str: 'https://mirror.ghproxy.com/', // GitHub Proxy
// str: 'https://ghp.ci/', // GitHub Proxy 子站
]
// 预设部分PIP镜像站
const pipMirrorsItems = [
'https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple', // 清华大学
'https://pypi.mirrors.ustc.edu.cn/simple', // 中国科技大学
'https://mirrors.pku.edu.cn/pypi/web/simple', // 北京大学
'https://mirrors.aliyun.com/pypi/simple', // 阿里云
'https://mirrors.cloud.tencent.com/pypi/simple', // 腾讯云
'https://mirrors.163.com/pypi/simple', // 网易云
'https://pypi.doubanio.com/simple', // 豆瓣
'https://mirrors.hust.edu.cn/pypi/web/simple', // 华中理工大学
'https://mirrors.bfsu.edu.cn/pypi/web/simple', // 北京外国语大学
]
// Github加速代理显示处理
const githubProxyDisplay = computed({
get: () => {
return SystemSettings.value.Advanced.GITHUB_PROXY || null
},
set: val => {
SystemSettings.value.Advanced.GITHUB_PROXY = val === null ? '' : val
},
})
// PIP加速代理显示处理
const pipProxyDisplay = computed({
get: () => {
return SystemSettings.value.Advanced.PIP_PROXY || null
},
set: val => {
SystemSettings.value.Advanced.PIP_PROXY = val === null ? '' : val
},
})
// 创建随机字符串
function createRandomString() {
const charset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'
const array = new Uint8Array(32)
window.crypto.getRandomValues(array)
SystemSettings.value.Basic.API_TOKEN = Array.from(array, byte => charset[byte % charset.length]).join('')
}
// 添加下载器
function addDownloader(downloader: string) {
let name = `下载器${downloaders.value.length + 1}`
while (downloaders.value.some(item => item.name === name)) {
name = `下载器${parseInt(name.split('下载器')[1]) + 1}`
}
downloaders.value.push({
name: name,
type: downloader,
default: false,
enabled: false,
config: {},
})
}
// 删除下载器
function removeDownloader(ele: DownloaderConf) {
const index = downloaders.value.indexOf(ele)
downloaders.value.splice(index, 1)
}
// 下载器变化
function onDownloaderChange(downloader: DownloaderConf, name: string) {
const index = downloaders.value.findIndex(item => item.name === name)
if (index !== -1) downloaders.value[index] = downloader
}
// 添加媒体服务器
function addMediaServer(mediaserver: string) {
let name = `服务器${mediaServers.value.length + 1}`
while (mediaServers.value.some(item => item.name === name)) {
name = `服务器${parseInt(name.split('服务器')[1]) + 1}`
}
mediaServers.value.push({
name: name,
type: mediaserver,
enabled: false,
config: {},
})
}
// 删除媒体服务器
function removeMediaServer(ele: MediaServerConf) {
const index = mediaServers.value.indexOf(ele)
if (index !== -1) mediaServers.value.splice(index, 1)
}
// 变更媒体服务器
function onMediaServerChange(mediaserver: MediaServerConf, name: string) {
const index = mediaServers.value.findIndex(item => item.name === name)
if (index !== -1) mediaServers.value[index] = mediaserver
}
// 添加计算属性
const moviePilotAutoUpdate = computed({
get: () => {
return ['release', 'dev'].includes(SystemSettings.value.Advanced.MOVIEPILOT_AUTO_UPDATE)
},
set: val => {
SystemSettings.value.Advanced.MOVIEPILOT_AUTO_UPDATE = val ? 'release' : 'false'
},
})
// Fanart语言多选处理
const fanartLanguageSelection = computed({
get: () => {
if (!SystemSettings.value.Advanced.FANART_LANG) return []
return (SystemSettings.value.Advanced.FANART_LANG.split(',') as string[]).filter(Boolean).map(lang => lang.trim())
},
set: (val: string[]) => {
SystemSettings.value.Advanced.FANART_LANG = val.join(',')
},
})
// 加载刮削开关设置
async function loadScrapingSwitchs() {
try {
const result = await api.get<{ value?: Record<string, boolean | ScrapingPolicy> }>('system/setting/ScrapingSwitchs')
if (result.value) {
const loadedSwitches = Object.fromEntries(
Object.entries(result.value).map(([key, value]) => [
key,
typeof value === 'boolean' ? (value ? 'missingOnly' : 'skip') : value,
]),
) as Record<string, ScrapingPolicy>
ScrapingPolicies.value = { ...ScrapingPolicies.value, ...loadedSwitches }
}
} catch (error) {
console.log(error)
}
}
// 保存刮削开关设置
async function saveScrapingSwitchs() {
try {
await api.post('system/setting/ScrapingSwitchs', ScrapingPolicies.value, { feedback: 'silent' })
return true
} catch (error) {
console.log(error)
$toast.error(t('setting.system.scrapingSwitchSaveError'))
return false
}
}
// 加载数据
async function loadPageData() {
await Promise.all([loadDownloaderSetting(), loadMediaServerSetting(), loadSystemSettings(), loadScrapingSwitchs()])
}
onMounted(loadPageData)
onActivated(async () => {
isRequest.value = true
})
onDeactivated(() => {
isRequest.value = false
})
onBeforeUnmount(() => {
invalidateLlmTestState()
})
watch(authDialogVisible, visible => {
if (visible) {
openProviderAuthDialog()
return
}
closeProviderAuthDialog()
})
watch([authSession, authPolling, authPopupBlocked], () => {
authDialogController?.updateProps(getProviderAuthDialogProps())
})
useSilentSettingRefresh(
async () => {
if (advancedDialog.value || testingLlm.value || savingBasic.value) return
await loadPageData()
},
{
active: computed(() => props.active),
},
)
watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
if (snapshotKey !== previousSnapshotKey) invalidateLlmTestState()
})
</script>
<template>
<VRow>
<VCol cols="12">
<VCard>
<VCardItem>
<VCardTitle>{{ t('setting.system.basicSettings') }}</VCardTitle>
<VCardSubtitle>{{ t('setting.system.basicSettingsDesc') }}</VCardSubtitle>
</VCardItem>
<VCardText>
<VForm @submit.prevent="() => {}">
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.Basic.APP_DOMAIN"
:label="t('setting.system.appDomain')"
:hint="t('setting.system.appDomainHint')"
placeholder="http://localhost:3000"
persistent-hint
prepend-inner-icon="mdi-web"
/>
</VCol>
<VCol cols="12" md="6">
<VRow>
<VCol cols="12" :md="SystemSettings.Basic.WALLPAPER === 'customize' ? 6 : 12">
<VSelect
v-model="SystemSettings.Basic.WALLPAPER"
:label="t('setting.system.wallpaper')"
:hint="t('setting.system.wallpaperHint')"
persistent-hint
:items="wallpaperItems"
prepend-inner-icon="mdi-image"
/>
</VCol>
<VCol v-if="SystemSettings.Basic.WALLPAPER === 'customize'" cols="12" md="6">
<VTextField
v-model="SystemSettings.Basic.CUSTOMIZE_WALLPAPER_API_URL"
:label="t('setting.system.customizeWallpaperApi')"
:hint="t('setting.system.customizeWallpaperApiHint')"
:placeholder="t('setting.system.customizeWallpaperApi')"
persistent-hint
:rules="[v => !!v || t('setting.system.customizeWallpaperApiRequired')]"
prepend-inner-icon="mdi-api"
/>
</VCol>
</VRow>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.Basic.API_TOKEN"
:label="t('setting.system.apiToken')"
:hint="t('setting.system.apiTokenHint')"
:placeholder="t('setting.system.apiTokenMinChars')"
persistent-hint
prepend-inner-icon="mdi-key"
:append-inner-icon="SystemSettings.Basic.API_TOKEN ? 'mdi-content-copy' : 'mdi-reload'"
@click:append-inner="
SystemSettings.Basic.API_TOKEN ? copyValue(SystemSettings.Basic.API_TOKEN) : createRandomString()
"
:rules="[
(v: string) => !!v || t('setting.system.apiTokenRequired'),
(v: string) => v.length >= 16 || t('setting.system.apiTokenLength'),
]"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.Basic.GITHUB_TOKEN"
:label="t('setting.system.githubToken')"
:placeholder="t('setting.system.githubTokenFormat')"
:hint="t('setting.system.githubTokenHint')"
persistent-hint
prepend-inner-icon="mdi-github"
>
</VTextField>
</VCol>
</VRow>
<VCard
variant="outlined"
:class="['mt-6', isTransparentTheme ? 'ai-agent-settings-card-transparent' : 'ai-agent-settings-card']"
>
<VCardItem class="pb-3">
<template #prepend>
<VAvatar color="primary" variant="tonal" size="40">
<VIcon icon="mdi-robot-outline" />
</VAvatar>
</template>
<VCardTitle class="text-subtitle-1">
{{ t('setting.system.aiAgentSectionTitle') }}
</VCardTitle>
<VCardSubtitle>
{{ t('setting.system.aiAgentSectionDesc') }}
</VCardSubtitle>
<template #append>
<VTooltip location="top">
<template #activator="{ props }">
<VBtn
v-bind="props"
:icon="aiAgentSettingsCollapsed ? 'mdi-chevron-down' : 'mdi-chevron-up'"
variant="text"
color="primary"
size="small"
:aria-label="aiAgentSettingsCollapsed ? t('setting.about.expand') : t('setting.about.collapse')"
@click="aiAgentSettingsCollapsed = !aiAgentSettingsCollapsed"
/>
</template>
<span>{{
aiAgentSettingsCollapsed ? t('setting.about.expand') : t('setting.about.collapse')
}}</span>
</VTooltip>
</template>
</VCardItem>
<VExpandTransition>
<VCardText v-show="!aiAgentSettingsCollapsed" class="pt-2">
<VRow>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Basic.AI_AGENT_ENABLE"
:label="t('setting.system.aiAgentEnable')"
:hint="t('setting.system.aiAgentEnableHint')"
persistent-hint
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<VSwitch
v-model="SystemSettings.Basic.AI_AGENT_GLOBAL"
:label="t('setting.system.aiAgentGlobal')"
:hint="t('setting.system.aiAgentGlobalHint')"
persistent-hint
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<VSwitch
v-model="SystemSettings.Basic.AI_AGENT_VERBOSE"
:label="t('setting.system.aiAgentVerbose')"
:hint="t('setting.system.aiAgentVerboseHint')"
persistent-hint
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<VSwitch
v-model="SystemSettings.Basic.AI_AGENT_HIDE_ENTRY"
:label="t('setting.system.aiAgentHideEntry')"
:hint="t('setting.system.aiAgentHideEntryHint')"
persistent-hint
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<VAutocomplete
v-model="SystemSettings.Basic.LLM_PROVIDER"
:label="t('setting.system.llmProvider')"
:hint="t('setting.system.llmProviderHint')"
persistent-hint
:items="llmProviderItems"
:loading="loadingLlmProviders"
prepend-inner-icon="mdi-robot"
@update:model-value="handleLlmProviderChanged"
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE && showLlmApiProtocolField" cols="12" md="6">
<VSelect
v-model="SystemSettings.Basic.LLM_API_PROTOCOL"
:label="t('setting.system.llmApiProtocol')"
:hint="t('setting.system.llmApiProtocolHint')"
persistent-hint
:items="apiProtocolItems"
prepend-inner-icon="mdi-swap-horizontal"
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE && showBaseUrlField" cols="12" md="6">
<VCombobox
:model-value="SystemSettings.Basic.LLM_BASE_URL || null"
@update:model-value="
(value: any) => {
if (typeof value === 'object' && value !== null) {
setBaseUrlPreset(value.id, value.value)
} else {
setBaseUrlPreset('', value || '')
}
}
"
:label="t('setting.system.llmBaseUrl')"
:hint="t('setting.system.llmBaseUrlHint')"
:placeholder="
selectedLlmProvider?.default_base_url || t('setting.system.llmBaseUrlPlaceholder')
"
:items="llmBaseUrlPresetItems"
item-title="title"
item-value="value"
persistent-hint
persistent-placeholder
prepend-inner-icon="mdi-link"
>
<template #item="{ props, item }">
<VListItem v-bind="props" :subtitle="item.raw.subtitle" />
</template>
</VCombobox>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE && showApiKeyField" cols="12" md="6">
<VTextField
v-model="SystemSettings.Basic.LLM_API_KEY"
:label="selectedLlmProvider?.api_key_label || t('setting.system.llmApiKey')"
:hint="selectedLlmProvider?.api_key_hint || t('setting.system.llmApiKeyHint')"
:placeholder="t('setting.system.llmApiKeyPlaceholder')"
persistent-hint
persistent-placeholder
type="password"
prepend-inner-icon="mdi-key-variant"
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE && llmProviderAuthMethods.length > 0" cols="12">
<VAlert type="info" variant="tonal">
<div class="d-flex flex-column flex-md-row justify-space-between ga-3">
<div>
<div class="text-subtitle-2">{{ t('setting.system.llmProviderAuth') }}</div>
<div class="text-body-2">
{{ selectedLlmProvider?.description || t('setting.system.llmProviderAuthHint') }}
</div>
<div v-if="providerConnected" class="text-body-2 mt-2">
{{
t('setting.system.llmProviderConnectedAs', {
label: llmProviderAuthLabel || selectedLlmProvider?.name,
})
}}
</div>
</div>
<div class="d-flex flex-wrap ga-2">
<VBtn
v-for="method in llmProviderAuthMethods"
:key="method.id"
color="primary"
variant="tonal"
prepend-icon="mdi-account-arrow-right-outline"
@click="startProviderAuth(method.id)"
>
{{ method.label }}
</VBtn>
<VBtn
v-if="providerConnected"
color="error"
variant="text"
prepend-icon="mdi-link-off"
@click="disconnectProviderAuth"
>
{{ t('setting.system.llmProviderDisconnect') }}
</VBtn>
</div>
</div>
</VAlert>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<div>
<VCombobox
:model-value="SystemSettings.Basic.LLM_MODEL || null"
@update:model-value="
(val: any) => {
SystemSettings.Basic.LLM_MODEL = typeof val === 'object' && val !== null ? val.id : val
handleLlmModelChanged()
}
"
:label="t('setting.system.llmModel')"
:hint="t('setting.system.llmModelHint')"
:placeholder="t('setting.system.llmModelPlaceholder')"
persistent-hint
persistent-placeholder
:items="llmModels"
item-title="name"
item-value="id"
:loading="loadingModels"
prepend-inner-icon="mdi-brain"
>
<template #append-inner>
<VBtn
variant="text"
icon="mdi-refresh"
size="small"
@click="refreshLlmModels(true)"
:disabled="!canRefreshModels"
/>
</template>
</VCombobox>
<VAlert v-if="selectedLlmModelInfo" type="info" variant="tonal" density="compact" class="mt-2">
{{ selectedLlmModelInfo }}
</VAlert>
<div class="d-flex justify-end mt-2">
<VBtn
color="info"
variant="tonal"
density="comfortable"
prepend-icon="mdi-connection"
:disabled="!canTestLlm"
:loading="testingLlm"
class="llm-test-trigger"
@click="testLlmConnection"
>
{{ t('setting.system.llmTestAction') }}
</VBtn>
</div>
</div>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<VSelect
v-model="SystemSettings.Basic.LLM_WEB_SEARCH_MODE"
:label="t('setting.system.llmWebSearchMode')"
:hint="webSearchModeHint"
persistent-hint
:items="webSearchModeItems"
prepend-inner-icon="mdi-web"
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<VTextField
v-model.number="SystemSettings.Basic.LLM_MAX_CONTEXT_TOKENS"
:label="t('setting.system.llmMaxContextTokens')"
:hint="t('setting.system.llmMaxContextTokensHint')"
persistent-hint
type="number"
prepend-inner-icon="mdi-counter"
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<VTextField
v-model.number="SystemSettings.Basic.LLM_TEMPERATURE"
:label="t('setting.system.llmTemperature')"
:hint="t('setting.system.llmTemperatureHint')"
persistent-hint
type="number"
min="0"
max="2"
step="0.1"
prepend-inner-icon="mdi-thermometer"
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE && showBaseUrlField" cols="12" md="6">
<VTextField
v-model="SystemSettings.Basic.LLM_USER_AGENT"
:label="t('setting.system.llmUserAgent')"
:hint="t('setting.system.llmUserAgentHint')"
:placeholder="t('setting.system.llmUserAgentPlaceholder')"
persistent-hint
persistent-placeholder
prepend-inner-icon="mdi-card-account-details-outline"
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<VSelect
v-model="SystemSettings.Basic.LLM_THINKING_LEVEL"
:label="t('setting.system.llmThinking')"
:hint="t('setting.system.llmThinkingHint')"
:items="thinkingLevelItems"
persistent-hint
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<VSelect
v-model="SystemSettings.Basic.AI_AGENT_JOB_INTERVAL"
:label="t('setting.system.aiAgentJobInterval')"
:hint="t('setting.system.aiAgentJobIntervalHint')"
persistent-hint
:items="[
{ 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 },
]"
prepend-inner-icon="mdi-timer-outline"
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12">
<VAlert type="info" variant="tonal" class="agent-mcp-summary">
<div class="agent-mcp-summary__content">
<div>
<div class="text-subtitle-2">{{ t('setting.system.aiAgentMcpTitle') }}</div>
<div class="text-body-2">
{{
t('setting.system.aiAgentMcpSummary', {
enabled: agentMcpEnabledCount,
total: agentMcpServers.length,
})
}}
</div>
<div v-if="agentMcpServers.length" class="agent-mcp-summary__chips mt-2">
<VChip
v-for="server in agentMcpServerPreview"
:key="server.id"
size="small"
variant="tonal"
:color="server.enabled ? 'success' : 'default'"
>
{{ server.name }}
</VChip>
<VChip v-if="agentMcpServers.length > 3" size="small" variant="tonal">
+{{ agentMcpServers.length - 3 }}
</VChip>
</div>
</div>
<VBtn
color="primary"
variant="tonal"
prepend-icon="mdi-server-network"
:loading="loadingAgentMcpServers"
@click="agentMcpDialog = true"
>
{{ t('setting.system.aiAgentMcpSettings') }}
</VBtn>
</div>
</VAlert>
</VCol>
</VRow>
<VRow>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE && showBaseUrlField" cols="12" md="6">
<VSwitch
v-model="SystemSettings.Basic.LLM_USE_PROXY"
:label="t('setting.system.llmUseProxy')"
:hint="t('setting.system.llmUseProxyHint')"
persistent-hint
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<VSwitch
v-model="SystemSettings.Basic.LLM_SUPPORT_IMAGE_INPUT"
:label="t('setting.system.llmSupportImageInput')"
:hint="t('setting.system.llmSupportImageInputHint')"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<VSwitch
v-model="SystemSettings.Basic.LLM_SUPPORT_AUDIO_INPUT"
:label="t('setting.system.llmSupportAudioInput')"
:hint="t('setting.system.llmSupportAudioInputHint')"
persistent-hint
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<VSwitch
v-model="SystemSettings.Basic.LLM_SUPPORT_AUDIO_OUTPUT"
:label="t('setting.system.llmSupportAudioOutput')"
:hint="t('setting.system.llmSupportAudioOutputHint')"
persistent-hint
/>
</VCol>
<VCol
v-if="SystemSettings.Basic.AI_AGENT_ENABLE && SystemSettings.Basic.LLM_SUPPORT_AUDIO_INPUT"
cols="12"
md="6"
>
<VSelect
v-model="SystemSettings.Basic.AUDIO_INPUT_PROVIDER"
:label="t('setting.system.audioInputProvider')"
:hint="t('setting.system.audioInputProviderHint')"
:items="audioProviderItems"
persistent-hint
prepend-inner-icon="mdi-microphone-message"
/>
</VCol>
<VCol
v-if="SystemSettings.Basic.AI_AGENT_ENABLE && SystemSettings.Basic.LLM_SUPPORT_AUDIO_INPUT"
cols="12"
md="6"
>
<VTextField
v-model="SystemSettings.Basic.AUDIO_INPUT_MODEL"
:label="t('setting.system.audioInputModel')"
:hint="t('setting.system.audioInputModelHint')"
persistent-hint
prepend-inner-icon="mdi-waveform"
/>
</VCol>
<VCol
v-if="SystemSettings.Basic.AI_AGENT_ENABLE && SystemSettings.Basic.LLM_SUPPORT_AUDIO_INPUT"
cols="12"
md="6"
>
<VTextField
v-model="SystemSettings.Basic.AUDIO_INPUT_API_KEY"
:label="t('setting.system.audioInputApiKey')"
:hint="t('setting.system.audioInputApiKeyHint')"
persistent-hint
prepend-inner-icon="mdi-key-variant"
type="password"
/>
</VCol>
<VCol
v-if="SystemSettings.Basic.AI_AGENT_ENABLE && SystemSettings.Basic.LLM_SUPPORT_AUDIO_INPUT"
cols="12"
md="6"
>
<VTextField
v-model="SystemSettings.Basic.AUDIO_INPUT_BASE_URL"
:label="t('setting.system.audioInputBaseUrl')"
:hint="t('setting.system.audioInputBaseUrlHint')"
persistent-hint
prepend-inner-icon="mdi-link-variant"
/>
</VCol>
<VCol
v-if="SystemSettings.Basic.AI_AGENT_ENABLE && SystemSettings.Basic.LLM_SUPPORT_AUDIO_INPUT"
cols="12"
md="6"
>
<VTextField
v-model="SystemSettings.Basic.AUDIO_INPUT_LANGUAGE"
:label="t('setting.system.audioInputLanguage')"
:hint="t('setting.system.audioInputLanguageHint')"
persistent-hint
prepend-inner-icon="mdi-translate"
/>
</VCol>
<VCol
v-if="SystemSettings.Basic.AI_AGENT_ENABLE && SystemSettings.Basic.LLM_SUPPORT_AUDIO_OUTPUT"
cols="12"
md="6"
>
<VSelect
v-model="SystemSettings.Basic.AUDIO_OUTPUT_PROVIDER"
:label="t('setting.system.audioOutputProvider')"
:hint="t('setting.system.audioOutputProviderHint')"
:items="audioProviderItems"
persistent-hint
prepend-inner-icon="mdi-account-voice"
/>
</VCol>
<VCol
v-if="SystemSettings.Basic.AI_AGENT_ENABLE && SystemSettings.Basic.LLM_SUPPORT_AUDIO_OUTPUT"
cols="12"
md="6"
>
<VTextField
v-model="SystemSettings.Basic.AUDIO_OUTPUT_MODEL"
:label="t('setting.system.audioOutputModel')"
:hint="t('setting.system.audioOutputModelHint')"
persistent-hint
prepend-inner-icon="mdi-waveform"
/>
</VCol>
<VCol
v-if="SystemSettings.Basic.AI_AGENT_ENABLE && SystemSettings.Basic.LLM_SUPPORT_AUDIO_OUTPUT"
cols="12"
md="6"
>
<VTextField
v-model="SystemSettings.Basic.AUDIO_OUTPUT_API_KEY"
:label="t('setting.system.audioOutputApiKey')"
:hint="t('setting.system.audioOutputApiKeyHint')"
persistent-hint
prepend-inner-icon="mdi-key-variant"
type="password"
/>
</VCol>
<VCol
v-if="SystemSettings.Basic.AI_AGENT_ENABLE && SystemSettings.Basic.LLM_SUPPORT_AUDIO_OUTPUT"
cols="12"
md="6"
>
<VTextField
v-model="SystemSettings.Basic.AUDIO_OUTPUT_BASE_URL"
:label="t('setting.system.audioOutputBaseUrl')"
:hint="t('setting.system.audioOutputBaseUrlHint')"
persistent-hint
prepend-inner-icon="mdi-link-variant"
/>
</VCol>
<VCol
v-if="SystemSettings.Basic.AI_AGENT_ENABLE && SystemSettings.Basic.LLM_SUPPORT_AUDIO_OUTPUT"
cols="12"
md="6"
>
<VTextField
v-model="SystemSettings.Basic.AUDIO_OUTPUT_VOICE"
:label="t('setting.system.audioOutputVoice')"
:hint="t('setting.system.audioOutputVoiceHint')"
persistent-hint
prepend-inner-icon="mdi-account-voice"
/>
</VCol>
<VCol
v-if="SystemSettings.Basic.AI_AGENT_ENABLE && SystemSettings.Basic.LLM_SUPPORT_AUDIO_OUTPUT"
cols="12"
>
<VSwitch
v-model="SystemSettings.Basic.AUDIO_OUTPUT_INCLUDE_TEXT"
:label="t('setting.system.audioOutputIncludeText')"
:hint="t('setting.system.audioOutputIncludeTextHint')"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12">
<VSwitch
v-model="SystemSettings.Basic.AI_AGENT_RETRY_TRANSFER"
:label="t('setting.system.aiAgentRetryTransfer')"
:hint="t('setting.system.aiAgentRetryTransferHint')"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12">
<VSwitch
v-model="SystemSettings.Basic.AI_RECOMMEND_ENABLED"
:label="t('setting.system.aiRecommendEnabled')"
:hint="t('setting.system.aiRecommendEnabledHint')"
persistent-hint
/>
</VCol>
<VCol
v-if="SystemSettings.Basic.AI_AGENT_ENABLE && SystemSettings.Basic.AI_RECOMMEND_ENABLED"
cols="12"
md="6"
>
<VTextarea
v-model="SystemSettings.Basic.AI_RECOMMEND_USER_PREFERENCE"
:label="t('setting.system.aiRecommendUserPreference')"
:hint="t('setting.system.aiRecommendUserPreferenceHint')"
persistent-hint
rows="1"
auto-grow
prepend-inner-icon="mdi-account-heart"
/>
</VCol>
<VCol
v-if="SystemSettings.Basic.AI_AGENT_ENABLE && SystemSettings.Basic.AI_RECOMMEND_ENABLED"
cols="12"
md="6"
>
<VTextField
v-model.number="SystemSettings.Basic.AI_RECOMMEND_MAX_ITEMS"
:label="t('setting.system.aiRecommendMaxItems')"
:hint="t('setting.system.aiRecommendMaxItemsHint')"
persistent-hint
type="number"
prepend-inner-icon="mdi-format-list-numbered"
/>
</VCol>
</VRow>
</VCardText>
</VExpandTransition>
</VCard>
</VForm>
</VCardText>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="setting-actions mt-4">
<VBtn
type="submit"
@click="saveBasicSettings"
prepend-icon="mdi-content-save"
:loading="savingBasic"
:disabled="testingLlm"
class="text-no-wrap"
>
{{ t('common.save') }}
</VBtn>
<VBtn
color="error"
@click="advancedDialog = true"
prepend-icon="mdi-cog"
append-icon="mdi-dots-horizontal"
class="text-no-wrap setting-actions__secondary"
>
{{ t('setting.system.advancedSettings') }}
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VCard>
<VCardItem>
<VCardTitle>{{ t('setting.system.downloaders') }}</VCardTitle>
<VCardSubtitle>{{ t('setting.system.downloadersDesc') }}</VCardSubtitle>
</VCardItem>
<VCardText>
<Draggable
v-model="downloaders"
handle=".cursor-move"
item-key="name"
tag="div"
:component-data="{ 'class': 'grid gap-3 grid-app-card' }"
>
<template #item="{ element }">
<DownloaderCard
:downloader="element"
:downloaders="downloaders"
@close="removeDownloader(element)"
@change="onDownloaderChange"
:allow-refresh="isRequest"
/>
</template>
</Draggable>
</VCardText>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveDownloaderSetting" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VBtn color="success" variant="tonal">
<VIcon icon="mdi-plus" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem v-for="item in downloaderOptions" :key="item.value" @click="addDownloader(item.value)">
<VListItemTitle>{{ item.title }}</VListItemTitle>
</VListItem>
<VListItem @click="addDownloader('custom')">
<VListItemTitle>{{ t('setting.system.custom') }}</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VCard>
<VCardItem>
<VCardTitle>{{ t('setting.system.mediaServers') }}</VCardTitle>
<VCardSubtitle>{{ t('setting.system.mediaServersDesc') }}</VCardSubtitle>
</VCardItem>
<VCardText>
<Draggable
v-model="mediaServers"
handle=".cursor-move"
item-key="name"
tag="div"
:component-data="{ 'class': 'grid gap-3 grid-app-card' }"
>
<template #item="{ element }">
<MediaServerCard
:mediaserver="element"
:mediaservers="mediaServers"
:default-sync-interval="legacyMediaServerSyncInterval ?? undefined"
@close="removeMediaServer(element)"
@change="onMediaServerChange"
/>
</template>
</Draggable>
</VCardText>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn type="submit" @click="saveMediaServerSetting" prepend-icon="mdi-content-save">
{{ t('common.save') }}
</VBtn>
<VBtn color="success" variant="tonal">
<VIcon icon="mdi-plus" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem v-for="item in mediaServerOptions" :key="item.value" @click="addMediaServer(item.value)">
<VListItemTitle>{{ item.title }}</VListItemTitle>
</VListItem>
<VListItem @click="addMediaServer('custom')">
<VListItemTitle>{{ t('setting.system.custom') }}</VListItemTitle>
</VListItem>
</VList>
</VMenu>
</VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
</VRow>
<AgentMcpSettingsDialog
v-if="agentMcpDialog"
v-model="agentMcpDialog"
:servers="agentMcpServers"
@saved="handleAgentMcpSaved"
/>
<!-- 高级系统设置 -->
<VDialog
v-if="advancedDialog"
v-model="advancedDialog"
scrollable
max-width="60rem"
:fullscreen="!display.mdAndUp.value"
>
<VCard>
<VCardItem class="py-2">
<template #prepend>
<VIcon icon="mdi-cog" class="me-2" />
</template>
<VCardTitle>{{ t('setting.system.advancedSettings') }}</VCardTitle>
<VCardSubtitle>{{ t('setting.system.advancedSettingsDesc') }}</VCardSubtitle>
</VCardItem>
<VDialogCloseBtn @click="advancedDialog = false" />
<VCardText>
<VTabs v-model="activeTab" show-arrows>
<VTab value="system">
<div>{{ t('setting.system.system') }}</div>
</VTab>
<VTab value="media">
<div>{{ t('setting.system.media') }}</div>
</VTab>
<VTab value="network">
<div>{{ t('setting.system.network') }}</div>
</VTab>
<VTab value="data">
<div>{{ t('setting.system.data') }}</div>
</VTab>
<VTab value="log">
<div>{{ t('setting.system.log') }}</div>
</VTab>
<VTab value="dev">
<div>{{ t('setting.system.lab') }}</div>
</VTab>
</VTabs>
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
<VWindowItem value="system">
<div>
<VRow>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.AUXILIARY_AUTH_ENABLE"
:label="t('setting.system.auxAuthEnable')"
:hint="t('setting.system.auxAuthEnableHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.GLOBAL_IMAGE_CACHE"
:label="t('setting.system.globalImageCache')"
:hint="t('setting.system.globalImageCacheHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.SUBSCRIBE_STATISTIC_SHARE"
:label="t('setting.system.subscribeStatisticShare')"
:hint="t('setting.system.subscribeStatisticShareHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.PLUGIN_STATISTIC_SHARE"
:label="t('setting.system.pluginStatisticShare')"
:hint="t('setting.system.pluginStatisticShareHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.USAGE_STATISTIC_SHARE"
:label="t('setting.system.usageStatisticShare')"
:hint="t('setting.system.usageStatisticShareHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.WORKFLOW_STATISTIC_SHARE"
:label="t('setting.system.workflowStatisticShare')"
:hint="t('setting.system.workflowStatisticShareHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.BIG_MEMORY_MODE"
:label="t('setting.system.bigMemoryMode')"
:hint="t('setting.system.bigMemoryModeHint')"
persistent-hint
/>
</VCol>
<VCol v-if="SystemSettings.Basic.DB_TYPE === 'sqlite'" cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.DB_WAL_ENABLE"
:label="t('setting.system.dbWalEnable')"
:hint="t('setting.system.dbWalEnableHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="moviePilotAutoUpdate"
:label="t('setting.system.moviePilotAutoUpdate')"
:hint="t('setting.system.moviePilotAutoUpdateHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.AUTO_UPDATE_RESOURCE"
:label="t('setting.system.autoUpdateResource')"
:hint="t('setting.system.autoUpdateResourceHint')"
persistent-hint
/>
</VCol>
</VRow>
</div>
</VWindowItem>
<VWindowItem value="media">
<div>
<VRow>
<VCol cols="12" md="6">
<VCombobox
v-model="SystemSettings.Advanced.TMDB_API_DOMAIN"
:label="t('setting.system.tmdbApiDomain')"
:hint="t('setting.system.tmdbApiDomainHint')"
persistent-hint
:placeholder="t('setting.system.tmdbApiDomainPlaceholder')"
:items="['api.themoviedb.org', 'api.tmdb.org']"
:rules="[(v: string) => !!v || t('setting.system.tmdbApiDomainRequired')]"
prepend-inner-icon="mdi-api"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.Advanced.TMDB_API_KEY"
:label="t('setting.system.tmdbApiKey')"
:hint="t('setting.system.tmdbApiKeyHint')"
persistent-hint
:placeholder="t('setting.system.tmdbApiKeyPlaceholder')"
:rules="[(v: string) => !!v || t('setting.system.tmdbApiKeyRequired')]"
prepend-inner-icon="mdi-key-variant"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.Advanced.ACOUSTID_API_KEY"
:label="t('setting.system.acoustIdApiKey')"
:hint="t('setting.system.acoustIdApiKeyHint')"
persistent-hint
:placeholder="t('setting.system.acoustIdApiKeyPlaceholder')"
:rules="[(v: string) => !!v || t('setting.system.acoustIdApiKeyRequired')]"
prepend-inner-icon="mdi-music-box-multiple-outline"
/>
</VCol>
<VCol cols="12" md="6">
<VCombobox
v-model="SystemSettings.Advanced.TMDB_IMAGE_DOMAIN"
:label="t('setting.system.tmdbImageDomain')"
:hint="t('setting.system.tmdbImageDomainHint')"
persistent-hint
:placeholder="t('setting.system.tmdbImageDomainPlaceholder')"
:items="['image.tmdb.org']"
:rules="[(v: string) => !!v || t('setting.system.tmdbImageDomainRequired')]"
prepend-inner-icon="mdi-image"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.Advanced.MUSIC_COVER_PROXY"
:label="t('setting.system.musicCoverProxy')"
:hint="t('setting.system.musicCoverProxyHint')"
persistent-hint
:placeholder="t('setting.system.musicCoverProxyPlaceholder')"
prepend-inner-icon="mdi-music"
/>
</VCol>
<VCol cols="12" md="6">
<VSelect
v-model="SystemSettings.Advanced.TMDB_LOCALE"
:label="t('setting.system.tmdbLocale')"
:hint="t('setting.system.tmdbLocaleHint')"
persistent-hint
:placeholder="t('setting.system.tmdbLocalePlaceholder')"
:items="tmdbLanguageItems"
prepend-inner-icon="mdi-translate"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.Advanced.META_CACHE_EXPIRE"
:label="t('setting.system.metaCacheExpire')"
:hint="t('setting.system.metaCacheExpireHint')"
persistent-hint
min="0"
type="number"
:suffix="t('setting.system.hour')"
:rules="[
(v: any) => v === 0 || !!v || t('setting.system.metaCacheExpireRequired'),
(v: any) => v >= 0 || t('setting.system.metaCacheExpireMin'),
]"
prepend-inner-icon="mdi-timer"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.SCRAP_FOLLOW_TMDB"
:label="t('setting.system.scrapFollowTmdb')"
:hint="t('setting.system.scrapFollowTmdbHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.TMDB_SCRAP_ORIGINAL_IMAGE"
:label="t('setting.system.scrapOriginalImage')"
:hint="t('setting.system.scrapOriginalImageHint')"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.MUSIC_METADATA_TO_SIMPLIFIED"
:label="t('setting.system.musicMetadataToSimplified')"
:hint="t('setting.system.musicMetadataToSimplifiedHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.RECOGNIZE_PLUGIN_FIRST"
:label="t('setting.system.recognizePluginFirst')"
:hint="t('setting.system.recognizePluginFirstHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.MEDIA_RECOGNIZE_SHARE"
:label="t('setting.system.mediaRecognizeShare')"
:hint="t('setting.system.mediaRecognizeShareHint')"
persistent-hint
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.FANART_ENABLE"
:label="t('setting.system.fanartEnable')"
:hint="t('setting.system.fanartEnableHint')"
persistent-hint
/>
</VCol>
<VCol v-if="SystemSettings.Advanced.FANART_ENABLE" cols="12" md="6">
<VSelect
v-model="fanartLanguageSelection"
:label="t('setting.system.fanartLang')"
:hint="t('setting.system.fanartLangHint')"
persistent-hint
:items="fanartLanguageItems"
multiple
chips
closable-chips
prepend-inner-icon="mdi-translate"
/>
</VCol>
</VRow>
<!-- 刮削开关设置 -->
<VRow class="mt-4">
<VCol cols="12">
<VExpansionPanels>
<VExpansionPanel>
<VExpansionPanelTitle class="text-lg">
<VIcon icon="mdi-checkbox-multiple-outline" class="me-2" />
{{ t('setting.system.scrapingSwitchSettings') }}
<!-- 帮助图标 -->
<VTooltip location="bottom" open-delay="200">
<template #activator="{ props: tooltipProps }">
<VBtn
v-bind="tooltipProps"
icon="mdi-help-circle"
size="small"
variant="text"
color="medium-emphasis"
class="ml-2"
@click.stop
/>
</template>
<div class="d-flex flex-column gap-2 py-2">
<div class="d-flex align-center">
<VIcon icon="mdi-file-remove" color="error" class="mr-2" />
<span>{{ t('setting.system.policy.skipDesc') }}</span>
</div>
<div class="d-flex align-center">
<VIcon icon="mdi-file-plus" color="success" class="mr-2" />
<span>{{ t('setting.system.policy.missingOnlyDesc') }}</span>
</div>
<div class="d-flex align-center">
<VIcon icon="mdi-file-replace" color="primary" class="mr-2" />
<span>{{ t('setting.system.policy.overwriteDesc') }}</span>
</div>
</div>
</VTooltip>
</VExpansionPanelTitle>
<VExpansionPanelText>
<VRow v-for="section in scrapingConfig" :key="section.section">
<VCol cols="12">
<VListSubheader class="text-lg">
{{ t(`setting.system.${section.section}`) }}
</VListSubheader>
</VCol>
<VCol v-for="item in section.items" :key="item.key" cols="12" md="4">
<div class="d-flex align-center">
<VBtnToggle
:model-value="ScrapingPolicies[item.key]"
@update:model-value="ScrapingPolicies[item.key] = $event"
color="primary"
variant="tonal"
size="small"
rounded="lg"
>
<VBtn value="skip" color="error" size="small">
<VIcon icon="mdi-file-remove" />
</VBtn>
<VBtn value="missingOnly" color="success" size="small">
<VIcon icon="mdi-file-plus" />
</VBtn>
<VBtn value="overwrite" color="primary" size="small">
<VIcon icon="mdi-file-replace" />
</VBtn>
</VBtnToggle>
<span class="ml-2">{{ t(item.label) }}</span>
</div>
</VCol>
<VDivider v-if="section.section !== 'music'" class="my-4" />
</VRow>
</VExpansionPanelText>
</VExpansionPanel>
</VExpansionPanels>
</VCol>
</VRow>
</div>
</VWindowItem>
<VWindowItem value="network">
<div>
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.Advanced.PROXY_HOST"
:label="t('setting.system.proxyHost')"
placeholder="http://127.0.0.1:7890"
:hint="t('setting.system.proxyHostHint')"
persistent-hint
prepend-inner-icon="mdi-server-network"
/>
</VCol>
<VCol cols="12" md="6">
<VCombobox
v-model="githubProxyDisplay"
:label="t('setting.system.githubProxy')"
:placeholder="t('setting.system.githubProxyPlaceholder')"
:hint="t('setting.system.githubProxyHint')"
persistent-hint
:items="githubMirrorsItems"
clearable
prepend-inner-icon="mdi-github"
/>
</VCol>
<VCol cols="12">
<VCombobox
v-model="pipProxyDisplay"
:label="t('setting.system.pipProxy')"
:placeholder="t('setting.system.pipProxyPlaceholder')"
:hint="t('setting.system.pipProxyHint')"
persistent-hint
:items="pipMirrorsItems"
clearable
prepend-inner-icon="mdi-package"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.DOH_ENABLE"
:label="t('setting.system.dohEnable')"
:hint="t('setting.system.dohEnableHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" v-show="SystemSettings.Advanced.DOH_ENABLE">
<VTextarea
v-model="SystemSettings.Advanced.DOH_RESOLVERS"
:label="t('setting.system.dohResolvers')"
:placeholder="t('setting.system.dohResolversPlaceholder')"
:hint="t('setting.system.dohResolversHint')"
persistent-hint
prepend-inner-icon="mdi-dns"
/>
</VCol>
<VCol cols="12" v-show="SystemSettings.Advanced.DOH_ENABLE">
<VTextarea
v-model="SystemSettings.Advanced.DOH_DOMAINS"
:label="t('setting.system.dohDomains')"
:placeholder="t('setting.system.dohDomainsPlaceholder')"
:hint="t('setting.system.dohDomainsHint')"
persistent-hint
prepend-inner-icon="mdi-domain"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12">
<VExpansionPanels>
<VExpansionPanel>
<VExpansionPanelTitle class="text-lg">
<template #default>
<VIcon icon="mdi-shield-check" class="me-2" />
{{ t('setting.system.securityImageDomains') }}
</template>
</VExpansionPanelTitle>
<VExpansionPanelText>
<div class="d-flex flex-wrap gap-2 mb-3">
<VChip
v-for="(domain, index) in SystemSettings.Advanced.SECURITY_IMAGE_DOMAINS"
:key="index"
closable
@click:close="SystemSettings.Advanced.SECURITY_IMAGE_DOMAINS.splice(index, 1)"
>
{{ domain }}
</VChip>
<VChip v-if="SystemSettings.Advanced.SECURITY_IMAGE_DOMAINS.length === 0" color="warning">
{{ t('setting.system.noSecurityImageDomains') }}
</VChip>
</div>
<div class="d-flex align-center gap-2">
<VTextField
v-model="newSecurityDomain"
:placeholder="t('setting.system.securityImageDomainAdd')"
hide-details
density="compact"
prepend-inner-icon="mdi-shield-check"
>
<template #append>
<VBtn icon color="primary" @click="addSecurityDomain" :disabled="!newSecurityDomain">
<VIcon icon="mdi-plus" />
</VBtn>
</template>
</VTextField>
</div>
<VDivider class="my-4" />
<div class="text-subtitle-2 mb-1">
{{ t('setting.system.imageProxyAllowedPrivateRanges') }}
</div>
<div class="text-caption text-medium-emphasis mb-3">
{{ t('setting.system.imageProxyAllowedPrivateRangesHint') }}
</div>
<div class="d-flex flex-wrap gap-2 mb-3">
<VChip
v-for="(range, index) in SystemSettings.Advanced.IMAGE_PROXY_ALLOWED_PRIVATE_RANGES"
:key="index"
closable
@click:close="SystemSettings.Advanced.IMAGE_PROXY_ALLOWED_PRIVATE_RANGES.splice(index, 1)"
>
{{ range }}
</VChip>
<VChip
v-if="SystemSettings.Advanced.IMAGE_PROXY_ALLOWED_PRIVATE_RANGES.length === 0"
color="warning"
>
{{ t('setting.system.noImageProxyAllowedPrivateRanges') }}
</VChip>
</div>
<div class="d-flex align-center gap-2">
<VTextField
v-model="newImageProxyAllowedPrivateRange"
:placeholder="t('setting.system.imageProxyAllowedPrivateRangeAdd')"
hide-details
density="compact"
prepend-inner-icon="mdi-ip-network"
>
<template #append>
<VBtn
icon
color="primary"
@click="addImageProxyAllowedPrivateRange"
:disabled="!newImageProxyAllowedPrivateRange"
>
<VIcon icon="mdi-plus" />
</VBtn>
</template>
</VTextField>
</div>
</VExpansionPanelText>
</VExpansionPanel>
</VExpansionPanels>
</VCol>
</VRow>
</div>
</VWindowItem>
<VWindowItem value="data">
<div>
<VRow>
<VCol cols="12">
<VSwitch
v-model="SystemSettings.Advanced.DB_BACKUP_ENABLE"
:label="t('setting.system.dbBackupEnable')"
:hint="t('setting.system.dbBackupEnableHint')"
persistent-hint
/>
</VCol>
<template v-if="SystemSettings.Advanced.DB_BACKUP_ENABLE">
<VCol cols="12" md="6">
<VCronField
v-model="SystemSettings.Advanced.DB_BACKUP_CRON"
:label="t('setting.system.dbBackupCron')"
:hint="t('setting.system.dbBackupCronHint')"
persistent-hint
clearable
:rules="dbBackupCronRules"
prepend-inner-icon="mdi-clock-outline"
/>
</VCol>
<VCol cols="12" md="6">
<VPathField
v-model="dbBackupPath"
storage="local"
:label="t('setting.system.dbBackupPath')"
:placeholder="t('setting.system.dbBackupPathPlaceholder')"
:hint="t('setting.system.dbBackupPathHint')"
persistent-hint
prepend-inner-icon="mdi-folder-outline"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model.number="SystemSettings.Advanced.DB_BACKUP_RETENTION_DAYS"
:label="t('setting.system.dbBackupRetentionDays')"
:hint="t('setting.system.dbBackupRetentionDaysHint')"
persistent-hint
min="0"
step="1"
type="number"
:suffix="t('setting.system.day')"
:rules="dbBackupRetentionRules"
prepend-inner-icon="mdi-calendar-remove-outline"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model.number="SystemSettings.Advanced.DB_BACKUP_MAX_COUNT"
:label="t('setting.system.dbBackupMaxCount')"
:hint="t('setting.system.dbBackupMaxCountHint')"
persistent-hint
min="0"
step="1"
type="number"
:rules="dbBackupMaxCountRules"
prepend-inner-icon="mdi-backup-restore"
/>
</VCol>
<VCol cols="12">
<VSwitch
v-model="SystemSettings.Advanced.DB_BACKUP_ON_UPGRADE"
:label="t('setting.system.dbBackupOnUpgrade')"
:hint="t('setting.system.dbBackupOnUpgradeHint')"
persistent-hint
/>
</VCol>
</template>
<VCol cols="12">
<VSwitch
v-model="SystemSettings.Advanced.DATA_CLEANUP_ENABLE"
:label="t('setting.system.dataCleanupEnable')"
:hint="t('setting.system.dataCleanupEnableHint')"
persistent-hint
/>
</VCol>
<template v-if="SystemSettings.Advanced.DATA_CLEANUP_ENABLE">
<VCol cols="12" md="6">
<VTextField
v-model.number="SystemSettings.Advanced.DATA_CLEANUP_MESSAGE_DAYS"
:label="t('setting.system.dataCleanupMessageDays')"
:hint="t('setting.system.dataCleanupMessageDaysHint')"
persistent-hint
min="0"
type="number"
:suffix="t('setting.system.day')"
:rules="dataCleanupFieldRules"
prepend-inner-icon="mdi-email-outline"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model.number="SystemSettings.Advanced.DATA_CLEANUP_DOWNLOAD_HISTORY_DAYS"
:label="t('setting.system.dataCleanupDownloadHistoryDays')"
:hint="t('setting.system.dataCleanupDownloadHistoryDaysHint')"
persistent-hint
min="0"
type="number"
:suffix="t('setting.system.day')"
:rules="dataCleanupFieldRules"
prepend-inner-icon="mdi-download-circle-outline"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model.number="SystemSettings.Advanced.DATA_CLEANUP_SITE_USERDATA_DAYS"
:label="t('setting.system.dataCleanupSiteUserDataDays')"
:hint="t('setting.system.dataCleanupSiteUserDataDaysHint')"
persistent-hint
min="0"
type="number"
:suffix="t('setting.system.day')"
:rules="dataCleanupFieldRules"
prepend-inner-icon="mdi-chart-line"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model.number="SystemSettings.Advanced.DATA_CLEANUP_TRANSFER_HISTORY_DAYS"
:label="t('setting.system.dataCleanupTransferHistoryDays')"
:hint="t('setting.system.dataCleanupTransferHistoryDaysHint')"
persistent-hint
min="0"
type="number"
:suffix="t('setting.system.day')"
:rules="dataCleanupFieldRules"
prepend-inner-icon="mdi-swap-horizontal"
/>
</VCol>
</template>
</VRow>
</div>
</VWindowItem>
<VWindowItem value="log">
<div>
<VRow>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.DEBUG"
:label="t('setting.system.debug')"
:hint="t('setting.system.debugHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSelect
v-if="!SystemSettings.Advanced.DEBUG"
v-model="SystemSettings.Advanced.LOG_LEVEL"
:label="t('setting.system.logLevel')"
:hint="t('setting.system.logLevelHint')"
persistent-hint
:items="logLevelItems"
prepend-inner-icon="mdi-format-list-bulleted"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.Advanced.LOG_MAX_FILE_SIZE"
:label="t('setting.system.logMaxFileSize')"
:hint="t('setting.system.logMaxFileSizeHint')"
persistent-hint
min="1"
type="number"
:suffix="t('setting.system.mb')"
:rules="[
(v: any) => v === 0 || !!v || t('setting.system.logMaxFileSizeRequired'),
(v: any) => v >= 1 || t('setting.system.logMaxFileSizeMin'),
]"
prepend-inner-icon="mdi-file-document"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.Advanced.LOG_BACKUP_COUNT"
:label="t('setting.system.logBackupCount')"
:hint="t('setting.system.logBackupCountHint')"
persistent-hint
min="1"
type="number"
:rules="[
(v: any) => v === 0 || !!v || t('setting.system.logBackupCountRequired'),
(v: any) => v >= 1 || t('setting.system.logBackupCountMin'),
]"
prepend-inner-icon="mdi-backup-restore"
/>
</VCol>
<VCol cols="12">
<VTextField
v-model="SystemSettings.Advanced.LOG_FILE_FORMAT"
:label="t('setting.system.logFileFormat')"
:hint="t('setting.system.logFileFormatHint')"
persistent-hint
prepend-inner-icon="mdi-format-text"
/>
</VCol>
</VRow>
</div>
</VWindowItem>
<VWindowItem value="dev">
<div>
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.Advanced.PLUGIN_LOCAL_REPO_PATHS"
:label="t('setting.system.pluginLocalRepoPaths')"
:hint="t('setting.system.pluginLocalRepoPathsHint')"
persistent-hint
prepend-inner-icon="mdi-folder"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model.number="SystemSettings.Advanced.TRANSFER_THREADS"
:label="t('setting.system.transferThreads')"
:hint="t('setting.system.transferThreadsHint')"
persistent-hint
type="number"
min="1"
prepend-inner-icon="mdi-swap-horizontal"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model.number="SystemSettings.Advanced.TRANSFER_MAX_FAILED_RETRIES"
:label="t('setting.system.transferMaxFailedRetries')"
:hint="t('setting.system.transferMaxFailedRetriesHint')"
persistent-hint
type="number"
min="1"
max="10"
prepend-inner-icon="mdi-refresh-alert"
/>
</VCol>
<VCol v-if="SystemSettings.Advanced.FS_PROXY_ENABLED" cols="12" md="6">
<VTextField
v-model.number="SystemSettings.Advanced.FS_PROXY_TIMEOUT"
:label="t('setting.system.fsProxyTimeout')"
:hint="t('setting.system.fsProxyTimeoutHint')"
persistent-hint
type="number"
min="5"
prepend-inner-icon="mdi-timer-outline"
/>
</VCol>
<VCol v-if="SystemSettings.Advanced.FS_PROXY_ENABLED" cols="12" md="6">
<VTextField
v-model.number="SystemSettings.Advanced.FS_PROXY_STALL_TIMEOUT"
:label="t('setting.system.fsProxyStallTimeout')"
:hint="t('setting.system.fsProxyStallTimeoutHint')"
persistent-hint
type="number"
min="10"
prepend-inner-icon="mdi-timer-alert-outline"
/>
</VCol>
</VRow>
<VRow>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.PLUGIN_AUTO_RELOAD"
:label="t('setting.system.pluginAutoReload')"
:hint="t('setting.system.pluginAutoReloadHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.FS_PROXY_ENABLED"
:label="t('setting.system.fsProxyEnabled')"
:hint="t('setting.system.fsProxyEnabledHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.ENCODING_DETECTION_PERFORMANCE_MODE"
:label="t('setting.system.encodingDetectionPerformanceMode')"
:hint="t('setting.system.encodingDetectionPerformanceModeHint')"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.RUST_ACCEL"
:label="t('setting.system.rustAccel')"
:hint="rustAccelHint"
:disabled="!rustAccelAvailable"
persistent-hint
/>
</VCol>
<VCol cols="12" md="6">
<VSwitch
v-model="SystemSettings.Advanced.MONITOR_NETWORK_FAST_MODE"
:label="t('setting.system.monitorNetworkFastMode')"
:hint="t('setting.system.monitorNetworkFastModeHint')"
persistent-hint
/>
</VCol>
</VRow>
</div>
</VWindowItem>
</VWindow>
</VCardText>
<VCardActions class="app-dialog-actions">
<VSpacer />
<VBtn color="primary" variant="flat" prepend-icon="mdi-content-save" @click="saveAdvancedSettings" class="px-5">
{{ t('common.save') }}
</VBtn>
</VCardActions>
</VCard>
</VDialog>
</template>
<style scoped>
.ai-agent-settings-card {
border-color: rgba(var(--v-theme-primary), 0.15);
background: linear-gradient(180deg, rgba(var(--v-theme-primary), 0.04) 0%, rgba(var(--v-theme-surface), 0.92) 100%);
}
.ai-agent-settings-card-transparent {
border-color: rgba(var(--v-theme-primary), 0);
background-color: rgba(var(--v-theme-surface), 0) !important;
}
.setting-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.setting-actions__secondary {
flex-shrink: 0;
}
.llm-test-trigger {
min-inline-size: 0;
}
.agent-mcp-summary__content {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.agent-mcp-summary__chips {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
@media (max-width: 600px) {
.agent-mcp-summary__content {
align-items: stretch;
flex-direction: column;
}
}
</style>