import JSON5 from 'json5'; import { ContentType, FieldPosition, ParameterPlaceholder, ParameterPlaceholderText, ParameterType, RequestMethod, RequestProtocol, ResponseContentType, } from '@/constants/itfxEnums'; import { isFormUrlEncodedContentType, isJsonOrXmlContentType, isNoneContentType, normalizeContentType, } from '@/utils/interfaceParameterModel'; import { defaultResolveParameterType, reorderJsonBodiesForMapping, } from '@/utils/jsonParameterTree'; import { deriveInterfaceNameFromCurl, extractJsonCandidates, parseCurlCommand, type ParsedCurl, type ParsedCurlQueryParam, } from '@/utils/parseCurl'; const PLACEHOLDER = ParameterPlaceholderText; const PH = { fromAccount: PLACEHOLDER[ParameterPlaceholder.FromAccount], fromMappingBody: PLACEHOLDER[ParameterPlaceholder.FromMappingBody], timeStamp: PLACEHOLDER[ParameterPlaceholder.TimeStamp], dateTime: PLACEHOLDER[ParameterPlaceholder.DateTime], url: PLACEHOLDER[ParameterPlaceholder.Url], token: PLACEHOLDER[ParameterPlaceholder.Token], }; const METHOD_TO_ENUM: Record = { GET: RequestMethod.GET, POST: RequestMethod.POST, PUT: RequestMethod.PUT, DELETE: RequestMethod.DELETE, OPTIONS: RequestMethod.OPTIONS, HEAD: RequestMethod.HEAD, PATCH: RequestMethod.PATCH, }; const SKIP_FIXED_HEADER = new Set(['host', 'connection', 'content-length']); const SIGNATURE_PARAM_RE = /^(sign|signature|sig|_aop_signature|_sign)$/i; const ACCOUNT_PARAM_RE = /^(appkey|app_key|accesskey|access_key|client_id|clientid|secret)$/i; const BODY_PARAM_NAME_RE = /(?:DTO|Body|Data|Payload|Content|Json|json|Request|param_json)$/i; export type CurlFormKvRow = { id: number; name: string; value: string; description: string; }; function isJsonLikeValue(rawValue: string): boolean { if (!rawValue?.trim()) return false; const attempts = [rawValue.trim()]; try { attempts.push(decodeURIComponent(rawValue.trim())); } catch { /* keep raw */ } for (const text of attempts) { if (!text.startsWith('{') && !text.startsWith('[')) continue; try { JSON.parse(text); return true; } catch { try { JSON5.parse(text); return true; } catch { /* invalid */ } } } return false; } function collectPreferredMappingJsonTexts(items: ParsedCurlQueryParam[]): string[] { const preferred: string[] = []; items.forEach((item) => { if (!isJsonLikeValue(item.value) && !BODY_PARAM_NAME_RE.test(item.name)) return; preferred.push(item.value); try { preferred.push(decodeURIComponent(item.value)); } catch { /* ignore */ } }); return preferred; } function truncateSample(value: string, max = 120): string { const text = value.length > max ? `${value.slice(0, max)}…` : value; return text.replace(/\s+/g, ' '); } function curlSampleDescription(rawValue: string): string { return rawValue ? `curl 示例(映射参考): ${truncateSample(rawValue)}` : ''; } /** Query KV:签名留空;URL Query 上的 *DTO/JSON 用 $FromMappingBody(与 curl 实际位置一致) */ export function inferQueryParameterPlaceholder(name: string, rawValue: string): string { const lower = name.toLowerCase(); if (SIGNATURE_PARAM_RE.test(name) || lower.includes('signature')) { return ''; } if (/^(timestamp|time_stamp|ts|_aop_timestamp)$/.test(lower)) { return PH.timeStamp; } if (/^(token|access_token|accesstoken)$/.test(lower)) { return PH.token; } if (ACCOUNT_PARAM_RE.test(lower)) { return PH.fromAccount; } if (isJsonLikeValue(rawValue) || BODY_PARAM_NAME_RE.test(name)) { return PH.fromMappingBody; } return ''; } /** Body 表单 KV:业务 JSON 字段用 $FromMappingBody */ export function inferBodyFormParameterPlaceholder( name: string, rawValue: string, options?: { isPrimaryMappingField?: boolean }, ): string { const lower = name.toLowerCase(); if (SIGNATURE_PARAM_RE.test(name) || lower.includes('signature')) { return ''; } if (/^(timestamp|time_stamp|ts|_aop_timestamp)$/.test(lower)) { return PH.timeStamp; } if (/^(token|access_token|accesstoken)$/.test(lower)) { return PH.token; } if (ACCOUNT_PARAM_RE.test(lower)) { return PH.fromAccount; } if ( options?.isPrimaryMappingField || isJsonLikeValue(rawValue) || BODY_PARAM_NAME_RE.test(name) ) { return PH.fromMappingBody; } return ''; } function buildFormKvRows( items: ParsedCurlQueryParam[], idFactory: () => number, inferValue: (name: string, rawValue: string, index: number) => string, ): CurlFormKvRow[] { return items.map((item, index) => ({ id: idFactory(), name: item.name, value: inferValue(item.name, item.value, index), description: curlSampleDescription(item.value), })); } /** * FormUrlEncoded 分流: * - URL Query(?a=1¬ifyBizEventDTO=...)→ queryForm,*DTO 占位 $FromMappingBody * - POST Body(--data / --data-urlencode)→ bodyForm;仅此时才把 Body 里的 JSON 字段放进 bodyForm */ function splitFormUrlEncodedParams(parsed: ParsedCurl): { queryItems: ParsedCurlQueryParam[]; bodyFormItems: ParsedCurlQueryParam[]; } { const hasPostBodyForm = parsed.formBodyParams.length > 0 || (parsed.bodyType === 'form-urlencoded' && !!parsed.body?.trim()); const queryItems: ParsedCurlQueryParam[] = [...parsed.queryParams]; const bodyFormItems: ParsedCurlQueryParam[] = [...parsed.formBodyParams]; if (!hasPostBodyForm) { return { queryItems, bodyFormItems }; } parsed.queryParams.forEach((item) => { if (isJsonLikeValue(item.value) || BODY_PARAM_NAME_RE.test(item.name)) { if (!bodyFormItems.some((b) => b.name === item.name)) { bodyFormItems.push(item); } } }); const queryOnly = queryItems.filter( (item) => !isJsonLikeValue(item.value) && !BODY_PARAM_NAME_RE.test(item.name), ); return { queryItems: queryOnly, bodyFormItems }; } function mapContentType(parsed: ParsedCurl): number { const ct = parsed.contentTypeHeader; if (ct.includes('application/x-www-form-urlencoded')) return ContentType.FormUrlEncoded; if (ct.includes('multipart/form-data')) return ContentType.FormData; if (ct.includes('application/json')) return ContentType.Json; if (ct.includes('application/xml') || ct.includes('text/xml')) return ContentType.Xml; switch (parsed.bodyType) { case 'json': return ContentType.Json; case 'xml': return ContentType.Xml; case 'form-urlencoded': case 'form-data': return ContentType.FormUrlEncoded; default: if (parsed.formBodyParams.length > 0) return ContentType.FormUrlEncoded; // 阿里等开放平台:POST 无 body,业务参数全在 URL Query(含 *DTO JSON) if ( (parsed.method === 'POST' || parsed.method === 'PUT') && parsed.queryParams.length > 0 && !parsed.body?.trim() ) { return ContentType.FormUrlEncoded; } if (parsed.body) return ContentType.Json; return ContentType.None; } } function mapMappedParamType(jsonBodies: string[]): number | undefined { if (!jsonBodies.length) return undefined; try { const root = JSON.parse(jsonBodies[0]); if (Array.isArray(root)) return ParameterType.Array; if (root && typeof root === 'object') return ParameterType.Object; } catch { /* ignore */ } return ParameterType.Object; } export interface CurlInterfaceFillPayload { basicForm: Record; queryParams: CurlFormKvRow[]; headerParams: CurlFormKvRow[]; bodyFormParams: CurlFormKvRow[]; /** 上游:固定配置 Header */ fixedParams: Array<{ fieldName: string; fieldValue: string; fieldPositions: string; fieldPositionList: number[]; description: string; }>; jsonBodies: string[]; requestExample: string; parsed: ParsedCurl; contentType: number; } export interface CurlSmartFillResult { interfaceName?: string; contentType?: number; isFormUrlEncoded: boolean; queryCount: number; headerCount: number; bodyFormCount: number; mappingBodyImported: boolean; requestBodyImported: boolean; suggestedRequestTab: 'query' | 'header' | 'body' | 'mappingBody' | 'demo'; summaryLines: string[]; } export function buildCurlInterfaceFillPayload( rawCurl: string, idFactory: () => number = () => Math.floor(Math.random() * 2099999999) + 1, ): CurlInterfaceFillPayload { const parsed = parseCurlCommand(rawCurl); let jsonBodies = extractJsonCandidates(parsed); const methodKey = METHOD_TO_ENUM[parsed.method] ?? RequestMethod.POST; const contentType = mapContentType(parsed); const isForm = isFormUrlEncodedContentType(contentType); const basicForm: Record = { name: deriveInterfaceNameFromCurl(parsed), requestAddress: parsed.pathname, requestProtocol: parsed.protocol === 'https' ? RequestProtocol.Https : RequestProtocol.Http, requestMethod: methodKey, contentType, responseContentType: ResponseContentType.Json, }; const mappedParamType = mapMappedParamType(jsonBodies); if (mappedParamType !== undefined) { basicForm.mappedParamType = mappedParamType; } let queryItems = parsed.queryParams; let bodyFormItems: ParsedCurlQueryParam[] = []; if (isForm) { const split = splitFormUrlEncodedParams(parsed); queryItems = split.queryItems; bodyFormItems = split.bodyFormItems; jsonBodies = reorderJsonBodiesForMapping( jsonBodies, collectPreferredMappingJsonTexts([...bodyFormItems, ...queryItems]), { resolveParameterType: defaultResolveParameterType }, ); } else if (!isJsonOrXmlContentType(contentType)) { queryItems = [...parsed.queryParams, ...parsed.formBodyParams]; } jsonBodies = reorderJsonBodiesForMapping(jsonBodies, [], { resolveParameterType: defaultResolveParameterType, }); const jsonLikeBodyIndexes = bodyFormItems .map((item, index) => (isJsonLikeValue(item.value) || BODY_PARAM_NAME_RE.test(item.name) ? index : -1)) .filter((index) => index >= 0); const primaryMappingFieldIndex = jsonLikeBodyIndexes[0]; const queryParams = buildFormKvRows(queryItems, idFactory, (name, value) => inferQueryParameterPlaceholder(name, value), ); const bodyFormParams = isForm ? buildFormKvRows(bodyFormItems, idFactory, (name, value, index) => inferBodyFormParameterPlaceholder(name, value, { isPrimaryMappingField: index === primaryMappingFieldIndex, }), ) : []; const headerParams = parsed.headers .filter((h) => !SKIP_FIXED_HEADER.has(h.name.toLowerCase())) .map((h) => ({ id: idFactory(), name: h.name, value: h.value || PH.token, description: h.value ? `curl 示例: ${truncateSample(h.value)}` : '', })); const fixedParams = headerParams.map((h) => ({ fieldName: h.name, fieldValue: h.value, fieldPositions: 'Header', fieldPositionList: [FieldPosition.Header], description: h.description, })); return { basicForm, queryParams, headerParams, bodyFormParams, fixedParams, jsonBodies, requestExample: parsed.rawCurl, parsed, contentType, }; } /** 同步父组件 v-model 与成功提示文案 */ export function buildSmartFillResult( payload: CurlInterfaceFillPayload, importResult: { mappingBodyImported: boolean; requestBodyImported: boolean; }, ): CurlSmartFillResult { const isForm = isFormUrlEncodedContentType(payload.contentType); const isNone = isNoneContentType(payload.contentType); const contentType = normalizeContentType(payload.contentType); let suggestedRequestTab: CurlSmartFillResult['suggestedRequestTab'] = 'body'; if (isNone) { suggestedRequestTab = importResult.mappingBodyImported ? 'mappingBody' : payload.queryParams.length > 0 ? 'query' : 'header'; } else if (isForm) { suggestedRequestTab = importResult.mappingBodyImported ? 'mappingBody' : 'body'; } else if (importResult.requestBodyImported) { suggestedRequestTab = 'body'; } else if (payload.queryParams.length > 0) { suggestedRequestTab = 'query'; } const requestTypeLabel = isForm ? 'FormUrlEncoded' : isNone ? 'none' : 'Json/Xml'; const summaryLines = [ `接口名:${payload.basicForm.name || '-'}`, `请求类型:${requestTypeLabel}`, `Query ${payload.queryParams.length} 项`, `Header ${payload.headerParams.length} 项`, ]; if (isNone) { summaryLines.push('无 HTTP Body'); if (importResult.mappingBodyImported) { summaryLines.push('Mapping Body 参数树已解析'); } } else if (isForm) { summaryLines.push(`Body 表单 ${payload.bodyFormParams.length} 项`); summaryLines.push( importResult.mappingBodyImported ? 'Mapping Body 参数树已解析' : 'Mapping Body 未解析(cURL 中无 JSON 业务体)', ); } else { summaryLines.push( importResult.requestBodyImported ? 'Body 参数树已解析' : 'Body 未解析(无 JSON/XML 请求体)', ); } return { interfaceName: payload.basicForm.name as string | undefined, contentType, isFormUrlEncoded: isForm, queryCount: payload.queryParams.length, headerCount: payload.headerParams.length, bodyFormCount: payload.bodyFormParams.length, mappingBodyImported: importResult.mappingBodyImported, requestBodyImported: importResult.requestBodyImported, suggestedRequestTab, summaryLines, }; } export function formatSmartFillMessage(result: CurlSmartFillResult): string { return `智能填充完成:${result.summaryLines.join(',')}`; }