125 lines
6.8 KiB
Python
125 lines
6.8 KiB
Python
from pathlib import Path
|
|
import shutil
|
|
|
|
protocol = Path('C:/wechat_rpa/protocol_engine.py')
|
|
text = protocol.read_text(encoding='utf-8')
|
|
old = " def cancel_pending_replies(self,keys):return self._control(keys,'delete')"
|
|
assert text.count(old) == 1
|
|
text = text.replace(old, ''' def handoff_pending_replies(self, keys):
|
|
"""Explicitly retire selected work to a human without claiming delivery.
|
|
|
|
Uncertain receipts remain in the send journal and the inbound dedup key
|
|
stays seen. No send/retry is scheduled by this action.
|
|
"""
|
|
result={'handed_off':[], 'deleted':[], 'missing':[], 'protected':[], 'scheduled':[]}
|
|
removed=[]
|
|
with self._lock:
|
|
for key in dict.fromkeys(str(k).strip() for k in keys if str(k).strip()):
|
|
state=self._pending.get(key)
|
|
if state is None:result['missing'].append(key);continue
|
|
if state.get('account')!=self.account or state.get('send_state')=='sending':
|
|
result['protected'].append(key);continue
|
|
if state.get('dedup_key'):self._seen[key]=state['dedup_key']
|
|
self._pending.pop(key,None)
|
|
result['handed_off'].append(key);result['deleted'].append(key)
|
|
removed.append((key,state))
|
|
self.needs_attention=any(s.get('account')==self.account and s.get('send_state')=='uncertain'
|
|
for s in self._pending.values())
|
|
self._save()
|
|
self._reply_wakeup.set()
|
|
for key,state in removed:
|
|
self.log(f"[协议转人工] {state.get('display_name','')} · 自动队列已移交人工;发送结果未作确认,不自动重发")
|
|
try:
|
|
from queue_log import QueueLog
|
|
QueueLog().append(key,state.get('display_name',''),'转人工',
|
|
'用户已确认人工接管;保留发送流水和去重记录,不自动重发')
|
|
except Exception:pass
|
|
self._listening_progress()
|
|
return result
|
|
|
|
''' + old)
|
|
compile(text, str(protocol), 'exec')
|
|
protocol.write_text(text, encoding='utf-8')
|
|
|
|
for root in (Path('C:/wechat_rpa'), Path('C:/kefu/wechat_rpa')):
|
|
path = root / 'gui_runtime.py'
|
|
text = path.read_text(encoding='utf-8')
|
|
def replace(old, new, count=1):
|
|
global text
|
|
assert text.count(old)==count, (str(path), old[:60], text.count(old), count)
|
|
text=text.replace(old,new)
|
|
|
|
replace('def delete_pending_reply_file(keys, path=""):', 'def delete_pending_reply_file(keys, path="", *, handoff=False):')
|
|
replace(' removed = []\n for key in sorted(requested):\n state = pending.pop(key, None)\n if isinstance(state, dict):\n removed.append((key, state))', ''' removed = []
|
|
protocol = raw.get("transport") == "protocol"
|
|
if handoff and protocol and not isinstance(raw.get("seen", {}), dict):
|
|
result["error"] = "协议去重记录格式不正确,无法安全转交人工"
|
|
return result
|
|
for key in sorted(requested):
|
|
state = pending.get(key)
|
|
if not isinstance(state, dict):
|
|
continue
|
|
if handoff and str(state.get("send_state") or "") == "sending":
|
|
result["protected"].append(key)
|
|
continue
|
|
if handoff and protocol and state.get("dedup_key"):
|
|
raw.setdefault("seen", {})[key] = state["dedup_key"]
|
|
pending.pop(key, None)
|
|
removed.append((key, state))''')
|
|
replace(' if not removed:\n return result\n\n directory =', ' if not removed:\n result["missing"] = sorted(requested.difference(result["protected"]))\n return result\n\n directory =')
|
|
replace(' result["missing"] = sorted(requested.difference(result["deleted"]))', ' result["missing"] = sorted(requested.difference(result["deleted"]).difference(result["protected"]))')
|
|
replace(' "手动删除",\n "用户从回复队列中手动删除;本条未完成回复不再自动发送",', ''' "转人工" if handoff else "手动删除",
|
|
("用户已确认人工接管;发送结果未作确认,保留发送流水和去重记录,不自动重发"
|
|
if handoff else "用户从回复队列中手动删除;本条未完成回复不再自动发送"),''')
|
|
replace('\n\nclass LogQueue:', '''
|
|
|
|
def handoff_pending_reply_file(keys, path=""):
|
|
"""Retire stopped work after explicit human takeover; preserve send evidence."""
|
|
result = delete_pending_reply_file(keys, path, handoff=True)
|
|
result["handed_off"] = list(result.get("deleted") or [])
|
|
return result
|
|
|
|
|
|
class LogQueue:''')
|
|
replace(' self._queued_cancellations = set()', ' self._queued_cancellations = set()\n self._queued_handoffs = set()')
|
|
replace(' def retry_pending_tasks(self, keys):', ''' @staticmethod
|
|
def _handoff_bot_tasks(bot, keys):
|
|
handoff = getattr(bot, "handoff_pending_replies", None)
|
|
if callable(handoff):
|
|
return handoff(keys)
|
|
result = bot.cancel_pending_replies(keys)
|
|
result["handed_off"] = list(result.get("deleted") or [])
|
|
return result
|
|
|
|
def handoff_pending_tasks(self, keys):
|
|
"""Keep explicit takeover separate from ordinary cancellation at startup."""
|
|
requested = {str(key or "").strip() for key in (keys or [])}
|
|
requested.discard("")
|
|
if not requested:
|
|
return {"handed_off": [], "deleted": [], "missing": [], "protected": [], "scheduled": []}
|
|
with self._cancel_lock:
|
|
bot = self.bot
|
|
if bot is None:
|
|
self._queued_handoffs.update(requested)
|
|
return {"handed_off": [], "deleted": [], "missing": [], "protected": [], "scheduled": sorted(requested)}
|
|
return self._handoff_bot_tasks(bot, requested)
|
|
|
|
def _drain_queued_handoffs(self, bot):
|
|
with self._cancel_lock:
|
|
requested = set(self._queued_handoffs)
|
|
self._queued_handoffs.clear()
|
|
if requested:
|
|
self._handoff_bot_tasks(bot, requested)
|
|
|
|
def retry_pending_tasks(self, keys):''')
|
|
drains = text.count(' self._drain_queued_cancellations(bot)')
|
|
replace(' self._drain_queued_cancellations(bot)', ' self._drain_queued_handoffs(bot)\n self._drain_queued_cancellations(bot)', drains)
|
|
compile(text, str(path), 'exec')
|
|
backup = root / 'backups/review-visibility-20260916'
|
|
backup.mkdir(parents=True, exist_ok=True)
|
|
assert not (backup / path.name).exists(), 'Backup already exists; do not rerun'
|
|
shutil.copy2(path, backup / path.name)
|
|
path.write_text(text, encoding='utf-8')
|
|
shutil.copy2(Path(__file__).with_name('test_review_handoff.py'), root / 'test_review_handoff.py')
|
|
print('Updated explicit handoff: protocol engine and both runtime modules.')
|