🐛 fix(app): 修复供应商预设识别并兼容Wails开发模式资源加载

- 抽离供应商预设匹配逻辑,避免自定义 OpenAI 端点误识别为千问 Coding Plan
- 调整 AI 设置弹窗的预设回填逻辑,并补充预设识别回归测试
- 通过 dev/prod build tag 拆分前端资源装配,避免开发模式依赖 frontend/dist
This commit is contained in:
Syngnat
2026-03-28 17:40:27 +08:00
parent fcd4d4026c
commit eeef0f06ed
7 changed files with 193 additions and 49 deletions

View File

@@ -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<AIProviderConfig, 'type' | 'baseUrl'>): 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<AIProviderConfig, 'type' | 'baseUrl' | 'apiFormat'>): ProviderPreset => {
const presetKey = resolveProviderPresetKey(provider, PROVIDER_PRESETS, 'custom');
return findPreset(presetKey);
};
const SAFETY_OPTIONS: { label: string; value: AISafetyLevel; desc: string; color: string; icon: string }[] = [

View File

@@ -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');
});
});

View File

@@ -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<AIProviderConfig, 'type' | 'baseUrl' | 'apiFormat'>): 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<AIProviderConfig, 'type' | 'baseUrl' | 'apiFormat'>,
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,