diff --git a/lite/proxy_lite.py b/lite/proxy_lite.py index 2ff8369..377a156 100644 --- a/lite/proxy_lite.py +++ b/lite/proxy_lite.py @@ -663,6 +663,55 @@ def _nodes_decrypt(blob: bytes) -> str: except Exception: return blob.decode("utf-8") +def _auth_save(username, password): + """登录凭据加密保存到用户数据目录(DPAPI,本机本用户可解)。""" + import json + text = json.dumps({"username": username, "password": password}, ensure_ascii=False) + _USER_DIR.mkdir(parents=True, exist_ok=True) + (_USER_DIR / "auth.json").write_bytes(_nodes_encrypt(text)) + +def _auth_load(): + """读取已保存的登录凭据;不存在或损坏返回 None。""" + try: + import json + f = _USER_DIR / "auth.json" + if not f.is_file(): + return None + data = json.loads(_nodes_decrypt(f.read_bytes())) + if not (isinstance(data, dict) and data.get("username") and data.get("password")): + return None + return data + except Exception: + return None + +def _nodes_sync_remote(): + """启动时用已保存凭据拉取最新节点配置(内容比对,变化才覆盖)。""" + cred = _auth_load() + if not cred: + return # 从未登录过,等登录框处理 + url = str(_config_val("nodes_url", "") or "").strip() + if not url: + return + try: + import json + data = _fetch_nodes_auth(url, cred["username"], cred["password"]) + if not (isinstance(data, list) and data + and all(isinstance(n, dict) for n in data) + and str(data[0].get("domain") or "").strip()): + raise ValueError("返回的节点数据无效") + text = json.dumps(data, ensure_ascii=False, indent=2) + "\n" + local = _USER_DIR / "nodes.json" + if local.is_file() and _nodes_decrypt(local.read_bytes()) == text: + log("节点配置已是最新") + return + _USER_DIR.mkdir(parents=True, exist_ok=True) + local.write_bytes(_nodes_encrypt(text)) + global _NODES + _NODES = None # 使后续读取(节点菜单/开启代理)拿到新配置 + log(f"节点配置已更新: {len(data)} 个节点 (nodes.json)") + except Exception as e: + log(f"节点配置在线更新失败,使用本地配置: {e}") + def _fetch_nodes_auth(url, user, password, timeout=10): """带 Basic 认证拉取 nodes.json(参与 nginx 认证;直连,不经过系统代理)。""" import json @@ -1182,6 +1231,12 @@ class LoginDialog(QDialog): self.ed_pass.returnPressed.connect(self._submit) form.addWidget(self.ed_pass) + # 保存过凭据时预填,重新登录只需点一下 + cred = _auth_load() + if cred: + self.ed_user.setText(cred["username"]) + self.ed_pass.setText(cred["password"]) + self.lb_err = QLabel("") self.lb_err.setStyleSheet(f"color: {C_RED}; font-size: 11px;") self.lb_err.setFixedHeight(14) @@ -1237,6 +1292,7 @@ class LoginDialog(QDialog): text = json.dumps(data, ensure_ascii=False, indent=2) + "\n" _USER_DIR.mkdir(parents=True, exist_ok=True) (_USER_DIR / "nodes.json").write_bytes(_nodes_encrypt(text)) + _auth_save(user, password) # 记住凭据,之后每次启动自动同步节点 self._done.emit(True, str(len(data))) except Exception as e: msg = ("认证失败,请检查用户名/密码" if getattr(e, "code", None) == 401 @@ -1325,6 +1381,8 @@ class MainWindow(QWidget): threading.Thread(target=self._check_update, daemon=True).start() # 后台检查 PAC 数据更新(首次启动自动拉取到用户数据目录) threading.Thread(target=_user_init_remote, daemon=True).start() + # 后台用已保存凭据同步最新节点配置(服务端改动下次启动生效) + threading.Thread(target=_nodes_sync_remote, daemon=True).start() # 节点未配置时延迟弹出登录框(等窗口显示后) QTimer.singleShot(600, self._maybe_show_login)