@aiscene/aiserver 1.4.2 → 1.4.4

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
  }
@@ -753,6 +760,73 @@ export class DebugWebSocketServer {
753
760
  session.abortController = abortController;
754
761
  // 将前端传来的 modelConfig 应用到环境变量
755
762
  this.applyModelConfig(request.modelConfig);
763
+ // 哨兵:保证 ai_act_result + debug_completed 一定且只发一次(即便发送时再次抛错)
764
+ let finalSent = false;
765
+ const sendFinal = (payload) => {
766
+ if (finalSent)
767
+ return;
768
+ finalSent = true;
769
+ const reportUrl = payload.reportUrl || null;
770
+ // 第一次尝试
771
+ try {
772
+ this.sendMessage(ws, {
773
+ type: 'ai_act_result',
774
+ sessionId,
775
+ actionType: 'aiAct',
776
+ success: payload.success,
777
+ result: payload.result,
778
+ dump: '',
779
+ reportUrl,
780
+ error: payload.error,
781
+ platform: request.platform || 'android',
782
+ }, request.deviceId);
783
+ }
784
+ catch (e1) {
785
+ logger.error(`[AiAct] sendFinal ai_act_result failed: ${e1.message}, retry`);
786
+ try {
787
+ this.sendMessage(ws, {
788
+ type: 'ai_act_result',
789
+ sessionId,
790
+ actionType: 'aiAct',
791
+ success: payload.success,
792
+ result: payload.result,
793
+ dump: '',
794
+ reportUrl,
795
+ error: (payload.error || '') + ' [send failed]',
796
+ platform: request.platform || 'android',
797
+ }, request.deviceId);
798
+ }
799
+ catch (e2) {
800
+ logger.error(`[AiAct] sendFinal retry also failed: ${e2.message}`);
801
+ }
802
+ }
803
+ // 不论上面成败,debug_completed 必须发
804
+ try {
805
+ this.sendMessage(ws, {
806
+ type: 'debug_completed',
807
+ sessionId,
808
+ success: payload.success,
809
+ reportUrl,
810
+ }, request.deviceId);
811
+ logger.info(`[AiAct] Sent ai_act_result + debug_completed for sessionId=${sessionId}, success=${payload.success}, reportUrl=${reportUrl || 'null'}`);
812
+ }
813
+ catch (e3) {
814
+ logger.error(`[AiAct] Failed to send debug_completed: ${e3.message}`);
815
+ }
816
+ };
817
+ // 安全提取 reportFile 路径,单独 try/catch,绝不向上抛
818
+ const safeExtractReportFile = () => {
819
+ try {
820
+ const agent = session.executor?.getAgent?.() || session.agent;
821
+ if (agent && typeof agent.reportFile === 'string') {
822
+ return agent.reportFile || '';
823
+ }
824
+ }
825
+ catch (e) {
826
+ logger.warn(`[AiAct] safeExtractReportFile failed: ${e.message}`);
827
+ }
828
+ return '';
829
+ };
756
830
  try {
757
831
  sessionManager.updateStatus(sessionId, 'running');
758
832
  this.sendMessage(ws, { type: 'ai_act_started', sessionId }, request.deviceId);
@@ -789,62 +863,55 @@ export class DebugWebSocketServer {
789
863
  }, { once: true });
790
864
  });
791
865
  const result = await Promise.race([executePromise, abortPromise]);
792
- // 如果是用户主动停止,由 handleStopDebug 统一发送结果消息,此处不再重复发送
866
+ // 用户主动停止由 handleStopDebug 统一发送结果消息,这里不重复发;同时把 finalSent 置位防止 finally 兜底再发
793
867
  if (abortController.signal.aborted) {
868
+ finalSent = true;
794
869
  logger.info(`[AiAct] Execution aborted by user, skipping result messages (handleStopDebug will send them)`);
795
870
  return;
796
871
  }
797
- // 清理 executor 引用
798
872
  session.executor = undefined;
799
873
  const status = result.success ? 'completed' : 'failed';
800
874
  sessionManager.updateStatus(sessionId, status);
801
875
  logger.info(`[AiAct] Execution finished: sessionId=${sessionId}, status=${status}, ws.readyState=${ws.readyState}`);
