Files
grok-register/.github/workflows/apply-proxy-fix.yml
T
Workflow config file is invalid. Please check your config file: model.ReadWorkflow: yaml: line 50: could not find expected ':'

515 lines
22 KiB
YAML

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