@aiscene/aiserver 1.4.3 → 1.4.5

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.
@@ -30,41 +30,71 @@ function getLocalIP() {
30
30
  }
31
31
  return '127.0.0.1';
32
32
  }
33
- /** 上传报告 HTML 到服务器 */
34
- async function uploadReportToServer(reportHTML, sessionId, uploadUrl) {
35
- if (!reportHTML) {
36
- logger.debug('[UploadReport] No report HTML to upload');
33
+ /** 上传报告文件到服务器(流式上传,不将整个文件读入内存) */
34
+ async function uploadReportToServer(reportFilePath, sessionId, uploadUrl) {
35
+ if (!reportFilePath) {
36
+ logger.info('[UploadReport] No report file path provided, skip upload');
37
37
  return null;
38
38
  }
39
+ logger.info(`[UploadReport] === Start upload === sessionId=${sessionId}, filePath=${reportFilePath}`);
39
40
  try {
40
- const buffer = Buffer.from(reportHTML, 'utf-8');
41
+ if (!fs.existsSync(reportFilePath)) {
42
+ logger.warn(`[UploadReport] Report file not found: ${reportFilePath}, skip upload`);
43
+ return null;
44
+ }
45
+ const stat = fs.statSync(reportFilePath);
46
+ const fileSizeMB = (stat.size / 1024 / 1024).toFixed(2);
47
+ logger.info(`[UploadReport] File exists: ${reportFilePath}, size=${stat.size} bytes (${fileSizeMB}MB)`);
48
+ logger.info(`[UploadReport] Uploading to: POST ${uploadUrl}, sessionId=${sessionId}`);
49
+ const uploadStart = Date.now();
50
+ // 读取文件内容(与 callback.ts uploadReportFile 保持一致的写法,确保后端能正确解析 multipart)
51
+ let fileBuffer;
52
+ if (stat.size > 50 * 1024 * 1024) {
53
+ fileBuffer = await new Promise((resolve, reject) => {
54
+ const chunks = [];
55
+ const readStream = fs.createReadStream(reportFilePath);
56
+ readStream.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
57
+ readStream.on('end', () => resolve(Buffer.concat(chunks)));
58
+ readStream.on('error', reject);
59
+ });
60
+ }
61
+ else {
62
+ fileBuffer = fs.readFileSync(reportFilePath);
63
+ }
64
+ const fileName = `report_${sessionId}.html`;
41
65
  const formData = new FormData();
42
- formData.append('file', buffer, {
43
- filename: `report_${sessionId}.html`,
44
- contentType: 'text/html',
45
- });
66
+ formData.append('file', fileBuffer, fileName);
46
67
  formData.append('sessionId', sessionId);
47
- logger.info(`[UploadReport] Request: POST ${uploadUrl}`);
48
- logger.info(`[UploadReport] Request body: sessionId=${sessionId}, fileSize=${buffer.length} bytes`);
49
68
  const response = await axios.post(uploadUrl, formData, {
50
69
  headers: {
51
70
  'User-Agent': 'aiserver/1.0',
52
- 'Accept': 'application/json',
71
+ 'Accept': '*/*',
72
+ 'Connection': 'close',
73
+ 'Expect': '',
74
+ // form-data 库会自动设置 Content-Type(含 boundary),必须带上
75
+ ...formData.getHeaders(),
53
76
  },
54
- timeout: 60000, // 60s 超时
77
+ timeout: 120000,
78
+ maxRedirects: 0,
55
79
  });
56
- logger.info(`[UploadReport] Response status: ${response.status}`);
80
+ const uploadCost = Date.now() - uploadStart;
81
+ logger.info(`[UploadReport] Response: status=${response.status}, cost=${uploadCost}ms`);
57
82
  logger.info(`[UploadReport] Response data: ${JSON.stringify(response.data)}`);
58
- if (response.data && response.data.reportUrl) {
59
- logger.info(`[UploadReport] Upload successful, reportUrl: ${response.data.reportUrl}`);
60
- return response.data.reportUrl;
83
+ // 后端返回 { code: 200, data: { sessionId, reportUrl } } 或 { success: true, data: { reportUrl } }
84
+ const reportUrl = response.data?.data?.reportUrl || response.data?.reportUrl;
85
+ if (reportUrl) {
86
+ logger.info(`[UploadReport] === Upload successful === reportUrl=${reportUrl}, cost=${uploadCost}ms, size=${fileSizeMB}MB`);
87
+ return reportUrl;
61
88
  }
62
- logger.warn(`[UploadReport] Upload response missing reportUrl`);
89
+ logger.warn(`[UploadReport] Upload response missing reportUrl, response=${JSON.stringify(response.data)}`);
63
90
  return null;
64
91
  }
65
92
  catch (error) {
66
93
  const err = error;
67
- logger.error(`[UploadReport] Upload failed: ${err.message}`);
94
+ logger.error(`[UploadReport] === Upload failed === error=${err.message}`);
95
+ if (err.code) {
96
+ logger.error(`[UploadReport] Error code: ${err.code}`);
97
+ }
68
98
  if (err.response) {
69
99
  logger.error(`[UploadReport] Response status: ${err.response.status}`);
70
100
  logger.error(`[UploadReport] Response data: ${JSON.stringify(err.response.data)}`);
@@ -467,20 +497,18 @@ export class DebugWebSocketServer {
467
497
  session.executor = undefined;
468
498
  sessionManager.updateStatus(sessionId, result.success ? 'completed' : 'failed');
469
499
  // 调试:打印 result 内容
470
- logger.info(`[executeRunCodeInProcess] result: success=${result.success}, reportHTML length=${(result.reportHTML || '').length}, errorMessage=${result.errorMessage}`);
471
- // 调试场景不再上传报告到服务端:reportHTML 直接通过长链接回前端,避免上传卡住
472
- const reportHTML = result.reportHTML || '';
473
- const reportUrl = null;
474
- logger.info(`[executeRunCodeInProcess] Skip server upload (debug mode), reportHTML size=${reportHTML.length}`);
500
+ logger.info(`[executeRunCodeInProcess] result: success=${result.success}, reportFile=${result.reportFile || ''}, errorMessage=${result.errorMessage}`);
501
+ const reportFile = result.reportFile || '';
502
+ const reportUrl = await this.safeUploadReport(reportFile, sessionId);
503
+ logger.info(`[executeRunCodeInProcess] report uploaded: reportUrl=${reportUrl || 'null'}, reportFile=${reportFile}`);
475
504
  this.sendMessage(ws, {
476
505
  type: 'action_result',
477
506
  sessionId,
478
507
  actionType: 'runCode',
479
508
  success: result.success,
480
509
  result: result.result,
481
- dump: result.executionDump || result.dump || '',
482
- reportHTML: reportHTML,
483
- reportUrl: reportUrl, // 上传后的报告 URL
510
+ dump: '',
511
+ reportUrl,
484
512
  error: result.errorMessage,
485
513
  platform: request.platform || 'android',
486
514
  }, request.deviceId);
@@ -489,7 +517,7 @@ export class DebugWebSocketServer {
489
517
  type: 'debug_completed',
490
518
  sessionId,
491
519
  success: result.success,
492
- reportUrl: reportUrl,
520
+ reportUrl,
493
521
  }, request.deviceId);
494
522
  }
495
523
  /** 自然语言模式:Fork worker 进程执行 aiAction */
@@ -560,37 +588,16 @@ export class DebugWebSocketServer {
560
588
  resultHandled = true;
561
589
  const success = workerResult.success !== false;
562
590
  sessionManager.updateStatus(sessionId, success ? 'completed' : 'failed');
563
- // 尝试读取 reportFile 内容并上传到服务器
564
- let reportHTML = '';
565
- let reportUrl = null;
566
- const reportFile = workerResult.reportFile;
567
- if (reportFile) {
568
- try {
569
- if (fs.existsSync(reportFile)) {
570
- reportHTML = fs.readFileSync(reportFile, 'utf-8');
571
- logger.info(`[Debug] Read reportFile: ${reportFile}, size=${reportHTML.length}`);
572
- }
573
- else {
574
- logger.warn(`[Debug] reportFile does not exist: ${reportFile}`);
575
- }
576
- }
577
- catch (e) {
578
- logger.warn(`[Debug] Read reportFile failed: ${e.message}`);
579
- }
580
- }
581
- if (reportHTML && reportHTML.length > 0) {
582
- // 调试场景不再上传报告到服务端:reportHTML 直接通过长链接回前端
583
- logger.info(`[Debug] Skip server upload (debug mode), reportHTML size=${reportHTML.length}`);
584
- }
591
+ const reportFile = workerResult.reportFile || '';
592
+ const reportUrl = await this.safeUploadReport(reportFile, sessionId);
593
+ logger.info(`[Debug] report uploaded: reportUrl=${reportUrl || 'null'}, reportFile=${reportFile}`);
585
594
  this.sendMessage(ws, {
586
595
  type: 'debug_completed',
587
596
  sessionId,
588
597
  success,
589
- dump: workerResult.dump || '',
590
- reportFile,
591
- reportHTML,
598
+ dump: '',
592
599
  reportUrl,
593
- errorMessage: workerResult.errorMessage,
600
+ error: workerResult.errorMessage,
594
601
  }, request.deviceId);
595
602
  };
596
603
  childProcess.on('message', (message) => {
@@ -692,25 +699,23 @@ export class DebugWebSocketServer {
692
699
  const status = result.success ? 'completed' : 'failed';
693
700
  sessionManager.updateStatus(sessionId, status);
694
701
  logger.info(`[Action] Execution finished: sessionId=${sessionId}, status=${status}, ws.readyState=${ws.readyState}`);
695
- // 调试场景不再上传报告到服务端:reportHTML 直接通过长链接回前端渲染即可,
696
- // 避免上传卡住导致 action_result 永远发不出来的问题。
697
- const reportHTML = result.reportHTML || '';
698
- logger.info(`[Action] Skip server upload (debug mode), reportHTML size=${reportHTML.length}`);
702
+ const reportFile = result.reportFile || '';
703
+ const reportUrl = await this.safeUploadReport(reportFile, sessionId);
704
+ logger.info(`[Action] report uploaded: reportUrl=${reportUrl || 'null'}, reportFile=${reportFile}`);
699
705
  this.sendMessage(ws, {
700
706
  type: 'action_result',
701
707
  sessionId,
702
708
  success: result.success,
703
709
  result: result.result,
704
- dump: result.executionDump || result.dump || '',
705
- reportHTML,
706
- reportUrl: null,
710
+ dump: '',
711
+ reportUrl,
707
712
  error: result.errorMessage,
708
713
  }, request.deviceId);
709
714
  this.sendMessage(ws, {
710
715
  type: 'debug_completed',
711
716
  sessionId,
712
717
  success: result.success,
713
- reportUrl: null,
718
+ reportUrl,
714
719
  }, request.deviceId);
715
720
  logger.info(`[Action] Sent action_result + debug_completed for sessionId=${sessionId}`);
716
721
  }
@@ -718,30 +723,32 @@ export class DebugWebSocketServer {
718
723
  session.executor = undefined;
719
724
  sessionManager.updateStatus(sessionId, 'failed');
720
725
  logger.error(`[Action] Execution error: sessionId=${sessionId}, error=${error.message}, ws.readyState=${ws.readyState}`);
721
- // 异常时也尝试提取 reportHTML
722
- let reportHTML = '';
726
+ // 异常时也尝试提取 reportFile 并上传
727
+ let reportFile = '';
723
728
  try {
724
729
  const agent = session.executor?.getAgent?.() || session.agent;
725
- if (agent && typeof agent.reportHTMLString === 'function') {
726
- reportHTML = agent.reportHTMLString({ inlineScreenshots: true }) || '';
730
+ if (agent && typeof agent.reportFile === 'string') {
731
+ reportFile = agent.reportFile || '';
727
732
  }
728
733
  }
729
734
  catch (e) {
730
- logger.warn(`[Action] Failed to extract reportHTML on error: ${e.message}`);
735
+ logger.warn(`[Action] Failed to extract reportFile on error: ${e.message}`);
731
736
  }
737
+ const reportUrl = await this.safeUploadReport(reportFile, sessionId);
732
738
  this.sendMessage(ws, {
733
739
  type: 'action_result',
734
740
  sessionId,
735
741
  success: false,
736
742
  error: error.message,
737
- reportHTML,
738
743
  dump: '',
744
+ reportUrl,
739
745
  }, request.deviceId);
740
746
  // 异常时也发送 debug_completed,确保前端能退出执行中状态
741
747
  this.sendMessage(ws, {
742
748
  type: 'debug_completed',
743
749
  sessionId,
744
750
  success: false,
751
+ reportUrl,
745
752
  }, request.deviceId);
746
753
  }
747
754
  }
@@ -759,8 +766,8 @@ export class DebugWebSocketServer {
759
766
  if (finalSent)
760
767
  return;
761
768
  finalSent = true;
762
- const safeReportHTML = typeof payload.reportHTML === 'string' ? payload.reportHTML : '';
763
- // 第一次尝试:带 reportHTML 一起发
769
+ const reportUrl = payload.reportUrl || null;
770
+ // 第一次尝试
764
771
  try {
765
772
  this.sendMessage(ws, {
766
773
  type: 'ai_act_result',
@@ -769,15 +776,13 @@ export class DebugWebSocketServer {
769
776
  success: payload.success,
770
777
  result: payload.result,
771
778
  dump: '',
772
- reportHTML: safeReportHTML,
773
- reportUrl: null,
779
+ reportUrl,
774
780
  error: payload.error,
775
781
  platform: request.platform || 'android',
776
782
  }, request.deviceId);
777
783
  }
778
784
  catch (e1) {
779
- logger.error(`[AiAct] sendFinal ai_act_result failed: ${e1.message}, retry without reportHTML`);
780
- // 二次降级:去掉 reportHTML 再发一次,最大限度保证前端拿到结束信号
785
+ logger.error(`[AiAct] sendFinal ai_act_result failed: ${e1.message}, retry`);
781
786
  try {
782
787
  this.sendMessage(ws, {
783
788
  type: 'ai_act_result',
@@ -786,14 +791,13 @@ export class DebugWebSocketServer {
786
791
  success: payload.success,
787
792
  result: payload.result,
788
793
  dump: '',
789
- reportHTML: '',
790
- reportUrl: null,
791
- error: (payload.error || '') + ' [reportHTML dropped: send failed]',
794
+ reportUrl,
795
+ error: (payload.error || '') + ' [send failed]',
792
796
  platform: request.platform || 'android',
793
797
  }, request.deviceId);
794
798
  }
795
799
  catch (e2) {
796
- logger.error(`[AiAct] sendFinal degraded ai_act_result also failed: ${e2.message}`);
800
+ logger.error(`[AiAct] sendFinal retry also failed: ${e2.message}`);
797
801
  }
798
802
  }
799
803
  // 不论上面成败,debug_completed 必须发
@@ -802,23 +806,24 @@ export class DebugWebSocketServer {
802
806
  type: 'debug_completed',
803
807
  sessionId,
804
808
  success: payload.success,
809
+ reportUrl,
805
810
  }, request.deviceId);
806
- logger.info(`[AiAct] Sent ai_act_result + debug_completed for sessionId=${sessionId}, success=${payload.success}`);
811
+ logger.info(`[AiAct] Sent ai_act_result + debug_completed for sessionId=${sessionId}, success=${payload.success}, reportUrl=${reportUrl || 'null'}`);
807
812
  }
808
813
  catch (e3) {
809
814
  logger.error(`[AiAct] Failed to send debug_completed: ${e3.message}`);
810
815
  }
811
816
  };
812
- // 安全提取 reportHTML,单独 try/catch,绝不向上抛
813
- const safeExtractReport = () => {
817
+ // 安全提取 reportFile 路径,单独 try/catch,绝不向上抛
818
+ const safeExtractReportFile = () => {
814
819
  try {
815
820
  const agent = session.executor?.getAgent?.() || session.agent;
816
- if (agent && typeof agent.reportHTMLString === 'function') {
817
- return agent.reportHTMLString({ inlineScreenshots: true }) || '';
821
+ if (agent && typeof agent.reportFile === 'string') {
822
+ return agent.reportFile || '';
818
823
  }
819
824
  }
820
825
  catch (e) {
821
- logger.warn(`[AiAct] safeExtractReport failed: ${e.message}`);
826
+ logger.warn(`[AiAct] safeExtractReportFile failed: ${e.message}`);
822
827
  }
823
828
  return '';
824
829
  };
@@ -868,10 +873,12 @@ export class DebugWebSocketServer {
868
873
  const status = result.success ? 'completed' : 'failed';
869
874
  sessionManager.updateStatus(sessionId, status);
870
875
  logger.info(`[AiAct] Execution finished: sessionId=${sessionId}, status=${status}, ws.readyState=${ws.readyState}`);
876
+ const reportFile = result.reportFile || safeExtractReportFile();
877
+ const reportUrl = await this.safeUploadReport(reportFile, sessionId);
871
878
  sendFinal({
872
879
  success: result.success,
873
880
  result: result.result,
874
- reportHTML: result.reportHTML || safeExtractReport(),
881
+ reportUrl,
875
882
  error: result.errorMessage,
876
883
  });
877
884
  }
@@ -879,20 +886,30 @@ export class DebugWebSocketServer {
879
886
  session.executor = undefined;
880
887
  sessionManager.updateStatus(sessionId, 'failed');
881
888
  logger.error(`[AiAct] Execution error: sessionId=${sessionId}, error=${error.message}, ws.readyState=${ws.readyState}`);
889
+ const reportFile = safeExtractReportFile();
890
+ const reportUrl = await this.safeUploadReport(reportFile, sessionId);
882
891
  sendFinal({
883
892
  success: false,
884
893
  error: error.message,
885
- reportHTML: safeExtractReport(),
894
+ reportUrl,
886
895
  });
887
896
  }
888
897
  finally {
889
898
  // 兜底:上面任何分支因任何原因没发结束消息,这里最后补一刀
890
899
  if (!finalSent) {
891
900
  logger.warn(`[AiAct] finally fallback: no final message sent yet for sessionId=${sessionId}, sending now`);
901
+ const reportFile = safeExtractReportFile();
902
+ let reportUrl = null;
903
+ try {
904
+ reportUrl = await this.safeUploadReport(reportFile, sessionId);
905
+ }
906
+ catch (e) {
907
+ logger.error(`[AiAct] finally fallback upload error: ${e.message}`);
908
+ }
892
909
  sendFinal({
893
910
  success: false,
894
911
  error: 'aiAct ended without explicit result (fallback in finally)',
895
- reportHTML: safeExtractReport(),
912
+ reportUrl,
896
913
  });
897
914
  }
898
915
  }
@@ -1202,31 +1219,24 @@ export class DebugWebSocketServer {
1202
1219
  default:
1203
1220
  throw new Error(`Unknown web action type: ${request.actionType}`);
1204
1221
  }
1205
- // 拉取报告
1206
- let dumpString = '';
1207
- let reportHTML = '';
1222
+ // 拉取报告文件路径
1223
+ let reportFile = '';
1208
1224
  try {
1209
- if (typeof agent.dumpDataString === 'function') {
1210
- dumpString = agent.dumpDataString({ inlineScreenshots: true });
1211
- }
1212
- if (typeof agent.reportHTMLString === 'function') {
1213
- reportHTML = agent.reportHTMLString({ inlineScreenshots: true });
1225
+ if (typeof agent.reportFile === 'string') {
1226
+ reportFile = agent.reportFile || '';
1214
1227
  }
1215
1228
  }
1216
1229
  catch (e) {
1217
- logger.warn(`[WebAction] dump/report extract failed: ${e.message}`);
1230
+ logger.warn(`[WebAction] reportFile extract failed: ${e.message}`);
1218
1231
  }
1219
1232
  // runCode 执行中有错误(如断言失败),标记失败但正常返回报告
1220
1233
  const runCodeError = request.__runCodeError;
1221
1234
  const actionSuccess = !runCodeError;
1222
1235
  sessionManager.updateStatus(sessionId, actionSuccess ? 'completed' : 'failed');
1223
1236
  stopWebDumpInterval();
1224
- // 上传 reportHTML 到服务器,拿到 reportUrl
1225
- const reportUrl = null;
1226
- if (reportHTML && reportHTML.length > 0) {
1227
- // 调试场景不再上传报告到服务端:reportHTML 直接通过长链接回前端
1228
- logger.info(`[WebAction] Skip server upload (debug mode), reportHTML size=${reportHTML.length}`);
1229
- }
1237
+ // 上传报告文件到服务端,拿到 reportUrl
1238
+ const reportUrl = await this.safeUploadReport(reportFile, sessionId);
1239
+ logger.info(`[WebAction] report uploaded: reportUrl=${reportUrl || 'null'}, reportFile=${reportFile}`);
1230
1240
  // 发送 web_action_result
1231
1241
  this.sendMessage(ws, {
1232
1242
  type: 'web_action_result',
@@ -1234,8 +1244,7 @@ export class DebugWebSocketServer {
1234
1244
  actionType: request.actionType,
1235
1245
  success: actionSuccess,
1236
1246
  result,
1237
- dump: dumpString || '',
1238
- reportHTML: reportHTML || '',
1247
+ dump: '',
1239
1248
  reportUrl,
1240
1249
  error: runCodeError || undefined,
1241
1250
  });
@@ -1246,8 +1255,7 @@ export class DebugWebSocketServer {
1246
1255
  actionType: request.actionType,
1247
1256
  success: actionSuccess,
1248
1257
  result,
1249
- dump: dumpString || '',
1250
- reportHTML: reportHTML || '',
1258
+ dump: '',
1251
1259
  reportUrl,
1252
1260
  error: runCodeError || undefined,
1253
1261
  platform: 'web',
@@ -1269,41 +1277,28 @@ export class DebugWebSocketServer {
1269
1277
  clearInterval(session.dumpIntervalId);
1270
1278
  session.dumpIntervalId = undefined;
1271
1279
  }
1272
- // 失败时也尝试提取 dump / reportHTML
1273
- let dumpString = '';
1274
- let reportHTML = '';
1280
+ // 失败时也尝试提取 reportFile 并上传
1281
+ let reportFile = '';
1275
1282
  const agentForReport = session?.webAgent;
1276
1283
  try {
1277
- if (agentForReport && typeof agentForReport.dumpDataString === 'function') {
1278
- dumpString = agentForReport.dumpDataString({ inlineScreenshots: true });
1279
- }
1280
- }
1281
- catch (dumpErr) {
1282
- logger.warn(`[WebAction] (error case) dump extract failed: ${dumpErr.message}`);
1283
- }
1284
- try {
1285
- if (agentForReport && typeof agentForReport.reportHTMLString === 'function') {
1286
- reportHTML = agentForReport.reportHTMLString({ inlineScreenshots: true });
1284
+ if (agentForReport && typeof agentForReport.reportFile === 'string') {
1285
+ reportFile = agentForReport.reportFile || '';
1287
1286
  }
1288
1287
  }
1289
1288
  catch (reportErr) {
1290
- logger.warn(`[WebAction] (error case) report extract failed: ${reportErr.message}`);
1291
- }
1292
- // 异常路径:调试场景不再上传报告到服务端
1293
- const reportUrl = null;
1294
- if (reportHTML && reportHTML.length > 0) {
1295
- logger.info(`[WebAction] (error case) Skip server upload (debug mode), reportHTML size=${reportHTML.length}`);
1289
+ logger.warn(`[WebAction] (error case) reportFile extract failed: ${reportErr.message}`);
1296
1290
  }
1291
+ // 上传报告文件到服务端,拿到 reportUrl
1292
+ const reportUrl = await this.safeUploadReport(reportFile, sessionId);
1293
+ logger.info(`[WebAction] (error case) report uploaded: reportUrl=${reportUrl || 'null'}, reportFile=${reportFile}`);
1297
1294
  this.sendMessage(ws, {
1298
1295
  type: 'web_action_result',
1299
1296
  sessionId,
1300
1297
  actionType: request.actionType,
1301
1298
  success: false,
1302
1299
  error: error.message,
1303
- dump: dumpString || '',
1304
- reportHTML: reportHTML || '',
1300
+ dump: '',
1305
1301
  reportUrl,
1306
- agentStatus: session?.webAgent ? 'initialized' : 'not_initialized',
1307
1302
  });
1308
1303
  // 与移动端对齐:同步发一份 action_result
1309
1304
  this.sendMessage(ws, {
@@ -1312,10 +1307,8 @@ export class DebugWebSocketServer {
1312
1307
  actionType: request.actionType,
1313
1308
  success: false,
1314
1309
  error: error.message,
1315
- dump: dumpString || '',
1316
- reportHTML: reportHTML || '',
1310
+ dump: '',
1317
1311
  reportUrl,
1318
- agentStatus: session?.webAgent ? 'initialized' : 'not_initialized',
1319
1312
  platform: 'web',
1320
1313
  });
1321
1314
  // 必须发 debug_completed,前端依赖它退出执行中状态
@@ -1444,26 +1437,16 @@ export class DebugWebSocketServer {
1444
1437
  return;
1445
1438
  }
1446
1439
  // 仿照 playground cancelTask:先获取当前执行数据,再 destroy agent
1447
- let dump = '';
1448
- let reportHTML = '';
1440
+ let reportFile = '';
1449
1441
  const agent = session.executor?.getAgent?.() || session.agent;
1450
- // 获取 dump 数据(必须在 destroy 之前,因为 dump 存储在 agent 内存中)
1442
+ // 获取 reportFile 路径
1451
1443
  try {
1452
- if (agent && typeof agent.dumpDataString === 'function') {
1453
- dump = agent.dumpDataString({ inlineScreenshots: true }) || '';
1444
+ if (agent && typeof agent.reportFile === 'string') {
1445
+ reportFile = agent.reportFile || '';
1454
1446
  }
1455
1447
  }
1456
1448
  catch (error) {
1457
- logger.warn(`Failed to get dump before cancel: ${error.message}`);
1458
- }
1459
- // 获取 reportHTML
1460
- try {
1461
- if (agent && typeof agent.reportHTMLString === 'function') {
1462
- reportHTML = agent.reportHTMLString({ inlineScreenshots: true }) || '';
1463
- }
1464
- }
1465
- catch (error) {
1466
- logger.warn(`Failed to get reportHTML before cancel: ${error.message}`);
1449
+ logger.warn(`Failed to get reportFile before cancel: ${error.message}`);
1467
1450
  }
1468
1451
  // 中断主进程内的执行(execute_action / execute_ai_act)
1469
1452
  if (session.abortController && !session.abortController.signal.aborted) {
@@ -1506,12 +1489,14 @@ export class DebugWebSocketServer {
1506
1489
  // 关闭 web 浏览器(如果存在)
1507
1490
  await this.closeWebSession(request.sessionId);
1508
1491
  sessionManager.updateStatus(request.sessionId, 'stopped');
1509
- // 发送带执行数据的停止消息(仿照 playground cancel 返回 dump + reportHTML)
1492
+ // 上传报告文件到服务端,拿到 reportUrl
1493
+ const reportUrl = await this.safeUploadReport(reportFile, request.sessionId);
1494
+ logger.info(`[StopDebug] report uploaded: reportUrl=${reportUrl || 'null'}, reportFile=${reportFile}`);
1495
+ // 发送带执行数据的停止消息
1510
1496
  this.sendMessage(ws, {
1511
1497
  type: 'debug_stopped',
1512
1498
  sessionId: request.sessionId,
1513
- dump,
1514
- reportHTML,
1499
+ reportUrl,
1515
1500
  });
1516
1501
  // 同时发送 action_result,让前端能获取到停止时的执行数据
1517
1502
  this.sendMessage(ws, {
@@ -1519,14 +1504,15 @@ export class DebugWebSocketServer {
1519
1504
  sessionId: request.sessionId,
1520
1505
  success: false,
1521
1506
  error: 'Action stopped by user',
1522
- dump,
1523
- reportHTML,
1507
+ dump: '',
1508
+ reportUrl,
1524
1509
  });
1525
1510
  // 发送 debug_completed
1526
1511
  this.sendMessage(ws, {
1527
1512
  type: 'debug_completed',
1528
1513
  sessionId: request.sessionId,
1529
1514
  success: false,
1515
+ reportUrl,
1530
1516
  });
1531
1517
  }
1532
1518
  // ==================== Get Logs ====================
@@ -1539,6 +1525,39 @@ export class DebugWebSocketServer {
1539
1525
  this.sendMessage(ws, { type: 'all_logs', sessionId: request.sessionId, logs: session.logs });
1540
1526
  }
1541
1527
  // ==================== Utilities ====================
1528
+ /**
1529
+ * 安全上传报告到服务端,返回 reportUrl;任何内部异常都 catch 后返回 null,绝不抛
1530
+ */
1531
+ async safeUploadReport(reportFile, sessionId) {
1532
+ if (!reportFile)
1533
+ return null;
1534
+ const uploadUrl = this.config?.callback?.debugReportUploadUrl;
1535
+ if (!uploadUrl) {
1536
+ logger.warn(`[UploadReport] debugReportUploadUrl not configured, skip upload (sessionId=${sessionId})`);
1537
+ return null;
1538
+ }
1539
+ const maxAttempts = 2;
1540
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1541
+ try {
1542
+ const url = await uploadReportToServer(reportFile, sessionId, uploadUrl);
1543
+ if (url)
1544
+ return url;
1545
+ // uploadReportToServer 返回 null 说明响应缺少 reportUrl,也算失败,重试一次
1546
+ if (attempt < maxAttempts) {
1547
+ logger.warn(`[UploadReport] Upload returned no reportUrl (attempt ${attempt}/${maxAttempts}), retrying...`);
1548
+ }
1549
+ }
1550
+ catch (e) {
1551
+ if (attempt < maxAttempts) {
1552
+ logger.warn(`[UploadReport] Upload failed (attempt ${attempt}/${maxAttempts}): ${e.message}, retrying...`);
1553
+ }
1554
+ else {
1555
+ logger.error(`[UploadReport] Upload failed after ${maxAttempts} attempts: ${e.message}`);
1556
+ }
1557
+ }
1558
+ }
1559
+ return null;
1560
+ }
1542
1561
  /**
1543
1562
  * 将前端传来的 modelConfig 应用到环境变量
1544
1563
  * ActionExecutor 通过环境变量读取模型配置,所以需要同步更新
@@ -1572,22 +1591,34 @@ export class DebugWebSocketServer {
1572
1591
  timestamp: new Date().toISOString(),
1573
1592
  };
1574
1593
  if (ws.readyState === ws.OPEN) {
1575
- const payload = JSON.stringify(msg);
1576
- // 大数据消息(如 reportHTML)截断,避免超出 WebSocket 帧大小限制
1594
+ let payload = JSON.stringify(msg);
1595
+ // WSS 自身 maxPayload=10MB(接收端),发送端我们保留 2MB 余量以兼容反代/网关
1577
1596
  const MAX_PAYLOAD_SIZE = 2 * 1024 * 1024; // 2MB
1578
1597
  if (payload.length > MAX_PAYLOAD_SIZE) {
1579
- logger.warn(`[WS] Message too large (${(payload.length / 1024 / 1024).toFixed(2)}MB), type=${message.type}, truncating reportHTML/dump`);
1580
- // 截断 reportHTML 和 dump 字段
1581
- if (msg.reportHTML && typeof msg.reportHTML === 'string' && msg.reportHTML.length > 512 * 1024) {
1582
- msg.reportHTML = msg.reportHTML.substring(0, 512 * 1024) + '\n...[truncated]';
1583
- }
1598
+ const originalSize = payload.length;
1599
+ let truncated = false;
1584
1600
  if (msg.dump && typeof msg.dump === 'string' && msg.dump.length > 512 * 1024) {
1585
1601
  msg.dump = msg.dump.substring(0, 512 * 1024) + '\n...[truncated]';
1602
+ truncated = true;
1603
+ }
1604
+ // 关键修复:截断后必须重新序列化,否则 ws.send 发的还是旧的大 payload
1605
+ if (truncated) {
1606
+ payload = JSON.stringify(msg);
1607
+ }
1608
+ if (payload.length > MAX_PAYLOAD_SIZE) {
1609
+ if (msg.dump)
1610
+ msg.dump = '';
1611
+ const prevError = typeof msg.error === 'string' ? msg.error : '';
1612
+ msg.error = (prevError ? prevError + ' | ' : '') + `report dropped (oversize: ${(originalSize / 1024 / 1024).toFixed(2)}MB > ${(MAX_PAYLOAD_SIZE / 1024 / 1024)}MB)`;
1613
+ msg.reportOversize = true;
1614
+ msg.reportOversizeBytes = originalSize;
1615
+ payload = JSON.stringify(msg);
1586
1616
  }
1617
+ logger.warn(`[WS] Message too large (orig=${(originalSize / 1024 / 1024).toFixed(2)}MB, sent=${(payload.length / 1024 / 1024).toFixed(2)}MB), type=${message.type}, sessionId=${message.sessionId}`);
1587
1618
  }
1588
1619
  ws.send(payload, (err) => {
1589
1620
  if (err) {
1590
- logger.error(`[WS] Send failed: type=${message.type}, error=${err.message}`);
1621
+ logger.error(`[WS] Send failed: type=${message.type}, sessionId=${message.sessionId}, error=${err.message}`);
1591
1622
  }
1592
1623
  });
1593
1624
  }