init
This commit is contained in:
parent
3e1e1cfc13
commit
5bec08604e
@ -45,7 +45,7 @@
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.34</version>
|
||||
<version>1.18.30</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@ -99,17 +99,33 @@
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
<version>1.18.30</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<!-- <plugin>-->
|
||||
<!-- <groupId>org.springframework.boot</groupId>-->
|
||||
<!-- <artifactId>spring-boot-maven-plugin</artifactId>-->
|
||||
<!-- <configuration>-->
|
||||
<!-- <excludes>-->
|
||||
<!-- <exclude>-->
|
||||
<!-- <groupId>org.projectlombok</groupId>-->
|
||||
<!-- <artifactId>lombok</artifactId>-->
|
||||
<!-- </exclude>-->
|
||||
<!-- </excludes>-->
|
||||
<!-- </configuration>-->
|
||||
<!-- </plugin>-->
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ import jakarta.annotation.PreDestroy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.servlet.resource.ResourceUrlProvider;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
@ -25,6 +26,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
public class BrowserService {
|
||||
|
||||
private final CookieRepository cookieRepository;
|
||||
private final ResourceUrlProvider resourceUrlProvider;
|
||||
private Playwright playwright;
|
||||
private Browser browser;
|
||||
|
||||
@ -35,8 +37,9 @@ public class BrowserService {
|
||||
private final Map<String, Object> contextLocks = new ConcurrentHashMap<>();
|
||||
|
||||
@Autowired
|
||||
public BrowserService(CookieRepository cookieRepository) {
|
||||
public BrowserService(CookieRepository cookieRepository, ResourceUrlProvider resourceUrlProvider) {
|
||||
this.cookieRepository = cookieRepository;
|
||||
this.resourceUrlProvider = resourceUrlProvider;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
@ -523,6 +526,54 @@ public class BrowserService {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean attemptQuickLogin(Frame frame) {
|
||||
boolean containerFound = false;
|
||||
if (Objects.isNull(frame)) return containerFound;
|
||||
ElementHandle elementHandle = frame.querySelector("#alibaba-login-box");
|
||||
if (Objects.isNull(elementHandle)) return containerFound;
|
||||
Frame quickLoginFrame = elementHandle.contentFrame();
|
||||
if (Objects.isNull(quickLoginFrame)) return containerFound;
|
||||
ElementHandle loginButton = quickLoginFrame.querySelector(".fm-button.fm-submit");
|
||||
if (Objects.isNull(loginButton)) return containerFound;
|
||||
if (loginButton.isVisible()) {
|
||||
loginButton.click();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean attemptQuickLoginV2(Frame frame) {
|
||||
try {
|
||||
String[] loginButtonSelectors = {".has-login", ".cm-has-login", ".fm-btn", ".fm-button", ".fm-submit"};
|
||||
|
||||
boolean containerFound = false;
|
||||
for (String s : loginButtonSelectors) {
|
||||
if (frame.querySelector(s) != null && frame.isVisible(s)) {
|
||||
containerFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!containerFound) return false;
|
||||
|
||||
ElementHandle loginButtonDialog = frame.querySelector(".has-login");
|
||||
if (loginButtonDialog == null) loginButtonDialog = frame.querySelector(".cm-has-login");
|
||||
|
||||
if (loginButtonDialog != null && loginButtonDialog.isVisible()) {
|
||||
log.info("【Login Task】Detected quick login in frame: {}", frame.url());
|
||||
|
||||
ElementHandle loginButton = frame.querySelector(".fm-button");
|
||||
if (loginButton == null) loginButton = frame.querySelector(".fm-submit");
|
||||
if (loginButton == null) return false;
|
||||
loginButton.click();
|
||||
log.info("【Login Task】quick login success!");
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("【Login Task】quick login fail : {}", e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新Cookie - 使用持久化浏览器上下文
|
||||
* Cookie会自动保存到UserData目录,类似真实浏览器行为
|
||||
@ -574,10 +625,18 @@ public class BrowserService {
|
||||
|
||||
// 3. 等待页面加载
|
||||
try {
|
||||
Thread.sleep(3000);
|
||||
Thread.sleep(5000);
|
||||
} catch (Exception e) {
|
||||
}
|
||||
|
||||
|
||||
// 判断是否有快捷登陆iframe
|
||||
for (Frame frame : page.frames()) {
|
||||
if (attemptQuickLogin(frame)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 重新加载页面以触发Cookie刷新
|
||||
log.info("【{}-Cookie Refresh】重新加载页面...", cookieId);
|
||||
try {
|
||||
@ -618,6 +677,7 @@ public class BrowserService {
|
||||
// 9. 更新数据库
|
||||
if (!newCookieStr.equals(cookie.getValue())) {
|
||||
cookie.setValue(newCookieStr);
|
||||
log.debug("【{}】🤖刷新浏览器后获取到的 cookie 为: {}", cookieId, newCookieStr);
|
||||
cookieRepository.save(cookie);
|
||||
log.info("【{}-Cookie Refresh】✅ Cookie已更新并保存到数据库: {}", cookieId, cookieId);
|
||||
} else {
|
||||
@ -652,7 +712,7 @@ public class BrowserService {
|
||||
log.info("【QR Login】Verifying cookies for account: {}", accountId);
|
||||
|
||||
try (BrowserContext context = browser.newContext(new Browser.NewContextOptions()
|
||||
.setUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0")
|
||||
.setUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0")
|
||||
// .setViewportSize(1920, 1080)
|
||||
)) {
|
||||
|
||||
@ -850,13 +910,13 @@ public class BrowserService {
|
||||
if (java.nio.file.Files.isDirectory(path)) {
|
||||
try (java.util.stream.Stream<java.nio.file.Path> stream = java.nio.file.Files.walk(path)) {
|
||||
stream.sorted(java.util.Comparator.reverseOrder())
|
||||
.forEach(p -> {
|
||||
try {
|
||||
java.nio.file.Files.delete(p);
|
||||
} catch (java.io.IOException e) {
|
||||
log.warn("删除文件失败: {}", p, e);
|
||||
}
|
||||
});
|
||||
.forEach(p -> {
|
||||
try {
|
||||
java.nio.file.Files.delete(p);
|
||||
} catch (java.io.IOException e) {
|
||||
log.warn("删除文件失败: {}", p, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -356,14 +356,14 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
|
||||
|
||||
/**
|
||||
* WebSocket连接循环 - 对应Python的main方法中的while True循环
|
||||
* WebSocket连接循环 - 重构版本,去除嵌套
|
||||
* 核心逻辑:外层循环保持运行,内部单次连接尝试,失败后延迟重试
|
||||
*/
|
||||
private void connectionLoop() {
|
||||
while (running.get()) {
|
||||
try {
|
||||
// 检查账号是否启用
|
||||
Optional<Cookie> cookieOpt = cookieRepository.findById(cookieId);
|
||||
if (cookieOpt.isEmpty() || !Boolean.TRUE.equals(cookieOpt.get().getEnabled())) {
|
||||
if (!isAccountEnabled()) {
|
||||
log.info("【{}】账号已禁用,停止连接循环", cookieId);
|
||||
break;
|
||||
}
|
||||
@ -372,48 +372,73 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
setConnectionState(ConnectionState.CONNECTING, "准备建立WebSocket连接");
|
||||
log.info("【{}】WebSocket目标地址: {}", cookieId, WEBSOCKET_URL);
|
||||
|
||||
// 创建WebSocket连接
|
||||
// 单次连接尝试
|
||||
connectWebSocket();
|
||||
|
||||
// 连接成功后,等待连接断开
|
||||
// WebSocket会在另一个线程中运行,这里需要阻塞等待
|
||||
log.info("【{}】WebSocket连接已建立,等待连接断开...", cookieId);
|
||||
while (connected.get() && running.get()) {
|
||||
try {
|
||||
Thread.sleep(1000); // 每秒检查一次连接状态
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 连接成功后,进入等待循环,直到连接断开
|
||||
waitForDisconnection();
|
||||
|
||||
log.info("【{}】WebSocket连接已断开", cookieId);
|
||||
|
||||
} catch (Exception e) {
|
||||
// 统一处理连接错误
|
||||
handleConnectionError(e);
|
||||
}
|
||||
|
||||
// 重连延迟
|
||||
// 计算并执行重连延迟
|
||||
if (running.get()) {
|
||||
int retryDelay = calculateRetryDelay(connectionFailures.get());
|
||||
log.info("【{}】{}秒后尝试重连...", cookieId, retryDelay);
|
||||
try {
|
||||
Thread.sleep(retryDelay * 1000L);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
sleepWithInterruptCheck(retryDelay * 1000L);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("【{}】WebSocket 连接循环已退出", cookieId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查账号是否启用
|
||||
*/
|
||||
private boolean isAccountEnabled() {
|
||||
try {
|
||||
Optional<Cookie> cookieOpt = cookieRepository.findById(cookieId);
|
||||
return cookieOpt.isPresent() && Boolean.TRUE.equals(cookieOpt.get().getEnabled());
|
||||
} catch (Exception e) {
|
||||
log.error("【{}】检查账号状态失败", cookieId, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建WebSocket连接
|
||||
* 等待WebSocket连接断开
|
||||
*/
|
||||
private void waitForDisconnection() {
|
||||
log.info("【{}】WebSocket连接已建立,等待连接断开...", cookieId);
|
||||
while (connected.get() && running.get()) {
|
||||
sleepWithInterruptCheck(1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 可中断的Sleep
|
||||
*/
|
||||
private void sleepWithInterruptCheck(long millis) {
|
||||
try {
|
||||
Thread.sleep(millis);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.warn("【{}】Sleep被中断", cookieId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建WebSocket连接 - 重构版本,纯粹的单次连接尝试
|
||||
* 失败直接抛异常,由 connectionLoop() 统一处理重试
|
||||
*/
|
||||
private void connectWebSocket() throws Exception {
|
||||
log.info("【{}】开始建立WebSocket连接...", cookieId);
|
||||
|
||||
// 配置WebSocket容器,设置缓冲区大小为10MB(解决1009错误:消息过大)
|
||||
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
|
||||
container.setDefaultMaxTextMessageBufferSize(10 * 1024 * 1024); // 10MB
|
||||
@ -422,9 +447,33 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
// 使用配置好的容器创建WebSocket客户端
|
||||
WebSocketClient client = new StandardWebSocketClient(container);
|
||||
|
||||
// 准备请求头 - 使用WebSocketHttpHeaders,添加所有必要的headers
|
||||
WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
|
||||
// 准备请求头
|
||||
WebSocketHttpHeaders headers = buildWebSocketHeaders();
|
||||
|
||||
try {
|
||||
// 发起WebSocket握手
|
||||
ListenableFuture<WebSocketSession> future =
|
||||
client.doHandshake(this, headers, URI.create(WEBSOCKET_URL));
|
||||
|
||||
// 等待连接完成(超时10秒)
|
||||
this.webSocketSession = future.get(10, TimeUnit.SECONDS);
|
||||
log.info("【{}】WebSocket连接建立成功", cookieId);
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new Exception("WebSocket连接被中断", e);
|
||||
} catch (java.util.concurrent.ExecutionException e) {
|
||||
throw new Exception("WebSocket连接执行失败: " + e.getMessage(), e);
|
||||
} catch (java.util.concurrent.TimeoutException e) {
|
||||
throw new Exception("WebSocket连接超时", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建WebSocket请求头
|
||||
*/
|
||||
private WebSocketHttpHeaders buildWebSocketHeaders() {
|
||||
WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
|
||||
headers.add("Accept-Encoding", "gzip, deflate, br, zstd");
|
||||
headers.add("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6");
|
||||
headers.add("Cache-Control", "no-cache");
|
||||
@ -437,24 +486,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
headers.add("sec-websocket-version", "13");
|
||||
headers.add("upgrade", "websocket");
|
||||
headers.add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0");
|
||||
|
||||
try {
|
||||
// doHandshake参数: WebSocketHandler, WebSocketHttpHeaders, URI
|
||||
ListenableFuture<WebSocketSession> future =
|
||||
client.doHandshake(this, headers, URI.create(WEBSOCKET_URL));
|
||||
|
||||
// 等待连接完成
|
||||
this.webSocketSession = future.get(10, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new Exception("WebSocket连接被中断", e);
|
||||
} catch (java.util.concurrent.ExecutionException e) {
|
||||
throw new Exception("WebSocket连接执行失败: " + e.getMessage(), e);
|
||||
} catch (java.util.concurrent.TimeoutException e) {
|
||||
throw new Exception("WebSocket连接超时", e);
|
||||
}
|
||||
|
||||
log.info("【{}】WebSocket连接建立成功", cookieId);
|
||||
return headers;
|
||||
}
|
||||
|
||||
|
||||
@ -590,7 +622,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
|
||||
JSONObject regHeaders = new JSONObject();
|
||||
regHeaders.put("cache-header", "app-key token ua wv");
|
||||
regHeaders.put("app-key",WEBSOCKET_APP_KEY);
|
||||
regHeaders.put("app-key", WEBSOCKET_APP_KEY);
|
||||
regHeaders.put("token", currentToken);
|
||||
regHeaders.put("ua", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0");
|
||||
regHeaders.put("dt", "j");
|
||||
@ -650,124 +682,109 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
|
||||
|
||||
/**
|
||||
* 刷新Token - 对应Python的refresh_token()方法
|
||||
* 添加自动降级机制:Token获取失败时自动刷新Cookie
|
||||
* 刷新Token - 重构版本,去除嵌套循环
|
||||
* 策略:尝试获取Token,失败则刷新Cookie后抛异常,由上层决定是否重试
|
||||
*/
|
||||
private String refreshToken() {
|
||||
int maxRetries = 3;
|
||||
int retryCount = 0;
|
||||
lastTokenRefreshStatus = "started";
|
||||
log.info("【{}】开始刷新token...", cookieId);
|
||||
|
||||
while (retryCount < maxRetries) {
|
||||
try {
|
||||
if (retryCount > 0) {
|
||||
log.info("【{}】Token获取失败,第 {} 次重试...", cookieId, retryCount);
|
||||
// 添加重试延迟,避免过快重试导致资源竞争
|
||||
try {
|
||||
Thread.sleep(2000 * retryCount); // 指数退避:2s, 4s, 6s
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
} else {
|
||||
log.info("【{}】开始刷新token...", cookieId);
|
||||
}
|
||||
lastTokenRefreshStatus = "started";
|
||||
|
||||
// 检查是否在消息冷却期内
|
||||
long currentTime = System.currentTimeMillis();
|
||||
long timeSinceLastMessage = currentTime - lastMessageReceivedTime.get();
|
||||
if (lastMessageReceivedTime.get() > 0 && timeSinceLastMessage < MESSAGE_COOLDOWN * 1000L) {
|
||||
long remainingTime = MESSAGE_COOLDOWN * 1000L - timeSinceLastMessage;
|
||||
log.info("【{}】收到消息后冷却中,放弃本次token刷新,还需等待 {} 秒",
|
||||
cookieId, remainingTime / 1000);
|
||||
lastTokenRefreshStatus = "skipped_cooldown";
|
||||
return null;
|
||||
}
|
||||
|
||||
// 从数据库重新加载Cookie(可能已被浏览器刷新更新)
|
||||
try {
|
||||
Optional<Cookie> cookieOpt = cookieRepository.findById(cookieId);
|
||||
if (cookieOpt.isPresent()) {
|
||||
String newCookiesStr = cookieOpt.get().getValue();
|
||||
if (!newCookiesStr.equals(this.cookiesStr)) {
|
||||
log.info("【{}】检测到数据库中的cookie已更新,重新加载cookie", cookieId);
|
||||
this.cookiesStr = newCookiesStr;
|
||||
this.cookies = parseCookies(this.cookiesStr);
|
||||
log.warn("【{}】Cookie已从数据库重新加载", cookieId);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("【{}】从数据库重新加载cookie失败,继续使用当前cookie: {}", cookieId, e.getMessage());
|
||||
}
|
||||
|
||||
// 尝试获取Token
|
||||
log.debug("【{}】🤖准备调用官方API获取Token...", cookieId);
|
||||
String token = attemptGetToken();
|
||||
log.debug("【{}】🤖准备调用官方API获取Token为: {}", cookieId, token);
|
||||
|
||||
if (token != null) {
|
||||
// Token获取成功
|
||||
this.currentToken = token;
|
||||
this.lastTokenRefreshTime.set(System.currentTimeMillis());
|
||||
this.lastMessageReceivedTime.set(0); // 重置消息接收时间
|
||||
log.warn("【{}】✅ Token刷新成功", cookieId);
|
||||
lastTokenRefreshStatus = "success";
|
||||
return token;
|
||||
}
|
||||
|
||||
// Token获取失败,尝试刷新Cookie
|
||||
log.warn("【{}】⚠️ Token获取失败,尝试通过浏览器刷新Cookie...", cookieId);
|
||||
|
||||
try {
|
||||
Map<String, String> newCookies = browserService.refreshCookies(cookieId);
|
||||
|
||||
if (newCookies != null && !newCookies.isEmpty()) {
|
||||
log.info("【{}】✅ Cookie刷新成功,重新加载...", cookieId);
|
||||
// 重新加载Cookie
|
||||
loadCookies();
|
||||
retryCount++;
|
||||
// 继续下一轮重试
|
||||
continue;
|
||||
} else {
|
||||
log.error("【{}】❌ Cookie刷新失败,尝试强制重建持久化上下文", cookieId);
|
||||
// 强制关闭持久化上下文,下次重试时会重新创建
|
||||
try {
|
||||
browserService.closePersistentContext(cookieId);
|
||||
Thread.sleep(3000); // 等待3秒确保资源完全释放
|
||||
|
||||
// 再次尝试刷新Cookie
|
||||
newCookies = browserService.refreshCookies(cookieId);
|
||||
if (newCookies != null && !newCookies.isEmpty()) {
|
||||
log.info("【{}】重建上下文后Cookie刷新成功", cookieId);
|
||||
loadCookies();
|
||||
retryCount++;
|
||||
continue;
|
||||
}
|
||||
} catch (Exception retryEx) {
|
||||
log.error("【{}】重建上下文后仍然失败: {}", cookieId, retryEx.getMessage());
|
||||
}
|
||||
|
||||
log.error("【{}】❌ Cookie刷新最终失败,无法继续", cookieId);
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("【{}】❌ Cookie刷新异常: {}", cookieId, e.getMessage());
|
||||
// 异常时也尝试关闭上下文
|
||||
try {
|
||||
browserService.closePersistentContext(cookieId);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("【{}】Token刷新过程异常", cookieId, e);
|
||||
break;
|
||||
}
|
||||
// 检查是否在消息冷却期内
|
||||
long currentTime = System.currentTimeMillis();
|
||||
long timeSinceLastMessage = currentTime - lastMessageReceivedTime.get();
|
||||
if (lastMessageReceivedTime.get() > 0 && timeSinceLastMessage < MESSAGE_COOLDOWN * 1000L) {
|
||||
long remainingTime = MESSAGE_COOLDOWN * 1000L - timeSinceLastMessage;
|
||||
log.info("【{}】收到消息后冷却中,放弃本次token刷新,还需等待 {} 秒",
|
||||
cookieId, remainingTime / 1000);
|
||||
lastTokenRefreshStatus = "skipped_cooldown";
|
||||
return null;
|
||||
}
|
||||
|
||||
log.error("【{}】❌ Token刷新最终失败,已重试 {} 次", cookieId, retryCount);
|
||||
lastTokenRefreshStatus = "failed";
|
||||
return null;
|
||||
// 从数据库重新加载Cookie(可能已被浏览器刷新更新)
|
||||
reloadCookieFromDatabase();
|
||||
|
||||
// 尝试获取Token
|
||||
log.debug("【{}】🤖准备调用官方API获取Token...", cookieId);
|
||||
String token = attemptGetToken();
|
||||
log.debug("【{}】🤖调用官方API获取Token结果: {}", cookieId, token != null ? "成功" : "失败");
|
||||
|
||||
if (token != null) {
|
||||
// Token获取成功
|
||||
this.currentToken = token;
|
||||
this.lastTokenRefreshTime.set(System.currentTimeMillis());
|
||||
this.lastMessageReceivedTime.set(0);
|
||||
log.warn("【{}】✅ Token刷新成功", cookieId);
|
||||
lastTokenRefreshStatus = "success";
|
||||
return token;
|
||||
}
|
||||
|
||||
// Token获取失败,尝试刷新Cookie然后返回null
|
||||
// 不在这里重试,失败后让connectionLoop处理重连
|
||||
log.warn("【{}】⚠️ Token获取失败,尝试通过浏览器刷新Cookie...", cookieId);
|
||||
refreshCookieViaBrowser();
|
||||
|
||||
// 尝试获取Token
|
||||
log.debug("【{}】🤖刷新Cookie后调用官方API获取Token...", cookieId);
|
||||
token = attemptGetToken();
|
||||
log.debug("【{}】🤖刷新Cookie后调用官方API获取Token结果: {}", cookieId, token != null ? "成功" : "失败");
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库重新加载Cookie(避免重复代码)
|
||||
*/
|
||||
private void reloadCookieFromDatabase() {
|
||||
try {
|
||||
Optional<Cookie> cookieOpt = cookieRepository.findById(cookieId);
|
||||
if (cookieOpt.isPresent()) {
|
||||
String newCookiesStr = cookieOpt.get().getValue();
|
||||
if (!newCookiesStr.equals(this.cookiesStr)) {
|
||||
log.info("【{}】检测到数据库中的cookie已更新,重新加载cookie", cookieId);
|
||||
this.cookiesStr = newCookiesStr;
|
||||
this.cookies = parseCookies(this.cookiesStr);
|
||||
log.warn("【{}】Cookie已从数据库重新加载", cookieId);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("【{}】从数据库重新加载cookie失败,继续使用当前cookie: {}", cookieId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过浏览器刷新Cookie(避免重复代码)
|
||||
*/
|
||||
private void refreshCookieViaBrowser() {
|
||||
try {
|
||||
Map<String, String> newCookies = browserService.refreshCookies(cookieId);
|
||||
|
||||
if (newCookies != null && !newCookies.isEmpty()) {
|
||||
log.info("【{}】✅ Cookie刷新成功,重新加载...", cookieId);
|
||||
loadCookies();
|
||||
return;
|
||||
}
|
||||
|
||||
// 首次失败,尝试重建上下文
|
||||
log.warn("【{}】Cookie刷新失败,尝试强制重建持久化上下文", cookieId);
|
||||
browserService.closePersistentContext(cookieId);
|
||||
Thread.sleep(3000); // 等待资源释放
|
||||
|
||||
// 再次尝试
|
||||
newCookies = browserService.refreshCookies(cookieId);
|
||||
if (newCookies != null && !newCookies.isEmpty()) {
|
||||
log.info("【{}】重建上下文后Cookie刷新成功", cookieId);
|
||||
loadCookies();
|
||||
} else {
|
||||
log.error("【{}】❌ Cookie刷新最终失败", cookieId);
|
||||
}
|
||||
lastTokenRefreshStatus = "success";
|
||||
} catch (Exception e) {
|
||||
log.error("【{}】❌ Cookie刷新异常: {}", cookieId, e.getMessage());
|
||||
try {
|
||||
browserService.closePersistentContext(cookieId);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
lastTokenRefreshStatus = "failed";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -811,9 +828,10 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
log.info("【{}】API端点: {}", cookieId, url);
|
||||
log.info("【{}】timestamp: {}", cookieId, timestamp);
|
||||
log.info("【{}】sign: {}", cookieId, sign);
|
||||
log.info("【{}】cookies: {}", cookieId, cookiesStr);
|
||||
|
||||
// 发送POST请求
|
||||
HttpRequest request = cn.hutool.http.HttpRequest.post(url);
|
||||
HttpRequest request = HttpRequest.post(url);
|
||||
request.form("data", dataVal);
|
||||
params.forEach((k, v) -> request.form(k, v.toString()));
|
||||
request.header("cookie", cookiesStr);
|
||||
@ -848,7 +866,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
JSONObject data = resJson.getJSONObject("data");
|
||||
if (data.containsKey("accessToken")) {
|
||||
String newToken = data.getString("accessToken");
|
||||
log.info("【{}】获取到accessToken", cookieId);
|
||||
log.info("【{}】获取到accessToken: {}", cookieId, newToken);
|
||||
return newToken;
|
||||
}
|
||||
}
|
||||
@ -2480,7 +2498,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
/**
|
||||
* 处理消息主逻辑 - 完整版(阶段1:基础消息处理)
|
||||
* 对应Python的handle_message()方法
|
||||
*
|
||||
* <p>
|
||||
* 阶段1包含:
|
||||
* 1. 检查账号状态
|
||||
* 2. 发送ACK确认
|
||||
@ -2538,7 +2556,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
String orderId = extractOrderId(message);
|
||||
if (orderId != null) {
|
||||
String msgTime = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
|
||||
.format(new java.util.Date());
|
||||
.format(new java.util.Date());
|
||||
log.info("[{}] 【{}】✅ 检测到订单ID: {},开始获取订单详情", msgTime, cookieId, orderId);
|
||||
|
||||
// 提取用户ID和商品ID用于订单详情获取
|
||||
@ -2563,7 +2581,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
try {
|
||||
log.info("【{}】🔍 完整消息结构: {}", cookieId, message.toJSONString());
|
||||
String msgTime = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
|
||||
.format(new java.util.Date());
|
||||
.format(new java.util.Date());
|
||||
|
||||
// 安全地检查订单状态(红色提醒)
|
||||
String redReminder = null;
|
||||
@ -2659,7 +2677,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
|
||||
// 格式化消息时间
|
||||
String msgTime = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
|
||||
.format(new java.util.Date(createTime));
|
||||
.format(new java.util.Date(createTime));
|
||||
|
||||
// ========== 步骤9: 判断消息方向 ==========
|
||||
// 对应Python: Line 7561-7568
|
||||
@ -2675,7 +2693,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
// ========== 步骤10: 消息通知 ==========
|
||||
// 对应Python: Line 7569-7582
|
||||
log.info("[{}] 【收到】用户: {} (ID: {}), 商品({}): {}",
|
||||
msgTime, sendUserName, sendUserId, itemId, sendMessage);
|
||||
msgTime, sendUserName, sendUserId, itemId, sendMessage);
|
||||
|
||||
// 🔔 立即发送消息通知(独立于自动回复功能)
|
||||
// 检查是否为群组消息,如果是群组消息则跳过通知
|
||||
@ -2745,7 +2763,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
// 对应Python: Line 7664-7669
|
||||
if (isAutoDeliveryTrigger(sendMessage)) {
|
||||
log.info("[{}] 【{}】检测到自动发货触发消息,即使在暂停期间也继续处理: {}",
|
||||
msgTime, cookieId, sendMessage);
|
||||
msgTime, cookieId, sendMessage);
|
||||
|
||||
// 异步处理自动发货
|
||||
final String finalSendUserName = sendUserName;
|
||||
@ -2774,7 +2792,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
|
||||
if ("我已小刀,待刀成".equals(cardTitle)) {
|
||||
log.info("[{}] 【{}】【系统】检测到\"我已小刀,待刀成\",即使在暂停期间也继续处理",
|
||||
msgTime, cookieId);
|
||||
msgTime, cookieId);
|
||||
|
||||
// 检查商品是否属于当前cookies
|
||||
if (itemId != null && !itemId.startsWith("auto_")) {
|
||||
@ -2819,7 +2837,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
return;
|
||||
} else {
|
||||
log.info("[{}] 【{}】收到卡片消息,标题: {}", msgTime, cookieId,
|
||||
cardTitle != null ? cardTitle : "未知");
|
||||
cardTitle != null ? cardTitle : "未知");
|
||||
// 不是目标卡片消息,继续正常处理流程
|
||||
}
|
||||
}
|
||||
@ -2845,7 +2863,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
// 4. 延迟后调用processChatMessageReply
|
||||
|
||||
log.info("【{}】防抖回复调度已启动: chatId={}, 用户={}, 消息={}",
|
||||
cookieId, finalChatId, finalSendUserName, finalSendMessage);
|
||||
cookieId, finalChatId, finalSendUserName, finalSendMessage);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("【{}】防抖回复调度失败", cookieId, e);
|
||||
@ -2870,9 +2888,9 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
try {
|
||||
// 获取同步数据
|
||||
JSONObject syncData = messageData.getJSONObject("body")
|
||||
.getJSONObject("syncPushPackage")
|
||||
.getJSONArray("data")
|
||||
.getJSONObject(0);
|
||||
.getJSONObject("syncPushPackage")
|
||||
.getJSONArray("data")
|
||||
.getJSONObject(0);
|
||||
|
||||
// 检查是否有必要的字段
|
||||
if (!syncData.containsKey("data")) {
|
||||
@ -2898,12 +2916,12 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
// 处理系统引导消息
|
||||
if (content.containsKey("sessionArouse")) {
|
||||
String msgTime = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
|
||||
.format(new java.util.Date());
|
||||
.format(new java.util.Date());
|
||||
log.info("[{}] 【{}】【系统】小闲鱼智能提示(已跳过)", msgTime, cookieId);
|
||||
return null;
|
||||
} else if (content.containsKey("contentType")) {
|
||||
String msgTime = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
|
||||
.format(new java.util.Date());
|
||||
.format(new java.util.Date());
|
||||
log.warn("[{}] 【{}】【系统】其他类型消息(已跳过)", msgTime, cookieId);
|
||||
return null;
|
||||
}
|
||||
@ -2949,7 +2967,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
if (message1Obj.containsKey("10") && message1Obj.get("10") instanceof JSONObject) {
|
||||
JSONObject message10 = message1Obj.getJSONObject("10");
|
||||
return message10.getString("senderUserId") != null ?
|
||||
message10.getString("senderUserId") : "unknown_user";
|
||||
message10.getString("senderUserId") : "unknown_user";
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
||||
@ -67,7 +67,7 @@ public class CaptchaHandler {
|
||||
dragSlider(sliderElement, distance, cookieId);
|
||||
|
||||
// 检查是否成功
|
||||
Thread.sleep(10000);
|
||||
Thread.sleep(5000);
|
||||
boolean success = checkSuccess(verificationUrl, cookieId);
|
||||
|
||||
if (success) {
|
||||
@ -249,7 +249,7 @@ public class CaptchaHandler {
|
||||
double randomX1 = 200 + Math.random() * 400; // 200-600px范围
|
||||
double randomY1 = 100 + Math.random() * 200; // 100-300px范围
|
||||
page.mouse().move(randomX1, randomY1);
|
||||
page.mouse().click(randomX1, randomY1);
|
||||
page.mouse().click(randomX1, randomY1, new Mouse.ClickOptions().setClickCount(1));
|
||||
Thread.sleep(100 + (long)(Math.random() * 200));
|
||||
|
||||
// 2. 再移动到接近滑块的位置(但不是精确位置)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user