Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,36 @@

7. 补充细节稳定性处理,尽量减少注册、订阅检测和账号管理过程中出现卡住或误判的情况。

### v1.1.1

1. 新增 `CloudMail` 邮箱服务实现,并完成服务注册、配置接入、邮件轮询、验证码提取和基础收件处理能力。

2. 新增上传目标 `newApi` 支持,可根据配置选择不同导入目标类型。

3. 新增 `Codex` 账号导出格式,支持后续登录、迁移和导入使用。

4. 新增 `CPA` 认证文件 `proxy_url` 支持,现可在 CPA 服务配置中保存和使用代理地址。

5. 优化 OAuth token 刷新兼容逻辑,完善异常返回与一次性令牌场景处理,降低刷新报错概率。

6. 优化批量验证流程,改为受控并发执行,减少长时间阻塞和卡死问题。

7. 修复模板渲染兼容问题,提升不同 Starlette 版本下页面渲染稳定性。

8. 修复六位数字误判为 OTP 的问题,避免邮箱域名或无关文本中的六位数字被错误识别为验证码。

9. 新增 Outlook 账户“注册状态”识别与展示功能,可直接看到“已注册/未注册”,并支持显示关联账号编号(如“已注册 #1”)。

10. 修复 Outlook 邮箱匹配大小写问题,避免 Outlook.com 因大小写差异被误判为未注册。

11. 修复 Outlook 列表列错位、乱码和占位文案问题,恢复中文显示并优化列表信息布局。

12. 优化 WebUI 端口冲突处理,默认端口占用时自动切换可用端口。

13. 增加启动时轻量字段迁移逻辑,自动补齐新增字段,提升旧数据升级兼容性。

14. 批量注册上限由 `100` 提升至 `1000`(前后端同步)。

## 核心能力

- Web UI 管理注册任务和账号数据
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "codex-console"
version = "1.1.0"
version = "1.1.1"
description = "OpenAI account management console"
requires-python = ">=3.10"
dependencies = [
Expand Down
3 changes: 2 additions & 1 deletion src/config/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,15 @@ class EmailServiceType(str, Enum):
DUCK_MAIL = "duck_mail"
FREEMAIL = "freemail"
IMAP_MAIL = "imap_mail"
CLOUDMAIL = "cloudmail"


# ============================================================================
# 应用常量
# ============================================================================

APP_NAME = "OpenAI/Codex CLI 自动注册系统"
APP_VERSION = "1.1.0"
APP_VERSION = "1.1.1"
APP_DESCRIPTION = "自动注册 OpenAI/Codex CLI 账号的系统"

# ============================================================================
Expand Down
2 changes: 2 additions & 0 deletions src/config/project_notice.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"qq_group_url": "https://qm.qq.com/q/4TETC3mWco",
"telegram_name": "codex_console",
"telegram_url": "https://t.me/codex_console",
"afdian_name": "dou-jiang",
"afdian_url": "https://afdian.com/a/dou-jiang",
}


Expand Down
4 changes: 2 additions & 2 deletions src/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class SettingDefinition:
),
"app_version": SettingDefinition(
db_key="app.version",
default_value="1.1.0",
default_value="1.1.1",
category=SettingCategory.GENERAL,
description="应用版本"
),
Expand Down Expand Up @@ -592,7 +592,7 @@ class Settings(BaseModel):

# 应用信息
app_name: str = "OpenAI/Codex CLI 自动注册系统"
app_version: str = "1.1.0"
app_version: str = "1.1.1"
debug: bool = False

# 数据库配置
Expand Down
25 changes: 25 additions & 0 deletions src/core/openai/token_refresh.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,31 @@ def _request_once(session: cffi_requests.Session):
session = self._create_session()
response = _request_once(session)

if response.status_code >= 400:
try:
error_payload = response.json()
except Exception:
error_payload = {}

error_text = str(error_payload.get("error") or "").lower()
error_description = str(error_payload.get("error_description") or response.text or "")
if response.status_code == 400 and error_text in {"invalid_grant", "unsupported_grant_type", "invalid_request"}:
fallback_data = {
"client_id": client_id,
"grant_type": "refresh_token",
"refresh_token": refresh_token,
}
response = session.post(
self.TOKEN_URL,
data=fallback_data,
headers={
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
},
timeout=30
)

