配置模型相关

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

@@ -1,4 +1,4 @@
<template>
<template>
<div class="system-edit-module-container">
<el-dialog :title="state.dialog.title" v-model="state.dialog.isShowDialog" width="900px">
<el-form
@@ -45,23 +45,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="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">
@@ -84,11 +67,22 @@
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="执行模式" prop="isAsync">
<el-radio-group v-model="state.ruleForm.isAsync">
<el-radio :label="0">同步</el-radio>
<el-radio :label="1">异步</el-radio>
</el-radio-group>
<el-form-item label="执行模式" prop="callMode">
<el-select v-model="state.ruleForm.callMode" placeholder="请选择调用模式" clearable style="width: 100%">
<el-option label="同步" :value="0"></el-option>
<el-option label="异步" :value="1"></el-option>
<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-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
@@ -97,7 +91,7 @@
</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-form-item label="自定义表单" prop="form">
<el-button @click="showFormDialog = true" style="width: 100%"> 配置表单字段 ({{ state.formFields.length }}) </el-button>
</el-form-item>
</el-col>
@@ -202,6 +196,65 @@
</template>
</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">
<div class="header-config-container">
@@ -401,38 +454,6 @@
</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">
@@ -460,73 +481,9 @@
</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
>
<template #footer>
<span class="dialog-footer"><el-button @click="showTokenConfigDialog = false"> </el-button></span>
</template>
</el-dialog>
</div>
</template>
@@ -555,23 +512,23 @@ export interface FormFieldOption {
// 定义自定义字段类型
export interface FormField {
label: string; // 字段描述
key: string; // 字段名称
type: 'string' | 'number' | 'select' | 'radio' | 'file'; // 字段类型
defaultValue: string; // 默认值
required: boolean; // 是否必填
label: string; // 字段描述
key: string; // 字段名称
type: 'string' | 'number' | 'select' | 'radio' | 'file'; // 字段类型
defaultValue: string; // 默认值
required: boolean; // 是否必填
// 字符串配置
maxLength?: number; // 最大长度
maxLength?: number; // 最大长度
// 数字配置
min?: number; // 最小值
max?: number; // 最大值
min?: number; // 最小值
max?: number; // 最大值
numberType?: 'any' | 'integer' | 'float' | 'positive-int' | 'positive-float' | 'negative-int' | 'negative-float'; // 数字子类型
// 文件上传配置
maxSize?: number; // 最大文件大小(MB)
maxCount?: number; // 最大上传数量
allowedTypes?: string; // 允许的文件格式,逗号分隔
maxSize?: number; // 最大文件大小(MB)
maxCount?: number; // 最大上传数量
allowedTypes?: string; // 允许的文件格式,逗号分隔
// 下拉框/单选框配置
options?: FormFieldOption[]; // 选项列表
options?: FormFieldOption[]; // 选项列表
}
const props = withDefaults(
@@ -586,9 +543,6 @@ const props = withDefaults(
);
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 }>>([]);
@@ -612,34 +566,22 @@ const typeOptionValue = (id: number | string): number | string => {
const editModuleFormRef = ref();
const emit = defineEmits(['refresh']);
const showHeaderDialog = ref(false);
const showAsyncQueryConfigDialog = ref(false);
const showStreamConfigDialog = 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 asyncQueryConfigForm = reactive({
url: '',
method: 'POST',
taskId: '',
resultPath: '',
statusPath: '',
intervalSeconds: 2,
statusValueFields: [{ key: '', value: '' }] as Array<{ key: string; value: string }>,
});
const queryResponseTypeOptions = [
{ label: '同步返回', value: 'sync' },
{ label: '等候回调', value: 'callback' },
{ label: '主动拉取', value: 'pull' },
];
const state = reactive({
ruleForm: {
@@ -649,14 +591,12 @@ const state = reactive({
operatorName: '',
baseUrl: '',
httpMethod: 'POST',
queryResponseType: 'sync',
queryCallbackUrl: '',
headMsg: '',
isPrivate: 0,
apiKey: '',
enabled: 1,
isChatModel: 0,
isAsync: 0, // 0-同步 1-异步默认0
callMode: 0,
maxConcurrency: 10,
queueLimit: 100,
timeoutSeconds: 30,
@@ -685,44 +625,52 @@ const state = reactive({
],
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: [
callMode: [{ required: true, message: '请选择调用模式', trigger: 'change' }],
asyncQueryConfig: [
{
validator: (_rule: unknown, _value: unknown, callback: (e?: Error) => void) => {
if (state.ruleForm.queryResponseType !== 'pull') {
if (Number(state.ruleForm.callMode) !== 1) {
callback();
return;
}
if (!pullConfigForm.url || String(pullConfigForm.url).trim() === '') {
callback(new Error('主动拉取时,请填写拉取地址'));
if (!String(asyncQueryConfigForm.url || '').trim()) {
callback(new Error('异步执行时,请填写查询地址'));
return;
}
// 验证响应字段至少有一个有效字段
const validResponseFields = pullConfigForm.responseFields.filter((f) => String(f.value || '').trim() !== '');
if (validResponseFields.length === 0) {
callback(new Error('主动拉取时,至少需要配置一个响应字段'));
if (!String(asyncQueryConfigForm.method || '').trim()) {
callback(new Error('异步执行时,请选择请求方式'));
return;
}
// 验证是否设置了返回主体字段
const hasMainBody = pullConfigForm.responseFields.some((f) => f.isMainBody && String(f.value || '').trim() !== '');
if (!hasMainBody) {
callback(new Error('主动拉取时,必须设置一个返回主体字段'));
if (!String(asyncQueryConfigForm.taskId || '').trim()) {
callback(new Error('异步执行时请填写任务ID路径'));
return;
}
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;
}
callback();
@@ -1012,77 +960,41 @@ const fieldsToUnknownObject = (fields: Array<{ key: string; value: string }>) =>
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>;
});
const buildAsyncQueryConfig = () => {
const statusValues: Record<string, string> = {};
asyncQueryConfigForm.statusValueFields.forEach((item) => {
const key = String(item.key || '').trim();
if (!key) return;
statusValues[key] = String(item.value ?? '');
});
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>) => {
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 (Number(state.ruleForm.callMode) === 1) {
return buildAsyncQueryConfig();
}
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: '',
};
return {};
};
// 添加请求头
@@ -1188,17 +1100,6 @@ const setMainBody = (index: number) => {
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: '' }]);
@@ -1210,7 +1111,7 @@ const parseFormFieldsUnified = (raw: unknown): Array<FormField> => {
if (Array.isArray(raw)) {
if (raw.length === 0) return [createEmptyFormField()];
// 确保新增字段有默认值
return (raw as any[]).map(item => {
return (raw as any[]).map((item) => {
const field = createEmptyFormField();
return { ...field, ...item };
}) as FormField[];
@@ -1275,14 +1176,12 @@ const fillFormFromDetailRow = (row: Record<string, unknown>) => {
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: ruleFormHeadMsg,
isPrivate,
apiKey: isPrivate === 1 ? String(row.apiKey ?? '') : '',
enabled: Number(row.enabled ?? 1),
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),
queueLimit: Number(row.queueLimit ?? 100),
timeoutSeconds,
@@ -1325,27 +1224,21 @@ const fillFormFromDetailRow = (row: Record<string, unknown>) => {
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),
};
});
asyncQueryConfigForm.url = String(qc.url || '');
asyncQueryConfigForm.method = String(qc.method || 'POST') || 'POST';
asyncQueryConfigForm.taskId = String(qc.task_id || '');
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>) || {}));
} else {
pullConfigForm.method = 'GET';
pullConfigForm.url = '';
pullConfigForm.headersFields = [{ key: '', value: '' }];
pullConfigForm.bodyFields = [{ key: '', value: '' }];
pullConfigForm.responseFields = [{ value: '', isTokenField: false, isMainBody: false }];
asyncQueryConfigForm.url = '';
asyncQueryConfigForm.method = 'POST';
asyncQueryConfigForm.taskId = '';
asyncQueryConfigForm.resultPath = '';
asyncQueryConfigForm.statusPath = '';
asyncQueryConfigForm.intervalSeconds = 2;
asyncQueryConfigForm.statusValueFields = [{ key: '', value: '' }];
}
};
// 打开弹窗(编辑时会请求 /model/getModel 详情)
@@ -1393,14 +1286,12 @@ const openDialog = async (type: string, row?: Record<string, unknown>) => {
operatorName: '',
baseUrl: '',
httpMethod: 'POST',
queryResponseType: 'sync',
queryCallbackUrl: '',
headMsg: '',
isPrivate: props.isSuperAdmin ? 1 : 0,
apiKey: '',
enabled: 1,
isChatModel: 0,
isAsync: 0,
callMode: 0,
maxConcurrency: 10,
queueLimit: 100,
timeoutSeconds: 30,
@@ -1420,11 +1311,13 @@ const openDialog = async (type: string, row?: Record<string, unknown>) => {
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 }];
asyncQueryConfigForm.url = '';
asyncQueryConfigForm.method = 'POST';
asyncQueryConfigForm.taskId = '';
asyncQueryConfigForm.resultPath = '';
asyncQueryConfigForm.statusPath = '';
asyncQueryConfigForm.intervalSeconds = 2;
asyncQueryConfigForm.statusValueFields = [{ key: '', value: '' }];
state.dialog.title = '新增模型配置';
state.dialog.submitTxt = '新 增';
}
@@ -1450,8 +1343,8 @@ const onSubmit = () => {
state.dialog.loading = true;
try {
// 触发所有自定义字段的验证
if (state.ruleForm.queryResponseType === 'pull') {
await editModuleFormRef.value?.validateField?.('queryPullConfig');
if (Number(state.ruleForm.callMode) === 1) {
await editModuleFormRef.value?.validateField?.('asyncQueryConfig');
}
// 验证响应映射(如果有配置)
@@ -1497,9 +1390,10 @@ const onSubmit = () => {
maxSize: f.type === 'file' && f.maxSize ? f.maxSize : undefined,
maxCount: f.type === 'file' && f.maxCount ? f.maxCount : undefined,
allowedTypes: f.type === 'file' && f.allowedTypes ? f.allowedTypes.trim() : undefined,
options: (f.type === 'select' || f.type === 'radio') && f.options
? f.options.filter(opt => String(opt.label || '').trim() !== '' || String(opt.value || '').trim() !== '')
: undefined,
options:
(f.type === 'select' || f.type === 'radio') && f.options
? f.options.filter((opt) => String(opt.label || '').trim() !== '' || String(opt.value || '').trim() !== '')
: undefined,
})) as unknown as ModelFormEntry[];
// 将headers转换为JSON对象格式
@@ -1520,7 +1414,7 @@ const onSubmit = () => {
isPrivate: state.ruleForm.isPrivate,
enabled: state.ruleForm.enabled,
isChatModel: state.ruleForm.isChatModel,
isAsync: state.ruleForm.isAsync,
callMode: state.ruleForm.callMode,
// 确保 API 密钥只在 isPrivate=1 时提交
apiKey: state.ruleForm.isPrivate === 1 ? String(state.ruleForm.apiKey ?? '').trim() : '',
form: processedFormFields,
@@ -1539,6 +1433,7 @@ const onSubmit = () => {
responseTokenField,
tokenConfig: fieldsToUnknownObject(state.tokenConfigFields.filter((f) => String(f.key || '').trim() !== '')),
queryConfig: buildQueryConfig(),
streamConfig: Number(state.ruleForm.callMode) === 2 ? {} : undefined,
};
if (state.dialog.type === 'edit') {
@@ -1583,6 +1478,10 @@ onMounted(() => {
}
}
.async-query-status-values {
width: 100%;
}
.form-config-container {
max-height: 550px;
overflow-y: auto;