@godscene/core 1.7.11

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 (189) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -0
  3. package/dist/es/agent/agent.mjs +767 -0
  4. package/dist/es/agent/common.mjs +0 -0
  5. package/dist/es/agent/execution-session.mjs +39 -0
  6. package/dist/es/agent/index.mjs +6 -0
  7. package/dist/es/agent/task-builder.mjs +343 -0
  8. package/dist/es/agent/task-cache.mjs +212 -0
  9. package/dist/es/agent/tasks.mjs +428 -0
  10. package/dist/es/agent/ui-utils.mjs +101 -0
  11. package/dist/es/agent/utils.mjs +167 -0
  12. package/dist/es/ai-model/auto-glm/actions.mjs +237 -0
  13. package/dist/es/ai-model/auto-glm/index.mjs +6 -0
  14. package/dist/es/ai-model/auto-glm/parser.mjs +237 -0
  15. package/dist/es/ai-model/auto-glm/planning.mjs +69 -0
  16. package/dist/es/ai-model/auto-glm/prompt.mjs +220 -0
  17. package/dist/es/ai-model/auto-glm/util.mjs +7 -0
  18. package/dist/es/ai-model/connectivity.mjs +136 -0
  19. package/dist/es/ai-model/conversation-history.mjs +193 -0
  20. package/dist/es/ai-model/index.mjs +12 -0
  21. package/dist/es/ai-model/inspect.mjs +395 -0
  22. package/dist/es/ai-model/llm-planning.mjs +231 -0
  23. package/dist/es/ai-model/prompt/common.mjs +5 -0
  24. package/dist/es/ai-model/prompt/describe.mjs +64 -0
  25. package/dist/es/ai-model/prompt/extraction.mjs +129 -0
  26. package/dist/es/ai-model/prompt/llm-locator.mjs +49 -0
  27. package/dist/es/ai-model/prompt/llm-planning.mjs +584 -0
  28. package/dist/es/ai-model/prompt/llm-section-locator.mjs +42 -0
  29. package/dist/es/ai-model/prompt/order-sensitive-judge.mjs +33 -0
  30. package/dist/es/ai-model/prompt/playwright-generator.mjs +115 -0
  31. package/dist/es/ai-model/prompt/ui-tars-planning.mjs +34 -0
  32. package/dist/es/ai-model/prompt/util.mjs +57 -0
  33. package/dist/es/ai-model/prompt/yaml-generator.mjs +201 -0
  34. package/dist/es/ai-model/service-caller/codex-app-server.mjs +573 -0
  35. package/dist/es/ai-model/service-caller/image-detail.mjs +4 -0
  36. package/dist/es/ai-model/service-caller/index.mjs +648 -0
  37. package/dist/es/ai-model/service-caller/request-timeout.mjs +47 -0
  38. package/dist/es/ai-model/ui-tars-planning.mjs +247 -0
  39. package/dist/es/common.mjs +382 -0
  40. package/dist/es/device/device-options.mjs +0 -0
  41. package/dist/es/device/index.mjs +340 -0
  42. package/dist/es/dump/html-utils.mjs +290 -0
  43. package/dist/es/dump/index.mjs +3 -0
  44. package/dist/es/dump/screenshot-restoration.mjs +30 -0
  45. package/dist/es/dump/screenshot-store.mjs +125 -0
  46. package/dist/es/index.mjs +17 -0
  47. package/dist/es/report-cli.mjs +149 -0
  48. package/dist/es/report-generator.mjs +203 -0
  49. package/dist/es/report-markdown.mjs +216 -0
  50. package/dist/es/report.mjs +287 -0
  51. package/dist/es/screenshot-item.mjs +120 -0
  52. package/dist/es/service/index.mjs +272 -0
  53. package/dist/es/service/utils.mjs +13 -0
  54. package/dist/es/skill/index.mjs +35 -0
  55. package/dist/es/task-runner.mjs +261 -0
  56. package/dist/es/task-timing.mjs +10 -0
  57. package/dist/es/tree.mjs +11 -0
  58. package/dist/es/types.mjs +202 -0
  59. package/dist/es/utils.mjs +232 -0
  60. package/dist/es/yaml/builder.mjs +11 -0
  61. package/dist/es/yaml/index.mjs +4 -0
  62. package/dist/es/yaml/player.mjs +425 -0
  63. package/dist/es/yaml/utils.mjs +100 -0
  64. package/dist/es/yaml.mjs +0 -0
  65. package/dist/lib/agent/agent.js +815 -0
  66. package/dist/lib/agent/common.js +5 -0
  67. package/dist/lib/agent/execution-session.js +73 -0
  68. package/dist/lib/agent/index.js +76 -0
  69. package/dist/lib/agent/task-builder.js +380 -0
  70. package/dist/lib/agent/task-cache.js +264 -0
  71. package/dist/lib/agent/tasks.js +471 -0
  72. package/dist/lib/agent/ui-utils.js +153 -0
  73. package/dist/lib/agent/utils.js +238 -0
  74. package/dist/lib/ai-model/auto-glm/actions.js +271 -0
  75. package/dist/lib/ai-model/auto-glm/index.js +64 -0
  76. package/dist/lib/ai-model/auto-glm/parser.js +280 -0
  77. package/dist/lib/ai-model/auto-glm/planning.js +103 -0
  78. package/dist/lib/ai-model/auto-glm/prompt.js +257 -0
  79. package/dist/lib/ai-model/auto-glm/util.js +44 -0
  80. package/dist/lib/ai-model/connectivity.js +180 -0
  81. package/dist/lib/ai-model/conversation-history.js +227 -0
  82. package/dist/lib/ai-model/index.js +127 -0
  83. package/dist/lib/ai-model/inspect.js +441 -0
  84. package/dist/lib/ai-model/llm-planning.js +268 -0
  85. package/dist/lib/ai-model/prompt/common.js +39 -0
  86. package/dist/lib/ai-model/prompt/describe.js +98 -0
  87. package/dist/lib/ai-model/prompt/extraction.js +169 -0
  88. package/dist/lib/ai-model/prompt/llm-locator.js +86 -0
  89. package/dist/lib/ai-model/prompt/llm-planning.js +621 -0
  90. package/dist/lib/ai-model/prompt/llm-section-locator.js +79 -0
  91. package/dist/lib/ai-model/prompt/order-sensitive-judge.js +70 -0
  92. package/dist/lib/ai-model/prompt/playwright-generator.js +176 -0
  93. package/dist/lib/ai-model/prompt/ui-tars-planning.js +71 -0
  94. package/dist/lib/ai-model/prompt/util.js +103 -0
  95. package/dist/lib/ai-model/prompt/yaml-generator.js +262 -0
  96. package/dist/lib/ai-model/service-caller/codex-app-server.js +622 -0
  97. package/dist/lib/ai-model/service-caller/image-detail.js +38 -0
  98. package/dist/lib/ai-model/service-caller/index.js +716 -0
  99. package/dist/lib/ai-model/service-caller/request-timeout.js +93 -0
  100. package/dist/lib/ai-model/ui-tars-planning.js +281 -0
  101. package/dist/lib/common.js +491 -0
  102. package/dist/lib/device/device-options.js +18 -0
  103. package/dist/lib/device/index.js +467 -0
  104. package/dist/lib/dump/html-utils.js +366 -0
  105. package/dist/lib/dump/index.js +58 -0
  106. package/dist/lib/dump/screenshot-restoration.js +64 -0
  107. package/dist/lib/dump/screenshot-store.js +165 -0
  108. package/dist/lib/index.js +184 -0
  109. package/dist/lib/report-cli.js +189 -0
  110. package/dist/lib/report-generator.js +244 -0
  111. package/dist/lib/report-markdown.js +253 -0
  112. package/dist/lib/report.js +333 -0
  113. package/dist/lib/screenshot-item.js +154 -0
  114. package/dist/lib/service/index.js +306 -0
  115. package/dist/lib/service/utils.js +47 -0
  116. package/dist/lib/skill/index.js +69 -0
  117. package/dist/lib/task-runner.js +298 -0
  118. package/dist/lib/task-timing.js +44 -0
  119. package/dist/lib/tree.js +51 -0
  120. package/dist/lib/types.js +298 -0
  121. package/dist/lib/utils.js +314 -0
  122. package/dist/lib/yaml/builder.js +55 -0
  123. package/dist/lib/yaml/index.js +79 -0
  124. package/dist/lib/yaml/player.js +459 -0
  125. package/dist/lib/yaml/utils.js +153 -0
  126. package/dist/lib/yaml.js +18 -0
  127. package/dist/types/agent/agent.d.ts +220 -0
  128. package/dist/types/agent/common.d.ts +0 -0
  129. package/dist/types/agent/execution-session.d.ts +36 -0
  130. package/dist/types/agent/index.d.ts +9 -0
  131. package/dist/types/agent/task-builder.d.ts +34 -0
  132. package/dist/types/agent/task-cache.d.ts +49 -0
  133. package/dist/types/agent/tasks.d.ts +70 -0
  134. package/dist/types/agent/ui-utils.d.ts +14 -0
  135. package/dist/types/agent/utils.d.ts +25 -0
  136. package/dist/types/ai-model/auto-glm/actions.d.ts +78 -0
  137. package/dist/types/ai-model/auto-glm/index.d.ts +6 -0
  138. package/dist/types/ai-model/auto-glm/parser.d.ts +18 -0
  139. package/dist/types/ai-model/auto-glm/planning.d.ts +12 -0
  140. package/dist/types/ai-model/auto-glm/prompt.d.ts +27 -0
  141. package/dist/types/ai-model/auto-glm/util.d.ts +13 -0
  142. package/dist/types/ai-model/connectivity.d.ts +20 -0
  143. package/dist/types/ai-model/conversation-history.d.ts +105 -0
  144. package/dist/types/ai-model/index.d.ts +16 -0
  145. package/dist/types/ai-model/inspect.d.ts +67 -0
  146. package/dist/types/ai-model/llm-planning.d.ts +19 -0
  147. package/dist/types/ai-model/prompt/common.d.ts +2 -0
  148. package/dist/types/ai-model/prompt/describe.d.ts +1 -0
  149. package/dist/types/ai-model/prompt/extraction.d.ts +7 -0
  150. package/dist/types/ai-model/prompt/llm-locator.d.ts +3 -0
  151. package/dist/types/ai-model/prompt/llm-planning.d.ts +10 -0
  152. package/dist/types/ai-model/prompt/llm-section-locator.d.ts +3 -0
  153. package/dist/types/ai-model/prompt/order-sensitive-judge.d.ts +2 -0
  154. package/dist/types/ai-model/prompt/playwright-generator.d.ts +26 -0
  155. package/dist/types/ai-model/prompt/ui-tars-planning.d.ts +2 -0
  156. package/dist/types/ai-model/prompt/util.d.ts +33 -0
  157. package/dist/types/ai-model/prompt/yaml-generator.d.ts +102 -0
  158. package/dist/types/ai-model/service-caller/codex-app-server.d.ts +42 -0
  159. package/dist/types/ai-model/service-caller/image-detail.d.ts +2 -0
  160. package/dist/types/ai-model/service-caller/index.d.ts +60 -0
  161. package/dist/types/ai-model/service-caller/request-timeout.d.ts +32 -0
  162. package/dist/types/ai-model/ui-tars-planning.d.ts +72 -0
  163. package/dist/types/common.d.ts +288 -0
  164. package/dist/types/device/device-options.d.ts +155 -0
  165. package/dist/types/device/index.d.ts +2565 -0
  166. package/dist/types/dump/html-utils.d.ts +75 -0
  167. package/dist/types/dump/index.d.ts +5 -0
  168. package/dist/types/dump/screenshot-restoration.d.ts +8 -0
  169. package/dist/types/dump/screenshot-store.d.ts +49 -0
  170. package/dist/types/index.d.ts +21 -0
  171. package/dist/types/report-cli.d.ts +36 -0
  172. package/dist/types/report-generator.d.ts +88 -0
  173. package/dist/types/report-markdown.d.ts +24 -0
  174. package/dist/types/report.d.ts +52 -0
  175. package/dist/types/screenshot-item.d.ts +67 -0
  176. package/dist/types/service/index.d.ts +24 -0
  177. package/dist/types/service/utils.d.ts +2 -0
  178. package/dist/types/skill/index.d.ts +25 -0
  179. package/dist/types/task-runner.d.ts +50 -0
  180. package/dist/types/task-timing.d.ts +8 -0
  181. package/dist/types/tree.d.ts +4 -0
  182. package/dist/types/types.d.ts +684 -0
  183. package/dist/types/utils.d.ts +45 -0
  184. package/dist/types/yaml/builder.d.ts +2 -0
  185. package/dist/types/yaml/index.d.ts +4 -0
  186. package/dist/types/yaml/player.d.ts +34 -0
  187. package/dist/types/yaml/utils.d.ts +9 -0
  188. package/dist/types/yaml.d.ts +215 -0
  189. package/package.json +130 -0
