This commit is contained in:
牟不懂哎
2026-04-19 15:11:59 +08:00
parent 285b893d07
commit 4a64e54935
29 changed files with 149 additions and 186 deletions
+144 -97
View File
@@ -22,6 +22,7 @@ from PySide6.QtWidgets import (
QTreeWidget, QTreeWidgetItem, QProgressBar, QStackedWidget,
QMenu, QHeaderView, QAbstractItemView, QFileDialog, QInputDialog,
QTabWidget, QTableWidget, QTableWidgetItem, QDialog, QSizePolicy,
QCheckBox,
)
from src.cli import CliOptions, CDP_PORT
@@ -29,7 +30,6 @@ from src.logger import Logger
from src.engine import DebugEngine
from src.navigator import MiniProgramNavigator
from src.cloud_audit import CloudAuditor
from src.userscript import load_userscripts_by_files
# ══════════════════════════════════════════
# 配色
@@ -71,7 +71,6 @@ if getattr(sys, 'frozen', False):
_BASE_DIR = os.path.dirname(sys.executable)
_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)
@@ -411,6 +410,28 @@ def build_qss(tn):
margin: 4px 8px;
}}
/* ── QCheckBox ── */
QCheckBox {{
color: {c['text1']};
background: transparent;
spacing: 5px;
}}
QCheckBox::indicator {{
width: 14px;
height: 14px;
border-radius: 3px;
border: 1px solid {c['border2']};
background: {c['input']};
}}
QCheckBox::indicator:checked {{
background: {c['accent']};
border-color: {c['accent']};
image: none;
}}
QCheckBox::indicator:hover {{
border-color: {c['accent']};
}}
/* ── Hook 行 ── */
QFrame.hook_row {{
background: {row_bg};
@@ -638,10 +659,13 @@ class App(QMainWindow):
self._cancel_ev = None
self._route_poll_id = None
self._all_routes = []
self._flat_routes = [] # tree visual order for prev/next
self._cloud_scan_active = False
self._cloud_scan_poll_timer = None
self._redirect_guard_on = False
self._hook_injected = set()
self._global_hook_scripts = set(self._cfg.get("global_hook_scripts", []))
self._global_inject_gen = 0
self._blocked_seen = 0
self._miniapp_connected = False
self._sb_fetch_gen = 0
@@ -650,8 +674,6 @@ class App(QMainWindow):
self._sts_q = queue.Queue()
self._rte_q = queue.Queue()
self._cld_q = queue.Queue()
self._selected_preload = list(self._cfg.get("selected_preload_scripts", []))
self._nav_route_idx = -1
self._sb_items = {}
@@ -866,30 +888,6 @@ class App(QMainWindow):
lay.addWidget(c1)
# Card 2: 前加载脚本
c2 = _make_card()
c2_lay = QVBoxLayout(c2)
c2_lay.setContentsMargins(16, 10, 16, 10)
c2_lay.setSpacing(4)
hdr_row = QHBoxLayout()
hdr_row.addWidget(_make_label("前加载脚本", bold=True))
hdr_row.addWidget(_make_label("(启动调试前可用)", muted=True))
hdr_row.addStretch()
self._btn_preload_refresh = _make_btn("刷新", self._preload_refresh)
hdr_row.addWidget(self._btn_preload_refresh)
c2_lay.addLayout(hdr_row)
preload_scroll = QScrollArea()
preload_scroll.setWidgetResizable(True)
preload_scroll.setMaximumHeight(160)
preload_inner = QWidget()
self._preload_container = QVBoxLayout(preload_inner)
self._preload_container.setSpacing(2)
self._preload_container.setContentsMargins(0, 0, 0, 0)
preload_scroll.setWidget(preload_inner)
c2_lay.addWidget(preload_scroll)
lay.addWidget(c2)
self._preload_refresh()
# Action row
ar = QHBoxLayout()
self._btn_start = _make_btn("▶ 启动调试", self._do_start)
@@ -1143,13 +1141,27 @@ class App(QMainWindow):
name_lbl.setFont(QFont(_FN, 10))
row_lay.addWidget(name_lbl, 1)
is_global = fn in self._global_hook_scripts
injected = fn in self._hook_injected
status_lbl = QLabel("● 已注入" if injected else "○ 未注入")
if is_global and injected:
status_text = "全局 ● 已注入"
elif is_global:
status_text = "全局 ○ 待注入"
elif injected:
status_text = "● 已注入"
else:
status_text = "○ 未注入"
status_lbl = QLabel(status_text)
c = _TH[self._tn]
status_lbl.setStyleSheet(f"color: {c['success'] if injected else c['text3']};")
row_lay.addWidget(status_lbl)
self._hook_status_lbls[fn] = status_lbl
global_cb = QCheckBox("全局")
global_cb.setChecked(is_global)
global_cb.toggled.connect(lambda checked, f=fn: self._hook_global_toggle(f, checked))
row_lay.addWidget(global_cb)
inject_btn = _make_btn("注入", lambda checked=False, f=fn: self._hook_inject(f))
row_lay.addWidget(inject_btn)
clear_btn = _make_btn("清除", lambda checked=False, f=fn: self._hook_clear(f))
@@ -1190,8 +1202,57 @@ class App(QMainWindow):
c = _TH[self._tn]
lbl = self._hook_status_lbls.get(filename)
if lbl:
lbl.setText("● 已注入" if injected else "○ 未注入")
lbl.setStyleSheet(f"color: {c['success'] if injected else c['text3']};")
is_global = filename in self._global_hook_scripts
if is_global and injected:
lbl.setText("全局 ● 已注入")
lbl.setStyleSheet(f"color: {c['success']};")
elif is_global:
lbl.setText("全局 ○ 待注入")
lbl.setStyleSheet(f"color: {c['text3']};")
elif injected:
lbl.setText("● 已注入")
lbl.setStyleSheet(f"color: {c['success']};")
else:
lbl.setText("○ 未注入")
lbl.setStyleSheet(f"color: {c['text3']};")
def _hook_global_toggle(self, filename, checked):
if checked:
self._global_hook_scripts.add(filename)
else:
self._global_hook_scripts.discard(filename)
injected = filename in self._hook_injected
self._hook_update_status(filename, injected)
self._auto_save()
def _hook_auto_inject_globals(self):
"""Auto-inject all global hook scripts (called when miniapp stabilizes)."""
if not self._engine or not self._loop or not self._loop.is_running():
return
hook_dir = os.path.join(_BASE_DIR, "hook_scripts")
for fn in list(self._global_hook_scripts):
if fn in self._hook_injected:
continue
filepath = os.path.join(hook_dir, fn)
if not os.path.isfile(filepath):
continue
try:
with open(filepath, "r", encoding="utf-8") as f:
source = f.read()
except Exception:
continue
asyncio.run_coroutine_threadsafe(
self._ahook_inject(fn, source), self._loop)
def _do_global_inject(self, gen):
"""独立的全局注入定时回调,只在小程序连接变化时触发,不受 CDP 等影响。"""
if gen != self._global_inject_gen:
return
if not self._miniapp_connected:
return
if self._global_hook_scripts:
self._hook_auto_inject_globals()
self._log_add("info", "[Hook] 自动注入全局脚本")
# ── 云扫描 ──
@@ -1342,11 +1403,11 @@ class App(QMainWindow):
# 自动反编译 & 自动提取 开关
func_row.addWidget(_make_label("自动反编译"))
self._tog_auto_dec = ToggleSwitch(self._cfg.get("auto_decompile", False))
self._tog_auto_dec.toggled.connect(lambda v: (self._auto_save(), self._ext_auto_process_pending() if v else None))
self._tog_auto_dec.toggled.connect(lambda v: self._auto_save())
func_row.addWidget(self._tog_auto_dec)
func_row.addWidget(_make_label("自动提取"))
self._tog_auto_scan = ToggleSwitch(self._cfg.get("auto_scan", False))
self._tog_auto_scan.toggled.connect(lambda v: (self._auto_save(), self._ext_auto_process_pending() if v else None))
self._tog_auto_scan.toggled.connect(lambda v: self._auto_save())
func_row.addWidget(self._tog_auto_scan)
main_lay.addLayout(func_row)
@@ -1688,7 +1749,12 @@ class App(QMainWindow):
c = _TH[self._tn]
output_base = os.path.join(_BASE_DIR, "output")
for appid, pkgs in sorted(appid_groups.items()):
# 按最新包文件的修改时间排序,最旧的先插入,最新的最后插入到位置0因此显示在最上面
def _appid_mtime(item):
_, pkgs = item
return max((os.path.getmtime(p["path"]) for p in pkgs if os.path.exists(p["path"])), default=0)
for appid, pkgs in sorted(appid_groups.items(), key=_appid_mtime):
row = QFrame()
row.setStyleSheet(
f"QFrame {{ background: {c['input']}; border-radius: 8px; }}"
@@ -1763,8 +1829,8 @@ class App(QMainWindow):
"btn_view": btn_view, "btn_del": btn_del, "lbl_name": lbl_name,
}
# 插入在 stretch 之前
self._ext_list_layout.insertWidget(self._ext_list_layout.count() - 1, row)
# 插入在最前面(最新的在上面)
self._ext_list_layout.insertWidget(0, row)
self._ext_status_lbl.setText(f"发现 {len(appid_groups)} 个小程序")
# 记录当前已知 appid 集合 (供目录监控用)
@@ -2732,47 +2798,6 @@ class App(QMainWindow):
self._stack.addWidget(page)
self._page_map["logs"] = self._stack.count() - 1
# ──────────────────────────────────
# 前加载脚本
# ──────────────────────────────────
def _preload_refresh(self):
while self._preload_container.count():
item = self._preload_container.takeAt(0)
w = item.widget()
if w:
w.deleteLater()
scripts_dir = os.path.join(_BASE_DIR, "userscripts")
js_files = sorted(f for f in os.listdir(scripts_dir) if f.endswith(".js")) if os.path.isdir(scripts_dir) else []
if not js_files:
lbl = QLabel("userscripts/ 目录下无 .js 文件")
lbl.setProperty("class", "muted")
self._preload_container.addWidget(lbl)
return
for fn in js_files:
row = QHBoxLayout()
tog = ToggleSwitch(fn in self._selected_preload)
tog.setFixedSize(36, 18)
tog.toggled.connect(lambda v, f=fn: self._preload_toggle(f, v))
row.addWidget(tog)
row.addWidget(QLabel(fn))
row.addStretch()
w = QWidget()
w.setLayout(row)
self._preload_container.addWidget(w)
def _preload_toggle(self, filename, val):
if val:
if filename not in self._selected_preload:
self._selected_preload.append(filename)
else:
if filename in self._selected_preload:
self._selected_preload.remove(filename)
self._auto_save()
# ──────────────────────────────────
# 页面切换
# ──────────────────────────────────
@@ -2836,11 +2861,11 @@ class App(QMainWindow):
"cdp_port": self._cp_ent.text(),
"debug_main": self._tog_dm.isChecked(),
"debug_frida": self._tog_df.isChecked(),
"selected_preload_scripts": list(self._selected_preload),
"extract_packages_dir": self._ext_path_ent.text(),
"extract_custom_patterns": dict(self._ext_custom_patterns),
"auto_decompile": self._tog_auto_dec.isChecked(),
"auto_scan": self._tog_auto_scan.isChecked(),
"global_hook_scripts": list(self._global_hook_scripts),
}
_save_cfg(data)
@@ -2898,22 +2923,15 @@ class App(QMainWindow):
except ValueError:
self._log_add("error", "[gui] 端口号无效")
return
scripts_dir = os.path.join(_BASE_DIR, "userscripts")
selected_files = [os.path.join(scripts_dir, fn) for fn in self._selected_preload]
opts = CliOptions(
cdp_port=cp,
debug_main=self._tog_dm.isChecked(),
debug_frida=self._tog_df.isChecked(),
scripts_dir=scripts_dir,
script_files=selected_files)
scripts_dir="",
script_files=[])
logger = Logger(opts)
logger.set_output_callback(lambda lv, tx: self._log_q.put((lv, tx)))
us = load_userscripts_by_files(selected_files) if selected_files else []
if us:
logger.info(f"[脚本] 已加载 {len(us)} 个")
else:
logger.info("[脚本] 无脚本")
self._engine = DebugEngine(opts, logger, us)
self._engine = DebugEngine(opts, logger)
self._navigator = MiniProgramNavigator(self._engine)
self._auditor = CloudAuditor(self._engine)
self._engine.on_status_change(lambda s: self._sts_q.put(s))
@@ -3115,6 +3133,8 @@ class App(QMainWindow):
def _do_go(self):
r = self._sel_route()
if r and self._engine and self._loop:
if r in self._flat_routes:
self._nav_route_idx = self._flat_routes.index(r)
asyncio.run_coroutine_threadsafe(
self._anav("navigate_to", r, "跳转"), self._loop)
@@ -3193,31 +3213,33 @@ class App(QMainWindow):
self._btn_auto.setEnabled(True)
def _do_prev(self):
if not self._all_routes:
routes = self._flat_routes or self._all_routes
if not routes:
self._log_add("error", "[导航] 请先获取路由")
return
if self._nav_route_idx <= 0:
self._nav_route_idx = len(self._all_routes) - 1
self._nav_route_idx = len(routes) - 1
else:
self._nav_route_idx -= 1
route = self._all_routes[self._nav_route_idx]
route = routes[self._nav_route_idx]
self._select_tree_route(route)
self._log_add("info", f"[导航] 上一个: {route} ({self._nav_route_idx + 1}/{len(self._all_routes)})")
self._log_add("info", f"[导航] 上一个: {route} ({self._nav_route_idx + 1}/{len(routes)})")
if self._engine and self._loop:
asyncio.run_coroutine_threadsafe(
self._anav("navigate_to", route, "跳转"), self._loop)
def _do_next(self):
if not self._all_routes:
routes = self._flat_routes or self._all_routes
if not routes:
self._log_add("error", "[导航] 请先获取路由")
return
if self._nav_route_idx >= len(self._all_routes) - 1:
if self._nav_route_idx >= len(routes) - 1:
self._nav_route_idx = 0
else:
self._nav_route_idx += 1
route = self._all_routes[self._nav_route_idx]
route = routes[self._nav_route_idx]
self._select_tree_route(route)
self._log_add("info", f"[导航] 下一个: {route} ({self._nav_route_idx + 1}/{len(self._all_routes)})")
self._log_add("info", f"[导航] 下一个: {route} ({self._nav_route_idx + 1}/{len(routes)})")
if self._engine and self._loop:
asyncio.run_coroutine_threadsafe(
self._anav("navigate_to", route, "跳转"), self._loop)
@@ -3320,6 +3342,9 @@ class App(QMainWindow):
parts = p.split("/")
g = parts[0] if len(parts) > 1 else "(root)"
groups.setdefault(g, []).append(p)
flat = [] # tree visual order for prev/next
tl = [p for p in pages if p in tabs]
if tl:
nd = QTreeWidgetItem(["TabBar"])
@@ -3330,6 +3355,7 @@ class App(QMainWindow):
child = QTreeWidgetItem([d])
child.setData(0, Qt.UserRole, p)
nd.addChild(child)
flat.append(p)
for g in sorted(groups):
nd = QTreeWidgetItem([g])
self._tree.addTopLevelItem(nd)
@@ -3340,8 +3366,16 @@ class App(QMainWindow):
child = QTreeWidgetItem([d])
child.setData(0, Qt.UserRole, p)
nd.addChild(child)
flat.append(p)
self._flat_routes = flat
self._tree.setUpdatesEnabled(True)
# 默认选中并定位到第一个页面
if flat and self._nav_route_idx < 0:
self._nav_route_idx = 0
self._select_tree_route(flat[0])
def _select_tree_route(self, route):
"""Select the tree item matching the given route path."""
for i in range(self._tree.topLevelItemCount()):
@@ -3606,17 +3640,25 @@ class App(QMainWindow):
lb.setText(f"{name}: {'已连接' if on else '未连接'}")
lb.setStyleSheet(f"color: {c['success'] if on else c['text2']};")
# 断开时立即禁用(除了已有路由时保留导航按钮)
if not is_connected:
if not is_connected and self._miniapp_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("状态: 未连接小程序")
# 清除注入标记,切换小程序时全局脚本会重新注入
self._hook_injected.clear()
self._hook_refresh()
# 已有路由数据时不清除侧栏信息(短暂断连不影响)
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))
# 小程序连接时,独立定时器触发全局 Hook 注入(不受 CDP 等状态变化影响)
if is_connected and not self._miniapp_connected and self._global_hook_scripts:
self._global_inject_gen += 1
_gi_gen = self._global_inject_gen
QTimer.singleShot(1500, lambda: self._do_global_inject(_gi_gen))
# 连接时延迟启用,等连接稳定(防止重启时反复抖动)
self._vc_stable_gen += 1
gen_stable = self._vc_stable_gen
@@ -3661,6 +3703,11 @@ class App(QMainWindow):
elif kind == "current":
r = item[1]
self._route_lbl.setText(f"当前路由: /{r}" if r else "当前路由: --")
if r:
routes = self._flat_routes or self._all_routes
if r in routes:
self._nav_route_idx = routes.index(r)
self._select_tree_route(r)
elif kind == "progress":
_, i, total, route = item
if total > 0: