fix: 增加参数设置和自动化评价总结功能

This commit is contained in:
spiritlhl
2026-04-16 21:51:37 +08:00
parent 8137a7ac6a
commit 7b74cbe1e7
8 changed files with 1187 additions and 245 deletions

View File

@@ -102,6 +102,13 @@ func WithEnableUpload(enable bool) ConfigOption {
} }
} }
// WithAnalyzeResult 设置是否启用测试后结果总结分析
func WithAnalyzeResult(enable bool) ConfigOption {
return func(c *Config) {
c.AnalyzeResult = enable
}
}
// WithEnableLogger 设置是否启用日志 // WithEnableLogger 设置是否启用日志
func WithEnableLogger(enable bool) ConfigOption { func WithEnableLogger(enable bool) ConfigOption {
return func(c *Config) { return func(c *Config) {

View File

@@ -28,12 +28,12 @@ func RunAllTests(preCheck utils.NetCheckResult, config *Config) *RunResult {
outputMutex sync.Mutex outputMutex sync.Mutex
infoMutex sync.Mutex infoMutex sync.Mutex
) )
startTime := time.Now() startTime := time.Now()
switch config.Language { switch config.Language {
case "zh": case "zh":
runner.RunChineseTests(preCheck, config, &wg1, &wg2, &wg3, runner.RunChineseTests(preCheck, config, &wg1, &wg2, &wg3,
&basicInfo, &securityInfo, &emailInfo, &mediaInfo, &ptInfo, &basicInfo, &securityInfo, &emailInfo, &mediaInfo, &ptInfo,
&output, tempOutput, startTime, &outputMutex, &infoMutex) &output, tempOutput, startTime, &outputMutex, &infoMutex)
case "en": case "en":
@@ -45,9 +45,12 @@ func RunAllTests(preCheck utils.NetCheckResult, config *Config) *RunResult {
&basicInfo, &securityInfo, &emailInfo, &mediaInfo, &ptInfo, &basicInfo, &securityInfo, &emailInfo, &mediaInfo, &ptInfo,
&output, tempOutput, startTime, &outputMutex, &infoMutex) &output, tempOutput, startTime, &outputMutex, &infoMutex)
} }
if config.AnalyzeResult {
output = runner.AppendAnalysisSummary(config, output, tempOutput, &outputMutex)
}
endTime := time.Now() endTime := time.Now()
return &RunResult{ return &RunResult{
Output: output, Output: output,
Duration: endTime.Sub(startTime), Duration: endTime.Sub(startTime),

View File

@@ -27,7 +27,7 @@ import (
) )
var ( var (
ecsVersion = "v0.1.118" // 融合怪版本号 ecsVersion = "v0.1.119" // 融合怪版本号
configs = params.NewConfig(ecsVersion) // 全局配置实例 configs = params.NewConfig(ecsVersion) // 全局配置实例
userSetFlags = make(map[string]bool) // 用于跟踪哪些参数是用户显式设置的 userSetFlags = make(map[string]bool) // 用于跟踪哪些参数是用户显式设置的
) )
@@ -99,6 +99,9 @@ func main() {
default: default:
fmt.Println("Unsupported language") fmt.Println("Unsupported language")
} }
if configs.AnalyzeResult {
output = runner.AppendAnalysisSummary(configs, output, tempOutput, &outputMutex)
}
if preCheck.Connected { if preCheck.Connected {
runner.HandleUploadResults(configs, output) runner.HandleUploadResults(configs, output)
} }

View 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")
}

View File

@@ -184,6 +184,7 @@ func HandleMenuMode(preCheck utils.NetCheckResult, config *params.Config) {
config.Language = result.language config.Language = result.language
if result.custom { if result.custom {
config.Choice = "custom"
applyCustomResult(result, preCheck, config) applyCustomResult(result, preCheck, config)
if config.SpeedTestStatus { if config.SpeedTestStatus {
config.OnlyChinaTest = utils.CheckChina(config.EnableLogger, config.Language) config.OnlyChinaTest = utils.CheckChina(config.EnableLogger, config.Language)

File diff suppressed because it is too large Load Diff

View File

@@ -42,6 +42,7 @@ type Config struct {
AutoChangeDiskMethod bool AutoChangeDiskMethod bool
FilePath string FilePath string
EnableUpload bool EnableUpload bool
AnalyzeResult bool
OnlyIpInfoCheck bool OnlyIpInfoCheck bool
Help bool Help bool
Finish bool Finish bool
@@ -85,6 +86,7 @@ func NewConfig(version string) *Config {
AutoChangeDiskMethod: true, AutoChangeDiskMethod: true,
FilePath: "goecs.txt", FilePath: "goecs.txt",
EnableUpload: true, EnableUpload: true,
AnalyzeResult: false,
OnlyIpInfoCheck: false, OnlyIpInfoCheck: false,
Help: false, Help: false,
Finish: false, Finish: false,
@@ -106,6 +108,7 @@ func normalizeBoolArgs(args []string) []string {
"disk": true, "ut": true, "security": true, "email": true, "disk": true, "ut": true, "security": true, "email": true,
"backtrace": true, "nt3": true, "speed": true, "ping": true, "backtrace": true, "nt3": true, "speed": true, "ping": true,
"tgdc": true, "web": true, "log": true, "upload": true, "tgdc": true, "web": true, "log": true, "upload": true,
"analysis": true, "analyze": true,
"diskmc": 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.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.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.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.Parse(args)
c.GoecsFlag.Visit(func(f *flag.Flag) { c.GoecsFlag.Visit(func(f *flag.Flag) {
@@ -271,6 +276,9 @@ func (c *Config) SaveUserSetParams() map[string]interface{} {
if c.UserSetFlags["spnum"] { if c.UserSetFlags["spnum"] {
saved["spnum"] = c.SpNum saved["spnum"] = c.SpNum
} }
if c.UserSetFlags["analysis"] || c.UserSetFlags["analyze"] {
saved["analysis"] = c.AnalyzeResult
}
return saved return saved
} }
@@ -389,6 +397,11 @@ func (c *Config) RestoreUserSetParams(saved map[string]interface{}) {
c.SpNum = intVal c.SpNum = intVal
} }
} }
if val, ok := saved["analysis"]; ok {
if boolVal, ok := val.(bool); ok {
c.AnalyzeResult = boolVal
}
}
c.ValidateParams() c.ValidateParams()
} }

View File

@@ -10,6 +10,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/oneclickvirt/ecs/internal/analysis"
"github.com/oneclickvirt/ecs/internal/params" "github.com/oneclickvirt/ecs/internal/params"
"github.com/oneclickvirt/ecs/internal/tests" "github.com/oneclickvirt/ecs/internal/tests"
"github.com/oneclickvirt/ecs/utils" "github.com/oneclickvirt/ecs/utils"
@@ -153,7 +154,7 @@ func RunBasicTests(preCheck utils.NetCheckResult, config *params.Config, basicIn
} }
if config.BasicStatus { if config.BasicStatus {
fmt.Printf("%s", *basicInfo) 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)) scanner := bufio.NewScanner(strings.NewReader(*basicInfo))
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
@@ -363,6 +364,12 @@ func RunSpeedTests(config *params.Config, output, tempOutput string, outputMutex
} }
} else if config.Choice == "6" { } else if config.Choice == "6" {
tests.CustomSP("net", "global", 11, config.Language) 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) time.Sleep(500 * time.Millisecond)
@@ -427,6 +434,25 @@ func AppendTimeInfo(config *params.Config, output, tempOutput string, startTime
}, tempOutput, output) }, 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 // 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) { func HandleSignalInterrupt(sig chan os.Signal, config *params.Config, startTime *time.Time, output *string, tempOutput string, uploadDone chan bool, outputMutex *sync.Mutex) {
select { select {