@aiscene/core 1.6.2

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 (279) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -0
  3. package/dist/es/agent/agent.mjs +749 -0
  4. package/dist/es/agent/agent.mjs.map +1 -0
  5. package/dist/es/agent/common.mjs +0 -0
  6. package/dist/es/agent/execution-session.mjs +41 -0
  7. package/dist/es/agent/execution-session.mjs.map +1 -0
  8. package/dist/es/agent/index.mjs +6 -0
  9. package/dist/es/agent/task-builder.mjs +332 -0
  10. package/dist/es/agent/task-builder.mjs.map +1 -0
  11. package/dist/es/agent/task-cache.mjs +214 -0
  12. package/dist/es/agent/task-cache.mjs.map +1 -0
  13. package/dist/es/agent/tasks.mjs +424 -0
  14. package/dist/es/agent/tasks.mjs.map +1 -0
  15. package/dist/es/agent/ui-utils.mjs +91 -0
  16. package/dist/es/agent/ui-utils.mjs.map +1 -0
  17. package/dist/es/agent/utils.mjs +198 -0
  18. package/dist/es/agent/utils.mjs.map +1 -0
  19. package/dist/es/ai-model/auto-glm/actions.mjs +237 -0
  20. package/dist/es/ai-model/auto-glm/actions.mjs.map +1 -0
  21. package/dist/es/ai-model/auto-glm/index.mjs +6 -0
  22. package/dist/es/ai-model/auto-glm/parser.mjs +239 -0
  23. package/dist/es/ai-model/auto-glm/parser.mjs.map +1 -0
  24. package/dist/es/ai-model/auto-glm/planning.mjs +71 -0
  25. package/dist/es/ai-model/auto-glm/planning.mjs.map +1 -0
  26. package/dist/es/ai-model/auto-glm/prompt.mjs +222 -0
  27. package/dist/es/ai-model/auto-glm/prompt.mjs.map +1 -0
  28. package/dist/es/ai-model/auto-glm/util.mjs +9 -0
  29. package/dist/es/ai-model/auto-glm/util.mjs.map +1 -0
  30. package/dist/es/ai-model/conversation-history.mjs +195 -0
  31. package/dist/es/ai-model/conversation-history.mjs.map +1 -0
  32. package/dist/es/ai-model/index.mjs +11 -0
  33. package/dist/es/ai-model/inspect.mjs +394 -0
  34. package/dist/es/ai-model/inspect.mjs.map +1 -0
  35. package/dist/es/ai-model/llm-planning.mjs +233 -0
  36. package/dist/es/ai-model/llm-planning.mjs.map +1 -0
  37. package/dist/es/ai-model/prompt/common.mjs +7 -0
  38. package/dist/es/ai-model/prompt/common.mjs.map +1 -0
  39. package/dist/es/ai-model/prompt/describe.mjs +66 -0
  40. package/dist/es/ai-model/prompt/describe.mjs.map +1 -0
  41. package/dist/es/ai-model/prompt/extraction.mjs +169 -0
  42. package/dist/es/ai-model/prompt/extraction.mjs.map +1 -0
  43. package/dist/es/ai-model/prompt/llm-locator.mjs +51 -0
  44. package/dist/es/ai-model/prompt/llm-locator.mjs.map +1 -0
  45. package/dist/es/ai-model/prompt/llm-planning.mjs +568 -0
  46. package/dist/es/ai-model/prompt/llm-planning.mjs.map +1 -0
  47. package/dist/es/ai-model/prompt/llm-section-locator.mjs +44 -0
  48. package/dist/es/ai-model/prompt/llm-section-locator.mjs.map +1 -0
  49. package/dist/es/ai-model/prompt/order-sensitive-judge.mjs +35 -0
  50. package/dist/es/ai-model/prompt/order-sensitive-judge.mjs.map +1 -0
  51. package/dist/es/ai-model/prompt/playwright-generator.mjs +117 -0
  52. package/dist/es/ai-model/prompt/playwright-generator.mjs.map +1 -0
  53. package/dist/es/ai-model/prompt/ui-tars-planning.mjs +36 -0
  54. package/dist/es/ai-model/prompt/ui-tars-planning.mjs.map +1 -0
  55. package/dist/es/ai-model/prompt/util.mjs +59 -0
  56. package/dist/es/ai-model/prompt/util.mjs.map +1 -0
  57. package/dist/es/ai-model/prompt/yaml-generator.mjs +219 -0
  58. package/dist/es/ai-model/prompt/yaml-generator.mjs.map +1 -0
  59. package/dist/es/ai-model/service-caller/codex-app-server.mjs +575 -0
  60. package/dist/es/ai-model/service-caller/codex-app-server.mjs.map +1 -0
  61. package/dist/es/ai-model/service-caller/image-detail.mjs +6 -0
  62. package/dist/es/ai-model/service-caller/image-detail.mjs.map +1 -0
  63. package/dist/es/ai-model/service-caller/index.mjs +473 -0
  64. package/dist/es/ai-model/service-caller/index.mjs.map +1 -0
  65. package/dist/es/ai-model/ui-tars-planning.mjs +249 -0
  66. package/dist/es/ai-model/ui-tars-planning.mjs.map +1 -0
  67. package/dist/es/common.mjs +371 -0
  68. package/dist/es/common.mjs.map +1 -0
  69. package/dist/es/device/device-options.mjs +0 -0
  70. package/dist/es/device/index.mjs +341 -0
  71. package/dist/es/device/index.mjs.map +1 -0
  72. package/dist/es/dump/html-utils.mjs +284 -0
  73. package/dist/es/dump/html-utils.mjs.map +1 -0
  74. package/dist/es/dump/image-restoration.mjs +43 -0
  75. package/dist/es/dump/image-restoration.mjs.map +1 -0
  76. package/dist/es/dump/index.mjs +3 -0
  77. package/dist/es/index.mjs +15 -0
  78. package/dist/es/index.mjs.map +1 -0
  79. package/dist/es/report-generator.mjs +162 -0
  80. package/dist/es/report-generator.mjs.map +1 -0
  81. package/dist/es/report.mjs +137 -0
  82. package/dist/es/report.mjs.map +1 -0
  83. package/dist/es/screenshot-item.mjs +105 -0
  84. package/dist/es/screenshot-item.mjs.map +1 -0
  85. package/dist/es/service/index.mjs +274 -0
  86. package/dist/es/service/index.mjs.map +1 -0
  87. package/dist/es/service/utils.mjs +15 -0
  88. package/dist/es/service/utils.mjs.map +1 -0
  89. package/dist/es/skill/index.mjs +38 -0
  90. package/dist/es/skill/index.mjs.map +1 -0
  91. package/dist/es/task-runner.mjs +263 -0
  92. package/dist/es/task-runner.mjs.map +1 -0
  93. package/dist/es/task-timing.mjs +12 -0
  94. package/dist/es/task-timing.mjs.map +1 -0
  95. package/dist/es/tree.mjs +13 -0
  96. package/dist/es/tree.mjs.map +1 -0
  97. package/dist/es/types.mjs +199 -0
  98. package/dist/es/types.mjs.map +1 -0
  99. package/dist/es/utils.mjs +229 -0
  100. package/dist/es/utils.mjs.map +1 -0
  101. package/dist/es/yaml/builder.mjs +13 -0
  102. package/dist/es/yaml/builder.mjs.map +1 -0
  103. package/dist/es/yaml/index.mjs +4 -0
  104. package/dist/es/yaml/player.mjs +434 -0
  105. package/dist/es/yaml/player.mjs.map +1 -0
  106. package/dist/es/yaml/utils.mjs +102 -0
  107. package/dist/es/yaml/utils.mjs.map +1 -0
  108. package/dist/es/yaml.mjs +0 -0
  109. package/dist/lib/agent/agent.js +797 -0
  110. package/dist/lib/agent/agent.js.map +1 -0
  111. package/dist/lib/agent/common.js +5 -0
  112. package/dist/lib/agent/execution-session.js +75 -0
  113. package/dist/lib/agent/execution-session.js.map +1 -0
  114. package/dist/lib/agent/index.js +81 -0
  115. package/dist/lib/agent/index.js.map +1 -0
  116. package/dist/lib/agent/task-builder.js +369 -0
  117. package/dist/lib/agent/task-builder.js.map +1 -0
  118. package/dist/lib/agent/task-cache.js +266 -0
  119. package/dist/lib/agent/task-cache.js.map +1 -0
  120. package/dist/lib/agent/tasks.js +467 -0
  121. package/dist/lib/agent/tasks.js.map +1 -0
  122. package/dist/lib/agent/ui-utils.js +143 -0
  123. package/dist/lib/agent/ui-utils.js.map +1 -0
  124. package/dist/lib/agent/utils.js +275 -0
  125. package/dist/lib/agent/utils.js.map +1 -0
  126. package/dist/lib/ai-model/auto-glm/actions.js +271 -0
  127. package/dist/lib/ai-model/auto-glm/actions.js.map +1 -0
  128. package/dist/lib/ai-model/auto-glm/index.js +66 -0
  129. package/dist/lib/ai-model/auto-glm/index.js.map +1 -0
  130. package/dist/lib/ai-model/auto-glm/parser.js +282 -0
  131. package/dist/lib/ai-model/auto-glm/parser.js.map +1 -0
  132. package/dist/lib/ai-model/auto-glm/planning.js +105 -0
  133. package/dist/lib/ai-model/auto-glm/planning.js.map +1 -0
  134. package/dist/lib/ai-model/auto-glm/prompt.js +259 -0
  135. package/dist/lib/ai-model/auto-glm/prompt.js.map +1 -0
  136. package/dist/lib/ai-model/auto-glm/util.js +46 -0
  137. package/dist/lib/ai-model/auto-glm/util.js.map +1 -0
  138. package/dist/lib/ai-model/conversation-history.js +229 -0
  139. package/dist/lib/ai-model/conversation-history.js.map +1 -0
  140. package/dist/lib/ai-model/index.js +125 -0
  141. package/dist/lib/ai-model/index.js.map +1 -0
  142. package/dist/lib/ai-model/inspect.js +440 -0
  143. package/dist/lib/ai-model/inspect.js.map +1 -0
  144. package/dist/lib/ai-model/llm-planning.js +270 -0
  145. package/dist/lib/ai-model/llm-planning.js.map +1 -0
  146. package/dist/lib/ai-model/prompt/common.js +41 -0
  147. package/dist/lib/ai-model/prompt/common.js.map +1 -0
  148. package/dist/lib/ai-model/prompt/describe.js +100 -0
  149. package/dist/lib/ai-model/prompt/describe.js.map +1 -0
  150. package/dist/lib/ai-model/prompt/extraction.js +209 -0
  151. package/dist/lib/ai-model/prompt/extraction.js.map +1 -0
  152. package/dist/lib/ai-model/prompt/llm-locator.js +88 -0
  153. package/dist/lib/ai-model/prompt/llm-locator.js.map +1 -0
  154. package/dist/lib/ai-model/prompt/llm-planning.js +605 -0
  155. package/dist/lib/ai-model/prompt/llm-planning.js.map +1 -0
  156. package/dist/lib/ai-model/prompt/llm-section-locator.js +81 -0
  157. package/dist/lib/ai-model/prompt/llm-section-locator.js.map +1 -0
  158. package/dist/lib/ai-model/prompt/order-sensitive-judge.js +72 -0
  159. package/dist/lib/ai-model/prompt/order-sensitive-judge.js.map +1 -0
  160. package/dist/lib/ai-model/prompt/playwright-generator.js +178 -0
  161. package/dist/lib/ai-model/prompt/playwright-generator.js.map +1 -0
  162. package/dist/lib/ai-model/prompt/ui-tars-planning.js +73 -0
  163. package/dist/lib/ai-model/prompt/ui-tars-planning.js.map +1 -0
  164. package/dist/lib/ai-model/prompt/util.js +105 -0
  165. package/dist/lib/ai-model/prompt/util.js.map +1 -0
  166. package/dist/lib/ai-model/prompt/yaml-generator.js +280 -0
  167. package/dist/lib/ai-model/prompt/yaml-generator.js.map +1 -0
  168. package/dist/lib/ai-model/service-caller/codex-app-server.js +624 -0
  169. package/dist/lib/ai-model/service-caller/codex-app-server.js.map +1 -0
  170. package/dist/lib/ai-model/service-caller/image-detail.js +40 -0
  171. package/dist/lib/ai-model/service-caller/image-detail.js.map +1 -0
  172. package/dist/lib/ai-model/service-caller/index.js +538 -0
  173. package/dist/lib/ai-model/service-caller/index.js.map +1 -0
  174. package/dist/lib/ai-model/ui-tars-planning.js +283 -0
  175. package/dist/lib/ai-model/ui-tars-planning.js.map +1 -0
  176. package/dist/lib/common.js +480 -0
  177. package/dist/lib/common.js.map +1 -0
  178. package/dist/lib/device/device-options.js +20 -0
  179. package/dist/lib/device/device-options.js.map +1 -0
  180. package/dist/lib/device/index.js +468 -0
  181. package/dist/lib/device/index.js.map +1 -0
  182. package/dist/lib/dump/html-utils.js +357 -0
  183. package/dist/lib/dump/html-utils.js.map +1 -0
  184. package/dist/lib/dump/image-restoration.js +77 -0
  185. package/dist/lib/dump/image-restoration.js.map +1 -0
  186. package/dist/lib/dump/index.js +60 -0
  187. package/dist/lib/dump/index.js.map +1 -0
  188. package/dist/lib/index.js +146 -0
  189. package/dist/lib/index.js.map +1 -0
  190. package/dist/lib/report-generator.js +200 -0
  191. package/dist/lib/report-generator.js.map +1 -0
  192. package/dist/lib/report.js +171 -0
  193. package/dist/lib/report.js.map +1 -0
  194. package/dist/lib/screenshot-item.js +139 -0
  195. package/dist/lib/screenshot-item.js.map +1 -0
  196. package/dist/lib/service/index.js +308 -0
  197. package/dist/lib/service/index.js.map +1 -0
  198. package/dist/lib/service/utils.js +49 -0
  199. package/dist/lib/service/utils.js.map +1 -0
  200. package/dist/lib/skill/index.js +72 -0
  201. package/dist/lib/skill/index.js.map +1 -0
  202. package/dist/lib/task-runner.js +300 -0
  203. package/dist/lib/task-runner.js.map +1 -0
  204. package/dist/lib/task-timing.js +46 -0
  205. package/dist/lib/task-timing.js.map +1 -0
  206. package/dist/lib/tree.js +53 -0
  207. package/dist/lib/tree.js.map +1 -0
  208. package/dist/lib/types.js +288 -0
  209. package/dist/lib/types.js.map +1 -0
  210. package/dist/lib/utils.js +308 -0
  211. package/dist/lib/utils.js.map +1 -0
  212. package/dist/lib/yaml/builder.js +57 -0
  213. package/dist/lib/yaml/builder.js.map +1 -0
  214. package/dist/lib/yaml/index.js +81 -0
  215. package/dist/lib/yaml/index.js.map +1 -0
  216. package/dist/lib/yaml/player.js +468 -0
  217. package/dist/lib/yaml/player.js.map +1 -0
  218. package/dist/lib/yaml/utils.js +155 -0
  219. package/dist/lib/yaml/utils.js.map +1 -0
  220. package/dist/lib/yaml.js +20 -0
  221. package/dist/lib/yaml.js.map +1 -0
  222. package/dist/types/agent/agent.d.ts +205 -0
  223. package/dist/types/agent/common.d.ts +0 -0
  224. package/dist/types/agent/execution-session.d.ts +36 -0
  225. package/dist/types/agent/index.d.ts +10 -0
  226. package/dist/types/agent/task-builder.d.ts +34 -0
  227. package/dist/types/agent/task-cache.d.ts +49 -0
  228. package/dist/types/agent/tasks.d.ts +69 -0
  229. package/dist/types/agent/ui-utils.d.ts +14 -0
  230. package/dist/types/agent/utils.d.ts +31 -0
  231. package/dist/types/ai-model/auto-glm/actions.d.ts +78 -0
  232. package/dist/types/ai-model/auto-glm/index.d.ts +6 -0
  233. package/dist/types/ai-model/auto-glm/parser.d.ts +18 -0
  234. package/dist/types/ai-model/auto-glm/planning.d.ts +12 -0
  235. package/dist/types/ai-model/auto-glm/prompt.d.ts +27 -0
  236. package/dist/types/ai-model/auto-glm/util.d.ts +13 -0
  237. package/dist/types/ai-model/conversation-history.d.ts +105 -0
  238. package/dist/types/ai-model/index.d.ts +14 -0
  239. package/dist/types/ai-model/inspect.d.ts +67 -0
  240. package/dist/types/ai-model/llm-planning.d.ts +19 -0
  241. package/dist/types/ai-model/prompt/common.d.ts +2 -0
  242. package/dist/types/ai-model/prompt/describe.d.ts +1 -0
  243. package/dist/types/ai-model/prompt/extraction.d.ts +7 -0
  244. package/dist/types/ai-model/prompt/llm-locator.d.ts +3 -0
  245. package/dist/types/ai-model/prompt/llm-planning.d.ts +10 -0
  246. package/dist/types/ai-model/prompt/llm-section-locator.d.ts +3 -0
  247. package/dist/types/ai-model/prompt/order-sensitive-judge.d.ts +2 -0
  248. package/dist/types/ai-model/prompt/playwright-generator.d.ts +26 -0
  249. package/dist/types/ai-model/prompt/ui-tars-planning.d.ts +2 -0
  250. package/dist/types/ai-model/prompt/util.d.ts +33 -0
  251. package/dist/types/ai-model/prompt/yaml-generator.d.ts +100 -0
  252. package/dist/types/ai-model/service-caller/codex-app-server.d.ts +42 -0
  253. package/dist/types/ai-model/service-caller/image-detail.d.ts +2 -0
  254. package/dist/types/ai-model/service-caller/index.d.ts +49 -0
  255. package/dist/types/ai-model/ui-tars-planning.d.ts +72 -0
  256. package/dist/types/common.d.ts +288 -0
  257. package/dist/types/device/device-options.d.ts +142 -0
  258. package/dist/types/device/index.d.ts +2528 -0
  259. package/dist/types/dump/html-utils.d.ts +63 -0
  260. package/dist/types/dump/image-restoration.d.ts +6 -0
  261. package/dist/types/dump/index.d.ts +5 -0
  262. package/dist/types/index.d.ts +17 -0
  263. package/dist/types/report-generator.d.ts +66 -0
  264. package/dist/types/report.d.ts +22 -0
  265. package/dist/types/screenshot-item.d.ts +66 -0
  266. package/dist/types/service/index.d.ts +24 -0
  267. package/dist/types/service/utils.d.ts +2 -0
  268. package/dist/types/skill/index.d.ts +25 -0
  269. package/dist/types/task-runner.d.ts +50 -0
  270. package/dist/types/task-timing.d.ts +8 -0
  271. package/dist/types/tree.d.ts +4 -0
  272. package/dist/types/types.d.ts +669 -0
  273. package/dist/types/utils.d.ts +40 -0
  274. package/dist/types/yaml/builder.d.ts +2 -0
  275. package/dist/types/yaml/index.d.ts +4 -0
  276. package/dist/types/yaml/player.d.ts +34 -0
  277. package/dist/types/yaml/utils.d.ts +9 -0
  278. package/dist/types/yaml.d.ts +215 -0
  279. package/package.json +111 -0
