feat: 添加 queryConfig 字段及相关配置功能
- 在 CreateModelParams 接口中新增 queryConfig 字段,支持更灵活的查询配置。 - 更新 ModelSelector 组件,优化模型选择界面,增强用户体验。 - 在 editModule.vue 中添加响应类型选择及主动拉取配置功能,提升配置灵活性与可用性。
This commit is contained in:
@@ -127,6 +127,7 @@ export interface CreateModelParams {
|
||||
extendMapping?: Record<string, unknown>;
|
||||
responseTokenField?: string;
|
||||
tokenConfig?: Record<string, unknown>;
|
||||
queryConfig?: Record<string, unknown>;
|
||||
maxConcurrency?: number;
|
||||
queueLimit?: number;
|
||||
timeoutSeconds: number;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="search-bar">
|
||||
<el-input v-model="searchParams.modelName" placeholder="搜索模型名称" clearable @clear="handleSearch">
|
||||
<template #prefix
|
||||
><el-icon><Search /></el-icon
|
||||
><el-icon> <Search /> </el-icon
|
||||
></template>
|
||||
</el-input>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
@@ -26,7 +26,9 @@
|
||||
<div class="model-type">{{ getModelTypeName(model.modelType) }}</div>
|
||||
<div class="model-badges">
|
||||
<el-tag v-if="model.isOwner === 0" type="warning" size="small">内置模型</el-tag>
|
||||
<el-icon v-if="selectedModel?.id === model.id" class="check-icon" color="#67c23a"><CircleCheck /></el-icon>
|
||||
<el-icon v-if="selectedModel?.id === model.id" class="check-icon" color="#67c23a">
|
||||
<CircleCheck />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="model-card-body">
|
||||
@@ -121,6 +123,7 @@ interface ModelItem {
|
||||
responseBody?: Record<string, unknown>;
|
||||
tokenConfig?: Record<string, unknown> | string;
|
||||
extendMapping?: Record<string, unknown> | string;
|
||||
queryConfig?: Record<string, unknown>;
|
||||
form?: ModelFormEntry[] | Record<string, unknown>;
|
||||
requestMapping?: Record<string, unknown>;
|
||||
responseMapping?: Record<string, unknown>;
|
||||
@@ -239,6 +242,85 @@ const parseJsonObjectField = (raw: unknown): Record<string, unknown> => {
|
||||
}
|
||||
return {};
|
||||
};
|
||||
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 flattenNestedObject = (obj: Record<string, unknown>, prefix = ''): Array<{ key: string; value: string }> => {
|
||||
const rows: Array<{ key: string; value: string }> = [];
|
||||
Object.entries(obj || {}).forEach(([k, v]) => {
|
||||
const fk = prefix ? `${prefix}.${k}` : k;
|
||||
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
||||
rows.push(...flattenNestedObject(v as Record<string, unknown>, fk));
|
||||
return;
|
||||
}
|
||||
rows.push({ key: fk, value: String(v ?? '') });
|
||||
});
|
||||
return rows;
|
||||
};
|
||||
|
||||
const nestFieldsToObject = (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 buildQueryConfigFromRaw = (rawQc: Record<string, unknown> | null): Record<string, unknown> => {
|
||||
if (!rawQc) return { responseType: 'sync', callbackUrl: '' };
|
||||
const rt = String(rawQc.responseType || 'sync');
|
||||
if (rt === 'callback') return { responseType: 'callback', callbackUrl: String(rawQc.callbackUrl || '') };
|
||||
if (rt === 'pull') {
|
||||
const hFields = Object.entries((rawQc.headers as Record<string, unknown>) || {}).map(([k, v]) => ({ key: k, value: String(v ?? '') }));
|
||||
const bFields = flattenNestedObject((rawQc.body as Record<string, unknown>) || {});
|
||||
return {
|
||||
responseType: 'pull',
|
||||
callbackUrl: '',
|
||||
method: String(rawQc.method || 'GET'),
|
||||
url: String(rawQc.url || ''),
|
||||
headers: fieldsToUnknownObject(hFields),
|
||||
body: nestFieldsToObject(bFields),
|
||||
response: ((rawQc.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),
|
||||
};
|
||||
})
|
||||
.filter((row) => row.value !== ''),
|
||||
};
|
||||
}
|
||||
return { responseType: 'sync', callbackUrl: '' };
|
||||
};
|
||||
|
||||
const fetchModelList = async () => {
|
||||
loading.value = true;
|
||||
@@ -325,8 +407,17 @@ const handleCreatePrivateModel = async () => {
|
||||
autoCleanSeconds: builtInModel.autoCleanSeconds || 300,
|
||||
remark: builtInModel.remark || '',
|
||||
|
||||
extendMapping: parseJsonObjectField(builtInModel.extendMapping),
|
||||
tokenConfig: parseJsonObjectField(builtInModel.tokenConfig),
|
||||
extendMapping: fieldsToUnknownObject(
|
||||
Object.entries(parseJsonObjectField(builtInModel.extendMapping)).map(([k, v]) => ({ key: k, value: String(v ?? '') }))
|
||||
),
|
||||
tokenConfig: fieldsToUnknownObject(
|
||||
Object.entries(parseJsonObjectField(builtInModel.tokenConfig)).map(([k, v]) => ({ key: k, value: String(v ?? '') }))
|
||||
),
|
||||
queryConfig: buildQueryConfigFromRaw(
|
||||
builtInModel.queryConfig && typeof builtInModel.queryConfig === 'object' && !Array.isArray(builtInModel.queryConfig)
|
||||
? (builtInModel.queryConfig as Record<string, unknown>)
|
||||
: null
|
||||
),
|
||||
};
|
||||
|
||||
const res: any = await addModelModule(createParams);
|
||||
|
||||
@@ -45,6 +45,23 @@
|
||||
</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">
|
||||
@@ -152,26 +169,16 @@
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
|
||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||
<el-form-item label="附加映射" prop="extendMapping">
|
||||
<el-input
|
||||
v-model="state.ruleForm.extendMapping"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder='请输入 JSON 对象,例如:{"\"foo\": \"bar\"}'
|
||||
clearable
|
||||
></el-input>
|
||||
<el-button @click="showExtendMappingDialog = true" style="width: 100%"
|
||||
>配置附加映射 ({{ state.extendMappingFields.length }})</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
|
||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||
<el-form-item label="Token计算配置" prop="tokenConfig">
|
||||
<el-input
|
||||
v-model="state.ruleForm.tokenConfig"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder='请输入 JSON 对象,例如:{\"promptRate\": 1, \"completionRate\": 1}'
|
||||
clearable
|
||||
></el-input>
|
||||
<el-button @click="showTokenConfigDialog = true" style="width: 100%">配置Token计算 ({{ state.tokenConfigFields.length }})</el-button>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -272,6 +279,134 @@
|
||||
</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>
|
||||
|
||||
@@ -303,6 +438,9 @@ 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 }>>([]);
|
||||
|
||||
@@ -329,6 +467,31 @@ 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: '',
|
||||
@@ -337,6 +500,8 @@ const state = reactive({
|
||||
operatorName: '',
|
||||
baseUrl: '',
|
||||
httpMethod: 'POST',
|
||||
queryResponseType: 'sync',
|
||||
queryCallbackUrl: '',
|
||||
headMsg: '',
|
||||
isPrivate: 0,
|
||||
apiKey: '',
|
||||
@@ -370,6 +535,39 @@ 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: [
|
||||
{
|
||||
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;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
operatorName: [{ required: true, message: '请选择运营商名称', trigger: 'change' }],
|
||||
apiKey: [
|
||||
{
|
||||
@@ -399,44 +597,26 @@ const state = reactive({
|
||||
expectedSeconds: [{ required: true, message: '请输入预计执行时间', trigger: 'blur' }],
|
||||
tokenConfig: [
|
||||
{
|
||||
validator: (_rule: unknown, value: unknown, callback: (e?: Error) => void) => {
|
||||
if (value === undefined || value === null || String(value).trim() === '') {
|
||||
callback();
|
||||
validator: (_rule: unknown, _value: unknown, callback: (e?: Error) => void) => {
|
||||
if (!state.tokenConfigFields.length || state.tokenConfigFields.some((x) => !String(x.key || '').trim())) {
|
||||
callback(new Error('Token计算配置字段名不能为空'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(String(value));
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
callback(new Error('Token计算配置必须为 JSON 对象'));
|
||||
} catch {
|
||||
callback(new Error('Token计算配置 JSON 格式不正确'));
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
extendMapping: [
|
||||
{
|
||||
validator: (_rule: unknown, value: unknown, callback: (e?: Error) => void) => {
|
||||
if (value === undefined || value === null || String(value).trim() === '') {
|
||||
callback();
|
||||
validator: (_rule: unknown, _value: unknown, callback: (e?: Error) => void) => {
|
||||
if (state.extendMappingFields.some((x) => !String(x.key || '').trim())) {
|
||||
callback(new Error('附加映射字段名不能为空'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(String(value));
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
callback(new Error('附加映射必须为 JSON 对象'));
|
||||
} catch {
|
||||
callback(new Error('附加映射 JSON 格式不正确'));
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -453,6 +633,8 @@ const state = reactive({
|
||||
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, // 记录哪一行被设置为返回主体
|
||||
});
|
||||
|
||||
@@ -531,16 +713,87 @@ const onIsPrivateChange = (val: string | number | boolean | undefined) => {
|
||||
}
|
||||
};
|
||||
|
||||
const parseJsonObjectField = (raw: string, fallback: Record<string, unknown>) => {
|
||||
try {
|
||||
const o = JSON.parse(raw || '{}');
|
||||
if (o && typeof o === 'object' && !Array.isArray(o)) {
|
||||
return o as Record<string, unknown>;
|
||||
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;
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
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(),
|
||||
};
|
||||
}
|
||||
return fallback;
|
||||
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: '',
|
||||
};
|
||||
};
|
||||
|
||||
// 添加请求头
|
||||
@@ -623,6 +876,17 @@ 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: '' }]);
|
||||
|
||||
@@ -665,6 +929,8 @@ 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: String(row.headMsg || ''),
|
||||
isPrivate,
|
||||
apiKey: isPrivate === 1 ? String(row.apiKey ?? '') : '',
|
||||
@@ -678,16 +944,17 @@ const fillFormFromDetailRow = (row: Record<string, unknown>) => {
|
||||
retryQueueMaxSeconds: Number(row.retryQueueMaxSeconds ?? 60),
|
||||
autoCleanSeconds: Number(row.autoCleanSeconds ?? 300),
|
||||
remark: String(row.remark || ''),
|
||||
extendMapping:
|
||||
typeof row.extendMapping === 'string' ? row.extendMapping : JSON.stringify((row.extendMapping as Record<string, unknown>) || {}, null, 2),
|
||||
extendMapping: '{}',
|
||||
responseTokenField: String(row.responseTokenField || ''),
|
||||
tokenConfig: typeof row.tokenConfig === 'string' ? row.tokenConfig : JSON.stringify((row.tokenConfig as Record<string, unknown>) || {}, null, 2),
|
||||
tokenConfig: '{}',
|
||||
};
|
||||
state.headers = ensureKeyValueRows(parseHeaders(String(row.headMsg || '')));
|
||||
state.formFields = ensureKeyValueRows(parseFormFields(row.form));
|
||||
// 解析请求映射和响应映射
|
||||
state.requestMappingFields = ensureKeyValueRows(parseRequestMappingFields(row.requestMapping));
|
||||
state.responseMappingFields = ensureResponseMappingRows(parseResponseMappingFields(row.responseMapping));
|
||||
state.extendMappingFields = ensureKeyValueRows(objectToFields((row.extendMapping as Record<string, unknown>) || {}));
|
||||
state.tokenConfigFields = ensureKeyValueRows(objectToFields((row.tokenConfig as Record<string, unknown>) || {}));
|
||||
|
||||
// 根据 responseTokenField 字段设置计费字段标记(单选)
|
||||
const tokenFieldKey = String(row.responseTokenField || '').trim();
|
||||
@@ -697,18 +964,43 @@ const fillFormFromDetailRow = (row: Record<string, unknown>) => {
|
||||
state.responseMappingFields[tokenFieldIndex].isTokenField = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 根据 responseBody 字段设置返回主体标记 (responseBody 是对象 {key: value})
|
||||
if (row.responseBody && typeof row.responseBody === 'object') {
|
||||
const responseBodyKey = Object.keys(row.responseBody)[0];
|
||||
if (responseBodyKey) {
|
||||
const mainBodyIndex = state.responseMappingFields.findIndex((f) => f.key === responseBodyKey);
|
||||
if (mainBodyIndex !== -1) {
|
||||
state.responseMappingFields[mainBodyIndex].isMainBody = true;
|
||||
state.mainBodyIndex = mainBodyIndex;
|
||||
// 根据 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>) => {
|
||||
@@ -755,6 +1047,8 @@ const openDialog = async (type: string, row?: Record<string, unknown>) => {
|
||||
operatorName: '',
|
||||
baseUrl: '',
|
||||
httpMethod: 'POST',
|
||||
queryResponseType: 'sync',
|
||||
queryCallbackUrl: '',
|
||||
headMsg: '',
|
||||
isPrivate: props.isSuperAdmin ? 1 : 0,
|
||||
apiKey: '',
|
||||
@@ -776,6 +1070,14 @@ const openDialog = async (type: string, row?: Record<string, unknown>) => {
|
||||
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 = '新 增';
|
||||
}
|
||||
@@ -800,6 +1102,9 @@ const onSubmit = () => {
|
||||
|
||||
state.dialog.loading = true;
|
||||
try {
|
||||
if (state.ruleForm.queryResponseType === 'pull') {
|
||||
await editModuleFormRef.value?.validateField?.('queryPullConfig');
|
||||
}
|
||||
state.ruleForm.headMsg = stringifyHeaders();
|
||||
const requestMapping = fieldsToObject(state.requestMappingFields);
|
||||
const responseMapping = fieldsToObject(state.responseMappingFields);
|
||||
@@ -833,9 +1138,10 @@ const onSubmit = () => {
|
||||
retryQueueMaxSeconds: state.ruleForm.retryQueueMaxSeconds,
|
||||
autoCleanSeconds: state.ruleForm.autoCleanSeconds,
|
||||
remark: state.ruleForm.remark || '',
|
||||
extendMapping: parseJsonObjectField(state.ruleForm.extendMapping || '{}', {}),
|
||||
extendMapping: fieldsToUnknownObject(state.extendMappingFields),
|
||||
responseTokenField,
|
||||
tokenConfig: parseJsonObjectField(state.ruleForm.tokenConfig || '{}', {}),
|
||||
tokenConfig: fieldsToUnknownObject(state.tokenConfigFields),
|
||||
queryConfig: buildQueryConfig(),
|
||||
};
|
||||
|
||||
if (state.dialog.type === 'edit') {
|
||||
@@ -879,6 +1185,7 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-config-container {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
@@ -890,6 +1197,7 @@ onMounted(() => {
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ml5 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user