mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat(provider): 模型测试支持自定义请求头,优化对话框布局与并发策略
- 前后端新增 request_headers 字段,测试时可自定义额外请求头 - ModelTestDialog 拆分为请求头/请求体并排双栏布局,增加格式化与重置按钮 - 区分 Pool 托管(并发5)和单 Key Provider(并发1)的测试并发数 - JsonImportInput 新增 multiple prop 支持单文件模式 - KeyFormDialog Service Account 输入改用 JsonImportInput,支持拖拽导入
This commit is contained in:
@@ -194,6 +194,7 @@ export interface TestModelFailoverRequest {
|
||||
api_format?: string
|
||||
endpoint_id?: string
|
||||
message?: string
|
||||
request_headers?: Record<string, unknown>
|
||||
request_body?: Record<string, unknown>
|
||||
request_id?: string
|
||||
concurrency?: number
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
:accept="accept"
|
||||
multiple
|
||||
:multiple="multiple"
|
||||
class="hidden"
|
||||
@change="handleFileSelect"
|
||||
>
|
||||
@@ -94,6 +94,7 @@ const props = withDefaults(defineProps<{
|
||||
disabled?: boolean
|
||||
resetKey?: string | number
|
||||
accept?: string
|
||||
multiple?: boolean
|
||||
dropTitle?: string
|
||||
dropHint?: string
|
||||
manualLabel?: string
|
||||
@@ -106,6 +107,7 @@ const props = withDefaults(defineProps<{
|
||||
disabled: false,
|
||||
resetKey: '',
|
||||
accept: '.json,.txt',
|
||||
multiple: true,
|
||||
dropTitle: '拖入导入文件或点击选择',
|
||||
dropHint: '支持 .json / .txt,可多选',
|
||||
manualLabel: '',
|
||||
@@ -218,13 +220,19 @@ function mergeFileContents(contents: string[]): string {
|
||||
}
|
||||
|
||||
async function readFiles(files: File[]) {
|
||||
const validFiles = files.filter(isValidFileType)
|
||||
const sourceFiles = props.multiple ? files : files.slice(0, 1)
|
||||
|
||||
if (!props.multiple && files.length > 1) {
|
||||
emitError('仅支持选择 1 个文件,已读取第一个文件', '提示')
|
||||
}
|
||||
|
||||
const validFiles = sourceFiles.filter(isValidFileType)
|
||||
if (validFiles.length === 0) {
|
||||
emitError('仅支持 .json 或 .txt 文件', '格式错误')
|
||||
return
|
||||
}
|
||||
if (validFiles.length < files.length) {
|
||||
emitError(`已忽略 ${files.length - validFiles.length} 个不支持的文件`, '提示')
|
||||
if (validFiles.length < sourceFiles.length) {
|
||||
emitError(`已忽略 ${sourceFiles.length - validFiles.length} 个不支持的文件`, '提示')
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface StartTestParams {
|
||||
apiFormat?: string
|
||||
endpointId?: string
|
||||
message?: string
|
||||
requestHeaders?: Record<string, unknown>
|
||||
requestBody?: Record<string, unknown>
|
||||
concurrency?: number
|
||||
onSuccess?: (result: TestModelFailoverResponse) => void
|
||||
@@ -139,6 +140,7 @@ export function useModelTest(options: UseModelTestOptions) {
|
||||
api_format: params.apiFormat,
|
||||
endpoint_id: params.endpointId,
|
||||
...(normalizedMessage ? { message: normalizedMessage } : {}),
|
||||
...(params.requestHeaders ? { request_headers: params.requestHeaders } : {}),
|
||||
...(params.requestBody ? { request_body: params.requestBody } : {}),
|
||||
request_id: reqId,
|
||||
concurrency: params.concurrency,
|
||||
|
||||
@@ -79,18 +79,19 @@
|
||||
{{ editingKey ? '' : '*' }}
|
||||
</Label>
|
||||
<template v-if="form.auth_type === 'service_account'">
|
||||
<Textarea
|
||||
:id="apiKeyInputId"
|
||||
<JsonImportInput
|
||||
v-model="form.auth_config_text"
|
||||
:required="!editingKey"
|
||||
:placeholder="editingKey ? '留空表示不修改' : '粘贴完整的 Service Account JSON'"
|
||||
class="min-h-[120px] font-mono text-xs"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
:disabled="saving"
|
||||
:reset-key="formNonce"
|
||||
accept=".json,.txt,application/json,text/plain"
|
||||
:multiple="false"
|
||||
drop-title="拖入 Service Account JSON 或点击选择"
|
||||
drop-hint="支持 .json / .txt,单文件导入"
|
||||
:manual-placeholder="editingKey ? '留空表示不修改,或粘贴完整的 Service Account JSON' : '粘贴完整的 Service Account JSON'"
|
||||
:manual-description="serviceAccountDescription"
|
||||
textarea-class="min-h-[160px] font-mono text-xs break-all !rounded-xl"
|
||||
@error="handleServiceAccountImportError"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground mt-1">
|
||||
JSON 格式,包含 project_id、private_key 等字段
|
||||
</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Input
|
||||
@@ -330,13 +331,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { Dialog, Button, Input, Label, Switch, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Textarea } from '@/components/ui'
|
||||
import { Dialog, Button, Input, Label, Switch, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui'
|
||||
import { Key, SquarePen } from 'lucide-vue-next'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useFormDialog } from '@/composables/useFormDialog'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { parseNumberInput, parseNullableNumberInput } from '@/utils/form'
|
||||
import { log } from '@/utils/logger'
|
||||
import JsonImportInput from '@/components/common/JsonImportInput.vue'
|
||||
import {
|
||||
addProviderKey,
|
||||
updateProviderKey,
|
||||
@@ -415,6 +417,12 @@ const visibleApiFormats = computed(() => {
|
||||
|
||||
const showAuthTypeSelector = computed(() => props.providerType === 'vertex_ai')
|
||||
|
||||
const serviceAccountDescription = computed(() => (
|
||||
props.editingKey
|
||||
? '留空表示不修改;JSON 格式,包含 project_id、private_key 等字段'
|
||||
: 'JSON 格式,包含 project_id、private_key 等字段'
|
||||
))
|
||||
|
||||
// 默认认证类型
|
||||
const defaultAuthType = 'api_key' as const
|
||||
|
||||
@@ -676,6 +684,10 @@ function parseAuthConfig(): Record<string, unknown> | null {
|
||||
}
|
||||
}
|
||||
|
||||
function handleServiceAccountImportError(payload: { message: string, title?: string }) {
|
||||
showError(payload.message, payload.title || '错误')
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
// 必须有 providerId
|
||||
if (!props.providerId) {
|
||||
|
||||
@@ -319,13 +319,18 @@
|
||||
:testing="modelTest.testing.value"
|
||||
:trace="modelTest.testTrace.value"
|
||||
:request-id="modelTest.requestId.value"
|
||||
:request-headers-draft="testRequestHeadersDraft"
|
||||
:request-headers-reset-value="testRequestHeadersResetValue"
|
||||
:request-headers-error="testRequestHeadersError"
|
||||
:request-body-draft="testRequestBodyDraft"
|
||||
:request-body-reset-value="testRequestBodyResetValue"
|
||||
:request-body-error="testRequestBodyError"
|
||||
:start-disabled="!selectedTestEndpoint || !!testRequestBodyError"
|
||||
:start-disabled="!selectedTestEndpoint || !!testRequestHeadersError || !!testRequestBodyError"
|
||||
@close="handleTestDialogClose"
|
||||
@back="handleTestDialogBack"
|
||||
@select-endpoint="handleSelectTestEndpoint"
|
||||
@start="handleStartMappingTest"
|
||||
@update:request-headers-draft="testRequestHeadersDraft = $event"
|
||||
@update:request-body-draft="testRequestBodyDraft = $event"
|
||||
/>
|
||||
</template>
|
||||
@@ -353,8 +358,12 @@ import { updateModel } from '@/api/endpoints/models'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
|
||||
import {
|
||||
buildDefaultModelTestRequestHeaders,
|
||||
buildDefaultModelTestRequestBody,
|
||||
parseModelTestRequestHeadersDraft,
|
||||
parseModelTestRequestBodyDraft,
|
||||
POOL_TEST_CONCURRENCY,
|
||||
SINGLE_TEST_CONCURRENCY,
|
||||
} from './model-test-request'
|
||||
|
||||
interface MappingItem {
|
||||
@@ -409,8 +418,14 @@ const pendingMappingKey = ref<string | null>(null)
|
||||
const testingModelName = ref<string | null>(null)
|
||||
const preselectedModelId = ref<string | null>(null)
|
||||
const selectedTestEndpoint = ref<ProviderEndpoint | null>(null)
|
||||
const testRequestHeadersDraft = ref('')
|
||||
const testRequestHeadersResetValue = ref('')
|
||||
const testRequestBodyDraft = ref('')
|
||||
const testRequestBodyResetValue = ref('')
|
||||
const isPoolManagedProvider = computed(() => Boolean(props.provider.pool_advanced))
|
||||
const activeEndpoints = computed(() => (props.endpoints ?? []).filter(endpoint => endpoint.is_active))
|
||||
const parsedTestRequestHeaders = computed(() => parseModelTestRequestHeadersDraft(testRequestHeadersDraft.value))
|
||||
const testRequestHeadersError = computed(() => parsedTestRequestHeaders.value.error)
|
||||
const parsedTestRequestBody = computed(() => parseModelTestRequestBodyDraft(testRequestBodyDraft.value))
|
||||
const testRequestBodyError = computed(() => parsedTestRequestBody.value.error)
|
||||
|
||||
@@ -651,7 +666,10 @@ function handleTestDialogClose() {
|
||||
testingModelName.value = null
|
||||
testingMapping.value = null
|
||||
selectedTestEndpoint.value = null
|
||||
testRequestHeadersDraft.value = ''
|
||||
testRequestHeadersResetValue.value = ''
|
||||
testRequestBodyDraft.value = ''
|
||||
testRequestBodyResetValue.value = ''
|
||||
}
|
||||
|
||||
function handleTestDialogBack() {
|
||||
@@ -678,7 +696,10 @@ function runMappingTest(testingKey: string, modelName: string) {
|
||||
testingMapping.value = null
|
||||
testingModelName.value = modelName
|
||||
selectedTestEndpoint.value = activeEndpoints.value[0] ?? null
|
||||
testRequestBodyDraft.value = buildDefaultModelTestRequestBody(modelName)
|
||||
testRequestHeadersResetValue.value = buildDefaultModelTestRequestHeaders()
|
||||
testRequestHeadersDraft.value = testRequestHeadersResetValue.value
|
||||
testRequestBodyResetValue.value = buildDefaultModelTestRequestBody(modelName)
|
||||
testRequestBodyDraft.value = testRequestBodyResetValue.value
|
||||
}
|
||||
|
||||
async function handleStartMappingTest() {
|
||||
@@ -689,6 +710,12 @@ async function handleStartMappingTest() {
|
||||
return
|
||||
}
|
||||
|
||||
const { value: requestHeaders, error: requestHeadersError } = parsedTestRequestHeaders.value
|
||||
if (!requestHeaders || requestHeadersError) {
|
||||
showError(`测试请求头无效: ${requestHeadersError || '无效 JSON'}`)
|
||||
return
|
||||
}
|
||||
|
||||
const { value: requestBody, error } = parsedTestRequestBody.value
|
||||
if (!requestBody || error) {
|
||||
showError(`测试请求体无效: ${error || '无效 JSON'}`)
|
||||
@@ -703,7 +730,9 @@ async function handleStartMappingTest() {
|
||||
displayLabel: `[${endpoint.api_format}] 映射 "${testingModelName.value}"`,
|
||||
apiFormat: endpoint.api_format,
|
||||
endpointId: endpoint.id,
|
||||
requestHeaders,
|
||||
requestBody,
|
||||
concurrency: isPoolManagedProvider.value ? POOL_TEST_CONCURRENCY : SINGLE_TEST_CONCURRENCY,
|
||||
})
|
||||
if (pendingMappingKey.value === currentMappingKey) {
|
||||
pendingMappingKey.value = null
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:open="open"
|
||||
size="2xl"
|
||||
size="3xl"
|
||||
:close-on-backdrop="false"
|
||||
@update:open="(val: boolean) => { if (!val) emit('close') }"
|
||||
>
|
||||
@@ -37,12 +37,12 @@
|
||||
当前测试会固定到选中的端点
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="grid gap-2 md:grid-cols-2">
|
||||
<button
|
||||
v-for="endpoint in endpoints"
|
||||
:key="endpoint.id"
|
||||
type="button"
|
||||
class="w-full rounded-lg border px-3 py-3 text-left transition-colors"
|
||||
class="h-full w-full rounded-lg border px-3 py-3 text-left transition-colors"
|
||||
:class="selectedEndpoint?.id === endpoint.id
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-border/60 hover:bg-muted/40'"
|
||||
@@ -65,33 +65,91 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="text-sm font-medium">
|
||||
测试请求体
|
||||
<div class="grid gap-4 lg:grid-cols-2 lg:items-start">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="text-sm font-medium">
|
||||
测试请求头
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 rounded-lg text-muted-foreground"
|
||||
title="格式化请求头 JSON"
|
||||
@click="formatRequestHeadersDraft"
|
||||
>
|
||||
<Code2 class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 rounded-lg text-muted-foreground"
|
||||
title="重置请求头"
|
||||
@click="resetRequestHeadersDraft"
|
||||
>
|
||||
<RotateCcw class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="formatRequestBodyDraft"
|
||||
<Textarea
|
||||
:model-value="requestHeadersDraft"
|
||||
class="min-h-[260px] font-mono text-xs"
|
||||
placeholder="输入 JSON 请求头"
|
||||
@update:model-value="emit('update:requestHeadersDraft', $event)"
|
||||
/>
|
||||
<div
|
||||
v-if="requestHeadersError"
|
||||
class="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive"
|
||||
>
|
||||
格式化 JSON
|
||||
</Button>
|
||||
{{ requestHeadersError }}
|
||||
</div>
|
||||
<div class="rounded-md border border-border/60 bg-muted/20 px-3 py-2 text-[11px] text-muted-foreground">
|
||||
这里的请求头会合并到测试请求里;鉴权头和必要系统头仍由后端按端点规则补齐。
|
||||
</div>
|
||||
</div>
|
||||
<Textarea
|
||||
:model-value="requestBodyDraft"
|
||||
class="min-h-[260px] font-mono text-xs"
|
||||
placeholder="输入 JSON 请求体"
|
||||
@update:model-value="emit('update:requestBodyDraft', $event)"
|
||||
/>
|
||||
<div
|
||||
v-if="requestBodyError"
|
||||
class="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive"
|
||||
>
|
||||
{{ requestBodyError }}
|
||||
</div>
|
||||
<div class="rounded-md border border-border/60 bg-muted/20 px-3 py-2 text-[11px] text-muted-foreground">
|
||||
会强制使用当前测试模型;这里编辑的是测试基础请求体,实际发送时会按端点格式转换并应用规则。
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="text-sm font-medium">
|
||||
测试请求体
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 rounded-lg text-muted-foreground"
|
||||
title="格式化请求体 JSON"
|
||||
@click="formatRequestBodyDraft"
|
||||
>
|
||||
<Code2 class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 rounded-lg text-muted-foreground"
|
||||
title="重置请求体"
|
||||
@click="resetRequestBodyDraft"
|
||||
>
|
||||
<RotateCcw class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Textarea
|
||||
:model-value="requestBodyDraft"
|
||||
class="min-h-[260px] font-mono text-xs"
|
||||
placeholder="输入 JSON 请求体"
|
||||
@update:model-value="emit('update:requestBodyDraft', $event)"
|
||||
/>
|
||||
<div
|
||||
v-if="requestBodyError"
|
||||
class="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive"
|
||||
>
|
||||
{{ requestBodyError }}
|
||||
</div>
|
||||
<div class="rounded-md border border-border/60 bg-muted/20 px-3 py-2 text-[11px] text-muted-foreground">
|
||||
会强制使用当前测试模型;这里编辑的是测试基础请求体,实际发送时会按端点格式转换并应用规则。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -582,7 +640,6 @@
|
||||
<template #footer>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="emit('close')"
|
||||
>
|
||||
{{ showSetup ? '取消' : '关闭' }}
|
||||
@@ -590,7 +647,6 @@
|
||||
<Button
|
||||
v-if="showResult"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="emit('back')"
|
||||
>
|
||||
返回
|
||||
@@ -601,7 +657,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { Check, Copy, Loader2, Maximize2, Minimize2 } from 'lucide-vue-next'
|
||||
import { Check, Code2, Copy, Loader2, Maximize2, Minimize2, RotateCcw } from 'lucide-vue-next'
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
@@ -635,7 +691,11 @@ const props = defineProps<{
|
||||
testing?: boolean
|
||||
trace?: RequestTrace | null
|
||||
requestId?: string | null
|
||||
requestHeadersDraft?: string
|
||||
requestHeadersResetValue?: string
|
||||
requestHeadersError?: string | null
|
||||
requestBodyDraft?: string
|
||||
requestBodyResetValue?: string
|
||||
requestBodyError?: string | null
|
||||
startDisabled?: boolean
|
||||
}>()
|
||||
@@ -645,10 +705,12 @@ const emit = defineEmits<{
|
||||
back: []
|
||||
start: []
|
||||
selectEndpoint: [endpointId: string]
|
||||
'update:requestHeadersDraft': [value: string]
|
||||
'update:requestBodyDraft': [value: string]
|
||||
}>()
|
||||
|
||||
const endpoints = computed(() => props.endpoints ?? [])
|
||||
const requestHeadersDraft = computed(() => props.requestHeadersDraft ?? '')
|
||||
const requestBodyDraft = computed(() => props.requestBodyDraft ?? '')
|
||||
const traceCandidates = computed(() => props.trace?.candidates ?? [])
|
||||
const showSetup = computed(() => props.open && !props.testing && !props.result)
|
||||
@@ -664,7 +726,7 @@ const dialogTitle = computed(() => {
|
||||
|
||||
const dialogDescription = computed(() => {
|
||||
if (showSetup.value && props.selectingModelName) {
|
||||
return `为 ${props.selectingModelName} 选择端点并编辑测试请求体`
|
||||
return `为 ${props.selectingModelName} 选择端点并编辑测试请求头与请求体`
|
||||
}
|
||||
if (props.testing && props.selectedEndpoint) {
|
||||
return `正在通过 ${formatApiFormat(props.selectedEndpoint.api_format)} 测试 ${props.selectingModelName || '模型'}`
|
||||
@@ -969,11 +1031,39 @@ function collapseInspectionContent() {
|
||||
inspectionExpandDepth.value = 0
|
||||
}
|
||||
|
||||
function formatRequestHeadersDraft() {
|
||||
formatJsonDraft(requestHeadersDraft.value, value => emit('update:requestHeadersDraft', value), '{}')
|
||||
}
|
||||
|
||||
function formatRequestBodyDraft() {
|
||||
formatJsonDraft(requestBodyDraft.value, value => emit('update:requestBodyDraft', value))
|
||||
}
|
||||
|
||||
function resetRequestHeadersDraft() {
|
||||
emit('update:requestHeadersDraft', props.requestHeadersResetValue ?? '{}')
|
||||
}
|
||||
|
||||
function resetRequestBodyDraft() {
|
||||
emit('update:requestBodyDraft', props.requestBodyResetValue ?? '')
|
||||
}
|
||||
|
||||
function formatJsonDraft(
|
||||
draft: string,
|
||||
onFormatted: (value: string) => void,
|
||||
emptyFallback?: string,
|
||||
) {
|
||||
const normalized = draft.trim()
|
||||
if (!normalized) {
|
||||
if (emptyFallback !== undefined) {
|
||||
onFormatted(emptyFallback)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(requestBodyDraft.value)
|
||||
const parsed = JSON.parse(normalized)
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return
|
||||
emit('update:requestBodyDraft', JSON.stringify(parsed, null, 2))
|
||||
onFormatted(JSON.stringify(parsed, null, 2))
|
||||
} catch {
|
||||
// keep user input untouched when JSON is invalid
|
||||
}
|
||||
|
||||
@@ -221,13 +221,18 @@
|
||||
:testing="modelTest.testing.value"
|
||||
:trace="modelTest.testTrace.value"
|
||||
:request-id="modelTest.requestId.value"
|
||||
:request-headers-draft="testRequestHeadersDraft"
|
||||
:request-headers-reset-value="testRequestHeadersResetValue"
|
||||
:request-headers-error="testRequestHeadersError"
|
||||
:request-body-draft="testRequestBodyDraft"
|
||||
:request-body-reset-value="testRequestBodyResetValue"
|
||||
:request-body-error="testRequestBodyError"
|
||||
:start-disabled="!selectedTestEndpoint || !!testRequestBodyError"
|
||||
:start-disabled="!selectedTestEndpoint || !!testRequestHeadersError || !!testRequestBodyError"
|
||||
@close="handleTestDialogClose"
|
||||
@back="handleTestDialogBack"
|
||||
@start="handleStartPendingTest"
|
||||
@select-endpoint="handleSelectTestEndpoint"
|
||||
@update:request-headers-draft="testRequestHeadersDraft = $event"
|
||||
@update:request-body-draft="testRequestBodyDraft = $event"
|
||||
/>
|
||||
</template>
|
||||
@@ -252,8 +257,12 @@ import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
|
||||
import ModelTestDialog from './ModelTestDialog.vue'
|
||||
import {
|
||||
buildDefaultModelTestRequestHeaders,
|
||||
buildDefaultModelTestRequestBody,
|
||||
parseModelTestRequestHeadersDraft,
|
||||
parseModelTestRequestBodyDraft,
|
||||
POOL_TEST_CONCURRENCY,
|
||||
SINGLE_TEST_CONCURRENCY,
|
||||
} from './model-test-request'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -280,8 +289,14 @@ const localModels = ref<Model[]>([])
|
||||
const togglingModelId = ref<string | null>(null)
|
||||
const pendingTestModel = ref<Model | null>(null)
|
||||
const selectedTestEndpoint = ref<ProviderEndpoint | null>(null)
|
||||
const testRequestHeadersDraft = ref('')
|
||||
const testRequestHeadersResetValue = ref('')
|
||||
const testRequestBodyDraft = ref('')
|
||||
const testRequestBodyResetValue = ref('')
|
||||
const isPoolManagedProvider = computed(() => Boolean(props.provider.pool_advanced))
|
||||
const activeEndpoints = computed(() => (props.endpoints ?? []).filter(endpoint => endpoint.is_active))
|
||||
const parsedTestRequestHeaders = computed(() => parseModelTestRequestHeadersDraft(testRequestHeadersDraft.value))
|
||||
const testRequestHeadersError = computed(() => parsedTestRequestHeaders.value.error)
|
||||
const parsedTestRequestBody = computed(() => parseModelTestRequestBodyDraft(testRequestBodyDraft.value))
|
||||
const testRequestBodyError = computed(() => parsedTestRequestBody.value.error)
|
||||
const models = computed(() => props.models ?? localModels.value)
|
||||
@@ -449,7 +464,10 @@ function handleTestDialogClose() {
|
||||
modelTest.resetState()
|
||||
pendingTestModel.value = null
|
||||
selectedTestEndpoint.value = null
|
||||
testRequestHeadersDraft.value = ''
|
||||
testRequestHeadersResetValue.value = ''
|
||||
testRequestBodyDraft.value = ''
|
||||
testRequestBodyResetValue.value = ''
|
||||
}
|
||||
|
||||
function handleTestDialogBack() {
|
||||
@@ -474,6 +492,12 @@ async function handleStartPendingTest() {
|
||||
return
|
||||
}
|
||||
|
||||
const { value: requestHeaders, error: requestHeadersError } = parsedTestRequestHeaders.value
|
||||
if (!requestHeaders || requestHeadersError) {
|
||||
showError(`测试请求头无效: ${requestHeadersError || '无效 JSON'}`)
|
||||
return
|
||||
}
|
||||
|
||||
const { value: requestBody, error } = parsedTestRequestBody.value
|
||||
if (!requestBody || error) {
|
||||
showError(`测试请求体无效: ${error || '无效 JSON'}`)
|
||||
@@ -490,8 +514,9 @@ async function handleStartPendingTest() {
|
||||
displayLabel: `${endpointPrefix}${modelName}`,
|
||||
apiFormat: endpoint.api_format,
|
||||
endpointId: endpoint.id,
|
||||
requestHeaders,
|
||||
requestBody,
|
||||
concurrency: 5,
|
||||
concurrency: isPoolManagedProvider.value ? POOL_TEST_CONCURRENCY : SINGLE_TEST_CONCURRENCY,
|
||||
onError: () => {
|
||||
if (activeEndpoints.value.length > 1) {
|
||||
return true
|
||||
@@ -510,9 +535,12 @@ async function testModelConnection(model: Model) {
|
||||
|
||||
pendingTestModel.value = model
|
||||
selectedTestEndpoint.value = activeEndpoints.value[0] ?? null
|
||||
testRequestBodyDraft.value = buildDefaultModelTestRequestBody(
|
||||
testRequestHeadersResetValue.value = buildDefaultModelTestRequestHeaders()
|
||||
testRequestHeadersDraft.value = testRequestHeadersResetValue.value
|
||||
testRequestBodyResetValue.value = buildDefaultModelTestRequestBody(
|
||||
model.global_model_name || model.provider_model_name,
|
||||
)
|
||||
testRequestBodyDraft.value = testRequestBodyResetValue.value
|
||||
modelTest.testResult.value = null
|
||||
modelTest.dialogOpen.value = true
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
const DEFAULT_MODEL_TEST_MESSAGE = 'Hello! This is a test message.'
|
||||
|
||||
/** Pool-managed provider runs concurrent checks; single-key provider does not. */
|
||||
export const POOL_TEST_CONCURRENCY = 5
|
||||
export const SINGLE_TEST_CONCURRENCY = 1
|
||||
|
||||
export function buildDefaultModelTestRequestBody(modelName: string): string {
|
||||
return JSON.stringify({
|
||||
model: modelName,
|
||||
@@ -15,14 +19,23 @@ export function buildDefaultModelTestRequestBody(modelName: string): string {
|
||||
}, null, 2)
|
||||
}
|
||||
|
||||
export function parseModelTestRequestBodyDraft(
|
||||
export function buildDefaultModelTestRequestHeaders(): string {
|
||||
return JSON.stringify({}, null, 2)
|
||||
}
|
||||
|
||||
function parseModelTestJsonObjectDraft(
|
||||
draft: string,
|
||||
options: {
|
||||
emptyValue: Record<string, unknown> | null
|
||||
emptyError: string | null
|
||||
invalidTypeError: string
|
||||
},
|
||||
): { value: Record<string, unknown> | null; error: string | null } {
|
||||
const normalized = draft.trim()
|
||||
if (!normalized) {
|
||||
return {
|
||||
value: null,
|
||||
error: '测试请求体不能为空',
|
||||
value: options.emptyValue,
|
||||
error: options.emptyError,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +44,7 @@ export function parseModelTestRequestBodyDraft(
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
return {
|
||||
value: null,
|
||||
error: '测试请求体必须是 JSON 对象',
|
||||
error: options.invalidTypeError,
|
||||
}
|
||||
}
|
||||
return {
|
||||
@@ -45,3 +58,23 @@ export function parseModelTestRequestBodyDraft(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function parseModelTestRequestBodyDraft(
|
||||
draft: string,
|
||||
): { value: Record<string, unknown> | null; error: string | null } {
|
||||
return parseModelTestJsonObjectDraft(draft, {
|
||||
emptyValue: null,
|
||||
emptyError: '测试请求体不能为空',
|
||||
invalidTypeError: '测试请求体必须是 JSON 对象',
|
||||
})
|
||||
}
|
||||
|
||||
export function parseModelTestRequestHeadersDraft(
|
||||
draft: string,
|
||||
): { value: Record<string, unknown> | null; error: string | null } {
|
||||
return parseModelTestJsonObjectDraft(draft, {
|
||||
emptyValue: {},
|
||||
emptyError: null,
|
||||
invalidTypeError: '测试请求头必须是 JSON 对象',
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user