feat(agent): #7 真实流量采集 — clash_api 节点总量 → usage_daily
sing-box 该构建未编入 v2ray_api、clash /connections 也不暴露用户,故无法做 准确 per-user 计费。改取节点累计总量(clash downloadTotal/uploadTotal)做 窗口增量,按当前 dp_uuid 均摊上报(单用户=准确,多用户近似,已注释标注)。 - render.go:节点 sing-box 配置加 experimental.clash_api(loopback+secret)。 - ClashUsageSource:轮询 /connections 总量,delta + 重启回绕保护;clash 的 up/down 是代理视角,对调为用户视角(下载→bytes_down)。 - SingBox.DpUUIDs() 供归属;Agent.UseClashUsage() 在 cmd/agent 接上。 已节点 live 验证:usage_daily 实时入库,下载增量正确落 bytes_down。 局限:多 dp_uuid 时均摊(准确计费需重编 sing-box 带 with_v2ray_api)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
package agentd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
agentv1 "github.com/wangjia/pangolin/server/internal/pb/agentv1"
|
||||
)
|
||||
|
||||
// ClashUsageSource reads the node's cumulative traffic from sing-box's clash_api
|
||||
// (/connections downloadTotal/uploadTotal) and reports per-window deltas.
|
||||
//
|
||||
// Limitation: the clash API exposes only NODE-TOTAL bytes (no per-user field, and
|
||||
// v2ray_api is not compiled into this sing-box build). So node-total delta is
|
||||
// split evenly across currently provisioned dp_uuids — exact for a single active
|
||||
// user, approximate for multiple. See todo #7 for accurate accounting (rebuild
|
||||
// sing-box with with_v2ray_api).
|
||||
type ClashUsageSource struct {
|
||||
sb *SingBox
|
||||
addr string
|
||||
secret string
|
||||
client *http.Client
|
||||
|
||||
mu sync.Mutex
|
||||
inited bool
|
||||
lastUp, lastDown int64
|
||||
}
|
||||
|
||||
func newClashUsageSource(sb *SingBox) *ClashUsageSource {
|
||||
return &ClashUsageSource{
|
||||
sb: sb,
|
||||
addr: clashAPIAddr,
|
||||
secret: clashAPISecret,
|
||||
client: &http.Client{Timeout: 5 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
type clashConnections struct {
|
||||
DownloadTotal int64 `json:"downloadTotal"`
|
||||
UploadTotal int64 `json:"uploadTotal"`
|
||||
}
|
||||
|
||||
// Collect returns per-dp_uuid usage accumulated since the previous call.
|
||||
// First call only sets the baseline (returns nil).
|
||||
func (c *ClashUsageSource) Collect() []*agentv1.UsageEntry {
|
||||
up, down, ok := c.fetchTotals()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if !c.inited {
|
||||
c.lastUp, c.lastDown, c.inited = up, down, true
|
||||
return nil
|
||||
}
|
||||
dUp, dDown := up-c.lastUp, down-c.lastDown
|
||||
c.lastUp, c.lastDown = up, down
|
||||
if dUp < 0 || dDown < 0 {
|
||||
// Counters reset (sing-box restarted) → skip this window.
|
||||
return nil
|
||||
}
|
||||
if dUp == 0 && dDown == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
uuids := c.sb.DpUUIDs()
|
||||
if len(uuids) == 0 {
|
||||
return nil
|
||||
}
|
||||
n := int64(len(uuids))
|
||||
perUp, perDown := dUp/n, dDown/n
|
||||
entries := make([]*agentv1.UsageEntry, 0, len(uuids))
|
||||
for _, u := range uuids {
|
||||
entries = append(entries, &agentv1.UsageEntry{
|
||||
DpUUID: u,
|
||||
BytesUp: perUp,
|
||||
BytesDown: perDown,
|
||||
SessionMinutes: 1, // 本窗口有流量 → 计 1 分钟在线(近似)
|
||||
})
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
func (c *ClashUsageSource) fetchTotals() (up, down int64, ok bool) {
|
||||
req, err := http.NewRequestWithContext(
|
||||
context.Background(), http.MethodGet, "http://"+c.addr+"/connections", nil)
|
||||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
if c.secret != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.secret)
|
||||
}
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return 0, 0, false
|
||||
}
|
||||
var cc clashConnections
|
||||
if err := json.NewDecoder(resp.Body).Decode(&cc); err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
// clash 的 up/down 是「代理视角」:为客户端下载,代理需把数据 upload 给客户端,
|
||||
// 故 clash.uploadTotal ≈ 用户下载、clash.downloadTotal ≈ 用户上传 → 此处对调,
|
||||
// 返回「用户视角」的 (up, down)。
|
||||
return cc.DownloadTotal, cc.UploadTotal, true
|
||||
}
|
||||
Reference in New Issue
Block a user