落地空运群内协同与锁舱出站。

群里按 BOT 通道激活当前工单、航线报价和成交跟进;没有 TMS 报价编号时用工单字段补齐仍调锁舱,避免被本地规则拦住。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-16 17:25:42 +08:00
co-authored by Cursor
parent 12219cacb1
commit d9029dec89
62 changed files with 6772 additions and 159 deletions
+178 -5
View File
@@ -17,9 +17,102 @@ logger = logging.getLogger(__name__)
INTENT_FIELDS = "collab_fields"
INTENT_QUOTE = "product_quote"
INTENT_DISSOLVE = "dissolve"
INTENT_KEEP_TMS = "quote_keep_tms"
INTENT_DROP_TMS = "quote_drop_tms"
INTENT_CONFIRM_TMS = "quote_confirm_tms"
INTENT_DEAL = "deal_won"
INTENT_LOST = "deal_lost"
INTENT_NEGOTIATE = "deal_negotiate"
INTENT_OTHER = "other"
_VALID = {INTENT_FIELDS, INTENT_QUOTE, INTENT_DISSOLVE, INTENT_OTHER}
_VALID = {
INTENT_FIELDS,
INTENT_QUOTE,
INTENT_DISSOLVE,
INTENT_KEEP_TMS,
INTENT_DROP_TMS,
INTENT_CONFIRM_TMS,
INTENT_DEAL,
INTENT_LOST,
INTENT_NEGOTIATE,
INTENT_OTHER,
}
# 口令不写死:这两句是建议说法,相近意思也认。有金额的仍当报价。
# 问句列了 1/2 时,产品常回「第一个」而不是整句口令。
_KEEP_HINTS = (
"在tms基础上",
"在tms上改",
"保留tms",
"按tms改",
"按tms的改",
"留着tms",
"在原来基础上",
"在原报价上",
"在原基础上",
)
_KEEP_CHOICE = {
"1",
"1.",
"1、",
"一",
"第一",
"第一个",
"第一条",
"选项1",
"选项一",
"选1",
"选一",
"第1",
"第1个",
}
_DROP_CHOICE = {
"2",
"2.",
"2、",
"二",
"第二",
"第二个",
"第二条",
"选项2",
"选项二",
"选2",
"选二",
"第2",
"第2个",
}
# 先认未成交,避免「未成交」被「成交」误伤。
_LOST_HINTS = ("未成交", "没成交", "没有成交", "没做成", "黄了")
_NEGOTIATE_HINTS = ("协商中", "继续谈", "再谈谈", "还在谈")
_DEAL_HINTS = ("成交", "成了", "做成了", "已经成交", "搞定了")
_CONFIRM_CHOICE = {
"确认报价",
"确认该报价",
"确认这个报价",
"确认tms报价",
"沿用该价",
"沿用tms",
"沿用tms报价",
}
_CONFIRM_HINTS = (
"确认报价",
"确认该报价",
"确认这个报价",
"确认tms报价",
"沿用该价",
"沿用tms",
)
_DROP_HINTS = (
"不用tms报价",
"不要tms报价",
"不用tms",
"不要tms",
"不要tms的价",
"按我的报价",
"按我刚才报",
"只用我的",
"全部按我",
)
def classify_group_text(
@@ -51,18 +144,80 @@ def classify_group_text(
if use_net and getattr(settings, "llm_data_usage_confirmed", False):
got = _classify_with_model(raw, settings)
if got in _VALID:
hinted_confirm = _hint_confirm_tms(raw)
if hinted_confirm and got in {INTENT_OTHER, INTENT_FIELDS}:
logger.info("群意图模型判成%s,按口令改回 %s text=%s", got, hinted_confirm, raw[:40])
return hinted_confirm
hinted_tms = _hint_tms_choice(raw)
if hinted_tms and got in {INTENT_OTHER, INTENT_FIELDS}:
logger.info("群意图模型判成%s,按口令改回 %s text=%s", got, hinted_tms, raw[:40])
return hinted_tms
hinted_deal = _hint_deal_outcome(raw)
# 销售说成交时,模型不能把群拆了。
if hinted_deal and got == INTENT_DISSOLVE:
logger.warning("群意图模型把成交判成解散,改回 %s text=%s", hinted_deal, raw[:40])
return hinted_deal
return got
except Exception:
logger.exception("群意图模型失败,回退标签字段")
if extract_collab_fields(raw):
return INTENT_FIELDS
from agent.llm.extract_text import extract_product_quote
if extract_product_quote(raw):
return INTENT_QUOTE
confirmed = _hint_confirm_tms(raw)
if confirmed:
return confirmed
hinted = _hint_tms_choice(raw)
if hinted:
return hinted
closed = _hint_deal_outcome(raw)
if closed:
return closed
if extract_collab_fields(raw):
return INTENT_FIELDS
return INTENT_OTHER
def _norm_intent_text(text: str) -> str:
return (text or "").replace(" ", "").replace(" ", "").lower()
def _hint_confirm_tms(text: str) -> str:
"""沿用当前 TMS 价。口令不写死,确认报价 / 沿用该价都认。"""
blob = _norm_intent_text(text)
if blob in _CONFIRM_CHOICE:
return INTENT_CONFIRM_TMS
if any(x in blob for x in _CONFIRM_HINTS):
return INTENT_CONFIRM_TMS
return ""
def _hint_tms_choice(text: str) -> str:
"""相近口令回退。有价格的句子不会走到这里。整句是 1/第一个 也认。"""
blob = _norm_intent_text(text)
if blob in _KEEP_CHOICE:
return INTENT_KEEP_TMS
if blob in _DROP_CHOICE:
return INTENT_DROP_TMS
if any(x in blob for x in _KEEP_HINTS):
return INTENT_KEEP_TMS
if any(x in blob for x in _DROP_HINTS):
return INTENT_DROP_TMS
return ""
def _hint_deal_outcome(text: str) -> str:
"""成交/未成交/协商中:建议说法 + 相近口语。"""
blob = _norm_intent_text(text)
if any(x in blob for x in _LOST_HINTS):
return INTENT_LOST
if any(x in blob for x in _NEGOTIATE_HINTS):
return INTENT_NEGOTIATE
if any(x in blob for x in _DEAL_HINTS):
return INTENT_DEAL
return ""
def _classify_with_model(text: str, settings) -> str:
"""DeepSeek 只回一个意图词。失败空串。"""
from agent.llm.http_client import LlmHttpClient
@@ -72,10 +227,17 @@ def _classify_with_model(text: str, settings) -> str:
system = (
"你是海运协同群意图分类。只回复一个英文词,不要解释。"
"collab_fields=在补贸易条款/货好时间/商品海关编码/含油含电含磁;"
"product_quote=产品在报价格或费用;"
"product_quote=产品在报价格或费用(含具体金额);"
"quote_confirm_tms=产品确认沿用当前TMS报价,没有报新金额;"
"quote_keep_tms=要在已有TMS报价上改,没有报新金额;"
"quote_drop_tms=不要TMS、只用产品刚报的价,没有报新金额;"
"deal_won=销售确认这单成交了;"
"deal_lost=销售确认这单未成交;"
"deal_negotiate=销售要继续协商;"
"dissolve=要把这个协同群解散;"
"other=闲聊、问候、对询价港口的随口更正(不当改单)。"
"一句话里既补字段又报价,回 product_quote。"
"有金额就是 product_quote,不要判成 quote_keep_tms。"
)
data = client.chat(
mode=LlmMode.ORDERED_ACTIONS,
@@ -85,7 +247,18 @@ def _classify_with_model(text: str, settings) -> str:
],
)
content = str(getattr(data, "content", "") or "").strip().lower()
for token in (INTENT_QUOTE, INTENT_DISSOLVE, INTENT_FIELDS, INTENT_OTHER):
for token in (
INTENT_QUOTE,
INTENT_CONFIRM_TMS,
INTENT_KEEP_TMS,
INTENT_DROP_TMS,
INTENT_LOST,
INTENT_NEGOTIATE,
INTENT_DEAL,
INTENT_DISSOLVE,
INTENT_FIELDS,
INTENT_OTHER,
):
if token in content:
return token
return ""