|
@@ -0,0 +1,476 @@
|
|
|
|
|
+package com.husj.openai.service;
|
|
|
|
|
+
|
|
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
|
+import com.husj.openai.config.AppProperties;
|
|
|
|
|
+import com.husj.openai.mail.EmailBundle;
|
|
|
|
|
+import com.husj.openai.mail.EmailProviderFactory;
|
|
|
|
|
+import com.husj.openai.model.OAuthStart;
|
|
|
|
|
+import com.husj.openai.model.RegisterResult;
|
|
|
|
|
+import com.husj.openai.util.PkceUtil;
|
|
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import okhttp3.*;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+
|
|
|
|
|
+import java.net.CookieManager;
|
|
|
|
|
+import java.net.CookiePolicy;
|
|
|
|
|
+import java.net.InetSocketAddress;
|
|
|
|
|
+import java.net.Proxy;
|
|
|
|
|
+import java.util.*;
|
|
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * OpenAI 主注册流程 — 完整复刻 Python run() 函数的 10 步注册 + 登录补全
|
|
|
|
|
+ */
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Service
|
|
|
|
|
+@RequiredArgsConstructor
|
|
|
|
|
+public class OpenAiRegisterService {
|
|
|
|
|
+
|
|
|
|
|
+ private static final String UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36";
|
|
|
|
|
+
|
|
|
|
|
+ private final AppProperties props;
|
|
|
|
|
+ private final EmailProviderFactory emailFactory;
|
|
|
|
|
+ private final SentinelService sentinelService;
|
|
|
|
|
+ private final OAuthService oauthService;
|
|
|
|
|
+ private final ObjectMapper mapper = new ObjectMapper();
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 执行一次完整的注册流程
|
|
|
|
|
+ */
|
|
|
|
|
+ public Optional<RegisterResult> run() {
|
|
|
|
|
+ String proxy = props.getProxy();
|
|
|
|
|
+ String mailProvider = props.getMailProvider();
|
|
|
|
|
+
|
|
|
|
|
+ log.info("\n{} 开启注册流程 {}", "=".repeat(20), "=".repeat(20));
|
|
|
|
|
+
|
|
|
|
|
+ OkHttpClient s = buildClient(proxy);
|
|
|
|
|
+ try {
|
|
|
|
|
+ // ===== 步骤1: 获取临时邮箱 =====
|
|
|
|
|
+ log.info("[步骤1] 正在初始化临时邮箱(provider={})...", mailProvider);
|
|
|
|
|
+ EmailBundle mailBundle = emailFactory.build(proxy, mailProvider);
|
|
|
|
|
+ String email = mailBundle.getEmail();
|
|
|
|
|
+ String password = mailBundle.getPassword();
|
|
|
|
|
+ log.info("[*] 当前邮箱提供商: {}", mailBundle.getProvider().name().toLowerCase());
|
|
|
|
|
+ log.info("[成功] 邮箱: {} | 临时密码: {}", email, password);
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 步骤2: 访问 OpenAI 授权页获取 Device ID =====
|
|
|
|
|
+ log.info("[步骤2] 访问 OpenAI 授权页获取 Device ID...");
|
|
|
|
|
+ OAuthStart oauth = oauthService.generateOAuthUrl(OAuthService.DEFAULT_REDIRECT_URI);
|
|
|
|
|
+ Response authPageResp = s.newCall(new Request.Builder().url(oauth.getAuthUrl())
|
|
|
|
|
+ .header("user-agent", UA).header("accept", "application/json, text/plain, */*").build()).execute();
|
|
|
|
|
+ authPageResp.close();
|
|
|
|
|
+
|
|
|
|
|
+ String did = getCookieValue(s, "oai-did");
|
|
|
|
|
+ if (did == null || did.isBlank()) {
|
|
|
|
|
+ log.warn("[失败] 未能从 Cookie 获取 oai-did");
|
|
|
|
|
+ return Optional.empty();
|
|
|
|
|
+ }
|
|
|
|
|
+ log.info("[成功] Device ID: {}", did);
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 步骤3: Sentinel + 提交注册邮箱 =====
|
|
|
|
|
+ log.info("[步骤3] 获取 Sentinel 载荷并提交注册邮箱...");
|
|
|
|
|
+ String authorizeContinueSentinel;
|
|
|
|
|
+ try {
|
|
|
|
|
+ authorizeContinueSentinel = sentinelService.buildSentinelPayload(s, did, "authorize_continue");
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("[失败] 获取 authorize_continue Sentinel 失败: {}", e.getMessage());
|
|
|
|
|
+ return Optional.empty();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> usernamePayload = new LinkedHashMap<>();
|
|
|
|
|
+ usernamePayload.put("username", Map.of("value", email, "kind", "email"));
|
|
|
|
|
+ usernamePayload.put("screen_hint", "signup");
|
|
|
|
|
+
|
|
|
|
|
+ Response signupRes = s.newCall(new Request.Builder()
|
|
|
|
|
+ .url("https://auth.openai.com/api/accounts/authorize/continue")
|
|
|
|
|
+ .header("referer", "https://auth.openai.com/create-account")
|
|
|
|
|
+ .header("accept", "application/json")
|
|
|
|
|
+ .header("content-type", "application/json")
|
|
|
|
|
+ .header("openai-sentinel-token", authorizeContinueSentinel)
|
|
|
|
|
+ .post(RequestBody.create(mapper.writeValueAsString(usernamePayload), MediaType.parse("application/json")))
|
|
|
|
|
+ .build()).execute();
|
|
|
|
|
+ log.info("[日志] 邮箱提交状态: {}", signupRes.code());
|
|
|
|
|
+ if (signupRes.code() != 200) {
|
|
|
|
|
+ String body = signupRes.body() != null ? signupRes.body().string() : "";
|
|
|
|
|
+ log.warn("[失败] 邮箱提交失败: {}", body.length() > 200 ? body.substring(0, 200) : body);
|
|
|
|
|
+ signupRes.close();
|
|
|
|
|
+ return Optional.empty();
|
|
|
|
|
+ }
|
|
|
|
|
+ signupRes.close();
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 步骤4: 设置账户密码 =====
|
|
|
|
|
+ log.info("[步骤4] 设置账户密码...");
|
|
|
|
|
+ Map<String, String> pwdPayload = Map.of("password", password, "username", email);
|
|
|
|
|
+ Response pwdRes = s.newCall(new Request.Builder()
|
|
|
|
|
+ .url("https://auth.openai.com/api/accounts/user/register")
|
|
|
|
|
+ .header("referer", "https://auth.openai.com/create-account/password")
|
|
|
|
|
+ .header("accept", "application/json")
|
|
|
|
|
+ .header("content-type", "application/json")
|
|
|
|
|
+ .post(RequestBody.create(mapper.writeValueAsString(pwdPayload), MediaType.parse("application/json")))
|
|
|
|
|
+ .build()).execute();
|
|
|
|
|
+ log.info("[日志] 密码设置状态: {}", pwdRes.code());
|
|
|
|
|
+ if (pwdRes.code() != 200) {
|
|
|
|
|
+ String body = pwdRes.body() != null ? pwdRes.body().string() : "";
|
|
|
|
|
+ log.warn("[失败] 密码设置失败: {}", body.length() > 200 ? body.substring(0, 200) : body);
|
|
|
|
|
+ pwdRes.close();
|
|
|
|
|
+ return Optional.empty();
|
|
|
|
|
+ }
|
|
|
|
|
+ pwdRes.close();
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 步骤5: 触发发送验证邮件 =====
|
|
|
|
|
+ log.info("[步骤5] 触发 OpenAI 发送验证邮件...");
|
|
|
|
|
+ s.newCall(new Request.Builder().url("https://auth.openai.com/create-account/password")
|
|
|
|
|
+ .header("user-agent", UA).get().build()).execute().close();
|
|
|
|
|
+ Response otpSendRes = s.newCall(new Request.Builder()
|
|
|
|
|
+ .url("https://auth.openai.com/api/accounts/email-otp/send")
|
|
|
|
|
+ .header("referer", "https://auth.openai.com/create-account/password")
|
|
|
|
|
+ .header("accept", "application/json")
|
|
|
|
|
+ .get().build()).execute();
|
|
|
|
|
+ log.info("[日志] 发送指令状态: {}", otpSendRes.code());
|
|
|
|
|
+ if (otpSendRes.code() != 200) {
|
|
|
|
|
+ String body = otpSendRes.body() != null ? otpSendRes.body().string() : "";
|
|
|
|
|
+ log.warn("[失败] 发送验证码失败: {}", body.length() > 200 ? body.substring(0, 200) : body);
|
|
|
|
|
+ otpSendRes.close();
|
|
|
|
|
+ return Optional.empty();
|
|
|
|
|
+ }
|
|
|
|
|
+ otpSendRes.close();
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 步骤6: 等待邮箱接收 6 位验证码 =====
|
|
|
|
|
+ log.info("[步骤6] 等待邮箱接收 6 位验证码...");
|
|
|
|
|
+ String code = mailBundle.fetchCode(180, 6000, Collections.emptySet());
|
|
|
|
|
+ if (code == null) {
|
|
|
|
|
+ log.warn("[失败] 邮箱长时间未收到验证码");
|
|
|
|
|
+ return Optional.empty();
|
|
|
|
|
+ }
|
|
|
|
|
+ log.info("[成功] 捕获验证码: {}", code);
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 步骤7: 提交验证码 =====
|
|
|
|
|
+ log.info("[步骤7] 提交验证码至 OpenAI...");
|
|
|
|
|
+ Response valRes = s.newCall(new Request.Builder()
|
|
|
|
|
+ .url("https://auth.openai.com/api/accounts/email-otp/validate")
|
|
|
|
|
+ .header("referer", "https://auth.openai.com/email-verification")
|
|
|
|
|
+ .header("accept", "application/json")
|
|
|
|
|
+ .header("content-type", "application/json")
|
|
|
|
|
+ .post(RequestBody.create(mapper.writeValueAsString(Map.of("code", code)), MediaType.parse("application/json")))
|
|
|
|
|
+ .build()).execute();
|
|
|
|
|
+ log.info("[日志] 验证码校验状态: {}", valRes.code());
|
|
|
|
|
+ if (valRes.code() != 200) {
|
|
|
|
|
+ String body = valRes.body() != null ? valRes.body().string() : "";
|
|
|
|
|
+ log.warn("[失败] 验证码校验失败: {}", body.length() > 200 ? body.substring(0, 200) : body);
|
|
|
|
|
+ valRes.close();
|
|
|
|
|
+ return Optional.empty();
|
|
|
|
|
+ }
|
|
|
|
|
+ valRes.close();
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 步骤8: 完善账户信息 =====
|
|
|
|
|
+ log.info("[步骤8] 完善账户基本信息...");
|
|
|
|
|
+ String createAccountSentinel;
|
|
|
|
|
+ try {
|
|
|
|
|
+ createAccountSentinel = sentinelService.buildSentinelPayload(s, did, "authorize_continue");
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("[失败] 获取 create_account Sentinel 失败: {}", e.getMessage());
|
|
|
|
|
+ return Optional.empty();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, String> accPayload = Map.of("name", PkceUtil.randomName(), "birthdate", PkceUtil.randomBirthdate());
|
|
|
|
|
+ Response accRes = s.newCall(new Request.Builder()
|
|
|
|
|
+ .url("https://auth.openai.com/api/accounts/create_account")
|
|
|
|
|
+ .header("referer", "https://auth.openai.com/about-you")
|
|
|
|
|
+ .header("accept", "application/json")
|
|
|
|
|
+ .header("content-type", "application/json")
|
|
|
|
|
+ .header("openai-sentinel-token", createAccountSentinel)
|
|
|
|
|
+ .post(RequestBody.create(mapper.writeValueAsString(accPayload), MediaType.parse("application/json")))
|
|
|
|
|
+ .build()).execute();
|
|
|
|
|
+ log.info("[日志] 账户创建状态: {}", accRes.code());
|
|
|
|
|
+ if (accRes.code() != 200) {
|
|
|
|
|
+ String body = accRes.body() != null ? accRes.body().string() : "";
|
|
|
|
|
+ log.warn("[失败] 账户创建失败: {}", body.length() > 200 ? body.substring(0, 200) : body);
|
|
|
|
|
+ accRes.close();
|
|
|
|
|
+ return Optional.empty();
|
|
|
|
|
+ }
|
|
|
|
|
+ accRes.close();
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 步骤9: 重新走登录流程获取 Token =====
|
|
|
|
|
+ log.info("[步骤9] 注册完成,重新走登录流程获取 Workspace / Token...");
|
|
|
|
|
+ String firstCode = code;
|
|
|
|
|
+ for (int loginAttempt = 0; loginAttempt < 3; loginAttempt++) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ log.info("[*] 正在通过登录流程获取 Token...{}", loginAttempt > 0 ? " (重试 " + loginAttempt + "/3)" : "");
|
|
|
|
|
+ OkHttpClient s2 = buildClient(proxy);
|
|
|
|
|
+ OAuthStart oauth2 = oauthService.generateOAuthUrl(OAuthService.DEFAULT_REDIRECT_URI);
|
|
|
|
|
+ s2.newCall(new Request.Builder().url(oauth2.getAuthUrl()).header("user-agent", UA).build()).execute().close();
|
|
|
|
|
+
|
|
|
|
|
+ String did2 = getCookieValue(s2, "oai-did");
|
|
|
|
|
+ if (did2 == null || did2.isBlank()) {
|
|
|
|
|
+ log.warn("[失败] 登录会话未能获取 oai-did");
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 提交登录邮箱
|
|
|
|
|
+ Map<String, Object> loginUsernamePayload = new LinkedHashMap<>();
|
|
|
|
|
+ loginUsernamePayload.put("username", Map.of("value", email, "kind", "email"));
|
|
|
|
|
+ loginUsernamePayload.put("screen_hint", "login");
|
|
|
|
|
+
|
|
|
|
|
+ Response lc = s2.newCall(new Request.Builder()
|
|
|
|
|
+ .url("https://auth.openai.com/api/accounts/authorize/continue")
|
|
|
|
|
+ .header("referer", "https://auth.openai.com/log-in")
|
|
|
|
|
+ .header("accept", "application/json")
|
|
|
|
|
+ .header("content-type", "application/json")
|
|
|
|
|
+ .header("openai-sentinel-token", sentinelService.buildSentinelPayload(s2, did2, "authorize_continue"))
|
|
|
|
|
+ .post(RequestBody.create(mapper.writeValueAsString(loginUsernamePayload), MediaType.parse("application/json")))
|
|
|
|
|
+ .build()).execute();
|
|
|
|
|
+ log.info("[日志] 登录邮箱提交状态: {}", lc.code());
|
|
|
|
|
+ if (lc.code() != 200) {
|
|
|
|
|
+ String body = lc.body() != null ? lc.body().string() : "";
|
|
|
|
|
+ log.warn("[失败] 登录邮箱提交失败: {}", body.length() > 200 ? body.substring(0, 200) : body);
|
|
|
|
|
+ lc.close();
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ String lcBody = lc.body() != null ? lc.body().string() : "{}";
|
|
|
|
|
+ lc.close();
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ Map<String, Object> lcData = mapper.readValue(lcBody, Map.class);
|
|
|
|
|
+ String continueUrl = String.valueOf(lcData.getOrDefault("continue_url", "")).strip();
|
|
|
|
|
+ if (!continueUrl.isBlank()) {
|
|
|
|
|
+ s2.newCall(new Request.Builder().url(continueUrl).header("user-agent", UA).get().build()).execute().close();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 提交密码
|
|
|
|
|
+ Response pw = s2.newCall(new Request.Builder()
|
|
|
|
|
+ .url("https://auth.openai.com/api/accounts/password/verify")
|
|
|
|
|
+ .header("referer", "https://auth.openai.com/log-in/password")
|
|
|
|
|
+ .header("accept", "application/json")
|
|
|
|
|
+ .header("content-type", "application/json")
|
|
|
|
|
+ .header("openai-sentinel-token", sentinelService.buildSentinelPayload(s2, did2, "authorize_continue"))
|
|
|
|
|
+ .post(RequestBody.create(mapper.writeValueAsString(Map.of("password", password)), MediaType.parse("application/json")))
|
|
|
|
|
+ .build()).execute();
|
|
|
|
|
+ log.info("[日志] 登录密码验证状态: {}", pw.code());
|
|
|
|
|
+ if (pw.code() != 200) {
|
|
|
|
|
+ String body = pw.body() != null ? pw.body().string() : "";
|
|
|
|
|
+ log.warn("[失败] 登录密码验证失败: {}", body.length() > 200 ? body.substring(0, 200) : body);
|
|
|
|
|
+ pw.close();
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ pw.close();
|
|
|
|
|
+
|
|
|
|
|
+ // 等待登录 OTP
|
|
|
|
|
+ List<String> existingCodes = mailBundle.extractAllCodes();
|
|
|
|
|
+ s2.newCall(new Request.Builder()
|
|
|
|
|
+ .url("https://auth.openai.com/email-verification")
|
|
|
|
|
+ .header("referer", "https://auth.openai.com/log-in/password")
|
|
|
|
|
+ .get().build()).execute().close();
|
|
|
|
|
+ log.info("[*] 正在等待登录 OTP...");
|
|
|
|
|
+ Thread.sleep(2000);
|
|
|
|
|
+
|
|
|
|
|
+ Set<String> baselineCodes = new HashSet<>(existingCodes);
|
|
|
|
|
+ baselineCodes.add(firstCode);
|
|
|
|
|
+ String otp2 = mailBundle.fetchCode(180, 2000, baselineCodes);
|
|
|
|
|
+ if (otp2 == null) {
|
|
|
|
|
+ log.warn("[失败] 未收到登录 OTP");
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ log.info("[成功] 捕获登录 OTP: {}", otp2);
|
|
|
|
|
+
|
|
|
|
|
+ // 提交登录 OTP
|
|
|
|
|
+ Response val2 = s2.newCall(new Request.Builder()
|
|
|
|
|
+ .url("https://auth.openai.com/api/accounts/email-otp/validate")
|
|
|
|
|
+ .header("referer", "https://auth.openai.com/email-verification")
|
|
|
|
|
+ .header("accept", "application/json")
|
|
|
|
|
+ .header("content-type", "application/json")
|
|
|
|
|
+ .post(RequestBody.create(mapper.writeValueAsString(Map.of("code", otp2)), MediaType.parse("application/json")))
|
|
|
|
|
+ .build()).execute();
|
|
|
|
|
+ log.info("[日志] 登录 OTP 校验状态: {}", val2.code());
|
|
|
|
|
+ if (val2.code() != 200) {
|
|
|
|
|
+ String body = val2.body() != null ? val2.body().string() : "";
|
|
|
|
|
+ log.warn("[失败] 登录 OTP 校验失败: {}", body.length() > 200 ? body.substring(0, 200) : body);
|
|
|
|
|
+ val2.close();
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ String val2Body = val2.body() != null ? val2.body().string() : "{}";
|
|
|
|
|
+ val2.close();
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ Map<String, Object> val2Data = mapper.readValue(val2Body, Map.class);
|
|
|
|
|
+ log.info("[成功] 登录 OTP 验证成功");
|
|
|
|
|
+
|
|
|
|
|
+ String consentUrl = String.valueOf(val2Data.getOrDefault("continue_url", "")).strip();
|
|
|
|
|
+ if (!consentUrl.isBlank()) {
|
|
|
|
|
+ s2.newCall(new Request.Builder().url(consentUrl).header("user-agent", UA).get().build()).execute().close();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 获取 auth cookie 中的 workspace
|
|
|
|
|
+ String authCookie = getCookieValue(s2, "oai-client-auth-session");
|
|
|
|
|
+ if (authCookie == null || authCookie.isBlank()) {
|
|
|
|
|
+ log.warn("[失败] 登录后未能获取 oai-client-auth-session");
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ Map<String, Object> authJson = PkceUtil.jwtHeaderNoVerify(authCookie);
|
|
|
|
|
+ if (authJson.isEmpty()) {
|
|
|
|
|
+ // try claims
|
|
|
|
|
+ authJson = PkceUtil.jwtClaimsNoVerify(authCookie);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Object workspacesObj = authJson.get("workspaces");
|
|
|
|
|
+ if (!(workspacesObj instanceof List<?> workspaces) || workspaces.isEmpty()) {
|
|
|
|
|
+ log.warn("[失败] Cookie 中无 workspaces: {}", authJson.keySet());
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ Map<?, ?> ws0 = (Map<?, ?>) workspaces.get(0);
|
|
|
|
|
+ String workspaceId = String.valueOf(ws0.get("id"));
|
|
|
|
|
+ log.info("[成功] Workspace ID: {}", workspaceId);
|
|
|
|
|
+
|
|
|
|
|
+ // 选择 Workspace
|
|
|
|
|
+ Response selResp = s2.newCall(new Request.Builder()
|
|
|
|
|
+ .url("https://auth.openai.com/api/accounts/workspace/select")
|
|
|
|
|
+ .header("referer", consentUrl)
|
|
|
|
|
+ .header("accept", "application/json")
|
|
|
|
|
+ .header("content-type", "application/json")
|
|
|
|
|
+ .post(RequestBody.create(mapper.writeValueAsString(Map.of("workspace_id", workspaceId)), MediaType.parse("application/json")))
|
|
|
|
|
+ .build()).execute();
|
|
|
|
|
+ log.info("[日志] Workspace 选择状态: {}", selResp.code());
|
|
|
|
|
+ if (selResp.code() != 200) {
|
|
|
|
|
+ String body = selResp.body() != null ? selResp.body().string() : "";
|
|
|
|
|
+ log.warn("[失败] Workspace 选择失败: {}", body.length() > 200 ? body.substring(0, 200) : body);
|
|
|
|
|
+ selResp.close();
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ String selBody = selResp.body() != null ? selResp.body().string() : "{}";
|
|
|
|
|
+ selResp.close();
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ Map<String, Object> selData = mapper.readValue(selBody, Map.class);
|
|
|
|
|
+
|
|
|
|
|
+ // 处理 organization_select 分支
|
|
|
|
|
+ if ("organization_select".equals(getNestedStr(selData, "page", "type"))) {
|
|
|
|
|
+ List<?> orgs = getNestedList(selData, "page", "payload", "data", "orgs");
|
|
|
|
|
+ if (orgs != null && !orgs.isEmpty()) {
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ Map<String, Object> org0 = (Map<String, Object>) orgs.get(0);
|
|
|
|
|
+ Map<String, String> orgPayload = new LinkedHashMap<>();
|
|
|
|
|
+ orgPayload.put("org_id", String.valueOf(org0.getOrDefault("id", "")));
|
|
|
|
|
+ orgPayload.put("project_id", String.valueOf(org0.getOrDefault("default_project_id", "")));
|
|
|
|
|
+ Response orgSel = s2.newCall(new Request.Builder()
|
|
|
|
|
+ .url("https://auth.openai.com/api/accounts/organization/select")
|
|
|
|
|
+ .header("accept", "application/json")
|
|
|
|
|
+ .header("content-type", "application/json")
|
|
|
|
|
+ .post(RequestBody.create(mapper.writeValueAsString(orgPayload), MediaType.parse("application/json")))
|
|
|
|
|
+ .build()).execute();
|
|
|
|
|
+ log.info("[日志] Organization 选择状态: {}", orgSel.code());
|
|
|
|
|
+ if (orgSel.code() != 200) {
|
|
|
|
|
+ String body = orgSel.body() != null ? orgSel.body().string() : "";
|
|
|
|
|
+ log.warn("[失败] Organization 选择失败: {}", body.length() > 200 ? body.substring(0, 200) : body);
|
|
|
|
|
+ orgSel.close();
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ Map<String, Object> orgData = mapper.readValue(orgSel.body().string(), Map.class);
|
|
|
|
|
+ selData = orgData;
|
|
|
|
|
+ orgSel.close();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!selData.containsKey("continue_url")) {
|
|
|
|
|
+ log.warn("[失败] 未能获取 continue_url: {}", mapper.writeValueAsString(selData));
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 步骤10: 跟踪重定向并换取 Token =====
|
|
|
|
|
+ log.info("[步骤10] 跟踪重定向并换取 Token...");
|
|
|
|
|
+ // Operate with followRedirects=false for manual tracking
|
|
|
|
|
+ OkHttpClient s2NoRedir = s2.newBuilder().followRedirects(false).followSslRedirects(false).build();
|
|
|
|
|
+ Response r = s2NoRedir.newCall(new Request.Builder()
|
|
|
|
|
+ .url(String.valueOf(selData.get("continue_url")))
|
|
|
|
|
+ .header("user-agent", UA).get().build()).execute();
|
|
|
|
|
+ String cbk = null;
|
|
|
|
|
+ for (int i = 0; i < 20; i++) {
|
|
|
|
|
+ String loc = r.header("Location", "");
|
|
|
|
|
+ log.info(" -> 重定向 #{} 状态: {} | 下一跳: {}", i + 1, r.code(),
|
|
|
|
|
+ loc != null && loc.length() > 80 ? loc.substring(0, 80) : loc);
|
|
|
|
|
+ r.close();
|
|
|
|
|
+ if (loc != null && loc.startsWith("http://localhost")) {
|
|
|
|
|
+ cbk = loc;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ if ((r.code() < 301 || r.code() > 303) || loc == null || loc.isBlank()) break;
|
|
|
|
|
+ r = s2NoRedir.newCall(new Request.Builder().url(loc).header("user-agent", UA).get().build()).execute();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (cbk == null) {
|
|
|
|
|
+ log.warn("[失败] 未能获取到 Callback URL");
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String tokenJson = oauthService.submitCallbackUrl(s2, cbk, oauth2.getState(), oauth2.getCodeVerifier(), oauth2.getRedirectUri());
|
|
|
|
|
+ log.info("[大功告成] 账号注册完毕!");
|
|
|
|
|
+
|
|
|
|
|
+ RegisterResult result = new RegisterResult();
|
|
|
|
|
+ result.setEmail(email);
|
|
|
|
|
+ result.setPassword(password);
|
|
|
|
|
+ result.setTokenJson(tokenJson);
|
|
|
|
|
+ return Optional.of(result);
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("[失败] 登录补全流程异常: {}", e.getMessage());
|
|
|
|
|
+ Thread.sleep(2000);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ log.warn("[失败] 登录补全流程 3 次均未完成。");
|
|
|
|
|
+ return Optional.empty();
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("[致命错误] 流程崩溃: {}", e.getMessage(), e);
|
|
|
|
|
+ return Optional.empty();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ===== helper methods =====
|
|
|
|
|
+
|
|
|
|
|
+ private OkHttpClient buildClient(String proxy) {
|
|
|
|
|
+ CookieManager cm = new CookieManager(null, CookiePolicy.ACCEPT_ALL);
|
|
|
|
|
+ OkHttpClient.Builder b = new OkHttpClient.Builder()
|
|
|
|
|
+ .cookieJar(new JavaNetCookieJar(cm))
|
|
|
|
|
+ .connectTimeout(15, TimeUnit.SECONDS)
|
|
|
|
|
+ .readTimeout(30, TimeUnit.SECONDS)
|
|
|
|
|
+ .followRedirects(true)
|
|
|
|
|
+ .followSslRedirects(true);
|
|
|
|
|
+ if (proxy != null && !proxy.isBlank()) {
|
|
|
|
|
+ String noProto = proxy.replaceFirst("^https?://", "");
|
|
|
|
|
+ String[] parts = noProto.split(":");
|
|
|
|
|
+ if (parts.length == 2) {
|
|
|
|
|
+ b.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(parts[0], Integer.parseInt(parts[1]))));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return b.build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String getCookieValue(OkHttpClient client, String name) {
|
|
|
|
|
+ // OkHttp's CookieJar is backed by JavaNetCookieJar/CookieManager — inspect via cookie jar
|
|
|
|
|
+ // We iterate all URLs we know cookies might be on
|
|
|
|
|
+ for (String domain : List.of("https://auth.openai.com", "https://openai.com")) {
|
|
|
|
|
+ List<Cookie> cookies = client.cookieJar().loadForRequest(HttpUrl.parse(domain));
|
|
|
|
|
+ for (Cookie c : cookies) {
|
|
|
|
|
+ if (c.name().equals(name)) return c.value();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ private String getNestedStr(Map<String, Object> map, String... keys) {
|
|
|
|
|
+ Object current = map;
|
|
|
|
|
+ for (String key : keys) {
|
|
|
|
|
+ if (!(current instanceof Map)) return "";
|
|
|
|
|
+ current = ((Map<String, Object>) current).get(key);
|
|
|
|
|
+ }
|
|
|
|
|
+ return current == null ? "" : String.valueOf(current);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ private List<?> getNestedList(Map<String, Object> map, String... keys) {
|
|
|
|
|
+ Object current = map;
|
|
|
|
|
+ for (String key : keys) {
|
|
|
|
|
+ if (!(current instanceof Map)) return null;
|
|
|
|
|
+ current = ((Map<String, Object>) current).get(key);
|
|
|
|
|
+ }
|
|
|
|
|
+ return current instanceof List ? (List<?>) current : null;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|