重构报价卡布局(产品说明、单卡重算、最优标签),并同步接口详情与工作台相关交互调整。 Co-authored-by: Cursor <cursoragent@cursor.com>
457 lines
13 KiB
TypeScript
457 lines
13 KiB
TypeScript
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¬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<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(',')}`;
|
||
}
|