@aiscene/aiserver 1.6.5 → 1.6.7
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 +6 -1
- package/dist/api/callback.d.ts.map +1 -1
- package/dist/api/callback.js +101 -3
- package/dist/api/callback.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +1 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +1 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/core/types.d.ts +2 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/debug/types.d.ts +12 -0
- package/dist/debug/types.d.ts.map +1 -1
- package/dist/debug/websocket-server.d.ts +9 -0
- package/dist/debug/websocket-server.d.ts.map +1 -1
- package/dist/debug/websocket-server.js +380 -3
- package/dist/debug/websocket-server.js.map +1 -1
- package/dist/executor/action-executor.d.ts +14 -0
- package/dist/executor/action-executor.d.ts.map +1 -1
- package/dist/executor/action-executor.js +74 -7
- package/dist/executor/action-executor.js.map +1 -1
- package/dist/executor/android-executor.d.ts +25 -0
- package/dist/executor/android-executor.d.ts.map +1 -1
- package/dist/executor/android-executor.js +134 -0
- package/dist/executor/android-executor.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/dist/proxy/whistle-manager.d.ts +119 -0
- package/dist/proxy/whistle-manager.d.ts.map +1 -0
- package/dist/proxy/whistle-manager.js +298 -0
- package/dist/proxy/whistle-manager.js.map +1 -0
- package/dist/task/scheduler.d.ts +5 -0
- package/dist/task/scheduler.d.ts.map +1 -1
- package/dist/task/scheduler.js +88 -4
- package/dist/task/scheduler.js.map +1 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +27 -0
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,9 @@ import { WebSocketServer } from 'ws';
|
|
|
2
2
|
import { fork } from 'child_process';
|
|
3
3
|
import { URL } from 'url';
|
|
4
4
|
import os from 'os';
|
|
5
|
-
import { uploadReportToServer } from '../api/callback.js';
|
|
5
|
+
import { uploadReportToServer, uploadCapturePackageFile } from '../api/callback.js';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
6
8
|
import { createLogger } from '../core/logger.js';
|
|
7
9
|
import { eventBus } from '../core/event-bus.js';
|
|
8
10
|
import { sessionManager } from './session-manager.js';
|
|
@@ -13,6 +15,7 @@ import { webBrowserPool } from './web-browser-pool.js';
|
|
|
13
15
|
import { ExecutorFactory } from '../executor/executor-factory.js';
|
|
14
16
|
import { instrumentCode } from '../executor/code-instrument.js';
|
|
15
17
|
import { playwrightSwipe } from '../core/native-swipe.js';
|
|
18
|
+
import { whistleManager } from '../proxy/whistle-manager.js';
|
|
16
19
|
const logger = createLogger('DebugWebSocket');
|
|
17
20
|
/** 获取本机可用的非内部 IPv4 地址,供客户端连接投屏 WebSocket */
|
|
18
21
|
function getLocalIP() {
|
|
@@ -226,6 +229,8 @@ export class DebugWebSocketServer {
|
|
|
226
229
|
}
|
|
227
230
|
handleControlConnection(ws) {
|
|
228
231
|
logger.info('[Control] New connection');
|
|
232
|
+
// 连接建立后发送 proxy_connected 确认,供客户端(android_device_run.js)确认握手
|
|
233
|
+
this.sendMessage(ws, { type: 'proxy_connected', timestamp: new Date().toISOString() });
|
|
229
234
|
// 心跳:收到 pong 标记为存活
|
|
230
235
|
ws.isAlive = true;
|
|
231
236
|
ws.on('pong', () => {
|
|
@@ -661,6 +666,10 @@ export class DebugWebSocketServer {
|
|
|
661
666
|
executionId: sessionId,
|
|
662
667
|
mobileMode: request.mobileMode,
|
|
663
668
|
deviceName: request.deviceName,
|
|
669
|
+
// 环境代理配置
|
|
670
|
+
proxyPort: request.proxyPort || 8899,
|
|
671
|
+
// 公司代理平台账号
|
|
672
|
+
proxyAccount: request.proxyAccount,
|
|
664
673
|
// 添加登录配置
|
|
665
674
|
...(request.loginConfig && {
|
|
666
675
|
loginUrl: request.loginConfig.loginUrl,
|
|
@@ -673,6 +682,14 @@ export class DebugWebSocketServer {
|
|
|
673
682
|
agreementSelector: request.loginConfig.agreementSelector,
|
|
674
683
|
}),
|
|
675
684
|
};
|
|
685
|
+
// 注意:proxy-service.jd.com 的规则由前端(浏览器端)设置,后端只负责设备代理 + 抓包
|
|
686
|
+
try {
|
|
687
|
+
await whistleManager.startCapture(sessionId, undefined, 3000, config.proxyAccount);
|
|
688
|
+
logger.info('[Debug] Whistle request capture started: sessionId=' + sessionId);
|
|
689
|
+
}
|
|
690
|
+
catch (captureErr) {
|
|
691
|
+
logger.warn('[Debug] Failed to start request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
|
|
692
|
+
}
|
|
676
693
|
// 使用 AbortSignal 监听停止请求
|
|
677
694
|
const executePromise = executor.execute(config);
|
|
678
695
|
const abortPromise = new Promise((resolve) => {
|
|
@@ -695,9 +712,40 @@ export class DebugWebSocketServer {
|
|
|
695
712
|
logger.info(`[executeRunCodeInProcess] Execution aborted by user, skipping result messages (handleStopDebug will send them)`);
|
|
696
713
|
return;
|
|
697
714
|
}
|
|
698
|
-
// 清理 executor
|
|
715
|
+
// 清理 executor(释放 device/agent 资源 + 清除设备代理)
|
|
716
|
+
if (executor && typeof executor.destroy === 'function') {
|
|
717
|
+
try {
|
|
718
|
+
await executor.destroy();
|
|
719
|
+
}
|
|
720
|
+
catch (destroyErr) {
|
|
721
|
+
logger.warn(`[executeRunCodeInProcess] Executor destroy error: ${destroyErr.message}`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
699
724
|
if (session)
|
|
700
725
|
session.executor = undefined;
|
|
726
|
+
// 停止请求抓包,采集调试期间的所有请求
|
|
727
|
+
let capturedRequests = [];
|
|
728
|
+
let captureDataUrl;
|
|
729
|
+
try {
|
|
730
|
+
capturedRequests = await whistleManager.stopCapture(sessionId);
|
|
731
|
+
if (capturedRequests.length > 0) {
|
|
732
|
+
logger.info('[Debug] Captured ' + capturedRequests.length + ' requests during debug session');
|
|
733
|
+
// 将抓包数据保存到文件并上传到 OSS
|
|
734
|
+
try {
|
|
735
|
+
const uploadResult = await this.saveAndUploadCaptureData(capturedRequests, sessionId);
|
|
736
|
+
if (uploadResult) {
|
|
737
|
+
captureDataUrl = uploadResult;
|
|
738
|
+
logger.info('[Debug] Capture data uploaded: ' + captureDataUrl);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
catch (uploadErr) {
|
|
742
|
+
logger.warn('[Debug] Failed to upload capture data: ' + (uploadErr instanceof Error ? uploadErr.message : String(uploadErr)));
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
catch (captureErr) {
|
|
747
|
+
logger.warn('[Debug] Failed to stop request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
|
|
748
|
+
}
|
|
701
749
|
sessionManager.updateStatus(sessionId, result.success ? 'completed' : 'failed');
|
|
702
750
|
// 调试:打印 result 内容
|
|
703
751
|
logger.info(`[executeRunCodeInProcess] result: success=${result.success}, reportFile=${result.reportFile || ''}, errorMessage=${result.errorMessage}`);
|
|
@@ -714,13 +762,16 @@ export class DebugWebSocketServer {
|
|
|
714
762
|
reportUrl,
|
|
715
763
|
error: result.errorMessage,
|
|
716
764
|
platform: request.platform || 'android',
|
|
765
|
+
captureDataUrl,
|
|
717
766
|
}, request.deviceId);
|
|
718
767
|
// 同时发一份 debug_completed,让前端调试面板能正确更新状态
|
|
768
|
+
// capturedRequests 只发摘要给前端展示,完整数据通过 captureDataUrl (OSS URL) 获取
|
|
719
769
|
this.sendMessage(ws, {
|
|
720
770
|
type: 'debug_completed',
|
|
721
771
|
sessionId,
|
|
722
772
|
success: result.success,
|
|
723
773
|
reportUrl,
|
|
774
|
+
captureDataUrl,
|
|
724
775
|
}, request.deviceId);
|
|
725
776
|
}
|
|
726
777
|
/** 自然语言模式:Fork worker 进程执行 aiAction */
|
|
@@ -741,6 +792,10 @@ export class DebugWebSocketServer {
|
|
|
741
792
|
taskId: sessionId,
|
|
742
793
|
nodeId: this.config.task.nodeId,
|
|
743
794
|
modelConfig: fullModelConfig,
|
|
795
|
+
// 环境代理配置
|
|
796
|
+
proxyPort: request.proxyPort || 8899,
|
|
797
|
+
// 公司代理平台账号
|
|
798
|
+
proxyAccount: request.proxyAccount,
|
|
744
799
|
// 添加登录配置
|
|
745
800
|
...(request.loginConfig && {
|
|
746
801
|
loginUrl: request.loginConfig.loginUrl,
|
|
@@ -753,6 +808,15 @@ export class DebugWebSocketServer {
|
|
|
753
808
|
agreementSelector: request.loginConfig.agreementSelector,
|
|
754
809
|
}),
|
|
755
810
|
};
|
|
811
|
+
// 注意:proxy-service.jd.com 的规则由前端(浏览器端)设置,后端只负责设备代理 + 抓包
|
|
812
|
+
// 始终启动请求抓包
|
|
813
|
+
try {
|
|
814
|
+
await whistleManager.startCapture(sessionId, undefined, 3000, execConfig.proxyAccount);
|
|
815
|
+
logger.info('[Debug-Worker] Whistle request capture started: sessionId=' + sessionId);
|
|
816
|
+
}
|
|
817
|
+
catch (captureErr) {
|
|
818
|
+
logger.warn('[Debug-Worker] Failed to start request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
|
|
819
|
+
}
|
|
756
820
|
// Fork worker process for long-running debug
|
|
757
821
|
const workerPath = new URL('../executor/worker-entry.js', import.meta.url).pathname;
|
|
758
822
|
// 优先使用 process.env 中已被 applyModelConfig() 设置的值(前端传入的模型配置),
|
|
@@ -799,10 +863,35 @@ export class DebugWebSocketServer {
|
|
|
799
863
|
return;
|
|
800
864
|
resultHandled = true;
|
|
801
865
|
const success = workerResult.success !== false;
|
|
866
|
+
// 停止请求抓包,采集调试期间的所有请求
|
|
867
|
+
let workerCapturedRequests = [];
|
|
868
|
+
try {
|
|
869
|
+
workerCapturedRequests = await whistleManager.stopCapture(sessionId);
|
|
870
|
+
if (workerCapturedRequests.length > 0) {
|
|
871
|
+
logger.info('[Debug-Worker] Captured ' + workerCapturedRequests.length + ' requests during debug session');
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
catch (captureErr) {
|
|
875
|
+
logger.warn('[Debug-Worker] Failed to stop request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
|
|
876
|
+
}
|
|
802
877
|
sessionManager.updateStatus(sessionId, success ? 'completed' : 'failed');
|
|
803
878
|
const reportFile = workerResult.reportFile || '';
|
|
804
879
|
const reportUrl = await this.safeUploadReport(reportFile, sessionId);
|
|
805
880
|
logger.info(`[Debug] report uploaded: reportUrl=${reportUrl || 'null'}, reportFile=${reportFile}`);
|
|
881
|
+
// 将 worker 抓包数据保存到文件并上传到 OSS
|
|
882
|
+
let workerCaptureDataUrl;
|
|
883
|
+
if (workerCapturedRequests.length > 0) {
|
|
884
|
+
try {
|
|
885
|
+
const uploadResult = await this.saveAndUploadCaptureData(workerCapturedRequests, sessionId);
|
|
886
|
+
if (uploadResult) {
|
|
887
|
+
workerCaptureDataUrl = uploadResult;
|
|
888
|
+
logger.info('[Debug-Worker] Capture data uploaded: ' + workerCaptureDataUrl);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
catch (uploadErr) {
|
|
892
|
+
logger.warn('[Debug-Worker] Failed to upload capture data: ' + (uploadErr instanceof Error ? uploadErr.message : String(uploadErr)));
|
|
893
|
+
}
|
|
894
|
+
}
|
|
806
895
|
this.sendMessage(ws, {
|
|
807
896
|
type: 'debug_completed',
|
|
808
897
|
sessionId,
|
|
@@ -810,6 +899,7 @@ export class DebugWebSocketServer {
|
|
|
810
899
|
dump: '',
|
|
811
900
|
reportUrl,
|
|
812
901
|
error: workerResult.errorMessage,
|
|
902
|
+
captureDataUrl: workerCaptureDataUrl,
|
|
813
903
|
}, request.deviceId);
|
|
814
904
|
};
|
|
815
905
|
childProcess.on('message', (message) => {
|
|
@@ -893,7 +983,20 @@ export class DebugWebSocketServer {
|
|
|
893
983
|
modelConfig: fullModelConfig,
|
|
894
984
|
executionId: sessionId,
|
|
895
985
|
skipAppRestart: request.skipAppRestart,
|
|
986
|
+
// 环境代理配置
|
|
987
|
+
proxyPort: request.proxyPort,
|
|
988
|
+
// 公司代理平台账号
|
|
989
|
+
proxyAccount: request.proxyAccount,
|
|
896
990
|
};
|
|
991
|
+
// 注意:proxy-service.jd.com 的规则由前端(浏览器端)设置,后端只负责设备代理 + 抓包
|
|
992
|
+
// 始终启动请求抓包
|
|
993
|
+
try {
|
|
994
|
+
await whistleManager.startCapture(sessionId, undefined, 3000, config.proxyAccount);
|
|
995
|
+
logger.info('[Action] Whistle request capture started: sessionId=' + sessionId);
|
|
996
|
+
}
|
|
997
|
+
catch (captureErr) {
|
|
998
|
+
logger.warn('[Action] Failed to start request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
|
|
999
|
+
}
|
|
897
1000
|
// 使用 AbortSignal 监听停止请求
|
|
898
1001
|
const executePromise = executor.execute(config);
|
|
899
1002
|
const abortPromise = new Promise((resolve) => {
|
|
@@ -907,8 +1010,38 @@ export class DebugWebSocketServer {
|
|
|
907
1010
|
logger.info(`[Action] Execution aborted by user, skipping result messages (handleStopDebug will send them)`);
|
|
908
1011
|
return;
|
|
909
1012
|
}
|
|
910
|
-
// 清理 executor
|
|
1013
|
+
// 清理 executor(释放 device/agent 资源 + 清除设备代理)
|
|
1014
|
+
if (executor && typeof executor.destroy === 'function') {
|
|
1015
|
+
try {
|
|
1016
|
+
await executor.destroy();
|
|
1017
|
+
}
|
|
1018
|
+
catch (destroyErr) {
|
|
1019
|
+
logger.warn(`[Action] Executor destroy error: ${destroyErr.message}`);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
911
1022
|
session.executor = undefined;
|
|
1023
|
+
// 停止请求抓包,采集执行期间的所有请求
|
|
1024
|
+
let capturedRequests = [];
|
|
1025
|
+
let captureDataUrl;
|
|
1026
|
+
try {
|
|
1027
|
+
capturedRequests = await whistleManager.stopCapture(sessionId);
|
|
1028
|
+
if (capturedRequests.length > 0) {
|
|
1029
|
+
logger.info('[Action] Captured ' + capturedRequests.length + ' requests during session');
|
|
1030
|
+
try {
|
|
1031
|
+
const uploadResult = await this.saveAndUploadCaptureData(capturedRequests, sessionId);
|
|
1032
|
+
if (uploadResult) {
|
|
1033
|
+
captureDataUrl = uploadResult;
|
|
1034
|
+
logger.info('[Action] Capture data uploaded: ' + captureDataUrl);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
catch (uploadErr) {
|
|
1038
|
+
logger.warn('[Action] Failed to upload capture data: ' + (uploadErr instanceof Error ? uploadErr.message : String(uploadErr)));
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
catch (captureErr) {
|
|
1043
|
+
logger.warn('[Action] Failed to stop request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
|
|
1044
|
+
}
|
|
912
1045
|
const status = result.success ? 'completed' : 'failed';
|
|
913
1046
|
sessionManager.updateStatus(sessionId, status);
|
|
914
1047
|
logger.info(`[Action] Execution finished: sessionId=${sessionId}, status=${status}, ws.readyState=${ws.readyState}`);
|
|
@@ -923,19 +1056,49 @@ export class DebugWebSocketServer {
|
|
|
923
1056
|
dump: '',
|
|
924
1057
|
reportUrl,
|
|
925
1058
|
error: result.errorMessage,
|
|
1059
|
+
captureDataUrl,
|
|
926
1060
|
}, request.deviceId);
|
|
927
1061
|
this.sendMessage(ws, {
|
|
928
1062
|
type: 'debug_completed',
|
|
929
1063
|
sessionId,
|
|
930
1064
|
success: result.success,
|
|
931
1065
|
reportUrl,
|
|
1066
|
+
captureDataUrl,
|
|
932
1067
|
}, request.deviceId);
|
|
933
1068
|
logger.info(`[Action] Sent action_result + debug_completed for sessionId=${sessionId}`);
|
|
934
1069
|
}
|
|
935
1070
|
catch (error) {
|
|
1071
|
+
// 清理 executor(释放 device/agent 资源 + 清除设备代理)
|
|
1072
|
+
if (session.executor && typeof session.executor.destroy === 'function') {
|
|
1073
|
+
try {
|
|
1074
|
+
await session.executor.destroy();
|
|
1075
|
+
}
|
|
1076
|
+
catch (destroyErr) {
|
|
1077
|
+
logger.warn(`[AiAct] Executor destroy error (catch): ${destroyErr.message}`);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
936
1080
|
session.executor = undefined;
|
|
937
1081
|
sessionManager.updateStatus(sessionId, 'failed');
|
|
938
1082
|
logger.error(`[Action] Execution error: sessionId=${sessionId}, error=${error.message}, ws.readyState=${ws.readyState}`);
|
|
1083
|
+
// 异常时也停止抓包并上传
|
|
1084
|
+
let captureDataUrl;
|
|
1085
|
+
try {
|
|
1086
|
+
const capturedRequests = await whistleManager.stopCapture(sessionId);
|
|
1087
|
+
if (capturedRequests.length > 0) {
|
|
1088
|
+
logger.info('[Action] Captured ' + capturedRequests.length + ' requests (error path)');
|
|
1089
|
+
try {
|
|
1090
|
+
const uploadResult = await this.saveAndUploadCaptureData(capturedRequests, sessionId);
|
|
1091
|
+
if (uploadResult)
|
|
1092
|
+
captureDataUrl = uploadResult;
|
|
1093
|
+
}
|
|
1094
|
+
catch (uploadErr) {
|
|
1095
|
+
logger.warn('[Action] Failed to upload capture data (error path): ' + (uploadErr instanceof Error ? uploadErr.message : String(uploadErr)));
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
catch (captureErr) {
|
|
1100
|
+
logger.warn('[Action] Failed to stop capture (error path): ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
|
|
1101
|
+
}
|
|
939
1102
|
// 异常时也尝试提取 reportFile 并上传
|
|
940
1103
|
let reportFile = '';
|
|
941
1104
|
try {
|
|
@@ -955,6 +1118,7 @@ export class DebugWebSocketServer {
|
|
|
955
1118
|
error: error.message,
|
|
956
1119
|
dump: '',
|
|
957
1120
|
reportUrl,
|
|
1121
|
+
captureDataUrl,
|
|
958
1122
|
}, request.deviceId);
|
|
959
1123
|
// 异常时也发送 debug_completed,确保前端能退出执行中状态
|
|
960
1124
|
this.sendMessage(ws, {
|
|
@@ -962,6 +1126,7 @@ export class DebugWebSocketServer {
|
|
|
962
1126
|
sessionId,
|
|
963
1127
|
success: false,
|
|
964
1128
|
reportUrl,
|
|
1129
|
+
captureDataUrl,
|
|
965
1130
|
}, request.deviceId);
|
|
966
1131
|
}
|
|
967
1132
|
}
|
|
@@ -973,6 +1138,14 @@ export class DebugWebSocketServer {
|
|
|
973
1138
|
session.abortController = abortController;
|
|
974
1139
|
// 将前端传来的 modelConfig 应用到环境变量,并获取补全后的 modelConfig(含 Planning/Insight)
|
|
975
1140
|
const fullModelConfig = this.applyModelConfig(request.modelConfig);
|
|
1141
|
+
// 始终启动请求抓包
|
|
1142
|
+
try {
|
|
1143
|
+
await whistleManager.startCapture(sessionId);
|
|
1144
|
+
logger.info('[AiAct] Whistle request capture started: sessionId=' + sessionId);
|
|
1145
|
+
}
|
|
1146
|
+
catch (captureErr) {
|
|
1147
|
+
logger.warn('[AiAct] Failed to start request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
|
|
1148
|
+
}
|
|
976
1149
|
// 哨兵:保证 ai_act_result + debug_completed 一定且只发一次(即便发送时再次抛错)
|
|
977
1150
|
let finalSent = false;
|
|
978
1151
|
const sendFinal = (payload) => {
|
|
@@ -980,6 +1153,7 @@ export class DebugWebSocketServer {
|
|
|
980
1153
|
return;
|
|
981
1154
|
finalSent = true;
|
|
982
1155
|
const reportUrl = payload.reportUrl || null;
|
|
1156
|
+
const captureDataUrl = payload.captureDataUrl;
|
|
983
1157
|
// 第一次尝试
|
|
984
1158
|
try {
|
|
985
1159
|
this.sendMessage(ws, {
|
|
@@ -992,6 +1166,7 @@ export class DebugWebSocketServer {
|
|
|
992
1166
|
reportUrl,
|
|
993
1167
|
error: payload.error,
|
|
994
1168
|
platform: request.platform || 'android',
|
|
1169
|
+
captureDataUrl,
|
|
995
1170
|
}, request.deviceId);
|
|
996
1171
|
}
|
|
997
1172
|
catch (e1) {
|
|
@@ -1007,6 +1182,7 @@ export class DebugWebSocketServer {
|
|
|
1007
1182
|
reportUrl,
|
|
1008
1183
|
error: (payload.error || '') + ' [send failed]',
|
|
1009
1184
|
platform: request.platform || 'android',
|
|
1185
|
+
captureDataUrl,
|
|
1010
1186
|
}, request.deviceId);
|
|
1011
1187
|
}
|
|
1012
1188
|
catch (e2) {
|
|
@@ -1020,6 +1196,7 @@ export class DebugWebSocketServer {
|
|
|
1020
1196
|
sessionId,
|
|
1021
1197
|
success: payload.success,
|
|
1022
1198
|
reportUrl,
|
|
1199
|
+
captureDataUrl,
|
|
1023
1200
|
}, request.deviceId);
|
|
1024
1201
|
logger.info(`[AiAct] Sent ai_act_result + debug_completed for sessionId=${sessionId}, success=${payload.success}, reportUrl=${reportUrl || 'null'}`);
|
|
1025
1202
|
}
|
|
@@ -1069,6 +1246,15 @@ export class DebugWebSocketServer {
|
|
|
1069
1246
|
deepThink: request.deepThink,
|
|
1070
1247
|
},
|
|
1071
1248
|
};
|
|
1249
|
+
// 注意:proxy-service.jd.com 的规则由前端(浏览器端)设置,后端只负责设备代理 + 抓包
|
|
1250
|
+
// 始终启动请求抓包
|
|
1251
|
+
try {
|
|
1252
|
+
await whistleManager.startCapture(sessionId, undefined, 3000, config.proxyAccount);
|
|
1253
|
+
logger.info('[Action] Whistle request capture started: sessionId=' + sessionId);
|
|
1254
|
+
}
|
|
1255
|
+
catch (captureErr) {
|
|
1256
|
+
logger.warn('[Action] Failed to start request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
|
|
1257
|
+
}
|
|
1072
1258
|
// 使用 AbortSignal 监听停止请求
|
|
1073
1259
|
const executePromise = executor.execute(config);
|
|
1074
1260
|
const abortPromise = new Promise((resolve) => {
|
|
@@ -1083,7 +1269,37 @@ export class DebugWebSocketServer {
|
|
|
1083
1269
|
logger.info(`[AiAct] Execution aborted by user, skipping result messages (handleStopDebug will send them)`);
|
|
1084
1270
|
return;
|
|
1085
1271
|
}
|
|
1272
|
+
// 清理 executor(释放 device/agent 资源 + 清除设备代理)
|
|
1273
|
+
if (executor && typeof executor.destroy === 'function') {
|
|
1274
|
+
try {
|
|
1275
|
+
await executor.destroy();
|
|
1276
|
+
}
|
|
1277
|
+
catch (destroyErr) {
|
|
1278
|
+
logger.warn(`[AiAct] Executor destroy error: ${destroyErr.message}`);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1086
1281
|
session.executor = undefined;
|
|
1282
|
+
// 停止请求抓包,采集执行期间的所有请求
|
|
1283
|
+
let captureDataUrl;
|
|
1284
|
+
try {
|
|
1285
|
+
const capturedRequests = await whistleManager.stopCapture(sessionId);
|
|
1286
|
+
if (capturedRequests.length > 0) {
|
|
1287
|
+
logger.info('[AiAct] Captured ' + capturedRequests.length + ' requests during session');
|
|
1288
|
+
try {
|
|
1289
|
+
const uploadResult = await this.saveAndUploadCaptureData(capturedRequests, sessionId);
|
|
1290
|
+
if (uploadResult) {
|
|
1291
|
+
captureDataUrl = uploadResult;
|
|
1292
|
+
logger.info('[AiAct] Capture data uploaded: ' + captureDataUrl);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
catch (uploadErr) {
|
|
1296
|
+
logger.warn('[AiAct] Failed to upload capture data: ' + (uploadErr instanceof Error ? uploadErr.message : String(uploadErr)));
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
catch (captureErr) {
|
|
1301
|
+
logger.warn('[AiAct] Failed to stop request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
|
|
1302
|
+
}
|
|
1087
1303
|
const status = result.success ? 'completed' : 'failed';
|
|
1088
1304
|
sessionManager.updateStatus(sessionId, status);
|
|
1089
1305
|
logger.info(`[AiAct] Execution finished: sessionId=${sessionId}, status=${status}, ws.readyState=${ws.readyState}`);
|
|
@@ -1094,18 +1310,48 @@ export class DebugWebSocketServer {
|
|
|
1094
1310
|
result: result.result,
|
|
1095
1311
|
reportUrl,
|
|
1096
1312
|
error: result.errorMessage,
|
|
1313
|
+
captureDataUrl,
|
|
1097
1314
|
});
|
|
1098
1315
|
}
|
|
1099
1316
|
catch (error) {
|
|
1317
|
+
// 清理 executor(释放 device/agent 资源 + 清除设备代理)
|
|
1318
|
+
if (session.executor && typeof session.executor.destroy === 'function') {
|
|
1319
|
+
try {
|
|
1320
|
+
await session.executor.destroy();
|
|
1321
|
+
}
|
|
1322
|
+
catch (destroyErr) {
|
|
1323
|
+
logger.warn(`[AiAct] Executor destroy error (catch): ${destroyErr.message}`);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1100
1326
|
session.executor = undefined;
|
|
1101
1327
|
sessionManager.updateStatus(sessionId, 'failed');
|
|
1102
1328
|
logger.error(`[AiAct] Execution error: sessionId=${sessionId}, error=${error.message}, ws.readyState=${ws.readyState}`);
|
|
1329
|
+
// 异常时也停止抓包并上传
|
|
1330
|
+
let captureDataUrl;
|
|
1331
|
+
try {
|
|
1332
|
+
const capturedRequests = await whistleManager.stopCapture(sessionId);
|
|
1333
|
+
if (capturedRequests.length > 0) {
|
|
1334
|
+
logger.info('[AiAct] Captured ' + capturedRequests.length + ' requests (error path)');
|
|
1335
|
+
try {
|
|
1336
|
+
const uploadResult = await this.saveAndUploadCaptureData(capturedRequests, sessionId);
|
|
1337
|
+
if (uploadResult)
|
|
1338
|
+
captureDataUrl = uploadResult;
|
|
1339
|
+
}
|
|
1340
|
+
catch (uploadErr) {
|
|
1341
|
+
logger.warn('[AiAct] Failed to upload capture data (error path): ' + (uploadErr instanceof Error ? uploadErr.message : String(uploadErr)));
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
catch (captureErr) {
|
|
1346
|
+
logger.warn('[AiAct] Failed to stop capture (error path): ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
|
|
1347
|
+
}
|
|
1103
1348
|
const reportFile = safeExtractReportFile();
|
|
1104
1349
|
const reportUrl = await this.safeUploadReport(reportFile, sessionId);
|
|
1105
1350
|
sendFinal({
|
|
1106
1351
|
success: false,
|
|
1107
1352
|
error: error.message,
|
|
1108
1353
|
reportUrl,
|
|
1354
|
+
captureDataUrl,
|
|
1109
1355
|
});
|
|
1110
1356
|
}
|
|
1111
1357
|
finally {
|
|
@@ -1193,6 +1439,14 @@ export class DebugWebSocketServer {
|
|
|
1193
1439
|
}
|
|
1194
1440
|
try {
|
|
1195
1441
|
this.sendMessage(ws, { type: 'web_action_started', sessionId, actionType: request.actionType });
|
|
1442
|
+
// 始终启动请求抓包
|
|
1443
|
+
try {
|
|
1444
|
+
await whistleManager.startCapture(sessionId);
|
|
1445
|
+
logger.info('[WebAction] Whistle request capture started: sessionId=' + sessionId);
|
|
1446
|
+
}
|
|
1447
|
+
catch (captureErr) {
|
|
1448
|
+
logger.warn('[WebAction] Failed to start request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
|
|
1449
|
+
}
|
|
1196
1450
|
// 懒启动浏览器(每个 session 只启一次)
|
|
1197
1451
|
if (!session.browser) {
|
|
1198
1452
|
// 并发上限保护:超过 webSessionMaxConcurrency 直接拒绝,防止 OOM / launch 失败
|
|
@@ -1493,6 +1747,27 @@ export class DebugWebSocketServer {
|
|
|
1493
1747
|
const actionSuccess = !runCodeError;
|
|
1494
1748
|
sessionManager.updateStatus(sessionId, actionSuccess ? 'completed' : 'failed');
|
|
1495
1749
|
stopWebDumpInterval();
|
|
1750
|
+
// 停止请求抓包,采集执行期间的所有请求
|
|
1751
|
+
let captureDataUrl;
|
|
1752
|
+
try {
|
|
1753
|
+
const capturedRequests = await whistleManager.stopCapture(sessionId);
|
|
1754
|
+
if (capturedRequests.length > 0) {
|
|
1755
|
+
logger.info('[WebAction] Captured ' + capturedRequests.length + ' requests during session');
|
|
1756
|
+
try {
|
|
1757
|
+
const uploadResult = await this.saveAndUploadCaptureData(capturedRequests, sessionId);
|
|
1758
|
+
if (uploadResult) {
|
|
1759
|
+
captureDataUrl = uploadResult;
|
|
1760
|
+
logger.info('[WebAction] Capture data uploaded: ' + captureDataUrl);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
catch (uploadErr) {
|
|
1764
|
+
logger.warn('[WebAction] Failed to upload capture data: ' + (uploadErr instanceof Error ? uploadErr.message : String(uploadErr)));
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
catch (captureErr) {
|
|
1769
|
+
logger.warn('[WebAction] Failed to stop request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
|
|
1770
|
+
}
|
|
1496
1771
|
// 上传报告文件到服务端,拿到 reportUrl
|
|
1497
1772
|
const reportUrl = await this.safeUploadReport(reportFile, sessionId);
|
|
1498
1773
|
logger.info(`[WebAction] report uploaded: reportUrl=${reportUrl || 'null'}, reportFile=${reportFile}`);
|
|
@@ -1506,6 +1781,7 @@ export class DebugWebSocketServer {
|
|
|
1506
1781
|
dump: '',
|
|
1507
1782
|
reportUrl,
|
|
1508
1783
|
error: runCodeError || undefined,
|
|
1784
|
+
captureDataUrl,
|
|
1509
1785
|
});
|
|
1510
1786
|
// 与移动端对齐:同步发一份 action_result,供 debugLogManager 走报告链路
|
|
1511
1787
|
this.sendMessage(ws, {
|
|
@@ -1518,6 +1794,7 @@ export class DebugWebSocketServer {
|
|
|
1518
1794
|
reportUrl,
|
|
1519
1795
|
error: runCodeError || undefined,
|
|
1520
1796
|
platform: 'web',
|
|
1797
|
+
captureDataUrl,
|
|
1521
1798
|
});
|
|
1522
1799
|
// 必须发 debug_completed,前端依赖它退出执行中状态
|
|
1523
1800
|
this.sendMessage(ws, {
|
|
@@ -1526,6 +1803,7 @@ export class DebugWebSocketServer {
|
|
|
1526
1803
|
success: actionSuccess,
|
|
1527
1804
|
reportUrl,
|
|
1528
1805
|
errorMessage: runCodeError || undefined,
|
|
1806
|
+
captureDataUrl,
|
|
1529
1807
|
});
|
|
1530
1808
|
}
|
|
1531
1809
|
catch (error) {
|
|
@@ -1536,6 +1814,25 @@ export class DebugWebSocketServer {
|
|
|
1536
1814
|
clearInterval(session.dumpIntervalId);
|
|
1537
1815
|
session.dumpIntervalId = undefined;
|
|
1538
1816
|
}
|
|
1817
|
+
// 异常时也停止抓包并上传
|
|
1818
|
+
let captureDataUrl;
|
|
1819
|
+
try {
|
|
1820
|
+
const capturedRequests = await whistleManager.stopCapture(sessionId);
|
|
1821
|
+
if (capturedRequests.length > 0) {
|
|
1822
|
+
logger.info('[WebAction] Captured ' + capturedRequests.length + ' requests (error path)');
|
|
1823
|
+
try {
|
|
1824
|
+
const uploadResult = await this.saveAndUploadCaptureData(capturedRequests, sessionId);
|
|
1825
|
+
if (uploadResult)
|
|
1826
|
+
captureDataUrl = uploadResult;
|
|
1827
|
+
}
|
|
1828
|
+
catch (uploadErr) {
|
|
1829
|
+
logger.warn('[WebAction] Failed to upload capture data (error path): ' + (uploadErr instanceof Error ? uploadErr.message : String(uploadErr)));
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
catch (captureErr) {
|
|
1834
|
+
logger.warn('[WebAction] Failed to stop capture (error path): ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
|
|
1835
|
+
}
|
|
1539
1836
|
// 失败时也尝试提取 reportFile 并上传
|
|
1540
1837
|
let reportFile = '';
|
|
1541
1838
|
const agentForReport = session?.webAgent;
|
|
@@ -1558,6 +1855,7 @@ export class DebugWebSocketServer {
|
|
|
1558
1855
|
error: error.message,
|
|
1559
1856
|
dump: '',
|
|
1560
1857
|
reportUrl,
|
|
1858
|
+
captureDataUrl,
|
|
1561
1859
|
});
|
|
1562
1860
|
// 与移动端对齐:同步发一份 action_result
|
|
1563
1861
|
this.sendMessage(ws, {
|
|
@@ -1569,6 +1867,7 @@ export class DebugWebSocketServer {
|
|
|
1569
1867
|
dump: '',
|
|
1570
1868
|
reportUrl,
|
|
1571
1869
|
platform: 'web',
|
|
1870
|
+
captureDataUrl,
|
|
1572
1871
|
});
|
|
1573
1872
|
// 必须发 debug_completed,前端依赖它退出执行中状态
|
|
1574
1873
|
this.sendMessage(ws, {
|
|
@@ -1576,6 +1875,7 @@ export class DebugWebSocketServer {
|
|
|
1576
1875
|
sessionId,
|
|
1577
1876
|
success: false,
|
|
1578
1877
|
reportUrl,
|
|
1878
|
+
captureDataUrl,
|
|
1579
1879
|
errorMessage: error.message,
|
|
1580
1880
|
});
|
|
1581
1881
|
}
|
|
@@ -1685,6 +1985,13 @@ export class DebugWebSocketServer {
|
|
|
1685
1985
|
return;
|
|
1686
1986
|
}
|
|
1687
1987
|
await this.closeWebSession(request.sessionId);
|
|
1988
|
+
// 停止请求抓包
|
|
1989
|
+
try {
|
|
1990
|
+
await whistleManager.stopCapture(request.sessionId);
|
|
1991
|
+
}
|
|
1992
|
+
catch (captureErr) {
|
|
1993
|
+
logger.warn('[CloseWebSession] Failed to stop request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
|
|
1994
|
+
}
|
|
1688
1995
|
sessionManager.updateStatus(request.sessionId, 'stopped');
|
|
1689
1996
|
this.sendMessage(ws, { type: 'web_session_closed', sessionId: request.sessionId });
|
|
1690
1997
|
}
|
|
@@ -1749,15 +2056,41 @@ export class DebugWebSocketServer {
|
|
|
1749
2056
|
}
|
|
1750
2057
|
// 关闭 web 浏览器(如果存在)
|
|
1751
2058
|
await this.closeWebSession(request.sessionId);
|
|
2059
|
+
// 停止请求抓包,采集调试期间的所有请求
|
|
2060
|
+
let stopCapturedRequests = [];
|
|
2061
|
+
try {
|
|
2062
|
+
stopCapturedRequests = await whistleManager.stopCapture(request.sessionId);
|
|
2063
|
+
if (stopCapturedRequests.length > 0) {
|
|
2064
|
+
logger.info('[StopDebug] Captured ' + stopCapturedRequests.length + ' requests during debug session');
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
catch (captureErr) {
|
|
2068
|
+
logger.warn('[StopDebug] Failed to stop request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
|
|
2069
|
+
}
|
|
1752
2070
|
sessionManager.updateStatus(request.sessionId, 'stopped');
|
|
1753
2071
|
// 上传报告文件到服务端,拿到 reportUrl
|
|
1754
2072
|
const reportUrl = await this.safeUploadReport(reportFile, request.sessionId);
|
|
1755
2073
|
logger.info(`[StopDebug] report uploaded: reportUrl=${reportUrl || 'null'}, reportFile=${reportFile}`);
|
|
2074
|
+
// 将停止时的抓包数据保存到文件并上传到 OSS
|
|
2075
|
+
let stopCaptureDataUrl;
|
|
2076
|
+
if (stopCapturedRequests.length > 0) {
|
|
2077
|
+
try {
|
|
2078
|
+
const uploadResult = await this.saveAndUploadCaptureData(stopCapturedRequests, request.sessionId);
|
|
2079
|
+
if (uploadResult) {
|
|
2080
|
+
stopCaptureDataUrl = uploadResult;
|
|
2081
|
+
logger.info('[StopDebug] Capture data uploaded: ' + stopCaptureDataUrl);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
catch (uploadErr) {
|
|
2085
|
+
logger.warn('[StopDebug] Failed to upload capture data: ' + (uploadErr instanceof Error ? uploadErr.message : String(uploadErr)));
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
1756
2088
|
// 发送带执行数据的停止消息
|
|
1757
2089
|
this.sendMessage(ws, {
|
|
1758
2090
|
type: 'debug_stopped',
|
|
1759
2091
|
sessionId: request.sessionId,
|
|
1760
2092
|
reportUrl,
|
|
2093
|
+
captureDataUrl: stopCaptureDataUrl,
|
|
1761
2094
|
});
|
|
1762
2095
|
// 同时发送 action_result,让前端能获取到停止时的执行数据
|
|
1763
2096
|
this.sendMessage(ws, {
|
|
@@ -1767,6 +2100,7 @@ export class DebugWebSocketServer {
|
|
|
1767
2100
|
error: 'Action stopped by user',
|
|
1768
2101
|
dump: '',
|
|
1769
2102
|
reportUrl,
|
|
2103
|
+
captureDataUrl: stopCaptureDataUrl,
|
|
1770
2104
|
});
|
|
1771
2105
|
// 发送 debug_completed
|
|
1772
2106
|
this.sendMessage(ws, {
|
|
@@ -1774,6 +2108,7 @@ export class DebugWebSocketServer {
|
|
|
1774
2108
|
sessionId: request.sessionId,
|
|
1775
2109
|
success: false,
|
|
1776
2110
|
reportUrl,
|
|
2111
|
+
captureDataUrl: stopCaptureDataUrl,
|
|
1777
2112
|
});
|
|
1778
2113
|
}
|
|
1779
2114
|
// ==================== Get Logs ====================
|
|
@@ -1937,6 +2272,48 @@ export class DebugWebSocketServer {
|
|
|
1937
2272
|
sendError(ws, error) {
|
|
1938
2273
|
this.sendMessage(ws, { type: 'error', error });
|
|
1939
2274
|
}
|
|
2275
|
+
/**
|
|
2276
|
+
* 精简 CapturedRequest 列表,去除大 body 只保留摘要信息,
|
|
2277
|
+
* 避免 WebSocket 消息过大(单条 body 上限 4KB,超出截断)
|
|
2278
|
+
*/
|
|
2279
|
+
/**
|
|
2280
|
+
* 将抓包数据保存到本地文件并上传到 OSS
|
|
2281
|
+
* 返回 OSS URL,失败则返回 undefined
|
|
2282
|
+
*/
|
|
2283
|
+
async saveAndUploadCaptureData(capturedRequests, sessionId) {
|
|
2284
|
+
if (!capturedRequests || capturedRequests.length === 0)
|
|
2285
|
+
return undefined;
|
|
2286
|
+
try {
|
|
2287
|
+
// 保存到本地临时文件
|
|
2288
|
+
const tmpDir = path.join(process.cwd(), 'tmp', 'captures');
|
|
2289
|
+
if (!fs.existsSync(tmpDir)) {
|
|
2290
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
2291
|
+
}
|
|
2292
|
+
const captureFile = path.join(tmpDir, `capture_${sessionId}_${Date.now()}.json`);
|
|
2293
|
+
fs.writeFileSync(captureFile, JSON.stringify(capturedRequests, null, 2));
|
|
2294
|
+
logger.info(`[CaptureUpload] Saved capture data to: ${captureFile} (${capturedRequests.length} requests)`);
|
|
2295
|
+
// 上传到 OSS
|
|
2296
|
+
const uploadUrl = this.config?.callback?.packageFileUploadUrl;
|
|
2297
|
+
if (!uploadUrl) {
|
|
2298
|
+
logger.warn('[CaptureUpload] packageFileUploadUrl not configured, skip upload');
|
|
2299
|
+
return undefined;
|
|
2300
|
+
}
|
|
2301
|
+
const captureDataUrl = await uploadCapturePackageFile(captureFile, sessionId, uploadUrl);
|
|
2302
|
+
if (captureDataUrl) {
|
|
2303
|
+
// 上传成功后删除本地临时文件
|
|
2304
|
+
try {
|
|
2305
|
+
fs.unlinkSync(captureFile);
|
|
2306
|
+
}
|
|
2307
|
+
catch { }
|
|
2308
|
+
return captureDataUrl;
|
|
2309
|
+
}
|
|
2310
|
+
return undefined;
|
|
2311
|
+
}
|
|
2312
|
+
catch (err) {
|
|
2313
|
+
logger.error('[CaptureUpload] Failed to save/upload capture data: ' + (err instanceof Error ? err.message : String(err)));
|
|
2314
|
+
return undefined;
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
1940
2317
|
async cleanupSession(sessionId) {
|
|
1941
2318
|
// 关闭 web 浏览器(如果存在)
|
|
1942
2319
|
await this.closeWebSession(sessionId);
|