@aiscene/aiserver 1.6.6 → 1.6.8

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.
Files changed (60) hide show
  1. package/dist/api/callback.d.ts +1 -1
  2. package/dist/api/callback.d.ts.map +1 -1
  3. package/dist/api/callback.js +168 -100
  4. package/dist/api/callback.js.map +1 -1
  5. package/dist/config/index.js +3 -3
  6. package/dist/config/index.js.map +1 -1
  7. package/dist/core/types.d.ts +2 -6
  8. package/dist/core/types.d.ts.map +1 -1
  9. package/dist/debug/dump-manager.d.ts.map +1 -1
  10. package/dist/debug/dump-manager.js +4 -1
  11. package/dist/debug/dump-manager.js.map +1 -1
  12. package/dist/debug/session-manager.d.ts +1 -0
  13. package/dist/debug/session-manager.d.ts.map +1 -1
  14. package/dist/debug/session-manager.js +4 -0
  15. package/dist/debug/session-manager.js.map +1 -1
  16. package/dist/debug/types.d.ts +0 -21
  17. package/dist/debug/types.d.ts.map +1 -1
  18. package/dist/debug/websocket-server.d.ts +0 -1
  19. package/dist/debug/websocket-server.d.ts.map +1 -1
  20. package/dist/debug/websocket-server.js +271 -190
  21. package/dist/debug/websocket-server.js.map +1 -1
  22. package/dist/executor/action-executor.d.ts +14 -0
  23. package/dist/executor/action-executor.d.ts.map +1 -1
  24. package/dist/executor/action-executor.js +77 -8
  25. package/dist/executor/action-executor.js.map +1 -1
  26. package/dist/executor/android-executor.d.ts +1 -0
  27. package/dist/executor/android-executor.d.ts.map +1 -1
  28. package/dist/executor/android-executor.js +26 -9
  29. package/dist/executor/android-executor.js.map +1 -1
  30. package/dist/executor/cli-executor.d.ts.map +1 -1
  31. package/dist/executor/cli-executor.js +3 -1
  32. package/dist/executor/cli-executor.js.map +1 -1
  33. package/dist/executor/ios-executor.d.ts.map +1 -1
  34. package/dist/executor/ios-executor.js +3 -1
  35. package/dist/executor/ios-executor.js.map +1 -1
  36. package/dist/executor/worker-entry.js +32 -3
  37. package/dist/executor/worker-entry.js.map +1 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +6 -6
  40. package/dist/index.js.map +1 -1
  41. package/dist/proxy/whistle-manager.d.ts +0 -8
  42. package/dist/proxy/whistle-manager.d.ts.map +1 -1
  43. package/dist/proxy/whistle-manager.js +0 -76
  44. package/dist/proxy/whistle-manager.js.map +1 -1
  45. package/dist/report/report-package.d.ts +7 -0
  46. package/dist/report/report-package.d.ts.map +1 -0
  47. package/dist/report/report-package.js +46 -0
  48. package/dist/report/report-package.js.map +1 -0
  49. package/dist/task/poller.d.ts +1 -0
  50. package/dist/task/poller.d.ts.map +1 -1
  51. package/dist/task/poller.js +14 -0
  52. package/dist/task/poller.js.map +1 -1
  53. package/dist/task/scheduler.d.ts +5 -1
  54. package/dist/task/scheduler.d.ts.map +1 -1
  55. package/dist/task/scheduler.js +82 -45
  56. package/dist/task/scheduler.js.map +1 -1
  57. package/dist/web/server.d.ts.map +1 -1
  58. package/dist/web/server.js +27 -0
  59. package/dist/web/server.js.map +1 -1
  60. package/package.json +2 -1
@@ -229,6 +229,8 @@ export class DebugWebSocketServer {
229
229
  }
