机器人显示名与站点标题可配;多段联运列表投影;协同群监听对账;工单附件落存;模板与工单后台补强
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user