Compare commits

..

20 Commits

Author SHA1 Message Date
jxxghp
229b7b0c12 chore: bump version to 2.8.5 in package.json 2025-11-20 19:37:58 +08:00
jxxghp
4b7b5ff8a4 fix #397 2025-11-20 19:37:33 +08:00
jxxghp
4906bde746 chore: bump version to 2.8.4 in package.json and refactor AccountSettingSystem.vue to streamline AI agent settings 2025-11-20 19:25:28 +08:00
jxxghp
a87a1a8988 Merge pull request #403 from madrays/v2 2025-11-20 19:11:51 +08:00
madrays
e05f45e681 增加自动拉取可用ai模型的易用性功能 2025-11-20 19:01:25 +08:00
jxxghp
b4acacea81 chore: bump version to 2.8.3 in package.json 2025-11-18 12:49:06 +08:00
jxxghp
fa9645b05b Merge pull request #402 from cddjr/trimemedia 2025-11-17 14:21:39 +08:00
景大侠
1ed4052814 fix #401 2025-11-17 14:08:51 +08:00
jxxghp
7dc814461f 更新 package.json 2025-11-16 06:31:32 +08:00
jxxghp
9154ec0e8c Merge pull request #400 from wikrin/cursor-move 2025-11-16 06:30:57 +08:00
jxxghp
3a2ea60583 Merge pull request #399 from wikrin/release_dates 2025-11-16 06:30:31 +08:00
Attente
b36bff3a1e feat(dashboard): 移除 Vue 渲染模式下的固定拖拽图标
更新`docs/module-federation-guide.md` 文档,使用 `v-hover` 实现仅在鼠标悬停时显示拖拽图标。
2025-11-15 18:03:29 +08:00
Attente
b3d8cbf280 feat: 为媒体信息添加数字/实体发行日期支持 2025-11-13 23:52:54 +08:00
jxxghp
38fb02d112 Merge pull request #398 from cddjr/trimemedia 2025-11-05 23:16:28 +08:00
景大侠
2597f893cd rename 2025-11-05 15:26:34 +08:00
景大侠
ebdd036654 避免飞牛媒体库的图片地址携带敏感数据 2025-11-05 15:15:36 +08:00
景大侠
5032f0e6a9 fix 飞牛影视无法显示图片
图片接口增加Cookies参数
2025-11-04 11:32:41 +08:00
jxxghp
ad963d718d refactor: Remove unused AI agent subheader from account settings 2025-11-01 10:39:25 +08:00
jxxghp
69d314bce3 feat: Add AI agent settings and localization support for LLM configuration 2025-10-31 11:46:45 +08:00
jxxghp
4a7425a947 feat: Add download count formatting function and update card components to use it 2025-10-18 20:13:41 +08:00
17 changed files with 334 additions and 23 deletions

View File

