mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-06-13 16:03:08 +08:00
feat: surface node p2p runtime visibility
This commit is contained in:
@@ -133,11 +133,25 @@ func gatewayCmd() {
|
|||||||
if loop == nil || server == nil || runtimeCfg == nil {
|
if loop == nil || server == nil || runtimeCfg == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
server.SetNodeP2PStatusHandler(func() map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"enabled": runtimeCfg.Gateway.Nodes.P2P.Enabled,
|
||||||
|
"transport": strings.TrimSpace(runtimeCfg.Gateway.Nodes.P2P.Transport),
|
||||||
|
"configured_stun": append([]string(nil), runtimeCfg.Gateway.Nodes.P2P.STUNServers...),
|
||||||
|
}
|
||||||
|
})
|
||||||
switch {
|
switch {
|
||||||
case runtimeCfg.Gateway.Nodes.P2P.Enabled && strings.EqualFold(strings.TrimSpace(runtimeCfg.Gateway.Nodes.P2P.Transport), "webrtc"):
|
case runtimeCfg.Gateway.Nodes.P2P.Enabled && strings.EqualFold(strings.TrimSpace(runtimeCfg.Gateway.Nodes.P2P.Transport), "webrtc"):
|
||||||
webrtcTransport := nodes.NewWebRTCTransport(runtimeCfg.Gateway.Nodes.P2P.STUNServers)
|
webrtcTransport := nodes.NewWebRTCTransport(runtimeCfg.Gateway.Nodes.P2P.STUNServers)
|
||||||
loop.SetNodeP2PTransport(webrtcTransport)
|
loop.SetNodeP2PTransport(webrtcTransport)
|
||||||
server.SetNodeWebRTCTransport(webrtcTransport)
|
server.SetNodeWebRTCTransport(webrtcTransport)
|
||||||
|
server.SetNodeP2PStatusHandler(func() map[string]interface{} {
|
||||||
|
snapshot := webrtcTransport.Snapshot()
|
||||||
|
snapshot["enabled"] = true
|
||||||
|
snapshot["transport"] = "webrtc"
|
||||||
|
snapshot["configured_stun"] = append([]string(nil), runtimeCfg.Gateway.Nodes.P2P.STUNServers...)
|
||||||
|
return snapshot
|
||||||
|
})
|
||||||
default:
|
default:
|
||||||
server.SetNodeWebRTCTransport(nil)
|
server.SetNodeWebRTCTransport(nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,11 +163,18 @@ func statusCmd() {
|
|||||||
}
|
}
|
||||||
fmt.Printf("Nodes: total=%d online=%d\n", len(ns), online)
|
fmt.Printf("Nodes: total=%d online=%d\n", len(ns), online)
|
||||||
fmt.Printf("Nodes Capabilities: run=%d model=%d camera=%d screen=%d location=%d canvas=%d\n", caps["run"], caps["model"], caps["camera"], caps["screen"], caps["location"], caps["canvas"])
|
fmt.Printf("Nodes Capabilities: run=%d model=%d camera=%d screen=%d location=%d canvas=%d\n", caps["run"], caps["model"], caps["camera"], caps["screen"], caps["location"], caps["canvas"])
|
||||||
if total, okCnt, avgMs, actionTop, err := collectNodeDispatchStats(filepath.Join(workspace, "memory", "nodes-dispatch-audit.jsonl")); err == nil && total > 0 {
|
fmt.Printf("Nodes P2P: enabled=%t transport=%s\n", cfg.Gateway.Nodes.P2P.Enabled, strings.TrimSpace(cfg.Gateway.Nodes.P2P.Transport))
|
||||||
|
if total, okCnt, avgMs, actionTop, transportTop, fallbackCnt, err := collectNodeDispatchStats(filepath.Join(workspace, "memory", "nodes-dispatch-audit.jsonl")); err == nil && total > 0 {
|
||||||
fmt.Printf("Nodes Dispatch: total=%d ok=%d fail=%d avg_ms=%d\n", total, okCnt, total-okCnt, avgMs)
|
fmt.Printf("Nodes Dispatch: total=%d ok=%d fail=%d avg_ms=%d\n", total, okCnt, total-okCnt, avgMs)
|
||||||
if actionTop != "" {
|
if actionTop != "" {
|
||||||
fmt.Printf("Nodes Dispatch Top Action: %s\n", actionTop)
|
fmt.Printf("Nodes Dispatch Top Action: %s\n", actionTop)
|
||||||
}
|
}
|
||||||
|
if transportTop != "" {
|
||||||
|
fmt.Printf("Nodes Dispatch Top Transport: %s\n", transportTop)
|
||||||
|
}
|
||||||
|
if fallbackCnt > 0 {
|
||||||
|
fmt.Printf("Nodes Dispatch Fallbacks: %d\n", fallbackCnt)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,23 +268,26 @@ func collectTriggerErrorCounts(path string) (map[string]int, error) {
|
|||||||
return counts, nil
|
return counts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectNodeDispatchStats(path string) (int, int, int, string, error) {
|
func collectNodeDispatchStats(path string) (int, int, int, string, string, int, error) {
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, 0, "", err
|
return 0, 0, 0, "", "", 0, err
|
||||||
}
|
}
|
||||||
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
|
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
|
||||||
total, okCnt, msSum := 0, 0, 0
|
total, okCnt, msSum, fallbackCnt := 0, 0, 0, 0
|
||||||
actionCnt := map[string]int{}
|
actionCnt := map[string]int{}
|
||||||
|
transportCnt := map[string]int{}
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
line = strings.TrimSpace(line)
|
line = strings.TrimSpace(line)
|
||||||
if line == "" {
|
if line == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var row struct {
|
var row struct {
|
||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
OK bool `json:"ok"`
|
UsedTransport string `json:"used_transport"`
|
||||||
DurationMS int `json:"duration_ms"`
|
FallbackFrom string `json:"fallback_from"`
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
DurationMS int `json:"duration_ms"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal([]byte(line), &row); err != nil {
|
if err := json.Unmarshal([]byte(line), &row); err != nil {
|
||||||
continue
|
continue
|
||||||
@@ -294,6 +304,13 @@ func collectNodeDispatchStats(path string) (int, int, int, string, error) {
|
|||||||
a = "unknown"
|
a = "unknown"
|
||||||
}
|
}
|
||||||
actionCnt[a]++
|
actionCnt[a]++
|
||||||
|
used := strings.TrimSpace(strings.ToLower(row.UsedTransport))
|
||||||
|
if used != "" {
|
||||||
|
transportCnt[used]++
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(row.FallbackFrom) != "" {
|
||||||
|
fallbackCnt++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
avg := 0
|
avg := 0
|
||||||
if total > 0 {
|
if total > 0 {
|
||||||
@@ -310,7 +327,18 @@ func collectNodeDispatchStats(path string) (int, int, int, string, error) {
|
|||||||
if topAction != "" {
|
if topAction != "" {
|
||||||
topAction = fmt.Sprintf("%s(%d)", topAction, topN)
|
topAction = fmt.Sprintf("%s(%d)", topAction, topN)
|
||||||
}
|
}
|
||||||
return total, okCnt, avg, topAction, nil
|
topTransport := ""
|
||||||
|
topTN := 0
|
||||||
|
for k, v := range transportCnt {
|
||||||
|
if v > topTN {
|
||||||
|
topTN = v
|
||||||
|
topTransport = k
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if topTransport != "" {
|
||||||
|
topTransport = fmt.Sprintf("%s(%d)", topTransport, topTN)
|
||||||
|
}
|
||||||
|
return total, okCnt, avg, topAction, topTransport, fallbackCnt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectSkillExecStats(path string) (int, int, int, float64, string, error) {
|
func collectSkillExecStats(path string) (int, int, int, float64, string, error) {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ type Server struct {
|
|||||||
nodeConnIDs map[string]string
|
nodeConnIDs map[string]string
|
||||||
nodeSockets map[string]*nodeSocketConn
|
nodeSockets map[string]*nodeSocketConn
|
||||||
nodeWebRTC *nodes.WebRTCTransport
|
nodeWebRTC *nodes.WebRTCTransport
|
||||||
|
nodeP2PStatus func() map[string]interface{}
|
||||||
gatewayVersion string
|
gatewayVersion string
|
||||||
webuiVersion string
|
webuiVersion string
|
||||||
configPath string
|
configPath string
|
||||||
@@ -120,6 +121,9 @@ func (s *Server) SetWebUIVersion(v string) { s.webuiVersion
|
|||||||
func (s *Server) SetNodeWebRTCTransport(t *nodes.WebRTCTransport) {
|
func (s *Server) SetNodeWebRTCTransport(t *nodes.WebRTCTransport) {
|
||||||
s.nodeWebRTC = t
|
s.nodeWebRTC = t
|
||||||
}
|
}
|
||||||
|
func (s *Server) SetNodeP2PStatusHandler(fn func() map[string]interface{}) {
|
||||||
|
s.nodeP2PStatus = fn
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) rememberNodeConnection(nodeID, connID string) {
|
func (s *Server) rememberNodeConnection(nodeID, connID string) {
|
||||||
nodeID = strings.TrimSpace(nodeID)
|
nodeID = strings.TrimSpace(nodeID)
|
||||||
@@ -1066,9 +1070,14 @@ func (s *Server) webUINodesPayload(ctx context.Context) map[string]interface{} {
|
|||||||
if !matched {
|
if !matched {
|
||||||
list = append([]nodes.NodeInfo{local}, list...)
|
list = append([]nodes.NodeInfo{local}, list...)
|
||||||
}
|
}
|
||||||
|
p2p := map[string]interface{}{}
|
||||||
|
if s.nodeP2PStatus != nil {
|
||||||
|
p2p = s.nodeP2PStatus()
|
||||||
|
}
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"nodes": list,
|
"nodes": list,
|
||||||
"trees": s.buildNodeAgentTrees(ctx, list),
|
"trees": s.buildNodeAgentTrees(ctx, list),
|
||||||
|
"p2p": p2p,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1548,7 +1557,11 @@ func (s *Server) handleWebUINodes(w http.ResponseWriter, r *http.Request) {
|
|||||||
list = append([]nodes.NodeInfo{local}, list...)
|
list = append([]nodes.NodeInfo{local}, list...)
|
||||||
}
|
}
|
||||||
trees := s.buildNodeAgentTrees(r.Context(), list)
|
trees := s.buildNodeAgentTrees(r.Context(), list)
|
||||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "nodes": list, "trees": trees})
|
p2p := map[string]interface{}{}
|
||||||
|
if s.nodeP2PStatus != nil {
|
||||||
|
p2p = s.nodeP2PStatus()
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "nodes": list, "trees": trees, "p2p": p2p})
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
var body struct {
|
var body struct {
|
||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
|
|||||||
@@ -428,3 +428,31 @@ func TestHandleWebUILogsLive(t *testing.T) {
|
|||||||
t.Fatalf("expected tail-ok entry, got: %+v", entry)
|
t.Fatalf("expected tail-ok entry, got: %+v", entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandleWebUINodesIncludesP2PSummary(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
srv := NewServer("127.0.0.1", 0, "", nodes.NewManager())
|
||||||
|
srv.SetNodeP2PStatusHandler(func() map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"enabled": true,
|
||||||
|
"transport": "webrtc",
|
||||||
|
"active_sessions": 2,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/webui/api/nodes", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.handleWebUINodes(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
var body map[string]interface{}
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||||
|
t.Fatalf("decode body: %v", err)
|
||||||
|
}
|
||||||
|
p2p, _ := body["p2p"].(map[string]interface{})
|
||||||
|
if p2p == nil || p2p["transport"] != "webrtc" {
|
||||||
|
t.Fatalf("expected p2p summary, got %+v", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,25 +33,44 @@ func (r *Router) Dispatch(ctx context.Context, req Request, mode string) (Respon
|
|||||||
if r.P2P == nil {
|
if r.P2P == nil {
|
||||||
return Response{OK: false, Node: req.Node, Action: req.Action, Error: "p2p transport unavailable"}, nil
|
return Response{OK: false, Node: req.Node, Action: req.Action, Error: "p2p transport unavailable"}, nil
|
||||||
}
|
}
|
||||||
return r.P2P.Send(ctx, req)
|
resp, err := r.P2P.Send(ctx, req)
|
||||||
|
return annotateTransport(resp, "p2p", r.P2P.Name(), ""), err
|
||||||
case "relay":
|
case "relay":
|
||||||
if r.Relay == nil {
|
if r.Relay == nil {
|
||||||
return Response{OK: false, Node: req.Node, Action: req.Action, Error: "relay transport unavailable"}, nil
|
return Response{OK: false, Node: req.Node, Action: req.Action, Error: "relay transport unavailable"}, nil
|
||||||
}
|
}
|
||||||
return r.Relay.Send(ctx, req)
|
resp, err := r.Relay.Send(ctx, req)
|
||||||
|
return annotateTransport(resp, "relay", r.Relay.Name(), ""), err
|
||||||
default: // auto
|
default: // auto
|
||||||
if r.P2P != nil {
|
if r.P2P != nil {
|
||||||
if resp, err := r.P2P.Send(ctx, req); err == nil && resp.OK {
|
if resp, err := r.P2P.Send(ctx, req); err == nil && resp.OK {
|
||||||
return resp, nil
|
return annotateTransport(resp, "auto", r.P2P.Name(), ""), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if r.Relay != nil {
|
if r.Relay != nil {
|
||||||
return r.Relay.Send(ctx, req)
|
resp, err := r.Relay.Send(ctx, req)
|
||||||
|
return annotateTransport(resp, "auto", r.Relay.Name(), "p2p"), err
|
||||||
}
|
}
|
||||||
return Response{}, fmt.Errorf("no transport available")
|
return Response{}, fmt.Errorf("no transport available")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func annotateTransport(resp Response, mode, usedTransport, fallbackFrom string) Response {
|
||||||
|
if resp.Payload == nil {
|
||||||
|
resp.Payload = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(mode) != "" {
|
||||||
|
resp.Payload["dispatch_mode"] = strings.TrimSpace(mode)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(usedTransport) != "" {
|
||||||
|
resp.Payload["used_transport"] = strings.TrimSpace(usedTransport)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(fallbackFrom) != "" {
|
||||||
|
resp.Payload["fallback_from"] = strings.TrimSpace(fallbackFrom)
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
// WebsocketP2PTransport uses the persistent node websocket as a request/response tunnel
|
// WebsocketP2PTransport uses the persistent node websocket as a request/response tunnel
|
||||||
// while the project evolves toward a true peer data channel.
|
// while the project evolves toward a true peer data channel.
|
||||||
type WebsocketP2PTransport struct {
|
type WebsocketP2PTransport struct {
|
||||||
|
|||||||
@@ -71,6 +71,29 @@ func NewWebRTCTransport(stunServers []string) *WebRTCTransport {
|
|||||||
|
|
||||||
func (t *WebRTCTransport) Name() string { return "p2p-webrtc" }
|
func (t *WebRTCTransport) Name() string { return "p2p-webrtc" }
|
||||||
|
|
||||||
|
func (t *WebRTCTransport) Snapshot() map[string]interface{} {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
nodes := make([]map[string]interface{}, 0, len(t.sessions))
|
||||||
|
active := 0
|
||||||
|
for nodeID, session := range t.sessions {
|
||||||
|
status := "connecting"
|
||||||
|
if session != nil && session.dc != nil && session.dc.ReadyState() == webrtc.DataChannelStateOpen {
|
||||||
|
status = "open"
|
||||||
|
active++
|
||||||
|
}
|
||||||
|
nodes = append(nodes, map[string]interface{}{
|
||||||
|
"node": nodeID,
|
||||||
|
"status": status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return map[string]interface{}{
|
||||||
|
"transport": "webrtc",
|
||||||
|
"active_sessions": active,
|
||||||
|
"nodes": nodes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (t *WebRTCTransport) BindSignaler(nodeID string, sender WireSender) {
|
func (t *WebRTCTransport) BindSignaler(nodeID string, sender WireSender) {
|
||||||
nodeID = strings.TrimSpace(nodeID)
|
nodeID = strings.TrimSpace(nodeID)
|
||||||
if nodeID == "" {
|
if nodeID == "" {
|
||||||
|
|||||||
@@ -22,20 +22,20 @@ type NodesTool struct {
|
|||||||
func NewNodesTool(m *nodes.Manager, r *nodes.Router, auditPath string) *NodesTool {
|
func NewNodesTool(m *nodes.Manager, r *nodes.Router, auditPath string) *NodesTool {
|
||||||
return &NodesTool{manager: m, router: r, auditPath: strings.TrimSpace(auditPath)}
|
return &NodesTool{manager: m, router: r, auditPath: strings.TrimSpace(auditPath)}
|
||||||
}
|
}
|
||||||
func (t *NodesTool) Name() string { return "nodes" }
|
func (t *NodesTool) Name() string { return "nodes" }
|
||||||
func (t *NodesTool) Description() string {
|
func (t *NodesTool) Description() string {
|
||||||
return "Manage paired nodes (status/describe/run/invoke/camera/screen/location/canvas)."
|
return "Manage paired nodes (status/describe/run/invoke/camera/screen/location/canvas)."
|
||||||
}
|
}
|
||||||
func (t *NodesTool) Parameters() map[string]interface{} {
|
func (t *NodesTool) Parameters() map[string]interface{} {
|
||||||
return map[string]interface{}{"type": "object", "properties": map[string]interface{}{
|
return map[string]interface{}{"type": "object", "properties": map[string]interface{}{
|
||||||
"action": map[string]interface{}{"type": "string", "description": "status|describe|run|invoke|agent_task|camera_snap|camera_clip|screen_record|screen_snapshot|location_get|canvas_snapshot|canvas_action"},
|
"action": map[string]interface{}{"type": "string", "description": "status|describe|run|invoke|agent_task|camera_snap|camera_clip|screen_record|screen_snapshot|location_get|canvas_snapshot|canvas_action"},
|
||||||
"node": map[string]interface{}{"type": "string", "description": "target node id"},
|
"node": map[string]interface{}{"type": "string", "description": "target node id"},
|
||||||
"mode": map[string]interface{}{"type": "string", "description": "auto|p2p|relay (default auto)"},
|
"mode": map[string]interface{}{"type": "string", "description": "auto|p2p|relay (default auto)"},
|
||||||
"args": map[string]interface{}{"type": "object", "description": "action args"},
|
"args": map[string]interface{}{"type": "object", "description": "action args"},
|
||||||
"task": map[string]interface{}{"type": "string", "description": "agent_task content for child node model"},
|
"task": map[string]interface{}{"type": "string", "description": "agent_task content for child node model"},
|
||||||
"model": map[string]interface{}{"type": "string", "description": "optional model for agent_task"},
|
"model": map[string]interface{}{"type": "string", "description": "optional model for agent_task"},
|
||||||
"command": map[string]interface{}{"type": "array", "items": map[string]interface{}{"type": "string"}, "description": "run command array shortcut"},
|
"command": map[string]interface{}{"type": "array", "items": map[string]interface{}{"type": "string"}, "description": "run command array shortcut"},
|
||||||
"facing": map[string]interface{}{"type": "string", "description": "camera facing: front|back|both"},
|
"facing": map[string]interface{}{"type": "string", "description": "camera facing: front|back|both"},
|
||||||
"duration_ms": map[string]interface{}{"type": "integer", "description": "clip/record duration"},
|
"duration_ms": map[string]interface{}{"type": "integer", "description": "clip/record duration"},
|
||||||
}, "required": []string{"action"}}
|
}, "required": []string{"action"}}
|
||||||
}
|
}
|
||||||
@@ -144,6 +144,12 @@ func (t *NodesTool) writeAudit(req nodes.Request, resp nodes.Response, mode stri
|
|||||||
"error": resp.Error,
|
"error": resp.Error,
|
||||||
"duration_ms": durationMs,
|
"duration_ms": durationMs,
|
||||||
}
|
}
|
||||||
|
if used, _ := resp.Payload["used_transport"].(string); strings.TrimSpace(used) != "" {
|
||||||
|
row["used_transport"] = strings.TrimSpace(used)
|
||||||
|
}
|
||||||
|
if fallback, _ := resp.Payload["fallback_from"].(string); strings.TrimSpace(fallback) != "" {
|
||||||
|
row["fallback_from"] = strings.TrimSpace(fallback)
|
||||||
|
}
|
||||||
b, _ := json.Marshal(row)
|
b, _ := json.Marshal(row)
|
||||||
f, err := os.OpenFile(t.auditPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
f, err := os.OpenFile(t.auditPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ type RuntimeSnapshot = {
|
|||||||
nodes?: {
|
nodes?: {
|
||||||
nodes?: any[];
|
nodes?: any[];
|
||||||
trees?: any[];
|
trees?: any[];
|
||||||
|
p2p?: Record<string, any>;
|
||||||
};
|
};
|
||||||
sessions?: {
|
sessions?: {
|
||||||
sessions?: Array<{ key: string; title?: string; channel?: string }>;
|
sessions?: Array<{ key: string; title?: string; channel?: string }>;
|
||||||
@@ -43,6 +44,8 @@ interface AppContextType {
|
|||||||
setNodes: (nodes: string) => void;
|
setNodes: (nodes: string) => void;
|
||||||
nodeTrees: string;
|
nodeTrees: string;
|
||||||
setNodeTrees: (trees: string) => void;
|
setNodeTrees: (trees: string) => void;
|
||||||
|
nodeP2P: Record<string, any>;
|
||||||
|
setNodeP2P: React.Dispatch<React.SetStateAction<Record<string, any>>>;
|
||||||
cron: CronJob[];
|
cron: CronJob[];
|
||||||
setCron: (cron: CronJob[]) => void;
|
setCron: (cron: CronJob[]) => void;
|
||||||
skills: Skill[];
|
skills: Skill[];
|
||||||
@@ -103,6 +106,7 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
const [configEditing, setConfigEditing] = useState(false);
|
const [configEditing, setConfigEditing] = useState(false);
|
||||||
const [nodes, setNodes] = useState('[]');
|
const [nodes, setNodes] = useState('[]');
|
||||||
const [nodeTrees, setNodeTrees] = useState('[]');
|
const [nodeTrees, setNodeTrees] = useState('[]');
|
||||||
|
const [nodeP2P, setNodeP2P] = useState<Record<string, any>>({});
|
||||||
const [cron, setCron] = useState<CronJob[]>([]);
|
const [cron, setCron] = useState<CronJob[]>([]);
|
||||||
const [skills, setSkills] = useState<Skill[]>([]);
|
const [skills, setSkills] = useState<Skill[]>([]);
|
||||||
const [clawhubInstalled, setClawhubInstalled] = useState(false);
|
const [clawhubInstalled, setClawhubInstalled] = useState(false);
|
||||||
@@ -161,6 +165,7 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
setNodes(JSON.stringify(j.nodes || [], null, 2));
|
setNodes(JSON.stringify(j.nodes || [], null, 2));
|
||||||
setNodeTrees(JSON.stringify(j.trees || [], null, 2));
|
setNodeTrees(JSON.stringify(j.trees || [], null, 2));
|
||||||
|
setNodeP2P(j.p2p || {});
|
||||||
setIsGatewayOnline(true);
|
setIsGatewayOnline(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setIsGatewayOnline(false);
|
setIsGatewayOnline(false);
|
||||||
@@ -265,6 +270,7 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
if (snapshot.nodes) {
|
if (snapshot.nodes) {
|
||||||
setNodes(JSON.stringify(Array.isArray(snapshot.nodes.nodes) ? snapshot.nodes.nodes : [], null, 2));
|
setNodes(JSON.stringify(Array.isArray(snapshot.nodes.nodes) ? snapshot.nodes.nodes : [], null, 2));
|
||||||
setNodeTrees(JSON.stringify(Array.isArray(snapshot.nodes.trees) ? snapshot.nodes.trees : [], null, 2));
|
setNodeTrees(JSON.stringify(Array.isArray(snapshot.nodes.trees) ? snapshot.nodes.trees : [], null, 2));
|
||||||
|
setNodeP2P(snapshot.nodes.p2p || {});
|
||||||
}
|
}
|
||||||
if (snapshot.sessions) {
|
if (snapshot.sessions) {
|
||||||
const arr = Array.isArray(snapshot.sessions.sessions) ? snapshot.sessions.sessions : [];
|
const arr = Array.isArray(snapshot.sessions.sessions) ? snapshot.sessions.sessions : [];
|
||||||
@@ -343,7 +349,7 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
return (
|
return (
|
||||||
<AppContext.Provider value={{
|
<AppContext.Provider value={{
|
||||||
token, setToken, sidebarOpen, setSidebarOpen, sidebarCollapsed, setSidebarCollapsed, isGatewayOnline, setIsGatewayOnline,
|
token, setToken, sidebarOpen, setSidebarOpen, sidebarCollapsed, setSidebarCollapsed, isGatewayOnline, setIsGatewayOnline,
|
||||||
cfg, setCfg, cfgRaw, setCfgRaw, configEditing, setConfigEditing, nodes, setNodes, nodeTrees, setNodeTrees,
|
cfg, setCfg, cfgRaw, setCfgRaw, configEditing, setConfigEditing, nodes, setNodes, nodeTrees, setNodeTrees, nodeP2P, setNodeP2P,
|
||||||
cron, setCron, skills, setSkills, clawhubInstalled, clawhubPath,
|
cron, setCron, skills, setSkills, clawhubInstalled, clawhubPath,
|
||||||
sessions, setSessions,
|
sessions, setSessions,
|
||||||
taskQueueItems, setTaskQueueItems, ekgSummary, setEkgSummary,
|
taskQueueItems, setTaskQueueItems, ekgSummary, setEkgSummary,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const resources = {
|
|||||||
tasks: 'Tasks',
|
tasks: 'Tasks',
|
||||||
subagentProfiles: 'Subagent Profiles',
|
subagentProfiles: 'Subagent Profiles',
|
||||||
subagentsRuntime: 'Agents',
|
subagentsRuntime: 'Agents',
|
||||||
|
nodeP2P: 'Node P2P',
|
||||||
agentTopology: 'Agent Topology',
|
agentTopology: 'Agent Topology',
|
||||||
agentTopologyHint: 'Unified graph for local agents, registered nodes, and mirrored remote agent branches.',
|
agentTopologyHint: 'Unified graph for local agents, registered nodes, and mirrored remote agent branches.',
|
||||||
runningTasks: 'running',
|
runningTasks: 'running',
|
||||||
@@ -549,6 +550,7 @@ const resources = {
|
|||||||
tasks: '任务管理',
|
tasks: '任务管理',
|
||||||
subagentProfiles: '子代理档案',
|
subagentProfiles: '子代理档案',
|
||||||
subagentsRuntime: 'Agents',
|
subagentsRuntime: 'Agents',
|
||||||
|
nodeP2P: '节点 P2P',
|
||||||
agentTopology: 'Agent 拓扑',
|
agentTopology: 'Agent 拓扑',
|
||||||
agentTopologyHint: '统一展示本地 agent、注册 node 以及远端镜像 agent 分支的关系图。',
|
agentTopologyHint: '统一展示本地 agent、注册 node 以及远端镜像 agent 分支的关系图。',
|
||||||
runningTasks: '运行中',
|
runningTasks: '运行中',
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const Dashboard: React.FC = () => {
|
|||||||
webuiVersion,
|
webuiVersion,
|
||||||
skills,
|
skills,
|
||||||
cfg,
|
cfg,
|
||||||
|
nodeP2P,
|
||||||
taskQueueItems,
|
taskQueueItems,
|
||||||
ekgSummary,
|
ekgSummary,
|
||||||
} = useAppContext();
|
} = useAppContext();
|
||||||
@@ -36,6 +37,9 @@ const Dashboard: React.FC = () => {
|
|||||||
const ekgEscalationCount = Number(ekgSummary?.escalation_count || 0);
|
const ekgEscalationCount = Number(ekgSummary?.escalation_count || 0);
|
||||||
const ekgTopProvider = (Array.isArray(ekgSummary?.provider_top_workload) ? ekgSummary.provider_top_workload[0]?.key : '') || '-';
|
const ekgTopProvider = (Array.isArray(ekgSummary?.provider_top_workload) ? ekgSummary.provider_top_workload[0]?.key : '') || '-';
|
||||||
const ekgTopErrSig = (Array.isArray(ekgSummary?.errsig_top_workload) ? ekgSummary.errsig_top_workload[0]?.key : '') || '-';
|
const ekgTopErrSig = (Array.isArray(ekgSummary?.errsig_top_workload) ? ekgSummary.errsig_top_workload[0]?.key : '') || '-';
|
||||||
|
const p2pEnabled = Boolean(nodeP2P?.enabled);
|
||||||
|
const p2pTransport = String(nodeP2P?.transport || (p2pEnabled ? 'enabled' : 'disabled'));
|
||||||
|
const p2pSessions = Number(nodeP2P?.active_sessions || 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 md:p-6 xl:p-8 w-full space-y-6 xl:space-y-8">
|
<div className="p-4 md:p-6 xl:p-8 w-full space-y-6 xl:space-y-8">
|
||||||
@@ -53,12 +57,13 @@ const Dashboard: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-5 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-6 gap-4">
|
||||||
<StatCard title={t('gatewayStatus')} value={isGatewayOnline ? t('online') : t('offline')} icon={<Activity className={`w-6 h-6 ${isGatewayOnline ? 'text-emerald-400' : 'text-red-400'}`} />} />
|
<StatCard title={t('gatewayStatus')} value={isGatewayOnline ? t('online') : t('offline')} icon={<Activity className={`w-6 h-6 ${isGatewayOnline ? 'text-emerald-400' : 'text-red-400'}`} />} />
|
||||||
<StatCard title={t('activeSessions')} value={sessions.length} icon={<MessageSquare className="w-6 h-6 text-blue-400" />} />
|
<StatCard title={t('activeSessions')} value={sessions.length} icon={<MessageSquare className="w-6 h-6 text-blue-400" />} />
|
||||||
<StatCard title={t('skills')} value={skills.length} icon={<Sparkles className="w-6 h-6 text-pink-400" />} />
|
<StatCard title={t('skills')} value={skills.length} icon={<Sparkles className="w-6 h-6 text-pink-400" />} />
|
||||||
<StatCard title={t('subagentsRuntime')} value={subagentCount} icon={<Wrench className="w-6 h-6 text-cyan-400" />} />
|
<StatCard title={t('subagentsRuntime')} value={subagentCount} icon={<Wrench className="w-6 h-6 text-cyan-400" />} />
|
||||||
<StatCard title={t('taskAudit')} value={recentTasks.length} icon={<Activity className="w-6 h-6 text-amber-400" />} />
|
<StatCard title={t('taskAudit')} value={recentTasks.length} icon={<Activity className="w-6 h-6 text-amber-400" />} />
|
||||||
|
<StatCard title={t('nodeP2P')} value={p2pEnabled ? `${p2pSessions} · ${p2pTransport}` : t('disabled')} icon={<Workflow className="w-6 h-6 text-violet-400" />} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user