From 223d20bee4719268db9b80cb94570917595097ab Mon Sep 17 00:00:00 2001 From: Aaron Liang <76561968+AaronL725@users.noreply.github.com> Date: Thu, 9 Jul 2026 22:30:53 +0800 Subject: [PATCH] chore: add temporary proxy fix workflow --- .github/workflows/apply-proxy-fix.yml | 514 ++++++++++++++++++++++++++ 1 file changed, 514 insertions(+) create mode 100644 .github/workflows/apply-proxy-fix.yml diff --git a/.github/workflows/apply-proxy-fix.yml b/.github/workflows/apply-proxy-fix.yml new file mode 100644 index 0000000..3c8cb0a --- /dev/null +++ b/.github/workflows/apply-proxy-fix.yml @@ -0,0 +1,514 @@ +name: Temporary Proxy Fix + +on: + push: + branches: + - main + paths: + - .github/apply-proxy-fix-trigger.txt + +permissions: + contents: write + +jobs: + apply-fix: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: main + fetch-depth: 0 + + - name: Apply minimal proxy fix + shell: bash + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + python - <<'PY' + from pathlib import Path + + def replace_once(text, old, new, label): + if old not in text: + raise SystemExit(f"missing expected block: {label}") + return text.replace(old, new, 1) + + p = Path("grok_register_ttk.py") + s = p.read_text(encoding="utf-8-sig") + + s = replace_once( + s, + '''import json\n''', + '''import json\nimport base64\nimport select\nimport socket\nimport socketserver\nimport ssl\nimport urllib.parse\n''', + "proxy helper imports", + ) + + old_get_proxies = '''def get_proxies():\n proxy = config.get("proxy", "")\n if proxy:\n return {"http": proxy, "https": proxy}\n return {}\n''' + new_get_proxies = r'''def get_configured_proxy(): + return str(config.get("proxy", "") or "").strip() + + +def get_proxies(): + proxy = get_configured_proxy() + if proxy: + return {"http": proxy, "https": proxy} + return {} + + +def _parse_proxy_url(proxy): + raw = str(proxy or "").strip() + if not raw: + return None + if "://" not in raw: + raw = "http://" + raw + try: + return urllib.parse.urlsplit(raw) + except Exception: + return None + + +def _safe_proxy_port(parsed): + try: + return parsed.port + except Exception: + return None + + +def _proxy_has_auth(proxy): + parsed = _parse_proxy_url(proxy) + return bool(parsed and parsed.hostname and (parsed.username is not None or parsed.password is not None)) + + +def _strip_proxy_auth(proxy): + raw = str(proxy or "").strip() + parsed = _parse_proxy_url(raw) + if not parsed or not parsed.hostname: + return raw + host = parsed.hostname + if ":" in host and not host.startswith("["): + host = f"[{host}]" + port = _safe_proxy_port(parsed) + netloc = f"{host}:{port}" if port else host + stripped = urllib.parse.urlunsplit((parsed.scheme or "http", netloc, parsed.path, parsed.query, parsed.fragment)) + if "://" not in raw: + return stripped.split("://", 1)[1] + return stripped + + +def _proxy_endpoint_terms(proxy=None): + parsed = _parse_proxy_url(proxy or get_configured_proxy()) + if not parsed or not parsed.hostname: + return [] + terms = [parsed.hostname] + port = _safe_proxy_port(parsed) + if port: + terms.append(f"{parsed.hostname}:{port}") + terms.append(f"port {port}") + return [x.lower() for x in terms if x] + + +def is_proxy_connection_error(exc): + if not get_configured_proxy(): + return False + err = str(exc or "").lower() + if not err: + return False + if any(x in err for x in ("proxy", "tunnel", "socks")): + return True + connect_markers = ( + "could not connect", + "failed to connect", + "connection refused", + "connection reset", + "connect error", + "timed out", + "timeout", + ) + if any(x in err for x in connect_markers): + terms = _proxy_endpoint_terms() + if not terms or any(t in err for t in terms): + return True + return False + + +def page_has_proxy_error(page_obj): + try: + url = str(getattr(page_obj, "url", "") or "") + title = str(page_obj.run_js("return document.title || ''") or "") + body = str(page_obj.run_js("return document.body ? document.body.innerText.slice(0, 2000) : ''") or "") + except Exception: + return False + text = f"{url}\n{title}\n{body}".lower() + return any( + marker in text + for marker in ( + "err_proxy", + "proxy connection failed", + "proxy server", + "proxy authentication", + "tunnel connection failed", + "无法连接到代理服务器", + "代理服务器", + ) + ) + + +class _ReusableThreadingTCPServer(socketserver.ThreadingTCPServer): + allow_reuse_address = True + daemon_threads = True + + +def _proxy_recv_until_headers(sock, timeout=20, limit=65536): + sock.settimeout(timeout) + data = b"" + while b"\r\n\r\n" not in data and len(data) < limit: + chunk = sock.recv(4096) + if not chunk: + break + data += chunk + return data + + +def _proxy_relay(left, right, timeout=60): + left.settimeout(timeout) + right.settimeout(timeout) + sockets = [left, right] + while True: + readable, _, _ = select.select(sockets, [], [], timeout) + if not readable: + return + for sock in readable: + data = sock.recv(65536) + if not data: + return + peer = right if sock is left else left + peer.sendall(data) + + +class _LocalAuthProxyBridgeHandler(socketserver.BaseRequestHandler): + def handle(self): + bridge = self.server.bridge + upstream = None + try: + initial = _proxy_recv_until_headers(self.request, timeout=bridge.timeout) + if not initial: + return + first_line = initial.split(b"\r\n", 1)[0].decode("latin1", "ignore") + if first_line.upper().startswith("CONNECT "): + target = first_line.split()[1] + upstream = bridge.open_upstream() + req = [f"CONNECT {target} HTTP/1.1", f"Host: {target}"] + if bridge.auth_header: + req.append(f"Proxy-Authorization: Basic {bridge.auth_header}") + upstream.sendall(("\r\n".join(req) + "\r\n\r\n").encode("latin1")) + response = _proxy_recv_until_headers(upstream, timeout=bridge.timeout) + if response: + self.request.sendall(response) + status = response.split(b"\r\n", 1)[0] + if b" 200 " not in status: + return + _proxy_relay(self.request, upstream, timeout=bridge.relay_timeout) + else: + upstream = bridge.open_upstream() + upstream.sendall(bridge.inject_proxy_auth(initial)) + _proxy_relay(self.request, upstream, timeout=bridge.relay_timeout) + except Exception: + return + finally: + if upstream is not None: + try: + upstream.close() + except Exception: + pass + + +class LocalAuthProxyBridge: + def __init__(self, proxy_url): + parsed = _parse_proxy_url(proxy_url) + if not parsed or not parsed.hostname: + raise ValueError("认证代理地址格式无效") + if (parsed.scheme or "http").lower() not in ("http", "https"): + raise ValueError("Chromium 本地认证代理桥仅支持 http/https 上游代理") + self.upstream_scheme = (parsed.scheme or "http").lower() + self.upstream_host = parsed.hostname + self.upstream_port = _safe_proxy_port(parsed) or (443 if self.upstream_scheme == "https" else 80) + username = urllib.parse.unquote(parsed.username or "") + password = urllib.parse.unquote(parsed.password or "") + raw_auth = f"{username}:{password}".encode("utf-8") + self.auth_header = base64.b64encode(raw_auth).decode("ascii") if (username or password) else "" + self.timeout = 20 + self.relay_timeout = 90 + self.server = None + self.thread = None + self.local_proxy = "" + + def open_upstream(self): + sock = socket.create_connection((self.upstream_host, self.upstream_port), timeout=self.timeout) + if self.upstream_scheme == "https": + context = ssl.create_default_context() + sock = context.wrap_socket(sock, server_hostname=self.upstream_host) + sock.settimeout(self.timeout) + return sock + + def inject_proxy_auth(self, data): + if not self.auth_header or b"\r\n\r\n" not in data: + return data + if b"\r\nproxy-authorization:" in data.lower(): + return data + head, body = data.split(b"\r\n\r\n", 1) + auth_line = f"Proxy-Authorization: Basic {self.auth_header}".encode("latin1") + return head + b"\r\n" + auth_line + b"\r\n\r\n" + body + + def start(self): + self.server = _ReusableThreadingTCPServer(("127.0.0.1", 0), _LocalAuthProxyBridgeHandler) + self.server.bridge = self + port = self.server.server_address[1] + self.local_proxy = f"http://127.0.0.1:{port}" + self.thread = threading.Thread(target=self.server.serve_forever, daemon=True) + self.thread.start() + return self.local_proxy + + def stop(self): + if self.server is not None: + try: + self.server.shutdown() + self.server.server_close() + except Exception: + pass + self.server = None + self.thread = None + self.local_proxy = "" + + +def stop_browser_proxy_bridge(): + global browser_proxy_bridge + if browser_proxy_bridge is not None: + try: + browser_proxy_bridge.stop() + except Exception: + pass + browser_proxy_bridge = None + + +def prepare_browser_proxy(use_proxy=True, log_callback=None): + proxy = get_configured_proxy() + if not use_proxy or not proxy: + return "", None + if _proxy_has_auth(proxy): + parsed = _parse_proxy_url(proxy) + scheme = (parsed.scheme or "http").lower() if parsed else "" + if scheme in ("http", "https"): + bridge = LocalAuthProxyBridge(proxy) + browser_proxy = bridge.start() + if log_callback: + log_callback(f"[*] 已为 Chromium 启动本地认证代理桥: {browser_proxy}") + return browser_proxy, bridge + stripped = _strip_proxy_auth(proxy) + if log_callback: + log_callback("[!] Chromium 暂不直接支持该认证代理协议,已使用去认证代理地址,失败将回退直连") + return stripped, None + return proxy, None +''' + s = replace_once(s, old_get_proxies, new_get_proxies, "proxy helpers") + + old_browser_http = '''def create_browser_options():\n options = ChromiumOptions()\n options.auto_port()\n options.set_timeouts(base=1)\n if os.path.exists(EXTENSION_PATH):\n options.add_extension(EXTENSION_PATH)\n return options\n\n\ndef _build_request_kwargs(**kwargs):\n request_kwargs = dict(kwargs)\n proxies = request_kwargs.pop("proxies", None)\n if proxies is None:\n proxies = get_proxies()\n if proxies:\n request_kwargs["proxies"] = proxies\n request_kwargs.setdefault("timeout", 15)\n return request_kwargs\n\n\ndef http_get(url, **kwargs):\n try:\n return requests.get(url, **_build_request_kwargs(**kwargs))\n except Exception as exc:\n err = str(exc)\n # 代理不可用时自动回退为直连,避免整个流程直接失败\n if "127.0.0.1 port 7890" in err or "Could not connect to server" in err:\n retry_kwargs = dict(kwargs)\n retry_kwargs["proxies"] = {}\n return requests.get(url, **_build_request_kwargs(**retry_kwargs))\n raise\n\n\ndef http_post(url, **kwargs):\n try:\n return requests.post(url, **_build_request_kwargs(**kwargs))\n except Exception as exc:\n err = str(exc)\n if "127.0.0.1 port 7890" in err or "Could not connect to server" in err:\n retry_kwargs = dict(kwargs)\n retry_kwargs["proxies"] = {}\n return requests.post(url, **_build_request_kwargs(**retry_kwargs))\n raise\n''' + new_browser_http = r'''def apply_browser_proxy_option(options, proxy): + if not proxy: + return + if hasattr(options, "set_proxy"): + try: + options.set_proxy(proxy) + return + except Exception: + pass + if not hasattr(options, "set_argument"): + raise AttributeError("当前 DrissionPage ChromiumOptions 不支持设置浏览器代理") + try: + options.set_argument(f"--proxy-server={proxy}") + except TypeError: + options.set_argument("--proxy-server", proxy) + + +def create_browser_options(browser_proxy=""): + options = ChromiumOptions() + options.auto_port() + options.set_timeouts(base=1) + apply_browser_proxy_option(options, browser_proxy) + if os.path.exists(EXTENSION_PATH): + options.add_extension(EXTENSION_PATH) + return options + + +def _build_request_kwargs(**kwargs): + request_kwargs = dict(kwargs) + proxies = request_kwargs.pop("proxies", None) + if proxies is None: + proxies = get_proxies() + if proxies: + request_kwargs["proxies"] = proxies + request_kwargs.setdefault("timeout", 15) + return request_kwargs + + +def http_get(url, **kwargs): + request_kwargs = _build_request_kwargs(**kwargs) + try: + return requests.get(url, **request_kwargs) + except Exception as exc: + # 代理不可用时自动回退为直连,避免整个流程直接失败。 + if request_kwargs.get("proxies") and is_proxy_connection_error(exc): + retry_kwargs = dict(kwargs) + retry_kwargs["proxies"] = {} + return requests.get(url, **_build_request_kwargs(**retry_kwargs)) + raise + + +def http_post(url, **kwargs): + request_kwargs = _build_request_kwargs(**kwargs) + try: + return requests.post(url, **request_kwargs) + except Exception as exc: + if request_kwargs.get("proxies") and is_proxy_connection_error(exc): + retry_kwargs = dict(kwargs) + retry_kwargs["proxies"] = {} + return requests.post(url, **_build_request_kwargs(**retry_kwargs)) + raise +''' + s = replace_once(s, old_browser_http, new_browser_http, "browser options and HTTP proxy fallback") + + s = s.replace(", proxies={}", "") + + s = replace_once( + s, + '''browser = None\npage = None\n''', + '''browser = None\npage = None\nbrowser_proxy_bridge = None\nbrowser_started_with_proxy = False\n''', + "browser proxy globals", + ) + + old_start_stop = '''def start_browser(log_callback=None):\n global browser, page\n last_exc = None\n for attempt in range(1, 5):\n try:\n browser = Chromium(create_browser_options())\n tabs = browser.get_tabs()\n page = tabs[-1] if tabs else browser.new_tab()\n if log_callback and getattr(browser, "user_data_path", None):\n log_callback(f"[Debug] 当前浏览器资料目录: {browser.user_data_path}")\n if log_callback and attempt > 1:\n log_callback(f"[*] 浏览器第 {attempt} 次启动成功")\n return browser, page\n except Exception as exc:\n last_exc = exc\n if log_callback:\n log_callback(f"[Debug] 浏览器启动失败(第{attempt}/4次): {exc}")\n try:\n if browser is not None:\n browser.quit(del_data=True)\n except Exception:\n pass\n browser = None\n page = None\n time.sleep(min(1.5 * attempt, 4))\n raise Exception(f"浏览器启动失败,已重试4次: {last_exc}")\n\n\ndef stop_browser():\n global browser, page\n if browser is not None:\n try:\n browser.quit(del_data=True)\n except Exception:\n pass\n browser = None\n page = None\n\n\ndef restart_browser(log_callback=None):\n stop_browser()\n return start_browser(log_callback=log_callback)\n''' + new_start_stop = r'''def start_browser(log_callback=None, use_proxy=True): + global browser, page, browser_proxy_bridge, browser_started_with_proxy + last_exc = None + proxy_enabled = bool(use_proxy and get_configured_proxy()) + for attempt in range(1, 5): + bridge = None + try: + browser_proxy, bridge = prepare_browser_proxy(use_proxy=use_proxy, log_callback=log_callback) + browser = Chromium(create_browser_options(browser_proxy=browser_proxy)) + browser_proxy_bridge = bridge + browser_started_with_proxy = bool(browser_proxy) + tabs = browser.get_tabs() + page = tabs[-1] if tabs else browser.new_tab() + if log_callback and getattr(browser, "user_data_path", None): + log_callback(f"[Debug] 当前浏览器资料目录: {browser.user_data_path}") + if log_callback and get_configured_proxy(): + mode = "代理" if browser_started_with_proxy else "直连" + log_callback(f"[*] 浏览器网络模式: {mode}") + if log_callback and attempt > 1: + log_callback(f"[*] 浏览器第 {attempt} 次启动成功") + return browser, page + except Exception as exc: + last_exc = exc + if bridge is not None: + try: + bridge.stop() + except Exception: + pass + if log_callback: + mode = "代理" if proxy_enabled else "直连" + log_callback(f"[Debug] 浏览器{mode}启动失败(第{attempt}/4次): {exc}") + try: + if browser is not None: + browser.quit(del_data=True) + except Exception: + pass + browser = None + page = None + browser_proxy_bridge = None + browser_started_with_proxy = False + time.sleep(min(1.5 * attempt, 4)) + raise Exception(f"浏览器启动失败,已重试4次: {last_exc}") + + +def stop_browser(): + global browser, page, browser_started_with_proxy + if browser is not None: + try: + browser.quit(del_data=True) + except Exception: + pass + stop_browser_proxy_bridge() + browser = None + page = None + browser_started_with_proxy = False + + +def restart_browser(log_callback=None, use_proxy=True): + stop_browser() + return start_browser(log_callback=log_callback, use_proxy=use_proxy) +''' + s = replace_once(s, old_start_stop, new_start_stop, "browser lifecycle proxy handling") + + old_open_signup = '''def open_signup_page(log_callback=None, cancel_callback=None):\n global browser, page\n raise_if_cancelled(cancel_callback)\n if browser is None:\n start_browser()\n if log_callback:\n log_callback("[*] 浏览器已启动")\n try:\n page = browser.get_tab(0)\n page.get(SIGNUP_URL)\n except Exception as e:\n if log_callback:\n log_callback(f"[Debug] 打开URL异常: {e}")\n try:\n page = browser.new_tab(SIGNUP_URL)\n except Exception as e2:\n if log_callback:\n log_callback(f"[Debug] 创建新标签页异常: {e2}")\n restart_browser()\n page = browser.new_tab(SIGNUP_URL)\n page.wait.doc_loaded()\n sleep_with_cancel(2, cancel_callback)\n if log_callback:\n log_callback(f"[*] 当前URL: {page.url}")\n click_email_signup_button(\n log_callback=log_callback, cancel_callback=cancel_callback\n )\n''' + new_open_signup = r'''def open_signup_page(log_callback=None, cancel_callback=None): + global browser, page + raise_if_cancelled(cancel_callback) + if browser is None: + start_browser(log_callback=log_callback) + if log_callback: + log_callback("[*] 浏览器已启动") + + def _open_with_current_browser(): + global page + try: + page = browser.get_tab(0) + page.get(SIGNUP_URL) + except Exception as e: + if log_callback: + log_callback(f"[Debug] 打开URL异常: {e}") + page = browser.new_tab(SIGNUP_URL) + page.wait.doc_loaded() + + try: + _open_with_current_browser() + except Exception as e: + if browser_started_with_proxy and get_configured_proxy(): + if log_callback: + log_callback(f"[!] 浏览器代理访问注册页失败,自动回退直连: {e}") + restart_browser(log_callback=log_callback, use_proxy=False) + _open_with_current_browser() + else: + raise + + if browser_started_with_proxy and page_has_proxy_error(page): + if log_callback: + log_callback("[!] 浏览器页面显示代理错误,自动回退直连") + restart_browser(log_callback=log_callback, use_proxy=False) + _open_with_current_browser() + + sleep_with_cancel(2, cancel_callback) + if log_callback: + log_callback(f"[*] 当前URL: {page.url}") + click_email_signup_button( + log_callback=log_callback, cancel_callback=cancel_callback + ) +''' + s = replace_once(s, old_open_signup, new_open_signup, "signup page proxy fallback") + + p.write_text(s, encoding="utf-8-sig") + PY + + git add grok_register_ttk.py + if git diff --cached --quiet; then + echo "No proxy fix changes to commit" + git reset --quiet + else + git diff --cached --check + git commit -m "fix(proxy): route browser traffic through configured proxy" + fi + + git rm .github/workflows/apply-proxy-fix.yml .github/apply-proxy-fix-trigger.txt + git commit -m "chore: remove temporary proxy fix workflow" + git push origin HEAD:main