mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-25 17:44:13 +08:00
Simplify LLM settings connectivity test
This commit is contained in:
@@ -1335,28 +1335,9 @@ export default {
|
||||
llmBaseUrl: 'LLM Base URL',
|
||||
llmBaseUrlHint: 'Base URL for LLM API, used for custom API endpoints',
|
||||
llmTestAction: 'Test Call',
|
||||
llmTestDisabledAgent: 'Please enable AI Assistant first',
|
||||
llmTestDisabledApiKey: 'Please save the LLM API key before testing',
|
||||
llmTestDisabledModel: 'Please save the LLM model before testing',
|
||||
llmTestDisabledUnsaved: 'Testing only uses saved configuration. Please save before testing',
|
||||
llmTestSaving: 'Basic settings are being saved, please test again in a moment',
|
||||
llmTestLoading: 'Testing LLM call, please wait',
|
||||
llmTestPanelTitle: 'Connectivity Test',
|
||||
llmTestPanelDesc: 'This test only uses saved AI assistant settings to verify the current LLM service can be called successfully.',
|
||||
llmTestSuccess: 'Test Succeeded',
|
||||
llmTestFailed: 'Test Failed',
|
||||
llmTestProvider: 'Provider',
|
||||
llmTestProviderValue: 'Provider: {value}',
|
||||
llmTestModel: 'Model',
|
||||
llmTestModelValue: 'Model: {value}',
|
||||
llmTestDuration: 'Duration',
|
||||
llmTestDurationValue: 'Duration: {duration} ms',
|
||||
llmTestReplyPreview: 'Reply Preview',
|
||||
llmTestReplyPreviewValue: 'Reply Preview: {value}',
|
||||
llmTestErrorMessage: 'Error Message',
|
||||
llmTestErrorMessageValue: 'Error Message: {value}',
|
||||
llmTestSuccessToast: 'LLM test call succeeded',
|
||||
llmTestFailedToast: 'LLM test call failed',
|
||||
llmTestFailedToastWithMessage: 'LLM test call failed: {message}',
|
||||
aiAgentGlobal: 'Global AI Assistant',
|
||||
aiAgentGlobalHint:
|
||||
'Enable global AI assistant functionality, all message conversations will be answered by the AI agent without using the /ai command',
|
||||
|
||||
@@ -1333,28 +1333,9 @@ export default {
|
||||
llmBaseUrl: 'LLM基础URL',
|
||||
llmBaseUrlHint: 'LLM API的基础URL地址,用于自定义API端点',
|
||||
llmTestAction: '测试调用',
|
||||
llmTestDisabledAgent: '请先启用智能助手',
|
||||
llmTestDisabledApiKey: '请先保存 LLM API 密钥后再测试',
|
||||
llmTestDisabledModel: '请先保存 LLM 模型后再测试',
|
||||
llmTestDisabledUnsaved: '测试仅针对已保存配置,请先保存后再测试',
|
||||
llmTestSaving: '基础设置保存中,请稍后再测试',
|
||||
llmTestLoading: '正在测试 LLM 调用,请稍候',
|
||||
llmTestPanelTitle: '连通性测试',
|
||||
llmTestPanelDesc: '测试按钮仅针对已保存的智能助手配置,用于验证当前 LLM 服务是否可正常调用。',
|
||||
llmTestSuccess: '测试成功',
|
||||
llmTestFailed: '测试失败',
|
||||
llmTestProvider: '供应商',
|
||||
llmTestProviderValue: '供应商:{value}',
|
||||
llmTestModel: '模型',
|
||||
llmTestModelValue: '模型:{value}',
|
||||
llmTestDuration: '耗时',
|
||||
llmTestDurationValue: '耗时:{duration} ms',
|
||||
llmTestReplyPreview: '结果摘要',
|
||||
llmTestReplyPreviewValue: '结果摘要:{value}',
|
||||
llmTestErrorMessage: '错误信息',
|
||||
llmTestErrorMessageValue: '错误信息:{value}',
|
||||
llmTestSuccessToast: 'LLM 调用测试成功',
|
||||
llmTestFailedToast: 'LLM 调用测试失败',
|
||||
llmTestFailedToastWithMessage: 'LLM 调用测试失败:{message}',
|
||||
aiAgentGlobal: '全局智能助手',
|
||||
aiAgentGlobalHint: '启用全局智能助手功能,所有消息对话均使用智能体回答而不用使用/ai命令',
|
||||
aiAgentJobInterval: '定时唤醒',
|
||||
|
||||
@@ -1335,28 +1335,9 @@ export default {
|
||||
llmBaseUrl: 'LLM基礎URL',
|
||||
llmBaseUrlHint: 'LLM API的基礎URL地址,用於自定義API端點',
|
||||
llmTestAction: '測試調用',
|
||||
llmTestDisabledAgent: '請先啟用智能助手',
|
||||
llmTestDisabledApiKey: '請先保存 LLM API 密鑰後再測試',
|
||||
llmTestDisabledModel: '請先保存 LLM 模型後再測試',
|
||||
llmTestDisabledUnsaved: '測試僅針對已保存配置,請先保存後再測試',
|
||||
llmTestSaving: '基礎設置保存中,請稍後再測試',
|
||||
llmTestLoading: '正在測試 LLM 調用,請稍候',
|
||||
llmTestPanelTitle: '連通性測試',
|
||||
llmTestPanelDesc: '測試按鈕僅針對已保存的智能助手配置,用於驗證當前 LLM 服務是否可正常調用。',
|
||||
llmTestSuccess: '測試成功',
|
||||
llmTestFailed: '測試失敗',
|
||||
llmTestProvider: '供應商',
|
||||
llmTestProviderValue: '供應商:{value}',
|
||||
llmTestModel: '模型',
|
||||
llmTestModelValue: '模型:{value}',
|
||||
llmTestDuration: '耗時',
|
||||
llmTestDurationValue: '耗時:{duration} ms',
|
||||
llmTestReplyPreview: '結果摘要',
|
||||
llmTestReplyPreviewValue: '結果摘要:{value}',
|
||||
llmTestErrorMessage: '錯誤信息',
|
||||
llmTestErrorMessageValue: '錯誤信息:{value}',
|
||||
llmTestSuccessToast: 'LLM 調用測試成功',
|
||||
llmTestFailedToast: 'LLM 調用測試失敗',
|
||||
llmTestFailedToastWithMessage: 'LLM 調用測試失敗:{message}',
|
||||
aiAgentGlobal: '全局智能助手',
|
||||
aiAgentGlobalHint: '啟用全局智能助手功能,所有消息對話均使用智能體回答而不用使用/ai命令',
|
||||
aiAgentJobInterval: '定時喚醒',
|
||||
|
||||
@@ -170,16 +170,6 @@ type LlmSettingsSnapshot = {
|
||||
LLM_BASE_URL: string
|
||||
}
|
||||
|
||||
type LlmTestResult = {
|
||||
success: boolean
|
||||
provider: string
|
||||
model: string
|
||||
duration_ms?: number
|
||||
reply_preview?: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
const llmTestResult = ref<LlmTestResult | null>(null)
|
||||
let llmTestRequestId = 0
|
||||
let llmTestAbortController: AbortController | null = null
|
||||
|
||||
@@ -197,6 +187,25 @@ 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(),
|
||||
api_key: snapshot.LLM_API_KEY.trim(),
|
||||
base_url: snapshot.LLM_BASE_URL.trim(),
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -204,42 +213,23 @@ function invalidateLlmTestState() {
|
||||
llmTestAbortController = null
|
||||
}
|
||||
testingLlm.value = false
|
||||
llmTestResult.value = null
|
||||
}
|
||||
|
||||
const savedLlmSnapshot = ref<LlmSettingsSnapshot>(buildLlmSnapshot())
|
||||
|
||||
const currentLlmSnapshot = computed(() => buildLlmSnapshot())
|
||||
const currentLlmSnapshotKey = computed(() => buildLlmSnapshotKey(currentLlmSnapshot.value))
|
||||
const savedLlmSnapshotKey = computed(() => buildLlmSnapshotKey(savedLlmSnapshot.value))
|
||||
|
||||
const hasSavedLlmChanges = computed(
|
||||
() => currentLlmSnapshotKey.value !== savedLlmSnapshotKey.value,
|
||||
)
|
||||
|
||||
const canTestLlm = computed(() => {
|
||||
const snapshot = currentLlmSnapshot.value
|
||||
return (
|
||||
snapshot.AI_AGENT_ENABLE &&
|
||||
Boolean(snapshot.LLM_PROVIDER.trim()) &&
|
||||
Boolean(snapshot.LLM_API_KEY.trim()) &&
|
||||
Boolean(snapshot.LLM_MODEL.trim()) &&
|
||||
!savingBasic.value &&
|
||||
!testingLlm.value &&
|
||||
!hasSavedLlmChanges.value
|
||||
!testingLlm.value
|
||||
)
|
||||
})
|
||||
|
||||
const llmTestDisabledReason = computed(() => {
|
||||
const snapshot = currentLlmSnapshot.value
|
||||
if (!snapshot.AI_AGENT_ENABLE) return t('setting.system.llmTestDisabledAgent')
|
||||
if (!snapshot.LLM_API_KEY.trim()) return t('setting.system.llmTestDisabledApiKey')
|
||||
if (!snapshot.LLM_MODEL.trim()) return t('setting.system.llmTestDisabledModel')
|
||||
if (savingBasic.value) return t('setting.system.llmTestSaving')
|
||||
if (testingLlm.value) return t('setting.system.llmTestLoading')
|
||||
if (hasSavedLlmChanges.value) return t('setting.system.llmTestDisabledUnsaved')
|
||||
return ''
|
||||
})
|
||||
|
||||
const activeTab = ref('system')
|
||||
|
||||
// 元数据语言
|
||||
@@ -390,7 +380,6 @@ async function loadSystemSettings() {
|
||||
if (result.data.hasOwnProperty(key)) (SystemSettings.value[sectionKey] as any)[key] = result.data[key]
|
||||
})
|
||||
}
|
||||
savedLlmSnapshot.value = buildLlmSnapshot()
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
@@ -418,10 +407,7 @@ async function saveBasicSettings() {
|
||||
savingBasic.value = true
|
||||
try {
|
||||
if (await saveSystemSetting(SystemSettings.value.Basic)) {
|
||||
savedLlmSnapshot.value = buildLlmSnapshot()
|
||||
$toast.success(t('setting.system.basicSaveSuccess'))
|
||||
} else {
|
||||
llmTestResult.value = null
|
||||
}
|
||||
} finally {
|
||||
savingBasic.value = false
|
||||
@@ -433,43 +419,36 @@ async function testLlmConnection() {
|
||||
|
||||
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
|
||||
llmTestResult.value = null
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('system/llm-test', null, {
|
||||
const result: { [key: string]: any } = await api.post('system/llm-test', payload, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
if (requestId !== llmTestRequestId || abortController.signal.aborted || currentLlmSnapshotKey.value !== snapshotKey) {
|
||||
if (
|
||||
requestId !== llmTestRequestId ||
|
||||
abortController.signal.aborted ||
|
||||
currentLlmSnapshotKey.value !== snapshotKey
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const data = result?.data ?? {}
|
||||
llmTestResult.value = {
|
||||
success: Boolean(result?.success),
|
||||
provider: data.provider ?? snapshot.LLM_PROVIDER,
|
||||
model: data.model ?? snapshot.LLM_MODEL,
|
||||
duration_ms: data.duration_ms,
|
||||
reply_preview: data.reply_preview,
|
||||
message: result?.message,
|
||||
}
|
||||
if (!result?.success) $toast.error(t('setting.system.llmTestFailedToast'))
|
||||
if (result?.success) $toast.success(t('setting.system.llmTestSuccessToast'))
|
||||
else showLlmTestFailedToast(result?.message)
|
||||
} catch (error) {
|
||||
if (requestId !== llmTestRequestId || abortController.signal.aborted || currentLlmSnapshotKey.value !== snapshotKey) {
|
||||
if (
|
||||
requestId !== llmTestRequestId ||
|
||||
abortController.signal.aborted ||
|
||||
currentLlmSnapshotKey.value !== snapshotKey
|
||||
) {
|
||||
return
|
||||
}
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
llmTestResult.value = {
|
||||
success: false,
|
||||
provider: snapshot.LLM_PROVIDER,
|
||||
model: snapshot.LLM_MODEL,
|
||||
message,
|
||||
}
|
||||
$toast.error(t('setting.system.llmTestFailedToast'))
|
||||
showLlmTestFailedToast(error instanceof Error ? error.message : String(error))
|
||||
console.log(error)
|
||||
} finally {
|
||||
if (requestId !== llmTestRequestId) return
|
||||
@@ -702,12 +681,9 @@ onBeforeUnmount(() => {
|
||||
invalidateLlmTestState()
|
||||
})
|
||||
|
||||
watch(
|
||||
currentLlmSnapshotKey,
|
||||
(snapshotKey, previousSnapshotKey) => {
|
||||
if (snapshotKey !== previousSnapshotKey) invalidateLlmTestState()
|
||||
},
|
||||
)
|
||||
watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
|
||||
if (snapshotKey !== previousSnapshotKey) invalidateLlmTestState()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -897,26 +873,43 @@ watch(
|
||||
/>
|
||||
</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>
|
||||
<div>
|
||||
<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>
|
||||
|
||||
<div class="d-flex justify-end mt-2">
|
||||
<VBtn
|
||||
variant="text"
|
||||
icon="mdi-refresh"
|
||||
size="small"
|
||||
@click="loadLlmModels"
|
||||
:disabled="!SystemSettings.Basic.LLM_API_KEY"
|
||||
/>
|
||||
</template>
|
||||
</VCombobox>
|
||||
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
|
||||
@@ -947,7 +940,7 @@ watch(
|
||||
prepend-inner-icon="mdi-timer-outline"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12">
|
||||
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Basic.LLM_SUPPORT_IMAGE_INPUT"
|
||||
:label="t('setting.system.llmSupportImageInput')"
|
||||
@@ -955,7 +948,7 @@ watch(
|
||||
persistent-hint
|
||||
/>
|
||||
</VCol>
|
||||
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12">
|
||||
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
|
||||
<VSwitch
|
||||
v-model="SystemSettings.Basic.AI_AGENT_RETRY_TRANSFER"
|
||||
:label="t('setting.system.aiAgentRetryTransfer')"
|
||||
@@ -1005,85 +998,6 @@ watch(
|
||||
</VCardText>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="() => {}">
|
||||
<VRow v-if="SystemSettings.Basic.AI_AGENT_ENABLE" class="mt-1">
|
||||
<VCol cols="12">
|
||||
<VSheet
|
||||
border
|
||||
rounded="lg"
|
||||
class="pa-4"
|
||||
>
|
||||
<div class="d-flex flex-column flex-md-row align-start align-md-center justify-space-between gap-4">
|
||||
<div class="flex-1-1-auto">
|
||||
<div class="text-subtitle-1 font-weight-medium">
|
||||
{{ t('setting.system.llmTestPanelTitle') }}
|
||||
</div>
|
||||
<div class="text-body-2 text-medium-emphasis mt-1">
|
||||
{{ t('setting.system.llmTestPanelDesc') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-100 w-md-auto">
|
||||
<VTooltip v-if="llmTestDisabledReason" location="top">
|
||||
<template #activator="{ props }">
|
||||
<span v-bind="props" class="d-flex">
|
||||
<VBtn
|
||||
color="secondary"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-connection"
|
||||
:disabled="true"
|
||||
:loading="testingLlm"
|
||||
:block="!display.smAndUp.value"
|
||||
class="text-no-wrap"
|
||||
>
|
||||
{{ t('setting.system.llmTestAction') }}
|
||||
</VBtn>
|
||||
</span>
|
||||
</template>
|
||||
<span>{{ llmTestDisabledReason }}</span>
|
||||
</VTooltip>
|
||||
<VBtn
|
||||
v-else
|
||||
color="secondary"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-connection"
|
||||
:loading="testingLlm"
|
||||
:block="!display.smAndUp.value"
|
||||
class="text-no-wrap"
|
||||
@click="testLlmConnection"
|
||||
>
|
||||
{{ t('setting.system.llmTestAction') }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VAlert
|
||||
v-if="llmTestResult"
|
||||
:type="llmTestResult.success ? 'success' : 'error'"
|
||||
variant="tonal"
|
||||
density="comfortable"
|
||||
class="mt-4"
|
||||
>
|
||||
<div class="text-subtitle-2 mb-2">
|
||||
{{ llmTestResult.success ? t('setting.system.llmTestSuccess') : t('setting.system.llmTestFailed') }}
|
||||
</div>
|
||||
<div class="text-body-2">
|
||||
{{ t('setting.system.llmTestProviderValue', { value: llmTestResult.provider }) }}
|
||||
</div>
|
||||
<div class="text-body-2">
|
||||
{{ t('setting.system.llmTestModelValue', { value: llmTestResult.model }) }}
|
||||
</div>
|
||||
<div v-if="llmTestResult.duration_ms !== undefined" class="text-body-2">
|
||||
{{ t('setting.system.llmTestDurationValue', { duration: llmTestResult.duration_ms }) }}
|
||||
</div>
|
||||
<div v-if="llmTestResult.success && llmTestResult.reply_preview" class="text-body-2">
|
||||
{{ t('setting.system.llmTestReplyPreviewValue', { value: llmTestResult.reply_preview }) }}
|
||||
</div>
|
||||
<div v-else-if="llmTestResult.message" class="text-body-2">
|
||||
{{ t('setting.system.llmTestErrorMessageValue', { value: llmTestResult.message }) }}
|
||||
</div>
|
||||
</VAlert>
|
||||
</VSheet>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<div class="setting-actions mt-4">
|
||||
<VBtn
|
||||
type="submit"
|
||||
@@ -1110,6 +1024,7 @@ watch(
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
@@ -1745,19 +1660,16 @@ watch(
|
||||
<style scoped>
|
||||
.setting-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.setting-actions__secondary {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 599px) {
|
||||
.setting-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.llm-test-trigger {
|
||||
min-inline-size: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user