Files
MoviePilot-Frontend/src/composables/useSetupWizard.ts

1652 lines
52 KiB
TypeScript
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.
import { ref, computed } from 'vue'
import { useToast } from 'vue-toastification'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import api from '@/api'
import { copyToClipboard } from '@/@core/utils/navigator'
import { User } from '@/api/types'
export interface WizardData {
basic: {
appDomain: string
apiToken: string
username: string
password: string
confirmPassword: string
recognizeSource: string
ocrHost: string
proxyHost: string
githubToken: string
}
siteAuth: {
auxiliaryAuthEnable: boolean
site: string
params: Record<string, string | number>
}
storage: {
downloadPath: string
libraryPath: string
transferType: string
overwriteMode: string
}
downloader: {
type: string
name: string
config: any
}
mediaServer: {
type: string
name: string
config: any
sync_libraries: any[]
switchs: any[]
}
notification: {
type: string
name: string
enabled: boolean
config: any
switchs: any[]
}
agent: {
enabled: boolean
global: boolean
verbose: boolean
provider: string
model: string
thinkingLevel: string
supportImageInput: boolean
supportAudioInputOutput: boolean
apiKey: string
baseUrl: string
maxContextTokens: number
voiceApiKey: string
voiceBaseUrl: string
voiceSttModel: string
voiceTtsModel: string
voiceTtsVoice: string
voiceLanguage: string
voiceReplyWithText: boolean
jobInterval: number
retryTransfer: boolean
recommendEnabled: boolean
recommendUserPreference: string
recommendMaxItems: number
}
preferences: {
quality: string
subtitle: string
resolution: string
personalizationOptions?: {
excludeDolbyVision: boolean
excludeBluray: boolean
}
ruleSequences?: Array<{
name: string
rule_string: string
media_type: string
category: string
}>
}
}
export interface ConnectivityTestState {
isTesting: boolean
testMessage: string
testProgress: number
testResult: 'success' | 'error' | null
showResult: boolean
}
export interface ValidationErrorState {
siteAuth: {
site: boolean
[key: string]: boolean
}
downloader: {
name: boolean
host: boolean
username: boolean
password: boolean
}
mediaServer: {
name: boolean
host: boolean
apikey: boolean
token: boolean
username: boolean
password: boolean
}
notification: {
name: boolean
[key: string]: boolean
}
agent: {
provider: boolean
apiKey: boolean
model: boolean
maxContextTokens: boolean
recommendMaxItems: boolean
}
}
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'
}
// 全局状态,所有组件共享
const currentStep = ref(1)
const totalSteps = 8
// 加载状态
const isLoading = ref(false)
// 选中的预设规则
const selectedPreset = ref('')
// 可认证站点列表
const authSites = ref<{
[key: string]: {
name: string
icon: string
params: {
[key: string]: {
name: string
type: string
placeholder?: string
tooltip?: string
}
}
}
}>({})
// 向导数据
const wizardData = ref<WizardData>({
basic: {
appDomain: '',
apiToken: '',
username: '',
password: '',
confirmPassword: '',
recognizeSource: 'themoviedb',
ocrHost: '',
proxyHost: '',
githubToken: '',
},
siteAuth: {
auxiliaryAuthEnable: false,
site: '',
params: {},
},
storage: {
downloadPath: '',
libraryPath: '',
transferType: 'link',
overwriteMode: 'never',
},
downloader: {
type: '',
name: '',
config: {},
},
mediaServer: {
type: '',
name: '',
config: {},
sync_libraries: [],
switchs: [],
},
notification: {
type: '',
name: '',
enabled: false,
config: {},
switchs: [],
},
agent: {
enabled: false,
global: false,
verbose: false,
provider: 'deepseek',
model: 'deepseek-chat',
thinkingLevel: 'off',
supportImageInput: true,
supportAudioInputOutput: false,
apiKey: '',
baseUrl: 'https://api.deepseek.com',
maxContextTokens: 64,
voiceApiKey: '',
voiceBaseUrl: '',
voiceSttModel: 'gpt-4o-mini-transcribe',
voiceTtsModel: 'gpt-4o-mini-tts',
voiceTtsVoice: 'alloy',
voiceLanguage: 'zh',
voiceReplyWithText: false,
jobInterval: 0,
retryTransfer: false,
recommendEnabled: false,
recommendUserPreference: '',
recommendMaxItems: 50,
},
preferences: {
quality: '4K',
subtitle: 'chinese',
resolution: '2160p',
},
})
// 连通性测试状态
const connectivityTest = ref<ConnectivityTestState>({
isTesting: false,
testMessage: '',
testProgress: 0,
testResult: null,
showResult: false,
})
// 验证错误状态
const validationErrors = ref<ValidationErrorState>({
siteAuth: {
site: false,
},
downloader: {
name: false,
host: false,
username: false,
password: false,
},
mediaServer: {
name: false,
host: false,
apikey: false,
token: false,
username: false,
password: false,
},
notification: {
name: false,
},
agent: {
provider: false,
apiKey: false,
model: false,
maxContextTokens: false,
recommendMaxItems: false,
},
})
export function useSetupWizard() {
const { t } = useI18n()
const router = useRouter()
const $toast = useToast()
// 类型到模块ID的映射关系
const typeToModuleMapping = {
// 下载器映射
downloader: {
'qbittorrent': 'QbittorrentModule',
'transmission': 'TransmissionModule',
'rtorrent': 'RtorrentModule',
},
// 媒体服务器映射
mediaServer: {
'emby': 'EmbyModule',
'jellyfin': 'JellyfinModule',
'plex': 'PlexModule',
'trimemedia': 'TrimeMediaModule',
'ugreen': 'UgreenModule',
},
// 通知映射
notification: {
'telegram': 'TelegramModule',
'wechat': 'WechatModule',
'slack': 'SlackModule',
'synologychat': 'SynologyChatModule',
'qqbot': 'QQBotModule',
'vocechat': 'VoceChatModule',
'webpush': 'WebPushModule',
},
}
// 步骤标题
const stepTitles = computed(() => [
t('setupWizard.basic.title'),
t('setupWizard.siteAuth.title'),
t('setupWizard.storage.title'),
t('setupWizard.downloader.title'),
t('setupWizard.mediaServer.title'),
t('setupWizard.notification.title'),
t('setupWizard.agent.title'),
t('setupWizard.preferences.title'),
])
// 步骤描述
const stepDescriptions = computed(() => [
t('setupWizard.basic.description'),
t('setupWizard.siteAuth.description'),
t('setupWizard.storage.description'),
t('setupWizard.downloader.description'),
t('setupWizard.mediaServer.description'),
t('setupWizard.notification.description'),
t('setupWizard.agent.description'),
t('setupWizard.preferences.description'),
])
// 创建随机API Token
function createRandomString() {
const charset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'
const array = new Uint8Array(32)
window.crypto.getRandomValues(array)
wizardData.value.basic.apiToken = Array.from(array, byte => charset[byte % charset.length]).join('')
}
// 复制到剪贴板
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.error(error)
}
}
// 选择下载器
function selectDownloader(type: string) {
if (wizardData.value.downloader.type === type) {
// 重复点击已选中的类型,取消选择
wizardData.value.downloader.type = ''
} else {
wizardData.value.downloader.type = type
// 如果名称为空或为默认名称,则设置默认名称
if (!wizardData.value.downloader.name || wizardData.value.downloader.name.includes('下载器')) {
wizardData.value.downloader.name = `${type} 下载器`
}
// 不清空config保留用户已输入的值
}
}
// 选择媒体服务器
function selectMediaServer(type: string) {
if (wizardData.value.mediaServer.type === type) {
// 重复点击已选中的类型,取消选择
wizardData.value.mediaServer.type = ''
} else {
wizardData.value.mediaServer.type = type
// 如果名称为空或为默认名称,则设置默认名称
if (!wizardData.value.mediaServer.name || wizardData.value.mediaServer.name.includes('服务器')) {
wizardData.value.mediaServer.name = `${type} 服务器`
}
// 不清空config和sync_libraries保留用户已输入的值
}
}
// 选择通知
function selectNotification(type: string) {
if (wizardData.value.notification.type === type) {
// 重复点击已选中的类型,取消选择
wizardData.value.notification.type = ''
} else {
wizardData.value.notification.type = type
// 如果名称为空或为默认名称,则设置默认名称
if (!wizardData.value.notification.name || wizardData.value.notification.name.includes('通知')) {
wizardData.value.notification.name = `${type} 通知`
}
wizardData.value.notification.enabled = true
// 不清空config和switchs保留用户已输入的值
}
}
// 选择预设规则
function selectPreset(preset: string) {
selectedPreset.value = preset
switch (preset) {
case '4k':
wizardData.value.preferences.quality = '4K'
wizardData.value.preferences.subtitle = 'bilingual'
wizardData.value.preferences.resolution = '2160p'
break
case 'balanced':
wizardData.value.preferences.quality = '1080P'
wizardData.value.preferences.subtitle = 'chinese'
wizardData.value.preferences.resolution = '1080p'
break
case 'chinese':
wizardData.value.preferences.quality = '1080P'
wizardData.value.preferences.subtitle = 'chinese'
wizardData.value.preferences.resolution = '1080p'
break
}
}
// 更新偏好设置
function updatePreferences(
personalizationOptions: { excludeDolbyVision: boolean; excludeBluray: boolean },
ruleSequences: Array<{ name: string; rule_string: string; media_type: string; category: string }>,
) {
wizardData.value.preferences.personalizationOptions = personalizationOptions
wizardData.value.preferences.ruleSequences = ruleSequences
}
// 清除验证错误状态
function clearValidationErrors() {
validationErrors.value.siteAuth = {
site: false,
}
validationErrors.value.downloader = {
name: false,
host: false,
username: false,
password: false,
}
validationErrors.value.mediaServer = {
name: false,
host: false,
apikey: false,
token: false,
username: false,
password: false,
}
validationErrors.value.notification = {
name: false,
}
validationErrors.value.agent = {
provider: false,
apiKey: false,
model: false,
maxContextTokens: false,
recommendMaxItems: false,
}
}
// 验证用户站点认证字段
function validateSiteAuthFields(): { isValid: boolean; errors: string[] } {
const errors: string[] = []
clearValidationErrors()
if (!wizardData.value.siteAuth.site) {
return {
isValid: true,
errors,
}
}
const selectedSite = authSites.value[wizardData.value.siteAuth.site]
if (!selectedSite) {
errors.push(t('setupWizard.siteAuth.siteConfigNotExist'))
validationErrors.value.siteAuth.site = true
return {
isValid: false,
errors,
}
}
const fields = Object.keys(selectedSite.params || {}).filter(key => {
return selectedSite.params[key]?.name && selectedSite.params[key]?.type
})
fields.forEach(key => {
const fieldKey = `${wizardData.value.siteAuth.site.toUpperCase()}_${key.toUpperCase()}`
const value = wizardData.value.siteAuth.params[fieldKey]
if (value === undefined || value === null || value === '') {
errors.push(t('setupWizard.siteAuth.fieldRequired', { name: selectedSite.params[key].name }))
validationErrors.value.siteAuth[fieldKey] = true
}
})
return {
isValid: errors.length === 0,
errors,
}
}
// 验证下载器字段
function validateDownloaderFields(): { isValid: boolean; errors: string[] } {
const errors: string[] = []
clearValidationErrors()
// 名称必输
if (!wizardData.value.downloader.name?.trim()) {
errors.push(t('downloader.nameRequired'))
validationErrors.value.downloader.name = true
}
// 主机地址必输
if (!wizardData.value.downloader.config?.host?.trim()) {
errors.push(t('downloader.hostRequired'))
validationErrors.value.downloader.host = true
}
// 根据下载器类型验证其他必输项
if (
wizardData.value.downloader.type === 'qbittorrent'
|| wizardData.value.downloader.type === 'transmission'
|| wizardData.value.downloader.type === 'rtorrent'
) {
if (!wizardData.value.downloader.config?.username?.trim()) {
errors.push(t('downloader.usernameRequired'))
validationErrors.value.downloader.username = true
}
if (!wizardData.value.downloader.config?.password?.trim()) {
errors.push(t('downloader.passwordRequired'))
validationErrors.value.downloader.password = true
}
}
return {
isValid: errors.length === 0,
errors,
}
}
// 验证媒体服务器字段
function validateMediaServerFields(): { isValid: boolean; errors: string[] } {
const errors: string[] = []
clearValidationErrors()
// 名称必输
if (!wizardData.value.mediaServer.name?.trim()) {
errors.push(t('mediaserver.nameRequired'))
validationErrors.value.mediaServer.name = true
}
// 主机地址必输
if (!wizardData.value.mediaServer.config?.host?.trim()) {
errors.push(t('mediaserver.hostRequired'))
validationErrors.value.mediaServer.host = true
}
// 根据媒体服务器类型验证API密钥或Token
if (wizardData.value.mediaServer.type === 'emby' || wizardData.value.mediaServer.type === 'jellyfin') {
if (!wizardData.value.mediaServer.config?.apikey?.trim()) {
errors.push(t('mediaserver.apiKeyRequired'))
validationErrors.value.mediaServer.apikey = true
}
} else if (wizardData.value.mediaServer.type === 'plex') {
if (!wizardData.value.mediaServer.config?.token?.trim()) {
errors.push(t('mediaserver.tokenRequired'))
validationErrors.value.mediaServer.token = true
}
} else if (wizardData.value.mediaServer.type === 'trimemedia' || wizardData.value.mediaServer.type === 'ugreen') {
if (!wizardData.value.mediaServer.config?.username?.trim()) {
errors.push(t('mediaserver.usernameRequired'))
validationErrors.value.mediaServer.username = true
}
if (!wizardData.value.mediaServer.config?.password?.trim()) {
errors.push(t('mediaserver.passwordRequired'))
validationErrors.value.mediaServer.password = true
}
}
return {
isValid: errors.length === 0,
errors,
}
}
// 验证通知字段
function validateNotificationFields(): { isValid: boolean; errors: string[] } {
const errors: string[] = []
clearValidationErrors()
// 名称必输
if (!wizardData.value.notification.name?.trim()) {
errors.push(t('notification.nameRequired'))
validationErrors.value.notification.name = true
}
// 根据通知类型验证必输项
const config = wizardData.value.notification.config || {}
switch (wizardData.value.notification.type) {
case 'wechat':
if (!config.WECHAT_CORPID?.trim()) {
errors.push(t('notification.wechat.corpIdRequired'))
validationErrors.value.notification.WECHAT_CORPID = true
}
if (!config.WECHAT_APP_ID?.trim()) {
errors.push(t('notification.wechat.appIdRequired'))
validationErrors.value.notification.WECHAT_APP_ID = true
}
if (!config.WECHAT_APP_SECRET?.trim()) {
errors.push(t('notification.wechat.appSecretRequired'))
validationErrors.value.notification.WECHAT_APP_SECRET = true
}
break
case 'telegram':
if (!config.TELEGRAM_TOKEN?.trim()) {
errors.push(t('notification.telegram.tokenRequired'))
validationErrors.value.notification.TELEGRAM_TOKEN = true
}
if (!config.TELEGRAM_CHAT_ID?.trim()) {
errors.push(t('notification.telegram.chatIdRequired'))
validationErrors.value.notification.TELEGRAM_CHAT_ID = true
}
break
case 'slack':
if (!config.SLACK_OAUTH_TOKEN?.trim()) {
errors.push(t('notification.slack.oauthTokenRequired'))
validationErrors.value.notification.SLACK_OAUTH_TOKEN = true
}
if (!config.SLACK_CHANNEL?.trim()) {
errors.push(t('notification.slack.channelRequired'))
validationErrors.value.notification.SLACK_CHANNEL = true
}
break
case 'synologychat':
if (!config.SYNOLOGYCHAT_WEBHOOK?.trim()) {
errors.push(t('notification.synologychat.webhookRequired'))
validationErrors.value.notification.SYNOLOGYCHAT_WEBHOOK = true
}
break
case 'vocechat':
if (!config.VOCECHAT_HOST?.trim()) {
errors.push(t('notification.vocechat.hostRequired'))
validationErrors.value.notification.VOCECHAT_HOST = true
}
if (!config.VOCECHAT_API_KEY?.trim()) {
errors.push(t('notification.vocechat.apiKeyRequired'))
validationErrors.value.notification.VOCECHAT_API_KEY = true
}
break
case 'webpush':
if (!config.WEBPUSH_USERNAME?.trim()) {
errors.push(t('notification.webpush.usernameRequired'))
validationErrors.value.notification.WEBPUSH_USERNAME = true
}
break
case 'qqbot':
if (!config.QQ_APP_ID?.trim()) {
errors.push(t('notification.qqbot.appIdRequired'))
validationErrors.value.notification.QQ_APP_ID = true
}
if (!config.QQ_APP_SECRET?.trim()) {
errors.push(t('notification.qqbot.appSecretRequired'))
validationErrors.value.notification.QQ_APP_SECRET = true
}
break
}
return {
isValid: errors.length === 0,
errors,
}
}
// 验证智能助手字段
function validateAgentFields(): { isValid: boolean; errors: string[] } {
const errors: string[] = []
clearValidationErrors()
if (!wizardData.value.agent.enabled) {
return {
isValid: true,
errors,
}
}
if (!wizardData.value.agent.provider?.trim()) {
errors.push(t('setupWizard.agent.providerRequired'))
validationErrors.value.agent.provider = true
}
if (!wizardData.value.agent.apiKey?.trim()) {
errors.push(t('setupWizard.agent.apiKeyRequired'))
validationErrors.value.agent.apiKey = true
}
if (!wizardData.value.agent.model?.trim()) {
errors.push(t('setupWizard.agent.modelRequired'))
validationErrors.value.agent.model = true
}
if (!wizardData.value.agent.maxContextTokens || wizardData.value.agent.maxContextTokens < 1) {
errors.push(t('setupWizard.agent.maxContextTokensRequired'))
validationErrors.value.agent.maxContextTokens = true
}
if (wizardData.value.agent.recommendEnabled && (!wizardData.value.agent.recommendMaxItems || wizardData.value.agent.recommendMaxItems < 1)) {
errors.push(t('setupWizard.agent.recommendMaxItemsRequired'))
validationErrors.value.agent.recommendMaxItems = true
}
return {
isValid: errors.length === 0,
errors,
}
}
// 验证当前步骤的必输项
function validateCurrentStep(): { isValid: boolean; errors: string[] } {
const errors: string[] = []
switch (currentStep.value) {
case 1: // 基础设置
if (!wizardData.value.basic.username) {
errors.push(t('dialog.userAddEdit.usernameRequired'))
}
// 密码是可选的,但如果输入了密码则需要验证
if (wizardData.value.basic.password) {
if (wizardData.value.basic.password.length < 6) {
errors.push(t('dialog.userAddEdit.passwordMinLength'))
}
if (!wizardData.value.basic.confirmPassword) {
errors.push(t('dialog.userAddEdit.confirmPasswordRequired'))
} else if (wizardData.value.basic.password !== wizardData.value.basic.confirmPassword) {
errors.push(t('dialog.userAddEdit.passwordMismatch'))
}
}
if (!wizardData.value.basic.apiToken) {
errors.push(t('setupWizard.basic.apiTokenRequired'))
}
break
case 2: // 存储设置
if (wizardData.value.siteAuth.site) {
const validation = validateSiteAuthFields()
errors.push(...validation.errors)
}
break
case 3: // 存储设置
if (!wizardData.value.storage.downloadPath) {
errors.push(t('setupWizard.storage.downloadPathRequired'))
}
if (!wizardData.value.storage.libraryPath) {
errors.push(t('setupWizard.storage.libraryPathRequired'))
}
break
case 4: // 下载器设置
if (wizardData.value.downloader.type) {
// 如果选择了下载器,则验证必输项
const validation = validateDownloaderFields()
errors.push(...validation.errors)
}
break
case 5: // 媒体服务器设置
if (wizardData.value.mediaServer.type) {
// 如果选择了媒体服务器,则验证必输项
const validation = validateMediaServerFields()
errors.push(...validation.errors)
}
break
case 6: // 通知设置
if (wizardData.value.notification.type) {
// 如果选择了通知,则验证必输项
const validation = validateNotificationFields()
errors.push(...validation.errors)
}
break
case 7: // 智能助手设置
if (wizardData.value.agent.enabled) {
const validation = validateAgentFields()
errors.push(...validation.errors)
}
break
case 8: // 偏好设置
// 偏好设置有默认值,不需要验证
break
}
return {
isValid: errors.length === 0,
errors,
}
}
// 检查是否需要进行测试
function shouldPerformTest(step: number): boolean {
switch (step) {
case 2: // 存储目录测试 - 总是需要测试
return false
case 3: // 存储目录测试 - 总是需要测试
return true
case 4: // 下载器测试 - 只有选择了下载器才测试
return !!wizardData.value.downloader.type
case 5: // 媒体服务器测试 - 只有选择了媒体服务器才测试
return !!wizardData.value.mediaServer.type
case 6: // 消息通知测试 - 只有选择了通知才测试
return !!wizardData.value.notification.type
default:
return false
}
}
// 连通性测试函数
async function testConnectivity(step: number) {
connectivityTest.value.isTesting = true
connectivityTest.value.testMessage = ''
connectivityTest.value.testProgress = 0
connectivityTest.value.testResult = null
connectivityTest.value.showResult = false
try {
let testResult: { success: boolean; message: string | null } = { success: false, message: null }
switch (step) {
case 2: // 存储目录测试
break
case 3: // 存储目录测试
testResult = await testStorageConnectivity()
break
case 4: // 下载器测试
testResult = await testDownloaderConnectivity()
break
case 5: // 媒体服务器测试
testResult = await testMediaServerConnectivity()
break
case 6: // 消息通知测试
testResult = await testNotificationConnectivity()
break
}
// 设置测试结果
connectivityTest.value.isTesting = false
connectivityTest.value.testResult = testResult.success ? 'success' : 'error'
connectivityTest.value.showResult = true
// 根据结果显示不同的消息
if (testResult.success) {
connectivityTest.value.testMessage = t('setupWizard.connectivityTestSuccess')
} else {
// 显示API返回的具体错误原因
connectivityTest.value.testMessage = testResult.message || t('setupWizard.connectivityTestFailed')
}
// 成功时2秒后隐藏结果失败时保持显示直到用户操作
if (testResult.success) {
connectivityTest.value.showResult = false
connectivityTest.value.testResult = null
}
return testResult.success
} catch (error) {
console.error('Connectivity test failed:', error)
connectivityTest.value.isTesting = false
connectivityTest.value.testResult = 'error'
connectivityTest.value.showResult = true
connectivityTest.value.testMessage = (error as Error).message || t('setupWizard.connectivityTestFailed')
return false
}
}
// 存储目录连通性测试
async function testStorageConnectivity() {
try {
connectivityTest.value.testProgress = 30
connectivityTest.value.testMessage = t('setupWizard.testingStorage')
// 等待设置生效
await new Promise(resolve => setTimeout(resolve, 2000))
connectivityTest.value.testProgress = 60
connectivityTest.value.testMessage = t('setupWizard.checkingStorage')
// 调用存储测试API - 使用FileManagerModule
const result: { [key: string]: any } = await api.get('system/moduletest/FileManagerModule')
connectivityTest.value.testProgress = 100
if (result.success) {
return { success: true, message: null }
} else {
return { success: false, message: result.message || t('setupWizard.storageTestFailed') }
}
} catch (error) {
console.error('Storage test failed:', error)
return { success: false, message: (error as Error).message || t('setupWizard.storageTestFailed') }
}
}
// 下载器连通性测试
async function testDownloaderConnectivity() {
try {
connectivityTest.value.testProgress = 30
connectivityTest.value.testMessage = t('setupWizard.testingDownloader')
// 等待设置生效
await new Promise(resolve => setTimeout(resolve, 2000))
connectivityTest.value.testProgress = 60
connectivityTest.value.testMessage = t('setupWizard.checkingDownloader')
// 获取正确的模块ID
const downloaderType = wizardData.value.downloader.type
if (!downloaderType) {
return { success: false, message: t('setupWizard.downloaderNotSelected') }
}
const moduleid = typeToModuleMapping.downloader[downloaderType as keyof typeof typeToModuleMapping.downloader]
if (!moduleid) {
return { success: false, message: t('setupWizard.unsupportedDownloaderType', { type: downloaderType }) }
}
const result: { [key: string]: any } = await api.get(`system/moduletest/${moduleid}`)
connectivityTest.value.testProgress = 100
if (result.success) {
return { success: true, message: null }
} else {
return { success: false, message: result.message || t('setupWizard.downloaderTestFailed') }
}
} catch (error) {
console.error('Downloader test failed:', error)
return { success: false, message: (error as Error).message || t('setupWizard.downloaderTestFailed') }
}
}
// 媒体服务器连通性测试
async function testMediaServerConnectivity() {
try {
connectivityTest.value.testProgress = 30
connectivityTest.value.testMessage = t('setupWizard.testingMediaServer')
// 等待设置生效
await new Promise(resolve => setTimeout(resolve, 2000))
connectivityTest.value.testProgress = 60
connectivityTest.value.testMessage = t('setupWizard.checkingMediaServer')
// 获取正确的模块ID
const mediaServerType = wizardData.value.mediaServer.type
if (!mediaServerType) {
return { success: false, message: t('setupWizard.mediaServerNotSelected') }
}
const moduleid = typeToModuleMapping.mediaServer[mediaServerType as keyof typeof typeToModuleMapping.mediaServer]
if (!moduleid) {
return { success: false, message: t('setupWizard.unsupportedMediaServerType', { type: mediaServerType }) }
}
const result: { [key: string]: any } = await api.get(`system/moduletest/${moduleid}`)
connectivityTest.value.testProgress = 100
if (result.success) {
return { success: true, message: null }
} else {
return { success: false, message: result.message || t('setupWizard.mediaServerTestFailed') }
}
} catch (error) {
console.error('Media server test failed:', error)
return { success: false, message: (error as Error).message || t('setupWizard.mediaServerTestFailed') }
}
}
// 消息通知连通性测试
async function testNotificationConnectivity() {
try {
connectivityTest.value.testProgress = 30
connectivityTest.value.testMessage = t('setupWizard.testingNotification')
// 等待设置生效
await new Promise(resolve => setTimeout(resolve, 2000))
connectivityTest.value.testProgress = 60
connectivityTest.value.testMessage = t('setupWizard.checkingNotification')
// 获取正确的模块ID
const notificationType = wizardData.value.notification.type
if (!notificationType) {
return { success: false, message: t('setupWizard.notificationNotSelected') }
}
const moduleid =
typeToModuleMapping.notification[notificationType as keyof typeof typeToModuleMapping.notification]
if (!moduleid) {
return { success: false, message: t('setupWizard.unsupportedNotificationType', { type: notificationType }) }
}
const result: { [key: string]: any } = await api.get(`system/moduletest/${moduleid}`)
connectivityTest.value.testProgress = 100
if (result.success) {
return { success: true, message: null }
} else {
return { success: false, message: result.message || t('setupWizard.notificationTestFailed') }
}
} catch (error) {
console.error('Notification test failed:', error)
return { success: false, message: (error as Error).message || t('setupWizard.notificationTestFailed') }
}
}
// 下一步
async function nextStep() {
// 验证当前步骤的必输项
const validation = validateCurrentStep()
if (!validation.isValid) {
// 显示验证错误
validation.errors.forEach(error => {
$toast.error(error)
})
return false
}
// 保存当前步骤的设置
const saved = await saveCurrentStepSettings()
if (!saved) {
return false
}
// 检查是否需要进行测试
const needsTest = shouldPerformTest(currentStep.value)
if (needsTest) {
const testResult = await testConnectivity(currentStep.value)
if (!testResult) {
return false
}
}
// 如果不是最后一步,则前进到下一步
if (currentStep.value < totalSteps) {
currentStep.value++
connectivityTest.value.showResult = false
}
return true
}
// 上一步
function prevStep() {
if (currentStep.value > 1) {
currentStep.value--
}
connectivityTest.value.showResult = false
}
// 保存当前步骤的设置
async function saveCurrentStepSettings() {
try {
switch (currentStep.value) {
case 1:
return await saveBasicSettings()
case 2:
return await saveSiteAuthSettings()
case 3:
return await saveStorageSettings()
case 4:
return await saveDownloaderSettings()
case 5:
return await saveMediaServerSettings()
case 6:
return await saveNotificationSettings()
case 7:
return await saveAgentSettings()
case 8:
return await savePreferenceSettings()
}
} catch (error) {
console.error('Save current step settings failed:', error)
$toast.error(t('setupWizard.saveStepFailed'))
return false
}
return true
}
// 完成向导
async function completeWizard() {
try {
// 先处理下一步(保存当前步骤设置)
const saved = await nextStep()
if (!saved) {
return
}
// 保存设置向导完成状态
await saveSetupWizardState()
$toast.success(t('setupWizard.completed'))
router.push('/')
} catch (error) {
console.error('Setup wizard failed:', error)
$toast.error(t('setupWizard.failed'))
}
}
// 更新用户密码
async function updateUserPassword() {
if (wizardData.value.basic.username && wizardData.value.basic.password) {
try {
// 获取当前用户信息
const currentUser: User = await api.get('user/current')
if (currentUser) {
// 更新现有用户的密码
const userData = {
name: wizardData.value.basic.username,
password: wizardData.value.basic.password,
is_active: currentUser.is_active,
is_superuser: currentUser.is_superuser,
}
await api.put(`user/${currentUser.id}`, userData)
} else {
// 如果用户不存在,创建新用户(通常不会发生)
const userData = {
name: wizardData.value.basic.username,
password: wizardData.value.basic.password,
is_active: true,
is_superuser: true,
}
await api.post('user/', userData)
}
} catch (error) {
console.error('Update user password failed:', error)
throw error
}
}
}
// 保存基础设置
async function saveBasicSettings() {
try {
const basicSettings = {
APP_DOMAIN: wizardData.value.basic.appDomain,
API_TOKEN: wizardData.value.basic.apiToken,
RECOGNIZE_SOURCE: 'themoviedb',
OCR_HOST: wizardData.value.basic.ocrHost,
PROXY_HOST: wizardData.value.basic.proxyHost,
GITHUB_TOKEN: wizardData.value.basic.githubToken,
}
// 保存基础设置
const response: { [key: string]: any } = await api.post('system/env', basicSettings)
if (!response.success) {
return false
}
// 如果输入了密码,验证密码一致性
if (wizardData.value.basic.password) {
if (wizardData.value.basic.password !== wizardData.value.basic.confirmPassword) {
$toast.error(t('dialog.userAddEdit.passwordMismatch'))
return false
}
// 更新用户密码
await updateUserPassword()
}
return true
} catch (error) {
console.error('Save basic settings failed:', error)
$toast.error(t('setupWizard.saveBasicSettingsFailed'))
return false
}
}
// 保存存储配置
async function saveStorageSettings() {
try {
// 创建本地存储
const storage = {
name: '本地存储',
type: 'local',
config: {},
}
await api.post('system/setting/Storages', [storage])
// 创建目录配置
const directory = {
name: '默认目录',
storage: 'local',
library_storage: 'local',
download_path: wizardData.value.storage.downloadPath,
library_path: wizardData.value.storage.libraryPath,
priority: 0,
monitor_type: 'downloader',
media_type: '',
media_category: '',
download_type_folder: true,
download_category_folder: true,
transfer_type: wizardData.value.storage.transferType,
overwrite_mode: wizardData.value.storage.overwriteMode,
renaming: true,
scraping: true,
notify: true,
library_type_folder: true,
library_category_folder: true,
}
await api.post('system/setting/Directories', [directory])
return true
} catch (error) {
console.error('Save storage settings failed:', error)
$toast.error(t('setupWizard.saveStorageSettingsFailed'))
return false
}
}
// 保存用户站点认证设置
async function saveSiteAuthSettings() {
try {
const envResponse: { [key: string]: any } = await api.post('system/env', {
AUXILIARY_AUTH_ENABLE: wizardData.value.siteAuth.auxiliaryAuthEnable,
})
if (!envResponse.success) {
return false
}
if (!wizardData.value.siteAuth.site) {
return true
}
const response: { [key: string]: any } = await api.post('site/auth', {
site: wizardData.value.siteAuth.site,
params: wizardData.value.siteAuth.params,
})
if (!response.success) {
$toast.error(t('setupWizard.saveSiteAuthSettingsFailed', { message: response.message }))
return false
}
return true
} catch (error) {
console.error('Save site auth settings failed:', error)
$toast.error(t('setupWizard.saveSiteAuthSettingsFailed', { message: (error as Error).message || '' }))
return false
}
}
// 保存下载器配置
async function saveDownloaderSettings() {
if (wizardData.value.downloader.type) {
try {
// 只保存当前选中类型的配置
const config = { ...wizardData.value.downloader.config }
const downloader = {
name: wizardData.value.downloader.name,
type: wizardData.value.downloader.type,
default: true,
enabled: true,
config: config,
}
await api.post('system/setting/Downloaders', [downloader])
return true
} catch (error) {
console.error('Save downloader settings failed:', error)
$toast.error(t('setupWizard.saveDownloaderSettingsFailed'))
return false
}
} else {
// 没有选择下载器时,清空现有配置
console.log('No downloader selected, skipping save')
return true
}
}
// 保存媒体服务器配置
async function saveMediaServerSettings() {
if (wizardData.value.mediaServer.type) {
try {
// 只保存当前选中类型的配置
const config = { ...wizardData.value.mediaServer.config }
const sync_libraries = [...(wizardData.value.mediaServer.sync_libraries || [])]
const mediaServer = {
name: wizardData.value.mediaServer.name,
type: wizardData.value.mediaServer.type,
enabled: true,
config: config,
sync_libraries: sync_libraries,
}
await api.post('system/setting/MediaServers', [mediaServer])
return true
} catch (error) {
console.error('Save media server settings failed:', error)
$toast.error(t('setupWizard.saveMediaServerSettingsFailed'))
return false
}
} else {
// 没有选择媒体服务器时,清空现有配置
console.log('No media server selected, skipping save')
return true
}
}
// 保存通知配置
async function saveNotificationSettings() {
if (wizardData.value.notification.type) {
try {
// 只保存当前选中类型的配置
const config = { ...wizardData.value.notification.config }
const switchs = [...(wizardData.value.notification.switchs || [])]
const notification = {
name: wizardData.value.notification.name,
type: wizardData.value.notification.type,
enabled: wizardData.value.notification.enabled,
config: config,
switchs: switchs,
}
await api.post('system/setting/Notifications', [notification])
return true
} catch (error) {
console.error('Save notification settings failed:', error)
$toast.error(t('setupWizard.saveNotificationSettingsFailed'))
return false
}
} else {
// 没有选择通知时,清空现有配置
console.log('No notification selected, skipping save')
return true
}
}
// 保存智能助手设置
async function saveAgentSettings() {
try {
const agentSettings = {
AI_AGENT_ENABLE: wizardData.value.agent.enabled,
AI_AGENT_GLOBAL: wizardData.value.agent.enabled ? wizardData.value.agent.global : false,
AI_AGENT_VERBOSE: wizardData.value.agent.enabled ? wizardData.value.agent.verbose : false,
LLM_PROVIDER: wizardData.value.agent.provider,
LLM_MODEL: wizardData.value.agent.model,
LLM_THINKING_LEVEL: wizardData.value.agent.thinkingLevel,
LLM_SUPPORT_IMAGE_INPUT: wizardData.value.agent.supportImageInput,
LLM_SUPPORT_AUDIO_INPUT_OUTPUT: wizardData.value.agent.supportAudioInputOutput,
LLM_API_KEY: wizardData.value.agent.apiKey,
LLM_BASE_URL: wizardData.value.agent.baseUrl || null,
LLM_MAX_CONTEXT_TOKENS: wizardData.value.agent.maxContextTokens,
AI_VOICE_API_KEY: wizardData.value.agent.voiceApiKey || null,
AI_VOICE_BASE_URL: wizardData.value.agent.voiceBaseUrl || null,
AI_VOICE_STT_MODEL: wizardData.value.agent.voiceSttModel,
AI_VOICE_TTS_MODEL: wizardData.value.agent.voiceTtsModel,
AI_VOICE_TTS_VOICE: wizardData.value.agent.voiceTtsVoice,
AI_VOICE_LANGUAGE: wizardData.value.agent.voiceLanguage,
AI_VOICE_REPLY_WITH_TEXT: wizardData.value.agent.voiceReplyWithText,
AI_AGENT_JOB_INTERVAL: wizardData.value.agent.enabled ? wizardData.value.agent.jobInterval : 0,
AI_AGENT_RETRY_TRANSFER: wizardData.value.agent.enabled ? wizardData.value.agent.retryTransfer : false,
AI_RECOMMEND_ENABLED:
wizardData.value.agent.enabled && wizardData.value.agent.recommendEnabled,
AI_RECOMMEND_USER_PREFERENCE: wizardData.value.agent.recommendUserPreference,
AI_RECOMMEND_MAX_ITEMS: wizardData.value.agent.recommendMaxItems,
}
await api.post('system/env', agentSettings)
return true
} catch (error) {
console.error('Save agent settings failed:', error)
$toast.error(t('setupWizard.saveAgentSettingsFailed'))
return false
}
}
// 保存资源偏好设置
async function savePreferenceSettings() {
try {
// 如果有自定义规则序列,保存到用户过滤规则组
if (wizardData.value.preferences.ruleSequences && wizardData.value.preferences.ruleSequences.length > 0) {
try {
// 保存当前选中的规则组到 UserFilterRuleGroups
const filterResponse: { [key: string]: any } = await api.post(
'system/setting/UserFilterRuleGroups',
wizardData.value.preferences.ruleSequences,
)
if (filterResponse.success) {
// 保存规则组名称到其他设置
const ruleGroupNames = wizardData.value.preferences.ruleSequences.map(rule => [rule.name])
// 保存到 SubscribeFilterRuleGroups
await api.post('system/setting/SubscribeFilterRuleGroups', ruleGroupNames)
// 保存到 BestVersionFilterRuleGroups
await api.post('system/setting/BestVersionFilterRuleGroups', ruleGroupNames)
}
} catch (error) {
console.error('Save rule sequences failed:', error)
}
}
return true
} catch (error) {
console.error('Save preference settings failed:', error)
$toast.error(t('setupWizard.savePreferenceSettingsFailed'))
return false
}
}
// 保存设置向导完成状态
async function saveSetupWizardState() {
try {
const response: { [key: string]: any } = await api.post('system/setting/SetupWizardState', '1')
if (response.success) {
console.log('Setup wizard state saved successfully')
}
} catch (error) {
console.error('Save setup wizard state failed:', error)
// 这里不显示错误提示,因为向导状态保存失败不应该阻止用户完成向导
}
}
// 加载系统设置
async function loadSystemSettings() {
try {
const result: { [key: string]: any } = await api.get('system/env')
if (result.success) {
// 加载基础设置
if (result.data.APP_DOMAIN) {
wizardData.value.basic.appDomain = result.data.APP_DOMAIN
}
if (result.data.API_TOKEN) {
wizardData.value.basic.apiToken = result.data.API_TOKEN
}
if (result.data.PROXY_HOST) {
wizardData.value.basic.proxyHost = result.data.PROXY_HOST
}
if (result.data.OCR_HOST) {
wizardData.value.basic.ocrHost = result.data.OCR_HOST
}
if (result.data.GITHUB_TOKEN) {
wizardData.value.basic.githubToken = result.data.GITHUB_TOKEN
}
wizardData.value.siteAuth.auxiliaryAuthEnable = Boolean(result.data.AUXILIARY_AUTH_ENABLE)
if (result.data.SUPERUSER) {
wizardData.value.basic.username = result.data.SUPERUSER
}
wizardData.value.agent.enabled = Boolean(result.data.AI_AGENT_ENABLE)
wizardData.value.agent.global = Boolean(result.data.AI_AGENT_GLOBAL)
wizardData.value.agent.verbose = Boolean(result.data.AI_AGENT_VERBOSE)
wizardData.value.agent.provider = result.data.LLM_PROVIDER || 'deepseek'
wizardData.value.agent.model = result.data.LLM_MODEL || ''
wizardData.value.agent.thinkingLevel = resolveThinkingLevelValue(result.data)
wizardData.value.agent.supportImageInput = result.data.LLM_SUPPORT_IMAGE_INPUT ?? true
wizardData.value.agent.supportAudioInputOutput = Boolean(result.data.LLM_SUPPORT_AUDIO_INPUT_OUTPUT)
wizardData.value.agent.apiKey = result.data.LLM_API_KEY || ''
wizardData.value.agent.baseUrl = result.data.LLM_BASE_URL || ''
wizardData.value.agent.maxContextTokens = result.data.LLM_MAX_CONTEXT_TOKENS || 64
wizardData.value.agent.voiceApiKey = result.data.AI_VOICE_API_KEY || ''
wizardData.value.agent.voiceBaseUrl = result.data.AI_VOICE_BASE_URL || ''
wizardData.value.agent.voiceSttModel = result.data.AI_VOICE_STT_MODEL || 'gpt-4o-mini-transcribe'
wizardData.value.agent.voiceTtsModel = result.data.AI_VOICE_TTS_MODEL || 'gpt-4o-mini-tts'
wizardData.value.agent.voiceTtsVoice = result.data.AI_VOICE_TTS_VOICE || 'alloy'
wizardData.value.agent.voiceLanguage = result.data.AI_VOICE_LANGUAGE || 'zh'
wizardData.value.agent.voiceReplyWithText = Boolean(result.data.AI_VOICE_REPLY_WITH_TEXT)
wizardData.value.agent.jobInterval = result.data.AI_AGENT_JOB_INTERVAL || 0
wizardData.value.agent.retryTransfer = Boolean(result.data.AI_AGENT_RETRY_TRANSFER)
wizardData.value.agent.recommendEnabled = Boolean(result.data.AI_RECOMMEND_ENABLED)
wizardData.value.agent.recommendUserPreference = result.data.AI_RECOMMEND_USER_PREFERENCE || ''
wizardData.value.agent.recommendMaxItems = result.data.AI_RECOMMEND_MAX_ITEMS || 50
// 如果没有API Token则创建一个随机的
if (!wizardData.value.basic.apiToken) {
createRandomString()
}
}
} catch (error) {
console.log('Load system settings failed:', error)
}
}
// 加载用户站点认证列表
async function loadAuthSites() {
try {
authSites.value = (await api.get('site/auth')) || {}
} catch (error) {
console.log('Load auth sites failed:', error)
}
}
// 加载用户站点认证设置
async function loadSiteAuthSettings() {
try {
const result: { [key: string]: any } = await api.get('system/setting/UserSiteAuthParams')
if (result.success && result.data?.value) {
wizardData.value.siteAuth.site = result.data.value.site || ''
wizardData.value.siteAuth.params = result.data.value.params || {}
}
} catch (error) {
console.log('Load site auth settings failed:', error)
}
}
// 加载存储设置
async function loadStorageSettings() {
try {
const result: { [key: string]: any } = await api.get('system/setting/Directories')
if (result.success && result.data?.value && result.data.value.length > 0) {
const directory = result.data.value[0]
wizardData.value.storage.downloadPath = directory.download_path || ''
wizardData.value.storage.libraryPath = directory.library_path || ''
wizardData.value.storage.transferType = directory.transfer_type || 'link'
wizardData.value.storage.overwriteMode = directory.overwrite_mode || 'never'
}
} catch (error) {
console.log('Load storage settings failed:', error)
}
}
// 加载下载器设置
async function loadDownloaderSettings() {
try {
const result: { [key: string]: any } = await api.get('system/setting/Downloaders')
if (result.success && result.data?.value && result.data.value.length > 0) {
const downloader = result.data.value[0]
wizardData.value.downloader.type = downloader.type
wizardData.value.downloader.name = downloader.name
wizardData.value.downloader.config = downloader.config || {}
}
} catch (error) {
console.log('Load downloader settings failed:', error)
}
}
// 加载媒体服务器设置
async function loadMediaServerSettings() {
try {
const result: { [key: string]: any } = await api.get('system/setting/MediaServers')
if (result.success && result.data?.value && result.data.value.length > 0) {
const mediaServer = result.data.value[0]
wizardData.value.mediaServer.type = mediaServer.type
wizardData.value.mediaServer.name = mediaServer.name
wizardData.value.mediaServer.config = mediaServer.config || {}
wizardData.value.mediaServer.sync_libraries = mediaServer.sync_libraries || []
}
} catch (error) {
console.log('Load media server settings failed:', error)
}
}
// 加载通知设置
async function loadNotificationSettings() {
try {
const result: { [key: string]: any } = await api.get('system/setting/Notifications')
if (result.success && result.data?.value && result.data.value.length > 0) {
const notification = result.data.value[0]
wizardData.value.notification.type = notification.type
wizardData.value.notification.name = notification.name
wizardData.value.notification.enabled = notification.enabled
wizardData.value.notification.config = notification.config || {}
wizardData.value.notification.switchs = notification.switchs || []
}
} catch (error) {
console.log('Load notification settings failed:', error)
}
}
// 初始化
async function initialize() {
isLoading.value = true
try {
await loadSystemSettings()
await loadAuthSites()
await loadSiteAuthSettings()
await loadStorageSettings()
await loadDownloaderSettings()
await loadMediaServerSettings()
await loadNotificationSettings()
} finally {
isLoading.value = false
}
}
return {
// 状态
currentStep,
totalSteps,
stepTitles,
stepDescriptions,
wizardData,
authSites,
selectedPreset,
connectivityTest,
validationErrors,
isLoading,
// 方法
createRandomString,
copyValue,
selectDownloader,
selectMediaServer,
selectNotification,
selectPreset,
updatePreferences,
validateCurrentStep,
validateSiteAuthFields,
validateDownloaderFields,
validateMediaServerFields,
validateNotificationFields,
validateAgentFields,
clearValidationErrors,
testConnectivity,
nextStep,
prevStep,
completeWizard,
initialize,
}
}