272 行
9.2 KiB
Python
可执行文件
272 行
9.2 KiB
Python
可执行文件
#!/usr/bin/env python3
|
|
"""Unified CLI for 2Captcha, YesCaptcha, and Anti-Captcha API methods."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
from pathlib import Path
|
|
from typing import Any, Dict
|
|
|
|
|
|
PROVIDERS = {
|
|
"2captcha": {
|
|
"endpoint": "https://api.2captcha.com",
|
|
"env_var": "CAPTCHA_2CAPTCHA_KEY",
|
|
},
|
|
"yescaptcha": {
|
|
"endpoint": "https://api.yescaptcha.com",
|
|
"env_var": "CAPTCHA_YESCAPTCHA_KEY",
|
|
},
|
|
"anti-captcha": {
|
|
"endpoint": "https://api.anti-captcha.com",
|
|
"env_var": "CAPTCHA_ANTI_CAPTCHA_KEY",
|
|
},
|
|
}
|
|
|
|
PROVIDER_ALIASES = {
|
|
"2captcha": "2captcha",
|
|
"2-captcha": "2captcha",
|
|
"yescaptcha": "yescaptcha",
|
|
"yes-captcha": "yescaptcha",
|
|
"anticaptcha": "anti-captcha",
|
|
"anti-captcha": "anti-captcha",
|
|
}
|
|
|
|
|
|
class CliError(Exception):
|
|
"""Raised for expected CLI errors."""
|
|
|
|
|
|
def normalize_provider(provider: str) -> str:
|
|
key = provider.strip().lower()
|
|
normalized = PROVIDER_ALIASES.get(key)
|
|
if not normalized:
|
|
supported = ", ".join(sorted(PROVIDERS))
|
|
raise CliError(f"Unsupported provider '{provider}'. Supported: {supported}")
|
|
return normalized
|
|
|
|
|
|
def resolve_api_key(provider: str, explicit_key: str | None) -> str:
|
|
if explicit_key:
|
|
return explicit_key
|
|
env_var = PROVIDERS[provider]["env_var"]
|
|
value = os.getenv(env_var, "").strip()
|
|
if value:
|
|
return value
|
|
raise CliError(
|
|
f"Missing API key for provider '{provider}'. "
|
|
f"Set --api-key or environment variable {env_var}."
|
|
)
|
|
|
|
|
|
def call_api(provider: str, method: str, payload: Dict[str, Any], timeout: int = 60) -> Dict[str, Any]:
|
|
base = PROVIDERS[provider]["endpoint"]
|
|
url = f"{base}/{method}"
|
|
body = json.dumps(payload).encode("utf-8")
|
|
request = urllib.request.Request(
|
|
url=url,
|
|
data=body,
|
|
headers={"Content-Type": "application/json"},
|
|
method="POST",
|
|
)
|
|
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
raw = response.read().decode("utf-8")
|
|
except urllib.error.HTTPError as exc:
|
|
detail = exc.read().decode("utf-8", errors="replace")
|
|
raise CliError(f"HTTP {exc.code} from {url}: {detail}") from exc
|
|
except urllib.error.URLError as exc:
|
|
raise CliError(f"Network error calling {url}: {exc}") from exc
|
|
|
|
try:
|
|
data = json.loads(raw)
|
|
except json.JSONDecodeError as exc:
|
|
raise CliError(f"Invalid JSON response from {url}: {raw}") from exc
|
|
|
|
return data
|
|
|
|
|
|
def ensure_api_success(response: Dict[str, Any]) -> None:
|
|
error_id = response.get("errorId")
|
|
if error_id in (0, "0", None):
|
|
return
|
|
|
|
code = response.get("errorCode", "UNKNOWN")
|
|
desc = response.get("errorDescription", "")
|
|
message = f"Provider API error (errorId={error_id}, errorCode={code})"
|
|
if desc:
|
|
message = f"{message}: {desc}"
|
|
raise CliError(message)
|
|
|
|
|
|
def load_task_payload(task_json: str | None, task_file: str | None) -> Dict[str, Any]:
|
|
if not task_json and not task_file:
|
|
raise CliError("Provide either --task-json or --task-file.")
|
|
|
|
if task_json and task_file:
|
|
raise CliError("Use only one of --task-json or --task-file.")
|
|
|
|
if task_json:
|
|
source = task_json
|
|
else:
|
|
path = Path(task_file or "")
|
|
if not path.exists():
|
|
raise CliError(f"Task file does not exist: {path}")
|
|
source = path.read_text()
|
|
|
|
try:
|
|
task = json.loads(source)
|
|
except json.JSONDecodeError as exc:
|
|
raise CliError(f"Invalid task JSON: {exc}") from exc
|
|
|
|
if not isinstance(task, dict):
|
|
raise CliError("Task JSON must be an object.")
|
|
if "type" not in task:
|
|
raise CliError("Task JSON must include a 'type' field.")
|
|
return task
|
|
|
|
|
|
def print_json(data: Dict[str, Any]) -> None:
|
|
print(json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True))
|
|
|
|
|
|
def cmd_balance(args: argparse.Namespace) -> int:
|
|
provider = normalize_provider(args.provider)
|
|
key = resolve_api_key(provider, args.api_key)
|
|
payload = {"clientKey": key}
|
|
result = call_api(provider, "getBalance", payload)
|
|
ensure_api_success(result)
|
|
print_json({"provider": provider, "result": result})
|
|
return 0
|
|
|
|
|
|
def cmd_create_task(args: argparse.Namespace) -> int:
|
|
provider = normalize_provider(args.provider)
|
|
key = resolve_api_key(provider, args.api_key)
|
|
task = load_task_payload(args.task_json, args.task_file)
|
|
|
|
payload: Dict[str, Any] = {"clientKey": key, "task": task}
|
|
if args.soft_id is not None:
|
|
payload["softId"] = args.soft_id
|
|
|
|
result = call_api(provider, "createTask", payload)
|
|
ensure_api_success(result)
|
|
print_json({"provider": provider, "result": result})
|
|
return 0
|
|
|
|
|
|
def cmd_get_task_result(args: argparse.Namespace) -> int:
|
|
provider = normalize_provider(args.provider)
|
|
key = resolve_api_key(provider, args.api_key)
|
|
payload = {"clientKey": key, "taskId": args.task_id}
|
|
|
|
result = call_api(provider, "getTaskResult", payload)
|
|
ensure_api_success(result)
|
|
print_json({"provider": provider, "result": result})
|
|
return 0
|
|
|
|
|
|
def cmd_solve(args: argparse.Namespace) -> int:
|
|
provider = normalize_provider(args.provider)
|
|
key = resolve_api_key(provider, args.api_key)
|
|
task = load_task_payload(args.task_json, args.task_file)
|
|
|
|
create_payload: Dict[str, Any] = {"clientKey": key, "task": task}
|
|
if args.soft_id is not None:
|
|
create_payload["softId"] = args.soft_id
|
|
|
|
create_result = call_api(provider, "createTask", create_payload)
|
|
ensure_api_success(create_result)
|
|
|
|
task_id = create_result.get("taskId")
|
|
if not task_id:
|
|
raise CliError(f"createTask succeeded but taskId missing: {create_result}")
|
|
|
|
deadline = time.time() + args.timeout
|
|
|
|
while time.time() < deadline:
|
|
poll_payload = {"clientKey": key, "taskId": task_id}
|
|
poll_result = call_api(provider, "getTaskResult", poll_payload)
|
|
ensure_api_success(poll_result)
|
|
|
|
status = poll_result.get("status")
|
|
if status == "ready":
|
|
print_json(
|
|
{
|
|
"provider": provider,
|
|
"taskId": task_id,
|
|
"createTask": create_result,
|
|
"getTaskResult": poll_result,
|
|
}
|
|
)
|
|
return 0
|
|
|
|
if status != "processing":
|
|
raise CliError(f"Unexpected getTaskResult status '{status}': {poll_result}")
|
|
|
|
time.sleep(args.poll_interval)
|
|
|
|
raise CliError(
|
|
f"Timeout waiting for task {task_id} after {args.timeout} seconds "
|
|
f"(poll interval {args.poll_interval}s)."
|
|
)
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(
|
|
description="Unified captcha API CLI for 2Captcha, YesCaptcha, Anti-Captcha."
|
|
)
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
|
|
p_balance = subparsers.add_parser("balance", help="Call getBalance")
|
|
p_balance.add_argument("--provider", required=True, help="2captcha|yescaptcha|anti-captcha")
|
|
p_balance.add_argument("--api-key", help="Provider API key (optional if env var is set)")
|
|
p_balance.set_defaults(func=cmd_balance)
|
|
|
|
p_create = subparsers.add_parser("create-task", help="Call createTask")
|
|
p_create.add_argument("--provider", required=True, help="2captcha|yescaptcha|anti-captcha")
|
|
p_create.add_argument("--api-key", help="Provider API key (optional if env var is set)")
|
|
p_create.add_argument("--task-json", help="Inline JSON object for 'task'")
|
|
p_create.add_argument("--task-file", help="Path to JSON file containing 'task' object")
|
|
p_create.add_argument("--soft-id", type=int, help="Optional softId value")
|
|
p_create.set_defaults(func=cmd_create_task)
|
|
|
|
p_result = subparsers.add_parser("get-task-result", help="Call getTaskResult")
|
|
p_result.add_argument("--provider", required=True, help="2captcha|yescaptcha|anti-captcha")
|
|
p_result.add_argument("--api-key", help="Provider API key (optional if env var is set)")
|
|
p_result.add_argument("--task-id", required=True, type=int, help="Task ID from createTask")
|
|
p_result.set_defaults(func=cmd_get_task_result)
|
|
|
|
p_solve = subparsers.add_parser("solve", help="Create task and poll until ready")
|
|
p_solve.add_argument("--provider", required=True, help="2captcha|yescaptcha|anti-captcha")
|
|
p_solve.add_argument("--api-key", help="Provider API key (optional if env var is set)")
|
|
p_solve.add_argument("--task-json", help="Inline JSON object for 'task'")
|
|
p_solve.add_argument("--task-file", help="Path to JSON file containing 'task' object")
|
|
p_solve.add_argument("--soft-id", type=int, help="Optional softId value")
|
|
p_solve.add_argument("--poll-interval", type=int, default=3, help="Polling interval in seconds")
|
|
p_solve.add_argument("--timeout", type=int, default=180, help="Solve timeout in seconds")
|
|
p_solve.set_defaults(func=cmd_solve)
|
|
|
|
return parser
|
|
|
|
|
|
def main() -> int:
|
|
parser = build_parser()
|
|
args = parser.parse_args()
|
|
try:
|
|
return args.func(args)
|
|
except CliError as exc:
|
|
print(f"ERROR: {exc}", file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|