178 lines
8.4 KiB
Plaintext
178 lines
8.4 KiB
Plaintext
def _manual_handoff_key(self, fp: bytes) -> str:
|
|
key = bytes(fp or b"").hex()
|
|
for stored_key, entry in getattr(self, "_manual_handoffs", {}).items():
|
|
if not isinstance(entry, dict) or entry.get("resumed_at"):
|
|
continue
|
|
state = entry.get("state") or {}
|
|
if key == stored_key or key in (state.get("render_identities") or []):
|
|
return stored_key
|
|
return ""
|
|
|
|
def _notice_manual_handoff(self, fp: bytes) -> None:
|
|
key = self._manual_handoff_key(fp)
|
|
if not key:
|
|
return
|
|
notified = getattr(self, "_manual_handoff_notified", None)
|
|
if notified is None:
|
|
notified = self._manual_handoff_notified = set()
|
|
if key in notified:
|
|
return
|
|
notified.add(key)
|
|
state = (self._manual_handoffs[key].get("state") or {})
|
|
detail = "此会话已转人工,尚不能证明有新客户消息,请在企业微信继续处理;旧回复不会自动重发"
|
|
try:
|
|
self.queue_log.append(key, str(state.get("display_name") or "客户"), "人工接管", detail)
|
|
self.report_progress(detail)
|
|
except Exception:
|
|
pass
|
|
|
|
def _resume_handoff_for_new_message(self, fp: bytes, chat_text: str = "", *, database_event=None) -> bool:
|
|
"""Resume only on a newer DB message or appended customer transcript block."""
|
|
def resume_unlocked():
|
|
key = self._manual_handoff_key(fp)
|
|
if not key:
|
|
return True
|
|
entry = self._manual_handoffs[key]
|
|
state = entry.get("state") or {}
|
|
proved_new = False
|
|
old_event = state.get("database_event") or {}
|
|
event = database_event if isinstance(database_event, dict) else {}
|
|
if old_event and event:
|
|
try:
|
|
proved_new = bool(
|
|
old_event.get("message_id") and event.get("message_id")
|
|
and str(old_event["message_id"]) != str(event["message_id"])
|
|
and all(old_event.get(field) == event.get(field) and event.get(field)
|
|
for field in ("account", "conv_id"))
|
|
and float(event.get("send_time") or 0) > float(old_event.get("send_time") or 0)
|
|
)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
if not proved_new and chat_text:
|
|
baseline = self._stored_send_baseline(state) or self._normalized_copied_blocks(
|
|
"\n".join(str(line) for line in state.get("last_lines") or [])
|
|
or str(state.get("chat_text") or "")
|
|
)
|
|
current = self._normalized_copied_blocks(chat_text)
|
|
appended = self._blocks_after_baseline(baseline, current)
|
|
blocks = self._copied_message_blocks(chat_text)
|
|
customer = str(state.get("customer_speaker") or "").strip()
|
|
proved_new = bool(
|
|
customer and appended and blocks and len(blocks) == len(current)
|
|
and blocks[-1][0].strip() == customer and blocks[-1][1].strip()
|
|
and customer not in self._known_outgoing_speakers()
|
|
)
|
|
if not proved_new:
|
|
self._notice_manual_handoff(fp)
|
|
return False
|
|
original = dict(entry)
|
|
entry["resumed_at"] = time.time()
|
|
entry["resume_reason"] = "已证明后续客户新消息;原回复仍交人工核对"
|
|
if not self._persist_pending_replies_unlocked():
|
|
self._manual_handoffs[key] = original
|
|
return False
|
|
cancelled = getattr(self, "_cancelled_reply_sessions", set())
|
|
cancelled.discard(key)
|
|
cancelled.discard(bytes(fp).hex())
|
|
return True
|
|
|
|
lock = getattr(self, "_pending_lock", None)
|
|
if lock is None:
|
|
return resume_unlocked()
|
|
with lock:
|
|
return resume_unlocked()
|
|
|
|
def handoff_pending_replies(self, keys, confirm_uncertain=False) -> dict:
|
|
"""Transfer work without declaring delivery or dropping receipt evidence."""
|
|
requested = sorted({str(key or "").strip() for key in (keys or []) if str(key or "").strip()})
|
|
result = {field: [] for field in (
|
|
"handed_off", "deleted", "missing", "protected", "scheduled", "requires_confirmation"
|
|
)}
|
|
removed = []
|
|
|
|
def handoff_unlocked():
|
|
pending = getattr(self, "_pending_reply_sessions", {})
|
|
handoffs = getattr(self, "_manual_handoffs", None)
|
|
if handoffs is None:
|
|
handoffs = self._manual_handoffs = {}
|
|
cancelled = getattr(self, "_cancelled_reply_sessions", None)
|
|
if cancelled is None:
|
|
cancelled = self._cancelled_reply_sessions = set()
|
|
previous_handoffs = dict(handoffs)
|
|
previous_cancelled = set(cancelled)
|
|
for key in requested:
|
|
state = pending.get(key)
|
|
if not isinstance(state, dict):
|
|
result["missing"].append(key)
|
|
continue
|
|
stage = str(state.get("stage") or "")
|
|
send_state = str(state.get("send_state") or "")
|
|
if stage == "sending" or send_state == "sending":
|
|
result["protected"].append(key)
|
|
continue
|
|
uncertain = (send_state in {"uncertain", "sent_uncommitted", "unknown"}
|
|
or stage in {"receipt_check", "uncertain", "unknown"}
|
|
or bool(state.get("send_dispatched_at")))
|
|
if uncertain and confirm_uncertain is not True:
|
|
result["protected"].append(key)
|
|
result["requires_confirmation"].append(key)
|
|
continue
|
|
try:
|
|
def encode(value):
|
|
if isinstance(value, (bytes, bytearray, memoryview)):
|
|
return bytes(value).hex()
|
|
raise TypeError("不可序列化的发送证据")
|
|
snapshot = json.loads(json.dumps(state, ensure_ascii=False, default=encode))
|
|
except (TypeError, ValueError) as exc:
|
|
result["error"] = str(exc)
|
|
result["protected"].append(key)
|
|
continue
|
|
handoffs[key] = {
|
|
"state": snapshot, "handed_off_at": time.time(),
|
|
"receipt_checked_by_user": confirm_uncertain is True,
|
|
"delivery_confirmed": False,
|
|
}
|
|
removed.append((key, state))
|
|
cancelled.add(key)
|
|
pending.pop(key, None)
|
|
if not removed:
|
|
return True
|
|
if self._persist_pending_replies_unlocked():
|
|
return True
|
|
for key, state in removed:
|
|
pending[key] = state
|
|
handoffs.clear()
|
|
handoffs.update(previous_handoffs)
|
|
cancelled.clear()
|
|
cancelled.update(previous_cancelled)
|
|
result["error"] = "待回复队列写入失败,人工移交未生效"
|
|
return False
|
|
|
|
lock = getattr(self, "_pending_lock", None)
|
|
if lock is None:
|
|
persisted = handoff_unlocked()
|
|
else:
|
|
with lock:
|
|
persisted = handoff_unlocked()
|
|
if not persisted:
|
|
return result
|
|
for key, state in removed:
|
|
result["handed_off"].append(key)
|
|
result["deleted"].append(key)
|
|
getattr(self, "_pending_exchanges", {}).pop(key, None)
|
|
getattr(self, "_pending_scan_progress", {}).pop(key, None)
|
|
if bytes(getattr(self, "_active_session_fp", None) or b"").hex() == key:
|
|
signature = state.get("generation_surface_signature")
|
|
if isinstance(signature, (bytes, bytearray)) and signature:
|
|
self._active_chat_signature = bytes(signature)
|
|
self._active_hold_key = ""
|
|
self._active_hold_since = 0.0
|
|
self._active_hold_expired = False
|
|
try:
|
|
self.queue_log.append(key, str(state.get("display_name") or "客户"), "人工接管",
|
|
"本条已移出自动队列,发送证据保留,未标记送达;请在企业微信继续处理")
|
|
except Exception:
|
|
pass
|
|
return result
|
|
|