# 代理通道触发地区/风控时,自动回退直连重试一次
if (
response.status_code in (401, 403)
Expand Down
6 changes: 4 additions & 2 deletions src/core/upload/sub2api_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def upload_to_sub2api(
api_key: str,
concurrency: int = 3,
priority: int = 50,
target_type: str = "sub2api",
) -> Tuple[bool, str]:
"""
上传账号列表到 Sub2API 平台(不走代理)
Expand Down Expand Up @@ -89,7 +90,7 @@ def upload_to_sub2api(

payload = {
"data": {
"type": "sub2api-data",
"type": "newapi-data" if str(target_type).lower() == "newapi" else "sub2api-data",
"version": 1,
"exported_at": exported_at,
"proxies": [],
Expand Down Expand Up @@ -138,6 +139,7 @@ def batch_upload_to_sub2api(
api_key: str,
concurrency: int = 3,
priority: int = 50,
target_type: str = "sub2api",
) -> dict:
"""
批量上传指定 ID 的账号到 Sub2API 平台
Expand Down Expand Up @@ -169,7 +171,7 @@ def batch_upload_to_sub2api(
if not accounts:
return results

success, message = upload_to_sub2api(accounts, api_url, api_key, concurrency, priority)
success, message = upload_to_sub2api(accounts, api_url, api_key, concurrency, priority, target_type)

if success:
for acc in accounts:
Expand Down
3 changes: 3 additions & 0 deletions src/database/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@ def create_cpa_service(
name: str,
api_url: str,
api_token: str,
proxy_url: Optional[str] = None,
enabled: bool = True,
priority: int = 0
) -> CpaService:
Expand All @@ -536,6 +537,7 @@ def create_cpa_service(
name=name,
api_url=api_url,
api_token=api_token,
proxy_url=proxy_url,
enabled=enabled,
priority=priority
)
Expand Down Expand Up @@ -597,6 +599,7 @@ def create_sub2api_service(
name: str,
api_url: str,
api_key: str,
target_type: str = 'sub2api',
enabled: bool = True,
priority: int = 0
) -> Sub2ApiService:
Expand Down
2 changes: 2 additions & 0 deletions src/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ class CpaService(Base):
name = Column(String(100), nullable=False) # 服务名称
api_url = Column(String(500), nullable=False) # API URL
api_token = Column(Text, nullable=False) # API Token
proxy_url = Column(String(1000)) # ?? URL
enabled = Column(Boolean, default=True)
priority = Column(Integer, default=0) # 优先级
created_at = Column(DateTime, default=datetime.utcnow)
Expand All @@ -211,6 +212,7 @@ class Sub2ApiService(Base):
name = Column(String(100), nullable=False) # 服务名称
api_url = Column(String(500), nullable=False) # API URL (host)
api_key = Column(Text, nullable=False) # x-api-key
target_type = Column(String(50), nullable=False, default='sub2api') # sub2api/newapi
enabled = Column(Boolean, default=True)
priority = Column(Integer, default=0) # 优先级
created_at = Column(DateTime, default=datetime.utcnow)
Expand Down
2 changes: 2 additions & 0 deletions src/database/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ def migrate_tables(self):
("accounts", "subscription_type", "VARCHAR(20)"),
("accounts", "subscription_at", "DATETIME"),
("accounts", "cookies", "TEXT"),
("cpa_services", "proxy_url", "VARCHAR(1000)"),
("sub2api_services", "target_type", "VARCHAR(50) DEFAULT 'sub2api'"),
("proxies", "is_default", "BOOLEAN DEFAULT 0"),
("bind_card_tasks", "checkout_session_id", "VARCHAR(120)"),
("bind_card_tasks", "publishable_key", "VARCHAR(255)"),
Expand Down
3 changes: 3 additions & 0 deletions src/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from .duck_mail import DuckMailService
from .freemail import FreemailService
from .imap_mail import ImapMailService
from .cloudmail import CloudMailService

# 注册服务
EmailServiceFactory.register(EmailServiceType.TEMPMAIL, TempmailService)
Expand All @@ -26,6 +27,7 @@
EmailServiceFactory.register(EmailServiceType.DUCK_MAIL, DuckMailService)
EmailServiceFactory.register(EmailServiceType.FREEMAIL, FreemailService)
EmailServiceFactory.register(EmailServiceType.IMAP_MAIL, ImapMailService)
EmailServiceFactory.register(EmailServiceType.CLOUDMAIL, CloudMailService)

# 导出 Outlook 模块的额外内容
from .outlook.base import (
Expand Down Expand Up @@ -59,6 +61,7 @@
'DuckMailService',
'FreemailService',
'ImapMailService',
'CloudMailService',
# Outlook 模块
'ProviderType',
'EmailMessage',
Expand Down
37 changes: 37 additions & 0 deletions src/services/cloudmail.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
CloudMail ??????
"""

from typing import Any, Dict, List, Optional

from .temp_mail import TempMailService
from .base import EmailServiceType


class CloudMailService(TempMailService):
"""?? CloudMail ????????"""

def __init__(self, config: Dict[str, Any] = None, name: str = None):
normalized = dict(config or {})
normalized.setdefault("enable_prefix", True)
super().__init__(normalized, name)
self.service_type = EmailServiceType.CLOUDMAIL

def list_emails(self, limit: int = 100, offset: int = 0, **kwargs) -> List[Dict[str, Any]]:
return super().list_emails(limit=limit, offset=offset, **kwargs)

def get_verification_code(
self,
email: str,
email_id: str = None,
timeout: int = 120,
pattern: str = r"(?<!\d)(\d{6})(?!\d)",
otp_sent_at: Optional[float] = None,
) -> Optional[str]:
return super().get_verification_code(
email=email,
email_id=email_id,
timeout=timeout,
pattern=pattern,
otp_sent_at=otp_sent_at,
)
6 changes: 6 additions & 0 deletions src/services/temp_mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
from ..core.http_client import HTTPClient, RequestConfig
from ..config.constants import OTP_CODE_PATTERN, OTP_CODE_SEMANTIC_PATTERN

OTP_DOMAIN_PATTERN = re.compile(r"@[A-Za-z0-9.-]+\.\d{6}(?!\d)")


logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -747,6 +749,10 @@ def get_verification_code(
reverse=True,
)[0]
code = str(best["code"])
if OTP_DOMAIN_PATTERN.search(str(best.get("detail_content") or "")) and code in str(best.get("detail_content") or ""):
logger.debug("??????????????????? OTP")
time.sleep(3)
continue
self._last_used_mail_ids[email] = str(best["mail_id"])
logger.info(
"从 TempMail 邮箱 %s 找到验证码: %s(mail_id=%s ts=%s semantic=%s)",
Expand Down
43 changes: 42 additions & 1 deletion src/web/routes/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
账号管理 API 路由
"""
import io
import asyncio
import json
import logging
import re
Expand Down Expand Up @@ -1636,6 +1637,42 @@ def make_account_entry(acc) -> dict:
)


@router.post("/export/codex")
async def export_accounts_codex(request: BatchExportRequest):
"""????? Codex ???????"""
with get_db() as db:
ids = resolve_account_ids(
db, request.ids, request.select_all,
request.status_filter, request.email_service_filter, request.search_filter
)
accounts = db.query(Account).filter(Account.id.in_(ids)).all()

timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
lines = []
for acc in accounts:
lines.append(json.dumps({
"email": acc.email,
"password": acc.password or "",
"client_id": acc.client_id or "",
"access_token": acc.access_token or "",
"refresh_token": acc.refresh_token or "",
"session_token": acc.session_token or "",
"account_id": acc.account_id or "",
"workspace_id": acc.workspace_id or "",
"cookies": acc.cookies or "",
"type": "codex",
"source": getattr(acc, "source", None) or "manual",
}, ensure_ascii=False))

content = "\n".join(lines)
filename = f"codex_accounts_{timestamp}.jsonl"
return StreamingResponse(
iter([content]),
media_type="application/x-ndjson",
headers={"Content-Disposition": f"attachment; filename={filename}"}
)


@router.post("/export/cpa")
async def export_accounts_cpa(request: BatchExportRequest):
"""导出账号为 CPA Token JSON 格式(每个账号单独一个 JSON 文件,打包为 ZIP)"""
Expand Down Expand Up @@ -2068,6 +2105,7 @@ async def batch_upload_accounts_to_sub2api(request: BatchSub2ApiUploadRequest):
ids, api_url, api_key,
concurrency=request.concurrency,
priority=request.priority,
target_type=locals().get("target_type", "sub2api"),
)
return results

Expand Down Expand Up @@ -2108,7 +2146,8 @@ async def upload_account_to_sub2api(account_id: int, request: Optional[Sub2ApiUp

success, message = upload_to_sub2api(
[account], api_url, api_key,
concurrency=concurrency, priority=priority
concurrency=concurrency, priority=priority,
target_type=locals().get("target_type", "sub2api")
)
if success:
return {"success": True, "message": message}
Expand Down Expand Up @@ -2147,6 +2186,7 @@ async def batch_upload_accounts_to_tm(request: BatchUploadTMRequest):

api_url = svc.api_url
api_key = svc.api_key
target_type = getattr(svc, "target_type", "sub2api")

ids = resolve_account_ids(
db, request.ids, request.select_all,
Expand Down Expand Up @@ -2175,6 +2215,7 @@ async def upload_account_to_tm(account_id: int, request: Optional[UploadTMReques

api_url = svc.api_url
api_key = svc.api_key
target_type = getattr(svc, "target_type", "sub2api")

account = crud.get_account_by_id(db, account_id)
if not account:
Expand Down
Loading
Loading