From 52ab4a569f40e7b126f9feb8704caba6b16616c8 Mon Sep 17 00:00:00 2001 From: Mison Date: Thu, 2 Apr 2026 14:45:49 +0800 Subject: [PATCH 1/7] Add configurable registration wait strategy --- .gitignore | 2 + docker-compose.dev.yml | 21 +++++ docker-compose.yml | 4 +- src/config/constants.py | 14 +++ src/config/settings.py | 8 ++ src/web/routes/registration.py | 55 ++++++++++- src/web/routes/settings.py | 12 +++ static/js/settings.js | 2 + templates/settings.html | 12 ++- tests/test_registration_wait_strategy.py | 91 +++++++++++++++++++ .../test_settings_registration_auto_fields.py | 19 ++++ 11 files changed, 233 insertions(+), 7 deletions(-) create mode 100644 docker-compose.dev.yml create mode 100644 tests/test_registration_wait_strategy.py diff --git a/.gitignore b/.gitignore index c93cf6e2..e293c2f0 100644 --- a/.gitignore +++ b/.gitignore @@ -36,7 +36,9 @@ uv.lock # Data and Logs data/ +data-dev/ logs/ +logs-dev/ *.db *.sqlite *.sqlite3 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 00000000..7825e8a0 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,21 @@ +services: + webui: + build: . + shm_size: "1gb" + ports: + - "16666:1455" + - "6081:6080" + environment: + - WEBUI_HOST=0.0.0.0 + - WEBUI_PORT=1455 + - DISPLAY=:99 + - ENABLE_VNC=1 + - VNC_PORT=5900 + - NOVNC_PORT=6080 + - DEBUG=0 + - LOG_LEVEL=info + - WEBUI_ACCESS_PASSWORD=admin123 + volumes: + - ./data-dev:/app/data + - ./logs-dev:/app/logs + restart: unless-stopped diff --git a/docker-compose.yml b/docker-compose.yml index 95aa46e6..509ce851 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,9 @@ -version: '3.8' - services: webui: build: . shm_size: "1gb" ports: - - "1455:1455" + - "15555:1455" - "6080:6080" # 如需使用 VNC 客户端直连可打开: # - "5900:5900" diff --git a/src/config/constants.py b/src/config/constants.py index fc8f7212..82dc4055 100644 --- a/src/config/constants.py +++ b/src/config/constants.py @@ -34,6 +34,12 @@ class RoleTag(str, Enum): CHILD = "child" +class RegistrationWaitStrategy(str, Enum): + """批量注册等待策略""" + START = "start" + COMPLETION = "completion" + + class PoolState(str, Enum): """账号池状态""" TEAM_POOL = "team_pool" @@ -84,6 +90,14 @@ def normalize_role_tag(value: str) -> str: return RoleTag.NONE.value +def normalize_registration_wait_strategy(value: str) -> str: + """标准化批量等待策略,未知值降级为 start。""" + text = str(value or "").strip().lower() + if text == RegistrationWaitStrategy.COMPLETION.value: + return RegistrationWaitStrategy.COMPLETION.value + return RegistrationWaitStrategy.START.value + + def normalize_pool_state(value: str) -> str: """标准化池状态,未知值降级为 candidate_pool。""" text = str(value or "").strip().lower() diff --git a/src/config/settings.py b/src/config/settings.py index 4d9f9845..b683255f 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -308,6 +308,12 @@ class SettingDefinition: category=SettingCategory.REGISTRATION, description="注册间隔最大值(秒)" ), + "registration_wait_strategy": SettingDefinition( + db_key="registration.wait_strategy", + default_value="start", + category=SettingCategory.REGISTRATION, + description="批量注册等待策略(start=启动间隔, completion=完成间隔)" + ), "registration_entry_flow": SettingDefinition( db_key="registration.entry_flow", default_value="native", @@ -585,6 +591,7 @@ class SettingDefinition: "registration_default_password_length": int, "registration_sleep_min": int, "registration_sleep_max": int, + "registration_wait_strategy": str, "registration_entry_flow": str, "registration_auto_enabled": bool, "registration_auto_check_interval": int, @@ -874,6 +881,7 @@ def proxy_url(self) -> Optional[str]: registration_default_password_length: int = 12 registration_sleep_min: int = 5 registration_sleep_max: int = 30 + registration_wait_strategy: str = "start" registration_entry_flow: str = "native" registration_auto_enabled: bool = False registration_auto_check_interval: int = 60 diff --git a/src/web/routes/registration.py b/src/web/routes/registration.py index bd0a9d78..021509d8 100644 --- a/src/web/routes/registration.py +++ b/src/web/routes/registration.py @@ -14,7 +14,9 @@ from ...config.constants import ( RoleTag, + RegistrationWaitStrategy, normalize_role_tag, + normalize_registration_wait_strategy, role_tag_to_account_label, ) from ...database import crud @@ -43,6 +45,20 @@ batch_tasks: Dict[str, dict] = {} +def _wait_strategy_label(strategy: str) -> str: + normalized = normalize_registration_wait_strategy(strategy) + if normalized == RegistrationWaitStrategy.COMPLETION.value: + return "完成间隔" + return "启动间隔" + + +def _current_wait_strategy(settings: Optional[Settings] = None) -> str: + current_settings = settings or get_settings() + return normalize_registration_wait_strategy( + getattr(current_settings, "registration_wait_strategy", RegistrationWaitStrategy.START.value) + ) + + def _cancel_batch_tasks(batch_id: str) -> None: batch = batch_tasks.get(batch_id) if not batch: @@ -968,6 +984,7 @@ async def run_batch_pipeline( interval_min: int, interval_max: int, concurrency: int, + wait_strategy: str = RegistrationWaitStrategy.START.value, auto_upload_cpa: bool = False, cpa_service_ids: List[int] = None, auto_upload_sub2api: bool = False, @@ -986,7 +1003,11 @@ async def run_batch_pipeline( semaphore = asyncio.Semaphore(concurrency) counter_lock = asyncio.Lock() running_tasks_list = [] - add_batch_log(f"[系统] 流水线模式启动,并发数: {concurrency},总任务: {len(task_uuids)}") + launch_state = {"launched": 0} + normalized_wait_strategy = normalize_registration_wait_strategy(wait_strategy) + add_batch_log( + f"[系统] 流水线模式启动,并发数: {concurrency},总任务: {len(task_uuids)},等待策略: {_wait_strategy_label(normalized_wait_strategy)}" + ) async def _run_and_release(idx: int, uuid: str, pfx: str): try: @@ -1014,6 +1035,21 @@ async def _run_and_release(idx: int, uuid: str, pfx: str): add_batch_log(f"{pfx} [失败] 注册失败: {t.error_message}") update_batch_status(completed=new_completed, success=new_success, failed=new_failed) finally: + should_delay_release = ( + normalized_wait_strategy == RegistrationWaitStrategy.COMPLETION.value + and launch_state["launched"] < len(task_uuids) + and not task_manager.is_batch_cancelled(batch_id) + ) + if should_delay_release: + wait_time = random.randint(interval_min, interval_max) + add_batch_log(f"{pfx} 完成后等待 {wait_time} 秒,再启动后续任务") + logger.info( + "批量任务 %s: 任务 %s 完成后等待 %s 秒再释放并发槽", + batch_id, + uuid, + wait_time, + ) + await asyncio.sleep(wait_time) semaphore.release() try: @@ -1035,9 +1071,15 @@ async def _run_and_release(idx: int, uuid: str, pfx: str): add_batch_log(f"{prefix} 开始注册...") t = asyncio.create_task(_run_and_release(i, task_uuid, prefix)) running_tasks_list.append(t) + launch_state["launched"] = i + 1 - if i < len(task_uuids) - 1 and not task_manager.is_batch_cancelled(batch_id): + if ( + normalized_wait_strategy == RegistrationWaitStrategy.START.value + and i < len(task_uuids) - 1 + and not task_manager.is_batch_cancelled(batch_id) + ): wait_time = random.randint(interval_min, interval_max) + add_batch_log(f"[系统] 等待 {wait_time} 秒后启动下一个任务") logger.info(f"批量任务 {batch_id}: 等待 {wait_time} 秒后启动下一个任务") await asyncio.sleep(wait_time) @@ -1087,6 +1129,7 @@ async def run_batch_registration( interval_max: int, concurrency: int = 1, mode: str = "pipeline", + wait_strategy: str = RegistrationWaitStrategy.START.value, auto_upload_cpa: bool = False, cpa_service_ids: List[int] = None, auto_upload_sub2api: bool = False, @@ -1113,6 +1156,7 @@ async def run_batch_registration( batch_id, task_uuids, email_service_type, proxy, email_service_config, email_service_id, interval_min, interval_max, concurrency, + wait_strategy=wait_strategy, auto_upload_cpa=auto_upload_cpa, cpa_service_ids=cpa_service_ids, auto_upload_sub2api=auto_upload_sub2api, sub2api_service_ids=sub2api_service_ids, auto_upload_tm=auto_upload_tm, tm_service_ids=tm_service_ids, @@ -1135,6 +1179,7 @@ async def run_auto_registration_batch(plan, settings: Settings) -> str: interval_min = max(0, int(settings.registration_auto_interval_min)) interval_max = max(interval_min, int(settings.registration_auto_interval_max)) concurrency = max(1, int(settings.registration_auto_concurrency)) + wait_strategy = _current_wait_strategy(settings) email_service_id = int(settings.registration_auto_email_service_id or 0) or None proxy = settings.registration_auto_proxy.strip() or None @@ -1178,6 +1223,7 @@ async def run_auto_registration_batch(plan, settings: Settings) -> str: interval_max=interval_max, concurrency=concurrency, mode=mode, + wait_strategy=wait_strategy, auto_upload_cpa=True, cpa_service_ids=[plan.cpa_service_id], auto_upload_sub2api=False, @@ -1339,6 +1385,7 @@ async def _start_batch_registration_internal( request.interval_max, request.concurrency, request.mode, + _current_wait_strategy(), request.auto_upload_cpa, request.cpa_service_ids, request.auto_upload_sub2api, @@ -1432,6 +1479,7 @@ async def _start_outlook_batch_registration_internal( request.interval_max, request.concurrency, request.mode, + _current_wait_strategy(), request.auto_upload_cpa, request.cpa_service_ids, request.auto_upload_sub2api, @@ -2084,6 +2132,7 @@ async def run_outlook_batch_registration( interval_max: int, concurrency: int = 1, mode: str = "pipeline", + wait_strategy: str = RegistrationWaitStrategy.START.value, auto_upload_cpa: bool = False, cpa_service_ids: List[int] = None, auto_upload_sub2api: bool = False, @@ -2129,6 +2178,7 @@ async def run_outlook_batch_registration( interval_max=interval_max, concurrency=concurrency, mode=mode, + wait_strategy=wait_strategy, auto_upload_cpa=auto_upload_cpa, cpa_service_ids=cpa_service_ids, auto_upload_sub2api=auto_upload_sub2api, @@ -2371,4 +2421,3 @@ async def delete_scheduled_registration_job(job_uuid: str): raise HTTPException(status_code=400, detail="无法删除执行中的计划任务") crud.delete_scheduled_registration_job(db, job_uuid) return {'success': True, 'message': '计划任务已删除'} - diff --git a/src/web/routes/settings.py b/src/web/routes/settings.py index 2f34acf6..e012ecd0 100644 --- a/src/web/routes/settings.py +++ b/src/web/routes/settings.py @@ -10,6 +10,7 @@ from pydantic import BaseModel from ...config.settings import get_settings, update_settings +from ...config.constants import RegistrationWaitStrategy, normalize_registration_wait_strategy from ...core.auto_registration import ( trigger_auto_registration_check, update_auto_registration_state, @@ -54,6 +55,7 @@ class RegistrationSettings(BaseModel): default_password_length: int = 12 sleep_min: int = 5 sleep_max: int = 30 + wait_strategy: str = RegistrationWaitStrategy.START.value entry_flow: str = "native" auto_enabled: bool = False auto_check_interval: int = 60 @@ -99,6 +101,7 @@ async def get_all_settings(): entry_flow_raw = str(settings.registration_entry_flow or "native").strip().lower() entry_flow = "abcard" if entry_flow_raw == "abcard" else "native" + wait_strategy = normalize_registration_wait_strategy(getattr(settings, "registration_wait_strategy", "start")) return { "proxy": { @@ -120,6 +123,7 @@ async def get_all_settings(): "default_password_length": settings.registration_default_password_length, "sleep_min": settings.registration_sleep_min, "sleep_max": settings.registration_sleep_max, + "wait_strategy": wait_strategy, "entry_flow": entry_flow, "auto_enabled": settings.registration_auto_enabled, "auto_check_interval": settings.registration_auto_check_interval, @@ -310,6 +314,7 @@ async def get_registration_settings(): entry_flow_raw = str(settings.registration_entry_flow or "native").strip().lower() entry_flow = "abcard" if entry_flow_raw == "abcard" else "native" + wait_strategy = normalize_registration_wait_strategy(getattr(settings, "registration_wait_strategy", "start")) return { "max_retries": settings.registration_max_retries, @@ -317,6 +322,7 @@ async def get_registration_settings(): "default_password_length": settings.registration_default_password_length, "sleep_min": settings.registration_sleep_min, "sleep_max": settings.registration_sleep_max, + "wait_strategy": wait_strategy, "entry_flow": entry_flow, "auto_enabled": settings.registration_auto_enabled, "auto_check_interval": settings.registration_auto_check_interval, @@ -344,6 +350,11 @@ async def update_registration_settings(request: RegistrationSettings): if request.sleep_min < 1 or request.sleep_max < request.sleep_min: raise HTTPException(status_code=400, detail="注册等待时间参数无效") + wait_strategy_raw = str(request.wait_strategy or RegistrationWaitStrategy.START.value).strip().lower() + if wait_strategy_raw not in {RegistrationWaitStrategy.START.value, RegistrationWaitStrategy.COMPLETION.value}: + raise HTTPException(status_code=400, detail="等待策略仅支持 start / completion") + wait_strategy = normalize_registration_wait_strategy(wait_strategy_raw) + flow_raw = (request.entry_flow or "native").strip().lower() # 兼容旧前端历史值:outlook -> native(Outlook 邮箱会在运行时自动走 outlook 链路)。 flow = "native" if flow_raw == "outlook" else flow_raw @@ -399,6 +410,7 @@ async def update_registration_settings(request: RegistrationSettings): registration_default_password_length=request.default_password_length, registration_sleep_min=request.sleep_min, registration_sleep_max=request.sleep_max, + registration_wait_strategy=wait_strategy, registration_entry_flow=flow, registration_auto_enabled=request.auto_enabled, registration_auto_check_interval=request.auto_check_interval, diff --git a/static/js/settings.js b/static/js/settings.js index 95a0e508..24642a03 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -397,6 +397,7 @@ async function loadSettings() { const entryFlowRaw = String(data.registration?.entry_flow || 'native').toLowerCase(); const entryFlow = entryFlowRaw === 'abcard' ? 'abcard' : 'native'; document.getElementById('registration-entry-flow').value = entryFlow; + document.getElementById('registration-wait-strategy').value = data.registration?.wait_strategy || 'start'; document.getElementById('sleep-min').value = data.registration?.sleep_min || 5; document.getElementById('sleep-max').value = data.registration?.sleep_max || 30; @@ -554,6 +555,7 @@ async function handleSaveRegistration(e) { timeout: parseInt(document.getElementById('timeout').value), default_password_length: parseInt(document.getElementById('password-length').value), entry_flow: document.getElementById('registration-entry-flow').value || 'native', + wait_strategy: document.getElementById('registration-wait-strategy').value || 'start', sleep_min: parseInt(document.getElementById('sleep-min').value), sleep_max: parseInt(document.getElementById('sleep-max').value), }; diff --git a/templates/settings.html b/templates/settings.html index 65e71d4d..70c82870 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -634,6 +634,17 @@

