空运未填报价日期时不默认当天,查出舱位后由销售点选再写回日期。
包装类型里的托盘不再被抄进增值服务。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
"""
|
||||
空运文字询价确定性流程(模型只负责抽字段,本文件只编排与执行)。
|
||||
|
||||
本文件职责:补问到必填齐 → 核对确定后建单 → 查 TMS(一条出卡,多条 H5 选线路)。
|
||||
本文件职责:补问到必填齐 → 核对确定后建单 → 查 TMS。
|
||||
说了报价日期:一条直接出卡,没写起运地且多条才进选线路。
|
||||
没说报价日期:不填当天,查价不带日期,返回的舱位全部进 H5(1 条也要点),选定后不再查价。
|
||||
对不上机场也不能跳过查价、本地说暂无报价。
|
||||
调用:handler 在 Worker/inbox 线程;禁止回调线程同步跑。
|
||||
禁止:直连 TMS;用图状态冒充主账成功;用户文字冒充文件已生成。
|
||||
@@ -971,6 +973,8 @@ class AirTextInquiryFlow:
|
||||
# 让本句按新询价走,避免正文里的「确认」误伤。
|
||||
sess = self._detach_ticketed_for_new_inquiry(sess)
|
||||
|
||||
added_source = text
|
||||
prev_added = ""
|
||||
snap = extract_inquiry_snapshot(
|
||||
text,
|
||||
injected_facts=injected_facts,
|
||||
@@ -994,19 +998,26 @@ class AirTextInquiryFlow:
|
||||
facts = dict(snap.get("facts") or {})
|
||||
if sess and sess.phase in {"clarify", "need_mode", "wait_confirm"}:
|
||||
# 补问轮:旧字段打底;运输方式沿用上一句(飞=空运)
|
||||
# 增值服务按句累加,本句没提时不能把上一句已记下的服务冲掉。
|
||||
from agent.llm.extract_text import detect_transport_mode
|
||||
from agent.schema.field_validate import harvest_oral_measures
|
||||
|
||||
prev_added = str((sess.facts or {}).get("added_services") or "")
|
||||
merged = dict(sess.facts)
|
||||
merged.update(facts)
|
||||
source = waiting_mode_source(sess, text)
|
||||
facts = harvest_oral_measures(source, merged)
|
||||
added_source = waiting_mode_source(sess, text)
|
||||
facts = harvest_oral_measures(added_source, merged)
|
||||
mode = mode or (sess.business_line or "").upper()
|
||||
if not mode:
|
||||
mode = detect_transport_mode(
|
||||
f"{sess.immutable_text or ''}\n{text}"
|
||||
)
|
||||
|
||||
if (mode or "").upper() == "AIR":
|
||||
from agent.schema.added_services import apply_air_value_added
|
||||
|
||||
facts = apply_air_value_added(added_source, facts, previous=prev_added)
|
||||
|
||||
if not mode:
|
||||
reply(copy.ask_transport())
|
||||
self._save(
|
||||
@@ -1268,9 +1279,31 @@ class AirTextInquiryFlow:
|
||||
options_matching_origin,
|
||||
parse_air_options,
|
||||
project_selected_quote,
|
||||
sort_cabin_options,
|
||||
)
|
||||
|
||||
options = parse_air_options(quote)
|
||||
# 没填报价日期:不按起运地收口,TMS 返回的舱位全部给销售点。1 条也要点。
|
||||
if not str((sess.facts or {}).get("报价日期") or "").strip() and options:
|
||||
quote = dict(quote)
|
||||
quote["airOptions"] = sort_cabin_options(options)
|
||||
up = self._ledger.upsert_quote(work_order_no=no, quote=quote, to_status="已报价")
|
||||
if not up.get("ok"):
|
||||
reply("主账写入报价失败,不能当作已报价。")
|
||||
sess.phase = "ledger_fail"
|
||||
self._save(sess)
|
||||
return "ledger_fail"
|
||||
sess.quote = quote
|
||||
copy.remember_tms_quote(sess, quote)
|
||||
sess.tms_quote = dict(quote)
|
||||
sess.status = "已报价"
|
||||
return self._offer_air_routes(
|
||||
sess=sess,
|
||||
work_order_no=no,
|
||||
quote=quote,
|
||||
reply=reply,
|
||||
cabin_pick=True,
|
||||
)
|
||||
stated_origin = str(
|
||||
(sess.facts or {}).get("起运港") or (sess.facts or {}).get("起运地") or ""
|
||||
).strip()
|
||||
@@ -1335,8 +1368,13 @@ class AirTextInquiryFlow:
|
||||
work_order_no: str,
|
||||
quote: dict[str, Any],
|
||||
reply: ReplyFn,
|
||||
cabin_pick: bool = False,
|
||||
) -> str:
|
||||
"""多条线路:企微只发入口卡,选线在 H5。"""
|
||||
"""
|
||||
企微只发入口卡,点选在 H5。
|
||||
|
||||
cabin_pick:没填报价日期,列出全部舱位(含 1 条)。选定后用该条总价出报价,不再查价。
|
||||
"""
|
||||
from agent.channel.h5.air_route_store import build_air_route_url, issue_air_route_token
|
||||
from agent.schema.air_route_options import parse_air_options
|
||||
|
||||
@@ -1345,6 +1383,7 @@ class AirTextInquiryFlow:
|
||||
work_order_no=work_order_no,
|
||||
quote=quote,
|
||||
facts=dict(sess.facts),
|
||||
cabin_pick=cabin_pick,
|
||||
)
|
||||
url = build_air_route_url(token)
|
||||
count = len(parse_air_options(quote))
|
||||
@@ -1353,8 +1392,12 @@ class AirTextInquiryFlow:
|
||||
sess.wait_version += 1
|
||||
self._save(sess)
|
||||
reply(
|
||||
copy.air_route_entry_text(work_order_no=work_order_no, count=count),
|
||||
copy.air_route_entry_card(work_order_no=work_order_no, count=count, url=url),
|
||||
copy.air_route_entry_text(
|
||||
work_order_no=work_order_no, count=count, cabin_pick=cabin_pick
|
||||
),
|
||||
copy.air_route_entry_card(
|
||||
work_order_no=work_order_no, count=count, url=url, cabin_pick=cabin_pick
|
||||
),
|
||||
)
|
||||
return "wait_air_route"
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ def _cabin_copy(code: str, default: str, **kwargs: Any) -> str:
|
||||
return text
|
||||
|
||||
|
||||
# 空运补问:必填 7 项 + 非必填。报价日期默认当天,不进待补充。
|
||||
# 空运补问:必填项 + 非必填。报价日期非必填,没说不默认当天,不进待补充。
|
||||
_AIR_EXAMPLES = {
|
||||
"起运港": "南京",
|
||||
"目的港": "吉隆坡",
|
||||
@@ -72,6 +72,7 @@ _AIR_EXAMPLES = {
|
||||
"提货地址": "东莞仓库",
|
||||
"收货地址": "马尼拉",
|
||||
"贸易条款": "DDP、DAP、CIF、CIP",
|
||||
"added_services": "托盘、前置仓、带电、卡转、卡转低消、MUST GO、备装、分泡、温控、快件、DG、TC",
|
||||
}
|
||||
_AIR_CLARIFY = tuple(
|
||||
(key, label, _AIR_EXAMPLES[key]) for key, label in AIR_REQUIRED_FIELDS
|
||||
@@ -567,15 +568,17 @@ def air_confirm_card(facts: dict[str, str] | None) -> str:
|
||||
"""
|
||||
确定查价前的核对卡。没有工单号。
|
||||
|
||||
起运地(非必填)紧挨在目的地(必填)上面。其余非必填仍在报价日期后面。
|
||||
起运地(非必填)紧挨在目的地(必填)上面。
|
||||
报价日期说了写成「报价日期:日期」;没说写成「报价日期(非必填):」,不填当天。
|
||||
"""
|
||||
src = dict(facts or {})
|
||||
lines = ["运输方式(必填):空运"]
|
||||
lines.append(_air_field_line("起运港", _air_known_value(src, "起运港"), required=False))
|
||||
for key, _label in AIR_REQUIRED_FIELDS:
|
||||
if key == "报价日期":
|
||||
val = _air_known_value(src, key) or (src.get("报价日期") or "").strip()
|
||||
lines.append(_air_field_line(key, val, required=True))
|
||||
val = _air_known_value(src, key)
|
||||
# 说了日期就只写日期;没说标明非必填并留空,禁止写成当天。
|
||||
lines.append(f"报价日期:{val}" if val else "报价日期(非必填):")
|
||||
continue
|
||||
lines.append(_air_field_line(key, _air_known_value(src, key), required=True))
|
||||
for key, _label in AIR_OPTIONAL_FIELDS:
|
||||
@@ -616,7 +619,8 @@ def ask_clarify(
|
||||
"""
|
||||
缺项补问:先回显已识别,再给可复制清单;示例另起一行,如「1.件数(必填):」下一行「(如:10)」。
|
||||
|
||||
报价日期写进已识别(销售说了用原话,没说用当天),不出现在待补充。
|
||||
空运报价日期:说了就写成「报价日期:日期」,没说写成「报价日期(非必填):」,不进待补充。
|
||||
海运报价日期写进已识别(销售说了用原话,没说用当天),不出现在待补充。
|
||||
空运非必填一并列出,没填进待补充,不挡查价。
|
||||
"""
|
||||
src = dict(facts or {})
|
||||
@@ -635,6 +639,10 @@ def ask_clarify(
|
||||
lines.append(f"起运地:{origin}")
|
||||
for key, label, _example in rows:
|
||||
val = known(src, key)
|
||||
if not sea and key == "报价日期":
|
||||
# 空运日期不默认当天。没说也要占一行,方便销售看到这项可以空着。
|
||||
lines.append(f"报价日期:{val}" if val else "报价日期(非必填):")
|
||||
continue
|
||||
if val:
|
||||
lines.append(f"{label}:{val}")
|
||||
if not sea:
|
||||
@@ -758,7 +766,9 @@ def inquiry_field_lines(
|
||||
dest = (facts.get("目的港") or facts.get("目的地") or "-").strip()
|
||||
cargo = (facts.get("品名") or "-").strip() or "-"
|
||||
pack = (facts.get("包装方式") or facts.get("包装类型") or "-").strip() or "-"
|
||||
quote_date = (facts.get("报价日期") or "-").strip() or "-"
|
||||
quote_date = (facts.get("报价日期") or "").strip()
|
||||
if quote_date in {"-", "—", "-"}:
|
||||
quote_date = ""
|
||||
if (transport_mode or "").strip() == "海运":
|
||||
qty = (facts.get("货量") or facts.get("货物数量") or "-").strip() or "-"
|
||||
load = (facts.get("整柜或拼柜") or "-").strip() or "-"
|
||||
@@ -776,7 +786,7 @@ def inquiry_field_lines(
|
||||
f"箱型箱量:{box}",
|
||||
f"贸易条款:{trade}",
|
||||
f"运输分类:{klass}",
|
||||
f"报价日期:{quote_date}",
|
||||
f"报价日期:{quote_date or '-'}",
|
||||
]
|
||||
if (transport_mode or "").strip() == "陆运":
|
||||
land_type = (facts.get("运输类型") or "").strip()
|
||||
@@ -794,7 +804,7 @@ def inquiry_field_lines(
|
||||
if land_type in _LAND_CUSTOMS_TYPES:
|
||||
lines.append(f"通关口岸:{(facts.get('通关口岸') or '').strip()}")
|
||||
return lines
|
||||
return [
|
||||
lines = [
|
||||
f"工单号:{work_order_no}",
|
||||
f"运输方式:{transport_mode}",
|
||||
f"起运地:{origin}",
|
||||
@@ -804,8 +814,13 @@ def inquiry_field_lines(
|
||||
f"重量(KG):{_with_unit(str(facts.get('毛重') or ''), 'KGS')}",
|
||||
f"体积(CBM):{_with_unit(str(facts.get('体积') or ''), 'CBM')}",
|
||||
f"包装类型:{pack}",
|
||||
f"报价日期:{quote_date}",
|
||||
f"报价日期:{quote_date}" if quote_date else "报价日期(非必填):",
|
||||
]
|
||||
# 建单确认和空运群:有内容才多一行。没填不出现。核对卡的空行不在这里。
|
||||
added = (facts.get("added_services") or facts.get("增值服务") or "").strip()
|
||||
if added and added not in {"-", "—", "-"}:
|
||||
lines.insert(-1, f"增值服务:{added}")
|
||||
return lines
|
||||
|
||||
|
||||
def inquiry_card(
|
||||
@@ -1379,30 +1394,41 @@ def tms_hit_card(
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def air_route_entry_card(*, work_order_no: str, count: int, url: str) -> dict[str, Any]:
|
||||
def air_route_entry_card(
|
||||
*,
|
||||
work_order_no: str,
|
||||
count: int,
|
||||
url: str,
|
||||
cabin_pick: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
多条线路入口卡:短文案跳转 H5,避免企微截断。
|
||||
入口卡:短文案跳转 H5,避免企微截断。
|
||||
|
||||
cabin_pick 为真时按舱位条数原文展示(1 条也发)。
|
||||
说了报价日期的选线路仍至少写 2 条,和原来多线路入口一致。
|
||||
"""
|
||||
n = max(int(count or 0), 2)
|
||||
n = max(int(count or 0), 1 if cabin_pick else 2)
|
||||
noun = "舱位" if cabin_pick else "线路"
|
||||
fallback = "https://ai.ytd-scm.com/inquiry-form"
|
||||
action_url = (url or "").strip() or fallback
|
||||
return {
|
||||
"msgtype": "template_card",
|
||||
"template_card": {
|
||||
"card_type": "text_notice",
|
||||
"source": {"desc": "TMS线路报价", "desc_color": 0},
|
||||
"main_title": {"title": f"{work_order_no}已查到{n}条线路"},
|
||||
"sub_title_text": "点击查看并选择一条线路,选定后回企微看价格明细。",
|
||||
"source": {"desc": "TMS舱位报价" if cabin_pick else "TMS线路报价", "desc_color": 0},
|
||||
"main_title": {"title": f"{work_order_no}已查到{n}条{noun}"},
|
||||
"sub_title_text": f"点击查看并选择一条{noun},选定后回企微看价格明细。",
|
||||
"card_action": {"type": 1, "url": action_url},
|
||||
"jump_list": [{"type": 1, "title": "查看并选择线路", "url": action_url}],
|
||||
"jump_list": [{"type": 1, "title": f"查看并选择{noun}", "url": action_url}],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def air_route_entry_text(*, work_order_no: str, count: int) -> str:
|
||||
"""入口卡失败时的文字兜底。"""
|
||||
n = max(int(count or 0), 2)
|
||||
return f"{work_order_no}已查到{n}条线路,请点击卡片查看并选择。"
|
||||
def air_route_entry_text(*, work_order_no: str, count: int, cabin_pick: bool = False) -> str:
|
||||
"""入口卡失败时的文字兜底。没填日期时 1 条舱位也提示去点选。"""
|
||||
n = max(int(count or 0), 1 if cabin_pick else 2)
|
||||
noun = "舱位" if cabin_pick else "线路"
|
||||
return f"{work_order_no}已查到{n}条{noun},请点击卡片查看并选择。"
|
||||
|
||||
|
||||
def tms_hit_wecom_payload(
|
||||
|
||||
Reference in New Issue
Block a user