Compare commits

..

4 Commits

Author SHA1 Message Date
spiritlhl
d2878dd705 fix:更新版本 2026-04-17 10:00:43 +08:00
spiritlhl
a80ce40739 fix:修复主界面选项 2026-04-17 10:00:19 +08:00
spiritlhl
a1b5691179 fix: 修复相关选项说明含义 2026-04-16 23:50:57 +08:00
github-actions[bot]
6cf35680d5 chore: update ECS_VERSION to 0.1.119 in goecs.sh 2026-04-16 14:07:07 +00:00
6 changed files with 277 additions and 95 deletions

17
go.mod
View File

@@ -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
View File

@@ -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=

View File

@@ -27,7 +27,7 @@ import (
)
var (
ecsVersion = "v0.1.119" // 融合怪版本号
ecsVersion = "v0.1.120" // 融合怪版本号
configs = params.NewConfig(ecsVersion) // 全局配置实例
userSetFlags = make(map[string]bool) // 用于跟踪哪些参数是用户显式设置的
)

View File

@@ -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.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.118"
ECS_VERSION="0.1.118"
_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

View File

@@ -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)
}

View File

@@ -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
}