import ast from pathlib import Path import shutil artifact = Path(__file__).parent roots = [Path(r'C:\wechat_rpa'), Path(r'C:\kefu\wechat_rpa')] def method(text, name): tree = ast.parse(text) cls = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name in ('WeChatBot', 'ProtocolBot')) node = next(n for n in cls.body if isinstance(n, ast.FunctionDef) and n.name == name) return node, ''.join(text.splitlines(keepends=True)[node.lineno - 1:node.end_lineno]) def replace_method(text, name, replacement): node, _ = method(text, name) lines = text.splitlines(keepends=True) lines[node.lineno - 1:node.end_lineno] = [replacement.rstrip() + '\n'] return ''.join(lines) for root in roots: backup = root / 'backups' / 'review-badges-20260917' backup.mkdir(parents=True, exist_ok=True) names = ['wechat_bot.py', 'review_policy.py'] if root == roots[0]: names.append('protocol_engine.py') for name in names: source, saved = root / name, backup / name if source.exists() and not saved.exists(): shutil.copy2(source, saved) shutil.copy2(artifact / 'review_policy.py', root / 'review_policy.py') path = root / 'wechat_bot.py' text = path.read_text(encoding='utf-8-sig') text = replace_method(text, '_reply_needs_review', ''' def _reply_needs_review( self, pending_state: dict | None, customer_text: str, reply_text: str ) -> tuple[bool, str]: """Re-evaluate enabled keywords and the verdict's effective judge mode.""" import ai_config from review_policy import selective_review_reason reason = selective_review_reason( pending_state, customer_text, reply_text, rules=getattr(ai_config, "AI_REVIEW_RULES", None) or [], ) return bool(reason), reason ''') text = replace_method(text, '_drop_stale_review_flags', ''' def _drop_stale_review_flags(self) -> None: """Re-evaluate known policy holds; independent manual/send guards remain.""" if self._review_mode_enabled(): return from contextlib import nullcontext from review_policy import can_recheck_review pending = getattr(self, "_pending_reply_sessions", None) or {} changed = [] with getattr(self, "_pending_lock", None) or nullcontext(): for key, state in list(pending.items()): if not isinstance(state, dict) or not state.get("awaiting_review") or not can_recheck_review(state): continue old_reason = str(state.get("review_reason") or "") origin = str(state.get("review_origin") or "") known = origin in {"global", "rule"} or not old_reason or any( word in old_reason for word in ("命中审核规则", "模型裁判")) if not known: continue customer = str(state.get("staged_user_text") or "\\n".join(str(line) for line in state.get("last_lines") or [])) reply = str(state.get("staged_reply_text") or state.get("reply_text") or "") # A legacy rule hold without its evidence cannot be re-evaluated. if old_reason and (not customer.strip() or not reply.strip()): continue needed, reason = self._reply_needs_review(state, customer, reply) if needed: if old_reason != reason: state["review_reason"] = reason state["stage_detail"] = reason changed.append(key) continue for field in ("awaiting_review", "review_requested_at", "review_approved", "review_approved_at", "review_reason", "review_origin"): state.pop(field, None) if state.get("stage") in {"manual_review", "awaiting_review"}: state.update(stage="queued", stage_detail="审核策略已更新,等待发送前核对", stage_error="", stage_started_at=time.time()) state["updated_at"] = time.time() changed.append(key) if changed: self._persist_pending_replies() ''') _, record = method(text, '_record_model_call') record = record.replace('self, fp: bytes, outcome: dict, *, task_id: str = "", customer_text: str = ""', 'self, fp: bytes, outcome: dict, *, task_id: str = "", customer_text: str = "",\n expected_task_id: str = "", expected_state: dict | None = None') start = record.index(' state = self._pending_reply_state(bytes(fp or b""))') end = record.index(' if not judge.get("participated"):', start) block = record[start:end] block = block.replace(' if state is not None:', ''' if expected_task_id and ( state is not expected_state or str((state or {}).get("model_task_id") or "") != expected_task_id ): state = None # A late verdict belongs to the earlier message batch. if state is not None:''') block = block.replace(' state["judge_risk"] = str(judge.get("risk") or "")', ' state["judge_risk"] = str(judge.get("risk") or "")\n state["judge_mode"] = str(outcome.get("judge_mode") or "").strip().lower()') record = record[:start] + ' from contextlib import nullcontext\n with getattr(self, "_pending_lock", None) or nullcontext():\n' + ''.join(' ' + line if line.strip() else line for line in block.splitlines(keepends=True)) + record[end:] text = replace_method(text, '_record_model_call', record) _, orchestrated = method(text, '_orchestrated_reply') orchestrated = orchestrated.replace(' import ai_chat\n import model_router\n', ''' import ai_chat import model_router import uuid from contextlib import nullcontext call_task_id = uuid.uuid4().hex with getattr(self, "_pending_lock", None) or nullcontext(): generation_state = self._pending_reply_state(bytes(fp or b"")) if generation_state is not None: generation_state["model_task_id"] = call_task_id for field in ("judge_risk", "judge_reason", "judge_score", "judge_mode"): generation_state.pop(field, None) self._persist_pending_replies() ''', 1) orchestrated = orchestrated.replace(' customer_text=customer_text,\n )', ' customer_text=customer_text,\n expected_task_id=call_task_id, expected_state=generation_state,\n )', 1) orchestrated = orchestrated.replace(' import uuid\n\n call_task_id = uuid.uuid4().hex\n outcome =', ' outcome =', 1) orchestrated = orchestrated.replace('fp, later, task_id=call_task_id, customer_text=customer_text\n', 'fp, later, task_id=call_task_id, customer_text=customer_text,\n expected_task_id=call_task_id, expected_state=generation_state,\n') orchestrated = orchestrated.replace('fp, outcome, task_id=call_task_id, customer_text=customer_text\n', 'fp, outcome, task_id=call_task_id, customer_text=customer_text,\n expected_task_id=call_task_id, expected_state=generation_state,\n') text = replace_method(text, '_orchestrated_reply', orchestrated) needle = ' "judge_risk": str(value.get("judge_risk") or "")[:32],' assert text.count(needle) == 2 text = text.replace(needle, needle + '\n "judge_mode": str(value.get("judge_mode") or "")[:32],') _, reset = method(text, '_reset_pending_batch') reset = reset.replace('def _reset_pending_batch(', 'def _reset_pending_batch_unlocked(', 1) reset = reset.replace(' state.pop("judge_risk", None)', ' state.pop("judge_risk", None)\n state.pop("judge_mode", None)') wrapper = ''' def _reset_pending_batch(self, fp: bytes) -> None: from contextlib import nullcontext with getattr(self, "_pending_lock", None) or nullcontext(): self._reset_pending_batch_unlocked(fp) ''' text = replace_method(text, '_reset_pending_batch', wrapper + reset) compile(text, str(path), 'exec') path.write_text(text, encoding='utf-8') path = roots[0] / 'protocol_engine.py' text = path.read_text(encoding='utf-8') text = replace_method(text, '_needs_review', ''' def _needs_review(self,state): if self.send_mode=='review':return True,'人工审核后点击“通过并发送”' import ai_config from review_policy import selective_review_reason reason=selective_review_reason(state,state.get('staged_user_text',''),state.get('reply_text',''), rules=getattr(ai_config,'AI_REVIEW_RULES',[]) or []) return bool(reason),reason ''') _, generate = method(text, '_generate') generate = generate.replace(' import ai_config,ai_chat,model_router\n', ''' import ai_config,ai_chat,model_router,uuid key=session_key(self.account,context['conv_id']);task_id=uuid.uuid4().hex with self._lock: generation_state=self._pending.get(key) if generation_state is not None: generation_state.update(model_task_id=task_id,judge_risk='',judge_mode='') ''', 1) generate = generate.replace(' if key in self._pending:', ' if self._pending.get(key) is generation_state and generation_state is not None:') generate = generate.replace(" self._pending[key]['judge_risk']=str((trace.get('judge') or {}).get('risk') or '')", " self._pending[key]['judge_risk']=str((trace.get('judge') or {}).get('risk') or '')\n self._pending[key]['judge_mode']=str(trace.get('judge_mode') or '').strip().lower()") generate = generate.replace(" if key in self._pending:self._pending[key]['judge_risk']=str((result.get('judge') or {}).get('risk') or '')", " if self._pending.get(key) is generation_state and generation_state is not None:\n generation_state.update(judge_risk=str((result.get('judge') or {}).get('risk') or ''),\n judge_mode=str(result.get('judge_mode') or '').strip().lower())") text = replace_method(text, '_generate', generate) text = text.replace(" if _send_protection(state) or state.get('stage')=='error':return", " if _send_protection(state) or state.get('stage') in ('error','manual_takeover'):return\n if state.get('manual_required') or state.get('foreign_draft_text') or state.get('foreign_draft_at') is not None:return", 1) text = text.replace(" if not state.get('awaiting_review'):", " if not state.get('awaiting_review') or state.get('review_reason')!=reason:", 1) text = text.replace(" if time.time()