Install
openclaw skills install @tankeito/server-mateBuild or extend a lightweight server monitoring and AI operations workflow for Linux hosts running Nginx or Apache, with optional centralized remote monitoring through the BT-Panel (Baota) HTTP API. Use when Codex needs to collect psutil metrics, parse access, error, or auth logs (locally OR pulled remotely from BT panels with no probe on the target host), design JSON payloads or APIs, add webhook alerts, generate PDF ops reports with SSL expiry summaries, answer natural-language monitoring questions, or implement guarded auto-ban and auto-heal behaviors.
openclaw skills install @tankeito/server-mateVersion: 1.6.1
Use this skill to design or implement a two-plane monitoring system:
The same Agent serves both layouts: leave panel_id empty on a site to keep the legacy local-tail path; set panel_id on a site to pull its access / error logs remotely with no probe installed on the target host.
OPENAI_API_KEY is injected by the runtime when AI analysis is enabled. Do not ask the user to export it manually. Treat webhook URLs or tokens in config.yaml as secrets and do not commit them../data/GeoIP.conf the same way. It may contain MaxMind AccountID and LicenseKey, so keep it local-only and out of Git../data/GeoIP.conf and geoipupdate. Treat the built-in public mirror fallback only as an operator-reviewed bootstrap path when no local .mmdb file is present.dry_run: true until reviewed.remote_panels and sites[].panel_id are first wired up, do not jump straight into --once. First prove the auth handshake works end-to-end with a minimal, side-effect-free probe:
build_panel_registry or equivalent).BTPanelClient.exec_shell("echo server-mate-probe") per panel and assert the returned stdout equals server-mate-probe.BTPanelAuthError: the most common root cause is NTP clock drift between the Agent host and the panel host — surface the hint emitted by the client and have the operator run chronyc tracking / timedatectl status on both ends before re-trying. Also verify api_key_env actually resolves to a non-empty string and that the panel URL includes the correct port.BTPanelError: confirm the panel URL is reachable from the Agent host, the panel's API access switch is enabled, and that no firewall is blocking the port.psutil, and the standard library for the first implementation../config.yaml plus local SQLite state such as ./metrics.db before adding external services../config.yaml, ./metrics.db, ./logs/, and ./reports/. Do not default to /opt, /var/log, or other system-wide directories.system_metrics + sites[] matrix layout from config.example.yaml instead of new single-site keys.remote_panels block in config.yaml and bind the relevant sites[] entry to it via panel_id. The local Agent will pull access / error logs through the BT HTTP API automatically. Always inject the api_key via api_key_env, never as plaintext, and remind the user that config.yaml containing any panel credential must stay out of version control._safe_remote_path helper (or an equivalent: shlex.quote PLUS a structural rejection of NUL / CR / LF / empty input). Never f-string a raw remote_path, glob, or filename into an ExecShell payload, even when the value "obviously" comes from config.yaml — configuration is an attacker-controlled surface in multi-tenant or shared-checkout deployments. The same rule applies to any new BT API helper added later (file move, log rotate, restart command, etc.): validate, then quote, then splice.dry_run: true and keep it there until the user has observed automation notifications and audit history for several days.dry_run to false, or enable auto_ban.enabled / auto_heal.enabled, unless the operator explicitly approves the command templates, allowlists, cooldowns, and audit destinations.automation_actions and banned_ips, and expose simple lookup queries in user-facing docs.Use external scheduling for production unless the user explicitly wants an always-on daemon-only design.
server_agent.py --once every 10 minutes from cron or a systemd timer.systemd deployments in Clawhub-style packaging:
.service file inside the skill package.server_agent.py --config ./config.yaml --generate-service, then paste it into /etc/systemd/system/server-mate.service.report_generator.py as one-shot scheduled jobs.01:00.01:10.1 at 01:20.report_generator.py run should iterate over every configured site unless the user explicitly passes --site.prefers-color-scheme). Toggling the theme dynamically adapts the Chart.js scales, grid lines, tick fonts, and legends for high-contrast viewing. Persists selection in localStorage.overflow-x: auto) for smooth swipe operations.http.server). Serves a premium, visually stunning dark-themed real-time console at / featuring SVG circle meters, network rate cards, Top CPU processes, active alert cards, blocked firewall IP tables, and dynamic line charts (CPU & QPS history) using Chart.js.llm_shield_cache) in state to save tokens.api_key directly in config.yaml under ai_analysis settings. Enables standalone monitoring cycles and AI diagnosis without external environment dependencies.ai_cooldown_seconds: 3600), reducing token consumption by up to 90%. Prints a 【AI: 缓存】 badge to inform the operator.collect_system_snapshot() to use a two-pass CPU sampling method (base times calculation -> sleep -> delta calculation) with a 1.0s wait. Prevents python startup and log-parsing transients from causing false CPU warnings and enables correct process CPU percentage measurement.cpu_high / iowait_high) with auto_ban. If CPU is exhausted, the Agent automatically checks for non-whitelisted client IPs driving requests above the rate threshold (cpu_spike_rpm_threshold: 60.0) and issues auto-ban commands (e.g. iptables) to mitigate CC/DDoS attacks on slow endpoints.run_alert_diagnostics() which triggers diagnostic shell commands (e.g., ps, uptime, ss, free, dmesg, df, systemctl status) when hardware/service alerts fire, appending commands output directly to webhooks.track_alert_recovery() and deliver_recovery_alerts() to track currently firing alerts in state and notify webhooks when resolved (e.g. ✅ Server-Mate 已恢复), including human-readable duration statistics and peak values.diagnostics section (enabled, timeout_seconds, max_lines_per_command, recovery_notification, recovery_min_duration_seconds).collect_system_snapshot() now collects 28+ metrics across four layers, all via psutil + standard library (zero new dependencies).
cpu_user_pct, cpu_system_pct, cpu_iowait_pct; swap_used_pct, swap_used_bytes, memory_used_bytes, memory_available_bytes; per-cycle delta disk_read_iops, disk_write_iops, disk_read_bytes_delta, disk_write_bytes_delta; net_rx_mbps, net_tx_mbps, net_rx_errs, net_tx_errs, net_rx_drop, net_tx_drop.process_count, process_running, process_sleeping, process_zombie; top_cpu_procs and top_mem_procs (top 5 each).disk_inode_used_pct on root; configurable extra_disk_partitions list with per-mount used_pct, free_bytes, and inode_used_pct.tcp_established, tcp_time_wait, tcp_close_wait from psutil.net_connections; configurable service_probes list checked via systemctl is-active (returns service_failed_units)._io_state: a mutable dict passed through run_cycle → collect_system_snapshot persists previous counter values across calls, enabling accurate per-cycle rate metrics without a resident process.evaluate_alerts(): iowait_high, swap_high, memory_critical, net_errors, high_iops, zombie_process, inode_low, disk_multi_low, tcp_timewait_high, service_down — each with a dedicated label, remediation suggestion, and detail block in render_alert_markdown().config.yaml / normalize_config(): iowait_pct (30%), swap_pct (60%), memory_min_available_mb (200 MB), net_error_count (100), disk_write_iops (5 000/s), inode_used_pct (90%), tcp_timewait_count (5 000).system_metrics config options: collect_processes, collect_tcp_states, service_probes, extra_disk_partitions — all default-safe and backwards compatible.migrate_schema() is called from init_database() and idempotently ALTER TABLE metric_rollups ADD COLUMN for all 11 new columns using PRAGMA table_info — existing databases are upgraded in-place without data loss.metric_rollups columns: avg_cpu_iowait_pct, avg_swap_used_pct, avg_net_rx_mbps, avg_net_tx_mbps, avg_disk_read_iops, avg_disk_write_iops, max_process_count, max_zombie_count, min_inode_free_pct, avg_tcp_established, max_tcp_timewait — all persisted through upsert_metric_rollup().panel_id empty) and remote sites (panel_id bound to a remote_panels entry) coexist in the same sites[] matrix and share every downstream pipeline (rollups, AI diagnosis, webhooks, PDF reports). Target servers require only that BT-Panel API access is enabled — no Python interpreter, no probe, no daemon, no log shipper.request_token = md5(str(request_time) + md5(api_key)), regenerated on every attempt (including retries).application/x-www-form-urlencoded body.requests.Session() per panel for TCP/TLS pooling and BT cookie persistence — eliminates handshake overhead in cron-driven, high-frequency workloads.tail -c +<offset> | head -c <chunk> ExecShell call capped at chunk_bytes (default 5 MB). When a remote error_log explodes by hundreds of MB the Agent's memory footprint stays constant, OOM and HTTP timeout are categorically prevented, and residual bytes roll over to subsequent cron ticks. Backlog is stamped into the persisted cursor (backlog_bytes, status="backlog") and emitted as a WARNING._safe_remote_path = shlex.quote + structural NUL/CR/LF rejection — applied to every remote path before it is spliced into any ExecShell command. Mandatory for every new BT helper added later.{"status": false, "msg": "request_token error"} payloads are recognised (English + Chinese variants) and surfaced with the operator-facing hint "Authentication failed. Please check if the time on the Agent server and the Remote BT panel are synchronized (NTP Time Drift)." — both in the raised BTPanelAuthError and in the WARNING log.scripts/log_reader.py): LocalLogReader (delegates to the existing inode/truncate-aware function) and BTRemoteLogReader (maintains remote_offset + remote_size, treats remote_size < remote_offset as logrotate). State persists into the existing server_agent_state.json so restarts neither double-read nor lose remote bytes.run_cycle: a single flaky panel can never crash the cron tick or starve other sites; the failure is captured and stamped into the cursor's status field for operator visibility.sites[] plus global system_metricsdry_run, whitelist checks, TTL-based unban, cooldown-based auto-heal, and SQLite audit traillogs.auth_log with ssh_brute_force alerting and optional linked auto-ban./data/GeoIP.conf and geoipupdate, with an operator-reviewed public mirror bootstrap fallbackconfig.example.yaml and docs updated for MaxMind GeoLite2 setup in the current workspaceCopyable cron examples:
*/10 * * * * /usr/bin/env bash -lc 'python3 ./scripts/server_agent.py --config ./config.yaml --once >> ./logs/server-mate-agent.log 2>&1'
0 1 * * * /usr/bin/env bash -lc 'python3 ./scripts/report_generator.py --config ./config.yaml pdf --range daily --send >> ./logs/server-mate-report.log 2>&1'
10 1 * * 1 /usr/bin/env bash -lc 'python3 ./scripts/report_generator.py --config ./config.yaml pdf --range weekly --send >> ./logs/server-mate-report.log 2>&1'
20 1 1 * * /usr/bin/env bash -lc 'python3 ./scripts/report_generator.py --config ./config.yaml pdf --range monthly --send >> ./logs/server-mate-report.log 2>&1'
Systemd note:
systemd, prefer Type=oneshot services plus timers for reports.Restart=always only for the long-running --daemon agent mode.