mirror of
http://bgp.hk.skcks.cn:10088/github.com/oneclickvirt/ecs
synced 2026-04-21 05:10:32 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7659d38c10 | ||
|
|
7ef49a3a9d | ||
|
|
0258be967d | ||
|
|
a1794da4e5 | ||
|
|
85a0b30950 | ||
|
|
21100fd949 |
@@ -56,7 +56,7 @@ Shell version: [https://github.com/spiritLHLS/ecs/blob/main/README_EN.md](https:
|
||||
- Memory test: Self-developed [memorytest](https://github.com/oneclickvirt/memorytest) supporting sysbench, dd, winsat, mbw, stream
|
||||
- Disk test: Self-developed [disktest](https://github.com/oneclickvirt/disktest) supporting dd, fio, winsat
|
||||
- Streaming platform unlock tests concurrent query: Self-developed to [UnlockTests](https://github.com/oneclickvirt/UnlockTests), logic modified from [RegionRestrictionCheck](https://github.com/lmc999/RegionRestrictionCheck) and others
|
||||
- IP quality/security information concurrent query: Self-developed, binary files compiled in [securityCheck](https://github.com/oneclickvirt/securityCheck)
|
||||
- IP quality/security information concurrent query: Self-developed, but open sourced
|
||||
- Email port test: Self-developed [portchecker](https://github.com/oneclickvirt/portchecker)
|
||||
- Three-network return path test: Modified from [zhanghanyun/backtrace](https://github.com/zhanghanyun/backtrace) to [oneclickvirt/backtrace](https://github.com/oneclickvirt/backtrace)
|
||||
- Three-network route test: Modified from [NTrace-core](https://github.com/nxtrace/NTrace-core) to [nt3](https://github.com/oneclickvirt/nt3)
|
||||
@@ -244,8 +244,7 @@ Usage: goecs [options]
|
||||
Set NT3 test type (supported: both, ipv4, ipv6) (default "ipv4")
|
||||
-ping
|
||||
Enable/Disable ping test
|
||||
-security
|
||||
Enable/Disable security test (default true)
|
||||
-security Enable/Disable security test (default false)
|
||||
-speed
|
||||
Enable/Disable speed test (default true)
|
||||
-spnum int
|
||||
|
||||
@@ -56,7 +56,7 @@ Shell 版本:[https://github.com/spiritLHLS/ecs](https://github.com/spiritLHLS
|
||||
- 内存测试:[memorytest](https://github.com/oneclickvirt/memorytest),支持 sysbench、dd、winsat、mbw、stream
|
||||
- 硬盘测试:[disktest](https://github.com/oneclickvirt/disktest),支持 dd、fio、winsat
|
||||
- 流媒体平台解锁测试并发查询:[UnlockTests](https://github.com/oneclickvirt/UnlockTests),逻辑借鉴 [RegionRestrictionCheck](https://github.com/lmc999/RegionRestrictionCheck) 等
|
||||
- IP 质量/安全信息并发查询:二进制文件编译至 [securityCheck](https://github.com/oneclickvirt/securityCheck)
|
||||
- IP 质量/安全信息并发查询:但已开源
|
||||
- 邮件端口测试:[portchecker](https://github.com/oneclickvirt/portchecker)
|
||||
- 上游及回程路由线路检测:借鉴 [zhanghanyun/backtrace](https://github.com/zhanghanyun/backtrace),二次开发至 [oneclickvirt/backtrace](https://github.com/oneclickvirt/backtrace)
|
||||
- 三网路由测试:基于 [NTrace-core](https://github.com/nxtrace/NTrace-core),二次开发至 [nt3](https://github.com/oneclickvirt/nt3)
|
||||
@@ -245,8 +245,7 @@ Usage: goecs [options]
|
||||
Set NT3 test type (supported: both, ipv4, ipv6) (default "ipv4")
|
||||
-ping
|
||||
Enable/Disable ping test
|
||||
-security
|
||||
Enable/Disable security test (default true)
|
||||
-security Enable/Disable security test (default false)
|
||||
-speed
|
||||
Enable/Disable speed test (default true)
|
||||
-spnum int
|
||||
|
||||
2
go.mod
2
go.mod
@@ -18,8 +18,6 @@ require (
|
||||
github.com/oneclickvirt/nt3 v0.0.11-20260112140912
|
||||
github.com/oneclickvirt/pingtest v0.0.9-20251104112920
|
||||
github.com/oneclickvirt/portchecker v0.0.3-20250728015900
|
||||
github.com/oneclickvirt/privatespeedtest v0.0.1-20260112130218
|
||||
github.com/oneclickvirt/security v0.0.8-20260202071316
|
||||
github.com/oneclickvirt/speedtest v0.0.11-20251102151740
|
||||
)
|
||||
|
||||
|
||||
15
go.sum
15
go.sum
@@ -14,22 +14,14 @@ github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5f
|
||||
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
||||
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
|
||||
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
|
||||
@@ -109,8 +101,6 @@ github.com/libp2p/go-nat v0.2.0 h1:Tyz+bUFAYqGyJ/ppPPymMGbIgNRH+WqC5QrT5fKrrGk=
|
||||
github.com/libp2p/go-nat v0.2.0/go.mod h1:3MJr+GRpRkyT65EpVPBstXLvOlAPzUVlG6Pwg9ohLJk=
|
||||
github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU=
|
||||
github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
@@ -121,7 +111,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
@@ -172,10 +161,6 @@ github.com/oneclickvirt/pingtest v0.0.9-20251104112920 h1:j3Fjhy0YHT/VF7iuAVVELa
|
||||
github.com/oneclickvirt/pingtest v0.0.9-20251104112920/go.mod h1:gxwsxxwitNQiGq2OI0ZogYoOLwc8DtuOdSRe6/EvRqs=
|
||||
github.com/oneclickvirt/portchecker v0.0.3-20250728015900 h1:AomzdppSOFB70AJESQhlp0IPbsHTTJGimAWDk2TzCWM=
|
||||
github.com/oneclickvirt/portchecker v0.0.3-20250728015900/go.mod h1:9sjMDPCd4Z40wkYB0S9gQPGH8YPtnNE1ZJthVIuHUzA=
|
||||
github.com/oneclickvirt/privatespeedtest v0.0.1-20260112130218 h1:h2k2fHtrsIIP/x/apEWkQGlTKuIumz8GrUR/df41YhE=
|
||||
github.com/oneclickvirt/privatespeedtest v0.0.1-20260112130218/go.mod h1:IXOlKKX4DUNqxOaW/K9bcdrBiWxo0jGSLXeBeo7NrTo=
|
||||
github.com/oneclickvirt/security v0.0.8-20260202071316 h1:ULZWXC99IzrdFEG05D2/MQklKAhztQNc6UYCE3fEQeU=
|
||||
github.com/oneclickvirt/security v0.0.8-20260202071316/go.mod h1:aPMIwqsz7wiUH1cqvtRr9+QcQRkKzlUWecDM6SGVddc=
|
||||
github.com/oneclickvirt/speedtest v0.0.11-20251102151740 h1:1NUrNt5ay6/xVNC5x62UrQjPqK8jgbKtyjBml/3boZg=
|
||||
github.com/oneclickvirt/speedtest v0.0.11-20251102151740/go.mod h1:fy0II2Wo7kDWVBKTwcHdodZwyfmJo0g8N9V02EwQDZE=
|
||||
github.com/oneclickvirt/stream v0.0.2-20250924154001 h1:GuJWdiPkoK84+y/+oHKr2Ghl3c/MzS9Z5m1nM+lMmy4=
|
||||
|
||||
2
goecs.go
2
goecs.go
@@ -27,7 +27,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ecsVersion = "v0.1.120" // 融合怪版本号
|
||||
ecsVersion = "v0.1.122" // 融合怪版本号
|
||||
configs = params.NewConfig(ecsVersion) // 全局配置实例
|
||||
userSetFlags = make(map[string]bool) // 用于跟踪哪些参数是用户显式设置的
|
||||
)
|
||||
|
||||
6
goecs.sh
6
goecs.sh
@@ -152,7 +152,7 @@ goecs_check() {
|
||||
os=$(uname -s 2>/dev/null || echo "Unknown")
|
||||
arch=$(uname -m 2>/dev/null || echo "Unknown")
|
||||
check_china
|
||||
ECS_VERSION="0.1.119"
|
||||
ECS_VERSION="0.1.122"
|
||||
for api in \
|
||||
"https://api.github.com/repos/oneclickvirt/ecs/releases/latest" \
|
||||
"https://githubapi.spiritlhl.workers.dev/repos/oneclickvirt/ecs/releases/latest" \
|
||||
@@ -164,8 +164,8 @@ goecs_check() {
|
||||
sleep 1
|
||||
done
|
||||
if [ -z "$ECS_VERSION" ]; then
|
||||
_yellow "Unable to get version info, using default version 0.1.119"
|
||||
ECS_VERSION="0.1.119"
|
||||
_yellow "Unable to get version info, using default version 0.1.122"
|
||||
ECS_VERSION="0.1.122"
|
||||
fi
|
||||
version_output=""
|
||||
for cmd_path in "goecs" "./goecs" "/usr/bin/goecs" "/usr/local/bin/goecs"; do
|
||||
|
||||
@@ -28,6 +28,12 @@ var (
|
||||
gbMultiRe = regexp.MustCompile(`(?im)^\s*Multi-Core\s*Score\s*[::]\s*([0-9][0-9,]*(?:\.[0-9]+)?)\s*$`)
|
||||
|
||||
alphaNumRe = regexp.MustCompile(`[a-z0-9]+`)
|
||||
|
||||
// New patterns for condensed summary
|
||||
ansiEscapeRe = regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
||||
streamFuncRe = regexp.MustCompile(`(?im)^\s*(?:Copy|Scale|Add|Triad)\s*:\s*(\d+(?:\.\d+)?)`)
|
||||
anyGbpsRe = regexp.MustCompile(`(?i)(\d+(?:\.\d+)?)\s*GB/s`)
|
||||
anyMbpsRe = regexp.MustCompile(`(?i)(\d+(?:\.\d+)?)\s*MB/s`)
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -64,9 +70,9 @@ type cpuStatsPayload struct {
|
||||
}
|
||||
|
||||
var (
|
||||
cpuStatsMu sync.Mutex
|
||||
cachedCPUStats *cpuStatsPayload
|
||||
cpuStatsExpireAt time.Time
|
||||
cpuStatsMu sync.Mutex
|
||||
cachedCPUStats *cpuStatsPayload
|
||||
cpuStatsExpireAt time.Time
|
||||
)
|
||||
|
||||
func parseFloatsByRegex(content string, re *regexp.Regexp) []float64 {
|
||||
@@ -604,49 +610,468 @@ func scopesText(scopes []string, lang string) string {
|
||||
return strings.Join(out, ", ")
|
||||
}
|
||||
|
||||
// GenerateSummary creates a concise post-test summary from final output.
|
||||
// stripAnsiCodes removes ANSI escape codes from a string.
|
||||
func stripAnsiCodes(s string) string {
|
||||
return ansiEscapeRe.ReplaceAllString(s, "")
|
||||
}
|
||||
|
||||
// extractAllSectionContent returns the concatenated text of every section whose
|
||||
// header contains markerZh or markerEn (case-sensitive). Section headers are
|
||||
// lines that start with three or more dashes (produced by PrintCenteredTitle).
|
||||
func extractAllSectionContent(output, markerZh, markerEn string) string {
|
||||
lines := strings.Split(output, "\n")
|
||||
var sb strings.Builder
|
||||
inSection := false
|
||||
for _, line := range lines {
|
||||
stripped := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(stripped, "---") {
|
||||
if strings.Contains(stripped, markerZh) || (markerEn != "" && strings.Contains(stripped, markerEn)) {
|
||||
inSection = true
|
||||
continue
|
||||
}
|
||||
// Any other section header ends the current one
|
||||
inSection = false
|
||||
continue
|
||||
}
|
||||
if inSection {
|
||||
sb.WriteString(line)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// extractMaxMemoryBandwidth returns the highest bandwidth value (in MB/s) found
|
||||
// inside the memory-test section(s) of the captured output.
|
||||
func extractMaxMemoryBandwidth(output string) float64 {
|
||||
content := extractAllSectionContent(output, "内存测试", "Memory-Test")
|
||||
if content == "" {
|
||||
return 0
|
||||
}
|
||||
maxMbps := 0.0
|
||||
// STREAM format: "Copy: 12345.6 ..." – the first number is Best Rate MB/s
|
||||
for _, m := range streamFuncRe.FindAllStringSubmatch(content, -1) {
|
||||
if v, err := strconv.ParseFloat(m[1], 64); err == nil && v > maxMbps {
|
||||
maxMbps = v
|
||||
}
|
||||
}
|
||||
// Values reported as GB/s (some dd / mbw outputs)
|
||||
for _, m := range anyGbpsRe.FindAllStringSubmatch(content, -1) {
|
||||
if v, err := strconv.ParseFloat(m[1], 64); err == nil {
|
||||
if mbps := v * 1024; mbps > maxMbps {
|
||||
maxMbps = mbps
|
||||
}
|
||||
}
|
||||
}
|
||||
// Values reported as MB/s (mbw, sysbench, dd, winsat …)
|
||||
for _, m := range anyMbpsRe.FindAllStringSubmatch(content, -1) {
|
||||
if v, err := strconv.ParseFloat(m[1], 64); err == nil && v > maxMbps {
|
||||
maxMbps = v
|
||||
}
|
||||
}
|
||||
return maxMbps
|
||||
}
|
||||
|
||||
// inferMemoryDDRAndChannels converts a memory bandwidth (MB/s) to a human-readable
|
||||
// DDR type + channel string using the thresholds from README_NEW_USER.
|
||||
//
|
||||
// DDR3 single: 10240–17408 MB/s
|
||||
// DDR4 single: 17408–34816 MB/s
|
||||
// DDR4 dual: 34816–51200 MB/s
|
||||
// DDR5 single: 51200–77824 MB/s
|
||||
// DDR5 dual: ≥77824 MB/s
|
||||
func inferMemoryDDRAndChannels(mbps float64, lang string) string {
|
||||
type tier struct {
|
||||
minMbps float64
|
||||
ddr, chZh, chEn string
|
||||
}
|
||||
tiers := []tier{
|
||||
{77824, "DDR5", "双通道", "Dual-Channel"},
|
||||
{51200, "DDR5", "单通道", "Single-Channel"},
|
||||
{34816, "DDR4", "双通道", "Dual-Channel"},
|
||||
{17408, "DDR4", "单通道", "Single-Channel"},
|
||||
{0, "DDR3", "单通道", "Single-Channel"},
|
||||
}
|
||||
for _, t := range tiers {
|
||||
if mbps >= t.minMbps {
|
||||
if lang == "zh" {
|
||||
return t.ddr + " " + t.chZh
|
||||
}
|
||||
return t.ddr + " " + t.chEn
|
||||
}
|
||||
}
|
||||
if lang == "zh" {
|
||||
return "DDR3 单通道"
|
||||
}
|
||||
return "DDR3 Single-Channel"
|
||||
}
|
||||
|
||||
// extractDiskTypeAndCount scans all disk-test section(s) for fio 4K rows and
|
||||
// returns the 4K read speed (MB/s) and the number of unique test paths found.
|
||||
// Falls back to any MB/s / GB/s value in the disk section when no 4K rows exist.
|
||||
func extractDiskTypeAndCount(output string) (readMbps float64, pathCount int) {
|
||||
content := extractAllSectionContent(output, "硬盘测试", "Disk-Test")
|
||||
if content == "" {
|
||||
return 0, 0
|
||||
}
|
||||
pathSet := make(map[string]struct{})
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
fields := strings.Fields(strings.TrimSpace(line))
|
||||
// fio output row: <path> <blocksize> <readSpeed> <readUnit(iops)> ...
|
||||
if len(fields) < 4 {
|
||||
continue
|
||||
}
|
||||
if !strings.EqualFold(fields[1], "4k") {
|
||||
continue
|
||||
}
|
||||
pathSet[fields[0]] = struct{}{}
|
||||
val, err := strconv.ParseFloat(fields[2], 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
unit := strings.ToUpper(fields[3])
|
||||
var mbps float64
|
||||
if strings.HasPrefix(unit, "GB") {
|
||||
mbps = val * 1024
|
||||
} else {
|
||||
mbps = val
|
||||
}
|
||||
if mbps > readMbps {
|
||||
readMbps = mbps
|
||||
}
|
||||
}
|
||||
pathCount = len(pathSet)
|
||||
// Fallback: no 4K rows found – use any speed value present (dd / winsat)
|
||||
if readMbps == 0 {
|
||||
for _, m := range anyGbpsRe.FindAllStringSubmatch(content, -1) {
|
||||
if v, err := strconv.ParseFloat(m[1], 64); err == nil {
|
||||
if mbps := v * 1024; mbps > readMbps {
|
||||
readMbps = mbps
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, m := range anyMbpsRe.FindAllStringSubmatch(content, -1) {
|
||||
if v, err := strconv.ParseFloat(m[1], 64); err == nil && v > readMbps {
|
||||
readMbps = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if pathCount == 0 && readMbps > 0 {
|
||||
pathCount = 1
|
||||
}
|
||||
return readMbps, pathCount
|
||||
}
|
||||
|
||||
// inferDiskType classifies a disk by its 4K (or sequential fallback) read speed.
|
||||
//
|
||||
// NVMe SSD : ≥200 MB/s
|
||||
// SATA SSD : 50–200 MB/s
|
||||
// HDD : 10–50 MB/s
|
||||
func inferDiskType(readMbps float64, lang string) string {
|
||||
switch {
|
||||
case readMbps >= 200:
|
||||
return "NVMe SSD"
|
||||
case readMbps >= 50:
|
||||
if lang == "zh" {
|
||||
return "SATA SSD"
|
||||
}
|
||||
return "SATA SSD"
|
||||
case readMbps >= 10:
|
||||
return "HDD"
|
||||
default:
|
||||
if lang == "zh" {
|
||||
return "低性能磁盘"
|
||||
}
|
||||
return "Low-Perf Disk"
|
||||
}
|
||||
}
|
||||
|
||||
// extractISPRanking parses the backtrace section and returns a ranking string
|
||||
// like "电信 > 联通 > 移动" based on the best route quality detected per ISP.
|
||||
// Quality tiers: [精品线路]=3, [优质线路]=2, [普通线路]=1.
|
||||
func extractISPRanking(output, lang string) string {
|
||||
content := extractAllSectionContent(output, "上游及回程线路检测", "Upstream")
|
||||
if content == "" {
|
||||
return ""
|
||||
}
|
||||
scores := map[string]int{"电信": 0, "联通": 0, "移动": 0}
|
||||
for _, raw := range strings.Split(content, "\n") {
|
||||
line := stripAnsiCodes(raw)
|
||||
var q int
|
||||
switch {
|
||||
case strings.Contains(line, "[精品线路]"):
|
||||
q = 3
|
||||
case strings.Contains(line, "[优质线路]"):
|
||||
q = 2
|
||||
case strings.Contains(line, "[普通线路]"):
|
||||
q = 1
|
||||
default:
|
||||
continue
|
||||
}
|
||||
for isp := range scores {
|
||||
if strings.Contains(line, isp) && q > scores[isp] {
|
||||
scores[isp] = q
|
||||
}
|
||||
}
|
||||
}
|
||||
if scores["电信"] == 0 && scores["联通"] == 0 && scores["移动"] == 0 {
|
||||
return ""
|
||||
}
|
||||
isps := []string{"电信", "联通", "移动"}
|
||||
order := map[string]int{"电信": 0, "联通": 1, "移动": 2}
|
||||
sort.Slice(isps, func(i, j int) bool {
|
||||
si, sj := scores[isps[i]], scores[isps[j]]
|
||||
if si != sj {
|
||||
return si > sj
|
||||
}
|
||||
return order[isps[i]] < order[isps[j]]
|
||||
})
|
||||
return strings.Join(isps, " > ")
|
||||
}
|
||||
|
||||
// extractCPURankCondensed returns "CPU排名 #N 为满血性能的XX.XX%" (zh) or the
|
||||
// English equivalent, using the same CPU stats lookup as summarizeCPUWithRanking.
|
||||
func extractCPURankCondensed(finalOutput, lang string) string {
|
||||
model := extractCPUModel(finalOutput)
|
||||
single, singleOK, multi, multiOK := extractCPUScores(finalOutput)
|
||||
if !singleOK && !multiOK {
|
||||
return ""
|
||||
}
|
||||
stats := loadCPUStats()
|
||||
entry := matchCPUStatsEntry(model, stats)
|
||||
if entry == nil || entry.Rank <= 0 {
|
||||
return ""
|
||||
}
|
||||
var score, maxScore float64
|
||||
if singleOK && entry.MaxSingle > 0 {
|
||||
score, maxScore = single, entry.MaxSingle
|
||||
} else if multiOK && entry.MaxMulti > 0 {
|
||||
score, maxScore = multi, entry.MaxMulti
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
pct := score / maxScore * 100
|
||||
if lang == "zh" {
|
||||
return fmt.Sprintf("CPU排名 #%d 为满血性能的%.2f%%", entry.Rank, pct)
|
||||
}
|
||||
return fmt.Sprintf("CPU rank #%d is %.2f%% of full performance", entry.Rank, pct)
|
||||
}
|
||||
|
||||
// extractBandwidthCondensed returns the peak bandwidth as a human-readable
|
||||
// "网络峰值带宽大于 X.XXGbps" / "> X.XXGbps" string.
|
||||
func extractBandwidthCondensed(vals []float64, lang string) string {
|
||||
if len(vals) == 0 {
|
||||
return ""
|
||||
}
|
||||
sort.Float64s(vals)
|
||||
maxV := vals[len(vals)-1]
|
||||
if lang == "zh" {
|
||||
if maxV >= 1000 {
|
||||
return fmt.Sprintf("网络峰值带宽大于 %.2fGbps", maxV/1000)
|
||||
}
|
||||
return fmt.Sprintf("网络峰值带宽大于 %.2fMbps", maxV)
|
||||
}
|
||||
if maxV >= 1000 {
|
||||
return fmt.Sprintf("Peak bandwidth > %.2fGbps", maxV/1000)
|
||||
}
|
||||
return fmt.Sprintf("Peak bandwidth > %.2fMbps", maxV)
|
||||
}
|
||||
|
||||
// sectionExists reports whether the output contains a section header matching
|
||||
// the given Chinese or English marker.
|
||||
func sectionExists(output, markerZh, markerEn string) bool {
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
stripped := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(stripped, "---") {
|
||||
if strings.Contains(stripped, markerZh) || (markerEn != "" && strings.Contains(stripped, markerEn)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// summaryRow holds a localised label/value pair for the markdown summary table.
|
||||
type summaryRow struct{ label, value string }
|
||||
|
||||
// GenerateSummary creates a Markdown table post-test summary from final output.
|
||||
func GenerateSummary(config *params.Config, finalOutput string) string {
|
||||
lang := config.Language
|
||||
scopes := testedScopes(config)
|
||||
bandwidthVals := parseFloatsByRegex(finalOutput, mbpsRe)
|
||||
latencyVals := parseFloatsByRegex(finalOutput, msRe)
|
||||
cpuLines := summarizeCPUWithRanking(finalOutput, lang)
|
||||
rows := make([]summaryRow, 0, 5)
|
||||
|
||||
if lang == "zh" {
|
||||
lines := []string{
|
||||
"测试结果总结:",
|
||||
fmt.Sprintf("- 本次覆盖: %s", scopesText(scopes, lang)),
|
||||
// helper: localised N/A string
|
||||
na := func() string {
|
||||
if lang == "zh" {
|
||||
return "无有效数据"
|
||||
}
|
||||
for _, line := range cpuLines {
|
||||
lines = append(lines, "- "+line)
|
||||
}
|
||||
if config.SpeedTestStatus {
|
||||
lines = append(lines, "- "+summarizeBandwidth(bandwidthVals, lang))
|
||||
lines = append(lines, "- 参考 README_NEW_USER: 一般境外机器带宽 100Mbps 起步,是否够用应以业务下载/传输需求为准。")
|
||||
}
|
||||
if config.PingTestStatus || config.TgdcTestStatus || config.WebTestStatus || config.BacktraceStatus || config.Nt3Status {
|
||||
lines = append(lines, "- "+summarizeLatency(latencyVals, lang))
|
||||
lines = append(lines, "- 参考 README_NEW_USER: 延迟 >= 9999ms 可视为目标不可用。")
|
||||
}
|
||||
lines = append(lines, "- 建议: 结合业务场景(高并发计算/存储/跨境网络)重点参考对应分项。")
|
||||
return strings.Join(lines, "\n")
|
||||
return "N/A"
|
||||
}
|
||||
|
||||
lines := []string{
|
||||
"Test Summary:",
|
||||
fmt.Sprintf("- Scope covered: %s", scopesText(scopes, lang)),
|
||||
// 1. CPU rank and full-blood percentage
|
||||
if config.CpuTestStatus {
|
||||
model := extractCPUModel(finalOutput)
|
||||
single, singleOK, multi, multiOK := extractCPUScores(finalOutput)
|
||||
if singleOK || multiOK {
|
||||
stats := loadCPUStats()
|
||||
entry := matchCPUStatsEntry(model, stats)
|
||||
if entry != nil && entry.Rank > 0 {
|
||||
var score, maxScore float64
|
||||
if singleOK && entry.MaxSingle > 0 {
|
||||
score, maxScore = single, entry.MaxSingle
|
||||
} else if multiOK && entry.MaxMulti > 0 {
|
||||
score, maxScore = multi, entry.MaxMulti
|
||||
}
|
||||
if maxScore > 0 {
|
||||
pct := score / maxScore * 100
|
||||
if lang == "zh" {
|
||||
rows = append(rows, summaryRow{"CPU排名", fmt.Sprintf("#%d 满血性能 %.2f%%", entry.Rank, pct)})
|
||||
} else {
|
||||
rows = append(rows, summaryRow{"CPU Rank", fmt.Sprintf("#%d (%.2f%% of max)", entry.Rank, pct)})
|
||||
}
|
||||
} else if lang == "zh" {
|
||||
rows = append(rows, summaryRow{"CPU排名", na()})
|
||||
} else {
|
||||
rows = append(rows, summaryRow{"CPU Rank", na()})
|
||||
}
|
||||
} else if sectionExists(finalOutput, "CPU测试", "CPU-Test") {
|
||||
if lang == "zh" {
|
||||
rows = append(rows, summaryRow{"CPU排名", na()})
|
||||
} else {
|
||||
rows = append(rows, summaryRow{"CPU Rank", na()})
|
||||
}
|
||||
}
|
||||
} else if sectionExists(finalOutput, "CPU测试", "CPU-Test") {
|
||||
if lang == "zh" {
|
||||
rows = append(rows, summaryRow{"CPU排名", na()})
|
||||
} else {
|
||||
rows = append(rows, summaryRow{"CPU Rank", na()})
|
||||
}
|
||||
}
|
||||
// If section not present at all, the test simply wasn't run – omit silently.
|
||||
}
|
||||
for _, line := range cpuLines {
|
||||
lines = append(lines, "- "+line)
|
||||
|
||||
// 2. Memory DDR type and channels, with average-level check
|
||||
if config.MemoryTestStatus {
|
||||
bw := extractMaxMemoryBandwidth(finalOutput)
|
||||
if bw > 0 {
|
||||
mem := inferMemoryDDRAndChannels(bw, lang)
|
||||
// README_NEW_USER threshold: < 10240 MB/s (≈10 GB/s) indicates overselling risk
|
||||
const memAvgThreshMbps = 10240.0
|
||||
if lang == "zh" {
|
||||
if bw >= memAvgThreshMbps {
|
||||
rows = append(rows, summaryRow{"内存", mem + " (达标)"})
|
||||
} else {
|
||||
rows = append(rows, summaryRow{"内存", mem + " (未达标)"})
|
||||
}
|
||||
} else {
|
||||
if bw >= memAvgThreshMbps {
|
||||
rows = append(rows, summaryRow{"Memory", mem + " (pass)"})
|
||||
} else {
|
||||
rows = append(rows, summaryRow{"Memory", mem + " (below avg)"})
|
||||
}
|
||||
}
|
||||
} else if sectionExists(finalOutput, "内存测试", "Memory-Test") {
|
||||
if lang == "zh" {
|
||||
rows = append(rows, summaryRow{"内存", na()})
|
||||
} else {
|
||||
rows = append(rows, summaryRow{"Memory", na()})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Disk type and path count, with average-level check
|
||||
if config.DiskTestStatus {
|
||||
readMbps, pathCount := extractDiskTypeAndCount(finalOutput)
|
||||
if readMbps > 0 || pathCount > 0 {
|
||||
if pathCount <= 0 {
|
||||
pathCount = 1
|
||||
}
|
||||
dtype := inferDiskType(readMbps, lang)
|
||||
// README_NEW_USER: < 10 MB/s = poor performance / severe overselling
|
||||
diskOK := readMbps >= 10
|
||||
var qual string
|
||||
if lang == "zh" {
|
||||
if diskOK {
|
||||
qual = " (达标)"
|
||||
} else {
|
||||
qual = " (未达标)"
|
||||
}
|
||||
rows = append(rows, summaryRow{"硬盘IO", fmt.Sprintf("%s %d路%s", dtype, pathCount, qual)})
|
||||
} else {
|
||||
if diskOK {
|
||||
qual = " (pass)"
|
||||
} else {
|
||||
qual = " (below avg)"
|
||||
}
|
||||
rows = append(rows, summaryRow{"Disk IO", fmt.Sprintf("%s %d path(s)%s", dtype, pathCount, qual)})
|
||||
}
|
||||
} else if sectionExists(finalOutput, "硬盘测试", "Disk-Test") {
|
||||
if lang == "zh" {
|
||||
rows = append(rows, summaryRow{"硬盘IO", na()})
|
||||
} else {
|
||||
rows = append(rows, summaryRow{"Disk IO", na()})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Network peak bandwidth
|
||||
if config.SpeedTestStatus {
|
||||
lines = append(lines, "- "+summarizeBandwidth(bandwidthVals, lang))
|
||||
lines = append(lines, "- README_NEW_USER note: offshore servers commonly start around 100Mbps; evaluate against your actual workload needs.")
|
||||
bwVals := parseFloatsByRegex(finalOutput, mbpsRe)
|
||||
var bwVal string
|
||||
if len(bwVals) > 0 {
|
||||
sort.Float64s(bwVals)
|
||||
maxV := bwVals[len(bwVals)-1]
|
||||
if maxV >= 1000 {
|
||||
bwVal = fmt.Sprintf("> %.2fGbps", maxV/1000)
|
||||
} else {
|
||||
bwVal = fmt.Sprintf("> %.2fMbps", maxV)
|
||||
}
|
||||
}
|
||||
if bwVal != "" {
|
||||
if lang == "zh" {
|
||||
rows = append(rows, summaryRow{"网络峰值带宽", bwVal})
|
||||
} else {
|
||||
rows = append(rows, summaryRow{"Peak Bandwidth", bwVal})
|
||||
}
|
||||
} else if sectionExists(finalOutput, "测速", "Speed-Test") {
|
||||
if lang == "zh" {
|
||||
rows = append(rows, summaryRow{"网络峰值带宽", na()})
|
||||
} else {
|
||||
rows = append(rows, summaryRow{"Peak Bandwidth", na()})
|
||||
}
|
||||
}
|
||||
}
|
||||
if config.PingTestStatus || config.TgdcTestStatus || config.WebTestStatus || config.BacktraceStatus || config.Nt3Status {
|
||||
lines = append(lines, "- "+summarizeLatency(latencyVals, lang))
|
||||
lines = append(lines, "- README_NEW_USER note: latency >= 9999ms should be treated as unavailable target.")
|
||||
|
||||
// 5. Domestic ISP ranking — only meaningful in Chinese mode (backtrace targets CN ISPs)
|
||||
if lang == "zh" && config.BacktraceStatus {
|
||||
if ranking := extractISPRanking(finalOutput, lang); ranking != "" {
|
||||
rows = append(rows, summaryRow{"国内三大运营商推荐排名", ranking})
|
||||
} else if sectionExists(finalOutput, "上游及回程线路检测", "") {
|
||||
rows = append(rows, summaryRow{"国内三大运营商推荐排名", na()})
|
||||
}
|
||||
}
|
||||
lines = append(lines, "- Suggestion: prioritize the metrics that match your workload (compute, storage, or cross-region networking).")
|
||||
return strings.Join(lines, "\n")
|
||||
|
||||
if len(rows) == 0 {
|
||||
if lang == "zh" {
|
||||
return "无足够数据生成摘要。"
|
||||
}
|
||||
return "Insufficient data for summary."
|
||||
}
|
||||
|
||||
// Render as Markdown table
|
||||
var sb strings.Builder
|
||||
if lang == "zh" {
|
||||
sb.WriteString("| 测试项目 | 结果 |\n")
|
||||
} else {
|
||||
sb.WriteString("| Test Item | Result |\n")
|
||||
}
|
||||
sb.WriteString("|:---------|:-------|\n")
|
||||
for _, r := range rows {
|
||||
sb.WriteString("| ")
|
||||
sb.WriteString(r.label)
|
||||
sb.WriteString(" | ")
|
||||
sb.WriteString(r.value)
|
||||
sb.WriteString(" |\n")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ func NewConfig(version string) *Config {
|
||||
MemoryTestStatus: true,
|
||||
DiskTestStatus: true,
|
||||
UtTestStatus: true,
|
||||
SecurityTestStatus: true,
|
||||
SecurityTestStatus: false,
|
||||
EmailTestStatus: true,
|
||||
BacktraceStatus: true,
|
||||
Nt3Status: true,
|
||||
@@ -158,7 +158,7 @@ func (c *Config) ParseFlags(args []string) {
|
||||
c.GoecsFlag.BoolVar(&c.MemoryTestStatus, "memory", true, "Enable/Disable memory test")
|
||||
c.GoecsFlag.BoolVar(&c.DiskTestStatus, "disk", true, "Enable/Disable disk test")
|
||||
c.GoecsFlag.BoolVar(&c.UtTestStatus, "ut", true, "Enable/Disable unlock media test")
|
||||
c.GoecsFlag.BoolVar(&c.SecurityTestStatus, "security", true, "Enable/Disable security test")
|
||||
c.GoecsFlag.BoolVar(&c.SecurityTestStatus, "security", false, "Enable/Disable security test")
|
||||
c.GoecsFlag.BoolVar(&c.EmailTestStatus, "email", true, "Enable/Disable email port test")
|
||||
c.GoecsFlag.BoolVar(&c.BacktraceStatus, "backtrace", true, "Enable/Disable backtrace test (in 'en' language or on windows it always false)")
|
||||
c.GoecsFlag.BoolVar(&c.Nt3Status, "nt3", true, "Enable/Disable NT3 test (in 'en' language or on windows it always false)")
|
||||
|
||||
@@ -444,11 +444,6 @@ func AppendAnalysisSummary(config *params.Config, output, tempOutput string, out
|
||||
if strings.TrimSpace(summary) == "" {
|
||||
return
|
||||
}
|
||||
if config.Language == "zh" {
|
||||
utils.PrintCenteredTitle("测试总结分析", config.Width)
|
||||
} else {
|
||||
utils.PrintCenteredTitle("Result Summary Analysis", config.Width)
|
||||
}
|
||||
fmt.Println(summary)
|
||||
}, tempOutput, output)
|
||||
}
|
||||
|
||||
@@ -5,9 +5,6 @@ import (
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/oneclickvirt/privatespeedtest/pst"
|
||||
"github.com/oneclickvirt/speedtest/model"
|
||||
"github.com/oneclickvirt/speedtest/sp"
|
||||
)
|
||||
@@ -34,209 +31,12 @@ func NearbySP() {
|
||||
}
|
||||
}
|
||||
|
||||
// formatString 格式化字符串到指定宽度
|
||||
func formatString(s string, width int) string {
|
||||
return fmt.Sprintf("%-*s", width, s)
|
||||
}
|
||||
|
||||
// printTableRow 打印表格行
|
||||
func printTableRow(result pst.SpeedTestResult) {
|
||||
location := result.City
|
||||
if result.CarrierType != "" {
|
||||
carrier := result.CarrierType
|
||||
switch carrier {
|
||||
case "Telecom":
|
||||
carrier = "电信"
|
||||
case "Unicom":
|
||||
carrier = "联通"
|
||||
case "Mobile":
|
||||
carrier = "移动"
|
||||
case "Other":
|
||||
carrier = "其他"
|
||||
}
|
||||
location = fmt.Sprintf("%s%s", carrier, result.City)
|
||||
}
|
||||
if len(location) > 15 {
|
||||
location = location[:15]
|
||||
}
|
||||
upload := "N/A"
|
||||
if result.UploadMbps > 0 {
|
||||
upload = fmt.Sprintf("%.2f Mbps", result.UploadMbps)
|
||||
}
|
||||
download := "N/A"
|
||||
if result.DownloadMbps > 0 {
|
||||
download = fmt.Sprintf("%.2f Mbps", result.DownloadMbps)
|
||||
}
|
||||
latency := fmt.Sprintf("%.2f ms", result.PingLatency.Seconds()*1000)
|
||||
packetLoss := "N/A"
|
||||
fmt.Print(formatString(location, 15))
|
||||
fmt.Print(formatString(upload, 16))
|
||||
fmt.Print(formatString(download, 16))
|
||||
fmt.Print(formatString(latency, 16))
|
||||
fmt.Print(formatString(packetLoss, 16))
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// privateSpeedTest 使用 privatespeedtest 进行单个运营商测速
|
||||
// operator 参数:只支持 "cmcc"、"cu"、"ct"、"other"
|
||||
// 返回值:实际测试的节点数量和错误信息
|
||||
func privateSpeedTest(num int, operator string) (int, error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Fprintf(os.Stderr, "[WARN] privateSpeedTest panic: %v\n", r)
|
||||
}
|
||||
}()
|
||||
*pst.NoProgress = true
|
||||
*pst.Quiet = true
|
||||
*pst.NoHeader = true
|
||||
*pst.NoProjectURL = true
|
||||
// 加载服务器列表
|
||||
serverList, err := pst.LoadServerList()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("加载自定义服务器列表失败")
|
||||
}
|
||||
// 使用三网测速模式(每个运营商选择指定数量的最低延迟节点)
|
||||
serversPerISP := num
|
||||
if serversPerISP <= 0 || serversPerISP > 5 {
|
||||
serversPerISP = 2
|
||||
}
|
||||
// 单个运营商测速:先过滤服务器列表
|
||||
var carrierType string
|
||||
switch strings.ToLower(operator) {
|
||||
case "cmcc":
|
||||
carrierType = "Mobile"
|
||||
case "cu":
|
||||
carrierType = "Unicom"
|
||||
case "ct":
|
||||
carrierType = "Telecom"
|
||||
case "other":
|
||||
carrierType = "Other"
|
||||
default:
|
||||
return 0, fmt.Errorf("不支持的运营商类型: %s", operator)
|
||||
}
|
||||
// 过滤出指定运营商的服务器
|
||||
filteredServers := pst.FilterServersByISP(serverList.Servers, carrierType)
|
||||
// 先找足够多的候选服务器用于去重(找 serversPerISP * 3 个,确保去重后还能剩下足够的服务器)
|
||||
candidateCount := serversPerISP * 3
|
||||
if candidateCount > len(filteredServers) {
|
||||
candidateCount = len(filteredServers)
|
||||
}
|
||||
// 使用 FindBestServers 选择最佳服务器
|
||||
candidateServers, err := pst.FindBestServers(
|
||||
filteredServers,
|
||||
candidateCount, // 选择更多候选节点用于去重
|
||||
5*time.Second, // ping 超时
|
||||
true, // 显示进度条
|
||||
true, // 静默
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("分组查找失败")
|
||||
}
|
||||
// 去重:确保同一运营商内城市不重复
|
||||
seenCities := make(map[string]bool)
|
||||
var bestServers []pst.ServerWithLatencyInfo
|
||||
// 保留更多备用节点,以应对测速失败的情况(保留 serversPerISP * 2 个备用节点)
|
||||
maxBackupServers := serversPerISP * 2
|
||||
for _, serverInfo := range candidateServers {
|
||||
city := serverInfo.Server.City
|
||||
if city == "" {
|
||||
city = "Unknown"
|
||||
}
|
||||
if !seenCities[city] {
|
||||
seenCities[city] = true
|
||||
bestServers = append(bestServers, serverInfo)
|
||||
// 去重后保留足够的备用节点
|
||||
if len(bestServers) >= maxBackupServers {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(bestServers) == 0 {
|
||||
return 0, fmt.Errorf("去重后没有可用的服务器")
|
||||
}
|
||||
// 执行测速并逐个打印结果(不打印表头)
|
||||
// 统计成功输出的节点数
|
||||
successCount := 0
|
||||
for i, serverInfo := range bestServers {
|
||||
// 如果已经成功输出了足够的节点,则停止测试
|
||||
if successCount >= serversPerISP {
|
||||
break
|
||||
}
|
||||
result := pst.RunSpeedTest(
|
||||
serverInfo.Server,
|
||||
false, // 不禁用下载测试
|
||||
false, // 不禁用上传测试
|
||||
6, // 并发线程数
|
||||
12*time.Second, // 超时时间
|
||||
&serverInfo,
|
||||
false, // 不显示进度条
|
||||
)
|
||||
// 只要测试成功且有任意一个速度值有效,就输出结果(部分成功也显示)
|
||||
if result.Success && (result.UploadMbps > 0 || result.DownloadMbps > 0) {
|
||||
printTableRow(result)
|
||||
// 只有上传和下载都成功时才计入成功数
|
||||
if result.UploadMbps > 0 && result.DownloadMbps > 0 {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
// 在测试之间暂停(如果还需要继续测试的话)
|
||||
if successCount < serversPerISP && i < len(bestServers)-1 {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
// 返回实际成功输出的节点数量
|
||||
return successCount, nil
|
||||
}
|
||||
|
||||
// privateSpeedTestWithFallback 使用私有测速,如果失败则回退到 global 节点
|
||||
// 主要用于 Other 类型的测速
|
||||
func privateSpeedTestWithFallback(num int, operator, language string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Fprintf(os.Stderr, "[WARN] privateSpeedTestWithFallback panic: %v\n", r)
|
||||
}
|
||||
}()
|
||||
// 先尝试私有节点测速
|
||||
testedCount, err := privateSpeedTest(num, operator)
|
||||
if err != nil || testedCount == 0 {
|
||||
// 私有节点失败,回退到 global 节点
|
||||
var url, parseType string
|
||||
url = model.NetGlobal
|
||||
parseType = "id"
|
||||
if runtime.GOOS == "windows" || sp.OfficialAvailableTest() != nil {
|
||||
sp.CustomSpeedTest(url, parseType, num, language)
|
||||
} else {
|
||||
sp.OfficialCustomSpeedTest(url, parseType, num, language)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func CustomSP(platform, operator string, num int, language string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Fprintf(os.Stderr, "[WARN] CustomSP panic: %v\n", r)
|
||||
}
|
||||
}()
|
||||
// 对于三网测速(cmcc、cu、ct)和 other,优先使用 privatespeedtest 进行私有测速
|
||||
opLower := strings.ToLower(operator)
|
||||
if opLower == "cmcc" || opLower == "cu" || opLower == "ct" || opLower == "other" {
|
||||
testedCount, err := privateSpeedTest(num, opLower)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[WARN] privatespeedtest failed: %v\n", err)
|
||||
// 全部失败,继续使用原有的公共节点兜底方案
|
||||
} else if testedCount >= num {
|
||||
// 私有节点测速成功且数量达标,直接返回
|
||||
return
|
||||
} else if testedCount > 0 {
|
||||
// 部分私有节点测速成功,但数量不足,用公共节点补充
|
||||
fmt.Fprintf(os.Stderr, "[INFO] 私有节点仅测试了 %d 个,补充 %d 个公共节点\n", testedCount, num-testedCount)
|
||||
num = num - testedCount // 只测剩余数量的公共节点
|
||||
// 继续执行下面的公共节点测速逻辑
|
||||
} else {
|
||||
// testedCount == 0,继续使用公共节点
|
||||
}
|
||||
}
|
||||
|
||||
var url, parseType string
|
||||
if strings.ToLower(platform) == "cn" {
|
||||
if strings.ToLower(operator) == "cmcc" {
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
"github.com/oneclickvirt/basics/system"
|
||||
butils "github.com/oneclickvirt/basics/utils"
|
||||
. "github.com/oneclickvirt/defaultset"
|
||||
"github.com/oneclickvirt/security/network"
|
||||
"github.com/oneclickvirt/basics/network"
|
||||
)
|
||||
|
||||
// IsAndroid 检测当前是否在 Android (Termux) 环境下运行
|
||||
@@ -136,13 +136,13 @@ func PrintCenteredTitle(title string, width int) {
|
||||
// PrintHead 根据语言打印头部信息
|
||||
func PrintHead(language string, width int, ecsVersion string) {
|
||||
if language == "zh" {
|
||||
PrintCenteredTitle("VPS融合怪测试", width)
|
||||
PrintCenteredTitle("VPS融合怪测试(非官方编译)", width)
|
||||
fmt.Printf("版本:%s\n", ecsVersion)
|
||||
fmt.Println("测评频道: https://t.me/+UHVoo2U4VyA5NTQ1\n" +
|
||||
"Go项目地址:https://github.com/oneclickvirt/ecs\n" +
|
||||
"Shell项目地址:https://github.com/spiritLHLS/ecs")
|
||||
} else {
|
||||
PrintCenteredTitle("VPS Fusion Monster Test", width)
|
||||
PrintCenteredTitle("VPS Fusion Monster Test (Unofficial)", width)
|
||||
fmt.Printf("Version: %s\n", ecsVersion)
|
||||
fmt.Println("Review Channel: https://t.me/+UHVoo2U4VyA5NTQ1\n" +
|
||||
"Go Project: https://github.com/oneclickvirt/ecs\n" +
|
||||
@@ -350,7 +350,7 @@ func PrintAndCapture(f func(), tempOutput, output string) string {
|
||||
func UploadText(absPath string) (string, string, error) {
|
||||
primaryURL := "http://hpaste.spiritlhl.net/api/UL/upload"
|
||||
backupURL := "https://paste.spiritlhl.net/api/UL/upload"
|
||||
token := network.SecurityUploadToken
|
||||
token := "OvwKx5qgJtf7PZgCKbtyojSU.MTcwMTUxNzY1MTgwMw"
|
||||
client := req.C().SetTimeout(6 * time.Second)
|
||||
client.R().
|
||||
SetRetryCount(2).
|
||||
|
||||
Reference in New Issue
Block a user