From 6104fb861ea6561d61a7116999ad2dd313512f5a Mon Sep 17 00:00:00 2001 From: jxxghp Date: Thu, 30 Jul 2026 13:32:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=8E=E7=AB=AF=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E6=99=BA=E8=83=BD=E5=8A=A9=E6=89=8B=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=20(#597)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/composables/useLlmProviderDirectory.ts | 19 ++++--- src/composables/useSetupWizard.ts | 21 +++++--- src/locales/en-US.ts | 6 +++ src/locales/zh-CN.ts | 5 ++ src/locales/zh-TW.ts | 5 ++ src/views/setting/AccountSettingSystem.vue | 37 +++++++++++--- src/views/setup/AgentSettingsStep.vue | 59 +++++++++++++++++----- 7 files changed, 116 insertions(+), 36 deletions(-) diff --git a/src/composables/useLlmProviderDirectory.ts b/src/composables/useLlmProviderDirectory.ts index 43c8bb10..6793d10f 100644 --- a/src/composables/useLlmProviderDirectory.ts +++ b/src/composables/useLlmProviderDirectory.ts @@ -85,6 +85,7 @@ interface UseLlmProviderDirectoryOptions { baseUrlPreset?: Ref useProxy?: Ref userAgent?: Ref + apiProtocol?: Ref model: Ref maxContextTokens?: Ref authConnected?: Ref @@ -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, diff --git a/src/composables/useSetupWizard.ts b/src/composables/useSetupWizard.ts index 2d6bdfea..28d3ece6 100644 --- a/src/composables/useSetupWizard.ts +++ b/src/composables/useSetupWizard.ts @@ -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 = { @@ -245,6 +248,7 @@ const wizardData = ref({ 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) diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index 8ed4abbb..47263289 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -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.', diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 3e1b9905..367cfa70 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -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: '提供商授权', diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index 8ce08c3b..1bf96ff7 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -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: '提供商授權', diff --git a/src/views/setting/AccountSettingSystem.vue b/src/views/setting/AccountSettingSystem.vue index 8870ca29..88d48bfa 100644 --- a/src/views/setting/AccountSettingSystem.vue +++ b/src/views/setting/AccountSettingSystem.vue @@ -53,6 +53,7 @@ const SystemSettings = ref({ 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" /> + + + { 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 }} diff --git a/src/views/setup/AgentSettingsStep.vue b/src/views/setup/AgentSettingsStep.vue index 0d535a54..a495ac1a 100644 --- a/src/views/setup/AgentSettingsStep.vue +++ b/src/views/setup/AgentSettingsStep.vue @@ -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 () => { /> + + + + { + if (typeof value === 'object' && value !== null) { + setBaseUrlPreset(value.id, value.value) + } else { + setBaseUrlPreset('', value || '') + } } - }" + " :label="t('setting.system.llmBaseUrl')" :hint="t('setting.system.llmBaseUrlHint')" :placeholder="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') }}
- {{ t('setting.system.llmProviderConnectedAs', { label: providerAuthLabel || selectedProvider?.name }) }} + {{ + t('setting.system.llmProviderConnectedAs', { label: providerAuthLabel || selectedProvider?.name }) + }}
@@ -417,10 +446,12 @@ onMounted(async () => {