chocolatito-code 1.6.13 → 1.6.15
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/COMPUTER-USE.md +37 -0
- package/README.md +29 -3
- package/dist/agent/historialDeCambios.d.ts +77 -0
- package/dist/agent/historialDeCambios.js +220 -0
- package/dist/agent/salidaDelAgente.d.ts +9 -0
- package/dist/agent/salidaDelAgente.js +19 -0
- package/dist/agent/undoManager.d.ts +21 -0
- package/dist/agent/undoManager.js +85 -5
- package/dist/config/permissions.js +4 -0
- package/dist/index.js +216 -27
- package/dist/prompts/systemPrompt.js +15 -11
- package/dist/servidor/archivos.d.ts +69 -0
- package/dist/servidor/archivos.js +156 -0
- package/dist/servidor/captura.d.ts +9 -0
- package/dist/servidor/captura.js +48 -0
- package/dist/servidor/fondo.d.ts +85 -0
- package/dist/servidor/fondo.js +134 -0
- package/dist/servidor/marca.d.ts +14 -0
- package/dist/servidor/marca.js +14 -0
- package/dist/servidor/pagina.d.ts +0 -26
- package/dist/servidor/pagina.js +337 -10
- package/dist/servidor/puente.d.ts +13 -0
- package/dist/servidor/puente.js +11 -0
- package/dist/servidor/servidor.d.ts +76 -2
- package/dist/servidor/servidor.js +245 -13
- package/dist/tools/computerUse.d.ts +11 -1
- package/dist/tools/computerUse.js +376 -34
- package/dist/tools/toolDefsComputer.js +39 -14
- package/dist/tools/visionBridge.d.ts +4 -2
- package/dist/tools/visionBridge.js +40 -15
- package/dist/tools/win/hostScript.js +300 -29
- package/dist/ui/comandos.js +1 -0
- package/dist/ui/ink/App.js +23 -3
- package/dist/ui/ink/Prompt.d.ts +5 -1
- package/dist/ui/ink/Prompt.js +92 -4
- package/dist/ui/ink/control.d.ts +2 -0
- package/dist/ui/ink/control.js +15 -0
- package/dist/ui/ink/montarApp.d.ts +0 -19
- package/dist/ui/ink/montarApp.js +3 -0
- package/dist/ui/permissionPrompt.js +44 -9
- package/dist/ui/pieFijo.js +5 -0
- package/dist/ui/prompt.js +6 -0
- package/dist/ui/renderer.d.ts +1 -0
- package/dist/ui/renderer.js +27 -2
- package/dist/ui/selector.js +5 -0
- package/dist/ui/spinner.d.ts +1 -0
- package/dist/ui/spinner.js +6 -0
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -127,6 +127,8 @@ function fail(action, msg) {
|
|
|
127
127
|
*/
|
|
128
128
|
/** La ultima foto de cada ventana, para poder contar solo lo que cambio. */
|
|
129
129
|
const ultimaFoto = new Map();
|
|
130
|
+
/** Coordenadas de los controles vistos en el ultimo snapshot para reintentos por coordenadas. */
|
|
131
|
+
const ultimoSnapshotCoords = new Map();
|
|
130
132
|
/** Identidad de un control que NO depende de su numero de ref, que se renumera. */
|
|
131
133
|
function claveDe(e) {
|
|
132
134
|
return `${e.role}::${e.name}`;
|
|
@@ -217,6 +219,60 @@ async function estadoTrasActuar(args) {
|
|
|
217
219
|
return (`\n\nASI HA QUEDADO "${res.title}" (los numeros son NUEVOS, usa estos):\n${filas}` +
|
|
218
220
|
(res.truncated ? `\n … hay mas controles; pide ui_snapshot con 'max' mas alto si te falta alguno.` : ""));
|
|
219
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* POR QUE: "Recuperarse solo, una vez" (patron Astra / OpenAI Operator).
|
|
224
|
+
* Si un clic por ref UIA falla (elemento descolgado del arbol, ventana recargada,
|
|
225
|
+
* o control que no expone Invoke/Toggle/SelectionItem), reintenta AUTOMATICAMENTE
|
|
226
|
+
* una vez mediante clic por coordenadas usando el rectangulo que UIA reporto antes
|
|
227
|
+
* de romperse (guardado en cache o via ui_rect). Solo si este segundo intento tambien
|
|
228
|
+
* falla, devuelve un error claro avisando al modelo de que hace falta usar vision (screenshot).
|
|
229
|
+
*/
|
|
230
|
+
async function uiClickFallback(params, res, mode) {
|
|
231
|
+
let fallbackCoord = res.x && res.y && res.x > 0 && res.y > 0 ? { x: res.x, y: res.y } : null;
|
|
232
|
+
if (!fallbackCoord && params.ref) {
|
|
233
|
+
const cached = ultimoSnapshotCoords.get(params.ref);
|
|
234
|
+
if (cached && cached.x > 0 && cached.y > 0) {
|
|
235
|
+
fallbackCoord = { x: cached.x, y: cached.y };
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
const rect = await winHost.send("ui_rect", { ref: params.ref });
|
|
239
|
+
if (rect.ok && rect.x > 0 && rect.y > 0)
|
|
240
|
+
fallbackCoord = { x: rect.x, y: rect.y };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (fallbackCoord) {
|
|
244
|
+
const elegida = await resolveWindow(params);
|
|
245
|
+
if (elegida.ok && elegida.hwnd && mode === "background") {
|
|
246
|
+
const bgRes = await winHost.send("bg_click", {
|
|
247
|
+
...pinned(elegida),
|
|
248
|
+
x: fallbackCoord.x,
|
|
249
|
+
y: fallbackCoord.y,
|
|
250
|
+
button: "left",
|
|
251
|
+
count: 1,
|
|
252
|
+
});
|
|
253
|
+
if (bgRes.ok)
|
|
254
|
+
return { ok: true, label: res.label || `[${params.ref}]`, via: "reintento coordenadas bg_click" };
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
const fgRes = await winHost.send("click", {
|
|
258
|
+
x: fallbackCoord.x,
|
|
259
|
+
y: fallbackCoord.y,
|
|
260
|
+
button: "left",
|
|
261
|
+
count: 1,
|
|
262
|
+
});
|
|
263
|
+
if (fgRes.ok)
|
|
264
|
+
return { ok: true, label: res.label || `[${params.ref}]`, via: "reintento coordenadas click" };
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
ok: false,
|
|
268
|
+
error: `fallo la activacion UIA (${res.error || "sin patron"}) y tambien el reintento por coordenadas en (${fallbackCoord.x}, ${fallbackCoord.y}). Hace falta vision: usa screenshot con analyze/grid.`,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
ok: false,
|
|
273
|
+
error: `fallo la activacion UIA (${res.error || "sin patron"}) y no habia coordenadas conocidas para reintentar. Hace falta vision: usa screenshot con analyze/grid.`,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
220
276
|
export async function computerUse(params, cwd = process.cwd(), apiKey) {
|
|
221
277
|
const { action } = params;
|
|
222
278
|
const mode = params.mode || "background";
|
|
@@ -224,26 +280,53 @@ export async function computerUse(params, cwd = process.cwd(), apiKey) {
|
|
|
224
280
|
let targetY = params.coordinate ? params.coordinate[1] : params.y;
|
|
225
281
|
let startX = params.start_coordinate ? params.start_coordinate[0] : params.x;
|
|
226
282
|
let startY = params.start_coordinate ? params.start_coordinate[1] : params.y;
|
|
227
|
-
// Soporte de coordenadas normalizadas (0.0 a 1.0
|
|
228
|
-
if (targetX !== undefined && targetY !== undefined
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
283
|
+
// Soporte de coordenadas normalizadas (0.0 a 1.0 o escala 0..1000 estilo Astra / OpenAI Operator)
|
|
284
|
+
if (targetX !== undefined && targetY !== undefined) {
|
|
285
|
+
const isFloat = targetX > 0 && targetX <= 1.0 && targetY > 0 && targetY <= 1.0;
|
|
286
|
+
const isMille = params.normalized === true || (targetX > 1.0 && targetX <= 1000 && targetY > 1.0 && targetY <= 1000 && params.normalized);
|
|
287
|
+
if (isFloat || isMille) {
|
|
288
|
+
try {
|
|
289
|
+
let sw = 1920, sh = 1080, offX = 0, offY = 0;
|
|
290
|
+
if (params.window) {
|
|
291
|
+
const wChoice = await resolveWindow(params);
|
|
292
|
+
if (wChoice.ok && wChoice.hwnd) {
|
|
293
|
+
const wInfo = await winHost.send("find_window", { hwnd: wChoice.hwnd });
|
|
294
|
+
if (wInfo && wInfo.width > 0 && wInfo.height > 0) {
|
|
295
|
+
sw = wInfo.width;
|
|
296
|
+
sh = wInfo.height;
|
|
297
|
+
offX = wInfo.x || 0;
|
|
298
|
+
offY = wInfo.y || 0;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (sw === 1920 && sh === 1080 && offX === 0 && offY === 0) {
|
|
303
|
+
const sInfo = await winHost.send("screen_info");
|
|
304
|
+
sw = sInfo.virtualWidth || sInfo.primaryWidth || 1920;
|
|
305
|
+
sh = sInfo.virtualHeight || sInfo.primaryHeight || 1080;
|
|
306
|
+
offX = sInfo.virtualX || 0;
|
|
307
|
+
offY = sInfo.virtualY || 0;
|
|
308
|
+
}
|
|
309
|
+
const factor = isFloat ? 1.0 : 1000.0;
|
|
310
|
+
targetX = Math.round(offX + (targetX / factor) * sw);
|
|
311
|
+
targetY = Math.round(offY + (targetY / factor) * sh);
|
|
312
|
+
}
|
|
313
|
+
catch { }
|
|
235
314
|
}
|
|
236
|
-
catch { }
|
|
237
315
|
}
|
|
238
|
-
if (startX !== undefined && startY !== undefined
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
316
|
+
if (startX !== undefined && startY !== undefined) {
|
|
317
|
+
const isFloat = startX > 0 && startX <= 1.0 && startY > 0 && startY <= 1.0;
|
|
318
|
+
const isMille = params.normalized === true || (startX > 1.0 && startX <= 1000 && startY > 1.0 && startY <= 1000 && params.normalized);
|
|
319
|
+
if (isFloat || isMille) {
|
|
320
|
+
try {
|
|
321
|
+
const sInfo = await winHost.send("screen_info");
|
|
322
|
+
const sw = sInfo.virtualWidth || sInfo.primaryWidth || 1920;
|
|
323
|
+
const sh = sInfo.virtualHeight || sInfo.primaryHeight || 1080;
|
|
324
|
+
const factor = isFloat ? 1.0 : 1000.0;
|
|
325
|
+
startX = Math.round((sInfo.virtualX || 0) + (startX / factor) * sw);
|
|
326
|
+
startY = Math.round((sInfo.virtualY || 0) + (startY / factor) * sh);
|
|
327
|
+
}
|
|
328
|
+
catch { }
|
|
245
329
|
}
|
|
246
|
-
catch { }
|
|
247
330
|
}
|
|
248
331
|
try {
|
|
249
332
|
switch (action) {
|
|
@@ -315,10 +398,33 @@ export async function computerUse(params, cwd = process.cwd(), apiKey) {
|
|
|
315
398
|
if (!res.ok)
|
|
316
399
|
return fail(action, res.error || "fallo desconocido");
|
|
317
400
|
const els = res.elements || [];
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
401
|
+
// Guardamos las coordenadas de los controles para el reintento resiliente estilo Astra
|
|
402
|
+
for (const e of els) {
|
|
403
|
+
if (e.ref && e.x && e.y) {
|
|
404
|
+
ultimoSnapshotCoords.set(e.ref, { x: e.x, y: e.y, label: `${e.role} "${e.name}"` });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// POR QUE: Leer texto que el arbol no expone (Astra / Operator).
|
|
408
|
+
// Cuando UIA devuelve 0 o casi 0 controles (canvas, app vieja sin UIA, Electron con
|
|
409
|
+
// accesibilidad desactivada), no dejamos al agente ciego pensando que la ventana esta vacia.
|
|
410
|
+
// Se intenta extraer texto nativo Win32 y si no hay nada se instruye explicitamente que DEBE
|
|
411
|
+
// usarse screenshot con vision.
|
|
412
|
+
const isCanvasOrEmpty = els.length === 0 || (els.length <= 2 && els.every((e) => !e.name && (e.role === "Pane" || e.role === "Window" || e.role === "Custom")));
|
|
413
|
+
if (isCanvasOrEmpty) {
|
|
414
|
+
const textRes = await winHost.send("extract_text", {
|
|
415
|
+
...pinned(choice),
|
|
416
|
+
...(choice.hwnd ? {} : windowArgs(params)),
|
|
417
|
+
});
|
|
418
|
+
const lineas = (textRes.ok && Array.isArray(textRes.lines)) ? textRes.lines : [];
|
|
419
|
+
if (lineas.length > 0) {
|
|
420
|
+
return (`Ventana "${res.title}": el arbol UIA devolvio ${els.length} controles interactivos, pero se extrajo texto de controles nativos Win32:\n\n` +
|
|
421
|
+
lineas.map((l) => ` ${l}`).join("\n") +
|
|
422
|
+
`\n\nSi necesitas interactuar con elementos que no estan en el arbol, usa screenshot con analyze/grid o clic por coordenadas.` +
|
|
423
|
+
ambiguityNote(params, choice));
|
|
424
|
+
}
|
|
425
|
+
return (`Ventana "${res.title}": el arbol de accesibilidad devolvio ${els.length} controles (vacio o casi vacio: es un lienzo grafico, canvas, Electron con accesibilidad rota o app sin UIA).\n` +
|
|
426
|
+
`La ventana es SOLO VISUAL: no intentes adivinar ni asumas que la ventana esta vacia.\n` +
|
|
427
|
+
`DEBES usar la accion 'screenshot' con 'analyze: true' (o con 'region' / 'grid: true') para leer su contenido con vision.` +
|
|
322
428
|
ambiguityNote(params, choice));
|
|
323
429
|
}
|
|
324
430
|
// Se apunta esta foto como referencia: si no, el resumen de cambios de la
|
|
@@ -359,16 +465,91 @@ export async function computerUse(params, cwd = process.cwd(), apiKey) {
|
|
|
359
465
|
const despues = await estadoTrasActuar(windowArgs(params));
|
|
360
466
|
return `Cambio: ${res.what}.${despues}`;
|
|
361
467
|
}
|
|
468
|
+
// Espera un cambio visual en la ventana o región (ideal para lienzos, juegos, transiciones o cuando UIA no emite eventos)
|
|
469
|
+
case "wait_visual_change": {
|
|
470
|
+
const elegida = await resolveWindow(params);
|
|
471
|
+
if (!elegida.ok)
|
|
472
|
+
return fail(action, elegida.error);
|
|
473
|
+
const ms = Math.min(Math.max(params.ms ?? 3000, 100), 30_000);
|
|
474
|
+
let rx1 = 0, ry1 = 0, rx2 = 0, ry2 = 0;
|
|
475
|
+
if (params.region && params.region.length === 4) {
|
|
476
|
+
[rx1, ry1, rx2, ry2] = params.region;
|
|
477
|
+
}
|
|
478
|
+
const res = await winHost.send("wait_visual_change", {
|
|
479
|
+
...pinned(elegida),
|
|
480
|
+
...(elegida.hwnd ? {} : windowArgs(params)),
|
|
481
|
+
timeoutMs: ms,
|
|
482
|
+
rx1,
|
|
483
|
+
ry1,
|
|
484
|
+
rx2,
|
|
485
|
+
ry2,
|
|
486
|
+
}, ms + 10_000);
|
|
487
|
+
if (!res.ok)
|
|
488
|
+
return fail(action, res.error || "fallo desconocido");
|
|
489
|
+
if (!res.changed) {
|
|
490
|
+
return `No se detectaron cambios visuales en ${ms}ms en "${elegida.title || params.window || "la pantalla"}".`;
|
|
491
|
+
}
|
|
492
|
+
const despues = await estadoTrasActuar(windowArgs(params));
|
|
493
|
+
return `Cambio visual detectado (~${res.diffPercent}% de pixeles en ${res.elapsed}ms).${despues}`;
|
|
494
|
+
}
|
|
495
|
+
// Encadenamiento de acciones rápidas estilo Astra y OpenAI Operator
|
|
496
|
+
case "sequence": {
|
|
497
|
+
const steps = params.steps;
|
|
498
|
+
if (!Array.isArray(steps) || steps.length === 0) {
|
|
499
|
+
return fail(action, 'falta el array "steps" con las acciones a encadenar.');
|
|
500
|
+
}
|
|
501
|
+
// POR QUE: Secuencias anidadas complican el flujo de errores y pueden saturar
|
|
502
|
+
// el host o eludir la aprobacion del usuario. Rechazar de inmediato cualquier
|
|
503
|
+
// paso con action="sequence".
|
|
504
|
+
for (let i = 0; i < steps.length; i++) {
|
|
505
|
+
if (steps[i]?.action === "sequence") {
|
|
506
|
+
return fail(action, `el paso ${i + 1} no puede ser otra 'sequence'. Las secuencias no se anidan; pon los pasos en una sola lista plana.`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
const logs = [];
|
|
510
|
+
let stepIdx = 1;
|
|
511
|
+
for (const step of steps) {
|
|
512
|
+
const merged = {
|
|
513
|
+
...step,
|
|
514
|
+
window: step.window || params.window,
|
|
515
|
+
mode: step.mode || params.mode || mode,
|
|
516
|
+
normalized: step.normalized !== undefined ? step.normalized : params.normalized,
|
|
517
|
+
};
|
|
518
|
+
const stepResult = await computerUse(merged, cwd, apiKey);
|
|
519
|
+
const firstLine = stepResult.split("\n")[0];
|
|
520
|
+
logs.push(`[Paso ${stepIdx}/${steps.length} ${merged.action}] ${firstLine}`);
|
|
521
|
+
if (stepResult.startsWith("Error en computer_use")) {
|
|
522
|
+
return `Secuencia interrumpida en el paso ${stepIdx}:\n${logs.join("\n")}`;
|
|
523
|
+
}
|
|
524
|
+
stepIdx++;
|
|
525
|
+
if (merged.ms) {
|
|
526
|
+
await new Promise((r) => setTimeout(r, merged.ms));
|
|
527
|
+
}
|
|
528
|
+
else if (stepIdx <= steps.length) {
|
|
529
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const despues = params.window ? await estadoTrasActuar(windowArgs(params)) : "";
|
|
533
|
+
return `Secuencia de ${steps.length} pasos completada con exito:\n${logs.join("\n")}${despues}`;
|
|
534
|
+
}
|
|
362
535
|
case "ui_click": {
|
|
363
536
|
if (!params.ref)
|
|
364
537
|
return fail(action, 'indica el "ref" que devolvio ui_snapshot.');
|
|
365
|
-
|
|
538
|
+
let res = await winHost.send("ui_click", { ref: params.ref }, 20_000);
|
|
539
|
+
if (!res.ok)
|
|
540
|
+
res = await uiClickFallback(params, res, mode);
|
|
366
541
|
if (!res.ok)
|
|
367
542
|
return fail(action, res.error || "fallo desconocido");
|
|
368
543
|
// El estado nuevo va aqui y no en otro viaje al modelo: cuesta 43 ms en
|
|
369
544
|
// la maquina contra varios segundos de ida y vuelta. Ver estadoTrasActuar.
|
|
370
545
|
const despues = await estadoTrasActuar(windowArgs(params));
|
|
371
|
-
|
|
546
|
+
// POR QUE: Astra y Operator no dan por hecho que el clic funciono solo por
|
|
547
|
+
// recibir ok de la API de ventanas. Si la ventana no mostro ningun cambio visible,
|
|
548
|
+
// se avisa al modelo para que no suponga un exito ciego y verifique.
|
|
549
|
+
if (despues.includes("no cambio en nada visible")) {
|
|
550
|
+
return `Hice clic pero la ventana NO reacciono (no cambio en nada visible). No asumas que funciono; comprueba con ui_snapshot o usa clic por coordenadas.${despues}`;
|
|
551
|
+
}
|
|
552
|
+
return `Pulsado [${params.ref}] ${res.label || ""} (via ${res.via}, sin mover el raton).${despues}`;
|
|
372
553
|
}
|
|
373
554
|
case "ui_type": {
|
|
374
555
|
if (!params.ref)
|
|
@@ -437,8 +618,23 @@ export async function computerUse(params, cwd = process.cwd(), apiKey) {
|
|
|
437
618
|
// ValuePattern borraria todo. En ambos se enfoca y se teclea, que inserta
|
|
438
619
|
// en el cursor en vez de arrasar el contenido.
|
|
439
620
|
const focus = await winHost.send("ui_focus", { ref: params.ref }, 15_000);
|
|
440
|
-
if (!focus.ok)
|
|
441
|
-
|
|
621
|
+
if (!focus.ok) {
|
|
622
|
+
// Recuperación Astra: si UIA focus falla, clic en sus coordenadas para colocar el cursor
|
|
623
|
+
const rect = await winHost.send("ui_rect", { ref: params.ref });
|
|
624
|
+
if (rect.ok && rect.x > 0 && rect.y > 0) {
|
|
625
|
+
const elegidaWin = await resolveWindow(params);
|
|
626
|
+
if (elegidaWin.ok && elegidaWin.hwnd && mode === "background") {
|
|
627
|
+
await winHost.send("bg_click", { ...pinned(elegidaWin), x: rect.x, y: rect.y, button: "left", count: 1 });
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
await winHost.send("click", { x: rect.x, y: rect.y, button: "left", count: 1 });
|
|
631
|
+
}
|
|
632
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
return fail(action, `${res.error}; ademas fallo el enfoque: ${focus.error}`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
442
638
|
const typed = await winHost.send("type", { text: params.text, delay: 6 }, 120_000);
|
|
443
639
|
if (!typed.ok)
|
|
444
640
|
return fail(action, typed.error || "fallo al teclear");
|
|
@@ -481,7 +677,8 @@ export async function computerUse(params, cwd = process.cwd(), apiKey) {
|
|
|
481
677
|
return `Sin coincidencias en el arbol de accesibilidad para "${query}", y no hay API key para recurrir a la vision.`;
|
|
482
678
|
}
|
|
483
679
|
const shotPath = path.join(ensureCache(), "find_target.jpg");
|
|
484
|
-
|
|
680
|
+
// Activamos la cuadrícula de coordenadas (grid) para que la visión tenga máxima precisión
|
|
681
|
+
const shot = await captureFor({ ...params, grid: true }, shotPath);
|
|
485
682
|
if (!shot.ok)
|
|
486
683
|
return fail(action, shot.error || "no se pudo capturar");
|
|
487
684
|
const loc = await locateElement(apiKey, shotPath, query, shot.imageWidth, shot.imageHeight);
|
|
@@ -491,8 +688,9 @@ export async function computerUse(params, cwd = process.cwd(), apiKey) {
|
|
|
491
688
|
return `No se encontro "${query}" ni en el arbol ni en la imagen. ${loc.reason || ""}`;
|
|
492
689
|
// De pixeles de imagen a pixeles de pantalla.
|
|
493
690
|
const screen = imageToScreen(loc.x, loc.y, shot);
|
|
494
|
-
|
|
495
|
-
|
|
691
|
+
const bboxNote = loc.bbox ? ` (caja [${loc.bbox.join(",")}])` : "";
|
|
692
|
+
return (`"${query}" localizado por vision multimodal en pantalla (${screen.x}, ${screen.y})${bboxNote}. ${loc.reason || ""}\n` +
|
|
693
|
+
`Haz clic con computer_use(action: "click", coordinate: [${screen.x}, ${screen.y}]${params.window ? `, window: "${params.window}"` : ""}).`);
|
|
496
694
|
}
|
|
497
695
|
// ---------------------------------------------------------------- captura
|
|
498
696
|
case "screenshot": {
|
|
@@ -505,9 +703,11 @@ export async function computerUse(params, cwd = process.cwd(), apiKey) {
|
|
|
505
703
|
return fail(action, shot.error || "no se pudo capturar");
|
|
506
704
|
const aviso = ambiguityNote(params, choice);
|
|
507
705
|
const sizeKb = (fs.statSync(out).size / 1024).toFixed(1);
|
|
706
|
+
const regStr = (params.region && params.region.length === 4) ? ` [recorte (${params.region.join(",")})]` : "";
|
|
707
|
+
const gridStr = params.grid ? " [cuadricula visual activa]" : "";
|
|
508
708
|
const head = shot.kind === "window"
|
|
509
|
-
? `Captura de la ventana "${shot.title}" (${shot.sourceWidth}x${shot.sourceHeight}, capturada en segundo plano)`
|
|
510
|
-
: `Captura de pantalla completa (${shot.sourceWidth}x${shot.sourceHeight})`;
|
|
709
|
+
? `Captura de la ventana "${shot.title}" (${shot.sourceWidth}x${shot.sourceHeight}${regStr}${gridStr}, capturada en segundo plano)`
|
|
710
|
+
: `Captura de pantalla completa (${shot.sourceWidth}x${shot.sourceHeight}${regStr}${gridStr})`;
|
|
511
711
|
const meta = `${head}\nArchivo: ${out} (${sizeKb} KB, imagen ${shot.imageWidth}x${shot.imageHeight}, escala ${shot.scale})`;
|
|
512
712
|
if (params.analyze === false)
|
|
513
713
|
return meta;
|
|
@@ -553,12 +753,22 @@ export async function computerUse(params, cwd = process.cwd(), apiKey) {
|
|
|
553
753
|
});
|
|
554
754
|
if (!res.ok)
|
|
555
755
|
return fail(action, res.error || "fallo desconocido");
|
|
556
|
-
|
|
756
|
+
const despues = await estadoTrasActuar(windowArgs(params));
|
|
757
|
+
// POR QUE: Astra y Operator verifican que la accion visualmente causo un cambio.
|
|
758
|
+
// Si Windows devuelve ok pero la ventana sigue identica, hay que avisar para no asumir exito.
|
|
759
|
+
if (despues.includes("no cambio en nada visible")) {
|
|
760
|
+
return `Hice clic pero la ventana NO reacciono (no cambio en nada visible). No asumas que funciono; comprueba con ui_snapshot o usa clic por coordenadas.${despues}`;
|
|
761
|
+
}
|
|
762
|
+
return `Clic ${button}${count > 1 ? ` x${count}` : ""} enviado en segundo plano a "${res.window}" en (${targetX}, ${targetY}). El raton del usuario no se ha movido.${despues}`;
|
|
557
763
|
}
|
|
558
764
|
const res = await winHost.send("click", { x: targetX, y: targetY, button, count });
|
|
559
765
|
if (!res.ok)
|
|
560
766
|
return fail(action, res.error || "fallo desconocido");
|
|
561
|
-
|
|
767
|
+
const despues = params.window ? await estadoTrasActuar(windowArgs(params)) : "";
|
|
768
|
+
if (despues.includes("no cambio en nada visible")) {
|
|
769
|
+
return `Hice clic pero la ventana NO reacciono (no cambio en nada visible). No asumas que funciono; comprueba con ui_snapshot o usa clic por coordenadas.${despues}`;
|
|
770
|
+
}
|
|
771
|
+
return `Clic ${button}${count > 1 ? ` x${count}` : ""} en (${targetX}, ${targetY}) sobre [${res.window}].${despues}`;
|
|
562
772
|
}
|
|
563
773
|
case "mouse_move":
|
|
564
774
|
case "move": {
|
|
@@ -590,6 +800,115 @@ export async function computerUse(params, cwd = process.cwd(), apiKey) {
|
|
|
590
800
|
return fail(action, res.error || "fallo desconocido");
|
|
591
801
|
return `Scroll ${res.direction} x${res.amount}.`;
|
|
592
802
|
}
|
|
803
|
+
// Scroll hacia un elemento o texto hasta que entre en el área visible (estilo Astra / OpenAI Operator)
|
|
804
|
+
case "scroll_into_view":
|
|
805
|
+
case "scroll_to": {
|
|
806
|
+
const rawTarget = params.target ?? params.query ?? params.text ?? params.ref;
|
|
807
|
+
if (rawTarget === undefined || rawTarget === "") {
|
|
808
|
+
return fail(action, 'indica el elemento o texto objetivo en "target", "query" o "ref".');
|
|
809
|
+
}
|
|
810
|
+
const targetStr = String(rawTarget).trim();
|
|
811
|
+
const targetRef = typeof rawTarget === "number" ? rawTarget : (typeof params.ref === "number" ? params.ref : undefined);
|
|
812
|
+
const maxAttempts = Math.min(Math.max(params.maxAttempts ?? params.repeat ?? 5, 1), 10);
|
|
813
|
+
const direction = params.direction || "down";
|
|
814
|
+
// POR QUE: Formularios y paneles web/escritorio no caben en una sola pantalla.
|
|
815
|
+
// Operator y Astra usan scroll iterativo comprobando tras cada paso si el elemento
|
|
816
|
+
// ya entro en el area visible, en vez de adivinar o asumir que el formulario cabe entero.
|
|
817
|
+
const choice = await resolveWindow(params);
|
|
818
|
+
if (params.window && !choice.ok)
|
|
819
|
+
return fail(action, choice.error);
|
|
820
|
+
// Si tenemos ref y soporta ScrollItemPattern nativo, intentarlo primero
|
|
821
|
+
if (targetRef) {
|
|
822
|
+
const direct = await winHost.send("ui_scroll_into_view", { ref: targetRef });
|
|
823
|
+
if (direct.ok) {
|
|
824
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
825
|
+
return `Elemento [${targetRef}] desplazado a la vista mediante patron de scroll.`;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
// Obtener limites de la ventana para verificar visibilidad real
|
|
829
|
+
let winX = 0, winY = 0, winW = 1920, winH = 1080;
|
|
830
|
+
if (choice.ok && choice.hwnd) {
|
|
831
|
+
const wInfo = await winHost.send("find_window", { ...pinned(choice) });
|
|
832
|
+
if (wInfo.ok && wInfo.width > 0) {
|
|
833
|
+
winX = wInfo.x;
|
|
834
|
+
winY = wInfo.y;
|
|
835
|
+
winW = wInfo.width;
|
|
836
|
+
winH = wInfo.height;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
const scrollCenterX = Math.round(winX + winW / 2);
|
|
840
|
+
const scrollCenterY = Math.round(winY + winH / 2);
|
|
841
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
842
|
+
const snap = await winHost.send("ui_snapshot", {
|
|
843
|
+
...pinned(choice),
|
|
844
|
+
...(choice.hwnd ? {} : windowArgs(params)),
|
|
845
|
+
max: 200,
|
|
846
|
+
filter: "",
|
|
847
|
+
interactiveOnly: false,
|
|
848
|
+
}, 25_000);
|
|
849
|
+
if (snap.ok && Array.isArray(snap.elements)) {
|
|
850
|
+
let found = null;
|
|
851
|
+
for (const el of snap.elements) {
|
|
852
|
+
if (targetRef !== undefined && el.ref === targetRef) {
|
|
853
|
+
found = el;
|
|
854
|
+
break;
|
|
855
|
+
}
|
|
856
|
+
const textoControl = `${el.name || ""} ${el.role || ""} ${el.value || ""}`.toLowerCase();
|
|
857
|
+
if (textoControl.includes(targetStr.toLowerCase())) {
|
|
858
|
+
found = el;
|
|
859
|
+
break;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (found) {
|
|
863
|
+
const inViewX = found.x >= winX && found.x <= winX + winW;
|
|
864
|
+
const inViewY = found.y >= winY && found.y <= winY + winH;
|
|
865
|
+
if (inViewX && inViewY) {
|
|
866
|
+
return `Elemento "${targetStr}" visible en la ventana tras ${attempt} intento(s) (ref [${found.ref}], ${found.role} "${found.name}" en ${found.x}, ${found.y}).`;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (attempt < maxAttempts) {
|
|
871
|
+
await winHost.send("scroll", {
|
|
872
|
+
x: scrollCenterX,
|
|
873
|
+
y: scrollCenterY,
|
|
874
|
+
direction,
|
|
875
|
+
amount: params.amount ?? 4,
|
|
876
|
+
});
|
|
877
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return fail(action, `no se encontro el elemento "${targetStr}" visible en "${choice.title || params.window || "la ventana"}" tras ${maxAttempts} intentos de scroll. Comprueba con ui_snapshot o usa screenshot con analyze.`);
|
|
881
|
+
}
|
|
882
|
+
// Extrae texto de la ventana mediante Win32 nativo o visión (Astra / Operator)
|
|
883
|
+
case "read_text":
|
|
884
|
+
case "get_text": {
|
|
885
|
+
const choice = await resolveWindow(params);
|
|
886
|
+
if (params.window && !choice.ok)
|
|
887
|
+
return fail(action, choice.error);
|
|
888
|
+
// POR QUE: Si la aplicacion es Win32 clasica (Notepad, dialogos, controles antiguos)
|
|
889
|
+
// pero UIA no devuelve arbol, extraemos el texto directamente por GetWindowText/WM_GETTEXT
|
|
890
|
+
// sin coste de tokens ni latencia de red. Si es un canvas o lienzo grafico, usamos vision.
|
|
891
|
+
const nativo = await winHost.send("extract_text", {
|
|
892
|
+
...pinned(choice),
|
|
893
|
+
...(choice.hwnd ? {} : windowArgs(params)),
|
|
894
|
+
});
|
|
895
|
+
if (nativo.ok && Array.isArray(nativo.lines) && nativo.lines.length > 1) {
|
|
896
|
+
return `Texto extraído de "${nativo.title || params.window || "la ventana"}":\n` +
|
|
897
|
+
nativo.lines.map((l) => ` ${l}`).join("\n");
|
|
898
|
+
}
|
|
899
|
+
const out = resolveOut(params.outputPath, "read_text_latest.jpg", cwd);
|
|
900
|
+
const shot = await captureFor(params, out, choice);
|
|
901
|
+
if (!shot.ok)
|
|
902
|
+
return fail(action, shot.error || "no se pudo capturar la ventana");
|
|
903
|
+
if (apiKey) {
|
|
904
|
+
const desc = await describeScreen(apiKey, out, params.question || "Transcribe fielmente todo el texto visible en esta ventana, linea por linea, respetando la distribucion.");
|
|
905
|
+
if (desc.ok) {
|
|
906
|
+
return `Texto extraído visualmente de "${shot.title || params.window || "la pantalla"}":\n${desc.text}`;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return (`La ventana "${shot.title || params.window || "objetivo"}" no expone texto nativo accesible.\n` +
|
|
910
|
+
`Es un lienzo gráfico (canvas, juego o app acelerada). Usa 'screenshot' con 'analyze: true' o proporciona una API key para visión automática.`);
|
|
911
|
+
}
|
|
593
912
|
case "type": {
|
|
594
913
|
if (params.text === undefined)
|
|
595
914
|
return fail(action, 'falta "text".');
|
|
@@ -705,8 +1024,22 @@ async function captureFor(params, outPath, choice) {
|
|
|
705
1024
|
originX: 0,
|
|
706
1025
|
originY: 0,
|
|
707
1026
|
};
|
|
1027
|
+
let rx1 = 0, ry1 = 0, rx2 = 0, ry2 = 0;
|
|
1028
|
+
if (params.region && params.region.length === 4) {
|
|
1029
|
+
[rx1, ry1, rx2, ry2] = params.region;
|
|
1030
|
+
}
|
|
1031
|
+
const drawGrid = params.grid === true;
|
|
708
1032
|
if (params.window) {
|
|
709
|
-
const res = await winHost.send("capture_window", {
|
|
1033
|
+
const res = await winHost.send("capture_window", {
|
|
1034
|
+
...(choice?.hwnd ? { hwnd: choice.hwnd } : { window: params.window }),
|
|
1035
|
+
path: outPath,
|
|
1036
|
+
maxLongEdge: 1568,
|
|
1037
|
+
grid: drawGrid,
|
|
1038
|
+
rx1,
|
|
1039
|
+
ry1,
|
|
1040
|
+
rx2,
|
|
1041
|
+
ry2,
|
|
1042
|
+
}, 45_000);
|
|
710
1043
|
if (!res.ok)
|
|
711
1044
|
return { ...bad, error: res.error };
|
|
712
1045
|
return {
|
|
@@ -722,7 +1055,16 @@ async function captureFor(params, outPath, choice) {
|
|
|
722
1055
|
originY: res.windowY,
|
|
723
1056
|
};
|
|
724
1057
|
}
|
|
725
|
-
const res = await winHost.send("capture_screen", {
|
|
1058
|
+
const res = await winHost.send("capture_screen", {
|
|
1059
|
+
path: outPath,
|
|
1060
|
+
maxLongEdge: 1568,
|
|
1061
|
+
cursor: true,
|
|
1062
|
+
grid: drawGrid,
|
|
1063
|
+
rx1,
|
|
1064
|
+
ry1,
|
|
1065
|
+
rx2,
|
|
1066
|
+
ry2,
|
|
1067
|
+
}, 45_000);
|
|
726
1068
|
if (!res.ok)
|
|
727
1069
|
return { ...bad, error: res.error };
|
|
728
1070
|
return {
|
|
@@ -78,15 +78,18 @@ export const COMPUTER_USE_TOOL = {
|
|
|
78
78
|
type: "function",
|
|
79
79
|
function: {
|
|
80
80
|
name: "computer_use",
|
|
81
|
-
description: "Controla aplicaciones de ESCRITORIO de Windows. Para paginas web usa siempre 'browser', que es mas fiable. " +
|
|
82
|
-
"REGLA DE ORO: nunca hagas clic en una coordenada que no te haya dado antes una herramienta. " +
|
|
81
|
+
description: "Controla aplicaciones de ESCRITORIO de Windows con capacidades multimodales avanzadas (nivel Astra y OpenAI Operator). Para paginas web usa siempre 'browser', que es mas fiable. " +
|
|
82
|
+
"REGLA DE ORO: nunca hagas clic en una coordenada que no te haya dado antes una herramienta o una cuadrícula visual. " +
|
|
83
83
|
"FLUJO CORRECTO: 'list_windows' para saber que hay abierto -> 'ui_snapshot' con la ventana objetivo para listar sus controles reales -> " +
|
|
84
84
|
"'ui_click' / 'ui_type' con el numero de ref. Eso actua en segundo plano, sin mover el raton ni robar el foco al usuario. " +
|
|
85
|
-
"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
"
|
|
85
|
+
"FLUJO COMPUESTO (RAPIDO): usa 'sequence' con 'steps' para encadenar clic -> escritura -> Enter en un solo turno. " +
|
|
86
|
+
"CUANTO CUESTA CADA COSA, que decide cual usar: ui_snapshot tarda unos 40 ms y te devuelve texto que entiendes directo. " +
|
|
87
|
+
"screenshot cuesta una llamada entera a otro modelo -varios segundos- mas los tokens de la imagen. " +
|
|
88
|
+
"O sea que screenshot es unas cien veces mas caro. Usalo SOLO cuando el arbol de accesibilidad no vea el control: lienzos, juegos, imagenes, interfaces dibujadas a mano. " +
|
|
89
|
+
"Para todo lo demas, ui_snapshot y luego find_element. " +
|
|
90
|
+
"CAMBIOS VISUALES: usa 'wait_visual_change' cuando interactúes con lienzos gráficos, juegos, videos o transiciones visuales donde el árbol no cambia. " +
|
|
91
|
+
"CUADRÍCULA VISUAL: usa 'grid: true' en screenshot o find_element para ver marcas de coordenadas exactas. " +
|
|
92
|
+
"Las coordenadas siempre son pixeles fisicos de pantalla o normalizadas 0..1000.",
|
|
90
93
|
parameters: {
|
|
91
94
|
type: "object",
|
|
92
95
|
properties: {
|
|
@@ -100,29 +103,45 @@ export const COMPUTER_USE_TOOL = {
|
|
|
100
103
|
"ui_focus",
|
|
101
104
|
"find_element",
|
|
102
105
|
"screenshot",
|
|
106
|
+
"sequence",
|
|
107
|
+
"scroll_into_view",
|
|
108
|
+
"scroll_to",
|
|
109
|
+
"read_text",
|
|
110
|
+
"get_text",
|
|
103
111
|
"click",
|
|
104
112
|
"left_click",
|
|
113
|
+
"mouse_click",
|
|
105
114
|
"double_click",
|
|
106
115
|
"triple_click",
|
|
107
116
|
"right_click",
|
|
108
117
|
"middle_click",
|
|
109
118
|
"mouse_move",
|
|
119
|
+
"move",
|
|
110
120
|
"left_click_drag",
|
|
121
|
+
"drag",
|
|
111
122
|
"scroll",
|
|
112
123
|
"type",
|
|
113
124
|
"key",
|
|
114
125
|
"hotkey",
|
|
115
126
|
"press",
|
|
127
|
+
"key_press",
|
|
116
128
|
"wait",
|
|
117
129
|
"wait_change",
|
|
130
|
+
"wait_visual_change",
|
|
118
131
|
"cursor_position",
|
|
119
132
|
"focus_window",
|
|
120
133
|
"get_active_window",
|
|
121
134
|
"open_app",
|
|
122
135
|
],
|
|
123
136
|
description: "list_windows: inventario de ventanas. ui_snapshot: lista los controles reales de una ventana con su ref. " +
|
|
124
|
-
"ui_click/ui_type/ui_focus:
|
|
125
|
-
"
|
|
137
|
+
"ui_click/ui_type/ui_focus: actúan sobre un ref sin tocar el ratón (con auto-recuperación por coordenadas). " +
|
|
138
|
+
"sequence: ejecuta una lista de acciones encadenadas ('steps') en una sola llamada, sin esperar turnos adicionales. " +
|
|
139
|
+
"find_element: busca un control por texto en el árbol o visualmente con cuadrícula. " +
|
|
140
|
+
"scroll_into_view: hace scroll iterativo hasta que el control/texto indicado ('target'/'ref') entra en el área visible. " +
|
|
141
|
+
"read_text: extrae texto de la ventana (vía controles nativos Win32 o visión multimodal) cuando el árbol UIA no lo expone. " +
|
|
142
|
+
"wait_change: se suscribe a la ventana y vuelve EN CUANTO cambia algo, con el estado nuevo. Usalo en vez de wait siempre que esperes una reaccion: wait duerme un tiempo fijo (corto si la app tarda, desperdiciado si no). " +
|
|
143
|
+
"wait_visual_change: detecta cambios de píxeles en pantalla/ventana/región (para lienzos, juegos y transiciones). " +
|
|
144
|
+
"screenshot: captura ventana o pantalla completa (soporta grid:true y region:[x1,y1,x2,y2]).",
|
|
126
145
|
},
|
|
127
146
|
window: {
|
|
128
147
|
type: "string",
|
|
@@ -137,15 +156,17 @@ export const COMPUTER_USE_TOOL = {
|
|
|
137
156
|
ref: { type: "integer", description: "Numero de control devuelto por ui_snapshot." },
|
|
138
157
|
query: { type: "string", description: "Texto del control a buscar en find_element." },
|
|
139
158
|
filter: { type: "string", description: "Filtra ui_snapshot o list_windows por texto." },
|
|
159
|
+
target: { type: "string", description: "Texto o ref objetivo para scroll_into_view." },
|
|
160
|
+
maxAttempts: { type: "integer", description: "Máximo de intentos de scroll para scroll_into_view (por defecto 5, tope 10)." },
|
|
140
161
|
max: { type: "integer", description: "Maximo de controles en ui_snapshot (por defecto 120)." },
|
|
141
162
|
coordinate: {
|
|
142
163
|
type: "array",
|
|
143
|
-
items: { type: "
|
|
144
|
-
description: "[x, y] en
|
|
164
|
+
items: { type: "number" },
|
|
165
|
+
description: "[x, y] en píxeles físicos o coordenadas normalizadas 0..1000.",
|
|
145
166
|
},
|
|
146
|
-
x: { type: "
|
|
147
|
-
y: { type: "
|
|
148
|
-
start_coordinate: { type: "array", items: { type: "
|
|
167
|
+
x: { type: "number", description: "Coordenada X en píxeles o normalizada." },
|
|
168
|
+
y: { type: "number", description: "Coordenada Y en píxeles o normalizada." },
|
|
169
|
+
start_coordinate: { type: "array", items: { type: "number" }, description: "[x, y] de inicio del arrastre." },
|
|
149
170
|
text: { type: "string", description: "Texto a escribir." },
|
|
150
171
|
keys: {
|
|
151
172
|
type: "string",
|
|
@@ -160,6 +181,10 @@ export const COMPUTER_USE_TOOL = {
|
|
|
160
181
|
outputPath: { type: "string", description: "Ruta donde guardar la captura." },
|
|
161
182
|
question: { type: "string", description: "Pregunta concreta para el analisis visual de la captura." },
|
|
162
183
|
analyze: { type: "boolean", description: "false para guardar la captura sin gastar tokens de vision." },
|
|
184
|
+
grid: { type: "boolean", description: "true dibuja una cuadrícula de coordenadas sobre la captura para facilitar la localización visual exacta." },
|
|
185
|
+
region: { type: "array", items: { type: "integer" }, description: "[x1, y1, x2, y2] área de interés para captura o espera visual a máxima resolución." },
|
|
186
|
+
steps: { type: "array", items: { type: "object" }, description: "Lista de acciones encadenadas para action='sequence'." },
|
|
187
|
+
normalized: { type: "boolean", description: "true si las coordenadas están en escala 0..1000 estilo Operator/Astra." },
|
|
163
188
|
appName: { type: "string", description: "Aplicacion o URL a lanzar con open_app." },
|
|
164
189
|
},
|
|
165
190
|
required: ["action"],
|