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';
|
||||
|
||||
/** 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>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user