@xenon-device-management/xenon 1.1.0

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 (228) hide show
  1. package/README.md +446 -0
  2. package/lib/package.json +207 -0
  3. package/lib/public/assets/Layouts-7IT8aFLI.js +11 -0
  4. package/lib/public/assets/Layouts-DPMls9vh.css +1 -0
  5. package/lib/public/assets/ai-settings-BbnfgdEx.js +11 -0
  6. package/lib/public/assets/apps-CRMrI4_p.js +16 -0
  7. package/lib/public/assets/apps-CcM77dgg.css +1 -0
  8. package/lib/public/assets/badge-B1nKs8zj.css +1 -0
  9. package/lib/public/assets/badge-CSvl5xIU.js +11 -0
  10. package/lib/public/assets/button-CJlKn4PZ.css +1 -0
  11. package/lib/public/assets/button-CvLaGFYj.js +26 -0
  12. package/lib/public/assets/calendar-6w-D6Oaw.js +6 -0
  13. package/lib/public/assets/clock-DcdeWBPr.js +6 -0
  14. package/lib/public/assets/cpu-DiSoXT9n.js +6 -0
  15. package/lib/public/assets/device-explorer-CajM63OJ.js +193 -0
  16. package/lib/public/assets/device-explorer-CxdUAoTL.css +1 -0
  17. package/lib/public/assets/index-ByQwMN5T.js +174 -0
  18. package/lib/public/assets/index-C1DBaoSh.js +1 -0
  19. package/lib/public/assets/index-qzCez_kk.css +1 -0
  20. package/lib/public/assets/lock-B23ibZmo.js +6 -0
  21. package/lib/public/assets/maintenance-settings-CirzA6yG.js +6 -0
  22. package/lib/public/assets/mouse-pointer-2-Cz76SHFb.js +6 -0
  23. package/lib/public/assets/plus-BBwlIevt.js +6 -0
  24. package/lib/public/assets/session-dashboard-C2k7FFv_.css +1 -0
  25. package/lib/public/assets/session-dashboard-HPDtwPOZ.js +62 -0
  26. package/lib/public/assets/settings-DrZsZwdc.js +1 -0
  27. package/lib/public/assets/trash-2-DQpvzJec.js +6 -0
  28. package/lib/public/assets/useSocket-Dxsqae2a.js +16 -0
  29. package/lib/public/assets/webhook-settings-CDPgsgkb.css +1 -0
  30. package/lib/public/assets/webhook-settings-Cp-B4Nrw.js +1 -0
  31. package/lib/public/assets/zap-DovP6iow.js +6 -0
  32. package/lib/public/favicon.ico +0 -0
  33. package/lib/public/favicon.png +0 -0
  34. package/lib/public/favicon.svg +9 -0
  35. package/lib/public/index.html +46 -0
  36. package/lib/public/logo.svg +17 -0
  37. package/lib/public/logo192.png +0 -0
  38. package/lib/public/logo512.png +0 -0
  39. package/lib/public/manifest.json +25 -0
  40. package/lib/public/robots.txt +3 -0
  41. package/lib/schema.json +348 -0
  42. package/lib/src/InternalHttpClient.js +212 -0
  43. package/lib/src/PluginContext.js +29 -0
  44. package/lib/src/XenonCapabilityManager.js +199 -0
  45. package/lib/src/app/index.js +167 -0
  46. package/lib/src/app/routers/apps.js +79 -0
  47. package/lib/src/app/routers/config.js +131 -0
  48. package/lib/src/app/routers/control.js +835 -0
  49. package/lib/src/app/routers/dashboard.js +301 -0
  50. package/lib/src/app/routers/grid.js +352 -0
  51. package/lib/src/app/routers/reservation.js +190 -0
  52. package/lib/src/app/routers/webhook.js +83 -0
  53. package/lib/src/app/swagger-docs.js +203 -0
  54. package/lib/src/app/swagger.js +366 -0
  55. package/lib/src/chromeUtils.js +148 -0
  56. package/lib/src/commands/handle.js +19 -0
  57. package/lib/src/commands/index.js +8 -0
  58. package/lib/src/config.js +73 -0
  59. package/lib/src/dashboard/asset-manager.js +84 -0
  60. package/lib/src/dashboard/commands.js +284 -0
  61. package/lib/src/dashboard/event-manager.js +699 -0
  62. package/lib/src/dashboard/services/app-service.js +134 -0
  63. package/lib/src/dashboard/services/failure-analysis-service.js +173 -0
  64. package/lib/src/dashboard/services/session-service.js +113 -0
  65. package/lib/src/data-service/CircuitBreaker.js +83 -0
  66. package/lib/src/data-service/config-service.js +155 -0
  67. package/lib/src/data-service/db.js +122 -0
  68. package/lib/src/data-service/device-service.js +320 -0
  69. package/lib/src/data-service/device-store.interface.js +2 -0
  70. package/lib/src/data-service/device-store.js +345 -0
  71. package/lib/src/data-service/pending-sessions-service.js +25 -0
  72. package/lib/src/data-service/pluginArgs.js +25 -0
  73. package/lib/src/data-service/prisma-service.js +31 -0
  74. package/lib/src/data-service/prisma-store.js +385 -0
  75. package/lib/src/data-service/queue-service.js +150 -0
  76. package/lib/src/data-service/web-config-service.js +130 -0
  77. package/lib/src/device-managers/AndroidDeviceManager.js +1155 -0
  78. package/lib/src/device-managers/ChromeDriverManager.js +68 -0
  79. package/lib/src/device-managers/HealthMonitorService.js +325 -0
  80. package/lib/src/device-managers/IOSDeviceManager.js +351 -0
  81. package/lib/src/device-managers/NodeDevices.js +82 -0
  82. package/lib/src/device-managers/android/AndroidStreamService.js +370 -0
  83. package/lib/src/device-managers/android/DeviceLockManager.js +45 -0
  84. package/lib/src/device-managers/cloud/CapabilityManager.js +26 -0
  85. package/lib/src/device-managers/cloud/Devices.js +86 -0
  86. package/lib/src/device-managers/iOSTracker.js +44 -0
  87. package/lib/src/device-managers/index.js +89 -0
  88. package/lib/src/device-managers/ios/IOSDiscoveryService.js +268 -0
  89. package/lib/src/device-managers/ios/IOSStreamService.js +893 -0
  90. package/lib/src/device-managers/ios/WDAClient.js +866 -0
  91. package/lib/src/device-utils.js +663 -0
  92. package/lib/src/enums/Capabilities.js +8 -0
  93. package/lib/src/enums/Cloud.js +11 -0
  94. package/lib/src/enums/Platform.js +9 -0
  95. package/lib/src/enums/SessionType.js +9 -0
  96. package/lib/src/enums/SocketEvents.js +15 -0
  97. package/lib/src/helpers/UniversalMjpegProxy.js +273 -0
  98. package/lib/src/helpers/index.js +229 -0
  99. package/lib/src/index.js +95 -0
  100. package/lib/src/interceptors/CommandInterceptor.js +524 -0
  101. package/lib/src/interfaces/ICloudManager.js +2 -0
  102. package/lib/src/interfaces/IDevice.js +2 -0
  103. package/lib/src/interfaces/IDeviceFilterOptions.js +2 -0
  104. package/lib/src/interfaces/IDeviceManager.js +2 -0
  105. package/lib/src/interfaces/IOptions.js +2 -0
  106. package/lib/src/interfaces/IPluginArgs.js +55 -0
  107. package/lib/src/interfaces/ISessionCapability.js +2 -0
  108. package/lib/src/logger.js +225 -0
  109. package/lib/src/plugin.js +244 -0
  110. package/lib/src/prisma.js +12 -0
  111. package/lib/src/profiling/AndroidAppProfiler.js +213 -0
  112. package/lib/src/proxy/wd-command-proxy.js +221 -0
  113. package/lib/src/scripts/generate-database-migration.js +59 -0
  114. package/lib/src/scripts/initialize-database.js +55 -0
  115. package/lib/src/scripts/install-go-ios.js +66 -0
  116. package/lib/src/scripts/prepare-prisma.js +89 -0
  117. package/lib/src/services/AICommandService.js +143 -0
  118. package/lib/src/services/AIService.js +466 -0
  119. package/lib/src/services/CleanupService.js +141 -0
  120. package/lib/src/services/EventBus.js +74 -0
  121. package/lib/src/services/InspectorService.js +395 -0
  122. package/lib/src/services/MetricsService.js +134 -0
  123. package/lib/src/services/NetworkConditioningService.js +173 -0
  124. package/lib/src/services/NotificationService.js +163 -0
  125. package/lib/src/services/RequestLogService.js +252 -0
  126. package/lib/src/services/ResourceIsolationService.js +122 -0
  127. package/lib/src/services/SecurityService.js +120 -0
  128. package/lib/src/services/ServerManager.js +284 -0
  129. package/lib/src/services/SessionHeartbeatService.js +158 -0
  130. package/lib/src/services/SessionLifecycleService.js +572 -0
  131. package/lib/src/services/SocketClient.js +71 -0
  132. package/lib/src/services/SocketServer.js +87 -0
  133. package/lib/src/services/TracingService.js +132 -0
  134. package/lib/src/services/VideoPipelineService.js +220 -0
  135. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +333 -0
  136. package/lib/src/services/healing/HealEtalonService.js +98 -0
  137. package/lib/src/services/healing/HealedLocatorGenerator.js +132 -0
  138. package/lib/src/services/healing/HealingOrchestrator.js +165 -0
  139. package/lib/src/services/healing/LlmHealingProvider.js +77 -0
  140. package/lib/src/services/healing/OcrHealingProvider.js +119 -0
  141. package/lib/src/services/healing/ResilioTreeHealingProvider.js +100 -0
  142. package/lib/src/services/healing/VisualAiHealingProvider.js +90 -0
  143. package/lib/src/services/healing/types.js +12 -0
  144. package/lib/src/services/omni-vision/OmniVisionService.js +718 -0
  145. package/lib/src/services/omni-vision/VisionAssertionService.js +68 -0
  146. package/lib/src/sessions/CloudSession.js +42 -0
  147. package/lib/src/sessions/LocalSession.js +313 -0
  148. package/lib/src/sessions/RemoteSession.js +287 -0
  149. package/lib/src/sessions/SessionManager.js +238 -0
  150. package/lib/src/sessions/XenonSession.js +44 -0
  151. package/lib/src/types/CLIArgs.js +2 -0
  152. package/lib/src/types/CloudArgs.js +2 -0
  153. package/lib/src/types/CloudSchema.js +131 -0
  154. package/lib/src/types/DeviceType.js +2 -0
  155. package/lib/src/types/DeviceUpdate.js +2 -0
  156. package/lib/src/types/IOSDevice.js +2 -0
  157. package/lib/src/types/Platform.js +2 -0
  158. package/lib/src/types/SessionStatus.js +11 -0
  159. package/lib/src/validators/CapabilityValidator.js +93 -0
  160. package/lib/test/e2e/android/conf.spec.js +43 -0
  161. package/lib/test/e2e/android/conf2.spec.js +44 -0
  162. package/lib/test/e2e/android/conf3.spec.js +44 -0
  163. package/lib/test/e2e/e2ehelper.js +113 -0
  164. package/lib/test/e2e/hubnode/forward-request.spec.js +224 -0
  165. package/lib/test/e2e/hubnode/hubnode.spec.js +214 -0
  166. package/lib/test/e2e/ios/conf1.spec.js +39 -0
  167. package/lib/test/e2e/ios/conf2.spec.js +39 -0
  168. package/lib/test/e2e/plugin-harness.js +236 -0
  169. package/lib/test/e2e/plugin.spec.js +97 -0
  170. package/lib/test/e2e/telemetry_verification.spec.js +83 -0
  171. package/lib/test/e2e/video-recording-test.spec.js +63 -0
  172. package/lib/test/helpers/test-container.js +112 -0
  173. package/lib/test/integration/androidDevices.spec.js +137 -0
  174. package/lib/test/integration/cliArgs.js +73 -0
  175. package/lib/test/integration/ios/01iOSSimulator.spec.js +291 -0
  176. package/lib/test/integration/ios/02iOSDevices.spec.js +75 -0
  177. package/lib/test/integration/testHelpers.js +74 -0
  178. package/lib/test/unit/AndroidDeviceManager.spec.js +178 -0
  179. package/lib/test/unit/ChromeDriverManager.spec.js +26 -0
  180. package/lib/test/unit/CleanupService.spec.js +21 -0
  181. package/lib/test/unit/DeviceModel.spec.js +157 -0
  182. package/lib/test/unit/FuzzyXmlHealingProvider.test.js +294 -0
  183. package/lib/test/unit/GetAdbOriginal.js +42 -0
  184. package/lib/test/unit/HealingCascade.test.js +128 -0
  185. package/lib/test/unit/IOSDeviceManager.spec.js +261 -0
  186. package/lib/test/unit/RemoteIOs.spec.js +78 -0
  187. package/lib/test/unit/ResilioTreeHealingProvider.test.js +96 -0
  188. package/lib/test/unit/commands.spec.js +27 -0
  189. package/lib/test/unit/config.spec.js +27 -0
  190. package/lib/test/unit/device-service.spec.js +307 -0
  191. package/lib/test/unit/device-utils.spec.js +313 -0
  192. package/lib/test/unit/fixtures/device.config.js +4 -0
  193. package/lib/test/unit/fixtures/devices.js +89 -0
  194. package/lib/test/unit/helpers.spec.js +62 -0
  195. package/lib/test/unit/omni-vision.spec.js +100 -0
  196. package/lib/test/unit/plugin.spec.js +133 -0
  197. package/lib/tsconfig.tsbuildinfo +1 -0
  198. package/package.json +207 -0
  199. package/prisma/data.db +0 -0
  200. package/prisma/dev.db +0 -0
  201. package/prisma/dev.db-journal +0 -0
  202. package/prisma/migrations/20231011074725_initial_tables/migration.sql +47 -0
  203. package/prisma/migrations/20231226115334_update_session_log/migration.sql +2 -0
  204. package/prisma/migrations/20251204113710_add_video_recording_enabled/migration.sql +29 -0
  205. package/prisma/migrations/20251204132449_add_log_table/migration.sql +11 -0
  206. package/prisma/migrations/20251205050111_add_profiling_support/migration.sql +47 -0
  207. package/prisma/migrations/20251205050947_add_is_error_field/migration.sql +24 -0
  208. package/prisma/migrations/20260126201337_add_app_model/migration.sql +18 -0
  209. package/prisma/migrations/20260130115722_add_performance_trace_and_xenon_sync/migration.sql +2 -0
  210. package/prisma/migrations/20260130135114_add_device_models/migration.sql +57 -0
  211. package/prisma/migrations/20260130140655_make_systemport_optional/migration.sql +45 -0
  212. package/prisma/migrations/20260130140932_make_device_fields_optional/migration.sql +45 -0
  213. package/prisma/migrations/20260130141040_final_schema_fix/migration.sql +45 -0
  214. package/prisma/migrations/20260130143234_add_device_health_fields/migration.sql +4 -0
  215. package/prisma/migrations/20260130144921_add_failure_category/migration.sql +2 -0
  216. package/prisma/migrations/20260131151456_add_webhook_config/migration.sql +10 -0
  217. package/prisma/migrations/20260201094507_add_device_tags/migration.sql +11 -0
  218. package/prisma/migrations/20260201103410_add_managed_process/migration.sql +15 -0
  219. package/prisma/migrations/20260201140637_add_web_config/migration.sql +22 -0
  220. package/prisma/migrations/20260201162232_add_session_progress/migration.sql +2 -0
  221. package/prisma/migrations/20260201174231_add_total_healed_count/migration.sql +2 -0
  222. package/prisma/migrations/migration_lock.toml +3 -0
  223. package/prisma/schema.prisma +210 -0
  224. package/schema.json +348 -0
  225. package/scripts/build-xenon.sh +32 -0
  226. package/scripts/dev/debug-gemini.ts +44 -0
  227. package/scripts/generate-types-from-schema.js +86 -0
  228. package/scripts/install-compatible-driver.js +39 -0
