From c44fcfeadd07d63cea18d2c318122b9d1a2bdb50 Mon Sep 17 00:00:00 2001 From: adminlove520 <791751568@qq.com> Date: Wed, 25 Mar 2026 15:40:01 +0800 Subject: [PATCH] Add ExaFree submodule and generate README for Register_GPT_v0 --- .gitmodules | 3 + CHANGELOG.md | 5 + README.md | 12 + Register_GPT_v0/.gitignore | 33 + Register_GPT_v0/LICENSE | 201 ++ Register_GPT_v0/README.md | 66 + Register_GPT_v0/__init__.py | 1 + ...SORA_ACTIVATION_AND_PHONE_BIND_ANALYSIS.md | 166 + .../docs/SORA_API_KEY_CALL_GUIDE_CN.md | 387 +++ .../docs/SORA_POOL_API_KEY_USAGE.md | 364 +++ Register_GPT_v0/main_protocol.py | 227 ++ Register_GPT_v0/protocol_register.py | 1511 +++++++++ Register_GPT_v0/protocol_sentinel.py | 138 + Register_GPT_v0/protocol_sora_phone.py | 2440 ++++++++++++++ Register_GPT_v0/run.py | 15 + Register_GPT_v0/screenshots/1.png | Bin 0 -> 103459 bytes Register_GPT_v0/screenshots/2.png | Bin 0 -> 140132 bytes Register_GPT_v0/screenshots/3.png | Bin 0 -> 113891 bytes Register_GPT_v0/screenshots/4.png | Bin 0 -> 121390 bytes Register_GPT_v0/screenshots/5.png | Bin 0 -> 179787 bytes Register_GPT_v0/scripts/__init__.py | 1 + .../scripts/get_outlook_refresh_token.py | 91 + .../scripts/sora_video_create_and_wait.py | 111 + Register_GPT_v0/tools/capture_sora_mobile.py | 65 + Register_GPT_v0/tools/monitor_sora_create.py | 119 + Register_GPT_v0/web/Dockerfile | 12 + Register_GPT_v0/web/backend/app/__init__.py | 1 + Register_GPT_v0/web/backend/app/config.py | 18 + Register_GPT_v0/web/backend/app/database.py | 322 ++ Register_GPT_v0/web/backend/app/main.py | 94 + .../web/backend/app/registration_env.py | 109 + .../web/backend/app/registration_state.py | 16 + .../web/backend/app/routers/__init__.py | 1 + .../web/backend/app/routers/accounts.py | 684 ++++ .../web/backend/app/routers/auth.py | 78 + .../web/backend/app/routers/bank_cards.py | 99 + .../web/backend/app/routers/dashboard.py | 71 + .../web/backend/app/routers/email_api.py | 135 + .../web/backend/app/routers/emails.py | 122 + .../web/backend/app/routers/logs.py | 51 + .../web/backend/app/routers/phone_bind.py | 54 + .../web/backend/app/routers/phones.py | 175 + .../web/backend/app/routers/register.py | 193 ++ .../web/backend/app/routers/settings.py | 114 + .../web/backend/app/routers/sms_api.py | 258 ++ .../web/backend/app/routers/sora_api.py | 2892 +++++++++++++++++ .../web/backend/app/routers/sora_keys.py | 151 + Register_GPT_v0/web/backend/app/security.py | 26 + .../web/backend/app/services/__init__.py | 1 + .../web/backend/app/services/hero_sms.py | 324 ++ .../web/backend/app/services/hotmail007.py | 113 + .../web/backend/app/services/otp_resolver.py | 161 + .../backend/app/services/phone_bind_runner.py | 460 +++ .../app/services/registration_runner.py | 483 +++ .../web/backend/app/services/sora_api_key.py | 166 + Register_GPT_v0/web/backend/requirements.txt | 7 + Register_GPT_v0/web/docker-compose.yml | 19 + Register_GPT_v0/web/frontend/index.html | 566 ++++ Register_GPT_v0/web/frontend/static/app.js | 2729 ++++++++++++++++ Register_GPT_v0/web/frontend/static/style.css | 2429 ++++++++++++++ Register_GPT_v0/web/run_web.py | 16 + packages/general/ExaFree | 1 + 62 files changed, 19107 insertions(+) create mode 100644 Register_GPT_v0/.gitignore create mode 100644 Register_GPT_v0/LICENSE create mode 100644 Register_GPT_v0/README.md create mode 100644 Register_GPT_v0/__init__.py create mode 100644 Register_GPT_v0/docs/SORA_ACTIVATION_AND_PHONE_BIND_ANALYSIS.md create mode 100644 Register_GPT_v0/docs/SORA_API_KEY_CALL_GUIDE_CN.md create mode 100644 Register_GPT_v0/docs/SORA_POOL_API_KEY_USAGE.md create mode 100644 Register_GPT_v0/main_protocol.py create mode 100644 Register_GPT_v0/protocol_register.py create mode 100644 Register_GPT_v0/protocol_sentinel.py create mode 100644 Register_GPT_v0/protocol_sora_phone.py create mode 100644 Register_GPT_v0/run.py create mode 100644 Register_GPT_v0/screenshots/1.png create mode 100644 Register_GPT_v0/screenshots/2.png create mode 100644 Register_GPT_v0/screenshots/3.png create mode 100644 Register_GPT_v0/screenshots/4.png create mode 100644 Register_GPT_v0/screenshots/5.png create mode 100644 Register_GPT_v0/scripts/__init__.py create mode 100644 Register_GPT_v0/scripts/get_outlook_refresh_token.py create mode 100644 Register_GPT_v0/scripts/sora_video_create_and_wait.py create mode 100644 Register_GPT_v0/tools/capture_sora_mobile.py create mode 100644 Register_GPT_v0/tools/monitor_sora_create.py create mode 100644 Register_GPT_v0/web/Dockerfile create mode 100644 Register_GPT_v0/web/backend/app/__init__.py create mode 100644 Register_GPT_v0/web/backend/app/config.py create mode 100644 Register_GPT_v0/web/backend/app/database.py create mode 100644 Register_GPT_v0/web/backend/app/main.py create mode 100644 Register_GPT_v0/web/backend/app/registration_env.py create mode 100644 Register_GPT_v0/web/backend/app/registration_state.py create mode 100644 Register_GPT_v0/web/backend/app/routers/__init__.py create mode 100644 Register_GPT_v0/web/backend/app/routers/accounts.py create mode 100644 Register_GPT_v0/web/backend/app/routers/auth.py create mode 100644 Register_GPT_v0/web/backend/app/routers/bank_cards.py create mode 100644 Register_GPT_v0/web/backend/app/routers/dashboard.py create mode 100644 Register_GPT_v0/web/backend/app/routers/email_api.py create mode 100644 Register_GPT_v0/web/backend/app/routers/emails.py create mode 100644 Register_GPT_v0/web/backend/app/routers/logs.py create mode 100644 Register_GPT_v0/web/backend/app/routers/phone_bind.py create mode 100644 Register_GPT_v0/web/backend/app/routers/phones.py create mode 100644 Register_GPT_v0/web/backend/app/routers/register.py create mode 100644 Register_GPT_v0/web/backend/app/routers/settings.py create mode 100644 Register_GPT_v0/web/backend/app/routers/sms_api.py create mode 100644 Register_GPT_v0/web/backend/app/routers/sora_api.py create mode 100644 Register_GPT_v0/web/backend/app/routers/sora_keys.py create mode 100644 Register_GPT_v0/web/backend/app/security.py create mode 100644 Register_GPT_v0/web/backend/app/services/__init__.py create mode 100644 Register_GPT_v0/web/backend/app/services/hero_sms.py create mode 100644 Register_GPT_v0/web/backend/app/services/hotmail007.py create mode 100644 Register_GPT_v0/web/backend/app/services/otp_resolver.py create mode 100644 Register_GPT_v0/web/backend/app/services/phone_bind_runner.py create mode 100644 Register_GPT_v0/web/backend/app/services/registration_runner.py create mode 100644 Register_GPT_v0/web/backend/app/services/sora_api_key.py create mode 100644 Register_GPT_v0/web/backend/requirements.txt create mode 100644 Register_GPT_v0/web/docker-compose.yml create mode 100644 Register_GPT_v0/web/frontend/index.html create mode 100644 Register_GPT_v0/web/frontend/static/app.js create mode 100644 Register_GPT_v0/web/frontend/static/style.css create mode 100644 Register_GPT_v0/web/run_web.py create mode 160000 packages/general/ExaFree diff --git a/.gitmodules b/.gitmodules index ee5c232..b4c2348 100644 --- a/.gitmodules +++ b/.gitmodules @@ -50,3 +50,6 @@ [submodule "packages/general/cursor-auto-register"] path = packages/general/cursor-auto-register url = https://github.com/VictorKTO/cursor-auto-register.git +[submodule "packages/general/ExaFree"] + path = packages/general/ExaFree + url = https://github.com/chengtx809/ExaFree.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 96dc5d7..f68ad5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 创建和管理光标配置文件 - 备份和恢复光标设置 - 适用于 Windows 和 macOS 系统 + - **packages/general/ExaFree** (submodule) - Exa 免费使用工具 + - 提供 Exa 相关服务的免费访问 + - 支持 Exa 功能的使用 + - 简单易用的界面 + - 适用于需要 Exa 服务的用户 - **Codex 相关子模块** - **packages/codex/codex-lb** (submodule) - Codex 负载均衡工具 diff --git a/README.md b/README.md index 4524152..d8dff11 100644 --- a/README.md +++ b/README.md @@ -299,6 +299,17 @@ AI-Account-Toolkit/ **使用指南**:[packages/general/cursor-auto-register/README.md](packages/general/cursor-auto-register/README.md) +### 24. ExaFree - Exa 免费使用工具 + +**功能**:Exa 免费使用工具,提供 Exa 相关服务的免费访问。 + +**主要文件**: +- 主程序文件 +- 配置文件 +- README.md - 项目说明 + +**使用指南**:[packages/general/ExaFree/README.md](packages/general/ExaFree/README.md) + ## 快速开始 ### 1. 环境准备 @@ -344,6 +355,7 @@ git submodule update - `packages/email/Hotmail-Outlook-Create-Account-Register-Auto/` - Hotmail 账号自动创建工具 - `packages/email/outlook-auto-register/` - Outlook 邮箱注册工具集 - `packages/general/cursor-auto-register/` - 光标设置管理工具 +- `packages/general/ExaFree/` - Exa 免费使用工具 ### 3. 配置设置 diff --git a/Register_GPT_v0/.gitignore b/Register_GPT_v0/.gitignore new file mode 100644 index 0000000..43a6c12 --- /dev/null +++ b/Register_GPT_v0/.gitignore @@ -0,0 +1,33 @@ +# 本地数据与数据库(勿提交到仓库) +data/ + +# 数据库文件 +*.db +*.db-journal +*.db-wal + +# Python +__pycache__/ +*.py[cod] +*.pyo +.venv/ +venv/ +env/ + +# 环境与密钥 +.env +.env.local +*.pem +.git_commit_msg.txt +Cookies + +# 日志与临时 +*.log +.DS_Store +logs/ +tmp_audio_check/ +.learnings/ + +# 调试与测试(勿提交) +debug_*.html +tests/ diff --git a/Register_GPT_v0/LICENSE b/Register_GPT_v0/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/Register_GPT_v0/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Register_GPT_v0/README.md b/Register_GPT_v0/README.md new file mode 100644 index 0000000..d9b054c --- /dev/null +++ b/Register_GPT_v0/README.md @@ -0,0 +1,66 @@ +# Register_GPT_v0 - GPT 注册工具 + +## 项目概述 + +Register_GPT_v0 是一个 GPT 账号注册工具,用于自动化注册 GPT 相关服务的账号。 + +## 核心功能 + +- 自动化 GPT 账号注册流程 +- 支持邮箱验证 +- 支持验证码处理 +- 支持代理配置 + +## 目录结构 + +``` +Register_GPT_v0/ +├── [相关文件] +└── README.md +``` + +## 环境要求 + +- Python 3.10+ +- 依赖项:根据项目需要安装 + +## 安装与使用 + +1. **安装依赖** + +```bash +pip install -r requirements.txt +``` + +2. **运行工具** + +```bash +python main.py +``` + +## 配置说明 + +具体配置选项请参考项目内部的配置文件和文档。 + +## 注意事项 + +- 请遵守各平台的使用条款,不要滥用注册功能 +- 确保网络环境稳定,避免注册过程中断 +- 合理设置注册频率,避免触发平台的反自动化机制 + +## 故障排除 + +如果遇到注册失败的情况,可以检查以下几点: + +- 网络连接是否正常 +- 邮箱服务是否可用 +- 验证码处理是否正确 +- 平台是否有新的注册限制 + +## 贡献 + +欢迎提交 Issue 和 Pull Request 来改进这个项目。 + +## 许可证 + +请参考项目的 LICENSE 文件。 \ No newline at end of file diff --git a/Register_GPT_v0/__init__.py b/Register_GPT_v0/__init__.py new file mode 100644 index 0000000..d6fe2c0 --- /dev/null +++ b/Register_GPT_v0/__init__.py @@ -0,0 +1 @@ +# 协议版注册:纯 HTTP 流程,见 protocol_register、main_protocol diff --git a/Register_GPT_v0/docs/SORA_ACTIVATION_AND_PHONE_BIND_ANALYSIS.md b/Register_GPT_v0/docs/SORA_ACTIVATION_AND_PHONE_BIND_ANALYSIS.md new file mode 100644 index 0000000..14c870f --- /dev/null +++ b/Register_GPT_v0/docs/SORA_ACTIVATION_AND_PHONE_BIND_ANALYSIS.md @@ -0,0 +1,166 @@ +# Sora 激活与手机号绑定 - 参考 genz27/sora-phone-bind 的实现分析 + +参考仓库:https://github.com/genz27/sora-phone-bind + +## 一、sora-phone-bind 核心接口(来自其 main.py,可直接对照源码) + +### 1. RT 转 AT + +- **URL**: `POST https://auth.openai.com/oauth/token` +- **Body** (JSON): + - `client_id`: 默认 `app_LlGpXReQgckcGGUo2JrYvtJK`(iOS/移动端用) + - `grant_type`: `"refresh_token"` + - `redirect_uri`: `"com.openai.chat://auth0.openai.com/ios/com.openai.chat/callback"` + - `refresh_token`: 我们的 RT +- **请求方式**: curl_cffi `AsyncSession`,`impersonate` 使用移动端指纹(如 `safari17_2_ios`、`safari18_0_ios`) +- **响应**: 取 `access_token`、`refresh_token`(若有新 RT 会返回) + +### 2. Sora 激活(有用户名才算“激活”) + +顺序如下: + +| 步骤 | 方法 | URL | 说明 | +|------|------|-----|------| +| 1 | GET | `https://sora.chatgpt.com/backend/m/bootstrap` | 激活 Sora2,必须先调 | +| 2 | GET | `https://sora.chatgpt.com/backend/me` | 获取当前用户信息 | +| 3 | 若 `me` 里已有 `username` | - | 视为已激活,结束 | +| 4 | POST | `https://sora.chatgpt.com/backend/project_y/profile/username/check` | Body: `{"username": "user_xxxxxxxx"}`,检查是否可用 | +| 5 | POST | `https://sora.chatgpt.com/backend/project_y/profile/username/set` | Body: `{"username": "user_xxxxxxxx"}`,设置用户名 | + +- **请求头**(与仓库一致): + - `Origin: https://sora.chatgpt.com` + - `Referer: https://sora.chatgpt.com/` + - `Authorization: Bearer {access_token}` + - `Content-Type: application/json` + - 移动端 UA(如 iPhone Safari / Chrome iOS),`Sec-Ch-Ua-Mobile: ?1`,`Sec-Ch-Ua-Platform: "iOS"` +- **请求方式**: 全部用 curl_cffi,`impersonate` 用移动端(如 `safari17_2_ios`),避免 403/404 因桌面端指纹被拦。 + +### 3. 手机号绑定 + +| 步骤 | 方法 | URL | Body | +|------|------|-----|------| +| 1 | POST | `https://sora.chatgpt.com/backend/project_y/phone_number/enroll/start` | `{"phone_number": "+1xxxxxxxxxx", "verification_expiry_window_ms": null}` | +| 2 | (轮询接码平台 API 获取短信验证码) | - | - | +| 3 | POST | `https://sora.chatgpt.com/backend/project_y/phone_number/enroll/finish` | `{"phone_number": "+1xxxxxxxxxx", "verification_code": "123456"}` | + +- 同一套请求头(Bearer AT + 移动端 UA/指纹)。 +- 若响应里含 `"already verified"` / `"phone number already"` 表示该号已被占用,需换号。 +- 验证码来源:接码平台提供的 API(配置格式为 `phone----api_url`,轮询 `api_url` 取 `data.code` 中 6 位数字)。 + +--- + +## 二、与我们当前实现的差异 + +| 项目 | 我们当前 | sora-phone-bind | +|------|----------|------------------| +| Sora 用户名设置 URL | 同:`/backend/project_y/profile/username/set` | 同 | +| 请求客户端 | requests / 桌面 Chrome 指纹 | curl_cffi + **移动端**指纹(Safari iOS 等) | +| 是否先调 bootstrap | 否 | **是**,先 GET `/backend/m/bootstrap` | +| 是否先 GET /backend/me | 否 | **是**,用于判断是否已有 username | +| 用户名生成 | 邮箱前缀 | 随机 `user_` + 8 位字母数字,且先 **check** 再 **set** | +| 手机号绑定 | 未做 | 有完整 enroll/start → 接码 → enroll/finish | + +我们之前 Sora 返回 403/404 很可能与**未用移动端指纹**、**未先调 bootstrap/me** 有关;路径本身与参考项目一致。 + +--- + +## 三、在我们项目中的实现建议 + +### 阶段 1:对齐 Sora 激活(先能稳定 200) + +1. **请求方式** + - 所有发往 `sora.chatgpt.com` 的请求改为 **curl_cffi**,`impersonate` 使用移动端(如 `safari17_2_ios`),与 sora-phone-bind 一致。 + +2. **调用顺序** + - 先 GET `https://sora.chatgpt.com/backend/m/bootstrap`(激活 Sora2); + - 再 GET `https://sora.chatgpt.com/backend/me`; + - 若 `me.username` 已存在,直接视为激活成功; + - 若无:生成随机 `user_xxxxxxxx`,先 POST `profile/username/check`,可用再 POST `profile/username/set`。 + +3. **请求头** + - 使用与 sora-phone-bind 相同的 Origin / Referer / Sec-Ch-Ua-Mobile / Sec-Ch-Ua-Platform 等(见其 `HEADERS` + 移动端 UA)。 + +4. **配置** + - 保持当前 `SORA_USERNAME_SET_URL` 为 `/backend/project_y/profile/username/set`,仅改调用顺序与指纹,不猜其它路径。 + +### 阶段 2:手机号绑定(独立流程,可选) + +1. **数据与配置** + - 接码平台配置:支持“手机号 + 获取验证码的 API URL”(可参考 sora-phone-bind 的 `phone----api_url` 或我们自己的表结构)。 + - 账号表可增加字段:如 `phone_bound`(是否已绑)、`phone`(脱敏存储可选)。 + +2. **流程** + - 输入:当前账号的 AT(或从 RT 用上述 client_id + redirect_uri 换 AT)。 + - 从池中取一个手机号,POST `phone_number/enroll/start`; + - 轮询接码 API 取 6 位验证码(与 sora-phone-bind 的 `get_code` 逻辑类似); + - POST `phone_number/enroll/finish` 提交验证码; + - 若返回“手机号已被使用”,换号重试或标记该号不可用; + - 成功后可选:再调一次 RT 换 AT 拿新 RT 并落库(若服务端返回新 RT)。 + +3. **与注册流程的关系** + - 注册完成后已有 AT/RT,先做**阶段 1 的 Sora 激活**; + - 手机号绑定可作为**单独任务/接口**(例如“对某账号或某批账号执行绑手机”),不必和注册强绑在同一请求里,便于接码池、重试、限流管理。 + +--- + +## 四、建议落地顺序 + +1. **先改 Sora 激活**:在 `protocol_register.py`(或独立 sora 模块)里,按上面顺序实现 bootstrap → me → check → set,且全部用 curl_cffi 移动端指纹;观察是否仍 403/404。 +2. **再做手机号绑定**:新增“绑手机”服务/接口,配置接码源,实现 enroll/start → 轮询 code → enroll/finish;数据库与前端按需加字段和入口。 +3. **RT 转 AT**:若我们已有用 web client_id 的换 token 逻辑,可保留;若需要与 sora-phone-bind 完全一致(例如为绑手机专门用移动端 client_id 换 AT),可增加一条分支使用其 `client_id` 与 `redirect_uri`。 + +以上接口与顺序均来自 [genz27/sora-phone-bind](https://github.com/genz27/sora-phone-bind) 的 main.py,可作为抓包/查资料之外的可靠实现参考。 + +--- + +## 五、本项目的「开始绑定手机」功能说明(实现文档补充) + +「开始绑定手机」即前端「手机号管理」页的 **开始绑定手机** 按钮所触发的批量任务:从**账号管理**读取待绑定账号,从**手机号管理**读取可用号码,使用**系统设置**里已配置的手机号接码 API 取验证码,依次完成 Sora 激活(若未激活)+ 调用 Sora 的 enroll/start、轮询验证码、enroll/finish,并回写账号与手机号状态。 + +### 5.1 数据来源 + +| 来源 | 表/接口 | 筛选与说明 | +|------|---------|------------| +| **账号** | `accounts`(账号管理) | `phone_bound = 0` 且 `(refresh_token IS NOT NULL OR access_token IS NOT NULL)`;按需排序(如 id 或 registered_at)。 | +| **手机号** | `phone_numbers`(手机号管理) | `used_count < max_use_count` 且 `activation_id IS NOT NULL`。来源:① 在「手机号管理」点「获取 OpenAI 号码」调用 `/api/sms-api/get-numbers` 写入;② **绑定任务执行时若表内无可用号码,会自动调接码 API 拉取**(与 get-numbers 同逻辑:hero_sms.get_number/get_number_v2,写入 phone_numbers 后继续绑定);③ 或手动添加(无 activation_id 则无法自动取码)。 | +| **接码配置** | 系统设置 | 已存在:`sms_api_url`、`sms_api_key`、`sms_openai_service`、`sms_max_price`。取验证码方式:现有接口 `GET /api/phones/{id}/sms-code`(内部调 `hero_sms.get_status_v2(base, key, activation_id)`),或后台任务内直接调 `hero_sms.get_status_v2`。 | + +与 sora-phone-bind 的差异:他们用「每行 phone----api_url」配置取码 URL;我们用「系统设置 sms_api_* + phone_numbers.activation_id」+ Hero-SMS 兼容协议(getStatusV2),无需 per-phone 的 api_url。 + +### 5.2 单条绑定流程(与参考项目对齐) + +对「一条账号 + 一个手机号」执行: + +1. **拿 AT** + 若账号无有效 `access_token`:用 `refresh_token` 调 `POST https://auth.openai.com/oauth/token`(body:client_id、grant_type=refresh_token、redirect_uri、refresh_token);可用 sora-phone-bind 的移动端 client_id/redirect_uri 或现有 web 配置;得到 AT(及可选新 RT 落库)。 + +2. **Sora 激活**(若尚未激活) + 顺序:GET `backend/m/bootstrap` → GET `backend/me`;若 `me.username` 已存在则跳过;否则随机 `user_xxxxxxxx`,POST `profile/username/check` → POST `profile/username/set`。请求均用 curl_cffi 移动端指纹发往 sora.chatgpt.com。 + +3. **发验证码** + `POST https://sora.chatgpt.com/backend/project_y/phone_number/enroll/start`,Body:`{"phone_number": "<当前手机号>", "verification_expiry_window_ms": null}`。若响应含 "already verified" / "phone number already":标记该手机号不可用并换号或跳过。 + +4. **轮询验证码** + 循环调用 `hero_sms.get_status_v2(base, key, activation_id)`(或等价地请求 `GET /api/phones/{id}/sms-code`),从返回中解析 6 位数字;超时或任务取消则结束本条。 + +5. **提交验证码** + `POST .../phone_number/enroll/finish`,Body:`{"phone_number": "<当前手机号>", "verification_code": "<6位码>"}`。 + +6. **落库** + - `accounts`:该账号 `phone_bound = 1`,可选写 `phone`(脱敏);若换 token 返回了新 RT 则更新 `refresh_token`。 + - `phone_numbers`:该行 `used_count = used_count + 1`。 + - `run_logs`:写入本条绑定结果(成功/失败原因)。 + +### 5.3 任务形态建议 + +- **触发**:前端「开始绑定手机」→ 调用后端 `POST /api/phone-bind/start`(可选参数:最大条数、是否仅未绑账号等),返回 `task_id`。 +- **执行**:后台异步任务队列;每次取一条「待绑定账号」+ 一条「可用手机号」,执行上述 5.2 流程;写 run_logs;支持暂停/停止(可复用现有注册任务的 stop 机制或单独 `phone_bind_stop` 状态)。 +- **进度与结果**:通过 `run_logs` 按 `task_id` 查询;或提供 `GET /api/phone-bind/status?task_id=xxx` 返回已处理数、成功数、失败数、当前状态。 + +### 5.4 实现检查清单 + +- [x] 后端:`POST /api/phone-bind/start` 创建任务,从 accounts 筛 `phone_bound=0` 且有 RT/AT,从 phone_numbers 筛可用且带 activation_id,入队执行。 +- [x] 后端:单条逻辑内实现 RT→AT、Sora 激活(bootstrap→me→check→set)、enroll/start、轮询 hero_sms.get_status_v2 取码、enroll/finish、更新 accounts 与 phone_numbers 及 run_logs。 +- [x] 后端:Sora 相关请求统一用 curl_cffi 移动端指纹(与阶段 1 一致)。 +- [x] 前端:「开始绑定手机」按钮改为请求 `POST /api/phone-bind/start`,并展示任务状态/日志(toast 展示 task_id,刷新仪表盘与日志;「停止绑定」调用 `POST /api/phone-bind/stop`;仪表盘加载时根据 `GET /api/phone-bind/status` 显示/隐藏停止按钮)。 +- [x] 配置:接码已用系统设置中的 sms_api_*,无需新增;账号与手机号均从现有「账号管理」「手机号管理」读取。 diff --git a/Register_GPT_v0/docs/SORA_API_KEY_CALL_GUIDE_CN.md b/Register_GPT_v0/docs/SORA_API_KEY_CALL_GUIDE_CN.md new file mode 100644 index 0000000..a1b5d83 --- /dev/null +++ b/Register_GPT_v0/docs/SORA_API_KEY_CALL_GUIDE_CN.md @@ -0,0 +1,387 @@ +# Sora Key 调用说明 + +这份文档说明如何调用本项目生成的 `srk_...` Key。 + +适用范围: + +- 文生视频 Key +- 图生视频 Key +- 文生 + 图生 Key +- 账号轮换池 Key + +## 1. 先说清楚这把 Key 是什么 + +- 这不是 OpenAI 官方 API Key。 +- 这把 Key 只能调用你本机这个项目暴露出来的本地包装接口。 +- 本地后端基址是: + +```text +http://127.0.0.1:1989 +``` + +不要用 `8000`。 + +## 2. 鉴权方式 + +推荐写法: + +```http +Authorization: Bearer srk_xxx +``` + +也支持: + +```http +X-API-Key: srk_xxx +``` + +下面示例统一用 `Authorization`。 + +## 3. Key 类型说明 + +### 3.1 文生视频 Key + +作用: + +- 可以调用文生视频创建接口 +- 可以调用视频任务查询 / 列表 / 取消 / 归档 + +不能做: + +- 图片上传 +- 图生视频创建 + +### 3.2 图生视频 Key + +作用: + +- 可以调用图片上传接口 +- 可以调用图生视频创建接口 +- 可以调用视频任务查询 / 列表 / 取消 / 归档 + +不能做: + +- 纯文生视频创建 + +### 3.3 文生 + 图生 Key + +作用: + +- 文生视频 +- 图生视频 +- 图片上传 +- 任务查询 / 列表 / 取消 / 归档 + +## 4. 轮换池 Key 是怎么工作的 + +如果这把 Key 是“轮换池 Key”,后端会自动: + +- 从可用 `Registered+Sora` 账号里选账号 +- 优先选当前活跃视频任务更少的账号 +- 创建任务前先做短租约占位,降低并发撞号概率 +- 记住 `task_id -> account_id` +- 后续 `get / cancel / archive` 自动回到原账号,不会查错号 + +当前“可用账号”的判断条件是: + +- `has_sora = 1` +- `sora_enabled = 1` +- `sora_quota_exhausted = 0` +- `refresh_token` 或 `access_token` 至少有一个 + +## 5. 当前可调用接口 + +```text +POST /api/sora-api/me +POST /api/sora-api/request + +POST /api/sora-api/video-gen/create +POST /api/sora-api/video-gen/create-and-wait +POST /api/sora-api/video-gen/upload-image +POST /api/sora-api/video-gen/create-with-image +POST /api/sora-api/video-gen/get +POST /api/sora-api/video-gen/list +POST /api/sora-api/video-gen/cancel +POST /api/sora-api/video-gen/archive +``` + +## 6. 最常用调用方式 + +下面把 `srk_xxx` 换成你的真实 Key。 + +### 6.1 检查这把 Key 当前用了哪个账号 + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/me \ + -H 'Authorization: Bearer srk_xxx' \ + -H 'Content-Type: application/json' \ + -d '{}' +``` + +返回里常见字段: + +- `used_account_id` +- `email` +- `me.username` + +### 6.2 文生视频:创建任务 + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/video-gen/create \ + -H 'Authorization: Bearer srk_xxx' \ + -H 'Content-Type: application/json' \ + -d '{ + "prompt": "A cinematic shot of ocean waves at sunrise.", + "n_variants": 1, + "n_frames": 300, + "resolution": 360, + "orientation": "wide" + }' +``` + +说明: + +- 后端会自动注入 `sora_create_task` sentinel +- 成功终态统一识别为 `succeeded` + +常见返回字段: + +- `task_id` +- `used_account_id` +- `used_email` +- `status` +- `normalized_status` + +### 6.3 文生视频:创建并轮询到出片 + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/video-gen/create-and-wait \ + -H 'Authorization: Bearer srk_xxx' \ + -H 'Content-Type: application/json' \ + -d '{ + "prompt": "A cinematic shot of ocean waves at sunrise.", + "n_variants": 1, + "n_frames": 300, + "resolution": 360, + "orientation": "wide", + "poll_interval_seconds": 5, + "timeout_seconds": 900 + }' +``` + +如果成功,关键字段一般有: + +- `ok: true` +- `task_id` +- `normalized_status: "succeeded"` +- `video_urls` + +### 6.4 图生视频:先上传图片 + +这一步只适用于: + +- 图生视频 Key +- 文生 + 图生 Key + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/video-gen/upload-image \ + -H 'Authorization: Bearer srk_xxx' \ + -F 'file=@/absolute/path/to/source.jpg' \ + -F 'auto_rotate=true' +``` + +成功后会拿到: + +- `media_id` +- `used_account_id` +- `used_email` + +### 6.5 图生视频:拿 `media_id` 创建任务 + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/video-gen/create \ + -H 'Authorization: Bearer srk_xxx' \ + -H 'Content-Type: application/json' \ + -d '{ + "prompt": "Animate this still image with gentle natural motion.", + "source_image_media_id": "media_xxx", + "n_variants": 1, + "n_frames": 300, + "resolution": 360, + "orientation": "wide" + }' +``` + +### 6.6 图生视频:一步上传并创建 + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/video-gen/create-with-image \ + -H 'Authorization: Bearer srk_xxx' \ + -F 'prompt=Animate this still image with gentle natural motion.' \ + -F 'file=@/absolute/path/to/source.jpg' \ + -F 'auto_rotate=true' \ + -F 'n_variants=1' \ + -F 'n_frames=300' \ + -F 'resolution=360' \ + -F 'orientation=wide' +``` + +## 7. 任务查询 + +### 7.1 查单个任务 + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/video-gen/get \ + -H 'Authorization: Bearer srk_xxx' \ + -H 'Content-Type: application/json' \ + -d '{ + "task_id": "task_xxx" + }' +``` + +### 7.2 查任务列表 + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/video-gen/list \ + -H 'Authorization: Bearer srk_xxx' \ + -H 'Content-Type: application/json' \ + -d '{ + "limit": 10, + "task_type_filter": "videos" + }' +``` + +### 7.3 取消任务 + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/video-gen/cancel \ + -H 'Authorization: Bearer srk_xxx' \ + -H 'Content-Type: application/json' \ + -d '{ + "task_id": "task_xxx" + }' +``` + +### 7.4 归档任务 + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/video-gen/archive \ + -H 'Authorization: Bearer srk_xxx' \ + -H 'Content-Type: application/json' \ + -d '{ + "task_id": "task_xxx" + }' +``` + +## 8. 返回字段怎么看 + +视频任务相关接口现在统一会尽量返回这些字段: + +- `status`: 上游原始状态 +- `normalized_status`: 本地归一化状态 +- `is_terminal`: 是否终态 +- `is_success`: 是否成功终态 +- `video_urls`: 已提取到的视频地址列表 + +成功终态认: + +```text +succeeded +``` + +不是 `completed`。 + +## 9. Python 调用示例 + +```python +import requests + +BASE_URL = "http://127.0.0.1:1989" +API_KEY = "srk_xxx" + +headers = { + "Authorization": f"Bearer {API_KEY}", + "Content-Type": "application/json", +} + +payload = { + "prompt": "A cinematic shot of ocean waves at sunrise.", + "n_variants": 1, + "n_frames": 300, + "resolution": 360, + "orientation": "wide", +} + +resp = requests.post( + f"{BASE_URL}/api/sora-api/video-gen/create", + headers=headers, + json=payload, + timeout=120, +) + +resp.raise_for_status() +print(resp.json()) +``` + +## 10. 常见错误 + +### 10.1 `401 Unauthorized` / `Invalid API key` + +通常是: + +- Key 写错了 +- Key 被停用了 +- Header 没带对 + +### 10.2 `403 当前 API Key 类型不能调用...` + +通常是 Key 类型不匹配: + +- 图生 Key 去调文生创建 +- 文生 Key 去调图片上传或图生创建 + +### 10.3 `404` + +通常是: + +- 打错端口 +- 打到了别的服务 +- 路径不是 `/api/sora-api/...` + +### 10.4 `429` / `quota_exceeded` + +表示当前账号额度不足。 + +如果是轮换池 Key: + +- 后端会优先尝试切到下一个可用账号 +- 若所有账号都耗尽,就会报出来 + +### 10.5 `too_many_concurrent_tasks` + +表示当前账号并发繁忙,不一定是额度没了。 + +## 11. 最短可用命令 + +如果你只是要最短的调用示例,直接用这个: + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/video-gen/create \ + -H 'Authorization: Bearer srk_xxx' \ + -H 'Content-Type: application/json' \ + -d '{ + "prompt": "A cinematic shot of ocean waves at sunrise.", + "n_variants": 1, + "n_frames": 300, + "resolution": 360, + "orientation": "wide" + }' +``` + +## 12. 文档位置 + +本说明文件就在: + +```text +docs/SORA_API_KEY_CALL_GUIDE_CN.md +``` diff --git a/Register_GPT_v0/docs/SORA_POOL_API_KEY_USAGE.md b/Register_GPT_v0/docs/SORA_POOL_API_KEY_USAGE.md new file mode 100644 index 0000000..ca91047 --- /dev/null +++ b/Register_GPT_v0/docs/SORA_POOL_API_KEY_USAGE.md @@ -0,0 +1,364 @@ +# Sora Pool API Key Usage + +## Scope + +- This key is for this local project only. +- It calls the local wrapper service under `/api/sora-api/*`. +- It is not an OpenAI official API key. + +## Base URL + +```text +http://127.0.0.1:1989 +``` + +Notes: + +- `1989` is the backend port for this project. +- `8000` on this machine is a different service and cannot be used for these routes. + +## Authentication + +Use the key in the `Authorization` header: + +```http +Authorization: Bearer srk_xxx +``` + +You can also use: + +```http +X-API-Key: srk_xxx +``` + +## Key Scope + +- `text_to_video`: can call text-to-video create routes. +- `image_to_video`: can call image upload / image-to-video create routes. +- `all_video`: can call both. +- List / get / cancel / archive routes now allow any video-capable key. + +## Pool Mode + +- Pool keys are created with `account_id = 0`. +- The backend automatically picks one available `Registered+Sora` account from the pool. +- Video task creation now prefers the account with the fewest active video tasks, then uses cursor order as a tie-breaker. +- The backend inserts a short-lived reservation before creating the upstream task, so concurrent create requests are less likely to collide on the same account. +- Active task occupancy is released after the task reaches a terminal state such as `succeeded`, `failed`, or `cancelled` via the tracked task routes. +- You do not need to pass `account_id`. + +## Main Endpoints + +### 1. Check current Sora account + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/me \ + -H 'Authorization: Bearer srk_xxx' \ + -H 'Content-Type: application/json' \ + -d '{}' +``` + +### 2. Generic Sora backend request + +Only `/backend/*` paths are allowed. + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/request \ + -H 'Authorization: Bearer srk_xxx' \ + -H 'Content-Type: application/json' \ + -d '{ + "method": "GET", + "path": "/backend/me", + "payload": {} + }' +``` + +### 3. Create video task + +This wrapper route now dispatches by task family: + +- Text-to-video: official Sora app / NF2 (`POST /backend/nf/create` or `/backend/nf/bulk_create`) +- Image-to-video: legacy storyboard path (`POST /backend/video_gen`) + +The backend will automatically inject the required `sora_create_task` sentinel header. + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/video-gen/create \ + -H 'Authorization: Bearer srk_xxx' \ + -H 'Content-Type: application/json' \ + -d '{ + "prompt": "A cinematic shot of ocean waves at sunrise.", + "n_variants": 4, + "n_frames": 300, + "resolution": 360, + "orientation": "wide" + }' +``` + +Response example: + +```json +{ + "ok": true, + "status_code": 200, + "task_family": "nf2", + "task_id": "task_01kkxqmadtex1t51mg3z62x0kh", + "used_account_id": 13, + "used_email": "example@outlook.com", + "data": { + "id": "task_01kkxqmadtex1t51mg3z62x0kh" + } +} +``` + +Optional audio-related fields for text-to-video: + +- `audio_caption`: describe ambient sound effects +- `audio_transcript`: provide spoken narration or dialogue + +Pool-mode note: + +- When you create a video task with a pool key, the backend now remembers `task_id -> used_account_id`. +- Later `get/cancel/archive` calls for that `task_id` will automatically reuse the same account. +- You can still pass `account_id` explicitly if you want to pin requests yourself. +- Success terminal state is `succeeded` (not `completed`). + +### 3a. Upload an image for image-to-video + +This route uploads the image to the upstream Sora account first and returns a reusable `media_id`. + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/video-gen/upload-image \ + -H 'Authorization: Bearer srk_xxx' \ + -F 'file=@/absolute/path/to/source.jpg' \ + -F 'auto_rotate=true' +``` + +Response example: + +```json +{ + "ok": true, + "status_code": 200, + "media_id": "media_01kkxy9ca6eb1vvtmvnsg5zbcq", + "used_account_id": 7, + "used_email": "example@outlook.com" +} +``` + +### 3c. Create image-to-video task from an uploaded `media_id` + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/video-gen/create \ + -H 'Authorization: Bearer srk_xxx' \ + -H 'Content-Type: application/json' \ + -d '{ + "prompt": "Animate this still image with gentle natural motion.", + "source_image_media_id": "media_01kkxy9ca6eb1vvtmvnsg5zbcq", + "n_variants": 1, + "n_frames": 300, + "resolution": 360, + "orientation": "wide" + }' +``` + +### 3d. One-shot image upload + create + +This is the easiest route for frontend or scripts that want a single request. + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/video-gen/create-with-image \ + -H 'Authorization: Bearer srk_xxx' \ + -F 'prompt=Animate this still image with gentle natural motion.' \ + -F 'file=@/absolute/path/to/source.jpg' \ + -F 'auto_rotate=true' \ + -F 'n_variants=1' \ + -F 'n_frames=300' \ + -F 'resolution=360' \ + -F 'orientation=wide' +``` + +### 3b. Create and wait until terminal state + +This route creates the task first, then polls `/video-gen/get` until the task reaches a terminal state or timeout. + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/video-gen/create-and-wait \ + -H 'Authorization: Bearer srk_xxx' \ + -H 'Content-Type: application/json' \ + -d '{ + "prompt": "A cinematic shot of ocean waves at sunrise.", + "n_variants": 1, + "n_frames": 300, + "resolution": 360, + "orientation": "wide", + "poll_interval_seconds": 5, + "timeout_seconds": 900 + }' +``` + +Response example: + +```json +{ + "ok": true, + "timed_out": false, + "task_id": "task_01kkxqmadtex1t51mg3z62x0kh", + "status": "succeeded", + "normalized_status": "succeeded", + "is_terminal": true, + "is_success": true, + "video_urls": [ + "https://..." + ], + "used_account_id": 13, + "used_email": "example@outlook.com", + "poll_attempts": 7, + "elapsed_seconds": 31.2 +} +``` + +### 4. Get video task + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/video-gen/get \ + -H 'Authorization: Bearer srk_xxx' \ + -H 'Content-Type: application/json' \ + -d '{ + "task_id": "task_01kkxqmadtex1t51mg3z62x0kh" + }' +``` + +`/video-gen/get`, `/video-gen/cancel`, `/video-gen/archive`, and `/video-gen/create-and-wait` now also return: + +- `normalized_status`: normalized task state, with successful terminal state unified to `succeeded` +- `is_terminal`: whether the task is at a terminal state +- `is_success`: whether the terminal state is successful +- `video_urls`: URLs extracted from the task payload when available + +### 5. List video tasks + +Use `task_type_filter: "videos"` for the normal video list. This value is what the live Sora backend currently expects. + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/video-gen/list \ + -H 'Authorization: Bearer srk_xxx' \ + -H 'Content-Type: application/json' \ + -d '{ + "limit": 10, + "task_type_filter": "videos" + }' +``` + +### 6. Cancel video task + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/video-gen/cancel \ + -H 'Authorization: Bearer srk_xxx' \ + -H 'Content-Type: application/json' \ + -d '{ + "task_id": "task_01kkxqmadtex1t51mg3z62x0kh" + }' +``` + +### 7. Archive video task + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/video-gen/archive \ + -H 'Authorization: Bearer srk_xxx' \ + -H 'Content-Type: application/json' \ + -d '{ + "task_id": "task_01kkxqmadtex1t51mg3z62x0kh" + }' +``` + +POST example: + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/request \ + -H 'Authorization: Bearer srk_xxx' \ + -H 'Content-Type: application/json' \ + -d '{ + "method": "POST", + "path": "/backend/some/path", + "payload": { + "foo": "bar" + } + }' +``` + +## Response Shape + +`/api/sora-api/me` usually returns: + +```json +{ + "ok": true, + "account_id": 13, + "email": "example@outlook.com", + "used_account_id": 13, + "me": { + "id": "user_xxx", + "username": "exampleuser" + } +} +``` + +## Common Errors + +### 404 Not Found + +Usually means you hit the wrong service or port. + +Check: + +- URL must be `http://127.0.0.1:1989` +- Path must be `/api/sora-api/...` + +### 401 Invalid API key + +Usually means: + +- the key is wrong +- the key is disabled +- you passed the wrong header + +### 404 / 429 inside Sora payload + +These are upstream Sora/backend results from the selected account, not local routing errors. + +### 400 invalid_request when creating video + +Usually means the payload shape is wrong. + +Use the dedicated route: + +- `/api/sora-api/video-gen/create` + +Do not post arbitrary JSON to `/backend/video_gen` unless you also match the live Sora payload shape. + +## Ready-to-use Script + +```bash +python scripts/sora_video_create_and_wait.py \ + --api-key srk_xxx \ + --prompt "A cinematic shot of ocean waves at sunrise." +``` + +Optional flags: + +- `--base-url http://127.0.0.1:1989` +- `--poll-interval 5` +- `--timeout 900` +- `--json` + +## Quick Verification + +If this command returns `200 OK`, the key is usable: + +```bash +curl -X POST http://127.0.0.1:1989/api/sora-api/me \ + -H 'Authorization: Bearer srk_xxx' \ + -H 'Content-Type: application/json' \ + -d '{}' +``` diff --git a/Register_GPT_v0/main_protocol.py b/Register_GPT_v0/main_protocol.py new file mode 100644 index 0000000..7314248 --- /dev/null +++ b/Register_GPT_v0/main_protocol.py @@ -0,0 +1,227 @@ +""" +协议版批量注册入口 +纯 HTTP 注册 + 可选浏览器绑卡体验 Plus。 + +用法: + 根目录: python run_protocol.py [--count N] [--workers W] [--plus] + 本目录: python run.py [--count N] [--workers W] [--plus] +""" + +import argparse +import builtins +import random +import threading +import time +from concurrent.futures import ThreadPoolExecutor, as_completed + + +def _progress_bar(current: int, total: int, width: int = 30, prefix: str = "") -> str: + """生成简易进度条字符串,如 [=========> ] 3/10""" + if total <= 0: + filled = 0 + else: + filled = min(int(width * current / total), width) + bar = "=" * filled + (">" if filled < width else "") + " " * max(0, width - filled - 1) + return f"{prefix}[{bar}] {current}/{total}" + + +def _log(msg: str, flush: bool = True) -> None: + print(msg, flush=flush) + + +_print_lock = threading.Lock() +# 主线程加载时保存一次真实 print,多线程里不能再用 builtins.print 赋值给 _orig_print(否则会变成 locked_print 导致死锁) +_orig_print = getattr(builtins, "print") + + +def _locked_print(*args, **kwargs): + kwargs.setdefault("flush", True) + with _print_lock: + _orig_print(*args, **kwargs) + + +def _register_one_task(do_plus: bool, index: int): + """单任务:设置当前账号索引并执行注册,多线程时用锁包装 print 避免输出交错。返回 (index, email, password, success)。""" + set_current_registration_index(index) + builtins.print = _locked_print + try: + email, password, success = _register_one_with_plus(do_plus) + return (index, email, password, success) + finally: + builtins.print = _orig_print + +from config import ( + cfg, + BATCH_INTERVAL_MIN, + BATCH_INTERVAL_MAX, + TOTAL_ACCOUNTS, + EMAIL_WORKER_URL, + set_current_registration_index, + get_proxy_url_for_session, +) +from email_outlook import load_outlook_accounts +from utils import ( + generate_random_password, + generate_user_info, + save_to_txt, + update_account_status, +) +from email_service import create_temp_email, wait_for_verification_email +from .protocol_register import register_one_protocol, activate_sora + + +def _register_one_with_plus(do_plus: bool): + """ + 单账号:协议注册 + 可选浏览器 Plus 试用。 + 返回: (email, password, success) + """ + email, jwt_token = create_temp_email() + if not email or not jwt_token: + print("[x] Failed to create temp email, skip this account") + return None, None, False + + password = generate_random_password() + user_info = generate_user_info() + + def get_otp(): + return wait_for_verification_email(jwt_token, email=email) + + result = register_one_protocol(email, password, jwt_token, get_otp, user_info) + email, password = result[0], result[1] + success = result[2] + status_extra = result[3] if len(result) > 3 else None + tokens = result[4] if len(result) > 4 else None + refresh_token = (tokens.get("refresh_token") or "") if isinstance(tokens, dict) else None + + if not success: + if status_extra == "finish_setup": + proxy_used = get_proxy_url_for_session() + save_to_txt(email, password, "Finish setup (check email)", proxy=proxy_used) + print("[*] Account saved: check email for 'Finish account setup' or try login with this email/password", flush=True) + return email, password, False + + proxy_used = get_proxy_url_for_session() + save_to_txt(email, password, "Registered", proxy=proxy_used, refresh_token=refresh_token) + + print("[*] Activating Sora2...", flush=True) + sora_ok = activate_sora( + tokens if isinstance(tokens, dict) else {}, + email, + proxy_url=proxy_used, + account_password=password, + get_otp_fn=get_otp, + ) + if sora_ok: + update_account_status(email, "Registered+Sora", password, proxy=proxy_used, refresh_token=refresh_token) + else: + print("[x] Account registered but Sora2 activation failed", flush=True) + return email, password, False + + if do_plus: + try: + from browser import create_driver, login, subscribe_plus_trial, cancel_subscription + driver = create_driver(headless=getattr(cfg.browser, "headless", False)) + try: + if login(driver, email, password): + if subscribe_plus_trial(driver): + update_account_status(email, "Plus activated", password, proxy=proxy_used) + time.sleep(5) + if cancel_subscription(driver): + update_account_status(email, "Subscription cancelled", password, proxy=proxy_used) + else: + update_account_status(email, "Cancel failed", password, proxy=proxy_used) + else: + update_account_status(email, "Plus failed", password, proxy=proxy_used) + finally: + driver.quit() + except Exception as e: + print(f"[!] Plus flow error: {e}") + update_account_status(email, "Plus error", password, proxy=proxy_used) + + return email, password, True + + +def run_batch_protocol(count: int = None, do_plus: bool = False, workers: int = 1): + if count is None: + count = TOTAL_ACCOUNTS + count = max(1, count) + workers = max(1, min(workers, count)) + + backend = (getattr(cfg.email, "backend", None) or "cloudflare").strip().lower() + if backend == "outlook": + accounts = load_outlook_accounts() + if not accounts: + _log("[x] backend=outlook but no accounts loaded; set email.outlook_accounts_file with lines: email----password----uuid----token") + return + else: + if not (EMAIL_WORKER_URL or "").strip(): + _log("[x] email.worker_url not set; configure config.yaml and retry") + return + + _log("\n" + "=" * 60) + _log(f"[*] Protocol batch registration total: {count} workers: {workers}" + (" (with Plus trial)" if do_plus else "")) + _log("=" * 60 + "\n") + _log("[!] For learning/research only; do not use for violations.\n") + time.sleep(2) + + success_count = 0 + fail_count = 0 + + if workers <= 1: + for i in range(count): + n = i + 1 + bar = _progress_bar(n, count, prefix="") + _log("\n" + "#" * 60) + _log(f"[*] Account {n}/{count} {bar}") + _log("#" * 60 + "\n") + set_current_registration_index(i) + email, password, success = _register_one_with_plus(do_plus) + if success: + success_count += 1 + _log(f"[ok] Account done success: {success_count} fail: {fail_count}") + else: + fail_count += 1 + _log(f"[x] Account failed success: {success_count} fail: {fail_count}") + _log("-" * 40) + if i < count - 1: + wait_time = random.randint(BATCH_INTERVAL_MIN, BATCH_INTERVAL_MAX) + _log(f"\n[*] Wait {wait_time}s before next account...") + time.sleep(wait_time) + else: + results = [None] * count + with ThreadPoolExecutor(max_workers=workers) as executor: + futures = {executor.submit(_register_one_task, do_plus, i): i for i in range(count)} + for fut in as_completed(futures): + idx = futures[fut] + try: + _, email, password, success = fut.result() + results[idx] = success + if success: + success_count += 1 + else: + fail_count += 1 + with _print_lock: + _log(f"[{'ok' if success else 'x'}] Account {idx + 1}/{count} success: {success_count} fail: {fail_count}") + except Exception as e: + results[idx] = False + fail_count += 1 + with _print_lock: + _log(f"[x] Account {idx + 1}/{count} exception: {e} success: {success_count} fail: {fail_count}") + + _log("\n" + "=" * 60) + _log("[*] Protocol batch registration finished") + _log(f" total: {count} success: {success_count} fail: {fail_count}") + _log("=" * 60) + + +def main(): + parser = argparse.ArgumentParser(description="Protocol batch ChatGPT registration (optional Plus trial)") + parser.add_argument("--count", "-n", type=int, default=None, help="Number of accounts, default from config") + parser.add_argument("--workers", "-w", type=int, default=1, help="Concurrent workers (threads), default 1") + parser.add_argument("--plus", "-p", action="store_true", help="After each registration, open browser for Plus trial then cancel") + args = parser.parse_args() + run_batch_protocol(count=args.count, do_plus=args.plus, workers=args.workers) + + +if __name__ == "__main__": + main() diff --git a/Register_GPT_v0/protocol_register.py b/Register_GPT_v0/protocol_register.py new file mode 100644 index 0000000..8a42799 --- /dev/null +++ b/Register_GPT_v0/protocol_register.py @@ -0,0 +1,1511 @@ +# -*- coding: utf-8 -*- +""" +协议版 ChatGPT 注册(严格按 protocol_keygen 一套) +入口:register_one_protocol(email, password, jwt_token, get_otp_fn, user_info, **kwargs)。 +流程(keygen 单流程):GET /oauth/authorize(screen_hint=signup) -> POST authorize/continue(sentinel) -> GET create-account/password -> POST user/register(sentinel) -> send_otp -> 邮局取验证码 -> validate_otp -> create_account -> callback -> 取 code 换 AT/RT 或 8.6 登录取 code 换 RT -> 返回 tokens 供 runner 写入账号列表。 +邮箱/代理/OAuth Client ID 等均从配置(Web 系统设置)获取。 +""" + +import base64 +import hashlib +import json +import os +import random +import re +import secrets +import time +import uuid +from urllib.parse import urlparse, parse_qs, urlencode +import requests +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry +import urllib3 + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +from config import ( + cfg, + HTTP_TIMEOUT, + get_proxy_url_for_session, +) +from utils import get_user_agent + +try: + from curl_cffi import requests as curl_requests + CURL_CFFI_AVAILABLE = True +except ImportError: + curl_requests = None + CURL_CFFI_AVAILABLE = False + +try: + from protocol_sentinel import build_sentinel_token, build_sentinel_token_pow_only +except Exception: + try: + from protocol.protocol_sentinel import build_sentinel_token, build_sentinel_token_pow_only + except Exception: + build_sentinel_token = None + build_sentinel_token_pow_only = None + +CHATGPT_ORIGIN = "https://chatgpt.com" +AUTH_ORIGIN = "https://auth.openai.com" + +# OAuth Code 换 Token(Codex / ChatGPT),运行时从 cfg.oauth 读(Web 下为系统设置) +OAUTH_ISSUER = AUTH_ORIGIN + + +def _format_error_status(prefix: str, payload) -> str: + """把协议错误转成可供 runner 判定/打标的稳定字符串。""" + code = "" + message = "" + if isinstance(payload, dict): + err = payload.get("error") or {} + if isinstance(err, dict): + code = (err.get("code") or "").strip() + message = (err.get("message") or "").strip() + parts = [prefix] + if code: + parts.append(code) + if message: + parts.append(message) + return ": ".join(parts) + + +def _get_oauth_client_id() -> str: + return (getattr(getattr(cfg, "oauth", None), "client_id", None) or "").strip() + + +def _get_oauth_redirect_uri() -> str: + return (getattr(getattr(cfg, "oauth", None), "redirect_uri", None) or "").strip() or f"{CHATGPT_ORIGIN}/" + + +def _has_cookie(session, name: str) -> bool: + """兼容 requests 与 curl_cffi:判断 session 是否含有名为 name 的 cookie。""" + try: + if getattr(session.cookies, "get", None): + if session.cookies.get(name): + return True + for c in getattr(session, "cookies", []): + if getattr(c, "name", None) == name: + return True + except Exception: + pass + return False + +# 密码规则:OpenAI 要求最少 12 位 +PASSWORD_MIN_LENGTH = 12 + + +class RetryException(Exception): + """需换 IP/会话重试时抛出;主循环捕获后重新开始。""" + pass + + +class RegistrationCancelled(Exception): + """用户请求停止注册时抛出。""" + pass + + +# 与 keygen 一致:使用 requests,TLS 指纹与 keygen 相同,便于过 CF +KEYGEN_USER_AGENT = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/145.0.0.0 Safari/537.36" +) + + +def _make_trace_headers(): + """与参考 chatgpt_register.py 一致:traceparent + datadog 头。""" + trace_id = random.randint(10**17, 10**18 - 1) + parent_id = random.randint(10**17, 10**18 - 1) + tp = f"00-{uuid.uuid4().hex}-{format(parent_id, '016x')}-01" + return { + "traceparent": tp, "tracestate": "dd=s:1;o:rum", + "x-datadog-origin": "rum", "x-datadog-sampling-priority": "1", + "x-datadog-trace-id": str(trace_id), "x-datadog-parent-id": str(parent_id), + } + + +def _mask_proxy_for_log(proxy: str) -> str: + """日志用:隐藏代理 URL 中的密码部分。""" + if not proxy or "@" not in proxy: + return proxy or "(无)" + try: + # protocol://user:pass@host -> protocol://user:****@host + pre, at_part = proxy.rsplit("@", 1) + if ":" in pre: + scheme_rest = pre.split("//", 1) + if len(scheme_rest) == 2 and ":" in scheme_rest[1]: + user, _ = scheme_rest[1].split(":", 1) + pre = f"{scheme_rest[0]}//{user}:****" + return f"{pre}@{at_part}" + except Exception: + return proxy[:50] + "..." if len(proxy or "") > 50 else (proxy or "(无)") + + +def _make_session(device_id: str = None): + """与 keygen 一致:使用 requests.Session()(TLS 指纹同 keygen,利于过 CF)。""" + proxy = get_proxy_url_for_session() + proxies = {"http": proxy, "https": proxy} if proxy else None + print(f"[*] 代理: {_mask_proxy_for_log(proxy)}", flush=True) + print("[*] Using requests (keygen 同款)", flush=True) + if device_id is None: + device_id = str(uuid.uuid4()) + + session = requests.Session() + retry = Retry(total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504]) + adapter = HTTPAdapter(max_retries=retry) + session.mount("https://", adapter) + session.mount("http://", adapter) + if proxies: + session.proxies = proxies + session.headers.update({ + "User-Agent": KEYGEN_USER_AGENT, + "Accept-Language": "en-US,en;q=0.9", + }) + try: + session.cookies.set("oai-did", device_id, domain=".auth.openai.com") + session.cookies.set("oai-did", device_id, domain="auth.openai.com") + except Exception: + pass + return session + + +# -------------------- 注册流程步骤(keygen 单流程) -------------------- + +def _ensure_password_page(session, state: str = None) -> None: + """0b 后 GET create-account/password,建立密码页会话后再 POST user/register(keygen 无此步,当前服务端可能依赖)。""" + url = f"{AUTH_ORIGIN}/create-account/password" + if state: + url = f"{url}?state={state}" + headers = { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "accept-language": "en-US,en;q=0.9", + "user-agent": KEYGEN_USER_AGENT, + "sec-ch-ua": '"Google Chrome";v="145", "Not?A_Brand";v="8", "Chromium";v="145"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "document", + "sec-fetch-mode": "navigate", + "sec-fetch-site": "same-origin", + "sec-fetch-user": "?1", + "upgrade-insecure-requests": "1", + "referer": f"{AUTH_ORIGIN}/create-account", + } + session.get(url, headers=headers, timeout=HTTP_TIMEOUT, allow_redirects=True, verify=False) + + +def _keygen_step0_oauth_and_continue(session, email: str, device_id: str, code_verifier: str, code_challenge: str, _step) -> str: + """ + keygen 可注册方案:GET /oauth/authorize (screen_hint=signup) + POST authorize/continue 带 sentinel。 + 代理从 config 的 get_proxy_url_for_session 已注入到 session。 + """ + client_id = _get_oauth_client_id() + if not client_id: + _step("[*] keygen 需配置 OAuth Client ID,跳过 Sentinel 流程") + return "" + redirect_uri = _get_oauth_redirect_uri() + state = secrets.token_urlsafe(32) + params = { + "response_type": "code", + "client_id": client_id, + "redirect_uri": redirect_uri, + "scope": "openid profile email offline_access", + "code_challenge": code_challenge, + "code_challenge_method": "S256", + "state": state, + "screen_hint": "signup", + "prompt": "login", + } + authorize_url = f"{AUTH_ORIGIN}/oauth/authorize?{urlencode(params)}" + _step("[*] keygen 0a GET /oauth/authorize (screen_hint=signup)") + # 与 keygen NAVIGATE_HEADERS 完全一致(含 user-agent、sec-ch-ua,无 Referer,verify=False) + nav_headers = { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "accept-language": "en-US,en;q=0.9", + "user-agent": KEYGEN_USER_AGENT, + "sec-ch-ua": '"Google Chrome";v="145", "Not?A_Brand";v="8", "Chromium";v="145"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "document", + "sec-fetch-mode": "navigate", + "sec-fetch-site": "same-origin", + "sec-fetch-user": "?1", + "upgrade-insecure-requests": "1", + } + try: + r = session.get(authorize_url, headers=nav_headers, timeout=HTTP_TIMEOUT, allow_redirects=True, verify=False) + except Exception as e: + print(f"[x] keygen 0a 失败: {e}", flush=True) + return "" + if not _has_cookie(session, "login_session"): + _step("[*] keygen 0a 未获得 login_session") + try: + preview = (getattr(r, "text", None) or "")[:300] + if preview: + print(f" 响应预览: {preview}", flush=True) + if "just a moment" in preview.lower() or "cloudflare" in preview.lower(): + print("[x] 0a 被 Cloudflare 拦截,请换代理或稍后用下一账号重试", flush=True) + except Exception: + pass + return "" + if not build_sentinel_token: + _step("[*] keygen 需 protocol_sentinel,跳过 Sentinel 流程") + return "" + sentinel_token = build_sentinel_token(session, device_id, flow="authorize_continue") + if not sentinel_token: + _step("[*] keygen 获取 sentinel token 失败") + return "" + _step("[*] keygen 0b POST authorize/continue + sentinel") + # 与 keygen 一致:COMMON_HEADERS + referer + oai-device-id + datadog + openai-sentinel-token + headers = { + "accept": "application/json", + "accept-language": "en-US,en;q=0.9", + "content-type": "application/json", + "origin": AUTH_ORIGIN, + "user-agent": KEYGEN_USER_AGENT, + "sec-ch-ua": '"Google Chrome";v="145", "Not?A_Brand";v="8", "Chromium";v="145"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-origin", + "referer": f"{AUTH_ORIGIN}/create-account", + "oai-device-id": device_id, + "openai-sentinel-token": sentinel_token, + } + headers.update(_make_trace_headers()) + try: + r = session.post( + f"{AUTH_ORIGIN}/api/accounts/authorize/continue", + json={"username": {"kind": "email", "value": email}, "screen_hint": "signup"}, + headers=headers, + timeout=HTTP_TIMEOUT, + verify=False, + ) + except Exception as e: + print(f"[x] keygen 0b 失败: {e}", flush=True) + return "" + if r.status_code != 200: + _step(f"[*] keygen 0b 返回 {r.status_code}") + return "" + return state + + +def _register_with_sentinel(session, email: str, password: str, device_id: str, _step) -> tuple: + """keygen 方案:POST user/register 带 openai-sentinel-token。先试完整 token(flow=authorize_continue),否则 PoW 仅串。""" + url = f"{AUTH_ORIGIN}/api/accounts/user/register" + headers = { + "accept": "application/json", + "accept-language": "en-US,en;q=0.9", + "content-type": "application/json", + "origin": AUTH_ORIGIN, + "user-agent": KEYGEN_USER_AGENT, + "sec-ch-ua": '"Google Chrome";v="145", "Not?A_Brand";v="8", "Chromium";v="145"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-origin", + "referer": f"{AUTH_ORIGIN}/create-account/password", + "oai-device-id": device_id, + } + headers.update(_make_trace_headers()) + sentinel_val = None + if build_sentinel_token: + sentinel_val = build_sentinel_token(session, device_id, flow="authorize_continue") + if not sentinel_val and build_sentinel_token_pow_only: + sentinel_val = build_sentinel_token_pow_only(device_id) + if sentinel_val: + headers["openai-sentinel-token"] = sentinel_val + r = session.post(url, json={"username": email, "password": password}, headers=headers, timeout=HTTP_TIMEOUT, verify=False) + try: + data = r.json() + except Exception: + data = {"text": (r.text or "")[:500]} + if r.status_code == 409: + err = data.get("error") or {} + err_code = err.get("code") if isinstance(err, dict) else None + if err_code == "invalid_state" or (isinstance(err, dict) and "invalid" in str(err).lower()): + raise RetryException("Step register returned 409 invalid_state") + if r.status_code == 400: + err = data.get("error") or {} + err_code = err.get("code") if isinstance(err, dict) else None + if err_code == "invalid_auth_step": + raise RetryException("Step register returned 400 invalid_auth_step") + return r.status_code, data + + +def _send_otp(session): + # keygen step3: NAVIGATE_HEADERS + referer, verify=False + url = f"{AUTH_ORIGIN}/api/accounts/email-otp/send" + headers = { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "accept-language": "en-US,en;q=0.9", + "user-agent": KEYGEN_USER_AGENT, + "sec-ch-ua": '"Google Chrome";v="145", "Not?A_Brand";v="8", "Chromium";v="145"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "document", + "sec-fetch-mode": "navigate", + "sec-fetch-site": "same-origin", + "sec-fetch-user": "?1", + "upgrade-insecure-requests": "1", + "referer": f"{AUTH_ORIGIN}/create-account/password", + } + r = session.get(url, headers=headers, timeout=HTTP_TIMEOUT, allow_redirects=True, verify=False) + try: + data = r.json() + except Exception: + data = {"final_url": str(r.url), "status": r.status_code} + return r.status_code, data + + +def _validate_otp(session, code: str): + url = f"{AUTH_ORIGIN}/api/accounts/email-otp/validate" + headers = { + "accept": "application/json", + "accept-language": "en-US,en;q=0.9", + "content-type": "application/json", + "origin": AUTH_ORIGIN, + "referer": f"{AUTH_ORIGIN}/email-verification", + "user-agent": KEYGEN_USER_AGENT, + "sec-ch-ua": '"Google Chrome";v="145", "Not?A_Brand";v="8", "Chromium";v="145"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-origin", + } + headers.update(_make_trace_headers()) + r = session.post(url, json={"code": code}, headers=headers, timeout=HTTP_TIMEOUT, verify=False) + try: + data = r.json() + except Exception: + data = {"text": (r.text or "")[:500]} + return r.status_code, data + + +def _create_account(session, name: str, birthdate: str): + url = f"{AUTH_ORIGIN}/api/accounts/create_account" + headers = { + "accept": "application/json", + "accept-language": "en-US,en;q=0.9", + "content-type": "application/json", + "origin": AUTH_ORIGIN, + "referer": f"{AUTH_ORIGIN}/about-you", + "user-agent": KEYGEN_USER_AGENT, + "sec-ch-ua": '"Google Chrome";v="145", "Not?A_Brand";v="8", "Chromium";v="145"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-origin", + } + headers.update(_make_trace_headers()) + r = session.post(url, json={"name": name, "birthdate": birthdate}, headers=headers, timeout=HTTP_TIMEOUT, verify=False) + try: + data = r.json() + except Exception: + data = {"text": (r.text or "")[:500]} + return r.status_code, data + + +def _callback(session, url: str): + if not url or not url.startswith("http"): + return None, None + headers = { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Upgrade-Insecure-Requests": "1", + } + r_first = session.get(url, headers=headers, timeout=HTTP_TIMEOUT, allow_redirects=False) + body_first = (r_first.text or "")[:50000] + location = r_first.headers.get("Location") or r_first.headers.get("location") or "" + if r_first.status_code in (301, 302, 303, 307, 308) and location: + r = session.get(location, headers=headers, timeout=HTTP_TIMEOUT, allow_redirects=True) + body = (r.text or "")[:50000] + final_url = str(r.url) + else: + r = r_first + body = body_first + final_url = str(r.url) + if not body and body_first: + body = body_first + return r.status_code, {"final_url": final_url, "body": body, "first_location": location} + + +def _generate_code_verifier() -> str: + """PKCE code_verifier,43~128 字符。""" + return base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b"=").decode("ascii") + + +def _generate_code_challenge(verifier: str) -> str: + """PKCE S256 code_challenge。""" + digest = hashlib.sha256(verifier.encode("utf-8")).digest() + return base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii") + + +def _parse_code_from_url(final_url: str) -> str: + """从 callback 最终 URL 的 query 或 fragment 中解析 OAuth code。""" + if not final_url or not isinstance(final_url, str): + return "" + try: + parsed = urlparse(final_url) + for part in (parsed.query, parsed.fragment): + if not part: + continue + params = parse_qs(part, keep_blank_values=False) + for key in ("code",): + vals = params.get(key) + if vals and isinstance(vals[0], str) and vals[0].strip(): + return vals[0].strip() + except Exception: + pass + return "" + + +def _parse_code_from_body(body: str) -> str: + """从 callback 响应体(HTML/JSON)中解析 OAuth code。""" + if not body or not isinstance(body, str): + return "" + try: + stripped = body.strip() + if stripped.startswith("{"): + data = json.loads(body) + if isinstance(data, dict): + c = data.get("code") or data.get("authorization_code") + if isinstance(c, str) and len(c.strip()) > 5: + return c.strip() + m = re.search(r"[\?&]code=([^&\s\"'<>]+)", body) + if m and m.group(1) and len(m.group(1).strip()) > 5: + return m.group(1).strip() + m = re.search(r"[\"']code[\"']\s*:\s*[\"']([^\"']{10,})[\"']", body, re.I) + if m: + return m.group(1).strip() + except Exception: + pass + return "" + + +def _parse_tokens_from_body(body: str) -> dict: + """从 callback 响应体(HTML/JSON)中解析 refresh_token、access_token。""" + out = {"refresh_token": "", "access_token": ""} + if not body or not isinstance(body, str): + return out + try: + stripped = body.strip() + if stripped.startswith("{"): + data = json.loads(body) + if isinstance(data, dict): + for key in ("refresh_token", "refresh_token_secret"): + v = data.get(key) + if isinstance(v, str) and len(v.strip()) > 10: + out["refresh_token"] = v.strip() + break + for key in ("access_token", "token"): + v = data.get(key) + if isinstance(v, str) and len(v.strip()) > 10: + out["access_token"] = v.strip() + break + for nest in ("session", "credentials", "auth"): + obj = data.get(nest) + if isinstance(obj, dict): + if not out["refresh_token"]: + v = obj.get("refresh_token") or obj.get("refresh_token_secret") + if isinstance(v, str) and len(v.strip()) > 10: + out["refresh_token"] = v.strip() + if not out["access_token"]: + v = obj.get("access_token") or obj.get("token") + if isinstance(v, str) and len(v.strip()) > 10: + out["access_token"] = v.strip() + for key_rt in ("refresh_token", "refresh_token_secret"): + m = re.search(r"[\"']" + re.escape(key_rt) + r"[\"']\s*:\s*[\"']([^\"']{15,})[\"']", body, re.I) + if m and not out["refresh_token"]: + out["refresh_token"] = m.group(1).strip() + break + for key_at in ("access_token", "token"): + m = re.search(r"[\"']" + re.escape(key_at) + r"[\"']\s*:\s*[\"']([^\"']{15,})[\"']", body, re.I) + if m and not out["access_token"]: + out["access_token"] = m.group(1).strip() + break + if not out["refresh_token"]: + m = re.search(r'"refresh_token"\s*:\s*"([A-Za-z0-9_\-\.]{50,800})"', body) + if m: + out["refresh_token"] = m.group(1).strip() + if not out["access_token"]: + m = re.search(r'"access_token"\s*:\s*"([A-Za-z0-9_\-\.]{50,1200})"', body) + if m: + out["access_token"] = m.group(1).strip() + if not out["refresh_token"] and "refresh_token" in body: + m = re.search(r"refresh_token[=:]\s*[\"']?([A-Za-z0-9_\-\.]{50,800})[\"']?", body, re.I) + if m: + out["refresh_token"] = m.group(1).strip() + except Exception: + pass + return out + + +def codex_exchange_code(session, code: str, code_verifier: str, redirect_uri: str = None): + """ + 用 authorization code 换取 Codex/ChatGPT tokens。与 keygen 一致:重试 1 次、Content-Type form、verify=False。 + POST https://auth.openai.com/oauth/token + redirect_uri 需与拿 code 时一致;不传则用系统设置或 chatgpt.com/。 + 返回含 access_token、refresh_token 等的 dict,失败返回 None。 + """ + client_id = _get_oauth_client_id() + if not client_id: + return None + uri = (redirect_uri or "").strip() or _get_oauth_redirect_uri() + resp = None + for attempt in range(2): + try: + resp = session.post( + f"{OAUTH_ISSUER}/oauth/token", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + data={ + "grant_type": "authorization_code", + "code": code, + "redirect_uri": uri, + "client_id": client_id, + "code_verifier": code_verifier, + }, + verify=False, + timeout=60, + ) + break + except Exception as e: + if attempt == 0: + print(" Token 交换超时,重试...", flush=True) + time.sleep(2) + continue + print(f" Token 交换失败: {e}", flush=True) + return None + if resp and resp.status_code == 200: + data = resp.json() + print(" Codex Token 获取成功!", flush=True) + print(f" Access Token 长度: {len(data.get('access_token', ''))}", flush=True) + print(f" Refresh Token: {'有' if data.get('refresh_token') else '无'}", flush=True) + print(f" ID Token: {'有' if data.get('id_token') else '无'}", flush=True) + return data + if resp: + print(f" Token 交换失败: {resp.status_code}", flush=True) + print(f" 响应: {(resp.text or '')[:300]}", flush=True) + return None + + +def _decode_oai_session_cookie(session) -> dict: + """从 oai-client-auth-session cookie 解码 JSON(尝试各 segment)。""" + val = "" + try: + val = (session.cookies.get("oai-client-auth-session") or "") if hasattr(session, "cookies") else "" + except Exception: + pass + if not val: + for c in getattr(session, "cookies", []): + if getattr(c, "name", None) == "oai-client-auth-session": + val = getattr(c, "value", "") or "" + break + if not val: + return {} + for i, part in enumerate(val.split(".")[:3]): + if not part: + continue + pad = 4 - len(part) % 4 + if pad != 4: + part = part + ("=" * pad) + try: + raw = base64.urlsafe_b64decode(part) + return json.loads(raw.decode("utf-8")) + except Exception: + continue + return {} + + +def _follow_consent_to_code(session, start_url: str, _step, max_depth: int = 15) -> str: + """跟随 consent 重定向链,从 302 Location 或 ConnectionError(重定向到 localhost)中解析 code。与 keygen _follow_and_extract_code 一致。""" + url = start_url + if not url or not url.startswith("http"): + url = f"{AUTH_ORIGIN}{start_url}" if start_url.startswith("/") else "" + if not url: + return "" + nav_headers = { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "accept-language": "en-US,en;q=0.9", + "user-agent": KEYGEN_USER_AGENT, + "sec-ch-ua": '"Google Chrome";v="145", "Not?A_Brand";v="8", "Chromium";v="145"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "document", + "sec-fetch-mode": "navigate", + "sec-fetch-site": "same-origin", + "sec-fetch-user": "?1", + "upgrade-insecure-requests": "1", + } + for _ in range(max_depth): + try: + r = session.get(url, headers=nav_headers, timeout=min(HTTP_TIMEOUT, 30), allow_redirects=False, verify=False) + except requests.exceptions.ConnectionError as e: + err_str = str(e) + m = re.search(r"(https?://(?:localhost|127\.0\.0\.1)[^\s\'\"<>]*)", err_str) + if m: + return _parse_code_from_url(m.group(1)) + return "" + except Exception as e: + err_str = str(e) + if "localhost" in err_str or "1455" in err_str or "127.0.0.1" in err_str: + m = re.search(r"(https?://(?:localhost|127\.0\.0\.1)[^\s\'\"<>]*)", err_str) + if m: + return _parse_code_from_url(m.group(1)) + return "" + if r.status_code in (301, 302, 303, 307, 308): + loc = (r.headers.get("Location") or r.headers.get("location") or "").strip() + if not loc: + return "" + code = _parse_code_from_url(loc) + if code: + return code + url = loc if loc.startswith("http") else f"{AUTH_ORIGIN}{loc}" + continue + if r.status_code == 200: + code = _parse_code_from_url(r.url) + if code: + return code + body = (r.text or "")[:200000] + code = _parse_code_from_body(body) + if code: + return code + next_url = "" + for pat in ( + r'"continue_url"\s*:\s*"([^"]+)"', + r'"redirect_url"\s*:\s*"([^"]+)"', + r'window\.location(?:\.href)?\s*=\s*["\']([^"\']+)["\']', + r'location\.replace\(\s*["\']([^"\']+)["\']\s*\)', + ): + m = re.search(pat, body, re.I) + if m and m.group(1): + next_url = m.group(1).replace("\\u0026", "&").replace("\\/", "/").strip() + break + if not next_url: + hrefs = re.findall(r'href=["\']([^"\']+)["\']', body, re.I) + for h in hrefs: + h2 = (h or "").replace("\\u0026", "&").replace("\\/", "/").strip() + if not h2: + continue + if "code=" in h2: + next_url = h2 + break + if "/oauth/" in h2 or "/auth/callback" in h2 or "localhost:1455" in h2: + next_url = h2 + break + if next_url: + code = _parse_code_from_url(next_url) + if code: + return code + url = next_url if next_url.startswith("http") else f"{AUTH_ORIGIN}{next_url}" + continue + _step(f"[*] 8.6 consent 200 但未解析到 code/next_url: {str(r.url)[:120]}") + try: + snippet = re.sub(r"\\s+", " ", body)[:220] + if snippet: + _step(f"[*] 8.6 consent body 片段: {snippet}") + except Exception: + pass + return "" + return "" + + +def _normalize_otp_code(raw) -> str: + digits = re.sub(r"\D", "", str(raw or "").strip()) + return digits[:6] if len(digits) >= 6 else "" + + +def _request_login_email_otp(session, device_id: str, _step) -> bool: + """ + 登录 8.6 邮箱验证码兜底触发:先 POST 再 GET /api/accounts/email-otp/send。 + 返回是否至少一次拿到 2xx。 + """ + ok = False + url = f"{AUTH_ORIGIN}/api/accounts/email-otp/send" + + api_headers = { + "accept": "application/json", + "accept-language": "en-US,en;q=0.9", + "content-type": "application/json", + "origin": AUTH_ORIGIN, + "referer": f"{AUTH_ORIGIN}/email-verification", + "user-agent": KEYGEN_USER_AGENT, + "sec-ch-ua": '"Google Chrome";v="145", "Not?A_Brand";v="8", "Chromium";v="145"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-origin", + "oai-device-id": device_id, + } + api_headers.update(_make_trace_headers()) + try: + r = session.post(url, json={}, headers=api_headers, timeout=min(HTTP_TIMEOUT, 30), verify=False) + _step(f"[*] 8.6 触发登录验证码发送 POST {r.status_code}") + if 200 <= r.status_code < 300: + ok = True + except Exception as e: + _step(f"[*] 8.6 触发登录验证码发送 POST 异常: {e}") + + nav_headers = { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "accept-language": "en-US,en;q=0.9", + "user-agent": KEYGEN_USER_AGENT, + "sec-ch-ua": '"Google Chrome";v="145", "Not?A_Brand";v="8", "Chromium";v="145"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "document", + "sec-fetch-mode": "navigate", + "sec-fetch-site": "same-origin", + "sec-fetch-user": "?1", + "upgrade-insecure-requests": "1", + "referer": f"{AUTH_ORIGIN}/email-verification", + } + try: + r = session.get(url, headers=nav_headers, timeout=min(HTTP_TIMEOUT, 30), allow_redirects=True, verify=False) + _step(f"[*] 8.6 触发登录验证码发送 GET {r.status_code}") + if 200 <= r.status_code < 300: + ok = True + except Exception as e: + _step(f"[*] 8.6 触发登录验证码发送 GET 异常: {e}") + return ok + + +def _poll_fresh_login_otp(get_otp_fn, _step, excluded_codes=None, attempts: int = 2) -> str: + excluded = set() + for x in (excluded_codes or []): + x_norm = _normalize_otp_code(x) + if x_norm: + excluded.add(x_norm) + rounds = max(1, attempts) + for i in range(rounds): + code = _normalize_otp_code(get_otp_fn() if get_otp_fn else "") + if code and code not in excluded: + return code + if code and code in excluded: + _step("[*] 8.6 收到旧验证码,继续等待新码...") + else: + _step("[*] 8.6 暂未取到新验证码") + if code: + excluded.add(code) + if i < rounds - 1: + time.sleep(2) + return "" + + +def _oauth_login_get_tokens(email: str, password: str, get_otp_fn, _step, prev_used_codes=None) -> dict: + """ + 严格按 keygen perform_codex_oauth_login_http:注册成功后用新 session 走 OAuth 登录, + GET authorize -> POST authorize/continue -> POST password/verify -> [email-otp] -> consent -> code 换 AT/RT。 + """ + client_id = _get_oauth_client_id() + if not client_id: + return {} + _step("[*] 8.6 登录取 RT(keygen 同款:新 session GET authorize -> ... -> code 换 token)") + device_id = str(uuid.uuid4()) + session = _make_session(device_id) + code_verifier = _generate_code_verifier() + code_challenge = _generate_code_challenge(code_verifier) + state = secrets.token_urlsafe(32) + redirect_uri = (_get_oauth_redirect_uri() or "").strip() or "http://localhost:1455/auth/callback" + params = { + "response_type": "code", + "client_id": client_id, + "redirect_uri": redirect_uri, + "scope": "openid profile email offline_access", + "code_challenge": code_challenge, + "code_challenge_method": "S256", + "state": state, + } + authorize_url = f"{AUTH_ORIGIN}/oauth/authorize?{urlencode(params)}" + nav_headers = { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "accept-language": "en-US,en;q=0.9", + "user-agent": KEYGEN_USER_AGENT, + "sec-ch-ua": '"Google Chrome";v="145", "Not?A_Brand";v="8", "Chromium";v="145"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "document", + "sec-fetch-mode": "navigate", + "sec-fetch-site": "same-origin", + "sec-fetch-user": "?1", + "upgrade-insecure-requests": "1", + } + try: + r = session.get(authorize_url, headers=nav_headers, timeout=HTTP_TIMEOUT, allow_redirects=True, verify=False) + except Exception as e: + _step(f"[*] 8.6 authorize 请求失败: {e}") + return {} + if not _has_cookie(session, "login_session"): + _step("[*] 8.6 未获得 login_session,可能需 sentinel") + api_headers = { + "accept": "application/json", + "accept-language": "en-US,en;q=0.9", + "content-type": "application/json", + "origin": AUTH_ORIGIN, + "user-agent": KEYGEN_USER_AGENT, + "sec-ch-ua": '"Google Chrome";v="145", "Not?A_Brand";v="8", "Chromium";v="145"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-origin", + "referer": f"{AUTH_ORIGIN}/log-in", + "oai-device-id": device_id, + } + api_headers.update(_make_trace_headers()) + if build_sentinel_token: + sentinel_ac = build_sentinel_token(session, device_id, flow="authorize_continue") + if sentinel_ac: + api_headers["openai-sentinel-token"] = sentinel_ac + try: + r = session.post( + f"{AUTH_ORIGIN}/api/accounts/authorize/continue", + json={"username": {"kind": "email", "value": email}}, + headers=api_headers, + timeout=HTTP_TIMEOUT, + verify=False, + ) + except Exception as e: + _step(f"[*] 8.6 authorize/continue 失败: {e}") + return {} + if r.status_code != 200: + _step(f"[*] 8.6 authorize/continue {r.status_code}(若 403 可能需 sentinel)") + try: + _step(f"[*] 8.6 响应: {(r.text or '')[:200]}") + except Exception: + pass + return {} + api_headers["referer"] = f"{AUTH_ORIGIN}/log-in/password" + api_headers.update(_make_trace_headers()) + if build_sentinel_token: + sentinel_pw = build_sentinel_token(session, device_id, flow="password_verify") + if sentinel_pw: + api_headers["openai-sentinel-token"] = sentinel_pw + try: + r = session.post( + f"{AUTH_ORIGIN}/api/accounts/password/verify", + json={"password": password}, + headers=api_headers, + timeout=HTTP_TIMEOUT, + allow_redirects=False, + verify=False, + ) + except Exception as e: + _step(f"[*] 8.6 password/verify 失败: {e}") + return {} + if r.status_code != 200: + _step(f"[*] 8.6 password/verify {r.status_code}(若 403 可能需 sentinel)") + try: + _step(f"[*] 8.6 响应: {(r.text or '')[:200]}") + except Exception: + pass + return {} + try: + data = r.json() + continue_url = (data.get("continue_url") or "").strip() + page_type = (data.get("page") or {}).get("type", "") + except Exception: + continue_url = "" + page_type = "" + if not continue_url: + _step("[*] 8.6 password/verify 200 但无 continue_url") + return {} + _step(f"[*] 8.6 continue_url: {continue_url[:80]}...") + if page_type == "email_otp_verification" or "email-verification" in continue_url: + excluded_codes = set(prev_used_codes or []) + # password/verify 进入邮箱验证页后,服务端通常已自动发码。 + # 先等现有新码,拿不到再主动 resend,避免刚发出的旧码被我们自己立刻作废。 + code_otp = _poll_fresh_login_otp(get_otp_fn, _step, excluded_codes=excluded_codes, attempts=1) + if not code_otp: + _request_login_email_otp(session, device_id, _step) + code_otp = _poll_fresh_login_otp(get_otp_fn, _step, excluded_codes=excluded_codes, attempts=2) + if not code_otp: + _step("[*] 8.6 需要邮箱验证码但未提供 get_otp_fn 或未取到") + return {} + api_headers["referer"] = f"{AUTH_ORIGIN}/email-verification" + api_headers.update(_make_trace_headers()) + try: + r = session.post( + f"{AUTH_ORIGIN}/api/accounts/email-otp/validate", + json={"code": code_otp}, + headers=api_headers, + timeout=HTTP_TIMEOUT, + verify=False, + ) + except Exception: + return {} + if r.status_code != 200: + _step(f"[*] 8.6 email-otp/validate {r.status_code}") + # 401 常见于验证码过期/拿到旧码,尝试再拉一枚新码重试一次 + if r.status_code in (400, 401) and get_otp_fn: + excluded_codes.add(code_otp) + code_otp_2 = _poll_fresh_login_otp(get_otp_fn, _step, excluded_codes=excluded_codes, attempts=1) + if not code_otp_2: + _request_login_email_otp(session, device_id, _step) + code_otp_2 = _poll_fresh_login_otp(get_otp_fn, _step, excluded_codes=excluded_codes, attempts=2) + if code_otp_2: + api_headers.update(_make_trace_headers()) + try: + r = session.post( + f"{AUTH_ORIGIN}/api/accounts/email-otp/validate", + json={"code": code_otp_2}, + headers=api_headers, + timeout=HTTP_TIMEOUT, + verify=False, + ) + _step(f"[*] 8.6 email-otp/validate 重试 {r.status_code}") + except Exception: + return {} + if r.status_code != 200: + return {} + try: + data = r.json() + continue_url = (data.get("continue_url") or "").strip() + except Exception: + pass + if continue_url and ( + "/about-you" in continue_url + or page_type in ("about_you", "about-you") + ): + _step("[*] 8.6 检测到 about-you,补提交资料后继续取 code") + yy = random.randint(1988, 2000) + mm = random.randint(1, 12) + dd = random.randint(1, 28) + status_create, data_create = _create_account(session, "User", f"{yy}-{str(mm).zfill(2)}-{str(dd).zfill(2)}") + if status_create not in (200, 201, 204): + _step(f"[*] 8.6 about-you 提交失败: {status_create}") + return {} + try: + next_url = ( + (data_create.get("continue_url") or "").strip() + or (data_create.get("url") or "").strip() + or (data_create.get("redirect_url") or "").strip() + ) + if next_url: + continue_url = next_url + _step(f"[*] 8.6 about-you -> continue_url: {continue_url[:80]}...") + except Exception: + pass + if not continue_url: + return {} + consent_url = continue_url if continue_url.startswith("http") else f"{AUTH_ORIGIN}{continue_url}" + auth_code = _follow_consent_to_code(session, consent_url, _step) + if not auth_code: + _step("[*] 8.6 直接 GET consent 未拿到 code,尝试 workspace/select...") + session_data = _decode_oai_session_cookie(session) + workspaces = (session_data or {}).get("workspaces") or [] + workspace_id = workspaces[0].get("id") if workspaces else None + if workspace_id: + api_headers["referer"] = consent_url + api_headers.update(_make_trace_headers()) + try: + r = session.post( + f"{AUTH_ORIGIN}/api/accounts/workspace/select", + json={"workspace_id": workspace_id}, + headers=api_headers, + timeout=HTTP_TIMEOUT, + allow_redirects=False, + verify=False, + ) + if r.status_code in (301, 302, 303, 307, 308): + loc = (r.headers.get("Location") or r.headers.get("location") or "").strip() + auth_code = _parse_code_from_url(loc) + if not auth_code and loc: + auth_code = _follow_consent_to_code( + session, loc if loc.startswith("http") else f"{AUTH_ORIGIN}{loc}", _step + ) + elif r.status_code == 200: + try: + ws_data = r.json() + ws_next = (ws_data.get("continue_url") or "").strip() + if ws_next: + auth_code = _follow_consent_to_code( + session, + ws_next if ws_next.startswith("http") else f"{AUTH_ORIGIN}{ws_next}", + _step, + ) + if not auth_code: + orgs = (ws_data.get("data") or {}).get("orgs") or [] + if orgs: + org_id = orgs[0].get("id") + proj = (orgs[0].get("projects") or [{}])[0].get("id") if orgs[0].get("projects") else None + body = {"org_id": org_id} + if proj: + body["project_id"] = proj + api_headers["referer"] = consent_url + api_headers.update(_make_trace_headers()) + r2 = session.post( + f"{AUTH_ORIGIN}/api/accounts/organization/select", + json=body, + headers=api_headers, + timeout=HTTP_TIMEOUT, + allow_redirects=False, + verify=False, + ) + if r2.status_code in (301, 302, 303, 307, 308): + loc2 = (r2.headers.get("Location") or r2.headers.get("location") or "").strip() + auth_code = _parse_code_from_url(loc2) or _follow_consent_to_code( + session, loc2 if loc2.startswith("http") else f"{AUTH_ORIGIN}{loc2}", _step + ) + elif r2.status_code == 200: + try: + next_url = (r2.json().get("continue_url") or "").strip() + if next_url: + auth_code = _follow_consent_to_code( + session, + next_url if next_url.startswith("http") else f"{AUTH_ORIGIN}{next_url}", + _step, + ) + except Exception: + pass + except Exception as e: + _step(f"[*] 8.6 workspace 响应解析异常: {e}") + except Exception as e: + _step(f"[*] 8.6 workspace/select 请求异常: {e}") + else: + _step("[*] 8.6 无 workspace_id(cookie 无 workspaces)") + if not auth_code: + _step("[*] 8.6 [4d] 备用: GET consent allow_redirects=True 以从最终 URL 或 ConnectionError 取 code") + nav_headers_4d = { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "accept-language": "en-US,en;q=0.9", + "user-agent": KEYGEN_USER_AGENT, + "sec-ch-ua": '"Google Chrome";v="145", "Not?A_Brand";v="8", "Chromium";v="145"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "document", + "sec-fetch-mode": "navigate", + "sec-fetch-site": "same-origin", + "sec-fetch-user": "?1", + "upgrade-insecure-requests": "1", + } + try: + r = session.get(consent_url, headers=nav_headers_4d, timeout=min(HTTP_TIMEOUT, 30), allow_redirects=True, verify=False) + auth_code = _parse_code_from_url(r.url) + if not auth_code and getattr(r, "history", None): + for h in r.history: + loc = (h.headers.get("Location") or h.headers.get("location") or "").strip() + auth_code = _parse_code_from_url(loc) + if auth_code: + break + except requests.exceptions.ConnectionError as e: + m = re.search(r"(https?://(?:localhost|127\.0\.0\.1)[^\s\'\"<>]*)", str(e)) + if m: + auth_code = _parse_code_from_url(m.group(1)) + except Exception: + pass + if not auth_code: + _step("[*] 8.6 跟随 consent 未解析到 code") + return {} + _step("[*] 8.6 已从 consent 拿到 code,换取 token...") + login_redirect_uri = (_get_oauth_redirect_uri() or "").strip() or "http://localhost:1455/auth/callback" + exchange = codex_exchange_code(session, auth_code, code_verifier, redirect_uri=login_redirect_uri) + if not exchange: + _step(f"[*] 8.6 code 换 token 失败,请确认 OAuth redirect_uri 与系统设置一致: {login_redirect_uri[:50]}...") + return {} + if not exchange.get("refresh_token"): + _step("[*] 8.6 换 token 成功但响应无 refresh_token") + try: + import protocol_sora_phone as sora_phone + except Exception: + try: + import protocol.protocol_sora_phone as sora_phone + except Exception: + sora_phone = None + if sora_phone: + try: + web_auth = sora_phone.sora_chatgpt_web_login_from_authenticated_session( + session, + email=email, + password=password, + get_otp_fn=get_otp_fn, + log_fn=_step, + ) + except Exception as e: + _step(f"[*] 8.7 复用登录 session 建立 Sora Web session 异常: {e}") + web_auth = {} + web_at = (web_auth.get("access_token") or "").strip() if isinstance(web_auth, dict) else "" + if web_at: + api_access_token = (exchange.get("access_token") or "").strip() + if api_access_token: + exchange["api_access_token"] = api_access_token + exchange["access_token"] = web_at + if isinstance(web_auth.get("session"), dict): + exchange["sora_session"] = dict(web_auth.get("session") or {}) + _step("[*] 8.7 已复用登录 session 建立 Sora Web session") + return dict(exchange) + + +def decode_jwt_payload(token: str) -> dict: + """解析 JWT token 的 payload 部分。""" + try: + parts = token.split(".") + if len(parts) != 3: + return {} + payload = parts[1] + padding = 4 - len(payload) % 4 + if padding != 4: + payload += "=" * padding + decoded = base64.urlsafe_b64decode(payload) + return json.loads(decoded) + except Exception: + return {} + + +def _parse_tokens_from_url(final_url: str) -> dict: + """从 callback 最终 URL 的 query 或 fragment 中解析 refresh_token、access_token。返回 {\"refresh_token\": \"\", \"access_token\": \"\"}。""" + out = {"refresh_token": "", "access_token": ""} + if not final_url or not isinstance(final_url, str): + return out + try: + parsed = urlparse(final_url) + for part in (parsed.query, parsed.fragment): + if not part: + continue + params = parse_qs(part, keep_blank_values=False) + for key_rt in ("refresh_token", "refresh_token_secret"): + vals = params.get(key_rt) or params.get(key_rt.replace("_", ".")) + if vals and isinstance(vals[0], str) and len(vals[0].strip()) > 10: + out["refresh_token"] = vals[0].strip() + break + for key_at in ("access_token", "token"): + vals = params.get(key_at) or params.get(key_at.replace("_", ".")) + if vals and isinstance(vals[0], str) and len(vals[0].strip()) > 10: + out["access_token"] = vals[0].strip() + break + except Exception: + pass + return out + + +def _parse_refresh_token_from_url(final_url: str) -> str: + """从 callback 最终 URL 的 query 或 fragment 中解析 refresh_token(兼容旧逻辑)。""" + return _parse_tokens_from_url(final_url).get("refresh_token", "") or "" + + +def _get_access_token_from_response(data: dict) -> str: + """从 create_account 等接口的 JSON 响应中提取 access_token(含 page 等嵌套)。""" + if not data or not isinstance(data, dict): + return "" + for key in ("access_token", "token"): + v = data.get(key) + if isinstance(v, str) and len(v.strip()) > 10: + return v.strip() + for nest in ("session", "credentials", "auth", "token", "page"): + obj = data.get(nest) + if isinstance(obj, dict): + v = obj.get("access_token") or obj.get("token") + if isinstance(v, str) and len(v.strip()) > 10: + return v.strip() + return "" + + +def _get_refresh_token_from_response(data: dict) -> str: + """从 create_account 等接口的 JSON 响应中提取 refresh_token(含 page 等嵌套)。""" + if not data or not isinstance(data, dict): + return "" + for key in ("refresh_token", "refresh_token_secret"): + v = data.get(key) + if isinstance(v, str) and len(v.strip()) > 10: + return v.strip() + for nest in ("session", "credentials", "auth", "token", "page"): + obj = data.get(nest) + if isinstance(obj, dict): + v = obj.get("refresh_token") or obj.get("refresh_token_secret") + if isinstance(v, str) and len(v.strip()) > 10: + return v.strip() + return "" + + +# -------------------- 入口 -------------------- + +def register_one_protocol(email: str, password: str, jwt_token: str, get_otp_fn, user_info: dict, **kwargs): + """ + 协议注册入口。 + 入参:email, password, jwt_token, get_otp_fn(), user_info(name/year/month/day), step_log_fn, stop_check 等。 + 返回:(email, password, success: bool[, status_extra[, tokens]])。 + """ + step_log_fn = kwargs.pop("step_log_fn", None) + stop_check = kwargs.pop("stop_check", None) + + def _step(msg: str): + if stop_check and callable(stop_check) and stop_check(): + raise RegistrationCancelled() + if msg: + print(msg, flush=True) + if step_log_fn: + try: + step_log_fn(msg.strip()) + except Exception: + pass + + _step(f"[*] register_one_protocol start {email}") + pwd = (password or "").strip() + if len(pwd) < PASSWORD_MIN_LENGTH: + raise ValueError(f"Password length must be >= {PASSWORD_MIN_LENGTH}, got {len(pwd)}. Set password in email row or use runner which auto-generates.") + password = pwd + name = user_info.get("name", "User") + year = user_info.get("year", "1990") + month = user_info.get("month", "01") + day = user_info.get("day", "01") + birthdate = f"{year}-{month}-{day}" + + device_id = str(uuid.uuid4()) + session = _make_session(device_id) + code_verifier = _generate_code_verifier() + code_challenge = _generate_code_challenge(code_verifier) + if not _get_oauth_client_id(): + _step("[*] 未配置 OAuth Client ID,请在系统设置中填写") + return email, password, False + if not build_sentinel_token: + _step("[*] Sentinel 未加载,请确保 protocol_sentinel 可用") + return email, password, False + try: + _step("[*] 0. GET authorize + POST authorize/continue (sentinel)") + try: + session.cookies.set("oai-did", device_id, domain=".auth.openai.com") + session.cookies.set("oai-did", device_id, domain="auth.openai.com") + except Exception: + pass + time.sleep(random.uniform(0.2, 0.5)) + auth_state = _keygen_step0_oauth_and_continue( + session, email, device_id, code_verifier, code_challenge, _step + ) + if not auth_state: + return email, password, False, "0a_no_session", None + time.sleep(random.uniform(0.5, 1.0)) + _step("[*] 1. GET create-account/password") + _ensure_password_page(session, auth_state) + time.sleep(random.uniform(0.5, 1.0)) + _step("[*] 2. Register (user/register + sentinel)") + status_reg, data_reg = _register_with_sentinel(session, email, password, device_id, _step) + if status_reg not in (200, 201, 204): + print(f"[x] 4. Register failed: status={status_reg} data={data_reg}", flush=True) + if status_reg == 400 and isinstance(data_reg, dict): + err = data_reg.get("error") or {} + if err.get("code") == "bad_request" or "register username" in str(err.get("message", "")).lower(): + print("[x] 若该邮箱已注册过,请换未注册邮箱重试", flush=True) + return email, password, False, _format_error_status("register_failed", data_reg) + print("[ok] 4. Register OK", flush=True) + _step("[*] 3. Send OTP") + status_otp, data_otp = _send_otp(session) + if status_otp not in (200, 201, 204) and (not isinstance(data_otp, dict) or data_otp.get("error")): + print(f"[x] 5. Send OTP failed: status={status_otp} data={data_otp}", flush=True) + return email, password, False, _format_error_status("send_otp_failed", data_otp) + + _step("[*] Waiting for email OTP...") + if stop_check and callable(stop_check) and stop_check(): + return email, password, False + code = get_otp_fn() + if not code or len(str(code).strip()) < 4: + print("[x] No OTP received or invalid", flush=True) + return email, password, False, "otp_missing" + # 规范为纯 6 位数字,避免空格/换行导致 wrong_email_otp_code + code = re.sub(r"\D", "", str(code).strip()) + if len(code) < 6: + print("[x] OTP too short after normalizing", flush=True) + return email, password, False, "otp_invalid" + code = code[:6] + + _step("[*] 6. Validate OTP") + status_val, data_val = _validate_otp(session, code) + if status_val not in (200, 201, 204): + err = (data_val.get("error") or {}) if isinstance(data_val, dict) else {} + err_code = err.get("code") if isinstance(err, dict) else "" + if status_val == 401 and err_code == "wrong_email_otp_code": + print("[x] 验证码错误或过期;正在重试一次获取新验证码...", flush=True) + time.sleep(3) + code2 = get_otp_fn() + if code2 and len(re.sub(r"\D", "", str(code2).strip())) >= 6: + code = re.sub(r"\D", "", str(code2).strip())[:6] + status_val, data_val = _validate_otp(session, code) + if status_val not in (200, 201, 204): + print(f"[x] 6. Validate OTP failed: status={status_val} data={data_val}", flush=True) + if status_val == 401 and (err_code == "wrong_email_otp_code" or "wrong" in str(err).lower()): + print("[x] 请确认验证码来自本邮箱最新一封 OpenAI 邮件且未过期;多任务并发时易拿错邮箱", flush=True) + return email, password, False, _format_error_status("validate_otp_failed", data_val) + + _step("[*] 7. Create account") + status_create, data_create = _create_account(session, name, birthdate) + if status_create not in (200, 201, 204): + print(f"[x] 7. Create account failed: status={status_create} data={data_create}", flush=True) + return email, password, False, _format_error_status("create_account_failed", data_create) + + callback_url = None + if isinstance(data_create, dict): + callback_url = data_create.get("continue_url") or data_create.get("url") or data_create.get("redirect_url") + + _step("[*] 8. Callback") + if callback_url: + _, _ = _callback(session, callback_url) + + print("[ok] Protocol registration success", flush=True) + has_client_id = bool(_get_oauth_client_id()) + if not has_client_id: + _step("[*] 未配置 OAuth Client ID,跳过登录取 RT;请在系统设置中填写") + tokens = {} + else: + _step("[*] 8. 按 keygen 仅通过登录取 RT(新 session GET authorize -> ... -> code 换 token)") + tokens = _oauth_login_get_tokens( + email, + password, + get_otp_fn, + _step, + prev_used_codes={code}, + ) + if tokens.get("refresh_token") or tokens.get("access_token"): + _step("[*] 8.6 登录取 code 已拿到 AT/RT") + else: + _step("[*] 8. 登录取 RT 未拿到 token(可能 403/sentinel 或 consent 未返回 code)") + if tokens.get("refresh_token") or tokens.get("access_token"): + _step(f"[*] 最终: RT={'有' if tokens.get('refresh_token') else '无'}, AT={'有' if tokens.get('access_token') else '无'}") + return email, password, True, None, (tokens if tokens else None) + except RegistrationCancelled: + print("[*] 注册已停止", flush=True) + return email, password, False + except RetryException: + raise + except (requests.RequestException, ValueError) as e: + print(f"[x] {e}", flush=True) + return email, password, False + except Exception as e: + print(f"[x] Unexpected error: {e}", flush=True) + return email, password, False + finally: + try: + session.close() + except Exception: + pass + + +SORA_ORIGIN = "https://sora.chatgpt.com" +# 旧版 project_y 用户名接口仅保留给兼容日志;实际激活逻辑委托 protocol_sora_phone.sora_ensure_activated。 +SORA_USERNAME_SET_URL = f"{SORA_ORIGIN}/backend/project_y/profile/username/set" + + +def _sora_username_from_email(email: str, max_len: int = 20) -> str: + """从邮箱生成 Sora 用户名:取 @ 前部分,只保留字母数字下划线。""" + if not email or "@" not in email: + return "user" + str(random.randint(1000, 9999)) + local = email.split("@", 1)[0].strip().lower() + safe = "".join(c for c in local if c.isalnum() or c == "_") + if not safe: + safe = "user" + safe = (safe[:max_len]) if len(safe) > max_len else safe + return safe if len(safe) >= 3 else f"user{random.randint(100, 999)}" + + +def activate_sora(tokens, email: str, **kwargs): + """ + 注册成功后激活 Sora。 + 优先走 protocol_sora_phone 里的新版 onboarding/me 链路,失败时由其内部回退旧接口。 + tokens 可为空;若无 AT/RT,会回退到 ChatGPT Web 登录补 access_token。 + kwargs: proxy_url, username(可选覆盖), step_log_fn。 + 返回 True 表示设置成功,False 表示未调或失败。 + """ + if isinstance(tokens, dict): + tokens = tokens + else: + tokens = {} + at = (tokens.get("access_token") or "").strip() + rt = (tokens.get("refresh_token") or "").strip() + if not rt and isinstance(tokens.get("session"), dict): + rt = ((tokens.get("session") or {}).get("refresh_token") or "").strip() + proxy_url = (kwargs.get("proxy_url") or "").strip() + step_log = kwargs.get("step_log_fn") + try: + import protocol_sora_phone as sora_phone + except Exception: + try: + import protocol.protocol_sora_phone as sora_phone + except Exception as exc: + if callable(step_log): + try: + step_log(f"[*] Sora helper 导入失败: {exc}") + except Exception: + pass + return False + if rt: + try: + mobile = sora_phone.rt_to_at_mobile(rt, proxy_url=proxy_url or None, log_fn=step_log) + mobile_at = (mobile.get("access_token") or "").strip() + mobile_rt = (mobile.get("refresh_token") or "").strip() + if mobile_at: + at = mobile_at + tokens["access_token"] = mobile_at + if callable(step_log): + try: + step_log("[*] Sora 使用移动端 RT->AT 成功") + except Exception: + pass + if mobile_rt: + rt = mobile_rt + tokens["refresh_token"] = mobile_rt + except Exception as exc: + if callable(step_log): + try: + step_log(f"[*] Sora 移动端 RT->AT 异常: {exc}") + except Exception: + pass + username = (kwargs.get("username") or "").strip() or _sora_username_from_email(email or "") + account_password = (kwargs.get("account_password") or "").strip() + get_otp_fn = kwargs.get("get_otp_fn") + if not at and not rt and not account_password: + if callable(step_log): + try: + step_log("[*] Sora 缺少 AT/RT 且无账号密码,无法走 Web session 回退") + except Exception: + pass + return False + try: + ok = False + if at: + ok = bool( + sora_phone.sora_ensure_activated( + at, + proxy_url=proxy_url or None, + log_fn=step_log, + username=username, + ) + ) + if ok: + return True + if account_password: + web_auth = sora_phone.sora_chatgpt_web_login( + email=email, + password=account_password, + get_otp_fn=get_otp_fn, + proxy_url=proxy_url or None, + log_fn=step_log, + ) + web_at = (web_auth.get("access_token") or "").strip() if isinstance(web_auth, dict) else "" + if web_at: + at = web_at + tokens["access_token"] = web_at + if isinstance(web_auth.get("session"), dict): + tokens["sora_session"] = dict(web_auth.get("session") or {}) + if callable(step_log): + try: + step_log("[*] Sora 已切换到 ChatGPT Web session access_token") + except Exception: + pass + return bool( + sora_phone.sora_ensure_activated( + at, + proxy_url=proxy_url or None, + log_fn=step_log, + username=username, + ) + ) + return False + except Exception as e: + if callable(step_log): + try: + step_log(f"[*] Sora 激活异常: {e}") + except Exception: + pass + return False diff --git a/Register_GPT_v0/protocol_sentinel.py b/Register_GPT_v0/protocol_sentinel.py new file mode 100644 index 0000000..0b655f8 --- /dev/null +++ b/Register_GPT_v0/protocol_sentinel.py @@ -0,0 +1,138 @@ +""" +Sentinel Token 生成(从 protocol_keygen 可注册方案移植)。 +用于 authorize/continue、user/register 等步骤的 openai-sentinel-token 头。 +""" +import base64 +import json +import random +import time +import uuid +from datetime import datetime, timezone + +# 与 keygen 一致,需与 sec-ch-ua 版本匹配 +SENTINEL_USER_AGENT = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/145.0.0.0 Safari/537.36" +) + + +class SentinelTokenGenerator: + """Sentinel PoW 纯 Python 生成器(逆向 sentinel SDK)。""" + + MAX_ATTEMPTS = 500000 + ERROR_PREFIX = "wQ8Lk5FbGpA2NcR9dShT6gYjU7VxZ4D" + + def __init__(self, device_id=None): + self.device_id = device_id or str(uuid.uuid4()) + self.requirements_seed = str(random.random()) + self.sid = str(uuid.uuid4()) + + @staticmethod + def _fnv1a_32(text): + h = 2166136261 + for ch in text: + h ^= ord(ch) + h = ((h * 16777619) & 0xFFFFFFFF) + h ^= (h >> 16) + h = ((h * 2246822507) & 0xFFFFFFFF) + h ^= (h >> 13) + h = ((h * 3266489909) & 0xFFFFFFFF) + h ^= (h >> 16) + return format(h & 0xFFFFFFFF, '08x') + + def _get_config(self): + screen_info = "1920x1080" + now = datetime.now(timezone.utc) + date_str = now.strftime("%a %b %d %Y %H:%M:%S GMT+0000 (Coordinated Universal Time)") + config = [ + screen_info, date_str, 4294705152, random.random(), + SENTINEL_USER_AGENT, "https://sentinel.openai.com/sentinel/20260124ceb8/sdk.js", + None, None, "en-US", "en-US,en", random.random(), + random.choice(["vendorSub", "productSub", "vendor", "maxTouchPoints"]) + "\u2212undefined", + random.choice(["location", "implementation", "URL", "documentURI", "compatMode"]), + random.choice(["Object", "Function", "Array", "Number", "parseFloat", "undefined"]), + random.uniform(1000, 50000), self.sid, "", + random.choice([4, 8, 12, 16]), time.time() * 1000 - random.uniform(1000, 50000), + ] + return config + + @staticmethod + def _base64_encode(data): + return base64.b64encode(json.dumps(data, separators=(',', ':'), ensure_ascii=False).encode()).decode() + + def _run_check(self, start_time, seed, difficulty, config, nonce): + config = list(config) + config[3] = nonce + config[9] = round((time.time() - start_time) * 1000) + data = self._base64_encode(config) + hash_hex = self._fnv1a_32(seed + data) + diff_len = len(difficulty) + if hash_hex[:diff_len] <= difficulty: + return data + "~S" + return None + + def generate_token(self, seed=None, difficulty=None): + if seed is None: + seed = self.requirements_seed + difficulty = difficulty or "0" + start_time = time.time() + config = self._get_config() + for i in range(self.MAX_ATTEMPTS): + result = self._run_check(start_time, seed, difficulty, config, i) + if result: + return "gAAAAAB" + result + return "gAAAAAB" + self.ERROR_PREFIX + self._base64_encode(str(None)) + + def generate_requirements_token(self): + config = self._get_config() + config[3] = 1 + config[9] = round(random.uniform(5, 50)) + return "gAAAAAC" + self._base64_encode(config) + + +def fetch_sentinel_challenge(session, device_id, flow="authorize_continue"): + """调用 sentinel 后端获取 challenge(含 c 与 proofofwork)。""" + gen = SentinelTokenGenerator(device_id=device_id) + p_token = gen.generate_requirements_token() + req_body = {"p": p_token, "id": device_id, "flow": flow} + headers = { + "Content-Type": "text/plain;charset=UTF-8", + "Referer": "https://sentinel.openai.com/backend-api/sentinel/frame.html", + "User-Agent": SENTINEL_USER_AGENT, + "Origin": "https://sentinel.openai.com", + "sec-ch-ua": '"Not:A-Brand";v="99", "Google Chrome";v="145", "Chromium";v="145"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + } + try: + resp = session.post( + "https://sentinel.openai.com/backend-api/sentinel/req", + data=json.dumps(req_body), headers=headers, timeout=15, verify=False, + ) + if resp.status_code != 200: + return None + return resp.json() + except Exception: + return None + + +def build_sentinel_token(session, device_id, flow="authorize_continue"): + """构建 openai-sentinel-token 头值(JSON 字符串,含 p/t/c/id/flow)。""" + challenge = fetch_sentinel_challenge(session, device_id, flow) + if not challenge: + return None + c_value = challenge.get("token", "") + pow_data = challenge.get("proofofwork", {}) or {} + gen = SentinelTokenGenerator(device_id=device_id) + if pow_data.get("required") and pow_data.get("seed"): + p_value = gen.generate_token(seed=pow_data["seed"], difficulty=pow_data.get("difficulty", "0")) + else: + p_value = gen.generate_requirements_token() + return json.dumps({"p": p_value, "t": "", "c": c_value, "id": device_id, "flow": flow}) + + +def build_sentinel_token_pow_only(device_id): + """仅 PoW 字符串(keygen 的 register 步骤用)。""" + gen = SentinelTokenGenerator(device_id=device_id) + return gen.generate_token() diff --git a/Register_GPT_v0/protocol_sora_phone.py b/Register_GPT_v0/protocol_sora_phone.py new file mode 100644 index 0000000..e01d0e6 --- /dev/null +++ b/Register_GPT_v0/protocol_sora_phone.py @@ -0,0 +1,2440 @@ +# -*- coding: utf-8 -*- +""" +Sora 激活与手机号绑定 HTTP 逻辑。 +优先对齐当前官方前端 bundle 暴露的 onboarding/me 接口,旧 project_y 接口仅作回退。 +全部使用 curl_cffi 移动端指纹请求 sora.chatgpt.com / auth.openai.com。 +供「开始绑定手机」任务调用,参数均显式传入(不依赖 config)。 +""" +import re +import random +import string +import uuid +import time +import os +from urllib.parse import parse_qs, urlencode, urlparse + +try: + from curl_cffi import requests as curl_requests + from curl_cffi import CurlMime + CURL_CFFI_AVAILABLE = True +except ImportError: + curl_requests = None + CurlMime = None + CURL_CFFI_AVAILABLE = False + +import requests + +try: + from protocol_sentinel import build_sentinel_token +except Exception: + try: + from protocol.protocol_sentinel import build_sentinel_token + except Exception: + build_sentinel_token = None + +SORA_ORIGIN = "https://sora.chatgpt.com" +SORA_LEGACY_ORIGIN = "https://sora.com" +CHATGPT_ORIGIN = "https://chatgpt.com" +AUTH_ORIGIN = "https://auth.openai.com" +CHATGPT_BACKEND_API_ORIGIN = f"{CHATGPT_ORIGIN}/backend-api" +CHATGPT_SECURITY_SETTINGS_URL = f"{CHATGPT_ORIGIN}/security-settings" +CHATGPT_MFA_RECENT_AUTH_MAX_AGE_SEC = 240 +CHATGPT_WEB_CLIENT_ID = "app_X8zY6vW2pQ9tR3dE7nK1jL5gH" +# 移动端 client_id / redirect_uri(与 sora-phone-bind 一致,用于 RT 换 AT) +MOBILE_CLIENT_ID = "app_LlGpXReQgckcGGUo2JrYvtJK" +MOBILE_REDIRECT_URI = "com.openai.chat://auth0.openai.com/ios/com.openai.chat/callback" + +MOBILE_FINGERPRINTS = ["safari17_2_ios", "safari18_0_ios"] +MOBILE_USER_AGENTS = [ + "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Mobile/15E148 Safari/604.1", +] +WEB_FINGERPRINT = "chrome131" +WEB_USER_AGENT = ( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/131.0.0.0 Safari/537.36" +) +DEFAULT_BROWSER_CDP_URLS = ( + "http://127.0.0.1:9222", + "http://127.0.0.1:9224", +) + +SORA_HEADERS_BASE = { + "Accept": "application/json, text/plain, */*", + "Accept-Language": "en-US,en;q=0.9", + "Cache-Control": "no-cache", + "Origin": SORA_ORIGIN, + "Pragma": "no-cache", + "Referer": f"{SORA_ORIGIN}/", + "Sec-Ch-Ua": '"Chromium";v="131", "Not_A Brand";v="24", "Google Chrome";v="131"', + "Sec-Ch-Ua-Mobile": "?1", + "Sec-Ch-Ua-Platform": '"iOS"', + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-origin", + "Content-Type": "application/json", +} + +DEFAULT_TIMEOUT = 30 +USERNAME_RETRY_CODES = { + "username_taken", + "username_rejected", + "username_invalid", + "username_required", + "reserved_username", +} + + +def _log(log_fn, message: str) -> None: + if callable(log_fn): + try: + log_fn(message) + except Exception: + pass + + +def _make_plain_session(proxy_url: str = None) -> requests.Session: + session = requests.Session() + session.trust_env = False + if proxy_url: + session.proxies = {"http": proxy_url, "https": proxy_url} + return session + + +def _make_web_session(proxy_url: str = None): + proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None + if CURL_CFFI_AVAILABLE and curl_requests: + session = curl_requests.Session(impersonate=WEB_FINGERPRINT) + if proxies: + session.proxies = proxies + return session + return _make_plain_session(proxy_url=proxy_url) + + +def _candidate_origins() -> tuple[str, ...]: + if SORA_LEGACY_ORIGIN == SORA_ORIGIN: + return (SORA_ORIGIN,) + return (SORA_ORIGIN, SORA_LEGACY_ORIGIN) + + +def _candidate_sora_web_origins(preferred_origin: str = "") -> tuple[str, ...]: + seen = [] + for value in ((preferred_origin or "").strip(), SORA_ORIGIN, SORA_LEGACY_ORIGIN): + origin = (value or "").rstrip("/") + if origin and origin not in seen: + seen.append(origin) + return tuple(seen) + + +def _candidate_browser_cdp_urls(cdp_urls=None) -> tuple[str, ...]: + raw_values = [] + if isinstance(cdp_urls, str): + raw_values.extend(cdp_urls.split(",")) + elif cdp_urls: + try: + raw_values.extend(list(cdp_urls)) + except Exception: + pass + env_value = ( + os.getenv("SORA_BROWSER_CDP_URLS") + or os.getenv("SORA_BROWSER_CDP_URL") + or "" + ).strip() + if env_value: + raw_values.extend(env_value.split(",")) + raw_values.extend(DEFAULT_BROWSER_CDP_URLS) + seen = [] + for value in raw_values: + item = str(value or "").strip() + if item and item not in seen: + seen.append(item) + return tuple(seen) + + +def _response_preview(resp, limit: int = 240) -> str: + try: + text = (resp.text or "").strip().replace("\n", " ") + except Exception: + text = "" + return text[:limit] + + +def _strip_nullish(value): + if isinstance(value, dict): + out = {} + for key, item in value.items(): + cleaned = _strip_nullish(item) + if cleaned is None: + continue + out[key] = cleaned + return out + if isinstance(value, list): + return [_strip_nullish(item) for item in value] + return value + + +def _decode_jwt_payload(token: str) -> dict: + value = (token or "").strip() + if not value: + return {} + parts = value.split(".") + if len(parts) != 3: + return {} + try: + payload = parts[1] + padding = (-len(payload)) % 4 + if padding: + payload += "=" * padding + import base64 + import json + return json.loads(base64.urlsafe_b64decode(payload.encode("ascii"))) + except Exception: + return {} + + +def _extract_error(resp) -> tuple[str, str, str]: + code = "" + message = "" + try: + data = resp.json() + except Exception: + return code, message, _response_preview(resp) + if isinstance(data, dict): + err = data.get("error") or {} + if isinstance(err, dict): + code = (err.get("code") or "").strip() + message = (err.get("message") or "").strip() + return code, message, _response_preview(resp) + + +def _build_sentinel_header(device_id: str, flow: str, proxy_url: str = None, log_fn=None) -> str: + if not build_sentinel_token: + return "" + try: + session = _make_plain_session(proxy_url=proxy_url) + return build_sentinel_token(session, device_id, flow=flow) or "" + except Exception as exc: + _log(log_fn, f"[sora] sentinel {flow} 异常: {exc}") + return "" + + +def _session_get(url: str, headers: dict, proxy_url: str = None, timeout: int = DEFAULT_TIMEOUT): + proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None + if CURL_CFFI_AVAILABLE and curl_requests: + return curl_requests.get( + url, + headers=headers, + proxies=proxies, + timeout=timeout, + impersonate=random.choice(MOBILE_FINGERPRINTS), + ) + session = _make_plain_session(proxy_url=proxy_url) + return session.get(url, headers=headers, timeout=timeout, verify=False) + + +def _session_post( + url: str, + headers: dict, + json: dict = None, + data: dict = None, + proxy_url: str = None, + timeout: int = DEFAULT_TIMEOUT, + allow_redirects: bool = True, +): + proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None + if CURL_CFFI_AVAILABLE and curl_requests: + return curl_requests.post( + url, + headers=headers, + json=json, + data=data, + proxies=proxies, + timeout=timeout, + allow_redirects=allow_redirects, + impersonate=random.choice(MOBILE_FINGERPRINTS), + ) + session = _make_plain_session(proxy_url=proxy_url) + kwargs = {"headers": headers, "timeout": timeout, "verify": False, "allow_redirects": allow_redirects} + if data is not None: + kwargs["data"] = data + else: + kwargs["json"] = json or {} + return session.post(url, **kwargs) + + +def _session_multipart_post( + url: str, + headers: dict, + *, + data: dict = None, + file_field_name: str, + filename: str, + file_bytes: bytes = None, + file_path: str = None, + content_type: str = None, + proxy_url: str = None, + timeout: int = DEFAULT_TIMEOUT, + allow_redirects: bool = True, +): + proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None + if CURL_CFFI_AVAILABLE and curl_requests and CurlMime is not None: + multipart_parts = [ + { + "name": file_field_name, + "filename": filename, + "content_type": content_type, + **({"local_path": file_path} if file_path else {"data": file_bytes or b""}), + } + ] + return curl_requests.post( + url, + headers=headers, + data=data or {}, + multipart=CurlMime.from_list(multipart_parts), + proxies=proxies, + timeout=timeout, + allow_redirects=allow_redirects, + impersonate=random.choice(MOBILE_FINGERPRINTS), + ) + session = _make_plain_session(proxy_url=proxy_url) + file_tuple = (filename, file_bytes if file_bytes is not None else open(file_path, "rb"), content_type) + try: + return session.post( + url, + headers=headers, + data=data or {}, + files={file_field_name: file_tuple}, + timeout=timeout, + verify=False, + allow_redirects=allow_redirects, + ) + finally: + if file_bytes is None and hasattr(file_tuple[1], "close"): + try: + file_tuple[1].close() + except Exception: + pass + + +def _web_session_get( + url: str, + headers: dict, + proxy_url: str = None, + timeout: int = DEFAULT_TIMEOUT, + allow_redirects: bool = True, + web_session=None, +): + if web_session is not None: + return web_session.get( + url, + headers=headers, + timeout=timeout, + verify=False, + allow_redirects=allow_redirects, + ) + proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None + if CURL_CFFI_AVAILABLE and curl_requests: + return curl_requests.get( + url, + headers=headers, + proxies=proxies, + timeout=timeout, + allow_redirects=allow_redirects, + impersonate=WEB_FINGERPRINT, + ) + session = _make_plain_session(proxy_url=proxy_url) + return session.get(url, headers=headers, timeout=timeout, verify=False, allow_redirects=allow_redirects) + + +def _web_session_post( + url: str, + headers: dict, + data: dict = None, + proxy_url: str = None, + timeout: int = DEFAULT_TIMEOUT, + allow_redirects: bool = True, + web_session=None, +): + if web_session is not None: + return web_session.post( + url, + headers=headers, + data=data or {}, + timeout=timeout, + verify=False, + allow_redirects=allow_redirects, + ) + proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None + if CURL_CFFI_AVAILABLE and curl_requests: + return curl_requests.post( + url, + headers=headers, + data=data, + proxies=proxies, + timeout=timeout, + allow_redirects=allow_redirects, + impersonate=WEB_FINGERPRINT, + ) + session = _make_plain_session(proxy_url=proxy_url) + return session.post( + url, + headers=headers, + data=data or {}, + timeout=timeout, + verify=False, + allow_redirects=allow_redirects, + ) + + +def _build_headers(access_token: str, device_id: str = None, origin: str = None) -> dict: + base_origin = (origin or SORA_ORIGIN).rstrip("/") + h = dict(SORA_HEADERS_BASE) + h["Origin"] = base_origin + h["Referer"] = f"{base_origin}/" + h["Authorization"] = f"Bearer {access_token}" + h["User-Agent"] = random.choice(MOBILE_USER_AGENTS) + h["oai-device-id"] = device_id or str(uuid.uuid4()) + return h + + +def _build_sora_web_headers( + access_token: str, + device_id: str = None, + referer: str = "", + origin: str = "", +) -> dict: + base_origin = (origin or SORA_ORIGIN).rstrip("/") + return { + "Accept": "application/json, text/plain, */*", + "Accept-Language": "en-US,en;q=0.9", + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json", + "Origin": base_origin, + "Referer": referer or f"{base_origin}/explore", + "User-Agent": WEB_USER_AGENT, + "Sec-Ch-Ua": '"Google Chrome";v="131", "Not_A Brand";v="24", "Chromium";v="131"', + "Sec-Ch-Ua-Mobile": "?0", + "Sec-Ch-Ua-Platform": '"macOS"', + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-origin", + "oai-device-id": device_id or str(uuid.uuid4()), + } + + +def _build_web_headers() -> dict: + return { + "Accept": "application/json, text/plain, */*", + "Accept-Language": "en-US,en;q=0.9", + "User-Agent": WEB_USER_AGENT, + } + + +def _web_session_json_post( + url: str, + headers: dict, + json: dict = None, + proxy_url: str = None, + timeout: int = DEFAULT_TIMEOUT, + allow_redirects: bool = True, + web_session=None, +): + if web_session is not None: + return web_session.post( + url, + headers=headers, + json=json or {}, + timeout=timeout, + verify=False, + allow_redirects=allow_redirects, + ) + proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None + if CURL_CFFI_AVAILABLE and curl_requests: + return curl_requests.post( + url, + headers=headers, + json=json or {}, + proxies=proxies, + timeout=timeout, + allow_redirects=allow_redirects, + impersonate=WEB_FINGERPRINT, + ) + session = _make_plain_session(proxy_url=proxy_url) + return session.post( + url, + headers=headers, + json=json or {}, + timeout=timeout, + verify=False, + allow_redirects=allow_redirects, + ) + + +def _build_html_headers(referer: str = "") -> dict: + headers = { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "Accept-Language": "en-US,en;q=0.9", + "Upgrade-Insecure-Requests": "1", + "User-Agent": WEB_USER_AGENT, + } + if referer: + headers["Referer"] = referer + return headers + + +def _build_chatgpt_backend_headers(access_token: str, device_id: str = None, referer: str = "") -> dict: + return { + "Accept": "application/json, text/plain, */*", + "Accept-Language": "en-US,en;q=0.9", + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json", + "Origin": CHATGPT_ORIGIN, + "Referer": referer or f"{CHATGPT_SECURITY_SETTINGS_URL}?action=enable&factor=sms", + "User-Agent": WEB_USER_AGENT, + "Sec-Ch-Ua": '"Google Chrome";v="131", "Not_A Brand";v="24", "Chromium";v="131"', + "Sec-Ch-Ua-Mobile": "?0", + "Sec-Ch-Ua-Platform": '"macOS"', + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-origin", + "oai-device-id": device_id or str(uuid.uuid4()), + } + + +def _load_register_helpers(): + try: + import protocol_register as pr + return pr + except Exception: + try: + import protocol.protocol_register as pr + return pr + except Exception: + return None + + +def _collect_response_urls(resp) -> list[str]: + urls = [] + seen = set() + + def _push(value: str): + candidate = (value or "").strip() + if not candidate or candidate in seen: + return + seen.add(candidate) + urls.append(candidate) + + try: + _push(str(getattr(resp, "url", "") or "")) + except Exception: + pass + try: + for item in getattr(resp, "history", []) or []: + _push(str(getattr(item, "url", "") or "")) + try: + _push((item.headers.get("Location") or item.headers.get("location") or "").strip()) + except Exception: + pass + except Exception: + pass + try: + _push((resp.headers.get("Location") or resp.headers.get("location") or "").strip()) + except Exception: + pass + return urls + + +def _copy_session_cookies(src_session, dst_session) -> None: + if src_session is None or dst_session is None: + return + try: + cookies = list(getattr(src_session, "cookies", None) or []) + except Exception: + cookies = [] + for cookie in cookies: + try: + dst_session.cookies.set( + cookie.name, + cookie.value, + domain=getattr(cookie, "domain", None), + path=getattr(cookie, "path", "/") or "/", + ) + continue + except Exception: + pass + try: + dst_session.cookies.set(cookie.name, cookie.value) + except Exception: + pass + + +def _copy_browser_cookie_dicts(cookies: list[dict], dst_session) -> int: + if dst_session is None: + return 0 + copied = 0 + for cookie in cookies or []: + if not isinstance(cookie, dict): + continue + name = str(cookie.get("name") or "").strip() + if not name: + continue + value = cookie.get("value") or "" + kwargs = { + "domain": cookie.get("domain") or None, + "path": cookie.get("path") or "/", + } + secure = cookie.get("secure") + if secure is not None: + kwargs["secure"] = bool(secure) + expires = cookie.get("expires") + try: + expires_value = float(expires) + except Exception: + expires_value = None + if expires_value and expires_value > 0: + kwargs["expires"] = int(expires_value) + try: + dst_session.cookies.set(name, value, **kwargs) + copied += 1 + continue + except Exception: + pass + try: + dst_session.cookies.set(name, value) + copied += 1 + except Exception: + pass + return copied + + +def _extract_api_error(resp) -> tuple[str, str, str]: + code, message, preview = _extract_error(resp) + if code or message: + return code, message, preview + try: + data = resp.json() + except Exception: + return code, message, preview + if not isinstance(data, dict): + return code, message, preview + detail = data.get("detail") + if isinstance(detail, dict): + code = (detail.get("code") or detail.get("type") or code or "").strip() + message = (detail.get("message") or detail.get("detail") or message or "").strip() + elif isinstance(detail, str) and detail.strip(): + raw_detail = detail.strip() + try: + import json + nested = json.loads(raw_detail) + except Exception: + nested = None + if isinstance(nested, dict): + nested_err = nested.get("error") or {} + if isinstance(nested_err, dict): + code = (nested_err.get("code") or nested_err.get("type") or code or "").strip() + message = (nested_err.get("message") or message or "").strip() + else: + message = raw_detail[:200] + else: + message = raw_detail[:200] + else: + code = (data.get("code") or data.get("type") or code or "").strip() + message = (data.get("message") or message or "").strip() + return code, message, preview + + +def _normalize_phone_number(phone_number: str) -> str: + raw = (phone_number or "").strip() + if not raw: + return "" + digits = re.sub(r"\D", "", raw) + if digits.startswith("00"): + digits = digits[2:] + if not digits: + return "" + return f"+{digits}" + + +def _chatgpt_pwd_auth_age_seconds(access_token: str): + payload = _decode_jwt_payload(access_token) + raw = payload.get("pwd_auth_time") + try: + value = float(raw) + except Exception: + return None + if value <= 0: + return None + if value > 10_000_000_000: + value = value / 1000.0 + return max(0, int(time.time() - value)) + + +def _chatgpt_needs_recent_auth(access_token: str) -> bool: + age = _chatgpt_pwd_auth_age_seconds(access_token) + return age is None or age > CHATGPT_MFA_RECENT_AUTH_MAX_AGE_SEC + + +def _warm_chatgpt_security_page(web_session, log_fn=None) -> str: + page_url = f"{CHATGPT_SECURITY_SETTINGS_URL}?action=enable&factor=sms" + try: + resp = web_session.get( + page_url, + headers=_build_html_headers(referer=CHATGPT_ORIGIN), + timeout=DEFAULT_TIMEOUT, + allow_redirects=True, + ) + return str(getattr(resp, "url", "") or page_url) + except Exception as exc: + _log(log_fn, f"[phone_bind] security-settings 预热异常: {exc}") + return page_url + + +def _read_sora_web_session(web_session, log_fn=None, preferred_origin: str = "") -> dict: + for origin in _candidate_sora_web_origins(preferred_origin): + sora_session_resp = web_session.get( + f"{origin}/api/auth/session", + headers={**_build_web_headers(), "Referer": f"{origin}/"}, + timeout=DEFAULT_TIMEOUT, + ) + if sora_session_resp.status_code != 200: + _log(log_fn, f"[sora] sora /api/auth/session HTTP {sora_session_resp.status_code} {_response_preview(sora_session_resp, 140)}") + continue + try: + sora_session = sora_session_resp.json() + except Exception: + _log(log_fn, f"[sora] sora /api/auth/session 非 JSON: {_response_preview(sora_session_resp, 140)}") + continue + if not isinstance(sora_session, dict) or not sora_session or sora_session == {"WARNING_BANNER": sora_session.get("WARNING_BANNER")}: + _log(log_fn, f"[sora] sora /api/auth/session 返回空会话: {_response_preview(sora_session_resp, 140)}") + continue + access_token = (sora_session.get("accessToken") or "").strip() + if access_token: + payload = _decode_jwt_payload(access_token) + client_id = (payload.get("client_id") or "").strip() + _log(log_fn, f"[sora] Web session 已建立 origin={origin} client_id={client_id or '-'}") + return {"access_token": access_token, "session": sora_session, "base_origin": origin} + return {} + + +def _read_chatgpt_web_session(web_session, log_fn=None) -> dict: + chatgpt_session_resp = web_session.get( + f"{CHATGPT_ORIGIN}/api/auth/session", + headers={**_build_web_headers(), "Referer": f"{CHATGPT_ORIGIN}/"}, + timeout=DEFAULT_TIMEOUT, + ) + if chatgpt_session_resp.status_code != 200: + _log(log_fn, f"[phone_bind] chatgpt /api/auth/session HTTP {chatgpt_session_resp.status_code} {_response_preview(chatgpt_session_resp, 140)}") + return {} + try: + session_data = chatgpt_session_resp.json() + except Exception: + _log(log_fn, f"[phone_bind] chatgpt /api/auth/session 非 JSON: {_response_preview(chatgpt_session_resp, 140)}") + return {} + if not isinstance(session_data, dict) or not session_data: + _log(log_fn, f"[phone_bind] chatgpt /api/auth/session 返回空会话: {_response_preview(chatgpt_session_resp, 140)}") + return {} + access_token = (session_data.get("accessToken") or "").strip() + if access_token: + payload = _decode_jwt_payload(access_token) + client_id = (payload.get("client_id") or "").strip() + age = _chatgpt_pwd_auth_age_seconds(access_token) + _log(log_fn, f"[phone_bind] ChatGPT Web session 已建立 client_id={client_id or '-'} pwd_auth_age={age if age is not None else '-'}s") + return {"access_token": access_token, "session": session_data} + + +def sora_probe_nf2_session( + access_token: str, + *, + proxy_url: str = None, + web_session=None, + preferred_origin: str = "", + log_fn=None, +) -> dict: + token = (access_token or "").strip() + if not token: + return {} + last_status = 0 + last_preview = "" + for origin in _candidate_sora_web_origins(preferred_origin): + try: + resp = sora_nf2_get_pending( + token, + proxy_url=proxy_url, + web_session=web_session, + base_origin=origin, + ) + except Exception as exc: + _log(log_fn, f"[sora] NF2 probe 请求异常 origin={origin}: {exc}") + continue + status_code = int(getattr(resp, "status_code", 0) or 0) + preview = _response_preview(resp, 160) + if status_code == 200: + return { + "ok": True, + "status_code": status_code, + "base_origin": origin, + "preview": preview, + } + last_status = status_code + last_preview = preview + _log(log_fn, f"[sora] NF2 probe HTTP {status_code} origin={origin} {preview or '-'}") + return { + "ok": False, + "status_code": last_status, + "base_origin": "", + "preview": last_preview, + } + + +def sora_import_browser_web_session( + *, + expected_email: str = "", + preferred_origin: str = "", + cdp_urls=None, + log_fn=None, +) -> dict: + expected = (expected_email or "").strip().lower() + try: + from playwright.sync_api import sync_playwright + except Exception as exc: + _log(log_fn, f"[sora] 浏览器 session fallback 不可用: {exc}") + return {} + + for cdp_url in _candidate_browser_cdp_urls(cdp_urls): + browser = None + try: + with sync_playwright() as playwright: + browser = playwright.chromium.connect_over_cdp(cdp_url) + contexts = list(getattr(browser, "contexts", []) or []) + if not contexts: + _log(log_fn, f"[sora] CDP {cdp_url} 无可用浏览器上下文") + continue + for context in contexts: + pages = list(getattr(context, "pages", []) or []) + page = None + for candidate in pages: + url = str(getattr(candidate, "url", "") or "") + if any(origin in url for origin in (SORA_ORIGIN, SORA_LEGACY_ORIGIN, CHATGPT_ORIGIN)): + page = candidate + break + if page is None: + page = pages[0] if pages else context.new_page() + for origin in _candidate_sora_web_origins(preferred_origin): + target_url = f"{origin}/explore" + try: + page.goto(target_url, wait_until="domcontentloaded", timeout=120000) + except Exception as exc: + _log(log_fn, f"[sora] CDP 导航失败 {target_url}: {exc}") + continue + try: + session_payload = page.evaluate( + """ + async () => { + const resp = await fetch('/api/auth/session', { credentials: 'include' }); + const text = await resp.text(); + let data = null; + try { + data = JSON.parse(text); + } catch (err) {} + return { + status: resp.status, + data, + text_preview: text.slice(0, 400), + }; + } + """ + ) + except Exception as exc: + _log(log_fn, f"[sora] CDP 读取 /api/auth/session 失败 origin={origin}: {exc}") + continue + if not isinstance(session_payload, dict): + continue + session_data = session_payload.get("data") or {} + if int(session_payload.get("status") or 0) != 200 or not isinstance(session_data, dict): + continue + access_token = (session_data.get("accessToken") or "").strip() + session_email = ( + ((session_data.get("user") or {}).get("email") or "") + or ((session_data.get("profile") or {}).get("email") or "") + ).strip().lower() + if expected and session_email and session_email != expected: + _log(log_fn, f"[sora] CDP 登录邮箱不匹配 browser={session_email} expected={expected}") + continue + if not access_token or not is_chatgpt_web_access_token(access_token): + continue + try: + cookies = context.cookies([SORA_ORIGIN, SORA_LEGACY_ORIGIN, CHATGPT_ORIGIN, AUTH_ORIGIN]) + except Exception as exc: + _log(log_fn, f"[sora] CDP 读取 cookies 失败 origin={origin}: {exc}") + continue + web_session = _make_web_session() + copied = _copy_browser_cookie_dicts(cookies, web_session) + if copied <= 0: + try: + web_session.close() + except Exception: + pass + continue + session_state = _read_sora_web_session(web_session, preferred_origin=origin, log_fn=log_fn) + effective_token = (session_state.get("access_token") or access_token).strip() + if not effective_token: + try: + web_session.close() + except Exception: + pass + continue + nf2_probe = sora_probe_nf2_session( + effective_token, + web_session=web_session, + preferred_origin=(session_state.get("base_origin") or origin), + log_fn=log_fn, + ) + if not nf2_probe.get("ok"): + try: + web_session.close() + except Exception: + pass + continue + return { + "access_token": effective_token, + "session": session_state.get("session") or session_data, + "web_session": web_session, + "base_origin": (nf2_probe.get("base_origin") or session_state.get("base_origin") or origin).strip(), + "email": session_email, + "cookie_count": copied, + "cdp_url": cdp_url, + "source": "browser_cdp", + } + except Exception as exc: + _log(log_fn, f"[sora] CDP 连接失败 {cdp_url}: {exc}") + finally: + if browser is not None: + try: + browser.close() + except Exception: + pass + return {} + + +def _complete_chatgpt_provider_flow( + web_session, + authorize_url: str, + *, + referer: str = "", + login_email: str = "", + login_password: str = "", + get_otp_fn=None, + proxy_url: str = None, + log_fn=None, + log_prefix: str = "chatgpt", + session_reader=None, +) -> dict: + pr = _load_register_helpers() + if not pr or web_session is None: + return {} + parsed = urlparse(authorize_url or "") + params = parse_qs(parsed.query, keep_blank_values=False) + redirect_uri = ((params.get("redirect_uri") or [""])[0] or "").strip() + state = ((params.get("state") or [""])[0] or "").strip() + device_id = ((params.get("device_id") or [""])[0] or "").strip() + if not redirect_uri or not state or not device_id: + _log(log_fn, f"[sora] {log_prefix} authorize 缺少 redirect_uri/state/device_id: {(authorize_url or '')[:180]}") + return {} + + auth_start = web_session.get( + authorize_url, + headers=_build_html_headers(referer=referer or CHATGPT_ORIGIN), + timeout=DEFAULT_TIMEOUT, + allow_redirects=True, + ) + _log(log_fn, f"[sora] {log_prefix} provider authorize -> {str(auth_start.url)[:160]}") + + callback_url = "" + auth_code = "" + for candidate in _collect_response_urls(auth_start): + if "api/auth/callback/openai" in candidate: + callback_url = candidate + break + auth_code = pr._parse_code_from_url(candidate) + if auth_code: + break + + continue_url = "" + page_type = "" + consent_url = str(getattr(auth_start, "url", "") or "").strip() or authorize_url + + if not callback_url and not auth_code: + if not login_email: + _log(log_fn, f"[sora] {log_prefix} 缺少登录邮箱,无法继续 provider flow") + return {} + api_headers = { + "accept": "application/json", + "accept-language": "en-US,en;q=0.9", + "content-type": "application/json", + "origin": AUTH_ORIGIN, + "user-agent": pr.KEYGEN_USER_AGENT, + "sec-ch-ua": '"Google Chrome";v="145", "Not?A_Brand";v="8", "Chromium";v="145"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-origin", + "referer": f"{AUTH_ORIGIN}/log-in", + "oai-device-id": device_id, + } + api_headers.update(pr._make_trace_headers()) + sentinel_auth = _build_sentinel_header(device_id, flow="authorize_continue", proxy_url=proxy_url, log_fn=log_fn) + if sentinel_auth: + api_headers["openai-sentinel-token"] = sentinel_auth + continue_resp = web_session.post( + f"{AUTH_ORIGIN}/api/accounts/authorize/continue", + headers=api_headers, + json={"username": {"kind": "email", "value": login_email}}, + timeout=DEFAULT_TIMEOUT, + ) + if continue_resp.status_code != 200: + _log(log_fn, f"[sora] {log_prefix} authorize/continue HTTP {continue_resp.status_code} {_response_preview(continue_resp, 140)}") + return {} + try: + continue_data = continue_resp.json() + except Exception: + continue_data = {} + continue_url = (continue_data.get("continue_url") or "").strip() + page_type = ((continue_data.get("page") or {}).get("type") or "").strip() + + needs_password = ( + not continue_url + or page_type in ("password", "password_verification") + or "/log-in/password" in continue_url + ) + if needs_password: + if not login_password: + _log(log_fn, f"[sora] {log_prefix} 已到 password 阶段但缺少账号密码") + return {} + api_headers["referer"] = f"{AUTH_ORIGIN}/log-in/password" + api_headers.update(pr._make_trace_headers()) + sentinel_pw = _build_sentinel_header(device_id, flow="password_verify", proxy_url=proxy_url, log_fn=log_fn) + if sentinel_pw: + api_headers["openai-sentinel-token"] = sentinel_pw + password_resp = web_session.post( + f"{AUTH_ORIGIN}/api/accounts/password/verify", + headers=api_headers, + json={"password": login_password}, + timeout=DEFAULT_TIMEOUT, + allow_redirects=False, + ) + if password_resp.status_code != 200: + _log(log_fn, f"[sora] {log_prefix} password/verify HTTP {password_resp.status_code} {_response_preview(password_resp, 140)}") + return {} + try: + password_data = password_resp.json() + except Exception: + password_data = {} + continue_url = (password_data.get("continue_url") or continue_url).strip() + page_type = ((password_data.get("page") or {}).get("type") or page_type).strip() + + if page_type == "email_otp_verification" or "email-verification" in continue_url: + otp_code = "" + if callable(get_otp_fn): + try: + otp_code = pr._normalize_otp_code(get_otp_fn() or "") + except Exception: + otp_code = "" + if not otp_code: + pr._request_login_email_otp(web_session, device_id, lambda msg: _log(log_fn, msg)) + if callable(get_otp_fn): + try: + otp_code = pr._normalize_otp_code(get_otp_fn() or "") + except Exception: + otp_code = "" + if not otp_code: + _log(log_fn, f"[sora] {log_prefix} 登录需要邮箱验证码,但未拿到新 OTP") + return {} + api_headers = { + "accept": "application/json", + "accept-language": "en-US,en;q=0.9", + "content-type": "application/json", + "origin": AUTH_ORIGIN, + "referer": f"{AUTH_ORIGIN}/email-verification", + "user-agent": pr.KEYGEN_USER_AGENT, + "sec-ch-ua": '"Google Chrome";v="145", "Not?A_Brand";v="8", "Chromium";v="145"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-origin", + "oai-device-id": device_id, + } + otp_resp = None + for attempt in range(2): + api_headers.update(pr._make_trace_headers()) + otp_resp = web_session.post( + f"{AUTH_ORIGIN}/api/accounts/email-otp/validate", + headers=api_headers, + json={"code": otp_code}, + timeout=DEFAULT_TIMEOUT, + ) + if otp_resp.status_code == 200: + break + _log(log_fn, f"[sora] {log_prefix} email-otp/validate HTTP {otp_resp.status_code} {_response_preview(otp_resp, 140)}") + if attempt == 0 and otp_resp.status_code in (400, 401): + pr._request_login_email_otp(web_session, device_id, lambda msg: _log(log_fn, msg)) + if callable(get_otp_fn): + try: + otp_code = pr._normalize_otp_code(get_otp_fn() or "") + except Exception: + otp_code = "" + if not otp_code: + break + continue + return {} + if not otp_resp or otp_resp.status_code != 200: + return {} + try: + otp_data = otp_resp.json() + except Exception: + otp_data = {} + continue_url = (otp_data.get("continue_url") or continue_url).strip() + page_type = ((otp_data.get("page") or {}).get("type") or page_type).strip() + + if continue_url and ("/about-you" in continue_url or page_type in ("about_you", "about-you")): + status_create, data_create = pr._create_account(web_session, "User", "1992-09-19") + if status_create not in (200, 201, 204): + _log(log_fn, f"[sora] {log_prefix} about-you 提交失败: {status_create}") + return {} + continue_url = ( + (data_create.get("continue_url") or "").strip() + or (data_create.get("url") or "").strip() + or (data_create.get("redirect_url") or "").strip() + or continue_url + ) + + if not callback_url and auth_code: + callback_url = f"{redirect_uri}?{urlencode({'code': auth_code, 'state': state})}" + if not callback_url: + if continue_url and "api/auth/callback/openai" in continue_url: + callback_url = continue_url + else: + consent_url = continue_url if continue_url.startswith("http") else (f"{AUTH_ORIGIN}{continue_url}" if continue_url else consent_url) + auth_code = pr._follow_consent_to_code(web_session, consent_url, lambda msg: _log(log_fn, msg)) + if not auth_code: + session_data = pr._decode_oai_session_cookie(web_session) + workspaces = (session_data or {}).get("workspaces") or [] + workspace_id = workspaces[0].get("id") if workspaces else None + if workspace_id: + api_headers = { + "accept": "application/json", + "accept-language": "en-US,en;q=0.9", + "content-type": "application/json", + "origin": AUTH_ORIGIN, + "user-agent": pr.KEYGEN_USER_AGENT, + "sec-ch-ua": '"Google Chrome";v="145", "Not?A_Brand";v="8", "Chromium";v="145"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-origin", + "referer": consent_url, + "oai-device-id": device_id, + } + api_headers.update(pr._make_trace_headers()) + ws_resp = web_session.post( + f"{AUTH_ORIGIN}/api/accounts/workspace/select", + headers=api_headers, + json={"workspace_id": workspace_id}, + timeout=DEFAULT_TIMEOUT, + allow_redirects=False, + ) + if ws_resp.status_code in (301, 302, 303, 307, 308): + loc = (ws_resp.headers.get("Location") or ws_resp.headers.get("location") or "").strip() + auth_code = pr._parse_code_from_url(loc) + if not auth_code and loc: + auth_code = pr._follow_consent_to_code( + web_session, + loc if loc.startswith("http") else f"{AUTH_ORIGIN}{loc}", + lambda msg: _log(log_fn, msg), + ) + if not auth_code: + _log(log_fn, f"[sora] {log_prefix} provider 跟随 consent 后仍未拿到 code") + return {} + callback_url = f"{redirect_uri}?{urlencode({'code': auth_code, 'state': state})}" + + callback_resp = web_session.get( + callback_url, + headers=_build_html_headers(referer=CHATGPT_ORIGIN), + timeout=DEFAULT_TIMEOUT, + allow_redirects=True, + ) + _log(log_fn, f"[sora] {log_prefix} callback -> {str(callback_resp.url)[:160]}") + reader = session_reader or _read_sora_web_session + if not callable(reader): + return {} + return reader(web_session, log_fn=log_fn) + + +def sora_chatgpt_web_login_from_authenticated_session( + web_session, + email: str = "", + password: str = "", + get_otp_fn=None, + log_fn=None, +) -> dict: + """ + 复用已在 auth.openai.com 完成登录的 session,建立 ChatGPT/Sora Web session。 + 优先沿着 provider flow 继续,避免 fresh 注册后再触发一轮完整邮箱 OTP。 + """ + if web_session is None: + return {} + + login_url = f"{CHATGPT_ORIGIN}/auth/login?next=/sora/login?next=%2Fauth%2Flogin_with" + proxy_url = None + try: + proxies = getattr(web_session, "proxies", None) or {} + proxy_url = (proxies.get("https") or proxies.get("http") or "").strip() or None + except Exception: + proxy_url = None + browser_session = _make_web_session(proxy_url=proxy_url) + _copy_session_cookies(web_session, browser_session) + try: + page_resp = browser_session.get( + login_url, + headers=_build_html_headers(), + timeout=DEFAULT_TIMEOUT, + allow_redirects=True, + ) + csrf_resp = browser_session.get( + f"{CHATGPT_ORIGIN}/api/auth/csrf", + headers={**_build_web_headers(), "Referer": str(page_resp.url)}, + timeout=DEFAULT_TIMEOUT, + ) + csrf_token = "" + if csrf_resp.status_code == 200: + try: + csrf_token = (csrf_resp.json().get("csrfToken") or "").strip() + except Exception: + csrf_token = "" + if not csrf_token: + _log(log_fn, f"[sora] 复用 session 获取 chatgpt csrf 失败 HTTP {csrf_resp.status_code} {_response_preview(csrf_resp, 120)}") + return {} + + signin_resp = browser_session.post( + f"{CHATGPT_ORIGIN}/api/auth/signin/openai", + headers={ + **_build_web_headers(), + "Content-Type": "application/x-www-form-urlencoded", + "Referer": str(page_resp.url), + }, + data={ + "csrfToken": csrf_token, + "callbackUrl": f"{CHATGPT_ORIGIN}/", + }, + timeout=DEFAULT_TIMEOUT, + allow_redirects=False, + ) + authorize_url = (signin_resp.headers.get("Location") or signin_resp.headers.get("location") or "").strip() + if signin_resp.status_code not in (301, 302, 303, 307, 308) or not authorize_url: + _log(log_fn, f"[sora] 复用 session chatgpt signin/openai HTTP {signin_resp.status_code} {_response_preview(signin_resp, 120)}") + return {} + + return _complete_chatgpt_provider_flow( + browser_session, + authorize_url, + referer=str(page_resp.url), + login_email=(email or "").strip(), + login_password=(password or "").strip(), + get_otp_fn=get_otp_fn, + proxy_url=proxy_url, + log_fn=log_fn, + log_prefix="复用 session", + ) + except Exception as exc: + _log(log_fn, f"[sora] 复用登录 session 建立 Web session 异常: {exc}") + return {} + finally: + try: + browser_session.close() + except Exception: + pass + + +def sora_chatgpt_web_login( + email: str, + password: str, + get_otp_fn=None, + proxy_url: str = None, + log_fn=None, + return_web_session: bool = False, +) -> dict: + """ + 通过 ChatGPT next-auth + auth.openai.com 登录,建立可供 Sora 使用的 Web session。 + 返回 {"access_token": str, "session": dict};失败返回空 dict。 + """ + login_email = (email or "").strip() + login_password = (password or "").strip() + if not login_email or not login_password: + return {} + pr = _load_register_helpers() + if not pr: + _log(log_fn, "[sora] 无法导入 protocol_register,跳过 ChatGPT Web 登录补链") + return {} + if callable(get_otp_fn): + seed_current_otps = getattr(get_otp_fn, "seed_current_otps", None) + if callable(seed_current_otps): + try: + seeded = seed_current_otps(folders=["junkemail", "inbox"]) + except Exception: + seeded = set() + if seeded: + _log(log_fn, f"[sora] chatgpt 预排除旧 OTP: {','.join(sorted(seeded))}") + + login_url = f"{CHATGPT_ORIGIN}/auth/login?next=/sora/login?next=%2Fauth%2Flogin_with" + web_session = _make_web_session(proxy_url=proxy_url) + try: + page_resp = None + csrf_token = "" + for attempt in range(2): + page_resp = web_session.get( + login_url, + headers=_build_html_headers(), + timeout=DEFAULT_TIMEOUT, + ) + csrf_resp = web_session.get( + f"{CHATGPT_ORIGIN}/api/auth/csrf", + headers={**_build_web_headers(), "Referer": str(page_resp.url)}, + timeout=DEFAULT_TIMEOUT, + ) + if csrf_resp.status_code == 200: + try: + csrf_token = (csrf_resp.json().get("csrfToken") or "").strip() + except Exception: + csrf_token = "" + if csrf_token: + break + _log(log_fn, f"[sora] chatgpt /api/auth/csrf 200 但无 csrfToken {_response_preview(csrf_resp, 120)}") + else: + _log(log_fn, f"[sora] chatgpt /api/auth/csrf HTTP {csrf_resp.status_code} {_response_preview(csrf_resp, 120)}") + if attempt == 0: + time.sleep(2) + if not csrf_token: + return {} + + signin_resp = web_session.post( + f"{CHATGPT_ORIGIN}/api/auth/signin/openai", + headers={ + **_build_web_headers(), + "Content-Type": "application/x-www-form-urlencoded", + "Referer": str(page_resp.url), + }, + data={ + "csrfToken": csrf_token, + "callbackUrl": f"{CHATGPT_ORIGIN}/", + }, + timeout=DEFAULT_TIMEOUT, + allow_redirects=False, + ) + authorize_url = (signin_resp.headers.get("Location") or signin_resp.headers.get("location") or "").strip() + if signin_resp.status_code not in (301, 302, 303, 307, 308) or not authorize_url: + _log(log_fn, f"[sora] chatgpt signin/openai HTTP {signin_resp.status_code} {_response_preview(signin_resp, 120)}") + return {} + + web_auth = _complete_chatgpt_provider_flow( + web_session, + authorize_url, + referer=str(page_resp.url), + login_email=login_email, + login_password=login_password, + get_otp_fn=get_otp_fn, + proxy_url=proxy_url, + log_fn=log_fn, + log_prefix="chatgpt", + ) + if ( + return_web_session + and isinstance(web_auth, dict) + and (web_auth.get("access_token") or "").strip() + ): + web_auth = dict(web_auth) + web_auth["web_session"] = web_session + web_session = None + return web_auth + except Exception as exc: + _log(log_fn, f"[sora] ChatGPT Web 登录异常: {exc}") + return {} + finally: + if web_session is not None: + try: + web_session.close() + except Exception: + pass + + +def chatgpt_mfa_info(access_token: str, proxy_url: str = None, log_fn=None, web_session=None) -> dict: + own_session = web_session is None + session = web_session or _make_web_session(proxy_url=proxy_url) + referer = _warm_chatgpt_security_page(session, log_fn=log_fn) + try: + resp = session.get( + f"{CHATGPT_BACKEND_API_ORIGIN}/accounts/mfa_info", + headers=_build_chatgpt_backend_headers(access_token, referer=referer), + timeout=DEFAULT_TIMEOUT, + ) + if resp.status_code != 200: + code, message, preview = _extract_api_error(resp) + _log(log_fn, f"[phone_bind] mfa_info HTTP {resp.status_code} code={code or '-'} msg={message or preview or '-'}") + return {} + data = resp.json() if hasattr(resp, "json") and callable(resp.json) else {} + if isinstance(data, dict): + _log( + log_fn, + f"[phone_bind] mfa_info mfa_enabled_v2={data.get('mfa_enabled_v2')} show_sms={data.get('show_sms')} show_passkey={data.get('show_passkey')}", + ) + return data if isinstance(data, dict) else {} + except Exception as exc: + _log(log_fn, f"[phone_bind] mfa_info 异常: {exc}") + return {} + finally: + if own_session: + try: + session.close() + except Exception: + pass + + +def _chatgpt_mfa_enroll_once(web_session, access_token: str, phone_number: str, *, channel: str = "sms", log_fn=None) -> tuple: + referer = _warm_chatgpt_security_page(web_session, log_fn=log_fn) + try: + resp = web_session.post( + f"{CHATGPT_BACKEND_API_ORIGIN}/accounts/mfa/enroll", + headers=_build_chatgpt_backend_headers(access_token, referer=referer), + json={ + "factor_type": "sms", + "phone_number": phone_number, + "phone_verification_channel": (channel or "sms").strip() or "sms", + }, + timeout=DEFAULT_TIMEOUT, + ) + except Exception as exc: + _log(log_fn, f"[phone_bind] mfa/enroll 异常: {exc}") + return False, {}, "other" + + if resp.status_code == 200: + try: + data = resp.json() + except Exception: + data = {} + if isinstance(data, dict) and isinstance(data.get("session_id"), str) and data.get("session_id"): + return True, data, "" + _log(log_fn, f"[phone_bind] mfa/enroll 返回异常结构: {_response_preview(resp, 180)}") + return False, data if isinstance(data, dict) else {}, "other" + + code, message, preview = _extract_api_error(resp) + text = " ".join(x for x in (code, message, preview) if x).lower() + _log(log_fn, f"[phone_bind] mfa/enroll HTTP {resp.status_code} code={code or '-'} msg={message or preview or '-'}") + if "recent_auth_required" in text or "re-authenticate" in text or "reauth" in text: + return False, {}, "recent_auth_required" + if "invalid_request" in text: + return False, {}, "invalid_request" + if "already" in text and ("phone" in text or "factor" in text): + return False, {}, "phone_used" + return False, {}, "other" + + +def _chatgpt_mfa_activate_enrollment(web_session, access_token: str, session_id: str, code: str, log_fn=None) -> bool: + referer = _warm_chatgpt_security_page(web_session, log_fn=log_fn) + try: + resp = web_session.post( + f"{CHATGPT_BACKEND_API_ORIGIN}/accounts/mfa/user/activate_enrollment", + headers=_build_chatgpt_backend_headers(access_token, referer=referer), + json={ + "code": code, + "factor_type": "sms", + "session_id": session_id, + }, + timeout=DEFAULT_TIMEOUT, + ) + if resp.status_code == 200: + return True + code_text, message, preview = _extract_api_error(resp) + _log(log_fn, f"[phone_bind] activate_enrollment HTTP {resp.status_code} code={code_text or '-'} msg={message or preview or '-'}") + return False + except Exception as exc: + _log(log_fn, f"[phone_bind] activate_enrollment 异常: {exc}") + return False + + +def chatgpt_open_recent_auth_session_for_mfa( + email: str, + password: str, + get_otp_fn=None, + proxy_url: str = None, + log_fn=None, +) -> dict: + login_email = (email or "").strip() + login_password = (password or "").strip() + if not login_email or not login_password: + return {} + if callable(get_otp_fn): + seed_current_otps = getattr(get_otp_fn, "seed_current_otps", None) + if callable(seed_current_otps): + try: + seeded = seed_current_otps(folders=["junkemail", "inbox"]) + except Exception: + seeded = set() + if seeded: + _log(log_fn, f"[phone_bind] reauth 预排除旧 OTP: {','.join(sorted(seeded))}") + + web_session = _make_web_session(proxy_url=proxy_url) + callback_url = f"{CHATGPT_SECURITY_SETTINGS_URL}?action=enable&factor=sms" + web_auth = {} + try: + page_resp = web_session.get( + callback_url, + headers=_build_html_headers(referer=CHATGPT_ORIGIN), + timeout=DEFAULT_TIMEOUT, + allow_redirects=True, + ) + csrf_resp = web_session.get( + f"{CHATGPT_ORIGIN}/api/auth/csrf", + headers={**_build_web_headers(), "Referer": str(page_resp.url)}, + timeout=DEFAULT_TIMEOUT, + ) + csrf_token = "" + if csrf_resp.status_code == 200: + try: + csrf_token = (csrf_resp.json().get("csrfToken") or "").strip() + except Exception: + csrf_token = "" + if not csrf_token: + _log(log_fn, f"[phone_bind] reauth 获取 csrf 失败 HTTP {csrf_resp.status_code} {_response_preview(csrf_resp, 120)}") + return {} + + device_id = str(uuid.uuid4()) + signin_resp = web_session.post( + f"{CHATGPT_ORIGIN}/api/auth/signin/openai?{urlencode({'reauth': 'password', 'max_age': '0', 'login_hint': login_email, 'ext-oai-did': device_id})}", + headers={ + **_build_web_headers(), + "Content-Type": "application/x-www-form-urlencoded", + "Referer": str(page_resp.url), + }, + data={ + "csrfToken": csrf_token, + "callbackUrl": callback_url, + }, + timeout=DEFAULT_TIMEOUT, + allow_redirects=False, + ) + authorize_url = (signin_resp.headers.get("Location") or signin_resp.headers.get("location") or "").strip() + if signin_resp.status_code not in (301, 302, 303, 307, 308) or not authorize_url: + _log(log_fn, f"[phone_bind] reauth signin/openai HTTP {signin_resp.status_code} {_response_preview(signin_resp, 120)}") + return {} + + web_auth = _complete_chatgpt_provider_flow( + web_session, + authorize_url, + referer=str(page_resp.url), + login_email=login_email, + login_password=login_password, + get_otp_fn=get_otp_fn, + proxy_url=proxy_url, + log_fn=log_fn, + log_prefix="reauth", + session_reader=_read_chatgpt_web_session, + ) + access_token = (web_auth.get("access_token") or "").strip() if isinstance(web_auth, dict) else "" + if not access_token: + return {} + mfa_info = chatgpt_mfa_info(access_token, proxy_url=proxy_url, log_fn=log_fn, web_session=web_session) + return { + "access_token": access_token, + "session": web_auth.get("session") or {}, + "mfa_info": mfa_info or {}, + "web_session": web_session, + } + except Exception as exc: + _log(log_fn, f"[phone_bind] reauth 建立 recent-auth session 异常: {exc}") + return {} + finally: + if not isinstance(web_auth, dict) or not (web_auth.get("access_token") or "").strip(): + try: + web_session.close() + except Exception: + pass + + +def sora_probe_web_auth(access_token: str = "", proxy_url: str = None, log_fn=None) -> dict: + """ + 探测当前 Sora Web 会话入口,帮助定位「Bearer token 不可用」与「Web session 未建立」。 + 返回示例: + { + "session_state": "null" | "present" | "error", + "provider_client_id": "...", + "provider_redirect_uri": "...", + "provider_audience": "...", + "token_client_id": "...", + } + """ + out = { + "session_state": "", + "provider_client_id": "", + "provider_redirect_uri": "", + "provider_audience": "", + "provider_scope": "", + "token_client_id": "", + } + + token_payload = _decode_jwt_payload(access_token) + token_client_id = (token_payload.get("client_id") or "").strip() + if token_client_id: + out["token_client_id"] = token_client_id + + try: + web_session = _make_web_session(proxy_url=proxy_url) + session_resp = web_session.get( + f"{SORA_ORIGIN}/api/auth/session", + headers=_build_web_headers(), + timeout=DEFAULT_TIMEOUT, + ) + if session_resp.status_code == 200: + body = (session_resp.text or "").strip() + if body == "null": + out["session_state"] = "null" + elif body: + out["session_state"] = "present" + else: + out["session_state"] = "empty" + else: + out["session_state"] = f"http_{session_resp.status_code}" + _log(log_fn, f"[sora] web /api/auth/session HTTP {session_resp.status_code} {_response_preview(session_resp, 120)}") + except Exception as exc: + out["session_state"] = "error" + _log(log_fn, f"[sora] web /api/auth/session 异常: {exc}") + + try: + web_session = locals().get("web_session") or _make_web_session(proxy_url=proxy_url) + csrf_resp = web_session.get( + f"{SORA_ORIGIN}/api/auth/csrf", + headers=_build_web_headers(), + timeout=DEFAULT_TIMEOUT, + ) + csrf_token = "" + if csrf_resp.status_code == 200: + try: + csrf_token = (csrf_resp.json().get("csrfToken") or "").strip() + except Exception: + csrf_token = "" + if not csrf_token: + _log(log_fn, f"[sora] web /api/auth/csrf 200 但无 csrfToken {_response_preview(csrf_resp, 120)}") + else: + _log(log_fn, f"[sora] web /api/auth/csrf HTTP {csrf_resp.status_code} {_response_preview(csrf_resp, 120)}") + + if csrf_token: + signin_resp = web_session.post( + f"{SORA_ORIGIN}/api/auth/signin/openai", + headers={ + **_build_web_headers(), + "Content-Type": "application/x-www-form-urlencoded", + }, + data={ + "csrfToken": csrf_token, + "callbackUrl": f"{SORA_ORIGIN}/", + }, + timeout=DEFAULT_TIMEOUT, + allow_redirects=False, + ) + loc = (signin_resp.headers.get("Location") or signin_resp.headers.get("location") or "").strip() + if signin_resp.status_code in (301, 302, 303, 307, 308) and loc: + parsed = urlparse(loc) + params = parse_qs(parsed.query, keep_blank_values=False) + out["provider_client_id"] = ((params.get("client_id") or [""])[0] or "").strip() + out["provider_redirect_uri"] = ((params.get("redirect_uri") or [""])[0] or "").strip() + out["provider_audience"] = ((params.get("audience") or [""])[0] or "").strip() + out["provider_scope"] = ((params.get("scope") or [""])[0] or "").strip() + else: + _log( + log_fn, + f"[sora] web signin/openai HTTP {signin_resp.status_code} {_response_preview(signin_resp, 120)}", + ) + except Exception as exc: + _log(log_fn, f"[sora] web signin/openai 探测异常: {exc}") + finally: + try: + web_session.close() + except Exception: + pass + + provider_client_id = out["provider_client_id"] + if provider_client_id: + _log( + log_fn, + f"[sora] web auth session={out['session_state'] or '-'} provider_client_id={provider_client_id} redirect_uri={out['provider_redirect_uri'] or '-'}", + ) + if token_client_id and provider_client_id and token_client_id != provider_client_id: + _log( + log_fn, + f"[sora] 当前 AT client_id={token_client_id} 与 Sora web provider client_id={provider_client_id} 不一致", + ) + return out + + +def rt_to_at_mobile(refresh_token: str, proxy_url: str = None, log_fn=None) -> dict: + """ + RT 换 AT(移动端 client_id/redirect_uri)。返回 {"access_token": str, "refresh_token": str|None},失败抛异常或返回空。 + """ + rt = (refresh_token or "").strip() + if not rt: + _log(log_fn, "[phone_bind] RT 为空") + return {} + for attempt in range(2): + try: + r = _session_post( + f"{AUTH_ORIGIN}/oauth/token", + headers={"Accept": "application/json", "Content-Type": "application/json"}, + json={ + "client_id": MOBILE_CLIENT_ID, + "grant_type": "refresh_token", + "redirect_uri": MOBILE_REDIRECT_URI, + "refresh_token": rt, + }, + proxy_url=proxy_url, + timeout=30, + ) + if r.status_code == 200: + d = r.json() + at = (d.get("access_token") or "").strip() + if at: + return {"access_token": at, "refresh_token": d.get("refresh_token")} + if log_fn and attempt == 0: + code, message, preview = _extract_error(r) + _log(log_fn, f"[phone_bind] RT 换 AT HTTP {r.status_code} code={code or '-'} msg={message or preview or '-'}") + except Exception as e: + _log(log_fn, f"[phone_bind] RT 换 AT 异常: {e}") + if attempt == 0: + time.sleep(2) + continue + return {} + + +def _legacy_sora_bootstrap(access_token: str, proxy_url: str = None, log_fn=None) -> bool: + """兼容旧版 GET backend/m/bootstrap。""" + for origin in _candidate_origins(): + try: + r = _session_get( + f"{origin}/backend/m/bootstrap", + headers=_build_headers(access_token, origin=origin), + proxy_url=proxy_url, + ) + if r.status_code == 200: + return True + code, message, preview = _extract_error(r) + _log(log_fn, f"[sora] legacy bootstrap {origin} HTTP {r.status_code} code={code or '-'} msg={message or preview or '-'}") + except Exception as e: + _log(log_fn, f"[sora] legacy bootstrap {origin} 异常: {e}") + return False + + +def sora_me(access_token: str, proxy_url: str = None, log_fn=None) -> dict: + """GET backend/me 获取当前用户信息。返回 dict,含 username 等;失败返回 {}.""" + try: + r = _session_get( + f"{SORA_ORIGIN}/backend/me", + headers=_build_headers(access_token), + proxy_url=proxy_url, + ) + if r.status_code == 200: + return r.json() if hasattr(r, "json") and callable(r.json) else {} + code, message, preview = _extract_error(r) + _log(log_fn, f"[sora] me HTTP {r.status_code} code={code or '-'} msg={message or preview or '-'}") + return {} + except Exception as e: + _log(log_fn, f"[sora] me 异常: {e}") + return {} + + +def _normalize_username(username: str) -> str: + value = "".join(c for c in (username or "").strip().lower() if c.isalnum() or c == "_") + if not value: + return "" + if not value[0].isalnum(): + value = "user_" + value + return value[:20] + + +def _normalize_video_orientation(orientation: str) -> str: + value = (orientation or "wide").strip().lower() + aliases = { + "landscape": "wide", + "16:9": "wide", + "wide": "wide", + "portrait": "tall", + "9:16": "tall", + "tall": "tall", + "square": "square", + "1:1": "square", + } + return aliases.get(value, "wide") + + +def _video_dimensions(resolution: int = 360, orientation: str = "wide") -> tuple[int, int]: + base = int(resolution or 360) + if base <= 0: + base = 360 + direction = _normalize_video_orientation(orientation) + if direction == "square": + return base, base + long_edge = int(round(base * 16 / 9)) + if direction == "tall": + return base, long_edge + return long_edge, base + + +def sora_build_simple_video_payload( + prompt: str, + *, + operation: str = "simple_compose", + n_variants: int = 4, + n_frames: int = 300, + resolution: int = 360, + orientation: str = "wide", + model: str = None, + seed: int = None, +) -> dict: + width, height = _video_dimensions(resolution=resolution, orientation=orientation) + payload = { + "type": "video_gen", + "operation": (operation or "simple_compose").strip() or "simple_compose", + "prompt": (prompt or "").strip(), + "n_variants": int(n_variants or 4), + "n_frames": int(n_frames or 300), + "width": width, + "height": height, + "inpaint_items": [], + "is_storyboard": False, + "model": (model or "").strip() or None, + "seed": seed, + } + return _strip_nullish(payload) + + +def is_chatgpt_web_access_token(access_token: str) -> bool: + payload = _decode_jwt_payload(access_token) + return (payload.get("client_id") or "").strip() == CHATGPT_WEB_CLIENT_ID + + +def _normalize_nf2_orientation(orientation: str) -> str: + value = (orientation or "portrait").strip().lower() + aliases = { + "portrait": "portrait", + "tall": "portrait", + "9:16": "portrait", + "wide": "landscape", + "landscape": "landscape", + "16:9": "landscape", + "square": "landscape", + "1:1": "landscape", + } + return aliases.get(value, "portrait") + + +def _nf2_size_from_resolution(resolution: int = 360) -> str: + try: + value = int(resolution or 360) + except Exception: + value = 360 + return "large" if value >= 720 else "small" + + +def sora_build_nf2_video_payload( + prompt: str, + *, + n_variants: int = 1, + n_frames: int = 300, + resolution: int = 360, + orientation: str = "portrait", + model: str = "sy_8", + style_id: str = "", + audio_caption: str = "", + audio_transcript: str = "", + video_caption: str = "", + seed: int = None, +) -> dict: + payload = { + "kind": "video", + "prompt": (prompt or "").strip(), + "title": None, + "orientation": _normalize_nf2_orientation(orientation), + "size": _nf2_size_from_resolution(resolution), + "n_frames": int(n_frames or 300), + "inpaint_items": [], + "remix_target_id": None, + "reroll_target_id": None, + "project_config": None, + "trim_config": None, + "metadata": None, + "cameo_ids": None, + "cameo_replacements": None, + "model": (model or "sy_8").strip() or "sy_8", + "style_id": (style_id or "").strip() or None, + "audio_caption": (audio_caption or "").strip() or None, + "audio_transcript": (audio_transcript or "").strip() or None, + "video_caption": (video_caption or "").strip() or None, + "storyboard_id": None, + "seed": seed, + } + try: + n = int(n_variants or 1) + except Exception: + n = 1 + if n > 1: + payload["nsamples"] = n + return _strip_nullish(payload) + + +def sora_build_image_video_payload( + prompt: str, + upload_media_id: str, + *, + operation: str = "simple_compose", + n_variants: int = 1, + n_frames: int = 300, + resolution: int = 360, + orientation: str = "wide", + model: str = None, + seed: int = None, +) -> dict: + payload = sora_build_simple_video_payload( + prompt, + operation=operation, + n_variants=n_variants, + n_frames=n_frames, + resolution=resolution, + orientation=orientation, + model=model, + seed=seed, + ) + payload["is_storyboard"] = True + payload["inpaint_items"] = [ + { + "type": "image", + "upload_media_id": (upload_media_id or "").strip(), + "frame_index": 0, + "x": 0, + "y": 0, + "width": int(payload.get("width") or 0), + "height": int(payload.get("height") or 0), + } + ] + return _strip_nullish(payload) + + +def sora_video_gen_create( + access_token: str, + prompt: str, + *, + operation: str = "simple_compose", + n_variants: int = 4, + n_frames: int = 300, + resolution: int = 360, + orientation: str = "wide", + model: str = None, + seed: int = None, + proxy_url: str = None, + log_fn=None, +): + device_id = str(uuid.uuid4()) + headers = _build_headers(access_token, device_id=device_id) + sentinel = _build_sentinel_header(device_id, "sora_create_task", proxy_url=proxy_url, log_fn=log_fn) + if sentinel: + headers["openai-sentinel-token"] = sentinel + payload = sora_build_simple_video_payload( + prompt, + operation=operation, + n_variants=n_variants, + n_frames=n_frames, + resolution=resolution, + orientation=orientation, + model=model, + seed=seed, + ) + return _session_post( + f"{SORA_ORIGIN}/backend/video_gen", + headers=headers, + json=payload, + proxy_url=proxy_url, + ) + + +def sora_nf2_create( + access_token: str, + prompt: str, + *, + n_variants: int = 1, + n_frames: int = 300, + resolution: int = 360, + orientation: str = "portrait", + model: str = "sy_8", + style_id: str = "", + audio_caption: str = "", + audio_transcript: str = "", + video_caption: str = "", + seed: int = None, + proxy_url: str = None, + log_fn=None, + web_session=None, + base_origin: str = None, +): + origin = (base_origin or SORA_ORIGIN).rstrip("/") + device_id = str(uuid.uuid4()) + headers = _build_sora_web_headers(access_token, device_id=device_id, origin=origin) + sentinel = _build_sentinel_header(device_id, "sora_2_create_task", proxy_url=proxy_url, log_fn=log_fn) + if sentinel: + headers["openai-sentinel-token"] = sentinel + payload = sora_build_nf2_video_payload( + prompt, + n_variants=n_variants, + n_frames=n_frames, + resolution=resolution, + orientation=orientation, + model=model, + style_id=style_id, + audio_caption=audio_caption, + audio_transcript=audio_transcript, + video_caption=video_caption, + seed=seed, + ) + path = "/backend/nf/bulk_create" if int(payload.get("nsamples") or 1) > 1 else "/backend/nf/create" + return _web_session_json_post( + f"{origin}{path}", + headers=headers, + json=payload, + proxy_url=proxy_url, + web_session=web_session, + ) + + +def sora_nf2_get_task(access_token: str, task_id: str, proxy_url: str = None, web_session=None, base_origin: str = None): + task = (task_id or "").strip() + origin = (base_origin or SORA_ORIGIN).rstrip("/") + return _web_session_get( + f"{origin}/backend/nf/tasks/{task}/v2", + headers=_build_sora_web_headers(access_token, origin=origin), + proxy_url=proxy_url, + web_session=web_session, + ) + + +def sora_nf2_get_pending(access_token: str, proxy_url: str = None, web_session=None, base_origin: str = None): + origin = (base_origin or SORA_ORIGIN).rstrip("/") + return _web_session_get( + f"{origin}/backend/nf/pending/v2", + headers=_build_sora_web_headers(access_token, origin=origin), + proxy_url=proxy_url, + web_session=web_session, + ) + + +def sora_nf2_get_draft(access_token: str, draft_id: str, proxy_url: str = None, web_session=None, base_origin: str = None): + draft = (draft_id or "").strip() + origin = (base_origin or SORA_ORIGIN).rstrip("/") + return _web_session_get( + f"{origin}/backend/project_y/profile/drafts/v2/{draft}", + headers=_build_sora_web_headers(access_token, origin=origin), + proxy_url=proxy_url, + web_session=web_session, + ) + + +def sora_nf2_stitch( + access_token: str, + generation_ids: list[str], + *, + for_download: bool = False, + proxy_url: str = None, + web_session=None, + base_origin: str = None, +): + origin = (base_origin or SORA_ORIGIN).rstrip("/") + query = "?for_download=true" if for_download else "" + payload = {"generation_ids": [str(item).strip() for item in (generation_ids or []) if str(item).strip()]} + return _web_session_json_post( + f"{origin}/backend/editor/stitch{query}", + headers=_build_sora_web_headers(access_token, origin=origin), + json=payload, + proxy_url=proxy_url, + web_session=web_session, + ) + + +def sora_upload_media( + access_token: str, + *, + filename: str, + content_type: str, + file_bytes: bytes = None, + file_path: str = None, + media_type: str = "image", + proxy_url: str = None, +): + headers = _build_headers(access_token, device_id=str(uuid.uuid4())) + headers.pop("Content-Type", None) + return _session_multipart_post( + f"{SORA_ORIGIN}/backend/uploads", + headers=headers, + data={ + "file_name": (filename or "").strip(), + "media_type": (media_type or "image").strip() or "image", + }, + file_field_name="file", + filename=(filename or "upload.bin").strip() or "upload.bin", + file_bytes=file_bytes, + file_path=file_path, + content_type=(content_type or "application/octet-stream").strip() or "application/octet-stream", + proxy_url=proxy_url, + ) + + +def _random_username(prefix: str = "user") -> str: + prefix = _normalize_username(prefix) or "user" + prefix = prefix[:11] + suffix = "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(8)) + return _normalize_username(f"{prefix}_{suffix}") + + +def sora_create_account(access_token: str, birth_date: str = None, proxy_url: str = None, log_fn=None) -> bool: + """按当前官方前端链路创建 Sora onboarding 账号。""" + device_id = str(uuid.uuid4()) + headers = _build_headers(access_token, device_id=device_id) + sentinel = _build_sentinel_header(device_id, "sora_create_account", proxy_url=proxy_url, log_fn=log_fn) + if sentinel: + headers["openai-sentinel-token"] = sentinel + payload = {"birth_date": birth_date or None} + try: + r = _session_post( + f"{SORA_ORIGIN}/backend/me/onboarding/create_account", + headers=headers, + json=payload, + proxy_url=proxy_url, + ) + if r.status_code in (200, 201, 204): + return True + code, message, preview = _extract_error(r) + if code == "account_already_created": + return True + _log(log_fn, f"[sora] create_account HTTP {r.status_code} code={code or '-'} msg={message or preview or '-'}") + return False + except Exception as exc: + _log(log_fn, f"[sora] create_account 异常: {exc}") + return False + + +def _update_me(access_token: str, payload: dict, proxy_url: str = None, log_fn=None) -> tuple[bool, str]: + try: + r = _session_post( + f"{SORA_ORIGIN}/backend/me", + headers=_build_headers(access_token), + json=payload, + proxy_url=proxy_url, + ) + if r.status_code == 200: + return True, "" + code, message, preview = _extract_error(r) + _log(log_fn, f"[sora] update_me HTTP {r.status_code} code={code or '-'} msg={message or preview or '-'}") + return False, code + except Exception as exc: + _log(log_fn, f"[sora] update_me 异常: {exc}") + return False, "" + + +def sora_username_check(access_token: str, username: str, proxy_url: str = None, log_fn=None) -> bool: + """保留旧版用户名检查接口;当前激活流程不再依赖它。""" + for origin in _candidate_origins(): + try: + r = _session_post( + f"{origin}/backend/project_y/profile/username/check", + headers=_build_headers(access_token, origin=origin), + json={"username": username}, + proxy_url=proxy_url, + ) + if r.status_code == 200: + d = r.json() if hasattr(r, "json") and callable(r.json) else {} + return d.get("available", False) + code, message, preview = _extract_error(r) + _log(log_fn, f"[sora] legacy username/check {origin} HTTP {r.status_code} code={code or '-'} msg={message or preview or '-'}") + except Exception as exc: + _log(log_fn, f"[sora] legacy username/check {origin} 异常: {exc}") + return False + + +def _legacy_sora_username_set(access_token: str, username: str, proxy_url: str = None, log_fn=None) -> bool: + for origin in _candidate_origins(): + try: + r = _session_post( + f"{origin}/backend/project_y/profile/username/set", + headers=_build_headers(access_token, origin=origin), + json={"username": username}, + proxy_url=proxy_url, + ) + if r.status_code == 200: + return True + code, message, preview = _extract_error(r) + _log(log_fn, f"[sora] legacy username/set {origin} HTTP {r.status_code} code={code or '-'} msg={message or preview or '-'}") + except Exception as exc: + _log(log_fn, f"[sora] legacy username/set {origin} 异常: {exc}") + return False + + +def sora_username_set(access_token: str, username: str, proxy_url: str = None, log_fn=None) -> bool: + """按当前前端链路 POST /backend/me 设置用户名,失败时回退旧接口。""" + normalized = _normalize_username(username) or _random_username() + ok, code = _update_me(access_token, {"username": normalized}, proxy_url=proxy_url, log_fn=log_fn) + if ok: + return True + if code in USERNAME_RETRY_CODES: + return False + return _legacy_sora_username_set(access_token, normalized, proxy_url=proxy_url, log_fn=log_fn) + + +def sora_bootstrap(access_token: str, proxy_url: str = None, log_fn=None) -> bool: + """ + 兼容旧调用名。 + 现版本优先尝试 onboarding create_account;若当前环境仍是旧接口,再回退 legacy bootstrap。 + """ + if sora_create_account(access_token, proxy_url=proxy_url, log_fn=log_fn): + return True + return _legacy_sora_bootstrap(access_token, proxy_url=proxy_url, log_fn=log_fn) + + +def sora_ensure_activated( + access_token: str, + proxy_url: str = None, + log_fn=None, + username: str = None, + birth_date: str = None, +) -> bool: + """ + 确保 Sora 已激活(有 username)。 + 新链路:GET /backend/me -> POST /backend/me/onboarding/create_account -> POST /backend/me(username) + 若新链路失败,再回退旧版 bootstrap + project_y username/set。 + 返回 True 表示已激活或激活成功。 + """ + me = sora_me(access_token, proxy_url, log_fn) + if me and me.get("username"): + _log(log_fn, f"[sora] 已激活 username={me.get('username')}") + return True + sora_probe_web_auth(access_token=access_token, proxy_url=proxy_url, log_fn=log_fn) + + if sora_create_account(access_token, birth_date=birth_date, proxy_url=proxy_url, log_fn=log_fn): + me = sora_me(access_token, proxy_url, log_fn) + if me and me.get("username"): + _log(log_fn, f"[sora] create_account 后已激活 username={me.get('username')}") + return True + + preferred = _normalize_username(username) + candidates = [] + if preferred: + candidates.append(preferred) + for _ in range(5): + candidates.append(_random_username(prefix=preferred or "user")) + + for uname in candidates: + ok, code = _update_me(access_token, {"username": uname}, proxy_url=proxy_url, log_fn=log_fn) + if ok: + _log(log_fn, f"[sora] 设置用户名成功: {uname}") + return True + if code and code not in USERNAME_RETRY_CODES: + break + + _log(log_fn, "[sora] 新版 onboarding/me 流程失败,回退 legacy project_y 接口") + _legacy_sora_bootstrap(access_token, proxy_url, log_fn) + for uname in candidates: + if sora_username_check(access_token, uname, proxy_url, log_fn): + if _legacy_sora_username_set(access_token, uname, proxy_url, log_fn): + _log(log_fn, f"[sora] legacy 设置用户名成功: {uname}") + return True + return False + + +def _legacy_sora_phone_enroll_start(access_token: str, phone_number: str, proxy_url: str = None, log_fn=None) -> tuple: + try: + r = _session_post( + f"{SORA_ORIGIN}/backend/project_y/phone_number/enroll/start", + headers=_build_headers(access_token), + json={"phone_number": phone_number, "verification_expiry_window_ms": None}, + proxy_url=proxy_url, + ) + if r.status_code == 200: + return True, None + text = (r.text or "").lower() + if "already verified" in text or "phone number already" in text: + return False, "phone_used" + _log(log_fn, f"[phone_bind] enroll/start HTTP {r.status_code} {_response_preview(r, 150)}") + return False, "other" + except Exception as e: + _log(log_fn, f"[phone_bind] enroll/start 异常: {e}") + return False, "other" + + +def _legacy_sora_phone_enroll_finish(access_token: str, phone_number: str, verification_code: str, proxy_url: str = None, log_fn=None) -> bool: + code = re.sub(r"\D", "", (verification_code or "").strip())[:6] + if not code: + return False + try: + r = _session_post( + f"{SORA_ORIGIN}/backend/project_y/phone_number/enroll/finish", + headers=_build_headers(access_token), + json={"phone_number": phone_number, "verification_code": code}, + proxy_url=proxy_url, + ) + ok = r.status_code == 200 + if not ok: + _log(log_fn, f"[phone_bind] enroll/finish HTTP {r.status_code} {_response_preview(r, 150)}") + return ok + except Exception as e: + _log(log_fn, f"[phone_bind] enroll/finish 异常: {e}") + return False + + +def sora_phone_enroll_start( + access_token: str, + phone_number: str, + proxy_url: str = None, + log_fn=None, + login_email: str = "", + login_password: str = "", + get_otp_fn=None, +) -> tuple: + """ + 优先走 ChatGPT MFA 手机绑定: + - GET /backend-api/accounts/mfa_info + - POST /backend-api/accounts/mfa/enroll + recent_auth_required 时走 reauth=password + max_age=0。 + + 返回: + - (True, None, context) + - (False, "phone_used"|"reauth_failed"|"sms_unavailable"|"other", None) + """ + normalized_phone = _normalize_phone_number(phone_number) + if not normalized_phone: + _log(log_fn, f"[phone_bind] 非法手机号: {phone_number}") + return False, "other", None + + current_at = (access_token or "").strip() + current_age = _chatgpt_pwd_auth_age_seconds(current_at) + if current_age is not None: + _log(log_fn, f"[phone_bind] 当前 access_token pwd_auth_age={current_age}s") + + def _close_session(obj) -> None: + if obj is None: + return + try: + obj.close() + except Exception: + pass + + def _try_enroll_with_session(web_session, token: str): + mfa_info = chatgpt_mfa_info(token, proxy_url=proxy_url, log_fn=log_fn, web_session=web_session) + if not mfa_info: + return False, "other", None + if mfa_info.get("show_sms") is False: + _log(log_fn, "[phone_bind] 当前账号未开放 SMS MFA 入口") + return False, "sms_unavailable", None + ok, data, err = _chatgpt_mfa_enroll_once( + web_session, + token, + normalized_phone, + channel="sms", + log_fn=log_fn, + ) + if not ok: + return False, err or "other", None + factor = data.get("factor") if isinstance(data, dict) else {} + return True, None, { + "web_session": web_session, + "access_token": token, + "session_id": (data.get("session_id") or "").strip(), + "factor_id": (factor.get("id") or "").strip() if isinstance(factor, dict) else "", + "factor_type": "sms", + "phone_number": normalized_phone, + } + + should_try_direct = bool(current_at) and not _chatgpt_needs_recent_auth(current_at) + if should_try_direct: + direct_session = _make_web_session(proxy_url=proxy_url) + try: + ok, err, context = _try_enroll_with_session(direct_session, current_at) + if ok: + return True, None, context + _close_session(direct_session) + if err == "phone_used": + return False, "phone_used", None + if err == "sms_unavailable": + return False, "sms_unavailable", None + except Exception as exc: + _log(log_fn, f"[phone_bind] 直连 MFA enroll 异常: {exc}") + _close_session(direct_session) + + if (login_email or "").strip() and (login_password or "").strip(): + reauth = chatgpt_open_recent_auth_session_for_mfa( + email=login_email, + password=login_password, + get_otp_fn=get_otp_fn, + proxy_url=proxy_url, + log_fn=log_fn, + ) + reauth_session = reauth.get("web_session") if isinstance(reauth, dict) else None + reauth_at = (reauth.get("access_token") or "").strip() if isinstance(reauth, dict) else "" + if not reauth_session or not reauth_at: + _log(log_fn, "[phone_bind] recent-auth session 建立失败") + return False, "reauth_failed", None + ok, err, context = _try_enroll_with_session(reauth_session, reauth_at) + if ok: + return True, None, context + _close_session(reauth_session) + if err == "phone_used": + return False, "phone_used", None + if err == "sms_unavailable": + return False, "sms_unavailable", None + return False, err or "other", None + + _log(log_fn, "[phone_bind] 无账号密码,回退 legacy project_y 手机绑定") + ok, err = _legacy_sora_phone_enroll_start(access_token, normalized_phone, proxy_url=proxy_url, log_fn=log_fn) + return ok, err, None + + +def sora_phone_enroll_finish( + access_token: str, + phone_number: str, + verification_code: str, + proxy_url: str = None, + log_fn=None, + context: dict = None, +) -> bool: + """优先提交 ChatGPT MFA 验证码;无上下文时回退 legacy project_y。""" + code = re.sub(r"\D", "", (verification_code or "").strip())[:6] + if not code: + return False + ctx = context if isinstance(context, dict) else {} + web_session = ctx.get("web_session") + session_id = (ctx.get("session_id") or "").strip() + effective_at = (ctx.get("access_token") or access_token or "").strip() + if web_session is not None and session_id and effective_at: + try: + return _chatgpt_mfa_activate_enrollment( + web_session, + effective_at, + session_id, + code, + log_fn=log_fn, + ) + finally: + try: + web_session.close() + except Exception: + pass + return _legacy_sora_phone_enroll_finish( + access_token, + _normalize_phone_number(phone_number) or phone_number, + code, + proxy_url=proxy_url, + log_fn=log_fn, + ) diff --git a/Register_GPT_v0/run.py b/Register_GPT_v0/run.py new file mode 100644 index 0000000..626f33b --- /dev/null +++ b/Register_GPT_v0/run.py @@ -0,0 +1,15 @@ +""" +协议版入口:在本目录(protocol/)内直接启动时运行此脚本。 +将上级目录加入 sys.path,使 config、email_service 等可被导入,然后执行批量注册。 +""" +import sys +from pathlib import Path + +_root = Path(__file__).resolve().parent.parent +if str(_root) not in sys.path: + sys.path.insert(0, str(_root)) + +from protocol.main_protocol import main + +if __name__ == "__main__": + main() diff --git a/Register_GPT_v0/screenshots/1.png b/Register_GPT_v0/screenshots/1.png new file mode 100644 index 0000000000000000000000000000000000000000..08efd83b5e4126657bb99c4bb3a2949dd42c4b43 GIT binary patch literal 103459 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ℜtPN@*Mw$aA%S6ZXs<%9dZuk0?sN}-+83% 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

X%Yz#QutXhj;Pa!m9G-_%$m&w{oWXgVa}! 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 Q#Q*>R07*qoM6N<$g8ovvRsaA1 literal 0 HcmV?d00001 diff --git a/Register_GPT_v0/scripts/__init__.py b/Register_GPT_v0/scripts/__init__.py new file mode 100644 index 0000000..89d8c00 --- /dev/null +++ b/Register_GPT_v0/scripts/__init__.py @@ -0,0 +1 @@ +# 辅助脚本包 diff --git a/Register_GPT_v0/scripts/get_outlook_refresh_token.py b/Register_GPT_v0/scripts/get_outlook_refresh_token.py new file mode 100644 index 0000000..78d7f81 --- /dev/null +++ b/Register_GPT_v0/scripts/get_outlook_refresh_token.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +""" +用浏览器登录一次,拿到 OAuth2 的 refresh_token,填到 mail.txt 第 4 列。 +用法:项目根目录 python -m protocol.scripts.get_outlook_refresh_token [client_id] +""" +import re +import sys +from pathlib import Path + +_root = Path(__file__).resolve().parent.parent.parent +if str(_root) not in sys.path: + sys.path.insert(0, str(_root)) + +LIVE_AUTHORIZE = "https://login.live.com/oauth20_authorize.srf" +LIVE_TOKEN = "https://login.live.com/oauth20_token.srf" +REDIRECT_URI = "https://login.live.com/oauth20_desktop.srf" +SCOPE = "wl.imap wl.offline_access" + + +def main(): + client_id = None + if len(sys.argv) >= 2 and sys.argv[1].strip(): + client_id = sys.argv[1].strip() + if not client_id: + try: + from config import cfg + client_id = (getattr(cfg.email, "outlook_client_id", None) or "").strip() + except Exception: + pass + if not client_id: + print("[x] Need client_id. Usage: python -m protocol.scripts.get_outlook_refresh_token ") + return + + auth_url = ( + f"{LIVE_AUTHORIZE}?" + f"client_id={client_id}&" + f"scope={SCOPE.replace(' ', '%20')}&" + f"response_type=code&" + f"redirect_uri={REDIRECT_URI}" + ) + print("[*] 1. Open this URL in browser and sign in with your Outlook account:") + print(auth_url) + print() + print("[*] 2. After consent, copy the FULL URL from the address bar (it contains code=...) and paste below.") + try: + raw = input("Paste redirect URL (or line with code=): ").strip() + except EOFError: + print("[x] No input.") + return + if not raw: + print("[x] Empty input.") + return + + if "code=" not in raw: + print("[x] Pasted text does not contain 'code='.") + return + m = re.search(r"code=([^&\s]+)", raw) + code = (m.group(1).strip() if m else "").strip() + if not code: + print("[x] Could not find 'code=' in the pasted text.") + return + + from utils import http_session + from config import HTTP_TIMEOUT + + data = { + "client_id": client_id, + "grant_type": "authorization_code", + "code": code, + "redirect_uri": REDIRECT_URI, + } + try: + r = http_session.post(LIVE_TOKEN, data=data, timeout=HTTP_TIMEOUT) + body = r.json() if r.text else {} + if r.status_code != 200: + err = body.get("error", "") or body.get("error_description", "") or r.text[:300] + print(f"[x] Token exchange failed: HTTP {r.status_code} - {err}") + return + refresh = body.get("refresh_token") + if not refresh: + print(f"[x] No refresh_token in response. Keys: {list(body.keys())}") + return + print() + print("[ok] refresh_token (copy to mail.txt 4th column):") + print(refresh) + except Exception as e: + print(f"[x] Error: {e}") + + +if __name__ == "__main__": + main() diff --git a/Register_GPT_v0/scripts/sora_video_create_and_wait.py b/Register_GPT_v0/scripts/sora_video_create_and_wait.py new file mode 100644 index 0000000..34679a4 --- /dev/null +++ b/Register_GPT_v0/scripts/sora_video_create_and_wait.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +import argparse +import json +import sys +from typing import Any +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen + + +def _post_json(url: str, body: dict[str, Any], api_key: str, timeout: int = 1800) -> tuple[int, dict[str, Any]]: + payload = json.dumps(body).encode("utf-8") + request = Request( + url, + data=payload, + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + }, + method="POST", + ) + try: + with urlopen(request, timeout=timeout) as response: + raw = response.read().decode("utf-8", errors="replace") + return response.status, json.loads(raw or "{}") + except HTTPError as exc: + raw = exc.read().decode("utf-8", errors="replace") + try: + payload = json.loads(raw or "{}") + except Exception: + payload = {"raw": raw} + return exc.code, payload + except URLError as exc: + raise RuntimeError(f"请求失败: {exc}") from exc + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Create a Sora video task and wait until succeeded or timeout.") + parser.add_argument("--base-url", default="http://127.0.0.1:1989", help="Local backend base URL") + parser.add_argument("--api-key", default="", help="Pool or account-bound srk_ API key") + parser.add_argument("--account-id", type=int, default=0, help="Optional fixed account_id for admin-mode calls") + parser.add_argument("--prompt", required=True, help="Video prompt") + parser.add_argument("--n-variants", type=int, default=1, help="Number of variants") + parser.add_argument("--n-frames", type=int, default=300, help="Frame count") + parser.add_argument("--resolution", type=int, default=360, help="Base resolution") + parser.add_argument("--orientation", choices=["wide", "tall", "square"], default="wide", help="Video orientation") + parser.add_argument("--task-family", choices=["video_gen", "nf2"], default="", help="Optional generation chain override") + parser.add_argument("--model", default="", help="Optional model override") + parser.add_argument("--seed", type=int, default=None, help="Optional random seed") + parser.add_argument("--poll-interval", type=float, default=5.0, help="Polling interval in seconds") + parser.add_argument("--timeout", type=int, default=900, help="Polling timeout in seconds") + parser.add_argument("--json", action="store_true", help="Print full JSON response") + return parser + + +def main() -> int: + parser = build_parser() + args = parser.parse_args() + api_key = (args.api_key or "").strip() + if not api_key: + parser.error("--api-key 不能为空") + + body = { + "prompt": args.prompt, + "n_variants": max(1, int(args.n_variants or 1)), + "n_frames": max(60, int(args.n_frames or 300)), + "resolution": max(360, int(args.resolution or 360)), + "orientation": args.orientation, + "poll_interval_seconds": max(1.0, float(args.poll_interval or 5.0)), + "timeout_seconds": max(5, int(args.timeout or 900)), + } + if (args.task_family or "").strip(): + body["task_family"] = args.task_family.strip() + if args.account_id > 0: + body["account_id"] = int(args.account_id) + if (args.model or "").strip(): + body["model"] = args.model.strip() + if args.seed is not None: + body["seed"] = int(args.seed) + + url = args.base_url.rstrip("/") + "/api/sora-api/video-gen/create-and-wait" + status_code, result = _post_json(url, body, api_key=api_key, timeout=max(30, int(args.timeout) + 60)) + + if args.json: + print(json.dumps({"http_status": status_code, "result": result}, ensure_ascii=False, indent=2)) + else: + print(f"http_status: {status_code}") + print(f"task_id: {result.get('task_id') or ''}") + print(f"status: {result.get('normalized_status') or result.get('status') or ''}") + print(f"used_account_id: {result.get('used_account_id') or ''}") + print(f"used_email: {result.get('used_email') or ''}") + print(f"poll_attempts: {result.get('poll_attempts') or 0}") + print(f"elapsed_seconds: {result.get('elapsed_seconds') or 0}") + print(f"timed_out: {bool(result.get('timed_out'))}") + print(f"message: {result.get('message') or ''}") + video_urls = result.get("video_urls") or [] + if video_urls: + print("video_urls:") + for item in video_urls: + print(f" - {item}") + + if status_code >= 400: + return 1 + if result.get("ok") and result.get("is_success"): + return 0 + if result.get("timed_out"): + return 2 + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Register_GPT_v0/tools/capture_sora_mobile.py b/Register_GPT_v0/tools/capture_sora_mobile.py new file mode 100644 index 0000000..fb0371d --- /dev/null +++ b/Register_GPT_v0/tools/capture_sora_mobile.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +import json +from datetime import datetime +from pathlib import Path + +from mitmproxy import ctx, http + + +DOMAINS = ( + "sora.chatgpt.com", + "chatgpt.com", + "auth.openai.com", + "openai.com", + "sentinel.openai.com", + "videos.openai.com", +) + +OUT_DIR = Path("/Users/mac/Desktop/Sora-Register-main/logs/mobile_capture") +OUT_DIR.mkdir(parents=True, exist_ok=True) + + +def _matches(host: str) -> bool: + value = (host or "").strip().lower() + return any(value == domain or value.endswith(f".{domain}") for domain in DOMAINS) + + +def _now() -> str: + return datetime.now().strftime("%Y%m%d_%H%M%S_%f") + + +def _save(kind: str, data: dict) -> None: + path = OUT_DIR / f"{_now()}_{kind}.json" + path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") + ctx.log.info(f"[capture] wrote {path}") + + +def request(flow: http.HTTPFlow) -> None: + if not _matches(flow.request.pretty_host): + return + payload = { + "kind": "request", + "host": flow.request.pretty_host, + "method": flow.request.method, + "url": flow.request.pretty_url, + "headers": dict(flow.request.headers), + "content_length": len(flow.request.raw_content or b""), + "text_preview": (flow.request.get_text(strict=False) or "")[:8000], + } + _save("request", payload) + + +def response(flow: http.HTTPFlow) -> None: + if not _matches(flow.request.pretty_host): + return + payload = { + "kind": "response", + "host": flow.request.pretty_host, + "method": flow.request.method, + "url": flow.request.pretty_url, + "status_code": flow.response.status_code if flow.response else 0, + "headers": dict(flow.response.headers) if flow.response else {}, + "content_length": len((flow.response.raw_content if flow.response else b"") or b""), + "text_preview": ((flow.response.get_text(strict=False) if flow.response else "") or "")[:8000], + } + _save("response", payload) diff --git a/Register_GPT_v0/tools/monitor_sora_create.py b/Register_GPT_v0/tools/monitor_sora_create.py new file mode 100644 index 0000000..cad3280 --- /dev/null +++ b/Register_GPT_v0/tools/monitor_sora_create.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +import argparse +import json +import sys +import time +from typing import Iterable + +from playwright.sync_api import sync_playwright + + +WATCH_PATHS = ( + "/backend/video_gen", + "/backend/nf/create", + "/backend/nf/bulk_create", +) + + +def _matches(url: str, paths: Iterable[str]) -> bool: + value = (url or "").strip() + return any(path in value for path in paths) + + +def _print_json(prefix: str, payload) -> None: + try: + text = json.dumps(payload, ensure_ascii=False) + except Exception: + text = repr(payload) + print(f"{prefix} {text}", flush=True) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Monitor Sora create requests in a real Chrome via CDP.") + parser.add_argument("--cdp-url", default="http://127.0.0.1:9222") + parser.add_argument("--timeout", type=int, default=300) + parser.add_argument("--open-explore", action="store_true") + args = parser.parse_args() + + deadline = time.time() + max(1, int(args.timeout)) + hit = {"done": False} + + with sync_playwright() as p: + browser = p.chromium.connect_over_cdp(args.cdp_url) + contexts = browser.contexts + if not contexts: + print("No browser contexts available.", file=sys.stderr, flush=True) + return 2 + context = contexts[0] + + def on_request(request): + if not _matches(request.url, WATCH_PATHS): + return + payload = { + "method": request.method, + "url": request.url, + "headers": { + k: v + for k, v in request.headers.items() + if k.lower() in {"content-type", "origin", "referer", "user-agent", "x-requested-with"} + }, + } + try: + post_data = request.post_data + except Exception: + post_data = None + if post_data: + payload["post_data"] = post_data[:4000] + _print_json("REQUEST", payload) + + def on_response(response): + if not _matches(response.url, WATCH_PATHS): + return + payload = { + "status": response.status, + "url": response.url, + "headers": { + k: v + for k, v in response.headers.items() + if k.lower() in {"content-type", "cf-ray", "server", "location"} + }, + } + try: + text = response.text() + except Exception as exc: + text = f"" + payload["body_preview"] = (text or "")[:4000] + _print_json("RESPONSE", payload) + hit["done"] = True + + for page in context.pages: + page.on("request", on_request) + page.on("response", on_response) + context.on("page", lambda page: (page.on("request", on_request), page.on("response", on_response))) + + pages = context.pages + page = pages[0] if pages else context.new_page() + if args.open_explore: + page.goto("https://sora.chatgpt.com/explore", wait_until="domcontentloaded", timeout=120000) + print( + json.dumps( + { + "page_count": len(context.pages), + "active_url": page.url, + "active_title": page.title(), + "deadline_epoch": int(deadline), + }, + ensure_ascii=False, + ), + flush=True, + ) + + while time.time() < deadline and not hit["done"]: + page.wait_for_timeout(1000) + + browser.close() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Register_GPT_v0/web/Dockerfile b/Register_GPT_v0/web/Dockerfile new file mode 100644 index 0000000..58dad42 --- /dev/null +++ b/Register_GPT_v0/web/Dockerfile @@ -0,0 +1,12 @@ +# 构建上下文建议为 protocol 目录: docker build -f web/Dockerfile . +FROM python:3.11-slim + +WORKDIR /app +COPY web/ /app/web/ +RUN pip install --no-cache-dir -r /app/web/backend/requirements.txt + +ENV DATA_DIR=/data +VOLUME /data +WORKDIR /app/web/backend +EXPOSE 1989 +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "1989"] diff --git a/Register_GPT_v0/web/backend/app/__init__.py b/Register_GPT_v0/web/backend/app/__init__.py new file mode 100644 index 0000000..a8d71df --- /dev/null +++ b/Register_GPT_v0/web/backend/app/__init__.py @@ -0,0 +1 @@ +# Protocol Admin backend diff --git a/Register_GPT_v0/web/backend/app/config.py b/Register_GPT_v0/web/backend/app/config.py new file mode 100644 index 0000000..4c1c874 --- /dev/null +++ b/Register_GPT_v0/web/backend/app/config.py @@ -0,0 +1,18 @@ +# 界面版配置:从环境变量或默认值加载,登录账号密码在此设置 +import os +from pathlib import Path + +def _str(key: str, default: str) -> str: + return os.environ.get(key, default).strip() or default + +# 默认 data 目录在 protocol/data(即 web 的上级的 data) +_default_data_dir = str(Path(__file__).resolve().parent.parent.parent.parent / "data") + +class Settings: + admin_username: str = _str("ADMIN_USERNAME", "admin") + admin_password: str = _str("ADMIN_PASSWORD", "admin123") + secret_key: str = _str("SECRET_KEY", "change-me-in-production") + data_dir: str = _str("DATA_DIR", _default_data_dir) + cors_origins: str = _str("CORS_ORIGINS", "*") + +settings = Settings() diff --git a/Register_GPT_v0/web/backend/app/database.py b/Register_GPT_v0/web/backend/app/database.py new file mode 100644 index 0000000..f103d54 --- /dev/null +++ b/Register_GPT_v0/web/backend/app/database.py @@ -0,0 +1,322 @@ +import os +import sqlite3 +from pathlib import Path +from contextlib import contextmanager +from app.config import settings + +DB_PATH = os.path.join(settings.data_dir, "admin.db") + + +def ensure_data_dir(): + Path(settings.data_dir).mkdir(parents=True, exist_ok=True) + + +def get_conn(): + ensure_data_dir() + return sqlite3.connect(DB_PATH, check_same_thread=False) + + +@contextmanager +def get_db(): + conn = get_conn() + conn.row_factory = sqlite3.Row + try: + yield conn + conn.commit() + except Exception: + conn.rollback() + raise + finally: + conn.close() + + +def init_db(): + ensure_data_dir() + with get_db() as conn: + c = conn.cursor() + # 系统设置(key-value) + c.execute(""" + CREATE TABLE IF NOT EXISTS system_settings ( + key TEXT PRIMARY KEY, + value TEXT + ) + """) + # 账号表(注册结果) + c.execute(""" + CREATE TABLE IF NOT EXISTS accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL, + password TEXT, + status TEXT, + registered_at TEXT, + has_sora INTEGER DEFAULT 0, + has_plus INTEGER DEFAULT 0, + phone_bound INTEGER DEFAULT 0, + proxy TEXT, + refresh_token TEXT, + access_token TEXT, + created_at TEXT DEFAULT (datetime('now')) + ) + """) + c.execute("CREATE INDEX IF NOT EXISTS idx_accounts_email ON accounts(email)") + c.execute("CREATE INDEX IF NOT EXISTS idx_accounts_status ON accounts(status)") + try: + c.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_email_unique ON accounts(LOWER(TRIM(email)))") + except Exception: + pass + # 邮箱管理 + c.execute(""" + CREATE TABLE IF NOT EXISTS emails ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL, + password TEXT, + uuid TEXT, + token TEXT, + remark TEXT, + created_at TEXT DEFAULT (datetime('now')) + ) + """) + # 手机号管理(activation_id 为 Hero-SMS 激活 ID,用于拉取验证码状态) + c.execute(""" + CREATE TABLE IF NOT EXISTS phone_numbers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + phone TEXT NOT NULL, + activation_id INTEGER, + max_use_count INTEGER DEFAULT 1, + used_count INTEGER DEFAULT 0, + remark TEXT, + created_at TEXT DEFAULT (datetime('now')) + ) + """) + c.execute("CREATE INDEX IF NOT EXISTS idx_phone_numbers_phone ON phone_numbers(phone)") + # 银行卡管理 + c.execute(""" + CREATE TABLE IF NOT EXISTS bank_cards ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + card_number_masked TEXT, + card_data TEXT, + max_use_count INTEGER DEFAULT 1, + used_count INTEGER DEFAULT 0, + remark TEXT, + created_at TEXT DEFAULT (datetime('now')) + ) + """) + # 运行日志(按任务或按条) + c.execute(""" + CREATE TABLE IF NOT EXISTS run_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + task_id TEXT, + level TEXT, + message TEXT, + created_at TEXT DEFAULT (datetime('now')) + ) + """) + c.execute("CREATE INDEX IF NOT EXISTS idx_run_logs_task_id ON run_logs(task_id)") + c.execute("CREATE INDEX IF NOT EXISTS idx_run_logs_created ON run_logs(created_at)") + # 管理员密码(可覆盖 config 的初始密码,存 bcrypt hash) + c.execute(""" + CREATE TABLE IF NOT EXISTS admin_users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT, + updated_at TEXT DEFAULT (datetime('now')) + ) + """) + # Sora 调用 API Key(仅保存 hash,明文只在创建时返回一次) + c.execute(""" + CREATE TABLE IF NOT EXISTS sora_api_keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL, + name TEXT, + key_hash TEXT NOT NULL UNIQUE, + key_prefix TEXT, + key_mask TEXT, + created_by TEXT, + scope TEXT DEFAULT 'text_to_video', + is_active INTEGER DEFAULT 1, + last_used_at TEXT, + created_at TEXT DEFAULT (datetime('now')) + ) + """) + c.execute("CREATE INDEX IF NOT EXISTS idx_sora_api_keys_account_id ON sora_api_keys(account_id)") + c.execute("CREATE INDEX IF NOT EXISTS idx_sora_api_keys_active ON sora_api_keys(is_active)") + # Sora 视频任务归属(池模式下让 task_id 跟随创建它的账号) + c.execute(""" + CREATE TABLE IF NOT EXISTS sora_video_tasks ( + task_id TEXT PRIMARY KEY, + account_id INTEGER NOT NULL, + api_key_id INTEGER, + task_family TEXT DEFAULT 'video_gen', + raw_status TEXT, + normalized_status TEXT, + is_active INTEGER DEFAULT 1, + lease_expires_at TEXT, + succeeded_at TEXT, + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT DEFAULT (datetime('now')) + ) + """) + c.execute("CREATE INDEX IF NOT EXISTS idx_sora_video_tasks_account_id ON sora_video_tasks(account_id)") + c.execute("CREATE INDEX IF NOT EXISTS idx_sora_video_tasks_api_key_id ON sora_video_tasks(api_key_id)") + c.execute(""" + CREATE TABLE IF NOT EXISTS sora_media_assets ( + media_id TEXT PRIMARY KEY, + account_id INTEGER NOT NULL, + api_key_id INTEGER, + media_type TEXT, + filename TEXT, + mime_type TEXT, + width INTEGER, + height INTEGER, + source_url TEXT, + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT DEFAULT (datetime('now')) + ) + """) + c.execute("CREATE INDEX IF NOT EXISTS idx_sora_media_assets_account_id ON sora_media_assets(account_id)") + c.execute("CREATE INDEX IF NOT EXISTS idx_sora_media_assets_api_key_id ON sora_media_assets(api_key_id)") + try: + c.execute("ALTER TABLE phone_numbers ADD COLUMN activation_id INTEGER") + except Exception: + pass + try: + c.execute("ALTER TABLE phone_numbers ADD COLUMN expired_at TEXT") + except Exception: + pass + try: + c.execute("ALTER TABLE accounts ADD COLUMN access_token TEXT") + except Exception: + pass + try: + c.execute("ALTER TABLE accounts ADD COLUMN sora_enabled INTEGER DEFAULT 1") + except Exception: + pass + try: + c.execute("ALTER TABLE accounts ADD COLUMN sora_quota_exhausted INTEGER DEFAULT 0") + except Exception: + pass + try: + c.execute("ALTER TABLE accounts ADD COLUMN sora_quota_note TEXT") + except Exception: + pass + try: + c.execute("ALTER TABLE accounts ADD COLUMN sora_quota_updated_at TEXT") + except Exception: + pass + try: + c.execute("ALTER TABLE accounts ADD COLUMN sora_last_error TEXT") + except Exception: + pass + try: + c.execute("ALTER TABLE sora_api_keys ADD COLUMN scope TEXT DEFAULT 'text_to_video'") + except Exception: + pass + try: + c.execute("ALTER TABLE sora_video_tasks ADD COLUMN task_family TEXT DEFAULT 'video_gen'") + except Exception: + pass + try: + c.execute("ALTER TABLE sora_video_tasks ADD COLUMN raw_status TEXT") + except Exception: + pass + try: + c.execute("ALTER TABLE sora_video_tasks ADD COLUMN normalized_status TEXT") + except Exception: + pass + try: + c.execute("ALTER TABLE sora_video_tasks ADD COLUMN is_active INTEGER DEFAULT 1") + except Exception: + pass + try: + c.execute("ALTER TABLE sora_video_tasks ADD COLUMN lease_expires_at TEXT") + except Exception: + pass + try: + c.execute("ALTER TABLE sora_video_tasks ADD COLUMN succeeded_at TEXT") + except Exception: + pass + try: + c.execute("UPDATE accounts SET sora_enabled = 1 WHERE sora_enabled IS NULL") + c.execute("UPDATE accounts SET sora_quota_exhausted = 0 WHERE sora_quota_exhausted IS NULL") + c.execute("UPDATE sora_api_keys SET scope = 'text_to_video' WHERE COALESCE(scope, '') = ''") + except Exception: + pass + try: + c.execute("UPDATE sora_video_tasks SET task_family = 'video_gen' WHERE COALESCE(task_family, '') = ''") + c.execute("UPDATE sora_video_tasks SET is_active = 1 WHERE is_active IS NULL") + c.execute( + "UPDATE sora_video_tasks SET succeeded_at = updated_at " + "WHERE normalized_status = 'succeeded' AND COALESCE(succeeded_at, '') = ''" + ) + except Exception: + pass + try: + c.execute("CREATE INDEX IF NOT EXISTS idx_sora_api_keys_scope ON sora_api_keys(scope)") + except Exception: + pass + try: + c.execute("CREATE INDEX IF NOT EXISTS idx_sora_video_tasks_active ON sora_video_tasks(is_active)") + except Exception: + pass + try: + c.execute("CREATE INDEX IF NOT EXISTS idx_sora_video_tasks_lease_expires_at ON sora_video_tasks(lease_expires_at)") + except Exception: + pass + try: + c.execute("CREATE INDEX IF NOT EXISTS idx_sora_video_tasks_succeeded_at ON sora_video_tasks(succeeded_at)") + except Exception: + pass + # 插入默认设置键 + defaults = [ + "sms_api_url", "sms_api_key", "thread_count", "proxy_url", "proxy_api_url", + "bank_card_api_url", "bank_card_api_key", "email_api_url", "email_api_key", + "card_use_limit", "phone_bind_limit", + "oauth_client_id", "oauth_redirect_uri", + "sora_daily_video_quota_per_account", + "sora_auto_rotate_cursor", + ] + for key in defaults: + c.execute( + "INSERT OR IGNORE INTO system_settings (key, value) VALUES (?, ?)", + (key, "") + ) + c.execute( + "INSERT OR IGNORE INTO system_settings (key, value) VALUES (?, ?)", + ("thread_count", "1") + ) + c.execute( + "INSERT OR IGNORE INTO system_settings (key, value) VALUES (?, ?)", + ("card_use_limit", "1") + ) + c.execute( + "INSERT OR IGNORE INTO system_settings (key, value) VALUES (?, ?)", + ("phone_bind_limit", "1") + ) + # 首次运行:插入默认管理员 admin / admin123 + c.execute("SELECT COUNT(*) FROM admin_users") + if c.fetchone()[0] == 0: + from app.security import get_password_hash + _hash = get_password_hash("admin123") + c.execute( + "INSERT INTO admin_users (username, password_hash) VALUES (?, ?)", + ("admin", _hash) + ) + # 账号表为空时插入测试数据(便于查看列表效果) + c.execute("SELECT COUNT(*) FROM accounts") + if c.fetchone()[0] == 0: + from datetime import datetime, timedelta + now = datetime.utcnow() + test_rows = [ + ("user1@temp-mail.test", "Pass123!a", "Registered+Sora", (now - timedelta(days=2)).strftime("%Y-%m-%d %H:%M"), 1, 0, 0, "http://127.0.0.1:7890", "rt_xxx1"), + ("user2@temp-mail.test", "Pass123!b", "Registered", (now - timedelta(days=1)).strftime("%Y-%m-%d %H:%M"), 0, 0, 0, None, "rt_xxx2"), + ("user3@temp-mail.test", "Pass123!c", "Plus activated", (now - timedelta(hours=12)).strftime("%Y-%m-%d %H:%M"), 1, 1, 0, "http://127.0.0.1:7891", "rt_xxx3"), + ("user4@temp-mail.test", "Pass123!d", "Registered+Sora", (now - timedelta(hours=5)).strftime("%Y-%m-%d %H:%M"), 1, 0, 1, None, None), + ("user5@temp-mail.test", "Pass123!e", "Finish setup (check email)", (now - timedelta(hours=1)).strftime("%Y-%m-%d %H:%M"), 0, 0, 0, None, None), + ("alice.demo@example.com", "Demo#456", "Registered+Sora", now.strftime("%Y-%m-%d %H:%M"), 1, 1, 1, "socks5://proxy:1080", "rt_xxx6"), + ] + for email, pwd, status, reg_at, sora, plus, phone, proxy, rt in test_rows: + c.execute( + """INSERT INTO accounts (email, password, status, registered_at, has_sora, has_plus, phone_bound, proxy, refresh_token, access_token) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", + (email, pwd, status, reg_at, sora, plus, phone, proxy or None, rt or None, None) + ) diff --git a/Register_GPT_v0/web/backend/app/main.py b/Register_GPT_v0/web/backend/app/main.py new file mode 100644 index 0000000..65cbbfd --- /dev/null +++ b/Register_GPT_v0/web/backend/app/main.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +import logging +from pathlib import Path +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse +from fastapi.middleware.cors import CORSMiddleware + +from fastapi import Depends +from app.config import settings +from app.database import init_db, get_db, DB_PATH +from app.routers import auth, accounts, settings as settings_router, emails, bank_cards, logs, dashboard, email_api, sms_api, phones, register as register_router, phone_bind as phone_bind_router, sora_api, sora_keys +from app.routers.auth import get_current_user + +# 不把轮询接口打进 access 日志,方便调试协议 +class SkipPollPathsFilter(logging.Filter): + _paths = ("/api/register/status", "/api/dashboard", "/api/logs", "/api/phone-bind/status") + def filter(self, record): + try: + # uvicorn AccessFormatter: record.args = (client_addr, method, full_path, http_version, status_code) + if getattr(record, "args", None) and len(record.args) >= 5: + full_path = record.args[2] + status_code = record.args[4] + if status_code == 200 and isinstance(full_path, str): + for p in self._paths: + if p in full_path: + return False + except Exception: + pass + return True + +app = FastAPI(title="Sora 批量注册", version="1.0.0") +app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origins.split(",") if settings.cors_origins else ["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(auth.router) +app.include_router(accounts.router) +app.include_router(settings_router.router) +app.include_router(emails.router) +app.include_router(bank_cards.router) +app.include_router(logs.router) +app.include_router(dashboard.router) +app.include_router(email_api.router) +app.include_router(sms_api.router) +app.include_router(phones.router) +app.include_router(register_router.router) +app.include_router(phone_bind_router.router) +app.include_router(sora_api.router) +app.include_router(sora_keys.router) + + +@app.on_event("startup") +def startup(): + init_db() + # 屏蔽 status/dashboard/logs 轮询的 200 访问日志 + skip_filter = SkipPollPathsFilter() + uvicorn_access = logging.getLogger("uvicorn.access") + uvicorn_access.addFilter(skip_filter) + for h in uvicorn_access.handlers: + h.addFilter(skip_filter) + print("[Sora 批量注册] 服务已启动 http://0.0.0.0:1989", flush=True) + + +# 前端:protocol/web/frontend +frontend_dir = Path(__file__).resolve().parent.parent.parent / "frontend" +static_dir = frontend_dir / "static" + + +@app.get("/api/debug/db-info", tags=["debug"]) +def debug_db_info(username: str = Depends(get_current_user)): + """返回当前后端使用的数据目录与 accounts 条数,用于核对「账号管理」是否与注册写入同库。""" + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute("SELECT COUNT(*) FROM accounts") + n = c.fetchone()[0] + return {"data_dir": settings.data_dir, "db_path": DB_PATH, "accounts_count": n} + + +@app.get("/") +def index(): + index_file = frontend_dir / "index.html" + if index_file.exists(): + return FileResponse(index_file) + return {"message": "Protocol Admin API", "docs": "/docs"} + + +if static_dir.exists(): + app.mount("/static", StaticFiles(directory=str(static_dir)), name="static") diff --git a/Register_GPT_v0/web/backend/app/registration_env.py b/Register_GPT_v0/web/backend/app/registration_env.py new file mode 100644 index 0000000..1ef3c1e --- /dev/null +++ b/Register_GPT_v0/web/backend/app/registration_env.py @@ -0,0 +1,109 @@ +""" +Web 端调用 protocol_register 前的 config/utils 注入。 +在首次 import protocol_register 之前调用 inject_registration_modules(), +并在每任务开始前调用 set_task_config() 设置当前线程的 proxy/timeout 等。 +""" +import sys +import threading +import types +from pathlib import Path + +# 线程局部:当前注册任务的 proxy、timeout 等,由 runner 在每任务开始前写入 +_reg_task = threading.local() + +# 默认超时与 UA(与 protocol_register 内默认一致) +DEFAULT_HTTP_TIMEOUT = 60 +DEFAULT_USER_AGENT = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" +) + + +def set_task_config( + *, + proxy_url=None, + timeout=DEFAULT_HTTP_TIMEOUT, + user_agent=None, + http_max_retries=5, + oauth_client_id=None, + oauth_redirect_uri=None, +): + """由 registration_runner 在每任务开始前调用,设置当前线程的注册配置。""" + _reg_task.proxy_url = proxy_url + _reg_task.timeout = timeout + _reg_task.user_agent = user_agent + _reg_task.http_max_retries = http_max_retries + _reg_task.oauth_client_id = oauth_client_id if oauth_client_id is not None else getattr(_reg_task, "oauth_client_id", "") + _reg_task.oauth_redirect_uri = oauth_redirect_uri if oauth_redirect_uri is not None else getattr(_reg_task, "oauth_redirect_uri", "") + + +def clear_task_config(): + """任务结束后可调用,清理当前线程配置(可选)。""" + for key in ("proxy_url", "timeout", "user_agent", "http_max_retries"): + if hasattr(_reg_task, key): + delattr(_reg_task, key) + + +def get_proxy_url_random(): + """供注入的 config 使用;优先返回当前任务线程的 proxy。""" + return getattr(_reg_task, "proxy_url", None) + + +def get_proxy_url_for_session(): + """供注入的 config 使用。""" + return getattr(_reg_task, "proxy_url", None) + + +def get_http_timeout(): + return getattr(_reg_task, "timeout", DEFAULT_HTTP_TIMEOUT) + + +def get_user_agent(): + return getattr(_reg_task, "user_agent", None) or DEFAULT_USER_AGENT + + +def _make_cfg(): + """最小 cfg:protocol_register 用到的 retry、oauth(按线程动态,oauth 来自系统设置)。""" + + class _Retry: + @property + def http_max_retries(self): + return getattr(_reg_task, "http_max_retries", 5) + + class _OAuth: + @property + def client_id(self): + return getattr(_reg_task, "oauth_client_id", None) or "" + + @property + def redirect_uri(self): + return getattr(_reg_task, "oauth_redirect_uri", None) or "" + + cfg = types.SimpleNamespace() + cfg.retry = _Retry() + cfg.oauth = _OAuth() + return cfg + + +def inject_registration_modules(): + """ + 在首次 import protocol_register 之前调用。 + 向 sys.modules 注入 config 与 utils,并确保协议包根目录在 sys.path 中。 + """ + root = Path(__file__).resolve().parent.parent.parent.parent # app -> backend -> web -> protocol + root_str = str(root) + if root_str not in sys.path: + sys.path.insert(0, root_str) + + # 仅在首次执行注册前注入,保证 protocol_register 的 from config/utils 使用桩 + _config = types.ModuleType("config") + _config.__registration_stub__ = True + _config.HTTP_TIMEOUT = DEFAULT_HTTP_TIMEOUT + _config.get_proxy_url_random = get_proxy_url_random + _config.get_proxy_url_for_session = get_proxy_url_for_session + _config.cfg = _make_cfg() + sys.modules["config"] = _config + + _utils = types.ModuleType("utils") + _utils.__registration_stub__ = True + _utils.get_user_agent = get_user_agent + sys.modules["utils"] = _utils diff --git a/Register_GPT_v0/web/backend/app/registration_state.py b/Register_GPT_v0/web/backend/app/registration_state.py new file mode 100644 index 0000000..7775fe4 --- /dev/null +++ b/Register_GPT_v0/web/backend/app/registration_state.py @@ -0,0 +1,16 @@ +"""注册任务停止标志,供调度线程与 worker 共享。""" +import threading + +_stop_requested = False +_lock = threading.Lock() + + +def set_stop_requested(value: bool) -> None: + with _lock: + global _stop_requested + _stop_requested = value + + +def is_stop_requested() -> bool: + with _lock: + return _stop_requested diff --git a/Register_GPT_v0/web/backend/app/routers/__init__.py b/Register_GPT_v0/web/backend/app/routers/__init__.py new file mode 100644 index 0000000..df5374a --- /dev/null +++ b/Register_GPT_v0/web/backend/app/routers/__init__.py @@ -0,0 +1 @@ +# API routers diff --git a/Register_GPT_v0/web/backend/app/routers/accounts.py b/Register_GPT_v0/web/backend/app/routers/accounts.py new file mode 100644 index 0000000..89fb2fa --- /dev/null +++ b/Register_GPT_v0/web/backend/app/routers/accounts.py @@ -0,0 +1,684 @@ +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Query +from fastapi.responses import StreamingResponse +from pydantic import BaseModel +from app.routers.auth import get_current_user +from app.database import get_db, init_db +import csv +import io +from datetime import datetime + +router = APIRouter(prefix="/api/accounts", tags=["accounts"]) + + +class AccountSoraStateBody(BaseModel): + sora_enabled: Optional[bool] = None + reset_quota: bool = False + + +class AccountSoraQuotaRecheckBody(BaseModel): + account_id: Optional[int] = None + limit: int = 10 + auto_cancel: bool = True + prompt: str = "A calm abstract light gradient slowly drifting." + n_frames: int = 60 + resolution: int = 360 + orientation: str = "wide" + + +def _load_quota_recheck_candidates(account_id: Optional[int] = None, limit: int = 10) -> list[dict]: + init_db() + with get_db() as conn: + c = conn.cursor() + if account_id is not None: + c.execute( + """SELECT id, email, status, + COALESCE(refresh_token, '') AS refresh_token, + COALESCE(access_token, '') AS access_token, + COALESCE(proxy, '') AS proxy, + COALESCE(has_sora, 0) AS has_sora, + COALESCE(sora_enabled, 1) AS sora_enabled, + COALESCE(sora_quota_exhausted, 0) AS sora_quota_exhausted, + COALESCE(sora_quota_note, '') AS sora_quota_note, + COALESCE(sora_quota_updated_at, '') AS sora_quota_updated_at + FROM accounts + WHERE id = ? + LIMIT 1""", + (int(account_id),), + ) + else: + c.execute( + """SELECT id, email, status, + COALESCE(refresh_token, '') AS refresh_token, + COALESCE(access_token, '') AS access_token, + COALESCE(proxy, '') AS proxy, + COALESCE(has_sora, 0) AS has_sora, + COALESCE(sora_enabled, 1) AS sora_enabled, + COALESCE(sora_quota_exhausted, 0) AS sora_quota_exhausted, + COALESCE(sora_quota_note, '') AS sora_quota_note, + COALESCE(sora_quota_updated_at, '') AS sora_quota_updated_at + FROM accounts + WHERE has_sora = 1 + AND COALESCE(sora_enabled, 1) = 1 + AND COALESCE(sora_quota_exhausted, 0) = 1 + AND (COALESCE(refresh_token, '') != '' OR COALESCE(access_token, '') != '') + ORDER BY + CASE WHEN COALESCE(sora_quota_updated_at, '') = '' THEN 1 ELSE 0 END, + sora_quota_updated_at ASC, + id ASC + LIMIT ?""", + (max(1, min(int(limit or 10), 50)),), + ) + rows = c.fetchall() + + items = [] + for row in rows: + items.append({ + "id": int(row[0]), + "email": row[1] or "", + "status": row[2] or "", + "refresh_token": (row[3] or "").strip(), + "access_token": (row[4] or "").strip(), + "proxy": (row[5] or "").strip(), + "has_sora": bool(row[6]), + "sora_enabled": bool(row[7]), + "sora_quota_exhausted": bool(row[8]), + "sora_quota_note": row[9] or "", + "sora_quota_updated_at": row[10] or "", + }) + return items + + +def _probe_account_sora_quota(account: dict, body: AccountSoraQuotaRecheckBody) -> dict: + from app.routers import sora_api as sora_router + + account_id = int(account["id"]) + email = account.get("email") or "" + result = { + "account_id": account_id, + "email": email, + "status": account.get("status") or "", + "quota_note": account.get("sora_quota_note") or "", + "quota_updated_at": account.get("sora_quota_updated_at") or "", + "result": "", + "message": "", + "recovered_to_pool": False, + "task_id": "", + "create_status_code": 0, + "cancel_status_code": 0, + "cancel_ok": False, + } + + if not account.get("has_sora"): + result["result"] = "skipped_no_sora" + result["message"] = "账号未开通 Sora" + return result + if not account.get("sora_enabled"): + result["result"] = "skipped_disabled" + result["message"] = "账号已停用" + return result + if not account.get("sora_quota_exhausted"): + result["result"] = "already_available" + result["message"] = "账号当前未标记额度不足,已经在轮换池内" + result["recovered_to_pool"] = True + return result + if not ((account.get("refresh_token") or "").strip() or (account.get("access_token") or "").strip()): + result["result"] = "skipped_no_token" + result["message"] = "账号没有可用 token,无法复检" + return result + + sora_phone = sora_router._import_sora_phone() + access_token = (account.get("access_token") or "").strip() + refresh_token = (account.get("refresh_token") or "").strip() + proxy_url = (account.get("proxy") or "").strip() + refresh_error = "" + + if refresh_token: + try: + token_out = sora_phone.rt_to_at_mobile(refresh_token, proxy_url=proxy_url) + new_access_token = (token_out.get("access_token") or "").strip() + new_refresh_token = (token_out.get("refresh_token") or "").strip() + if new_access_token: + access_token = new_access_token + if new_refresh_token: + refresh_token = new_refresh_token + if new_access_token or new_refresh_token: + sora_router._save_account_tokens( + account_id, + access_token=new_access_token, + refresh_token=new_refresh_token, + ) + except Exception as exc: + refresh_error = str(exc or "").strip()[:300] + + if not access_token: + detail = refresh_error or "缺少 access_token" + sora_router._mark_account_last_error(account_id, f"quota recheck auth failed: {detail}") + result["result"] = "auth_failed" + result["message"] = f"换取 access_token 失败:{detail}" + return result + + prompt = (body.prompt or "").strip() or "A calm abstract light gradient slowly drifting." + orientation = (body.orientation or "wide").strip().lower() + if orientation not in ("wide", "tall", "square"): + orientation = "wide" + payload = sora_phone.sora_build_simple_video_payload( + prompt, + n_variants=1, + n_frames=max(60, int(body.n_frames or 60)), + resolution=max(360, int(body.resolution or 360)), + orientation=orientation, + model=None, + seed=None, + ) + request_body = sora_router.SoraRequestBody( + access_token=access_token, + proxy_url=proxy_url, + method="POST", + path="/backend/video_gen", + payload=payload, + ) + request_data = { + "account": account, + "access_token": access_token, + "refresh_token": refresh_token, + "proxy_url": proxy_url, + } + + try: + response, response_payload, quota_reason = sora_router._do_sora_request( + request_body, + request_data, + inject_watermark_free=False, + ) + except Exception as exc: + detail = str(exc or "").strip()[:300] + sora_router._mark_account_last_error(account_id, f"quota recheck failed: {detail}") + result["result"] = "probe_failed" + result["message"] = f"探针请求异常:{detail}" + return result + + result["create_status_code"] = int(response.status_code or 0) + decorated = sora_router._decorate_video_task_result( + { + "ok": 200 <= response.status_code < 300, + "status_code": response.status_code, + "data": response_payload, + "used_account_id": account_id, + "used_email": email, + } + ) + task_id = (decorated.get("task_id") or "").strip() + result["task_id"] = task_id + busy_reason = sora_router._extract_busy_reason(response_payload, response.text or "") + + if quota_reason: + sora_router._mark_account_quota_exhausted(account_id, quota_reason) + result["result"] = "still_exhausted" + result["message"] = f"账号仍然额度不足:{quota_reason}" + return result + + if busy_reason or sora_router._is_too_many_concurrent_tasks_result(decorated): + sora_router._clear_account_quota_exhausted(account_id) + result["result"] = "recovered_busy" + result["message"] = "额度已恢复,但账号当前并发繁忙,已重新回池" + result["recovered_to_pool"] = True + return result + + if 200 <= response.status_code < 300 and task_id: + if body.auto_cancel: + cancel_request = sora_router.SoraRequestBody( + access_token=access_token, + proxy_url=proxy_url, + method="POST", + path=f"/backend/video_gen/{task_id}/cancel", + payload={}, + ) + try: + cancel_response, cancel_payload, _ = sora_router._do_sora_request( + cancel_request, + request_data, + inject_watermark_free=False, + ) + result["cancel_status_code"] = int(cancel_response.status_code or 0) + result["cancel_ok"] = 200 <= cancel_response.status_code < 300 + cancel_decorated = sora_router._decorate_video_task_result( + { + "ok": 200 <= cancel_response.status_code < 300, + "status_code": cancel_response.status_code, + "data": cancel_payload, + "used_account_id": account_id, + "used_email": email, + }, + task_id=task_id, + ) + sora_router._sync_video_task_result( + task_id, + account_id, + cancel_decorated, + default_active=not bool(result["cancel_ok"]), + ) + except Exception as exc: + detail = str(exc or "").strip()[:300] + sora_router._remember_video_task( + task_id, + account_id, + raw_status=decorated.get("status") or "", + normalized_status=decorated.get("normalized_status") or "", + is_active=True, + ) + result["message"] = f"探针创建成功,但自动取消失败:{detail}" + else: + sora_router._remember_video_task( + task_id, + account_id, + raw_status=decorated.get("status") or "", + normalized_status=decorated.get("normalized_status") or "", + is_active=True, + ) + + sora_router._clear_account_quota_exhausted(account_id) + result["result"] = "recovered" + base_message = "探针创建成功,额度已恢复并重新回池" + if result["message"]: + base_message += f";{result['message']}" + result["message"] = base_message + result["recovered_to_pool"] = True + if body.auto_cancel and not result["cancel_ok"] and result["cancel_status_code"]: + result["message"] += f";取消返回 HTTP {result['cancel_status_code']}" + return result + + detail = f"探针返回 HTTP {response.status_code}" + if refresh_error: + detail += f",RT->AT 报错:{refresh_error}" + sora_router._mark_account_last_error(account_id, detail) + result["result"] = "probe_failed" + result["message"] = detail + return result + + +@router.get("") +def list_accounts( + username: str = Depends(get_current_user), + status: str = Query(None), + has_sora: bool = Query(None), + has_plus: bool = Query(None), + phone_bound: bool = Query(None), + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=200), +): + init_db() + with get_db() as conn: + c = conn.cursor() + where = [] + params = [] + if status: + where.append("status = ?") + params.append(status) + if has_sora is not None: + where.append("has_sora = ?") + params.append(1 if has_sora else 0) + if has_plus is not None: + where.append("has_plus = ?") + params.append(1 if has_plus else 0) + if phone_bound is not None: + where.append("phone_bound = ?") + params.append(1 if phone_bound else 0) + where_sql = " AND ".join(where) if where else "1=1" + c.execute( + f"SELECT COUNT(*) FROM accounts WHERE {where_sql}", + params + ) + total = c.fetchone()[0] + offset = (page - 1) * page_size + c.execute( + f"""SELECT id, email, password, status, registered_at, + has_sora, has_plus, phone_bound, proxy, refresh_token, access_token, created_at, + COALESCE(sora_enabled, 1) AS sora_enabled, + COALESCE(sora_quota_exhausted, 0) AS sora_quota_exhausted, + COALESCE(sora_quota_note, '') AS sora_quota_note, + COALESCE(sora_quota_updated_at, '') AS sora_quota_updated_at, + COALESCE(sora_last_error, '') AS sora_last_error + FROM accounts WHERE {where_sql} + ORDER BY id DESC LIMIT ? OFFSET ?""", + params + [page_size, offset] + ) + rows = c.fetchall() + items = [] + for r in rows: + items.append({ + "id": r[0], + "email": r[1], + "password": r[2], + "status": r[3], + "registered_at": r[4], + "has_sora": bool(r[5]), + "has_plus": bool(r[6]), + "phone_bound": bool(r[7]), + "proxy": r[8], + "refresh_token": (r[9] or "")[:20] + "..." if r[9] else "", + "access_token": (r[10] or "")[:20] + "..." if r[10] else "", + "created_at": r[11], + "sora_enabled": bool(r[12]), + "sora_quota_exhausted": bool(r[13]), + "sora_quota_note": r[14] or "", + "sora_quota_updated_at": r[15] or "", + "sora_last_error": r[16] or "", + }) + return {"total": total, "page": page, "page_size": page_size, "items": items} + + +@router.post("/sora-quota/recheck") +def recheck_sora_quota( + body: AccountSoraQuotaRecheckBody, + username: str = Depends(get_current_user), +): + limit = max(1, min(int(body.limit or 10), 50)) + candidates = _load_quota_recheck_candidates(account_id=body.account_id, limit=limit) + if body.account_id is not None and not candidates: + raise HTTPException(status_code=404, detail="账号不存在") + + if not candidates: + return { + "ok": True, + "message": "当前没有被标记额度不足的账号", + "checked_count": 0, + "recovered_count": 0, + "still_exhausted_count": 0, + "failed_count": 0, + "busy_count": 0, + "items": [], + } + + items = [_probe_account_sora_quota(account, body) for account in candidates] + recovered_count = sum(1 for item in items if item.get("result") == "recovered") + busy_count = sum(1 for item in items if item.get("result") == "recovered_busy") + still_exhausted_count = sum(1 for item in items if item.get("result") == "still_exhausted") + failed_count = sum( + 1 + for item in items + if item.get("result") in ("probe_failed", "auth_failed", "skipped_no_token", "skipped_disabled", "skipped_no_sora") + ) + message = ( + f"已复检 {len(items)} 个账号,恢复 {recovered_count + busy_count} 个," + f"仍然额度不足 {still_exhausted_count} 个,失败/跳过 {failed_count} 个" + ) + return { + "ok": True, + "message": message, + "checked_count": len(items), + "recovered_count": recovered_count, + "busy_count": busy_count, + "still_exhausted_count": still_exhausted_count, + "failed_count": failed_count, + "items": items, + } + + +@router.get("/next-sora-available") +def next_sora_available_account(username: str = Depends(get_current_user)): + """ + 手动轮换:返回下一个可用 Sora 账号(仅可用且有 token 的账号)。 + """ + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute("SELECT value FROM system_settings WHERE key = 'sora_manual_rotate_cursor'") + row = c.fetchone() + try: + cursor = int((row[0] if row else "0") or "0") + except Exception: + cursor = 0 + + c.execute( + """SELECT id, email, status, + COALESCE(sora_enabled, 1) AS sora_enabled, + COALESCE(sora_quota_exhausted, 0) AS sora_quota_exhausted, + COALESCE(sora_quota_note, '') AS sora_quota_note, + COALESCE(sora_quota_updated_at, '') AS sora_quota_updated_at + FROM accounts + WHERE has_sora = 1 + AND COALESCE(sora_enabled, 1) = 1 + AND COALESCE(sora_quota_exhausted, 0) = 0 + AND (COALESCE(refresh_token, '') != '' OR COALESCE(access_token, '') != '') + ORDER BY id ASC""" + ) + rows = c.fetchall() + if not rows: + raise HTTPException(status_code=404, detail="暂无可用 Sora 账号(请检查 token/额度/启停状态)") + + pick = None + for r in rows: + if int(r[0]) > cursor: + pick = r + break + if pick is None: + pick = rows[0] + + c.execute( + "INSERT OR REPLACE INTO system_settings (key, value) VALUES (?, ?)", + ("sora_manual_rotate_cursor", str(int(pick[0]))), + ) + + return { + "id": int(pick[0]), + "email": pick[1] or "", + "status": pick[2] or "", + "sora_enabled": bool(pick[3]), + "sora_quota_exhausted": bool(pick[4]), + "sora_quota_note": pick[5] or "", + "sora_quota_updated_at": pick[6] or "", + } + + +@router.get("/{account_id}") +def get_account_detail(account_id: int, username: str = Depends(get_current_user)): + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + """SELECT id, email, status, registered_at, + has_sora, has_plus, phone_bound, + COALESCE(refresh_token, '') AS refresh_token, + COALESCE(access_token, '') AS access_token, + COALESCE(sora_enabled, 1) AS sora_enabled, + COALESCE(sora_quota_exhausted, 0) AS sora_quota_exhausted, + COALESCE(sora_quota_note, '') AS sora_quota_note, + COALESCE(sora_quota_updated_at, '') AS sora_quota_updated_at, + COALESCE(sora_last_error, '') AS sora_last_error + FROM accounts + WHERE id = ?""", + (account_id,), + ) + row = c.fetchone() + if not row: + raise HTTPException(status_code=404, detail="账号不存在") + return { + "id": row[0], + "email": row[1] or "", + "status": row[2] or "", + "registered_at": row[3] or "", + "has_sora": bool(row[4]), + "has_plus": bool(row[5]), + "phone_bound": bool(row[6]), + "has_token": bool((row[7] or "").strip() or (row[8] or "").strip()), + "sora_enabled": bool(row[9]), + "sora_quota_exhausted": bool(row[10]), + "sora_quota_note": row[11] or "", + "sora_quota_updated_at": row[12] or "", + "sora_last_error": row[13] or "", + } + + +@router.post("/{account_id}/sora-state") +def update_account_sora_state( + account_id: int, + body: AccountSoraStateBody, + username: str = Depends(get_current_user), +): + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute("SELECT id FROM accounts WHERE id = ?", (account_id,)) + if not c.fetchone(): + raise HTTPException(status_code=404, detail="账号不存在") + + if body.sora_enabled is not None: + c.execute( + "UPDATE accounts SET sora_enabled = ? WHERE id = ?", + (1 if body.sora_enabled else 0, account_id), + ) + + if body.reset_quota: + c.execute( + """UPDATE accounts + SET sora_quota_exhausted = 0, + sora_quota_note = '', + sora_last_error = '', + sora_quota_updated_at = datetime('now') + WHERE id = ?""", + (account_id,), + ) + + c.execute( + """SELECT id, email, + COALESCE(sora_enabled, 1), + COALESCE(sora_quota_exhausted, 0), + COALESCE(sora_quota_note, ''), + COALESCE(sora_quota_updated_at, ''), + COALESCE(sora_last_error, '') + FROM accounts + WHERE id = ?""", + (account_id,), + ) + row = c.fetchone() + + return { + "id": row[0], + "email": row[1] or "", + "sora_enabled": bool(row[2]), + "sora_quota_exhausted": bool(row[3]), + "sora_quota_note": row[4] or "", + "sora_quota_updated_at": row[5] or "", + "sora_last_error": row[6] or "", + } + + +@router.get("/export") +def export_accounts( + username: str = Depends(get_current_user), + status: str = Query(None), + has_sora: bool = Query(None), + has_plus: bool = Query(None), +): + init_db() + with get_db() as conn: + c = conn.cursor() + where = [] + params = [] + if status: + where.append("status = ?") + params.append(status) + if has_sora is not None: + where.append("has_sora = ?") + params.append(1 if has_sora else 0) + if has_plus is not None: + where.append("has_plus = ?") + params.append(1 if has_plus else 0) + where_sql = " AND ".join(where) if where else "1=1" + c.execute( + f"""SELECT email, password, status, registered_at, has_sora, has_plus, phone_bound, proxy, refresh_token, access_token, + COALESCE(sora_enabled, 1), COALESCE(sora_quota_exhausted, 0), COALESCE(sora_quota_note, '') + FROM accounts WHERE {where_sql} ORDER BY id DESC""", + params + ) + rows = c.fetchall() + buf = io.StringIO() + writer = csv.writer(buf) + writer.writerow(["email", "password", "status", "registered_at", "has_sora", "has_plus", "phone_bound", "proxy", "refresh_token", "access_token", "sora_enabled", "sora_quota_exhausted", "sora_quota_note"]) + for r in rows: + writer.writerow([ + r[0], r[1], r[2], r[3], + "Y" if r[4] else "N", "Y" if r[5] else "N", "Y" if r[6] else "N", + r[7] or "", r[8] or "", r[9] or "", + "Y" if r[10] else "N", "Y" if r[11] else "N", r[12] or "" + ]) + buf.seek(0) + return StreamingResponse( + iter([buf.getvalue()]), + media_type="text/csv", + headers={"Content-Disposition": "attachment; filename=accounts.csv"} + ) + + +@router.get("/export-sora2") +def export_sora2_accounts( + username: str = Depends(get_current_user), + has_sora: bool = Query(True, description="是否只导出 has_sora=1 的账号"), + require_token: bool = Query(False, description="是否要求 refresh_token/access_token 至少有一个"), + only_available: bool = Query(False, description="是否仅导出未停用且未标记额度不足账号"), + format: str = Query("txt", description="导出格式: txt 或 csv"), + separator: str = Query("----", description="txt 模式下字段分隔符"), +): + """ + 导出 Sora2 可用账号: + - txt: email----password----refresh_token----access_token(无表头) + - csv: 带表头 + """ + fmt = (format or "txt").strip().lower() + if fmt not in ("txt", "csv"): + fmt = "txt" + sep = separator if isinstance(separator, str) and separator else "----" + + init_db() + with get_db() as conn: + c = conn.cursor() + where = [] + params = [] + if has_sora is not None: + where.append("has_sora = ?") + params.append(1 if has_sora else 0) + if require_token: + where.append("(COALESCE(refresh_token, '') != '' OR COALESCE(access_token, '') != '')") + if only_available: + where.append("COALESCE(sora_enabled, 1) = 1") + where.append("COALESCE(sora_quota_exhausted, 0) = 0") + where_sql = " AND ".join(where) if where else "1=1" + c.execute( + f"""SELECT email, password, refresh_token, access_token, status, registered_at + FROM accounts + WHERE {where_sql} + ORDER BY id DESC""", + params + ) + rows = c.fetchall() + + now = datetime.now().strftime("%Y%m%d-%H%M%S") + if fmt == "csv": + buf = io.StringIO() + writer = csv.writer(buf) + writer.writerow(["email", "password", "refresh_token", "access_token", "status", "registered_at"]) + for r in rows: + writer.writerow([r[0] or "", r[1] or "", r[2] or "", r[3] or "", r[4] or "", r[5] or ""]) + buf.seek(0) + return StreamingResponse( + iter([buf.getvalue()]), + media_type="text/csv", + headers={"Content-Disposition": f"attachment; filename=sora2-accounts-{now}.csv"} + ) + + lines = [] + for r in rows: + lines.append(sep.join([ + (r[0] or "").strip(), + (r[1] or "").strip(), + (r[2] or "").strip(), + (r[3] or "").strip(), + ])) + body = "\n".join(lines) + return StreamingResponse( + iter([body]), + media_type="text/plain; charset=utf-8", + headers={"Content-Disposition": f"attachment; filename=sora2-accounts-{now}.txt"} + ) diff --git a/Register_GPT_v0/web/backend/app/routers/auth.py b/Register_GPT_v0/web/backend/app/routers/auth.py new file mode 100644 index 0000000..a6ff0d0 --- /dev/null +++ b/Register_GPT_v0/web/backend/app/routers/auth.py @@ -0,0 +1,78 @@ +from datetime import datetime, timedelta +from fastapi import APIRouter, Depends, HTTPException +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from pydantic import BaseModel +from jose import JWTError, jwt +from app.config import settings +from app.database import get_db, init_db +from app.security import get_password_hash, verify_password + +router = APIRouter(prefix="/api/auth", tags=["auth"]) +security = HTTPBearer(auto_error=False) + + +def _check_admin(username: str, password: str) -> bool: + with get_db() as conn: + c = conn.cursor() + c.execute("SELECT password_hash FROM admin_users WHERE username = ?", (username,)) + row = c.fetchone() + if row and row[0]: + return verify_password(password, row[0]) + # 无 DB 记录时:先认默认 admin/admin123,再认配置 + if username == "admin" and password == "admin123": + return True + return username == settings.admin_username and password == settings.admin_password + + +def create_token(username: str) -> str: + expire = datetime.utcnow() + timedelta(hours=24) + payload = {"sub": username, "exp": expire} + return jwt.encode(payload, settings.secret_key, algorithm="HS256") + + +def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)): + if not credentials or not credentials.credentials: + raise HTTPException(status_code=401, detail="Not authenticated") + try: + payload = jwt.decode(credentials.credentials, settings.secret_key, algorithms=["HS256"]) + username = payload.get("sub") + if not username: + raise HTTPException(status_code=401, detail="Invalid token") + return username + except JWTError: + raise HTTPException(status_code=401, detail="Invalid token") + + +def get_optional_user(credentials: HTTPAuthorizationCredentials = Depends(security)): + """不抛错,无凭证或无效时返回 None。用于 debug 等可选鉴权接口。""" + if not credentials or not credentials.credentials: + return None + try: + payload = jwt.decode(credentials.credentials, settings.secret_key, algorithms=["HS256"]) + return payload.get("sub") or None + except JWTError: + return None + + +class LoginIn(BaseModel): + username: str + password: str + + +class LoginOut(BaseModel): + token: str + username: str + + +@router.post("/login", response_model=LoginOut) +def login(data: LoginIn): + init_db() + if not _check_admin(data.username, data.password): + raise HTTPException(status_code=401, detail="Wrong username or password") + token = create_token(data.username) + return LoginOut(token=token, username=data.username) + + +@router.get("/me") +def me(username: str = Depends(get_current_user)): + return {"username": username} diff --git a/Register_GPT_v0/web/backend/app/routers/bank_cards.py b/Register_GPT_v0/web/backend/app/routers/bank_cards.py new file mode 100644 index 0000000..3d2b845 --- /dev/null +++ b/Register_GPT_v0/web/backend/app/routers/bank_cards.py @@ -0,0 +1,99 @@ +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel +from app.routers.auth import get_current_user +from app.database import get_db, init_db + +router = APIRouter(prefix="/api/bank-cards", tags=["bank_cards"]) + + +class BankCardCreate(BaseModel): + card_number_masked: str = "" + card_data: str = "" + max_use_count: int = 1 + remark: str = "" + + +class BatchImportBody(BaseModel): + lines: str = "" # 每行一条卡信息(可仅后四位或掩码) + + +@router.get("") +def list_cards(username: str = Depends(get_current_user)): + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + "SELECT id, card_number_masked, card_data, max_use_count, used_count, remark, created_at FROM bank_cards ORDER BY id DESC" + ) + rows = c.fetchall() + return { + "items": [ + { + "id": r[0], "card_number_masked": r[1], "card_data": r[2], + "max_use_count": r[3], "used_count": r[4], "remark": r[5], "created_at": r[6] + } + for r in rows + ] + } + + +@router.post("") +def create_card(body: BankCardCreate, username: str = Depends(get_current_user)): + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + "INSERT INTO bank_cards (card_number_masked, card_data, max_use_count, remark) VALUES (?, ?, ?, ?)", + (body.card_number_masked, body.card_data, body.max_use_count, body.remark) + ) + lid = c.lastrowid + return {"ok": True, "id": lid} + + +@router.delete("/{id}") +def delete_card(id: int, username: str = Depends(get_current_user)): + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute("DELETE FROM bank_cards WHERE id = ?", (id,)) + if c.rowcount == 0: + raise HTTPException(status_code=404, detail="Not found") + return {"ok": True} + + +@router.post("/batch-import") +def batch_import(body: BatchImportBody, username: str = Depends(get_current_user)): + """每行一条卡(如后四位或掩码),max_use_count 从系统设置读取""" + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute("SELECT value FROM system_settings WHERE key = ?", ("card_use_limit",)) + row = c.fetchone() + limit = int(row[0]) if row and row[0] else 1 + added = 0 + with get_db() as conn: + c = conn.cursor() + for line in body.lines.strip().split("\n"): + line = line.strip() + if not line or line.startswith("#"): + continue + c.execute( + "INSERT INTO bank_cards (card_number_masked, card_data, max_use_count, remark) VALUES (?, ?, ?, ?)", + (line[:20], line, limit, "") + ) + added += 1 + return {"ok": True, "added": added} + + +class BatchDeleteBody(BaseModel): + ids: list[int] = [] + + +@router.post("/batch-delete") +def batch_delete(body: BatchDeleteBody, username: str = Depends(get_current_user)): + init_db() + with get_db() as conn: + c = conn.cursor() + for id in body.ids: + c.execute("DELETE FROM bank_cards WHERE id = ?", (id,)) + return {"ok": True} diff --git a/Register_GPT_v0/web/backend/app/routers/dashboard.py b/Register_GPT_v0/web/backend/app/routers/dashboard.py new file mode 100644 index 0000000..2696ba4 --- /dev/null +++ b/Register_GPT_v0/web/backend/app/routers/dashboard.py @@ -0,0 +1,71 @@ +"""批量注册页仪表盘:统计与 API 配置状态""" +from fastapi import APIRouter, Depends +from app.routers.auth import get_current_user +from app.database import get_db, init_db + +router = APIRouter(prefix="/api/dashboard", tags=["dashboard"]) + + +@router.get("") +def get_dashboard(username: str = Depends(get_current_user)): + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute("SELECT COUNT(*) FROM accounts") + total_registered = c.fetchone()[0] + c.execute( + "SELECT COUNT(*) FROM accounts WHERE date(created_at) = date('now')" + ) + today_registered = c.fetchone()[0] + c.execute("SELECT COUNT(*) FROM accounts WHERE phone_bound = 1") + phone_bound_count = c.fetchone()[0] + c.execute("SELECT COUNT(*) FROM accounts WHERE has_plus = 1") + plus_count = c.fetchone()[0] + c.execute( + """SELECT COUNT(*) FROM accounts + WHERE has_sora = 1 + AND COALESCE(sora_enabled, 1) = 1 + AND COALESCE(sora_quota_exhausted, 0) = 0 + AND (COALESCE(refresh_token, '') != '' OR COALESCE(access_token, '') != '')""" + ) + sora_available_count = c.fetchone()[0] + c.execute( + """SELECT COUNT(*) FROM sora_video_tasks + WHERE task_id NOT LIKE 'lease_%' + AND normalized_status = 'succeeded' + AND date(COALESCE(succeeded_at, updated_at), 'localtime') = date('now', 'localtime')""" + ) + today_generated_videos = c.fetchone()[0] + c.execute( + "SELECT key, value FROM system_settings WHERE key IN ('email_api_key', 'sms_api_key', 'bank_card_api_key', 'captcha_api_key', 'thread_count', 'last_run_success', 'last_run_fail')" + ) + settings = dict(c.fetchall()) + email_api_set = bool(settings.get("email_api_key") and str(settings.get("email_api_key", "")).strip()) + sms_api_set = bool(settings.get("sms_api_key") and str(settings.get("sms_api_key", "")).strip()) + bank_api_set = bool(settings.get("bank_card_api_key") and str(settings.get("bank_card_api_key", "")).strip()) + captcha_api_set = bool(settings.get("captcha_api_key") and str(settings.get("captcha_api_key", "")).strip()) + thread_count = settings.get("thread_count") or "1" + try: + success_count = int(settings.get("last_run_success") or 0) + except (TypeError, ValueError): + success_count = 0 + try: + fail_count = int(settings.get("last_run_fail") or 0) + except (TypeError, ValueError): + fail_count = 0 + return { + "today_registered": today_registered, + "total_registered": total_registered, + "phone_bound_count": phone_bound_count, + "plus_count": plus_count, + "sora_available_count": sora_available_count, + "today_generatable_videos": sora_available_count, + "today_generated_videos": today_generated_videos, + "email_api_set": email_api_set, + "sms_api_set": sms_api_set, + "bank_api_set": bank_api_set, + "captcha_api_set": captcha_api_set, + "thread_count": thread_count, + "success_count": success_count, + "fail_count": fail_count, + } diff --git a/Register_GPT_v0/web/backend/app/routers/email_api.py b/Register_GPT_v0/web/backend/app/routers/email_api.py new file mode 100644 index 0000000..b6c8417 --- /dev/null +++ b/Register_GPT_v0/web/backend/app/routers/email_api.py @@ -0,0 +1,135 @@ +""" +邮箱 API(Hotmail007)对接:余额、库存、拉取并可选导入到邮箱表 +""" +from fastapi import APIRouter, Depends, HTTPException, Query +from pydantic import BaseModel +from app.routers.auth import get_current_user +from app.database import get_db, init_db +from app.services.hotmail007 import get_balance, get_stock, get_mail, get_first_mail, MAIL_TYPES + +router = APIRouter(prefix="/api/email-api", tags=["email-api"]) + + +def _get_email_api_settings(): + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute("SELECT key, value FROM system_settings WHERE key IN ('email_api_url', 'email_api_key', 'email_api_default_type')") + rows = c.fetchall() + out = {} + for k, v in rows: + out[k] = (v or "").strip() + base = out.get("email_api_url") or "https://gapi.hotmail007.com" + key = out.get("email_api_key") + default_type = out.get("email_api_default_type") or "outlook" + if default_type not in MAIL_TYPES: + default_type = "outlook" + return base, key, default_type + + +@router.get("/balance") +def api_balance(username: str = Depends(get_current_user)): + """查询 Hotmail007 余额""" + base, key, _ = _get_email_api_settings() + if not key: + raise HTTPException(status_code=400, detail="请先在系统设置中配置邮箱 API KEY (clientKey)") + balance = get_balance(base, key) + if balance is None: + raise HTTPException(status_code=502, detail="请求余额失败,请检查 API 地址与 KEY") + return {"balance": balance} + + +@router.get("/stock") +def api_stock( + mail_type: str = Query(None, description="outlook / hotmail / hotmail Trusted / outlook Trusted"), + username: str = Depends(get_current_user), +): + """查询邮箱库存(不要求 KEY)""" + base, _, _ = _get_email_api_settings() + stock = get_stock(base, mail_type if mail_type in MAIL_TYPES else None) + if stock is None: + raise HTTPException(status_code=502, detail="请求库存失败") + return {"stock": stock, "mail_type": mail_type or "全部"} + + +class FetchMailBody(BaseModel): + mail_type: str = "outlook" + quantity: int = 1 + import_to_emails: bool = True + + +@router.post("/fetch-mail") +def api_fetch_mail(body: FetchMailBody, username: str = Depends(get_current_user)): + """从 Hotmail007 拉取邮箱,可选导入到邮箱管理表""" + base, key, default_type = _get_email_api_settings() + if not key: + raise HTTPException(status_code=400, detail="请先在系统设置中配置邮箱 API KEY (clientKey)") + mail_type = body.mail_type if body.mail_type in MAIL_TYPES else default_type + quantity = max(1, min(body.quantity, 100)) + items = get_mail(base, key, quantity, mail_type) + if not items: + return {"count": 0, "imported": 0, "message": "未拉取到数据或请求失败"} + imported = 0 + if body.import_to_emails: + with get_db() as conn: + c = conn.cursor() + for row in items: + c.execute( + "INSERT INTO emails (email, password, uuid, token, remark) VALUES (?, ?, ?, ?, ?)", + ( + row["email"], + row["password"], + row.get("client_id") or "", + row.get("refresh_token") or "", + "Hotmail007", + ), + ) + imported += 1 + return {"count": len(items), "imported": imported, "items": items} + + +@router.get("/first-mail") +def api_first_mail( + email_id: int = Query(..., description="邮箱表主键 id"), + folder: str = Query("inbox", description="inbox / junkemail"), + username: str = Depends(get_current_user), +): + """通过 Hotmail007 API 获取该邮箱最新一封邮件(收件箱)""" + base, key, _ = _get_email_api_settings() + if not key: + raise HTTPException(status_code=400, detail="请先在系统设置中配置邮箱 API KEY (clientKey)") + with get_db() as conn: + c = conn.cursor() + c.execute("SELECT email, password, token, uuid FROM emails WHERE id = ?", (email_id,)) + row = c.fetchone() + if not row: + raise HTTPException(status_code=404, detail="邮箱不存在") + email, password, token, uuid = row + account = f"{email}:{password or ''}:{token or ''}:{uuid or ''}" + data = get_first_mail(base, key, account, folder=folder) + if data is None: + raise HTTPException(status_code=502, detail="未收到邮件或 API 请求失败") + return {"mail": data} + + +@router.get("/mail-list") +def api_mail_list( + email_id: int = Query(..., description="邮箱表主键 id"), + folder: str = Query("inbox", description="inbox / junkemail"), + username: str = Depends(get_current_user), +): + """获取该邮箱收件箱邮件列表(当前 Hotmail007 仅支持最新一封,返回 list 长度 0 或 1)""" + base, key, _ = _get_email_api_settings() + if not key: + raise HTTPException(status_code=400, detail="请先在系统设置中配置邮箱 API KEY (clientKey)") + with get_db() as conn: + c = conn.cursor() + c.execute("SELECT email, password, token, uuid FROM emails WHERE id = ?", (email_id,)) + row = c.fetchone() + if not row: + raise HTTPException(status_code=404, detail="邮箱不存在") + email, password, token, uuid = row + account = f"{email}:{password or ''}:{token or ''}:{uuid or ''}" + data = get_first_mail(base, key, account, folder=folder) + list_ = [data] if (data and isinstance(data, dict) and len(data) > 0) else [] + return {"list": list_} diff --git a/Register_GPT_v0/web/backend/app/routers/emails.py b/Register_GPT_v0/web/backend/app/routers/emails.py new file mode 100644 index 0000000..9f41177 --- /dev/null +++ b/Register_GPT_v0/web/backend/app/routers/emails.py @@ -0,0 +1,122 @@ +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel +from app.routers.auth import get_current_user +from app.database import get_db, init_db + +router = APIRouter(prefix="/api/emails", tags=["emails"]) + + +class EmailCreate(BaseModel): + email: str + password: str = "" + uuid: str = "" + token: str = "" + remark: str = "" + + +@router.get("") +def list_emails(username: str = Depends(get_current_user)): + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute("SELECT id, email, password, uuid, token, remark, created_at FROM emails ORDER BY id DESC") + rows = c.fetchall() + c.execute("SELECT email FROM accounts") + registered_emails = {row[0].strip().lower() for row in c.fetchall() if row[0]} + return { + "items": [ + { + "id": r[0], + "email": r[1], + "password": r[2], + "uuid": r[3], + "token": r[4], + "remark": r[5], + "created_at": r[6], + "registered": (r[1] or "").strip().lower() in registered_emails, + } + for r in rows + ] + } + + +@router.post("") +def create_email(body: EmailCreate, username: str = Depends(get_current_user)): + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + "INSERT INTO emails (email, password, uuid, token, remark) VALUES (?, ?, ?, ?, ?)", + (body.email, body.password, body.uuid, body.token, body.remark) + ) + return {"ok": True, "id": c.lastrowid} + + +@router.get("/export") +def export_emails(username: str = Depends(get_current_user)): + """返回全部邮箱(含密码),用于批量导出,格式与批量导入一致""" + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute("SELECT email, password, uuid, token, remark FROM emails ORDER BY id DESC") + rows = c.fetchall() + return { + "items": [ + {"email": r[0], "password": r[1] or "", "uuid": r[2] or "", "token": r[3] or "", "remark": r[4] or ""} + for r in rows + ] + } + + +@router.get("/{id}") +def get_email(id: int, username: str = Depends(get_current_user)): + """获取单条邮箱详情(含密码),用于查看邮箱/登录""" + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute("SELECT id, email, password, uuid, token, remark FROM emails WHERE id = ?", (id,)) + row = c.fetchone() + if not row: + raise HTTPException(status_code=404, detail="Not found") + return {"id": row[0], "email": row[1], "password": row[2] or "", "uuid": row[3] or "", "token": row[4] or "", "remark": row[5] or ""} + + +@router.delete("/{id}") +def delete_email(id: int, username: str = Depends(get_current_user)): + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute("DELETE FROM emails WHERE id = ?", (id,)) + if c.rowcount == 0: + raise HTTPException(status_code=404, detail="Not found") + return {"ok": True} + + +class BatchImportBody(BaseModel): + lines: str = "" + + +@router.post("/batch-import") +def batch_import(body: BatchImportBody, username: str = Depends(get_current_user)): + """Body: {"lines": "邮箱----密码----uuid----token 多行"}""" + init_db() + added = 0 + with get_db() as conn: + c = conn.cursor() + for line in body.lines.strip().split("\n"): + line = line.strip() + if not line or line.startswith("#"): + continue + parts = [p.strip() for p in line.split("----")] + email = parts[0] if parts else "" + if not email: + continue + password = parts[1] if len(parts) > 1 else "" + uuid = parts[2] if len(parts) > 2 else "" + token = parts[3] if len(parts) > 3 else "" + c.execute( + "INSERT INTO emails (email, password, uuid, token, remark) VALUES (?, ?, ?, ?, ?)", + (email, password, uuid, token, "") + ) + added += 1 + return {"ok": True, "added": added} diff --git a/Register_GPT_v0/web/backend/app/routers/logs.py b/Register_GPT_v0/web/backend/app/routers/logs.py new file mode 100644 index 0000000..c7fafda --- /dev/null +++ b/Register_GPT_v0/web/backend/app/routers/logs.py @@ -0,0 +1,51 @@ +from fastapi import APIRouter, Depends, Query +from app.routers.auth import get_current_user +from app.database import get_db, init_db + +router = APIRouter(prefix="/api/logs", tags=["logs"]) + + +@router.get("") +def list_logs( + username: str = Depends(get_current_user), + task_id: str = Query(None), + page: int = Query(1, ge=1), + page_size: int = Query(100, ge=1, le=500), +): + init_db() + with get_db() as conn: + c = conn.cursor() + if task_id: + c.execute( + "SELECT COUNT(*) FROM run_logs WHERE task_id = ?", (task_id,) + ) + else: + c.execute("SELECT COUNT(*) FROM run_logs") + total = c.fetchone()[0] + offset = (page - 1) * page_size + if task_id: + c.execute( + "SELECT id, task_id, level, message, created_at FROM run_logs WHERE task_id = ? ORDER BY id DESC LIMIT ? OFFSET ?", + (task_id, page_size, offset) + ) + else: + c.execute( + "SELECT id, task_id, level, message, created_at FROM run_logs ORDER BY id DESC LIMIT ? OFFSET ?", + (page_size, offset) + ) + rows = c.fetchall() + items = [ + {"id": r[0], "task_id": r[1], "level": r[2], "message": r[3], "created_at": r[4]} + for r in rows + ] + return {"total": total, "page": page, "page_size": page_size, "items": items} + + +@router.delete("") +def clear_logs(username: str = Depends(get_current_user)): + """清空 run_logs 表所有记录。""" + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute("DELETE FROM run_logs") + return {"ok": True, "message": "已清空日志"} diff --git a/Register_GPT_v0/web/backend/app/routers/phone_bind.py b/Register_GPT_v0/web/backend/app/routers/phone_bind.py new file mode 100644 index 0000000..c0c1bc5 --- /dev/null +++ b/Register_GPT_v0/web/backend/app/routers/phone_bind.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +""" +开始绑定手机:POST /api/phone-bind/start 启动任务,GET /api/phone-bind/status 查状态,POST /api/phone-bind/stop 停止。 +""" +import threading +from datetime import datetime + +from fastapi import APIRouter, Depends + +from app.routers.auth import get_current_user +from app.services.phone_bind_runner import ( + set_phone_bind_stop, + set_phone_bind_task_started, + get_phone_bind_status, + run_phone_bind_loop, + _log, +) + +router = APIRouter(prefix="/api/phone-bind", tags=["phone-bind"]) + + +def _run_bind_task(task_id: str, max_count: int = None): + try: + run_phone_bind_loop(task_id, max_count=max_count) + except Exception as e: + _log(task_id, "error", f"绑定任务异常: {e}") + + +@router.post("/start") +def start_phone_bind( + max_count: int = None, + username: str = Depends(get_current_user), +): + """启动绑定手机任务:从账号管理取未绑账号,从手机号管理取可用号码,逐个绑定。max_count 可选,不传则处理到无数据为止。""" + task_id = f"phone_bind_{datetime.utcnow().strftime('%Y%m%d%H%M%S')}" + if not set_phone_bind_task_started(task_id): + st = get_phone_bind_status() + return {"ok": False, "message": "绑定任务已在运行", "task_id": st.get("task_id")} + t = threading.Thread(target=_run_bind_task, args=(task_id,), kwargs={"max_count": max_count}, daemon=True) + t.start() + return {"ok": True, "message": "绑定任务已启动", "task_id": task_id} + + +@router.get("/status") +def phone_bind_status(username: str = Depends(get_current_user)): + """查询绑定任务状态。""" + return get_phone_bind_status() + + +@router.post("/stop") +def stop_phone_bind(username: str = Depends(get_current_user)): + """请求停止绑定任务。""" + set_phone_bind_stop(True) + return {"ok": True, "message": "已请求停止,当前条完成后退出"} diff --git a/Register_GPT_v0/web/backend/app/routers/phones.py b/Register_GPT_v0/web/backend/app/routers/phones.py new file mode 100644 index 0000000..3b37fcb --- /dev/null +++ b/Register_GPT_v0/web/backend/app/routers/phones.py @@ -0,0 +1,175 @@ +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel +from app.routers.auth import get_current_user +from app.database import get_db, init_db + +router = APIRouter(prefix="/api/phones", tags=["phones"]) + + +class PhoneCreate(BaseModel): + phone: str = "" + max_use_count: int = 1 + remark: str = "" + + +class BatchImportBody(BaseModel): + lines: str = "" + + +class BatchDeleteBody(BaseModel): + ids: list[int] = [] + + +def _get_sms_settings(): + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute("SELECT key, value FROM system_settings WHERE key IN ('sms_api_url', 'sms_api_key')") + rows = c.fetchall() + out = {} + for k, v in rows: + out[k] = (v or "").strip() + base = out.get("sms_api_url") or "https://hero-sms.com/stubs/handler_api.php" + key = out.get("sms_api_key") + return base, key + + +@router.get("") +def list_phones(username: str = Depends(get_current_user)): + from app.services import hero_sms + init_db() + base, key = _get_sms_settings() + with get_db() as conn: + c = conn.cursor() + c.execute( + "SELECT id, activation_id FROM phone_numbers WHERE expired_at IS NOT NULL AND expired_at < datetime('now')" + ) + expired_rows = c.fetchall() + for row in expired_rows: + aid = row[1] + if aid is not None and key: + try: + hero_sms.set_status(base, key, aid, 8) + except Exception: + pass + c.execute( + "DELETE FROM phone_numbers WHERE expired_at IS NOT NULL AND expired_at < datetime('now')" + ) + c.execute( + "SELECT id, phone, activation_id, max_use_count, used_count, remark, expired_at, created_at FROM phone_numbers ORDER BY id DESC" + ) + rows = c.fetchall() + return { + "items": [ + { + "id": r[0], "phone": r[1], "activation_id": r[2], "max_use_count": r[3], + "used_count": r[4], "remark": r[5], "expired_at": r[6], "created_at": r[7] + } + for r in rows + ] + } + + +@router.post("") +def create_phone(body: PhoneCreate, username: str = Depends(get_current_user)): + init_db() + if not (body.phone or "").strip(): + raise HTTPException(status_code=400, detail="手机号不能为空") + with get_db() as conn: + c = conn.cursor() + c.execute( + "INSERT INTO phone_numbers (phone, max_use_count, remark) VALUES (?, ?, ?)", + (body.phone.strip(), body.max_use_count, body.remark) + ) + lid = c.lastrowid + return {"ok": True, "id": lid} + + +@router.delete("/{id}") +def delete_phone(id: int, username: str = Depends(get_current_user)): + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute("DELETE FROM phone_numbers WHERE id = ?", (id,)) + if c.rowcount == 0: + raise HTTPException(status_code=404, detail="Not found") + return {"ok": True} + + +@router.get("/{id}/sms-code") +def get_phone_sms_code(id: int, username: str = Depends(get_current_user)): + """查询该号码的短信验证码(接码平台 getStatusV2)""" + from app.services import hero_sms + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute("SELECT activation_id FROM phone_numbers WHERE id = ?", (id,)) + row = c.fetchone() + if not row: + raise HTTPException(status_code=404, detail="Not found") + activation_id = row[0] + if activation_id is None: + raise HTTPException(status_code=400, detail="该号码无 activation_id,无法查码") + base, key = _get_sms_settings() + if not key: + raise HTTPException(status_code=400, detail="请先配置手机号接码 API KEY") + out = hero_sms.get_status_v2(base, key, activation_id) + if not out: + return {"status": "error", "code": None, "message": "请求失败"} + return {"status": out.get("status", "wait"), "code": out.get("code"), "message": "已收到验证码" if out.get("code") else "等待短信中"} + + +@router.post("/{id}/release") +def release_phone(id: int, username: str = Depends(get_current_user)): + """销毁:通知接码平台取消该号码(setStatus=8)并从列表删除""" + from app.services import hero_sms + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute("SELECT activation_id FROM phone_numbers WHERE id = ?", (id,)) + row = c.fetchone() + if not row: + raise HTTPException(status_code=404, detail="Not found") + activation_id = row[0] + base, key = _get_sms_settings() + if activation_id is not None and key: + hero_sms.set_status(base, key, activation_id, 8) + with get_db() as conn: + c = conn.cursor() + c.execute("DELETE FROM phone_numbers WHERE id = ?", (id,)) + return {"ok": True} + + +@router.post("/batch-import") +def batch_import(body: BatchImportBody, username: str = Depends(get_current_user)): + """每行一个手机号,max_use_count 从系统设置 phone_bind_limit 读取""" + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute("SELECT value FROM system_settings WHERE key = ?", ("phone_bind_limit",)) + row = c.fetchone() + limit = int(row[0]) if row and row[0] else 1 + added = 0 + with get_db() as conn: + c = conn.cursor() + for line in body.lines.strip().split("\n"): + line = line.strip() + if not line or line.startswith("#"): + continue + c.execute( + "INSERT INTO phone_numbers (phone, max_use_count, remark) VALUES (?, ?, ?)", + (line, limit, "") + ) + added += 1 + return {"ok": True, "added": added} + + +@router.post("/batch-delete") +def batch_delete(body: BatchDeleteBody, username: str = Depends(get_current_user)): + init_db() + with get_db() as conn: + c = conn.cursor() + for id in body.ids: + c.execute("DELETE FROM phone_numbers WHERE id = ?", (id,)) + return {"ok": True} + diff --git a/Register_GPT_v0/web/backend/app/routers/register.py b/Register_GPT_v0/web/backend/app/routers/register.py new file mode 100644 index 0000000..a448e51 --- /dev/null +++ b/Register_GPT_v0/web/backend/app/routers/register.py @@ -0,0 +1,193 @@ +""" +开启注册:POST /api/register/start 启动调度,GET /api/register/status 查询状态与心跳。 +""" +import threading +from concurrent.futures import ThreadPoolExecutor, as_completed, TimeoutError as FuturesTimeoutError +from datetime import datetime, timezone + +from fastapi import APIRouter, Depends + +from app.registration_state import set_stop_requested, is_stop_requested +from app.routers.auth import get_current_user +from app.database import get_db, init_db +from app.services.registration_runner import ( + _get_registration_settings, + fetch_unregistered_emails, + run_one_task, +) + + +def _log_run(task_id: str, level: str, message: str): + try: + with get_db() as conn: + c = conn.cursor() + created = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + c.execute( + "INSERT INTO run_logs (task_id, level, message, created_at) VALUES (?, ?, ?, ?)", + (task_id, level, message, created), + ) + except Exception: + pass + +router = APIRouter(prefix="/api/register", tags=["register"]) + +_registration_running = False +_registration_heartbeat: str | None = None +_registration_lock = threading.Lock() + + +def _run_registration_loop(): + """后台线程:按 thread_count 并发取未注册邮箱并执行,写心跳到 system_settings。""" + global _registration_running, _registration_heartbeat + task_id = f"register_{datetime.utcnow().strftime('%Y%m%d%H%M%S')}" + settings = _get_registration_settings() + thread_count = max(1, min(32, int(settings.get("thread_count") or "1"))) + init_db() + + def _update_heartbeat(): + global _registration_heartbeat + with _registration_lock: + _registration_heartbeat = datetime.utcnow().isoformat() + "Z" + with get_db() as conn: + c = conn.cursor() + c.execute( + "INSERT OR REPLACE INTO system_settings (key, value) VALUES ('last_registration_heartbeat', ?)", + (_registration_heartbeat,), + ) + + try: + _log_run(task_id, "info", "注册任务已启动") + failed_this_run = set() # 本 run 内已失败过的邮箱,不再重复拉取,避免无限重试同一条 + while True: + if is_stop_requested(): + _log_run(task_id, "info", "已请求停止,立即结束") + break + batch = fetch_unregistered_emails(limit=thread_count) + batch = [row for row in batch if (row[1] or "").strip().lower() not in failed_this_run] + if not batch: + _log_run(task_id, "info", "注册任务结束,无更多未注册邮箱") + break + _update_heartbeat() + _log_run(task_id, "info", f"本批开始注册 共 {len(batch)} 条") + ex = ThreadPoolExecutor(max_workers=min(len(batch), thread_count)) + futures = { + ex.submit(run_one_task, task_id, settings, email_row=row): row + for row in batch + } + stopped_early = False + done_iterator = as_completed(futures, timeout=1.0) + num_futures = len(futures) + num_done = 0 + try: + while num_done < num_futures: + try: + fut = next(done_iterator) + except FuturesTimeoutError: + if is_stop_requested(): + _log_run(task_id, "info", "已请求停止,立即结束当前批次") + stopped_early = True + break + _update_heartbeat() + continue + except StopIteration: + break + if is_stop_requested(): + _log_run(task_id, "info", "已请求停止,立即结束当前批次") + stopped_early = True + break + num_done += 1 + try: + result = fut.result() + ok = result[0] if isinstance(result, (tuple, list)) and len(result) > 0 else result + if ok is False: + row = futures.get(fut) + if row and len(row) > 1: + failed_this_run.add((row[1] or "").strip().lower()) + except Exception: + pass + _update_heartbeat() + finally: + ex.shutdown(wait=not stopped_early) + if stopped_early: + break + finally: + try: + _log_run(task_id, "info", "注册调度已退出") + except Exception: + pass + with _registration_lock: + _registration_running = False + set_stop_requested(False) + + +@router.post("/start") +def start_registration(username: str = Depends(get_current_user)): + """启动一次注册任务(后台调度直到无未注册邮箱)。若已在运行则返回 409。""" + global _registration_running + with _registration_lock: + if _registration_running: + return {"ok": False, "message": "注册任务已在运行中"} + _registration_running = True + set_stop_requested(False) + t = threading.Thread(target=_run_registration_loop, daemon=True) + t.start() + return {"ok": True, "message": "已启动注册任务"} + + +@router.post("/stop") +def stop_registration(username: str = Depends(get_current_user)): + """请求停止注册任务(调度与进行中的任务都会尽快退出)。""" + global _registration_running + with _registration_lock: + if not _registration_running: + return {"ok": False, "message": "当前无运行中的注册任务"} + _registration_running = False + set_stop_requested(True) + return {"ok": True, "message": "已请求停止,正在立即结束"} + + +def _parse_heartbeat_time(s: str | None): + """解析 ISO 心跳时间,失败返回 None。""" + if not s or not isinstance(s, str): + return None + s = s.strip().replace("Z", "+00:00") + try: + return datetime.fromisoformat(s) + except Exception: + return None + + +# 心跳超过此分钟数仍视为任务已死,返回 running: false +_STATUS_HEARTBEAT_DEAD_MINUTES = 5 + + +@router.get("/status") +def get_registration_status(username: str = Depends(get_current_user)): + """返回是否运行中、最近心跳时间、last_run_success/fail。running 来自本进程的 _registration_running;若心跳超过 5 分钟则强制视为已停止。""" + with _registration_lock: + running = _registration_running + heartbeat = _registration_heartbeat + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + "SELECT key, value FROM system_settings WHERE key IN ('last_run_success', 'last_run_fail', 'last_registration_heartbeat')" + ) + rows = c.fetchall() + kv = {r[0]: r[1] for r in rows} + last_heartbeat = heartbeat or kv.get("last_registration_heartbeat") + # 若认为在运行但心跳超时,视为已停止(避免重启/异常后一直显示正在注册) + if running and last_heartbeat: + ht = _parse_heartbeat_time(last_heartbeat) + if ht: + now = datetime.now(timezone.utc) + if ht.tzinfo is None: + ht = ht.replace(tzinfo=timezone.utc) + if (now - ht).total_seconds() > _STATUS_HEARTBEAT_DEAD_MINUTES * 60: + running = False + return { + "running": running, + "last_heartbeat": last_heartbeat, + "last_run_success": int(kv.get("last_run_success") or 0), + "last_run_fail": int(kv.get("last_run_fail") or 0), + } diff --git a/Register_GPT_v0/web/backend/app/routers/settings.py b/Register_GPT_v0/web/backend/app/routers/settings.py new file mode 100644 index 0000000..baeac66 --- /dev/null +++ b/Register_GPT_v0/web/backend/app/routers/settings.py @@ -0,0 +1,114 @@ +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel +from app.routers.auth import get_current_user +from app.database import get_db, init_db +from app.security import get_password_hash + +router = APIRouter(prefix="/api/settings", tags=["settings"]) + + +def _clamp_retry(value: str) -> str: + try: + n = int((value or "").strip()) + return str(max(1, min(5, n))) + except (ValueError, TypeError): + return "2" + + +class LoginUpdateBody(BaseModel): + admin_username: str = "" + admin_password: str = "" + + +class SettingsBody(BaseModel): + sms_api_url: str = "" + sms_api_key: str = "" + sms_openai_service: str = "openai" + sms_max_price: str = "0.55" + thread_count: str = "1" + retry_count: str = "2" + proxy_url: str = "" + proxy_api_url: str = "" + bank_card_api_url: str = "" + bank_card_api_key: str = "" + bank_card_api_platform: str = "寻汇" + email_api_url: str = "" + email_api_key: str = "" + email_api_default_type: str = "outlook" + captcha_api_url: str = "" + captcha_api_key: str = "" + card_use_limit: str = "1" + phone_bind_limit: str = "1" + oauth_client_id: str = "" + oauth_redirect_uri: str = "" + admin_username: str = "" + admin_password: str = "" + + +@router.get("") +def get_settings(username: str = Depends(get_current_user)): + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute("SELECT key, value FROM system_settings") + rows = c.fetchall() + out = {} + for k, v in rows: + out[k] = v or "" + return out + + +@router.put("") +def update_settings(body: SettingsBody, username: str = Depends(get_current_user)): + init_db() + with get_db() as conn: + c = conn.cursor() + for key, value in [ + ("sms_api_url", body.sms_api_url), + ("sms_api_key", body.sms_api_key), + ("sms_openai_service", (body.sms_openai_service or "dr").strip()), + ("sms_max_price", (body.sms_max_price or "0.55").strip()), + ("thread_count", body.thread_count), + ("retry_count", _clamp_retry(body.retry_count)), + ("proxy_url", body.proxy_url), + ("proxy_api_url", body.proxy_api_url), + ("bank_card_api_url", body.bank_card_api_url), + ("bank_card_api_key", body.bank_card_api_key), + ("bank_card_api_platform", (body.bank_card_api_platform or "寻汇").strip()), + ("email_api_url", body.email_api_url), + ("email_api_key", body.email_api_key), + ("email_api_default_type", (body.email_api_default_type or "outlook").strip()), + ("captcha_api_url", body.captcha_api_url), + ("captcha_api_key", body.captcha_api_key), + ("card_use_limit", body.card_use_limit), + ("phone_bind_limit", body.phone_bind_limit), + ("oauth_client_id", (body.oauth_client_id or "").strip()), + ("oauth_redirect_uri", (body.oauth_redirect_uri or "").strip()), + ]: + c.execute( + "INSERT OR REPLACE INTO system_settings (key, value) VALUES (?, ?)", + (key, value or "") + ) + if body.admin_username and body.admin_password: + hash_val = get_password_hash(body.admin_password) + c.execute( + "INSERT INTO admin_users (username, password_hash, updated_at) VALUES (?, ?, datetime('now')) ON CONFLICT(username) DO UPDATE SET password_hash=?, updated_at=datetime('now')", + (body.admin_username, hash_val, hash_val) + ) + return {"ok": True} + + +@router.put("/login") +def update_login(body: LoginUpdateBody, username: str = Depends(get_current_user)): + """仅修改登录账号与密码,与系统设置分离""" + if not body.admin_username or not body.admin_password: + raise HTTPException(status_code=400, detail="账号与密码均不能为空") + hash_val = get_password_hash(body.admin_password) + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + "INSERT INTO admin_users (username, password_hash, updated_at) VALUES (?, ?, datetime('now')) ON CONFLICT(username) DO UPDATE SET password_hash=?, updated_at=datetime('now')", + (body.admin_username.strip(), hash_val, hash_val) + ) + return {"ok": True} diff --git a/Register_GPT_v0/web/backend/app/routers/sms_api.py b/Register_GPT_v0/web/backend/app/routers/sms_api.py new file mode 100644 index 0000000..17ee261 --- /dev/null +++ b/Register_GPT_v0/web/backend/app/routers/sms_api.py @@ -0,0 +1,258 @@ +""" +手机号接码 API(Hero-SMS / SMS-Activate 兼容) +文档: https://hero-sms.com/cn/api +""" +from datetime import datetime, timedelta +from fastapi import APIRouter, Depends, HTTPException, Query, Request +from pydantic import BaseModel +from jose import JWTError, jwt +from app.config import settings +from app.routers.auth import get_current_user +from app.database import get_db, init_db +from app.services import hero_sms + +# 接码平台未返回到期时间时,默认有效期(分钟) +PHONE_DEFAULT_EXPIRE_MINUTES = 20 + +router = APIRouter(prefix="/api/sms-api", tags=["sms-api"]) +OPENAI_SERVICE = "openai" + + +def _get_sms_settings(): + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + "SELECT key, value FROM system_settings WHERE key IN ('sms_api_url', 'sms_api_key', 'sms_openai_service', 'sms_max_price')" + ) + rows = c.fetchall() + out = {} + for k, v in rows: + out[k] = (v or "").strip() + base = out.get("sms_api_url") or "https://hero-sms.com/stubs/handler_api.php" + key = out.get("sms_api_key") + openai_service = (out.get("sms_openai_service") or "").strip() or "dr" + try: + max_price = float(out.get("sms_max_price") or "0.55") + except (TypeError, ValueError): + max_price = 0.55 + return base, key, openai_service, max_price + + +@router.get("/balance") +def api_balance(username: str = Depends(get_current_user)): + """查询 Hero-SMS 余额""" + base, key, _, _ = _get_sms_settings() + if not key: + raise HTTPException(status_code=400, detail="请先在系统设置中配置手机号接码 API KEY") + balance = hero_sms.get_balance(base, key) + if balance is None: + raise HTTPException(status_code=502, detail="请求余额失败,请检查 API 地址与 KEY") + return {"balance": balance} + + +@router.get("/countries") +def api_countries(username: str = Depends(get_current_user)): + """国家列表""" + base, key, _, _ = _get_sms_settings() + if not key: + raise HTTPException(status_code=400, detail="请先配置手机号接码 API KEY") + data = hero_sms.get_countries(base, key) + return {"countries": data} + + +@router.get("/services") +def api_services( + country: int = Query(0, description="国家 ID"), + username: str = Depends(get_current_user), +): + """服务列表(如 openai 等)""" + base, key, _, _ = _get_sms_settings() + if not key: + raise HTTPException(status_code=400, detail="请先配置手机号接码 API KEY") + data = hero_sms.get_services_list(base, key, country=country, lang="cn") + return {"services": data} + + +@router.get("/prices") +def api_prices( + service: str = Query(None), + country: int = Query(None), + username: str = Depends(get_current_user), +): + """价格/库存""" + base, key, _, _ = _get_sms_settings() + if not key: + raise HTTPException(status_code=400, detail="请先配置手机号接码 API KEY") + data = hero_sms.get_prices(base, key, service=service, country=country) + return data if data is not None else {} + + +def _collect_service_keys(prices) -> list: + """从 getPrices 全量返回中收集所有服务代号(用于 service is incorrect 时提示).""" + keys = [] + if isinstance(prices, dict) and prices.get("status") != "false": + for country_id, val in prices.items(): + if isinstance(val, dict): + for k, v in val.items(): + if isinstance(v, dict) and (v.get("count") is not None or v.get("cost") is not None): + keys.append(k) + return list(dict.fromkeys(keys)) + + +def _parse_prices_to_count(prices, service_name: str) -> tuple: + """从 getPrices 返回中解析指定服务的数量,兼容 list 或 dict。返回 (total_count, by_country).""" + total_count = 0 + by_country = [] + + def add_info(country_id, info): + nonlocal total_count + if not isinstance(info, dict): + return + c = info.get("count") or info.get("physicalCount") or 0 + try: + n = int(c) + except (TypeError, ValueError): + return + total_count += n + by_country.append({"country": country_id, "count": n, "cost": info.get("cost")}) + + if isinstance(prices, dict) and prices and set(prices.keys()) <= {"prices", "data", "result"}: + inner = prices.get("prices") or prices.get("data") or prices.get("result") + if inner is not None: + prices = inner + + if isinstance(prices, list): + for item in prices: + if not isinstance(item, dict): + continue + for country_id, val in item.items(): + if not isinstance(val, dict): + continue + if service_name in val: + add_info(country_id, val[service_name]) + else: + add_info(country_id, val) + elif isinstance(prices, dict): + if service_name in prices and isinstance(prices[service_name], dict): + for country_id, info in prices[service_name].items(): + add_info(country_id, info) + else: + for country_id, val in prices.items(): + if not isinstance(val, dict): + continue + if service_name in val: + add_info(country_id, val[service_name]) + else: + add_info(country_id, val) + return total_count, by_country + + +def _openai_availability_auth(request: Request): + """debug=1 时一律放行;否则要求 Authorization: Bearer 。""" + if request.query_params.get("debug") == "1": + return + auth = request.headers.get("Authorization") or "" + if not auth.startswith("Bearer "): + raise HTTPException(status_code=401, detail="Not authenticated") + token = auth[7:].strip() + if not token: + raise HTTPException(status_code=401, detail="Not authenticated") + try: + jwt.decode(token, settings.secret_key, algorithms=["HS256"]) + except JWTError: + raise HTTPException(status_code=401, detail="Invalid token") + + +@router.get("/openai-availability") +def api_openai_availability( + request: Request, + debug: int = Query(0, description="1 时返回 getPrices 原始数据,便于排查数量为 0"), +): + """OpenAI 可用数量汇总:余额 + 各国家库存。debug=1 时免登录访问。""" + _openai_availability_auth(request) + base, key, openai_service, _ = _get_sms_settings() + if not key: + raise HTTPException(status_code=400, detail="请先配置手机号接码 API KEY") + balance = hero_sms.get_balance(base, key) + prices = hero_sms.get_prices(base, key, service=openai_service) + service_hint = [] + if isinstance(prices, dict) and prices.get("status") == "false" and "incorrect" in (prices.get("msg") or ""): + full_prices = hero_sms.get_prices(base, key) + if isinstance(full_prices, dict) and full_prices.get("status") != "false": + service_hint = _collect_service_keys(full_prices) + elif isinstance(full_prices, list): + for item in full_prices: + if isinstance(item, dict): + for val in item.values(): + if isinstance(val, dict): + service_hint.extend(k for k in val if isinstance(val.get(k), dict)) + service_hint = list(dict.fromkeys(service_hint)) + total_count, by_country = _parse_prices_to_count(prices, openai_service) if prices and not service_hint else (0, []) + out = { + "balance": balance if balance is not None else 0, + "total_count": total_count, + "by_country": by_country, + } + if service_hint: + out["service_hint"] = service_hint + if debug: + out["prices_raw"] = prices + return out + + +class GetNumbersBody(BaseModel): + service: str = "openai" + country: int = 0 + quantity: int = 1 + + +@router.post("/get-numbers") +def api_get_numbers(body: GetNumbersBody, username: str = Depends(get_current_user)): + """从接码平台获取号码并写入手机号管理表(可绑定次数取自系统设置)""" + base, key, openai_service, max_price = _get_sms_settings() + if not key: + raise HTTPException(status_code=400, detail="请先配置手机号接码 API KEY") + quantity = max(1, min(body.quantity, 20)) + service = (body.service or "").strip() + if not service or service == "openai": + service = openai_service + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute("SELECT value FROM system_settings WHERE key = ?", ("phone_bind_limit",)) + row = c.fetchone() + limit = int(row[0]) if row and row[0] else 1 + got = [] + errors = [] + for _ in range(quantity): + result = hero_sms.get_number_auto(base, key, service, body.country, max_price=max_price) + if not result: + break + if result.get("error"): + errors.append(result["error"]) + break + expired_at = result.get("expired_at") + if not (expired_at and str(expired_at).strip()): + default_end = (datetime.utcnow() + timedelta(minutes=PHONE_DEFAULT_EXPIRE_MINUTES)).strftime("%Y-%m-%d %H:%M:%S") + expired_at = default_end + else: + raw = str(expired_at).strip() + if "T" in raw: + raw = raw.replace("Z", "").split(".")[0].replace("T", " ") + expired_at = raw + with get_db() as conn: + c = conn.cursor() + country_code = result.get("country") + remark = "Hero-SMS" + if country_code not in (None, "", 0): + remark = f"Hero-SMS(country={country_code})" + c.execute( + "INSERT INTO phone_numbers (phone, activation_id, max_use_count, remark, expired_at) VALUES (?, ?, ?, ?, ?)", + (result["phone_number"], result["activation_id"], limit, remark, expired_at), + ) + got.append({"id": c.lastrowid, "phone": result["phone_number"], "activation_id": result["activation_id"]}) + out = {"got": len(got), "items": got} + if errors: + out["errors"] = errors + return out diff --git a/Register_GPT_v0/web/backend/app/routers/sora_api.py b/Register_GPT_v0/web/backend/app/routers/sora_api.py new file mode 100644 index 0000000..aff9ea6 --- /dev/null +++ b/Register_GPT_v0/web/backend/app/routers/sora_api.py @@ -0,0 +1,2892 @@ +# -*- coding: utf-8 -*- +""" +Sora API 调用接口: +- rt -> at +- bootstrap +- me +- ensure activate +- 通用请求(限制 /backend/* 路径) +- 账号池自动轮换(额度耗尽自动切换下一个可用账号) +- API Key 请求自动注入去水印 header +""" +import json +import mimetypes +import time +import uuid +from typing import Any, Dict, Optional +from urllib.parse import unquote, urlencode + +from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile +from pydantic import BaseModel, Field +import requests + +from app.database import get_db, init_db +from app.registration_env import inject_registration_modules +from app.services.otp_resolver import build_otp_fetcher +from app.services.sora_api_key import ( + SORA_API_KEY_SCOPE_IMAGE, + SORA_API_KEY_SCOPE_TEXT, + get_sora_api_caller, + sora_api_key_scope_allows, + sora_api_key_scope_label, +) + +router = APIRouter(prefix="/api/sora-api", tags=["sora-api"]) + +_NF2_WEB_SESSION_CACHE: dict[int, dict] = {} +_NF2_WEB_SESSION_TTL_SECONDS = 20 * 60 +_TASK_FAMILY_VIDEO_GEN = "video_gen" +_TASK_FAMILY_NF2 = "nf2" + + +def _normalize_task_family(value: str) -> str: + normalized = (value or "").strip().lower() + if normalized in {"nf2", "sora_app", "sora_app_nf2"}: + return _TASK_FAMILY_NF2 + return _TASK_FAMILY_VIDEO_GEN + + +def _wants_legacy_text_video(value: str) -> bool: + normalized = (value or "").strip().lower() + return normalized in {"video_gen", "legacy", "old", "old_chain"} + + +def _close_nf2_web_session(web_session) -> None: + if web_session is None: + return + try: + web_session.close() + except Exception: + pass + + +def _drop_nf2_web_session(account_id: Optional[int]) -> None: + if account_id in (None, ""): + return + try: + key = int(account_id) + except Exception: + return + entry = _NF2_WEB_SESSION_CACHE.pop(key, None) + if isinstance(entry, dict): + _close_nf2_web_session(entry.get("web_session")) + + +def _store_nf2_web_session( + account_id: Optional[int], + web_session, + *, + access_token: str = "", + proxy_url: str = "", + web_origin: str = "", +) -> None: + if account_id in (None, "") or web_session is None: + return + try: + key = int(account_id) + except Exception: + return + previous = _NF2_WEB_SESSION_CACHE.get(key) + previous_session = previous.get("web_session") if isinstance(previous, dict) else None + if previous_session is not None and previous_session is not web_session: + _close_nf2_web_session(previous_session) + _NF2_WEB_SESSION_CACHE[key] = { + "web_session": web_session, + "access_token": (access_token or "").strip(), + "proxy_url": (proxy_url or "").strip(), + "web_origin": (web_origin or "").strip(), + "updated_at": time.time(), + } + + +def _get_nf2_web_session(account_id: Optional[int]) -> Optional[dict]: + if account_id in (None, ""): + return None + try: + key = int(account_id) + except Exception: + return None + entry = _NF2_WEB_SESSION_CACHE.get(key) + if not isinstance(entry, dict): + return None + updated_at = float(entry.get("updated_at") or 0.0) + if not updated_at or (time.time() - updated_at) > _NF2_WEB_SESSION_TTL_SECONDS: + _drop_nf2_web_session(key) + return None + if entry.get("web_session") is None: + _NF2_WEB_SESSION_CACHE.pop(key, None) + return None + return entry + + +def _touch_nf2_web_session(account_id: Optional[int], data: dict) -> None: + if account_id in (None, ""): + return + if not isinstance(data, dict): + return + web_session = data.get("web_session") + if web_session is None: + return + _store_nf2_web_session( + account_id, + web_session, + access_token=(data.get("access_token") or "").strip(), + proxy_url=(data.get("proxy_url") or "").strip(), + web_origin=(data.get("web_origin") or "").strip(), + ) + + +def _import_sora_phone(): + inject_registration_modules() + import protocol_sora_phone as sora_phone + return sora_phone + + +def _import_protocol_register(): + inject_registration_modules() + import protocol_register as pr + return pr + + +def _load_account(account_id: int) -> Optional[dict]: + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + """SELECT id, email, password, refresh_token, access_token, proxy, has_sora, + COALESCE(sora_enabled, 1) AS sora_enabled, + COALESCE(sora_quota_exhausted, 0) AS sora_quota_exhausted, + COALESCE(sora_quota_note, '') AS sora_quota_note, + COALESCE(sora_quota_updated_at, '') AS sora_quota_updated_at + FROM accounts WHERE id = ?""", + (account_id,), + ) + row = c.fetchone() + if not row: + return None + return { + "id": row[0], + "email": row[1] or "", + "password": row[2] or "", + "refresh_token": (row[3] or "").strip(), + "access_token": (row[4] or "").strip(), + "proxy": (row[5] or "").strip(), + "has_sora": bool(row[6]), + "sora_enabled": bool(row[7]), + "sora_quota_exhausted": bool(row[8]), + "sora_quota_note": row[9] or "", + "sora_quota_updated_at": row[10] or "", + } + + +def _load_email_mailbox(email: str) -> Optional[dict]: + account_email = (email or "").strip() + if not account_email: + return None + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + """SELECT email, password, uuid, token + FROM emails + WHERE LOWER(TRIM(email)) = LOWER(TRIM(?)) + LIMIT 1""", + (account_email,), + ) + row = c.fetchone() + if not row: + return None + return { + "email": row[0] or "", + "password": row[1] or "", + "uuid": row[2] or "", + "token": row[3] or "", + } + + +def _build_account_otp_fetcher(email: str): + mailbox = _load_email_mailbox(email) + if not mailbox: + return None + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + """SELECT key, value FROM system_settings + WHERE key IN ('email_api_url', 'email_api_key')""" + ) + rows = c.fetchall() + settings = {k: (v or "").strip() for k, v in rows} + base_url = (settings.get("email_api_url") or "https://gapi.hotmail007.com").rstrip("/") + client_key = settings.get("email_api_key") or "" + if not client_key: + return None + account_str = f"{mailbox['email']}:{mailbox['password']}:{mailbox['token']}:{mailbox['uuid']}" + return build_otp_fetcher(base_url, client_key, account_str, timeout_sec=120, interval_sec=5) + + +def _pick_next_available_account(exclude_ids: list = None) -> dict | None: + """Round-robin 从可用 Sora 账号池中挑选下一个账号。 + 返回 account dict 或 None(无可用账号)。 + """ + exclude = set(exclude_ids or []) + init_db() + with get_db() as conn: + c = conn.cursor() + # 读取上次使用的游标 + c.execute("SELECT value FROM system_settings WHERE key = 'sora_auto_rotate_cursor'") + row = c.fetchone() + try: + cursor = int((row[0] if row else "0") or "0") + except Exception: + cursor = 0 + + c.execute( + """SELECT id, email, refresh_token, access_token, proxy, has_sora, + COALESCE(sora_enabled, 1) AS sora_enabled, + COALESCE(sora_quota_exhausted, 0) AS sora_quota_exhausted, + COALESCE(sora_quota_note, '') AS sora_quota_note, + COALESCE(sora_quota_updated_at, '') AS sora_quota_updated_at + FROM accounts + WHERE has_sora = 1 + AND COALESCE(sora_enabled, 1) = 1 + AND COALESCE(sora_quota_exhausted, 0) = 0 + AND (COALESCE(refresh_token, '') != '' OR COALESCE(access_token, '') != '') + ORDER BY id ASC""" + ) + rows = c.fetchall() + if not rows: + return None + + # 从 cursor 之后的第一个可用账号开始 + pick = None + for r in rows: + if int(r[0]) > cursor and int(r[0]) not in exclude: + pick = r + break + # 没找到则回绕到最早的可用账号 + if pick is None: + for r in rows: + if int(r[0]) not in exclude: + pick = r + break + if pick is None: + return None + + # 更新游标 + c.execute( + "INSERT OR REPLACE INTO system_settings (key, value) VALUES (?, ?)", + ("sora_auto_rotate_cursor", str(int(pick[0]))), + ) + + return { + "id": pick[0], + "email": pick[1] or "", + "refresh_token": (pick[2] or "").strip(), + "access_token": (pick[3] or "").strip(), + "proxy": (pick[4] or "").strip(), + "has_sora": bool(pick[5]), + "sora_enabled": bool(pick[6]), + "sora_quota_exhausted": bool(pick[7]), + "sora_quota_note": pick[8] or "", + "sora_quota_updated_at": pick[9] or "", + } + + +def _save_account_tokens(account_id: int, access_token: str = "", refresh_token: str = "") -> None: + init_db() + with get_db() as conn: + c = conn.cursor() + if access_token: + c.execute("UPDATE accounts SET access_token = ? WHERE id = ?", (access_token, account_id)) + if refresh_token: + c.execute("UPDATE accounts SET refresh_token = ? WHERE id = ?", (refresh_token, account_id)) + + +def _mark_account_sora(account_id: int) -> None: + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + """UPDATE accounts + SET has_sora = 1, + status = CASE + WHEN COALESCE(status, '') = 'Registered' THEN 'Registered+Sora' + ELSE status + END + WHERE id = ?""", + (account_id,), + ) + + +def _mark_account_quota_exhausted(account_id: int, note: str = "") -> None: + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + """UPDATE accounts + SET sora_quota_exhausted = 1, + sora_quota_note = ?, + sora_last_error = ?, + sora_quota_updated_at = datetime('now') + WHERE id = ?""", + ((note or "quota_exceeded").strip(), (note or "quota_exceeded").strip(), account_id), + ) + + +def _clear_account_quota_exhausted(account_id: int) -> None: + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + """UPDATE accounts + SET sora_quota_exhausted = 0, + sora_quota_note = '', + sora_last_error = '', + sora_quota_updated_at = datetime('now') + WHERE id = ?""", + (account_id,), + ) + + +def _mark_account_last_error(account_id: int, message: str = "") -> None: + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + """UPDATE accounts + SET sora_last_error = ?, + sora_quota_updated_at = datetime('now') + WHERE id = ?""", + ((message or "").strip()[:500], account_id), + ) + + +def _remember_media_asset( + media_id: str, + account_id: int, + payload: Optional[dict] = None, + api_key_id: Optional[int] = None, +) -> None: + asset_id = (media_id or "").strip() + if not asset_id or not account_id: + return + data = payload or {} + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + """INSERT INTO sora_media_assets + (media_id, account_id, api_key_id, media_type, filename, mime_type, width, height, source_url, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now')) + ON CONFLICT(media_id) DO UPDATE SET + account_id = excluded.account_id, + api_key_id = COALESCE(excluded.api_key_id, sora_media_assets.api_key_id), + media_type = COALESCE(excluded.media_type, sora_media_assets.media_type), + filename = COALESCE(excluded.filename, sora_media_assets.filename), + mime_type = COALESCE(excluded.mime_type, sora_media_assets.mime_type), + width = COALESCE(excluded.width, sora_media_assets.width), + height = COALESCE(excluded.height, sora_media_assets.height), + source_url = COALESCE(excluded.source_url, sora_media_assets.source_url), + updated_at = datetime('now')""", + ( + asset_id, + int(account_id), + int(api_key_id) if api_key_id is not None else None, + (data.get("type") or "").strip() or None, + (data.get("filename") or "").strip() or None, + (data.get("mime_type") or "").strip() or None, + int(data.get("width")) if str(data.get("width") or "").isdigit() else None, + int(data.get("height")) if str(data.get("height") or "").isdigit() else None, + (data.get("url") or "").strip() or None, + ), + ) + + +def _load_media_asset(media_id: str) -> Optional[dict]: + asset_id = (media_id or "").strip() + if not asset_id: + return None + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + """SELECT media_id, account_id, api_key_id, media_type, filename, mime_type, width, height, source_url, created_at, updated_at + FROM sora_media_assets + WHERE media_id = ? + LIMIT 1""", + (asset_id,), + ) + row = c.fetchone() + if not row: + return None + return { + "media_id": row[0] or "", + "account_id": int(row[1] or 0), + "api_key_id": row[2], + "media_type": row[3] or "", + "filename": row[4] or "", + "mime_type": row[5] or "", + "width": row[6], + "height": row[7], + "url": row[8] or "", + "created_at": row[9] or "", + "updated_at": row[10] or "", + } + + +def _remember_video_task( + task_id: str, + account_id: int, + api_key_id: Optional[int] = None, + task_family: str = _TASK_FAMILY_VIDEO_GEN, + raw_status: str = "", + normalized_status: str = "", + is_active: Optional[bool] = True, +) -> None: + task = (task_id or "").strip() + if not task or not account_id: + return + raw_value = (raw_status or "").strip() or None + normalized_value = _normalize_video_status(normalized_status or raw_status) or None + task_family_value = _normalize_task_family(task_family) + active_value = None if is_active is None else (1 if is_active else 0) + succeeded_value = "now" if normalized_value == "succeeded" else "" + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + """INSERT INTO sora_video_tasks (task_id, account_id, api_key_id, task_family, raw_status, normalized_status, is_active, lease_expires_at, succeeded_at, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, NULL, CASE WHEN ? = 'now' THEN datetime('now') ELSE NULL END, datetime('now'), datetime('now')) + ON CONFLICT(task_id) DO UPDATE SET + account_id = excluded.account_id, + api_key_id = COALESCE(excluded.api_key_id, sora_video_tasks.api_key_id), + task_family = COALESCE(excluded.task_family, sora_video_tasks.task_family), + raw_status = COALESCE(excluded.raw_status, sora_video_tasks.raw_status), + normalized_status = COALESCE(excluded.normalized_status, sora_video_tasks.normalized_status), + is_active = COALESCE(excluded.is_active, sora_video_tasks.is_active), + lease_expires_at = NULL, + succeeded_at = CASE + WHEN sora_video_tasks.succeeded_at IS NOT NULL THEN sora_video_tasks.succeeded_at + WHEN COALESCE(excluded.normalized_status, sora_video_tasks.normalized_status) = 'succeeded' THEN datetime('now') + ELSE sora_video_tasks.succeeded_at + END, + updated_at = datetime('now')""", + ( + task, + int(account_id), + int(api_key_id) if api_key_id is not None else None, + task_family_value, + raw_value, + normalized_value, + active_value, + succeeded_value, + ), + ) + + +def _claim_reserved_video_task( + reservation_task_id: str, + task_id: str, + account_id: int, + api_key_id: Optional[int] = None, + task_family: str = _TASK_FAMILY_VIDEO_GEN, + raw_status: str = "", + normalized_status: str = "", + is_active: Optional[bool] = True, +) -> None: + reservation = (reservation_task_id or "").strip() + task = (task_id or "").strip() + if not task or not account_id: + return + raw_value = (raw_status or "").strip() or None + normalized_value = _normalize_video_status(normalized_status or raw_status) or None + task_family_value = _normalize_task_family(task_family) + active_value = None if is_active is None else (1 if is_active else 0) + succeeded_value = "now" if normalized_value == "succeeded" else "" + init_db() + with get_db() as conn: + c = conn.cursor() + if reservation: + c.execute("DELETE FROM sora_video_tasks WHERE task_id = ?", (reservation,)) + c.execute( + """INSERT INTO sora_video_tasks (task_id, account_id, api_key_id, task_family, raw_status, normalized_status, is_active, lease_expires_at, succeeded_at, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, NULL, CASE WHEN ? = 'now' THEN datetime('now') ELSE NULL END, datetime('now'), datetime('now')) + ON CONFLICT(task_id) DO UPDATE SET + account_id = excluded.account_id, + api_key_id = COALESCE(excluded.api_key_id, sora_video_tasks.api_key_id), + task_family = COALESCE(excluded.task_family, sora_video_tasks.task_family), + raw_status = COALESCE(excluded.raw_status, sora_video_tasks.raw_status), + normalized_status = COALESCE(excluded.normalized_status, sora_video_tasks.normalized_status), + is_active = COALESCE(excluded.is_active, sora_video_tasks.is_active), + lease_expires_at = NULL, + succeeded_at = CASE + WHEN sora_video_tasks.succeeded_at IS NOT NULL THEN sora_video_tasks.succeeded_at + WHEN COALESCE(excluded.normalized_status, sora_video_tasks.normalized_status) = 'succeeded' THEN datetime('now') + ELSE sora_video_tasks.succeeded_at + END, + updated_at = datetime('now')""", + ( + task, + int(account_id), + int(api_key_id) if api_key_id is not None else None, + task_family_value, + raw_value, + normalized_value, + active_value, + succeeded_value, + ), + ) + + +def _release_video_task_reservation(reservation_task_id: str) -> None: + reservation = (reservation_task_id or "").strip() + if not reservation: + return + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute("DELETE FROM sora_video_tasks WHERE task_id = ?", (reservation,)) + + +def _sync_video_task_result( + task_id: str, + account_id: int, + result: Optional[dict], + api_key_id: Optional[int] = None, + default_active: Optional[bool] = None, + task_family: str = "", +) -> None: + task = (task_id or "").strip() + if not task or not account_id: + return + raw_status = ((result or {}).get("status") or "").strip() + normalized_status = _normalize_video_status((result or {}).get("normalized_status") or raw_status) + if normalized_status: + is_active = normalized_status not in _VIDEO_TERMINAL_STATUSES + else: + is_active = default_active + _remember_video_task( + task, + account_id, + api_key_id=api_key_id, + task_family=task_family or (result or {}).get("task_family") or _TASK_FAMILY_VIDEO_GEN, + raw_status=raw_status, + normalized_status=normalized_status, + is_active=is_active, + ) + + +def _reserve_pool_video_account( + api_key_id: Optional[int] = None, + exclude_ids: list = None, + task_family: str = _TASK_FAMILY_VIDEO_GEN, +) -> Optional[dict]: + exclude = {int(x) for x in (exclude_ids or []) if int(x)} + task_family_value = _normalize_task_family(task_family) + init_db() + with get_db() as conn: + conn.execute("BEGIN IMMEDIATE") + c = conn.cursor() + c.execute( + "DELETE FROM sora_video_tasks WHERE task_id LIKE ? AND lease_expires_at IS NOT NULL AND lease_expires_at <= datetime('now')", + (f"{_VIDEO_RESERVATION_PREFIX}%",), + ) + c.execute("SELECT value FROM system_settings WHERE key = 'sora_auto_rotate_cursor'") + row = c.fetchone() + try: + cursor = int((row[0] if row else "0") or "0") + except Exception: + cursor = 0 + c.execute( + """SELECT a.id, a.email, a.refresh_token, a.access_token, a.proxy, a.has_sora, + COALESCE(a.sora_enabled, 1) AS sora_enabled, + COALESCE(a.sora_quota_exhausted, 0) AS sora_quota_exhausted, + COALESCE(a.sora_quota_note, '') AS sora_quota_note, + COALESCE(a.sora_quota_updated_at, '') AS sora_quota_updated_at, + COALESCE(t.active_count, 0) AS active_count + FROM accounts a + LEFT JOIN ( + SELECT account_id, COUNT(*) AS active_count + FROM sora_video_tasks + WHERE is_active = 1 + AND (lease_expires_at IS NULL OR lease_expires_at > datetime('now')) + GROUP BY account_id + ) t ON t.account_id = a.id + WHERE a.has_sora = 1 + AND COALESCE(a.sora_enabled, 1) = 1 + AND COALESCE(a.sora_quota_exhausted, 0) = 0 + AND (COALESCE(a.refresh_token, '') != '' OR COALESCE(a.access_token, '') != '') + ORDER BY a.id ASC""" + ) + rows = [r for r in c.fetchall() if int(r["id"]) not in exclude] + if not rows: + return None + min_active = min(int(r["active_count"] or 0) for r in rows) + candidates = [r for r in rows if int(r["active_count"] or 0) == min_active] + pick = None + for r in candidates: + if int(r["id"]) > cursor: + pick = r + break + if pick is None: + pick = candidates[0] + c.execute( + "INSERT OR REPLACE INTO system_settings (key, value) VALUES (?, ?)", + ("sora_auto_rotate_cursor", str(int(pick["id"]))), + ) + reservation_task_id = f"{_VIDEO_RESERVATION_PREFIX}{uuid.uuid4().hex}" + c.execute( + """INSERT INTO sora_video_tasks + (task_id, account_id, api_key_id, task_family, raw_status, normalized_status, is_active, lease_expires_at, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, 1, datetime('now', ?), datetime('now'), datetime('now'))""", + ( + reservation_task_id, + int(pick["id"]), + int(api_key_id) if api_key_id is not None else None, + task_family_value, + "reserving", + "running", + f"+{_VIDEO_RESERVATION_SECONDS} seconds", + ), + ) + return { + "id": int(pick["id"]), + "email": pick["email"] or "", + "refresh_token": (pick["refresh_token"] or "").strip(), + "access_token": (pick["access_token"] or "").strip(), + "proxy": (pick["proxy"] or "").strip(), + "has_sora": bool(pick["has_sora"]), + "sora_enabled": bool(pick["sora_enabled"]), + "sora_quota_exhausted": bool(pick["sora_quota_exhausted"]), + "sora_quota_note": pick["sora_quota_note"] or "", + "sora_quota_updated_at": pick["sora_quota_updated_at"] or "", + "active_task_count": int(pick["active_count"] or 0) + 1, + "reservation_task_id": reservation_task_id, + } + + +def _lookup_video_task_meta(task_id: str) -> Optional[dict]: + task = (task_id or "").strip() + if not task: + return None + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + "SELECT account_id, COALESCE(task_family, ?) FROM sora_video_tasks WHERE task_id = ? LIMIT 1", + (_TASK_FAMILY_VIDEO_GEN, task), + ) + row = c.fetchone() + if not row: + return None + try: + account_id = int(row[0]) + except Exception: + account_id = None + return { + "account_id": account_id, + "task_family": _normalize_task_family(row[1] or _TASK_FAMILY_VIDEO_GEN), + } + + +def _lookup_video_task_account(task_id: str) -> Optional[int]: + meta = _lookup_video_task_meta(task_id) + if not meta: + return None + try: + return int(meta.get("account_id")) + except Exception: + return None + + +def _extract_quota_reason(status_code: int, payload: Any, raw_text: str = "") -> str: + code = "" + parts = [] + if raw_text: + parts.append(raw_text) + if payload is not None: + try: + parts.append(json.dumps(payload, ensure_ascii=False)) + except Exception: + parts.append(str(payload)) + if isinstance(payload, dict): + err = payload.get("error") or {} + if isinstance(err, dict): + code = (err.get("code") or "").strip().lower() + merged = " ".join(parts).lower() + if code == "too_many_concurrent_tasks": + return "" + if not merged and status_code not in (402, 429): + return "" + + keywords = [ + "insufficient_quota", + "quota_exceeded", + "billing_hard_limit_reached", + "out of credits", + "insufficient credits", + "rate_limit_exceeded", + "usage limit", + "credit balance", + ] + if code in keywords: + return code + if status_code == 402: + return f"http_{status_code}" + if status_code == 429 and not any(k in merged for k in keywords): + return "" + for k in keywords: + if k in merged: + return k + return "" + + +def _extract_sora_error_code(payload: Any) -> str: + if not isinstance(payload, dict): + return "" + error = payload.get("error") or {} + if isinstance(error, dict): + return (error.get("code") or "").strip().lower() + return "" + + +def _is_too_many_concurrent_tasks_result(result: Optional[dict]) -> bool: + payload = (result or {}).get("data") + code = _extract_sora_error_code(payload) + if code == "too_many_concurrent_tasks": + return True + text = "" + try: + text = json.dumps(payload, ensure_ascii=False).lower() + except Exception: + text = str(payload or "").lower() + return "too_many_concurrent_tasks" in text + + +def _extract_busy_reason(payload: Any, raw_text: str = "") -> str: + code = "" + message = "" + if isinstance(payload, dict): + err = payload.get("error") or {} + if isinstance(err, dict): + code = (err.get("code") or "").strip().lower() + message = (err.get("message") or "").strip().lower() + merged = " ".join( + part for part in [raw_text.lower() if raw_text else "", message] if part + ) + if code == "too_many_concurrent_tasks": + return code + if "generations in progress" in merged: + return "too_many_concurrent_tasks" + return "" + + +class SoraUpstreamTransportError(RuntimeError): + pass + + +_TRANSPORT_ERROR_HINTS = ( + "connect tunnel failed", + "proxyerror", + "proxy error", + "connection refused", + "connection reset", + "failed to connect", + "curl: (7)", + "curl: (35)", + "curl: (56)", +) +_TRANSPORT_RETRY_COUNT = 2 + + +def _extract_transport_error_message(exc: Exception) -> str: + if exc is None: + return "" + message = (str(exc) or exc.__class__.__name__ or "").strip() + if not message: + return "" + lowered = message.lower() + if isinstance(exc, (requests.exceptions.ProxyError, requests.exceptions.ConnectionError)): + return message[:400] + if any(token in lowered for token in _TRANSPORT_ERROR_HINTS): + return message[:400] + module_name = (exc.__class__.__module__ or "").lower() + class_name = (exc.__class__.__name__ or "").lower() + if "curl_cffi" in module_name and class_name in {"proxyerror", "connectionerror"}: + return message[:400] + return "" + + +def _raise_transport_http_error(detail: str, all_accounts: bool = False) -> None: + prefix = "所有可用账号代理或网络异常" if all_accounts else "Sora 上游代理或网络异常" + suffix = f":{detail}" if detail else "" + raise HTTPException(status_code=503, detail=f"{prefix}{suffix}") + + +def _run_transport_safe_request(fn): + last_detail = "" + for attempt in range(max(1, _TRANSPORT_RETRY_COUNT + 1)): + try: + return fn() + except Exception as exc: + detail = _extract_transport_error_message(exc) + if not detail: + raise + last_detail = detail + if attempt >= _TRANSPORT_RETRY_COUNT: + raise SoraUpstreamTransportError(last_detail) from exc + time.sleep(0.35 * attempt) + raise SoraUpstreamTransportError(last_detail or "unknown transport error") + + +class SoraTokenBody(BaseModel): + account_id: Optional[int] = None + access_token: str = "" + refresh_token: str = "" + proxy_url: str = "" + + +class SoraRequestBody(SoraTokenBody): + method: str = "GET" + path: str = "/backend/me" + payload: Dict[str, Any] = Field(default_factory=dict) + + +class SoraVideoGenCreateBody(SoraTokenBody): + prompt: str + auto_rotate: bool = False + task_family: str = "" + operation: str = "simple_compose" + n_variants: int = 4 + n_frames: int = 300 + resolution: int = 360 + orientation: str = "wide" + model: str = "" + style_id: str = "" + audio_caption: str = "" + audio_transcript: str = "" + video_caption: str = "" + seed: Optional[int] = None + source_image_media_id: str = "" + extra_payload: Dict[str, Any] = Field(default_factory=dict) + + +class SoraVideoTaskBody(SoraTokenBody): + task_id: str + + +class SoraVideoListBody(SoraTokenBody): + limit: int = 20 + last_id: str = "" + task_type_filter: str = "videos" + + +class SoraVideoGenCreateAndWaitBody(SoraVideoGenCreateBody): + poll_interval_seconds: float = Field(default=5.0, ge=1.0, le=60.0) + timeout_seconds: int = Field(default=900, ge=5, le=7200) + + +class SoraVideoGenNfCreateBody(SoraTokenBody): + prompt: str + auto_rotate: bool = False + n_variants: int = 1 + n_frames: int = 300 + resolution: int = 360 + orientation: str = "portrait" + model: str = "sy_8" + style_id: str = "" + audio_caption: str = "" + audio_transcript: str = "" + video_caption: str = "" + seed: Optional[int] = None + extra_payload: Dict[str, Any] = Field(default_factory=dict) + + +class SoraDraftBody(SoraTokenBody): + draft_id: str + + +class SoraStitchBody(SoraTokenBody): + generation_ids: list[str] = Field(default_factory=list) + for_download: bool = False + + +_VIDEO_SUCCESS_STATUSES = {"succeeded"} +_VIDEO_FAILURE_STATUSES = {"failed", "cancelled", "rejected", "expired", "error"} +_VIDEO_TERMINAL_STATUSES = _VIDEO_SUCCESS_STATUSES | _VIDEO_FAILURE_STATUSES +_VIDEO_RETRYABLE_POLL_STATUS_CODES = {404, 409, 425} +_VIDEO_RESERVATION_PREFIX = "lease_" +_VIDEO_RESERVATION_SECONDS = 180 +_VIDEO_URL_KEYS = { + "url", + "src", + "uri", + "download_url", + "downloadurl", + "signed_url", + "signedurl", + "stream_url", + "streamurl", + "video_url", + "videourl", + "playback_url", + "playbackurl", +} +_VIDEO_URL_EXTENSIONS = (".mp4", ".mov", ".webm", ".m3u8") + + +def _normalize_video_status(status: str) -> str: + value = (status or "").strip().lower() + if not value: + return "" + aliases = { + "complete": "succeeded", + "completed": "succeeded", + "done": "succeeded", + "success": "succeeded", + "succeed": "succeeded", + "succeeded": "succeeded", + "canceled": "cancelled", + "cancelled": "cancelled", + "in_progress": "running", + "inprogress": "running", + "processing": "running", + } + return aliases.get(value, value) + + +def _find_string_field(payload: Any, keys: tuple[str, ...], depth: int = 0) -> str: + if depth > 6: + return "" + if isinstance(payload, dict): + for key in keys: + value = payload.get(key) + if isinstance(value, str) and value.strip(): + return value.strip() + for value in payload.values(): + found = _find_string_field(value, keys, depth + 1) + if found: + return found + elif isinstance(payload, list): + for item in payload[:50]: + found = _find_string_field(item, keys, depth + 1) + if found: + return found + return "" + + +def _video_url_priority(url: str) -> tuple[int, int, int, int]: + value = (url or "").strip() + lowered = value.lower() + decoded = unquote(lowered) + base = decoded.split("?", 1)[0] + manifest_penalty = 1 if base.endswith(".m3u8") else 0 + extension_rank = 0 + if base.endswith(".mov"): + extension_rank = 1 + elif base.endswith(".webm"): + extension_rank = 2 + elif base.endswith(".m3u8"): + extension_rank = 3 + quality_rank = 50 + quality_checks = ( + (0, ("no_watermark", "downloadable", "/src.mp4", "/source.mp4", "/source_wm.mp4", "original")), + (1, ("/hd.mp4", "_hd.mp4", "/high.mp4", "_high.mp4")), + (2, ("/md.mp4", "_md.mp4", "/medium.mp4", "_medium.mp4")), + (3, ("/ld.mp4", "_ld.mp4", "/low.mp4", "_low.mp4")), + (4, ("watermark", "_wm.mp4", "/wm.mp4")), + ) + for rank, needles in quality_checks: + if any(needle in decoded for needle in needles): + quality_rank = rank + break + watermark_penalty = 0 + if ( + "watermark" in decoded + or "_wm." in base + or "/wm." in base + or "/wm/" in base + ) and "no_watermark" not in decoded: + watermark_penalty = 1 + return (manifest_penalty, quality_rank, watermark_penalty, extension_rank) + + +def _merge_video_urls(*groups: Any) -> list[str]: + ranked: list[tuple[tuple[int, int, int, int], int, str]] = [] + seen: set[str] = set() + sequence = 0 + for group in groups: + if isinstance(group, str): + candidates = [group] + elif isinstance(group, (list, tuple, set)): + candidates = list(group) + else: + candidates = [] + for item in candidates: + if not isinstance(item, str): + continue + value = item.strip() + if not value or value in seen: + continue + seen.add(value) + ranked.append((_video_url_priority(value), sequence, value)) + sequence += 1 + ranked.sort(key=lambda item: (item[0], item[1])) + return [item[2] for item in ranked] + + +def _collect_video_urls(payload: Any, depth: int = 0, urls: Optional[list[str]] = None) -> list[str]: + if urls is None: + urls = [] + if depth > 6: + return urls + if isinstance(payload, dict): + for key, value in payload.items(): + lowered_key = str(key or "").strip().lower() + if isinstance(value, str): + candidate = value.strip() + lower_candidate = candidate.lower() + if candidate.startswith(("http://", "https://")): + base_url = lower_candidate.split("?", 1)[0] + if lowered_key in _VIDEO_URL_KEYS or base_url.endswith(_VIDEO_URL_EXTENSIONS): + if candidate not in urls: + urls.append(candidate) + elif isinstance(value, (dict, list)): + _collect_video_urls(value, depth + 1, urls) + elif isinstance(payload, list): + for item in payload[:50]: + _collect_video_urls(item, depth + 1, urls) + return urls + + +def _decorate_video_task_result(result: dict, task_id: str = "") -> dict: + payload = result.get("data") + raw_status = _find_string_field(payload, ("status", "state")) + normalized_status = _normalize_video_status(raw_status) + resolved_task_id = (task_id or _find_string_field(payload, ("task_id", "id"))).strip() + video_urls = _merge_video_urls(_collect_video_urls(payload)) + return { + **result, + "task_family": _TASK_FAMILY_VIDEO_GEN, + "task_id": resolved_task_id, + "status": raw_status, + "normalized_status": normalized_status, + "is_terminal": normalized_status in _VIDEO_TERMINAL_STATUSES, + "is_success": normalized_status in _VIDEO_SUCCESS_STATUSES, + "video_urls": video_urls, + } + + +def _find_dict_matching(payload: Any, predicate, depth: int = 0): + if depth > 8: + return None + if isinstance(payload, dict): + try: + if predicate(payload): + return payload + except Exception: + pass + for value in payload.values(): + found = _find_dict_matching(value, predicate, depth + 1) + if found is not None: + return found + elif isinstance(payload, list): + for item in payload[:80]: + found = _find_dict_matching(item, predicate, depth + 1) + if found is not None: + return found + return None + + +def _extract_nf2_task_id(payload: Any) -> str: + if isinstance(payload, dict): + task = payload.get("task") + if isinstance(task, dict): + task_id = (task.get("id") or task.get("task_id") or "").strip() + if task_id: + return task_id + top_id = (payload.get("task_id") or "").strip() + if top_id: + return top_id + kind = (payload.get("kind") or "").strip().lower() + status = (payload.get("status") or payload.get("state") or "").strip() + top_level_id = (payload.get("id") or "").strip() + if top_level_id and status and kind != "sora_draft": + return top_level_id + nested = _find_dict_matching( + payload, + lambda item: isinstance(item.get("id"), str) + and isinstance(item.get("status"), str) + and (item.get("kind") or "").strip().lower() != "sora_draft", + ) + if isinstance(nested, dict): + return (nested.get("id") or "").strip() + return _find_string_field(payload, ("task_id",)) + + +def _extract_nf2_draft_id(payload: Any) -> str: + if isinstance(payload, dict): + draft = payload.get("draft") + if isinstance(draft, dict): + draft_id = (draft.get("id") or "").strip() + if draft_id: + return draft_id + kind = (payload.get("kind") or "").strip().lower() + top_level_id = (payload.get("id") or "").strip() + if kind == "sora_draft" and top_level_id: + return top_level_id + nested = _find_dict_matching( + payload, + lambda item: ((item.get("kind") or "").strip().lower() == "sora_draft") and isinstance(item.get("id"), str), + ) + if isinstance(nested, dict): + return (nested.get("id") or "").strip() + return "" + + +def _extract_nf2_download_urls(payload: Any) -> dict: + watermark_url = _find_string_field(payload, ("watermark",)) + no_watermark_url = _find_string_field(payload, ("no_watermark",)) + downloadable_url = _find_string_field(payload, ("downloadable_url",)) + urls = _merge_video_urls( + [no_watermark_url, downloadable_url, watermark_url], + _collect_video_urls(payload), + ) + return { + "watermark_url": watermark_url, + "no_watermark_url": no_watermark_url, + "downloadable_url": downloadable_url, + "media_urls": urls, + "video_urls": urls, + } + + +def _decorate_nf2_result(result: dict, task_id: str = "") -> dict: + payload = result.get("data") + raw_status = _find_string_field(payload, ("status", "state")) + normalized_status = _normalize_video_status(raw_status) + resolved_task_id = (task_id or _extract_nf2_task_id(payload)).strip() + draft_id = _extract_nf2_draft_id(payload) + download_info = _extract_nf2_download_urls(payload) + return { + **result, + "task_family": _TASK_FAMILY_NF2, + "task_id": resolved_task_id, + "draft_id": draft_id, + "status": raw_status, + "normalized_status": normalized_status, + "is_terminal": normalized_status in _VIDEO_TERMINAL_STATUSES, + "is_success": normalized_status in _VIDEO_SUCCESS_STATUSES, + **download_info, + } + + +def _merge_nf2_lookup_result(task_result: dict, draft_result: dict) -> dict: + merged = {**task_result} + for key in ("draft_id", "no_watermark_url", "downloadable_url", "watermark_url"): + if draft_result.get(key): + merged[key] = draft_result.get(key) + merged_urls = _merge_video_urls( + [ + draft_result.get("no_watermark_url"), + draft_result.get("downloadable_url"), + draft_result.get("watermark_url"), + ], + draft_result.get("video_urls") or [], + [ + task_result.get("no_watermark_url"), + task_result.get("downloadable_url"), + task_result.get("watermark_url"), + ], + task_result.get("video_urls") or [], + ) + if merged_urls: + merged["video_urls"] = merged_urls + merged["media_urls"] = merged_urls + return merged + + +def _is_pool_api_key_caller(caller: dict) -> bool: + return (caller.get("auth_type") or "") == "api_key" and caller.get("account_id") is None + + +def _require_api_key_video_scope(caller: dict, capability: str) -> None: + if (caller.get("auth_type") or "") != "api_key": + return + scope = caller.get("api_key_scope") or SORA_API_KEY_SCOPE_TEXT + if sora_api_key_scope_allows(scope, capability): + return + target = "文生视频" if capability == SORA_API_KEY_SCOPE_TEXT else "图生视频" + raise HTTPException( + status_code=403, + detail=f"当前 API Key 类型是「{sora_api_key_scope_label(scope)}」,不能调用{target}接口", + ) + + +def _require_api_key_any_video_scope(caller: dict) -> None: + if (caller.get("auth_type") or "") != "api_key": + return + scope = caller.get("api_key_scope") or SORA_API_KEY_SCOPE_TEXT + if scope in (SORA_API_KEY_SCOPE_TEXT, SORA_API_KEY_SCOPE_IMAGE) or sora_api_key_scope_allows(scope, SORA_API_KEY_SCOPE_TEXT): + return + raise HTTPException(status_code=403, detail="当前 API Key 不能调用视频接口") + + +def _payload_is_image_to_video(payload: Any) -> bool: + if not isinstance(payload, dict): + return False + media_id = (payload.get("source_image_media_id") or "").strip() + if media_id: + return True + if bool(payload.get("is_storyboard")) and isinstance(payload.get("inpaint_items"), list) and payload.get("inpaint_items"): + return True + for item in payload.get("inpaint_items") or []: + if not isinstance(item, dict): + continue + if (item.get("upload_media_id") or item.get("uploaded_file_id") or item.get("generation_id") or "").strip(): + return True + return False + + +def _resolve_tokens( + body: SoraTokenBody, + allow_refresh: bool = True, + prefer_refresh_token_for_sora: bool = False, + default_account_id: Optional[int] = None, + locked_account_id: Optional[int] = None, + allow_direct_tokens: bool = True, + allow_pool_rotation: bool = False, + exclude_account_ids: list = None, +) -> dict: + account = None + request_account_id = body.account_id + if locked_account_id is not None: + if request_account_id is not None and int(request_account_id) != int(locked_account_id): + raise HTTPException(status_code=403, detail="API Key 仅允许访问绑定账号") + request_account_id = locked_account_id + elif request_account_id is None: + request_account_id = default_account_id + + access_token = (body.access_token or "").strip() if allow_direct_tokens else "" + refresh_token = (body.refresh_token or "").strip() if allow_direct_tokens else "" + proxy_url = (body.proxy_url or "").strip() if allow_direct_tokens else "" + + # 池模式自动选账号 + if request_account_id is None and allow_pool_rotation and not access_token and not refresh_token: + picked = _pick_next_available_account(exclude_ids=exclude_account_ids) + if not picked: + raise HTTPException(status_code=404, detail="账号池中无可用 Sora 账号(请检查 token/额度/启停状态)") + account = picked + request_account_id = picked["id"] + access_token = picked["access_token"] + refresh_token = picked["refresh_token"] + proxy_url = picked["proxy"] + elif request_account_id is not None: + account = _load_account(int(request_account_id)) + if not account: + raise HTTPException(status_code=404, detail="账号不存在") + if not account["sora_enabled"]: + raise HTTPException(status_code=403, detail="该账号已停用,请在账号管理中启用后再调用") + if account["sora_quota_exhausted"]: + # 池模式下遇到额度不足不直接报错,而是跳过该账号 + if allow_pool_rotation: + excl = list(exclude_account_ids or []) + [int(request_account_id)] + picked = _pick_next_available_account(exclude_ids=excl) + if not picked: + raise HTTPException(status_code=429, detail="所有账号额度已耗尽,请添加新账号或重置额度") + account = picked + request_account_id = picked["id"] + access_token = picked["access_token"] + refresh_token = picked["refresh_token"] + proxy_url = picked["proxy"] + else: + note = account["sora_quota_note"] or "quota_exceeded" + when = account["sora_quota_updated_at"] or "" + suffix = f"({when})" if when else "" + raise HTTPException(status_code=429, detail=f"该账号已标记额度不足{suffix}:{note},请切换账号或重置额度状态") + if not access_token: + access_token = account["access_token"] + if not refresh_token: + refresh_token = account["refresh_token"] + if not proxy_url: + proxy_url = account["proxy"] + + if refresh_token and allow_refresh and (prefer_refresh_token_for_sora or not access_token): + sora_phone = _import_sora_phone() + out = sora_phone.rt_to_at_mobile(refresh_token, proxy_url=proxy_url) + new_access_token = (out.get("access_token") or "").strip() + new_rt = (out.get("refresh_token") or "").strip() + if new_access_token: + access_token = new_access_token + if new_rt: + refresh_token = new_rt + if request_account_id is not None and (new_access_token or new_rt): + _save_account_tokens(int(request_account_id), access_token=new_access_token, refresh_token=new_rt) + + return { + "account": account, + "access_token": access_token, + "refresh_token": refresh_token, + "proxy_url": proxy_url, + } + + +def _ensure_nf2_access_token( + data: dict, + account_id: Optional[int] = None, + force_web_login: bool = False, +) -> dict: + sora_phone = _import_sora_phone() + current = dict(data or {}) + access_token = (current.get("access_token") or "").strip() + web_session = current.get("web_session") + account = current.get("account") + if account is None and account_id is not None: + account = _load_account(int(account_id)) + current["account"] = account + if ( + web_session is not None + and access_token + and sora_phone.is_chatgpt_web_access_token(access_token) + and not force_web_login + ): + return current + if account is not None and force_web_login: + _drop_nf2_web_session(account.get("id")) + current["web_session"] = None + if account is not None and not force_web_login: + cached = _get_nf2_web_session(account.get("id")) + if cached: + cached_session = cached.get("web_session") + cached_access_token = "" + try: + session_state = sora_phone._read_sora_web_session(cached_session) + except Exception: + session_state = {} + if isinstance(session_state, dict): + cached_access_token = (session_state.get("access_token") or "").strip() + if not cached_access_token: + cached_access_token = (cached.get("access_token") or "").strip() + if cached_access_token and sora_phone.is_chatgpt_web_access_token(cached_access_token): + cached_origin = (session_state.get("base_origin") or cached.get("web_origin") or "").strip() + probe = sora_phone.sora_probe_nf2_session( + cached_access_token, + web_session=cached_session, + preferred_origin=cached_origin, + ) + if probe.get("ok"): + current["access_token"] = cached_access_token + current["web_session"] = cached_session + current["web_origin"] = (probe.get("base_origin") or cached_origin or "").strip() + _save_account_tokens(int(account["id"]), access_token=cached_access_token) + _store_nf2_web_session( + int(account["id"]), + cached_session, + access_token=cached_access_token, + proxy_url=(current.get("proxy_url") or cached.get("proxy_url") or "").strip(), + web_origin=(current.get("web_origin") or "").strip(), + ) + return current + _drop_nf2_web_session(account.get("id")) + current["web_session"] = None + if access_token and sora_phone.is_chatgpt_web_access_token(access_token) and not account: + return current + if not account: + return current + browser_auth = sora_phone.sora_import_browser_web_session( + expected_email=account.get("email") or "", + preferred_origin=current.get("web_origin") or "", + ) + browser_access_token = (browser_auth.get("access_token") or "").strip() if isinstance(browser_auth, dict) else "" + browser_web_session = browser_auth.get("web_session") if isinstance(browser_auth, dict) else None + if browser_access_token and browser_web_session is not None: + current["access_token"] = browser_access_token + current["web_session"] = browser_web_session + current["web_origin"] = (browser_auth.get("base_origin") or "").strip() + _store_nf2_web_session( + int(account["id"]), + browser_web_session, + access_token=browser_access_token, + proxy_url=(current.get("proxy_url") or "").strip(), + web_origin=(current.get("web_origin") or "").strip(), + ) + _save_account_tokens(int(account["id"]), access_token=browser_access_token) + return current + if not (account.get("email") or "").strip() or not (account.get("password") or "").strip(): + return current + otp_fetcher = _build_account_otp_fetcher(account.get("email") or "") + web_auth = sora_phone.sora_chatgpt_web_login( + account.get("email") or "", + account.get("password") or "", + get_otp_fn=otp_fetcher, + proxy_url=current.get("proxy_url") or "", + return_web_session=True, + ) + new_access_token = (web_auth.get("access_token") or "").strip() + new_web_session = web_auth.get("web_session") if isinstance(web_auth, dict) else None + if not new_access_token: + if new_web_session is not None: + _close_nf2_web_session(new_web_session) + return current + new_origin = (web_auth.get("base_origin") or "").strip() + if new_web_session is not None: + probe = sora_phone.sora_probe_nf2_session( + new_access_token, + web_session=new_web_session, + preferred_origin=new_origin, + ) + if probe.get("ok"): + new_origin = (probe.get("base_origin") or new_origin or "").strip() + else: + _close_nf2_web_session(new_web_session) + return current + current["access_token"] = new_access_token + current["web_session"] = new_web_session + current["web_origin"] = new_origin + if new_web_session is not None: + _store_nf2_web_session( + int(account["id"]), + new_web_session, + access_token=new_access_token, + proxy_url=(current.get("proxy_url") or "").strip(), + web_origin=(current.get("web_origin") or "").strip(), + ) + _save_account_tokens(int(account["id"]), access_token=new_access_token) + return current + + +def _candidate_nf2_origins(data: dict) -> list[str]: + sora_phone = _import_sora_phone() + seen = [] + for value in ( + (data.get("web_origin") or "").strip(), + (getattr(sora_phone, "SORA_ORIGIN", "") or "").strip(), + (getattr(sora_phone, "SORA_LEGACY_ORIGIN", "") or "").strip(), + ): + origin = (value or "").rstrip("/") + if origin and origin not in seen: + seen.append(origin) + return seen + + +def _run_nf2_request_with_origin_fallback(data: dict, request_fn): + origins = _candidate_nf2_origins(data or {}) + last_resp = None + last_exc = None + for index, origin in enumerate(origins): + try: + resp = _run_transport_safe_request(lambda: request_fn(data, origin)) + except SoraUpstreamTransportError as exc: + last_exc = exc + if index + 1 < len(origins): + continue + raise + last_resp = resp + status_code = int(getattr(resp, "status_code", 0) or 0) + if index + 1 < len(origins) and status_code in (401, 403, 404): + continue + data["web_origin"] = origin + return resp + if last_resp is not None: + return last_resp + if last_exc is not None: + raise last_exc + raise SoraUpstreamTransportError("nf2 request failed without response") + + +def _run_nf2_session_request( + body: SoraTokenBody, + *, + default_account_id: Optional[int] = None, + locked_account_id: Optional[int] = None, + allow_pool_rotation: bool = False, + request_fn, + decorate_fn=None, +) -> dict: + data = _resolve_tokens( + body, + allow_refresh=False, + default_account_id=default_account_id, + locked_account_id=locked_account_id, + allow_pool_rotation=allow_pool_rotation, + ) + resolved_account_id = (data.get("account") or {}).get("id") + if resolved_account_id is None: + resolved_account_id = default_account_id + + final_result = None + for attempt in range(2): + data = _ensure_nf2_access_token( + data, + account_id=resolved_account_id, + force_web_login=bool(attempt), + ) + if not (data.get("access_token") or "").strip(): + raise HTTPException(status_code=400, detail="缺少可用的 ChatGPT Web access_token") + if resolved_account_id and data.get("web_session") is None: + if attempt == 0: + data["access_token"] = "" + continue + raise HTTPException(status_code=400, detail="无法建立可用的 ChatGPT/Sora Web session") + try: + resp = _run_nf2_request_with_origin_fallback(data, request_fn) + except SoraUpstreamTransportError as exc: + if data["account"] is not None: + _mark_account_last_error(data["account"]["id"], f"transport error: {exc}") + _raise_transport_http_error(str(exc), all_accounts=False) + result = _build_account_result(resp, _parse_response_payload(resp), data) + if int(resp.status_code or 0) == 401 and resolved_account_id and attempt == 0: + _drop_nf2_web_session(int(resolved_account_id)) + data["access_token"] = "" + data["web_session"] = None + final_result = result + continue + if resolved_account_id and data.get("web_session") is not None: + _touch_nf2_web_session(int(resolved_account_id), data) + final_result = result + break + + if callable(decorate_fn): + return decorate_fn(final_result or {}) + return final_result or {} + + +def _sora_caller_rules(caller: dict) -> dict: + auth_type = caller.get("auth_type") or "admin" + if auth_type == "api_key": + bound_account_id = caller.get("account_id") + # account_id 为 None 表示池模式(创建 Key 时 account_id=0) + if bound_account_id is None: + return { + "default_account_id": None, + "locked_account_id": None, + "allow_direct_tokens": False, + "allow_pool_rotation": True, + "inject_watermark_free": True, + } + return { + "default_account_id": int(bound_account_id), + "locked_account_id": int(bound_account_id), + "allow_direct_tokens": False, + "allow_pool_rotation": False, + "inject_watermark_free": True, + } + return { + "default_account_id": None, + "locked_account_id": None, + "allow_direct_tokens": True, + "allow_pool_rotation": False, + "inject_watermark_free": False, + } + + +def _locked_sora_caller_rules(caller: dict, account_id: int) -> dict: + rules = dict(_sora_caller_rules(caller)) + rules["default_account_id"] = int(account_id) + rules["locked_account_id"] = int(account_id) + rules["allow_pool_rotation"] = False + return rules + + +@router.post("/rt-to-at") +def rt_to_at(body: SoraTokenBody, caller: dict = Depends(get_sora_api_caller)): + rules = _sora_caller_rules(caller) + resolve_keys = {k: v for k, v in rules.items() if k not in ("inject_watermark_free",)} + data = _resolve_tokens(body, allow_refresh=True, **resolve_keys) + if not data["access_token"]: + if data["account"] is not None: + _mark_account_last_error(data["account"]["id"], "RT->AT failed") + raise HTTPException(status_code=502, detail="RT 换 AT 失败,请检查 refresh_token/代理") + return { + "ok": True, + "account_id": data["account"]["id"] if data["account"] else None, + "email": data["account"]["email"] if data["account"] else "", + "access_token": data["access_token"], + "refresh_token": data["refresh_token"], + } + + +@router.post("/bootstrap") +def sora_bootstrap(body: SoraTokenBody, caller: dict = Depends(get_sora_api_caller)): + rules = _sora_caller_rules(caller) + resolve_keys = {k: v for k, v in rules.items() if k not in ("inject_watermark_free",)} + data = _resolve_tokens(body, allow_refresh=True, prefer_refresh_token_for_sora=True, **resolve_keys) + at = data["access_token"] + if not at: + raise HTTPException(status_code=400, detail="缺少 access_token(或 refresh_token)") + sora_phone = _import_sora_phone() + ok = sora_phone.sora_bootstrap(at, proxy_url=data["proxy_url"]) + if not ok: + if data["account"] is not None: + _mark_account_last_error(data["account"]["id"], "Sora bootstrap failed") + raise HTTPException(status_code=502, detail="Sora bootstrap 失败") + if data["account"] is not None: + _clear_account_quota_exhausted(data["account"]["id"]) + return {"ok": True, "used_account_id": data["account"]["id"] if data["account"] else None} + + +@router.post("/me") +def sora_me(body: SoraTokenBody, caller: dict = Depends(get_sora_api_caller)): + rules = _sora_caller_rules(caller) + resolve_keys = {k: v for k, v in rules.items() if k not in ("inject_watermark_free",)} + data = _resolve_tokens(body, allow_refresh=True, prefer_refresh_token_for_sora=True, **resolve_keys) + at = data["access_token"] + if not at: + raise HTTPException(status_code=400, detail="缺少 access_token(或 refresh_token)") + sora_phone = _import_sora_phone() + me = sora_phone.sora_me(at, proxy_url=data["proxy_url"]) + if not me: + if data["account"] is not None: + _mark_account_last_error(data["account"]["id"], "Sora me failed") + raise HTTPException(status_code=502, detail="Sora me 请求失败") + if data["account"] is not None: + _clear_account_quota_exhausted(data["account"]["id"]) + return { + "ok": True, + "account_id": data["account"]["id"] if data["account"] else None, + "email": data["account"]["email"] if data["account"] else "", + "used_account_id": data["account"]["id"] if data["account"] else None, + "me": me, + } + + +@router.post("/activate") +def sora_activate(body: SoraTokenBody, caller: dict = Depends(get_sora_api_caller)): + rules = _sora_caller_rules(caller) + resolve_keys = {k: v for k, v in rules.items() if k not in ("inject_watermark_free",)} + data = _resolve_tokens(body, allow_refresh=True, prefer_refresh_token_for_sora=True, **resolve_keys) + account = data["account"] + tokens = { + "access_token": data["access_token"], + "refresh_token": data["refresh_token"], + } + logs = [] + + def _step(msg: str) -> None: + text = (msg or "").strip() + if text: + logs.append(text[:500]) + + sora_phone = _import_sora_phone() + ok = False + if account is not None: + pr = _import_protocol_register() + get_otp_fn = _build_account_otp_fetcher(account["email"]) + ok = pr.activate_sora( + tokens, + account["email"], + proxy_url=data["proxy_url"], + step_log_fn=_step, + account_password=(account.get("password") or "").strip(), + get_otp_fn=get_otp_fn, + ) + else: + at = tokens["access_token"] + if not at: + raise HTTPException(status_code=400, detail="缺少 access_token(或 refresh_token)") + ok = sora_phone.sora_ensure_activated(at, proxy_url=data["proxy_url"], log_fn=_step) + + if not ok: + detail = logs[-1] if logs else "Sora 激活失败" + if account is not None: + _mark_account_last_error(account["id"], detail) + raise HTTPException(status_code=502, detail=detail) + + if account is not None: + _save_account_tokens( + account["id"], + access_token=(tokens.get("access_token") or "").strip(), + refresh_token=(tokens.get("refresh_token") or "").strip(), + ) + + at = (tokens.get("access_token") or "").strip() + me = sora_phone.sora_me(at, proxy_url=data["proxy_url"]) if at else {} + if account is not None: + _clear_account_quota_exhausted(account["id"]) + _mark_account_sora(account["id"]) + return { + "ok": True, + "account_id": account["id"] if account else None, + "email": account["email"] if account else "", + "used_account_id": account["id"] if account else None, + "username": (me or {}).get("username") or "", + "me": me or {}, + } + + +# 最大自动重试次数(池模式下额度耗尽时自动切换账号重试) +_MAX_POOL_RETRIES = 5 + + +def _validate_sora_request(body: SoraRequestBody) -> tuple[str, str]: + method = (body.method or "GET").strip().upper() + path = (body.path or "").strip() + if method not in ("GET", "POST"): + raise HTTPException(status_code=400, detail="method 仅支持 GET/POST") + if not path.startswith("/backend/"): + raise HTTPException(status_code=400, detail="path 仅允许 /backend/*") + return method, path + + +def _is_video_gen_create_request(method: str, path: str) -> bool: + return method == "POST" and path.rstrip("/") == "/backend/video_gen" + + +def _do_sora_request(body: SoraRequestBody, data: dict, inject_watermark_free: bool = False): + """执行单次 Sora 后端请求,返回 (response, payload, quota_reason)。""" + at = data["access_token"] + method, path = _validate_sora_request(body) + + sora_phone = _import_sora_phone() + url = f"{sora_phone.SORA_ORIGIN}{path}" + device_id = None + if method == "POST": + device_id = sora_phone.uuid.uuid4() + headers = sora_phone._build_headers(at, device_id=str(device_id) if device_id else None) + + # API Key 调用注入去水印 header + if inject_watermark_free: + headers["x-sora-watermark"] = "disabled" + + if _is_video_gen_create_request(method, path): + sentinel = sora_phone._build_sentinel_header( + headers.get("oai-device-id") or str(device_id), + "sora_create_task", + proxy_url=data["proxy_url"], + ) + if sentinel: + headers["openai-sentinel-token"] = sentinel + + if method == "GET": + r = sora_phone._session_get(url, headers=headers, proxy_url=data["proxy_url"]) + else: + payload = body.payload or {} + if _is_video_gen_create_request(method, path): + payload = sora_phone._strip_nullish(payload) + r = sora_phone._session_post( + url, + headers=headers, + json=payload, + proxy_url=data["proxy_url"], + ) + try: + payload = r.json() + except Exception: + payload = {"text": (r.text or "")[:1000]} + + quota_reason = _extract_quota_reason(r.status_code, payload, r.text or "") + return r, payload, quota_reason + + +def _run_sora_request(body: SoraRequestBody, caller: dict, rules_override: Optional[dict] = None) -> dict: + rules = dict(rules_override) if rules_override is not None else _sora_caller_rules(caller) + inject_watermark_free = rules.pop("inject_watermark_free", False) + allow_pool = rules.get("allow_pool_rotation", False) + data = _resolve_tokens(body, allow_refresh=True, prefer_refresh_token_for_sora=True, **rules) + at = data["access_token"] + if not at: + raise HTTPException(status_code=400, detail="缺少 access_token(或 refresh_token)") + + _validate_sora_request(body) + + try: + r, payload, quota_reason = _run_transport_safe_request( + lambda: _do_sora_request(body, data, inject_watermark_free=inject_watermark_free) + ) + except SoraUpstreamTransportError as exc: + if data["account"] is not None: + _mark_account_last_error(data["account"]["id"], f"transport error: {exc}") + _raise_transport_http_error(str(exc), all_accounts=False) + + if quota_reason: + tried_ids = [] + if data["account"] is not None: + _mark_account_quota_exhausted(data["account"]["id"], quota_reason) + tried_ids.append(data["account"]["id"]) + + if allow_pool: + for _ in range(_MAX_POOL_RETRIES): + next_account = _pick_next_available_account(exclude_ids=tried_ids) + if not next_account: + break + try: + next_data = _resolve_tokens( + SoraTokenBody(account_id=next_account["id"]), + allow_refresh=True, + prefer_refresh_token_for_sora=True, + default_account_id=next_account["id"], + locked_account_id=next_account["id"], + allow_direct_tokens=False, + ) + except Exception: + tried_ids.append(next_account["id"]) + continue + if not next_data["access_token"]: + tried_ids.append(next_account["id"]) + continue + try: + r2, payload2, quota_reason2 = _run_transport_safe_request( + lambda: _do_sora_request(body, next_data, inject_watermark_free=inject_watermark_free) + ) + except SoraUpstreamTransportError as exc: + _mark_account_last_error(next_account["id"], f"transport error: {exc}") + tried_ids.append(next_account["id"]) + continue + if quota_reason2: + _mark_account_quota_exhausted(next_account["id"], quota_reason2) + tried_ids.append(next_account["id"]) + continue + data = next_data + r, payload, quota_reason = r2, payload2, quota_reason2 + break + + if quota_reason: + raise HTTPException( + status_code=429, + detail=f"{'所有' if allow_pool else ''}账号额度不足,已自动标记不可用:{quota_reason}" + ) + + if 200 <= r.status_code < 300: + if data["account"] is not None: + _clear_account_quota_exhausted(data["account"]["id"]) + else: + if data["account"] is not None: + _mark_account_last_error(data["account"]["id"], f"HTTP {r.status_code}") + return { + "ok": 200 <= r.status_code < 300, + "status_code": r.status_code, + "data": payload, + "used_account_id": data["account"]["id"] if data["account"] else None, + "used_email": data["account"]["email"] if data["account"] else "", + } + + +def _parse_response_payload(resp) -> Any: + try: + return resp.json() + except Exception: + return {"text": (resp.text or "")[:1000]} + + +def _build_account_result(resp, payload: Any, data: dict) -> dict: + if 200 <= resp.status_code < 300: + if data["account"] is not None: + _clear_account_quota_exhausted(data["account"]["id"]) + else: + if data["account"] is not None: + _mark_account_last_error(data["account"]["id"], f"HTTP {resp.status_code}") + return { + "ok": 200 <= resp.status_code < 300, + "status_code": resp.status_code, + "data": payload, + "used_account_id": data["account"]["id"] if data["account"] else None, + "used_email": data["account"]["email"] if data["account"] else "", + } + + +def _run_nf2_create_request(data: dict, body: SoraVideoGenNfCreateBody, payload: dict) -> tuple[dict, str]: + sora_phone = _import_sora_phone() + def _request(current: dict, origin: str): + device_id = str(sora_phone.uuid.uuid4()) + headers = sora_phone._build_sora_web_headers( + current["access_token"], + device_id=device_id, + origin=origin, + ) + sentinel = sora_phone._build_sentinel_header( + device_id, + "sora_2_create_task", + proxy_url=current["proxy_url"], + ) + if sentinel: + headers["openai-sentinel-token"] = sentinel + path = "/backend/nf/bulk_create" if int((payload or {}).get("nsamples") or 1) > 1 else "/backend/nf/create" + return sora_phone._web_session_json_post( + f"{origin}{path}", + headers=headers, + json=sora_phone._strip_nullish(payload), + proxy_url=current["proxy_url"], + web_session=current.get("web_session"), + ) + + try: + resp = _run_nf2_request_with_origin_fallback(data, _request) + except SoraUpstreamTransportError as exc: + if data["account"] is not None: + _mark_account_last_error(data["account"]["id"], f"transport error: {exc}") + _raise_transport_http_error(str(exc), all_accounts=False) + parsed = _parse_response_payload(resp) + quota_reason = _extract_quota_reason(resp.status_code, parsed, getattr(resp, "text", "") or "") + return _build_account_result(resp, parsed, data), quota_reason + + +def _run_nf2_task_lookup(task_id: str, body: SoraTokenBody, caller: dict) -> dict: + sora_phone = _import_sora_phone() + account_id = body.account_id or _lookup_video_task_account(task_id) + rules_override = _locked_sora_caller_rules(caller, account_id) if account_id and _is_pool_api_key_caller(caller) else None + decorated = _run_nf2_session_request( + body, + default_account_id=account_id, + locked_account_id=account_id if account_id and rules_override else None, + allow_pool_rotation=False, + request_fn=lambda data, origin: sora_phone.sora_nf2_get_task( + data["access_token"], + task_id, + proxy_url=data["proxy_url"], + web_session=data.get("web_session"), + base_origin=origin, + ), + decorate_fn=lambda result: _decorate_nf2_result(result, task_id=task_id), + ) + draft_id = (decorated.get("draft_id") or "").strip() + if decorated.get("ok") and decorated.get("is_success") and draft_id: + draft_result = _run_nf2_session_request( + SoraDraftBody( + account_id=body.account_id, + access_token=body.access_token, + refresh_token=body.refresh_token, + proxy_url=body.proxy_url, + draft_id=draft_id, + ), + default_account_id=account_id, + locked_account_id=account_id if account_id and rules_override else None, + allow_pool_rotation=False, + request_fn=lambda data, origin: sora_phone.sora_nf2_get_draft( + data["access_token"], + draft_id, + proxy_url=data["proxy_url"], + web_session=data.get("web_session"), + base_origin=origin, + ), + decorate_fn=_decorate_nf2_result, + ) + if draft_result.get("ok"): + decorated = _merge_nf2_lookup_result(decorated, draft_result) + if account_id: + _sync_video_task_result( + task_id, + int(account_id), + decorated, + caller.get("api_key_id"), + default_active=None, + task_family=_TASK_FAMILY_NF2, + ) + return decorated + + +@router.post("/request") +def sora_request(body: SoraRequestBody, caller: dict = Depends(get_sora_api_caller)): + _, path = _validate_sora_request(body) + if path.startswith("/backend/video_gen"): + if path.rstrip("/") == "/backend/video_gen" and (body.method or "GET").strip().upper() == "POST": + _require_api_key_video_scope( + caller, + SORA_API_KEY_SCOPE_IMAGE if _payload_is_image_to_video(body.payload or {}) else SORA_API_KEY_SCOPE_TEXT, + ) + else: + _require_api_key_any_video_scope(caller) + return _run_sora_request(body, caller) + + +def _build_video_gen_list_path(limit: int = 20, last_id: str = "", task_type_filter: str = "videos") -> str: + params = { + "limit": max(1, min(int(limit or 20), 100)), + } + if (last_id or "").strip(): + params["last_id"] = (last_id or "").strip() + if (task_type_filter or "").strip(): + params["task_type_filters"] = (task_type_filter or "").strip() + return f"/backend/video_gen?{urlencode(params)}" + + +def _run_video_task_lookup(task_id: str, body: SoraTokenBody, caller: dict) -> dict: + meta = _lookup_video_task_meta(task_id) + task_family = _normalize_task_family((meta or {}).get("task_family") or "") + if task_family == _TASK_FAMILY_NF2: + return _run_nf2_task_lookup(task_id, body, caller) + account_id = body.account_id or ((meta or {}).get("account_id") if meta else None) + rules_override = _locked_sora_caller_rules(caller, account_id) if account_id and _is_pool_api_key_caller(caller) else None + result = _run_sora_request( + SoraRequestBody( + account_id=account_id, + access_token=body.access_token, + refresh_token=body.refresh_token, + proxy_url=body.proxy_url, + method="GET", + path=f"/backend/video_gen/{task_id}", + payload={}, + ), + caller, + rules_override=rules_override, + ) + decorated = _decorate_video_task_result(result, task_id=task_id) + if account_id: + _sync_video_task_result( + task_id, + int(account_id), + decorated, + caller.get("api_key_id"), + default_active=None, + task_family=_TASK_FAMILY_VIDEO_GEN, + ) + return decorated + + +@router.post("/video-gen/create") +def sora_video_gen_create(body: SoraVideoGenCreateBody, caller: dict = Depends(get_sora_api_caller)): + sora_phone = _import_sora_phone() + source_image_media_id = (body.source_image_media_id or "").strip() + source_asset = _load_media_asset(source_image_media_id) if source_image_media_id else None + forced_account_id = None + wants_legacy_text_video = _wants_legacy_text_video(body.task_family) + + if not source_image_media_id and not wants_legacy_text_video: + return sora_video_gen_nf_create( + SoraVideoGenNfCreateBody( + account_id=body.account_id, + access_token=body.access_token, + refresh_token=body.refresh_token, + proxy_url=body.proxy_url, + prompt=body.prompt, + auto_rotate=body.auto_rotate, + n_variants=body.n_variants, + n_frames=body.n_frames, + resolution=body.resolution, + orientation=body.orientation, + model=(body.model or "sy_8").strip() or "sy_8", + style_id=body.style_id, + audio_caption=body.audio_caption, + audio_transcript=body.audio_transcript, + video_caption=body.video_caption, + seed=body.seed, + extra_payload=body.extra_payload, + ), + caller, + ) + + if source_image_media_id: + _require_api_key_video_scope(caller, SORA_API_KEY_SCOPE_IMAGE) + if source_asset: + forced_account_id = int(source_asset["account_id"]) + if body.account_id is not None and int(body.account_id) != forced_account_id: + raise HTTPException(status_code=403, detail="source_image_media_id 绑定的账号与当前请求账号不一致") + elif body.account_id is None and not (body.access_token or "").strip() and not (body.refresh_token or "").strip(): + raise HTTPException(status_code=400, detail="source_image_media_id 未在本地记录,请先调用 /api/sora-api/video-gen/upload-image 或 /create-with-image") + payload = sora_phone.sora_build_image_video_payload( + body.prompt, + source_image_media_id, + operation=body.operation, + n_variants=body.n_variants, + n_frames=body.n_frames, + resolution=body.resolution, + orientation=body.orientation, + model=(body.model or "").strip() or None, + seed=body.seed, + ) + else: + payload = sora_phone.sora_build_simple_video_payload( + body.prompt, + operation=body.operation, + n_variants=body.n_variants, + n_frames=body.n_frames, + resolution=body.resolution, + orientation=body.orientation, + model=(body.model or "").strip() or None, + seed=body.seed, + ) + if body.extra_payload: + payload.update(body.extra_payload) + admin_auto_rotate = bool(body.auto_rotate) and body.account_id is None and not (body.access_token or "").strip() and not (body.refresh_token or "").strip() + pool_dispatch = not forced_account_id and (_is_pool_api_key_caller(caller) or admin_auto_rotate) and body.account_id is None + tried_ids = [] + result = None + reservation_task_id = "" + fixed_account_id = forced_account_id or (int(body.account_id) if body.account_id is not None else None) + + while True: + request_body = SoraRequestBody( + account_id=fixed_account_id, + access_token=body.access_token, + refresh_token=body.refresh_token, + proxy_url=body.proxy_url, + method="POST", + path="/backend/video_gen", + payload=payload, + ) + rules_override = _locked_sora_caller_rules(caller, fixed_account_id) if fixed_account_id else None + reserved_account_id = None + reserved_email = "" + + if pool_dispatch: + reserved = _reserve_pool_video_account( + caller.get("api_key_id"), + exclude_ids=tried_ids, + task_family=_TASK_FAMILY_VIDEO_GEN, + ) + if not reserved: + if result is None: + raise HTTPException(status_code=404, detail="账号池中无可用 Sora 账号(请检查 token/额度/启停状态)") + break + reservation_task_id = reserved["reservation_task_id"] + reserved_account_id = int(reserved["id"]) + reserved_email = reserved["email"] or "" + request_body.account_id = reserved_account_id + request_body.access_token = "" + request_body.refresh_token = "" + request_body.proxy_url = "" + rules_override = _locked_sora_caller_rules(caller, reserved_account_id) + + try: + result = _run_sora_request(request_body, caller, rules_override=rules_override) + except HTTPException as exc: + if reservation_task_id: + _release_video_task_reservation(reservation_task_id) + if pool_dispatch and reserved_account_id is not None and exc.status_code in (429, 503): + tried_ids.append(reserved_account_id) + result = { + "ok": False, + "status_code": exc.status_code, + "data": { + "error": { + "code": "quota_exceeded" if exc.status_code == 429 else "upstream_transport_error", + "message": str(exc.detail), + } + }, + "used_account_id": reserved_account_id, + "used_email": reserved_email, + } + reservation_task_id = "" + continue + raise + + task_id = ((result.get("data") or {}).get("id") or "").strip() + decorated = _decorate_video_task_result({ + **result, + "task_id": task_id, + "request_payload": payload, + "source_image_media_id": source_image_media_id, + }, task_id=task_id) + + used_account_id = decorated.get("used_account_id") + busy_reason = _extract_busy_reason(decorated.get("data"), "") + should_retry_pool = pool_dispatch and used_account_id and (busy_reason or _is_too_many_concurrent_tasks_result(decorated)) + + if reservation_task_id: + if task_id and used_account_id: + _claim_reserved_video_task( + reservation_task_id, + task_id, + int(used_account_id), + api_key_id=caller.get("api_key_id"), + task_family=_TASK_FAMILY_VIDEO_GEN, + raw_status=decorated.get("status") or "", + normalized_status=decorated.get("normalized_status") or "", + is_active=not bool(decorated.get("is_terminal")), + ) + else: + _release_video_task_reservation(reservation_task_id) + reservation_task_id = "" + elif task_id and used_account_id: + _sync_video_task_result( + task_id, + int(used_account_id), + decorated, + caller.get("api_key_id"), + default_active=True, + task_family=_TASK_FAMILY_VIDEO_GEN, + ) + + if should_retry_pool: + tried_ids.append(int(used_account_id)) + continue + return decorated + + if result is None: + raise HTTPException(status_code=500, detail="视频任务创建失败") + task_id = ((result.get("data") or {}).get("id") or "").strip() + decorated = _decorate_video_task_result({ + **result, + "task_id": task_id, + "request_payload": payload, + "source_image_media_id": source_image_media_id, + }, task_id=task_id) + if task_id and decorated.get("used_account_id"): + _sync_video_task_result( + task_id, + int(decorated["used_account_id"]), + decorated, + caller.get("api_key_id"), + default_active=True, + task_family=_TASK_FAMILY_VIDEO_GEN, + ) + return decorated + + +@router.post("/video-gen/create-and-wait") +def sora_video_gen_create_and_wait(body: SoraVideoGenCreateAndWaitBody, caller: dict = Depends(get_sora_api_caller)): + create_result = sora_video_gen_create( + SoraVideoGenCreateBody( + account_id=body.account_id, + access_token=body.access_token, + refresh_token=body.refresh_token, + proxy_url=body.proxy_url, + prompt=body.prompt, + auto_rotate=body.auto_rotate, + task_family=body.task_family, + operation=body.operation, + n_variants=body.n_variants, + n_frames=body.n_frames, + resolution=body.resolution, + orientation=body.orientation, + model=body.model, + style_id=body.style_id, + audio_caption=body.audio_caption, + audio_transcript=body.audio_transcript, + video_caption=body.video_caption, + seed=body.seed, + source_image_media_id=body.source_image_media_id, + extra_payload=body.extra_payload, + ), + caller, + ) + task_id = (create_result.get("task_id") or "").strip() + if not create_result.get("ok") or not task_id: + return { + "ok": False, + "timed_out": False, + "task_id": task_id, + "task_family": create_result.get("task_family") or "", + "status": create_result.get("status") or "", + "normalized_status": create_result.get("normalized_status") or "", + "is_terminal": bool(create_result.get("is_terminal")), + "is_success": bool(create_result.get("is_success")), + "video_urls": create_result.get("video_urls") or [], + "used_account_id": create_result.get("used_account_id"), + "used_email": create_result.get("used_email") or "", + "poll_attempts": 0, + "elapsed_seconds": 0.0, + "message": "视频任务创建失败", + "create_result": create_result, + "final_result": create_result, + } + + started_at = time.time() + poll_attempts = 0 + final_result = None + timed_out = False + poll_interval_seconds = float(body.poll_interval_seconds or 5.0) + timeout_seconds = int(body.timeout_seconds or 900) + + while True: + poll_attempts += 1 + final_result = _run_video_task_lookup(task_id, body, caller) + if final_result.get("is_terminal"): + break + if (not final_result.get("ok")) and int(final_result.get("status_code") or 0) not in _VIDEO_RETRYABLE_POLL_STATUS_CODES: + break + if (time.time() - started_at) >= timeout_seconds: + timed_out = True + break + time.sleep(poll_interval_seconds) + + elapsed_seconds = round(max(0.0, time.time() - started_at), 2) + normalized_status = (final_result or {}).get("normalized_status") or "" + ok = bool(final_result and final_result.get("is_success")) + if ok: + message = "视频任务已成功出片(succeeded)" + elif timed_out: + current_status = normalized_status or (final_result or {}).get("status") or "unknown" + message = f"轮询超时,当前状态:{current_status}" + elif final_result and not final_result.get("ok"): + code = final_result.get("status_code") + message = f"轮询查询失败,HTTP {code}" + else: + current_status = normalized_status or (final_result or {}).get("status") or "unknown" + message = f"视频任务已结束,状态:{current_status}" + + return { + "ok": ok, + "timed_out": timed_out, + "task_id": task_id, + "task_family": (final_result or {}).get("task_family") or create_result.get("task_family") or "", + "status": (final_result or {}).get("status") or "", + "normalized_status": normalized_status, + "is_terminal": bool((final_result or {}).get("is_terminal")), + "is_success": bool((final_result or {}).get("is_success")), + "video_urls": (final_result or {}).get("video_urls") or [], + "used_account_id": create_result.get("used_account_id"), + "used_email": create_result.get("used_email") or "", + "poll_attempts": poll_attempts, + "elapsed_seconds": elapsed_seconds, + "message": message, + "create_result": create_result, + "final_result": final_result, + } + + +@router.post("/video-gen-nf/create") +def sora_video_gen_nf_create(body: SoraVideoGenNfCreateBody, caller: dict = Depends(get_sora_api_caller)): + sora_phone = _import_sora_phone() + _require_api_key_video_scope(caller, SORA_API_KEY_SCOPE_TEXT) + payload = sora_phone.sora_build_nf2_video_payload( + body.prompt, + n_variants=body.n_variants, + n_frames=body.n_frames, + resolution=body.resolution, + orientation=body.orientation, + model=body.model, + style_id=body.style_id, + audio_caption=body.audio_caption, + audio_transcript=body.audio_transcript, + video_caption=body.video_caption, + seed=body.seed, + ) + if body.extra_payload: + payload.update(body.extra_payload) + payload = sora_phone._strip_nullish(payload) + + admin_auto_rotate = bool(body.auto_rotate) and body.account_id is None and not (body.access_token or "").strip() and not (body.refresh_token or "").strip() + pool_dispatch = (_is_pool_api_key_caller(caller) or admin_auto_rotate) and body.account_id is None + tried_ids = [] + result = None + reservation_task_id = "" + fixed_account_id = int(body.account_id) if body.account_id is not None else None + + while True: + reserved_account_id = None + reserved_email = "" + request_account_id = fixed_account_id + if pool_dispatch: + reserved = _reserve_pool_video_account( + caller.get("api_key_id"), + exclude_ids=tried_ids, + task_family=_TASK_FAMILY_NF2, + ) + if not reserved: + if result is None: + raise HTTPException(status_code=404, detail="账号池中无可用 Sora 账号(请检查 token/额度/启停状态)") + break + reservation_task_id = reserved["reservation_task_id"] + reserved_account_id = int(reserved["id"]) + reserved_email = reserved["email"] or "" + request_account_id = reserved_account_id + + data = _resolve_tokens( + SoraTokenBody( + account_id=request_account_id, + access_token=body.access_token if reserved_account_id is None else "", + refresh_token=body.refresh_token if reserved_account_id is None else "", + proxy_url=body.proxy_url if reserved_account_id is None else "", + ), + allow_refresh=False, + default_account_id=request_account_id, + locked_account_id=request_account_id if request_account_id and (reserved_account_id is not None or caller.get("account_id") is not None) else None, + allow_direct_tokens=reserved_account_id is None, + allow_pool_rotation=False, + ) + quota_reason = "" + for auth_attempt in range(2): + data = _ensure_nf2_access_token( + data, + account_id=request_account_id, + force_web_login=bool(auth_attempt), + ) + if not (data.get("access_token") or "").strip(): + break + if request_account_id and data.get("web_session") is None: + if auth_attempt == 0: + data["access_token"] = "" + continue + result = { + "ok": False, + "status_code": 400, + "data": {"error": {"code": "missing_web_session", "message": "无法建立可用的 ChatGPT/Sora Web session"}}, + "used_account_id": request_account_id, + "used_email": reserved_email or ((data.get("account") or {}).get("email") or ""), + } + break + result, quota_reason = _run_nf2_create_request(data, body, payload) + if int(result.get("status_code") or 0) == 401 and request_account_id and auth_attempt == 0: + _drop_nf2_web_session(int(request_account_id)) + data["access_token"] = "" + data["web_session"] = None + continue + if request_account_id and data.get("web_session") is not None: + _touch_nf2_web_session(int(request_account_id), data) + break + + if not (data.get("access_token") or "").strip(): + if reservation_task_id: + _release_video_task_reservation(reservation_task_id) + reservation_task_id = "" + if pool_dispatch and request_account_id: + tried_ids.append(int(request_account_id)) + result = { + "ok": False, + "status_code": 400, + "data": {"error": {"code": "missing_web_access_token", "message": "缺少可用的 ChatGPT Web access_token"}}, + "used_account_id": request_account_id, + "used_email": reserved_email or ((data.get("account") or {}).get("email") or ""), + } + continue + raise HTTPException(status_code=400, detail="缺少可用的 ChatGPT Web access_token") + + if quota_reason: + if data["account"] is not None: + _mark_account_quota_exhausted(data["account"]["id"], quota_reason) + if reservation_task_id: + _release_video_task_reservation(reservation_task_id) + reservation_task_id = "" + if pool_dispatch and request_account_id: + tried_ids.append(int(request_account_id)) + continue + raise HTTPException(status_code=429, detail=f"账号额度不足,已自动标记不可用:{quota_reason}") + + decorated = _decorate_nf2_result({ + **result, + "request_payload": payload, + }) + task_id = (decorated.get("task_id") or "").strip() + used_account_id = decorated.get("used_account_id") + busy_reason = _extract_busy_reason(decorated.get("data"), "") + should_retry_pool = pool_dispatch and used_account_id and ( + busy_reason + or _is_too_many_concurrent_tasks_result(decorated) + or int(decorated.get("status_code") or 0) == 401 + ) + + if reservation_task_id: + if task_id and used_account_id: + _claim_reserved_video_task( + reservation_task_id, + task_id, + int(used_account_id), + api_key_id=caller.get("api_key_id"), + task_family=_TASK_FAMILY_NF2, + raw_status=decorated.get("status") or "", + normalized_status=decorated.get("normalized_status") or "", + is_active=not bool(decorated.get("is_terminal")), + ) + else: + _release_video_task_reservation(reservation_task_id) + reservation_task_id = "" + elif task_id and used_account_id: + _sync_video_task_result( + task_id, + int(used_account_id), + decorated, + caller.get("api_key_id"), + default_active=True, + task_family=_TASK_FAMILY_NF2, + ) + + if should_retry_pool: + tried_ids.append(int(used_account_id)) + continue + return decorated + + if result is None: + raise HTTPException(status_code=500, detail="NF2 视频任务创建失败") + return _decorate_nf2_result({**result, "request_payload": payload}) + + +@router.post("/video-gen-nf/get") +def sora_video_gen_nf_get(body: SoraVideoTaskBody, caller: dict = Depends(get_sora_api_caller)): + _require_api_key_any_video_scope(caller) + task_id = (body.task_id or "").strip() + if not task_id: + raise HTTPException(status_code=400, detail="缺少 task_id") + return _run_nf2_task_lookup(task_id, body, caller) + + +@router.post("/video-gen-nf/pending") +def sora_video_gen_nf_pending(body: SoraTokenBody, caller: dict = Depends(get_sora_api_caller)): + _require_api_key_any_video_scope(caller) + sora_phone = _import_sora_phone() + default_account_id = caller.get("account_id") if (caller.get("auth_type") or "") == "api_key" else None + return _run_nf2_session_request( + body, + default_account_id=default_account_id, + locked_account_id=default_account_id, + allow_pool_rotation=_is_pool_api_key_caller(caller), + request_fn=lambda data, origin: sora_phone.sora_nf2_get_pending( + data["access_token"], + proxy_url=data["proxy_url"], + web_session=data.get("web_session"), + base_origin=origin, + ), + ) + + +@router.post("/video-gen-nf/draft/get") +def sora_video_gen_nf_draft_get(body: SoraDraftBody, caller: dict = Depends(get_sora_api_caller)): + _require_api_key_any_video_scope(caller) + sora_phone = _import_sora_phone() + draft_id = (body.draft_id or "").strip() + if not draft_id: + raise HTTPException(status_code=400, detail="缺少 draft_id") + default_account_id = caller.get("account_id") if (caller.get("auth_type") or "") == "api_key" else None + return _run_nf2_session_request( + body, + default_account_id=default_account_id, + locked_account_id=default_account_id, + allow_pool_rotation=_is_pool_api_key_caller(caller), + request_fn=lambda data, origin: sora_phone.sora_nf2_get_draft( + data["access_token"], + draft_id, + proxy_url=data["proxy_url"], + web_session=data.get("web_session"), + base_origin=origin, + ), + decorate_fn=_decorate_nf2_result, + ) + + +@router.post("/video-gen-nf/stitch") +def sora_video_gen_nf_stitch(body: SoraStitchBody, caller: dict = Depends(get_sora_api_caller)): + _require_api_key_any_video_scope(caller) + sora_phone = _import_sora_phone() + generation_ids = [str(item).strip() for item in (body.generation_ids or []) if str(item).strip()] + if not generation_ids: + raise HTTPException(status_code=400, detail="缺少 generation_ids") + default_account_id = caller.get("account_id") if (caller.get("auth_type") or "") == "api_key" else None + return _run_nf2_session_request( + body, + default_account_id=default_account_id, + locked_account_id=default_account_id, + allow_pool_rotation=_is_pool_api_key_caller(caller), + request_fn=lambda data, origin: sora_phone.sora_nf2_stitch( + data["access_token"], + generation_ids, + for_download=bool(body.for_download), + proxy_url=data["proxy_url"], + web_session=data.get("web_session"), + base_origin=origin, + ), + decorate_fn=_decorate_nf2_result, + ) + + +def _upload_image_bytes_with_retry( + *, + filename: str, + content_type: str, + file_bytes: bytes, + account_id: Optional[int], + auto_rotate: bool, + access_token: str, + refresh_token: str, + proxy_url: str, + caller: dict, + exclude_account_ids: Optional[list[int]] = None, +) -> dict: + _require_api_key_video_scope(caller, SORA_API_KEY_SCOPE_IMAGE) + token_body = SoraTokenBody( + account_id=account_id, + access_token=access_token, + refresh_token=refresh_token, + proxy_url=proxy_url, + ) + rules = dict(_sora_caller_rules(caller)) + if bool(auto_rotate) and account_id is None and not access_token.strip() and not refresh_token.strip(): + rules["allow_pool_rotation"] = True + resolve_keys = {k: v for k, v in rules.items() if k not in ("inject_watermark_free",)} + sora_phone = _import_sora_phone() + allow_pool_upload = bool(resolve_keys.get("allow_pool_rotation")) and account_id is None and not access_token.strip() and not refresh_token.strip() + tried_ids = [int(x) for x in (exclude_account_ids or []) if int(x)] + last_transport_error = "" + + while True: + try: + data = _resolve_tokens( + token_body, + allow_refresh=True, + prefer_refresh_token_for_sora=True, + exclude_account_ids=tried_ids, + **resolve_keys, + ) + except HTTPException: + if allow_pool_upload and last_transport_error: + _raise_transport_http_error(last_transport_error, all_accounts=True) + raise + + at = data["access_token"] + if not at: + raise HTTPException(status_code=400, detail="缺少 access_token(或 refresh_token)") + + try: + resp = _run_transport_safe_request( + lambda: sora_phone.sora_upload_media( + at, + filename=filename, + content_type=content_type, + file_bytes=file_bytes, + media_type="image", + proxy_url=data["proxy_url"], + ) + ) + except SoraUpstreamTransportError as exc: + used_account = data["account"] + last_transport_error = str(exc) + if used_account is not None: + _mark_account_last_error(used_account["id"], f"image upload transport error: {exc}") + if allow_pool_upload and used_account is not None: + tried_ids.append(int(used_account["id"])) + if len(tried_ids) <= _MAX_POOL_RETRIES: + continue + _raise_transport_http_error(last_transport_error, all_accounts=True) + _raise_transport_http_error(last_transport_error, all_accounts=False) + + try: + payload = resp.json() + except Exception: + payload = {"text": (resp.text or "")[:1000]} + + ok = 200 <= resp.status_code < 300 + used_account = data["account"] + if ok and used_account is not None: + _clear_account_quota_exhausted(used_account["id"]) + elif used_account is not None: + _mark_account_last_error(used_account["id"], f"image upload HTTP {resp.status_code}") + + media_id = ((payload or {}).get("id") or "").strip() if isinstance(payload, dict) else "" + if ok and media_id and used_account is not None: + _remember_media_asset(media_id, int(used_account["id"]), payload if isinstance(payload, dict) else {}, caller.get("api_key_id")) + + return { + "ok": ok, + "status_code": resp.status_code, + "media_id": media_id, + "media": payload, + "used_account_id": used_account["id"] if used_account else None, + "used_email": used_account["email"] if used_account else "", + "source_image_media_id": media_id, + } + + +@router.post("/video-gen/upload-image") +async def sora_video_gen_upload_image( + file: UploadFile = File(...), + account_id: Optional[int] = Form(default=None), + auto_rotate: bool = Form(default=False), + access_token: str = Form(default=""), + refresh_token: str = Form(default=""), + proxy_url: str = Form(default=""), + caller: dict = Depends(get_sora_api_caller), +): + filename = (file.filename or "").strip() + if not filename: + raise HTTPException(status_code=400, detail="缺少图片文件名") + content_type = (file.content_type or mimetypes.guess_type(filename)[0] or "").strip().lower() or "application/octet-stream" + if not content_type.startswith("image/"): + raise HTTPException(status_code=400, detail="仅支持图片文件上传") + file_bytes = await file.read() + if not file_bytes: + raise HTTPException(status_code=400, detail="上传文件为空") + + return _upload_image_bytes_with_retry( + filename=filename, + content_type=content_type, + file_bytes=file_bytes, + account_id=account_id, + auto_rotate=auto_rotate, + access_token=access_token, + refresh_token=refresh_token, + proxy_url=proxy_url, + caller=caller, + exclude_account_ids=None, + ) + + +@router.post("/video-gen/create-with-image") +async def sora_video_gen_create_with_image( + prompt: str = Form(...), + file: UploadFile = File(...), + account_id: Optional[int] = Form(default=None), + auto_rotate: bool = Form(default=False), + access_token: str = Form(default=""), + refresh_token: str = Form(default=""), + proxy_url: str = Form(default=""), + operation: str = Form(default="simple_compose"), + n_variants: int = Form(default=1), + n_frames: int = Form(default=300), + resolution: int = Form(default=360), + orientation: str = Form(default="wide"), + model: str = Form(default=""), + seed: Optional[int] = Form(default=None), + caller: dict = Depends(get_sora_api_caller), +): + filename = (file.filename or "").strip() + if not filename: + raise HTTPException(status_code=400, detail="缺少图片文件名") + content_type = (file.content_type or mimetypes.guess_type(filename)[0] or "").strip().lower() or "application/octet-stream" + if not content_type.startswith("image/"): + raise HTTPException(status_code=400, detail="仅支持图片文件上传") + file_bytes = await file.read() + if not file_bytes: + raise HTTPException(status_code=400, detail="上传文件为空") + + allow_pool_retry = bool(auto_rotate) and account_id is None and not access_token.strip() and not refresh_token.strip() + tried_ids: list[int] = [] + last_result: Optional[dict] = None + + while True: + try: + upload_result = _upload_image_bytes_with_retry( + filename=filename, + content_type=content_type, + file_bytes=file_bytes, + account_id=account_id, + auto_rotate=auto_rotate, + access_token=access_token, + refresh_token=refresh_token, + proxy_url=proxy_url, + caller=caller, + exclude_account_ids=tried_ids, + ) + except HTTPException as exc: + if allow_pool_retry and last_result is not None and exc.status_code in (404, 503): + return last_result + raise + + if not upload_result.get("ok"): + return upload_result + + used_account_id = upload_result.get("used_account_id") + try: + create_result = sora_video_gen_create( + SoraVideoGenCreateBody( + account_id=used_account_id, + prompt=prompt, + auto_rotate=False, + operation=operation, + n_variants=n_variants, + n_frames=n_frames, + resolution=resolution, + orientation=orientation, + model=model, + seed=seed, + source_image_media_id=upload_result.get("media_id") or "", + extra_payload={}, + ), + caller, + ) + except HTTPException as exc: + if allow_pool_retry and used_account_id and exc.status_code in (429, 503): + last_result = { + "ok": False, + "status_code": exc.status_code, + "data": {"error": {"code": "upstream_transport_error" if exc.status_code == 503 else "quota_exceeded", "message": str(exc.detail)}}, + "used_account_id": used_account_id, + "used_email": upload_result.get("used_email") or "", + "task_id": "", + "request_payload": {}, + "source_image_media_id": upload_result.get("media_id") or "", + "status": "", + "normalized_status": "", + "is_terminal": False, + "is_success": False, + "video_urls": [], + "uploaded_media": upload_result.get("media") or {}, + } + tried_ids.append(int(used_account_id)) + if len(tried_ids) <= _MAX_POOL_RETRIES: + continue + return last_result + raise + + create_result["uploaded_media"] = upload_result.get("media") or {} + create_result["source_image_media_id"] = upload_result.get("media_id") or "" + if allow_pool_retry and used_account_id: + busy_reason = _extract_busy_reason(create_result.get("data"), "") + if busy_reason or _is_too_many_concurrent_tasks_result(create_result): + last_result = create_result + tried_ids.append(int(used_account_id)) + if len(tried_ids) <= _MAX_POOL_RETRIES: + continue + return create_result + + +@router.post("/video-gen/list") +def sora_video_gen_list(body: SoraVideoListBody, caller: dict = Depends(get_sora_api_caller)): + _require_api_key_any_video_scope(caller) + return _run_sora_request( + SoraRequestBody( + account_id=body.account_id, + access_token=body.access_token, + refresh_token=body.refresh_token, + proxy_url=body.proxy_url, + method="GET", + path=_build_video_gen_list_path( + limit=body.limit, + last_id=body.last_id, + task_type_filter=body.task_type_filter, + ), + payload={}, + ), + caller, + ) + + +@router.post("/video-gen/get") +def sora_video_gen_get(body: SoraVideoTaskBody, caller: dict = Depends(get_sora_api_caller)): + _require_api_key_any_video_scope(caller) + task_id = (body.task_id or "").strip() + if not task_id: + raise HTTPException(status_code=400, detail="缺少 task_id") + return _run_video_task_lookup(task_id, body, caller) + + +@router.post("/video-gen/cancel") +def sora_video_gen_cancel(body: SoraVideoTaskBody, caller: dict = Depends(get_sora_api_caller)): + _require_api_key_any_video_scope(caller) + task_id = (body.task_id or "").strip() + if not task_id: + raise HTTPException(status_code=400, detail="缺少 task_id") + account_id = body.account_id or _lookup_video_task_account(task_id) + rules_override = _locked_sora_caller_rules(caller, account_id) if account_id and _is_pool_api_key_caller(caller) else None + result = _run_sora_request( + SoraRequestBody( + account_id=account_id, + access_token=body.access_token, + refresh_token=body.refresh_token, + proxy_url=body.proxy_url, + method="POST", + path=f"/backend/video_gen/{task_id}/cancel", + payload={}, + ), + caller, + rules_override=rules_override, + ) + decorated = _decorate_video_task_result(result, task_id=task_id) + if account_id: + _sync_video_task_result(task_id, int(account_id), decorated, caller.get("api_key_id"), default_active=None) + return decorated + + +@router.post("/video-gen/archive") +def sora_video_gen_archive(body: SoraVideoTaskBody, caller: dict = Depends(get_sora_api_caller)): + _require_api_key_any_video_scope(caller) + task_id = (body.task_id or "").strip() + if not task_id: + raise HTTPException(status_code=400, detail="缺少 task_id") + account_id = body.account_id or _lookup_video_task_account(task_id) + rules_override = _locked_sora_caller_rules(caller, account_id) if account_id and _is_pool_api_key_caller(caller) else None + result = _run_sora_request( + SoraRequestBody( + account_id=account_id, + access_token=body.access_token, + refresh_token=body.refresh_token, + proxy_url=body.proxy_url, + method="POST", + path=f"/backend/video_gen/{task_id}/archive", + payload={}, + ), + caller, + rules_override=rules_override, + ) + decorated = _decorate_video_task_result(result, task_id=task_id) + if account_id: + _sync_video_task_result(task_id, int(account_id), decorated, caller.get("api_key_id"), default_active=None) + return decorated diff --git a/Register_GPT_v0/web/backend/app/routers/sora_keys.py b/Register_GPT_v0/web/backend/app/routers/sora_keys.py new file mode 100644 index 0000000..f82e9a1 --- /dev/null +++ b/Register_GPT_v0/web/backend/app/routers/sora_keys.py @@ -0,0 +1,151 @@ +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Query +from pydantic import BaseModel + +from app.database import get_db, init_db +from app.routers.auth import get_current_user +from app.services.sora_api_key import ( + SORA_API_KEY_SCOPE_TEXT, + generate_sora_api_key, + hash_sora_api_key, + mask_sora_api_key, + normalize_sora_api_key_scope, + sora_api_key_scope_label, +) + +router = APIRouter(prefix="/api/sora-keys", tags=["sora-keys"]) + + +class CreateSoraKeyBody(BaseModel): + account_id: int = 0 # 0 = 池模式(自动轮换) + name: str = "" + scope: str = SORA_API_KEY_SCOPE_TEXT + + +@router.get("") +def list_sora_api_keys( + username: str = Depends(get_current_user), + account_id: Optional[int] = Query(None, ge=0), + active_only: bool = Query(True), + scope: str = Query(""), + key_mode: str = Query(""), +): + init_db() + where = [] + params = [] + if account_id is not None: + where.append("k.account_id = ?") + params.append(account_id) + if active_only: + where.append("k.is_active = 1") + normalized_scope = "" + if (scope or "").strip(): + normalized_scope = normalize_sora_api_key_scope(scope) + where.append("COALESCE(k.scope, ?) = ?") + params.extend([SORA_API_KEY_SCOPE_TEXT, normalized_scope]) + mode = (key_mode or "").strip().lower() + if mode == "pool": + where.append("k.account_id = 0") + elif mode == "bound": + where.append("k.account_id != 0") + where_sql = " AND ".join(where) if where else "1=1" + with get_db() as conn: + c = conn.cursor() + c.execute( + f"""SELECT k.id, k.account_id, a.email, k.name, k.key_prefix, k.key_mask, + k.is_active, k.last_used_at, k.created_at, k.created_by, + COALESCE(k.scope, ?) + FROM sora_api_keys k + LEFT JOIN accounts a ON a.id = k.account_id + WHERE {where_sql} + ORDER BY k.id DESC + LIMIT 500""", + [SORA_API_KEY_SCOPE_TEXT] + params, + ) + rows = c.fetchall() + items = [] + for r in rows: + email_display = r[2] or "" + if r[1] == 0: + email_display = "[自动轮换池]" + items.append( + { + "id": r[0], + "account_id": r[1], + "email": email_display, + "name": r[3] or "", + "key_prefix": r[4] or "", + "key_mask": r[5] or "", + "is_active": bool(r[6]), + "last_used_at": r[7] or "", + "created_at": r[8] or "", + "created_by": r[9] or "", + "scope": normalize_sora_api_key_scope(r[10] or SORA_API_KEY_SCOPE_TEXT), + "scope_label": sora_api_key_scope_label(r[10] or SORA_API_KEY_SCOPE_TEXT), + "key_mode": "pool" if int(r[1] or 0) == 0 else "bound", + } + ) + return {"items": items} + + +@router.post("") +def create_sora_api_key(body: CreateSoraKeyBody, username: str = Depends(get_current_user)): + account_id = int(body.account_id if body.account_id is not None else -1) + if account_id < 0: + raise HTTPException(status_code=400, detail="account_id 无效") + scope = normalize_sora_api_key_scope(body.scope) + + raw_key = generate_sora_api_key() + key_hash = hash_sora_api_key(raw_key) + key_mask = mask_sora_api_key(raw_key) + key_prefix = raw_key[:12] + name = (body.name or "").strip() + + init_db() + with get_db() as conn: + c = conn.cursor() + email_display = "" + if account_id == 0: + # 池模式:不绑定固定账号,自动轮换 + email_display = "[自动轮换池]" + else: + c.execute("SELECT id, email FROM accounts WHERE id = ?", (account_id,)) + account = c.fetchone() + if not account: + raise HTTPException(status_code=404, detail="账号不存在") + email_display = account[1] or "" + + c.execute( + """INSERT INTO sora_api_keys + (account_id, name, key_hash, key_prefix, key_mask, created_by, scope, is_active, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, 1, datetime('now'))""", + (account_id, name, key_hash, key_prefix, key_mask, username, scope), + ) + key_id = c.lastrowid + c.execute("SELECT created_at FROM sora_api_keys WHERE id = ?", (key_id,)) + created_at = (c.fetchone() or [""])[0] or "" + + return { + "id": key_id, + "account_id": account_id, + "email": email_display, + "name": name, + "api_key": raw_key, + "key_mask": key_mask, + "created_at": created_at, + "scope": scope, + "scope_label": sora_api_key_scope_label(scope), + "key_mode": "pool" if account_id == 0 else "bound", + } + + +@router.delete("/{key_id}") +def disable_sora_api_key(key_id: int, username: str = Depends(get_current_user)): + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute("UPDATE sora_api_keys SET is_active = 0 WHERE id = ?", (key_id,)) + if c.rowcount <= 0: + raise HTTPException(status_code=404, detail="API Key 不存在") + return {"ok": True} diff --git a/Register_GPT_v0/web/backend/app/security.py b/Register_GPT_v0/web/backend/app/security.py new file mode 100644 index 0000000..b9c39f0 --- /dev/null +++ b/Register_GPT_v0/web/backend/app/security.py @@ -0,0 +1,26 @@ +import bcrypt +from passlib.context import CryptContext + +pwd_ctx = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def verify_password(plain: str, hashed: str) -> bool: + hash_value = (hashed or "").strip() + if not hash_value: + return False + try: + return pwd_ctx.verify(plain, hash_value) + except Exception: + pass + try: + return bcrypt.checkpw((plain or "").encode("utf-8"), hash_value.encode("utf-8")) + except Exception: + return False + + +def get_password_hash(password: str) -> str: + secret = (password or "").encode("utf-8") + try: + return pwd_ctx.hash(password) + except Exception: + return bcrypt.hashpw(secret, bcrypt.gensalt()).decode("utf-8") diff --git a/Register_GPT_v0/web/backend/app/services/__init__.py b/Register_GPT_v0/web/backend/app/services/__init__.py new file mode 100644 index 0000000..a0b12d7 --- /dev/null +++ b/Register_GPT_v0/web/backend/app/services/__init__.py @@ -0,0 +1 @@ +# services diff --git a/Register_GPT_v0/web/backend/app/services/hero_sms.py b/Register_GPT_v0/web/backend/app/services/hero_sms.py new file mode 100644 index 0000000..c5cfe17 --- /dev/null +++ b/Register_GPT_v0/web/backend/app/services/hero_sms.py @@ -0,0 +1,324 @@ +""" +Hero-SMS 接码平台 API(兼容 SMS-Activate 协议) +文档: https://hero-sms.com/cn/api +服务端: https://hero-sms.com/stubs/handler_api.php +""" +import re +import json +import requests +from typing import Optional, List, Dict, Any + +BASE_URL = "https://hero-sms.com/stubs/handler_api.php" +TIMEOUT = 30 +SERVICE_NOT_AVAILABLE_TOKENS = ( + "SERVICE_NOT_AVAILABLE", + "service not available", +) + + +def _get(base_url: str, api_key: str, action: str, **params) -> Optional[str]: + url = (base_url or BASE_URL).strip() + params["action"] = action + params["api_key"] = api_key + try: + r = requests.get(url, params=params, timeout=TIMEOUT) + r.raise_for_status() + return (r.text or "").strip() + except Exception: + return None + + +def get_balance(base_url: str, api_key: str) -> Optional[float]: + """查询余额. action=getBalance → 返回 ACCESS_BALANCE:数字""" + text = _get(base_url, api_key, "getBalance") + if not text or not text.startswith("ACCESS_BALANCE"): + return None + m = re.search(r"ACCESS_BALANCE[:\s]+([\d.]+)", text) + if m: + try: + return float(m.group(1)) + except ValueError: + pass + return None + + +def get_number( + base_url: str, + api_key: str, + service: str, + country: int = 0, + operator: Optional[str] = None, + max_price: Optional[float] = None, +) -> Optional[Dict[str, Any]]: + """ + 申请号码. action=getNumber&service=xxx&country=n + 返回 ACCESS_NUMBER:activationId:phoneNumber 或错误前缀(返回 {"error": "原始响应"}) + """ + params = {"service": service, "country": country} + if operator: + params["operator"] = operator + if max_price is not None: + params["maxPrice"] = max_price + text = _get(base_url, api_key, "getNumber", **params) + if not text: + return {"error": "无响应"} + if not text.startswith("ACCESS_NUMBER"): + return {"error": text} + parts = text.split(":") + if len(parts) >= 3: + try: + return { + "activation_id": int(parts[1]), + "phone_number": parts[2], + "raw": text, + } + except (ValueError, IndexError): + pass + return {"error": text} + + +def get_number_v2( + base_url: str, + api_key: str, + service: str, + country: int = 0, + max_price: Optional[float] = None, + **kwargs: Any, +) -> Optional[Dict[str, Any]]: + """ + 申请号码 V2。文档: action=getNumberV2&service=xxx&country=n&maxPrice=n。 + 支持两种返回格式:单条 { activationId, phoneNumber } 或 { data: [ { id, phone, ... } ] }。 + 统一返回 { activation_id, phone_number [, raw ] } 或 { error }。 + """ + try: + url = (base_url or BASE_URL).strip() + params = {"action": "getNumberV2", "api_key": api_key, "service": service, "country": country, **kwargs} + if max_price is not None: + params["maxPrice"] = max_price + r = requests.get(url, params=params, timeout=TIMEOUT) + r.raise_for_status() + raw_text = (r.text or "").strip() + if not raw_text: + return {"error": "接口返回为空,请检查 API 地址与网络"} + try: + data = json.loads(raw_text) + except json.JSONDecodeError: + if raw_text and len(raw_text) < 200 and "<" not in raw_text: + return {"error": raw_text} + return {"error": "接口返回非 JSON: " + (raw_text[:150] if raw_text else "空")} + + item = None + if isinstance(data, dict): + if data.get("activationId") is not None: + item = data + elif isinstance(data.get("data"), list) and len(data["data"]) > 0: + item = data["data"][0] + + if not item or not isinstance(item, dict): + err = (data.get("message") or data.get("error") if isinstance(data, dict) else None) or getattr(r, "text", str(data)) + return {"error": str(err)[:300] if err else "无可用号码或返回格式异常"} + + activation_id = item.get("activationId") or item.get("id") + phone = item.get("phoneNumber") or item.get("phone") or item.get("number") or "" + if activation_id is None: + return {"error": "返回中无 activationId/id"} + expired_at = item.get("activationEndTime") or item.get("expiredAt") or item.get("expired_at") or None + out = { + "activation_id": int(activation_id), + "phone_number": str(phone), + "raw": data, + } + if expired_at: + out["expired_at"] = str(expired_at).strip() + return out + except Exception as e: + return {"error": str(e)} + + +def _is_service_not_available_error(error: Optional[str]) -> bool: + text = str(error or "") + return any(token in text for token in SERVICE_NOT_AVAILABLE_TOKENS) + + +def _country_candidates_from_prices( + base_url: str, + api_key: str, + service: str, + max_price: Optional[float] = None, + limit: int = 8, +) -> List[int]: + prices = get_prices(base_url, api_key, service=service) + candidates = [] + if isinstance(prices, dict): + for country_id, val in prices.items(): + if not isinstance(val, dict): + continue + info = val.get(service) if isinstance(val.get(service), dict) else None + if not info: + continue + try: + count = int(info.get("count") or info.get("physicalCount") or 0) + cost = float(info.get("cost")) + cid = int(country_id) + except (TypeError, ValueError): + continue + if count <= 0: + continue + if max_price is not None and cost > max_price: + continue + candidates.append((cost, -count, cid)) + candidates.sort() + return [cid for _, _, cid in candidates[:limit]] + + +def get_number_auto( + base_url: str, + api_key: str, + service: str, + country: int = 0, + operator: Optional[str] = None, + max_price: Optional[float] = None, + country_limit: int = 8, +) -> Optional[Dict[str, Any]]: + """ + 申请号码,优先使用传入 country。 + 若 country=0 命中 Hero-SMS 的 SERVICE_NOT_AVAILABLE,则自动按价格/库存挑选具体国家重试。 + """ + + def _try_one(country_code: int) -> Optional[Dict[str, Any]]: + result = get_number( + base_url, + api_key, + service, + country=country_code, + operator=operator, + max_price=max_price, + ) + if result and not result.get("error"): + result["country"] = country_code + return result + fallback = get_number_v2( + base_url, + api_key, + service, + country=country_code, + max_price=max_price, + ) + if fallback and not fallback.get("error"): + fallback["country"] = country_code + return fallback + err = "" + if isinstance(result, dict) and result.get("error"): + err = str(result.get("error")) + elif isinstance(fallback, dict) and fallback.get("error"): + err = str(fallback.get("error")) + return {"error": err or "无可用号码"} + + first = _try_one(country) + if first and not first.get("error"): + return first + if country != 0 or not _is_service_not_available_error((first or {}).get("error")): + return first + + for country_code in _country_candidates_from_prices( + base_url, + api_key, + service, + max_price=max_price, + limit=country_limit, + ): + if country_code == country: + continue + result = _try_one(country_code) + if result and not result.get("error"): + result["auto_country_fallback"] = True + return result + return first + + +def get_status(base_url: str, api_key: str, activation_id: int) -> Optional[Dict[str, Any]]: + """ + 查询激活状态. action=getStatus&id=xxx + 返回 STATUS_WAIT_CODE | STATUS_OK:code | 其他 + """ + text = _get(base_url, api_key, "getStatus", id=activation_id) + if not text: + return None + if text == "STATUS_WAIT_CODE": + return {"status": "wait", "code": None} + if text.startswith("STATUS_OK"): + code = text.split(":", 1)[-1].strip() if ":" in text else "" + return {"status": "ok", "code": code or None} + return {"status": "raw", "raw": text} + + +def get_status_v2(base_url: str, api_key: str, activation_id: int) -> Optional[Dict[str, Any]]: + """查询状态 V2,返回 JSON(含 sms.code)。""" + try: + url = (base_url or BASE_URL).strip() + params = {"action": "getStatusV2", "api_key": api_key, "id": activation_id} + r = requests.get(url, params=params, timeout=TIMEOUT) + r.raise_for_status() + data = r.json() + if isinstance(data, dict): + sms = data.get("sms") or {} + code = sms.get("code") or (data.get("call") or {}).get("code") + return {"status": "ok" if code else "wait", "code": code, "data": data} + except Exception: + pass + return None + + +def set_status(base_url: str, api_key: str, activation_id: int, status: int) -> bool: + """ + 设置激活状态. action=setStatus&id=xxx&status=n + status: 1=已发短信(准备收码) 3=请求重发 6=完成 8=取消退款 + """ + text = _get(base_url, api_key, "setStatus", id=activation_id, status=status) + return text is not None and ("ACCESS" in (text or "") or text == "OK") + + +def get_countries(base_url: str, api_key: str) -> List[Dict[str, Any]]: + """国家列表。文档: action=getCountries。返回国家列表(含 id 等)。""" + try: + url = (base_url or BASE_URL).strip() + params = {"action": "getCountries", "api_key": api_key} + r = requests.get(url, params=params, timeout=TIMEOUT) + r.raise_for_status() + data = r.json() + if isinstance(data, list): + return data + except Exception: + pass + return [] + + +def get_services_list(base_url: str, api_key: str, country: int = 0, lang: str = "cn") -> List[Dict[str, Any]]: + """服务列表。文档: action=getServicesList&country=n&lang=cn。返回 services 数组。""" + try: + url = (base_url or BASE_URL).strip() + params = {"action": "getServicesList", "api_key": api_key, "country": country, "lang": lang} + r = requests.get(url, params=params, timeout=TIMEOUT) + r.raise_for_status() + data = r.json() + if isinstance(data, dict) and "services" in data: + return data.get("services") or [] + except Exception: + pass + return [] + + +def get_prices(base_url: str, api_key: str, service: Optional[str] = None, country: Optional[int] = None) -> Any: + """价格/库存. action=getPrices&service=xxx&country=n""" + try: + url = (base_url or BASE_URL).strip() + params = {"action": "getPrices", "api_key": api_key} + if service: + params["service"] = service + if country is not None: + params["country"] = country + r = requests.get(url, params=params, timeout=TIMEOUT) + r.raise_for_status() + return r.json() + except Exception: + return None diff --git a/Register_GPT_v0/web/backend/app/services/hotmail007.py b/Register_GPT_v0/web/backend/app/services/hotmail007.py new file mode 100644 index 0000000..cbc7d3a --- /dev/null +++ b/Register_GPT_v0/web/backend/app/services/hotmail007.py @@ -0,0 +1,113 @@ +""" +Hotmail007 邮箱 API 接入 +文档: https://hotmail007.com/zh/apiDoc +请求 host: https://gapi.hotmail007.com +""" +import requests +from typing import Optional, List, Dict, Any + +BASE_URL = "https://gapi.hotmail007.com" +TIMEOUT = 30 + +MAIL_TYPES = ("outlook", "hotmail", "hotmail Trusted", "outlook Trusted") + + +def get_balance(base_url: str, client_key: str) -> Optional[float]: + """查询余额. GET /api/user/balance?clientKey=xxx""" + url = (base_url or BASE_URL).rstrip("/") + "/api/user/balance" + try: + r = requests.get(url, params={"clientKey": client_key}, timeout=TIMEOUT) + r.raise_for_status() + data = r.json() + if data.get("success") and data.get("code") == 0: + return float(data.get("data", 0)) + except Exception: + pass + return None + + +def get_stock(base_url: str, mail_type: Optional[str] = None) -> Optional[int]: + """查询库存. GET /api/mail/getStock?mailType=xxx (mailType 可选)""" + url = (base_url or BASE_URL).rstrip("/") + "/api/mail/getStock" + params = {} + if mail_type: + params["mailType"] = mail_type + try: + r = requests.get(url, params=params or None, timeout=TIMEOUT) + r.raise_for_status() + data = r.json() + if data.get("success") and data.get("code") == 0: + return int(data.get("data", 0)) + except Exception: + pass + return None + + +def get_mail( + base_url: str, + client_key: str, + quantity: int, + mail_type: Optional[str] = None, +) -> List[Dict[str, Any]]: + """ + 拉取邮箱. GET /api/mail/getMail?clientKey=xxx&mailType=xxx&quantity=n + 返回 data 为 ["Account:Password:Refresh_token:Client_id", ...] + 解析为 [{"email","password","refresh_token","client_id"}, ...] + """ + url = (base_url or BASE_URL).rstrip("/") + "/api/mail/getMail" + params = {"clientKey": client_key, "quantity": quantity} + if mail_type: + params["mailType"] = mail_type + out = [] + try: + r = requests.get(url, params=params, timeout=TIMEOUT) + r.raise_for_status() + data = r.json() + if not data.get("success") or data.get("code") != 0: + return out + raw_list = data.get("data") or [] + for raw in raw_list: + if not isinstance(raw, str): + continue + # Account:Password:Refresh_token:Client_id,其中 Refresh_token 可能含冒号 + parts = raw.split(":") + if len(parts) < 4: + continue + email = parts[0].strip() + password = parts[1].strip() + client_id = parts[-1].strip() + refresh_token = ":".join(parts[2:-1]).strip() + out.append({ + "email": email, + "password": password, + "refresh_token": refresh_token, + "client_id": client_id, + }) + except Exception: + pass + return out + + +def get_first_mail( + base_url: str, + client_key: str, + account: str, + folder: str = "inbox", +) -> Optional[Dict[str, Any]]: + """ + 获取该邮箱最新一封邮件. GET /v1/mail/getFirstMail + account 格式: email:password:refresh_token:client_id + folder: inbox / junkemail + """ + url = (base_url or BASE_URL).rstrip("/") + "/v1/mail/getFirstMail" + params = {"clientKey": client_key, "account": account, "folder": folder} + try: + r = requests.get(url, params=params, timeout=TIMEOUT) + r.raise_for_status() + data = r.json() + if not data.get("success") or data.get("code") != 0: + return None + return data.get("data") + except Exception: + pass + return None diff --git a/Register_GPT_v0/web/backend/app/services/otp_resolver.py b/Register_GPT_v0/web/backend/app/services/otp_resolver.py new file mode 100644 index 0000000..1c36fa9 --- /dev/null +++ b/Register_GPT_v0/web/backend/app/services/otp_resolver.py @@ -0,0 +1,161 @@ +""" +注册 OTP:通过 Hotmail007 拉信,从邮件正文/标题解析 6 位验证码。 +与参考 chatgpt_register.py 对齐:多模式优先匹配(code:、verify、>xxx<、纯 6 位),只返回 6 位数字。 +""" +import re +import time +from typing import Callable, Optional + +from app.services.hotmail007 import get_first_mail + +# 与参考一致:优先匹配 OpenAI 邮件里常见格式,再回退到任意 6 位 +_OTP_PATTERNS = [ + r">\s*(\d{6})\s*<", # HTML 中 >123456< + r"(\d{6})\s*\n", # 行末 6 位 + r"code[:\s]+(\d{6})", # code: 123456 / code 123456 + r"verify.*?(\d{6})", # verify...123456 + r"\b(\d{6})\b", # 独立 6 位数字 + r"(\d{6})", # 任意 6 位(最后回退) +] + + +def _extract_otp_from_mail(data: Optional[dict]) -> Optional[str]: + """从 get_first_mail 返回的 data 中解析 6 位验证码,与参考提取顺序一致。""" + if not data or not isinstance(data, dict): + return None + texts = [] + for key in ("Subject", "subject", "Text", "text", "Body", "body", "Html", "html", "Content", "content"): + v = data.get(key) + if isinstance(v, str) and v.strip(): + texts.append(v) + combined = " ".join(texts) + for pattern in _OTP_PATTERNS: + m = re.search(pattern, combined, re.IGNORECASE | re.DOTALL) + if m: + raw = m.group(1) + digits = re.sub(r"\D", "", raw) + if len(digits) >= 6: + return digits[:6] + return None + + +def get_otp_for_email( + base_url: str, + client_key: str, + account_str: str, + timeout_sec: float = 120, + interval_sec: float = 5, + folder: str = "inbox", + folders=None, + exclude_codes=None, + stop_check=None, +) -> Optional[str]: + """ + 轮询该邮箱最新一封邮件,解析 6 位验证码,超时返回 None。 + stop_check: 可调用对象,返回 True 时立即结束并返回 None。 + """ + if not client_key or not account_str: + return None + excluded = set(exclude_codes or []) + folder_list = [f for f in (folders or [folder]) if isinstance(f, str) and f.strip()] + if not folder_list: + folder_list = ["inbox"] + deadline = time.monotonic() + timeout_sec + while time.monotonic() < deadline: + if stop_check and callable(stop_check) and stop_check(): + return None + for current_folder in folder_list: + data = get_first_mail(base_url, client_key, account_str, folder=current_folder) + otp = _extract_otp_from_mail(data) + if otp and otp not in excluded: + return otp + for _ in range(int(interval_sec)): + if stop_check and callable(stop_check) and stop_check(): + return None + time.sleep(1) + return None + + +def peek_latest_otps( + base_url: str, + client_key: str, + account_str: str, + folder: str = "inbox", + folders=None, +) -> set[str]: + """ + 读取指定文件夹当前“最新一封”邮件里的验证码,用于在发起新 OTP 前先排除旧码。 + """ + if not client_key or not account_str: + return set() + folder_list = [f for f in (folders or [folder]) if isinstance(f, str) and f.strip()] + if not folder_list: + folder_list = ["inbox"] + out: set[str] = set() + for current_folder in folder_list: + data = get_first_mail(base_url, client_key, account_str, folder=current_folder) + otp = _extract_otp_from_mail(data) + if otp: + out.add(otp) + return out + + +def build_otp_fetcher( + base_url: str, + client_key: str, + account_str: str, + timeout_sec: float = 120, + interval_sec: float = 5, + stop_check=None, +) -> Callable[[], Optional[str]]: + """ + 构造带状态的 OTP 获取器。 + - 自动排除已使用过的验证码 + - 可在登录二次验证前预先排除收件箱/垃圾箱当前顶上的旧验证码 + """ + used_otps: set[str] = set() + + def seed_current_otps(folder: str = "inbox", folders=None) -> set[str]: + current = peek_latest_otps( + base_url, + client_key, + account_str, + folder=folder, + folders=folders, + ) + used_otps.update(current) + return set(current) + + def get_used_otps() -> set[str]: + return set(used_otps) + + def get_otp_fn() -> Optional[str]: + search_folders = ["inbox"] if not used_otps else ["junkemail", "inbox"] + otp = get_otp_for_email( + base_url, + client_key, + account_str, + timeout_sec=timeout_sec, + interval_sec=interval_sec, + folders=search_folders, + exclude_codes=used_otps, + stop_check=stop_check, + ) + # 首次取码可兜底一次;后续步骤必须拿“新码”,避免复用旧码导致 401。 + if not otp and not used_otps: + otp = get_otp_for_email( + base_url, + client_key, + account_str, + timeout_sec=20, + interval_sec=3, + folders=["inbox", "junkemail"], + stop_check=stop_check, + ) + if otp: + used_otps.add(otp) + return otp + + setattr(get_otp_fn, "seed_current_otps", seed_current_otps) + setattr(get_otp_fn, "get_used_otps", get_used_otps) + return get_otp_fn diff --git a/Register_GPT_v0/web/backend/app/services/phone_bind_runner.py b/Register_GPT_v0/web/backend/app/services/phone_bind_runner.py new file mode 100644 index 0000000..55e01d6 --- /dev/null +++ b/Register_GPT_v0/web/backend/app/services/phone_bind_runner.py @@ -0,0 +1,460 @@ +# -*- coding: utf-8 -*- +""" +开始绑定手机:只从账号管理取已具备 Sora 资格的账号 +(Registered+Sora、has_sora=1、sora_enabled=1、phone_bound=0 且有 RT/AT), +从手机号管理取可用号码,执行 Sora 激活 + enroll/start -> 轮询验证码 -> enroll/finish, +更新 accounts.phone_bound、phone_numbers.used_count。 +""" +import re +import random +import threading +from concurrent.futures import ThreadPoolExecutor, as_completed +from datetime import datetime, timedelta + +from app.database import get_db, init_db +from app.services import hero_sms +from app.services.otp_resolver import build_otp_fetcher + +# 绑定任务状态(与 register 类似,独立 stop 标志) +_phone_bind_running = False +_phone_bind_task_id = None +_phone_bind_heartbeat = None +_phone_bind_stop = False +_phone_bind_lock = threading.Lock() + +PHONE_CODE_POLL_INTERVAL = 5 +PHONE_CODE_MAX_RETRIES = 60 +PHONE_BIND_PREFERRED_COUNTRY = 52 +PHONE_BIND_PREFERRED_PHONE_PREFIXES = ("66",) + + +def is_phone_bind_stop_requested() -> bool: + with _phone_bind_lock: + return _phone_bind_stop + + +def set_phone_bind_stop(value: bool) -> None: + with _phone_bind_lock: + global _phone_bind_stop + _phone_bind_stop = value + + +def set_phone_bind_task_started(task_id: str) -> bool: + """返回 False 表示已在运行。""" + with _phone_bind_lock: + global _phone_bind_running, _phone_bind_task_id, _phone_bind_heartbeat, _phone_bind_stop + if _phone_bind_running: + return False + _phone_bind_running = True + _phone_bind_task_id = task_id + _phone_bind_heartbeat = datetime.utcnow().isoformat() + "Z" + _phone_bind_stop = False + return True + + +def get_phone_bind_status() -> dict: + with _phone_bind_lock: + return { + "running": _phone_bind_running, + "task_id": _phone_bind_task_id, + "heartbeat": _phone_bind_heartbeat, + } + + +# 接码平台未返回到期时间时,默认有效期(分钟),与 sms_api 一致 +_PHONE_DEFAULT_EXPIRE_MINUTES = 20 + + +def _get_settings(): + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + "SELECT key, value FROM system_settings WHERE key IN (" + "'sms_api_url', 'sms_api_key', 'proxy_url', 'sms_openai_service', 'sms_max_price', 'phone_bind_limit'," + "'email_api_url', 'email_api_key')" + ) + rows = c.fetchall() + out = {} + for k, v in rows: + out[k] = (v or "").strip() + out.setdefault("sms_api_url", "https://hero-sms.com/stubs/handler_api.php") + return out + + +def _log(task_id: str, level: str, message: str): + try: + with get_db() as conn: + c = conn.cursor() + c.execute( + "INSERT INTO run_logs (task_id, level, message, created_at) VALUES (?, ?, ?, ?)", + (task_id, level, (message or "")[:500], datetime.now().strftime("%Y-%m-%d %H:%M:%S")), + ) + except Exception: + pass + + +def fetch_accounts_to_bind(limit: int = 50, exclude_ids=None): + """账号管理:仅挑已具备 Sora 资格且未绑手机、仍可进入轮换池的账号。""" + init_db() + with get_db() as conn: + c = conn.cursor() + where = [ + "phone_bound = 0", + "COALESCE(status, '') = 'Registered+Sora'", + "COALESCE(has_sora, 0) = 1", + "COALESCE(sora_enabled, 1) = 1", + """( + (refresh_token IS NOT NULL AND refresh_token != '') + OR (access_token IS NOT NULL AND access_token != '') + )""", + ] + params = [] + exclude_ids = [int(x) for x in (exclude_ids or []) if str(x).strip()] + if exclude_ids: + where.append("id NOT IN ({})".format(",".join(["?"] * len(exclude_ids)))) + params.extend(exclude_ids) + params.append(limit) + c.execute( + f"""SELECT id, email, password, refresh_token, access_token, proxy FROM accounts + WHERE {' AND '.join(where)} + ORDER BY id ASC LIMIT ?""", + tuple(params), + ) + return c.fetchall() + + +def _load_email_mailbox(email: str): + account_email = (email or "").strip() + if not account_email: + return None + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + """SELECT email, password, uuid, token + FROM emails + WHERE LOWER(TRIM(email)) = LOWER(TRIM(?)) + LIMIT 1""", + (account_email,), + ) + row = c.fetchone() + if not row: + return None + return { + "email": row[0] or "", + "password": row[1] or "", + "uuid": row[2] or "", + "token": row[3] or "", + } + + +def _build_account_otp_fetcher(email: str): + mailbox = _load_email_mailbox(email) + if not mailbox: + return None + settings = _get_settings() + base_url = (settings.get("email_api_url") or "https://gapi.hotmail007.com").rstrip("/") + client_key = settings.get("email_api_key") or "" + if not client_key: + return None + account_str = f"{mailbox['email']}:{mailbox['password']}:{mailbox['token']}:{mailbox['uuid']}" + return build_otp_fetcher(base_url, client_key, account_str, timeout_sec=120, interval_sec=5, stop_check=is_phone_bind_stop_requested) + + +def fetch_phones_available(limit: int = 50): + """手机号管理:仅挑当前已验证更稳定的泰国号。""" + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + """SELECT id, phone, activation_id, max_use_count, used_count FROM phone_numbers + WHERE activation_id IS NOT NULL AND used_count < max_use_count + AND ( + COALESCE(remark, '') LIKE ? + OR REPLACE(REPLACE(COALESCE(phone, ''), '+', ''), ' ', '') LIKE ? + ) + ORDER BY id ASC LIMIT ?""", + (f"%country={PHONE_BIND_PREFERRED_COUNTRY}%", f"{PHONE_BIND_PREFERRED_PHONE_PREFIXES[0]}%", limit), + ) + return c.fetchall() + + +def _fetch_numbers_from_api(task_id: str, max_try: int = 3) -> int: + """无可用手机号时从接码 API 拉取泰国号并写入 phone_numbers,返回成功写入条数。""" + settings = _get_settings() + base = settings.get("sms_api_url") or "https://hero-sms.com/stubs/handler_api.php" + key = settings.get("sms_api_key") or "" + if not key: + _log(task_id, "warning", "未配置接码 API KEY,无法自动拉取手机号") + return 0 + service = (settings.get("sms_openai_service") or "openai").strip() or "openai" + try: + max_price = float(settings.get("sms_max_price") or "0") + except (TypeError, ValueError): + max_price = 0 + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute("SELECT value FROM system_settings WHERE key = ?", ("phone_bind_limit",)) + row = c.fetchone() + limit = int(row[0]) if row and row[0] else 1 + country = PHONE_BIND_PREFERRED_COUNTRY + inserted = 0 + for _ in range(max_try): + result = hero_sms.get_number_auto(base, key, service, country, max_price=max_price) + if not result: + break + if result.get("error"): + _log(task_id, "warning", f"自动拉取泰国手机号失败: {str(result['error'])[:200]}") + break + expired_at = result.get("expired_at") + if not (expired_at and str(expired_at).strip()): + default_end = (datetime.utcnow() + timedelta(minutes=_PHONE_DEFAULT_EXPIRE_MINUTES)).strftime("%Y-%m-%d %H:%M:%S") + expired_at = default_end + else: + raw = str(expired_at).strip() + if "T" in raw: + raw = raw.replace("Z", "").split(".")[0].replace("T", " ") + expired_at = raw + with get_db() as conn: + c = conn.cursor() + country_code = result.get("country") + remark = "Hero-SMS(自动)" + if country_code not in (None, "", 0): + remark = f"Hero-SMS(自动 country={country_code})" + c.execute( + "INSERT INTO phone_numbers (phone, activation_id, max_use_count, remark, expired_at) VALUES (?, ?, ?, ?, ?)", + (result["phone_number"], result["activation_id"], limit, remark, expired_at), + ) + inserted += 1 + suffix = f" country={country_code}" if country_code not in (None, "", 0) else "" + _log(task_id, "info", "自动拉取泰国手机号: " + str(result["phone_number"]) + suffix) + return inserted + + +def _ensure_phone_inventory(task_id: str, needed: int) -> list: + """确保本轮至少拿到 needed 条可用泰国号,拿不到则返回当前能拿到的全部。""" + needed = max(0, int(needed or 0)) + phones = fetch_phones_available(limit=max(needed, 1)) + while len(phones) < needed: + missing = needed - len(phones) + _log(task_id, "info", f"无足够泰国手机号,尝试自动补拉 {missing} 条") + inserted = _fetch_numbers_from_api(task_id, max_try=max(missing, 1)) + if inserted <= 0: + break + phones = fetch_phones_available(limit=max(needed, 1)) + return phones + + +def run_one_phone_bind(task_id: str, account_id: int, email: str, account_password: str, refresh_token: str, access_token: str, account_proxy: str, + phone_id: int, phone: str, activation_id: int, + sms_base: str, sms_key: str, proxy_url: str) -> bool: + """ + 单条绑定:拿 AT -> Sora 激活 -> enroll/start -> 轮询验证码 -> enroll/finish -> 更新 DB。 + 返回 True 表示成功。 + """ + from app.registration_env import inject_registration_modules + inject_registration_modules() + + import protocol_sora_phone as sora_phone + + def log(msg): + _log(task_id, "info", msg) + + def close_enroll_ctx(ctx): + if not isinstance(ctx, dict): + return + sess = ctx.get("web_session") + if sess is None: + return + try: + sess.close() + except Exception: + pass + + get_otp_fn = _build_account_otp_fetcher(email) + if get_otp_fn: + seed_current_otps = getattr(get_otp_fn, "seed_current_otps", None) + if callable(seed_current_otps): + try: + seeded = seed_current_otps(folders=["junkemail", "inbox"]) + except Exception: + seeded = set() + if seeded: + log(f"[绑定] {email} 预排除旧 OTP: {','.join(sorted(seeded))}") + + at = (access_token or "").strip() + if not at and (refresh_token or "").strip(): + log(f"[绑定] {email} RT 换 AT...") + out = sora_phone.rt_to_at_mobile(refresh_token.strip(), proxy_url=proxy_url or account_proxy, log_fn=log) + at = (out.get("access_token") or "").strip() + new_rt = out.get("refresh_token") + if new_rt and isinstance(new_rt, str): + try: + with get_db() as conn: + c = conn.cursor() + c.execute("UPDATE accounts SET refresh_token = ? WHERE id = ?", (new_rt.strip(), account_id)) + except Exception: + pass + if not at: + log(f"[绑定] {email} 无 AT,跳过") + return False + + if is_phone_bind_stop_requested(): + return False + + log(f"[绑定] {email} Sora 激活...") + if not sora_phone.sora_ensure_activated(at, proxy_url=proxy_url or account_proxy, log_fn=log): + log(f"[绑定] {email} Sora 激活失败") + return False + + if is_phone_bind_stop_requested(): + return False + + log(f"[绑定] {email} 发送验证码 -> {phone}") + ok, err, enroll_ctx = sora_phone.sora_phone_enroll_start( + at, + phone, + proxy_url=proxy_url or account_proxy, + log_fn=log, + login_email=email, + login_password=(account_password or "").strip(), + get_otp_fn=get_otp_fn, + ) + if not ok: + if err == "phone_used": + log(f"[绑定] 手机号已被使用: {phone}") + elif err == "reauth_failed": + log(f"[绑定] {email} recent reauth 失败") + elif err == "sms_unavailable": + log(f"[绑定] {email} 当前未开放 SMS MFA") + elif err == "invalid_request": + log(f"[绑定] {email} 手机号参数被上游拒绝: {phone}") + return False + + code = None + for i in range(PHONE_CODE_MAX_RETRIES): + if is_phone_bind_stop_requested(): + close_enroll_ctx(enroll_ctx) + return False + out = hero_sms.get_status_v2(sms_base, sms_key, activation_id) + if out and out.get("code"): + raw = out.get("code") + m = re.search(r"\d{6}", str(raw)) + if m: + code = m.group() + break + import time + time.sleep(PHONE_CODE_POLL_INTERVAL) + + if not code: + log(f"[绑定] {email} 获取验证码超时") + close_enroll_ctx(enroll_ctx) + return False + + log(f"[绑定] {email} 提交验证码...") + if not sora_phone.sora_phone_enroll_finish( + at, + phone, + code, + proxy_url=proxy_url or account_proxy, + log_fn=log, + context=enroll_ctx, + ): + log(f"[绑定] {email} 验证码提交失败") + close_enroll_ctx(enroll_ctx) + return False + + with get_db() as conn: + c = conn.cursor() + c.execute("UPDATE accounts SET phone_bound = 1 WHERE id = ?", (account_id,)) + c.execute("UPDATE phone_numbers SET used_count = used_count + 1 WHERE id = ?", (phone_id,)) + close_enroll_ctx(enroll_ctx) + log(f"[绑定] 成功 {email} -> {phone}") + return True + + +def run_phone_bind_loop(task_id: str, max_count: int = None): + """后台循环:取待绑定账号与可用手机号,按 max_count 并发执行直到达到目标成功数或停止。""" + global _phone_bind_running, _phone_bind_heartbeat, _phone_bind_stop + settings = _get_settings() + sms_base = settings.get("sms_api_url") or "https://hero-sms.com/stubs/handler_api.php" + sms_key = settings.get("sms_api_key") or "" + proxy_url = settings.get("proxy_url") or "" + if not sms_key: + _log(task_id, "error", "请先在系统设置中配置手机号接码 API KEY") + with _phone_bind_lock: + _phone_bind_running = False + return + + processed = 0 + success_count = 0 + skipped_account_ids = set() + try: + while True: + if is_phone_bind_stop_requested(): + _log(task_id, "info", "已请求停止绑定") + break + if max_count is not None and success_count >= max_count: + _log(task_id, "info", f"已达到目标绑定数量 {max_count}") + break + + remaining_target = max_count - success_count if max_count is not None else 1 + batch_size = max(1, int(remaining_target)) + + accounts = fetch_accounts_to_bind(limit=max(batch_size * 2, batch_size), exclude_ids=skipped_account_ids) + if not accounts: + if skipped_account_ids: + _log(task_id, "info", "无更多可尝试账号(本轮失败账号已跳过)") + else: + _log(task_id, "info", "无待绑定账号(需满足 Registered+Sora、has_sora=1、sora_enabled=1、phone_bound=0 且有 RT/AT)") + break + + batch_size = min(batch_size, len(accounts)) + phones = _ensure_phone_inventory(task_id, batch_size) + if not phones: + _log(task_id, "info", "无可用泰国手机号(已尝试自动拉取仍无)") + break + + batch_size = min(batch_size, len(phones)) + accounts = accounts[:batch_size] + phones = phones[:batch_size] + + with _phone_bind_lock: + _phone_bind_heartbeat = datetime.utcnow().isoformat() + "Z" + + if batch_size > 1: + _log(task_id, "info", f"开始并发绑定,本轮并发 {batch_size} 条,目标剩余 {remaining_target}") + + with ThreadPoolExecutor(max_workers=batch_size) as executor: + future_map = {} + for acc, ph in zip(accounts, phones): + account_id, email, account_password, rt, at, account_proxy = acc[0], acc[1], acc[2] or "", acc[3], acc[4], acc[5] or "" + phone_id, phone, act_id = ph[0], ph[1], ph[2] + future = executor.submit( + run_one_phone_bind, + task_id, + account_id, email, account_password, rt or "", at or "", account_proxy, + phone_id, phone, act_id, + sms_base, sms_key, proxy_url, + ) + future_map[future] = (account_id, email, phone) + + for future in as_completed(future_map): + account_id, email, phone = future_map[future] + processed += 1 + try: + ok = bool(future.result()) + except Exception as exc: + ok = False + _log(task_id, "error", f"单条绑定线程异常 {email} -> {phone}: {exc}") + if ok: + success_count += 1 + else: + skipped_account_ids.add(account_id) + _log(task_id, "warning", f"单条绑定失败,跳过账号继续下一条: {email} -> {phone}") + finally: + with _phone_bind_lock: + _phone_bind_running = False + _log(task_id, "info", f"绑定任务结束 处理 {processed} 条 成功 {success_count} 条") diff --git a/Register_GPT_v0/web/backend/app/services/registration_runner.py b/Register_GPT_v0/web/backend/app/services/registration_runner.py new file mode 100644 index 0000000..72170eb --- /dev/null +++ b/Register_GPT_v0/web/backend/app/services/registration_runner.py @@ -0,0 +1,483 @@ +# -*- coding: utf-8 -*- +""" +单条注册任务运行器:从 DB 取未注册邮箱与配置,调 protocol_register,落库 accounts/run_logs,更新 last_run_success/fail。 +不实现 8 步协议,仅「取配置 → 调 register_one_protocol / activate_sora → 落库」。 +环境变量:PRINT_STEP_LOGS=1 时步骤日志同时输出到 stdout,便于终端跑测。 +""" +import os +import random +from datetime import datetime +from typing import Callable, Optional, Tuple + +from app.database import get_db, init_db +from app.registration_env import inject_registration_modules, set_task_config, clear_task_config +from app.registration_state import is_stop_requested +from app.services.otp_resolver import build_otp_fetcher + +# 首次执行注册前注入 config/utils,再懒加载 protocol_register,避免未注入时导入 +_injected = False + + +def _ensure_injected(): + global _injected + if not _injected: + inject_registration_modules() + _injected = True + + +def _get_registration_settings() -> dict: + """从 system_settings 读取注册所需配置。""" + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + """SELECT key, value FROM system_settings WHERE key IN ( + 'thread_count', 'retry_count', 'proxy_url', 'email_api_url', 'email_api_key', + 'oauth_client_id', 'oauth_redirect_uri' + )""" + ) + rows = c.fetchall() + out = {} + for k, v in rows: + out[k] = (v or "").strip() + out.setdefault("retry_count", "2") + out.setdefault("thread_count", "1") + return out + + +def fetch_one_unregistered_email(conn, order_random: bool = False) -> Optional[Tuple]: + """取一条未注册邮箱。返回 (id, email, password, uuid, token) 或 None。order_random=True 时随机取一条便于轮换邮箱。""" + c = conn.cursor() + order = "ORDER BY RANDOM()" if order_random else "" + c.execute( + f"""SELECT e.id, e.email, e.password, e.uuid, e.token + FROM emails e + LEFT JOIN accounts a ON LOWER(TRIM(e.email)) = LOWER(TRIM(a.email)) + WHERE a.email IS NULL + AND e.email IS NOT NULL + AND TRIM(e.email) != '' + AND COALESCE(e.remark, '') NOT LIKE '%[skip_bad_request]%' + {order} + LIMIT 1""" + ) + row = c.fetchone() + return tuple(row) if row else None + + +def fetch_unregistered_emails(limit: int = 10): + """取最多 limit 条未注册邮箱(随机顺序),用于多线程分配。返回 [(id, email, password, uuid, token), ...]。""" + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + """SELECT e.id, e.email, e.password, e.uuid, e.token + FROM emails e + LEFT JOIN accounts a ON LOWER(TRIM(e.email)) = LOWER(TRIM(a.email)) + WHERE a.email IS NULL + AND e.email IS NOT NULL + AND TRIM(e.email) != '' + AND COALESCE(e.remark, '') NOT LIKE '%[skip_bad_request]%' + ORDER BY RANDOM() + LIMIT ?""", + (max(1, limit),), + ) + rows = c.fetchall() + return [tuple(r) for r in rows] + + +def _default_user_info() -> dict: + """协议需要的 name, year, month, day(字符串)。""" + y = random.randint(1985, 2000) + m = random.randint(1, 12) + d = random.randint(1, 28) + return { + "name": "User", + "year": str(y), + "month": str(m).zfill(2), + "day": str(d).zfill(2), + } + + +def _run_one_registration( + email_id: int, + email: str, + password: str, + uuid_val: str, + token: str, + settings: dict, + task_id: str, +) -> Tuple[bool, Optional[str], Optional[dict], Callable[[], Optional[str]]]: + """ + 执行单条注册(不重试)。返回 (success, status_extra, tokens, get_otp_fn)。 + """ + _ensure_injected() + import protocol_register as pr # 注入后导入 + + base = (settings.get("email_api_url") or "https://gapi.hotmail007.com").rstrip("/") + key = settings.get("email_api_key") or "" + # 支持多行 proxy_url:每行一个代理,随机选用其一 + proxy_raw = (settings.get("proxy_url") or "").strip() + proxy_lines = [p.strip() for p in proxy_raw.splitlines() if p.strip()] + proxy_url = random.choice(proxy_lines) if proxy_lines else None + + account_str = f"{email}:{password or ''}:{token or ''}:{uuid_val or ''}" + get_otp_fn = build_otp_fetcher( + base, + key, + account_str, + timeout_sec=120, + interval_sec=5, + stop_check=is_stop_requested, + ) + + _print_steps = os.environ.get("PRINT_STEP_LOGS", "").strip().lower() in ("1", "true", "yes") + + def _step_log(msg: str) -> None: + try: + with get_db() as conn: + c = conn.cursor() + created = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + c.execute( + "INSERT INTO run_logs (task_id, level, message, created_at) VALUES (?, ?, ?, ?)", + (task_id, "info", (msg or "")[:500], created), + ) + except Exception: + pass + if _print_steps and msg: + print(f" [step] {msg}", flush=True) + + set_task_config( + proxy_url=proxy_url, + timeout=60, + http_max_retries=5, + oauth_client_id=settings.get("oauth_client_id") or "", + oauth_redirect_uri=settings.get("oauth_redirect_uri") or "", + ) + try: + result = pr.register_one_protocol( + email, + password, + token or "", + get_otp_fn, + _default_user_info(), + proxy_url=proxy_url, + step_log_fn=_step_log, + stop_check=is_stop_requested, + ) + except pr.RetryException as e: + with get_db() as conn: + c = conn.cursor() + created = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + c.execute( + "INSERT INTO run_logs (task_id, level, message, created_at) VALUES (?, ?, ?, ?)", + (task_id, "info", f"409 会话已清理,将重试 {email}: {e!s}", created), + ) + return False, str(e), None, get_otp_fn + except Exception as e: + with get_db() as conn: + c = conn.cursor() + created = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + c.execute( + "INSERT INTO run_logs (task_id, level, message, created_at) VALUES (?, ?, ?, ?)", + (task_id, "error", f"注册异常 {email}: {e!s}", created), + ) + return False, str(e), None, get_otp_fn + finally: + clear_task_config() + + # result: (email, password, success[, status_extra[, tokens]]) + success = bool(result[2]) if len(result) > 2 else False + status_extra = result[3] if len(result) > 3 else None + tokens = result[4] if len(result) > 4 else None + if isinstance(tokens, dict): + pass + else: + tokens = None + return success, status_extra, tokens, get_otp_fn + + +# 密码规则:与 protocol_register.PASSWORD_MIN_LENGTH 一致,OpenAI 要求最少 12 位,建议含大小写+数字+符号 +PASSWORD_MIN_LENGTH = 12 + + +def _random_password() -> str: + """生成随机密码:至少 12 位,含大小写、数字、符号,满足 OpenAI 协议要求。""" + import string + upper = random.choices(string.ascii_uppercase, k=2) + lower = random.choices(string.ascii_lowercase, k=2) + digit = random.choices(string.digits, k=2) + symbol = random.choices("!@#$%&*", k=2) + rest = random.choices(string.ascii_letters + string.digits + "!@#$%&*", k=PASSWORD_MIN_LENGTH - 8) + parts = upper + lower + digit + symbol + rest + random.shuffle(parts) + return "".join(parts) + + +def run_one_with_retry( + email_id: int, + email: str, + password: str, + uuid_val: str, + token: str, + settings: dict, + task_id: str, +) -> bool: + """ + 单条任务带重试(1~5 次),成功写 accounts、run_logs、last_run_success,失败写 run_logs、last_run_fail。 + 返回是否最终成功。 + """ + pwd = (password or "").strip() or _random_password() + if len(pwd) < PASSWORD_MIN_LENGTH: + pwd = _random_password() + retry_count = max(1, min(5, int(settings.get("retry_count") or "2"))) + last_error = None + _now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + with get_db() as conn: + c = conn.cursor() + c.execute( + "INSERT INTO run_logs (task_id, level, message, created_at) VALUES (?, ?, ?, ?)", + (task_id, "info", f"正在注册账号 {email}", _now), + ) + if is_stop_requested(): + with get_db() as conn: + c = conn.cursor() + created = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + c.execute( + "INSERT INTO run_logs (task_id, level, message, created_at) VALUES (?, ?, ?, ?)", + (task_id, "info", f"任务已停止,跳过 {email}", created), + ) + return False + for attempt in range(retry_count): + if is_stop_requested(): + with get_db() as conn: + c = conn.cursor() + created = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + c.execute( + "INSERT INTO run_logs (task_id, level, message, created_at) VALUES (?, ?, ?, ?)", + (task_id, "info", f"任务已停止,跳过 {email}", created), + ) + return False + use_settings = settings + _restore_direct_auth = None + err = str(last_error or "").lower() + retry_no_proxy = ( + attempt == 1 + and last_error + and ( + "409" in str(last_error) + or "invalid_state" in err + or "invalid_auth_step" in err + or "invalid authorization step" in err + or "tls" in err + or "connection timed out" in err + or "curl: (28)" in str(last_error) + or "curl: (35)" in str(last_error) + ) + ) + if retry_no_proxy: + use_settings = {**settings, "proxy_url": ""} + _restore_direct_auth = os.environ.get("USE_DIRECT_AUTH") + os.environ["USE_DIRECT_AUTH"] = "1" + print("[*] retry: no proxy + USE_DIRECT_AUTH=1", flush=True) + with get_db() as conn: + c = conn.cursor() + created = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + c.execute( + "INSERT INTO run_logs (task_id, level, message, created_at) VALUES (?, ?, ?, ?)", + (task_id, "info", "重试:无代理 + 直连 auth", created), + ) + try: + success, status_extra, tokens, get_otp_fn = _run_one_registration( + email_id, email, pwd, uuid_val, token, use_settings, task_id + ) + finally: + if _restore_direct_auth is not None: + if _restore_direct_auth: + os.environ["USE_DIRECT_AUTH"] = _restore_direct_auth + else: + os.environ.pop("USE_DIRECT_AUTH", None) + if success: + _ensure_injected() + import protocol_register as pr + proxy_raw = (settings.get("proxy_url") or "").strip() + proxy_lines = [p.strip() for p in proxy_raw.splitlines() if p.strip()] + same_proxy = random.choice(proxy_lines) if proxy_lines else None + sora_ok = False + sora_tokens = dict(tokens or {}) if isinstance(tokens, dict) else {} + + def _sora_step_log(msg: str): + try: + with get_db() as conn: + c = conn.cursor() + c.execute( + "INSERT INTO run_logs (task_id, level, message, created_at) VALUES (?, ?, ?, ?)", + (task_id, "info", (msg or "")[:500], datetime.now().strftime("%Y-%m-%d %H:%M:%S")), + ) + except Exception: + pass + + try: + sora_ok = pr.activate_sora( + sora_tokens, + email, + proxy_url=same_proxy, + step_log_fn=_sora_step_log, + account_password=pwd, + get_otp_fn=get_otp_fn, + ) + except Exception as exc: + with get_db() as conn: + c = conn.cursor() + c.execute( + "INSERT INTO run_logs (task_id, level, message, created_at) VALUES (?, ?, ?, ?)", + (task_id, "error", f"Sora2 激活异常 {email}: {exc!s}", datetime.now().strftime("%Y-%m-%d %H:%M:%S")), + ) + + if (not sora_ok) and same_proxy: + _sora_step_log("[*] Sora2 激活失败,尝试直连再次补激活") + try: + sora_ok = pr.activate_sora( + sora_tokens, + email, + proxy_url="", + step_log_fn=_sora_step_log, + account_password=pwd, + get_otp_fn=get_otp_fn, + ) + except Exception as exc: + with get_db() as conn: + c = conn.cursor() + c.execute( + "INSERT INTO run_logs (task_id, level, message, created_at) VALUES (?, ?, ?, ?)", + (task_id, "error", f"Sora2 直连补激活异常 {email}: {exc!s}", datetime.now().strftime("%Y-%m-%d %H:%M:%S")), + ) + try: + rt = "" + at = "" + if isinstance(sora_tokens, dict): + rt = sora_tokens.get("refresh_token") or "" + if not rt and isinstance(sora_tokens.get("session"), dict): + rt = (sora_tokens.get("session") or {}).get("refresh_token") or "" + rt = (rt or "").strip() if rt else "" + at = (sora_tokens.get("access_token") or "").strip() or None + with get_db() as conn: + c = conn.cursor() + c.execute( + """INSERT OR REPLACE INTO accounts (email, password, status, registered_at, has_sora, has_plus, phone_bound, proxy, refresh_token, access_token) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", + ( + email, + pwd, + "Registered+Sora" if sora_ok else "Registered", + datetime.now().strftime("%Y-%m-%d %H:%M"), + 1 if sora_ok else 0, + 0, + 0, + (same_proxy or ""), + rt or None, + at, + ), + ) + created = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + c.execute( + "INSERT INTO run_logs (task_id, level, message, created_at) VALUES (?, ?, ?, ?)", + (task_id, "info", (f"Sora2 注册成功 {email}" if sora_ok else f"账号已注册但未完成 Sora2 激活 {email}"), created), + ) + try: + from app.database import DB_PATH + c.execute("SELECT COUNT(*) FROM accounts") + n = c.fetchone()[0] + created2 = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + c.execute( + "INSERT INTO run_logs (task_id, level, message, created_at) VALUES (?, ?, ?, ?)", + (task_id, "info", f"账号已写入 accounts 表,数据文件: {DB_PATH},当前共 {n} 条", created2), + ) + except Exception: + pass + stat_key = "last_run_success" if sora_ok else "last_run_fail" + c.execute("SELECT value FROM system_settings WHERE key = ?", (stat_key,)) + r2 = c.fetchone() + prev = int((r2[0] or "0")) if r2 else 0 + c.execute( + "INSERT OR REPLACE INTO system_settings (key, value) VALUES (?, ?)", + (stat_key, str(prev + 1)), + ) + except Exception as e: + created = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + with get_db() as conn: + c = conn.cursor() + c.execute( + "INSERT INTO run_logs (task_id, level, message, created_at) VALUES (?, ?, ?, ?)", + (task_id, "error", f"注册成功但写入账号列表失败 {email}: {e!s}", created), + ) + return False + return bool(sora_ok) + if not success and (str(status_extra or "").strip() == "0a_no_session"): + print(f"[*] 0a 未过 {email},跳过该邮箱,下一批自动换用其他账号", flush=True) + with get_db() as conn: + c = conn.cursor() + created = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + c.execute( + "INSERT INTO run_logs (task_id, level, message, created_at) VALUES (?, ?, ?, ?)", + (task_id, "info", f"0a 未过 {email},跳过该邮箱改用下一账号", created), + ) + return False + last_error = status_extra or "注册失败" + with get_db() as conn: + c = conn.cursor() + created = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + c.execute( + "INSERT INTO run_logs (task_id, level, message, created_at) VALUES (?, ?, ?, ?)", + (task_id, "error", f"尝试 {attempt + 1}/{retry_count} 失败 {email}: {last_error}", created), + ) + + with get_db() as conn: + c = conn.cursor() + created = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + c.execute( + "INSERT INTO run_logs (task_id, level, message, created_at) VALUES (?, ?, ?, ?)", + (task_id, "error", f"注册失败 {email} (已重试 {retry_count} 次)", created), + ) + c.execute("SELECT value FROM system_settings WHERE key = 'last_run_fail'") + row = c.fetchone() + prev = int((row[0] or "0")) if row else 0 + c.execute( + "INSERT OR REPLACE INTO system_settings (key, value) VALUES ('last_run_fail', ?)", + (str(prev + 1),), + ) + # 该错误通常表示邮箱已注册或已不可用于该注册流程,打标避免后续反复重试同邮箱。 + err_text = str(last_error or "").lower() + if "bad_request" in err_text or "failed to register username" in err_text: + c.execute("SELECT COALESCE(remark, '') FROM emails WHERE id = ?", (email_id,)) + row2 = c.fetchone() + old_remark = (row2[0] if row2 else "") or "" + if "[skip_bad_request]" not in old_remark: + new_remark = (old_remark + " [skip_bad_request]").strip() + c.execute("UPDATE emails SET remark = ? WHERE id = ?", (new_remark, email_id)) + return False + + +def run_one_task( + task_id: str, + settings: Optional[dict] = None, + email_row: Optional[Tuple] = None, +) -> bool: + """ + 执行单条注册任务。若传 email_row 则用该行;否则从 DB 取一条未注册邮箱。 + 返回是否执行并成功(无任务可执行时返回 False)。 + """ + init_db() + if settings is None: + settings = _get_registration_settings() + if email_row is not None: + row = email_row + else: + with get_db() as conn: + # 随机取一条,避免单个异常邮箱(如 0a/429)长期卡住队列头部。 + row = fetch_one_unregistered_email(conn, order_random=True) + if not row: + return False + if is_stop_requested(): + return False + email_id, email, password, uuid_val, token = row + return run_one_with_retry(email_id, email, password or "", uuid_val or "", token or "", settings, task_id) diff --git a/Register_GPT_v0/web/backend/app/services/sora_api_key.py b/Register_GPT_v0/web/backend/app/services/sora_api_key.py new file mode 100644 index 0000000..2cb46c5 --- /dev/null +++ b/Register_GPT_v0/web/backend/app/services/sora_api_key.py @@ -0,0 +1,166 @@ +import hashlib +import secrets +from typing import Dict, Optional + +from fastapi import Depends, Header, HTTPException + +from app.database import get_db, init_db +from app.routers.auth import get_optional_user + + +SORA_API_KEY_SCOPE_TEXT = "text_to_video" +SORA_API_KEY_SCOPE_IMAGE = "image_to_video" +SORA_API_KEY_SCOPE_ALL = "all_video" + +_SORA_API_KEY_SCOPE_ALIASES = { + "text": SORA_API_KEY_SCOPE_TEXT, + "text_to_video": SORA_API_KEY_SCOPE_TEXT, + "text-video": SORA_API_KEY_SCOPE_TEXT, + "text2video": SORA_API_KEY_SCOPE_TEXT, + "文生视频": SORA_API_KEY_SCOPE_TEXT, + "image": SORA_API_KEY_SCOPE_IMAGE, + "image_to_video": SORA_API_KEY_SCOPE_IMAGE, + "image-video": SORA_API_KEY_SCOPE_IMAGE, + "image2video": SORA_API_KEY_SCOPE_IMAGE, + "图生视频": SORA_API_KEY_SCOPE_IMAGE, + "all": SORA_API_KEY_SCOPE_ALL, + "all_video": SORA_API_KEY_SCOPE_ALL, + "both": SORA_API_KEY_SCOPE_ALL, + "combined": SORA_API_KEY_SCOPE_ALL, + "hybrid": SORA_API_KEY_SCOPE_ALL, + "文生+图生": SORA_API_KEY_SCOPE_ALL, +} + +_SORA_API_KEY_SCOPE_LABELS = { + SORA_API_KEY_SCOPE_TEXT: "文生视频", + SORA_API_KEY_SCOPE_IMAGE: "图生视频", + SORA_API_KEY_SCOPE_ALL: "文生+图生", +} + +_SORA_API_KEY_SCOPE_CAPABILITIES = { + SORA_API_KEY_SCOPE_TEXT: {SORA_API_KEY_SCOPE_TEXT}, + SORA_API_KEY_SCOPE_IMAGE: {SORA_API_KEY_SCOPE_IMAGE}, + SORA_API_KEY_SCOPE_ALL: {SORA_API_KEY_SCOPE_TEXT, SORA_API_KEY_SCOPE_IMAGE}, +} + + +def generate_sora_api_key() -> str: + # 32 bytes random -> 43 chars base64url;统一加前缀方便识别 + token = secrets.token_urlsafe(32).replace("-", "").replace("_", "") + return f"srk_{token}" + + +def hash_sora_api_key(raw_key: str) -> str: + return hashlib.sha256((raw_key or "").encode("utf-8")).hexdigest() + + +def mask_sora_api_key(raw_key: str) -> str: + key = (raw_key or "").strip() + if not key: + return "" + if len(key) <= 14: + return f"{key[:4]}***{key[-2:]}" + return f"{key[:12]}...{key[-4:]}" + + +def normalize_sora_api_key_scope(scope: str) -> str: + value = (scope or "").strip().lower() + return _SORA_API_KEY_SCOPE_ALIASES.get(value, SORA_API_KEY_SCOPE_TEXT) + + +def sora_api_key_scope_label(scope: str) -> str: + normalized = normalize_sora_api_key_scope(scope) + return _SORA_API_KEY_SCOPE_LABELS.get(normalized, _SORA_API_KEY_SCOPE_LABELS[SORA_API_KEY_SCOPE_TEXT]) + + +def sora_api_key_scope_allows(scope: str, capability: str) -> bool: + normalized_scope = normalize_sora_api_key_scope(scope) + normalized_capability = normalize_sora_api_key_scope(capability) + return normalized_capability in _SORA_API_KEY_SCOPE_CAPABILITIES.get(normalized_scope, set()) + + +def _extract_api_key( + authorization: Optional[str], + x_api_key: Optional[str], + x_sora_api_key: Optional[str], +) -> str: + key = (x_sora_api_key or "").strip() or (x_api_key or "").strip() + if key: + return key + auth = (authorization or "").strip() + if not auth: + return "" + parts = auth.split(" ", 1) + if len(parts) == 2 and parts[0].lower() == "bearer": + candidate = parts[1].strip() + else: + candidate = auth + if candidate.startswith("srk_"): + return candidate + return "" + + +def authenticate_sora_api_key(raw_key: str) -> Optional[Dict]: + key = (raw_key or "").strip() + if not key: + return None + key_hash = hash_sora_api_key(key) + init_db() + with get_db() as conn: + c = conn.cursor() + c.execute( + """SELECT id, account_id, name, is_active, COALESCE(scope, ?) + FROM sora_api_keys + WHERE key_hash = ? + LIMIT 1""", + (SORA_API_KEY_SCOPE_TEXT, key_hash), + ) + row = c.fetchone() + if not row: + return None + if not bool(row[3]): + return None + c.execute("UPDATE sora_api_keys SET last_used_at = datetime('now') WHERE id = ?", (row[0],)) + return { + "id": row[0], + "account_id": row[1] if row[1] != 0 else None, # 0 = 池模式 → None + "name": row[2] or "", + "scope": normalize_sora_api_key_scope(row[4] or SORA_API_KEY_SCOPE_TEXT), + } + + +def get_sora_api_caller( + username: Optional[str] = Depends(get_optional_user), + authorization: Optional[str] = Header(default=None), + x_api_key: Optional[str] = Header(default=None, alias="X-API-Key"), + x_sora_api_key: Optional[str] = Header(default=None, alias="X-Sora-Api-Key"), +): + """ + Sora 调用支持两种鉴权: + 1) 管理员 JWT(Authorization: Bearer ) + 2) 本地 Sora API Key(Authorization: Bearer srk_xxx / X-API-Key / X-Sora-Api-Key) + """ + if username: + return { + "auth_type": "admin", + "username": username, + "api_key_id": None, + "account_id": None, + } + + raw_key = _extract_api_key(authorization=authorization, x_api_key=x_api_key, x_sora_api_key=x_sora_api_key) + if not raw_key: + raise HTTPException(status_code=401, detail="Not authenticated") + + key_row = authenticate_sora_api_key(raw_key) + if not key_row: + raise HTTPException(status_code=401, detail="Invalid API key") + + return { + "auth_type": "api_key", + "username": "", + "api_key_id": key_row["id"], + "account_id": key_row["account_id"], + "api_key_scope": key_row["scope"], + "api_key_scope_label": sora_api_key_scope_label(key_row["scope"]), + } diff --git a/Register_GPT_v0/web/backend/requirements.txt b/Register_GPT_v0/web/backend/requirements.txt new file mode 100644 index 0000000..2bb8f05 --- /dev/null +++ b/Register_GPT_v0/web/backend/requirements.txt @@ -0,0 +1,7 @@ +fastapi>=0.104.0 +uvicorn[standard]>=0.24.0 +python-multipart>=0.0.6 +passlib[bcrypt]>=1.7.4 +python-jose[cryptography]>=3.3.0 +aiosqlite>=0.19.0 +pydantic>=2.0.0 diff --git a/Register_GPT_v0/web/docker-compose.yml b/Register_GPT_v0/web/docker-compose.yml new file mode 100644 index 0000000..5bfb9ac --- /dev/null +++ b/Register_GPT_v0/web/docker-compose.yml @@ -0,0 +1,19 @@ +# 在 protocol 目录执行: docker-compose -f web/docker-compose.yml up -d +# 默认账号 admin / admin123,请通过环境变量修改 +version: "3" +services: + admin: + build: + context: .. + dockerfile: web/Dockerfile + ports: + - "1989:1989" + environment: + - ADMIN_USERNAME=admin + - ADMIN_PASSWORD=admin123 + - SECRET_KEY=change-me-in-production + - DATA_DIR=/data + volumes: + - admin-data:/data +volumes: + admin-data: diff --git a/Register_GPT_v0/web/frontend/index.html b/Register_GPT_v0/web/frontend/index.html new file mode 100644 index 0000000..03df0cc --- /dev/null +++ b/Register_GPT_v0/web/frontend/index.html @@ -0,0 +1,566 @@ + + + + + + Sora 批量注册 + + + + + + + + +
+ + + diff --git a/Register_GPT_v0/web/frontend/static/app.js b/Register_GPT_v0/web/frontend/static/app.js new file mode 100644 index 0000000..346f035 --- /dev/null +++ b/Register_GPT_v0/web/frontend/static/app.js @@ -0,0 +1,2729 @@ +const API_BASE = ""; +let token = localStorage.getItem("admin_token"); +let currentPage = 1; +let accountsTotal = 0; + +function api(url, options = {}) { + const headers = { "Content-Type": "application/json", ...options.headers }; + if (token) headers["Authorization"] = "Bearer " + token; + return fetch(API_BASE + url, { ...options, headers }).then(async (r) => { + if (r.status === 401) { + if (!url.includes("/auth/login")) { + localStorage.removeItem("admin_token"); + window.location.reload(); + } + const text = await r.text(); + let msg = "Unauthorized"; + try { + const j = JSON.parse(text); + if (j.detail) msg = typeof j.detail === "string" ? j.detail : JSON.stringify(j.detail); + } catch (_) {} + throw new Error(msg); + } + if (!r.ok) throw new Error(await r.text()); + const ct = r.headers.get("content-type"); + if (ct && ct.includes("application/json")) return r.json(); + return r.text(); + }); +} + +function showPage(name) { + document.querySelectorAll(".panel").forEach((el) => el.classList.add("hidden")); + document.querySelectorAll(".nav a").forEach((a) => a.classList.remove("active")); + const panel = document.getElementById("panel-" + name); + const link = document.querySelector('.nav a[data-tab="' + name + '"]'); + if (panel) panel.classList.remove("hidden"); + if (link) link.classList.add("active"); + if (name === "accounts") loadAccounts(); + if (name === "emails") { + loadEmails(); + api("/api/settings").then((d) => { + const sel = document.getElementById("email-api-mail-type"); + if (sel && d.email_api_default_type) { + if ([].some.call(sel.options, (o) => o.value === d.email_api_default_type)) sel.value = d.email_api_default_type; + } + }).catch(() => {}); + } + if (name === "bank-cards") loadBankCards(); + if (name === "phones") loadPhones(); + if (name === "video") loadSoraVideoWorkspace(); + if (name === "keys") loadSoraKeyManagement(); + if (name === "logs") { + loadDashboard(); + loadLogs(); + updateRegisterStatusOnce(); + startRegisterStatusPoll(); + } else { + stopRegisterStatusPoll(); + } + if (name === "settings") loadSettings(); +} + +var registerStatusPollTimer = null; +var REGISTER_POLL_INTERVAL_MS = 1500; + +/** 状态来源:GET /api/register/status 的 running 字段(后端 _registration_running,重启后必为 false) */ +function updateRegisterButtonFromStatus(s) { + var btnStart = document.getElementById("btn-start-register"); + var btnStop = document.getElementById("btn-stop-register"); + var heartbeatEl = document.getElementById("register-status-heartbeat"); + if (!btnStart) return; + var running = !!(s && s.running === true); + if (running) { + btnStart.textContent = "正在注册"; + btnStart.disabled = true; + btnStart.classList.add("btn-dash-disabled"); + if (btnStop) { btnStop.style.display = ""; } + if (heartbeatEl) { + heartbeatEl.style.display = ""; + heartbeatEl.textContent = s.last_heartbeat ? "最后心跳时间 " + (s.last_heartbeat.replace("T", " ").replace("Z", "").slice(0, 19)) : ""; + } + } else { + btnStart.textContent = "开启注册"; + btnStart.disabled = false; + btnStart.classList.remove("btn-dash-disabled"); + if (btnStop) { btnStop.style.display = "none"; } + if (heartbeatEl) { + heartbeatEl.style.display = "none"; + heartbeatEl.textContent = ""; + } + } +} + +function updateRegisterStatusOnce() { + api("/api/register/status").then(function(s) { + updateRegisterButtonFromStatus(s); + }).catch(function() { + updateRegisterButtonFromStatus({ running: false }); + }); +} + +function startRegisterStatusPoll() { + stopRegisterStatusPoll(); + registerStatusPollTimer = setInterval(function() { + api("/api/register/status").then(function(s) { + updateRegisterButtonFromStatus(s); + loadDashboard(); + loadLogs(); + }).catch(function() { + updateRegisterButtonFromStatus({ running: false }); + }); + }, REGISTER_POLL_INTERVAL_MS); +} + +document.addEventListener("visibilitychange", function() { + if (document.visibilityState === "visible") { + var panelLogs = document.getElementById("panel-logs"); + if (panelLogs && !panelLogs.classList.contains("hidden")) { + updateRegisterStatusOnce(); + } + } +}); + +function stopRegisterStatusPoll() { + if (registerStatusPollTimer) { + clearInterval(registerStatusPollTimer); + registerStatusPollTimer = null; + } +} + +function showModal(html) { + document.getElementById("modal-body").innerHTML = html; + document.getElementById("modal").classList.remove("hidden"); +} +function hideModal() { + document.getElementById("modal").classList.add("hidden"); + var mc = document.querySelector(".modal-content"); + if (mc) mc.classList.remove("modal-content-wide"); +} +document.querySelector(".modal-close").addEventListener("click", hideModal); +document.getElementById("modal").addEventListener("click", (e) => { + if (e.target.id === "modal") hideModal(); +}); + +function toast(msg, type) { + type = type || "success"; + var container = document.getElementById("toast-container"); + var el = document.createElement("div"); + el.className = "toast " + type; + el.textContent = msg; + container.appendChild(el); + setTimeout(function() { + el.style.opacity = "0"; + el.style.transform = "translateX(100%)"; + setTimeout(function() { el.remove(); }, 250); + }, 2500); +} +function confirmBox(msg, onConfirm) { + showModal( + '
' + + '

' + escapeHtml(msg) + '

' + + '
' + + '' + + '' + + '
' + + '
' + ); + document.querySelector(".btn-cancel").addEventListener("click", function() { hideModal(); }); + document.querySelector(".btn-ok").addEventListener("click", function() { + hideModal(); + if (onConfirm) onConfirm(); + }); +} + +// Login +if (!token) { + document.getElementById("login-page").classList.remove("hidden"); + document.getElementById("admin-page").classList.add("hidden"); +} else { + document.getElementById("login-page").classList.add("hidden"); + document.getElementById("admin-page").classList.remove("hidden"); + api("/api/auth/me").then((d) => { + var u = document.getElementById("current-user"); if (u) { var t = u.querySelector(".user-name-text"); if (t) t.textContent = d.username; else u.textContent = d.username; } + }).catch(() => { + localStorage.removeItem("admin_token"); + window.location.reload(); + }); +} + +document.getElementById("login-form").addEventListener("submit", (e) => { + e.preventDefault(); + const username = document.getElementById("login-username").value.trim(); + const password = document.getElementById("login-password").value; + const errEl = document.getElementById("login-error"); + errEl.textContent = ""; + api("/api/auth/login", { + method: "POST", + body: JSON.stringify({ username, password }), + }) + .then((d) => { + if (!d || !d.token) { + errEl.textContent = "登录返回异常,请重试"; + return; + } + token = d.token; + localStorage.setItem("admin_token", token); + document.getElementById("login-page").classList.add("hidden"); + document.getElementById("admin-page").classList.remove("hidden"); + var cu = document.getElementById("current-user"); if (cu) { var ct = cu.querySelector(".user-name-text"); if (ct) ct.textContent = d.username || username; else cu.textContent = d.username || username; } + errEl.textContent = ""; + showPage("accounts"); + }) + .catch((err) => { + errEl.textContent = err.message || "登录失败"; + }); +}); + +document.getElementById("btn-logout").addEventListener("click", () => { + localStorage.removeItem("admin_token"); + window.location.reload(); +}); + +// 侧栏默认收起,可展开;状态存 localStorage;收起时用底部按钮,展开时用头部按钮 +(function() { + var sidebar = document.getElementById("sidebar"); + var key = "sidebarCollapsed"; + function toggleSidebar() { + sidebar.classList.toggle("collapsed"); + localStorage.setItem(key, sidebar.classList.contains("collapsed") ? "1" : "0"); + } + if (sidebar) { + var saved = localStorage.getItem(key); + if (saved === "0" || saved === "false") sidebar.classList.remove("collapsed"); + else sidebar.classList.add("collapsed"); + var btnHeader = document.getElementById("sidebar-toggle"); + var btnFooter = document.getElementById("sidebar-toggle-footer"); + if (btnHeader) btnHeader.addEventListener("click", toggleSidebar); + if (btnFooter) btnFooter.addEventListener("click", toggleSidebar); + } +})(); + +// Nav tabs +document.querySelectorAll('.nav a[data-tab]').forEach((a) => { + a.addEventListener("click", (e) => { + e.preventDefault(); + showPage(a.getAttribute("data-tab")); + }); +}); + +// Accounts +function setCurrentSoraAccountId(accountId) { + var id = parseInt(accountId, 10) || 0; + if (!id) return; + var idInput = document.getElementById("sora-api-account-id"); + if (idInput) idInput.value = String(id); + localStorage.setItem("sora_api_last_account_id", String(id)); +} + +function isSoraAccountUsable(account) { + return !!(account && account.has_sora && account.sora_enabled && !account.sora_quota_exhausted && account.has_token); +} + +function formatAccountStatus(status, account) { + var text = status || ""; + if (text === "Registered+Sora" && account && account.has_sora) return "Registered+Sora2"; + return text; +} + +function getSoraAccountAvailabilityMessage(account) { + if (!account) return "尚未选择生成账号"; + if (!account.has_sora) return "该账号尚未开通 Sora"; + if (!account.sora_enabled) return "该账号已停用"; + if (account.sora_quota_exhausted) return "该账号已标记额度不足"; + if (!account.has_token) return "该账号缺少 token"; + return "当前账号可用于视频生成"; +} + +function renderSoraVideoAccountSummary(account) { + var box = document.getElementById("sora-video-account-summary"); + if (!box) return; + if (!account) { + box.innerHTML = ''; + return; + } + var quotaText = getSoraQuotaText(account); + var quotaClass = account.sora_quota_exhausted ? "is-bad" : "is-ok"; + var enableClass = account.sora_enabled ? "is-ok" : "is-bad"; + var soraClass = account.has_sora ? "is-ok" : "is-warn"; + var tokenClass = account.has_token ? "is-ok" : "is-bad"; + var statusText = formatAccountStatus(account.status, account) || "未设置"; + var registeredAt = account.registered_at || "未记录"; + var lastError = account.sora_last_error || ""; + box.innerHTML = + '" + + '" + + (lastError ? ('") : ""); +} + +function loadSoraAccountDetails(accountId, options) { + var id = parseInt(accountId, 10) || 0; + var msgEl = document.getElementById("sora-api-msg"); + if (!id) { + renderSoraVideoAccountSummary(null); + if (!(options && options.silent) && msgEl) msgEl.textContent = "请先输入有效账号 ID"; + return Promise.reject(new Error("请先输入有效账号 ID")); + } + if (!(options && options.silent) && msgEl) msgEl.textContent = "加载账号状态..."; + return api("/api/accounts/" + id).then(function(d) { + setCurrentSoraAccountId(d.id); + renderSoraVideoAccountSummary(d); + if (!(options && options.skipKeyList)) loadSoraApiKeyList(d.id); + if (!(options && options.silent) && msgEl) msgEl.textContent = getSoraAccountAvailabilityMessage(d); + return d; + }).catch(function(err) { + renderSoraVideoAccountSummary(null); + if (!(options && options.silent) && msgEl) msgEl.textContent = "加载失败:" + parseApiErrorMessage(err); + throw err; + }); +} + +function pickNextAvailableSoraAccount(options) { + var msgEl = document.getElementById("sora-api-msg"); + if (!(options && options.silent) && msgEl) msgEl.textContent = "切换中..."; + return api("/api/accounts/next-sora-available").then(function(d) { + return loadSoraAccountDetails(d.id, { silent: true }).then(function(account) { + var text = ((options && options.messagePrefix) || "已切换到可用账号") + " ID " + d.id + "(" + (d.email || "") + ")"; + if (msgEl) msgEl.textContent = text; + if (options && options.toast) toast(text, options.toastType || "success"); + return account; + }); + }).catch(function(err) { + var message = parseApiErrorMessage(err); + if (msgEl) msgEl.textContent = "切换失败:" + message; + throw err; + }); +} + +function ensureSoraVideoAccountReady() { + var currentId = getSoraAccountIdFromInput(); + if (!currentId) { + return pickNextAvailableSoraAccount({ messagePrefix: "已自动选择可用账号" }).catch(function() { + return null; + }); + } + return loadSoraAccountDetails(currentId, { silent: true }).then(function(account) { + if (isSoraAccountUsable(account)) { + var msgEl = document.getElementById("sora-api-msg"); + if (msgEl) msgEl.textContent = "当前账号可用于视频生成"; + return account; + } + return pickNextAvailableSoraAccount({ messagePrefix: "当前账号不可用,已自动切换到可用账号" }); + }).catch(function() { + return pickNextAvailableSoraAccount({ messagePrefix: "当前账号不存在或不可用,已自动切换到可用账号" }).catch(function() { + return null; + }); + }); +} + +function loadSoraVideoWorkspace() { + ensureSoraVideoAccountReady(); +} + +function getSoraQuotaText(r) { + if (!r || !r.sora_enabled) return "已停用"; + if (r.sora_quota_exhausted) { + var note = (r.sora_quota_note || "额度不足"); + return "额度不足(" + note + ")"; + } + return "可用"; +} + +function updateSoraAccountState(accountId, payload, successMsg) { + var id = parseInt(accountId, 10) || 0; + if (!id) return; + api("/api/accounts/" + id + "/sora-state", { + method: "POST", + body: JSON.stringify(payload || {}), + }).then(function () { + if (successMsg) toast(successMsg); + loadAccounts(); + loadSoraApiKeyList(id); + }).catch(function (err) { + toast("操作失败: " + parseApiErrorMessage(err), "error"); + }); +} + +function getSoraQuotaRecheckLabel(result) { + var labels = { + recovered: "已恢复并回池", + recovered_busy: "已恢复,当前繁忙", + still_exhausted: "仍然额度不足", + probe_failed: "复检失败", + auth_failed: "鉴权失败", + skipped_no_token: "跳过,无 token", + skipped_disabled: "跳过,已停用", + skipped_no_sora: "跳过,未开通 Sora", + already_available: "本来就在池中", + }; + return labels[result] || result || "未知"; +} + +function showSoraQuotaRecheckReport(report) { + var items = Array.isArray(report && report.items) ? report.items : []; + var rows = items.length + ? items.map(function(item) { + return ( + "" + + "" + escapeHtml(String(item.account_id || "")) + "" + + "" + escapeHtml(item.email || "") + "" + + "" + escapeHtml(getSoraQuotaRecheckLabel(item.result)) + "" + + "" + escapeHtml(item.message || "") + "" + + "" + escapeHtml(item.task_id || "-") + "" + + "" + ); + }).join("") + : '没有可展示的复检结果'; + showModal( + '
' + + '

额度复检结果

' + + '' + + '
' + + '' + + '' + + '' + rows + '' + + '
ID邮箱结果说明探针任务
' + + '
' + + '
' + ); +} + +function runSoraQuotaRecheck(options) { + var opts = options || {}; + var payload = { + limit: opts.accountId ? 1 : 10, + auto_cancel: true, + }; + if (opts.accountId) payload.account_id = parseInt(opts.accountId, 10) || 0; + var button = opts.button || null; + var originalText = button ? button.textContent : ""; + if (button) { + button.disabled = true; + button.textContent = "复检中..."; + } + return api("/api/accounts/sora-quota/recheck", { + method: "POST", + body: JSON.stringify(payload), + }).then(function(report) { + showSoraQuotaRecheckReport(report); + toast(report.message || "额度复检已完成"); + loadAccounts(); + var currentId = getSoraAccountIdFromInput(); + if (currentId) loadSoraAccountDetails(currentId, { silent: true }).catch(function() {}); + return report; + }).catch(function(err) { + toast("额度复检失败: " + parseApiErrorMessage(err), "error"); + throw err; + }).finally(function() { + if (button) { + button.disabled = false; + button.textContent = originalText; + } + }); +} + +function loadAccounts() { + const status = document.getElementById("filter-status").value; + const sora = document.getElementById("filter-sora").value; + const plus = document.getElementById("filter-plus").value; + const params = new URLSearchParams({ page: currentPage, page_size: 20 }); + if (status) params.set("status", status); + if (sora) params.set("has_sora", sora); + if (plus) params.set("has_plus", plus); + api("/api/debug/db-info").then(function (info) { + const el = document.getElementById("accounts-db-hint"); + if (el) el.textContent = "共 " + (info.accounts_count != null ? info.accounts_count : "?") + " 条。若用脚本注册,请用相同 DATA_DIR 启动本后端,否则新账号不会出现在本列表。"; + }).catch(function () {}); + api("/api/accounts?" + params).then((d) => { + accountsTotal = d.total; + const tbody = document.getElementById("accounts-tbody"); + tbody.innerHTML = d.items + .map( + (r) => + ` + ${r.id} + ${escapeHtml(r.email)} + ${escapeHtml(r.password || "")} + ${escapeHtml(formatAccountStatus(r.status, r) || "")} + ${r.has_sora ? "是" : "否"} + ${r.has_plus ? "是" : "否"} + ${r.phone_bound ? "是" : "否"} + ${escapeHtml((r.refresh_token || "").slice(0, 24))}${(r.refresh_token || "").length > 24 ? "…" : ""} + ${escapeHtml(r.registered_at || r.created_at || "")} + ${escapeHtml(getSoraQuotaText(r))} + + + + ${r.sora_quota_exhausted ? `` : ""} + + + + ` + ) + .join(""); + const pag = document.getElementById("accounts-pagination"); + const totalPages = Math.ceil(d.total / d.page_size) || 1; + pag.innerHTML = `共 ${d.total} 条 ` + (totalPages > 1 ? ` ${currentPage}/${totalPages} ` : ""); + pag.querySelectorAll("button").forEach((btn) => { + btn.addEventListener("click", () => { + if (btn.dataset.page === "prev" && currentPage > 1) currentPage--; + if (btn.dataset.page === "next" && currentPage < totalPages) currentPage++; + loadAccounts(); + }); + }); + tbody.querySelectorAll(".btn-create-sora-key").forEach((btn) => { + btn.addEventListener("click", () => { + var id = parseInt(btn.dataset.id, 10) || 0; + if (!id) return; + setCurrentSoraAccountId(id); + createSoraApiKey(id); + }); + }); + tbody.querySelectorAll(".btn-list-sora-key").forEach((btn) => { + btn.addEventListener("click", () => { + var id = parseInt(btn.dataset.id, 10) || 0; + if (!id) return; + setCurrentSoraAccountId(id); + loadSoraApiKeyList(id); + }); + }); + tbody.querySelectorAll(".btn-use-sora-account").forEach((btn) => { + btn.addEventListener("click", () => { + var id = parseInt(btn.dataset.id, 10) || 0; + if (!id) return; + setCurrentSoraAccountId(id); + loadSoraAccountDetails(id, { silent: true }).catch(function() {}); + var msgEl = document.getElementById("sora-api-msg"); + if (msgEl) msgEl.textContent = "已切换到账号 ID " + id + ",可去左侧“视频生成”直接创建任务"; + toast("已切换到视频生成账号 ID " + id); + }); + }); + tbody.querySelectorAll(".btn-reset-sora-quota").forEach((btn) => { + btn.addEventListener("click", () => { + var id = parseInt(btn.dataset.id, 10) || 0; + if (!id) return; + updateSoraAccountState(id, { reset_quota: true }, "已重置额度状态"); + }); + }); + tbody.querySelectorAll(".btn-probe-sora-quota").forEach((btn) => { + btn.addEventListener("click", () => { + var id = parseInt(btn.dataset.id, 10) || 0; + if (!id) return; + confirmBox( + "这会对账号 ID " + id + " 发起一个最小视频探针,创建成功后会立即取消,并在额度恢复时自动回池。继续吗?", + function() { runSoraQuotaRecheck({ accountId: id, button: btn }); } + ); + }); + }); + tbody.querySelectorAll(".btn-toggle-sora-account").forEach((btn) => { + btn.addEventListener("click", () => { + var id = parseInt(btn.dataset.id, 10) || 0; + var enable = String(btn.dataset.enable || "1") === "1"; + if (!id) return; + updateSoraAccountState(id, { sora_enabled: enable }, enable ? "账号已启用" : "账号已停用"); + }); + }); + }); +} +document.getElementById("filter-status").addEventListener("change", () => { currentPage = 1; loadAccounts(); }); +document.getElementById("filter-sora").addEventListener("change", () => { currentPage = 1; loadAccounts(); }); +document.getElementById("filter-plus").addEventListener("change", () => { currentPage = 1; loadAccounts(); }); +document.getElementById("btn-recheck-sora-quota").addEventListener("click", function() { + var btn = this; + confirmBox( + "这会对当前被标记“额度不足”的账号逐个发起最小视频探针,创建成功后立即取消,并自动让已恢复账号重新回池。继续吗?", + function() { runSoraQuotaRecheck({ button: btn }); } + ); +}); + +document.getElementById("btn-export-accounts").addEventListener("click", () => { + const status = document.getElementById("filter-status").value; + const sora = document.getElementById("filter-sora").value; + const plus = document.getElementById("filter-plus").value; + const params = new URLSearchParams(); + if (status) params.set("status", status); + if (sora) params.set("has_sora", sora); + if (plus) params.set("has_plus", plus); + fetch(API_BASE + "/api/accounts/export?" + params, { headers: { Authorization: "Bearer " + token } }) + .then((r) => { if (!r.ok) throw new Error(r.statusText); return r.blob(); }) + .then((blob) => { + const a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = "accounts.csv"; + a.click(); + URL.revokeObjectURL(a.href); + }) + .catch((err) => toast("导出失败: " + err.message, "error")); +}); + +function parseApiErrorMessage(err) { + var msg = (err && err.message) ? err.message : "请求错误"; + try { + var obj = JSON.parse(msg); + if (obj && obj.detail) { + return typeof obj.detail === "string" ? obj.detail : JSON.stringify(obj.detail); + } + } catch (_) {} + return msg; +} + +function copyTextToClipboard(text, successMsg) { + var value = (text == null) ? "" : String(text); + var done = function() { toast(successMsg || "已复制"); }; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(value).then(done).catch(function() {}); + return; + } + var helper = document.createElement("textarea"); + helper.value = value; + document.body.appendChild(helper); + helper.select(); + try { document.execCommand("copy"); done(); } catch (_) {} + helper.remove(); +} + +function getSoraAccountIdFromInput() { + var raw = (document.getElementById("sora-api-account-id").value || "").trim(); + var id = parseInt(raw, 10); + if (!id || id < 1) return null; + localStorage.setItem("sora_api_last_account_id", String(id)); + return id; +} + +var SORA_KEY_SCOPE_TEXT = "text_to_video"; +var SORA_KEY_SCOPE_IMAGE = "image_to_video"; +var SORA_KEY_SCOPE_ALL = "all_video"; +var SORA_TASK_FAMILY_VIDEO_GEN = "video_gen"; +var SORA_TASK_FAMILY_NF2 = "nf2"; + +function normalizeSoraKeyScope(scope) { + var value = (scope || "").toString().trim().toLowerCase(); + var aliases = { + text: SORA_KEY_SCOPE_TEXT, + text_to_video: SORA_KEY_SCOPE_TEXT, + "text-video": SORA_KEY_SCOPE_TEXT, + text2video: SORA_KEY_SCOPE_TEXT, + image: SORA_KEY_SCOPE_IMAGE, + image_to_video: SORA_KEY_SCOPE_IMAGE, + "image-video": SORA_KEY_SCOPE_IMAGE, + image2video: SORA_KEY_SCOPE_IMAGE, + all: SORA_KEY_SCOPE_ALL, + all_video: SORA_KEY_SCOPE_ALL, + both: SORA_KEY_SCOPE_ALL, + combined: SORA_KEY_SCOPE_ALL, + hybrid: SORA_KEY_SCOPE_ALL, + }; + return aliases[value] || SORA_KEY_SCOPE_TEXT; +} + +function getSoraKeyScopeLabel(scope) { + var normalized = normalizeSoraKeyScope(scope); + if (normalized === SORA_KEY_SCOPE_IMAGE) return "图生视频"; + if (normalized === SORA_KEY_SCOPE_ALL) return "文生+图生"; + return "文生视频"; +} + +function getSoraKeyScopeClass(scope) { + var normalized = normalizeSoraKeyScope(scope); + if (normalized === SORA_KEY_SCOPE_IMAGE) return "key-chip-scope-image"; + if (normalized === SORA_KEY_SCOPE_ALL) return "key-chip-scope-all"; + return "key-chip-scope-text"; +} + +function getSoraKeyModeLabel(mode, accountId) { + var normalizedMode = (mode || "").toString().trim().toLowerCase(); + if (!normalizedMode) normalizedMode = (parseInt(accountId, 10) === 0 ? "pool" : "bound"); + return normalizedMode === "pool" ? "轮换池" : "账号绑定"; +} + +function syncSoraKeyScopeOptions() { + document.querySelectorAll(".key-scope-option").forEach(function(option) { + var input = option.querySelector('input[type="radio"]'); + option.classList.toggle("is-selected", !!(input && input.checked)); + }); +} + +function renderSoraApiKeyList(items) { + var listEl = document.getElementById("sora-key-list"); + if (!listEl) return; + var rows = items || []; + if (!rows.length) { + listEl.innerHTML = "该账号暂无可用 API Key"; + return; + } + listEl.innerHTML = rows + .map(function (r) { + var when = r.created_at ? ("创建于 " + escapeHtml(r.created_at)) : ""; + var used = r.last_used_at ? (",最近调用 " + escapeHtml(r.last_used_at)) : ""; + var name = r.name ? ("" + escapeHtml(r.name) + "") : ""; + var scope = '' + escapeHtml(r.scope_label || getSoraKeyScopeLabel(r.scope)) + ""; + return "
" + name + scope + "" + escapeHtml(r.key_mask || "") + "" + when + used + "
"; + }) + .join(""); +} + +function loadSoraApiKeyList(accountId) { + var id = parseInt(accountId, 10) || 0; + var listEl = document.getElementById("sora-key-list"); + if (!id) { + if (listEl) listEl.innerHTML = ""; + return; + } + if (listEl) listEl.innerHTML = "加载中..."; + api("/api/sora-keys?account_id=" + id + "&active_only=true") + .then(function (d) { + renderSoraApiKeyList((d && d.items) || []); + }) + .catch(function (err) { + if (listEl) listEl.textContent = "查询失败:" + parseApiErrorMessage(err); + }); +} + +function showCreatedSoraApiKey(result) { + var raw = (result && result.api_key) ? result.api_key : ""; + var email = (result && result.email) ? result.email : ""; + var accountId = parseInt((result && result.account_id) || 0, 10) || 0; + var scopeLabel = (result && result.scope_label) ? result.scope_label : getSoraKeyScopeLabel(result && result.scope); + var modeLabel = getSoraKeyModeLabel(result && result.key_mode, accountId); + var accountText = accountId === 0 ? "[自动轮换池]" : (email + " (ID " + String(accountId || "") + ")"); + showModal( + '" + ); + var btnCopy = document.getElementById("btn-copy-sora-api-key"); + if (!btnCopy) return; + btnCopy.addEventListener("click", function () { + copyTextToClipboard(raw, "API Key 已复制"); + }); +} + +function createSoraApiKey(accountId, options) { + var opts = options || {}; + var id = parseInt(accountId, 10); + var allowPool = !!opts.allowPool; + var msgEl = opts.msgEl || document.getElementById(opts.messageElementId || "sora-api-msg"); + if ((isNaN(id) || id < 1) && !allowPool) { + if (msgEl) msgEl.textContent = "请先输入有效账号 ID"; + toast("请先输入有效账号 ID", "info"); + return Promise.resolve(null); + } + if (allowPool && (isNaN(id) || id < 0)) id = 0; + var scope = normalizeSoraKeyScope(opts.scope || SORA_KEY_SCOPE_TEXT); + var name = (opts.name || "").trim(); + if (msgEl) msgEl.textContent = id === 0 ? "正在生成轮换池 API Key..." : "正在生成 API Key..."; + if (id > 0) setCurrentSoraAccountId(id); + return api("/api/sora-keys", { + method: "POST", + body: JSON.stringify({ account_id: id, name: name, scope: scope }), + }).then(function(d) { + if (msgEl) msgEl.textContent = "API Key 已生成(可在弹窗中复制)"; + showCreatedSoraApiKey(d || {}); + if (id > 0) loadSoraApiKeyList(id); + if (typeof opts.onSuccess === "function") opts.onSuccess(d || {}); + return d || {}; + }).catch(function(err) { + if (msgEl) msgEl.textContent = "生成失败:" + parseApiErrorMessage(err); + throw err; + }); +} + +function buildSoraKeyStats(items) { + var rows = items || []; + var stats = { + total: rows.length, + active: 0, + pool: 0, + text: 0, + image: 0, + all: 0, + }; + rows.forEach(function(item) { + var scope = normalizeSoraKeyScope(item.scope); + if (item.is_active) stats.active += 1; + if ((item.key_mode || "") === "pool" || parseInt(item.account_id || 0, 10) === 0) stats.pool += 1; + if (scope === SORA_KEY_SCOPE_IMAGE) stats.image += 1; + else if (scope === SORA_KEY_SCOPE_ALL) stats.all += 1; + else stats.text += 1; + }); + return stats; +} + +function renderSoraKeyStats(items) { + var wrap = document.getElementById("sora-key-stats"); + if (!wrap) return; + var stats = buildSoraKeyStats(items); + wrap.innerHTML = [ + ['全部 Key', stats.total], + ['启用中', stats.active], + ['轮换池 Key', stats.pool], + ['文生视频', stats.text], + ['图生视频', stats.image], + ['文生+图生', stats.all], + ].map(function(entry) { + return '
' + escapeHtml(entry[0]) + '' + escapeHtml(String(entry[1])) + '
'; + }).join(""); +} + +function renderSoraKeyManagementTable(items) { + var tbody = document.getElementById("sora-key-table-body"); + if (!tbody) return; + var rows = items || []; + if (!rows.length) { + tbody.innerHTML = '当前条件下没有找到 Key'; + return; + } + tbody.innerHTML = rows.map(function(item) { + var accountId = parseInt(item.account_id || 0, 10) || 0; + var scopeLabel = item.scope_label || getSoraKeyScopeLabel(item.scope); + var modeLabel = getSoraKeyModeLabel(item.key_mode, accountId); + var accountText = accountId === 0 + ? '自动轮换池' + : ('
ID ' + String(accountId) + '' + escapeHtml(item.email || "") + '
'); + var statusBadge = item.is_active + ? '启用中' + : '已停用'; + var actions = item.is_active + ? '' + : '已停用'; + return '' + + '' + + '' + String(item.id) + '' + + '' + + '
' + + '' + escapeHtml(item.name || "未命名 Key") + '' + + '创建人 ' + escapeHtml(item.created_by || "-") + '' + + '
' + + '' + + '' + escapeHtml(scopeLabel) + '' + + '' + escapeHtml(modeLabel) + '' + + '' + accountText + '' + + '' + escapeHtml(item.key_mask || "") + '' + + '' + statusBadge + '' + + '' + escapeHtml(item.last_used_at || "未调用") + '' + + '' + escapeHtml(item.created_at || "") + '' + + '
' + actions + '
' + + ''; + }).join(""); +} + +function loadSoraKeyManagement() { + var msgEl = document.getElementById("sora-key-manager-msg"); + var mode = (document.getElementById("sora-key-filter-mode").value || "").trim(); + var scopeInput = (document.getElementById("sora-key-filter-scope").value || "").trim(); + var status = (document.getElementById("sora-key-filter-status").value || "all").trim(); + var qs = ["active_only=false"]; + if (mode) qs.push("key_mode=" + encodeURIComponent(mode)); + if (scopeInput) qs.push("scope=" + encodeURIComponent(normalizeSoraKeyScope(scopeInput))); + if (msgEl) msgEl.textContent = "正在加载 Key 列表..."; + api("/api/sora-keys?" + qs.join("&")) + .then(function(d) { + var items = (d && d.items) || []; + if (status === "active") items = items.filter(function(item) { return !!item.is_active; }); + if (status === "inactive") items = items.filter(function(item) { return !item.is_active; }); + renderSoraKeyStats(items); + renderSoraKeyManagementTable(items); + if (msgEl) msgEl.textContent = "已加载 " + String(items.length) + " 条 Key"; + }) + .catch(function(err) { + renderSoraKeyStats([]); + renderSoraKeyManagementTable([]); + if (msgEl) msgEl.textContent = "加载失败:" + parseApiErrorMessage(err); + }); +} + +function createSoraPoolApiKey() { + var name = (document.getElementById("sora-key-create-name").value || "").trim(); + var selected = document.querySelector('input[name="sora-key-scope"]:checked'); + var scope = selected ? selected.value : SORA_KEY_SCOPE_TEXT; + createSoraApiKey(0, { + allowPool: true, + name: name, + scope: scope, + messageElementId: "sora-key-manager-msg", + onSuccess: function() { + loadSoraKeyManagement(); + }, + }).catch(function() {}); +} + +function disableSoraApiKey(keyId) { + var numericId = parseInt(keyId, 10); + if (!numericId || numericId < 1) return; + confirmBox("确定停用这把 API Key?停用后会立刻失效。", function() { + api("/api/sora-keys/" + numericId, { method: "DELETE" }) + .then(function() { + toast("API Key 已停用"); + loadSoraKeyManagement(); + }) + .catch(function(err) { + toast("停用失败:" + parseApiErrorMessage(err), "error"); + }); + }); +} + +(function initSoraAccountInput() { + var saved = localStorage.getItem("sora_api_last_account_id"); + var id = parseInt(saved || "", 10); + if (!id || id < 1) return; + var input = document.getElementById("sora-api-account-id"); + if (input && !input.value) input.value = String(id); +})(); + +document.getElementById("sora-api-account-id").addEventListener("change", function() { + var id = parseInt(this.value || "", 10); + if (!id || id < 1) return; + setCurrentSoraAccountId(id); + loadSoraAccountDetails(id, { silent: true }).catch(function() {}); +}); + +document.getElementById("btn-sora-me").addEventListener("click", function() { + var id = getSoraAccountIdFromInput(); + var msgEl = document.getElementById("sora-api-msg"); + if (!id) { + msgEl.textContent = "请先输入有效账号 ID"; + toast("请先输入有效账号 ID", "info"); + return; + } + msgEl.textContent = "请求中..."; + api("/api/sora-api/me", { + method: "POST", + body: JSON.stringify({ account_id: id }), + }).then(function(d) { + var me = d.me || {}; + var uname = me.username ? ("username=" + me.username) : "未设置 username"; + msgEl.textContent = "调用成功," + uname; + toast("Sora API 调用成功"); + }).catch(function(err) { + msgEl.textContent = "失败:" + parseApiErrorMessage(err); + }); +}); + +document.getElementById("btn-sora-pick-next").addEventListener("click", function() { + pickNextAvailableSoraAccount({ toast: true }).catch(function() {}); +}); + +document.getElementById("btn-sora-key-create").addEventListener("click", function() { + var id = getSoraAccountIdFromInput(); + createSoraApiKey(id, { scope: SORA_KEY_SCOPE_TEXT }).catch(function() {}); +}); + +document.getElementById("btn-sora-key-list").addEventListener("click", function() { + var id = getSoraAccountIdFromInput(); + if (!id) { + document.getElementById("sora-api-msg").textContent = "请先输入有效账号 ID"; + return; + } + loadSoraApiKeyList(id); +}); + +document.getElementById("btn-sora-activate").addEventListener("click", function() { + var id = getSoraAccountIdFromInput(); + var msgEl = document.getElementById("sora-api-msg"); + if (!id) { + msgEl.textContent = "请先输入有效账号 ID"; + toast("请先输入有效账号 ID", "info"); + return; + } + msgEl.textContent = "激活中..."; + api("/api/sora-api/activate", { + method: "POST", + body: JSON.stringify({ account_id: id }), + }).then(function(d) { + var uname = d.username ? ("username=" + d.username) : "未获取到 username"; + msgEl.textContent = "激活成功," + uname; + toast("Sora 激活成功"); + loadAccounts(); + }).catch(function(err) { + msgEl.textContent = "失败:" + parseApiErrorMessage(err); + }); +}); + +document.getElementById("btn-key-manager-create").addEventListener("click", createSoraPoolApiKey); +document.getElementById("btn-key-manager-refresh").addEventListener("click", loadSoraKeyManagement); +document.getElementById("btn-sora-key-filter-refresh").addEventListener("click", loadSoraKeyManagement); +document.getElementById("sora-key-filter-mode").addEventListener("change", loadSoraKeyManagement); +document.getElementById("sora-key-filter-scope").addEventListener("change", loadSoraKeyManagement); +document.getElementById("sora-key-filter-status").addEventListener("change", loadSoraKeyManagement); +document.querySelectorAll('input[name="sora-key-scope"]').forEach(function(input) { + input.addEventListener("change", syncSoraKeyScopeOptions); +}); +document.getElementById("sora-key-table-body").addEventListener("click", function(e) { + var btn = e.target.closest('button[data-action="disable-key"]'); + if (!btn) return; + disableSoraApiKey(btn.getAttribute("data-key-id")); +}); +syncSoraKeyScopeOptions(); + +var soraVideoTasks = []; +var soraVideoSelectedTaskId = localStorage.getItem("sora_video_selected_task_id") || ""; +var soraVideoPollers = {}; +var soraVideoUiClock = null; +var soraVideoCreateInFlight = false; +var soraVideoSnapshotRefreshInFlight = {}; +var soraVideoMediaReloadAttempts = {}; +var SORA_VIDEO_TASKS_STORAGE_KEY = "sora_video_workspace_tasks_v2"; +var SORA_VIDEO_AUTO_ROTATE_STORAGE_KEY = "sora_video_auto_rotate"; + +function normalizeSoraVideoStatus(status) { + var value = (status || "").toString().trim().toLowerCase(); + if (!value) return ""; + var aliases = { + complete: "succeeded", + completed: "succeeded", + done: "succeeded", + success: "succeeded", + succeed: "succeeded", + succeeded: "succeeded", + canceled: "cancelled", + cancelled: "cancelled", + in_progress: "running", + inprogress: "running", + processing: "running" + }; + return aliases[value] || value; +} + +function isSoraVideoSuccessStatus(status) { + return normalizeSoraVideoStatus(status) === "succeeded"; +} + +function isSoraVideoTerminalStatus(status) { + var value = normalizeSoraVideoStatus(status); + return ["succeeded", "failed", "cancelled", "rejected", "expired", "error"].indexOf(value) >= 0; +} + +function parseNumeric(value) { + var num = Number(value); + return Number.isFinite(num) ? num : null; +} + +function parseSoraDateMs(value) { + var raw = (value || "").toString().trim(); + if (!raw) return 0; + var normalized = raw.indexOf("T") >= 0 ? raw : raw.replace(" ", "T"); + var ms = Date.parse(normalized); + return Number.isFinite(ms) ? ms : 0; +} + +function formatSoraDateTime(value) { + var ms = parseSoraDateMs(value); + if (!ms) return value || "--"; + return new Date(ms).toLocaleString("zh-CN", { hour12: false }); +} + +function formatDuration(totalSeconds) { + var seconds = Math.max(0, Math.round(Number(totalSeconds) || 0)); + var hours = Math.floor(seconds / 3600); + var minutes = Math.floor((seconds % 3600) / 60); + var remain = seconds % 60; + if (hours > 0) return String(hours) + ":" + String(minutes).padStart(2, "0") + ":" + String(remain).padStart(2, "0"); + return String(minutes).padStart(2, "0") + ":" + String(remain).padStart(2, "0"); +} + +function trimVideoPrompt(value, maxLength) { + var text = (value || "").toString().trim(); + if (!text) return ""; + var max = Math.max(8, parseInt(maxLength || "48", 10) || 48); + return text.length > max ? text.slice(0, max - 1) + "…" : text; +} + +function pickSoraPreviewUrl(videoUrls) { + var urls = Array.isArray(videoUrls) ? videoUrls.slice() : []; + if (!urls.length) return ""; + function getMatchText(url) { + var text = (url || "").toLowerCase(); + try { + return decodeURIComponent(text); + } catch (_) { + return text; + } + } + function findByKeyword(keyword) { + for (var i = 0; i < urls.length; i += 1) { + if (getMatchText(urls[i]).indexOf(keyword) >= 0) return urls[i]; + } + return ""; + } + return findByKeyword("no_watermark") || + findByKeyword("downloadable") || + findByKeyword("/src.mp4") || + findByKeyword("/source.mp4") || + findByKeyword("/source_wm.mp4") || + findByKeyword("original") || + findByKeyword("/hd.mp4") || + findByKeyword("/md.mp4") || + findByKeyword("/ld.mp4") || + findByKeyword("watermarked.mp4") || + urls[0] || + ""; +} + +function getSoraVideoView(result) { + if (!result || typeof result !== "object") return {}; + return result.final_result && typeof result.final_result === "object" ? result.final_result : result; +} + +function setSoraVideoTaskId(taskId) { + var value = (taskId || "").toString().trim(); + var input = document.getElementById("sora-video-task-id"); + if (input) input.value = value; + if (value) localStorage.setItem("sora_video_last_task_id", value); + else localStorage.removeItem("sora_video_last_task_id"); +} + +function getSoraVideoTaskId() { + return (document.getElementById("sora-video-task-id").value || "").trim(); +} + +function isSoraVideoAutoRotateEnabled() { + var el = document.getElementById("sora-video-auto-rotate"); + return !!(el && el.checked); +} + +function setSoraVideoAutoRotateEnabled(enabled) { + var checked = !!enabled; + var el = document.getElementById("sora-video-auto-rotate"); + if (el) el.checked = checked; + localStorage.setItem(SORA_VIDEO_AUTO_ROTATE_STORAGE_KEY, checked ? "1" : "0"); +} + +function getSoraVideoTaskMode() { + return normalizeSoraKeyScope((document.getElementById("sora-video-task-mode").value || "text_to_video").trim()); +} + +function getSoraVideoTaskFamily() { + var el = document.getElementById("sora-video-task-family"); + var value = ((el && el.value) || SORA_TASK_FAMILY_VIDEO_GEN).trim().toLowerCase(); + return value === SORA_TASK_FAMILY_NF2 ? SORA_TASK_FAMILY_NF2 : SORA_TASK_FAMILY_VIDEO_GEN; +} + +function getSoraVideoSelectedImageFile() { + var input = document.getElementById("sora-video-image-file"); + return input && input.files && input.files[0] ? input.files[0] : null; +} + +function updateSoraVideoImageMeta() { + var metaEl = document.getElementById("sora-video-image-meta"); + var file = getSoraVideoSelectedImageFile(); + if (!metaEl) return; + if (!file) { + metaEl.textContent = "上传一张图片作为视频的起始画面。"; + return; + } + var sizeKb = Math.max(1, Math.round((Number(file.size || 0) / 1024))); + metaEl.textContent = file.name + " · " + sizeKb + " KB"; +} + +function updateSoraVideoComposerMode() { + var mode = getSoraVideoTaskMode(); + var imageField = document.getElementById("sora-video-image-field"); + var audioFields = document.getElementById("sora-video-audio-fields"); + var familyField = document.getElementById("sora-video-task-family-field"); + var promptEl = document.getElementById("sora-video-prompt"); + if (imageField) imageField.classList.toggle("hidden", mode !== SORA_KEY_SCOPE_IMAGE); + if (audioFields) audioFields.classList.toggle("hidden", mode !== SORA_KEY_SCOPE_TEXT); + if (familyField) familyField.classList.toggle("hidden", mode !== SORA_KEY_SCOPE_TEXT); + if (promptEl && !promptEl.value.trim()) { + promptEl.placeholder = mode === SORA_KEY_SCOPE_IMAGE + ? "例如:让画面中的人物轻轻转头,头发和光线自然摆动。" + : "例如:A cinematic shot of ocean waves at sunrise."; + } + updateSoraVideoImageMeta(); +} + +function apiForm(url, formData, extraOptions) { + var options = extraOptions || {}; + var headers = Object.assign({}, options.headers || {}); + if (token) headers.Authorization = "Bearer " + token; + return fetch(API_BASE + url, { + method: options.method || "POST", + body: formData, + headers: headers + }).then(async function(r) { + if (r.status === 401) { + if (!url.includes("/auth/login")) { + localStorage.removeItem("admin_token"); + window.location.reload(); + } + throw new Error(await r.text()); + } + if (!r.ok) throw new Error(await r.text()); + var ct = r.headers.get("content-type"); + if (ct && ct.includes("application/json")) return r.json(); + return r.text(); + }); +} + +function getSoraVideoPollOptions() { + return { + pollIntervalSeconds: Math.max(1, parseInt(document.getElementById("sora-video-poll-interval").value || "5", 10) || 5), + timeoutSeconds: Math.max(30, parseInt(document.getElementById("sora-video-timeout").value || "900", 10) || 900) + }; +} + +function getSoraVideoComposerPayload() { + var prompt = (document.getElementById("sora-video-prompt").value || "").trim(); + if (!prompt) throw new Error("请输入视频 prompt"); + var taskMode = getSoraVideoTaskMode(); + var autoRotate = isSoraVideoAutoRotateEnabled(); + var accountId = getSoraAccountIdFromInput(); + if (!autoRotate && !accountId) { + throw new Error("关闭自动轮换时,请先选择一个有效账号"); + } + var imageFile = getSoraVideoSelectedImageFile(); + if (taskMode === SORA_KEY_SCOPE_IMAGE && !imageFile) { + throw new Error("图生视频模式下请先上传参考图"); + } + return { + prompt: prompt, + taskMode: taskMode, + taskFamily: taskMode === SORA_KEY_SCOPE_IMAGE ? SORA_TASK_FAMILY_VIDEO_GEN : getSoraVideoTaskFamily(), + imageFile: imageFile, + autoRotate: autoRotate, + account_id: accountId, + audio_caption: taskMode === SORA_KEY_SCOPE_TEXT ? (document.getElementById("sora-video-audio-caption").value || "").trim() : "", + audio_transcript: taskMode === SORA_KEY_SCOPE_TEXT ? (document.getElementById("sora-video-audio-transcript").value || "").trim() : "", + batchCount: Math.max(1, Math.min(parseInt(document.getElementById("sora-video-batch-count").value || "1", 10) || 1, 6)), + n_variants: Math.max(1, Math.min(parseInt(document.getElementById("sora-video-variants").value || "1", 10) || 1, 4)), + n_frames: Math.max(60, parseInt(document.getElementById("sora-video-frames").value || "300", 10) || 300), + resolution: Math.max(360, parseInt(document.getElementById("sora-video-resolution").value || "360", 10) || 360), + orientation: (document.getElementById("sora-video-orientation").value || "wide").trim(), + pollIntervalSeconds: getSoraVideoPollOptions().pollIntervalSeconds, + timeoutSeconds: getSoraVideoPollOptions().timeoutSeconds + }; +} + +function serializeSoraVideoTask(task) { + return { + task_id: task.task_id, + prompt: task.prompt || "", + task_mode: task.task_mode || SORA_KEY_SCOPE_TEXT, + task_family: task.task_family || "", + status: task.status || "", + normalized_status: task.normalized_status || "", + is_terminal: !!task.is_terminal, + is_success: !!task.is_success, + used_account_id: task.used_account_id || null, + used_email: task.used_email || "", + created_local_at: task.created_local_at || "", + remote_created_at: task.remote_created_at || "", + last_update_at: task.last_update_at || "", + progress_pct: task.progress_pct, + progress_pos_in_queue: task.progress_pos_in_queue, + estimated_queue_wait_time: task.estimated_queue_wait_time, + video_urls: Array.isArray(task.video_urls) ? task.video_urls.slice(0, 4) : [], + poll_interval_seconds: task.poll_interval_seconds || 5, + timeout_seconds: task.timeout_seconds || 900, + error_message: task.error_message || "", + auto_rotate: !!task.auto_rotate, + source_image_media_id: task.source_image_media_id || "", + source_image_name: task.source_image_name || "" + }; +} + +function persistSoraVideoWorkspace() { + localStorage.setItem(SORA_VIDEO_TASKS_STORAGE_KEY, JSON.stringify(soraVideoTasks.slice(0, 18).map(serializeSoraVideoTask))); + if (soraVideoSelectedTaskId) localStorage.setItem("sora_video_selected_task_id", soraVideoSelectedTaskId); + else localStorage.removeItem("sora_video_selected_task_id"); +} + +function loadPersistedSoraVideoTasks() { + var raw = localStorage.getItem(SORA_VIDEO_TASKS_STORAGE_KEY); + if (!raw) return []; + try { + var parsed = JSON.parse(raw); + if (!Array.isArray(parsed)) return []; + return parsed.filter(function(task) { + return task && typeof task.task_id === "string" && task.task_id.trim(); + }).map(function(task) { + return { + task_id: task.task_id.trim(), + prompt: task.prompt || "", + task_mode: normalizeSoraKeyScope(task.task_mode || SORA_KEY_SCOPE_TEXT), + task_family: (task.task_family || "").trim(), + status: task.status || "", + normalized_status: task.normalized_status || "", + is_terminal: !!task.is_terminal, + is_success: !!task.is_success, + used_account_id: task.used_account_id || null, + used_email: task.used_email || "", + created_local_at: task.created_local_at || "", + remote_created_at: task.remote_created_at || "", + last_update_at: task.last_update_at || "", + progress_pct: task.progress_pct, + progress_pos_in_queue: task.progress_pos_in_queue, + estimated_queue_wait_time: task.estimated_queue_wait_time, + video_urls: Array.isArray(task.video_urls) ? task.video_urls : [], + poll_interval_seconds: task.poll_interval_seconds || 5, + timeout_seconds: task.timeout_seconds || 900, + error_message: task.error_message || "", + auto_rotate: !!task.auto_rotate, + source_image_media_id: task.source_image_media_id || "", + source_image_name: task.source_image_name || "", + polling: false, + raw_result: null + }; + }); + } catch (_) { + return []; + } +} + +function getSoraVideoTaskIndex(taskId) { + for (var i = 0; i < soraVideoTasks.length; i += 1) { + if (soraVideoTasks[i].task_id === taskId) return i; + } + return -1; +} + +function getSoraVideoTask(taskId) { + var index = getSoraVideoTaskIndex(taskId); + return index >= 0 ? soraVideoTasks[index] : null; +} + +function ensureSelectedSoraVideoTask() { + if (soraVideoSelectedTaskId && getSoraVideoTask(soraVideoSelectedTaskId)) return; + soraVideoSelectedTaskId = soraVideoTasks.length ? soraVideoTasks[0].task_id : ""; +} + +function extractSoraVideoResultMessage(result) { + var data = (result && result.data) || {}; + var error = (data && data.error) || {}; + if (typeof data.message === "string" && data.message.trim()) return data.message.trim(); + if (typeof error.message === "string" && error.message.trim()) return error.message.trim(); + if (typeof error.code === "string" && error.code.trim()) return error.code.trim(); + if (typeof result.message === "string" && result.message.trim()) return result.message.trim(); + return ""; +} + +function buildSoraVideoTaskFromResult(result, meta) { + var seed = meta || {}; + var view = getSoraVideoView(result); + var data = (view && view.data) || (result && result.data) || {}; + var normalizedStatus = normalizeSoraVideoStatus(view.normalized_status || result.normalized_status || data.status || result.status || seed.normalized_status || ""); + var rawStatus = (view.status || result.status || data.status || seed.status || "").toString().trim(); + var isSuccess = typeof view.is_success === "boolean" ? view.is_success : (typeof result.is_success === "boolean" ? result.is_success : isSoraVideoSuccessStatus(normalizedStatus || rawStatus)); + var isTerminal = typeof view.is_terminal === "boolean" ? view.is_terminal : (typeof result.is_terminal === "boolean" ? result.is_terminal : isSoraVideoTerminalStatus(normalizedStatus || rawStatus)); + var progressPct = parseNumeric(data.progress_pct); + if (isSuccess) progressPct = 100; + return { + task_id: (result.task_id || view.task_id || seed.task_id || "").trim(), + prompt: data.prompt || seed.prompt || "", + task_mode: normalizeSoraKeyScope((seed.task_mode || result.task_mode || view.task_mode || (seed.source_image_media_id || result.source_image_media_id ? SORA_KEY_SCOPE_IMAGE : SORA_KEY_SCOPE_TEXT))), + task_family: (result.task_family || view.task_family || seed.task_family || "").trim(), + status: rawStatus, + normalized_status: normalizedStatus || rawStatus, + is_terminal: !!isTerminal, + is_success: !!isSuccess, + used_account_id: result.used_account_id || view.used_account_id || seed.used_account_id || null, + used_email: result.used_email || view.used_email || seed.used_email || "", + created_local_at: seed.created_local_at || new Date().toISOString(), + remote_created_at: data.created_at || seed.remote_created_at || "", + last_update_at: new Date().toISOString(), + progress_pct: progressPct, + progress_pos_in_queue: parseNumeric(data.progress_pos_in_queue), + estimated_queue_wait_time: parseNumeric(data.estimated_queue_wait_time), + video_urls: Array.isArray(view.video_urls) ? view.video_urls : (Array.isArray(result.video_urls) ? result.video_urls : []), + poll_interval_seconds: seed.poll_interval_seconds || 5, + timeout_seconds: seed.timeout_seconds || 900, + error_message: !result.ok ? (extractSoraVideoResultMessage(result) || ("HTTP " + String(result.status_code || ""))) : "", + auto_rotate: !!seed.auto_rotate, + source_image_media_id: result.source_image_media_id || view.source_image_media_id || seed.source_image_media_id || "", + source_image_name: seed.source_image_name || "", + polling: !isTerminal, + raw_result: result + }; +} + +function upsertSoraVideoTask(taskPatch) { + var patch = taskPatch || {}; + if (!patch.task_id) return null; + var index = getSoraVideoTaskIndex(patch.task_id); + var current = index >= 0 ? soraVideoTasks[index] : null; + var next = { + task_id: patch.task_id, + prompt: patch.prompt || (current ? current.prompt : ""), + task_mode: patch.task_mode || (current ? current.task_mode : SORA_KEY_SCOPE_TEXT), + task_family: patch.task_family != null ? patch.task_family : (current ? current.task_family : ""), + status: patch.status || (current ? current.status : ""), + normalized_status: patch.normalized_status || (current ? current.normalized_status : ""), + is_terminal: typeof patch.is_terminal === "boolean" ? patch.is_terminal : (current ? current.is_terminal : false), + is_success: typeof patch.is_success === "boolean" ? patch.is_success : (current ? current.is_success : false), + used_account_id: patch.used_account_id != null ? patch.used_account_id : (current ? current.used_account_id : null), + used_email: patch.used_email || (current ? current.used_email : ""), + created_local_at: patch.created_local_at || (current ? current.created_local_at : new Date().toISOString()), + remote_created_at: patch.remote_created_at || (current ? current.remote_created_at : ""), + last_update_at: patch.last_update_at || new Date().toISOString(), + progress_pct: patch.progress_pct != null ? patch.progress_pct : (current ? current.progress_pct : null), + progress_pos_in_queue: patch.progress_pos_in_queue != null ? patch.progress_pos_in_queue : (current ? current.progress_pos_in_queue : null), + estimated_queue_wait_time: patch.estimated_queue_wait_time != null ? patch.estimated_queue_wait_time : (current ? current.estimated_queue_wait_time : null), + video_urls: Array.isArray(patch.video_urls) ? patch.video_urls : (current ? current.video_urls : []), + poll_interval_seconds: patch.poll_interval_seconds || (current ? current.poll_interval_seconds : 5), + timeout_seconds: patch.timeout_seconds || (current ? current.timeout_seconds : 900), + error_message: patch.error_message != null ? patch.error_message : (current ? current.error_message : ""), + auto_rotate: typeof patch.auto_rotate === "boolean" ? patch.auto_rotate : (current ? current.auto_rotate : false), + source_image_media_id: patch.source_image_media_id != null ? patch.source_image_media_id : (current ? current.source_image_media_id : ""), + source_image_name: patch.source_image_name != null ? patch.source_image_name : (current ? current.source_image_name : ""), + polling: typeof patch.polling === "boolean" ? patch.polling : (current ? current.polling : false), + raw_result: patch.raw_result || (current ? current.raw_result : null) + }; + if (index >= 0) soraVideoTasks[index] = next; + else soraVideoTasks.unshift(next); + soraVideoTasks = soraVideoTasks.slice(0, 18); + ensureSelectedSoraVideoTask(); + persistSoraVideoWorkspace(); + return next; +} + +function upsertSoraVideoTaskFromResult(result, meta) { + return upsertSoraVideoTask(buildSoraVideoTaskFromResult(result, meta)); +} + +function getSoraVideoTaskElapsedSeconds(task) { + var startedMs = parseSoraDateMs((task && task.remote_created_at) || (task && task.created_local_at) || ""); + if (!startedMs) return 0; + return Math.max(0, Math.round((Date.now() - startedMs) / 1000)); +} + +function getSoraVideoTaskProgressPercent(task) { + if (!task) return 0; + if (parseNumeric(task.progress_pct) != null) return Math.max(0, Math.min(100, parseNumeric(task.progress_pct))); + if (task.is_success) return 100; + if (task.is_terminal) return 100; + if (task.normalized_status === "running") return 62; + if (task.normalized_status === "queued") return 18; + return 8; +} + +function getSoraVideoTaskProgressText(task) { + if (!task) return "0%"; + if (parseNumeric(task.progress_pct) != null) return String(Math.round(parseNumeric(task.progress_pct))) + "%"; + if (task.is_success) return "100%"; + if (task.normalized_status === "running") return "生成中"; + if (task.normalized_status === "queued") return "排队中"; + return task.normalized_status || "等待中"; +} + +function getSoraVideoTaskQueueText(task) { + if (!task) return "等待中"; + var parts = []; + if (task.progress_pos_in_queue != null) parts.push("队列 #" + String(task.progress_pos_in_queue)); + if (task.estimated_queue_wait_time != null) parts.push("预计 " + formatDuration(task.estimated_queue_wait_time)); + if (parts.length) return parts.join(" · "); + if (task.is_success) return "已完成"; + if (task.is_terminal) return "已结束"; + if (task.normalized_status === "running") return "正在生成"; + return "等待中"; +} + +function setSelectedSoraVideoTask(taskId) { + soraVideoSelectedTaskId = (taskId || "").trim(); + ensureSelectedSoraVideoTask(); + persistSoraVideoWorkspace(); + renderSoraVideoWorkspace(); +} + +function renderSoraVideoOverview() { + var box = document.getElementById("sora-video-overview"); + if (!box) return; + var activeCount = soraVideoTasks.filter(function(task) { return !task.is_terminal; }).length; + var successCount = soraVideoTasks.filter(function(task) { return task.is_success; }).length; + var pollingCount = Object.keys(soraVideoPollers).length; + box.innerHTML = [ + { label: "任务总数", value: String(soraVideoTasks.length) }, + { label: "并行中", value: String(activeCount) }, + { label: "轮询中", value: String(pollingCount) }, + { label: "自动轮换", value: isSoraVideoAutoRotateEnabled() ? "已开启" : "手动账号" } + ].map(function(item) { + return '
' + escapeHtml(item.label) + '' + escapeHtml(item.value) + '
'; + }).join(""); +} + +function renderSoraVideoStage() { + var mediaEl = document.getElementById("sora-video-stage-media"); + var titleEl = document.getElementById("sora-video-stage-title"); + var promptEl = document.getElementById("sora-video-stage-prompt"); + var statusEl = document.getElementById("sora-video-stage-status"); + var progressFillEl = document.getElementById("sora-video-stage-progress-fill"); + var progressTextEl = document.getElementById("sora-video-stage-progress-text"); + var elapsedEl = document.getElementById("sora-video-stage-elapsed"); + var queueEl = document.getElementById("sora-video-stage-queue"); + var accountEl = document.getElementById("sora-video-stage-account"); + var createdEl = document.getElementById("sora-video-stage-created"); + var hintEl = document.getElementById("sora-video-stage-hint"); + var kickerEl = document.getElementById("sora-video-stage-kicker"); + var task = getSoraVideoTask(soraVideoSelectedTaskId); + if (!task) { + if (mediaEl) { + mediaEl.innerHTML = '
Sora

还没有任务,点击右上角黄色按钮开始生成。

'; + mediaEl.setAttribute("data-task-id", ""); + mediaEl.setAttribute("data-preview-url", ""); + } + if (titleEl) titleEl.textContent = "选择一个任务进行预览"; + if (promptEl) promptEl.textContent = "生成完成后,视频会显示在这个大框里;任务未完成时会显示当前进度和时间。"; + if (statusEl) { statusEl.textContent = "idle"; statusEl.className = "sora-status-badge is-pending"; } + if (progressFillEl) progressFillEl.style.width = "0%"; + if (progressTextEl) progressTextEl.textContent = "0%"; + if (elapsedEl) elapsedEl.textContent = "00:00"; + if (queueEl) queueEl.textContent = "等待中"; + if (accountEl) accountEl.textContent = "自动选择"; + if (createdEl) createdEl.textContent = "创建时间 --"; + if (hintEl) hintEl.textContent = "支持多任务并行轮询"; + if (kickerEl) kickerEl.textContent = "等待任务"; + return; + } + var previewUrl = pickSoraPreviewUrl(task.video_urls); + if (mediaEl) { + if (previewUrl) { + var renderedTaskId = mediaEl.getAttribute("data-task-id") || ""; + var renderedPreviewUrl = mediaEl.getAttribute("data-preview-url") || ""; + var videoEl = mediaEl.querySelector("video"); + if (renderedTaskId !== task.task_id || renderedPreviewUrl !== previewUrl || !videoEl) { + mediaEl.innerHTML = ''; + mediaEl.setAttribute("data-task-id", task.task_id); + mediaEl.setAttribute("data-preview-url", previewUrl); + videoEl = mediaEl.querySelector("video"); + if (videoEl) { + videoEl.addEventListener("loadeddata", function() { + delete soraVideoMediaReloadAttempts[task.task_id]; + }, { once: true }); + videoEl.addEventListener("error", function() { + var attempt = soraVideoMediaReloadAttempts[task.task_id] || 0; + if (attempt >= 1) return; + soraVideoMediaReloadAttempts[task.task_id] = attempt + 1; + refreshSoraVideoTaskSnapshot(task.task_id, { + message: "视频预览地址已刷新,正在重新加载...", + resetMediaRetry: true + }).catch(function() {}); + }, { once: true }); + } + } + } else { + mediaEl.innerHTML = '
' + escapeHtml(task.normalized_status || "task") + '

' + escapeHtml(task.prompt || "任务已创建,正在等待更多进度。") + '

'; + mediaEl.setAttribute("data-task-id", task.task_id); + mediaEl.setAttribute("data-preview-url", ""); + } + } + if (titleEl) titleEl.textContent = trimVideoPrompt(task.prompt || task.task_id, 54) || task.task_id; + if (promptEl) promptEl.textContent = task.prompt || "这个任务当前还没有返回 prompt。"; + if (statusEl) { + statusEl.textContent = task.normalized_status || "unknown"; + statusEl.className = "sora-status-badge " + (task.is_success ? "is-success" : (task.is_terminal ? "is-failed" : "is-pending")); + } + if (progressFillEl) progressFillEl.style.width = String(getSoraVideoTaskProgressPercent(task)) + "%"; + if (progressTextEl) progressTextEl.textContent = getSoraVideoTaskProgressText(task); + if (elapsedEl) elapsedEl.textContent = formatDuration(getSoraVideoTaskElapsedSeconds(task)); + if (queueEl) queueEl.textContent = getSoraVideoTaskQueueText(task); + if (accountEl) accountEl.textContent = task.used_email ? (String(task.used_account_id || "--") + " · " + task.used_email) : (task.used_account_id ? String(task.used_account_id) : "自动选择"); + if (createdEl) createdEl.textContent = "创建时间 " + formatSoraDateTime(task.remote_created_at || task.created_local_at || ""); + if (hintEl) hintEl.textContent = task.error_message || (task.is_success ? "预览默认优先使用更顺畅的中低码率流" : (task.is_terminal ? "任务已结束" : (task.polling ? "后台自动轮询中" : "点击任务卡可继续轮询"))); + if (kickerEl) { + var modeLabel = task.task_mode === SORA_KEY_SCOPE_IMAGE ? "图生视频" : "文生视频"; + var familyLabel = task.task_family === "nf2" ? "官方 App" : "旧链路"; + kickerEl.textContent = (task.is_success ? "预览就绪" : (task.is_terminal ? "任务结束" : "实时进度")) + " · " + modeLabel + " · " + familyLabel; + } +} + +function renderSoraVideoTaskList() { + var listEl = document.getElementById("sora-video-task-list"); + if (!listEl) return; + if (!soraVideoTasks.length) { + listEl.innerHTML = '
还没有任务。点击右上角黄色按钮创建后,会自动进入这里并并行轮询。
'; + return; + } + var tasks = soraVideoTasks.slice().sort(function(a, b) { + if (!!a.is_terminal !== !!b.is_terminal) return a.is_terminal ? 1 : -1; + return parseSoraDateMs(b.remote_created_at || b.created_local_at || "") - parseSoraDateMs(a.remote_created_at || a.created_local_at || ""); + }); + listEl.innerHTML = tasks.map(function(task) { + var progress = getSoraVideoTaskProgressPercent(task); + var statusClass = task.is_success ? "is-success" : (task.is_terminal ? "is-failed" : "is-pending"); + var familyLabel = task.task_family === "nf2" ? "官方 App" : "旧链路"; + return ( + '
' + + '
' + + '
' + + '

' + escapeHtml(trimVideoPrompt(task.prompt || task.task_id, 28) || task.task_id) + '

' + + '

' + escapeHtml(task.task_mode === SORA_KEY_SCOPE_IMAGE ? '图生视频' : '文生视频') + ' · ' + escapeHtml(familyLabel) + ' · task_id ' + escapeHtml(task.task_id) + '

' + + '
' + + '' + escapeHtml(task.normalized_status || 'unknown') + '' + + '
' + + '

' + escapeHtml(task.prompt || "这个任务当前还没有返回 prompt。") + '

' + + '
' + + '
进度 ' + escapeHtml(getSoraVideoTaskProgressText(task)) + '耗时 ' + escapeHtml(formatDuration(getSoraVideoTaskElapsedSeconds(task))) + '
' + + '
' + escapeHtml(getSoraVideoTaskQueueText(task)) + '账号 ' + escapeHtml(task.used_account_id ? String(task.used_account_id) : '--') + '
' + + '
' + + '' + + '' + + '' + + '
' + + '
' + ); + }).join(""); + listEl.querySelectorAll("[data-video-task-select], [data-video-task-focus]").forEach(function(node) { + node.addEventListener("click", function(event) { + var target = event.currentTarget.getAttribute("data-video-task-select") || event.currentTarget.getAttribute("data-video-task-focus") || ""; + if (target) setSelectedSoraVideoTask(target); + }); + }); + listEl.querySelectorAll("[data-video-task-copy]").forEach(function(node) { + node.addEventListener("click", function(event) { + event.stopPropagation(); + var taskId = event.currentTarget.getAttribute("data-video-task-copy") || ""; + copyTextToClipboard(taskId, "task_id 已复制"); + }); + }); + listEl.querySelectorAll("[data-video-task-toggle]").forEach(function(node) { + node.addEventListener("click", function(event) { + event.stopPropagation(); + var taskId = event.currentTarget.getAttribute("data-video-task-toggle") || ""; + var task = getSoraVideoTask(taskId); + if (!task) return; + if (task.polling) stopSoraVideoTaskPolling(taskId, { message: "已暂停 " + taskId }); + else startSoraVideoTaskPolling(taskId); + }); + }); +} + +function renderSoraVideoWorkspace() { + ensureSelectedSoraVideoTask(); + renderSoraVideoOverview(); + renderSoraVideoStage(); + renderSoraVideoTaskList(); +} + +function startSoraVideoUiClock() { + if (soraVideoUiClock) return; + soraVideoUiClock = window.setInterval(function() { + if (soraVideoTasks.length) renderSoraVideoWorkspace(); + }, 1000); +} + +function stopSoraVideoTaskPolling(taskId, options) { + var timer = soraVideoPollers[taskId]; + if (timer) { + clearTimeout(timer); + delete soraVideoPollers[taskId]; + } + var task = getSoraVideoTask(taskId); + if (task) { + upsertSoraVideoTask({ + task_id: taskId, + polling: false, + error_message: options && options.message ? options.message : task.error_message + }); + } + if (options && options.toast) toast(options.toast, options.type || "info"); + if (options && options.message) { + var msgEl = document.getElementById("sora-video-msg"); + if (msgEl) msgEl.textContent = options.message; + } + renderSoraVideoWorkspace(); +} + +function stopAllSoraVideoTaskPolling(options) { + Object.keys(soraVideoPollers).forEach(function(taskId) { + stopSoraVideoTaskPolling(taskId); + }); + if (options && options.message) { + var msgEl = document.getElementById("sora-video-msg"); + if (msgEl) msgEl.textContent = options.message; + } + if (options && options.toast) toast(options.toast, options.type || "info"); +} + +function fetchSoraVideoTask(taskId) { + var accountId = getSoraAccountIdFromInput(); + function request(body) { + return api("/api/sora-api/video-gen/get", { + method: "POST", + body: JSON.stringify(body) + }); + } + return request({ task_id: taskId }).catch(function(err) { + var message = parseApiErrorMessage(err); + if (accountId && (message.indexOf("缺少 access_token") >= 0 || message.indexOf("refresh_token") >= 0)) { + return request({ + account_id: accountId, + task_id: taskId + }); + } + throw err; + }); +} + +function refreshSoraVideoTaskSnapshot(taskId, options) { + var cleanTaskId = (taskId || "").trim(); + if (!cleanTaskId) return Promise.resolve(null); + if (soraVideoSnapshotRefreshInFlight[cleanTaskId]) return Promise.resolve(getSoraVideoTask(cleanTaskId)); + soraVideoSnapshotRefreshInFlight[cleanTaskId] = true; + var current = getSoraVideoTask(cleanTaskId) || { task_id: cleanTaskId }; + return fetchSoraVideoTask(cleanTaskId).then(function(result) { + var nextTask = upsertSoraVideoTaskFromResult(result, current); + if (nextTask && nextTask.used_account_id) setCurrentSoraAccountId(nextTask.used_account_id); + if (options && options.resetMediaRetry) delete soraVideoMediaReloadAttempts[cleanTaskId]; + if (options && options.message) { + var msgEl = document.getElementById("sora-video-msg"); + if (msgEl) msgEl.textContent = options.message; + } + renderSoraVideoWorkspace(); + return nextTask; + }).catch(function(err) { + var message = parseApiErrorMessage(err); + upsertSoraVideoTask({ task_id: cleanTaskId, error_message: message }); + renderSoraVideoWorkspace(); + throw err; + }).finally(function() { + delete soraVideoSnapshotRefreshInFlight[cleanTaskId]; + }); +} + +function startSoraVideoTaskPolling(taskId) { + var cleanTaskId = (taskId || "").trim(); + if (!cleanTaskId) return; + var task = getSoraVideoTask(cleanTaskId); + if (!task || task.is_terminal) { + stopSoraVideoTaskPolling(cleanTaskId); + return; + } + stopSoraVideoTaskPolling(cleanTaskId); + upsertSoraVideoTask({ task_id: cleanTaskId, polling: true, error_message: "" }); + renderSoraVideoWorkspace(); + function tick() { + var current = getSoraVideoTask(cleanTaskId); + if (!current) return stopSoraVideoTaskPolling(cleanTaskId); + var timeoutMs = Math.max(30000, Math.round((current.timeout_seconds || 900) * 1000)); + if (getSoraVideoTaskElapsedSeconds(current) * 1000 >= timeoutMs) { + stopSoraVideoTaskPolling(cleanTaskId, { message: "任务 " + cleanTaskId + " 轮询超时" }); + return; + } + fetchSoraVideoTask(cleanTaskId).then(function(result) { + var nextTask = upsertSoraVideoTaskFromResult(result, current); + if (nextTask && nextTask.used_account_id) { + setCurrentSoraAccountId(nextTask.used_account_id); + } + renderSoraVideoWorkspace(); + if (nextTask && nextTask.is_terminal) { + stopSoraVideoTaskPolling(cleanTaskId, { + message: nextTask.is_success ? ("任务 " + cleanTaskId + " 已成功出片") : ("任务 " + cleanTaskId + " 已结束"), + toast: nextTask.is_success ? "有任务生成完成" : "", + type: nextTask.is_success ? "success" : "info" + }); + return; + } + var currentTask = getSoraVideoTask(cleanTaskId); + if (!currentTask) return; + soraVideoPollers[cleanTaskId] = window.setTimeout(tick, Math.max(1000, Math.round((currentTask.poll_interval_seconds || 5) * 1000))); + }).catch(function(err) { + var message = parseApiErrorMessage(err); + upsertSoraVideoTask({ task_id: cleanTaskId, polling: true, error_message: message }); + renderSoraVideoWorkspace(); + var currentTask = getSoraVideoTask(cleanTaskId); + if (!currentTask) return; + soraVideoPollers[cleanTaskId] = window.setTimeout(tick, Math.max(1000, Math.round((currentTask.poll_interval_seconds || 5) * 1000))); + }); + } + tick(); +} + +function refreshAllSoraVideoTasks() { + var pending = soraVideoTasks.filter(function(task) { return !task.is_terminal; }); + var completed = soraVideoTasks.filter(function(task) { return task.is_terminal; }); + if (!pending.length && !completed.length) { + var msgEl = document.getElementById("sora-video-msg"); + if (msgEl) msgEl.textContent = "当前没有可刷新的任务"; + return; + } + pending.forEach(function(task) { + startSoraVideoTaskPolling(task.task_id); + }); + completed.forEach(function(task) { + refreshSoraVideoTaskSnapshot(task.task_id, { resetMediaRetry: true }).catch(function() {}); + }); + var msgEl = document.getElementById("sora-video-msg"); + if (msgEl) msgEl.textContent = "已刷新全部任务,未完成任务继续轮询,已完成任务会重新获取预览地址"; +} + +function clearFinishedSoraVideoTasks() { + soraVideoTasks = soraVideoTasks.filter(function(task) { return !task.is_terminal; }); + ensureSelectedSoraVideoTask(); + persistSoraVideoWorkspace(); + renderSoraVideoWorkspace(); +} + +function openSoraVideoComposer() { + var overlay = document.getElementById("sora-video-compose-overlay"); + if (overlay) overlay.classList.remove("hidden"); + updateSoraVideoComposerMode(); + var promptEl = document.getElementById("sora-video-prompt"); + if (promptEl) window.setTimeout(function() { promptEl.focus(); }, 30); +} + +function closeSoraVideoComposer() { + var overlay = document.getElementById("sora-video-compose-overlay"); + if (overlay) overlay.classList.add("hidden"); + var msgEl = document.getElementById("sora-video-compose-msg"); + if (msgEl) msgEl.textContent = ""; +} + +function createSoraVideoRequestBody(payload) { + var body = { + prompt: payload.prompt, + auto_rotate: payload.autoRotate, + task_family: payload.taskFamily || SORA_TASK_FAMILY_VIDEO_GEN, + n_variants: payload.n_variants, + n_frames: payload.n_frames, + resolution: payload.resolution, + orientation: payload.orientation, + source_image_media_id: payload.source_image_media_id || "" + }; + if (payload.audio_caption) body.audio_caption = payload.audio_caption; + if (payload.audio_transcript) body.audio_transcript = payload.audio_transcript; + if (!payload.autoRotate) body.account_id = payload.account_id; + return body; +} + +function createSoraVideoTasks() { + var composeMsgEl = document.getElementById("sora-video-compose-msg"); + var globalMsgEl = document.getElementById("sora-video-msg"); + var buttonEl = document.getElementById("btn-sora-video-create"); + var payload; + if (soraVideoCreateInFlight) return; + try { + payload = getSoraVideoComposerPayload(); + } catch (err) { + var message = parseApiErrorMessage(err); + if (composeMsgEl) composeMsgEl.textContent = message; + toast(message, "info"); + return; + } + soraVideoCreateInFlight = true; + if (buttonEl) buttonEl.disabled = true; + if (composeMsgEl) composeMsgEl.textContent = "正在发起 " + payload.batchCount + " 条任务..."; + if (globalMsgEl) globalMsgEl.textContent = "正在创建任务并加入并行队列..."; + var requests = []; + for (var i = 0; i < payload.batchCount; i += 1) { + if (payload.taskMode === SORA_KEY_SCOPE_IMAGE) { + var form = new FormData(); + form.append("prompt", payload.prompt); + form.append("auto_rotate", payload.autoRotate ? "true" : "false"); + form.append("n_variants", String(payload.n_variants)); + form.append("n_frames", String(payload.n_frames)); + form.append("resolution", String(payload.resolution)); + form.append("orientation", payload.orientation); + form.append("file", payload.imageFile, payload.imageFile.name || ("image-" + String(i + 1) + ".png")); + if (!payload.autoRotate && payload.account_id) form.append("account_id", String(payload.account_id)); + requests.push(apiForm("/api/sora-api/video-gen/create-with-image", form)); + } else { + requests.push(api("/api/sora-api/video-gen/create", { + method: "POST", + body: JSON.stringify(createSoraVideoRequestBody(payload)) + })); + } + } + Promise.allSettled(requests).then(function(results) { + var successCount = 0; + var failures = []; + results.forEach(function(entry) { + if (entry.status !== "fulfilled") { + failures.push(parseApiErrorMessage(entry.reason)); + return; + } + var result = entry.value || {}; + var taskId = (result.task_id || "").trim(); + if (!result.ok || !taskId) { + failures.push(extractSoraVideoResultMessage(result) || ("HTTP " + String(result.status_code || ""))); + return; + } + var task = upsertSoraVideoTaskFromResult(result, { + task_id: taskId, + prompt: payload.prompt, + task_mode: payload.taskMode, + used_account_id: result.used_account_id || null, + used_email: result.used_email || "", + created_local_at: new Date().toISOString(), + poll_interval_seconds: payload.pollIntervalSeconds, + timeout_seconds: payload.timeoutSeconds, + auto_rotate: payload.autoRotate, + source_image_media_id: result.source_image_media_id || "", + source_image_name: payload.imageFile ? payload.imageFile.name : "" + }); + if (task && task.used_account_id) { + setCurrentSoraAccountId(task.used_account_id); + loadSoraAccountDetails(task.used_account_id, { silent: true, skipKeyList: true }).catch(function() {}); + } + if (successCount === 0 && task) setSelectedSoraVideoTask(task.task_id); + if (task) startSoraVideoTaskPolling(task.task_id); + successCount += 1; + }); + renderSoraVideoWorkspace(); + if (successCount > 0) { + if (composeMsgEl) composeMsgEl.textContent = "已创建 " + successCount + " 条任务,正在并行轮询..."; + if (globalMsgEl) globalMsgEl.textContent = "已创建 " + successCount + " 条任务,任务墙会继续显示进度和耗时"; + toast("已创建 " + successCount + " 条任务", "success"); + if (!failures.length) window.setTimeout(closeSoraVideoComposer, 250); + } else { + if (composeMsgEl) composeMsgEl.textContent = "创建失败:" + failures.join(";"); + if (globalMsgEl) globalMsgEl.textContent = "创建失败:" + failures.join(";"); + } + if (failures.length) { + toast(failures[0], "error"); + } + }).finally(function() { + soraVideoCreateInFlight = false; + if (buttonEl) buttonEl.disabled = false; + }); +} + +function importSoraVideoTask() { + var taskId = getSoraVideoTaskId(); + var msgEl = document.getElementById("sora-video-msg"); + if (!taskId) { + if (msgEl) msgEl.textContent = "请先输入 task_id"; + toast("请先输入 task_id", "info"); + return; + } + upsertSoraVideoTask({ + task_id: taskId, + prompt: "", + created_local_at: new Date().toISOString(), + poll_interval_seconds: getSoraVideoPollOptions().pollIntervalSeconds, + timeout_seconds: getSoraVideoPollOptions().timeoutSeconds, + polling: true + }); + setSelectedSoraVideoTask(taskId); + startSoraVideoTaskPolling(taskId); + if (msgEl) msgEl.textContent = "已把 " + taskId + " 加入任务墙并开始轮询"; +} + +(function initSoraVideoTool() { + renderSoraVideoAccountSummary(null); + setSoraVideoAutoRotateEnabled(localStorage.getItem(SORA_VIDEO_AUTO_ROTATE_STORAGE_KEY) !== "0"); + soraVideoTasks = loadPersistedSoraVideoTasks(); + ensureSelectedSoraVideoTask(); + renderSoraVideoWorkspace(); + startSoraVideoUiClock(); + var savedTaskId = localStorage.getItem("sora_video_last_task_id"); + if (savedTaskId) setSoraVideoTaskId(savedTaskId); + soraVideoTasks.forEach(function(task) { + if (!task.is_terminal) startSoraVideoTaskPolling(task.task_id); + }); + var selectedTask = getSoraVideoTask(soraVideoSelectedTaskId); + if (selectedTask && selectedTask.is_terminal) { + refreshSoraVideoTaskSnapshot(selectedTask.task_id, { resetMediaRetry: true }).catch(function() {}); + } + document.getElementById("sora-video-task-id").addEventListener("change", function() { + setSoraVideoTaskId(this.value || ""); + }); + document.getElementById("sora-video-auto-rotate").addEventListener("change", function() { + setSoraVideoAutoRotateEnabled(this.checked); + renderSoraVideoOverview(); + }); + document.getElementById("sora-video-task-mode").addEventListener("change", updateSoraVideoComposerMode); + document.getElementById("sora-video-image-file").addEventListener("change", updateSoraVideoImageMeta); + document.getElementById("btn-sora-video-open-composer").addEventListener("click", openSoraVideoComposer); + document.getElementById("btn-sora-video-compose-close").addEventListener("click", closeSoraVideoComposer); + document.getElementById("btn-sora-video-compose-backdrop").addEventListener("click", closeSoraVideoComposer); + document.getElementById("btn-sora-video-create").addEventListener("click", createSoraVideoTasks); + document.getElementById("btn-sora-video-import-task").addEventListener("click", importSoraVideoTask); + document.getElementById("btn-sora-video-refresh-all").addEventListener("click", refreshAllSoraVideoTasks); + document.getElementById("btn-sora-video-stop-all").addEventListener("click", function() { + stopAllSoraVideoTaskPolling({ + message: "已暂停全部任务轮询", + toast: "已暂停全部轮询", + type: "info" + }); + }); + document.getElementById("btn-sora-video-clear-finished").addEventListener("click", clearFinishedSoraVideoTasks); + updateSoraVideoComposerMode(); +})(); + +document.getElementById("btn-go-video-page").addEventListener("click", function() { + showPage("video"); +}); + +// Emails +function loadEmails() { + document.getElementById("email-api-balance").textContent = "--"; + document.getElementById("email-api-msg").textContent = ""; + api("/api/email-api/balance").then((d) => { + document.getElementById("email-api-balance").textContent = String(d.balance); + }).catch(() => { + document.getElementById("email-api-balance").textContent = "未配置或请求失败"; + }); + api("/api/emails").then((d) => { + document.getElementById("emails-tbody").innerHTML = (d.items || []) + .map( + (r) => + ` + ${r.id} + ${escapeHtml(r.email)} + + ${escapeHtml(r.password || "")} + ${r.password ? `` : ""} + + ${escapeHtml((r.uuid || "").slice(0, 12))} + ${r.registered ? '已注册' : '未注册'} + + + + + ` + ) + .join(""); + document.getElementById("emails-tbody").querySelectorAll(".btn-op-view").forEach((btn) => { + btn.addEventListener("click", () => { + const id = btn.dataset.id; + showModal(''); + var modalContent = document.querySelector(".modal-content"); + if (modalContent) modalContent.classList.add("modal-content-wide"); + api("/api/email-api/mail-list?email_id=" + encodeURIComponent(id)) + .then((d) => { + var list = d.list || []; + function renderMailDetail(mail) { + var isObj = mail && typeof mail === "object" && !Array.isArray(mail); + var subject = isObj && (mail.subject != null || mail.title != null) ? (mail.subject ?? mail.title) : ""; + var body = isObj && (mail.body != null || mail.content != null || mail.text != null || mail.Text != null) ? (mail.body ?? mail.content ?? mail.text ?? mail.Text) : ""; + var from = isObj && mail.from != null ? mail.from : ""; + var date = isObj && mail.date != null ? mail.date : ""; + var html = isObj && (mail.html != null || mail.Html != null) ? (mail.html ?? mail.Html) : ""; + var previewHtml = ""; + if (html) previewHtml = "
" + html + "
"; + else if (body) previewHtml = "
" + escapeHtml(String(body)) + "
"; + else if (isObj) previewHtml = "
" + escapeHtml(JSON.stringify(mail, null, 2)) + "
"; + else previewHtml = "
" + escapeHtml(String(mail)) + "
"; + var rawHtml = "
" + escapeHtml(JSON.stringify(mail, null, 2)) + "
"; + return '

发件人 ' + escapeHtml(String(from)) + '

主题 ' + escapeHtml(String(subject)) + (date ? '

时间 ' + escapeHtml(String(date)) : '') + '

"; + } + function bindTabSwitch() { + document.querySelectorAll(".email-view-detail .email-tab").forEach(function(tab) { + tab.onclick = function() { + document.querySelectorAll(".email-view-detail .email-tab").forEach(function(t) { t.classList.remove("active"); }); + document.querySelectorAll(".email-view-detail .email-tab-panel").forEach(function(p) { p.classList.add("hidden"); }); + this.classList.add("active"); + var pid = "email-panel-" + this.getAttribute("data-tab"); + var panel = document.getElementById(pid); + if (panel) panel.classList.remove("hidden"); + }; + }); + } + var listHtml = '"; + var detailHtml = ''; + document.getElementById("modal-body").innerHTML = '"; + bindTabSwitch(); + document.querySelectorAll(".email-view-list-item").forEach(function(item) { + item.addEventListener("click", function() { + var idx = parseInt(this.getAttribute("data-index"), 10); + var mail = list[idx]; + if (!mail) return; + document.querySelectorAll(".email-view-list-item").forEach(function(el) { el.classList.remove("active"); }); + this.classList.add("active"); + var inner = document.querySelector(".email-view-detail-inner"); + if (inner) { + inner.innerHTML = renderMailDetail(mail) + ''; + bindTabSwitch(); + } + }); + }); + }) + .catch((err) => { + if (modalContent) modalContent.classList.remove("modal-content-wide"); + document.getElementById("modal-body").innerHTML = + ''; + }); + }); + }); + document.getElementById("emails-tbody").querySelectorAll(".btn-op.danger").forEach((btn) => { + btn.addEventListener("click", () => { + confirmBox("确定删除该邮箱?", function() { + api("/api/emails/" + btn.dataset.id, { method: "DELETE" }).then(() => { toast("已删除"); loadEmails(); }); + }); + }); + }); + document.getElementById("emails-tbody").querySelectorAll(".btn-copy-email-password").forEach((btn) => { + btn.addEventListener("click", () => { + var pwd = decodeURIComponent(btn.dataset.password || ""); + copyTextToClipboard(pwd, "邮箱密码已复制"); + }); + }); + }); +} +document.getElementById("btn-add-email").addEventListener("click", () => { + showModal(` +
+ + + + + + +
+ `); + document.getElementById("email-form").addEventListener("submit", (e) => { + e.preventDefault(); + const fd = new FormData(e.target); + api("/api/emails", { + method: "POST", + body: JSON.stringify({ + email: fd.get("email"), + password: fd.get("password"), + uuid: fd.get("uuid"), + token: fd.get("token"), + remark: fd.get("remark"), + }), + }).then(() => { hideModal(); loadEmails(); }); + }); +}); +document.getElementById("btn-batch-import-email").addEventListener("click", () => { + showModal(` +

每行一条:邮箱----密码----UUID----Token

+ + + `); + document.getElementById("email-import-submit").addEventListener("click", () => { + const lines = document.getElementById("email-import-lines").value; + api("/api/emails/batch-import", { method: "POST", body: JSON.stringify({ lines }) }).then((d) => { + hideModal(); + toast("已导入 " + d.added + " 条"); + loadEmails(); + }); + }); +}); +document.getElementById("btn-batch-export-email").addEventListener("click", () => { + api("/api/emails/export").then((d) => { + const items = d.items || []; + const lines = items.map((r) => [r.email, r.password || "", r.uuid || "", r.token || ""].join("----")); + const blob = new Blob([lines.join("\n")], { type: "text/plain;charset=utf-8" }); + const a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = "emails-" + new Date().toISOString().slice(0, 10) + ".txt"; + a.click(); + URL.revokeObjectURL(a.href); + toast("已导出 " + items.length + " 条"); + }).catch((err) => toast("导出失败: " + (err.message || "请求错误"), "error")); +}); +document.getElementById("link-to-settings").addEventListener("click", function(e) { + e.preventDefault(); + showPage("settings"); +}); +document.getElementById("btn-email-api-stock").addEventListener("click", function() { + const mailType = document.getElementById("email-api-mail-type").value; + const msg = document.getElementById("email-api-msg"); + msg.textContent = "查询中..."; + api("/api/email-api/stock?mailType=" + encodeURIComponent(mailType)).then((d) => { + msg.textContent = "库存:" + d.stock + "(" + (d.mail_type || "全部") + ")"; + }).catch((err) => { + msg.textContent = "失败:" + (err.message || "请求错误"); + }); +}); +document.getElementById("btn-email-api-fetch").addEventListener("click", function() { + const mailType = document.getElementById("email-api-mail-type").value; + const quantity = parseInt(document.getElementById("email-api-quantity").value, 10) || 1; + const msg = document.getElementById("email-api-msg"); + msg.textContent = "拉取中..."; + api("/api/email-api/fetch-mail", { + method: "POST", + body: JSON.stringify({ mail_type: mailType, quantity, import_to_emails: true }), + }).then((d) => { + msg.textContent = "拉取 " + d.count + " 条,已导入 " + d.imported + " 条"; + if (d.imported) loadEmails(); + }).catch((err) => { + msg.textContent = "失败:" + (err.message || "请求错误"); + }); +}); + +// Bank cards +function loadBankCards() { + api("/api/bank-cards").then((d) => { + document.getElementById("cards-tbody").innerHTML = (d.items || []) + .map( + (r) => + ` + + ${r.id} + ${escapeHtml(r.card_number_masked || "")} + ${r.used_count}/${r.max_use_count} + ${escapeHtml(r.remark || "")} + + ` + ) + .join(""); + document.getElementById("cards-tbody").querySelectorAll(".btn-link.danger").forEach((btn) => { + btn.addEventListener("click", () => { + confirmBox("确定删除该银行卡?", function() { + api("/api/bank-cards/" + btn.dataset.id, { method: "DELETE" }).then(() => { toast("已删除"); loadBankCards(); }); + }); + }); + }); + }); +} +document.getElementById("btn-add-card").addEventListener("click", () => { + showModal(` +
+ + + + +
+ `); + document.getElementById("card-form").addEventListener("submit", (e) => { + e.preventDefault(); + const fd = new FormData(e.target); + api("/api/bank-cards", { + method: "POST", + body: JSON.stringify({ + card_number_masked: fd.get("card_number_masked"), + card_data: fd.get("card_number_masked"), + max_use_count: parseInt(fd.get("max_use_count") || 1, 10), + remark: fd.get("remark"), + }), + }).then(() => { hideModal(); loadBankCards(); }); + }); +}); +document.getElementById("btn-batch-import-card").addEventListener("click", () => { + showModal(` +

每行一条卡信息(掩码或后四位),使用次数从系统设置读取

+ + + `); + document.getElementById("card-import-submit").addEventListener("click", () => { + const lines = document.getElementById("card-import-lines").value; + api("/api/bank-cards/batch-import", { method: "POST", body: JSON.stringify({ lines }) }).then((d) => { + hideModal(); + toast("已导入 " + d.added + " 条"); + loadBankCards(); + }); + }); +}); +document.getElementById("btn-batch-delete-card").addEventListener("click", () => { + const ids = Array.from(document.querySelectorAll(".card-id:checked")).map((c) => parseInt(c.value, 10)); + if (!ids.length) { toast("请先勾选要删除的卡", "info"); return; } + confirmBox("确定删除已选 " + ids.length + " 条银行卡?", function() { + api("/api/bank-cards/batch-delete", { method: "POST", body: JSON.stringify({ ids }) }).then(() => { + toast("已删除"); + loadBankCards(); + }); + }); +}); + +// Phones +document.getElementById("link-to-settings-phones").addEventListener("click", function(e) { + e.preventDefault(); + showPage("settings"); +}); +function refreshSmsApiSummary() { + var balanceEl = document.getElementById("sms-api-balance"); + var countEl = document.getElementById("sms-api-openai-count"); + var msgEl = document.getElementById("sms-api-msg"); + balanceEl.textContent = "--"; + countEl.textContent = "--"; + msgEl.textContent = ""; + api("/api/sms-api/openai-availability").then(function(d) { + balanceEl.textContent = String(d.balance != null ? d.balance : 0); + countEl.textContent = String(d.total_count != null ? d.total_count : 0); + if (d.service_hint && d.service_hint.length) { + msgEl.textContent = "当前服务代号不被支持。可用代号: " + d.service_hint.join(", ") + ",请到系统设置修改「OpenAI 服务 ID」"; + } + }).catch(function() { + balanceEl.textContent = "未配置或失败"; + countEl.textContent = "--"; + }); +} +function formatExpiredAtLocal(utcStr) { + if (!utcStr) return "—"; + var s = String(utcStr).trim(); + if (s.indexOf("Z") === -1 && s.indexOf("+") === -1 && s.indexOf("-") >= 0) s = s.replace(" ", "T") + "Z"; + var d = new Date(s); + if (isNaN(d.getTime())) return utcStr; + return d.toLocaleString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false }); +} +function loadPhones() { + refreshSmsApiSummary(); + var tbody = document.getElementById("phones-tbody"); + api("/api/phones?_=" + Date.now()).then((d) => { + var items = d.items || []; + tbody.innerHTML = items + .map( + (r) => + "" + + "" + + "" + r.id + "" + + "" + escapeHtml(r.phone || "") + "" + + "" + (r.used_count != null ? r.used_count : 0) + "/" + (r.max_use_count != null ? r.max_use_count : 1) + "" + + "" + escapeHtml(formatExpiredAtLocal(r.expired_at)) + "" + + "" + escapeHtml(r.remark || "") + "" + + "" + + " " + + " " + + "" + + "" + + "" + ) + .join(""); + tbody.querySelectorAll(".btn-op.sms-code").forEach(function(btn) { + btn.addEventListener("click", function() { + var id = btn.dataset.id; + btn.disabled = true; + btn.textContent = "查询中..."; + api("/api/phones/" + id + "/sms-code").then(function(d) { + btn.disabled = false; + btn.textContent = "收码"; + if (d.code) showModal("

短信验证码

" + escapeHtml(d.code) + "

" + (d.message || "") + "

"); + else toast(d.message || "等待短信中", "info"); + }).catch(function(e) { + btn.disabled = false; + btn.textContent = "收码"; + toast(e.message || "失败", "info"); + }); + }); + }); + tbody.querySelectorAll(".btn-op.release-phone").forEach(function(btn) { + btn.addEventListener("click", function() { + confirmBox("确定销毁该号码?将通知接码平台取消并从列表移除。", function() { + api("/api/phones/" + btn.dataset.id + "/release", { method: "POST" }).then(function() { + toast("已销毁"); + loadPhones(); + }).catch(function(e) { toast(e.message || "失败", "info"); }); + }); + }); + }); + tbody.querySelectorAll(".btn-op.danger").forEach(function(btn) { + btn.addEventListener("click", function() { + confirmBox("确定删除该手机号?", function() { + api("/api/phones/" + btn.dataset.id, { method: "DELETE" }).then(function() { + toast("已删除"); + loadPhones(); + }); + }); + }); + }); + }).catch(function(err) { + tbody.innerHTML = "加载失败:" + escapeHtml(err.message || "请求错误") + ""; + }); +} +document.getElementById("btn-add-phone").addEventListener("click", function() { + showModal( + "
" + + "" + + "" + + "" + + "" + + "
" + ); + document.getElementById("phone-form").addEventListener("submit", function(e) { + e.preventDefault(); + var fd = new FormData(e.target); + api("/api/phones", { + method: "POST", + body: JSON.stringify({ + phone: fd.get("phone"), + max_use_count: parseInt(fd.get("max_use_count") || 1, 10), + remark: fd.get("remark"), + }), + }).then(function() { hideModal(); toast("已添加"); loadPhones(); }); + }); +}); +document.getElementById("btn-batch-import-phone").addEventListener("click", function() { + showModal( + "

每行一个手机号,可绑定次数使用系统设置中的「手机号绑定数」。

" + + "" + + "" + ); + document.getElementById("phone-import-submit").addEventListener("click", function() { + var lines = document.getElementById("phone-import-lines").value; + api("/api/phones/batch-import", { method: "POST", body: JSON.stringify({ lines }) }).then(function(d) { + hideModal(); + toast("已导入 " + d.added + " 条"); + loadPhones(); + }); + }); +}); +document.getElementById("btn-batch-delete-phone").addEventListener("click", function() { + var ids = Array.from(document.querySelectorAll(".phone-id:checked")).map(function(c) { return parseInt(c.value, 10); }); + if (!ids.length) { toast("请先勾选要删除的手机号", "info"); return; } + confirmBox("确定删除已选 " + ids.length + " 个手机号?", function() { + api("/api/phones/batch-delete", { method: "POST", body: JSON.stringify({ ids }) }).then(function() { + toast("已删除"); + loadPhones(); + }); + }); +}); +document.getElementById("btn-sms-api-test").addEventListener("click", function() { + var msgEl = document.getElementById("sms-api-msg"); + msgEl.textContent = "测试中..."; + api("/api/sms-api/balance").then(function(d) { + msgEl.textContent = "接口正常,余额:" + d.balance; + }).catch(function(err) { + msgEl.textContent = "失败:" + (err.message || "请求错误"); + }); +}); +document.getElementById("btn-sms-api-refresh-openai").addEventListener("click", function() { + refreshSmsApiSummary(); +}); +document.getElementById("btn-sms-api-debug-prices").addEventListener("click", function() { + var msgEl = document.getElementById("sms-api-msg"); + msgEl.textContent = "加载中..."; + api("/api/sms-api/openai-availability?debug=1").then(function(d) { + msgEl.textContent = ""; + var raw = d.prices_raw; + var text = raw === undefined ? "(无 prices_raw)" : JSON.stringify(raw, null, 2); + var desc = "

接码平台 getPrices 接口的原始返回(当前「OpenAI 服务 ID」下的价格/库存)。若「OpenAI 可用数量」一直为 0,可据此核对返回结构或到系统设置中修改服务代号。

"; + showModal(desc + "
" + escapeHtml(text) + "
"); + }).catch(function(err) { + msgEl.textContent = "失败:" + (err.message || "请求错误"); + }); +}); +document.getElementById("btn-sms-api-services").addEventListener("click", function() { + var msgEl = document.getElementById("sms-api-msg"); + var country = parseInt(document.getElementById("sms-api-country").value, 10) || 0; + msgEl.textContent = "加载中..."; + api("/api/sms-api/services?country=" + country).then(function(d) { + msgEl.textContent = ""; + var list = d.services || []; + var text = list.length ? JSON.stringify(list, null, 2) : "(空),请检查 API 与 country"; + showModal("

接码平台服务列表(country=" + country + "),请找到 OpenAI 对应的 id 或 shortName 填到系统设置「OpenAI 服务 ID」:

" + escapeHtml(text) + "
"); + }).catch(function(err) { + msgEl.textContent = "失败:" + (err.message || "请求错误"); + }); +}); +document.getElementById("btn-sms-api-get-numbers").addEventListener("click", function() { + var msgEl = document.getElementById("sms-api-msg"); + var quantity = parseInt(document.getElementById("sms-api-get-quantity").value, 10) || 1; + var country = parseInt(document.getElementById("sms-api-country").value, 10) || 0; + msgEl.textContent = "获取中..."; + api("/api/sms-api/get-numbers", { + method: "POST", + body: JSON.stringify({ country: country, quantity: quantity }), + }).then(function(d) { + if (d.got) { + msgEl.textContent = "已获取 " + d.got + " 个号码并加入列表"; + } else { + var errMsg = (d.errors && d.errors[0]) ? ("获取失败:" + d.errors[0]) : "已获取 0 个号码并加入列表"; + if (d.errors && d.errors[0] === "BAD_SERVICE") errMsg += "(请到系统设置将「OpenAI 服务 ID」改为 dr 并保存)"; + msgEl.textContent = errMsg; + } + loadPhones(); + }).catch(function(err) { + msgEl.textContent = "失败:" + (err.message || "请求错误"); + }); +}); + +// 批量注册 - 仪表盘与日志 +function loadDashboard() { + api("/api/dashboard").then(function(d) { + document.getElementById("dash-today").textContent = d.today_registered != null ? d.today_registered : 0; + document.getElementById("dash-total").textContent = d.total_registered != null ? d.total_registered : 0; + document.getElementById("dash-phone").textContent = d.phone_bound_count != null ? d.phone_bound_count : 0; + document.getElementById("dash-plus").textContent = d.plus_count != null ? d.plus_count : 0; + document.getElementById("dash-sora-available").textContent = d.sora_available_count != null ? d.sora_available_count : 0; + document.getElementById("dash-sora-daily-capacity").textContent = d.today_generatable_videos != null ? d.today_generatable_videos : 0; + document.getElementById("dash-sora-generated-today").textContent = d.today_generated_videos != null ? d.today_generated_videos : 0; + document.getElementById("dash-success").textContent = d.success_count != null ? d.success_count : 0; + document.getElementById("dash-fail").textContent = d.fail_count != null ? d.fail_count : 0; + document.getElementById("dash-email-api").textContent = d.email_api_set ? "已设置" : "未设置"; + document.getElementById("dash-sms-api").textContent = d.sms_api_set ? "已设置" : "未设置"; + document.getElementById("dash-bank-api").textContent = d.bank_api_set ? "已设置" : "未设置"; + document.getElementById("dash-captcha-api").textContent = d.captcha_api_set ? "已设置" : "未设置"; + document.getElementById("dash-threads").textContent = d.thread_count != null ? d.thread_count : "1"; + api("/api/phone-bind/status").then(function(s) { + var stopBtn = document.getElementById("btn-stop-bind-phone"); + if (stopBtn) stopBtn.style.display = (s && s.running) ? "" : "none"; + }).catch(function() {}); + }).catch(function() { + document.getElementById("dash-today").textContent = "—"; + document.getElementById("dash-total").textContent = "—"; + document.getElementById("dash-phone").textContent = "—"; + document.getElementById("dash-plus").textContent = "—"; + document.getElementById("dash-sora-available").textContent = "—"; + document.getElementById("dash-sora-daily-capacity").textContent = "—"; + document.getElementById("dash-sora-generated-today").textContent = "—"; + document.getElementById("dash-success").textContent = "—"; + document.getElementById("dash-fail").textContent = "—"; + document.getElementById("dash-email-api").textContent = "—"; + document.getElementById("dash-sms-api").textContent = "—"; + document.getElementById("dash-bank-api").textContent = "—"; + document.getElementById("dash-captcha-api").textContent = "—"; + document.getElementById("dash-threads").textContent = "—"; + }); +} +var currentLogLimit = 20; +function loadLogs(limit) { + if (limit != null && limit !== undefined) currentLogLimit = Math.min(Math.max(Number(limit) || 20, 1), 100); + limit = currentLogLimit; + api("/api/logs?page=1&page_size=" + limit).then(function(d) { + var list = document.getElementById("log-list"); + var items = d.items || []; + var total = d.total || 0; + var titleEl = document.getElementById("log-panel-title"); + var expandEl = document.getElementById("log-expand-area"); + if (titleEl) titleEl.textContent = "最近 " + limit + " 条日志"; + list.classList.toggle("log-list-expanded", limit > 20); + list.innerHTML = items.length ? items.map(function(r) { + var levelClass = (r.level === "error") ? " log-line--error" : " log-line--info"; + return "
" + escapeHtml(r.created_at) + " " + escapeHtml(r.message) + "
"; + }).join("") : "
暂无日志
"; + if (expandEl) { + if (limit < 100 && total > 20) { + expandEl.innerHTML = ""; + expandEl.style.display = ""; + expandEl.className = "log-panel-expand"; + document.getElementById("btn-expand-logs").addEventListener("click", function() { loadLogs(100); }); + } else if (limit === 100 && total > 20) { + expandEl.innerHTML = "已显示最多 100 条 "; + expandEl.style.display = ""; + expandEl.className = "log-panel-expand log-panel-expand--done"; + document.getElementById("btn-collapse-logs").addEventListener("click", function() { loadLogs(20); }); + } else { + expandEl.innerHTML = ""; + expandEl.style.display = "none"; + expandEl.className = "log-panel-expand"; + } + } + }).catch(function() { + document.getElementById("log-list").innerHTML = "
加载失败
"; + var expandEl = document.getElementById("log-expand-area"); + if (expandEl) { expandEl.innerHTML = ""; expandEl.style.display = "none"; } + }); +} +document.getElementById("btn-start-register").addEventListener("click", function() { + if (this.disabled) return; + api("/api/register/start", { method: "POST" }).then(function(d) { + if (d && d.ok) { + toast(d.message || "已启动注册任务", "success"); + updateRegisterStatusOnce(); + loadDashboard(); + loadLogs(); + } else { + toast(d.message || "启动失败", "error"); + } + }).catch(function(err) { + toast(err.message || "请求失败", "error"); + }); +}); +document.getElementById("btn-stop-register").addEventListener("click", function() { + api("/api/register/stop", { method: "POST" }).then(function(d) { + if (d && d.ok) { + toast(d.message || "已请求停止", "info"); + updateRegisterStatusOnce(); + } else { + toast(d.message || "操作失败", "error"); + } + }).catch(function(err) { + toast(err.message || "请求失败", "error"); + }); +}); +document.getElementById("btn-start-bind-phone").addEventListener("click", function() { + var btn = this; + var stopBtn = document.getElementById("btn-stop-bind-phone"); + var bindCountEl = document.getElementById("phone-bind-max-count"); + var bindCountRaw = bindCountEl ? String(bindCountEl.value || "").trim() : ""; + var bindCount = null; + var url = "/api/phone-bind/start"; + if (bindCountRaw !== "") { + bindCount = parseInt(bindCountRaw, 10); + if (!isFinite(bindCount) || bindCount < 1) { + toast("绑定数量必须是大于 0 的整数", "error"); + return; + } + bindCount = Math.min(bindCount, 100); + if (bindCountEl) bindCountEl.value = String(bindCount); + url += "?max_count=" + encodeURIComponent(bindCount); + } + btn.disabled = true; + api(url, { method: "POST" }).then(function(d) { + if (d.ok) { + var msg = "绑定任务已启动"; + if (bindCount != null) msg += ",按 " + bindCount + " 并发执行,目标成功 " + bindCount + " 个"; + msg += ",task_id: " + (d.task_id || ""); + toast(msg, "success"); + if (stopBtn) stopBtn.style.display = ""; + loadDashboard(); + loadLogs(); + } else { + toast(d.message || "启动失败", "info"); + } + }).catch(function(err) { + toast(err.message || "请求失败", "error"); + }).finally(function() { + btn.disabled = false; + }); +}); +document.getElementById("btn-stop-bind-phone").addEventListener("click", function() { + api("/api/phone-bind/stop", { method: "POST" }).then(function(d) { + toast(d.message || "已请求停止", "info"); + }).catch(function(err) { + toast(err.message || "请求失败", "error"); + }); +}); +document.getElementById("btn-start-plus").addEventListener("click", function() { + toast("开始开通 Plus 功能开发中", "info"); +}); +document.getElementById("btn-refresh-dashboard").addEventListener("click", function() { + loadDashboard(); + loadLogs(); +}); +document.getElementById("btn-clear-logs").addEventListener("click", function() { + confirmBox("确定清空所有日志?", function() { + api("/api/logs", { method: "DELETE" }).then(function(d) { + toast(d.message || "已清空日志", "success"); + loadDashboard(); + loadLogs(); + }).catch(function(err) { + toast(err.message || "清空失败", "error"); + }); + }); +}); + +// Settings +var SETTINGS_KEYS = [ + "sms_api_url", "sms_api_key", "sms_openai_service", "sms_max_price", "thread_count", "proxy_url", "proxy_api_url", + "bank_card_api_url", "bank_card_api_key", "bank_card_api_platform", "email_api_url", "email_api_key", "email_api_default_type", + "captcha_api_url", "captcha_api_key", "oauth_client_id", "oauth_redirect_uri", + "retry_count", "card_use_limit", "phone_bind_limit" +]; +function loadSettings() { + api("/api/settings").then((d) => { + const form = document.getElementById("settings-form"); + SETTINGS_KEYS.forEach((k) => { + const el = form.querySelector(`[name="${k}"]`); + if (el) el.value = d[k] != null ? d[k] : ""; + }); + }); +} +document.getElementById("settings-form").addEventListener("submit", (e) => { + e.preventDefault(); + const fd = new FormData(e.target); + const body = {}; + SETTINGS_KEYS.forEach((k) => { body[k] = fd.get(k) || ""; }); + api("/api/settings", { method: "PUT", body: JSON.stringify(body) }) + .then(() => { + toast("已保存"); + loadSettings(); + }) + .catch((err) => { + toast(err && err.message ? err.message : "保存失败"); + }); +}); + +function escapeHtml(s) { + if (s == null) return ""; + const div = document.createElement("div"); + div.textContent = s; + return div.innerHTML; +} + +// 点击用户名弹出修改账号密码 +document.getElementById("current-user").addEventListener("click", function() { + showModal(` + + `); + document.getElementById("login-update-form").addEventListener("submit", function(e) { + e.preventDefault(); + var fd = new FormData(this); + var username = (fd.get("admin_username") || "").toString().trim(); + var password = (fd.get("admin_password") || "").toString(); + if (!username || !password) { + toast("账号与密码均不能为空", "error"); + return; + } + api("/api/settings/login", { method: "PUT", body: JSON.stringify({ admin_username: username, admin_password: password }) }) + .then(function() { + hideModal(); + toast("已修改,请重新登录"); + localStorage.removeItem("admin_token"); + window.location.reload(); + }) + .catch(function(err) { + toast(err.message || "保存失败", "error"); + }); + }); +}); + +// Default tab(登录后默认打开批量注册) +if (token) showPage("logs"); diff --git a/Register_GPT_v0/web/frontend/static/style.css b/Register_GPT_v0/web/frontend/static/style.css new file mode 100644 index 0000000..b9aef96 --- /dev/null +++ b/Register_GPT_v0/web/frontend/static/style.css @@ -0,0 +1,2429 @@ +/* 浅色柔和风格:圆角、阴影、Plus Jakarta Sans 字体 */ +:root { + --bg: #f1f5f9; + --bg-card: #ffffff; + --bg-input: #f8fafc; + --border: #e2e8f0; + --border-focus: #94a3b8; + --text: #1e293b; + --text-muted: #64748b; + --accent: #6366f1; + --accent-hover: #4f46e5; + --accent-light: #eef2ff; + --danger: #dc2626; + --danger-bg: #fef2f2; + --radius: 12px; + --radius-sm: 8px; + --shadow: 0 1px 3px rgba(0,0,0,0.06); + --shadow-md: 0 4px 12px rgba(0,0,0,0.08); + --font: "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +* { box-sizing: border-box; } +body { + margin: 0; + font-family: var(--font); + font-size: 14px; + background: var(--bg); + color: var(--text); + line-height: 1.6; + -webkit-font-smoothing: antialiased; +} + +.hidden { display: none !important; } + +/* 登录页 */ +#login-page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #e0e7ff 0%, #f1f5f9 50%, #fce7f3 100%); +} +.login-box { + width: 360px; + padding: 2.25rem; + background: var(--bg-card); + border-radius: var(--radius); + box-shadow: var(--shadow-md); + border: 1px solid var(--border); +} +.login-box h1 { + margin: 0 0 1.75rem; + font-size: 1.35rem; + font-weight: 700; + text-align: center; + color: var(--text); + letter-spacing: -0.02em; +} +.login-box input { + width: 100%; + padding: 0.75rem 1rem; + margin-bottom: 0.75rem; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text); + font-family: var(--font); + font-size: 14px; + transition: border-color 0.2s; +} +.login-box input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-light); +} +.login-box input::placeholder { color: var(--text-muted); } +.login-box button { + width: 100%; + padding: 0.75rem 1rem; + margin-top: 0.5rem; + background: var(--accent); + border: none; + border-radius: var(--radius-sm); + color: #fff; + font-family: var(--font); + font-weight: 600; + font-size: 14px; + cursor: pointer; + transition: background 0.2s; +} +.login-box button:hover { background: var(--accent-hover); } +.login-box .error { + margin-top: 0.75rem; + color: var(--danger); + font-size: 13px; +} + +/* 后台布局:左右结构 */ +.layout-sidebar { + display: flex; + min-height: 100vh; +} +.sidebar { + width: 220px; + flex-shrink: 0; + background: var(--bg-card); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + box-shadow: var(--shadow); + transition: width 0.2s ease; +} +.sidebar-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 0.75rem; + border-bottom: 1px solid var(--border); + min-height: 52px; + gap: 0.5rem; +} +.sidebar .logo { + padding: 0; + font-weight: 700; + font-size: 1rem; + color: var(--text); + letter-spacing: -0.02em; + border-bottom: none; + line-height: 1.3; + display: flex; + align-items: center; + gap: 0.5rem; + min-width: 0; +} +.sidebar .logo-icon { + flex-shrink: 0; + width: 28px; + height: 28px; + background: var(--accent); + color: #fff; + border-radius: var(--radius-sm); + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 14px; +} +.sidebar .logo-text { + white-space: nowrap; + overflow: hidden; +} +.sidebar-toggle { + flex-shrink: 0; + width: 32px; + height: 32px; + padding: 0; + border: none; + background: var(--bg-input); + border-radius: var(--radius-sm); + color: var(--text-muted); + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + transition: color 0.2s, background 0.2s; +} +.sidebar-toggle:hover { + color: var(--accent); + background: var(--accent-light); +} +.sidebar-toggle svg { + transition: transform 0.2s ease; +} +.sidebar:not(.collapsed) .sidebar-toggle svg { + transform: rotate(180deg); +} +/* 收起时仅显示图标,侧栏加宽、图标加大且居中 */ +.sidebar.collapsed { + width: 80px; +} +/* 收起时:头部只保留 Logo 居中,展开/收起按钮移到底部单独一行,不遮挡 */ +.sidebar.collapsed .sidebar-header { + padding: 0.5rem 0; + justify-content: center; + min-height: 56px; +} +.sidebar.collapsed .sidebar-header .sidebar-toggle-in-header { + display: none; +} +.sidebar:not(.collapsed) .sidebar-toggle-in-footer { + display: none; +} +.sidebar.collapsed .logo { + width: 56px; + height: 56px; + min-width: 56px; + min-height: 56px; + padding: 0; + margin: 0; + justify-content: center; + align-items: center; +} +.sidebar.collapsed .logo-icon { + width: 32px; + height: 32px; + font-size: 16px; +} +/* 隐藏文字完全移出布局流,避免影响图标居中 */ +.sidebar.collapsed .logo-text, +.sidebar.collapsed .nav-text, +.sidebar.collapsed .footer-text, +.sidebar.collapsed .toggle-footer-text { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + pointer-events: none; +} +.sidebar.collapsed .user-name { + padding: 0.6rem; + min-height: 44px; + display: flex; + align-items: center; + justify-content: center; + background: var(--bg-input); +} +.sidebar.collapsed .user-name .user-name-text { + overflow: hidden; + width: 0; + opacity: 0; + padding: 0; + margin: 0; + white-space: nowrap; + pointer-events: none; +} +.sidebar.collapsed .user-name .user-name-icon { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--text-muted); +} +.sidebar.collapsed .user-name .user-name-icon svg { + display: block; +} +/* 收起时:每个菜单/底部项为相同尺寸的正方形,左右对称留白,图标在框内居中 */ +.sidebar.collapsed .nav { + align-items: center; + padding: 0.5rem 12px; + gap: 4px; +} +.sidebar.collapsed .nav a { + width: 56px; + height: 56px; + min-width: 56px; + min-height: 56px; + padding: 0; + margin: 0 auto; + display: flex; + justify-content: center; + align-items: center; + border-radius: var(--radius-sm); +} +.sidebar.collapsed .nav a .nav-icon { + width: 24px; + height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; +} +.sidebar.collapsed .nav a .nav-icon svg { + width: 22px; + height: 22px; +} +.sidebar.collapsed .sidebar-footer { + align-items: center; + gap: 4px; + padding: 0.5rem 12px; +} +.sidebar.collapsed .sidebar-link { + width: 56px; + height: 56px; + min-width: 56px; + min-height: 56px; + padding: 0; + margin: 0 auto; + display: flex; + justify-content: center; + align-items: center; + border-radius: var(--radius-sm); +} +.sidebar.collapsed .sidebar-link .nav-icon { + display: inline-flex; + align-items: center; + justify-content: center; +} +.sidebar.collapsed .sidebar-link .nav-icon svg { + width: 20px; + height: 20px; +} +.sidebar.collapsed .user-name { + width: 56px; + height: 56px; + min-width: 56px; + min-height: 56px; + padding: 0; + margin: 0 auto; + display: flex; + justify-content: center; + align-items: center; + border-radius: var(--radius-sm); +} +.sidebar.collapsed .btn-logout { + width: 56px; + height: 56px; + min-width: 56px; + min-height: 56px; + padding: 0; + margin: 0 auto; + display: block; + text-indent: -999px; + font-size: 0; + background: var(--bg-input) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2'%3E%3Cpath d='M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4'/%3E%3Cpolyline points='16 17 21 12 16 7'/%3E%3Cline x1='21' y1='12' x2='9' y2='12'/%3E%3C/svg%3E") center no-repeat; + border: 1px solid var(--border); + border-radius: var(--radius-sm); +} +/* 收起时底部展开按钮:与其它项一致的 56x56 正方形 */ +.sidebar-toggle-in-footer { + width: 100%; + padding: 0.4rem 0.75rem; + margin: 0; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-muted); + cursor: pointer; + font-family: var(--font); + font-size: 12px; + transition: color 0.2s, background 0.2s, border-color 0.2s; +} +.sidebar-toggle-in-footer:hover { + color: var(--accent); + background: var(--accent-light); + border-color: var(--accent); +} +.sidebar-toggle-in-footer .nav-icon { + display: inline-flex; + align-items: center; + justify-content: center; +} +.sidebar.collapsed .sidebar-toggle-in-footer { + width: 56px; + height: 56px; + min-width: 56px; + min-height: 56px; + padding: 0; + margin: 0 auto; + display: flex; + justify-content: center; + align-items: center; + border-radius: var(--radius-sm); +} +.sidebar.collapsed .sidebar-toggle-in-footer .nav-icon svg { + width: 22px; + height: 22px; +} +.sidebar .nav { + flex: 1; + padding: 0.75rem 0.5rem; + display: flex; + flex-direction: column; + gap: 2px; +} +.sidebar .nav a { + padding: 0.6rem 1rem; + color: var(--text-muted); + text-decoration: none; + border-radius: var(--radius-sm); + font-weight: 500; + font-size: 13px; + transition: color 0.2s, background 0.2s; + display: flex; + align-items: center; + gap: 0.75rem; +} +.sidebar .nav a .nav-icon { + flex-shrink: 0; + width: 24px; + height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + color: inherit; +} +.sidebar .nav a .nav-icon svg { + display: block; + width: 22px; + height: 22px; +} +.sidebar .nav a .nav-text { + white-space: nowrap; + overflow: hidden; +} +.sidebar .nav a:hover { color: var(--accent); background: var(--accent-light); } +.sidebar .nav a.active { color: var(--accent); background: var(--accent-light); } +.sidebar-footer { + padding: 1rem 0.75rem; + border-top: 1px solid var(--border); + display: flex; + flex-direction: column; + gap: 0.6rem; +} +.sidebar-footer .sidebar-link { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 12px; + color: var(--text); + text-decoration: none; + padding: 0.45rem 0.75rem; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + transition: color 0.2s, border-color 0.2s, background 0.2s; +} +.sidebar-footer .sidebar-link .nav-icon { + flex-shrink: 0; + width: 20px; + height: 20px; + display: inline-flex; + align-items: center; + justify-content: center; +} +.sidebar-footer .sidebar-link .nav-icon svg { + width: 18px; + height: 18px; +} +.sidebar-footer .sidebar-link:hover { + color: var(--accent); + border-color: var(--accent); + background: var(--accent-light); +} +.sidebar-footer .user-name { + font-size: 13px; + font-weight: 500; + color: var(--text); + padding: 0.5rem 0.75rem; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg-input); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 0.5rem; +} +.sidebar-footer .user-name .user-name-icon { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--text-muted); +} +.sidebar-footer .user-name .user-name-icon svg { + display: block; + width: 18px; + height: 18px; +} +.sidebar-footer .user-name .user-name-text { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} +.sidebar-footer .user-name-clickable { + cursor: pointer; +} +.sidebar-footer .user-name-clickable:hover { + border-color: var(--accent); + color: var(--accent); +} +.sidebar-footer .btn-logout { + padding: 0.4rem 0.75rem; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-muted); + cursor: pointer; + font-family: var(--font); + font-size: 12px; + transition: color 0.2s, border-color 0.2s; +} +.sidebar-footer .btn-logout:hover { color: var(--text); border-color: var(--border-focus); } + +.main { flex: 1; display: flex; flex-direction: column; min-height: 0; padding: 1.5rem; overflow: hidden; min-width: 0; } +.main > .panel { flex: 1; min-height: 0; overflow: auto; display: flex; flex-direction: column; } +#panel-logs.panel { overflow: hidden; } +.panel h2 { + margin: 0 0 0.5rem; + font-size: 1.125rem; + font-weight: 600; + color: var(--text); + letter-spacing: -0.01em; +} +.panel-desc { + margin: 0 0 1rem; + font-size: 12px; + color: var(--text-muted); +} +.panel-desc a { color: var(--accent); text-decoration: none; } +.panel-desc a:hover { text-decoration: underline; } +.toolbar { + margin-bottom: 1.25rem; + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + align-items: center; +} +.toolbar label { display: flex; align-items: center; gap: 0.5rem; font-weight: 500; color: var(--text); } +.toolbar select, .toolbar input[type="text"], .toolbar input[type="number"] { + padding: 0.5rem 0.75rem; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text); + font-family: var(--font); + font-size: 13px; +} +.toolbar select:focus, .toolbar input:focus { + outline: none; + border-color: var(--accent); +} +.toolbar button { + padding: 0.5rem 1rem; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text); + font-family: var(--font); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: background 0.2s, border-color 0.2s, color 0.2s; +} +.toolbar button:hover { background: var(--accent-light); border-color: var(--accent); color: var(--accent); } + +.table-wrap { + overflow-x: auto; + border-radius: var(--radius); + border: 1px solid var(--border); + background: var(--bg-card); + box-shadow: var(--shadow); +} +.data-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} +.data-table th, .data-table td { + padding: 0.65rem 1rem; + text-align: left; + border-bottom: 1px solid var(--border); +} +.data-table th { + background: var(--bg-input); + color: var(--text-muted); + font-weight: 600; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.03em; +} +.data-table tr:last-child td { border-bottom: none; } +.data-table tr:hover td { background: var(--accent-light); } +.data-table .btn-link { + background: none; + border: none; + color: var(--accent); + cursor: pointer; + padding: 0; + font-family: var(--font); + font-size: 13px; + font-weight: 500; +} +.data-table .btn-link:hover { text-decoration: underline; } +.data-table .btn-link.danger { color: var(--danger); } + +/* 手机号操作列:销毁 / 删除 / 收码 小按钮 */ +.data-table .btn-op { + display: inline-block; + padding: 0.28rem 0.6rem; + margin: 0 0.15rem 0.15rem 0; + font-family: var(--font); + font-size: 12px; + font-weight: 500; + border-radius: 6px; + border: 1px solid transparent; + cursor: pointer; + transition: background 0.2s, border-color 0.2s, color 0.2s; +} +.data-table .btn-op.release-phone { + background: #f0f9ff; + border-color: #0ea5e9; + color: #0369a1; +} +.data-table .btn-op.release-phone:hover { + background: #e0f2fe; + border-color: #0284c7; + color: #075985; +} +.data-table .btn-op.danger { + background: #fef2f2; + border-color: #f87171; + color: #b91c1c; +} +.data-table .btn-op.danger:hover { + background: #fee2e2; + border-color: #ef4444; + color: #991b1b; +} +.data-table .btn-op.sms-code { + background: #f0fdf4; + border-color: #22c55e; + color: #15803d; +} +.data-table .btn-op.sms-code:hover { + background: #dcfce7; + border-color: #16a34a; + color: #166534; +} +.data-table .btn-op.btn-op-view { + background: #eef2ff; + border-color: #6366f1; + color: #4338ca; +} +.data-table .btn-op.btn-op-view:hover { + background: #e0e7ff; + border-color: #4f46e5; + color: #3730a3; +} +.data-table td .btn-op { text-decoration: none; } +.data-table td .btn-op:hover { text-decoration: none; } + +.pagination { margin-top: 1rem; font-size: 13px; color: var(--text-muted); } +.pagination button { + padding: 0.35rem 0.65rem; + margin-right: 0.25rem; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text); + cursor: pointer; + font-family: var(--font); + font-size: 13px; +} +.pagination button:hover { background: var(--accent-light); border-color: var(--accent); color: var(--accent); } + +/* 日志区域:黑色终端风格,浅色文字 */ +.log-list { + font-family: "Consolas", "Monaco", "Courier New", monospace; + font-size: 12px; + line-height: 1.5; + max-height: 400px; + overflow-y: auto; + background: #1a1a1a; + border: 1px solid #333; + border-radius: var(--radius-sm); + padding: 0.75rem 1rem; + color: #d4d4d4; +} +.log-list .log-line { + padding: 0.25rem 0; + border-bottom: 1px solid #2d2d2d; + word-break: break-all; +} +.log-list .log-line:last-child { border-bottom: none; } +.log-list .log-line .ts { + color: #858585; + margin-right: 0.5rem; + font-size: 11px; +} +.log-list .log-line--error .ts { color: #f59e0b; } +.log-list .log-line--error { color: #fbbf24; } +.log-list .log-line--info .ts { color: #6b7280; } +.log-list .log-line--info { color: #e5e7eb; } +.log-list-fixed { overflow-y: auto; } +.log-list.log-list-fixed { max-height: none; } + +/* 批量注册 - 仪表盘 */ +.dashboard-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.25rem; + margin-bottom: 0.75rem; + box-shadow: var(--shadow); +} +.dashboard-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.75rem; + margin-bottom: 1.25rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--border); +} +.dashboard-inline-field { + display: inline-flex; + align-items: center; + gap: 0.5rem; + min-height: 40px; + padding: 0 0.8rem; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg-input); + color: var(--text-muted); + font-size: 13px; +} +.dashboard-inline-field span { + color: var(--text-muted); + white-space: nowrap; +} +.dashboard-inline-field input { + width: 72px; + padding: 0.3rem 0.45rem; + border: 1px solid var(--border); + border-radius: 8px; + background: #fff; + color: var(--text); + font-family: var(--font); + font-size: 13px; +} +.dashboard-inline-field input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.12); +} +.btn-dash { + padding: 0.5rem 1.25rem; + font-family: var(--font); + font-size: 14px; + font-weight: 600; + border-radius: var(--radius-sm); + border: none; + cursor: pointer; + transition: background 0.2s, transform 0.1s; +} +.btn-dash:hover { transform: translateY(-1px); } +.btn-dash.btn-primary { + background: var(--accent); + color: #fff; +} +.btn-dash.btn-primary:hover { background: var(--accent-hover); } +.btn-dash.btn-secondary { + background: var(--bg-input); + color: var(--text); + border: 1px solid var(--border); +} +.btn-dash.btn-secondary:hover { background: var(--border); } +.btn-dash:disabled, +.btn-dash.btn-dash-disabled { + opacity: 0.85; + cursor: not-allowed; + transform: none; +} +.btn-dash.btn-dash-disabled { background: #6b7280; color: #fff; } +.register-heartbeat { + font-size: 11px; + color: #dc2626; + margin-left: 0.25rem; + padding: 0.25rem 0.5rem; + border: 1px solid #dc2626; + border-radius: var(--radius-sm); + display: inline-flex; + align-items: center; + line-height: 1; +} +.dashboard-stats { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 0.75rem; +} +.dashboard-stats .stat-item { + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 0.6rem 0.75rem; + display: flex; + flex-direction: column; + gap: 0.2rem; +} +.dashboard-stats .stat-label { + font-size: 11px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.03em; +} +.dashboard-stats .stat-value { + font-size: 1.1rem; + font-weight: 700; + color: var(--text); +} +.dashboard-stats .stat-value.stat-ok { color: #15803d; } +.dashboard-stats .stat-value.stat-fail { color: var(--danger); } +.dashboard-stats .stat-value.stat-value-muted { + font-size: 12px; + font-weight: 400; + color: var(--text-muted); +} + +.status-registered { color: #15803d; font-weight: 400; font-size: 12px; } +.status-unregistered { color: var(--text-muted); font-weight: 400; font-size: 12px; } +/* 日志面板:标题栏 + 黑色日志框一体 */ +.log-panel { + margin-top: 0.5rem; + background: #1a1a1a; + border: 1px solid #333; + border-radius: var(--radius-sm); + overflow: hidden; +} +.log-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5rem 1rem; + background: #1a1a1a; + border-bottom: 1px solid #2d2d2d; + color: #d4d4d4; + font-size: 12px; + font-weight: 600; +} +.log-panel-title { + color: #d4d4d4; +} +.log-panel-actions { + display: flex; + align-items: center; + gap: 0.5rem; +} +.log-panel-refresh { + padding: 0.25rem 0.6rem; + font-size: 12px; + color: #858585; + background: transparent; + border: 1px solid #444; + border-radius: var(--radius-sm); + cursor: pointer; + font-family: var(--font); + transition: color 0.2s, border-color 0.2s; +} +.log-panel-refresh:hover { + color: #d4d4d4; + border-color: #666; +} +.log-panel-refresh.log-panel-clear { + color: #f87171; + border-color: #dc2626; +} +.log-panel-refresh.log-panel-clear:hover { + color: #fca5a5; + border-color: #ef4444; +} +/* 日志面板内列表高度自适应,占满剩余空间 */ +#panel-logs .log-panel { + flex: 1; + min-height: 200px; + display: flex; + flex-direction: column; + overflow: hidden; +} +.log-panel .log-list { + border: none; + border-radius: 0; + flex: 1; + min-height: 120px; + overflow-y: auto; + overflow-x: hidden; +} +.log-panel .log-list.log-list-expanded { + min-height: 160px; +} +/* 日志区滚动条美化(深色主题,仅列表内一条滚动条) */ +.log-panel .log-list::-webkit-scrollbar { + width: 6px; +} +.log-panel .log-list::-webkit-scrollbar-track { + background: #1a1a1a; + border-radius: 3px; +} +.log-panel .log-list::-webkit-scrollbar-thumb { + background: #404040; + border-radius: 3px; +} +.log-panel .log-list::-webkit-scrollbar-thumb:hover { + background: #525252; +} +.log-panel .log-list::-webkit-scrollbar-thumb:active { + background: #737373; +} +.log-panel .log-list { + scrollbar-width: thin; + scrollbar-color: #404040 #1a1a1a; + scrollbar-gutter: stable; +} +.log-panel-expand { + flex-shrink: 0; + padding: 0.5rem 1rem; + border-bottom: 1px solid var(--border, #333); + background: #151515; + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; +} +.log-panel-expand:empty, +.log-panel-expand[style*="display: none"] { + display: none !important; +} +.log-panel-expand--done { + padding: 0.45rem 1rem; +} +.log-panel-expand-btn { + display: inline-flex; + align-items: center; + gap: 0.35rem; + background: #252525; + border: 1px solid #4b5563; + color: #d1d5db; + padding: 0.4rem 0.85rem; + font-size: 0.8125rem; + cursor: pointer; + border-radius: 6px; + font-family: var(--font); + transition: background 0.15s, border-color 0.15s, color 0.15s; +} +.log-panel-expand-btn:hover { + background: #2d2d2d; + border-color: #6b7280; + color: #f3f4f6; +} +.log-panel-expand-icon { + font-size: 0.6rem; + opacity: 0.85; +} +.log-panel-expand-done { + color: #9ca3af; + font-size: 0.8125rem; +} +.log-panel-collapse-btn { + background: transparent; + border: none; + color: #6b7280; + font-size: 0.8125rem; + cursor: pointer; + padding: 0.2rem 0.4rem; + font-family: var(--font); + text-decoration: underline; + text-underline-offset: 2px; +} +.log-panel-collapse-btn:hover { + color: #9ca3af; +} + +/* 邮箱管理 - Hotmail007 拉取卡片 */ +.email-api-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.25rem; + margin-bottom: 1.25rem; + box-shadow: var(--shadow); +} +.email-api-card h3 { margin: 0 0 0.5rem; font-size: 0.95rem; font-weight: 600; color: var(--text); } +.email-api-desc { margin: 0 0 0.75rem; font-size: 12px; color: var(--text-muted); } +.email-api-desc a { color: var(--accent); text-decoration: none; } +.email-api-desc a:hover { text-decoration: underline; } +.email-api-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; } +.email-api-row label { display: flex; align-items: center; gap: 0.35rem; font-size: 13px; } +.email-api-row input[type="number"] { width: 70px; padding: 0.35rem 0.5rem; border: 1px solid var(--border); border-radius: var(--radius-sm); } +.email-api-row input[type="text"] { padding: 0.35rem 0.5rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-input); } +.email-api-row select { padding: 0.35rem 0.5rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-input); } +.email-api-row button { padding: 0.4rem 0.75rem; font-size: 12px; background: var(--accent); color: #fff; border: none; border-radius: var(--radius-sm); cursor: pointer; font-family: var(--font); } +.email-api-row button:hover { background: var(--accent-hover); } +.email-api-row button.btn-default { background: var(--bg-input); color: var(--text); border: 1px solid var(--border); } +.email-api-row button.btn-default:hover { background: var(--accent-light); border-color: var(--accent); color: var(--accent); } +.sora-key-list { + margin-top: 0.65rem; + border-top: 1px dashed var(--border); + padding-top: 0.65rem; + color: var(--text-muted); + font-size: 12px; + line-height: 1.6; +} +.sora-key-list .key-item { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.35rem; +} +.sora-key-list .key-tag { + display: inline-flex; + align-items: center; + height: 20px; + border-radius: 999px; + padding: 0 0.5rem; + background: var(--accent-light); + color: var(--accent-hover); +} +.email-api-msg { margin: 0.5rem 0 0; font-size: 12px; color: var(--text-muted); } +.key-page-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; +} +.key-page-head h2 { + margin-bottom: 0.35rem; +} +.key-page-head-actions { + display: flex; + align-items: center; + gap: 0.75rem; +} +.key-manager-grid { + display: grid; + grid-template-columns: minmax(0, 1.28fr) minmax(300px, 0.82fr); + gap: 1.25rem; + margin-bottom: 1.25rem; + align-items: start; +} +.key-manager-card { + background: var(--bg-card); + border: 1px solid rgba(148, 163, 184, 0.18); + border-radius: 24px; + padding: 1.25rem; + box-shadow: var(--shadow); +} +.key-manager-card-create { + background: + radial-gradient(circle at top right, rgba(250, 204, 21, 0.18), transparent 34%), + linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(255, 255, 255, 0.98)); +} +.key-manager-card-stats { + background: + radial-gradient(circle at top left, rgba(250, 204, 21, 0.14), transparent 34%), + linear-gradient(180deg, rgba(255, 251, 235, 0.96), rgba(255, 255, 255, 0.96)); +} +.key-manager-card-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; +} +.key-manager-card-head h3 { + margin: 0 0 0.35rem; +} +.key-manager-card-head p { + margin: 0; + font-size: 12px; + line-height: 1.6; + color: var(--text-muted); +} +.key-manager-form { + display: grid; + gap: 1rem; +} +.key-manager-field { + display: grid; + gap: 0.45rem; +} +.key-manager-field > span { + font-size: 12px; + font-weight: 600; + color: var(--text-muted); +} +.key-manager-field input[type="text"] { + width: 100%; + padding: 0.8rem 0.9rem; + border: 1px solid rgba(148, 163, 184, 0.26); + border-radius: 14px; + background: rgba(255, 255, 255, 0.92); + color: var(--text); + font-family: var(--font); +} +.key-scope-options { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.85rem; +} +.key-scope-option { + position: relative; + display: grid; + gap: 0.35rem; + min-height: 126px; + padding: 1rem; + border: 1px solid rgba(148, 163, 184, 0.22); + border-radius: 18px; + background: rgba(255, 255, 255, 0.84); + cursor: pointer; + transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease, background 0.18s ease; +} +.key-scope-option:hover { + transform: translateY(-1px); + border-color: rgba(245, 158, 11, 0.45); +} +.key-scope-option.is-selected { + border-color: rgba(245, 158, 11, 0.65); + background: linear-gradient(180deg, rgba(255, 248, 220, 0.95), rgba(255, 255, 255, 0.98)); + box-shadow: 0 14px 28px rgba(245, 158, 11, 0.12); +} +.key-scope-option input { + position: absolute; + opacity: 0; + pointer-events: none; +} +.key-scope-option strong { + font-size: 15px; + color: var(--text); +} +.key-scope-option small { + color: var(--text-muted); + line-height: 1.6; +} +.key-manager-inline { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} +.key-chip { + display: inline-flex; + align-items: center; + height: 28px; + padding: 0 0.7rem; + border-radius: 999px; + font-size: 12px; + font-weight: 600; +} +.key-chip-pool { + background: rgba(59, 130, 246, 0.1); + color: #2563eb; +} +.key-chip-rotate { + background: rgba(16, 185, 129, 0.12); + color: #047857; +} +.key-chip-local { + background: rgba(148, 163, 184, 0.14); + color: #475569; +} +.key-chip-scope-text { + background: rgba(245, 158, 11, 0.14); + color: #b45309; +} +.key-chip-scope-image { + background: rgba(14, 165, 233, 0.12); + color: #0369a1; +} +.key-chip-scope-all { + background: rgba(99, 102, 241, 0.12); + color: #4f46e5; +} +.key-chip-inactive { + background: rgba(239, 68, 68, 0.12); + color: #b91c1c; +} +.key-manager-actions { + display: flex; + align-items: center; + gap: 0.75rem; +} +.key-stats-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.8rem; +} +.key-stat-card { + padding: 0.95rem 1rem; + border-radius: 18px; + border: 1px solid rgba(148, 163, 184, 0.2); + background: rgba(255, 255, 255, 0.82); +} +.key-stat-card span { + display: block; + font-size: 12px; + color: var(--text-muted); +} +.key-stat-card strong { + display: block; + margin-top: 0.25rem; + font-size: 1.5rem; + color: var(--text); +} +.key-manager-footnote { + margin: 1rem 0 0; + font-size: 12px; + line-height: 1.7; + color: var(--text-muted); +} +.key-toolbar { + align-items: center; +} +.key-toolbar label { + white-space: nowrap; +} +.key-data-table td { + vertical-align: middle; +} +.key-cell-stack { + display: grid; + gap: 0.3rem; +} +.key-cell-inline { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + align-items: center; +} +.key-empty { + padding: 1.2rem 0; + text-align: center; + color: var(--text-muted); +} +.key-row-actions { + display: flex; + align-items: center; + gap: 0.45rem; +} +.key-row-actions button { + padding: 0.38rem 0.7rem; + border: 1px solid var(--border); + border-radius: 10px; + background: #fff; + color: var(--text); + font-family: var(--font); + font-size: 12px; + cursor: pointer; +} +.key-row-actions button:hover { + border-color: var(--accent); + color: var(--accent); + background: var(--accent-light); +} +.key-row-actions button.key-btn-danger:hover { + border-color: #ef4444; + color: #b91c1c; + background: rgba(254, 226, 226, 0.95); +} +.video-page-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; +} +.video-page-head h2 { + margin-bottom: 0.35rem; +} +.video-page-head-actions { + display: flex; + align-items: center; + justify-content: flex-end; +} +.video-launch-btn { + min-width: 132px; + height: 48px; + padding: 0 1.2rem; + border: none; + border-radius: 14px; + background: linear-gradient(135deg, #facc15 0%, #f59e0b 100%); + color: #1f2937; + font-family: var(--font); + font-size: 14px; + font-weight: 700; + cursor: pointer; + box-shadow: 0 12px 30px rgba(245, 158, 11, 0.28); + transition: transform 0.18s ease, box-shadow 0.18s ease; +} +.video-launch-btn:hover { + transform: translateY(-1px); + box-shadow: 0 16px 34px rgba(245, 158, 11, 0.32); +} +.video-stage-shell { + display: grid; + grid-template-columns: minmax(0, 1.45fr) minmax(320px, 0.72fr); + gap: 1.25rem; + align-items: start; +} +.video-stage-card { + overflow: hidden; + border: 1px solid rgba(148, 163, 184, 0.18); + border-radius: 28px; + background: + radial-gradient(circle at top right, rgba(250, 204, 21, 0.16), transparent 36%), + linear-gradient(160deg, #111827 0%, #1f2937 52%, #0f172a 100%); + box-shadow: 0 24px 60px rgba(15, 23, 42, 0.22); +} +.video-stage-media { + position: relative; + aspect-ratio: 16 / 9; + background: + radial-gradient(circle at top left, rgba(250, 204, 21, 0.18), transparent 34%), + linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent), + #0b1120; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} +.video-stage-media::after { + content: ""; + position: absolute; + inset: 0; + background: + linear-gradient(135deg, rgba(255, 255, 255, 0.04), transparent 30%, transparent 70%, rgba(255, 255, 255, 0.06)), + linear-gradient(0deg, rgba(15, 23, 42, 0.2), rgba(15, 23, 42, 0.2)); + pointer-events: none; +} +.video-stage-media video { + display: block; + width: 100%; + height: 100%; + object-fit: contain; + background: #000; +} +.video-stage-placeholder { + position: relative; + z-index: 1; + width: min(72%, 420px); + padding: 1.8rem; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 24px; + background: rgba(15, 23, 42, 0.58); + backdrop-filter: blur(16px); + color: rgba(255, 255, 255, 0.88); + text-align: center; +} +.video-stage-placeholder-icon { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 86px; + height: 42px; + margin-bottom: 0.8rem; + padding: 0 1rem; + border-radius: 999px; + background: rgba(250, 204, 21, 0.16); + color: #fef3c7; + font-size: 13px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} +.video-stage-placeholder p { + margin: 0; + font-size: 14px; + line-height: 1.7; +} +.video-stage-meta { + padding: 1.35rem 1.4rem 1.4rem; + color: #e5e7eb; +} +.video-stage-title-row { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 0.8rem; +} +.video-stage-kicker { + margin: 0 0 0.35rem; + color: rgba(250, 204, 21, 0.9); + font-size: 12px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} +.video-stage-title-row h3 { + margin: 0; + font-size: 1.45rem; + line-height: 1.2; + color: #fff; +} +.video-stage-prompt { + margin: 0.9rem 0 1rem; + min-height: 2.8em; + color: rgba(226, 232, 240, 0.86); + font-size: 14px; +} +.video-stage-progress-track, +.video-task-progress-track { + position: relative; + height: 10px; + border-radius: 999px; + overflow: hidden; + background: rgba(148, 163, 184, 0.18); +} +.video-stage-progress-fill, +.video-task-progress-fill { + display: block; + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, #facc15 0%, #f59e0b 48%, #fb7185 100%); + transition: width 0.35s ease; +} +.video-stage-stats { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.75rem; + margin-top: 1rem; +} +.video-stage-stat { + padding: 0.85rem 0.9rem; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 16px; + background: rgba(255, 255, 255, 0.04); +} +.video-stage-stat span { + display: block; + margin-bottom: 0.2rem; + color: rgba(226, 232, 240, 0.7); + font-size: 12px; +} +.video-stage-stat strong { + color: #fff; + font-size: 14px; + font-weight: 700; + word-break: break-word; +} +.video-stage-footer { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 0.65rem; + margin-top: 0.95rem; +} +.video-stage-footnote { + color: rgba(226, 232, 240, 0.72); + font-size: 12px; +} +.video-side-column { + display: grid; + gap: 1rem; +} +.video-side-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 22px; + padding: 1.15rem; + box-shadow: var(--shadow); +} +.video-side-card-accent { + background: + linear-gradient(180deg, rgba(250, 204, 21, 0.18), rgba(250, 204, 21, 0.04)), + var(--bg-card); + border-color: rgba(245, 158, 11, 0.25); +} +.video-side-card-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 0.8rem; + margin-bottom: 0.85rem; +} +.video-side-card-head h3, +.video-task-board-head h3 { + margin: 0; + font-size: 1rem; +} +.video-side-card-head p, +.video-task-board-head p { + margin: 0.2rem 0 0; + color: var(--text-muted); + font-size: 12px; +} +.video-toggle { + display: inline-flex; + align-items: center; + gap: 0.5rem; + min-height: 38px; + padding: 0.35rem 0.75rem; + border-radius: 999px; + background: rgba(255, 255, 255, 0.68); + border: 1px solid rgba(245, 158, 11, 0.25); + color: var(--text); + font-size: 12px; + font-weight: 600; +} +.video-toggle input { + margin: 0; +} +.video-overview-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.75rem; +} +.video-overview-item { + padding: 0.8rem 0.9rem; + border: 1px solid rgba(148, 163, 184, 0.16); + border-radius: 16px; + background: rgba(255, 255, 255, 0.74); +} +.video-overview-item span { + display: block; + margin-bottom: 0.15rem; + color: var(--text-muted); + font-size: 12px; +} +.video-overview-item strong { + color: var(--text); + font-size: 16px; + font-weight: 700; +} +.sora-account-summary { + display: grid; + gap: 0.65rem; + margin-top: 0.85rem; +} +.sora-account-summary-empty { + padding: 0.9rem 1rem; + border: 1px dashed var(--border); + border-radius: var(--radius-sm); + background: var(--bg-input); + color: var(--text-muted); + font-size: 12px; +} +.sora-account-summary-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.65rem; +} +.sora-account-summary-item { + padding: 0.85rem 0.9rem; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg-input); +} +.sora-account-summary-label { + display: block; + margin-bottom: 0.15rem; + font-size: 12px; + color: var(--text-muted); +} +.sora-account-summary-value { + color: var(--text); + font-size: 13px; + font-weight: 600; + word-break: break-word; +} +.sora-account-flags { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} +.sora-account-flag { + display: inline-flex; + align-items: center; + min-height: 28px; + padding: 0.2rem 0.7rem; + border-radius: 999px; + border: 1px solid var(--border); + background: var(--bg-input); + color: var(--text); + font-size: 12px; + font-weight: 600; +} +.sora-account-flag.is-ok { + background: #ecfdf5; + border-color: #a7f3d0; + color: #065f46; +} +.sora-account-flag.is-warn { + background: #fff7ed; + border-color: #fdba74; + color: #9a3412; +} +.sora-account-flag.is-bad { + background: #fef2f2; + border-color: #fecaca; + color: #991b1b; +} +.video-advanced-card { + margin-top: 0.85rem; + border-top: 1px dashed var(--border); + padding-top: 0.85rem; +} +.video-advanced-card summary { + cursor: pointer; + color: var(--text); + font-weight: 600; +} +.video-advanced-card-body { + margin-top: 0.85rem; +} +.video-task-board { + margin-top: 1.2rem; + border: 1px solid var(--border); + border-radius: 24px; + background: var(--bg-card); + padding: 1.15rem; + box-shadow: var(--shadow); +} +.video-task-board-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; +} +.video-task-board-actions { + display: flex; + flex-wrap: wrap; + gap: 0.65rem; +} +.video-task-board-actions button, +.video-task-actions button { + min-height: 38px; + padding: 0.5rem 0.8rem; + border-radius: 12px; + border: 1px solid var(--border); + background: var(--bg-input); + color: var(--text); + font-family: var(--font); + font-size: 12px; + font-weight: 600; + cursor: pointer; +} +.video-task-board-actions button:hover, +.video-task-actions button:hover { + border-color: var(--accent); + color: var(--accent); + background: var(--accent-light); +} +.video-task-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); + gap: 0.85rem; + margin-top: 1rem; +} +.video-task-empty { + grid-column: 1 / -1; + padding: 1.4rem; + border: 1px dashed var(--border); + border-radius: 18px; + background: var(--bg-input); + color: var(--text-muted); + text-align: center; +} +.video-task-card { + padding: 1rem; + border: 1px solid var(--border); + border-radius: 20px; + background: linear-gradient(180deg, #fff, #f8fafc); + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.05); + transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease; +} +.video-task-card:hover { + transform: translateY(-1px); + border-color: rgba(99, 102, 241, 0.35); + box-shadow: 0 16px 28px rgba(15, 23, 42, 0.08); +} +.video-task-card.is-active { + border-color: rgba(245, 158, 11, 0.42); + box-shadow: 0 18px 32px rgba(245, 158, 11, 0.12); +} +.video-task-card-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 0.75rem; + margin-bottom: 0.65rem; +} +.video-task-card-title { + margin: 0; + color: var(--text); + font-size: 14px; + font-weight: 700; +} +.video-task-card-subtitle { + margin: 0.2rem 0 0; + color: var(--text-muted); + font-size: 12px; +} +.video-task-prompt { + margin: 0 0 0.8rem; + color: var(--text); + font-size: 13px; + line-height: 1.6; + min-height: 3.2em; +} +.video-task-progress-meta, +.video-task-meta { + display: flex; + flex-wrap: wrap; + gap: 0.55rem; + margin-top: 0.55rem; + color: var(--text-muted); + font-size: 12px; +} +.video-task-actions { + display: flex; + flex-wrap: wrap; + gap: 0.55rem; + margin-top: 0.8rem; +} +.video-task-actions .is-primary { + background: var(--accent); + border-color: var(--accent); + color: #fff; +} +.video-task-actions .is-primary:hover { + background: var(--accent-hover); + border-color: var(--accent-hover); + color: #fff; +} +.video-compose-overlay { + position: fixed; + inset: 0; + z-index: 50; +} +.video-compose-backdrop { + position: absolute; + inset: 0; + background: rgba(15, 23, 42, 0.46); + backdrop-filter: blur(6px); +} +.video-compose-dialog { + position: absolute; + top: 50%; + left: 50%; + width: min(720px, calc(100vw - 2rem)); + max-height: calc(100vh - 2rem); + transform: translate(-50%, -50%); + overflow: auto; + border-radius: 28px; + border: 1px solid rgba(245, 158, 11, 0.24); + background: + radial-gradient(circle at top right, rgba(250, 204, 21, 0.18), transparent 32%), + #fffdf7; + box-shadow: 0 28px 70px rgba(15, 23, 42, 0.22); + padding: 1.35rem; +} +.video-compose-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; +} +.video-compose-kicker { + margin: 0 0 0.3rem; + color: #b45309; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} +.video-compose-head h3 { + margin: 0; + font-size: 1.35rem; +} +.video-compose-head p { + margin: 0.35rem 0 0; + color: var(--text-muted); + font-size: 13px; +} +.video-compose-close { + width: 38px; + height: 38px; + border: none; + border-radius: 50%; + background: rgba(148, 163, 184, 0.14); + color: var(--text); + font-size: 24px; + line-height: 1; + cursor: pointer; +} +.video-compose-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.75rem; +} +.video-compose-grid-compact { + grid-template-columns: minmax(0, 220px); + margin-bottom: 0.9rem; +} +.video-compose-grid label { + display: flex; + flex-direction: column; + gap: 0.35rem; + color: var(--text); + font-size: 13px; +} +.video-compose-grid input, +.video-compose-grid select { + height: 42px; + padding: 0 0.85rem; + border: 1px solid var(--border); + border-radius: 12px; + background: #fff; + color: var(--text); + font-family: var(--font); +} +.video-compose-grid input:focus, +.video-compose-grid select:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-light); +} +.video-compose-actions { + display: flex; + justify-content: flex-end; + margin-top: 1rem; +} +.sora-video-field { + display: flex; + flex-direction: column; + gap: 0.35rem; + margin-bottom: 0.85rem; + font-size: 13px; + color: var(--text); +} +.sora-video-image-field { + padding: 0.9rem 1rem; + border: 1px dashed rgba(245, 158, 11, 0.35); + border-radius: 16px; + background: rgba(255, 251, 235, 0.72); +} +.sora-video-image-field input[type="file"] { + display: block; + width: 100%; + padding: 0.6rem 0.75rem; + border: 1px solid var(--border); + border-radius: 12px; + background: #fff; + color: var(--text); + font-family: var(--font); +} +.sora-video-image-field small { + color: var(--text-muted); + line-height: 1.6; +} +.sora-video-card textarea, +.sora-video-card input[type="text"], +.sora-video-card input[type="number"], +.sora-video-card select { + font-family: var(--font); +} +.sora-video-card textarea { + width: 100%; + min-height: 92px; + padding: 0.75rem 0.9rem; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg-input); + color: var(--text); + resize: vertical; +} +.sora-video-card textarea:focus, +.sora-video-card input:focus, +.sora-video-card select:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-light); +} +.sora-video-summary { + display: flex; + flex-wrap: wrap; + gap: 0.6rem; + align-items: center; + margin-bottom: 0.75rem; + font-size: 12px; + color: var(--text-muted); +} +.sora-status-badge { + display: inline-flex; + align-items: center; + padding: 0.2rem 0.55rem; + border-radius: 999px; + font-size: 12px; + font-weight: 600; + border: 1px solid var(--border); + background: var(--bg-input); + color: var(--text); +} +.sora-status-badge.is-success { + background: #ecfdf5; + border-color: #a7f3d0; + color: #065f46; +} +.sora-status-badge.is-failed { + background: #fef2f2; + border-color: #fecaca; + color: #991b1b; +} +.sora-status-badge.is-pending { + background: #eff6ff; + border-color: #bfdbfe; + color: #1d4ed8; +} +.sora-video-links { + display: grid; + gap: 0.9rem; +} +.sora-video-link-item { + padding: 0.85rem; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg-input); +} +.sora-video-link-item a { + color: var(--accent-hover); + text-decoration: none; + word-break: break-all; +} +.sora-video-link-item a:hover { + text-decoration: underline; +} +.sora-video-link-item video { + display: block; + width: 100%; + max-width: 540px; + margin-top: 0.6rem; + border-radius: var(--radius-sm); + background: #000; +} +.sora-video-raw { + margin-top: 0.85rem; +} +.sora-video-raw summary { + cursor: pointer; + color: var(--text); + font-weight: 500; +} +.sora-video-raw pre { + margin: 0.65rem 0 0; + padding: 0.85rem; + border-radius: var(--radius-sm); + background: #0f172a; + color: #e2e8f0; + overflow: auto; + font-size: 12px; + line-height: 1.55; +} +@media (max-width: 1080px) { + .key-manager-grid { + grid-template-columns: 1fr; + } + .key-page-head, + .key-manager-card-head-table { + flex-direction: column; + } + .key-scope-options { + grid-template-columns: 1fr; + } + .video-stage-shell { + grid-template-columns: 1fr; + } + .video-stage-stats { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .video-page-head, + .video-task-board-head { + flex-direction: column; + } + .video-compose-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} +@media (max-width: 720px) { + .key-page-head-actions, + .key-manager-actions { + width: 100%; + } + .key-page-head-actions .btn-default, + .key-manager-actions .video-launch-btn { + width: 100%; + } + .key-stats-grid { + grid-template-columns: 1fr; + } + .video-stage-title-row { + flex-direction: column; + } + .video-stage-stats, + .video-overview-grid, + .video-compose-grid { + grid-template-columns: 1fr; + } + .video-task-list { + grid-template-columns: 1fr; + } +} + +.form-grid { max-width: 520px; } +.form-grid label { display: block; margin-bottom: 0.75rem; font-weight: 500; color: var(--text); } +.form-grid input { + width: 100%; + padding: 0.55rem 0.75rem; + margin-top: 0.35rem; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text); + font-family: var(--font); + font-size: 14px; +} +.form-grid input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-light); +} +.form-grid hr { margin: 1.25rem 0; border: none; border-top: 1px solid var(--border); } +.form-grid button { + padding: 0.55rem 1.25rem; + background: var(--accent); + border: none; + border-radius: var(--radius-sm); + color: #fff; + font-family: var(--font); + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; +} +.form-grid button:hover { background: var(--accent-hover); } + +/* 系统设置:左右布局,一行 2 个卡片 */ +.settings-grid { + display: grid; + grid-template-columns: repeat(3, minmax(240px, 1fr)); + gap: 1rem; + width: 100%; + max-width: 100%; +} +@media (max-width: 900px) { + .settings-grid { + grid-template-columns: repeat(2, 1fr); + } +} +@media (max-width: 560px) { + .settings-grid { + grid-template-columns: 1fr; + } +} +.settings-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1rem; + box-shadow: var(--shadow); + min-width: 0; +} +.settings-card h3 { + margin: 0 0 0.5rem; + font-size: 0.95rem; + font-weight: 600; + color: var(--text); + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border); +} +.settings-card-desc { + margin: 0 0 0.75rem; + font-size: 12px; + color: var(--text-muted); +} +.settings-card-desc a { color: var(--accent); text-decoration: none; } +.settings-card-desc a:hover { text-decoration: underline; } +.settings-card label { + display: block; + margin-bottom: 0.75rem; + font-weight: 500; + color: var(--text); + font-size: 13px; +} +.settings-card label:last-of-type { margin-bottom: 0; } +.settings-row-2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + margin-bottom: 0.75rem; +} +.settings-row-2:last-of-type { margin-bottom: 0; } +.settings-row-2 label { margin-bottom: 0; } +.settings-card input { + width: 100%; + padding: 0.5rem 0.75rem; + margin-top: 0.3rem; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text); + font-family: var(--font); + font-size: 13px; +} +.settings-card input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 2px var(--accent-light); +} +.settings-card select { + width: 100%; + padding: 0.5rem 0.75rem; + margin-top: 0.3rem; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text); + font-family: var(--font); + font-size: 13px; + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2364748b' d='M6 8L2 4h8z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0.75rem center; + padding-right: 2rem; +} +.settings-card select:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 2px var(--accent-light); +} +.settings-submit { + grid-column: 1 / -1; + padding-top: 0.5rem; +} +.settings-submit button { + padding: 0.55rem 1.5rem; + background: var(--accent); + border: none; + border-radius: var(--radius-sm); + color: #fff; + font-family: var(--font); + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; +} +.settings-submit button:hover { background: var(--accent-hover); } + +.modal { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.4); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; + backdrop-filter: blur(4px); +} +.modal-content { + background: var(--bg-card); + border-radius: var(--radius); + border: 1px solid var(--border); + padding: 1.5rem; + max-width: 90%; + max-height: 80vh; + overflow: auto; + position: relative; + box-shadow: var(--shadow-md); +} +.modal-content.modal-content-wide { + width: 90%; + max-width: 820px; + max-height: 85vh; +} +.modal-close { + position: absolute; + top: 0.75rem; + right: 1rem; + cursor: pointer; + font-size: 1.25rem; + color: var(--text-muted); + line-height: 1; +} +.modal-close:hover { color: var(--text); } +#modal-body .modal-desc { + margin: 0 0 0.75rem; + font-size: 13px; + color: var(--text-muted); + line-height: 1.5; +} +#modal-body .modal-desc strong { color: var(--text); } + +/* 修改登录账号弹窗 */ +.login-update-modal { + min-width: 320px; + max-width: 400px; +} +.login-update-title { + margin: 0 0 0.5rem; + font-size: 1.1rem; + font-weight: 600; + color: var(--text); + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border); +} +.login-update-desc { + margin: 0 0 1rem; + font-size: 13px; + color: var(--text-muted); + line-height: 1.5; +} +.login-update-modal label { + display: block; + margin-bottom: 0.75rem; + font-weight: 500; + font-size: 13px; + color: var(--text); +} +.login-update-modal label input { + display: block; + width: 100%; + margin-top: 0.35rem; + padding: 0.5rem 0.75rem; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text); + font-family: var(--font); + font-size: 13px; +} +.login-update-modal label input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 2px var(--accent-light); +} +.login-update-actions { + margin-top: 1.25rem; + padding-top: 0.75rem; + border-top: 1px solid var(--border); +} +.login-update-btn { + padding: 0.5rem 1.25rem; + background: var(--accent); + color: #fff; + border: none; + border-radius: var(--radius-sm); + font-family: var(--font); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} +.login-update-btn:hover { + background: var(--accent-hover); +} + +.email-view-card p { margin: 0.6rem 0; font-size: 13px; } +.email-view-card .btn-copy { + margin-left: 0.5rem; + padding: 0.2rem 0.5rem; + font-size: 12px; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + cursor: pointer; + color: var(--text); +} +.email-view-card .btn-copy:hover { border-color: var(--accent); color: var(--accent); } +.email-view-card a { color: var(--accent); } +.email-view-card .error { color: var(--danger); } +.email-view-card .email-body { + max-height: 200px; + overflow: auto; + white-space: pre-wrap; + word-break: break-word; + font-size: 12px; + padding: 0.5rem; + background: var(--bg-input); + border-radius: var(--radius-sm); + margin: 0.25rem 0; +} +.email-view-card .email-body-html { max-height: 280px; overflow: auto; font-size: 12px; padding: 0.5rem; background: #fff; border-radius: var(--radius-sm); border: 1px solid var(--border); } +.email-view-card .email-view-tabs { margin: 0.5rem 0; display: flex; gap: 0.25rem; } +.email-view-card .email-tab { + padding: 0.35rem 0.75rem; + font-size: 12px; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + cursor: pointer; + color: var(--text-muted); +} +.email-view-card .email-tab:hover { border-color: var(--accent); color: var(--accent); } +.email-view-card .email-tab.active { background: var(--accent-light); border-color: var(--accent); color: var(--accent); } +.email-view-card .email-tab-panel { margin-top: 0.25rem; } +.email-view-card .email-tab-panel.hidden { display: none; } +.email-view-card .email-view-fallback { margin-top: 0.75rem; font-size: 12px; color: var(--text-muted); } +.email-view-card .email-view-empty { color: var(--text-muted); } + +/* 邮件查看:列表 + 详情 布局 */ +.email-view-card.email-view-layout { + display: flex; + gap: 1rem; + min-height: 320px; +} +.email-view-list { + flex-shrink: 0; + width: 200px; + border-right: 1px solid var(--border); + padding-right: 0.75rem; + display: flex; + flex-direction: column; +} +.email-view-list-title { + font-weight: 600; + font-size: 13px; + margin-bottom: 0.25rem; + color: var(--text); +} +.email-view-list-inner { + overflow: auto; + flex: 1; + min-height: 0; +} +.email-view-list-item { + padding: 0.5rem 0.4rem; + border-radius: var(--radius-sm); + cursor: pointer; + font-size: 12px; + margin-bottom: 0.25rem; + border: 1px solid transparent; +} +.email-view-list-item:hover { + background: var(--bg-input); +} +.email-view-list-item.active { + background: var(--accent-light); + border-color: var(--accent); + color: var(--accent); +} +.email-view-list-item-subject { + font-weight: 500; + color: var(--text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.email-view-list-item-meta { + color: var(--text-muted); + font-size: 11px; + margin-top: 0.2rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.email-view-detail { + flex: 1; + min-width: 0; + overflow: auto; +} +.email-view-detail-inner { + padding-right: 0.25rem; +} +.email-view-card.email-view-layout .email-body-html { + max-height: 360px; +} + +/* 统一提示:Toast */ +.toast-container { + position: fixed; + top: 1rem; + right: 1rem; + z-index: 200; + display: flex; + flex-direction: column; + gap: 0.5rem; + pointer-events: none; +} +.toast { + padding: 0.65rem 1rem; + border-radius: var(--radius-sm); + font-size: 13px; + font-weight: 500; + box-shadow: var(--shadow-md); + border: 1px solid transparent; + max-width: 320px; + pointer-events: auto; + animation: toast-in 0.25s ease; +} +@keyframes toast-in { + from { opacity: 0; transform: translateX(100%); } + to { opacity: 1; transform: translateX(0); } +} +.toast.success { + background: #ecfdf5; + color: #065f46; + border-color: #a7f3d0; +} +.toast.error { + background: #fef2f2; + color: #991b1b; + border-color: #fecaca; +} +.toast.info { + background: var(--accent-light); + color: var(--accent-hover); + border-color: var(--accent); +} +/* 确认框(在 modal 内使用) */ +.confirm-dialog { text-align: center; } +.confirm-dialog .confirm-msg { margin-bottom: 1.25rem; font-size: 14px; color: var(--text); } +.confirm-dialog .confirm-btns { display: flex; justify-content: center; gap: 0.75rem; } +.confirm-dialog .confirm-btns button { + padding: 0.5rem 1.25rem; + border-radius: var(--radius-sm); + font-family: var(--font); + font-size: 13px; + font-weight: 500; + cursor: pointer; + border: none; +} +.confirm-dialog .confirm-btns .btn-primary { background: var(--accent); color: #fff; } +.confirm-dialog .confirm-btns .btn-primary:hover { background: var(--accent-hover); } +.confirm-dialog .confirm-btns .btn-default { + background: var(--bg-input); + color: var(--text); + border: 1px solid var(--border); +} +.confirm-dialog .confirm-btns .btn-default:hover { border-color: var(--border-focus); } diff --git a/Register_GPT_v0/web/run_web.py b/Register_GPT_v0/web/run_web.py new file mode 100644 index 0000000..e1eedfd --- /dev/null +++ b/Register_GPT_v0/web/run_web.py @@ -0,0 +1,16 @@ +""" +界面版启动入口:将 protocol 与 web/backend 加入路径后启动 uvicorn。 +在 protocol 目录执行: python web/run_web.py +或在 protocol/web 目录执行: python run_web.py +""" +import sys +from pathlib import Path + +root = Path(__file__).resolve().parent +backend = root / "backend" +if str(backend) not in sys.path: + sys.path.insert(0, str(backend)) + +if __name__ == "__main__": + import uvicorn + uvicorn.run("app.main:app", host="0.0.0.0", port=1989, reload=True) diff --git a/packages/general/ExaFree b/packages/general/ExaFree new file mode 160000 index 0000000..76c2248 --- /dev/null +++ b/packages/general/ExaFree @@ -0,0 +1 @@ +Subproject commit 76c22483fc9543029b4ff845d6f36729e22e4f37
+
+ +
+
+

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%0DcvZ#tPT75p9V+l#1PR>{1dUdrB9?d>9= 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 z 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 literal 0 HcmV?d00001 diff --git a/Register_GPT_v0/screenshots/5.png b/Register_GPT_v0/screenshots/5.png new file mode 100644 index 0000000000000000000000000000000000000000..f151a685c78904362bb1059e0ab6e2f20be6b856 GIT binary patch literal 179787 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 z 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