feat(ai): 接入 CodeBuddy CLI 并兼容官方登录态

- 新增 CodeBuddy CLI provider,支持 codebuddy/cbc 命令调用与输出解析
- 将 Base URL、API Key/Auth Token、自定义请求头映射到 CodeBuddy CLI 环境变量
- 扩展 custom provider 路由与测试链路,兼容空 Base URL 和 CLI 默认模型选择
- AI 设置新增 CodeBuddy 预设,并补齐 preset 回显识别与匹配逻辑
- 修正就绪态、模型列表与表单校验,允许留空凭证直接复用本机已登录账号
- 补充前后端定向测试并覆盖 CodeBuddy 配置展示文案

Close #574
This commit is contained in:
Syngnat
2026-06-18 12:06:58 +08:00
parent c8fe90cbee
commit f457f6aaca
16 changed files with 842 additions and 30 deletions

View File

@@ -29,6 +29,7 @@ const PRESETS: PresetMatcher[] = [
defaultBaseUrl: QWEN_CODING_PLAN_ANTHROPIC_BASE_URL,
fixedApiFormat: 'claude-cli',
},
{ key: 'codebuddy', backendType: 'custom', defaultBaseUrl: '', fixedApiFormat: 'codebuddy-cli' },
{ key: 'custom', backendType: 'custom', defaultBaseUrl: '' },
];
@@ -112,7 +113,7 @@ describe('ai provider preset helpers', () => {
it('keeps the user-entered base URL for custom-like presets', () => {
expect(resolvePresetBaseURL({
presetKey: 'custom',
presetKey: 'codebuddy',
presetDefaultBaseUrl: '',
valuesBaseUrl: 'https://example-proxy.internal/v1',
})).toBe('https://example-proxy.internal/v1');
@@ -182,4 +183,18 @@ describe('resolveProviderPresetKey', () => {
expect(key).toBe('qwen-bailian');
});
it('能识别没有 Base URL 的 CodeBuddy CLI 预设', () => {
const key = resolveProviderPresetKey(
{
type: 'custom',
apiFormat: 'codebuddy-cli',
baseUrl: '',
},
PRESETS,
'custom',
);
expect(key).toBe('codebuddy');
});
});

View File

@@ -17,7 +17,7 @@ export const QWEN_CODING_PLAN_MODELS = [
'glm-4.7',
];
const CUSTOM_LIKE_PRESET_KEYS = new Set(['custom', 'ollama']);
const CUSTOM_LIKE_PRESET_KEYS = new Set(['custom', 'ollama', 'codebuddy']);
export interface ResolvePresetModelSelectionInput {
presetKey: string;
@@ -126,6 +126,17 @@ export const resolveProviderPresetKey = (
}
const fingerprint = getProviderFingerprint(provider.baseUrl);
const formatOnlyPreset = presets.find((preset) =>
preset.backendType === provider.type
&& Boolean(preset.fixedApiFormat)
&& preset.fixedApiFormat === provider.apiFormat
&& getProviderFingerprint(preset.defaultBaseUrl) === ''
&& fingerprint === '',
);
if (formatOnlyPreset) {
return formatOnlyPreset.key;
}
const exactPreset = presets.find((preset) =>
preset.backendType === provider.type
&& fingerprint !== ''