feat: 后端修改前端智能助手设置 (#597)

This commit is contained in:
jxxghp
2026-07-30 13:32:19 +08:00
committed by GitHub
parent 01e7dcee56
commit 6104fb861e
7 changed files with 116 additions and 36 deletions
+11 -8
View File
@@ -85,6 +85,7 @@ interface UseLlmProviderDirectoryOptions {
baseUrlPreset?: Ref<string>
useProxy?: Ref<boolean>
userAgent?: Ref<string>
apiProtocol?: Ref<string>
model: Ref<string>
maxContextTokens?: Ref<number>
authConnected?: Ref<boolean>
@@ -122,10 +123,12 @@ export function useLlmProviderDirectory(options: UseLlmProviderDirectoryOptions)
})),
)
const providerConnected = computed(() => Boolean(selectedProvider.value?.auth_status?.connected))
const showBaseUrlField = computed(
() => Boolean(selectedProvider.value && (selectedProvider.value.oauth_methods || []).length === 0),
const showBaseUrlField = computed(() =>
Boolean(selectedProvider.value && (selectedProvider.value.oauth_methods || []).length === 0),
)
const showApiKeyField = computed(() => selectedProvider.value?.supports_api_key !== false)
// OpenAI 兼容接口才需要选择 API 协议(Chat Completions / Responses)。
const showApiProtocolField = computed(() => selectedProvider.value?.runtime === 'openai')
const hasUsableCredential = computed(() => {
if (providerConnected.value) return true
return Boolean(normalizeValue(options.apiKey.value))
@@ -195,6 +198,9 @@ export function useLlmProviderDirectory(options: UseLlmProviderDirectoryOptions)
if (options.maxContextTokens) {
options.maxContextTokens.value = 64
}
if (options.apiProtocol) {
options.apiProtocol.value = 'auto'
}
models.value = []
options.model.value = ''
syncAuthConnected()
@@ -269,9 +275,7 @@ export function useLlmProviderDirectory(options: UseLlmProviderDirectoryOptions)
updateProviderAuthStatus(normalizeValue(options.provider.value), payload.auth_status)
const currentModelId = normalizeValue(options.model.value)
const matchedModel = currentModelId
? models.value.find(item => item.id === currentModelId)
: null
const matchedModel = currentModelId ? models.value.find(item => item.id === currentModelId) : null
if (matchedModel) {
applyModelMetadata(matchedModel.id)
@@ -301,9 +305,7 @@ export function useLlmProviderDirectory(options: UseLlmProviderDirectoryOptions)
authPolling.value = true
clearPollTimer()
try {
const result: { [key: string]: any } = await api.post(
`llm/provider-auth/${authSession.value.session_id}/poll`,
)
const result: { [key: string]: any } = await api.post(`llm/provider-auth/${authSession.value.session_id}/poll`)
if (!result.success) {
throw new Error(result.message || 'Poll LLM auth failed')
}
@@ -393,6 +395,7 @@ export function useLlmProviderDirectory(options: UseLlmProviderDirectoryOptions)
providerConnected,
showBaseUrlField,
showApiKeyField,
showApiProtocolField,
hasUsableCredential,
canRefreshModels,
setBaseUrlPreset,
+13 -8
View File
@@ -56,6 +56,7 @@ export interface WizardData {
authConnected: boolean
model: string
thinkingLevel: string
apiProtocol: string
supportImageInput: boolean
supportAudioInput: boolean
supportAudioOutput: boolean
@@ -142,7 +143,9 @@ export interface ValidationErrorState {
}
function normalizeThinkingLevelValue(value?: unknown) {
const normalized = String(value ?? '').trim().toLowerCase()
const normalized = String(value ?? '')
.trim()
.toLowerCase()
if (!normalized) return ''
const aliasMap: Record<string, string> = {
@@ -245,6 +248,7 @@ const wizardData = ref<WizardData>({
authConnected: false,
model: 'deepseek-chat',
thinkingLevel: 'off',
apiProtocol: 'auto',
supportImageInput: true,
supportAudioInput: false,
supportAudioOutput: false,
@@ -593,10 +597,7 @@ export function useSetupWizard() {
errors.push(t('downloader.passwordRequired'))
validationErrors.value.downloader.password = true
}
} else if (
wizardData.value.downloader.type === 'transmission'
|| wizardData.value.downloader.type === 'rtorrent'
) {
} else if (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
@@ -799,7 +800,10 @@ export function useSetupWizard() {
validationErrors.value.agent.maxContextTokens = true
}
if (wizardData.value.agent.recommendEnabled && (!wizardData.value.agent.recommendMaxItems || wizardData.value.agent.recommendMaxItems < 1)) {
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
}
@@ -1446,6 +1450,7 @@ export function useSetupWizard() {
LLM_PROVIDER: wizardData.value.agent.provider,
LLM_MODEL: wizardData.value.agent.model,
LLM_THINKING_LEVEL: wizardData.value.agent.thinkingLevel,
LLM_API_PROTOCOL: wizardData.value.agent.apiProtocol || 'auto',
LLM_SUPPORT_IMAGE_INPUT: wizardData.value.agent.supportImageInput,
LLM_SUPPORT_AUDIO_INPUT: wizardData.value.agent.supportAudioInput,
LLM_SUPPORT_AUDIO_OUTPUT: wizardData.value.agent.supportAudioOutput,
@@ -1469,8 +1474,7 @@ export function useSetupWizard() {
AUDIO_OUTPUT_INCLUDE_TEXT: wizardData.value.agent.audioOutputIncludeText,
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_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,
}
@@ -1562,6 +1566,7 @@ export function useSetupWizard() {
wizardData.value.agent.authConnected = false
wizardData.value.agent.model = result.data.LLM_MODEL || ''
wizardData.value.agent.thinkingLevel = resolveThinkingLevelValue(result.data)
wizardData.value.agent.apiProtocol = result.data.LLM_API_PROTOCOL || 'auto'
wizardData.value.agent.supportImageInput = result.data.LLM_SUPPORT_IMAGE_INPUT ?? true
wizardData.value.agent.supportAudioInput = Boolean(result.data.LLM_SUPPORT_AUDIO_INPUT)
wizardData.value.agent.supportAudioOutput = Boolean(result.data.LLM_SUPPORT_AUDIO_OUTPUT)
+6
View File
@@ -1799,6 +1799,12 @@ export default {
'When enabled, Agent connections to the current LLM provider use the system proxy from advanced settings.',
llmUserAgent: 'User-Agent',
llmUserAgentHint: 'User-Agent sent to OpenAI-compatible APIs. Leave empty to use the SDK default.',
llmApiProtocol: 'API Protocol',
llmApiProtocolHint:
'Request protocol for OpenAI-compatible APIs: auto-select by model capability, or force Chat Completions / Responses.',
llmApiProtocolAuto: 'Auto (auto)',
llmApiProtocolChatCompletions: 'Chat Completions',
llmApiProtocolResponses: 'Responses',
llmTemperature: 'Temperature',
llmTemperatureHint:
'Controls response randomness. Lower values are steadier and higher values are more varied. Backend default is 0.3; 0-2 is usually recommended.',
+5
View File
@@ -1784,6 +1784,11 @@ export default {
llmUseProxyHint: '启用后,Agent 连接当前 LLM 提供商时会应用高级设置中的系统代理',
llmUserAgent: 'User-Agent',
llmUserAgentHint: 'OpenAI 兼容接口请求使用的 User-Agent,留空则使用 SDK 默认值',
llmApiProtocol: 'API 协议',
llmApiProtocolHint: 'OpenAI 兼容接口的请求协议:自动按模型能力选择,或强制使用 Chat Completions / Responses',
llmApiProtocolAuto: '自动 (auto)',
llmApiProtocolChatCompletions: 'Chat Completions',
llmApiProtocolResponses: 'Responses',
llmTemperature: '温度参数',
llmTemperatureHint: '控制回复随机性,数值越低越稳定,越高越发散;后端默认 0.3,通常建议 0-2',
llmProviderAuth: '提供商授权',
+5
View File
@@ -1783,6 +1783,11 @@ export default {
llmUseProxyHint: '啟用後,Agent 連接目前 LLM 提供商時會套用進階設定中的系統代理',
llmUserAgent: 'User-Agent',
llmUserAgentHint: 'OpenAI 兼容接口請求使用的 User-Agent,留空則使用 SDK 預設值',
llmApiProtocol: 'API 協議',
llmApiProtocolHint: 'OpenAI 兼容接口的請求協議:自動按模型能力選擇,或強制使用 Chat Completions / Responses',
llmApiProtocolAuto: '自動 (auto)',
llmApiProtocolChatCompletions: 'Chat Completions',
llmApiProtocolResponses: 'Responses',
llmTemperature: '溫度參數',
llmTemperatureHint: '控制回覆隨機性,數值越低越穩定,越高越發散;後端預設 0.3,通常建議 0-2',
llmProviderAuth: '提供商授權',
+31 -6
View File
@@ -53,6 +53,7 @@ const SystemSettings = ref<any>({
LLM_PROVIDER: 'deepseek',
LLM_MODEL: 'deepseek-chat',
LLM_THINKING_LEVEL: 'off',
LLM_API_PROTOCOL: 'auto',
LLM_SUPPORT_IMAGE_INPUT: false,
LLM_SUPPORT_AUDIO_INPUT: false,
LLM_SUPPORT_AUDIO_OUTPUT: false,
@@ -221,6 +222,7 @@ type LlmSettingsSnapshot = {
LLM_PROVIDER: string
LLM_MODEL: string
LLM_THINKING_LEVEL: string
LLM_API_PROTOCOL: string
LLM_API_KEY: string
LLM_BASE_URL: string
LLM_USE_PROXY: boolean
@@ -292,6 +294,13 @@ const llmUserAgentRef = computed({
},
})
const llmApiProtocolRef = computed({
get: () => String(SystemSettings.value.Basic.LLM_API_PROTOCOL ?? 'auto'),
set: value => {
SystemSettings.value.Basic.LLM_API_PROTOCOL = value || 'auto'
},
})
const llmModelRef = computed({
get: () => String(SystemSettings.value.Basic.LLM_MODEL ?? ''),
set: value => {
@@ -317,6 +326,7 @@ const {
providerConnected,
showBaseUrlField,
showApiKeyField,
showApiProtocolField: showLlmApiProtocolField,
canRefreshModels,
setBaseUrlPreset,
authDialogVisible,
@@ -339,6 +349,7 @@ const {
baseUrlPreset: llmBaseUrlPresetRef,
useProxy: llmUseProxyRef,
userAgent: llmUserAgentRef,
apiProtocol: llmApiProtocolRef,
model: llmModelRef,
maxContextTokens: llmMaxContextRef,
})
@@ -397,6 +408,7 @@ function buildLlmSnapshot(): LlmSettingsSnapshot {
LLM_PROVIDER: String(SystemSettings.value.Basic.LLM_PROVIDER ?? ''),
LLM_MODEL: String(SystemSettings.value.Basic.LLM_MODEL ?? ''),
LLM_THINKING_LEVEL: String(SystemSettings.value.Basic.LLM_THINKING_LEVEL ?? 'off'),
LLM_API_PROTOCOL: String(SystemSettings.value.Basic.LLM_API_PROTOCOL ?? 'auto'),
LLM_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),
@@ -416,6 +428,7 @@ function buildLlmTestPayload(snapshot: LlmSettingsSnapshot) {
provider: snapshot.LLM_PROVIDER.trim(),
model: snapshot.LLM_MODEL.trim(),
thinking_level: snapshot.LLM_THINKING_LEVEL.trim(),
api_protocol: snapshot.LLM_API_PROTOCOL.trim() || 'auto',
api_key: snapshot.LLM_API_KEY.trim(),
base_url: snapshot.LLM_BASE_URL.trim(),
use_proxy: snapshot.LLM_USE_PROXY,
@@ -513,6 +526,12 @@ const thinkingLevelItems = computed(() => [
{ title: t('setting.system.llmThinkingLevelXhigh'), value: 'xhigh' },
])
const apiProtocolItems = computed(() => [
{ title: t('setting.system.llmApiProtocolAuto'), value: 'auto' },
{ title: t('setting.system.llmApiProtocolChatCompletions'), value: 'chat_completions' },
{ title: t('setting.system.llmApiProtocolResponses'), value: 'responses' },
])
const activeTab = ref('system')
//
@@ -608,9 +627,7 @@ function addSecurityDomain() {
function addImageProxyAllowedPrivateRange() {
if (
newImageProxyAllowedPrivateRange.value &&
!SystemSettings.value.Advanced.IMAGE_PROXY_ALLOWED_PRIVATE_RANGES.includes(
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 = ''
@@ -1214,6 +1231,16 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
@update:model-value="handleLlmProviderChanged"
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE && showLlmApiProtocolField" cols="12" md="6">
<VSelect
v-model="SystemSettings.Basic.LLM_API_PROTOCOL"
:label="t('setting.system.llmApiProtocol')"
:hint="t('setting.system.llmApiProtocolHint')"
persistent-hint
:items="apiProtocolItems"
prepend-inner-icon="mdi-swap-horizontal"
/>
</VCol>
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE && showBaseUrlField" cols="12" md="6">
<VCombobox
:model-value="SystemSettings.Basic.LLM_BASE_URL"
@@ -2265,9 +2292,7 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
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)
"
@click:close="SystemSettings.Advanced.IMAGE_PROXY_ALLOWED_PRIVATE_RANGES.splice(index, 1)"
>
{{ range }}
</VChip>
+43 -12
View File
@@ -54,6 +54,13 @@ const userAgentRef = computed({
},
})
const apiProtocolRef = computed({
get: () => wizardData.value.agent.apiProtocol,
set: value => {
wizardData.value.agent.apiProtocol = value || 'auto'
},
})
const modelRef = computed({
get: () => wizardData.value.agent.model,
set: value => {
@@ -86,6 +93,7 @@ const {
providerConnected,
showBaseUrlField,
showApiKeyField,
showApiProtocolField,
canRefreshModels,
setBaseUrlPreset,
authDialogVisible,
@@ -108,6 +116,7 @@ const {
baseUrlPreset: baseUrlPresetRef,
useProxy: useProxyRef,
userAgent: userAgentRef,
apiProtocol: apiProtocolRef,
model: modelRef,
maxContextTokens: maxContextTokensRef,
authConnected: authConnectedRef,
@@ -183,6 +192,12 @@ const thinkingLevelItems = computed(() => [
{ title: t('setting.system.llmThinkingLevelXhigh'), value: 'xhigh' },
])
const apiProtocolItems = computed(() => [
{ title: t('setting.system.llmApiProtocolAuto'), value: 'auto' },
{ title: t('setting.system.llmApiProtocolChatCompletions'), value: 'chat_completions' },
{ title: t('setting.system.llmApiProtocolResponses'), value: 'responses' },
])
const audioProviderItems = computed(() => [
{ title: t('setting.system.audioProviderOpenAiAudio'), value: 'openai' },
{ title: t('setting.system.audioProviderChatAudio'), value: 'openai_chat_audio' },
@@ -324,16 +339,30 @@ onMounted(async () => {
/>
</VCol>
<VCol v-if="showApiProtocolField" cols="12" md="6">
<VSelect
v-model="wizardData.agent.apiProtocol"
:label="t('setting.system.llmApiProtocol')"
:hint="t('setting.system.llmApiProtocolHint')"
:items="apiProtocolItems"
persistent-hint
prepend-inner-icon="mdi-swap-horizontal"
color="primary"
/>
</VCol>
<VCol v-if="showBaseUrlField" cols="12" md="6">
<VCombobox
:model-value="wizardData.agent.baseUrl"
@update:model-value="(value: any) => {
@update:model-value="
(value: any) => {
if (typeof value === 'object' && value !== null) {
setBaseUrlPreset(value.id, value.value);
setBaseUrlPreset(value.id, value.value)
} else {
setBaseUrlPreset('', value || '');
setBaseUrlPreset('', value || '')
}
}"
}
"
:label="t('setting.system.llmBaseUrl')"
:hint="t('setting.system.llmBaseUrlHint')"
:placeholder="selectedProvider?.default_base_url || 'https://api.deepseek.com'"
@@ -366,9 +395,7 @@ onMounted(async () => {
:hint="selectedProvider?.api_key_hint || t('setting.system.llmApiKeyHint')"
:placeholder="t('setting.system.llmApiKeyPlaceholder')"
:error="validationErrors.agent.apiKey"
:error-messages="
validationErrors.agent.apiKey ? [t('setupWizard.agent.authOrApiKeyRequired')] : []
"
:error-messages="validationErrors.agent.apiKey ? [t('setupWizard.agent.authOrApiKeyRequired')] : []"
persistent-hint
prepend-inner-icon="mdi-key-variant"
type="password"
@@ -384,7 +411,9 @@ onMounted(async () => {
{{ selectedProvider?.description || t('setting.system.llmProviderAuthHint') }}
</div>
<div v-if="providerConnected" class="text-body-2 mt-2">
{{ t('setting.system.llmProviderConnectedAs', { label: providerAuthLabel || selectedProvider?.name }) }}
{{
t('setting.system.llmProviderConnectedAs', { label: providerAuthLabel || selectedProvider?.name })
}}
</div>
</div>
@@ -417,10 +446,12 @@ onMounted(async () => {
<VCol cols="12" md="6">
<VCombobox
:model-value="wizardData.agent.model"
@update:model-value="(val: any) => {
wizardData.agent.model = typeof val === 'object' && val !== null ? val.id : val;
handleModelChanged();
}"
@update:model-value="
(val: any) => {
wizardData.agent.model = typeof val === 'object' && val !== null ? val.id : val
handleModelChanged()
}
"
:label="t('setting.system.llmModel')"
:hint="t('setting.system.llmModelHint')"
:items="llmModels"