mirror of
https://github.com/zc-zhangchen/any-auto-register.git
synced 2026-05-08 16:24:07 +08:00
feat: 新增 LuckMail 平台及自动分配邮箱渠道支持
- 引入 LuckMail 核心 SDK - 封装 \LuckMailMailbox\ 适配器接入注册工厂 - 支持智能匹配各类项目的子渠道编码 - 完善前端 UI 配置选项
This commit is contained in:
@@ -13,6 +13,7 @@ CONFIG_KEYS = [
|
||||
"moemail_api_url",
|
||||
"mail_provider",
|
||||
"cfworker_api_url", "cfworker_admin_token", "cfworker_domain", "cfworker_fingerprint",
|
||||
"luckmail_base_url", "luckmail_api_key", "luckmail_email_type",
|
||||
"cpa_api_url", "cpa_api_key",
|
||||
"team_manager_url", "team_manager_key",
|
||||
"cliproxyapi_management_key",
|
||||
|
||||
28
api/tasks.py
28
api/tasks.py
@@ -104,9 +104,12 @@ def _run_register(task_id: str, req: RegisterTaskRequest):
|
||||
PlatformCls = get(req.platform)
|
||||
|
||||
def _build_mailbox(proxy: Optional[str]):
|
||||
from core.config_store import config_store
|
||||
merged_extra = config_store.get_all().copy()
|
||||
merged_extra.update({k: v for k, v in req.extra.items() if v is not None and v != ""})
|
||||
return create_mailbox(
|
||||
provider=req.extra.get("mail_provider", "laoudo"),
|
||||
extra=req.extra,
|
||||
provider=merged_extra.get("mail_provider", "laoudo"),
|
||||
extra=merged_extra,
|
||||
proxy=proxy,
|
||||
)
|
||||
|
||||
@@ -126,11 +129,15 @@ def _run_register(task_id: str, req: RegisterTaskRequest):
|
||||
_log(task_id, f"第 {i+1} 个账号启动前延迟 {wait_seconds:g} 秒")
|
||||
time.sleep(wait_seconds)
|
||||
next_start_time = time.time() + req.register_delay_seconds
|
||||
from core.config_store import config_store
|
||||
merged_extra = config_store.get_all().copy()
|
||||
merged_extra.update({k: v for k, v in req.extra.items() if v is not None and v != ""})
|
||||
|
||||
_config = RegisterConfig(
|
||||
executor_type=req.executor_type,
|
||||
captcha_solver=req.captcha_solver,
|
||||
proxy=_proxy,
|
||||
extra=req.extra,
|
||||
extra=merged_extra,
|
||||
)
|
||||
_mailbox = _build_mailbox(_proxy)
|
||||
_platform = PlatformCls(config=_config, mailbox=_mailbox)
|
||||
@@ -197,6 +204,21 @@ def create_register_task(
|
||||
req: RegisterTaskRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
):
|
||||
mail_provider = req.extra.get("mail_provider")
|
||||
if mail_provider == "luckmail":
|
||||
platform = req.platform
|
||||
if platform in ("tavily", "openblocklabs"):
|
||||
raise HTTPException(400, f"LuckMail 渠道暂时不支持 {platform} 项目注册")
|
||||
|
||||
mapping = {
|
||||
"trae": "trae",
|
||||
"cursor": "cursor",
|
||||
"grok": "grok",
|
||||
"kiro": "kiro",
|
||||
"chatgpt": "openai"
|
||||
}
|
||||
req.extra["luckmail_project_code"] = mapping.get(platform, platform)
|
||||
|
||||
task_id = f"task_{int(time.time()*1000)}"
|
||||
with _tasks_lock:
|
||||
_tasks[task_id] = {"id": task_id, "status": "pending",
|
||||
|
||||
@@ -118,6 +118,13 @@ def create_mailbox(provider: str, extra: dict = None, proxy: str = None) -> 'Bas
|
||||
fingerprint=extra.get("cfworker_fingerprint", ""),
|
||||
proxy=proxy,
|
||||
)
|
||||
elif provider == "luckmail":
|
||||
return LuckMailMailbox(
|
||||
base_url=extra.get("luckmail_base_url") or "https://mails.luckyous.com/",
|
||||
api_key=extra.get("luckmail_api_key", ""),
|
||||
project_code=extra.get("luckmail_project_code", ""),
|
||||
email_type=extra.get("luckmail_email_type", ""),
|
||||
)
|
||||
else: # laoudo
|
||||
return LaoudoMailbox(
|
||||
auth_token=extra.get("laoudo_auth", ""),
|
||||
@@ -610,6 +617,72 @@ class MoeMailMailbox(BaseMailbox):
|
||||
raise TimeoutError(f"等待验证码超时 ({timeout}s)")
|
||||
|
||||
|
||||
class LuckMailMailbox(BaseMailbox):
|
||||
"""LuckMail 付费接码平台 - 通过 SDK 创建订单并获取验证码"""
|
||||
|
||||
def __init__(self, base_url: str, api_key: str,
|
||||
project_code: str = "", email_type: str = ""):
|
||||
if not base_url or not api_key:
|
||||
raise RuntimeError(
|
||||
"LuckMail 未配置:请在全局设置中填写 luckmail_base_url 和 luckmail_api_key"
|
||||
)
|
||||
from .luckmail import LuckMailClient
|
||||
self._client = LuckMailClient(
|
||||
base_url=base_url,
|
||||
api_key=api_key,
|
||||
)
|
||||
self._project_code = project_code
|
||||
self._email_type = email_type or None
|
||||
self._order_no = None
|
||||
|
||||
def get_email(self) -> MailboxAccount:
|
||||
try:
|
||||
body = {"project_code": self._project_code}
|
||||
if self._email_type:
|
||||
body["email_type"] = self._email_type
|
||||
order = self._client.user._sync_create_order(body)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"LuckMail 创建订单失败: {e}") from e
|
||||
self._order_no = order.order_no
|
||||
email = order.email_address
|
||||
self._log(f"[LuckMail] 订单 {order.order_no} 分配邮箱: {email}")
|
||||
self._log(f"[LuckMail] 超时时间: {order.expired_at}")
|
||||
return MailboxAccount(email=email, account_id=order.order_no)
|
||||
|
||||
def get_current_ids(self, account: MailboxAccount) -> set:
|
||||
# LuckMail 由服务端管理邮件,本地无需维护 ID 集合
|
||||
return set()
|
||||
|
||||
def wait_for_code(self, account: MailboxAccount, keyword: str = "",
|
||||
timeout: int = 120, before_ids: set = None,
|
||||
code_pattern: str = None, **kwargs) -> str:
|
||||
order_no = account.account_id or self._order_no
|
||||
if not order_no:
|
||||
raise RuntimeError("LuckMail 未创建订单,无法等待验证码")
|
||||
|
||||
def on_poll(result):
|
||||
self._log(f"[LuckMail] 轮询中... 状态: {result.status}")
|
||||
|
||||
try:
|
||||
code_result = self._client.user._sync_wait_for_code(
|
||||
order_no=order_no,
|
||||
timeout=timeout,
|
||||
interval=3.0,
|
||||
on_poll=on_poll,
|
||||
)
|
||||
except Exception as e:
|
||||
raise TimeoutError(f"LuckMail 等待验证码失败: {e}") from e
|
||||
|
||||
if code_result.status == "success" and code_result.verification_code:
|
||||
code = code_result.verification_code
|
||||
self._log(f"[LuckMail] 收到验证码: {code}")
|
||||
return code
|
||||
|
||||
raise TimeoutError(
|
||||
f"LuckMail 等待验证码超时 ({timeout}s),最终状态: {code_result.status}"
|
||||
)
|
||||
|
||||
|
||||
class FreemailMailbox(BaseMailbox):
|
||||
"""
|
||||
Freemail 自建邮箱服务(基于 Cloudflare Worker)
|
||||
|
||||
63
core/luckmail/__init__.py
Normal file
63
core/luckmail/__init__.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
LuckMailSdk - Python SDK for LuckMail Email System
|
||||
支持同步/异步双模式,智能识别调用上下文自动切换
|
||||
"""
|
||||
|
||||
from .client import LuckMailClient
|
||||
from .user import UserAPI
|
||||
from .supplier import SupplierAPI
|
||||
from .exceptions import (
|
||||
LuckMailError,
|
||||
AuthError,
|
||||
APIError,
|
||||
NetworkError,
|
||||
TimeoutError,
|
||||
)
|
||||
from .models import (
|
||||
UserInfo,
|
||||
EmailItem,
|
||||
ProjectItem,
|
||||
OrderInfo,
|
||||
OrderCode,
|
||||
PurchaseItem,
|
||||
TagItem,
|
||||
TokenCode,
|
||||
TokenAliveResult,
|
||||
TokenMailItem,
|
||||
TokenMailList,
|
||||
TokenMailDetail,
|
||||
AppealInfo,
|
||||
SupplierProfile,
|
||||
SupplierEmailItem,
|
||||
AppealItem,
|
||||
DashboardSummary,
|
||||
)
|
||||
|
||||
__version__ = "1.2.1"
|
||||
__all__ = [
|
||||
"LuckMailClient",
|
||||
"UserAPI",
|
||||
"SupplierAPI",
|
||||
"LuckMailError",
|
||||
"AuthError",
|
||||
"APIError",
|
||||
"NetworkError",
|
||||
"TimeoutError",
|
||||
"UserInfo",
|
||||
"EmailItem",
|
||||
"ProjectItem",
|
||||
"OrderInfo",
|
||||
"OrderCode",
|
||||
"PurchaseItem",
|
||||
"TagItem",
|
||||
"TokenCode",
|
||||
"TokenAliveResult",
|
||||
"TokenMailItem",
|
||||
"TokenMailList",
|
||||
"TokenMailDetail",
|
||||
"AppealInfo",
|
||||
"SupplierProfile",
|
||||
"SupplierEmailItem",
|
||||
"AppealItem",
|
||||
"DashboardSummary",
|
||||
]
|
||||
224
core/luckmail/client.py
Normal file
224
core/luckmail/client.py
Normal file
@@ -0,0 +1,224 @@
|
||||
"""
|
||||
LuckMailClient - 主客户端入口
|
||||
|
||||
整合用户端和供应商端 API,提供统一的访问入口。
|
||||
支持同步/异步双模式,智能识别调用上下文。
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from .http_client import LuckMailHttpClient
|
||||
from .user import UserAPI
|
||||
from .supplier import SupplierAPI
|
||||
|
||||
|
||||
class LuckMailClient:
|
||||
"""
|
||||
LuckMail SDK 主客户端
|
||||
|
||||
提供用户端(user)和供应商端(supplier)两套 API 访问入口。
|
||||
所有 API 方法均支持同步/异步双模式,根据调用上下文自动识别,
|
||||
无需手动区分,大幅降低接入成本。
|
||||
|
||||
Args:
|
||||
base_url: API 基础 URL,如 https://your-domain.com
|
||||
api_key: API Key(在平台「个人设置」页面生成)
|
||||
api_secret: API Secret(可选,用于 HMAC 签名验证,安全性更高)
|
||||
timeout: 请求超时时间(秒),默认 30
|
||||
use_hmac: 是否使用 HMAC 签名验证,默认 False
|
||||
|
||||
用户端示例(同步)::
|
||||
|
||||
from luckmail import LuckMailClient
|
||||
|
||||
client = LuckMailClient(
|
||||
base_url="https://your-domain.com",
|
||||
api_key="your_api_key_here"
|
||||
)
|
||||
|
||||
# 查询余额
|
||||
balance = client.user.get_balance()
|
||||
print(f"余额: {balance}")
|
||||
|
||||
# 接码(一行搞定)
|
||||
code = client.user.create_and_wait('twitter')
|
||||
print(f"验证码: {code.verification_code}")
|
||||
|
||||
用户端示例(异步)::
|
||||
|
||||
import asyncio
|
||||
from luckmail import LuckMailClient
|
||||
|
||||
client = LuckMailClient(
|
||||
base_url="https://your-domain.com",
|
||||
api_key="your_api_key_here"
|
||||
)
|
||||
|
||||
async def main():
|
||||
balance = await client.user.get_balance()
|
||||
print(f"余额: {balance}")
|
||||
|
||||
code = await client.user.create_and_wait('twitter')
|
||||
print(f"验证码: {code.verification_code}")
|
||||
|
||||
asyncio.run(main())
|
||||
|
||||
供应商端示例::
|
||||
|
||||
# 查看数据看板
|
||||
summary = client.supplier.get_dashboard()
|
||||
print(f"今日接码: {summary.today_assigned}")
|
||||
|
||||
# 处理申述
|
||||
client.supplier.reply_appeal("APL001", result=1, reply="同意退款")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str,
|
||||
api_key: str,
|
||||
api_secret: Optional[str] = None,
|
||||
timeout: float = 30.0,
|
||||
use_hmac: bool = False,
|
||||
):
|
||||
self._http = LuckMailHttpClient(
|
||||
base_url=base_url,
|
||||
api_key=api_key,
|
||||
api_secret=api_secret,
|
||||
timeout=timeout,
|
||||
use_hmac=use_hmac,
|
||||
)
|
||||
# 用户端 API
|
||||
self.user = UserAPI(self._http)
|
||||
# 供应商端 API
|
||||
self.supplier = SupplierAPI(self._http)
|
||||
|
||||
# ===== 快捷方法(用户端常用操作)=====
|
||||
|
||||
def create_and_wait(
|
||||
self,
|
||||
project_code: str,
|
||||
email_type: Optional[str] = None,
|
||||
domain: Optional[str] = None,
|
||||
specified_email: Optional[str] = None,
|
||||
variant_mode: Optional[str] = None,
|
||||
timeout: int = 300,
|
||||
interval: float = 3.0,
|
||||
on_poll=None,
|
||||
):
|
||||
"""
|
||||
创建接码订单并等待验证码(一站式方法)
|
||||
|
||||
自动创建订单并轮询等待验证码,智能识别同步/异步上下文。
|
||||
|
||||
Args:
|
||||
project_code: 项目编码,如 'twitter', 'facebook'
|
||||
email_type: 邮箱类型(可选)
|
||||
domain: 指定域名(可选)
|
||||
specified_email: 指定邮箱(可选)
|
||||
variant_mode: 谷歌变种模式(可选,仅 email_type=google_variant 时有效): dot / plus / mixed / all
|
||||
timeout: 最大等待时间(秒),默认 300
|
||||
interval: 轮询间隔(秒),默认 3.0
|
||||
on_poll: 每次轮询的回调函数(可选)
|
||||
|
||||
Returns:
|
||||
OrderCode: 验证码结果
|
||||
|
||||
同步示例::
|
||||
|
||||
result = client.create_and_wait('twitter')
|
||||
if result.status == 'success':
|
||||
print(f"✅ 验证码: {result.verification_code}")
|
||||
print(f"📧 来自: {result.mail_from}")
|
||||
else:
|
||||
print(f"❌ 接码失败: {result.status}")
|
||||
|
||||
异步示例::
|
||||
|
||||
result = await client.create_and_wait('twitter', email_type='ms_graph')
|
||||
if result.status == 'success':
|
||||
print(f"✅ 验证码: {result.verification_code}")
|
||||
|
||||
带进度回调的示例::
|
||||
|
||||
def on_poll(code_result):
|
||||
print(f"轮询中... 状态: {code_result.status}")
|
||||
|
||||
result = client.create_and_wait('twitter', on_poll=on_poll)
|
||||
"""
|
||||
from .http_client import _is_async_context
|
||||
if _is_async_context():
|
||||
return self._async_create_and_wait(
|
||||
project_code, email_type, domain, specified_email, variant_mode,
|
||||
timeout, interval, on_poll
|
||||
)
|
||||
return self._sync_create_and_wait(
|
||||
project_code, email_type, domain, specified_email, variant_mode,
|
||||
timeout, interval, on_poll
|
||||
)
|
||||
|
||||
async def _async_create_and_wait(
|
||||
self, project_code, email_type, domain, specified_email, variant_mode,
|
||||
timeout, interval, on_poll
|
||||
):
|
||||
"""异步创建并等待验证码"""
|
||||
body = {"project_code": project_code}
|
||||
if email_type:
|
||||
body["email_type"] = email_type
|
||||
if domain:
|
||||
body["domain"] = domain
|
||||
if specified_email:
|
||||
body["specified_email"] = specified_email
|
||||
if variant_mode:
|
||||
body["variant_mode"] = variant_mode
|
||||
|
||||
order = await self.user._async_create_order(body)
|
||||
return await self.user._async_wait_for_code(
|
||||
order.order_no, timeout, interval, on_poll
|
||||
)
|
||||
|
||||
def _sync_create_and_wait(
|
||||
self, project_code, email_type, domain, specified_email, variant_mode,
|
||||
timeout, interval, on_poll
|
||||
):
|
||||
"""同步创建并等待验证码"""
|
||||
body = {"project_code": project_code}
|
||||
if email_type:
|
||||
body["email_type"] = email_type
|
||||
if domain:
|
||||
body["domain"] = domain
|
||||
if specified_email:
|
||||
body["specified_email"] = specified_email
|
||||
if variant_mode:
|
||||
body["variant_mode"] = variant_mode
|
||||
|
||||
order = self.user._sync_create_order(body)
|
||||
return self.user._sync_wait_for_code(
|
||||
order.order_no, timeout, interval, on_poll
|
||||
)
|
||||
|
||||
def close(self):
|
||||
"""关闭客户端(同步)"""
|
||||
self._http.close()
|
||||
|
||||
async def aclose(self):
|
||||
"""关闭客户端(异步)"""
|
||||
await self._http.aclose()
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
await self.aclose()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"LuckMailClient(base_url={self._http.base_url!r}, "
|
||||
f"api_key={self._http.api_key[:8]}...)"
|
||||
)
|
||||
35
core/luckmail/exceptions.py
Normal file
35
core/luckmail/exceptions.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
LuckMailSdk 异常类定义
|
||||
"""
|
||||
|
||||
|
||||
class LuckMailError(Exception):
|
||||
"""LuckMail SDK 基础异常"""
|
||||
pass
|
||||
|
||||
|
||||
class AuthError(LuckMailError):
|
||||
"""鉴权失败异常"""
|
||||
def __init__(self, message: str = "Authentication failed"):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class APIError(LuckMailError):
|
||||
"""API 调用异常"""
|
||||
def __init__(self, code: int, message: str, data=None):
|
||||
self.code = code
|
||||
self.message = message
|
||||
self.data = data
|
||||
super().__init__(f"API Error [{code}]: {message}")
|
||||
|
||||
|
||||
class NetworkError(LuckMailError):
|
||||
"""网络请求异常"""
|
||||
def __init__(self, message: str = "Network error occurred"):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class TimeoutError(LuckMailError):
|
||||
"""超时异常"""
|
||||
def __init__(self, message: str = "Request timed out"):
|
||||
super().__init__(message)
|
||||
355
core/luckmail/http_client.py
Normal file
355
core/luckmail/http_client.py
Normal file
@@ -0,0 +1,355 @@
|
||||
"""
|
||||
核心 HTTP 客户端(基于 curl_cffi)
|
||||
支持同步/异步双模式,智能识别调用上下文自动切换
|
||||
|
||||
支持 TLS 指纹模拟,避免被目标网站识别为机器人。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import secrets
|
||||
import threading
|
||||
import time
|
||||
from typing import Any, Dict, Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from curl_cffi import requests as curl_requests
|
||||
|
||||
from .exceptions import APIError, AuthError, NetworkError
|
||||
|
||||
|
||||
def _is_async_context() -> bool:
|
||||
"""检测当前是否处于异步上下文(事件循环正在运行)"""
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
return loop.is_running()
|
||||
except RuntimeError:
|
||||
return False
|
||||
|
||||
|
||||
def _generate_hmac_signature(api_secret: str, api_key: str, timestamp: str, nonce: str) -> str:
|
||||
"""生成 HMAC-SHA256 签名
|
||||
|
||||
签名内容:api_key + timestamp + nonce,使用 api_secret 作为密钥
|
||||
"""
|
||||
message = f"{api_key}{timestamp}{nonce}"
|
||||
signature = hmac.new(
|
||||
api_secret.encode("utf-8"),
|
||||
message.encode("utf-8"),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
return signature
|
||||
|
||||
|
||||
class _SyncRunner:
|
||||
"""同步运行异步函数的工具类"""
|
||||
|
||||
_lock = threading.Lock()
|
||||
_loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
_thread: Optional[threading.Thread] = None
|
||||
|
||||
@classmethod
|
||||
def _ensure_loop(cls):
|
||||
"""确保后台事件循环正在运行"""
|
||||
with cls._lock:
|
||||
if cls._loop is None or not cls._loop.is_running():
|
||||
cls._loop = asyncio.new_event_loop()
|
||||
cls._thread = threading.Thread(
|
||||
target=cls._loop.run_forever,
|
||||
daemon=True,
|
||||
name="LuckMailSdk-EventLoop"
|
||||
)
|
||||
cls._thread.start()
|
||||
|
||||
@classmethod
|
||||
def run(cls, coro) -> Any:
|
||||
"""在后台事件循环中同步运行协程"""
|
||||
cls._ensure_loop()
|
||||
future = asyncio.run_coroutine_threadsafe(coro, cls._loop)
|
||||
return future.result()
|
||||
|
||||
|
||||
class LuckMailHttpClient:
|
||||
"""
|
||||
LuckMail HTTP 客户端(基于 curl_cffi)
|
||||
|
||||
使用 curl_cffi 作为底层 HTTP 库,支持 TLS 指纹模拟。
|
||||
自动识别调用上下文(同步/异步),提供统一的请求接口。
|
||||
|
||||
Args:
|
||||
base_url: API 基础 URL,如 https://your-domain.com
|
||||
api_key: API Key(必填)
|
||||
api_secret: API Secret(可选,用于 HMAC 签名验证,安全性更高)
|
||||
timeout: 请求超时时间(秒),默认 30
|
||||
use_hmac: 是否使用 HMAC 签名验证,默认 False(使用时需提供 api_secret)
|
||||
impersonate: 浏览器指纹模拟,默认 "chrome"(可选 "firefox"、"safari" 等)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str,
|
||||
api_key: str,
|
||||
api_secret: Optional[str] = None,
|
||||
timeout: float = 30.0,
|
||||
use_hmac: bool = False,
|
||||
impersonate: str = "chrome",
|
||||
):
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.api_key = api_key
|
||||
self.api_secret = api_secret
|
||||
self.timeout = timeout
|
||||
self.use_hmac = use_hmac and api_secret is not None
|
||||
self.impersonate = impersonate
|
||||
|
||||
# 同步 Session(延迟初始化)
|
||||
self._sync_session: Optional[curl_requests.Session] = None
|
||||
# 异步 Session(延迟初始化)
|
||||
self._async_session: Optional[Any] = None
|
||||
|
||||
def _get_sync_session(self) -> curl_requests.Session:
|
||||
"""获取或创建同步 Session"""
|
||||
if self._sync_session is None:
|
||||
self._sync_session = curl_requests.Session(
|
||||
impersonate=self.impersonate,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
return self._sync_session
|
||||
|
||||
async def _get_async_session(self):
|
||||
"""获取或创建异步 Session"""
|
||||
if self._async_session is None:
|
||||
self._async_session = curl_requests.AsyncSession(
|
||||
impersonate=self.impersonate,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
return self._async_session
|
||||
|
||||
def _build_headers(self) -> Dict[str, str]:
|
||||
"""构建请求头(含鉴权信息)"""
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
if self.use_hmac and self.api_secret:
|
||||
# HMAC 签名模式
|
||||
timestamp = str(int(time.time()))
|
||||
nonce = secrets.token_hex(16)
|
||||
signature = _generate_hmac_signature(
|
||||
self.api_secret, self.api_key, timestamp, nonce
|
||||
)
|
||||
headers["X-API-Key"] = self.api_key
|
||||
headers["X-Timestamp"] = timestamp
|
||||
headers["X-Nonce"] = nonce
|
||||
headers["X-Signature"] = signature
|
||||
elif self.api_key:
|
||||
# 普通 API Key 模式(推荐)
|
||||
headers["X-API-Key"] = self.api_key
|
||||
|
||||
return headers
|
||||
|
||||
def _build_url(self, path: str, params: Optional[Dict] = None) -> str:
|
||||
"""构建完整 URL"""
|
||||
url = f"{self.base_url}{path}"
|
||||
if params:
|
||||
# 过滤 None 值
|
||||
filtered = {k: v for k, v in params.items() if v is not None}
|
||||
if filtered:
|
||||
url = f"{url}?{urlencode(filtered)}"
|
||||
return url
|
||||
|
||||
def _parse_response(self, status_code: int, content: bytes) -> Any:
|
||||
"""解析响应数据"""
|
||||
try:
|
||||
data = json.loads(content)
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
# 非 JSON 响应(如文件流)直接返回字节内容
|
||||
return content
|
||||
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
code = data.get("code", -1)
|
||||
message = data.get("message", "Unknown error")
|
||||
|
||||
if code != 0:
|
||||
if status_code == 401 or code == 401:
|
||||
raise AuthError(message)
|
||||
raise APIError(code, message, data.get("data"))
|
||||
|
||||
return data.get("data")
|
||||
|
||||
# ===================== 异步方法 =====================
|
||||
|
||||
async def _async_request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
params: Optional[Dict] = None,
|
||||
json_data: Optional[Dict] = None,
|
||||
) -> Any:
|
||||
"""异步 HTTP 请求"""
|
||||
session = await self._get_async_session()
|
||||
headers = self._build_headers()
|
||||
url = self._build_url(path, params)
|
||||
|
||||
try:
|
||||
if method.upper() == "GET":
|
||||
response = await session.get(url, headers=headers)
|
||||
elif method.upper() == "POST":
|
||||
response = await session.post(
|
||||
url, headers=headers, json=json_data or {}
|
||||
)
|
||||
elif method.upper() == "PUT":
|
||||
response = await session.put(
|
||||
url, headers=headers, json=json_data or {}
|
||||
)
|
||||
elif method.upper() == "DELETE":
|
||||
response = await session.delete(url, headers=headers)
|
||||
else:
|
||||
raise ValueError(f"不支持的 HTTP 方法: {method}")
|
||||
|
||||
return self._parse_response(response.status_code, response.content)
|
||||
|
||||
except (AuthError, APIError):
|
||||
raise
|
||||
except Exception as e:
|
||||
err_msg = str(e).lower()
|
||||
if "timeout" in err_msg:
|
||||
from .exceptions import TimeoutError as LuckTimeoutError
|
||||
raise LuckTimeoutError(f"请求超时: {path}") from e
|
||||
raise NetworkError(f"请求失败: {e}") from e
|
||||
|
||||
async def _async_get_stream(self, path: str, params: Optional[Dict] = None) -> bytes:
|
||||
"""异步获取流式响应(文件下载等)"""
|
||||
session = await self._get_async_session()
|
||||
headers = self._build_headers()
|
||||
url = self._build_url(path, params)
|
||||
|
||||
try:
|
||||
response = await session.get(url, headers=headers)
|
||||
return response.content
|
||||
except Exception as e:
|
||||
err_msg = str(e).lower()
|
||||
if "timeout" in err_msg:
|
||||
from .exceptions import TimeoutError as LuckTimeoutError
|
||||
raise LuckTimeoutError(f"请求超时: {path}") from e
|
||||
raise NetworkError(f"网络错误: {e}") from e
|
||||
|
||||
async def aclose(self):
|
||||
"""关闭异步客户端"""
|
||||
if self._async_session is not None:
|
||||
await self._async_session.close()
|
||||
self._async_session = None
|
||||
|
||||
# ===================== 同步方法 =====================
|
||||
|
||||
def _sync_request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
params: Optional[Dict] = None,
|
||||
json_data: Optional[Dict] = None,
|
||||
) -> Any:
|
||||
"""同步 HTTP 请求(使用 curl_cffi)"""
|
||||
session = self._get_sync_session()
|
||||
headers = self._build_headers()
|
||||
url = self._build_url(path, params)
|
||||
|
||||
try:
|
||||
if method.upper() == "GET":
|
||||
response = session.get(url, headers=headers)
|
||||
elif method.upper() == "POST":
|
||||
response = session.post(
|
||||
url, headers=headers, json=json_data or {}
|
||||
)
|
||||
elif method.upper() == "PUT":
|
||||
response = session.put(
|
||||
url, headers=headers, json=json_data or {}
|
||||
)
|
||||
elif method.upper() == "DELETE":
|
||||
response = session.delete(url, headers=headers)
|
||||
else:
|
||||
raise ValueError(f"不支持的 HTTP 方法: {method}")
|
||||
|
||||
return self._parse_response(response.status_code, response.content)
|
||||
|
||||
except (AuthError, APIError):
|
||||
raise
|
||||
except Exception as e:
|
||||
err_msg = str(e).lower()
|
||||
if "timeout" in err_msg:
|
||||
from .exceptions import TimeoutError as LuckTimeoutError
|
||||
raise LuckTimeoutError(f"请求超时: {path}") from e
|
||||
raise NetworkError(f"请求失败: {e}") from e
|
||||
|
||||
def _sync_get_stream(self, path: str, params: Optional[Dict] = None) -> bytes:
|
||||
"""同步获取流式响应"""
|
||||
session = self._get_sync_session()
|
||||
headers = self._build_headers()
|
||||
url = self._build_url(path, params)
|
||||
|
||||
try:
|
||||
response = session.get(url, headers=headers)
|
||||
return response.content
|
||||
except Exception as e:
|
||||
err_msg = str(e).lower()
|
||||
if "timeout" in err_msg:
|
||||
from .exceptions import TimeoutError as LuckTimeoutError
|
||||
raise LuckTimeoutError(f"请求超时: {path}") from e
|
||||
raise NetworkError(f"网络错误: {e}") from e
|
||||
|
||||
# ===================== 统一接口(智能识别同步/异步)=====================
|
||||
|
||||
def request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
params: Optional[Dict] = None,
|
||||
json_data: Optional[Dict] = None,
|
||||
):
|
||||
"""
|
||||
统一请求接口,智能识别调用上下文:
|
||||
- 在 async 函数中调用:自动返回协程,需要 await
|
||||
- 在普通函数中调用:直接返回结果
|
||||
|
||||
使用示例:
|
||||
# 同步调用
|
||||
result = client.request("GET", "/api/v1/openapi/user/info")
|
||||
|
||||
# 异步调用
|
||||
result = await client.request("GET", "/api/v1/openapi/user/info")
|
||||
"""
|
||||
if _is_async_context():
|
||||
return self._async_request(method, path, params=params, json_data=json_data)
|
||||
else:
|
||||
return self._sync_request(method, path, params=params, json_data=json_data)
|
||||
|
||||
def get_stream(self, path: str, params: Optional[Dict] = None):
|
||||
"""
|
||||
流式 GET 请求(用于文件下载),智能识别同步/异步上下文
|
||||
"""
|
||||
if _is_async_context():
|
||||
return self._async_get_stream(path, params=params)
|
||||
else:
|
||||
return self._sync_get_stream(path, params=params)
|
||||
|
||||
def close(self):
|
||||
"""关闭同步客户端资源"""
|
||||
if self._sync_session is not None:
|
||||
self._sync_session.close()
|
||||
self._sync_session = None
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
await self.aclose()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
251
core/luckmail/models.py
Normal file
251
core/luckmail/models.py
Normal file
@@ -0,0 +1,251 @@
|
||||
"""
|
||||
数据模型定义
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, List, Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserInfo:
|
||||
"""用户信息"""
|
||||
id: int
|
||||
username: str
|
||||
email: str
|
||||
balance: str
|
||||
status: int
|
||||
api_email_enabled: int = 0
|
||||
api_email_price: str = "0.0000"
|
||||
|
||||
|
||||
@dataclass
|
||||
class EmailItem:
|
||||
"""邮箱列表项"""
|
||||
id: int
|
||||
address: str
|
||||
type: str
|
||||
status: int
|
||||
domain: str
|
||||
total_used: int = 0
|
||||
success_count: int = 0
|
||||
fail_count: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProjectPrice:
|
||||
"""项目定价"""
|
||||
email_type: str
|
||||
code_price: str
|
||||
buy_price: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProjectItem:
|
||||
"""项目信息"""
|
||||
id: int
|
||||
name: str
|
||||
code: str
|
||||
email_types: List[str]
|
||||
timeout_seconds: int
|
||||
warranty_hours: int
|
||||
daily_limit: int
|
||||
description: str
|
||||
prices: List[ProjectPrice] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class OrderInfo:
|
||||
"""订单信息(创建后)"""
|
||||
order_no: str
|
||||
email_address: str
|
||||
project: str
|
||||
price: str
|
||||
timeout_seconds: int
|
||||
expired_at: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class OrderCode:
|
||||
"""订单验证码查询结果"""
|
||||
order_no: str
|
||||
status: str # pending / success / timeout / cancelled
|
||||
verification_code: Optional[str] = None
|
||||
mail_from: Optional[str] = None
|
||||
mail_subject: Optional[str] = None
|
||||
mail_body_html: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class PurchaseItem:
|
||||
"""已购邮箱"""
|
||||
id: int
|
||||
email_address: str
|
||||
token: str
|
||||
project_name: str
|
||||
price: str
|
||||
status: int = 1
|
||||
tag_id: int = 0
|
||||
tag_name: str = ""
|
||||
user_disabled: int = 0
|
||||
warranty_hours: int = 0
|
||||
warranty_until: Optional[str] = None
|
||||
created_at: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class TokenCode:
|
||||
"""Token 查询验证码结果"""
|
||||
email_address: str
|
||||
project: str
|
||||
has_new_mail: bool
|
||||
verification_code: Optional[str] = None
|
||||
mail: Optional[dict] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class TokenAliveResult:
|
||||
"""Token 测活结果"""
|
||||
email_address: str
|
||||
project: str
|
||||
alive: bool
|
||||
status: str
|
||||
message: str = ""
|
||||
mail_count: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class TokenMailItem:
|
||||
"""Token 邮件列表项"""
|
||||
message_id: str
|
||||
from_addr: str = ""
|
||||
subject: str = ""
|
||||
body: str = ""
|
||||
html_body: str = ""
|
||||
received_at: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class TokenMailList:
|
||||
"""Token 邮件列表结果"""
|
||||
email_address: str
|
||||
project: str
|
||||
warranty_until: str = ""
|
||||
mails: List[TokenMailItem] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TokenMailDetail:
|
||||
"""Token 邮件详情结果"""
|
||||
message_id: str
|
||||
from_addr: str = ""
|
||||
to: str = ""
|
||||
subject: str = ""
|
||||
body_text: str = ""
|
||||
body_html: str = ""
|
||||
received_at: str = ""
|
||||
verification_code: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class AppealInfo:
|
||||
"""申述信息"""
|
||||
appeal_no: str
|
||||
appeal_type: int
|
||||
reason: str
|
||||
description: str
|
||||
status: int
|
||||
created_at: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class TagItem:
|
||||
"""邮箱标签"""
|
||||
id: int
|
||||
name: str
|
||||
remark: str = ""
|
||||
limit_type: int = 0 # 0=不下发 1=可下发
|
||||
purchase_count: int = 0
|
||||
created_at: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class PageResult:
|
||||
"""分页结果"""
|
||||
list: List[Any]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
|
||||
|
||||
# ===== 供应商模型 =====
|
||||
|
||||
@dataclass
|
||||
class SupplierProfile:
|
||||
"""供应商个人信息"""
|
||||
id: int
|
||||
username: str
|
||||
email: str
|
||||
balance: str
|
||||
frozen_balance: str
|
||||
code_commission_rate: str
|
||||
buy_commission_rate: str
|
||||
status: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class SupplierEmailItem:
|
||||
"""供应商邮箱列表项"""
|
||||
id: int
|
||||
address: str
|
||||
type: str
|
||||
status: int
|
||||
domain: str
|
||||
total_used: int = 0
|
||||
success_count: int = 0
|
||||
fail_count: int = 0
|
||||
is_short_term: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class AppealItem:
|
||||
"""申述列表项(供应商端)"""
|
||||
id: int
|
||||
appeal_no: str
|
||||
order_no: str
|
||||
reason: str
|
||||
status: int
|
||||
created_at: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class AppealDetail:
|
||||
"""申述详情"""
|
||||
appeal_no: str
|
||||
order_no: str
|
||||
reason: str
|
||||
status: int
|
||||
supplier_reply: Optional[str] = None
|
||||
created_at: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImportResult:
|
||||
"""导入邮箱结果"""
|
||||
success: int
|
||||
duplicate: int
|
||||
failed: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class DashboardSummary:
|
||||
"""供应商数据看板"""
|
||||
total_emails: int
|
||||
active_emails: int
|
||||
total_assigned: int
|
||||
total_success: int
|
||||
success_rate: float
|
||||
total_commission: str
|
||||
available_balance: str
|
||||
today_assigned: int
|
||||
today_success: int
|
||||
today_commission: str
|
||||
email_category: dict = field(default_factory=dict)
|
||||
463
core/luckmail/supplier.py
Normal file
463
core/luckmail/supplier.py
Normal file
@@ -0,0 +1,463 @@
|
||||
"""
|
||||
供应商端 API 接口
|
||||
Base URL: {base_url}/api/v1/openapi/supplier
|
||||
|
||||
所有方法均支持同步/异步双模式,根据调用上下文自动识别。
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from .http_client import LuckMailHttpClient, _is_async_context
|
||||
from .models import (
|
||||
AppealDetail,
|
||||
AppealItem,
|
||||
DashboardSummary,
|
||||
ImportResult,
|
||||
PageResult,
|
||||
SupplierEmailItem,
|
||||
SupplierProfile,
|
||||
)
|
||||
|
||||
_SUPPLIER_PREFIX = "/api/v1/openapi/supplier"
|
||||
|
||||
|
||||
def _parse_supplier_profile(data: dict) -> SupplierProfile:
|
||||
return SupplierProfile(
|
||||
id=data.get("id", 0),
|
||||
username=data.get("username", ""),
|
||||
email=data.get("email", ""),
|
||||
balance=data.get("balance", "0.0000"),
|
||||
frozen_balance=data.get("frozen_balance", "0.0000"),
|
||||
code_commission_rate=data.get("code_commission_rate", "0.0000"),
|
||||
buy_commission_rate=data.get("buy_commission_rate", "0.0000"),
|
||||
status=data.get("status", 1),
|
||||
)
|
||||
|
||||
|
||||
def _parse_supplier_email(data: dict) -> SupplierEmailItem:
|
||||
return SupplierEmailItem(
|
||||
id=data.get("id", 0),
|
||||
address=data.get("address", ""),
|
||||
type=data.get("type", ""),
|
||||
status=data.get("status", 1),
|
||||
domain=data.get("domain", ""),
|
||||
total_used=data.get("total_used", 0),
|
||||
success_count=data.get("success_count", 0),
|
||||
fail_count=data.get("fail_count", 0),
|
||||
is_short_term=data.get("is_short_term", 0),
|
||||
)
|
||||
|
||||
|
||||
def _parse_appeal_item(data: dict) -> AppealItem:
|
||||
return AppealItem(
|
||||
id=data.get("id", 0),
|
||||
appeal_no=data.get("appeal_no", ""),
|
||||
order_no=data.get("order_no", ""),
|
||||
reason=data.get("reason", ""),
|
||||
status=data.get("status", 1),
|
||||
created_at=data.get("created_at", ""),
|
||||
)
|
||||
|
||||
|
||||
def _parse_appeal_detail(data: dict) -> AppealDetail:
|
||||
return AppealDetail(
|
||||
appeal_no=data.get("appeal_no", ""),
|
||||
order_no=data.get("order_no", ""),
|
||||
reason=data.get("reason", ""),
|
||||
status=data.get("status", 1),
|
||||
supplier_reply=data.get("supplier_reply"),
|
||||
created_at=data.get("created_at"),
|
||||
)
|
||||
|
||||
|
||||
def _parse_page_result(data: dict, item_parser=None) -> PageResult:
|
||||
items = data.get("list", [])
|
||||
if item_parser:
|
||||
items = [item_parser(i) for i in items]
|
||||
return PageResult(
|
||||
list=items,
|
||||
total=data.get("total", 0),
|
||||
page=data.get("page", 1),
|
||||
page_size=data.get("page_size", 20),
|
||||
)
|
||||
|
||||
|
||||
class SupplierAPI:
|
||||
"""
|
||||
供应商端 API 接口集合
|
||||
|
||||
所有方法智能支持同步/异步调用:
|
||||
- 在 async 函数中:await client.supplier.get_profile()
|
||||
- 在普通函数中:client.supplier.get_profile()
|
||||
|
||||
Args:
|
||||
http_client: LuckMailHttpClient 实例
|
||||
"""
|
||||
|
||||
def __init__(self, http_client: LuckMailHttpClient):
|
||||
self._client = http_client
|
||||
|
||||
def _path(self, path: str) -> str:
|
||||
"""拼接供应商 API 路径"""
|
||||
return f"{_SUPPLIER_PREFIX}{path}"
|
||||
|
||||
# ===== 供应商信息 =====
|
||||
|
||||
def get_profile(self):
|
||||
"""
|
||||
获取供应商个人信息
|
||||
|
||||
Returns:
|
||||
SupplierProfile: 供应商信息(余额、佣金率等)
|
||||
|
||||
示例::
|
||||
profile = client.supplier.get_profile()
|
||||
print(profile.username, profile.balance)
|
||||
"""
|
||||
if _is_async_context():
|
||||
return self._async_get_profile()
|
||||
return self._sync_get_profile()
|
||||
|
||||
async def _async_get_profile(self) -> SupplierProfile:
|
||||
data = await self._client._async_request("GET", self._path("/profile"))
|
||||
return _parse_supplier_profile(data)
|
||||
|
||||
def _sync_get_profile(self) -> SupplierProfile:
|
||||
data = self._client._sync_request("GET", self._path("/profile"))
|
||||
return _parse_supplier_profile(data)
|
||||
|
||||
# ===== 邮箱管理 =====
|
||||
|
||||
def get_emails(
|
||||
self,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
keyword: Optional[str] = None,
|
||||
email_type: Optional[str] = None,
|
||||
is_short_term: Optional[int] = None,
|
||||
status: Optional[int] = None,
|
||||
):
|
||||
"""
|
||||
获取邮箱列表(分页)
|
||||
|
||||
Args:
|
||||
page: 页码,默认 1
|
||||
page_size: 每页数量,默认 20
|
||||
keyword: 邮箱地址关键词搜索
|
||||
email_type: 邮箱类型:ms_graph / ms_imap / google_variant / self_built
|
||||
is_short_term: 仅微软邮箱有效:0=长效 1=短效
|
||||
status: 状态:1=正常 2=异常 4=禁用
|
||||
|
||||
Returns:
|
||||
PageResult: 分页结果,list 为 SupplierEmailItem 列表
|
||||
|
||||
示例::
|
||||
result = client.supplier.get_emails(email_type='ms_graph', is_short_term=0)
|
||||
print(f"长效 MS Graph 邮箱: {result.total} 个")
|
||||
"""
|
||||
params = {
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"keyword": keyword,
|
||||
"type": email_type,
|
||||
"is_short_term": is_short_term,
|
||||
"status": status,
|
||||
}
|
||||
if _is_async_context():
|
||||
return self._async_get_emails(params)
|
||||
return self._sync_get_emails(params)
|
||||
|
||||
async def _async_get_emails(self, params: dict) -> PageResult:
|
||||
data = await self._client._async_request("GET", self._path("/emails"), params=params)
|
||||
return _parse_page_result(data, _parse_supplier_email)
|
||||
|
||||
def _sync_get_emails(self, params: dict) -> PageResult:
|
||||
data = self._client._sync_request("GET", self._path("/emails"), params=params)
|
||||
return _parse_page_result(data, _parse_supplier_email)
|
||||
|
||||
def import_emails(
|
||||
self,
|
||||
email_type: str,
|
||||
emails: List[dict],
|
||||
is_short_term: int = 0,
|
||||
):
|
||||
"""
|
||||
批量导入邮箱到供应商资源池
|
||||
|
||||
Args:
|
||||
email_type: 邮箱类型:microsoft / ms_graph / ms_imap / google_variant / self_built
|
||||
emails: 邮箱列表,每项包含 address、password、client_id、refresh_token 等
|
||||
is_short_term: 仅微软邮箱有效,0=长效(默认)1=短效
|
||||
|
||||
Returns:
|
||||
ImportResult: 导入结果
|
||||
|
||||
示例::
|
||||
result = client.supplier.import_emails(
|
||||
email_type='ms_graph',
|
||||
is_short_term=0,
|
||||
emails=[
|
||||
{
|
||||
'address': 'user1@outlook.com',
|
||||
'client_id': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
|
||||
'refresh_token': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
|
||||
}
|
||||
]
|
||||
)
|
||||
print(f"成功: {result.success}, 重复: {result.duplicate}")
|
||||
"""
|
||||
body: Dict[str, Any] = {
|
||||
"type": email_type,
|
||||
"is_short_term": is_short_term,
|
||||
"emails": emails,
|
||||
}
|
||||
if _is_async_context():
|
||||
return self._async_import_emails(body)
|
||||
return self._sync_import_emails(body)
|
||||
|
||||
async def _async_import_emails(self, body: dict) -> ImportResult:
|
||||
data = await self._client._async_request(
|
||||
"POST", self._path("/emails/import"), json_data=body
|
||||
)
|
||||
return ImportResult(
|
||||
success=data.get("success", 0),
|
||||
duplicate=data.get("duplicate", 0),
|
||||
failed=data.get("failed", 0),
|
||||
)
|
||||
|
||||
def _sync_import_emails(self, body: dict) -> ImportResult:
|
||||
data = self._client._sync_request(
|
||||
"POST", self._path("/emails/import"), json_data=body
|
||||
)
|
||||
return ImportResult(
|
||||
success=data.get("success", 0),
|
||||
duplicate=data.get("duplicate", 0),
|
||||
failed=data.get("failed", 0),
|
||||
)
|
||||
|
||||
def export_emails(
|
||||
self,
|
||||
keyword: Optional[str] = None,
|
||||
email_type: Optional[str] = None,
|
||||
is_short_term: Optional[int] = None,
|
||||
status: Optional[int] = None,
|
||||
):
|
||||
"""
|
||||
导出邮箱(txt 文件流)
|
||||
|
||||
Args:
|
||||
keyword: 关键词过滤
|
||||
email_type: 邮箱类型过滤
|
||||
is_short_term: 0=长效 1=短效
|
||||
status: 状态过滤
|
||||
|
||||
Returns:
|
||||
bytes: txt 文件内容
|
||||
|
||||
示例::
|
||||
content = client.supplier.export_emails(email_type='ms_graph')
|
||||
with open("emails.txt", "wb") as f:
|
||||
f.write(content)
|
||||
"""
|
||||
params = {
|
||||
"keyword": keyword,
|
||||
"type": email_type,
|
||||
"is_short_term": is_short_term,
|
||||
"status": status,
|
||||
}
|
||||
if _is_async_context():
|
||||
return self._client._async_get_stream(self._path("/emails/export"), params=params)
|
||||
return self._client._sync_get_stream(self._path("/emails/export"), params=params)
|
||||
|
||||
# ===== 申述管理 =====
|
||||
|
||||
def get_appeals(
|
||||
self,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
status: Optional[int] = None,
|
||||
appeal_type: Optional[int] = None,
|
||||
):
|
||||
"""
|
||||
获取申述列表(分页)
|
||||
|
||||
Args:
|
||||
page: 页码
|
||||
page_size: 每页数量
|
||||
status: 申述状态:1=待处理 2=已同意 3=待仲裁 4=已拒绝
|
||||
appeal_type: 申述类型过滤
|
||||
|
||||
Returns:
|
||||
PageResult: 分页结果,list 为 AppealItem 列表
|
||||
|
||||
示例::
|
||||
result = client.supplier.get_appeals(status=1)
|
||||
print(f"待处理申述: {result.total} 个")
|
||||
"""
|
||||
params = {
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"status": status,
|
||||
"type": appeal_type,
|
||||
}
|
||||
if _is_async_context():
|
||||
return self._async_get_appeals(params)
|
||||
return self._sync_get_appeals(params)
|
||||
|
||||
async def _async_get_appeals(self, params: dict) -> PageResult:
|
||||
data = await self._client._async_request("GET", self._path("/appeals"), params=params)
|
||||
return _parse_page_result(data, _parse_appeal_item)
|
||||
|
||||
def _sync_get_appeals(self, params: dict) -> PageResult:
|
||||
data = self._client._sync_request("GET", self._path("/appeals"), params=params)
|
||||
return _parse_page_result(data, _parse_appeal_item)
|
||||
|
||||
def get_appeal(self, appeal_no: str):
|
||||
"""
|
||||
获取申述详情
|
||||
|
||||
Args:
|
||||
appeal_no: 申述单号
|
||||
|
||||
Returns:
|
||||
AppealDetail: 申述详情
|
||||
|
||||
示例::
|
||||
detail = client.supplier.get_appeal("APL20240310001")
|
||||
print(detail.reason, detail.status)
|
||||
"""
|
||||
if _is_async_context():
|
||||
return self._async_get_appeal(appeal_no)
|
||||
return self._sync_get_appeal(appeal_no)
|
||||
|
||||
async def _async_get_appeal(self, appeal_no: str) -> AppealDetail:
|
||||
data = await self._client._async_request(
|
||||
"GET", self._path(f"/appeal/{appeal_no}")
|
||||
)
|
||||
return _parse_appeal_detail(data)
|
||||
|
||||
def _sync_get_appeal(self, appeal_no: str) -> AppealDetail:
|
||||
data = self._client._sync_request(
|
||||
"GET", self._path(f"/appeal/{appeal_no}")
|
||||
)
|
||||
return _parse_appeal_detail(data)
|
||||
|
||||
def reply_appeal(self, appeal_no: str, result: int, reply: str):
|
||||
"""
|
||||
处理申述(回复)
|
||||
|
||||
Args:
|
||||
appeal_no: 申述单号
|
||||
result: 处理结果:1=同意退款 2=拒绝申述 3=申请仲裁
|
||||
reply: 回复内容说明
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
示例::
|
||||
# 同意退款
|
||||
client.supplier.reply_appeal("APL20240310001", result=1, reply="邮箱确有问题,同意退款")
|
||||
|
||||
# 拒绝申述
|
||||
client.supplier.reply_appeal("APL20240310001", result=2, reply="邮箱状态正常,拒绝申述")
|
||||
"""
|
||||
body = {"result": result, "reply": reply}
|
||||
if _is_async_context():
|
||||
return self._async_reply_appeal(appeal_no, body)
|
||||
return self._sync_reply_appeal(appeal_no, body)
|
||||
|
||||
async def _async_reply_appeal(self, appeal_no: str, body: dict) -> None:
|
||||
await self._client._async_request(
|
||||
"POST", self._path(f"/appeal/{appeal_no}/reply"), json_data=body
|
||||
)
|
||||
|
||||
def _sync_reply_appeal(self, appeal_no: str, body: dict) -> None:
|
||||
self._client._sync_request(
|
||||
"POST", self._path(f"/appeal/{appeal_no}/reply"), json_data=body
|
||||
)
|
||||
|
||||
def batch_reply_appeals(
|
||||
self,
|
||||
appeal_nos: List[str],
|
||||
result: int,
|
||||
reply: str,
|
||||
):
|
||||
"""
|
||||
批量处理申述
|
||||
|
||||
Args:
|
||||
appeal_nos: 申述单号列表(最多 100 条)
|
||||
result: 处理结果:1=同意退款 2=拒绝申述 3=申请仲裁
|
||||
reply: 回复内容说明
|
||||
|
||||
Returns:
|
||||
dict: 包含 success 和 failed 数量
|
||||
|
||||
示例::
|
||||
result = client.supplier.batch_reply_appeals(
|
||||
appeal_nos=["APL001", "APL002", "APL003"],
|
||||
result=2,
|
||||
reply="经验证邮箱正常,拒绝申述"
|
||||
)
|
||||
print(f"成功处理: {result['success']}")
|
||||
"""
|
||||
body = {
|
||||
"appeal_nos": appeal_nos,
|
||||
"result": result,
|
||||
"reply": reply,
|
||||
}
|
||||
if _is_async_context():
|
||||
return self._async_batch_reply_appeals(body)
|
||||
return self._sync_batch_reply_appeals(body)
|
||||
|
||||
async def _async_batch_reply_appeals(self, body: dict) -> dict:
|
||||
return await self._client._async_request(
|
||||
"POST", self._path("/appeals/batch-reply"), json_data=body
|
||||
)
|
||||
|
||||
def _sync_batch_reply_appeals(self, body: dict) -> dict:
|
||||
return self._client._sync_request(
|
||||
"POST", self._path("/appeals/batch-reply"), json_data=body
|
||||
)
|
||||
|
||||
# ===== 数据看板 =====
|
||||
|
||||
def get_dashboard(self):
|
||||
"""
|
||||
获取数据看板总览
|
||||
|
||||
Returns:
|
||||
DashboardSummary: 看板数据,包含邮箱总量、接码统计、佣金数据等
|
||||
|
||||
示例::
|
||||
summary = client.supplier.get_dashboard()
|
||||
print(f"总邮箱: {summary.total_emails}")
|
||||
print(f"今日佣金: {summary.today_commission}")
|
||||
print(f"成功率: {summary.success_rate}%")
|
||||
"""
|
||||
if _is_async_context():
|
||||
return self._async_get_dashboard()
|
||||
return self._sync_get_dashboard()
|
||||
|
||||
async def _async_get_dashboard(self) -> DashboardSummary:
|
||||
data = await self._client._async_request("GET", self._path("/dashboard/summary"))
|
||||
return self._build_dashboard(data)
|
||||
|
||||
def _sync_get_dashboard(self) -> DashboardSummary:
|
||||
data = self._client._sync_request("GET", self._path("/dashboard/summary"))
|
||||
return self._build_dashboard(data)
|
||||
|
||||
def _build_dashboard(self, data: dict) -> DashboardSummary:
|
||||
return DashboardSummary(
|
||||
total_emails=data.get("total_emails", 0),
|
||||
active_emails=data.get("active_emails", 0),
|
||||
total_assigned=data.get("total_assigned", 0),
|
||||
total_success=data.get("total_success", 0),
|
||||
success_rate=data.get("success_rate", 0.0),
|
||||
total_commission=data.get("total_commission", "0.0000"),
|
||||
available_balance=data.get("available_balance", "0.0000"),
|
||||
today_assigned=data.get("today_assigned", 0),
|
||||
today_success=data.get("today_success", 0),
|
||||
today_commission=data.get("today_commission", "0.0000"),
|
||||
email_category=data.get("email_category", {}),
|
||||
)
|
||||
1360
core/luckmail/user.py
Normal file
1360
core/luckmail/user.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -50,6 +50,9 @@ export default function Register() {
|
||||
cfworker_admin_token: cfg.cfworker_admin_token || '',
|
||||
cfworker_domain: cfg.cfworker_domain || '',
|
||||
cfworker_fingerprint: cfg.cfworker_fingerprint || '',
|
||||
luckmail_base_url: cfg.luckmail_base_url || 'https://mails.luckyous.com/',
|
||||
luckmail_api_key: cfg.luckmail_api_key || '',
|
||||
luckmail_email_type: cfg.luckmail_email_type || '',
|
||||
})
|
||||
})
|
||||
}, [form])
|
||||
@@ -84,6 +87,9 @@ export default function Register() {
|
||||
cfworker_admin_token: values.cfworker_admin_token,
|
||||
cfworker_domain: values.cfworker_domain,
|
||||
cfworker_fingerprint: values.cfworker_fingerprint,
|
||||
luckmail_base_url: values.luckmail_base_url,
|
||||
luckmail_api_key: values.luckmail_api_key,
|
||||
luckmail_email_type: values.luckmail_email_type,
|
||||
yescaptcha_key: values.yescaptcha_key,
|
||||
solver_url: values.solver_url,
|
||||
},
|
||||
@@ -185,6 +191,7 @@ export default function Register() {
|
||||
{ value: 'freemail', label: 'Freemail' },
|
||||
{ value: 'laoudo', label: 'Laoudo' },
|
||||
{ value: 'cfworker', label: 'CF Worker' },
|
||||
{ value: 'luckmail', label: 'LuckMail' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -217,6 +224,19 @@ export default function Register() {
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
{mailProvider === 'luckmail' && (
|
||||
<>
|
||||
<Form.Item name="luckmail_base_url" label="平台地址">
|
||||
<Input placeholder="https://mails.luckyous.com" />
|
||||
</Form.Item>
|
||||
<Form.Item name="luckmail_api_key" label="API Key">
|
||||
<Input.Password placeholder="ak_..." />
|
||||
</Form.Item>
|
||||
<Form.Item name="luckmail_email_type" label="邮箱类型(可选)">
|
||||
<Input placeholder="ms_graph / ms_imap" />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{captchaSolver === 'yescaptcha' && (
|
||||
|
||||
@@ -21,6 +21,7 @@ const SELECT_FIELDS: Record<string, { label: string; value: string }[]> = {
|
||||
{ label: 'MoeMail (sall.cc)', value: 'moemail' },
|
||||
{ label: 'Freemail(自建 CF Worker)', value: 'freemail' },
|
||||
{ label: 'CF Worker(自建域名)', value: 'cfworker' },
|
||||
{ label: 'LuckMail(付费接码平台)', value: 'luckmail' },
|
||||
],
|
||||
default_executor: [
|
||||
{ label: 'API 协议(无浏览器)', value: 'protocol' },
|
||||
@@ -105,6 +106,15 @@ const TAB_ITEMS = [
|
||||
{ key: 'cfworker_fingerprint', label: 'Fingerprint', placeholder: '6703363b...' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'LuckMail',
|
||||
desc: '付费接码平台,支持 Outlook / Gmail 等多种邮箱类型',
|
||||
fields: [
|
||||
{ key: 'luckmail_base_url', label: '平台地址', placeholder: 'https://mails.luckyous.com' },
|
||||
{ key: 'luckmail_api_key', label: 'API Key', secret: true },
|
||||
{ key: 'luckmail_email_type', label: '邮箱类型(可选)', placeholder: 'ms_graph / ms_imap / self_built' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -521,7 +531,12 @@ export default function Settings() {
|
||||
const [activeTab, setActiveTab] = useState('register')
|
||||
|
||||
useEffect(() => {
|
||||
apiFetch('/config').then(form.setFieldsValue)
|
||||
apiFetch('/config').then((data) => {
|
||||
if (!data.luckmail_base_url) {
|
||||
data.luckmail_base_url = 'https://mails.luckyous.com/'
|
||||
}
|
||||
form.setFieldsValue(data)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const save = async () => {
|
||||
|
||||
Reference in New Issue
Block a user