Files
device_agent/capability_browser.go
2026-05-30 17:28:38 +02:00

148 lines
4.1 KiB
Go

package main
import (
"fmt"
"path/filepath"
"strings"
"time"
"fn-registry/functions/browser"
)
// runBrowserOp executes a browser CDP operation by connecting to Chrome's
// remote debugging endpoint, dispatching the op, and closing the connection.
//
// Supported ops (passed via args.op):
// - list_tabs → []CdpTab
// - navigate(url) → ok
// - click_text(text, tag?, exact?) → ok
// - evaluate(expression) → string
// - screenshot(filename?) → path saved
// - type_text(selector, text) → ok
// - get_html(selector?) → html
//
// host + port come from manifest. Defaults: 127.0.0.1:9223.
func runBrowserOp(cap *Capability, op string, args map[string]any) (any, int, error) {
host := cap.ChromeCDPHost
if host == "" {
host = "127.0.0.1"
}
port := cap.ChromeCDPPort
if port == 0 {
port = 9223
}
// list_tabs uses HTTP only — no websocket.
if op == "list_tabs" {
tabs, err := browser.CdpListTabs(host, port)
if err != nil {
return nil, -1, err
}
return map[string]any{"tabs": tabs}, 0, nil
}
// launch_chrome bootstraps Chrome with --remote-debugging-port.
// Idempotent: if the port is already serving CDP, ChromeLaunch's
// waitCDPReady connects to the existing process and returns success.
if op == "launch_chrome" {
opts := browser.ChromeLaunchOpts{Port: port}
if hl, ok := args["headless"].(bool); ok {
opts.Headless = hl
}
if udd, ok := args["user_data_dir"].(string); ok && udd != "" {
opts.UserDataDir = udd
}
if cp, ok := args["chrome_path"].(string); ok && cp != "" {
opts.ChromePath = cp
}
pid, err := browser.ChromeLaunch(opts)
if err != nil {
return nil, -1, err
}
return map[string]any{"pid": pid, "port": port}, 0, nil
}
// Operations that need a websocket connection.
conn, err := browser.CdpConnectHost(host, port)
if err != nil {
return nil, -1, fmt.Errorf("cdp connect %s:%d: %w", host, port, err)
}
defer browser.CdpClose(conn, 0)
switch op {
case "navigate":
url, _ := args["url"].(string)
if url == "" {
return nil, -1, fmt.Errorf("navigate: url required")
}
if err := browser.CdpNavigate(conn, url); err != nil {
return nil, -1, err
}
return map[string]any{"ok": true, "url": url}, 0, nil
case "click_text":
text, _ := args["text"].(string)
if text == "" {
return nil, -1, fmt.Errorf("click_text: text required")
}
tag, _ := args["tag"].(string)
exact, _ := args["exact"].(bool)
opts := browser.FindByTextOpts{Tag: tag, Exact: exact}
if err := browser.CdpClickText(conn, text, opts); err != nil {
return nil, -1, err
}
return map[string]any{"ok": true, "clicked": text}, 0, nil
case "evaluate":
expr, _ := args["expression"].(string)
if expr == "" {
return nil, -1, fmt.Errorf("evaluate: expression required")
}
val, err := browser.CdpEvaluate(conn, expr)
if err != nil {
return nil, -1, err
}
return map[string]any{"value": val}, 0, nil
case "screenshot":
dir := cap.ScreenshotDir
if dir == "" {
dir = filepath.Join("local_files", "screenshots")
}
fname, _ := args["filename"].(string)
if fname == "" {
fname = fmt.Sprintf("shot_%d.png", time.Now().UnixNano())
}
fname = strings.ReplaceAll(fname, "..", "_")
out := filepath.Join(dir, fname)
opts := browser.CdpScreenshotOpts{Format: "png"}
if fp, ok := args["full_page"].(bool); ok {
opts.FullPage = fp
}
if err := browser.CdpScreenshot(conn, out, opts); err != nil {
return nil, -1, err
}
return map[string]any{"path": out}, 0, nil
case "type_text":
text, _ := args["text"].(string)
if text == "" {
return nil, -1, fmt.Errorf("type_text: text required (will be typed into focused element)")
}
if err := browser.CdpTypeText(conn, text); err != nil {
return nil, -1, err
}
return map[string]any{"ok": true}, 0, nil
case "get_html":
html, err := browser.CdpGetHTML(conn)
if err != nil {
return nil, -1, err
}
return map[string]any{"html": html}, 0, nil
default:
return nil, -1, fmt.Errorf("browser op %q not supported (list_tabs|navigate|click_text|evaluate|screenshot|type_text|get_html)", op)
}
}