package httpapi import ( "crypto/sha256" "encoding/hex" "net" "net/http" "os" "path/filepath" "strings" "time" "rk3588sys/agent/internal/assets" "rk3588sys/agent/internal/audit" "rk3588sys/agent/internal/metrics" "rk3588sys/agent/internal/sysinfo" ) func defaultAuditPath(baseDir string) string { if strings.TrimSpace(baseDir) == "" { return filepath.Join("logs", "agent_audit.jsonl") } return filepath.Join(baseDir, "logs", "agent_audit.jsonl") } func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { errorJSON(w, http.StatusMethodNotAllowed, "method not allowed") return } if !s.authorize(r, false) { errorJSON(w, http.StatusUnauthorized, "unauthorized") return } now := time.Now() resp := map[string]any{ "timestamp_ms": now.UnixMilli(), "uptime_sec": sysinfo.UptimeSec(), } if cpu, err := metrics.ReadCPUStat(); err == nil { s.cpuMu.Lock() usage := 0.0 intervalMS := int64(0) if !s.lastCPUTS.IsZero() { usage = metrics.CPUUsage(s.lastCPU, cpu) intervalMS = now.Sub(s.lastCPUTS).Milliseconds() } s.lastCPU = cpu s.lastCPUTS = now s.cpuMu.Unlock() resp["cpu"] = map[string]any{ "usage_pct": usage, "sample_ms": intervalMS, "total_ticks": cpu.Total, "idle_ticks": cpu.Idle, } } else { resp["cpu"] = map[string]any{"error": err.Error()} } if mem, err := metrics.ReadMemInfo(); err == nil { resp["memory"] = mem } else { resp["memory"] = map[string]any{"error": err.Error()} } if la, err := metrics.ReadLoadAvg(); err == nil { resp["load_avg"] = la } else { resp["load_avg"] = map[string]any{"error": err.Error()} } diskPath := s.baseDir if strings.TrimSpace(diskPath) == "" { diskPath = "/" } if du, err := metrics.ReadDiskUsage(diskPath); err == nil { resp["disk"] = du } else { resp["disk"] = map[string]any{"path": diskPath, "error": err.Error()} } if nio, err := metrics.ReadNetDev(); err == nil { resp["net"] = nio } else { resp["net"] = map[string]any{"error": err.Error()} } if dio, err := metrics.ReadDiskStats(); err == nil { resp["disk_io"] = dio } else { resp["disk_io"] = map[string]any{"error": err.Error()} } media := map[string]any{"supported": s.proc != nil && s.proc.Enabled()} if s.proc == nil || !s.proc.Enabled() { resp["media_server"] = media writeJSON(w, http.StatusOK, resp) return } st, err := s.proc.Status() if err != nil { media["error"] = err.Error() } else { media["running"] = st.Running media["pid"] = st.Pid media["config_path"] = st.ConfigPath media["started_at_ms"] = st.StartedAtMS if st.Running && st.StartedAtMS > 0 { media["uptime_sec"] = (now.UnixMilli() - st.StartedAtMS) / 1000 } } resp["media_server"] = media writeJSON(w, http.StatusOK, resp) } func (s *Server) handleVersions(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { errorJSON(w, http.StatusMethodNotAllowed, "method not allowed") return } if !s.authorize(r, false) { errorJSON(w, http.StatusUnauthorized, "unauthorized") return } resp := map[string]any{} agentInfo := map[string]any{"version": s.version, "git_sha": s.gitSHA} if bin, err := assets.ExecutableInfo(); err == nil { agentInfo["binary"] = bin } else { agentInfo["binary_error"] = err.Error() } resp["agent"] = agentInfo mediaInfo := map[string]any{"supported": s.proc != nil && s.proc.Enabled()} if s.proc != nil && s.proc.Enabled() { if ver, err := s.proc.Version(); err == nil { mediaInfo["version"] = ver } else { mediaInfo["version_error"] = err.Error() } if bin, err := s.proc.BinaryInfo(); err == nil { mediaInfo["binary"] = bin } else { mediaInfo["binary_error"] = err.Error() } } resp["media_server"] = mediaInfo if m, err := s.store.List(); err == nil { resp["models"] = map[string]any{"count": len(m.Items), "items": m.Items} } else { resp["models"] = map[string]any{"error": err.Error()} } writeJSON(w, http.StatusOK, resp) } func (s *Server) handleAssets(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { errorJSON(w, http.StatusMethodNotAllowed, "method not allowed") return } if !s.authorize(r, false) { errorJSON(w, http.StatusUnauthorized, "unauthorized") return } resp := map[string]any{} if bin, err := assets.ExecutableInfo(); err == nil { resp["agent_binary"] = bin } else { resp["agent_binary_error"] = err.Error() } if st, err := os.Stat(s.agentCfg.ConfigPath); err == nil && !st.IsDir() { if info, err := assets.Info(s.agentCfg.ConfigPath); err == nil { resp["config"] = info } else { resp["config_error"] = err.Error() } } lastGood := s.agentCfg.ConfigPath + ".last_good.json" if st, err := os.Stat(lastGood); err == nil && !st.IsDir() { if info, err := assets.Info(lastGood); err == nil { resp["config_last_good"] = info } else { resp["config_last_good_error"] = err.Error() } } if s.proc != nil && s.proc.Enabled() { if bin, err := s.proc.BinaryInfo(); err == nil { resp["media_server_binary"] = bin } else { resp["media_server_binary_error"] = err.Error() } } if m, err := s.store.List(); err == nil { resp["models"] = m } else { resp["models_error"] = err.Error() } writeJSON(w, http.StatusOK, resp) } func (s *Server) handleTask(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { errorJSON(w, http.StatusMethodNotAllowed, "method not allowed") return } if !s.authorize(r, false) { errorJSON(w, http.StatusUnauthorized, "unauthorized") return } id := strings.TrimPrefix(r.URL.Path, "/v1/tasks/") if strings.TrimSpace(id) == "" { errorJSON(w, http.StatusNotFound, "not found") return } t, ok := s.tasks.Get(id) if !ok { errorJSON(w, http.StatusNotFound, "not found") return } writeJSON(w, http.StatusOK, t) } func (s *Server) recordAudit(r *http.Request, action string, ok bool, detail string) { if s.audit == nil { return } tok := strings.TrimSpace(r.Header.Get("X-RK-Token")) entry := audit.Entry{ TimeMS: time.Now().UnixMilli(), Action: action, Success: ok, RemoteIP: remoteIP(r), TokenHash: tokenHash(tok), Detail: detail, RequestID: strings.TrimSpace(r.Header.Get("X-Request-Id")), } _ = s.audit.Record(entry) } func remoteIP(r *http.Request) string { if v := strings.TrimSpace(r.Header.Get("X-Forwarded-For")); v != "" { parts := strings.Split(v, ",") if len(parts) > 0 { return strings.TrimSpace(parts[0]) } } host, _, err := net.SplitHostPort(r.RemoteAddr) if err == nil { return host } return r.RemoteAddr } func tokenHash(tok string) string { if tok == "" { return "" } h := sha256.Sum256([]byte(tok)) return hex.EncodeToString(h[:]) }