diff --git a/wechat_rpa/__pycache__/conversation_store.cpython-311.pyc b/wechat_rpa/__pycache__/conversation_store.cpython-311.pyc index 1ab6c05..5405f8b 100644 Binary files a/wechat_rpa/__pycache__/conversation_store.cpython-311.pyc and b/wechat_rpa/__pycache__/conversation_store.cpython-311.pyc differ diff --git a/wechat_rpa/__pycache__/wechat_bot.cpython-311.pyc b/wechat_rpa/__pycache__/wechat_bot.cpython-311.pyc index c44d330..6af91ed 100644 Binary files a/wechat_rpa/__pycache__/wechat_bot.cpython-311.pyc and b/wechat_rpa/__pycache__/wechat_bot.cpython-311.pyc differ diff --git a/wechat_rpa/__pycache__/wechat_gui.cpython-311.pyc b/wechat_rpa/__pycache__/wechat_gui.cpython-311.pyc index 6e546d9..5664c77 100644 Binary files a/wechat_rpa/__pycache__/wechat_gui.cpython-311.pyc and b/wechat_rpa/__pycache__/wechat_gui.cpython-311.pyc differ diff --git a/wechat_rpa/app_main.py b/wechat_rpa/app_main.py new file mode 100644 index 0000000..dc5bd50 --- /dev/null +++ b/wechat_rpa/app_main.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +"""打包与源码共用的启动入口。 + +关键点:默认(Qt 界面)路径下绝不 import wechat_gui——那个模块顶层就会加载 +tkinter/tcl,冷启动平白多出一大截;tk 经典界面只在显式要求或 PySide6 缺失 +时才加载。 +""" + +import os +import sys + + +def main() -> None: + if "--classic-ui" not in sys.argv and os.environ.get("WECOM_RPA_CLASSIC_UI") != "1": + try: + from wechat_gui_qt import main as qt_main + except ImportError as exc: + if not (exc.name or "").startswith("PySide6"): + raise + else: + qt_main() + return + + from wechat_gui import run_classic_ui + + run_classic_ui() + + +if __name__ == "__main__": + main() diff --git a/wechat_rpa/build_installer.ps1 b/wechat_rpa/build_installer.ps1 index 83a127b..dd26b6a 100644 --- a/wechat_rpa/build_installer.ps1 +++ b/wechat_rpa/build_installer.ps1 @@ -18,7 +18,9 @@ if (-not $SkipAppBuild) { } } -$appExe = Join-Path $projectRoot "dist\ZhenAI-WeCom-Assistant-v$version.exe" +$appName = "ZhenAI-WeCom-Assistant-v$version" +$appDir = Join-Path $projectRoot "dist\$appName" +$appExe = Join-Path $appDir "$appName.exe" if (-not (Test-Path -LiteralPath $appExe)) { throw ("Application EXE was not found: " + $appExe) } @@ -74,7 +76,7 @@ $outputDir = Join-Path $projectRoot "dist\installer" New-Item -ItemType Directory -Path $outputDir -Force | Out-Null Push-Location $projectRoot try { - & $makensis "/V3" "/INPUTCHARSET" "UTF8" ("/DAPP_VERSION=" + $version) ("/DAPP_FILE_VERSION=" + $fileVersion) ("/DSOURCE_EXE=" + $appExe) $nsi + & $makensis "/V3" "/INPUTCHARSET" "UTF8" ("/DAPP_VERSION=" + $version) ("/DAPP_FILE_VERSION=" + $fileVersion) ("/DSOURCE_DIR=" + $appDir) $nsi if ($LASTEXITCODE -ne 0) { throw "Installer compilation failed" } diff --git a/wechat_rpa/build_windows_exe.ps1 b/wechat_rpa/build_windows_exe.ps1 index acc297e..3f8179c 100644 --- a/wechat_rpa/build_windows_exe.ps1 +++ b/wechat_rpa/build_windows_exe.ps1 @@ -22,7 +22,7 @@ function Test-BuildPython([string]$Candidate) { if (-not $Candidate -or -not (Test-Path -LiteralPath $Candidate)) { return $false } - & $Candidate -c "import PyInstaller, PySide6, requests, numpy, PIL, pyautogui, pyperclip, win32gui, mcp" *> $null + & $Candidate -c "import PyInstaller, PySide6, requests, numpy, PIL, pyautogui, pyperclip, win32gui, mcp, rapidocr_onnxruntime" *> $null return $LASTEXITCODE -eq 0 } @@ -61,15 +61,14 @@ finally { Pop-Location } -$exe = Join-Path $projectRoot "dist\ZhenAI-WeCom-Assistant-v$version.exe" +$appName = "ZhenAI-WeCom-Assistant-v$version" +$appDir = Join-Path $projectRoot "dist\$appName" +$exe = Join-Path $appDir "$appName.exe" if (-not (Test-Path -LiteralPath $exe)) { throw ("Build completed but EXE was not found: " + $exe) } -$packageToc = Join-Path $projectRoot "build\wechat_rpa\PKG-00.toc" -if (-not (Test-Path -LiteralPath $packageToc)) { - throw "PyInstaller package manifest was not found" -} +$internalDir = Join-Path $appDir "_internal" $requiredRuntimeFiles = @( "PySide6\QtWebEngineProcess.exe", "PySide6\resources\qtwebengine_resources.pak", @@ -77,9 +76,8 @@ $requiredRuntimeFiles = @( "PySide6\translations\qtwebengine_locales\zh-CN.pak", "VCRUNTIME140.dll" ) -$packageManifest = (Get-Content -LiteralPath $packageToc -Raw).Replace('\\', '\') foreach ($requiredFile in $requiredRuntimeFiles) { - if (-not $packageManifest.Contains($requiredFile)) { + if (-not (Test-Path -LiteralPath (Join-Path $internalDir $requiredFile))) { throw ("Required embedded runtime file is missing: " + $requiredFile) } } @@ -103,5 +101,6 @@ finally { Write-Output "Packaged Qt WebEngine self-check passed" $hash = (Get-FileHash -LiteralPath $exe -Algorithm SHA256).Hash +Write-Output ("App folder: " + $appDir) Write-Output ("EXE: " + $exe) Write-Output ("SHA256: " + $hash) diff --git a/wechat_rpa/gui_runtime.py b/wechat_rpa/gui_runtime.py new file mode 100644 index 0000000..ecc7d2d --- /dev/null +++ b/wechat_rpa/gui_runtime.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- +"""GUI 无关的后台运行时:日志转发与机器人轮询线程。 + +原先住在 wechat_gui.py(Tk 控制台)里,Qt 控制台 import 它时会连带把整个 +tkinter/tcl 拖进进程——打包后的 EXE 每次冷启动都白付这笔钱。抽出来之后两个 +界面共用,谁也不用替对方的依赖买单。 +""" + +import glob +import os +import threading +import time +import traceback + +from runtime_paths import application_data_dir + + +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 + + +class LogQueue: + """把后台线程的标准输出转发到界面,同时留一份带时间戳的磁盘副本。 + + 界面日志随窗口关闭就没了,出问题时无从回溯——真正卡住发送的那一行往往 + 几分钟前就被刷走了。落盘副本让事后还查得到。 + + 日志目录用的是可长期写入的数据目录:打包成 EXE 后 __file__ 指向随进程 + 销毁的解包目录,往那儿写等于退出即丢。 + """ + + LOG_DIR = os.path.join(str(application_data_dir()), "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): + """在后台运行企业微信轮询,避免阻塞界面主线程。""" + + 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() diff --git a/wechat_rpa/installer.nsi b/wechat_rpa/installer.nsi index eb9f5c0..533e4f2 100644 --- a/wechat_rpa/installer.nsi +++ b/wechat_rpa/installer.nsi @@ -10,13 +10,13 @@ Unicode true !define APP_FILE_VERSION "1.0.0.0" !endif -!ifndef SOURCE_EXE - !define SOURCE_EXE "dist\ZhenAI-WeCom-Assistant-v${APP_VERSION}.exe" +!ifndef SOURCE_DIR + !define SOURCE_DIR "dist\ZhenAI-WeCom-Assistant-v${APP_VERSION}" !endif !define APP_NAME "甄AI客服" !define APP_PUBLISHER "甄养堂" -; Qt WebEngine/PyInstaller 单文件程序不能在打包后改名,否则浏览器子进程启动会失败。 +; Qt WebEngine/PyInstaller 程序不能在打包后改名,否则浏览器子进程启动会失败。 !define APP_EXE "ZhenAI-WeCom-Assistant-v${APP_VERSION}.exe" !define APP_DIR_NAME "ZhenAIService" !define UNINSTALL_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\ZhenAIService" @@ -63,9 +63,11 @@ Section "安装甄AI客服" MainSection SetRegView 64 SetOutPath "$INSTDIR" + ; 清掉旧版单文件 EXE 和上一版的运行库目录,避免新旧 DLL 混装。 Delete "$INSTDIR\ZhenAI-WeCom-Assistant-v*.exe" Delete "$INSTDIR\甄AI客服.exe" - File /oname=${APP_EXE} "${SOURCE_EXE}" + RMDir /r "$INSTDIR\_internal" + File /r "${SOURCE_DIR}\*.*" WriteUninstaller "$INSTDIR\卸载甄AI客服.exe" CreateShortCut "$DESKTOP\甄AI客服.lnk" "$INSTDIR\${APP_EXE}" "" "$INSTDIR\${APP_EXE}" 0 @@ -96,6 +98,7 @@ Section "Uninstall" Delete "$INSTDIR\ZhenAI-WeCom-Assistant-v*.exe" Delete "$INSTDIR\甄AI客服.exe" Delete "$INSTDIR\卸载甄AI客服.exe" + RMDir /r "$INSTDIR\_internal" RMDir "$INSTDIR" DeleteRegKey HKCU "${UNINSTALL_KEY}" diff --git a/wechat_rpa/logs/gui_20260731_121001.log b/wechat_rpa/logs/gui_20260731_121001.log deleted file mode 100644 index 8a3b530..0000000 --- a/wechat_rpa/logs/gui_20260731_121001.log +++ /dev/null @@ -1,73 +0,0 @@ -12:10:01 [待回复恢复] 已从磁盘恢复 4 个未完成任务。 -12:10:01 [*] 正在查找企业微信主窗口... -12:10:01 [*] 企业微信存在 2 个同类顶层窗口,已挑选真正渲染了主界面的那一个(其余为子进程空壳窗口)。 -12:10:01 [+] 检测到系统 DPI 缩放比例: 200.0%,启用自适应几何缩放。 -12:10:01 [+] 挂载成功: HWND=0x000308E0, ClassName='WeWorkWindow', Title='企业微信', State='可监听' -12:10:01 窗口坐标: (459,427) → (2715,1745),尺寸: 2256×1318 -12:10:01 动态导航宽度: 320px (置信度 1.00) -12:10:01 会话列表区域: left=779, top=539, 460×1206px -12:10:01 输入框估算坐标: (2154, 1625) -12:10:01 聊天区域: 1420×832px -12:10:01 [+] 窗口激活模式已开启:企业微信未显示时会自动还原到前台 -12:10:01 [i] 不会设置系统级置顶;仅检测到未读红点后才执行操作 -12:10:01 [+] 人机共存已开启:鼠标静止 5 秒后才自动操作 -12:10:01 [+] 连续消息合并等待:2 秒 -12:10:06 [待回复恢复] 找到已读未回复会话,正在按头像和名称复合指纹重新打开。 -12:10:06 [页面校验] 点击前会话列表已变化,已取消这次点击。 -12:10:06 [会话识别] 检测到未读纯色文字头像候选,将先确认消息页与会话身份再回复。 -12:10:06 -[新消息] 正在处理 row0(坐标: 1009, 612) -12:10:39 [待回复恢复] 找到已读未回复会话,正在按头像和名称复合指纹重新打开。 -12:10:39 [页面校验] 点击前会话列表已变化,已取消这次点击。 -12:10:46 [待回复恢复] 找到已读未回复会话,正在按头像和名称复合指纹重新打开。 -12:10:47 [页面校验] 会话列表发生变化,实际打开对象与目标不一致,已停止后续发送。 -12:10:49 [页面校准] 消息侧栏 320px→320px,输入面板顶边 1068px→1012px;会话列表、消息区与输入区域已同步重算。 -12:10:51 [剪贴板] 成功提取 1 行聊天记录(共采集 1 屏 / 去重后 1 行) -12:10:51 -[待回复恢复] 发现已读但没有回复的客户消息,正在恢复本次回复... -12:10:51 -[会话守护] 当前会话仍有已读未回复任务,正在安全重试... -12:10:51 [会话守护] 复制文字未证明有新客户消息,将用视觉确认是否新增媒体。 -12:10:51 [会话守护] 发现客户新消息,先合并连续消息再回复。 -12:10:51 [消息合并] 开始收集本会话 2 秒内的连续消息… -12:10:54 [消息合并] 收集完成(期间检测到 0 次消息画面更新),将只发起 1 次模型请求。 -12:10:55 [剪贴板] 成功提取 1 行聊天记录(共采集 1 屏 / 去重后 1 行) -12:10:56 [档案] 首次遇到该会话,已暂存(1 行可见历史) -12:10:56 [AI] 本次提取的新内容: -你好 -12:10:56 [AI] 媒体/视觉模式,已截取完整聊天消息区域 (7232 bytes) -12:10:56 [AI] 使用视觉模式分析聊天截图... -12:10:56 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/files/upload -12:10:56 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 文件上传(AI 页面守护)","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/files/upload","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:10:56 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/chat-messages -12:10:56 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 视觉 chat-messages","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/chat-messages","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:11:03 [AI] 回复内容: 你好呀,我是贴心管家,有什么想问的您慢慢说 -12:11:04 [发送保护] 检测到人工草稿,已保留原内容并取消自动发送。 -12:11:08 [待回复恢复] 找到已读未回复会话,正在按头像和名称复合指纹重新打开。 -12:11:09 [页面校验] 会话列表发生变化,实际打开对象与目标不一致,已停止后续发送。 -12:11:12 -[会话守护] 当前会话仍有已读未回复任务,正在安全重试... -12:11:13 [剪贴板] 成功提取 1 行聊天记录(共采集 1 屏 / 去重后 1 行) -12:11:13 [会话守护] 复制文字未证明有新客户消息,将用视觉确认是否新增媒体。 -12:11:14 [档案] 首次遇到该会话,已暂存(1 行可见历史) -12:11:14 [AI] 本次提取的新内容: -在不在 -12:11:14 [AI] 媒体/视觉模式,已截取完整聊天消息区域 (6873 bytes) -12:11:14 [AI] 使用视觉模式分析聊天截图... -12:11:14 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/files/upload -12:11:14 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 文件上传(AI 页面守护)","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/files/upload","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:11:14 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/chat-messages -12:11:14 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 视觉 chat-messages","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/chat-messages","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:11:22 [AI] 回复内容: 在呢,您有什么事就直接说,我这边看着呢 -12:11:23 [发送保护] 检测到人工草稿,已保留原内容并取消自动发送。 -12:11:27 [待回复恢复] 找到已读未回复会话,正在按头像和名称复合指纹重新打开。 -12:11:28 [消息合并] 开始收集本会话 2 秒内的连续消息… -12:11:30 [消息合并] 收集完成(期间检测到 0 次消息画面更新),将只发起 1 次模型请求。 -12:11:31 [人手] 检测到鼠标操作,暂停自动回复,还需静止 5s… -12:11:32 [AI] 媒体/视觉模式,已截取完整聊天消息区域 (33368 bytes) -12:11:32 [AI] 使用视觉模式分析聊天截图... -12:11:32 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/files/upload -12:11:32 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 文件上传(AI 页面守护)","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/files/upload","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:11:32 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/chat-messages -12:11:32 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 视觉 chat-messages","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/chat-messages","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:11:39 [AI] 回复内容: 看到了,是打卡异常提醒。您是想问怎么补卡吗? diff --git a/wechat_rpa/logs/gui_20260731_122543.log b/wechat_rpa/logs/gui_20260731_122543.log deleted file mode 100644 index 23092f2..0000000 --- a/wechat_rpa/logs/gui_20260731_122543.log +++ /dev/null @@ -1,260 +0,0 @@ -12:25:43 [待回复恢复] 已从磁盘恢复 6 个未完成任务。 -12:25:43 [*] 正在查找企业微信主窗口... -12:25:43 [*] 企业微信存在 2 个同类顶层窗口,已挑选真正渲染了主界面的那一个(其余为子进程空壳窗口)。 -12:25:43 [+] 检测到系统 DPI 缩放比例: 200.0%,启用自适应几何缩放。 -12:25:43 [+] 挂载成功: HWND=0x000308E0, ClassName='WeWorkWindow', Title='企业微信', State='可监听' -12:25:43 窗口坐标: (459,427) → (2715,1745),尺寸: 2256×1318 -12:25:43 动态导航宽度: 320px (置信度 1.00) -12:25:43 会话列表区域: left=779, top=539, 460×1206px -12:25:43 输入框估算坐标: (2154, 1625) -12:25:43 聊天区域: 1420×832px -12:25:43 [+] 窗口激活模式已开启:企业微信未显示时会自动还原到前台 -12:25:43 [i] 不会设置系统级置顶;仅检测到未读红点后才执行操作 -12:25:43 [+] 人机共存已开启:鼠标静止 5 秒后才自动操作 -12:25:43 [+] 连续消息合并等待:2 秒 -12:25:47 [待回复恢复] 找到已读未回复会话,正在按头像和名称复合指纹重新打开。 -12:25:47 [页面校验] 点击前会话列表已变化,已取消这次点击。 -12:25:57 [待回复恢复] 找到已读未回复会话,正在按头像和名称复合指纹重新打开。 -12:25:57 [页面校验] 点击前会话列表已变化,已取消这次点击。 -12:26:17 [待回复恢复] 找到已读未回复会话,正在按头像和名称复合指纹重新打开。 -12:26:17 [页面校验] 点击前会话列表已变化,已取消这次点击。 -12:26:17 -[新消息] 正在处理 row0(坐标: 1009, 612) -12:26:18 [页面校验] 会话列表发生变化,实际打开对象与目标不一致,已停止后续发送。 -12:26:18 [页面校验] 未能可靠打开目标会话,本轮停止,等待下次重新识别。 -12:26:20 [页面校准] 消息侧栏 320px→320px,输入面板顶边 1068px→1012px;会话列表、消息区与输入区域已同步重算。 -12:26:21 -[会话守护] 当前会话仍有已读未回复任务,正在安全重试... -12:26:22 [剪贴板] 成功提取 1 行聊天记录(共采集 1 屏 / 去重后 1 行) -12:26:22 [会话守护] 复制文字未证明有新客户消息,将用视觉确认是否新增媒体。 -12:26:23 [档案] 首次遇到该会话,已暂存(1 行可见历史) -12:26:23 [AI] 本次提取的新内容: -在不在 -12:26:23 [AI] 媒体/视觉模式,已截取完整聊天消息区域 (6751 bytes) -12:26:23 [AI] 使用视觉模式分析聊天截图... -12:26:23 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/files/upload -12:26:23 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 文件上传(AI 页面守护)","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/files/upload","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:26:24 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/chat-messages -12:26:24 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 视觉 chat-messages","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/chat-messages","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:26:34 [AI] 回复内容: 在呢,您有什么事慢慢说,我这边听着呢 -12:26:35 [发送保护] 检测到人工草稿,已保留原内容并取消自动发送。 -12:26:41 [待回复恢复] 找到已读未回复会话,正在按头像和名称复合指纹重新打开。 -12:26:41 [页面校验] 点击前会话列表已变化,已取消这次点击。 -12:26:44 -[会话守护] 当前会话仍有已读未回复任务,正在安全重试... -12:26:45 [剪贴板] 成功提取 1 行聊天记录(共采集 1 屏 / 去重后 1 行) -12:26:45 [会话守护] 复制文字未证明有新客户消息,将用视觉确认是否新增媒体。 -12:26:46 [档案] 首次遇到该会话,已暂存(1 行可见历史) -12:26:46 [AI] 本次提取的新内容: -在不在 -12:26:46 [AI] 媒体/视觉模式,已截取完整聊天消息区域 (6751 bytes) -12:26:46 [AI] 使用视觉模式分析聊天截图... -12:26:46 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/files/upload -12:26:46 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 文件上传(AI 页面守护)","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/files/upload","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:26:46 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/chat-messages -12:26:46 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 视觉 chat-messages","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/chat-messages","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:26:56 [AI] 回复内容: 在呢,您有什么事直接跟我说就行,我这边看着呢 -12:26:57 [发送保护] 检测到人工草稿,已保留原内容并取消自动发送。 -12:27:02 -[会话守护] 当前会话仍有已读未回复任务,正在安全重试... -12:27:04 [剪贴板] 成功提取 1 行聊天记录(共采集 1 屏 / 去重后 1 行) -12:27:04 [会话守护] 复制文字未证明有新客户消息,将用视觉确认是否新增媒体。 -12:27:04 [档案] 首次遇到该会话,已暂存(1 行可见历史) -12:27:05 [AI] 本次提取的新内容: -在不在 -12:27:05 [AI] 媒体/视觉模式,已截取完整聊天消息区域 (6751 bytes) -12:27:05 [AI] 使用视觉模式分析聊天截图... -12:27:05 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/files/upload -12:27:05 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 文件上传(AI 页面守护)","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/files/upload","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:27:05 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/chat-messages -12:27:05 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 视觉 chat-messages","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/chat-messages","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:27:11 [AI] 回复内容: 在呢,您说,我这边听着 -12:27:12 [发送保护] 检测到人工草稿,已保留原内容并取消自动发送。 -12:27:15 -[会话守护] 当前会话仍有已读未回复任务,正在安全重试... -12:27:17 [剪贴板] 成功提取 1 行聊天记录(共采集 1 屏 / 去重后 1 行) -12:27:17 [会话守护] 复制文字未证明有新客户消息,将用视觉确认是否新增媒体。 -12:27:18 [档案] 首次遇到该会话,已暂存(1 行可见历史) -12:27:18 [AI] 本次提取的新内容: -在不在 -12:27:18 [AI] 媒体/视觉模式,已截取完整聊天消息区域 (6751 bytes) -12:27:18 [AI] 使用视觉模式分析聊天截图... -12:27:18 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/files/upload -12:27:18 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 文件上传(AI 页面守护)","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/files/upload","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:27:18 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/chat-messages -12:27:18 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 视觉 chat-messages","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/chat-messages","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:27:26 [AI] 回复内容: 在呢,您有什么事直接跟我说就行,我这边看着呢 -12:27:27 [发送保护] 检测到人工草稿,已保留原内容并取消自动发送。 -12:27:30 [待回复恢复] 找到已读未回复会话,正在按头像和名称复合指纹重新打开。 -12:27:30 [页面校验] 点击前会话列表已变化,已取消这次点击。 -12:27:33 -[会话守护] 当前会话仍有已读未回复任务,正在安全重试... -12:27:34 [剪贴板] 成功提取 1 行聊天记录(共采集 1 屏 / 去重后 1 行) -12:27:34 [会话守护] 复制文字未证明有新客户消息,将用视觉确认是否新增媒体。 -12:27:35 [档案] 首次遇到该会话,已暂存(1 行可见历史) -12:27:35 [AI] 本次提取的新内容: -在不在 -12:27:35 [AI] 媒体/视觉模式,已截取完整聊天消息区域 (6751 bytes) -12:27:35 [AI] 使用视觉模式分析聊天截图... -12:27:35 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/files/upload -12:27:35 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 文件上传(AI 页面守护)","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/files/upload","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:27:35 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/chat-messages -12:27:35 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 视觉 chat-messages","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/chat-messages","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:27:43 [AI] 回复内容: 在呢,您慢慢说,有什么事需要我帮您处理? -12:27:44 [发送保护] 检测到人工草稿,已保留原内容并取消自动发送。 -12:27:50 [待回复恢复] 找到已读未回复会话,正在按头像和名称复合指纹重新打开。 -12:27:50 [页面校验] 点击前会话列表已变化,已取消这次点击。 -12:27:50 [排队] 当前会话已占用 69 秒仍未回完,先放行其他会话,稍后再回来处理它。 -12:27:53 -[会话守护] 当前会话仍有已读未回复任务,正在安全重试... -12:27:54 [剪贴板] 成功提取 1 行聊天记录(共采集 1 屏 / 去重后 1 行) -12:27:54 [会话守护] 复制文字未证明有新客户消息,将用视觉确认是否新增媒体。 -12:27:55 [档案] 首次遇到该会话,已暂存(1 行可见历史) -12:27:55 [AI] 本次提取的新内容: -在不在 -12:27:55 [AI] 媒体/视觉模式,已截取完整聊天消息区域 (6751 bytes) -12:27:55 [AI] 使用视觉模式分析聊天截图... -12:27:55 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/files/upload -12:27:55 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 文件上传(AI 页面守护)","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/files/upload","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:27:55 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/chat-messages -12:27:55 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 视觉 chat-messages","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/chat-messages","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:28:02 [AI] 回复内容: 在呢,您慢慢说,想问什么直接跟我讲就行 -12:28:03 [发送保护] 检测到人工草稿,已保留原内容并取消自动发送。 -12:28:08 -[会话守护] 当前会话仍有已读未回复任务,正在安全重试... -12:28:09 [剪贴板] 成功提取 1 行聊天记录(共采集 1 屏 / 去重后 1 行) -12:28:09 [会话守护] 复制文字未证明有新客户消息,将用视觉确认是否新增媒体。 -12:28:10 [档案] 首次遇到该会话,已暂存(1 行可见历史) -12:28:10 [AI] 本次提取的新内容: -在不在 -12:28:10 [AI] 媒体/视觉模式,已截取完整聊天消息区域 (6751 bytes) -12:28:10 [AI] 使用视觉模式分析聊天截图... -12:28:10 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/files/upload -12:28:10 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 文件上传(AI 页面守护)","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/files/upload","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:28:10 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/chat-messages -12:28:10 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 视觉 chat-messages","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/chat-messages","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:28:22 [AI] 回复内容: 在呢,您有什么事直接跟我说,我这边听着 -12:28:24 [发送保护] 检测到人工草稿,已保留原内容并取消自动发送。 -12:28:27 -[会话守护] 当前会话仍有已读未回复任务,正在安全重试... -12:28:28 [剪贴板] 成功提取 1 行聊天记录(共采集 1 屏 / 去重后 1 行) -12:28:28 [会话守护] 复制文字未证明有新客户消息,将用视觉确认是否新增媒体。 -12:28:29 [档案] 首次遇到该会话,已暂存(1 行可见历史) -12:28:29 [AI] 本次提取的新内容: -在不在 -12:28:29 [AI] 媒体/视觉模式,已截取完整聊天消息区域 (6751 bytes) -12:28:29 [AI] 使用视觉模式分析聊天截图... -12:28:29 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/files/upload -12:28:29 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 文件上传(AI 页面守护)","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/files/upload","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:28:29 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/chat-messages -12:28:29 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 视觉 chat-messages","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/chat-messages","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:28:35 [AI] 回复内容: 在呢,您有什么事直接跟我说,我这边看着呢 -12:28:36 [发送保护] 检测到人工草稿,已保留原内容并取消自动发送。 -12:28:40 -[会话守护] 当前会话仍有已读未回复任务,正在安全重试... -12:28:41 [剪贴板] 成功提取 1 行聊天记录(共采集 1 屏 / 去重后 1 行) -12:28:41 [会话守护] 复制文字未证明有新客户消息,将用视觉确认是否新增媒体。 -12:28:42 [档案] 首次遇到该会话,已暂存(1 行可见历史) -12:28:42 [AI] 本次提取的新内容: -在不在 -12:28:42 [AI] 媒体/视觉模式,已截取完整聊天消息区域 (6751 bytes) -12:28:42 [AI] 使用视觉模式分析聊天截图... -12:28:42 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/files/upload -12:28:42 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 文件上传(AI 页面守护)","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/files/upload","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:28:42 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/chat-messages -12:28:42 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 视觉 chat-messages","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/chat-messages","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:28:49 [AI] 回复内容: 在呢,您有什么事直接说就行,我这边听着呢 -12:28:50 [发送保护] 检测到人工草稿,已保留原内容并取消自动发送。 -12:28:54 -[会话守护] 当前会话仍有已读未回复任务,正在安全重试... -12:28:55 [剪贴板] 成功提取 1 行聊天记录(共采集 1 屏 / 去重后 1 行) -12:28:55 [会话守护] 复制文字未证明有新客户消息,将用视觉确认是否新增媒体。 -12:28:56 [档案] 首次遇到该会话,已暂存(1 行可见历史) -12:28:56 [AI] 本次提取的新内容: -在不在 -12:28:56 [AI] 媒体/视觉模式,已截取完整聊天消息区域 (6751 bytes) -12:28:56 [AI] 使用视觉模式分析聊天截图... -12:28:56 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/files/upload -12:28:56 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 文件上传(AI 页面守护)","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/files/upload","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:28:56 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/chat-messages -12:28:56 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 视觉 chat-messages","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/chat-messages","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:29:06 [AI] 回复内容: 在呢,您慢慢说,想问什么我这边听着呢 -12:29:07 [发送保护] 检测到人工草稿,已保留原内容并取消自动发送。 -12:29:10 -[会话守护] 当前会话仍有已读未回复任务,正在安全重试... -12:29:11 [剪贴板] 成功提取 1 行聊天记录(共采集 1 屏 / 去重后 1 行) -12:29:11 [会话守护] 复制文字未证明有新客户消息,将用视觉确认是否新增媒体。 -12:29:12 [档案] 首次遇到该会话,已暂存(1 行可见历史) -12:29:12 [AI] 本次提取的新内容: -在不在 -12:29:12 [AI] 媒体/视觉模式,已截取完整聊天消息区域 (6751 bytes) -12:29:12 [AI] 使用视觉模式分析聊天截图... -12:29:12 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/files/upload -12:29:12 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 文件上传(AI 页面守护)","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/files/upload","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:29:12 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/chat-messages -12:29:12 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 视觉 chat-messages","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/chat-messages","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:29:22 [AI] 回复内容: 在呢,您有什么事就说,我这边看着呢 -12:29:23 [发送保护] 检测到人工草稿,已保留原内容并取消自动发送。 -12:29:27 -[会话守护] 当前会话仍有已读未回复任务,正在安全重试... -12:29:28 [剪贴板] 成功提取 1 行聊天记录(共采集 1 屏 / 去重后 1 行) -12:29:28 [会话守护] 复制文字未证明有新客户消息,将用视觉确认是否新增媒体。 -12:29:29 [档案] 首次遇到该会话,已暂存(1 行可见历史) -12:29:29 [AI] 本次提取的新内容: -在不在 -12:29:29 [AI] 媒体/视觉模式,已截取完整聊天消息区域 (6751 bytes) -12:29:29 [AI] 使用视觉模式分析聊天截图... -12:29:29 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/files/upload -12:29:29 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 文件上传(AI 页面守护)","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/files/upload","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:29:29 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/chat-messages -12:29:29 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 视觉 chat-messages","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/chat-messages","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:29:36 [AI] 回复内容: 在呢,您慢慢说,有什么事需要我帮您处理? -12:29:37 [发送保护] 检测到人工草稿,已保留原内容并取消自动发送。 -12:29:40 [待回复恢复] 找到已读未回复会话,正在按头像和名称复合指纹重新打开。 -12:29:41 [页面校验] 点击前会话列表已变化,已取消这次点击。 -12:29:43 -[会话守护] 当前会话仍有已读未回复任务,正在安全重试... -12:29:44 [剪贴板] 成功提取 1 行聊天记录(共采集 1 屏 / 去重后 1 行) -12:29:45 [会话守护] 复制文字未证明有新客户消息,将用视觉确认是否新增媒体。 -12:29:45 [档案] 首次遇到该会话,已暂存(1 行可见历史) -12:29:45 [AI] 本次提取的新内容: -在不在 -12:29:45 [AI] 媒体/视觉模式,已截取完整聊天消息区域 (6751 bytes) -12:29:45 [AI] 使用视觉模式分析聊天截图... -12:29:45 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/files/upload -12:29:45 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 文件上传(AI 页面守护)","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/files/upload","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:29:46 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/chat-messages -12:29:46 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 视觉 chat-messages","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/chat-messages","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:29:54 [AI] 回复内容: 在呢,我这边看着消息,您有什么事直接说就行 -12:29:55 [发送保护] 检测到人工草稿,已保留原内容并取消自动发送。 -12:30:02 [待回复恢复] 找到已读未回复会话,正在按头像和名称复合指纹重新打开。 -12:30:03 [页面校验] 点击前会话列表已变化,已取消这次点击。 -12:30:05 -[会话守护] 当前会话仍有已读未回复任务,正在安全重试... -12:30:06 [剪贴板] 成功提取 1 行聊天记录(共采集 1 屏 / 去重后 1 行) -12:30:07 [会话守护] 复制文字未证明有新客户消息,将用视觉确认是否新增媒体。 -12:30:07 [档案] 首次遇到该会话,已暂存(1 行可见历史) -12:30:07 [AI] 本次提取的新内容: -在不在 -12:30:07 [AI] 媒体/视觉模式,已截取完整聊天消息区域 (6751 bytes) -12:30:07 [AI] 使用视觉模式分析聊天截图... -12:30:07 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/files/upload -12:30:07 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 文件上传(AI 页面守护)","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/files/upload","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:30:08 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/chat-messages -12:30:08 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 视觉 chat-messages","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/chat-messages","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:30:16 [AI] 回复内容: 在呢,您慢慢说,有什么事需要我帮您处理? -12:30:17 [发送保护] 检测到人工草稿,已保留原内容并取消自动发送。 -12:30:22 -[会话守护] 当前会话仍有已读未回复任务,正在安全重试... -12:30:23 [剪贴板] 成功提取 1 行聊天记录(共采集 1 屏 / 去重后 1 行) -12:30:23 [会话守护] 复制文字未证明有新客户消息,将用视觉确认是否新增媒体。 -12:30:24 [档案] 首次遇到该会话,已暂存(1 行可见历史) -12:30:24 [AI] 本次提取的新内容: -在不在 -12:30:24 [AI] 媒体/视觉模式,已截取完整聊天消息区域 (6751 bytes) -12:30:24 [AI] 使用视觉模式分析聊天截图... -12:30:24 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/files/upload -12:30:24 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 文件上传(AI 页面守护)","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/files/upload","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:30:24 [开发模式] 模型请求地址: http://ai.zhenyangtang.com.cn/v1/chat-messages -12:30:24 [开发模式] 本次模型配置(聊天内容与敏感值未输出): {"服务类型":"dify","调用协议":"Dify 视觉 chat-messages","API 基础地址":"http://ai.zhenyangtang.com.cn/v1","实际请求地址":"http://ai.zhenyangtang.com.cn/v1/chat-messages","模型名称":"gpt-5.6-sol","API Key":"[已配置,值已隐藏]","请求超时(秒)":120,"最大回复 tokens":500,"温度":0.35,"始终视觉模式":true,"媒体消息自动视觉":true,"AI 页面守护":true,"上下文":true,"MCP 工具":false} -12:30:35 [AI] 回复内容: 在呢,我这会儿在线,您想咨询什么事情? diff --git a/wechat_rpa/logs/gui_20260803_100646.log b/wechat_rpa/logs/gui_20260803_100646.log new file mode 100644 index 0000000..8d1c480 --- /dev/null +++ b/wechat_rpa/logs/gui_20260803_100646.log @@ -0,0 +1,2 @@ +10:06:46 [*] 正在查找企业微信主窗口... +10:06:46 [-] 未找到企业微信!请确认客户端已经登录并正在运行。 diff --git a/wechat_rpa/logs/gui_20260803_100648.log b/wechat_rpa/logs/gui_20260803_100648.log new file mode 100644 index 0000000..b88c77d --- /dev/null +++ b/wechat_rpa/logs/gui_20260803_100648.log @@ -0,0 +1,2 @@ +10:06:48 [*] 正在查找企业微信主窗口... +10:06:48 [-] 未找到企业微信!请确认客户端已经登录并正在运行。 diff --git a/wechat_rpa/media/6d65646961303031_1785721795667953700.png b/wechat_rpa/media/6d65646961303031_1785721795667953700.png new file mode 100644 index 0000000..b0bf51f --- /dev/null +++ b/wechat_rpa/media/6d65646961303031_1785721795667953700.png @@ -0,0 +1 @@ +chat-png \ No newline at end of file diff --git a/wechat_rpa/media/6d65646961303035_1785721795879391800.png b/wechat_rpa/media/6d65646961303035_1785721795879391800.png new file mode 100644 index 0000000..b0bf51f --- /dev/null +++ b/wechat_rpa/media/6d65646961303035_1785721795879391800.png @@ -0,0 +1 @@ +chat-png \ No newline at end of file diff --git a/wechat_rpa/media/7261636530303031_1785721795731719300.png b/wechat_rpa/media/7261636530303031_1785721795731719300.png new file mode 100644 index 0000000..b0bf51f --- /dev/null +++ b/wechat_rpa/media/7261636530303031_1785721795731719300.png @@ -0,0 +1 @@ +chat-png \ No newline at end of file diff --git a/wechat_rpa/media/766f696365303032_1785721796287875500.png b/wechat_rpa/media/766f696365303032_1785721796287875500.png new file mode 100644 index 0000000..b0bf51f --- /dev/null +++ b/wechat_rpa/media/766f696365303032_1785721796287875500.png @@ -0,0 +1 @@ +chat-png \ No newline at end of file diff --git a/wechat_rpa/media/766f696365303033_1785721795931864700.png b/wechat_rpa/media/766f696365303033_1785721795931864700.png new file mode 100644 index 0000000..b0bf51f --- /dev/null +++ b/wechat_rpa/media/766f696365303033_1785721795931864700.png @@ -0,0 +1 @@ +chat-png \ No newline at end of file diff --git a/wechat_rpa/qt-ui-smoke-1.png b/wechat_rpa/qt-ui-smoke-1.png new file mode 100644 index 0000000..afe0b5e Binary files /dev/null and b/wechat_rpa/qt-ui-smoke-1.png differ diff --git a/wechat_rpa/qt-ui-smoke-2.png b/wechat_rpa/qt-ui-smoke-2.png new file mode 100644 index 0000000..a74f89b Binary files /dev/null and b/wechat_rpa/qt-ui-smoke-2.png differ diff --git a/wechat_rpa/qt-ui-smoke-3.png b/wechat_rpa/qt-ui-smoke-3.png new file mode 100644 index 0000000..4e7e687 Binary files /dev/null and b/wechat_rpa/qt-ui-smoke-3.png differ diff --git a/wechat_rpa/qt-ui-smoke-4.png b/wechat_rpa/qt-ui-smoke-4.png new file mode 100644 index 0000000..7a4ff0c Binary files /dev/null and b/wechat_rpa/qt-ui-smoke-4.png differ diff --git a/wechat_rpa/qt-ui-smoke-5.png b/wechat_rpa/qt-ui-smoke-5.png new file mode 100644 index 0000000..293f6e6 Binary files /dev/null and b/wechat_rpa/qt-ui-smoke-5.png differ diff --git a/wechat_rpa/qt-ui-smoke-6.png b/wechat_rpa/qt-ui-smoke-6.png new file mode 100644 index 0000000..0933198 Binary files /dev/null and b/wechat_rpa/qt-ui-smoke-6.png differ diff --git a/wechat_rpa/qt-ui-smoke-7.png b/wechat_rpa/qt-ui-smoke-7.png new file mode 100644 index 0000000..ce79be5 Binary files /dev/null and b/wechat_rpa/qt-ui-smoke-7.png differ diff --git a/wechat_rpa/requirements.txt b/wechat_rpa/requirements.txt index e401607..f8fc58d 100644 --- a/wechat_rpa/requirements.txt +++ b/wechat_rpa/requirements.txt @@ -8,3 +8,5 @@ pywin32>=306 requests>=2.31.0 mcp[cli]>=1.0.0 PySide6>=6.8,<6.11 +# 会话身份靠昵称 OCR;构建环境缺了它,打出来的包会静默失去识别能力 +rapidocr-onnxruntime>=1.4.4,<1.5 diff --git a/wechat_rpa/session_name.py b/wechat_rpa/session_name.py index 780b745..10e8a3c 100644 --- a/wechat_rpa/session_name.py +++ b/wechat_rpa/session_name.py @@ -131,7 +131,15 @@ class NameReader: try: from rapidocr_onnxruntime import RapidOCR - self._engine = RapidOCR() + try: + # onnxruntime 默认按核数开满线程池且空转抢 CPU。识别的 + # 都是几十像素高的小裁片,两个线程绰绰有余;不限的话 + # 低配机每轮轮询都会被 OCR 抖一下。 + self._engine = RapidOCR( + intra_op_num_threads=2, inter_op_num_threads=1 + ) + except Exception: + self._engine = RapidOCR() except Exception as exc: self._engine_failed = True print(f" [身份] OCR 引擎不可用,无法读取会话昵称: {exc}") diff --git a/wechat_rpa/wechat_gui.py b/wechat_rpa/wechat_gui.py index dbc698e..3378d15 100644 --- a/wechat_rpa/wechat_gui.py +++ b/wechat_rpa/wechat_gui.py @@ -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 diff --git a/wechat_rpa/wechat_gui_qt.py b/wechat_rpa/wechat_gui_qt.py index 556fdf1..6957a51 100644 --- a/wechat_rpa/wechat_gui_qt.py +++ b/wechat_rpa/wechat_gui_qt.py @@ -15,6 +15,7 @@ from pathlib import Path from PySide6.QtCore import ( QEasingCurve, QEvent, + QObject, QPoint, QPointF, QPropertyAnimation, @@ -74,26 +75,21 @@ from app_version import APP_VERSION, release_status from runtime_paths import application_data_dir, resource_path -try: - import __main__ as _legacy - - if not hasattr(_legacy, "BotThread"): - raise ImportError -except ImportError: - import wechat_gui as _legacy - - -BotThread = _legacy.BotThread -LogQueue = _legacy.LogQueue -MESSAGE_BATCH_WINDOW_SECONDS = getattr( - _legacy, "MESSAGE_BATCH_WINDOW_SECONDS", 20.0 +# 直接用 GUI 无关的共享运行时;过去 import wechat_gui 会连带把整个 +# tkinter/tcl 拖进 Qt 进程,冷启动白付一笔加载费。 +from gui_runtime import ( + BotThread, + LogQueue, + normalize_message_batch_window_seconds, ) -MESSAGE_BATCH_WINDOW_MIN_SECONDS = int(getattr( - _legacy, "MESSAGE_BATCH_WINDOW_MIN_SECONDS", 1.0 -)) -MESSAGE_BATCH_WINDOW_MAX_SECONDS = int(getattr( - _legacy, "MESSAGE_BATCH_WINDOW_MAX_SECONDS", 120.0 -)) +from gui_runtime import ( + MESSAGE_BATCH_WINDOW_SECONDS, + MESSAGE_BATCH_WINDOW_MAX_SECONDS as _BATCH_MAX, + MESSAGE_BATCH_WINDOW_MIN_SECONDS as _BATCH_MIN, +) + +MESSAGE_BATCH_WINDOW_MIN_SECONDS = int(_BATCH_MIN) +MESSAGE_BATCH_WINDOW_MAX_SECONDS = int(_BATCH_MAX) SCRIPT_DIR = application_data_dir() APP_SETTINGS_FILE = SCRIPT_DIR / "app_settings.json" CUSTOMER_SERVICE_URL = "http://kf.zhenyangtang.com.cn/" @@ -385,18 +381,37 @@ class PortalWebView(QWebEngineView): class PortalPage(QWidget): + """AI 客服网页页。 + + WebEngine 视图按需创建:Chromium 子进程初始化在低配机上要好几秒、常驻 + 两三百 MB 内存,构造期就拉起会拖慢整个窗口的首帧。 + """ + def __init__(self, parent: QWidget | None = None): super().__init__(parent) self.setObjectName("PageRoot") - layout = QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) + self._layout = QVBoxLayout(self) + self._layout.setContentsMargins(0, 0, 0, 0) + self._layout.setSpacing(0) self.progress = QProgressBar() self.progress.setRange(0, 100) self.progress.hide() - layout.addWidget(self.progress) + self._layout.addWidget(self.progress) + self._placeholder = QLabel("正在准备 AI 客服页面…") + self._placeholder.setAlignment(Qt.AlignCenter) + self._placeholder.setStyleSheet("color:#617269;font-size:14px;background:#f4f7f5;") + self._layout.addWidget(self._placeholder, 1) + + self.view: PortalWebView | None = None + self.page: QWebEnginePage | None = None + self.profile: QWebEngineProfile | None = None + + def ensure_view(self) -> None: + """真正创建 WebEngine 视图;重复调用无副作用。""" + if self.view is not None: + return self.view = PortalWebView() self.view.setFocusPolicy(Qt.StrongFocus) self.view.settings().setAttribute( @@ -421,7 +436,8 @@ class PortalPage(QWidget): self.view.loadStarted.connect(self._load_started) self.view.loadProgress.connect(self.progress.setValue) self.view.loadFinished.connect(self._load_finished) - layout.addWidget(self.view, 1) + self._placeholder.hide() + self._layout.addWidget(self.view, 1) if os.environ.get("WECOM_RPA_DISABLE_PORTAL") != "1": self.view.setUrl(QUrl(CUSTOMER_SERVICE_URL)) @@ -432,6 +448,10 @@ class PortalPage(QWidget): "AI 客服网页在测试模式下未加载" ) + def focus_view(self) -> None: + if self.view is not None: + self.view.setFocus(Qt.OtherFocusReason) + def _install_light_theme(self) -> None: css_path = resource_path("edge_light_theme", "light-theme.css") try: @@ -471,10 +491,14 @@ class PortalPage(QWidget): self.view.setFocus(Qt.OtherFocusReason) def reload(self) -> None: - self.view.reload() + if self.view is None: + self.ensure_view() + else: + self.view.reload() def open_external(self) -> None: - QDesktopServices.openUrl(self.view.url() or QUrl(CUSTOMER_SERVICE_URL)) + url = self.view.url() if self.view is not None else QUrl() + QDesktopServices.openUrl(url if url and not url.isEmpty() else QUrl(CUSTOMER_SERVICE_URL)) class MetricCard(QFrame): @@ -1689,6 +1713,9 @@ class LogPage(QWidget): self.editor = QTextEdit() self.editor.setReadOnly(True) self.editor.setAcceptRichText(True) + # 机器人每轮轮询都在打日志,跑一天就是几万块富文本;不封顶的话文档 + # 越长每次追加越慢,整个界面跟着卡。磁盘副本是全量的,界面只留近况。 + self.editor.document().setMaximumBlockCount(2000) self.editor.setStyleSheet( "QTextEdit{font-family:'Cascadia Mono','Microsoft YaHei UI';font-size:13px;line-height:1.5;}" ) @@ -2193,7 +2220,7 @@ class MainWindow(QMainWindow): settings["poll_interval"] = max(0.2, float(settings["poll_interval"])) settings["mouse_idle_seconds"] = max(0.0, float(settings["mouse_idle_seconds"])) settings["message_batch_window_seconds"] = ( - _legacy.normalize_message_batch_window_seconds( + normalize_message_batch_window_seconds( settings["message_batch_window_seconds"] ) ) @@ -2234,7 +2261,8 @@ class MainWindow(QMainWindow): if index == 5: self.queue_page.refresh_data() if index == 0: - QTimer.singleShot(80, lambda: self.portal_page.view.setFocus(Qt.OtherFocusReason)) + self.portal_page.ensure_view() + QTimer.singleShot(80, self.portal_page.focus_view) def _refresh_queue_page(self) -> None: """队列页开着的时候,让它跟着机器人一起动。""" @@ -2303,9 +2331,11 @@ class MainWindow(QMainWindow): previous_state = event.oldState() saved_geometry = self.saveGeometry() was_maximized = bool(previous_state & Qt.WindowMaximized) + portal_view = self.portal_page.view portal_was_visible = bool( self.stack.currentIndex() == 0 - and not self.portal_page.view.isHidden() + and portal_view is not None + and not portal_view.isHidden() ) QTimer.singleShot( 0, @@ -2355,12 +2385,13 @@ class MainWindow(QMainWindow): # window while that surface is still visible can leave a large black DWM # window above WeCom on some Windows/GPU combinations. Tear down the # visible surface first and let Qt flush that state before hiding us. + portal_view = self.portal_page.view self._portal_was_visible = ( - self.portal_page.view.isVisible() + portal_view is not None and portal_view.isVisible() if portal_was_visible is None else bool(portal_was_visible) ) - if self._portal_was_visible: + if self._portal_was_visible and self.portal_page.view is not None: self.portal_page.view.hide() QApplication.processEvents() self.capsule.show_near(self) @@ -2378,8 +2409,9 @@ class MainWindow(QMainWindow): self.activateWindow() if getattr(self, "_portal_was_visible", False) and self.stack.currentIndex() == 0: def restore_portal() -> None: - self.portal_page.view.show() - self.portal_page.view.setFocus(Qt.OtherFocusReason) + if self.portal_page.view is not None: + self.portal_page.view.show() + self.portal_page.view.setFocus(Qt.OtherFocusReason) QTimer.singleShot(80, restore_portal) self._portal_was_visible = False @@ -2564,6 +2596,45 @@ def handle_startup_update(release: object) -> bool: return not forced +class _StartupSyncBridge(QObject): + finished = Signal(object) + + +def _start_background_startup_sync(window: "MainWindow") -> None: + """云端配置同步放到后台线程执行。 + + 过去它在窗口出现之前同步跑,网络一慢冷启动就跟着慢(超时 3 秒起步、 + DNS 卡住时更久)。现在窗口先出来,结果回来后再补日志和升级提示。 + """ + bridge = _StartupSyncBridge(window) + + def deliver(result: object) -> None: + payload = result if isinstance(result, dict) else {} + for diagnostic in payload.get("diagnostics") or []: + window.append_log(str(diagnostic), "notify") + if not handle_startup_update(payload.get("release")): + window.close() + + bridge.finished.connect(deliver) + + def worker() -> None: + try: + import backend_client + + result = backend_client.startup_sync_config(timeout=3.0) + except Exception: + # 云端暂时不可用时仍执行上次成功同步的强制升级策略。 + try: + import backend_client + + result = {"release": backend_client.cached_release_status()} + except Exception: + result = {} + bridge.finished.emit(result or {}) + + threading.Thread(target=worker, daemon=True, name="startup-sync").start() + + def run_packaging_self_check(app: QApplication) -> int: """离线验证随 EXE 打包的 Qt WebEngine 能否真正创建并加载页面。""" probe = QWebEngineView() @@ -2609,23 +2680,14 @@ def main() -> None: if "--packaging-self-check" in sys.argv: raise SystemExit(run_packaging_self_check(app)) - startup_result = {} - try: - import backend_client - - startup_result = backend_client.startup_sync_config(timeout=3.0) - except Exception: - # 云端暂时不可用时仍执行上次成功同步的强制升级策略。 - startup_result = {"release": backend_client.cached_release_status()} - - if not handle_startup_update(startup_result.get("release")): - return - window = MainWindow() - for diagnostic in startup_result.get("diagnostics") or []: - window.append_log(str(diagnostic), "notify") console_shutdown = install_console_shutdown_handler(app, window) window.show() + # WebEngine(Chromium 子进程)挪到首帧之后再拉起:低配机上它初始化要 + # 好几秒,放在构造期会让用户对着白屏等。 + QTimer.singleShot(120, window.portal_page.ensure_view) + if "--qt-smoke-test" not in sys.argv: + _start_background_startup_sync(window) if "--qt-smoke-test" in sys.argv: for index in range(window.stack.count()): diff --git a/wechat_rpa/wechat_rpa.spec b/wechat_rpa/wechat_rpa.spec index b8a409d..ec2d56c 100644 --- a/wechat_rpa/wechat_rpa.spec +++ b/wechat_rpa/wechat_rpa.spec @@ -3,7 +3,7 @@ import os from pathlib import Path -from PyInstaller.utils.hooks import collect_submodules +from PyInstaller.utils.hooks import collect_data_files, collect_submodules project_root = Path(SPECPATH) @@ -14,9 +14,11 @@ hidden_imports = [ "app_version", "backend_client", "conversation_store", + "gui_runtime", "mcp_bridge", "registration_store", "runtime_paths", + "wechat_gui", "wechat_gui_qt", "PySide6.QtWebEngineCore", "PySide6.QtWebEngineWidgets", @@ -24,29 +26,35 @@ hidden_imports = [ hidden_imports += collect_submodules("mcp") a = Analysis( - [str(project_root / "wechat_gui.py")], + [str(project_root / "app_main.py")], pathex=[str(project_root)], binaries=[], datas=[ (str(project_root / "edge_light_theme"), "edge_light_theme"), (str(project_root / "assets" / "brand"), "assets/brand"), - ], + ] + # 昵称 OCR 的模型与配置:只收代码不收这些文件的话,打包后 RapidOCR + # 初始化直接失败,会话身份静默退化。 + + collect_data_files("rapidocr_onnxruntime"), hiddenimports=hidden_imports, hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=["PyQt5", "PyQt6", "PySide2"], noarchive=False, - optimize=0, + # 去掉 assert 与 __debug__ 分支;不动 docstring(有库在运行期读它)。 + optimize=1, ) pyz = PYZ(a.pure) +# 目录形态(onedir):单文件 EXE 每次冷启动都要把几百 MB 的 Qt WebEngine +# 解压进临时目录再被杀毒软件逐个扫一遍,低配硬盘上一等就是几十秒。 +# 目录形态零解压,冷启动只剩正常的模块加载。 exe = EXE( pyz, a.scripts, - a.binaries, - a.datas, [], + exclude_binaries=True, name=app_name, debug=False, bootloader_ignore_signals=False, @@ -60,3 +68,12 @@ exe = EXE( entitlements_file=None, icon=str(project_root / "assets" / "brand" / "zhenyangtang-icon.ico"), ) + +coll = COLLECT( + exe, + a.binaries, + a.datas, + strip=False, + upx=False, + name=app_name, +)