@aiscene/aiserver 1.6.6 → 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 (36) 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 +28 -9
  4. package/dist/api/callback.js.map +1 -1
  5. package/dist/config/index.js +1 -1
  6. package/dist/core/types.d.ts +0 -5
  7. package/dist/core/types.d.ts.map +1 -1
  8. package/dist/debug/types.d.ts +0 -21
  9. package/dist/debug/types.d.ts.map +1 -1
  10. package/dist/debug/websocket-server.d.ts +0 -1
  11. package/dist/debug/websocket-server.d.ts.map +1 -1
  12. package/dist/debug/websocket-server.js +236 -190
  13. package/dist/debug/websocket-server.js.map +1 -1
  14. package/dist/executor/action-executor.d.ts +14 -0
  15. package/dist/executor/action-executor.d.ts.map +1 -1
  16. package/dist/executor/action-executor.js +74 -7
  17. package/dist/executor/action-executor.js.map +1 -1
  18. package/dist/executor/android-executor.d.ts +5 -0
  19. package/dist/executor/android-executor.d.ts.map +1 -1
  20. package/dist/executor/android-executor.js +24 -4
  21. package/dist/executor/android-executor.js.map +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +6 -6
  24. package/dist/index.js.map +1 -1
  25. package/dist/proxy/whistle-manager.d.ts +0 -8
  26. package/dist/proxy/whistle-manager.d.ts.map +1 -1
  27. package/dist/proxy/whistle-manager.js +0 -76
  28. package/dist/proxy/whistle-manager.js.map +1 -1
  29. package/dist/task/scheduler.d.ts +5 -1
  30. package/dist/task/scheduler.d.ts.map +1 -1
  31. package/dist/task/scheduler.js +82 -45
  32. package/dist/task/scheduler.js.map +1 -1
  33. package/dist/web/server.d.ts.map +1 -1
  34. package/dist/web/server.js +27 -0
  35. package/dist/web/server.js.map +1 -1
  36. package/package.json +1 -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', () => {
@@ -665,9 +667,7 @@ export class DebugWebSocketServer {
665
667
  mobileMode: request.mobileMode,
666
668
  deviceName: request.deviceName,
667
669
  // 环境代理配置
668
- environmentId: request.environmentId,
669
- proxyPort: request.proxyPort || (request.hostMappings && request.hostMappings.length > 0 ? 8899 : undefined),
670
- hostMappings: request.hostMappings,
670
+ proxyPort: request.proxyPort || 8899,
671
671
  // 公司代理平台账号
672
672
  proxyAccount: request.proxyAccount,
673
673
  // 添加登录配置
@@ -682,38 +682,13 @@ export class DebugWebSocketServer {
682
682
  agreementSelector: request.loginConfig.agreementSelector,
683
683
  }),
684
684
  };
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
- }
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)));
717
692
  }
718
693
  // 使用 AbortSignal 监听停止请求
719
694
  const executePromise = executor.execute(config);
@@ -737,7 +712,15 @@ export class DebugWebSocketServer {
737
712
  logger.info(`[executeRunCodeInProcess] Execution aborted by user, skipping result messages (handleStopDebug will send them)`);
738
713
  return;
739
714
  }
740
- // 清理 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
+ }
741
724
  if (session)
742
725
  session.executor = undefined;
743
726
  // 停止请求抓包,采集调试期间的所有请求
@@ -763,19 +746,6 @@ export class DebugWebSocketServer {
763
746
  catch (captureErr) {
764
747
  logger.warn('[Debug] Failed to stop request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
765
748
  }
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
749
  sessionManager.updateStatus(sessionId, result.success ? 'completed' : 'failed');
780
750
  // 调试:打印 result 内容
781
751
  logger.info(`[executeRunCodeInProcess] result: success=${result.success}, reportFile=${result.reportFile || ''}, errorMessage=${result.errorMessage}`);
@@ -801,7 +771,6 @@ export class DebugWebSocketServer {
801
771
  sessionId,
802
772
  success: result.success,
803
773
  reportUrl,
804
- capturedRequests: capturedRequests.length > 0 ? this.simplifyCapturedRequests(capturedRequests) : undefined,
805
774
  captureDataUrl,
806
775
  }, request.deviceId);
807
776
  }
@@ -824,9 +793,7 @@ export class DebugWebSocketServer {
824
793
  nodeId: this.config.task.nodeId,
825
794
  modelConfig: fullModelConfig,
826
795
  // 环境代理配置
827
- environmentId: request.environmentId,
828
- proxyPort: request.proxyPort || (request.hostMappings && request.hostMappings.length > 0 ? 8899 : undefined),
829
- hostMappings: request.hostMappings,
796
+ proxyPort: request.proxyPort || 8899,
830
797
  // 公司代理平台账号
831
798
  proxyAccount: request.proxyAccount,
832
799
  // 添加登录配置
@@ -841,38 +808,14 @@ export class DebugWebSocketServer {
841
808
  agreementSelector: request.loginConfig.agreementSelector,
842
809
  }),
