feat(browser): auto-commit con 3 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-26 23:30:35 +02:00
parent d7387d9d2c
commit 2ff111bae4
3 changed files with 74 additions and 34 deletions
+47 -21
View File
@@ -11,15 +11,15 @@ anti-envio-al-contacto-equivocado:
1. Abre el chat por su nombre exacto (`open_first=True`). Si no abre, aborta. Con
`open_first=False`, asume el chat abierto pero VERIFICA que el aria-label del composer
contiene el nombre; si no, aborta por seguridad.
2. Hace click de raton real en el boton "Adjuntar" del footer: esto expone el
`<input type=file>` "vivo" que escucha la SPA (antes de pulsarlo el input persistente
no dispara el preview).
2. ASEGURA que el menu "Adjuntar" esta ABIERTO (es un toggle `aria-expanded`: clickar
cuando ya esta abierto lo cierra). Solo entonces el `<input type=file>` queda "vivo".
3. Asigna la imagen al input via `cdp_set_file_input` (`DOM.setFileInputFiles`): la
imagen aparece como miniatura en la bandeja inline.
4. Espera a que la bandeja aparezca (boton "Quitar archivo adjunto" presente) y hace click
real en el boton de enviar la bandeja (icono `wds-ic-send-filled`); verifica que la
bandeja se cerro (sin adjuntos) para confirmar el envio de la imagen.
5. Si `caption` no esta vacio, lo envia como un MENSAJE DE TEXTO de seguimiento via
real en el boton de enviar la bandeja (icono `wds-ic-send-filled`).
5. Verifica el envio comprobando que la bandeja se cerro Y que la ultima fila de `#main`
es ahora una imagen (las filas se virtualizan, asi que NO sirve contar filas).
6. Si `caption` no esta vacio, lo envia como un MENSAJE DE TEXTO de seguimiento via
`whatsapp_send_message` (con `open_first=False`, el chat ya esta abierto). En la
WhatsApp Web compacta actual el caption embebido en la imagen no es automatizable de
forma fiable, asi que la descripcion viaja como una segunda burbuja: [imagen][caption].
@@ -54,6 +54,16 @@ def _center(expr: str, port: int, substr: str):
return None
def _adj_expanded(port: int, substr: str) -> str:
"""Estado aria-expanded del boton 'Adjuntar' ('true'/'false'/'no-btn')."""
r = cdp_eval(
'/*ADJEXP*/var e=document.querySelector(\'button[aria-label="Adjuntar"]\'); '
"e?e.getAttribute('aria-expanded'):'no-btn'",
port=port, target_url_substr=substr,
)
return r.get("value") or "no-btn"
def _attachment_count(port: int, substr: str) -> int:
"""Numero de adjuntos en la bandeja inline (botones 'Quitar archivo adjunto')."""
r = cdp_eval(
@@ -64,6 +74,16 @@ def _attachment_count(port: int, substr: str) -> int:
return v if isinstance(v, int) else 0
def _last_row_is_image(port: int, substr: str) -> bool:
"""True si la ultima fila renderizada de #main contiene una imagen (blob)."""
r = cdp_eval(
'/*LASTIMG*/(() => {const r=[...document.querySelectorAll(\'#main [role="row"]\')]'
".slice(-1)[0]; return r?!!r.querySelector('img[src^=\"blob:\"]'):false;})()",
port=port, target_url_substr=substr,
)
return bool(r.get("value"))
def whatsapp_send_image(
name: str,
image_path: str,
@@ -131,7 +151,9 @@ def whatsapp_send_image(
if name not in (chk.get("value") or ""):
return fail("el chat abierto no coincide con el destinatario; abortado por seguridad")
# 2. Click real en "Adjuntar" para exponer el <input type=file> vivo.
# 2. Asegurar el menu "Adjuntar" ABIERTO. Es un TOGGLE (aria-expanded): clickar cuando
# ya esta abierto lo cierra y el input vivo desaparece. Por eso clickamos SOLO si no
# esta expandido y reintentamos hasta verlo abierto.
adj = _center(
'/*ADJUNTAR*/(() => {const e=document.querySelector(\'button[aria-label="Adjuntar"]\');'
"if(!e)return null;const b=e.getBoundingClientRect();"
@@ -140,21 +162,23 @@ def whatsapp_send_image(
)
if not adj:
return fail("boton 'Adjuntar' no encontrado en el footer")
cdp_click_xy(adj["x"], adj["y"], port=port, target_url_substr=S)
time.sleep(0.8)
for _ in range(3):
if _adj_expanded(port, S) == "true":
break
cdp_click_xy(adj["x"], adj["y"], port=port, target_url_substr=S)
time.sleep(0.6)
if _adj_expanded(port, S) != "true":
return fail("no se pudo abrir el menu 'Adjuntar' (aria-expanded sigue en false)")
# 3. Asignar la imagen al input multiple (el que se activa tras Adjuntar);
# fallback al primer input file si el selector con [multiple] no resuelve.
r = cdp_set_file_input('input[type="file"][multiple]', abs_img,
# 3. Asignar la imagen al primer <input type=file> (el "vivo" mientras el menu esta
# abierto). El composer queda VACIO, asi que luego el unico wds-ic-send-filled es el
# de enviar la bandeja.
r = cdp_set_file_input('input[type="file"]', abs_img,
port=port, target_url_substr=S)
if not r.get("ok"):
r = cdp_set_file_input('input[type="file"]', abs_img,
port=port, target_url_substr=S)
if not r.get("ok"):
return fail("no se pudo adjuntar la imagen: " + r.get("error", ""))
# 4. Esperar a que la bandeja aparezca (adjunto presente). El composer queda VACIO,
# asi que el unico boton wds-ic-send-filled es el de enviar la bandeja.
# 4. Esperar a que la bandeja aparezca (adjunto presente).
attached = False
for _ in range(15):
time.sleep(0.2)
@@ -163,6 +187,7 @@ def whatsapp_send_image(
break
if not attached:
return fail("el preview no aparecio tras adjuntar la imagen")
time.sleep(0.3)
# 5. Click real en el boton de enviar la bandeja (icono wds-ic-send-filled).
snd = _center(
@@ -176,15 +201,16 @@ def whatsapp_send_image(
return fail("boton de enviar (wds-ic-send-filled) no encontrado")
cdp_click_xy(snd["x"], snd["y"], port=port, target_url_substr=S)
# 6. Verificar que la bandeja se cerro (sin adjuntos) -> imagen enviada.
# 6. Confirmar envio: la bandeja se cierra (adjuntos=0) Y la ultima fila de #main es ya
# una imagen. Las filas de #main se VIRTUALIZAN, asi que contar filas no sirve.
image_sent = False
for _ in range(15):
for _ in range(20):
time.sleep(0.2)
if _attachment_count(port, S) == 0:
if _attachment_count(port, S) == 0 and _last_row_is_image(port, S):
image_sent = True
break
if not image_sent:
return fail("la bandeja no se cerro tras pulsar enviar; envio incierto")
return fail("no se confirmo la imagen en el chat tras pulsar enviar; envio incierto")
# 7. Caption opcional como mensaje de texto de seguimiento (segunda burbuja).
caption_sent = False