From 6ab8ffc7a52e1b50a59434f4c3f2d4e9dc700c8c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 12:42:16 +0000 Subject: [PATCH] feat(auth): add optional CPA OIDC export after SSO registration --- .../workflows/temp-cpa-oidc-issue-trigger.yml | 34 --- .gitignore | 3 + README.md | 36 +++ config.example.json | 11 + grok_register_ttk.py | 67 +++++ tools/apply_cpa_oidc_upgrade.py | 260 ------------------ 6 files changed, 117 insertions(+), 294 deletions(-) delete mode 100644 .github/workflows/temp-cpa-oidc-issue-trigger.yml delete mode 100644 tools/apply_cpa_oidc_upgrade.py diff --git a/.github/workflows/temp-cpa-oidc-issue-trigger.yml b/.github/workflows/temp-cpa-oidc-issue-trigger.yml deleted file mode 100644 index d6dce03..0000000 --- a/.github/workflows/temp-cpa-oidc-issue-trigger.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Temporary CPA OIDC issue trigger - -on: - issues: - types: - - edited - -permissions: - contents: write - -jobs: - apply-upgrade: - if: github.event.issue.number == 7 - runs-on: ubuntu-latest - steps: - - name: Checkout main - uses: actions/checkout@v4 - with: - ref: main - fetch-depth: 0 - - - name: Apply CPA OIDC upgrade - run: python tools/apply_cpa_oidc_upgrade.py - - - name: Commit upgrade and clean temporary files - shell: bash - run: | - rm -f tools/apply_cpa_oidc_upgrade.py - rm -f .github/workflows/temp-cpa-oidc-issue-trigger.yml - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add grok_register_ttk.py config.example.json README.md .gitignore cpa_export.py cpa_xai .github/workflows/temp-cpa-oidc-issue-trigger.yml tools/apply_cpa_oidc_upgrade.py - git commit -m "feat(auth): add optional CPA OIDC export after SSO registration" - git push origin HEAD:main diff --git a/.gitignore b/.gitignore index 91efff3..e9f0137 100755 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ config.local.json .vscode/ DrissionPage/ autoPortData/ +cpa_auths/ +xai-*.json +cpa_auth_failed.txt diff --git a/README.md b/README.md index 9104d01..de9572e 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Grok Register 是一个面向自动化流程研究、测试环境验证和个人 - 支持验证码邮件轮询和解析。 - 支持成功账号实时写入 `accounts_*.txt`。 - 支持将 SSO token 写入 grok2api 本地或远端池。 +- 支持注册成功后可选导出 CLIProxyAPI 使用的 CPA xAI OAuth 凭证。 - 支持注册后尝试开启 NSFW。 - 支持页面卡住检测、当前账号重试、浏览器重启和内存清理。 @@ -112,6 +113,12 @@ cp config.example.json config.json | `grok2api_auto_add_remote` | 是否写入远端 grok2api | | `grok2api_remote_base` | 远端 grok2api 地址,可填站点根地址或 `/admin/api` 管理 API 地址 | | `grok2api_remote_app_key` | 远端 grok2api app key | +| `cpa_export_enabled` | 是否在 SSO 成功后额外导出 CPA xAI OIDC 凭证 | +| `cpa_auth_dir` | 本地 CPA 导出目录,生成 `xai-*.json` | +| `cpa_copy_to_hotload` | 是否额外复制到 CLIProxyAPI 的 auth-dir | +| `cpa_hotload_dir` | CLIProxyAPI auth-dir 路径 | +| `cpa_proxy` | CPA/OIDC 专用代理;留空则回退到 `proxy` | +| `cpa_headless` | CPA/OIDC 浏览器是否无头,默认建议 `false` | ### Cloudflare 临时邮箱匿名模式(默认) @@ -217,6 +224,33 @@ curl -X POST "https://你的-Cloud-Mail-域名/api/public/genToken" \ `config.json` 包含个人配置和密钥,不要提交到 Git。 +### CPA / xAI OIDC 导出(可选) + +当 `cpa_export_enabled=true` 时,程序会在**账号已经注册成功、SSO 已保存**之后,额外发起一次 xAI Device Authorization,自动批准授权并导出 `xai-*.json`。 + +这条线路是附加功能,不会替代原有的 SSO 保存流程。即使 OIDC 导出失败,原账号与 SSO 结果仍会保留。 + +推荐最小配置: + +```json +{ + "cpa_export_enabled": true, + "cpa_auth_dir": "./cpa_auths", + "cpa_base_url": "https://cli-chat-proxy.grok.com/v1", + "cpa_proxy": "", + "cpa_headless": false +} +``` + +如需让 CLIProxyAPI 自动热加载,可额外填写: + +```json +{ + "cpa_copy_to_hotload": true, + "cpa_hotload_dir": "/path/to/cli-proxy-api/auth-dir" +} +``` + ## 运行 ### CLI 模式 @@ -255,6 +289,8 @@ GUI 模式会打开 Tkinter 窗口,适合手动调整配置和观察日志。 - `accounts_*.txt`:成功账号、密码和 SSO token。 - `mail_credentials.txt`:临时邮箱凭证。 +- `cpa_auths/xai-*.json`:可选导出的 CPA xAI OAuth 凭证。 +- `cpa_auths/cpa_auth_failed.txt`:OIDC 导出失败记录。 - `*.log`:可选日志文件。 这些文件包含敏感信息,已被 `.gitignore` 忽略。 diff --git a/config.example.json b/config.example.json index 956e22b..98e9f7f 100755 --- a/config.example.json +++ b/config.example.json @@ -21,6 +21,17 @@ "grok2api_auto_add_remote": false, "grok2api_remote_base": "", "grok2api_remote_app_key": "", + "api_reverse_tools": "", + "cpa_export_enabled": false, + "cpa_auth_dir": "./cpa_auths", + "cpa_copy_to_hotload": false, + "cpa_hotload_dir": "", + "cpa_base_url": "https://cli-chat-proxy.grok.com/v1", + "cpa_proxy": "", + "cpa_headless": false, + "cpa_force_standalone": true, + "cpa_mint_timeout_sec": 300, + "cpa_mint_cookie_inject": true, "defaultDomains": "example.com", "email_provider": "cloudflare", "yyds_api_key": "", diff --git a/grok_register_ttk.py b/grok_register_ttk.py index ef2b2ae..cc61d69 100755 --- a/grok_register_ttk.py +++ b/grok_register_ttk.py @@ -68,6 +68,17 @@ DEFAULT_CONFIG = { "grok2api_auto_add_remote": False, "grok2api_remote_base": "", "grok2api_remote_app_key": "", + "api_reverse_tools": "", + "cpa_export_enabled": False, + "cpa_auth_dir": "./cpa_auths", + "cpa_copy_to_hotload": False, + "cpa_hotload_dir": "", + "cpa_base_url": "https://cli-chat-proxy.grok.com/v1", + "cpa_proxy": "", + "cpa_headless": False, + "cpa_force_standalone": True, + "cpa_mint_timeout_sec": 300, + "cpa_mint_cookie_inject": True, } config = DEFAULT_CONFIG.copy() @@ -2947,6 +2958,43 @@ return String(cfInput.value || '').trim().length; ) + +def maybe_export_cpa_xai_after_success(email, password, sso="", log_callback=None, cancel_callback=None): + if not bool(config.get("cpa_export_enabled", False)): + return {"ok": False, "skipped": True, "reason": "disabled"} + logger = log_callback or (lambda message: None) + try: + from cpa_export import export_cpa_xai_for_account + except Exception as exc: + logger(f"[!] CPA 模块导入失败,已跳过 OIDC 导出: {exc}") + return {"ok": False, "error": str(exc)} + current_page = None + try: + current_page = page + except Exception: + current_page = None + try: + result = export_cpa_xai_for_account( + email=email, + password=password, + page=current_page, + sso=sso, + config=config, + log_callback=logger, + cancel_callback=cancel_callback, + ) + except Exception as exc: + logger(f"[!] CPA OIDC 导出失败,账号已保留: {exc}") + return {"ok": False, "error": str(exc)} + if result.get("ok"): + exported_path = result.get("hotload_path") or result.get("path") or "" + suffix = f": {exported_path}" if exported_path else "" + logger(f"[+] CPA OIDC 导出成功{suffix}") + elif not result.get("skipped"): + logger(f"[!] CPA OIDC 导出失败,账号已保留: {result.get('error') or result}") + return result + + class GrokRegisterGUI: def __init__(self, root): self.root = root @@ -3119,6 +3167,16 @@ class GrokRegisterGUI: self.grok2api_remote_key_entry = tk_entry(config_frame, textvariable=self.grok2api_remote_key_var, width=72) add_field(self.grok2api_remote_key_entry, 11, 1, columnspan=3) + add_label(12, 0, "OIDC / CPA:") + self.cpa_export_var = tk.BooleanVar(value=bool(config.get("cpa_export_enabled", False))) + self.cpa_export_check = tk_checkbutton(config_frame, text="注册成功后导出 CPA xAI OIDC", variable=self.cpa_export_var) + add_field(self.cpa_export_check, 12, 1, sticky=tk.W) + + add_label(12, 2, "CPA 输出目录:") + self.cpa_auth_dir_var = tk.StringVar(value=str(config.get("cpa_auth_dir", "./cpa_auths"))) + self.cpa_auth_dir_entry = tk_entry(config_frame, textvariable=self.cpa_auth_dir_var, width=34) + add_field(self.cpa_auth_dir_entry, 12, 3) + btn_frame = tk.Frame(main_frame, bg=UI_BG) btn_frame.grid(row=1, column=0, sticky=tk.EW, pady=(0, 6)) self.start_btn = tk_button(btn_frame, text="开始注册", command=self.start_registration) @@ -3211,6 +3269,8 @@ class GrokRegisterGUI: config["grok2api_auto_add_remote"] = bool(self.grok2api_remote_auto_var.get()) config["grok2api_remote_base"] = self.grok2api_remote_base_var.get().strip() config["grok2api_remote_app_key"] = self.grok2api_remote_key_var.get().strip() + config["cpa_export_enabled"] = bool(self.cpa_export_var.get()) + config["cpa_auth_dir"] = self.cpa_auth_dir_var.get().strip() or "./cpa_auths" raw_paths = [x.strip() for x in self.cloudflare_paths_var.get().split(",") if x.strip()] if len(raw_paths) >= 4: config["cloudflare_path_domains"] = raw_paths[0] if raw_paths[0].startswith("/") else ("/" + raw_paths[0]) @@ -3506,6 +3566,13 @@ def run_registration_cli(count): except Exception as file_exc: cli_log(f"[Debug] 保存账号文件失败: {file_exc}") add_token_to_grok2api_pools(sso, email=email, log_callback=cli_log) + maybe_export_cpa_xai_after_success( + email=email, + password=profile.get("password", ""), + sso=sso, + log_callback=cli_log, + cancel_callback=controller.should_stop, + ) success_count += 1 retry_count_for_slot = 0 i += 1 diff --git a/tools/apply_cpa_oidc_upgrade.py b/tools/apply_cpa_oidc_upgrade.py deleted file mode 100644 index 88a333f..0000000 --- a/tools/apply_cpa_oidc_upgrade.py +++ /dev/null @@ -1,260 +0,0 @@ -#!/usr/bin/env python3 -import ast -import json -from pathlib import Path - - -ROOT = Path(__file__).resolve().parents[1] -APP_PATH = ROOT / "grok_register_ttk.py" -CONFIG_PATH = ROOT / "config.example.json" -README_PATH = ROOT / "README.md" -GITIGNORE_PATH = ROOT / ".gitignore" - - -def ensure_contains(text, needle): - if needle not in text: - raise RuntimeError("missing expected marker: %s" % needle) - - -def replace_once(text, old, new): - if old not in text: - raise RuntimeError("anchor not found for replacement:\n%s" % old[:240]) - return text.replace(old, new, 1) - - -app = APP_PATH.read_text(encoding="utf-8-sig") - -config_block_old = ''' "grok2api_auto_add_remote": False, - "grok2api_remote_base": "", - "grok2api_remote_app_key": "", -}''' -config_block_new = ''' "grok2api_auto_add_remote": False, - "grok2api_remote_base": "", - "grok2api_remote_app_key": "", - "api_reverse_tools": "", - "cpa_export_enabled": False, - "cpa_auth_dir": "./cpa_auths", - "cpa_copy_to_hotload": False, - "cpa_hotload_dir": "", - "cpa_base_url": "https://cli-chat-proxy.grok.com/v1", - "cpa_proxy": "", - "cpa_headless": False, - "cpa_force_standalone": True, - "cpa_mint_timeout_sec": 300, - "cpa_mint_cookie_inject": True, -}''' -if '"cpa_export_enabled": False' not in app: - app = replace_once(app, config_block_old, config_block_new) - -helper_block = ''' - - -def maybe_export_cpa_xai_after_success(email, password, sso="", log_callback=None, cancel_callback=None): - if not bool(config.get("cpa_export_enabled", False)): - return {"ok": False, "skipped": True, "reason": "disabled"} - logger = log_callback or (lambda message: None) - try: - from cpa_export import export_cpa_xai_for_account - except Exception as exc: - logger(f"[!] CPA 模块导入失败,已跳过 OIDC 导出: {exc}") - return {"ok": False, "error": str(exc)} - current_page = None - try: - current_page = page - except Exception: - current_page = None - try: - result = export_cpa_xai_for_account( - email=email, - password=password, - page=current_page, - sso=sso, - config=config, - log_callback=logger, - cancel_callback=cancel_callback, - ) - except Exception as exc: - logger(f"[!] CPA OIDC 导出失败,账号已保留: {exc}") - return {"ok": False, "error": str(exc)} - if result.get("ok"): - exported_path = result.get("hotload_path") or result.get("path") or "" - suffix = f": {exported_path}" if exported_path else "" - logger(f"[+] CPA OIDC 导出成功{suffix}") - elif not result.get("skipped"): - logger(f"[!] CPA OIDC 导出失败,账号已保留: {result.get('error') or result}") - return result -''' -if 'def maybe_export_cpa_xai_after_success(' not in app: - app = replace_once(app, '\n\nclass GrokRegisterGUI:\n', helper_block + '\n\nclass GrokRegisterGUI:\n') - -gui_fields_anchor = ''' add_label(11, 0, "grok2api 远端 app_key:") - self.grok2api_remote_key_var = tk.StringVar(value=str(config.get("grok2api_remote_app_key", ""))) - self.grok2api_remote_key_entry = tk_entry(config_frame, textvariable=self.grok2api_remote_key_var, width=72) - add_field(self.grok2api_remote_key_entry, 11, 1, columnspan=3) - - btn_frame = tk.Frame(main_frame, bg=UI_BG)''' -gui_fields_insert = ''' add_label(11, 0, "grok2api 远端 app_key:") - self.grok2api_remote_key_var = tk.StringVar(value=str(config.get("grok2api_remote_app_key", ""))) - self.grok2api_remote_key_entry = tk_entry(config_frame, textvariable=self.grok2api_remote_key_var, width=72) - add_field(self.grok2api_remote_key_entry, 11, 1, columnspan=3) - - add_label(12, 0, "OIDC / CPA:") - self.cpa_export_var = tk.BooleanVar(value=bool(config.get("cpa_export_enabled", False))) - self.cpa_export_check = tk_checkbutton(config_frame, text="注册成功后导出 CPA xAI OIDC", variable=self.cpa_export_var) - add_field(self.cpa_export_check, 12, 1, sticky=tk.W) - - add_label(12, 2, "CPA 输出目录:") - self.cpa_auth_dir_var = tk.StringVar(value=str(config.get("cpa_auth_dir", "./cpa_auths"))) - self.cpa_auth_dir_entry = tk_entry(config_frame, textvariable=self.cpa_auth_dir_var, width=34) - add_field(self.cpa_auth_dir_entry, 12, 3) - - btn_frame = tk.Frame(main_frame, bg=UI_BG)''' -if 'self.cpa_export_var = tk.BooleanVar' not in app: - app = replace_once(app, gui_fields_anchor, gui_fields_insert) - -start_config_anchor = ''' config["grok2api_auto_add_remote"] = bool(self.grok2api_remote_auto_var.get()) - config["grok2api_remote_base"] = self.grok2api_remote_base_var.get().strip() - config["grok2api_remote_app_key"] = self.grok2api_remote_key_var.get().strip() - raw_paths = [x.strip() for x in self.cloudflare_paths_var.get().split(",") if x.strip()]''' -start_config_insert = ''' config["grok2api_auto_add_remote"] = bool(self.grok2api_remote_auto_var.get()) - config["grok2api_remote_base"] = self.grok2api_remote_base_var.get().strip() - config["grok2api_remote_app_key"] = self.grok2api_remote_key_var.get().strip() - config["cpa_export_enabled"] = bool(self.cpa_export_var.get()) - config["cpa_auth_dir"] = self.cpa_auth_dir_var.get().strip() or "./cpa_auths" - raw_paths = [x.strip() for x in self.cloudflare_paths_var.get().split(",") if x.strip()]''' -if 'config["cpa_export_enabled"] = bool(self.cpa_export_var.get())' not in app: - app = replace_once(app, start_config_anchor, start_config_insert) - -gui_success_anchor = ' add_token_to_grok2api_pools(sso, email=email, log_callback=self.log)\n self.success_count += 1' -gui_success_insert = ''' add_token_to_grok2api_pools(sso, email=email, log_callback=self.log) - maybe_export_cpa_xai_after_success( - email=email, - password=profile.get("password", ""), - sso=sso, - log_callback=self.log, - cancel_callback=self.should_stop, - ) - self.success_count += 1''' -if 'maybe_export_cpa_xai_after_success(' not in app.split('class CliStopController', 1)[0]: - app = replace_once(app, gui_success_anchor, gui_success_insert) - -cli_success_anchor = ' add_token_to_grok2api_pools(sso, email=email, log_callback=cli_log)\n success_count += 1' -cli_success_insert = ''' add_token_to_grok2api_pools(sso, email=email, log_callback=cli_log) - maybe_export_cpa_xai_after_success( - email=email, - password=profile.get("password", ""), - sso=sso, - log_callback=cli_log, - cancel_callback=controller.should_stop, - ) - success_count += 1''' -cli_part = app.split('def main_cli():', 1)[0] -if 'maybe_export_cpa_xai_after_success(' not in cli_part.split('def run_registration_cli', 1)[-1]: - app = replace_once(app, cli_success_anchor, cli_success_insert) - -ast.parse(app) -APP_PATH.write_text(app, encoding="utf-8-sig") - -config = json.loads(CONFIG_PATH.read_text(encoding="utf-8")) -if "cpa_export_enabled" not in config: - new_config = {} - for key, value in config.items(): - new_config[key] = value - if key == "grok2api_remote_app_key": - new_config["api_reverse_tools"] = "" - new_config["cpa_export_enabled"] = False - new_config["cpa_auth_dir"] = "./cpa_auths" - new_config["cpa_copy_to_hotload"] = False - new_config["cpa_hotload_dir"] = "" - new_config["cpa_base_url"] = "https://cli-chat-proxy.grok.com/v1" - new_config["cpa_proxy"] = "" - new_config["cpa_headless"] = False - new_config["cpa_force_standalone"] = True - new_config["cpa_mint_timeout_sec"] = 300 - new_config["cpa_mint_cookie_inject"] = True - config = new_config - CONFIG_PATH.write_text(json.dumps(config, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") - -gitignore = GITIGNORE_PATH.read_text(encoding="utf-8") -for line in ("cpa_auths/", "xai-*.json", "cpa_auth_failed.txt"): - if line not in gitignore: - if not gitignore.endswith("\n"): - gitignore += "\n" - gitignore += line + "\n" -GITIGNORE_PATH.write_text(gitignore, encoding="utf-8") - -readme = README_PATH.read_text(encoding="utf-8") -feature_old = "- 支持将 SSO token 写入 grok2api 本地或远端池。" -feature_new = "- 支持将 SSO token 写入 grok2api 本地或远端池。\n- 支持注册成功后可选导出 CLIProxyAPI 使用的 CPA xAI OAuth 凭证。" -if "CPA xAI OAuth 凭证" not in readme: - readme = replace_once(readme, feature_old, feature_new) - -table_old = "| `grok2api_remote_app_key` | 远端 grok2api app key |" -table_new = '''| `grok2api_remote_app_key` | 远端 grok2api app key | -| `cpa_export_enabled` | 是否在 SSO 成功后额外导出 CPA xAI OIDC 凭证 | -| `cpa_auth_dir` | 本地 CPA 导出目录,生成 `xai-*.json` | -| `cpa_copy_to_hotload` | 是否额外复制到 CLIProxyAPI 的 auth-dir | -| `cpa_hotload_dir` | CLIProxyAPI auth-dir 路径 | -| `cpa_proxy` | CPA/OIDC 专用代理;留空则回退到 `proxy` | -| `cpa_headless` | CPA/OIDC 浏览器是否无头,默认建议 `false` |''' -if "| `cpa_export_enabled` |" not in readme: - readme = replace_once(readme, table_old, table_new) - -section_anchor = "`config.json` 包含个人配置和密钥,不要提交到 Git。\n" -section_block = '''`config.json` 包含个人配置和密钥,不要提交到 Git。 - -### CPA / xAI OIDC 导出(可选) - -当 `cpa_export_enabled=true` 时,程序会在**账号已经注册成功、SSO 已保存**之后,额外发起一次 xAI Device Authorization,自动批准授权并导出 `xai-*.json`。 - -这条线路是附加功能,不会替代原有的 SSO 保存流程。即使 OIDC 导出失败,原账号与 SSO 结果仍会保留。 - -推荐最小配置: - -```json -{ - "cpa_export_enabled": true, - "cpa_auth_dir": "./cpa_auths", - "cpa_base_url": "https://cli-chat-proxy.grok.com/v1", - "cpa_proxy": "", - "cpa_headless": false -} -``` - -如需让 CLIProxyAPI 自动热加载,可额外填写: - -```json -{ - "cpa_copy_to_hotload": true, - "cpa_hotload_dir": "/path/to/cli-proxy-api/auth-dir" -} -``` -''' -if "### CPA / xAI OIDC 导出(可选)" not in readme: - readme = replace_once(readme, section_anchor, section_block) - -output_old = "- `accounts_*.txt`:成功账号、密码和 SSO token。\n- `mail_credentials.txt`:临时邮箱凭证。" -output_new = "- `accounts_*.txt`:成功账号、密码和 SSO token。\n- `mail_credentials.txt`:临时邮箱凭证。\n- `cpa_auths/xai-*.json`:可选导出的 CPA xAI OAuth 凭证。\n- `cpa_auths/cpa_auth_failed.txt`:OIDC 导出失败记录。" -if "cpa_auths/xai-*.json" not in readme: - readme = replace_once(readme, output_old, output_new) - -README_PATH.write_text(readme, encoding="utf-8") - -for path in ( - APP_PATH, - ROOT / "cpa_export.py", - ROOT / "cpa_xai" / "__init__.py", - ROOT / "cpa_xai" / "proxyutil.py", - ROOT / "cpa_xai" / "oauth_device.py", - ROOT / "cpa_xai" / "schema.py", - ROOT / "cpa_xai" / "writer.py", - ROOT / "cpa_xai" / "browser_confirm.py", - ROOT / "cpa_xai" / "mint.py", -): - source = path.read_text(encoding="utf-8-sig" if path == APP_PATH else "utf-8") - ast.parse(source) - -ensure_contains(APP_PATH.read_text(encoding="utf-8-sig"), 'def maybe_export_cpa_xai_after_success(') -ensure_contains(README_PATH.read_text(encoding="utf-8"), "### CPA / xAI OIDC 导出(可选)") -ensure_contains(CONFIG_PATH.read_text(encoding="utf-8"), '"cpa_export_enabled": false') -print("CPA/OIDC upgrade applied successfully.")