843
810
  };
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
- }
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)));
876
819
  }
877
820
  // Fork worker process for long-running debug
878
821
  const workerPath = new URL('../executor/worker-entry.js', import.meta.url).pathname;
@@ -931,22 +874,6 @@ export class DebugWebSocketServer {
931
874
  catch (captureErr) {
932
875
  logger.warn('[Debug-Worker] Failed to stop request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
933
876
  }
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
877
  sessionManager.updateStatus(sessionId, success ? 'completed' : 'failed');
951
878
  const reportFile = workerResult.reportFile || '';
952
879
  const reportUrl = await this.safeUploadReport(reportFile, sessionId);
@@ -972,7 +899,6 @@ export class DebugWebSocketServer {
972
899
  dump: '',
973
900
  reportUrl,
974
901
  error: workerResult.errorMessage,
975
- capturedRequests: workerCapturedRequests.length > 0 ? this.simplifyCapturedRequests(workerCapturedRequests) : undefined,
976
902
  captureDataUrl: workerCaptureDataUrl,
977
903
  }, request.deviceId);
978
904
  };
@@ -1058,12 +984,19 @@ export class DebugWebSocketServer {
1058
984
  executionId: sessionId,
1059
985
  skipAppRestart: request.skipAppRestart,
1060
986
  // 环境代理配置
1061
- environmentId: request.environmentId,
1062
987
  proxyPort: request.proxyPort,
1063
- hostMappings: request.hostMappings,
1064
988
  // 公司代理平台账号
1065
989
  proxyAccount: request.proxyAccount,
1066
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
+ }
1067
1000
  // 使用 AbortSignal 监听停止请求
1068
1001
  const executePromise = executor.execute(config);
1069
1002
  const abortPromise = new Promise((resolve) => {
@@ -1077,8 +1010,38 @@ export class DebugWebSocketServer {
1077
1010
  logger.info(`[Action] Execution aborted by user, skipping result messages (handleStopDebug will send them)`);
1078
1011
  return;
1079
1012
  }
1080
- // 清理 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
+ }
1081
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
+ }
1082
1045
  const status = result.success ? 'completed' : 'failed';
1083
1046
  sessionManager.updateStatus(sessionId, status);
1084
1047
  logger.info(`[Action] Execution finished: sessionId=${sessionId}, status=${status}, ws.readyState=${ws.readyState}`);
@@ -1093,19 +1056,49 @@ export class DebugWebSocketServer {
1093
1056
  dump: '',
1094
1057
  reportUrl,
1095
1058
  error: result.errorMessage,
1059
+ captureDataUrl,
1096
1060
  }, request.deviceId);
1097
1061
  this.sendMessage(ws, {
1098
1062
  type: 'debug_completed',
1099
1063
  sessionId,
1100
1064
  success: result.success,
1101
1065
  reportUrl,
1066
+ captureDataUrl,
1102
1067
  }, request.deviceId);
1103
1068
  logger.info(`[Action] Sent action_result + debug_completed for sessionId=${sessionId}`);
1104
1069
  }
1105
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
+ }
1106
1080
  session.executor = undefined;
1107
1081
  sessionManager.updateStatus(sessionId, 'failed');
1108
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
+ }
1109
1102
  // 异常时也尝试提取 reportFile 并上传
1110
1103
  let reportFile = '';
1111
1104
  try {
@@ -1125,6 +1118,7 @@ export class DebugWebSocketServer {
1125
1118
  error: error.message,
1126
1119
  dump: '',
1127
1120
  reportUrl,
1121
+ captureDataUrl,
1128
1122
  }, request.deviceId);
1129
1123
  // 异常时也发送 debug_completed,确保前端能退出执行中状态
1130
1124
  this.sendMessage(ws, {
@@ -1132,6 +1126,7 @@ export class DebugWebSocketServer {
1132
1126
  sessionId,
1133
1127
  success: false,
1134
1128
  reportUrl,
1129
+ captureDataUrl,
1135
1130
  }, request.deviceId);
1136
1131
  }
1137
1132
  }
@@ -1143,6 +1138,14 @@ export class DebugWebSocketServer {
1143
1138
  session.abortController = abortController;
1144
1139
  // 将前端传来的 modelConfig 应用到环境变量,并获取补全后的 modelConfig(含 Planning/Insight)
1145
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
+ }
1146
1149
  // 哨兵:保证 ai_act_result + debug_completed 一定且只发一次(即便发送时再次抛错)
1147
1150
  let finalSent = false;
