mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-03 04:59:46 +08:00
🐛 fix(app): 修复供应商预设识别并兼容Wails开发模式资源加载
- 抽离供应商预设匹配逻辑,避免自定义 OpenAI 端点误识别为千问 Coding Plan - 调整 AI 设置弹窗的预设回填逻辑,并补充预设识别回归测试 - 通过 dev/prod build tag 拆分前端资源装配,避免开发模式依赖 frontend/dist
This commit is contained in:
@@ -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 }[] = [
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user