Files
kefu/deploy/review-badges-20260917/patch_review_policy.py
T
2026-09-21 10:34:06 +08:00

180 lines
12 KiB
Python

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()<float(state.get('send_ready_at',0)):return", """ if not review and (state.get('awaiting_review') or state.get('review_reason')):
with self._lock:state.update(awaiting_review=False,review_reason='')
self._stage(key,'ready_to_send','审核策略已更新,等待发送前核对')
if time.time()<float(state.get('send_ready_at',0)):return""", 1)
text = text.replace(" awaiting_review=False,review_reason='',manual_required=False,manual_reason='')",
" awaiting_review=False,review_reason='',manual_required=False,manual_reason='',\n judge_risk='',judge_mode='')", 1)
compile(text, str(path), 'exec')
path.write_text(text, encoding='utf-8')
print('Review policy integration applied to both visual clients and target protocol engine.')