mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-13 21:57:29 +08:00
166 lines
3.4 KiB
Go
166 lines
3.4 KiB
Go
package nodes
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const defaultNodeTTL = 60 * time.Second
|
|
|
|
// Manager keeps paired node metadata and basic routing helpers.
|
|
type Handler func(req Request) Response
|
|
|
|
type Manager struct {
|
|
mu sync.RWMutex
|
|
nodes map[string]NodeInfo
|
|
handlers map[string]Handler
|
|
ttl time.Duration
|
|
}
|
|
|
|
var defaultManager = NewManager()
|
|
|
|
func DefaultManager() *Manager { return defaultManager }
|
|
|
|
func NewManager() *Manager {
|
|
m := &Manager{nodes: map[string]NodeInfo{}, handlers: map[string]Handler{}, ttl: defaultNodeTTL}
|
|
go m.reaperLoop()
|
|
return m
|
|
}
|
|
|
|
func (m *Manager) Upsert(info NodeInfo) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
info.LastSeenAt = time.Now().UTC()
|
|
info.Online = true
|
|
m.nodes[info.ID] = info
|
|
}
|
|
|
|
func (m *Manager) MarkOffline(id string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if n, ok := m.nodes[id]; ok {
|
|
n.Online = false
|
|
m.nodes[id] = n
|
|
}
|
|
}
|
|
|
|
func (m *Manager) Get(id string) (NodeInfo, bool) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
n, ok := m.nodes[id]
|
|
return n, ok
|
|
}
|
|
|
|
func (m *Manager) List() []NodeInfo {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
out := make([]NodeInfo, 0, len(m.nodes))
|
|
for _, n := range m.nodes {
|
|
out = append(out, n)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].LastSeenAt.After(out[j].LastSeenAt) })
|
|
return out
|
|
}
|
|
|
|
func (m *Manager) RegisterHandler(nodeID string, h Handler) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if strings.TrimSpace(nodeID) == "" || h == nil {
|
|
return
|
|
}
|
|
m.handlers[nodeID] = h
|
|
}
|
|
|
|
func (m *Manager) Invoke(req Request) (Response, bool) {
|
|
m.mu.RLock()
|
|
h, ok := m.handlers[req.Node]
|
|
m.mu.RUnlock()
|
|
if !ok {
|
|
return Response{}, false
|
|
}
|
|
resp := h(req)
|
|
if strings.TrimSpace(resp.Node) == "" {
|
|
resp.Node = req.Node
|
|
}
|
|
if strings.TrimSpace(resp.Action) == "" {
|
|
resp.Action = req.Action
|
|
}
|
|
return resp, true
|
|
}
|
|
|
|
func (m *Manager) SupportsAction(nodeID, action string) bool {
|
|
n, ok := m.Get(nodeID)
|
|
if !ok || !n.Online {
|
|
return false
|
|
}
|
|
switch strings.ToLower(strings.TrimSpace(action)) {
|
|
case "run":
|
|
return n.Capabilities.Run
|
|
case "camera_snap", "camera_clip":
|
|
return n.Capabilities.Camera
|
|
case "screen_record", "screen_snapshot":
|
|
return n.Capabilities.Screen
|
|
case "location_get":
|
|
return n.Capabilities.Location
|
|
case "canvas_snapshot", "canvas_action":
|
|
return n.Capabilities.Canvas
|
|
default:
|
|
return n.Capabilities.Invoke
|
|
}
|
|
}
|
|
|
|
func (m *Manager) PickFor(action string) (NodeInfo, bool) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
for _, n := range m.nodes {
|
|
if !n.Online {
|
|
continue
|
|
}
|
|
switch strings.ToLower(strings.TrimSpace(action)) {
|
|
case "run":
|
|
if n.Capabilities.Run {
|
|
return n, true
|
|
}
|
|
case "camera_snap", "camera_clip":
|
|
if n.Capabilities.Camera {
|
|
return n, true
|
|
}
|
|
case "screen_record", "screen_snapshot":
|
|
if n.Capabilities.Screen {
|
|
return n, true
|
|
}
|
|
case "location_get":
|
|
if n.Capabilities.Location {
|
|
return n, true
|
|
}
|
|
case "canvas_snapshot", "canvas_action":
|
|
if n.Capabilities.Canvas {
|
|
return n, true
|
|
}
|
|
default:
|
|
if n.Capabilities.Invoke {
|
|
return n, true
|
|
}
|
|
}
|
|
}
|
|
return NodeInfo{}, false
|
|
}
|
|
|
|
func (m *Manager) reaperLoop() {
|
|
t := time.NewTicker(15 * time.Second)
|
|
defer t.Stop()
|
|
for range t.C {
|
|
cutoff := time.Now().UTC().Add(-m.ttl)
|
|
m.mu.Lock()
|
|
for id, n := range m.nodes {
|
|
if n.Online && !n.LastSeenAt.IsZero() && n.LastSeenAt.Before(cutoff) {
|
|
n.Online = false
|
|
m.nodes[id] = n
|
|
}
|
|
}
|
|
m.mu.Unlock()
|
|
}
|
|
}
|