Files
HomeAgent/internal/network/monitor.go
JianFeeeee 6d38a164eb chore(module): Go module 路径随托管地迁移 gitcode → github
## 背景

仓库早已迁移到 GitHub(`github.com/JianFeeeee/HomeAgent`,CI/CD 都在那边跑),
gitcode 保留为**国内只读镜像**(`ci.yml` 的 sync-gitcode-main 快进推送)。
但 go.mod 的 module 路径一直是 `gitcode.com/JianFeeeee/...` —— 既与权威源
不符,`meta.SourceURL`(AGPL §13 的源码提供入口)也指向镜像而非实际运行的代码。

## 关键区分(这是本次改动安全的前提)

两个 module 是**不同**的,影响面差一个数量级:

| module | 谁依赖 | 改它的后果 |
|---|---|---|
| `.../HomeAgent`(主仓) | **只有主仓自己** | 内部重构,零外部影响 |
| `.../homeagent-sdk`(SDK 仓) | 主仓 49 文件 + 所有外部插件 | 需重编插件 |

★ 实测确认:**插件不 import 主仓 module**(`strings qq/plugin.bin | grep HomeAgent` = 0),
只依赖 SDK 仓。而插件与内核的边界是 **RPC 方法名字符串**
(`plugin.init` / `tool.invoke`…,见 proc/protocol.go),**与 Go module 名无关**。
⇒ module 改名**不改变任何 wire 协议**,现网已装的插件照常运行。

SDK 的 GitHub 仓名是 `homeagentsdk`(无连字符),module 路径必须与之精确匹配,
否则 `go get` 会因路径不符失败,故取 `github.com/JianFeeeee/homeagentsdk`。

## 改动

- `go.mod` / `go.sum`:两个 module 路径 + require/replace
- 277 个 Go 文件 + Makefile + 打包脚本的 import 与 ldflags 路径
- SDK 仓 `meta.CoreModule`、`tools/hmapdev` 的 module 声明与工程模板
- 对外文档/链接的仓库地址(README、PLUGIN_DEV、OVERVIEW 等)

## 明确**保留** gitcode 的地方(镜像机制,不能动)

- `GITCODE_TOKEN`、`GC_URL="…@gitcode.com"`(CI 同步 job)
- `gitcode.com/api/v5/...`(release 附件上传)
- git remote `gitcode`(镜像拉取/推送)
- `docs/zh/ci-cd-runbook.md`(整篇讲镜像运维)

## 验证

- `go build ./...` / `go vet ./...` 绿
- `go test ./... -count=1` → 48 包 ok / 0 FAIL
- hmapdev 工具链编译通过(`third_party/homeagent-sdk/tools/hmapdev`)
- 全仓残留检查:module 形态的 `gitcode.com/JianFeeeee` = 0
- 镜像机制抽验:GC_URL / @gitcode / api/v5 / remote 均完好

## 顺带修正

`hmapdev/templates.go` 的注释此前写「gitcode 的模块不在 proxy.golang.org 上」——
module 已迁 GitHub,该论断不再成立。改为如实说明**为何仍默认 replace 到本机 SDK**:
离线可构建、避免版本漂移、内网可用。理由与托管地无关,故 replace 保留。
2026-10-08 10:40:17 +08:00

226 lines
4.6 KiB
Go

