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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user