mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-14 03:57:29 +08:00
webui phase2: add native nodes/cron api endpoints and management UI
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Record<string, ChatItem[]>>({ 'webui:default': [] })
|
||||
const [msg, setMsg] = useState('')
|
||||
const [nodes, setNodes] = useState<string>('[]')
|
||||
const [cron, setCron] = useState<CronJob[]>([])
|
||||
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() {
|
||||
<div className="panel-title">Config</div>
|
||||
<div className="row"><button onClick={loadConfig}>Load</button><button onClick={saveConfig}>Save+Reload</button></div>
|
||||
<textarea value={cfgText} onChange={(e) => setCfgText(e.target.value)} />
|
||||
<div className="panel-title">Cron</div>
|
||||
<div className="row"><button onClick={refreshCron}>Refresh</button></div>
|
||||
<div className="cron-list">
|
||||
{cron.map((j) => (
|
||||
<div key={j.id} className="cron-item">
|
||||
<div><strong>{j.name || j.id}</strong><div className="muted">{j.id}</div></div>
|
||||
<div className="row">
|
||||
<button onClick={() => cronAction(j.enabled ? 'disable' : 'enable', j.id)}>{j.enabled ? 'Disable' : 'Enable'}</button>
|
||||
<button onClick={() => cronAction('delete', j.id)}>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="panel-title">Nodes</div>
|
||||
<div className="row"><button onClick={refreshNodes}>Refresh</button></div>
|
||||
<pre>{nodes}</pre>
|
||||
|
||||
@@ -17,5 +17,9 @@ button{cursor:pointer;border:1px solid #374151;background:#0f172a;color:#fff;pad
|
||||
.row{display:flex;gap:8px;margin-bottom:8px}
|
||||
textarea{width:100%;min-height:180px;background:#0b1220;color:#e5e7eb;border:1px solid #374151;border-radius:8px;padding:8px}
|
||||
pre{flex:1;overflow:auto;background:#0b1220;border:1px solid #374151;border-radius:8px;padding:8px;white-space:pre-wrap}
|
||||
.cron-list{max-height:180px;overflow:auto;border:1px solid #374151;border-radius:8px;padding:6px;margin-bottom:8px;background:#0b1220}
|
||||
.cron-item{display:flex;justify-content:space-between;gap:8px;align-items:center;border-bottom:1px solid #1f2937;padding:6px 0}
|
||||
.cron-item:last-child{border-bottom:none}
|
||||
.muted{font-size:12px;color:#9ca3af}
|
||||
@media (max-width: 1024px){.layout{grid-template-columns:200px 1fr}.right{grid-column:1 / span 2;max-height:42vh}}
|
||||
@media (max-width: 768px){.layout{grid-template-columns:1fr}.sessions{order:2}.chat{order:1;min-height:50vh}.right{order:3}.topbar input{min-width:160px;width:52%}}
|
||||
|
||||
Reference in New Issue
Block a user