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:15:30 +02:00
parent 0dd2718c95
commit d0960bed70
3 changed files with 93 additions and 84 deletions
+45 -43
View File
@@ -1,8 +1,9 @@
"""Envia una imagen (con caption opcional) a un chat de WhatsApp Web via Chrome DevTools Protocol.
Compone `whatsapp_open_chat` (abrir + verificar destinatario) con primitivas CDP del
registry (`cdp_eval`, `cdp_click_xy`, `cdp_set_file_input`, `cdp_type_chars`) para adjuntar
y enviar una imagen a un contacto/grupo SIN abrir ventana nueva ni darle foco al sistema.
registry (`cdp_eval`, `cdp_click_xy`, `cdp_set_file_input`) y `whatsapp_send_message` para
adjuntar y enviar una imagen a un contacto/grupo SIN abrir ventana nueva ni darle foco al
sistema.
Flujo (modelo de bandeja de medios INLINE de la WhatsApp Web actual), con salvaguarda
anti-envio-al-contacto-equivocado:
@@ -14,15 +15,17 @@ anti-envio-al-contacto-equivocado:
`<input type=file>` "vivo" que escucha la SPA (antes de pulsarlo el input persistente
no dispara el preview).
3. Asigna la imagen al input via `cdp_set_file_input` (`DOM.setFileInputFiles`): la
imagen aparece como miniatura en la bandeja inline sobre el composer.
4. Espera a que el preview aparezca (boton "Quitar archivo adjunto" presente).
5. Si `caption` no esta vacio, enfoca el composer del footer y teclea el texto con
teclado CDP real (`cdp_type_chars`); verifica el `innerText` antes de enviar.
6. Click de raton real en el boton enviar (icono `wds-ic-send-filled`) y verifica que la
bandeja se cerro (sin adjuntos) para confirmar el envio.
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
`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].
Validado contra WhatsApp Web real. Accion CON EFECTO REAL E IRREVERSIBLE: envia la imagen
de verdad.
(y el caption) de verdad.
"""
import json
@@ -34,9 +37,9 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from browser.cdp_eval import cdp_eval
from browser.cdp_click_xy import cdp_click_xy
from browser.cdp_type_chars import cdp_type_chars
from browser.cdp_set_file_input import cdp_set_file_input
from browser.whatsapp_open_chat import whatsapp_open_chat
from browser.whatsapp_send_message import whatsapp_send_message
def _center(expr: str, port: int, substr: str):
@@ -70,7 +73,7 @@ def whatsapp_send_image(
target_url_substr: str = "whatsapp",
open_first: bool = True,
) -> dict:
"""Envia una imagen (con caption opcional) a un chat de WhatsApp Web ya logueado.
"""Envia una imagen (con caption opcional de seguimiento) a un chat de WhatsApp Web.
Accion CON EFECTO: envia la imagen DE VERDAD (no reversible). Verifica `name`.
@@ -80,8 +83,9 @@ def whatsapp_send_image(
al destinatario correcto antes de adjuntar.
image_path: Ruta de la imagen a enviar. Se expande (`~`) y se convierte a ruta
ABSOLUTA; debe existir en disco.
caption: Texto opcional que acompana la imagen. Se teclea en el composer con teclado
CDP real; "" (default) envia la imagen sin caption.
caption: Texto opcional descriptivo. Se envia como un MENSAJE DE TEXTO de seguimiento
(segunda burbuja [imagen][caption]) via `whatsapp_send_message`; "" (default)
envia solo la imagen.
port: Puerto de remote debugging de Chrome. Default 9222.
target_url_substr: Substring que debe contener la URL del target (pestana). Default
"whatsapp".
@@ -91,12 +95,14 @@ def whatsapp_send_image(
Returns:
dict con claves:
ok: bool — alias de sent (True si la imagen se envio).
sent: bool — True si la imagen se adjunto, (caption escrito) y se envio.
ok: bool — True si la imagen se envio y (si habia caption) el caption tambien.
sent: bool — True si la IMAGEN se envio.
caption_sent: bool — True si el caption de seguimiento se envio (False si no
habia caption o si fallo).
recipient: str — el nombre solicitado.
image: str — ruta absoluta de la imagen.
caption: str — caption solicitado.
error: str — motivo del fallo (vacio si sent=True).
error: str — motivo del fallo (vacio si todo ok).
Nunca lanza: los fallos se reportan en "sent"/"ok" + "error".
"""
@@ -104,7 +110,7 @@ def whatsapp_send_image(
abs_img = os.path.abspath(os.path.expanduser(image_path))
def fail(error: str) -> dict:
return {"ok": False, "sent": False, "recipient": name,
return {"ok": False, "sent": False, "caption_sent": False, "recipient": name,
"image": abs_img, "caption": caption, "error": error}
# 0. La imagen debe existir.
@@ -147,7 +153,8 @@ def whatsapp_send_image(
if not r.get("ok"):
return fail("no se pudo adjuntar la imagen: " + r.get("error", ""))
# 4. Esperar a que el preview/bandeja aparezca (adjunto presente).
# 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.
attached = False
for _ in range(15):
time.sleep(0.2)
@@ -157,27 +164,7 @@ def whatsapp_send_image(
if not attached:
return fail("el preview no aparecio tras adjuntar la imagen")
# 5. Caption opcional: enfocar el composer y teclear con teclado real.
if caption:
cdp_eval(
'/*FOCUS*/var b=document.querySelector(\'div[contenteditable="true"]\'); '
"if(b){b.focus();}",
port=port, target_url_substr=S,
)
time.sleep(0.25)
cdp_type_chars(caption, port=port, target_url_substr=S, delay_ms=15)
time.sleep(0.3)
chk = cdp_eval(
'/*COMPOSER*/var b=document.querySelector(\'div[contenteditable="true"]\'); '
"b?b.innerText.replace(/\\n/g,''):''",
port=port, target_url_substr=S,
)
composer = chk.get("value") or ""
if composer != caption:
return fail("el composer no contiene el caption esperado (no enviado): "
+ repr(composer))
# 6. Click real en el boton enviar (icono wds-ic-send-filled).
# 5. Click real en el boton de enviar la bandeja (icono wds-ic-send-filled).
snd = _center(
'/*SEND*/(() => {const e=document.querySelector(\'span[data-icon="wds-ic-send-filled"]\');'
"if(!e)return null;const b=e.getBoundingClientRect();"
@@ -189,14 +176,29 @@ 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)
# 7. Verificar que la bandeja se cerro (sin adjuntos) -> envio confirmado.
# 6. Verificar que la bandeja se cerro (sin adjuntos) -> imagen enviada.
image_sent = False
for _ in range(15):
time.sleep(0.2)
if _attachment_count(port, S) == 0:
return {"ok": True, "sent": True, "recipient": name,
"image": abs_img, "caption": caption, "error": ""}
image_sent = True
break
if not image_sent:
return fail("la bandeja no se cerro tras pulsar enviar; envio incierto")
return fail("la bandeja no se cerro tras pulsar enviar; envio incierto")
# 7. Caption opcional como mensaje de texto de seguimiento (segunda burbuja).
caption_sent = False
if caption:
m = whatsapp_send_message(name, caption, port=port, target_url_substr=S,
open_first=False)
caption_sent = bool(m.get("sent"))
if not caption_sent:
return {"ok": False, "sent": True, "caption_sent": False, "recipient": name,
"image": abs_img, "caption": caption,
"error": "imagen enviada pero el caption fallo: " + m.get("reason", "")}
return {"ok": True, "sent": True, "caption_sent": caption_sent, "recipient": name,
"image": abs_img, "caption": caption, "error": ""}
if __name__ == "__main__":