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 | |
|---|---|---|---|
|
|
85a0b30950 | ||
|
|
21100fd949 | ||
|
|
d2878dd705 | ||
|
|
a80ce40739 | ||
|
|
a1b5691179 | ||
|
|
6cf35680d5 |
17
go.mod
17
go.mod
@@ -3,6 +3,7 @@ module github.com/oneclickvirt/ecs
|
||||
go 1.25.4
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/bubbles v1.0.0
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/imroc/req/v3 v3.54.0
|
||||
@@ -27,11 +28,15 @@ require (
|
||||
github.com/StackExchange/wmi v1.2.1 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.9.0 // indirect
|
||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
@@ -56,12 +61,12 @@ require (
|
||||
github.com/koron/go-ssdp v0.0.4 // indirect
|
||||
github.com/libp2p/go-nat v0.2.0 // indirect
|
||||
github.com/libp2p/go-netroute v0.2.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||
github.com/miekg/dns v1.1.61 // indirect
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
|
||||
22
go.sum
22
go.sum
@@ -6,22 +6,40 @@ github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwTo
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
||||
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=
|
||||
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
|
||||
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
|
||||
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
|
||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
|
||||
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -93,6 +111,8 @@ github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9t
|
||||
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=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
@@ -103,6 +123,8 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J
|
||||
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=
|
||||
github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
|
||||
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
||||
|
||||
2
goecs.go
2
goecs.go
@@ -27,7 +27,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ecsVersion = "v0.1.119" // 融合怪版本号
|
||||
ecsVersion = "v0.1.121" // 融合怪版本号
|
||||
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.118"
|
||||
ECS_VERSION="0.1.120"
|
||||
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.118"
|
||||
ECS_VERSION="0.1.118"
|
||||
_yellow "Unable to get version info, using default version 0.1.120"
|
||||
ECS_VERSION="0.1.120"
|
||||
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,415 @@ 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
|
||||
}
|
||||
|
||||
// GenerateSummary creates a concise one-line 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)
|
||||
parts := make([]string, 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 {
|
||||
s := extractCPURankCondensed(finalOutput, lang)
|
||||
if s != "" {
|
||||
parts = append(parts, s)
|
||||
} else if sectionExists(finalOutput, "CPU测试", "CPU-Test") {
|
||||
// Section ran but no rank could be derived (test failure or no DB match)
|
||||
if lang == "zh" {
|
||||
parts = append(parts, "CPU排名: "+na())
|
||||
} else {
|
||||
parts = append(parts, "CPU rank: "+na())
|
||||
}
|
||||
}
|
||||
// If the section doesn't appear 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 {
|
||||
parts = append(parts, "内存为 "+mem+"(达标)")
|
||||
} else {
|
||||
parts = append(parts, "内存为 "+mem+"(未达标)")
|
||||
}
|
||||
} else {
|
||||
if bw >= memAvgThreshMbps {
|
||||
parts = append(parts, "Memory: "+mem+"(pass)")
|
||||
} else {
|
||||
parts = append(parts, "Memory: "+mem+"(below avg)")
|
||||
}
|
||||
}
|
||||
} else if sectionExists(finalOutput, "内存测试", "Memory-Test") {
|
||||
if lang == "zh" {
|
||||
parts = append(parts, "内存: "+na())
|
||||
} else {
|
||||
parts = append(parts, "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
|
||||
if lang == "zh" {
|
||||
var label string
|
||||
if diskOK {
|
||||
label = "(达标)"
|
||||
} else {
|
||||
label = "(未达标)"
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("硬盘IO为 %s %d路%s", dtype, pathCount, label))
|
||||
} else {
|
||||
var label string
|
||||
if diskOK {
|
||||
label = "(pass)"
|
||||
} else {
|
||||
label = "(below avg)"
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("Disk IO: %s %d path(s)%s", dtype, pathCount, label))
|
||||
}
|
||||
} else if sectionExists(finalOutput, "硬盘测试", "Disk-Test") {
|
||||
if lang == "zh" {
|
||||
parts = append(parts, "硬盘IO: "+na())
|
||||
} else {
|
||||
parts = append(parts, "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)
|
||||
s := extractBandwidthCondensed(bwVals, lang)
|
||||
if s != "" {
|
||||
parts = append(parts, s)
|
||||
} else if sectionExists(finalOutput, "测速", "Speed-Test") {
|
||||
if lang == "zh" {
|
||||
parts = append(parts, "网络带宽: "+na())
|
||||
} else {
|
||||
parts = append(parts, "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 != "" {
|
||||
parts = append(parts, "国内三大运营商推荐排名为 "+ranking)
|
||||
} else if sectionExists(finalOutput, "上游及回程线路检测", "") {
|
||||
parts = append(parts, "国内三大运营商推荐排名: "+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(parts) == 0 {
|
||||
if lang == "zh" {
|
||||
return "测试总结: 无足够数据生成摘要。"
|
||||
}
|
||||
return "Test Summary: insufficient data for summary."
|
||||
}
|
||||
|
||||
prefix := "测试总结: "
|
||||
if lang != "zh" {
|
||||
prefix = "Test Summary: "
|
||||
}
|
||||
return prefix + strings.Join(parts, " | ")
|
||||
}
|
||||
|
||||
@@ -217,6 +217,9 @@ func HandleMenuMode(preCheck utils.NetCheckResult, config *params.Config) {
|
||||
config.Nt3Location = "ALL"
|
||||
SetRouteTestStatus(config)
|
||||
}
|
||||
// Apply quick options set on the main menu page
|
||||
config.AnalyzeResult = result.mainAnalyze
|
||||
config.EnableUpload = result.mainUpload
|
||||
}
|
||||
config.RestoreUserSetParams(savedParams)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ var (
|
||||
tBtnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("0")).Background(lipgloss.Color("120")).Bold(true).Padding(0, 2)
|
||||
tBtnDimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("252")).Background(lipgloss.Color("238")).Padding(0, 2)
|
||||
tPanelStyle = lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder()).BorderForeground(lipgloss.Color("238")).Padding(0, 1)
|
||||
tOnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("82")).Bold(true)
|
||||
tOffStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("203"))
|
||||
tValStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("117"))
|
||||
tCurStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("220")).Bold(true)
|
||||
)
|
||||
|
||||
type menuPhase int
|
||||
@@ -79,12 +83,14 @@ type advSetting struct {
|
||||
}
|
||||
|
||||
type tuiResult struct {
|
||||
choice string
|
||||
language string
|
||||
quit bool
|
||||
custom bool
|
||||
toggles []testToggle
|
||||
advanced []advSetting
|
||||
choice string
|
||||
language string
|
||||
quit bool
|
||||
custom bool
|
||||
toggles []testToggle
|
||||
advanced []advSetting
|
||||
mainAnalyze bool
|
||||
mainUpload bool
|
||||
}
|
||||
|
||||
type tuiModel struct {
|
||||
@@ -93,9 +99,12 @@ type tuiModel struct {
|
||||
preCheck utils.NetCheckResult
|
||||
langPreset bool
|
||||
|
||||
langCursor int
|
||||
mainCursor int
|
||||
mainItems []mainMenuItem
|
||||
langCursor int
|
||||
mainCursor int
|
||||
mainItems []mainMenuItem
|
||||
mainAnalyze bool
|
||||
mainUpload bool
|
||||
mainExtraTotal int
|
||||
|
||||
customCursor int
|
||||
toggles []testToggle
|
||||
@@ -120,14 +129,14 @@ type tuiModel struct {
|
||||
func defaultMainItems() []mainMenuItem {
|
||||
return []mainMenuItem{
|
||||
{id: "1", zh: "融合怪完全体(能测全测)", en: "Full Test (All Available Tests)", descZh: "系统信息、CPU、内存、磁盘、解锁、IP质量、邮件端口、回程、NT3、测速、TGDC、网站延迟。", descEn: "Runs all available modules: system, compute, memory, disk, unlock, security, routing and speed.", needNet: false},
|
||||
{id: "2", zh: "极简版", en: "Minimal Suite", descZh: "基础系统+CPU+内存+磁盘+5个测速节点,适合快速健康检查。", descEn: "Basic system + CPU + memory + disk + 5 speed nodes for quick health checks.", needNet: false},
|
||||
{id: "3", zh: "精简版", en: "Standard Suite", descZh: "在极简版基础上增加平台解锁与路由能力评估。", descEn: "Minimal suite plus unlock and routing capability checks.", needNet: false},
|
||||
{id: "4", zh: "精简网络版", en: "Network Suite", descZh: "强调网络回程与路由质量,辅以基础硬件测试。", descEn: "Network-focused profile with backtrace/routing plus basic hardware checks.", needNet: false},
|
||||
{id: "5", zh: "精简解锁版", en: "Unlock Suite", descZh: "以流媒体和平台解锁能力为主,附基础性能测试。", descEn: "Unlock-focused profile with essential compute/storage checks.", needNet: false},
|
||||
{id: "2", zh: "极简版", en: "Minimal Suite", descZh: "系统信息+CPU+内存+磁盘+测速节点×5,不含解锁/网络/路由测试。", descEn: "System info + CPU + memory + disk + 5 speed nodes. No unlock/network/routing tests.", needNet: false},
|
||||
{id: "3", zh: "精简版", en: "Standard Suite", descZh: "系统信息+CPU+内存+磁盘+跨国平台解锁+三网回程路由+测速节点×5。", descEn: "System info + CPU + memory + disk + streaming unlock + 3-network routing + 5 speed nodes.", needNet: false},
|
||||
{id: "4", zh: "精简网络版", en: "Network Suite", descZh: "系统信息+CPU+内存+磁盘+上游及三网回程路由+测速节点×5。", descEn: "System info + CPU + memory + disk + upstream/3-network backtrace routing + 5 speed nodes.", needNet: false},
|
||||
{id: "5", zh: "精简解锁版", en: "Unlock Suite", descZh: "系统信息+CPU+内存+磁盘IO+跨国平台解锁+测速节点×5。", descEn: "System info + CPU + memory + disk IO + streaming unlock + 5 speed nodes.", needNet: false},
|
||||
{id: "6", zh: "网络单项", en: "Network Only", descZh: "仅网络维度:IP质量、回程、NT3、延迟、TGDC、网站和测速。", descEn: "Network-only profile: IP quality, route, latency, TGDC, websites, speed.", needNet: true},
|
||||
{id: "7", zh: "解锁单项", en: "Unlock Only", descZh: "仅进行跨国平台解锁与流媒体可用性检测。", descEn: "Unlock-only profile for cross-border media/service availability.", needNet: true},
|
||||
{id: "8", zh: "硬件单项", en: "Hardware Only", descZh: "系统信息、CPU、内存、dd/fio 磁盘测试。", descEn: "Hardware-only profile with system, CPU, memory and disk tests.", needNet: false},
|
||||
{id: "9", zh: "IP质量检测", en: "IP Quality", descZh: "15库 IP质量 + 邮件端口,适合网络身份风险评估。", descEn: "IP quality across multiple datasets plus email port checks.", needNet: true},
|
||||
{id: "9", zh: "IP质量检测", en: "IP Quality", descZh: "15个数据库IP质量检测+邮件端口连通性检测。", descEn: "IP quality check across 15 databases + email port connectivity test.", needNet: true},
|
||||
{id: "10", zh: "三网回程线路", en: "3-Network Route", descZh: "三网回程、NT3路由、延迟、TGDC、网站延迟专项。", descEn: "3-network backtrace + NT3 route + latency/TGDC/website checks.", needNet: true},
|
||||
{id: "custom", zh: ">>> 高级自定义(全参数模式)", en: ">>> Advanced Custom (Full Parameters)", descZh: "按参数逐项配置,支持测试项、方法、路径、上传和结果分析。", descEn: "Configure per-parameter with test toggles, methods, paths, upload and analysis.", needNet: false},
|
||||
{id: "0", zh: "退出程序", en: "Exit Program", descZh: "退出当前程序。", descEn: "Exit program.", needNet: false},
|
||||
@@ -147,7 +156,7 @@ func defaultTestToggles() []testToggle {
|
||||
{key: "nt3", nameZh: "NT3路由", nameEn: "NT3 Route", descZh: "按指定地区与协议执行详细路由追踪。", descEn: "Run detailed route trace by selected location/protocol.", enabled: false, needNet: true},
|
||||
{key: "speed", nameZh: "测速", nameEn: "Speed Test", descZh: "测试下载/上传带宽与延迟。", descEn: "Measure download/upload bandwidth and latency.", enabled: false, needNet: true},
|
||||
{key: "ping", nameZh: "Ping测试", nameEn: "Ping Test", descZh: "全国/多地区延迟质量测试。", descEn: "Latency quality checks across multiple regions.", enabled: false, needNet: true},
|
||||
{key: "tgdc", nameZh: "Telegram DC测试", nameEn: "Telegram DC Test", descZh: "检测 Telegram 数据中心延迟表现。", descEn: "Measure latency to Telegram data centers.", enabled: false, needNet: true},
|
||||
{key: "tgdc", nameZh: "Telegram DC测试", nameEn: "Telegram DC Test", descZh: "检测各 Telegram 数据中心节点延迟。", descEn: "Measure latency to each Telegram data center node.", enabled: false, needNet: true},
|
||||
{key: "web", nameZh: "网站延迟", nameEn: "Website Latency", descZh: "检测常见网站访问延迟。", descEn: "Check latency to commonly used websites.", enabled: false, needNet: true},
|
||||
}
|
||||
}
|
||||
@@ -160,43 +169,43 @@ func defaultAdvSettings(config *params.Config) []advSetting {
|
||||
adv := []advSetting{
|
||||
{
|
||||
key: "cpum", nameZh: "CPU测试方法", nameEn: "CPU Method", kind: "option",
|
||||
descZh: "选择 CPU 压测方法,不同方法偏向不同负载模型。",
|
||||
descEn: "Choose CPU benchmark method. Different methods model different workloads.",
|
||||
descZh: "选择 CPU 基准测试工具(sysbench/geekbench/winsat)。",
|
||||
descEn: "Choose CPU benchmark tool (sysbench/geekbench/winsat).",
|
||||
options: []advOption{
|
||||
option("sysbench", "Sysbench", "Sysbench", "通用基准,稳定易比较。", "General-purpose benchmark with stable comparability."),
|
||||
option("geekbench", "Geekbench", "Geekbench", "偏综合应用模型,便于横向对比。", "Application-like synthetic benchmark for broad comparison."),
|
||||
option("winsat", "WinSAT", "WinSAT", "Windows 场景下常用基准。", "Common benchmark in Windows environments."),
|
||||
option("sysbench", "Sysbench", "Sysbench", "通用 CPU 基准测试工具。", "General-purpose CPU benchmark tool."),
|
||||
option("geekbench", "Geekbench", "Geekbench", "综合场景 CPU 基准测试工具。", "Synthetic benchmark simulating real-world application workloads."),
|
||||
option("winsat", "WinSAT", "WinSAT", "Windows 环境下的 CPU 基准测试工具。", "CPU benchmark tool for Windows environments."),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "cput", nameZh: "CPU线程模式", nameEn: "CPU Thread Mode", kind: "option",
|
||||
descZh: "单线程看核心峰值,多线程看整机并发能力。",
|
||||
descEn: "Single-core shows peak core power; multi-core shows parallel throughput.",
|
||||
descZh: "单线程: 测试单核最高运算速度; 多线程: 测试全核并发吞吐。",
|
||||
descEn: "Single-thread: peak single-core speed; Multi-thread: full-core parallel throughput.",
|
||||
options: []advOption{
|
||||
option("multi", "多线程", "Multi-thread", "评估整机并发算力。", "Evaluate full-machine parallel compute capability."),
|
||||
option("single", "单线程", "Single-thread", "评估单核心峰值性能。", "Evaluate peak single-core performance."),
|
||||
option("multi", "多线程", "Multi-thread", "测试所有核心并发运算吞吐。", "Measure parallel compute throughput across all cores."),
|
||||
option("single", "单线程", "Single-thread", "测试单核最高运算速度。", "Measure peak single-core compute speed."),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "memorym", nameZh: "内存测试方法", nameEn: "Memory Method", kind: "option",
|
||||
descZh: "选择内存测试方法,结果关注带宽与访问效率。",
|
||||
descEn: "Choose memory benchmark method to evaluate bandwidth/access efficiency.",
|
||||
descZh: "选择内存基准测试工具。",
|
||||
descEn: "Choose memory benchmark tool.",
|
||||
options: []advOption{
|
||||
option("stream", "STREAM", "STREAM", "侧重带宽测试。", "Focused on memory bandwidth."),
|
||||
option("sysbench", "Sysbench", "Sysbench", "通用内存压测。", "General-purpose memory stress benchmark."),
|
||||
option("dd", "dd", "dd", "基于系统工具的简化测试。", "Simple system-tool-based measurement."),
|
||||
option("winsat", "WinSAT", "WinSAT", "Windows 环境内存基准。", "Windows-oriented memory benchmark."),
|
||||
option("auto", "自动", "Auto", "自动选择可用且优先方法。", "Automatically select the preferred available method."),
|
||||
option("stream", "STREAM", "STREAM", "专项内存带宽基准测试工具(STREAM)。", "Memory bandwidth benchmark tool (STREAM)."),
|
||||
option("sysbench", "Sysbench", "Sysbench", "通用内存基准测试工具。", "General-purpose memory benchmark tool."),
|
||||
option("dd", "dd", "dd", "使用 dd 命令测量内存顺序读写。", "Measure memory sequential R/W using dd command."),
|
||||
option("winsat", "WinSAT", "WinSAT", "Windows 环境内存基准测试工具。", "Memory benchmark tool for Windows environments."),
|
||||
option("auto", "自动", "Auto", "按优先级自动选择可用测试工具。", "Automatically select the preferred available tool."),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "diskm", nameZh: "磁盘测试方法", nameEn: "Disk Method", kind: "option",
|
||||
descZh: "选择磁盘测试方法,评估顺序/随机读写能力。",
|
||||
descEn: "Choose disk method to evaluate sequential/random I/O performance.",
|
||||
descZh: "选择磁盘基准测试工具。",
|
||||
descEn: "Choose disk benchmark tool.",
|
||||
options: []advOption{
|
||||
option("fio", "FIO", "FIO", "更全面的磁盘 I/O 基准。", "Comprehensive disk I/O benchmark."),
|
||||
option("dd", "dd", "dd", "快速顺序写读基准。", "Quick sequential write/read benchmark."),
|
||||
option("winsat", "WinSAT", "WinSAT", "Windows 磁盘基准。", "Disk benchmark for Windows environments."),
|
||||
option("fio", "FIO", "FIO", "多队列深度顺序/随机 I/O 全面基准测试。", "Comprehensive sequential/random I/O benchmark with multiple queue depths."),
|
||||
option("dd", "dd", "dd", "使用 dd 命令进行顺序读写基准测试。", "Sequential read/write benchmark using dd command."),
|
||||
option("winsat", "WinSAT", "WinSAT", "Windows 环境磁盘基准测试工具。", "Disk benchmark tool for Windows environments."),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -207,10 +216,16 @@ func defaultAdvSettings(config *params.Config) []advSetting {
|
||||
},
|
||||
{
|
||||
key: "diskmc", nameZh: "多磁盘检测", nameEn: "Multi-Disk Check", kind: "bool",
|
||||
descZh: "启用后尝试检测并测试多磁盘路径。",
|
||||
descEn: "When enabled, detect and benchmark multiple disk paths.",
|
||||
descZh: "启用后检测并测试所有已挂载磁盘路径。",
|
||||
descEn: "When enabled, detect and benchmark all mounted disk paths.",
|
||||
boolVal: config.DiskMultiCheck,
|
||||
},
|
||||
{
|
||||
key: "autodiskm", nameZh: "磁盘方法失败自动切换", nameEn: "Auto Switch Disk Method", kind: "bool",
|
||||
descZh: "所选磁盘测试方法失败时自动切换为其他可用方法。",
|
||||
descEn: "Automatically try another available disk method if the selected method fails.",
|
||||
boolVal: config.AutoChangeDiskMethod,
|
||||
},
|
||||
{
|
||||
key: "nt3loc", nameZh: "NT3测试地区", nameEn: "NT3 Location", kind: "option",
|
||||
descZh: "选择路由追踪地区。显示中文全称,内部仍使用标准参数值。",
|
||||
@@ -235,19 +250,19 @@ func defaultAdvSettings(config *params.Config) []advSetting {
|
||||
},
|
||||
{
|
||||
key: "spnum", nameZh: "测速节点数/运营商", nameEn: "Speed Nodes per ISP", kind: "option",
|
||||
descZh: "每个运营商选择的测速节点数量。",
|
||||
descEn: "Number of speed test nodes selected per ISP.",
|
||||
descZh: "每个运营商参与测速的节点数量。",
|
||||
descEn: "Number of speed test nodes per ISP.",
|
||||
options: []advOption{
|
||||
option("1", "1 个", "1 node", "最快速,覆盖最少。", "Fastest run with least coverage."),
|
||||
option("2", "2 个", "2 nodes", "默认平衡。", "Default balanced option."),
|
||||
option("3", "3 个", "3 nodes", "覆盖更广,耗时增加。", "Broader coverage with more runtime."),
|
||||
option("4", "4 个", "4 nodes", "更完整网络采样。", "More complete network sampling."),
|
||||
option("5", "5 个", "5 nodes", "高覆盖,耗时较高。", "High coverage with longer runtime."),
|
||||
option("6", "6 个", "6 nodes", "深度采样。", "Deep sampling."),
|
||||
option("7", "7 个", "7 nodes", "深度采样。", "Deep sampling."),
|
||||
option("8", "8 个", "8 nodes", "深度采样。", "Deep sampling."),
|
||||
option("9", "9 个", "9 nodes", "深度采样。", "Deep sampling."),
|
||||
option("10", "10 个", "10 nodes", "最全面,耗时最高。", "Most comprehensive, longest runtime."),
|
||||
option("1", "1 个", "1 node", "每运营商1节点,耗时最短,覆盖面最小。", "1 node per ISP, shortest runtime, least coverage."),
|
||||
option("2", "2 个", "2 nodes", "每运营商2节点(默认值)。", "2 nodes per ISP (default)."),
|
||||
option("3", "3 个", "3 nodes", "每运营商3节点,覆盖面扩大,耗时增加。", "3 nodes per ISP, wider coverage, longer runtime."),
|
||||
option("4", "4 个", "4 nodes", "每运营商4节点。", "4 nodes per ISP."),
|
||||
option("5", "5 个", "5 nodes", "每运营商5节点,覆盖面宽,耗时较高。", "5 nodes per ISP, wide coverage, higher runtime."),
|
||||
option("6", "6 个", "6 nodes", "每运营商6节点。", "6 nodes per ISP."),
|
||||
option("7", "7 个", "7 nodes", "每运营商7节点。", "7 nodes per ISP."),
|
||||
option("8", "8 个", "8 nodes", "每运营商8节点。", "8 nodes per ISP."),
|
||||
option("9", "9 个", "9 nodes", "每运营商9节点。", "9 nodes per ISP."),
|
||||
option("10", "10 个", "10 nodes", "每运营商10节点,覆盖面最宽,耗时最高。", "10 nodes per ISP, widest coverage, longest runtime."),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -264,8 +279,8 @@ func defaultAdvSettings(config *params.Config) []advSetting {
|
||||
},
|
||||
{
|
||||
key: "analysis", nameZh: "测试后结果总结分析", nameEn: "Post-Test Summary Analysis", kind: "bool",
|
||||
descZh: "测试结束后生成简明总结,提炼优势、短板和用途建议。",
|
||||
descEn: "Generate concise final summary with strengths, limits and usage hints.",
|
||||
descZh: "测试结束后输出简明总结(含CPU排名、带宽和延迟数据)。",
|
||||
descEn: "Output a concise summary after tests (CPU rank, bandwidth, latency scores).",
|
||||
boolVal: config.AnalyzeResult,
|
||||
},
|
||||
{
|
||||
@@ -329,21 +344,24 @@ func newTuiModel(preCheck utils.NetCheckResult, config *params.Config, langPrese
|
||||
ti.CharLimit = 255
|
||||
ti.Width = 45
|
||||
m := tuiModel{
|
||||
config: config,
|
||||
preCheck: preCheck,
|
||||
langPreset: langPreset,
|
||||
mainItems: defaultMainItems(),
|
||||
toggles: toggles,
|
||||
advanced: advanced,
|
||||
customTotal: len(toggles) + len(advanced) + 1,
|
||||
statsTotal: statsTotal,
|
||||
statsDaily: statsDaily,
|
||||
hasStats: hasStats,
|
||||
cmpVersion: cmpVersion,
|
||||
newVersion: newVersion,
|
||||
width: config.Width,
|
||||
height: 24,
|
||||
textInput: ti,
|
||||
config: config,
|
||||
preCheck: preCheck,
|
||||
langPreset: langPreset,
|
||||
mainItems: defaultMainItems(),
|
||||
mainAnalyze: config.AnalyzeResult,
|
||||
mainUpload: config.EnableUpload,
|
||||
mainExtraTotal: 2,
|
||||
toggles: toggles,
|
||||
advanced: advanced,
|
||||
customTotal: len(toggles) + len(advanced) + 1,
|
||||
statsTotal: statsTotal,
|
||||
statsDaily: statsDaily,
|
||||
hasStats: hasStats,
|
||||
cmpVersion: cmpVersion,
|
||||
newVersion: newVersion,
|
||||
width: config.Width,
|
||||
height: 24,
|
||||
textInput: ti,
|
||||
}
|
||||
if langPreset {
|
||||
m.phase = phaseMain
|
||||
@@ -431,7 +449,7 @@ func (m tuiModel) viewLang() string {
|
||||
cursor := " "
|
||||
style := tNormStyle
|
||||
if m.langCursor == i {
|
||||
cursor = " > "
|
||||
cursor = tCurStyle.Render(" > ")
|
||||
style = tSelStyle
|
||||
}
|
||||
s.WriteString(fmt.Sprintf("%s%s\n", cursor, style.Render(l)))
|
||||
@@ -444,20 +462,39 @@ func (m tuiModel) viewLang() string {
|
||||
|
||||
func (m tuiModel) updateMain(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
key := msg.String()
|
||||
maxCursor := len(m.mainItems) + m.mainExtraTotal - 1
|
||||
switch key {
|
||||
case "up", "k":
|
||||
if m.mainCursor > 0 {
|
||||
m.mainCursor--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.mainCursor < len(m.mainItems)-1 {
|
||||
if m.mainCursor < maxCursor {
|
||||
m.mainCursor++
|
||||
}
|
||||
case "home":
|
||||
m.mainCursor = 0
|
||||
case "end":
|
||||
m.mainCursor = len(m.mainItems) - 1
|
||||
m.mainCursor = maxCursor
|
||||
case " ":
|
||||
if m.mainCursor >= len(m.mainItems) {
|
||||
switch m.mainCursor - len(m.mainItems) {
|
||||
case 0:
|
||||
m.mainAnalyze = !m.mainAnalyze
|
||||
case 1:
|
||||
m.mainUpload = !m.mainUpload
|
||||
}
|
||||
}
|
||||
case "enter":
|
||||
if m.mainCursor >= len(m.mainItems) {
|
||||
switch m.mainCursor - len(m.mainItems) {
|
||||
case 0:
|
||||
m.mainAnalyze = !m.mainAnalyze
|
||||
case 1:
|
||||
m.mainUpload = !m.mainUpload
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
item := m.mainItems[m.mainCursor]
|
||||
if item.needNet && !m.preCheck.Connected {
|
||||
return m, nil
|
||||
@@ -467,6 +504,8 @@ func (m tuiModel) updateMain(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
m.customCursor = 0
|
||||
return m, nil
|
||||
}
|
||||
m.result.mainAnalyze = m.mainAnalyze
|
||||
m.result.mainUpload = m.mainUpload
|
||||
m.result.choice = item.id
|
||||
return m, tea.Quit
|
||||
case "q", "ctrl+c":
|
||||
@@ -484,6 +523,8 @@ func (m tuiModel) updateMain(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
m.mainCursor = i
|
||||
m.result.mainAnalyze = m.mainAnalyze
|
||||
m.result.mainUpload = m.mainUpload
|
||||
m.result.choice = item.id
|
||||
return m, tea.Quit
|
||||
}
|
||||
@@ -493,6 +534,21 @@ func (m tuiModel) updateMain(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (m tuiModel) selectedMainDesc(lang string) string {
|
||||
if m.mainCursor >= len(m.mainItems) {
|
||||
switch m.mainCursor - len(m.mainItems) {
|
||||
case 0:
|
||||
if lang == "zh" {
|
||||
return "测试结束后输出简明总结(含CPU排名、带宽和延迟数据)。默认关闭。"
|
||||
}
|
||||
return "Output a concise summary after tests (CPU rank, bandwidth, latency scores). Disabled by default."
|
||||
case 1:
|
||||
if lang == "zh" {
|
||||
return "上传测试结果到服务端并生成可分享链接。默认启用。"
|
||||
}
|
||||
return "Upload test results to the server and generate a shareable link. Enabled by default."
|
||||
}
|
||||
return ""
|
||||
}
|
||||
item := m.mainItems[m.mainCursor]
|
||||
if lang == "zh" {
|
||||
return item.descZh
|
||||
@@ -537,7 +593,7 @@ func (m tuiModel) viewMain() string {
|
||||
cursor := " "
|
||||
style := tNormStyle
|
||||
if m.mainCursor == i {
|
||||
cursor = " > "
|
||||
cursor = tCurStyle.Render(" > ")
|
||||
style = tSelStyle
|
||||
}
|
||||
label := item.en
|
||||
@@ -573,10 +629,59 @@ func (m tuiModel) viewMain() string {
|
||||
s.WriteString(tSectStyle.Render(panelTitle) + "\n")
|
||||
s.WriteString(tPanelStyle.Width(maxInt(60, m.width-6)).Render(panelBody) + "\n")
|
||||
s.WriteString("\n")
|
||||
// Quick options: analyze + upload
|
||||
if lang == "zh" {
|
||||
s.WriteString(tHelpStyle.Render(" ↑/↓/j/k 移动 Enter 确认 数字 快速选择 q 退出"))
|
||||
s.WriteString(tSectStyle.Render(" 快速选项:") + " " + tDimStyle.Render("Space/Enter 切换"))
|
||||
} else {
|
||||
s.WriteString(tHelpStyle.Render(" Up/Down/j/k Navigate Enter Confirm Number Quick-Select q Quit"))
|
||||
s.WriteString(tSectStyle.Render(" Quick Options:") + " " + tDimStyle.Render("Space/Enter to toggle"))
|
||||
}
|
||||
s.WriteString("\n")
|
||||
for qi, qState := range []bool{m.mainAnalyze, m.mainUpload} {
|
||||
qIdx := len(m.mainItems) + qi
|
||||
cur := " "
|
||||
nameStyle := tNormStyle
|
||||
if m.mainCursor == qIdx {
|
||||
cur = tCurStyle.Render(" > ")
|
||||
nameStyle = tSelStyle
|
||||
}
|
||||
chk := tChkOffStyle.Render("[ ]")
|
||||
if qState {
|
||||
chk = tChkOnStyle.Render("[x]")
|
||||
}
|
||||
var qName, qVal string
|
||||
if qi == 0 {
|
||||
if lang == "zh" {
|
||||
qName = "测试后自动总结分析"
|
||||
} else {
|
||||
qName = "Post-test Summary Analysis"
|
||||
}
|
||||
} else {
|
||||
if lang == "zh" {
|
||||
qName = "上传结果并生成分享链接"
|
||||
} else {
|
||||
qName = "Upload Result & Share Link"
|
||||
}
|
||||
}
|
||||
if qState {
|
||||
if lang == "zh" {
|
||||
qVal = tOnStyle.Render("开启")
|
||||
} else {
|
||||
qVal = tOnStyle.Render("ON")
|
||||
}
|
||||
} else {
|
||||
if lang == "zh" {
|
||||
qVal = tOffStyle.Render("关闭")
|
||||
} else {
|
||||
qVal = tOffStyle.Render("OFF")
|
||||
}
|
||||
}
|
||||
s.WriteString(fmt.Sprintf("%s%s %s %s\n", cur, chk, nameStyle.Render(qName), qVal))
|
||||
}
|
||||
s.WriteString("\n")
|
||||
if lang == "zh" {
|
||||
s.WriteString(tHelpStyle.Render(" ↑/↓/j/k 移动 Enter 确认 Space 切换 数字 快速选择 q 退出"))
|
||||
} else {
|
||||
s.WriteString(tHelpStyle.Render(" Up/Down/j/k Move Enter Confirm Space Toggle Number Quick-Select q Quit"))
|
||||
}
|
||||
s.WriteString("\n")
|
||||
return s.String()
|
||||
@@ -766,11 +871,28 @@ func (m tuiModel) viewCustom() string {
|
||||
var s strings.Builder
|
||||
s.WriteString("\n")
|
||||
if lang == "zh" {
|
||||
s.WriteString(tTitleStyle.Render(" 高级自定义参数模式"))
|
||||
s.WriteString(tTitleStyle.Render(fmt.Sprintf(" VPS融合怪 %s — 高级自定义", m.config.EcsVersion)))
|
||||
} else {
|
||||
s.WriteString(tTitleStyle.Render(" Advanced Custom Parameter Mode"))
|
||||
s.WriteString(tTitleStyle.Render(fmt.Sprintf(" VPS Fusion Monster %s — Advanced Custom", m.config.EcsVersion)))
|
||||
}
|
||||
s.WriteString("\n\n")
|
||||
s.WriteString("\n")
|
||||
if m.preCheck.Connected && m.cmpVersion == -1 {
|
||||
if lang == "zh" {
|
||||
s.WriteString(tWarnStyle.Render(fmt.Sprintf(" ! 检测到新版本 %s 如有必要请更新", m.newVersion)))
|
||||
} else {
|
||||
s.WriteString(tWarnStyle.Render(fmt.Sprintf(" ! New version %s detected", m.newVersion)))
|
||||
}
|
||||
s.WriteString("\n")
|
||||
}
|
||||
if m.preCheck.Connected && m.hasStats {
|
||||
if lang == "zh" {
|
||||
s.WriteString(tInfoStyle.Render(fmt.Sprintf(" 总使用量: %s | 今日使用: %s", utils.FormatGoecsNumber(m.statsTotal), utils.FormatGoecsNumber(m.statsDaily))))
|
||||
} else {
|
||||
s.WriteString(tInfoStyle.Render(fmt.Sprintf(" Total Usage: %s | Daily Usage: %s", utils.FormatGoecsNumber(m.statsTotal), utils.FormatGoecsNumber(m.statsDaily))))
|
||||
}
|
||||
s.WriteString("\n")
|
||||
}
|
||||
s.WriteString("\n")
|
||||
if lang == "zh" {
|
||||
s.WriteString(tSectStyle.Render(" 测试开关 (空格切换, a 全选/全不选):"))
|
||||
} else {
|
||||
@@ -781,7 +903,7 @@ func (m tuiModel) viewCustom() string {
|
||||
cursor := " "
|
||||
style := tNormStyle
|
||||
if m.customCursor == i {
|
||||
cursor = " > "
|
||||
cursor = tCurStyle.Render(" > ")
|
||||
style = tSelStyle
|
||||
}
|
||||
if t.needNet && !m.preCheck.Connected {
|
||||
@@ -807,22 +929,51 @@ func (m tuiModel) viewCustom() string {
|
||||
for i, a := range m.advanced {
|
||||
idx := len(m.toggles) + i
|
||||
cursor := " "
|
||||
if m.customCursor == idx {
|
||||
cursor = " > "
|
||||
}
|
||||
style := tNormStyle
|
||||
if m.customCursor == idx {
|
||||
cursor = tCurStyle.Render(" > ")
|
||||
style = tSelStyle
|
||||
}
|
||||
name := a.nameEn
|
||||
if lang == "zh" {
|
||||
name = a.nameZh
|
||||
}
|
||||
value := m.advDisplayValue(a, lang)
|
||||
if a.kind == "option" {
|
||||
value = "< " + value + " >"
|
||||
var valueRendered string
|
||||
switch a.kind {
|
||||
case "bool":
|
||||
if a.boolVal {
|
||||
if lang == "zh" {
|
||||
valueRendered = tOnStyle.Render("开启")
|
||||
} else {
|
||||
valueRendered = tOnStyle.Render("ON")
|
||||
}
|
||||
} else {
|
||||
if lang == "zh" {
|
||||
valueRendered = tOffStyle.Render("关闭")
|
||||
} else {
|
||||
valueRendered = tOffStyle.Render("OFF")
|
||||
}
|
||||
}
|
||||
case "option":
|
||||
op := a.options[a.current]
|
||||
lbl := op.labelEn
|
||||
if lang == "zh" {
|
||||
lbl = op.labelZh
|
||||
}
|
||||
valueRendered = tDimStyle.Render("< ") + tValStyle.Render(lbl) + tDimStyle.Render(" >")
|
||||
case "text":
|
||||
v := strings.TrimSpace(a.textVal)
|
||||
if v == "" {
|
||||
if lang == "zh" {
|
||||
valueRendered = tDimStyle.Render("(默认)")
|
||||
} else {
|
||||
valueRendered = tDimStyle.Render("(default)")
|
||||
}
|
||||
} else {
|
||||
valueRendered = tValStyle.Render(v)
|
||||
}
|
||||
}
|
||||
s.WriteString(fmt.Sprintf("%s%-26s %s\n", cursor, style.Render(name+":"), tDimStyle.Render(value)))
|
||||
s.WriteString(fmt.Sprintf("%s%-26s %s\n", cursor, style.Render(name+":"), valueRendered))
|
||||
}
|
||||
|
||||
s.WriteString("\n")
|
||||
@@ -967,6 +1118,8 @@ func applyCustomResult(result tuiResult, preCheck utils.NetCheckResult, config *
|
||||
config.DiskTestPath = strings.TrimSpace(a.textVal)
|
||||
case "diskmc":
|
||||
config.DiskMultiCheck = a.boolVal
|
||||
case "autodiskm":
|
||||
config.AutoChangeDiskMethod = a.boolVal
|
||||
case "nt3loc":
|
||||
config.Nt3Location = a.options[a.current].value
|
||||
case "nt3t":
|
||||
@@ -995,5 +1148,4 @@ func applyCustomResult(result tuiResult, preCheck utils.NetCheckResult, config *
|
||||
if !config.BasicStatus && !config.CpuTestStatus && !config.MemoryTestStatus && !config.DiskTestStatus {
|
||||
config.OnlyIpInfoCheck = true
|
||||
}
|
||||
config.AutoChangeDiskMethod = true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user