流结束时的 usage 用了非阻塞发送,通道一满就被丢掉;很多服务商默认还不在流里带 usage。 现在每次真正打到模型的调用都会: 在本地日志(分类 AI,消息 AI 调用完成)写下 scene / prompt / completion / cached 登录后同步上报到后台日汇总
277 lines
7.1 KiB
Go
277 lines
7.1 KiB
Go
package main
|
||
|
||
// network_info.go:采集本机网卡信息(Windows 跑 ipconfig /all 并结构化解析,其它平台用 Go net)。
|
||
// 自动识别主局域网 IPv4,供启动台一键复制。
|
||
|
||
import (
|
||
"net"
|
||
"os"
|
||
"os/exec"
|
||
"runtime"
|
||
"strings"
|
||
)
|
||
|
||
// NetAdapter 单块网卡的摘要信息。
|
||
type NetAdapter struct {
|
||
Name string `json:"name"`
|
||
Description string `json:"description,omitempty"`
|
||
MAC string `json:"mac,omitempty"`
|
||
IPv4 []string `json:"ipv4,omitempty"`
|
||
IPv6 []string `json:"ipv6,omitempty"`
|
||
Gateway []string `json:"gateway,omitempty"`
|
||
DNS []string `json:"dns,omitempty"`
|
||
DHCP bool `json:"dhcp"`
|
||
Primary bool `json:"primary"`
|
||
Virtual bool `json:"virtual"`
|
||
}
|
||
|
||
// NetworkInfo 本机网络总览。
|
||
type NetworkInfo struct {
|
||
Hostname string `json:"hostname"`
|
||
PrimaryIPv4 string `json:"primaryIPv4"`
|
||
Adapters []NetAdapter `json:"adapters"`
|
||
Raw string `json:"raw"`
|
||
}
|
||
|
||
// GetNetworkInfo 一键采集本机网卡信息。
|
||
func (a *App) GetNetworkInfo() (NetworkInfo, error) {
|
||
out := NetworkInfo{Adapters: []NetAdapter{}}
|
||
out.Hostname, _ = os.Hostname()
|
||
|
||
if runtime.GOOS == "windows" {
|
||
raw, err := exec.Command("powershell", "-NoProfile", "-Command",
|
||
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ipconfig /all").CombinedOutput()
|
||
if err != nil || len(raw) == 0 {
|
||
raw, err = exec.Command("ipconfig", "/all").CombinedOutput()
|
||
}
|
||
out.Raw = string(raw)
|
||
if err == nil && len(raw) > 0 {
|
||
out.Adapters = parseIPConfig(out.Raw)
|
||
}
|
||
}
|
||
if len(out.Adapters) == 0 {
|
||
out.Adapters = listAdaptersGo()
|
||
if out.Raw == "" {
|
||
out.Raw = formatAdaptersRaw(out.Hostname, out.Adapters)
|
||
}
|
||
}
|
||
markPrimary(&out)
|
||
return out, nil
|
||
}
|
||
|
||
func isVirtualName(s string) bool {
|
||
low := strings.ToLower(s)
|
||
for _, k := range []string{
|
||
"virtual", "vmware", "vbox", "virtualbox", "hyper-v", "vethernet",
|
||
"wsl", "docker", "vpn", "tap-windows", "zerotier", "tailscale",
|
||
"loopback", "蓝牙", "bluetooth", "isatap", "teredo", "microsoft wi-fi direct",
|
||
} {
|
||
if strings.Contains(low, k) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func isPrivateIPv4(ip string) bool {
|
||
p := net.ParseIP(strings.Split(ip, "/")[0])
|
||
if p == nil {
|
||
return false
|
||
}
|
||
v4 := p.To4()
|
||
if v4 == nil {
|
||
return false
|
||
}
|
||
return v4[0] == 10 ||
|
||
(v4[0] == 172 && v4[1] >= 16 && v4[1] <= 31) ||
|
||
(v4[0] == 192 && v4[1] == 168)
|
||
}
|
||
|
||
func markPrimary(info *NetworkInfo) {
|
||
best := -1
|
||
bestScore := -1
|
||
for i := range info.Adapters {
|
||
info.Adapters[i].Virtual = isVirtualName(info.Adapters[i].Name + " " + info.Adapters[i].Description)
|
||
score := 0
|
||
if info.Adapters[i].Virtual {
|
||
score -= 50
|
||
}
|
||
for _, ip := range info.Adapters[i].IPv4 {
|
||
if strings.HasPrefix(ip, "127.") {
|
||
continue
|
||
}
|
||
score += 10
|
||
if isPrivateIPv4(ip) {
|
||
score += 30
|
||
}
|
||
if len(info.Adapters[i].Gateway) > 0 {
|
||
score += 20
|
||
}
|
||
break
|
||
}
|
||
if score > bestScore {
|
||
bestScore = score
|
||
best = i
|
||
}
|
||
}
|
||
if best >= 0 && bestScore > 0 {
|
||
info.Adapters[best].Primary = true
|
||
for _, ip := range info.Adapters[best].IPv4 {
|
||
if !strings.HasPrefix(ip, "127.") {
|
||
info.PrimaryIPv4 = strings.Split(ip, "/")[0]
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// parseIPConfig 解析 Windows `ipconfig /all` 文本(兼容中英文关键字)。
|
||
func parseIPConfig(raw string) []NetAdapter {
|
||
lines := strings.Split(strings.ReplaceAll(raw, "\r\n", "\n"), "\n")
|
||
var list []NetAdapter
|
||
var cur *NetAdapter
|
||
|
||
flush := func() {
|
||
if cur == nil {
|
||
return
|
||
}
|
||
if cur.Name != "" || len(cur.IPv4) > 0 || len(cur.IPv6) > 0 {
|
||
list = append(list, *cur)
|
||
}
|
||
cur = nil
|
||
}
|
||
|
||
for _, line := range lines {
|
||
trim := strings.TrimSpace(line)
|
||
if trim == "" {
|
||
continue
|
||
}
|
||
// 适配器标题行:不以空格开头,且以冒号结束或含“适配器/adapter”
|
||
if len(line) > 0 && line[0] != ' ' && line[0] != '\t' {
|
||
flush()
|
||
name := strings.TrimRight(trim, ":")
|
||
cur = &NetAdapter{Name: name}
|
||
continue
|
||
}
|
||
if cur == nil {
|
||
continue
|
||
}
|
||
key, val, ok := splitKV(trim)
|
||
if !ok {
|
||
continue
|
||
}
|
||
lk := strings.ToLower(key)
|
||
switch {
|
||
case strings.Contains(lk, "description") || strings.Contains(key, "描述"):
|
||
cur.Description = val
|
||
case strings.Contains(lk, "physical address") || strings.Contains(key, "物理地址"):
|
||
cur.MAC = val
|
||
case strings.Contains(lk, "dhcp enabled") || strings.Contains(key, "dhcp 已启用") || strings.Contains(key, "DHCP 已启用"):
|
||
cur.DHCP = strings.EqualFold(val, "yes") || val == "是"
|
||
case strings.Contains(lk, "ipv4") || strings.Contains(key, "IPv4"):
|
||
cur.IPv4 = append(cur.IPv4, cleanIP(val))
|
||
case strings.Contains(lk, "ipv6") || strings.Contains(key, "IPv6"):
|
||
if !strings.HasPrefix(strings.ToLower(val), "fe80") {
|
||
cur.IPv6 = append(cur.IPv6, cleanIP(val))
|
||
}
|
||
case strings.Contains(lk, "default gateway") || strings.Contains(key, "默认网关"):
|
||
if v := cleanIP(val); v != "" {
|
||
cur.Gateway = append(cur.Gateway, v)
|
||
}
|
||
case strings.Contains(lk, "dns servers") || strings.Contains(key, "dns 服务器") || strings.Contains(key, "DNS 服务器"):
|
||
if v := cleanIP(val); v != "" {
|
||
cur.DNS = append(cur.DNS, v)
|
||
}
|
||
}
|
||
}
|
||
flush()
|
||
return list
|
||
}
|
||
|
||
func splitKV(line string) (key, val string, ok bool) {
|
||
// ipconfig 用 ". . . . :" 或直接 ":" 分隔
|
||
idx := strings.LastIndex(line, ":")
|
||
if idx <= 0 {
|
||
return "", "", false
|
||
}
|
||
key = strings.TrimSpace(strings.TrimRight(line[:idx], ". \t"))
|
||
val = strings.TrimSpace(line[idx+1:])
|
||
if key == "" {
|
||
return "", "", false
|
||
}
|
||
return key, val, true
|
||
}
|
||
|
||
func cleanIP(v string) string {
|
||
v = strings.TrimSpace(v)
|
||
// 去掉 "(首选)" / "(Preferred)" 等后缀
|
||
if i := strings.IndexAny(v, " ("); i > 0 {
|
||
v = strings.TrimSpace(v[:i])
|
||
}
|
||
return v
|
||
}
|
||
|
||
func listAdaptersGo() []NetAdapter {
|
||
ifaces, err := net.Interfaces()
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
var list []NetAdapter
|
||
for _, iface := range ifaces {
|
||
if iface.Flags&net.FlagLoopback != 0 {
|
||
continue
|
||
}
|
||
addrs, _ := iface.Addrs()
|
||
a := NetAdapter{Name: iface.Name, MAC: iface.HardwareAddr.String()}
|
||
for _, addr := range addrs {
|
||
s := addr.String()
|
||
ip, _, _ := net.ParseCIDR(s)
|
||
if ip == nil {
|
||
ip = net.ParseIP(s)
|
||
}
|
||
if ip == nil {
|
||
continue
|
||
}
|
||
if v4 := ip.To4(); v4 != nil {
|
||
a.IPv4 = append(a.IPv4, v4.String())
|
||
} else if !ip.IsLinkLocalUnicast() {
|
||
a.IPv6 = append(a.IPv6, ip.String())
|
||
}
|
||
}
|
||
if len(a.IPv4) == 0 && len(a.IPv6) == 0 {
|
||
continue
|
||
}
|
||
list = append(list, a)
|
||
}
|
||
return list
|
||
}
|
||
|
||
func formatAdaptersRaw(host string, adapters []NetAdapter) string {
|
||
var b strings.Builder
|
||
b.WriteString("Hostname: " + host + "\n\n")
|
||
for _, a := range adapters {
|
||
b.WriteString(a.Name)
|
||
if a.Description != "" {
|
||
b.WriteString(" (" + a.Description + ")")
|
||
}
|
||
b.WriteString(":\n")
|
||
if a.MAC != "" {
|
||
b.WriteString(" MAC: " + a.MAC + "\n")
|
||
}
|
||
for _, ip := range a.IPv4 {
|
||
b.WriteString(" IPv4: " + ip + "\n")
|
||
}
|
||
for _, ip := range a.IPv6 {
|
||
b.WriteString(" IPv6: " + ip + "\n")
|
||
}
|
||
for _, g := range a.Gateway {
|
||
b.WriteString(" Gateway: " + g + "\n")
|
||
}
|
||
for _, d := range a.DNS {
|
||
b.WriteString(" DNS: " + d + "\n")
|
||
}
|
||
b.WriteString("\n")
|
||
}
|
||
return b.String()
|
||
}
|