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 | |
|---|---|---|---|
|
|
d2878dd705 | ||
|
|
a80ce40739 | ||
|
|
a1b5691179 | ||
|
|
6cf35680d5 | ||
|
|
7b74cbe1e7 | ||
|
|
8137a7ac6a |
@@ -102,6 +102,13 @@ func WithEnableUpload(enable bool) ConfigOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithAnalyzeResult 设置是否启用测试后结果总结分析
|
||||
func WithAnalyzeResult(enable bool) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.AnalyzeResult = enable
|
||||
}
|
||||
}
|
||||
|
||||
// WithEnableLogger 设置是否启用日志
|
||||
func WithEnableLogger(enable bool) ConfigOption {
|
||||
return func(c *Config) {
|
||||
|
||||
@@ -28,12 +28,12 @@ func RunAllTests(preCheck utils.NetCheckResult, config *Config) *RunResult {
|
||||
outputMutex sync.Mutex
|
||||
infoMutex sync.Mutex
|
||||
)
|
||||
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
|
||||
switch config.Language {
|
||||
case "zh":
|
||||
runner.RunChineseTests(preCheck, config, &wg1, &wg2, &wg3,
|
||||
runner.RunChineseTests(preCheck, config, &wg1, &wg2, &wg3,
|
||||
&basicInfo, &securityInfo, &emailInfo, &mediaInfo, &ptInfo,
|
||||
&output, tempOutput, startTime, &outputMutex, &infoMutex)
|
||||
case "en":
|
||||
@@ -45,9 +45,12 @@ func RunAllTests(preCheck utils.NetCheckResult, config *Config) *RunResult {
|
||||
&basicInfo, &securityInfo, &emailInfo, &mediaInfo, &ptInfo,
|
||||
&output, tempOutput, startTime, &outputMutex, &infoMutex)
|
||||
}
|
||||
|
||||
if config.AnalyzeResult {
|
||||
output = runner.AppendAnalysisSummary(config, output, tempOutput, &outputMutex)
|
||||
}
|
||||
|
||||
endTime := time.Now()
|
||||
|
||||
|
||||
return &RunResult{
|
||||
Output: output,
|
||||
Duration: endTime.Sub(startTime),
|
||||
|
||||
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=
|
||||
|
||||
5
goecs.go
5
goecs.go
@@ -27,7 +27,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ecsVersion = "v0.1.118" // 融合怪版本号
|
||||
ecsVersion = "v0.1.120" // 融合怪版本号
|
||||
configs = params.NewConfig(ecsVersion) // 全局配置实例
|
||||
userSetFlags = make(map[string]bool) // 用于跟踪哪些参数是用户显式设置的
|
||||
)
|
||||
@@ -99,6 +99,9 @@ func main() {
|
||||
default:
|
||||
fmt.Println("Unsupported language")
|
||||
}
|
||||
if configs.AnalyzeResult {
|
||||
output = runner.AppendAnalysisSummary(configs, output, tempOutput, &outputMutex)
|
||||
}
|
||||
if preCheck.Connected {
|
||||
runner.HandleUploadResults(configs, output)
|
||||
}
|
||||
|
||||
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.117"
|
||||
ECS_VERSION="0.1.119"
|
||||
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.117"
|
||||
ECS_VERSION="0.1.117"
|
||||
_yellow "Unable to get version info, using default version 0.1.119"
|
||||
ECS_VERSION="0.1.119"
|
||||
fi
|
||||
version_output=""
|
||||
for cmd_path in "goecs" "./goecs" "/usr/bin/goecs" "/usr/local/bin/goecs"; do
|
||||
|
||||
652
internal/analysis/summary.go
Normal file
652
internal/analysis/summary.go
Normal file
@@ -0,0 +1,652 @@
|
||||
package analysis
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
"github.com/oneclickvirt/ecs/internal/params"
|
||||
)
|
||||
|
||||
var (
|
||||
mbpsRe = regexp.MustCompile(`(?i)(\d+(?:\.\d+)?)\s*mbps`)
|
||||
msRe = regexp.MustCompile(`(?i)(\d+(?:\.\d+)?)\s*ms`)
|
||||
|
||||
cpuModelZhRe = regexp.MustCompile(`(?im)^\s*CPU\s*型号\s*[::]\s*(.+?)\s*$`)
|
||||
cpuModelEnRe = regexp.MustCompile(`(?im)^\s*CPU\s*Model\s*[::]\s*(.+?)\s*$`)
|
||||
|
||||
threadScoreEnRe = regexp.MustCompile(`(?im)^\s*(\d+)\s*Thread\(s\)\s*Test\s*:\s*([0-9][0-9,]*(?:\.[0-9]+)?)\s*$`)
|
||||
threadScoreZhRe = regexp.MustCompile(`(?im)^\s*(\d+)\s*线程测试\((?:单核|多核)\)得分\s*[::]\s*([0-9][0-9,]*(?:\.[0-9]+)?)\s*$`)
|
||||
gbSingleRe = regexp.MustCompile(`(?im)^\s*Single-Core\s*Score\s*[::]\s*([0-9][0-9,]*(?:\.[0-9]+)?)\s*$`)
|
||||
gbMultiRe = regexp.MustCompile(`(?im)^\s*Multi-Core\s*Score\s*[::]\s*([0-9][0-9,]*(?:\.[0-9]+)?)\s*$`)
|
||||
|
||||
alphaNumRe = regexp.MustCompile(`[a-z0-9]+`)
|
||||
)
|
||||
|
||||
const (
|
||||
cpuStatsPrimaryURL = "https://raw.githubusercontent.com/oneclickvirt/ecs/ranks/cpu_statistics.json"
|
||||
cpuStatsFallbackURL = "https://github.com/oneclickvirt/ecs/raw/refs/heads/ranks/cpu_statistics.json"
|
||||
cpuCDNProbeTestURL = "https://raw.githubusercontent.com/spiritLHLS/ecs/main/back/test"
|
||||
cpuStatsCacheTTL = 30 * time.Minute
|
||||
cpuStatsFailCacheTTL = 5 * time.Minute
|
||||
cpuStatsRequestTimout = 6 * time.Second
|
||||
)
|
||||
|
||||
var cpuStatsCDNList = []string{
|
||||
"https://cdn.spiritlhl.net/",
|
||||
"http://cdn3.spiritlhl.net/",
|
||||
"http://cdn1.spiritlhl.net/",
|
||||
"http://cdn2.spiritlhl.net/",
|
||||
}
|
||||
|
||||
type cpuStatsEntry struct {
|
||||
CPUPrefix string `json:"cpu_prefix"`
|
||||
CPUModel string `json:"cpu_model"`
|
||||
SampleCount int `json:"sample_count"`
|
||||
MaxSingle float64 `json:"max_single_score"`
|
||||
MaxMulti float64 `json:"max_multi_score"`
|
||||
AvgSingle float64 `json:"avg_single_score"`
|
||||
AvgMulti float64 `json:"avg_multi_score"`
|
||||
Rank int `json:"rank"`
|
||||
TypicalCores int `json:"typical_cores"`
|
||||
TypicalThread int `json:"typical_threads"`
|
||||
}
|
||||
|
||||
type cpuStatsPayload struct {
|
||||
CPUStatistics []cpuStatsEntry `json:"cpu_statistics"`
|
||||
}
|
||||
|
||||
var (
|
||||
cpuStatsMu sync.Mutex
|
||||
cachedCPUStats *cpuStatsPayload
|
||||
cpuStatsExpireAt time.Time
|
||||
)
|
||||
|
||||
func parseFloatsByRegex(content string, re *regexp.Regexp) []float64 {
|
||||
matches := re.FindAllStringSubmatch(content, -1)
|
||||
vals := make([]float64, 0, len(matches))
|
||||
for _, m := range matches {
|
||||
if len(m) < 2 {
|
||||
continue
|
||||
}
|
||||
v, err := strconv.ParseFloat(m[1], 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
vals = append(vals, v)
|
||||
}
|
||||
return vals
|
||||
}
|
||||
|
||||
func parseFloatString(s string) (float64, bool) {
|
||||
clean := strings.ReplaceAll(strings.TrimSpace(s), ",", "")
|
||||
v, err := strconv.ParseFloat(clean, 64)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return v, true
|
||||
}
|
||||
|
||||
func extractCPUModel(output string) string {
|
||||
for _, re := range []*regexp.Regexp{cpuModelZhRe, cpuModelEnRe} {
|
||||
m := re.FindStringSubmatch(output)
|
||||
if len(m) >= 2 {
|
||||
model := strings.TrimSpace(m[1])
|
||||
if model != "" {
|
||||
return model
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractCPUScores(output string) (single float64, singleOK bool, multi float64, multiOK bool) {
|
||||
for _, re := range []*regexp.Regexp{threadScoreEnRe, threadScoreZhRe} {
|
||||
matches := re.FindAllStringSubmatch(output, -1)
|
||||
for _, m := range matches {
|
||||
if len(m) < 3 {
|
||||
continue
|
||||
}
|
||||
threads, err := strconv.Atoi(strings.TrimSpace(m[1]))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
score, ok := parseFloatString(m[2])
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if threads == 1 {
|
||||
single, singleOK = score, true
|
||||
continue
|
||||
}
|
||||
if threads > 1 && (!multiOK || score > multi) {
|
||||
multi, multiOK = score, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !singleOK {
|
||||
if m := gbSingleRe.FindStringSubmatch(output); len(m) >= 2 {
|
||||
if v, ok := parseFloatString(m[1]); ok {
|
||||
single, singleOK = v, true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !multiOK {
|
||||
if m := gbMultiRe.FindStringSubmatch(output); len(m) >= 2 {
|
||||
if v, ok := parseFloatString(m[1]); ok {
|
||||
multi, multiOK = v, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func normalizeCPUString(s string) string {
|
||||
s = strings.ToLower(s)
|
||||
b := strings.Builder{}
|
||||
b.Grow(len(s))
|
||||
for _, r := range s {
|
||||
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func cpuTokens(s string) []string {
|
||||
lower := strings.ToLower(s)
|
||||
raw := alphaNumRe.FindAllString(lower, -1)
|
||||
if len(raw) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[string]struct{}, len(raw))
|
||||
out := make([]string, 0, len(raw))
|
||||
for _, t := range raw {
|
||||
if len(t) < 2 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[t]; ok {
|
||||
continue
|
||||
}
|
||||
seen[t] = struct{}{}
|
||||
out = append(out, t)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func fuzzyScoreCPUModel(model string, entry cpuStatsEntry) float64 {
|
||||
nm := normalizeCPUString(model)
|
||||
ne := normalizeCPUString(entry.CPUModel)
|
||||
np := normalizeCPUString(entry.CPUPrefix)
|
||||
if nm == "" || (ne == "" && np == "") {
|
||||
return 0
|
||||
}
|
||||
|
||||
if nm == ne || nm == np {
|
||||
return 1
|
||||
}
|
||||
|
||||
containsScore := 0.0
|
||||
for _, candidate := range []string{ne, np} {
|
||||
if candidate == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(candidate, nm) || strings.Contains(nm, candidate) {
|
||||
shortLen := len(nm)
|
||||
if len(candidate) < shortLen {
|
||||
shortLen = len(candidate)
|
||||
}
|
||||
longLen := len(nm)
|
||||
if len(candidate) > longLen {
|
||||
longLen = len(candidate)
|
||||
}
|
||||
if longLen > 0 {
|
||||
ratio := float64(shortLen) / float64(longLen)
|
||||
if ratio > containsScore {
|
||||
containsScore = ratio
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modelTokens := cpuTokens(model)
|
||||
if len(modelTokens) == 0 {
|
||||
return containsScore
|
||||
}
|
||||
|
||||
entryTokenSet := make(map[string]struct{})
|
||||
for _, t := range cpuTokens(entry.CPUModel + " " + entry.CPUPrefix) {
|
||||
entryTokenSet[t] = struct{}{}
|
||||
}
|
||||
overlap := 0
|
||||
for _, t := range modelTokens {
|
||||
if _, ok := entryTokenSet[t]; ok {
|
||||
overlap++
|
||||
}
|
||||
}
|
||||
overlapScore := float64(overlap) / float64(len(modelTokens))
|
||||
|
||||
if containsScore > overlapScore {
|
||||
return containsScore
|
||||
}
|
||||
return overlapScore
|
||||
}
|
||||
|
||||
func loadCPUStats() *cpuStatsPayload {
|
||||
cpuStatsMu.Lock()
|
||||
defer cpuStatsMu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
if now.Before(cpuStatsExpireAt) {
|
||||
return cachedCPUStats
|
||||
}
|
||||
|
||||
client := req.C()
|
||||
client.SetTimeout(cpuStatsRequestTimout)
|
||||
endpoints := []string{cpuStatsPrimaryURL, cpuStatsFallbackURL}
|
||||
|
||||
availableCDN := detectAvailableCPUCDN(client)
|
||||
for _, endpoint := range endpoints {
|
||||
urls := []string{}
|
||||
if availableCDN != "" {
|
||||
urls = append(urls, availableCDN+endpoint)
|
||||
}
|
||||
urls = append(urls, endpoint)
|
||||
|
||||
for _, u := range urls {
|
||||
payload := tryDecodeCPUStatsFromURL(client, u)
|
||||
if payload == nil {
|
||||
continue
|
||||
}
|
||||
cachedCPUStats = payload
|
||||
cpuStatsExpireAt = now.Add(cpuStatsCacheTTL)
|
||||
return cachedCPUStats
|
||||
}
|
||||
}
|
||||
|
||||
cachedCPUStats = nil
|
||||
cpuStatsExpireAt = now.Add(cpuStatsFailCacheTTL)
|
||||
return nil
|
||||
}
|
||||
|
||||
func detectAvailableCPUCDN(client *req.Client) string {
|
||||
for _, baseURL := range cpuStatsCDNList {
|
||||
if checkCPUCDN(client, baseURL) {
|
||||
return baseURL
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func checkCPUCDN(client *req.Client, baseURL string) bool {
|
||||
resp, err := client.R().SetHeader("User-Agent", "goecs-summary/1.0").Get(baseURL + cpuCDNProbeTestURL)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return false
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(io.LimitReader(resp.Body, 4<<10))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(string(b), "success")
|
||||
}
|
||||
|
||||
func tryDecodeCPUStatsFromURL(client *req.Client, u string) *cpuStatsPayload {
|
||||
resp, err := client.R().SetHeader("User-Agent", "goecs-summary/1.0").Get(u)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var payload cpuStatsPayload
|
||||
dec := json.NewDecoder(io.LimitReader(resp.Body, 8<<20))
|
||||
if err := dec.Decode(&payload); err != nil {
|
||||
return nil
|
||||
}
|
||||
if len(payload.CPUStatistics) == 0 {
|
||||
return nil
|
||||
}
|
||||
return &payload
|
||||
}
|
||||
|
||||
func matchCPUStatsEntry(model string, payload *cpuStatsPayload) *cpuStatsEntry {
|
||||
if payload == nil || model == "" || len(payload.CPUStatistics) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
trimModel := strings.TrimSpace(model)
|
||||
normModel := normalizeCPUString(trimModel)
|
||||
|
||||
for i := range payload.CPUStatistics {
|
||||
entry := &payload.CPUStatistics[i]
|
||||
if strings.EqualFold(strings.TrimSpace(entry.CPUModel), trimModel) {
|
||||
return entry
|
||||
}
|
||||
}
|
||||
|
||||
for i := range payload.CPUStatistics {
|
||||
entry := &payload.CPUStatistics[i]
|
||||
if normModel == normalizeCPUString(entry.CPUModel) || normModel == normalizeCPUString(entry.CPUPrefix) {
|
||||
return entry
|
||||
}
|
||||
}
|
||||
|
||||
bestIdx := -1
|
||||
bestScore := 0.0
|
||||
for i := range payload.CPUStatistics {
|
||||
score := fuzzyScoreCPUModel(trimModel, payload.CPUStatistics[i])
|
||||
if score > bestScore {
|
||||
bestScore = score
|
||||
bestIdx = i
|
||||
continue
|
||||
}
|
||||
if score == bestScore && bestIdx >= 0 && payload.CPUStatistics[i].SampleCount > payload.CPUStatistics[bestIdx].SampleCount {
|
||||
bestIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
if bestIdx >= 0 && bestScore >= 0.45 {
|
||||
return &payload.CPUStatistics[bestIdx]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cpuTierText(score float64, lang string) string {
|
||||
if lang == "zh" {
|
||||
switch {
|
||||
case score >= 5000:
|
||||
return "按 README_NEW_USER 的 Sysbench 口径,单核 >5000 可视为高性能第一梯队。"
|
||||
case score < 500:
|
||||
return "按 README_NEW_USER 的 Sysbench 口径,单核 <500 属于偏弱性能。"
|
||||
default:
|
||||
return "按 README_NEW_USER 的 Sysbench 口径,可按每约 1000 分视作一个性能档位。"
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case score >= 5000:
|
||||
return "Per README_NEW_USER Sysbench guidance, single-core > 5000 is considered first-tier high performance."
|
||||
case score < 500:
|
||||
return "Per README_NEW_USER Sysbench guidance, single-core < 500 is considered weak performance."
|
||||
default:
|
||||
return "Per README_NEW_USER Sysbench guidance, roughly every 1000 points is about one performance tier."
|
||||
}
|
||||
}
|
||||
|
||||
func summarizeCPUWithRanking(finalOutput, lang string) []string {
|
||||
model := extractCPUModel(finalOutput)
|
||||
single, singleOK, multi, multiOK := extractCPUScores(finalOutput)
|
||||
if !singleOK && !multiOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
stats := loadCPUStats()
|
||||
entry := matchCPUStatsEntry(model, stats)
|
||||
|
||||
var score float64
|
||||
var avg float64
|
||||
var max float64
|
||||
kind := "single"
|
||||
|
||||
if singleOK && entry != nil && entry.AvgSingle > 0 && entry.MaxSingle > 0 {
|
||||
score, avg, max = single, entry.AvgSingle, entry.MaxSingle
|
||||
} else if multiOK && entry != nil && entry.AvgMulti > 0 && entry.MaxMulti > 0 {
|
||||
score, avg, max = multi, entry.AvgMulti, entry.MaxMulti
|
||||
kind = "multi"
|
||||
} else if singleOK {
|
||||
score = single
|
||||
} else {
|
||||
score = multi
|
||||
kind = "multi"
|
||||
}
|
||||
|
||||
lines := make([]string, 0, 4)
|
||||
if lang == "zh" {
|
||||
if kind == "single" {
|
||||
lines = append(lines, fmt.Sprintf("CPU: 检测到单核得分 %.2f。", score))
|
||||
} else {
|
||||
lines = append(lines, fmt.Sprintf("CPU: 检测到多核得分 %.2f。", score))
|
||||
}
|
||||
} else {
|
||||
if kind == "single" {
|
||||
lines = append(lines, fmt.Sprintf("CPU: detected single-core score %.2f.", score))
|
||||
} else {
|
||||
lines = append(lines, fmt.Sprintf("CPU: detected multi-core score %.2f.", score))
|
||||
}
|
||||
}
|
||||
|
||||
if kind == "single" {
|
||||
lines = append(lines, cpuTierText(score, lang))
|
||||
}
|
||||
|
||||
if entry == nil || avg <= 0 || max <= 0 {
|
||||
if lang == "zh" {
|
||||
if model != "" {
|
||||
lines = append(lines, fmt.Sprintf("CPU 对标: 未在在线榜单中稳定匹配到型号 \"%s\",已仅给出本机分数解读。", model))
|
||||
} else {
|
||||
lines = append(lines, "CPU 对标: 未提取到 CPU 型号,已仅给出本机分数解读。")
|
||||
}
|
||||
} else {
|
||||
if model != "" {
|
||||
lines = append(lines, fmt.Sprintf("CPU ranking: no reliable online match found for model \"%s\"; local score interpretation only.", model))
|
||||
} else {
|
||||
lines = append(lines, "CPU ranking: CPU model not found in output; local score interpretation only.")
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
reachAvg := score >= avg
|
||||
gapToMax := max - score
|
||||
fullBlood := false
|
||||
if max > 0 {
|
||||
ratioDiff := (score - max) / max
|
||||
if ratioDiff < 0 {
|
||||
ratioDiff = -ratioDiff
|
||||
}
|
||||
fullBlood = ratioDiff <= 0.05
|
||||
}
|
||||
pctOfAvg := score / avg * 100
|
||||
pctOfMax := score / max * 100
|
||||
|
||||
if lang == "zh" {
|
||||
lines = append(lines,
|
||||
fmt.Sprintf("CPU 对标: 匹配 \"%s\"(样本 %d,排名 #%d)。", entry.CPUModel, entry.SampleCount, entry.Rank),
|
||||
fmt.Sprintf("平均分达标: %s(本机 %.2f,均值 %.2f,达成率 %.2f%%)。", map[bool]string{true: "是", false: "否"}[reachAvg], score, avg, pctOfAvg),
|
||||
fmt.Sprintf("满血对比: 满血分 %.2f,本机为 %.2f%%,差值 %.2f。", max, pctOfMax, gapToMax),
|
||||
fmt.Sprintf("满血判定(±5%%波动): %s。", map[bool]string{true: "是", false: "否"}[fullBlood]),
|
||||
)
|
||||
} else {
|
||||
lines = append(lines,
|
||||
fmt.Sprintf("CPU ranking: matched \"%s\" (samples %d, rank #%d).", entry.CPUModel, entry.SampleCount, entry.Rank),
|
||||
fmt.Sprintf("Average-level check: %s (local %.2f vs avg %.2f, %.2f%% of avg).", map[bool]string{true: "pass", false: "below avg"}[reachAvg], score, avg, pctOfAvg),
|
||||
fmt.Sprintf("Full-blood comparison: max %.2f, local is %.2f%% of max, gap %.2f.", max, pctOfMax, gapToMax),
|
||||
fmt.Sprintf("Full-blood status (within ±5%%): %s.", map[bool]string{true: "yes", false: "no"}[fullBlood]),
|
||||
)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func summarizeBandwidth(vals []float64, lang string) string {
|
||||
if len(vals) == 0 {
|
||||
if lang == "zh" {
|
||||
return "测速: 未检测到有效 Mbps 数据。"
|
||||
}
|
||||
return "Speed: no valid Mbps values found."
|
||||
}
|
||||
sort.Float64s(vals)
|
||||
maxV := vals[len(vals)-1]
|
||||
if lang == "zh" {
|
||||
switch {
|
||||
case maxV >= 2000:
|
||||
return fmt.Sprintf("测速: 峰值约 %.2f Mbps,属于高带宽网络。", maxV)
|
||||
case maxV >= 800:
|
||||
return fmt.Sprintf("测速: 峰值约 %.2f Mbps,带宽表现较好。", maxV)
|
||||
case maxV >= 200:
|
||||
return fmt.Sprintf("测速: 峰值约 %.2f Mbps,带宽中等可用。", maxV)
|
||||
default:
|
||||
return fmt.Sprintf("测速: 峰值约 %.2f Mbps,带宽偏低,建议关注线路与机型。", maxV)
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case maxV >= 2000:
|
||||
return fmt.Sprintf("Speed: peak around %.2f Mbps, high-bandwidth profile.", maxV)
|
||||
case maxV >= 800:
|
||||
return fmt.Sprintf("Speed: peak around %.2f Mbps, strong bandwidth performance.", maxV)
|
||||
case maxV >= 200:
|
||||
return fmt.Sprintf("Speed: peak around %.2f Mbps, moderate and usable bandwidth.", maxV)
|
||||
default:
|
||||
return fmt.Sprintf("Speed: peak around %.2f Mbps, relatively limited bandwidth.", maxV)
|
||||
}
|
||||
}
|
||||
|
||||
func summarizeLatency(vals []float64, lang string) string {
|
||||
if len(vals) == 0 {
|
||||
if lang == "zh" {
|
||||
return "延迟: 未检测到有效 ms 数据。"
|
||||
}
|
||||
return "Latency: no valid ms values found."
|
||||
}
|
||||
sort.Float64s(vals)
|
||||
minV := vals[0]
|
||||
if lang == "zh" {
|
||||
switch {
|
||||
case minV <= 15:
|
||||
return fmt.Sprintf("延迟: 最优约 %.2f ms,实时交互体验优秀。", minV)
|
||||
case minV <= 45:
|
||||
return fmt.Sprintf("延迟: 最优约 %.2f ms,整体交互体验良好。", minV)
|
||||
case minV <= 90:
|
||||
return fmt.Sprintf("延迟: 最优约 %.2f ms,可用但有一定时延。", minV)
|
||||
default:
|
||||
return fmt.Sprintf("延迟: 最优约 %.2f ms,时延偏高,建议优化线路。", minV)
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case minV <= 15:
|
||||
return fmt.Sprintf("Latency: best around %.2f ms, excellent for interactive workloads.", minV)
|
||||
case minV <= 45:
|
||||
return fmt.Sprintf("Latency: best around %.2f ms, generally responsive.", minV)
|
||||
case minV <= 90:
|
||||
return fmt.Sprintf("Latency: best around %.2f ms, usable with moderate delay.", minV)
|
||||
default:
|
||||
return fmt.Sprintf("Latency: best around %.2f ms, relatively high and may impact responsiveness.", minV)
|
||||
}
|
||||
}
|
||||
|
||||
func testedScopes(config *params.Config) []string {
|
||||
scopes := make([]string, 0, 8)
|
||||
if config.BasicStatus {
|
||||
scopes = append(scopes, "basic")
|
||||
}
|
||||
if config.CpuTestStatus {
|
||||
scopes = append(scopes, "cpu")
|
||||
}
|
||||
if config.MemoryTestStatus {
|
||||
scopes = append(scopes, "memory")
|
||||
}
|
||||
if config.DiskTestStatus {
|
||||
scopes = append(scopes, "disk")
|
||||
}
|
||||
if config.UtTestStatus {
|
||||
scopes = append(scopes, "unlock")
|
||||
}
|
||||
if config.SecurityTestStatus {
|
||||
scopes = append(scopes, "security")
|
||||
}
|
||||
if config.Nt3Status || config.BacktraceStatus || config.PingTestStatus || config.TgdcTestStatus || config.WebTestStatus {
|
||||
scopes = append(scopes, "network")
|
||||
}
|
||||
if config.SpeedTestStatus {
|
||||
scopes = append(scopes, "speed")
|
||||
}
|
||||
return scopes
|
||||
}
|
||||
|
||||
func scopesText(scopes []string, lang string) string {
|
||||
if len(scopes) == 0 {
|
||||
if lang == "zh" {
|
||||
return "无"
|
||||
}
|
||||
return "none"
|
||||
}
|
||||
labelsZh := map[string]string{
|
||||
"basic": "系统基础", "cpu": "CPU", "memory": "内存", "disk": "磁盘", "unlock": "解锁", "security": "IP质量", "network": "网络路由", "speed": "带宽测速",
|
||||
}
|
||||
labelsEn := map[string]string{
|
||||
"basic": "system basics", "cpu": "CPU", "memory": "memory", "disk": "disk", "unlock": "unlock", "security": "IP quality", "network": "network route", "speed": "bandwidth",
|
||||
}
|
||||
out := make([]string, 0, len(scopes))
|
||||
for _, s := range scopes {
|
||||
if lang == "zh" {
|
||||
out = append(out, labelsZh[s])
|
||||
} else {
|
||||
out = append(out, labelsEn[s])
|
||||
}
|
||||
}
|
||||
return strings.Join(out, ", ")
|
||||
}
|
||||
|
||||
// GenerateSummary creates a concise 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)
|
||||
|
||||
if lang == "zh" {
|
||||
lines := []string{
|
||||
"测试结果总结:",
|
||||
fmt.Sprintf("- 本次覆盖: %s", scopesText(scopes, lang)),
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
lines := []string{
|
||||
"Test Summary:",
|
||||
fmt.Sprintf("- Scope covered: %s", scopesText(scopes, lang)),
|
||||
}
|
||||
for _, line := range cpuLines {
|
||||
lines = append(lines, "- "+line)
|
||||
}
|
||||
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.")
|
||||
}
|
||||
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.")
|
||||
}
|
||||
lines = append(lines, "- Suggestion: prioritize the metrics that match your workload (compute, storage, or cross-region networking).")
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
@@ -184,6 +184,7 @@ func HandleMenuMode(preCheck utils.NetCheckResult, config *params.Config) {
|
||||
config.Language = result.language
|
||||
|
||||
if result.custom {
|
||||
config.Choice = "custom"
|
||||
applyCustomResult(result, preCheck, config)
|
||||
if config.SpeedTestStatus {
|
||||
config.OnlyChinaTest = utils.CheckChina(config.EnableLogger, config.Language)
|
||||
@@ -216,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)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,7 @@ type Config struct {
|
||||
AutoChangeDiskMethod bool
|
||||
FilePath string
|
||||
EnableUpload bool
|
||||
AnalyzeResult bool
|
||||
OnlyIpInfoCheck bool
|
||||
Help bool
|
||||
Finish bool
|
||||
@@ -85,6 +86,7 @@ func NewConfig(version string) *Config {
|
||||
AutoChangeDiskMethod: true,
|
||||
FilePath: "goecs.txt",
|
||||
EnableUpload: true,
|
||||
AnalyzeResult: false,
|
||||
OnlyIpInfoCheck: false,
|
||||
Help: false,
|
||||
Finish: false,
|
||||
@@ -106,6 +108,7 @@ func normalizeBoolArgs(args []string) []string {
|
||||
"disk": true, "ut": true, "security": true, "email": true,
|
||||
"backtrace": true, "nt3": true, "speed": true, "ping": true,
|
||||
"tgdc": true, "web": true, "log": true, "upload": true,
|
||||
"analysis": true, "analyze": true,
|
||||
"diskmc": true,
|
||||
}
|
||||
|
||||
@@ -180,6 +183,8 @@ func (c *Config) ParseFlags(args []string) {
|
||||
c.GoecsFlag.IntVar(&c.SpNum, "spnum", 2, "Set the number of servers per operator for speed test")
|
||||
c.GoecsFlag.BoolVar(&c.EnableLogger, "log", false, "Enable/Disable logging in the current path")
|
||||
c.GoecsFlag.BoolVar(&c.EnableUpload, "upload", true, "Enable/Disable upload the result")
|
||||
c.GoecsFlag.BoolVar(&c.AnalyzeResult, "analysis", false, "Enable/Disable post-test concise summary analysis")
|
||||
c.GoecsFlag.BoolVar(&c.AnalyzeResult, "analyze", false, "Enable/Disable post-test concise summary analysis")
|
||||
c.GoecsFlag.Parse(args)
|
||||
|
||||
c.GoecsFlag.Visit(func(f *flag.Flag) {
|
||||
@@ -271,6 +276,9 @@ func (c *Config) SaveUserSetParams() map[string]interface{} {
|
||||
if c.UserSetFlags["spnum"] {
|
||||
saved["spnum"] = c.SpNum
|
||||
}
|
||||
if c.UserSetFlags["analysis"] || c.UserSetFlags["analyze"] {
|
||||
saved["analysis"] = c.AnalyzeResult
|
||||
}
|
||||
|
||||
return saved
|
||||
}
|
||||
@@ -389,6 +397,11 @@ func (c *Config) RestoreUserSetParams(saved map[string]interface{}) {
|
||||
c.SpNum = intVal
|
||||
}
|
||||
}
|
||||
if val, ok := saved["analysis"]; ok {
|
||||
if boolVal, ok := val.(bool); ok {
|
||||
c.AnalyzeResult = boolVal
|
||||
}
|
||||
}
|
||||
|
||||
c.ValidateParams()
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/oneclickvirt/ecs/internal/analysis"
|
||||
"github.com/oneclickvirt/ecs/internal/params"
|
||||
"github.com/oneclickvirt/ecs/internal/tests"
|
||||
"github.com/oneclickvirt/ecs/utils"
|
||||
@@ -153,7 +154,7 @@ func RunBasicTests(preCheck utils.NetCheckResult, config *params.Config, basicIn
|
||||
}
|
||||
if config.BasicStatus {
|
||||
fmt.Printf("%s", *basicInfo)
|
||||
} else if (config.Input == "6" || config.Input == "9") && config.SecurityTestStatus {
|
||||
} else if (config.Choice == "6" || config.Choice == "9") && config.SecurityTestStatus {
|
||||
scanner := bufio.NewScanner(strings.NewReader(*basicInfo))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
@@ -363,6 +364,12 @@ func RunSpeedTests(config *params.Config, output, tempOutput string, outputMutex
|
||||
}
|
||||
} else if config.Choice == "6" {
|
||||
tests.CustomSP("net", "global", 11, config.Language)
|
||||
} else {
|
||||
// Custom menu mode and any other fallback choices.
|
||||
tests.NearbySP()
|
||||
tests.CustomSP("net", "cu", config.SpNum, config.Language)
|
||||
tests.CustomSP("net", "ct", config.SpNum, config.Language)
|
||||
tests.CustomSP("net", "cmcc", config.SpNum, config.Language)
|
||||
}
|
||||
// 等待第三方库的输出完全刷新到标准输出
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
@@ -427,6 +434,25 @@ func AppendTimeInfo(config *params.Config, output, tempOutput string, startTime
|
||||
}, tempOutput, output)
|
||||
}
|
||||
|
||||
// AppendAnalysisSummary appends a concise bilingual summary for easier interpretation.
|
||||
func AppendAnalysisSummary(config *params.Config, output, tempOutput string, outputMutex *sync.Mutex) string {
|
||||
outputMutex.Lock()
|
||||
defer outputMutex.Unlock()
|
||||
finalOutput := output
|
||||
return utils.PrintAndCapture(func() {
|
||||
summary := analysis.GenerateSummary(config, finalOutput)
|
||||
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)
|
||||
}
|
||||
|
||||
// HandleSignalInterrupt handles interrupt signals
|
||||
func HandleSignalInterrupt(sig chan os.Signal, config *params.Config, startTime *time.Time, output *string, tempOutput string, uploadDone chan bool, outputMutex *sync.Mutex) {
|
||||
select {
|
||||
|
||||
Reference in New Issue
Block a user