diff --git a/src/api/settings/modelConfig/modelModule/index.ts b/src/api/settings/modelConfig/modelModule/index.ts index a706c79..993edb6 100644 --- a/src/api/settings/modelConfig/modelModule/index.ts +++ b/src/api/settings/modelConfig/modelModule/index.ts @@ -127,6 +127,7 @@ export interface CreateModelParams { extendMapping?: Record; responseTokenField?: string; tokenConfig?: Record; + queryConfig?: Record; maxConcurrency?: number; queueLimit?: number; timeoutSeconds: number; diff --git a/src/components/model/ModelSelector.vue b/src/components/model/ModelSelector.vue index 7751e58..e83de2e 100644 --- a/src/components/model/ModelSelector.vue +++ b/src/components/model/ModelSelector.vue @@ -4,7 +4,7 @@
@@ -121,6 +123,7 @@ interface ModelItem { responseBody?: Record; tokenConfig?: Record | string; extendMapping?: Record | string; + queryConfig?: Record; form?: ModelFormEntry[] | Record; requestMapping?: Record; responseMapping?: Record; @@ -239,6 +242,85 @@ const parseJsonObjectField = (raw: unknown): Record => { } return {}; }; +const fieldsToUnknownObject = (fields: Array<{ key: string; value: string }>) => { + const obj: Record = {}; + fields.forEach((f) => { + const k = String(f.key || '').trim(); + if (!k) return; + obj[k] = String(f.value ?? ''); + }); + return obj; +}; + +const flattenNestedObject = (obj: Record, 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, fk)); + return; + } + rows.push({ key: fk, value: String(v ?? '') }); + }); + return rows; +}; + +const nestFieldsToObject = (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; + }); + }); + return root; +}; + +const buildQueryConfigFromRaw = (rawQc: Record | null): Record => { + 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) || {}).map(([k, v]) => ({ key: k, value: String(v ?? '') })); + const bFields = flattenNestedObject((rawQc.body as Record) || {}); + 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; + 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) + : null + ), }; const res: any = await addModelModule(createParams); diff --git a/src/views/settings/modelConfig/modelModule/component/editModule.vue b/src/views/settings/modelConfig/modelModule/component/editModule.vue index fb95d0a..0d75179 100644 --- a/src/views/settings/modelConfig/modelModule/component/editModule.vue +++ b/src/views/settings/modelConfig/modelModule/component/editModule.vue @@ -45,6 +45,23 @@ + + + + + + + + + + + + + + + 配置主动拉取 + + @@ -152,26 +169,16 @@ - + - + 配置附加映射 ({{ state.extendMappingFields.length }}) - + - + 配置Token计算 ({{ state.tokenConfigFields.length }}) @@ -272,6 +279,134 @@ + + + + + + + + + + + + + + + + + 配置请求头 ({{ pullConfigForm.headersFields.length }}) + + + 配置请求体 ({{ pullConfigForm.bodyFields.length }}) + + + 配置响应字段 ({{ pullConfigForm.responseFields.length }}) + + + + + + + +
+
+ + = + + 删除 +
+ + 添加字段 +
+ +
+ + + +
+
+ + = + + 删除 +
+ + 添加字段 +
+ +
+ + + +
+
+ + = + + 删除 +
+ + 添加字段 +
+ +
+ + + +
+
+ + = + + + + 删除 +
+ + 添加字段 +
+ +
+ + + +
+
+ + = + + {{ field.isTokenField ? '✓ 计费字段' : '设置计费字段' }} + + + {{ field.isMainBody ? '✓ 返回主体' : '设置返回主体' }} + + 删除 +
+ + + 添加字段 + +
+ +
@@ -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>([]); @@ -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) => { - try { - const o = JSON.parse(raw || '{}'); - if (o && typeof o === 'object' && !Array.isArray(o)) { - return o as Record; +const fieldsToUnknownObject = (fields: Array<{ key: string; value: string }>) => { + const obj: Record = {}; + 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 = {}; + 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; + }); + }); + return root; +}; + +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; } - } 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) => { 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: String(row.headMsg || ''), isPrivate, apiKey: isPrivate === 1 ? String(row.apiKey ?? '') : '', @@ -678,16 +944,17 @@ const fillFormFromDetailRow = (row: Record) => { 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) || {}, null, 2), + extendMapping: '{}', responseTokenField: String(row.responseTokenField || ''), - tokenConfig: typeof row.tokenConfig === 'string' ? row.tokenConfig : JSON.stringify((row.tokenConfig as Record) || {}, 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) || {})); + state.tokenConfigFields = ensureKeyValueRows(objectToFields((row.tokenConfig as Record) || {})); // 根据 responseTokenField 字段设置计费字段标记(单选) const tokenFieldKey = String(row.responseTokenField || '').trim(); @@ -697,18 +964,43 @@ const fillFormFromDetailRow = (row: Record) => { 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; + 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), + }; + }); + } 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) => { @@ -755,6 +1047,8 @@ const openDialog = async (type: string, row?: Record) => { 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) => { 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; }