package network
import (
"context"
"fmt"
"net"
"net/http"
"sync"
"time"
"github.com/JianFeeeee/HomeAgent/pkg/types"
)
type Monitor struct {
mu sync.RWMutex
client *http.Client
interval time.Duration
endpoints []string
status []EndpointStatus
}
type EndpointStatus struct {
URL string
Reachable bool
Latency time.Duration
LastCheck time.Time
Error string
}
func NewMonitor(interval time.Duration) *Monitor {
return &Monitor{
client: &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 5 * time.Second,
ResponseHeaderTimeout: 5 * time.Second,
DisableKeepAlives: false,
MaxIdleConns: 2,
IdleConnTimeout: 30 * time.Second,
},
},
interval: interval,
status: make([]EndpointStatus, 0),
}
}
func (m *Monitor) Start(ctx context.Context, endpoints []string) {
m.mu.Lock()
m.endpoints = endpoints
m.status = make([]EndpointStatus, len(endpoints))
for i, ep := range endpoints {
m.status[i] = EndpointStatus{URL: ep, Reachable: false}
}
m.mu.Unlock()
interval := m.interval
if interval <= 0 {
interval = 30 * time.Second
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
m.checkAll(ctx)
for {
select {
case <-ticker.C:
m.checkAll(ctx)
case <-ctx.Done():
return
}
}
}
func (m *Monitor) CheckOnce(ctx context.Context, endpoint string) EndpointStatus {
start := time.Now()
req, err := http.NewRequestWithContext(ctx, "HEAD", endpoint, nil)
if err != nil {
return EndpointStatus{URL: endpoint, Reachable: false, Error: err.Error(), LastCheck: time.Now()}
}
resp, err := m.client.Do(req)
latency := time.Since(start)
if err != nil {
return EndpointStatus{URL: endpoint, Reachable: false, Latency: latency, Error: err.Error(), LastCheck: time.Now()}
}
resp.Body.Close()
return EndpointStatus{
URL: endpoint,
Reachable: resp.StatusCode < 500,
Latency: latency,
LastCheck: time.Now(),
}
}
func (m *Monitor) checkAll(ctx context.Context) {
m.mu.RLock()
endpoints := m.endpoints
m.mu.RUnlock()
var wg sync.WaitGroup
results := make([]EndpointStatus, len(endpoints))
for i, ep := range endpoints {
wg.Add(1)
go func(idx int, url string) {
defer wg.Done()
results[idx] = m.CheckOnce(ctx, url)
}(i, ep)
}
wg.Wait()
m.mu.Lock()
m.status = results
m.mu.Unlock()
}
func (m *Monitor) Status() []EndpointStatus {
m.mu.RLock()
defer m.mu.RUnlock()
status := make([]EndpointStatus, len(m.status))
copy(status, m.status)
return status
}
func (m *Monitor) AllReachable() bool {
m.mu.RLock()
defer m.mu.RUnlock()
for _, s := range m.status {
if !s.Reachable {
return false
}
}
return len(m.status) > 0
}
func (m *Monitor) AggregateResult() types.NetworkCheckResult {
m.mu.RLock()
defer m.mu.RUnlock()
result := types.NetworkCheckResult{
LLMAPIReachable: true,
EndpointsConfigured: len(m.status) > 0,
DNSResolving: true,
TCPReachable: true,
}
var totalLatency time.Duration
checked := 0
if !result.EndpointsConfigured {
// 无任何探活端点:不谎报"可达",标为未配置
result.LLMAPIReachable = false
result.Error = "no LLM endpoints configured for health check"
}
for _, s := range m.status {
if !s.Reachable {
result.LLMAPIReachable = false
result.Error = fmt.Sprintf("endpoint %s unreachable: %s", s.URL, s.Error)
}
if s.Latency > 0 {
totalLatency += s.Latency
checked++
}
}
if checked > 0 {
result.Latency = totalLatency / time.Duration(checked)
}
// 延迟阈值检测:平均延迟 > 5s 标记为降级
if result.Latency > 5*time.Second {
result.LatencyDegraded = true
if result.Error == "" {
result.Error = fmt.Sprintf("high latency: %v", result.Latency)
}
}
// DNS 多目标检测
result.DNSResolving = m.checkDNSMulti()
// TCP 拨测:检测基础网络通畅性
result.TCPReachable = m.checkTCPReachability()
return result
}
func (m *Monitor) checkDNSMulti() bool {
targets := []string{"google.com", "baidu.com", "cloudflare.com"}
for _, target := range targets {
_, err := net.LookupHost(target)
if err == nil {
return true
}
}
return false
}
func (m *Monitor) checkTCPReachability() bool {
targets := []struct {
host string
port string
}{
{"8.8.8.8", "53"},
{"1.1.1.1", "53"},
{"208.67.222.222", "53"},
}
for _, t := range targets {
conn, err := net.DialTimeout("tcp", net.JoinHostPort(t.host, t.port), 3*time.Second)
if err == nil {
conn.Close()
return true
}
}
return false
}