518 lines
28 KiB
Diff
518 lines
28 KiB
Diff
--- C:\kefu\deploy\review-assistant-20260917\visual-before\source\wechat_bot.py
|
||
|
||
+++ C:\kefu\wechat_rpa\wechat_bot.py
|
||
|
||
@@ -2030,6 +2030,8 @@
|
||
|
||
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)
|
||
+ if not reason and self._review_assistant_recipient({**state, "display_name": name, "database_event": supplied or event}):
|
||
+ reason = "审核助理联系人仅接收提醒,不参与自动回复"
|
||
cache = getattr(self, "_excluded_session_cache", None)
|
||
if cache is None:
|
||
cache = self._excluded_session_cache = {}
|
||
@@ -8133,6 +8135,334 @@
|
||
|
||
else:
|
||
time.sleep(interval)
|
||
|
||
+ def _review_assistant_settings(self):
|
||
+ loader = getattr(self, "_review_assistant_settings_loader", None)
|
||
+ if loader is not None:
|
||
+ return loader()
|
||
+ try:
|
||
+ with open(os.path.join(_SCRIPT_DIR, "app_settings.json"), encoding="utf-8") as handle:
|
||
+ return json.load(handle)
|
||
+ except (OSError, ValueError):
|
||
+ return {}
|
||
+
|
||
+ def _review_assistant_warn(self, detail):
|
||
+ if detail != getattr(self, "_review_assistant_warning", None):
|
||
+ self._review_assistant_warning = detail
|
||
+ print(" [助理审核] " + detail + ";保留人工审核,不猜测人工回复。")
|
||
+
|
||
+ def _get_review_assistant(self, *, verify=False):
|
||
+ """Bind only to the live window's verified login, never a browser account."""
|
||
+ stop = getattr(self, "_stop_check", None)
|
||
+ if stop is not None and stop.is_set():
|
||
+ return None
|
||
+ try:
|
||
+ settings = self._review_assistant_settings()
|
||
+ configured = settings.get("review_assistant_accounts") if isinstance(settings, dict) else None
|
||
+ if not isinstance(configured, dict) or not any(
|
||
+ isinstance(value, dict) and value.get("enabled") for value in configured.values()):
|
||
+ self.close_review_assistant()
|
||
+ return None
|
||
+ current = getattr(self, "_review_assistant", None)
|
||
+ if current is not None and not verify:
|
||
+ return current if current.enabled() else None
|
||
+ from wecom_native_sender import NativeClient
|
||
+ _, pid = win32process.GetWindowThreadProcessId(self.hwnd)
|
||
+ with NativeClient(pid) as client:
|
||
+ account = str(client.account)
|
||
+ identity = (account, int(pid))
|
||
+ if identity != getattr(self, "_review_assistant_identity", None):
|
||
+ self.close_review_assistant()
|
||
+ from review_assistant import ReviewAssistantCoordinator
|
||
+ current = ReviewAssistantCoordinator(
|
||
+ root=_SCRIPT_DIR, settings_loader=self._review_assistant_settings,
|
||
+ account=account, pid=pid, db=getattr(self, "_db_source", None),
|
||
+ log=lambda message: print(" [助理审核] " + str(message)),
|
||
+ )
|
||
+ self._review_assistant = current
|
||
+ self._review_assistant_identity = identity
|
||
+ self._review_assistant_warning = None
|
||
+ return current if current is not None and current.enabled() else None
|
||
+ except Exception as exc:
|
||
+ self.close_review_assistant()
|
||
+ self._review_assistant_warn("当前账号或本机协议不可核验(" + type(exc).__name__ + ")")
|
||
+ return None
|
||
+
|
||
+ def close_review_assistant(self):
|
||
+ assistant = getattr(self, "_review_assistant", None)
|
||
+ self._review_assistant = None
|
||
+ self._review_assistant_identity = None
|
||
+ if assistant is not None:
|
||
+ try:
|
||
+ assistant.close()
|
||
+ except Exception:
|
||
+ pass
|
||
+
|
||
+ @staticmethod
|
||
+ def _review_assistant_protected(state):
|
||
+ return bool(state.get("send_state") or state.get("send_dispatched_at") or
|
||
+ str(state.get("stage") or "") in {
|
||
+ "sending", "receipt_check", "uncertain", "unknown", "sent_uncommitted", "failed"})
|
||
+
|
||
+ def _review_assistant_context(self, state, *, fp=None):
|
||
+ source = getattr(self, "_db_source", None)
|
||
+ identity = getattr(self, "_review_assistant_identity", None)
|
||
+ if source is None or not identity:
|
||
+ return None
|
||
+ account = str(identity[0])
|
||
+ event = state.get("database_event") or {}
|
||
+ bound_account = str(state.get("account") or event.get("account") or "")
|
||
+ conv_id = str(state.get("conv_id") or event.get("conv_id") or "")
|
||
+ if bound_account and bound_account != account:
|
||
+ return None
|
||
+ if conv_id:
|
||
+ context = source.get_conversation_context_by_id(account, conv_id, limit=120)
|
||
+ else:
|
||
+ # This API rejects duplicate names even when an account is supplied.
|
||
+ name = str(state.get("display_name") or "")
|
||
+ context = source.get_conversation_context(name, account=account, limit=120) if name else None
|
||
+ if (not isinstance(context, dict) or str(context.get("account") or "") != account
|
||
+ or not context.get("conv_id") or (conv_id and context.get("conv_id") != conv_id)):
|
||
+ return None
|
||
+ if fp is not None:
|
||
+ state["account"] = account
|
||
+ state["conv_id"] = str(context["conv_id"])
|
||
+ return context
|
||
+
|
||
+ def _review_assistant_recipient(self, state):
|
||
+ assistant = self._get_review_assistant()
|
||
+ if assistant is None:
|
||
+ return False
|
||
+ try:
|
||
+ context = self._review_assistant_context(state)
|
||
+ return bool(context and assistant.is_recipient(context["conv_id"]))
|
||
+ except Exception:
|
||
+ return False
|
||
+
|
||
+ def _review_assistant_resumed(self, state):
|
||
+ assistant = self._get_review_assistant(verify=True)
|
||
+ if assistant is None or not isinstance(state, dict):
|
||
+ return False
|
||
+ try:
|
||
+ context = self._review_assistant_context(state)
|
||
+ return bool(context and assistant.resumed(context["conv_id"]))
|
||
+ except Exception:
|
||
+ return False
|
||
+
|
||
+ def _register_assistant_review(self, fp):
|
||
+ state = self._pending_reply_state(fp)
|
||
+ if not state or not state.get("awaiting_review") or self._review_assistant_protected(state):
|
||
+ return
|
||
+ assistant = self._get_review_assistant(verify=True)
|
||
+ if assistant is None:
|
||
+ return
|
||
+ try:
|
||
+ context = self._review_assistant_context(state, fp=fp)
|
||
+ if not context:
|
||
+ self._review_assistant_warn("无法按当前账号核对待审核会话")
|
||
+ return
|
||
+ if assistant.is_recipient(context["conv_id"]):
|
||
+ return
|
||
+ # Bind the task identity durably before a reminder may be queued.
|
||
+ if not self._persist_pending_replies():
|
||
+ return
|
||
+ review_id = assistant.ensure_review(state, context)
|
||
+ if review_id:
|
||
+ state["assistant_review_id"] = str(review_id)
|
||
+ self._persist_pending_replies()
|
||
+ assistant.enqueue_notifications()
|
||
+ except Exception as exc:
|
||
+ self._review_assistant_warn("审核提醒登记暂不可用(" + type(exc).__name__ + ")")
|
||
+
|
||
+ def poll_review_assistant(self):
|
||
+ """Observe phone replies without moving the window or sending the old draft."""
|
||
+ now = time.monotonic()
|
||
+ if now < getattr(self, "_review_assistant_poll_at", 0.0):
|
||
+ return
|
||
+ self._review_assistant_poll_at = now + 2.0
|
||
+ stop = getattr(self, "_stop_check", None)
|
||
+ if stop is not None and stop.is_set():
|
||
+ return
|
||
+ assistant = self._get_review_assistant(verify=True)
|
||
+ if assistant is None:
|
||
+ return
|
||
+ from contextlib import nullcontext
|
||
+ items = list(getattr(self, "_pending_reply_sessions", {}).items())
|
||
+ start = int(getattr(self, "_review_assistant_cursor", 0)) % max(1, len(items))
|
||
+ for offset in range(min(8, len(items))):
|
||
+ if offset and time.monotonic() - now >= 0.5:
|
||
+ break
|
||
+ index = (start + offset) % len(items)
|
||
+ self._review_assistant_cursor = index + 1
|
||
+ key, state = items[index]
|
||
+ if not state.get("awaiting_review") or self._review_assistant_protected(state):
|
||
+ continue
|
||
+ try:
|
||
+ fp = bytes.fromhex(key)
|
||
+ context = self._review_assistant_context(state, fp=fp)
|
||
+ if not context or assistant.is_recipient(context["conv_id"]):
|
||
+ continue
|
||
+ review_id = assistant.ensure_review(state, context)
|
||
+ if not review_id:
|
||
+ continue
|
||
+ state["assistant_review_id"] = str(review_id)
|
||
+ if not self._persist_pending_replies():
|
||
+ continue
|
||
+ evidence = assistant.reconcile(state, context)
|
||
+ if (not isinstance(evidence, dict) or evidence.get("account") != context["account"]
|
||
+ or evidence.get("conv_id") != context["conv_id"]
|
||
+ or str(evidence.get("review_id") or "") != str(review_id)):
|
||
+ continue
|
||
+ with getattr(self, "_pending_lock", None) or nullcontext():
|
||
+ if (self._pending_reply_state(fp) is not state or not state.get("awaiting_review")
|
||
+ or self._review_assistant_protected(state)):
|
||
+ continue
|
||
+ current_event = str((state.get("database_event") or {}).get("message_id") or "")
|
||
+ if current_event and current_event not in {str(item.get("dedup_key") or "")
|
||
+ for item in context.get("messages") or []}:
|
||
+ continue # A DB event arrived while this older snapshot was being checked.
|
||
+ discarded = getattr(self, "_review_assistant_discarded_drafts", None)
|
||
+ if discarded is None:
|
||
+ discarded = self._review_assistant_discarded_drafts = {}
|
||
+ draft_key = str(context["account"]) + "\0" + str(context["conv_id"])
|
||
+ previous = discarded.get(draft_key)
|
||
+ discarded[draft_key] = {"account": context["account"], "conv_id": context["conv_id"],
|
||
+ "draft": str(state.get("reply_text") or state.get("staged_reply_text") or ""),
|
||
+ "review_id": str(review_id), "completed_at": time.time()}
|
||
+ # Remove only this exact old task; a concurrent replacement must survive.
|
||
+ self._pending_reply_sessions.pop(key)
|
||
+ if not self._persist_pending_replies():
|
||
+ self._pending_reply_sessions[key] = state
|
||
+ if previous is None:
|
||
+ discarded.pop(draft_key, None)
|
||
+ else:
|
||
+ discarded[draft_key] = previous
|
||
+ continue
|
||
+ getattr(self, "_pending_scan_progress", {}).pop(key, None)
|
||
+ getattr(self, "_pending_exchanges", {}).pop(key, None)
|
||
+ self.queue_log.append(self.session_id_of(fp), str(state.get("display_name") or "客户"),
|
||
+ "已审核", "已核对同账号人工回复,旧审核草稿已作废;后续新消息重新经过风控")
|
||
+ # Preserve customer messages arriving after the proven human reply.
|
||
+ human_time = float(evidence.get("send_time") or 0)
|
||
+ human_row = int(evidence.get("rowid") or 0)
|
||
+ later = [item for item in context.get("messages") or []
|
||
+ if item.get("is_self") is False and item.get("dedup_key")
|
||
+ and item.get("account", context["account"]) == context["account"]
|
||
+ and item.get("conv_id", context["conv_id"]) == context["conv_id"]
|
||
+ and (float(item.get("send_time") or 0) > human_time or
|
||
+ (float(item.get("send_time") or 0) == human_time and int(item.get("rowid") or 0) > human_row))]
|
||
+ last = max(later, key=lambda item: (float(item.get("send_time") or 0), int(item.get("rowid") or 0))) if later else {}
|
||
+ if last:
|
||
+ self.enqueue_detected(key, display_name=str(context.get("display_name") or ""),
|
||
+ chat_text=str(context.get("text") or ""), dedup_key=str(last["dedup_key"]),
|
||
+ detected_by="人工审核续接", database_event={
|
||
+ "account": context["account"], "conv_id": context["conv_id"],
|
||
+ "message_id": last["dedup_key"], "send_time": last.get("send_time"),
|
||
+ "name_unique": False})
|
||
+ except Exception as exc:
|
||
+ self._review_assistant_warn("人工回复核验暂不可用(" + type(exc).__name__ + ")")
|
||
+ assistant.enqueue_notifications()
|
||
+
|
||
+ def _is_discarded_review_draft(self, state, draft):
|
||
+ """Called only after normal composer identity and focus checks have passed."""
|
||
+ if not draft or self._review_assistant_protected(state or {}):
|
||
+ return False
|
||
+ try:
|
||
+ if self._get_review_assistant(verify=True) is None:
|
||
+ return False
|
||
+ context = self._review_assistant_context(state or {})
|
||
+ if not context:
|
||
+ return False
|
||
+ key = str(context["account"]) + "\0" + str(context["conv_id"])
|
||
+ entry = getattr(self, "_review_assistant_discarded_drafts", {}).get(key) or {}
|
||
+ # Strict raw comparison: edited human text is never eligible for deletion.
|
||
+ matches = bool(entry.get("draft") and str(draft).replace("\r\n", "\n") ==
|
||
+ str(entry["draft"]).replace("\r\n", "\n"))
|
||
+ if entry and not matches:
|
||
+ self._retire_discarded_review_marker(state)
|
||
+ return matches
|
||
+ except Exception:
|
||
+ return False
|
||
+
|
||
+ def _retire_discarded_review_marker(self, state):
|
||
+ discarded = getattr(self, "_review_assistant_discarded_drafts", {})
|
||
+ if not discarded:
|
||
+ return True
|
||
+ try:
|
||
+ context = self._review_assistant_context(state or {})
|
||
+ if not context:
|
||
+ return True # No verified editor binding: leave the marker and never touch text.
|
||
+ key = str(context["account"]) + "\0" + str(context["conv_id"])
|
||
+ old = discarded.pop(key, None)
|
||
+ if old is None:
|
||
+ return True
|
||
+ if self._persist_pending_replies():
|
||
+ return True
|
||
+ discarded[key] = old
|
||
+ except Exception:
|
||
+ if 'old' in locals() and old is not None:
|
||
+ discarded[key] = old
|
||
+ return False
|
||
+
|
||
+ def _record_assistant_automated_attempt(self, state, reply_text):
|
||
+ assistant = self._get_review_assistant(verify=True)
|
||
+ if assistant is None:
|
||
+ return True
|
||
+ try:
|
||
+ context = self._review_assistant_context(state or {})
|
||
+ if not context:
|
||
+ self._review_assistant_warn("自动发送前无法核对会话,暂缓本次发送")
|
||
+ return False
|
||
+ state["account"], state["conv_id"] = context["account"], context["conv_id"]
|
||
+ assistant.record_automated_attempt({**state, "reply_text": reply_text})
|
||
+ return True
|
||
+ except Exception as exc:
|
||
+ self._review_assistant_warn("自动发送留痕失败(" + type(exc).__name__ + "),暂缓本次发送")
|
||
+ return False
|
||
+
|
||
+ def _record_assistant_automated_send(self, state):
|
||
+ assistant = self._get_review_assistant()
|
||
+ if assistant is None or not isinstance(state, dict):
|
||
+ return
|
||
+ try:
|
||
+ # A verified visual receipt closes this local approval, but cannot
|
||
+ # grant blanket-mode resume or invent a server message identifier.
|
||
+ assistant.complete_local_review(state)
|
||
+ context = self._review_assistant_context(state)
|
||
+ if not context:
|
||
+ return
|
||
+ dispatched = float(state.get("send_dispatched_at") or 0)
|
||
+ reply = str(state.get("reply_text") or state.get("staged_reply_text") or "")
|
||
+ matches = [item for item in context.get("messages") or []
|
||
+ if item.get("account") == context["account"] and item.get("conv_id") == context["conv_id"]
|
||
+ and item.get("is_self") is True and str(item.get("sender_id") or "") == context["account"]
|
||
+ and str(item.get("content") or "").strip() == reply.strip()
|
||
+ and dispatched > 0 and float(item.get("send_time") or 0) >= dispatched - 1
|
||
+ and str(item.get("server_id") or "") not in {"", "0", "None"}]
|
||
+ if len(matches) == 1:
|
||
+ assistant.record_automated_send({**state, "account": context["account"],
|
||
+ "conv_id": context["conv_id"]}, {"status": "confirmed", "transport": "visual",
|
||
+ "accountId": context["account"], "conversationId": context["conv_id"],
|
||
+ "serverId": str(matches[0]["server_id"]), "confirmedAt": time.time()})
|
||
+ except Exception as exc:
|
||
+ self._review_assistant_warn("自动回复留痕暂不可用(" + type(exc).__name__ + ")")
|
||
+
|
||
+ def _reply_review_decision(self, pending_state, customer_text, reply_text):
|
||
+ resumed = held = False
|
||
+ assistant = self._get_review_assistant(verify=True)
|
||
+ if assistant is not None:
|
||
+ try:
|
||
+ context = self._review_assistant_context(pending_state or {})
|
||
+ if context:
|
||
+ held = assistant.has_pending(context["conv_id"])
|
||
+ resumed = assistant.resumed(context["conv_id"]) and not held
|
||
+ else:
|
||
+ held = True # Cannot exclude an existing review without exact conversation evidence.
|
||
+ except Exception:
|
||
+ held = True # Missing review state cannot grant an automatic send.
|
||
+ blanket = self._review_mode_enabled() and not resumed
|
||
+ hit, reason = self._reply_needs_review(pending_state, customer_text, reply_text)
|
||
+ return blanket, bool(hit or held), reason or ("本会话仍有待审核消息,等待同账号人工回复" if held else "")
|
||
+
|
||
def _review_mode_enabled(self) -> bool:
|
||
return str(getattr(self, "send_mode", SEND_MODE_AUTO)) == SEND_MODE_REVIEW
|
||
|
||
@@ -8792,6 +9122,7 @@
|
||
|
||
def _load_pending_replies(self) -> dict:
|
||
"""Restore read-but-unreplied work after a crash without restoring stale tasks."""
|
||
self._manual_handoffs = {}
|
||
+ self._review_assistant_discarded_drafts = {}
|
||
path = getattr(self, "_pending_reply_path", "")
|
||
if not path or not os.path.exists(path):
|
||
return {}
|
||
@@ -8802,6 +9133,10 @@
|
||
|
||
return {}
|
||
if not isinstance(raw, dict):
|
||
return {}
|
||
+ discarded = raw.get("assistant_discarded_drafts")
|
||
+ if isinstance(discarded, dict):
|
||
+ self._review_assistant_discarded_drafts = {str(key): dict(value) for key, value in discarded.items()
|
||
+ if isinstance(value, dict) and value.get("account") and value.get("conv_id")}
|
||
handoffs = raw.get("manual_handoffs")
|
||
if isinstance(handoffs, dict):
|
||
self._manual_handoffs = {str(key): dict(entry) for key, entry in handoffs.items()
|
||
@@ -8911,6 +9246,9 @@
|
||
|
||
"uncertain_since": uncertain_since,
|
||
"send_dispatched_at": send_dispatched_at,
|
||
"awaiting_review": bool(value.get("awaiting_review", False)),
|
||
+ "account": str(value.get("account") or ""),
|
||
+ "conv_id": str(value.get("conv_id") or ""),
|
||
+ "assistant_review_id": str(value.get("assistant_review_id") or ""),
|
||
"review_reason": str(value.get("review_reason") or "")[:500],
|
||
"review_origin": str(value.get("review_origin") or "")[:16],
|
||
"review_approved": bool(value.get("review_approved", False)),
|
||
@@ -9095,6 +9433,9 @@
|
||
|
||
value.get("send_dispatched_at", 0.0) or 0.0
|
||
),
|
||
"awaiting_review": bool(value.get("awaiting_review", False)),
|
||
+ "account": str(value.get("account") or ""),
|
||
+ "conv_id": str(value.get("conv_id") or ""),
|
||
+ "assistant_review_id": str(value.get("assistant_review_id") or ""),
|
||
"review_reason": str(value.get("review_reason") or "")[:500],
|
||
"review_origin": str(value.get("review_origin") or "")[:16],
|
||
"review_approved": bool(value.get("review_approved", False)),
|
||
@@ -9205,8 +9546,10 @@
|
||
|
||
item[field] = bytes(signature).hex() if signature else ""
|
||
serializable[str(key)] = item
|
||
handoffs = getattr(self, "_manual_handoffs", {})
|
||
- if handoffs:
|
||
- serializable = {"pending": serializable, "manual_handoffs": handoffs}
|
||
+ discarded = getattr(self, "_review_assistant_discarded_drafts", {})
|
||
+ if handoffs or discarded:
|
||
+ serializable = {"pending": serializable, "manual_handoffs": handoffs,
|
||
+ "assistant_discarded_drafts": discarded}
|
||
tmp = path + ".tmp"
|
||
try:
|
||
with open(tmp, "w", encoding="utf-8") as handle:
|
||
@@ -9767,6 +10110,7 @@
|
||
|
||
state.pop("send_dispatched_at", None)
|
||
state.pop("awaiting_review", None)
|
||
state.pop("review_requested_at", None)
|
||
+ state.pop("assistant_review_id", None)
|
||
state.pop("review_reason", None)
|
||
state.pop("review_origin", None)
|
||
state.pop("judge_risk", None)
|
||
@@ -10303,7 +10647,10 @@
|
||
|
||
persisted = handoff_unlocked()
|
||
if not persisted:
|
||
return result
|
||
+ from gui_runtime import cancel_assistant_review_for_state
|
||
for key, state in removed:
|
||
+ if state.get("assistant_review_id"):
|
||
+ cancel_assistant_review_for_state(state, os.path.dirname(self._pending_reply_path))
|
||
result["handed_off"].append(key)
|
||
result["deleted"].append(key)
|
||
getattr(self, "_pending_exchanges", {}).pop(key, None)
|
||
@@ -10406,7 +10753,10 @@
|
||
|
||
self._active_hold_key = ""
|
||
self._active_hold_since = 0.0
|
||
self._active_hold_expired = False
|
||
+ from gui_runtime import cancel_assistant_review_for_state
|
||
for key, _state, name in removed:
|
||
+ if _state.get("assistant_review_id"):
|
||
+ cancel_assistant_review_for_state(_state, os.path.dirname(self._pending_reply_path))
|
||
try:
|
||
self.queue_log.append(
|
||
key,
|
||
@@ -10613,6 +10963,7 @@
|
||
|
||
key = str(session_id or "")
|
||
exchange = getattr(self, "_pending_exchanges", {}).pop(key, None)
|
||
pending_state = getattr(self, "_pending_reply_sessions", {}).get(key) or {}
|
||
+ self._record_assistant_automated_send(pending_state)
|
||
if not exchange:
|
||
staged_user = str(pending_state.get("staged_user_text") or "")
|
||
staged_reply = str(pending_state.get("staged_reply_text") or "")
|
||
@@ -12461,8 +12812,17 @@
|
||
|
||
print(" [发送保护] 无法确认输入框焦点,已取消发送。")
|
||
return self._deny_send("确认不了输入框焦点,不敢往里打字")
|
||
normalized_draft = self._normalized_message_text(existing_draft)
|
||
+ if not normalized_draft and not self._retire_discarded_review_marker(pending_state):
|
||
+ return self._deny_send("旧审核稿作废记录尚未保存,等待重试")
|
||
if normalized_draft:
|
||
- if self._draft_is_our_own(
|
||
+ if self._is_discarded_review_draft(pending_state, existing_draft):
|
||
+ pyautogui.hotkey('ctrl', 'a')
|
||
+ pyautogui.press('backspace')
|
||
+ if not self._retire_discarded_review_marker(pending_state):
|
||
+ return self._deny_send("已清除旧审核稿,但作废记录尚未保存,等待重试")
|
||
+ self._clear_foreign_draft_hold(expected_fp)
|
||
+ print(" [助理审核] 已核对人工回复,仅清除本会话作废的原始 AI 草稿,重新处理新消息。")
|
||
+ elif self._draft_is_our_own(
|
||
normalized_draft,
|
||
pending_state,
|
||
expected_reply,
|
||
@@ -12682,22 +13042,15 @@
|
||
|
||
# 才破例停下来,其余照常自动发送。两者是 OR:任何一个成立都要停。
|
||
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()
|
||
- rule_hit, rule_reason = (
|
||
- (False, "")
|
||
- if blanket_review
|
||
- else self._reply_needs_review(
|
||
- pending_state,
|
||
- str((pending_state or {}).get("staged_user_text") or "\n".join(
|
||
- str(line) for line in (pending_state or {}).get("last_lines") or []
|
||
- )),
|
||
- reply_text,
|
||
- )
|
||
+ blanket_review, rule_hit, rule_reason = self._reply_review_decision(
|
||
+ pending_state, str((pending_state or {}).get("staged_user_text") or "\n".join(
|
||
+ str(line) for line in (pending_state or {}).get("last_lines") or [])), reply_text,
|
||
)
|
||
if (blanket_review or rule_hit) and not (pending_state or {}).get(
|
||
"review_approved", False
|
||
):
|
||
pending_state["awaiting_review"] = True
|
||
+ pending_state.pop("assistant_review_id", None)
|
||
pending_state["review_requested_at"] = time.time()
|
||
pending_state["reply_text"] = reply_text
|
||
pending_state["last_pasted_draft"] = expected_reply
|
||
@@ -12718,6 +13071,8 @@
|
||
|
||
)
|
||
if not persisted:
|
||
reason = "回复已填入输入框,但审核任务状态未能保存;已禁止自动发送"
|
||
+ if persisted:
|
||
+ self._register_assistant_review(expected_fp)
|
||
self._last_send_failure_reason = reason
|
||
self.report_operation("人工审核", 1, 1, reason, expected_fp)
|
||
print(f" [人工审核] {reason}。")
|
||
@@ -12749,6 +13104,8 @@
|
||
|
||
return self._deny_send("建不出发送前的聊天锚点,无法核对回执")
|
||
if baseline_raw:
|
||
print(" [发送保护] 该会话复制不出消息表头,改用整屏文字锚点核对回执。")
|
||
+ if not self._record_assistant_automated_attempt(pending_state, reply_text):
|
||
+ return self._deny_send("助理审核自动发送留痕未完成,本轮不发送")
|
||
pending_state["send_state"] = "sending"
|
||
pending_state["reply_text"] = reply_text
|
||
pending_state["send_surface_signature"] = final_surface or pending_surface
|