feat: AI端点测试动态参数表单、接口工作流行内测试、本地开发环境改为线上域名

- 后端新增 /ai/endpoint/test 和 /ai/endpoint/stream 接口,支持直接端点测试
- 前端增加行内测试功能(场景绑定+接口工作流)
- 测试对话框增加动态参数表单和参数定义编辑
- 支持 _meta 格式的默认输入参数处理
- web、web-admin 本地开发环境 API 调用改为线上域名 https://lifescript.happylifeos.com

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 16:10:24 +08:00
parent e06b22ad69
commit d3746fa6c7
17 changed files with 559 additions and 28 deletions
+8
View File
@@ -237,6 +237,10 @@ export function listAiEndpoints() {
return request({ url: '/ai/endpoints', method: 'get' })
}
export function getEndpointTestTemplate(id: string) {
return request({ url: '/ai/endpoints/test-template', method: 'get', params: { id } })
}
export function saveAiEndpoint(data: AiEndpointConfig) {
return request({ url: '/ai/endpoints', method: data.id ? 'put' : 'post', data })
}
@@ -249,6 +253,10 @@ export function listAiScenes() {
return request({ url: '/ai/scenes', method: 'get' })
}
export function getSceneTestTemplate(sceneCode: string) {
return request({ url: '/ai/scenes/test-template', method: 'get', params: { sceneCode } })
}
export function saveAiScene(data: AiSceneBinding) {
return request({ url: '/ai/scenes', method: data.id ? 'put' : 'post', data })
}
+13 -2
View File
@@ -273,16 +273,17 @@ export interface AiEndpointRuntimeRequest {
export interface TestParamField {
name: string
label: string
type: 'string' | 'textarea' | 'number' | 'boolean'
type: 'string' | 'textarea' | 'number' | 'boolean' | 'json'
value: any
defaultValue: any
required: boolean
placeholder?: string
}
export interface ParamDefinition {
name: string
label: string
type: 'string' | 'textarea' | 'number' | 'boolean'
type: 'string' | 'textarea' | 'number' | 'boolean' | 'json'
defaultValue: any
required: boolean
}
@@ -296,3 +297,13 @@ export interface AiRuntimeTestResponse {
errorCode?: string
errorMessage?: string
}
export interface AiTestTemplateResponse {
sceneCode?: string
endpointId?: string
endpointCode?: string
endpointName?: string
providerType?: string
inputs: Record<string, any>
paramFields: TestParamField[]
}
+87 -7
View File
@@ -218,6 +218,7 @@
<el-option label="多行" value="textarea" />
<el-option label="数字" value="number" />
<el-option label="开关" value="boolean" />
<el-option label="JSON" value="json" />
</el-select>
<el-input v-model="param.defaultValue" placeholder="默认值" style="width: 100px" />
<el-checkbox v-model="param.required">必填</el-checkbox>
@@ -298,6 +299,7 @@
<el-input v-if="field.type === 'textarea'" v-model="field.value" type="textarea" :rows="3" :placeholder="field.name" @input="syncEndpointJsonFromFields" />
<el-input-number v-if="field.type === 'number'" v-model="field.value" :placeholder="field.name" @change="syncEndpointJsonFromFields" style="width: 100%" />
<el-switch v-if="field.type === 'boolean'" v-model="field.value" @change="syncEndpointJsonFromFields" />
<el-input v-if="field.type === 'json'" v-model="field.value" type="textarea" :rows="4" :placeholder="field.placeholder || field.name" @input="syncEndpointJsonFromFields" />
</el-form-item>
</el-form>
<el-collapse style="margin-top: 12px">
@@ -336,6 +338,8 @@ import {
deleteAiEndpoint,
deleteAiProvider,
deleteAiScene,
getEndpointTestTemplate,
getSceneTestTemplate,
listAiCallLogs,
listAiEndpoints,
listAiProviders,
@@ -348,7 +352,7 @@ import {
testAiRuntime,
testEndpointRuntime
} from '@/api/aiconfig'
import type { AiCallLog, AiEndpointConfig, AiProvider, AiRuntimeTestResponse, AiSceneBinding, TestParamField, ParamDefinition } from '@/types/aiconfig'
import type { AiCallLog, AiEndpointConfig, AiProvider, AiRuntimeTestResponse, AiSceneBinding, TestParamField, ParamDefinition, AiTestTemplateResponse } from '@/types/aiconfig'
const activeTab = ref('providers')
const loading = ref(false)
@@ -478,27 +482,67 @@ function openScene(row?: AiSceneBinding) {
sceneDialog.value = true
}
function openRuntimeTest() {
async function openRuntimeTest() {
testForm.sceneCode = scenes.value.find(item => item.isEnabled === 1 && item.endpointId)?.sceneCode || scenes.value[0]?.sceneCode || ''
testResult.value = null
nonStreamResult.value = null
await loadSceneTemplate(testForm.sceneCode)
testDialog.value = true
}
function openSceneRuntimeTest(row: AiSceneBinding) {
async function openSceneRuntimeTest(row: AiSceneBinding) {
testForm.sceneCode = row.sceneCode
testResult.value = null
nonStreamResult.value = null
await loadSceneTemplate(row.sceneCode)
testDialog.value = true
}
function openEndpointTest(row: AiEndpointConfig) {
async function openEndpointTest(row: AiEndpointConfig) {
if (!row.id) return
endpointTestRow.value = row
endpointParamFields.value = parseParamFields(row.defaultInputs)
endpointInputsJson.value = buildInputsJson(endpointParamFields.value)
endpointTestResult.value = null
endpointNonStreamResult.value = null
endpointTestDialog.value = true
endpointTesting.value = true
try {
const res = await getEndpointTestTemplate(row.id!)
applyEndpointTemplate(res.data as AiTestTemplateResponse)
} catch (error: any) {
ElMessage.warning(error?.message || '测试样例加载失败,已使用本地默认参数')
} finally {
endpointTesting.value = false
}
}
async function loadSceneTemplate(sceneCode: string) {
if (!sceneCode) {
testInputsJson.value = '{\n "prompt": "请用一句中文回复测试成功。"\n}'
return
}
try {
const res = await getSceneTestTemplate(sceneCode)
const template = res.data as AiTestTemplateResponse
testInputsJson.value = JSON.stringify(template.inputs || {}, null, 2)
} catch (error: any) {
testInputsJson.value = '{\n "prompt": "请用一句中文回复测试成功。"\n}'
ElMessage.warning(error?.message || '场景测试样例加载失败,已使用通用测试参数')
}
}
function applyEndpointTemplate(template: AiTestTemplateResponse) {
const inputs = template.inputs || {}
endpointInputsJson.value = JSON.stringify(inputs, null, 2)
endpointParamFields.value = (template.paramFields || []).map(field => ({
...field,
value: field.type === 'json'
? JSON.stringify(field.value ?? {}, null, 2)
: field.value,
defaultValue: field.value,
required: Boolean(field.required)
}))
}
function parseParamFields(defaultInputs?: string): TestParamField[] {
@@ -533,16 +577,28 @@ function parseParamFields(defaultInputs?: string): TestParamField[] {
}
}
function inferParamType(val: any): 'string' | 'textarea' | 'number' | 'boolean' {
function inferParamType(val: any): 'string' | 'textarea' | 'number' | 'boolean' | 'json' {
if (typeof val === 'number') return 'number'
if (typeof val === 'boolean') return 'boolean'
if (val && typeof val === 'object') return 'json'
if (typeof val === 'string' && val.length > 80) return 'textarea'
return 'string'
}
function buildInputsJson(fields: TestParamField[]): string {
const obj: Record<string, any> = {}
fields.forEach(f => { obj[f.name] = f.value })
fields.forEach(f => {
if (!f.name) return
if (f.type === 'json') {
try {
obj[f.name] = typeof f.value === 'string' ? JSON.parse(f.value || '{}') : f.value
} catch {
obj[f.name] = f.value
}
} else {
obj[f.name] = f.value
}
})
return JSON.stringify(obj, null, 2)
}
@@ -555,7 +611,9 @@ function syncEndpointFieldsFromJson() {
const parsed = JSON.parse(endpointInputsJson.value || '{}')
endpointParamFields.value.forEach(field => {
if (field.name in parsed) {
field.value = parsed[field.name]
field.value = field.type === 'json'
? JSON.stringify(parsed[field.name] ?? {}, null, 2)
: parsed[field.name]
}
})
} catch { /* ignore parse errors */ }
@@ -726,6 +784,7 @@ async function submitRuntimeTest() {
async function submitEndpointNonStreamTest() {
if (!endpointTestRow.value) return
if (!validateEndpointFields()) return
let inputs: Record<string, any>
try {
inputs = JSON.parse(endpointInputsJson.value || '{}')
@@ -757,6 +816,7 @@ async function submitEndpointNonStreamTest() {
async function submitEndpointStreamTest() {
if (!endpointTestRow.value) return
if (!validateEndpointFields()) return
let inputs: Record<string, any>
try {
inputs = JSON.parse(endpointInputsJson.value || '{}')
@@ -796,6 +856,26 @@ async function submitEndpointStreamTest() {
}
}
function validateEndpointFields() {
for (const field of endpointParamFields.value) {
if (!field.required) continue
const value = field.value
if (value === null || value === undefined || String(value).trim() === '') {
ElMessage.error(`请填写必填参数:${field.label || field.name}`)
return false
}
}
for (const field of endpointParamFields.value.filter(item => item.type === 'json')) {
try {
JSON.parse(field.value || '{}')
} catch {
ElMessage.error(`参数 ${field.label || field.name} 不是有效 JSON`)
return false
}
}
return true
}
onMounted(loadAll)
</script>