feat(browser): cdp_collect_console + cdp_print_pdf + cdp_select_option + cdp_set_file_input
Cuatro primitivas CDP nuevas para el dominio browser, base de nuevas tools del browser_mcp: - cdp_collect_console: snapshot temporal de console + exceptions + log entries - cdp_print_pdf: Page.printToPDF -> []byte - cdp_select_option: selecciona <option> en un <select> y dispara input/change - cdp_set_file_input: sube archivos a un <input type=file> via DOM.setFileInputFiles Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// CdpSetFileInput sube archivos a un <input type="file"> identificado por el
|
||||
// selector CSS. Resuelve el nodo via DOM.getDocument + DOM.querySelector y luego
|
||||
// asigna los archivos con DOM.setFileInputFiles. Util para automatizar formularios
|
||||
// de subida sin simular el dialogo nativo de seleccion de archivos.
|
||||
//
|
||||
// Cada path de paths se valida con os.Stat ANTES de enviar el comando: si alguno
|
||||
// no existe (o no es accesible) se devuelve error inmediato sin tocar el DOM. Los
|
||||
// paths deben ser absolutos y accesibles por el proceso de Chrome (ver Gotchas en
|
||||
// el .md): Chrome lee los archivos desde su propio contexto, no desde el de este
|
||||
// programa.
|
||||
func CdpSetFileInput(c *CDPConn, selector string, paths []string) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("cdp set file input: conexion nula")
|
||||
}
|
||||
if selector == "" {
|
||||
return fmt.Errorf("cdp set file input: selector vacio")
|
||||
}
|
||||
if len(paths) == 0 {
|
||||
return fmt.Errorf("cdp set file input: lista de paths vacia")
|
||||
}
|
||||
|
||||
// Validar que cada path exista en disco antes de mandar nada a Chrome.
|
||||
for _, p := range paths {
|
||||
if p == "" {
|
||||
return fmt.Errorf("cdp set file input: path vacio en la lista")
|
||||
}
|
||||
if _, err := os.Stat(p); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("cdp set file input: el archivo no existe: %q", p)
|
||||
}
|
||||
return fmt.Errorf("cdp set file input: no se puede acceder al archivo %q: %w", p, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Obtener el nodo raiz del documento.
|
||||
docRes, err := c.sendCDP("DOM.getDocument", map[string]any{"depth": 0})
|
||||
if err != nil {
|
||||
return fmt.Errorf("cdp set file input: DOM.getDocument: %w", err)
|
||||
}
|
||||
root, ok := docRes["root"].(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("cdp set file input: respuesta de DOM.getDocument sin root")
|
||||
}
|
||||
rootNodeID, ok := root["nodeId"].(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("cdp set file input: DOM.getDocument sin nodeId raiz")
|
||||
}
|
||||
|
||||
// Resolver el input por selector.
|
||||
qsRes, err := c.sendCDP("DOM.querySelector", map[string]any{
|
||||
"nodeId": int(rootNodeID),
|
||||
"selector": selector,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("cdp set file input: DOM.querySelector %q: %w", selector, err)
|
||||
}
|
||||
nodeIDVal, ok := qsRes["nodeId"].(float64)
|
||||
if !ok || int(nodeIDVal) == 0 {
|
||||
return fmt.Errorf("cdp set file input: el selector %q no coincide con ningun elemento", selector)
|
||||
}
|
||||
|
||||
// Asignar los archivos al input.
|
||||
files := make([]any, len(paths))
|
||||
for i, p := range paths {
|
||||
files[i] = p
|
||||
}
|
||||
if _, err := c.sendCDP("DOM.setFileInputFiles", map[string]any{
|
||||
"files": files,
|
||||
"nodeId": int(nodeIDVal),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("cdp set file input: DOM.setFileInputFiles en %q: %w", selector, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user