From bbf78e655dc66335f8d5b3ccc9777b09cf43a18f Mon Sep 17 00:00:00 2001 From: RoyirinRosmontis Date: Sun, 23 Aug 2026 13:45:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8A=9F=E8=83=BD=EF=BC=9A?= =?UTF-8?q?=20-=20=E7=99=BB=E5=BD=95=E6=88=90=E5=8A=9F=E5=90=8E=E5=87=AD?= =?UTF-8?q?=E6=8D=AE=E5=8A=A0=E5=AF=86=E8=90=BD=E7=9B=98=EF=BC=88auth.json?= =?UTF-8?q?=EF=BC=8CDPAPI=20=E5=90=8C=20nodes.json=20=E4=BF=9D=E6=8A=A4?= =?UTF-8?q?=E7=BA=A7=E5=88=AB=EF=BC=89=20-=20=E6=AF=8F=E6=AC=A1=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E5=90=8E=E5=8F=B0=E7=94=A8=E5=B7=B2=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E5=87=AD=E6=8D=AE=E6=8B=89=E5=8F=96=E6=9C=80=E6=96=B0=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E9=85=8D=E7=BD=AE=EF=BC=88=E5=86=85=E5=AE=B9=E6=AF=94?= =?UTF-8?q?=E5=AF=B9=EF=BC=8C=E5=8F=98=E5=8C=96=E6=89=8D=E8=A6=86=E7=9B=96?= =?UTF-8?q?=EF=BC=89=EF=BC=9A=E6=9C=8D=E5=8A=A1=E7=AB=AF=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=20nodes.json=20=E5=90=8E=EF=BC=8C=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=E4=B8=8B=E6=AC=A1=E5=90=AF=E5=8A=A8=E8=87=AA=E5=8A=A8=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=20-=20=E5=87=AD=E6=8D=AE=E5=A4=B1=E6=95=88=EF=BC=8840?= =?UTF-8?q?1=EF=BC=89=E6=88=96=E7=BD=91=E7=BB=9C=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E6=97=B6=E4=BF=9D=E7=95=99=E6=9C=AC=E5=9C=B0=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E4=B8=8D=E7=A0=B4=E5=9D=8F=EF=BC=9B=E7=99=BB=E5=BD=95=E6=A1=86?= =?UTF-8?q?=E5=9C=A8=E4=BF=9D=E5=AD=98=E8=BF=87=E5=87=AD=E6=8D=AE=E6=97=B6?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E9=A2=84=E5=A1=AB=EF=BC=8C=E9=87=8D=E6=96=B0?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E4=B8=80=E9=94=AE=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lite/proxy_lite.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) 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)