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

2451 lines
95 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- eslint-disable sonarjs/no-duplicate-string -->
<script lang="ts" setup>
import { useToast } from 'vue-toastification'
import api from '@/api'
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 { 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 SystemSettings = ref<any>({
// 基础设置
Basic: {
DB_TYPE: 'sqlite',
APP_DOMAIN: null,
API_TOKEN: null,
WALLPAPER: 'tmdb',
MEDIASERVER_SYNC_INTERVAL: null,
RECOGNIZE_SOURCE: 'themoviedb',
GITHUB_TOKEN: null,
OCR_HOST: null,
CUSTOMIZE_WALLPAPER_API_URL: null,
AI_AGENT_ENABLE: false,
AI_AGENT_GLOBAL: false,
AI_AGENT_VERBOSE: false,
AI_AGENT_JOB_INTERVAL: 24,
LLM_PROVIDER: 'deepseek',
LLM_MODEL: 'deepseek-chat',
LLM_THINKING_LEVEL: 'off',
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: 64,
LLM_USER_AGENT: null,
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,
// 媒体
RECOGNIZE_PLUGIN_FIRST: false,
MEDIA_RECOGNIZE_SHARE: true,
TMDB_API_DOMAIN: null,
TMDB_API_KEY: null,
TMDB_IMAGE_DOMAIN: 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,
},
})
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' },
],
},
]
// 刮削策略设置
const ScrapingPolicies = ref<Record<string, 'skip' | 'missingOnly' | 'overwrite'>>(
Object.fromEntries(scrapingConfig.flatMap(section => section.items.map(item => [item.key, 'missingOnly']))),
)
// 是否发送请求的总开关
const isRequest = ref(true)
// 选中的媒体服务器
const mediaServers = ref<MediaServerConf[]>([])
// 下载器
const downloaders = ref<DownloaderConf[]>([])
// 提示框
const $toast = useToast()
// 高级设置对话框
const advancedDialog = ref(false)
const savingBasic = ref(false)
const testingLlm = ref(false)
const rustAccelAvailable = ref(false)
// 智能助手配置项较多,默认收起以降低基础设置页的视觉占用。
const aiAgentSettingsCollapsed = ref(true)
type LlmSettingsSnapshot = {
AI_AGENT_ENABLE: boolean
LLM_PROVIDER: string
LLM_MODEL: string
LLM_THINKING_LEVEL: string
LLM_API_KEY: string
LLM_BASE_URL: string
LLM_USE_PROXY: boolean
LLM_BASE_URL_PRESET: string
LLM_USER_AGENT: string
}
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 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,
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,
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_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 ?? ''),
}
}
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_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(),
}
}
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, any>) {
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 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 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 = [
(v: any) => v === 0 || !!v || t('setting.system.dataCleanupDaysRequired'),
(v: any) => v >= 0 || t('setting.system.dataCleanupDaysMin'),
]
// 安全域名添加变量
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: { [key: string]: any } = await api.get('system/setting/Downloaders')
downloaders.value = result.data?.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)
}
const result: { [key: string]: any } = await api.post('system/setting/Downloaders', downloaders.value)
if (result.success) $toast.success(t('setting.system.downloaderSaveSuccess'))
else $toast.error(t('setting.system.downloaderSaveFailed'))
await loadDownloaderSetting()
} catch (error) {
console.log(error)
}
}
// 处理默认下载器状态
function handleDefaultDownloaders(enabledDownloaders: any[], downloaders: any[]) {
const enabledDefaultDownloader = enabledDownloaders.find(item => item.default)
if (enabledDownloaders.length > 0 && !enabledDefaultDownloader) {
downloaders = downloaders.map(item => {
if (item === enabledDownloaders[0]) {
$toast.info(t('setting.system.defaultDownloaderNotice', { name: item.name }))
return { ...item, default: true }
}
// 清除其他下载器的默认下载器状态
return { ...item, default: false }
})
}
return downloaders
}
// 调用API查询媒体服务器设置
async function loadMediaServerSetting() {
try {
const result: { [key: string]: any } = await api.get('system/setting/MediaServers')
mediaServers.value = result.data?.value ?? []
} catch (error) {
console.log(error)
}
}
// 调用API保存媒体服务器设置
async function saveMediaServerSetting() {
try {
const result: { [key: string]: any } = await api.post('system/setting/MediaServers', mediaServers.value)
if (result.success) $toast.success(t('setting.system.mediaServerSaveSuccess'))
else $toast.error(t('setting.system.mediaServerSaveFailed'))
await loadMediaServerSetting()
} catch (error) {
console.log(error)
}
}
// 加载系统设置
async function loadSystemSettings() {
invalidateLlmTestState()
try {
const result: { [key: string]: any } = await api.get('system/env')
if (result.success) {
// 将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 (result.data.hasOwnProperty(key)) (SystemSettings.value[sectionKey] as any)[key] = result.data[key]
})
}
const accelAvailable = Boolean(result.data.RUST_ACCEL_AVAILABLE ?? result.data.RUST_ACCEL_ENABLED)
rustAccelAvailable.value = accelAvailable
if (!accelAvailable) SystemSettings.value.Advanced.RUST_ACCEL = false
SystemSettings.value.Basic.LLM_THINKING_LEVEL = resolveThinkingLevelValue(result.data)
await loadLlmProviders()
}
} catch (error) {
console.log(error)
}
}
// 调用API保存设置
async function saveSystemSetting(value: { [key: string]: any }) {
try {
const result: { [key: string]: any } = await api.post('system/env', value)
if (result.success) {
return true
} else {
$toast.error(t('setting.system.saveFailed', { message: result?.message }))
return false
}
} catch (error) {
console.log(error)
}
return false
}
// 保存基础设置
async function saveBasicSettings() {
savingBasic.value = true
try {
if (await saveSystemSetting(SystemSettings.value.Basic)) {
// 更新全局设置store使Web Agent图标实时生效
globalSettingsStore.setData({ ...globalSettingsStore.getData, ...SystemSettings.value.Basic })
$toast.success(t('setting.system.basicSaveSuccess'))
}
} finally {
savingBasic.value = false
}
}
async function testLlmConnection() {
if (!canTestLlm.value) return
const snapshot = buildLlmSnapshot()
const snapshotKey = buildLlmSnapshotKey(snapshot)
const payload = buildLlmTestPayload(snapshot)
const requestId = ++llmTestRequestId
if (llmTestAbortController) llmTestAbortController.abort()
const abortController = new AbortController()
llmTestAbortController = abortController
testingLlm.value = true
try {
const result: { [key: string]: any } = await api.post('llm/test', payload, {
signal: abortController.signal,
})
if (
requestId !== llmTestRequestId ||
abortController.signal.aborted ||
currentLlmSnapshotKey.value !== snapshotKey
) {
return
}
if (result?.success) $toast.success(t('setting.system.llmTestSuccessToast'))
else showLlmTestFailedToast(result?.message)
} 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) return
if (llmTestAbortController === abortController) llmTestAbortController = null
testingLlm.value = false
}
}
// 保存高级设置
async function saveAdvancedSettings() {
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 && scrapingResult) {
advancedDialog.value = false
$toast.success(t('setting.system.advancedSaveSuccess'))
}
}
// 当字段为空时,将其设置为 null 提交,以便后端恢复为默认值
function cleanEmptyFields(settings: any, fields: string[]) {
fields.forEach(field => {
if (settings[field]?.trim?.() === '') {
settings[field] = null
}
})
}
// 快捷复制到剪贴板
async function copyValue(value: string) {
try {
let success
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(',')
.filter(Boolean)
.map((lang: any) => lang.trim())
},
set: (val: string[]) => {
SystemSettings.value.Advanced.FANART_LANG = val.join(',')
},
})
// 加载刮削开关设置
async function loadScrapingSwitchs() {
try {
const result: { [key: string]: any } = await api.get('system/setting/ScrapingSwitchs')
if (result.success && result.data?.value) {
const loadedSwitches = result.data.value
for (const key in loadedSwitches) {
if (typeof loadedSwitches[key] === 'boolean') {
// 兼容旧数据
loadedSwitches[key] = loadedSwitches[key] ? 'missingOnly' : 'skip'
}
}
ScrapingPolicies.value = { ...ScrapingPolicies.value, ...loadedSwitches }
}
} catch (error) {
console.log(error)
}
}
// 保存刮削开关设置
async function saveScrapingSwitchs() {
try {
const result: { [key: string]: any } = await api.post('system/setting/ScrapingSwitchs', ScrapingPolicies.value)
if (result.success) {
return true
} else {
$toast.error(t('setting.system.scrapingSwitchSaveFailed', { message: result?.message }))
return false
}
} 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">
<VSelect
v-model="SystemSettings.Basic.RECOGNIZE_SOURCE"
:label="t('setting.system.recognizeSource')"
:hint="t('setting.system.recognizeSourceHint')"
persistent-hint
:items="[
{ title: 'TheMovieDb', value: 'themoviedb' },
{ title: '豆瓣', value: 'douban' },
]"
prepend-inner-icon="mdi-database"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.Basic.MEDIASERVER_SYNC_INTERVAL"
:label="t('setting.system.mediaServerSyncInterval')"
:hint="t('setting.system.mediaServerSyncIntervalHint')"
persistent-hint
:suffix="t('setting.system.hours')"
type="number"
min="1"
:rules="[
(v: any) => !!v || t('setting.system.required'),
(v: any) => !isNaN(v) || t('setting.system.numbersOnly'),
(v: any) => v >= 1 || t('setting.system.minInterval'),
]"
prepend-inner-icon="mdi-sync"
/>
</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>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.Basic.OCR_HOST"
:label="t('setting.system.ocrHost')"
placeholder="https://movie-pilot.org"
:hint="t('setting.system.ocrHostHint')"
persistent-hint
prepend-inner-icon="mdi-text-recognition"
/>
</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="4">
<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="4">
<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="4">
<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">
<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 && showBaseUrlField" cols="12" md="6">
<VCombobox
:model-value="SystemSettings.Basic.LLM_BASE_URL"
@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 || 'https://api.deepseek.com'"
:items="llmBaseUrlPresetItems"
item-title="title"
item-value="value"
persistent-hint
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 && showBaseUrlField" cols="12">
<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 && 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
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"
@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.llmModelHint')"
persistent-hint
: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">
<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 && showBaseUrlField" cols="12" md="6">
<VTextField
v-model="SystemSettings.Basic.LLM_USER_AGENT"
:label="t('setting.system.llmUserAgent')"
:hint="t('setting.system.llmUserAgentHint')"
persistent-hint
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>
</VRow>
<VRow>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="4">
<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" @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"
@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" @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>
<!-- 高级系统设置 -->
<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">
<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">
<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.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 !== 'episode'" 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.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>
</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.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>
</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;
}
</style>