机器人显示名与站点标题可配;多段联运列表投影;协同群监听对账;工单附件落存;模板与工单后台补强
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -205,46 +205,6 @@ def handle_air_group(
|
||||
intent,
|
||||
wo or "-",
|
||||
)
|
||||
# #region agent log
|
||||
try:
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from agent.llm.extract_text import labeled_quote_date
|
||||
|
||||
_payload = {
|
||||
"sessionId": "717349",
|
||||
"runId": "post-fix",
|
||||
"hypothesisId": "H1",
|
||||
"location": "air_group_collab.py:handle_air_group",
|
||||
"message": "air group inbound classified",
|
||||
"data": {
|
||||
"chat_tail": chat_id[-8:],
|
||||
"sender_tail": (message.sender_id or "")[-8:],
|
||||
"intent": intent,
|
||||
"wo": wo or "",
|
||||
"text_head": (text or "")[:100],
|
||||
"labeled_qd": labeled_quote_date(text),
|
||||
"source": str((message.raw or {}).get("source") or ""),
|
||||
},
|
||||
"timestamp": int(time.time() * 1000),
|
||||
}
|
||||
_line = json.dumps(_payload, ensure_ascii=False) + "\n"
|
||||
for _p in (
|
||||
Path(__file__).resolve().parents[3] / "debug-717349.log",
|
||||
Path(r"C:\Users\ytd_extdev_0903\incoming\debug-717349.log"),
|
||||
Path(r"E:\wwwroot\agent\current\debug-717349.log"),
|
||||
):
|
||||
try:
|
||||
_p.parent.mkdir(parents=True, exist_ok=True)
|
||||
with _p.open("a", encoding="utf-8") as _fh:
|
||||
_fh.write(_line)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
# #endregion
|
||||
if intent in {INTENT_LOCK, INTENT_RELEASE, INTENT_RELEASE_ASK}:
|
||||
return _handle_cabin(
|
||||
engine,
|
||||
@@ -314,17 +274,34 @@ def _handle_file(engine, message: InboundMessage) -> str:
|
||||
wo = copy.extract_work_order_no(
|
||||
" ".join([message.content or "", filename])
|
||||
)
|
||||
# 多段已激活的附件也跟当前单,不能再要求先发工单号。
|
||||
followed = _follow_activated_multi(
|
||||
engine,
|
||||
chat_id=chat_id,
|
||||
sender_id=message.sender_id,
|
||||
text=" ".join(part for part in ((message.content or "").strip(), filename) if part),
|
||||
injected_quote=None,
|
||||
named_wo=wo,
|
||||
)
|
||||
if followed is not None:
|
||||
return followed
|
||||
# 多段附件:落库归段,禁止把文件名当文字报价(会回「没有识别到报价」)。
|
||||
from agent.policy.multi_group_ops import apply_multi_file, ticket_is_multi
|
||||
from agent.policy.sea_group_ops import ticket_no as _ticket_no
|
||||
|
||||
multi_ticket = None
|
||||
if wo:
|
||||
getter = getattr(engine.ledger, "get_ticket", None)
|
||||
raw = getter(work_order_no=wo) if callable(getter) else None
|
||||
if raw is not None and ticket_is_multi(raw):
|
||||
multi_ticket = raw
|
||||
if multi_ticket is None:
|
||||
bound = current_ticket(engine, chat_id)
|
||||
if bound is not None and ticket_is_multi(bound):
|
||||
multi_ticket = bound
|
||||
if multi_ticket is not None:
|
||||
logger.info(
|
||||
"air_group 多段附件 wo=%s file=%s",
|
||||
_ticket_no(multi_ticket),
|
||||
filename[:80],
|
||||
)
|
||||
return apply_multi_file(
|
||||
flow=engine,
|
||||
ticket=multi_ticket,
|
||||
chat_id=chat_id,
|
||||
sender_id=message.sender_id,
|
||||
filename=filename,
|
||||
media=media,
|
||||
)
|
||||
if wo:
|
||||
ticket, phase = activate_ticket(
|
||||
flow=engine, chat_id=chat_id, work_order_no=wo, speak=False
|
||||
@@ -588,36 +565,6 @@ def _handle_bound(
|
||||
keep_tms=intent == INTENT_KEEP_TMS,
|
||||
)
|
||||
if intent == INTENT_FIELDS:
|
||||
# #region agent log
|
||||
try:
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
_p = Path(__file__).resolve().parents[3] / "debug-866775.log"
|
||||
with _p.open("a", encoding="utf-8") as _fh:
|
||||
_fh.write(
|
||||
json.dumps(
|
||||
{
|
||||
"sessionId": "866775",
|
||||
"hypothesisId": "C",
|
||||
"location": "air_group_collab.py:_handle_bound",
|
||||
"message": "INTENT_FIELDS path",
|
||||
"data": {
|
||||
"text_head": (message.content or "")[:80],
|
||||
"has_quote_date_label": (
|
||||
"报价日期" in (message.content or "")
|
||||
),
|
||||
},
|
||||
"timestamp": int(time.time() * 1000),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
# #endregion
|
||||
return apply_air_fields(
|
||||
flow=engine,
|
||||
ticket=ticket,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
意图:询价卡片按钮(复制工单号 / 拉产品进群 / 跳过协同 / 采用 / 不采用 / 成交)。
|
||||
|
||||
本文件职责:校验当前会话允许的动作后交给流程执行。
|
||||
本文件职责:校验当前会话允许的动作后交给流程执行;卡片按钮集中防抖入口。
|
||||
禁止:不校验 wait/允许集合就改主账;禁止一个函数处理未声明的新按钮。
|
||||
"""
|
||||
|
||||
@@ -15,6 +15,7 @@ from agent.policy import inquiry_copy as copy
|
||||
from agent.policy.air_text_flow import get_air_text_flow
|
||||
from agent.policy.land_text_flow import get_land_text_flow
|
||||
from agent.policy.sea_text_flow import get_sea_text_flow
|
||||
from agent.redis_coord.card_debounce import debounce_key, try_acquire as try_card_debounce
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -57,6 +58,7 @@ def handle_card_action(
|
||||
|
||||
wait_version:会话书签版本;错版本丢弃,不重放副作用。
|
||||
payload 须带 task_id / event_key,才能找回点卡当时的工单。
|
||||
连点:同一 task_id+按钮在防抖 TTL 内只跑一次业务,避免多条智能体提示。
|
||||
"""
|
||||
_ = thread_id
|
||||
if not (sender_id or "").strip():
|
||||
@@ -68,6 +70,41 @@ def handle_card_action(
|
||||
return "card_discard"
|
||||
|
||||
msg_store = store or get_message_store()
|
||||
wo_hint = ""
|
||||
if sess is not None:
|
||||
wo_hint = (sess.work_order_no or "").strip()
|
||||
if not wo_hint:
|
||||
wo_hint = copy.work_order_from_card_meta(payload)
|
||||
# 集中防抖:键用 task_id+动作,不用 ResponseCode(企微每次点击换新码)
|
||||
dkey = debounce_key(
|
||||
action=action,
|
||||
task_id=str((payload or {}).get("task_id") or ""),
|
||||
work_order_no=wo_hint,
|
||||
sender_id=sender_id,
|
||||
event_key=str((payload or {}).get("event_key") or ""),
|
||||
)
|
||||
if not try_card_debounce(dkey):
|
||||
logger.info(
|
||||
"card 防抖丢弃 sender=%s action=%s key=%s",
|
||||
sender_id,
|
||||
action,
|
||||
dkey,
|
||||
)
|
||||
# 仍用新 ResponseCode 置灰,避免企微按钮一直可点;不发业务 ack
|
||||
code = str((payload or {}).get("response_code") or "").strip()
|
||||
if code:
|
||||
msg_store.enqueue_outbound(
|
||||
touser=sender_id,
|
||||
content=copy.BTN_SKIP_COLLAB_DONE,
|
||||
dedupe_key=f"card-grey:{sender_id}:{wo_hint}:{code}",
|
||||
payload=copy.skip_collab_card_update(
|
||||
response_code=code,
|
||||
replace_name=action if action in {
|
||||
copy.BTN_DEAL, copy.BTN_LOST, copy.BTN_NEGOTIATE
|
||||
} else "",
|
||||
),
|
||||
)
|
||||
return "card_debounced"
|
||||
|
||||
def reply(text: str, extra: dict | None = None) -> str:
|
||||
# 销售询价正文不加 [test];[test] 只给运维告警
|
||||
@@ -79,13 +116,16 @@ def handle_card_action(
|
||||
if not wo:
|
||||
wo = copy.work_order_from_card_meta(payload)
|
||||
code = str((payload or {}).get("response_code") or extra.get("response_code") or "")[:24]
|
||||
msgtype = str(extra.get("msgtype") or "text")
|
||||
# 文本 ack 按工单+动作去重;置灰更新仍带 code(每次 ResponseCode 只能用一次)
|
||||
if msgtype == "update_template_card":
|
||||
dedupe_key = f"card-grey:{sender_id}:{wo}:{code}:{action}"
|
||||
else:
|
||||
dedupe_key = f"card-ack:{sender_id}:{wo}:{action}:{body[:24]}"
|
||||
ok, info = msg_store.enqueue_outbound(
|
||||
touser=sender_id,
|
||||
content=body,
|
||||
dedupe_key=(
|
||||
f"card:{sender_id}:{wo}:{code}:"
|
||||
f"{sess.wait_version if sess else 0}:{action}:{body[:20]}"
|
||||
),
|
||||
dedupe_key=dedupe_key,
|
||||
payload=extra,
|
||||
)
|
||||
if not ok:
|
||||
|
||||
@@ -103,7 +103,12 @@ def handle_group_inbound(
|
||||
or ""
|
||||
).upper()
|
||||
if line == "MULTI":
|
||||
from agent.policy.multi_group_ops import bot_sender_when_air_multi, handle_multi_inbound, sender_mode
|
||||
from agent.policy.multi_group_ops import (
|
||||
apply_multi_file,
|
||||
bot_sender_when_air_multi,
|
||||
handle_multi_inbound,
|
||||
sender_mode,
|
||||
)
|
||||
|
||||
if not (
|
||||
_sender_allowed(ticket, message.sender_id)
|
||||
@@ -111,6 +116,22 @@ def handle_group_inbound(
|
||||
):
|
||||
logger.info("group_inbound 多段忽略非成员 sender=%s", message.sender_id)
|
||||
return "group_ignore"
|
||||
kind_multi = (message.msg_type or "text").lower()
|
||||
# 附件走归段落库;禁止把文件名塞进文字报价。
|
||||
if kind_multi in {"file", "image", "video"}:
|
||||
media = dict(message.media or {})
|
||||
filename = str(
|
||||
media.get("filename") or media.get("FileName") or (message.content or "")
|
||||
).strip()
|
||||
with bot_sender_when_air_multi(ticket):
|
||||
return apply_multi_file(
|
||||
flow=engine,
|
||||
ticket=ticket,
|
||||
chat_id=chat_id,
|
||||
sender_id=message.sender_id,
|
||||
filename=filename,
|
||||
media=media,
|
||||
)
|
||||
# 会话存档听到的报价也走这里。含空运时群是机器人建的,必须改走桥才能发得出去。
|
||||
with bot_sender_when_air_multi(ticket):
|
||||
return handle_multi_inbound(
|
||||
@@ -142,6 +163,20 @@ def handle_group_inbound(
|
||||
|
||||
sess = _session_of_ticket(engine, ticket)
|
||||
if is_handoff_ticket(ticket, sess):
|
||||
# 转人工后不再跟报价/成交/补字段,但「解散群」仍要执行,否则协同群清不掉。
|
||||
from agent.routing.group_intent import INTENT_DISSOLVE, classify_group_text
|
||||
|
||||
kind0 = (message.msg_type or "text").lower()
|
||||
if kind0 in {"", "text"} and (
|
||||
classify_group_text(message.content or "", injected=injected_intent)
|
||||
== INTENT_DISSOLVE
|
||||
):
|
||||
logger.info(
|
||||
"group_inbound 转人工仍解散 wo=%s chat=%s",
|
||||
ticket_no(ticket),
|
||||
chat_id,
|
||||
)
|
||||
return dissolve_group(flow=engine, ticket=ticket, sess=sess)
|
||||
logger.info(
|
||||
"group_inbound 转人工不再跟 wo=%s chat=%s",
|
||||
ticket_no(ticket),
|
||||
@@ -221,12 +256,13 @@ def handle_group_inbound(
|
||||
text=message.content or "",
|
||||
)
|
||||
if intent == INTENT_CONFIRM_TMS:
|
||||
return apply_confirm_tms_quote(
|
||||
phase = apply_confirm_tms_quote(
|
||||
flow=engine,
|
||||
ticket=ticket,
|
||||
sender_id=message.sender_id,
|
||||
sess=sess,
|
||||
)
|
||||
return phase
|
||||
if intent in {INTENT_KEEP_TMS, INTENT_DROP_TMS}:
|
||||
return apply_tms_quote_choice(
|
||||
flow=engine,
|
||||
|
||||
Reference in New Issue
Block a user