落地陆运私聊文字询价:选项核对后再建单查价,并按线路拉产品进群。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,10 +16,13 @@ from typing import Any, Iterable
|
||||
from agent.schema.field_validate import (
|
||||
AIR_COLLAB_FIELDS,
|
||||
AIR_REQUIRED_FIELDS,
|
||||
LAND_DISPLAY,
|
||||
SEA_COLLAB_DISPLAY,
|
||||
SEA_COLLAB_FIELDS,
|
||||
SEA_REQUIRED_FIELDS,
|
||||
display_name,
|
||||
)
|
||||
from agent.schema.land_options import option_prompt as _land_option_prompt
|
||||
|
||||
# 空运补问:与合同 8 项一致。报价日期默认当天,不进待补充。
|
||||
_AIR_EXAMPLES = {
|
||||
@@ -96,9 +99,111 @@ _CONTINUE_HINT = (
|
||||
|
||||
|
||||
ASK_TRANSPORT = "请补充运输方式:海运 / 空运 / 陆运。"
|
||||
ASK_LAND_SUBTYPE = "请补充陆运类型:国内运输拼车 / 国内运输整车 / 跨境集拼 / 跨境整车 / 中港。"
|
||||
# 旧一句只问运输类型;销售要一次看到类型、线路、分类。残留调用也走完整清单。
|
||||
ASK_LAND_SUBTYPE = _land_option_prompt(current={})
|
||||
FIRST_CHAT = "你好,我是询价机器人。请直接描述客户需求,例如「上海到洛杉矶空运,3 件 180kg,下周三前到」,我会解析需求并查询 TMS 标准报价。"
|
||||
ROUTE_MODEL_FAILED = "刚才没听清,请再说一次:是新询价,还是要接着办哪张工单?"
|
||||
LAND_CONFIRM_TAIL = "请回复“确定”进行TMS查价,内容有误时,请重新发内容。"
|
||||
LAND_CONFIRM_EXPIRED = "刚才的陆运核对已过期,请把陆运需求再发一次,出核对卡后再回复确定。"
|
||||
LAND_TMS_RETRY = "工单 {wo} 上次查价没成功。回复「确定」按原需求再查一次,不用重新填。"
|
||||
LAND_TMS_RETRYING = "正在重新查询TMS报价。工单:{wo}"
|
||||
|
||||
_LAND_LCL_KEYS = ("起运港", "目的港", "品名", "体积", "毛重", "报价日期")
|
||||
_LAND_FTL_KEYS = ("起运港", "目的港", "品名", "车型/数量", "报价日期")
|
||||
_LAND_HK_FULL_KEYS = ("起运港", "目的港", "品名", "报价日期")
|
||||
_LAND_LCL_TYPES = frozenset({"国内运输拼车", "跨境集拼"})
|
||||
_LAND_FTL_TYPES = frozenset({"国内运输整车", "跨境整车"})
|
||||
_LAND_CUSTOMS_TYPES = frozenset({"跨境集拼", "跨境整车", "中港"})
|
||||
_LAND_EXAMPLES = {
|
||||
"起运港": "广州",
|
||||
"目的港": "深圳",
|
||||
"品名": "衣服",
|
||||
"体积": "100CBM",
|
||||
"毛重": "50kg",
|
||||
"车型/数量": "9米6/1",
|
||||
"通关口岸": "皇岗",
|
||||
}
|
||||
|
||||
|
||||
def _land_cargo_keys(land_type: str, facts: dict[str, str] | None = None) -> tuple[str, ...]:
|
||||
"""当前运输类型要展示的货物内部键。中港整车不要车型/数量。"""
|
||||
if land_type == "中港":
|
||||
load = str((facts or {}).get("中港装载方式") or "").strip()
|
||||
if load == "零担/集拼":
|
||||
return _LAND_FTL_KEYS
|
||||
# 整车或还没选定装载:卡片/补问都不出现车型/数量
|
||||
return _LAND_HK_FULL_KEYS
|
||||
if land_type in _LAND_FTL_TYPES:
|
||||
return _LAND_FTL_KEYS
|
||||
return _LAND_LCL_KEYS
|
||||
|
||||
|
||||
def land_confirm_card(facts: dict[str, str] | None) -> str:
|
||||
"""
|
||||
确定查价前的核对卡。没有工单号。
|
||||
|
||||
行列出运输类型/线路/分类(中港再加装载)和该类型货物字段。
|
||||
国内不出现通关口岸;跨境/中港通关口岸非必填,没填留空行。
|
||||
"""
|
||||
src = dict(facts or {})
|
||||
land_type = (src.get("运输类型") or "").strip()
|
||||
lines = [
|
||||
f"运输类型(必填):{land_type}",
|
||||
f"线路类别(必填):{(src.get('线路类别') or '').strip()}",
|
||||
f"运输分类(必填):{(src.get('运输分类') or '').strip()}",
|
||||
]
|
||||
if land_type == "中港":
|
||||
lines.append(f"中港装载方式(必填):{(src.get('中港装载方式') or '').strip()}")
|
||||
for key in _land_cargo_keys(land_type, src):
|
||||
label = LAND_DISPLAY.get(key, key)
|
||||
val = (src.get(key) or "").strip()
|
||||
lines.append(f"{label}(必填):{val}")
|
||||
if land_type in _LAND_CUSTOMS_TYPES:
|
||||
lines.append(f"通关口岸(非必填):{(src.get('通关口岸') or '').strip()}")
|
||||
lines.append("")
|
||||
lines.append(LAND_CONFIRM_TAIL)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def land_group_no_staff() -> str:
|
||||
"""没有对上线路的陆运产品,不建空群。"""
|
||||
return "没有可拉的产品人员。请先在后台「员工管理」维护本线路的陆运产品岗后再试。"
|
||||
|
||||
|
||||
def ask_land_fields(
|
||||
*,
|
||||
facts: dict[str, str] | None = None,
|
||||
missing_keys: Iterable[str] = (),
|
||||
land_subtype: str = "",
|
||||
) -> str:
|
||||
"""
|
||||
陆运货物缺项补问。不出核对卡、不出现确定句。
|
||||
|
||||
报价日期不进待补充。
|
||||
"""
|
||||
src = dict(facts or {})
|
||||
skip = {"报价日期", "运输类型", "线路类别", "运输分类", "中港装载方式"}
|
||||
missing = [k for k in missing_keys if k and k not in skip]
|
||||
land_type = (land_subtype or src.get("运输类型") or "").strip()
|
||||
lines = ["请确认并补充以下信息:", "运输方式:陆运"]
|
||||
if land_type:
|
||||
lines.append(f"运输类型:{land_type}")
|
||||
for key in _land_cargo_keys(land_type, src):
|
||||
val = (src.get(key) or "").strip()
|
||||
if val:
|
||||
lines.append(f"{LAND_DISPLAY.get(key, key)}:{val}")
|
||||
lines.append("")
|
||||
lines.append("待补充:")
|
||||
if not missing:
|
||||
return ""
|
||||
for key in missing:
|
||||
label = display_name(key, "LAND")
|
||||
example = _LAND_EXAMPLES.get(key, "")
|
||||
if example:
|
||||
lines.append(f"{label}:(如:{example})")
|
||||
else:
|
||||
lines.append(f"{label}:")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _air_known_value(facts: dict[str, str], key: str) -> str:
|
||||
@@ -259,6 +364,26 @@ def inquiry_field_lines(
|
||||
f"箱型箱量:{box}",
|
||||
f"报价日期:{quote_date}",
|
||||
]
|
||||
if (transport_mode or "").strip() == "陆运":
|
||||
land_type = (facts.get("运输类型") or "").strip()
|
||||
lines = [
|
||||
f"工单号:{work_order_no}",
|
||||
f"运输方式:{transport_mode}",
|
||||
f"运输类型:{land_type or '-'}",
|
||||
f"线路类别:{(facts.get('线路类别') or '-').strip() or '-'}",
|
||||
f"运输分类:{(facts.get('运输分类') or '-').strip() or '-'}",
|
||||
]
|
||||
if land_type == "中港":
|
||||
lines.append(
|
||||
f"中港装载方式:{(facts.get('中港装载方式') or '-').strip() or '-'}"
|
||||
)
|
||||
for key in _land_cargo_keys(land_type, facts):
|
||||
label = LAND_DISPLAY.get(key, key)
|
||||
val = (facts.get(key) or "-").strip() or "-"
|
||||
lines.append(f"{label}:{val}")
|
||||
if land_type in _LAND_CUSTOMS_TYPES:
|
||||
lines.append(f"通关口岸:{(facts.get('通关口岸') or '').strip()}")
|
||||
return lines
|
||||
return [
|
||||
f"工单号:{work_order_no}",
|
||||
f"运输方式:{transport_mode}",
|
||||
@@ -1006,19 +1131,22 @@ def sea_group_brief(
|
||||
has_price: bool,
|
||||
tms_quote: dict[str, Any] | None = None,
|
||||
product_quote: dict[str, Any] | None = None,
|
||||
transport_mode: str = "海运",
|
||||
) -> str:
|
||||
"""
|
||||
群里自动摘要:询价 + 协同 + TMS 报价 + 人工报价。
|
||||
|
||||
TMS 无价只写「TMS 暂无匹配报价」,产品报的价单独放人工报价,不能混进 TMS。
|
||||
陆运传入 transport_mode=陆运,始发站/货物字段跟核对卡同一套。
|
||||
"""
|
||||
no = (work_order_no or "").strip()
|
||||
sales = (sales_name or "").strip() or "销售"
|
||||
products = [n.strip() for n in (product_names or []) if (n or "").strip()]
|
||||
at_bits = [f"@{sales}"] + [f"@{n}" for n in products]
|
||||
mode = (transport_mode or "海运").strip() or "海运"
|
||||
lines = [" ".join(at_bits), f"工单号:{no}", "询价字段:"]
|
||||
lines.extend(
|
||||
inquiry_field_lines(work_order_no=no, facts=facts, transport_mode="海运")[1:]
|
||||
inquiry_field_lines(work_order_no=no, facts=facts, transport_mode=mode)[1:]
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("协同补充字段:")
|
||||
@@ -1123,8 +1251,9 @@ def scheme_horizontal_rows(
|
||||
"""
|
||||
卡片字段:一行一项,每格独立字数,不会像副标题整段截成 8CB…。
|
||||
|
||||
企微 horizontal 最多 6 条。运输方式写进航线行,避免丢字段。
|
||||
企微 horizontal 最多 6 条。运输方式写进行程行,避免丢字段。
|
||||
海运用货物数量/整柜或拼柜/箱型箱量,禁止套空运件数毛重体积包装。
|
||||
陆运用运输类型/线路类别/体积重量或车型数量,禁止套海运箱型或空运件数包装。
|
||||
不用 sub_title 堆字段(它会排到列表前面,且超长必截断)。
|
||||
"""
|
||||
src = facts or {}
|
||||
@@ -1145,6 +1274,15 @@ def scheme_horizontal_rows(
|
||||
{"keyname": "时效", "value": _clip(tms_eta_display(q), 26)},
|
||||
{"keyname": "报价有效期", "value": _clip(tms_valid_until_display(q), 26)},
|
||||
]
|
||||
if mode == "陆运":
|
||||
return [
|
||||
{"keyname": "工单号", "value": _clip(work_order_no, 26)},
|
||||
{"keyname": "线路", "value": _clip(f"陆运 {origin}→{dest}", 26)},
|
||||
{"keyname": "类型", "value": _clip(_land_scheme_type_line(src), 26)},
|
||||
{"keyname": "货物", "value": _clip(_land_scheme_goods_line(src), 26)},
|
||||
{"keyname": "时效", "value": _clip(tms_eta_display(q), 26)},
|
||||
{"keyname": "报价有效期", "value": _clip(tms_valid_until_display(q), 26)},
|
||||
]
|
||||
pieces = _with_unit(str(src.get("件数") or ""), "件")
|
||||
weight = _with_unit(str(src.get("毛重") or ""), "KGS")
|
||||
volume = _with_unit(str(src.get("体积") or ""), "CBM")
|
||||
@@ -1159,6 +1297,39 @@ def scheme_horizontal_rows(
|
||||
]
|
||||
|
||||
|
||||
def _land_scheme_type_line(facts: dict[str, str]) -> str:
|
||||
"""方案卡「类型」格:运输类型、中港装载、线路类别、运输分类。"""
|
||||
src = facts or {}
|
||||
land_type = (src.get("运输类型") or "-").strip() or "-"
|
||||
bits = [land_type]
|
||||
if land_type == "中港":
|
||||
load = (src.get("中港装载方式") or "").strip()
|
||||
if load:
|
||||
bits.append(load)
|
||||
route = (src.get("线路类别") or "-").strip() or "-"
|
||||
klass = (src.get("运输分类") or "-").strip() or "-"
|
||||
bits.extend([route, klass])
|
||||
return ";".join(bits)
|
||||
|
||||
|
||||
def _land_scheme_goods_line(facts: dict[str, str]) -> str:
|
||||
"""方案卡「货物」格:品名 + 当前类型的体积重量或车型/数量。"""
|
||||
src = facts or {}
|
||||
land_type = (src.get("运输类型") or "").strip()
|
||||
bits: list[str] = [(src.get("品名") or "-").strip() or "-"]
|
||||
for key in _land_cargo_keys(land_type, src):
|
||||
if key in {"起运港", "目的港", "品名", "报价日期"}:
|
||||
continue
|
||||
val = (src.get(key) or "").strip()
|
||||
if val:
|
||||
bits.append(val)
|
||||
if land_type in _LAND_CUSTOMS_TYPES:
|
||||
customs = (src.get("通关口岸") or "").strip()
|
||||
if customs:
|
||||
bits.append(customs)
|
||||
return ";".join(bits)
|
||||
|
||||
|
||||
def scheme_wecom_payload(
|
||||
*,
|
||||
work_order_no: str,
|
||||
@@ -1171,7 +1342,7 @@ def scheme_wecom_payload(
|
||||
一张报价方案卡:标题带工单号、引用条、字段、三个短按钮。
|
||||
|
||||
按钮文案必须短:生成Excel / 生成PDF / 不采用。长文案横排会被裁。
|
||||
海运/空运字段分开,禁止海运卡再出件数毛重体积。
|
||||
海运/空运/陆运字段分开,禁止陆运卡再出箱型或空运件数包装。
|
||||
"""
|
||||
_ = status
|
||||
src = dict(quote or {})
|
||||
|
||||
Reference in New Issue
Block a user