🐛 fix(ai/provider/chat-ui): 修复千问 Coding Plan 预设与 Claude CLI 报错

- 统一千问 Coding Plan 到 claude-cli 链路
- 修正旧配置识别与模型列表逻辑
- 透传 Claude CLI 鉴权失败和错误事件
- 移除误杀正常回复的启动定时器
This commit is contained in:
Syngnat
2026-03-27 17:02:51 +08:00
parent 4f74c44147
commit 09aa526570
10 changed files with 765 additions and 62 deletions

View File

@@ -7,6 +7,7 @@ import {
QWEN_CODING_PLAN_MODELS,
resolvePresetBaseURL,
resolvePresetModelSelection,
resolvePresetTransport,
} from './aiProviderPresets';
describe('ai provider preset helpers', () => {
@@ -24,6 +25,27 @@ describe('ai provider preset helpers', () => {
})).toBe('qwen-coding-plan');
});
it('maps Coding Plan Claude CLI config back to the dedicated Coding Plan preset', () => {
expect(matchQwenPresetKey({
type: 'custom',
apiFormat: 'claude-cli',
baseUrl: QWEN_CODING_PLAN_ANTHROPIC_BASE_URL,
})).toBe('qwen-coding-plan');
});
it('does not keep a baked-in model list for the Coding Plan preset', () => {
expect(QWEN_CODING_PLAN_MODELS).toEqual([
'qwen3.5-plus',
'kimi-k2.5',
'glm-5',
'MiniMax-M2.5',
'qwen3-max-2026-01-23',
'qwen3-coder-next',
'qwen3-coder-plus',
'glm-4.7',
]);
});
it('keeps built-in preset model empty when the preset intentionally requires an explicit selection', () => {
expect(resolvePresetModelSelection({
presetKey: 'qwen-coding-plan',
@@ -65,4 +87,25 @@ describe('ai provider preset helpers', () => {
valuesBaseUrl: 'https://example-proxy.internal/v1',
})).toBe('https://example-proxy.internal/v1');
});
it('forces qwen coding plan to save as custom plus claude-cli', () => {
expect(resolvePresetTransport({
presetBackendType: 'custom',
presetFixedApiFormat: 'claude-cli',
valuesApiFormat: 'anthropic',
})).toEqual({
type: 'custom',
apiFormat: 'claude-cli',
});
});
it('keeps custom preset transport editable', () => {
expect(resolvePresetTransport({
presetBackendType: 'custom',
valuesApiFormat: 'gemini',
})).toEqual({
type: 'custom',
apiFormat: 'gemini',
});
});
});

View File

@@ -1,4 +1,4 @@
import type { AIProviderConfig } from '../types';
import type { AIProviderConfig, AIProviderType } from '../types';
export const LEGACY_QWEN_BAILIAN_OPENAI_BASE_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1';
export const LEGACY_QWEN_CODING_PLAN_OPENAI_BASE_URL = 'https://coding.dashscope.aliyuncs.com/v1';
@@ -7,12 +7,14 @@ export const QWEN_CODING_PLAN_ANTHROPIC_BASE_URL = 'https://coding.dashscope.ali
export const QWEN_BAILIAN_MODELS_BASE_URL = LEGACY_QWEN_BAILIAN_OPENAI_BASE_URL;
export const QWEN_CODING_PLAN_MODELS = [
'qwen3.5-plus',
'kimi-k2.5',
'glm-5',
'MiniMax-M2.5',
'qwen3-max-2026-01-23',
'qwen3-coder-next',
'qwen3-coder-plus',
'qwen3-coder-480b-a35b-instruct',
'qwen3-coder-30b-a3b-instruct',
'qwen3-coder-flash',
'qwen-plus',
'qwen-turbo',
'glm-4.7',
];
const CUSTOM_LIKE_PRESET_KEYS = new Set(['custom', 'ollama']);
@@ -36,6 +38,17 @@ export interface ResolvePresetBaseURLInput {
valuesBaseUrl?: string;
}
export interface ResolvePresetTransportInput {
presetBackendType: AIProviderType;
presetFixedApiFormat?: string;
valuesApiFormat?: string;
}
export interface ResolvePresetTransportResult {
type: AIProviderType;
apiFormat?: string;
}
export const getProviderHostname = (raw?: string): string => {
if (!raw) return '';
try {
@@ -56,7 +69,7 @@ export const getProviderFingerprint = (raw?: string): string => {
}
};
export const matchQwenPresetKey = (provider: Pick<AIProviderConfig, 'type' | 'baseUrl'>): string | null => {
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),
@@ -103,3 +116,28 @@ export const resolvePresetBaseURL = ({
}
return presetDefaultBaseUrl;
};
export const resolvePresetTransport = ({
presetBackendType,
presetFixedApiFormat,
valuesApiFormat,
}: ResolvePresetTransportInput): ResolvePresetTransportResult => {
if (presetFixedApiFormat) {
return {
type: presetBackendType,
apiFormat: presetFixedApiFormat,
};
}
if (presetBackendType === 'custom') {
return {
type: presetBackendType,
apiFormat: valuesApiFormat || 'openai',
};
}
return {
type: presetBackendType,
apiFormat: undefined,
};
};