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>
This commit is contained in:
@@ -0,0 +1,396 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user