Files
Developer 3253828fef
Tests / Lint (push) Has been cancelled
Tests / Unit Tests (push) Has been cancelled
Tests / E2E Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Initial commit: navegator - Chrome CDP automation for LLMs
Add complete navegator system for stealthy browser automation:
- CDP client with WebSocket communication
- Browser API with navigation, storage, network, runtime
- Stealth flags and anti-detection scripts
- Persistent profile support
- Examples and comprehensive documentation

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-24 23:33:07 +01:00

397 lines
11 KiB
Go

package browser
import (
"context"
"encoding/json"
"fmt"
)
// EvaluateResult representa el resultado de una evaluación de JavaScript.
type EvaluateResult struct {
Type string `json:"type"`
Value interface{} `json:"value"`
Description string `json:"description"`
ObjectID string `json:"objectId,omitempty"`
SubType string `json:"subtype,omitempty"`
RawResult json.RawMessage `json:"-"`
}
// Evaluate ejecuta código JavaScript en el contexto de la página.
func (b *Browser) Evaluate(ctx context.Context, expression string) (*EvaluateResult, error) {
params := map[string]interface{}{
"expression": expression,
"returnByValue": true,
"awaitPromise": true,
"userGesture": true,
}
var response struct {
Result struct {
Type string `json:"type"`
Value interface{} `json:"value"`
Description string `json:"description"`
ObjectID string `json:"objectId"`
SubType string `json:"subtype"`
} `json:"result"`
ExceptionDetails *struct {
Text string `json:"text"`
Exception struct {
Description string `json:"description"`
} `json:"exception"`
} `json:"exceptionDetails"`
}
if err := b.cdpClient.Execute(ctx, "Runtime.evaluate", params, &response); err != nil {
return nil, fmt.Errorf("failed to evaluate: %w", err)
}
if response.ExceptionDetails != nil {
return nil, fmt.Errorf("JavaScript exception: %s - %s",
response.ExceptionDetails.Text,
response.ExceptionDetails.Exception.Description)
}
result := &EvaluateResult{
Type: response.Result.Type,
Value: response.Result.Value,
Description: response.Result.Description,
ObjectID: response.Result.ObjectID,
SubType: response.Result.SubType,
}
return result, nil
}
// EvaluateOnNode ejecuta JavaScript en el contexto de un nodo específico.
func (b *Browser) EvaluateOnNode(ctx context.Context, nodeID int64, expression string) (*EvaluateResult, error) {
// Primero obtener el objectId del nodo
var objResult struct {
Object struct {
ObjectID string `json:"objectId"`
} `json:"object"`
}
params := map[string]interface{}{
"nodeId": nodeID,
}
if err := b.cdpClient.Execute(ctx, "DOM.resolveNode", params, &objResult); err != nil {
return nil, fmt.Errorf("failed to resolve node: %w", err)
}
// Ejecutar función en el objeto
callParams := map[string]interface{}{
"functionDeclaration": fmt.Sprintf("function() { return (%s); }", expression),
"objectId": objResult.Object.ObjectID,
"returnByValue": true,
"awaitPromise": true,
}
var response struct {
Result struct {
Type string `json:"type"`
Value interface{} `json:"value"`
Description string `json:"description"`
} `json:"result"`
ExceptionDetails *struct {
Text string `json:"text"`
} `json:"exceptionDetails"`
}
if err := b.cdpClient.Execute(ctx, "Runtime.callFunctionOn", callParams, &response); err != nil {
return nil, fmt.Errorf("failed to call function: %w", err)
}
if response.ExceptionDetails != nil {
return nil, fmt.Errorf("JavaScript exception: %s", response.ExceptionDetails.Text)
}
return &EvaluateResult{
Type: response.Result.Type,
Value: response.Result.Value,
Description: response.Result.Description,
}, nil
}
// EvaluateAsync ejecuta JavaScript de forma asíncrona (retorna Promise).
func (b *Browser) EvaluateAsync(ctx context.Context, expression string) (*EvaluateResult, error) {
params := map[string]interface{}{
"expression": expression,
"returnByValue": true,
"awaitPromise": true,
"userGesture": true,
}
var response struct {
Result struct {
Type string `json:"type"`
Value interface{} `json:"value"`
Description string `json:"description"`
ObjectID string `json:"objectId"`
} `json:"result"`
ExceptionDetails *struct {
Text string `json:"text"`
Exception struct {
Description string `json:"description"`
} `json:"exception"`
} `json:"exceptionDetails"`
}
if err := b.cdpClient.Execute(ctx, "Runtime.evaluate", params, &response); err != nil {
return nil, fmt.Errorf("failed to evaluate async: %w", err)
}
if response.ExceptionDetails != nil {
return nil, fmt.Errorf("JavaScript exception: %s - %s",
response.ExceptionDetails.Text,
response.ExceptionDetails.Exception.Description)
}
return &EvaluateResult{
Type: response.Result.Type,
Value: response.Result.Value,
Description: response.Result.Description,
ObjectID: response.Result.ObjectID,
}, nil
}
// CallFunction ejecuta una función JavaScript con argumentos.
func (b *Browser) CallFunction(ctx context.Context, functionDeclaration string, args ...interface{}) (*EvaluateResult, error) {
// Convertir args a formato CDP
cdpArgs := make([]map[string]interface{}, len(args))
for i, arg := range args {
cdpArgs[i] = map[string]interface{}{
"value": arg,
}
}
params := map[string]interface{}{
"functionDeclaration": functionDeclaration,
"arguments": cdpArgs,
"returnByValue": true,
"awaitPromise": true,
"userGesture": true,
}
var response struct {
Result struct {
Type string `json:"type"`
Value interface{} `json:"value"`
Description string `json:"description"`
} `json:"result"`
ExceptionDetails *struct {
Text string `json:"text"`
} `json:"exceptionDetails"`
}
if err := b.cdpClient.Execute(ctx, "Runtime.callFunctionOn", params, &response); err != nil {
return nil, fmt.Errorf("failed to call function: %w", err)
}
if response.ExceptionDetails != nil {
return nil, fmt.Errorf("JavaScript exception: %s", response.ExceptionDetails.Text)
}
return &EvaluateResult{
Type: response.Result.Type,
Value: response.Result.Value,
Description: response.Result.Description,
}, nil
}
// GetProperty obtiene una propiedad de un objeto.
func (b *Browser) GetProperty(ctx context.Context, objectID string, propertyName string) (*EvaluateResult, error) {
params := map[string]interface{}{
"objectId": objectID,
}
var response struct {
Result []struct {
Name string `json:"name"`
Value struct {
Type string `json:"type"`
Value interface{} `json:"value"`
Description string `json:"description"`
} `json:"value"`
} `json:"result"`
}
if err := b.cdpClient.Execute(ctx, "Runtime.getProperties", params, &response); err != nil {
return nil, fmt.Errorf("failed to get properties: %w", err)
}
for _, prop := range response.Result {
if prop.Name == propertyName {
return &EvaluateResult{
Type: prop.Value.Type,
Value: prop.Value.Value,
Description: prop.Value.Description,
}, nil
}
}
return nil, fmt.Errorf("property not found: %s", propertyName)
}
// AddBinding agrega un binding (función JS que llama a Go).
type BindingCallback func(args []interface{}) interface{}
// AddBinding expone una función Go al contexto JavaScript.
func (b *Browser) AddBinding(ctx context.Context, name string, callback BindingCallback) error {
// Agregar binding en Runtime
params := map[string]interface{}{
"name": name,
}
if err := b.cdpClient.Execute(ctx, "Runtime.addBinding", params, nil); err != nil {
return fmt.Errorf("failed to add binding: %w", err)
}
// Registrar evento para manejar llamadas
b.cdpClient.On("Runtime.bindingCalled", func(eventParams json.RawMessage) {
var event struct {
Name string `json:"name"`
Payload string `json:"payload"`
ExecutionContextID int64 `json:"executionContextId"`
}
if err := json.Unmarshal(eventParams, &event); err != nil {
return
}
if event.Name != name {
return
}
// Parsear args
var args []interface{}
if err := json.Unmarshal([]byte(event.Payload), &args); err != nil {
return
}
// Ejecutar callback
result := callback(args)
// Devolver resultado (evaluando código que lo retorna)
returnScript := fmt.Sprintf("window.%s_result = %v", name, result)
b.Evaluate(ctx, returnScript)
})
// Inyectar wrapper en JavaScript
wrapperScript := fmt.Sprintf(`
window.%s = async (...args) => {
const payload = JSON.stringify(args);
window.%s_result = undefined;
await window.chrome.runtime.sendMessage({
type: 'binding',
name: '%s',
payload: payload
});
// Esperar resultado (polling simple)
while (window.%s_result === undefined) {
await new Promise(r => setTimeout(r, 10));
}
return window.%s_result;
};
`, name, name, name, name, name)
_, err := b.Evaluate(ctx, wrapperScript)
return err
}
// ConsoleMessage representa un mensaje de consola.
type ConsoleMessage struct {
Type string `json:"type"`
Args []interface{} `json:"args"`
Text string `json:"text"`
URL string `json:"url"`
Line int `json:"lineNumber"`
Column int `json:"columnNumber"`
}
// OnConsole registra un handler para mensajes de consola.
func (b *Browser) OnConsole(handler func(msg *ConsoleMessage)) {
b.cdpClient.On("Runtime.consoleAPICalled", func(params json.RawMessage) {
var event struct {
Type string `json:"type"`
Args []struct {
Type string `json:"type"`
Value interface{} `json:"value"`
} `json:"args"`
StackTrace struct {
CallFrames []struct {
URL string `json:"url"`
LineNumber int `json:"lineNumber"`
ColumnNumber int `json:"columnNumber"`
} `json:"callFrames"`
} `json:"stackTrace"`
}
if err := json.Unmarshal(params, &event); err != nil {
return
}
msg := &ConsoleMessage{
Type: event.Type,
Args: make([]interface{}, len(event.Args)),
}
// Construir texto del mensaje
text := ""
for i, arg := range event.Args {
msg.Args[i] = arg.Value
if i > 0 {
text += " "
}
text += fmt.Sprintf("%v", arg.Value)
}
msg.Text = text
// Agregar info de stack trace si existe
if len(event.StackTrace.CallFrames) > 0 {
frame := event.StackTrace.CallFrames[0]
msg.URL = frame.URL
msg.Line = frame.LineNumber
msg.Column = frame.ColumnNumber
}
handler(msg)
})
}
// EnableConsole habilita eventos de consola.
func (b *Browser) EnableConsole(ctx context.Context) error {
return b.cdpClient.Execute(ctx, "Runtime.enable", nil, nil)
}
// QuerySelector helper para ejecutar querySelector desde JavaScript.
func (b *Browser) QuerySelector(ctx context.Context, selector string) (*EvaluateResult, error) {
script := fmt.Sprintf(`document.querySelector('%s')`, selector)
return b.Evaluate(ctx, script)
}
// QuerySelectorAll ejecuta querySelectorAll y retorna array de elementos.
func (b *Browser) QuerySelectorAll(ctx context.Context, selector string) (*EvaluateResult, error) {
script := fmt.Sprintf(`Array.from(document.querySelectorAll('%s'))`, selector)
return b.Evaluate(ctx, script)
}
// WaitForFunction espera a que una función JavaScript retorne true.
func (b *Browser) WaitForFunction(ctx context.Context, function string, pollInterval int) error {
script := fmt.Sprintf(`
new Promise((resolve) => {
const check = () => {
if (%s) {
resolve(true);
} else {
setTimeout(check, %d);
}
};
check();
})
`, function, pollInterval)
_, err := b.EvaluateAsync(ctx, script)
return err
}