""" 协商中到期再问:Worker HTTP 槽轮询,回到当初那个会话。 本文件职责:认领主账到期单、发成交跟进。不在回调线程跑。 禁止:群解散后改发私聊;占 LibreOffice 槽;把六态写进 Redis。 """ from __future__ import annotations import logging from typing import Any, Optional from agent.config import Settings, get_settings from agent.policy import inquiry_copy as copy logger = logging.getLogger(__name__) def poll_due_negotiate(*, ledger: Any = None, settings: Optional[Settings] = None) -> int: """ 认领到期协商中工单并再问一次。 返回发出条数。主账已清空 next_followup_at,本轮失败也不再自动补发。 """ cfg = settings or get_settings() book = ledger if book is None: if str(getattr(cfg, "ledger_backend", "http") or "http").lower() == "memory": return 0 from agent.ledger.http_ledger import HttpLedger book = HttpLedger() claimer = getattr(book, "claim_due_negotiate", None) if not callable(claimer): return 0 rows = claimer() or [] sent = 0 for row in rows: if not isinstance(row, dict): continue try: if _reask_one(row, settings=cfg, ledger=book): sent += 1 except Exception: logger.exception("协商再问失败 wo=%s", row.get("work_order_no") or row.get("workOrderNo")) return sent def _reask_one(row: dict[str, Any], *, settings: Settings, ledger: Any = None) -> bool: no = str(row.get("work_order_no") or row.get("workOrderNo") or "").strip() if not no: return False chat_id = str(row.get("collab_chat_id") or row.get("collabChatId") or "").strip() sales = str(row.get("sales_wecom_id") or row.get("salesWecomId") or "").strip() name = str(row.get("sales_name") or row.get("salesName") or "").strip() or "销售" facts = dict(row.get("facts") or {}) quote = dict(row.get("quote") or {}) line = str(row.get("business_line") or row.get("businessLine") or "").strip().upper() if chat_id: ok = _send_group_reask( chat_id=chat_id, work_order_no=no, sales_name=name, facts=facts, quote=quote, business_line=line, settings=settings, ) if not ok: logger.warning("协商再问原群失败且不补发私聊 wo=%s chat=%s", no, chat_id) return False _start_deal_wait(ledger, no) return True if not sales: logger.warning("协商再问无销售 userid wo=%s", no) return False from agent.channel.queue import get_message_store text = copy.deal_card(work_order_no=no, quote=quote, facts=facts, business_line=line) payload = copy.deal_wecom_payload(work_order_no=no, quote=quote, facts=facts, business_line=line) ok, info = get_message_store().enqueue_outbound( touser=sales, content=text, dedupe_key=f"deal-reask:{no}", payload=payload, ) if not ok: logger.warning("协商再问私聊入队失败 wo=%s info=%s", no, info) return False _start_deal_wait(ledger, no) return True def _start_deal_wait(ledger: Any, work_order_no: str) -> None: """再问发出后重新盯成交跟进。ledger 可空。""" from agent.policy.event_exception import WAIT_DEAL, start_wait start_wait(ledger, work_order_no=work_order_no, wait_kind=WAIT_DEAL) def _send_group_reask( *, chat_id: str, work_order_no: str, sales_name: str, facts: dict[str, str], quote: dict[str, Any], business_line: str, settings: Settings, ) -> bool: text = copy.group_deal_followup(sales_name=sales_name, work_order_no=work_order_no) if business_line == "AIR": from agent.channel.aibot.reply import AibotReplyClient out = AibotReplyClient().send_group(chat_id=chat_id, content=text) return bool(out.get("ok")) from agent.channel.outbox.sender import WeComAppClient client = WeComAppClient( corp_id=settings.wecom_corp_id, secret=settings.wecom_secret, agent_id=int(settings.wecom_agent_id or "0") or 0, ) out = client.send_group(chat_id=chat_id, content=text) if not out.get("ok"): return False carder = getattr(client, "send_group_card", None) if callable(carder): payload = copy.deal_wecom_payload( work_order_no=work_order_no, quote=quote, facts=facts, business_line=business_line ) carder(chat_id=chat_id, template_card=payload.get("template_card") or {}) return True