落地海运协同群:测服靠会话存档听群话,报价单能带上协同字段。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,18 +1,27 @@
|
||||
"""
|
||||
意图:海运协同群里补协同字段。
|
||||
意图:海运协同群入站(文字意图 / 附件)。
|
||||
|
||||
本文件职责:群聊文字 → 只抽协同六字段 → 静默写主账。
|
||||
禁止:回「已记下」;把闲聊/报价金额写成报价版本;改工单六态。
|
||||
本文件职责:对上工单后交给 sea_group_ops;一类动作一条分支。
|
||||
禁止:改询价七项;终态装没看见;把销售附件当报价。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
|
||||
from agent.channel.wecom.models import InboundMessage
|
||||
from agent.llm.extract_text import extract_collab_fields
|
||||
from agent.policy.sea_group_ops import (
|
||||
apply_collab_update,
|
||||
apply_product_quote,
|
||||
dissolve_group,
|
||||
group_client_of,
|
||||
handle_group_file,
|
||||
send_group_text,
|
||||
ticket_no,
|
||||
ticket_view,
|
||||
)
|
||||
from agent.policy.sea_text_flow import get_sea_text_flow
|
||||
from agent.schema.field_validate import SEA_COLLAB_FIELDS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -22,12 +31,36 @@ def handle_group_collab(
|
||||
*,
|
||||
flow=None,
|
||||
ledger=None,
|
||||
injected_intent: str = "",
|
||||
injected_quote: Optional[dict[str, Any]] = None,
|
||||
) -> str:
|
||||
"""
|
||||
处理一条群聊文字。
|
||||
处理一条群聊入站。兼容旧测试名。
|
||||
|
||||
必须能对上已绑定工单,且发言人是被拉进来的销售或产品。
|
||||
识别到协同字段才写库;否则忽略。不回复。
|
||||
"""
|
||||
return handle_group_inbound(
|
||||
message,
|
||||
flow=flow,
|
||||
ledger=ledger,
|
||||
injected_intent=injected_intent,
|
||||
injected_quote=injected_quote,
|
||||
)
|
||||
|
||||
|
||||
def handle_group_inbound(
|
||||
message: InboundMessage,
|
||||
*,
|
||||
flow=None,
|
||||
ledger=None,
|
||||
injected_intent: str = "",
|
||||
injected_quote: Optional[dict[str, Any]] = None,
|
||||
) -> str:
|
||||
"""
|
||||
群里一条文字或附件。
|
||||
|
||||
文字走 DeepSeek A(单测可注入);附件只看扩展名。
|
||||
协同已结束或终态要回话,不能当没看见。
|
||||
"""
|
||||
if (message.chat_type or "") != "group":
|
||||
return "group_ignore"
|
||||
@@ -41,40 +74,105 @@ def handle_group_collab(
|
||||
|
||||
ticket = _find_ticket(book, chat_id)
|
||||
if ticket is None:
|
||||
logger.info("group_inbound 对不上工单 chat=%s", chat_id)
|
||||
return "group_ignore"
|
||||
if not _sender_allowed(ticket, message.sender_id):
|
||||
logger.info("group_collab 忽略非成员 sender=%s chat=%s", message.sender_id, chat_id)
|
||||
logger.info("group_inbound 忽略非成员 sender=%s chat=%s", message.sender_id, chat_id)
|
||||
return "group_ignore"
|
||||
|
||||
facts = extract_collab_fields(message.content or "")
|
||||
if not facts:
|
||||
return "group_ignore"
|
||||
sess = _session_of_ticket(engine, ticket)
|
||||
view = ticket_view(ticket)
|
||||
if view.get("collab_ended") or (sess is not None and getattr(sess, "collab_ended", False)):
|
||||
from agent.policy import inquiry_copy as copy
|
||||
|
||||
if isinstance(ticket, dict):
|
||||
wo = str(ticket.get("work_order_no") or "")
|
||||
else:
|
||||
wo = str(getattr(ticket, "work_order_no", "") or "")
|
||||
if not wo:
|
||||
return "group_ignore"
|
||||
send_group_text(
|
||||
group_client_of(engine),
|
||||
chat_id,
|
||||
copy.group_dissolved(work_order_no=ticket_no(ticket)),
|
||||
)
|
||||
return "group_already_ended"
|
||||
|
||||
engine.apply_collab_facts(
|
||||
work_order_no=wo,
|
||||
facts=facts,
|
||||
sender_id=message.sender_id,
|
||||
kind = (message.msg_type or "text").lower()
|
||||
if kind in {"file", "image", "video"}:
|
||||
media = dict(message.media or {})
|
||||
filename = str(
|
||||
media.get("filename") or media.get("FileName") or (message.content or "")
|
||||
).strip()
|
||||
return handle_group_file(
|
||||
flow=engine,
|
||||
ticket=ticket,
|
||||
sender_id=message.sender_id,
|
||||
filename=filename,
|
||||
media=media,
|
||||
sess=sess,
|
||||
)
|
||||
|
||||
from agent.routing.group_intent import (
|
||||
INTENT_DISSOLVE,
|
||||
INTENT_FIELDS,
|
||||
INTENT_QUOTE,
|
||||
classify_group_text,
|
||||
)
|
||||
logger.info(
|
||||
"group_collab 写入 wo=%s keys=%s sender=%s",
|
||||
wo,
|
||||
sorted(k for k in facts if k in SEA_COLLAB_FIELDS),
|
||||
message.sender_id,
|
||||
)
|
||||
return "collab_field_saved"
|
||||
|
||||
intent = classify_group_text(message.content or "", injected=injected_intent)
|
||||
if intent == INTENT_FIELDS:
|
||||
return apply_collab_update(
|
||||
flow=engine,
|
||||
ticket=ticket,
|
||||
text=message.content or "",
|
||||
sender_id=message.sender_id,
|
||||
sess=sess,
|
||||
)
|
||||
if intent == INTENT_QUOTE:
|
||||
return apply_product_quote(
|
||||
flow=engine,
|
||||
ticket=ticket,
|
||||
text=message.content or "",
|
||||
sender_id=message.sender_id,
|
||||
sess=sess,
|
||||
injected_quote=injected_quote,
|
||||
)
|
||||
if intent == INTENT_DISSOLVE:
|
||||
return dissolve_group(flow=engine, ticket=ticket, sess=sess)
|
||||
logger.info("group_inbound 其它意图 chat=%s intent=%s", chat_id, intent)
|
||||
return "group_ignore"
|
||||
|
||||
|
||||
def _find_ticket(book: object, chat_id: str):
|
||||
finder = getattr(book, "find_by_collab_chat", None)
|
||||
if callable(finder):
|
||||
return finder(chat_id=chat_id)
|
||||
hit = finder(chat_id=chat_id)
|
||||
if hit is not None:
|
||||
return hit
|
||||
try:
|
||||
from agent.channel.wshoto.rooms import ticket_no_for_room
|
||||
|
||||
no = ticket_no_for_room(chat_id)
|
||||
except Exception:
|
||||
logger.exception("存档房间反查工单失败 chat=%s", chat_id)
|
||||
no = ""
|
||||
if not no:
|
||||
return None
|
||||
getter = getattr(book, "get_ticket", None)
|
||||
if callable(getter):
|
||||
hit = getter(work_order_no=no)
|
||||
if hit is not None:
|
||||
return hit
|
||||
getter = getattr(book, "get_for_agent", None)
|
||||
if callable(getter):
|
||||
return getter(work_order_no=no, sender_id="")
|
||||
return None
|
||||
|
||||
|
||||
def _session_of_ticket(engine: object, ticket: object):
|
||||
"""用工单号找回销售侧书签,方便写 collab_facts / deal_version。"""
|
||||
no = ticket_no(ticket)
|
||||
getter = getattr(engine, "session_by_work_order", None)
|
||||
if callable(getter) and no:
|
||||
return getter(no)
|
||||
by_ticket = getattr(engine, "_by_ticket", None)
|
||||
if isinstance(by_ticket, dict) and no:
|
||||
return by_ticket.get(no)
|
||||
return None
|
||||
|
||||
|
||||
@@ -82,12 +180,9 @@ def _sender_allowed(ticket: object, sender_id: str) -> bool:
|
||||
uid = (sender_id or "").strip()
|
||||
if not uid:
|
||||
return False
|
||||
if isinstance(ticket, dict):
|
||||
sales = str(ticket.get("sales_wecom_id") or ticket.get("salesWecomId") or "")
|
||||
members = ticket.get("collab_member_ids") or ticket.get("memberIds") or []
|
||||
else:
|
||||
sales = str(getattr(ticket, "sales_wecom_id", "") or "")
|
||||
members = list(getattr(ticket, "collab_member_ids", []) or [])
|
||||
view = ticket_view(ticket)
|
||||
sales = str(view.get("sales_wecom_id") or view.get("salesWecomId") or "")
|
||||
members = view.get("collab_member_ids") or view.get("memberIds") or []
|
||||
if uid == sales:
|
||||
return True
|
||||
return uid in {str(x) for x in members}
|
||||
|
||||
Reference in New Issue
Block a user