私聊建单前拦住枪支弹药爆炸物和毒品,避免真家伙进入报价。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -31,6 +31,8 @@ def _front_bookmark(sess) -> bool:
|
||||
return False
|
||||
phase = (sess.phase or "").strip()
|
||||
line = (sess.business_line or "").upper()
|
||||
if phase == "sensitive_block":
|
||||
return True
|
||||
if phase == "need_mode" and not line:
|
||||
return False
|
||||
return phase in {"clarify", "need_mode", "need_land_options", "wait_confirm"}
|
||||
@@ -87,7 +89,9 @@ def _carry_waiting_inquiry_to_owner(*, owner: str, sender_id: str) -> None:
|
||||
raw = (sess.immutable_text or "").strip()
|
||||
if raw and raw not in texts:
|
||||
texts.append(raw)
|
||||
if not facts and not texts:
|
||||
holds = [str(getattr(item, "sensitive_hold", "") or "").strip() for item in drafts]
|
||||
holds = [item for item in holds if item]
|
||||
if not facts and not texts and not holds:
|
||||
return
|
||||
from agent.policy.air_text_flow import FlowSession
|
||||
|
||||
@@ -99,6 +103,7 @@ def _carry_waiting_inquiry_to_owner(*, owner: str, sender_id: str) -> None:
|
||||
business_line="",
|
||||
facts=facts,
|
||||
immutable_text="\n".join(texts),
|
||||
sensitive_hold="\n".join(holds),
|
||||
)
|
||||
)
|
||||
logger.info(
|
||||
@@ -245,7 +250,7 @@ def _pick_text_flow(*, sender_id: str, text: str, injected_mode: str = "", prefe
|
||||
if (
|
||||
multi_draft
|
||||
and (multi_draft.business_line or "").upper() == "MULTI"
|
||||
and (multi_draft.phase or "") == "clarify"
|
||||
and (multi_draft.phase or "") in {"clarify", "sensitive_block"}
|
||||
):
|
||||
return multi
|
||||
_drop_other_mode_drafts(owner=spoken, sender_id=sender_id)
|
||||
@@ -265,7 +270,7 @@ def _pick_text_flow(*, sender_id: str, text: str, injected_mode: str = "", prefe
|
||||
if (
|
||||
multi_sess
|
||||
and (multi_sess.business_line or "").upper() == "MULTI"
|
||||
and (multi_sess.phase or "") in {"clarify", "wait_confirm"}
|
||||
and (multi_sess.phase or "") in {"clarify", "wait_confirm", "sensitive_block"}
|
||||
and not (looks_like_confirm(text) and (multi_sess.phase or "") != "wait_confirm")
|
||||
):
|
||||
return multi
|
||||
|
||||
@@ -68,6 +68,8 @@ class FlowSession:
|
||||
virtual_work_order_no: str = ""
|
||||
# 核对卡还没有工单号时,图片/附件先记在这里,确定建单后再挂到主账。
|
||||
pending_files: tuple[dict[str, str], ...] = ()
|
||||
# 建单前出现过、还没被品名改口清掉的违禁原话。空表示当前没有。
|
||||
sensitive_hold: str = ""
|
||||
|
||||
|
||||
def is_waiting_mode_session(sess) -> bool:
|
||||
@@ -152,6 +154,7 @@ def _session_from_payload(raw: dict[str, Any]) -> FlowSession:
|
||||
for item in (raw.get("pending_files") or [])
|
||||
if isinstance(item, dict) and str(item.get("object_key") or "").strip()
|
||||
),
|
||||
sensitive_hold=str(raw.get("sensitive_hold") or ""),
|
||||
)
|
||||
|
||||
|
||||
@@ -306,6 +309,65 @@ class AirTextInquiryFlow:
|
||||
"invited_product_names": sess.invited_product_names,
|
||||
}
|
||||
|
||||
def _refuse_sensitive(
|
||||
self,
|
||||
*,
|
||||
sender_id: str,
|
||||
text: str,
|
||||
facts: dict[str, Any],
|
||||
reply: ReplyFn,
|
||||
sess: Optional[FlowSession],
|
||||
business_line: str = "",
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
私聊还没建单时,违禁品只回一句并停在原处。
|
||||
|
||||
已有正式工单号的会话直接放过,销售后面补的字按原流程。
|
||||
没拦住时把品名改口写进 facts,并清掉书签上的旧原话。
|
||||
拦住时保存已认定字段,方便改口后接着补问。不建单、不追问缺项。
|
||||
调用在 Worker 的 on_text 里,不占企微回调线程。
|
||||
"""
|
||||
from agent.policy.handoff_human import is_virtual_no
|
||||
from agent.policy.sensitive_cargo import BLOCK_REPLY, judge
|
||||
|
||||
if sess is not None:
|
||||
number = (sess.work_order_no or "").strip()
|
||||
if number and not is_virtual_no(number):
|
||||
return None
|
||||
blocked, new_hold, _edits = judge(
|
||||
text=text,
|
||||
facts=facts,
|
||||
hold=str(getattr(sess, "sensitive_hold", "") or "") if sess else "",
|
||||
)
|
||||
if sess is not None:
|
||||
sess.sensitive_hold = new_hold
|
||||
if not blocked:
|
||||
return None
|
||||
kept = self._handoff_state(sess)
|
||||
pending = tuple(getattr(sess, "pending_files", ()) or ()) if sess else ()
|
||||
line = (business_line or (getattr(sess, "business_line", "") if sess else "") or "").strip()
|
||||
self._save(
|
||||
FlowSession(
|
||||
sender_id=sender_id,
|
||||
thread_id=sender_id,
|
||||
phase="sensitive_block",
|
||||
business_line=line,
|
||||
facts={str(key): str(value) for key, value in (facts or {}).items()},
|
||||
immutable_text=(sess.immutable_text if sess and sess.immutable_text else text),
|
||||
sensitive_hold=new_hold,
|
||||
wait_version=(sess.wait_version + 1) if sess else 1,
|
||||
pending_files=pending,
|
||||
clarify_missing_sig=str(kept.get("clarify_missing_sig") or ""),
|
||||
clarify_retry=int(kept.get("clarify_retry") or 0),
|
||||
handoff_offer=str(kept.get("handoff_offer") or ""),
|
||||
handoff_committed=bool(kept.get("handoff_committed")),
|
||||
virtual_work_order_no=str(kept.get("virtual_work_order_no") or ""),
|
||||
)
|
||||
)
|
||||
reply(BLOCK_REPLY)
|
||||
logger.info("sensitive_block sender=%s line=%s", sender_id, line or "-")
|
||||
return "sensitive_block"
|
||||
|
||||
def _block_if_handoff_committed(self, sess: Optional[FlowSession], reply: ReplyFn) -> bool:
|
||||
"""真正转人工之后这一票不再跟。返回 True 表示已经拦住。"""
|
||||
from agent.policy.handoff_human import is_handoff_frozen
|
||||
@@ -963,6 +1025,16 @@ class AirTextInquiryFlow:
|
||||
if sess and sess.phase == "wait_confirm" and looks_like_confirm(text):
|
||||
# 确定查价不再抽字段,避免「确定」被模型改成空 facts。
|
||||
logger.info("air.confirm sender=%s", sender_id)
|
||||
refused = self._refuse_sensitive(
|
||||
sender_id=sender_id,
|
||||
text=text,
|
||||
facts=dict(sess.facts),
|
||||
reply=reply,
|
||||
sess=sess,
|
||||
business_line=sess.business_line,
|
||||
)
|
||||
if refused:
|
||||
return refused
|
||||
return self._confirm_or_hold(
|
||||
sender_id=sender_id,
|
||||
facts=dict(sess.facts),
|
||||
@@ -997,7 +1069,7 @@ class 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", "wait_confirm"}:
|
||||
if sess and sess.phase in {"clarify", "need_mode", "wait_confirm", "sensitive_block"}:
|
||||
# 补问轮:旧字段打底;运输方式沿用上一句(飞=空运)
|
||||
# 增值服务按句累加,本句没提时不能把上一句已记下的服务冲掉。
|
||||
from agent.llm.extract_text import detect_transport_mode
|
||||
@@ -1019,6 +1091,18 @@ class AirTextInquiryFlow:
|
||||
|
||||
facts = apply_air_value_added(added_source, facts, previous=prev_added)
|
||||
|
||||
if not mode or mode == "AIR":
|
||||
refused = self._refuse_sensitive(
|
||||
sender_id=sender_id,
|
||||
text=text,
|
||||
facts=facts,
|
||||
reply=reply,
|
||||
sess=sess,
|
||||
business_line=mode or (sess.business_line if sess else ""),
|
||||
)
|
||||
if refused:
|
||||
return refused
|
||||
|
||||
if not mode:
|
||||
reply(copy.ask_transport())
|
||||
self._save(
|
||||
@@ -1195,6 +1279,16 @@ class AirTextInquiryFlow:
|
||||
from agent.policy.handoff_human import void_virtual_no
|
||||
|
||||
old = self.session_of(sender_id)
|
||||
refused = self._refuse_sensitive(
|
||||
sender_id=sender_id,
|
||||
text="",
|
||||
facts=facts,
|
||||
reply=reply,
|
||||
sess=old,
|
||||
business_line="AIR",
|
||||
)
|
||||
if refused:
|
||||
return refused
|
||||
if old:
|
||||
void_virtual_no(old)
|
||||
self._save(
|
||||
|
||||
@@ -40,7 +40,9 @@ from agent.schema.tms_land_query import assemble_land_query
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_LAND_FRONT = frozenset({"need_mode", "need_land_options", "clarify", "wait_confirm", "wait_land_route", "idle", ""})
|
||||
_LAND_FRONT = frozenset(
|
||||
{"need_mode", "need_land_options", "clarify", "wait_confirm", "wait_land_route", "sensitive_block", "idle", ""}
|
||||
)
|
||||
|
||||
|
||||
class LandTextInquiryFlow(SeaTextInquiryFlow):
|
||||
@@ -97,6 +99,16 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
||||
if sess and sess.phase == "wait_confirm" and looks_like_confirm(text):
|
||||
# 确定查价不再抽字段,避免「确定」被模型改成空运/空 facts。
|
||||
logger.info("land.confirm sender=%s", sender_id)
|
||||
refused = self._refuse_sensitive(
|
||||
sender_id=sender_id,
|
||||
text=text,
|
||||
facts=dict(sess.facts),
|
||||
reply=reply,
|
||||
sess=sess,
|
||||
business_line=sess.business_line,
|
||||
)
|
||||
if refused:
|
||||
return refused
|
||||
return self._confirm_or_hold(
|
||||
sender_id=sender_id,
|
||||
facts=dict(sess.facts),
|
||||
@@ -176,6 +188,18 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
||||
# 复制清单冒号空着的项,菜单文字和模型猜测都不能当成已填。
|
||||
facts = drop_blank_paste_options(facts, source)
|
||||
|
||||
if not mode or mode == "LAND":
|
||||
refused = self._refuse_sensitive(
|
||||
sender_id=sender_id,
|
||||
text=text,
|
||||
facts=facts,
|
||||
reply=reply,
|
||||
sess=sess,
|
||||
business_line=mode or (sess.business_line if sess else ""),
|
||||
)
|
||||
if refused:
|
||||
return refused
|
||||
|
||||
if not mode:
|
||||
mode = "LAND" if (sess and (sess.business_line or "").upper() == "LAND") else ""
|
||||
if not mode:
|
||||
@@ -296,6 +320,16 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
||||
"""
|
||||
import uuid
|
||||
|
||||
refused = self._refuse_sensitive(
|
||||
sender_id=sender_id,
|
||||
text="",
|
||||
facts=facts,
|
||||
reply=reply,
|
||||
sess=self.session_of(sender_id),
|
||||
business_line="LAND",
|
||||
)
|
||||
if refused:
|
||||
return refused
|
||||
tms_ready = assemble_land_query(facts)
|
||||
if not tms_ready.get("ok"):
|
||||
return self._advance_front(
|
||||
|
||||
@@ -43,7 +43,7 @@ from agent.schema.land_options import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 只有补问/核对还能续同一张多段草稿。已出号的拉群停点不能拦住新询价。
|
||||
_FRONT = frozenset({"clarify", "wait_confirm"})
|
||||
_FRONT = frozenset({"clarify", "wait_confirm", "sensitive_block"})
|
||||
|
||||
|
||||
class MultiTextInquiryFlow:
|
||||
@@ -170,6 +170,52 @@ class MultiTextInquiryFlow:
|
||||
reply("多段询价请先把各段需求发齐并回复确定。需要人来接管时,请在工单创建后再说。")
|
||||
return "multi_handoff_later"
|
||||
|
||||
def _gate_sensitive(
|
||||
self,
|
||||
*,
|
||||
sender_id: str,
|
||||
text: str,
|
||||
reply: ReplyFn,
|
||||
segments: list[Segment],
|
||||
sess: Optional[FlowSession],
|
||||
immutable: str,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
多段私聊建单前的违禁品拦截。
|
||||
|
||||
已有工单号的旧会话不把违禁原话带进新的一段。没命中则把品名改口写回各段。
|
||||
命中只回一句,不补问、不建单。
|
||||
"""
|
||||
from agent.policy.sensitive_cargo import BLOCK_REPLY, judge
|
||||
|
||||
facts: dict[str, str] = {}
|
||||
for seg in segments:
|
||||
for key, value in (seg.facts or {}).items():
|
||||
if str(value or "").strip():
|
||||
facts[str(key)] = str(value)
|
||||
use_old = sess is not None and not (sess.work_order_no or "").strip()
|
||||
blocked, new_hold, edits = judge(
|
||||
text=text,
|
||||
facts=facts,
|
||||
hold=str(sess.sensitive_hold or "") if use_old else "",
|
||||
)
|
||||
for seg in segments:
|
||||
for key, value in edits.items():
|
||||
seg.facts[key] = value
|
||||
if not blocked:
|
||||
if use_old and sess is not None:
|
||||
sess.sensitive_hold = new_hold
|
||||
if edits:
|
||||
self._put_segments(sess, segments)
|
||||
self._save(sess)
|
||||
return None
|
||||
draft = self._draft(sender_id, segments, phase="sensitive_block", immutable=immutable or text)
|
||||
draft.sensitive_hold = new_hold
|
||||
self._save(draft)
|
||||
reply(BLOCK_REPLY)
|
||||
logger.info("multi.sensitive_block sender=%s", sender_id)
|
||||
return "sensitive_block"
|
||||
|
||||
def on_text(
|
||||
self,
|
||||
*,
|
||||
@@ -196,6 +242,19 @@ class MultiTextInquiryFlow:
|
||||
sess = self.session_of(sid)
|
||||
if sess and self.match_button(raw, sess):
|
||||
return self._pull_group(sess, reply)
|
||||
if sess and (sess.phase or "") == "sensitive_block" and not (sess.work_order_no or "").strip():
|
||||
refused = self._gate_sensitive(
|
||||
sender_id=sid,
|
||||
text=raw,
|
||||
reply=reply,
|
||||
segments=self._segments(sess),
|
||||
sess=sess,
|
||||
immutable=sess.immutable_text or raw,
|
||||
)
|
||||
if refused:
|
||||
return refused
|
||||
sess.phase = "clarify"
|
||||
self._save(sess)
|
||||
verdict = classify_multi_text(raw)
|
||||
if verdict == "same_mode":
|
||||
reply(copy.MULTI_SAME_MODE)
|
||||
@@ -276,7 +335,28 @@ class MultiTextInquiryFlow:
|
||||
附件口播经常没有「起运港:」这种标签,要按段走和单聊一样的抽取,
|
||||
不能只认标签行,否则会整段待补充。
|
||||
"""
|
||||
current = self.session_of(sender_id)
|
||||
refused = self._gate_sensitive(
|
||||
sender_id=sender_id,
|
||||
text=immutable,
|
||||
reply=reply,
|
||||
segments=list(segments),
|
||||
sess=current if current and not (current.work_order_no or "").strip() else None,
|
||||
immutable=immutable,
|
||||
)
|
||||
if refused:
|
||||
return refused
|
||||
filled = self._enrich_segments(segments, allow_b=allow_b, chat_fn=chat_fn)
|
||||
refused = self._gate_sensitive(
|
||||
sender_id=sender_id,
|
||||
text="",
|
||||
reply=reply,
|
||||
segments=filled,
|
||||
sess=self.session_of(sender_id),
|
||||
immutable=immutable,
|
||||
)
|
||||
if refused:
|
||||
return refused
|
||||
ready, updated, gaps = refresh_segments(filled)
|
||||
if not ready:
|
||||
self._reply_gaps(updated, gaps, reply)
|
||||
@@ -358,6 +438,16 @@ class MultiTextInquiryFlow:
|
||||
|
||||
口语补句走单聊同一套抽取,不在这里猜「广州-深圳」。
|
||||
"""
|
||||
refused = self._gate_sensitive(
|
||||
sender_id=sess.sender_id,
|
||||
text=text,
|
||||
reply=reply,
|
||||
segments=self._segments(sess),
|
||||
sess=sess,
|
||||
immutable=sess.immutable_text or text,
|
||||
)
|
||||
if refused:
|
||||
return refused
|
||||
from agent.llm.extract_text import extract_inquiry_snapshot
|
||||
from agent.policy.system_exception import handle_extract_tech_fail
|
||||
|
||||
@@ -407,6 +497,16 @@ class MultiTextInquiryFlow:
|
||||
def _create_ticket(self, sess: FlowSession, reply: ReplyFn) -> str:
|
||||
"""回复确定后建一张 MULTI 工单。含空运只给手动拉群提示,不含空运出拉群卡。"""
|
||||
segments = self._segments(sess)
|
||||
refused = self._gate_sensitive(
|
||||
sender_id=sess.sender_id,
|
||||
text="",
|
||||
reply=reply,
|
||||
segments=segments,
|
||||
sess=sess,
|
||||
immutable=sess.immutable_text,
|
||||
)
|
||||
if refused:
|
||||
return refused
|
||||
ready, updated, gaps = refresh_segments(segments)
|
||||
if not ready:
|
||||
self._reply_gaps(updated, gaps, reply)
|
||||
|
||||
@@ -113,6 +113,16 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
if sess and sess.phase == "wait_confirm" and looks_like_confirm(text):
|
||||
# 确定查价不再抽字段,避免「确定」被模型改成空 facts。
|
||||
logger.info("sea.confirm sender=%s", sender_id)
|
||||
refused = self._refuse_sensitive(
|
||||
sender_id=sender_id,
|
||||
text=text,
|
||||
facts=dict(sess.facts),
|
||||
reply=reply,
|
||||
sess=sess,
|
||||
business_line=sess.business_line,
|
||||
)
|
||||
if refused:
|
||||
return refused
|
||||
return self._confirm_or_hold(
|
||||
sender_id=sender_id,
|
||||
facts=dict(sess.facts),
|
||||
@@ -145,7 +155,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", "wait_confirm"}:
|
||||
if sess and sess.phase in {"clarify", "need_mode", "wait_confirm", "sensitive_block"}:
|
||||
from agent.llm.extract_text import detect_transport_mode
|
||||
from agent.schema.field_validate import harvest_oral_measures
|
||||
|
||||
@@ -156,6 +166,18 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
if not mode:
|
||||
mode = detect_transport_mode(f"{sess.immutable_text or ''}\n{text}")
|
||||
|
||||
if not mode or mode == "SEA":
|
||||
refused = self._refuse_sensitive(
|
||||
sender_id=sender_id,
|
||||
text=text,
|
||||
facts=facts,
|
||||
reply=reply,
|
||||
sess=sess,
|
||||
business_line=mode or (sess.business_line if sess else ""),
|
||||
)
|
||||
if refused:
|
||||
return refused
|
||||
|
||||
if not mode:
|
||||
reply(copy.ask_transport())
|
||||
self._save(
|
||||
@@ -374,6 +396,16 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
from agent.policy.handoff_human import void_virtual_no
|
||||
|
||||
old = self.session_of(sender_id)
|
||||
refused = self._refuse_sensitive(
|
||||
sender_id=sender_id,
|
||||
text="",
|
||||
facts=facts,
|
||||
reply=reply,
|
||||
sess=old,
|
||||
business_line="SEA",
|
||||
)
|
||||
if refused:
|
||||
return refused
|
||||
if old:
|
||||
void_virtual_no(old)
|
||||
pending = tuple(getattr(old, "pending_files", ()) or ()) if old else ()
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
"""
|
||||
私聊建单前的违禁品拦截。
|
||||
|
||||
本文件职责:用固定词表判断当前这句话、尚未改口的原话,以及品名、起运地、备注
|
||||
是否涉及真家伙(枪支、弹药、爆炸物、毒品)。命中则不能建单。
|
||||
群聊、已经建成的工单不在这里判断,由调用方避开。
|
||||
调用:Worker 里的私聊文字流程,在出补问或建单之前。只做字符串匹配,不调模型。
|
||||
禁止:把水枪、玩具枪、仿真枪、气枪、烟花、礼炮、鞭炮算进去;禁止拦化学品、电池、油品。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
# 销售私聊里只回这一句。不要再追问缺项,也不要带工单号。
|
||||
BLOCK_REPLY = "涉及危险品,无法发起报价。"
|
||||
|
||||
# 先圈出放行说法。词表里的「枪支」若落在这些说法内部,不算命中。
|
||||
_ALLOW = (
|
||||
"玩具枪",
|
||||
"水枪",
|
||||
"仿真枪",
|
||||
"气枪",
|
||||
"烟花",
|
||||
"礼炮",
|
||||
"鞭炮",
|
||||
"炮仗",
|
||||
)
|
||||
|
||||
# 真家伙。不用单字「枪」「炮」「弹」,避免误伤水枪和礼炮。
|
||||
_BLOCK = (
|
||||
"汽油弹",
|
||||
"燃烧弹",
|
||||
"原子弹",
|
||||
"手榴弹",
|
||||
"导火索",
|
||||
"甲基苯丙胺",
|
||||
"氯胺酮",
|
||||
"高射炮",
|
||||
"榴弹炮",
|
||||
"冲锋枪",
|
||||
"狙击枪",
|
||||
"霰弹枪",
|
||||
"火药枪",
|
||||
"摇头丸",
|
||||
"海洛因",
|
||||
"可卡因",
|
||||
"炸弹",
|
||||
"炸药",
|
||||
"爆破",
|
||||
"雷管",
|
||||
"手雷",
|
||||
"地雷",
|
||||
"导弹",
|
||||
"核弹",
|
||||
"枪支",
|
||||
"枪械",
|
||||
"手枪",
|
||||
"步枪",
|
||||
"机枪",
|
||||
"猎枪",
|
||||
"子弹",
|
||||
"弹药",
|
||||
"弹头",
|
||||
"大炮",
|
||||
"火炮",
|
||||
"毒品",
|
||||
"冰毒",
|
||||
"大麻",
|
||||
"鸦片",
|
||||
"吗啡",
|
||||
"K粉",
|
||||
)
|
||||
|
||||
# 改口动词。多段原文里的「品名:衣服」不能走这里,否则会抄到每一段。
|
||||
_FIELD_EDIT = re.compile(
|
||||
r"(货物品名|品名|起运地|起运港|始发站|备注)\s*(?:改为|改成|是|为)\s*(\S+)"
|
||||
)
|
||||
# 单票里销售按清单重写「品名:服装」。多段「段1 / 段2」整单不靠这个改字段。
|
||||
_LABEL_EDIT = re.compile(
|
||||
r"(货物品名|品名|起运地|起运港|始发站|备注)\s*[::]\s*(\S+)"
|
||||
)
|
||||
_MULTI_SHEET = re.compile(r"段\s*\d|第\s*[一二三四五六七八九\d]\s*段")
|
||||
|
||||
# 销售侧说法写回内部键。起运地在主账里仍是起运港。
|
||||
_WRITE_KEYS = {
|
||||
"品名": ("品名", "货物品名"),
|
||||
"货物品名": ("品名", "货物品名"),
|
||||
"起运地": ("起运港", "起运地", "始发站"),
|
||||
"起运港": ("起运港", "起运地", "始发站"),
|
||||
"始发站": ("起运港", "起运地", "始发站"),
|
||||
"备注": ("备注",),
|
||||
}
|
||||
|
||||
# 建单前要看的当前字段。没写进这几项、但原话里出现过的,靠 sensitive_hold 记住。
|
||||
_SCAN_KEYS = ("品名", "货物品名", "起运港", "起运地", "始发站", "备注")
|
||||
|
||||
|
||||
def contains_blocked(text: str) -> bool:
|
||||
"""
|
||||
这句话里有没有要拦的词。
|
||||
|
||||
放行说法内部的字不算。例如「玩具枪」放行,「玩具枪支」仍因「枪支」拦下。
|
||||
不做英文、拼音、谐音。纯字符串查找,可在 Worker 线程同步调用。
|
||||
"""
|
||||
raw = text or ""
|
||||
if not raw:
|
||||
return False
|
||||
allow_spans: list[tuple[int, int]] = []
|
||||
for phrase in _ALLOW:
|
||||
start = 0
|
||||
while True:
|
||||
found = raw.find(phrase, start)
|
||||
if found < 0:
|
||||
break
|
||||
allow_spans.append((found, found + len(phrase)))
|
||||
start = found + len(phrase)
|
||||
|
||||
def _inside(begin: int, end: int) -> bool:
|
||||
return any(left <= begin and end <= right for left, right in allow_spans)
|
||||
|
||||
for word in _BLOCK:
|
||||
start = 0
|
||||
while True:
|
||||
found = raw.find(word, start)
|
||||
if found < 0:
|
||||
break
|
||||
if not _inside(found, found + len(word)):
|
||||
return True
|
||||
start = found + len(word)
|
||||
return False
|
||||
|
||||
|
||||
def apply_field_edits(text: str, facts: dict[str, Any]) -> dict[str, str]:
|
||||
"""
|
||||
把当前这句话里的「品名改为…」「起运地改为…」「备注改为…」写进字段。
|
||||
|
||||
只改这一句写明的项。返回实际写下的键值,方便多段把品名写回每一段。
|
||||
会改传入的 facts。值末尾的标点去掉,避免「服装,」留在品名里。
|
||||
"""
|
||||
written: dict[str, str] = {}
|
||||
pairs = list(_FIELD_EDIT.findall(text or ""))
|
||||
if not _MULTI_SHEET.search(text or ""):
|
||||
pairs.extend(_LABEL_EDIT.findall(text or ""))
|
||||
for label, raw_value in pairs:
|
||||
value = str(raw_value or "").strip(",,。;;、 ")
|
||||
if not value:
|
||||
continue
|
||||
for key in _WRITE_KEYS.get(label, (label,)):
|
||||
facts[key] = value
|
||||
written[key] = value
|
||||
return written
|
||||
|
||||
|
||||
def is_cargo_rename(text: str) -> bool:
|
||||
"""当前句有没有把品名重新说成一个新值。没说品名的补重量不算改口。"""
|
||||
body = text or ""
|
||||
labels = [label for label, _value in _FIELD_EDIT.findall(body)]
|
||||
if not _MULTI_SHEET.search(body):
|
||||
labels.extend(label for label, _value in _LABEL_EDIT.findall(body))
|
||||
return any(label in {"品名", "货物品名"} for label in labels)
|
||||
|
||||
|
||||
def judge(*, text: str, facts: dict[str, Any], hold: str) -> tuple[bool, str]:
|
||||
"""
|
||||
决定这一轮私聊能不能继续往建单走。
|
||||
|
||||
返回 (是否拦截, 新的未改口原话, 本句写下的字段)。
|
||||
当前句里仍有违禁词:拦住,并把这句记下来。
|
||||
当前句把品名说成普通货、且这句里没有违禁词:以前没落进字段的那句作废。
|
||||
只补重量、件数:旧原话继续有效。
|
||||
品名改干净之后,起运地或备注里还留着违禁词,仍然拦截。
|
||||
会就地更新 facts 里本句写明的品名、起运地、备注。
|
||||
第三个返回值是本句写下的键,多段用来写回每一段,避免把别的字段盖掉。
|
||||
"""
|
||||
edits = apply_field_edits(text, facts)
|
||||
message_hit = contains_blocked(text)
|
||||
if message_hit:
|
||||
new_hold = (text or "").strip() or (hold or "")
|
||||
elif is_cargo_rename(text):
|
||||
new_hold = ""
|
||||
else:
|
||||
new_hold = hold or ""
|
||||
field_text = "\n".join(str((facts or {}).get(key) or "") for key in _SCAN_KEYS)
|
||||
blocked = message_hit or contains_blocked(new_hold) or contains_blocked(field_text)
|
||||
return blocked, new_hold, edits
|
||||
Reference in New Issue
Block a user