@arcadialdev/arcality 2.2.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/.agents/skills/e2e-testing-expert/SKILL.md +28 -0
- package/.agents/skills/frontend-design/LICENSE.txt +177 -0
- package/.agents/skills/frontend-design/SKILL.md +42 -0
- package/.agents/skills/nodejs-backend-patterns/SKILL.md +639 -0
- package/.agents/skills/nodejs-backend-patterns/references/advanced-patterns.md +430 -0
- package/.agents/skills/playwright-best-practices/LICENSE.md +7 -0
- package/.agents/skills/playwright-best-practices/README.md +147 -0
- package/.agents/skills/playwright-best-practices/SKILL.md +303 -0
- package/.agents/skills/playwright-best-practices/advanced/authentication-flows.md +360 -0
- package/.agents/skills/playwright-best-practices/advanced/authentication.md +871 -0
- package/.agents/skills/playwright-best-practices/advanced/clock-mocking.md +364 -0
- package/.agents/skills/playwright-best-practices/advanced/mobile-testing.md +409 -0
- package/.agents/skills/playwright-best-practices/advanced/multi-context.md +288 -0
- package/.agents/skills/playwright-best-practices/advanced/multi-user.md +393 -0
- package/.agents/skills/playwright-best-practices/advanced/network-advanced.md +452 -0
- package/.agents/skills/playwright-best-practices/advanced/third-party.md +464 -0
- package/.agents/skills/playwright-best-practices/architecture/pom-vs-fixtures.md +363 -0
- package/.agents/skills/playwright-best-practices/architecture/test-architecture.md +369 -0
- package/.agents/skills/playwright-best-practices/architecture/when-to-mock.md +383 -0
- package/.agents/skills/playwright-best-practices/browser-apis/browser-apis.md +391 -0
- package/.agents/skills/playwright-best-practices/browser-apis/iframes.md +403 -0
- package/.agents/skills/playwright-best-practices/browser-apis/service-workers.md +504 -0
- package/.agents/skills/playwright-best-practices/browser-apis/websockets.md +403 -0
- package/.agents/skills/playwright-best-practices/core/annotations.md +424 -0
- package/.agents/skills/playwright-best-practices/core/assertions-waiting.md +361 -0
- package/.agents/skills/playwright-best-practices/core/configuration.md +452 -0
- package/.agents/skills/playwright-best-practices/core/fixtures-hooks.md +417 -0
- package/.agents/skills/playwright-best-practices/core/global-setup.md +434 -0
- package/.agents/skills/playwright-best-practices/core/locators.md +242 -0
- package/.agents/skills/playwright-best-practices/core/page-object-model.md +315 -0
- package/.agents/skills/playwright-best-practices/core/projects-dependencies.md +453 -0
- package/.agents/skills/playwright-best-practices/core/test-data.md +492 -0
- package/.agents/skills/playwright-best-practices/core/test-suite-structure.md +361 -0
- package/.agents/skills/playwright-best-practices/core/test-tags.md +298 -0
- package/.agents/skills/playwright-best-practices/debugging/console-errors.md +420 -0
- package/.agents/skills/playwright-best-practices/debugging/debugging.md +504 -0
- package/.agents/skills/playwright-best-practices/debugging/error-testing.md +360 -0
- package/.agents/skills/playwright-best-practices/debugging/flaky-tests.md +496 -0
- package/.agents/skills/playwright-best-practices/frameworks/angular.md +530 -0
- package/.agents/skills/playwright-best-practices/frameworks/nextjs.md +469 -0
- package/.agents/skills/playwright-best-practices/frameworks/react.md +531 -0
- package/.agents/skills/playwright-best-practices/frameworks/vue.md +574 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/ci-cd.md +468 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/docker.md +283 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/github-actions.md +546 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/gitlab.md +397 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/other-providers.md +521 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/parallel-sharding.md +371 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/performance.md +453 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/reporting.md +424 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/test-coverage.md +497 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/accessibility.md +359 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/api-testing.md +719 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/browser-extensions.md +506 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/canvas-webgl.md +493 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/component-testing.md +500 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/drag-drop.md +576 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/electron.md +509 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/file-operations.md +377 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/file-upload-download.md +562 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/forms-validation.md +561 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/graphql-testing.md +331 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/i18n.md +508 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/performance-testing.md +476 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/security-testing.md +430 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/visual-regression.md +634 -0
- package/.env.example +21 -0
- package/README.md +30 -0
- package/bin/arcality.mjs +86 -0
- package/package.json +66 -0
- package/playwright.config.ts +12 -0
- package/scripts/cleanup-qmsdev.mjs +63 -0
- package/scripts/discover-view.mjs +52 -0
- package/scripts/extract-view.mjs +64 -0
- package/scripts/gen-and-run.mjs +838 -0
- package/scripts/init.mjs +290 -0
- package/scripts/migrate-to-central-out.mjs +157 -0
- package/scripts/postinstall.mjs +63 -0
- package/scripts/rebrand-report.mjs +241 -0
- package/scripts/setup.mjs +166 -0
- package/src/KnowledgeService.ts +239 -0
- package/src/arcalityClient.mjs +266 -0
- package/src/configLoader.mjs +179 -0
- package/src/configManager.mjs +172 -0
- package/src/consoleBanner.ts +32 -0
- package/src/envSetup.ts +205 -0
- package/src/index.ts +25 -0
- package/src/projectInspector.ts +42 -0
- package/src/services/collectiveMemoryService.ts +178 -0
- package/src/testRunner.ts +201 -0
- package/tests/_helpers/ArcalityReporter.ts +490 -0
- package/tests/_helpers/agentic-runner.spec.ts +741 -0
- package/tests/_helpers/ai-agent-helper.ts +1573 -0
- package/tests/_helpers/discover-view.spec.ts +238 -0
- package/tests/_helpers/extract-view.spec.ts +118 -0
- package/tests/_helpers/qa-tools.ts +333 -0
- package/tests/_helpers/smart-action.spec.ts +1458 -0
|
@@ -0,0 +1,1573 @@
|
|
|
1
|
+
import { Page, TestInfo } from '@playwright/test';
|
|
2
|
+
import { qaAdvancedTools } from './qa-tools';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import * as crypto from 'crypto';
|
|
7
|
+
import { KnowledgeService, PortalIngestRequest, PortalContextResponse } from '../../src/KnowledgeService';
|
|
8
|
+
import { pushField, getKnownFieldIdentifiers } from '../../src/services/collectiveMemoryService';
|
|
9
|
+
|
|
10
|
+
// Caché por sesión: fields ya enviados en esta ejecución del test (no persistente, solo RAM)
|
|
11
|
+
const _sessionFieldCache = new Set<string>();
|
|
12
|
+
|
|
13
|
+
export interface PageState {
|
|
14
|
+
url: string;
|
|
15
|
+
title: string;
|
|
16
|
+
components: any[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SingleAction {
|
|
20
|
+
idx?: number;
|
|
21
|
+
action: 'click' | 'double_click' | 'fill' | 'select' | 'wait';
|
|
22
|
+
selector?: string;
|
|
23
|
+
value?: string;
|
|
24
|
+
frameIdx?: number;
|
|
25
|
+
description?: string; // Nombre del componente
|
|
26
|
+
type?: string; // Tipo del componente (FIELD, ACTION, etc)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AgentAction {
|
|
30
|
+
thought: string;
|
|
31
|
+
actions: SingleAction[];
|
|
32
|
+
finish?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* AIAgentHelper: Gestiona percepción (DOM + Visión) y el motor cognitivo de Arcality.
|
|
37
|
+
*/
|
|
38
|
+
export class AIAgentHelper {
|
|
39
|
+
private page: Page;
|
|
40
|
+
private contextDir: string;
|
|
41
|
+
private testInfo?: TestInfo;
|
|
42
|
+
private knowledgeService: KnowledgeService;
|
|
43
|
+
|
|
44
|
+
private lastScreenshot: string | null = null;
|
|
45
|
+
private logs: string[] = [];
|
|
46
|
+
private checkpoints: Map<string, { url: string; storageState: any; localStorage: Record<string, string>; sessionStorage: Record<string, string> }> = new Map();
|
|
47
|
+
private sessionStorage: Map<string, string> = new Map();
|
|
48
|
+
|
|
49
|
+
constructor(page: Page, contextDir: string = 'out', testInfo?: TestInfo, knowledgeService?: KnowledgeService) {
|
|
50
|
+
this.page = page;
|
|
51
|
+
this.contextDir = contextDir;
|
|
52
|
+
this.testInfo = testInfo;
|
|
53
|
+
this.knowledgeService = knowledgeService || KnowledgeService.getInstance();
|
|
54
|
+
|
|
55
|
+
// Track console logs and network errors for QA context
|
|
56
|
+
this.page.on('console', msg => {
|
|
57
|
+
if (msg.type() === 'error' || msg.type() === 'warning') {
|
|
58
|
+
this.logs.push(`[${msg.type().toUpperCase()}] ${msg.text()}`);
|
|
59
|
+
if (this.logs.length > 30) this.logs.shift();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
this.page.on('requestfailed', request => {
|
|
64
|
+
const failure = request.failure();
|
|
65
|
+
this.logs.push(`[NETWORK_ERROR] ${request.method()} ${request.url()} - ${failure?.errorText || 'Unknown error'}`);
|
|
66
|
+
if (this.logs.length > 30) this.logs.shift();
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private loadSkills(): string {
|
|
71
|
+
try {
|
|
72
|
+
let skillsContext = "";
|
|
73
|
+
let loadedCount = 0;
|
|
74
|
+
|
|
75
|
+
const toolsRoot = process.env.ARCALITY_ROOT || path.join(__dirname, '..', '..');
|
|
76
|
+
const possibleDirs = [
|
|
77
|
+
path.join(toolsRoot, '.agent', 'skills'),
|
|
78
|
+
path.join(toolsRoot, '.agents', 'skills')
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
for (const rootDir of possibleDirs) {
|
|
82
|
+
if (!fs.existsSync(rootDir)) continue;
|
|
83
|
+
|
|
84
|
+
if (loadedCount === 0) {
|
|
85
|
+
skillsContext += "\n🛠️ SKILLS INSTALADAS (Metodologías activas):\n";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
|
89
|
+
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
let content = "";
|
|
92
|
+
let skillName = entry.name;
|
|
93
|
+
|
|
94
|
+
if (entry.isDirectory()) {
|
|
95
|
+
// Look for SKILL.md in the subdirectory
|
|
96
|
+
const skillPath = path.join(rootDir, entry.name, 'SKILL.md');
|
|
97
|
+
if (fs.existsSync(skillPath)) {
|
|
98
|
+
content = fs.readFileSync(skillPath, 'utf8');
|
|
99
|
+
}
|
|
100
|
+
} else if (entry.name.endsWith('.md')) {
|
|
101
|
+
// Flat .md file at the root of skills/
|
|
102
|
+
const skillPath = path.join(rootDir, entry.name);
|
|
103
|
+
content = fs.readFileSync(skillPath, 'utf8');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (content) {
|
|
107
|
+
skillsContext += `\n--- SKILL: ${skillName} ---\n${content}\n`;
|
|
108
|
+
loadedCount++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (loadedCount > 0) {
|
|
114
|
+
console.log(`>> 📑 ${loadedCount} habilidades QA inyectadas al cerebro.`);
|
|
115
|
+
return skillsContext;
|
|
116
|
+
}
|
|
117
|
+
} catch (e: any) {
|
|
118
|
+
console.warn(`>>ARCALITY_STATUS>> ⚠️ Error cargando habilidades: ${e.message}`);
|
|
119
|
+
}
|
|
120
|
+
return "";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private loadMemory(prompt: string): string {
|
|
124
|
+
try {
|
|
125
|
+
const memoryFile = path.join(this.contextDir, 'memoria-agente.json');
|
|
126
|
+
if (fs.existsSync(memoryFile)) {
|
|
127
|
+
const memories = JSON.parse(fs.readFileSync(memoryFile, 'utf8'));
|
|
128
|
+
|
|
129
|
+
// Extraer palabras clave del prompt actual
|
|
130
|
+
const keywords = prompt.toLowerCase().split(' ').filter(w => w.length > 4);
|
|
131
|
+
|
|
132
|
+
// Filtrar solo memorias EXITOSAS y SEMÁNTICAMENTE RELEVANTES
|
|
133
|
+
const relevantSuccess = memories
|
|
134
|
+
.filter((m: any) => {
|
|
135
|
+
if (!m.success) return false;
|
|
136
|
+
const memPrompt = (m.prompt || '').toLowerCase();
|
|
137
|
+
// Al menos 3 palabras clave deben coincidir o ser muy similares
|
|
138
|
+
const matches = keywords.filter(kw => memPrompt.includes(kw)).length;
|
|
139
|
+
return matches >= 3 || (keywords.length > 0 && matches === keywords.length);
|
|
140
|
+
})
|
|
141
|
+
.slice(-1); // Solo el MÁS reciente
|
|
142
|
+
|
|
143
|
+
const relevantFailures = memories
|
|
144
|
+
.filter((m: any) => !m.success && m.error)
|
|
145
|
+
.slice(-1); // Solo el más reciente
|
|
146
|
+
|
|
147
|
+
let memoryContext = "";
|
|
148
|
+
if (relevantSuccess.length) {
|
|
149
|
+
const best = relevantSuccess[0];
|
|
150
|
+
memoryContext += `\n📚 SUCCESS GUIDANCE FOR THIS MISSION:
|
|
151
|
+
The following steps led to SUCCESS for a similar mission ("${best.prompt}"):
|
|
152
|
+
${best.steps.slice(0, 15).map((s: any) => ` - ${s}`).join('\n')}
|
|
153
|
+
(Use these steps as a roadmap if the current UI allows it!)
|
|
154
|
+
`;
|
|
155
|
+
}
|
|
156
|
+
if (relevantFailures.length) {
|
|
157
|
+
memoryContext += `\n⚠️ PREVIOUS FAILURES TO AVOID:\n${relevantFailures.map((m: any) => `- ${m.prompt}`).join('\n')}\n`;
|
|
158
|
+
}
|
|
159
|
+
return memoryContext;
|
|
160
|
+
}
|
|
161
|
+
} catch { }
|
|
162
|
+
return "";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async getPageState(): Promise<PageState> {
|
|
166
|
+
if (this.page.isClosed()) return { url: '', title: '', components: [] };
|
|
167
|
+
return this.getPageStateRefined();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private async getPageStateRefined(): Promise<PageState> {
|
|
171
|
+
const frames = this.page.frames();
|
|
172
|
+
let globalIdx = 0;
|
|
173
|
+
const allComponents: any[] = [];
|
|
174
|
+
|
|
175
|
+
for (let fIdx = 0; fIdx < frames.length; fIdx++) {
|
|
176
|
+
const frame = frames[fIdx];
|
|
177
|
+
if (frame.isDetached()) continue;
|
|
178
|
+
try {
|
|
179
|
+
const results = await frame.evaluate(({ startIdx }) => {
|
|
180
|
+
const isVisible = (el: HTMLElement) => {
|
|
181
|
+
const style = window.getComputedStyle(el);
|
|
182
|
+
const rect = el.getBoundingClientRect();
|
|
183
|
+
const hasSize = rect.width > 1 && rect.height > 1;
|
|
184
|
+
const isNotTranslucent = parseFloat(style.opacity || '1') > 0.05;
|
|
185
|
+
const isCssVisible = style.display !== 'none' && style.visibility !== 'hidden';
|
|
186
|
+
return isCssVisible && isNotTranslucent && hasSize;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const nodes = Array.from(document.querySelectorAll('button, input, select, textarea, [role], a, h1, h2, label, span, p, i, svg, img, [class*="alert"],[class*="toast"],[class*="message"],[class*="error"],[class*="success"],[id*="alert"],[id*="toast"],[role="alert"],[role="status"],.error-message,.success-message'));
|
|
190
|
+
let localCount = 0;
|
|
191
|
+
|
|
192
|
+
return nodes
|
|
193
|
+
.filter(el => {
|
|
194
|
+
const htmlEl = el as HTMLElement;
|
|
195
|
+
// SPECIAL RULE: File inputs are often invisible but interactable via Playwright.
|
|
196
|
+
if (htmlEl.tagName.toLowerCase() === 'input' && (htmlEl as HTMLInputElement).type === 'file') {
|
|
197
|
+
return window.getComputedStyle(htmlEl).display !== 'none';
|
|
198
|
+
}
|
|
199
|
+
return isVisible(htmlEl);
|
|
200
|
+
})
|
|
201
|
+
.map(el => {
|
|
202
|
+
const htmlEl = el as HTMLElement;
|
|
203
|
+
const tag = htmlEl.tagName.toLowerCase();
|
|
204
|
+
const role = htmlEl.getAttribute('role') || '';
|
|
205
|
+
const text = (htmlEl.innerText || htmlEl.getAttribute('aria-label') || htmlEl.getAttribute('title') || '').trim().replace(/\s+/g, ' ');
|
|
206
|
+
|
|
207
|
+
const textLower = text.toLowerCase();
|
|
208
|
+
// Semantic boost for critical feedback
|
|
209
|
+
const isCriticalFailure = (textLower.includes('error') || textLower.includes('falló') || textLower.includes('no se puede') || textLower.includes('ya existe') || textLower.includes('existe') || textLower.includes('inválido') || textLower.includes('incorrecto') || textLower.includes('ligado') || textLower.includes('depende') || textLower.includes('duplicado') || textLower.includes('repetido')) && (text.length < 200);
|
|
210
|
+
const isCriticalSuccess = (textLower.includes('exitosamente') || textLower.includes('guardado') || textLower.includes('creado') || textLower.includes('success') || (textLower.includes('correctamente') && text.length < 100)) && (text.length < 200);
|
|
211
|
+
const isCriticalFeedback = isCriticalFailure || isCriticalSuccess;
|
|
212
|
+
|
|
213
|
+
if (!isCriticalFeedback && (tag === 'span' || tag === 'p' || tag === 'label' || tag === 'i' || tag === 'svg' || tag === 'img' || tag === 'div')) {
|
|
214
|
+
const hasIdentification = htmlEl.hasAttribute('aria-label') || htmlEl.hasAttribute('title') || role || htmlEl.id;
|
|
215
|
+
// Filtrar contenedores gigantes de texto que confunden a la IA (Blobs de ruido)
|
|
216
|
+
if (text.length > 300 && !isCriticalFeedback && !hasIdentification) return null;
|
|
217
|
+
if (text.length < 2 && !hasIdentification && tag !== 'svg' && tag !== 'img' && tag !== 'i') return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const idx = startIdx + localCount++;
|
|
221
|
+
htmlEl.setAttribute('agent-idx', idx.toString());
|
|
222
|
+
|
|
223
|
+
let name = text || htmlEl.getAttribute('aria-label') || htmlEl.getAttribute('title') || htmlEl.getAttribute('placeholder');
|
|
224
|
+
|
|
225
|
+
// Contextual search for unnamed elements (Icons, Action buttons)
|
|
226
|
+
if (!name) {
|
|
227
|
+
// 1. Proactive Icon detection (Move this UP)
|
|
228
|
+
const iconMatch = htmlEl.outerHTML.match(/fa-([a-z0-9-]+)|md-([a-z0-9-]+)|bi-([a-z0-9-]+)/i);
|
|
229
|
+
if (iconMatch) {
|
|
230
|
+
name = `Icon:${iconMatch[1] || iconMatch[2] || iconMatch[3]}`;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const htmlLower = htmlEl.outerHTML.toLowerCase();
|
|
235
|
+
const classLower = (htmlEl.className || '').toString().toLowerCase();
|
|
236
|
+
|
|
237
|
+
// Semantic Action Detection
|
|
238
|
+
const isDeleteAction = htmlLower.includes('trash') || htmlLower.includes('delete') || htmlLower.includes('eliminar') || htmlLower.includes('remove');
|
|
239
|
+
const isEditAction = htmlLower.includes('edit') || htmlLower.includes('pencil') || htmlLower.includes('modify') || htmlLower.includes('editar') || htmlLower.includes('modificar') || htmlLower.includes('pen');
|
|
240
|
+
|
|
241
|
+
if (isDeleteAction && (!name || name.length < 3)) name = '[INFERRED] Eliminar';
|
|
242
|
+
if (isEditAction && (!name || name.length < 3)) name = '[INFERRED] Editar';
|
|
243
|
+
|
|
244
|
+
let type = 'INFO';
|
|
245
|
+
const isErrorClass = classLower.includes('error') || classLower.includes('danger') || classLower.includes('fail');
|
|
246
|
+
// Strict constraint: to be considered an error strictly by CSS class, it must have > 2 words (a real message) or not be a label.
|
|
247
|
+
const isError = isCriticalFailure || (isErrorClass && text.split(' ').length > 2);
|
|
248
|
+
const isSuccess = classLower.includes('success') || isCriticalSuccess || textLower.includes('éxito');
|
|
249
|
+
|
|
250
|
+
const isTab = role === 'tab' || classLower.includes('tab') || classLower.includes('pestaña');
|
|
251
|
+
|
|
252
|
+
if (tag === 'input' || tag === 'textarea' || tag === 'select') type = 'FIELD';
|
|
253
|
+
else if (tag === 'button' || role === 'button' || tag === 'a' || role === 'link' || isTab) type = 'ACTION';
|
|
254
|
+
else if (role === 'option') type = 'OPTION';
|
|
255
|
+
else if (role === 'combobox' || htmlEl.hasAttribute('aria-expanded')) type = 'DROPDOWN';
|
|
256
|
+
else if (isError || classLower.includes('alert') || classLower.includes('toast') || role === 'status' || role === 'alert' || isCriticalFeedback) {
|
|
257
|
+
type = isSuccess ? 'SUCCESS_MESSAGE' : (isError ? 'ERROR_MESSAGE' : 'MESSAGE');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (type.includes('MESSAGE')) {
|
|
261
|
+
const prefix = isError ? '🛑 [ERROR_CRÍTICO] ' : (isSuccess ? '✨ [ÉXITO_DETECTADO] ' : '[INFO] ');
|
|
262
|
+
name = prefix + (name || text || 'Mensaje del sistema');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Add tag context and row context for small names
|
|
266
|
+
if (type === 'ACTION' || type === 'FIELD') {
|
|
267
|
+
name = `[${tag}] ${name || ''}`.trim();
|
|
268
|
+
if (!name || name.length < 10) {
|
|
269
|
+
const row = htmlEl.closest('tr');
|
|
270
|
+
if (row) {
|
|
271
|
+
const rowText = (row.innerText || '').split('\n')[0].substring(0, 20).trim();
|
|
272
|
+
if (rowText) name += ` (${rowText})`;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!name) name = tag;
|
|
278
|
+
|
|
279
|
+
let score = 10;
|
|
280
|
+
if (type === 'MESSAGE') score = 200; // Prioridad máxima para leer feedback
|
|
281
|
+
else if (type === 'FIELD') score = 100;
|
|
282
|
+
else if (type === 'OPTION') score = 90;
|
|
283
|
+
else if (type === 'ACTION') score = 80;
|
|
284
|
+
else if (type === 'DROPDOWN') score = 70;
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
idx,
|
|
288
|
+
type,
|
|
289
|
+
name: name,
|
|
290
|
+
value: (htmlEl as any).value || '',
|
|
291
|
+
frameIdx: 0, // Placeholder
|
|
292
|
+
selector: `[agent-idx="${idx}"]`,
|
|
293
|
+
score: score
|
|
294
|
+
};
|
|
295
|
+
}).filter(Boolean);
|
|
296
|
+
}, { startIdx: globalIdx });
|
|
297
|
+
|
|
298
|
+
results.forEach((c: any) => {
|
|
299
|
+
c.frameIdx = fIdx;
|
|
300
|
+
allComponents.push(c);
|
|
301
|
+
});
|
|
302
|
+
globalIdx += results.length;
|
|
303
|
+
} catch { }
|
|
304
|
+
}
|
|
305
|
+
console.log(`>>ARCALITY_STATUS>> 🔍 Percepción profunda encontró ${allComponents.length} componentes`);
|
|
306
|
+
|
|
307
|
+
const sortedComponents = allComponents.sort((a, b) => b.score - a.score).slice(0, 500);
|
|
308
|
+
const title = await this.page.title();
|
|
309
|
+
const url = this.page.url();
|
|
310
|
+
|
|
311
|
+
// FLUJO DE INGESTA (KnowledgeService Hook)
|
|
312
|
+
try {
|
|
313
|
+
const domHash = this.knowledgeService.calculateDomHash(sortedComponents);
|
|
314
|
+
const viewport = this.page.viewportSize() || { width: 1280, height: 720 };
|
|
315
|
+
const projectId = this.knowledgeService.getProjectId() || process.env.ARCALITY_PROJECT_ID || '';
|
|
316
|
+
|
|
317
|
+
if (projectId) {
|
|
318
|
+
const requestPayload: PortalIngestRequest = {
|
|
319
|
+
project_id: projectId,
|
|
320
|
+
target_url: url,
|
|
321
|
+
page_title: title,
|
|
322
|
+
dom_hash: domHash,
|
|
323
|
+
viewport: { width: viewport.width, height: viewport.height },
|
|
324
|
+
components: sortedComponents.map(c => {
|
|
325
|
+
let compType: 'ACTION' | 'FIELD' | 'INFO' | 'NAVIGATION' | 'DATA' = 'INFO';
|
|
326
|
+
if (c.type === 'ACTION') compType = 'ACTION';
|
|
327
|
+
else if (c.type === 'FIELD' || c.type === 'OPTION' || c.type === 'DROPDOWN') compType = 'FIELD';
|
|
328
|
+
else if (c.name?.toLowerCase().includes('nav') || c.type === 'ACTION') compType = 'NAVIGATION';
|
|
329
|
+
return {
|
|
330
|
+
tag: c.selector?.split(']')[0]?.replace('[', '') || 'div',
|
|
331
|
+
component_type: compType,
|
|
332
|
+
semantic_label: c.name,
|
|
333
|
+
text_content: c.value,
|
|
334
|
+
is_interactive: compType === 'ACTION' || compType === 'FIELD',
|
|
335
|
+
attributes: { idx: c.idx, originalType: c.type },
|
|
336
|
+
bounding_rect: (c as any).rect || { x: 0, y: 0, width: 0, height: 0 }
|
|
337
|
+
};
|
|
338
|
+
})
|
|
339
|
+
};
|
|
340
|
+
await this.knowledgeService.ingest(requestPayload);
|
|
341
|
+
|
|
342
|
+
// HOOK: Catalogar campos de formulario en Memoria Colectiva (fire-and-forget, sin bloqueo)
|
|
343
|
+
try {
|
|
344
|
+
const currentPath = new URL(url).pathname;
|
|
345
|
+
|
|
346
|
+
const fieldComponents = sortedComponents.filter(
|
|
347
|
+
c => c.type === 'FIELD' || c.type === 'OPTION' || c.type === 'DROPDOWN'
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
// Solo obtener contexto si hay campos nuevos que enviar (para no hacer fetch inútil)
|
|
351
|
+
const newFields = fieldComponents.filter(fc => {
|
|
352
|
+
const identifier = (fc.name || fc.value || '').trim();
|
|
353
|
+
return identifier.length >= 2 && !_sessionFieldCache.has(identifier);
|
|
354
|
+
}).slice(0, 5); // Máximo 5 campos nuevos por percepción
|
|
355
|
+
|
|
356
|
+
if (newFields.length > 0) {
|
|
357
|
+
// Obtener existentes del servidor (una sola llamada por percepción)
|
|
358
|
+
const existingFieldIds = await getKnownFieldIdentifiers(currentPath);
|
|
359
|
+
|
|
360
|
+
for (const fc of newFields) {
|
|
361
|
+
const identifier = (fc.name || fc.value || '').trim();
|
|
362
|
+
|
|
363
|
+
// Marcar como enviado en esta sesión (aunque falle, evitamos re-intentos infinitos)
|
|
364
|
+
_sessionFieldCache.add(identifier);
|
|
365
|
+
|
|
366
|
+
const selectorLower = (fc.selector || '').toLowerCase();
|
|
367
|
+
const nameLower = identifier.toLowerCase();
|
|
368
|
+
let fieldType: string = 'text';
|
|
369
|
+
if (nameLower.includes('email') || selectorLower.includes('email')) fieldType = 'email';
|
|
370
|
+
else if (nameLower.includes('password') || selectorLower.includes('password')) fieldType = 'password';
|
|
371
|
+
else if (fc.type === 'DROPDOWN' || selectorLower.includes('select')) fieldType = 'select';
|
|
372
|
+
else if (selectorLower.includes('textarea')) fieldType = 'textarea';
|
|
373
|
+
else if (selectorLower.includes('checkbox')) fieldType = 'checkbox';
|
|
374
|
+
else if (selectorLower.includes('date')) fieldType = 'date';
|
|
375
|
+
else if (selectorLower.includes('number')) fieldType = 'number';
|
|
376
|
+
|
|
377
|
+
// FIRE-AND-FORGET: NO await, nunca bloquea la percepción
|
|
378
|
+
pushField({
|
|
379
|
+
field_identifier: identifier.substring(0, 100),
|
|
380
|
+
field_type: fieldType,
|
|
381
|
+
is_required: false,
|
|
382
|
+
existingFieldIds
|
|
383
|
+
}).catch(() => { /* silencioso */ });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
} catch { /* Silencioso — no interrumpir el runner */ }
|
|
387
|
+
}
|
|
388
|
+
} catch (e: any) {
|
|
389
|
+
console.log(`>>ARCALITY_STATUS>> ⚠️ Error en hook de ingesta: ${e.message}`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
url,
|
|
394
|
+
title,
|
|
395
|
+
components: sortedComponents
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Busca una acción sugerida en la memoria SIN usar tokens.
|
|
401
|
+
* Compara URL, componentes visuales Y palabras clave del prompt.
|
|
402
|
+
*/
|
|
403
|
+
async getActionFromGuia(prompt: string, historyLength: number): Promise<AgentAction | null> {
|
|
404
|
+
try {
|
|
405
|
+
const fs = require('fs');
|
|
406
|
+
const path = require('path');
|
|
407
|
+
const memoryFile = path.join(this.contextDir, 'memoria-agente.json');
|
|
408
|
+
|
|
409
|
+
if (!fs.existsSync(memoryFile)) return null;
|
|
410
|
+
|
|
411
|
+
const memories = JSON.parse(fs.readFileSync(memoryFile, 'utf8'));
|
|
412
|
+
const state = await this.getPageState();
|
|
413
|
+
|
|
414
|
+
// Estrategia: Solo usar pasos de misiones COMPLETAMENTE EXITOSAS y en la MISMA URL
|
|
415
|
+
const currentUrl = this.page.url();
|
|
416
|
+
|
|
417
|
+
// NUEVA VALIDACIÓN: Extraer palabras clave críticas del prompt
|
|
418
|
+
const extractCriticalKeywords = (text: string): Set<string> => {
|
|
419
|
+
const lower = text.toLowerCase();
|
|
420
|
+
const keywords = new Set<string>();
|
|
421
|
+
|
|
422
|
+
// Palabras de posición (CRÍTICAS)
|
|
423
|
+
if (lower.includes('primera')) keywords.add('primera');
|
|
424
|
+
if (lower.includes('segunda')) keywords.add('segunda');
|
|
425
|
+
if (lower.includes('tercera')) keywords.add('tercera');
|
|
426
|
+
if (lower.includes('cuarta')) keywords.add('cuarta');
|
|
427
|
+
if (lower.includes('última')) keywords.add('última');
|
|
428
|
+
|
|
429
|
+
// Números ordinales en inglés (si el agente piensa en inglés)
|
|
430
|
+
if (lower.includes('first')) keywords.add('primera');
|
|
431
|
+
if (lower.includes('second')) keywords.add('segunda');
|
|
432
|
+
if (lower.includes('third')) keywords.add('tercera');
|
|
433
|
+
if (lower.includes('fourth')) keywords.add('cuarta');
|
|
434
|
+
if (lower.includes('last')) keywords.add('última');
|
|
435
|
+
|
|
436
|
+
return keywords;
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// Estrategia SEMÁNTICA:
|
|
440
|
+
// 1. Buscamos misiones exitosas previas que compartan la intención (verbos y sustantivos clave)
|
|
441
|
+
const currentKeywords = extractCriticalKeywords(prompt);
|
|
442
|
+
const currentVerbs = ['elimina', 'borra', 'quita', 'delete', 'remove', 'crea', 'nueva', 'añade', 'add', 'create', 'new', 'edita', 'modifica', 'cambia', 'edit', 'update'].filter(v => prompt.toLowerCase().includes(v));
|
|
443
|
+
|
|
444
|
+
const matchingRun = memories
|
|
445
|
+
.filter((m: any) => m.success && m.steps_data && m.steps_data[historyLength])
|
|
446
|
+
.reverse()
|
|
447
|
+
.find((m: any) => {
|
|
448
|
+
const memPrompt = (m.prompt || '').toLowerCase();
|
|
449
|
+
const currentPrompt = prompt.toLowerCase();
|
|
450
|
+
|
|
451
|
+
// VALIDACIÓN DE SIMILITUD DE PROMPT (70%+)
|
|
452
|
+
// Aumentamos el umbral para evitar que misiones de "verificación" se confundan con "creación"
|
|
453
|
+
const words = currentPrompt.split(' ').filter(w => w.length > 3);
|
|
454
|
+
if (words.length === 0) return false;
|
|
455
|
+
const matches = words.filter(w => memPrompt.includes(w)).length;
|
|
456
|
+
if (matches / words.length < 0.7) return false;
|
|
457
|
+
|
|
458
|
+
// VALIDACIÓN DE INTENCIÓN (Verbos)
|
|
459
|
+
// Si el prompt actual tiene verbos de "eliminar" y la memoria es de "crear", NO COINCIDEN.
|
|
460
|
+
const memVerbs = ['elimina', 'borra', 'quita', 'delete', 'remove', 'crea', 'nueva', 'añade', 'add', 'create', 'new', 'edita', 'modifica', 'cambia', 'edit', 'update'].filter(v => memPrompt.includes(v));
|
|
461
|
+
|
|
462
|
+
const hasSharedVerb = currentVerbs.some(cv => memVerbs.includes(cv));
|
|
463
|
+
const hasOppositeIntent = (currentVerbs.some(v => ['elimina', 'borra', 'delete'].includes(v)) && memVerbs.some(v => ['crea', 'nueva', 'create', 'new'].includes(v))) ||
|
|
464
|
+
(currentVerbs.some(v => ['crea', 'nueva', 'create', 'new'].includes(v)) && memVerbs.some(v => ['elimina', 'borra', 'delete'].includes(v)));
|
|
465
|
+
|
|
466
|
+
if (hasOppositeIntent) return false;
|
|
467
|
+
if (!hasSharedVerb && currentVerbs.length > 0) return false;
|
|
468
|
+
|
|
469
|
+
// Si la misión actual NO tiene verbos pero la de memoria SÍ, probablemente son misiones distintas
|
|
470
|
+
if (currentVerbs.length === 0 && memVerbs.length > 0) return false;
|
|
471
|
+
|
|
472
|
+
// VALIDACIÓN DE POSICIÓN/ORDEN
|
|
473
|
+
const memKeywords = extractCriticalKeywords(m.prompt || '');
|
|
474
|
+
for (const kw of currentKeywords) {
|
|
475
|
+
if (!memKeywords.has(kw)) return false;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const memStep = m.steps_data[historyLength];
|
|
479
|
+
if (!memStep) return false;
|
|
480
|
+
|
|
481
|
+
const memUrl = (memStep.url || '').split('?')[0];
|
|
482
|
+
const currUrl = currentUrl.split('?')[0];
|
|
483
|
+
|
|
484
|
+
// URL Validation
|
|
485
|
+
if (memUrl !== currUrl) return false;
|
|
486
|
+
|
|
487
|
+
// FUZZY MATCHING: Find if any current component matches the remembered one
|
|
488
|
+
const foundComponent = state.components.find(c => {
|
|
489
|
+
const cleanMemName = (memStep.componentName || '').replace(/\[.*?\]/g, '').replace(/\(.*?\)/g, '').trim().toLowerCase();
|
|
490
|
+
const cleanCurrName = (c.name || '').replace(/\[.*?\]/g, '').replace(/\(.*?\)/g, '').trim().toLowerCase();
|
|
491
|
+
return (c.selector === memStep.selector) ||
|
|
492
|
+
(cleanMemName !== '' && (cleanMemName === cleanCurrName || cleanCurrName.includes(cleanMemName)));
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
return !!foundComponent;
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
if (matchingRun) {
|
|
499
|
+
const memStep = matchingRun.steps_data[historyLength];
|
|
500
|
+
|
|
501
|
+
// GUARD: Reject guide steps with unreliable component names
|
|
502
|
+
const memCompName = memStep.componentName || '';
|
|
503
|
+
if (memCompName.length > 80) {
|
|
504
|
+
console.log(`>>ARCALITY_STATUS>> ⚠️ Guía rechazada: Nombre de componente demasiado largo (${memCompName.length} chars). Delegando a IA.`);
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
if (memCompName.includes('[INFERRED]')) {
|
|
508
|
+
console.log(`>>ARCALITY_STATUS>> ⚠️ Guía rechazada: Componente con nombre inferido "${memCompName}". Delegando a IA.`);
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
if (!memCompName || memCompName.length < 3) {
|
|
512
|
+
console.log(`>>ARCALITY_STATUS>> ⚠️ Guía rechazada: Componente sin nombre claro. Delegando a IA.`);
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const foundComponent = state.components.find(c => {
|
|
517
|
+
const cleanMemName = (memStep.componentName || '').replace(/\[.*?\]/g, '').replace(/\(.*?\)/g, '').trim().toLowerCase();
|
|
518
|
+
const cleanCurrName = (c.name || '').replace(/\[.*?\]/g, '').replace(/\(.*?\)/g, '').trim().toLowerCase();
|
|
519
|
+
return (c.selector === memStep.selector) ||
|
|
520
|
+
(cleanMemName !== '' && (cleanMemName === cleanCurrName || cleanCurrName.includes(cleanMemName)));
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
if (foundComponent) {
|
|
524
|
+
return {
|
|
525
|
+
thought: `[GUÍA DE ÉXITO] Reutilizando paso maestro aprendido: ${memStep.action_data.action} en ${foundComponent.name}`,
|
|
526
|
+
actions: [{
|
|
527
|
+
...memStep.action_data,
|
|
528
|
+
idx: foundComponent.idx,
|
|
529
|
+
description: foundComponent.name,
|
|
530
|
+
selector: foundComponent.selector,
|
|
531
|
+
frameIdx: foundComponent.frameIdx,
|
|
532
|
+
type: foundComponent.type
|
|
533
|
+
}],
|
|
534
|
+
finish: memStep.finish || false
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
} catch (e) { }
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async askIA(prompt: string, history: string[] = []): Promise<AgentAction> {
|
|
543
|
+
// MODO BRIDGE: Si existe una URL de portal, delegamos la decisión al servidor (SaaS)
|
|
544
|
+
if (process.env.ARCALITY_PORTAL_URL) {
|
|
545
|
+
return this.askBridge(prompt, history);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (!process.env.ANTHROPIC_API_KEY) throw new Error("ANTHROPIC_API_KEY missing");
|
|
549
|
+
|
|
550
|
+
const state = await this.getPageState();
|
|
551
|
+
const screenshot = await this.page.screenshot({ type: 'png' });
|
|
552
|
+
const base64Img = screenshot.toString('base64');
|
|
553
|
+
|
|
554
|
+
const componentsList = state.components.map(c =>
|
|
555
|
+
`IDX:${c.idx} | TYPE:${c.type} | NAME: "${c.name}"${c.value ? ` | VAL:"${c.value}"` : ''} `
|
|
556
|
+
).join('\n');
|
|
557
|
+
|
|
558
|
+
// Tool mapping for Anthropic Claude
|
|
559
|
+
const rawTools = [
|
|
560
|
+
{
|
|
561
|
+
name: "perform_ui_actions",
|
|
562
|
+
description: "Execute standard UI interactions (click, fill, wait) to progress in the mission.",
|
|
563
|
+
parameters: {
|
|
564
|
+
type: "object",
|
|
565
|
+
properties: {
|
|
566
|
+
thought: { type: "string", description: "Reasoning about why these actions are taken." },
|
|
567
|
+
actions: {
|
|
568
|
+
type: "array",
|
|
569
|
+
items: {
|
|
570
|
+
type: "object",
|
|
571
|
+
properties: {
|
|
572
|
+
idx: { type: "number", description: "The IDX from the components list." },
|
|
573
|
+
action: { enum: ["click", "double_click", "fill", "select", "wait"] },
|
|
574
|
+
value: { type: "string", description: "Text to fill if action is 'fill'." }
|
|
575
|
+
},
|
|
576
|
+
required: ["action"]
|
|
577
|
+
}
|
|
578
|
+
},
|
|
579
|
+
finish: { type: "boolean", description: "CRITICAL: Set to true only when the mission objective is fully achieved. If true, actions should ideally be empty." }
|
|
580
|
+
},
|
|
581
|
+
required: ["thought", "actions"]
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
name: "inspect_element_details",
|
|
586
|
+
description: "QA Skill: Get deep technical metadata of an element (attributes, computed styles). Use this if an element is hidden, disabled, or not responding correctly.",
|
|
587
|
+
parameters: {
|
|
588
|
+
type: "object",
|
|
589
|
+
properties: {
|
|
590
|
+
idx: { type: "number", description: "The IDX of the component to research." }
|
|
591
|
+
},
|
|
592
|
+
required: ["idx"]
|
|
593
|
+
}
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
name: "report_application_bug",
|
|
597
|
+
description: "QA Skill: Use this when the mission objective cannot be achieved. This includes: (1) Technical BUGS (elements not appearing), (2) BUSINESS ERRORS (validation messages like 'Conflict', 'Already exists', 'Permission denied'), or (3) Any condition that violates the user's success criteria.",
|
|
598
|
+
parameters: {
|
|
599
|
+
type: "object",
|
|
600
|
+
properties: {
|
|
601
|
+
bug_description: { type: "string", description: "Detailed description of the failure or error message." },
|
|
602
|
+
expected_behavior: { type: "string", description: "What the mission expected (Success Criteria)." },
|
|
603
|
+
actual_behavior: { type: "string", description: "What actually happened (Failure Criteria)." }
|
|
604
|
+
},
|
|
605
|
+
required: ["bug_description", "expected_behavior", "actual_behavior"]
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
name: "report_inability_to_proceed",
|
|
610
|
+
description: "CRITICAL: Use this when you are STUCK, CONFUSED, or CANNOT find required elements after 3+ attempts. This tool allows you to gracefully admit confusion and help the user rephrase the prompt. Use when: (1) You've clicked the same element 3+ times, (2) Cannot find a specific UI element mentioned in the prompt, (3) The prompt is ambiguous or contradictory.",
|
|
611
|
+
parameters: {
|
|
612
|
+
type: "object",
|
|
613
|
+
properties: {
|
|
614
|
+
confusion_reason: {
|
|
615
|
+
type: "string",
|
|
616
|
+
description: "Explain why you cannot proceed (e.g., 'Cannot find Menu icon after 5 attempts', 'Stuck clicking same element repeatedly', 'Prompt asks for first but I only see second')"
|
|
617
|
+
},
|
|
618
|
+
attempts_summary: {
|
|
619
|
+
type: "string",
|
|
620
|
+
description: "Summarize what you tried (e.g., 'Clicked TAE-063 5 times, went back to list 5 times')"
|
|
621
|
+
},
|
|
622
|
+
prompt_suggestion: {
|
|
623
|
+
type: "string",
|
|
624
|
+
description: "Suggest how the user can rephrase for clarity (e.g., 'Please specify the exact text or position of the menu icon')"
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
required: ["confusion_reason", "attempts_summary", "prompt_suggestion"]
|
|
628
|
+
}
|
|
629
|
+
},
|
|
630
|
+
...qaAdvancedTools.map(t => ({
|
|
631
|
+
name: t.name,
|
|
632
|
+
description: t.description,
|
|
633
|
+
parameters: (t as any).input_schema || (t as any).parameters
|
|
634
|
+
}))
|
|
635
|
+
];
|
|
636
|
+
|
|
637
|
+
const credentialsContext = (process.env.LOGIN_USER && process.env.LOGIN_PASSWORD)
|
|
638
|
+
? `\n🔑 QA CREDENTIALS (Use if login is needed):
|
|
639
|
+
- User: ${process.env.LOGIN_USER}
|
|
640
|
+
- Password: ${process.env.LOGIN_PASSWORD} \n`
|
|
641
|
+
: '';
|
|
642
|
+
|
|
643
|
+
const memoryContext = this.loadMemory(prompt);
|
|
644
|
+
const skillsContext = this.loadSkills();
|
|
645
|
+
|
|
646
|
+
let projectInfo = '';
|
|
647
|
+
try {
|
|
648
|
+
const { getProjectContext } = require('../src/projectInspector');
|
|
649
|
+
const ctx = getProjectContext(process.cwd());
|
|
650
|
+
projectInfo = `\n🏗️ PROJECT CONTEXT:
|
|
651
|
+
- Name: ${ctx.name}
|
|
652
|
+
- Tech Stack: ${ctx.framework}
|
|
653
|
+
- Environment: ${process.env.NODE_ENV || 'development'}
|
|
654
|
+
- Active Config: ${process.env.ACTIVE_CONFIG || 'Default'}\n`;
|
|
655
|
+
} catch { }
|
|
656
|
+
|
|
657
|
+
// FLUJO DE CONTEXTO (KnowledgeService Hook)
|
|
658
|
+
let memoryBackendContext = '';
|
|
659
|
+
try {
|
|
660
|
+
const contextData = await this.knowledgeService.getContext(state.url);
|
|
661
|
+
|
|
662
|
+
if (contextData && (contextData.rules?.length > 0 || contextData.fields?.length > 0)) {
|
|
663
|
+
memoryBackendContext = `\n🧠 BACKEND COLLECTIVE MEMORY FOR THIS PAGE:\n`;
|
|
664
|
+
if (contextData.rules?.length > 0) {
|
|
665
|
+
memoryBackendContext += `BUSINESS RULES:\n`;
|
|
666
|
+
contextData.rules.forEach(r => memoryBackendContext += `- [${r.severity}] ${r.title}: ${r.description}\n`);
|
|
667
|
+
}
|
|
668
|
+
if (contextData.fields?.length > 0) {
|
|
669
|
+
memoryBackendContext += `KNOWN FIELDS:\n`;
|
|
670
|
+
contextData.fields.forEach(f => memoryBackendContext += `- ${f.field_identifier} (${f.field_type}) - Required: ${f.is_required}\n`);
|
|
671
|
+
}
|
|
672
|
+
console.log(`>>ARCALITY_STATUS>> 🧠 Contexto de Memoria Colectiva inyectado (${contextData.rules?.length || 0} reglas, ${contextData.fields?.length || 0} campos).`);
|
|
673
|
+
}
|
|
674
|
+
} catch (e: any) {
|
|
675
|
+
console.log(`>>ARCALITY_STATUS>> ⚠️ Error en hook de contexto: ${e.message}`);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const systemPromptBlocks = [
|
|
679
|
+
{
|
|
680
|
+
type: "text",
|
|
681
|
+
text: `# IDENTITY
|
|
682
|
+
You are **Arcality**, an elite Senior QA Web Testing Agent. You have 10+ years of experience in manual and automated testing. You are meticulous, thorough, and NEVER take shortcuts. You treat every mission as a real client engagement where your professional reputation is on the line. You MUST use your full arsenal of QA tools — guessing and blind retries are UNACCEPTABLE.
|
|
683
|
+
|
|
684
|
+
# MISSION
|
|
685
|
+
${prompt}
|
|
686
|
+
|
|
687
|
+
${projectInfo}
|
|
688
|
+
${skillsContext}
|
|
689
|
+
${memoryContext}
|
|
690
|
+
${credentialsContext}
|
|
691
|
+
${memoryBackendContext}`
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
type: "text",
|
|
695
|
+
text: `\n# METHODOLOGY (MANDATORY — Follow in order)
|
|
696
|
+
Every turn, you MUST follow these 4 phases:
|
|
697
|
+
|
|
698
|
+
## Phase 1: OBSERVE
|
|
699
|
+
- Read ALL components in CURRENT COMPONENTS carefully.
|
|
700
|
+
- Check the URL — are you on the right page?
|
|
701
|
+
- Read HISTORY — what have you already done? Don't repeat actions.
|
|
702
|
+
- Look for MESSAGE components — errors ([ERROR_CRÍTICO]) or success messages.
|
|
703
|
+
- If you see [INFERRED] in any component name → that name is UNRELIABLE. You MUST call \`inspect_element_details\` on it BEFORE interacting.
|
|
704
|
+
|
|
705
|
+
## Phase 2: PLAN
|
|
706
|
+
- Based on your observations, decide: What is the ONE next meaningful step toward completing the mission?
|
|
707
|
+
- If filling a form: identify ALL required fields first, then plan to fill multiple fields per turn (3-5 fills at once when possible).
|
|
708
|
+
- Generate UNIQUE data for fields (include timestamps, random suffixes) to avoid "duplicado" errors.
|
|
709
|
+
- If unsure about an element: use \`inspect_element_details\` FIRST. DO NOT guess.
|
|
710
|
+
|
|
711
|
+
## Phase 3: ACT
|
|
712
|
+
- Execute your planned actions using \`perform_ui_actions\`.
|
|
713
|
+
- Group multiple related actions in one call (e.g., click+fill for 3 fields = 6 actions in one call).
|
|
714
|
+
- For dropdowns/selects: Click to open → WAIT for next turn to see options → Select.
|
|
715
|
+
- For menus: Click menu icon → WAIT for next turn → Click option.
|
|
716
|
+
|
|
717
|
+
## Phase 4: VERIFY
|
|
718
|
+
- After critical actions (SAVE/CREATE/DELETE), verify the result:
|
|
719
|
+
- Did a success toast appear?
|
|
720
|
+
- Did the URL change to a list view?
|
|
721
|
+
- Are there any new error messages?
|
|
722
|
+
- Use \`validate_element_state\` BEFORE clicking SAVE to ensure the button is enabled.
|
|
723
|
+
- Use \`extract_table_data\` AFTER a save to confirm the record exists in the list.
|
|
724
|
+
|
|
725
|
+
# MANDATORY TOOL USAGE (NON-NEGOTIABLE)
|
|
726
|
+
These are NOT optional. You MUST use these tools in these situations:
|
|
727
|
+
|
|
728
|
+
| Situation | Tool | When |
|
|
729
|
+
|-----------|------|------|
|
|
730
|
+
| Action FAILED (Timeout/Error) | \`inspect_element_details\` | IMMEDIATELY, before any retry |
|
|
731
|
+
| Element name has [INFERRED] | \`inspect_element_details\` | BEFORE interacting with it |
|
|
732
|
+
| Before clicking SAVE/GUARDAR | \`validate_element_state\` | To confirm button is enabled |
|
|
733
|
+
| After successful SAVE | \`extract_table_data\` | To verify record was created |
|
|
734
|
+
| UI is frozen/unresponsive | \`capture_console_errors\` | To check for JS errors |
|
|
735
|
+
| Bug found in application | \`create_test_evidence\` | To document with annotated screenshot |
|
|
736
|
+
| Mission complete | \`create_test_evidence\` | To document the final successful state |
|
|
737
|
+
| Modal/popup blocking page | \`send_keyboard_event\` (Escape) | To close the overlay |
|
|
738
|
+
| Time/Date/Color picker field | \`interact_native_control\` | INSTEAD of fill — native inputs need special handling |
|
|
739
|
+
| Element not found on screen | \`scroll_page\` | To reveal off-screen content |
|
|
740
|
+
| Need to close dropdown/menu | \`send_keyboard_event\` (Escape) | After inspecting options |
|
|
741
|
+
| Navigate between form sections | \`send_keyboard_event\` (Tab) | To move focus between fields |
|
|
742
|
+
|
|
743
|
+
# CRITICAL RULES
|
|
744
|
+
1. **NEVER BLIND-RETRY**: If an action fails, you MUST investigate with \`inspect_element_details\` before retrying. Repeating the exact same failed action is FORBIDDEN.
|
|
745
|
+
2. **NO REPEAT ACTIONS**: Check HISTORY. If you already filled a field or clicked a button, do NOT do it again unless the UI clearly reset OR the button belongs to a different step/modal/section mentioned in the mission (e.g., Save in modal vs Save in main form).
|
|
746
|
+
|
|
747
|
+
3. **CURRENT STATE > MEMORY**: What you see in CURRENT COMPONENTS is the truth. SUCCESS MEMORY is a guide, not gospel.
|
|
748
|
+
4. **PAGE TRANSITIONS**: If URL contains '/edit' or you see a 'Save' button, you ARE in the edit view. Don't go back.
|
|
749
|
+
5. **MENUS & DROPDOWNS**: After clicking a toggle/menu, WAIT for the next turn. Never click the same menu icon twice in a row.
|
|
750
|
+
6. **ERROR HANDLING**: If you see '🛑 [ERROR_CRÍTICO]', STOP everything. Analyze the error, fix the data, then retry.
|
|
751
|
+
7. **DATA UNIQUENESS**: When generating test data, ALWAYS include a unique suffix (timestamp, counter). Never use "Test123" or "Prueba" alone.
|
|
752
|
+
8. **FINISH CORRECTLY**: Use \`finish: true\` ONLY when the mission objective is FULLY achieved.
|
|
753
|
+
- **BEWARE**: If you see a record in a table, confirm it is the one you JUST created (e.g., check timestamp or log message). Do NOT assume an existing record is your success.
|
|
754
|
+
- **MESSAGES**: Be suspicious of success messages that appear right after login or page load. Real success messages usually appear IMMEDIATELY after you click SAVE.
|
|
755
|
+
9. **LARGE COMPONENTS**: If a component name is >50 characters, it's probably a CONTAINER, not a field. Do not interact with it.
|
|
756
|
+
10. **STUCK DETECTION**: If history shows 3+ similar actions without progress, use \`report_inability_to_proceed\`.
|
|
757
|
+
11. **NEGATIVE TESTING**: If mission expects an error (e.g., "verifica que no se pueda guardar sin campos"), and you see that error, the mission is SUCCESS. Use \`finish: true\`.
|
|
758
|
+
12. **GUIDE CONFLICT**: If SUCCESS MEMORY disagrees with what you see on screen, ABANDON the memory and trust your eyes.
|
|
759
|
+
13. **NATIVE CONTROLS**: For \`<input type="time">\`, \`<input type="date">\`, or similar native pickers, NEVER use \`fill\` repeatedly. Use \`interact_native_control\` or \`send_keyboard_event\` instead. If \`fill\` fails once, switch strategy immediately.`,
|
|
760
|
+
cache_control: { type: "ephemeral" }
|
|
761
|
+
}
|
|
762
|
+
];
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
// Adjuntar al reporte de Playwright (Evidencia Visual)
|
|
766
|
+
if (this.testInfo) {
|
|
767
|
+
await this.testInfo.attach(`turn-${history.length}-vision`, {
|
|
768
|
+
body: screenshot,
|
|
769
|
+
contentType: 'image/png'
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const anthropicMessages: any[] = [
|
|
774
|
+
{
|
|
775
|
+
role: "user",
|
|
776
|
+
content: [
|
|
777
|
+
{
|
|
778
|
+
type: "image",
|
|
779
|
+
source: {
|
|
780
|
+
type: "base64",
|
|
781
|
+
media_type: "image/png",
|
|
782
|
+
data: base64Img
|
|
783
|
+
},
|
|
784
|
+
cache_control: { type: "ephemeral" }
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
type: "text",
|
|
788
|
+
text: `MISSION STATUS:
|
|
789
|
+
- URL: ${state.url}
|
|
790
|
+
- Title: ${state.title}
|
|
791
|
+
|
|
792
|
+
PROGRESS TRACKING:
|
|
793
|
+
- IMPORTANT: This mission is a SEQUENCE of steps. Analyze the HISTORY to see which steps are DONE.
|
|
794
|
+
- SEARCH FOR MESSAGES: If you see a component with type 'MESSAGE' (especially [ERROR] or [SUCCESS]) that matches the mission objective, the MISSION IS COMPLETE.
|
|
795
|
+
- If mission is complete, use 'perform_ui_actions' with 'finish: true' immediately.
|
|
796
|
+
- DO NOT restart or go back to previous steps if you are already in the target view.
|
|
797
|
+
|
|
798
|
+
CURRENT COMPONENTS:
|
|
799
|
+
${componentsList}
|
|
800
|
+
|
|
801
|
+
CONSOLE LOGS:
|
|
802
|
+
${this.logs.join('\n') || 'None'}
|
|
803
|
+
|
|
804
|
+
HISTORY (last 25 steps):
|
|
805
|
+
${history.slice(-25).join('\n') || 'None'}`
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
type: "text",
|
|
809
|
+
text: `Please analyze the current state and provide the next step.`,
|
|
810
|
+
cache_control: { type: "ephemeral" }
|
|
811
|
+
}
|
|
812
|
+
]
|
|
813
|
+
}
|
|
814
|
+
];
|
|
815
|
+
|
|
816
|
+
console.log(`>>ARCALITY_STATUS>> 🧠 Arcality está procesando la visión...`);
|
|
817
|
+
const startTime = Date.now();
|
|
818
|
+
|
|
819
|
+
for (let turn = 0; turn < 10; turn++) {
|
|
820
|
+
let response: Response;
|
|
821
|
+
let retryCount = 0;
|
|
822
|
+
const maxRetries = 3;
|
|
823
|
+
|
|
824
|
+
while (true) {
|
|
825
|
+
const isProxyMode = !!process.env.ARCALITY_API_URL;
|
|
826
|
+
const endpointUrl = isProxyMode
|
|
827
|
+
? `${process.env.ARCALITY_API_URL}/api/v1/ai/proxy`
|
|
828
|
+
: "https://api.anthropic.com/v1/messages";
|
|
829
|
+
|
|
830
|
+
const headers: Record<string, string> = {
|
|
831
|
+
"Content-Type": "application/json"
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
if (isProxyMode) {
|
|
835
|
+
headers["x-api-key"] = process.env.ARCALITY_API_KEY || "";
|
|
836
|
+
} else {
|
|
837
|
+
headers["x-api-key"] = process.env.ANTHROPIC_API_KEY!;
|
|
838
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
response = await fetch(endpointUrl, {
|
|
842
|
+
method: "POST",
|
|
843
|
+
headers,
|
|
844
|
+
body: JSON.stringify({
|
|
845
|
+
model: process.env.CLAUDE_MODEL || "claude-3-5-sonnet-20241022",
|
|
846
|
+
max_tokens: 4096,
|
|
847
|
+
system: systemPromptBlocks,
|
|
848
|
+
tools: rawTools.map(t => ({
|
|
849
|
+
name: t.name,
|
|
850
|
+
description: t.description,
|
|
851
|
+
input_schema: (t as any).parameters || (t as any).input_schema
|
|
852
|
+
})),
|
|
853
|
+
messages: anthropicMessages,
|
|
854
|
+
temperature: 0.2
|
|
855
|
+
})
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
if (response.ok) break;
|
|
859
|
+
|
|
860
|
+
const errorText = await response.text();
|
|
861
|
+
const isTransient = response.status === 529 || response.status === 429 || response.status === 503;
|
|
862
|
+
|
|
863
|
+
if (isTransient && retryCount < maxRetries) {
|
|
864
|
+
retryCount++;
|
|
865
|
+
const delay = Math.pow(2, retryCount) * 1000;
|
|
866
|
+
console.log(`\n>>ARCALITY_STATUS>> ⚠️ Arcality está saturado (${response.status}). Reintentando...`);
|
|
867
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
throw new Error(`Arcality Brain Error: ${errorText}`);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const data: any = await response.json();
|
|
875
|
+
const message = data;
|
|
876
|
+
|
|
877
|
+
anthropicMessages.push({
|
|
878
|
+
role: "assistant",
|
|
879
|
+
content: message.content
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
const toolCalls = message.content.filter((c: any) => c.type === 'tool_use');
|
|
883
|
+
const textResponse = message.content.find((c: any) => c.type === 'text')?.text || "";
|
|
884
|
+
|
|
885
|
+
if (toolCalls.length === 0) {
|
|
886
|
+
console.log(`(${((Date.now() - startTime) / 1000).toFixed(1)}s)`);
|
|
887
|
+
return { thought: textResponse || "No action decided", actions: [] };
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const toolResults: any[] = [];
|
|
891
|
+
let uiActionCall = null;
|
|
892
|
+
|
|
893
|
+
for (const toolUse of toolCalls) {
|
|
894
|
+
const toolName = toolUse.name;
|
|
895
|
+
const toolInput = toolUse.input;
|
|
896
|
+
|
|
897
|
+
if (toolName === "perform_ui_actions") {
|
|
898
|
+
uiActionCall = { id: toolUse.id, input: toolInput };
|
|
899
|
+
if (toolCalls.length > 1) {
|
|
900
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: "UI Action buffered." });
|
|
901
|
+
}
|
|
902
|
+
} else if (toolName === "inspect_element_details") {
|
|
903
|
+
const idx = toolInput.idx;
|
|
904
|
+
const c = state.components.find(comp => comp.idx === idx);
|
|
905
|
+
console.log(`>>ARCALITY_STATUS>> 🔍 Investigando IDX:${idx}...`);
|
|
906
|
+
|
|
907
|
+
let details = "Element not found or no longer present.";
|
|
908
|
+
if (c) {
|
|
909
|
+
try {
|
|
910
|
+
details = await this.page.frames()[c.frameIdx].evaluate((s) => {
|
|
911
|
+
const el = document.querySelector(s) as HTMLElement;
|
|
912
|
+
if (!el) return "Not found";
|
|
913
|
+
const styles = window.getComputedStyle(el);
|
|
914
|
+
return JSON.stringify({
|
|
915
|
+
tag: el.tagName,
|
|
916
|
+
id: el.id,
|
|
917
|
+
classes: el.className,
|
|
918
|
+
isVisible: el.offsetParent !== null,
|
|
919
|
+
rect: el.getBoundingClientRect(),
|
|
920
|
+
attributes: Array.from(el.attributes).map(a => `${a.name}="${a.value}"`),
|
|
921
|
+
style: { display: styles.display, pointerEvents: styles.pointerEvents, opacity: styles.opacity }
|
|
922
|
+
}, null, 2);
|
|
923
|
+
}, c.selector);
|
|
924
|
+
} catch (e: any) { details = `Error inspecting: ${e.message}`; }
|
|
925
|
+
}
|
|
926
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: details });
|
|
927
|
+
} else if (toolName === "report_application_bug") {
|
|
928
|
+
console.error(`🐛 [BUG DETECTED] ${toolInput.bug_description}`);
|
|
929
|
+
|
|
930
|
+
// REGLA DE APRENDIZAJE: Registrar bug como regla de negocio para el futuro
|
|
931
|
+
try {
|
|
932
|
+
const url = this.page.url();
|
|
933
|
+
await this.knowledgeService.saveRule(
|
|
934
|
+
"Validación Detectada (Auto-Learning)",
|
|
935
|
+
`Error: ${toolInput.bug_description}. Se esperaba: ${toolInput.expected_behavior}`,
|
|
936
|
+
'WARNING',
|
|
937
|
+
url
|
|
938
|
+
);
|
|
939
|
+
} catch (e) { }
|
|
940
|
+
|
|
941
|
+
throw new Error(`APPLICATION BUG: ${toolInput.bug_description}`);
|
|
942
|
+
} else if (toolName === "report_inability_to_proceed") {
|
|
943
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: "Confusion acknowledged." });
|
|
944
|
+
return {
|
|
945
|
+
thought: `DISCULPA: ${toolInput.confusion_reason}\n\n💡 SUGERENCIA: ${toolInput.prompt_suggestion}`,
|
|
946
|
+
actions: [],
|
|
947
|
+
finish: true
|
|
948
|
+
};
|
|
949
|
+
} else if (toolName === "validate_element_state") {
|
|
950
|
+
const { idx, validations } = toolInput;
|
|
951
|
+
const c = state.components.find(comp => comp.idx === idx);
|
|
952
|
+
let result = "";
|
|
953
|
+
if (!c) {
|
|
954
|
+
result = "Element not found.";
|
|
955
|
+
} else {
|
|
956
|
+
try {
|
|
957
|
+
result = await this.page.frames()[c.frameIdx].evaluate(({ selector, validations }) => {
|
|
958
|
+
const el = document.querySelector(selector) as HTMLElement;
|
|
959
|
+
if (!el) return "Element no longer exists in DOM.";
|
|
960
|
+
|
|
961
|
+
const styles = window.getComputedStyle(el);
|
|
962
|
+
const results = validations.map((v: any) => {
|
|
963
|
+
let pass = false;
|
|
964
|
+
let actual = "";
|
|
965
|
+
switch (v.type) {
|
|
966
|
+
case 'exists':
|
|
967
|
+
pass = !!el;
|
|
968
|
+
actual = pass ? "exists" : "none";
|
|
969
|
+
break;
|
|
970
|
+
case 'visible':
|
|
971
|
+
pass = el.offsetParent !== null && styles.visibility !== 'hidden' && styles.display !== 'none' && parseFloat(styles.opacity) > 0;
|
|
972
|
+
actual = pass ? "visible" : "hidden";
|
|
973
|
+
break;
|
|
974
|
+
case 'enabled':
|
|
975
|
+
pass = !(el as any).disabled && el.getAttribute('aria-disabled') !== 'true';
|
|
976
|
+
actual = pass ? "enabled" : "disabled";
|
|
977
|
+
break;
|
|
978
|
+
case 'disabled':
|
|
979
|
+
pass = (el as any).disabled || el.getAttribute('aria-disabled') === 'true';
|
|
980
|
+
actual = pass ? "disabled" : "enabled";
|
|
981
|
+
break;
|
|
982
|
+
case 'contains_text':
|
|
983
|
+
actual = el.innerText || "";
|
|
984
|
+
pass = actual.toLowerCase().includes(v.expected_value.toLowerCase());
|
|
985
|
+
break;
|
|
986
|
+
case 'has_value':
|
|
987
|
+
actual = (el as HTMLInputElement).value || "";
|
|
988
|
+
pass = actual === v.expected_value;
|
|
989
|
+
break;
|
|
990
|
+
case 'is_empty':
|
|
991
|
+
actual = (el as HTMLInputElement).value || el.innerText || "";
|
|
992
|
+
pass = actual.trim() === "";
|
|
993
|
+
break;
|
|
994
|
+
case 'has_class':
|
|
995
|
+
actual = el.className;
|
|
996
|
+
pass = el.classList.contains(v.expected_value);
|
|
997
|
+
break;
|
|
998
|
+
}
|
|
999
|
+
return `${v.type}: ${pass ? 'PASS' : 'FAIL'} (Actual: "${actual}")`;
|
|
1000
|
+
});
|
|
1001
|
+
return results.join('\n');
|
|
1002
|
+
}, { selector: c.selector, validations });
|
|
1003
|
+
} catch (e: any) { result = `Error validating: ${e.message}`; }
|
|
1004
|
+
}
|
|
1005
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: result });
|
|
1006
|
+
} else if (toolName === "extract_table_data") {
|
|
1007
|
+
const { table_identifier, max_rows = 50 } = toolInput;
|
|
1008
|
+
console.log(`>>ARCALITY_STATUS>> 📊 Extrayendo tabla: ${table_identifier}...`);
|
|
1009
|
+
try {
|
|
1010
|
+
const tableData = await this.page.evaluate(({ identifier, limit }) => {
|
|
1011
|
+
const findTable = () => {
|
|
1012
|
+
const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, th, label, span, p, div'))
|
|
1013
|
+
.filter(el => el.textContent?.includes(identifier));
|
|
1014
|
+
|
|
1015
|
+
for (const h of headers) {
|
|
1016
|
+
let curr: HTMLElement | null = h as HTMLElement;
|
|
1017
|
+
for (let i = 0; i < 5; i++) {
|
|
1018
|
+
if (!curr) break;
|
|
1019
|
+
const table = curr.querySelector('table') || (curr.tagName === 'TABLE' ? curr : null);
|
|
1020
|
+
if (table) return table;
|
|
1021
|
+
curr = curr.parentElement;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
return document.querySelector('table');
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
const table = findTable();
|
|
1028
|
+
if (!table) return "No table found with identifier: " + identifier;
|
|
1029
|
+
|
|
1030
|
+
const rows = Array.from(table.querySelectorAll('tr')).slice(0, limit);
|
|
1031
|
+
return rows.map(r => {
|
|
1032
|
+
return Array.from(r.querySelectorAll('td, th')).map(c => c.textContent?.trim() || "");
|
|
1033
|
+
});
|
|
1034
|
+
}, { identifier: table_identifier, limit: max_rows });
|
|
1035
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: JSON.stringify(tableData, null, 2) });
|
|
1036
|
+
} catch (e: any) { toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error extracting: ${e.message}` }); }
|
|
1037
|
+
} else if (toolName === "capture_console_errors") {
|
|
1038
|
+
const logs = this.logs.length > 0 ? this.logs.join('\n') : "No console errors/warnings/network errors captured.";
|
|
1039
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: logs });
|
|
1040
|
+
} else if (toolName === "assert_url_pattern") {
|
|
1041
|
+
const { pattern, should_match = true } = toolInput;
|
|
1042
|
+
const url = this.page.url();
|
|
1043
|
+
const match = url.includes(pattern);
|
|
1044
|
+
const pass = should_match ? match : !match;
|
|
1045
|
+
const result = `URL Assertion: ${pass ? 'PASS' : 'FAIL'} (Current URL: ${url} | Pattern: ${pattern} | Expected match: ${should_match})`;
|
|
1046
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: result });
|
|
1047
|
+
|
|
1048
|
+
} else if (toolName === "measure_page_performance") {
|
|
1049
|
+
const { thresholds = {} } = toolInput;
|
|
1050
|
+
console.log(`>>ARCALITY_STATUS>> ⚡ Midiendo rendimiento de página...`);
|
|
1051
|
+
try {
|
|
1052
|
+
const perfData = await this.page.evaluate(() => {
|
|
1053
|
+
const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
|
|
1054
|
+
const paint = performance.getEntriesByType('paint');
|
|
1055
|
+
const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[];
|
|
1056
|
+
const fcp = paint.find(p => p.name === 'first-contentful-paint');
|
|
1057
|
+
|
|
1058
|
+
return {
|
|
1059
|
+
load_time_ms: nav ? Math.round(nav.loadEventEnd - nav.startTime) : null,
|
|
1060
|
+
dom_content_loaded_ms: nav ? Math.round(nav.domContentLoadedEventEnd - nav.startTime) : null,
|
|
1061
|
+
first_contentful_paint_ms: fcp ? Math.round(fcp.startTime) : null,
|
|
1062
|
+
network_requests: resources.length,
|
|
1063
|
+
total_resource_size_kb: Math.round(resources.reduce((sum, r) => sum + (r.transferSize || 0), 0) / 1024)
|
|
1064
|
+
};
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
const results: string[] = [
|
|
1068
|
+
`📊 PERFORMANCE METRICS:`,
|
|
1069
|
+
` Load Time: ${perfData.load_time_ms ?? 'N/A'}ms`,
|
|
1070
|
+
` DOM Content Loaded: ${perfData.dom_content_loaded_ms ?? 'N/A'}ms`,
|
|
1071
|
+
` First Contentful Paint: ${perfData.first_contentful_paint_ms ?? 'N/A'}ms`,
|
|
1072
|
+
` Network Requests: ${perfData.network_requests}`,
|
|
1073
|
+
` Total Resource Size: ${perfData.total_resource_size_kb}KB`
|
|
1074
|
+
];
|
|
1075
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: results.join('\n') });
|
|
1076
|
+
} catch (e: any) {
|
|
1077
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error measuring performance: ${e.message}` });
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
} else if (toolName === "save_navigation_checkpoint") {
|
|
1081
|
+
const { checkpoint_name, include_storage = true } = toolInput;
|
|
1082
|
+
console.log(`>>ARCALITY_STATUS>> 💾 Guardando punto de control: "${checkpoint_name}"...`);
|
|
1083
|
+
try {
|
|
1084
|
+
const url = this.page.url();
|
|
1085
|
+
const context = this.page.context();
|
|
1086
|
+
const storageState = await context.storageState();
|
|
1087
|
+
|
|
1088
|
+
let localStorage: Record<string, string> = {};
|
|
1089
|
+
let sessionStorage: Record<string, string> = {};
|
|
1090
|
+
|
|
1091
|
+
if (include_storage) {
|
|
1092
|
+
const storageData = await this.page.evaluate(() => {
|
|
1093
|
+
const ls: Record<string, string> = {};
|
|
1094
|
+
for (let i = 0; i < window.localStorage.length; i++) {
|
|
1095
|
+
const key = window.localStorage.key(i);
|
|
1096
|
+
if (key) ls[key] = window.localStorage.getItem(key) || '';
|
|
1097
|
+
}
|
|
1098
|
+
const ss: Record<string, string> = {};
|
|
1099
|
+
for (let i = 0; i < window.sessionStorage.length; i++) {
|
|
1100
|
+
const key = window.sessionStorage.key(i);
|
|
1101
|
+
if (key) ss[key] = window.sessionStorage.getItem(key) || '';
|
|
1102
|
+
}
|
|
1103
|
+
return { localStorage: ls, sessionStorage: ss };
|
|
1104
|
+
});
|
|
1105
|
+
localStorage = storageData.localStorage;
|
|
1106
|
+
sessionStorage = storageData.sessionStorage;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
this.checkpoints.set(checkpoint_name, { url, storageState, localStorage, sessionStorage });
|
|
1110
|
+
|
|
1111
|
+
const result = `✅ Checkpoint "${checkpoint_name}" saved successfully.`;
|
|
1112
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: result });
|
|
1113
|
+
} catch (e: any) {
|
|
1114
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error saving checkpoint: ${e.message}` });
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
} else if (toolName === "restore_navigation_checkpoint") {
|
|
1118
|
+
const { checkpoint_name } = toolInput;
|
|
1119
|
+
console.log(`>>ARCALITY_STATUS>> 🔄 Restaurando punto de control: "${checkpoint_name}"...`);
|
|
1120
|
+
|
|
1121
|
+
const checkpoint = this.checkpoints.get(checkpoint_name);
|
|
1122
|
+
if (!checkpoint) {
|
|
1123
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `❌ Checkpoint "${checkpoint_name}" not found.` });
|
|
1124
|
+
} else {
|
|
1125
|
+
try {
|
|
1126
|
+
const context = this.page.context();
|
|
1127
|
+
await context.clearCookies();
|
|
1128
|
+
if (checkpoint.storageState.cookies?.length) {
|
|
1129
|
+
await context.addCookies(checkpoint.storageState.cookies);
|
|
1130
|
+
}
|
|
1131
|
+
await this.page.goto(checkpoint.url, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
1132
|
+
await this.page.evaluate(({ ls, ss }) => {
|
|
1133
|
+
window.localStorage.clear();
|
|
1134
|
+
for (const [k, v] of Object.entries(ls)) window.localStorage.setItem(k, v);
|
|
1135
|
+
window.sessionStorage.clear();
|
|
1136
|
+
for (const [k, v] of Object.entries(ss)) window.sessionStorage.setItem(k, v);
|
|
1137
|
+
}, { ls: checkpoint.localStorage, ss: checkpoint.sessionStorage });
|
|
1138
|
+
await this.page.reload({ waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
1139
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `✅ Checkpoint "${checkpoint_name}" restored.` });
|
|
1140
|
+
} catch (e: any) {
|
|
1141
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error restoring checkpoint: ${e.message}` });
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
} else if (toolName === "create_test_evidence") {
|
|
1146
|
+
const { evidence_type, annotations, save_as, description, finish_run = false } = toolInput;
|
|
1147
|
+
console.log(`📸 [SKILL] Creating test evidence: "${save_as}"...`);
|
|
1148
|
+
try {
|
|
1149
|
+
const filename = save_as.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
1150
|
+
|
|
1151
|
+
// 1. Appy visual annotations if requested
|
|
1152
|
+
if (annotations && annotations.length > 0) {
|
|
1153
|
+
await this.page.evaluate((anns) => {
|
|
1154
|
+
anns.forEach((ann: any) => {
|
|
1155
|
+
const el = document.querySelector(`[agent-idx="${ann.idx}"]`) as HTMLElement;
|
|
1156
|
+
if (el) {
|
|
1157
|
+
const rect = el.getBoundingClientRect();
|
|
1158
|
+
const div = document.createElement('div');
|
|
1159
|
+
div.className = 'arcality-annotation';
|
|
1160
|
+
div.style.position = 'absolute';
|
|
1161
|
+
div.style.border = `3px solid ${ann.color || 'red'}`;
|
|
1162
|
+
div.style.top = `${rect.top + window.scrollY}px`;
|
|
1163
|
+
div.style.left = `${rect.left + window.scrollX}px`;
|
|
1164
|
+
div.style.width = `${rect.width}px`;
|
|
1165
|
+
div.style.height = `${rect.height}px`;
|
|
1166
|
+
div.style.pointerEvents = 'none';
|
|
1167
|
+
div.style.zIndex = '1000000';
|
|
1168
|
+
|
|
1169
|
+
const label = document.createElement('span');
|
|
1170
|
+
label.innerText = ann.label;
|
|
1171
|
+
label.style.background = ann.color || 'red';
|
|
1172
|
+
label.style.color = 'white';
|
|
1173
|
+
label.style.fontSize = '12px';
|
|
1174
|
+
label.style.padding = '2px 4px';
|
|
1175
|
+
label.style.position = 'absolute';
|
|
1176
|
+
label.style.top = '-20px';
|
|
1177
|
+
label.style.left = '0';
|
|
1178
|
+
div.appendChild(label);
|
|
1179
|
+
document.body.appendChild(div);
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
}, annotations);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
let buffer: Buffer = await this.page.screenshot({
|
|
1186
|
+
type: 'png',
|
|
1187
|
+
fullPage: evidence_type === 'full_page_screenshot'
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
// 2. Clean up annotations
|
|
1191
|
+
await this.page.evaluate(() => {
|
|
1192
|
+
document.querySelectorAll('.arcality-annotation').forEach(el => el.remove());
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
if (this.testInfo) {
|
|
1196
|
+
// Attach image
|
|
1197
|
+
await this.testInfo.attach(filename, { body: buffer, contentType: 'image/png' });
|
|
1198
|
+
|
|
1199
|
+
// Attach description if exists
|
|
1200
|
+
if (description) {
|
|
1201
|
+
await this.testInfo.attach(`${filename}_description`, {
|
|
1202
|
+
body: Buffer.from(description, 'utf-8'),
|
|
1203
|
+
contentType: 'text/plain'
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `✅ Evidence "${filename}" created and attached.` });
|
|
1208
|
+
|
|
1209
|
+
if (finish_run) {
|
|
1210
|
+
this.lastScreenshot = base64Img;
|
|
1211
|
+
return { thought: `Evidence "${description || filename}" created. Mission complete.`, actions: [], finish: true };
|
|
1212
|
+
}
|
|
1213
|
+
} else {
|
|
1214
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `⚠️ testInfo not available.` });
|
|
1215
|
+
}
|
|
1216
|
+
} catch (e: any) {
|
|
1217
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error creating evidence: ${e.message}` });
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
} else if (toolName === "send_keyboard_event") {
|
|
1221
|
+
const { key, idx, repeat = 1 } = toolInput;
|
|
1222
|
+
console.log(`>>ARCALITY_STATUS>> ⌨️ Enviando tecla: "${key}" ${idx !== undefined ? `(focus en IDX:${idx})` : '(focus actual)'} x${repeat}`);
|
|
1223
|
+
try {
|
|
1224
|
+
// Focus on element if idx provided
|
|
1225
|
+
if (idx !== undefined) {
|
|
1226
|
+
const c = state.components.find(comp => comp.idx === idx);
|
|
1227
|
+
if (c) {
|
|
1228
|
+
const loc = this.page.frames()[c.frameIdx].locator(c.selector).first();
|
|
1229
|
+
await loc.focus({ timeout: 5000 }).catch(async () => {
|
|
1230
|
+
await loc.click({ timeout: 5000, force: true });
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
for (let i = 0; i < Math.min(repeat, 20); i++) {
|
|
1236
|
+
await this.page.keyboard.press(key);
|
|
1237
|
+
if (repeat > 1) await this.page.waitForTimeout(100);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `✅ Key "${key}" pressed ${repeat} time(s).` });
|
|
1241
|
+
} catch (e: any) {
|
|
1242
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error sending key: ${e.message}` });
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
} else if (toolName === "interact_native_control") {
|
|
1246
|
+
const { idx, control_type, value } = toolInput;
|
|
1247
|
+
const c = state.components.find(comp => comp.idx === idx);
|
|
1248
|
+
console.log(`>>ARCALITY_STATUS>> 🎛️ Control nativo: ${control_type} IDX:${idx} → "${value}"`);
|
|
1249
|
+
|
|
1250
|
+
if (!c) {
|
|
1251
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: "Element not found." });
|
|
1252
|
+
} else {
|
|
1253
|
+
try {
|
|
1254
|
+
const frame = this.page.frames()[c.frameIdx];
|
|
1255
|
+
const loc = frame.locator(c.selector).first();
|
|
1256
|
+
|
|
1257
|
+
switch (control_type) {
|
|
1258
|
+
case 'time':
|
|
1259
|
+
case 'date':
|
|
1260
|
+
case 'datetime': {
|
|
1261
|
+
// Strategy 1: Direct Playwright fill (works on most browsers)
|
|
1262
|
+
try {
|
|
1263
|
+
await loc.fill(value, { timeout: 3000 });
|
|
1264
|
+
const actualValue = await loc.inputValue();
|
|
1265
|
+
if (actualValue && actualValue !== '') {
|
|
1266
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `✅ ${control_type} set to "${actualValue}" via fill().` });
|
|
1267
|
+
break;
|
|
1268
|
+
}
|
|
1269
|
+
} catch { /* fill failed, try keyboard */ }
|
|
1270
|
+
|
|
1271
|
+
// Strategy 2: Click + select all + type
|
|
1272
|
+
await loc.click({ force: true });
|
|
1273
|
+
await this.page.keyboard.press('Control+a');
|
|
1274
|
+
await this.page.waitForTimeout(100);
|
|
1275
|
+
|
|
1276
|
+
// For time inputs, type digits directly (e.g., "1305" for "13:05")
|
|
1277
|
+
const digits = value.replace(/[:-T]/g, '');
|
|
1278
|
+
await this.page.keyboard.type(digits, { delay: 50 });
|
|
1279
|
+
|
|
1280
|
+
const finalValue = await loc.inputValue().catch(() => '');
|
|
1281
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `✅ ${control_type} set via keyboard. Current value: "${finalValue}".` });
|
|
1282
|
+
break;
|
|
1283
|
+
}
|
|
1284
|
+
case 'color':
|
|
1285
|
+
await loc.fill(value, { timeout: 3000 });
|
|
1286
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `✅ Color set to "${value}".` });
|
|
1287
|
+
break;
|
|
1288
|
+
case 'range': {
|
|
1289
|
+
const numVal = parseFloat(value);
|
|
1290
|
+
await loc.fill(String(numVal), { timeout: 3000 }).catch(async () => {
|
|
1291
|
+
// Fallback: set value via JS
|
|
1292
|
+
await frame.evaluate(({ sel, val }) => {
|
|
1293
|
+
const el = document.querySelector(sel) as HTMLInputElement;
|
|
1294
|
+
if (el) {
|
|
1295
|
+
el.value = val;
|
|
1296
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1297
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1298
|
+
}
|
|
1299
|
+
}, { sel: c.selector, val: String(numVal) });
|
|
1300
|
+
});
|
|
1301
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `✅ Range set to ${numVal}.` });
|
|
1302
|
+
break;
|
|
1303
|
+
}
|
|
1304
|
+
case 'select': {
|
|
1305
|
+
// Try by label first, then by value
|
|
1306
|
+
try {
|
|
1307
|
+
await loc.selectOption({ label: value }, { timeout: 3000 });
|
|
1308
|
+
} catch {
|
|
1309
|
+
try {
|
|
1310
|
+
await loc.selectOption({ value: value }, { timeout: 3000 });
|
|
1311
|
+
} catch {
|
|
1312
|
+
await loc.selectOption(value, { timeout: 3000 });
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
const selected = await loc.inputValue().catch(() => 'unknown');
|
|
1316
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `✅ Option selected. Value: "${selected}".` });
|
|
1317
|
+
break;
|
|
1318
|
+
}
|
|
1319
|
+
case 'contenteditable':
|
|
1320
|
+
await loc.click({ force: true });
|
|
1321
|
+
await this.page.keyboard.press('Control+a');
|
|
1322
|
+
await this.page.keyboard.type(value, { delay: 30 });
|
|
1323
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `✅ Content set to "${value}".` });
|
|
1324
|
+
break;
|
|
1325
|
+
default:
|
|
1326
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `❌ Unknown control type: ${control_type}` });
|
|
1327
|
+
}
|
|
1328
|
+
} catch (e: any) {
|
|
1329
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error interacting with native control: ${e.message}` });
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
} else if (toolName === "scroll_page") {
|
|
1334
|
+
const { direction, amount = 500, idx } = toolInput;
|
|
1335
|
+
console.log(`>>ARCALITY_STATUS>> 📜 Scroll: ${direction} ${amount}px ${idx !== undefined ? `(container IDX:${idx})` : '(page)'}`);
|
|
1336
|
+
try {
|
|
1337
|
+
if (idx !== undefined) {
|
|
1338
|
+
const c = state.components.find(comp => comp.idx === idx);
|
|
1339
|
+
if (c) {
|
|
1340
|
+
const loc = this.page.frames()[c.frameIdx].locator(c.selector).first();
|
|
1341
|
+
await loc.evaluate((el, args) => {
|
|
1342
|
+
const d = args.direction;
|
|
1343
|
+
const a = args.amount;
|
|
1344
|
+
if (d === 'top') el.scrollTop = 0;
|
|
1345
|
+
else if (d === 'bottom') el.scrollTop = el.scrollHeight;
|
|
1346
|
+
else if (d === 'down') el.scrollTop += a;
|
|
1347
|
+
else if (d === 'up') el.scrollTop -= a;
|
|
1348
|
+
else if (d === 'left') el.scrollLeft -= a;
|
|
1349
|
+
else if (d === 'right') el.scrollLeft += a;
|
|
1350
|
+
}, { direction, amount });
|
|
1351
|
+
}
|
|
1352
|
+
} else {
|
|
1353
|
+
switch (direction) {
|
|
1354
|
+
case 'down': await this.page.mouse.wheel(0, amount); break;
|
|
1355
|
+
case 'up': await this.page.mouse.wheel(0, -amount); break;
|
|
1356
|
+
case 'right': await this.page.mouse.wheel(amount, 0); break;
|
|
1357
|
+
case 'left': await this.page.mouse.wheel(-amount, 0); break;
|
|
1358
|
+
case 'top': await this.page.evaluate(() => window.scrollTo(0, 0)); break;
|
|
1359
|
+
case 'bottom': await this.page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); break;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
await this.page.waitForTimeout(300);
|
|
1363
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `✅ Scrolled ${direction} ${direction === 'top' || direction === 'bottom' ? 'completely' : `${amount}px`}. Page state will refresh on next turn.` });
|
|
1364
|
+
} catch (e: any) {
|
|
1365
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error scrolling: ${e.message}` });
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
if (toolResults.length > 0) {
|
|
1371
|
+
anthropicMessages.push({
|
|
1372
|
+
role: "user",
|
|
1373
|
+
content: toolResults
|
|
1374
|
+
});
|
|
1375
|
+
// Si solo eran herramientas de investigación/QA, continuamos el loop interno de la IA
|
|
1376
|
+
if (!uiActionCall) continue;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
if (uiActionCall) {
|
|
1380
|
+
const input = uiActionCall.input;
|
|
1381
|
+
console.log(`(${((Date.now() - startTime) / 1000).toFixed(1)}s)`);
|
|
1382
|
+
this.lastScreenshot = base64Img;
|
|
1383
|
+
return {
|
|
1384
|
+
thought: input.thought,
|
|
1385
|
+
actions: input.actions.map((a: any) => {
|
|
1386
|
+
const c = state.components.find(comp => comp.idx === a.idx);
|
|
1387
|
+
let finalValue = a.value;
|
|
1388
|
+
// AUTO-FIX for Date inputs: Convert DD/MM/YYYY to YYYY-MM-DD
|
|
1389
|
+
if (a.action === 'fill' && finalValue && /^\d{2}\/\d{2}\/\d{4}$/.test(finalValue)) {
|
|
1390
|
+
const [d, m, y] = finalValue.split('/');
|
|
1391
|
+
finalValue = `${y}-${m}-${d}`;
|
|
1392
|
+
}
|
|
1393
|
+
return { ...a, value: finalValue, selector: c?.selector, frameIdx: c?.frameIdx, description: c?.name, type: c?.type };
|
|
1394
|
+
}),
|
|
1395
|
+
finish: input.finish
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
return { thought: "Max internal turns reached", actions: [] };
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
/**
|
|
1403
|
+
* QA Skill: Genera una explicación "humana" y amigable de por qué la misión fue exitosa.
|
|
1404
|
+
*/
|
|
1405
|
+
async humanizeMissionResult(prompt: string, history: string[]): Promise<string | null> {
|
|
1406
|
+
if (!process.env.ANTHROPIC_API_KEY) return null;
|
|
1407
|
+
|
|
1408
|
+
try {
|
|
1409
|
+
const isProxyMode = !!process.env.ARCALITY_API_URL;
|
|
1410
|
+
const endpointUrl = isProxyMode
|
|
1411
|
+
? `${process.env.ARCALITY_API_URL}/api/v1/ai/proxy`
|
|
1412
|
+
: "https://api.anthropic.com/v1/messages";
|
|
1413
|
+
|
|
1414
|
+
const headers: Record<string, string> = {
|
|
1415
|
+
"Content-Type": "application/json"
|
|
1416
|
+
};
|
|
1417
|
+
|
|
1418
|
+
if (isProxyMode) {
|
|
1419
|
+
headers["x-api-key"] = process.env.ARCALITY_API_KEY || "";
|
|
1420
|
+
} else {
|
|
1421
|
+
headers["x-api-key"] = process.env.ANTHROPIC_API_KEY!;
|
|
1422
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const response = await fetch(endpointUrl, {
|
|
1426
|
+
method: "POST",
|
|
1427
|
+
headers,
|
|
1428
|
+
body: JSON.stringify({
|
|
1429
|
+
model: process.env.CLAUDE_MODEL || "claude-3-5-sonnet-20241022",
|
|
1430
|
+
max_tokens: 300,
|
|
1431
|
+
system: "Eres un Senior QA personalizador de reportes. Tu tarea es resumir en una sola frase corta, profesional y amigable por qué una misión de testing fue exitosa basándote en el historial de pasos. Usa un tono de victoria. No menciones detalles técnicos como 'selectores' o 'DOM'. El resumen debe empezar con algo como '¡Misión cumplida! ...'",
|
|
1432
|
+
messages: [
|
|
1433
|
+
{ role: "user", content: `Misión original: ${prompt}\n\nhistory of steps:\n${history.join('\n')}` }
|
|
1434
|
+
]
|
|
1435
|
+
})
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
const data: any = await response.json();
|
|
1439
|
+
return data.content?.[0]?.text || null;
|
|
1440
|
+
} catch (e) {
|
|
1441
|
+
return null;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
/**
|
|
1446
|
+
* MODO BRIDGE: Envía la percepción de la CLI hacia el Portal Web (SaaS)
|
|
1447
|
+
* para que el servidor decida la acción usando su propia infraestructura e IP.
|
|
1448
|
+
*/
|
|
1449
|
+
private async askBridge(prompt: string, history: string[] = []): Promise<AgentAction> {
|
|
1450
|
+
console.log(`>>ARCALITY_STATUS>> 🌐 Conectando con el cerebro de Arcality (Modo SaaS)...`);
|
|
1451
|
+
|
|
1452
|
+
const state = await this.getPageState();
|
|
1453
|
+
const screenshot = await this.page.screenshot({ type: 'png' });
|
|
1454
|
+
|
|
1455
|
+
// Payload enriquecido para el portal
|
|
1456
|
+
const payload = {
|
|
1457
|
+
mission: prompt,
|
|
1458
|
+
history: history,
|
|
1459
|
+
percept: {
|
|
1460
|
+
url: state.url,
|
|
1461
|
+
title: state.title,
|
|
1462
|
+
components: state.components,
|
|
1463
|
+
screenshot: screenshot.toString('base64'),
|
|
1464
|
+
logs: this.logs
|
|
1465
|
+
},
|
|
1466
|
+
context: {
|
|
1467
|
+
version: "1.0.1",
|
|
1468
|
+
model: process.env.CLAUDE_MODEL || "claude-3-5-sonnet-20241022",
|
|
1469
|
+
config: process.env.ACTIVE_CONFIG || "Default"
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
|
|
1473
|
+
try {
|
|
1474
|
+
const baseUrl = process.env.ARCALITY_PORTAL_URL || "";
|
|
1475
|
+
const url = baseUrl.endsWith('/')
|
|
1476
|
+
? baseUrl + 'api/v1/decide'
|
|
1477
|
+
: baseUrl + '/api/v1/decide';
|
|
1478
|
+
|
|
1479
|
+
const response = await fetch(url, {
|
|
1480
|
+
method: 'POST',
|
|
1481
|
+
headers: {
|
|
1482
|
+
'Content-Type': 'application/json',
|
|
1483
|
+
'x-arcality-cli-version': '1.0.1'
|
|
1484
|
+
},
|
|
1485
|
+
body: JSON.stringify(payload)
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
if (!response.ok) {
|
|
1489
|
+
const errorText = await response.text();
|
|
1490
|
+
throw new Error(`SaaS Bridge Error (${response.status}): ${errorText}`);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
const decision = await response.json() as AgentAction;
|
|
1494
|
+
return decision;
|
|
1495
|
+
|
|
1496
|
+
} catch (err: any) {
|
|
1497
|
+
console.error(chalk.red(`\n❌ Error en el Bridge de Arcality: ${err.message}`));
|
|
1498
|
+
console.log(chalk.yellow(`\n⚠️ Reintentando modo local (Direct Claude)...`));
|
|
1499
|
+
|
|
1500
|
+
// Fallback al modo local si el bridge falla por red o configuración
|
|
1501
|
+
delete process.env.ARCALITY_PORTAL_URL;
|
|
1502
|
+
return this.askIA(prompt, history);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
/**
|
|
1507
|
+
* QA Skill: Summarizes the mission execution into a clean, human-friendly English YAML format.
|
|
1508
|
+
*/
|
|
1509
|
+
async summarizeMissionToYaml(prompt: string, history: string[], startUrl: string): Promise<string | null> {
|
|
1510
|
+
if (!process.env.ANTHROPIC_API_KEY) return null;
|
|
1511
|
+
|
|
1512
|
+
try {
|
|
1513
|
+
const historyStr = history.slice(-30).join('\n');
|
|
1514
|
+
const isProxyMode = !!process.env.ARCALITY_API_URL;
|
|
1515
|
+
const endpointUrl = isProxyMode
|
|
1516
|
+
? `${process.env.ARCALITY_API_URL}/api/v1/ai/proxy`
|
|
1517
|
+
: "https://api.anthropic.com/v1/messages";
|
|
1518
|
+
|
|
1519
|
+
const headers: Record<string, string> = {
|
|
1520
|
+
"Content-Type": "application/json"
|
|
1521
|
+
};
|
|
1522
|
+
|
|
1523
|
+
if (isProxyMode) {
|
|
1524
|
+
headers["x-api-key"] = process.env.ARCALITY_API_KEY || "";
|
|
1525
|
+
} else {
|
|
1526
|
+
headers["x-api-key"] = process.env.ANTHROPIC_API_KEY!;
|
|
1527
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
const response = await fetch(endpointUrl, {
|
|
1531
|
+
method: "POST",
|
|
1532
|
+
headers,
|
|
1533
|
+
body: JSON.stringify({
|
|
1534
|
+
model: process.env.CLAUDE_MODEL || "claude-3-5-sonnet-20241022",
|
|
1535
|
+
max_tokens: 1000,
|
|
1536
|
+
system: `You are an elite QA Engineer. Your task is to transform a raw execution history into a clean, human-friendly English YAML Mission Card.
|
|
1537
|
+
|
|
1538
|
+
STRICT YAML STRUCTURE:
|
|
1539
|
+
Title: "Descriptive title for the test"
|
|
1540
|
+
StartAt: "The relative path where the test began"
|
|
1541
|
+
Steps: |
|
|
1542
|
+
- Bullet points of human-readable steps performed
|
|
1543
|
+
Data:
|
|
1544
|
+
key: "value" # Group any data used (names, emails, prices). Set to null if no specific data was used.
|
|
1545
|
+
SuccessCriteria: |
|
|
1546
|
+
- Bullet points of how success was verified
|
|
1547
|
+
|
|
1548
|
+
RULES:
|
|
1549
|
+
1. Use professional English.
|
|
1550
|
+
2. The 'Steps' must be what actually happened in the history, but simplified for a human to read.
|
|
1551
|
+
3. 'Data' should extract values mentioned in the history (e.g., if you filled a name with 'Test-123', include it).
|
|
1552
|
+
4. Output ONLY the YAML block. No preamble or explanations.`,
|
|
1553
|
+
messages: [
|
|
1554
|
+
{ role: "user", content: `Original Mission: ${prompt}\nStarting URL: ${startUrl}\n\nExecution history:\n${historyStr}` }
|
|
1555
|
+
],
|
|
1556
|
+
temperature: 0.1
|
|
1557
|
+
})
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
const data: any = await response.json();
|
|
1561
|
+
let yaml = data.content?.[0]?.text || null;
|
|
1562
|
+
|
|
1563
|
+
if (yaml) {
|
|
1564
|
+
// Remove markdown blocks if present
|
|
1565
|
+
yaml = yaml.replace(/```yaml\n?/g, "").replace(/```/g, "").trim();
|
|
1566
|
+
}
|
|
1567
|
+
return yaml;
|
|
1568
|
+
} catch (e) {
|
|
1569
|
+
console.error("Error generating Smart YAML:", e);
|
|
1570
|
+
return null;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}
|