账号支持只看本账号看板;模板重新解析改为保留原映射并多轮补缺。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -37,6 +37,9 @@ class QuoteTemplateParseBody(BaseModel):
|
||||
yellow_count: int = Field(default=0, alias="yellowCount")
|
||||
truncated: bool = False
|
||||
request_id: str = Field(default="", alias="requestId")
|
||||
# 未换文件重新解析:true + existingMapping,回写时旧字段优先补缺
|
||||
merge_existing: bool = Field(default=False, alias="mergeExisting")
|
||||
existing_mapping: Optional[dict[str, Any]] = Field(default=None, alias="existingMapping")
|
||||
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
@@ -103,7 +106,10 @@ def enqueue_parse(
|
||||
"cellCount": body.cell_count or len(inventory),
|
||||
"yellowCount": body.yellow_count,
|
||||
"truncated": bool(body.truncated),
|
||||
"mergeExisting": bool(body.merge_existing),
|
||||
}
|
||||
if body.merge_existing and isinstance(body.existing_mapping, dict):
|
||||
payload["existingMapping"] = body.existing_mapping
|
||||
result = enqueue_recognition(
|
||||
kind="quote_template_parse",
|
||||
payload=payload,
|
||||
|
||||
@@ -111,28 +111,50 @@ def process_recognition(data: dict[str, Any], *, allow_network: bool = True) ->
|
||||
|
||||
|
||||
def _process_quote_template_parse(payload: dict[str, Any], *, allow_network: bool) -> dict[str, Any]:
|
||||
"""跑 sheet_map 并回写主账;失败也必须回写 failed+原因。"""
|
||||
"""
|
||||
跑 sheet_map 并回写主账;失败也必须回写 failed+原因。
|
||||
|
||||
mergeExisting=true(未换文件重新解析)时:与 existingMapping 补缺合并后再回写,
|
||||
旧字段优先保留;换文件/首传不传该标记,整份覆盖。
|
||||
"""
|
||||
from agent.config import get_settings
|
||||
from agent.llm.mode_sheet_map import invoke_sheet_map
|
||||
from agent.llm.mode_sheet_map import (
|
||||
assess_mapping_gaps,
|
||||
invoke_sheet_map,
|
||||
merge_mapping_prefer_existing,
|
||||
)
|
||||
|
||||
version_id = str(payload.get("versionId") or "").strip()
|
||||
settings = get_settings()
|
||||
net = bool(allow_network and settings.llm_allow_network)
|
||||
result = invoke_sheet_map(payload, allow_network=net)
|
||||
result = invoke_sheet_map(payload, allow_network=net, timeout_seconds=240.0, max_rounds=3)
|
||||
merge_existing = bool(payload.get("mergeExisting") or payload.get("merge_existing"))
|
||||
existing = payload.get("existingMapping") or payload.get("existing_mapping")
|
||||
body: dict[str, Any]
|
||||
if result.get("ok") and isinstance(result.get("mapping"), dict):
|
||||
mapping = dict(result["mapping"])
|
||||
gaps = result.get("gaps_after")
|
||||
if not isinstance(gaps, list):
|
||||
gaps = mapping.get("mappingGaps") if isinstance(mapping.get("mappingGaps"), list) else []
|
||||
mapping["mappingGaps"] = [str(g) for g in gaps if g]
|
||||
if merge_existing and isinstance(existing, dict):
|
||||
mapping = merge_mapping_prefer_existing(existing, mapping)
|
||||
# 合并后再对照清单复评缺口,避免角标沿用合并前数字
|
||||
remaining = assess_mapping_gaps(payload, mapping)
|
||||
mapping["mappingGaps"] = [str(g) for g in remaining if g]
|
||||
if not remaining:
|
||||
mapping["unmappedRequired"] = []
|
||||
mapping["unmappedLabels"] = []
|
||||
else:
|
||||
gaps = result.get("gaps_after")
|
||||
if not isinstance(gaps, list):
|
||||
gaps = mapping.get("mappingGaps") if isinstance(mapping.get("mappingGaps"), list) else []
|
||||
mapping["mappingGaps"] = [str(g) for g in gaps if g]
|
||||
# 运营可读摘要:点数之外固定带上未解析项
|
||||
n_fields = len(mapping.get("fields") or {})
|
||||
n_fee = len(mapping.get("feeRegions") or []) if isinstance(mapping.get("feeRegions"), list) else 0
|
||||
n_route = len(mapping.get("routeRegions") or []) if isinstance(mapping.get("routeRegions"), list) else 0
|
||||
points = n_fields + n_fee + n_route
|
||||
parse_message = f"已解析 {points} 个映射点"
|
||||
if mapping["mappingGaps"]:
|
||||
if merge_existing:
|
||||
parse_message += "(保留原映射并补缺)"
|
||||
if mapping.get("mappingGaps"):
|
||||
preview = ";".join(mapping["mappingGaps"][:8])
|
||||
more = len(mapping["mappingGaps"]) - 8
|
||||
parse_message += f";未解析到 {len(mapping['mappingGaps'])} 项:{preview}"
|
||||
@@ -144,6 +166,7 @@ def _process_quote_template_parse(payload: dict[str, Any], *, allow_network: boo
|
||||
"mappingJson": mapping,
|
||||
"parseStatus": "parsed",
|
||||
"parseMessage": parse_message,
|
||||
"mergeExisting": merge_existing,
|
||||
}
|
||||
else:
|
||||
err = str(result.get("error") or "AI 解析失败")
|
||||
@@ -152,12 +175,14 @@ def _process_quote_template_parse(payload: dict[str, Any], *, allow_network: boo
|
||||
"ok": False,
|
||||
"parseStatus": "failed",
|
||||
"parseMessage": err,
|
||||
"mergeExisting": merge_existing,
|
||||
}
|
||||
posted = _post_parse_result(body)
|
||||
logger.info(
|
||||
"quote_template_parse done versionId=%s ok=%s posted=%s",
|
||||
"quote_template_parse done versionId=%s ok=%s merge=%s posted=%s",
|
||||
version_id,
|
||||
body.get("ok"),
|
||||
merge_existing,
|
||||
posted.get("ok"),
|
||||
)
|
||||
return {"ok": bool(body.get("ok")), "posted": posted, "kind": "quote_template_parse"}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
报价工作表映射 mode:上传时用千问产出 mappingJson。
|
||||
|
||||
本文件职责:加载映射分析提示词、拼用户消息、调千问、剥 Markdown、校验 JSON、
|
||||
对照单元格清单做缺项检测,必要时再补跑一轮。
|
||||
对照单元格清单做缺项检测;有缺口则多轮补跑(默认最多 3 轮),目标一次流程内齐全。
|
||||
禁止:发明运价;在企微回调线程同步调用;出单阶段再调本 mode。
|
||||
"""
|
||||
|
||||
@@ -22,30 +22,58 @@ MODE = LlmMode.SHEET_MAP
|
||||
|
||||
_PROMPT_CACHE: str | None = None
|
||||
|
||||
# 清单原文片段 → 白名单 code(用于缺项检测;不要求全覆盖词典)
|
||||
_LABEL_TO_CODE: list[tuple[tuple[str, ...], str]] = [
|
||||
(("提货地址", "Place of Receipt", "place of receipt"), "pickup_address"),
|
||||
(("送货地址", "派送地址", "Place of Delivery", "place of delivery"), "delivery_address"),
|
||||
(("货值", "Cargo Value", "cargo value"), "cargo_value"),
|
||||
(("海关编码", "HS Code", "HS编码", "hs code"), "hs_code"),
|
||||
(("油、电、磁", "油,电,磁", "油电磁", "IL, ELEC", "L,ELEC", "oil"), "oil_elec_mag"),
|
||||
# 清单原文片段 → 白名单 code(按运输方式过滤;关键词避免过短英文误报)
|
||||
_LABEL_TO_CODE_COMMON: list[tuple[tuple[str, ...], str]] = [
|
||||
(("报价单编号", "询价单号", "RFQ No", "Quote No"), "quote_no"),
|
||||
(("报价日期", "询价日期", "Inquiry Date", "Quote Date"), "quote_date"),
|
||||
(("有效期至", "有效期", "Validity"), "valid_until"),
|
||||
(("客户名称", "Customer Name", "Customer"), "customer_name"),
|
||||
(("联系人", "Contact Person", "Contact Name"), "contact_name"),
|
||||
(("电话", "Tel", "Phone", "Mobile"), "contact_phone"),
|
||||
(("币别", "Currency"), "currency"),
|
||||
(("业务员", "Sales"), "sales_name"),
|
||||
(("品名", "Commodity", "Goods Description"), "commodity"),
|
||||
]
|
||||
|
||||
_LABEL_TO_CODE_SEA: list[tuple[tuple[str, ...], str]] = [
|
||||
(("提货地址", "Place of Receipt"), "pickup_address"),
|
||||
(("送货地址", "派送地址", "Place of Delivery"), "delivery_address"),
|
||||
(("货值", "Cargo Value"), "cargo_value"),
|
||||
(("海关编码", "HS Code", "HS编码"), "hs_code"),
|
||||
(("油、电、磁", "油,电,磁", "油电磁"), "oil_elec_mag"),
|
||||
(("特殊说明", "特别说明", "Special Remarks"), "special_remarks"),
|
||||
(("要求货物到达", "Requested Arriv", "抵达时间"), "eta_request"),
|
||||
(("运输时效", "Transit Time", "航程时间", "Navigation time"), "transit_time"),
|
||||
(("船期", "Sailing", "Requested Sh"), "sailing_schedule"),
|
||||
(("开船日", "On Board"), "on_board_date"),
|
||||
(("船公司", "Shipping Line", "Shipping Co"), "shipping_line"),
|
||||
(("要求货物到达", "Requested Arrival"), "eta_request"),
|
||||
(("运输时效", "Transit Time", "航程时间"), "transit_time"),
|
||||
(("船期", "Sailing Schedule"), "sailing_schedule"),
|
||||
(("开船日", "On Board Date"), "on_board_date"),
|
||||
(("船公司", "Shipping Line", "Shipping Company"), "shipping_line"),
|
||||
(("货好时间", "Cargo Ready"), "cargo_ready_date"),
|
||||
(("贸易条款", "Terms"), "trade_terms"),
|
||||
(("贸易条款", "Trade Terms", "Incoterms"), "trade_terms"),
|
||||
(("整柜或散货", "整柜或拼柜", "FCL or LCL"), "fcl_or_lcl"),
|
||||
(("箱型", "柜量", "Type & Qty", "Cont."), "container_type_qty"),
|
||||
(("起运港", "POL"), "pol"),
|
||||
(("目的港", "POD"), "pod"),
|
||||
(("品名", "Commodity"), "commodity"),
|
||||
(("箱型箱量", "箱型", "柜量", "Type & Qty"), "container_type_qty"),
|
||||
(("起运港", "Port of Loading", "POL"), "pol"),
|
||||
(("目的港", "Port of Discharge", "POD"), "pod"),
|
||||
(("货物数量", "Quantity"), "quantity"),
|
||||
(("询价日期", "Inquiry Date", "报价日期"), "quote_date"),
|
||||
(("RFQ", "询价单号", "报价单编号"), "quote_no"),
|
||||
(("有效期", "period"), "valid_until"),
|
||||
]
|
||||
|
||||
_LABEL_TO_CODE_AIR: list[tuple[tuple[str, ...], str]] = [
|
||||
(("起运地", "起运港", "Departure", "Origin Airport"), "origin"),
|
||||
(("目的地", "目的港", "Destination Airport"), "destination"),
|
||||
(("件数", "Pieces", "Pkgs"), "pieces"),
|
||||
(("毛重", "重量", "Gross Weight", "Weight"), "weight_kg"),
|
||||
(("体积", "Volume", "CBM"), "volume_cbm"),
|
||||
(("计费重", "Chargeable Weight"), "chargeable_weight_kg"),
|
||||
(("航班", "Flight"), "flight_no"),
|
||||
(("中转机场", "Transfer Airport"), "transfer_airport"),
|
||||
(("贸易条款", "Trade Terms", "Incoterms"), "trade_terms"),
|
||||
(("特殊说明", "特别说明", "Special Remarks"), "special_remarks"),
|
||||
]
|
||||
|
||||
_LABEL_TO_CODE_LAND: list[tuple[tuple[str, ...], str]] = [
|
||||
(("始发地", "起运地", "发站", "Origin"), "origin"),
|
||||
(("目的地", "到站", "Destination"), "destination"),
|
||||
(("运输时效", "时效", "Transit Time"), "transit_time"),
|
||||
(("特殊说明", "特别说明"), "special_remarks"),
|
||||
]
|
||||
|
||||
_SEA_FEE_MARKERS: list[tuple[tuple[str, ...], tuple[str, ...]]] = [
|
||||
@@ -65,6 +93,33 @@ _SEA_FEE_MARKERS: list[tuple[tuple[str, ...], tuple[str, ...]]] = [
|
||||
]
|
||||
|
||||
|
||||
def _labels_for_mode(mode: str) -> list[tuple[tuple[str, ...], str]]:
|
||||
m = (mode or "sea").strip().lower()
|
||||
out = list(_LABEL_TO_CODE_COMMON)
|
||||
if m in ("sea", "海运"):
|
||||
out.extend(_LABEL_TO_CODE_SEA)
|
||||
elif m in ("air", "空运"):
|
||||
out.extend(_LABEL_TO_CODE_AIR)
|
||||
elif m in ("land", "陆运"):
|
||||
out.extend(_LABEL_TO_CODE_LAND)
|
||||
else:
|
||||
out.extend(_LABEL_TO_CODE_SEA)
|
||||
return out
|
||||
|
||||
|
||||
def _label_hit(blob: str, labels: tuple[str, ...]) -> bool:
|
||||
"""清单命中:中文标签直接包含;英文至少 4 字符,降低误报。"""
|
||||
for lab in labels:
|
||||
if not lab:
|
||||
continue
|
||||
if any("\u4e00" <= ch <= "\u9fff" for ch in lab):
|
||||
if lab in blob:
|
||||
return True
|
||||
elif len(lab) >= 4 and lab.lower() in blob.lower():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def mode_meta() -> dict[str, Any]:
|
||||
return {
|
||||
"mode": MODE.value,
|
||||
@@ -178,7 +233,7 @@ def assess_mapping_gaps(payload: dict[str, Any], mapping: dict[str, Any] | None)
|
||||
"""
|
||||
对照单元格清单,列出 mapping 明显缺口(中文说明,供补跑用户消息)。
|
||||
|
||||
不打外网。海运:费用段标题出现则要求对应 feeRegion;常见业务标签出现则要求 fields code。
|
||||
不打外网。按运输方式过滤字段词典;海运另检查三段费用区。
|
||||
"""
|
||||
blob = _inventory_blob(payload)
|
||||
if not blob.strip():
|
||||
@@ -196,24 +251,89 @@ def assess_mapping_gaps(payload: dict[str, Any], mapping: dict[str, Any] | None)
|
||||
for r in fee_regions
|
||||
if isinstance(r, dict)
|
||||
).lower()
|
||||
route_regions = mapping.get("routeRegions") if isinstance(mapping.get("routeRegions"), list) else []
|
||||
|
||||
gaps: list[str] = []
|
||||
mode = str(payload.get("transportMode") or payload.get("transport_mode") or "sea").strip().lower()
|
||||
# 字段匹配用:去掉费用段标题,避免「目的港费用」误报成 pod 等字段缺口
|
||||
field_blob = blob
|
||||
if mode in ("sea", "海运"):
|
||||
for markers, matchers in _SEA_FEE_MARKERS:
|
||||
if not any(m in blob for m in markers):
|
||||
continue
|
||||
if not any(x in fee_blob for x in matchers):
|
||||
gaps.append(f"费用区缺失:清单有「{markers[0]}」,须建对应 feeRegion")
|
||||
for m in markers:
|
||||
field_blob = field_blob.replace(m, " ")
|
||||
elif mode in ("land", "陆运"):
|
||||
# 陆运主表常见项目/线路矩阵:清单有「项目」或车型价列但无 routeRegions
|
||||
if (
|
||||
("项目" in blob or "车型" in blob or "3T" in blob or "拼车" in blob)
|
||||
and not route_regions
|
||||
):
|
||||
gaps.append("线路区缺失:陆运表须建 routeRegions 承接线路/车型报价")
|
||||
|
||||
for labels, code in _LABEL_TO_CODE:
|
||||
if code in field_codes:
|
||||
# 陆运已建 routeRegions 时,始发地/目的地/时效常在线路矩阵列头,不再强制 fields
|
||||
land_route_covered = mode in ("land", "陆运") and bool(route_regions)
|
||||
land_skip_codes = {"origin", "destination", "transit_time"} if land_route_covered else set()
|
||||
|
||||
for labels, code in _labels_for_mode(mode):
|
||||
if code in field_codes or code in land_skip_codes:
|
||||
continue
|
||||
if any(lab in blob for lab in labels):
|
||||
if _label_hit(field_blob, labels):
|
||||
gaps.append(f"字段缺失:清单有「{labels[0]}」→ 应映射 {code}")
|
||||
return gaps
|
||||
|
||||
|
||||
def merge_mapping_prefer_existing(
|
||||
old_mapping: dict[str, Any] | None,
|
||||
new_mapping: dict[str, Any] | None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
补缺合并:旧 fields / 区域优先,新结果只补充旧里没有的 code/id。
|
||||
|
||||
用于「未换文件重新解析」:避免已对上的字段被新一轮 AI 冲掉。
|
||||
"""
|
||||
new_mapping = dict(new_mapping or {})
|
||||
old_mapping = old_mapping if isinstance(old_mapping, dict) else {}
|
||||
out = dict(new_mapping)
|
||||
|
||||
new_fields = new_mapping.get("fields") if isinstance(new_mapping.get("fields"), dict) else {}
|
||||
old_fields = old_mapping.get("fields") if isinstance(old_mapping.get("fields"), dict) else {}
|
||||
fields: dict[str, Any] = dict(new_fields)
|
||||
for code, cell in old_fields.items():
|
||||
if cell is not None and str(cell).strip():
|
||||
fields[str(code)] = cell
|
||||
out["fields"] = fields
|
||||
|
||||
out["feeRegions"] = _merge_regions_by_id(
|
||||
old_mapping.get("feeRegions"), new_mapping.get("feeRegions")
|
||||
)
|
||||
out["routeRegions"] = _merge_regions_by_id(
|
||||
old_mapping.get("routeRegions"), new_mapping.get("routeRegions")
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _merge_regions_by_id(old_arr: Any, new_arr: Any) -> list[dict[str, Any]]:
|
||||
"""区域按 id 合并:旧整段保留,新 id 追加。"""
|
||||
out: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
for src in (old_arr, new_arr):
|
||||
if not isinstance(src, list):
|
||||
continue
|
||||
for item in src:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
rid = str(item.get("id") or "").strip()
|
||||
if rid and rid in seen:
|
||||
continue
|
||||
if rid:
|
||||
seen.add(rid)
|
||||
out.append(dict(item))
|
||||
return out
|
||||
|
||||
|
||||
def invoke_shell(*, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"""
|
||||
无网/单测入口:若带 assistant_json 则直接解析;否则返回未实现。
|
||||
@@ -273,10 +393,12 @@ def invoke_sheet_map(
|
||||
*,
|
||||
allow_network: bool = False,
|
||||
chat_fn: Optional[Any] = None,
|
||||
timeout_seconds: float = 180.0,
|
||||
timeout_seconds: float = 240.0,
|
||||
max_rounds: int = 3,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
调用千问产出 mappingJson;若对照清单有明显缺口,再补跑一轮(最多 2 次 chat)。
|
||||
调用千问产出 mappingJson;对照清单有缺口则补跑,最多 max_rounds 轮(默认 3),
|
||||
目标一次流程内尽量齐全,缺口写进 mappingGaps。
|
||||
|
||||
allow_network=False 且无 chat_fn:不打外网。
|
||||
副作用:可能出站 HTTPS;应在 Worker HTTP 槽内调用。
|
||||
@@ -293,36 +415,54 @@ def invoke_sheet_map(
|
||||
else:
|
||||
chat = chat_fn
|
||||
|
||||
rounds_cap = max(1, min(int(max_rounds or 3), 4))
|
||||
first = _chat_once(chat=chat, payload=payload, gaps=None)
|
||||
if not first.get("ok"):
|
||||
return first
|
||||
|
||||
mapping = first["mapping"]
|
||||
gaps = assess_mapping_gaps(payload, mapping)
|
||||
rounds = 1
|
||||
if gaps:
|
||||
logger.info(
|
||||
"sheet_map gaps versionId=%s n=%s sample=%s",
|
||||
payload.get("versionId"),
|
||||
len(gaps),
|
||||
gaps[:5],
|
||||
)
|
||||
second = _chat_once(chat=chat, payload=payload, gaps=gaps)
|
||||
rounds = 2
|
||||
if second.get("ok") and second.get("mapping"):
|
||||
mapping = second["mapping"]
|
||||
# 第二轮仍失败结构时保留第一轮
|
||||
elif not second.get("ok"):
|
||||
logger.warning(
|
||||
"sheet_map 补跑失败,保留第一轮 versionId=%s err=%s",
|
||||
payload.get("versionId"),
|
||||
second.get("error"),
|
||||
)
|
||||
|
||||
remaining = assess_mapping_gaps(payload, mapping)
|
||||
# 缺口落进 mappingJson,主账/后台可直接展示「表上有、未映射上」
|
||||
# 有缺口就补跑,直到齐、轮次用尽、或本轮未减少缺口
|
||||
while remaining and rounds < rounds_cap:
|
||||
logger.info(
|
||||
"sheet_map gaps versionId=%s round=%s n=%s sample=%s",
|
||||
payload.get("versionId"),
|
||||
rounds,
|
||||
len(remaining),
|
||||
remaining[:5],
|
||||
)
|
||||
nxt = _chat_once(chat=chat, payload=payload, gaps=remaining)
|
||||
rounds += 1
|
||||
if not (nxt.get("ok") and nxt.get("mapping")):
|
||||
logger.warning(
|
||||
"sheet_map 补跑失败,保留上一轮 versionId=%s err=%s",
|
||||
payload.get("versionId"),
|
||||
nxt.get("error"),
|
||||
)
|
||||
break
|
||||
mapping = nxt["mapping"]
|
||||
after = assess_mapping_gaps(payload, mapping)
|
||||
if len(after) >= len(remaining):
|
||||
# 未继续收敛则停止,避免空转烧额度
|
||||
logger.info(
|
||||
"sheet_map gaps 未收敛 versionId=%s before=%s after=%s stop",
|
||||
payload.get("versionId"),
|
||||
len(remaining),
|
||||
len(after),
|
||||
)
|
||||
remaining = after
|
||||
break
|
||||
remaining = after
|
||||
|
||||
# 缺口落进 mappingJson;齐全时清空 AI 自报未映射,避免列表角标被说明性标签抬高
|
||||
if isinstance(mapping, dict):
|
||||
mapping["mappingGaps"] = list(remaining)
|
||||
if not remaining:
|
||||
mapping["unmappedRequired"] = []
|
||||
mapping["unmappedLabels"] = []
|
||||
elif not isinstance(mapping.get("unmappedLabels"), list):
|
||||
mapping["unmappedLabels"] = []
|
||||
logger.info(
|
||||
"sheet_map ok versionId=%s rounds=%s fields=%s feeRegions=%s routeRegions=%s remain_gaps=%s",
|
||||
payload.get("versionId"),
|
||||
|
||||
@@ -360,6 +360,8 @@ transportMode:sea | air | land
|
||||
5. 空运必须能承接 departureCharges + 空运费;陆运主表必须用 routeRegions 承接 landSegments。
|
||||
6. 看不清 → unmappedRequired / unmappedLabels;表上有、能对上白名单却未映射 → 视为错误,必须补进 fields。
|
||||
7. 用户未声明运输方式时推断 transportMode,并在 notes 说明。
|
||||
8. **一次齐全**:第一轮就要尽量映射清单中全部可见业务标签;宁可多写 fields/feeRegions/routeRegions,也不要省略。补跑时输出完整 mappingJson(覆盖式),不要只回增量。
|
||||
9. 说明性/公式性文字(如「1CBM=***KG」「重量体积取大」)无法落白名单字段时写入 unmappedLabels,不要假装成业务字段缺口。
|
||||
|
||||
【用户消息将提供】
|
||||
运输方式:海运 | 空运 | 陆运
|
||||
@@ -368,4 +370,4 @@ transportMode:sea | air | land
|
||||
(可选)TMS 样例或出单投影说明——仅对齐字段
|
||||
(补跑时)缺口清单:请据此补全整份 mappingJson,不要只回增量。
|
||||
|
||||
输出前自检 fields 与 feeRegions/routeRegions 是否覆盖清单中的可见业务标签,再只输出 mappingJson。
|
||||
输出前自检:fields 与 feeRegions/routeRegions 是否覆盖清单中的可见业务标签;有缺口则先补齐再输出。只输出 mappingJson。
|
||||
|
||||
Reference in New Issue
Block a user