feat(stage-pages,stage-ui,i18n): disable auto trigger for chatcompletion check and use explict button (#1336)
Co-authored-by-agent: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Neko <neko@ayaka.moe>
This commit is contained in:
@@ -20,8 +20,12 @@ dialogs:
|
||||
baseUrlHelp: API endpoint URL (use default if unsure)
|
||||
accountId: Account ID
|
||||
validationSuccess: Configuration validation success
|
||||
validationPartial: Configuration partially validated
|
||||
validationFailed: Configuration validation failed
|
||||
validationError: 'Validation error: {error}'
|
||||
testGeneration: Test Generation
|
||||
testGenerationRunning: Testing...
|
||||
testGenerationFailed: Generation test failed
|
||||
skipForNow: Skip for now
|
||||
saveAndContinue: Save and Continue
|
||||
next: Next
|
||||
|
||||
@@ -19,8 +19,12 @@ dialogs:
|
||||
baseUrlHelp: URL del endpoint de la API (usa el predeterminado si no estás seguro)
|
||||
accountId: ID de Cuenta
|
||||
validationSuccess: La validación de la configuración fue exitosa
|
||||
validationPartial: Configuración parcialmente validada
|
||||
validationFailed: La validación de la configuración falló
|
||||
validationError: 'Error de validación: {error}'
|
||||
testGeneration: Prueba de Generación
|
||||
testGenerationRunning: Probando...
|
||||
testGenerationFailed: Prueba de generación fallida
|
||||
skipForNow: Omitir por ahora
|
||||
saveAndContinue: Guardar y Continuar
|
||||
next: Siguiente
|
||||
|
||||
@@ -19,8 +19,12 @@ dialogs:
|
||||
baseUrlHelp: URL de l'endpoint API (utilisez la valeur par défaut si vous n’êtes pas sûr)
|
||||
accountId: ID du compte
|
||||
validationSuccess: Validation de la configuration réussie
|
||||
validationPartial: Configuration partiellement validée
|
||||
validationFailed: La validation de la configuration a échouée.
|
||||
validationError: 'Erreur de validation: {error}'
|
||||
testGeneration: Test de génération
|
||||
testGenerationRunning: Test en cours...
|
||||
testGenerationFailed: Test de génération échoué
|
||||
skipForNow: Passer pour l'instant
|
||||
saveAndContinue: Sauvegarder et Continuer
|
||||
next: Suivant
|
||||
|
||||
@@ -19,8 +19,12 @@ dialogs:
|
||||
baseUrlHelp: APIエンドポイントURL(不明な場合はデフォルトを使用)
|
||||
accountId: アカウントID
|
||||
validationSuccess: 設定検証に成功
|
||||
validationPartial: 設定が部分的に検証されました
|
||||
validationFailed: 設定検証に失敗
|
||||
validationError: '検証エラー: {error}'
|
||||
testGeneration: 生成テスト
|
||||
testGenerationRunning: テスト中...
|
||||
testGenerationFailed: 生成テストに失敗
|
||||
skipForNow: 今はスキップ
|
||||
saveAndContinue: 保存して続行
|
||||
next: 次へ
|
||||
|
||||
@@ -19,8 +19,12 @@ dialogs:
|
||||
baseUrlHelp: API 엔드포인트 URL(확실하지 않은 경우 기본값 사용)
|
||||
accountId: 계정 ID
|
||||
validationSuccess: 구성 검증 성공
|
||||
validationPartial: 구성이 부분적으로 검증됨
|
||||
validationFailed: 구성 검증 실패
|
||||
validationError: '검증 오류: {error}'
|
||||
testGeneration: 생성 테스트
|
||||
testGenerationRunning: 테스트 중...
|
||||
testGenerationFailed: 생성 테스트 실패
|
||||
skipForNow: 지금은 건너뛰기
|
||||
saveAndContinue: 저장 및 계속
|
||||
next: 다음
|
||||
|
||||
@@ -19,8 +19,12 @@ dialogs:
|
||||
baseUrlHelp: URL конечной точки API (оставьте по умолчанию, если не уверены)
|
||||
accountId: ID аккаунта
|
||||
validationSuccess: Configuration validation success
|
||||
validationPartial: Конфигурация частично проверена
|
||||
validationFailed: Проверка конфигурации не удалась
|
||||
validationError: 'Ошибка проверки: {error}'
|
||||
testGeneration: Тест генерации
|
||||
testGenerationRunning: Тестирование...
|
||||
testGenerationFailed: Тест генерации не удался
|
||||
skipForNow: Пропустить на сейчас
|
||||
saveAndContinue: Сохранить и продолжить
|
||||
next: Далее
|
||||
|
||||
@@ -19,8 +19,12 @@ dialogs:
|
||||
baseUrlHelp: Địa chỉ API đầu cuối (dùng mặc định nếu không chắc)
|
||||
accountId: ID tài khoản
|
||||
validationSuccess: Xác thực cấu hình thành công
|
||||
validationPartial: Cấu hình được xác thực một phần
|
||||
validationFailed: Xác thực cấu hình thất bại
|
||||
validationError: 'Lỗi xác thực: {error}'
|
||||
testGeneration: Kiểm tra tạo
|
||||
testGenerationRunning: Đang kiểm tra...
|
||||
testGenerationFailed: Kiểm tra tạo thất bại
|
||||
skipForNow: Bỏ qua
|
||||
saveAndContinue: Lưu và tiếp tục
|
||||
next: Tiếp
|
||||
|
||||
@@ -19,8 +19,12 @@ dialogs:
|
||||
baseUrlHelp: API 端点 URL(如果不确定请使用默认值)
|
||||
accountId: 账户 ID
|
||||
validationSuccess: 配置验证成功
|
||||
validationPartial: 配置部分验证通过
|
||||
validationFailed: 配置验证失败
|
||||
validationError: '验证错误:{error}'
|
||||
testGeneration: 测试生成
|
||||
testGenerationRunning: 测试中...
|
||||
testGenerationFailed: 生成测试失败
|
||||
skipForNow: 暂时跳过
|
||||
saveAndContinue: 保存并继续
|
||||
next: 下一步
|
||||
|
||||
@@ -19,8 +19,12 @@ dialogs:
|
||||
baseUrlHelp: API 端點 URL(如果不確定請使用預設值)
|
||||
accountId: 帳號 ID
|
||||
validationSuccess: 設定驗證成功
|
||||
validationPartial: 設定部分驗證通過
|
||||
validationFailed: 設定驗證失敗
|
||||
validationError: '驗證錯誤:{error}'
|
||||
testGeneration: 測試生成
|
||||
testGenerationRunning: 測試中...
|
||||
testGenerationFailed: 生成測試失敗
|
||||
skipForNow: 先略過
|
||||
saveAndContinue: 儲存並繼續
|
||||
next: 下一步
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
ProviderValidationAlerts,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness'
|
||||
@@ -53,6 +53,11 @@ const {
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
forceValid,
|
||||
hasManualValidators,
|
||||
isManualTesting,
|
||||
manualTestPassed,
|
||||
manualTestMessage,
|
||||
runManualTest,
|
||||
} = useProviderValidation(providerId)
|
||||
|
||||
function goToModelSelection() {
|
||||
@@ -88,39 +93,18 @@ function goToModelSelection() {
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
<div class="w-full flex items-center justify-between">
|
||||
<span>{{ t('settings.dialogs.onboarding.validationFailed') }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-2 rounded bg-red-100 px-2 py-0.5 text-xs text-red-600 font-medium transition-colors dark:bg-red-800/30 hover:bg-red-200 dark:text-red-300 dark:hover:bg-red-700/40"
|
||||
@click="forceValid"
|
||||
>
|
||||
{{ t('settings.pages.providers.common.continueAnyway') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
<div class="w-full flex items-center justify-between">
|
||||
<span>{{ t('settings.dialogs.onboarding.validationSuccess') }}</span>
|
||||
<button
|
||||
type="button"
|
||||
:class="['ml-2 rounded px-2 py-0.5 text-xs font-medium transition-colors', 'bg-green-100 text-green-600 hover:bg-green-200', 'dark:bg-green-800/30 dark:text-green-300 dark:hover:bg-green-700/40']"
|
||||
@click="goToModelSelection"
|
||||
>
|
||||
{{ t('settings.pages.providers.common.goToModelSelection') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<ProviderValidationAlerts
|
||||
:is-valid="isValid"
|
||||
:is-validating="isValidating"
|
||||
:validation-message="validationMessage"
|
||||
:has-manual-validators="hasManualValidators"
|
||||
:is-manual-testing="isManualTesting"
|
||||
:manual-test-passed="manualTestPassed"
|
||||
:manual-test-message="manualTestMessage"
|
||||
:on-run-test="runManualTest"
|
||||
:on-force-valid="forceValid"
|
||||
:on-go-to-model-selection="goToModelSelection"
|
||||
/>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderAccountIdInput,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
ProviderValidationAlerts,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
@@ -66,6 +66,11 @@ const {
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
forceValid,
|
||||
hasManualValidators,
|
||||
isManualTesting,
|
||||
manualTestPassed,
|
||||
manualTestMessage,
|
||||
runManualTest,
|
||||
} = useProviderValidation(providerId)
|
||||
</script>
|
||||
|
||||
@@ -113,30 +118,18 @@ const {
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
<div class="w-full flex items-center justify-between">
|
||||
<span>{{ t('settings.dialogs.onboarding.validationFailed') }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-2 rounded bg-red-100 px-2 py-0.5 text-xs text-red-600 font-medium transition-colors dark:bg-red-800/30 hover:bg-red-200 dark:text-red-300 dark:hover:bg-red-700/40"
|
||||
@click="forceValid"
|
||||
>
|
||||
{{ t('settings.pages.providers.common.continueAnyway') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
<ProviderValidationAlerts
|
||||
:is-valid="isValid"
|
||||
:is-validating="isValidating"
|
||||
:validation-message="validationMessage"
|
||||
:has-manual-validators="hasManualValidators"
|
||||
:is-manual-testing="isManualTesting"
|
||||
:manual-test-passed="manualTestPassed"
|
||||
:manual-test-message="manualTestMessage"
|
||||
:on-run-test="runManualTest"
|
||||
:on-force-valid="forceValid"
|
||||
:on-go-to-model-selection="() => router.push('/settings/modules/consciousness')"
|
||||
/>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderAccountIdInput,
|
||||
ProviderApiKeyInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
ProviderValidationAlerts,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
@@ -47,6 +47,11 @@ const {
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
forceValid,
|
||||
hasManualValidators,
|
||||
isManualTesting,
|
||||
manualTestPassed,
|
||||
manualTestMessage,
|
||||
runManualTest,
|
||||
} = useProviderValidation(providerId)
|
||||
</script>
|
||||
|
||||
@@ -78,30 +83,18 @@ const {
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
<div class="w-full flex items-center justify-between">
|
||||
<span>{{ t('settings.dialogs.onboarding.validationFailed') }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-2 rounded bg-red-100 px-2 py-0.5 text-xs text-red-600 font-medium transition-colors dark:bg-red-800/30 hover:bg-red-200 dark:text-red-300 dark:hover:bg-red-700/40"
|
||||
@click="forceValid"
|
||||
>
|
||||
{{ t('settings.pages.providers.common.continueAnyway') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
<ProviderValidationAlerts
|
||||
:is-valid="isValid"
|
||||
:is-validating="isValidating"
|
||||
:validation-message="validationMessage"
|
||||
:has-manual-validators="hasManualValidators"
|
||||
:is-manual-testing="isManualTesting"
|
||||
:manual-test-passed="manualTestPassed"
|
||||
:manual-test-message="manualTestMessage"
|
||||
:on-run-test="runManualTest"
|
||||
:on-force-valid="forceValid"
|
||||
:on-go-to-model-selection="() => router.push('/settings/modules/consciousness')"
|
||||
/>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
ProviderValidationAlerts,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
@@ -38,6 +38,11 @@ const {
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
forceValid,
|
||||
hasManualValidators,
|
||||
isManualTesting,
|
||||
manualTestPassed,
|
||||
manualTestMessage,
|
||||
runManualTest,
|
||||
} = useProviderValidation(providerId)
|
||||
</script>
|
||||
|
||||
@@ -60,30 +65,18 @@ const {
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
<div class="w-full flex items-center justify-between">
|
||||
<span>{{ t('settings.dialogs.onboarding.validationFailed') }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-2 rounded bg-red-100 px-2 py-0.5 text-xs text-red-600 font-medium transition-colors dark:bg-red-800/30 hover:bg-red-200 dark:text-red-300 dark:hover:bg-red-700/40"
|
||||
@click="forceValid"
|
||||
>
|
||||
{{ t('settings.pages.providers.common.continueAnyway') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
<ProviderValidationAlerts
|
||||
:is-valid="isValid"
|
||||
:is-validating="isValidating"
|
||||
:validation-message="validationMessage"
|
||||
:has-manual-validators="hasManualValidators"
|
||||
:is-manual-testing="isManualTesting"
|
||||
:manual-test-passed="manualTestPassed"
|
||||
:manual-test-message="manualTestMessage"
|
||||
:on-run-test="runManualTest"
|
||||
:on-force-valid="forceValid"
|
||||
:on-go-to-model-selection="() => router.push('/settings/modules/consciousness')"
|
||||
/>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
Alert,
|
||||
ProviderAdvancedSettings,
|
||||
ProviderBaseUrlInput,
|
||||
ProviderBasicSettings,
|
||||
ProviderSettingsContainer,
|
||||
ProviderSettingsLayout,
|
||||
ProviderValidationAlerts,
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
|
||||
@@ -39,6 +39,11 @@ const {
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
forceValid,
|
||||
hasManualValidators,
|
||||
isManualTesting,
|
||||
manualTestPassed,
|
||||
manualTestMessage,
|
||||
runManualTest,
|
||||
} = useProviderValidation(providerId)
|
||||
|
||||
const headers = ref<{ key: string, value: string }[]>(Object.entries(providers.value[providerId]?.headers || {}).map(([key, value]) => ({ key, value } as { key: string, value: string })) || [{ key: '', value: '' }])
|
||||
@@ -176,30 +181,18 @@ onMounted(() => {
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
<div class="w-full flex items-center justify-between">
|
||||
<span>{{ t('settings.dialogs.onboarding.validationFailed') }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-2 rounded bg-red-100 px-2 py-0.5 text-xs text-red-600 font-medium transition-colors dark:bg-red-800/30 hover:bg-red-200 dark:text-red-300 dark:hover:bg-red-700/40"
|
||||
@click="forceValid"
|
||||
>
|
||||
{{ t('settings.pages.providers.common.continueAnyway') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<Alert v-if="isValid && isValidating === 0" type="success">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationSuccess') }}
|
||||
</template>
|
||||
</Alert>
|
||||
<ProviderValidationAlerts
|
||||
:is-valid="isValid"
|
||||
:is-validating="isValidating"
|
||||
:validation-message="validationMessage"
|
||||
:has-manual-validators="hasManualValidators"
|
||||
:is-manual-testing="isManualTesting"
|
||||
:manual-test-passed="manualTestPassed"
|
||||
:manual-test-message="manualTestMessage"
|
||||
:on-run-test="runManualTest"
|
||||
:on-force-valid="forceValid"
|
||||
:on-go-to-model-selection="() => router.push('/settings/modules/consciousness')"
|
||||
/>
|
||||
</ProviderSettingsContainer>
|
||||
</ProviderSettingsLayout>
|
||||
</template>
|
||||
|
||||
@@ -5,6 +5,7 @@ export { default as ProviderBaseUrlInput } from './provider-base-url-input.vue'
|
||||
export { default as ProviderBasicSettings } from './provider-basic-settings.vue'
|
||||
export { default as ProviderSettingsContainer } from './provider-settings-container.vue'
|
||||
export { default as ProviderSettingsLayout } from './provider-settings-layout.vue'
|
||||
export { default as ProviderValidationAlerts } from './provider-validation-alerts.vue'
|
||||
|
||||
export { default as SpeechPlaygroundOpenAICompatible } from './speech-playground-openai-compatible.vue'
|
||||
export { default as SpeechPlayground } from './speech-playground.vue'
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Alert from '../../misc/alert.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
isValid: boolean
|
||||
isValidating: number
|
||||
validationMessage: string
|
||||
hasManualValidators: boolean
|
||||
isManualTesting: boolean
|
||||
manualTestPassed: boolean
|
||||
manualTestMessage: string
|
||||
onRunTest: () => void
|
||||
onForceValid: () => void
|
||||
onGoToModelSelection: () => void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Validation Error -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
<div class="w-full flex items-center justify-between">
|
||||
<span>{{ t('settings.dialogs.onboarding.validationFailed') }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-2 rounded bg-red-100 px-2 py-0.5 text-xs text-red-600 font-medium transition-colors dark:bg-red-800/30 hover:bg-red-200 dark:text-red-300 dark:hover:bg-red-700/40"
|
||||
@click="props.onForceValid"
|
||||
>
|
||||
{{ t('settings.pages.providers.common.continueAnyway') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<!-- Partial Validation: manual validators exist, no test attempted yet -->
|
||||
<Alert v-else-if="isValid && isValidating === 0 && hasManualValidators && !manualTestPassed && !manualTestMessage" type="info">
|
||||
<template #title>
|
||||
<div class="w-full flex items-center justify-between">
|
||||
<span>{{ t('settings.dialogs.onboarding.validationPartial') }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
:disabled="isManualTesting"
|
||||
:class="['rounded px-2 py-0.5 text-xs font-medium transition-colors', isManualTesting ? 'opacity-50 cursor-not-allowed' : '', 'bg-blue-100 text-blue-600 hover:bg-blue-200', 'dark:bg-blue-800/30 dark:text-blue-300 dark:hover:bg-blue-700/40']"
|
||||
@click="props.onRunTest"
|
||||
>
|
||||
{{ isManualTesting ? t('settings.dialogs.onboarding.testGenerationRunning') : t('settings.dialogs.onboarding.testGeneration') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded bg-blue-100 px-2 py-0.5 text-xs text-blue-600 font-medium transition-colors dark:bg-blue-800/30 hover:bg-blue-200 dark:text-blue-300 dark:hover:bg-blue-700/40"
|
||||
@click="props.onGoToModelSelection"
|
||||
>
|
||||
{{ t('settings.pages.providers.common.goToModelSelection') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<!-- Full Validation Success -->
|
||||
<Alert v-else-if="isValid && isValidating === 0 && (!hasManualValidators || manualTestPassed)" type="success">
|
||||
<template #title>
|
||||
<div class="w-full flex items-center justify-between">
|
||||
<span>{{ t('settings.dialogs.onboarding.validationSuccess') }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-2 rounded bg-green-100 px-2 py-0.5 text-xs text-green-600 font-medium transition-colors dark:bg-green-800/30 hover:bg-green-200 dark:text-green-300 dark:hover:bg-green-700/40"
|
||||
@click="props.onGoToModelSelection"
|
||||
>
|
||||
{{ t('settings.pages.providers.common.goToModelSelection') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<!-- Manual Test Failed -->
|
||||
<Alert v-else-if="hasManualValidators && !manualTestPassed && manualTestMessage && !isManualTesting" type="error">
|
||||
<template #title>
|
||||
<div class="w-full flex items-center justify-between">
|
||||
<span>{{ t('settings.dialogs.onboarding.testGenerationFailed') }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-2 rounded bg-red-100 px-2 py-0.5 text-xs text-red-600 font-medium transition-colors dark:bg-red-800/30 hover:bg-red-200 dark:text-red-300 dark:hover:bg-red-700/40"
|
||||
@click="props.onForceValid"
|
||||
>
|
||||
{{ t('settings.pages.providers.common.continueAnyway') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
{{ manualTestMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
</template>
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { RemovableRef } from '@vueuse/core'
|
||||
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
@@ -52,6 +53,11 @@ export function useProviderValidation(providerId: string) {
|
||||
const isValid = ref(false)
|
||||
const validationMessage = ref('')
|
||||
|
||||
const hasManualValidators = computed(() => !!providerMetadata.value?.validators.runManualValidation)
|
||||
const isManualTesting = ref(false)
|
||||
const manualTestPassed = ref(false)
|
||||
const manualTestMessage = ref('')
|
||||
|
||||
async function validateConfiguration() {
|
||||
if (!providerMetadata.value)
|
||||
return
|
||||
@@ -85,7 +91,7 @@ export function useProviderValidation(providerId: string) {
|
||||
catch (error) {
|
||||
isValid.value = false
|
||||
finalValidationMessage = t('settings.dialogs.onboarding.validationError', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
error: errorMessageFrom(error),
|
||||
})
|
||||
}
|
||||
finally {
|
||||
@@ -120,19 +126,54 @@ export function useProviderValidation(providerId: string) {
|
||||
|
||||
watch(credentials, () => {
|
||||
debouncedValidateConfiguration()
|
||||
// Reset manual test state when credentials change
|
||||
manualTestPassed.value = false
|
||||
manualTestMessage.value = ''
|
||||
}, { deep: true })
|
||||
|
||||
async function runManualTest() {
|
||||
if (!providerMetadata.value?.validators.runManualValidation)
|
||||
return
|
||||
|
||||
isManualTesting.value = true
|
||||
manualTestMessage.value = ''
|
||||
|
||||
try {
|
||||
const config = { ...credentials.value }
|
||||
if (config?.apiKey)
|
||||
config.apiKey = config.apiKey.trim()
|
||||
if (config?.baseUrl)
|
||||
config.baseUrl = config.baseUrl.trim()
|
||||
|
||||
const result = await providerMetadata.value.validators.runManualValidation(config)
|
||||
manualTestPassed.value = result.valid
|
||||
if (!result.valid)
|
||||
manualTestMessage.value = result.reason
|
||||
}
|
||||
catch (error) {
|
||||
manualTestPassed.value = false
|
||||
manualTestMessage.value = errorMessageFrom(error)
|
||||
}
|
||||
finally {
|
||||
isManualTesting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleResetSettings() {
|
||||
const defaultOptions = providerMetadata.value?.defaultOptions ? providerMetadata.value.defaultOptions() : {}
|
||||
providers.value[providerId] = { ...defaultOptions }
|
||||
isValid.value = false
|
||||
validationMessage.value = ''
|
||||
isValidating.value = 0
|
||||
manualTestPassed.value = false
|
||||
manualTestMessage.value = ''
|
||||
}
|
||||
|
||||
function forceValid() {
|
||||
isValid.value = true
|
||||
validationMessage.value = ''
|
||||
manualTestPassed.value = true
|
||||
manualTestMessage.value = ''
|
||||
providersStore.forceProviderConfigured(providerId)
|
||||
}
|
||||
|
||||
@@ -148,5 +189,11 @@ export function useProviderValidation(providerId: string) {
|
||||
validationMessage,
|
||||
handleResetSettings,
|
||||
forceValid,
|
||||
// Manual test generation
|
||||
hasManualValidators,
|
||||
isManualTesting,
|
||||
manualTestPassed,
|
||||
manualTestMessage,
|
||||
runManualTest,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,12 @@ export interface ProviderRuntimeValidator<TConfig> {
|
||||
name: string
|
||||
validator: (config: TConfig, provider: ProviderInstance, providerExtra: ProviderExtraMethods<TConfig>, contextOptions: { t: ComposerTranslation }) => MaybePromise<ProviderValidationResult>
|
||||
schedule?: ProviderValidatorSchedule
|
||||
/**
|
||||
* When true the validator is excluded from automatic validation runs.
|
||||
* It must be triggered explicitly by the user (e.g. a "Test Generation" button).
|
||||
* This is used for costly checks like chat-completion probes that bill per request.
|
||||
*/
|
||||
manualOnly?: boolean
|
||||
}
|
||||
|
||||
export interface ModelInfo {
|
||||
|
||||
@@ -291,6 +291,7 @@ export function createOpenAICompatibleValidators<TConfig extends { apiKey?: stri
|
||||
id: 'openai-compatible:check-chat-completions',
|
||||
name: t('settings.pages.providers.catalog.edit.validators.openai-compatible.check-supports-chat-completion.title'),
|
||||
schedule: options?.schedule,
|
||||
manualOnly: true,
|
||||
validator: async (config, provider, providerExtra, contextOptions) => {
|
||||
const errors: Array<{ error: unknown }> = []
|
||||
const result = await getChatCheckResult(
|
||||
|
||||
@@ -26,6 +26,8 @@ export interface ProviderValidationPlan {
|
||||
definition: ProviderDefinition
|
||||
configValidators: ProviderConfigValidator<Record<string, unknown>>[]
|
||||
providerValidators: ProviderRuntimeValidator<Record<string, unknown>>[]
|
||||
/** Provider validators that require explicit user action (e.g. "Test Generation" button). */
|
||||
manualProviderValidators: ProviderRuntimeValidator<Record<string, unknown>>[]
|
||||
providerExtra: ProviderExtraMethods<Record<string, unknown>> | undefined
|
||||
shouldValidate: boolean
|
||||
}
|
||||
@@ -83,7 +85,12 @@ export function getValidatorsOfProvider(options: {
|
||||
const { definition } = options
|
||||
|
||||
const configValidators = (definition.validators?.validateConfig || []).map(creator => creator(options.contextOptions))
|
||||
const providerValidators = (definition.validators?.validateProvider || []).map(creator => creator(options.contextOptions))
|
||||
const allProviderValidators = (definition.validators?.validateProvider || []).map(creator => creator(options.contextOptions))
|
||||
|
||||
// Separate automatic validators from manual-only ones (e.g. chat completion probes)
|
||||
const providerValidators = allProviderValidators.filter(v => !v.manualOnly)
|
||||
const manualProviderValidators = allProviderValidators.filter(v => v.manualOnly)
|
||||
|
||||
const steps: ProviderValidationStep[] = [
|
||||
...createConfigValidationSteps(configValidators),
|
||||
...createProviderValidationSteps(providerValidators),
|
||||
@@ -99,6 +106,7 @@ export function getValidatorsOfProvider(options: {
|
||||
definition,
|
||||
configValidators: configValidators as ProviderValidationPlan['configValidators'],
|
||||
providerValidators: providerValidators as ProviderValidationPlan['providerValidators'],
|
||||
manualProviderValidators: manualProviderValidators as ProviderValidationPlan['manualProviderValidators'],
|
||||
providerExtra: definition.extraMethods as ProviderValidationPlan['providerExtra'],
|
||||
shouldValidate,
|
||||
}
|
||||
@@ -156,7 +164,7 @@ export async function validateProvider(
|
||||
for (let i = 0; i < providerValidators.length; i++) {
|
||||
const step = steps[providerStepOffset + i]
|
||||
step.status = 'invalid'
|
||||
step.reason = error instanceof Error ? error.message : String(error)
|
||||
step.reason = errorMessageFrom(error)
|
||||
}
|
||||
return steps
|
||||
}
|
||||
@@ -174,7 +182,70 @@ export async function validateProvider(
|
||||
}
|
||||
catch (error) {
|
||||
step.status = 'invalid'
|
||||
step.reason = error instanceof Error ? error.message : String(error)
|
||||
step.reason = errorMessageFrom(error)
|
||||
onValidatorError?.({ kind: 'provider', index, step, error })
|
||||
}
|
||||
}))
|
||||
|
||||
return steps
|
||||
}
|
||||
|
||||
/**
|
||||
* Run only the manual-only validators (e.g. chat completion probes).
|
||||
* These are excluded from automatic validation to avoid costly API calls.
|
||||
* Must be triggered explicitly by the user.
|
||||
*/
|
||||
export async function validateProviderManual(
|
||||
plan: ProviderValidationPlan,
|
||||
contextOptions: { t: ComposerTranslation },
|
||||
callbacks: ProviderValidationCallbacks = {},
|
||||
) {
|
||||
const { manualProviderValidators, config, definition, providerExtra } = plan
|
||||
if (manualProviderValidators.length === 0)
|
||||
return []
|
||||
|
||||
const runContext = {
|
||||
...contextOptions,
|
||||
validationCache: new Map<string, unknown>(),
|
||||
}
|
||||
const { onValidatorError, onValidatorStart, onValidatorSuccess } = callbacks
|
||||
|
||||
let providerInstance: ProviderInstance
|
||||
try {
|
||||
providerInstance = await definition.createProvider(config as any)
|
||||
}
|
||||
catch (error) {
|
||||
return manualProviderValidators.map(v => ({
|
||||
id: v.id,
|
||||
label: v.name,
|
||||
status: 'invalid' as ProviderValidationStepStatus,
|
||||
reason: errorMessageFrom(error),
|
||||
kind: 'provider' as ProviderValidationStepKind,
|
||||
}))
|
||||
}
|
||||
|
||||
const steps: ProviderValidationStep[] = manualProviderValidators.map(v => ({
|
||||
id: v.id,
|
||||
label: v.name,
|
||||
status: 'idle' as ProviderValidationStepStatus,
|
||||
reason: '',
|
||||
kind: 'provider' as ProviderValidationStepKind,
|
||||
}))
|
||||
|
||||
await Promise.all(manualProviderValidators.map(async (validatorDefinition, index) => {
|
||||
const step = steps[index]
|
||||
step.status = 'validating'
|
||||
step.reason = ''
|
||||
onValidatorStart?.({ kind: 'provider', index, step })
|
||||
try {
|
||||
const result = await validatorDefinition.validator(config, providerInstance as any, providerExtra as any, runContext)
|
||||
step.status = result.valid ? 'valid' : 'invalid'
|
||||
step.reason = result.valid ? '' : result.reason
|
||||
onValidatorSuccess?.({ kind: 'provider', index, step, result })
|
||||
}
|
||||
catch (error) {
|
||||
step.status = 'invalid'
|
||||
step.reason = errorMessageFrom(error)
|
||||
onValidatorError?.({ kind: 'provider', index, step, error })
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -147,6 +147,15 @@ export interface ProviderMetadata {
|
||||
reason: string
|
||||
valid: boolean
|
||||
}
|
||||
/**
|
||||
* Run only the manual-only validators. Returns validation result.
|
||||
* Only available when the provider has manual validators.
|
||||
*/
|
||||
runManualValidation?: (config: Record<string, unknown>) => Promise<{
|
||||
errors: unknown[]
|
||||
reason: string
|
||||
valid: boolean
|
||||
}>
|
||||
}
|
||||
transcriptionFeatures?: {
|
||||
supportsGenerate: boolean
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { ProviderMetadata } from '../providers'
|
||||
import { listModels } from '@xsai/model'
|
||||
|
||||
import { isModelProvider } from '../../libs/providers/types'
|
||||
import { getValidatorsOfProvider, validateProvider } from '../../libs/providers/validators/run'
|
||||
import { getValidatorsOfProvider, validateProvider, validateProviderManual } from '../../libs/providers/validators/run'
|
||||
|
||||
function getCategoryFromTasks(tasks: string[]): ProviderMetadata['category'] {
|
||||
if (tasks.some(task => ['speech-to-text', 'automatic-speech-recognition', 'asr', 'stt'].includes(task.toLowerCase()))) {
|
||||
@@ -100,6 +100,8 @@ export function convertProviderDefinitionToMetadata(
|
||||
const keyExtractor = (input: string): string => input
|
||||
const category = getCategoryFromTasks(definition.tasks)
|
||||
const schemaDefaults = extractSchemaDefaults(definition, t)
|
||||
const allValidators = (definition.validators?.validateProvider || []).map(creator => creator({ t }))
|
||||
const hasManualValidators = allValidators.some(v => v.manualOnly)
|
||||
|
||||
return {
|
||||
id: definition.id,
|
||||
@@ -194,6 +196,8 @@ export function convertProviderDefinitionToMetadata(
|
||||
|
||||
// Run full validation pipeline (config + provider validators) only when required.
|
||||
// This preserves strict config checks while avoiding unnecessary network checks.
|
||||
// NOTICE: manualOnly validators (e.g. chat completion probes) are already excluded
|
||||
// from the plan's providerValidators by getValidatorsOfProvider.
|
||||
if (plan.shouldValidate) {
|
||||
await validateProvider(plan, { t })
|
||||
const invalidSteps = plan.steps.filter(step => step.status === 'invalid')
|
||||
@@ -230,6 +234,26 @@ export function convertProviderDefinitionToMetadata(
|
||||
await validateProvider(plan, { t })
|
||||
return buildConfigValidationResult(plan)
|
||||
},
|
||||
runManualValidation: hasManualValidators
|
||||
? async (config) => {
|
||||
const plan = getValidatorsOfProvider({
|
||||
definition,
|
||||
config,
|
||||
schemaDefaults,
|
||||
contextOptions: { t },
|
||||
})
|
||||
const steps = await validateProviderManual(plan, { t })
|
||||
const invalidSteps = steps.filter(step => step.status === 'invalid')
|
||||
if (invalidSteps.length === 0) {
|
||||
return { errors: [], reason: '', valid: true }
|
||||
}
|
||||
return {
|
||||
errors: invalidSteps.map(step => new Error(step.reason || `${step.id} is invalid`)),
|
||||
reason: invalidSteps.map(step => step.reason).filter(Boolean).join('; '),
|
||||
valid: false,
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
transcriptionFeatures: definition.capabilities?.transcription
|
||||
? {
|
||||
|
||||
Reference in New Issue
Block a user