注册配置

+
+
+ + + 启动间隔会在启动新任务前等待;完成间隔会在前一任务完成后等待再复用并发槽 +
+
+
@@ -746,4 +757,3 @@

数据库信息

- diff --git a/tests/test_registration_wait_strategy.py b/tests/test_registration_wait_strategy.py new file mode 100644 index 00000000..4f31a39a --- /dev/null +++ b/tests/test_registration_wait_strategy.py @@ -0,0 +1,91 @@ +import asyncio +from contextlib import contextmanager +from types import SimpleNamespace + +from src.config.constants import RegistrationWaitStrategy +from src.web.routes import registration as registration_routes + + +@contextmanager +def _fake_get_db(): + yield None + + +def _patch_batch_dependencies(monkeypatch): + monkeypatch.setattr(registration_routes, "get_db", _fake_get_db) + monkeypatch.setattr( + registration_routes.crud, + "get_registration_task", + lambda db, uuid: SimpleNamespace(status="completed", error_message=None), + ) + monkeypatch.setattr(registration_routes.task_manager, "init_batch", lambda batch_id, total: None) + monkeypatch.setattr(registration_routes.task_manager, "add_batch_log", lambda batch_id, message: None) + monkeypatch.setattr(registration_routes.task_manager, "update_batch_status", lambda batch_id, **kwargs: None) + monkeypatch.setattr(registration_routes.task_manager, "is_batch_cancelled", lambda batch_id: False) + + +def _run_pipeline(monkeypatch, wait_strategy: str, wait_seconds: int): + _patch_batch_dependencies(monkeypatch) + registration_routes.batch_tasks.clear() + + events = [] + sleep_calls = [] + real_sleep = asyncio.sleep + + async def fake_run_registration_task(uuid, *args, **kwargs): + events.append(("run", uuid)) + + async def fake_sleep(seconds): + sleep_calls.append(seconds) + await real_sleep(0) + + monkeypatch.setattr(registration_routes, "run_registration_task", fake_run_registration_task) + monkeypatch.setattr(registration_routes.random, "randint", lambda low, high: wait_seconds) + monkeypatch.setattr(registration_routes.asyncio, "sleep", fake_sleep) + + batch_id = f"batch-{wait_strategy}" + asyncio.run( + registration_routes.run_batch_pipeline( + batch_id=batch_id, + task_uuids=["task-1", "task-2"], + email_service_type="tempmail", + proxy=None, + email_service_config=None, + email_service_id=None, + interval_min=wait_seconds, + interval_max=wait_seconds, + concurrency=1, + wait_strategy=wait_strategy, + ) + ) + logs = list(registration_routes.batch_tasks[batch_id]["logs"]) + registration_routes.batch_tasks.clear() + return events, sleep_calls, logs + + +def test_run_batch_pipeline_start_wait_strategy(monkeypatch): + events, sleep_calls, logs = _run_pipeline( + monkeypatch, + RegistrationWaitStrategy.START.value, + 7, + ) + + assert events == [("run", "task-1"), ("run", "task-2")] + assert sleep_calls == [7] + assert any("等待策略: 启动间隔" in log for log in logs) + assert any("[系统] 等待 7 秒后启动下一个任务" in log for log in logs) + assert not any("完成后等待 7 秒" in log for log in logs) + + +def test_run_batch_pipeline_completion_wait_strategy(monkeypatch): + events, sleep_calls, logs = _run_pipeline( + monkeypatch, + RegistrationWaitStrategy.COMPLETION.value, + 9, + ) + + assert events == [("run", "task-1"), ("run", "task-2")] + assert sleep_calls == [9] + assert any("等待策略: 完成间隔" in log for log in logs) + assert any("[任务1] 完成后等待 9 秒,再启动后续任务" in log for log in logs) + assert not any("[系统] 等待 9 秒后启动下一个任务" in log for log in logs) diff --git a/tests/test_settings_registration_auto_fields.py b/tests/test_settings_registration_auto_fields.py index cb07e7b9..10ff752b 100644 --- a/tests/test_settings_registration_auto_fields.py +++ b/tests/test_settings_registration_auto_fields.py @@ -13,6 +13,7 @@ class DummySettings: registration_default_password_length = 12 registration_sleep_min = 5 registration_sleep_max = 30 + registration_wait_strategy = "completion" registration_entry_flow = "abcard" registration_auto_enabled = True registration_auto_check_interval = 90 @@ -33,6 +34,7 @@ def test_get_registration_settings_includes_auto_fields(monkeypatch): result = asyncio.run(settings_routes.get_registration_settings()) assert result["entry_flow"] == "abcard" + assert result["wait_strategy"] == "completion" assert result["auto_enabled"] is True assert result["auto_check_interval"] == 90 assert result["auto_min_ready_auth_files"] == 3 @@ -99,6 +101,7 @@ def fake_get_db(): default_password_length=16, sleep_min=7, sleep_max=15, + wait_strategy="completion", entry_flow="abcard", auto_enabled=True, auto_check_interval=120, @@ -119,6 +122,7 @@ def fake_get_db(): assert len(update_calls) == 1 payload = update_calls[0] assert payload["registration_entry_flow"] == "abcard" + assert payload["registration_wait_strategy"] == "completion" assert payload["registration_auto_enabled"] is True assert payload["registration_auto_check_interval"] == 120 assert payload["registration_auto_min_ready_auth_files"] == 5 @@ -147,3 +151,18 @@ def test_update_registration_settings_rejects_missing_cpa_when_enabled(): assert "必须选择一个 CPA 服务" in exc.detail else: raise AssertionError("expected HTTPException for missing CPA service") + + +def test_update_registration_settings_rejects_invalid_wait_strategy(): + request = settings_routes.RegistrationSettings( + auto_enabled=False, + wait_strategy="invalid", + ) + + try: + asyncio.run(settings_routes.update_registration_settings(request)) + except settings_routes.HTTPException as exc: + assert exc.status_code == 400 + assert "等待策略" in exc.detail + else: + raise AssertionError("expected HTTPException for invalid wait strategy") From 2f2a79975632d9bcae1b281b2c6259972fcb3575 Mon Sep 17 00:00:00 2001 From: Mison Date: Thu, 2 Apr 2026 14:46:47 +0800 Subject: [PATCH 2/7] Remove unrelated docker compose change --- docker-compose.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 509ce851..95aa46e6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,11 @@ +version: '3.8' + services: webui: build: . shm_size: "1gb" ports: - - "15555:1455" + - "1455:1455" - "6080:6080" # 如需使用 VNC 客户端直连可打开: # - "5900:5900" From 4e6d11b22b621231c91a6913b1fe2f2db88190fa Mon Sep 17 00:00:00 2001 From: Mison Date: Thu, 2 Apr 2026 15:00:28 +0800 Subject: [PATCH 3/7] Clarify registration wait mode in settings UI --- static/js/settings.js | 19 +++++++++++++++++++ templates/settings.html | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/static/js/settings.js b/static/js/settings.js index 24642a03..de5c9816 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -133,6 +133,11 @@ function initEventListeners() { if (elements.registrationForm) { elements.registrationForm.addEventListener('submit', handleSaveRegistration); } + const registrationWaitStrategyEl = document.getElementById('registration-wait-strategy'); + if (registrationWaitStrategyEl) { + registrationWaitStrategyEl.addEventListener('change', updateRegistrationWaitStrategyHint); + updateRegistrationWaitStrategyHint(); + } // 备份数据库 if (elements.backupBtn) { @@ -398,6 +403,7 @@ async function loadSettings() { const entryFlow = entryFlowRaw === 'abcard' ? 'abcard' : 'native'; document.getElementById('registration-entry-flow').value = entryFlow; document.getElementById('registration-wait-strategy').value = data.registration?.wait_strategy || 'start'; + updateRegistrationWaitStrategyHint(); document.getElementById('sleep-min').value = data.registration?.sleep_min || 5; document.getElementById('sleep-max').value = data.registration?.sleep_max || 30; @@ -432,6 +438,19 @@ async function loadSettings() { } } +function updateRegistrationWaitStrategyHint() { + const selectEl = document.getElementById('registration-wait-strategy'); + const statusEl = document.getElementById('registration-wait-strategy-status'); + if (!selectEl || !statusEl) return; + + if (selectEl.value === 'completion') { + statusEl.textContent = '当前模式:完成间隔。每个任务完成后,会等待随机秒数,再启动后续任务。'; + return; + } + + statusEl.textContent = '当前模式:启动间隔。每次启动新任务前,会等待随机秒数;如果前一任务本身更慢,后续任务可能在完成后立即接上。'; +} + // 保存系统设置(端口 + 访问控制) async function handleSaveSystemSettings(e) { e.preventDefault(); diff --git a/templates/settings.html b/templates/settings.html index 70c82870..6e923d02 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -642,6 +642,7 @@

