fix: harden token pools and CPA export
This commit is contained in:
@@ -1,34 +0,0 @@
|
|||||||
name: Temporary audit fixes
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
issues:
|
|
||||||
types: [edited, opened, reopened]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
apply:
|
|
||||||
if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' || github.event.issue.number == 7
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Repair audit patch escapes
|
|
||||||
run: python tools/fix_audit_patch_escapes.py
|
|
||||||
- name: Apply audit fixes
|
|
||||||
run: python tools/apply_audit_fixes.py
|
|
||||||
- name: Commit and cleanup
|
|
||||||
run: |
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
||||||
git rm -f tools/apply_audit_fixes.py tools/fix_audit_patch_escapes.py .github/workflows/temp-audit-fixes.yml
|
|
||||||
git add -A
|
|
||||||
if git diff --cached --quiet; then
|
|
||||||
echo "No changes to commit"
|
|
||||||
else
|
|
||||||
git commit -m "fix: harden token pools and CPA export"
|
|
||||||
git push
|
|
||||||
fi
|
|
||||||
@@ -24,3 +24,5 @@ autoPortData/
|
|||||||
cpa_auths/
|
cpa_auths/
|
||||||
xai-*.json
|
xai-*.json
|
||||||
cpa_auth_failed.txt
|
cpa_auth_failed.txt
|
||||||
|
screenshots/
|
||||||
|
*.png
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ GUI 模式会打开 Tkinter 窗口,适合手动调整配置和观察日志。
|
|||||||
- `mail_credentials.txt`:临时邮箱凭证。
|
- `mail_credentials.txt`:临时邮箱凭证。
|
||||||
- `cpa_auths/xai-*.json`:可选导出的 CPA xAI OAuth 凭证。
|
- `cpa_auths/xai-*.json`:可选导出的 CPA xAI OAuth 凭证。
|
||||||
- `cpa_auths/cpa_auth_failed.txt`:OIDC 导出失败记录。
|
- `cpa_auths/cpa_auth_failed.txt`:OIDC 导出失败记录。
|
||||||
|
- `screenshots/`:CPA/OIDC 浏览器失败调试截图,已被 `.gitignore` 忽略。
|
||||||
- `*.log`:可选日志文件。
|
- `*.log`:可选日志文件。
|
||||||
|
|
||||||
这些文件包含敏感信息,已被 `.gitignore` 忽略。
|
这些文件包含敏感信息,已被 `.gitignore` 忽略。
|
||||||
@@ -321,10 +322,14 @@ GUI 数量控件可能有上限。CLI 模式直接读取 `config.json` 中的 `r
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
.
|
.
|
||||||
├── grok_register_ttk.py # 主程序
|
├── grok_register_ttk.py # 主程序(GUI / CLI)
|
||||||
|
├── cpa_export.py # 注册成功后的 CPA/OIDC 导出入口
|
||||||
|
├── cpa_xai/ # xAI Device Auth、浏览器授权和凭证写入模块
|
||||||
├── cf_mail_debug.py # Cloudflare 邮箱调试工具
|
├── cf_mail_debug.py # Cloudflare 邮箱调试工具
|
||||||
├── config.example.json # 配置示例
|
├── config.example.json # 配置示例
|
||||||
├── requirements.txt # Python 依赖
|
├── requirements.txt # Python 依赖
|
||||||
|
├── tests/ # 现有测试用例
|
||||||
|
├── assets/ # README 资源
|
||||||
└── README.md
|
└── README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -189,10 +189,11 @@ def create_standalone_page(proxy: Optional[str] = None, headless: bool = False,
|
|||||||
pass
|
pass
|
||||||
break
|
break
|
||||||
|
|
||||||
from .proxyutil import proxy_for_chromium, proxy_log_label, resolve_proxy
|
from .proxyutil import prepare_chromium_proxy, proxy_log_label, resolve_proxy
|
||||||
|
|
||||||
resolved = resolve_proxy(proxy)
|
resolved = resolve_proxy(proxy)
|
||||||
chrome_proxy = proxy_for_chromium(resolved)
|
proxy_bridge = None
|
||||||
|
chrome_proxy, proxy_bridge = prepare_chromium_proxy(resolved, log=logger)
|
||||||
if chrome_proxy:
|
if chrome_proxy:
|
||||||
options.set_argument("--proxy-server=%s" % chrome_proxy)
|
options.set_argument("--proxy-server=%s" % chrome_proxy)
|
||||||
logger("browser proxy=%s (chromium %s)" % (proxy_log_label(resolved), chrome_proxy))
|
logger("browser proxy=%s (chromium %s)" % (proxy_log_label(resolved), chrome_proxy))
|
||||||
@@ -200,19 +201,50 @@ def create_standalone_page(proxy: Optional[str] = None, headless: bool = False,
|
|||||||
logger("browser proxy=(none)")
|
logger("browser proxy=(none)")
|
||||||
|
|
||||||
browser = Chromium(options)
|
browser = Chromium(options)
|
||||||
|
if proxy_bridge is not None:
|
||||||
|
try:
|
||||||
|
setattr(browser, "_cpa_proxy_bridge", proxy_bridge)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
_register_mint_browser(browser)
|
||||||
page = browser.latest_tab
|
page = browser.latest_tab
|
||||||
logger("standalone chromium started")
|
logger("standalone chromium started")
|
||||||
return browser, page
|
return browser, page
|
||||||
|
|
||||||
|
|
||||||
def close_standalone(browser: Any) -> None:
|
def close_standalone(browser: Any) -> None:
|
||||||
|
if browser is None:
|
||||||
|
return
|
||||||
|
_unregister_mint_browser(browser)
|
||||||
|
bridge = getattr(browser, "_cpa_proxy_bridge", None)
|
||||||
try:
|
try:
|
||||||
browser.quit()
|
browser.quit()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
if bridge is not None:
|
||||||
|
try:
|
||||||
|
bridge.stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
_mint_tls = threading.local()
|
_mint_tls = threading.local()
|
||||||
|
_mint_registry_lock = threading.Lock()
|
||||||
|
_mint_registry = set()
|
||||||
|
|
||||||
|
|
||||||
|
def _register_mint_browser(browser: Any) -> None:
|
||||||
|
if browser is None:
|
||||||
|
return
|
||||||
|
with _mint_registry_lock:
|
||||||
|
_mint_registry.add(browser)
|
||||||
|
|
||||||
|
|
||||||
|
def _unregister_mint_browser(browser: Any) -> None:
|
||||||
|
if browser is None:
|
||||||
|
return
|
||||||
|
with _mint_registry_lock:
|
||||||
|
_mint_registry.discard(browser)
|
||||||
|
|
||||||
|
|
||||||
def _mint_tls_get():
|
def _mint_tls_get():
|
||||||
@@ -417,8 +449,9 @@ def release_mint_browser(owned: bool, success: bool, log: Optional[LogFn] = None
|
|||||||
|
|
||||||
def shutdown_mint_browsers() -> None:
|
def shutdown_mint_browsers() -> None:
|
||||||
state = _mint_tls_get()
|
state = _mint_tls_get()
|
||||||
browser = state.get("browser")
|
with _mint_registry_lock:
|
||||||
if browser is not None:
|
browsers = list(_mint_registry)
|
||||||
|
for browser in browsers:
|
||||||
try:
|
try:
|
||||||
close_standalone(browser)
|
close_standalone(browser)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -824,7 +857,7 @@ def mint_with_browser(
|
|||||||
session = request_device_code(proxy=resolved or None)
|
session = request_device_code(proxy=resolved or None)
|
||||||
last_error = None
|
last_error = None
|
||||||
break
|
break
|
||||||
except BaseException as exc:
|
except Exception as exc:
|
||||||
last_error = exc
|
last_error = exc
|
||||||
logger("request_device_code attempt %s/3 failed: %s" % (attempt, exc))
|
logger("request_device_code attempt %s/3 failed: %s" % (attempt, exc))
|
||||||
_sleep(1.5 * attempt)
|
_sleep(1.5 * attempt)
|
||||||
@@ -852,22 +885,28 @@ def mint_with_browser(
|
|||||||
token_box = {}
|
token_box = {}
|
||||||
error_box = {}
|
error_box = {}
|
||||||
|
|
||||||
|
def combined_cancel():
|
||||||
|
return stop_event.is_set() or bool(cancel and cancel())
|
||||||
|
|
||||||
def _poll() -> None:
|
def _poll() -> None:
|
||||||
try:
|
try:
|
||||||
time.sleep(2)
|
for _ in range(20):
|
||||||
|
if combined_cancel():
|
||||||
|
raise OAuthDeviceError("cancelled")
|
||||||
|
time.sleep(0.1)
|
||||||
result = poll_device_token(
|
result = poll_device_token(
|
||||||
session.device_code,
|
session.device_code,
|
||||||
token_endpoint=session.token_endpoint,
|
token_endpoint=session.token_endpoint,
|
||||||
interval=max(session.interval, 5),
|
interval=max(session.interval, 5),
|
||||||
expires_in=min(session.expires_in, int(browser_timeout_sec) + 60),
|
expires_in=min(session.expires_in, int(browser_timeout_sec) + 60),
|
||||||
log=logger,
|
log=logger,
|
||||||
cancel=cancel,
|
cancel=combined_cancel,
|
||||||
proxy=resolved or None,
|
proxy=resolved or None,
|
||||||
)
|
)
|
||||||
token_box["token"] = result
|
token_box["token"] = result
|
||||||
stop_event.set()
|
stop_event.set()
|
||||||
logger("token poll SUCCESS — stop_event set")
|
logger("token poll SUCCESS — stop_event set")
|
||||||
except BaseException as exc:
|
except Exception as exc:
|
||||||
error_box["err"] = exc
|
error_box["err"] = exc
|
||||||
stop_event.set()
|
stop_event.set()
|
||||||
|
|
||||||
@@ -897,8 +936,16 @@ def mint_with_browser(
|
|||||||
)
|
)
|
||||||
if hard:
|
if hard:
|
||||||
stop_event.set()
|
stop_event.set()
|
||||||
|
thread.join(timeout=5)
|
||||||
|
if thread.is_alive():
|
||||||
|
logger("token poll thread did not stop within 5s after browser failure")
|
||||||
raise
|
raise
|
||||||
thread.join(timeout=max(browser_timeout_sec, 60) + 30)
|
thread.join(timeout=max(browser_timeout_sec, 60) + 30)
|
||||||
|
if thread.is_alive():
|
||||||
|
stop_event.set()
|
||||||
|
thread.join(timeout=5)
|
||||||
|
if thread.is_alive():
|
||||||
|
raise OAuthDeviceError("token poll thread did not stop after timeout")
|
||||||
if "token" in token_box:
|
if "token" in token_box:
|
||||||
token_result = token_box["token"]
|
token_result = token_box["token"]
|
||||||
success = True
|
success = True
|
||||||
|
|||||||
+14
-6
@@ -167,7 +167,7 @@ def _post_form(url, form, timeout=30.0, proxy=None, retries=0, retry_sleep=1.5):
|
|||||||
except urllib.error.HTTPError as exc:
|
except urllib.error.HTTPError as exc:
|
||||||
body = exc.read().decode("utf-8", errors="replace")
|
body = exc.read().decode("utf-8", errors="replace")
|
||||||
status = int(exc.code)
|
status = int(exc.code)
|
||||||
except BaseException as exc:
|
except Exception as exc:
|
||||||
last_error = exc
|
last_error = exc
|
||||||
if not _is_transient_net_error(exc) or attempt >= int(retries):
|
if not _is_transient_net_error(exc) or attempt >= int(retries):
|
||||||
raise
|
raise
|
||||||
@@ -216,6 +216,14 @@ def request_device_code(client_id=CLIENT_ID, scope=SCOPE, timeout=30.0, proxy=No
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _sleep_with_cancel(seconds, cancel=None):
|
||||||
|
deadline = time.time() + max(float(seconds), 0.0)
|
||||||
|
while time.time() < deadline:
|
||||||
|
if cancel and cancel():
|
||||||
|
raise OAuthDeviceError("cancelled")
|
||||||
|
time.sleep(min(0.2, max(deadline - time.time(), 0.0)))
|
||||||
|
|
||||||
|
|
||||||
def poll_device_token(
|
def poll_device_token(
|
||||||
device_code,
|
device_code,
|
||||||
token_endpoint,
|
token_endpoint,
|
||||||
@@ -249,7 +257,7 @@ def poll_device_token(
|
|||||||
retry_sleep=1.0,
|
retry_sleep=1.0,
|
||||||
)
|
)
|
||||||
net_streak = 0
|
net_streak = 0
|
||||||
except BaseException as exc:
|
except Exception as exc:
|
||||||
if not _is_transient_net_error(exc):
|
if not _is_transient_net_error(exc):
|
||||||
raise
|
raise
|
||||||
net_streak += 1
|
net_streak += 1
|
||||||
@@ -257,7 +265,7 @@ def poll_device_token(
|
|||||||
logger("oauth poll network blip (%s/%s): %s — retry in %ss" % (net_streak, max_net_streak, exc, wait_seconds))
|
logger("oauth poll network blip (%s/%s): %s — retry in %ss" % (net_streak, max_net_streak, exc, wait_seconds))
|
||||||
if net_streak >= max_net_streak:
|
if net_streak >= max_net_streak:
|
||||||
raise OAuthDeviceError("device auth aborted after %s network errors: %s" % (net_streak, exc))
|
raise OAuthDeviceError("device auth aborted after %s network errors: %s" % (net_streak, exc))
|
||||||
time.sleep(wait_seconds)
|
_sleep_with_cancel(wait_seconds, cancel)
|
||||||
continue
|
continue
|
||||||
if status == 200 and isinstance(payload, dict) and payload.get("access_token"):
|
if status == 200 and isinstance(payload, dict) and payload.get("access_token"):
|
||||||
access_token = str(payload.get("access_token") or "").strip()
|
access_token = str(payload.get("access_token") or "").strip()
|
||||||
@@ -281,7 +289,7 @@ def poll_device_token(
|
|||||||
if error_code == "slow_down":
|
if error_code == "slow_down":
|
||||||
sleep_seconds = min(sleep_seconds + 5, 30)
|
sleep_seconds = min(sleep_seconds + 5, 30)
|
||||||
logger("oauth poll: %s (sleep %ss)" % (error_code, sleep_seconds))
|
logger("oauth poll: %s (sleep %ss)" % (error_code, sleep_seconds))
|
||||||
time.sleep(sleep_seconds)
|
_sleep_with_cancel(sleep_seconds, cancel)
|
||||||
continue
|
continue
|
||||||
if error_code in ("expired_token", "access_denied"):
|
if error_code in ("expired_token", "access_denied"):
|
||||||
raise OAuthDeviceError("device auth failed: %s: %s" % (error_code, error_description))
|
raise OAuthDeviceError("device auth failed: %s: %s" % (error_code, error_description))
|
||||||
@@ -293,8 +301,8 @@ def poll_device_token(
|
|||||||
logger("oauth poll soft HTTP %s: %r — retry in %ss" % (status, payload, wait_seconds))
|
logger("oauth poll soft HTTP %s: %r — retry in %ss" % (status, payload, wait_seconds))
|
||||||
if net_streak >= max_net_streak:
|
if net_streak >= max_net_streak:
|
||||||
raise OAuthDeviceError("device auth aborted after repeated soft HTTP failures status=%s" % status)
|
raise OAuthDeviceError("device auth aborted after repeated soft HTTP failures status=%s" % status)
|
||||||
time.sleep(wait_seconds)
|
_sleep_with_cancel(wait_seconds, cancel)
|
||||||
continue
|
continue
|
||||||
logger("oauth poll unexpected HTTP %s: %r" % (status, payload))
|
logger("oauth poll unexpected HTTP %s: %r" % (status, payload))
|
||||||
time.sleep(sleep_seconds)
|
_sleep_with_cancel(sleep_seconds, cancel)
|
||||||
raise OAuthDeviceError("device auth timed out waiting for user approval")
|
raise OAuthDeviceError("device auth timed out waiting for user approval")
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ def build_cpa_xai_auth(
|
|||||||
expired = ""
|
expired = ""
|
||||||
if exp:
|
if exp:
|
||||||
expired = datetime.fromtimestamp(exp, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
expired = datetime.fromtimestamp(exp, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
elif expires_in:
|
||||||
|
expired = datetime.fromtimestamp(time.time() + int(expires_in or 0), tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
payload = {
|
payload = {
|
||||||
"type": "xai",
|
"type": "xai",
|
||||||
"access_token": access,
|
"access_token": access,
|
||||||
|
|||||||
+12
-1
@@ -8,13 +8,24 @@ from pathlib import Path
|
|||||||
from .schema import credential_file_name
|
from .schema import credential_file_name
|
||||||
|
|
||||||
|
|
||||||
|
def _is_relative_to(path, root):
|
||||||
|
try:
|
||||||
|
path.relative_to(root)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def write_cpa_xai_auth(auth_dir, payload, filename=None):
|
def write_cpa_xai_auth(auth_dir, payload, filename=None):
|
||||||
root = Path(auth_dir).expanduser().resolve()
|
root = Path(auth_dir).expanduser().resolve()
|
||||||
root.mkdir(parents=True, exist_ok=True)
|
root.mkdir(parents=True, exist_ok=True)
|
||||||
target_name = filename or credential_file_name(payload.get("email", ""), payload.get("sub", ""))
|
target_name = filename or credential_file_name(payload.get("email", ""), payload.get("sub", ""))
|
||||||
|
target_name = Path(str(target_name)).name
|
||||||
if not str(target_name).endswith(".json"):
|
if not str(target_name).endswith(".json"):
|
||||||
target_name = str(target_name) + ".json"
|
target_name = str(target_name) + ".json"
|
||||||
destination = root / str(target_name)
|
destination = (root / str(target_name)).resolve()
|
||||||
|
if not _is_relative_to(destination, root):
|
||||||
|
raise ValueError("CPA auth filename must stay inside auth_dir")
|
||||||
data = json.dumps(payload, indent=2, ensure_ascii=False) + "\n"
|
data = json.dumps(payload, indent=2, ensure_ascii=False) + "\n"
|
||||||
file_descriptor, temp_name = tempfile.mkstemp(prefix=".xai-", suffix=".tmp", dir=str(root))
|
file_descriptor, temp_name = tempfile.mkstemp(prefix=".xai-", suffix=".tmp", dir=str(root))
|
||||||
try:
|
try:
|
||||||
|
|||||||
+92
-25
@@ -1,12 +1,22 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
Grok 注册机 - TTK GUI 版本
|
Grok 注册机 - TTK GUI 版本
|
||||||
整合 DrissionPage_example.py, openai_register.py, batch_open_nsfw.py
|
整合 DrissionPage_example.py, openai_register.py, batch_open_nsfw.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, messagebox, scrolledtext
|
from tkinter import ttk, messagebox, scrolledtext
|
||||||
|
TK_AVAILABLE = True
|
||||||
|
TK_IMPORT_ERROR = None
|
||||||
|
except ImportError as exc:
|
||||||
|
tk = None
|
||||||
|
ttk = None
|
||||||
|
messagebox = None
|
||||||
|
scrolledtext = None
|
||||||
|
TK_AVAILABLE = False
|
||||||
|
TK_IMPORT_ERROR = exc
|
||||||
import threading
|
import threading
|
||||||
import datetime
|
import datetime
|
||||||
import time
|
import time
|
||||||
@@ -58,11 +68,11 @@ DEFAULT_CONFIG = {
|
|||||||
"cloudmail_public_token": "",
|
"cloudmail_public_token": "",
|
||||||
"cloudmail_domains": "",
|
"cloudmail_domains": "",
|
||||||
"cloudmail_path_messages": "/api/public/emailList",
|
"cloudmail_path_messages": "/api/public/emailList",
|
||||||
"proxy": "http://127.0.0.1:7890",
|
"proxy": "",
|
||||||
"enable_nsfw": True,
|
"enable_nsfw": True,
|
||||||
"register_count": 1,
|
"register_count": 1,
|
||||||
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
|
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
|
||||||
"grok2api_auto_add_local": True,
|
"grok2api_auto_add_local": False,
|
||||||
"grok2api_local_token_file": "",
|
"grok2api_local_token_file": "",
|
||||||
"grok2api_pool_name": "ssoBasic",
|
"grok2api_pool_name": "ssoBasic",
|
||||||
"grok2api_auto_add_remote": False,
|
"grok2api_auto_add_remote": False,
|
||||||
@@ -100,8 +110,14 @@ def load_config():
|
|||||||
try:
|
try:
|
||||||
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
||||||
loaded = json.load(f)
|
loaded = json.load(f)
|
||||||
|
if not isinstance(loaded, dict):
|
||||||
|
raise ValueError("config root must be a JSON object")
|
||||||
config = {**DEFAULT_CONFIG, **loaded}
|
config = {**DEFAULT_CONFIG, **loaded}
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
|
message = f"配置文件解析失败: {CONFIG_FILE}: {exc}"
|
||||||
|
print(f"[!] {message}", file=sys.stderr)
|
||||||
|
raise SystemExit(message)
|
||||||
|
else:
|
||||||
config = DEFAULT_CONFIG.copy()
|
config = DEFAULT_CONFIG.copy()
|
||||||
return config
|
return config
|
||||||
|
|
||||||
@@ -647,20 +663,30 @@ def add_token_to_grok2api_local_pool(raw_token, email="", log_callback=None):
|
|||||||
token = _normalize_sso_token(raw_token)
|
token = _normalize_sso_token(raw_token)
|
||||||
if not token:
|
if not token:
|
||||||
return False
|
return False
|
||||||
token_file = resolve_grok2api_local_token_file()
|
token_file = os.path.abspath(resolve_grok2api_local_token_file())
|
||||||
pool_name = str(config.get("grok2api_pool_name", "ssoBasic") or "ssoBasic").strip()
|
pool_name = str(config.get("grok2api_pool_name", "ssoBasic") or "ssoBasic").strip() or "ssoBasic"
|
||||||
if not pool_name:
|
parent = os.path.dirname(token_file)
|
||||||
pool_name = "ssoBasic"
|
os.makedirs(parent, exist_ok=True)
|
||||||
os.makedirs(os.path.dirname(token_file), exist_ok=True)
|
lock_path = token_file + ".lock"
|
||||||
|
try:
|
||||||
|
from filelock import FileLock
|
||||||
|
except Exception as exc:
|
||||||
|
raise RuntimeError(f"filelock 依赖不可用,拒绝非原子写入 token 池: {exc}")
|
||||||
|
with FileLock(lock_path, timeout=30):
|
||||||
data = {}
|
data = {}
|
||||||
if os.path.exists(token_file):
|
if os.path.exists(token_file):
|
||||||
try:
|
try:
|
||||||
with open(token_file, "r", encoding="utf-8") as f:
|
with open(token_file, "r", encoding="utf-8") as f:
|
||||||
data = json.load(f) or {}
|
data = json.load(f) or {}
|
||||||
|
except Exception as exc:
|
||||||
|
broken_path = token_file + f".broken-{int(time.time())}"
|
||||||
|
try:
|
||||||
|
os.replace(token_file, broken_path)
|
||||||
except Exception:
|
except Exception:
|
||||||
data = {}
|
broken_path = token_file
|
||||||
|
raise RuntimeError(f"本地 token 文件 JSON 解析失败,已停止写入以避免覆盖: {broken_path}: {exc}")
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
data = {}
|
raise RuntimeError("本地 token 文件根节点不是 JSON object,拒绝覆盖")
|
||||||
pool = data.get(pool_name)
|
pool = data.get(pool_name)
|
||||||
if not isinstance(pool, list):
|
if not isinstance(pool, list):
|
||||||
pool = []
|
pool = []
|
||||||
@@ -674,11 +700,31 @@ def add_token_to_grok2api_local_pool(raw_token, email="", log_callback=None):
|
|||||||
if log_callback:
|
if log_callback:
|
||||||
log_callback(f"[*] grok2api 本地池已存在 token: {pool_name}")
|
log_callback(f"[*] grok2api 本地池已存在 token: {pool_name}")
|
||||||
return True
|
return True
|
||||||
entry = {"token": token, "tags": ["auto-register"], "note": email}
|
pool.append({"token": token, "tags": ["auto-register"], "note": email})
|
||||||
pool.append(entry)
|
|
||||||
data[pool_name] = pool
|
data[pool_name] = pool
|
||||||
with open(token_file, "w", encoding="utf-8") as f:
|
if os.path.exists(token_file):
|
||||||
|
backup_path = token_file + ".bak"
|
||||||
|
try:
|
||||||
|
with open(token_file, "rb") as src, open(backup_path, "wb") as dst:
|
||||||
|
dst.write(src.read())
|
||||||
|
dst.flush()
|
||||||
|
os.fsync(dst.fileno())
|
||||||
|
except Exception as exc:
|
||||||
|
raise RuntimeError(f"创建本地 token 备份失败,拒绝继续写入: {exc}")
|
||||||
|
temp_path = token_file + ".tmp"
|
||||||
|
try:
|
||||||
|
with open(temp_path, "w", encoding="utf-8") as f:
|
||||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
f.write("\n")
|
||||||
|
f.flush()
|
||||||
|
os.fsync(f.fileno())
|
||||||
|
os.replace(temp_path, token_file)
|
||||||
|
finally:
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
try:
|
||||||
|
os.unlink(temp_path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
if log_callback:
|
if log_callback:
|
||||||
log_callback(f"[+] 已写入 grok2api 本地池: {pool_name} ({token_file})")
|
log_callback(f"[+] 已写入 grok2api 本地池: {pool_name} ({token_file})")
|
||||||
return True
|
return True
|
||||||
@@ -751,21 +797,30 @@ def add_token_to_grok2api_remote_pool(raw_token, email="", log_callback=None):
|
|||||||
if log_callback:
|
if log_callback:
|
||||||
log_callback(f"[Debug] /tokens/add 写入失败,尝试 /tokens 全量模式: {'; '.join(add_errors)}")
|
log_callback(f"[Debug] /tokens/add 写入失败,尝试 /tokens 全量模式: {'; '.join(add_errors)}")
|
||||||
|
|
||||||
# 兜底:旧版全量保存接口
|
# 兜底:旧版全量保存接口。必须先成功读取远端旧状态,避免空池覆盖。
|
||||||
current = {}
|
current = {}
|
||||||
fallback_base = api_bases[0] if api_bases else base
|
fallback_base = api_bases[0] if api_bases else base
|
||||||
|
loaded_remote_state = False
|
||||||
|
load_errors = []
|
||||||
for api_base in api_bases or [base]:
|
for api_base in api_bases or [base]:
|
||||||
try:
|
try:
|
||||||
resp = http_get(f"{api_base}/tokens", headers=headers, params=query, timeout=20)
|
resp = http_get(f"{api_base}/tokens", headers=headers, params=query, timeout=20)
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
payload = resp.json()
|
payload = resp.json()
|
||||||
current = payload.get("tokens", {}) if isinstance(payload, dict) else {}
|
if isinstance(payload, dict):
|
||||||
|
candidate = payload.get("tokens") if "tokens" in payload else payload
|
||||||
|
if isinstance(candidate, dict):
|
||||||
|
current = candidate
|
||||||
fallback_base = api_base
|
fallback_base = api_base
|
||||||
|
loaded_remote_state = True
|
||||||
break
|
break
|
||||||
except Exception:
|
load_errors.append(f"{api_base}/tokens: unexpected payload")
|
||||||
continue
|
else:
|
||||||
if not isinstance(current, dict):
|
load_errors.append(f"{api_base}/tokens: HTTP {resp.status_code}")
|
||||||
current = {}
|
except Exception as exc:
|
||||||
|
load_errors.append(f"{api_base}/tokens: {exc}")
|
||||||
|
if not loaded_remote_state:
|
||||||
|
raise RuntimeError("无法安全读取远端 token 池,拒绝执行全量覆盖: " + "; ".join(load_errors))
|
||||||
pool = current.get(pool_name)
|
pool = current.get(pool_name)
|
||||||
if not isinstance(pool, list):
|
if not isinstance(pool, list):
|
||||||
pool = []
|
pool = []
|
||||||
@@ -874,7 +929,7 @@ def http_post(url, **kwargs):
|
|||||||
|
|
||||||
def raise_if_cancelled(cancel_callback=None):
|
def raise_if_cancelled(cancel_callback=None):
|
||||||
if cancel_callback and cancel_callback():
|
if cancel_callback and cancel_callback():
|
||||||
raise RegistrationCancelled("鐢ㄦ埛鍋滄娉ㄥ唽")
|
raise RegistrationCancelled("用户停止注册")
|
||||||
|
|
||||||
|
|
||||||
def sleep_with_cancel(seconds, cancel_callback=None):
|
def sleep_with_cancel(seconds, cancel_callback=None):
|
||||||
@@ -1063,7 +1118,7 @@ def yyds_create_account(address=None, domain=None, api_key=None, jwt=None):
|
|||||||
data = resp.json()
|
data = resp.json()
|
||||||
if data.get("success"):
|
if data.get("success"):
|
||||||
return data.get("data", {})
|
return data.get("data", {})
|
||||||
raise Exception(f"YYDS 鍒涘缓閭澶辫触: {data}")
|
raise Exception(f"YYDS 创建邮箱失败: {data}")
|
||||||
|
|
||||||
|
|
||||||
def yyds_get_token(address, api_key=None, jwt=None):
|
def yyds_get_token(address, api_key=None, jwt=None):
|
||||||
@@ -1809,7 +1864,7 @@ def tk_entry(parent, textvariable=None, width=30, **kwargs):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def tk_button(parent, text="", command=None, state=tk.NORMAL, **kwargs):
|
def tk_button(parent, text="", command=None, state="normal", **kwargs):
|
||||||
return tk.Button(
|
return tk.Button(
|
||||||
parent,
|
parent,
|
||||||
text=text,
|
text=text,
|
||||||
@@ -2822,6 +2877,8 @@ def wait_for_sso_cookie(timeout=120, log_callback=None, cancel_callback=None):
|
|||||||
final_no_submit_state = ""
|
final_no_submit_state = ""
|
||||||
final_no_submit_since = None
|
final_no_submit_since = None
|
||||||
final_no_submit_timeout = 25
|
final_no_submit_timeout = 25
|
||||||
|
last_wait_exception_message = ""
|
||||||
|
last_wait_exception_at = 0.0
|
||||||
|
|
||||||
while time.time() < deadline:
|
while time.time() < deadline:
|
||||||
raise_if_cancelled(cancel_callback)
|
raise_if_cancelled(cancel_callback)
|
||||||
@@ -2948,8 +3005,14 @@ return String(cfInput.value || '').trim().length;
|
|||||||
refresh_active_page()
|
refresh_active_page()
|
||||||
except AccountRetryNeeded:
|
except AccountRetryNeeded:
|
||||||
raise
|
raise
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
pass
|
if log_callback:
|
||||||
|
now = time.time()
|
||||||
|
message = f"{exc.__class__.__name__}: {exc}"
|
||||||
|
if message != last_wait_exception_message or now - last_wait_exception_at >= 10:
|
||||||
|
log_callback(f"[Debug] 等待 sso cookie 时出现异常,将继续等待: {message}")
|
||||||
|
last_wait_exception_message = message
|
||||||
|
last_wait_exception_at = now
|
||||||
|
|
||||||
sleep_with_cancel(1, cancel_callback)
|
sleep_with_cancel(1, cancel_callback)
|
||||||
|
|
||||||
@@ -3648,6 +3711,10 @@ def main():
|
|||||||
if len(sys.argv) > 1 and sys.argv[1].strip().lower() in ("start", "cli", "--cli"):
|
if len(sys.argv) > 1 and sys.argv[1].strip().lower() in ("start", "cli", "--cli"):
|
||||||
main_cli()
|
main_cli()
|
||||||
return
|
return
|
||||||
|
if not TK_AVAILABLE:
|
||||||
|
print(f"[!] GUI 模式需要 Tkinter,但当前环境不可用: {TK_IMPORT_ERROR}", file=sys.stderr)
|
||||||
|
print("[*] 可改用 CLI 模式: python grok_register_ttk.py cli", file=sys.stderr)
|
||||||
|
return
|
||||||
root = tk.Tk()
|
root = tk.Tk()
|
||||||
setup_light_theme(root)
|
setup_light_theme(root)
|
||||||
app = GrokRegisterGUI(root)
|
app = GrokRegisterGUI(root)
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ cssselect==1.3.0
|
|||||||
curl_cffi==0.13.0
|
curl_cffi==0.13.0
|
||||||
DataRecorder==3.6.2
|
DataRecorder==3.6.2
|
||||||
DownloadKit==2.0.7
|
DownloadKit==2.0.7
|
||||||
DrissionPage==4.1.1.2
|
DrissionPage>=4.1.1.2,<4.2
|
||||||
filelock==3.19.1
|
filelock==3.19.1
|
||||||
idna==3.13
|
idna==3.13
|
||||||
lxml==6.1.0
|
lxml==6.1.0
|
||||||
|
|||||||
@@ -1,643 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Apply targeted audit fixes without rewriting the project structure.
|
|
||||||
|
|
||||||
This script is intentionally idempotent because the repository may already
|
|
||||||
contain some of the fixes from earlier interrupted bot runs.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
import ast
|
|
||||||
import re
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
APP = ROOT / "grok_register_ttk.py"
|
|
||||||
REQ = ROOT / "requirements.txt"
|
|
||||||
GITIGNORE = ROOT / ".gitignore"
|
|
||||||
README = ROOT / "README.md"
|
|
||||||
OAUTH = ROOT / "cpa_xai" / "oauth_device.py"
|
|
||||||
BROWSER = ROOT / "cpa_xai" / "browser_confirm.py"
|
|
||||||
SCHEMA = ROOT / "cpa_xai" / "schema.py"
|
|
||||||
WRITER = ROOT / "cpa_xai" / "writer.py"
|
|
||||||
|
|
||||||
|
|
||||||
def read(path, encoding="utf-8"):
|
|
||||||
return path.read_text(encoding=encoding)
|
|
||||||
|
|
||||||
|
|
||||||
def write(path, content, encoding="utf-8"):
|
|
||||||
path.write_text(content, encoding=encoding)
|
|
||||||
|
|
||||||
|
|
||||||
def replace_required(text, old, new, label):
|
|
||||||
count = text.count(old)
|
|
||||||
if count != 1:
|
|
||||||
raise RuntimeError(f"{label}: expected one match, got {count}")
|
|
||||||
return text.replace(old, new, 1)
|
|
||||||
|
|
||||||
|
|
||||||
def replace_if_present(text, old, new):
|
|
||||||
return text.replace(old, new, 1) if old in text else text
|
|
||||||
|
|
||||||
|
|
||||||
def replace_func_between(text, start_marker, end_marker, new_block, label):
|
|
||||||
start = text.find(start_marker)
|
|
||||||
if start < 0:
|
|
||||||
if new_block in text:
|
|
||||||
return text
|
|
||||||
raise RuntimeError(f"{label}: start marker not found")
|
|
||||||
end = text.find(end_marker, start)
|
|
||||||
if end < 0:
|
|
||||||
raise RuntimeError(f"{label}: end marker not found")
|
|
||||||
return text[:start] + new_block + text[end:]
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_line(content, line):
|
|
||||||
lines = content.splitlines()
|
|
||||||
if line not in lines:
|
|
||||||
content = content.rstrip() + "\n" + line + "\n"
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
|
||||||
# Trigger token: audit-fixes-v4
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Main application fixes.
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
app = read(APP, encoding="utf-8-sig")
|
|
||||||
|
|
||||||
# L-01 / M-05: UTF-8 without BOM and CLI can start without Tkinter installed.
|
|
||||||
app = replace_if_present(
|
|
||||||
app,
|
|
||||||
"import tkinter as tk\nfrom tkinter import ttk, messagebox, scrolledtext\n",
|
|
||||||
"try:\n import tkinter as tk\n from tkinter import ttk, messagebox, scrolledtext\n TK_AVAILABLE = True\n TK_IMPORT_ERROR = None\nexcept ImportError as exc:\n tk = None\n ttk = None\n messagebox = None\n scrolledtext = None\n TK_AVAILABLE = False\n TK_IMPORT_ERROR = exc\n",
|
|
||||||
)
|
|
||||||
app = app.replace(
|
|
||||||
'def tk_button(parent, text="", command=None, state=tk.NORMAL, **kwargs):',
|
|
||||||
'def tk_button(parent, text="", command=None, state="normal", **kwargs):',
|
|
||||||
)
|
|
||||||
|
|
||||||
# M-02: safer defaults, matching config.example.json.
|
|
||||||
app = app.replace(' "proxy": "http://127.0.0.1:7890",', ' "proxy": "",')
|
|
||||||
app = app.replace(' "grok2api_auto_add_local": True,', ' "grok2api_auto_add_local": False,')
|
|
||||||
old_load_config = '''def load_config():
|
|
||||||
global config
|
|
||||||
if os.path.exists(CONFIG_FILE):
|
|
||||||
try:
|
|
||||||
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
|
||||||
loaded = json.load(f)
|
|
||||||
config = {**DEFAULT_CONFIG, **loaded}
|
|
||||||
except Exception:
|
|
||||||
config = DEFAULT_CONFIG.copy()
|
|
||||||
return config
|
|
||||||
'''
|
|
||||||
new_load_config = '''def load_config():
|
|
||||||
global config
|
|
||||||
if os.path.exists(CONFIG_FILE):
|
|
||||||
try:
|
|
||||||
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
|
||||||
loaded = json.load(f)
|
|
||||||
if not isinstance(loaded, dict):
|
|
||||||
raise ValueError("config root must be a JSON object")
|
|
||||||
config = {**DEFAULT_CONFIG, **loaded}
|
|
||||||
except Exception as exc:
|
|
||||||
message = f"配置文件解析失败: {CONFIG_FILE}: {exc}"
|
|
||||||
print(f"[!] {message}", file=sys.stderr)
|
|
||||||
raise SystemExit(message)
|
|
||||||
else:
|
|
||||||
config = DEFAULT_CONFIG.copy()
|
|
||||||
return config
|
|
||||||
'''
|
|
||||||
app = replace_if_present(app, old_load_config, new_load_config)
|
|
||||||
|
|
||||||
# H-05 / M-01: local token writes are locked, backed up and atomic.
|
|
||||||
new_local_pool = '''def add_token_to_grok2api_local_pool(raw_token, email="", log_callback=None):
|
|
||||||
token = _normalize_sso_token(raw_token)
|
|
||||||
if not token:
|
|
||||||
return False
|
|
||||||
token_file = os.path.abspath(resolve_grok2api_local_token_file())
|
|
||||||
pool_name = str(config.get("grok2api_pool_name", "ssoBasic") or "ssoBasic").strip() or "ssoBasic"
|
|
||||||
parent = os.path.dirname(token_file)
|
|
||||||
os.makedirs(parent, exist_ok=True)
|
|
||||||
lock_path = token_file + ".lock"
|
|
||||||
try:
|
|
||||||
from filelock import FileLock
|
|
||||||
except Exception as exc:
|
|
||||||
raise RuntimeError(f"filelock 依赖不可用,拒绝非原子写入 token 池: {exc}")
|
|
||||||
with FileLock(lock_path, timeout=30):
|
|
||||||
data = {}
|
|
||||||
if os.path.exists(token_file):
|
|
||||||
try:
|
|
||||||
with open(token_file, "r", encoding="utf-8") as f:
|
|
||||||
data = json.load(f) or {}
|
|
||||||
except Exception as exc:
|
|
||||||
broken_path = token_file + f".broken-{int(time.time())}"
|
|
||||||
try:
|
|
||||||
os.replace(token_file, broken_path)
|
|
||||||
except Exception:
|
|
||||||
broken_path = token_file
|
|
||||||
raise RuntimeError(f"本地 token 文件 JSON 解析失败,已停止写入以避免覆盖: {broken_path}: {exc}")
|
|
||||||
if not isinstance(data, dict):
|
|
||||||
raise RuntimeError("本地 token 文件根节点不是 JSON object,拒绝覆盖")
|
|
||||||
pool = data.get(pool_name)
|
|
||||||
if not isinstance(pool, list):
|
|
||||||
pool = []
|
|
||||||
existing = set()
|
|
||||||
for item in pool:
|
|
||||||
if isinstance(item, str):
|
|
||||||
existing.add(_normalize_sso_token(item))
|
|
||||||
elif isinstance(item, dict):
|
|
||||||
existing.add(_normalize_sso_token(item.get("token", "")))
|
|
||||||
if token in existing:
|
|
||||||
if log_callback:
|
|
||||||
log_callback(f"[*] grok2api 本地池已存在 token: {pool_name}")
|
|
||||||
return True
|
|
||||||
pool.append({"token": token, "tags": ["auto-register"], "note": email})
|
|
||||||
data[pool_name] = pool
|
|
||||||
if os.path.exists(token_file):
|
|
||||||
backup_path = token_file + ".bak"
|
|
||||||
try:
|
|
||||||
with open(token_file, "rb") as src, open(backup_path, "wb") as dst:
|
|
||||||
dst.write(src.read())
|
|
||||||
dst.flush()
|
|
||||||
os.fsync(dst.fileno())
|
|
||||||
except Exception as exc:
|
|
||||||
raise RuntimeError(f"创建本地 token 备份失败,拒绝继续写入: {exc}")
|
|
||||||
temp_path = token_file + ".tmp"
|
|
||||||
try:
|
|
||||||
with open(temp_path, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
||||||
f.write("\n")
|
|
||||||
f.flush()
|
|
||||||
os.fsync(f.fileno())
|
|
||||||
os.replace(temp_path, token_file)
|
|
||||||
finally:
|
|
||||||
if os.path.exists(temp_path):
|
|
||||||
try:
|
|
||||||
os.unlink(temp_path)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if log_callback:
|
|
||||||
log_callback(f"[+] 已写入 grok2api 本地池: {pool_name} ({token_file})")
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
if "with FileLock(lock_path" not in app:
|
|
||||||
app = replace_func_between(
|
|
||||||
app,
|
|
||||||
"def add_token_to_grok2api_local_pool(raw_token, email=\"\", log_callback=None):\n",
|
|
||||||
"def get_grok2api_remote_api_bases(base):\n",
|
|
||||||
new_local_pool,
|
|
||||||
"atomic local token pool",
|
|
||||||
)
|
|
||||||
|
|
||||||
# H-01: remote fallback must not POST a full replacement unless old state was read.
|
|
||||||
old_remote_fallback = ''' # 兜底:旧版全量保存接口
|
|
||||||
current = {}
|
|
||||||
fallback_base = api_bases[0] if api_bases else base
|
|
||||||
for api_base in api_bases or [base]:
|
|
||||||
try:
|
|
||||||
resp = http_get(f"{api_base}/tokens", headers=headers, params=query, timeout=20)
|
|
||||||
if resp.status_code == 200:
|
|
||||||
payload = resp.json()
|
|
||||||
current = payload.get("tokens", {}) if isinstance(payload, dict) else {}
|
|
||||||
fallback_base = api_base
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
if not isinstance(current, dict):
|
|
||||||
current = {}
|
|
||||||
'''
|
|
||||||
new_remote_fallback = ''' # 兜底:旧版全量保存接口。必须先成功读取远端旧状态,避免空池覆盖。
|
|
||||||
current = {}
|
|
||||||
fallback_base = api_bases[0] if api_bases else base
|
|
||||||
loaded_remote_state = False
|
|
||||||
load_errors = []
|
|
||||||
for api_base in api_bases or [base]:
|
|
||||||
try:
|
|
||||||
resp = http_get(f"{api_base}/tokens", headers=headers, params=query, timeout=20)
|
|
||||||
if resp.status_code == 200:
|
|
||||||
payload = resp.json()
|
|
||||||
if isinstance(payload, dict):
|
|
||||||
candidate = payload.get("tokens") if "tokens" in payload else payload
|
|
||||||
if isinstance(candidate, dict):
|
|
||||||
current = candidate
|
|
||||||
fallback_base = api_base
|
|
||||||
loaded_remote_state = True
|
|
||||||
break
|
|
||||||
load_errors.append(f"{api_base}/tokens: unexpected payload")
|
|
||||||
else:
|
|
||||||
load_errors.append(f"{api_base}/tokens: HTTP {resp.status_code}")
|
|
||||||
except Exception as exc:
|
|
||||||
load_errors.append(f"{api_base}/tokens: {exc}")
|
|
||||||
if not loaded_remote_state:
|
|
||||||
raise RuntimeError("无法安全读取远端 token 池,拒绝执行全量覆盖: " + "; ".join(load_errors))
|
|
||||||
'''
|
|
||||||
app = replace_if_present(app, old_remote_fallback, new_remote_fallback)
|
|
||||||
|
|
||||||
# M-06: do not hide unexpected SSO wait exceptions forever.
|
|
||||||
if "last_wait_exception_message" not in app:
|
|
||||||
app = replace_if_present(
|
|
||||||
app,
|
|
||||||
''' final_no_submit_state = ""
|
|
||||||
final_no_submit_since = None
|
|
||||||
final_no_submit_timeout = 25
|
|
||||||
|
|
||||||
while time.time() < deadline:
|
|
||||||
''',
|
|
||||||
''' final_no_submit_state = ""
|
|
||||||
final_no_submit_since = None
|
|
||||||
final_no_submit_timeout = 25
|
|
||||||
last_wait_exception_message = ""
|
|
||||||
last_wait_exception_at = 0.0
|
|
||||||
|
|
||||||
while time.time() < deadline:
|
|
||||||
''',
|
|
||||||
)
|
|
||||||
app = replace_if_present(
|
|
||||||
app,
|
|
||||||
''' except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sleep_with_cancel(1, cancel_callback)
|
|
||||||
''',
|
|
||||||
''' except Exception as exc:
|
|
||||||
if log_callback:
|
|
||||||
now = time.time()
|
|
||||||
message = f"{exc.__class__.__name__}: {exc}"
|
|
||||||
if message != last_wait_exception_message or now - last_wait_exception_at >= 10:
|
|
||||||
log_callback(f"[Debug] 等待 sso cookie 时出现异常,将继续等待: {message}")
|
|
||||||
last_wait_exception_message = message
|
|
||||||
last_wait_exception_at = now
|
|
||||||
|
|
||||||
sleep_with_cancel(1, cancel_callback)
|
|
||||||
''',
|
|
||||||
)
|
|
||||||
|
|
||||||
# L-02: repair known mojibake that affects logs/control flow.
|
|
||||||
for bad, good in {
|
|
||||||
"鐢ㄦ埛鍋滄娉ㄥ唽": "用户停止注册",
|
|
||||||
"YYDS 鍒涘缓閭澶辫触": "YYDS 创建邮箱失败",
|
|
||||||
"YYDS 鑾峰彇JWT澶辫触": "YYDS 获取 JWT 失败",
|
|
||||||
}.items():
|
|
||||||
app = app.replace(bad, good)
|
|
||||||
|
|
||||||
# M-05: GUI-only Tk failure should not break CLI mode.
|
|
||||||
old_main = '''def main():
|
|
||||||
if len(sys.argv) > 1 and sys.argv[1].strip().lower() in ("start", "cli", "--cli"):
|
|
||||||
main_cli()
|
|
||||||
return
|
|
||||||
root = tk.Tk()
|
|
||||||
setup_light_theme(root)
|
|
||||||
app = GrokRegisterGUI(root)
|
|
||||||
root.mainloop()
|
|
||||||
'''
|
|
||||||
new_main = '''def main():
|
|
||||||
if len(sys.argv) > 1 and sys.argv[1].strip().lower() in ("start", "cli", "--cli"):
|
|
||||||
main_cli()
|
|
||||||
return
|
|
||||||
if not TK_AVAILABLE:
|
|
||||||
print(f"[!] GUI 模式需要 Tkinter,但当前环境不可用: {TK_IMPORT_ERROR}", file=sys.stderr)
|
|
||||||
print("[*] 可改用 CLI 模式: python grok_register_ttk.py cli", file=sys.stderr)
|
|
||||||
return
|
|
||||||
root = tk.Tk()
|
|
||||||
setup_light_theme(root)
|
|
||||||
app = GrokRegisterGUI(root)
|
|
||||||
root.mainloop()
|
|
||||||
'''
|
|
||||||
app = replace_if_present(app, old_main, new_main)
|
|
||||||
|
|
||||||
ast.parse(app)
|
|
||||||
write(APP, app, encoding="utf-8")
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# CPA browser / OAuth fixes.
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
browser = read(BROWSER)
|
|
||||||
browser = browser.replace("except BaseException as exc:", "except Exception as exc:")
|
|
||||||
old_proxy_block = ''' from .proxyutil import proxy_for_chromium, proxy_log_label, resolve_proxy
|
|
||||||
|
|
||||||
resolved = resolve_proxy(proxy)
|
|
||||||
chrome_proxy = proxy_for_chromium(resolved)
|
|
||||||
if chrome_proxy:
|
|
||||||
options.set_argument("--proxy-server=%s" % chrome_proxy)
|
|
||||||
logger("browser proxy=%s (chromium %s)" % (proxy_log_label(resolved), chrome_proxy))
|
|
||||||
else:
|
|
||||||
logger("browser proxy=(none)")
|
|
||||||
|
|
||||||
browser = Chromium(options)
|
|
||||||
page = browser.latest_tab
|
|
||||||
logger("standalone chromium started")
|
|
||||||
return browser, page
|
|
||||||
'''
|
|
||||||
new_proxy_block = ''' from .proxyutil import prepare_chromium_proxy, proxy_log_label, resolve_proxy
|
|
||||||
|
|
||||||
resolved = resolve_proxy(proxy)
|
|
||||||
proxy_bridge = None
|
|
||||||
chrome_proxy, proxy_bridge = prepare_chromium_proxy(resolved, log=logger)
|
|
||||||
if chrome_proxy:
|
|
||||||
options.set_argument("--proxy-server=%s" % chrome_proxy)
|
|
||||||
logger("browser proxy=%s (chromium %s)" % (proxy_log_label(resolved), chrome_proxy))
|
|
||||||
else:
|
|
||||||
logger("browser proxy=(none)")
|
|
||||||
|
|
||||||
browser = Chromium(options)
|
|
||||||
if proxy_bridge is not None:
|
|
||||||
try:
|
|
||||||
setattr(browser, "_cpa_proxy_bridge", proxy_bridge)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
_register_mint_browser(browser)
|
|
||||||
page = browser.latest_tab
|
|
||||||
logger("standalone chromium started")
|
|
||||||
return browser, page
|
|
||||||
'''
|
|
||||||
browser = replace_if_present(browser, old_proxy_block, new_proxy_block)
|
|
||||||
old_close_block = '''def close_standalone(browser: Any) -> None:
|
|
||||||
try:
|
|
||||||
browser.quit()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
_mint_tls = threading.local()
|
|
||||||
'''
|
|
||||||
new_close_block = '''def close_standalone(browser: Any) -> None:
|
|
||||||
if browser is None:
|
|
||||||
return
|
|
||||||
_unregister_mint_browser(browser)
|
|
||||||
bridge = getattr(browser, "_cpa_proxy_bridge", None)
|
|
||||||
try:
|
|
||||||
browser.quit()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if bridge is not None:
|
|
||||||
try:
|
|
||||||
bridge.stop()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
_mint_tls = threading.local()
|
|
||||||
_mint_registry_lock = threading.Lock()
|
|
||||||
_mint_registry = set()
|
|
||||||
|
|
||||||
|
|
||||||
def _register_mint_browser(browser: Any) -> None:
|
|
||||||
if browser is None:
|
|
||||||
return
|
|
||||||
with _mint_registry_lock:
|
|
||||||
_mint_registry.add(browser)
|
|
||||||
|
|
||||||
|
|
||||||
def _unregister_mint_browser(browser: Any) -> None:
|
|
||||||
if browser is None:
|
|
||||||
return
|
|
||||||
with _mint_registry_lock:
|
|
||||||
_mint_registry.discard(browser)
|
|
||||||
'''
|
|
||||||
if "_mint_registry = set()" not in browser:
|
|
||||||
browser = replace_required(browser, old_close_block, new_close_block, "CPA global browser registry")
|
|
||||||
old_shutdown = '''def shutdown_mint_browsers() -> None:
|
|
||||||
state = _mint_tls_get()
|
|
||||||
browser = state.get("browser")
|
|
||||||
if browser is not None:
|
|
||||||
try:
|
|
||||||
close_standalone(browser)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
state.update({"browser": None, "page": None, "served": 0, "proxy": None, "headless": None})
|
|
||||||
'''
|
|
||||||
new_shutdown = '''def shutdown_mint_browsers() -> None:
|
|
||||||
state = _mint_tls_get()
|
|
||||||
with _mint_registry_lock:
|
|
||||||
browsers = list(_mint_registry)
|
|
||||||
for browser in browsers:
|
|
||||||
try:
|
|
||||||
close_standalone(browser)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
state.update({"browser": None, "page": None, "served": 0, "proxy": None, "headless": None})
|
|
||||||
'''
|
|
||||||
browser = replace_if_present(browser, old_shutdown, new_shutdown)
|
|
||||||
old_poll = ''' def _poll() -> None:
|
|
||||||
try:
|
|
||||||
time.sleep(2)
|
|
||||||
result = poll_device_token(
|
|
||||||
session.device_code,
|
|
||||||
token_endpoint=session.token_endpoint,
|
|
||||||
interval=max(session.interval, 5),
|
|
||||||
expires_in=min(session.expires_in, int(browser_timeout_sec) + 60),
|
|
||||||
log=logger,
|
|
||||||
cancel=cancel,
|
|
||||||
proxy=resolved or None,
|
|
||||||
)
|
|
||||||
token_box["token"] = result
|
|
||||||
stop_event.set()
|
|
||||||
logger("token poll SUCCESS — stop_event set")
|
|
||||||
except Exception as exc:
|
|
||||||
error_box["err"] = exc
|
|
||||||
stop_event.set()
|
|
||||||
'''
|
|
||||||
new_poll = ''' def combined_cancel():
|
|
||||||
return stop_event.is_set() or bool(cancel and cancel())
|
|
||||||
|
|
||||||
def _poll() -> None:
|
|
||||||
try:
|
|
||||||
for _ in range(20):
|
|
||||||
if combined_cancel():
|
|
||||||
raise OAuthDeviceError("cancelled")
|
|
||||||
time.sleep(0.1)
|
|
||||||
result = poll_device_token(
|
|
||||||
session.device_code,
|
|
||||||
token_endpoint=session.token_endpoint,
|
|
||||||
interval=max(session.interval, 5),
|
|
||||||
expires_in=min(session.expires_in, int(browser_timeout_sec) + 60),
|
|
||||||
log=logger,
|
|
||||||
cancel=combined_cancel,
|
|
||||||
proxy=resolved or None,
|
|
||||||
)
|
|
||||||
token_box["token"] = result
|
|
||||||
stop_event.set()
|
|
||||||
logger("token poll SUCCESS — stop_event set")
|
|
||||||
except Exception as exc:
|
|
||||||
error_box["err"] = exc
|
|
||||||
stop_event.set()
|
|
||||||
'''
|
|
||||||
browser = replace_if_present(browser, old_poll, new_poll)
|
|
||||||
old_join = ''' if hard:
|
|
||||||
stop_event.set()
|
|
||||||
raise
|
|
||||||
thread.join(timeout=max(browser_timeout_sec, 60) + 30)
|
|
||||||
'''
|
|
||||||
new_join = ''' if hard:
|
|
||||||
stop_event.set()
|
|
||||||
thread.join(timeout=5)
|
|
||||||
if thread.is_alive():
|
|
||||||
logger("token poll thread did not stop within 5s after browser failure")
|
|
||||||
raise
|
|
||||||
thread.join(timeout=max(browser_timeout_sec, 60) + 30)
|
|
||||||
if thread.is_alive():
|
|
||||||
stop_event.set()
|
|
||||||
thread.join(timeout=5)
|
|
||||||
if thread.is_alive():
|
|
||||||
raise OAuthDeviceError("token poll thread did not stop after timeout")
|
|
||||||
'''
|
|
||||||
browser = replace_if_present(browser, old_join, new_join)
|
|
||||||
ast.parse(browser)
|
|
||||||
write(BROWSER, browser)
|
|
||||||
|
|
||||||
oauth = read(OAUTH)
|
|
||||||
oauth = oauth.replace("except BaseException as exc:", "except Exception as exc:")
|
|
||||||
old_poll_header = '''def poll_device_token(
|
|
||||||
device_code,
|
|
||||||
token_endpoint,
|
|
||||||
client_id=CLIENT_ID,
|
|
||||||
interval=5,
|
|
||||||
expires_in=1800,
|
|
||||||
timeout=30.0,
|
|
||||||
log=None,
|
|
||||||
cancel=None,
|
|
||||||
proxy=None,
|
|
||||||
):
|
|
||||||
logger = log or (lambda message: None)
|
|
||||||
deadline = time.time() + max(int(expires_in) - 5, 30)
|
|
||||||
sleep_seconds = max(int(interval), 1)
|
|
||||||
net_streak = 0
|
|
||||||
max_net_streak = 20
|
|
||||||
while time.time() < deadline:
|
|
||||||
if cancel and cancel():
|
|
||||||
raise OAuthDeviceError("cancelled")
|
|
||||||
'''
|
|
||||||
new_poll_header = '''def _sleep_with_cancel(seconds, cancel=None):
|
|
||||||
deadline = time.time() + max(float(seconds), 0.0)
|
|
||||||
while time.time() < deadline:
|
|
||||||
if cancel and cancel():
|
|
||||||
raise OAuthDeviceError("cancelled")
|
|
||||||
time.sleep(min(0.2, max(deadline - time.time(), 0.0)))
|
|
||||||
|
|
||||||
|
|
||||||
def poll_device_token(
|
|
||||||
device_code,
|
|
||||||
token_endpoint,
|
|
||||||
client_id=CLIENT_ID,
|
|
||||||
interval=5,
|
|
||||||
expires_in=1800,
|
|
||||||
timeout=30.0,
|
|
||||||
log=None,
|
|
||||||
cancel=None,
|
|
||||||
proxy=None,
|
|
||||||
):
|
|
||||||
logger = log or (lambda message: None)
|
|
||||||
deadline = time.time() + max(int(expires_in) - 5, 30)
|
|
||||||
sleep_seconds = max(int(interval), 1)
|
|
||||||
net_streak = 0
|
|
||||||
max_net_streak = 20
|
|
||||||
while time.time() < deadline:
|
|
||||||
if cancel and cancel():
|
|
||||||
raise OAuthDeviceError("cancelled")
|
|
||||||
'''
|
|
||||||
if "def _sleep_with_cancel(" not in oauth:
|
|
||||||
oauth = replace_required(oauth, old_poll_header, new_poll_header, "oauth cancellable sleep helper")
|
|
||||||
oauth = oauth.replace("time.sleep(wait_seconds)", "_sleep_with_cancel(wait_seconds, cancel)")
|
|
||||||
oauth = oauth.replace("time.sleep(sleep_seconds)", "_sleep_with_cancel(sleep_seconds, cancel)")
|
|
||||||
ast.parse(oauth)
|
|
||||||
write(OAUTH, oauth)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# CPA schema/writer hardening.
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
schema = read(SCHEMA)
|
|
||||||
old_expired = ''' expired = ""
|
|
||||||
if exp:
|
|
||||||
expired = datetime.fromtimestamp(exp, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
||||||
'''
|
|
||||||
new_expired = ''' expired = ""
|
|
||||||
if exp:
|
|
||||||
expired = datetime.fromtimestamp(exp, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
||||||
elif expires_in:
|
|
||||||
expired = datetime.fromtimestamp(time.time() + int(expires_in or 0), tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
||||||
'''
|
|
||||||
schema = replace_if_present(schema, old_expired, new_expired)
|
|
||||||
ast.parse(schema)
|
|
||||||
write(SCHEMA, schema)
|
|
||||||
|
|
||||||
writer = read(WRITER)
|
|
||||||
old_writer_head = '''def write_cpa_xai_auth(auth_dir, payload, filename=None):
|
|
||||||
root = Path(auth_dir).expanduser().resolve()
|
|
||||||
root.mkdir(parents=True, exist_ok=True)
|
|
||||||
target_name = filename or credential_file_name(payload.get("email", ""), payload.get("sub", ""))
|
|
||||||
if not str(target_name).endswith(".json"):
|
|
||||||
target_name = str(target_name) + ".json"
|
|
||||||
destination = root / str(target_name)
|
|
||||||
'''
|
|
||||||
new_writer_head = '''def _is_relative_to(path, root):
|
|
||||||
try:
|
|
||||||
path.relative_to(root)
|
|
||||||
return True
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def write_cpa_xai_auth(auth_dir, payload, filename=None):
|
|
||||||
root = Path(auth_dir).expanduser().resolve()
|
|
||||||
root.mkdir(parents=True, exist_ok=True)
|
|
||||||
target_name = filename or credential_file_name(payload.get("email", ""), payload.get("sub", ""))
|
|
||||||
target_name = Path(str(target_name)).name
|
|
||||||
if not str(target_name).endswith(".json"):
|
|
||||||
target_name = str(target_name) + ".json"
|
|
||||||
destination = (root / str(target_name)).resolve()
|
|
||||||
if not _is_relative_to(destination, root):
|
|
||||||
raise ValueError("CPA auth filename must stay inside auth_dir")
|
|
||||||
'''
|
|
||||||
writer = replace_if_present(writer, old_writer_head, new_writer_head)
|
|
||||||
ast.parse(writer)
|
|
||||||
write(WRITER, writer)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Requirements / ignore / docs.
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
req = read(REQ)
|
|
||||||
req = req.replace("DrissionPage==4.1.1.2", "DrissionPage>=4.1.1.2,<4.2")
|
|
||||||
write(REQ, req)
|
|
||||||
|
|
||||||
gitignore = read(GITIGNORE)
|
|
||||||
gitignore = ensure_line(gitignore, "screenshots/")
|
|
||||||
gitignore = ensure_line(gitignore, "*.png")
|
|
||||||
write(GITIGNORE, gitignore)
|
|
||||||
|
|
||||||
readme = read(README)
|
|
||||||
readme = readme.replace(
|
|
||||||
"- `cpa_auths/cpa_auth_failed.txt`:OIDC 导出失败记录。\n- `*.log`:可选日志文件。",
|
|
||||||
"- `cpa_auths/cpa_auth_failed.txt`:OIDC 导出失败记录。\n- `screenshots/`:CPA/OIDC 浏览器失败调试截图,已被 `.gitignore` 忽略。\n- `*.log`:可选日志文件。",
|
|
||||||
)
|
|
||||||
old_tree = '''```text
|
|
||||||
.
|
|
||||||
├── grok_register_ttk.py # 主程序
|
|
||||||
├── cf_mail_debug.py # Cloudflare 邮箱调试工具
|
|
||||||
├── config.example.json # 配置示例
|
|
||||||
├── requirements.txt # Python 依赖
|
|
||||||
└── README.md
|
|
||||||
```
|
|
||||||
'''
|
|
||||||
new_tree = '''```text
|
|
||||||
.
|
|
||||||
├── grok_register_ttk.py # 主程序(GUI / CLI)
|
|
||||||
├── cpa_export.py # 注册成功后的 CPA/OIDC 导出入口
|
|
||||||
├── cpa_xai/ # xAI Device Auth、浏览器授权和凭证写入模块
|
|
||||||
├── cf_mail_debug.py # Cloudflare 邮箱调试工具
|
|
||||||
├── config.example.json # 配置示例
|
|
||||||
├── requirements.txt # Python 依赖
|
|
||||||
├── tests/ # 现有测试用例
|
|
||||||
├── assets/ # README 资源
|
|
||||||
└── README.md
|
|
||||||
```
|
|
||||||
'''
|
|
||||||
readme = replace_if_present(readme, old_tree, new_tree)
|
|
||||||
write(README, readme)
|
|
||||||
|
|
||||||
# Final syntax validation for touched Python files.
|
|
||||||
for path in (APP, OAUTH, BROWSER, SCHEMA, WRITER):
|
|
||||||
ast.parse(read(path, encoding="utf-8"))
|
|
||||||
|
|
||||||
print("audit fixes applied")
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Repair string escapes inside the temporary audit patch script before running it.
|
|
||||||
|
|
||||||
The patch script embeds Python source inside triple-quoted strings. Source snippets
|
|
||||||
that should generate literal backslash-n in grok_register_ttk.py must therefore
|
|
||||||
use double escaping in this script.
|
|
||||||
"""
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
path = Path(__file__).resolve().parent / "apply_audit_fixes.py"
|
|
||||||
text = path.read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
replacements = {
|
|
||||||
'f.write("\\n")': 'f.write("\\\\n")',
|
|
||||||
'line = f"{email}----{password or \'\'}----{sso}\\n"': 'line = f"{email}----{password or \'\'}----{sso}\\\\n"',
|
|
||||||
}
|
|
||||||
|
|
||||||
for old, new in replacements.items():
|
|
||||||
text = text.replace(old, new)
|
|
||||||
|
|
||||||
path.write_text(text, encoding="utf-8")
|
|
||||||
print("audit patch escapes repaired")
|
|
||||||
Reference in New Issue
Block a user