|
|
/** 从样例 JSON 泛化出 JSON Schema(阶段0专用,无业务状态机) */
|
|
|
|
|
|
type JsonSchema = Record<string, unknown>;
|
|
|
|
|
|
function inferValueSchema(value: unknown): JsonSchema {
|
|
|
if (value === null) {
|
|
|
return { type: "null" };
|
|
|
}
|
|
|
if (Array.isArray(value)) {
|
|
|
const itemSchemas = value.map((item) => inferValueSchema(item));
|
|
|
return {
|
|
|
type: "array",
|
|
|
items: mergeSchemas(itemSchemas),
|
|
|
minItems: value.length > 0 ? 1 : 0,
|
|
|
};
|
|
|
}
|
|
|
switch (typeof value) {
|
|
|
case "boolean":
|
|
|
return { type: "boolean" };
|
|
|
case "number":
|
|
|
return Number.isInteger(value)
|
|
|
? { type: "integer" }
|
|
|
: { type: "number" };
|
|
|
case "string":
|
|
|
return { type: "string" };
|
|
|
case "object": {
|
|
|
const obj = value as Record<string, unknown>;
|
|
|
const properties: Record<string, JsonSchema> = {};
|
|
|
for (const [key, child] of Object.entries(obj)) {
|
|
|
properties[key] = inferValueSchema(child);
|
|
|
}
|
|
|
return {
|
|
|
type: "object",
|
|
|
properties,
|
|
|
additionalProperties: true,
|
|
|
};
|
|
|
}
|
|
|
default:
|
|
|
return {};
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function mergeSchemas(schemas: JsonSchema[]): JsonSchema {
|
|
|
if (schemas.length === 0) {
|
|
|
return {};
|
|
|
}
|
|
|
if (schemas.length === 1) {
|
|
|
return schemas[0]!;
|
|
|
}
|
|
|
const types = new Set(schemas.map((s) => s.type).filter(Boolean));
|
|
|
if (types.size === 1) {
|
|
|
const type = [...types][0];
|
|
|
if (type === "object") {
|
|
|
const propMap = new Map<string, JsonSchema[]>();
|
|
|
for (const schema of schemas) {
|
|
|
const props = schema.properties as Record<string, JsonSchema> | undefined;
|
|
|
if (!props) {
|
|
|
continue;
|
|
|
}
|
|
|
for (const [key, child] of Object.entries(props)) {
|
|
|
const list = propMap.get(key) ?? [];
|
|
|
list.push(child);
|
|
|
propMap.set(key, list);
|
|
|
}
|
|
|
}
|
|
|
const properties: Record<string, JsonSchema> = {};
|
|
|
for (const [key, children] of propMap) {
|
|
|
properties[key] = mergeSchemas(children);
|
|
|
}
|
|
|
return { type: "object", properties, additionalProperties: true };
|
|
|
}
|
|
|
if (type === "array") {
|
|
|
const items = mergeSchemas(
|
|
|
schemas.map((s) => (s.items as JsonSchema | undefined) ?? {}),
|
|
|
);
|
|
|
return { type: "array", items, minItems: 1 };
|
|
|
}
|
|
|
}
|
|
|
return { oneOf: schemas };
|
|
|
}
|
|
|
|
|
|
function annotateQuoteContract(schema: JsonSchema): JsonSchema {
|
|
|
const props = schema.properties as Record<string, JsonSchema> | undefined;
|
|
|
if (!props) {
|
|
|
return schema;
|
|
|
}
|
|
|
|
|
|
if (props.rates) {
|
|
|
props.rates = {
|
|
|
...props.rates,
|
|
|
description: "报价 rates 分支(standard/guaranteed/fastest)",
|
|
|
};
|
|
|
}
|
|
|
if (props.data && typeof props.data === "object") {
|
|
|
const dataProps = (props.data as JsonSchema).properties as
|
|
|
| Record<string, JsonSchema>
|
|
|
| undefined;
|
|
|
if (dataProps?.rates) {
|
|
|
dataProps.rates = {
|
|
|
...dataProps.rates,
|
|
|
description: "嵌套 data.rates 报价分支",
|
|
|
};
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return schema;
|
|
|
}
|
|
|
|
|
|
export type QuoteContractSchemaDocument = {
|
|
|
$schema: string;
|
|
|
$id: string;
|
|
|
title: string;
|
|
|
description: string;
|
|
|
generatedAt: string;
|
|
|
sourceHar: {
|
|
|
url: string;
|
|
|
method: string;
|
|
|
status: number;
|
|
|
};
|
|
|
contractHints: {
|
|
|
minRateTiers: number;
|
|
|
placeIdPattern: string;
|
|
|
quoteUrlPatterns: string[];
|
|
|
};
|
|
|
body: JsonSchema;
|
|
|
};
|
|
|
|
|
|
export function buildQuoteContractSchema(input: {
|
|
|
body: unknown;
|
|
|
harEntry: { url: string; method: string; status: number };
|
|
|
}): QuoteContractSchemaDocument {
|
|
|
const bodySchema = annotateQuoteContract(inferValueSchema(input.body));
|
|
|
|
|
|
return {
|
|
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
|
$id: "https://chajia.local/schemas/mothership-quote-contract.json",
|
|
|
title: "MotherShip Quote Response Contract",
|
|
|
description:
|
|
|
"从 baseline.har 最佳 quote 响应泛化;供阶段1 Contract-Driven Capture 白名单校验。",
|
|
|
generatedAt: new Date().toISOString(),
|
|
|
sourceHar: {
|
|
|
url: input.harEntry.url,
|
|
|
method: input.harEntry.method,
|
|
|
status: input.harEntry.status,
|
|
|
},
|
|
|
contractHints: {
|
|
|
minRateTiers: 4,
|
|
|
placeIdPattern: "^[A-Za-z0-9_-]{8,128}$",
|
|
|
quoteUrlPatterns: [
|
|
|
"services.mothership.com/axel/quote",
|
|
|
],
|
|
|
primaryRatesPath: "data.rates",
|
|
|
allRatesPath: "data.availableRates",
|
|
|
tierMapping: {
|
|
|
"standard/lowest": "data.rates.standard.lowest",
|
|
|
"standard/fastest": "data.rates.standard.fastest",
|
|
|
"guaranteed/lowest":
|
|
|
"data.rates.guaranteed.lowest | data.rates.guaranteed.bestValue",
|
|
|
"guaranteed/fastest":
|
|
|
"data.rates.guaranteed.fastest | data.rates.guaranteed.bestValue",
|
|
|
},
|
|
|
note:
|
|
|
"axel/quote 的 guaranteed 分支可能仅含 bestValue,无独立 lowest/fastest 键;阶段1 需映射而非 UI tab 切换。",
|
|
|
},
|
|
|
body: bodySchema,
|
|
|
};
|
|
|
}
|