1148
1151
  const sendFinal = (payload) => {
@@ -1150,6 +1153,7 @@ export class DebugWebSocketServer {
1150
1153
  return;
1151
1154
  finalSent = true;
1152
1155
  const reportUrl = payload.reportUrl || null;
1156
+ const captureDataUrl = payload.captureDataUrl;
1153
1157
  // 第一次尝试
1154
1158
  try {
1155
1159
  this.sendMessage(ws, {
@@ -1162,6 +1166,7 @@ export class DebugWebSocketServer {
1162
1166
  reportUrl,
1163
1167
  error: payload.error,
1164
1168
  platform: request.platform || 'android',
1169
+ captureDataUrl,
1165
1170
  }, request.deviceId);
1166
1171
  }
1167
1172
  catch (e1) {
@@ -1177,6 +1182,7 @@ export class DebugWebSocketServer {
1177
1182
  reportUrl,
1178
1183
  error: (payload.error || '') + ' [send failed]',
1179
1184
  platform: request.platform || 'android',
1185
+ captureDataUrl,
1180
1186
  }, request.deviceId);
1181
1187
  }
1182
1188
  catch (e2) {
@@ -1190,6 +1196,7 @@ export class DebugWebSocketServer {
1190
1196
  sessionId,
1191
1197
  success: payload.success,
1192
1198
  reportUrl,
1199
+ captureDataUrl,
1193
1200
  }, request.deviceId);
1194
1201
  logger.info(`[AiAct] Sent ai_act_result + debug_completed for sessionId=${sessionId}, success=${payload.success}, reportUrl=${reportUrl || 'null'}`);
1195
1202
  }
@@ -1239,6 +1246,15 @@ export class DebugWebSocketServer {
1239
1246
  deepThink: request.deepThink,
1240
1247
  },
1241
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
+ }
1242
1258
  // 使用 AbortSignal 监听停止请求
1243
1259
  const executePromise = executor.execute(config);
1244
1260
  const abortPromise = new Promise((resolve) => {
@@ -1253,7 +1269,37 @@ export class DebugWebSocketServer {
1253
1269
  logger.info(`[AiAct] Execution aborted by user, skipping result messages (handleStopDebug will send them)`);
1254
1270
  return;
1255
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
+ }
1256
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
+ }
1257
1303
  const status = result.success ? 'completed' : 'failed';
1258
1304
  sessionManager.updateStatus(sessionId, status);
1259
1305
  logger.info(`[AiAct] Execution finished: sessionId=${sessionId}, status=${status}, ws.readyState=${ws.readyState}`);
@@ -1264,18 +1310,48 @@ export class DebugWebSocketServer {
1264
1310
  result: result.result,
1265
1311
  reportUrl,
1266
1312
  error: result.errorMessage,
1313
+ captureDataUrl,
1267
1314
  });
1268
1315
  }
1269
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
+ }
1270
1326
  session.executor = undefined;
1271
1327
  sessionManager.updateStatus(sessionId, 'failed');
1272
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
+ }
1273
1348
  const reportFile = safeExtractReportFile();
1274
1349
  const reportUrl = await this.safeUploadReport(reportFile, sessionId);
1275
1350
  sendFinal({
1276
1351
  success: false,
1277
1352
  error: error.message,
1278
1353
  reportUrl,
1354
+ captureDataUrl,
1279
1355
  });
1280
1356
  }
