feat: AI 测试对话框增加动态参数表单和参数定义编辑

- 测试对话框支持从 defaultInputs 解析参数并渲染动态表单
- 支持 _meta 格式的参数定义(label/type/required/value)
- 接口工作流编辑页面增加参数定义区域(增删改)
- 后端 AiTemplateRenderer.mergeInputs 兼容 _meta 格式
- JSON 编辑区折叠为「高级编辑」模式

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 15:01:21 +08:00
parent 5888816646
commit e06b22ad69
3 changed files with 170 additions and 20 deletions
+17
View File
@@ -270,6 +270,23 @@ export interface AiEndpointRuntimeRequest {
inputs: Record<string, any>
}
export interface TestParamField {
name: string
label: string
type: 'string' | 'textarea' | 'number' | 'boolean'
value: any
defaultValue: any
required: boolean
}
export interface ParamDefinition {
name: string
label: string
type: 'string' | 'textarea' | 'number' | 'boolean'
defaultValue: any
required: boolean
}
export interface AiRuntimeTestResponse {
sceneCode: string
status: string
+144 -19
View File
@@ -209,6 +209,22 @@
<el-form-item label="机器人 ID"><el-input v-model="endpointForm.botId" /></el-form-item>
<el-form-item label="请求模板"><el-input v-model="endpointForm.requestTemplate" type="textarea" :rows="4" placeholder="JSON 模板,可使用 {{prompt}} 等变量" /></el-form-item>
<el-form-item label="默认入参"><el-input v-model="endpointForm.defaultInputs" type="textarea" :rows="3" placeholder="JSON 格式,会与用户端入参合并" /></el-form-item>
<el-form-item label="参数定义">
<div v-for="(param, index) in paramDefinitions" :key="index" class="param-row" style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center">
<el-input v-model="param.name" placeholder="参数名" style="width: 110px" />
<el-input v-model="param.label" placeholder="显示标签" style="width: 120px" />
<el-select v-model="param.type" style="width: 100px">
<el-option label="文本" value="string" />
<el-option label="多行" value="textarea" />
<el-option label="数字" value="number" />
<el-option label="开关" value="boolean" />
</el-select>
<el-input v-model="param.defaultValue" placeholder="默认值" style="width: 100px" />
<el-checkbox v-model="param.required">必填</el-checkbox>
<el-button link type="danger" @click="removeParam(index)">删除</el-button>
</div>
<el-button link type="primary" @click="addParam">+ 添加参数</el-button>
</el-form-item>
<el-form-item label="支持流式"><el-switch v-model="endpointStream" active-text="支持" inactive-text="不支持" /></el-form-item>
<el-form-item label="启用状态"><el-switch v-model="endpointEnabled" active-text="启用" inactive-text="禁用" /></el-form-item>
</el-form>
@@ -275,11 +291,20 @@
<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 v-if="endpointParamFields.length > 0" label-width="120px" style="margin-top: 16px">
<el-form-item v-for="field in endpointParamFields" :key="field.name" :label="field.label" :required="field.required"
style="margin-bottom: 12px">
<el-input v-if="field.type === 'string'" v-model="field.value" :placeholder="field.name" @input="syncEndpointJsonFromFields" />
<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-form-item>
</el-form>
<el-collapse style="margin-top: 12px">
<el-collapse-item title="高级编辑(JSON">
<el-input v-model="endpointInputsJson" type="textarea" :rows="6" placeholder="请输入 JSON 入参" @input="syncEndpointFieldsFromJson" />
</el-collapse-item>
</el-collapse>
<el-alert
v-if="endpointNonStreamResult"
:type="endpointNonStreamResult.status === 'success' ? 'success' : 'error'"
@@ -323,7 +348,7 @@ import {
testAiRuntime,
testEndpointRuntime
} from '@/api/aiconfig'
import type { AiCallLog, AiEndpointConfig, AiProvider, AiRuntimeTestResponse, AiSceneBinding } from '@/types/aiconfig'
import type { AiCallLog, AiEndpointConfig, AiProvider, AiRuntimeTestResponse, AiSceneBinding, TestParamField, ParamDefinition } from '@/types/aiconfig'
const activeTab = ref('providers')
const loading = ref(false)
@@ -347,6 +372,9 @@ const endpointTestRow = ref<AiEndpointConfig | null>(null)
const endpointTestResult = ref<AiRuntimeTestResponse | null>(null)
const endpointNonStreamResult = ref<AiRuntimeTestResponse | null>(null)
const endpointInputsJson = ref('{}')
const endpointParamFields = ref<TestParamField[]>([])
const paramDefinitions = ref<ParamDefinition[]>([])
const providerForm = reactive<AiProvider>(newProvider())
const endpointForm = reactive<AiEndpointConfig>(newEndpoint())
@@ -441,6 +469,7 @@ function openProvider(row?: AiProvider) {
function openEndpoint(row?: AiEndpointConfig) {
assignForm(endpointForm, row ? { ...row } : newEndpoint())
paramDefinitions.value = parseDefinitionsFromDefaultInputs(row?.defaultInputs)
endpointDialog.value = true
}
@@ -465,25 +494,116 @@ function openSceneRuntimeTest(row: AiSceneBinding) {
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 = '{}'
}
endpointParamFields.value = parseParamFields(row.defaultInputs)
endpointInputsJson.value = buildInputsJson(endpointParamFields.value)
endpointTestResult.value = null
endpointNonStreamResult.value = null
endpointTestDialog.value = true
}
function parseParamFields(defaultInputs?: string): TestParamField[] {
if (!defaultInputs) return []
try {
const parsed = JSON.parse(defaultInputs)
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return []
return Object.entries(parsed).map(([key, val]: [string, any]) => {
// 新格式:{ _meta: {...}, value: ... }
if (val && typeof val === 'object' && '_meta' in val) {
return {
name: key,
label: val._meta.label || key,
type: val._meta.type || 'string',
value: val.value,
defaultValue: val.value,
required: val._meta.required || false
} as TestParamField
}
// 旧格式:直接值
return {
name: key,
label: key,
type: inferParamType(val),
value: val,
defaultValue: val,
required: false
} as TestParamField
})
} catch {
return []
}
}
function inferParamType(val: any): 'string' | 'textarea' | 'number' | 'boolean' {
if (typeof val === 'number') return 'number'
if (typeof val === 'boolean') return 'boolean'
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 })
return JSON.stringify(obj, null, 2)
}
function syncEndpointJsonFromFields() {
endpointInputsJson.value = buildInputsJson(endpointParamFields.value)
}
function syncEndpointFieldsFromJson() {
try {
const parsed = JSON.parse(endpointInputsJson.value || '{}')
endpointParamFields.value.forEach(field => {
if (field.name in parsed) {
field.value = parsed[field.name]
}
})
} catch { /* ignore parse errors */ }
}
function addParam() {
paramDefinitions.value.push({ name: '', label: '', type: 'string', defaultValue: '', required: false })
}
function removeParam(index: number) {
paramDefinitions.value.splice(index, 1)
}
function buildDefaultInputsFromDefinitions(defs: ParamDefinition[]): string {
const obj: Record<string, any> = {}
defs.forEach(d => {
if (d.name) {
obj[d.name] = {
_meta: { label: d.label || d.name, type: d.type, required: d.required },
value: d.defaultValue
}
}
})
return JSON.stringify(obj)
}
function parseDefinitionsFromDefaultInputs(defaultInputs?: string): ParamDefinition[] {
if (!defaultInputs) return []
try {
const parsed = JSON.parse(defaultInputs)
if (typeof parsed !== 'object' || parsed === null) return []
return Object.entries(parsed).map(([key, val]: [string, any]) => {
if (val && typeof val === 'object' && '_meta' in val) {
return {
name: key,
label: val._meta.label || key,
type: val._meta.type || 'string',
defaultValue: val.value,
required: val._meta.required || false
} as ParamDefinition
}
return { name: key, label: key, type: inferParamType(val), defaultValue: val, required: false } as ParamDefinition
})
} catch {
return []
}
}
async function submitProvider() {
await saveAiProvider({ ...providerForm })
providerDialog.value = false
@@ -492,7 +612,12 @@ async function submitProvider() {
}
async function submitEndpoint() {
await saveAiEndpoint({ ...endpointForm })
const data = { ...endpointForm }
// 如果有参数定义,合并到 defaultInputs
if (paramDefinitions.value.length > 0) {
data.defaultInputs = buildDefaultInputsFromDefinitions(paramDefinitions.value)
}
await saveAiEndpoint(data)
endpointDialog.value = false
ElMessage.success('接口工作流已保存')
await loadAll()