@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.
- package/README.md +446 -0
- package/lib/package.json +207 -0
- package/lib/public/assets/Layouts-7IT8aFLI.js +11 -0
- package/lib/public/assets/Layouts-DPMls9vh.css +1 -0
- package/lib/public/assets/ai-settings-BbnfgdEx.js +11 -0
- package/lib/public/assets/apps-CRMrI4_p.js +16 -0
- package/lib/public/assets/apps-CcM77dgg.css +1 -0
- package/lib/public/assets/badge-B1nKs8zj.css +1 -0
- package/lib/public/assets/badge-CSvl5xIU.js +11 -0
- package/lib/public/assets/button-CJlKn4PZ.css +1 -0
- package/lib/public/assets/button-CvLaGFYj.js +26 -0
- package/lib/public/assets/calendar-6w-D6Oaw.js +6 -0
- package/lib/public/assets/clock-DcdeWBPr.js +6 -0
- package/lib/public/assets/cpu-DiSoXT9n.js +6 -0
- package/lib/public/assets/device-explorer-CajM63OJ.js +193 -0
- package/lib/public/assets/device-explorer-CxdUAoTL.css +1 -0
- package/lib/public/assets/index-ByQwMN5T.js +174 -0
- package/lib/public/assets/index-C1DBaoSh.js +1 -0
- package/lib/public/assets/index-qzCez_kk.css +1 -0
- package/lib/public/assets/lock-B23ibZmo.js +6 -0
- package/lib/public/assets/maintenance-settings-CirzA6yG.js +6 -0
- package/lib/public/assets/mouse-pointer-2-Cz76SHFb.js +6 -0
- package/lib/public/assets/plus-BBwlIevt.js +6 -0
- package/lib/public/assets/session-dashboard-C2k7FFv_.css +1 -0
- package/lib/public/assets/session-dashboard-HPDtwPOZ.js +62 -0
- package/lib/public/assets/settings-DrZsZwdc.js +1 -0
- package/lib/public/assets/trash-2-DQpvzJec.js +6 -0
- package/lib/public/assets/useSocket-Dxsqae2a.js +16 -0
- package/lib/public/assets/webhook-settings-CDPgsgkb.css +1 -0
- package/lib/public/assets/webhook-settings-Cp-B4Nrw.js +1 -0
- package/lib/public/assets/zap-DovP6iow.js +6 -0
- package/lib/public/favicon.ico +0 -0
- package/lib/public/favicon.png +0 -0
- package/lib/public/favicon.svg +9 -0
- package/lib/public/index.html +46 -0
- package/lib/public/logo.svg +17 -0
- package/lib/public/logo192.png +0 -0
- package/lib/public/logo512.png +0 -0
- package/lib/public/manifest.json +25 -0
- package/lib/public/robots.txt +3 -0
- package/lib/schema.json +348 -0
- package/lib/src/InternalHttpClient.js +212 -0
- package/lib/src/PluginContext.js +29 -0
- package/lib/src/XenonCapabilityManager.js +199 -0
- package/lib/src/app/index.js +167 -0
- package/lib/src/app/routers/apps.js +79 -0
- package/lib/src/app/routers/config.js +131 -0
- package/lib/src/app/routers/control.js +835 -0
- package/lib/src/app/routers/dashboard.js +301 -0
- package/lib/src/app/routers/grid.js +352 -0
- package/lib/src/app/routers/reservation.js +190 -0
- package/lib/src/app/routers/webhook.js +83 -0
- package/lib/src/app/swagger-docs.js +203 -0
- package/lib/src/app/swagger.js +366 -0
- package/lib/src/chromeUtils.js +148 -0
- package/lib/src/commands/handle.js +19 -0
- package/lib/src/commands/index.js +8 -0
- package/lib/src/config.js +73 -0
- package/lib/src/dashboard/asset-manager.js +84 -0
- package/lib/src/dashboard/commands.js +284 -0
- package/lib/src/dashboard/event-manager.js +699 -0
- package/lib/src/dashboard/services/app-service.js +134 -0
- package/lib/src/dashboard/services/failure-analysis-service.js +173 -0
- package/lib/src/dashboard/services/session-service.js +113 -0
- package/lib/src/data-service/CircuitBreaker.js +83 -0
- package/lib/src/data-service/config-service.js +155 -0
- package/lib/src/data-service/db.js +122 -0
- package/lib/src/data-service/device-service.js +320 -0
- package/lib/src/data-service/device-store.interface.js +2 -0
- package/lib/src/data-service/device-store.js +345 -0
- package/lib/src/data-service/pending-sessions-service.js +25 -0
- package/lib/src/data-service/pluginArgs.js +25 -0
- package/lib/src/data-service/prisma-service.js +31 -0
- package/lib/src/data-service/prisma-store.js +385 -0
- package/lib/src/data-service/queue-service.js +150 -0
- package/lib/src/data-service/web-config-service.js +130 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +1155 -0
- package/lib/src/device-managers/ChromeDriverManager.js +68 -0
- package/lib/src/device-managers/HealthMonitorService.js +325 -0
- package/lib/src/device-managers/IOSDeviceManager.js +351 -0
- package/lib/src/device-managers/NodeDevices.js +82 -0
- package/lib/src/device-managers/android/AndroidStreamService.js +370 -0
- package/lib/src/device-managers/android/DeviceLockManager.js +45 -0
- package/lib/src/device-managers/cloud/CapabilityManager.js +26 -0
- package/lib/src/device-managers/cloud/Devices.js +86 -0
- package/lib/src/device-managers/iOSTracker.js +44 -0
- package/lib/src/device-managers/index.js +89 -0
- package/lib/src/device-managers/ios/IOSDiscoveryService.js +268 -0
- package/lib/src/device-managers/ios/IOSStreamService.js +893 -0
- package/lib/src/device-managers/ios/WDAClient.js +866 -0
- package/lib/src/device-utils.js +663 -0
- package/lib/src/enums/Capabilities.js +8 -0
- package/lib/src/enums/Cloud.js +11 -0
- package/lib/src/enums/Platform.js +9 -0
- package/lib/src/enums/SessionType.js +9 -0
- package/lib/src/enums/SocketEvents.js +15 -0
- package/lib/src/helpers/UniversalMjpegProxy.js +273 -0
- package/lib/src/helpers/index.js +229 -0
- package/lib/src/index.js +95 -0
- package/lib/src/interceptors/CommandInterceptor.js +524 -0
- package/lib/src/interfaces/ICloudManager.js +2 -0
- package/lib/src/interfaces/IDevice.js +2 -0
- package/lib/src/interfaces/IDeviceFilterOptions.js +2 -0
- package/lib/src/interfaces/IDeviceManager.js +2 -0
- package/lib/src/interfaces/IOptions.js +2 -0
- package/lib/src/interfaces/IPluginArgs.js +55 -0
- package/lib/src/interfaces/ISessionCapability.js +2 -0
- package/lib/src/logger.js +225 -0
- package/lib/src/plugin.js +244 -0
- package/lib/src/prisma.js +12 -0
- package/lib/src/profiling/AndroidAppProfiler.js +213 -0
- package/lib/src/proxy/wd-command-proxy.js +221 -0
- package/lib/src/scripts/generate-database-migration.js +59 -0
- package/lib/src/scripts/initialize-database.js +55 -0
- package/lib/src/scripts/install-go-ios.js +66 -0
- package/lib/src/scripts/prepare-prisma.js +89 -0
- package/lib/src/services/AICommandService.js +143 -0
- package/lib/src/services/AIService.js +466 -0
- package/lib/src/services/CleanupService.js +141 -0
- package/lib/src/services/EventBus.js +74 -0
- package/lib/src/services/InspectorService.js +395 -0
- package/lib/src/services/MetricsService.js +134 -0
- package/lib/src/services/NetworkConditioningService.js +173 -0
- package/lib/src/services/NotificationService.js +163 -0
- package/lib/src/services/RequestLogService.js +252 -0
- package/lib/src/services/ResourceIsolationService.js +122 -0
- package/lib/src/services/SecurityService.js +120 -0
- package/lib/src/services/ServerManager.js +284 -0
- package/lib/src/services/SessionHeartbeatService.js +158 -0
- package/lib/src/services/SessionLifecycleService.js +572 -0
- package/lib/src/services/SocketClient.js +71 -0
- package/lib/src/services/SocketServer.js +87 -0
- package/lib/src/services/TracingService.js +132 -0
- package/lib/src/services/VideoPipelineService.js +220 -0
- package/lib/src/services/healing/FuzzyXmlHealingProvider.js +333 -0
- package/lib/src/services/healing/HealEtalonService.js +98 -0
- package/lib/src/services/healing/HealedLocatorGenerator.js +132 -0
- package/lib/src/services/healing/HealingOrchestrator.js +165 -0
- package/lib/src/services/healing/LlmHealingProvider.js +77 -0
- package/lib/src/services/healing/OcrHealingProvider.js +119 -0
- package/lib/src/services/healing/ResilioTreeHealingProvider.js +100 -0
- package/lib/src/services/healing/VisualAiHealingProvider.js +90 -0
- package/lib/src/services/healing/types.js +12 -0
- package/lib/src/services/omni-vision/OmniVisionService.js +718 -0
- package/lib/src/services/omni-vision/VisionAssertionService.js +68 -0
- package/lib/src/sessions/CloudSession.js +42 -0
- package/lib/src/sessions/LocalSession.js +313 -0
- package/lib/src/sessions/RemoteSession.js +287 -0
- package/lib/src/sessions/SessionManager.js +238 -0
- package/lib/src/sessions/XenonSession.js +44 -0
- package/lib/src/types/CLIArgs.js +2 -0
- package/lib/src/types/CloudArgs.js +2 -0
- package/lib/src/types/CloudSchema.js +131 -0
- package/lib/src/types/DeviceType.js +2 -0
- package/lib/src/types/DeviceUpdate.js +2 -0
- package/lib/src/types/IOSDevice.js +2 -0
- package/lib/src/types/Platform.js +2 -0
- package/lib/src/types/SessionStatus.js +11 -0
- package/lib/src/validators/CapabilityValidator.js +93 -0
- package/lib/test/e2e/android/conf.spec.js +43 -0
- package/lib/test/e2e/android/conf2.spec.js +44 -0
- package/lib/test/e2e/android/conf3.spec.js +44 -0
- package/lib/test/e2e/e2ehelper.js +113 -0
- package/lib/test/e2e/hubnode/forward-request.spec.js +224 -0
- package/lib/test/e2e/hubnode/hubnode.spec.js +214 -0
- package/lib/test/e2e/ios/conf1.spec.js +39 -0
- package/lib/test/e2e/ios/conf2.spec.js +39 -0
- package/lib/test/e2e/plugin-harness.js +236 -0
- package/lib/test/e2e/plugin.spec.js +97 -0
- package/lib/test/e2e/telemetry_verification.spec.js +83 -0
- package/lib/test/e2e/video-recording-test.spec.js +63 -0
- package/lib/test/helpers/test-container.js +112 -0
- package/lib/test/integration/androidDevices.spec.js +137 -0
- package/lib/test/integration/cliArgs.js +73 -0
- package/lib/test/integration/ios/01iOSSimulator.spec.js +291 -0
- package/lib/test/integration/ios/02iOSDevices.spec.js +75 -0
- package/lib/test/integration/testHelpers.js +74 -0
- package/lib/test/unit/AndroidDeviceManager.spec.js +178 -0
- package/lib/test/unit/ChromeDriverManager.spec.js +26 -0
- package/lib/test/unit/CleanupService.spec.js +21 -0
- package/lib/test/unit/DeviceModel.spec.js +157 -0
- package/lib/test/unit/FuzzyXmlHealingProvider.test.js +294 -0
- package/lib/test/unit/GetAdbOriginal.js +42 -0
- package/lib/test/unit/HealingCascade.test.js +128 -0
- package/lib/test/unit/IOSDeviceManager.spec.js +261 -0
- package/lib/test/unit/RemoteIOs.spec.js +78 -0
- package/lib/test/unit/ResilioTreeHealingProvider.test.js +96 -0
- package/lib/test/unit/commands.spec.js +27 -0
- package/lib/test/unit/config.spec.js +27 -0
- package/lib/test/unit/device-service.spec.js +307 -0
- package/lib/test/unit/device-utils.spec.js +313 -0
- package/lib/test/unit/fixtures/device.config.js +4 -0
- package/lib/test/unit/fixtures/devices.js +89 -0
- package/lib/test/unit/helpers.spec.js +62 -0
- package/lib/test/unit/omni-vision.spec.js +100 -0
- package/lib/test/unit/plugin.spec.js +133 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +207 -0
- package/prisma/data.db +0 -0
- package/prisma/dev.db +0 -0
- package/prisma/dev.db-journal +0 -0
- package/prisma/migrations/20231011074725_initial_tables/migration.sql +47 -0
- package/prisma/migrations/20231226115334_update_session_log/migration.sql +2 -0
- package/prisma/migrations/20251204113710_add_video_recording_enabled/migration.sql +29 -0
- package/prisma/migrations/20251204132449_add_log_table/migration.sql +11 -0
- package/prisma/migrations/20251205050111_add_profiling_support/migration.sql +47 -0
- package/prisma/migrations/20251205050947_add_is_error_field/migration.sql +24 -0
- package/prisma/migrations/20260126201337_add_app_model/migration.sql +18 -0
- package/prisma/migrations/20260130115722_add_performance_trace_and_xenon_sync/migration.sql +2 -0
- package/prisma/migrations/20260130135114_add_device_models/migration.sql +57 -0
- package/prisma/migrations/20260130140655_make_systemport_optional/migration.sql +45 -0
- package/prisma/migrations/20260130140932_make_device_fields_optional/migration.sql +45 -0
- package/prisma/migrations/20260130141040_final_schema_fix/migration.sql +45 -0
- package/prisma/migrations/20260130143234_add_device_health_fields/migration.sql +4 -0
- package/prisma/migrations/20260130144921_add_failure_category/migration.sql +2 -0
- package/prisma/migrations/20260131151456_add_webhook_config/migration.sql +10 -0
- package/prisma/migrations/20260201094507_add_device_tags/migration.sql +11 -0
- package/prisma/migrations/20260201103410_add_managed_process/migration.sql +15 -0
- package/prisma/migrations/20260201140637_add_web_config/migration.sql +22 -0
- package/prisma/migrations/20260201162232_add_session_progress/migration.sql +2 -0
- package/prisma/migrations/20260201174231_add_total_healed_count/migration.sql +2 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +210 -0
- package/schema.json +348 -0
- package/scripts/build-xenon.sh +32 -0
- package/scripts/dev/debug-gemini.ts +44 -0
- package/scripts/generate-types-from-schema.js +86 -0
- package/scripts/install-compatible-driver.js +39 -0
|
@@ -0,0 +1,466 @@
|
|
|
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 __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.AI_SERVICE = exports.AIService = void 0;
|
|
49
|
+
const generative_ai_1 = require("@google/generative-ai");
|
|
50
|
+
const openai_1 = __importDefault(require("openai"));
|
|
51
|
+
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
52
|
+
const axios_1 = __importDefault(require("axios"));
|
|
53
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
54
|
+
const fs = __importStar(require("fs"));
|
|
55
|
+
const path = __importStar(require("path"));
|
|
56
|
+
const config_1 = require("../config");
|
|
57
|
+
class GeminiProvider {
|
|
58
|
+
constructor(apiKey, modelName) {
|
|
59
|
+
this.genAI = new generative_ai_1.GoogleGenerativeAI(apiKey.trim());
|
|
60
|
+
this.modelName =
|
|
61
|
+
modelName && modelName.trim() !== '' ? modelName.trim() : 'gemini-3-flash-preview';
|
|
62
|
+
}
|
|
63
|
+
analyze(prompt, screenshotBase64) {
|
|
64
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
65
|
+
const parts = [prompt];
|
|
66
|
+
if (screenshotBase64) {
|
|
67
|
+
parts.push({
|
|
68
|
+
inlineData: {
|
|
69
|
+
data: screenshotBase64,
|
|
70
|
+
mimeType: 'image/png',
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// Deep Intelligence: Try v1 (stable), then v1beta (experimental)
|
|
75
|
+
// Optimization: Preview models (like gemini-3-flash-preview) often require v1beta.
|
|
76
|
+
const isPreview = this.modelName.includes('preview') || this.modelName.includes('experimental');
|
|
77
|
+
const versions = isPreview ? ['v1beta', 'v1'] : ['v1', 'v1beta'];
|
|
78
|
+
let lastError = null;
|
|
79
|
+
for (const version of versions) {
|
|
80
|
+
try {
|
|
81
|
+
const model = this.genAI.getGenerativeModel({ model: this.modelName }, { apiVersion: version });
|
|
82
|
+
const result = yield model.generateContent(parts);
|
|
83
|
+
const response = yield result.response;
|
|
84
|
+
return response.text();
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
lastError = err;
|
|
88
|
+
// If we get a 429, the connection IS WORKING, just rate limited.
|
|
89
|
+
if (err.message.includes('429') || err.message.includes('Quota exceeded')) {
|
|
90
|
+
logger_1.default.info(`[Gemini] Connection verified (Rate Limited) via ${version} endpoint.`);
|
|
91
|
+
return 'CONNECTION_OK_RATE_LIMITED';
|
|
92
|
+
}
|
|
93
|
+
if (err.message.includes('404') ||
|
|
94
|
+
err.message.includes('not found') ||
|
|
95
|
+
err.message.includes('unsupported')) {
|
|
96
|
+
logger_1.default.info(`[Gemini] ${version} endpoint failed for ${this.modelName}. Trying next...`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
throw lastError;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
class OpenAIProvider {
|
|
107
|
+
constructor(apiKey, model = 'gpt-4o', baseURL) {
|
|
108
|
+
this.client = new openai_1.default({ apiKey, baseURL });
|
|
109
|
+
this.model = model;
|
|
110
|
+
}
|
|
111
|
+
analyze(prompt, screenshotBase64) {
|
|
112
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
113
|
+
const messages = [
|
|
114
|
+
{
|
|
115
|
+
role: 'user',
|
|
116
|
+
content: [{ type: 'text', text: prompt }],
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
if (screenshotBase64) {
|
|
120
|
+
messages[0].content.push({
|
|
121
|
+
type: 'image_url',
|
|
122
|
+
image_url: { url: `data:image/png;base64,${screenshotBase64}` },
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const response = yield this.client.chat.completions.create({
|
|
126
|
+
model: this.model,
|
|
127
|
+
messages,
|
|
128
|
+
max_tokens: 500,
|
|
129
|
+
});
|
|
130
|
+
return response.choices[0].message.content || '';
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
class AnthropicProvider {
|
|
135
|
+
constructor(apiKey, model = 'claude-3-5-sonnet-20240620') {
|
|
136
|
+
this.client = new sdk_1.default({ apiKey });
|
|
137
|
+
this.model = model;
|
|
138
|
+
}
|
|
139
|
+
analyze(prompt, screenshotBase64) {
|
|
140
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
141
|
+
const content = [{ type: 'text', text: prompt }];
|
|
142
|
+
if (screenshotBase64) {
|
|
143
|
+
content.push({
|
|
144
|
+
type: 'image',
|
|
145
|
+
source: {
|
|
146
|
+
type: 'base64',
|
|
147
|
+
media_type: 'image/png',
|
|
148
|
+
data: screenshotBase64,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const response = yield this.client.messages.create({
|
|
153
|
+
model: this.model,
|
|
154
|
+
max_tokens: 500,
|
|
155
|
+
messages: [{ role: 'user', content }],
|
|
156
|
+
});
|
|
157
|
+
return response.content[0].text || '';
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
class OllamaProvider {
|
|
162
|
+
constructor(baseUrl = 'http://localhost:11434', model = 'llama3') {
|
|
163
|
+
this.isAvailable = null; // Cache availability status
|
|
164
|
+
this.lastAvailabilityCheck = 0;
|
|
165
|
+
this.AVAILABILITY_CHECK_INTERVAL = 60000; // Check every 60s
|
|
166
|
+
this.baseUrl = baseUrl;
|
|
167
|
+
this.model = model;
|
|
168
|
+
}
|
|
169
|
+
checkAvailability() {
|
|
170
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
// Use cached status if checked recently
|
|
173
|
+
if (this.isAvailable !== null &&
|
|
174
|
+
now - this.lastAvailabilityCheck < this.AVAILABILITY_CHECK_INTERVAL) {
|
|
175
|
+
return this.isAvailable;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
// Quick health check - Ollama has a /api/tags endpoint
|
|
179
|
+
yield axios_1.default.get(`${this.baseUrl}/api/tags`, { timeout: 2000 });
|
|
180
|
+
this.isAvailable = true;
|
|
181
|
+
this.lastAvailabilityCheck = now;
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
this.isAvailable = false;
|
|
186
|
+
this.lastAvailabilityCheck = now;
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
analyze(prompt, screenshotBase64) {
|
|
192
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
193
|
+
var _a;
|
|
194
|
+
// Check if Ollama is available before attempting
|
|
195
|
+
const available = yield this.checkAvailability();
|
|
196
|
+
if (!available) {
|
|
197
|
+
throw new Error(`Ollama service unavailable at ${this.baseUrl}. Is Ollama running?`);
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const response = yield axios_1.default.post(`${this.baseUrl}/api/generate`, {
|
|
201
|
+
model: this.model,
|
|
202
|
+
prompt: prompt,
|
|
203
|
+
images: screenshotBase64 ? [screenshotBase64] : [],
|
|
204
|
+
stream: false,
|
|
205
|
+
}, { timeout: 30000 });
|
|
206
|
+
return response.data.response || '';
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
// Mark as unavailable on 404/ECONNREFUSED
|
|
210
|
+
if (((_a = err.response) === null || _a === void 0 ? void 0 : _a.status) === 404 || err.code === 'ECONNREFUSED') {
|
|
211
|
+
this.isAvailable = false;
|
|
212
|
+
throw new Error(`Ollama service unavailable at ${this.baseUrl}. Is Ollama running?`);
|
|
213
|
+
}
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
class AIService {
|
|
220
|
+
constructor() {
|
|
221
|
+
this.provider = null;
|
|
222
|
+
this.isMock = false;
|
|
223
|
+
this.initializeProvider();
|
|
224
|
+
}
|
|
225
|
+
initializeProvider() {
|
|
226
|
+
const providerType = config_1.config.aiProvider;
|
|
227
|
+
const genericModel = config_1.config.aiModel;
|
|
228
|
+
const baseUrl = config_1.config.aiBaseUrl;
|
|
229
|
+
logger_1.default.info(`[AIService] Initializing with provider: ${providerType}`);
|
|
230
|
+
if (process.env.GEMINI_API_KEY === 'mock') {
|
|
231
|
+
this.isMock = true;
|
|
232
|
+
logger_1.default.info('[AIService] Running in MOCK mode.');
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
switch (providerType) {
|
|
237
|
+
case 'gemini':
|
|
238
|
+
const geminiKey = config_1.config.geminiApiKey;
|
|
239
|
+
if (geminiKey)
|
|
240
|
+
this.provider = new GeminiProvider(geminiKey, config_1.config.geminiModel || genericModel);
|
|
241
|
+
break;
|
|
242
|
+
case 'openai':
|
|
243
|
+
const openaiKey = config_1.config.openaiApiKey;
|
|
244
|
+
if (openaiKey)
|
|
245
|
+
this.provider = new OpenAIProvider(openaiKey, config_1.config.openaiModel || genericModel || 'gpt-4o', baseUrl);
|
|
246
|
+
break;
|
|
247
|
+
case 'anthropic':
|
|
248
|
+
const anthropicKey = config_1.config.anthropicApiKey;
|
|
249
|
+
if (anthropicKey)
|
|
250
|
+
this.provider = new AnthropicProvider(anthropicKey, config_1.config.anthropicModel || genericModel || 'claude-3-5-sonnet-20240620');
|
|
251
|
+
break;
|
|
252
|
+
case 'ollama':
|
|
253
|
+
this.provider = new OllamaProvider(baseUrl || 'http://localhost:11434', config_1.config.ollamaModel || genericModel || 'llama3');
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
if (!this.provider && !this.isMock) {
|
|
257
|
+
logger_1.default.warn(`[AIService] No valid API key found for ${providerType}. AI features disabled.`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
logger_1.default.error(`[AIService] Initialization error: ${err.message}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
isEnabled() {
|
|
265
|
+
return this.provider !== null || this.isMock;
|
|
266
|
+
}
|
|
267
|
+
analyzeFailure(context) {
|
|
268
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
269
|
+
// Re-initialize provider to pick up runtime config changes
|
|
270
|
+
this.initializeProvider();
|
|
271
|
+
if (!this.isEnabled())
|
|
272
|
+
return null;
|
|
273
|
+
if (this.isMock) {
|
|
274
|
+
return `
|
|
275
|
+
Root Cause: The test failed because the **'Login' button** was obscured by a system permission dialog ("Allow Xenon to access location?"). This prevented the automated click from registering.
|
|
276
|
+
|
|
277
|
+
Fix: Add a pre-emptive check for the location permission dialog or use the \`autoAcceptAlerts\` capability to handle system popups automatically.
|
|
278
|
+
`.trim();
|
|
279
|
+
}
|
|
280
|
+
logger_1.default.info(`[AIService] Analyzing failure for session ${context.sessionId} via ${config_1.config.aiProvider}`);
|
|
281
|
+
try {
|
|
282
|
+
const prompt = this.constructPrompt(context);
|
|
283
|
+
const screenshotBase64 = this.getScreenshotBase64(context.screenshotPath);
|
|
284
|
+
const text = yield this.provider.analyze(prompt, screenshotBase64 || undefined);
|
|
285
|
+
logger_1.default.info(`[AIService] Analysis complete for ${context.sessionId}`);
|
|
286
|
+
return text;
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
logger_1.default.error(`[AIService] Analysis failed: ${err.message}`);
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Finds coordinates for an element based on a visual description (Tier 4)
|
|
296
|
+
*/
|
|
297
|
+
visualFind(screenshotBase64, description) {
|
|
298
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
299
|
+
var _a, _b;
|
|
300
|
+
this.initializeProvider();
|
|
301
|
+
if (!this.isEnabled())
|
|
302
|
+
return null;
|
|
303
|
+
const prompt = `
|
|
304
|
+
Task: Find the center coordinates (X, Y) of the element described as: "${description}"
|
|
305
|
+
Response Format: JSON only, strictly { "x": number, "y": number }.
|
|
306
|
+
Scale: 0 to screen width/height.
|
|
307
|
+
Instructions: Look at the provided screenshot and find the exact center of the specified element.
|
|
308
|
+
`;
|
|
309
|
+
try {
|
|
310
|
+
const response = yield this.provider.analyze(prompt, screenshotBase64);
|
|
311
|
+
const data = JSON.parse(response.replace(/```json|```/g, '').trim());
|
|
312
|
+
return data;
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
// Log service unavailability at debug level (expected), other errors at warn level
|
|
316
|
+
if (((_a = err.message) === null || _a === void 0 ? void 0 : _a.includes('unavailable')) || ((_b = err.response) === null || _b === void 0 ? void 0 : _b.status) === 404) {
|
|
317
|
+
logger_1.default.debug(`[AIService] visualFind skipped: ${err.message}`);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
logger_1.default.warn(`[AIService] visualFind failed: ${err.message}`);
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Heals a broken locator using deep LLM reasoning (Tier 5)
|
|
328
|
+
*/
|
|
329
|
+
healLocator(context) {
|
|
330
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
331
|
+
var _a, _b;
|
|
332
|
+
this.initializeProvider();
|
|
333
|
+
if (!this.isEnabled())
|
|
334
|
+
return null;
|
|
335
|
+
// Deep Intelligence: Skip healing if the selector is explicitly meant for failure verification
|
|
336
|
+
if (context.selector.includes('NON_EXISTENT')) {
|
|
337
|
+
logger_1.default.info(`[AIService] Skipping healing for verification locator: ${context.selector}`);
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
const prompt = `
|
|
341
|
+
You are an automated self-healing engine.
|
|
342
|
+
The locator "${context.selector}" (strategy: ${context.strategy}) failed to find an element.
|
|
343
|
+
|
|
344
|
+
Current Page Source (XML):
|
|
345
|
+
${context.xml.substring(0, 10000)} ... [truncated]
|
|
346
|
+
|
|
347
|
+
Analyze the XML and the provided screenshot. Find the element that most likely matches the developer's intent.
|
|
348
|
+
Return a stable, optimized XPath for this element.
|
|
349
|
+
|
|
350
|
+
Response Format: JSON only, strictly { "recommendedXpath": "string", "reason": "string" }.
|
|
351
|
+
`;
|
|
352
|
+
try {
|
|
353
|
+
const response = yield this.provider.analyze(prompt, context.screenshotBase64);
|
|
354
|
+
const data = JSON.parse(response.replace(/```json|```/g, '').trim());
|
|
355
|
+
return data;
|
|
356
|
+
}
|
|
357
|
+
catch (err) {
|
|
358
|
+
// Log service unavailability at debug level (expected), other errors at warn level
|
|
359
|
+
if (((_a = err.message) === null || _a === void 0 ? void 0 : _a.includes('unavailable')) || ((_b = err.response) === null || _b === void 0 ? void 0 : _b.status) === 404) {
|
|
360
|
+
logger_1.default.debug(`[AIService] healLocator skipped: ${err.message}`);
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
logger_1.default.warn(`[AIService] healLocator failed: ${err.message}`);
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
testConnection(testConfig) {
|
|
370
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
371
|
+
try {
|
|
372
|
+
let testProvider = null;
|
|
373
|
+
const providerType = testConfig.aiProvider || config_1.config.aiProvider;
|
|
374
|
+
const model = testConfig.aiModel || config_1.config.aiModel;
|
|
375
|
+
const baseUrl = testConfig.aiBaseUrl || config_1.config.aiBaseUrl;
|
|
376
|
+
logger_1.default.info(`[AIService] Testing connection for provider: ${providerType}`);
|
|
377
|
+
switch (providerType) {
|
|
378
|
+
case 'gemini':
|
|
379
|
+
// Prioritize Environment Key (via config singleton), then fallback to UI input
|
|
380
|
+
const geminiKey = (config_1.config.geminiApiKey || testConfig.geminiApiKey || '').trim();
|
|
381
|
+
if (!geminiKey)
|
|
382
|
+
throw new Error('Gemini API Key missing. Please set XENON_GEMINI_API_KEY environment variable.');
|
|
383
|
+
const targetGeminiModel = (config_1.config.geminiModel || model || '').trim() || 'gemini-3-flash-preview';
|
|
384
|
+
testProvider = new GeminiProvider(geminiKey, targetGeminiModel);
|
|
385
|
+
break;
|
|
386
|
+
case 'openai':
|
|
387
|
+
const openaiKey = (config_1.config.openaiApiKey || testConfig.openaiApiKey || '').trim();
|
|
388
|
+
if (!openaiKey)
|
|
389
|
+
throw new Error('OpenAI API Key missing. Please set XENON_OPENAI_API_KEY environment variable.');
|
|
390
|
+
testProvider = new OpenAIProvider(openaiKey, config_1.config.openaiModel || model || 'gpt-4o', baseUrl);
|
|
391
|
+
break;
|
|
392
|
+
case 'anthropic':
|
|
393
|
+
const anthropicKey = (config_1.config.anthropicApiKey || testConfig.anthropicApiKey || '').trim();
|
|
394
|
+
if (!anthropicKey)
|
|
395
|
+
throw new Error('Anthropic API Key missing. Please set XENON_ANTHROPIC_API_KEY environment variable.');
|
|
396
|
+
testProvider = new AnthropicProvider(anthropicKey, config_1.config.anthropicModel || model || 'claude-3-5-sonnet-20240620');
|
|
397
|
+
break;
|
|
398
|
+
case 'ollama':
|
|
399
|
+
testProvider = new OllamaProvider(baseUrl || 'http://localhost:11434', config_1.config.ollamaModel || model || 'llama3');
|
|
400
|
+
break;
|
|
401
|
+
default:
|
|
402
|
+
throw new Error(`Unsupported provider: ${providerType}`);
|
|
403
|
+
}
|
|
404
|
+
if (!testProvider)
|
|
405
|
+
throw new Error('Failed to initialize provider for testing');
|
|
406
|
+
// Send a minimal ping command
|
|
407
|
+
const responseText = yield testProvider.analyze('Hello. Response: OK');
|
|
408
|
+
if (responseText === 'CONNECTION_OK_RATE_LIMITED') {
|
|
409
|
+
return {
|
|
410
|
+
success: true,
|
|
411
|
+
message: `Successfully connected to ${providerType}! (Note: You are currently out of quota/rate-limited, but the setup is correct)`,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
return { success: true, message: `Successfully connected to ${providerType}!` };
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
logger_1.default.error(`[AIService] Connection test failed: ${err.message}`);
|
|
418
|
+
return { success: false, message: `Connection failed: ${err.message}` };
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
constructPrompt(context) {
|
|
423
|
+
return `
|
|
424
|
+
You are an Elite Mobile Automation Expert and Root Cause Analyst.
|
|
425
|
+
Your mission is to analyze a failed Appium test session and explain EXACTLY why it failed in simple, human-readable terms.
|
|
426
|
+
|
|
427
|
+
### Context:
|
|
428
|
+
- **Session ID**: ${context.sessionId}
|
|
429
|
+
- **Primary Failure Reason**: ${context.failureReason}
|
|
430
|
+
|
|
431
|
+
### Last 10 Commands:
|
|
432
|
+
${JSON.stringify(context.commandLogs, null, 2)}
|
|
433
|
+
|
|
434
|
+
### Last 50 Device Log Lines:
|
|
435
|
+
${context.deviceLogs.join('\n')}
|
|
436
|
+
|
|
437
|
+
### Task:
|
|
438
|
+
1. Identify if it was a functional bug (app issue), a flaky selector, a system dialog, or an infrastructure failure.
|
|
439
|
+
2. If a screenshot is provided, look for visual clues (e.g., error popups, ANR, crash dialogs).
|
|
440
|
+
3. Provide a concise summary (max 3 sentences) starting with "Root Cause:".
|
|
441
|
+
4. Suggest a specific fix.
|
|
442
|
+
|
|
443
|
+
Formatting: Use Markdown.
|
|
444
|
+
`.trim();
|
|
445
|
+
}
|
|
446
|
+
getScreenshotBase64(screenshotPath) {
|
|
447
|
+
if (!screenshotPath)
|
|
448
|
+
return null;
|
|
449
|
+
const fullPath = path.isAbsolute(screenshotPath)
|
|
450
|
+
? screenshotPath
|
|
451
|
+
: path.join(config_1.config.sessionAssetsPath, screenshotPath);
|
|
452
|
+
if (!fs.existsSync(fullPath)) {
|
|
453
|
+
logger_1.default.warn(`[AIService] Screenshot not found at ${fullPath}`);
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
return fs.readFileSync(fullPath).toString('base64');
|
|
458
|
+
}
|
|
459
|
+
catch (err) {
|
|
460
|
+
logger_1.default.warn(`[AIService] Failed to read screenshot: ${err.message}`);
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
exports.AIService = AIService;
|
|
466
|
+
exports.AI_SERVICE = new AIService();
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
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;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
9
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
10
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
11
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
12
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
13
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
14
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.CleanupService = void 0;
|
|
22
|
+
const prisma_1 = require("../prisma");
|
|
23
|
+
const typedi_1 = require("typedi");
|
|
24
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
25
|
+
const fs_1 = __importDefault(require("fs"));
|
|
26
|
+
let CleanupService = class CleanupService {
|
|
27
|
+
constructor() {
|
|
28
|
+
this.log = logger_1.default.scope('CleanupService');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Orchestrates the build and session cleanup based on retention policy.
|
|
32
|
+
*/
|
|
33
|
+
runCleanup(pluginArgs) {
|
|
34
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
35
|
+
const { buildCleanupDays = 30, buildCleanupMaxCount = 100, deleteBuildAssets = true, } = pluginArgs;
|
|
36
|
+
this.log.info(`Starting cleanup: Retention = ${buildCleanupDays} days, Max Builds = ${buildCleanupMaxCount}, Purge Assets = ${deleteBuildAssets}`);
|
|
37
|
+
try {
|
|
38
|
+
const buildIdsToPurge = new Set();
|
|
39
|
+
// 1. Identify builds to delete (by age)
|
|
40
|
+
const expirationDate = new Date();
|
|
41
|
+
expirationDate.setDate(expirationDate.getDate() - buildCleanupDays);
|
|
42
|
+
const buildsByAge = yield prisma_1.prisma.build.findMany({
|
|
43
|
+
where: {
|
|
44
|
+
createdAt: {
|
|
45
|
+
lt: expirationDate,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
select: { id: true },
|
|
49
|
+
});
|
|
50
|
+
buildsByAge.forEach((b) => buildIdsToPurge.add(b.id));
|
|
51
|
+
// 2. Identify builds to delete (by count)
|
|
52
|
+
const totalBuilds = yield prisma_1.prisma.build.count();
|
|
53
|
+
if (totalBuilds > buildCleanupMaxCount) {
|
|
54
|
+
const buildsToKeep = yield prisma_1.prisma.build.findMany({
|
|
55
|
+
orderBy: { createdAt: 'desc' },
|
|
56
|
+
take: buildCleanupMaxCount,
|
|
57
|
+
select: { id: true },
|
|
58
|
+
});
|
|
59
|
+
const keepIds = new Set(buildsToKeep.map((b) => b.id));
|
|
60
|
+
const allBuildIDs = yield prisma_1.prisma.build.findMany({ select: { id: true } });
|
|
61
|
+
for (const b of allBuildIDs) {
|
|
62
|
+
if (!keepIds.has(b.id)) {
|
|
63
|
+
buildIdsToPurge.add(b.id);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (buildIdsToPurge.size === 0) {
|
|
68
|
+
this.log.info('No builds identified for cleanup.');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.log.info(`Identified ${buildIdsToPurge.size} builds for purging.`);
|
|
72
|
+
// 3. Purge sessions and assets for these builds
|
|
73
|
+
for (const buildId of buildIdsToPurge) {
|
|
74
|
+
yield this.purgeBuild(buildId, deleteBuildAssets);
|
|
75
|
+
}
|
|
76
|
+
this.log.info('✅ Build cleanup completed successfully.');
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
this.log.error(`❌ Cleanup failed: ${err.message}`);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Completely purges a build, its sessions, and all associated metadata/assets.
|
|
85
|
+
*/
|
|
86
|
+
purgeBuild(buildId, deleteAssets) {
|
|
87
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
88
|
+
const sessions = yield prisma_1.prisma.session.findMany({
|
|
89
|
+
where: { build_id: buildId },
|
|
90
|
+
include: {
|
|
91
|
+
SessionLog: {
|
|
92
|
+
select: { screenshot: true },
|
|
93
|
+
where: { screenshot: { not: null } },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
for (const session of sessions) {
|
|
98
|
+
if (deleteAssets) {
|
|
99
|
+
// Delete video
|
|
100
|
+
if (session.video_recording) {
|
|
101
|
+
this.unlinkSilently(session.video_recording);
|
|
102
|
+
}
|
|
103
|
+
// Delete screenshots
|
|
104
|
+
for (const logItem of session.SessionLog) {
|
|
105
|
+
if (logItem.screenshot) {
|
|
106
|
+
this.unlinkSilently(logItem.screenshot);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Delete relations
|
|
111
|
+
// Note: We use deleteMany which is efficient.
|
|
112
|
+
yield Promise.all([
|
|
113
|
+
prisma_1.prisma.sessionLog.deleteMany({ where: { session_id: session.id } }),
|
|
114
|
+
prisma_1.prisma.log.deleteMany({ where: { session_id: session.id } }),
|
|
115
|
+
prisma_1.prisma.profiling.deleteMany({ where: { session_id: session.id } }),
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
118
|
+
// Delete sessions for this build
|
|
119
|
+
yield prisma_1.prisma.session.deleteMany({ where: { build_id: buildId } });
|
|
120
|
+
// Finally delete the build
|
|
121
|
+
yield prisma_1.prisma.build.delete({ where: { id: buildId } });
|
|
122
|
+
this.log.debug(`Purged build: ${buildId} (${sessions.length} sessions removed)`);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
unlinkSilently(filePath) {
|
|
126
|
+
try {
|
|
127
|
+
// Check if it's a valid path and exists
|
|
128
|
+
if (filePath && fs_1.default.existsSync(filePath)) {
|
|
129
|
+
fs_1.default.unlinkSync(filePath);
|
|
130
|
+
this.log.debug(`Deleted asset: ${filePath}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
this.log.warn(`Failed to delete asset ${filePath}: ${e.message}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
exports.CleanupService = CleanupService;
|
|
139
|
+
exports.CleanupService = CleanupService = __decorate([
|
|
140
|
+
(0, typedi_1.Service)()
|
|
141
|
+
], CleanupService);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
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;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.EVENT_BUS = exports.EventBus = void 0;
|
|
16
|
+
const typedi_1 = require("typedi");
|
|
17
|
+
const events_1 = require("events");
|
|
18
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
19
|
+
/**
|
|
20
|
+
* Global Event Bus for decoupled communication between services.
|
|
21
|
+
*
|
|
22
|
+
* Supports:
|
|
23
|
+
* - Session events (started, stopped, failed, recovered)
|
|
24
|
+
* - Device events (attached, detached, health_changed)
|
|
25
|
+
* - HTTP events (request, response, error)
|
|
26
|
+
* - System events (startup, shutdown, config_changed)
|
|
27
|
+
*/
|
|
28
|
+
let EventBus = class EventBus {
|
|
29
|
+
constructor() {
|
|
30
|
+
this.log = logger_1.default.scope('EventBus');
|
|
31
|
+
this.emitter = new events_1.EventEmitter();
|
|
32
|
+
this.emitter.setMaxListeners(50); // Increase limit for many subscribers
|
|
33
|
+
this.log.info('Global Event Bus Initialized');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Emit an event to all subscribers
|
|
37
|
+
*/
|
|
38
|
+
emit(event, data) {
|
|
39
|
+
this.log.debug(`[Emit] ${event}`);
|
|
40
|
+
this.emitter.emit(event, data);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Subscribe to an event
|
|
44
|
+
*/
|
|
45
|
+
on(event, listener) {
|
|
46
|
+
this.emitter.on(event, listener);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Subscribe to an event once
|
|
50
|
+
*/
|
|
51
|
+
once(event, listener) {
|
|
52
|
+
this.emitter.once(event, listener);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Unsubscribe from an event
|
|
56
|
+
*/
|
|
57
|
+
off(event, listener) {
|
|
58
|
+
this.emitter.removeListener(event, listener);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Clear all listeners for an event
|
|
62
|
+
*/
|
|
63
|
+
removeAllListeners(event) {
|
|
64
|
+
this.emitter.removeAllListeners(event);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
exports.EventBus = EventBus;
|
|
68
|
+
exports.EventBus = EventBus = __decorate([
|
|
69
|
+
(0, typedi_1.Service)(),
|
|
70
|
+
__metadata("design:paramtypes", [])
|
|
71
|
+
], EventBus);
|
|
72
|
+
// Export singleton
|
|
73
|
+
const typedi_2 = require("typedi");
|
|
74
|
+
exports.EVENT_BUS = typedi_2.Container.get(EventBus);
|