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