diff --git a/tools/apply_post_modularization_fixes.py b/tools/apply_post_modularization_fixes.py
new file mode 100644
index 0000000..1c54551
--- /dev/null
+++ b/tools/apply_post_modularization_fixes.py
@@ -0,0 +1,511 @@
+#!/usr/bin/env python3
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def read(path):
+ return (ROOT / path).read_text(encoding="utf-8")
+
+
+def write(path, text):
+ (ROOT / path).write_text(text, encoding="utf-8")
+
+
+def replace_once(text, old, new, label):
+ count = text.count(old)
+ if count != 1:
+ raise RuntimeError(f"{label}: expected one match, got {count}")
+ return text.replace(old, new, 1)
+
+
+# grok_register_ttk.py: single config implementation, full state compatibility,
+# and complete GUI batch counter reset.
+path = "grok_register_ttk.py"
+text = read(path)
+old_config_impl = '''def load_config():
+ global config
+ if os.path.exists(CONFIG_FILE):
+ try:
+ with open(CONFIG_FILE, "r", encoding="utf-8") as f:
+ loaded = json.load(f)
+ config = validate_config_structure(loaded)
+ except ConfigError:
+ raise
+ except Exception as exc:
+ raise ConfigError(f"配置文件解析失败: {CONFIG_FILE}: {exc}") from exc
+ else:
+ config = validate_config_structure(DEFAULT_CONFIG.copy())
+ return config
+
+
+def save_config():
+ global config
+ config = validate_config_structure(config)
+ config_dir = os.path.dirname(os.path.abspath(CONFIG_FILE))
+ os.makedirs(config_dir, exist_ok=True)
+ fd = None
+ temp_path = None
+ try:
+ fd, temp_path = tempfile.mkstemp(prefix=".config-", suffix=".json.tmp", dir=config_dir)
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
+ fd = None
+ json.dump(config, f, indent=4, ensure_ascii=False)
+ f.write("\\n")
+ f.flush()
+ os.fsync(f.fileno())
+ try:
+ os.chmod(temp_path, 0o600)
+ except Exception:
+ pass
+ os.replace(temp_path, CONFIG_FILE)
+ temp_path = None
+ try:
+ os.chmod(CONFIG_FILE, 0o600)
+ except Exception:
+ pass
+ except Exception as exc:
+ raise ConfigError(f"保存配置失败: {exc}") from exc
+ finally:
+ if fd is not None:
+ try:
+ os.close(fd)
+ except Exception:
+ pass
+ if temp_path and os.path.exists(temp_path):
+ try:
+ os.unlink(temp_path)
+ except Exception:
+ pass
+
+
+'''
+text = replace_once(text, old_config_impl, "", "duplicate config implementation")
+old_setattr = ''' elif name in {"_cf_domain_index", "_cloudmail_domain_index"}:
+ setattr(_mail_service, name, int(value))
+ self.__dict__.pop(name, None)
+ return
+ super().__setattr__(name, value)
+'''
+new_setattr = ''' elif name in {"_cf_domain_index", "_cloudmail_domain_index"}:
+ setattr(_mail_service, name, int(value))
+ self.__dict__.pop(name, None)
+ return
+ elif name in {"browser", "page", "browser_proxy_bridge", "browser_started_with_proxy", "cf_clearance"}:
+ setattr(_registration_browser, name, value)
+ self.__dict__.pop(name, None)
+ return
+ super().__setattr__(name, value)
+'''
+text = replace_once(text, old_setattr, new_setattr, "compatibility state writes")
+old_reset = ''' self.stop_requested = False
+ self.success_count = 0
+ self.fail_count = 0
+ self.results = []
+'''
+new_reset = ''' self.stop_requested = False
+ self._reset_batch_counters()
+ self.results = []
+'''
+text = replace_once(text, old_reset, new_reset, "GUI counter reset call")
+anchor = ''' def should_stop(self):
+ return self.stop_requested or not self.is_running
+
+ def start_registration(self):
+'''
+replacement = ''' def should_stop(self):
+ return self.stop_requested or not self.is_running
+
+ def _reset_batch_counters(self):
+ self.success_count = 0
+ self.fail_count = 0
+ self.registered_unsaved_count = 0
+ self.postprocess_warning_count = 0
+
+ def start_registration(self):
+'''
+text = replace_once(text, anchor, replacement, "GUI reset helper")
+write(path, text)
+
+
+# Restore exact registration URL.
+path = "registration_browser.py"
+text = read(path)
+text = replace_once(
+ text,
+ 'SIGNUP_URL = "https://accounts.x.ai/sign-up"',
+ 'SIGNUP_URL = "https://accounts.x.ai/sign-up?redirect=grok-com"',
+ "registration URL",
+)
+write(path, text)
+
+
+# Python 3.9-compatible typing.
+path = "cpa_export.py"
+text = read(path)
+text = replace_once(text, "from pathlib import Path\n", "from pathlib import Path\nfrom typing import Optional\n", "Optional import")
+text = replace_once(text, " hotload_dir: Path | None\n", " hotload_dir: Optional[Path]\n", "Python 3.9 union")
+write(path, text)
+
+
+# Make browser options explicitly reusable by CPA without importing the main app.
+path = "browser_runtime.py"
+text = read(path)
+old_options = '''def create_browser_options(browser_proxy=""):
+ options = ChromiumOptions()
+ options.auto_port()
+ options.set_timeouts(base=1)
+ apply_browser_proxy_option(options, browser_proxy)
+ if _extension_path and os.path.exists(_extension_path):
+ options.add_extension(_extension_path)
+ return options
+'''
+new_options = '''def create_browser_options(browser_proxy="", extension_path=None):
+ options = ChromiumOptions()
+ options.auto_port()
+ options.set_timeouts(base=1)
+ apply_browser_proxy_option(options, browser_proxy)
+ effective_extension = _extension_path if extension_path is None else str(extension_path or "")
+ if effective_extension and os.path.exists(effective_extension):
+ options.add_extension(effective_extension)
+ return options
+'''
+text = replace_once(text, old_options, new_options, "browser options factory")
+write(path, text)
+
+path = "cpa_xai/browser_session.py"
+text = read(path)
+text = text.replace("import sys\n", "")
+old_probe = ''' options = None
+ package_root = Path(__file__).resolve().parents[1]
+ try:
+ register_file = package_root / "grok_register_ttk.py"
+ if register_file.is_file():
+ register_dir = str(package_root)
+ if register_dir not in sys.path:
+ sys.path.insert(0, register_dir)
+ try:
+ from grok_register_ttk import create_browser_options # type: ignore
+
+ options = create_browser_options()
+ logger("using register create_browser_options (turnstilePatch)")
+ except Exception as exc: # noqa: BLE001
+ logger("register browser options unavailable: %s" % exc)
+ options = None
+ except Exception as exc: # noqa: BLE001
+ logger("register options probe failed: %s" % exc)
+ options = None
+'''
+new_probe = ''' options = None
+ package_root = Path(__file__).resolve().parents[1]
+ try:
+ from browser_runtime import create_browser_options
+
+ options = create_browser_options(
+ extension_path=package_root / "turnstilePatch"
+ )
+ logger("using shared browser_runtime.create_browser_options")
+ except Exception as exc: # noqa: BLE001
+ logger("shared browser options unavailable: %s" % exc)
+ options = None
+'''
+text = replace_once(text, old_probe, new_probe, "CPA reverse import removal")
+write(path, text)
+
+
+# Tighten CPA hotload validation.
+path = "app_config.py"
+text = read(path)
+text = replace_once(
+ text,
+ ' if cfg["cpa_copy_to_hotload"] and not cfg["cpa_hotload_dir"]:\n',
+ ' if cfg["cpa_export_enabled"] and cfg["cpa_copy_to_hotload"] and not cfg["cpa_hotload_dir"]:\n',
+ "CPA hotload validation",
+)
+write(path, text)
+
+
+# Centralize mail body normalization and fix address filtering independent of logging.
+path = "mail_service.py"
+text = read(path)
+insert_anchor = '''def _pick_list_payload(data):
+'''
+helper = '''def normalize_mail_body(*sources):
+ """Return normalized text from provider payloads with string/list HTML support."""
+ parts = []
+ for source in sources:
+ if not isinstance(source, dict):
+ continue
+ for key in ("text", "raw", "content", "intro", "body", "snippet"):
+ value = source.get(key)
+ values = value if isinstance(value, (list, tuple)) else [value]
+ for item in values:
+ if isinstance(item, str) and item.strip():
+ parts.append(item)
+ html_value = source.get("html")
+ html_items = html_value if isinstance(html_value, (list, tuple)) else [html_value]
+ for item in html_items:
+ if isinstance(item, str) and item.strip():
+ parts.append(re.sub(r"<[^>]+>", " ", item))
+ return "\\n".join(parts)
+
+
+'''
+text = replace_once(text, insert_anchor, helper + insert_anchor, "mail body normalizer")
+old_filter = ''' if not address_matched and log_callback:
+ log_callback(f"[Debug] 跳过疑似非目标邮件 id={msg_id} address={msg_addr} to={recipients}")
+ continue
+'''
+new_filter = ''' if not address_matched:
+ if log_callback:
+ log_callback(f"[Debug] 跳过疑似非目标邮件 id={msg_id} address={msg_addr} to={recipients}")
+ continue
+'''
+text = replace_once(text, old_filter, new_filter, "Cloudflare target filtering")
+old_cf_body = ''' parts = []
+ # 先直接从列表项取内容,避免 detail 接口差异导致漏码
+ for field in ("text", "raw", "content", "intro", "body", "snippet"):
+ value = msg.get(field)
+ if isinstance(value, str) and value.strip():
+ parts.append(value)
+ html_list = msg.get("html") or []
+ if isinstance(html_list, str):
+ html_list = [html_list]
+ for h in html_list:
+ parts.append(re.sub(r"<[^>]+>", " ", h))
+ subject = str(msg.get("subject", "") or "")
+ combined = "\\n".join(parts)
+'''
+new_cf_body = ''' # 先直接从列表项取内容,避免 detail 接口差异导致漏码
+ subject = str(msg.get("subject", "") or "")
+ combined = normalize_mail_body(msg)
+'''
+text = replace_once(text, old_cf_body, new_cf_body, "Cloudflare body normalization")
+old_cf_detail = ''' for field in ("text", "raw", "content", "intro", "body", "snippet"):
+ value = detail.get(field)
+ if isinstance(value, str) and value.strip():
+ combined += "\\n" + value
+ html_list2 = detail.get("html") or []
+ if isinstance(html_list2, str):
+ html_list2 = [html_list2]
+ for h in html_list2:
+ combined += "\\n" + re.sub(r"<[^>]+>", " ", h)
+'''
+new_cf_detail = ''' detail_body = normalize_mail_body(detail)
+ if detail_body:
+ combined += "\\n" + detail_body
+'''
+text = replace_once(text, old_cf_detail, new_cf_detail, "Cloudflare detail normalization")
+old_cloudmail_body = ''' parts = []
+ code_value = str(msg.get("code", "") or "").strip()
+ if code_value:
+ parts.append(f"verification code: {code_value}")
+ for field in ("text", "content", "html", "body", "snippet"):
+ value = msg.get(field)
+ values = value if isinstance(value, list) else [value]
+ for item in values:
+ if isinstance(item, str) and item.strip():
+ parts.append(re.sub(r"<[^>]+>", " ", item))
+ subject = str(msg.get("subject", "") or "")
+ combined = "\\n".join(parts)
+'''
+new_cloudmail_body = ''' code_value = str(msg.get("code", "") or "").strip()
+ combined = normalize_mail_body(msg)
+ if code_value:
+ combined = f"verification code: {code_value}\\n{combined}"
+ subject = str(msg.get("subject", "") or "")
+'''
+text = replace_once(text, old_cloudmail_body, new_cloudmail_body, "Cloud Mail body normalization")
+old_duck_body = ''' parts = []
+ text_body = detail.get("text") or ""
+ if text_body:
+ parts.append(text_body)
+ html_list = detail.get("html") or []
+ for h in html_list:
+ parts.append(re.sub(r"<[^>]+>", " ", h))
+ combined = "\\n".join(parts)
+'''
+text = replace_once(text, old_duck_body, ' combined = normalize_mail_body(detail)\n', "DuckMail body normalization")
+old_yyds_body = ''' parts = []
+ text_body = detail.get("text") or ""
+ if text_body:
+ parts.append(text_body)
+ html_list = detail.get("html") or []
+ for h in html_list:
+ parts.append(re.sub(r"<[^>]+>", " ", h))
+ combined = "\\n".join(parts)
+'''
+text = replace_once(text, old_yyds_body, ' combined = normalize_mail_body(detail)\n', "YYDS body normalization")
+write(path, text)
+
+
+# Ignore pending recovery artifacts.
+path = ".gitignore"
+text = read(path)
+if "*.pending.jsonl\n" not in text:
+ text += "\n*.pending.jsonl\n*.pending.jsonl.lock\n"
+write(path, text)
+
+
+# Remove duplicate/overlapping registration flow tests while retaining one test per semantic boundary.
+path = "tests/test_registration_flow.py"
+text = read(path)
+duplicate_block = ''' def test_cleanup_failure_does_not_change_success_statistics(self):
+ fake = FakeOps()
+ ops = fake.operations()
+ base_cleanup = ops.cleanup
+ def cleanup(reason):
+ if "已成功" in reason:
+ raise RuntimeError("cleanup failed")
+ base_cleanup(reason)
+ ops.cleanup = cleanup
+ batch = run_batch(2, self.callbacks(), lambda *args: None, ops, cleanup_interval=1)
+ self.assertEqual((batch.success_count, batch.fail_count, batch.processed_count), (2, 0, 2))
+
+ def test_cancel_during_next_account_wait_is_normal_cancellation(self):
+ fake = FakeOps()
+ ops = fake.operations()
+ ops.sleep = lambda seconds: (_ for _ in ()).throw(Cancelled())
+ batch = run_batch(2, self.callbacks(), lambda *args: None, ops)
+ self.assertTrue(batch.cancelled)
+ self.assertEqual(batch.processed_count, 1)
+
+ def test_final_cleanup_does_not_mask_original_error(self):
+ fake = FakeOps()
+ ops = fake.operations()
+ ops.start_browser = lambda: (_ for _ in ()).throw(ValueError("original"))
+ ops.cleanup = lambda reason: (_ for _ in ()).throw(RuntimeError("cleanup"))
+ with self.assertRaisesRegex(ValueError, "original"):
+ run_batch(1, self.callbacks(), lambda *args: None, ops)
+
+ def test_optional_postprocessing_exceptions_become_warning(self):
+ fake = FakeOps()
+ ops = fake.operations()
+ ops.add_tokens = lambda sso, email: (_ for _ in ()).throw(RuntimeError("pool"))
+ ops.export_cpa = lambda email, password, sso: (_ for _ in ()).throw(RuntimeError("cpa"))
+ batch = run_batch(1, self.callbacks(), lambda *args: None, ops)
+ self.assertEqual(batch.success_count, 1)
+ self.assertEqual(batch.postprocess_warning_count, 1)
+
+'''
+text = replace_once(text, duplicate_block, "", "duplicate flow tests")
+write(path, text)
+
+
+# Focused regression tests for the post-modularization contract.
+regression_tests = '''import json
+import tempfile
+import unittest
+from pathlib import Path
+from unittest.mock import patch
+
+import app_config
+import browser_runtime
+import cpa_export
+import grok_register_ttk as app
+import mail_service
+import registration_browser
+from cpa_xai import browser_session
+
+
+class PostModularizationRegressionTests(unittest.TestCase):
+ def test_signup_url_preserves_redirect(self):
+ self.assertEqual(
+ registration_browser.SIGNUP_URL,
+ "https://accounts.x.ai/sign-up?redirect=grok-com",
+ )
+
+ def test_config_identity_survives_load(self):
+ original_path = app_config.CONFIG_FILE
+ try:
+ with tempfile.TemporaryDirectory() as directory:
+ config_path = Path(directory) / "config.json"
+ payload = dict(app_config.DEFAULT_CONFIG)
+ payload["register_count"] = 3
+ config_path.write_text(json.dumps(payload), encoding="utf-8")
+ app_config.CONFIG_FILE = str(config_path)
+ loaded = app.load_config()
+ self.assertIs(loaded, app_config.config)
+ self.assertIs(app.config, app_config.config)
+ self.assertEqual(app.config["register_count"], 3)
+ finally:
+ app_config.CONFIG_FILE = original_path
+
+ def test_legacy_runtime_state_assignments_are_forwarded(self):
+ sentinel = object()
+ original = registration_browser.page
+ try:
+ app.page = sentinel
+ self.assertIs(registration_browser.page, sentinel)
+ self.assertIs(app.page, sentinel)
+ finally:
+ app.page = original
+
+ def test_gui_reset_clears_all_batch_counters(self):
+ gui = app.GrokRegisterGUI.__new__(app.GrokRegisterGUI)
+ gui.success_count = 1
+ gui.fail_count = 2
+ gui.registered_unsaved_count = 3
+ gui.postprocess_warning_count = 4
+ gui._reset_batch_counters()
+ self.assertEqual(
+ (gui.success_count, gui.fail_count, gui.registered_unsaved_count, gui.postprocess_warning_count),
+ (0, 0, 0, 0),
+ )
+
+ def test_cpa_hotload_requirement_only_applies_when_export_enabled(self):
+ cfg = dict(app_config.DEFAULT_CONFIG)
+ cfg["cpa_copy_to_hotload"] = True
+ cfg["cpa_export_enabled"] = False
+ self.assertTrue(app_config.validate_run_requirements(cfg)["cpa_copy_to_hotload"])
+ cfg["cpa_export_enabled"] = True
+ with self.assertRaises(app_config.ConfigError):
+ app_config.validate_run_requirements(cfg)
+
+ def test_mail_body_normalizes_string_and_list_html(self):
+ text = mail_service.normalize_mail_body(
+ {"text": "plain", "html": "one"},
+ {"html": ["two"]},
+ )
+ self.assertIn("plain", text)
+ self.assertIn("one", text)
+ self.assertIn("two", text)
+
+ def test_cloudflare_skips_non_target_mail_without_logger(self):
+ message = {
+ "id": "1",
+ "to": [{"address": "other@example.com"}],
+ "subject": "ABC-123 xAI",
+ "text": "ABC-123",
+ }
+ with patch.object(mail_service, "get_cloudflare_api_base", return_value="https://mail.example"), \
+ patch.object(mail_service, "cloudflare_get_messages", return_value=[message]), \
+ patch.object(mail_service, "cloudflare_get_message_detail") as detail, \
+ patch.object(mail_service, "raise_if_cancelled", return_value=None), \
+ patch.object(mail_service, "sleep_with_cancel", return_value=None), \
+ patch.object(mail_service.time, "time", side_effect=[0, 0, 2, 2]):
+ with self.assertRaises(Exception):
+ mail_service.cloudflare_get_oai_code(
+ "token", "target@example.com", timeout=1, poll_interval=0, log_callback=None
+ )
+ detail.assert_not_called()
+
+ def test_cpa_browser_session_does_not_import_main_module(self):
+ source = Path(browser_session.__file__).read_text(encoding="utf-8")
+ self.assertNotIn("from grok_register_ttk", source)
+ self.assertIn("from browser_runtime import create_browser_options", source)
+
+ def test_browser_options_accept_explicit_extension_path(self):
+ self.assertIn("extension_path", browser_runtime.create_browser_options.__code__.co_varnames)
+
+ def test_cpa_export_annotations_are_python39_compatible(self):
+ annotation = cpa_export.CpaExportSettings.__annotations__["hotload_dir"]
+ self.assertNotIsInstance(annotation, str)
+
+
+if __name__ == "__main__":
+ unittest.main()
+'''
+write("tests/test_post_modularization_regressions.py", regression_tests)
+
+print("post-modularization fixes applied")