@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.
Files changed (40) hide show
  1. package/dist/api/callback.d.ts +6 -1
  2. package/dist/api/callback.d.ts.map +1 -1
  3. package/dist/api/callback.js +101 -3
  4. package/dist/api/callback.js.map +1 -1
  5. package/dist/config/index.d.ts.map +1 -1
  6. package/dist/config/index.js +1 -0
  7. package/dist/config/index.js.map +1 -1
  8. package/dist/config/schema.d.ts +1 -0
  9. package/dist/config/schema.d.ts.map +1 -1
  10. package/dist/core/types.d.ts +2 -0
  11. package/dist/core/types.d.ts.map +1 -1
  12. package/dist/debug/types.d.ts +12 -0
  13. package/dist/debug/types.d.ts.map +1 -1
  14. package/dist/debug/websocket-server.d.ts +9 -0
  15. package/dist/debug/websocket-server.d.ts.map +1 -1
  16. package/dist/debug/websocket-server.js +380 -3
  17. package/dist/debug/websocket-server.js.map +1 -1
  18. package/dist/executor/action-executor.d.ts +14 -0
  19. package/dist/executor/action-executor.d.ts.map +1 -1
  20. package/dist/executor/action-executor.js +74 -7
  21. package/dist/executor/action-executor.js.map +1 -1
  22. package/dist/executor/android-executor.d.ts +25 -0
  23. package/dist/executor/android-executor.d.ts.map +1 -1
  24. package/dist/executor/android-executor.js +134 -0
  25. package/dist/executor/android-executor.js.map +1 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +6 -6
  28. package/dist/index.js.map +1 -1
  29. package/dist/proxy/whistle-manager.d.ts +119 -0
  30. package/dist/proxy/whistle-manager.d.ts.map +1 -0
  31. package/dist/proxy/whistle-manager.js +298 -0
  32. package/dist/proxy/whistle-manager.js.map +1 -0
  33. package/dist/task/scheduler.d.ts +5 -0
  34. package/dist/task/scheduler.d.ts.map +1 -1
  35. package/dist/task/scheduler.js +88 -4
  36. package/dist/task/scheduler.js.map +1 -1
  37. package/dist/web/server.d.ts.map +1 -1
  38. package/dist/web/server.js +27 -0
  39. package/dist/web/server.js.map +1 -1
  40. 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);