from pathlib import Path from shutil import copy2 import ast roots=[Path(r'C:\kefu\wechat_rpa'),Path(r'C:\wechat_rpa')] helpers=''' def _confirmed_open_session_exclusion(self, fp: bytes, name: str = "") -> str: """Confirm excluded headers twice against the actual selected identity.""" from reply_session_policy import excluded_session_reason, visual_group_title_reason try: title = self._open_chat_display_name() reason = excluded_session_reason(title) or visual_group_title_reason(title, name=name) if not reason: # Extra list OCR is only needed for a possible member suffix. if name or not re.search(r"[((]\\d+[^))]*[))]$", str(title or "")): return "" image = self.capture_session_list() row = self.detect_selected_row(image) if row < 0 or not self._session_fp_matches(self._session_fingerprint(image, row, row_center=True), fp): return "" name = self._row_display_name(image, row, row_center=True) reason = visual_group_title_reason(title, name=name) if not reason: return "" def proven(identity): if not identity or not self._message_workspace_selected(self._capture_full_window()): return False current = self._raw_selected_session_fingerprint() or self._selected_session_fingerprint() if current: return self._chat_target_matches(fp, identity, current_fp=current, current_identity=self._chat_identity_signature(), selected_fp_checked=True) # An opened row can disappear from the unread filter, but a # separately observed list name must still prove this target. return bool(name and self._fp_from_name(name) == fp and self._chat_surface_signature()) identity = self._chat_identity_signature() if not proven(identity): return "" time.sleep(0.06) second_title = self._open_chat_display_name() second_identity = self._chat_identity_signature() if (second_identity != identity or not proven(second_identity) or session_name.normalize_name(second_title) != session_name.normalize_name(title)): return "" second_reason = excluded_session_reason(second_title) or visual_group_title_reason(second_title, name=name) return reason if second_reason == reason else "" except Exception: return "" # Missing OCR or geometry cannot prove a group. def _skip_excluded_reply_session(self, fp: bytes, *, inspect_open=False, display_name="", metadata=None) -> bool: """Skip proven excluded sessions, retaining possibly-sent evidence.""" from contextlib import nullcontext from reply_session_policy import excluded_session_reason raw = bytes(fp or b"") if not raw: return False key = raw.hex() state = (getattr(self, "_pending_reply_sessions", {}) or {}).get(key) or {} name = str(display_name or state.get("display_name") or "") event = state.get("database_event") if isinstance(state.get("database_event"), dict) else {} supplied = metadata if isinstance(metadata, dict) else {} reason = excluded_session_reason(name, metadata=supplied) or excluded_session_reason(name, metadata=state) cache = getattr(self, "_excluded_session_cache", None) if cache is None: cache = self._excluded_session_cache = {} cached = cache.get(key) or {} conv_id = str(supplied.get("conv_id") or event.get("conv_id") or state.get("conv_id") or "") personal = (conv_id.startswith("M:") and len(conv_id) > 2 or re.fullmatch(r"S:\\d+_\\d+", conv_id) is not None) if not reason and not personal and time.monotonic() < float(cached.get("until") or 0): reason = str(cached.get("reason") or "") if not reason and inspect_open and not personal: reason = self._confirmed_open_session_exclusion(raw, name) if reason: cache[key] = {"reason": reason, "until": time.monotonic() + 300} if len(cache) > 256: for stale in sorted(cache, key=lambda item: cache[item].get("until", 0))[:-256]: cache.pop(stale, None) if not reason: return False self._last_session_exclusion_reason = reason lock = getattr(self, "_pending_lock", None) with lock if lock is not None else nullcontext(): current = (getattr(self, "_pending_reply_sessions", {}) or {}).get(key) if isinstance(current, dict): protected = (str(current.get("stage") or "") in {"sending", "receipt_check", "uncertain", "unknown"} or str(current.get("send_state") or "") in {"sending", "uncertain", "unknown", "sent_uncommitted"} or bool(current.get("send_dispatch_started"))) if not protected: self._clear_reply_pending(raw) logged = getattr(self, "_excluded_session_logged", None) if logged is None: logged = self._excluded_session_logged = set() marker = (key, reason) if marker not in logged: if len(logged) >= 256: logged.clear() logged.add(marker) try: self.queue_log.append(self.session_id_of(raw), name or "已排除会话", "跳过", reason) except Exception: pass self.report_progress(reason) print(f" [会话过滤] {reason}") return True ''' def editfn(s,name,old,new): node=next(n for n in ast.walk(ast.parse(s)) if isinstance(n,ast.FunctionDef) and n.name==name) lines=s.splitlines(True);seg=''.join(lines[node.lineno-1:node.end_lineno]) assert old in seg,(name,old) return ''.join(lines[:node.lineno-1])+seg.replace(old,new,1)+''.join(lines[node.end_lineno:]) for root in roots: p=root/'wechat_bot.py';backup=root/'backups/skip-sessions-20260917';backup.mkdir(parents=True,exist_ok=True) if not (backup/p.name).exists():copy2(p,backup/p.name) s=p.read_text(encoding='utf-8-sig') assert ' def _skip_excluded_reply_session(' not in s s=s.replace(' "企业微信团队", "微信团队", "离职继承",',' "企业微信团队", "微信团队", "离职继承",\n "企小码", "企小码会话", "企小码会话管理", "企小碼", "企小碼會話管理",',1) s=s.replace(' def _purge_system_pending_tasks(self) -> int:\n',helpers+' def _purge_system_pending_tasks(self) -> int:\n',1) s=editfn(s,'_purge_system_pending_tasks',''' if self._is_system_entry_name(name): pending.pop(key, None)''',''' protected = (str(state.get("stage") or "") in {"sending", "receipt_check", "uncertain", "unknown"} or str(state.get("send_state") or "") in {"sending", "uncertain", "unknown", "sent_uncommitted"} or bool(state.get("send_dispatch_started"))) if self._is_system_entry_name(name) and not protected: pending.pop(key, None)''') s=editfn(s,'enqueue_detected',' now = time.time()',''' if self._skip_excluded_reply_session(fp, display_name=display_name, metadata=database_event): return False, str(getattr(self, "_last_session_exclusion_reason", "会话不参与自动回复")) now = time.time()''') s=editfn(s,'_mark_reply_pending',' lock = getattr(self, "_pending_lock", None)',''' if self._skip_excluded_reply_session(fp, display_name=display_name): return False lock = getattr(self, "_pending_lock", None)''') s=editfn(s,'_session_accepts_replies',' key = bytes(fp or b"").hex()',''' if self._skip_excluded_reply_session(fp, inspect_open=True): return False key = bytes(fp or b"").hex()''') s=editfn(s,'click_session',' self._last_click_failure_reason = "unknown"',''' self._last_click_failure_reason = "unknown" if expected_fp and self._skip_excluded_reply_session(expected_fp): self._last_click_failure_reason = "excluded_session" return False''') s=editfn(s,'click_session',''' if opened_matches: self._last_click_failure_reason = ""''',''' if opened_matches: if self._skip_excluded_reply_session(expected_fp or opened_fp, inspect_open=True): self._last_click_failure_reason = "excluded_session" return False self._last_click_failure_reason = ""''') s=editfn(s,'_target_from_session_image',' if fp in processed_fp or fp in non_conv_fp:',''' if self._skip_excluded_reply_session(fp, display_name=row_name): non_conv_fp.add(fp) continue if fp in processed_fp or fp in non_conv_fp:''') s=editfn(s,'_target_from_filtered_unread_image',' if self._flat_session_rejected(fp):',''' if self._skip_excluded_reply_session(fp, display_name=row_name): non_conv_fp.add(fp) continue if self._flat_session_rejected(fp):''') s=editfn(s,'_resume_orphaned_pending_reply',' if bool(state.get("awaiting_review", False)):',''' if self._skip_excluded_reply_session(target_fp): continue if bool(state.get("awaiting_review", False)):''') s=editfn(s,'_check_selected_session',' pending_state = self._pending_reply_state(fp)',''' if self._skip_excluded_reply_session(fp, inspect_open=True): return False pending_state = self._pending_reply_state(fp)''') s=editfn(s,'_generate_ai_reply',''' self._consume_database_update(fp) initial_state''',''' if self._skip_excluded_reply_session(fp, inspect_open=True): self._generation_logged = True return None self._consume_database_update(fp) initial_state''') s=editfn(s,'_send_reply_inner',' self._last_send_failure_reason = ""',''' self._last_send_failure_reason = "" exclusion_fp = expected_fp or getattr(self, "_active_session_fp", None) if exclusion_fp and self._skip_excluded_reply_session(exclusion_fp, inspect_open=True): return self._deny_send(str(getattr(self, "_last_session_exclusion_reason", "会话不参与自动回复")))''') s=editfn(s,'_send_reply_inner',' blanket_review = self._review_mode_enabled()',''' if self._skip_excluded_reply_session(expected_fp, inspect_open=True): return self._deny_send(str(getattr(self, "_last_session_exclusion_reason", "会话不参与自动回复"))) blanket_review = self._review_mode_enabled()''') ast.parse(s);p.write_text(s,encoding='utf-8',newline='\n') print(str(root)+': visual exclusion applied')