@@ -0,0 +1,718 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
22
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
23
+ };
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
42
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
43
+ return new (P || (P = Promise))(function (resolve, reject) {
44
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
45
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
46
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
47
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
48
+ });
49
+ };
50
+ var __importDefault = (this && this.__importDefault) || function (mod) {
51
+ return (mod && mod.__esModule) ? mod : { "default": mod };
52
+ };
53
+ var OmniVisionService_1;
54
+ Object.defineProperty(exports, "__esModule", { value: true });
55
+ exports.OmniVisionService = void 0;
56
+ const typedi_1 = require("typedi");
57
+ const tesseract_js_1 = __importStar(require("tesseract.js"));
58
+ const sharp_1 = __importDefault(require("sharp"));
59
+ const AIService_1 = require("../AIService");
60
+ const logger_1 = __importDefault(require("../../logger"));
61
+ const crypto_1 = __importDefault(require("crypto"));
62
+ // Mobile UIs have sparse text on complex backgrounds - reduce contrast to preserve readability
63
+ const DEFAULT_CONTRAST = 1.2; // Slightly above 1.0 for mild enhancement (was 0.8 which was too aggressive)
64
+ const UPSCALE_THRESHOLD = 800; // Only upscale very small images (was 1500)
65
+ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
66
+ constructor() {
67
+ this.logger = logger_1.default.scope('OmniVision');
68
+ this.virtualElementStore = new Map();
69
+ this.sharedWorker = null;
70
+ this.workerBusy = false;
71
+ this.uiLensCache = new Map();
72
+ }
73
+ getWorker() {
74
+ return __awaiter(this, void 0, void 0, function* () {
75
+ if (this.sharedWorker)
76
+ return this.sharedWorker;
77
+ this.logger.info('Initializing persistent OCR worker with explicit configuration...');
78
+ const worker = yield tesseract_js_1.default.createWorker('eng');
79
+ // PSM.SPARSE_TEXT is optimized for irregular text layouts like mobile UIs
80
+ // AUTO mode assumes document structure which fails on sparse mobile screens
81
+ yield worker.setParameters({
82
+ tessedit_pageseg_mode: tesseract_js_1.PSM.SPARSE_TEXT,
83
+ });
84
+ this.sharedWorker = worker;
85
+ this.logger.info('OCR worker initialized successfully with PSM.SPARSE_TEXT (optimized for mobile UI).');
86
+ return this.sharedWorker;
87
+ });
88
+ }
89
+ parseHocr(hocr) {
90
+ if (!hocr)
91
+ return [];
92
+ const words = [];
93
+ // Match word spans: support both ocr_word and ocrx_word
94
+ const wordRegex = /<span[^>]*class=['"]ocrx?_word['"][^>]*title=['"]bbox (\d+) (\d+) (\d+) (\d+); x_wconf (\d+)['"]>([^<]+)<\/span>/g;
95
+ let match;
96
+ while ((match = wordRegex.exec(hocr)) !== null) {
97
+ words.push({
98
+ text: match[6].trim(),
99
+ confidence: parseInt(match[5]),
100
+ bbox: {
101
+ x0: parseInt(match[1]),
102
+ y0: parseInt(match[2]),
103
+ x1: parseInt(match[3]),
104
+ y1: parseInt(match[4]),
105
+ },
106
+ });
107
+ }
108
+ return words;
109
+ }
110
+ parseTsv(tsv) {
111
+ var _a;
112
+ if (!tsv)
113
+ return [];
114
+ this.logger.debug(`Raw TSV snippet: ${tsv.substring(0, 200).replace(/\n/g, '\\n')}`);
115
+ const lines = tsv.split(/\r?\n/);
116
+ const words = [];
117
+ // Header: level page_num block_num par_num line_num word_num left top width height conf text
118
+ for (let i = 1; i < lines.length; i++) {
119
+ const cols = lines[i].split('\t');
120
+ if (cols.length < 12)
121
+ continue;
122
+ const level = parseInt(cols[0]);
123
+ const conf = parseFloat(cols[10]);
124
+ const text = (_a = cols[11]) === null || _a === void 0 ? void 0 : _a.trim();
125
+ // Level 5 is "word" in Tesseract TSV
126
+ if (level === 5 && text && conf > 0) {
127
+ words.push({
128
+ text: text,
129
+ confidence: conf,
130
+ bbox: {
131
+ x0: parseInt(cols[6]),
132
+ y0: parseInt(cols[7]),
133
+ x1: parseInt(cols[6]) + parseInt(cols[8]),
134
+ y1: parseInt(cols[7]) + parseInt(cols[9]),
135
+ },
136
+ });
137
+ }
138
+ }
139
+ return words;
140
+ }
141
+ validateBuffer(buffer, context) {
142
+ if (!buffer || buffer.length === 0) {
143
+ throw new Error(`[OmniVision] ${context}: Input Buffer is empty.`);
144
+ }
145
+ if (buffer.length < 500) {
146
+ // A valid screenshot is usually > 50KB. 500 bytes is a safe physiological minimum for a tiny image.
147
+ this.logger.warn(`[OmniVision] ${context}: Buffer size exceptionally small (${buffer.length} bytes). Possible truncation.`);
148
+ }
149
+ }
150
+ normalizeWordBBox(word) {
151
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
152
+ const text = ((word === null || word === void 0 ? void 0 : word.text) || '').trim();
153
+ if (!text)
154
+ return null;
155
+ const confRaw = word === null || word === void 0 ? void 0 : word.confidence;
156
+ const confidence = typeof confRaw === 'number' ? confRaw : typeof confRaw === 'string' ? parseFloat(confRaw) : 0;
157
+ const bbox = (word === null || word === void 0 ? void 0 : word.bbox) || (word === null || word === void 0 ? void 0 : word.boundingBox) || (word === null || word === void 0 ? void 0 : word.box);
158
+ if (bbox && typeof bbox === 'object') {
159
+ const x0 = (_c = (_b = (_a = bbox.x0) !== null && _a !== void 0 ? _a : bbox.left) !== null && _b !== void 0 ? _b : bbox.x) !== null && _c !== void 0 ? _c : bbox[0];
160
+ const y0 = (_f = (_e = (_d = bbox.y0) !== null && _d !== void 0 ? _d : bbox.top) !== null && _e !== void 0 ? _e : bbox.y) !== null && _f !== void 0 ? _f : bbox[1];
161
+ const x1 = (_h = (_g = bbox.x1) !== null && _g !== void 0 ? _g : bbox.right) !== null && _h !== void 0 ? _h : (bbox.x0 !== undefined && bbox.width !== undefined ? bbox.x0 + bbox.width : bbox[2]);
162
+ const y1 = (_k = (_j = bbox.y1) !== null && _j !== void 0 ? _j : bbox.bottom) !== null && _k !== void 0 ? _k : (bbox.y0 !== undefined && bbox.height !== undefined ? bbox.y0 + bbox.height : bbox[3]);
163
+ if ([x0, y0, x1, y1].every((v) => typeof v === 'number' && isFinite(v))) {
164
+ return { x0, y0, x1, y1, text, confidence };
165
+ }
166
+ }
167
+ return null;
168
+ }
169
+ rgbToBasicColorName(r, g, b) {
170
+ // Very lightweight naming for enterprise stability (no ML dependency)
171
+ // Convert to HSV-ish buckets
172
+ const max = Math.max(r, g, b);
173
+ const min = Math.min(r, g, b);
174
+ const delta = max - min;
175
+ const v = max / 255;
176
+ const s = max === 0 ? 0 : delta / max;
177
+ // Grayscale
178
+ if (s < 0.15) {
179
+ if (v > 0.85)
180
+ return 'white';
181
+ if (v < 0.2)
182
+ return 'black';
183
+ return 'gray';
184
+ }
185
+ let h = 0;
186
+ if (delta === 0)
187
+ h = 0;
188
+ else if (max === r)
189
+ h = ((g - b) / delta) % 6;
190
+ else if (max === g)
191
+ h = (b - r) / delta + 2;
192
+ else
193
+ h = (r - g) / delta + 4;
194
+ h = Math.round(h * 60);
195
+ if (h < 0)
196
+ h += 360;
197
+ if (h < 15 || h >= 345)
198
+ return 'red';
199
+ if (h < 45)
200
+ return 'orange';
201
+ if (h < 70)
202
+ return 'yellow';
203
+ if (h < 170)
204
+ return 'green';
205
+ if (h < 200)
206
+ return 'cyan';
207
+ if (h < 255)
208
+ return 'blue';
209
+ if (h < 290)
210
+ return 'purple';
211
+ if (h < 345)
212
+ return 'pink';
213
+ return 'unknown';
214
+ }
215
+ sampleAverageColorName(image, bbox, imageW, imageH) {
216
+ return __awaiter(this, void 0, void 0, function* () {
217
+ const left = Math.max(0, Math.min(imageW - 1, Math.floor(bbox.x0)));
218
+ const top = Math.max(0, Math.min(imageH - 1, Math.floor(bbox.y0)));
219
+ const right = Math.max(0, Math.min(imageW, Math.ceil(bbox.x1)));
220
+ const bottom = Math.max(0, Math.min(imageH, Math.ceil(bbox.y1)));
221
+ const width = Math.max(1, right - left);
222
+ const height = Math.max(1, bottom - top);
223
+ // Sample a small, stable grid to reduce cost
224
+ const targetW = 8;
225
+ const targetH = 8;
226
+ try {
227
+ const raw = yield image
228
+ .clone()
229
+ .extract({ left, top, width, height })
230
+ .resize(targetW, targetH, { fit: 'fill' })
231
+ .raw()
232
+ .toBuffer();
233
+ // raw is RGB (or RGBA) depending on input; ensure 3 channels
234
+ const channels = raw.length === targetW * targetH * 4 ? 4 : 3;
235
+ let r = 0, g = 0, b = 0;
236
+ for (let i = 0; i < raw.length; i += channels) {
237
+ r += raw[i];
238
+ g += raw[i + 1];
239
+ b += raw[i + 2];
240
+ }
241
+ const n = raw.length / channels;
242
+ return this.rgbToBasicColorName(r / n, g / n, b / n);
243
+ }
244
+ catch (e) {
245
+ return null;
246
+ }
247
+ });
248
+ }
249
+ preprocessImage(buffer) {
250
+ return __awaiter(this, void 0, void 0, function* () {
251
+ try {
252
+ this.validateBuffer(buffer, 'Pre-processing');
253
+ let sharpImage = (0, sharp_1.default)(buffer);
254
+ // Convert to grayscale and apply mild contrast to enhance text visibility
255
+ // Using DEFAULT_CONTRAST of 1.2 for mild enhancement without washing out text
256
+ sharpImage = sharpImage.greyscale().linear(DEFAULT_CONTRAST, -(128 * DEFAULT_CONTRAST) + 128);
257
+ // Sharpen edges to define text against complex backgrounds
258
+ sharpImage = sharpImage.sharpen();
259
+ // NOTE: Upscaling removed to ensure bounding box coordinates match original image
260
+ // The PSM.SPARSE_TEXT mode is optimized for mobile UIs and doesn't require upscaling
261
+ const processedBuffer = yield sharpImage.toBuffer();
262
+ this.logger.debug('Image pre-processed: grayscale + contrast + sharpen applied (no upscaling).');
263
+ return processedBuffer;
264
+ }
265
+ catch (e) {
266
+ this.logger.warn(`Image pre-processing skipped: ${e.message}. Using original buffer.`);
267
+ return buffer;
268
+ }
269
+ });
270
+ }
271
+ performOcr(buffer) {
272
+ return __awaiter(this, void 0, void 0, function* () {
273
+ this.validateBuffer(buffer, 'OCR Engine');
274
+ // Simple lock to avoid concurrent worker use in this context
275
+ while (this.workerBusy)
276
+ yield new Promise((r) => setTimeout(r, 100));
277
+ this.workerBusy = true;
278
+ try {
279
+ // Pre-process image for better OCR accuracy
280
+ const processedBuffer = yield this.preprocessImage(buffer);
281
+ this.validateBuffer(processedBuffer, 'OCR Post-processing');
282
+ const worker = yield this.getWorker();
283
+ // CRITICAL: Tesseract.js v6+ disables all outputs except 'text' by default
284
+ // We must explicitly enable hocr, tsv, and blocks for spatial data
285
+ const result = yield worker.recognize(processedBuffer, {}, {
286
+ hocr: true,
287
+ tsv: true,
288
+ blocks: true,
289
+ text: true,
290
+ });
291
+ const data = result.data;
292
+ // Debug: Log what Tesseract v7 actually returns
293
+ this.logger.debug(`Tesseract result keys: ${Object.keys(result).join(', ')}`);
294
+ this.logger.debug(`Tesseract result.data keys: ${Object.keys(result.data).join(', ')}`);
295
+ let words = data.words || [];
296
+ if (words.length === 0) {
297
+ const blocks = data.blocks || data.layoutBlocks || [];
298
+ if (blocks.length > 0) {
299
+ this.logger.debug(`Extracting words from ${blocks.length} blocks...`);
300
+ // DIAGNOSTIC: Log actual block structure
301
+ blocks.forEach((block, i) => {
302
+ this.logger.debug(`Block[${i}] keys: ${Object.keys(block).join(', ')}`);
303
+ const paras = block.paragraphs || [];
304
+ this.logger.debug(`Block[${i}] has ${paras.length} paragraphs`);
305
+ if (paras.length > 0) {
306
+ this.logger.debug(`Block[${i}].paragraphs[0] keys: ${Object.keys(paras[0]).join(', ')}`);
307
+ }
308
+ paras.forEach((para) => {
309
+ (para.lines || []).forEach((line) => {
310
+ (line.words || []).forEach((word) => words.push(word));
311
+ });
312
+ });
313
+ });
314
+ // If block extraction still yielded nothing, try direct 'words' property on block
315
+ if (words.length === 0) {
316
+ blocks.forEach((block) => {
317
+ if (block.words && Array.isArray(block.words)) {
318
+ words.push(...block.words);
319
+ }
320
+ });
321
+ }
322
+ }
323
+ }
324
+ // ALWAYS attempt HOCR parsing - it's more reliable than block.paragraphs
325
+ if (data.hocr) {
326
+ this.logger.info(`HOCR parsing. Block extraction yielded ${words.length} words. HOCR length: ${data.hocr.length} chars`);
327
+ const hocrWords = this.parseHocr(data.hocr);
328
+ if (hocrWords.length > words.length) {
329
+ this.logger.info(`✓ HOCR yielded ${hocrWords.length} words (better than block extraction's ${words.length}). Using HOCR.`);
330
+ words = hocrWords;
331
+ }
332
+ else if (hocrWords.length > 0) {
333
+ this.logger.debug(`HOCR yielded ${hocrWords.length} words. Block extraction: ${words.length}. Keeping blocks.`);
334
+ }
335
+ else {
336
+ this.logger.warn(`HOCR parsing returned 0 words despite ${data.hocr.length} chars of data.`);
337
+ // Log a snippet of HOCR for debugging
338
+ this.logger.debug(`Raw HOCR snippet: ${data.hocr.substring(0, 500).replace(/\n/g, '\\n')}`);
339
+ }
340
+ }
341
+ else {
342
+ this.logger.warn('HOCR data not available from Tesseract.');
343
+ }
344
+ // Fallback Level 3: TSV Parsing
345
+ if (words.length === 0 && data.tsv) {
346
+ this.logger.info(`TSV fallback triggered. TSV length: ${data.tsv.length} chars`);
347
+ const tsvWords = this.parseTsv(data.tsv);
348
+ if (tsvWords.length > 0) {
349
+ this.logger.info(`✓ Recovered ${tsvWords.length} words from TSV metadata.`);
350
+ words = tsvWords;
351
+ }
352
+ else {
353
+ this.logger.warn(`TSV parsing returned 0 words despite ${data.tsv.length} chars of data.`);
354
+ }
355
+ }
356
+ else if (words.length === 0) {
357
+ this.logger.warn(`TSV fallback skipped. data.tsv exists: ${!!data.tsv}`);
358
+ }
359
+ this.logger.debug(`Final word count: ${words.length}`);
360
+ if (words.length === 0 && data.text) {
361
+ this.logger.warn(`Spatial Blindness: Found text but 0 bounding boxes. Text snippet: "${data.text.substring(0, 50)}..."`);
362
+ }
363
+ return {
364
+ text: data.text || '',
365
+ words: words,
366
+ };
367
+ }
368
+ catch (e) {
369
+ this.logger.error(`OCR Operation failed: ${e.message}`);
370
+ // Reset worker on critical failure
371
+ if (this.sharedWorker) {
372
+ yield this.sharedWorker.terminate();
373
+ this.sharedWorker = null;
374
+ }
375
+ throw e;
376
+ }
377
+ finally {
378
+ this.workerBusy = false;
379
+ }
380
+ });
381
+ }
382
+ /**
383
+ * Proactive OCR Search: Finds elements matching text even if not in XML
384
+ */
385
+ findByText(driver, text) {
386
+ return __awaiter(this, void 0, void 0, function* () {
387
+ this.logger.info(`Searching for text: "${text}" via proactive OCR...`);
388
+ try {
389
+ const screenshot = yield driver.getScreenshot();
390
+ const buffer = Buffer.from(screenshot, 'base64');
391
+ const { text: ocrText, words } = yield this.performOcr(buffer);
392
+ if (words.length === 0) {
393
+ this.logger.warn(`OCR proactive search yielded no words. Text content: "${ocrText.trim().substring(0, 100)}..."`);
394
+ return [];
395
+ }
396
+ const matches = words.filter((w) => w.text && w.text.toLowerCase().includes(text.toLowerCase()) && w.confidence > 60);
397
+ return matches.map((m) => {
398
+ const el = {
399
+ id: `omni_ocr_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
400
+ text: m.text,
401
+ rect: {
402
+ x: m.bbox.x0,
403
+ y: m.bbox.y0,
404
+ width: m.bbox.x1 - m.bbox.x0,
405
+ height: m.bbox.y1 - m.bbox.y0,
406
+ },
407
+ confidence: m.confidence / 100,
408
+ };
409
+ this.virtualElementStore.set(el.id, el);
410
+ return el;
411
+ });
412
+ }
413
+ catch (err) {
414
+ this.logger.error(`OCR proactive search failed: ${err.message}`);
415
+ return [];
416
+ }
417
+ });
418
+ }
419
+ /**
420
+ * AI-based visual find for icons or specific descriptions
421
+ */
422
+ findByIcon(driver, iconDescription) {
423
+ return __awaiter(this, void 0, void 0, function* () {
424
+ this.logger.info(`Searching for: "${iconDescription}" via AI Vision...`);
425
+ try {
426
+ const screenshot = yield driver.getScreenshot();
427
+ const coordinates = yield AIService_1.AI_SERVICE.visualFind(screenshot, iconDescription);
428
+ if (coordinates) {
429
+ const el = {
430
+ id: `omni_ai_${Date.now()}`,
431
+ rect: {
432
+ x: coordinates.x - 20,
433
+ y: coordinates.y - 20,
434
+ width: 40,
435
+ height: 40,
436
+ },
437
+ confidence: 0.85,
438
+ };
439
+ this.virtualElementStore.set(el.id, el);
440
+ return el;
441
+ }
442
+ }
443
+ catch (err) {
444
+ this.logger.error(`AI proactive find failed: ${err.message}`);
445
+ }
446
+ return null;
447
+ });
448
+ }
449
+ /**
450
+ * Extract comprehensive screen metadata
451
+ */
452
+ analyzeScreen(driver) {
453
+ return __awaiter(this, void 0, void 0, function* () {
454
+ this.logger.info('Performing full screen analysis...');
455
+ try {
456
+ const screenshot = yield driver.getScreenshot();
457
+ if (!screenshot || screenshot.trim() === '') {
458
+ throw new Error('Screenshot capture returned empty data.');
459
+ }
460
+ const buffer = Buffer.from(screenshot, 'base64');
461
+ this.validateBuffer(buffer, 'Screen Analysis');
462
+ // 1. Get all text via OCR using the robust worker flow
463
+ const { text: ocrText, words } = yield this.performOcr(buffer);
464
+ // 2. Ask AI for qualitative analysis (Non-blocking)
465
+ let aiAnalysis = null;
466
+ try {
467
+ aiAnalysis = yield AIService_1.AI_SERVICE.analyzeFailure({
468
+ sessionId: driver.sessionId,
469
+ failureReason: 'Screen Analysis Request',
470
+ commandLogs: [],
471
+ deviceLogs: [],
472
+ screenshotPath: screenshot, // In a real scenario, we'd save this to a file first or pass base64
473
+ });
474
+ }
475
+ catch (aiErr) {
476
+ this.logger.warn(`AI insights skipped: ${aiErr.message}`);
477
+ }
478
+ return {
479
+ timestamp: new Date().toISOString(),
480
+ ocr: {
481
+ text: ocrText,
482
+ words: words.map((w) => ({
483
+ text: w.text || '',
484
+ confidence: w.confidence || 0,
485
+ bbox: w.bbox || { x0: 0, y0: 0, x1: 0, y1: 0 },
486
+ })),
487
+ },
488
+ ai_insights: aiAnalysis,
489
+ };
490
+ }
491
+ catch (err) {
492
+ this.logger.error(`Screen analysis failed: ${err.message}`);
493
+ return { status: 'error', message: err.message };
494
+ }
495
+ });
496
+ }
497
+ addVirtualElement(element) {
498
+ this.virtualElementStore.set(element.id, element);
499
+ }
500
+ getVirtualElement(id) {
501
+ return this.virtualElementStore.get(id);
502
+ }
503
+ /**
504
+ * Omni-Click: Find a target by OCR text and click its center using W3C actions.
505
+ */
506
+ omniClickByText(driver, req) {
507
+ return __awaiter(this, void 0, void 0, function* () {
508
+ const text = (req.text || '').trim();
509
+ const index = typeof req.index === 'number' ? req.index : 1;
510
+ const takeANewScreenShot = req.takeANewScreenShot !== false;
511
+ if (!text)
512
+ return { clicked: false, message: 'Text is empty' };
513
+ if (!Number.isFinite(index) || index < 1) {
514
+ return { clicked: false, message: 'index must be >= 1' };
515
+ }
516
+ if (typeof (driver === null || driver === void 0 ? void 0 : driver.performActions) !== 'function') {
517
+ return {
518
+ clicked: false,
519
+ message: 'Driver does not support performActions (W3C actions required for omniClick)',
520
+ };
521
+ }
522
+ this.logger.info(`Omni-Click: searching for text "${text}" (index=${index})...`);
523
+ // OCR scan
524
+ const screenshot = yield driver.getScreenshot();
525
+ const buffer = Buffer.from(screenshot, 'base64');
526
+ const { words } = yield this.performOcr(buffer);
527
+ const normalized = words.map((w) => this.normalizeWordBBox(w)).filter(Boolean);
528
+ const matches = normalized
529
+ .filter((w) => w.text.toLowerCase().includes(text.toLowerCase()))
530
+ .sort((a, b) => (b.confidence || 0) - (a.confidence || 0) || a.y0 - b.y0 || a.x0 - b.x0);
531
+ if (matches.length === 0) {
532
+ return { clicked: false, message: `No OCR match found for text "${text}"` };
533
+ }
534
+ const pick = matches[Math.min(index - 1, matches.length - 1)];
535
+ const rect = {
536
+ x: Math.max(0, Math.round(pick.x0)),
537
+ y: Math.max(0, Math.round(pick.y0)),
538
+ width: Math.max(1, Math.round(pick.x1 - pick.x0)),
539
+ height: Math.max(1, Math.round(pick.y1 - pick.y0)),
540
+ };
541
+ const cx = Math.round(rect.x + rect.width / 2);
542
+ const cy = Math.round(rect.y + rect.height / 2);
543
+ // Click via touch actions
544
+ yield driver.performActions([
545
+ {
546
+ type: 'pointer',
547
+ id: 'finger1',
548
+ parameters: { pointerType: 'touch' },
549
+ actions: [
550
+ { type: 'pointerMove', duration: 0, x: cx, y: cy },
551
+ { type: 'pointerDown', button: 0 },
552
+ { type: 'pause', duration: 120 },
553
+ { type: 'pointerUp', button: 0 },
554
+ ],
555
+ },
556
+ ]);
557
+ if (typeof driver.releaseActions === 'function') {
558
+ // best-effort
559
+ yield driver.releaseActions().catch(() => { });
560
+ }
561
+ return {
562
+ clicked: true,
563
+ message: `Clicked OCR match for "${text}"`,
564
+ target: {
565
+ text: pick.text,
566
+ index,
567
+ x: cx,
568
+ y: cy,
569
+ rect,
570
+ confidence: Math.max(0, Math.min(1, (pick.confidence || 0) / 100)),
571
+ },
572
+ };
573
+ });
574
+ }
575
+ /**
576
+ * Omni-Click by Icon: Find a target by visual description and click its center.
577
+ */
578
+ omniClickByIcon(driver, req) {
579
+ return __awaiter(this, void 0, void 0, function* () {
580
+ const icon = (req.icon || '').trim();
581
+ if (!icon)
582
+ return { clicked: false, message: 'Icon description is empty' };
583
+ if (typeof (driver === null || driver === void 0 ? void 0 : driver.performActions) !== 'function') {
584
+ return {
585
+ clicked: false,
586
+ message: 'Driver does not support performActions',
587
+ };
588
+ }
589
+ this.logger.info(`Omni-Click: searching for icon "${icon}" via AI...`);
590
+ const element = yield this.findByIcon(driver, icon);
591
+ if (!element) {
592
+ return { clicked: false, message: `No visual match found for icon "${icon}"` };
593
+ }
594
+ const cx = Math.round(element.rect.x + element.rect.width / 2);
595
+ const cy = Math.round(element.rect.y + element.rect.height / 2);
596
+ yield driver.performActions([
597
+ {
598
+ type: 'pointer',
599
+ id: 'finger1',
600
+ parameters: { pointerType: 'touch' },
601
+ actions: [
602
+ { type: 'pointerMove', duration: 0, x: cx, y: cy },
603
+ { type: 'pointerDown', button: 0 },
604
+ { type: 'pause', duration: 120 },
605
+ { type: 'pointerUp', button: 0 },
606
+ ],
607
+ },
608
+ ]);
609
+ return {
610
+ clicked: true,
611
+ message: `Clicked visual match for "${icon}"`,
612
+ target: {
613
+ text: icon,
614
+ index: 1,
615
+ x: cx,
616
+ y: cy,
617
+ rect: element.rect,
618
+ confidence: element.confidence,
619
+ },
620
+ };
621
+ });
622
+ }
623
+ /**
624
+ * UI Lens Export: OCR + lightweight visual metadata similar to "UI lens" tools.
625
+ */
626
+ exportUiLensMetadata(driver, options) {
627
+ return __awaiter(this, void 0, void 0, function* () {
628
+ const takeANewScreenShot = (options === null || options === void 0 ? void 0 : options.takeANewScreenShot) !== false;
629
+ const maxItems = typeof (options === null || options === void 0 ? void 0 : options.maxItems) === 'number' ? options.maxItems : 200;
630
+ const screenshot = yield driver.getScreenshot();
631
+ const hash = crypto_1.default.createHash('sha1').update(screenshot).digest('hex');
632
+ const cacheKey = String((driver === null || driver === void 0 ? void 0 : driver.sessionId) || 'unknown');
633
+ const cached = this.uiLensCache.get(cacheKey);
634
+ if (cached &&
635
+ cached.hash === hash &&
636
+ Date.now() - cached.createdAt < OmniVisionService_1.UI_LENS_CACHE_TTL_MS) {
637
+ return cached.items;
638
+ }
639
+ const buffer = Buffer.from(screenshot, 'base64');
640
+ const baseImage = (0, sharp_1.default)(buffer);
641
+ const meta = yield baseImage.metadata();
642
+ const imageW = meta.width || 0;
643
+ const imageH = meta.height || 0;
644
+ const { words } = yield this.performOcr(buffer);
645
+ const normalized = words.map((w) => this.normalizeWordBBox(w)).filter(Boolean);
646
+ // Reduce noise: keep higher-confidence words first
647
+ const sliced = normalized
648
+ .sort((a, b) => (b.confidence || 0) - (a.confidence || 0))
649
+ .slice(0, Math.max(1, Math.min(1000, maxItems)));
650
+ // Precompute centers
651
+ const nodes = sliced.map((w) => {
652
+ const cx = (w.x0 + w.x1) / 2;
653
+ const cy = (w.y0 + w.y1) / 2;
654
+ return Object.assign(Object.assign({}, w), { cx, cy });
655
+ });
656
+ const toPosition = (cx, cy) => {
657
+ const horiz = imageW
658
+ ? cx < imageW / 3
659
+ ? 'left'
660
+ : cx < (2 * imageW) / 3
661
+ ? 'center'
662
+ : 'right'
663
+ : 'center';
664
+ const vert = imageH
665
+ ? cy < imageH / 3
666
+ ? 'top'
667
+ : cy < (2 * imageH) / 3
668
+ ? 'middle'
669
+ : 'bottom'
670
+ : 'middle';
671
+ return `${vert} ${horiz}`;
672
+ };
673
+ // Alignment: simplistic row grouping by y tolerance
674
+ const yTol = 10;
675
+ const alignedRow = (cy) => {
676
+ const inSameRow = nodes.filter((n) => Math.abs(n.cy - cy) <= yTol);
677
+ return inSameRow.length >= 2 ? 'aligned' : 'not aligned';
678
+ };
679
+ // Above/Below: nearest vertically overlapping candidate
680
+ const findNeighbor = (node, dir) => {
681
+ var _a;
682
+ const candidates = nodes
683
+ .filter((n) => {
684
+ if (dir === 'above' && n.cy >= node.cy)
685
+ return false;
686
+ if (dir === 'below' && n.cy <= node.cy)
687
+ return false;
688
+ const overlapX = Math.min(n.x1, node.x1) - Math.max(n.x0, node.x0);
689
+ return overlapX > 0;
690
+ })
691
+ .sort((a, b) => Math.abs(a.cy - node.cy) - Math.abs(b.cy - node.cy));
692
+ return ((_a = candidates[0]) === null || _a === void 0 ? void 0 : _a.text) || null;
693
+ };
694
+ const items = [];
695
+ for (const n of nodes) {
696
+ const color = imageW && imageH ? yield this.sampleAverageColorName(baseImage, n, imageW, imageH) : null;
697
+ items.push({
698
+ text: n.text,
699
+ color,
700
+ position: toPosition(n.cx, n.cy),
701
+ aligned: alignedRow(n.cy),
702
+ above: findNeighbor(n, 'above'),
703
+ below: findNeighbor(n, 'below'),
704
+ icon: null,
705
+ icon_color: null,
706
+ icon_category: null,
707
+ });
708
+ }
709
+ this.uiLensCache.set(cacheKey, { hash, createdAt: Date.now(), items });
710
+ return items;
711
+ });
712
+ }
713
+ };
714
+ exports.OmniVisionService = OmniVisionService;
715
+ OmniVisionService.UI_LENS_CACHE_TTL_MS = 10000; // 10s TTL to avoid repeated OCR on rapid calls
716
+ exports.OmniVisionService = OmniVisionService = OmniVisionService_1 = __decorate([
717
+ (0, typedi_1.Service)()
718
+ ], OmniVisionService);