Commit 2a026455 authored by 王舵's avatar 王舵

feat: 监听browser连接断开,重新启动 browser

parent a8007006
...@@ -14,36 +14,35 @@ import java.util.concurrent.*; ...@@ -14,36 +14,35 @@ import java.util.concurrent.*;
@Slf4j @Slf4j
@Component // Spring默认单例模式 @Component // Spring默认单例模式
public class PlaywrightManagerImpl implements PlaywrightManager { public class PlaywrightManagerImpl implements PlaywrightManager {
// 共享的Playwright实例 // 共享的Playwright实例
private volatile Playwright playwright; private volatile Playwright playwright;
// 共享的浏览器实例 // 共享的浏览器实例
private volatile Browser browser; private volatile Browser browser;
// 用户浏览器上下文映射表(用户ID -> BrowserContext) // 用户浏览器上下文映射表(用户ID -> BrowserContext)
private final ConcurrentMap<String, BrowserContext> userContexts = new ConcurrentHashMap<>(); private final ConcurrentMap<String, BrowserContext> userContexts = new ConcurrentHashMap<>();
// 用户上下文创建时间映射表(用于超时清理) // 用户上下文创建时间映射表(用于超时清理)
private final ConcurrentMap<String, Long> contextCreationTimes = new ConcurrentHashMap<>(); private final ConcurrentMap<String, Long> contextCreationTimes = new ConcurrentHashMap<>();
// 用户上下文超时时间(毫秒),默认30分钟 // 用户上下文超时时间(毫秒),默认30分钟
private static final long CONTEXT_TIMEOUT = 30 * 60 * 1000; private static final long CONTEXT_TIMEOUT = 30 * 60 * 1000;
// 清理任务调度器 // 清理任务调度器
private final ScheduledExecutorService cleanupScheduler = private final ScheduledExecutorService cleanupScheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Executors.newSingleThreadScheduledExecutor(r -> { Thread t = new Thread(r, "PlaywrightCleanupScheduler");
Thread t = new Thread(r, "PlaywrightCleanupScheduler"); t.setDaemon(true); // 设置为守护线程
t.setDaemon(true); // 设置为守护线程 return t;
return t; });
});
// 标记是否已经初始化 // 标记是否已经初始化
private volatile boolean initialized = false; private volatile boolean initialized = false;
// 用于同步初始化过程 // 用于同步初始化过程
private final Object initLock = new Object(); private final Object initLock = new Object();
/** /**
* 延迟初始化Playwright和浏览器实例 * 延迟初始化Playwright和浏览器实例
*/ */
...@@ -53,10 +52,10 @@ public class PlaywrightManagerImpl implements PlaywrightManager { ...@@ -53,10 +52,10 @@ public class PlaywrightManagerImpl implements PlaywrightManager {
if (!initialized) { if (!initialized) {
try { try {
log.info("正在初始化Playwright管理器..."); log.info("正在初始化Playwright管理器...");
// 创建Playwright实例 // 创建Playwright实例
this.playwright = Playwright.create(); this.playwright = Playwright.create();
// 启动Chrome浏览器,无头模式 // 启动Chrome浏览器,无头模式
this.browser = playwright.chromium().launch(new BrowserType.LaunchOptions() this.browser = playwright.chromium().launch(new BrowserType.LaunchOptions()
.setHeadless(true) .setHeadless(true)
...@@ -65,11 +64,23 @@ public class PlaywrightManagerImpl implements PlaywrightManager { ...@@ -65,11 +64,23 @@ public class PlaywrightManagerImpl implements PlaywrightManager {
"--disable-dev-shm-usage", "--disable-dev-shm-usage",
"--disable-gpu", "--disable-gpu",
"--remote-allow-origins=*"))); "--remote-allow-origins=*")));
this.browser.onDisconnected((browser) -> {
log.info("浏览器实例已断开连接");
this.browser.close();
this.browser = playwright.chromium().launch(new BrowserType.LaunchOptions()
.setHeadless(true)
.setArgs(java.util.Arrays.asList(
"--no-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu",
"--remote-allow-origins=*")));
});
// 每5分钟检查一次超时的用户上下文 // 每5分钟检查一次超时的用户上下文
cleanupScheduler.scheduleAtFixedRate(this::cleanupExpiredContexts, cleanupScheduler.scheduleAtFixedRate(this::cleanupExpiredContexts,
5, 3600, TimeUnit.MINUTES); 5, 3600, TimeUnit.MINUTES);
this.initialized = true; this.initialized = true;
log.info("Playwright管理器初始化成功"); log.info("Playwright管理器初始化成功");
} catch (Exception e) { } catch (Exception e) {
...@@ -80,15 +91,15 @@ public class PlaywrightManagerImpl implements PlaywrightManager { ...@@ -80,15 +91,15 @@ public class PlaywrightManagerImpl implements PlaywrightManager {
} }
} }
} }
// 移除@PostConstruct注解,避免在Spring初始化时自动调用 // 移除@PostConstruct注解,避免在Spring初始化时自动调用
/* /*
@PostConstruct * @PostConstruct
public void initialize() { * public void initialize() {
lazyInitialize(); * lazyInitialize();
} * }
*/ */
@Override @Override
public Playwright getPlaywright() { public Playwright getPlaywright() {
lazyInitialize(); lazyInitialize();
...@@ -97,7 +108,7 @@ public class PlaywrightManagerImpl implements PlaywrightManager { ...@@ -97,7 +108,7 @@ public class PlaywrightManagerImpl implements PlaywrightManager {
} }
return playwright; return playwright;
} }
@Override @Override
public Browser getBrowser() { public Browser getBrowser() {
lazyInitialize(); lazyInitialize();
...@@ -106,30 +117,33 @@ public class PlaywrightManagerImpl implements PlaywrightManager { ...@@ -106,30 +117,33 @@ public class PlaywrightManagerImpl implements PlaywrightManager {
} }
return browser; return browser;
} }
@Override @Override
public BrowserContext getUserContext(String userId) { public BrowserContext getUserContext(String userId) {
lazyInitialize(); lazyInitialize();
Browser.NewContextOptions options = new Browser.NewContextOptions() Browser.NewContextOptions options = new Browser.NewContextOptions()
.setViewportSize(1920, 1080) // 设置视口大小为全高清分辨率,适用于Windows 11桌面环境 .setViewportSize(1920, 1080) // 设置视口大小为全高清分辨率,适用于Windows 11桌面环境
.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"); // 设置用户代理为Windows 11 Chrome浏览器 .setUserAgent(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"); // 设置用户代理为Windows
// 11
// Chrome浏览器
return getUserContext(userId, options); return getUserContext(userId, options);
} }
@Override @Override
public BrowserContext getUserContext(String userId, Browser.NewContextOptions options) { public BrowserContext getUserContext(String userId, Browser.NewContextOptions options) {
lazyInitialize(); lazyInitialize();
if (userId == null || userId.isEmpty()) { if (userId == null || userId.isEmpty()) {
throw new IllegalArgumentException("User ID cannot be null or empty"); throw new IllegalArgumentException("User ID cannot be null or empty");
} }
if (options == null) { if (options == null) {
options = new Browser.NewContextOptions(); options = new Browser.NewContextOptions();
} }
// 尝试从缓存中获取已存在的上下文 // 尝试从缓存中获取已存在的上下文
BrowserContext context = userContexts.get(userId); BrowserContext context = userContexts.get(userId);
// 如果上下文不存在或已关闭,则创建新的 // 如果上下文不存在或已关闭,则创建新的
if (context == null || isContextClosed(context)) { if (context == null || isContextClosed(context)) {
try { try {
...@@ -142,19 +156,19 @@ public class PlaywrightManagerImpl implements PlaywrightManager { ...@@ -142,19 +156,19 @@ public class PlaywrightManagerImpl implements PlaywrightManager {
throw new RuntimeException("Failed to create browser context for user: " + userId, e); throw new RuntimeException("Failed to create browser context for user: " + userId, e);
} }
} }
return context; return context;
} }
@Override @Override
public void releaseUserContext(String userId) { public void releaseUserContext(String userId) {
if (userId == null || userId.isEmpty()) { if (userId == null || userId.isEmpty()) {
return; return;
} }
BrowserContext context = userContexts.remove(userId); BrowserContext context = userContexts.remove(userId);
contextCreationTimes.remove(userId); contextCreationTimes.remove(userId);
if (context != null) { if (context != null) {
try { try {
context.close(); context.close();
...@@ -164,7 +178,7 @@ public class PlaywrightManagerImpl implements PlaywrightManager { ...@@ -164,7 +178,7 @@ public class PlaywrightManagerImpl implements PlaywrightManager {
} }
} }
} }
/** /**
* 检查BrowserContext是否已关闭 * 检查BrowserContext是否已关闭
* *
...@@ -182,14 +196,14 @@ public class PlaywrightManagerImpl implements PlaywrightManager { ...@@ -182,14 +196,14 @@ public class PlaywrightManagerImpl implements PlaywrightManager {
return true; return true;
} }
} }
/** /**
* 清理过期的用户上下文 * 清理过期的用户上下文
*/ */
private void cleanupExpiredContexts() { private void cleanupExpiredContexts() {
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
long expiredThreshold = currentTime - CONTEXT_TIMEOUT; long expiredThreshold = currentTime - CONTEXT_TIMEOUT;
for (String userId : contextCreationTimes.keySet()) { for (String userId : contextCreationTimes.keySet()) {
Long creationTime = contextCreationTimes.get(userId); Long creationTime = contextCreationTimes.get(userId);
if (creationTime != null && creationTime < expiredThreshold) { if (creationTime != null && creationTime < expiredThreshold) {
...@@ -198,7 +212,7 @@ public class PlaywrightManagerImpl implements PlaywrightManager { ...@@ -198,7 +212,7 @@ public class PlaywrightManagerImpl implements PlaywrightManager {
} }
} }
} }
/** /**
* 销毁所有资源 * 销毁所有资源
*/ */
...@@ -206,7 +220,7 @@ public class PlaywrightManagerImpl implements PlaywrightManager { ...@@ -206,7 +220,7 @@ public class PlaywrightManagerImpl implements PlaywrightManager {
@Override @Override
public void destroy() { public void destroy() {
log.info("开始销毁Playwright管理器资源..."); log.info("开始销毁Playwright管理器资源...");
try { try {
// 关闭清理任务调度器 // 关闭清理任务调度器
cleanupScheduler.shutdown(); cleanupScheduler.shutdown();
...@@ -216,12 +230,12 @@ public class PlaywrightManagerImpl implements PlaywrightManager { ...@@ -216,12 +230,12 @@ public class PlaywrightManagerImpl implements PlaywrightManager {
} catch (Exception e) { } catch (Exception e) {
log.warn("关闭清理任务调度器时发生异常", e); log.warn("关闭清理任务调度器时发生异常", e);
} }
// 关闭所有用户上下文 // 关闭所有用户上下文
for (String userId : userContexts.keySet()) { for (String userId : userContexts.keySet()) {
releaseUserContext(userId); releaseUserContext(userId);
} }
// 关闭浏览器 // 关闭浏览器
try { try {
if (browser != null && browser.isConnected()) { if (browser != null && browser.isConnected()) {
...@@ -231,7 +245,7 @@ public class PlaywrightManagerImpl implements PlaywrightManager { ...@@ -231,7 +245,7 @@ public class PlaywrightManagerImpl implements PlaywrightManager {
} catch (Exception e) { } catch (Exception e) {
log.warn("关闭浏览器实例时发生异常", e); log.warn("关闭浏览器实例时发生异常", e);
} }
// 关闭Playwright // 关闭Playwright
try { try {
if (playwright != null) { if (playwright != null) {
...@@ -241,7 +255,7 @@ public class PlaywrightManagerImpl implements PlaywrightManager { ...@@ -241,7 +255,7 @@ public class PlaywrightManagerImpl implements PlaywrightManager {
} catch (Exception e) { } catch (Exception e) {
log.warn("关闭Playwright实例时发生异常", e); log.warn("关闭Playwright实例时发生异常", e);
} }
log.info("Playwright管理器资源已全部销毁"); log.info("Playwright管理器资源已全部销毁");
} }
} }
\ No newline at end of file
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
spring: spring:
# 开发环境数据源配置 # 开发环境数据源配置
datasource: datasource:
url: jdbc:h2:file:./data/hiagent_dev_db;DB_CLOSE_ON_EXIT=FALSE url: jdbc:mysql://${DB_HOST:127.0.0.1}:3306/hiagent?allowMultiQueries=true&allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Shanghai
driver-class-name: org.h2.Driver driver-class-name: ${DB_DRIVER:com.mysql.cj.jdbc.Driver}
username: sa username: ${DB_NAME:root}
password: sa password: ${DB_PASSWORD:password}
# 开发环境JPA配置 # 开发环境JPA配置
jpa: jpa:
...@@ -21,7 +21,7 @@ spring: ...@@ -21,7 +21,7 @@ spring:
init: init:
schema-locations: classpath:schema.sql schema-locations: classpath:schema.sql
data-locations: classpath:data.sql data-locations: classpath:data.sql
mode: always # 总是执行创建表和数据脚本,实现重新初始化 mode: never # 总是执行创建表和数据脚本,实现重新初始化
# 开启H2控制台 # 开启H2控制台
h2: h2:
......
/*
* @Date: 2025-12-29 14:42:27
* @LastEditors: wangduo3 wangduo3@hisense.com
* @LastEditTime: 2025-12-29 16:56:52
* @FilePath: /pangea-agent/frontend/vite.config.ts
*/
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue"; import vue from "@vitejs/plugin-vue";
import path from "path"; import path from "path";
...@@ -30,14 +36,14 @@ export default defineConfig({ ...@@ -30,14 +36,14 @@ export default defineConfig({
}, },
proxy: { proxy: {
"/api": { "/api": {
// target: "http://localhost:8080", target: "http://localhost:8080",
target: "http://agent-backend.clouddev.hisense.com", // target: "http://agent-backend.clouddev.hisense.com",
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, "/api"), rewrite: (path) => path.replace(/^\/api/, "/api"),
}, },
"/ws": { "/ws": {
// target: "http://localhost:8080", target: "http://localhost:8080",
target: "http://agent-backend.clouddev.hisense.com", // target: "http://agent-backend.clouddev.hisense.com",
ws: true, // 启用WebSocket代理 ws: true, // 启用WebSocket代理
changeOrigin: true, changeOrigin: true,
}, },
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment