落地空运群内协同与锁舱出站。
群里按 BOT 通道激活当前工单、航线报价和成交跟进;没有 TMS 报价编号时用工单字段补齐仍调锁舱,避免被本地规则拦住。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import uuid
|
||||
from typing import Any, Iterable
|
||||
|
||||
from agent.schema.field_validate import (
|
||||
AIR_COLLAB_FIELDS,
|
||||
AIR_REQUIRED_FIELDS,
|
||||
SEA_COLLAB_DISPLAY,
|
||||
SEA_COLLAB_FIELDS,
|
||||
@@ -238,8 +239,8 @@ def inquiry_field_lines(
|
||||
|
||||
起运/目的展示销售原文(南京/北京),三字码只给出站 TMS,不对销售展示。
|
||||
"""
|
||||
origin = (facts.get("起运港") or "-").strip()
|
||||
dest = (facts.get("目的港") or "-").strip()
|
||||
origin = (facts.get("起运港") or facts.get("起运地") or "-").strip()
|
||||
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 "-"
|
||||
@@ -452,7 +453,7 @@ def quote_fee_lines(quote: dict[str, Any] | None) -> list[tuple[str, str]]:
|
||||
"""
|
||||
src = dict(quote or {})
|
||||
header_ccy = str(src.get("currency") or src.get("currencyCode") or "").strip()
|
||||
raw = src.get("fee_lines")
|
||||
raw = src.get("fee_lines") or src.get("fee_rows")
|
||||
parsed: Any = raw
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
try:
|
||||
@@ -988,6 +989,12 @@ def sea_group_create_fail(*, reason: str = "") -> str:
|
||||
return "建群失败,请稍后重试。"
|
||||
|
||||
|
||||
def quote_is_product(quote: dict[str, Any] | None) -> bool:
|
||||
"""来源是产品/航线人工,不能写进 TMS 报价段。"""
|
||||
label = str((quote or {}).get("source_label") or (quote or {}).get("source") or "")
|
||||
return "产品" in label or "航线" in label
|
||||
|
||||
|
||||
def sea_group_brief(
|
||||
*,
|
||||
work_order_no: str,
|
||||
@@ -997,12 +1004,13 @@ def sea_group_brief(
|
||||
sales_name: str,
|
||||
product_names: list[str],
|
||||
has_price: bool,
|
||||
tms_quote: dict[str, Any] | None = None,
|
||||
product_quote: dict[str, Any] | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
群里自动摘要:@销售 @产品 + 工单号 + 询价字段 + 协同字段 + TMS 报价。
|
||||
群里自动摘要:询价 + 协同 + TMS 报价 + 人工报价。
|
||||
|
||||
三段之间空一行,方便群里扫。协同没填写 -。
|
||||
HS编码对外写成商品海关编码。无价只写「TMS 暂无匹配报价」。
|
||||
TMS 无价只写「TMS 暂无匹配报价」,产品报的价单独放人工报价,不能混进 TMS。
|
||||
"""
|
||||
no = (work_order_no or "").strip()
|
||||
sales = (sales_name or "").strip() or "销售"
|
||||
@@ -1019,12 +1027,30 @@ def sea_group_brief(
|
||||
label = SEA_COLLAB_DISPLAY.get(key, key)
|
||||
val = (collab.get(key) or collab.get(label) or "").strip() or "-"
|
||||
lines.append(f"{label}:{val}")
|
||||
tms = dict(tms_quote) if tms_quote is not None else {}
|
||||
manual = dict(product_quote or {})
|
||||
if tms_quote is None:
|
||||
src = dict(quote or {})
|
||||
if has_price and src and not quote_is_product(src):
|
||||
tms = src
|
||||
elif quote_is_product(src):
|
||||
manual = manual or src
|
||||
lines.append("")
|
||||
lines.append("TMS报价:")
|
||||
if has_price and quote:
|
||||
lines.extend(quote_detail_lines(quote))
|
||||
if tms and (tms.get("total") or quote_fee_lines(tms)):
|
||||
lines.extend(quote_detail_lines(tms))
|
||||
lines.append(group_tms_confirm_hint())
|
||||
else:
|
||||
lines.append("TMS 暂无匹配报价")
|
||||
if manual and (quote_fee_lines(manual) or str(manual.get("total") or "").strip() not in {"", "-"}):
|
||||
lines.append("")
|
||||
lines.append("人工报价:")
|
||||
fee_lines = quote_fee_lines(manual)
|
||||
if fee_lines:
|
||||
lines.extend(f"{name}:{amount}" for name, amount in fee_lines)
|
||||
else:
|
||||
header_ccy = str(manual.get("currency") or "").strip()
|
||||
lines.append(f"合计:{money_text(str(manual.get('total') or '-'), header_ccy)}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@@ -1335,6 +1361,46 @@ def group_only_excel_pdf() -> str:
|
||||
return "群里只收 Excel 和 PDF,其它文件不会存入工单"
|
||||
|
||||
|
||||
def group_only_product_can_quote() -> str:
|
||||
return "只有产品能报价格,请产品在群里发价格或 Excel/PDF"
|
||||
|
||||
|
||||
def group_tms_confirm_hint() -> str:
|
||||
"""报价有效期下方:有 TMS 价时告诉产品还可以确认沿用。"""
|
||||
return "有TMS报价时可回复“确认报价”沿用该价;也可直接回复协同报价或上传报价附件。"
|
||||
|
||||
|
||||
def quote_attachment_saved_app(work_order_no: str, count: int = 1) -> str:
|
||||
"""群里告诉大家:模板或产品附件已进后台。"""
|
||||
no = (work_order_no or "").strip() or "WO"
|
||||
n = count if count > 0 else 1
|
||||
return f"工单{no}的{n}个附件已保存到后台工单"
|
||||
|
||||
|
||||
def group_confirm_need_tms() -> str:
|
||||
return "本单还没有可沿用的 TMS 报价,请直接回复协同报价或上传报价附件。"
|
||||
|
||||
|
||||
def group_ask_tms_or_product() -> str:
|
||||
return (
|
||||
"本单已有 TMS 报价。\n"
|
||||
"1. 要在 TMS 基础上改,请回复:在TMS基础上改(或直接回 1 / 第一个)\n"
|
||||
"2. 不要 TMS 报价、只用你刚说的价格,请回复:不用TMS报价(或直接回 2 / 第二个)"
|
||||
)
|
||||
|
||||
|
||||
def group_tms_choice_unclear() -> str:
|
||||
return "还没确定怎么用 TMS 报价。请回复:在TMS基础上改 / 不用TMS报价,也可以直接回 1 或 2。"
|
||||
|
||||
|
||||
def group_need_price_before_choice() -> str:
|
||||
return "请产品先发价格,再说在TMS基础上改或不用TMS报价"
|
||||
|
||||
|
||||
def group_file_saved() -> str:
|
||||
return "附件已保存后台"
|
||||
|
||||
|
||||
def group_quote_not_recognized() -> str:
|
||||
return "没有识别到报价,请产品再发一次价格或 Excel/PDF"
|
||||
|
||||
@@ -1349,6 +1415,10 @@ def group_deal_stale(*, work_order_no: str) -> str:
|
||||
return f"这张成交确认已过期。工单{no}已有更新的报价,请点击群里最新一条成交跟进。"
|
||||
|
||||
|
||||
def group_deal_need_quote() -> str:
|
||||
return "还没有可确认的报价,请先完成报价再确认成交状态。"
|
||||
|
||||
|
||||
def group_deal_sales_only() -> str:
|
||||
return "只有负责该工单的销售可以确认成交状态。"
|
||||
|
||||
@@ -1364,14 +1434,263 @@ def group_cannot_recreate(*, work_order_no: str) -> str:
|
||||
return f"工单{no}协同已结束,不能再拉群。"
|
||||
|
||||
|
||||
def group_deal_mention(*, sales_name: str, work_order_no: str) -> str:
|
||||
def group_deal_followup(*, sales_name: str, work_order_no: str) -> str:
|
||||
"""
|
||||
群里文字成交跟进。只提示输入,不含报价合计、不含点选按钮。
|
||||
|
||||
私聊成交卡仍走 deal_card,不改这里。
|
||||
"""
|
||||
name = (sales_name or "").strip() or "销售"
|
||||
return f"@{name}\n{deal_card(work_order_no=work_order_no)}"
|
||||
no = (work_order_no or "").strip()
|
||||
return (
|
||||
f"@{name}\n"
|
||||
f"询价工单 {no}\n"
|
||||
"报价单已发送,请确认成交状态\n"
|
||||
"咨询成交状态:请输入成交 / 未成交 / 协商中(未成交需补充原因)"
|
||||
)
|
||||
|
||||
|
||||
def group_deal_mention(*, sales_name: str, work_order_no: str) -> str:
|
||||
return group_deal_followup(sales_name=sales_name, work_order_no=work_order_no)
|
||||
|
||||
|
||||
def deal_ack(outcome: str, work_order_no: str) -> str:
|
||||
if outcome == "已成交":
|
||||
return f"已记录为「已成交」。工单 {work_order_no} 已关闭智能体跟进。"
|
||||
if outcome == "未成交":
|
||||
return f"已记录为「未成交」。请补充未成交原因,工单 {work_order_no} 将关闭智能体跟进。"
|
||||
return "已记录为「未成交」。请补充未成交原因。"
|
||||
return f"已记录为「协商中」。工单 {work_order_no} 跟进周期已顺延。"
|
||||
|
||||
|
||||
def air_need_work_order() -> str:
|
||||
"""空运群还没有当前单,且这句话里没有工单号。"""
|
||||
return "请先 @我 并发送要跟的工单号,例如 WO202609160001。"
|
||||
|
||||
|
||||
def air_lock_need_work_order() -> str:
|
||||
"""锁舱/释放必须自带工单号,不猜当前单。"""
|
||||
return "锁舱或释放舱位请带上工单号,例如 WO202609160001锁舱。"
|
||||
|
||||
|
||||
def air_ticket_not_found(work_order_no: str) -> str:
|
||||
no = (work_order_no or "").strip()
|
||||
return f"找不到工单{no},请核对工单号后再试。"
|
||||
|
||||
|
||||
def air_ticket_not_air(work_order_no: str) -> str:
|
||||
no = (work_order_no or "").strip()
|
||||
return f"工单{no}不是空运单,不能在空运群里跟。"
|
||||
|
||||
|
||||
def air_ticket_closed(work_order_no: str, status: str) -> str:
|
||||
no = (work_order_no or "").strip()
|
||||
st = (status or "").strip() or "已关闭"
|
||||
return f"工单{no}已是「{st}」,不能再激活。"
|
||||
|
||||
|
||||
def air_ticket_occupied(work_order_no: str) -> str:
|
||||
no = (work_order_no or "").strip()
|
||||
return f"工单{no}已经在另一个群跟了,请到那个群继续。"
|
||||
|
||||
|
||||
def air_group_brief(
|
||||
*,
|
||||
work_order_no: str,
|
||||
facts: dict[str, str],
|
||||
quote: dict[str, Any] | None,
|
||||
collab_facts: dict[str, str] | None,
|
||||
has_price: bool,
|
||||
tms_quote: dict[str, Any] | None = None,
|
||||
product_quote: dict[str, Any] | None = None,
|
||||
sales_name: str = "",
|
||||
airline_names: list[str] | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
空运群激活摘要:与海运进群同一套结构,字段用空运。
|
||||
|
||||
先 @ 销售和航线,再工单号 + 询价字段 + 协同补充(含电/含磁)+ TMS报价。
|
||||
不写「当前工单/询价要点/现在等到哪一步」。
|
||||
"""
|
||||
no = (work_order_no or "").strip()
|
||||
at_bits: list[str] = []
|
||||
sales = (sales_name or "").strip()
|
||||
if sales:
|
||||
at_bits.append(f"@{sales}")
|
||||
for name in airline_names or []:
|
||||
token = f"@{(name or '').strip()}"
|
||||
if (name or "").strip() and token not in at_bits:
|
||||
at_bits.append(token)
|
||||
lines = ([(" ".join(at_bits))] if at_bits else []) + [f"工单号:{no}", "询价字段:"]
|
||||
lines.extend(
|
||||
inquiry_field_lines(work_order_no=no, facts=facts, transport_mode="空运")[1:]
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("协同补充字段:")
|
||||
collab = dict(collab_facts or {})
|
||||
for key in AIR_COLLAB_FIELDS:
|
||||
val = (collab.get(key) or "").strip() or "-"
|
||||
lines.append(f"{key}:{val}")
|
||||
tms = dict(tms_quote) if tms_quote is not None else {}
|
||||
manual = dict(product_quote or {})
|
||||
if tms_quote is None:
|
||||
src = dict(quote or {})
|
||||
if has_price and src and not quote_is_product(src):
|
||||
tms = src
|
||||
elif quote_is_product(src):
|
||||
manual = manual or src
|
||||
lines.append("")
|
||||
lines.append("TMS报价:")
|
||||
if tms and (tms.get("total") or quote_fee_lines(tms)):
|
||||
lines.extend(quote_detail_lines(tms))
|
||||
lines.append(group_tms_confirm_hint())
|
||||
else:
|
||||
lines.append("TMS 暂无匹配报价")
|
||||
if manual and (quote_fee_lines(manual) or str(manual.get("total") or "").strip() not in {"", "-"}):
|
||||
lines.append("")
|
||||
lines.append("人工报价:")
|
||||
fee_lines = quote_fee_lines(manual)
|
||||
if fee_lines:
|
||||
lines.extend(f"{name}:{amount}" for name, amount in fee_lines)
|
||||
else:
|
||||
header_ccy = str(manual.get("currency") or "").strip()
|
||||
lines.append(f"合计:{money_text(str(manual.get('total') or '-'), header_ccy)}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def air_activate_summary(
|
||||
*,
|
||||
work_order_no: str,
|
||||
facts: dict,
|
||||
status: str = "",
|
||||
wait_hint: str = "",
|
||||
quote: dict[str, Any] | None = None,
|
||||
collab_facts: dict[str, str] | None = None,
|
||||
) -> str:
|
||||
"""兼容旧名:激活成功改走海运同款群摘要。"""
|
||||
_ = status, wait_hint
|
||||
src = dict(quote or {})
|
||||
return air_group_brief(
|
||||
work_order_no=work_order_no,
|
||||
facts=dict(facts or {}),
|
||||
quote=src,
|
||||
collab_facts=collab_facts,
|
||||
has_price=bool(src.get("total") or src.get("quoteId")),
|
||||
tms_quote=src if src and not quote_is_product(src) else {},
|
||||
product_quote=src if quote_is_product(src) else {},
|
||||
)
|
||||
|
||||
|
||||
def air_wait_hint(*, status: str, has_quote: bool, pending_lock: bool = False) -> str:
|
||||
"""激活摘要里的「现在等到哪一步」。"""
|
||||
if pending_lock:
|
||||
return "等待航线选择要锁的舱位选项"
|
||||
st = (status or "").strip()
|
||||
if st == "协商中":
|
||||
return "协商中,可继续改价或确认成交"
|
||||
if has_quote or st == "已报价":
|
||||
return "已有报价,可补含电/含磁、航线改价、锁舱或确认成交"
|
||||
return "等待航线报价或锁舱(须先有 TMS 报价)"
|
||||
|
||||
|
||||
def air_airline_only_quote() -> str:
|
||||
return "只有岗位为「航线·空运」的人能报价格。"
|
||||
|
||||
|
||||
def air_airline_only_cabin() -> str:
|
||||
return "只有岗位为「航线·空运」的人能锁舱或释放舱位。"
|
||||
|
||||
|
||||
def air_sales_only_deal() -> str:
|
||||
return "只有负责该工单的销售可以确认成交状态。"
|
||||
|
||||
|
||||
def air_fields_not_recognized() -> str:
|
||||
return "听到了。空运群里可补充是否含电、是否含磁。"
|
||||
|
||||
|
||||
def air_quote_not_recognized() -> str:
|
||||
return "没有识别到报价,请航线再发一次价格或 Excel/PDF。"
|
||||
|
||||
|
||||
def air_fields_denied() -> str:
|
||||
return "只有这张单的销售或航线可以补含电/含磁。"
|
||||
|
||||
|
||||
def air_no_tms_quote_to_lock(work_order_no: str) -> str:
|
||||
no = (work_order_no or "").strip()
|
||||
return f"工单{no}还没有可锁的 TMS 报价,不能锁舱。"
|
||||
|
||||
|
||||
def air_ask_lock_option(options: list) -> str:
|
||||
lines = ["这张单有多条舱位选项,请回复要锁哪一条,例如 AIR-OPT-01:"]
|
||||
for item in options:
|
||||
no = str(item.get("optionNo") or item.get("option_no") or "").strip()
|
||||
airline = str(item.get("airlineName") or item.get("airlineCode") or "").strip()
|
||||
if no and airline:
|
||||
lines.append(f"{no} {airline}")
|
||||
elif no:
|
||||
lines.append(no)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def air_lock_ok(
|
||||
*,
|
||||
work_order_no: str,
|
||||
operator: str,
|
||||
option_no: str,
|
||||
exec_no: str,
|
||||
quote_no: str,
|
||||
) -> str:
|
||||
return (
|
||||
f"工单{work_order_no} 已通知TMS执行空运锁舱,执行成功。\n"
|
||||
f"操作人:{operator}\n"
|
||||
f"舱位选项:{option_no}\n"
|
||||
f"执行编号:{exec_no}\n"
|
||||
f"报价编号:{quote_no}"
|
||||
)
|
||||
|
||||
|
||||
def air_lock_fail(*, work_order_no: str, operator: str, reason: str) -> str:
|
||||
why = (reason or "").strip() or "TMS未返回原因"
|
||||
return (
|
||||
f"工单{work_order_no} 空运锁舱失败。\n"
|
||||
f"操作人:{operator}\n"
|
||||
f"锁舱失败的原因:{why}\n"
|
||||
"已通知IT运维排查,请稍等。"
|
||||
)
|
||||
|
||||
|
||||
def air_release_ok(
|
||||
*,
|
||||
work_order_no: str,
|
||||
operator: str,
|
||||
option_no: str,
|
||||
exec_no: str,
|
||||
quote_no: str,
|
||||
) -> str:
|
||||
return (
|
||||
f"工单{work_order_no} 已通知TMS执行空运释放舱位成功\n"
|
||||
f"操作人:{operator}\n"
|
||||
f"舱位选项:{option_no}\n"
|
||||
f"执行编号:{exec_no}\n"
|
||||
f"报价编号:{quote_no}"
|
||||
)
|
||||
|
||||
|
||||
def air_release_fail(*, work_order_no: str, operator: str, reason: str) -> str:
|
||||
why = (reason or "").strip() or "TMS未返回原因"
|
||||
return (
|
||||
f"工单{work_order_no} 空运释放舱位失败\n"
|
||||
f"操作人:{operator}\n"
|
||||
f"释放舱位失败的原因:{why}"
|
||||
)
|
||||
|
||||
|
||||
def air_release_deal_blocked(work_order_no: str) -> str:
|
||||
no = (work_order_no or "").strip()
|
||||
return f"释放舱位失败,工单{no}已成交。"
|
||||
|
||||
|
||||
def air_release_need_lock(work_order_no: str) -> str:
|
||||
no = (work_order_no or "").strip()
|
||||
return f"工单{no}还没有有效锁舱,不能释放。"
|
||||
|
||||
Reference in New Issue
Block a user