更新
This commit is contained in:
@@ -7,7 +7,9 @@ tkinter/tcl 拖进进程——打包后的 EXE 每次冷启动都白付这笔钱
|
||||
"""
|
||||
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
@@ -33,6 +35,76 @@ def normalize_message_batch_window_seconds(value, default=MESSAGE_BATCH_WINDOW_S
|
||||
return seconds
|
||||
|
||||
|
||||
def delete_pending_reply_file(keys, path=""):
|
||||
"""Atomically remove selected reply tasks while the bot is not running."""
|
||||
requested = {str(key or "").strip() for key in (keys or [])}
|
||||
requested.discard("")
|
||||
result = {"deleted": [], "missing": sorted(requested), "protected": [], "scheduled": []}
|
||||
if not requested:
|
||||
return result
|
||||
|
||||
target = str(path or os.path.join(application_data_dir(), "pending_replies.json"))
|
||||
try:
|
||||
with open(target, encoding="utf-8") as handle:
|
||||
raw = json.load(handle)
|
||||
except FileNotFoundError:
|
||||
return result
|
||||
except (OSError, ValueError, TypeError) as exc:
|
||||
result["error"] = str(exc)
|
||||
return result
|
||||
|
||||
if not isinstance(raw, dict):
|
||||
result["error"] = "待回复队列文件格式不正确"
|
||||
return result
|
||||
pending = raw["pending"] if isinstance(raw.get("pending"), dict) else raw
|
||||
removed = []
|
||||
for key in sorted(requested):
|
||||
state = pending.pop(key, None)
|
||||
if isinstance(state, dict):
|
||||
removed.append((key, state))
|
||||
if not removed:
|
||||
return result
|
||||
|
||||
directory = os.path.dirname(os.path.abspath(target)) or "."
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
handle = tempfile.NamedTemporaryFile(
|
||||
"w",
|
||||
encoding="utf-8",
|
||||
dir=directory,
|
||||
prefix=".pending_replies_",
|
||||
suffix=".tmp",
|
||||
delete=False,
|
||||
)
|
||||
try:
|
||||
with handle:
|
||||
json.dump(raw, handle, ensure_ascii=False, indent=2)
|
||||
os.replace(handle.name, target)
|
||||
except Exception as exc:
|
||||
try:
|
||||
os.unlink(handle.name)
|
||||
except OSError:
|
||||
pass
|
||||
result["error"] = str(exc)
|
||||
return result
|
||||
|
||||
result["deleted"] = [key for key, _state in removed]
|
||||
result["missing"] = sorted(requested.difference(result["deleted"]))
|
||||
try:
|
||||
from queue_log import QueueLog
|
||||
|
||||
queue_log = QueueLog()
|
||||
for key, state in removed:
|
||||
queue_log.append(
|
||||
key,
|
||||
str(state.get("display_name") or "(未识别昵称)"),
|
||||
"手动删除",
|
||||
"用户从回复队列中手动删除;本条未完成回复不再自动发送",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
class LogQueue:
|
||||
"""把后台线程的标准输出转发到界面,同时留一份带时间戳的磁盘副本。
|
||||
|
||||
@@ -45,6 +117,31 @@ class LogQueue:
|
||||
|
||||
LOG_DIR = os.path.join(str(application_data_dir()), "logs")
|
||||
KEEP_FILES = 20
|
||||
_PROGRESS_PREFIXES = (
|
||||
("[新消息]", "未读定位"),
|
||||
("[未读扫描]", "未读扫描"),
|
||||
("[页面校验]", "页面校验"),
|
||||
("[会话校验]", "会话校验"),
|
||||
("[会话识别]", "会话识别"),
|
||||
("[会话守护]", "会话守护"),
|
||||
("[页面恢复]", "页面恢复"),
|
||||
("[页面清理]", "页面清理"),
|
||||
("[视觉重定位]", "视觉重定位"),
|
||||
("[视觉安全]", "视觉安全"),
|
||||
("[视觉队列]", "视觉队列"),
|
||||
("[目标指纹]", "目标确认"),
|
||||
("[消息合并]", "消息合并"),
|
||||
("[剪贴板]", "消息复制"),
|
||||
("[发送保护]", "发送保护"),
|
||||
("[发送对账]", "发送对账"),
|
||||
("[发送回执]", "发送回执"),
|
||||
("[安全模式]", "安全模式"),
|
||||
("[安全验证]", "安全验证"),
|
||||
("[人手]", "人机协同"),
|
||||
("[AI 页面确认]", "AI 页面确认"),
|
||||
("[AI页面守护]", "AI 页面守护"),
|
||||
("[AI]", "AI 处理"),
|
||||
)
|
||||
|
||||
def __init__(self, target_queue):
|
||||
self.target_queue = target_queue
|
||||
@@ -81,12 +178,37 @@ class LogQueue:
|
||||
if not text:
|
||||
return
|
||||
self.target_queue.put(("log", text))
|
||||
progress = self.progress_from_log(text)
|
||||
if progress:
|
||||
# 旧代码里大量关键闸门只写了 print。把这些结构化标签同步到进程
|
||||
# 通道,扫描、页面恢复等底层动作也能进入胶囊,不会只显示模型阶段。
|
||||
self.target_queue.put(("progress", progress))
|
||||
if self._handle is not None:
|
||||
try:
|
||||
self._handle.write(f"{time.strftime('%H:%M:%S')} {text}\n")
|
||||
except Exception:
|
||||
self._handle = None
|
||||
|
||||
@classmethod
|
||||
def progress_from_log(cls, message):
|
||||
"""把带操作标签的日志压成一行胶囊进程;聊天正文不进入浮窗。"""
|
||||
first_line = next(
|
||||
(line.strip() for line in str(message or "").splitlines() if line.strip()),
|
||||
"",
|
||||
)
|
||||
if not first_line:
|
||||
return ""
|
||||
for prefix, phase in cls._PROGRESS_PREFIXES:
|
||||
if not first_line.startswith(prefix):
|
||||
continue
|
||||
detail = first_line[len(prefix):].strip()
|
||||
if prefix == "[AI]" and (
|
||||
not detail or detail.startswith("本次提取的新内容")
|
||||
):
|
||||
return ""
|
||||
return f"{phase} · {detail}" if detail else phase
|
||||
return ""
|
||||
|
||||
def flush(self):
|
||||
if self._handle is not None:
|
||||
try:
|
||||
@@ -116,6 +238,8 @@ class BotThread(threading.Thread):
|
||||
self.mouse_idle_enabled = mouse_idle_enabled
|
||||
self.mouse_idle_seconds = mouse_idle_seconds
|
||||
self.bot = None
|
||||
self._cancel_lock = threading.Lock()
|
||||
self._queued_cancellations = set()
|
||||
self.message_batch_window_seconds = MESSAGE_BATCH_WINDOW_SECONDS
|
||||
self.set_message_batch_window_seconds(message_batch_window_seconds)
|
||||
self.stop_event = threading.Event()
|
||||
@@ -136,6 +260,38 @@ class BotThread(threading.Thread):
|
||||
# 进度提示纯属好看,永远不该把轮询带下去
|
||||
pass
|
||||
|
||||
def _report_visual_state(self, payload):
|
||||
"""把只读视觉监听结果送到 Qt 主线程显示。"""
|
||||
try:
|
||||
self.target_queue.put(("visual", dict(payload or {})))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def cancel_pending_tasks(self, keys):
|
||||
"""Cancel live work, or queue cancellation until the bot finishes starting."""
|
||||
requested = {str(key or "").strip() for key in (keys or [])}
|
||||
requested.discard("")
|
||||
if not requested:
|
||||
return {"deleted": [], "missing": [], "protected": [], "scheduled": []}
|
||||
with self._cancel_lock:
|
||||
bot = self.bot
|
||||
if bot is None:
|
||||
self._queued_cancellations.update(requested)
|
||||
return {
|
||||
"deleted": [],
|
||||
"missing": [],
|
||||
"protected": [],
|
||||
"scheduled": sorted(requested),
|
||||
}
|
||||
return bot.cancel_pending_replies(requested)
|
||||
|
||||
def _drain_queued_cancellations(self, bot):
|
||||
with self._cancel_lock:
|
||||
requested = set(self._queued_cancellations)
|
||||
self._queued_cancellations.clear()
|
||||
if requested:
|
||||
bot.cancel_pending_replies(requested)
|
||||
|
||||
def run(self):
|
||||
bot = None
|
||||
failed = False
|
||||
@@ -145,6 +301,7 @@ class BotThread(threading.Thread):
|
||||
bot_module.AUTO_REPLY_TEXT = self.reply_text
|
||||
bot = bot_module.WeChatBot()
|
||||
self.bot = bot
|
||||
self._drain_queued_cancellations(bot)
|
||||
bot.mouse_idle_enabled = self.mouse_idle_enabled
|
||||
bot.mouse_idle_seconds = self.mouse_idle_seconds
|
||||
bot.message_batch_window_seconds = self.message_batch_window_seconds
|
||||
@@ -152,6 +309,7 @@ class BotThread(threading.Thread):
|
||||
bot.safe_window_mode = True
|
||||
bot.auto_activate_window = True
|
||||
bot.progress_cb = self._report_progress
|
||||
bot._visual_state_cb = self._report_visual_state
|
||||
|
||||
if not bot.connect(activate=False, wait_if_missing=True):
|
||||
failed = True
|
||||
@@ -210,9 +368,20 @@ class BotThread(threading.Thread):
|
||||
))
|
||||
self.target_queue.put(("status", "error"))
|
||||
finally:
|
||||
if bot is not None:
|
||||
try:
|
||||
bot.stop_visual_observer()
|
||||
except Exception:
|
||||
pass
|
||||
self.bot = None
|
||||
if not failed:
|
||||
self.target_queue.put(("status", "stopped"))
|
||||
|
||||
def stop(self):
|
||||
self.stop_event.set()
|
||||
bot = getattr(self, "bot", None)
|
||||
if bot is not None:
|
||||
try:
|
||||
bot.stop_visual_observer()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user