机器人显示名与站点标题可配;多段联运列表投影;协同群监听对账;工单附件落存;模板与工单后台补强

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-10-19 09:41:59 +08:00
co-authored by Cursor
parent 277cdc1c28
commit 2b528758b4
119 changed files with 6760 additions and 3516 deletions
+63 -16
View File
@@ -390,7 +390,11 @@ def send_group_text(
content: str,
mention_userids: Optional[list[str]] = None,
) -> None:
"""群文本可带企微 userid,方便客户端真正点到人。旧客户端无此参数就只发正文。"""
"""
群文本可带企微 userid,方便客户端真正点到人。旧客户端无此参数就只发正文。
发完后把该群标热:销售接着回「解散群」时,存档轮询会优先扫到,少等几分钟。
"""
if not client or not chat_id or not content:
return
sender = getattr(client, "send_group", None)
@@ -401,6 +405,13 @@ def send_group_text(
sender(chat_id=chat_id, content=content, mention_userids=mentions)
except TypeError:
sender(chat_id=chat_id, content=content)
try:
from agent.channel.wshoto.rooms import mark_room_hot, watch_collab_room
watch_collab_room(chat_id)
mark_room_hot(chat_id)
except Exception:
logger.exception("发群后登记热窗失败 chat=%s", chat_id)
def hydrate_ticket(flow: Any, ticket: object) -> object:
@@ -572,11 +583,13 @@ def send_brief_and_sheet(
quote_override: Optional[dict[str, Any]] = None,
tms_quote: Optional[dict[str, Any]] = None,
product_quote: Optional[dict[str, Any]] = None,
persist_attachment: bool = False,
) -> str:
"""
群里再发一版摘要 + 填好的海运模板。旧消息不撤。
TMS 和人工报价分段写,无价也发表。
persist_attachment:正式有价出单传 True,登记后台附件;激活/补字段重发保持 False。
"""
ticket = hydrate_ticket(flow, ticket)
view = ticket_view(ticket)
@@ -638,6 +651,7 @@ def send_brief_and_sheet(
sess=sess,
quote_override=quote,
business_line=line,
persist_attachment=persist_attachment,
)
return "brief_and_sheet"
@@ -672,11 +686,14 @@ def send_filled_sheet(
quote_override: Optional[dict[str, Any]] = None,
business_line: str = "",
chat_id: str = "",
persist_attachment: bool = False,
) -> dict[str, Any]:
"""
只发填好的模板文件,不再发文字摘要。
business_line 决定匹配哪套后台模板;空运必须传 AIR,禁止落到海运默认模板。
persist_attachment:True 时主账登记 inq_attachment(正式有价出单);
激活空白模版保持 False,避免后台附件列堆满空白表。
"""
incoming_view = ticket_view(ticket) if ticket is not None else {}
incoming_facts = dict(incoming_view.get("facts") or {})
@@ -720,6 +737,7 @@ def send_filled_sheet(
or ""
),
business_line=line or "SEA",
persist_attachment=persist_attachment,
)
@@ -795,17 +813,21 @@ def _render_and_send_sheet(
quote: dict[str, Any],
sales_id: str,
business_line: str = "SEA",
persist_attachment: bool = False,
) -> dict[str, Any]:
"""
按业务线匹配后台模板、填表、发群文件。
空运必须 business_line=AIR,否则会套上海运默认模板。
persist_attachment:正式有价出单 True(写 inq_attachment);
激活空白模版 False(只发企微,不堆后台附件列)。
"""
renderer = getattr(flow.ledger, "render_quote", None)
client = group_client_of(flow)
if not callable(renderer) or not chat_id:
return {"ok": False, "error": "no_renderer_or_chat"}
line = (business_line or "SEA").strip() or "SEA"
# 激活空白模版:persist=False;产品/分段有价出单:persist=True 写后台附件。
result = renderer(
work_order_no=work_order_no,
business_line=line,
@@ -813,6 +835,7 @@ def _render_and_send_sheet(
quote=quote,
format="xlsx",
sales_wecom_id=sales_id,
persist_attachment=bool(persist_attachment),
) or {}
if not result.get("ok"):
logger.warning("群填表失败 wo=%s line=%s err=%s", work_order_no, line, result.get("error"))
@@ -955,6 +978,7 @@ def apply_product_quote(
quote = merge_quote_fees(base, quote)
_save_product_quote(flow, no, quote, sess)
ticket2 = _reload(flow, chat_id, no) or ticket
# 正式有价出单:Excel 进群的同时登记后台附件,销售在工单列表能看到。
send_brief_and_sheet(
flow=flow,
ticket=ticket2,
@@ -962,6 +986,7 @@ def apply_product_quote(
quote_override=quote,
tms_quote={},
product_quote=quote,
persist_attachment=True,
)
emit_group_deal(flow=flow, ticket=ticket2, sess=sess, quote=quote)
return "product_quote_sent"
@@ -1005,6 +1030,7 @@ def apply_tms_quote_choice(
_clear_pending_quote(no, sess, flow)
_save_product_quote(flow, no, quote, sess)
ticket2 = _reload(flow, chat_id, no) or ticket
# 正式有价出单:登记后台附件(与产品文字报价同一口径)。
send_brief_and_sheet(
flow=flow,
ticket=ticket2,
@@ -1012,6 +1038,7 @@ def apply_tms_quote_choice(
quote_override=quote,
tms_quote=base if keep_tms else {},
product_quote=overlay if keep_tms else quote,
persist_attachment=True,
)
emit_group_deal(flow=flow, ticket=ticket2, sess=sess, quote=quote)
return "product_quote_sent"
@@ -1104,6 +1131,7 @@ def _persist_quote_sheet(
or "SEA"
).strip() or "SEA"
if callable(renderer) and no:
# 确认落库:需要把填好的报价单登记到后台附件。
result = renderer(
work_order_no=no,
business_line=line,
@@ -1121,10 +1149,15 @@ def _persist_quote_sheet(
or view.get("salesWecomId")
or ""
),
persist_attachment=True,
) or {}
if result.get("ok"):
filename = filename_from_render(no, result)
object_key = str(result.get("objectKey") or filename)
object_key = str(result.get("objectKey") or "").strip()
# 主账 render 已写 inq_attachment 时不再重复挂一条「只有文件名」的空壳。
if object_key or result.get("attachmentSaved"):
return
object_key = filename
_archive_file(flow, no, filename, {"media_id": object_key}, sender_id)
@@ -1168,7 +1201,11 @@ def handle_group_file(
def dissolve_group(*, flow: Any, ticket: object, sess: Any = None) -> str:
"""解散应用群,这单协同结束,不改六态。"""
"""
解散应用群,这单协同结束,不改六态。
先发「群已解散」再移人:移完人销售可能已不在群,收不到确认。
"""
view = ticket_view(ticket)
no = ticket_no(ticket)
chat_id = str((sess.collab_chat_id if sess else "") or view.get("collab_chat_id") or "")
@@ -1176,6 +1213,8 @@ def dissolve_group(*, flow: Any, ticket: object, sess: Any = None) -> str:
members = list(view.get("collab_member_ids") or view.get("memberIds") or [])
if sess is not None:
members = list(sess.invited_userids or members)
# 先回话再移人,避免销售被移出后看不到确认。
send_group_text(client, chat_id, copy.group_dissolved(work_order_no=no))
dismiss = getattr(client, "dismiss_group", None)
if callable(dismiss) and chat_id:
dismiss(chat_id=chat_id, userids=members)
@@ -1192,7 +1231,6 @@ def dissolve_group(*, flow: Any, ticket: object, sess: Any = None) -> str:
if sess is not None:
sess.collab_ended = True
flow._save(sess)
send_group_text(client, chat_id, copy.group_dissolved(work_order_no=no))
return "group_dissolved"
@@ -1263,6 +1301,18 @@ def apply_group_deal(
view = ticket_view(ticket)
chat_id = str((sess.collab_chat_id if sess else "") or view.get("collab_chat_id") or "")
client = group_client_of(flow)
line = str(
view.get("business_line")
or view.get("businessLine")
or (sess.business_line if sess else "")
or "SEA"
).strip().upper()
status_now = ticket_status(ticket) or (
str(getattr(sess, "status", "") or "").strip() if sess is not None else ""
)
# 同一终态再确认一次(BOT+存档双通道):只静默,不刷「不能修改工单信息」。
if status_now in {"已成交", "未成交"} and outcome == status_now:
return "deal_already_recorded"
stopped = halt_closed_group(flow=flow, ticket=ticket, sess=sess, chat_id=chat_id)
if stopped:
return stopped
@@ -1270,7 +1320,6 @@ def apply_group_deal(
send_group_text(client, chat_id, copy.group_deal_sales_only())
return "not_sales"
quote = _current_quote(view, sess)
status_now = ticket_status(ticket) or (sess.status if sess else "")
usable_total = str(quote.get("total") or "").strip()
if usable_total in {"", "-"}:
usable_total = ""
@@ -1286,7 +1335,7 @@ def apply_group_deal(
outcome=outcome,
facts=facts_of_group(view, sess),
quote=quote,
business_line=str(view.get("business_line") or (sess.business_line if sess else "SEA") or "SEA"),
business_line=line or "SEA",
sales_name=str(view.get("sales_name") or (getattr(sess, "invited_sales_name", "") if sess else "") or ""),
lost_reason=inline,
)
@@ -1303,17 +1352,15 @@ def apply_group_deal(
sess.allowed = ()
flow._save(sess)
ack_reason = inline if phase == "done" and outcome == "未成交" else ""
send_group_text(
client,
chat_id,
copy.deal_ack(
outcome,
no,
str(result.get("next_followup_at") or ""),
lost_reason=ack_reason,
followup_hours=result.get("negotiate_hours"),
),
ack = copy.deal_ack(
outcome,
no,
str(result.get("next_followup_at") or ""),
lost_reason=ack_reason,
followup_hours=result.get("negotiate_hours"),
business_line=line,
)
send_group_text(client, chat_id, ack)
return phase