180 lines
12 KiB
Python
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.')
|