feat: 前端增加行内测试功能(场景绑定+接口工作流)

- 场景绑定表和接口工作流表操作列增加「测试」按钮
- 新增接口工作流测试对话框(流式+非流式)
- API 层提取 fetchSseStream 共享辅助函数
- 新增 testEndpointRuntime 和 streamEndpointRuntime

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 14:17:41 +08:00
parent cccb720060
commit c8f22bb157
3 changed files with 192 additions and 11 deletions
+159 -3
View File
@@ -97,10 +97,11 @@
</el-tag>
</template>
</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 }">
<el-button link type="primary" @click="openEndpoint(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>
</el-table-column>
</el-table>
@@ -134,10 +135,11 @@
</el-tag>
</template>
</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 }">
<el-button link type="primary" @click="openScene(row)">编辑</el-button>
<el-button link type="danger" @click="removeScene(row)">删除</el-button>
<el-button link type="success" @click="openSceneRuntimeTest(row)">测试</el-button>
</template>
</el-table-column>
</el-table>
@@ -267,6 +269,37 @@
<el-button type="primary" :loading="testing" @click="submitRuntimeTest">流式测试</el-button>
</template>
</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>
</template>
@@ -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<AiRuntimeTestResponse | null>(null)
const nonStreamResult = ref<AiRuntimeTestResponse | null>(null)
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 endpointForm = reactive<AiEndpointConfig>(newEndpoint())
const sceneForm = reactive<AiSceneBinding>(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<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)
</script>
@@ -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;
}
</style>