Files
admin-ui/src/views/settings/modelConfig/modelModule/component/editModule.vue
2910410219 5152121c33 fix: 更新开发环境 API 地址并增强 editModule.vue 表单验证
- 在 editModule.vue 中添加响应字段和请求映射的验证逻辑,确保用户输入有效性,提升表单提交的可靠性。
2026-05-26 10:11:21 +08:00

1307 lines
51 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="system-edit-module-container">
<el-dialog :title="state.dialog.title" v-model="state.dialog.isShowDialog" width="900px">
<el-form
ref="editModuleFormRef"
v-loading="state.dialog.detailLoading"
:model="state.ruleForm"
:rules="state.rules"
size="default"
label-width="140px"
>
<!-- 基础配置 -->
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="模型名称" prop="modelName">
<el-input v-model="state.ruleForm.modelName" placeholder="请输入模型名称" clearable></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="模型类型" prop="modelType" required>
<el-select v-model="state.ruleForm.modelType" placeholder="请选择模型类型" clearable style="width: 100%">
<el-option v-for="t in modelTypeOptions" :key="String(t.id)" :label="t.label" :value="typeOptionValue(t.id)"></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="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>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="请求方式" prop="httpMethod">
<el-select v-model="state.ruleForm.httpMethod" placeholder="请选择请求方式" clearable style="width: 100%">
<el-option label="GET" value="GET"></el-option>
<el-option label="POST" value="POST"></el-option>
<el-option label="PUT" value="PUT"></el-option>
<el-option label="DELETE" value="DELETE"></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="queryResponseType">
<el-select v-model="state.ruleForm.queryResponseType" placeholder="请选择响应类型" clearable style="width: 100%">
<el-option v-for="item in queryResponseTypeOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col v-if="state.ruleForm.queryResponseType === 'callback'" :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
<el-form-item label="回调地址" prop="queryCallbackUrl">
<el-input v-model="state.ruleForm.queryCallbackUrl" placeholder="请输入回调地址" clearable></el-input>
</el-form-item>
</el-col>
<el-col v-if="state.ruleForm.queryResponseType === 'pull'" :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="主动拉取配置" prop="queryPullConfig">
<el-button @click="showPullConfigDialog = true" style="width: 100%">配置主动拉取</el-button>
</el-form-item>
</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">
<el-radio :label="0">本地模型</el-radio>
<el-radio :label="1">服务商模型</el-radio>
</el-radio-group>
</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>
</el-form-item>
</el-col>
<el-col v-if="!props.isSuperAdmin && state.dialog.type === 'add'" :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="是否对话模型" prop="isChatModel">
<el-radio-group v-model="state.ruleForm.isChatModel">
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="请求头绑定" prop="headMsg">
<el-button @click="showHeaderDialog = true" style="width: 100%"> 配置请求头 ({{ state.headers.length }}) </el-button>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="自定义字段" prop="form">
<el-button @click="showFormDialog = true" style="width: 100%"> 配置表单字段 ({{ state.formFields.length }}) </el-button>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="是否启用" prop="enabled">
<el-radio-group v-model="state.ruleForm.enabled">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</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>
<!-- 高级配置折叠区域 -->
<el-divider content-position="left">
<el-button link type="primary" @click="state.showAdvanced = !state.showAdvanced">
{{ state.showAdvanced ? '收起高级配置' : '展开高级配置' }}
<el-icon class="ml5">
<component :is="state.showAdvanced ? 'ArrowUp' : 'ArrowDown'" />
</el-icon>
</el-button>
</el-divider>
<el-collapse-transition>
<el-row v-show="state.showAdvanced" :gutter="20">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="最大并发数" prop="maxConcurrency">
<el-input-number v-model="state.ruleForm.maxConcurrency" :min="1" :max="1000" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="排队队列上限" prop="queueLimit">
<el-input-number v-model="state.ruleForm.queueLimit" :min="1" :max="10000" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="请求超时时间(秒)" prop="timeoutSeconds">
<el-input-number v-model="state.ruleForm.timeoutSeconds" :min="1" :max="3600" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="预计执行时间(秒)" prop="expectedSeconds">
<el-input-number v-model="state.ruleForm.expectedSeconds" :min="1" :max="3600" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="失败重试次数" prop="retryTimes">
<el-input-number v-model="state.ruleForm.retryTimes" :min="0" :max="10" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="重试最大排队时间" prop="retryQueueMaxSeconds">
<el-input-number v-model="state.ruleForm.retryQueueMaxSeconds" :min="0" :max="3600" style="width: 100%"></el-input-number>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="自动清理间隔(秒)" prop="autoCleanSeconds">
<el-input-number v-model="state.ruleForm.autoCleanSeconds" :min="0" :max="86400" style="width: 100%"> </el-input-number>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="请求映射" prop="requestMapping">
<el-button @click="showRequestMappingDialog = true" style="width: 100%">
配置请求映射 ({{ state.requestMappingFields.length }})
</el-button>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="响应映射" prop="responseMapping">
<el-button @click="showResponseMappingDialog = true" style="width: 100%">
配置响应映射 ({{ state.responseMappingFields.length }})
</el-button>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="附加映射" prop="extendMapping">
<el-button @click="showExtendMappingDialog = true" style="width: 100%"
>配置附加映射 ({{ state.extendMappingFields.length }})</el-button
>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="Token计算配置" prop="tokenConfig">
<el-button @click="showTokenConfigDialog = true" style="width: 100%">配置Token计算 ({{ state.tokenConfigFields.length }})</el-button>
</el-form-item>
</el-col>
</el-row>
</el-collapse-transition>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancel" size="default"> </el-button>
<el-button type="primary" @click="onSubmit" size="default" :loading="state.dialog.loading" :disabled="state.dialog.detailLoading">
{{ state.dialog.submitTxt }}
</el-button>
</span>
</template>
</el-dialog>
<!-- 请求头配置弹窗 -->
<el-dialog v-model="showHeaderDialog" title="配置请求头" width="600px" :close-on-click-modal="false">
<div class="header-config-container">
<div v-for="(header, index) in state.headers" :key="index" class="header-item">
<el-input v-model="header.key" placeholder="请输入 Key" style="width: 40%" clearable></el-input>
<span class="separator">:</span>
<el-input v-model="header.value" placeholder="请输入 Value" style="width: 40%" clearable></el-input>
<el-button type="danger" link @click="removeHeader(index)">删除</el-button>
</div>
<el-button type="primary" link @click="addHeader" style="margin-top: 10px">+ 添加请求头</el-button>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="showHeaderDialog = false" size="default"> </el-button>
<el-button type="primary" @click="confirmHeaders" size="default"> </el-button>
</span>
</template>
</el-dialog>
<!-- 自定义字段配置弹窗 -->
<el-dialog v-model="showFormDialog" title="配置表单字段" width="600px" :close-on-click-modal="false">
<div class="form-config-container">
<div v-for="(field, index) in state.formFields" :key="index" class="form-field-item">
<el-input v-model="field.key" placeholder="请输入字段名 (Key)" style="width: 40%" clearable></el-input>
<span class="separator">=</span>
<el-input v-model="field.value" placeholder="请输入字段值 (Value)" style="width: 40%" clearable></el-input>
<el-button type="danger" link @click="removeFormField(index)">删除</el-button>
</div>
<el-button type="primary" link @click="addFormField" style="margin-top: 10px">+ 添加字段</el-button>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="showFormDialog = false" size="default"> </el-button>
<el-button type="primary" @click="confirmFormFields" size="default"> </el-button>
</span>
</template>
</el-dialog>
<!-- 请求映射配置弹窗 -->
<el-dialog v-model="showRequestMappingDialog" title="配置请求映射" width="600px" :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>
<span class="separator">=</span>
<el-input v-model="field.value" placeholder="请输入字段值 (Value)" style="width: 40%" clearable></el-input>
<el-button type="danger" link @click="removeRequestMappingField(index)">删除</el-button>
</div>
<el-button type="primary" link @click="addRequestMappingField" style="margin-top: 10px">+ 添加字段</el-button>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="showRequestMappingDialog = false" size="default"> </el-button>
<el-button type="primary" @click="confirmRequestMappingFields" size="default"> </el-button>
</span>
</template>
</el-dialog>
<!-- 响应映射配置弹窗 -->
<el-dialog v-model="showResponseMappingDialog" title="配置响应映射" width="700px" :close-on-click-modal="false">
<div class="mapping-config-container">
<div v-for="(field, index) in state.responseMappingFields" :key="index" class="mapping-field-item">
<el-input
v-model="field.key"
placeholder="请输入字段名 (Key)"
style="width: 30%"
clearable
@input="syncTokenFieldOnKeyChange(index)"
></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">
{{ field.isTokenField ? '✓ 计费字段' : '设置计费字段' }}
</el-button>
<el-button :type="field.isMainBody ? 'success' : 'primary'" :plain="!field.isMainBody" @click="setMainBody(index)" size="small">
{{ field.isMainBody ? '✓ 返回主体' : '设置返回主体' }}
</el-button>
<el-button type="danger" link @click="removeResponseMappingField(index)">删除</el-button>
</div>
<el-button type="primary" link @click="addResponseMappingField" style="margin-top: 10px">+ 添加字段</el-button>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="showResponseMappingDialog = false" size="default"> </el-button>
<el-button type="primary" @click="confirmResponseMappingFields" size="default"> </el-button>
</span>
</template>
</el-dialog>
<!-- 主动拉取配置弹窗 -->
<el-dialog v-model="showPullConfigDialog" title="配置主动拉取" width="800px" :close-on-click-modal="false">
<el-form label-width="120px">
<el-form-item label="请求方式">
<el-select v-model="pullConfigForm.method" style="width: 100%">
<el-option label="GET" value="GET"></el-option>
<el-option label="POST" value="POST"></el-option>
<el-option label="PUT" value="PUT"></el-option>
<el-option label="DELETE" value="DELETE"></el-option>
</el-select>
</el-form-item>
<el-form-item label="拉取地址">
<el-input v-model="pullConfigForm.url" placeholder="请输入拉取 URL" clearable></el-input>
</el-form-item>
<el-form-item label="请求头(headers)">
<el-button @click="showPullHeadersDialog = true" style="width: 100%">配置请求头 ({{ pullConfigForm.headersFields.length }})</el-button>
</el-form-item>
<el-form-item label="请求体(body)">
<el-button @click="showPullBodyDialog = true" style="width: 100%">配置请求体 ({{ pullConfigForm.bodyFields.length }})</el-button>
</el-form-item>
<el-form-item label="响应映射(response)">
<el-button @click="showPullResponseDialog = true" style="width: 100%">配置响应字段 ({{ pullConfigForm.responseFields.length }})</el-button>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showPullConfigDialog = false" size="default"> </el-button>
<el-button type="primary" @click="showPullConfigDialog = false" size="default"> </el-button>
</span>
</template>
</el-dialog>
<!-- 附加映射配置 -->
<el-dialog v-model="showExtendMappingDialog" title="配置附加映射" width="600px" :close-on-click-modal="false">
<div class="mapping-config-container">
<div v-for="(field, index) in state.extendMappingFields" :key="index" class="mapping-field-item">
<el-input v-model="field.key" placeholder="请输入字段名 (Key)" style="width: 40%" clearable></el-input>
<span class="separator">=</span>
<el-input v-model="field.value" placeholder="请输入字段值 (Value)" style="width: 40%" clearable></el-input>
<el-button type="danger" link @click="state.extendMappingFields.splice(index, 1)">删除</el-button>
</div>
<el-button type="primary" link @click="state.extendMappingFields.push({ key: '', value: '' })" style="margin-top: 10px">+ 添加字段</el-button>
</div>
<template #footer
><span class="dialog-footer"><el-button @click="showExtendMappingDialog = false"> </el-button></span></template
>
</el-dialog>
<!-- Token计算配置 -->
<el-dialog v-model="showTokenConfigDialog" title="配置Token计算" width="600px" :close-on-click-modal="false">
<div class="mapping-config-container">
<div v-for="(field, index) in state.tokenConfigFields" :key="index" class="mapping-field-item">
<el-input v-model="field.key" placeholder="请输入字段名 (Key)" style="width: 40%" clearable></el-input>
<span class="separator">=</span>
<el-input v-model="field.value" placeholder="请输入字段值 (Value)" style="width: 40%" clearable></el-input>
<el-button type="danger" link @click="state.tokenConfigFields.splice(index, 1)">删除</el-button>
</div>
<el-button type="primary" link @click="state.tokenConfigFields.push({ key: '', value: '' })" style="margin-top: 10px">+ 添加字段</el-button>
</div>
<template #footer
><span class="dialog-footer"><el-button @click="showTokenConfigDialog = false"> </el-button></span></template
>
</el-dialog>
<!-- Pull headers -->
<el-dialog v-model="showPullHeadersDialog" title="配置请求头(headers)" width="600px" :close-on-click-modal="false">
<div class="mapping-config-container">
<div v-for="(field, index) in pullConfigForm.headersFields" :key="index" class="mapping-field-item">
<el-input v-model="field.key" placeholder="请输入字段名 (Key)" style="width: 40%" clearable></el-input>
<span class="separator">=</span>
<el-input v-model="field.value" placeholder="请输入字段值 (Value)" style="width: 40%" clearable></el-input>
<el-button type="danger" link @click="pullConfigForm.headersFields.splice(index, 1)">删除</el-button>
</div>
<el-button type="primary" link @click="pullConfigForm.headersFields.push({ key: '', value: '' })" style="margin-top: 10px"
>+ 添加字段</el-button
>
</div>
<template #footer
><span class="dialog-footer"><el-button @click="showPullHeadersDialog = false"> </el-button></span></template
>
</el-dialog>
<!-- Pull body -->
<el-dialog v-model="showPullBodyDialog" title="配置请求体(body)" width="600px" :close-on-click-modal="false">
<div class="mapping-config-container">
<div v-for="(field, index) in pullConfigForm.bodyFields" :key="index" class="mapping-field-item">
<el-input v-model="field.key" placeholder="请输入字段路径 (如 task_id.id)" style="width: 40%" clearable></el-input>
<span class="separator">=</span>
<el-select v-model="field.value" placeholder="请输入或选择字段值" style="width: 40%" clearable filterable allow-create default-first-option>
<el-option v-for="(item, idx) in responseMappingValueOptions" :key="idx" :label="item" :value="item"> </el-option>
</el-select>
<el-button type="danger" link @click="pullConfigForm.bodyFields.splice(index, 1)">删除</el-button>
</div>
<el-button type="primary" link @click="pullConfigForm.bodyFields.push({ key: '', value: '' })" style="margin-top: 10px">+ 添加字段</el-button>
</div>
<template #footer
><span class="dialog-footer"><el-button @click="showPullBodyDialog = false"> </el-button></span></template
>
</el-dialog>
<!-- Pull response -->
<el-dialog v-model="showPullResponseDialog" title="配置响应映射(response)" width="600px" :close-on-click-modal="false">
<div class="mapping-config-container">
<div v-for="(field, index) in pullConfigForm.responseFields" :key="index" class="mapping-field-item">
<el-input v-model="field.value" placeholder="请输入响应字段路径 (如 content.video_url)" style="width: 30%" clearable></el-input>
<span class="separator">=</span>
<el-button :type="field.isTokenField ? 'warning' : 'primary'" :plain="!field.isTokenField" @click="setPullTokenField(index)" size="small">
{{ field.isTokenField ? '✓ 计费字段' : '设置计费字段' }}
</el-button>
<el-button :type="field.isMainBody ? 'success' : 'primary'" :plain="!field.isMainBody" @click="setPullMainBody(index)" size="small">
{{ field.isMainBody ? '✓ 返回主体' : '设置返回主体' }}
</el-button>
<el-button type="danger" link @click="pullConfigForm.responseFields.splice(index, 1)">删除</el-button>
</div>
<el-button
type="primary"
link
@click="pullConfigForm.responseFields.push({ value: '', isTokenField: false, isMainBody: false })"
style="margin-top: 10px"
>
+ 添加字段
</el-button>
</div>
<template #footer
><span class="dialog-footer"><el-button @click="showPullResponseDialog = false"> </el-button></span></template
>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="systemEditModule">
import { reactive, ref, computed, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { ArrowUp, ArrowDown } from '@element-plus/icons-vue';
import {
addModelModule,
updateModelModule,
getModelModuleDetail,
getOperatorList,
type ModelFormEntry,
type CreateModelParams,
} from '/@/api/settings/modelConfig/modelModule/index';
export type ModelTypeOption = { id: number | string; label: string };
const props = withDefaults(
defineProps<{
modelTypes?: ModelTypeOption[];
isSuperAdmin?: boolean;
}>(),
{
modelTypes: () => [] as ModelTypeOption[],
isSuperAdmin: false,
}
);
const modelTypeOptions = computed(() => props.modelTypes);
const responseMappingValueOptions = computed(() => {
return state.responseMappingFields.map((f) => String(f.value || '').trim()).filter((v) => v !== '');
});
const operatorNameOptions = ref<Array<{ label: string; value: string }>>([]);
const loadOperatorOptions = async () => {
try {
const res: any = await getOperatorList();
const list = res?.data?.list;
operatorNameOptions.value = Array.isArray(list)
? list.filter((item: unknown) => typeof item === 'string' && item.trim() !== '').map((name: string) => ({ label: name, value: name }))
: [];
} catch {
operatorNameOptions.value = [];
}
};
const typeOptionValue = (id: number | string): number | string => {
const n = Number(id);
return Number.isNaN(n) ? id : n;
};
const editModuleFormRef = ref();
const emit = defineEmits(['refresh']);
const showHeaderDialog = ref(false);
const showFormDialog = ref(false);
const showRequestMappingDialog = ref(false);
const showResponseMappingDialog = ref(false);
const showPullConfigDialog = ref(false);
const showExtendMappingDialog = ref(false);
const showTokenConfigDialog = ref(false);
const showPullHeadersDialog = ref(false);
const showPullBodyDialog = ref(false);
const showPullResponseDialog = ref(false);
const pullConfigForm = reactive({
method: 'GET',
url: 'https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks',
headersFields: [
{ key: 'Content-Type', value: 'application/json' },
{ key: 'Authorization', value: 'Bearer ' },
] as Array<{ key: string; value: string }>,
bodyFields: [{ key: 'task_id.id', value: '111' }] as Array<{ key: string; value: string }>,
responseFields: [
{ value: 'status', isTokenField: false, isMainBody: false },
{ value: 'content.video_url', isTokenField: false, isMainBody: false },
{ value: 'usage.completion_tokens', isTokenField: false, isMainBody: false },
] as Array<{ value: string; isTokenField?: boolean; isMainBody?: boolean }>,
});
const queryResponseTypeOptions = [
{ label: '同步返回', value: 'sync' },
{ label: '等候回调', value: 'callback' },
{ label: '主动拉取', value: 'pull' },
];
const state = reactive({
ruleForm: {
id: '',
modelName: '',
modelType: null as number | string | null,
operatorName: '',
baseUrl: '',
httpMethod: 'POST',
queryResponseType: 'sync',
queryCallbackUrl: '',
headMsg: '',
isPrivate: 0,
apiKey: '',
enabled: 1,
isChatModel: 0,
maxConcurrency: 10,
queueLimit: 100,
timeoutSeconds: 30,
expectedSeconds: 15,
retryTimes: 3,
retryQueueMaxSeconds: 60,
autoCleanSeconds: 300,
remark: '',
extendMapping: '{}',
responseTokenField: '',
tokenConfig: '{}',
},
rules: {
modelName: [{ required: true, message: '请输入模型名称', trigger: 'blur' }],
modelType: [
{
validator: (_rule: unknown, value: unknown, callback: (e?: Error) => void) => {
if (value === null || value === undefined || value === '') {
callback(new Error('请选择模型类型'));
} else {
callback();
}
},
trigger: 'change',
},
],
baseUrl: [{ required: true, message: '请输入模型服务地址', trigger: 'blur' }],
httpMethod: [{ required: true, message: '请选择请求方式', trigger: 'change' }],
queryResponseType: [{ required: true, message: '请选择响应类型', trigger: 'change' }],
queryCallbackUrl: [
{
validator: (_rule: unknown, value: unknown, callback: (e?: Error) => void) => {
if (state.ruleForm.queryResponseType !== 'callback') {
callback();
return;
}
if (!value || String(value).trim() === '') {
callback(new Error('请选择等候回调时,回调地址必填'));
return;
}
callback();
},
trigger: 'blur',
},
],
queryPullConfig: [
{
validator: (_rule: unknown, _value: unknown, callback: (e?: Error) => void) => {
if (state.ruleForm.queryResponseType !== 'pull') {
callback();
return;
}
if (!pullConfigForm.url || String(pullConfigForm.url).trim() === '') {
callback(new Error('主动拉取时,请填写拉取地址'));
return;
}
// 验证响应字段至少有一个有效字段
const validResponseFields = pullConfigForm.responseFields.filter((f) => String(f.value || '').trim() !== '');
if (validResponseFields.length === 0) {
callback(new Error('主动拉取时,至少需要配置一个响应字段'));
return;
}
// 验证是否设置了返回主体字段
const hasMainBody = pullConfigForm.responseFields.some((f) => f.isMainBody && String(f.value || '').trim() !== '');
if (!hasMainBody) {
callback(new Error('主动拉取时,必须设置一个返回主体字段'));
return;
}
callback();
},
trigger: 'change',
},
],
operatorName: [{ required: true, message: '请选择运营商名称', trigger: 'change' }],
apiKey: [
{
validator: (_rule: unknown, value: unknown, callback: (e?: Error) => void) => {
// 管理员不需要验证 apiKey
if (props.isSuperAdmin) {
callback();
return;
}
// 普通用户如果是服务商模型isPrivate = 1apiKey 必填
if (state.ruleForm.isPrivate === 1) {
if (!value || String(value).trim() === '') {
callback(new Error('服务商模型必须输入 API 密钥'));
} else {
callback();
}
} else {
callback();
}
},
trigger: 'blur',
},
],
maxConcurrency: [{ required: true, message: '请输入最大并发数', trigger: 'blur' }],
queueLimit: [{ required: true, message: '请输入排队队列上限', trigger: 'blur' }],
timeoutSeconds: [{ required: true, message: '请输入请求超时时间', trigger: 'blur' }],
expectedSeconds: [{ required: true, message: '请输入预计执行时间', trigger: 'blur' }],
requestMapping: [
{
validator: (_rule: unknown, _value: unknown, callback: (e?: Error) => void) => {
const emptyKeys = state.requestMappingFields.filter((x) => String(x.key || '').trim() === '' && String(x.value || '').trim() !== '');
if (emptyKeys.length > 0) {
callback(new Error('请求映射字段名不能为空'));
return;
}
callback();
},
trigger: 'change',
},
],
responseMapping: [
{
validator: (_rule: unknown, _value: unknown, callback: (e?: Error) => void) => {
// 检查是否有空键名但有值的字段
const emptyKeys = state.responseMappingFields.filter((x) => String(x.key || '').trim() === '' && String(x.value || '').trim() !== '');
if (emptyKeys.length > 0) {
callback(new Error('响应映射字段名不能为空'));
return;
}
// 检查是否设置了返回主体字段(必填)
const validFields = state.responseMappingFields.filter((x) => String(x.key || '').trim() !== '');
if (validFields.length > 0) {
const hasMainBody = state.responseMappingFields.some((f) => f.isMainBody && String(f.key || '').trim() !== '');
if (!hasMainBody) {
callback(new Error('响应映射必须设置一个返回主体字段'));
return;
}
}
callback();
},
trigger: 'change',
},
],
tokenConfig: [
{
validator: (_rule: unknown, _value: unknown, callback: (e?: Error) => void) => {
const emptyKeys = state.tokenConfigFields.filter((x) => String(x.key || '').trim() === '' && String(x.value || '').trim() !== '');
if (emptyKeys.length > 0) {
callback(new Error('Token计算配置字段名不能为空'));
return;
}
callback();
},
trigger: 'change',
},
],
extendMapping: [
{
validator: (_rule: unknown, _value: unknown, callback: (e?: Error) => void) => {
const emptyKeys = state.extendMappingFields.filter((x) => String(x.key || '').trim() === '' && String(x.value || '').trim() !== '');
if (emptyKeys.length > 0) {
callback(new Error('附加映射字段名不能为空'));
return;
}
callback();
},
trigger: 'change',
},
],
},
dialog: {
isShowDialog: false,
type: '',
title: '',
submitTxt: '',
loading: false,
detailLoading: false,
},
showAdvanced: false,
headers: [] as Array<{ key: string; value: string }>,
formFields: [] as Array<{ key: string; value: string }>,
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 }>,
mainBodyIndex: -1, // 记录哪一行被设置为返回主体
});
// 将数组转换为对象
const fieldsToObject = (fields: Array<{ key: string; value: string }>) => {
const obj: Record<string, string> = {};
fields.forEach((f) => {
if (f.key && f.key.trim()) {
obj[f.key.trim()] = f.value || '';
}
});
return obj;
};
const parseHeaders = (headMsg: string) => parseKeyValueString(headMsg);
// 统一的字段解析函数支持数组、对象、JSON字符串
const parseFieldsUnified = (raw: unknown): Array<{ key: string; value: string }> => {
if (!raw) return [];
// 如果是字符串尝试解析为JSON
if (typeof raw === 'string') {
try {
const parsed = JSON.parse(raw);
return parseFieldsUnified(parsed);
} catch {
return [];
}
}
// 如果是数组格式 [{ key, value }]
if (Array.isArray(raw)) {
return (raw as ModelFormEntry[])
.filter((item) => item && (item.key !== undefined || item.value !== undefined))
.map((item) => ({
key: String(item.key ?? '').trim(),
value: String(item.value ?? '').trim(),
}));
}
// 如果是对象格式 { key: value }
if (typeof raw === 'object') {
const fields: Array<{ key: string; value: string }> = [];
Object.keys(raw as Record<string, unknown>).forEach((key) => {
const v = (raw as Record<string, unknown>)[key];
const value = String(v ?? '');
fields.push({ key, value });
});
return fields;
}
return [];
};
// 解析 requestMapping 对象为数组
const parseRequestMappingFields = (mapping: unknown) => {
if (!mapping || typeof mapping !== 'object' || Array.isArray(mapping)) return [];
return Object.entries(mapping).map(([key, value]) => ({ key, value: String(value) }));
};
// 解析 responseMapping 对象为数组
const parseResponseMappingFields = (mapping: unknown) => {
if (!mapping || typeof mapping !== 'object' || Array.isArray(mapping)) return [];
return Object.entries(mapping).map(([key, value]) => ({ key, value: String(value) }));
};
const parseKeyValueString = (raw: string) => {
if (!raw) return [];
const headers: Array<{ key: string; value: string }> = [];
const pairs = raw.split(',');
pairs.forEach((pair) => {
const idx = pair.indexOf(':');
if (idx === -1) return;
const key = pair.slice(0, idx).trim();
const value = pair.slice(idx + 1).trim();
if (key) {
headers.push({ key, value });
}
});
return headers;
};
const stringifyHeaders = () => {
return state.headers
.filter((h) => h.key?.trim() && h.value?.trim())
.map((h) => `${h.key.trim()}:${h.value.trim()}`)
.join(',');
};
const onIsPrivateChange = (val: string | number | boolean | undefined) => {
if (val === 0 || val === '0') {
state.ruleForm.apiKey = '';
}
};
const fieldsToUnknownObject = (fields: Array<{ key: string; value: string }>) => {
const obj: Record<string, unknown> = {};
fields.forEach((f) => {
const k = String(f.key || '').trim();
if (!k) return;
obj[k] = String(f.value ?? '');
});
return obj;
};
const fieldsToNestedObject = (fields: Array<{ key: string; value: string }>) => {
const root: Record<string, unknown> = {};
fields.forEach((f) => {
const path = String(f.key || '').trim();
if (!path) return;
const parts = path
.split('.')
.map((p) => p.trim())
.filter(Boolean);
if (!parts.length) return;
let cur: Record<string, unknown> = root;
parts.forEach((part, idx) => {
if (idx === parts.length - 1) {
cur[part] = String(f.value ?? '');
return;
}
if (!cur[part] || typeof cur[part] !== 'object' || Array.isArray(cur[part])) {
cur[part] = {};
}
cur = cur[part] as Record<string, unknown>;
});
});
return root;
};
const objectToFields = (obj: Record<string, unknown>) => {
return Object.entries(obj).map(([key, value]) => ({ key, value: String(value ?? '') }));
};
const nestedObjectToFields = (obj: Record<string, unknown>, prefix = ''): Array<{ key: string; value: string }> => {
const rows: Array<{ key: string; value: string }> = [];
Object.entries(obj || {}).forEach(([key, value]) => {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (value && typeof value === 'object' && !Array.isArray(value)) {
rows.push(...nestedObjectToFields(value as Record<string, unknown>, fullKey));
return;
}
rows.push({ key: fullKey, value: String(value ?? '') });
});
return rows;
};
const buildQueryConfig = () => {
const responseType = state.ruleForm.queryResponseType || 'sync';
if (responseType === 'callback') {
return {
responseType: 'callback',
callbackUrl: String(state.ruleForm.queryCallbackUrl || '').trim(),
};
}
if (responseType === 'pull') {
return {
responseType: 'pull',
callbackUrl: '',
method: pullConfigForm.method || 'GET',
url: pullConfigForm.url || '',
headers: fieldsToUnknownObject(pullConfigForm.headersFields),
body: fieldsToNestedObject(pullConfigForm.bodyFields),
response: pullConfigForm.responseFields
.map((row) => ({
value: String(row.value || '').trim(),
isTokenField: Boolean(row.isTokenField),
isMainBody: Boolean(row.isMainBody),
}))
.filter((row) => row.value !== ''),
};
}
return {
responseType: 'sync',
callbackUrl: '',
};
};
// 添加请求头
const addHeader = () => {
state.headers.push({ key: '', value: '' });
};
// 删除请求头
const removeHeader = (index: number) => {
state.headers.splice(index, 1);
};
// 确认请求头配置
const confirmHeaders = () => {
state.ruleForm.headMsg = stringifyHeaders();
showHeaderDialog.value = false;
};
// 添加表单字段
const addFormField = () => {
state.formFields.push({ key: '', value: '' });
};
// 删除表单字段
const removeFormField = (index: number) => {
state.formFields.splice(index, 1);
};
// 确认表单字段配置
const confirmFormFields = () => {
showFormDialog.value = false;
};
// 请求映射字段操作
const addRequestMappingField = () => {
state.requestMappingFields.push({ key: '', value: '' });
};
const removeRequestMappingField = (index: number) => {
state.requestMappingFields.splice(index, 1);
};
const confirmRequestMappingFields = () => {
showRequestMappingDialog.value = false;
};
// 响应映射字段操作
const addResponseMappingField = () => {
state.responseMappingFields.push({ key: '', value: '', isMainBody: false, isTokenField: false });
};
const removeResponseMappingField = (index: number) => {
const removed = state.responseMappingFields[index];
state.responseMappingFields.splice(index, 1);
if (removed?.isTokenField) {
state.ruleForm.responseTokenField = '';
}
};
const setTokenField = (index: number) => {
state.responseMappingFields.forEach((field, i) => {
field.isTokenField = i === index;
});
state.ruleForm.responseTokenField = state.responseMappingFields[index]?.key?.trim?.() || '';
};
const syncTokenFieldOnKeyChange = (index: number) => {
const row = state.responseMappingFields[index];
if (!row?.isTokenField) return;
state.ruleForm.responseTokenField = row.key?.trim?.() || '';
};
// 设置返回主体(单选)
const setMainBody = (index: number) => {
// 清除所有字段的返回主体标记
state.responseMappingFields.forEach((field, i) => {
field.isMainBody = i === index;
});
state.mainBodyIndex = index;
};
const confirmResponseMappingFields = () => {
showResponseMappingDialog.value = false;
};
const setPullTokenField = (index: number) => {
pullConfigForm.responseFields.forEach((field, i) => {
field.isTokenField = i === index;
});
};
const setPullMainBody = (index: number) => {
pullConfigForm.responseFields.forEach((field, i) => {
field.isMainBody = i === index;
});
};
const ensureKeyValueRows = (rows: Array<{ key: string; value: string }>) => (rows.length ? rows : [{ key: '', value: '' }]);
const ensureResponseMappingRows = (rows: Array<{ key: string; value: string; isMainBody?: boolean; isTokenField?: boolean }>) => {
if (!rows.length) return [{ key: '', value: '', isMainBody: false, isTokenField: false }];
return rows.map((row) => ({ ...row, isMainBody: row.isMainBody || false, isTokenField: row.isTokenField || false }));
};
/** 从 getModel 返回的 data 中取出单条模型对象 */
const unwrapModelDetailPayload = (data: unknown): Record<string, unknown> | null => {
if (data == null) return null;
if (Array.isArray(data)) return null;
if (typeof data !== 'object') return null;
const o = data as Record<string, unknown>;
if (typeof o.modelName === 'string') return o;
if (o.model && typeof o.model === 'object' && !Array.isArray(o.model)) {
return o.model as Record<string, unknown>;
}
if (o.detail && typeof o.detail === 'object' && !Array.isArray(o.detail)) {
return o.detail as Record<string, unknown>;
}
if (o.info && typeof o.info === 'object' && !Array.isArray(o.info)) {
return o.info as Record<string, unknown>;
}
return o;
};
const fillFormFromDetailRow = (row: Record<string, unknown>) => {
const timeoutSeconds =
row.timeoutSeconds != null && row.timeoutSeconds !== ''
? Number(row.timeoutSeconds)
: row.timeoutMs != null
? Math.floor(Number(row.timeoutMs) / 1000)
: 30;
const isPrivate = row.isPrivate !== undefined && row.isPrivate !== null ? Number(row.isPrivate) : 0;
state.ruleForm = {
id: row.id as string,
modelName: String(row.modelName ?? ''),
modelType: row.modelType !== undefined && row.modelType !== null ? typeOptionValue(row.modelType as number | string) : null,
operatorName: String(row.operatorName ?? ''),
baseUrl: String(row.baseUrl ?? ''),
httpMethod: String(row.httpMethod || 'POST'),
queryResponseType: String((row.queryConfig as Record<string, unknown>)?.responseType || 'sync'),
queryCallbackUrl: String((row.queryConfig as Record<string, unknown>)?.callbackUrl || ''),
headMsg: String(row.headMsg || ''),
isPrivate,
apiKey: isPrivate === 1 ? String(row.apiKey ?? '') : '',
enabled: Number(row.enabled ?? 1),
isChatModel: row.isChatModel !== undefined && row.isChatModel !== null ? Number(row.isChatModel) : 0,
maxConcurrency: Number(row.maxConcurrency ?? 10),
queueLimit: Number(row.queueLimit ?? 100),
timeoutSeconds,
expectedSeconds: Number(row.expectedSeconds ?? 15),
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(String(row.headMsg || '')));
state.formFields = ensureKeyValueRows(parseFieldsUnified(row.form));
state.requestMappingFields = ensureKeyValueRows(parseRequestMappingFields(row.requestMapping));
state.responseMappingFields = ensureResponseMappingRows(parseResponseMappingFields(row.responseMapping));
state.extendMappingFields = ensureKeyValueRows(parseFieldsUnified(row.extendMapping));
state.tokenConfigFields = ensureKeyValueRows(parseFieldsUnified(row.tokenConfig));
// 根据 responseTokenField 字段设置计费字段标记(单选)
const tokenFieldKey = String(row.responseTokenField || '').trim();
if (tokenFieldKey) {
const tokenFieldIndex = state.responseMappingFields.findIndex((f) => String(f.key || '').trim() === tokenFieldKey);
if (tokenFieldIndex !== -1) {
state.responseMappingFields[tokenFieldIndex].isTokenField = true;
}
}
// 根据 responseBody 字段设置返回主体标记(单选)
if (row.responseBody && typeof row.responseBody === 'object' && !Array.isArray(row.responseBody)) {
const bodyKeys = Object.keys(row.responseBody);
if (bodyKeys.length > 0) {
const bodyKey = bodyKeys[0];
const bodyFieldIndex = state.responseMappingFields.findIndex((f) => String(f.key || '').trim() === bodyKey);
if (bodyFieldIndex !== -1) {
state.responseMappingFields[bodyFieldIndex].isMainBody = true;
state.mainBodyIndex = bodyFieldIndex;
}
}
}
if (row.queryConfig && typeof row.queryConfig === 'object' && !Array.isArray(row.queryConfig)) {
const qc = row.queryConfig as Record<string, unknown>;
pullConfigForm.method = String(qc.method || 'GET');
pullConfigForm.url = String(qc.url || 'https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks');
pullConfigForm.headersFields = ensureKeyValueRows(objectToFields((qc.headers as Record<string, unknown>) || {}));
pullConfigForm.bodyFields = ensureKeyValueRows(nestedObjectToFields((qc.body as Record<string, unknown>) || {}));
pullConfigForm.responseFields = ((qc.response as unknown[]) || []).map((item) => {
if (typeof item === 'string') {
return { value: item, isTokenField: false, isMainBody: false };
}
const row = item as Record<string, unknown>;
return {
value: String(row.value ?? ''),
isTokenField: Boolean(row.isTokenField),
isMainBody: Boolean(row.isMainBody),
};
});
} else {
pullConfigForm.method = 'GET';
pullConfigForm.url = '';
pullConfigForm.headersFields = [{ key: '', value: '' }];
pullConfigForm.bodyFields = [{ key: '', value: '' }];
pullConfigForm.responseFields = [{ value: '', isTokenField: false, isMainBody: false }];
}
};
// 打开弹窗(编辑时会请求 /model/getModel 详情)
const openDialog = async (type: string, row?: Record<string, unknown>) => {
state.dialog.type = type;
state.dialog.isShowDialog = true;
state.showAdvanced = false;
state.dialog.detailLoading = false;
if (type === 'edit') {
const listRowId = row?.id;
if (listRowId === undefined || listRowId === null || listRowId === '') {
ElMessage.error('缺少模型 ID');
state.dialog.isShowDialog = false;
return;
}
state.dialog.title = '修改模型配置';
state.dialog.submitTxt = '修 改';
state.dialog.detailLoading = true;
try {
const res: any = await getModelModuleDetail(listRowId as string | number);
if (res.code !== 0) {
ElMessage.error(res.message || '获取模型详情失败');
state.dialog.isShowDialog = false;
return;
}
const detail = unwrapModelDetailPayload(res.data) ?? row ?? null;
if (!detail || typeof detail !== 'object') {
ElMessage.error('获取模型详情失败');
state.dialog.isShowDialog = false;
return;
}
fillFormFromDetailRow(detail as Record<string, unknown>);
} catch {
// 接口错误由 request 全局提示后端 message
state.dialog.isShowDialog = false;
} finally {
state.dialog.detailLoading = false;
}
} else {
state.ruleForm = {
id: '',
modelName: '',
modelType: null,
operatorName: '',
baseUrl: '',
httpMethod: 'POST',
queryResponseType: 'sync',
queryCallbackUrl: '',
headMsg: '',
isPrivate: props.isSuperAdmin ? 1 : 0,
apiKey: '',
enabled: 1,
isChatModel: 0,
maxConcurrency: 10,
queueLimit: 100,
timeoutSeconds: 30,
expectedSeconds: 15,
retryTimes: 3,
retryQueueMaxSeconds: 60,
autoCleanSeconds: 300,
remark: '',
extendMapping: '{}',
responseTokenField: '',
tokenConfig: '{}',
};
state.headers = [{ key: '', value: '' }];
state.formFields = [{ key: '', value: '' }];
state.requestMappingFields = [{ key: '', value: '' }];
state.responseMappingFields = [{ key: '', value: '', isMainBody: false, isTokenField: false }];
state.mainBodyIndex = -1;
state.extendMappingFields = [{ key: '', value: '' }];
state.tokenConfigFields = [{ key: '', value: '' }];
pullConfigForm.method = 'GET';
pullConfigForm.url = '';
pullConfigForm.headersFields = [{ key: '', value: '' }];
pullConfigForm.bodyFields = [{ key: '', value: '' }];
pullConfigForm.responseFields = [{ value: '', isTokenField: false, isMainBody: false }];
state.dialog.title = '新增模型配置';
state.dialog.submitTxt = '新 增';
}
};
// 关闭弹窗
const closeDialog = () => {
state.dialog.isShowDialog = false;
state.dialog.detailLoading = false;
editModuleFormRef.value?.resetFields();
};
// 取消
const onCancel = () => {
closeDialog();
};
// 提交
const onSubmit = () => {
editModuleFormRef.value.validate(async (valid: boolean) => {
if (!valid) return;
state.dialog.loading = true;
try {
// 触发所有自定义字段的验证
if (state.ruleForm.queryResponseType === 'pull') {
await editModuleFormRef.value?.validateField?.('queryPullConfig');
}
// 验证响应映射(如果有配置)
const validResponseFields = state.responseMappingFields.filter((x) => String(x.key || '').trim() !== '');
if (validResponseFields.length > 0) {
await editModuleFormRef.value?.validateField?.('responseMapping');
}
// 验证请求映射(如果有配置)
const validRequestFields = state.requestMappingFields.filter((x) => String(x.key || '').trim() !== '');
if (validRequestFields.length > 0) {
await editModuleFormRef.value?.validateField?.('requestMapping');
}
state.ruleForm.headMsg = stringifyHeaders();
// 过滤掉空键名的字段
const requestMapping = fieldsToObject(state.requestMappingFields.filter((f) => String(f.key || '').trim() !== ''));
const responseMapping = fieldsToObject(state.responseMappingFields.filter((f) => String(f.key || '').trim() !== ''));
// 获取被设置为返回主体的字段 {key: value}
const responseBodyField = state.responseMappingFields.find((f) => f.isMainBody && String(f.key || '').trim() !== '');
const responseBody = responseBodyField ? { [responseBodyField.key.trim()]: responseBodyField.value } : {};
// 获取计费字段(可选)
const responseTokenField =
state.responseMappingFields.find((f) => f.isTokenField)?.key?.trim() || String(state.ruleForm.responseTokenField || '').trim();
const submitData: CreateModelParams = {
modelName: state.ruleForm.modelName,
modelType: state.ruleForm.modelType as number | string,
operatorName: state.ruleForm.operatorName,
baseUrl: state.ruleForm.baseUrl,
httpMethod: state.ruleForm.httpMethod || 'POST',
headMsg: state.ruleForm.headMsg,
isPrivate: state.ruleForm.isPrivate,
enabled: state.ruleForm.enabled,
isChatModel: state.ruleForm.isChatModel,
// 确保 API 密钥只在 isPrivate=1 时提交
apiKey: state.ruleForm.isPrivate === 1 ? String(state.ruleForm.apiKey ?? '').trim() : '',
form: state.formFields
.filter((f) => String(f.key || '').trim() !== '')
.map((f) => ({ key: String(f.key).trim(), value: String(f.value ?? '') })),
requestMapping,
responseMapping,
responseBody,
maxConcurrency: state.ruleForm.maxConcurrency,
queueLimit: state.ruleForm.queueLimit,
timeoutSeconds: state.ruleForm.timeoutSeconds,
expectedSeconds: state.ruleForm.expectedSeconds,
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() !== '')),
queryConfig: buildQueryConfig(),
};
if (state.dialog.type === 'edit') {
await updateModelModule({ ...submitData, id: state.ruleForm.id });
ElMessage.success('修改成功');
} else {
await addModelModule(submitData);
ElMessage.success('新增成功');
}
closeDialog();
emit('refresh');
} catch {
// 接口错误由 request 全局提示后端 message
} finally {
state.dialog.loading = false;
}
});
};
// 暴露变量
defineExpose({
openDialog,
});
onMounted(() => {
loadOperatorOptions();
});
</script>
<style scoped lang="scss">
.mapping-config-container {
.mapping-field-item {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
.separator {
font-weight: bold;
color: #606266;
}
}
}
.form-config-container {
max-height: 400px;
overflow-y: auto;
}
.form-field-item {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.ml5 {
margin-left: 5px;
}
.header-config-container {
max-height: 400px;
overflow-y: auto;
}
.header-item {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.separator {
font-weight: bold;
color: #606266;
}
</style>