协商中再问按后台超时小时顺延并可强制清跟进时刻;工单与账号支持按筛选导出 Excel。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -48,6 +48,8 @@ _PENDING_LOCK = "__pending_air_lock"
|
||||
_PENDING_LOCK_OPTION = "__pending_lock_option"
|
||||
_PENDING_LOCK_AIRLINE = "__pending_lock_airline"
|
||||
_PENDING_LOCK_FLIGHT = "__pending_lock_flight"
|
||||
|
||||
|
||||
_LOCK_ID = "__air_lock_id"
|
||||
_LOCK_OPTION = "__air_lock_option"
|
||||
_LOCK_QUOTE = "__air_lock_quote"
|
||||
@@ -861,6 +863,7 @@ def apply_air_deal(
|
||||
no,
|
||||
str(result.get("next_followup_at") or ""),
|
||||
lost_reason=ack_reason,
|
||||
followup_hours=result.get("negotiate_hours"),
|
||||
),
|
||||
)
|
||||
return phase
|
||||
@@ -1518,11 +1521,13 @@ def _resolve_lock_params(
|
||||
air = _normalize_airline(airline or said_air or str(facts.get(_PENDING_LOCK_AIRLINE) or ""))
|
||||
flight = _normalize_flight(flight_no or said_flight or str(facts.get(_PENDING_LOCK_FLIGHT) or ""))
|
||||
chosen = _option_row(quote, option)
|
||||
single_auto = False
|
||||
if chosen is None and not option:
|
||||
options = _air_options(quote)
|
||||
if len(options) == 1:
|
||||
chosen = options[0]
|
||||
option = str(chosen.get("optionNo") or chosen.get("option_no") or "").strip()
|
||||
single_auto = True
|
||||
if not _airline_ready(air) and chosen is not None:
|
||||
air = _normalize_airline(
|
||||
str(chosen.get("airlineCode") or chosen.get("airline") or "")
|
||||
|
||||
@@ -2007,6 +2007,7 @@ class AirTextInquiryFlow:
|
||||
sess.work_order_no,
|
||||
str(result.get("next_followup_at") or ""),
|
||||
lost_reason=ack_reason,
|
||||
followup_hours=result.get("negotiate_hours"),
|
||||
)
|
||||
)
|
||||
return phase
|
||||
|
||||
@@ -88,7 +88,7 @@ def shift_workdays(start: date, count: int) -> date:
|
||||
"""
|
||||
从 start 当天之后数 count 个工作日。
|
||||
|
||||
成交选协商中:顺延 3 个工作日后,第四个工作日再问,故 count=4。
|
||||
仍给单测与节假日校验用;协商再问已改走 add_workday_hours。
|
||||
"""
|
||||
if count < 1:
|
||||
return start
|
||||
@@ -101,11 +101,51 @@ def shift_workdays(start: date, count: int) -> date:
|
||||
return cur
|
||||
|
||||
|
||||
def next_followup_at(now: datetime | None = None) -> datetime:
|
||||
"""第四个工作日 Asia/Shanghai 09:00。"""
|
||||
# 协商中再问:主账 timeoutCode;缺配置时默认 48 工作小时。
|
||||
TIMEOUT_CODE_DEAL_NEGOTIATE = "deal_negotiate"
|
||||
DEFAULT_NEGOTIATE_HOURS = 48.0
|
||||
|
||||
|
||||
def add_workday_hours(start: datetime, hours: float) -> datetime:
|
||||
"""
|
||||
从 start 起累加 hours 个工作小时;非工作日整天跳过。
|
||||
|
||||
与 workday_elapsed_hours 互逆:工作日当天 0–24 都算。
|
||||
起点落在休息日:从下一个工作日 00:00 起算。
|
||||
hours≤0 时原样返回(调用方应不排期)。
|
||||
"""
|
||||
base = start.astimezone(SHANGHAI) if start.tzinfo else start.replace(tzinfo=SHANGHAI)
|
||||
remaining = float(hours)
|
||||
if remaining <= 0:
|
||||
return base
|
||||
cur = base
|
||||
if not is_workday(cur.date()):
|
||||
cur = datetime.combine(cur.date() + timedelta(days=1), time(0, 0), tzinfo=SHANGHAI)
|
||||
while not is_workday(cur.date()):
|
||||
cur = datetime.combine(cur.date() + timedelta(days=1), time(0, 0), tzinfo=SHANGHAI)
|
||||
while remaining > 0:
|
||||
if not is_workday(cur.date()):
|
||||
cur = datetime.combine(cur.date() + timedelta(days=1), time(0, 0), tzinfo=SHANGHAI)
|
||||
continue
|
||||
day_end = datetime.combine(cur.date() + timedelta(days=1), time(0, 0), tzinfo=SHANGHAI)
|
||||
room = (day_end - cur).total_seconds() / 3600.0
|
||||
if remaining <= room + 1e-9:
|
||||
return cur + timedelta(hours=remaining)
|
||||
remaining -= room
|
||||
cur = day_end
|
||||
return cur
|
||||
|
||||
|
||||
def next_followup_at(now: datetime | None = None, *, hours: float | None = None) -> datetime:
|
||||
"""
|
||||
距今 hours 个工作小时后的再问时刻(默认 48)。
|
||||
|
||||
非工作日不计入;提醒时刻不再固定 09:00。
|
||||
hours≤0 时返回起点(调用方应把 0 当作不排再问)。
|
||||
"""
|
||||
base = now.astimezone(SHANGHAI) if now else datetime.now(SHANGHAI)
|
||||
day = shift_workdays(base.date(), 4)
|
||||
return datetime.combine(day, time(9, 0), tzinfo=SHANGHAI)
|
||||
h = DEFAULT_NEGOTIATE_HOURS if hours is None else float(hours)
|
||||
return add_workday_hours(base, h)
|
||||
|
||||
|
||||
def workday_elapsed_hours(start: datetime, now: datetime) -> float:
|
||||
|
||||
@@ -13,7 +13,12 @@ from typing import Any, Callable, Optional
|
||||
|
||||
from agent.config import get_settings
|
||||
from agent.policy import inquiry_copy as copy
|
||||
from agent.policy.deal_followup import next_followup_at, pick_leaders
|
||||
from agent.policy.deal_followup import (
|
||||
DEFAULT_NEGOTIATE_HOURS,
|
||||
TIMEOUT_CODE_DEAL_NEGOTIATE,
|
||||
next_followup_at,
|
||||
pick_leaders,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -156,8 +161,14 @@ def finish_deal_outcome(
|
||||
status = (outcome or "").strip()
|
||||
extra: dict[str, Any] = {}
|
||||
summary = ""
|
||||
negotiate_hours = 0.0
|
||||
if status == "协商中":
|
||||
extra["next_followup_at"] = next_followup_at().strftime("%Y-%m-%d %H:%M:%S")
|
||||
# 小时读后台超时响应 deal_negotiate;0=不排再问;缺配置默认 48
|
||||
negotiate_hours = _negotiate_followup_hours(ledger)
|
||||
if negotiate_hours > 0:
|
||||
extra["next_followup_at"] = next_followup_at(hours=negotiate_hours).strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
if status == "已成交":
|
||||
staff: list[dict[str, Any]] = []
|
||||
lister = getattr(ledger, "list_staff_by_role", None)
|
||||
@@ -242,9 +253,38 @@ def finish_deal_outcome(
|
||||
"notify_summary": summary,
|
||||
"status": status,
|
||||
"next_followup_at": str(extra.get("next_followup_at") or ""),
|
||||
"negotiate_hours": negotiate_hours,
|
||||
}
|
||||
|
||||
|
||||
def _negotiate_followup_hours(ledger: Any) -> float:
|
||||
"""
|
||||
读主账 deal_negotiate 超时小时;找不到或异常则用默认 48。
|
||||
|
||||
允许小数;负数当 0(不排再问)。不在回调线程打主账以外的慢活。
|
||||
"""
|
||||
lookup = getattr(ledger, "get_timeout_by_code", None)
|
||||
if not callable(lookup):
|
||||
return DEFAULT_NEGOTIATE_HOURS
|
||||
try:
|
||||
row = lookup(timeout_code=TIMEOUT_CODE_DEAL_NEGOTIATE) or {}
|
||||
except Exception:
|
||||
logger.exception("读协商再问小时失败,用默认 %s", DEFAULT_NEGOTIATE_HOURS)
|
||||
return DEFAULT_NEGOTIATE_HOURS
|
||||
if not row.get("found"):
|
||||
return DEFAULT_NEGOTIATE_HOURS
|
||||
raw = row.get("timeoutHours")
|
||||
if raw is None:
|
||||
raw = row.get("timeout_hours")
|
||||
try:
|
||||
hours = float(raw)
|
||||
except (TypeError, ValueError):
|
||||
return DEFAULT_NEGOTIATE_HOURS
|
||||
if hours < 0:
|
||||
return 0.0
|
||||
return hours
|
||||
|
||||
|
||||
def _transition_deal_status(
|
||||
writer: Callable[..., dict[str, Any]],
|
||||
*,
|
||||
|
||||
@@ -2775,6 +2775,7 @@ def deal_ack(
|
||||
work_order_no: str,
|
||||
next_followup_at: str = "",
|
||||
lost_reason: str = "",
|
||||
followup_hours: float | None = None,
|
||||
) -> str:
|
||||
"""成交三态回执。后台 deal.recorded_won / lost.ask_reason / negotiating.recorded。"""
|
||||
no = (work_order_no or "").strip()
|
||||
@@ -2789,26 +2790,42 @@ def deal_ack(
|
||||
if reason:
|
||||
return f"已记录为「未成交」。{lost_reason_ack(reason, work_order_no=no)}"
|
||||
return _r("lost.ask_reason", "已记录为「未成交」。请补充未成交原因。", ticket_no=no)
|
||||
when = _followup_month_day(next_followup_at) or "第四个工作日"
|
||||
from agent.policy.event_exception import format_timeout_hours
|
||||
|
||||
# 小时缺省按后台默认 48;0 表示运营关掉再问,话术仍说明已记录协商中
|
||||
hours_val = 48.0 if followup_hours is None else float(followup_hours)
|
||||
hours_text = format_timeout_hours(hours_val if hours_val > 0 else 48.0)
|
||||
when = _followup_when_text(next_followup_at) or f"{hours_text} 个工作小时后"
|
||||
return _r(
|
||||
"negotiating.recorded",
|
||||
f"已记录为「协商中」。已顺延三个工作日,{when}的9:00再次发起成交状态询问。",
|
||||
f"已记录为「协商中」。已顺延 {hours_text} 个工作小时,将于 {when} 再次发起成交状态询问。",
|
||||
followup_when=when,
|
||||
followup_hours=hours_text,
|
||||
ticket_no=no,
|
||||
)
|
||||
|
||||
|
||||
def _followup_month_day(raw: str) -> str:
|
||||
"""把主账再问时刻收成「9月24日」。"""
|
||||
"""把主账再问时刻收成「9月24日」(旧模板兼容)。"""
|
||||
when = _followup_when_text(raw)
|
||||
if "日" in when:
|
||||
return when.split("日", 1)[0] + "日"
|
||||
return when
|
||||
|
||||
|
||||
def _followup_when_text(raw: str) -> str:
|
||||
"""把主账再问时刻收成「9月24日15:30」。"""
|
||||
from datetime import datetime
|
||||
|
||||
text = (raw or "").strip()
|
||||
if not text:
|
||||
return ""
|
||||
for fmt, n in (("%Y-%m-%d %H:%M:%S", 19), ("%Y-%m-%d", 10)):
|
||||
for fmt, n in (("%Y-%m-%d %H:%M:%S", 19), ("%Y-%m-%d %H:%M", 16), ("%Y-%m-%d", 10)):
|
||||
try:
|
||||
dt = datetime.strptime(text[:n], fmt)
|
||||
return f"{dt.month}月{dt.day}日"
|
||||
if n <= 10:
|
||||
return f"{dt.month}月{dt.day}日"
|
||||
return f"{dt.month}月{dt.day}日{dt.hour}:{dt.minute:02d}"
|
||||
except ValueError:
|
||||
continue
|
||||
return ""
|
||||
|
||||
@@ -1305,7 +1305,13 @@ def apply_group_deal(
|
||||
send_group_text(
|
||||
client,
|
||||
chat_id,
|
||||
copy.deal_ack(outcome, no, str(result.get("next_followup_at") or ""), lost_reason=ack_reason),
|
||||
copy.deal_ack(
|
||||
outcome,
|
||||
no,
|
||||
str(result.get("next_followup_at") or ""),
|
||||
lost_reason=ack_reason,
|
||||
followup_hours=result.get("negotiate_hours"),
|
||||
),
|
||||
)
|
||||
return phase
|
||||
|
||||
|
||||
Reference in New Issue
Block a user