husj 1 bulan lalu
induk
melakukan
5cd94f879d

+ 71 - 0
.gitignore

@@ -0,0 +1,71 @@
+# Compiled class files
+*.class
+
+# Log files
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# JVM crash logs
+hs_err_pid*
+replay_pid*
+
+# Maven
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/wrapper/maven-wrapper.jar
+
+# Gradle
+.gradle/
+build/
+!gradle/wrapper/gradle-wrapper.jar
+
+# IntelliJ IDEA
+.idea/
+*.iml
+*.iws
+*.ipr
+out/
+
+# Eclipse
+.project
+.classpath
+.settings/
+bin/
+
+# VS Code
+.vscode/
+
+# NetBeans
+nbproject/private/
+nbbuild/
+dist/
+nbdist/
+.nb-gradle/
+
+# OS generated files
+.DS_Store
+Thumbs.db
+
+# Environment files
+.env
+.env.*

+ 23 - 10
src/main/java/com/husj/openai/service/OAuthService.java

@@ -13,7 +13,8 @@ import java.time.Instant;
 import java.util.*;
 
 /**
- * OAuth URL 生成 与 Token 换取 — 对应 Python generate_oauth_url() + submit_callback_url()
+ * OAuth URL 生成 与 Token 换取 — 对应 Python generate_oauth_url() +
+ * submit_callback_url()
  */
 @Slf4j
 @Service
