working v1
This commit is contained in:
347
bridge/kosmi/playwright_client.go
Normal file
347
bridge/kosmi/playwright_client.go
Normal file
@@ -0,0 +1,347 @@
|
||||
package bkosmi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/playwright-community/playwright-go"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// PlaywrightClient manages a Playwright browser instance to connect to Kosmi
|
||||
type PlaywrightClient struct {
|
||||
roomURL string
|
||||
log *logrus.Entry
|
||||
pw *playwright.Playwright
|
||||
browser playwright.Browser
|
||||
page playwright.Page
|
||||
messageCallback func(*NewMessagePayload)
|
||||
connected bool
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewPlaywrightClient creates a new Playwright-based Kosmi client
|
||||
func NewPlaywrightClient(roomURL string, log *logrus.Entry) *PlaywrightClient {
|
||||
return &PlaywrightClient{
|
||||
roomURL: roomURL,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// Connect launches Playwright and navigates to the Kosmi room
|
||||
func (c *PlaywrightClient) Connect() error {
|
||||
c.log.Info("Launching Playwright browser for Kosmi connection")
|
||||
|
||||
// Create Playwright instance (using system Chromium, no install needed)
|
||||
pw, err := playwright.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start Playwright: %w", err)
|
||||
}
|
||||
c.pw = pw
|
||||
|
||||
// Launch browser using system Chromium
|
||||
browser, err := pw.Chromium.Launch(playwright.BrowserTypeLaunchOptions{
|
||||
Headless: playwright.Bool(true),
|
||||
ExecutablePath: playwright.String("/usr/bin/chromium"),
|
||||
Args: []string{
|
||||
"--no-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--disable-blink-features=AutomationControlled",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to launch browser: %w", err)
|
||||
}
|
||||
c.browser = browser
|
||||
|
||||
// Create context and page
|
||||
context, err := browser.NewContext(playwright.BrowserNewContextOptions{
|
||||
UserAgent: playwright.String("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create context: %w", err)
|
||||
}
|
||||
|
||||
page, err := context.NewPage()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create page: %w", err)
|
||||
}
|
||||
c.page = page
|
||||
|
||||
// Inject WebSocket hook before navigation
|
||||
c.log.Info("Injecting WebSocket interceptor...")
|
||||
if err := c.injectWebSocketHook(); err != nil {
|
||||
return fmt.Errorf("failed to inject WebSocket hook: %w", err)
|
||||
}
|
||||
|
||||
// Navigate to the room
|
||||
c.log.Infof("Navigating to Kosmi room: %s", c.roomURL)
|
||||
if _, err := page.Goto(c.roomURL, playwright.PageGotoOptions{
|
||||
WaitUntil: playwright.WaitUntilStateNetworkidle,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to navigate to room: %w", err)
|
||||
}
|
||||
|
||||
// Wait for page to be ready
|
||||
if err := page.WaitForLoadState(playwright.PageWaitForLoadStateOptions{
|
||||
State: playwright.LoadStateNetworkidle,
|
||||
}); err != nil {
|
||||
c.log.Warnf("Page load state warning: %v", err)
|
||||
}
|
||||
|
||||
c.log.Info("Page loaded, waiting for WebSocket connection...")
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
c.mu.Lock()
|
||||
c.connected = true
|
||||
c.mu.Unlock()
|
||||
|
||||
c.log.Info("Successfully connected to Kosmi via Playwright")
|
||||
|
||||
// Start message listener
|
||||
go c.listenForMessages()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// injectWebSocketHook injects the WebSocket interception script
|
||||
func (c *PlaywrightClient) injectWebSocketHook() error {
|
||||
script := c.getWebSocketHookScript()
|
||||
|
||||
return c.page.AddInitScript(playwright.Script{
|
||||
Content: playwright.String(script),
|
||||
})
|
||||
}
|
||||
|
||||
// getWebSocketHookScript returns the JavaScript to hook WebSocket
|
||||
func (c *PlaywrightClient) getWebSocketHookScript() string {
|
||||
return `
|
||||
(function() {
|
||||
if (window.__KOSMI_WS_HOOK_INSTALLED__) {
|
||||
return;
|
||||
}
|
||||
|
||||
const OriginalWebSocket = window.WebSocket;
|
||||
window.__KOSMI_MESSAGE_QUEUE__ = [];
|
||||
|
||||
window.WebSocket = function(url, protocols) {
|
||||
const socket = new OriginalWebSocket(url, protocols);
|
||||
|
||||
if (url.includes('engine.kosmi.io') || url.includes('gql-ws')) {
|
||||
console.log('[Kosmi Bridge] WebSocket hook active for:', url);
|
||||
window.__KOSMI_WS_CONNECTED__ = true;
|
||||
|
||||
const originalAddEventListener = socket.addEventListener.bind(socket);
|
||||
socket.addEventListener = function(type, listener, options) {
|
||||
if (type === 'message') {
|
||||
const wrappedListener = function(event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
window.__KOSMI_MESSAGE_QUEUE__.push({
|
||||
timestamp: Date.now(),
|
||||
data: data
|
||||
});
|
||||
} catch (e) {}
|
||||
return listener.call(this, event);
|
||||
};
|
||||
return originalAddEventListener(type, wrappedListener, options);
|
||||
}
|
||||
return originalAddEventListener(type, listener, options);
|
||||
};
|
||||
|
||||
let realOnMessage = null;
|
||||
Object.defineProperty(socket, 'onmessage', {
|
||||
get: function() { return realOnMessage; },
|
||||
set: function(handler) {
|
||||
realOnMessage = function(event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
window.__KOSMI_MESSAGE_QUEUE__.push({
|
||||
timestamp: Date.now(),
|
||||
data: data
|
||||
});
|
||||
} catch (e) {}
|
||||
if (handler) { handler.call(socket, event); }
|
||||
};
|
||||
},
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
|
||||
return socket;
|
||||
};
|
||||
|
||||
window.WebSocket.prototype = OriginalWebSocket.prototype;
|
||||
window.WebSocket.CONNECTING = OriginalWebSocket.CONNECTING;
|
||||
window.WebSocket.OPEN = OriginalWebSocket.OPEN;
|
||||
window.WebSocket.CLOSING = OriginalWebSocket.CLOSING;
|
||||
window.WebSocket.CLOSED = OriginalWebSocket.CLOSED;
|
||||
|
||||
window.__KOSMI_WS_HOOK_INSTALLED__ = true;
|
||||
console.log('[Kosmi Bridge] WebSocket hook installed');
|
||||
})();
|
||||
`
|
||||
}
|
||||
|
||||
// listenForMessages polls for new messages from the WebSocket queue
|
||||
func (c *PlaywrightClient) listenForMessages() {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
c.mu.RLock()
|
||||
if !c.connected {
|
||||
c.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
// Poll for messages
|
||||
result, err := c.page.Evaluate(`
|
||||
(function() {
|
||||
if (!window.__KOSMI_MESSAGE_QUEUE__) return [];
|
||||
const messages = window.__KOSMI_MESSAGE_QUEUE__.slice();
|
||||
window.__KOSMI_MESSAGE_QUEUE__ = [];
|
||||
return messages;
|
||||
})();
|
||||
`)
|
||||
|
||||
if err != nil {
|
||||
c.log.Debugf("Error polling messages: %v", err)
|
||||
<-ticker.C
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse messages
|
||||
var messages []struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(fmt.Sprintf("%v", result)), &messages); err != nil {
|
||||
<-ticker.C
|
||||
continue
|
||||
}
|
||||
|
||||
if len(messages) > 0 {
|
||||
c.log.Infof("Processing %d messages from queue", len(messages))
|
||||
}
|
||||
|
||||
for _, msg := range messages {
|
||||
c.processMessage(msg.Data)
|
||||
}
|
||||
|
||||
<-ticker.C
|
||||
}
|
||||
}
|
||||
|
||||
// processMessage processes a single WebSocket message
|
||||
func (c *PlaywrightClient) processMessage(data map[string]interface{}) {
|
||||
// Check if this is a newMessage subscription event
|
||||
msgType, ok := data["type"].(string)
|
||||
if !ok || msgType != "next" {
|
||||
return
|
||||
}
|
||||
|
||||
payload, ok := data["payload"].(map[string]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
dataField, ok := payload["data"].(map[string]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
newMessage, ok := dataField["newMessage"].(map[string]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse into our struct
|
||||
jsonBytes, err := json.Marshal(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"newMessage": newMessage,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var msgPayload NewMessagePayload
|
||||
if err := json.Unmarshal(jsonBytes, &msgPayload); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Call the callback
|
||||
if c.messageCallback != nil {
|
||||
c.messageCallback(&msgPayload)
|
||||
}
|
||||
}
|
||||
|
||||
// SendMessage sends a message to the Kosmi chat
|
||||
func (c *PlaywrightClient) SendMessage(text string) error {
|
||||
c.mu.RLock()
|
||||
if !c.connected {
|
||||
c.mu.RUnlock()
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
selector := `div[role="textbox"][contenteditable="true"]`
|
||||
|
||||
// Wait for the input to be available
|
||||
_, err := c.page.WaitForSelector(selector, playwright.PageWaitForSelectorOptions{
|
||||
Timeout: playwright.Float(5000),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("chat input not available: %w", err)
|
||||
}
|
||||
|
||||
// Get the input element
|
||||
input := c.page.Locator(selector)
|
||||
|
||||
// Clear and type the message
|
||||
if err := input.Fill(text); err != nil {
|
||||
return fmt.Errorf("failed to fill message: %w", err)
|
||||
}
|
||||
|
||||
// Press Enter to send
|
||||
if err := input.Press("Enter"); err != nil {
|
||||
return fmt.Errorf("failed to press Enter: %w", err)
|
||||
}
|
||||
|
||||
c.log.Debugf("Sent message: %s", text)
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnMessage sets the callback for new messages
|
||||
func (c *PlaywrightClient) OnMessage(callback func(*NewMessagePayload)) {
|
||||
c.messageCallback = callback
|
||||
}
|
||||
|
||||
// Disconnect closes the browser
|
||||
func (c *PlaywrightClient) Disconnect() error {
|
||||
c.mu.Lock()
|
||||
c.connected = false
|
||||
c.mu.Unlock()
|
||||
|
||||
c.log.Info("Closing Playwright browser")
|
||||
|
||||
if c.browser != nil {
|
||||
if err := c.browser.Close(); err != nil {
|
||||
c.log.Warnf("Error closing browser: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if c.pw != nil {
|
||||
if err := c.pw.Stop(); err != nil {
|
||||
c.log.Warnf("Error stopping Playwright: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user