diff --git a/README.md b/README.md index cc9b218..5bc4eb3 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,17 @@ HarborForge.Monitor/ |------|------| | `GET /health` | 健康检查,返回 Monitor 版本和标识符 | | `GET /telemetry` | 返回最新的遥测数据快照 | +| `POST /openclaw` | 接收 OpenClaw 插件推送的元数据(版本、代理等) | + +### OpenClaw 元数据 enrichment + +当 OpenClaw 插件通过 `POST /openclaw` 推送元数据后,Monitor 会在后续的心跳上报中自动将这些信息附加到遥测数据中: + +- `openclaw_version` — OpenClaw 运行时版本 +- `plugin_version` — 插件版本 +- `agents` — 代理列表 + +如果插件从未推送过元数据,这些字段会被省略,心跳上报完全不受影响。 **重要**:桥接端口是可选的。如果 `MONITOR_PORT` 为 0 或未设置,桥接服务不会启动,Monitor 的心跳上报功能完全不受影响。即使桥接服务启动失败,心跳上报也会继续正常工作。 diff --git a/cmd/harborforge-monitor/main.go b/cmd/harborforge-monitor/main.go index 422168f..9bb534d 100644 --- a/cmd/harborforge-monitor/main.go +++ b/cmd/harborforge-monitor/main.go @@ -73,6 +73,19 @@ func main() { // Update bridge with latest telemetry if bridgeSrv != nil { bridgeSrv.UpdatePayload(payload) + + // Enrich payload with OpenClaw metadata if available + if meta := bridgeSrv.GetOpenClawMeta(); meta != nil { + if meta.Version != "" { + payload.OpenClawVersion = meta.Version + } + if meta.PluginVersion != "" { + payload.PluginVersion = meta.PluginVersion + } + if len(meta.Agents) > 0 { + payload.Agents = meta.Agents + } + } } if printPayload || dryRun { diff --git a/internal/bridge/bridge.go b/internal/bridge/bridge.go index 67ca7a6..cf86c98 100644 --- a/internal/bridge/bridge.go +++ b/internal/bridge/bridge.go @@ -1,15 +1,19 @@ // Package bridge provides a local HTTP server on MONITOR_PORT for // communication between the HarborForge OpenClaw plugin and Monitor. // -// The plugin queries this endpoint to enrich its telemetry with -// host/hardware data. The bridge is optional: if monitorPort is 0 -// or not set, the bridge is not started and Monitor operates normally. +// The bridge serves two purposes: +// 1. Expose hardware telemetry to the plugin via GET /telemetry +// 2. Receive OpenClaw metadata from the plugin via POST /openclaw +// +// The bridge is optional: if monitorPort is 0 or not set, the bridge +// is not started and Monitor operates normally. package bridge import ( "context" "encoding/json" "fmt" + "io" "log" "net" "net/http" @@ -20,6 +24,14 @@ import ( "git.hangman-lab.top/zhi/HarborForge.Monitor/internal/telemetry" ) +// OpenClawMeta holds metadata received from the OpenClaw plugin. +// This data is optional enrichment for heartbeat uploads. +type OpenClawMeta struct { + Version string `json:"version"` + PluginVersion string `json:"plugin_version"` + Agents []any `json:"agents,omitempty"` +} + // Server is the local bridge HTTP server. type Server struct { cfg config.Config @@ -29,6 +41,9 @@ type Server struct { mu sync.RWMutex lastPayload *telemetry.Payload lastUpdated time.Time + + openclawMeta *OpenClawMeta + openclawUpdated time.Time } // New creates a bridge server. It does not start listening. @@ -57,6 +72,14 @@ type bridgeResponse struct { LastUpdated *time.Time `json:"last_updated,omitempty"` } +// GetOpenClawMeta returns the latest OpenClaw metadata received from +// the plugin, or nil if no metadata has been received. +func (s *Server) GetOpenClawMeta() *OpenClawMeta { + s.mu.RLock() + defer s.mu.RUnlock() + return s.openclawMeta +} + func (s *Server) handler() http.Handler { mux := http.NewServeMux() @@ -91,6 +114,40 @@ func (s *Server) handler() http.Handler { json.NewEncoder(w).Encode(resp) }) + // OpenClaw metadata endpoint — plugin POSTs its metadata here + mux.HandleFunc("/openclaw", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + body, err := io.ReadAll(io.LimitReader(r.Body, 64*1024)) + if err != nil { + http.Error(w, "read error", http.StatusBadRequest) + return + } + defer r.Body.Close() + + var meta OpenClawMeta + if err := json.Unmarshal(body, &meta); err != nil { + http.Error(w, "invalid json", http.StatusBadRequest) + return + } + + s.mu.Lock() + s.openclawMeta = &meta + s.openclawUpdated = time.Now() + s.mu.Unlock() + + s.logger.Printf("received OpenClaw metadata: version=%s plugin=%s agents=%d", + meta.Version, meta.PluginVersion, len(meta.Agents)) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{ + "status": "ok", + }) + }) + return mux } diff --git a/internal/telemetry/telemetry.go b/internal/telemetry/telemetry.go index f9c2a87..6c906b9 100644 --- a/internal/telemetry/telemetry.go +++ b/internal/telemetry/telemetry.go @@ -35,6 +35,10 @@ type Payload struct { SwapPct float64 `json:"swap_pct,omitempty"` LoadAvg []float64 `json:"load_avg,omitempty"` UptimeSeconds uint64 `json:"uptime_seconds,omitempty"` + + // Optional OpenClaw metadata, enriched from plugin bridge. + // These fields are omitted if no plugin data is available. + OpenClawVersion string `json:"openclaw_version,omitempty"` } func BuildPayload(ctx context.Context, cfg config.Config) (Payload, error) {