diff --git a/web-admin/src/api/aiconfig.ts b/web-admin/src/api/aiconfig.ts index e9c5a8e..e331700 100644 --- a/web-admin/src/api/aiconfig.ts +++ b/web-admin/src/api/aiconfig.ts @@ -1,12 +1,13 @@ import request from '@/utils/request' -import type { - AiConfigPageRequest, - AiConfigCreateRequest, +import type { + AiConfigPageRequest, + AiConfigCreateRequest, AiConfigUpdateRequest, AiProvider, AiEndpointConfig, AiSceneBinding, - AiRuntimeRequest + AiRuntimeRequest, + AiEndpointRuntimeRequest } from '@/types/aiconfig' // 分页查询AI配置 @@ -287,18 +288,19 @@ function parseSseFrame(frame: string): AiRuntimeStreamEvent | null { } } -export async function streamAiRuntime( - data: AiRuntimeRequest, +async function fetchSseStream( + url: string, + body: Record, onEvent: (event: AiRuntimeStreamEvent, output: string) => void ) { const token = localStorage.getItem('adminToken') - const response = await fetch(`${import.meta.env.VITE_APP_BASE_API}/ai/runtime/stream`, { + const response = await fetch(`${import.meta.env.VITE_APP_BASE_API}${url}`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) }, - body: JSON.stringify(data) + body: JSON.stringify(body) }) if (!response.ok || !response.body) { throw new Error(`流式测试请求失败(${response.status})`) @@ -335,3 +337,21 @@ export async function streamAiRuntime( if (buffer.trim()) consumeText('\n\n') return output } + +export async function streamAiRuntime( + data: AiRuntimeRequest, + onEvent: (event: AiRuntimeStreamEvent, output: string) => void +) { + return fetchSseStream('/ai/runtime/stream', data, onEvent) +} + +export async function streamEndpointRuntime( + data: AiEndpointRuntimeRequest, + onEvent: (event: AiRuntimeStreamEvent, output: string) => void +) { + return fetchSseStream('/ai/endpoint/stream', data, onEvent) +} + +export function testEndpointRuntime(data: AiEndpointRuntimeRequest) { + return request({ url: '/ai/endpoint/test', method: 'post', data, timeout: 60000 }) +} diff --git a/web-admin/src/types/aiconfig.ts b/web-admin/src/types/aiconfig.ts index 3e26356..56c7ae0 100644 --- a/web-admin/src/types/aiconfig.ts +++ b/web-admin/src/types/aiconfig.ts @@ -265,6 +265,11 @@ export interface AiRuntimeRequest { inputs: Record } +export interface AiEndpointRuntimeRequest { + endpointId: string + inputs: Record +} + export interface AiRuntimeTestResponse { sceneCode: string status: string diff --git a/web-admin/src/views/aiconfig/AiRoutingList.vue b/web-admin/src/views/aiconfig/AiRoutingList.vue index 66643de..34fc2ea 100644 --- a/web-admin/src/views/aiconfig/AiRoutingList.vue +++ b/web-admin/src/views/aiconfig/AiRoutingList.vue @@ -97,10 +97,11 @@ - + @@ -134,10 +135,11 @@ - + @@ -267,6 +269,37 @@ 流式测试 + + +
+ 接口名称: + {{ endpointTestRow?.endpointName }}({{ endpointTestRow?.endpointCode }}) +
+ + + + + + +
{{ endpointNonStreamResult.output || endpointNonStreamResult.errorMessage || '暂无输出' }}
+ +
{{ endpointTestResult.output || endpointTestResult.errorMessage || '暂无输出' }}
+ +
@@ -286,7 +319,9 @@ import { saveAiProvider, saveAiScene, streamAiRuntime, - testAiRuntime + streamEndpointRuntime, + testAiRuntime, + testEndpointRuntime } from '@/api/aiconfig' import type { AiCallLog, AiEndpointConfig, AiProvider, AiRuntimeTestResponse, AiSceneBinding } from '@/types/aiconfig' @@ -306,6 +341,13 @@ const testResult = ref(null) const nonStreamResult = ref(null) const testInputsJson = ref('{\n "prompt": "请用一句中文回复测试成功。"\n}') +const endpointTestDialog = ref(false) +const endpointTesting = ref(false) +const endpointTestRow = ref(null) +const endpointTestResult = ref(null) +const endpointNonStreamResult = ref(null) +const endpointInputsJson = ref('{}') + const providerForm = reactive(newProvider()) const endpointForm = reactive(newEndpoint()) const sceneForm = reactive(newScene()) @@ -414,6 +456,34 @@ function openRuntimeTest() { testDialog.value = true } +function openSceneRuntimeTest(row: AiSceneBinding) { + testForm.sceneCode = row.sceneCode + testResult.value = null + nonStreamResult.value = null + testDialog.value = true +} + +function openEndpointTest(row: AiEndpointConfig) { + endpointTestRow.value = row + if (row.defaultInputs) { + try { + const parsed = JSON.parse(row.defaultInputs) + if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { + endpointInputsJson.value = JSON.stringify(parsed, null, 2) + } else { + endpointInputsJson.value = '{}' + } + } catch { + endpointInputsJson.value = '{}' + } + } else { + endpointInputsJson.value = '{}' + } + endpointTestResult.value = null + endpointNonStreamResult.value = null + endpointTestDialog.value = true +} + async function submitProvider() { await saveAiProvider({ ...providerForm }) providerDialog.value = false @@ -529,6 +599,78 @@ async function submitRuntimeTest() { } } +async function submitEndpointNonStreamTest() { + if (!endpointTestRow.value) return + let inputs: Record + try { + inputs = JSON.parse(endpointInputsJson.value || '{}') + } catch { + ElMessage.error('入参 JSON 格式不正确') + return + } + endpointTesting.value = true + try { + const res = await testEndpointRuntime({ endpointId: endpointTestRow.value.id!, inputs }) + endpointNonStreamResult.value = res.data as AiRuntimeTestResponse + if (endpointNonStreamResult.value.status === 'success') { + ElMessage.success('非流式测试成功') + } else { + ElMessage.error(`测试失败: ${endpointNonStreamResult.value.errorMessage || endpointNonStreamResult.value.errorCode}`) + } + await loadAll() + } catch (error: any) { + endpointNonStreamResult.value = { + sceneCode: '', + status: 'failed', + errorMessage: error?.message || '非流式测试失败' + } as AiRuntimeTestResponse + ElMessage.error(error?.message || '非流式测试失败') + } finally { + endpointTesting.value = false + } +} + +async function submitEndpointStreamTest() { + if (!endpointTestRow.value) return + let inputs: Record + try { + inputs = JSON.parse(endpointInputsJson.value || '{}') + } catch { + ElMessage.error('入参 JSON 格式不正确') + return + } + endpointTesting.value = true + endpointTestResult.value = { sceneCode: '', status: 'success', output: '', streamChunks: 0, durationMs: 0 } + const startedAt = Date.now() + try { + await streamEndpointRuntime({ endpointId: endpointTestRow.value.id!, inputs }, (event, output) => { + if (!endpointTestResult.value) return + endpointTestResult.value.output = output + if (event.type === 'delta') { + endpointTestResult.value.streamChunks = (endpointTestResult.value.streamChunks || 0) + 1 + } + if (event.type === 'done') { + endpointTestResult.value.status = 'success' + } + if (event.type === 'error') { + endpointTestResult.value.status = 'failed' + endpointTestResult.value.errorCode = event.code + endpointTestResult.value.errorMessage = event.message + } + endpointTestResult.value.durationMs = Date.now() - startedAt + }) + await loadAll() + } catch (error: any) { + if (endpointTestResult.value) { + endpointTestResult.value.status = 'failed' + endpointTestResult.value.errorMessage = error?.message || '流式测试失败' + endpointTestResult.value.durationMs = Date.now() - startedAt + } + } finally { + endpointTesting.value = false + } +} + onMounted(loadAll) @@ -630,4 +772,18 @@ onMounted(loadAll) border-radius: var(--ls-radius-md); white-space: pre-wrap; } + +.endpoint-info { + padding: 10px 14px; + color: rgba(226, 232, 240, 0.85); + background: rgba(0, 0, 0, 0.15); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: var(--ls-radius-md); + font-size: 14px; +} + +.endpoint-label { + color: var(--ls-text-muted); + margin-right: 6px; +}