feat: JSON编辑器增强 - Token映射下拉、字段选项key:value、序列化始终包装type

1. 模型配置弹窗: Token映射三个字段由输入框改为下拉菜单
   - 同步/流式从响应映射取key, 异步从回调响应映射取key
   - 切换返回类型时清空Token映射, 无映射配置时禁用并提示
2. JSON编辑器序列化: 所有节点始终输出{type, value/attrs}格式
   - 容器(object/array)始终{type, attrs}, 标量始终{type, value}
3. 字段配置增强:
   - 对象类型新增"循环生成"开关
   - 下拉控件选项列表改为key:value双输入框
4. 类型注释更新
This commit is contained in:
2026-07-15 16:33:58 +08:00
parent a6eaf57898
commit 5742608b97
5 changed files with 78 additions and 21 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
import request from '/@/utils/request';
/** Token 映射 */
/** Token 映射(三个字段存的是对应响应映射中的 key,不是 JSON Path */
export interface TokenMapping {
promptTokens: string;
completionTokens: string;
@@ -25,6 +25,10 @@
</el-select>
<div class="form-hint">JSON 数据类型</div>
</el-form-item>
<el-form-item v-if="form.jsonType === 'object'" label="循环生成">
<el-switch v-model="form.loop" />
<span class="form-hint">此对象字段是否循环生成</span>
</el-form-item>
<el-form-item v-if="jsonTypeIsScalar" label="默认值">
<template v-if="form.jsonType === 'boolean'">
<el-select v-model="form.defaultValue" placeholder="选择默认值" clearable>
@@ -53,10 +57,11 @@
<el-form-item v-if="form.fieldType === 'select'" label="选项列表">
<div class="options-editor">
<div v-for="(opt, idx) in form.options" :key="idx" class="option-row">
<el-input v-model="form.options[idx]" size="small" placeholder="选项值" />
<el-input v-model="opt.label" size="small" placeholder="显示名" style="width:120px" />
<el-input v-model="opt.value" size="small" placeholder="选项值" style="width:120px" />
<el-button size="small" text type="danger" @click="form.options.splice(idx, 1)">×</el-button>
</div>
<el-button size="small" class="add-option-btn" @click="form.options.push('')"><el-icon><Plus /></el-icon> 添加选项</el-button>
<el-button size="small" class="add-option-btn" @click="form.options.push({ label: '', value: '' })"><el-icon><Plus /></el-icon> 添加选项</el-button>
</div>
</el-form-item>
<template v-if="form.fieldType === 'string' || form.fieldType === 'textarea'">
@@ -125,6 +130,10 @@
</el-select>
<div class="form-hint">JSON 数据类型</div>
</el-form-item>
<el-form-item v-if="form.jsonType === 'object'" label="循环生成">
<el-switch v-model="form.loop" />
<span class="form-hint">此对象字段是否循环生成</span>
</el-form-item>
<el-form-item v-if="jsonTypeIsScalar" label="默认值">
<template v-if="form.jsonType === 'boolean'">
<el-select v-model="form.defaultValue" placeholder="选择默认值" clearable>
@@ -153,10 +162,11 @@
<el-form-item v-if="form.fieldType === 'select'" label="选项列表">
<div class="options-editor">
<div v-for="(opt, idx) in form.options" :key="idx" class="option-row">
<el-input v-model="form.options[idx]" size="small" placeholder="选项值" />
<el-input v-model="opt.label" size="small" placeholder="显示名" style="width:120px" />
<el-input v-model="opt.value" size="small" placeholder="选项值" style="width:120px" />
<el-button size="small" text type="danger" @click="form.options.splice(idx, 1)">×</el-button>
</div>
<el-button size="small" class="add-option-btn" @click="form.options.push('')"><el-icon><Plus /></el-icon> 添加选项</el-button>
<el-button size="small" class="add-option-btn" @click="form.options.push({ label: '', value: '' })"><el-icon><Plus /></el-icon> 添加选项</el-button>
</div>
</el-form-item>
<template v-if="form.fieldType === 'string' || form.fieldType === 'textarea'">
@@ -204,7 +214,7 @@ interface UploadRule { format: string; maxSize?: number; maxCount?: number; }
interface FieldFormData {
nodeKey: string; nodeId: string; jsonType: string; fieldType: string;
label: string; description: string; role: string;
isForm: boolean; required: boolean; defaultValue: string; options: string[];
isForm: boolean; required: boolean; loop: boolean; defaultValue: string; options: Array<{ label: string; value: string }>;
constraint: {
minLength?: number; maxLength?: number; pattern?: string;
min?: number; max?: number; numberType?: string;
@@ -231,7 +241,7 @@ const emit = defineEmits<{
const form = reactive<FieldFormData>({
nodeKey: '', nodeId: '', jsonType: 'string', fieldType: 'string',
label: '', description: '', role: '',
isForm: true, required: false, defaultValue: '', options: [], constraint: {},
isForm: true, required: false, loop: false, defaultValue: '', options: [], constraint: {},
});
const uploadRules = ref<UploadRule[]>([]);
@@ -251,6 +261,7 @@ function resetForm() {
form.nodeKey = ''; form.nodeId = ''; form.jsonType = 'string'; form.fieldType = 'string';
form.label = ''; form.description = ''; form.role = '';
form.isForm = true; form.required = false; form.defaultValue = '';
form.loop = false;
form.options = []; form.constraint = {};
// @ts-ignore
form._createParentId = undefined; form._isArrayParent = false;
@@ -265,8 +276,13 @@ function loadForm(data: FieldFormData | null) {
form.jsonType = data.jsonType || 'string'; form.fieldType = data.fieldType;
form.label = data.label || ''; form.description = data.description || ''; form.role = data.role || '';
form.isForm = data.isForm ?? true; form.required = data.required || false;
form.loop = data.loop ?? false;
form.defaultValue = data.defaultValue || '';
form.options = data.options ? [...data.options] : [];
// 兼容旧格式:如果 options 是字符串数组,转为 { label, value }
if (form.options.length > 0 && typeof (form.options as any)[0] === 'string') {
form.options = (data.options as string[]).map(s => ({ label: s, value: s }));
}
form.constraint = data.constraint ? { ...data.constraint } : {};
// @ts-ignore
form._createParentId = data._createParentId; form._isArrayParent = data._isArrayParent ?? false;
@@ -93,7 +93,7 @@ interface JsonNodeData {
isForm?: boolean;
required?: boolean;
defaultValue?: string | number;
options?: string[];
options?: Array<{ label: string; value: string }>;
constraint?: Record<string, any>;
};
}
+21 -6
View File
@@ -75,8 +75,9 @@ interface FieldConfig {
role?: string;
isForm?: boolean;
required?: boolean;
loop?: boolean;
defaultValue?: string | number;
options?: string[];
options?: Array<{ label: string; value: string }>;
constraint?: {
minLength?: number; maxLength?: number; pattern?: string;
min?: number; max?: number; numberType?: string;
@@ -100,7 +101,7 @@ interface FieldFormData {
nodeId: string;
jsonType: string;
fieldType: string; label: string; description: string; role: string;
isForm: boolean; required: boolean; defaultValue: string; options: string[];
isForm: boolean; required: boolean; loop: boolean; defaultValue: string; options: Array<{ label: string; value: string }>;
constraint: {
minLength?: number; maxLength?: number; pattern?: string;
min?: number; max?: number; numberType?: string;
@@ -182,11 +183,21 @@ function nodeToValue(node: JsonNodeData): any {
else if (node.type === 'boolean') rawValue = node.primitiveValue === true || node.primitiveValue === 'true';
else rawValue = String(node.primitiveValue);
if (node.config && hasAnyConfig(node)) {
const dataKey = (node.type === 'object' || node.type === 'array') ? 'attrs' : 'value';
return { type: node.type, [dataKey]: rawValue, ...node.config };
// 容器类型(object/array)始终输出 { type, attrs, ...config }
if (node.type === 'object' || node.type === 'array') {
const result: Record<string, any> = { type: node.type, attrs: rawValue };
if (node.config && hasAnyConfig(node)) {
Object.assign(result, node.config);
}
return result;
}
return rawValue;
// 标量类型也始终输出 { type, value, ...config }
const result: Record<string, any> = { type: node.type, value: rawValue };
if (node.config && hasAnyConfig(node)) {
Object.assign(result, node.config);
}
return result;
}
function defaultFormType(jsonType: string): string {
@@ -274,6 +285,7 @@ function formToConfig(form: FieldFormData): FieldConfig {
cfg.fieldType = form.fieldType || defaultFormType(form.jsonType);
cfg.isForm = form.isForm ?? true;
cfg.required = form.required ?? false;
cfg.loop = form.loop ?? false;
// 非骨架字段:有值才发
if (form.description) cfg.description = form.description;
if (form.role) cfg.role = form.role;
@@ -299,6 +311,7 @@ function hasAnyConfig(node: JsonNodeData): boolean {
if (c.label || c.description || c.role || c.defaultValue) return true;
if (c.options?.length) return true;
if (c.required) return true;
if (c.loop) return true;
if (c.isForm === false) return true;
if (c.constraint && Object.keys(c.constraint).length > 0) return true;
if (c.fieldType && c.fieldType !== defaultFormType(node.type)) return true;
@@ -324,6 +337,7 @@ function openFieldDialog(nodeId: string) {
role: cfg.role || '',
isForm: cfg.isForm ?? true,
required: cfg.required ?? false,
loop: cfg.loop ?? false,
defaultValue: String(cfg.defaultValue ?? ''),
options: cfg.options || [],
constraint: cfg.constraint ? { ...cfg.constraint } : {},
@@ -355,6 +369,7 @@ function openAddChildDialog(parentNodeId: string) {
role: '',
isForm: true,
required: false,
loop: false,
defaultValue: '',
options: [],
constraint: {},
@@ -128,17 +128,23 @@
<el-row :gutter="20">
<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8" class="mb20">
<el-form-item label="输入 Token 路径">
<el-input v-model="formData.tokenMapping.promptTokens" placeholder="如 data.usage.prompt_tokens" clearable />
<el-select v-model="formData.tokenMapping.promptTokens" :disabled="!hasMappingConfig" :placeholder="mappingPlaceholder" clearable style="width: 100%">
<el-option v-for="key in tokenMappingOptions" :key="key" :label="key" :value="key" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8" class="mb20">
<el-form-item label="输出 Token 路径">
<el-input v-model="formData.tokenMapping.completionTokens" placeholder="如 data.usage.completion_tokens" clearable />
<el-select v-model="formData.tokenMapping.completionTokens" :disabled="!hasMappingConfig" :placeholder="mappingPlaceholder" clearable style="width: 100%">
<el-option v-for="key in tokenMappingOptions" :key="key" :label="key" :value="key" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8" class="mb20">
<el-form-item label="总 Token 路径">
<el-input v-model="formData.tokenMapping.totalTokens" placeholder="如 data.usage.total_tokens" clearable />
<el-select v-model="formData.tokenMapping.totalTokens" :disabled="!hasMappingConfig" :placeholder="mappingPlaceholder" clearable style="width: 100%">
<el-option v-for="key in tokenMappingOptions" :key="key" :label="key" :value="key" />
</el-select>
</el-form-item>
</el-col>
</el-row>
@@ -379,6 +385,27 @@ watch(isReasoningModel, (val) => {
if (!val) formData.chatModel = false;
});
/** 根据返回类型获取 Token 映射下拉选项(响应映射的 key 列表) */
const tokenMappingOptions = computed(() => {
const source = formData.responseType === 2
? formData.asyncTaskMapping.responseMapping
: formData.responseMapping;
return source ? Object.keys(source) : [];
});
/** 是否有可用的映射配置 */
const hasMappingConfig = computed(() => tokenMappingOptions.value.length > 0);
/** 下拉占位文本 */
const mappingPlaceholder = computed(() =>
hasMappingConfig.value ? '请选择' : '请先配置响应映射'
);
/** 切换返回类型时清空 token mapping */
watch(() => formData.responseType, () => {
formData.tokenMapping = { ...defaultTokenMapping };
});
/** JSON 编辑器弹窗状态 */
const jsonEditorVisible = ref(false);
const jsonEditorData = ref<Record<string, unknown>>({});
@@ -547,10 +574,9 @@ const handleSubmit = async () => {
let asyncTaskMapping: AsyncTaskMapping | undefined;
let lastFrame: string | undefined;
if (formData.responseType === 3) {
const hasTokenConfig = Object.values(formData.tokenMapping).some((v) => v);
tokenMapping = hasTokenConfig ? { ...formData.tokenMapping } : undefined;
}
// 所有返回类型共用 Token 映射(存的是响应映射中的 key)
const hasTokenConfig = Object.values(formData.tokenMapping).some((v) => v);
tokenMapping = hasTokenConfig ? { ...formData.tokenMapping } : undefined;
if (formData.responseType === 2) {
const atm = formData.asyncTaskMapping;