802
- this.sendMessage(ws, {
803
- type: 'ai_act_result',
804
- sessionId,
876
+ const reportFile = result.reportFile || safeExtractReportFile();
877
+ const reportUrl = await this.safeUploadReport(reportFile, sessionId);
878
+ sendFinal({
805
879
  success: result.success,
806
880
  result: result.result,
807
- dump: result.executionDump || result.dump || '',
808
- reportHTML: result.reportHTML || '',
881
+ reportUrl,
809
882
  error: result.errorMessage,
810
- }, request.deviceId);
811
- // 发送 debug_completed,让前端能正确识别执行完成状态
812
- this.sendMessage(ws, {
813
- type: 'debug_completed',
814
- sessionId,
815
- success: result.success,
816
- }, request.deviceId);
817
- logger.info(`[AiAct] Sent ai_act_result + debug_completed for sessionId=${sessionId}`);
883
+ });
818
884
  }
819
885
  catch (error) {
820
886
  session.executor = undefined;
821
887
  sessionManager.updateStatus(sessionId, 'failed');
822
888
  logger.error(`[AiAct] Execution error: sessionId=${sessionId}, error=${error.message}, ws.readyState=${ws.readyState}`);
823
- // 异常时也尝试提取 reportHTML
824
- let reportHTML = '';
825
- try {
826
- const agent = session.executor?.getAgent?.() || session.agent;
827
- if (agent && typeof agent.reportHTMLString === 'function') {
828
- reportHTML = agent.reportHTMLString({ inlineScreenshots: true }) || '';
829
- }
830
- }
831
- catch (e) {
832
- logger.warn(`[AiAct] Failed to extract reportHTML on error: ${e.message}`);
833
- }
834
- this.sendMessage(ws, {
835
- type: 'ai_act_result',
836
- sessionId,
889
+ const reportFile = safeExtractReportFile();
890
+ const reportUrl = await this.safeUploadReport(reportFile, sessionId);
891
+ sendFinal({
837
892
  success: false,
838
893
  error: error.message,
839
- reportHTML,
840
- dump: '',
841
- }, request.deviceId);
842
- // 异常时也发送 debug_completed,确保前端能退出执行中状态
843
- this.sendMessage(ws, {
844
- type: 'debug_completed',
845
- sessionId,
846
- success: false,
847
- }, request.deviceId);
894
+ reportUrl,
895
+ });
896
+ }
897
+ finally {
898
+ // 兜底:上面任何分支因任何原因没发结束消息,这里最后补一刀
899
+ if (!finalSent) {
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
+ }
909
+ sendFinal({
910
+ success: false,
911
+ error: 'aiAct ended without explicit result (fallback in finally)',
912
+ reportUrl,
913
+ });
914
+ }
848
915
  }
849
916
  }
850
917
  // ==================== Execute Web Action (Playwright + Midscene, with screencast) ====================
@@ -1152,31 +1219,24 @@ export class DebugWebSocketServer {
1152
1219
  default:
1153
1220
  throw new Error(`Unknown web action type: ${request.actionType}`);
1154
1221
  }
