私聊建单前拦住枪支弹药爆炸物和毒品,避免真家伙进入报价。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-14 20:24:13 +08:00
co-authored by Cursor
parent 9ffba70741
commit 617c5e2619
8 changed files with 656 additions and 7 deletions
@@ -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