@@ -0,0 +1,216 @@
1
+ import { basename } from "node:path";
2
+ import { extractInsightParam, paramStr, typeStr } from "./agent/ui-utils.mjs";
3
+ import { ScreenshotItem } from "./screenshot-item.mjs";
4
+ import { normalizeScreenshotRef } from "./dump/screenshot-store.mjs";
5
+ function toExecutionDump(execution) {
6
+ if (!execution || 'object' != typeof execution) throw new Error('executionToMarkdown: execution is required');
7
+ if (!Array.isArray(execution.tasks)) throw new Error('executionToMarkdown: execution.tasks must be an array');
8
+ if (!execution.name) throw new Error('executionToMarkdown: execution.name is required');
9
+ return execution;
10
+ }
11
+ function toReportDump(report) {
12
+ if (!report || 'object' != typeof report) throw new Error('reportToMarkdown: report is required');
13
+ if (!Array.isArray(report.executions)) throw new Error('reportToMarkdown: report.executions must be an array');
14
+ return report;
15
+ }
16
+ function formatTime(ts) {
17
+ if ('number' != typeof ts || Number.isNaN(ts)) return 'N/A';
18
+ return new Date(ts).toISOString();
19
+ }
20
+ function resolveTaskTiming(task) {
21
+ const timing = task.timing;
22
+ if (!timing) return {};
23
+ const start = timing.start ?? timing.callAiStart ?? timing.callActionStart;
24
+ const end = timing.end ?? timing.callAiEnd ?? timing.callActionEnd ?? timing.captureAfterCallingSnapshotEnd;
25
+ const cost = timing.cost ?? ('number' == typeof start && 'number' == typeof end ? end - start : void 0);
26
+ return {
27
+ start,
28
+ end,
29
+ cost
30
+ };
31
+ }
32
+ function safeTaskParam(task) {
33
+ const readable = paramStr(task);
34
+ if (readable) return readable;
35
+ if ('Insight' === task.type) return extractInsightParam(task.param).content;
36
+ return '';
37
+ }
38
+ function formatSize(size) {
39
+ if (!size || 'number' != typeof size.width || 'number' != typeof size.height || Number.isNaN(size.width) || Number.isNaN(size.height)) return;
40
+ return `${size.width} x ${size.height}`;
41
+ }
42
+ function extractLocateCenter(task) {
43
+ const outputCenter = task.output?.element?.center;
44
+ if (Array.isArray(outputCenter) && outputCenter.length >= 2 && 'number' == typeof outputCenter[0] && 'number' == typeof outputCenter[1]) return [
45
+ outputCenter[0],
46
+ outputCenter[1]
47
+ ];
48
+ const paramLocateCenter = task.param?.locate?.center;
49
+ if (Array.isArray(paramLocateCenter) && paramLocateCenter.length >= 2 && 'number' == typeof paramLocateCenter[0] && 'number' == typeof paramLocateCenter[1]) return [
50
+ paramLocateCenter[0],
51
+ paramLocateCenter[1]
52
+ ];
53
+ const paramCenter = task.param?.center;
54
+ if (Array.isArray(paramCenter) && paramCenter.length >= 2 && 'number' == typeof paramCenter[0] && 'number' == typeof paramCenter[1]) return [
55
+ paramCenter[0],
56
+ paramCenter[1]
57
+ ];
58
+ }
59
+ function tryExtractBase64(screenshot) {
60
+ if (!screenshot || 'object' != typeof screenshot) return;
61
+ const s = screenshot;
62
+ if ('string' == typeof s.base64 && s.base64.length > 0) return s.base64;
63
+ }
64
+ function screenshotAttachment(screenshot, screenshotBaseDir, executionIndex, taskIndex) {
65
+ if (screenshot instanceof ScreenshotItem) {
66
+ const ext = screenshot.extension;
67
+ const suggestedFileName = `execution-${executionIndex + 1}-task-${taskIndex + 1}-${screenshot.id}.${ext}`;
68
+ const filePath = `${screenshotBaseDir}/${suggestedFileName}`;
69
+ return {
70
+ markdown: `\n![task-${taskIndex + 1}](${filePath})`,
71
+ attachment: {
72
+ id: screenshot.id,
73
+ suggestedFileName,
74
+ filePath,
75
+ mimeType: `image/${'jpeg' === ext ? 'jpeg' : 'png'}`,
76
+ executionIndex,
77
+ taskIndex,
78
+ base64Data: tryExtractBase64(screenshot)
79
+ }
80
+ };
81
+ }
82
+ const ref = normalizeScreenshotRef(screenshot);
83
+ if (ref) {
84
+ const ext = 'image/jpeg' === ref.mimeType ? 'jpeg' : 'png';
85
+ const suggestedFileName = `execution-${executionIndex + 1}-task-${taskIndex + 1}-${ref.id}.${ext}`;
86
+ const filePath = ref.path || `${screenshotBaseDir}/${suggestedFileName}`;
87
+ return {
88
+ markdown: `\n![task-${taskIndex + 1}](${filePath})`,
89
+ attachment: {
90
+ id: ref.id,
91
+ suggestedFileName,
92
+ filePath,
93
+ mimeType: ref.mimeType,
94
+ executionIndex,
95
+ taskIndex,
96
+ base64Data: tryExtractBase64(screenshot)
97
+ }
98
+ };
99
+ }
100
+ const base64 = tryExtractBase64(screenshot);
101
+ if (base64) {
102
+ const ext = base64.startsWith('data:image/jpeg') ? 'jpeg' : 'png';
103
+ const id = `restored-${executionIndex + 1}-${taskIndex + 1}`;
104
+ const suggestedFileName = `execution-${executionIndex + 1}-task-${taskIndex + 1}-${id}.${ext}`;
105
+ const filePath = `${screenshotBaseDir}/${suggestedFileName}`;
106
+ return {
107
+ markdown: `\n![task-${taskIndex + 1}](${filePath})`,
108
+ attachment: {
109
+ id,
110
+ suggestedFileName,
111
+ filePath,
112
+ mimeType: `image/${ext}`,
113
+ executionIndex,
114
+ taskIndex,
115
+ base64Data: base64
116
+ }
117
+ };
118
+ }
119
+ throw new Error(`executionToMarkdown: missing screenshot for execution #${executionIndex + 1} task #${taskIndex + 1}`);
120
+ }
121
+ function recorderMarkdownSection(recorder, screenshotBaseDir, executionIndex, taskIndex) {
122
+ if (!recorder?.length) return {
123
+ lines: [],
124
+ attachments: []
125
+ };
126
+ const lines = [
127
+ '',
128
+ '### Recorder'
129
+ ];
130
+ const attachments = [];
131
+ recorder.forEach((item, recorderIndex)=>{
132
+ lines.push(`- #${recorderIndex + 1} type=${item.type}, ts=${formatTime(item.ts)}, timing=${item.timing || 'N/A'}`);
133
+ if (!item.screenshot) return;
134
+ const imageResult = screenshotAttachment(item.screenshot, screenshotBaseDir, executionIndex, taskIndex);
135
+ lines.push(imageResult.markdown);
136
+ attachments.push(imageResult.attachment);
137
+ });
138
+ return {
139
+ lines,
140
+ attachments
141
+ };
142
+ }
143
+ function renderExecution(executionRaw, executionIndex, options) {
144
+ const execution = toExecutionDump(executionRaw);
145
+ const screenshotBaseDir = options?.screenshotBaseDir ?? './screenshots';
146
+ const lines = [];
147
+ const attachments = [];
148
+ lines.push(`# ${execution.name}`);
149
+ if (execution.description) lines.push('', execution.description);
150
+ lines.push('', `- Execution start: ${formatTime(execution.logTime)}`);
151
+ lines.push(`- Task count: ${execution.tasks.length}`);
152
+ execution.tasks.forEach((task, taskIndex)=>{
153
+ const title = typeStr(task);
154
+ const detail = safeTaskParam(task);
155
+ const time = resolveTaskTiming(task);
156
+ lines.push('', `## ${taskIndex + 1}. ${title}${detail ? ` - ${detail}` : ''}`);
157
+ lines.push(`- Status: ${task.status || 'unknown'}`);
158
+ lines.push(`- Start: ${formatTime(time.start)}`);
159
+ lines.push(`- End: ${formatTime(time.end)}`);
160
+ lines.push(`- Cost(ms): ${'number' == typeof time.cost ? time.cost : 'N/A'}`);
161
+ lines.push(`- Screen size: ${formatSize(task.uiContext?.shotSize) || 'N/A'}`);
162
+ if ('Locate' === task.subType) {
163
+ const locateCenter = extractLocateCenter(task);
164
+ if (locateCenter) lines.push(`- Locate center: (${locateCenter[0]}, ${locateCenter[1]})`);
165
+ }
166
+ if (task.errorMessage) lines.push(`- Error: ${task.errorMessage}`);
167
+ if (task.uiContext?.screenshot) {
168
+ const imageResult = screenshotAttachment(task.uiContext.screenshot, screenshotBaseDir, executionIndex, taskIndex);
169
+ lines.push(imageResult.markdown);
170
+ attachments.push(imageResult.attachment);
171
+ }
172
+ const recorderSection = recorderMarkdownSection(task.recorder, screenshotBaseDir, executionIndex, taskIndex);
173
+ if (recorderSection.lines.length) {
174
+ lines.push(...recorderSection.lines);
175
+ attachments.push(...recorderSection.attachments);
176
+ }
177
+ });
178
+ return {
179
+ markdown: lines.join('\n'),
180
+ attachments
181
+ };
182
+ }
183
+ function reportFileName(execution, executionIndex) {
184
+ const safeName = execution.name.trim().replace(/\s+/g, '-').replace(/[^a-zA-Z0-9-_]/g, '') || `execution-${executionIndex + 1}`;
185
+ return `${executionIndex + 1}-${basename(safeName)}.md`;
186
+ }
187
+ function executionToMarkdown(execution, options) {
188
+ return renderExecution(execution, 0, options);
189
+ }
190
+ function reportToMarkdown(report) {
191
+ const reportDump = toReportDump(report);
192
+ const executionResults = reportDump.executions.map((execution, index)=>{
193
+ const rendered = renderExecution(execution, index);
194
+ return {
195
+ executionIndex: index,
196
+ executionName: execution.name,
197
+ markdown: rendered.markdown,
198
+ attachments: rendered.attachments,
199
+ suggestedFileName: reportFileName(execution, index)
200
+ };
201
+ });
202
+ const attachments = executionResults.flatMap((item)=>item.attachments);
203
+ const header = [
204
+ `# ${reportDump.groupName}`,
205
+ reportDump.groupDescription ? `\n${reportDump.groupDescription}` : '',
206
+ `\n- SDK Version: ${reportDump.sdkVersion}`,
207
+ `- Execution count: ${reportDump.executions.length}`,
208
+ '\n## Suggested execution markdown files',
209
+ ...executionResults.map((item)=>`- ${item.suggestedFileName} (${item.executionName})`)
210
+ ].filter(Boolean).join('\n');
211
+ return {
212
+ markdown: `${header}\n\n${executionResults.map((item)=>item.markdown).join('\n\n---\n\n')}`,
213
+ attachments
214
+ };
215
+ }
216
+ export { executionToMarkdown, reportToMarkdown };
@@ -0,0 +1,287 @@
1
+ import { appendFileSync, copyFileSync, existsSync, mkdirSync, readdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
+ import { getMidsceneRunSubDir } from "@godscene/shared/common";
4
+ import { antiEscapeScriptTag, logMsg } from "@godscene/shared/utils";
5
+ import { getReportFileName } from "./agent/index.mjs";
6
+ import { extractAllDumpScriptsSync, extractLastDumpScriptSync, getBaseUrlFixScript, streamDumpScriptsSync, streamImageScriptsToFile } from "./dump/html-utils.mjs";
7
+ import { normalizeScreenshotRef, resolveScreenshotSource } from "./dump/screenshot-store.mjs";
8
+ import { ReportActionDump } from "./types.mjs";
9
+ import { getReportTpl, getVersion, reportHTMLContent } from "./utils.mjs";
10
+ function _define_property(obj, key, value) {
11
+ if (key in obj) Object.defineProperty(obj, key, {
12
+ value: value,
13
+ enumerable: true,
14
+ configurable: true,
15
+ writable: true
16
+ });
17
+ else obj[key] = value;
18
+ return obj;
19
+ }
20
+ function isDirectoryModeReport(reportFilePath) {
21
+ const reportDir = dirname(reportFilePath);
22
+ return 'index.html' === basename(reportFilePath) && existsSync(join(reportDir, 'screenshots'));
23
+ }
24
+ function dedupeExecutionsKeepLatest(executions) {
25
+ let noIdCounter = 0;
26
+ const deduped = new Map();
27
+ for (const exec of executions){
28
+ const key = exec.id || `__no_id_${noIdCounter++}`;
29
+ deduped.set(key, exec);
30
+ }
31
+ return Array.from(deduped.values());
32
+ }
33
+ function peekReportSdkVersion(reportFilePath) {
34
+ try {
35
+ const dump = extractLastDumpScriptSync(reportFilePath);
36
+ if (!dump) return;
37
+ const match = dump.match(/"sdkVersion"\s*:\s*"([^"]+)"/);
38
+ return match?.[1];
39
+ } catch {
40
+ return;
41
+ }
42
+ }
43
+ const warnedMismatchedVersions = new Set();
44
+ class ReportMergingTool {
45
+ createEmptyDumpString(groupName, groupDescription) {
46
+ return new ReportActionDump({
47
+ sdkVersion: '',
48
+ groupName,
49
+ groupDescription,
50
+ modelBriefs: [],
51
+ executions: []
52
+ }).serialize();
53
+ }
54
+ append(reportInfo) {
55
+ if (reportInfo.reportFilePath) {
56
+ const sourceVersion = peekReportSdkVersion(reportInfo.reportFilePath);
57
+ const currentVersion = getVersion();
58
+ if (sourceVersion && currentVersion && sourceVersion !== currentVersion && !warnedMismatchedVersions.has(sourceVersion)) {
59
+ warnedMismatchedVersions.add(sourceVersion);
60
+ logMsg(`[@godscene/core] ReportMergingTool version mismatch: source report was written by @godscene/core@${sourceVersion} but the merger is @godscene/core@${currentVersion}. This commonly means @godscene/core and the device package (e.g. @godscene/android) resolve to different versions in node_modules. Merged output may silently drop intermediate steps. Align the versions and reinstall (rm -rf node_modules package-lock.json && npm install).`);
61
+ }
62
+ }
63
+ this.reportInfos.push(reportInfo);
64
+ }
65
+ clear() {
66
+ this.reportInfos = [];
67
+ }
68
+ mergeDumpScripts(contents) {
69
+ const unescaped = contents.map((c)=>antiEscapeScriptTag(c)).filter((c)=>c.length > 0);
70
+ if (0 === unescaped.length) return '';
71
+ if (1 === unescaped.length) return unescaped[0];
72
+ const base = ReportActionDump.fromSerializedString(unescaped[0]);
73
+ const allExecutions = [
74
+ ...base.executions
75
+ ];
76
+ for(let i = 1; i < unescaped.length; i++){
77
+ const other = ReportActionDump.fromSerializedString(unescaped[i]);
78
+ allExecutions.push(...other.executions);
79
+ }
80
+ base.executions = dedupeExecutionsKeepLatest(allExecutions);
81
+ return base.serialize();
82
+ }
83
+ mergeReports(reportFileName = 'AUTO', opts) {
84
+ const { rmOriginalReports = false, overwrite = false } = opts ?? {};
85
+ if (0 === this.reportInfos.length) {
86
+ logMsg('No reports to merge');
87
+ return null;
88
+ }
89
+ const targetDir = getMidsceneRunSubDir('report');
90
+ const hasDirectoryModeReport = this.reportInfos.some((info)=>{
91
+ const reportFilePath = info.reportFilePath;
92
+ return Boolean(reportFilePath && isDirectoryModeReport(reportFilePath));
93
+ });
94
+ const resolvedName = 'AUTO' === reportFileName ? getReportFileName('merged-report') : reportFileName;
95
+ const outputFilePath = hasDirectoryModeReport ? resolve(targetDir, resolvedName, 'index.html') : resolve(targetDir, `${resolvedName}.html`);
96
+ if ('AUTO' !== reportFileName && existsSync(outputFilePath)) {
97
+ if (!overwrite) throw new Error(`Report file already exists: ${outputFilePath}\nSet overwrite to true to overwrite this file.`);
98
+ if (hasDirectoryModeReport) rmSync(dirname(outputFilePath), {
99
+ recursive: true,
100
+ force: true
101
+ });
102
+ else unlinkSync(outputFilePath);
103
+ }
104
+ if (hasDirectoryModeReport) mkdirSync(dirname(outputFilePath), {
105
+ recursive: true
106
+ });
107
+ logMsg(`Start merging ${this.reportInfos.length} reports...\nCreating template file...`);
108
+ try {
109
+ const htmlEndTag = '</html>';
110
+ const tpl = getReportTpl();
111
+ const htmlEndIdx = tpl.lastIndexOf(htmlEndTag);
112
+ const tplWithoutClose = -1 !== htmlEndIdx ? tpl.slice(0, htmlEndIdx) : tpl;
113
+ appendFileSync(outputFilePath, tplWithoutClose);
114
+ if (hasDirectoryModeReport) appendFileSync(outputFilePath, getBaseUrlFixScript());
115
+ for(let i = 0; i < this.reportInfos.length; i++){
116
+ const reportInfo = this.reportInfos[i];
117
+ logMsg(`Processing report ${i + 1}/${this.reportInfos.length}`);
118
+ const { reportAttributes } = reportInfo;
119
+ let dumpString = this.createEmptyDumpString(reportAttributes.testTitle, reportAttributes.testDescription);
120
+ let mergedGroupId = `merged-group-${i}`;
121
+ if (reportInfo.reportFilePath) {
122
+ if (isDirectoryModeReport(reportInfo.reportFilePath)) {
123
+ const reportDir = dirname(reportInfo.reportFilePath);
124
+ const screenshotsDir = join(reportDir, 'screenshots');
125
+ const mergedScreenshotsDir = join(dirname(outputFilePath), 'screenshots');
126
+ mkdirSync(mergedScreenshotsDir, {
127
+ recursive: true
128
+ });
129
+ for (const file of readdirSync(screenshotsDir)){
130
+ const src = join(screenshotsDir, file);
131
+ const dest = join(mergedScreenshotsDir, file);
132
+ copyFileSync(src, dest);
133
+ }
134
+ } else streamImageScriptsToFile(reportInfo.reportFilePath, outputFilePath);
135
+ const allDumps = extractAllDumpScriptsSync(reportInfo.reportFilePath).filter((d)=>d.openTag.includes('data-group-id'));
136
+ const groupIdMatch = allDumps[0]?.openTag.match(/data-group-id="([^"]+)"/);
137
+ if (groupIdMatch) mergedGroupId = decodeURIComponent(groupIdMatch[1]);
138
+ const extractedDumpString = allDumps.length > 0 ? this.mergeDumpScripts(allDumps.map((d)=>d.content)) : extractLastDumpScriptSync(reportInfo.reportFilePath);
139
+ if (extractedDumpString) dumpString = extractedDumpString;
140
+ }
141
+ const reportHtmlStr = `${reportHTMLContent({
142
+ dumpString,
143
+ attributes: {
144
+ 'data-group-id': mergedGroupId,
145
+ playwright_test_duration: reportAttributes.testDuration,
146
+ playwright_test_status: reportAttributes.testStatus,
147
+ playwright_test_title: reportAttributes.testTitle,
148
+ playwright_test_id: reportAttributes.testId,
149
+ playwright_test_description: reportAttributes.testDescription,
150
+ is_merged: true
151
+ }
152
+ }, void 0, void 0, false)}\n`;
153
+ appendFileSync(outputFilePath, reportHtmlStr);
154
+ }
155
+ appendFileSync(outputFilePath, `${htmlEndTag}\n`);
156
+ logMsg(`Successfully merged new report: ${outputFilePath}`);
157
+ if (rmOriginalReports) {
158
+ for (const info of this.reportInfos)if (info.reportFilePath) try {
159
+ if (isDirectoryModeReport(info.reportFilePath)) {
160
+ const reportDir = dirname(info.reportFilePath);
161
+ rmSync(reportDir, {
162
+ recursive: true,
163
+ force: true
164
+ });
165
+ } else unlinkSync(info.reportFilePath);
166
+ } catch (error) {
167
+ logMsg(`Error deleting report ${info.reportFilePath}: ${error}`);
168
+ }
169
+ logMsg(`Removed ${this.reportInfos.length} original reports`);
170
+ }
171
+ return outputFilePath;
172
+ } catch (error) {
173
+ logMsg(`Error in mergeReports: ${error}`);
174
+ throw error;
175
+ }
176
+ }
177
+ constructor(){
178
+ _define_property(this, "reportInfos", []);
179
+ }
180
+ }
181
+ function collectDedupedExecutions(htmlPath) {
182
+ let baseDump = null;
183
+ let executionSerial = 0;
184
+ const latestSerialByExecutionId = new Map();
185
+ streamDumpScriptsSync(htmlPath, (dumpScript)=>{
186
+ if (!dumpScript.openTag.includes('data-group-id')) return false;
187
+ const groupedDump = ReportActionDump.fromSerializedString(antiEscapeScriptTag(dumpScript.content));
188
+ for (const execution of groupedDump.executions){
189
+ executionSerial += 1;
190
+ if (execution.id) latestSerialByExecutionId.set(execution.id, executionSerial);
191
+ }
192
+ return false;
193
+ });
194
+ const executions = [];
195
+ executionSerial = 0;
196
+ streamDumpScriptsSync(htmlPath, (dumpScript)=>{
197
+ if (!dumpScript.openTag.includes('data-group-id')) return false;
198
+ const groupedDump = ReportActionDump.fromSerializedString(antiEscapeScriptTag(dumpScript.content));
199
+ if (!baseDump) baseDump = groupedDump;
200
+ for (const execution of groupedDump.executions){
201
+ executionSerial += 1;
202
+ if (!execution.id || latestSerialByExecutionId.get(execution.id) === executionSerial) executions.push(execution);
203
+ }
204
+ return false;
205
+ });
206
+ if (!baseDump) throw new Error(`No report dump scripts found in ${htmlPath}`);
207
+ return {
208
+ baseDump,
209
+ executions
210
+ };
211
+ }
212
+ function extensionByMimeType(mimeType) {
213
+ if ('image/png' === mimeType) return 'png';
214
+ if ('image/jpeg' === mimeType) return 'jpeg';
215
+ throw new Error(`Unsupported screenshot mime type: ${mimeType}`);
216
+ }
217
+ function externalizeScreenshotsInExecution(execution, opts) {
218
+ const visit = (node)=>{
219
+ if (Array.isArray(node)) {
220
+ for (const item of node)visit(item);
221
+ return;
222
+ }
223
+ if ('object' != typeof node || null === node) return;
224
+ const ref = normalizeScreenshotRef(node);
225
+ if (ref) {
226
+ const ext = extensionByMimeType(ref.mimeType);
227
+ const fileName = `${ref.id}.${ext}`;
228
+ const relativePath = `./screenshots/${fileName}`;
229
+ const absolutePath = join(opts.screenshotsDir, fileName);
230
+ if (!opts.writtenFiles.has(fileName)) {
231
+ const resolved = resolveScreenshotSource(ref, {
232
+ reportPath: opts.htmlPath
233
+ });
234
+ if ('data-uri' === resolved.type) {
235
+ const rawBase64 = resolved.dataUri.replace(/^data:image\/[a-zA-Z+]+;base64,/, '');
236
+ writeFileSync(absolutePath, Buffer.from(rawBase64, 'base64'));
237
+ } else copyFileSync(resolved.filePath, absolutePath);
238
+ opts.writtenFiles.add(fileName);
239
+ }
240
+ ref.storage = 'file';
241
+ ref.path = relativePath;
242
+ return;
243
+ }
244
+ for (const value of Object.values(node))visit(value);
245
+ };
246
+ visit(execution);
247
+ }
248
+ function splitReportHtmlByExecution(options) {
249
+ const { htmlPath, outputDir } = options;
250
+ const screenshotsDir = join(outputDir, 'screenshots');
251
+ mkdirSync(outputDir, {
252
+ recursive: true
253
+ });
254
+ mkdirSync(screenshotsDir, {
255
+ recursive: true
256
+ });
257
+ const executionJsonFiles = [];
258
+ const writtenScreenshotFiles = new Set();
259
+ const { baseDump, executions } = collectDedupedExecutions(htmlPath);
260
+ let fileIndex = 0;
261
+ for (const execution of executions){
262
+ fileIndex += 1;
263
+ externalizeScreenshotsInExecution(execution, {
264
+ htmlPath,
265
+ screenshotsDir,
266
+ writtenFiles: writtenScreenshotFiles
267
+ });
268
+ const singleExecutionDump = new ReportActionDump({
269
+ sdkVersion: baseDump.sdkVersion,
270
+ groupName: baseDump.groupName,
271
+ groupDescription: baseDump.groupDescription,
272
+ modelBriefs: baseDump.modelBriefs,
273
+ deviceType: baseDump.deviceType,
274
+ executions: [
275
+ execution
276
+ ]
277
+ });
278
+ const jsonFilePath = join(outputDir, `${fileIndex}.execution.json`);
279
+ writeFileSync(jsonFilePath, singleExecutionDump.serialize(2), 'utf-8');
280
+ executionJsonFiles.push(jsonFilePath);
281
+ }
282
+ return {
283
+ executionJsonFiles,
284
+ screenshotFiles: Array.from(writtenScreenshotFiles).sort().map((fileName)=>join(screenshotsDir, fileName))
285
+ };
286
+ }
287
+ export { ReportMergingTool, collectDedupedExecutions, dedupeExecutionsKeepLatest, isDirectoryModeReport, splitReportHtmlByExecution };
@@ -0,0 +1,120 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { uuid } from "@godscene/shared/utils";
3
+ import { extractImageByIdSync } from "./dump/html-utils.mjs";
4
+ import { normalizeScreenshotRef } from "./dump/screenshot-store.mjs";
5
+ function _define_property(obj, key, value) {
6
+ if (key in obj) Object.defineProperty(obj, key, {
7
+ value: value,
8
+ enumerable: true,
9
+ configurable: true,
10
+ writable: true
11
+ });
12
+ else obj[key] = value;
13
+ return obj;
14
+ }
15
+ function detectFormat(base64) {
16
+ if (base64.startsWith('data:image/jpeg')) return 'jpeg';
17
+ if (base64.startsWith('data:image/jpg')) return 'jpeg';
18
+ return 'png';
19
+ }
20
+ class ScreenshotItem {
21
+ static create(base64, capturedAt) {
22
+ return new ScreenshotItem(uuid(), base64, capturedAt);
23
+ }
24
+ get id() {
25
+ return this._id;
26
+ }
27
+ get format() {
28
+ return this._format;
29
+ }
30
+ get extension() {
31
+ return 'jpeg' === this._format ? 'jpeg' : 'png';
32
+ }
33
+ get capturedAt() {
34
+ return this._capturedAt;
35
+ }
36
+ get base64() {
37
+ if (null !== this._base64) return this._base64;
38
+ const loadFromFile = ()=>{
39
+ if (null === this._persistedPath) throw new Error(`Screenshot ${this._id}: file recovery path missing`);
40
+ const buffer = readFileSync(this._persistedPath);
41
+ return `data:image/${this._format};base64,${buffer.toString('base64')}`;
42
+ };
43
+ const loadFromInline = ()=>{
44
+ if (null === this._persistedHtmlPath) throw new Error(`Screenshot ${this._id}: HTML recovery path missing`);
45
+ const data = extractImageByIdSync(this._persistedHtmlPath, this._id);
46
+ if (data) return data;
47
+ throw new Error(`Screenshot ${this._id}: cannot recover from HTML (id not found in ${this._persistedHtmlPath})`);
48
+ };
49
+ if (this._serializedRef?.storage === 'file') return loadFromFile();
50
+ if (this._serializedRef?.storage === 'inline') return loadFromInline();
51
+ if (null !== this._persistedPath) return loadFromFile();
52
+ if (null !== this._persistedHtmlPath) return loadFromInline();
53
+ throw new Error(`Screenshot ${this._id}: base64 data released without recovery path`);
54
+ }
55
+ hasBase64() {
56
+ return null !== this._base64;
57
+ }
58
+ markPersistedInline(htmlPath) {
59
+ const ref = this.createRef('inline');
60
+ this._serializedRef = ref;
61
+ this._persistedHtmlPath = htmlPath;
62
+ this._base64 = null;
63
+ return ref;
64
+ }
65
+ registerPersistedFileCopy(relativePath, absolutePath) {
66
+ const ref = this.createRef('file', relativePath);
67
+ this._persistedPath = absolutePath;
68
+ this._base64 = null;
69
+ return ref;
70
+ }
71
+ markPersistedToPath(relativePath, absolutePath) {
72
+ const ref = this.registerPersistedFileCopy(relativePath, absolutePath);
73
+ this._serializedRef = ref;
74
+ return ref;
75
+ }
76
+ toSerializable() {
77
+ return this._serializedRef ?? {
78
+ type: 'midscene_screenshot_ref',
79
+ id: this._id,
80
+ capturedAt: this._capturedAt,
81
+ mimeType: 'jpeg' === this._format ? 'image/jpeg' : 'image/png',
82
+ storage: 'inline'
83
+ };
84
+ }
85
+ static isSerialized(value) {
86
+ return null !== normalizeScreenshotRef(value);
87
+ }
88
+ createRef(storage, relativePath) {
89
+ const baseRef = {
90
+ type: 'midscene_screenshot_ref',
91
+ id: this._id,
92
+ capturedAt: this._capturedAt,
93
+ mimeType: 'jpeg' === this._format ? 'image/jpeg' : 'image/png',
94
+ storage
95
+ };
96
+ if ('file' === storage) return {
97
+ ...baseRef,
98
+ storage,
99
+ path: relativePath
100
+ };
101
+ return baseRef;
102
+ }
103
+ get rawBase64() {
104
+ return this.base64.replace(/^data:image\/(png|jpeg|jpg);base64,/, '');
105
+ }
106
+ constructor(id, base64, capturedAt){
107
+ _define_property(this, "_id", void 0);
108
+ _define_property(this, "_base64", void 0);
109
+ _define_property(this, "_format", void 0);
110
+ _define_property(this, "_capturedAt", void 0);
111
+ _define_property(this, "_serializedRef", null);
112
+ _define_property(this, "_persistedPath", null);
113
+ _define_property(this, "_persistedHtmlPath", null);
114
+ this._id = id;
115
+ this._base64 = base64;
116
+ this._format = detectFormat(base64);
117
+ this._capturedAt = capturedAt;
118
+ }
119
+ }
120
+ export { ScreenshotItem };