176 lines
6.1 KiB
Python
176 lines
6.1 KiB
Python
"""
|
||
协商中到期再问: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
|
||
# 进程/双 Worker 防重:同一工单同一天只发一轮(主账 next_followup 清不掉时曾连发 WO024)
|
||
if _already_sent_reask(no, settings):
|
||
logger.warning("协商再问去重跳过 wo=%s", 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
|
||
_mark_sent_reask(no, settings)
|
||
_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
|
||
_mark_sent_reask(no, settings)
|
||
_start_deal_wait(ledger, no)
|
||
return True
|
||
|
||
|
||
def _already_sent_reask(work_order_no: str, settings: Settings) -> bool:
|
||
"""
|
||
跨进程去重:Redis SET NX。
|
||
|
||
返回 True=今天已发过应跳过;False=抢到发送权。
|
||
主账 next_followup 若因 updateById 清不掉 null,没有这层会每分钟连发。
|
||
"""
|
||
from datetime import datetime
|
||
|
||
from agent.policy.deal_followup import SHANGHAI
|
||
from agent.redis_coord import get_redis_runtime
|
||
from agent.redis_coord.idempotency import try_acquire
|
||
|
||
day = datetime.now(SHANGHAI).strftime("%Y-%m-%d")
|
||
try:
|
||
runtime = get_redis_runtime(settings)
|
||
client = getattr(runtime, "client", None)
|
||
if client is None:
|
||
return False
|
||
first = try_acquire(
|
||
client,
|
||
scope="deal_reask",
|
||
idempotency_key=f"{work_order_no}:{day}",
|
||
ttl_seconds=86400,
|
||
)
|
||
return not first
|
||
except Exception:
|
||
logger.exception("协商再问 Redis 去重失败 wo=%s", work_order_no)
|
||
return False
|
||
|
||
|
||
def _mark_sent_reask(work_order_no: str, settings: Settings) -> None:
|
||
"""兼容旧调用点;去重键已在发送前 SET NX。"""
|
||
_ = (work_order_no, settings)
|
||
|
||
|
||
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
|