feat(agent): agent 0.10.0 新增资源/日志/配置回读端点,前端接入 VPS 资源与公网 Nginx 日志(v1.2.4)

agent 0.10.0:新增 GET /api/v1/metrics(资源采集,复用 internal/sysinfo;CPU 两次采样差值 + 1 秒内复用上次结果)、GET /api/v1/logs/nginx(日志尾部,路径白名单来自 nginx -T / 环境变量 / 默认目录,32KB 块倒读、单行 4KB 截断、上限 2000 行)、GET /api/v1/frps/config(frps.toml 回读,256KB 上限)

internal/sysinfo:补 LoadAvg 与 UptimeSeconds(Linux /proc/loadavg 与 /proc/uptime、Windows GetTickCount64、其它平台空值)

应用侧:AgentClient 三个只读方法;request 错误改为带状态码的 statusError,新增 ErrAgentEndpointMissing,把 404/405 统一转成「agent 版本过低需 ≥ 0.10.0」提示;补 service/handler/router

前端:公网 Agent 页「VPS 资源」横排带(占满环境与依赖整行);日志中心「公网 Nginx」页签;frps 同源配置弹窗「比对 VPS 实际文件」

二进制:内嵌 fonu-agent-linux-amd64 / arm64 用新代码重编;VERSION → 1.2.4
This commit is contained in:
2026-09-24 10:15:11 +08:00
parent f75e672a45
commit 2f59f5adc4
19 changed files with 953 additions and 7 deletions
+59 -1
View File
@@ -4,9 +4,12 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
@@ -54,7 +57,7 @@ func (c *AgentClient) request(ctx context.Context, method, path string, payload
defer resp.Body.Close()
data, _ := io.ReadAll(io.LimitReader(resp.Body, 2<<20))
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("agent 返回 HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(data)))
return &statusError{code: resp.StatusCode, body: strings.TrimSpace(string(data))}
}
if result != nil && len(data) > 0 {
if err := json.Unmarshal(data, result); err != nil {
@@ -74,6 +77,61 @@ func (c *AgentClient) Status(ctx context.Context) (map[string]any, error) {
return result, err
}
// agentMinVersionInsights 是 P1 三个只读端点(metrics / logs / frps-config)要求的最低 agent 版本
const agentMinVersionInsights = "0.10.0"
// ErrAgentEndpointMissing 表示 VPS 上的 agent 版本过旧、没有该端点(HTTP 404/405)
var ErrAgentEndpointMissing = errors.New("VPS 上的 agent 版本过低,不支持该接口")
// statusError 保留 HTTP 状态码,便于上层区分「端点不存在」与真实错误
type statusError struct {
code int
body string
}
func (e *statusError) Error() string {
return fmt.Sprintf("agent 返回 HTTP %d: %s", e.code, e.body)
}
// wrapEndpointMissing 把「端点不存在」统一成带升级提示的错误,其它错误原样返回
func wrapEndpointMissing(err error) error {
var statusErr *statusError
if errors.As(err, &statusErr) && (statusErr.code == http.StatusNotFound || statusErr.code == http.StatusMethodNotAllowed) {
return fmt.Errorf("%w(需 ≥ %s),请在服务端详情里升级 Agent", ErrAgentEndpointMissing, agentMinVersionInsights)
}
return err
}
// Metrics 采集 VPS 主机资源(CPU / 内存 / 磁盘 / 负载 / 运行时长)
func (c *AgentClient) Metrics(ctx context.Context) (map[string]any, error) {
var result map[string]any
if err := c.request(ctx, http.MethodGet, "/api/v1/metrics", nil, &result); err != nil {
return nil, wrapEndpointMissing(err)
}
return result, nil
}
// NginxLogs 读取 VPS 上 Nginx 的 access / error 日志尾部
func (c *AgentClient) NginxLogs(ctx context.Context, kind string, lines int) (map[string]any, error) {
query := url.Values{}
query.Set("type", kind)
query.Set("lines", strconv.Itoa(lines))
var result map[string]any
if err := c.request(ctx, http.MethodGet, "/api/v1/logs/nginx?"+query.Encode(), nil, &result); err != nil {
return nil, wrapEndpointMissing(err)
}
return result, nil
}
// FRPSConfig 回读 VPS 上的 frps.toml(用于与下发内容比对)
func (c *AgentClient) FRPSConfig(ctx context.Context) (map[string]any, error) {
var result map[string]any
if err := c.request(ctx, http.MethodGet, "/api/v1/frps/config", nil, &result); err != nil {
return nil, wrapEndpointMissing(err)
}
return result, nil
}
func (c *AgentClient) SyncRoute(ctx context.Context, domain, upstream string, tls bool, certDir string) error {
_, err := c.SyncRouteEx(ctx, domain, upstream, tls, certDir, false, 0, false, nil, "")
return err