调价后的新价格写入报价单;多段段前的货物带到各段,协同群不再把体积里的星号吃掉。海运字段齐了先核对再查价。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
海运文字询价确定性流程。
|
||||
|
||||
本文件职责:海运七项补问 → 建单出确认卡 → 主账查 TMS → 有价/无价卡 → 跳过协同或拉群。
|
||||
本文件职责:海运补问 → 无工单号核对卡 → 回复确定后建单查 TMS → 有价/无价卡 → 跳过协同或拉群。
|
||||
空运 Owner 仍在 air_text_flow,禁止把海运再堆回去。
|
||||
调用:handler 在 Worker/inbox 线程;禁止回调线程同步跑。
|
||||
禁止:直连 TMS;本地拆箱型;用户文字冒充群报价版本。
|
||||
@@ -108,6 +108,21 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
adjusted = try_private_adjust(self, sess, text, reply)
|
||||
if adjusted:
|
||||
return adjusted
|
||||
from agent.schema.land_options import looks_like_confirm
|
||||
|
||||
if sess and sess.phase == "wait_confirm" and looks_like_confirm(text):
|
||||
# 确定查价不再抽字段,避免「确定」被模型改成空 facts。
|
||||
logger.info("sea.confirm sender=%s", sender_id)
|
||||
return self._confirm_or_hold(
|
||||
sender_id=sender_id,
|
||||
facts=dict(sess.facts),
|
||||
text=sess.immutable_text or text,
|
||||
reply=reply,
|
||||
sess=sess,
|
||||
)
|
||||
if not sess and looks_like_confirm(text):
|
||||
reply(copy.SEA_CONFIRM_EXPIRED)
|
||||
return "confirm_expired"
|
||||
# 上一单已出号:新一轮从本句重收,禁止把旧贸易条款/分类带进下一张工单。
|
||||
sess = self._detach_ticketed_for_new_inquiry(sess)
|
||||
|
||||
@@ -132,7 +147,7 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
return llm_phase
|
||||
mode = (snap.get("business_line") or "").upper()
|
||||
facts = dict(snap.get("facts") or {})
|
||||
if sess and sess.phase in {"clarify", "need_mode"}:
|
||||
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
|
||||
|
||||
@@ -205,7 +220,8 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
# 整句只回「港到港」是贸易条款举例,不要改写成运输分类。
|
||||
class_source = "" if short_sea_trade_reply(source) == "港到港" else source
|
||||
facts = harvest_sea_class(class_source, facts)
|
||||
# 贸易条款标签行原样优先;整句回复门到门/港到港要收下;没写这两个词时模型冒充的门到门丢掉。
|
||||
# 贸易条款标签行原样优先;整句回复或长句里写了门到门/港到港都要收下。
|
||||
# 原话没出现这两个词时,模型冒充的门到门、港到港继续丢掉。
|
||||
facts = apply_spoken_sea_trade_reply(source, facts)
|
||||
spoken = harvest_spoken_trade_term(source)
|
||||
if spoken.get("贸易条款") and not str(facts.get("贸易条款") or "").strip():
|
||||
@@ -243,13 +259,109 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
),
|
||||
)
|
||||
|
||||
return self._create_and_quote(
|
||||
return self._hold_for_confirm(
|
||||
sender_id=sender_id,
|
||||
facts=facts,
|
||||
text=sess.immutable_text if sess and sess.immutable_text else text,
|
||||
reply=reply,
|
||||
sess=sess,
|
||||
)
|
||||
|
||||
def _hold_for_confirm(
|
||||
self,
|
||||
*,
|
||||
sender_id: str,
|
||||
facts: dict[str, str],
|
||||
text: str,
|
||||
reply: ReplyFn,
|
||||
sess: Optional[FlowSession],
|
||||
) -> str:
|
||||
"""必填已齐:出海运核对卡,等确定再建单。核对卡没有工单号。"""
|
||||
reply(copy.sea_confirm_card(facts))
|
||||
pending = tuple(getattr(sess, "pending_files", ()) or ()) if sess else ()
|
||||
self._save(
|
||||
FlowSession(
|
||||
sender_id=sender_id,
|
||||
thread_id=sender_id,
|
||||
phase="wait_confirm",
|
||||
business_line="SEA",
|
||||
facts=facts,
|
||||
immutable_text=text,
|
||||
wait_version=(sess.wait_version + 1) if sess else 1,
|
||||
pending_files=pending,
|
||||
)
|
||||
)
|
||||
return "wait_confirm"
|
||||
|
||||
def _confirm_or_hold(
|
||||
self,
|
||||
*,
|
||||
sender_id: str,
|
||||
facts: dict[str, str],
|
||||
text: str,
|
||||
reply: ReplyFn,
|
||||
sess: FlowSession,
|
||||
) -> str:
|
||||
"""确定意图:字段仍齐则建单查价。缺了就回到补问,不建单。"""
|
||||
check = validate_required_fields(facts=facts, business_line="SEA")
|
||||
tms_ready = assemble_sea_query(check["facts"])
|
||||
ready_facts = dict(tms_ready.get("facts") or facts)
|
||||
if not check["ok"] or not tms_ready.get("ok"):
|
||||
keys = list(check["missing"] or [])
|
||||
for key in tms_ready.get("missing") or []:
|
||||
if key not in keys:
|
||||
keys.append(key)
|
||||
new_sess = FlowSession(
|
||||
sender_id=sender_id,
|
||||
thread_id=sender_id,
|
||||
phase="clarify",
|
||||
business_line="SEA",
|
||||
facts=ready_facts,
|
||||
immutable_text=sess.immutable_text or text,
|
||||
wait_version=sess.wait_version + 1,
|
||||
)
|
||||
return self._clarify_or_handoff(
|
||||
sess=sess,
|
||||
keys=keys,
|
||||
new_sess=new_sess,
|
||||
reply=reply,
|
||||
clarify_text=copy.ask_clarify(
|
||||
facts=new_sess.facts,
|
||||
missing_keys=keys,
|
||||
transport_mode="海运",
|
||||
),
|
||||
)
|
||||
return self._create_and_quote(
|
||||
sender_id=sender_id,
|
||||
facts=dict(tms_ready["facts"]),
|
||||
text=text,
|
||||
reply=reply,
|
||||
)
|
||||
|
||||
def _flush_pending_files(self, sender_id: str, work_order_no: str) -> None:
|
||||
"""确定建单后,把核对前收下的图片/附件挂到这张工单。挂完即清,避免重试重复挂。"""
|
||||
sess = self.session_of(sender_id)
|
||||
files = tuple(getattr(sess, "pending_files", ()) or ()) if sess else ()
|
||||
if not files or not (work_order_no or "").strip():
|
||||
return
|
||||
for row in files:
|
||||
key = str(row.get("object_key") or "").strip()
|
||||
if not key:
|
||||
continue
|
||||
try:
|
||||
self._ledger.attach_file(
|
||||
work_order_no=work_order_no,
|
||||
file_name=str(row.get("file_name") or "") or "attachment.bin",
|
||||
object_key=key,
|
||||
content_type=str(row.get("content_type") or "") or "application/octet-stream",
|
||||
sender_id=sender_id,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("海运核对后挂附件失败 wo=%s", work_order_no)
|
||||
if sess is not None:
|
||||
sess.pending_files = ()
|
||||
self._save(sess)
|
||||
|
||||
def _create_and_quote(
|
||||
self,
|
||||
*,
|
||||
@@ -266,6 +378,7 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
old = self.session_of(sender_id)
|
||||
if old:
|
||||
void_virtual_no(old)
|
||||
pending = tuple(getattr(old, "pending_files", ()) or ()) if old else ()
|
||||
self._save(
|
||||
FlowSession(
|
||||
sender_id=sender_id,
|
||||
@@ -274,6 +387,7 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
business_line="SEA",
|
||||
facts=facts,
|
||||
immutable_text=text,
|
||||
pending_files=pending,
|
||||
)
|
||||
)
|
||||
created = self._ledger.create_ticket(
|
||||
@@ -286,6 +400,7 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
if not no:
|
||||
reply("建单失败,请稍后把刚才那条再发一次。海运字段已经记下。")
|
||||
return "ledger_fail"
|
||||
self._flush_pending_files(sender_id, no)
|
||||
first, history_wo = self._resolve_similar(created, facts=facts, current_no=no)
|
||||
out_id = reply(
|
||||
copy.inquiry_card(
|
||||
|
||||
Reference in New Issue
Block a user