更新
This commit is contained in:
+18
-196
@@ -73,22 +73,6 @@ AUTO_REPLY_TEXT = "在的,您慢慢说,我这边看着呢。"
|
||||
POLL_INTERVAL = 2.0
|
||||
MOUSE_IDLE_ENABLED = True
|
||||
MOUSE_IDLE_SECONDS = 20.0
|
||||
MESSAGE_BATCH_WINDOW_SECONDS = 20.0
|
||||
MESSAGE_BATCH_WINDOW_MIN_SECONDS = 1.0
|
||||
MESSAGE_BATCH_WINDOW_MAX_SECONDS = 120.0
|
||||
|
||||
|
||||
def normalize_message_batch_window_seconds(value, default=MESSAGE_BATCH_WINDOW_SECONDS):
|
||||
"""读取本地设置时安全归一化消息合并等待时间。"""
|
||||
if isinstance(value, bool):
|
||||
return float(default)
|
||||
try:
|
||||
seconds = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return float(default)
|
||||
if not MESSAGE_BATCH_WINDOW_MIN_SECONDS <= seconds <= MESSAGE_BATCH_WINDOW_MAX_SECONDS:
|
||||
return float(default)
|
||||
return seconds
|
||||
|
||||
# 暖白与医疗绿组成的浅色主题,保持长时间使用时的清晰度与舒适度。
|
||||
BG = "#F2F6F3"
|
||||
@@ -210,186 +194,14 @@ PAGES = (
|
||||
)
|
||||
|
||||
|
||||
class LogQueue:
|
||||
"""把后台线程的标准输出转发到界面,同时留一份带时间戳的磁盘副本。
|
||||
|
||||
界面日志随窗口关闭就没了,出问题时无从回溯——真正卡住发送的那一行往往
|
||||
几分钟前就被刷走了。落盘副本让事后还查得到。
|
||||
"""
|
||||
|
||||
LOG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
|
||||
KEEP_FILES = 20
|
||||
|
||||
def __init__(self, target_queue):
|
||||
self.target_queue = target_queue
|
||||
self._handle = None
|
||||
self._open_log_file()
|
||||
|
||||
def _open_log_file(self):
|
||||
try:
|
||||
os.makedirs(self.LOG_DIR, exist_ok=True)
|
||||
self._prune_old_logs()
|
||||
stamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
self._handle = open(
|
||||
os.path.join(self.LOG_DIR, f"gui_{stamp}.log"),
|
||||
"a",
|
||||
encoding="utf-8",
|
||||
buffering=1,
|
||||
)
|
||||
except Exception:
|
||||
self._handle = None
|
||||
|
||||
def _prune_old_logs(self):
|
||||
try:
|
||||
files = sorted(
|
||||
glob.glob(os.path.join(self.LOG_DIR, "gui_*.log")),
|
||||
key=os.path.getmtime,
|
||||
)
|
||||
for path in files[: max(0, len(files) - self.KEEP_FILES + 1)]:
|
||||
os.unlink(path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def write(self, message):
|
||||
text = str(message).rstrip()
|
||||
if not text:
|
||||
return
|
||||
self.target_queue.put(("log", text))
|
||||
if self._handle is not None:
|
||||
try:
|
||||
self._handle.write(f"{time.strftime('%H:%M:%S')} {text}\n")
|
||||
except Exception:
|
||||
self._handle = None
|
||||
|
||||
def flush(self):
|
||||
if self._handle is not None:
|
||||
try:
|
||||
self._handle.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
if self._handle is not None:
|
||||
try:
|
||||
self._handle.close()
|
||||
except Exception:
|
||||
pass
|
||||
self._handle = None
|
||||
|
||||
|
||||
class BotThread(threading.Thread):
|
||||
"""在后台运行企业微信轮询,避免阻塞 Tk 主线程。"""
|
||||
|
||||
def __init__(self, target_queue, reply_text, poll_seconds,
|
||||
mouse_idle_enabled=True, mouse_idle_seconds=20.0,
|
||||
message_batch_window_seconds=MESSAGE_BATCH_WINDOW_SECONDS):
|
||||
super().__init__(daemon=True)
|
||||
self.target_queue = target_queue
|
||||
self.reply_text = reply_text
|
||||
self.poll_seconds = poll_seconds
|
||||
self.mouse_idle_enabled = mouse_idle_enabled
|
||||
self.mouse_idle_seconds = mouse_idle_seconds
|
||||
self.bot = None
|
||||
self.message_batch_window_seconds = MESSAGE_BATCH_WINDOW_SECONDS
|
||||
self.set_message_batch_window_seconds(message_batch_window_seconds)
|
||||
self.stop_event = threading.Event()
|
||||
|
||||
def set_message_batch_window_seconds(self, value):
|
||||
"""更新下一次消息合并窗口;不会改变已经开始等待的窗口快照。"""
|
||||
seconds = normalize_message_batch_window_seconds(value)
|
||||
self.message_batch_window_seconds = seconds
|
||||
bot = getattr(self, "bot", None)
|
||||
if bot is not None:
|
||||
bot.message_batch_window_seconds = seconds
|
||||
|
||||
def _report_progress(self, text):
|
||||
"""把机器人此刻在做什么送到界面上那行进度文字。"""
|
||||
try:
|
||||
self.target_queue.put(("progress", str(text or "")))
|
||||
except Exception:
|
||||
# 进度提示纯属好看,永远不该把轮询带下去
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
bot = None
|
||||
failed = False
|
||||
try:
|
||||
import wechat_bot as bot_module
|
||||
|
||||
bot_module.AUTO_REPLY_TEXT = self.reply_text
|
||||
bot = bot_module.WeChatBot()
|
||||
self.bot = 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
|
||||
bot._stop_check = self.stop_event
|
||||
bot.safe_window_mode = True
|
||||
bot.auto_activate_window = True
|
||||
bot.progress_cb = self._report_progress
|
||||
|
||||
if not bot.connect(activate=False, wait_if_missing=True):
|
||||
failed = True
|
||||
self.target_queue.put(("status", "error"))
|
||||
return
|
||||
|
||||
print("[+] 窗口激活模式已开启:企业微信未显示时会自动还原到前台")
|
||||
print("[i] 不会设置系统级置顶;仅检测到未读红点后才执行操作")
|
||||
if bot.mouse_idle_enabled:
|
||||
print(
|
||||
f"[+] 人机共存已开启:鼠标静止 "
|
||||
f"{bot.mouse_idle_seconds:.0f} 秒后才自动操作"
|
||||
)
|
||||
print(
|
||||
f"[+] 连续消息合并等待:"
|
||||
f"{bot.message_batch_window_seconds:.0f} 秒"
|
||||
)
|
||||
|
||||
last_ready = bool(bot._window_ready)
|
||||
self.target_queue.put(("status", "running" if last_ready else "waiting"))
|
||||
self.target_queue.put(("info", {
|
||||
"hwnd": f"0x{bot.hwnd:08X}",
|
||||
"size": f"{bot.R - bot.L} x {bot.B - bot.T}",
|
||||
"input": f"({bot.input_x}, {bot.input_y})",
|
||||
}))
|
||||
|
||||
while not self.stop_event.is_set():
|
||||
# GUI 保存后只改变尚未开始的下一轮合并窗口;当前窗口不会被截断。
|
||||
bot.message_batch_window_seconds = self.message_batch_window_seconds
|
||||
bot._poll_once()
|
||||
if bot.security_verification_required:
|
||||
failed = True
|
||||
self.target_queue.put(("status", "verification"))
|
||||
self.stop_event.set()
|
||||
break
|
||||
ready = bool(bot._window_ready)
|
||||
if ready != last_ready:
|
||||
last_ready = ready
|
||||
self.target_queue.put(("status", "running" if ready else "waiting"))
|
||||
if ready:
|
||||
self.target_queue.put(("info", {
|
||||
"hwnd": f"0x{bot.hwnd:08X}",
|
||||
"size": f"{bot.R - bot.L} x {bot.B - bot.T}",
|
||||
"input": f"({bot.input_x}, {bot.input_y})",
|
||||
}))
|
||||
self.target_queue.put(("stats", {
|
||||
"replied": bot.reply_count,
|
||||
"false_pos": len(bot.false_pos_rows),
|
||||
}))
|
||||
self.stop_event.wait(self.poll_seconds)
|
||||
except Exception as exc:
|
||||
failed = True
|
||||
self.target_queue.put((
|
||||
"log",
|
||||
f"[-] 后台线程异常:{exc}\n{traceback.format_exc()}",
|
||||
))
|
||||
self.target_queue.put(("status", "error"))
|
||||
finally:
|
||||
self.bot = None
|
||||
if not failed:
|
||||
self.target_queue.put(("status", "stopped"))
|
||||
|
||||
def stop(self):
|
||||
self.stop_event.set()
|
||||
from gui_runtime import ( # noqa: F401 # 供旧代码与测试通过 wechat_gui 引用
|
||||
MESSAGE_BATCH_WINDOW_MAX_SECONDS,
|
||||
MESSAGE_BATCH_WINDOW_MIN_SECONDS,
|
||||
MESSAGE_BATCH_WINDOW_SECONDS,
|
||||
BotThread,
|
||||
LogQueue,
|
||||
normalize_message_batch_window_seconds,
|
||||
)
|
||||
|
||||
|
||||
class ActionButton(tk.Button):
|
||||
@@ -3918,6 +3730,11 @@ class App(tk.Tk):
|
||||
self._log.configure(state="normal")
|
||||
self._log.insert("end", f"[{timestamp}] ", "dim")
|
||||
self._log.insert("end", str(message) + "\n", tag)
|
||||
# 界面日志只留近况,全量在磁盘副本里;不裁的话跑一整天后每次追加
|
||||
# 都要重排几万行文本,界面越用越卡。
|
||||
overflow = int(self._log.index("end-1c").split(".")[0]) - 2000
|
||||
if overflow > 0:
|
||||
self._log.delete("1.0", f"{overflow + 1}.0")
|
||||
self._log.see("end")
|
||||
self._log.configure(state="disabled")
|
||||
|
||||
@@ -4058,6 +3875,11 @@ def main():
|
||||
qt_main()
|
||||
return
|
||||
|
||||
run_classic_ui()
|
||||
|
||||
|
||||
def run_classic_ui():
|
||||
"""Tk 经典界面;仅在显式要求或 PySide6 不可用时才走到这里。"""
|
||||
startup_result = {}
|
||||
try:
|
||||
import backend_client
|
||||
|
||||
Reference in New Issue
Block a user