feat: 前端增加行内测试功能(场景绑定+接口工作流)
- 场景绑定表和接口工作流表操作列增加「测试」按钮 - 新增接口工作流测试对话框(流式+非流式) - API 层提取 fetchSseStream 共享辅助函数 - 新增 testEndpointRuntime 和 streamEndpointRuntime Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,13 @@
|
|||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
import type {
|
import type {
|
||||||
AiConfigPageRequest,
|
AiConfigPageRequest,
|
||||||
AiConfigCreateRequest,
|
AiConfigCreateRequest,
|
||||||
AiConfigUpdateRequest,
|
AiConfigUpdateRequest,
|
||||||
AiProvider,
|
AiProvider,
|
||||||
AiEndpointConfig,
|
AiEndpointConfig,
|
||||||
AiSceneBinding,
|
AiSceneBinding,
|
||||||
AiRuntimeRequest
|
AiRuntimeRequest,
|
||||||
|
AiEndpointRuntimeRequest
|
||||||
} from '@/types/aiconfig'
|
} from '@/types/aiconfig'
|
||||||
|
|
||||||
// 分页查询AI配置
|
// 分页查询AI配置
|
||||||
@@ -287,18 +288,19 @@ function parseSseFrame(frame: string): AiRuntimeStreamEvent | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function streamAiRuntime(
|
async function fetchSseStream(
|
||||||
data: AiRuntimeRequest,
|
url: string,
|
||||||
|
body: Record<string, any>,
|
||||||
onEvent: (event: AiRuntimeStreamEvent, output: string) => void
|
onEvent: (event: AiRuntimeStreamEvent, output: string) => void
|
||||||
) {
|
) {
|
||||||
const token = localStorage.getItem('adminToken')
|
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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(token ? { Authorization: `Bearer ${token}` } : {})
|
...(token ? { Authorization: `Bearer ${token}` } : {})
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(body)
|
||||||
})
|
})
|
||||||
if (!response.ok || !response.body) {
|
if (!response.ok || !response.body) {
|
||||||
throw new Error(`流式测试请求失败(${response.status})`)
|
throw new Error(`流式测试请求失败(${response.status})`)
|
||||||
@@ -335,3 +337,21 @@ export async function streamAiRuntime(
|
|||||||
if (buffer.trim()) consumeText('\n\n')
|
if (buffer.trim()) consumeText('\n\n')
|
||||||
return output
|
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 })
|
||||||
|
}
|
||||||
|
|||||||
@@ -265,6 +265,11 @@ export interface AiRuntimeRequest {
|
|||||||
inputs: Record<string, any>
|
inputs: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AiEndpointRuntimeRequest {
|
||||||
|
endpointId: string
|
||||||
|
inputs: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
export interface AiRuntimeTestResponse {
|
export interface AiRuntimeTestResponse {
|
||||||
sceneCode: string
|
sceneCode: string
|
||||||
status: string
|
status: string
|
||||||
|
|||||||
@@ -97,10 +97,11 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="150" fixed="right" align="center">
|
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" @click="openEndpoint(row)">编辑</el-button>
|
<el-button link type="primary" @click="openEndpoint(row)">编辑</el-button>
|
||||||
<el-button link type="danger" @click="removeEndpoint(row)">删除</el-button>
|
<el-button link type="danger" @click="removeEndpoint(row)">删除</el-button>
|
||||||
|
<el-button link type="success" :disabled="row.isEnabled !== 1" @click="openEndpointTest(row)">测试</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -134,10 +135,11 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="150" fixed="right" align="center">
|
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" @click="openScene(row)">编辑</el-button>
|
<el-button link type="primary" @click="openScene(row)">编辑</el-button>
|
||||||
<el-button link type="danger" @click="removeScene(row)">删除</el-button>
|
<el-button link type="danger" @click="removeScene(row)">删除</el-button>
|
||||||
|
<el-button link type="success" @click="openSceneRuntimeTest(row)">测试</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -267,6 +269,37 @@
|
|||||||
<el-button type="primary" :loading="testing" @click="submitRuntimeTest">流式测试</el-button>
|
<el-button type="primary" :loading="testing" @click="submitRuntimeTest">流式测试</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="endpointTestDialog" title="接口工作流测试" width="760px">
|
||||||
|
<div class="endpoint-info">
|
||||||
|
<span class="endpoint-label">接口名称:</span>
|
||||||
|
<span>{{ endpointTestRow?.endpointName }}({{ endpointTestRow?.endpointCode }})</span>
|
||||||
|
</div>
|
||||||
|
<el-form label-width="110px" style="margin-top: 16px">
|
||||||
|
<el-form-item label="入参 JSON">
|
||||||
|
<el-input v-model="endpointInputsJson" type="textarea" :rows="6" placeholder="请输入 JSON 入参" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-alert
|
||||||
|
v-if="endpointNonStreamResult"
|
||||||
|
:type="endpointNonStreamResult.status === 'success' ? 'success' : 'error'"
|
||||||
|
:title="endpointNonStreamResult.status === 'success' ? '非流式测试成功' : '非流式测试失败'"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
<pre v-if="endpointNonStreamResult" class="test-output">{{ endpointNonStreamResult.output || endpointNonStreamResult.errorMessage || '暂无输出' }}</pre>
|
||||||
|
<el-alert
|
||||||
|
v-if="endpointTestResult"
|
||||||
|
:type="endpointTestResult.status === 'success' ? 'success' : 'error'"
|
||||||
|
:title="endpointTestResult.status === 'success' ? '流式测试成功' : '流式测试失败'"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
<pre v-if="endpointTestResult" class="test-output">{{ endpointTestResult.output || endpointTestResult.errorMessage || '暂无输出' }}</pre>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="endpointTestDialog = false">关闭</el-button>
|
||||||
|
<el-button :loading="endpointTesting" @click="submitEndpointNonStreamTest">非流式测试</el-button>
|
||||||
|
<el-button type="primary" :loading="endpointTesting" @click="submitEndpointStreamTest">流式测试</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -286,7 +319,9 @@ import {
|
|||||||
saveAiProvider,
|
saveAiProvider,
|
||||||
saveAiScene,
|
saveAiScene,
|
||||||
streamAiRuntime,
|
streamAiRuntime,
|
||||||
testAiRuntime
|
streamEndpointRuntime,
|
||||||
|
testAiRuntime,
|
||||||
|
testEndpointRuntime
|
||||||
} from '@/api/aiconfig'
|
} from '@/api/aiconfig'
|
||||||
import type { AiCallLog, AiEndpointConfig, AiProvider, AiRuntimeTestResponse, AiSceneBinding } from '@/types/aiconfig'
|
import type { AiCallLog, AiEndpointConfig, AiProvider, AiRuntimeTestResponse, AiSceneBinding } from '@/types/aiconfig'
|
||||||
|
|
||||||
@@ -306,6 +341,13 @@ const testResult = ref<AiRuntimeTestResponse | null>(null)
|
|||||||
const nonStreamResult = ref<AiRuntimeTestResponse | null>(null)
|
const nonStreamResult = ref<AiRuntimeTestResponse | null>(null)
|
||||||
const testInputsJson = ref('{\n "prompt": "请用一句中文回复测试成功。"\n}')
|
const testInputsJson = ref('{\n "prompt": "请用一句中文回复测试成功。"\n}')
|
||||||
|
|
||||||
|
const endpointTestDialog = ref(false)
|
||||||
|
const endpointTesting = ref(false)
|
||||||
|
const endpointTestRow = ref<AiEndpointConfig | null>(null)
|
||||||
|
const endpointTestResult = ref<AiRuntimeTestResponse | null>(null)
|
||||||
|
const endpointNonStreamResult = ref<AiRuntimeTestResponse | null>(null)
|
||||||
|
const endpointInputsJson = ref('{}')
|
||||||
|
|
||||||
const providerForm = reactive<AiProvider>(newProvider())
|
const providerForm = reactive<AiProvider>(newProvider())
|
||||||
const endpointForm = reactive<AiEndpointConfig>(newEndpoint())
|
const endpointForm = reactive<AiEndpointConfig>(newEndpoint())
|
||||||
const sceneForm = reactive<AiSceneBinding>(newScene())
|
const sceneForm = reactive<AiSceneBinding>(newScene())
|
||||||
@@ -414,6 +456,34 @@ function openRuntimeTest() {
|
|||||||
testDialog.value = true
|
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() {
|
async function submitProvider() {
|
||||||
await saveAiProvider({ ...providerForm })
|
await saveAiProvider({ ...providerForm })
|
||||||
providerDialog.value = false
|
providerDialog.value = false
|
||||||
@@ -529,6 +599,78 @@ async function submitRuntimeTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function submitEndpointNonStreamTest() {
|
||||||
|
if (!endpointTestRow.value) return
|
||||||
|
let inputs: Record<string, any>
|
||||||
|
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<string, any>
|
||||||
|
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)
|
onMounted(loadAll)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -630,4 +772,18 @@ onMounted(loadAll)
|
|||||||
border-radius: var(--ls-radius-md);
|
border-radius: var(--ls-radius-md);
|
||||||
white-space: pre-wrap;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user