172 lines
11 KiB
Python
172 lines
11 KiB
Python
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')
|