@@ -0,0 +1,137 @@
1
+ import { appendFileSync, copyFileSync, existsSync, mkdirSync, readdirSync, rmSync, unlinkSync } from "node:fs";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
+ import { getMidsceneRunSubDir } from "@midscene/shared/common";
4
+ import { antiEscapeScriptTag, logMsg } from "@midscene/shared/utils";
5
+ import { getReportFileName } from "./agent/index.mjs";
6
+ import { extractAllDumpScriptsSync, extractLastDumpScriptSync, getBaseUrlFixScript, streamImageScriptsToFile } from "./dump/html-utils.mjs";
7
+ import { GroupedActionDump } from "./types.mjs";
8
+ import { getReportTpl, reportHTMLContent } from "./utils.mjs";
9
+ function _define_property(obj, key, value) {
10
+ if (key in obj) Object.defineProperty(obj, key, {
11
+ value: value,
12
+ enumerable: true,
13
+ configurable: true,
14
+ writable: true
15
+ });
16
+ else obj[key] = value;
17
+ return obj;
18
+ }
19
+ class ReportMergingTool {
20
+ append(reportInfo) {
21
+ this.reportInfos.push(reportInfo);
22
+ }
23
+ clear() {
24
+ this.reportInfos = [];
25
+ }
26
+ isDirectoryModeReport(reportFilePath) {
27
+ const reportDir = dirname(reportFilePath);
28
+ return 'index.html' === basename(reportFilePath) && existsSync(join(reportDir, 'screenshots'));
29
+ }
30
+ mergeDumpScripts(contents) {
31
+ const unescaped = contents.map((c)=>antiEscapeScriptTag(c)).filter((c)=>c.length > 0);
32
+ if (0 === unescaped.length) return '';
33
+ if (1 === unescaped.length) return unescaped[0];
34
+ const base = GroupedActionDump.fromSerializedString(unescaped[0]);
35
+ const allExecutions = [
36
+ ...base.executions
37
+ ];
38
+ for(let i = 1; i < unescaped.length; i++){
39
+ const other = GroupedActionDump.fromSerializedString(unescaped[i]);
40
+ allExecutions.push(...other.executions);
41
+ }
42
+ let noIdCounter = 0;
43
+ const deduped = new Map();
44
+ for (const exec of allExecutions){
45
+ const key = exec.id || `__no_id_${noIdCounter++}`;
46
+ deduped.set(key, exec);
47
+ }
48
+ base.executions = Array.from(deduped.values());
49
+ return base.serialize();
50
+ }
51
+ mergeReports(reportFileName = 'AUTO', opts) {
52
+ if (this.reportInfos.length <= 1) {
53
+ logMsg('Not enough reports to merge');
54
+ return null;
55
+ }
56
+ const { rmOriginalReports = false, overwrite = false } = opts ?? {};
57
+ const targetDir = getMidsceneRunSubDir('report');
58
+ const hasDirectoryModeReport = this.reportInfos.some((info)=>this.isDirectoryModeReport(info.reportFilePath));
59
+ const resolvedName = 'AUTO' === reportFileName ? getReportFileName('merged-report') : reportFileName;
60
+ const outputFilePath = hasDirectoryModeReport ? resolve(targetDir, resolvedName, 'index.html') : resolve(targetDir, `${resolvedName}.html`);
61
+ if ('AUTO' !== reportFileName && existsSync(outputFilePath)) {
62
+ if (!overwrite) throw new Error(`Report file already exists: ${outputFilePath}\nSet overwrite to true to overwrite this file.`);
63
+ if (hasDirectoryModeReport) rmSync(dirname(outputFilePath), {
64
+ recursive: true,
65
+ force: true
66
+ });
67
+ else unlinkSync(outputFilePath);
68
+ }
69
+ if (hasDirectoryModeReport) mkdirSync(dirname(outputFilePath), {
70
+ recursive: true
71
+ });
72
+ logMsg(`Start merging ${this.reportInfos.length} reports...\nCreating template file...`);
73
+ try {
74
+ appendFileSync(outputFilePath, getReportTpl());
75
+ if (hasDirectoryModeReport) appendFileSync(outputFilePath, getBaseUrlFixScript());
76
+ for(let i = 0; i < this.reportInfos.length; i++){
77
+ const reportInfo = this.reportInfos[i];
78
+ logMsg(`Processing report ${i + 1}/${this.reportInfos.length}`);
79
+ if (this.isDirectoryModeReport(reportInfo.reportFilePath)) {
80
+ const reportDir = dirname(reportInfo.reportFilePath);
81
+ const screenshotsDir = join(reportDir, 'screenshots');
82
+ const mergedScreenshotsDir = join(dirname(outputFilePath), 'screenshots');
83
+ mkdirSync(mergedScreenshotsDir, {
84
+ recursive: true
85
+ });
86
+ for (const file of readdirSync(screenshotsDir)){
87
+ const src = join(screenshotsDir, file);
88
+ const dest = join(mergedScreenshotsDir, file);
89
+ copyFileSync(src, dest);
90
+ }
91
+ } else streamImageScriptsToFile(reportInfo.reportFilePath, outputFilePath);
92
+ const allDumps = extractAllDumpScriptsSync(reportInfo.reportFilePath).filter((d)=>d.openTag.includes('data-group-id'));
93
+ const groupIdMatch = allDumps[0]?.openTag.match(/data-group-id="([^"]+)"/);
94
+ const mergedGroupId = groupIdMatch ? decodeURIComponent(groupIdMatch[1]) : `merged-group-${i}`;
95
+ const dumpString = allDumps.length > 0 ? this.mergeDumpScripts(allDumps.map((d)=>d.content)) : extractLastDumpScriptSync(reportInfo.reportFilePath);
96
+ const { reportAttributes } = reportInfo;
97
+ const reportHtmlStr = `${reportHTMLContent({
98
+ dumpString,
99
+ attributes: {
100
+ 'data-group-id': mergedGroupId,
101
+ playwright_test_duration: reportAttributes.testDuration,
102
+ playwright_test_status: reportAttributes.testStatus,
103
+ playwright_test_title: reportAttributes.testTitle,
104
+ playwright_test_id: reportAttributes.testId,
105
+ playwright_test_description: reportAttributes.testDescription
106
+ }
107
+ }, void 0, void 0, false)}\n`;
108
+ appendFileSync(outputFilePath, reportHtmlStr);
109
+ }
110
+ logMsg(`Successfully merged new report: ${outputFilePath}`);
111
+ if (rmOriginalReports) {
112
+ for (const info of this.reportInfos)try {
113
+ if (this.isDirectoryModeReport(info.reportFilePath)) {
114
+ const reportDir = dirname(info.reportFilePath);
115
+ rmSync(reportDir, {
116
+ recursive: true,
117
+ force: true
118
+ });
119
+ } else unlinkSync(info.reportFilePath);
120
+ } catch (error) {
121
+ logMsg(`Error deleting report ${info.reportFilePath}: ${error}`);
122
+ }
123
+ logMsg(`Removed ${this.reportInfos.length} original reports`);
124
+ }
125
+ return outputFilePath;
126
+ } catch (error) {
127
+ logMsg(`Error in mergeReports: ${error}`);
128
+ throw error;
129
+ }
130
+ }
131
+ constructor(){
132
+ _define_property(this, "reportInfos", []);
133
+ }
134
+ }
135
+ export { ReportMergingTool };
136
+
137
+ //# sourceMappingURL=report.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.mjs","sources":["../../src/report.ts"],"sourcesContent":["import {\n appendFileSync,\n copyFileSync,\n existsSync,\n mkdirSync,\n readdirSync,\n rmSync,\n unlinkSync,\n} from 'node:fs';\nimport * as path from 'node:path';\nimport { getMidsceneRunSubDir } from '@midscene/shared/common';\nimport { antiEscapeScriptTag, logMsg } from '@midscene/shared/utils';\nimport { getReportFileName } from './agent';\nimport {\n extractAllDumpScriptsSync,\n extractLastDumpScriptSync,\n getBaseUrlFixScript,\n streamImageScriptsToFile,\n} from './dump/html-utils';\nimport { GroupedActionDump } from './types';\nimport type { ReportFileWithAttributes } from './types';\nimport { getReportTpl, reportHTMLContent } from './utils';\n\nexport class ReportMergingTool {\n private reportInfos: ReportFileWithAttributes[] = [];\n public append(reportInfo: ReportFileWithAttributes) {\n this.reportInfos.push(reportInfo);\n }\n public clear() {\n this.reportInfos = [];\n }\n\n /**\n * Check if a report is in directory mode (html-and-external-assets).\n * Directory mode reports: {name}/index.html + {name}/screenshots/\n */\n private isDirectoryModeReport(reportFilePath: string): boolean {\n const reportDir = path.dirname(reportFilePath);\n return (\n path.basename(reportFilePath) === 'index.html' &&\n existsSync(path.join(reportDir, 'screenshots'))\n );\n }\n\n /**\n * Merge multiple dump script contents (from the same source report)\n * into a single serialized GroupedActionDump string.\n * If there's only one dump, return it as-is. If multiple, merge\n * all executions into the first dump's group structure.\n */\n private mergeDumpScripts(contents: string[]): string {\n const unescaped = contents\n .map((c) => antiEscapeScriptTag(c))\n .filter((c) => c.length > 0);\n if (unescaped.length === 0) return '';\n if (unescaped.length === 1) return unescaped[0];\n\n // Parse all dumps and collect executions, deduplicating by id (keep last).\n // Only executions with a stable id are deduped; old-format entries without\n // id are always kept (they may be distinct despite sharing the same name).\n const base = GroupedActionDump.fromSerializedString(unescaped[0]);\n const allExecutions = [...base.executions];\n for (let i = 1; i < unescaped.length; i++) {\n const other = GroupedActionDump.fromSerializedString(unescaped[i]);\n allExecutions.push(...other.executions);\n }\n let noIdCounter = 0;\n const deduped = new Map<string, (typeof allExecutions)[0]>();\n for (const exec of allExecutions) {\n const key = exec.id || `__no_id_${noIdCounter++}`;\n deduped.set(key, exec);\n }\n base.executions = Array.from(deduped.values());\n return base.serialize();\n }\n\n public mergeReports(\n reportFileName: 'AUTO' | string = 'AUTO',\n opts?: {\n rmOriginalReports?: boolean;\n overwrite?: boolean;\n },\n ): string | null {\n if (this.reportInfos.length <= 1) {\n logMsg('Not enough reports to merge');\n return null;\n }\n\n const { rmOriginalReports = false, overwrite = false } = opts ?? {};\n const targetDir = getMidsceneRunSubDir('report');\n\n // Check if any source report is directory mode\n const hasDirectoryModeReport = this.reportInfos.some((info) =>\n this.isDirectoryModeReport(info.reportFilePath),\n );\n\n const resolvedName =\n reportFileName === 'AUTO'\n ? getReportFileName('merged-report')\n : reportFileName;\n\n // Directory mode: output as {name}/index.html to keep relative paths working\n // Inline mode: output as {name}.html (single file)\n const outputFilePath = hasDirectoryModeReport\n ? path.resolve(targetDir, resolvedName, 'index.html')\n : path.resolve(targetDir, `${resolvedName}.html`);\n\n if (reportFileName !== 'AUTO' && existsSync(outputFilePath)) {\n if (!overwrite) {\n throw new Error(\n `Report file already exists: ${outputFilePath}\\nSet overwrite to true to overwrite this file.`,\n );\n }\n if (hasDirectoryModeReport) {\n rmSync(path.dirname(outputFilePath), { recursive: true, force: true });\n } else {\n unlinkSync(outputFilePath);\n }\n }\n\n if (hasDirectoryModeReport) {\n mkdirSync(path.dirname(outputFilePath), { recursive: true });\n }\n\n logMsg(\n `Start merging ${this.reportInfos.length} reports...\\nCreating template file...`,\n );\n\n try {\n // Write template\n appendFileSync(outputFilePath, getReportTpl());\n\n // For directory-mode output, inject base URL fix script\n if (hasDirectoryModeReport) {\n appendFileSync(outputFilePath, getBaseUrlFixScript());\n }\n\n // Process all reports one by one\n for (let i = 0; i < this.reportInfos.length; i++) {\n const reportInfo = this.reportInfos[i];\n logMsg(`Processing report ${i + 1}/${this.reportInfos.length}`);\n\n if (this.isDirectoryModeReport(reportInfo.reportFilePath)) {\n // Directory mode: copy external screenshot files\n const reportDir = path.dirname(reportInfo.reportFilePath);\n const screenshotsDir = path.join(reportDir, 'screenshots');\n const mergedScreenshotsDir = path.join(\n path.dirname(outputFilePath),\n 'screenshots',\n );\n mkdirSync(mergedScreenshotsDir, { recursive: true });\n for (const file of readdirSync(screenshotsDir)) {\n const src = path.join(screenshotsDir, file);\n const dest = path.join(mergedScreenshotsDir, file);\n copyFileSync(src, dest);\n }\n } else {\n // Inline mode: stream image scripts to output file\n streamImageScriptsToFile(reportInfo.reportFilePath, outputFilePath);\n }\n\n // Extract all dump scripts from the source report.\n // After the per-execution append refactor, a single source report\n // may contain multiple <script type=\"midscene_web_dump\"> tags\n // (one per execution). We merge them into a single GroupedActionDump.\n // Filter by data-group-id to exclude false matches from the template's\n // bundled JS code, which also references the midscene_web_dump type string.\n const allDumps = extractAllDumpScriptsSync(\n reportInfo.reportFilePath,\n ).filter((d) => d.openTag.includes('data-group-id'));\n const groupIdMatch = allDumps[0]?.openTag.match(\n /data-group-id=\"([^\"]+)\"/,\n );\n const mergedGroupId = groupIdMatch\n ? decodeURIComponent(groupIdMatch[1])\n : `merged-group-${i}`;\n const dumpString =\n allDumps.length > 0\n ? this.mergeDumpScripts(allDumps.map((d) => d.content))\n : extractLastDumpScriptSync(reportInfo.reportFilePath);\n const { reportAttributes } = reportInfo;\n\n const reportHtmlStr = `${reportHTMLContent(\n {\n dumpString,\n attributes: {\n 'data-group-id': mergedGroupId,\n playwright_test_duration: reportAttributes.testDuration,\n playwright_test_status: reportAttributes.testStatus,\n playwright_test_title: reportAttributes.testTitle,\n playwright_test_id: reportAttributes.testId,\n playwright_test_description: reportAttributes.testDescription,\n },\n },\n undefined,\n undefined,\n false,\n )}\\n`;\n\n appendFileSync(outputFilePath, reportHtmlStr);\n }\n\n logMsg(`Successfully merged new report: ${outputFilePath}`);\n\n // Remove original reports if needed\n if (rmOriginalReports) {\n for (const info of this.reportInfos) {\n try {\n if (this.isDirectoryModeReport(info.reportFilePath)) {\n // Directory mode: remove the entire report directory\n const reportDir = path.dirname(info.reportFilePath);\n rmSync(reportDir, { recursive: true, force: true });\n } else {\n unlinkSync(info.reportFilePath);\n }\n } catch (error) {\n logMsg(`Error deleting report ${info.reportFilePath}: ${error}`);\n }\n }\n logMsg(`Removed ${this.reportInfos.length} original reports`);\n }\n return outputFilePath;\n } catch (error) {\n logMsg(`Error in mergeReports: ${error}`);\n throw error;\n }\n }\n}\n"],"names":["ReportMergingTool","reportInfo","reportFilePath","reportDir","path","existsSync","contents","unescaped","c","antiEscapeScriptTag","base","GroupedActionDump","allExecutions","i","other","noIdCounter","deduped","Map","exec","key","Array","reportFileName","opts","logMsg","rmOriginalReports","overwrite","targetDir","getMidsceneRunSubDir","hasDirectoryModeReport","info","resolvedName","getReportFileName","outputFilePath","Error","rmSync","unlinkSync","mkdirSync","appendFileSync","getReportTpl","getBaseUrlFixScript","screenshotsDir","mergedScreenshotsDir","file","readdirSync","src","dest","copyFileSync","streamImageScriptsToFile","allDumps","extractAllDumpScriptsSync","d","groupIdMatch","mergedGroupId","decodeURIComponent","dumpString","extractLastDumpScriptSync","reportAttributes","reportHtmlStr","reportHTMLContent","undefined","error"],"mappings":"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"}
@@ -0,0 +1,105 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { uuid } from "@midscene/shared/utils";
3
+ import { extractImageByIdSync } from "./dump/html-utils.mjs";
4
+ function _define_property(obj, key, value) {
5
+ if (key in obj) Object.defineProperty(obj, key, {
6
+ value: value,
7
+ enumerable: true,
8
+ configurable: true,
9
+ writable: true
10
+ });
11
+ else obj[key] = value;
12
+ return obj;
13
+ }
14
+ function detectFormat(base64) {
15
+ if (base64.startsWith('data:image/jpeg')) return 'jpeg';
16
+ if (base64.startsWith('data:image/jpg')) return 'jpeg';
17
+ return 'png';
18
+ }
19
+ class ScreenshotItem {
20
+ static create(base64, capturedAt) {
21
+ return new ScreenshotItem(uuid(), base64, capturedAt);
22
+ }
23
+ get id() {
24
+ return this._id;
25
+ }
26
+ get format() {
27
+ return this._format;
28
+ }
29
+ get extension() {
30
+ return 'jpeg' === this._format ? 'jpeg' : 'png';
31
+ }
32
+ get capturedAt() {
33
+ return this._capturedAt;
34
+ }
35
+ get base64() {
36
+ if (null !== this._base64) return this._base64;
37
+ if (null !== this._persistedPath) {
38
+ const buffer = readFileSync(this._persistedPath);
39
+ return `data:image/${this._format};base64,${buffer.toString('base64')}`;
40
+ }
41
+ if (null !== this._persistedHtmlPath) {
42
+ const data = extractImageByIdSync(this._persistedHtmlPath, this._id);
43
+ if (data) return data;
44
+ throw new Error(`Screenshot ${this._id}: cannot recover from HTML (id not found in ${this._persistedHtmlPath})`);
45
+ }
46
+ throw new Error(`Screenshot ${this._id}: base64 data released without recovery path`);
47
+ }
48
+ hasBase64() {
49
+ return null !== this._base64;
50
+ }
51
+ markPersistedInline(htmlPath) {
52
+ this._persistedAs = {
53
+ $screenshot: this._id,
54
+ capturedAt: this._capturedAt
55
+ };
56
+ this._persistedHtmlPath = htmlPath;
57
+ this._base64 = null;
58
+ }
59
+ markPersistedToPath(relativePath, absolutePath) {
60
+ this._persistedAs = {
61
+ base64: relativePath,
62
+ capturedAt: this._capturedAt
63
+ };
64
+ this._persistedPath = absolutePath;
65
+ this._base64 = null;
66
+ }
67
+ toSerializable() {
68
+ return this._persistedAs ?? {
69
+ $screenshot: this._id,
70
+ capturedAt: this._capturedAt
71
+ };
72
+ }
73
+ static isSerialized(value) {
74
+ if ('object' != typeof value || null === value) return false;
75
+ const record = value;
76
+ if ('$screenshot' in record && 'string' == typeof record.$screenshot) {
77
+ if (!('capturedAt' in record) || 'number' != typeof record.capturedAt) return false;
78
+ return true;
79
+ }
80
+ if ('base64' in record && 'string' == typeof record.base64) {
81
+ if (!('capturedAt' in record) || 'number' != typeof record.capturedAt) return false;
82
+ return true;
83
+ }
84
+ return false;
85
+ }
86
+ get rawBase64() {
87
+ return this.base64.replace(/^data:image\/(png|jpeg|jpg);base64,/, '');
88
+ }
89
+ constructor(id, base64, capturedAt){
90
+ _define_property(this, "_id", void 0);
91
+ _define_property(this, "_base64", void 0);
92
+ _define_property(this, "_format", void 0);
93
+ _define_property(this, "_capturedAt", void 0);
94
+ _define_property(this, "_persistedAs", null);
95
+ _define_property(this, "_persistedPath", null);
96
+ _define_property(this, "_persistedHtmlPath", null);
97
+ this._id = id;
98
+ this._base64 = base64;
99
+ this._format = detectFormat(base64);
100
+ this._capturedAt = capturedAt;
101
+ }
102
+ }
103
+ export { ScreenshotItem };
104
+
105
+ //# sourceMappingURL=screenshot-item.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screenshot-item.mjs","sources":["../../src/screenshot-item.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { uuid } from '@midscene/shared/utils';\nimport { extractImageByIdSync } from './dump/html-utils';\n\n/**\n * Serialization format for ScreenshotItem\n * - { $screenshot: \"id\" } - inline mode, references imageMap in HTML\n * - { base64: \"path\" } - directory mode, references external file path\n */\nexport type ScreenshotSerializeFormat =\n | { $screenshot: string; capturedAt: number }\n | { base64: string; capturedAt: number };\n\n/**\n * Detect image format from base64 data URI prefix.\n */\nfunction detectFormat(base64: string): 'png' | 'jpeg' {\n if (base64.startsWith('data:image/jpeg')) return 'jpeg';\n if (base64.startsWith('data:image/jpg')) return 'jpeg';\n return 'png';\n}\n\n/**\n * ScreenshotItem encapsulates screenshot data.\n *\n * Supports lazy loading after memory release:\n * - inline mode: reads from HTML file using streaming (extractImageByIdSync)\n * - directory mode: reads from file on disk\n *\n * After persistence, memory is released but the screenshot can be recovered\n * on-demand from disk, making it safe to release memory at any time.\n */\nexport class ScreenshotItem {\n private _id: string;\n private _base64: string | null;\n private _format: 'png' | 'jpeg';\n private _capturedAt: number;\n private _persistedAs: ScreenshotSerializeFormat | null = null;\n private _persistedPath: string | null = null;\n private _persistedHtmlPath: string | null = null;\n\n private constructor(id: string, base64: string, capturedAt: number) {\n this._id = id;\n this._base64 = base64;\n this._format = detectFormat(base64);\n this._capturedAt = capturedAt;\n }\n\n /** Create a new ScreenshotItem from base64 data */\n static create(base64: string, capturedAt: number): ScreenshotItem {\n return new ScreenshotItem(uuid(), base64, capturedAt);\n }\n\n get id(): string {\n return this._id;\n }\n\n /** Get the image format (png or jpeg) */\n get format(): 'png' | 'jpeg' {\n return this._format;\n }\n\n /** Get the file extension for this screenshot */\n get extension(): string {\n return this._format === 'jpeg' ? 'jpeg' : 'png';\n }\n\n /** Get screenshot capture timestamp in milliseconds */\n get capturedAt(): number {\n return this._capturedAt;\n }\n\n get base64(): string {\n // If data is in memory, return it directly\n if (this._base64 !== null) {\n return this._base64;\n }\n\n // Directory mode: recover from file\n if (this._persistedPath !== null) {\n const buffer = readFileSync(this._persistedPath);\n return `data:image/${this._format};base64,${buffer.toString('base64')}`;\n }\n\n // Inline mode: recover from HTML file using streaming\n if (this._persistedHtmlPath !== null) {\n const data = extractImageByIdSync(this._persistedHtmlPath, this._id);\n if (data) {\n return data;\n }\n throw new Error(\n `Screenshot ${this._id}: cannot recover from HTML (id not found in ${this._persistedHtmlPath})`,\n );\n }\n\n throw new Error(\n `Screenshot ${this._id}: base64 data released without recovery path`,\n );\n }\n\n /** Check if base64 data is still available in memory (not yet released) */\n hasBase64(): boolean {\n return this._base64 !== null;\n }\n\n /**\n * Mark as persisted to HTML (inline mode).\n * Releases base64 memory, but keeps HTML path for lazy loading recovery.\n * @param htmlPath - absolute path to the HTML file containing the image\n */\n markPersistedInline(htmlPath: string): void {\n this._persistedAs = {\n $screenshot: this._id,\n capturedAt: this._capturedAt,\n };\n this._persistedHtmlPath = htmlPath;\n this._base64 = null;\n }\n\n /**\n * Mark as persisted to file (directory mode).\n * Releases base64 memory, but keeps file path for lazy loading recovery.\n * @param relativePath - relative path for serialization (e.g., \"./screenshots/id.jpeg\")\n * @param absolutePath - absolute path for lazy loading recovery\n */\n markPersistedToPath(relativePath: string, absolutePath: string): void {\n this._persistedAs = {\n base64: relativePath,\n capturedAt: this._capturedAt,\n };\n this._persistedPath = absolutePath;\n this._base64 = null;\n }\n\n /** Serialize for JSON - format depends on persistence state */\n toSerializable(): ScreenshotSerializeFormat {\n return (\n this._persistedAs ?? {\n $screenshot: this._id,\n capturedAt: this._capturedAt,\n }\n );\n }\n\n /** Check if a value is a serialized ScreenshotItem reference (inline or directory mode) */\n static isSerialized(value: unknown): value is ScreenshotSerializeFormat {\n if (typeof value !== 'object' || value === null) return false;\n const record = value as Record<string, unknown>;\n // Check for inline mode: { $screenshot: string }\n if ('$screenshot' in record && typeof record.$screenshot === 'string') {\n if (!('capturedAt' in record) || typeof record.capturedAt !== 'number') {\n return false;\n }\n return true;\n }\n // Check for directory mode: { base64: string } where base64 is a path\n if ('base64' in record && typeof record.base64 === 'string') {\n if (!('capturedAt' in record) || typeof record.capturedAt !== 'number') {\n return false;\n }\n return true;\n }\n return false;\n }\n\n /**\n * Get base64 data without the data URI prefix.\n * Useful for writing raw binary data to files.\n */\n get rawBase64(): string {\n return this.base64.replace(/^data:image\\/(png|jpeg|jpg);base64,/, '');\n }\n}\n"],"names":["detectFormat","base64","ScreenshotItem","capturedAt","uuid","buffer","readFileSync","data","extractImageByIdSync","Error","htmlPath","relativePath","absolutePath","value","record","id"],"mappings":"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"}
@@ -0,0 +1,274 @@
1
+ import { isAutoGLM } from "../ai-model/auto-glm/util.mjs";
2
+ import { AIResponseParseError, AiExtractElementInfo, AiLocateElement, callAIWithObjectResponse } from "../ai-model/index.mjs";
3
+ import { AiLocateSection, buildSearchAreaConfig } from "../ai-model/inspect.mjs";
4
+ import { elementDescriberInstruction } from "../ai-model/prompt/describe.mjs";
5
+ import { expandSearchArea } from "../common.mjs";
6
+ import { ServiceError } from "../types.mjs";
7
+ import { compositeElementInfoImg, cropByRect } from "@midscene/shared/img";
8
+ import { getDebug } from "@midscene/shared/logger";
9
+ import { assert } from "@midscene/shared/utils";
10
+ import { createServiceDump } from "./utils.mjs";
11
+ function _define_property(obj, key, value) {
12
+ if (key in obj) Object.defineProperty(obj, key, {
13
+ value: value,
14
+ enumerable: true,
15
+ configurable: true,
16
+ writable: true
17
+ });
18
+ else obj[key] = value;
19
+ return obj;
20
+ }
21
+ const debug = getDebug('ai:service');
22
+ class Service {
23
+ async locate(query, opt, modelConfig, abortSignal) {
24
+ const queryPrompt = 'string' == typeof query ? query : query.prompt;
25
+ assert(queryPrompt, 'query is required for locate');
26
+ assert('object' == typeof query, 'query should be an object for locate');
27
+ const hasPlanLocatedElement = !!opt?.planLocatedElement?.rect;
28
+ let searchAreaPrompt;
29
+ if (query.deepLocate && !hasPlanLocatedElement) searchAreaPrompt = query.prompt;
30
+ const { modelFamily } = modelConfig;
31
+ if (searchAreaPrompt && !modelFamily) {
32
+ console.warn('The "deepLocate" feature is not supported with multimodal LLM. Please config VL model for Midscene. https://midscenejs.com/model-config');
33
+ searchAreaPrompt = void 0;
34
+ }
35
+ if (searchAreaPrompt && isAutoGLM(modelFamily)) {
36
+ console.warn('The "deepLocate" feature is not supported with AutoGLM.');
37
+ searchAreaPrompt = void 0;
38
+ }
39
+ const context = opt?.context || await this.contextRetrieverFn();
40
+ let searchArea;
41
+ let searchAreaRawResponse;
42
+ let searchAreaUsage;
43
+ let searchAreaResponse;
44
+ if (query.deepLocate && hasPlanLocatedElement) {
45
+ const searchAreaConfig = await buildSearchAreaConfig({
46
+ context,
47
+ baseRect: opt.planLocatedElement.rect,
48
+ modelFamily
49
+ });
50
+ searchArea = searchAreaConfig.rect;
51
+ searchAreaRawResponse = JSON.stringify({
52
+ source: 'plan-located-element',
53
+ rect: opt.planLocatedElement.rect
54
+ });
55
+ searchAreaResponse = {
56
+ rect: searchArea,
57
+ imageBase64: searchAreaConfig.imageBase64,
58
+ scale: searchAreaConfig.scale,
59
+ rawResponse: searchAreaRawResponse
60
+ };
61
+ } else if (searchAreaPrompt) {
62
+ searchAreaResponse = await AiLocateSection({
63
+ context,
64
+ sectionDescription: searchAreaPrompt,
65
+ modelConfig,
66
+ abortSignal
67
+ });
68
+ assert(searchAreaResponse.rect, `cannot find search area for "${searchAreaPrompt}"${searchAreaResponse.error ? `: ${searchAreaResponse.error}` : ''}`);
69
+ searchAreaRawResponse = searchAreaResponse.rawResponse;
70
+ searchAreaUsage = searchAreaResponse.usage;
71
+ searchArea = searchAreaResponse.rect;
72
+ }
73
+ const startTime = Date.now();
74
+ const { parseResult, rect, rawResponse, usage, reasoning_content } = await AiLocateElement({
75
+ context,
76
+ targetElementDescription: queryPrompt,
77
+ searchConfig: searchAreaResponse,
78
+ modelConfig,
79
+ abortSignal
80
+ });
81
+ const timeCost = Date.now() - startTime;
82
+ const taskInfo = {
83
+ ...this.taskInfo ? this.taskInfo : {},
84
+ durationMs: timeCost,
85
+ rawResponse: JSON.stringify(rawResponse),
86
+ formatResponse: JSON.stringify(parseResult),
87
+ usage,
88
+ searchArea,
89
+ searchAreaRawResponse,
90
+ searchAreaUsage,
91
+ reasoning_content
92
+ };
93
+ let errorLog;
94
+ if (parseResult.errors?.length) errorLog = `failed to locate element: \n${parseResult.errors.join('\n')}`;
95
+ const dumpData = {
96
+ type: 'locate',
97
+ userQuery: {
98
+ element: queryPrompt
99
+ },
100
+ matchedElement: [],
101
+ matchedRect: rect,
102
+ data: null,
103
+ taskInfo,
104
+ deepLocate: !!searchArea,
105
+ error: errorLog
106
+ };
107
+ const elements = parseResult.elements || [];
108
+ const dump = createServiceDump({
109
+ ...dumpData,
110
+ matchedElement: elements
111
+ });
112
+ if (errorLog) throw new ServiceError(errorLog, dump);
113
+ if (elements.length > 1) throw new ServiceError(`locate: multiple elements found, length = ${elements.length}`, dump);
114
+ if (1 === elements.length) return {
115
+ element: {
116
+ center: elements[0].center,
117
+ rect: elements[0].rect,
118
+ description: elements[0].description
119
+ },
120
+ rect,
121
+ dump
122
+ };
123
+ return {
124
+ element: null,
125
+ rect,
126
+ dump
127
+ };
128
+ }
129
+ async extract(dataDemand, modelConfig, opt, pageDescription, multimodalPrompt, context) {
130
+ assert(context, 'context is required for extract');
131
+ assert('object' == typeof dataDemand || 'string' == typeof dataDemand, `dataDemand should be object or string, but get ${typeof dataDemand}`);
132
+ const startTime = Date.now();
133
+ let parseResult;
134
+ let rawResponse;
135
+ let usage;
136
+ let reasoning_content;
137
+ try {
138
+ const result = await AiExtractElementInfo({
139
+ context,
140
+ dataQuery: dataDemand,
141
+ multimodalPrompt,
142
+ extractOption: opt,
143
+ modelConfig,
144
+ pageDescription
145
+ });
146
+ parseResult = result.parseResult;
147
+ rawResponse = result.rawResponse;
148
+ usage = result.usage;
149
+ reasoning_content = result.reasoning_content;
150
+ } catch (error) {
151
+ if (error instanceof AIResponseParseError) {
152
+ const timeCost = Date.now() - startTime;
153
+ const taskInfo = {
154
+ ...this.taskInfo ? this.taskInfo : {},
155
+ durationMs: timeCost,
156
+ rawResponse: error.rawResponse,
157
+ usage: error.usage
158
+ };
159
+ const dump = createServiceDump({
160
+ type: 'extract',
161
+ userQuery: {
162
+ dataDemand
163
+ },
164
+ matchedElement: [],
165
+ data: null,
166
+ taskInfo,
167
+ error: error.message
168
+ });
169
+ throw new ServiceError(error.message, dump);
170
+ }
171
+ throw error;
172
+ }
173
+ const timeCost = Date.now() - startTime;
174
+ const taskInfo = {
175
+ ...this.taskInfo ? this.taskInfo : {},
176
+ durationMs: timeCost,
177
+ rawResponse,
178
+ formatResponse: JSON.stringify(parseResult),
179
+ usage,
180
+ reasoning_content
181
+ };
182
+ let errorLog;
183
+ if (parseResult.errors?.length) errorLog = `AI response error: \n${parseResult.errors.join('\n')}`;
184
+ const dumpData = {
185
+ type: 'extract',
186
+ userQuery: {
187
+ dataDemand
188
+ },
189
+ matchedElement: [],
190
+ data: null,
191
+ taskInfo,
192
+ error: errorLog
193
+ };
194
+ const { data, thought } = parseResult || {};
195
+ const dump = createServiceDump({
196
+ ...dumpData,
197
+ data
198
+ });
199
+ if (errorLog && !data) throw new ServiceError(errorLog, dump);
200
+ return {
201
+ data,
202
+ thought,
203
+ usage,
204
+ reasoning_content,
205
+ dump
206
+ };
207
+ }
208
+ async describe(target, modelConfig, opt) {
209
+ assert(target, 'target is required for service.describe');
210
+ const context = await this.contextRetrieverFn();
211
+ const { shotSize } = context;
212
+ const screenshotBase64 = context.screenshot.base64;
213
+ assert(screenshotBase64, 'screenshot is required for service.describe');
214
+ const { modelFamily } = modelConfig;
215
+ const systemPrompt = elementDescriberInstruction();
216
+ const defaultRectSize = 30;
217
+ const targetRect = Array.isArray(target) ? {
218
+ left: Math.floor(target[0] - defaultRectSize / 2),
219
+ top: Math.floor(target[1] - defaultRectSize / 2),
220
+ width: defaultRectSize,
221
+ height: defaultRectSize
222
+ } : target;
223
+ let imagePayload = await compositeElementInfoImg({
224
+ inputImgBase64: screenshotBase64,
225
+ size: shotSize,
226
+ elementsPositionInfo: [
227
+ {
228
+ rect: targetRect
229
+ }
230
+ ],
231
+ borderThickness: 3
232
+ });
233
+ if (opt?.deepLocate) {
234
+ const searchArea = expandSearchArea(targetRect, shotSize);
235
+ debug('describe: cropping to searchArea', searchArea);
236
+ const croppedResult = await cropByRect(imagePayload, searchArea, 'qwen2.5-vl' === modelFamily);
237
+ imagePayload = croppedResult.imageBase64;
238
+ }
239
+ const msgs = [
240
+ {
241
+ role: 'system',
242
+ content: systemPrompt
243
+ },
244
+ {
245
+ role: 'user',
246
+ content: [
247
+ {
248
+ type: 'image_url',
249
+ image_url: {
250
+ url: imagePayload,
251
+ detail: 'high'
252
+ }
253
+ }
254
+ ]
255
+ }
256
+ ];
257
+ const res = await callAIWithObjectResponse(msgs, modelConfig);
258
+ const { content } = res;
259
+ assert(!content.error, `describe failed: ${content.error}`);
260
+ assert(content.description, 'failed to describe the element');
261
+ return content;
262
+ }
263
+ constructor(context, opt){
264
+ _define_property(this, "contextRetrieverFn", void 0);
265
+ _define_property(this, "taskInfo", void 0);
266
+ assert(context, 'context is required for Service');
267
+ if ('function' == typeof context) this.contextRetrieverFn = context;
268
+ else this.contextRetrieverFn = ()=>Promise.resolve(context);
269
+ if (void 0 !== opt?.taskInfo) this.taskInfo = opt.taskInfo;
270
+ }
271
+ }
272
+ export { Service as default };
273
+
274
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service/index.mjs","sources":["../../../src/service/index.ts"],"sourcesContent":["import { isAutoGLM, isUITars } from '@/ai-model/auto-glm/util';\nimport {\n AIResponseParseError,\n AiExtractElementInfo,\n AiLocateElement,\n callAIWithObjectResponse,\n} from '@/ai-model/index';\nimport { AiLocateSection, buildSearchAreaConfig } from '@/ai-model/inspect';\nimport { elementDescriberInstruction } from '@/ai-model/prompt/describe';\nimport { type AIArgs, expandSearchArea } from '@/common';\nimport type {\n AIDescribeElementResponse,\n AIUsageInfo,\n DetailedLocateParam,\n LocateResultElement,\n LocateResultWithDump,\n PartialServiceDumpFromSDK,\n PlanningLocateParam,\n Rect,\n ServiceExtractOption,\n ServiceExtractParam,\n ServiceExtractResult,\n ServiceTaskInfo,\n UIContext,\n} from '@/types';\nimport { ServiceError } from '@/types';\nimport type { IModelConfig } from '@midscene/shared/env';\nimport { compositeElementInfoImg, cropByRect } from '@midscene/shared/img';\nimport { getDebug } from '@midscene/shared/logger';\nimport { assert } from '@midscene/shared/utils';\nimport type { TMultimodalPrompt } from '../common';\nimport { createServiceDump } from './utils';\n\nexport interface LocateOpts {\n context?: UIContext;\n planLocatedElement?: LocateResultElement;\n}\n\nexport type AnyValue<T> = {\n [K in keyof T]: unknown extends T[K] ? any : T[K];\n};\n\ninterface ServiceOptions {\n taskInfo?: Omit<ServiceTaskInfo, 'durationMs'>;\n}\n\nconst debug = getDebug('ai:service');\nexport default class Service {\n contextRetrieverFn: () => Promise<UIContext> | UIContext;\n\n taskInfo?: Omit<ServiceTaskInfo, 'durationMs'>;\n\n constructor(\n context: UIContext | (() => Promise<UIContext> | UIContext),\n opt?: ServiceOptions,\n ) {\n assert(context, 'context is required for Service');\n if (typeof context === 'function') {\n this.contextRetrieverFn = context;\n } else {\n this.contextRetrieverFn = () => Promise.resolve(context);\n }\n\n if (typeof opt?.taskInfo !== 'undefined') {\n this.taskInfo = opt.taskInfo;\n }\n }\n\n async locate(\n query: PlanningLocateParam,\n opt: LocateOpts,\n modelConfig: IModelConfig,\n abortSignal?: AbortSignal,\n ): Promise<LocateResultWithDump> {\n const queryPrompt = typeof query === 'string' ? query : query.prompt;\n assert(queryPrompt, 'query is required for locate');\n\n assert(typeof query === 'object', 'query should be an object for locate');\n\n const hasPlanLocatedElement = !!opt?.planLocatedElement?.rect;\n\n let searchAreaPrompt;\n if (query.deepLocate && !hasPlanLocatedElement) {\n searchAreaPrompt = query.prompt;\n }\n\n const { modelFamily } = modelConfig;\n\n if (searchAreaPrompt && !modelFamily) {\n console.warn(\n 'The \"deepLocate\" feature is not supported with multimodal LLM. Please config VL model for Midscene. https://midscenejs.com/model-config',\n );\n searchAreaPrompt = undefined;\n }\n\n if (searchAreaPrompt && isAutoGLM(modelFamily)) {\n console.warn('The \"deepLocate\" feature is not supported with AutoGLM.');\n searchAreaPrompt = undefined;\n }\n\n const context = opt?.context || (await this.contextRetrieverFn());\n\n let searchArea: Rect | undefined = undefined;\n let searchAreaRawResponse: string | undefined = undefined;\n let searchAreaUsage: AIUsageInfo | undefined = undefined;\n let searchAreaResponse:\n | Awaited<ReturnType<typeof AiLocateSection>>\n | undefined = undefined;\n if (query.deepLocate && hasPlanLocatedElement) {\n const searchAreaConfig = await buildSearchAreaConfig({\n context,\n baseRect: opt.planLocatedElement!.rect,\n modelFamily,\n });\n searchArea = searchAreaConfig.rect;\n\n searchAreaRawResponse = JSON.stringify({\n source: 'plan-located-element',\n rect: opt.planLocatedElement!.rect,\n });\n searchAreaResponse = {\n rect: searchArea,\n imageBase64: searchAreaConfig.imageBase64,\n scale: searchAreaConfig.scale,\n rawResponse: searchAreaRawResponse,\n };\n } else if (searchAreaPrompt) {\n searchAreaResponse = await AiLocateSection({\n context,\n sectionDescription: searchAreaPrompt,\n modelConfig,\n abortSignal,\n });\n assert(\n searchAreaResponse.rect,\n `cannot find search area for \"${searchAreaPrompt}\"${\n searchAreaResponse.error ? `: ${searchAreaResponse.error}` : ''\n }`,\n );\n searchAreaRawResponse = searchAreaResponse.rawResponse;\n searchAreaUsage = searchAreaResponse.usage;\n searchArea = searchAreaResponse.rect;\n }\n\n const startTime = Date.now();\n const { parseResult, rect, rawResponse, usage, reasoning_content } =\n await AiLocateElement({\n context,\n targetElementDescription: queryPrompt,\n searchConfig: searchAreaResponse,\n modelConfig,\n abortSignal,\n });\n\n const timeCost = Date.now() - startTime;\n const taskInfo: ServiceTaskInfo = {\n ...(this.taskInfo ? this.taskInfo : {}),\n durationMs: timeCost,\n rawResponse: JSON.stringify(rawResponse),\n formatResponse: JSON.stringify(parseResult),\n usage,\n searchArea,\n searchAreaRawResponse,\n searchAreaUsage,\n reasoning_content,\n };\n\n let errorLog: string | undefined;\n if (parseResult.errors?.length) {\n errorLog = `failed to locate element: \\n${parseResult.errors.join('\\n')}`;\n }\n\n const dumpData: PartialServiceDumpFromSDK = {\n type: 'locate',\n userQuery: {\n element: queryPrompt,\n },\n matchedElement: [],\n matchedRect: rect,\n data: null,\n taskInfo,\n deepLocate: !!searchArea,\n error: errorLog,\n };\n\n const elements = parseResult.elements || [];\n\n const dump = createServiceDump({\n ...dumpData,\n matchedElement: elements,\n });\n\n if (errorLog) {\n throw new ServiceError(errorLog, dump);\n }\n\n if (elements.length > 1) {\n throw new ServiceError(\n `locate: multiple elements found, length = ${elements.length}`,\n dump,\n );\n }\n\n if (elements.length === 1) {\n return {\n element: {\n center: elements[0]!.center,\n rect: elements[0]!.rect,\n description: elements[0]!.description,\n },\n rect,\n dump,\n };\n }\n\n return {\n element: null,\n rect,\n dump,\n };\n }\n\n async extract<T>(\n dataDemand: ServiceExtractParam,\n modelConfig: IModelConfig,\n opt?: ServiceExtractOption,\n pageDescription?: string,\n multimodalPrompt?: TMultimodalPrompt,\n context?: UIContext,\n ): Promise<ServiceExtractResult<T>> {\n assert(context, 'context is required for extract');\n assert(\n typeof dataDemand === 'object' || typeof dataDemand === 'string',\n `dataDemand should be object or string, but get ${typeof dataDemand}`,\n );\n\n const startTime = Date.now();\n\n let parseResult: Awaited<\n ReturnType<typeof AiExtractElementInfo<T>>\n >['parseResult'];\n let rawResponse: string;\n let usage: Awaited<ReturnType<typeof AiExtractElementInfo<T>>>['usage'];\n let reasoning_content: string | undefined;\n\n try {\n const result = await AiExtractElementInfo<T>({\n context,\n dataQuery: dataDemand,\n multimodalPrompt,\n extractOption: opt,\n modelConfig,\n pageDescription,\n });\n parseResult = result.parseResult;\n rawResponse = result.rawResponse;\n usage = result.usage;\n reasoning_content = result.reasoning_content;\n } catch (error) {\n if (error instanceof AIResponseParseError) {\n // Create dump with usage and rawResponse from the error\n const timeCost = Date.now() - startTime;\n const taskInfo: ServiceTaskInfo = {\n ...(this.taskInfo ? this.taskInfo : {}),\n durationMs: timeCost,\n rawResponse: error.rawResponse,\n usage: error.usage,\n };\n const dump = createServiceDump({\n type: 'extract',\n userQuery: { dataDemand },\n matchedElement: [],\n data: null,\n taskInfo,\n error: error.message,\n });\n throw new ServiceError(error.message, dump);\n }\n throw error;\n }\n\n const timeCost = Date.now() - startTime;\n const taskInfo: ServiceTaskInfo = {\n ...(this.taskInfo ? this.taskInfo : {}),\n durationMs: timeCost,\n rawResponse,\n formatResponse: JSON.stringify(parseResult),\n usage,\n reasoning_content,\n };\n\n let errorLog: string | undefined;\n if (parseResult.errors?.length) {\n errorLog = `AI response error: \\n${parseResult.errors.join('\\n')}`;\n }\n\n const dumpData: PartialServiceDumpFromSDK = {\n type: 'extract',\n userQuery: {\n dataDemand,\n },\n matchedElement: [],\n data: null,\n taskInfo,\n error: errorLog,\n };\n\n const { data, thought } = parseResult || {};\n\n const dump = createServiceDump({\n ...dumpData,\n data,\n });\n\n if (errorLog && !data) {\n throw new ServiceError(errorLog, dump);\n }\n\n return {\n data,\n thought,\n usage,\n reasoning_content,\n dump,\n };\n }\n\n async describe(\n target: Rect | [number, number],\n modelConfig: IModelConfig,\n opt?: {\n deepLocate?: boolean;\n },\n ): Promise<Pick<AIDescribeElementResponse, 'description'>> {\n assert(target, 'target is required for service.describe');\n const context = await this.contextRetrieverFn();\n const { shotSize } = context;\n const screenshotBase64 = context.screenshot.base64;\n assert(screenshotBase64, 'screenshot is required for service.describe');\n // The result of the \"describe\" function will be used for positioning, so essentially it is a form of grounding.\n const { modelFamily } = modelConfig;\n const systemPrompt = elementDescriberInstruction();\n\n // Convert [x,y] center point to Rect if needed\n const defaultRectSize = 30;\n const targetRect: Rect = Array.isArray(target)\n ? {\n left: Math.floor(target[0] - defaultRectSize / 2),\n top: Math.floor(target[1] - defaultRectSize / 2),\n width: defaultRectSize,\n height: defaultRectSize,\n }\n : target;\n\n let imagePayload = await compositeElementInfoImg({\n inputImgBase64: screenshotBase64,\n size: shotSize,\n elementsPositionInfo: [\n {\n rect: targetRect,\n },\n ],\n borderThickness: 3,\n });\n\n if (opt?.deepLocate) {\n const searchArea = expandSearchArea(targetRect, shotSize);\n // Always crop in describe mode. Unlike locate's deepLocate (where\n // cropping too small loses context for finding elements), describe's\n // deepLocate intentionally zooms in so the model produces a more\n // precise description from a focused view. expandSearchArea already\n // guarantees a minimum 400x400 area with surrounding context.\n debug('describe: cropping to searchArea', searchArea);\n const croppedResult = await cropByRect(\n imagePayload,\n searchArea,\n modelFamily === 'qwen2.5-vl',\n );\n imagePayload = croppedResult.imageBase64;\n }\n\n const msgs: AIArgs = [\n { role: 'system', content: systemPrompt },\n {\n role: 'user',\n content: [\n {\n type: 'image_url',\n image_url: {\n url: imagePayload,\n detail: 'high',\n },\n },\n ],\n },\n ];\n\n const res = await callAIWithObjectResponse<AIDescribeElementResponse>(\n msgs,\n modelConfig,\n );\n\n const { content } = res;\n assert(!content.error, `describe failed: ${content.error}`);\n assert(content.description, 'failed to describe the element');\n return content;\n }\n}\n"],"names":["debug","getDebug","Service","query","opt","modelConfig","abortSignal","queryPrompt","assert","hasPlanLocatedElement","searchAreaPrompt","modelFamily","console","undefined","isAutoGLM","context","searchArea","searchAreaRawResponse","searchAreaUsage","searchAreaResponse","searchAreaConfig","buildSearchAreaConfig","JSON","AiLocateSection","startTime","Date","parseResult","rect","rawResponse","usage","reasoning_content","AiLocateElement","timeCost","taskInfo","errorLog","dumpData","elements","dump","createServiceDump","ServiceError","dataDemand","pageDescription","multimodalPrompt","result","AiExtractElementInfo","error","AIResponseParseError","data","thought","target","shotSize","screenshotBase64","systemPrompt","elementDescriberInstruction","defaultRectSize","targetRect","Array","Math","imagePayload","compositeElementInfoImg","expandSearchArea","croppedResult","cropByRect","msgs","res","callAIWithObjectResponse","content","Promise"],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { uuid } from "@midscene/shared/utils";
2
+ function createServiceDump(data) {
3
+ const baseData = {
4
+ logTime: Date.now()
5
+ };
6
+ const finalData = {
7
+ logId: uuid(),
8
+ ...baseData,
9
+ ...data
10
+ };
11
+ return finalData;
12
+ }
13
+ export { createServiceDump };
14
+
15
+ //# sourceMappingURL=utils.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service/utils.mjs","sources":["../../../src/service/utils.ts"],"sourcesContent":["import type { DumpMeta, PartialServiceDumpFromSDK, ServiceDump } from '@/types';\nimport { uuid } from '@midscene/shared/utils';\n\nexport function createServiceDump(\n data: PartialServiceDumpFromSDK,\n): ServiceDump {\n const baseData: DumpMeta = {\n logTime: Date.now(),\n };\n const finalData: ServiceDump = {\n logId: uuid(),\n ...baseData,\n ...data,\n };\n\n return finalData;\n}\n"],"names":["createServiceDump","data","baseData","Date","finalData","uuid"],"mappings":";AAGO,SAASA,kBACdC,IAA+B;IAE/B,MAAMC,WAAqB;QACzB,SAASC,KAAK,GAAG;IACnB;IACA,MAAMC,YAAyB;QAC7B,OAAOC;QACP,GAAGH,QAAQ;QACX,GAAGD,IAAI;IACT;IAEA,OAAOG;AACT"}