From 352975d75347bb165d9103a14edfd2de7e2b84a5 Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Sun, 30 Aug 2026 10:57:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E6=94=AF=E6=8C=81=E7=9B=B4?= =?UTF-8?q?=E6=8E=A5=E9=85=8D=E7=BD=AE=E6=A8=A1=E5=9E=8BAPI=E5=AF=86?= =?UTF-8?q?=E9=92=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/features/settings/SettingsView.vue | 32 ++++++++++++++++--- frontend/src/services/providerService.ts | 12 +++++++ frontend/src/stores/provider.spec.ts | 18 +++++++++-- frontend/src/stores/provider.ts | 22 ++++++++++--- 4 files changed, 73 insertions(+), 11 deletions(-) diff --git a/frontend/src/features/settings/SettingsView.vue b/frontend/src/features/settings/SettingsView.vue index cf81403..de7ee7a 100644 --- a/frontend/src/features/settings/SettingsView.vue +++ b/frontend/src/features/settings/SettingsView.vue @@ -18,8 +18,10 @@ const showProviderForm = ref(false) const editingProviderId = ref(null) const providerAction = ref('') const testResults = ref>({}) +const providerApiKey = ref('') const providerForm = reactive({ preset_id: '', provider_type: 'openai_compatible' as ProviderType, name: '', base_url: '', default_model: '', credential_id: '', enabled: true }) const formModels = computed(() => editingProviderId.value ? providerStore.modelsByProvider[editingProviderId.value] ?? [] : []) +const selectedPreset = computed(() => providerStore.presets.find((item) => item.preset_id === providerForm.preset_id) ?? null) onMounted(async () => { await Promise.all([providerStore.loadProviders(), providerStore.loadPresets(), settingsStore.loadDiagnostics()]) @@ -38,7 +40,9 @@ function openProvider(provider?: ProviderConfig) { 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 }) + providerApiKey.value = '' showProviderForm.value = true + if (providerForm.credential_id) void providerStore.loadCredentialStatus(providerForm.credential_id).catch(() => undefined) if (provider) void providerStore.loadModels(provider.provider_id).catch(() => undefined) } @@ -51,18 +55,33 @@ function applyProviderPreset() { base_url: preset.base_url, credential_id: preset.default_credential_id ?? '', }) + providerApiKey.value = '' + if (providerForm.credential_id) void providerStore.loadCredentialStatus(providerForm.credential_id).catch(() => undefined) +} + +function closeProvider() { + providerApiKey.value = '' + showProviderForm.value = false } async function saveProvider() { providerAction.value = '' + const credentialId = providerForm.credential_id.trim() + const requiresApiKey = Boolean(selectedPreset.value?.requires_credential) + if (requiresApiKey && !providerApiKey.value && !providerStore.credentialConfiguredById[credentialId]) { + providerAction.value = '请输入 API Key。密钥将由后端加密保存。' + return + } const data = { ...providerForm, base_url: providerForm.base_url || undefined, credential_id: providerForm.credential_id || undefined, capabilities: {}, has_credential: Boolean(providerForm.credential_id) } try { + if (providerApiKey.value) await providerStore.saveCredential(credentialId, providerApiKey.value) const saved = editingProviderId.value ? await providerStore.updateProvider(editingProviderId.value, data) : await providerStore.addProvider(data) - showProviderForm.value = false + closeProvider() if (saved.enabled) void providerStore.loadModels(saved.provider_id).catch(() => undefined) } catch (error) { + providerApiKey.value = '' providerAction.value = error instanceof Error ? error.message : 'Provider 保存失败' } } @@ -125,7 +144,7 @@ async function chooseDefaultModel(provider: ProviderConfig, event: Event) {

AI Core 诊断

{{ settingsStore.diagnosticsError }}
{{ settingsStore.aiCoreStatus }}

Sidecar 状态

AI Core 不可用时,Markdown 编辑仍可继续使用。

{{ settingsStore.aiCoreAddress }}

开发 API 地址

正式桌面环境由 Sidecar Manager 动态提供。

- diff --git a/frontend/src/services/providerService.ts b/frontend/src/services/providerService.ts index 2e52a20..a8bed0d 100644 --- a/frontend/src/services/providerService.ts +++ b/frontend/src/services/providerService.ts @@ -49,6 +49,18 @@ export async function listProviderPresets(): Promise { return response.items } +export async function getCredentialStatus(credentialId: string): Promise { + const response = await apiClient.get<{ credential_id: string; configured: boolean }>(`/api/credentials/${encodeURIComponent(credentialId)}`) + return response.configured +} + +export async function putCredential(credentialId: string, apiKey: string): Promise { + await apiClient.put<{ credential_id: string; configured: boolean }>( + `/api/credentials/${encodeURIComponent(credentialId)}`, + { api_key: apiKey }, + ) +} + export async function updateProvider(providerId: string, data: Partial): Promise { const response = await apiClient.patch(`/api/providers/${providerId}`, { name: data.name, diff --git a/frontend/src/stores/provider.spec.ts b/frontend/src/stores/provider.spec.ts index a2798d5..f4388e1 100644 --- a/frontend/src/stores/provider.spec.ts +++ b/frontend/src/stores/provider.spec.ts @@ -8,6 +8,8 @@ vi.mock('@/services/providerService', () => ({ mockModels: {}, listProviders: vi.fn(), listProviderPresets: vi.fn(), + getCredentialStatus: vi.fn(), + putCredential: vi.fn(), listModels: vi.fn(), createProvider: vi.fn(), updateProvider: vi.fn(), @@ -16,7 +18,7 @@ vi.mock('@/services/providerService', () => ({ })) import { useProviderStore } from './provider' -import { listModels, listProviderPresets, listProviders } from '@/services/providerService' +import { getCredentialStatus, listModels, listProviderPresets, listProviders, putCredential } from '@/services/providerService' import { ApiErrorClass } from '@/services/apiClient' const providers: ProviderConfig[] = [ @@ -49,6 +51,8 @@ beforeEach(() => { vi.clearAllMocks() vi.mocked(listProviders).mockResolvedValue(providers) vi.mocked(listProviderPresets).mockResolvedValue(presets) + vi.mocked(getCredentialStatus).mockResolvedValue(false) + vi.mocked(putCredential).mockResolvedValue(undefined) }) describe('provider store model discovery', () => { @@ -89,6 +93,16 @@ describe('provider store model discovery', () => { await expect(store.loadModels('openai')).rejects.toThrow('Credential is unavailable') - expect(store.modelErrorsByProvider.openai).toContain('DEEPSEEK_API_KEY') + expect(store.modelErrorsByProvider.openai).toContain('填写并保存') + }) + + it('sends an API key to the credential endpoint without storing it in Pinia', async () => { + const store = useProviderStore() + + await store.saveCredential('deepseek', 'sk-test-sensitive-value') + + expect(putCredential).toHaveBeenCalledWith('deepseek', 'sk-test-sensitive-value') + expect(store.credentialConfiguredById.deepseek).toBe(true) + expect(JSON.stringify(store.$state)).not.toContain('sk-test-sensitive-value') }) }) diff --git a/frontend/src/stores/provider.ts b/frontend/src/stores/provider.ts index ca12557..b097fe0 100644 --- a/frontend/src/stores/provider.ts +++ b/frontend/src/stores/provider.ts @@ -1,7 +1,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 { createProvider, deleteProvider as deleteProviderRequest, getCredentialStatus, listModels, listProviderPresets, listProviders, mockProviders, mockModels, putCredential, testProvider as testProviderRequest, updateProvider as updateProviderRequest } from '@/services/providerService' import { ApiErrorClass } from '@/services/apiClient' export const useProviderStore = defineStore('provider', () => { @@ -10,6 +10,7 @@ export const useProviderStore = defineStore('provider', () => { const modelsByProvider = ref>(mockModels) const modelLoadingByProvider = ref>({}) const modelErrorsByProvider = ref>({}) + const credentialConfiguredById = ref>({}) const defaultProviderId = ref('mock') const isLoading = ref(false) const error = ref(null) @@ -53,10 +54,7 @@ export const useProviderStore = defineStore('provider', () => { 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}。` + message = '尚未配置 API Key,请编辑该 Provider 后填写并保存。' } else if (reason instanceof ApiErrorClass && reason.code === 'PROVIDER_AUTH_FAILED') { message = `鉴权失败,请检查凭据“${credentialId || '未设置'}”对应的 API Key 是否有效。` } @@ -73,6 +71,17 @@ export const useProviderStore = defineStore('provider', () => { ) } + async function loadCredentialStatus(credentialId: string): Promise { + const configured = await getCredentialStatus(credentialId) + credentialConfiguredById.value[credentialId] = configured + return configured + } + + async function saveCredential(credentialId: string, apiKey: string) { + await putCredential(credentialId, apiKey) + credentialConfiguredById.value[credentialId] = true + } + async function addProvider(data: Omit) { const newProvider = await createProvider(data) providers.value.push(newProvider) @@ -108,6 +117,7 @@ export const useProviderStore = defineStore('provider', () => { modelsByProvider, modelLoadingByProvider, modelErrorsByProvider, + credentialConfiguredById, defaultProviderId, enabledProviders, defaultProvider, @@ -117,6 +127,8 @@ export const useProviderStore = defineStore('provider', () => { loadPresets, loadModels, refreshEnabledModels, + loadCredentialStatus, + saveCredential, addProvider, updateProvider, deleteProvider,