@aiscene/aiserver 1.8.1 → 1.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/callback.d.ts.map +1 -1
- package/dist/api/callback.js +5 -0
- package/dist/api/callback.js.map +1 -1
- package/dist/api/device-api.d.ts.map +1 -1
- package/dist/api/device-api.js +9 -2
- package/dist/api/device-api.js.map +1 -1
- package/dist/api/task-api.d.ts.map +1 -1
- package/dist/api/task-api.js +6 -1
- package/dist/api/task-api.js.map +1 -1
- package/dist/auth/browser-auth.d.ts +4 -0
- package/dist/auth/browser-auth.d.ts.map +1 -0
- package/dist/auth/browser-auth.js +59 -0
- package/dist/auth/browser-auth.js.map +1 -0
- package/dist/auth/node-auth.d.ts +27 -0
- package/dist/auth/node-auth.d.ts.map +1 -0
- package/dist/auth/node-auth.js +132 -0
- package/dist/auth/node-auth.js.map +1 -0
- package/dist/config/cli.d.ts +12 -0
- package/dist/config/cli.d.ts.map +1 -1
- package/dist/config/cli.js +59 -2
- package/dist/config/cli.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +32 -10
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +3 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/core/types.d.ts +15 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/debug/types.d.ts +2 -0
- package/dist/debug/types.d.ts.map +1 -1
- package/dist/debug/web-screencast.d.ts.map +1 -1
- package/dist/debug/web-screencast.js +0 -6
- package/dist/debug/web-screencast.js.map +1 -1
- package/dist/debug/websocket-server.d.ts +6 -3
- package/dist/debug/websocket-server.d.ts.map +1 -1
- package/dist/debug/websocket-server.js +137 -103
- package/dist/debug/websocket-server.js.map +1 -1
- package/dist/device/heartbeat.d.ts.map +1 -1
- package/dist/device/heartbeat.js +9 -2
- package/dist/device/heartbeat.js.map +1 -1
- package/dist/executor/action-executor.d.ts.map +1 -1
- package/dist/executor/action-executor.js +6 -0
- package/dist/executor/action-executor.js.map +1 -1
- package/dist/executor/android-executor.d.ts +3 -0
- package/dist/executor/android-executor.d.ts.map +1 -1
- package/dist/executor/android-executor.js +51 -0
- package/dist/executor/android-executor.js.map +1 -1
- package/dist/executor/cli-executor.d.ts +3 -1
- package/dist/executor/cli-executor.d.ts.map +1 -1
- package/dist/executor/cli-executor.js +59 -77
- package/dist/executor/cli-executor.js.map +1 -1
- package/dist/executor/ios-executor.d.ts.map +1 -1
- package/dist/executor/ios-executor.js +4 -0
- package/dist/executor/ios-executor.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +79 -27
- package/dist/index.js.map +1 -1
- package/dist/login/android-login-context.d.ts +1 -0
- package/dist/login/android-login-context.d.ts.map +1 -1
- package/dist/login/android-login-context.js +79 -16
- package/dist/login/android-login-context.js.map +1 -1
- package/dist/node/service.d.ts.map +1 -1
- package/dist/node/service.js +13 -3
- package/dist/node/service.js.map +1 -1
- package/dist/task/execution-profile.d.ts +32 -0
- package/dist/task/execution-profile.d.ts.map +1 -0
- package/dist/task/execution-profile.js +227 -0
- package/dist/task/execution-profile.js.map +1 -0
- package/dist/task/scheduler.d.ts +1 -0
- package/dist/task/scheduler.d.ts.map +1 -1
- package/dist/task/scheduler.js +28 -27
- package/dist/task/scheduler.js.map +1 -1
- package/dist/task/types.js +1 -0
- package/dist/task/types.js.map +1 -1
- package/dist/web/server.d.ts +6 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +141 -9
- package/dist/web/server.js.map +1 -1
- package/package.json +2 -2
|
@@ -478,10 +478,10 @@ export class DebugWebSocketServer {
|
|
|
478
478
|
}
|
|
479
479
|
}
|
|
480
480
|
/**
|
|
481
|
-
* Web
|
|
481
|
+
* Web 调试主进程内嵌执行
|
|
482
482
|
* - 首次请求:懒启动浏览器 + PlaywrightAgent + 自动投屏
|
|
483
483
|
* - 复用请求:跳过初始化,直接执行 naturalLanguage
|
|
484
|
-
* -
|
|
484
|
+
* - 执行完成后自动关闭浏览器,释放资源
|
|
485
485
|
*/
|
|
486
486
|
async handleStartDebugWeb(ws, request) {
|
|
487
487
|
// 1) 决定 sessionId(客户端传了就用以复用)
|
|
@@ -597,11 +597,19 @@ export class DebugWebSocketServer {
|
|
|
597
597
|
const contextOptions = {
|
|
598
598
|
viewport: request.viewport || { width: 1280, height: 800 },
|
|
599
599
|
};
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
context = acquired.context;
|
|
600
|
+
browser = await playwright.chromium.launch({ headless: false });
|
|
601
|
+
context = await browser.newContext(contextOptions);
|
|
603
602
|
page = await context.newPage();
|
|
604
|
-
releaseBrowser =
|
|
603
|
+
releaseBrowser = async () => {
|
|
604
|
+
try {
|
|
605
|
+
await context.close();
|
|
606
|
+
}
|
|
607
|
+
catch { /* ignore */ }
|
|
608
|
+
try {
|
|
609
|
+
await browser.close();
|
|
610
|
+
}
|
|
611
|
+
catch { /* ignore */ }
|
|
612
|
+
};
|
|
605
613
|
}
|
|
606
614
|
session.browser = browser;
|
|
607
615
|
session.context = context;
|
|
@@ -636,12 +644,6 @@ export class DebugWebSocketServer {
|
|
|
636
644
|
page,
|
|
637
645
|
logPrefix: `[WebDebug ${sessionId}]`,
|
|
638
646
|
});
|
|
639
|
-
// 兼容旧版表单登录配置(仅首次启动时执行)
|
|
640
|
-
if (request.loginConfig && request.loginConfig.loginUrl
|
|
641
|
-
&& request.loginConfig.loginUsername && request.loginConfig.loginPassword) {
|
|
642
|
-
const loginConfig = { ...request.loginConfig, mobileMode: request.mobileMode || false };
|
|
643
|
-
await this.performWebLogin(page, loginConfig);
|
|
644
|
-
}
|
|
645
647
|
}
|
|
646
648
|
else {
|
|
647
649
|
logger.info(`[WebDebug] Reusing existing session: ${sessionId}`);
|
|
@@ -660,7 +662,7 @@ export class DebugWebSocketServer {
|
|
|
660
662
|
// 5) 若指定了 url,则导航(首启或复用,传了就 goto)
|
|
661
663
|
if (request.url) {
|
|
662
664
|
try {
|
|
663
|
-
await
|
|
665
|
+
await this.safeWebGoto(page, request.url, `[WebDebug] Navigate to ${request.url}`);
|
|
664
666
|
logger.info(`[WebDebug] Navigated to: ${request.url}`);
|
|
665
667
|
}
|
|
666
668
|
catch (gotoErr) {
|
|
@@ -697,7 +699,7 @@ export class DebugWebSocketServer {
|
|
|
697
699
|
logger.info(`[WebDebug] report uploaded: reportUrl=${reportUrl || 'null'}, reportFile=${reportFile}`);
|
|
698
700
|
const success = !execError;
|
|
699
701
|
sessionManager.updateStatus(sessionId, success ? 'completed' : 'failed');
|
|
700
|
-
//
|
|
702
|
+
// 执行完成后自动关闭浏览器,释放资源
|
|
701
703
|
this.sendMessage(ws, {
|
|
702
704
|
type: 'debug_completed',
|
|
703
705
|
sessionId,
|
|
@@ -705,6 +707,12 @@ export class DebugWebSocketServer {
|
|
|
705
707
|
reportUrl,
|
|
706
708
|
errorMessage: execError,
|
|
707
709
|
});
|
|
710
|
+
try {
|
|
711
|
+
await this.closeWebSession(sessionId);
|
|
712
|
+
}
|
|
713
|
+
catch (closeErr) {
|
|
714
|
+
logger.warn(`[WebDebug] Auto close browser failed: ${closeErr.message}`);
|
|
715
|
+
}
|
|
708
716
|
}
|
|
709
717
|
catch (error) {
|
|
710
718
|
const msg = error.message;
|
|
@@ -712,7 +720,13 @@ export class DebugWebSocketServer {
|
|
|
712
720
|
sessionManager.updateStatus(sessionId, 'failed');
|
|
713
721
|
this.sendMessage(ws, { type: 'debug_error', sessionId, error: msg });
|
|
714
722
|
this.sendMessage(ws, { type: 'debug_completed', sessionId, success: false, errorMessage: msg });
|
|
715
|
-
//
|
|
723
|
+
// 致命错误也自动关闭浏览器
|
|
724
|
+
try {
|
|
725
|
+
await this.closeWebSession(sessionId);
|
|
726
|
+
}
|
|
727
|
+
catch (closeErr) {
|
|
728
|
+
logger.warn(`[WebDebug] Auto close browser (fatal) failed: ${closeErr.message}`);
|
|
729
|
+
}
|
|
716
730
|
}
|
|
717
731
|
}
|
|
718
732
|
/**
|
|
@@ -792,21 +806,11 @@ export class DebugWebSocketServer {
|
|
|
792
806
|
skipAppRestart: request.skipAppRestart,
|
|
793
807
|
mobileMode: request.mobileMode,
|
|
794
808
|
deviceName: request.deviceName,
|
|
809
|
+
loginContext: request.loginContext,
|
|
795
810
|
// 环境代理配置
|
|
796
811
|
proxyPort: request.proxyPort,
|
|
797
812
|
// 公司代理平台账号
|
|
798
813
|
proxyAccount: request.proxyAccount,
|
|
799
|
-
// 添加登录配置
|
|
800
|
-
...(request.loginConfig && {
|
|
801
|
-
loginUrl: request.loginConfig.loginUrl,
|
|
802
|
-
loginUsername: request.loginConfig.loginUsername,
|
|
803
|
-
loginPassword: request.loginConfig.loginPassword,
|
|
804
|
-
usernameSelector: request.loginConfig.usernameSelector,
|
|
805
|
-
passwordSelector: request.loginConfig.passwordSelector,
|
|
806
|
-
loginButtonSelector: request.loginConfig.loginButtonSelector,
|
|
807
|
-
loginModeSelector: request.loginConfig.loginModeSelector,
|
|
808
|
-
agreementSelector: request.loginConfig.agreementSelector,
|
|
809
|
-
}),
|
|
810
814
|
};
|
|
811
815
|
// 注意:proxy-service.jd.com 的规则由前端(浏览器端)设置,后端只负责设备代理 + 抓包
|
|
812
816
|
try {
|
|
@@ -919,21 +923,11 @@ export class DebugWebSocketServer {
|
|
|
919
923
|
nodeId: this.config.task.nodeId,
|
|
920
924
|
modelConfig: fullModelConfig,
|
|
921
925
|
skipAppRestart: request.skipAppRestart,
|
|
926
|
+
loginContext: request.loginContext,
|
|
922
927
|
// 环境代理配置
|
|
923
928
|
proxyPort: request.proxyPort,
|
|
924
929
|
// 公司代理平台账号
|
|
925
930
|
proxyAccount: request.proxyAccount,
|
|
926
|
-
// 添加登录配置
|
|
927
|
-
...(request.loginConfig && {
|
|
928
|
-
loginUrl: request.loginConfig.loginUrl,
|
|
929
|
-
loginUsername: request.loginConfig.loginUsername,
|
|
930
|
-
loginPassword: request.loginConfig.loginPassword,
|
|
931
|
-
usernameSelector: request.loginConfig.usernameSelector,
|
|
932
|
-
passwordSelector: request.loginConfig.passwordSelector,
|
|
933
|
-
loginButtonSelector: request.loginConfig.loginButtonSelector,
|
|
934
|
-
loginModeSelector: request.loginConfig.loginModeSelector,
|
|
935
|
-
agreementSelector: request.loginConfig.agreementSelector,
|
|
936
|
-
}),
|
|
937
931
|
};
|
|
938
932
|
// 注意:proxy-service.jd.com 的规则由前端(浏览器端)设置,后端只负责设备代理 + 抓包
|
|
939
933
|
// 始终启动请求抓包
|
|
@@ -1514,58 +1508,79 @@ export class DebugWebSocketServer {
|
|
|
1514
1508
|
}
|
|
1515
1509
|
}
|
|
1516
1510
|
// ==================== Execute Web Action (Playwright + Midscene, with screencast) ====================
|
|
1517
|
-
|
|
1518
|
-
if (!
|
|
1519
|
-
|
|
1520
|
-
|
|
1511
|
+
normalizeWebUrl(value) {
|
|
1512
|
+
if (!value)
|
|
1513
|
+
return '';
|
|
1514
|
+
try {
|
|
1515
|
+
const parsed = new URL(value);
|
|
1516
|
+
parsed.hash = '';
|
|
1517
|
+
return parsed.toString().replace(/\/$/, '');
|
|
1521
1518
|
}
|
|
1522
|
-
|
|
1519
|
+
catch {
|
|
1520
|
+
return String(value).split('#')[0].replace(/\/$/, '');
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
getCurrentWebUrl(page) {
|
|
1523
1524
|
try {
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
await page.
|
|
1546
|
-
|
|
1547
|
-
const loginButtonSelector = loginConfig.loginButtonSelector || 'a.J_ping:has-text("同意协议并登录")';
|
|
1548
|
-
logger.info(`Clicking login button with selector: ${loginButtonSelector}`);
|
|
1549
|
-
await page.click(loginButtonSelector);
|
|
1550
|
-
// Wait for login to complete
|
|
1551
|
-
await new Promise(resolve => setTimeout(resolve, loginConfig.mobileMode ? 8000 : 5000));
|
|
1552
|
-
// Check if login was successful
|
|
1553
|
-
try {
|
|
1554
|
-
const currentUrl = page.url();
|
|
1555
|
-
if (currentUrl !== loginConfig.loginUrl && !currentUrl.includes('login')) {
|
|
1556
|
-
logger.info('Login appears to be successful');
|
|
1557
|
-
}
|
|
1558
|
-
else {
|
|
1559
|
-
logger.warn('Still on login page, login may have failed');
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1562
|
-
catch (e) {
|
|
1563
|
-
logger.warn('Could not verify login status');
|
|
1564
|
-
}
|
|
1525
|
+
return typeof page?.url === 'function' ? page.url() : '';
|
|
1526
|
+
}
|
|
1527
|
+
catch {
|
|
1528
|
+
return '';
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
async waitForWebPageStable(page) {
|
|
1532
|
+
await page.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(() => undefined);
|
|
1533
|
+
await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => undefined);
|
|
1534
|
+
}
|
|
1535
|
+
async safeWebGoto(page, url, label) {
|
|
1536
|
+
const targetUrl = this.normalizeWebUrl(url);
|
|
1537
|
+
const currentUrl = this.normalizeWebUrl(this.getCurrentWebUrl(page));
|
|
1538
|
+
if (currentUrl && targetUrl && currentUrl === targetUrl) {
|
|
1539
|
+
logger.info(`${label}: already at target url, skip duplicate goto`);
|
|
1540
|
+
await this.waitForWebPageStable(page);
|
|
1541
|
+
return null;
|
|
1542
|
+
}
|
|
1543
|
+
try {
|
|
1544
|
+
logger.info(`${label}: ${url}`);
|
|
1545
|
+
const result = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
|
|
1546
|
+
await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => undefined);
|
|
1547
|
+
return result;
|
|
1565
1548
|
}
|
|
1566
1549
|
catch (error) {
|
|
1567
|
-
|
|
1568
|
-
|
|
1550
|
+
const message = error.message || '';
|
|
1551
|
+
const latestUrl = this.normalizeWebUrl(this.getCurrentWebUrl(page));
|
|
1552
|
+
// 导航被中断/中止时,检查页面是否已到达目标 URL,若已到达则继续执行
|
|
1553
|
+
const isAborted = message.includes('ERR_ABORTED') || message.includes('is interrupted by another navigation');
|
|
1554
|
+
if (isAborted) {
|
|
1555
|
+
// 短暂等待让页面重定向/跳转完成
|
|
1556
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
1557
|
+
const settledUrl = this.normalizeWebUrl(this.getCurrentWebUrl(page));
|
|
1558
|
+
if (settledUrl && settledUrl === targetUrl) {
|
|
1559
|
+
logger.warn(`${label}: navigation aborted but page reached target url, continue`);
|
|
1560
|
+
await this.waitForWebPageStable(page);
|
|
1561
|
+
return null;
|
|
1562
|
+
}
|
|
1563
|
+
// URL 不完全匹配但导航被中止,可能是目标页面发生了重定向(登录回调等),
|
|
1564
|
+
// 尝试重新导航一次
|
|
1565
|
+
logger.warn(`${label}: navigation aborted, current=${settledUrl}, retrying goto`);
|
|
1566
|
+
try {
|
|
1567
|
+
const retryResult = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
1568
|
+
await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => undefined);
|
|
1569
|
+
return retryResult;
|
|
1570
|
+
}
|
|
1571
|
+
catch (retryError) {
|
|
1572
|
+
const retryMessage = retryError.message || '';
|
|
1573
|
+
const finalUrl = this.normalizeWebUrl(this.getCurrentWebUrl(page));
|
|
1574
|
+
// 重试后仍报错,但只要页面不在 about:blank 就继续
|
|
1575
|
+
if (finalUrl && finalUrl !== 'about:blank') {
|
|
1576
|
+
logger.warn(`${label}: retry goto also failed (${retryMessage.substring(0, 80)}), but page has content at ${finalUrl}, continue`);
|
|
1577
|
+
await this.waitForWebPageStable(page);
|
|
1578
|
+
return null;
|
|
1579
|
+
}
|
|
1580
|
+
throw retryError;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
throw error;
|
|
1569
1584
|
}
|
|
1570
1585
|
}
|
|
1571
1586
|
async handleExecuteWebAction(ws, request) {
|
|
@@ -1642,15 +1657,23 @@ export class DebugWebSocketServer {
|
|
|
1642
1657
|
};
|
|
1643
1658
|
}
|
|
1644
1659
|
else {
|
|
1645
|
-
//
|
|
1660
|
+
// 桌面模式:每次执行独立启动 Chromium,结束后真正关闭,避免残留 Chrome 进程。
|
|
1646
1661
|
const contextOptions = {
|
|
1647
1662
|
viewport: request.viewport || { width: 1280, height: 800 },
|
|
1648
1663
|
};
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
context = acquired.context;
|
|
1664
|
+
browser = await playwright.chromium.launch({ headless: false });
|
|
1665
|
+
context = await browser.newContext(contextOptions);
|
|
1652
1666
|
page = await context.newPage();
|
|
1653
|
-
releaseBrowser =
|
|
1667
|
+
releaseBrowser = async () => {
|
|
1668
|
+
try {
|
|
1669
|
+
await context.close();
|
|
1670
|
+
}
|
|
1671
|
+
catch { /* ignore */ }
|
|
1672
|
+
try {
|
|
1673
|
+
await browser.close();
|
|
1674
|
+
}
|
|
1675
|
+
catch { /* ignore */ }
|
|
1676
|
+
};
|
|
1654
1677
|
}
|
|
1655
1678
|
// mobileMode 下注入脚本,将 touchstart/touchmove/wheel 事件监听器设为 passive:false
|
|
1656
1679
|
// 修复部分移动端网页在 passive listener 中调用 preventDefault 导致点击失效的问题
|
|
@@ -1714,20 +1737,25 @@ export class DebugWebSocketServer {
|
|
|
1714
1737
|
// 取出 agent / page
|
|
1715
1738
|
const agent = session.webAgent;
|
|
1716
1739
|
const page = session.page;
|
|
1740
|
+
// 执行前检查浏览器和页面是否仍然存活,防止在已崩溃的浏览器上操作导致不明错误
|
|
1741
|
+
const browserRef = session.browser;
|
|
1742
|
+
if (browserRef && typeof browserRef.isConnected === 'function' && !browserRef.isConnected()) {
|
|
1743
|
+
throw new Error('Browser has been disconnected before action execution. The browser process may have crashed or been killed.');
|
|
1744
|
+
}
|
|
1745
|
+
if (page && typeof page.isClosed === 'function' && page.isClosed()) {
|
|
1746
|
+
throw new Error('Page has been closed before action execution. The browser context may have been terminated.');
|
|
1747
|
+
}
|
|
1717
1748
|
// 刷新 web session 活跃时间,避免 TTL 误杀
|
|
1718
1749
|
this.touchWebSession(sessionId);
|
|
1750
|
+
// 标记 web action 正在执行中,防止 TTL GC 或并发路径误关浏览器
|
|
1751
|
+
session.webActionRunning = true;
|
|
1719
1752
|
await executeWebLogin({
|
|
1720
1753
|
source: request,
|
|
1721
1754
|
page,
|
|
1722
1755
|
logPrefix: `[WebAction ${sessionId}]`,
|
|
1723
1756
|
});
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
const loginConfig = {
|
|
1727
|
-
...request.loginConfig,
|
|
1728
|
-
mobileMode: request.mobileMode || false
|
|
1729
|
-
};
|
|
1730
|
-
await this.performWebLogin(page, loginConfig);
|
|
1757
|
+
if (request.url && request.actionType !== 'goto') {
|
|
1758
|
+
await this.safeWebGoto(page, request.url, `[WebAction] Navigate before action`);
|
|
1731
1759
|
}
|
|
1732
1760
|
// ===== 周期 dump(与移动端 handleExecuteAction 行为一致) =====
|
|
1733
1761
|
const sendDump = (dump) => {
|
|
@@ -1765,6 +1793,8 @@ export class DebugWebSocketServer {
|
|
|
1765
1793
|
const intervalDump = agent.dumpDataString({ inlineScreenshots: false });
|
|
1766
1794
|
sendDump(intervalDump);
|
|
1767
1795
|
}
|
|
1796
|
+
// 每次 dump 周期刷新活跃时间,防止长时间 aiAct 操作期间被 TTL GC 误杀
|
|
1797
|
+
this.touchWebSession(sessionId);
|
|
1768
1798
|
}
|
|
1769
1799
|
catch (dumpError) {
|
|
1770
1800
|
logger.warn(`[WebAction] interval dump failed: ${dumpError.message}`);
|
|
@@ -1787,7 +1817,7 @@ export class DebugWebSocketServer {
|
|
|
1787
1817
|
case 'goto': {
|
|
1788
1818
|
if (!request.url)
|
|
1789
1819
|
throw new Error('goto requires url parameter');
|
|
1790
|
-
result = await
|
|
1820
|
+
result = await this.safeWebGoto(page, request.url, `[WebAction] goto`);
|
|
1791
1821
|
break;
|
|
1792
1822
|
}
|
|
1793
1823
|
case 'aiAction': {
|
|
@@ -1833,11 +1863,6 @@ export class DebugWebSocketServer {
|
|
|
1833
1863
|
const code = request.actionParams?.code || request.code || '';
|
|
1834
1864
|
if (!code)
|
|
1835
1865
|
throw new Error('runCode requires actionParams.code');
|
|
1836
|
-
// 如果提供了URL,先导航到指定页面
|
|
1837
|
-
if (request.url) {
|
|
1838
|
-
logger.info(`[WebAction] Navigating to URL before runCode: ${request.url}`);
|
|
1839
|
-
await page.goto(request.url, { waitUntil: 'networkidle' });
|
|
1840
|
-
}
|
|
1841
1866
|
// 行号探针:与真机 runCode 一致,通过 instrumentCode 在每条语句前插入 __lineCallback(line),
|
|
1842
1867
|
// 推送 type='execution_line' 给前端,使 web 与真机调试体验一致
|
|
1843
1868
|
const onExecutionLine = (line) => {
|
|
@@ -2036,6 +2061,10 @@ export class DebugWebSocketServer {
|
|
|
2036
2061
|
});
|
|
2037
2062
|
}
|
|
2038
2063
|
finally {
|
|
2064
|
+
// 清除执行中标记,允许后续关闭浏览器
|
|
2065
|
+
if (session) {
|
|
2066
|
+
session.webActionRunning = false;
|
|
2067
|
+
}
|
|
2039
2068
|
// 不管成功还是失败,web 调试结束后必须关闭浏览器
|
|
2040
2069
|
try {
|
|
2041
2070
|
await this.closeWebSession(sessionId);
|
|
@@ -2094,6 +2123,11 @@ export class DebugWebSocketServer {
|
|
|
2094
2123
|
const session = sessionManager.get(sessionId);
|
|
2095
2124
|
if (!session || !session.browser)
|
|
2096
2125
|
return;
|
|
2126
|
+
// 如果 web action 正在执行中,跳过关闭,防止 TTL GC 或其他并发路径误杀浏览器
|
|
2127
|
+
if (session.webActionRunning) {
|
|
2128
|
+
logger.warn(`[WebSession] Session ${sessionId} has running web action, skipping close to avoid race condition`);
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2097
2131
|
// 停止周期 dump
|
|
2098
2132
|
if (session.dumpIntervalId) {
|
|
2099
2133
|
clearInterval(session.dumpIntervalId);
|