Files
kefu/deploy/review-visibility-20260916/visual_handoff_methods.txt
T
2026-09-21 10:34:06 +08:00

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