From 56e1517743e2418fff84a15da0d274dda788f6ca Mon Sep 17 00:00:00 2001 From: 2910410219 <2910410219@qq.com> Date: Fri, 5 Jun 2026 15:56:44 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=A8=A1=E5=9E=8B=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=EF=BC=8C=E6=96=B0=E5=A2=9E=E5=AD=97=E6=AE=B5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=92=8C=E8=AF=B7=E6=B1=82=E6=98=A0=E5=B0=84=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96=E7=95=8C=E9=9D=A2=E5=85=83?= =?UTF-8?q?=E7=B4=A0=EF=BC=8C=E7=A7=BB=E9=99=A4=E5=86=97=E4=BD=99=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + src/api/settings/creation/index.ts | 1 + .../settings/modelConfig/modelModule/index.ts | 8 +- src/components/model/ModelSelector.vue | 11 +- src/views/settings/creation/index.vue | 16 +- .../modelModule/component/editModule.vue | 224 +++++++++++++++--- .../modelConfig/modelModule/index.vue | 5 +- 7 files changed, 219 insertions(+), 48 deletions(-) diff --git a/.gitignore b/.gitignore index c590da0..28c7f59 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .DS_Store node_modules /dist + +# Vue language service / accidental transpile artifacts src/**/*.vue.js diff --git a/src/api/settings/creation/index.ts b/src/api/settings/creation/index.ts index 3655fbc..b1b401e 100644 --- a/src/api/settings/creation/index.ts +++ b/src/api/settings/creation/index.ts @@ -88,6 +88,7 @@ export interface ExecutionItem { timestamp: string; content: string; label: string; + type?: string; } export interface ExecutionFlowItem { diff --git a/src/api/settings/modelConfig/modelModule/index.ts b/src/api/settings/modelConfig/modelModule/index.ts index e58643c..09ffd5f 100644 --- a/src/api/settings/modelConfig/modelModule/index.ts +++ b/src/api/settings/modelConfig/modelModule/index.ts @@ -129,10 +129,12 @@ export interface ModelModuleItem { retryTimes: number; retryQueueMaxSeconds: number; autoCleanSeconds: number; - remark?: string; headMsg?: string | Record; form?: ModelFormEntry[] | Record; requestMapping?: Record; + requiredFields?: string[]; + firstFrame?: string; + lastFrame?: string; responseMapping?: Record; } @@ -161,6 +163,9 @@ export interface CreateModelParams { apiKey?: string; form: ModelFormEntry[]; requestMapping?: Record; + requiredFields?: string[]; + firstFrame?: string; + lastFrame?: string; responseMapping?: Record; responseBody?: Record; extendMapping?: Record; @@ -175,7 +180,6 @@ export interface CreateModelParams { retryTimes?: number; retryQueueMaxSeconds: number; autoCleanSeconds: number; - remark?: string; } export interface ModelConfigTypeItem { diff --git a/src/components/model/ModelSelector.vue b/src/components/model/ModelSelector.vue index 68d9860..68dc0df 100644 --- a/src/components/model/ModelSelector.vue +++ b/src/components/model/ModelSelector.vue @@ -444,10 +444,13 @@ const handleCreatePrivateModel = async () => { isPrivate: builtInModel.isPrivate ?? 1, enabled: builtInModel.enabled ?? 1, isChatModel: builtInModel.isChatModel || 0, - isAsync: builtInModel.isAsync ?? 0, + callMode: builtInModel.callMode ?? builtInModel.isAsync ?? 0, apiKey: apiKeyForm.apiKey, form: formList, requestMapping: (builtInModel.requestMapping as Record) || {}, + requiredFields: Array.isArray(builtInModel.requiredFields) ? builtInModel.requiredFields : [], + firstFrame: String(builtInModel.firstFrame || ''), + lastFrame: String(builtInModel.lastFrame || ''), responseMapping: (builtInModel.responseMapping as Record) || {}, responseBody: builtInModel.responseBody || {}, maxConcurrency: builtInModel.maxConcurrency || 10, @@ -457,8 +460,6 @@ const handleCreatePrivateModel = async () => { retryTimes: builtInModel.retryTimes || 3, retryQueueMaxSeconds: builtInModel.retryQueueMaxSeconds || 60, autoCleanSeconds: builtInModel.autoCleanSeconds || 300, - remark: builtInModel.remark || '', - extendMapping: fieldsToUnknownObject( Object.entries(parseJsonObjectField(builtInModel.extendMapping)).map(([k, v]) => ({ key: k, value: String(v ?? '') })) ), @@ -470,6 +471,10 @@ const handleCreatePrivateModel = async () => { ? (builtInModel.queryConfig as Record) : null ), + streamConfig: + builtInModel.streamConfig && typeof builtInModel.streamConfig === 'object' && !Array.isArray(builtInModel.streamConfig) + ? (builtInModel.streamConfig as Record) + : undefined, }; const res: any = await addModelModule(createParams); diff --git a/src/views/settings/creation/index.vue b/src/views/settings/creation/index.vue index f2d32cc..1469266 100644 --- a/src/views/settings/creation/index.vue +++ b/src/views/settings/creation/index.vue @@ -1558,9 +1558,13 @@ const handleCreateChatModelFromBuiltIn = async () => { isPrivate: builtInModel.isPrivate ?? 1, enabled: builtInModel.enabled ?? 1, isChatModel: 1, // 设置为会话模型 + callMode: builtInModel.callMode ?? builtInModel.isAsync ?? 0, apiKey: chatModelApiKeyForm.apiKey, form: builtInModel.form || {}, requestMapping: builtInModel.requestMapping || {}, + requiredFields: Array.isArray(builtInModel.requiredFields) ? builtInModel.requiredFields : [], + firstFrame: String(builtInModel.firstFrame || ''), + lastFrame: String(builtInModel.lastFrame || ''), responseMapping: builtInModel.responseMapping || {}, responseBody: builtInModel.responseBody || {}, tokenMapping: builtInModel.tokenMapping || '', @@ -1572,7 +1576,6 @@ const handleCreateChatModelFromBuiltIn = async () => { retryTimes: builtInModel.retryTimes || 3, retryQueueMaxSeconds: builtInModel.retryQueueMaxSeconds || 60, autoCleanSeconds: builtInModel.autoCleanSeconds || 300, - remark: builtInModel.remark || '', }; await addModelModule(createParams); @@ -3799,10 +3802,13 @@ const cleanupReferencesToNode = (deletedNodeId: string) => { inputSource: normalizedInputSource, }); - if (selectedElement.value?.id === node.id) { - selectedElement.value.properties = { - ...props, - inputSource: normalizedInputSource, + if (selectedElement.value?.id === node.id && selectedElement.value) { + selectedElement.value = { + ...selectedElement.value, + properties: { + ...props, + inputSource: normalizedInputSource, + }, }; } }); diff --git a/src/views/settings/modelConfig/modelModule/component/editModule.vue b/src/views/settings/modelConfig/modelModule/component/editModule.vue index fd62d27..f4f4ac9 100644 --- a/src/views/settings/modelConfig/modelModule/component/editModule.vue +++ b/src/views/settings/modelConfig/modelModule/component/editModule.vue @@ -23,13 +23,6 @@ - - - - - - - @@ -45,6 +38,9 @@ + +
服务商模型配置
+
@@ -53,6 +49,13 @@ + + + + + + + @@ -103,11 +106,6 @@ - - - - - @@ -405,12 +403,21 @@ - +
- + = - + + + {{ field.required ? '✓ 必填字段' : '设置必填字段' }} + + + {{ field.isFirstFrame ? '✓ 首帧参数' : '设置首帧参数' }} + + + {{ field.isLastFrame ? '✓ 尾帧参数' : '设置尾帧参数' }} + 删除
+ 添加字段 @@ -436,7 +443,7 @@ > = - + {{ field.isTokenField ? '✓ 计费字段' : '设置计费字段' }} @@ -531,6 +538,22 @@ export interface FormField { options?: FormFieldOption[]; // 选项列表 } +export interface KeyValueField { + key: string; + value: string; +} + +export interface RequestMappingField extends KeyValueField { + required?: boolean; + isFirstFrame?: boolean; + isLastFrame?: boolean; +} + +export interface ResponseMappingField extends KeyValueField { + isMainBody?: boolean; + isTokenField?: boolean; +} + const props = withDefaults( defineProps<{ modelTypes?: ModelTypeOption[]; @@ -597,6 +620,9 @@ const state = reactive({ enabled: 1, isChatModel: 0, callMode: 0, + firstFrame: '', + lastFrame: '', + requiredFields: [] as string[], maxConcurrency: 10, queueLimit: 100, timeoutSeconds: 30, @@ -604,7 +630,6 @@ const state = reactive({ retryTimes: 3, retryQueueMaxSeconds: 60, autoCleanSeconds: 300, - remark: '', extendMapping: '{}', responseTokenField: '', tokenConfig: '{}', @@ -678,7 +703,22 @@ const state = reactive({ trigger: 'change', }, ], - operatorName: [{ required: true, message: '请选择运营商名称', trigger: 'change' }], + operatorName: [ + { + validator: (_rule: unknown, value: unknown, callback: (e?: Error) => void) => { + if (!(props.isSuperAdmin || state.ruleForm.isPrivate === 1)) { + callback(); + return; + } + if (!value || String(value).trim() === '') { + callback(new Error('请选择运营商名称')); + return; + } + callback(); + }, + trigger: 'change', + }, + ], apiKey: [ { validator: (_rule: unknown, value: unknown, callback: (e?: Error) => void) => { @@ -713,6 +753,11 @@ const state = reactive({ callback(new Error('请求映射字段名不能为空')); return; } + const duplicatedSpecialSelection = state.requestMappingFields.some((x) => x.isFirstFrame && x.isLastFrame); + if (duplicatedSpecialSelection) { + callback(new Error('同一行不能同时设置为首帧和尾帧参数')); + return; + } callback(); }, trigger: 'change', @@ -777,12 +822,12 @@ const state = reactive({ detailLoading: false, }, showAdvanced: false, - headers: [] as Array<{ key: string; value: string }>, + headers: [] as KeyValueField[], formFields: [] as Array, - 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 }>, + requestMappingFields: [] as RequestMappingField[], + responseMappingFields: [] as ResponseMappingField[], + extendMappingFields: [] as KeyValueField[], + tokenConfigFields: [] as KeyValueField[], mainBodyIndex: -1, // 记录哪一行被设置为返回主体 }); @@ -806,7 +851,7 @@ const createEmptyFormField = (): FormField => { }; // 将数组转换为对象 -const fieldsToObject = (fields: Array<{ key: string; value: string }>) => { +const fieldsToObject = (fields: KeyValueField[]) => { const obj: Record = {}; fields.forEach((f) => { if (f.key && f.key.trim()) { @@ -820,7 +865,7 @@ const fieldsToObject = (fields: Array<{ key: string; value: string }>) => { // 1. 旧格式:逗号分隔的"key1:value1,key2:value2"字符串 // 2. JSON字符串:格式化为JSON对象的字符串 // 3. 新格式:直接是Record对象 -const parseHeaders = (raw: unknown): Array<{ key: string; value: string }> => { +const parseHeaders = (raw: unknown): KeyValueField[] => { if (!raw) return []; // 如果是字符串,先尝试解析为JSON,如果失败则尝试解析为旧格式key:value,key2:value2 @@ -1051,11 +1096,78 @@ const confirmFormFields = () => { }; // 请求映射字段操作 const addRequestMappingField = () => { - state.requestMappingFields.push({ key: '', value: '' }); + state.requestMappingFields.push({ key: '', value: '', required: false, isFirstFrame: false, isLastFrame: false }); }; const removeRequestMappingField = (index: number) => { + const removed = state.requestMappingFields[index]; state.requestMappingFields.splice(index, 1); + if (removed?.required) { + state.ruleForm.requiredFields = state.ruleForm.requiredFields.filter((item) => item !== String(removed.key || '').trim()); + } + if (removed?.isFirstFrame) { + state.ruleForm.firstFrame = ''; + } + if (removed?.isLastFrame) { + state.ruleForm.lastFrame = ''; + } +}; + +const toggleRequiredField = (index: number) => { + const row = state.requestMappingFields[index]; + if (!row) return; + row.required = !row.required; + state.ruleForm.requiredFields = state.requestMappingFields + .filter((field) => field.required) + .map((field) => String(field.key || '').trim()) + .filter(Boolean); +}; + +const setFirstFrameField = (index: number) => { + state.requestMappingFields.forEach((field, i) => { + field.isFirstFrame = i === index; + if (i === index) { + field.isLastFrame = false; + } + }); + state.ruleForm.firstFrame = state.requestMappingFields[index]?.key?.trim?.() || ''; + if (state.requestMappingFields[index]?.isLastFrame) { + state.ruleForm.lastFrame = ''; + } +}; + +const setLastFrameField = (index: number) => { + state.requestMappingFields.forEach((field, i) => { + field.isLastFrame = i === index; + if (i === index) { + field.isFirstFrame = false; + } + }); + state.ruleForm.lastFrame = state.requestMappingFields[index]?.key?.trim?.() || ''; + if (state.requestMappingFields[index]?.isFirstFrame) { + state.ruleForm.firstFrame = ''; + } +}; + +const syncRequestSpecialFieldsOnKeyChange = (index: number) => { + const row = state.requestMappingFields[index]; + if (!row) return; + if (row.required) { + const nextKey = row.key?.trim?.() || ''; + state.ruleForm.requiredFields = state.requestMappingFields + .filter((field) => field.required) + .map((field) => String(field.key || '').trim()) + .filter(Boolean); + if (!nextKey) { + row.required = false; + } + } + if (row.isFirstFrame) { + state.ruleForm.firstFrame = row.key?.trim?.() || ''; + } + if (row.isLastFrame) { + state.ruleForm.lastFrame = row.key?.trim?.() || ''; + } }; const confirmRequestMappingFields = () => { @@ -1101,7 +1213,7 @@ const confirmResponseMappingFields = () => { showResponseMappingDialog.value = false; }; -const ensureKeyValueRows = (rows: Array<{ key: string; value: string }>) => (rows.length ? rows : [{ key: '', value: '' }]); +const ensureKeyValueRows = (rows: T[], createEmpty: () => T): T[] => (rows.length ? rows : [createEmpty()]); // 解析旧格式的form数据兼容到新格式 const parseFormFieldsUnified = (raw: unknown): Array => { @@ -1182,6 +1294,9 @@ const fillFormFromDetailRow = (row: Record) => { enabled: Number(row.enabled ?? 1), isChatModel: row.isChatModel !== undefined && row.isChatModel !== null ? Number(row.isChatModel) : 0, callMode: row.callMode !== undefined && row.callMode !== null ? Number(row.callMode) : row.isAsync !== undefined && row.isAsync !== null ? Number(row.isAsync) : 0, + firstFrame: String(row.firstFrame || ''), + lastFrame: String(row.lastFrame || ''), + requiredFields: Array.isArray(row.requiredFields) ? row.requiredFields.map((item) => String(item || '').trim()).filter(Boolean) : [], maxConcurrency: Number(row.maxConcurrency ?? 10), queueLimit: Number(row.queueLimit ?? 100), timeoutSeconds, @@ -1189,17 +1304,33 @@ const fillFormFromDetailRow = (row: Record) => { retryTimes: Number(row.retryTimes ?? 3), retryQueueMaxSeconds: Number(row.retryQueueMaxSeconds ?? 60), autoCleanSeconds: Number(row.autoCleanSeconds ?? 300), - remark: String(row.remark || ''), extendMapping: '{}', responseTokenField: String(row.responseTokenField || ''), tokenConfig: '{}', }; - state.headers = ensureKeyValueRows(parseHeaders(row.headMsg)); + state.headers = ensureKeyValueRows(parseHeaders(row.headMsg), () => ({ key: '', value: '' })); state.formFields = parseFormFieldsUnified(row.form); - state.requestMappingFields = ensureKeyValueRows(parseRequestMappingFields(row.requestMapping)); - state.responseMappingFields = ensureKeyValueRows(parseResponseMappingFields(row.responseMapping)); - state.extendMappingFields = ensureKeyValueRows(parseFieldsUnified(row.extendMapping)); - state.tokenConfigFields = ensureKeyValueRows(parseFieldsUnified(row.tokenConfig)); + state.requestMappingFields = ensureKeyValueRows(parseRequestMappingFields(row.requestMapping) as RequestMappingField[], () => ({ + key: '', + value: '', + required: false, + isFirstFrame: false, + isLastFrame: false, + })); + state.requestMappingFields.forEach((field) => { + const fieldKey = String(field.key || '').trim(); + field.required = fieldKey !== '' && state.ruleForm.requiredFields.includes(fieldKey); + field.isFirstFrame = fieldKey !== '' && fieldKey === String(row.firstFrame || '').trim(); + field.isLastFrame = fieldKey !== '' && fieldKey === String(row.lastFrame || '').trim(); + }); + state.responseMappingFields = ensureKeyValueRows(parseResponseMappingFields(row.responseMapping) as ResponseMappingField[], () => ({ + key: '', + value: '', + isMainBody: false, + isTokenField: false, + })); + state.extendMappingFields = ensureKeyValueRows(parseFieldsUnified(row.extendMapping), () => ({ key: '', value: '' })); + state.tokenConfigFields = ensureKeyValueRows(parseFieldsUnified(row.tokenConfig), () => ({ key: '', value: '' })); // 根据 responseTokenField 字段设置计费字段标记(单选) const tokenFieldKey = String(row.responseTokenField || '').trim(); @@ -1230,7 +1361,7 @@ const fillFormFromDetailRow = (row: Record) => { 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) || {})); + asyncQueryConfigForm.statusValueFields = ensureKeyValueRows(objectToFields((qc.status_values as Record) || {}), () => ({ key: '', value: '' })); } else { asyncQueryConfigForm.url = ''; asyncQueryConfigForm.method = 'POST'; @@ -1292,6 +1423,9 @@ const openDialog = async (type: string, row?: Record) => { enabled: 1, isChatModel: 0, callMode: 0, + firstFrame: '', + lastFrame: '', + requiredFields: [], maxConcurrency: 10, queueLimit: 100, timeoutSeconds: 30, @@ -1299,14 +1433,13 @@ const openDialog = async (type: string, row?: Record) => { retryTimes: 3, retryQueueMaxSeconds: 60, autoCleanSeconds: 300, - remark: '', extendMapping: '{}', responseTokenField: '', tokenConfig: '{}', }; state.headers = [{ key: '', value: '' }]; state.formFields = [createEmptyFormField()]; - state.requestMappingFields = [{ key: '', value: '' }]; + state.requestMappingFields = [{ key: '', value: '', required: false, isFirstFrame: false, isLastFrame: false }]; state.responseMappingFields = [{ key: '', value: '', isMainBody: false, isTokenField: false }]; state.mainBodyIndex = -1; state.extendMappingFields = [{ key: '', value: '' }]; @@ -1363,6 +1496,10 @@ const onSubmit = () => { // 过滤掉空键名的字段 const requestMapping = fieldsToObject(state.requestMappingFields.filter((f) => String(f.key || '').trim() !== '')); + const requiredFields = state.requestMappingFields + .filter((f) => Boolean(f.required) && String(f.key || '').trim() !== '') + .map((f) => String(f.key || '').trim()); + state.ruleForm.requiredFields = requiredFields; const responseMapping = fieldsToObject(state.responseMappingFields.filter((f) => String(f.key || '').trim() !== '')); // 获取被设置为返回主体的字段 {key: value} @@ -1419,6 +1556,9 @@ const onSubmit = () => { apiKey: state.ruleForm.isPrivate === 1 ? String(state.ruleForm.apiKey ?? '').trim() : '', form: processedFormFields, requestMapping, + requiredFields, + firstFrame: String(state.ruleForm.firstFrame || '').trim(), + lastFrame: String(state.ruleForm.lastFrame || '').trim(), responseMapping, responseBody, maxConcurrency: state.ruleForm.maxConcurrency, @@ -1428,7 +1568,6 @@ const onSubmit = () => { retryTimes: state.ruleForm.retryTimes, retryQueueMaxSeconds: state.ruleForm.retryQueueMaxSeconds, autoCleanSeconds: state.ruleForm.autoCleanSeconds, - remark: state.ruleForm.remark || '', extendMapping: fieldsToUnknownObject(state.extendMappingFields.filter((f) => String(f.key || '').trim() !== '')), responseTokenField, tokenConfig: fieldsToUnknownObject(state.tokenConfigFields.filter((f) => String(f.key || '').trim() !== '')), @@ -1482,6 +1621,17 @@ onMounted(() => { width: 100%; } +.provider-section-col { + margin-bottom: 0; +} + +.provider-section-title { + font-size: 13px; + font-weight: 600; + color: #606266; + padding: 2px 0 4px; +} + .form-config-container { max-height: 550px; overflow-y: auto; diff --git a/src/views/settings/modelConfig/modelModule/index.vue b/src/views/settings/modelConfig/modelModule/index.vue index 0285e0c..2074820 100644 --- a/src/views/settings/modelConfig/modelModule/index.vue +++ b/src/views/settings/modelConfig/modelModule/index.vue @@ -281,10 +281,13 @@ const handleCreatePrivateModelAndSetChat = async () => { isPrivate: builtInModel.isPrivate ?? 1, enabled: builtInModel.enabled ?? 1, isChatModel: 1, // 设置为会话模型 - isAsync: builtInModel.isAsync ?? 0, + callMode: builtInModel.callMode ?? builtInModel.isAsync ?? 0, apiKey: apiKeyForm.apiKey, form: builtInModel.form || {}, requestMapping: builtInModel.requestMapping || {}, + requiredFields: Array.isArray(builtInModel.requiredFields) ? builtInModel.requiredFields : [], + firstFrame: String(builtInModel.firstFrame || ''), + lastFrame: String(builtInModel.lastFrame || ''), responseMapping: builtInModel.responseMapping || {}, responseBody: builtInModel.responseBody || {}, tokenMapping: builtInModel.tokenMapping || '',