From 88b44692fbf847e46ea956634c179db19326f33e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=89=9F=E4=B8=8D=E6=87=82=E5=93=8E?= <1797499294@qq.com> Date: Wed, 8 Apr 2026 21:36:37 +0800 Subject: [PATCH] v1.0.2 --- README.md | 11 +- WMPFDebugger.spec | 52 - gui.py | 1083 +++++++---------- gui_config.json | 4 +- proto/wmpf_debug.proto | 299 ----- src/__pycache__/__init__.cpython-310.pyc | Bin 148 -> 140 bytes src/__pycache__/__init__.cpython-314.pyc | Bin 157 -> 163 bytes src/__pycache__/cli.cpython-310.pyc | Bin 2680 -> 2656 bytes src/__pycache__/cli.cpython-314.pyc | Bin 4892 -> 4898 bytes src/__pycache__/cloud_audit.cpython-310.pyc | Bin 7481 -> 7473 bytes src/__pycache__/cloud_audit.cpython-314.pyc | Bin 14451 -> 14457 bytes src/__pycache__/codex.cpython-310.pyc | Bin 6449 -> 6441 bytes src/__pycache__/codex.cpython-314.pyc | Bin 14415 -> 14421 bytes src/__pycache__/constants.cpython-310.pyc | Bin 3132 -> 3124 bytes src/__pycache__/constants.cpython-314.pyc | Bin 3945 -> 3951 bytes src/__pycache__/engine.cpython-310.pyc | Bin 15346 -> 15346 bytes src/__pycache__/engine.cpython-313.pyc | Bin 0 -> 29875 bytes src/__pycache__/engine.cpython-314.pyc | Bin 31129 -> 31886 bytes src/__pycache__/logger.cpython-310.pyc | Bin 1944 -> 1936 bytes src/__pycache__/logger.cpython-314.pyc | Bin 3180 -> 3186 bytes src/__pycache__/navigator.cpython-310.pyc | Bin 5572 -> 5564 bytes src/__pycache__/navigator.cpython-314.pyc | Bin 9525 -> 9590 bytes src/__pycache__/userscript.cpython-310.pyc | Bin 3589 -> 3581 bytes src/__pycache__/userscript.cpython-314.pyc | Bin 7205 -> 7211 bytes src/engine.py | 7 +- src/navigator.py | 5 +- .../__pycache__/__init__.cpython-310.pyc | Bin 160 -> 152 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 169 -> 175 bytes .../wmpf_debug_pb2.cpython-310.pyc | Bin 10385 -> 10377 bytes .../wmpf_debug_pb2.cpython-314.pyc | Bin 14017 -> 14023 bytes 30 files changed, 440 insertions(+), 1021 deletions(-) delete mode 100644 WMPFDebugger.spec delete mode 100644 proto/wmpf_debug.proto create mode 100644 src/__pycache__/engine.cpython-313.pyc diff --git a/README.md b/README.md index a3e11ea..195548e 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,18 @@ ## 截图预览 ### 主界面 / Control Panel -> 使用First的时候,不建议开启调试(调试主包和调试frida)的两个选项,这两个选项是用来输出全量日志的,开启之后用起来可能会比较卡。 -![QQ20260402-192426](https://s1.galgame.fun/imgb/u0/20260402_69ce51f6c6f60.png) +![zhuyem](https://s1.galgame.fun/imgb/u0/20260408_69d655f49ca7e.png) ### 路由导航 -![微信图片_2026-04-02_192531_552](https://s1.galgame.fun/imgb/u0/20260402_69ce523e8b90d.png) +![luyou](https://s1.galgame.fun/imgb/u0/20260408_69d6560ca1a39.png) ### 云函数分析 ![QQ20260402-192623](https://s1.galgame.fun/imgb/u0/20260402_69ce527ca5480.png) +### 调试开关(JSRPC 与调用 wx.cloud) +![调试](https://s1.galgame.fun/imgb/u0/20260408_69d6564195009.png) + ### 安全扫描 待优化 @@ -137,6 +139,7 @@ pyinstaller WMPFDebugger.spec - [evi0s/WMPFDebugger](https://github.com/evi0s/WMPFDebugger) - [0xsdeo/HeartK](https://github.com/0xsdeo/HeartK) - [残笑/FindSomething](https://github.com/momosecurity/FindSomething) +- [进击的HACK/JSRPC与调用wx.cloud](https://mp.weixin.qq.com/s/hTlekrCPiMJCvsHYx7CAxw) --- ## 致谢 @@ -146,7 +149,7 @@ pyinstaller WMPFDebugger.spec --- ## 交流群 -![微信图片_20260403222425_810_3](https://s1.galgame.fun/imgb/u0/20260403_69cfdafc8cb99.jpg) +![微信图片_20260408212151_1256_3](https://s1.galgame.fun/imgb/u0/20260408_69d6567d777a9.jpg) ## 免责声明 diff --git a/WMPFDebugger.spec b/WMPFDebugger.spec deleted file mode 100644 index 31004be..0000000 --- a/WMPFDebugger.spec +++ /dev/null @@ -1,52 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - - -a = Analysis( - ['gui.py'], - pathex=[], - binaries=[], - datas=[ - ('src/nav_inject.js', 'src'), - ('src/cloud_audit_inject.js', 'src'), - ('frida/hook.js', 'frida'), - ('frida/config', 'frida/config'), - ('icon.ico', '.'), - ], - hiddenimports=[ - 'frida', - 'websockets', - 'google.protobuf', - 'PySide6.QtCore', - 'PySide6.QtGui', - 'PySide6.QtWidgets', - ], - hookspath=[], - hooksconfig={}, - runtime_hooks=[], - excludes=[], - noarchive=False, - optimize=0, -) -pyz = PYZ(a.pure) - -exe = EXE( - pyz, - a.scripts, - a.binaries, - a.datas, - [], - name='First', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=False, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None, - icon=['icon.ico'], -) diff --git a/gui.py b/gui.py index 7e934d2..efeb86e 100644 --- a/gui.py +++ b/gui.py @@ -9,7 +9,6 @@ import os import queue import sys import threading -from datetime import datetime from PySide6.QtCore import ( Qt, QTimer, QPropertyAnimation, QEasingCurve, Property, QRect, @@ -29,7 +28,6 @@ from src.engine import DebugEngine from src.navigator import MiniProgramNavigator from src.cloud_audit import CloudAuditor from src.userscript import load_userscripts_by_files -from src.js_analyzer import analyze_js, merge_results, CATEGORY_INFO, save_report, load_reports, delete_report # ══════════════════════════════════════════ # 配色 @@ -54,12 +52,12 @@ _TH = {"dark": _D, "light": _L} _FN = "Microsoft YaHei UI" _FM = "Consolas" _MENU = [ - ("control", "\u25c9", "控制台"), - ("navigator", "\u2b21", "路由导航"), - ("hook", "\u25c8", "Hook"), - ("cloud", "\u2601", "云扫描"), - ("security", "\u2623", "敏感提取"), - ("logs", "\u2261", "运行日志"), + ("control", "◉", "控制台"), + ("navigator", "⬡", "路由导航"), + ("hook", "◈", "Hook"), + ("cloud", "☁", "云扫描"), + ("vconsole", "◇", "调试开关"), + ("logs", "≡", "运行日志"), ] # ══════════════════════════════════════════ @@ -72,7 +70,6 @@ _CFG_FILE = os.path.join(_BASE_DIR, "gui_config.json") os.makedirs(os.path.join(_BASE_DIR, "userscripts"), exist_ok=True) os.makedirs(os.path.join(_BASE_DIR, "hook_scripts"), exist_ok=True) -os.makedirs(os.path.join(_BASE_DIR, "scan_reports"), exist_ok=True) def _load_cfg(): @@ -572,88 +569,6 @@ def _make_entry(placeholder="", width=None): return e -# ══════════════════════════════════════════ -# 敏感提取 — 独立进程 worker -# ══════════════════════════════════════════ - -def _sec_analyze_one(src): - """独立进程 Pool 的 worker 函数:分析单个 JS 源码。""" - try: - return analyze_js(src) - except Exception: - return None - - -def _sec_worker_proc(js_sources, appid, base_dir, result_q, name=""): - """在独立进程中并行分析 JS,通过 multiprocessing.Queue 回传进度和结果。""" - try: - total = len(js_sources) - total_size = sum(len(s) for s in js_sources) - - if total == 0: - result_q.put(("done", {}, 0, 0)) - return - - import os - workers = min(total, max(4, os.cpu_count() or 4)) - result_q.put(("progress", 35, f"并行分析 ({workers} 线程) ...")) - - from concurrent.futures import ThreadPoolExecutor, as_completed - results = [] - done_count = 0 - with ThreadPoolExecutor(max_workers=workers) as pool: - futs = {pool.submit(_sec_analyze_one, src): i for i, src in enumerate(js_sources)} - for fut in as_completed(futs): - r = fut.result() - if r: - results.append(r) - done_count += 1 - if done_count % 5 == 0 or done_count == total: - pct = 30 + int(done_count / total * 60) - result_q.put(("progress", pct, f"分析中 {done_count}/{total} ...")) - - merged = merge_results(results) if results else {} - result_q.put(("progress", 95, "生成报告...")) - - try: - save_report(base_dir, appid, merged, total, total_size, name=name) - except Exception: - pass - - result_q.put(("done", merged, total, total_size)) - except Exception as e: - result_q.put(("error", f"分析进程异常: {e}")) - result_q.put(("done", None, 0, 0)) - - -def _sec_run_worker(js_sources, appid, base_dir, sec_q, name=""): - """启动独立分析进程,用后台线程转发 multiprocessing.Queue 到 UI 的 queue.Queue。""" - mp_q = multiprocessing.Queue() - proc = multiprocessing.Process( - target=_sec_worker_proc, - args=(js_sources, appid, base_dir, mp_q, name), - daemon=True, - ) - proc.start() - - # 后台线程:等待进程完成,转发消息到 UI 队列 - def _relay(): - while proc.is_alive() or not mp_q.empty(): - try: - item = mp_q.get(timeout=0.2) - sec_q.put(item) - except Exception: - pass - # 确保队列清空 - while not mp_q.empty(): - try: - sec_q.put(mp_q.get_nowait()) - except Exception: - break - - t = threading.Thread(target=_relay, daemon=True) - t.start() - # ══════════════════════════════════════════ # 主窗口 @@ -685,15 +600,13 @@ class App(QMainWindow): self._redirect_guard_on = False self._hook_injected = set() self._blocked_seen = 0 - self._sec_scanning = False - self._sec_result = None - self._sec_view = "scan" # "scan" | "report" | "history" - + self._miniapp_connected = False + self._sb_fetch_gen = 0 + self._vc_stable_gen = 0 self._log_q = queue.Queue() self._sts_q = queue.Queue() self._rte_q = queue.Queue() self._cld_q = queue.Queue() - self._sec_q = queue.Queue() self._selected_preload = list(self._cfg.get("selected_preload_scripts", [])) self._nav_route_idx = -1 @@ -731,7 +644,7 @@ class App(QMainWindow): sb_head = QFrame() sb_head.setObjectName("sb_head") - sb_head.setFixedHeight(76) + sb_head.setFixedHeight(90) sb_head_lay = QVBoxLayout(sb_head) sb_head_lay.addStretch() @@ -785,6 +698,30 @@ class App(QMainWindow): sb_nav_lay.addStretch() sb_lay.addWidget(sb_nav, 1) + # 侧栏小程序信息卡片 + sb_app_card = QFrame() + sb_app_card.setStyleSheet( + "QFrame { background: rgba(128,128,128,0.08); border-radius: 8px; }" + "QLabel { background: transparent; }") + sb_app_card_lay = QVBoxLayout(sb_app_card) + sb_app_card_lay.setContentsMargins(8, 6, 8, 6) + sb_app_card_lay.setSpacing(1) + self._sb_app_name = QLabel("未连接") + self._sb_app_name.setAlignment(Qt.AlignCenter) + self._sb_app_name.setFont(QFont(_FN, 8)) + self._sb_app_name.setStyleSheet("color: #5c5c6c;") + self._sb_app_name.setWordWrap(True) + sb_app_card_lay.addWidget(self._sb_app_name) + self._sb_app_id = QLabel("") + self._sb_app_id.setAlignment(Qt.AlignCenter) + self._sb_app_id.setFont(QFont(_FN, 8)) + self._sb_app_id.setStyleSheet("color: #9e9ea8;") + self._sb_app_id.setVisible(False) + self._sb_app_id.setWordWrap(True) + sb_app_card_lay.addWidget(self._sb_app_id) + sb_lay.addWidget(sb_app_card) + sb_lay.addSpacing(4) + self._sb_theme = QLabel() self._sb_theme.setObjectName("sb_theme") self._sb_theme.setAlignment(Qt.AlignCenter) @@ -846,7 +783,7 @@ class App(QMainWindow): self._build_navigator() self._build_hook() self._build_cloud() - self._build_security() + self._build_vconsole() self._build_logs() # ── 控制台 ── @@ -880,19 +817,6 @@ class App(QMainWindow): row1.addStretch() c1_lay.addLayout(row1) - c1_lay.addWidget(_make_label("调试选项", bold=True)) - chkr = QHBoxLayout() - self._tog_dm = ToggleSwitch(self._cfg.get("debug_main", False)) - self._tog_dm.toggled.connect(lambda v: self._auto_save()) - chkr.addWidget(self._tog_dm) - chkr.addWidget(QLabel("调试主包")) - chkr.addSpacing(24) - self._tog_df = ToggleSwitch(self._cfg.get("debug_frida", False)) - self._tog_df.toggled.connect(lambda v: self._auto_save()) - chkr.addWidget(self._tog_df) - chkr.addWidget(QLabel("调试 Frida")) - chkr.addStretch() - c1_lay.addLayout(chkr) lay.addWidget(c1) # Card 2: 前加载脚本 @@ -959,11 +883,37 @@ class App(QMainWindow): self._app_lbl = QLabel("应用: --") self._app_lbl.setProperty("class", "muted") c3_lay.addWidget(self._app_lbl) + self._appname_lbl = QLabel("") + self._appname_lbl.setProperty("class", "muted") + self._appname_lbl.setVisible(False) + c3_lay.addWidget(self._appname_lbl) lay.addWidget(c3) self._stack.addWidget(page) self._page_map["control"] = self._stack.count() - 1 + # Card 4: 常见问题解决方案 + c4 = _make_card() + c4_lay = QVBoxLayout(c4) + c4_lay.setContentsMargins(16, 10, 16, 10) + c4_lay.setSpacing(8) + c4_lay.addWidget(_make_label("常见问题解决方案", bold=True)) + + faq_items = [ + ("Frida 连接失败", "请确认当前版本是否在WMPF版本区间内,如无法解决建议安装建议版本。"), + ("DevTools 打开内容为空", "点击启动调试前请勿打开小程序, 启动调试打开后再次启动小程序即可。"), + (r"Frida 已显示连接,但小程序端显示未连接/或步骤确认没问题且无法断点", r"若操作顺序无误,建议先彻底卸载微信并重启电脑-·如有重要聊天记录请提前备份·-。删除路径C:\Users\用户名\AppData\Roaming\Tencent\xwechat\XPlugin\Plugins\RadiumWMPF下所有以数字命名的文件夹,再次重启电脑后,安装微信 4.1.0.30 版本。安装完成后检查上述路径,确认文件夹编号为 16389。"), + ] + + for title, solution in faq_items: + item_lbl = QLabel(f"• {title}\n {solution}") + item_lbl.setWordWrap(True) + item_lbl.setStyleSheet("color: #FFC107;") + c4_lay.addWidget(item_lbl) + + c4_lay.addStretch() + lay.addWidget(c4) + # ── 路由导航 ── def _build_navigator(self): @@ -1010,9 +960,6 @@ class App(QMainWindow): self._btn_go = _make_btn("跳转", self._do_go) self._btn_go.setEnabled(False) b1.addWidget(self._btn_go) - self._btn_redir = _make_btn("重定向", self._do_redir) - self._btn_redir.setEnabled(False) - b1.addWidget(self._btn_redir) self._btn_relaunch = _make_btn("重启到页面", self._do_relaunch) self._btn_relaunch.setEnabled(False) b1.addWidget(self._btn_relaunch) @@ -1058,6 +1005,8 @@ class App(QMainWindow): self._prog.setFixedHeight(6) lay.addWidget(self._prog) self._route_lbl = QLabel("当前路由: --") + self._route_lbl.setFixedHeight(22) + self._route_lbl.setProperty("class", "bold") lay.addWidget(self._route_lbl) self._stack.addWidget(page) @@ -1278,145 +1227,172 @@ class App(QMainWindow): self._stack.addWidget(page) self._page_map["cloud"] = self._stack.count() - 1 - # ── 敏感提取 ── + # ── 调试开关 (vConsole) ── - def _build_security(self): + def _build_vconsole(self): page = QWidget() lay = QVBoxLayout(page) - lay.setContentsMargins(24, 8, 24, 8) - lay.setSpacing(6) + lay.setContentsMargins(24, 12, 24, 16) + lay.setSpacing(10) + lay.setAlignment(Qt.AlignTop) - # 顶部操作栏 - bar = QHBoxLayout() - self._btn_sec_scan = _make_btn("扫描当前小程序", self._sec_do_scan) - bar.addWidget(self._btn_sec_scan) - self._btn_sec_history = _make_btn("历史记录", self._sec_show_history) - bar.addWidget(self._btn_sec_history) - self._btn_sec_back = _make_btn("返回", self._sec_back_to_scan) - self._btn_sec_back.setVisible(False) - bar.addWidget(self._btn_sec_back) - bar.addStretch() - self._sec_status_lbl = QLabel("") - self._sec_status_lbl.setProperty("class", "muted") - bar.addWidget(self._sec_status_lbl) - lay.addLayout(bar) + # 风险警告卡片 + warn_card = _make_card() + warn_lay = QVBoxLayout(warn_card) + warn_lay.setContentsMargins(16, 12, 16, 12) + warn_lay.setSpacing(6) + warn_title = QLabel("⚠ 风险提示") + warn_title.setFont(QFont(_FN, 11, QFont.Bold)) + warn_title.setStyleSheet("color: #e6a23c;") + warn_lay.addWidget(warn_title) + warn_text = QLabel( + "非正规开启小程序调试有封号风险。测试需谨慎!\n" + "请勿在主力账号上使用,建议使用测试号操作。") + warn_text.setWordWrap(True) + warn_text.setStyleSheet("color: #e6a23c; font-size: 12px;") + warn_lay.addWidget(warn_text) + lay.addWidget(warn_card) - # 进度条 - self._sec_prog = QProgressBar() - self._sec_prog.setFixedHeight(6) - self._sec_prog.setRange(0, 100) - self._sec_prog.setValue(0) - self._sec_prog.setTextVisible(False) - self._sec_prog.setVisible(False) - lay.addWidget(self._sec_prog) + # 功能说明卡片 + info_card = _make_card() + info_lay = QVBoxLayout(info_card) + info_lay.setContentsMargins(16, 12, 16, 12) + info_lay.setSpacing(6) + info_lay.addWidget(_make_label("功能说明", bold=True)) + desc = QLabel( + "通过官方 API wx.setEnableDebug 开启小程序内置的 vConsole 调试面板。\n\n" + "开启后可以:\n" + " • 在小程序内直接执行 JS 代码\n" + " • 调用 wx.cloud.callFunction 调试云函数\n\n" + "关闭后重启小程序即可恢复正常。") + desc.setWordWrap(True) + desc.setProperty("class", "muted") + info_lay.addWidget(desc) + ref_lbl = QLabel( + '学习文档: ' + '官方文档 wx.setEnableDebug') + ref_lbl.setOpenExternalLinks(True) + ref_lbl.setStyleSheet("font-size: 11px;") + info_lay.addWidget(ref_lbl) + lay.addWidget(info_card) - # 主内容区 — 内含 QStackedWidget 切换 scan/report/history - self._sec_stack = QStackedWidget() + # 操作卡片 + op_card = _make_card() + op_lay = QVBoxLayout(op_card) + op_lay.setContentsMargins(16, 12, 16, 12) + op_lay.setSpacing(8) + op_lay.addWidget(_make_label("操作", bold=True)) - # --- 扫描提示页 (index 0) --- - scan_hint = QWidget() - sh_lay = QVBoxLayout(scan_hint) - sh_lay.setAlignment(Qt.AlignCenter) - hint_lbl = QLabel("连接小程序后,点击「扫描当前小程序」提取 JS 中的敏感信息") - hint_lbl.setProperty("class", "muted") - hint_lbl.setAlignment(Qt.AlignCenter) - hint_lbl.setWordWrap(True) - sh_lay.addWidget(hint_lbl) - self._sec_stack.addWidget(scan_hint) + btn_row = QHBoxLayout() + self._btn_vc_enable = _make_btn("▶ 开启调试", self._do_vc_enable) + self._btn_vc_enable.setFont(QFont(_FN, 10, QFont.Bold)) + self._btn_vc_enable.setEnabled(False) + btn_row.addWidget(self._btn_vc_enable) + self._btn_vc_disable = _make_btn("■ 关闭调试", self._do_vc_disable) + self._btn_vc_disable.setFont(QFont(_FN, 10, QFont.Bold)) + self._btn_vc_disable.setEnabled(False) + btn_row.addWidget(self._btn_vc_disable) + btn_row.addStretch() + op_lay.addLayout(btn_row) - # --- 报告页 (index 1) --- - report_page = QWidget() - rp_lay = QVBoxLayout(report_page) - rp_lay.setContentsMargins(0, 0, 0, 0) - rp_lay.setSpacing(6) + self._vc_status_lbl = QLabel("状态: 未连接小程序") + self._vc_status_lbl.setProperty("class", "muted") + op_lay.addWidget(self._vc_status_lbl) + lay.addWidget(op_card) - # 报告头部: appid + 概要 - rp_hdr = QHBoxLayout() - self._sec_rpt_header = QLabel("") - self._sec_rpt_header.setProperty("class", "bold") - rp_hdr.addWidget(self._sec_rpt_header) - rp_hdr.addStretch() - self._sec_rpt_summary = QLabel("") - self._sec_rpt_summary.setProperty("class", "muted") - rp_hdr.addWidget(self._sec_rpt_summary) - rp_lay.addLayout(rp_hdr) - - # 左右分栏 - rp_split = QHBoxLayout() - rp_split.setSpacing(8) - - # 左栏: 类别列表 (滚动) - left_scroll = QScrollArea() - left_scroll.setWidgetResizable(True) - left_scroll.setFrameShape(QFrame.NoFrame) - left_scroll.setFixedWidth(220) - left_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - left_inner = QWidget() - self._sec_cat_lay = QVBoxLayout(left_inner) - self._sec_cat_lay.setContentsMargins(0, 0, 0, 0) - self._sec_cat_lay.setSpacing(2) - self._sec_cat_lay.addStretch() - left_scroll.setWidget(left_inner) - rp_split.addWidget(left_scroll) - - # 右栏: 详情列表 (滚动) - right_card = _make_card() - right_card_lay = QVBoxLayout(right_card) - right_card_lay.setContentsMargins(0, 0, 0, 0) - right_card_lay.setSpacing(0) - - # 右栏顶部: 类别名 + 复制全部 - self._sec_detail_bar = QHBoxLayout() - self._sec_detail_bar.setContentsMargins(14, 8, 14, 4) - self._sec_detail_title = QLabel("选择左侧类别查看详情") - self._sec_detail_title.setProperty("class", "bold") - self._sec_detail_bar.addWidget(self._sec_detail_title) - self._sec_detail_bar.addStretch() - self._btn_sec_copy_all = _make_btn("复制全部", self._sec_copy_all) - self._btn_sec_copy_all.setVisible(False) - self._sec_detail_bar.addWidget(self._btn_sec_copy_all) - right_card_lay.addLayout(self._sec_detail_bar) - - # 右栏内容: QTextEdit (只读,自带滚动条,填满剩余空间) - self._sec_detail_text = QTextEdit() - self._sec_detail_text.setReadOnly(True) - self._sec_detail_text.setFont(QFont(_FM, 9)) - self._sec_detail_text.setTextInteractionFlags( - Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard) - self._sec_detail_text.setFrameShape(QFrame.NoFrame) - right_card_lay.addWidget(self._sec_detail_text, 1) - - rp_split.addWidget(right_card, 1) - rp_lay.addLayout(rp_split, 1) - - self._sec_cur_cat = None # 当前选中类别 key - self._sec_cat_widgets = {} # key -> row widget - self._sec_stack.addWidget(report_page) - - # --- 历史页 (index 2) --- - history_page = QWidget() - hp_lay = QVBoxLayout(history_page) - hp_lay.setContentsMargins(0, 0, 0, 0) - hp_lay.setSpacing(6) - - self._sec_hist_tree = QTreeWidget() - self._sec_hist_tree.setHeaderLabels(["时间", "AppID", "名称", "JS数量", "发现项"]) - self._sec_hist_tree.setColumnCount(5) - self._sec_hist_tree.header().setStretchLastSection(True) - self._sec_hist_tree.setRootIsDecorated(False) - self._sec_hist_tree.setAlternatingRowColors(False) - self._sec_hist_tree.header().setDefaultAlignment(Qt.AlignLeft | Qt.AlignVCenter) - self._sec_hist_tree.itemDoubleClicked.connect(self._sec_hist_open) - self._sec_hist_tree.setContextMenuPolicy(Qt.CustomContextMenu) - self._sec_hist_tree.customContextMenuRequested.connect(self._sec_hist_menu) - hp_lay.addWidget(self._sec_hist_tree, 1) - - self._sec_stack.addWidget(history_page) - - lay.addWidget(self._sec_stack, 1) + lay.addStretch() self._stack.addWidget(page) - self._page_map["security"] = self._stack.count() - 1 + self._page_map["vconsole"] = self._stack.count() - 1 + + def _do_vc_enable(self): + if not self._engine or not self._loop or not self._loop.is_running(): + self._log_add("error", "[调试] 请先启动调试并连接小程序") + return + from PySide6.QtWidgets import QMessageBox + r = QMessageBox.warning( + self, "风险确认", + "非正规开启小程序调试有封号风险。\n测试需谨慎!\n\n确定要开启吗?", + QMessageBox.Ok | QMessageBox.Cancel, + QMessageBox.Cancel, + ) + if r != QMessageBox.Ok: + return + self._btn_vc_enable.setEnabled(False) + asyncio.run_coroutine_threadsafe(self._avc_set_debug(True), self._loop) + + def _do_vc_disable(self): + if not self._engine or not self._loop or not self._loop.is_running(): + self._log_add("error", "[调试] 请先启动调试并连接小程序") + return + self._btn_vc_disable.setEnabled(False) + asyncio.run_coroutine_threadsafe(self._avc_set_debug(False), self._loop) + + async def _avc_set_debug(self, enable): + try: + val = "true" if enable else "false" + # 先确保 navigator 已注入,通过 wxFrame.wx 调用避免超时 + await self._navigator._ensure(force=True) + result = await self._engine.evaluate_js( + "(function(){" + "try{" + "var nav=window.nav;" + "if(!nav||!nav.wxFrame||!nav.wxFrame.wx)return JSON.stringify({err:'no wxFrame'});" + f"nav.wxFrame.wx.setEnableDebug({{enableDebug:{val}," + "success:function(){console.log('[First] setEnableDebug success')}," + "fail:function(e){console.error('[First] setEnableDebug fail',e)}" + "});" + "return JSON.stringify({ok:true})" + "}catch(e){return JSON.stringify({err:e.message})}" + "})()", + timeout=5.0, + ) + value = None + if result: + r = result.get("result", {}) + inner = r.get("result", {}) + value = inner.get("value") + if value: + import json as _json + info = _json.loads(value) + if info.get("err"): + raise RuntimeError(info["err"]) + state = "已开启" if enable else "已关闭" + self._rte_q.put(("__vc__", enable, True)) + self._log_q.put(("info", f"[调试] vConsole {state}")) + except Exception as e: + self._rte_q.put(("__vc__", enable, False)) + self._log_q.put(("error", f"[调试] 操作失败: {e}")) + + async def _avc_detect_debug(self): + """自动检测小程序是否已开启 vConsole 调试。""" + try: + await self._navigator._ensure(force=True) + result = await self._engine.evaluate_js( + "(function(){" + "try{" + "var f=window.nav&&window.nav.wxFrame?window.nav.wxFrame:window;" + "var c=f.__wxConfig||{};" + "var d=!!c.debug;" + "var v=!!(f.document&&f.document.getElementById('__vconsole'));" + "return JSON.stringify({debug:d,vconsole:v})" + "}catch(e){return JSON.stringify({err:e.message})}" + "})()", + timeout=5.0, + ) + value = None + if result: + r = result.get("result", {}) + inner = r.get("result", {}) + value = inner.get("value") + if value: + info = json.loads(value) + if info.get("err"): + return + is_debug = info.get("debug", False) or info.get("vconsole", False) + self._rte_q.put(("__vc_detect__", is_debug)) + except Exception: + pass # ── 日志 ── @@ -1426,6 +1402,29 @@ class App(QMainWindow): lay.setContentsMargins(24, 12, 24, 16) lay.setSpacing(10) + # 调试选项卡片 + dc = _make_card() + dc_lay = QVBoxLayout(dc) + dc_lay.setContentsMargins(16, 10, 16, 10) + dc_lay.setSpacing(6) + dc_lay.addWidget(_make_label("调试选项", bold=True)) + warn_lbl = QLabel("⚠ 开启后可能导致小程序卡死,请谨慎使用") + warn_lbl.setStyleSheet("color: #fbbf24; font-size: 9px;") + dc_lay.addWidget(warn_lbl) + chkr = QHBoxLayout() + self._tog_dm = ToggleSwitch(self._cfg.get("debug_main", False)) + self._tog_dm.toggled.connect(lambda v: self._auto_save()) + chkr.addWidget(self._tog_dm) + chkr.addWidget(QLabel("调试主包")) + chkr.addSpacing(24) + self._tog_df = ToggleSwitch(self._cfg.get("debug_frida", False)) + self._tog_df.toggled.connect(lambda v: self._auto_save()) + chkr.addWidget(self._tog_df) + chkr.addWidget(QLabel("调试 Frida")) + chkr.addStretch() + dc_lay.addLayout(chkr) + lay.addWidget(dc) + hdr = QHBoxLayout() hdr.addWidget(_make_label("日志输出", bold=True)) hdr.addStretch() @@ -1520,11 +1519,12 @@ class App(QMainWindow): self.setStyleSheet(build_qss(self._tn)) self._update_theme_label() self._update_toggle_colors() + self._refresh_sb_app_card() self._hl_sb() self._auto_save() def _update_theme_label(self): - txt = "\u2600 浅色模式" if self._tn == "dark" else "\u263d 深色模式" + txt = "☀ 浅色模式" if self._tn == "dark" else "☽ 深色模式" self._sb_theme.setText(txt) def _update_toggle_colors(self): @@ -1532,6 +1532,15 @@ class App(QMainWindow): for tog in (self._tog_dm, self._tog_df): tog.set_colors(c["accent"], c["text4"]) + def _refresh_sb_app_card(self): + """主题切换时刷新侧栏小程序卡片颜色。""" + c = _TH[self._tn] + if self._sb_app_id.isVisible(): + self._sb_app_name.setStyleSheet(f"color: {c['success']};") + self._sb_app_id.setStyleSheet(f"color: {c['success']};") + else: + self._sb_app_name.setStyleSheet(f"color: {c['text3']};") + def _auto_save(self): data = { "theme": self._tn, @@ -1675,13 +1684,18 @@ class App(QMainWindow): self._guard_label.setText("防跳转: 关闭") self._devtools_lbl.setText("") self._devtools_copy_hint.setText("") - if self._sec_scanning: - self._sec_scanning = False - self._btn_sec_scan.setEnabled(True) - self._sec_prog.setVisible(False) + # 引擎停止,清除侧栏和运行状态卡片的小程序信息 + c = _TH[self._tn] + self._sb_app_name.setText("未连接") + self._sb_app_name.setStyleSheet(f"color: {c['text3']};") + self._sb_app_id.setText("") + self._sb_app_id.setVisible(False) + self._app_lbl.setText("AppID: --") + self._appname_lbl.setText("") + self._appname_lbl.setVisible(False) def _nav_btns(self, on): - for b in (self._btn_go, self._btn_redir, self._btn_relaunch, + for b in (self._btn_go, self._btn_relaunch, self._btn_back, self._btn_auto, self._btn_prev, self._btn_next, self._btn_copy_route): b.setEnabled(on) @@ -1697,9 +1711,86 @@ class App(QMainWindow): self._rte_q.put(("routes", self._navigator.pages, self._navigator.tab_bar_pages)) self._rte_q.put(("app_info", self._navigator.app_info)) QTimer.singleShot(0, self._poll_route_start) + # fetch_config 的 name 可能为空,补充通过 wxFrame 路径获取完整信息 + await self._afetch_app_info() except Exception as e: self._log_q.put(("error", f"[导航] 获取失败: {e}")) + async def _afetch_app_info(self): + """通过 nav_inject 的 wxFrame.__wxConfig 获取小程序名称和appid,用于侧栏显示。""" + try: + # 强制重新注入 navigator(重连后 WebView 上下文是全新的) + await self._navigator._ensure(force=True) + result = await self._engine.evaluate_js( + "(function(){" + "try{" + "var nav=window.nav;" + "if(!nav||!nav.wxFrame)return JSON.stringify({err:'no nav'});" + "var c=nav.wxFrame.__wxConfig||{};" + "var ai=c.accountInfo||{};" + "var aa=ai.appAccount||{};" + "return JSON.stringify({" + "appid:aa.appId||ai.appId||c.appid||''," + "name:aa.nickname||ai.nickname||c.appname||''" + "})" + "}catch(e){return JSON.stringify({err:e.message})}" + "})()", + timeout=5.0, + ) + value = None + if result: + r = result.get("result", {}) + inner = r.get("result", {}) + value = inner.get("value") + if value: + info = json.loads(value) + if info.get("err"): + return + self._rte_q.put(("app_info", info)) + except Exception: + pass + + def _delayed_stable_connect(self, gen): + """连接稳定后再启用按钮和触发后续操作,gen 不匹配说明中间又断过,跳过。""" + if gen != self._vc_stable_gen: + return + if not self._miniapp_connected: + return + self._nav_btns(True) + self._btn_vc_enable.setEnabled(True) + self._btn_vc_disable.setEnabled(True) + self._vc_status_lbl.setText("状态: 就绪") + # 自动检测 vConsole 调试状态 + if self._engine and self._loop and self._loop.is_running(): + asyncio.run_coroutine_threadsafe(self._avc_detect_debug(), self._loop) + # 延迟获取侧栏信息 + self._sb_fetch_gen += 1 + fetch_gen = self._sb_fetch_gen + QTimer.singleShot(1500, lambda: self._delayed_fetch_app_info(fetch_gen)) + # 自动恢复云扫描 + if not self._cloud_scan_active and self._auditor: + self._cloud_start_scan() + + def _delayed_fetch_app_info(self, gen): + """延迟调用,只有最后一次触发的 gen 匹配才执行。""" + if gen != self._sb_fetch_gen: + return + if self._miniapp_connected and self._engine and self._loop and self._loop.is_running(): + asyncio.run_coroutine_threadsafe(self._afetch_app_info(), self._loop) + + def _delayed_clear_app_info(self, gen): + """延迟清除侧栏信息,gen 不匹配说明已重连,跳过。""" + if gen != self._sb_fetch_gen: + return + c = _TH[self._tn] + self._sb_app_name.setText("未连接") + self._sb_app_name.setStyleSheet(f"color: {c['text3']};") + self._sb_app_id.setText("") + self._sb_app_id.setVisible(False) + self._app_lbl.setText("AppID: --") + self._appname_lbl.setText("") + self._appname_lbl.setVisible(False) + def _poll_route_start(self): if not self._running: return @@ -1735,12 +1826,6 @@ class App(QMainWindow): asyncio.run_coroutine_threadsafe( self._anav("navigate_to", r, "跳转"), self._loop) - def _do_redir(self): - r = self._sel_route() - if r and self._engine and self._loop: - asyncio.run_coroutine_threadsafe( - self._anav("redirect_to", r, "重定向"), self._loop) - def _do_relaunch(self): r = self._sel_route() if r and self._engine and self._loop: @@ -1856,8 +1941,6 @@ class App(QMainWindow): menu.addSeparator() menu.addAction("跳转", lambda: asyncio.run_coroutine_threadsafe( self._anav("navigate_to", route, "跳转"), self._loop) if self._engine and self._loop else None) - menu.addAction("重定向", lambda: asyncio.run_coroutine_threadsafe( - self._anav("redirect_to", route, "重定向"), self._loop) if self._engine and self._loop else None) menu.addAction("重启到页面", lambda: asyncio.run_coroutine_threadsafe( self._anav("relaunch_to", route, "重启"), self._loop) if self._engine and self._loop else None) menu.exec(self._tree.viewport().mapToGlobal(pos)) @@ -1905,13 +1988,16 @@ class App(QMainWindow): self._fill_tree(self._all_routes, self._navigator.tab_bar_pages) return flt = [p for p in self._all_routes if q in p.lower()] + self._tree.setUpdatesEnabled(False) self._tree.clear() for p in flt: item = QTreeWidgetItem([p]) item.setData(0, Qt.UserRole, p) self._tree.addTopLevelItem(item) + self._tree.setUpdatesEnabled(True) def _fill_tree(self, pages, tab_bar): + self._tree.setUpdatesEnabled(False) self._tree.clear() tabs = set(tab_bar) groups = {} @@ -1939,6 +2025,7 @@ class App(QMainWindow): child = QTreeWidgetItem([d]) child.setData(0, Qt.UserRole, p) nd.addChild(child) + self._tree.setUpdatesEnabled(True) def _select_tree_route(self, route): """Select the tree item matching the given route path.""" @@ -2152,399 +2239,6 @@ class App(QMainWindow): except Exception as e: self._log_add("error", f"[云扫描] 导出失败: {e}") - # ────────────────────────────────── - # 敏感提取业务 - # ────────────────────────────────── - - def _sec_do_scan(self): - if self._sec_scanning: - self._log_add("info", "[敏感提取] 扫描正在进行中,请稍候...") - return - if not self._engine or not self._loop or not self._loop.is_running(): - self._log_add("error", "[敏感提取] 请先启动调试并连接小程序") - return - if not self._engine.miniapp_clients: - self._log_add("error", "[敏感提取] 小程序未连接,请先打开小程序") - return - self._sec_scanning = True - self._btn_sec_scan.setEnabled(False) - self._sec_prog.setVisible(True) - self._sec_prog.setValue(0) - self._sec_status_lbl.setText("正在提取 JS 源码...") - self._sec_stack.setCurrentIndex(0) - self._log_add("info", "[敏感提取] 开始扫描...") - fut = asyncio.run_coroutine_threadsafe(self._asec_scan(), self._loop) - # 捕获 future 异常,防止静默失败 - def _on_scan_done(f): - try: - f.result() - except Exception as e: - self._sec_q.put(("error", f"扫描协程异常: {e}")) - self._sec_q.put(("done", None, 0, 0)) - fut.add_done_callback(_on_scan_done) - - async def _asec_scan(self): - try: - # Step 1: 通过 CDP Debugger 获取所有 JS 脚本源码 - self._sec_q.put(("progress", 5, "正在收集脚本列表...")) - - script_ids = [] - - def _on_parsed(data): - p = data.get("params", {}) - sid = p.get("scriptId") - if sid: - script_ids.append((sid, p.get("url", ""))) - - self._engine.on_cdp_event("Debugger.scriptParsed", _on_parsed) - try: - try: - await self._engine.send_cdp_command("Debugger.disable", timeout=3.0) - except Exception: - pass - await asyncio.sleep(0.3) - try: - await self._engine.send_cdp_command("Debugger.enable", timeout=5.0) - except asyncio.TimeoutError: - self._sec_q.put(("log", "Debugger.enable 超时,尝试重新启用...")) - await asyncio.sleep(0.5) - try: - await self._engine.send_cdp_command("Debugger.enable", timeout=5.0) - except Exception as e2: - self._sec_q.put(("log", f"Debugger.enable 重试失败: {e2}")) - try: - await self._engine.send_cdp_command( - "Debugger.setSkipAllPauses", {"skip": True}, timeout=3.0) - except Exception: - pass - # 等待 scriptParsed 事件全部到达: - # 先等 2 秒让大部分事件到达,然后连续 3 次(每次 0.4s)数量不变才认为稳定 - await asyncio.sleep(2.0) - stable_count = 0 - prev = 0 - for _ in range(15): - cur = len(script_ids) - if cur == prev and cur > 0: - stable_count += 1 - if stable_count >= 3: - break - else: - stable_count = 0 - prev = cur - await asyncio.sleep(0.4) - self._sec_q.put(("log", f"脚本收集完成: {len(script_ids)} 个")) - except Exception as e: - self._sec_q.put(("log", f"Debugger 启动异常: {e}")) - finally: - self._engine.off_cdp_event("Debugger.scriptParsed", _on_parsed) - - if not script_ids: - self._sec_q.put(("error", "未能获取到脚本列表,请确保小程序已打开")) - self._sec_q.put(("done", None, 0, 0)) - return - - self._sec_q.put(("progress", 15, - f"发现 {len(script_ids)} 个脚本,正在提取源码...")) - - # Step 2: 逐个获取脚本源码 - js_sources = [] - for i, (sid, url) in enumerate(script_ids): - try: - resp = await self._engine.send_cdp_command( - "Debugger.getScriptSource", {"scriptId": sid}, timeout=8.0) - source = resp.get("result", {}).get("scriptSource", "") - if source and len(source) > 20: - js_sources.append(source) - except Exception: - pass - if i % 10 == 0: - pct = 15 + int((i + 1) / len(script_ids) * 15) - self._sec_q.put(("progress", pct, - f"提取源码 {i+1}/{len(script_ids)} ...")) - - try: - await self._engine.send_cdp_command("Debugger.disable", timeout=3.0) - except Exception: - pass - - if not js_sources: - self._sec_q.put(("error", "脚本源码提取为空")) - self._sec_q.put(("done", None, 0, 0)) - return - - total_size = sum(len(s) for s in js_sources) - self._sec_q.put(("progress", 30, - f"已提取 {len(js_sources)} 个 JS 源码,启动独立分析进程...")) - - # Step 3: 获取小程序信息 (若还没获取过,自动 fetch_config) - appid = "" - app_name = "" - if self._navigator: - if not self._navigator.app_info or not self._navigator.app_info.get("appid"): - try: - await self._navigator.fetch_config() - except Exception: - pass - appid = self._navigator.app_info.get("appid", "") - app_name = self._navigator.app_info.get("name", "") - if not appid: - appid = "unknown" - - # Step 4: 在独立进程中分析 (不阻塞主进程/UI) - _sec_run_worker(js_sources, appid, _BASE_DIR, self._sec_q, name=app_name) - - except Exception as e: - self._sec_q.put(("error", f"分析失败: {e}")) - self._sec_q.put(("done", None, 0, 0)) - - def _sec_extract_val(self, result): - if not result: - return None - r = result.get("result", {}) - inner = r.get("result", {}) - return inner.get("value") - - def _sec_show_report(self, result, js_count=0, total_size=0, appid="", scan_time="", name=""): - """在报告页渲染分析结果 (左类别列表 + 右详情)""" - self._sec_result = result - c = _TH[self._tn] - - if not scan_time: - scan_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - header_parts = [] - if name: - header_parts.append(name) - if appid: - header_parts.append(f"AppID: {appid}") - self._sec_rpt_header.setText(" | ".join(header_parts) if header_parts else "分析报告") - - total_findings = sum(len(v) for v in result.values() if isinstance(v, list)) - summary = f"{js_count} 个 JS ({self._fmt_size(total_size)}) | " \ - f"{total_findings} 项 | {scan_time}" if js_count else \ - f"{total_findings} 项 | {scan_time}" - self._sec_rpt_summary.setText(summary) - - # 清除旧的左栏类别行 - while self._sec_cat_lay.count() > 1: - item = self._sec_cat_lay.takeAt(0) - w = item.widget() - if w: - w.deleteLater() - self._sec_cat_widgets.clear() - self._sec_cur_cat = None - - # 生成左栏类别行 - for key in CATEGORY_INFO: - items = result.get(key, []) - cn, _ = CATEGORY_INFO[key] - count = len(items) - - row = QFrame() - row.setFixedHeight(38) - row.setCursor(Qt.PointingHandCursor) - row_lay = QHBoxLayout(row) - row_lay.setContentsMargins(0, 0, 6, 0) - row_lay.setSpacing(6) - - # 绿色竖条 - bar = QFrame() - bar.setFixedSize(4, 24) - bar.setStyleSheet(f"background: {c['accent']}; border-radius: 2px;") - row_lay.addWidget(bar) - - name_lbl = QLabel(cn) - name_lbl.setProperty("class", "bold") - name_lbl.setFont(QFont(_FN, 10)) - row_lay.addWidget(name_lbl, 1) - - count_lbl = QLabel(str(count)) - count_lbl.setFont(QFont(_FM, 9)) - if count > 0: - count_lbl.setStyleSheet(f"color: {c['accent']};") - else: - count_lbl.setStyleSheet(f"color: {c['text3']};") - row_lay.addWidget(count_lbl) - - copy_btn = QPushButton("复制") - copy_btn.setFixedSize(52, 26) - copy_btn.setFont(QFont(_FN, 9)) - copy_btn.clicked.connect(lambda _, k=key, b=copy_btn: self._sec_copy_cat(k, b)) - row_lay.addWidget(copy_btn) - - row.mousePressEvent = lambda e, k=key: self._sec_select_cat(k) - self._sec_cat_widgets[key] = row - self._sec_cat_lay.insertWidget(self._sec_cat_lay.count() - 1, row) - - # 清空右侧 - self._sec_clear_detail() - self._sec_detail_title.setText("选择左侧类别查看详情") - self._btn_sec_copy_all.setVisible(False) - - # 自动选中第一个有内容的类别 - for key in CATEGORY_INFO: - if result.get(key): - self._sec_select_cat(key) - break - - self._sec_stack.setCurrentIndex(1) - self._btn_sec_back.setVisible(True) - - def _sec_select_cat(self, key): - """选中左栏类别,在右栏显示详情列表""" - c = _TH[self._tn] - self._sec_cur_cat = key - - # 高亮左栏选中项 - for k, row in self._sec_cat_widgets.items(): - if k == key: - row.setStyleSheet( - f"QFrame {{ background: {c['sb_active']}; border-radius: 6px; }}" - f" QLabel {{ color: {c['text1']}; }}" - f" QPushButton {{ color: {c['text1']}; }}" - ) - else: - row.setStyleSheet("") - - items = self._sec_result.get(key, []) if self._sec_result else [] - cn, _ = CATEGORY_INFO.get(key, (key, "")) - - self._sec_detail_title.setText(f"{cn} ({len(items)})") - self._btn_sec_copy_all.setVisible(len(items) > 0) - - # 直接填充 QTextEdit,每行一条 - if items: - self._sec_detail_text.setStyleSheet( - f"QTextEdit {{ color: {c['text1']}; background: transparent;" - f" selection-background-color: {c['accent']}; }}" - ) - self._sec_detail_text.setPlainText("\n".join(str(v) for v in items)) - else: - self._sec_detail_text.clear() - - def _sec_clear_detail(self): - """清空右栏详情""" - self._sec_detail_text.clear() - - def _sec_copy_cat(self, key, btn=None): - """复制某个类别的全部内容""" - items = self._sec_result.get(key, []) if self._sec_result else [] - if items: - QApplication.clipboard().setText("\n".join(str(v) for v in items)) - cn, _ = CATEGORY_INFO.get(key, (key, "")) - self._sec_status_lbl.setText(f"已复制 {cn} ({len(items)} 项)") - if btn and isinstance(btn, QPushButton): - old_text = btn.text() - btn.setText("已复制") - QTimer.singleShot(1200, lambda: btn.setText(old_text) if btn else None) - - def _sec_copy_all(self): - """复制当前选中类别的全部内容""" - if self._sec_cur_cat: - self._sec_copy_cat(self._sec_cur_cat, self._btn_sec_copy_all) - - def _sec_show_history(self): - self._sec_hist_tree.clear() - reports = load_reports(_BASE_DIR) - for r in reports: - summary = r.get("summary", {}) - total = sum(summary.values()) - item = QTreeWidgetItem([ - r.get("time", ""), - r.get("appid", ""), - r.get("name", ""), - str(r.get("js_count", 0)), - str(total), - ]) - item.setData(0, Qt.UserRole, r) - self._sec_hist_tree.addTopLevelItem(item) - self._sec_stack.setCurrentIndex(2) - self._btn_sec_back.setVisible(True) - self._sec_status_lbl.setText(f"共 {len(reports)} 条记录") - - def _sec_hist_open(self, item): - r = item.data(0, Qt.UserRole) - if not r: - return - result = r.get("result", {}) - self._sec_show_report( - result, - js_count=r.get("js_count", 0), - total_size=r.get("total_size", 0), - appid=r.get("appid", ""), - scan_time=r.get("time", ""), - name=r.get("name", ""), - ) - - def _sec_hist_menu(self, pos): - item = self._sec_hist_tree.itemAt(pos) - if not item: - return - r = item.data(0, Qt.UserRole) - menu = QMenu(self) - menu.addAction("查看报告", lambda: self._sec_hist_open(item)) - if r: - menu.addAction("删除", lambda: self._sec_hist_delete(item, r)) - menu.exec(self._sec_hist_tree.viewport().mapToGlobal(pos)) - - def _sec_hist_delete(self, item, r): - fn = r.get("_filename", "") - if fn: - delete_report(_BASE_DIR, fn) - idx = self._sec_hist_tree.indexOfTopLevelItem(item) - if idx >= 0: - self._sec_hist_tree.takeTopLevelItem(idx) - - def _sec_back_to_scan(self): - if self._sec_result: - self._sec_stack.setCurrentIndex(1) - else: - self._sec_stack.setCurrentIndex(0) - self._btn_sec_back.setVisible(False) - self._sec_status_lbl.setText("") - - def _handle_sec(self, item): - kind = item[0] - if kind == "progress": - _, pct, msg = item - self._sec_prog.setValue(pct) - self._sec_status_lbl.setText(msg) - elif kind == "log": - self._log_add("info", f"[敏感提取] {item[1]}") - elif kind == "error": - self._log_add("error", f"[敏感提取] {item[1]}") - self._sec_status_lbl.setText(item[1]) - elif kind == "done": - _, result, js_count, total_size = item - self._sec_scanning = False - self._btn_sec_scan.setEnabled(True) - self._sec_prog.setValue(100) - QTimer.singleShot(500, lambda: self._sec_prog.setVisible(False)) - if result: - total_findings = sum(len(v) for v in result.values() if isinstance(v, list)) - self._sec_status_lbl.setText(f"分析完成,发现 {total_findings} 项") - self._log_add("info", f"[敏感提取] 完成: {js_count} 个 JS, 发现 {total_findings} 项") - appid = "" - app_name = "" - if self._navigator and self._navigator.app_info: - appid = self._navigator.app_info.get("appid", "") - app_name = self._navigator.app_info.get("name", "") - self._sec_show_report(result, js_count, total_size, appid, name=app_name) - else: - self._sec_status_lbl.setText("分析完成,无结果") - - @staticmethod - def _fmt_size(n): - if n < 1024: - return f"{n} B" - elif n < 1024 * 1024: - return f"{n/1024:.1f} KB" - else: - return f"{n/1024/1024:.1f} MB" - - @staticmethod - def _html_esc(s): - return s.replace("&", "&").replace("<", "<").replace(">", ">") - # ────────────────────────────────── # 轮询 # ────────────────────────────────── @@ -2561,12 +2255,14 @@ class App(QMainWindow): else: lv, tx = msg self._log_add(lv, tx) + last_sts = None for _ in range(50): try: - s = self._sts_q.get_nowait() + last_sts = self._sts_q.get_nowait() except queue.Empty: break - self._apply_sts(s) + if last_sts is not None: + self._apply_sts(last_sts) for _ in range(50): try: item = self._rte_q.get_nowait() @@ -2579,23 +2275,33 @@ class App(QMainWindow): except queue.Empty: break self._handle_cld(item) - for _ in range(50): - try: - item = self._sec_q.get_nowait() - except queue.Empty: - break - self._handle_sec(item) def _apply_sts(self, sts): c = _TH[self._tn] + is_connected = sts.get("miniapp", False) for key, (dot, lb, name) in self._dots.items(): on = sts.get(key, False) dot.set_color(c["success"] if on else c["text4"]) lb.setText(f"{name}: {'已连接' if on else '未连接'}") lb.setStyleSheet(f"color: {c['success'] if on else c['text2']};") - self._nav_btns(sts.get("miniapp", False)) - if sts.get("miniapp") and not self._cloud_scan_active and self._auditor: - self._cloud_start_scan() + # 断开时立即禁用(除了已有路由时保留导航按钮) + if not is_connected: + if not self._all_routes: + self._nav_btns(False) + self._btn_vc_enable.setEnabled(False) + self._btn_vc_disable.setEnabled(False) + self._vc_status_lbl.setText("状态: 未连接小程序") + # 已有路由数据时不清除侧栏信息(短暂断连不影响) + if not self._all_routes: + self._sb_fetch_gen += 1 + gen = self._sb_fetch_gen + QTimer.singleShot(5000, lambda: self._delayed_clear_app_info(gen)) + # 连接时延迟启用,等连接稳定(防止重启时反复抖动) + self._vc_stable_gen += 1 + gen_stable = self._vc_stable_gen + if is_connected: + QTimer.singleShot(1500, lambda: self._delayed_stable_connect(gen_stable)) + self._miniapp_connected = is_connected def _handle_rte(self, item): kind = item[0] @@ -2606,11 +2312,31 @@ class App(QMainWindow): elif kind == "app_info": info = item[1] aid = info.get("appid", "") + aname = info.get("name", "") ent = info.get("entry", "") - txt = f"应用: {aid}" if aid else "应用: --" + # 运行状态卡片 — appid + txt = f"AppID: {aid}" if aid else "AppID: --" if ent: txt += f" | 入口: {ent}" self._app_lbl.setText(txt) + # 运行状态卡片 — 名称 + if aname: + self._appname_lbl.setText(f"当前链接小程序: {aname}") + self._appname_lbl.setVisible(True) + else: + self._appname_lbl.setVisible(False) + # 更新侧栏小程序信息卡片 + c = _TH[self._tn] + if aname or aid: + self._sb_app_name.setText(f"名称: {aname}" if aname else "名称: --") + self._sb_app_name.setStyleSheet(f"color: {c['success']};") + self._sb_app_id.setText(f"AppID: {aid}" if aid else "AppID: --") + self._sb_app_id.setStyleSheet(f"color: {c['success']};") + self._sb_app_id.setVisible(True) + else: + self._sb_app_name.setText("未连接") + self._sb_app_name.setStyleSheet(f"color: {c['text3']};") + self._sb_app_id.setVisible(False) elif kind == "current": r = item[1] self._route_lbl.setText(f"当前路由: /{r}" if r else "当前路由: --") @@ -2633,6 +2359,30 @@ class App(QMainWindow): self._btn_auto.setEnabled(True) self._btn_autostop.setEnabled(False) self._log_add("info", "[导航] 遍历完成") + elif kind == "__vc__": + _, enable, ok = item + c = _TH[self._tn] + if ok: + if enable: + self._vc_status_lbl.setText("状态: 已开启 (重启小程序后生效)") + self._vc_status_lbl.setStyleSheet(f"color: {c['success']};") + else: + self._vc_status_lbl.setText("状态: 已关闭 (重启小程序后生效)") + self._vc_status_lbl.setStyleSheet(f"color: {c['text3']};") + else: + self._vc_status_lbl.setText("状态: 操作失败") + self._vc_status_lbl.setStyleSheet(f"color: {c['error']};") + self._btn_vc_enable.setEnabled(True) + self._btn_vc_disable.setEnabled(True) + elif kind == "__vc_detect__": + is_debug = item[1] + c = _TH[self._tn] + if is_debug: + self._vc_status_lbl.setText("状态: 已开启") + self._vc_status_lbl.setStyleSheet(f"color: {c['success']};") + else: + self._vc_status_lbl.setText("状态: 未开启") + self._vc_status_lbl.setStyleSheet(f"color: {c['text3']};") def _handle_cld(self, item): kind = item[0] @@ -2729,8 +2479,19 @@ if __name__ == "__main__": import signal signal.signal(signal.SIGINT, signal.SIG_DFL) # Ctrl+C 直接退出 + # Windows 任务栏图标: 设置 AppUserModelID 使其显示自定义图标而非 Python 默认图标 + if sys.platform == "win32": + try: + import ctypes + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("spade.first.gui") + except Exception: + pass + app = QApplication(sys.argv) app.setFont(QFont(_FN, 9)) + _ico = os.path.join(_BASE_DIR, "icon.ico") + if os.path.exists(_ico): + app.setWindowIcon(QIcon(_ico)) window = App() window.show() sys.exit(app.exec()) diff --git a/gui_config.json b/gui_config.json index b10c967..1fd8366 100644 --- a/gui_config.json +++ b/gui_config.json @@ -4,7 +4,5 @@ "cdp_port": "62000", "debug_main": false, "debug_frida": false, - "selected_preload_scripts": [ - "bypass_debugger.js" - ] + "selected_preload_scripts": [] } \ No newline at end of file diff --git a/proto/wmpf_debug.proto b/proto/wmpf_debug.proto deleted file mode 100644 index b624b13..0000000 --- a/proto/wmpf_debug.proto +++ /dev/null @@ -1,299 +0,0 @@ -syntax = "proto2"; - -package mmbizwxadevremote; - -message WARemoteDebug_DataFormat { - optional uint32 cmd = 1; - optional string uuid = 2; - optional bytes data = 3; -} - -message WARemoteDebug_BaseReq { - optional uint32 clientVersion = 1; -} - -message WARemoteDebug_BaseResp { - optional int32 errcode = 1; - optional string errmsg = 2; -} - -message WARemoteDebug_CommReq { - optional WARemoteDebug_BaseReq baseRequest = 1; -} - -message WARemoteDebug_CommResp { - optional WARemoteDebug_BaseResp baseResponse = 1; -} - -message WARemoteDebug_DebugMessage { - optional uint32 seq = 1; - optional uint32 after = 2; - optional string category = 3; - optional bytes data = 4; - optional uint32 compressAlgo = 5; - optional uint32 originalSize = 6; -} - -message WARemoteDebug_SendDebugMessageReq { - optional WARemoteDebug_BaseReq baseRequest = 1; - repeated WARemoteDebug_DebugMessage debugMessageList = 2; - optional uint32 recvAck = 3; -} - -message WARemoteDebug_SendDebugMessageResp { - optional WARemoteDebug_BaseResp baseResponse = 1; - optional uint32 sendAck = 2; -} - -message WARemoteDebug_NewSendDebugMessageReq { - optional WARemoteDebug_BaseReq baseRequest = 1; - repeated WARemoteDebug_DebugMessage debugMessageList = 2; - optional uint32 recvAck = 3; -} - -message WARemoteDebug_NewSendDebugMessageResp { - optional WARemoteDebug_BaseResp baseResponse = 1; - optional uint32 minAck = 2; - optional uint32 maxAck = 3; -} - -message WARemoteDebug_MessageNotify { - repeated WARemoteDebug_DebugMessage debugMessageList = 1; -} - -message WARemoteDebug_MessageNotifyResp { - optional uint32 recvAck = 1; -} - -message WARemoteDebug_EventNotify { -} - -message WARemoteDebug_EventNotifyResp { -} - -message WARemoteDebug_WxHeartBeatReq { - optional WARemoteDebug_BaseReq baseRequest = 1; - optional uint32 recvAck = 2; -} - -message WARemoteDebug_WxHeartBeatResp { - optional WARemoteDebug_BaseResp baseResponse = 1; -} - -message WARemoteDebug_DevHeartBeatReq { - optional WARemoteDebug_BaseReq baseRequest = 1; - optional uint32 recvAck = 2; -} - -message WARemoteDebug_DevHeartBeatResp { - optional WARemoteDebug_BaseResp baseResponse = 1; -} - -message WARemoteDebug_RoomInfo { - optional bool joinRoom = 1; - optional string roomId = 2; - optional string originalMd5 = 3; - optional uint32 roomStatus = 4; - optional uint32 wxConnStatus = 5; - optional uint32 devConnStatus = 6; -} - -message WARemoteDebug_WxLoginReq { - optional WARemoteDebug_BaseReq baseRequest = 1; - optional string loginTicket = 2; -} - -message WARemoteDebug_WxLoginResp { - optional WARemoteDebug_BaseResp baseResponse = 1; - optional WARemoteDebug_RoomInfo roomInfo = 2; -} - -message WARemoteDebug_DevLoginReq { - optional WARemoteDebug_BaseReq baseRequest = 1; - optional string newticket = 2; - optional uint32 autodev = 3; -} - -message WARemoteDebug_DevLoginResp { - optional WARemoteDebug_BaseResp baseResponse = 1; - optional WARemoteDebug_RoomInfo roomInfo = 2; -} - -message WARemoteDebug_WxJoinRoomReq { - optional WARemoteDebug_BaseReq baseRequest = 1; - optional string username = 2; - optional string roomId = 3; - optional string wxpkgInfo = 4; -} - -message WARemoteDebug_WxJoinRoomResp { - optional WARemoteDebug_BaseResp baseResponse = 1; -} - -message WARemoteDebug_DevJoinRoomReq { - optional WARemoteDebug_BaseReq baseRequest = 1; - optional string appid = 2; - optional string roomId = 3; - optional string wxpkgInfo = 4; -} - -message WARemoteDebug_DevJoinRoomResp { - optional WARemoteDebug_BaseResp baseResponse = 1; -} - -message WARemoteDebug_WxQuitRoomReq { - optional WARemoteDebug_BaseReq baseRequest = 1; -} - -message WARemoteDebug_WxQuitRoomResp { - optional WARemoteDebug_BaseResp baseResponse = 1; -} - -message WARemoteDebug_DevQuitRoomReq { - optional WARemoteDebug_BaseReq baseRequest = 1; -} - -message WARemoteDebug_DevQuitRoomResp { - optional WARemoteDebug_BaseResp baseResponse = 1; -} - -message WARemoteDebug_WxSyncMessageReq { - optional WARemoteDebug_BaseReq baseRequest = 1; - optional uint32 minSeq = 2; - optional uint32 maxSeq = 3; -} - -message WARemoteDebug_WxSyncMessageResp { - optional WARemoteDebug_BaseResp baseResponse = 1; - repeated WARemoteDebug_DebugMessage debugMessageList = 2; - optional uint32 sendAck = 3; -} - -message WARemoteDebug_DevSyncMessageReq { - optional WARemoteDebug_BaseReq baseRequest = 1; - optional uint32 minSeq = 2; - optional uint32 maxSeq = 3; -} - -message WARemoteDebug_DevSyncMessageResp { - optional WARemoteDebug_BaseResp baseResponse = 1; - repeated WARemoteDebug_DebugMessage debugMessageList = 2; - optional uint32 sendAck = 3; -} - -message WARemoteDebug_MethodWithArgs { - optional string methodName = 1; - repeated string methodArgList = 2; -} - -message WARemoteDebug_RegisterInterface { - optional string objName = 1; - repeated WARemoteDebug_MethodWithArgs objMethodList = 2; -} - -message WARemoteDebug_DeviceInfo { - optional string deviceName = 1; - optional string deviceModel = 2; - optional string systemVersion = 3; - optional string wechatVersion = 4; - optional uint32 publibVersion = 5; - optional float screenWidth = 6; - optional float pixelRatio = 7; - optional string userAgent = 8; -} - -message WARemoteDebug_SetupContext { - optional WARemoteDebug_RegisterInterface registerInterface = 1; - optional WARemoteDebug_DeviceInfo deviceInfo = 2; - optional string configureJs = 3; - optional string publicJsMd5 = 4; - optional string threeJsMd5 = 5; - optional uint32 supportCompressAlgo = 6; -} - -message WARemoteDebug_CallInterface { - optional string objName = 1; - optional string methodName = 2; - repeated string methodArgList = 3; - optional uint32 callId = 4; -} - -message WARemoteDebug_CallInterfaceResult { - optional string ret = 1; - optional uint32 callId = 2; - optional string debugInfo = 3; -} - -message WARemoteDebug_EvaluateJavascript { - optional string script = 1; - optional uint32 evaluateId = 2; - optional string debugInfo = 3; -} - -message WARemoteDebug_EvaluateJavascriptResult { - optional string ret = 1; - optional uint32 evaluateId = 2; -} - -message WARemoteDebug_Breakpoint { - optional bool isHit = 1; -} - -message WARemoteDebug_Ping { - optional uint64 pingId = 1; - optional string payload = 2; -} - -message WARemoteDebug_Pong { - optional uint64 pingId = 1; - optional uint32 networkType = 2; - optional string payload = 3; -} - -message WARemoteDebug_DomOp { - optional string params = 1; - optional uint32 webviewId = 2; -} - -message WARemoteDebug_DomEvent { - optional string params = 1; - optional uint32 webviewId = 2; -} - -message WARemoteDebug_NetworkDebugAPI { - optional string apiName = 1; - optional string taskId = 2; - optional string requestHeaders = 3; - optional uint64 timestamp = 4; -} - -message WARemoteDebug_ChromeDevtools { - optional uint64 opId = 1; - optional string payload = 2; - optional string jscontextId = 3; -} - -message WARemoteDebug_ChromeDevtoolsResult { - optional uint64 opId = 1; - optional string payload = 2; - optional string jscontextId = 3; -} - -message WARemoteDebug_AddJsContext { - optional string jscontextId = 1; - optional string jscontextName = 2; -} - -message WARemoteDebug_RemoveJsContext { - optional string jscontextId = 1; -} - -message WARemoteDebug_ConnectJsContext { - optional string jscontextId = 1; -} - -message WARemoteDebug_CustomMessage { - optional string method = 1; - optional string payload = 2; - optional bytes raw = 3; -} diff --git a/src/__pycache__/__init__.cpython-310.pyc b/src/__pycache__/__init__.cpython-310.pyc index e5947f1f81f95bd31a4fb836accce2ba5760ee98..e558f38335c627800449593931d457ae508ee5c7 100644 GIT binary patch delta 60 zcmbQj*u%)3&&$ij00fJ_T$;#jqpI(06%$&VT2vfUR-B)cSyEaYe)-Nm0&&ez)E!KC*FVD-#PfRJ+FQ_cZ W$j@^#G*Zwo(bO+4O4gs4qW}QFMir$1 diff --git a/src/__pycache__/__init__.cpython-314.pyc b/src/__pycache__/__init__.cpython-314.pyc index 206a3c5c8619d8f653fa43343917517b34c5b80e..40ece51511851f776193fe6299f60078b413d82f 100644 GIT binary patch delta 52 zcmbQsxR{Y!n~#@^0SFd-xipd6l+j_Lql83$d0tL_VoGt0TV_#liGqfaCWJLH+Xw)E C#}FC- delta 46 zcmZ3?IG2%In~#@^0SNk5?ViYO%4jvwQ9>xSIJ+djAf~)RH!riaDx)+pZ(^Yl02}oV AO#lD@ diff --git a/src/__pycache__/cli.cpython-310.pyc b/src/__pycache__/cli.cpython-310.pyc index 7ce915e3770fd496639547e34727781ff1927815..63fcb06ba5215361140f9e67261cb10d5a5243d3 100644 GIT binary patch delta 55 zcmew%@<4<)pO=@50SFdl_eSZd2WVA3K}MwoBuHSG4tw& g`v$nVq$ZW7r>7Q84q%nyRL}?jE7RPZ#rll}0P@=zbpQYW diff --git a/src/__pycache__/cli.cpython-314.pyc b/src/__pycache__/cli.cpython-314.pyc index e7408703cdf94952e99ef5ed3324c91432620cf9..3bedcb307b91c9dbfe146bcaf3afe93cf2259c13 100644 GIT binary patch delta 56 zcmbQEwn&Xzn~#@^0SFd-xwMh{2qUBQ&TZsA!pLYk`2wSaP-<~@Nq#|0d4+CXW@%MMX=2_cCM7`t DNOKOo diff --git a/src/__pycache__/cloud_audit.cpython-310.pyc b/src/__pycache__/cloud_audit.cpython-310.pyc index 88c86e14fcd365c640208cf177e45eb3c6f446be..2e23723b31877514a48c36f1b8e454b88b1badc0 100644 GIT binary patch delta 383 zcmWlTze~eF6vsW2*0!P49~l~3q!t}SBG|D)1(l%SU=>knRYG!&p-C>>B`MS*>c4QS zAmZd$GKqrV=BVTS9WHL(z2W`f`@Wy|UYp~lf{Mk$1dr$6pRuwrS!--JyT=4#(oG2R z+$bj99Z32S3cH4jNt6bYdT}djp`hnFEN3A<^k4+p6p1Yh62j!b^dXaAkTBUcBa_KJ z)Ad?1ztJhgp@&S{a6L%BOSL^43a=PjIj*yeh2kJ$l4;v48;9HuSr$VdC7fs@3u}Us zHysB!T8$qAoYkI4xv|f1t=k%lUs00Es5NSl+YA1Uq+BW^FhW|@Wc~<}c delta 363 zcmW-aKTE?v7{&-?N|Kb~i7yc#OjwCseK^Pl&zT1%}M+lJYuh){D# zvFC;nHFps0hd3~UtFVXt=K4l?WwR1@Q)}8*#=?WZLm}dF7rGvoZ5To>`*47`g!~~_ zEbMn(hfmVTvJj!D88MoC6;*E7QBX$wbw*qMTkLy!YSyaa8V zr!RYmAg)A#s6j+eP|3y?R|p#5A*)S)<*LaO1YDx7{Rntmnb`+`v(YTmTEeQN$*NeS z6&L1~0oY}Y{4r>-`+QljZ~0#dRM_)EUu`GQVIoJYsxN^m+tU|;75lmbz!@WjYjDE8 h3-jQHDa9vn%3g{O>P5nj61iot)KTvKL)b?t_yc>8boc-O diff --git a/src/__pycache__/cloud_audit.cpython-314.pyc b/src/__pycache__/cloud_audit.cpython-314.pyc index 8e67aed6b916863ca620e842ff590f15dba942d6..84ef03928176d520e58e2ad444f6eeaf62d3bca4 100644 GIT binary patch delta 56 zcmexd@Uwthn~#@^0SFd-xwMgcG83co5fnTgSM@_Z%pO=@50SFd0N4-~e*gdg diff --git a/src/__pycache__/codex.cpython-314.pyc b/src/__pycache__/codex.cpython-314.pyc index 38a48e890cc69548027fb93c0fc5fda4c8e8553c..818c4e49c99ef86a0bf088542486dd76bca28734 100644 GIT binary patch delta 75 zcmX?KaJ7J2n~#@^0SFd-xwMh{45Oi~vsFxJacWU4|etBL_ Zequ^-j9X?=afyP4ktT$-nTg5Q3;^~58j}D3 delta 69 zcmcawaK34|YH@Z+ XenCumg>GJEX;nsPV%}yBCSNlEPjMLZ diff --git a/src/__pycache__/constants.cpython-310.pyc b/src/__pycache__/constants.cpython-310.pyc index 31b213d762a0d183c16d14eed13a707cb3e85e74..160804289f7978ac72bffe6137c93d69a93ef0f4 100644 GIT binary patch delta 37 scmdlZu|Fr{z;0MC32qW}N^ delta 45 zcmdlYu}6YCpO=@50SG3o+P#r`DHEf`kXovkngc1C1Aq diff --git a/src/__pycache__/engine.cpython-310.pyc b/src/__pycache__/engine.cpython-310.pyc index 5ffe4c11bfdc4f04c1d05f2065fb91165020542c..8f3c4bbf8b49355d11b260d050a516f5b327a4f3 100644 GIT binary patch delta 309 zcmexV{;8ZdpO=@50SKhOU(HP1$g9iDs5{x3S&G#yv#7Xaa}x7GIY!CJDoVPnQd!bj zGLyrUycuODFH+KFl-+zmsh){ZeR6=R9CvwQQC?>N3hr-lw{T zl?5cVIa95YiE;Jh#~M}wn}C)TZ37X4AY$WYUCniDimO5FH6UUwhzJ7_aUfzHkkDi< zT0i-Zz9!?Q$!Z3LY@0#iTPDvnn92>-1~zQ_WLrZObi?;-E;LkPXFCAWaA@*Oi|qm% zK+IDh0&M%q%?Xy=jEuV`=UA1oodXG9n*72_6X;YS>oS2|AOSTHp#>r?OrBvK1pvaL BSMLA- delta 307 zcmexV{;8ZdpO=@50SNSNAIMDD$g9iDXgb-NS&FxyvLquv&&|+CLBm9Ia}@JIIcDAz zsmTcnB9o1j#91Y?q_U(Z`zd)d%1oZ6q{}F?`GitE6Qk;64^=tV)S{yNqRF|cUW~Gn zcc`w}oTXO5#JFnmV+|{TjXHx#dd-9 zAm&LB0k-|b<^)S_M#i0!bF9kP&Vqz5PJUsf33RHEb(z3UkboM9&;k+XC(p2s0s#63 BRxJPk diff --git a/src/__pycache__/engine.cpython-313.pyc b/src/__pycache__/engine.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9954a9dfe814b983674ecd767a82fcfa5f05fcfd GIT binary patch literal 29875 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

