落地陆运群内协同:按填写样例拉群并只列本单协同项,进群把私聊已抽到的字段带上。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -64,6 +64,90 @@ AIR_COLLAB_FIELDS = ("是否含电", "是否含磁")
|
||||
SEA_COLLAB_DISPLAY = {
|
||||
"HS编码": "商品海关编码",
|
||||
}
|
||||
# 陆运摘要标题:海关编码,不要写成海运的商品海关编码。
|
||||
LAND_COLLAB_DISPLAY = {
|
||||
"HS编码": "海关编码",
|
||||
}
|
||||
# 填写样例允许拉群的类型+线路 → 协同内部键(顺序与规格一致)。
|
||||
_LAND_COLLAB_BY_PAIR: dict[tuple[str, str], tuple[str, ...]] = {
|
||||
("国内运输拼车", "国内长途/零担"): (
|
||||
"客户名称",
|
||||
"包装方式",
|
||||
"货值",
|
||||
"是否为危险品",
|
||||
),
|
||||
("国内运输拼车", "国内城配/拖车/打包"): (
|
||||
"客户名称",
|
||||
"包装方式",
|
||||
"货值",
|
||||
"是否为危险品",
|
||||
),
|
||||
("国内运输整车", "国内长途/零担"): (
|
||||
"客户名称",
|
||||
"货值",
|
||||
"包装方式",
|
||||
"是否为危险品",
|
||||
),
|
||||
("国内运输整车", "国内城配/拖车/打包"): (
|
||||
"客户名称",
|
||||
"货值",
|
||||
"包装方式",
|
||||
"是否为危险品",
|
||||
),
|
||||
("跨境集拼", "中港/中亚/中欧"): (
|
||||
"客户名称",
|
||||
"HS编码",
|
||||
"贸易条款",
|
||||
"包装方式",
|
||||
"是否为危险品",
|
||||
),
|
||||
("跨境集拼", "东南亚"): (
|
||||
"客户名称",
|
||||
"HS编码",
|
||||
"贸易条款",
|
||||
"包装方式",
|
||||
"是否为危险品",
|
||||
),
|
||||
("跨境整车", "中港/中亚/中欧"): (
|
||||
"客户名称",
|
||||
"HS编码",
|
||||
"贸易条款",
|
||||
"货值",
|
||||
"包装方式",
|
||||
"是否为危险品",
|
||||
),
|
||||
("跨境整车", "东南亚"): (
|
||||
"客户名称",
|
||||
"HS编码",
|
||||
"贸易条款",
|
||||
"货值",
|
||||
"包装方式",
|
||||
"是否为危险品",
|
||||
),
|
||||
("中港整车", "中港/中亚/中欧"): (
|
||||
"客户名称",
|
||||
"HS编码",
|
||||
"车型/数量",
|
||||
"贸易条款",
|
||||
"货值",
|
||||
"包装方式",
|
||||
"是否为危险品",
|
||||
),
|
||||
("中港零担/集拼", "中港/中亚/中欧"): (
|
||||
"客户名称",
|
||||
"HS编码",
|
||||
"贸易条款",
|
||||
"包装方式",
|
||||
"是否为危险品",
|
||||
),
|
||||
}
|
||||
# 意图分类用:海运六项 ∪ 陆运全部协同键。
|
||||
ALL_COLLAB_KEYS = tuple(
|
||||
dict.fromkeys(
|
||||
list(SEA_COLLAB_FIELDS)
|
||||
+ ["客户名称", "包装方式", "货值", "是否为危险品", "车型/数量"]
|
||||
)
|
||||
)
|
||||
|
||||
# 口语里已经带上 TMS 文档规定的单位时,补进空字段。
|
||||
# 只认「数字+单位」,不猜港口,不把长宽高换算成立方。
|
||||
@@ -102,6 +186,9 @@ _ALIASES = {
|
||||
"HS CODE": "HS编码",
|
||||
"商品海关编码": "HS编码",
|
||||
"海关编码": "HS编码",
|
||||
"客户名称": "客户名称",
|
||||
"货值": "货值",
|
||||
"是否为危险品": "是否为危险品",
|
||||
}
|
||||
|
||||
|
||||
@@ -224,11 +311,75 @@ def harvest_oral_measures(text: str, facts: dict[str, str] | None = None) -> dic
|
||||
return harvest_named_air_route(raw, merged)
|
||||
|
||||
|
||||
def land_collab_fields(land_type: str, route: str) -> tuple[str, ...]:
|
||||
"""
|
||||
本单陆运协同内部键,顺序与规格一致。
|
||||
|
||||
对不上填写样例返回空元组,调用方不得按海运六项去列。
|
||||
海关编码内部仍是 HS编码。副作用:无。
|
||||
"""
|
||||
from agent.schema.land_options import canonicalize_route, canonicalize_transport_type
|
||||
|
||||
t = canonicalize_transport_type(land_type) or (land_type or "").strip()
|
||||
r = canonicalize_route(route) or (route or "").strip()
|
||||
return _LAND_COLLAB_BY_PAIR.get((t, r), ())
|
||||
|
||||
|
||||
def collab_fields_for_ticket(facts: dict[str, str] | None, business_line: str) -> tuple[str, ...]:
|
||||
"""
|
||||
按业务线取出群摘要 / 写入允许的协同键。
|
||||
|
||||
LAND 看事实里的运输类型+线路类别;SEA/AIR 用固定名单。
|
||||
"""
|
||||
line = (business_line or "").strip().upper()
|
||||
if line == "AIR":
|
||||
return AIR_COLLAB_FIELDS
|
||||
if line == "LAND":
|
||||
src = dict(facts or {})
|
||||
return land_collab_fields(
|
||||
str(src.get("运输类型") or ""),
|
||||
str(src.get("线路类别") or ""),
|
||||
)
|
||||
return SEA_COLLAB_FIELDS
|
||||
|
||||
|
||||
def collab_display_name(key: str, business_line: str) -> str:
|
||||
"""协同项对外标题。陆运 HS编码 → 海关编码;海运 → 商品海关编码。"""
|
||||
line = (business_line or "").strip().upper()
|
||||
if line == "LAND":
|
||||
return LAND_COLLAB_DISPLAY.get(key, key)
|
||||
return SEA_COLLAB_DISPLAY.get(key, key)
|
||||
|
||||
|
||||
def pick_collab_from_facts(
|
||||
facts: dict[str, str] | None,
|
||||
keys: tuple[str, ...],
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
从私聊事实里取出已有协同键,供进群预填。
|
||||
|
||||
只取 keys 里非空值。海关编码/商品海关编码经 normalize 折成 HS编码。
|
||||
不在 keys 的询价项(如跨境整车车型/数量)不会被取出。
|
||||
不覆盖调用方已有协同值;本函数只返回候选。
|
||||
"""
|
||||
allowed = set(keys or ())
|
||||
if not allowed:
|
||||
return {}
|
||||
src = normalize_facts(facts)
|
||||
out: dict[str, str] = {}
|
||||
for key in keys:
|
||||
val = (src.get(key) or "").strip()
|
||||
if val:
|
||||
out[key] = val
|
||||
return out
|
||||
|
||||
|
||||
def harvest_oral_collab(text: str) -> dict[str, str]:
|
||||
"""
|
||||
群里口语协同三项:含油/含电/含磁、货好时间。
|
||||
群里口语协同:含油/含电/含磁、货好时间、是否为危险品。
|
||||
|
||||
只认原话里已经说死的词(不含电、随时可提),不猜贸易条款和海关编码。
|
||||
只认原话里已经说死的词(不含电、随时可提、普货),不猜贸易条款和海关编码。
|
||||
危险品:否词(不是危险品/非危/普货)先于是词,避免「不是危险品」收成是。
|
||||
"""
|
||||
raw = text or ""
|
||||
out: dict[str, str] = {}
|
||||
@@ -236,6 +387,11 @@ def harvest_oral_collab(text: str) -> dict[str, str]:
|
||||
("是否含电", ("不含电", "不带电", "无电"), ("含电", "带电")),
|
||||
("是否含油", ("不含油", "不带油", "无油"), ("含油", "带油")),
|
||||
("是否含磁", ("不含磁", "不带磁", "无磁"), ("含磁", "带磁")),
|
||||
(
|
||||
"是否为危险品",
|
||||
("不是危险品", "非危险品", "非危", "普货"),
|
||||
("危险品", "有危", "含危"),
|
||||
),
|
||||
)
|
||||
for key, no_words, yes_words in yes_no:
|
||||
if any(w in raw for w in no_words):
|
||||
@@ -279,7 +435,8 @@ def _pricing_required_keys(
|
||||
land_subtype: str,
|
||||
facts: dict[str, str] | None = None,
|
||||
) -> list[str]:
|
||||
"""当前激活的首次查价内部键列表。中港再按装载方式收窄。"""
|
||||
"""当前激活的首次查价内部键列表。中港整车/零担是两个运输类型。"""
|
||||
_ = facts
|
||||
line = (business_line or "").strip().upper()
|
||||
pricing = contract.get("pricing_required") or {}
|
||||
if line == "SEA":
|
||||
@@ -291,14 +448,6 @@ def _pricing_required_keys(
|
||||
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 []
|
||||
|
||||
|
||||
@@ -1,61 +1,64 @@
|
||||
"""
|
||||
陆运选项规则:运输类型、线路类别、运输分类、中港装载方式。
|
||||
陆运选项规则:运输类型、线路类别、运输分类。
|
||||
|
||||
本文件职责:从销售原话收取标准项、校验联动、生成补问清单、判断「确定查价」意图。
|
||||
联动错误只纠正那一项能选什么,不整单作废。
|
||||
禁止:改六态;发明枚举;把选项当货物字段。
|
||||
本文件职责:从销售原话收取标准项、校验是否在名单内、生成补问清单(含填写样例表)、判断「确定查价」意图。
|
||||
三项各自必填,组合不拦截查价。拉群必须对上填写样例。运输分类句里的「中港/东南亚」不得冒充类型或线路。
|
||||
禁止:改六态;发明枚举;把选项当货物字段;再要单独的中港装载方式。
|
||||
线程:纯函数,无共享可变状态,可在 Worker 槽调用。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
# 与后台员工管理陆运「负责线路」同一套字,禁止改字。
|
||||
TRANSPORT_TYPES = (
|
||||
"国内运输拼车",
|
||||
"国内运输整车",
|
||||
"跨境集拼",
|
||||
"跨境整车",
|
||||
"中港",
|
||||
"中港整车",
|
||||
"中港零担/集拼",
|
||||
)
|
||||
DOMESTIC_ROUTES = ("国内长途/零担", "国内城配/拖车/打包")
|
||||
CROSS_ROUTES = ("中港/中亚/中欧", "东南亚")
|
||||
ROUTES_BY_TYPE = {
|
||||
"国内运输拼车": DOMESTIC_ROUTES,
|
||||
"国内运输整车": DOMESTIC_ROUTES,
|
||||
"跨境集拼": CROSS_ROUTES,
|
||||
"跨境整车": CROSS_ROUTES,
|
||||
"中港": CROSS_ROUTES,
|
||||
}
|
||||
CROSS_CLASSES = (
|
||||
"普件,中港、东南亚常用线路/常规车型",
|
||||
"普件,中亚、中欧、东南亚涉及国内+国外段",
|
||||
"普件,中亚、中欧、东南亚涉及境外段清派等特殊要求",
|
||||
ROUTES = (
|
||||
"国内长途/零担",
|
||||
"国内城配/拖车/打包",
|
||||
"中港/中亚/中欧",
|
||||
"东南亚",
|
||||
)
|
||||
LAND_CLASSES = (
|
||||
"普件(东南亚、中港常用线路/常规车型)",
|
||||
"普件(东南亚、中亚、中欧涉及国内+国外段)",
|
||||
"普件(东南亚、中亚、中欧涉及境外段清派等特殊要求)",
|
||||
"急件",
|
||||
)
|
||||
DOMESTIC_CLASSES = (
|
||||
"普件,常用线路/常规车型",
|
||||
"普件,非常用线路/临时线路/特种车型",
|
||||
"急件",
|
||||
)
|
||||
CLASSES_BY_ROUTE = {
|
||||
"中港/中亚/中欧": CROSS_CLASSES,
|
||||
"东南亚": CROSS_CLASSES,
|
||||
"国内长途/零担": DOMESTIC_CLASSES,
|
||||
"国内城配/拖车/打包": DOMESTIC_CLASSES,
|
||||
}
|
||||
HK_LOADS = ("整车", "零担/集拼")
|
||||
|
||||
# 口语别名 → 标准运输类型。长词优先,避免「拼车」误伤「整车」。
|
||||
# 填写样例,同时作为拉群门禁:对不上的类型+线路可以查价,但不能拉群。
|
||||
SAMPLE_PAIRS = (
|
||||
("国内运输拼车", "国内长途/零担"),
|
||||
("国内运输拼车", "国内城配/拖车/打包"),
|
||||
("国内运输整车", "国内长途/零担"),
|
||||
("国内运输整车", "国内城配/拖车/打包"),
|
||||
("跨境集拼", "中港/中亚/中欧"),
|
||||
("跨境集拼", "东南亚"),
|
||||
("跨境整车", "中港/中亚/中欧"),
|
||||
("跨境整车", "东南亚"),
|
||||
("中港整车", "中港/中亚/中欧"),
|
||||
("中港零担/集拼", "中港/中亚/中欧"),
|
||||
)
|
||||
|
||||
# 口语别名 → 标准运输类型。长词优先。禁止把单独「中港」收成类型。
|
||||
_TYPE_ALIASES = (
|
||||
("国内运输拼车", "国内运输拼车"),
|
||||
("国内运输整车", "国内运输整车"),
|
||||
("中港零担/集拼", "中港零担/集拼"),
|
||||
("中港零担", "中港零担/集拼"),
|
||||
("中港集拼", "中港零担/集拼"),
|
||||
("中港整车", "中港整车"),
|
||||
("国内拼车", "国内运输拼车"),
|
||||
("国内整车", "国内运输整车"),
|
||||
("跨境集拼", "跨境集拼"),
|
||||
("跨境整车", "跨境整车"),
|
||||
("中港", "中港"),
|
||||
)
|
||||
|
||||
_ROUTE_ALIASES = (
|
||||
@@ -67,20 +70,9 @@ _ROUTE_ALIASES = (
|
||||
("东南亚", "东南亚"),
|
||||
)
|
||||
|
||||
# 分类全称从长到短;「急件」单独出现才算急件,避免普件句被误收。
|
||||
# 分类全称从长到短;「急件」单独出现才算急件。
|
||||
_CLASS_CANON = tuple(
|
||||
sorted(
|
||||
{*(CROSS_CLASSES + DOMESTIC_CLASSES)},
|
||||
key=len,
|
||||
reverse=True,
|
||||
)
|
||||
)
|
||||
|
||||
_HK_LOAD_ALIASES = (
|
||||
("零担/集拼", "零担/集拼"),
|
||||
("零担", "零担/集拼"),
|
||||
("集拼", "零担/集拼"),
|
||||
("整车", "整车"),
|
||||
sorted(LAND_CLASSES, key=len, reverse=True)
|
||||
)
|
||||
|
||||
_CONFIRM_PHRASES = ("可以查了", "确定", "确认", "查价")
|
||||
@@ -92,6 +84,7 @@ def canonicalize_transport_type(raw: str) -> str:
|
||||
把销售原话收成标准运输类型。
|
||||
|
||||
参数:任意一句。返回 TRANSPORT_TYPES 之一或空串。
|
||||
只说「中港」不算选完,避免分类句里的中港把类型改掉。
|
||||
副作用:无。先匹配更长别名。
|
||||
"""
|
||||
text = (raw or "").strip()
|
||||
@@ -104,7 +97,7 @@ def canonicalize_transport_type(raw: str) -> str:
|
||||
|
||||
|
||||
def canonicalize_route(raw: str) -> str:
|
||||
"""把原话收成标准线路类别;对不上返回空串。"""
|
||||
"""把原话收成标准线路类别;对不上返回空串。不把单独「中港」收成中港/中亚/中欧。"""
|
||||
text = (raw or "").strip()
|
||||
if not text:
|
||||
return ""
|
||||
@@ -136,14 +129,25 @@ def canonicalize_land_class(raw: str) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def sample_pair_allowed(land_type: str, route: str) -> bool:
|
||||
"""
|
||||
类型+线路是否在填写样例里。
|
||||
|
||||
查价不调用本函数;点「拉产品进群协同」时必须为真,否则不建群。
|
||||
入参先收成标准项。副作用:无。
|
||||
"""
|
||||
t = canonicalize_transport_type(land_type) or (land_type or "").strip()
|
||||
r = canonicalize_route(route) or (route or "").strip()
|
||||
return (t, r) in SAMPLE_PAIRS
|
||||
|
||||
|
||||
def canonicalize_hk_load(raw: str) -> str:
|
||||
"""中港装载方式:整车或零担/集拼。"""
|
||||
text = (raw or "").strip()
|
||||
if not text:
|
||||
return ""
|
||||
for needle, canon in _HK_LOAD_ALIASES:
|
||||
if needle in text:
|
||||
return canon
|
||||
"""
|
||||
旧中港装载方式不再收取。
|
||||
|
||||
保留函数名避免旧调用处报缺符号;恒返回空串。新单用运输类型「中港整车」「中港零担/集拼」。
|
||||
"""
|
||||
_ = raw
|
||||
return ""
|
||||
|
||||
|
||||
@@ -151,9 +155,9 @@ def harvest_land_options(text: str, current: dict[str, str] | None = None) -> di
|
||||
"""
|
||||
从一句原话收取陆运选项,合并到已有值上。
|
||||
|
||||
先收运输分类并把命中句从原文挖掉,再收类型/线路,避免分类里的东南亚、中港冒充选项。
|
||||
已有非空项默认保留;本句明确写出另一标准项时才覆盖。
|
||||
已有值对不上标准枚举则丢掉,避免模型把「陆运」写成运输类型后整张清单重问。
|
||||
不改货物字段。返回新 dict,不修改 current。
|
||||
已有值对不上标准枚举则丢掉。不改货物字段。返回新 dict,不修改 current。
|
||||
"""
|
||||
merged = {
|
||||
k: str(v).strip()
|
||||
@@ -162,23 +166,22 @@ def harvest_land_options(text: str, current: dict[str, str] | None = None) -> di
|
||||
}
|
||||
_keep_canonical_options(merged)
|
||||
blob = text or ""
|
||||
found_type = canonicalize_transport_type(blob)
|
||||
found_route = canonicalize_route(blob)
|
||||
found_class = canonicalize_land_class(blob)
|
||||
found_load = canonicalize_hk_load(blob)
|
||||
remainder = blob.replace(found_class, " ") if found_class else blob
|
||||
found_type = canonicalize_transport_type(remainder)
|
||||
found_route = canonicalize_route(remainder)
|
||||
if found_type:
|
||||
merged["运输类型"] = found_type
|
||||
if found_route:
|
||||
merged["线路类别"] = found_route
|
||||
if found_class:
|
||||
merged["运输分类"] = found_class
|
||||
if found_load:
|
||||
merged["中港装载方式"] = found_load
|
||||
merged.pop("中港装载方式", None)
|
||||
return merged
|
||||
|
||||
|
||||
def _keep_canonical_options(merged: dict[str, str]) -> None:
|
||||
"""只留能对上枚举的选项;模型胡写的「陆运」不能冒充运输类型。"""
|
||||
"""只留能对上枚举的选项;模型胡写的「陆运」不能冒充运输类型。旧中港两项作废。"""
|
||||
t = canonicalize_transport_type(merged.get("运输类型") or "")
|
||||
if t:
|
||||
merged["运输类型"] = t
|
||||
@@ -194,138 +197,124 @@ def _keep_canonical_options(merged: dict[str, str]) -> None:
|
||||
merged["运输分类"] = c
|
||||
else:
|
||||
merged.pop("运输分类", None)
|
||||
load = canonicalize_hk_load(merged.get("中港装载方式") or "")
|
||||
if load:
|
||||
merged["中港装载方式"] = load
|
||||
else:
|
||||
merged.pop("中港装载方式", None)
|
||||
merged.pop("中港装载方式", None)
|
||||
|
||||
|
||||
def validate_land_options(opts: dict[str, str]) -> dict[str, Any]:
|
||||
"""
|
||||
校验选项是否齐全且联动正确。
|
||||
校验三项是否都在名单内。组合不拦截。
|
||||
|
||||
返回 ok / missing / errors。errors 每项 {field, message},只纠正错的那一项。
|
||||
中港缺装载方式进 missing,不改货物字段。
|
||||
返回 ok / missing / errors。对不上名单的项当缺项,不写联动纠正句。
|
||||
副作用:无。
|
||||
"""
|
||||
src = {k: str(v).strip() for k, v in dict(opts or {}).items() if str(v).strip()}
|
||||
t = canonicalize_transport_type(src.get("运输类型") or "") or src.get("运输类型") or ""
|
||||
r = canonicalize_route(src.get("线路类别") or "") or src.get("线路类别") or ""
|
||||
c = canonicalize_land_class(src.get("运输分类") or "") or src.get("运输分类") or ""
|
||||
load = canonicalize_hk_load(src.get("中港装载方式") or "") or src.get("中港装载方式") or ""
|
||||
t = canonicalize_transport_type(src.get("运输类型") or "")
|
||||
r = canonicalize_route(src.get("线路类别") or "")
|
||||
c = canonicalize_land_class(src.get("运输分类") or "")
|
||||
|
||||
missing: list[str] = []
|
||||
errors: list[dict[str, str]] = []
|
||||
|
||||
if t not in TRANSPORT_TYPES:
|
||||
missing.append("运输类型")
|
||||
t = ""
|
||||
|
||||
if t:
|
||||
allowed_routes = ROUTES_BY_TYPE.get(t) or ()
|
||||
if not r:
|
||||
missing.append("线路类别")
|
||||
elif r not in allowed_routes:
|
||||
errors.append(
|
||||
{
|
||||
"field": "线路类别",
|
||||
"message": "线路类别只能选:" + "、".join(f"「{x}」" for x in allowed_routes),
|
||||
}
|
||||
)
|
||||
r = ""
|
||||
elif r:
|
||||
# 类型还没有,线路先记下但不判联动
|
||||
pass
|
||||
|
||||
if r:
|
||||
allowed_cls = CLASSES_BY_ROUTE.get(r) or ()
|
||||
if not c:
|
||||
missing.append("运输分类")
|
||||
elif c not in allowed_cls:
|
||||
errors.append(
|
||||
{
|
||||
"field": "运输分类",
|
||||
"message": "运输分类只能选:" + "、".join(f"「{x}」" for x in allowed_cls),
|
||||
}
|
||||
)
|
||||
c = ""
|
||||
elif "运输分类" not in missing and not c:
|
||||
if r not in ROUTES:
|
||||
missing.append("线路类别")
|
||||
r = ""
|
||||
if c not in LAND_CLASSES:
|
||||
missing.append("运输分类")
|
||||
c = ""
|
||||
|
||||
if t == "中港":
|
||||
if load not in HK_LOADS:
|
||||
missing.append("中港装载方式")
|
||||
ok = not missing and not errors
|
||||
ok = not missing
|
||||
return {
|
||||
"ok": ok,
|
||||
"missing": missing,
|
||||
"errors": errors,
|
||||
"errors": [],
|
||||
"normalized": {
|
||||
"运输类型": t,
|
||||
"线路类别": r,
|
||||
"运输分类": c,
|
||||
"中港装载方式": load if t == "中港" else "",
|
||||
"中港装载方式": "",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _class_prompt_lines() -> list[str]:
|
||||
lines = ["运输分类(必填):可选择回复,"]
|
||||
for i, name in enumerate(LAND_CLASSES, start=1):
|
||||
lines.append(f"{i}.{name}")
|
||||
return lines
|
||||
|
||||
|
||||
def option_prompt_needs_sample(*, current: dict[str, str]) -> bool:
|
||||
"""运输类型或线路类别缺任一,就要发填写样例图。只缺分类不发。"""
|
||||
src = {k: str(v).strip() for k, v in dict(current or {}).items() if str(v).strip()}
|
||||
t = canonicalize_transport_type(src.get("运输类型") or "")
|
||||
r = canonicalize_route(src.get("线路类别") or "")
|
||||
return t not in TRANSPORT_TYPES or r not in ROUTES
|
||||
|
||||
|
||||
def sample_image_path() -> Path:
|
||||
"""填写样例表 PNG。和源码放一起,随 agent 发布包走。"""
|
||||
return Path(__file__).resolve().parent.parent / "assets" / "land_sample_pairs.png"
|
||||
|
||||
|
||||
def sample_fallback_text() -> str:
|
||||
"""
|
||||
图片发不出去时的文字兜底。
|
||||
|
||||
一行一种搭配,不再画框线。
|
||||
"""
|
||||
lines = ["填写样例"]
|
||||
for i, (typ, route) in enumerate(SAMPLE_PAIRS, start=1):
|
||||
lines.append(f"{i}.{typ} + {route}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _sample_table() -> str:
|
||||
"""兼容旧名:补问正文不再内嵌样例,只作图片失败兜底。"""
|
||||
return sample_fallback_text()
|
||||
|
||||
|
||||
def option_prompt(*, current: dict[str, str]) -> str:
|
||||
"""
|
||||
按已识别项生成补问清单。
|
||||
|
||||
全缺:一次列出类型、线路、分类、中港装载。
|
||||
类型已知:不再问类型,只列合法线路/分类。
|
||||
缺哪项列哪项。运输类型或线路类别缺任一则提示见图,样例图另发出站。只缺分类不附图。
|
||||
不修改 current。
|
||||
"""
|
||||
src = {k: str(v).strip() for k, v in dict(current or {}).items() if str(v).strip()}
|
||||
t = canonicalize_transport_type(src.get("运输类型") or "") or src.get("运输类型") or ""
|
||||
r = canonicalize_route(src.get("线路类别") or "") or src.get("线路类别") or ""
|
||||
t = canonicalize_transport_type(src.get("运输类型") or "")
|
||||
r = canonicalize_route(src.get("线路类别") or "")
|
||||
c = canonicalize_land_class(src.get("运输分类") or "")
|
||||
need_type = t not in TRANSPORT_TYPES
|
||||
need_route = r not in ROUTES
|
||||
need_class = c not in LAND_CLASSES
|
||||
lines: list[str] = []
|
||||
|
||||
if t not in TRANSPORT_TYPES:
|
||||
lines.append("请一次选择:运输类型、线路类别、运输分类。")
|
||||
if need_type and need_route and need_class:
|
||||
lines.append("请一次选择:运输类型、线路类别、运输分类")
|
||||
lines.append("")
|
||||
lines.append("运输类型:")
|
||||
lines.append("、".join(TRANSPORT_TYPES))
|
||||
lines.append("运输类型(必填):可选择回复," + "、".join(TRANSPORT_TYPES))
|
||||
lines.append("")
|
||||
lines.append("线路类别(跟运输类型联动):")
|
||||
lines.append("国内运输拼车 / 国内运输整车:" + "、".join(DOMESTIC_ROUTES))
|
||||
lines.append("跨境集拼 / 跨境整车 / 中港:" + "、".join(CROSS_ROUTES))
|
||||
lines.append("线路类别(必填):可选择回复," + "、".join(ROUTES))
|
||||
lines.append("")
|
||||
lines.append("运输分类(跟线路类别联动):")
|
||||
lines.append("中港/中亚/中欧、东南亚:" + ";".join(CROSS_CLASSES))
|
||||
lines.append("国内长途/零担、国内城配/拖车/打包:" + ";".join(DOMESTIC_CLASSES))
|
||||
lines.extend(_class_prompt_lines())
|
||||
lines.append("")
|
||||
lines.append("运输类型选中港时,还要选择:" + "、".join(HK_LOADS))
|
||||
lines.append("填写样例见图。")
|
||||
return "\n".join(lines).strip()
|
||||
|
||||
# 类型已认出来:不再问运输类型
|
||||
allowed_routes = ROUTES_BY_TYPE.get(t) or ()
|
||||
if r not in allowed_routes:
|
||||
lines.append(f"运输类型已选「{t}」。请选择线路类别:")
|
||||
lines.append("、".join(allowed_routes))
|
||||
route_for_class = ""
|
||||
else:
|
||||
route_for_class = r
|
||||
|
||||
if route_for_class:
|
||||
allowed_cls = CLASSES_BY_ROUTE.get(route_for_class) or ()
|
||||
if src.get("运输分类") not in allowed_cls:
|
||||
lines.append(f"线路类别已选「{route_for_class}」。请选择运输分类:")
|
||||
lines.append(";".join(allowed_cls))
|
||||
elif t in TRANSPORT_TYPES:
|
||||
# 线路未定,把该类型下各线路的分类一并列出,方便一次粘贴
|
||||
seen: set[str] = set()
|
||||
lines.append("运输分类(跟线路类别联动):")
|
||||
for route in allowed_routes:
|
||||
if route in seen:
|
||||
continue
|
||||
seen.add(route)
|
||||
lines.append(f"{route}:" + ";".join(CLASSES_BY_ROUTE.get(route) or ()))
|
||||
|
||||
if t == "中港" and (src.get("中港装载方式") not in HK_LOADS):
|
||||
lines.append("中港请再选择装载方式:" + "、".join(HK_LOADS))
|
||||
if need_type:
|
||||
lines.append("运输类型(必填):可选择回复," + "、".join(TRANSPORT_TYPES))
|
||||
if need_route:
|
||||
if lines:
|
||||
lines.append("")
|
||||
lines.append("线路类别(必填):可选择回复," + "、".join(ROUTES))
|
||||
if need_class:
|
||||
if lines:
|
||||
lines.append("")
|
||||
lines.extend(_class_prompt_lines())
|
||||
if need_type or need_route:
|
||||
lines.append("")
|
||||
lines.append("填写样例见图。")
|
||||
return "\n".join(lines).strip()
|
||||
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
本文件职责:核对卡通过后、建单查价前,确认当前运输类型的货物字段能拼出查价正文。
|
||||
带去:运输类型、线路类别、始发/目的/品名/报价日期,以及该类型的体积重量或车型/数量;通关口岸有值才带。
|
||||
出站类型/线路必须译成 TMS 6.1.1 英文枚举,线路键名 routeCategory。工单与卡片仍用中文。
|
||||
中港整车不带车型/数量。车型/数量是一个字段,出站再拆。
|
||||
禁止:把运输分类、中港装载方式放进出站;直连 TMS;改六态。
|
||||
中港整车、中港零担/集拼出站类型都是 CHINA_HK。中港整车不带车型/数量。
|
||||
禁止:把运输分类放进出站;直连 TMS;改六态。
|
||||
线程:纯函数。
|
||||
"""
|
||||
|
||||
@@ -16,20 +16,21 @@ import re
|
||||
from typing import Any
|
||||
|
||||
from agent.schema.field_validate import apply_quote_date_default, normalize_facts
|
||||
from agent.schema.land_options import canonicalize_hk_load
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
LCL_TYPES = frozenset({"国内运输拼车", "跨境集拼"})
|
||||
FTL_TYPES = frozenset({"国内运输整车", "跨境整车"})
|
||||
CUSTOMS_TYPES = frozenset({"跨境集拼", "跨境整车", "中港"})
|
||||
FTL_TYPES = frozenset({"国内运输整车", "跨境整车", "中港零担/集拼"})
|
||||
HK_FTL_TYPE = "中港整车"
|
||||
CUSTOMS_TYPES = frozenset({"跨境集拼", "跨境整车", "中港整车", "中港零担/集拼"})
|
||||
|
||||
_TYPE_ENUM = {
|
||||
"国内运输拼车": "DOMESTIC_LTL",
|
||||
"国内运输整车": "DOMESTIC_FTL",
|
||||
"跨境集拼": "CROSS_BORDER_LTL",
|
||||
"跨境整车": "CROSS_BORDER_FTL",
|
||||
"中港": "CHINA_HK",
|
||||
"中港整车": "CHINA_HK",
|
||||
"中港零担/集拼": "CHINA_HK",
|
||||
}
|
||||
_ROUTE_ENUM = {
|
||||
"国内长途/零担": "DOMESTIC_LONG_LTL",
|
||||
@@ -91,13 +92,12 @@ def assemble_land_query(facts: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""
|
||||
把已采集陆运字段拼成 TMS 查价参数。
|
||||
|
||||
不齐则 ok=False,missing 给销售补问。齐了也不带分类/中港装载。
|
||||
不齐则 ok=False,missing 给销售补问。齐了也不带分类。
|
||||
改了运输类型后,outbound 只带当前类型需要的键。
|
||||
"""
|
||||
src = apply_quote_date_default(
|
||||
normalize_facts({str(k): v for k, v in dict(facts or {}).items() if v is not None})
|
||||
)
|
||||
# 选项键不走港口别名折叠,再从原文补回
|
||||
raw = {str(k): str(v).strip() for k, v in dict(facts or {}).items() if v is not None}
|
||||
land_type = (raw.get("运输类型") or src.get("运输类型") or "").strip()
|
||||
route = (raw.get("线路类别") or src.get("线路类别") or "").strip()
|
||||
@@ -106,7 +106,6 @@ def assemble_land_query(facts: dict[str, Any] | None) -> dict[str, Any]:
|
||||
cargo = (src.get("品名") or "").strip()
|
||||
quote_date = (src.get("报价日期") or "").strip()
|
||||
customs = (raw.get("通关口岸") or src.get("通关口岸") or "").strip()
|
||||
hk_load = canonicalize_hk_load(raw.get("中港装载方式") or src.get("中港装载方式") or "")
|
||||
|
||||
missing: list[str] = []
|
||||
if not land_type:
|
||||
@@ -141,10 +140,9 @@ def assemble_land_query(facts: dict[str, Any] | None) -> dict[str, Any]:
|
||||
missing.append("毛重")
|
||||
else:
|
||||
outbound["weightKg"] = wgt
|
||||
elif land_type == "中港" and hk_load != "零担/集拼":
|
||||
# 中港整车(或尚未选定装载)不采集、不带出车型/数量
|
||||
elif land_type == HK_FTL_TYPE:
|
||||
pass
|
||||
elif land_type in FTL_TYPES or land_type == "中港":
|
||||
elif land_type in FTL_TYPES:
|
||||
combo = (
|
||||
src.get("车型/数量")
|
||||
or raw.get("车型/数量")
|
||||
|
||||
Reference in New Issue
Block a user