230
230
  handleControlConnection(ws) {
231
231
  logger.info('[Control] New connection');
232
+ // 连接建立后发送 proxy_connected 确认,供客户端(android_device_run.js)确认握手
233
+ this.sendMessage(ws, { type: 'proxy_connected', timestamp: new Date().toISOString() });
232
234
  // 心跳:收到 pong 标记为存活
233
235
  ws.isAlive = true;
234
236
  ws.on('pong', () => {
@@ -254,7 +256,9 @@ export class DebugWebSocketServer {
254
256
  logger.info(`[WebSession] Client disconnected, keep browser alive for reuse: ${sessionId}`);
255
257
  continue;
256
258
  }
257
- this.cleanupSession(sessionId);
259
+ this.cleanupSession(sessionId).catch((error) => {
260
+ logger.warn(`[Control] Session cleanup failed: sessionId=${sessionId}, error=${error.message}`);
261
+ });
258
262
  }
259
263
  // 清理任务日志订阅
260
264
  this.cleanupClientSubscriptions(ws);
@@ -498,6 +502,7 @@ export class DebugWebSocketServer {
498
502
  session.webDeviceName = request.mobileMode ? (request.deviceName || 'iPhone 13') : undefined;
499
503
  // modelConfig 走构造参数(与 handleExecuteWebAction 一致,避免多会话相互覆盖 env)
500
504
  session.webAgent = new midscene.PlaywrightAgent(page, {
505
+ outputFormat: 'html-and-external-assets',
501
506
  modelConfig: this.applyModelConfig(request.modelConfig),
502
507
  });
503
508
  // 注册并发池 + 启动 TTL 计时
@@ -665,9 +670,7 @@ export class DebugWebSocketServer {
665
670
  mobileMode: request.mobileMode,
666
671
  deviceName: request.deviceName,
667
672
  // 环境代理配置
668
- environmentId: request.environmentId,
669
- proxyPort: request.proxyPort || (request.hostMappings && request.hostMappings.length > 0 ? 8899 : undefined),
670
- hostMappings: request.hostMappings,
673
+ proxyPort: request.proxyPort || 8899,
671
674
  // 公司代理平台账号
672
675
  proxyAccount: request.proxyAccount,
673
676
  // 添加登录配置
@@ -682,38 +685,13 @@ export class DebugWebSocketServer {
682
685
  agreementSelector: request.loginConfig.agreementSelector,
683
686
  }),
684
687
  };
685
- // Setup proxy rules: 如果有 proxyAccount,使用公司代理平台;否则走本地 Whistle
686
- const whistleApiBase = config.proxyAccount ? `http://proxy-pc.jd.com/account/${config.proxyAccount}` : undefined;
687
- if (config.hostMappings && config.hostMappings.length > 0) {
688
- try {
689
- const envId = config.environmentId || sessionId;
690
- const ruleGroupName = whistleManager.generateRuleGroupName(envId);
691
- const success = await whistleManager.createRuleGroup(ruleGroupName, config.hostMappings, whistleApiBase);
692
- if (success) {
693
- const sess = sessionManager.get(sessionId);
694
- if (sess) {
695
- sess.whistleRuleGroupName = ruleGroupName;
696
- sess.whistleApiBase = whistleApiBase;
697
- }
698
- if (!config.proxyAccount && !config.proxyPort)
699
- config.proxyPort = 8899;
700
- logger.info('[Debug] Whistle proxy setup: ruleGroup=' + ruleGroupName + ', proxyPort=' + config.proxyPort + ', proxyAccount=' + (config.proxyAccount || 'N/A'));
701
- // 开始请求抓包(传入 proxyAccount,自动选择正确的 Whistle 实例)
702
- try {
703
- await whistleManager.startCapture(sessionId, undefined, 3000, config.proxyAccount);
704
- logger.info('[Debug] Whistle request capture started: sessionId=' + sessionId);
705
- }
706
- catch (captureErr) {
707
- logger.warn('[Debug] Failed to start request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
708
- }
709
- }
710
- else {
711
- logger.warn('[Debug] Whistle proxy setup failed, continuing without proxy');
712
- }
713
- }
714
- catch (err) {
715
- logger.warn('[Debug] Whistle proxy setup error: ' + (err instanceof Error ? err.message : String(err)));
716
- }
688
+ // 注意:proxy-service.jd.com 的规则由前端(浏览器端)设置,后端只负责设备代理 + 抓包
689
+ try {
690
+ await whistleManager.startCapture(sessionId, undefined, 3000, config.proxyAccount);
691
+ logger.info('[Debug] Whistle request capture started: sessionId=' + sessionId);
692
+ }
693
+ catch (captureErr) {
694
+ logger.warn('[Debug] Failed to start request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
717
695
  }
718
696
  // 使用 AbortSignal 监听停止请求
719
697
  const executePromise = executor.execute(config);
@@ -737,7 +715,15 @@ export class DebugWebSocketServer {
737
715
  logger.info(`[executeRunCodeInProcess] Execution aborted by user, skipping result messages (handleStopDebug will send them)`);
738
716
  return;
739
717
  }
740
- // 清理 executor 引用
718
+ // 清理 executor(释放 device/agent 资源 + 清除设备代理)
719
+ if (executor && typeof executor.destroy === 'function') {
720
+ try {
721
+ await executor.destroy();
722
+ }
723
+ catch (destroyErr) {
724
+ logger.warn(`[executeRunCodeInProcess] Executor destroy error: ${destroyErr.message}`);
725
+ }
726
+ }
741
727
  if (session)
742
728
  session.executor = undefined;
743
729
  // 停止请求抓包,采集调试期间的所有请求
@@ -763,19 +749,6 @@ export class DebugWebSocketServer {
763
749
  catch (captureErr) {
764
750
  logger.warn('[Debug] Failed to stop request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
765
751
  }
766
- // Teardown Whistle proxy rules if they were set
767
- const currentSess = sessionManager.get(sessionId);
768
- const whistleRuleName = currentSess ? currentSess.whistleRuleGroupName : undefined;
769
- if (whistleRuleName) {
770
- whistleManager.removeRuleGroup(whistleRuleName, whistleApiBase).catch((err) => {
771
- logger.warn('[Debug] Failed to teardown Whistle proxy: ' + (err instanceof Error ? err.message : String(err)));
772
- });
773
- if (currentSess) {
774
- currentSess.whistleRuleGroupName = undefined;
775
- currentSess.whistleApiBase = undefined;
776
- }
777
- logger.info('[Debug] Whistle proxy teardown: ruleGroup=' + whistleRuleName);
778
- }
779
752
  sessionManager.updateStatus(sessionId, result.success ? 'completed' : 'failed');
780
753
  // 调试:打印 result 内容
781
754
  logger.info(`[executeRunCodeInProcess] result: success=${result.success}, reportFile=${result.reportFile || ''}, errorMessage=${result.errorMessage}`);
@@ -801,7 +774,6 @@ export class DebugWebSocketServer {
801
774
  sessionId,
802
775
  success: result.success,
803
776
  reportUrl,
804
- capturedRequests: capturedRequests.length > 0 ? this.simplifyCapturedRequests(capturedRequests) : undefined,
805
777
  captureDataUrl,
806
778
  }, request.deviceId);
807
779
  }
@@ -824,9 +796,7 @@ export class DebugWebSocketServer {
824
796
  nodeId: this.config.task.nodeId,
825
797
  modelConfig: fullModelConfig,
826
798
  // 环境代理配置
827
- environmentId: request.environmentId,
828
- proxyPort: request.proxyPort || (request.hostMappings && request.hostMappings.length > 0 ? 8899 : undefined),
829
- hostMappings: request.hostMappings,
799
+ proxyPort: request.proxyPort || 8899,
830
800
  // 公司代理平台账号
831
801
  proxyAccount: request.proxyAccount,
832
802
  // 添加登录配置
@@ -841,38 +811,14 @@ export class DebugWebSocketServer {
841
811
  agreementSelector: request.loginConfig.agreementSelector,
842
812
  }),
843
813
  };
844
- // Setup proxy rules: 如果有 proxyAccount,使用公司代理平台;否则走本地 Whistle
845
- const workerWhistleApiBase = execConfig.proxyAccount ? `http://proxy-pc.jd.com/account/${execConfig.proxyAccount}` : undefined;
846
- if (execConfig.hostMappings && execConfig.hostMappings.length > 0) {
847
- try {
848
- const envId = execConfig.environmentId || sessionId;
849
- const ruleGroupName = whistleManager.generateRuleGroupName(envId);
850
- const success = await whistleManager.createRuleGroup(ruleGroupName, execConfig.hostMappings, workerWhistleApiBase);
851
- if (success) {
852
- const sess = sessionManager.get(sessionId);
853
- if (sess) {
854
- sess.whistleRuleGroupName = ruleGroupName;
855
- sess.whistleApiBase = workerWhistleApiBase;
856
- }
857
- if (!execConfig.proxyAccount && !execConfig.proxyPort)
858
- execConfig.proxyPort = 8899;
859
- logger.info('[Debug-Worker] Whistle proxy setup: ruleGroup=' + ruleGroupName + ', proxyPort=' + execConfig.proxyPort + ', proxyAccount=' + (execConfig.proxyAccount || 'N/A'));
860
- // 开始请求抓包(传入 proxyAccount,自动选择正确的 Whistle 实例)
861
- try {
862
- await whistleManager.startCapture(sessionId, undefined, 3000, execConfig.proxyAccount);
863
- logger.info('[Debug-Worker] Whistle request capture started: sessionId=' + sessionId);
864
- }
865
- catch (captureErr) {
866
- logger.warn('[Debug-Worker] Failed to start request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
867
- }
868
- }
869
- else {
870
- logger.warn('[Debug-Worker] Whistle proxy setup failed, continuing without proxy');
871
- }
872
- }
873
- catch (err) {
874
- logger.warn('[Debug-Worker] Whistle proxy setup error: ' + (err instanceof Error ? err.message : String(err)));
875
- }
814
+ // 注意:proxy-service.jd.com 的规则由前端(浏览器端)设置,后端只负责设备代理 + 抓包
815
+ // 始终启动请求抓包
816
+ try {
817
+ await whistleManager.startCapture(sessionId, undefined, 3000, execConfig.proxyAccount);
818
+ logger.info('[Debug-Worker] Whistle request capture started: sessionId=' + sessionId);
819
+ }
820
+ catch (captureErr) {
821
+ logger.warn('[Debug-Worker] Failed to start request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
876
822
  }
877
823
  // Fork worker process for long-running debug
878
824
  const workerPath = new URL('../executor/worker-entry.js', import.meta.url).pathname;
@@ -931,22 +877,6 @@ export class DebugWebSocketServer {
931
877
  catch (captureErr) {
932
878
  logger.warn('[Debug-Worker] Failed to stop request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
933
879
  }
934
- // Teardown Whistle proxy rules if they were set
935
- const curSess = sessionManager.get(sessionId);
936
- const wRuleName = curSess ? curSess.whistleRuleGroupName : undefined;
937
- if (wRuleName) {
938
- try {
939
- await whistleManager.removeRuleGroup(wRuleName, workerWhistleApiBase);
940
- if (curSess) {
941
- curSess.whistleRuleGroupName = undefined;
942
- curSess.whistleApiBase = undefined;
943
- }
944
- logger.info('[Debug-Worker] Whistle proxy teardown: ruleGroup=' + wRuleName);
945
- }
946
- catch (err) {
947
- logger.warn('[Debug-Worker] Failed to teardown Whistle proxy: ' + (err instanceof Error ? err.message : String(err)));
948
- }
949
- }
950
880
  sessionManager.updateStatus(sessionId, success ? 'completed' : 'failed');
951
881
  const reportFile = workerResult.reportFile || '';
952
882
  const reportUrl = await this.safeUploadReport(reportFile, sessionId);
@@ -972,7 +902,6 @@ export class DebugWebSocketServer {
972
902
  dump: '',
973
903
  reportUrl,
974
904
  error: workerResult.errorMessage,
975
- capturedRequests: workerCapturedRequests.length > 0 ? this.simplifyCapturedRequests(workerCapturedRequests) : undefined,
976
905
  captureDataUrl: workerCaptureDataUrl,
977
906
  }, request.deviceId);
978
907
  };
@@ -1058,12 +987,19 @@ export class DebugWebSocketServer {
1058
987
  executionId: sessionId,
1059
988
  skipAppRestart: request.skipAppRestart,
1060
989
  // 环境代理配置
1061
- environmentId: request.environmentId,
1062
990
  proxyPort: request.proxyPort,
1063
- hostMappings: request.hostMappings,
1064
991
  // 公司代理平台账号
1065
992
  proxyAccount: request.proxyAccount,
1066
993
  };
994
+ // 注意:proxy-service.jd.com 的规则由前端(浏览器端)设置,后端只负责设备代理 + 抓包
995
+ // 始终启动请求抓包
996
+ try {
997
+ await whistleManager.startCapture(sessionId, undefined, 3000, config.proxyAccount);
998
+ logger.info('[Action] Whistle request capture started: sessionId=' + sessionId);
999
+ }
1000
+ catch (captureErr) {
1001
+ logger.warn('[Action] Failed to start request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
1002
+ }
1067
1003
  // 使用 AbortSignal 监听停止请求
1068
1004
  const executePromise = executor.execute(config);
1069
1005
  const abortPromise = new Promise((resolve) => {
@@ -1077,8 +1013,38 @@ export class DebugWebSocketServer {
1077
1013
  logger.info(`[Action] Execution aborted by user, skipping result messages (handleStopDebug will send them)`);
1078
1014
  return;
1079
1015
  }
1080
- // 清理 executor 引用
1016
+ // 清理 executor(释放 device/agent 资源 + 清除设备代理)
1017
+ if (executor && typeof executor.destroy === 'function') {
1018
+ try {
1019
+ await executor.destroy();
1020
+ }
1021
+ catch (destroyErr) {
1022
+ logger.warn(`[Action] Executor destroy error: ${destroyErr.message}`);
1023
+ }
1024
+ }
1081
1025
  session.executor = undefined;
1026
+ // 停止请求抓包,采集执行期间的所有请求
1027
+ let capturedRequests = [];
1028
+ let captureDataUrl;
1029
+ try {
1030
+ capturedRequests = await whistleManager.stopCapture(sessionId);
1031
+ if (capturedRequests.length > 0) {
1032
+ logger.info('[Action] Captured ' + capturedRequests.length + ' requests during session');
1033
+ try {
1034
+ const uploadResult = await this.saveAndUploadCaptureData(capturedRequests, sessionId);
1035
+ if (uploadResult) {
1036
+ captureDataUrl = uploadResult;
1037
+ logger.info('[Action] Capture data uploaded: ' + captureDataUrl);
1038
+ }
1039
+ }
1040
+ catch (uploadErr) {
1041
+ logger.warn('[Action] Failed to upload capture data: ' + (uploadErr instanceof Error ? uploadErr.message : String(uploadErr)));
1042
+ }
1043
+ }
1044
+ }
1045
+ catch (captureErr) {
1046
+ logger.warn('[Action] Failed to stop request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
1047
+ }
1082
1048
  const status = result.success ? 'completed' : 'failed';
1083
1049
  sessionManager.updateStatus(sessionId, status);
1084
1050
  logger.info(`[Action] Execution finished: sessionId=${sessionId}, status=${status}, ws.readyState=${ws.readyState}`);
@@ -1093,19 +1059,49 @@ export class DebugWebSocketServer {
1093
1059
  dump: '',
1094
1060
  reportUrl,
1095
1061
  error: result.errorMessage,
1062
+ captureDataUrl,
1096
1063
  }, request.deviceId);
1097
1064
  this.sendMessage(ws, {
1098
1065
  type: 'debug_completed',
1099
1066
  sessionId,
1100
1067
  success: result.success,
1101
1068
  reportUrl,
1069
+ captureDataUrl,
1102
1070
  }, request.deviceId);
1103
1071
  logger.info(`[Action] Sent action_result + debug_completed for sessionId=${sessionId}`);
1104
1072
  }
1105
1073
  catch (error) {
1074
+ // 清理 executor(释放 device/agent 资源 + 清除设备代理)
1075
+ if (session.executor && typeof session.executor.destroy === 'function') {
1076
+ try {
1077
+ await session.executor.destroy();
1078
+ }
1079
+ catch (destroyErr) {
1080
+ logger.warn(`[AiAct] Executor destroy error (catch): ${destroyErr.message}`);
1081
+ }
1082
+ }
1106
1083
  session.executor = undefined;
1107
1084
  sessionManager.updateStatus(sessionId, 'failed');
1108
1085
  logger.error(`[Action] Execution error: sessionId=${sessionId}, error=${error.message}, ws.readyState=${ws.readyState}`);
1086
+ // 异常时也停止抓包并上传
1087
+ let captureDataUrl;
1088
+ try {
1089
+ const capturedRequests = await whistleManager.stopCapture(sessionId);
1090
+ if (capturedRequests.length > 0) {
1091
+ logger.info('[Action] Captured ' + capturedRequests.length + ' requests (error path)');
1092
+ try {
1093
+ const uploadResult = await this.saveAndUploadCaptureData(capturedRequests, sessionId);
1094
+ if (uploadResult)
1095
+ captureDataUrl = uploadResult;
1096
+ }
1097
+ catch (uploadErr) {
1098
+ logger.warn('[Action] Failed to upload capture data (error path): ' + (uploadErr instanceof Error ? uploadErr.message : String(uploadErr)));
1099
+ }
1100
+ }
1101
+ }
1102
+ catch (captureErr) {
1103
+ logger.warn('[Action] Failed to stop capture (error path): ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
1104
+ }
1109
1105
  // 异常时也尝试提取 reportFile 并上传
1110
1106
  let reportFile = '';
1111
1107
  try {
@@ -1125,6 +1121,7 @@ export class DebugWebSocketServer {
1125
1121
  error: error.message,
1126
1122
  dump: '',
1127
1123
  reportUrl,
1124
+ captureDataUrl,
1128
1125
  }, request.deviceId);
1129
1126
  // 异常时也发送 debug_completed,确保前端能退出执行中状态
1130
1127
  this.sendMessage(ws, {
@@ -1132,6 +1129,7 @@ export class DebugWebSocketServer {
1132
1129
  sessionId,
1133
1130
  success: false,
1134
1131
  reportUrl,
1132
+ captureDataUrl,
1135
1133
  }, request.deviceId);
1136
1134
  }
1137
1135
  }
@@ -1143,6 +1141,14 @@ export class DebugWebSocketServer {
1143
1141
  session.abortController = abortController;
1144
1142
  // 将前端传来的 modelConfig 应用到环境变量,并获取补全后的 modelConfig(含 Planning/Insight)
1145
1143
  const fullModelConfig = this.applyModelConfig(request.modelConfig);
1144
+ // 始终启动请求抓包
1145
+ try {
1146
+ await whistleManager.startCapture(sessionId);
1147
+ logger.info('[AiAct] Whistle request capture started: sessionId=' + sessionId);
1148
+ }
1149
+ catch (captureErr) {
1150
+ logger.warn('[AiAct] Failed to start request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
1151
+ }
1146
1152
  // 哨兵:保证 ai_act_result + debug_completed 一定且只发一次(即便发送时再次抛错)
1147
1153
  let finalSent = false;
1148
1154
  const sendFinal = (payload) => {
@@ -1150,6 +1156,7 @@ export class DebugWebSocketServer {
1150
1156
  return;
1151
1157
  finalSent = true;
1152
1158
  const reportUrl = payload.reportUrl || null;
1159
+ const captureDataUrl = payload.captureDataUrl;
1153
1160
  // 第一次尝试
1154
1161
  try {
1155
1162
  this.sendMessage(ws, {
@@ -1162,6 +1169,7 @@ export class DebugWebSocketServer {
1162
1169
  reportUrl,
1163
1170
  error: payload.error,
1164
1171
  platform: request.platform || 'android',
1172
+ captureDataUrl,
1165
1173
  }, request.deviceId);
1166
1174
  }
1167
1175
  catch (e1) {
@@ -1177,6 +1185,7 @@ export class DebugWebSocketServer {
1177
1185
  reportUrl,
1178
1186
  error: (payload.error || '') + ' [send failed]',
1179
1187
  platform: request.platform || 'android',
1188
+ captureDataUrl,
1180
1189
  }, request.deviceId);
1181
1190
  }
1182
1191
  catch (e2) {
@@ -1190,6 +1199,7 @@ export class DebugWebSocketServer {
1190
1199
  sessionId,
1191
1200
  success: payload.success,
1192
1201
  reportUrl,
1202
+ captureDataUrl,
1193
1203
  }, request.deviceId);
1194
1204
  logger.info(`[AiAct] Sent ai_act_result + debug_completed for sessionId=${sessionId}, success=${payload.success}, reportUrl=${reportUrl || 'null'}`);
1195
1205
  }
@@ -1239,6 +1249,15 @@ export class DebugWebSocketServer {
1239
1249
  deepThink: request.deepThink,
1240
1250
  },
1241
1251
  };
1252
+ // 注意:proxy-service.jd.com 的规则由前端(浏览器端)设置,后端只负责设备代理 + 抓包
1253
+ // 始终启动请求抓包
1254
+ try {
1255
+ await whistleManager.startCapture(sessionId, undefined, 3000, config.proxyAccount);
1256
+ logger.info('[Action] Whistle request capture started: sessionId=' + sessionId);
1257
+ }
1258
+ catch (captureErr) {
1259
+ logger.warn('[Action] Failed to start request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
1260
+ }
1242
1261
  // 使用 AbortSignal 监听停止请求
1243
1262
  const executePromise = executor.execute(config);
1244
1263
  const abortPromise = new Promise((resolve) => {
@@ -1253,7 +1272,37 @@ export class DebugWebSocketServer {
1253
1272
  logger.info(`[AiAct] Execution aborted by user, skipping result messages (handleStopDebug will send them)`);
1254
1273
  return;
1255
1274
  }
1275
+ // 清理 executor(释放 device/agent 资源 + 清除设备代理)
1276
+ if (executor && typeof executor.destroy === 'function') {
1277
+ try {
1278
+ await executor.destroy();
1279
+ }
1280
+ catch (destroyErr) {
1281
+ logger.warn(`[AiAct] Executor destroy error: ${destroyErr.message}`);
1282
+ }
1283
+ }
1256
1284
  session.executor = undefined;
1285
+ // 停止请求抓包,采集执行期间的所有请求
1286
+ let captureDataUrl;
1287
+ try {
1288
+ const capturedRequests = await whistleManager.stopCapture(sessionId);
1289
+ if (capturedRequests.length > 0) {
1290
+ logger.info('[AiAct] Captured ' + capturedRequests.length + ' requests during session');
1291
+ try {
1292
+ const uploadResult = await this.saveAndUploadCaptureData(capturedRequests, sessionId);
1293
+ if (uploadResult) {
1294
+ captureDataUrl = uploadResult;
1295
+ logger.info('[AiAct] Capture data uploaded: ' + captureDataUrl);
1296
+ }
1297
+ }
1298
+ catch (uploadErr) {
1299
+ logger.warn('[AiAct] Failed to upload capture data: ' + (uploadErr instanceof Error ? uploadErr.message : String(uploadErr)));
1300
+ }
1301
+ }
1302
+ }
1303
+ catch (captureErr) {
1304
+ logger.warn('[AiAct] Failed to stop request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
1305
+ }
1257
1306
  const status = result.success ? 'completed' : 'failed';
1258
1307
  sessionManager.updateStatus(sessionId, status);
1259
1308
  logger.info(`[AiAct] Execution finished: sessionId=${sessionId}, status=${status}, ws.readyState=${ws.readyState}`);
@@ -1264,18 +1313,48 @@ export class DebugWebSocketServer {
1264
1313
  result: result.result,
1265
1314
  reportUrl,
1266
1315
  error: result.errorMessage,
1316
+ captureDataUrl,
1267
1317
  });
1268
1318
  }
1269
1319
  catch (error) {
1320
+ // 清理 executor(释放 device/agent 资源 + 清除设备代理)
1321
+ if (session.executor && typeof session.executor.destroy === 'function') {
1322
+ try {
1323
+ await session.executor.destroy();
1324
+ }
1325
+ catch (destroyErr) {
1326
+ logger.warn(`[AiAct] Executor destroy error (catch): ${destroyErr.message}`);
1327
+ }
1328
+ }
1270
1329
  session.executor = undefined;
1271
1330
  sessionManager.updateStatus(sessionId, 'failed');
1272
1331
  logger.error(`[AiAct] Execution error: sessionId=${sessionId}, error=${error.message}, ws.readyState=${ws.readyState}`);
1332
+ // 异常时也停止抓包并上传
1333
+ let captureDataUrl;
1334
+ try {
1335
+ const capturedRequests = await whistleManager.stopCapture(sessionId);
1336
+ if (capturedRequests.length > 0) {
1337
+ logger.info('[AiAct] Captured ' + capturedRequests.length + ' requests (error path)');
1338
+ try {
1339
+ const uploadResult = await this.saveAndUploadCaptureData(capturedRequests, sessionId);
1340
+ if (uploadResult)
1341
+ captureDataUrl = uploadResult;
1342
+ }
1343
+ catch (uploadErr) {
1344
+ logger.warn('[AiAct] Failed to upload capture data (error path): ' + (uploadErr instanceof Error ? uploadErr.message : String(uploadErr)));
1345
+ }
1346
+ }
1347
+ }
1348
+ catch (captureErr) {
1349
+ logger.warn('[AiAct] Failed to stop capture (error path): ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
1350
+ }
1273
1351
  const reportFile = safeExtractReportFile();
1274
1352
  const reportUrl = await this.safeUploadReport(reportFile, sessionId);
1275
1353
  sendFinal({
1276
1354
  success: false,
1277
1355
  error: error.message,
1278
1356
  reportUrl,
1357
+ captureDataUrl,
1279
1358
  });
1280
1359
  }
1281
1360
  finally {
@@ -1363,6 +1442,14 @@ export class DebugWebSocketServer {
1363
1442
  }
1364
1443
  try {
1365
1444
  this.sendMessage(ws, { type: 'web_action_started', sessionId, actionType: request.actionType });
1445
+ // 始终启动请求抓包
1446
+ try {
1447
+ await whistleManager.startCapture(sessionId);
1448
+ logger.info('[WebAction] Whistle request capture started: sessionId=' + sessionId);
1449
+ }
1450
+ catch (captureErr) {
1451
+ logger.warn('[WebAction] Failed to start request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
1452
+ }
1366
1453
  // 懒启动浏览器(每个 session 只启一次)
1367
1454
  if (!session.browser) {
1368
1455
  // 并发上限保护:超过 webSessionMaxConcurrency 直接拒绝,防止 OOM / launch 失败
@@ -1458,6 +1545,7 @@ export class DebugWebSocketServer {
1458
1545
  // 注意:worker 子进程路径(handleStartDebug 等)仍需经过 applyModelConfig 写 env,
1459
1546
  // 因为 fork 子进程必须通过 env 透传配置,不影响这里。
1460
1547
  session.webAgent = new midscene.PlaywrightAgent(page, {
1548
+ outputFormat: 'html-and-external-assets',
1461
1549
  modelConfig: this.applyModelConfig(request.modelConfig),
1462
1550
  });
1463
1551
  // 注册到 web session 并发池,开始计入并发数 + TTL
@@ -1663,6 +1751,27 @@ export class DebugWebSocketServer {
1663
1751
  const actionSuccess = !runCodeError;
1664
1752
  sessionManager.updateStatus(sessionId, actionSuccess ? 'completed' : 'failed');
1665
1753
  stopWebDumpInterval();
1754
+ // 停止请求抓包,采集执行期间的所有请求
1755
+ let captureDataUrl;
1756
+ try {
1757
+ const capturedRequests = await whistleManager.stopCapture(sessionId);
1758
+ if (capturedRequests.length > 0) {
1759
+ logger.info('[WebAction] Captured ' + capturedRequests.length + ' requests during session');
1760
+ try {
1761
+ const uploadResult = await this.saveAndUploadCaptureData(capturedRequests, sessionId);
1762
+ if (uploadResult) {
1763
+ captureDataUrl = uploadResult;
1764
+ logger.info('[WebAction] Capture data uploaded: ' + captureDataUrl);
1765
+ }
1766
+ }
1767
+ catch (uploadErr) {
1768
+ logger.warn('[WebAction] Failed to upload capture data: ' + (uploadErr instanceof Error ? uploadErr.message : String(uploadErr)));
1769
+ }
1770
+ }
1771
+ }
1772
+ catch (captureErr) {
1773
+ logger.warn('[WebAction] Failed to stop request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
1774
+ }
1666
1775
  // 上传报告文件到服务端,拿到 reportUrl
1667
1776
  const reportUrl = await this.safeUploadReport(reportFile, sessionId);
1668
1777
  logger.info(`[WebAction] report uploaded: reportUrl=${reportUrl || 'null'}, reportFile=${reportFile}`);
@@ -1676,6 +1785,7 @@ export class DebugWebSocketServer {
1676
1785
  dump: '',
1677
1786
  reportUrl,
1678
1787
  error: runCodeError || undefined,
1788
+ captureDataUrl,
1679
1789
  });
1680
1790
  // 与移动端对齐:同步发一份 action_result,供 debugLogManager 走报告链路
1681
1791
  this.sendMessage(ws, {
@@ -1688,6 +1798,7 @@ export class DebugWebSocketServer {
1688
1798
  reportUrl,
1689
1799
  error: runCodeError || undefined,
1690
1800
  platform: 'web',
1801
+ captureDataUrl,
1691
1802
  });
1692
1803
  // 必须发 debug_completed,前端依赖它退出执行中状态
1693
1804
  this.sendMessage(ws, {
@@ -1696,6 +1807,7 @@ export class DebugWebSocketServer {
1696
1807
  success: actionSuccess,
1697
1808
  reportUrl,
1698
1809
  errorMessage: runCodeError || undefined,
1810
+ captureDataUrl,
1699
1811
  });
1700
1812
  }
1701
1813
  catch (error) {
@@ -1706,6 +1818,25 @@ export class DebugWebSocketServer {
1706
1818
  clearInterval(session.dumpIntervalId);
1707
1819
  session.dumpIntervalId = undefined;
1708
1820
  }
1821
+ // 异常时也停止抓包并上传
1822
+ let captureDataUrl;
1823
+ try {
1824
+ const capturedRequests = await whistleManager.stopCapture(sessionId);
1825
+ if (capturedRequests.length > 0) {
1826
+ logger.info('[WebAction] Captured ' + capturedRequests.length + ' requests (error path)');
1827
+ try {
1828
+ const uploadResult = await this.saveAndUploadCaptureData(capturedRequests, sessionId);
1829
+ if (uploadResult)
1830
+ captureDataUrl = uploadResult;
1831
+ }
1832
+ catch (uploadErr) {
1833
+ logger.warn('[WebAction] Failed to upload capture data (error path): ' + (uploadErr instanceof Error ? uploadErr.message : String(uploadErr)));
1834
+ }
1835
+ }
1836
+ }
1837
+ catch (captureErr) {
1838
+ logger.warn('[WebAction] Failed to stop capture (error path): ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
1839
+ }
1709
1840
  // 失败时也尝试提取 reportFile 并上传
1710
1841
  let reportFile = '';
1711
1842
  const agentForReport = session?.webAgent;
@@ -1728,6 +1859,7 @@ export class DebugWebSocketServer {
1728
1859
  error: error.message,
1729
1860
  dump: '',
1730
1861
  reportUrl,
1862
+ captureDataUrl,
1731
1863
  });
1732
1864
  // 与移动端对齐:同步发一份 action_result
1733
1865
  this.sendMessage(ws, {
@@ -1739,6 +1871,7 @@ export class DebugWebSocketServer {
1739
1871
  dump: '',
1740
1872
  reportUrl,
1741
1873
  platform: 'web',
1874
+ captureDataUrl,
1742
1875
  });
1743
1876
  // 必须发 debug_completed,前端依赖它退出执行中状态
1744
1877
  this.sendMessage(ws, {
@@ -1746,6 +1879,7 @@ export class DebugWebSocketServer {
1746
1879
  sessionId,
1747
1880
  success: false,
1748
1881
  reportUrl,
1882
+ captureDataUrl,
1749
1883
  errorMessage: error.message,
1750
1884
  });
1751
1885
  }
@@ -1862,23 +1996,6 @@ export class DebugWebSocketServer {
1862
1996
  catch (captureErr) {
1863
1997
  logger.warn('[CloseWebSession] Failed to stop request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
1864
1998
  }
1865
- // Teardown Whistle proxy rules if they were set
1866
- const closeSess = sessionManager.get(request.sessionId);
1867
- const closeWhistleRule = closeSess ? closeSess.whistleRuleGroupName : undefined;
1868
- if (closeWhistleRule) {
1869
- try {
1870
- const closeWhistleApiBase = closeSess ? closeSess.whistleApiBase : undefined;
1871
- await whistleManager.removeRuleGroup(closeWhistleRule, closeWhistleApiBase);
1872
- if (closeSess) {
1873
- closeSess.whistleRuleGroupName = undefined;
1874
- closeSess.whistleApiBase = undefined;
1875
- }
1876
- logger.info('[CloseWebSession] Whistle proxy teardown: ruleGroup=' + closeWhistleRule);
1877
- }
1878
- catch (err) {
1879
- logger.warn('[CloseWebSession] Failed to teardown Whistle proxy: ' + (err instanceof Error ? err.message : String(err)));
1880
- }
1881
- }
1882
1999
  sessionManager.updateStatus(request.sessionId, 'stopped');
1883
2000
  this.sendMessage(ws, { type: 'web_session_closed', sessionId: request.sessionId });
1884
2001
  }
@@ -1954,23 +2071,6 @@ export class DebugWebSocketServer {
1954
2071
  catch (captureErr) {
1955
2072
  logger.warn('[StopDebug] Failed to stop request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
1956
2073
  }
1957
- // Teardown Whistle proxy rules if they were set
1958
- const stopSess = sessionManager.get(request.sessionId);
1959
- const stopWhistleRule = stopSess ? stopSess.whistleRuleGroupName : undefined;
1960
- if (stopWhistleRule) {
1961
- try {
1962
- const stopWhistleApiBase = stopSess ? stopSess.whistleApiBase : undefined;
1963
- await whistleManager.removeRuleGroup(stopWhistleRule, stopWhistleApiBase);
1964
- if (stopSess) {
1965
- stopSess.whistleRuleGroupName = undefined;
1966
- stopSess.whistleApiBase = undefined;
1967
- }
1968
- logger.info('[StopDebug] Whistle proxy teardown: ruleGroup=' + stopWhistleRule);
1969
- }
1970
- catch (err) {
1971
- logger.warn('[StopDebug] Failed to teardown Whistle proxy: ' + (err instanceof Error ? err.message : String(err)));
1972
- }
1973
- }
1974
2074
  sessionManager.updateStatus(request.sessionId, 'stopped');
1975
2075
  // 上传报告文件到服务端,拿到 reportUrl
1976
2076
  const reportUrl = await this.safeUploadReport(reportFile, request.sessionId);
@@ -1994,7 +2094,6 @@ export class DebugWebSocketServer {
1994
2094
  type: 'debug_stopped',
1995
2095
  sessionId: request.sessionId,
1996
2096
  reportUrl,
1997
- capturedRequests: stopCapturedRequests.length > 0 ? this.simplifyCapturedRequests(stopCapturedRequests) : undefined,
1998
2097
  captureDataUrl: stopCaptureDataUrl,
1999
2098
  });
2000
2099
  // 同时发送 action_result,让前端能获取到停止时的执行数据
@@ -2005,6 +2104,7 @@ export class DebugWebSocketServer {
2005
2104
  error: 'Action stopped by user',
2006
2105
  dump: '',
2007
2106
  reportUrl,
2107
+ captureDataUrl: stopCaptureDataUrl,
2008
2108
  });
2009
2109
  // 发送 debug_completed
2010
2110
  this.sendMessage(ws, {
@@ -2012,7 +2112,6 @@ export class DebugWebSocketServer {
2012
2112
  sessionId: request.sessionId,
2013
2113
  success: false,
2014
2114
  reportUrl,
2015
- capturedRequests: stopCapturedRequests.length > 0 ? this.simplifyCapturedRequests(stopCapturedRequests) : undefined,
2016
2115
  captureDataUrl: stopCaptureDataUrl,
2017
2116
  });
2018
2117
  }
@@ -2135,6 +2234,14 @@ export class DebugWebSocketServer {
2135
2234
  timestamp: new Date().toISOString(),
2136
2235
  };
2137
2236
  if (ws.readyState === ws.OPEN) {
2237
+ const messageType = String(message.type || '');
2238
+ const canDropUnderBackpressure = messageType === 'action_dump' ||
2239
+ messageType === 'execution_line' ||
2240
+ messageType === 'task_log';
2241
+ if (canDropUnderBackpressure && ws.bufferedAmount > 2 * 1024 * 1024) {
2242
+ logger.warn(`[WS] Dropping ${messageType} due to backpressure: buffered=${ws.bufferedAmount}, sessionId=${message.sessionId || ''}`);
2243
+ return;
2244
+ }
2138
2245
  let payload = JSON.stringify(msg);
2139
2246
  // WSS 自身 maxPayload=10MB(接收端),发送端我们保留 2MB 余量以兼容反代/网关
2140
2247
  const MAX_PAYLOAD_SIZE = 2 * 1024 * 1024; // 2MB
@@ -2219,56 +2326,29 @@ export class DebugWebSocketServer {
2219
2326
  return undefined;
2220
2327
  }
2221
2328
  }
2222
- simplifyCapturedRequests(requests) {
2223
- const MAX_BODY_LENGTH = 4096;
2224
- return requests.map(req => {
2225
- const simplified = {
2226
- id: req.id,
2227
- url: req.url,
2228
- method: req.method,
2229
- statusCode: req.statusCode,
2230
- clientIp: req.clientIp,
2231
- startTime: req.startTime,
2232
- endTime: req.endTime,
2233
- duration: req.duration,
2234
- dnsTime: req.dnsTime,
2235
- requestTime: req.requestTime,
2236
- responseTime: req.responseTime,
2237
- };
2238
- // 请求头只保留 content-type 和 content-length
2239
- if (req.reqHeaders) {
2240
- const filteredReqHeaders = {};
2241
- for (const [k, v] of Object.entries(req.reqHeaders)) {
2242
- if (k.toLowerCase() === 'content-type' || k.toLowerCase() === 'content-length') {
2243
- filteredReqHeaders[k] = v;
2244
- }
2245
- }
2246
- simplified.reqHeaders = filteredReqHeaders;
2247
- }
2248
- if (req.resHeaders) {
2249
- const filteredResHeaders = {};
2250
- for (const [k, v] of Object.entries(req.resHeaders)) {
2251
- if (k.toLowerCase() === 'content-type' || k.toLowerCase() === 'content-length') {
2252
- filteredResHeaders[k] = v;
2253
- }
2254
- }
2255
- simplified.resHeaders = filteredResHeaders;
2256
- }
2257
- // body 截断
2258
- if (req.reqBody) {
2259
- simplified.reqBody = req.reqBody.length > MAX_BODY_LENGTH
2260
- ? req.reqBody.substring(0, MAX_BODY_LENGTH) + '...[truncated]'
2261
- : req.reqBody;
2329
+ async cleanupSession(sessionId) {
2330
+ const session = sessionManager.get(sessionId);
2331
+ if (session?.abortController && !session.abortController.signal.aborted) {
2332
+ session.abortController.abort();
2333
+ }
2334
+ if (session?.executor && typeof session.executor.destroy === 'function') {
2335
+ try {
2336
+ await session.executor.destroy();
2262
2337
  }
2263
- if (req.resBody) {
2264
- simplified.resBody = req.resBody.length > MAX_BODY_LENGTH
2265
- ? req.resBody.substring(0, MAX_BODY_LENGTH) + '...[truncated]'
2266
- : req.resBody;
2338
+ catch (error) {
2339
+ logger.warn(`[Cleanup] Executor destroy failed: sessionId=${sessionId}, error=${error.message}`);
2267
2340
  }
2268
- return simplified;
2269
- });
2270
- }
2271
- async cleanupSession(sessionId) {
2341
+ session.executor = undefined;
2342
+ }
2343
+ if (session?.process && !session.process.killed) {
2344
+ session.process.kill('SIGTERM');
2345
+ }
2346
+ try {
2347
+ await whistleManager.stopCapture(sessionId);
2348
+ }
2349
+ catch (error) {
2350
+ logger.warn(`[Cleanup] Capture stop failed: sessionId=${sessionId}, error=${error.message}`);
2351
+ }
2272
2352
  // 关闭 web 浏览器(如果存在)
2273
2353
  await this.closeWebSession(sessionId);
2274
2354
  dumpManager.stopPeriodicDump(sessionId);
@@ -2279,6 +2359,7 @@ export class DebugWebSocketServer {
2279
2359
  return sessionManager.getAll();
2280
2360
  }
2281
2361
  async close() {
2362
+ await Promise.all(sessionManager.getAll().map((session) => this.cleanupSession(session.sessionId)));
2282
2363
  await screencastManager.closeAll();
2283
2364
  await webScreencastManager.closeAll();
2284
2365
  await webBrowserPool.closeAll();