F$Rp#+4< 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 N9f)%rL%EJj{sZXdE$#pS diff --git a/src/__pycache__/logger.cpython-310.pyc b/src/__pycache__/logger.cpython-310.pyc index 24f2094e125d8c3c38ac5a0aef7bb93bf3c30a99..11b624841f8e5b472ca0c4fa236ab2b4bf2780e9 100644 GIT binary patch delta 58 zcmbQiKY^b+pO=@50SFdzPY02*`@oB#j- delta 66 zcmbQhKZBn;pO=@50SL5s_iW_uWYjQqwu%WYPAw{qDJ#y;$t)=?j&VsX&MwI>h$*Nn U$;i)hGc;1rFwxw+h4Bt60C)iv761SM diff --git a/src/__pycache__/logger.cpython-314.pyc b/src/__pycache__/logger.cpython-314.pyc index 0476d98fc0aef81084b9ade9c59bd99d530d8171..32f71ee9fb0ed790c4e2c9934667ae1d76a335b1 100644 GIT binary patch delta 75 zcmaDO@kxSPn~#@^0SFd-xwMfxlF`u4*(xTqIJKxa#xW%~GcU8aq$sf@zbM8fzdSD| ZKQW~^#x1j`xI{t2NE5=^+|PKI3jn@W8k+zB delta 69 zcmew)@kWAMn~#@^0SL5s_iW^jWYo5Bwu%WYPAw{qaZJh0%*!k;DM~EKFN$$VEzT~< XFNi6x(9O#%t;#4(%-cMb@h%qt+p!nU diff --git a/src/__pycache__/navigator.cpython-310.pyc b/src/__pycache__/navigator.cpython-310.pyc index 8c71a368d2be0da024c4281d51b6b8a85791dc0c..51b51ed74d0f88bb6c4b2b5378bba8dbfa9db363 100644 GIT binary patch delta 58 zcmX@2y+@lnpO=@50SFdTL1t6 delta 66 zcmdm^eMFl(pO=@50SK7i9oWcS$)sW7Y!wq)oLW>IQ&yaxlUY()9OIH&oL!P%5K~ZD Ul98Y1W@x0KVWPQt36q2{0KUZ)lK=n! diff --git a/src/__pycache__/navigator.cpython-314.pyc b/src/__pycache__/navigator.cpython-314.pyc index d5c27db43edf2d704cc70b815dc743d7738a7ea3..6b5b3f68246aa1fce04735b0b98c6515e8798ee7 100644 GIT binary patch delta 343 zcmdn$_05YqM%`<31Mv(U~Xbze7kuvyAUH&n(gEbB651xWr;-!iMASPYHDeE z@$uyq&iQ$1ndzD}HRYLkDf#6{Vl_1ylXr2*Giq+W$Fp1$@0SDo8x#tFfuxB z_UC73V)UHs&u_JPqF@IjW8h{f;X_P}VVm!Y@-Q37ERRaJap=J;O delta 302 zcmez7wbhGHn~#@^0SK7i9mw1vx{>cXleV?9RZM7cYEf~FV@hsjUS@GgQDRAcQH)D! zadt_5K}>mtZeC_-RYqxI-exi8CKkrcn`g2MF^VSImS^UrdS~S(3AlQJS4s@Phz@sKf_h1_}Mo3|tai-x;`sxH{NAh)q7k)y1RDz^VR`2`KX8 zi^ApzZcQdO3!uA-gD1xei*KIC`+)*uR-kIf_v>3Ml-e uouPr@CQHC47FJgIFMQ0bN}sr7Sh+sgOs-UU$M40&sPvfuL>37E6$1cg;afHU diff --git a/src/__pycache__/userscript.cpython-310.pyc b/src/__pycache__/userscript.cpython-310.pyc index 69e0d456afe6adb273cfc540b88c0f68ba1292e1..902e14fd459b51f6ac1ff463d16492296bd53aa7 100644 GIT binary patch delta 37 rcmZpb`76zx&&$ij00fJ_T-wOZ!pvwmS%6uJ)h)BAxMZ_A^Eoa6ygUj> delta 45 zcmew>-73SK&&$ij00cItcW>loVP>?PEWj+qTToe&k)P*gXr!QFqPbb0`5YGj0_Y2o diff --git a/src/__pycache__/userscript.cpython-314.pyc b/src/__pycache__/userscript.cpython-314.pyc index 4c459be10ad7bcd3458faaf0a9c1aa704275450a..146dba2dcf899d5167ca8cf2fccdcdb7ae67a54e 100644 GIT binary patch delta 56 zcmZ2#vD$)Ln~#@^0SFd-xwMh{G83cIv!t{*#wE2lyClCL#x1j` JxMX6!ApqJZ6A%CZ delta 63 zcmbQixPXy6pO=@50SG3n+C7omM#I_JDkiizwWv6ztT;a>v!t{*#wE2lyClCLrl7JU RBR|j0&`3eUL~~-gApmBs6pR1> diff --git a/src/third_party/__pycache__/__init__.cpython-314.pyc b/src/third_party/__pycache__/__init__.cpython-314.pyc index bbccedc056f2056eafa9012d71640a0d460be151..dd7924acebd33de905e46f4bec4c60119ac7386e 100644 GIT binary patch delta 52 zcmZ3mtZeC_-RYqxI-o!#X03ovu Ae*gdg diff --git a/src/third_party/__pycache__/wmpf_debug_pb2.cpython-310.pyc b/src/third_party/__pycache__/wmpf_debug_pb2.cpython-310.pyc index 22d21a9edc0eaa67e282ff2398ab7049453e9013..0a96f2758b54956681a04149ec2defb4028b05d1 100644 GIT binary patch delta 37 rcmbOj*cr&3&&$ij00fJ_T-wO3sLtp(Syx?()h)BAxMXvPx-BCB#fl1q delta 45 zcmeASoEXTR&&$ij00c>^c5mcXRA=;@tg9}?TToe&k)P*gXr!QFqPf{e-Ifsm5TOgw diff --git a/src/third_party/__pycache__/wmpf_debug_pb2.cpython-314.pyc b/src/third_party/__pycache__/wmpf_debug_pb2.cpython-314.pyc index aedae970899029430f998797472a1b4d8db5b52a..a16749b514db3fe2cfce6ff49ba911e6c1b44054 100644 GIT binary patch delta 74 zcmX?@dpwt0n~#@^0SFd-xwMhHTi-Cq*(xTqIJKxa#xW%~GcU8aq$sf@zbM8fzdSD| YKQW~^#x1j`xI{t2NE5Iy WUl3DXp_`XkT9r|nn78=2K05#$PZ-(&