diff --git a/frontend/src/contracts/index.ts b/frontend/src/contracts/index.ts index cf1a1be..2a3d973 100644 --- a/frontend/src/contracts/index.ts +++ b/frontend/src/contracts/index.ts @@ -295,6 +295,7 @@ export interface ProviderPreset { name: string provider_type: ProviderType base_url: string + default_credential_id?: string | null requires_credential: boolean } @@ -523,6 +524,7 @@ export interface ApiProviderPreset { name: string provider_type: ApiProviderType base_url: string + default_credential_id?: string | null requires_credential: boolean } diff --git a/frontend/src/features/settings/SettingsView.vue b/frontend/src/features/settings/SettingsView.vue index 62503f8..cf81403 100644 --- a/frontend/src/features/settings/SettingsView.vue +++ b/frontend/src/features/settings/SettingsView.vue @@ -35,7 +35,9 @@ function presetIdFor(provider?: ProviderConfig) { function openProvider(provider?: ProviderConfig) { editingProviderId.value = provider?.provider_id ?? null - Object.assign(providerForm, { preset_id: presetIdFor(provider), provider_type: provider?.provider_type ?? 'openai_compatible', name: provider?.name ?? '', base_url: provider?.base_url ?? '', default_model: provider?.default_model ?? '', credential_id: provider?.credential_id ?? '', enabled: provider?.enabled ?? true }) + const presetId = presetIdFor(provider) + const preset = providerStore.presets.find((item) => item.preset_id === presetId) + Object.assign(providerForm, { preset_id: presetId, provider_type: provider?.provider_type ?? 'openai_compatible', name: provider?.name ?? '', base_url: provider?.base_url ?? '', default_model: provider?.default_model ?? '', credential_id: provider?.credential_id ?? preset?.default_credential_id ?? '', enabled: provider?.enabled ?? true }) showProviderForm.value = true if (provider) void providerStore.loadModels(provider.provider_id).catch(() => undefined) } @@ -47,6 +49,7 @@ function applyProviderPreset() { provider_type: preset.provider_type, name: preset.name, base_url: preset.base_url, + credential_id: preset.default_credential_id ?? '', }) } @@ -141,7 +144,7 @@ async function chooseDefaultModel(provider: ProviderConfig, event: Event) { -
此处不输入或回显 API Key,密钥明文由 Stronghold 保存。
+
这里填写凭据标识,不是 API Key。DeepSeek 开发环境默认读取 DEEPSEEK_API_KEY,密钥明文不会保存到 Provider。
diff --git a/frontend/src/stores/provider.spec.ts b/frontend/src/stores/provider.spec.ts index 95cb3ad..a2798d5 100644 --- a/frontend/src/stores/provider.spec.ts +++ b/frontend/src/stores/provider.spec.ts @@ -17,6 +17,7 @@ vi.mock('@/services/providerService', () => ({ import { useProviderStore } from './provider' import { listModels, listProviderPresets, listProviders } from '@/services/providerService' +import { ApiErrorClass } from '@/services/apiClient' const providers: ProviderConfig[] = [ { @@ -27,6 +28,7 @@ const providers: ProviderConfig[] = [ default_model: '', enabled: true, capabilities: { chat: true }, + credential_id: 'deepseek', has_credential: true, }, ] @@ -37,6 +39,7 @@ const presets: ProviderPreset[] = [ name: 'DeepSeek', provider_type: 'openai_compatible', base_url: 'https://api.deepseek.com', + default_credential_id: 'deepseek', requires_credential: true, }, ] @@ -76,4 +79,16 @@ describe('provider store model discovery', () => { expect(store.modelLoadingByProvider.openai).toBe(false) expect(store.modelErrorsByProvider.openai).toBe('认证失败') }) + + it('explains how to inject a missing DeepSeek credential', async () => { + vi.mocked(listModels).mockRejectedValue( + new ApiErrorClass('PROVIDER_CREDENTIAL_MISSING', 'Credential is unavailable') + ) + const store = useProviderStore() + await store.loadProviders() + + await expect(store.loadModels('openai')).rejects.toThrow('Credential is unavailable') + + expect(store.modelErrorsByProvider.openai).toContain('DEEPSEEK_API_KEY') + }) }) diff --git a/frontend/src/stores/provider.ts b/frontend/src/stores/provider.ts index 999920c..ca12557 100644 --- a/frontend/src/stores/provider.ts +++ b/frontend/src/stores/provider.ts @@ -2,6 +2,7 @@ import { defineStore } from 'pinia' import { ref, computed } from 'vue' import type { ProviderConfig, ModelInfo, ProviderPreset } from '@/contracts' import { createProvider, deleteProvider as deleteProviderRequest, listModels, listProviderPresets, listProviders, mockProviders, mockModels, testProvider as testProviderRequest, updateProvider as updateProviderRequest } from '@/services/providerService' +import { ApiErrorClass } from '@/services/apiClient' export const useProviderStore = defineStore('provider', () => { const providers = ref(mockProviders) @@ -48,7 +49,17 @@ export const useProviderStore = defineStore('provider', () => { modelsByProvider.value[providerId] = uniqueModels return uniqueModels } catch (reason) { - const message = reason instanceof Error ? reason.message : '模型列表获取失败' + const provider = providers.value.find((item) => item.provider_id === providerId) + const credentialId = provider?.credential_id + let message = reason instanceof Error ? reason.message : '模型列表获取失败' + if (reason instanceof ApiErrorClass && reason.code === 'PROVIDER_CREDENTIAL_MISSING') { + const environmentName = credentialId === 'deepseek' + ? 'DEEPSEEK_API_KEY' + : `AINOTE_CREDENTIAL_${credentialId?.replace(/[^A-Za-z0-9]/g, '_').toUpperCase() || ''}` + message = `未找到凭据“${credentialId || '未设置'}”,请在启动 AI Core 前设置 ${environmentName}。` + } else if (reason instanceof ApiErrorClass && reason.code === 'PROVIDER_AUTH_FAILED') { + message = `鉴权失败,请检查凭据“${credentialId || '未设置'}”对应的 API Key 是否有效。` + } modelErrorsByProvider.value[providerId] = message throw reason } finally {