from pathlib import Path import shutil protocol = Path('C:/wechat_rpa/protocol_engine.py') text = protocol.read_text(encoding='utf-8') old = " def cancel_pending_replies(self,keys):return self._control(keys,'delete')" assert text.count(old) == 1 text = text.replace(old, ''' def handoff_pending_replies(self, keys): """Explicitly retire selected work to a human without claiming delivery. Uncertain receipts remain in the send journal and the inbound dedup key stays seen. No send/retry is scheduled by this action. """ result={'handed_off':[], 'deleted':[], 'missing':[], 'protected':[], 'scheduled':[]} removed=[] with self._lock: for key in dict.fromkeys(str(k).strip() for k in keys if str(k).strip()): state=self._pending.get(key) if state is None:result['missing'].append(key);continue if state.get('account')!=self.account or state.get('send_state')=='sending': result['protected'].append(key);continue if state.get('dedup_key'):self._seen[key]=state['dedup_key'] self._pending.pop(key,None) result['handed_off'].append(key);result['deleted'].append(key) removed.append((key,state)) self.needs_attention=any(s.get('account')==self.account and s.get('send_state')=='uncertain' for s in self._pending.values()) self._save() self._reply_wakeup.set() for key,state in removed: self.log(f"[协议转人工] {state.get('display_name','')} · 自动队列已移交人工;发送结果未作确认,不自动重发") try: from queue_log import QueueLog QueueLog().append(key,state.get('display_name',''),'转人工', '用户已确认人工接管;保留发送流水和去重记录,不自动重发') except Exception:pass self._listening_progress() return result ''' + old) compile(text, str(protocol), 'exec') protocol.write_text(text, encoding='utf-8') for root in (Path('C:/wechat_rpa'), Path('C:/kefu/wechat_rpa')): path = root / 'gui_runtime.py' text = path.read_text(encoding='utf-8') def replace(old, new, count=1): global text assert text.count(old)==count, (str(path), old[:60], text.count(old), count) text=text.replace(old,new) replace('def delete_pending_reply_file(keys, path=""):', 'def delete_pending_reply_file(keys, path="", *, handoff=False):') replace(' removed = []\n for key in sorted(requested):\n state = pending.pop(key, None)\n if isinstance(state, dict):\n removed.append((key, state))', ''' removed = [] protocol = raw.get("transport") == "protocol" if handoff and protocol and not isinstance(raw.get("seen", {}), dict): result["error"] = "协议去重记录格式不正确,无法安全转交人工" return result for key in sorted(requested): state = pending.get(key) if not isinstance(state, dict): continue if handoff and str(state.get("send_state") or "") == "sending": result["protected"].append(key) continue if handoff and protocol and state.get("dedup_key"): raw.setdefault("seen", {})[key] = state["dedup_key"] pending.pop(key, None) removed.append((key, state))''') replace(' if not removed:\n return result\n\n directory =', ' if not removed:\n result["missing"] = sorted(requested.difference(result["protected"]))\n return result\n\n directory =') replace(' result["missing"] = sorted(requested.difference(result["deleted"]))', ' result["missing"] = sorted(requested.difference(result["deleted"]).difference(result["protected"]))') replace(' "手动删除",\n "用户从回复队列中手动删除;本条未完成回复不再自动发送",', ''' "转人工" if handoff else "手动删除", ("用户已确认人工接管;发送结果未作确认,保留发送流水和去重记录,不自动重发" if handoff else "用户从回复队列中手动删除;本条未完成回复不再自动发送"),''') replace('\n\nclass LogQueue:', ''' def handoff_pending_reply_file(keys, path=""): """Retire stopped work after explicit human takeover; preserve send evidence.""" result = delete_pending_reply_file(keys, path, handoff=True) result["handed_off"] = list(result.get("deleted") or []) return result class LogQueue:''') replace(' self._queued_cancellations = set()', ' self._queued_cancellations = set()\n self._queued_handoffs = set()') replace(' def retry_pending_tasks(self, keys):', ''' @staticmethod def _handoff_bot_tasks(bot, keys): handoff = getattr(bot, "handoff_pending_replies", None) if callable(handoff): return handoff(keys) result = bot.cancel_pending_replies(keys) result["handed_off"] = list(result.get("deleted") or []) return result def handoff_pending_tasks(self, keys): """Keep explicit takeover separate from ordinary cancellation at startup.""" requested = {str(key or "").strip() for key in (keys or [])} requested.discard("") if not requested: return {"handed_off": [], "deleted": [], "missing": [], "protected": [], "scheduled": []} with self._cancel_lock: bot = self.bot if bot is None: self._queued_handoffs.update(requested) return {"handed_off": [], "deleted": [], "missing": [], "protected": [], "scheduled": sorted(requested)} return self._handoff_bot_tasks(bot, requested) def _drain_queued_handoffs(self, bot): with self._cancel_lock: requested = set(self._queued_handoffs) self._queued_handoffs.clear() if requested: self._handoff_bot_tasks(bot, requested) def retry_pending_tasks(self, keys):''') drains = text.count(' self._drain_queued_cancellations(bot)') replace(' self._drain_queued_cancellations(bot)', ' self._drain_queued_handoffs(bot)\n self._drain_queued_cancellations(bot)', drains) compile(text, str(path), 'exec') backup = root / 'backups/review-visibility-20260916' backup.mkdir(parents=True, exist_ok=True) assert not (backup / path.name).exists(), 'Backup already exists; do not rerun' shutil.copy2(path, backup / path.name) path.write_text(text, encoding='utf-8') shutil.copy2(Path(__file__).with_name('test_review_handoff.py'), root / 'test_review_handoff.py') print('Updated explicit handoff: protocol engine and both runtime modules.')