front/src/utils/curlToInterfaceFill.ts
zzh e201b2f458 feat: 查价卡片改版与接口/工作台体验优化
重构报价卡布局(产品说明、单卡重算、最优标签),并同步接口详情与工作台相关交互调整。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 16:14:16 +08:00

457 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string, number> = {
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&notifyBizEventDTO=...)→ 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<string, unknown>;
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<string, unknown> = {
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(',')}`;
}