Initialize project and mask sensitive information

This commit is contained in:
qinzhaoxuan
2026-03-13 20:55:19 +08:00
parent 5b2ccb3ac0
commit 8106f503ba
7 changed files with 1275 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
CLOUDFLARE_TEMP_EMAIL_BASE_URL="https://example.com/"
ADMIN_PASSWORDS='["***","***"]'
CLI_PROXY_API_BASE_URL=
CLI_PROXY_API_KEY=
+5
View File
@@ -0,0 +1,5 @@
.env
venv/
tokens/
__pycache__/
*.pyc
+53
View File
@@ -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
```
+149
View File
@@ -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("&amp;", "&").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
+481
View File
@@ -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}")
+544
View File
@@ -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())
+39
View File
@@ -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