更新模型配置,新增字段支持和请求映射功能,优化界面元素,移除冗余代码
This commit is contained in:
@@ -23,13 +23,6 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||
<el-form-item label="运营商名称" prop="operatorName" required>
|
||||
<el-select v-model="state.ruleForm.operatorName" placeholder="请选择运营商" clearable style="width: 100%">
|
||||
<el-option v-for="item in operatorNameOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||
<el-form-item label="模型服务地址" prop="baseUrl">
|
||||
<el-input v-model="state.ruleForm.baseUrl" placeholder="请输入模型服务地址" clearable></el-input>
|
||||
@@ -45,6 +38,9 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="props.isSuperAdmin || state.ruleForm.isPrivate === 1" :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20 provider-section-col">
|
||||
<div class="provider-section-title">服务商模型配置</div>
|
||||
</el-col>
|
||||
<el-col v-if="!props.isSuperAdmin" :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||
<el-form-item label="访问类型" prop="isPrivate">
|
||||
<el-radio-group v-model="state.ruleForm.isPrivate" @change="onIsPrivateChange">
|
||||
@@ -53,6 +49,13 @@
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="props.isSuperAdmin || state.ruleForm.isPrivate === 1" :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||
<el-form-item label="运营商名称" prop="operatorName" required>
|
||||
<el-select v-model="state.ruleForm.operatorName" placeholder="请选择运营商" clearable style="width: 100%">
|
||||
<el-option v-for="item in operatorNameOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="!props.isSuperAdmin && state.ruleForm.isPrivate === 1" :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
|
||||
<el-form-item label="API 密钥" prop="apiKey" required>
|
||||
<el-input v-model="state.ruleForm.apiKey" type="password" show-password placeholder="请输入 API 密钥字符串" clearable></el-input>
|
||||
@@ -103,11 +106,6 @@
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
|
||||
<el-form-item label="备注说明" prop="remark">
|
||||
<el-input v-model="state.ruleForm.remark" type="textarea" placeholder="请输入备注说明" :rows="3" clearable></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 高级配置折叠区域 -->
|
||||
@@ -405,12 +403,21 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 请求映射配置弹窗 -->
|
||||
<el-dialog v-model="showRequestMappingDialog" title="配置请求映射" width="600px" :close-on-click-modal="false">
|
||||
<el-dialog v-model="showRequestMappingDialog" title="配置请求映射" width="860px" :close-on-click-modal="false">
|
||||
<div class="mapping-config-container">
|
||||
<div v-for="(field, index) in state.requestMappingFields" :key="index" class="mapping-field-item">
|
||||
<el-input v-model="field.key" placeholder="请输入字段名 (Key)" style="width: 40%" clearable></el-input>
|
||||
<el-input v-model="field.key" placeholder="请输入字段名 (Key)" style="width: 18%" clearable @input="syncRequestSpecialFieldsOnKeyChange(index)"></el-input>
|
||||
<span class="separator">=</span>
|
||||
<el-input v-model="field.value" placeholder="请输入字段值 (Value)" style="width: 40%" clearable></el-input>
|
||||
<el-input v-model="field.value" placeholder="请输入字段值 (Value)" style="width: 18%" clearable></el-input>
|
||||
<el-button :type="field.required ? 'success' : 'primary'" :plain="!field.required" @click="toggleRequiredField(index)" size="small">
|
||||
{{ field.required ? '✓ 必填字段' : '设置必填字段' }}
|
||||
</el-button>
|
||||
<el-button :type="field.isFirstFrame ? 'success' : 'primary'" :plain="!field.isFirstFrame" @click="setFirstFrameField(index)" size="small">
|
||||
{{ field.isFirstFrame ? '✓ 首帧参数' : '设置首帧参数' }}
|
||||
</el-button>
|
||||
<el-button :type="field.isLastFrame ? 'success' : 'primary'" :plain="!field.isLastFrame" @click="setLastFrameField(index)" size="small">
|
||||
{{ field.isLastFrame ? '✓ 尾帧参数' : '设置尾帧参数' }}
|
||||
</el-button>
|
||||
<el-button type="danger" link @click="removeRequestMappingField(index)">删除</el-button>
|
||||
</div>
|
||||
<el-button type="primary" link @click="addRequestMappingField" style="margin-top: 10px">+ 添加字段</el-button>
|
||||
@@ -436,7 +443,7 @@
|
||||
></el-input>
|
||||
<span class="separator">=</span>
|
||||
<el-input v-model="field.value" placeholder="请输入字段值 (Value)" style="width: 30%" clearable></el-input>
|
||||
<el-button :type="field.isTokenField ? 'warning' : 'primary'" :plain="!field.isTokenField" @click="setTokenField(index)" size="small">
|
||||
<el-button :type="field.isTokenField ? 'success' : 'primary'" :plain="!field.isTokenField" @click="setTokenField(index)" size="small">
|
||||
{{ field.isTokenField ? '✓ 计费字段' : '设置计费字段' }}
|
||||
</el-button>
|
||||
<el-button :type="field.isMainBody ? 'success' : 'primary'" :plain="!field.isMainBody" @click="setMainBody(index)" size="small">
|
||||
@@ -531,6 +538,22 @@ export interface FormField {
|
||||
options?: FormFieldOption[]; // 选项列表
|
||||
}
|
||||
|
||||
export interface KeyValueField {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface RequestMappingField extends KeyValueField {
|
||||
required?: boolean;
|
||||
isFirstFrame?: boolean;
|
||||
isLastFrame?: boolean;
|
||||
}
|
||||
|
||||
export interface ResponseMappingField extends KeyValueField {
|
||||
isMainBody?: boolean;
|
||||
isTokenField?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelTypes?: ModelTypeOption[];
|
||||
@@ -597,6 +620,9 @@ const state = reactive({
|
||||
enabled: 1,
|
||||
isChatModel: 0,
|
||||
callMode: 0,
|
||||
firstFrame: '',
|
||||
lastFrame: '',
|
||||
requiredFields: [] as string[],
|
||||
maxConcurrency: 10,
|
||||
queueLimit: 100,
|
||||
timeoutSeconds: 30,
|
||||
@@ -604,7 +630,6 @@ const state = reactive({
|
||||
retryTimes: 3,
|
||||
retryQueueMaxSeconds: 60,
|
||||
autoCleanSeconds: 300,
|
||||
remark: '',
|
||||
extendMapping: '{}',
|
||||
responseTokenField: '',
|
||||
tokenConfig: '{}',
|
||||
@@ -678,7 +703,22 @@ const state = reactive({
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
operatorName: [{ required: true, message: '请选择运营商名称', trigger: 'change' }],
|
||||
operatorName: [
|
||||
{
|
||||
validator: (_rule: unknown, value: unknown, callback: (e?: Error) => void) => {
|
||||
if (!(props.isSuperAdmin || state.ruleForm.isPrivate === 1)) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (!value || String(value).trim() === '') {
|
||||
callback(new Error('请选择运营商名称'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
apiKey: [
|
||||
{
|
||||
validator: (_rule: unknown, value: unknown, callback: (e?: Error) => void) => {
|
||||
@@ -713,6 +753,11 @@ const state = reactive({
|
||||
callback(new Error('请求映射字段名不能为空'));
|
||||
return;
|
||||
}
|
||||
const duplicatedSpecialSelection = state.requestMappingFields.some((x) => x.isFirstFrame && x.isLastFrame);
|
||||
if (duplicatedSpecialSelection) {
|
||||
callback(new Error('同一行不能同时设置为首帧和尾帧参数'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'change',
|
||||
@@ -777,12 +822,12 @@ const state = reactive({
|
||||
detailLoading: false,
|
||||
},
|
||||
showAdvanced: false,
|
||||
headers: [] as Array<{ key: string; value: string }>,
|
||||
headers: [] as KeyValueField[],
|
||||
formFields: [] as Array<FormField>,
|
||||
requestMappingFields: [] as Array<{ key: string; value: string }>,
|
||||
responseMappingFields: [] as Array<{ key: string; value: string; isMainBody?: boolean; isTokenField?: boolean }>,
|
||||
extendMappingFields: [] as Array<{ key: string; value: string }>,
|
||||
tokenConfigFields: [] as Array<{ key: string; value: string }>,
|
||||
requestMappingFields: [] as RequestMappingField[],
|
||||
responseMappingFields: [] as ResponseMappingField[],
|
||||
extendMappingFields: [] as KeyValueField[],
|
||||
tokenConfigFields: [] as KeyValueField[],
|
||||
mainBodyIndex: -1, // 记录哪一行被设置为返回主体
|
||||
});
|
||||
|
||||
@@ -806,7 +851,7 @@ const createEmptyFormField = (): FormField => {
|
||||
};
|
||||
|
||||
// 将数组转换为对象
|
||||
const fieldsToObject = (fields: Array<{ key: string; value: string }>) => {
|
||||
const fieldsToObject = (fields: KeyValueField[]) => {
|
||||
const obj: Record<string, string> = {};
|
||||
fields.forEach((f) => {
|
||||
if (f.key && f.key.trim()) {
|
||||
@@ -820,7 +865,7 @@ const fieldsToObject = (fields: Array<{ key: string; value: string }>) => {
|
||||
// 1. 旧格式:逗号分隔的"key1:value1,key2:value2"字符串
|
||||
// 2. JSON字符串:格式化为JSON对象的字符串
|
||||
// 3. 新格式:直接是Record<string, string>对象
|
||||
const parseHeaders = (raw: unknown): Array<{ key: string; value: string }> => {
|
||||
const parseHeaders = (raw: unknown): KeyValueField[] => {
|
||||
if (!raw) return [];
|
||||
|
||||
// 如果是字符串,先尝试解析为JSON,如果失败则尝试解析为旧格式key:value,key2:value2
|
||||
@@ -1051,11 +1096,78 @@ const confirmFormFields = () => {
|
||||
};
|
||||
// 请求映射字段操作
|
||||
const addRequestMappingField = () => {
|
||||
state.requestMappingFields.push({ key: '', value: '' });
|
||||
state.requestMappingFields.push({ key: '', value: '', required: false, isFirstFrame: false, isLastFrame: false });
|
||||
};
|
||||
|
||||
const removeRequestMappingField = (index: number) => {
|
||||
const removed = state.requestMappingFields[index];
|
||||
state.requestMappingFields.splice(index, 1);
|
||||
if (removed?.required) {
|
||||
state.ruleForm.requiredFields = state.ruleForm.requiredFields.filter((item) => item !== String(removed.key || '').trim());
|
||||
}
|
||||
if (removed?.isFirstFrame) {
|
||||
state.ruleForm.firstFrame = '';
|
||||
}
|
||||
if (removed?.isLastFrame) {
|
||||
state.ruleForm.lastFrame = '';
|
||||
}
|
||||
};
|
||||
|
||||
const toggleRequiredField = (index: number) => {
|
||||
const row = state.requestMappingFields[index];
|
||||
if (!row) return;
|
||||
row.required = !row.required;
|
||||
state.ruleForm.requiredFields = state.requestMappingFields
|
||||
.filter((field) => field.required)
|
||||
.map((field) => String(field.key || '').trim())
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
const setFirstFrameField = (index: number) => {
|
||||
state.requestMappingFields.forEach((field, i) => {
|
||||
field.isFirstFrame = i === index;
|
||||
if (i === index) {
|
||||
field.isLastFrame = false;
|
||||
}
|
||||
});
|
||||
state.ruleForm.firstFrame = state.requestMappingFields[index]?.key?.trim?.() || '';
|
||||
if (state.requestMappingFields[index]?.isLastFrame) {
|
||||
state.ruleForm.lastFrame = '';
|
||||
}
|
||||
};
|
||||
|
||||
const setLastFrameField = (index: number) => {
|
||||
state.requestMappingFields.forEach((field, i) => {
|
||||
field.isLastFrame = i === index;
|
||||
if (i === index) {
|
||||
field.isFirstFrame = false;
|
||||
}
|
||||
});
|
||||
state.ruleForm.lastFrame = state.requestMappingFields[index]?.key?.trim?.() || '';
|
||||
if (state.requestMappingFields[index]?.isFirstFrame) {
|
||||
state.ruleForm.firstFrame = '';
|
||||
}
|
||||
};
|
||||
|
||||
const syncRequestSpecialFieldsOnKeyChange = (index: number) => {
|
||||
const row = state.requestMappingFields[index];
|
||||
if (!row) return;
|
||||
if (row.required) {
|
||||
const nextKey = row.key?.trim?.() || '';
|
||||
state.ruleForm.requiredFields = state.requestMappingFields
|
||||
.filter((field) => field.required)
|
||||
.map((field) => String(field.key || '').trim())
|
||||
.filter(Boolean);
|
||||
if (!nextKey) {
|
||||
row.required = false;
|
||||
}
|
||||
}
|
||||
if (row.isFirstFrame) {
|
||||
state.ruleForm.firstFrame = row.key?.trim?.() || '';
|
||||
}
|
||||
if (row.isLastFrame) {
|
||||
state.ruleForm.lastFrame = row.key?.trim?.() || '';
|
||||
}
|
||||
};
|
||||
|
||||
const confirmRequestMappingFields = () => {
|
||||
@@ -1101,7 +1213,7 @@ const confirmResponseMappingFields = () => {
|
||||
showResponseMappingDialog.value = false;
|
||||
};
|
||||
|
||||
const ensureKeyValueRows = (rows: Array<{ key: string; value: string }>) => (rows.length ? rows : [{ key: '', value: '' }]);
|
||||
const ensureKeyValueRows = <T extends KeyValueField>(rows: T[], createEmpty: () => T): T[] => (rows.length ? rows : [createEmpty()]);
|
||||
|
||||
// 解析旧格式的form数据兼容到新格式
|
||||
const parseFormFieldsUnified = (raw: unknown): Array<FormField> => {
|
||||
@@ -1182,6 +1294,9 @@ const fillFormFromDetailRow = (row: Record<string, unknown>) => {
|
||||
enabled: Number(row.enabled ?? 1),
|
||||
isChatModel: row.isChatModel !== undefined && row.isChatModel !== null ? Number(row.isChatModel) : 0,
|
||||
callMode: row.callMode !== undefined && row.callMode !== null ? Number(row.callMode) : row.isAsync !== undefined && row.isAsync !== null ? Number(row.isAsync) : 0,
|
||||
firstFrame: String(row.firstFrame || ''),
|
||||
lastFrame: String(row.lastFrame || ''),
|
||||
requiredFields: Array.isArray(row.requiredFields) ? row.requiredFields.map((item) => String(item || '').trim()).filter(Boolean) : [],
|
||||
maxConcurrency: Number(row.maxConcurrency ?? 10),
|
||||
queueLimit: Number(row.queueLimit ?? 100),
|
||||
timeoutSeconds,
|
||||
@@ -1189,17 +1304,33 @@ const fillFormFromDetailRow = (row: Record<string, unknown>) => {
|
||||
retryTimes: Number(row.retryTimes ?? 3),
|
||||
retryQueueMaxSeconds: Number(row.retryQueueMaxSeconds ?? 60),
|
||||
autoCleanSeconds: Number(row.autoCleanSeconds ?? 300),
|
||||
remark: String(row.remark || ''),
|
||||
extendMapping: '{}',
|
||||
responseTokenField: String(row.responseTokenField || ''),
|
||||
tokenConfig: '{}',
|
||||
};
|
||||
state.headers = ensureKeyValueRows(parseHeaders(row.headMsg));
|
||||
state.headers = ensureKeyValueRows(parseHeaders(row.headMsg), () => ({ key: '', value: '' }));
|
||||
state.formFields = parseFormFieldsUnified(row.form);
|
||||
state.requestMappingFields = ensureKeyValueRows(parseRequestMappingFields(row.requestMapping));
|
||||
state.responseMappingFields = ensureKeyValueRows(parseResponseMappingFields(row.responseMapping));
|
||||
state.extendMappingFields = ensureKeyValueRows(parseFieldsUnified(row.extendMapping));
|
||||
state.tokenConfigFields = ensureKeyValueRows(parseFieldsUnified(row.tokenConfig));
|
||||
state.requestMappingFields = ensureKeyValueRows(parseRequestMappingFields(row.requestMapping) as RequestMappingField[], () => ({
|
||||
key: '',
|
||||
value: '',
|
||||
required: false,
|
||||
isFirstFrame: false,
|
||||
isLastFrame: false,
|
||||
}));
|
||||
state.requestMappingFields.forEach((field) => {
|
||||
const fieldKey = String(field.key || '').trim();
|
||||
field.required = fieldKey !== '' && state.ruleForm.requiredFields.includes(fieldKey);
|
||||
field.isFirstFrame = fieldKey !== '' && fieldKey === String(row.firstFrame || '').trim();
|
||||
field.isLastFrame = fieldKey !== '' && fieldKey === String(row.lastFrame || '').trim();
|
||||
});
|
||||
state.responseMappingFields = ensureKeyValueRows(parseResponseMappingFields(row.responseMapping) as ResponseMappingField[], () => ({
|
||||
key: '',
|
||||
value: '',
|
||||
isMainBody: false,
|
||||
isTokenField: false,
|
||||
}));
|
||||
state.extendMappingFields = ensureKeyValueRows(parseFieldsUnified(row.extendMapping), () => ({ key: '', value: '' }));
|
||||
state.tokenConfigFields = ensureKeyValueRows(parseFieldsUnified(row.tokenConfig), () => ({ key: '', value: '' }));
|
||||
|
||||
// 根据 responseTokenField 字段设置计费字段标记(单选)
|
||||
const tokenFieldKey = String(row.responseTokenField || '').trim();
|
||||
@@ -1230,7 +1361,7 @@ const fillFormFromDetailRow = (row: Record<string, unknown>) => {
|
||||
asyncQueryConfigForm.resultPath = String(qc.result_path || '');
|
||||
asyncQueryConfigForm.statusPath = String(qc.status_path || '');
|
||||
asyncQueryConfigForm.intervalSeconds = Number(qc.interval_seconds ?? 2) || 2;
|
||||
asyncQueryConfigForm.statusValueFields = ensureKeyValueRows(objectToFields((qc.status_values as Record<string, unknown>) || {}));
|
||||
asyncQueryConfigForm.statusValueFields = ensureKeyValueRows(objectToFields((qc.status_values as Record<string, unknown>) || {}), () => ({ key: '', value: '' }));
|
||||
} else {
|
||||
asyncQueryConfigForm.url = '';
|
||||
asyncQueryConfigForm.method = 'POST';
|
||||
@@ -1292,6 +1423,9 @@ const openDialog = async (type: string, row?: Record<string, unknown>) => {
|
||||
enabled: 1,
|
||||
isChatModel: 0,
|
||||
callMode: 0,
|
||||
firstFrame: '',
|
||||
lastFrame: '',
|
||||
requiredFields: [],
|
||||
maxConcurrency: 10,
|
||||
queueLimit: 100,
|
||||
timeoutSeconds: 30,
|
||||
@@ -1299,14 +1433,13 @@ const openDialog = async (type: string, row?: Record<string, unknown>) => {
|
||||
retryTimes: 3,
|
||||
retryQueueMaxSeconds: 60,
|
||||
autoCleanSeconds: 300,
|
||||
remark: '',
|
||||
extendMapping: '{}',
|
||||
responseTokenField: '',
|
||||
tokenConfig: '{}',
|
||||
};
|
||||
state.headers = [{ key: '', value: '' }];
|
||||
state.formFields = [createEmptyFormField()];
|
||||
state.requestMappingFields = [{ key: '', value: '' }];
|
||||
state.requestMappingFields = [{ key: '', value: '', required: false, isFirstFrame: false, isLastFrame: false }];
|
||||
state.responseMappingFields = [{ key: '', value: '', isMainBody: false, isTokenField: false }];
|
||||
state.mainBodyIndex = -1;
|
||||
state.extendMappingFields = [{ key: '', value: '' }];
|
||||
@@ -1363,6 +1496,10 @@ const onSubmit = () => {
|
||||
|
||||
// 过滤掉空键名的字段
|
||||
const requestMapping = fieldsToObject(state.requestMappingFields.filter((f) => String(f.key || '').trim() !== ''));
|
||||
const requiredFields = state.requestMappingFields
|
||||
.filter((f) => Boolean(f.required) && String(f.key || '').trim() !== '')
|
||||
.map((f) => String(f.key || '').trim());
|
||||
state.ruleForm.requiredFields = requiredFields;
|
||||
const responseMapping = fieldsToObject(state.responseMappingFields.filter((f) => String(f.key || '').trim() !== ''));
|
||||
|
||||
// 获取被设置为返回主体的字段 {key: value}
|
||||
@@ -1419,6 +1556,9 @@ const onSubmit = () => {
|
||||
apiKey: state.ruleForm.isPrivate === 1 ? String(state.ruleForm.apiKey ?? '').trim() : '',
|
||||
form: processedFormFields,
|
||||
requestMapping,
|
||||
requiredFields,
|
||||
firstFrame: String(state.ruleForm.firstFrame || '').trim(),
|
||||
lastFrame: String(state.ruleForm.lastFrame || '').trim(),
|
||||
responseMapping,
|
||||
responseBody,
|
||||
maxConcurrency: state.ruleForm.maxConcurrency,
|
||||
@@ -1428,7 +1568,6 @@ const onSubmit = () => {
|
||||
retryTimes: state.ruleForm.retryTimes,
|
||||
retryQueueMaxSeconds: state.ruleForm.retryQueueMaxSeconds,
|
||||
autoCleanSeconds: state.ruleForm.autoCleanSeconds,
|
||||
remark: state.ruleForm.remark || '',
|
||||
extendMapping: fieldsToUnknownObject(state.extendMappingFields.filter((f) => String(f.key || '').trim() !== '')),
|
||||
responseTokenField,
|
||||
tokenConfig: fieldsToUnknownObject(state.tokenConfigFields.filter((f) => String(f.key || '').trim() !== '')),
|
||||
@@ -1482,6 +1621,17 @@ onMounted(() => {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.provider-section-col {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.provider-section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
padding: 2px 0 4px;
|
||||
}
|
||||
|
||||
.form-config-container {
|
||||
max-height: 550px;
|
||||
overflow-y: auto;
|
||||
|
||||
Reference in New Issue
Block a user