落地多段联运私聊:齐了才建单拉群,附件只收格子值,核对改正不再整单重开。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,569 @@
|
||||
"""
|
||||
多段联运的群内报价与成交跟进。
|
||||
|
||||
本文件职责:按发言人岗位把报价归到对应段;一段报完就出该段报价单;
|
||||
全部段都出了报价单,才在群里发一张整票成交跟进,各段价格不加总。
|
||||
含空运时,原有群发出工单号后在这里激活:@人、发各段需求和模板。
|
||||
调用:群入站 Handler,在 Worker 线程。禁止查 TMS 运价;禁止按段各建工单。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from agent.llm.extract_text import extract_collab_fields, extract_product_quote
|
||||
from agent.policy import inquiry_copy as copy
|
||||
from agent.policy.multi_segments import Segment, group_brief, includes_air, segments_from_payload
|
||||
from agent.policy.sea_group_ops import (
|
||||
apply_group_deal,
|
||||
group_client_of,
|
||||
is_sales,
|
||||
send_filled_sheet,
|
||||
send_group_text,
|
||||
ticket_no,
|
||||
ticket_view,
|
||||
)
|
||||
from agent.schema.field_validate import collab_fields_for_ticket
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_ROLE_MODE = {"sea": "SEA", "land": "LAND", "air": "AIR"}
|
||||
SheetFn = Callable[..., dict[str, Any]]
|
||||
|
||||
|
||||
def ticket_is_multi(ticket: object) -> bool:
|
||||
"""主账业务线是多段联运。"""
|
||||
view = ticket_view(ticket)
|
||||
line = str(view.get("business_line") or view.get("businessLine") or "").upper()
|
||||
return line == "MULTI"
|
||||
|
||||
|
||||
def load_segments(ticket: object) -> list[Segment]:
|
||||
"""从工单事实里的 segments_json 读回各段。坏数据当没有段。"""
|
||||
view = ticket_view(ticket)
|
||||
facts = dict(view.get("facts") or {})
|
||||
raw = facts.get("segments_json") or ""
|
||||
if not raw:
|
||||
return []
|
||||
try:
|
||||
payload = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("多段 segments_json 无法解析 wo=%s", ticket_no(ticket))
|
||||
return []
|
||||
return segments_from_payload(payload)
|
||||
|
||||
|
||||
def segment_quotes(ticket: object) -> dict[str, dict[str, Any]]:
|
||||
"""已落到各段的报价。键是 SEA/LAND/AIR。"""
|
||||
quote = dict(ticket_view(ticket).get("quote") or {})
|
||||
raw = quote.get("segment_quotes") or {}
|
||||
if not isinstance(raw, dict):
|
||||
return {}
|
||||
out: dict[str, dict[str, Any]] = {}
|
||||
for key, val in raw.items():
|
||||
mode = str(key or "").upper()
|
||||
if mode in _ROLE_MODE.values() and isinstance(val, dict):
|
||||
out[mode] = dict(val)
|
||||
return out
|
||||
|
||||
|
||||
def sender_mode(flow: Any, sender_id: str) -> str:
|
||||
"""
|
||||
发言人负责哪一段。
|
||||
|
||||
海运产品归海运,陆运产品归陆运,航线·空运归空运。对不上返回空。
|
||||
"""
|
||||
book = getattr(flow, "ledger", None) or getattr(flow, "_ledger", None)
|
||||
finder = getattr(book, "get_staff_by_wecom_id", None) if book is not None else None
|
||||
if not callable(finder):
|
||||
return ""
|
||||
try:
|
||||
row = finder(wecom_id=sender_id) or {}
|
||||
except Exception:
|
||||
logger.exception("多段查岗位失败 wecom=%s", sender_id)
|
||||
return ""
|
||||
role = str(row.get("roleCode") or row.get("role_code") or "").strip().lower()
|
||||
return _ROLE_MODE.get(role, "")
|
||||
|
||||
|
||||
def handle_multi_inbound(
|
||||
*,
|
||||
flow: Any,
|
||||
ticket: object,
|
||||
chat_id: str,
|
||||
sender_id: str,
|
||||
text: str,
|
||||
injected_quote: Optional[dict[str, Any]] = None,
|
||||
sheet_fn: Optional[SheetFn] = None,
|
||||
) -> str:
|
||||
"""
|
||||
群里一条多段消息。
|
||||
|
||||
只含工单号:绑定本群并发摘要和各段模板。
|
||||
有价格:归到发言人那一段并出该段报价单。全部出齐才成交跟进。
|
||||
副作用:写主账报价、可能发群消息和模板。不查 TMS。
|
||||
"""
|
||||
room = (chat_id or "").strip()
|
||||
no = ticket_no(ticket)
|
||||
segments = load_segments(ticket)
|
||||
if not segments:
|
||||
send_group_text(group_client_of(flow), room, f"工单{no}没有可报价的运输段。")
|
||||
return "multi_no_segments"
|
||||
only_no = copy.extract_work_order_no(text or "") == no and _only_work_order(text or "")
|
||||
if only_no or not (ticket_view(ticket).get("collab_chat_id") or ""):
|
||||
_bind_room(flow, ticket, room, sender_id)
|
||||
ticket = _reload(flow, no) or ticket
|
||||
cabin = _cabin_intent(text or "")
|
||||
if cabin and includes_air(segments):
|
||||
return _cabin(flow, ticket, room, sender_id, text or "", cabin)
|
||||
if only_no:
|
||||
_send_activation(flow, ticket, room, segments, sender_id=sender_id, sheet_fn=sheet_fn)
|
||||
return "multi_activated"
|
||||
mode = sender_mode(flow, sender_id)
|
||||
target = next((seg for seg in segments if seg.mode == mode), None)
|
||||
fields = {}
|
||||
if target is not None:
|
||||
keys = collab_fields_for_ticket(target.facts, target.mode)
|
||||
fields = extract_collab_fields(text or "", allowed_keys=keys)
|
||||
if fields:
|
||||
_save_collab(flow, ticket, target, fields)
|
||||
ticket = _reload(flow, no) or ticket
|
||||
segments = load_segments(ticket)
|
||||
target = next((seg for seg in segments if seg.mode == mode), target)
|
||||
quote = extract_product_quote(text or "", injected=injected_quote)
|
||||
if not quote:
|
||||
if fields:
|
||||
return "multi_collab_saved"
|
||||
if is_sales(ticket, sender_id):
|
||||
return _maybe_deal(flow, ticket, sender_id, text)
|
||||
send_group_text(group_client_of(flow), room, copy.group_quote_not_recognized())
|
||||
return "quote_not_recognized"
|
||||
if target is None:
|
||||
send_group_text(group_client_of(flow), room, "没有对上你负责的运输段,请让对应岗位报价。")
|
||||
return "quote_wrong_role"
|
||||
return _save_segment_quote(
|
||||
flow=flow,
|
||||
ticket=ticket,
|
||||
chat_id=room,
|
||||
segment=target,
|
||||
segments=segments,
|
||||
quote=quote,
|
||||
sheet_fn=sheet_fn,
|
||||
)
|
||||
|
||||
|
||||
def deal_text(*, work_order_no: str, segments: list[Segment], quotes: dict[str, dict[str, Any]]) -> str:
|
||||
"""整票成交跟进文字。每段各自的价格、有效期,海运陆运再加时效。不加总。"""
|
||||
no = (work_order_no or "").strip()
|
||||
lines = [f"询价工单 {no}", copy.DEAL_SUBTITLE]
|
||||
for seg in segments:
|
||||
quote = quotes.get(seg.mode) or {}
|
||||
lines.append(f"段{seg.index}:{seg.word()}")
|
||||
rows = copy.deal_horizontal_rows(quote=quote, facts=seg.facts, business_line=seg.mode)
|
||||
lines.extend(f"{row['keyname']}:{row['value']}" for row in rows)
|
||||
lines.append(copy.DEAL_HINT)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def deal_payload(
|
||||
*,
|
||||
work_order_no: str,
|
||||
segments: list[Segment],
|
||||
quotes: dict[str, dict[str, Any]],
|
||||
card_version: int = 1,
|
||||
) -> dict[str, Any]:
|
||||
"""成交卡一行一段报价,不出现各段加总。按钮仍是整票一次确认。"""
|
||||
base = copy.deal_wecom_payload(
|
||||
work_order_no=work_order_no,
|
||||
quote={"source_label": "分段报价"},
|
||||
business_line="MULTI",
|
||||
card_version=card_version,
|
||||
)
|
||||
rows = []
|
||||
for seg in segments:
|
||||
quote = quotes.get(seg.mode) or {}
|
||||
total = copy.money_text(
|
||||
str(quote.get("total") or "-").strip() or "-",
|
||||
str(quote.get("currency") or ""),
|
||||
)
|
||||
rows.append({"keyname": f"段{seg.index}{seg.word()}", "value": copy._clip(total, 26)})
|
||||
card = dict(base.get("template_card") or {})
|
||||
card["horizontal_content_list"] = rows
|
||||
card["source"] = {"desc": "分段报价", "desc_color": 0}
|
||||
return {"msgtype": "template_card", "template_card": card}
|
||||
|
||||
|
||||
def _save_segment_quote(
|
||||
*,
|
||||
flow: Any,
|
||||
ticket: object,
|
||||
chat_id: str,
|
||||
segment: Segment,
|
||||
segments: list[Segment],
|
||||
quote: dict[str, Any],
|
||||
sheet_fn: Optional[SheetFn],
|
||||
) -> str:
|
||||
"""写入这一段报价并发该段报价单。其他段未齐时不发成交跟进。"""
|
||||
no = ticket_no(ticket)
|
||||
stored = segment_quotes(ticket)
|
||||
stored[segment.mode] = dict(quote)
|
||||
version = int(dict(ticket_view(ticket).get("quote") or {}).get("deal_version") or 0)
|
||||
all_ready = all(seg.mode in stored and _has_money(stored[seg.mode]) for seg in segments)
|
||||
if all_ready:
|
||||
version += 1
|
||||
payload = {
|
||||
"segment_quotes": stored,
|
||||
"source_label": "分段报价",
|
||||
"deal_version": version,
|
||||
}
|
||||
writer = getattr(flow.ledger, "upsert_quote", None)
|
||||
if callable(writer):
|
||||
writer(
|
||||
work_order_no=no,
|
||||
quote=payload,
|
||||
to_status="已报价" if all_ready else "",
|
||||
)
|
||||
ticket = _reload(flow, no) or ticket
|
||||
_send_sheet(
|
||||
flow=flow,
|
||||
ticket=ticket,
|
||||
chat_id=chat_id,
|
||||
segment=segment,
|
||||
quote=quote,
|
||||
sheet_fn=sheet_fn,
|
||||
)
|
||||
client = group_client_of(flow)
|
||||
if not all_ready:
|
||||
send_group_text(client, chat_id, f"段{segment.index}{segment.word()}报价单已发出。其他段报完后一起跟进成交。")
|
||||
return "segment_quoted"
|
||||
text = deal_text(work_order_no=no, segments=segments, quotes=stored)
|
||||
send_group_text(client, chat_id, text)
|
||||
carder = getattr(client, "send_group_card", None)
|
||||
if callable(carder):
|
||||
carder(
|
||||
chat_id=chat_id,
|
||||
template_card=deal_payload(
|
||||
work_order_no=no,
|
||||
segments=segments,
|
||||
quotes=stored,
|
||||
card_version=version,
|
||||
).get("template_card")
|
||||
or {},
|
||||
)
|
||||
logger.info("multi.deal wo=%s segments=%s", no, [seg.mode for seg in segments])
|
||||
return "multi_deal_sent"
|
||||
|
||||
|
||||
def _cabin(flow: Any, ticket: object, chat_id: str, sender_id: str, text: str, intent: str) -> str:
|
||||
"""空运段的锁舱、释放走现有空运规则,不挡住其他段报价。"""
|
||||
from agent.policy.air_group_ops import apply_air_lock, apply_air_release
|
||||
|
||||
if intent == "release":
|
||||
return apply_air_release(
|
||||
flow=flow,
|
||||
ticket=ticket,
|
||||
sender_id=sender_id,
|
||||
chat_id=chat_id,
|
||||
instruction=text,
|
||||
)
|
||||
return apply_air_lock(
|
||||
flow=flow,
|
||||
ticket=ticket,
|
||||
sender_id=sender_id,
|
||||
chat_id=chat_id,
|
||||
instruction=text,
|
||||
)
|
||||
|
||||
|
||||
def _cabin_intent(text: str) -> str:
|
||||
raw = text or ""
|
||||
if "释放" in raw and "舱" in raw:
|
||||
return "release"
|
||||
if "锁舱" in raw:
|
||||
return "lock"
|
||||
return ""
|
||||
|
||||
|
||||
def _send_activation(
|
||||
flow: Any,
|
||||
ticket: object,
|
||||
chat_id: str,
|
||||
segments: list[Segment],
|
||||
*,
|
||||
sender_id: str = "",
|
||||
sheet_fn: Optional[SheetFn],
|
||||
) -> None:
|
||||
"""原有群激活:@具体销售和各段产品,发核对同款字段,每段一份模板。不含 TMS。"""
|
||||
no = ticket_no(ticket)
|
||||
sales_name, products, mention_ids = _activation_mentions(
|
||||
flow, ticket, segments, sender_id=sender_id
|
||||
)
|
||||
brief = group_brief(
|
||||
work_order_no=no,
|
||||
segments=segments,
|
||||
sales_name=sales_name,
|
||||
mention_names=products,
|
||||
)
|
||||
send_group_text(group_client_of(flow), chat_id, brief, mention_userids=mention_ids)
|
||||
for seg in segments:
|
||||
_send_sheet(flow=flow, ticket=ticket, chat_id=chat_id, segment=seg, quote={}, sheet_fn=sheet_fn)
|
||||
|
||||
|
||||
def _activation_mentions(
|
||||
flow: Any,
|
||||
ticket: object,
|
||||
segments: list[Segment],
|
||||
sender_id: str = "",
|
||||
) -> tuple[str, list[str], list[str]]:
|
||||
"""激活时要 @ 的人:具体销售 + 各段对得上的产品岗;空运按航线岗名单。"""
|
||||
from agent.policy.sea_group_ops import resolve_group_mentions
|
||||
|
||||
sales_name, products, mention_ids = resolve_group_mentions(
|
||||
flow, ticket, None, fallback_sales_id=sender_id
|
||||
)
|
||||
book = getattr(flow, "ledger", None) or getattr(flow, "_ledger", None)
|
||||
for seg in segments:
|
||||
for row in _staff_rows(book, seg):
|
||||
uid = str(row.get("wecomId") or row.get("wecom_id") or "").strip()
|
||||
name = str(row.get("name") or "").strip() or uid
|
||||
if uid and uid not in mention_ids:
|
||||
mention_ids.append(uid)
|
||||
if name and name not in products and name != sales_name:
|
||||
products.append(name)
|
||||
return sales_name, products, mention_ids
|
||||
|
||||
|
||||
def _staff_rows(book: Any, seg: Segment) -> list[dict[str, Any]]:
|
||||
"""一段要对上的产品人员。线路没填时,陆运/空运按岗位名单,不猜港口。"""
|
||||
if book is None:
|
||||
return []
|
||||
try:
|
||||
if seg.mode == "SEA":
|
||||
matcher = getattr(book, "match_sea_staff", None)
|
||||
if not callable(matcher):
|
||||
return []
|
||||
rows = matcher(
|
||||
origin=str(seg.facts.get("起运港") or ""),
|
||||
destination=str(seg.facts.get("目的港") or ""),
|
||||
) or []
|
||||
return [dict(x) for x in rows if isinstance(x, dict)]
|
||||
if seg.mode == "LAND":
|
||||
matcher = getattr(book, "match_land_staff", None)
|
||||
rows = []
|
||||
if callable(matcher):
|
||||
rows = matcher(route_category=str(seg.facts.get("线路类别") or "")) or []
|
||||
hits = [dict(x) for x in rows if isinstance(x, dict)]
|
||||
if hits:
|
||||
return hits
|
||||
lister = getattr(book, "list_staff_by_role", None)
|
||||
if not callable(lister):
|
||||
return []
|
||||
return [dict(x) for x in (lister(role_code="land") or []) if isinstance(x, dict)]
|
||||
if seg.mode == "AIR":
|
||||
lister = getattr(book, "list_staff_by_role", None)
|
||||
if not callable(lister):
|
||||
return []
|
||||
return [dict(x) for x in (lister(role_code="air") or []) if isinstance(x, dict)]
|
||||
except Exception:
|
||||
logger.exception("多段激活匹配产品失败 mode=%s", seg.mode)
|
||||
return []
|
||||
|
||||
|
||||
def _send_sheet(
|
||||
*,
|
||||
flow: Any,
|
||||
ticket: object,
|
||||
chat_id: str,
|
||||
segment: Segment,
|
||||
quote: dict[str, Any],
|
||||
sheet_fn: Optional[SheetFn],
|
||||
) -> None:
|
||||
"""按这一段的运输方式匹配模板。失败只记日志,不挡住报价归段。"""
|
||||
sender = sheet_fn or send_filled_sheet
|
||||
try:
|
||||
sender(
|
||||
flow=flow,
|
||||
ticket=_sheet_ticket(ticket, segment),
|
||||
quote_override=dict(quote or {}),
|
||||
business_line=segment.mode,
|
||||
chat_id=chat_id,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("多段模板发送失败 wo=%s mode=%s", ticket_no(ticket), segment.mode)
|
||||
|
||||
|
||||
def _sheet_ticket(ticket: object, segment: Segment) -> object:
|
||||
"""填表时用这一段的询价字段,避免海运模板吃到空运字段。"""
|
||||
view = ticket_view(ticket)
|
||||
facts = _facts_for_segment_sheet(segment)
|
||||
sales_id = str(view.get("sales_wecom_id") or view.get("salesWecomId") or "").strip()
|
||||
if isinstance(ticket, dict):
|
||||
cloned = dict(ticket)
|
||||
cloned["facts"] = facts
|
||||
cloned["business_line"] = segment.mode
|
||||
cloned["businessLine"] = segment.mode
|
||||
if sales_id:
|
||||
cloned["sales_wecom_id"] = sales_id
|
||||
cloned["salesWecomId"] = sales_id
|
||||
return cloned
|
||||
return {
|
||||
"work_order_no": view.get("work_order_no"),
|
||||
"workOrderNo": view.get("work_order_no"),
|
||||
"facts": facts,
|
||||
"business_line": segment.mode,
|
||||
"businessLine": segment.mode,
|
||||
"collab_chat_id": view.get("collab_chat_id"),
|
||||
"sales_wecom_id": sales_id,
|
||||
"salesWecomId": sales_id,
|
||||
"quote": {},
|
||||
}
|
||||
|
||||
|
||||
def _facts_for_segment_sheet(segment: Segment) -> dict[str, str]:
|
||||
"""
|
||||
一段填表用的字段。
|
||||
|
||||
多段不查 TMS,陆运常缺线路类别。
|
||||
后台陆运拼车模板关键词是「国内拼车」,询价字段是「国内运输拼车」,
|
||||
不补别名就套不上表,只会发出空运模板。
|
||||
"""
|
||||
facts = {k: str(v).strip() for k, v in dict(segment.facts).items() if str(v or "").strip()}
|
||||
if segment.mode != "LAND":
|
||||
return facts
|
||||
from agent.schema.land_options import SAMPLE_PAIRS, canonicalize_transport_type
|
||||
|
||||
land_type = canonicalize_transport_type(str(facts.get("运输类型") or ""))
|
||||
if land_type:
|
||||
facts["运输类型"] = land_type
|
||||
if land_type and not str(facts.get("线路类别") or "").strip():
|
||||
for pair_type, pair_route in SAMPLE_PAIRS:
|
||||
if pair_type == land_type:
|
||||
facts["线路类别"] = pair_route
|
||||
break
|
||||
hint = _LAND_SHEET_HINTS.get(land_type, "")
|
||||
if hint:
|
||||
facts["模板匹配"] = hint
|
||||
return facts
|
||||
|
||||
|
||||
_LAND_SHEET_HINTS = {
|
||||
"国内运输拼车": "国内拼车 国内零担",
|
||||
"国内运输整车": "国内整车",
|
||||
"跨境整车": "跨境整车",
|
||||
"跨境集拼": "跨境集拼 跨境零担",
|
||||
"中港整车": "中港",
|
||||
"中港零担/集拼": "中港",
|
||||
}
|
||||
|
||||
|
||||
def _save_collab(flow: Any, ticket: object, segment: Segment, fields: dict[str, str]) -> None:
|
||||
"""协同字段写进对应段,不挡报价。"""
|
||||
segments = load_segments(ticket)
|
||||
for seg in segments:
|
||||
if seg.mode != segment.mode:
|
||||
continue
|
||||
seg.facts.update({k: str(v) for k, v in fields.items() if str(v).strip()})
|
||||
_write_segments(flow, ticket_no(ticket), segments)
|
||||
writer = getattr(flow.ledger, "patch_collab_facts", None)
|
||||
if callable(writer):
|
||||
writer(work_order_no=ticket_no(ticket), facts=fields, sender_id="")
|
||||
|
||||
|
||||
def _bind_room(flow: Any, ticket: object, chat_id: str, sender_id: str) -> None:
|
||||
"""把原有群绑到这张多段工单。已绑同一个群则不重复建。"""
|
||||
view = ticket_view(ticket)
|
||||
if str(view.get("collab_chat_id") or "") == chat_id:
|
||||
return
|
||||
binder = getattr(flow.ledger, "bind_collab_group", None)
|
||||
members = [str(x) for x in (view.get("collab_member_ids") or [])]
|
||||
if sender_id and sender_id not in members:
|
||||
members.append(sender_id)
|
||||
sales = str(view.get("sales_wecom_id") or view.get("salesWecomId") or sender_id or "")
|
||||
if sales and sales not in members:
|
||||
members.append(sales)
|
||||
if callable(binder):
|
||||
binder(
|
||||
work_order_no=ticket_no(ticket),
|
||||
chat_id=chat_id,
|
||||
member_ids=members,
|
||||
product_names=list(view.get("product_names") or []),
|
||||
product_ids=list(view.get("product_ids") or []),
|
||||
)
|
||||
|
||||
|
||||
def _maybe_deal(flow: Any, ticket: object, sender_id: str, text: str) -> str:
|
||||
"""销售在群里确认成交。还有段没报价单时不确认。"""
|
||||
from agent.routing.group_intent import (
|
||||
INTENT_DEAL,
|
||||
INTENT_LOST,
|
||||
INTENT_NEGOTIATE,
|
||||
classify_group_text,
|
||||
)
|
||||
|
||||
intent = classify_group_text(text or "")
|
||||
outcome = {INTENT_DEAL: "已成交", INTENT_LOST: "未成交", INTENT_NEGOTIATE: "协商中"}.get(intent)
|
||||
if not outcome:
|
||||
return "multi_noop"
|
||||
segments = load_segments(ticket)
|
||||
quotes = segment_quotes(ticket)
|
||||
if not all(seg.mode in quotes and _has_money(quotes[seg.mode]) for seg in segments):
|
||||
send_group_text(
|
||||
group_client_of(flow),
|
||||
str(ticket_view(ticket).get("collab_chat_id") or ""),
|
||||
"还有运输段没有报价单,全部报完后再确认成交。",
|
||||
)
|
||||
return "deal_wait_segments"
|
||||
return apply_group_deal(flow=flow, ticket=ticket, sender_id=sender_id, outcome=outcome)
|
||||
|
||||
|
||||
def _write_segments(flow: Any, work_order_no: str, segments: list[Segment]) -> None:
|
||||
from agent.policy.multi_segments import segments_to_payload
|
||||
|
||||
writer = getattr(flow.ledger, "update_facts", None)
|
||||
if not callable(writer):
|
||||
return
|
||||
current = {}
|
||||
getter = getattr(flow.ledger, "get_ticket", None)
|
||||
if callable(getter):
|
||||
ticket = getter(work_order_no=work_order_no)
|
||||
current = dict(ticket_view(ticket).get("facts") or {})
|
||||
current["segments_json"] = json.dumps(segments_to_payload(segments), ensure_ascii=False)
|
||||
writer(work_order_no=work_order_no, facts=current)
|
||||
|
||||
|
||||
def _reload(flow: Any, work_order_no: str) -> object:
|
||||
getter = getattr(flow.ledger, "get_ticket", None)
|
||||
if callable(getter):
|
||||
return getter(work_order_no=work_order_no)
|
||||
return None
|
||||
|
||||
|
||||
def _has_money(quote: dict[str, Any]) -> bool:
|
||||
return bool(str(quote.get("total") or "").strip() or quote.get("fee_rows") or quote.get("fee_lines"))
|
||||
|
||||
|
||||
def _only_work_order(text: str) -> bool:
|
||||
"""去掉工单号和 @询价小助手 后几乎没字,当作激活。与空运群同一套,避免 WO 数字被当成报价。"""
|
||||
from agent.routing.air_group_intent import _only_work_order as air_only
|
||||
|
||||
return air_only(text)
|
||||
|
||||
|
||||
def send_segment_templates(*, flow: Any, ticket: object, chat_id: str, sheet_fn: Optional[SheetFn] = None) -> None:
|
||||
"""拉群或激活后,按段各发一份匹配模板。没有段则不发。"""
|
||||
for seg in load_segments(ticket):
|
||||
_send_sheet(
|
||||
flow=flow,
|
||||
ticket=ticket,
|
||||
chat_id=chat_id,
|
||||
segment=seg,
|
||||
quote={},
|
||||
sheet_fn=sheet_fn,
|
||||
)
|
||||
|
||||
|
||||
def multi_has_air(ticket: object) -> bool:
|
||||
"""这张多段单含空运段,锁舱和含电含磁才适用。"""
|
||||
return includes_air(load_segments(ticket))
|
||||
Reference in New Issue
Block a user