Merge main into pr27-cfworker-domain-pools

Resolve PR #27 conflicts by keeping the configurable CFWorker domain pool
changes alongside existing MaliAPI, CodexProxy, and chatgpt settings.
This commit is contained in:
zhangchen
2026-03-30 23:49:59 +08:00
8 changed files with 474 additions and 17 deletions

View File

@@ -12,6 +12,7 @@ CONFIG_KEYS = [
"freemail_api_url", "freemail_admin_token", "freemail_username", "freemail_password",
"moemail_api_url",
"mail_provider",
"maliapi_base_url", "maliapi_api_key", "maliapi_domain", "maliapi_auto_domain_strategy",
"cfworker_api_url", "cfworker_admin_token", "cfworker_custom_auth", "cfworker_domain",
"cfworker_domains", "cfworker_enabled_domains", "cfworker_fingerprint",
"smstome_cookie", "smstome_country_slugs", "smstome_phone_attempts", "smstome_otp_timeout_seconds",
@@ -19,6 +20,7 @@ CONFIG_KEYS = [
"luckmail_base_url", "luckmail_api_key", "luckmail_email_type", "luckmail_domain",
"cpa_api_url", "cpa_api_key",
"team_manager_url", "team_manager_key",
"codex_proxy_url", "codex_proxy_key", "codex_proxy_upload_type",
"cliproxyapi_management_key",
"grok2api_url", "grok2api_app_key", "grok2api_pool", "grok2api_quota",
"kiro_manager_path", "kiro_manager_exe",

View File

@@ -113,6 +113,14 @@ def create_mailbox(provider: str, extra: dict = None, proxy: str = None) -> 'Bas
api_url=extra.get("moemail_api_url", "https://sall.cc"),
proxy=proxy,
)
elif provider == "maliapi":
return MaliAPIMailbox(
api_url=extra.get("maliapi_base_url", "https://maliapi.215.im/v1"),
api_key=extra.get("maliapi_api_key", ""),
domain=extra.get("maliapi_domain", ""),
auto_domain_strategy=extra.get("maliapi_auto_domain_strategy", ""),
proxy=proxy,
)
elif provider == "cfworker":
return CFWorkerMailbox(
api_url=extra.get("cfworker_api_url", ""),
@@ -411,6 +419,188 @@ class DuckMailMailbox(BaseMailbox):
raise TimeoutError(f"等待验证码超时 ({timeout}s)")
class MaliAPIMailbox(BaseMailbox):
"""YYDS Mail / MaliAPI 临时邮箱服务"""
def __init__(
self,
api_url: str = "https://maliapi.215.im/v1",
api_key: str = "",
domain: str = "",
auto_domain_strategy: str = "",
proxy: str = None,
):
self.api = (api_url or "https://maliapi.215.im/v1").rstrip("/")
self.api_key = str(api_key or "").strip()
self.domain = str(domain or "").strip()
self.auto_domain_strategy = str(auto_domain_strategy or "").strip()
self.proxy = {"http": proxy, "https": proxy} if proxy else None
self._email = None
self._temp_token = None
def _headers(self, bearer: str = "") -> dict[str, str]:
headers = {
"accept": "application/json",
"content-type": "application/json",
}
if self.api_key:
headers["X-API-Key"] = self.api_key
if bearer:
headers["Authorization"] = f"Bearer {bearer}"
return headers
def _request(self, method: str, path: str, *, json_body: dict = None,
params: dict = None, bearer: str = "") -> Any:
import requests
response = requests.request(
method,
f"{self.api}{path}",
headers=self._headers(bearer),
json=json_body,
params=params,
proxies=self.proxy,
timeout=15,
)
try:
payload = response.json()
except Exception:
payload = {}
if response.status_code >= 400:
error = response.text or f"HTTP {response.status_code}"
error_code = ""
if isinstance(payload, dict):
error = str(payload.get("error") or error).strip()
error_code = str(payload.get("errorCode") or "").strip()
if error_code:
raise RuntimeError(f"MaliAPI 请求失败: {error} ({error_code})")
raise RuntimeError(f"MaliAPI 请求失败: {str(error).strip()}")
if isinstance(payload, dict):
if payload.get("success") is False:
error = str(payload.get("error") or "unknown error").strip()
error_code = str(payload.get("errorCode") or "").strip()
if error_code:
raise RuntimeError(f"MaliAPI 请求失败: {error} ({error_code})")
raise RuntimeError(f"MaliAPI 请求失败: {error}")
if "data" in payload:
return payload.get("data")
return payload
def _ensure_api_key(self) -> None:
if not self.api_key:
raise RuntimeError("MaliAPI 未配置:请在全局设置中填写 maliapi_api_key")
def _list_messages(self, account: MailboxAccount) -> list[dict]:
data = self._request("GET", "/messages", params={"address": account.email})
if isinstance(data, dict):
messages = data.get("messages", [])
else:
messages = data
return [item for item in (messages or []) if isinstance(item, dict)]
def _get_message_detail(self, message_id: str) -> dict:
data = self._request("GET", f"/messages/{message_id}")
if isinstance(data, dict) and isinstance(data.get("message"), dict):
return data["message"]
return data if isinstance(data, dict) else {}
def get_email(self) -> MailboxAccount:
self._ensure_api_key()
body = {}
if self.domain:
body["domain"] = self.domain
if self.auto_domain_strategy:
body["autoDomainStrategy"] = self.auto_domain_strategy
data = self._request("POST", "/accounts", json_body=body)
if not isinstance(data, dict):
raise RuntimeError(f"MaliAPI 返回异常: {data}")
email = str(data.get("address") or data.get("email") or "").strip()
temp_token = str(
data.get("tempToken")
or data.get("temp_token")
or data.get("token")
or ""
).strip()
inbox_id = str(data.get("id") or "").strip()
if not email:
raise RuntimeError(f"MaliAPI 返回空邮箱: {data}")
self._email = email
self._temp_token = temp_token
self._log(f"[MaliAPI] 生成邮箱: {email}")
return MailboxAccount(
email=email,
account_id=temp_token or inbox_id or email,
extra={
"provider": "maliapi",
"temp_token": temp_token,
"inbox_id": inbox_id,
},
)
def get_current_ids(self, account: MailboxAccount) -> set:
self._ensure_api_key()
try:
return {
str(message.get("id"))
for message in self._list_messages(account)
if message.get("id") is not None
}
except Exception:
return set()
def wait_for_code(self, account: MailboxAccount, keyword: str = "",
timeout: int = 120, before_ids: set = None,
code_pattern: str = None, **kwargs) -> str:
import re
import time
self._ensure_api_key()
seen = {str(mid) for mid in (before_ids or set())}
start = time.time()
while time.time() - start < timeout:
try:
for message in self._list_messages(account):
message_id = str(message.get("id") or "").strip()
if not message_id or message_id in seen:
continue
seen.add(message_id)
try:
detail = self._get_message_detail(message_id)
except Exception:
detail = message
search_text = " ".join([
str(detail.get("subject") or message.get("subject") or ""),
str(detail.get("text") or ""),
str(detail.get("html") or ""),
str(message.get("subject") or ""),
str(message.get("snippet") or ""),
]).strip()
search_text = self._decode_raw_content(search_text) or search_text
search_text = re.sub(
r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
"",
search_text,
)
if keyword and keyword.lower() not in search_text.lower():
continue
code = self._safe_extract(search_text, code_pattern)
if code:
self._log(f"[MaliAPI] 收到验证码: {code}")
return code
except Exception:
pass
time.sleep(3)
raise TimeoutError(f"等待验证码超时 ({timeout}s)")
class CFWorkerMailbox(BaseMailbox):
"""Cloudflare Worker 自建临时邮箱服务"""

View File

@@ -333,6 +333,15 @@ export default function Accounts() {
message.success('已复制')
}
const getRefreshToken = (record: any): string => {
try {
const extra = JSON.parse(record.extra_json || '{}')
return extra.refresh_token || ''
} catch {
return ''
}
}
const exportCsv = () => {
const header = 'email,password,status,region,cashier_url,created_at'
const rows = accounts.map((a) => [a.email, a.password, a.status, a.region, a.cashier_url, a.created_at].join(','))
@@ -415,6 +424,10 @@ export default function Accounts() {
laoudo_auth: cfg.laoudo_auth,
laoudo_email: cfg.laoudo_email,
laoudo_account_id: cfg.laoudo_account_id,
maliapi_base_url: cfg.maliapi_base_url,
maliapi_api_key: cfg.maliapi_api_key,
maliapi_domain: cfg.maliapi_domain,
maliapi_auto_domain_strategy: cfg.maliapi_auto_domain_strategy,
yescaptcha_key: cfg.yescaptcha_key,
moemail_api_url: cfg.moemail_api_url,
duckmail_address: cfg.duckmail_address,
@@ -569,6 +582,22 @@ export default function Accounts() {
</Space>
),
},
{
title: 'RT',
key: 'refresh_token',
render: (_: any, record: any) => {
const rt = getRefreshToken(record)
if (!rt) return <span style={{ color: '#ccc' }}>-</span>
return (
<Space>
<Text style={{ fontFamily: 'monospace', fontSize: 11, filter: 'blur(4px)', maxWidth: 80, overflow: 'hidden', display: 'inline-block', verticalAlign: 'middle' }}>
{rt.slice(0, 16)}
</Text>
<Button type="text" size="small" icon={<CopyOutlined />} onClick={() => copyText(rt)} />
</Space>
)
},
},
{
title: '状态',
dataIndex: 'status',
@@ -820,22 +849,41 @@ export default function Accounts() {
maskClosable={false}
>
{currentAccount && (
<Form form={detailForm} layout="vertical" initialValues={currentAccount}>
<Form.Item name="status" label="状态">
<Select
options={[
{ value: 'registered', label: '已注册' },
{ value: 'trial', label: '试用中' },
{ value: 'subscribed', label: '已订阅' },
{ value: 'expired', label: '已过期' },
{ value: 'invalid', label: '已失效' },
]}
/>
</Form.Item>
<Form.Item name="token" label="Token">
<Input.TextArea rows={2} style={{ fontFamily: 'monospace' }} />
</Form.Item>
</Form>
<>
<Form form={detailForm} layout="vertical" initialValues={currentAccount}>
<Form.Item name="status" label="状态">
<Select
options={[
{ value: 'registered', label: '已注册' },
{ value: 'trial', label: '试用中' },
{ value: 'subscribed', label: '已订阅' },
{ value: 'expired', label: '已过期' },
{ value: 'invalid', label: '已失效' },
]}
/>
</Form.Item>
<Form.Item name="token" label="Access Token">
<Input.TextArea rows={2} style={{ fontFamily: 'monospace' }} />
</Form.Item>
</Form>
{(() => {
const rt = getRefreshToken(currentAccount)
if (!rt) return null
return (
<div style={{ marginTop: 8 }}>
<div style={{ marginBottom: 4, fontWeight: 500, fontSize: 13 }}>Refresh Token</div>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 8, background: 'rgba(0,0,0,0.03)', border: '1px solid #e5e7eb', borderRadius: 6, padding: '6px 10px' }}>
<Text
style={{ fontFamily: 'monospace', fontSize: 11, wordBreak: 'break-all', flex: 1, userSelect: 'text' }}
copyable={{ text: rt, tooltips: ['复制 RT', '已复制'] }}
>
{rt}
</Text>
</div>
</div>
)
})()}
</>
)}
</Modal>
</div>

View File

@@ -39,6 +39,10 @@ export default function Register() {
laoudo_auth: cfg.laoudo_auth || '',
laoudo_email: cfg.laoudo_email || '',
laoudo_account_id: cfg.laoudo_account_id || '',
maliapi_base_url: cfg.maliapi_base_url || 'https://maliapi.215.im/v1',
maliapi_api_key: cfg.maliapi_api_key || '',
maliapi_domain: cfg.maliapi_domain || '',
maliapi_auto_domain_strategy: cfg.maliapi_auto_domain_strategy || 'balanced',
duckmail_api_url: cfg.duckmail_api_url || '',
duckmail_provider_url: cfg.duckmail_provider_url || '',
duckmail_bearer: cfg.duckmail_bearer || '',
@@ -83,6 +87,10 @@ export default function Register() {
laoudo_auth: values.laoudo_auth,
laoudo_email: values.laoudo_email,
laoudo_account_id: values.laoudo_account_id,
maliapi_base_url: values.maliapi_base_url,
maliapi_api_key: values.maliapi_api_key,
maliapi_domain: values.maliapi_domain,
maliapi_auto_domain_strategy: values.maliapi_auto_domain_strategy,
moemail_api_url: values.moemail_api_url,
duckmail_api_url: values.duckmail_api_url,
duckmail_provider_url: values.duckmail_provider_url,
@@ -157,6 +165,8 @@ export default function Register() {
mail_provider: 'moemail',
count: 1,
register_delay_seconds: 0,
maliapi_base_url: 'https://maliapi.215.im/v1',
maliapi_auto_domain_strategy: 'balanced',
solver_url: 'http://localhost:8889',
}}>
<Card title="基本配置" style={{ marginBottom: 16 }}>
@@ -206,6 +216,7 @@ export default function Register() {
options={[
{ value: 'moemail', label: 'MoeMail (sall.cc)' },
{ value: 'tempmail_lol', label: 'TempMail.lol' },
{ value: 'maliapi', label: 'YYDS Mail / MaliAPI' },
{ value: 'duckmail', label: 'DuckMail' },
{ value: 'freemail', label: 'Freemail' },
{ value: 'laoudo', label: 'Laoudo' },
@@ -227,6 +238,28 @@ export default function Register() {
</Form.Item>
</>
)}
{mailProvider === 'maliapi' && (
<>
<Form.Item name="maliapi_base_url" label="API URL">
<Input placeholder="https://maliapi.215.im/v1" />
</Form.Item>
<Form.Item name="maliapi_api_key" label="API Key">
<Input.Password placeholder="AC-..." />
</Form.Item>
<Form.Item name="maliapi_domain" label="邮箱域名(可选)">
<Input placeholder="example.com" />
</Form.Item>
<Form.Item name="maliapi_auto_domain_strategy" label="自动域名策略">
<Select
options={[
{ value: 'balanced', label: 'balanced' },
{ value: 'prefer_owned', label: 'prefer_owned' },
{ value: 'prefer_public', label: 'prefer_public' },
]}
/>
</Form.Item>
</>
)}
{mailProvider === 'cfworker' && (
<>
<Form.Item name="cfworker_api_url" label="API URL">

View File

@@ -20,10 +20,16 @@ const SELECT_FIELDS: Record<string, { label: string; value: string }[]> = {
{ label: 'TempMail.lol自动生成', value: 'tempmail_lol' },
{ label: 'DuckMail自动生成', value: 'duckmail' },
{ label: 'MoeMail (sall.cc)', value: 'moemail' },
{ label: 'YYDS Mail / MaliAPI', value: 'maliapi' },
{ label: 'Freemail自建 CF Worker', value: 'freemail' },
{ label: 'CF Worker自建域名', value: 'cfworker' },
{ label: 'LuckMail订单接码 / 已购邮箱)', value: 'luckmail' },
],
maliapi_auto_domain_strategy: [
{ label: 'balanced', value: 'balanced' },
{ label: 'prefer_owned', value: 'prefer_owned' },
{ label: 'prefer_public', value: 'prefer_public' },
],
default_executor: [
{ label: 'API 协议(无浏览器)', value: 'protocol' },
{ label: '无头浏览器', value: 'headless' },
@@ -34,6 +40,10 @@ const SELECT_FIELDS: Record<string, { label: string; value: string }[]> = {
{ label: '本地 Solver (Camoufox)', value: 'local_solver' },
{ label: '手动', value: 'manual' },
],
codex_proxy_upload_type: [
{ label: 'ATAccess Token推荐', value: 'at' },
{ label: 'RTRefresh Token', value: 'rt' },
],
}
const TAB_ITEMS = [
@@ -83,6 +93,16 @@ const TAB_ITEMS = [
desc: '自动注册账号并生成临时邮箱',
fields: [{ key: 'moemail_api_url', label: 'API URL', placeholder: 'https://sall.cc' }],
},
{
title: 'YYDS Mail / MaliAPI',
desc: '基于 API Key 创建临时邮箱并轮询收件箱消息',
fields: [
{ key: 'maliapi_base_url', label: 'API URL', placeholder: 'https://maliapi.215.im/v1' },
{ key: 'maliapi_api_key', label: 'API Key', secret: true },
{ key: 'maliapi_domain', label: '邮箱域名(可选)', placeholder: 'example.com' },
{ key: 'maliapi_auto_domain_strategy', label: '自动域名策略', type: 'select' },
],
},
{
title: 'TempMail.lol',
desc: '自动生成邮箱无需配置需要代理访问CN IP 被封)',
@@ -104,7 +124,6 @@ const TAB_ITEMS = [
{ key: 'cfworker_api_url', label: 'API URL', placeholder: 'https://apimail.example.com' },
{ key: 'cfworker_admin_token', label: '管理员 Token', secret: true },
{ key: 'cfworker_custom_auth', label: '站点密码', secret: true },
{ key: 'cfworker_custom_auth', label: '站点密码', secret: true },
{ key: 'cfworker_fingerprint', label: 'Fingerprint', placeholder: '6703363b...' },
],
},
@@ -156,6 +175,15 @@ const TAB_ITEMS = [
{ key: 'team_manager_key', label: 'API Key', secret: true },
],
},
{
title: 'CodexProxy',
desc: '注册完成后自动上传到 CodexProxy 管理平台',
fields: [
{ key: 'codex_proxy_url', label: 'API URL', placeholder: 'https://your-codex-proxy.example.com' },
{ key: 'codex_proxy_key', label: 'Admin Key', secret: true },
{ key: 'codex_proxy_upload_type', label: '上传类型' },
],
},
{
title: 'SMSToMe 手机验证',
desc: 'ChatGPT add_phone 阶段自动取号并轮询短信验证码',
@@ -709,6 +737,9 @@ export default function Settings() {
useEffect(() => {
apiFetch('/config').then((data) => {
if (!data.maliapi_base_url) {
data.maliapi_base_url = 'https://maliapi.215.im/v1'
}
if (!data.luckmail_base_url) {
data.luckmail_base_url = 'https://mails.luckyous.com/'
}

View File

@@ -316,6 +316,127 @@ def upload_to_team_manager(
return False, f"上传异常: {str(e)}"
def upload_to_codex_proxy(
account,
api_url: str = None,
api_key: str = None,
) -> Tuple[bool, str]:
"""上传单账号到 CodexProxy通过 refresh_token不走代理
api_url / api_key 为空时自动从 ConfigStore 读取。"""
if not api_url:
api_url = _get_config_value("codex_proxy_url")
if not api_key:
api_key = _get_config_value("codex_proxy_key")
if not api_url:
return False, "CodexProxy API URL 未配置"
if not api_key:
return False, "CodexProxy Admin Key 未配置"
refresh_token = getattr(account, "refresh_token", "")
if not refresh_token:
return False, "账号缺少 refresh_token"
url = api_url.rstrip("/") + "/api/admin/accounts"
headers = {
"x-admin-key": api_key,
"Content-Type": "application/json",
"accept": "*/*",
}
payload = {
"refresh_token": refresh_token,
"proxy_url": "",
}
try:
resp = cffi_requests.post(
url,
headers=headers,
json=payload,
proxies=None,
verify=False,
timeout=30,
impersonate="chrome110",
)
if resp.status_code in (200, 201):
try:
data = resp.json()
msg = data.get("message", "上传成功")
except Exception:
msg = "上传成功"
return True, msg
error_msg = f"上传失败: HTTP {resp.status_code}"
try:
detail = resp.json()
if isinstance(detail, dict):
error_msg = detail.get("message", error_msg)
except Exception:
error_msg = f"{error_msg} - {resp.text[:200]}"
return False, error_msg
except Exception as e:
logger.error(f"CodexProxy 上传异常: {e}")
return False, f"上传异常: {str(e)}"
def upload_at_to_codex_proxy(
account,
api_url: str = None,
api_key: str = None,
) -> Tuple[bool, str]:
"""上传单账号到 CodexProxy通过 access_token走 /api/admin/accounts/at"""
if not api_url:
api_url = _get_config_value("codex_proxy_url")
if not api_key:
api_key = _get_config_value("codex_proxy_key")
if not api_url:
return False, "CodexProxy API URL 未配置"
if not api_key:
return False, "CodexProxy Admin Key 未配置"
access_token = getattr(account, "access_token", "")
if not access_token:
return False, "账号缺少 access_token"
url = api_url.rstrip("/") + "/api/admin/accounts/at"
headers = {
"x-admin-key": api_key,
"Content-Type": "application/json",
"accept": "*/*",
}
payload = {
"access_token": access_token,
"proxy_url": "",
}
try:
resp = cffi_requests.post(
url,
headers=headers,
json=payload,
proxies=None,
verify=False,
timeout=30,
impersonate="chrome110",
)
if resp.status_code in (200, 201):
try:
data = resp.json()
msg = data.get("message", "上传成功")
except Exception:
msg = "上传成功"
return True, msg
error_msg = f"上传失败: HTTP {resp.status_code}"
try:
detail = resp.json()
if isinstance(detail, dict):
error_msg = detail.get("message", error_msg)
except Exception:
error_msg = f"{error_msg} - {resp.text[:200]}"
return False, error_msg
except Exception as e:
logger.error(f"CodexProxy AT 上传异常: {e}")
return False, f"上传异常: {str(e)}"
def test_cpa_connection(api_url: str, api_token: str, proxy: str = None) -> Tuple[bool, str]:
"""测试 CPA 连接(不走代理)"""
if not api_url:

View File

@@ -163,6 +163,11 @@ class ChatGPTPlatform(BasePlatform):
{"key": "api_url", "label": "TM API URL", "type": "text"},
{"key": "api_key", "label": "TM API Key", "type": "text"},
]},
{"id": "upload_codex_proxy", "label": "上传 CodexProxy",
"params": [
{"key": "api_url", "label": "API URL", "type": "text"},
{"key": "api_key", "label": "Admin Key", "type": "text"},
]},
]
def execute_action(self, action_id: str, account: Account, params: dict) -> dict:
@@ -218,4 +223,10 @@ class ChatGPTPlatform(BasePlatform):
api_key=params.get("api_key"))
return {"ok": ok, "data": msg}
elif action_id == "upload_codex_proxy":
from platforms.chatgpt.cpa_upload import upload_to_codex_proxy
ok, msg = upload_to_codex_proxy(a, api_url=params.get("api_url"),
api_key=params.get("api_key"))
return {"ok": ok, "data": msg}
raise NotImplementedError(f"未知操作: {action_id}")

View File

@@ -21,6 +21,27 @@ def sync_account(account) -> list[dict[str, Any]]:
persist_cpa_sync_result(account, ok, msg)
results.append({"name": "CPA", "ok": ok, "msg": msg})
codex_proxy_url = str(config_store.get("codex_proxy_url", "") or "").strip()
if codex_proxy_url:
upload_type = str(config_store.get("codex_proxy_upload_type", "at") or "at").strip().lower()
extra = account.extra or {}
class _CP:
pass
cp = _CP()
cp.access_token = extra.get("access_token") or account.token
cp.refresh_token = extra.get("refresh_token", "")
if upload_type == "rt":
from platforms.chatgpt.cpa_upload import upload_to_codex_proxy
ok, msg = upload_to_codex_proxy(cp)
results.append({"name": "CodexProxy(RT)", "ok": ok, "msg": msg})
else:
from platforms.chatgpt.cpa_upload import upload_at_to_codex_proxy
ok, msg = upload_at_to_codex_proxy(cp)
results.append({"name": "CodexProxy(AT)", "ok": ok, "msg": msg})
elif platform == "grok":
grok2api_url = str(config_store.get("grok2api_url", "") or "").strip()
if grok2api_url: