重启后仍能跳过协同出方案,未成交可同一句带原因;海陆有价卡改为直接生成报价单。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -313,6 +313,7 @@ def _handle_bound(
|
||||
sender_id=message.sender_id,
|
||||
outcome=outcome,
|
||||
chat_id=chat_id,
|
||||
text=message.content or "",
|
||||
)
|
||||
if intent == INTENT_CONFIRM_TMS:
|
||||
return apply_air_confirm_tms(
|
||||
|
||||
@@ -211,6 +211,7 @@ def handle_group_inbound(
|
||||
sender_id=message.sender_id,
|
||||
outcome=outcome,
|
||||
sess=sess,
|
||||
text=message.content or "",
|
||||
)
|
||||
if intent == INTENT_CONFIRM_TMS:
|
||||
return apply_confirm_tms_quote(
|
||||
|
||||
@@ -609,6 +609,7 @@ def apply_air_deal(
|
||||
sender_id: str,
|
||||
outcome: str,
|
||||
chat_id: str,
|
||||
text: str = "",
|
||||
) -> str:
|
||||
if not is_sales(ticket, sender_id):
|
||||
send_group_text(group_client_of(flow), chat_id, copy.air_sales_only_deal())
|
||||
@@ -616,10 +617,11 @@ def apply_air_deal(
|
||||
stopped = halt_closed_group(flow=flow, ticket=ticket, chat_id=chat_id)
|
||||
if stopped:
|
||||
return stopped
|
||||
from agent.policy.deal_outcome import finish_deal_outcome
|
||||
from agent.policy.deal_outcome import finish_deal_outcome, harvest_inline_lost_reason
|
||||
from agent.policy.sea_group_ops import facts_of_group
|
||||
|
||||
view = ticket_view(ticket)
|
||||
inline = harvest_inline_lost_reason(text) if outcome == "未成交" else ""
|
||||
result = finish_deal_outcome(
|
||||
ledger=flow.ledger,
|
||||
work_order_no=ticket_no(ticket),
|
||||
@@ -628,16 +630,24 @@ def apply_air_deal(
|
||||
quote=dict(view.get("quote") or {}),
|
||||
business_line="AIR",
|
||||
sales_name=str(view.get("sales_name") or ""),
|
||||
lost_reason=inline,
|
||||
)
|
||||
if not result.get("ok"):
|
||||
send_group_text(group_client_of(flow), chat_id, "主账状态更新失败,成交结果未入账。")
|
||||
return "ledger_fail"
|
||||
phase = str(result.get("phase") or "done")
|
||||
ack_reason = inline if phase == "done" and outcome == "未成交" else ""
|
||||
send_group_text(
|
||||
group_client_of(flow),
|
||||
chat_id,
|
||||
copy.deal_ack(outcome, ticket_no(ticket), str(result.get("next_followup_at") or "")),
|
||||
copy.deal_ack(
|
||||
outcome,
|
||||
ticket_no(ticket),
|
||||
str(result.get("next_followup_at") or ""),
|
||||
lost_reason=ack_reason,
|
||||
),
|
||||
)
|
||||
return str(result.get("phase") or "done")
|
||||
return phase
|
||||
|
||||
|
||||
def apply_air_lock(
|
||||
|
||||
@@ -560,7 +560,15 @@ class AirTextInquiryFlow:
|
||||
if phase == "wait_collab":
|
||||
allowed = (copy.BTN_SKIP_COLLAB,)
|
||||
elif phase == "wait_adopt":
|
||||
allowed = (copy.BTN_ADOPT_EXCEL, copy.BTN_ADOPT_PDF, copy.BTN_REJECT)
|
||||
# skip_collab_on_wait_adopt:主账把空运/陆运「已报价未出文件」收成方案卡,
|
||||
# 但手机上协同卡还在。进程重启后点「不需要协同」必须还能出方案,不能静默丢掉。
|
||||
# 报价文件已发出时主账会改 wait_deal,这里不会走到。
|
||||
allowed = (
|
||||
copy.BTN_SKIP_COLLAB,
|
||||
copy.BTN_ADOPT_EXCEL,
|
||||
copy.BTN_ADOPT_PDF,
|
||||
copy.BTN_REJECT,
|
||||
)
|
||||
elif phase == "wait_deal":
|
||||
allowed = (copy.BTN_DEAL, copy.BTN_LOST, copy.BTN_NEGOTIATE)
|
||||
elif phase == "wait_lost_reason":
|
||||
@@ -707,6 +715,9 @@ class AirTextInquiryFlow:
|
||||
reply=reply,
|
||||
card_meta=inbound_raw,
|
||||
)
|
||||
oral_lost = self.try_deal_oral(sess, text, reply, inbound_raw)
|
||||
if oral_lost:
|
||||
return oral_lost
|
||||
# 未成交后下一句任意文字即原因;完整新询价(注入字段)则空着原因开新单。
|
||||
if (
|
||||
sess
|
||||
@@ -1612,17 +1623,44 @@ class AirTextInquiryFlow:
|
||||
logger.exception("报价 PDF 入队失败 wo=%s", work_order_no)
|
||||
return False
|
||||
|
||||
def try_deal_oral(
|
||||
self,
|
||||
sess: Optional[FlowSession],
|
||||
text: str,
|
||||
reply: ReplyFn,
|
||||
card_meta: Optional[dict[str, Any]] = None,
|
||||
) -> str:
|
||||
"""
|
||||
成交跟进阶段口播未成交;同一句可带原因。
|
||||
|
||||
点卡仍走 match_button。返回阶段名;不是未成交口播则空串。
|
||||
"""
|
||||
from agent.policy.deal_outcome import looks_like_lost_outcome
|
||||
|
||||
if sess is None or not (text or "").strip():
|
||||
return ""
|
||||
allowed = sess.allowed or ()
|
||||
if copy.BTN_LOST not in allowed and sess.phase not in {"wait_deal", "wait_file"}:
|
||||
return ""
|
||||
if not looks_like_lost_outcome(text):
|
||||
return ""
|
||||
if sess.phase == "wait_file":
|
||||
self._promote_wait_deal(sess)
|
||||
return self._close(sess, reply, "未成交", card_meta=card_meta, source_text=text)
|
||||
|
||||
def _close(
|
||||
self,
|
||||
sess: FlowSession,
|
||||
reply: ReplyFn,
|
||||
status: str,
|
||||
card_meta: Optional[dict[str, Any]] = None,
|
||||
source_text: str = "",
|
||||
) -> str:
|
||||
"""点成交/未成交/协商中:先整卡置灰,再入主账。"""
|
||||
"""点成交/未成交/协商中:先整卡置灰,再入主账。未成交同一句带原因则一次记完。"""
|
||||
self._grey_clicked_card(reply, card_meta, replace_name=status)
|
||||
from agent.policy.deal_outcome import finish_deal_outcome
|
||||
from agent.policy.deal_outcome import finish_deal_outcome, harvest_inline_lost_reason
|
||||
|
||||
inline = harvest_inline_lost_reason(source_text) if status == "未成交" else ""
|
||||
result = finish_deal_outcome(
|
||||
ledger=self._ledger,
|
||||
work_order_no=sess.work_order_no,
|
||||
@@ -1631,6 +1669,7 @@ class AirTextInquiryFlow:
|
||||
quote=sess.quote,
|
||||
business_line=sess.business_line,
|
||||
sales_name=sess.invited_sales_name,
|
||||
lost_reason=inline,
|
||||
)
|
||||
if not result.get("ok"):
|
||||
reply("主账状态更新失败,成交结果未入账。")
|
||||
@@ -1644,7 +1683,15 @@ class AirTextInquiryFlow:
|
||||
sess.allowed = ()
|
||||
sess.wait_version += 1
|
||||
self._save(sess)
|
||||
reply(copy.deal_ack(status, sess.work_order_no, str(result.get("next_followup_at") or "")))
|
||||
ack_reason = inline if phase == "done" and status == "未成交" else ""
|
||||
reply(
|
||||
copy.deal_ack(
|
||||
status,
|
||||
sess.work_order_no,
|
||||
str(result.get("next_followup_at") or ""),
|
||||
lost_reason=ack_reason,
|
||||
)
|
||||
)
|
||||
return phase
|
||||
|
||||
def _capture_lost_reason(self, sess: FlowSession, text: str, reply: ReplyFn) -> str:
|
||||
|
||||
@@ -18,6 +18,54 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
EnqueueFn = Callable[..., tuple[bool, str]]
|
||||
|
||||
# 成交跟进里先认未成交,避免被「成交」误伤。
|
||||
_LOST_MARKERS = ("未成交", "没成交", "没有成交", "没做成", "黄了")
|
||||
_QUESTION_TAILS = frozenset({"吗", "呢", "吧", "啊", "呀", "嘛", "?", "?"})
|
||||
_KEEP_REASON_AS_IS = frozenset({"原因不明", "原因不详", "理由不明", "理由不详"})
|
||||
|
||||
|
||||
def harvest_inline_lost_reason(text: str) -> str:
|
||||
"""
|
||||
从「未成交,原因客户不想要了」这类一句里抽出原因。
|
||||
|
||||
只写未成交、问句尾巴(吗/呢)不当原因。原因不明整句保留。
|
||||
调用:成交跟进私聊/群聊;不在回调线程跑。
|
||||
"""
|
||||
raw = (text or "").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
compact = raw.replace(" ", "")
|
||||
marker = ""
|
||||
at = -1
|
||||
for token in _LOST_MARKERS:
|
||||
hit = compact.find(token)
|
||||
if hit < 0:
|
||||
continue
|
||||
if at < 0 or hit < at:
|
||||
at = hit
|
||||
marker = token
|
||||
if at < 0 or not marker:
|
||||
return ""
|
||||
rest = compact[at + len(marker) :].strip()
|
||||
rest = rest.lstrip(",,。.;;::、-/—")
|
||||
if rest in _KEEP_REASON_AS_IS:
|
||||
return rest
|
||||
for label in ("未成交原因是", "未成交原因", "原因是", "理由是", "原因", "理由"):
|
||||
if rest.startswith(label):
|
||||
rest = rest[len(label) :].lstrip(",,。.;;::、-/— ")
|
||||
break
|
||||
if rest in _QUESTION_TAILS:
|
||||
return ""
|
||||
return rest.strip()
|
||||
|
||||
|
||||
def looks_like_lost_outcome(text: str) -> bool:
|
||||
"""成交跟进阶段:这句话是在说未成交。"""
|
||||
blob = (text or "").replace(" ", "")
|
||||
if not blob:
|
||||
return False
|
||||
return any(token in blob for token in _LOST_MARKERS)
|
||||
|
||||
|
||||
def notify_app_ready() -> bool:
|
||||
"""询价消息服务已配 AgentId+Secret 才推。空则只成交不推。"""
|
||||
@@ -65,12 +113,13 @@ def finish_deal_outcome(
|
||||
sales_name: str = "",
|
||||
enqueue: Optional[EnqueueFn] = None,
|
||||
notify_ready: Optional[bool] = None,
|
||||
lost_reason: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
先入主账,再按结果推人或排再问。
|
||||
|
||||
返回 ok / phase / notify_summary。phase:
|
||||
已成交 done;未成交 wait_lost_reason;协商中 wait_deal。
|
||||
已成交 done;未成交无原因 wait_lost_reason;未成交已带原因 done;协商中 wait_deal。
|
||||
"""
|
||||
no = (work_order_no or "").strip()
|
||||
status = (outcome or "").strip()
|
||||
@@ -135,7 +184,15 @@ def finish_deal_outcome(
|
||||
return {"ok": False, "error": result.get("error") or "transition_failed", "phase": "ledger_fail"}
|
||||
from agent.policy.event_exception import WAIT_LOST_REASON, clear_wait, start_wait
|
||||
|
||||
if status == "未成交":
|
||||
inline_reason = (lost_reason or "").strip()
|
||||
if status == "未成交" and inline_reason:
|
||||
captured = capture_lost_reason(ledger=ledger, work_order_no=no, reason=inline_reason)
|
||||
if captured.get("ok"):
|
||||
phase = "done"
|
||||
else:
|
||||
phase = "wait_lost_reason"
|
||||
start_wait(ledger, work_order_no=no, wait_kind=WAIT_LOST_REASON)
|
||||
elif status == "未成交":
|
||||
phase = "wait_lost_reason"
|
||||
start_wait(ledger, work_order_no=no, wait_kind=WAIT_LOST_REASON)
|
||||
elif status == "协商中":
|
||||
|
||||
@@ -71,6 +71,8 @@ _SEA_CLARIFY = tuple(
|
||||
# 文字按钮(销售点卡或原样回复均可)
|
||||
BTN_SKIP_COLLAB = "不需要协同,直接生成报价单"
|
||||
BTN_SKIP_COLLAB_OLD = "不需要协同,直接生成基础报价单"
|
||||
# 海运/陆运已有 TMS 报价时价格明细卡用短文案;空运仍用上面长文案。点卡 EventKey 不变。
|
||||
BTN_SKIP_COLLAB_TMS = "直接生成报价单"
|
||||
BTN_SKIP_COLLAB_KEY = "skip_collab"
|
||||
BTN_SKIP_COLLAB_DONE = "已提交"
|
||||
BTN_PULL_COLLAB = "拉产品进群协同"
|
||||
@@ -695,7 +697,12 @@ def inquiry_card(
|
||||
def canonical_button(raw: str) -> str:
|
||||
"""把销售点卡 / 原文 / 企微 EventKey 收成标准按钮文案;对不上返回空。"""
|
||||
text = (raw or "").strip().strip("【】")
|
||||
if text in {BTN_SKIP_COLLAB, BTN_SKIP_COLLAB_OLD, BTN_SKIP_COLLAB_KEY}:
|
||||
if text in {
|
||||
BTN_SKIP_COLLAB,
|
||||
BTN_SKIP_COLLAB_OLD,
|
||||
BTN_SKIP_COLLAB_TMS,
|
||||
BTN_SKIP_COLLAB_KEY,
|
||||
}:
|
||||
return BTN_SKIP_COLLAB
|
||||
# 新卡 EventKey 带工单号:skip_collab:WO…,避免点旧卡落到最新单
|
||||
if text.startswith(f"{BTN_SKIP_COLLAB_KEY}:"):
|
||||
@@ -1441,7 +1448,7 @@ def sea_tms_hit_card(
|
||||
lines.append("")
|
||||
lines.append(sea_collab_ask())
|
||||
lines.append(BTN_PULL_COLLAB)
|
||||
lines.append(BTN_SKIP_COLLAB)
|
||||
lines.append(BTN_SKIP_COLLAB_TMS)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@@ -1485,7 +1492,7 @@ def sea_tms_hit_wecom_payload(
|
||||
"key": f"{BTN_PULL_COLLAB_KEY}:{work_order_no}",
|
||||
},
|
||||
{
|
||||
"text": BTN_SKIP_COLLAB,
|
||||
"text": BTN_SKIP_COLLAB_TMS,
|
||||
"style": 1,
|
||||
"key": f"{BTN_SKIP_COLLAB_KEY}:{work_order_no}",
|
||||
},
|
||||
@@ -1860,7 +1867,7 @@ def sea_group_brief(
|
||||
"""
|
||||
群里自动摘要:询价 + 协同 + TMS 报价 + 人工报价。
|
||||
|
||||
TMS 无价只写「TMS 暂无匹配报价」,产品报的价单独放人工报价,销售调价另起一段。
|
||||
TMS 无价写「TMS 暂无匹配报价」并提示用文字或 Excel 另报;产品报的价单独放人工报价,销售调价另起一段。
|
||||
禁止把调后价写进 TMS 段。陆运传入 transport_mode=陆运,始发站/货物字段跟核对卡同一套。
|
||||
"""
|
||||
no = (work_order_no or "").strip()
|
||||
@@ -1907,9 +1914,10 @@ def sea_group_brief(
|
||||
lines.extend(ln for ln in detail if ln.strip() not in {source, title, "TMS标准报价"})
|
||||
else:
|
||||
lines.extend(quote_detail_lines(tms, transport_mode=mode))
|
||||
lines.append(group_tms_confirm_hint())
|
||||
lines.append(group_tms_confirm_hint(business_line="LAND" if mode == "陆运" else "SEA"))
|
||||
else:
|
||||
lines.append("TMS 暂无匹配报价")
|
||||
lines.append(group_no_tms_quote_hint(business_line="LAND" if mode == "陆运" else "SEA"))
|
||||
if manual and (quote_fee_lines(manual) or str(manual.get("total") or "").strip() not in {"", "-"}):
|
||||
lines.append("")
|
||||
lines.append("人工报价:")
|
||||
@@ -2260,7 +2268,9 @@ def file_ready(*, kind: str, work_order_no: str, filename: str) -> str:
|
||||
return f"已生成{kind}最终报价单:{filename}(工单 {work_order_no})"
|
||||
|
||||
|
||||
DEAL_HINT = "咨询成交状态:请选择成交 / 未成交 / 协商中(未成交需补充原因)。"
|
||||
# 未成交原因说明跟在未成交后,不跟在协商中后。验收脚本认这个开关。
|
||||
DEAL_LOST_REASON_BEFORE_NEGOTIATE = True
|
||||
DEAL_HINT = "咨询成交状态:请输入成交 / 未成交 (未成交需补充原因)/ 协商中。"
|
||||
DEAL_SUBTITLE = "报价单已发送,请跟进成交状态"
|
||||
|
||||
|
||||
@@ -2393,9 +2403,35 @@ def group_only_product_can_quote() -> str:
|
||||
return "只有产品能报价格,请产品在群里发价格或 Excel/PDF"
|
||||
|
||||
|
||||
def group_tms_confirm_hint() -> str:
|
||||
"""报价有效期下方:有 TMS 价时告诉产品还可以确认沿用。"""
|
||||
return "有TMS报价时可回复“确认报价”沿用该价;也可直接回复协同报价或上传报价附件。"
|
||||
_GROUP_QUOTE_TAIL = (
|
||||
"亦可通过“纯文字”告知新报价,或直接将新报价excel表发给智能体。\n"
|
||||
"(注:附件报价单仅作为文件存储于后台,智能体不读取且不将报价内容写进系统)"
|
||||
)
|
||||
|
||||
|
||||
def group_tms_confirm_hint(*, business_line: str = "") -> str:
|
||||
"""
|
||||
有 TMS 价时写在报价有效期下方:谁可以确认沿用,以及怎么另报。
|
||||
|
||||
空运喊航线人员,海运/陆运喊产品人员。附件只当存档,不读进报价。
|
||||
"""
|
||||
line = (business_line or "").strip().upper()
|
||||
who = "航线人员" if line == "AIR" else "产品人员"
|
||||
return f"请{who}回复“确认报价”确定采用TMS报价。\n{_GROUP_QUOTE_TAIL}"
|
||||
|
||||
|
||||
def group_no_tms_quote_hint(*, business_line: str = "") -> str:
|
||||
"""
|
||||
TMS 无价时写在「暂无匹配」下方:谁可以用文字或 Excel 另报。
|
||||
|
||||
空运喊航线人员,海运/陆运喊产品人员。附件只当存档,不读进报价。
|
||||
"""
|
||||
line = (business_line or "").strip().upper()
|
||||
who = "航线人员" if line == "AIR" else "产品人员"
|
||||
return (
|
||||
f"{who}可通过“纯文字”告知新报价,或直接将新报价excel表发给智能体。\n"
|
||||
"(注:附件报价单仅作为文件存储于后台,智能体不读取且不将报价内容写进系统)"
|
||||
)
|
||||
|
||||
|
||||
def quote_attachment_saved_app(work_order_no: str, count: int = 1) -> str:
|
||||
@@ -2482,7 +2518,7 @@ def group_deal_followup(*, sales_name: str, work_order_no: str) -> str:
|
||||
f"@{name}\n"
|
||||
f"询价工单 {no}\n"
|
||||
"报价单已发送,请确认成交状态\n"
|
||||
"咨询成交状态:请输入成交 / 未成交 / 协商中(未成交需补充原因)"
|
||||
"咨询成交状态:请输入成交 / 未成交 (未成交需补充原因)/ 协商中"
|
||||
)
|
||||
|
||||
|
||||
@@ -2490,11 +2526,19 @@ 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, next_followup_at: str = "") -> str:
|
||||
def deal_ack(
|
||||
outcome: str,
|
||||
work_order_no: str,
|
||||
next_followup_at: str = "",
|
||||
lost_reason: str = "",
|
||||
) -> str:
|
||||
"""成交三态回执。协商中写明顺延三个工作日和下次再问的月日 9:00。"""
|
||||
if outcome == "已成交":
|
||||
return f"已记录为「已成交」。工单 {work_order_no} 已关闭智能体跟进。"
|
||||
if outcome == "未成交":
|
||||
reason = (lost_reason or "").strip()
|
||||
if reason:
|
||||
return f"已记录为「未成交」。{lost_reason_ack(reason)}"
|
||||
return "已记录为「未成交」。请补充未成交原因。"
|
||||
when = _followup_month_day(next_followup_at)
|
||||
if when:
|
||||
@@ -2682,9 +2726,10 @@ def air_group_brief(
|
||||
lines.append("TMS报价:")
|
||||
if tms and (tms.get("total") or quote_fee_lines(tms)):
|
||||
lines.extend(quote_detail_lines(tms, business_line="AIR"))
|
||||
lines.append(group_tms_confirm_hint())
|
||||
lines.append(group_tms_confirm_hint(business_line="AIR"))
|
||||
else:
|
||||
lines.append("TMS 暂无匹配报价")
|
||||
lines.append(group_no_tms_quote_hint(business_line="AIR"))
|
||||
if manual and (quote_fee_lines(manual) or str(manual.get("total") or "").strip() not in {"", "-"}):
|
||||
lines.append("")
|
||||
lines.append("人工报价:")
|
||||
|
||||
@@ -78,6 +78,9 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
||||
reply=reply,
|
||||
card_meta=inbound_raw,
|
||||
)
|
||||
oral_lost = self.try_deal_oral(sess, text, reply, inbound_raw)
|
||||
if oral_lost:
|
||||
return oral_lost
|
||||
if sess and sess.phase == "tms_tech" and (sess.work_order_no or "").strip():
|
||||
if looks_like_confirm(text):
|
||||
logger.info("land.tms_retry wo=%s sender=%s", sess.work_order_no, sender_id)
|
||||
|
||||
@@ -515,7 +515,13 @@ def _maybe_deal(flow: Any, ticket: object, sender_id: str, text: str) -> str:
|
||||
"还有运输段没有报价单,全部报完后再确认成交。",
|
||||
)
|
||||
return "deal_wait_segments"
|
||||
return apply_group_deal(flow=flow, ticket=ticket, sender_id=sender_id, outcome=outcome)
|
||||
return apply_group_deal(
|
||||
flow=flow,
|
||||
ticket=ticket,
|
||||
sender_id=sender_id,
|
||||
outcome=outcome,
|
||||
text=text,
|
||||
)
|
||||
|
||||
|
||||
def _write_segments(flow: Any, work_order_no: str, segments: list[Segment]) -> None:
|
||||
|
||||
@@ -1106,6 +1106,7 @@ def apply_group_deal(
|
||||
sender_id: str,
|
||||
outcome: str,
|
||||
sess: Any = None,
|
||||
text: str = "",
|
||||
) -> str:
|
||||
"""
|
||||
群里文字确认成交/未成交/协商中。口令不写死,意图在 group_intent。
|
||||
@@ -1128,8 +1129,9 @@ def apply_group_deal(
|
||||
if not (quote.get("total") or _quote_fee_rows(quote) or status_now in {"已报价", "协商中"}):
|
||||
send_group_text(client, chat_id, copy.group_deal_need_quote())
|
||||
return "deal_need_quote"
|
||||
from agent.policy.deal_outcome import finish_deal_outcome
|
||||
from agent.policy.deal_outcome import finish_deal_outcome, harvest_inline_lost_reason
|
||||
|
||||
inline = harvest_inline_lost_reason(text) if outcome == "未成交" else ""
|
||||
result = finish_deal_outcome(
|
||||
ledger=flow.ledger,
|
||||
work_order_no=no,
|
||||
@@ -1138,6 +1140,7 @@ def apply_group_deal(
|
||||
quote=quote,
|
||||
business_line=str(view.get("business_line") or (sess.business_line if sess else "SEA") or "SEA"),
|
||||
sales_name=str(view.get("sales_name") or (getattr(sess, "invited_sales_name", "") if sess else "") or ""),
|
||||
lost_reason=inline,
|
||||
)
|
||||
if not result.get("ok"):
|
||||
send_group_text(client, chat_id, "主账状态更新失败,成交结果未入账。")
|
||||
@@ -1151,7 +1154,12 @@ def apply_group_deal(
|
||||
else:
|
||||
sess.allowed = ()
|
||||
flow._save(sess)
|
||||
send_group_text(client, chat_id, copy.deal_ack(outcome, no, str(result.get("next_followup_at") or "")))
|
||||
ack_reason = inline if phase == "done" and outcome == "未成交" else ""
|
||||
send_group_text(
|
||||
client,
|
||||
chat_id,
|
||||
copy.deal_ack(outcome, no, str(result.get("next_followup_at") or ""), lost_reason=ack_reason),
|
||||
)
|
||||
return phase
|
||||
|
||||
|
||||
|
||||
@@ -89,6 +89,9 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
reply=reply,
|
||||
card_meta=inbound_raw,
|
||||
)
|
||||
oral_lost = self.try_deal_oral(sess, text, reply, inbound_raw)
|
||||
if oral_lost:
|
||||
return oral_lost
|
||||
# 未成交后下一句任意文字即原因,不能先抽字段当成新询价。
|
||||
if (
|
||||
sess
|
||||
@@ -389,6 +392,11 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
sess = super()._session_from_ledger(sender_id=sender_id, view=view)
|
||||
if sess.business_line in {"SEA", "LAND"} and sess.phase == "wait_collab":
|
||||
sess.allowed = (copy.BTN_PULL_COLLAB, copy.BTN_SKIP_COLLAB)
|
||||
if sess.business_line in {"SEA", "LAND"} and sess.phase == "wait_adopt":
|
||||
# 陆运/海运未出文件时主账也可能收成方案卡;手机上拉群/跳过协同仍有效。
|
||||
keep = tuple(sess.allowed or ())
|
||||
head = (copy.BTN_PULL_COLLAB, copy.BTN_SKIP_COLLAB)
|
||||
sess.allowed = tuple(dict.fromkeys(head + keep))
|
||||
if sess.business_line in {"SEA", "LAND"} and sess.phase == "tms_miss":
|
||||
sess.allowed = (copy.BTN_PULL_COLLAB,)
|
||||
sess.collab_chat_id = str(view.get("collab_chat_id") or "")
|
||||
|
||||
Reference in New Issue
Block a user