配置模型相关

This commit is contained in:
2026-06-05 13:07:00 +08:00
parent 6ef063ac09
commit eea5874dbf
2 changed files with 207 additions and 307 deletions

View File

@@ -119,8 +119,8 @@ export interface ModelModuleItem {
/** 会话开关状态列表接口返回0 关 1 开;会话开关接口就绪后生效) */ /** 会话开关状态列表接口返回0 关 1 开;会话开关接口就绪后生效) */
chatSessionEnabled?: number; chatSessionEnabled?: number;
enabled: number; enabled: number;
/** 是否异步 0-同步 1-异步依赖requestMapping */ /** 调用模式:0-同步 1-异步 2-流式 */
isAsync?: number; callMode?: number;
maxConcurrency: number; maxConcurrency: number;
queueLimit: number; queueLimit: number;
timeoutMs?: number; timeoutMs?: number;
@@ -156,8 +156,8 @@ export interface CreateModelParams {
isPrivate: number; isPrivate: number;
enabled: number; enabled: number;
isChatModel: number; isChatModel: number;
/** 是否异步 0-同步 1-异步依赖requestMapping默认0 */ /** 调用模式:0-同步 1-异步 2-流式默认0 */
isAsync: number; callMode: number;
apiKey?: string; apiKey?: string;
form: ModelFormEntry[]; form: ModelFormEntry[];
requestMapping?: Record<string, unknown>; requestMapping?: Record<string, unknown>;
@@ -167,6 +167,7 @@ export interface CreateModelParams {
responseTokenField?: string; responseTokenField?: string;
tokenConfig?: Record<string, unknown>; tokenConfig?: Record<string, unknown>;
queryConfig?: Record<string, unknown>; queryConfig?: Record<string, unknown>;
streamConfig?: Record<string, unknown>;
maxConcurrency?: number; maxConcurrency?: number;
queueLimit?: number; queueLimit?: number;
timeoutSeconds: number; timeoutSeconds: number;

View File

@@ -1,4 +1,4 @@
<template> <template>
<div class="system-edit-module-container"> <div class="system-edit-module-container">
<el-dialog :title="state.dialog.title" v-model="state.dialog.isShowDialog" width="900px"> <el-dialog :title="state.dialog.title" v-model="state.dialog.isShowDialog" width="900px">
<el-form <el-form
@@ -45,23 +45,6 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </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-col v-if="!props.isSuperAdmin" :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="访问类型" prop="isPrivate"> <el-form-item label="访问类型" prop="isPrivate">
<el-radio-group v-model="state.ruleForm.isPrivate" @change="onIsPrivateChange"> <el-radio-group v-model="state.ruleForm.isPrivate" @change="onIsPrivateChange">
@@ -84,11 +67,22 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20"> <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="执行模式" prop="isAsync"> <el-form-item label="执行模式" prop="callMode">
<el-radio-group v-model="state.ruleForm.isAsync"> <el-select v-model="state.ruleForm.callMode" placeholder="请选择调用模式" clearable style="width: 100%">
<el-radio :label="0">同步</el-radio> <el-option label="同步" :value="0"></el-option>
<el-radio :label="1">异步</el-radio> <el-option label="异步" :value="1"></el-option>
</el-radio-group> <el-option label="流式" :value="2"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col v-if="state.ruleForm.callMode === 1" :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="查询/回调配置" prop="asyncQueryConfig">
<el-button @click="showAsyncQueryConfigDialog = true" style="width: 100%">配置查询/回调</el-button>
</el-form-item>
</el-col>
<el-col v-if="state.ruleForm.callMode === 2" :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="流式配置" prop="streamConfig">
<el-button @click="showStreamConfigDialog = true" style="width: 100%">配置流式参数</el-button>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20"> <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
@@ -97,7 +91,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20"> <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="自定义字段" prop="form"> <el-form-item label="自定义表单" prop="form">
<el-button @click="showFormDialog = true" style="width: 100%"> 配置表单字段 ({{ state.formFields.length }}) </el-button> <el-button @click="showFormDialog = true" style="width: 100%"> 配置表单字段 ({{ state.formFields.length }}) </el-button>
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -202,6 +196,65 @@
</template> </template>
</el-dialog> </el-dialog>
<!-- 查询/回调配置弹窗 -->
<el-dialog v-model="showAsyncQueryConfigDialog" title="配置查询/回调" width="800px" :close-on-click-modal="false">
<el-form label-width="140px">
<el-form-item label="查询地址">
<el-input v-model="asyncQueryConfigForm.url" placeholder="请输入查询地址" clearable></el-input>
</el-form-item>
<el-form-item label="请求方式">
<el-select v-model="asyncQueryConfigForm.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="任务ID路径">
<el-input v-model="asyncQueryConfigForm.taskId" placeholder="如 data.task_id" clearable></el-input>
</el-form-item>
<el-form-item label="结果路径">
<el-input v-model="asyncQueryConfigForm.resultPath" placeholder="如 data.audio_url" clearable></el-input>
</el-form-item>
<el-form-item label="状态路径">
<el-input v-model="asyncQueryConfigForm.statusPath" placeholder="如 data.task_status" clearable></el-input>
</el-form-item>
<el-form-item label="轮询间隔(秒)">
<el-input-number v-model="asyncQueryConfigForm.intervalSeconds" :min="1" :max="3600" style="width: 100%"></el-input-number>
</el-form-item>
<el-form-item label="状态值映射">
<div class="mapping-config-container async-query-status-values">
<div v-for="(field, index) in asyncQueryConfigForm.statusValueFields" :key="index" class="mapping-field-item">
<el-input v-model="field.key" placeholder="状态名称,如 succeeded" style="width: 40%" clearable></el-input>
<span class="separator">=</span>
<el-input v-model="field.value" placeholder="状态值,按字符串提交" style="width: 40%" clearable></el-input>
<el-button type="danger" link @click="removeAsyncQueryStatusValueField(index)">删除</el-button>
</div>
<el-button type="primary" link @click="addAsyncQueryStatusValueField" style="margin-top: 10px">+ 添加字段</el-button>
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showAsyncQueryConfigDialog = false" size="default"> </el-button>
<el-button type="primary" @click="showAsyncQueryConfigDialog = false" size="default"> </el-button>
</span>
</template>
</el-dialog>
<!-- 流式配置弹窗 -->
<el-dialog v-model="showStreamConfigDialog" title="配置流式参数" width="600px" :close-on-click-modal="false">
<div class="stream-config-placeholder">
流式配置表单内容待确定当前将按空对象提交
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="showStreamConfigDialog = false" size="default"> </el-button>
<el-button type="primary" @click="showStreamConfigDialog = false" size="default"> </el-button>
</span>
</template>
</el-dialog>
<!-- 请求头配置弹窗 --> <!-- 请求头配置弹窗 -->
<el-dialog v-model="showHeaderDialog" title="配置请求头" width="600px" :close-on-click-modal="false"> <el-dialog v-model="showHeaderDialog" title="配置请求头" width="600px" :close-on-click-modal="false">
<div class="header-config-container"> <div class="header-config-container">
@@ -401,38 +454,6 @@
</template> </template>
</el-dialog> </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"> <el-dialog v-model="showExtendMappingDialog" title="配置附加映射" width="600px" :close-on-click-modal="false">
<div class="mapping-config-container"> <div class="mapping-config-container">
@@ -460,73 +481,9 @@
</div> </div>
<el-button type="primary" link @click="state.tokenConfigFields.push({ key: '', value: '' })" style="margin-top: 10px">+ 添加字段</el-button> <el-button type="primary" link @click="state.tokenConfigFields.push({ key: '', value: '' })" style="margin-top: 10px">+ 添加字段</el-button>
</div> </div>
<template #footer <template #footer>
><span class="dialog-footer"><el-button @click="showTokenConfigDialog = false"> </el-button></span></template <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> </el-dialog>
</div> </div>
</template> </template>
@@ -555,23 +512,23 @@ export interface FormFieldOption {
// 定义自定义字段类型 // 定义自定义字段类型
export interface FormField { export interface FormField {
label: string; // 字段描述 label: string; // 字段描述
key: string; // 字段名称 key: string; // 字段名称
type: 'string' | 'number' | 'select' | 'radio' | 'file'; // 字段类型 type: 'string' | 'number' | 'select' | 'radio' | 'file'; // 字段类型
defaultValue: string; // 默认值 defaultValue: string; // 默认值
required: boolean; // 是否必填 required: boolean; // 是否必填
// 字符串配置 // 字符串配置
maxLength?: number; // 最大长度 maxLength?: number; // 最大长度
// 数字配置 // 数字配置
min?: number; // 最小值 min?: number; // 最小值
max?: number; // 最大值 max?: number; // 最大值
numberType?: 'any' | 'integer' | 'float' | 'positive-int' | 'positive-float' | 'negative-int' | 'negative-float'; // 数字子类型 numberType?: 'any' | 'integer' | 'float' | 'positive-int' | 'positive-float' | 'negative-int' | 'negative-float'; // 数字子类型
// 文件上传配置 // 文件上传配置
maxSize?: number; // 最大文件大小(MB) maxSize?: number; // 最大文件大小(MB)
maxCount?: number; // 最大上传数量 maxCount?: number; // 最大上传数量
allowedTypes?: string; // 允许的文件格式,逗号分隔 allowedTypes?: string; // 允许的文件格式,逗号分隔
// 下拉框/单选框配置 // 下拉框/单选框配置
options?: FormFieldOption[]; // 选项列表 options?: FormFieldOption[]; // 选项列表
} }
const props = withDefaults( const props = withDefaults(
@@ -586,9 +543,6 @@ const props = withDefaults(
); );
const modelTypeOptions = computed(() => props.modelTypes); 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 operatorNameOptions = ref<Array<{ label: string; value: string }>>([]);
@@ -612,34 +566,22 @@ const typeOptionValue = (id: number | string): number | string => {
const editModuleFormRef = ref(); const editModuleFormRef = ref();
const emit = defineEmits(['refresh']); const emit = defineEmits(['refresh']);
const showHeaderDialog = ref(false); const showHeaderDialog = ref(false);
const showAsyncQueryConfigDialog = ref(false);
const showStreamConfigDialog = ref(false);
const showFormDialog = ref(false); const showFormDialog = ref(false);
const showRequestMappingDialog = ref(false); const showRequestMappingDialog = ref(false);
const showResponseMappingDialog = ref(false); const showResponseMappingDialog = ref(false);
const showPullConfigDialog = ref(false);
const showExtendMappingDialog = ref(false); const showExtendMappingDialog = ref(false);
const showTokenConfigDialog = ref(false); const showTokenConfigDialog = ref(false);
const showPullHeadersDialog = ref(false); const asyncQueryConfigForm = reactive({
const showPullBodyDialog = ref(false); url: '',
const showPullResponseDialog = ref(false); method: 'POST',
const pullConfigForm = reactive({ taskId: '',
method: 'GET', resultPath: '',
url: 'https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks', statusPath: '',
headersFields: [ intervalSeconds: 2,
{ key: 'Content-Type', value: 'application/json' }, statusValueFields: [{ key: '', value: '' }] as Array<{ key: string; value: string }>,
{ 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({ const state = reactive({
ruleForm: { ruleForm: {
@@ -649,14 +591,12 @@ const state = reactive({
operatorName: '', operatorName: '',
baseUrl: '', baseUrl: '',
httpMethod: 'POST', httpMethod: 'POST',
queryResponseType: 'sync',
queryCallbackUrl: '',
headMsg: '', headMsg: '',
isPrivate: 0, isPrivate: 0,
apiKey: '', apiKey: '',
enabled: 1, enabled: 1,
isChatModel: 0, isChatModel: 0,
isAsync: 0, // 0-同步 1-异步默认0 callMode: 0,
maxConcurrency: 10, maxConcurrency: 10,
queueLimit: 100, queueLimit: 100,
timeoutSeconds: 30, timeoutSeconds: 30,
@@ -685,44 +625,52 @@ const state = reactive({
], ],
baseUrl: [{ required: true, message: '请输入模型服务地址', trigger: 'blur' }], baseUrl: [{ required: true, message: '请输入模型服务地址', trigger: 'blur' }],
httpMethod: [{ required: true, message: '请选择请求方式', trigger: 'change' }], httpMethod: [{ required: true, message: '请选择请求方式', trigger: 'change' }],
queryResponseType: [{ required: true, message: '请选择响应类型', trigger: 'change' }], callMode: [{ required: true, message: '请选择调用模式', trigger: 'change' }],
queryCallbackUrl: [ asyncQueryConfig: [
{
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) => { validator: (_rule: unknown, _value: unknown, callback: (e?: Error) => void) => {
if (state.ruleForm.queryResponseType !== 'pull') { if (Number(state.ruleForm.callMode) !== 1) {
callback(); callback();
return; return;
} }
if (!pullConfigForm.url || String(pullConfigForm.url).trim() === '') { if (!String(asyncQueryConfigForm.url || '').trim()) {
callback(new Error('主动拉取时,请填写拉取地址')); callback(new Error('异步执行时,请填写查询地址'));
return; return;
} }
// 验证响应字段至少有一个有效字段 if (!String(asyncQueryConfigForm.method || '').trim()) {
const validResponseFields = pullConfigForm.responseFields.filter((f) => String(f.value || '').trim() !== ''); callback(new Error('异步执行时,请选择请求方式'));
if (validResponseFields.length === 0) {
callback(new Error('主动拉取时,至少需要配置一个响应字段'));
return; return;
} }
// 验证是否设置了返回主体字段 if (!String(asyncQueryConfigForm.taskId || '').trim()) {
const hasMainBody = pullConfigForm.responseFields.some((f) => f.isMainBody && String(f.value || '').trim() !== ''); callback(new Error('异步执行时请填写任务ID路径'));
if (!hasMainBody) { return;
callback(new Error('主动拉取时,必须设置一个返回主体字段')); }
if (!String(asyncQueryConfigForm.resultPath || '').trim()) {
callback(new Error('异步执行时,请填写结果路径'));
return;
}
if (!String(asyncQueryConfigForm.statusPath || '').trim()) {
callback(new Error('异步执行时,请填写状态路径'));
return;
}
if (!asyncQueryConfigForm.intervalSeconds || Number(asyncQueryConfigForm.intervalSeconds) <= 0) {
callback(new Error('异步执行时,请填写有效的轮询间隔'));
return;
}
const validStatusValues = asyncQueryConfigForm.statusValueFields.filter(
(item) => String(item.key || '').trim() !== '' && String(item.value || '').trim() !== ''
);
if (validStatusValues.length === 0) {
callback(new Error('异步执行时,请至少配置一个状态值映射'));
return;
}
const hasInvalidStatusValue = asyncQueryConfigForm.statusValueFields.some(
(item) =>
(String(item.key || '').trim() === '' && String(item.value || '').trim() !== '') ||
(String(item.key || '').trim() !== '' && String(item.value || '').trim() === '')
);
if (hasInvalidStatusValue) {
callback(new Error('状态值映射的键和值都必须完整填写'));
return; return;
} }
callback(); callback();
@@ -1012,77 +960,41 @@ const fieldsToUnknownObject = (fields: Array<{ key: string; value: string }>) =>
return obj; return obj;
}; };
const fieldsToNestedObject = (fields: Array<{ key: string; value: string }>) => { const buildAsyncQueryConfig = () => {
const root: Record<string, unknown> = {}; const statusValues: Record<string, string> = {};
fields.forEach((f) => { asyncQueryConfigForm.statusValueFields.forEach((item) => {
const path = String(f.key || '').trim(); const key = String(item.key || '').trim();
if (!path) return; if (!key) return;
const parts = path statusValues[key] = String(item.value ?? '');
.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; return {
url: String(asyncQueryConfigForm.url || '').trim(),
method: String(asyncQueryConfigForm.method || 'POST').trim() || 'POST',
task_id: String(asyncQueryConfigForm.taskId || '').trim(),
result_path: String(asyncQueryConfigForm.resultPath || '').trim(),
status_path: String(asyncQueryConfigForm.statusPath || '').trim(),
status_values: statusValues,
interval_seconds: Number(asyncQueryConfigForm.intervalSeconds || 0),
};
};
const addAsyncQueryStatusValueField = () => {
asyncQueryConfigForm.statusValueFields.push({ key: '', value: '' });
};
const removeAsyncQueryStatusValueField = (index: number) => {
asyncQueryConfigForm.statusValueFields.splice(index, 1);
}; };
const objectToFields = (obj: Record<string, unknown>) => { const objectToFields = (obj: Record<string, unknown>) => {
return Object.entries(obj).map(([key, value]) => ({ key, value: String(value ?? '') })); 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 buildQueryConfig = () => {
const responseType = state.ruleForm.queryResponseType || 'sync'; if (Number(state.ruleForm.callMode) === 1) {
if (responseType === 'callback') { return buildAsyncQueryConfig();
return {
responseType: 'callback',
callbackUrl: String(state.ruleForm.queryCallbackUrl || '').trim(),
};
} }
if (responseType === 'pull') { return {};
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: '',
};
}; };
// 添加请求头 // 添加请求头
@@ -1188,17 +1100,6 @@ const setMainBody = (index: number) => {
const confirmResponseMappingFields = () => { const confirmResponseMappingFields = () => {
showResponseMappingDialog.value = false; 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 ensureKeyValueRows = (rows: Array<{ key: string; value: string }>) => (rows.length ? rows : [{ key: '', value: '' }]);
@@ -1210,7 +1111,7 @@ const parseFormFieldsUnified = (raw: unknown): Array<FormField> => {
if (Array.isArray(raw)) { if (Array.isArray(raw)) {
if (raw.length === 0) return [createEmptyFormField()]; if (raw.length === 0) return [createEmptyFormField()];
// 确保新增字段有默认值 // 确保新增字段有默认值
return (raw as any[]).map(item => { return (raw as any[]).map((item) => {
const field = createEmptyFormField(); const field = createEmptyFormField();
return { ...field, ...item }; return { ...field, ...item };
}) as FormField[]; }) as FormField[];
@@ -1275,14 +1176,12 @@ const fillFormFromDetailRow = (row: Record<string, unknown>) => {
operatorName: String(row.operatorName ?? ''), operatorName: String(row.operatorName ?? ''),
baseUrl: String(row.baseUrl ?? ''), baseUrl: String(row.baseUrl ?? ''),
httpMethod: String(row.httpMethod || 'POST'), 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: ruleFormHeadMsg, headMsg: ruleFormHeadMsg,
isPrivate, isPrivate,
apiKey: isPrivate === 1 ? String(row.apiKey ?? '') : '', apiKey: isPrivate === 1 ? String(row.apiKey ?? '') : '',
enabled: Number(row.enabled ?? 1), enabled: Number(row.enabled ?? 1),
isChatModel: row.isChatModel !== undefined && row.isChatModel !== null ? Number(row.isChatModel) : 0, isChatModel: row.isChatModel !== undefined && row.isChatModel !== null ? Number(row.isChatModel) : 0,
isAsync: row.isAsync !== undefined && row.isAsync !== null ? Number(row.isAsync) : 0, callMode: row.callMode !== undefined && row.callMode !== null ? Number(row.callMode) : row.isAsync !== undefined && row.isAsync !== null ? Number(row.isAsync) : 0,
maxConcurrency: Number(row.maxConcurrency ?? 10), maxConcurrency: Number(row.maxConcurrency ?? 10),
queueLimit: Number(row.queueLimit ?? 100), queueLimit: Number(row.queueLimit ?? 100),
timeoutSeconds, timeoutSeconds,
@@ -1325,27 +1224,21 @@ const fillFormFromDetailRow = (row: Record<string, unknown>) => {
if (row.queryConfig && typeof row.queryConfig === 'object' && !Array.isArray(row.queryConfig)) { if (row.queryConfig && typeof row.queryConfig === 'object' && !Array.isArray(row.queryConfig)) {
const qc = row.queryConfig as Record<string, unknown>; const qc = row.queryConfig as Record<string, unknown>;
pullConfigForm.method = String(qc.method || 'GET'); asyncQueryConfigForm.url = String(qc.url || '');
pullConfigForm.url = String(qc.url || 'https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks'); asyncQueryConfigForm.method = String(qc.method || 'POST') || 'POST';
pullConfigForm.headersFields = ensureKeyValueRows(objectToFields((qc.headers as Record<string, unknown>) || {})); asyncQueryConfigForm.taskId = String(qc.task_id || '');
pullConfigForm.bodyFields = ensureKeyValueRows(nestedObjectToFields((qc.body as Record<string, unknown>) || {})); asyncQueryConfigForm.resultPath = String(qc.result_path || '');
pullConfigForm.responseFields = ((qc.response as unknown[]) || []).map((item) => { asyncQueryConfigForm.statusPath = String(qc.status_path || '');
if (typeof item === 'string') { asyncQueryConfigForm.intervalSeconds = Number(qc.interval_seconds ?? 2) || 2;
return { value: item, isTokenField: false, isMainBody: false }; asyncQueryConfigForm.statusValueFields = ensureKeyValueRows(objectToFields((qc.status_values as Record<string, unknown>) || {}));
}
const row = item as Record<string, unknown>;
return {
value: String(row.value ?? ''),
isTokenField: Boolean(row.isTokenField),
isMainBody: Boolean(row.isMainBody),
};
});
} else { } else {
pullConfigForm.method = 'GET'; asyncQueryConfigForm.url = '';
pullConfigForm.url = ''; asyncQueryConfigForm.method = 'POST';
pullConfigForm.headersFields = [{ key: '', value: '' }]; asyncQueryConfigForm.taskId = '';
pullConfigForm.bodyFields = [{ key: '', value: '' }]; asyncQueryConfigForm.resultPath = '';
pullConfigForm.responseFields = [{ value: '', isTokenField: false, isMainBody: false }]; asyncQueryConfigForm.statusPath = '';
asyncQueryConfigForm.intervalSeconds = 2;
asyncQueryConfigForm.statusValueFields = [{ key: '', value: '' }];
} }
}; };
// 打开弹窗(编辑时会请求 /model/getModel 详情) // 打开弹窗(编辑时会请求 /model/getModel 详情)
@@ -1393,14 +1286,12 @@ const openDialog = async (type: string, row?: Record<string, unknown>) => {
operatorName: '', operatorName: '',
baseUrl: '', baseUrl: '',
httpMethod: 'POST', httpMethod: 'POST',
queryResponseType: 'sync',
queryCallbackUrl: '',
headMsg: '', headMsg: '',
isPrivate: props.isSuperAdmin ? 1 : 0, isPrivate: props.isSuperAdmin ? 1 : 0,
apiKey: '', apiKey: '',
enabled: 1, enabled: 1,
isChatModel: 0, isChatModel: 0,
isAsync: 0, callMode: 0,
maxConcurrency: 10, maxConcurrency: 10,
queueLimit: 100, queueLimit: 100,
timeoutSeconds: 30, timeoutSeconds: 30,
@@ -1420,11 +1311,13 @@ const openDialog = async (type: string, row?: Record<string, unknown>) => {
state.mainBodyIndex = -1; state.mainBodyIndex = -1;
state.extendMappingFields = [{ key: '', value: '' }]; state.extendMappingFields = [{ key: '', value: '' }];
state.tokenConfigFields = [{ key: '', value: '' }]; state.tokenConfigFields = [{ key: '', value: '' }];
pullConfigForm.method = 'GET'; asyncQueryConfigForm.url = '';
pullConfigForm.url = ''; asyncQueryConfigForm.method = 'POST';
pullConfigForm.headersFields = [{ key: '', value: '' }]; asyncQueryConfigForm.taskId = '';
pullConfigForm.bodyFields = [{ key: '', value: '' }]; asyncQueryConfigForm.resultPath = '';
pullConfigForm.responseFields = [{ value: '', isTokenField: false, isMainBody: false }]; asyncQueryConfigForm.statusPath = '';
asyncQueryConfigForm.intervalSeconds = 2;
asyncQueryConfigForm.statusValueFields = [{ key: '', value: '' }];
state.dialog.title = '新增模型配置'; state.dialog.title = '新增模型配置';
state.dialog.submitTxt = '新 增'; state.dialog.submitTxt = '新 增';
} }
@@ -1450,8 +1343,8 @@ const onSubmit = () => {
state.dialog.loading = true; state.dialog.loading = true;
try { try {
// 触发所有自定义字段的验证 // 触发所有自定义字段的验证
if (state.ruleForm.queryResponseType === 'pull') { if (Number(state.ruleForm.callMode) === 1) {
await editModuleFormRef.value?.validateField?.('queryPullConfig'); await editModuleFormRef.value?.validateField?.('asyncQueryConfig');
} }
// 验证响应映射(如果有配置) // 验证响应映射(如果有配置)
@@ -1497,9 +1390,10 @@ const onSubmit = () => {
maxSize: f.type === 'file' && f.maxSize ? f.maxSize : undefined, maxSize: f.type === 'file' && f.maxSize ? f.maxSize : undefined,
maxCount: f.type === 'file' && f.maxCount ? f.maxCount : undefined, maxCount: f.type === 'file' && f.maxCount ? f.maxCount : undefined,
allowedTypes: f.type === 'file' && f.allowedTypes ? f.allowedTypes.trim() : undefined, allowedTypes: f.type === 'file' && f.allowedTypes ? f.allowedTypes.trim() : undefined,
options: (f.type === 'select' || f.type === 'radio') && f.options options:
? f.options.filter(opt => String(opt.label || '').trim() !== '' || String(opt.value || '').trim() !== '') (f.type === 'select' || f.type === 'radio') && f.options
: undefined, ? f.options.filter((opt) => String(opt.label || '').trim() !== '' || String(opt.value || '').trim() !== '')
: undefined,
})) as unknown as ModelFormEntry[]; })) as unknown as ModelFormEntry[];
// 将headers转换为JSON对象格式 // 将headers转换为JSON对象格式
@@ -1520,7 +1414,7 @@ const onSubmit = () => {
isPrivate: state.ruleForm.isPrivate, isPrivate: state.ruleForm.isPrivate,
enabled: state.ruleForm.enabled, enabled: state.ruleForm.enabled,
isChatModel: state.ruleForm.isChatModel, isChatModel: state.ruleForm.isChatModel,
isAsync: state.ruleForm.isAsync, callMode: state.ruleForm.callMode,
// 确保 API 密钥只在 isPrivate=1 时提交 // 确保 API 密钥只在 isPrivate=1 时提交
apiKey: state.ruleForm.isPrivate === 1 ? String(state.ruleForm.apiKey ?? '').trim() : '', apiKey: state.ruleForm.isPrivate === 1 ? String(state.ruleForm.apiKey ?? '').trim() : '',
form: processedFormFields, form: processedFormFields,
@@ -1539,6 +1433,7 @@ const onSubmit = () => {
responseTokenField, responseTokenField,
tokenConfig: fieldsToUnknownObject(state.tokenConfigFields.filter((f) => String(f.key || '').trim() !== '')), tokenConfig: fieldsToUnknownObject(state.tokenConfigFields.filter((f) => String(f.key || '').trim() !== '')),
queryConfig: buildQueryConfig(), queryConfig: buildQueryConfig(),
streamConfig: Number(state.ruleForm.callMode) === 2 ? {} : undefined,
}; };
if (state.dialog.type === 'edit') { if (state.dialog.type === 'edit') {
@@ -1583,6 +1478,10 @@ onMounted(() => {
} }
} }
.async-query-status-values {
width: 100%;
}
.form-config-container { .form-config-container {
max-height: 550px; max-height: 550px;
overflow-y: auto; overflow-y: auto;