From eeef0f06ede02a54475f50d676e8b3b05dc2a8f5 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Sat, 28 Mar 2026 17:40:27 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(app):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=BE=9B=E5=BA=94=E5=95=86=E9=A2=84=E8=AE=BE=E8=AF=86=E5=88=AB?= =?UTF-8?q?=E5=B9=B6=E5=85=BC=E5=AE=B9Wails=E5=BC=80=E5=8F=91=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E8=B5=84=E6=BA=90=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 抽离供应商预设匹配逻辑,避免自定义 OpenAI 端点误识别为千问 Coding Plan - 调整 AI 设置弹窗的预设回填逻辑,并补充预设识别回归测试 - 通过 dev/prod build tag 拆分前端资源装配,避免开发模式依赖 frontend/dist --- assets_dev.go | 9 ++ assets_prod.go | 13 +++ frontend/package.json.md5 | 2 +- frontend/src/components/AISettingsModal.tsx | 29 +----- frontend/src/utils/aiProviderPresets.test.ts | 92 +++++++++++++++++-- frontend/src/utils/aiProviderPresets.ts | 93 +++++++++++++++++--- main.go | 4 - 7 files changed, 193 insertions(+), 49 deletions(-) create mode 100644 assets_dev.go create mode 100644 assets_prod.go diff --git a/assets_dev.go b/assets_dev.go new file mode 100644 index 0000000..2d3caf3 --- /dev/null +++ b/assets_dev.go @@ -0,0 +1,9 @@ +//go:build dev + +package main + +import "os" + +// 开发模式下由 Wails DevServer 提供前端资源,这里只提供一个稳定的占位 FS, +// 避免编译时依赖 frontend/dist 被并发重建。 +var assets = os.DirFS(".") diff --git a/assets_prod.go b/assets_prod.go new file mode 100644 index 0000000..6cf94d5 --- /dev/null +++ b/assets_prod.go @@ -0,0 +1,13 @@ +//go:build !dev + +package main + +import ( + "embed" + "io/fs" +) + +//go:embed all:frontend/dist +var embeddedAssets embed.FS + +var assets fs.FS = embeddedAssets diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 3018db7..efbd2b6 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -dcb87159cf0f1f6f750d1c4870911d3f \ No newline at end of file +6ba85e4f456d2c0d230cab198c7dc02b \ No newline at end of file diff --git a/frontend/src/components/AISettingsModal.tsx b/frontend/src/components/AISettingsModal.tsx index e148379..403f352 100644 --- a/frontend/src/components/AISettingsModal.tsx +++ b/frontend/src/components/AISettingsModal.tsx @@ -3,12 +3,10 @@ import { Modal, Button, Input, Select, Form, message as antdMessage, Tooltip, Ta import { PlusOutlined, DeleteOutlined, EditOutlined, CheckOutlined, ApiOutlined, SafetyCertificateOutlined, RobotOutlined, ThunderboltOutlined, CloudOutlined, ExperimentOutlined, KeyOutlined, LinkOutlined, AppstoreOutlined, ToolOutlined } from '@ant-design/icons'; import type { AIProviderConfig, AIProviderType, AISafetyLevel, AIContextLevel } from '../types'; import { - getProviderFingerprint, - getProviderHostname, - matchQwenPresetKey, QWEN_BAILIAN_ANTHROPIC_BASE_URL, QWEN_CODING_PLAN_ANTHROPIC_BASE_URL, QWEN_CODING_PLAN_MODELS, + resolveProviderPresetKey, resolvePresetBaseURL, resolvePresetModelSelection, resolvePresetTransport, @@ -62,28 +60,9 @@ const PROVIDER_PRESETS: ProviderPreset[] = [ const findPreset = (key: string): ProviderPreset => PROVIDER_PRESETS.find(p => p.key === key) || PROVIDER_PRESETS[PROVIDER_PRESETS.length - 1]; -const matchProviderPreset = (provider: Pick): ProviderPreset => { - const qwenPresetKey = matchQwenPresetKey(provider); - if (qwenPresetKey) { - return findPreset(qwenPresetKey); - } - const fingerprint = getProviderFingerprint(provider.baseUrl); - const exactPreset = PROVIDER_PRESETS.find(pr => - pr.backendType === provider.type - && fingerprint !== '' - && fingerprint === getProviderFingerprint(pr.defaultBaseUrl) - ); - if (exactPreset) { - return exactPreset; - } - - const host = getProviderHostname(provider.baseUrl); - if (host.endsWith('moonshot.cn')) { - return findPreset('moonshot'); - } - return PROVIDER_PRESETS.find(pr => pr.backendType === provider.type && host !== '' && host === getProviderHostname(pr.defaultBaseUrl)) - || PROVIDER_PRESETS.find(pr => pr.backendType === provider.type) - || findPreset('custom'); +const matchProviderPreset = (provider: Pick): ProviderPreset => { + const presetKey = resolveProviderPresetKey(provider, PROVIDER_PRESETS, 'custom'); + return findPreset(presetKey); }; const SAFETY_OPTIONS: { label: string; value: AISafetyLevel; desc: string; color: string; icon: string }[] = [ diff --git a/frontend/src/utils/aiProviderPresets.test.ts b/frontend/src/utils/aiProviderPresets.test.ts index 98bfc67..c1384e1 100644 --- a/frontend/src/utils/aiProviderPresets.test.ts +++ b/frontend/src/utils/aiProviderPresets.test.ts @@ -1,15 +1,37 @@ import { describe, expect, it } from 'vitest'; - +import type { AIProviderType } from '../types'; import { - matchQwenPresetKey, + LEGACY_QWEN_CODING_PLAN_OPENAI_BASE_URL, + QWEN_BAILIAN_ANTHROPIC_BASE_URL, QWEN_BAILIAN_MODELS_BASE_URL, QWEN_CODING_PLAN_ANTHROPIC_BASE_URL, QWEN_CODING_PLAN_MODELS, + matchQwenPresetKey, resolvePresetBaseURL, resolvePresetModelSelection, resolvePresetTransport, + resolveProviderPresetKey, } from './aiProviderPresets'; +type PresetMatcher = { + key: string; + backendType: AIProviderType; + defaultBaseUrl: string; + fixedApiFormat?: string; +}; + +const PRESETS: PresetMatcher[] = [ + { key: 'openai', backendType: 'openai', defaultBaseUrl: 'https://api.openai.com/v1' }, + { key: 'qwen-bailian', backendType: 'anthropic', defaultBaseUrl: QWEN_BAILIAN_ANTHROPIC_BASE_URL }, + { + key: 'qwen-coding-plan', + backendType: 'custom', + defaultBaseUrl: QWEN_CODING_PLAN_ANTHROPIC_BASE_URL, + fixedApiFormat: 'claude-cli', + }, + { key: 'custom', backendType: 'custom', defaultBaseUrl: '' }, +]; + describe('ai provider preset helpers', () => { it('maps legacy Bailian compatible-mode URL back to the Bailian preset', () => { expect(matchQwenPresetKey({ @@ -18,13 +40,6 @@ describe('ai provider preset helpers', () => { })).toBe('qwen-bailian'); }); - it('maps Coding Plan anthropic URL to the dedicated Coding Plan preset', () => { - expect(matchQwenPresetKey({ - type: 'anthropic', - baseUrl: QWEN_CODING_PLAN_ANTHROPIC_BASE_URL, - })).toBe('qwen-coding-plan'); - }); - it('maps Coding Plan Claude CLI config back to the dedicated Coding Plan preset', () => { expect(matchQwenPresetKey({ type: 'custom', @@ -33,6 +48,21 @@ describe('ai provider preset helpers', () => { })).toBe('qwen-coding-plan'); }); + it('maps legacy Coding Plan OpenAI config back to the dedicated Coding Plan preset', () => { + expect(matchQwenPresetKey({ + type: 'openai', + baseUrl: LEGACY_QWEN_CODING_PLAN_OPENAI_BASE_URL, + })).toBe('qwen-coding-plan'); + }); + + it('does not treat a custom OpenAI endpoint as the built-in Coding Plan preset', () => { + expect(matchQwenPresetKey({ + type: 'custom', + apiFormat: 'openai', + baseUrl: LEGACY_QWEN_CODING_PLAN_OPENAI_BASE_URL, + })).toBeNull(); + }); + it('does not keep a baked-in model list for the Coding Plan preset', () => { expect(QWEN_CODING_PLAN_MODELS).toEqual([ 'qwen3.5-plus', @@ -109,3 +139,47 @@ describe('ai provider preset helpers', () => { }); }); }); + +describe('resolveProviderPresetKey', () => { + it('不会把自定义 OpenAI 端点误识别成千问 Coding Plan', () => { + const key = resolveProviderPresetKey( + { + type: 'custom', + apiFormat: 'openai', + baseUrl: LEGACY_QWEN_CODING_PLAN_OPENAI_BASE_URL, + }, + PRESETS, + 'custom', + ); + + expect(key).toBe('custom'); + }); + + it('仍然能识别当前内置的千问 Coding Plan 预设', () => { + const key = resolveProviderPresetKey( + { + type: 'custom', + apiFormat: 'claude-cli', + baseUrl: QWEN_CODING_PLAN_ANTHROPIC_BASE_URL, + }, + PRESETS, + 'custom', + ); + + expect(key).toBe('qwen-coding-plan'); + }); + + it('仍然能识别当前内置的千问百炼预设', () => { + const key = resolveProviderPresetKey( + { + type: 'anthropic', + apiFormat: undefined, + baseUrl: QWEN_BAILIAN_ANTHROPIC_BASE_URL, + }, + PRESETS, + 'custom', + ); + + expect(key).toBe('qwen-bailian'); + }); +}); diff --git a/frontend/src/utils/aiProviderPresets.ts b/frontend/src/utils/aiProviderPresets.ts index 7d39a5c..0e96558 100644 --- a/frontend/src/utils/aiProviderPresets.ts +++ b/frontend/src/utils/aiProviderPresets.ts @@ -49,6 +49,13 @@ export interface ResolvePresetTransportResult { apiFormat?: string; } +export interface ProviderPresetMatcher { + key: string; + backendType: AIProviderType; + defaultBaseUrl: string; + fixedApiFormat?: string; +} + export const getProviderHostname = (raw?: string): string => { if (!raw) return ''; try { @@ -71,25 +78,91 @@ export const getProviderFingerprint = (raw?: string): string => { export const matchQwenPresetKey = (provider: Pick): string | null => { const fingerprint = getProviderFingerprint(provider.baseUrl); - const bailianFingerprints = new Set([ - getProviderFingerprint(LEGACY_QWEN_BAILIAN_OPENAI_BASE_URL), - getProviderFingerprint(QWEN_BAILIAN_ANTHROPIC_BASE_URL), - ]); - if (fingerprint !== '' && bailianFingerprints.has(fingerprint)) { + + if ( + fingerprint !== '' + && fingerprint === getProviderFingerprint(QWEN_BAILIAN_ANTHROPIC_BASE_URL) + && provider.type === 'anthropic' + ) { return 'qwen-bailian'; } - const codingPlanFingerprints = new Set([ - getProviderFingerprint(LEGACY_QWEN_CODING_PLAN_OPENAI_BASE_URL), - getProviderFingerprint(QWEN_CODING_PLAN_ANTHROPIC_BASE_URL), - ]); - if (fingerprint !== '' && codingPlanFingerprints.has(fingerprint)) { + if ( + fingerprint !== '' + && fingerprint === getProviderFingerprint(LEGACY_QWEN_BAILIAN_OPENAI_BASE_URL) + && provider.type === 'openai' + ) { + return 'qwen-bailian'; + } + + if ( + fingerprint !== '' + && fingerprint === getProviderFingerprint(QWEN_CODING_PLAN_ANTHROPIC_BASE_URL) + && provider.type === 'custom' + && provider.apiFormat === 'claude-cli' + ) { + return 'qwen-coding-plan'; + } + + if ( + fingerprint !== '' + && fingerprint === getProviderFingerprint(LEGACY_QWEN_CODING_PLAN_OPENAI_BASE_URL) + && provider.type === 'openai' + ) { return 'qwen-coding-plan'; } return null; }; +export const resolveProviderPresetKey = ( + provider: Pick, + presets: ProviderPresetMatcher[], + fallbackKey = 'custom', +): string => { + const qwenPresetKey = matchQwenPresetKey(provider); + if (qwenPresetKey) { + return qwenPresetKey; + } + + const fingerprint = getProviderFingerprint(provider.baseUrl); + const exactPreset = presets.find((preset) => + preset.backendType === provider.type + && fingerprint !== '' + && fingerprint === getProviderFingerprint(preset.defaultBaseUrl) + && (!preset.fixedApiFormat || preset.fixedApiFormat === provider.apiFormat), + ); + if (exactPreset) { + return exactPreset.key; + } + + // custom 供应商必须保守处理,避免仅凭 host 错误吞掉用户显式保存的自定义配置。 + if (provider.type === 'custom') { + return fallbackKey; + } + + const host = getProviderHostname(provider.baseUrl); + if (provider.type === 'anthropic' && host.endsWith('moonshot.cn')) { + const moonshotPreset = presets.find((preset) => preset.key === 'moonshot'); + if (moonshotPreset) { + return moonshotPreset.key; + } + } + + const hostPreset = presets.find((preset) => + preset.backendType === provider.type + && host !== '' + && host === getProviderHostname(preset.defaultBaseUrl) + && (!preset.fixedApiFormat || preset.fixedApiFormat === provider.apiFormat), + ); + if (hostPreset) { + return hostPreset.key; + } + + const typePreset = presets.find((preset) => preset.backendType === provider.type && !preset.fixedApiFormat); + return typePreset?.key || fallbackKey; +}; + export const resolvePresetModelSelection = ({ presetKey, presetDefaultModel, diff --git a/main.go b/main.go index 8c4d999..d312635 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "embed" aiservice "GoNavi-Wails/internal/ai/service" "GoNavi-Wails/internal/app" @@ -15,9 +14,6 @@ import ( "github.com/wailsapp/wails/v2/pkg/options/windows" ) -//go:embed all:frontend/dist -var assets embed.FS - func main() { // Create an instance of the app structure application := app.NewApp()