mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-13 06:47:30 +08:00
293 lines
7.4 KiB
Go
293 lines
7.4 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/YspCoder/clawgo/pkg/tools"
|
|
)
|
|
|
|
func resolveRelativeFilePath(root, raw string) (string, string, error) {
|
|
root = strings.TrimSpace(root)
|
|
if root == "" {
|
|
return "", "", fmt.Errorf("workspace not configured")
|
|
}
|
|
clean := filepath.Clean(strings.TrimSpace(raw))
|
|
if clean == "." || clean == "" || strings.HasPrefix(clean, "..") || filepath.IsAbs(clean) {
|
|
return "", "", fmt.Errorf("invalid path")
|
|
}
|
|
full := filepath.Join(root, clean)
|
|
cleanRoot := filepath.Clean(root)
|
|
if full != cleanRoot {
|
|
prefix := cleanRoot + string(os.PathSeparator)
|
|
if !strings.HasPrefix(filepath.Clean(full), prefix) {
|
|
return "", "", fmt.Errorf("invalid path")
|
|
}
|
|
}
|
|
return clean, full, nil
|
|
}
|
|
|
|
func relativeFilePathStatus(err error) int {
|
|
if err == nil {
|
|
return 200
|
|
}
|
|
switch {
|
|
case err.Error() == "workspace not configured":
|
|
return 500
|
|
default:
|
|
return 400
|
|
}
|
|
}
|
|
|
|
func readRelativeTextFile(root, raw string) (string, string, bool, error) {
|
|
clean, full, err := resolveRelativeFilePath(root, raw)
|
|
if err != nil {
|
|
return "", "", false, err
|
|
}
|
|
b, err := os.ReadFile(full)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return clean, "", false, nil
|
|
}
|
|
return clean, "", false, err
|
|
}
|
|
return clean, string(b), true, nil
|
|
}
|
|
|
|
func writeRelativeTextFile(root, raw string, content string, ensureDir bool) (string, error) {
|
|
clean, full, err := resolveRelativeFilePath(root, raw)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if ensureDir {
|
|
if err := os.MkdirAll(filepath.Dir(full), 0755); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
if err := os.WriteFile(full, []byte(content), 0644); err != nil {
|
|
return "", err
|
|
}
|
|
return clean, nil
|
|
}
|
|
|
|
func (s *Server) memoryFilePath(name string) string {
|
|
workspace := strings.TrimSpace(s.workspacePath)
|
|
if workspace == "" {
|
|
return ""
|
|
}
|
|
return filepath.Join(workspace, "memory", strings.TrimSpace(name))
|
|
}
|
|
|
|
func buildAgentTreeRoot(nodeID string, items []map[string]interface{}) map[string]interface{} {
|
|
rootID := "main"
|
|
for _, item := range items {
|
|
if strings.TrimSpace(stringFromMap(item, "type")) == "router" && strings.TrimSpace(stringFromMap(item, "agent_id")) != "" {
|
|
rootID = strings.TrimSpace(stringFromMap(item, "agent_id"))
|
|
break
|
|
}
|
|
}
|
|
nodesByID := make(map[string]map[string]interface{}, len(items)+1)
|
|
for _, item := range items {
|
|
id := strings.TrimSpace(stringFromMap(item, "agent_id"))
|
|
if id == "" {
|
|
continue
|
|
}
|
|
nodesByID[id] = map[string]interface{}{
|
|
"agent_id": id,
|
|
"display_name": stringFromMap(item, "display_name"),
|
|
"role": stringFromMap(item, "role"),
|
|
"type": stringFromMap(item, "type"),
|
|
"transport": fallbackString(stringFromMap(item, "transport"), "local"),
|
|
"managed_by": stringFromMap(item, "managed_by"),
|
|
"node_id": stringFromMap(item, "node_id"),
|
|
"parent_agent_id": stringFromMap(item, "parent_agent_id"),
|
|
"enabled": boolFromMap(item, "enabled"),
|
|
"children": []map[string]interface{}{},
|
|
}
|
|
}
|
|
root, ok := nodesByID[rootID]
|
|
if !ok {
|
|
root = map[string]interface{}{
|
|
"agent_id": rootID,
|
|
"display_name": "Main Agent",
|
|
"role": "orchestrator",
|
|
"type": "router",
|
|
"transport": "local",
|
|
"managed_by": "derived",
|
|
"enabled": true,
|
|
"children": []map[string]interface{}{},
|
|
}
|
|
nodesByID[rootID] = root
|
|
}
|
|
for _, item := range items {
|
|
id := strings.TrimSpace(stringFromMap(item, "agent_id"))
|
|
if id == "" || id == rootID {
|
|
continue
|
|
}
|
|
parentID := strings.TrimSpace(stringFromMap(item, "parent_agent_id"))
|
|
if parentID == "" {
|
|
parentID = rootID
|
|
}
|
|
parent, ok := nodesByID[parentID]
|
|
if !ok {
|
|
parent = root
|
|
}
|
|
children, _ := parent["children"].([]map[string]interface{})
|
|
parent["children"] = append(children, nodesByID[id])
|
|
}
|
|
return map[string]interface{}{
|
|
"node_id": nodeID,
|
|
"agent_id": root["agent_id"],
|
|
"root": root,
|
|
"child_cnt": len(root["children"].([]map[string]interface{})),
|
|
}
|
|
}
|
|
|
|
func stringFromMap(item map[string]interface{}, key string) string {
|
|
return tools.MapStringArg(item, key)
|
|
}
|
|
|
|
func boolFromMap(item map[string]interface{}, key string) bool {
|
|
if item == nil {
|
|
return false
|
|
}
|
|
v, _ := tools.MapBoolArg(item, key)
|
|
return v
|
|
}
|
|
|
|
func rawStringFromMap(item map[string]interface{}, key string) string {
|
|
return tools.MapRawStringArg(item, key)
|
|
}
|
|
|
|
func stringListFromMap(item map[string]interface{}, key string) []string {
|
|
return tools.MapStringListArg(item, key)
|
|
}
|
|
|
|
func intFromMap(item map[string]interface{}, key string, fallback int) int {
|
|
return tools.MapIntArg(item, key, fallback)
|
|
}
|
|
|
|
func fallbackString(value, fallback string) string {
|
|
value = strings.TrimSpace(value)
|
|
if value != "" {
|
|
return value
|
|
}
|
|
return strings.TrimSpace(fallback)
|
|
}
|
|
|
|
func firstNonEmptyString(values ...string) string {
|
|
for _, v := range values {
|
|
if strings.TrimSpace(v) != "" {
|
|
return strings.TrimSpace(v)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func detectLocalIP() string {
|
|
ifaces, err := net.Interfaces()
|
|
if err == nil {
|
|
for _, iface := range ifaces {
|
|
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
|
|
continue
|
|
}
|
|
addrs, _ := iface.Addrs()
|
|
for _, a := range addrs {
|
|
var ip net.IP
|
|
switch v := a.(type) {
|
|
case *net.IPNet:
|
|
ip = v.IP
|
|
case *net.IPAddr:
|
|
ip = v.IP
|
|
}
|
|
if ip == nil || ip.IsLoopback() {
|
|
continue
|
|
}
|
|
ip = ip.To4()
|
|
if ip == nil {
|
|
continue
|
|
}
|
|
return ip.String()
|
|
}
|
|
}
|
|
}
|
|
conn, err := net.Dial("udp", "8.8.8.8:80")
|
|
if err == nil {
|
|
defer conn.Close()
|
|
if ua, ok := conn.LocalAddr().(*net.UDPAddr); ok && ua.IP != nil {
|
|
if ip := ua.IP.To4(); ip != nil {
|
|
return ip.String()
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func normalizeCronJob(v interface{}) map[string]interface{} {
|
|
if v == nil {
|
|
return map[string]interface{}{}
|
|
}
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return map[string]interface{}{"raw": fmt.Sprintf("%v", v)}
|
|
}
|
|
var m map[string]interface{}
|
|
if err := json.Unmarshal(b, &m); err != nil {
|
|
return map[string]interface{}{"raw": string(b)}
|
|
}
|
|
out := map[string]interface{}{}
|
|
for k, val := range m {
|
|
out[k] = val
|
|
}
|
|
if sch, ok := m["schedule"].(map[string]interface{}); ok {
|
|
kind := stringFromMap(sch, "kind")
|
|
if expr := stringFromMap(sch, "expr"); expr != "" {
|
|
out["expr"] = expr
|
|
} else if strings.EqualFold(strings.TrimSpace(kind), "every") {
|
|
if every := intFromMap(sch, "everyMs", 0); every > 0 {
|
|
out["expr"] = fmt.Sprintf("@every %s", (time.Duration(every) * time.Millisecond).String())
|
|
}
|
|
} else if strings.EqualFold(strings.TrimSpace(kind), "at") {
|
|
if at := intFromMap(sch, "atMs", 0); at > 0 {
|
|
out["expr"] = time.UnixMilli(int64(at)).Format(time.RFC3339)
|
|
}
|
|
}
|
|
}
|
|
if payload, ok := m["payload"].(map[string]interface{}); ok {
|
|
if msg, ok := payload["message"]; ok {
|
|
out["message"] = msg
|
|
}
|
|
if d, ok := payload["deliver"]; ok {
|
|
out["deliver"] = d
|
|
}
|
|
if c, ok := payload["channel"]; ok {
|
|
out["channel"] = c
|
|
}
|
|
if to, ok := payload["to"]; ok {
|
|
out["to"] = to
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func normalizeCronJobs(v interface{}) []map[string]interface{} {
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return []map[string]interface{}{}
|
|
}
|
|
var arr []interface{}
|
|
if err := json.Unmarshal(b, &arr); err != nil {
|
|
return []map[string]interface{}{}
|
|
}
|
|
out := make([]map[string]interface{}, 0, len(arr))
|
|
for _, it := range arr {
|
|
out = append(out, normalizeCronJob(it))
|
|
}
|
|
return out
|
|
}
|