1281
1357
  finally {
@@ -1363,6 +1439,14 @@ export class DebugWebSocketServer {
1363
1439
  }
1364
1440
  try {
1365
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
+ }
1366
1450
  // 懒启动浏览器(每个 session 只启一次)
1367
1451
  if (!session.browser) {
1368
1452
  // 并发上限保护:超过 webSessionMaxConcurrency 直接拒绝,防止 OOM / launch 失败
@@ -1663,6 +1747,27 @@ export class DebugWebSocketServer {
1663
1747
  const actionSuccess = !runCodeError;
1664
1748
  sessionManager.updateStatus(sessionId, actionSuccess ? 'completed' : 'failed');
1665
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
+ }
1666
1771
  // 上传报告文件到服务端,拿到 reportUrl
1667
1772
  const reportUrl = await this.safeUploadReport(reportFile, sessionId);
1668
1773
  logger.info(`[WebAction] report uploaded: reportUrl=${reportUrl || 'null'}, reportFile=${reportFile}`);
@@ -1676,6 +1781,7 @@ export class DebugWebSocketServer {
1676
1781
  dump: '',
1677
1782
  reportUrl,
1678
1783
  error: runCodeError || undefined,
1784
+ captureDataUrl,
1679
1785
  });
1680
1786
  // 与移动端对齐:同步发一份 action_result,供 debugLogManager 走报告链路
1681
1787
  this.sendMessage(ws, {
@@ -1688,6 +1794,7 @@ export class DebugWebSocketServer {
1688
1794
  reportUrl,
1689
1795
  error: runCodeError || undefined,
1690
1796
  platform: 'web',
1797
+ captureDataUrl,
1691
1798
  });
1692
1799
  // 必须发 debug_completed,前端依赖它退出执行中状态
1693
1800
  this.sendMessage(ws, {
@@ -1696,6 +1803,7 @@ export class DebugWebSocketServer {
1696
1803
  success: actionSuccess,
1697
1804
  reportUrl,
1698
1805
  errorMessage: runCodeError || undefined,
1806
+ captureDataUrl,
1699
1807
  });
1700
1808
  }
1701
1809
  catch (error) {
@@ -1706,6 +1814,25 @@ export class DebugWebSocketServer {
1706
1814
  clearInterval(session.dumpIntervalId);
1707
1815
  session.dumpIntervalId = undefined;
1708
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
+ }
1709
1836
  // 失败时也尝试提取 reportFile 并上传
1710
1837
  let reportFile = '';
1711
1838
  const agentForReport = session?.webAgent;
@@ -1728,6 +1855,7 @@ export class DebugWebSocketServer {
1728
1855
  error: error.message,
1729
1856
  dump: '',
1730
1857
  reportUrl,
1858
+ captureDataUrl,
1731
1859
  });
1732
1860
  // 与移动端对齐:同步发一份 action_result
1733
1861
  this.sendMessage(ws, {
@@ -1739,6 +1867,7 @@ export class DebugWebSocketServer {
1739
1867
  dump: '',
1740
1868
  reportUrl,
1741
1869
  platform: 'web',
1870
+ captureDataUrl,
1742
1871
  });
1743
1872
  // 必须发 debug_completed,前端依赖它退出执行中状态
1744
1873
  this.sendMessage(ws, {
@@ -1746,6 +1875,7 @@ export class DebugWebSocketServer {
1746
1875
  sessionId,
1747
1876
  success: false,
1748
1877
  reportUrl,
1878
+ captureDataUrl,
1749
1879
  errorMessage: error.message,
1750
1880
  });
1751
1881
  }
@@ -1862,23 +1992,6 @@ export class DebugWebSocketServer {
1862
1992
  catch (captureErr) {
1863
1993
  logger.warn('[CloseWebSession] Failed to stop request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
1864
1994
  }
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
1995
  sessionManager.updateStatus(request.sessionId, 'stopped');
1883
1996
  this.sendMessage(ws, { type: 'web_session_closed', sessionId: request.sessionId });
1884
1997
  }
@@ -1954,23 +2067,6 @@ export class DebugWebSocketServer {
1954
2067
  catch (captureErr) {
1955
2068
  logger.warn('[StopDebug] Failed to stop request capture: ' + (captureErr instanceof Error ? captureErr.message : String(captureErr)));
1956
2069
  }
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
2070
  sessionManager.updateStatus(request.sessionId, 'stopped');
1975
2071
  // 上传报告文件到服务端,拿到 reportUrl
1976
2072
  const reportUrl = await this.safeUploadReport(reportFile, request.sessionId);
@@ -1994,7 +2090,6 @@ export class DebugWebSocketServer {
1994
2090
  type: 'debug_stopped',
1995
2091
  sessionId: request.sessionId,
1996
2092
  reportUrl,
1997
- capturedRequests: stopCapturedRequests.length > 0 ? this.simplifyCapturedRequests(stopCapturedRequests) : undefined,
1998
2093
  captureDataUrl: stopCaptureDataUrl,
1999
2094
  });
2000
2095
  // 同时发送 action_result,让前端能获取到停止时的执行数据
@@ -2005,6 +2100,7 @@ export class DebugWebSocketServer {
2005
2100
  error: 'Action stopped by user',
2006
2101
  dump: '',
2007
2102
  reportUrl,
2103
+ captureDataUrl: stopCaptureDataUrl,
2008
2104
  });
2009
2105
  // 发送 debug_completed
2010
2106
  this.sendMessage(ws, {
@@ -2012,7 +2108,6 @@ export class DebugWebSocketServer {
2012
2108
  sessionId: request.sessionId,
2013
2109
  success: false,
2014
2110
  reportUrl,
2015
- capturedRequests: stopCapturedRequests.length > 0 ? this.simplifyCapturedRequests(stopCapturedRequests) : undefined,
2016
2111
  captureDataUrl: stopCaptureDataUrl,
2017
2112
  });
2018
2113
  }
@@ -2219,55 +2314,6 @@ export class DebugWebSocketServer {
2219
2314
  return undefined;
2220
2315
  }
2221
2316
  }
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;
2262
- }
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;
2267
- }
2268
- return simplified;
2269
- });
2270
- }
2271
2317
  async cleanupSession(sessionId) {
2272
2318
  // 关闭 web 浏览器(如果存在)
2273
2319
  await this.closeWebSession(sessionId);