落地陆运私聊文字询价:选项核对后再建单查价,并按线路拉产品进群。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-17 16:23:30 +08:00
co-authored by Cursor
parent 83b907c4b8
commit be95d823a2
52 changed files with 4534 additions and 76 deletions
+61 -4
View File
@@ -44,6 +44,18 @@ SEA_REQUIRED_FIELDS = (
)
SEA_DISPLAY = {key: label for key, label in SEA_REQUIRED_FIELDS}
# 陆运销售侧名称:卡片写始发站/货物品名;内部仍用合同键起运港/品名。
LAND_DISPLAY = {
"起运港": "始发站",
"目的港": "目的地",
"品名": "货物品名",
"毛重": "重量",
"体积": "体积",
"车型/数量": "车型/数量",
"通关口岸": "通关口岸",
"报价日期": "报价日期",
}
# 协同补充字段:拉群后可静默写入,不挡首次查价。内部键仍是 HS编码。
SEA_COLLAB_FIELDS = ("贸易条款", "货好时间", "HS编码", "是否含油", "是否含电", "是否含磁")
# 空运群只补含电/含磁,摘要不要把海运六项搬过去。
@@ -83,7 +95,9 @@ _ALIASES = {
"重量(KG)": "毛重",
"重量": "毛重",
"体积(CBM)": "体积",
"车型/数量": "车型数量",
"车型/数量": "车型/数量",
"车型数量": "车型/数量",
"数量": "数量",
"HS": "HS编码",
"HS CODE": "HS编码",
"商品海关编码": "HS编码",
@@ -114,9 +128,33 @@ def normalize_facts(facts: dict[str, Any] | None) -> dict[str, str]:
if not val:
continue
out[key] = val
_fold_land_vehicle_qty(out)
return out
def _fold_land_vehicle_qty(merged: dict[str, str]) -> None:
"""
把拆开的车型、数量收成一个「车型/数量」。
销售侧只认这一项必填;模型若仍拆成两项,这里合并,禁止再分别补问。
不改主账。有「车型/数量」则保留原文,缺的再用车型、数量拼上。
"""
combo = (merged.get("车型/数量") or "").strip()
vehicle = (merged.get("车型") or "").strip()
qty = (merged.get("数量") or "").strip()
if not combo:
if vehicle and qty:
combo = f"{vehicle}/{qty}"
elif vehicle:
combo = vehicle
elif qty:
combo = qty
elif qty and qty not in combo:
combo = f"{combo}/{qty}"
if combo:
merged["车型/数量"] = combo
def _harvest_packaging(raw: str) -> str:
"""当前原话里最后出现的包装词;没有则空串。"""
last = ""
@@ -235,8 +273,13 @@ def apply_quote_date_default(facts: dict[str, str]) -> dict[str, str]:
return merged
def _pricing_required_keys(contract: dict[str, Any], business_line: str, land_subtype: str) -> list[str]:
"""当前激活的首次查价内部键列表。"""
def _pricing_required_keys(
contract: dict[str, Any],
business_line: str,
land_subtype: str,
facts: dict[str, str] | None = None,
) -> list[str]:
"""当前激活的首次查价内部键列表。中港再按装载方式收窄。"""
line = (business_line or "").strip().upper()
pricing = contract.get("pricing_required") or {}
if line == "SEA":
@@ -248,6 +291,14 @@ def _pricing_required_keys(contract: dict[str, Any], business_line: str, land_su
block = land.get(land_subtype) or {}
if not block:
return []
if land_subtype == "中港":
from agent.schema.land_options import canonicalize_hk_load
load = canonicalize_hk_load(str((facts or {}).get("中港装载方式") or ""))
by_load = block.get("by_hk_load") or {}
picked = by_load.get(load) if load else None
if isinstance(picked, list):
return list(picked)
return list(block.get("always") or [])
return []
@@ -259,6 +310,8 @@ def display_name(internal_key: str, business_line: str = "AIR") -> str:
return AIR_DISPLAY.get(internal_key, internal_key)
if line == "SEA":
return SEA_DISPLAY.get(internal_key, internal_key)
if line == "LAND":
return LAND_DISPLAY.get(internal_key, internal_key)
return internal_key
@@ -292,6 +345,10 @@ def validate_required_fields(
"errors": [],
"business_line": line,
}
if line == "LAND":
land_subtype = (land_subtype or "").strip() or str(
normalized.get("运输类型") or ""
).strip()
if line == "LAND" and not (land_subtype or "").strip():
return {
"ok": False,
@@ -305,7 +362,7 @@ def validate_required_fields(
"business_line": line,
}
required = _pricing_required_keys(contract, line, land_subtype)
required = _pricing_required_keys(contract, line, land_subtype, normalized)
missing = [k for k in required if k != "报价日期" and not normalized.get(k)]
missing_display = [display_name(k, line) for k in missing]
ok = len(missing) == 0