safesight-edge/agent/internal/httpapi/extras.go
2026-04-18 12:16:10 +08:00

274 lines
6.7 KiB
Go

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,
"build_id": s.buildID,
"build_type": s.buildType,
"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[:])
}