mirror of
https://github.com/zc-zhangchen/any-auto-register.git
synced 2026-05-10 09:20:01 +08:00
feat(cfworker): support configurable domain pools
This commit is contained in:
@@ -12,7 +12,8 @@ CONFIG_KEYS = [
|
||||
"freemail_api_url", "freemail_admin_token", "freemail_username", "freemail_password",
|
||||
"moemail_api_url",
|
||||
"mail_provider",
|
||||
"cfworker_api_url", "cfworker_admin_token", "cfworker_custom_auth", "cfworker_domain", "cfworker_fingerprint",
|
||||
"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",
|
||||
"smstome_poll_interval_seconds", "smstome_sync_max_pages_per_country",
|
||||
"luckmail_base_url", "luckmail_api_key", "luckmail_email_type", "luckmail_domain",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""邮箱池基类 - 抽象临时邮箱/收件服务"""
|
||||
import json
|
||||
import random
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
@@ -117,6 +118,9 @@ def create_mailbox(provider: str, extra: dict = None, proxy: str = None) -> 'Bas
|
||||
api_url=extra.get("cfworker_api_url", ""),
|
||||
admin_token=extra.get("cfworker_admin_token", ""),
|
||||
domain=extra.get("cfworker_domain", ""),
|
||||
domain_override=extra.get("cfworker_domain_override", ""),
|
||||
domains=extra.get("cfworker_domains", ""),
|
||||
enabled_domains=extra.get("cfworker_enabled_domains", ""),
|
||||
fingerprint=extra.get("cfworker_fingerprint", ""),
|
||||
custom_auth=extra.get("cfworker_custom_auth", ""),
|
||||
proxy=proxy,
|
||||
@@ -411,10 +415,20 @@ class CFWorkerMailbox(BaseMailbox):
|
||||
"""Cloudflare Worker 自建临时邮箱服务"""
|
||||
|
||||
def __init__(self, api_url: str, admin_token: str = "", domain: str = "",
|
||||
fingerprint: str = "", custom_auth: str = "", proxy: str = None):
|
||||
domain_override: str = "", domains: Any = None,
|
||||
enabled_domains: Any = None, fingerprint: str = "",
|
||||
custom_auth: str = "", proxy: str = None):
|
||||
self.api = api_url.rstrip("/")
|
||||
self.admin_token = admin_token
|
||||
self.domain = domain
|
||||
self.domain = self._normalize_domain(domain)
|
||||
self.domain_override = self._normalize_domain(domain_override)
|
||||
self.domains = self._parse_domains(domains)
|
||||
raw_enabled_domains = self._parse_domains(enabled_domains)
|
||||
if self.domains:
|
||||
allowed = set(self.domains)
|
||||
self.enabled_domains = [d for d in raw_enabled_domains if d in allowed]
|
||||
else:
|
||||
self.enabled_domains = raw_enabled_domains
|
||||
self.fingerprint = fingerprint
|
||||
self.custom_auth = custom_auth
|
||||
self.proxy = {"http": proxy, "https": proxy} if proxy else None
|
||||
@@ -479,18 +493,67 @@ class CFWorkerMailbox(BaseMailbox):
|
||||
) from e
|
||||
|
||||
def _generate_local_part(self) -> str:
|
||||
import random, string
|
||||
import string
|
||||
# 避免纯数字开头,提高邮箱格式“像真人”的程度
|
||||
prefix = "".join(random.choices(string.ascii_lowercase, k=6))
|
||||
suffix = "".join(random.choices(string.digits, k=4))
|
||||
return f"{prefix}{suffix}"
|
||||
|
||||
@staticmethod
|
||||
def _normalize_domain(domain: Any) -> str:
|
||||
value = str(domain or "").strip().lower()
|
||||
if value.startswith("@"):
|
||||
value = value[1:]
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def _parse_domains(cls, value: Any) -> list[str]:
|
||||
if not value:
|
||||
return []
|
||||
|
||||
items: list[Any]
|
||||
if isinstance(value, (list, tuple, set)):
|
||||
items = list(value)
|
||||
elif isinstance(value, str):
|
||||
text = value.strip()
|
||||
if not text:
|
||||
return []
|
||||
try:
|
||||
parsed = json.loads(text)
|
||||
except Exception:
|
||||
parsed = None
|
||||
if isinstance(parsed, list):
|
||||
items = parsed
|
||||
else:
|
||||
items = [part for chunk in text.splitlines() for part in chunk.split(",")]
|
||||
else:
|
||||
items = [value]
|
||||
|
||||
domains: list[str] = []
|
||||
seen = set()
|
||||
for item in items:
|
||||
domain = cls._normalize_domain(item)
|
||||
if not domain or domain in seen:
|
||||
continue
|
||||
seen.add(domain)
|
||||
domains.append(domain)
|
||||
return domains
|
||||
|
||||
def _pick_domain(self) -> str:
|
||||
if self.domain_override:
|
||||
return self.domain_override
|
||||
if self.enabled_domains:
|
||||
return random.choice(self.enabled_domains)
|
||||
return self.domain
|
||||
|
||||
def get_email(self) -> MailboxAccount:
|
||||
self._ensure_api_configured()
|
||||
name = self._generate_local_part()
|
||||
payload = {"enablePrefix": True, "name": name}
|
||||
if self.domain:
|
||||
payload["domain"] = self.domain
|
||||
selected_domain = self._pick_domain()
|
||||
if selected_domain:
|
||||
payload["domain"] = selected_domain
|
||||
self._log(f"[CFWorker] 本次使用域名: {selected_domain}")
|
||||
data = self._request_json("POST", "/admin/new_address", payload=payload, timeout=15)
|
||||
email = data.get("email", data.get("address", ""))
|
||||
token = data.get("token", data.get("jwt", ""))
|
||||
@@ -498,7 +561,11 @@ class CFWorkerMailbox(BaseMailbox):
|
||||
raise RuntimeError(f"CFWorker API /admin/new_address 返回缺少 email/jwt: {data}")
|
||||
self._token = token
|
||||
print(f"[CFWorker] 生成邮箱: {email} token={token[:40] if token else 'NONE'}...")
|
||||
return MailboxAccount(email=email, account_id=token)
|
||||
return MailboxAccount(
|
||||
email=email,
|
||||
account_id=token,
|
||||
extra={"cfworker_domain": selected_domain} if selected_domain else None,
|
||||
)
|
||||
|
||||
def _get_mails(self, email: str) -> list:
|
||||
self._ensure_api_configured()
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function Register() {
|
||||
cfworker_api_url: cfg.cfworker_api_url || '',
|
||||
cfworker_admin_token: cfg.cfworker_admin_token || '',
|
||||
cfworker_custom_auth: cfg.cfworker_custom_auth || '',
|
||||
cfworker_domain: cfg.cfworker_domain || '',
|
||||
cfworker_domain_override: '',
|
||||
cfworker_fingerprint: cfg.cfworker_fingerprint || '',
|
||||
smstome_cookie: cfg.smstome_cookie || '',
|
||||
smstome_country_slugs: cfg.smstome_country_slugs || '',
|
||||
@@ -94,7 +94,7 @@ export default function Register() {
|
||||
cfworker_api_url: values.cfworker_api_url,
|
||||
cfworker_admin_token: values.cfworker_admin_token,
|
||||
cfworker_custom_auth: values.cfworker_custom_auth,
|
||||
cfworker_domain: values.cfworker_domain,
|
||||
cfworker_domain_override: values.cfworker_domain_override,
|
||||
cfworker_fingerprint: values.cfworker_fingerprint,
|
||||
smstome_cookie: values.smstome_cookie,
|
||||
smstome_country_slugs: values.smstome_country_slugs,
|
||||
@@ -238,7 +238,11 @@ export default function Register() {
|
||||
<Form.Item name="cfworker_custom_auth" label="Site Password">
|
||||
<Input.Password placeholder="private site password" />
|
||||
</Form.Item>
|
||||
<Form.Item name="cfworker_domain" label="域名">
|
||||
<Form.Item
|
||||
name="cfworker_domain_override"
|
||||
label="单次任务指定域名(可选)"
|
||||
extra="留空时将从设置页已启用的域名列表中随机选择。"
|
||||
>
|
||||
<Input placeholder="example.com" />
|
||||
</Form.Item>
|
||||
<Form.Item name="cfworker_fingerprint" label="Fingerprint (可选)">
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
SyncOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { apiFetch } from '@/lib/utils'
|
||||
|
||||
@@ -103,7 +104,7 @@ 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_domain', label: '邮箱域名', placeholder: 'example.com' },
|
||||
{ key: 'cfworker_custom_auth', label: '站点密码', secret: true },
|
||||
{ key: 'cfworker_fingerprint', label: 'Fingerprint', placeholder: '6703363b...' },
|
||||
],
|
||||
},
|
||||
@@ -261,6 +262,41 @@ function formatResultText(data: unknown) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeDomainList(input: unknown): string[] {
|
||||
const items = Array.isArray(input) ? input : []
|
||||
const seen = new Set<string>()
|
||||
const domains: string[] = []
|
||||
for (const item of items) {
|
||||
const domain = String(item || '').trim().toLowerCase().replace(/^@/, '')
|
||||
if (!domain || seen.has(domain)) continue
|
||||
seen.add(domain)
|
||||
domains.push(domain)
|
||||
}
|
||||
return domains
|
||||
}
|
||||
|
||||
function parseStoredDomainList(value: unknown): string[] {
|
||||
if (Array.isArray(value)) return normalizeDomainList(value)
|
||||
if (typeof value !== 'string') return []
|
||||
|
||||
const text = value.trim()
|
||||
if (!text) return []
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(text)
|
||||
if (Array.isArray(parsed)) {
|
||||
return normalizeDomainList(parsed)
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return normalizeDomainList(
|
||||
text
|
||||
.split('\n')
|
||||
.flatMap((line) => line.split(','))
|
||||
.map((item) => item.trim()),
|
||||
)
|
||||
}
|
||||
|
||||
function ConfigField({ field }: { field: FieldConfig }) {
|
||||
const [showSecret, setShowSecret] = useState(false)
|
||||
const options = SELECT_FIELDS[field.key]
|
||||
@@ -299,6 +335,133 @@ function ConfigSection({ section }: { section: SectionConfig }) {
|
||||
)
|
||||
}
|
||||
|
||||
function CFWorkerDomainPoolSection({ form }: { form: any }) {
|
||||
const watchedDomains = Form.useWatch('cfworker_domains', form) || []
|
||||
const watchedEnabledDomains = Form.useWatch('cfworker_enabled_domains', form) || []
|
||||
const normalizedDomains = normalizeDomainList(watchedDomains)
|
||||
const enabledDomains = normalizeDomainList(watchedEnabledDomains).filter((domain) => normalizedDomains.includes(domain))
|
||||
|
||||
const updateEnabledDomains = (nextDomains: string[]) => {
|
||||
form.setFieldValue('cfworker_enabled_domains', normalizeDomainList(nextDomains))
|
||||
}
|
||||
|
||||
const toggleEnabledDomain = (domain: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
updateEnabledDomains([...enabledDomains, domain])
|
||||
return
|
||||
}
|
||||
updateEnabledDomains(enabledDomains.filter((item) => item !== domain))
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
title="CF Worker 域名池"
|
||||
extra={<span style={{ fontSize: 12, color: '#7a8ba3' }}>注册时会从已启用域名中随机选择一个</span>}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<Form.List name="cfworker_domains">
|
||||
{(fields, { add, remove }) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{fields.map((field) => (
|
||||
<Space key={field.key} align="start" style={{ display: 'flex' }}>
|
||||
<Form.Item
|
||||
{...field}
|
||||
label={field.name === 0 ? '全部域名' : ''}
|
||||
style={{ flex: 1, marginBottom: 0 }}
|
||||
rules={[
|
||||
{
|
||||
validator: async (_, value) => {
|
||||
if (!String(value || '').trim()) {
|
||||
throw new Error('请输入域名')
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="example.com" />
|
||||
</Form.Item>
|
||||
<Button
|
||||
danger
|
||||
onClick={() => {
|
||||
const currentDomains = Array.isArray(form.getFieldValue('cfworker_domains'))
|
||||
? [...form.getFieldValue('cfworker_domains')]
|
||||
: []
|
||||
const removedDomain = String(currentDomains[field.name] || '').trim().toLowerCase().replace(/^@/, '')
|
||||
remove(field.name)
|
||||
if (!removedDomain) return
|
||||
const enabledDomains = normalizeDomainList(form.getFieldValue('cfworker_enabled_domains'))
|
||||
form.setFieldValue(
|
||||
'cfworker_enabled_domains',
|
||||
enabledDomains.filter((domain) => domain !== removedDomain),
|
||||
)
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
))}
|
||||
{fields.length === 0 ? (
|
||||
<Typography.Text type="secondary">还没有配置域名。添加后即可在下方选择启用项。</Typography.Text>
|
||||
) : null}
|
||||
<Button type="dashed" onClick={() => add('')} icon={<PlusOutlined />} block>
|
||||
添加域名
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Form.List>
|
||||
|
||||
<Form.Item name="cfworker_enabled_domains" hidden>
|
||||
<Select mode="multiple" options={normalizedDomains.map((domain) => ({ label: domain, value: domain }))} />
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<div style={{ marginBottom: 8, fontWeight: 500 }}>已启用域名</div>
|
||||
{enabledDomains.length > 0 ? (
|
||||
<Space wrap>
|
||||
{enabledDomains.map((domain) => (
|
||||
<Tag
|
||||
key={domain}
|
||||
color="blue"
|
||||
closable
|
||||
onClose={(event) => {
|
||||
event.preventDefault()
|
||||
updateEnabledDomains(enabledDomains.filter((item) => item !== domain))
|
||||
}}
|
||||
>
|
||||
{domain}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
) : (
|
||||
<Typography.Text type="secondary">暂无启用域名,点击下方域名即可启用。</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<div style={{ marginBottom: 8, fontWeight: 500 }}>点击切换启用状态</div>
|
||||
{normalizedDomains.length > 0 ? (
|
||||
<Space wrap>
|
||||
{normalizedDomains.map((domain) => (
|
||||
<Tag.CheckableTag
|
||||
key={domain}
|
||||
checked={enabledDomains.includes(domain)}
|
||||
onChange={(checked) => toggleEnabledDomain(domain, checked)}
|
||||
>
|
||||
{domain}
|
||||
</Tag.CheckableTag>
|
||||
))}
|
||||
</Space>
|
||||
) : (
|
||||
<Typography.Text type="secondary">请先在上方添加域名。</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
<Typography.Text type="secondary" style={{ display: 'block', marginTop: 12 }}>
|
||||
仅已启用域名会参与注册;点击已启用标签可直接移除。
|
||||
</Typography.Text>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function SolverStatus() {
|
||||
const [running, setRunning] = useState<boolean | null>(null)
|
||||
|
||||
@@ -549,15 +712,37 @@ export default function Settings() {
|
||||
if (!data.luckmail_base_url) {
|
||||
data.luckmail_base_url = 'https://mails.luckyous.com/'
|
||||
}
|
||||
data.cfworker_domains = parseStoredDomainList(data.cfworker_domains)
|
||||
data.cfworker_enabled_domains = parseStoredDomainList(data.cfworker_enabled_domains)
|
||||
form.setFieldsValue(data)
|
||||
})
|
||||
}, [])
|
||||
}, [form])
|
||||
|
||||
const save = async () => {
|
||||
setSaving(true)
|
||||
try {
|
||||
const values = form.getFieldsValue()
|
||||
const values = form.getFieldsValue(true)
|
||||
const domains = normalizeDomainList(values.cfworker_domains)
|
||||
const enabledDomains = normalizeDomainList(values.cfworker_enabled_domains).filter((domain) => domains.includes(domain))
|
||||
|
||||
if (domains.length > 0 && enabledDomains.length === 0) {
|
||||
setActiveTab('mailbox')
|
||||
message.error('CF Worker 至少需要启用一个域名')
|
||||
return
|
||||
}
|
||||
|
||||
values.cfworker_domains = JSON.stringify(domains)
|
||||
values.cfworker_enabled_domains = JSON.stringify(enabledDomains)
|
||||
if (domains.length > 0) {
|
||||
values.cfworker_domain = ''
|
||||
}
|
||||
|
||||
await apiFetch('/config', { method: 'PUT', body: JSON.stringify({ data: values }) })
|
||||
form.setFieldsValue({
|
||||
cfworker_domains: domains,
|
||||
cfworker_enabled_domains: enabledDomains,
|
||||
cfworker_domain: domains.length > 0 ? '' : values.cfworker_domain,
|
||||
})
|
||||
message.success('保存成功')
|
||||
setSaved(true)
|
||||
setTimeout(() => setSaved(false), 2000)
|
||||
@@ -602,6 +787,7 @@ export default function Settings() {
|
||||
{currentTab.sections.map((section) => (
|
||||
<ConfigSection key={section.title} section={section} />
|
||||
))}
|
||||
{activeTab === 'mailbox' ? <CFWorkerDomainPoolSection form={form} /> : null}
|
||||
<Button type="primary" icon={<SaveOutlined />} onClick={save} loading={saving} block>
|
||||
{saved ? '已保存 ✓' : '保存配置'}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user