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:
@@ -1,6 +1,6 @@
|
|||||||
import request from '/@/utils/request';
|
import request from '/@/utils/request';
|
||||||
|
|
||||||
/** Token 映射 */
|
/** Token 映射(三个字段存的是对应响应映射中的 key,不是 JSON Path) */
|
||||||
export interface TokenMapping {
|
export interface TokenMapping {
|
||||||
promptTokens: string;
|
promptTokens: string;
|
||||||
completionTokens: string;
|
completionTokens: string;
|
||||||
|
|||||||
@@ -25,6 +25,10 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
<div class="form-hint">JSON 数据类型</div>
|
<div class="form-hint">JSON 数据类型</div>
|
||||||
</el-form-item>
|
</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="默认值">
|
<el-form-item v-if="jsonTypeIsScalar" label="默认值">
|
||||||
<template v-if="form.jsonType === 'boolean'">
|
<template v-if="form.jsonType === 'boolean'">
|
||||||
<el-select v-model="form.defaultValue" placeholder="选择默认值" clearable>
|
<el-select v-model="form.defaultValue" placeholder="选择默认值" clearable>
|
||||||
@@ -53,10 +57,11 @@
|
|||||||
<el-form-item v-if="form.fieldType === 'select'" label="选项列表">
|
<el-form-item v-if="form.fieldType === 'select'" label="选项列表">
|
||||||
<div class="options-editor">
|
<div class="options-editor">
|
||||||
<div v-for="(opt, idx) in form.options" :key="idx" class="option-row">
|
<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>
|
<el-button size="small" text type="danger" @click="form.options.splice(idx, 1)">×</el-button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<template v-if="form.fieldType === 'string' || form.fieldType === 'textarea'">
|
<template v-if="form.fieldType === 'string' || form.fieldType === 'textarea'">
|
||||||
@@ -125,6 +130,10 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
<div class="form-hint">JSON 数据类型</div>
|
<div class="form-hint">JSON 数据类型</div>
|
||||||
</el-form-item>
|
</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="默认值">
|
<el-form-item v-if="jsonTypeIsScalar" label="默认值">
|
||||||
<template v-if="form.jsonType === 'boolean'">
|
<template v-if="form.jsonType === 'boolean'">
|
||||||
<el-select v-model="form.defaultValue" placeholder="选择默认值" clearable>
|
<el-select v-model="form.defaultValue" placeholder="选择默认值" clearable>
|
||||||
@@ -153,10 +162,11 @@
|
|||||||
<el-form-item v-if="form.fieldType === 'select'" label="选项列表">
|
<el-form-item v-if="form.fieldType === 'select'" label="选项列表">
|
||||||
<div class="options-editor">
|
<div class="options-editor">
|
||||||
<div v-for="(opt, idx) in form.options" :key="idx" class="option-row">
|
<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>
|
<el-button size="small" text type="danger" @click="form.options.splice(idx, 1)">×</el-button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<template v-if="form.fieldType === 'string' || form.fieldType === 'textarea'">
|
<template v-if="form.fieldType === 'string' || form.fieldType === 'textarea'">
|
||||||
@@ -204,7 +214,7 @@ interface UploadRule { format: string; maxSize?: number; maxCount?: number; }
|
|||||||
interface FieldFormData {
|
interface FieldFormData {
|
||||||
nodeKey: string; nodeId: string; jsonType: string; fieldType: string;
|
nodeKey: string; nodeId: string; jsonType: string; fieldType: string;
|
||||||
label: string; description: string; role: 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: {
|
constraint: {
|
||||||
minLength?: number; maxLength?: number; pattern?: string;
|
minLength?: number; maxLength?: number; pattern?: string;
|
||||||
min?: number; max?: number; numberType?: string;
|
min?: number; max?: number; numberType?: string;
|
||||||
@@ -231,7 +241,7 @@ const emit = defineEmits<{
|
|||||||
const form = reactive<FieldFormData>({
|
const form = reactive<FieldFormData>({
|
||||||
nodeKey: '', nodeId: '', jsonType: 'string', fieldType: 'string',
|
nodeKey: '', nodeId: '', jsonType: 'string', fieldType: 'string',
|
||||||
label: '', description: '', role: '',
|
label: '', description: '', role: '',
|
||||||
isForm: true, required: false, defaultValue: '', options: [], constraint: {},
|
isForm: true, required: false, loop: false, defaultValue: '', options: [], constraint: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const uploadRules = ref<UploadRule[]>([]);
|
const uploadRules = ref<UploadRule[]>([]);
|
||||||
@@ -251,6 +261,7 @@ function resetForm() {
|
|||||||
form.nodeKey = ''; form.nodeId = ''; form.jsonType = 'string'; form.fieldType = 'string';
|
form.nodeKey = ''; form.nodeId = ''; form.jsonType = 'string'; form.fieldType = 'string';
|
||||||
form.label = ''; form.description = ''; form.role = '';
|
form.label = ''; form.description = ''; form.role = '';
|
||||||
form.isForm = true; form.required = false; form.defaultValue = '';
|
form.isForm = true; form.required = false; form.defaultValue = '';
|
||||||
|
form.loop = false;
|
||||||
form.options = []; form.constraint = {};
|
form.options = []; form.constraint = {};
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
form._createParentId = undefined; form._isArrayParent = false;
|
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.jsonType = data.jsonType || 'string'; form.fieldType = data.fieldType;
|
||||||
form.label = data.label || ''; form.description = data.description || ''; form.role = data.role || '';
|
form.label = data.label || ''; form.description = data.description || ''; form.role = data.role || '';
|
||||||
form.isForm = data.isForm ?? true; form.required = data.required || false;
|
form.isForm = data.isForm ?? true; form.required = data.required || false;
|
||||||
|
form.loop = data.loop ?? false;
|
||||||
form.defaultValue = data.defaultValue || '';
|
form.defaultValue = data.defaultValue || '';
|
||||||
form.options = data.options ? [...data.options] : [];
|
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 } : {};
|
form.constraint = data.constraint ? { ...data.constraint } : {};
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
form._createParentId = data._createParentId; form._isArrayParent = data._isArrayParent ?? false;
|
form._createParentId = data._createParentId; form._isArrayParent = data._isArrayParent ?? false;
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ interface JsonNodeData {
|
|||||||
isForm?: boolean;
|
isForm?: boolean;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
defaultValue?: string | number;
|
defaultValue?: string | number;
|
||||||
options?: string[];
|
options?: Array<{ label: string; value: string }>;
|
||||||
constraint?: Record<string, any>;
|
constraint?: Record<string, any>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,8 +75,9 @@ interface FieldConfig {
|
|||||||
role?: string;
|
role?: string;
|
||||||
isForm?: boolean;
|
isForm?: boolean;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
loop?: boolean;
|
||||||
defaultValue?: string | number;
|
defaultValue?: string | number;
|
||||||
options?: string[];
|
options?: Array<{ label: string; value: string }>;
|
||||||
constraint?: {
|
constraint?: {
|
||||||
minLength?: number; maxLength?: number; pattern?: string;
|
minLength?: number; maxLength?: number; pattern?: string;
|
||||||
min?: number; max?: number; numberType?: string;
|
min?: number; max?: number; numberType?: string;
|
||||||
@@ -100,7 +101,7 @@ interface FieldFormData {
|
|||||||
nodeId: string;
|
nodeId: string;
|
||||||
jsonType: string;
|
jsonType: string;
|
||||||
fieldType: string; label: string; description: string; role: 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: {
|
constraint: {
|
||||||
minLength?: number; maxLength?: number; pattern?: string;
|
minLength?: number; maxLength?: number; pattern?: string;
|
||||||
min?: number; max?: number; numberType?: 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 if (node.type === 'boolean') rawValue = node.primitiveValue === true || node.primitiveValue === 'true';
|
||||||
else rawValue = String(node.primitiveValue);
|
else rawValue = String(node.primitiveValue);
|
||||||
|
|
||||||
if (node.config && hasAnyConfig(node)) {
|
// 容器类型(object/array)始终输出 { type, attrs, ...config }
|
||||||
const dataKey = (node.type === 'object' || node.type === 'array') ? 'attrs' : 'value';
|
if (node.type === 'object' || node.type === 'array') {
|
||||||
return { type: node.type, [dataKey]: rawValue, ...node.config };
|
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 {
|
function defaultFormType(jsonType: string): string {
|
||||||
@@ -274,6 +285,7 @@ function formToConfig(form: FieldFormData): FieldConfig {
|
|||||||
cfg.fieldType = form.fieldType || defaultFormType(form.jsonType);
|
cfg.fieldType = form.fieldType || defaultFormType(form.jsonType);
|
||||||
cfg.isForm = form.isForm ?? true;
|
cfg.isForm = form.isForm ?? true;
|
||||||
cfg.required = form.required ?? false;
|
cfg.required = form.required ?? false;
|
||||||
|
cfg.loop = form.loop ?? false;
|
||||||
// 非骨架字段:有值才发
|
// 非骨架字段:有值才发
|
||||||
if (form.description) cfg.description = form.description;
|
if (form.description) cfg.description = form.description;
|
||||||
if (form.role) cfg.role = form.role;
|
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.label || c.description || c.role || c.defaultValue) return true;
|
||||||
if (c.options?.length) return true;
|
if (c.options?.length) return true;
|
||||||
if (c.required) return true;
|
if (c.required) return true;
|
||||||
|
if (c.loop) return true;
|
||||||
if (c.isForm === false) return true;
|
if (c.isForm === false) return true;
|
||||||
if (c.constraint && Object.keys(c.constraint).length > 0) return true;
|
if (c.constraint && Object.keys(c.constraint).length > 0) return true;
|
||||||
if (c.fieldType && c.fieldType !== defaultFormType(node.type)) return true;
|
if (c.fieldType && c.fieldType !== defaultFormType(node.type)) return true;
|
||||||
@@ -324,6 +337,7 @@ function openFieldDialog(nodeId: string) {
|
|||||||
role: cfg.role || '',
|
role: cfg.role || '',
|
||||||
isForm: cfg.isForm ?? true,
|
isForm: cfg.isForm ?? true,
|
||||||
required: cfg.required ?? false,
|
required: cfg.required ?? false,
|
||||||
|
loop: cfg.loop ?? false,
|
||||||
defaultValue: String(cfg.defaultValue ?? ''),
|
defaultValue: String(cfg.defaultValue ?? ''),
|
||||||
options: cfg.options || [],
|
options: cfg.options || [],
|
||||||
constraint: cfg.constraint ? { ...cfg.constraint } : {},
|
constraint: cfg.constraint ? { ...cfg.constraint } : {},
|
||||||
@@ -355,6 +369,7 @@ function openAddChildDialog(parentNodeId: string) {
|
|||||||
role: '',
|
role: '',
|
||||||
isForm: true,
|
isForm: true,
|
||||||
required: false,
|
required: false,
|
||||||
|
loop: false,
|
||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
options: [],
|
options: [],
|
||||||
constraint: {},
|
constraint: {},
|
||||||
|
|||||||
@@ -128,17 +128,23 @@
|
|||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8" class="mb20">
|
<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8" class="mb20">
|
||||||
<el-form-item label="输入 Token 路径">
|
<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-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8" class="mb20">
|
<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8" class="mb20">
|
||||||
<el-form-item label="输出 Token 路径">
|
<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-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8" class="mb20">
|
<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8" class="mb20">
|
||||||
<el-form-item label="总 Token 路径">
|
<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-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
@@ -379,6 +385,27 @@ watch(isReasoningModel, (val) => {
|
|||||||
if (!val) formData.chatModel = false;
|
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 编辑器弹窗状态 */
|
/** JSON 编辑器弹窗状态 */
|
||||||
const jsonEditorVisible = ref(false);
|
const jsonEditorVisible = ref(false);
|
||||||
const jsonEditorData = ref<Record<string, unknown>>({});
|
const jsonEditorData = ref<Record<string, unknown>>({});
|
||||||
@@ -547,10 +574,9 @@ const handleSubmit = async () => {
|
|||||||
let asyncTaskMapping: AsyncTaskMapping | undefined;
|
let asyncTaskMapping: AsyncTaskMapping | undefined;
|
||||||
let lastFrame: string | undefined;
|
let lastFrame: string | undefined;
|
||||||
|
|
||||||
if (formData.responseType === 3) {
|
// 所有返回类型共用 Token 映射(存的是响应映射中的 key)
|
||||||
const hasTokenConfig = Object.values(formData.tokenMapping).some((v) => v);
|
const hasTokenConfig = Object.values(formData.tokenMapping).some((v) => v);
|
||||||
tokenMapping = hasTokenConfig ? { ...formData.tokenMapping } : undefined;
|
tokenMapping = hasTokenConfig ? { ...formData.tokenMapping } : undefined;
|
||||||
}
|
|
||||||
|
|
||||||
if (formData.responseType === 2) {
|
if (formData.responseType === 2) {
|
||||||
const atm = formData.asyncTaskMapping;
|
const atm = formData.asyncTaskMapping;
|
||||||
|
|||||||
Reference in New Issue
Block a user