1155
- // 拉取报告
1156
- let dumpString = '';
1157
- let reportHTML = '';
1222
+ // 拉取报告文件路径
1223
+ let reportFile = '';
1158
1224
  try {
1159
- if (typeof agent.dumpDataString === 'function') {
1160
- dumpString = agent.dumpDataString({ inlineScreenshots: true });
1161
- }
1162
- if (typeof agent.reportHTMLString === 'function') {
1163
- reportHTML = agent.reportHTMLString({ inlineScreenshots: true });
1225
+ if (typeof agent.reportFile === 'string') {
1226
+ reportFile = agent.reportFile || '';
1164
1227
  }
1165
1228
  }
1166
1229
  catch (e) {
1167
- logger.warn(`[WebAction] dump/report extract failed: ${e.message}`);
1230
+ logger.warn(`[WebAction] reportFile extract failed: ${e.message}`);
1168
1231
  }
1169
1232
  // runCode 执行中有错误(如断言失败),标记失败但正常返回报告
1170
1233
  const runCodeError = request.__runCodeError;
1171
1234
  const actionSuccess = !runCodeError;
1172
1235
  sessionManager.updateStatus(sessionId, actionSuccess ? 'completed' : 'failed');
1173
1236
  stopWebDumpInterval();
1174
- // 上传 reportHTML 到服务器,拿到 reportUrl
1175
- const reportUrl = null;
1176
- if (reportHTML && reportHTML.length > 0) {
1177
- // 调试场景不再上传报告到服务端:reportHTML 直接通过长链接回前端
1178
- logger.info(`[WebAction] Skip server upload (debug mode), reportHTML size=${reportHTML.length}`);
1179
- }
1237
+ // 上传报告文件到服务端,拿到 reportUrl
1238
+ const reportUrl = await this.safeUploadReport(reportFile, sessionId);
1239
+ logger.info(`[WebAction] report uploaded: reportUrl=${reportUrl || 'null'}, reportFile=${reportFile}`);
1180
1240
  // 发送 web_action_result
1181
1241
  this.sendMessage(ws, {
1182
1242
  type: 'web_action_result',
@@ -1184,8 +1244,7 @@ export class DebugWebSocketServer {
1184
1244
  actionType: request.actionType,
1185
1245
  success: actionSuccess,
1186
1246
  result,
1187
- dump: dumpString || '',
1188
- reportHTML: reportHTML || '',
1247
+ dump: '',
1189
1248
  reportUrl,
1190
1249
  error: runCodeError || undefined,
1191
1250
  });
@@ -1196,8 +1255,7 @@ export class DebugWebSocketServer {
1196
1255
  actionType: request.actionType,
1197
1256
  success: actionSuccess,
1198
1257
  result,
1199
- dump: dumpString || '',
1200
- reportHTML: reportHTML || '',
1258
+ dump: '',
1201
1259
  reportUrl,
1202
1260
  error: runCodeError || undefined,
1203
1261
  platform: 'web',
@@ -1219,41 +1277,28 @@ export class DebugWebSocketServer {
1219
1277
  clearInterval(session.dumpIntervalId);
1220
1278
  session.dumpIntervalId = undefined;
1221
1279
  }
1222
- // 失败时也尝试提取 dump / reportHTML
1223
- let dumpString = '';
1224
- let reportHTML = '';
1280
+ // 失败时也尝试提取 reportFile 并上传
1281
+ let reportFile = '';
1225
1282
  const agentForReport = session?.webAgent;
1226
1283
  try {
1227
- if (agentForReport && typeof agentForReport.dumpDataString === 'function') {
1228
- dumpString = agentForReport.dumpDataString({ inlineScreenshots: true });
1229
- }
1230
- }
1231
- catch (dumpErr) {
1232
- logger.warn(`[WebAction] (error case) dump extract failed: ${dumpErr.message}`);
1233
- }
1234
- try {
1235
- if (agentForReport && typeof agentForReport.reportHTMLString === 'function') {
1236
- reportHTML = agentForReport.reportHTMLString({ inlineScreenshots: true });
1284
+ if (agentForReport && typeof agentForReport.reportFile === 'string') {
1285
+ reportFile = agentForReport.reportFile || '';
1237
1286
  }
1238
1287
  }
1239
1288
  catch (reportErr) {
1240
- logger.warn(`[WebAction] (error case) report extract failed: ${reportErr.message}`);
1241
- }
1242
- // 异常路径:调试场景不再上传报告到服务端
1243
- const reportUrl = null;
1244
- if (reportHTML && reportHTML.length > 0) {
1245
- 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}`);
1246
1290
  }
1291
+ // 上传报告文件到服务端,拿到 reportUrl
1292
+ const reportUrl = await this.safeUploadReport(reportFile, sessionId);
1293
+ logger.info(`[WebAction] (error case) report uploaded: reportUrl=${reportUrl || 'null'}, reportFile=${reportFile}`);
1247
1294
  this.sendMessage(ws, {
1248
1295
  type: 'web_action_result',
1249
1296
  sessionId,
1250
1297
  actionType: request.actionType,
1251
1298
  success: false,
1252
1299
  error: error.message,
1253
- dump: dumpString || '',
1254
- reportHTML: reportHTML || '',
1300
+ dump: '',
1255
1301
  reportUrl,
1256
- agentStatus: session?.webAgent ? 'initialized' : 'not_initialized',
1257
1302
  });
1258
1303
  // 与移动端对齐:同步发一份 action_result
1259
1304
  this.sendMessage(ws, {
@@ -1262,10 +1307,8 @@ export class DebugWebSocketServer {
1262
1307
  actionType: request.actionType,
1263
1308
  success: false,
1264
1309
  error: error.message,
1265
- dump: dumpString || '',
1266
- reportHTML: reportHTML || '',
1310
+ dump: '',
1267
1311
  reportUrl,
1268
- agentStatus: session?.webAgent ? 'initialized' : 'not_initialized',
1269
1312
  platform: 'web',
1270
1313
  });
1271
1314
  // 必须发 debug_completed,前端依赖它退出执行中状态
@@ -1394,26 +1437,16 @@ export class DebugWebSocketServer {
1394
1437
  return;
1395
1438
  }
1396
1439
  // 仿照 playground cancelTask:先获取当前执行数据,再 destroy agent
1397
- let dump = '';
1398
- let reportHTML = '';
1440
+ let reportFile = '';
1399
1441
  const agent = session.executor?.getAgent?.() || session.agent;
1400
- // 获取 dump 数据(必须在 destroy 之前,因为 dump 存储在 agent 内存中)
1401
- try {
1402
- if (agent && typeof agent.dumpDataString === 'function') {
1403
- dump = agent.dumpDataString({ inlineScreenshots: true }) || '';
1404
- }
1405
- }
1406
- catch (error) {
1407
- logger.warn(`Failed to get dump before cancel: ${error.message}`);
1408
- }
1409
- // 获取 reportHTML
1442
+ // 获取 reportFile 路径
1410
1443
  try {
1411
- if (agent && typeof agent.reportHTMLString === 'function') {
1412
- reportHTML = agent.reportHTMLString({ inlineScreenshots: true }) || '';
1444
+ if (agent && typeof agent.reportFile === 'string') {
1445
+ reportFile = agent.reportFile || '';
1413
1446
  }
1414
1447
  }
1415
1448
  catch (error) {
1416
- logger.warn(`Failed to get reportHTML before cancel: ${error.message}`);
1449
+ logger.warn(`Failed to get reportFile before cancel: ${error.message}`);
1417
1450
  }
1418
1451
  // 中断主进程内的执行(execute_action / execute_ai_act)
1419
1452
  if (session.abortController && !session.abortController.signal.aborted) {
@@ -1456,12 +1489,14 @@ export class DebugWebSocketServer {
1456
1489
  // 关闭 web 浏览器(如果存在)
1457
1490
  await this.closeWebSession(request.sessionId);
1458
1491
  sessionManager.updateStatus(request.sessionId, 'stopped');
1459
- // 发送带执行数据的停止消息(仿照 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
+ // 发送带执行数据的停止消息
1460
1496
  this.sendMessage(ws, {
1461
1497
  type: 'debug_stopped',
1462
1498
  sessionId: request.sessionId,
1463
- dump,
1464
- reportHTML,
1499
+ reportUrl,
1465
1500
  });
1466
1501
  // 同时发送 action_result,让前端能获取到停止时的执行数据
1467
1502
  this.sendMessage(ws, {
@@ -1469,14 +1504,15 @@ export class DebugWebSocketServer {
1469
1504
  sessionId: request.sessionId,
1470
1505
  success: false,
1471
1506
  error: 'Action stopped by user',
1472
- dump,
1473
- reportHTML,
1507
+ dump: '',
1508
+ reportUrl,
1474
1509
  });
1475
1510
  // 发送 debug_completed
1476
1511
  this.sendMessage(ws, {
1477
1512
  type: 'debug_completed',
1478
1513
  sessionId: request.sessionId,
1479
1514
  success: false,
1515
+ reportUrl,
1480
1516
  });
1481
1517
  }
1482
1518
  // ==================== Get Logs ====================
@@ -1489,6 +1525,39 @@ export class DebugWebSocketServer {
1489
1525
  this.sendMessage(ws, { type: 'all_logs', sessionId: request.sessionId, logs: session.logs });
1490
1526
  }
1491
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
+ }
1492
1561
  /**
1493
1562
  * 将前端传来的 modelConfig 应用到环境变量
1494
1563
  * ActionExecutor 通过环境变量读取模型配置,所以需要同步更新
@@ -1522,22 +1591,34 @@ export class DebugWebSocketServer {
1522
1591
  timestamp: new Date().toISOString(),
1523
1592
  };
1524
1593
  if (ws.readyState === ws.OPEN) {
1525
- const payload = JSON.stringify(msg);
1526
- // 大数据消息(如 reportHTML)截断,避免超出 WebSocket 帧大小限制
1594
+ let payload = JSON.stringify(msg);
1595
+ // WSS 自身 maxPayload=10MB(接收端),发送端我们保留 2MB 余量以兼容反代/网关
1527
1596
  const MAX_PAYLOAD_SIZE = 2 * 1024 * 1024; // 2MB
1528
1597
  if (payload.length > MAX_PAYLOAD_SIZE) {
1529
- logger.warn(`[WS] Message too large (${(payload.length / 1024 / 1024).toFixed(2)}MB), type=${message.type}, truncating reportHTML/dump`);
1530
- // 截断 reportHTML 和 dump 字段
1531
- if (msg.reportHTML && typeof msg.reportHTML === 'string' && msg.reportHTML.length > 512 * 1024) {
1532
- msg.reportHTML = msg.reportHTML.substring(0, 512 * 1024) + '\n...[truncated]';
1533
- }
1598
+ const originalSize = payload.length;
1599
+ let truncated = false;
1534
1600
  if (msg.dump && typeof msg.dump === 'string' && msg.dump.length > 512 * 1024) {
1535
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);
1536
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}`);
1537
1618
  }
1538
1619
  ws.send(payload, (err) => {
1539
1620
  if (err) {
1540
- 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}`);
1541
1622
  }
1542
1623
  });
1543
1624
  }