Fix/frontend review findings #4
@@ -295,6 +295,7 @@ export interface ProviderPreset {
|
|||||||
name: string
|
name: string
|
||||||
provider_type: ProviderType
|
provider_type: ProviderType
|
||||||
base_url: string
|
base_url: string
|
||||||
|
default_credential_id?: string | null
|
||||||
requires_credential: boolean
|
requires_credential: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,6 +524,7 @@ export interface ApiProviderPreset {
|
|||||||
name: string
|
name: string
|
||||||
provider_type: ApiProviderType
|
provider_type: ApiProviderType
|
||||||
base_url: string
|
base_url: string
|
||||||
|
default_credential_id?: string | null
|
||||||
requires_credential: boolean
|
requires_credential: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ function presetIdFor(provider?: ProviderConfig) {
|
|||||||
|
|
||||||
function openProvider(provider?: ProviderConfig) {
|
function openProvider(provider?: ProviderConfig) {
|
||||||
editingProviderId.value = provider?.provider_id ?? null
|
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
|
showProviderForm.value = true
|
||||||
if (provider) void providerStore.loadModels(provider.provider_id).catch(() => undefined)
|
if (provider) void providerStore.loadModels(provider.provider_id).catch(() => undefined)
|
||||||
}
|
}
|
||||||
@@ -47,6 +49,7 @@ function applyProviderPreset() {
|
|||||||
provider_type: preset.provider_type,
|
provider_type: preset.provider_type,
|
||||||
name: preset.name,
|
name: preset.name,
|
||||||
base_url: preset.base_url,
|
base_url: preset.base_url,
|
||||||
|
credential_id: preset.default_credential_id ?? '',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +144,7 @@ async function chooseDefaultModel(provider: ProviderConfig, event: Event) {
|
|||||||
<input v-model="providerForm.default_model" class="input" :list="editingProviderId ? 'provider-model-options' : undefined" placeholder="保存后自动获取,也可以手动输入" />
|
<input v-model="providerForm.default_model" class="input" :list="editingProviderId ? 'provider-model-options' : undefined" placeholder="保存后自动获取,也可以手动输入" />
|
||||||
<datalist id="provider-model-options"><option v-for="model in formModels" :key="model.model_id" :value="model.model_id">{{ model.name }}</option></datalist>
|
<datalist id="provider-model-options"><option v-for="model in formModels" :key="model.model_id" :value="model.model_id">{{ model.name }}</option></datalist>
|
||||||
</div>
|
</div>
|
||||||
<div class="field"><label>Credential ID</label><input v-model="providerForm.credential_id" class="input" placeholder="由桌面 Host 注入的凭据标识" /><small class="subtle">此处不输入或回显 API Key,密钥明文由 Stronghold 保存。</small></div>
|
<div class="field"><label>Credential ID</label><input v-model="providerForm.credential_id" class="input" placeholder="例如 deepseek" /><small class="subtle">这里填写凭据标识,不是 API Key。DeepSeek 开发环境默认读取 DEEPSEEK_API_KEY,密钥明文不会保存到 Provider。</small></div>
|
||||||
<label class="inline-actions"><input v-model="providerForm.enabled" type="checkbox" /> 启用</label>
|
<label class="inline-actions"><input v-model="providerForm.enabled" type="checkbox" /> 启用</label>
|
||||||
<div class="inline-actions"><button class="button-primary">保存并获取模型</button><button type="button" class="button-secondary" @click="showProviderForm = false">取消</button></div>
|
<div class="inline-actions"><button class="button-primary">保存并获取模型</button><button type="button" class="button-secondary" @click="showProviderForm = false">取消</button></div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ vi.mock('@/services/providerService', () => ({
|
|||||||
|
|
||||||
import { useProviderStore } from './provider'
|
import { useProviderStore } from './provider'
|
||||||
import { listModels, listProviderPresets, listProviders } from '@/services/providerService'
|
import { listModels, listProviderPresets, listProviders } from '@/services/providerService'
|
||||||
|
import { ApiErrorClass } from '@/services/apiClient'
|
||||||
|
|
||||||
const providers: ProviderConfig[] = [
|
const providers: ProviderConfig[] = [
|
||||||
{
|
{
|
||||||
@@ -27,6 +28,7 @@ const providers: ProviderConfig[] = [
|
|||||||
default_model: '',
|
default_model: '',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
capabilities: { chat: true },
|
capabilities: { chat: true },
|
||||||
|
credential_id: 'deepseek',
|
||||||
has_credential: true,
|
has_credential: true,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -37,6 +39,7 @@ const presets: ProviderPreset[] = [
|
|||||||
name: 'DeepSeek',
|
name: 'DeepSeek',
|
||||||
provider_type: 'openai_compatible',
|
provider_type: 'openai_compatible',
|
||||||
base_url: 'https://api.deepseek.com',
|
base_url: 'https://api.deepseek.com',
|
||||||
|
default_credential_id: 'deepseek',
|
||||||
requires_credential: true,
|
requires_credential: true,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -76,4 +79,16 @@ describe('provider store model discovery', () => {
|
|||||||
expect(store.modelLoadingByProvider.openai).toBe(false)
|
expect(store.modelLoadingByProvider.openai).toBe(false)
|
||||||
expect(store.modelErrorsByProvider.openai).toBe('认证失败')
|
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')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import type { ProviderConfig, ModelInfo, ProviderPreset } from '@/contracts'
|
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, listModels, listProviderPresets, listProviders, mockProviders, mockModels, testProvider as testProviderRequest, updateProvider as updateProviderRequest } from '@/services/providerService'
|
||||||
|
import { ApiErrorClass } from '@/services/apiClient'
|
||||||
|
|
||||||
export const useProviderStore = defineStore('provider', () => {
|
export const useProviderStore = defineStore('provider', () => {
|
||||||
const providers = ref<ProviderConfig[]>(mockProviders)
|
const providers = ref<ProviderConfig[]>(mockProviders)
|
||||||
@@ -48,7 +49,17 @@ export const useProviderStore = defineStore('provider', () => {
|
|||||||
modelsByProvider.value[providerId] = uniqueModels
|
modelsByProvider.value[providerId] = uniqueModels
|
||||||
return uniqueModels
|
return uniqueModels
|
||||||
} catch (reason) {
|
} 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() || '<ID>'}`
|
||||||
|
message = `未找到凭据“${credentialId || '未设置'}”,请在启动 AI Core 前设置 ${environmentName}。`
|
||||||
|
} else if (reason instanceof ApiErrorClass && reason.code === 'PROVIDER_AUTH_FAILED') {
|
||||||
|
message = `鉴权失败,请检查凭据“${credentialId || '未设置'}”对应的 API Key 是否有效。`
|
||||||
|
}
|
||||||
modelErrorsByProvider.value[providerId] = message
|
modelErrorsByProvider.value[providerId] = message
|
||||||
throw reason
|
throw reason
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user