@@ -30,8 +31,9 @@ public class OAuthService {
     /**
      * 生成 OAuth 授权 URL (含 PKCE)
      */
-    public OAuthStart generateOAuthUrl(String redirectUri) {
-        if (redirectUri == null || redirectUri.isBlank()) redirectUri = DEFAULT_REDIRECT_URI;
+    public OAuthStart generateOAuthUrl(String redirectUri, String screenHint) {
+        if (redirectUri == null || redirectUri.isBlank())
+            redirectUri = DEFAULT_REDIRECT_URI;
         String state = PkceUtil.randomState();
         String verifier = PkceUtil.pkceVerifier();
         String challenge = PkceUtil.sha256B64urlNoPad(verifier);
@@ -45,12 +47,16 @@ public class OAuthService {
         params.put("code_challenge", challenge);
         params.put("code_challenge_method", "S256");
         params.put("prompt", "login");
+        if (screenHint != null && !screenHint.isBlank()) {
+            params.put("screen_hint", screenHint);
+        }
         params.put("id_token_add_organizations", "true");
         params.put("codex_cli_simplified_flow", "true");
 
         StringBuilder query = new StringBuilder();
         for (Map.Entry<String, String> e : params.entrySet()) {
-            if (!query.isEmpty()) query.append("&");
+            if (!query.isEmpty())
+                query.append("&");
             query.append(URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8))
                     .append("=")
                     .append(URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8));
@@ -63,16 +69,19 @@ public class OAuthService {
      * 提交 callback URL 换取 token (对应 Python submit_callback_url)
      */
     public String submitCallbackUrl(OkHttpClient client, String callbackUrl, String expectedState,
-                                     String codeVerifier, String redirectUri) throws Exception {
+            String codeVerifier, String redirectUri) throws Exception {
         Map<String, String> cb = PkceUtil.parseCallbackUrl(callbackUrl);
         String error = cb.get("error");
         if (error != null && !error.isBlank()) {
             throw new RuntimeException("OAuth 错误: " + error + ": " + cb.get("error_description"));
         }
         String code = cb.get("code");
-        if (code == null || code.isBlank()) throw new IllegalArgumentException("Callback URL 缺少 ?code=");
-        if (cb.get("state") == null || cb.get("state").isBlank()) throw new IllegalArgumentException("Callback URL 缺少 ?state=");
-        if (!cb.get("state").equals(expectedState)) throw new IllegalArgumentException("State 校验不匹配");
+        if (code == null || code.isBlank())
+            throw new IllegalArgumentException("Callback URL 缺少 ?code=");
+        if (cb.get("state") == null || cb.get("state").isBlank())
+            throw new IllegalArgumentException("Callback URL 缺少 ?state=");
+        if (!cb.get("state").equals(expectedState))
+            throw new IllegalArgumentException("State 校验不匹配");
 
         // Exchange code for token
         FormBody.Builder fb = new FormBody.Builder()
@@ -104,12 +113,16 @@ public class OAuthService {
             int expiresIn = 0;
             Object expObj = tokenResp.get("expires_in");
             if (expObj != null) {
-                try { expiresIn = Integer.parseInt(expObj.toString()); } catch (Exception ignored) {}
+                try {
+                    expiresIn = Integer.parseInt(expObj.toString());
+                } catch (Exception ignored) {
+                }
             }
 
             Map<String, Object> claims = PkceUtil.jwtClaimsNoVerify(idToken);
             @SuppressWarnings("unchecked")
-            Map<String, Object> authClaims = (Map<String, Object>) claims.getOrDefault("https://api.openai.com/auth", Map.of());
+            Map<String, Object> authClaims = (Map<String, Object>) claims.getOrDefault("https://api.openai.com/auth",
+                    Map.of());
 
             long now = Instant.now().getEpochSecond();
             Map<String, Object> config = new LinkedHashMap<>();

+ 84 - 26
src/main/java/com/husj/openai/service/OpenAiRegisterService.java

@@ -56,11 +56,23 @@ public class OpenAiRegisterService {
 
             // ===== 步骤2: 访问 OpenAI 授权页获取 Device ID =====
             log.info("[步骤2] 访问 OpenAI 授权页获取 Device ID...");
-            OAuthStart oauth = oauthService.generateOAuthUrl(OAuthService.DEFAULT_REDIRECT_URI);
+            OAuthStart oauth = oauthService.generateOAuthUrl(OAuthService.DEFAULT_REDIRECT_URI, "signup");
             Response authPageResp = s.newCall(new Request.Builder().url(oauth.getAuthUrl())
                     .header("user-agent", UA).header("accept", "application/json, text/plain, */*").build()).execute();
+            String authBody = authPageResp.body() != null ? authPageResp.body().string() : "";
             authPageResp.close();
 
+            try {
+                @SuppressWarnings("unchecked")
+                Map<String, Object> authJson = mapper.readValue(authBody, Map.class);
+                String continueUrl = String.valueOf(authJson.getOrDefault("continue_url", "")).strip();
+                if (!continueUrl.isBlank()) {
+                    s.newCall(new Request.Builder().url(continueUrl).header("user-agent", UA).get().build()).execute()
+                            .close();
+                }
+            } catch (Exception ignored) {
+            }
+
             String did = getCookieValue(s, "oai-did");
             if (did == null || did.isBlank()) {
                 log.warn("[失败] 未能从 Cookie 获取 oai-did");
@@ -88,7 +100,8 @@ public class OpenAiRegisterService {
                     .header("accept", "application/json")
                     .header("content-type", "application/json")
                     .header("openai-sentinel-token", authorizeContinueSentinel)
-                    .post(RequestBody.create(mapper.writeValueAsString(usernamePayload), MediaType.parse("application/json")))
+                    .post(RequestBody.create(mapper.writeValueAsString(usernamePayload),
+                            MediaType.parse("application/json")))
                     .build()).execute();
             log.info("[日志] 邮箱提交状态: {}", signupRes.code());
             if (signupRes.code() != 200) {
@@ -101,13 +114,16 @@ public class OpenAiRegisterService {
 
             // ===== 步骤4: 设置账户密码 =====
             log.info("[步骤4] 设置账户密码...");
-            Map<String, String> pwdPayload = Map.of("password", password, "username", email);
+            Map<String, String> pwdPayload = new LinkedHashMap<>();
+            pwdPayload.put("username", email);
+            pwdPayload.put("password", password);
             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")))
+                    .post(RequestBody.create(mapper.writeValueAsString(pwdPayload),
+                            MediaType.parse("application/json")))
                     .build()).execute();
             log.info("[日志] 密码设置状态: {}", pwdRes.code());
             if (pwdRes.code() != 200) {
@@ -147,12 +163,15 @@ public class OpenAiRegisterService {
 
             // ===== 步骤7: 提交验证码 =====
             log.info("[步骤7] 提交验证码至 OpenAI...");
+            Map<String, String> otpPayload = new LinkedHashMap<>();
+            otpPayload.put("code", code);
             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")))
+                    .post(RequestBody.create(mapper.writeValueAsString(otpPayload),
+                            MediaType.parse("application/json")))
                     .build()).execute();
             log.info("[日志] 验证码校验状态: {}", valRes.code());
             if (valRes.code() != 200) {
@@ -173,14 +192,17 @@ public class OpenAiRegisterService {
                 return Optional.empty();
             }
 
-            Map<String, String> accPayload = Map.of("name", PkceUtil.randomName(), "birthdate", PkceUtil.randomBirthdate());
+            Map<String, String> accPayload = new LinkedHashMap<>();
+            accPayload.put("name", PkceUtil.randomName());
+            accPayload.put("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")))
+                    .post(RequestBody.create(mapper.writeValueAsString(accPayload),
+                            MediaType.parse("application/json")))
                     .build()).execute();
             log.info("[日志] 账户创建状态: {}", accRes.code());
             if (accRes.code() != 200) {
@@ -198,8 +220,23 @@ public class OpenAiRegisterService {
                 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();
+                    OAuthStart oauth2 = oauthService.generateOAuthUrl(OAuthService.DEFAULT_REDIRECT_URI, "login");
+                    Response lRes = s2
+                            .newCall(new Request.Builder().url(oauth2.getAuthUrl()).header("user-agent", UA).build())
+                            .execute();
+                    String lBody = lRes.body() != null ? lRes.body().string() : "";
+                    lRes.close();
+
+                    try {
+                        @SuppressWarnings("unchecked")
+                        Map<String, Object> lJson = mapper.readValue(lBody, Map.class);
+                        String lContinueUrl = String.valueOf(lJson.getOrDefault("continue_url", "")).strip();
+                        if (!lContinueUrl.isBlank()) {
+                            s2.newCall(new Request.Builder().url(lContinueUrl).header("user-agent", UA).get().build())
+                                    .execute().close();
+                        }
+                    } catch (Exception ignored) {
+                    }
 
                     String did2 = getCookieValue(s2, "oai-did");
                     if (did2 == null || did2.isBlank()) {
@@ -217,8 +254,10 @@ public class OpenAiRegisterService {
                             .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")))
+                            .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) {
@@ -233,17 +272,22 @@ public class OpenAiRegisterService {
                     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();
+                        s2.newCall(new Request.Builder().url(continueUrl).header("user-agent", UA).get().build())
+                                .execute().close();
                     }
 
                     // 提交密码
+                    Map<String, String> lPwdPayload = new LinkedHashMap<>();
+                    lPwdPayload.put("password", password);
                     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")))
+                            .header("openai-sentinel-token",
+                                    sentinelService.buildSentinelPayload(s2, did2, "authorize_continue"))
+                            .post(RequestBody.create(mapper.writeValueAsString(lPwdPayload),
+                                    MediaType.parse("application/json")))
                             .build()).execute();
                     log.info("[日志] 登录密码验证状态: {}", pw.code());
                     if (pw.code() != 200) {
@@ -273,12 +317,15 @@ public class OpenAiRegisterService {
                     log.info("[成功] 捕获登录 OTP: {}", otp2);
 
                     // 提交登录 OTP
+                    Map<String, String> lOtpPayload = new LinkedHashMap<>();
+                    lOtpPayload.put("code", otp2);
                     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")))
+                            .post(RequestBody.create(mapper.writeValueAsString(lOtpPayload),
+                                    MediaType.parse("application/json")))
                             .build()).execute();
                     log.info("[日志] 登录 OTP 校验状态: {}", val2.code());
                     if (val2.code() != 200) {
@@ -295,7 +342,8 @@ public class OpenAiRegisterService {
 
                     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();
+                        s2.newCall(new Request.Builder().url(consentUrl).header("user-agent", UA).get().build())
+                                .execute().close();
                     }
 
                     // 获取 auth cookie 中的 workspace
@@ -325,7 +373,8 @@ public class OpenAiRegisterService {
                             .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")))
+                            .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) {
@@ -352,12 +401,14 @@ public class OpenAiRegisterService {
                                     .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")))
+                                    .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);
+                                log.warn("[失败] Organization 选择失败: {}",
+                                        body.length() > 200 ? body.substring(0, 200) : body);
                                 orgSel.close();
                                 continue;
                             }
@@ -390,8 +441,10 @@ public class OpenAiRegisterService {
                             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 ((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) {
@@ -399,7 +452,8 @@ public class OpenAiRegisterService {
                         continue;
                     }
 
-                    String tokenJson = oauthService.submitCallbackUrl(s2, cbk, oauth2.getState(), oauth2.getCodeVerifier(), oauth2.getRedirectUri());
+                    String tokenJson = oauthService.submitCallbackUrl(s2, cbk, oauth2.getState(),
+                            oauth2.getCodeVerifier(), oauth2.getRedirectUri());
                     log.info("[大功告成] 账号注册完毕!");
 
                     RegisterResult result = new RegisterResult();
@@ -443,12 +497,14 @@ public class OpenAiRegisterService {
     }
 
     private String getCookieValue(OkHttpClient client, String name) {
-        // OkHttp's CookieJar is backed by JavaNetCookieJar/CookieManager — inspect via cookie jar
+        // 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();
+                if (c.name().equals(name))
+                    return c.value();
             }
         }
         return null;
@@ -458,7 +514,8 @@ public class OpenAiRegisterService {
     private String getNestedStr(Map<String, Object> map, String... keys) {
         Object current = map;
         for (String key : keys) {
-            if (!(current instanceof Map)) return "";
+            if (!(current instanceof Map))
+                return "";
             current = ((Map<String, Object>) current).get(key);
         }
         return current == null ? "" : String.valueOf(current);
@@ -468,7 +525,8 @@ public class OpenAiRegisterService {
     private List<?> getNestedList(Map<String, Object> map, String... keys) {
         Object current = map;
         for (String key : keys) {
-            if (!(current instanceof Map)) return null;
+            if (!(current instanceof Map))
+                return null;
             current = ((Map<String, Object>) current).get(key);
         }
         return current instanceof List ? (List<?>) current : null;