注册配置

启动间隔会在启动新任务前等待;完成间隔会在前一任务完成后等待再复用并发槽 +
@@ -756,4 +757,3 @@

数据库信息

- From cb1fc2ffd8a3b3c77cdef6f00daf0f2e051cad35 Mon Sep 17 00:00:00 2001 From: Mison Date: Thu, 2 Apr 2026 15:07:08 +0800 Subject: [PATCH 4/7] Show wait strategy on registration page --- static/js/app.js | 37 +++++++++++++++++++++++++++++++++++++ templates/index.html | 3 ++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/static/js/app.js b/static/js/app.js index 2bc40773..c9882ac5 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -52,6 +52,7 @@ let batchWsReconnectAttempts = 0; let wsManualClose = false; let batchWsManualClose = false; let autoMonitorLastLogIndex = 0; +let registrationWaitStrategy = 'start'; const WS_RECONNECT_BASE_DELAY = 1000; const WS_RECONNECT_MAX_DELAY = 10000; @@ -110,11 +111,13 @@ const elements = { outlookConcurrencyMode: document.getElementById('outlook-concurrency-mode'), outlookConcurrencyCount: document.getElementById('outlook-concurrency-count'), outlookConcurrencyHint: document.getElementById('outlook-concurrency-hint'), + outlookWaitStrategyHint: document.getElementById('outlook-wait-strategy-hint'), outlookIntervalGroup: document.getElementById('outlook-interval-group'), // 批量并发控件 concurrencyMode: document.getElementById('concurrency-mode'), concurrencyCount: document.getElementById('concurrency-count'), concurrencyHint: document.getElementById('concurrency-hint'), + waitStrategyHint: document.getElementById('wait-strategy-hint'), intervalGroup: document.getElementById('interval-group'), // 注册后自动操作 autoUploadCpa: document.getElementById('auto-upload-cpa'), @@ -162,6 +165,7 @@ const elements = { document.addEventListener('DOMContentLoaded', () => { initEventListeners(); handleModeChange({ target: elements.regMode }); + updateRegistrationWaitStrategyHints(); loadAvailableServices(); loadRecentAccounts(); loadAutoRegistrationSettings(); @@ -297,9 +301,11 @@ function initEventListeners() { // 并发模式切换 elements.concurrencyMode.addEventListener('change', () => { handleConcurrencyModeChange(elements.concurrencyMode, elements.concurrencyHint, elements.intervalGroup); + updateRegistrationWaitStrategyHints(); }); elements.outlookConcurrencyMode.addEventListener('change', () => { handleConcurrencyModeChange(elements.outlookConcurrencyMode, elements.outlookConcurrencyHint, elements.outlookIntervalGroup); + updateRegistrationWaitStrategyHints(); }); if (elements.refreshSchedulesBtn) { @@ -546,6 +552,7 @@ function handleServiceChange(e) { elements.regModeGroup.style.display = 'none'; elements.batchCountGroup.style.display = 'none'; elements.batchOptions.style.display = 'none'; + updateRegistrationWaitStrategyHints(); loadOutlookAccounts(); addLog('info', '[系统] 已切换到 Outlook 批量注册模式'); return; @@ -553,6 +560,7 @@ function handleServiceChange(e) { isOutlookBatchMode = false; elements.outlookBatchSection.style.display = 'none'; elements.regModeGroup.style.display = 'block'; + updateRegistrationWaitStrategyHints(); } // 显示服务信息 @@ -612,6 +620,7 @@ function handleModeChange(e) { elements.batchCountGroup.style.display = isBatchMode ? 'block' : 'none'; elements.batchOptions.style.display = isBatchMode ? 'block' : 'none'; + updateRegistrationWaitStrategyHints(); if (elements.autoRegistrationSection) { elements.autoRegistrationSection.style.display = isAutoMode ? 'block' : 'none'; } @@ -645,6 +654,32 @@ function handleConcurrencyModeChange(selectEl, hintEl, intervalGroupEl) { } } +function updateRegistrationWaitStrategyHints() { + const strategyLabel = registrationWaitStrategy === 'completion' ? '完成间隔' : '启动间隔'; + const normalMode = elements.concurrencyMode?.value || 'pipeline'; + const outlookMode = elements.outlookConcurrencyMode?.value || 'pipeline'; + + if (elements.waitStrategyHint) { + if (normalMode === 'parallel') { + elements.waitStrategyHint.textContent = `当前全局等待策略:${strategyLabel}。当前并行模式下不会使用间隔等待。`; + } else if (registrationWaitStrategy === 'completion') { + elements.waitStrategyHint.textContent = '当前全局等待策略:完成间隔。前一个任务完成后,会等待随机秒数,再启动后续任务。'; + } else { + elements.waitStrategyHint.textContent = '当前全局等待策略:启动间隔。启动新任务前会等待随机秒数;如果前一任务更慢,后续任务可能在完成后立即接上。'; + } + } + + if (elements.outlookWaitStrategyHint) { + if (outlookMode === 'parallel') { + elements.outlookWaitStrategyHint.textContent = `当前全局等待策略:${strategyLabel}。当前并行模式下不会使用间隔等待。`; + } else if (registrationWaitStrategy === 'completion') { + elements.outlookWaitStrategyHint.textContent = '当前全局等待策略:完成间隔。前一个任务完成后,会等待随机秒数,再启动后续任务。'; + } else { + elements.outlookWaitStrategyHint.textContent = '当前全局等待策略:启动间隔。启动新任务前会等待随机秒数;如果前一任务更慢,后续任务可能在完成后立即接上。'; + } + } +} + function initScheduleForm() { if (!elements.scheduleForm) return; if (!elements.scheduleStartDate.value) { @@ -1034,6 +1069,7 @@ async function loadAutoRegistrationSettings() { try { const data = await api.get('/settings'); const reg = data.registration || {}; + registrationWaitStrategy = reg.wait_strategy || 'start'; elements.autoRegistrationEnabled.checked = reg.auto_enabled || false; elements.autoRegistrationCheckInterval.value = reg.auto_check_interval || 60; elements.autoRegistrationMinReady.value = reg.auto_min_ready_auth_files || 1; @@ -1048,6 +1084,7 @@ async function loadAutoRegistrationSettings() { elements.concurrencyHint, elements.autoRegistrationIntervalGroup ); + updateRegistrationWaitStrategyHints(); elements.autoRegistrationEmailServiceId.dataset.selectedId = String(reg.auto_email_service_id || 0); elements.autoRegistrationCpaServiceId.dataset.selectedId = String(reg.auto_cpa_service_id || 0); populateAutoRegistrationEmailServiceOptions(reg.auto_email_service_id || 0); diff --git a/templates/index.html b/templates/index.html index 5c0f399f..7fe263e9 100644 --- a/templates/index.html +++ b/templates/index.html @@ -426,6 +426,7 @@

📝 注册设置

同时最多运行 N 个任务,每隔 interval 秒启动新任务 +
@@ -481,6 +482,7 @@

📝 注册设置

同时最多运行 N 个任务,每隔 interval 秒启动新任务 +
@@ -841,4 +843,3 @@

📋 已注册账号

- From 61d63a01fc24b975b88cc43029e794ce017c3fa6 Mon Sep 17 00:00:00 2001 From: Mison Date: Thu, 2 Apr 2026 15:20:53 +0800 Subject: [PATCH 5/7] Bold wait strategy prefix on registration page --- static/js/app.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/static/js/app.js b/static/js/app.js index c9882ac5..fb16d257 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -658,24 +658,25 @@ function updateRegistrationWaitStrategyHints() { const strategyLabel = registrationWaitStrategy === 'completion' ? '完成间隔' : '启动间隔'; const normalMode = elements.concurrencyMode?.value || 'pipeline'; const outlookMode = elements.outlookConcurrencyMode?.value || 'pipeline'; + const prefix = '当前全局等待策略:'; if (elements.waitStrategyHint) { if (normalMode === 'parallel') { - elements.waitStrategyHint.textContent = `当前全局等待策略:${strategyLabel}。当前并行模式下不会使用间隔等待。`; + elements.waitStrategyHint.innerHTML = `${prefix}${strategyLabel}。当前并行模式下不会使用间隔等待。`; } else if (registrationWaitStrategy === 'completion') { - elements.waitStrategyHint.textContent = '当前全局等待策略:完成间隔。前一个任务完成后,会等待随机秒数,再启动后续任务。'; + elements.waitStrategyHint.innerHTML = `${prefix}完成间隔。前一个任务完成后,会等待随机秒数,再启动后续任务。`; } else { - elements.waitStrategyHint.textContent = '当前全局等待策略:启动间隔。启动新任务前会等待随机秒数;如果前一任务更慢,后续任务可能在完成后立即接上。'; + elements.waitStrategyHint.innerHTML = `${prefix}启动间隔。启动新任务前会等待随机秒数;如果前一任务更慢,后续任务可能在完成后立即接上。`; } } if (elements.outlookWaitStrategyHint) { if (outlookMode === 'parallel') { - elements.outlookWaitStrategyHint.textContent = `当前全局等待策略:${strategyLabel}。当前并行模式下不会使用间隔等待。`; + elements.outlookWaitStrategyHint.innerHTML = `${prefix}${strategyLabel}。当前并行模式下不会使用间隔等待。`; } else if (registrationWaitStrategy === 'completion') { - elements.outlookWaitStrategyHint.textContent = '当前全局等待策略:完成间隔。前一个任务完成后,会等待随机秒数,再启动后续任务。'; + elements.outlookWaitStrategyHint.innerHTML = `${prefix}完成间隔。前一个任务完成后,会等待随机秒数,再启动后续任务。`; } else { - elements.outlookWaitStrategyHint.textContent = '当前全局等待策略:启动间隔。启动新任务前会等待随机秒数;如果前一任务更慢,后续任务可能在完成后立即接上。'; + elements.outlookWaitStrategyHint.innerHTML = `${prefix}启动间隔。启动新任务前会等待随机秒数;如果前一任务更慢,后续任务可能在完成后立即接上。`; } } } From 5e479f89826244c3b967f317c1e65364a88020cc Mon Sep 17 00:00:00 2001 From: Mison Date: Thu, 2 Apr 2026 15:37:46 +0800 Subject: [PATCH 6/7] Tone down wait strategy hint prefix color --- static/js/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/app.js b/static/js/app.js index fb16d257..033a4ffd 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -658,7 +658,7 @@ function updateRegistrationWaitStrategyHints() { const strategyLabel = registrationWaitStrategy === 'completion' ? '完成间隔' : '启动间隔'; const normalMode = elements.concurrencyMode?.value || 'pipeline'; const outlookMode = elements.outlookConcurrencyMode?.value || 'pipeline'; - const prefix = '当前全局等待策略:'; + const prefix = '当前全局等待策略:'; if (elements.waitStrategyHint) { if (normalMode === 'parallel') { From 7d8fa3162b1ea9f4594f8c907b1351b065c018f7 Mon Sep 17 00:00:00 2001 From: Mison Date: Thu, 2 Apr 2026 15:48:15 +0800 Subject: [PATCH 7/7] Document wait strategy verification --- ...R-REGISTRATION-WAIT-STRATEGY-2026-04-02.md | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docs/reviews/CR-REGISTRATION-WAIT-STRATEGY-2026-04-02.md diff --git a/docs/reviews/CR-REGISTRATION-WAIT-STRATEGY-2026-04-02.md b/docs/reviews/CR-REGISTRATION-WAIT-STRATEGY-2026-04-02.md new file mode 100644 index 00000000..7e69c024 --- /dev/null +++ b/docs/reviews/CR-REGISTRATION-WAIT-STRATEGY-2026-04-02.md @@ -0,0 +1,70 @@ +# Registration Wait Strategy Review + +## Scope + +- Branch: `feature/registration-wait-strategy` +- Base: `upstream/main` +- Goal: add a configurable global wait strategy for batch registration and surface the active mode in settings and registration UI + +## Verification + +### Automated Tests + +Command: + +```bash +docker exec codex-console-dev-webui-1 python -m pytest \ + tests/test_settings_registration_auto_fields.py \ + tests/test_registration_wait_strategy.py -q +``` + +Result: + +```text +...... [100%] +6 passed in 1.91s +``` + +### Runtime Checks + +Command: + +```bash +python3 - <<'PY' +import urllib.request +for url in ['http://127.0.0.1:16666/login', 'http://127.0.0.1:15555/login']: + with urllib.request.urlopen(url, timeout=10) as r: + print(url, r.status) +PY +``` + +Result: + +```text +http://127.0.0.1:16666/login 200 +http://127.0.0.1:15555/login 200 +``` + +- Dev service confirmed on `16666` +- Formal service confirmed on `15555` +- Dev service rebuilt after the final UI color adjustment + +### Review Check + +Command: + +```bash +coderabbit review --prompt-only --base upstream/main -t committed +``` + +Result: + +```text +Review completed: No findings +``` + +## Conclusion + +- No blocking findings found in code review +- Global wait strategy is persisted, consumed by pipeline scheduling, and visible in both settings and registration UI +- Formal service remained unaffected during dev verification