From eea5874dbf1aab41513b7e14eb19e28196d9659d Mon Sep 17 00:00:00 2001 From: 2910410219 <2910410219@qq.com> Date: Fri, 5 Jun 2026 13:07:00 +0800 Subject: [PATCH] =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=A8=A1=E5=9E=8B=E7=9B=B8?= =?UTF-8?q?=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../settings/modelConfig/modelModule/index.ts | 9 +- .../modelModule/component/editModule.vue | 505 +++++++----------- 2 files changed, 207 insertions(+), 307 deletions(-) diff --git a/src/api/settings/modelConfig/modelModule/index.ts b/src/api/settings/modelConfig/modelModule/index.ts index d930467..e58643c 100644 --- a/src/api/settings/modelConfig/modelModule/index.ts +++ b/src/api/settings/modelConfig/modelModule/index.ts @@ -119,8 +119,8 @@ export interface ModelModuleItem { /** 会话开关状态(列表接口返回,0 关 1 开;会话开关接口就绪后生效) */ chatSessionEnabled?: number; enabled: number; - /** 是否异步 0-同步 1-异步,依赖requestMapping */ - isAsync?: number; + /** 调用模式:0-同步 1-异步 2-流式 */ + callMode?: number; maxConcurrency: number; queueLimit: number; timeoutMs?: number; @@ -156,8 +156,8 @@ export interface CreateModelParams { isPrivate: number; enabled: number; isChatModel: number; - /** 是否异步 0-同步 1-异步,依赖requestMapping,默认0 */ - isAsync: number; + /** 调用模式:0-同步 1-异步 2-流式,默认0 */ + callMode: number; apiKey?: string; form: ModelFormEntry[]; requestMapping?: Record; @@ -167,6 +167,7 @@ export interface CreateModelParams { responseTokenField?: string; tokenConfig?: Record; queryConfig?: Record; + streamConfig?: Record; maxConcurrency?: number; queueLimit?: number; timeoutSeconds: number; diff --git a/src/views/settings/modelConfig/modelModule/component/editModule.vue b/src/views/settings/modelConfig/modelModule/component/editModule.vue index 6038b3b..fd62d27 100644 --- a/src/views/settings/modelConfig/modelModule/component/editModule.vue +++ b/src/views/settings/modelConfig/modelModule/component/editModule.vue @@ -1,4 +1,4 @@ - - - - - - - - - - - - - - - - - 配置请求头 ({{ pullConfigForm.headersFields.length }}) - - - 配置请求体 ({{ pullConfigForm.bodyFields.length }}) - - - 配置响应字段 ({{ pullConfigForm.responseFields.length }}) - - - - -
@@ -460,73 +481,9 @@
+ 添加字段 - -
- - - -
-
- - = - - 删除 -
- + 添加字段 -
- -
- - - -
-
- - = - - - - 删除 -
- + 添加字段 -
- -
- - - -
-
- - = - - {{ field.isTokenField ? '✓ 计费字段' : '设置计费字段' }} - - - {{ field.isMainBody ? '✓ 返回主体' : '设置返回主体' }} - - 删除 -
- - + 添加字段 - -
- +
@@ -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>([]); @@ -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 = {}; - 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 = 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; - }); +const buildAsyncQueryConfig = () => { + const statusValues: Record = {}; + 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) => { return Object.entries(obj).map(([key, value]) => ({ key, value: String(value ?? '') })); }; -const nestedObjectToFields = (obj: Record, 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, 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 => { 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) => { operatorName: String(row.operatorName ?? ''), baseUrl: String(row.baseUrl ?? ''), httpMethod: String(row.httpMethod || 'POST'), - queryResponseType: String((row.queryConfig as Record)?.responseType || 'sync'), - queryCallbackUrl: String((row.queryConfig as Record)?.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) => { if (row.queryConfig && typeof row.queryConfig === 'object' && !Array.isArray(row.queryConfig)) { const qc = row.queryConfig as Record; - 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) || {})); - pullConfigForm.bodyFields = ensureKeyValueRows(nestedObjectToFields((qc.body as Record) || {})); - pullConfigForm.responseFields = ((qc.response as unknown[]) || []).map((item) => { - if (typeof item === 'string') { - return { value: item, isTokenField: false, isMainBody: false }; - } - const row = item as Record; - 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) || {})); } 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) => { 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) => { 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;