落地海运私聊文字询价全流程,海运多项费用改走文本明细,避免企微卡片引用条截断。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -32,6 +32,21 @@ AIR_REQUIRED_FIELDS = (
|
||||
)
|
||||
AIR_DISPLAY = {key: label for key, label in AIR_REQUIRED_FIELDS}
|
||||
|
||||
# 海运询价必填 7 项:销售侧名称。内部键「货量」对外叫「货物数量」。
|
||||
SEA_REQUIRED_FIELDS = (
|
||||
("起运港", "起运港"),
|
||||
("目的港", "目的港"),
|
||||
("品名", "品名"),
|
||||
("货量", "货物数量"),
|
||||
("整柜或拼柜", "整柜或拼柜"),
|
||||
("箱型箱量", "箱型箱量"),
|
||||
("报价日期", "报价日期"),
|
||||
)
|
||||
SEA_DISPLAY = {key: label for key, label in SEA_REQUIRED_FIELDS}
|
||||
|
||||
# 协同补充字段:拉群后可静默写入,不挡首次查价。
|
||||
SEA_COLLAB_FIELDS = ("贸易条款", "货好时间", "HS编码", "是否含油", "是否含电", "是否含磁")
|
||||
|
||||
# 口语里已经带上 TMS 文档规定的单位时,补进空字段。
|
||||
# 只认「数字+单位」,不猜港口,不把长宽高换算成立方。
|
||||
_RE_PIECES = re.compile(r"(?<![\d.])(\d+)\s*(件|pcs)\b", re.I)
|
||||
@@ -63,6 +78,8 @@ _ALIASES = {
|
||||
"重量": "毛重",
|
||||
"体积(CBM)": "体积",
|
||||
"车型/数量": "车型数量",
|
||||
"HS": "HS编码",
|
||||
"HS CODE": "HS编码",
|
||||
}
|
||||
|
||||
|
||||
@@ -161,6 +178,20 @@ def harvest_oral_measures(text: str, facts: dict[str, str] | None = None) -> dic
|
||||
return harvest_named_air_route(raw, merged)
|
||||
|
||||
|
||||
def project_sea_cargo_qty(facts: dict[str, str] | None) -> dict[str, str]:
|
||||
"""
|
||||
海运货物数量投影:原话「15件」常被抽成空运「件数」。
|
||||
|
||||
货量已有则不覆盖。件数有、货量空时,货量用件数。不猜吨位、不改空运校验。
|
||||
"""
|
||||
merged = dict(facts or {})
|
||||
qty = (merged.get("货量") or merged.get("货物数量") or "").strip()
|
||||
pieces = (merged.get("件数") or "").strip()
|
||||
if not qty and pieces:
|
||||
merged["货量"] = pieces
|
||||
return merged
|
||||
|
||||
|
||||
def apply_quote_date_default(facts: dict[str, str]) -> dict[str, str]:
|
||||
"""销售未提供报价日期时填当天;已有则不覆盖。"""
|
||||
merged = dict(facts)
|
||||
@@ -192,6 +223,8 @@ def display_name(internal_key: str, business_line: str = "AIR") -> str:
|
||||
line = (business_line or "").upper()
|
||||
if line == "AIR":
|
||||
return AIR_DISPLAY.get(internal_key, internal_key)
|
||||
if line == "SEA":
|
||||
return SEA_DISPLAY.get(internal_key, internal_key)
|
||||
return internal_key
|
||||
|
||||
|
||||
@@ -212,6 +245,8 @@ def validate_required_fields(
|
||||
contract = require_formal_contract("inquiry-required-fields-v1")
|
||||
normalized = apply_quote_date_default(normalize_facts(facts))
|
||||
line = (business_line or "").strip().upper()
|
||||
if line == "SEA":
|
||||
normalized = project_sea_cargo_qty(normalized)
|
||||
if line not in {"SEA", "AIR", "LAND"}:
|
||||
return {
|
||||
"ok": False,
|
||||
|
||||
@@ -33,7 +33,7 @@ def inquiry_fingerprint(facts: dict[str, Any], *, business_line: str) -> str:
|
||||
空运箱型不在空运必填里;海运箱型箱量若有则纳入。
|
||||
"""
|
||||
line = (business_line or "").strip().upper()
|
||||
keys = ["起运港", "目的港", "品名", "件数", "货量", "毛重", "体积", "箱型箱量", "报价日期"]
|
||||
keys = ["起运港", "目的港", "品名", "件数", "货量", "毛重", "体积", "箱型箱量", "整柜或拼柜", "报价日期"]
|
||||
parts = [line]
|
||||
for key in keys:
|
||||
parts.append(str(facts.get(key) or "").strip())
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
海运查价参数:对照 TMS V2 /quote/v2/query 是否带全。
|
||||
|
||||
本文件职责:询价卡片出现前,确认海运七项能拼出查价正文。
|
||||
港口只带名称,不做港口字典对应;箱型箱量保持原文字段,containerItems 只透传模型投影。
|
||||
禁止:本地拆箱型;因对不上港口字典跳过 TMS;改六态。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from agent.schema.field_validate import apply_quote_date_default
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _load_type(raw: str) -> str:
|
||||
"""整柜/拼柜原文透传;出站可带 FCL/LCL 提示,不拒绝口语。"""
|
||||
text = (raw or "").strip()
|
||||
return text
|
||||
|
||||
|
||||
def parse_container_items(facts: dict[str, Any] | None) -> list[dict[str, Any]]:
|
||||
"""
|
||||
只收下 DeepSeek B 已投影的 containerItems。
|
||||
|
||||
本地不得从「箱型箱量」字符串拆 type/quantity。
|
||||
"""
|
||||
src = dict(facts or {})
|
||||
raw = src.get("containerItems")
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
try:
|
||||
raw = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
if not isinstance(raw, list):
|
||||
return []
|
||||
items: list[dict[str, Any]] = []
|
||||
for row in raw:
|
||||
if isinstance(row, dict):
|
||||
items.append(dict(row))
|
||||
return items
|
||||
|
||||
|
||||
def assemble_sea_query(facts: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""
|
||||
把已采集海运字段拼成 TMS 查价参数。
|
||||
|
||||
不齐则 ok=False,missing 给销售补问;不建单、不出卡片。
|
||||
齐了也不因没有港口代码拦网。报价日期未说则填当天。
|
||||
"""
|
||||
src = apply_quote_date_default(
|
||||
{str(k): str(v) for k, v in dict(facts or {}).items() if v is not None}
|
||||
)
|
||||
origin = str(src.get("起运港") or "").strip()
|
||||
dest = str(src.get("目的港") or "").strip()
|
||||
cargo = str(src.get("品名") or "").strip()
|
||||
qty = str(src.get("货量") or src.get("货物数量") or "").strip()
|
||||
load = _load_type(str(src.get("整柜或拼柜") or ""))
|
||||
box = str(src.get("箱型箱量") or "").strip()
|
||||
quote_date = str(src.get("报价日期") or "").strip()
|
||||
|
||||
missing: list[str] = []
|
||||
missing_display: list[str] = []
|
||||
pairs = (
|
||||
("起运港", "起运港", origin),
|
||||
("目的港", "目的港", dest),
|
||||
("品名", "品名", cargo),
|
||||
("货量", "货物数量", qty),
|
||||
("整柜或拼柜", "整柜或拼柜", load),
|
||||
("箱型箱量", "箱型箱量", box),
|
||||
("报价日期", "报价日期", quote_date),
|
||||
)
|
||||
for key, label, val in pairs:
|
||||
if not val:
|
||||
missing.append(key)
|
||||
missing_display.append(label)
|
||||
|
||||
ok = not missing
|
||||
out_facts = dict(src)
|
||||
if origin:
|
||||
out_facts["起运港"] = origin
|
||||
if dest:
|
||||
out_facts["目的港"] = dest
|
||||
if cargo:
|
||||
out_facts["品名"] = cargo
|
||||
if qty:
|
||||
out_facts["货量"] = qty
|
||||
if load:
|
||||
out_facts["整柜或拼柜"] = load
|
||||
if box:
|
||||
out_facts["箱型箱量"] = box
|
||||
if quote_date:
|
||||
out_facts["报价日期"] = quote_date
|
||||
items = parse_container_items(src)
|
||||
if items:
|
||||
out_facts["containerItems"] = items
|
||||
|
||||
logger.info(
|
||||
"tms.sea.assemble ok=%s origin=%s dest=%s load=%s missing=%s",
|
||||
ok,
|
||||
origin or "-",
|
||||
dest or "-",
|
||||
load or "-",
|
||||
missing,
|
||||
)
|
||||
return {
|
||||
"ok": ok,
|
||||
"missing": missing,
|
||||
"missing_display": missing_display,
|
||||
"facts": out_facts,
|
||||
}
|
||||
Reference in New Issue
Block a user