@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.
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +1 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +1 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/core/types.d.ts +0 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/debug/websocket-server.d.ts +4 -0
- package/dist/debug/websocket-server.d.ts.map +1 -1
- package/dist/debug/websocket-server.js +259 -178
- package/dist/debug/websocket-server.js.map +1 -1
- package/dist/executor/action-executor.d.ts +8 -0
- package/dist/executor/action-executor.d.ts.map +1 -1
- package/dist/executor/action-executor.js +73 -21
- package/dist/executor/action-executor.js.map +1 -1
- package/dist/executor/android-executor.d.ts.map +1 -1
- package/dist/executor/android-executor.js +1 -2
- package/dist/executor/android-executor.js.map +1 -1
- package/dist/executor/cli-executor.d.ts.map +1 -1
- package/dist/executor/cli-executor.js +1 -2
- package/dist/executor/cli-executor.js.map +1 -1
- package/dist/executor/ios-executor.d.ts.map +1 -1
- package/dist/executor/ios-executor.js +1 -2
- package/dist/executor/ios-executor.js.map +1 -1
- package/package.json +1 -1
|
@@ -30,41 +30,71 @@ function getLocalIP() {
|
|
|
30
30
|
}
|
|
31
31
|
return '127.0.0.1';
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
34
|
-
async function uploadReportToServer(
|
|
35
|
-
if (!
|
|
36
|
-
logger.
|
|
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
|
-
|
|
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',
|
|
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': '
|
|
71
|
+
'Accept': '*/*',
|
|
72
|
+
'Connection': 'close',
|
|
73
|
+
'Expect': '',
|
|
74
|
+
// form-data 库会自动设置 Content-Type(含 boundary),必须带上
|
|
75
|
+
...formData.getHeaders(),
|
|
53
76
|
},
|
|
54
|
-
timeout:
|
|
77
|
+
timeout: 120000,
|
|
78
|
+
maxRedirects: 0,
|
|
55
79
|
});
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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},
|
|
471
|
-
|
|
472
|
-
const
|
|
473
|
-
|
|
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:
|
|
482
|
-
|
|
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
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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:
|
|
590
|
-
reportFile,
|
|
591
|
-
reportHTML,
|
|
598
|
+
dump: '',
|
|
592
599
|
reportUrl,
|
|
593
|
-
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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:
|
|
705
|
-
|
|
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
|
|
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
|
-
// 异常时也尝试提取
|
|
722
|
-
let
|
|
726
|
+
// 异常时也尝试提取 reportFile 并上传
|
|
727
|
+
let reportFile = '';
|
|
723
728
|
try {
|
|
724
729
|
const agent = session.executor?.getAgent?.() || session.agent;
|
|
725
|
-
if (agent && typeof agent.
|
|
726
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
-
|
|
808
|
-
reportHTML: result.reportHTML || '',
|
|
881
|
+
reportUrl,
|
|
809
882
|
error: result.errorMessage,
|
|
810
|
-
}
|
|
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
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
sessionId,
|
|
846
|
-
|
|
847
|
-
|
|
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
|
|
1157
|
-
let reportHTML = '';
|
|
1222
|
+
// 拉取报告文件路径
|
|
1223
|
+
let reportFile = '';
|
|
1158
1224
|
try {
|
|
1159
|
-
if (typeof agent.
|
|
1160
|
-
|
|
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]
|
|
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
|
-
//
|
|
1175
|
-
const reportUrl =
|
|
1176
|
-
|
|
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:
|
|
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:
|
|
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
|
-
// 失败时也尝试提取
|
|
1223
|
-
let
|
|
1224
|
-
let reportHTML = '';
|
|
1280
|
+
// 失败时也尝试提取 reportFile 并上传
|
|
1281
|
+
let reportFile = '';
|
|
1225
1282
|
const agentForReport = session?.webAgent;
|
|
1226
1283
|
try {
|
|
1227
|
-
if (agentForReport && typeof agentForReport.
|
|
1228
|
-
|
|
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)
|
|
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:
|
|
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:
|
|
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
|
|
1398
|
-
let reportHTML = '';
|
|
1440
|
+
let reportFile = '';
|
|
1399
1441
|
const agent = session.executor?.getAgent?.() || session.agent;
|
|
1400
|
-
// 获取
|
|
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.
|
|
1412
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1526
|
-
//
|
|
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
|
-
|
|
1530
|
-
|
|
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
|
}
|