From deaeac8cba07b8f5a782f6ee949e8221447e2ef1 Mon Sep 17 00:00:00 2001 From: DBT Date: Wed, 25 Feb 2026 13:11:12 +0000 Subject: [PATCH] webui phase2: add native nodes/cron api endpoints and management UI --- cmd/clawgo/cmd_gateway.go | 16 ++++++++++ pkg/nodes/registry_server.go | 62 +++++++++++++++++++++++++++++++++++- webui/src/App.tsx | 41 ++++++++++++++++++------ webui/src/styles.css | 4 +++ 4 files changed, 113 insertions(+), 10 deletions(-) diff --git a/cmd/clawgo/cmd_gateway.go b/cmd/clawgo/cmd_gateway.go index ff112a6..0da4270 100644 --- a/cmd/clawgo/cmd_gateway.go +++ b/cmd/clawgo/cmd_gateway.go @@ -189,6 +189,22 @@ func gatewayCmd() { registryServer.SetConfigAfterHook(func() { _ = syscall.Kill(os.Getpid(), syscall.SIGHUP) }) + registryServer.SetCronHandler(func(action, id string) (interface{}, error) { + switch strings.ToLower(strings.TrimSpace(action)) { + case "", "list": + return cronService.ListJobs(true), nil + case "delete": + return map[string]interface{}{"deleted": cronService.RemoveJob(strings.TrimSpace(id)), "id": strings.TrimSpace(id)}, nil + case "enable": + j := cronService.EnableJob(strings.TrimSpace(id), true) + return map[string]interface{}{"ok": j != nil, "id": strings.TrimSpace(id)}, nil + case "disable": + j := cronService.EnableJob(strings.TrimSpace(id), false) + return map[string]interface{}{"ok": j != nil, "id": strings.TrimSpace(id)}, nil + default: + return nil, fmt.Errorf("unsupported cron action: %s", action) + } + }) if err := registryServer.Start(ctx); err != nil { fmt.Printf("Error starting node registry server: %v\n", err) } else { diff --git a/pkg/nodes/registry_server.go b/pkg/nodes/registry_server.go index 21b06f2..eea26d9 100644 --- a/pkg/nodes/registry_server.go +++ b/pkg/nodes/registry_server.go @@ -21,6 +21,7 @@ type RegistryServer struct { configPath string onChat func(ctx context.Context, sessionKey, content string) (string, error) onConfigAfter func() + onCron func(action, id string) (interface{}, error) webUIDir string } @@ -40,7 +41,10 @@ func (s *RegistryServer) SetChatHandler(fn func(ctx context.Context, sessionKey, s.onChat = fn } func (s *RegistryServer) SetConfigAfterHook(fn func()) { s.onConfigAfter = fn } -func (s *RegistryServer) SetWebUIDir(dir string) { s.webUIDir = strings.TrimSpace(dir) } +func (s *RegistryServer) SetCronHandler(fn func(action, id string) (interface{}, error)) { + s.onCron = fn +} +func (s *RegistryServer) SetWebUIDir(dir string) { s.webUIDir = strings.TrimSpace(dir) } func (s *RegistryServer) Start(ctx context.Context) error { if s.mgr == nil { @@ -58,6 +62,8 @@ func (s *RegistryServer) Start(ctx context.Context) error { mux.HandleFunc("/webui/api/config", s.handleWebUIConfig) mux.HandleFunc("/webui/api/chat", s.handleWebUIChat) mux.HandleFunc("/webui/api/upload", s.handleWebUIUpload) + mux.HandleFunc("/webui/api/nodes", s.handleWebUINodes) + mux.HandleFunc("/webui/api/cron", s.handleWebUICron) s.server = &http.Server{Addr: s.addr, Handler: mux} go func() { <-ctx.Done() @@ -301,6 +307,60 @@ func (s *RegistryServer) handleWebUIChat(w http.ResponseWriter, r *http.Request) _ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "reply": resp, "session": session}) } +func (s *RegistryServer) handleWebUINodes(w http.ResponseWriter, r *http.Request) { + if !s.checkAuth(r) { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + list := []NodeInfo{} + if s.mgr != nil { + list = s.mgr.List() + } + _ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "nodes": list}) +} + +func (s *RegistryServer) handleWebUICron(w http.ResponseWriter, r *http.Request) { + if !s.checkAuth(r) { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + if s.onCron == nil { + http.Error(w, "cron handler not configured", http.StatusInternalServerError) + return + } + if r.Method == http.MethodGet { + res, err := s.onCron("list", "") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "jobs": res}) + return + } + if r.Method == http.MethodPost { + var body struct { + Action string `json:"action"` + ID string `json:"id"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + http.Error(w, "invalid json", http.StatusBadRequest) + return + } + res, err := s.onCron(strings.ToLower(strings.TrimSpace(body.Action)), strings.TrimSpace(body.ID)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "result": res}) + return + } + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) +} + func (s *RegistryServer) checkAuth(r *http.Request) bool { if s.token == "" { return true diff --git a/webui/src/App.tsx b/webui/src/App.tsx index 609850b..1d602bd 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -1,8 +1,8 @@ import { useEffect, useMemo, useState } from 'react' type ChatItem = { role: 'user' | 'assistant'; text: string } - type Session = { key: string; title: string } +type CronJob = { id: string; name: string; enabled: boolean; schedule?: { kind?: string } } const defaultSessions: Session[] = [{ key: 'webui:default', title: 'Default' }] @@ -14,6 +14,7 @@ export function App() { const [chat, setChat] = useState>({ 'webui:default': [] }) const [msg, setMsg] = useState('') const [nodes, setNodes] = useState('[]') + const [cron, setCron] = useState([]) const activeChat = useMemo(() => chat[active] || [], [chat, active]) const q = token ? `?token=${encodeURIComponent(token)}` : '' @@ -34,17 +35,24 @@ export function App() { } async function refreshNodes() { - const payload = { - session: active, - message: '调用nodes工具,action=status,并输出JSON。', - } - const r = await fetch(`/webui/api/chat${q}`, { + const r = await fetch(`/webui/api/nodes${q}`) + const j = await r.json() + setNodes(JSON.stringify(j.nodes || [], null, 2)) + } + + async function refreshCron() { + const r = await fetch(`/webui/api/cron${q}`) + const j = await r.json() + setCron(j.jobs || []) + } + + async function cronAction(action: 'delete' | 'enable' | 'disable', id: string) { + await fetch(`/webui/api/cron${q}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), + body: JSON.stringify({ action, id }), }) - const t = await r.text() - setNodes(t) + await refreshCron() } async function send() { @@ -84,6 +92,8 @@ export function App() { useEffect(() => { loadConfig().catch(() => {}) + refreshNodes().catch(() => {}) + refreshCron().catch(() => {}) }, []) return ( @@ -122,6 +132,19 @@ export function App() {
Config