@@ -245,13 +245,21 @@ const props = defineProps({
<template>
<div class="dashboard-widget">
<!-- 仪表板内容 -->
<v-card>
<v-card-title>{{ config.title || '仪表板组件' }}</v-card-title>
<v-card-text>
<!-- 组件内容 -->
</v-card-text>
</v-card>
<v-hover>
<!-- 仪表板内容 -->
<template #default="{ isHovering, props: hoverProps }">
<v-card v-bind="hoverProps">
<v-card-title>{{ config.title || '仪表板组件' }}</v-card-title>
<v-card-text>
<!-- 组件内容 -->
</v-card-text>
<!-- 只在悬停时显示拖拽图标 -->
<div v-show="isHovering" class="absolute right-5 top-5">
<v-icon class="cursor-move">mdi-drag</v-icon>
</div>
</v-card>
</template>
</v-hover>
</div>
</template>
```

View File

@@ -1,6 +1,6 @@
{
"name": "moviepilot",
"version": "2.8.1",
"version": "2.8.5",
"private": true,
"type": "module",
"bin": "dist/service.js",

View File

@@ -23,6 +23,13 @@ export function kFormatter(num: number) {
: Math.abs(num).toFixed(0).replace(regex, ',')
}
// 格式化下载量显示超过1000显示为x.xk格式
export function formatDownloadCount(num: number): string {
if (!num || num < 1000) return num?.toLocaleString() || '0'
return `${(num / 1000).toFixed(1)}k`
}
/**
* Format and return date in Humanize format
* Intl docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/format

View File

@@ -314,6 +314,8 @@ export interface MediaInfo {
production_countries?: any[]
// 语种
spoken_languages?: string[]
// 数字/实体发行日期
release_dates?: MediaRelease[]
// 状态
status?: string
// 标签
@@ -368,6 +370,18 @@ export interface TmdbSeason {
vote_average?: number
}
// 发行信息
export interface MediaRelease {
// 发行日期
date: string
// 发行地区
iso_code: string
// 备注
note?: string
// 发行类型
type: number
}
// TMDB集信息
export interface TmdbEpisode {
// 上映日期
@@ -992,6 +1006,8 @@ export interface MediaServerPlayItem {
percent?: number
// 媒体服务器类型
server_type?: string
// 图片是否需要Cookies
use_cookies?: boolean
}
// 媒体服务器媒体库
@@ -1014,6 +1030,8 @@ export interface MediaServerLibrary {
link?: string
// 媒体服务器类型
server_type?: string
// 图片是否需要Cookies
use_cookies?: boolean
}
// 消息通知

View File

@@ -26,7 +26,12 @@ async function goPlay() {
// 计算图片地址
const getImgUrl = computed(() => {
const image = props.media?.image || ''
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
let url = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
const use_cookies = props.media?.use_cookies
if (use_cookies) {
url += `&use_cookies=${encodeURIComponent(use_cookies)}`
}
return url
})
</script>

View File

@@ -52,20 +52,28 @@ async function goPlay() {
}
// 生成图片代理路径
function getImgUrl(url: string) {
function getImgUrl(url: string, use_cookies?: boolean) {
if (!url) return getDefaultImage()
else return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}`
let imgurl = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}`
if (use_cookies) {
imgurl += `&use_cookies=${encodeURIComponent(use_cookies)}`
}
return imgurl
}
// 根据多张图片生成媒体库封面
async function drawImages(imageList: string[]) {
async function drawImages(imageList: string[], use_cookies?: boolean) {
// 图片
const IMAGES = imageList
if (IMAGES.length === 0) return getDefaultImage()
// 为所有图片添加system/img前缀
for (let i = 0; i < IMAGES.length; i++)
for (let i = 0; i < IMAGES.length; i++) {
IMAGES[i] = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(IMAGES[i])}`
if (use_cookies) {
IMAGES[i] += `&use_cookies=${encodeURIComponent(use_cookies)}`
}
}
// canvas
const canvas = canvasRef.value
@@ -137,8 +145,8 @@ async function drawImages(imageList: string[]) {
onMounted(async () => {
if (props.media?.image_list && props.media?.image_list.length > 0)
imgUrl.value = await drawImages(props.media?.image_list || [])
else imgUrl.value = getImgUrl(props.media?.image || '')
imgUrl.value = await drawImages(props.media?.image_list || [], props.media?.use_cookies)
else imgUrl.value = getImgUrl(props.media?.image || '', props.media?.use_cookies)
})
</script>

View File

@@ -6,6 +6,7 @@ import type { Plugin } from '@/api/types'
import { getLogoUrl } from '@/utils/imageUtils'
import { getDominantColor } from '@/@core/utils/image'
import { isNullOrEmptyObject } from '@/@core/utils'
import { formatDownloadCount } from '@/@core/utils/formatters'
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
import { useI18n } from 'vue-i18n'
@@ -244,7 +245,7 @@ const dropdownItems = ref([
</div>
<div v-if="props.count" class="ms-2 flex-shrink-0 download-count align-middle items-center">
<VIcon size="small" icon="mdi-download" />
<span class="text-sm">{{ props.count?.toLocaleString() }}</span>
<span class="text-sm">{{ formatDownloadCount(props.count) }}</span>
</div>
</div>
<div class="absolute bottom-0 right-0">
@@ -327,7 +328,7 @@ const dropdownItems = ref([
}}</VBtn>
<div class="text-xs mt-2" v-if="props.count">
<VIcon icon="mdi-fire" />{{
t('plugin.totalDownloads', { count: props.count?.toLocaleString() })
t('plugin.totalDownloads', { count: formatDownloadCount(props.count) })
}}
</div>
</div>

View File

@@ -6,6 +6,7 @@ import type { Plugin } from '@/api/types'
import { isNullOrEmptyObject } from '@core/utils'
import { getLogoUrl } from '@/utils/imageUtils'
import { getDominantColor } from '@/@core/utils/image'
import { formatDownloadCount } from '@/@core/utils/formatters'
import VersionHistory from '@/components/misc/VersionHistory.vue'
import ProgressDialog from '../dialog/ProgressDialog.vue'
import PluginConfigDialog from '../dialog/PluginConfigDialog.vue'
@@ -492,7 +493,7 @@ watch(
</div>
<span v-if="props.count" class="ms-2 flex-shrink-0 download-count items-center align-middle">
<VIcon size="small" icon="mdi-download" />
<span class="text-sm">{{ props.count?.toLocaleString() }}</span>
<span class="text-sm">{{ formatDownloadCount(props.count) }}</span>
</span>
</div>
<div class="absolute bottom-0 right-0">

View File

@@ -28,7 +28,12 @@ function getChipColor(type: string) {
const getImgUrl = computed(() => {
if (imageLoadError.value) return noImage
const image = props.media?.image || ''
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
let url = `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(image)}`
const use_cookies = props.media?.use_cookies
if (use_cookies) {
url += `&use_cookies=${encodeURIComponent(use_cookies)}`
}
return url
})
// 跳转播放

View File

@@ -6,10 +6,20 @@ import type { DownloaderConf, MediaInfo, TorrentInfo, TransferDirectoryConf } fr
import { formatFileSize } from '@/@core/utils/formatters'
import { VCardTitle, VChip } from 'vuetify/lib/components/index.mjs'
import { useI18n } from 'vue-i18n'
import MediaIdSelector from '../misc/MediaIdSelector.vue'
import { numberValidator } from '@/@validators'
import { useGlobalSettingsStore } from '@/stores'
// 多语言支持
const { t } = useI18n()
// 从 provide 中获取全局设置
const globalSettingsStore = useGlobalSettingsStore()
const globalSettings = globalSettingsStore.globalSettings
// 当前识别类型
const mediaSource = ref(globalSettings.RECOGNIZE_SOURCE || 'themoviedb')
// 输入参数
const props = defineProps({
title: String,
@@ -38,6 +48,18 @@ const directories = ref<TransferDirectoryConf[]>([])
// 是否正在加载
const loading = ref(false)
// 是否显示高级选项
const showAdvancedOptions = ref(false)
// TMDB ID
const tmdbid = ref<number | undefined>(undefined)
// 豆瓣ID
const doubanId = ref<string | undefined>(undefined)
// TMDB选择对话框
const mediaSelectorDialog = ref(false)
// 计算按钮图标
const icon = computed(() => (loading.value ? 'mdi-progress-download' : 'mdi-download'))
@@ -96,6 +118,14 @@ async function addDownload() {
payload.media_in = props.media
}
// 添加媒体ID辅助识别
if (tmdbid.value) {
payload.tmdbid = tmdbid.value
}
if (doubanId.value) {
payload.doubanid = doubanId.value
}
const endpoint = props.media ? 'download/' : 'download/add'
result = await api.post(endpoint, payload)
@@ -202,6 +232,56 @@ onMounted(() => {
/>
</VCol>
</VRow>
<VRow class="px-5 mt-2">
<VCol cols="12">
<VBtn
variant="text"
size="small"
:prepend-icon="showAdvancedOptions ? 'mdi-chevron-up' : 'mdi-chevron-down'"
@click="showAdvancedOptions = !showAdvancedOptions"
>
{{
showAdvancedOptions
? t('dialog.addDownload.hideAdvancedOptions')
: t('dialog.addDownload.showAdvancedOptions')
}}
</VBtn>
</VCol>
</VRow>
<VRow v-show="showAdvancedOptions" class="px-5">
<VCol cols="12">
<VTextField
v-if="mediaSource === 'themoviedb'"
v-model="tmdbid"
:label="t('dialog.reorganize.tmdbId')"
:placeholder="t('dialog.reorganize.mediaIdPlaceholder')"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
:hint="t('dialog.reorganize.mediaIdHint')"
persistent-hint
prepend-inner-icon="mdi-identifier"
size="small"
variant="underlined"
density="comfortable"
@click:append-inner="mediaSelectorDialog = true"
/>
<VTextField
v-else
v-model="doubanId"
:label="t('dialog.reorganize.doubanId')"
:placeholder="t('dialog.reorganize.mediaIdPlaceholder')"
:rules="[numberValidator]"
append-inner-icon="mdi-magnify"
:hint="t('dialog.reorganize.mediaIdHint')"
persistent-hint
prepend-inner-icon="mdi-identifier"
size="small"
variant="underlined"
density="comfortable"
@click:append-inner="mediaSelectorDialog = true"
/>
</VCol>
</VRow>
</VCardText>
<VCardText class="text-center">
<VBtn variant="elevated" :disabled="loading" @click="addDownload" :prepend-icon="icon" class="px-5">
@@ -209,5 +289,15 @@ onMounted(() => {
</VBtn>
</VCardText>
</VCard>
<!-- 媒体ID选择器 -->
<VDialog v-model="mediaSelectorDialog" width="40rem" scrollable max-height="85vh">
<MediaIdSelector
v-if="mediaSource === 'themoviedb'"
v-model="tmdbid"
@close="mediaSelectorDialog = false"
:type="mediaSource"
/>
<MediaIdSelector v-else v-model="doubanId" @close="mediaSelectorDialog = false" :type="mediaSource" />
</VDialog>
</VDialog>
</template>

View File

@@ -91,10 +91,6 @@ onUnmounted(() => {
<!-- Vue 渲染模式 -->
<div v-if="pluginRenderMode === 'vue'">
<component :is="dynamicPluginComponent" :config="props.config" :allow-refresh="props.allowRefresh" :api="api" />
<!-- Vue 模式下也可以显示拖拽句柄 -->
<div class="absolute right-5 top-5">
<VIcon class="cursor-move">mdi-drag</VIcon>
</div>
</div>
<!-- Vuetify 渲染模式 -->
<VHover v-else-if="pluginRenderMode === 'vuetify'">

View File

@@ -187,6 +187,7 @@ export function useSetupWizard() {
'emby': 'EmbyModule',
'jellyfin': 'JellyfinModule',
'plex': 'PlexModule',
'trimemedia': 'TrimeMediaModule',
},
// 通知映射
notification: {

View File

@@ -788,6 +788,8 @@ export default {
originalTitle: 'Original Title',
status: 'Status',
releaseDate: 'Release Date',
digitalRelease: 'Digital Release',
physicalRelease: 'Physical Release',
originalLanguage: 'Original Language',
productionCountries: 'Production Countries',
productionCompanies: 'Production Companies',
@@ -1242,6 +1244,18 @@ export default {
'Used to increase the rate limit threshold when plugins access Github APIit is recommended to configure, otherwise plugins may not work properly',
ocrHost: 'OCR Server',
ocrHostHint: 'Used for site check-in, updating site cookies and other captcha recognition',
aiAgent: 'Enable AI Assistant',
aiAgentEnable: 'Enable AI Assistant',
aiAgentEnableHint: 'Enable AI assistant functionality, requires LLM configuration',
llmProvider: 'LLM Provider',
llmProviderHint: 'Select the LLM service provider to use',
llmModel: 'LLM Model Name',
llmModelHint: 'Specify the LLM model to use, such as gpt-3.5-turbo, deepseek-chat, etc.',
llmApiKey: 'LLM API Key',
llmApiKeyHint: 'API key from the LLM service provider for authentication',
llmApiKeyPlaceholder: 'Please enter API key',
llmBaseUrl: 'LLM Base URL',
llmBaseUrlHint: 'Base URL for LLM API, used for custom API endpoints',
advancedSettings: 'Advanced Settings',
advancedSettingsDesc: 'System advanced settings, only need to be adjusted in special cases',
downloaders: 'Downloaders',
@@ -1900,6 +1914,8 @@ export default {
startDownload: 'Start Download',
downloadSuccess: '{site} {title} downloaded successfully!',
downloadFailed: '{site} {title} download failed: {message}!',
showAdvancedOptions: 'Show Advanced Options',
hideAdvancedOptions: 'Hide Advanced Options',
},
subscribeShare: {
shareSubscription: 'Share Subscription',

View File

@@ -786,6 +786,8 @@ export default {
originalTitle: '原始标题',
status: '状态',
releaseDate: '上映日期',
digitalRelease: '数字发行',
physicalRelease: '实体发行',
originalLanguage: '原始语言',
productionCountries: '出品国家',
productionCompanies: '制作公司',
@@ -1238,6 +1240,18 @@ export default {
githubTokenHint: '用于提高插件等访问Github API时的限流阈值建议配置否则插件可能无法正常使用',
ocrHost: '验证码识别服务器',
ocrHostHint: '用于站点签到、更新站点Cookie等识别验证码',
aiAgent: '启用智能助手',
aiAgentEnable: '启用智能助手',
aiAgentEnableHint: '启用后可使用智能助手功能需要配置LLM相关参数',
llmProvider: 'LLM提供商',
llmProviderHint: '选择使用的LLM服务提供商',
llmModel: 'LLM模型名称',
llmModelHint: '指定使用的LLM模型如gpt-3.5-turbo、deepseek-chat等',
llmApiKey: 'LLM API密钥',
llmApiKeyHint: 'LLM服务提供商的API密钥用于身份验证',
llmApiKeyPlaceholder: '请输入API密钥',
llmBaseUrl: 'LLM基础URL',
llmBaseUrlHint: 'LLM API的基础URL地址用于自定义API端点',
advancedSettings: '高级设置',
advancedSettingsDesc: '系统进阶设置,特殊情况下才需要调整',
downloaders: '下载器',
@@ -1876,6 +1890,8 @@ export default {
startDownload: '开始下载',
downloadSuccess: '{site} {title} 下载成功!',
downloadFailed: '{site} {title} 下载失败:{message}',
showAdvancedOptions: '显示高级选项',
hideAdvancedOptions: '隐藏高级选项',
},
subscribeShare: {
shareSubscription: '分享订阅',

View File

@@ -773,6 +773,8 @@ export default {
originalTitle: '原始標題',
status: '狀態',
releaseDate: '上映日期',
digitalRelease: '數位發行',
physicalRelease: '實體發行',
originalLanguage: '原始語言',
productionCountries: '出品國家',
productionCompanies: '製作公司',
@@ -1226,6 +1228,18 @@ export default {
githubTokenHint: '用於提高插件等訪問Github API時的限流閾值建議配置否則插件可能無法正常使用',
ocrHost: '驗證碼識別服務器',
ocrHostHint: '用於站點簽到、更新站點Cookie等識別驗證碼',
aiAgent: '啟用智能助手',
aiAgentEnable: '啟用智能助手',
aiAgentEnableHint: '啟用後可使用智能助手功能需要配置LLM相關參數',
llmProvider: 'LLM提供商',
llmProviderHint: '選擇使用的LLM服務提供商',
llmModel: 'LLM模型名稱',
llmModelHint: '指定使用的LLM模型如gpt-3.5-turbo、deepseek-chat等',
llmApiKey: 'LLM API密鑰',
llmApiKeyHint: 'LLM服務提供商的API密鑰用於身份驗證',
llmApiKeyPlaceholder: '請輸入API密鑰',
llmBaseUrl: 'LLM基礎URL',
llmBaseUrlHint: 'LLM API的基礎URL地址用於自定義API端點',
advancedSettings: '高級設置',
advancedSettingsDesc: '系統進階設置,特殊情況下才需要調整',
downloaders: '下載器',
@@ -1862,6 +1876,8 @@ export default {
startDownload: '開始下載',
downloadSuccess: '{site} {title} 下載成功!',
downloadFailed: '{site} {title} 下載失敗:{message}',
showAdvancedOptions: '顯示高級選項',
hideAdvancedOptions: '隱藏高級選項',
},
subscribeShare: {
shareSubscription: '分享訂閱',

View File

@@ -428,6 +428,17 @@ const getProductionCompanies = computed(() => {
return mediaDetail.value.production_companies?.map(company => company.name)
})
// 获取最早实体/数字发行日期
const getEarliestReleaseDate = computed(() => {
const filteredDates = mediaDetail.value.release_dates?.filter(date => [4, 5].includes(date.type))
if (!filteredDates || filteredDates.length === 0)
return null
return filteredDates.reduce((earliest, current) =>
new Date(current.date) < new Date(earliest.date) ? current : earliest,
)
})
// 计算存在状态的颜色
function getExistColor(season: number) {
const state = seasonsNotExisted.value[season]
@@ -840,6 +851,17 @@ onBeforeMount(() => {
</span>
</span>
</div>
<div v-if="mediaDetail.type === '电影' && getEarliestReleaseDate" class="media-fact">
<span>{{ t(getEarliestReleaseDate.type === 4 ? 'media.info.digitalRelease' : 'media.info.physicalRelease') }}</span>
<span class="media-fact-value">
<span class="flex items-center justify-end">
<span class="inline-flex items-center justify-center h-4 w-4 text-[0.6rem] font-bold text-current border border-current leading-none">
{{ getEarliestReleaseDate.iso_code }}
</span>
<span class="ml-1.5">{{ getEarliestReleaseDate.date.slice(0, 10) }}</span>
</span>
</span>
</div>
<div v-if="mediaDetail.original_language" class="media-fact">
<span>{{ t('media.info.originalLanguage') }}</span>
<span class="media-fact-value">{{ mediaDetail.original_language }}</span>

View File

@@ -31,6 +31,11 @@ const SystemSettings = ref<any>({
GITHUB_TOKEN: null,
OCR_HOST: null,
CUSTOMIZE_WALLPAPER_API_URL: null,
AI_AGENT_ENABLE: false,
LLM_PROVIDER: 'deepseek',
LLM_MODEL: 'deepseek-chat',
LLM_API_KEY: null,
LLM_BASE_URL: 'https://api.deepseek.com',
},
// 高级系统设置
Advanced: {
@@ -114,6 +119,10 @@ const progressDialog = ref(false)
// 高级设置对话框
const advancedDialog = ref(false)
// LLM 模型列表
const llmModels = ref<string[]>([])
const loadingModels = ref(false)
const activeTab = ref('system')
// 元数据语言
@@ -149,6 +158,30 @@ const logLevelItems = [
// 安全域名添加变量
const newSecurityDomain = ref('')
// 加载LLM模型列表
async function loadLlmModels() {
loadingModels.value = true
try {
const result: { [key: string]: any } = await api.get('system/llm-models', {
params: {
provider: SystemSettings.value.Basic.LLM_PROVIDER,
api_key: SystemSettings.value.Basic.LLM_API_KEY,
base_url: SystemSettings.value.Basic.LLM_BASE_URL,
},
})
if (result.success) {
llmModels.value = result.data
if (llmModels.value.length > 0) SystemSettings.value.Basic.LLM_MODEL = llmModels.value[0]
} else {
$toast.error(result.message)
}
} catch (error) {
console.log(error)
}
loadingModels.value = false
}
// 添加安全域名
function addSecurityDomain() {
if (
@@ -607,6 +640,74 @@ onDeactivated(() => {
/>
</VCol>
</VRow>
<VDivider class="my-4" />
<VRow>
<VCol cols="12">
<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">
<VSelect
v-model="SystemSettings.Basic.LLM_PROVIDER"
:label="t('setting.system.llmProvider')"
:hint="t('setting.system.llmProviderHint')"
persistent-hint
:items="[
{ title: 'OpenAI', value: 'openai' },
{ title: 'Google', value: 'google' },
{ title: 'DeepSeek', value: 'deepseek' },
]"
prepend-inner-icon="mdi-robot"
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<VTextField
v-model="SystemSettings.Basic.LLM_BASE_URL"
:label="t('setting.system.llmBaseUrl')"
:hint="t('setting.system.llmBaseUrlHint')"
placeholder="https://api.deepseek.com"
persistent-hint
prepend-inner-icon="mdi-link"
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
<VTextField
v-model="SystemSettings.Basic.LLM_API_KEY"
:label="t('setting.system.llmApiKey')"
: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" cols="12" md="6">
<VCombobox
v-model="SystemSettings.Basic.LLM_MODEL"
:label="t('setting.system.llmModel')"
:hint="t('setting.system.llmModelHint')"
:placeholder="t('setting.system.llmModelHint')"
persistent-hint
:items="llmModels"
:loading="loadingModels"
prepend-inner-icon="mdi-brain"
>
<template #append-inner>
<VBtn
variant="text"
icon="mdi-refresh"
size="small"
@click="loadLlmModels"
:disabled="!SystemSettings.Basic.LLM_API_KEY"
/>
</template>
</VCombobox>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardText>