feat: waiter CLI remote mode calls REST API directly for management commands

This commit is contained in:
root
2026-07-16 20:44:29 +08:00
parent 951d31cca0
commit 95d9591375
3 changed files with 172 additions and 7 deletions

View File

@ -1,6 +1,7 @@
package main
import (
"encoding/json"
"fmt"
"strings"
)
@ -28,6 +29,7 @@ Server commands (sent to agent):
/plugin info <name> plugin details
/memory query <text> query graph memory
/knowledge list knowledge base
/knowledge delete <name> delete knowledge item
/agents list agents
/chat <text> send to agent
@ -64,7 +66,136 @@ Any other text is sent to the agent directly.`)
reconnect()
return true
case cmd == "/status":
if rc := state.RemoteConn(); rc != nil {
d, _ := rc.DoAPI("GET", "/api/v1/status", "")
printJSON(d)
} else {
state.Send("/status")
}
return true
case cmd == "/kernel":
if rc := state.RemoteConn(); rc != nil {
d, _ := rc.DoAPI("GET", "/api/v1/kernel", "")
printJSON(d)
} else {
state.Send("/kernel")
}
return true
case strings.HasPrefix(cmd, "/settings set "):
parts := strings.SplitN(cmd[14:], " ", 2)
if len(parts) < 2 {
fmt.Println("usage: /settings set <key> <value>")
return true
}
if rc := state.RemoteConn(); rc != nil {
body := fmt.Sprintf(`{"%s":%q}`, parts[0], parts[1])
rc.DoAPI("PUT", "/api/v1/settings", body)
fmt.Println("ok")
} else {
state.Send(cmd[1:])
}
return true
case strings.HasPrefix(cmd, "/settings"):
if rc := state.RemoteConn(); rc != nil {
d, _ := rc.DoAPI("GET", "/api/v1/settings", "")
printJSON(d)
} else {
state.Send(cmd[1:])
}
return true
case cmd == "/plugin list":
if rc := state.RemoteConn(); rc != nil {
d, _ := rc.DoAPI("GET", "/api/v1/plugins", "")
printJSON(d)
} else {
state.Send("/plugin list")
}
return true
case strings.HasPrefix(cmd, "/plugin install "):
url := strings.TrimSpace(cmd[16:])
if rc := state.RemoteConn(); rc != nil {
body := fmt.Sprintf(`{"url":%q}`, url)
d, _ := rc.DoAPI("POST", "/api/v1/plugins", body)
printJSON(d)
} else {
state.Send(cmd[1:])
}
return true
case strings.HasPrefix(cmd, "/plugin remove "):
name := strings.TrimSpace(cmd[15:])
if rc := state.RemoteConn(); rc != nil {
d, _ := rc.DoAPI("DELETE", "/api/v1/plugins/"+name, "")
printJSON(d)
} else {
state.Send(cmd[1:])
}
return true
case strings.HasPrefix(cmd, "/plugin info "):
name := strings.TrimSpace(cmd[13:])
if rc := state.RemoteConn(); rc != nil {
d, _ := rc.DoAPI("GET", "/api/v1/plugins/"+name, "")
printJSON(d)
} else {
state.Send(cmd[1:])
}
return true
case strings.HasPrefix(cmd, "/memory query "):
q := strings.TrimSpace(cmd[14:])
if rc := state.RemoteConn(); rc != nil {
d, _ := rc.DoAPI("GET", "/api/v1/memory?query="+q, "")
printJSON(d)
} else {
state.Send(cmd[1:])
}
return true
case strings.HasPrefix(cmd, "/knowledge delete "):
name := strings.TrimSpace(cmd[18:])
if rc := state.RemoteConn(); rc != nil {
d, _ := rc.DoAPI("DELETE", "/api/v1/knowledge/"+name, "")
printJSON(d)
} else {
state.Send(cmd[1:])
}
return true
case cmd == "/knowledge":
if rc := state.RemoteConn(); rc != nil {
d, _ := rc.DoAPI("GET", "/api/v1/knowledge", "")
printJSON(d)
} else {
state.Send("/knowledge")
}
return true
case cmd == "/agents":
if rc := state.RemoteConn(); rc != nil {
d, _ := rc.DoAPI("GET", "/api/v1/agents", "")
printJSON(d)
} else {
state.Send("/agents")
}
return true
default:
return false
}
}
func printJSON(d map[string]interface{}) {
if d == nil {
fmt.Println("(no data)")
return
}
b, _ := json.MarshalIndent(d, "", " ")
fmt.Println(string(b))
}

View File

@ -76,19 +76,19 @@ func (c *localConn) Close() error {
func dialRemote(baseURL, apiKey string) (Conn, error) {
baseURL = strings.TrimRight(baseURL, "/")
return &remoteConn{url: baseURL + "/api/v1/chat", apiKey: apiKey}, nil
return &remoteConn{baseURL: baseURL, apiKey: apiKey}, nil
}
type remoteConn struct {
url string
apiKey string
mu sync.Mutex
buf []string
closed bool
baseURL string
apiKey string
mu sync.Mutex
buf []string
closed bool
}
func (c *remoteConn) Send(line string) error {
req, err := http.NewRequest("POST", c.url, strings.NewReader(
req, err := http.NewRequest("POST", c.baseURL+"/api/v1/chat", strings.NewReader(
fmt.Sprintf(`{"message":%q}`, line),
))
if err != nil {
@ -117,6 +117,31 @@ func (c *remoteConn) Send(line string) error {
return nil
}
func (c *remoteConn) DoAPI(method, path string, body string) (map[string]interface{}, error) {
var bodyReader io.Reader
if body != "" {
bodyReader = strings.NewReader(body)
}
req, err := http.NewRequest(method, c.baseURL+path, bodyReader)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
if c.apiKey != "" {
req.Header.Set("X-API-Key", c.apiKey)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return result, nil
}
func (c *remoteConn) ReadLine() (string, error) {
c.mu.Lock()
defer c.mu.Unlock()

View File

@ -41,6 +41,15 @@ func (s *State) Connected() bool {
return s.conn != nil
}
func (s *State) RemoteConn() *remoteConn {
s.mu.Lock()
defer s.mu.Unlock()
if rc, ok := s.conn.(*remoteConn); ok {
return rc
}
return nil
}
func (s *State) Send(line string) error {
s.mu.Lock()
c := s.conn