新增功能

This commit is contained in:
Your Name
2026-08-19 17:35:59 +08:00
parent f8c78739e7
commit 05050ff6f3
188 changed files with 18838 additions and 5337 deletions
+201 -4
View File
@@ -148,8 +148,12 @@ CALL_WINDOW_KEYWORDS = (
# 人机共存:人工移动鼠标后,机器人暂停;鼠标静止满此秒数才继续操作
MOUSE_IDLE_ENABLED = True
MOUSE_IDLE_SECONDS = 20
MOUSE_IDLE_SECONDS = 5
MOUSE_MOVE_THRESHOLD = 8 # 位移超过此像素才视为人工移动(过滤抖动)
SEND_DELAY_SECONDS = 1.0 # AI 回复写入输入框后、最终发送前的独立等待
SEND_DELAY_MAX_SECONDS = 30.0
SEND_MODE_AUTO = "auto"
SEND_MODE_REVIEW = "review"
# 企业微信 UI 布局参数(相对于窗口左上角的像素偏移,适用于大多数 PC 版本)
NAV_BAR_W = 68 # 最左侧导航图标栏宽度
@@ -1303,6 +1307,8 @@ class WeChatBot:
# 人机共存:人工操作鼠标时暂停自动回复
self.mouse_idle_enabled = MOUSE_IDLE_ENABLED
self.mouse_idle_seconds = MOUSE_IDLE_SECONDS
self.send_delay_seconds = SEND_DELAY_SECONDS
self.send_mode = SEND_MODE_AUTO
# 每次开始新的消息合并窗口时读取实例值。GUI 可在监听期间更新它,
# 已经开始的窗口保持原截止时间,下一轮回复使用新设置
self.message_batch_window_seconds = MESSAGE_BATCH_WINDOW_SECONDS
@@ -6813,6 +6819,38 @@ class WeChatBot:
else:
time.sleep(0.4)
def wait_before_send(self, expected_fp: bytes | None = None) -> bool:
"""在最终发送前等待独立延迟,并允许停止监听或删除任务打断。"""
raw_seconds = getattr(self, "send_delay_seconds", 0.0)
try:
seconds = float(raw_seconds)
except (TypeError, ValueError):
seconds = SEND_DELAY_SECONDS
if isinstance(raw_seconds, bool):
seconds = SEND_DELAY_SECONDS
seconds = min(SEND_DELAY_MAX_SECONDS, max(0.0, seconds))
if seconds <= 0:
return True
print(f" [发送保护] 发送前等待 {seconds:g} 秒…")
deadline = time.monotonic() + seconds
while True:
if expected_fp is not None and self._reply_task_cancelled(expected_fp):
return False
remaining = deadline - time.monotonic()
if remaining <= 0:
return True
interval = min(0.2, remaining)
stop_check = getattr(self, "_stop_check", None)
if stop_check is not None:
if stop_check.wait(interval):
return False
else:
time.sleep(interval)
def _review_mode_enabled(self) -> bool:
return str(getattr(self, "send_mode", SEND_MODE_AUTO)) == SEND_MODE_REVIEW
def _mouse_is_idle_now(self) -> bool:
"""Check human activity without blocking the receipt/recovery queue."""
if not getattr(self, "mouse_idle_enabled", False):
@@ -7385,6 +7423,13 @@ class WeChatBot:
)
except (TypeError, ValueError):
send_dispatched_at = 0.0
try:
review_requested_at = max(
0.0,
float(value.get("review_requested_at", 0.0) or 0.0),
)
except (TypeError, ValueError):
review_requested_at = 0.0
raw_baseline_blocks = value.get("send_baseline_blocks")
send_baseline_blocks = None
if isinstance(raw_baseline_blocks, list) and raw_baseline_blocks:
@@ -7428,6 +7473,8 @@ class WeChatBot:
),
"uncertain_since": uncertain_since,
"send_dispatched_at": send_dispatched_at,
"awaiting_review": bool(value.get("awaiting_review", False)),
"review_requested_at": review_requested_at,
"reply_text": str(value.get("reply_text") or ""),
"staged_user_text": str(value.get("staged_user_text") or ""),
"staged_reply_text": str(value.get("staged_reply_text") or ""),
@@ -7565,6 +7612,10 @@ class WeChatBot:
"send_dispatched_at": float(
value.get("send_dispatched_at", 0.0) or 0.0
),
"awaiting_review": bool(value.get("awaiting_review", False)),
"review_requested_at": float(
value.get("review_requested_at", 0.0) or 0.0
),
"reply_text": str(value.get("reply_text") or ""),
"staged_user_text": str(value.get("staged_user_text") or ""),
"staged_reply_text": str(value.get("staged_reply_text") or ""),
@@ -7725,6 +7776,14 @@ class WeChatBot:
"""
if self._reply_task_cancelled(fp):
return
if self._reply_awaiting_review(fp):
self._set_task_stage(
fp,
"manual_review",
detail="AI 回复已填入输入框,等待人工核对并发送",
)
self._log_queue_event(fp, "待审核", "回复草稿已填入企业微信输入框")
return
if sent:
self._log_queue_event(fp, "已发送", reply_text)
return
@@ -7749,6 +7808,10 @@ class WeChatBot:
)
self._log_queue_event(fp, "失败", reason or "发送未成功,任务保留")
def _reply_awaiting_review(self, fp: bytes) -> bool:
state = self._pending_reply_state(bytes(fp or b"")) or {}
return bool(state.get("awaiting_review", False))
_GENERATION_EVENTS = frozenset({"生成回复", "跳过", "失败"})
_QUEUE_EVENT_PROGRESS = {
@@ -7756,6 +7819,7 @@ class WeChatBot:
"开始处理": "正在读取消息",
"调用模型": "正在调用 AI 模型",
"生成回复": "回复已生成,准备发送",
"待审核": "回复草稿等待人工审核",
"已发送": "回复已发送",
"发送待核对": "正在核对发送结果",
"对账": "正在核对发送结果",
@@ -7925,6 +7989,7 @@ class WeChatBot:
"blocked_modal": "弹窗阻塞",
"call_paused": "视频/通话暂停",
"manual_takeover": "人工接管",
"manual_review": "等待人工审核",
"uncertain": "发送待核对",
}
@@ -8024,6 +8089,8 @@ class WeChatBot:
state.pop("registration_lead", None)
state.pop("archive_enabled", None)
state.pop("send_dispatched_at", None)
state.pop("awaiting_review", None)
state.pop("review_requested_at", None)
state.pop("send_baseline_blocks", None)
state.pop("send_baseline_text", None)
state.pop("last_pasted_draft", None)
@@ -8456,6 +8523,88 @@ class WeChatBot:
pass
return result
def retry_pending_replies(self, keys) -> dict:
"""Move selected retry-wait tasks back to the runnable queue safely.
This deliberately refuses normal in-flight work, manual-review drafts and
any transaction whose Enter key may already have been dispatched. The UI
therefore cannot turn an innocent refresh click into a duplicate reply.
"""
requested = {str(key or "").strip() for key in (keys or [])}
requested.discard("")
result = {
"retried": [],
"missing": [],
"protected": [],
"not_retryable": [],
"scheduled": [],
}
if not requested:
return result
lock = getattr(self, "_pending_lock", None)
def retry_unlocked() -> bool:
pending = getattr(self, "_pending_reply_sessions", {})
originals = {}
for key in sorted(requested):
state = pending.get(key)
if not isinstance(state, dict):
result["missing"].append(key)
continue
send_state = str(state.get("send_state") or "")
if bool(state.get("awaiting_review", False)) or send_state in {
"sending",
"sent_uncommitted",
"uncertain",
}:
result["protected"].append(key)
continue
if str(state.get("stage") or "") != "retry_wait":
result["not_retryable"].append(key)
continue
try:
fp = bytes.fromhex(key)
except ValueError:
result["missing"].append(key)
continue
originals[key] = dict(state)
state["last_resume_attempt"] = 0.0
self._set_task_stage_unlocked(
fp,
"queued",
detail="用户请求立即重试",
)
result["retried"].append(key)
if not result["retried"]:
return True
if self._persist_pending_replies_unlocked():
return True
for key, state in originals.items():
pending[key] = state
result["error"] = "待回复队列写入失败"
result["retried"].clear()
return False
if lock is None:
persisted = retry_unlocked()
else:
with lock:
persisted = retry_unlocked()
if not persisted:
return result
for key in result["retried"]:
state = getattr(self, "_pending_reply_sessions", {}).get(key) or {}
try:
self.queue_log.append(
key,
str(state.get("display_name") or "") or "(未识别昵称)",
"手动重试",
"用户从任务队列请求立即重试,已重新排队",
)
except Exception:
pass
return result
def _clear_reply_pending(self, fp: bytes) -> None:
lock = getattr(self, "_pending_lock", None)
if lock is None:
@@ -10009,6 +10158,8 @@ class WeChatBot:
"""
if not normalized_draft or not isinstance(pending_state, dict):
return False
if pending_state.get("awaiting_review", False):
return False
# 已经按过回车的事务归回执核对管,这里擅自清掉草稿再发一遍就是重复发送
if str(pending_state.get("send_state") or ""):
return False
@@ -10066,6 +10217,14 @@ class WeChatBot:
return self._deny_send("检测到登录/安全验证页,已禁止一切自动操作")
if not self._await_send_gate():
return self._deny_send("发送限频未过,等待后仍不允许发送")
if not self.wait_before_send(expected_fp):
if expected_fp is not None and self._reply_task_cancelled(expected_fp):
return self._deny_send("任务已被手动删除")
return self._deny_send("监听已停止,未发送自动回复")
if expected_fp is not None and self._reply_task_cancelled(expected_fp):
return self._deny_send("任务已被手动删除")
if self._security_gate_visible():
return self._deny_send("发送前等待期间出现登录/安全验证页,已禁止发送")
if not self.wait_for_mouse_idle():
return self._deny_send("人在用鼠标,等待其停下超时")
self.report_operation("发送", 2, 12, "限频通过且鼠标空闲", expected_fp)
@@ -10411,6 +10570,24 @@ class WeChatBot:
print(" [发送保护] 任务已被手动删除,已清除尚未发送的自动草稿。")
return self._deny_send("任务已被手动删除")
if self._review_mode_enabled():
pending_state["awaiting_review"] = True
pending_state["review_requested_at"] = time.time()
pending_state["reply_text"] = reply_text
pending_state["last_pasted_draft"] = expected_reply
persisted = self._set_task_stage(
expected_fp,
"manual_review",
detail="AI 回复已填入输入框,等待人工核对并发送",
)
reason = "回复已填入输入框,等待人工审核发送"
if not persisted:
reason = "回复已填入输入框,但审核任务状态未能保存;已禁止自动发送"
self._last_send_failure_reason = reason
self.report_operation("人工审核", 1, 1, reason, expected_fp)
print(f" [人工审核] {reason}")
return False
if pending_state:
baseline_text = "\n".join(
str(line)
@@ -11605,6 +11782,11 @@ class WeChatBot:
self._active_hold_since = 0.0
self._active_hold_expired = False
return False
if bool((pending.get(key) or {}).get("awaiting_review", False)):
self._active_hold_key = ""
self._active_hold_since = 0.0
self._active_hold_expired = False
return False
now = time.monotonic()
if getattr(self, "_active_hold_key", "") != key:
self._active_hold_key = key
@@ -11653,6 +11835,8 @@ class WeChatBot:
pending.pop(key, None)
self._persist_pending_replies()
continue
if bool(state.get("awaiting_review", False)):
continue
if self._session_is_unrepliable(target_fp):
# 已经证实没有输入框,连打开都不必,免得每轮都被它占满
self._clear_reply_pending(target_fp)
@@ -11841,13 +12025,17 @@ class WeChatBot:
return True
time.sleep(0.2)
self._set_task_stage(target_fp, "sending", detail="恢复任务,正在发送回复")
if self.send_reply(
sent = self.send_reply(
reply_text,
session_id=target_fp.hex(),
expected_fp=target_fp,
):
)
if sent:
state.pop("resume_failures", None)
self._log_send_outcome(target_fp, True, reply_text)
elif self._reply_awaiting_review(target_fp):
state.pop("resume_failures", None)
self._log_send_outcome(target_fp, False, reply_text)
else:
self._log_send_outcome(target_fp, False)
self._note_resume_failure(state)
@@ -12151,6 +12339,10 @@ class WeChatBot:
return False
if pending_state is not None:
if bool(pending_state.get("awaiting_review", False)):
if self._nothing_left_to_answer(fp):
return True
return False
send_reconciliation = self._reconcile_uncertain_send(fp, pending_state)
if send_reconciliation == "sent":
return True
@@ -12338,7 +12530,7 @@ class WeChatBot:
)
self._log_send_outcome(fp, sent, reply_text)
time.sleep(0.5)
return sent
return bool(sent or self._reply_awaiting_review(fp))
def _generate_ai_reply(
self,
@@ -13371,6 +13563,11 @@ class WeChatBot:
session_id=target_fp.hex(),
expected_fp=target_fp,
):
if self._reply_awaiting_review(target_fp):
self.report_progress(f"{who} 的回复草稿等待人工审核")
self._log_send_outcome(target_fp, False, reply_text)
processed_fp.add(target_fp)
break
print(" [发送保护] 本次回复未发送,已停止继续处理当前批次。")
self.report_progress(f"{who} 未发送成功,稍后重试")
self._log_send_outcome(target_fp, False)