chocolatito-code 1.6.13 → 1.6.14
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 +9 -2
- package/dist/config/permissions.js +4 -0
- package/dist/index.js +129 -24
- 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/fondo.d.ts +85 -0
- package/dist/servidor/fondo.js +134 -0
- package/dist/servidor/pagina.js +266 -2
- package/dist/servidor/puente.d.ts +13 -0
- package/dist/servidor/puente.js +11 -0
- package/dist/servidor/servidor.d.ts +6 -0
- package/dist/servidor/servidor.js +129 -6
- 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/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
|
@@ -28,10 +28,12 @@ export interface LocateResult {
|
|
|
28
28
|
found?: boolean;
|
|
29
29
|
reason?: string;
|
|
30
30
|
error?: string;
|
|
31
|
+
bbox?: [number, number, number, number];
|
|
31
32
|
}
|
|
32
33
|
/**
|
|
33
34
|
* Localiza un elemento y devuelve su centro EN PIXELES DE LA IMAGEN.
|
|
34
|
-
*
|
|
35
|
-
*
|
|
35
|
+
* Soporta modelos multimodales (DeepSeek Vision, GPT-4o, Claude, Gemini Astra/Operator),
|
|
36
|
+
* coordenadas directas en píxeles, cuadrículas visuales, bounding boxes [x1, y1, x2, y2]
|
|
37
|
+
* o box_2d normalizado [ymin, xmin, ymax, xmax].
|
|
36
38
|
*/
|
|
37
39
|
export declare function locateElement(apiKey: string, imagePath: string, target: string, imageWidth: number, imageHeight: number): Promise<LocateResult>;
|
|
@@ -122,26 +122,29 @@ async function ask(apiKey, prompt, imagePath, maxTokens) {
|
|
|
122
122
|
/** Describe lo que se ve, orientado a decidir el siguiente paso. */
|
|
123
123
|
export async function describeScreen(apiKey, imagePath, question) {
|
|
124
124
|
const q = question && question.trim().length > 0 ? question.trim() : "Describe el estado actual de la pantalla.";
|
|
125
|
-
const prompt = `Eres los ojos de un agente que controla este ordenador
|
|
126
|
-
`de forma concreta y sin adornos.\n\n` +
|
|
125
|
+
const prompt = `Eres los ojos de un agente avanzado que controla este ordenador (estilo Google Project Astra y OpenAI Operator). ` +
|
|
126
|
+
`Observa la captura y responde en español, de forma concreta, estructurada y sin adornos.\n\n` +
|
|
127
127
|
`Pregunta: ${q}\n\n` +
|
|
128
|
-
`Incluye siempre:
|
|
129
|
-
`(botones, campos,
|
|
130
|
-
`
|
|
131
|
-
`
|
|
128
|
+
`Incluye siempre: qué aplicación y qué pantalla o diálogo concreto se ve, qué elementos accionables hay ` +
|
|
129
|
+
`(botones, campos, pestañas, menús) con su texto exacto, coordenadas de cuadrícula si están visibles, ` +
|
|
130
|
+
`y si hay diálogos modales, alertas, spinners de carga o errores. ` +
|
|
131
|
+
`Si algo está a medio cargar, dilo explícitamente. No inventes elementos que no aparezcan.\n` +
|
|
132
|
+
`Sé conciso: máximo 14 líneas, directo al grano.`;
|
|
132
133
|
return ask(apiKey, prompt, imagePath, 1600);
|
|
133
134
|
}
|
|
134
135
|
/**
|
|
135
136
|
* Localiza un elemento y devuelve su centro EN PIXELES DE LA IMAGEN.
|
|
136
|
-
*
|
|
137
|
-
*
|
|
137
|
+
* Soporta modelos multimodales (DeepSeek Vision, GPT-4o, Claude, Gemini Astra/Operator),
|
|
138
|
+
* coordenadas directas en píxeles, cuadrículas visuales, bounding boxes [x1, y1, x2, y2]
|
|
139
|
+
* o box_2d normalizado [ymin, xmin, ymax, xmax].
|
|
138
140
|
*/
|
|
139
141
|
export async function locateElement(apiKey, imagePath, target, imageWidth, imageHeight) {
|
|
140
|
-
const prompt = `Observa la captura de pantalla
|
|
142
|
+
const prompt = `Observa la captura de pantalla de ${imageWidth} x ${imageHeight} píxeles.\n` +
|
|
143
|
+
`Si la imagen tiene una cuadrícula de coordenadas con números (x,y), úsalos como referencia precisa.\n\n` +
|
|
141
144
|
`Localiza este elemento: "${target}"\n\n` +
|
|
142
|
-
`Responde
|
|
143
|
-
`{"found": true, "x": <centro horizontal en
|
|
144
|
-
`Si el elemento no aparece en la captura, responde {"found": false, "x": 0, "y": 0, "reason": "<por
|
|
145
|
+
`Responde ÚNICAMENTE con un objeto JSON, sin texto alrededor y sin bloques de código:\n` +
|
|
146
|
+
`{"found": true, "x": <centro horizontal en píxeles>, "y": <centro vertical en píxeles>, "reason": "<qué viste>", "bbox": [<x1>, <y1>, <x2>, <y2>]}\n` +
|
|
147
|
+
`Si el elemento no aparece en la captura, responde {"found": false, "x": 0, "y": 0, "reason": "<por qué>"}.\n` +
|
|
145
148
|
`El origen (0,0) es la esquina superior izquierda de la imagen. No inventes: si dudas, found=false.`;
|
|
146
149
|
const res = await ask(apiKey, prompt, imagePath, 900);
|
|
147
150
|
if (!res.ok)
|
|
@@ -152,10 +155,31 @@ export async function locateElement(apiKey, imagePath, target, imageWidth, image
|
|
|
152
155
|
return { ok: false, error: `El modelo de vision no devolvio JSON: ${raw.slice(0, 200)}` };
|
|
153
156
|
try {
|
|
154
157
|
const parsed = JSON.parse(match[0]);
|
|
155
|
-
|
|
156
|
-
|
|
158
|
+
let x = Number(parsed.x);
|
|
159
|
+
let y = Number(parsed.y);
|
|
160
|
+
// Soporte para box_2d estilo Gemini/Astra: [ymin, xmin, ymax, xmax] normalizado 0..1000
|
|
161
|
+
if ((!Number.isFinite(x) || !Number.isFinite(y) || (x === 0 && y === 0)) && Array.isArray(parsed.box_2d) && parsed.box_2d.length === 4) {
|
|
162
|
+
const [ymin, xmin, ymax, xmax] = parsed.box_2d.map(Number);
|
|
163
|
+
x = Math.round(((xmin + xmax) / 2.0 / 1000.0) * imageWidth);
|
|
164
|
+
y = Math.round(((ymin + ymax) / 2.0 / 1000.0) * imageHeight);
|
|
165
|
+
}
|
|
166
|
+
else if (Array.isArray(parsed.bbox) && parsed.bbox.length === 4 && (!Number.isFinite(x) || !Number.isFinite(y))) {
|
|
167
|
+
const [x1, y1, x2, y2] = parsed.bbox.map(Number);
|
|
168
|
+
x = Math.round((x1 + x2) / 2.0);
|
|
169
|
+
y = Math.round((y1 + y2) / 2.0);
|
|
170
|
+
}
|
|
171
|
+
// Coordenadas normalizadas 0.0 a 1.0
|
|
172
|
+
if (x > 0 && x <= 1.0 && y > 0 && y <= 1.0) {
|
|
173
|
+
x = Math.round(x * imageWidth);
|
|
174
|
+
y = Math.round(y * imageHeight);
|
|
175
|
+
}
|
|
176
|
+
else if (parsed.normalized === true) {
|
|
177
|
+
x = Math.round((x / 1000.0) * imageWidth);
|
|
178
|
+
y = Math.round((y / 1000.0) * imageHeight);
|
|
179
|
+
}
|
|
180
|
+
x = Math.round(x);
|
|
181
|
+
y = Math.round(y);
|
|
157
182
|
const found = parsed.found === true && Number.isFinite(x) && Number.isFinite(y);
|
|
158
|
-
// Un punto fuera de la imagen es una alucinacion, no un acierto.
|
|
159
183
|
const inside = x >= 0 && y >= 0 && x <= imageWidth && y <= imageHeight;
|
|
160
184
|
return {
|
|
161
185
|
ok: true,
|
|
@@ -163,6 +187,7 @@ export async function locateElement(apiKey, imagePath, target, imageWidth, image
|
|
|
163
187
|
x,
|
|
164
188
|
y,
|
|
165
189
|
reason: typeof parsed.reason === "string" ? parsed.reason : "",
|
|
190
|
+
bbox: Array.isArray(parsed.bbox) ? parsed.bbox : undefined,
|
|
166
191
|
};
|
|
167
192
|
}
|
|
168
193
|
catch (err) {
|
|
@@ -52,6 +52,8 @@ public static class Choco
|
|
|
52
52
|
[DllImport("user32.dll")] static extern bool ClientToScreen(IntPtr h, ref POINT p);
|
|
53
53
|
[DllImport("user32.dll")] static extern bool ScreenToClient(IntPtr h, ref POINT p);
|
|
54
54
|
[DllImport("user32.dll")] static extern bool EnumWindows(EnumProc cb, IntPtr l);
|
|
55
|
+
[DllImport("user32.dll")] static extern bool EnumChildWindows(IntPtr hWndParent, EnumProc lpEnumFunc, IntPtr lParam);
|
|
56
|
+
[DllImport("user32.dll", CharSet = CharSet.Auto)] static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, StringBuilder lParam);
|
|
55
57
|
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
|
|
56
58
|
[DllImport("user32.dll")] static extern bool PrintWindow(IntPtr h, IntPtr hdc, uint flags);
|
|
57
59
|
[DllImport("user32.dll")] static extern bool PostMessage(IntPtr h, uint msg, IntPtr w, IntPtr l);
|
|
@@ -304,49 +306,102 @@ public static class Choco
|
|
|
304
306
|
return scale;
|
|
305
307
|
}
|
|
306
308
|
|
|
307
|
-
|
|
309
|
+
static void DrawCoordinateGrid(Graphics g, int width, int height, int originX, int originY)
|
|
310
|
+
{
|
|
311
|
+
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
|
|
312
|
+
using (Pen gridPen = new Pen(Color.FromArgb(100, 230, 60, 60), 1))
|
|
313
|
+
using (Brush textBrush = new SolidBrush(Color.FromArgb(245, 255, 255, 255)))
|
|
314
|
+
using (Brush bgBrush = new SolidBrush(Color.FromArgb(160, 20, 20, 20)))
|
|
315
|
+
using (Font font = new Font(FontFamily.GenericMonospace, 8.5f, FontStyle.Bold))
|
|
316
|
+
{
|
|
317
|
+
gridPen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dot;
|
|
318
|
+
int step = 100;
|
|
319
|
+
for (int x = 0; x < width; x += step)
|
|
320
|
+
{
|
|
321
|
+
g.DrawLine(gridPen, x, 0, x, height);
|
|
322
|
+
}
|
|
323
|
+
for (int y = 0; y < height; y += step)
|
|
324
|
+
{
|
|
325
|
+
g.DrawLine(gridPen, 0, y, width, y);
|
|
326
|
+
}
|
|
327
|
+
for (int x = 0; x < width; x += step)
|
|
328
|
+
{
|
|
329
|
+
for (int y = 0; y < height; y += step)
|
|
330
|
+
{
|
|
331
|
+
if (x % 200 == 0 && y % 200 == 0 && x + 70 < width && y + 16 < height)
|
|
332
|
+
{
|
|
333
|
+
string lbl = (originX + x) + "," + (originY + y);
|
|
334
|
+
SizeF sz = g.MeasureString(lbl, font);
|
|
335
|
+
g.FillRectangle(bgBrush, x + 2, y + 2, sz.Width + 2, sz.Height);
|
|
336
|
+
g.DrawString(lbl, font, textBrush, x + 3, y + 2);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
public static string CaptureScreen(string path, int maxLongEdge, bool drawCursor, bool drawGrid, int rx1, int ry1, int rx2, int ry2)
|
|
308
344
|
{
|
|
309
345
|
int vx = GetSystemMetrics(76), vy = GetSystemMetrics(77);
|
|
310
346
|
int vw = GetSystemMetrics(78), vh = GetSystemMetrics(79);
|
|
311
|
-
|
|
347
|
+
int sx = vx, sy = vy, sw = vw, sh = vh;
|
|
348
|
+
if (rx2 > rx1 && ry2 > ry1)
|
|
349
|
+
{
|
|
350
|
+
sx = vx + rx1; sy = vy + ry1;
|
|
351
|
+
sw = Math.Min(vw, rx2 - rx1); sh = Math.Min(vh, ry2 - ry1);
|
|
352
|
+
}
|
|
353
|
+
if (sw <= 0 || sh <= 0) return "{\"ok\":false,\"error\":\"area invalida para captura\"}";
|
|
354
|
+
|
|
355
|
+
using (Bitmap bmp = new Bitmap(sw, sh, PixelFormat.Format32bppArgb))
|
|
312
356
|
{
|
|
313
357
|
using (Graphics g = Graphics.FromImage(bmp))
|
|
314
358
|
{
|
|
315
|
-
g.CopyFromScreen(
|
|
359
|
+
g.CopyFromScreen(sx, sy, 0, 0, new Size(sw, sh), CopyPixelOperation.SourceCopy);
|
|
316
360
|
if (drawCursor)
|
|
317
361
|
{
|
|
318
362
|
POINT c; GetCursorPos(out c);
|
|
319
|
-
|
|
320
|
-
using (Pen p = new Pen(Color.FromArgb(255, 235, 75, 75), 3))
|
|
363
|
+
if (c.X >= sx && c.X <= sx + sw && c.Y >= sy && c.Y <= sy + sh)
|
|
321
364
|
{
|
|
322
|
-
g.
|
|
323
|
-
|
|
324
|
-
|
|
365
|
+
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
|
|
366
|
+
using (Pen p = new Pen(Color.FromArgb(255, 235, 75, 75), 3))
|
|
367
|
+
{
|
|
368
|
+
int relX = c.X - sx;
|
|
369
|
+
int relY = c.Y - sy;
|
|
370
|
+
g.DrawEllipse(p, relX - 16, relY - 16, 32, 32);
|
|
371
|
+
g.DrawLine(p, relX - 24, relY, relX + 24, relY);
|
|
372
|
+
g.DrawLine(p, relX, relY - 24, relX, relY + 24);
|
|
373
|
+
}
|
|
325
374
|
}
|
|
326
375
|
}
|
|
376
|
+
if (drawGrid)
|
|
377
|
+
{
|
|
378
|
+
DrawCoordinateGrid(g, sw, sh, sx, sy);
|
|
379
|
+
}
|
|
327
380
|
}
|
|
328
381
|
double s = SaveScaled(bmp, path, maxLongEdge);
|
|
329
|
-
return "{\"ok\":true,\"path\":" + Q(path) + ",\"screenWidth\":" + N(
|
|
330
|
-
",\"originX\":" + N(
|
|
331
|
-
",\"imageWidth\":" + N((int)Math.Round(
|
|
382
|
+
return "{\"ok\":true,\"path\":" + Q(path) + ",\"screenWidth\":" + N(sw) + ",\"screenHeight\":" + N(sh) +
|
|
383
|
+
",\"originX\":" + N(sx) + ",\"originY\":" + N(sy) +
|
|
384
|
+
",\"imageWidth\":" + N((int)Math.Round(sw * s)) + ",\"imageHeight\":" + N((int)Math.Round(sh * s)) +
|
|
332
385
|
",\"scale\":" + s.ToString("0.#####", CultureInfo.InvariantCulture) + "}";
|
|
333
|
-
|
|
386
|
+
}
|
|
334
387
|
}
|
|
335
388
|
|
|
336
389
|
// Captura UNA ventana concreta sin traerla al frente.
|
|
337
390
|
// PrintWindow(PW_RENDERFULLCONTENT=2) pide a la ventana que se redibuje en
|
|
338
391
|
// nuestro DC, asi funciona aunque este tapada o en segundo plano.
|
|
339
|
-
public static string CaptureWindow(IntPtr h, string path, int maxLongEdge, bool clientOnly)
|
|
392
|
+
public static string CaptureWindow(IntPtr h, string path, int maxLongEdge, bool clientOnly, bool drawGrid, int rx1, int ry1, int rx2, int ry2)
|
|
340
393
|
{
|
|
341
394
|
if (h == IntPtr.Zero) return "{\"ok\":false,\"error\":\"ventana no encontrada\"}";
|
|
342
395
|
RECT r;
|
|
343
396
|
if (clientOnly) { GetClientRect(h, out r); } else { GetWindowRect(h, out r); }
|
|
344
397
|
int w = r.Right - r.Left, hh = r.Bottom - r.Top;
|
|
345
398
|
if (w <= 0 || hh <= 0) return "{\"ok\":false,\"error\":\"ventana sin area visible (minimizada?)\"}";
|
|
346
|
-
|
|
399
|
+
RECT wr2; GetWindowRect(h, out wr2);
|
|
400
|
+
|
|
401
|
+
using (Bitmap full = new Bitmap(w, hh, PixelFormat.Format32bppArgb))
|
|
347
402
|
{
|
|
348
403
|
bool printed = false;
|
|
349
|
-
using (Graphics g = Graphics.FromImage(
|
|
404
|
+
using (Graphics g = Graphics.FromImage(full))
|
|
350
405
|
{
|
|
351
406
|
IntPtr hdc = g.GetHdc();
|
|
352
407
|
try { printed = PrintWindow(h, hdc, 2); }
|
|
@@ -354,19 +409,52 @@ public static class Choco
|
|
|
354
409
|
}
|
|
355
410
|
// Chrome/Electron a veces devuelven negro con PrintWindow: si sale casi
|
|
356
411
|
// todo negro y la ventana esta visible, caemos a copia de pantalla.
|
|
357
|
-
if (!printed || IsMostlyBlank(
|
|
412
|
+
if (!printed || IsMostlyBlank(full))
|
|
358
413
|
{
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
414
|
+
using (Graphics g2 = Graphics.FromImage(full))
|
|
415
|
+
g2.CopyFromScreen(wr2.Left, wr2.Top, 0, 0, new Size(w, hh), CopyPixelOperation.SourceCopy);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
int cw = w, ch = hh, cx = 0, cy = 0;
|
|
419
|
+
if (rx2 > rx1 && ry2 > ry1)
|
|
420
|
+
{
|
|
421
|
+
cx = Math.Max(0, rx1); cy = Math.Max(0, ry1);
|
|
422
|
+
cw = Math.Min(w - cx, rx2 - rx1); ch = Math.Min(hh - cy, ry2 - ry1);
|
|
423
|
+
}
|
|
424
|
+
if (cw <= 0 || ch <= 0) return "{\"ok\":false,\"error\":\"region fuera de los limites de la ventana\"}";
|
|
425
|
+
|
|
426
|
+
Bitmap targetBmp = full;
|
|
427
|
+
bool isCropped = (cx > 0 || cy > 0 || cw < w || ch < hh);
|
|
428
|
+
if (isCropped)
|
|
429
|
+
{
|
|
430
|
+
Bitmap crop = new Bitmap(cw, ch, PixelFormat.Format32bppArgb);
|
|
431
|
+
using (Graphics gCrop = Graphics.FromImage(crop))
|
|
432
|
+
{
|
|
433
|
+
gCrop.DrawImage(full, new Rectangle(0, 0, cw, ch), new Rectangle(cx, cy, cw, ch), GraphicsUnit.Pixel);
|
|
434
|
+
}
|
|
435
|
+
targetBmp = crop;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
try
|
|
439
|
+
{
|
|
440
|
+
if (drawGrid)
|
|
441
|
+
{
|
|
442
|
+
using (Graphics gGrid = Graphics.FromImage(targetBmp))
|
|
443
|
+
{
|
|
444
|
+
DrawCoordinateGrid(gGrid, cw, ch, wr2.Left + cx, wr2.Top + cy);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
double s = SaveScaled(targetBmp, path, maxLongEdge);
|
|
448
|
+
return "{\"ok\":true,\"path\":" + Q(path) + ",\"title\":" + Q(Title(h)) +
|
|
449
|
+
",\"windowX\":" + N(wr2.Left + cx) + ",\"windowY\":" + N(wr2.Top + cy) +
|
|
450
|
+
",\"windowWidth\":" + N(cw) + ",\"windowHeight\":" + N(ch) +
|
|
451
|
+
",\"imageWidth\":" + N((int)Math.Round(cw * s)) + ",\"imageHeight\":" + N((int)Math.Round(ch * s)) +
|
|
452
|
+
",\"scale\":" + s.ToString("0.#####", CultureInfo.InvariantCulture) + "}";
|
|
453
|
+
}
|
|
454
|
+
finally
|
|
455
|
+
{
|
|
456
|
+
if (isCropped && targetBmp != null) targetBmp.Dispose();
|
|
362
457
|
}
|
|
363
|
-
double s = SaveScaled(bmp, path, maxLongEdge);
|
|
364
|
-
RECT wr2; GetWindowRect(h, out wr2);
|
|
365
|
-
return "{\"ok\":true,\"path\":" + Q(path) + ",\"title\":" + Q(Title(h)) +
|
|
366
|
-
",\"windowX\":" + N(wr2.Left) + ",\"windowY\":" + N(wr2.Top) +
|
|
367
|
-
",\"windowWidth\":" + N(w) + ",\"windowHeight\":" + N(hh) +
|
|
368
|
-
",\"imageWidth\":" + N((int)Math.Round(w * s)) + ",\"imageHeight\":" + N((int)Math.Round(hh * s)) +
|
|
369
|
-
",\"scale\":" + s.ToString("0.#####", CultureInfo.InvariantCulture) + "}";
|
|
370
458
|
}
|
|
371
459
|
}
|
|
372
460
|
|
|
@@ -672,6 +760,98 @@ public static class Choco
|
|
|
672
760
|
}
|
|
673
761
|
}
|
|
674
762
|
|
|
763
|
+
public static string WaitVisualChange(IntPtr hwnd, int timeoutMs, int rx1, int ry1, int rx2, int ry2)
|
|
764
|
+
{
|
|
765
|
+
if (timeoutMs < 100) timeoutMs = 100;
|
|
766
|
+
if (timeoutMs > 30000) timeoutMs = 30000;
|
|
767
|
+
int vx = 0, vy = 0, vw = 0, vh = 0;
|
|
768
|
+
if (hwnd != IntPtr.Zero)
|
|
769
|
+
{
|
|
770
|
+
RECT r; GetWindowRect(hwnd, out r);
|
|
771
|
+
vx = r.Left; vy = r.Top; vw = r.Right - r.Left; vh = r.Bottom - r.Top;
|
|
772
|
+
}
|
|
773
|
+
else
|
|
774
|
+
{
|
|
775
|
+
vx = GetSystemMetrics(76); vy = GetSystemMetrics(77);
|
|
776
|
+
vw = GetSystemMetrics(78); vh = GetSystemMetrics(79);
|
|
777
|
+
}
|
|
778
|
+
if (rx2 > rx1 && ry2 > ry1)
|
|
779
|
+
{
|
|
780
|
+
vx += rx1; vy += ry1;
|
|
781
|
+
vw = Math.Min(vw, rx2 - rx1); vh = Math.Min(vh, ry2 - ry1);
|
|
782
|
+
}
|
|
783
|
+
if (vw <= 2 || vh <= 2) return "{\"ok\":false,\"error\":\"area invalida para cambio visual\"}";
|
|
784
|
+
|
|
785
|
+
int sampleW = Math.Min(vw, 64);
|
|
786
|
+
int sampleH = Math.Min(vh, 64);
|
|
787
|
+
byte[] prev = SamplePixels(vx, vy, vw, vh, sampleW, sampleH);
|
|
788
|
+
|
|
789
|
+
int elapsed = 0;
|
|
790
|
+
int interval = 120;
|
|
791
|
+
while (elapsed < timeoutMs)
|
|
792
|
+
{
|
|
793
|
+
Thread.Sleep(interval);
|
|
794
|
+
elapsed += interval;
|
|
795
|
+
byte[] curr = SamplePixels(vx, vy, vw, vh, sampleW, sampleH);
|
|
796
|
+
int diff = 0;
|
|
797
|
+
int total = curr.Length / 3;
|
|
798
|
+
for (int i = 0; i < curr.Length; i += 3)
|
|
799
|
+
{
|
|
800
|
+
int dr = Math.Abs((int)curr[i] - (int)prev[i]);
|
|
801
|
+
int dg = Math.Abs((int)curr[i + 1] - (int)prev[i + 1]);
|
|
802
|
+
int db = Math.Abs((int)curr[i + 2] - (int)prev[i + 2]);
|
|
803
|
+
if (dr + dg + db > 30) diff++;
|
|
804
|
+
}
|
|
805
|
+
double pct = (double)diff / (double)total;
|
|
806
|
+
if (pct > 0.015)
|
|
807
|
+
{
|
|
808
|
+
return "{\"ok\":true,\"changed\":true,\"diffPercent\":" + (pct * 100).ToString("0.#", CultureInfo.InvariantCulture) + ",\"elapsed\":" + N(elapsed) + "}";
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return "{\"ok\":true,\"changed\":false,\"waited\":" + N(timeoutMs) + "}";
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
static byte[] SamplePixels(int x, int y, int w, int h, int sw, int sh)
|
|
815
|
+
{
|
|
816
|
+
byte[] bytes = new byte[sw * sh * 3];
|
|
817
|
+
try
|
|
818
|
+
{
|
|
819
|
+
using (Bitmap full = new Bitmap(w, h, PixelFormat.Format24bppRgb))
|
|
820
|
+
{
|
|
821
|
+
using (Graphics g = Graphics.FromImage(full))
|
|
822
|
+
{
|
|
823
|
+
g.CopyFromScreen(x, y, 0, 0, new Size(w, h), CopyPixelOperation.SourceCopy);
|
|
824
|
+
}
|
|
825
|
+
using (Bitmap thumb = new Bitmap(sw, sh, PixelFormat.Format24bppRgb))
|
|
826
|
+
{
|
|
827
|
+
using (Graphics g2 = Graphics.FromImage(thumb))
|
|
828
|
+
{
|
|
829
|
+
g2.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.Bilinear;
|
|
830
|
+
g2.DrawImage(full, 0, 0, sw, sh);
|
|
831
|
+
}
|
|
832
|
+
BitmapData bd = thumb.LockBits(new Rectangle(0, 0, sw, sh), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
|
|
833
|
+
try
|
|
834
|
+
{
|
|
835
|
+
int stride = bd.Stride;
|
|
836
|
+
IntPtr ptr = bd.Scan0;
|
|
837
|
+
byte[] row = new byte[Math.Abs(stride)];
|
|
838
|
+
for (int rowIdx = 0; rowIdx < sh; rowIdx++)
|
|
839
|
+
{
|
|
840
|
+
Marshal.Copy(new IntPtr(ptr.ToInt64() + (rowIdx * stride)), row, 0, sw * 3);
|
|
841
|
+
Array.Copy(row, 0, bytes, rowIdx * sw * 3, sw * 3);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
finally
|
|
845
|
+
{
|
|
846
|
+
thumb.UnlockBits(bd);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
catch { }
|
|
852
|
+
return bytes;
|
|
853
|
+
}
|
|
854
|
+
|
|
675
855
|
public static string RefRect(int id)
|
|
676
856
|
{
|
|
677
857
|
AutomationElement e = Ref(id);
|
|
@@ -702,11 +882,71 @@ public static class Choco
|
|
|
702
882
|
if (e.TryGetCurrentPattern(SelectionItemPattern.Pattern, out p)) { ((SelectionItemPattern)p).Select(); return "{\"ok\":true,\"via\":\"select\",\"label\":" + Q(label) + "}"; }
|
|
703
883
|
if (e.TryGetCurrentPattern(TogglePattern.Pattern, out p)) { ((TogglePattern)p).Toggle(); return "{\"ok\":true,\"via\":\"toggle\",\"label\":" + Q(label) + "}"; }
|
|
704
884
|
if (e.TryGetCurrentPattern(ExpandCollapsePattern.Pattern, out p)) { ((ExpandCollapsePattern)p).Expand(); return "{\"ok\":true,\"via\":\"expand\",\"label\":" + Q(label) + "}"; }
|
|
705
|
-
|
|
885
|
+
int cx = 0, cy = 0;
|
|
886
|
+
try {
|
|
887
|
+
System.Windows.Rect r = e.Current.BoundingRectangle;
|
|
888
|
+
cx = (int)Math.Round(r.X + r.Width / 2.0);
|
|
889
|
+
cy = (int)Math.Round(r.Y + r.Height / 2.0);
|
|
890
|
+
} catch { }
|
|
891
|
+
return "{\"ok\":false,\"noPattern\":true,\"x\":" + N(cx) + ",\"y\":" + N(cy) + ",\"label\":" + Q(label) + ",\"error\":" + Q("El elemento " + label + " no expone patron activable; usa click sobre sus coordenadas.") + "}";
|
|
706
892
|
}
|
|
707
893
|
catch (Exception ex) { return "{\"ok\":false,\"error\":" + Q(ex.Message) + "}"; }
|
|
708
894
|
}
|
|
709
895
|
|
|
896
|
+
// POR QUE: Formularios y listas largas no caben en una sola pantalla. Si un control
|
|
897
|
+
// soporta ScrollItemPattern, lo traemos al area visible directamente sin mover el raton.
|
|
898
|
+
public static string RefScrollIntoView(int id)
|
|
899
|
+
{
|
|
900
|
+
AutomationElement e = Ref(id);
|
|
901
|
+
if (e == null) return "{\"ok\":false,\"error\":\"ref inexistente; repite ui_snapshot\"}";
|
|
902
|
+
try
|
|
903
|
+
{
|
|
904
|
+
object sp;
|
|
905
|
+
if (e.TryGetCurrentPattern(ScrollItemPattern.Pattern, out sp))
|
|
906
|
+
{
|
|
907
|
+
((ScrollItemPattern)sp).ScrollIntoView();
|
|
908
|
+
return "{\"ok\":true,\"via\":\"scroll_item_pattern\"}";
|
|
909
|
+
}
|
|
910
|
+
return "{\"ok\":false,\"error\":\"el elemento no admite ScrollItemPattern\"}";
|
|
911
|
+
}
|
|
912
|
+
catch (Exception ex) { return "{\"ok\":false,\"error\":" + Q(ex.Message) + "}"; }
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// POR QUE: Ventanas antiguas (WinForms, Delphi, MFC) o Electron con accesibilidad rota
|
|
916
|
+
// devuelven un árbol UIA vacío, dejando al modelo a ciegas. Este método recupera
|
|
917
|
+
// el texto de los controles nativos Win32 (editores, etiquetas, botones) directamente.
|
|
918
|
+
public static string ExtractWindowText(IntPtr hwnd)
|
|
919
|
+
{
|
|
920
|
+
if (hwnd == IntPtr.Zero) return "{\"ok\":false,\"error\":\"ventana no encontrada\"}";
|
|
921
|
+
List<string> lines = new List<string>();
|
|
922
|
+
HashSet<string> seen = new HashSet<string>();
|
|
923
|
+
string title = Title(hwnd);
|
|
924
|
+
if (!string.IsNullOrEmpty(title)) { lines.Add(title); seen.Add(title); }
|
|
925
|
+
|
|
926
|
+
EnumChildWindows(hwnd, (child, lParam) =>
|
|
927
|
+
{
|
|
928
|
+
if (!IsWindowVisible(child)) return true;
|
|
929
|
+
StringBuilder sb = new StringBuilder(1024);
|
|
930
|
+
GetWindowText(child, sb, 1024);
|
|
931
|
+
string txt = sb.ToString().Trim();
|
|
932
|
+
if (txt.Length == 0)
|
|
933
|
+
{
|
|
934
|
+
SendMessage(child, 0x000D, (IntPtr)1024, sb);
|
|
935
|
+
txt = sb.ToString().Trim();
|
|
936
|
+
}
|
|
937
|
+
if (txt.Length > 0 && !seen.Contains(txt))
|
|
938
|
+
{
|
|
939
|
+
seen.Add(txt);
|
|
940
|
+
lines.Add(txt);
|
|
941
|
+
}
|
|
942
|
+
return true;
|
|
943
|
+
}, IntPtr.Zero);
|
|
944
|
+
|
|
945
|
+
List<string> escaped = new List<string>();
|
|
946
|
+
foreach (string line in lines) escaped.Add(Q(line));
|
|
947
|
+
return "{\"ok\":true,\"title\":" + Q(title) + ",\"lines\":[" + string.Join(",", escaped.ToArray()) + "]}";
|
|
948
|
+
}
|
|
949
|
+
|
|
710
950
|
public static string RefSetValue(int id, string text, bool replace)
|
|
711
951
|
{
|
|
712
952
|
AutomationElement e = Ref(id);
|
|
@@ -1052,16 +1292,47 @@ while ($true) {
|
|
|
1052
1292
|
}
|
|
1053
1293
|
'wait' { Start-Sleep -Milliseconds ([int](Get-Prop $cmd 'ms' 1000)); $res = '{"ok":true}' }
|
|
1054
1294
|
'capture_screen' {
|
|
1055
|
-
$res = [Choco]::CaptureScreen(
|
|
1295
|
+
$res = [Choco]::CaptureScreen(
|
|
1296
|
+
[string](Get-Prop $cmd 'path' ''),
|
|
1297
|
+
[int](Get-Prop $cmd 'maxLongEdge' 1568),
|
|
1298
|
+
[bool](Get-Prop $cmd 'cursor' $true),
|
|
1299
|
+
[bool](Get-Prop $cmd 'grid' $false),
|
|
1300
|
+
[int](Get-Prop $cmd 'rx1' 0),
|
|
1301
|
+
[int](Get-Prop $cmd 'ry1' 0),
|
|
1302
|
+
[int](Get-Prop $cmd 'rx2' 0),
|
|
1303
|
+
[int](Get-Prop $cmd 'ry2' 0)
|
|
1304
|
+
)
|
|
1056
1305
|
}
|
|
1057
1306
|
'capture_window' {
|
|
1058
|
-
$res = [Choco]::CaptureWindow(
|
|
1307
|
+
$res = [Choco]::CaptureWindow(
|
|
1308
|
+
(Resolve-Hwnd $cmd),
|
|
1309
|
+
[string](Get-Prop $cmd 'path' ''),
|
|
1310
|
+
[int](Get-Prop $cmd 'maxLongEdge' 1568),
|
|
1311
|
+
[bool](Get-Prop $cmd 'clientOnly' $false),
|
|
1312
|
+
[bool](Get-Prop $cmd 'grid' $false),
|
|
1313
|
+
[int](Get-Prop $cmd 'rx1' 0),
|
|
1314
|
+
[int](Get-Prop $cmd 'ry1' 0),
|
|
1315
|
+
[int](Get-Prop $cmd 'rx2' 0),
|
|
1316
|
+
[int](Get-Prop $cmd 'ry2' 0)
|
|
1317
|
+
)
|
|
1059
1318
|
}
|
|
1060
1319
|
'ui_snapshot' {
|
|
1061
1320
|
$res = [Choco]::Snapshot((Resolve-Hwnd $cmd), [int](Get-Prop $cmd 'max' 250), [string](Get-Prop $cmd 'filter' ''), [bool](Get-Prop $cmd 'interactiveOnly' $true))
|
|
1062
1321
|
}
|
|
1063
1322
|
'wait_change' { $res = [Choco]::WaitChange((Resolve-Hwnd $cmd), [int](Get-Prop $cmd 'timeoutMs' 3000)) }
|
|
1323
|
+
'wait_visual_change' {
|
|
1324
|
+
$res = [Choco]::WaitVisualChange(
|
|
1325
|
+
(Resolve-Hwnd $cmd),
|
|
1326
|
+
[int](Get-Prop $cmd 'timeoutMs' 3000),
|
|
1327
|
+
[int](Get-Prop $cmd 'rx1' 0),
|
|
1328
|
+
[int](Get-Prop $cmd 'ry1' 0),
|
|
1329
|
+
[int](Get-Prop $cmd 'rx2' 0),
|
|
1330
|
+
[int](Get-Prop $cmd 'ry2' 0)
|
|
1331
|
+
)
|
|
1332
|
+
}
|
|
1064
1333
|
'ui_click' { $res = [Choco]::RefInvoke([int](Get-Prop $cmd 'ref' 0)) }
|
|
1334
|
+
'ui_scroll_into_view' { $res = [Choco]::RefScrollIntoView([int](Get-Prop $cmd 'ref' 0)) }
|
|
1335
|
+
'extract_text' { $res = [Choco]::ExtractWindowText((Resolve-Hwnd $cmd)) }
|
|
1065
1336
|
'ui_set_value' { $res = [Choco]::RefSetValue([int](Get-Prop $cmd 'ref' 0), [string](Get-Prop $cmd 'text' ''), [bool](Get-Prop $cmd 'replace' $false)) }
|
|
1066
1337
|
'ui_focus' { $res = [Choco]::RefFocus([int](Get-Prop $cmd 'ref' 0)) }
|
|
1067
1338
|
'ui_rect' { $res = [Choco]::RefRect([int](Get-Prop $cmd 'ref' 0)) }
|
package/dist/ui/ink/App.js
CHANGED
|
@@ -77,8 +77,28 @@ export function App() {
|
|
|
77
77
|
// un proceso hijo lo suelten por el camino. Ver ink/teclado.ts: es el fallo
|
|
78
78
|
// del "no puedo escribir hasta que pulso Enter".
|
|
79
79
|
useTecladoSiempreNuestro(turno.esperando);
|
|
80
|
-
const
|
|
81
|
-
|
|
80
|
+
const [tamano, setTamano] = useState(() => ({
|
|
81
|
+
cols: Math.max(20, (stdout?.columns && stdout.columns > 0 ? stdout.columns : process.stdout?.columns) || 80),
|
|
82
|
+
filas: Math.max(6, (stdout?.rows && stdout.rows > 0 ? stdout.rows : process.stdout?.rows) || 24),
|
|
83
|
+
}));
|
|
84
|
+
// Adaptar el árbol y todos los marcos dinámicamente cuando se maximiza o redimensiona la terminal
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const alRedimensionar = () => {
|
|
87
|
+
setTamano({
|
|
88
|
+
cols: Math.max(20, (stdout?.columns && stdout.columns > 0 ? stdout.columns : process.stdout?.columns) || 80),
|
|
89
|
+
filas: Math.max(6, (stdout?.rows && stdout.rows > 0 ? stdout.rows : process.stdout?.rows) || 24),
|
|
90
|
+
});
|
|
91
|
+
repintar();
|
|
92
|
+
};
|
|
93
|
+
stdout?.on("resize", alRedimensionar);
|
|
94
|
+
process.stdout?.on("resize", alRedimensionar);
|
|
95
|
+
return () => {
|
|
96
|
+
stdout?.off("resize", alRedimensionar);
|
|
97
|
+
process.stdout?.off("resize", alRedimensionar);
|
|
98
|
+
};
|
|
99
|
+
}, [stdout]);
|
|
100
|
+
const cols = tamano.cols;
|
|
101
|
+
const filas = tamano.filas;
|
|
82
102
|
const permanentes = pendientes();
|
|
83
103
|
const barra = turno.esperando ? [] : lineasDelPie();
|
|
84
104
|
// EL TECHO DE LA ZONA DE ABAJO, Y POR QUÉ SE MIDE TAN DE CERCA
|
|
@@ -94,7 +114,7 @@ export function App() {
|
|
|
94
114
|
const techo = Math.max(1, filas - 1);
|
|
95
115
|
const sitioParaLaCola = Math.max(0, techo - (turno.esperando ? ALTO_DE_LA_CAJA : barra.length));
|
|
96
116
|
const cola = recortarCola(colgando(), cols, sitioParaLaCola);
|
|
97
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: permanentes, children: (linea) => _jsx(Text, { children: conCuerpo(linea.texto) }, linea.id) }), _jsxs(Box, { flexDirection: "column", children: [cola.map((linea, i) => (_jsx(Text, { children: conCuerpo(linea) }, `cola-${i}`))), turno.esperando ? (_jsx(Prompt, { alEnviar: turno.responder, alCancelar: () => turno.responder("/exit"), cwd: turno.cwd, promptSymbol: turno.promptSymbol, avisos: [],
|
|
117
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: permanentes, children: (linea) => _jsx(Text, { children: conCuerpo(linea.texto) }, linea.id) }), _jsxs(Box, { flexDirection: "column", children: [cola.map((linea, i) => (_jsx(Text, { children: conCuerpo(linea) }, `cola-${i}`))), turno.esperando ? (_jsx(Prompt, { cols: cols, filas: filas, alEnviar: turno.responder, alCancelar: () => turno.responder("/exit"), cwd: turno.cwd, promptSymbol: turno.promptSymbol, avisos: [],
|
|
98
118
|
// NO SE DESMONTA EL ÁRBOL AL ENVIAR
|
|
99
119
|
//
|
|
100
120
|
// El prompt nació montándose y desmontándose en cada vuelta, y para
|
package/dist/ui/ink/Prompt.d.ts
CHANGED
|
@@ -56,5 +56,9 @@ export interface PromptProps {
|
|
|
56
56
|
* como si estuviera solo.
|
|
57
57
|
*/
|
|
58
58
|
alto?: number;
|
|
59
|
+
/** Columnas disponibles en el terminal. */
|
|
60
|
+
cols?: number;
|
|
61
|
+
/** Filas totales de la ventana del terminal. */
|
|
62
|
+
filas?: number;
|
|
59
63
|
}
|
|
60
|
-
export declare function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, cerrarAlEnviar, alto, }: PromptProps): React.ReactElement;
|
|
64
|
+
export declare function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, cerrarAlEnviar, alto, cols: colsProp, filas: filasProp, }: PromptProps): React.ReactElement;
|