Initialize project and mask sensitive information
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
CLOUDFLARE_TEMP_EMAIL_BASE_URL="https://example.com/"
|
||||
ADMIN_PASSWORDS='["***","***"]'
|
||||
CLI_PROXY_API_BASE_URL=
|
||||
CLI_PROXY_API_KEY=
|
||||
@@ -0,0 +1,5 @@
|
||||
.env
|
||||
venv/
|
||||
tokens/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -0,0 +1,53 @@
|
||||
|
||||
|
||||
> **⚠️ 声明:本仓库代码仅供学习交流使用,严禁用于任何商业用途!如果您觉得有帮助,欢迎点个 Star ⭐️ 支持一下!**
|
||||
|
||||
## 环境准备
|
||||
1. 创建并激活虚拟环境(Python 3.9+):
|
||||
```bash
|
||||
cd qwen-register
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
2. 安装依赖:
|
||||
```bash
|
||||
pip install requests
|
||||
```
|
||||
|
||||
3. 配置环境变量:
|
||||
```bash
|
||||
CLOUDFLARE_TEMP_EMAIL_BASE_URL=https://example.com/
|
||||
ADMIN_PASSWORDS='["***","***"]'
|
||||
CLI_PROXY_API_BASE_URL=http://example.com:8317
|
||||
CLI_PROXY_API_KEY=sk-***
|
||||
```
|
||||
|
||||
## 运行
|
||||
```bash
|
||||
cd qwen-register
|
||||
source venv/bin/activate
|
||||
python qwen_register.py
|
||||
```
|
||||
|
||||
默认会批量注册 `5` 个账号。可以通过 `--count` 或环境变量 `QWEN_REGISTER_COUNT` 覆盖:
|
||||
|
||||
```bash
|
||||
python qwen_register.py --count 1
|
||||
python qwen_register.py --count 10
|
||||
```
|
||||
|
||||
批量模式下:
|
||||
- 单条失败会记录到结果里,并继续后续注册
|
||||
- 每两条注册之间会随机等待 `10-30` 秒
|
||||
- 最终会输出 `success_count` 和 `failed_count` 统计
|
||||
- 运行中的阶段日志输出到 `stderr`,最终 JSON 结果输出到 `stdout`
|
||||
|
||||
如果要在注册成功后自动注册、激活、生成官方 Qwen OAuth 凭证并上传到 CLIProxyAPI:
|
||||
|
||||
```bash
|
||||
python qwen_register.py --count 5 \
|
||||
--cli-proxy-api-base-url http://example.com:8317 \
|
||||
--cli-proxy-api-key sk-*** \
|
||||
--oauth-headed
|
||||
```
|
||||
@@ -0,0 +1,149 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from email import message_from_string
|
||||
import re
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class CloudflareTempEmailError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class CloudflareTempEmailClient:
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str,
|
||||
admin_passwords: list[str],
|
||||
*,
|
||||
session: requests.Session | None = None,
|
||||
timeout: float = 20.0,
|
||||
) -> None:
|
||||
if not base_url:
|
||||
raise ValueError("base_url is required")
|
||||
if not admin_passwords:
|
||||
raise ValueError("admin_passwords is required")
|
||||
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.admin_passwords = [item for item in admin_passwords if item]
|
||||
self.timeout = timeout
|
||||
self.session = session or requests.Session()
|
||||
|
||||
def create_address(self, *, name: str, domain: str = "", enable_prefix: bool = True) -> dict[str, Any]:
|
||||
payload = {
|
||||
"enablePrefix": enable_prefix,
|
||||
"name": name,
|
||||
"domain": domain,
|
||||
}
|
||||
errors: list[str] = []
|
||||
for password in self.admin_passwords:
|
||||
try:
|
||||
response = self.session.post(
|
||||
f"{self.base_url}/admin/new_address",
|
||||
headers={"x-admin-auth": password, "Content-Type": "application/json"},
|
||||
json=payload,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if not data.get("address") or not data.get("jwt"):
|
||||
raise CloudflareTempEmailError(f"malformed create address response: {data}")
|
||||
return data
|
||||
except Exception as exc: # pragma: no cover - exercised via fallback test
|
||||
errors.append(str(exc))
|
||||
continue
|
||||
raise CloudflareTempEmailError("; ".join(errors) or "create address failed")
|
||||
|
||||
def list_mails(self, jwt: str, *, limit: int = 20, offset: int = 0) -> list[dict[str, Any]]:
|
||||
response = self.session.get(
|
||||
f"{self.base_url}/api/mails",
|
||||
headers={"Authorization": f"Bearer {jwt}"},
|
||||
params={"limit": limit, "offset": offset},
|
||||
timeout=self.timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if isinstance(data, list):
|
||||
return [item for item in data if isinstance(item, dict)]
|
||||
if isinstance(data, dict):
|
||||
items = data.get("results") or data.get("mails") or data.get("items") or []
|
||||
return [item for item in items if isinstance(item, dict)]
|
||||
return []
|
||||
|
||||
def get_mail(self, jwt: str, mail_id: str) -> dict[str, Any]:
|
||||
response = self.session.get(
|
||||
f"{self.base_url}/api/mails/{mail_id}",
|
||||
headers={"Authorization": f"Bearer {jwt}"},
|
||||
timeout=self.timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if not isinstance(data, dict):
|
||||
raise CloudflareTempEmailError("malformed mail detail response")
|
||||
return data
|
||||
|
||||
def wait_for_verification_link(
|
||||
self,
|
||||
jwt: str,
|
||||
*,
|
||||
timeout_seconds: float = 120.0,
|
||||
poll_interval_seconds: float = 5.0,
|
||||
) -> dict[str, str]:
|
||||
deadline = time.time() + timeout_seconds
|
||||
while time.time() <= deadline:
|
||||
for item in self.list_mails(jwt):
|
||||
mail_id = str(item.get("id") or "").strip()
|
||||
link = self.extract_verification_link(item)
|
||||
if link:
|
||||
return {"mail_id": mail_id, "link": link}
|
||||
if not mail_id:
|
||||
continue
|
||||
mail = self.get_mail(jwt, mail_id)
|
||||
link = self.extract_verification_link(mail)
|
||||
if link:
|
||||
return {"mail_id": mail_id, "link": link}
|
||||
time.sleep(max(poll_interval_seconds, 0.0))
|
||||
raise TimeoutError("activation mail timeout")
|
||||
|
||||
@staticmethod
|
||||
def extract_verification_link(payload: dict[str, Any]) -> str | None:
|
||||
parts = [str(v) for v in payload.values() if v is not None]
|
||||
raw = str(payload.get("raw") or "")
|
||||
if raw:
|
||||
try:
|
||||
message = message_from_string(raw)
|
||||
for part in message.walk():
|
||||
candidate = CloudflareTempEmailClient._decode_mail_part(part)
|
||||
if candidate:
|
||||
parts.append(candidate)
|
||||
except Exception:
|
||||
parts.append(raw)
|
||||
blob = " ".join(parts)
|
||||
patterns = [
|
||||
r'https://chat\.qwen\.ai[^\s"\'<>]+',
|
||||
r'https://[^"\']+activation[^"\']+',
|
||||
]
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, blob, flags=re.IGNORECASE)
|
||||
if match:
|
||||
return match.group(0).replace("&", "&").rstrip('"\'>#')
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _decode_mail_part(part: Any) -> str:
|
||||
payload = part.get_payload(decode=True)
|
||||
if payload is None:
|
||||
return ""
|
||||
charset = part.get_content_charset() or "utf-8"
|
||||
try:
|
||||
return payload.decode(charset, errors="ignore")
|
||||
except LookupError:
|
||||
return payload.decode("utf-8", errors="ignore")
|
||||
|
||||
@staticmethod
|
||||
def extract_code(payload: dict[str, Any]) -> str | None:
|
||||
blob = " ".join(str(v) for v in payload.values() if v is not None)
|
||||
match = re.search(r"(?<!\d)(\d{6})(?!\d)", blob)
|
||||
return match.group(1) if match else None
|
||||
@@ -0,0 +1,481 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import pty
|
||||
import re
|
||||
import select
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
AUTHORIZE_URL_RE = re.compile(r"https://chat\.qwen\.ai/authorize\?user_code=[^\s]+&client=qwen-code")
|
||||
CONTAINER_CREDENTIAL_RE = re.compile(r"/root/\.cli-proxy-api/(?P<filename>[^\s]+\.json)")
|
||||
IDENTITY_PROMPT = "Please input your email address or alias for Qwen:"
|
||||
SUCCESS_MARKER = "Qwen authentication successful"
|
||||
|
||||
|
||||
def safe_email_name(email: str) -> str:
|
||||
return email.replace("@", "_at_").replace(".", "_")
|
||||
|
||||
|
||||
def write_qwen_login_config(work_dir: Path, api_key: str = "sk-temp-qwen-login") -> tuple[Path, Path]:
|
||||
work_dir.mkdir(parents=True, exist_ok=True)
|
||||
auth_dir = work_dir / "auth"
|
||||
auth_dir.mkdir(parents=True, exist_ok=True)
|
||||
config_path = work_dir / "config.yaml"
|
||||
config_path.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
'port: 8317',
|
||||
'auth-dir: "/root/.cli-proxy-api"',
|
||||
"api-keys:",
|
||||
f' - "{api_key}"',
|
||||
"",
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return config_path, auth_dir
|
||||
|
||||
|
||||
class QwenOAuthLoginRunner:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
config_path: Path,
|
||||
auth_dir: Path,
|
||||
image: str = "eceasy/cli-proxy-api:latest",
|
||||
process_factory: Callable[..., subprocess.Popen[str]] | None = None,
|
||||
) -> None:
|
||||
self.config_path = config_path
|
||||
self.auth_dir = auth_dir
|
||||
self.image = image
|
||||
self.process_factory = process_factory or subprocess.Popen
|
||||
self.process: subprocess.Popen[str] | None = None
|
||||
self._master_fd: int | None = None
|
||||
self._output_buffer = ""
|
||||
|
||||
def start(self) -> None:
|
||||
if self.process is not None:
|
||||
return
|
||||
command = [
|
||||
"docker",
|
||||
"run",
|
||||
"--rm",
|
||||
"-i",
|
||||
"-v",
|
||||
f"{self.config_path}:/CLIProxyAPI/config.yaml",
|
||||
"-v",
|
||||
f"{self.auth_dir}:/root/.cli-proxy-api",
|
||||
self.image,
|
||||
"/CLIProxyAPI/CLIProxyAPI",
|
||||
"--qwen-login",
|
||||
]
|
||||
if self.process_factory is subprocess.Popen:
|
||||
master_fd, slave_fd = pty.openpty()
|
||||
self._master_fd = master_fd
|
||||
try:
|
||||
self.process = self.process_factory(
|
||||
command,
|
||||
stdin=slave_fd,
|
||||
stdout=slave_fd,
|
||||
stderr=slave_fd,
|
||||
text=False,
|
||||
close_fds=True,
|
||||
)
|
||||
finally:
|
||||
os.close(slave_fd)
|
||||
return
|
||||
self.process = self.process_factory(
|
||||
command,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
)
|
||||
|
||||
def wait_for_authorize_url(self, timeout_seconds: float = 30.0) -> str:
|
||||
return self._wait_for_match(AUTHORIZE_URL_RE, timeout_seconds)
|
||||
|
||||
def wait_for_identity_prompt(self, timeout_seconds: float = 120.0) -> None:
|
||||
self._wait_for_text(IDENTITY_PROMPT, timeout_seconds)
|
||||
|
||||
def submit_identity(self, email: str) -> None:
|
||||
if self.process is None:
|
||||
raise RuntimeError("runner process is not started")
|
||||
if self._master_fd is not None:
|
||||
os.write(self._master_fd, f"{email}\n".encode("utf-8"))
|
||||
return
|
||||
if self.process.stdin is None:
|
||||
raise RuntimeError("runner process is not started")
|
||||
self.process.stdin.write(f"{email}\n")
|
||||
self.process.stdin.flush()
|
||||
|
||||
def wait_for_credentials(self, timeout_seconds: float = 60.0) -> Path:
|
||||
filename = self._wait_for_group(CONTAINER_CREDENTIAL_RE, "filename", timeout_seconds)
|
||||
return self.auth_dir / filename
|
||||
|
||||
def close(self) -> None:
|
||||
if self.process is None:
|
||||
return
|
||||
if self.process.poll() is None:
|
||||
self.process.terminate()
|
||||
try:
|
||||
self.process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.process.kill()
|
||||
self.process = None
|
||||
if self._master_fd is not None:
|
||||
os.close(self._master_fd)
|
||||
self._master_fd = None
|
||||
|
||||
def _wait_for_text(self, needle: str, timeout_seconds: float) -> str:
|
||||
deadline = time.time() + timeout_seconds
|
||||
while time.time() < deadline:
|
||||
if needle in self._output_buffer:
|
||||
return needle
|
||||
if not self._read_available_text(timeout_seconds=0.2):
|
||||
time.sleep(0.1)
|
||||
raise TimeoutError(f"did not observe text: {needle}")
|
||||
|
||||
def _wait_for_match(self, pattern: re.Pattern[str], timeout_seconds: float) -> str:
|
||||
deadline = time.time() + timeout_seconds
|
||||
while time.time() < deadline:
|
||||
match = pattern.search(self._output_buffer)
|
||||
if match:
|
||||
return match.group(0)
|
||||
if not self._read_available_text(timeout_seconds=0.2):
|
||||
time.sleep(0.1)
|
||||
raise TimeoutError(f"did not observe pattern: {pattern.pattern}")
|
||||
|
||||
def _wait_for_group(self, pattern: re.Pattern[str], group_name: str, timeout_seconds: float) -> str:
|
||||
deadline = time.time() + timeout_seconds
|
||||
while time.time() < deadline:
|
||||
match = pattern.search(self._output_buffer)
|
||||
if match:
|
||||
return match.group(group_name)
|
||||
if not self._read_available_text(timeout_seconds=0.2):
|
||||
time.sleep(0.1)
|
||||
raise TimeoutError(f"did not observe pattern: {pattern.pattern}")
|
||||
|
||||
def _read_available_text(self, timeout_seconds: float = 0.0) -> bool:
|
||||
if self.process is None:
|
||||
raise RuntimeError("runner process is not started")
|
||||
if self._master_fd is not None:
|
||||
readable, _, _ = select.select([self._master_fd], [], [], timeout_seconds)
|
||||
if not readable:
|
||||
if self.process.poll() is not None:
|
||||
raise RuntimeError("qwen oauth login process exited unexpectedly")
|
||||
return False
|
||||
chunk = os.read(self._master_fd, 4096)
|
||||
if not chunk:
|
||||
if self.process.poll() is not None:
|
||||
raise RuntimeError("qwen oauth login process exited unexpectedly")
|
||||
return False
|
||||
self._output_buffer += chunk.decode("utf-8", errors="replace")
|
||||
return True
|
||||
if self.process.stdout is None:
|
||||
raise RuntimeError("runner process is not started")
|
||||
line = self.process.stdout.readline()
|
||||
if line:
|
||||
self._output_buffer += line
|
||||
return True
|
||||
if self.process.poll() is not None:
|
||||
raise RuntimeError("qwen oauth login process exited unexpectedly")
|
||||
return False
|
||||
|
||||
|
||||
class QwenOAuthBrowserAutomator:
|
||||
def __init__(self, *, headed: bool = True) -> None:
|
||||
self.headed = headed
|
||||
|
||||
def authorize(self, authorize_url: str, email: str, password: str) -> None:
|
||||
with sync_playwright() as playwright:
|
||||
browser = self._launch_browser(playwright)
|
||||
context = browser.new_context(viewport={"width": 1440, "height": 960})
|
||||
page = context.new_page()
|
||||
page.set_default_timeout(5000)
|
||||
page.goto(authorize_url, wait_until="domcontentloaded")
|
||||
self._assert_not_invalid(page)
|
||||
self._login_if_needed(page, email, password)
|
||||
self._confirm_if_present(page)
|
||||
self._approve_if_present(page)
|
||||
self._wait_for_authorization_progress(page)
|
||||
browser.close()
|
||||
|
||||
def _launch_browser(self, playwright: Any) -> Any:
|
||||
try:
|
||||
return playwright.chromium.launch(channel="chrome", headless=not self.headed)
|
||||
except Exception:
|
||||
return playwright.chromium.launch(headless=not self.headed)
|
||||
|
||||
def _assert_not_invalid(self, page: Any) -> None:
|
||||
text = page.locator("body").inner_text()
|
||||
if "无效的 user code" in text or "认证失败" in text:
|
||||
raise RuntimeError("qwen oauth page reported invalid user code")
|
||||
|
||||
def _login_if_needed(self, page: Any, email: str, password: str) -> None:
|
||||
email_was_updated = self._fill_email(page, email)
|
||||
if email_was_updated and not self._has_visible_password_input(page):
|
||||
self._click_submit(page, [r"下一步", r"Next", r"登录", r"Sign in"])
|
||||
if self._fill_password(page, password):
|
||||
self._click_submit(page, [r"登录", r"Sign in"])
|
||||
|
||||
def _approve_if_present(self, page: Any) -> None:
|
||||
self._click_submit(page, [r"授权", r"Authorize", r"同意", r"Allow"], required=False)
|
||||
|
||||
def _confirm_if_present(self, page: Any) -> None:
|
||||
self._click_submit(page, [r"确定", r"确认", r"Confirm", r"OK", r"继续"], required=False)
|
||||
|
||||
def _wait_for_authorization_progress(self, page: Any) -> None:
|
||||
deadline = time.time() + 20
|
||||
success_markers = [
|
||||
"授权成功",
|
||||
"认证成功",
|
||||
"认证完成",
|
||||
"Authorization successful",
|
||||
"Authenticated",
|
||||
"You can close this window",
|
||||
]
|
||||
while time.time() < deadline:
|
||||
self._assert_not_invalid(page)
|
||||
self._confirm_if_present(page)
|
||||
self._approve_if_present(page)
|
||||
try:
|
||||
body_text = page.locator("body").inner_text(timeout=1000)
|
||||
except Exception:
|
||||
body_text = ""
|
||||
if any(marker in body_text for marker in success_markers):
|
||||
return
|
||||
current_url = page.url
|
||||
if "user_code=" not in current_url and "/authorize" not in current_url and "/auth?" not in current_url:
|
||||
return
|
||||
page.wait_for_timeout(1000)
|
||||
|
||||
def _fill_email(self, page: Any, email: str) -> bool:
|
||||
for selector in self._email_selectors():
|
||||
locator = page.locator(selector).first
|
||||
try:
|
||||
locator.wait_for(state="visible", timeout=3000)
|
||||
current_value = (locator.input_value() or "").strip()
|
||||
if current_value and current_value.lower() == email.lower():
|
||||
return False
|
||||
self._set_input_value(locator, email)
|
||||
return True
|
||||
except PlaywrightTimeoutError:
|
||||
continue
|
||||
except Exception:
|
||||
continue
|
||||
return False
|
||||
|
||||
def _fill_password(self, page: Any, password: str) -> bool:
|
||||
deadline = time.time() + 15
|
||||
while time.time() < deadline:
|
||||
if self._fill_first(page, self._password_selectors(), password, wait_timeout=3000):
|
||||
return True
|
||||
page.wait_for_timeout(500)
|
||||
try:
|
||||
page.screenshot(path=str(Path(tempfile.gettempdir()) / "qwen-oauth-password-timeout.png"), full_page=True)
|
||||
except Exception:
|
||||
pass
|
||||
raise RuntimeError("could not fill qwen password input")
|
||||
|
||||
def _has_visible_password_input(self, page: Any) -> bool:
|
||||
for selector in self._password_selectors():
|
||||
locator = page.locator(selector).first
|
||||
try:
|
||||
locator.wait_for(state="visible", timeout=500)
|
||||
return True
|
||||
except PlaywrightTimeoutError:
|
||||
continue
|
||||
except Exception:
|
||||
continue
|
||||
return False
|
||||
|
||||
def _fill_first(self, page: Any, selectors: list[str], value: str, *, wait_timeout: int = 2000) -> bool:
|
||||
for selector in selectors:
|
||||
locator = page.locator(selector).first
|
||||
try:
|
||||
locator.wait_for(state="visible", timeout=wait_timeout)
|
||||
self._set_input_value(locator, value)
|
||||
return True
|
||||
except PlaywrightTimeoutError:
|
||||
continue
|
||||
except Exception:
|
||||
continue
|
||||
return False
|
||||
|
||||
def _set_input_value(self, locator: Any, value: str) -> None:
|
||||
locator.click()
|
||||
locator.evaluate(
|
||||
"""(element, nextValue) => {
|
||||
const prototype = Object.getPrototypeOf(element);
|
||||
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value');
|
||||
if (descriptor && descriptor.set) {
|
||||
descriptor.set.call(element, nextValue);
|
||||
} else {
|
||||
element.value = nextValue;
|
||||
}
|
||||
element.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
element.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}""",
|
||||
value,
|
||||
)
|
||||
actual_value = locator.input_value()
|
||||
if actual_value == value:
|
||||
return
|
||||
locator.fill(value)
|
||||
actual_value = locator.input_value()
|
||||
if actual_value == value:
|
||||
return
|
||||
locator.press("Meta+A")
|
||||
locator.type(value, delay=30)
|
||||
actual_value = locator.input_value()
|
||||
if actual_value != value:
|
||||
raise RuntimeError("failed to set input value")
|
||||
|
||||
def _click_submit(self, page: Any, names: list[str], *, required: bool = True) -> bool:
|
||||
for selector in ["button[type='submit']", "input[type='submit']"]:
|
||||
locator = page.locator(selector)
|
||||
count = locator.count()
|
||||
for index in range(count):
|
||||
candidate = locator.nth(index)
|
||||
try:
|
||||
candidate.wait_for(state="visible", timeout=1000)
|
||||
label = (candidate.inner_text() or "").strip()
|
||||
if self._is_forbidden_button_text(label):
|
||||
continue
|
||||
candidate.click()
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
for role in ["button", "link"]:
|
||||
for name in names:
|
||||
locator = page.get_by_role(role, name=re.compile(name, re.IGNORECASE))
|
||||
count = locator.count()
|
||||
for index in range(count):
|
||||
candidate = locator.nth(index)
|
||||
try:
|
||||
candidate.wait_for(state="visible", timeout=1000)
|
||||
label = (candidate.inner_text() or "").strip()
|
||||
if self._is_forbidden_button_text(label):
|
||||
continue
|
||||
candidate.click()
|
||||
return True
|
||||
except PlaywrightTimeoutError:
|
||||
continue
|
||||
if required:
|
||||
raise RuntimeError(f"could not find clickable element for: {names}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _is_forbidden_button_text(text: str) -> bool:
|
||||
lower = text.lower()
|
||||
forbidden = ["google", "github", "apple", "microsoft", "wechat", "扫码", "qr", "二维码"]
|
||||
return any(item in lower for item in forbidden)
|
||||
|
||||
@staticmethod
|
||||
def _email_selectors() -> list[str]:
|
||||
return [
|
||||
"input[type='email']",
|
||||
"input[name='email']",
|
||||
"input[placeholder*='邮箱']",
|
||||
"input[placeholder*='Email']",
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _password_selectors() -> list[str]:
|
||||
return [
|
||||
"input[type='password']",
|
||||
"input[name='password']",
|
||||
"input[placeholder*='密码']",
|
||||
"input[placeholder*='Password']",
|
||||
]
|
||||
|
||||
|
||||
def provision_qwen_oauth_credentials(
|
||||
*,
|
||||
email: str,
|
||||
password: str,
|
||||
output_dir: Path,
|
||||
headed: bool = True,
|
||||
runner: QwenOAuthLoginRunner | None = None,
|
||||
browser_automator: QwenOAuthBrowserAutomator | None = None,
|
||||
work_dir: Path | None = None,
|
||||
log_fn: Callable[[str], None] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
temp_dir: tempfile.TemporaryDirectory[str] | None = None
|
||||
if runner is None:
|
||||
if work_dir is None:
|
||||
temp_dir = tempfile.TemporaryDirectory(prefix="qwen-oauth-")
|
||||
work_dir = Path(temp_dir.name)
|
||||
config_path, auth_dir = write_qwen_login_config(work_dir)
|
||||
runner = QwenOAuthLoginRunner(config_path=config_path, auth_dir=auth_dir)
|
||||
if browser_automator is None:
|
||||
browser_automator = QwenOAuthBrowserAutomator(headed=headed)
|
||||
|
||||
try:
|
||||
if log_fn is not None:
|
||||
log_fn("启动 CLIProxyAPI Qwen OAuth")
|
||||
runner.start()
|
||||
authorize_url = runner.wait_for_authorize_url()
|
||||
if log_fn is not None:
|
||||
log_fn("获取授权链接成功")
|
||||
browser_automator.authorize(authorize_url, email, password)
|
||||
if log_fn is not None:
|
||||
log_fn("浏览器登录完成,等待 CLI 输入邮箱")
|
||||
runner.wait_for_identity_prompt()
|
||||
runner.submit_identity(email)
|
||||
if log_fn is not None:
|
||||
log_fn("已提交邮箱 alias,等待凭证文件")
|
||||
official_file = runner.wait_for_credentials()
|
||||
official_payload = wait_for_json_file(official_file)
|
||||
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
destination = output_dir / f"qwen_oauth_{safe_email_name(email)}.json"
|
||||
shutil.copy2(official_file, destination)
|
||||
if log_fn is not None:
|
||||
log_fn(f"OAuth 凭证已保存: {destination.name}")
|
||||
return {
|
||||
"status": "success",
|
||||
"authorize_url": authorize_url,
|
||||
"oauth_file": str(destination),
|
||||
"oauth_payload": official_payload,
|
||||
}
|
||||
finally:
|
||||
runner.close()
|
||||
if temp_dir is not None:
|
||||
temp_dir.cleanup()
|
||||
|
||||
|
||||
def wait_for_json_file(path: Path, timeout_seconds: float = 20.0) -> dict[str, Any]:
|
||||
deadline = time.time() + timeout_seconds
|
||||
while time.time() < deadline:
|
||||
if not path.exists():
|
||||
time.sleep(0.2)
|
||||
continue
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8").strip()
|
||||
except OSError:
|
||||
time.sleep(0.2)
|
||||
continue
|
||||
if not text:
|
||||
time.sleep(0.2)
|
||||
continue
|
||||
try:
|
||||
payload = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
time.sleep(0.2)
|
||||
continue
|
||||
if isinstance(payload, dict):
|
||||
return payload
|
||||
time.sleep(0.2)
|
||||
raise TimeoutError(f"oauth credential file did not become valid json: {path}")
|
||||
@@ -0,0 +1,544 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
from cloudflare_temp_email_client import CloudflareTempEmailClient
|
||||
from qwen_oauth_login import provision_qwen_oauth_credentials
|
||||
from router_management_client import RouterManagementClient
|
||||
|
||||
OUT_DIR = Path(__file__).parent.resolve()
|
||||
TOKENS_DIR = OUT_DIR / "tokens"
|
||||
|
||||
COMMON_US_FIRST_NAMES = [
|
||||
"James",
|
||||
"John",
|
||||
"Robert",
|
||||
"Michael",
|
||||
"William",
|
||||
"David",
|
||||
"Richard",
|
||||
"Joseph",
|
||||
"Thomas",
|
||||
"Charles",
|
||||
"Mary",
|
||||
"Patricia",
|
||||
"Jennifer",
|
||||
"Linda",
|
||||
"Elizabeth",
|
||||
"Barbara",
|
||||
"Susan",
|
||||
"Jessica",
|
||||
"Sarah",
|
||||
"Karen",
|
||||
]
|
||||
|
||||
COMMON_US_MIDDLE_NAMES = [
|
||||
"alex",
|
||||
"jordan",
|
||||
"taylor",
|
||||
"morgan",
|
||||
"casey",
|
||||
"quinn",
|
||||
"blake",
|
||||
"reese",
|
||||
]
|
||||
|
||||
COMMON_US_LAST_NAMES = [
|
||||
"Smith",
|
||||
"Johnson",
|
||||
"Williams",
|
||||
"Brown",
|
||||
"Jones",
|
||||
"Garcia",
|
||||
"Miller",
|
||||
"Davis",
|
||||
"Rodriguez",
|
||||
"Martinez",
|
||||
"Wilson",
|
||||
"Anderson",
|
||||
"Thomas",
|
||||
"Taylor",
|
||||
"Moore",
|
||||
"Jackson",
|
||||
"Martin",
|
||||
"Lee",
|
||||
"Perez",
|
||||
"Thompson",
|
||||
]
|
||||
|
||||
|
||||
def log_message(
|
||||
message: str,
|
||||
*,
|
||||
level: str = "INFO",
|
||||
item_index: int | None = None,
|
||||
total_count: int | None = None,
|
||||
) -> None:
|
||||
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
scope = (
|
||||
f" [{item_index}/{total_count}]"
|
||||
if item_index is not None and total_count is not None
|
||||
else ""
|
||||
)
|
||||
print(f"{timestamp} [{level}]{scope} {message}", file=sys.stderr)
|
||||
|
||||
|
||||
def load_dotenv_file(path: Path) -> None:
|
||||
if not path.exists():
|
||||
return
|
||||
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
if not key or key in os.environ:
|
||||
continue
|
||||
if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
|
||||
value = value[1:-1]
|
||||
os.environ[key] = value
|
||||
|
||||
|
||||
def sha256_hex(value: str) -> str:
|
||||
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def build_signup_payload(
|
||||
*, name: str, email: str, password: str, module: str = "chat"
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"name": name,
|
||||
"email": email,
|
||||
"password": sha256_hex(password),
|
||||
"agree": True,
|
||||
"profile_image_url": "",
|
||||
"oauth_sub": "",
|
||||
"oauth_token": "",
|
||||
"module": module,
|
||||
}
|
||||
|
||||
|
||||
def build_signin_payload(*, email: str, password: str) -> dict[str, Any]:
|
||||
return {
|
||||
"email": email,
|
||||
"password": sha256_hex(password),
|
||||
}
|
||||
|
||||
|
||||
def extract_token_payload(data: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"access_token": (data.get("token") or "").strip(),
|
||||
"email": (data.get("email") or "").strip(),
|
||||
"user_id": (data.get("id") or "").strip(),
|
||||
"role": (data.get("role") or "").strip(),
|
||||
"token_type": (data.get("token_type") or "Bearer").strip(),
|
||||
"expired": data.get("expires_at"),
|
||||
"type": "qwen",
|
||||
"raw": data,
|
||||
}
|
||||
|
||||
|
||||
def write_token_artifacts(
|
||||
*, out_dir: Path, email: str, password: str, token_payload: dict[str, Any]
|
||||
) -> dict[str, str]:
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
safe_email = email.replace("@", "_at_").replace(".", "_")
|
||||
timestamp = int(time.time())
|
||||
token_file = out_dir / f"token_{safe_email}_{timestamp}.json"
|
||||
accounts_file = out_dir / "accounts.txt"
|
||||
token_file.write_text(
|
||||
json.dumps(token_payload, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
with accounts_file.open("a", encoding="utf-8") as handle:
|
||||
handle.write(f"{email}----{password}\n")
|
||||
return {"token_file": str(token_file), "accounts_file": str(accounts_file)}
|
||||
|
||||
|
||||
def write_oauth_artifact(*, out_dir: Path, email: str, source_file: Path) -> str:
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
destination = (
|
||||
out_dir / f"qwen_oauth_{email.replace('@', '_at_').replace('.', '_')}.json"
|
||||
)
|
||||
shutil.copy2(source_file, destination)
|
||||
return str(destination)
|
||||
|
||||
|
||||
class QwenClient:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
base_url: str = "https://chat.qwen.ai",
|
||||
session: requests.Session | None = None,
|
||||
timeout: float = 20.0,
|
||||
) -> None:
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.session = session or requests.Session()
|
||||
self.timeout = timeout
|
||||
self.session.headers.update(
|
||||
{
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
)
|
||||
|
||||
def signup(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
response = self.session.post(
|
||||
f"{self.base_url}/api/v1/auths/signup",
|
||||
json=payload,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def signin(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
response = self.session.post(
|
||||
f"{self.base_url}/api/v2/auths/signin",
|
||||
json=payload,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def activate(self, url: str) -> dict[str, Any]:
|
||||
response = self.session.get(
|
||||
url,
|
||||
timeout=self.timeout,
|
||||
allow_redirects=True,
|
||||
)
|
||||
response.raise_for_status()
|
||||
try:
|
||||
data = response.json()
|
||||
if isinstance(data, dict):
|
||||
return data
|
||||
except ValueError:
|
||||
pass
|
||||
return {"status": "ok", "url": url}
|
||||
|
||||
|
||||
def generate_profile() -> dict[str, str]:
|
||||
first = random.choice(COMMON_US_FIRST_NAMES)
|
||||
last = random.choice(COMMON_US_LAST_NAMES)
|
||||
middle = random.choice(COMMON_US_MIDDLE_NAMES)
|
||||
return {
|
||||
"name": f"{first} {last}",
|
||||
"email_local": f"{first.lower()}.{middle}.{last.lower()}",
|
||||
}
|
||||
|
||||
|
||||
def random_password(length: int = 14) -> str:
|
||||
import string
|
||||
|
||||
chars = string.ascii_letters + string.digits + "!@#$%^&*"
|
||||
password = "".join(random.choice(chars) for _ in range(length))
|
||||
if not any(ch.isalpha() for ch in password):
|
||||
password = "Aa1!" + password[4:]
|
||||
if not any(ch.isdigit() for ch in password):
|
||||
password = "A1a!" + password[4:]
|
||||
return password
|
||||
|
||||
|
||||
def parse_admin_passwords(value: str) -> list[str]:
|
||||
raw = (value or "").strip()
|
||||
if not raw:
|
||||
return []
|
||||
if raw.startswith("["):
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
parsed = []
|
||||
return [str(item).strip() for item in parsed if str(item).strip()]
|
||||
return [item.strip() for item in raw.split(",") if item.strip()]
|
||||
|
||||
|
||||
def register_once(
|
||||
*,
|
||||
email_client: CloudflareTempEmailClient,
|
||||
qwen_client: QwenClient,
|
||||
out_dir: Path = TOKENS_DIR,
|
||||
upload_client: RouterManagementClient | None = None,
|
||||
oauth_provisioner: Any = provision_qwen_oauth_credentials,
|
||||
oauth_headed: bool = True,
|
||||
log_fn: Any = log_message,
|
||||
item_index: int | None = None,
|
||||
total_count: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
profile = generate_profile()
|
||||
log_fn("初始化注册任务", item_index=item_index, total_count=total_count)
|
||||
mailbox = email_client.create_address(name=profile["email_local"])
|
||||
email = mailbox["address"]
|
||||
password = random_password()
|
||||
name = profile["name"]
|
||||
log_fn(f"创建临时邮箱: {email}", item_index=item_index, total_count=total_count)
|
||||
|
||||
log_fn("提交 Qwen 注册", item_index=item_index, total_count=total_count)
|
||||
signup_data = qwen_client.signup(
|
||||
build_signup_payload(name=name, email=email, password=password)
|
||||
)
|
||||
token_payload = extract_token_payload(signup_data)
|
||||
activation_result: dict[str, Any]
|
||||
try:
|
||||
log_fn("等待激活邮件", item_index=item_index, total_count=total_count)
|
||||
verification = email_client.wait_for_verification_link(
|
||||
mailbox["jwt"],
|
||||
timeout_seconds=120.0,
|
||||
poll_interval_seconds=5.0,
|
||||
)
|
||||
activate_response = qwen_client.activate(verification["link"])
|
||||
signin_data = qwen_client.signin(
|
||||
build_signin_payload(email=email, password=password)
|
||||
)
|
||||
refreshed_token_payload = extract_token_payload(
|
||||
signin_data.get("data") or signin_data
|
||||
)
|
||||
if refreshed_token_payload["access_token"]:
|
||||
token_payload = refreshed_token_payload
|
||||
log_fn("激活成功", item_index=item_index, total_count=total_count)
|
||||
activation_result = {
|
||||
"status": "success",
|
||||
"mail_id": verification["mail_id"],
|
||||
"link": verification["link"],
|
||||
"response": activate_response,
|
||||
}
|
||||
except TimeoutError as exc:
|
||||
log_fn(
|
||||
f"激活超时: {exc}",
|
||||
level="WARN",
|
||||
item_index=item_index,
|
||||
total_count=total_count,
|
||||
)
|
||||
activation_result = {
|
||||
"status": "timeout",
|
||||
"error": str(exc),
|
||||
}
|
||||
except Exception as exc:
|
||||
log_fn(
|
||||
f"激活失败: {exc}",
|
||||
level="ERROR",
|
||||
item_index=item_index,
|
||||
total_count=total_count,
|
||||
)
|
||||
activation_result = {
|
||||
"status": "failed",
|
||||
"error": str(exc),
|
||||
}
|
||||
if not token_payload["access_token"]:
|
||||
log_fn("重新登录获取 token", item_index=item_index, total_count=total_count)
|
||||
signin_data = qwen_client.signin(
|
||||
build_signin_payload(email=email, password=password)
|
||||
)
|
||||
token_payload = extract_token_payload(signin_data.get("data") or signin_data)
|
||||
if not token_payload["access_token"]:
|
||||
raise RuntimeError(f"Qwen did not return a token payload: {signup_data}")
|
||||
|
||||
log_fn("保存本地认证文件", item_index=item_index, total_count=total_count)
|
||||
artifact_paths = write_token_artifacts(
|
||||
out_dir=out_dir,
|
||||
email=email,
|
||||
password=password,
|
||||
token_payload=token_payload,
|
||||
)
|
||||
oauth_result: dict[str, Any]
|
||||
upload_source = Path(artifact_paths["token_file"])
|
||||
try:
|
||||
log_fn("启动 Qwen OAuth", item_index=item_index, total_count=total_count)
|
||||
oauth_data = oauth_provisioner(
|
||||
email=email,
|
||||
password=password,
|
||||
output_dir=out_dir,
|
||||
headed=oauth_headed,
|
||||
log_fn=lambda message, level="INFO": log_fn(
|
||||
message, level=level, item_index=item_index, total_count=total_count
|
||||
),
|
||||
)
|
||||
oauth_result = oauth_data
|
||||
if oauth_data.get("status") == "success" and oauth_data.get("oauth_file"):
|
||||
upload_source = Path(str(oauth_data["oauth_file"]))
|
||||
log_fn("Qwen OAuth 成功", item_index=item_index, total_count=total_count)
|
||||
except Exception as exc:
|
||||
oauth_result = {
|
||||
"status": "failed",
|
||||
"error": str(exc),
|
||||
}
|
||||
log_fn(
|
||||
f"Qwen OAuth 失败: {exc}",
|
||||
level="ERROR",
|
||||
item_index=item_index,
|
||||
total_count=total_count,
|
||||
)
|
||||
print(f"[qwen-register] qwen oauth automation failed: {exc}", file=sys.stderr)
|
||||
upload_result: dict[str, Any] = {"status": "skipped"}
|
||||
if upload_client is not None:
|
||||
try:
|
||||
log_fn(
|
||||
f"上传认证文件: {upload_source.name}",
|
||||
item_index=item_index,
|
||||
total_count=total_count,
|
||||
)
|
||||
upload_response = upload_client.upload_auth_file(upload_source)
|
||||
upload_result = {
|
||||
"status": "success",
|
||||
"response": upload_response,
|
||||
"source_file": str(upload_source),
|
||||
}
|
||||
log_fn("上传成功", item_index=item_index, total_count=total_count)
|
||||
except Exception as exc:
|
||||
upload_result = {
|
||||
"status": "failed",
|
||||
"error": str(exc),
|
||||
"source_file": str(upload_source),
|
||||
}
|
||||
log_fn(
|
||||
f"上传失败: {exc}",
|
||||
level="ERROR",
|
||||
item_index=item_index,
|
||||
total_count=total_count,
|
||||
)
|
||||
print(f"[qwen-register] auth upload failed: {exc}", file=sys.stderr)
|
||||
log_fn("完成", item_index=item_index, total_count=total_count)
|
||||
return {
|
||||
"email": email,
|
||||
"password": password,
|
||||
"activation": activation_result,
|
||||
"oauth": oauth_result,
|
||||
"token_payload": token_payload,
|
||||
"upload": upload_result,
|
||||
**artifact_paths,
|
||||
}
|
||||
|
||||
|
||||
def run_registration_batch(
|
||||
*,
|
||||
count: int,
|
||||
register_fn: Any,
|
||||
log_fn: Any = log_message,
|
||||
) -> dict[str, Any]:
|
||||
if count <= 0:
|
||||
raise ValueError("count must be greater than 0")
|
||||
results: list[dict[str, Any]] = []
|
||||
log_fn(f"开始批量注册,总数: {count}")
|
||||
for index in range(count):
|
||||
try:
|
||||
log_fn("开始当前注册", item_index=index + 1, total_count=count)
|
||||
results.append(register_fn(index + 1, count))
|
||||
except Exception as exc:
|
||||
failure = {
|
||||
"status": "failed",
|
||||
"error": str(exc),
|
||||
}
|
||||
results.append(failure)
|
||||
log_fn(
|
||||
f"当前注册失败: {exc}",
|
||||
level="ERROR",
|
||||
item_index=index + 1,
|
||||
total_count=count,
|
||||
)
|
||||
print(
|
||||
f"[qwen-register] batch item {index + 1}/{count} failed: {exc}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if index < count - 1:
|
||||
delay_seconds = random.randint(10, 30)
|
||||
log_fn(
|
||||
f"等待下一次注册: {delay_seconds}s",
|
||||
item_index=index + 1,
|
||||
total_count=count,
|
||||
)
|
||||
time.sleep(delay_seconds)
|
||||
failed_count = sum(1 for item in results if item.get("status") == "failed")
|
||||
summary = {
|
||||
"count": count,
|
||||
"success_count": count - failed_count,
|
||||
"failed_count": failed_count,
|
||||
"results": results,
|
||||
}
|
||||
log_fn(
|
||||
f"批量注册完成,成功: {summary['success_count']},失败: {summary['failed_count']}"
|
||||
)
|
||||
return summary
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description="Qwen register via HTTP")
|
||||
parser.add_argument("--once", action="store_true", help="run once and exit")
|
||||
parser.add_argument(
|
||||
"--mail-base-url",
|
||||
default=os.environ.get(
|
||||
"CLOUDFLARE_TEMP_EMAIL_BASE_URL", "https://example.com/"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--admin-passwords", default=os.environ.get("ADMIN_PASSWORDS", "")
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cli-proxy-api-base-url", default=os.environ.get("CLI_PROXY_API_BASE_URL", "")
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cli-proxy-api-key", default=os.environ.get("CLI_PROXY_API_KEY", "")
|
||||
)
|
||||
parser.add_argument(
|
||||
"--oauth-headed",
|
||||
action="store_true",
|
||||
default=os.environ.get("QWEN_OAUTH_HEADED", "1") != "0",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--count",
|
||||
type=int,
|
||||
default=int(os.environ.get("QWEN_REGISTER_COUNT", "5") or "5"),
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
load_dotenv_file(OUT_DIR / ".env")
|
||||
args = build_parser().parse_args()
|
||||
admin_passwords = parse_admin_passwords(args.admin_passwords)
|
||||
if not admin_passwords:
|
||||
raise SystemExit("ADMIN_PASSWORDS is required")
|
||||
|
||||
email_client = CloudflareTempEmailClient(
|
||||
base_url=args.mail_base_url,
|
||||
admin_passwords=admin_passwords,
|
||||
)
|
||||
qwen_client = QwenClient()
|
||||
upload_client = None
|
||||
if args.cli_proxy_api_base_url and args.cli_proxy_api_key:
|
||||
upload_client = RouterManagementClient(
|
||||
base_url=args.cli_proxy_api_base_url,
|
||||
api_key=args.cli_proxy_api_key,
|
||||
)
|
||||
batch_output = run_registration_batch(
|
||||
count=args.count,
|
||||
register_fn=lambda item_index, total_count: register_once(
|
||||
email_client=email_client,
|
||||
qwen_client=qwen_client,
|
||||
upload_client=upload_client,
|
||||
oauth_headed=args.oauth_headed,
|
||||
item_index=item_index,
|
||||
total_count=total_count,
|
||||
),
|
||||
log_fn=log_message,
|
||||
)
|
||||
output: dict[str, Any] | list[dict[str, Any]]
|
||||
if args.count == 1:
|
||||
output = batch_output["results"][0]
|
||||
else:
|
||||
output = batch_output
|
||||
# print(json.dumps(output, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,39 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class RouterManagementClient:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
base_url: str,
|
||||
api_key: str,
|
||||
session: requests.Session | None = None,
|
||||
timeout: float = 20.0,
|
||||
) -> None:
|
||||
if not base_url:
|
||||
raise ValueError("base_url is required")
|
||||
if not api_key:
|
||||
raise ValueError("api_key is required")
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.api_key = api_key
|
||||
self.session = session or requests.Session()
|
||||
self.timeout = timeout
|
||||
|
||||
def upload_auth_file(self, file_path: Path) -> dict[str, Any]:
|
||||
with file_path.open("rb") as handle:
|
||||
response = self.session.post(
|
||||
f"{self.base_url}/v0/management/auth-files",
|
||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||
files={"file": (file_path.name, handle, "application/json")},
|
||||
timeout=self.timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if not isinstance(data, dict):
|
||||
raise RuntimeError("malformed upload response")
|
||||
return data
|
||||
Reference in New Issue
Block a user