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:
@@ -15,7 +15,15 @@ public class AiTemplateRenderer {
|
|||||||
Map<String, Object> inputs = new HashMap<>();
|
Map<String, Object> inputs = new HashMap<>();
|
||||||
if (StringUtils.hasText(defaultInputs)) {
|
if (StringUtils.hasText(defaultInputs)) {
|
||||||
try {
|
try {
|
||||||
inputs.putAll(JSON.parseObject(defaultInputs));
|
JSONObject parsed = JSON.parseObject(defaultInputs);
|
||||||
|
parsed.forEach((key, value) -> {
|
||||||
|
// 兼容 _meta 格式:{ "_meta": {...}, "value": "..." }
|
||||||
|
if (value instanceof JSONObject && ((JSONObject) value).containsKey("_meta")) {
|
||||||
|
inputs.put(key, ((JSONObject) value).get("value"));
|
||||||
|
} else {
|
||||||
|
inputs.put(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
inputs.put("default_input", defaultInputs);
|
inputs.put("default_input", defaultInputs);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -270,6 +270,23 @@ export interface AiEndpointRuntimeRequest {
|
|||||||
inputs: Record<string, any>
|
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 {
|
export interface AiRuntimeTestResponse {
|
||||||
sceneCode: string
|
sceneCode: string
|
||||||
status: string
|
status: string
|
||||||
|
|||||||
@@ -209,6 +209,22 @@
|
|||||||
<el-form-item label="机器人 ID"><el-input v-model="endpointForm.botId" /></el-form-item>
|
<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.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="默认入参"><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="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-item label="启用状态"><el-switch v-model="endpointEnabled" active-text="启用" inactive-text="禁用" /></el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
@@ -275,11 +291,20 @@
|
|||||||
<span class="endpoint-label">接口名称:</span>
|
<span class="endpoint-label">接口名称:</span>
|
||||||
<span>{{ endpointTestRow?.endpointName }}({{ endpointTestRow?.endpointCode }})</span>
|
<span>{{ endpointTestRow?.endpointName }}({{ endpointTestRow?.endpointCode }})</span>
|
||||||
</div>
|
</div>
|
||||||
<el-form label-width="110px" style="margin-top: 16px">
|
<el-form v-if="endpointParamFields.length > 0" label-width="120px" style="margin-top: 16px">
|
||||||
<el-form-item label="入参 JSON">
|
<el-form-item v-for="field in endpointParamFields" :key="field.name" :label="field.label" :required="field.required"
|
||||||
<el-input v-model="endpointInputsJson" type="textarea" :rows="6" placeholder="请输入 JSON 入参" />
|
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-item>
|
||||||
</el-form>
|
</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
|
<el-alert
|
||||||
v-if="endpointNonStreamResult"
|
v-if="endpointNonStreamResult"
|
||||||
:type="endpointNonStreamResult.status === 'success' ? 'success' : 'error'"
|
:type="endpointNonStreamResult.status === 'success' ? 'success' : 'error'"
|
||||||
@@ -323,7 +348,7 @@ import {
|
|||||||
testAiRuntime,
|
testAiRuntime,
|
||||||
testEndpointRuntime
|
testEndpointRuntime
|
||||||
} from '@/api/aiconfig'
|
} 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 activeTab = ref('providers')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -347,6 +372,9 @@ const endpointTestRow = ref<AiEndpointConfig | null>(null)
|
|||||||
const endpointTestResult = ref<AiRuntimeTestResponse | null>(null)
|
const endpointTestResult = ref<AiRuntimeTestResponse | null>(null)
|
||||||
const endpointNonStreamResult = ref<AiRuntimeTestResponse | null>(null)
|
const endpointNonStreamResult = ref<AiRuntimeTestResponse | null>(null)
|
||||||
const endpointInputsJson = ref('{}')
|
const endpointInputsJson = ref('{}')
|
||||||
|
const endpointParamFields = ref<TestParamField[]>([])
|
||||||
|
|
||||||
|
const paramDefinitions = ref<ParamDefinition[]>([])
|
||||||
|
|
||||||
const providerForm = reactive<AiProvider>(newProvider())
|
const providerForm = reactive<AiProvider>(newProvider())
|
||||||
const endpointForm = reactive<AiEndpointConfig>(newEndpoint())
|
const endpointForm = reactive<AiEndpointConfig>(newEndpoint())
|
||||||
@@ -441,6 +469,7 @@ function openProvider(row?: AiProvider) {
|
|||||||
|
|
||||||
function openEndpoint(row?: AiEndpointConfig) {
|
function openEndpoint(row?: AiEndpointConfig) {
|
||||||
assignForm(endpointForm, row ? { ...row } : newEndpoint())
|
assignForm(endpointForm, row ? { ...row } : newEndpoint())
|
||||||
|
paramDefinitions.value = parseDefinitionsFromDefaultInputs(row?.defaultInputs)
|
||||||
endpointDialog.value = true
|
endpointDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,25 +494,116 @@ function openSceneRuntimeTest(row: AiSceneBinding) {
|
|||||||
|
|
||||||
function openEndpointTest(row: AiEndpointConfig) {
|
function openEndpointTest(row: AiEndpointConfig) {
|
||||||
endpointTestRow.value = row
|
endpointTestRow.value = row
|
||||||
if (row.defaultInputs) {
|
endpointParamFields.value = parseParamFields(row.defaultInputs)
|
||||||
try {
|
endpointInputsJson.value = buildInputsJson(endpointParamFields.value)
|
||||||
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
|
endpointTestResult.value = null
|
||||||
endpointNonStreamResult.value = null
|
endpointNonStreamResult.value = null
|
||||||
endpointTestDialog.value = true
|
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() {
|
async function submitProvider() {
|
||||||
await saveAiProvider({ ...providerForm })
|
await saveAiProvider({ ...providerForm })
|
||||||
providerDialog.value = false
|
providerDialog.value = false
|
||||||
@@ -492,7 +612,12 @@ async function submitProvider() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function submitEndpoint() {
|
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
|
endpointDialog.value = false
|
||||||
ElMessage.success('接口工作流已保存')
|
ElMessage.success('接口工作流已保存')
|
||||||
await loadAll()
|
await loadAll()
|
||||||
|
|||||||
Reference in New Issue
Block a user