Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions src/main/presenter/llmProviderPresenter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1001,9 +1001,43 @@ export class LLMProviderPresenter implements ILlmProviderPresenter {
return this.config.maxConcurrentStreams
}

async check(providerId: string): Promise<{ isOk: boolean; errorMsg: string | null }> {
const provider = this.getProviderInstance(providerId)
return provider.check()
async check(
providerId: string,
modelId?: string
): Promise<{ isOk: boolean; errorMsg: string | null }> {
try {
const provider = this.getProviderInstance(providerId)

// 如果提供了modelId,使用completions方法进行测试
if (modelId) {
try {
const testMessage = [{ role: 'user' as const, content: 'hi' }]
const response: LLMResponse | null = await Promise.race([
provider.completions(testMessage, modelId, 0.1, 10),
new Promise<null>((resolve) => setTimeout(() => resolve(null), 60000))
])
// 检查响应是否有效
if (
response &&
(response.content || response.content === '' || response.reasoning_content)
) {
return { isOk: true, errorMsg: null }
} else {
return { isOk: false, errorMsg: 'Model response is invalid' }
}
} catch (error) {
console.error(`Model ${modelId} check failed:`, error)
const errorMessage = error instanceof Error ? error.message : String(error)
return { isOk: false, errorMsg: `Model test failed: ${errorMessage}` }
}
} else {
return { isOk: false, errorMsg: 'Model ID is required' }
}
} catch (error) {
console.error(`Provider ${providerId} check failed:`, error)
const errorMessage = error instanceof Error ? error.message : String(error)
return { isOk: false, errorMsg: `Provider check failed: ${errorMessage}` }
}
}

async getKeyStatus(providerId: string): Promise<KeyStatus | null> {
Expand Down
13 changes: 13 additions & 0 deletions src/renderer/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { useSettingsStore } from '@/stores/settings'
import { useThemeStore } from '@/stores/theme'
import { useLanguageStore } from '@/stores/language'
import TranslatePopup from '@/components/popup/TranslatePopup.vue'
import ModelCheckDialog from '@/components/settings/ModelCheckDialog.vue'
import { useModelCheckStore } from '@/stores/modelCheck'

const route = useRoute()
const configPresenter = usePresenter('configPresenter')
Expand All @@ -22,6 +24,7 @@ const { toast } = useToast()
const settingsStore = useSettingsStore()
const themeStore = useThemeStore()
const langStore = useLanguageStore()
const modelCheckStore = useModelCheckStore()
// 错误通知队列及当前正在显示的错误
const errorQueue = ref<Array<{ id: string; title: string; message: string; type: string }>>([])
const currentErrorId = ref<string | null>(null)
Expand Down Expand Up @@ -298,5 +301,15 @@ onBeforeUnmount(() => {
<Toaster />
<SelectedTextContextMenu />
<TranslatePopup />
<!-- 全局模型检查弹窗 -->
<ModelCheckDialog
:open="modelCheckStore.isDialogOpen"
:provider-id="modelCheckStore.currentProviderId"
@update:open="
(open) => {
if (!open) modelCheckStore.closeDialog()
}
"
/>
</div>
</template>
206 changes: 206 additions & 0 deletions src/renderer/src/components/settings/ModelCheckDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
<template>
<Dialog v-model:open="isOpen" @update:open="onOpenChange">
<DialogContent class="sm:max-w-[500px] max-h-[80vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle>{{ t('settings.provider.dialog.modelCheck.title') }}</DialogTitle>
<DialogDescription>
{{ t('settings.provider.dialog.modelCheck.description') }}
</DialogDescription>
</DialogHeader>

<!-- 显示错误或成功消息 -->
<div v-if="result" class="mb-4 flex-shrink-0">
<div v-if="result.isOk" class="p-4 bg-green-50 border border-green-200 rounded-lg">
<div class="flex items-center">
<Icon icon="lucide:check-circle" class="w-5 h-5 text-green-600 mr-2 flex-shrink-0" />
<span class="text-green-800 font-medium">{{
t('settings.provider.dialog.modelCheck.success')
}}</span>
</div>
</div>
<div v-else class="p-4 bg-red-50 border border-red-200 rounded-lg">
<div class="flex items-start">
<Icon icon="lucide:x-circle" class="w-5 h-5 text-red-600 mr-2 mt-0.5 flex-shrink-0" />
<div class="text-red-800 min-w-0 flex-1">
<div class="font-medium">{{ t('settings.provider.dialog.modelCheck.failed') }}</div>
<div class="text-sm mt-1 break-words whitespace-pre-wrap overflow-y-auto max-h-40">
{{ result.errorMsg }}
</div>
</div>
</div>
</div>
</div>

<!-- 主要内容区域 -->
<div class="flex-1 min-h-0 overflow-y-auto">
<!-- 没有模型的提示 -->
<div v-if="!hasModels && !result" class="py-6">
<div class="text-center text-muted-foreground">
<Icon icon="lucide:info" class="w-8 h-8 mx-auto mb-2" />
<p>{{ t('settings.provider.dialog.modelCheck.noModels') }}</p>
</div>
</div>

<!-- 模型选择表单 -->
<div v-if="!result && hasModels" class="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center gap-4">
<Label for="model" class="text-right">
{{ t('settings.provider.dialog.modelCheck.model') }}
</Label>
<Select v-model="selectedModelId" required>
<SelectTrigger class="col-span-3">
<SelectValue
:placeholder="t('settings.provider.dialog.modelCheck.modelPlaceholder')"
/>
</SelectTrigger>
<SelectContent class="max-h-60">
<SelectItem v-for="model in availableModels" :key="model.id" :value="model.id">
{{ model.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
</div>

<!-- 进度指示器 -->
<div v-if="isChecking" class="flex items-center justify-center py-6">
<div class="flex items-center">
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mr-3"></div>
<span class="text-muted-foreground">{{
t('settings.provider.dialog.modelCheck.checking')
}}</span>
</div>
</div>
</div>

<DialogFooter class="flex-shrink-0">
<Button type="button" variant="outline" @click="closeDialog">
{{ result ? t('dialog.close') : t('dialog.cancel') }}
</Button>
<Button
v-if="!result && hasModels"
type="button"
:disabled="!selectedModelId || isChecking"
@click="handleCheck"
>
<div
v-if="isChecking"
class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"
></div>
{{
isChecking
? t('settings.provider.dialog.modelCheck.checking')
: t('settings.provider.dialog.modelCheck.test')
}}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>

<script setup lang="ts">
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select'
import { useSettingsStore } from '@/stores/settings'
import { Icon } from '@iconify/vue'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'

const { t } = useI18n()
const settingsStore = useSettingsStore()

const props = defineProps<{
open: boolean
providerId: string
}>()

const emit = defineEmits<{
(e: 'update:open', value: boolean): void
}>()

const isOpen = ref(props.open)
const isChecking = ref(false)
const selectedModelId = ref<string>('')
const result = ref<{ isOk: boolean; errorMsg: string | null } | null>(null)

// 计算可用的模型列表
const availableModels = computed(() => {
const providerModels = settingsStore.enabledModels.find((p) => p.providerId === props.providerId)
return providerModels?.models || []
})

// 检查是否有可用的模型
const hasModels = computed(() => availableModels.value.length > 0)

// 监听 open 属性变化
watch(
() => props.open,
(newVal) => {
if (newVal && !isOpen.value) {
resetDialog()
}
isOpen.value = newVal
}
)

// 监听 isOpen 变化,同步更新到父组件
watch(
() => isOpen.value,
(newVal) => {
emit('update:open', newVal)
}
)

const onOpenChange = (open: boolean) => {
isOpen.value = open
if (!open) {
resetDialog()
}
}

const resetDialog = () => {
selectedModelId.value = ''
result.value = null
isChecking.value = false
}

const closeDialog = () => {
isOpen.value = false
}

const handleCheck = async () => {
if (!selectedModelId.value) return

try {
isChecking.value = true
result.value = null

// 调用设置store的检查方法
const checkResult = await settingsStore.checkProvider(props.providerId, selectedModelId.value)
result.value = checkResult
} catch (error) {
console.error('Model check failed:', error)
result.value = {
isOk: false,
errorMsg: error instanceof Error ? error.message : 'Unknown error occurred'
}
} finally {
isChecking.value = false
}
}
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
:provider-websites="providerWebsites"
@api-host-change="handleApiHostChange"
@api-key-change="handleApiKeyChange"
@validate-key="handleApiKeyEnter"
@validate-key="openModelCheckDialog"
@delete-provider="showDeleteProviderDialog = true"
@oauth-success="handleOAuthSuccess"
@oauth-error="handleOAuthError"
Expand Down Expand Up @@ -73,6 +73,7 @@ import AzureProviderConfig from './AzureProviderConfig.vue'
import GeminiSafetyConfig from './GeminiSafetyConfig.vue'
import ProviderModelManager from './ProviderModelManager.vue'
import ProviderDialogContainer from './ProviderDialogContainer.vue'
import { useModelCheckStore } from '@/stores/modelCheck'
import { levelToValueMap, safetyCategories } from '@/lib/gemini'

interface ProviderWebsites {
Expand Down Expand Up @@ -105,6 +106,7 @@ const props = defineProps<{
}>()

const settingsStore = useSettingsStore()
const modelCheckStore = useModelCheckStore()
const apiKey = ref(props.provider.apiKey || '')
const apiHost = ref(props.provider.baseUrl || '')
const azureApiVersion = ref('')
Expand Down Expand Up @@ -227,14 +229,6 @@ watch(
{ immediate: true } // Removed deep: true as provider object itself changes
)

const handleApiKeyEnter = async (value: string) => {
const inputElement = document.getElementById(`${props.provider.id}-apikey`)
if (inputElement) {
inputElement.blur()
}
await settingsStore.updateProviderApi(props.provider.id, value, undefined)
await validateApiKey()
}
const handleApiKeyChange = async (value: string) => {
await settingsStore.updateProviderApi(props.provider.id, value, undefined)
}
Expand Down Expand Up @@ -350,4 +344,8 @@ const handleConfigChanged = async () => {
// 模型配置变更后重新初始化数据
await initData()
}

const openModelCheckDialog = () => {
modelCheckStore.openDialog(props.provider.id)
}
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
variant="outline"
size="xs"
class="text-xs text-normal rounded-lg"
@click="validateApiKey"
@click="openModelCheckDialog"
>
<Icon icon="lucide:check-check" class="w-4 h-4 text-muted-foreground" />
{{ t('settings.provider.verifyKey') }}
Expand Down Expand Up @@ -244,6 +244,7 @@ import {
DialogFooter
} from '@/components/ui/dialog'
import { useSettingsStore } from '@/stores/settings'
import { useModelCheckStore } from '@/stores/modelCheck'
import type { LLM_PROVIDER } from '@shared/presenter'

const { t } = useI18n()
Expand All @@ -253,6 +254,7 @@ const props = defineProps<{
}>()

const settingsStore = useSettingsStore()
const modelCheckStore = useModelCheckStore()
const apiHost = ref(props.provider.baseUrl || '')
const apiKey = ref(props.provider.apiKey || '')
const showPullModelDialog = ref(false)
Expand Down Expand Up @@ -521,6 +523,10 @@ const validateApiKey = async () => {
}
}

const openModelCheckDialog = () => {
modelCheckStore.openDialog(props.provider.id)
}

// 监听 provider 变化
watch(
() => props.provider,
Expand Down
Loading