mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-12 21:03:59 +00:00
1ac08155b0
Makes the Stop button on the traces page actually stop running traces. Seven-phase implementation across provider HTTP, agent router, trace persistence, WS events, tool exec, i18n, and integration tests. - Provider HTTP+SSE ctx-aware: close socket on cancel via CtxBody wrapper - Router 2-phase abort: CAS state machine, 3s grace, force-mark fallback - Trace retry: 3 inline retries + 10-max retry queue, stale recovery 10min - trace.status WS event: real-time UI updates (invalidates query on receive) - Tool exec: process-group kill (SIGTERM→3s→SIGKILL), Rod page ctx watch - i18n: 6 abort toast variants in en/vi/zh - Integration: 9 scenarios, -race clean Fixes tenant-ctx loss in forceMarkTraceAborted and retry worker broadcast (caught by code-reviewer: C1/C2). Stale threshold intentionally 10min because start_time-based; last_span_at migration is a follow-up.
123 lines
2.8 KiB
Go
123 lines
2.8 KiB
Go
package browser
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/go-rod/rod"
|
|
"github.com/go-rod/rod/lib/proto"
|
|
)
|
|
|
|
// watchPageClose spawns a goroutine that closes page when ctx is cancelled.
|
|
// Returns a cancel func that stops the watchdog on normal-path close.
|
|
// Uses sync.Once so page.Close() is idempotent if both paths fire concurrently.
|
|
func watchPageClose(ctx context.Context, page *rod.Page) (stopWatchdog func()) {
|
|
var once sync.Once
|
|
closeFn := func() { _ = page.Close() }
|
|
stopped := make(chan struct{})
|
|
|
|
go func() {
|
|
select {
|
|
case <-ctx.Done():
|
|
once.Do(closeFn)
|
|
case <-stopped:
|
|
}
|
|
}()
|
|
|
|
return func() {
|
|
close(stopped)
|
|
}
|
|
}
|
|
|
|
// Snapshot takes an accessibility snapshot of a page.
|
|
func (m *Manager) Snapshot(ctx context.Context, targetID string, opts SnapshotOptions) (*SnapshotResult, error) {
|
|
tenantID := tenantIDFromCtx(ctx)
|
|
m.mu.Lock()
|
|
page, err := m.getPageForTenant(targetID, tenantID)
|
|
m.mu.Unlock()
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := proto.AccessibilityGetFullAXTree{}.Call(page)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get AX tree: %w", err)
|
|
}
|
|
|
|
snap := FormatSnapshot(result.Nodes, opts)
|
|
info, _ := page.Info()
|
|
snap.TargetID = targetID
|
|
if info != nil {
|
|
snap.URL = info.URL
|
|
snap.Title = info.Title
|
|
}
|
|
|
|
// Cache refs
|
|
m.refs.Store(targetID, snap.Refs)
|
|
|
|
return snap, nil
|
|
}
|
|
|
|
// Screenshot captures a page screenshot as PNG bytes.
|
|
func (m *Manager) Screenshot(ctx context.Context, targetID string, fullPage bool) ([]byte, error) {
|
|
tenantID := tenantIDFromCtx(ctx)
|
|
m.mu.Lock()
|
|
page, err := m.getPageForTenant(targetID, tenantID)
|
|
m.mu.Unlock()
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if fullPage {
|
|
return page.Screenshot(fullPage, &proto.PageCaptureScreenshot{
|
|
Format: proto.PageCaptureScreenshotFormatPng,
|
|
})
|
|
}
|
|
return page.Screenshot(false, nil)
|
|
}
|
|
|
|
// Navigate navigates a page to a URL.
|
|
// A ctx-cancel watchdog closes the page if ctx is done during the blocking WaitStable call.
|
|
func (m *Manager) Navigate(ctx context.Context, targetID, url string) error {
|
|
tenantID := tenantIDFromCtx(ctx)
|
|
m.mu.Lock()
|
|
page, err := m.getPageForTenant(targetID, tenantID)
|
|
m.mu.Unlock()
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Watchdog: close page on ctx cancel to unblock any pending Rod CDP calls.
|
|
stop := watchPageClose(ctx, page)
|
|
defer stop()
|
|
|
|
if err := page.Navigate(url); err != nil {
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
return fmt.Errorf("navigate: %w", err)
|
|
}
|
|
if err := page.WaitStable(300 * time.Millisecond); err != nil {
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
return fmt.Errorf("wait stable after navigate: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Close shuts down the browser if running.
|
|
func (m *Manager) Close() error {
|
|
return m.Stop(context.Background())
|
|
}
|
|
|
|
// Refs returns the RefStore for external use (e.g. actions).
|
|
func (m *Manager) Refs() *RefStore {
|
|
return m.refs
|
|
}
|