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) } }