diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6e81c99..37002ec 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -12,7 +12,7 @@ body: id: version attributes: label: ipcheck version - placeholder: ipcheck 0.7.0 + placeholder: ipcheck 0.8.0 validations: required: true - type: dropdown diff --git a/CHANGELOG.md b/CHANGELOG.md index 19f0fce..d16fdcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,29 @@ All notable changes to ipcheck are documented here. The project follows ## [Unreleased] +## [0.8.0] - 2026-07-23 + +### Added + +- Added an adaptive terminal dashboard with score bars, concise status icons, + actionable bottleneck hints, and compact output for narrow terminals. +- Added animated TTY progress while network requests are running, while keeping + redirected output stable and preserving graceful `Ctrl+C` cancellation. +- Added reproducible English and Chinese static previews alongside the animated + terminal GIFs. + ### Changed - Added a localized Chinese terminal GIF to the Chinese README while keeping the English demo in the English README. +- Renamed successful endpoint status from the ambiguous `OK`/“正常” to + `REACH`/“可达” and condensed default per-client conclusions. +- Replaced readiness-score rule v1 with a component-based rule v2 that scores + reachability, median TTFB, P95, and jitter separately and reduces the score + cliff at service-verdict boundaries. +- Kept measured bandwidth speed and sample completeness as separate scoring + inputs, so a very slow partial estimate can no longer receive a smaller + penalty than a faster completed sample. ## [0.7.0] - 2026-07-22 @@ -103,6 +122,7 @@ All notable changes to ipcheck are documented here. The project follows - Homebrew and direct-download packaging. - Median/P95 TTFB, jitter, reference bandwidth, and macOS `networkQuality`. +[0.8.0]: https://github.com/jacklv-coder/ipcheck/compare/v0.7.0...v0.8.0 [0.7.0]: https://github.com/jacklv-coder/ipcheck/compare/v0.6.1...v0.7.0 [0.6.1]: https://github.com/jacklv-coder/ipcheck/compare/v0.6.0...v0.6.1 [0.6.0]: https://github.com/jacklv-coder/ipcheck/compare/v0.5.0...v0.6.0 diff --git a/README.md b/README.md index 9225fb1..89f82c5 100644 --- a/README.md +++ b/README.md @@ -12,35 +12,32 @@ Know whether your AI coding CLI is slow, blocked, or using the wrong gateway. **Codex** and **Claude Code**. It auto-detects installed clients and safe, non-secret routing configuration, then reports reachability, median/P95 time-to-first-byte (TTFB), jitter, reference bandwidth, and a clear -`GOOD`, `FAIR`, `POOR`, or `BLOCKED` result. Interactive runs show live, +`GOOD`, `FAIR`, `POOR`, or `BLOCKED` result. Interactive runs show animated, color-coded progress, can be cancelled cleanly with `Ctrl+C`, and finish with -a direct answer plus a rule-based readiness score. +an adaptive terminal dashboard plus a rule-based readiness score. ![Animated ipcheck terminal demo](assets/ipcheck-demo.gif) ```text $ ipcheck -ipcheck v0.7.0 — AI coding network diagnostics +ipcheck v0.8.0 AI coding network check +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Developer verdict - Ready to code? YES - Readiness score: 95/100 · COMFORTABLE - This network is ready for AI-assisted development. + ✓ Ready to code? YES + 100/100 ████████████████████ COMFORTABLE -Detected clients - Codex model=gpt-5.6-sol, route=https://chatgpt.com + https://api.openai.com - Claude Code model=deepseek-v4-flash, route=https://dashscope.aliyuncs.com/apps/anthropic + This network is ready for AI-assisted development. -Service results - Codex GOOD The service path is reachable with acceptable latency and jitter. - Claude Code GOOD The service path is reachable with acceptable latency and jitter. +◆ AI service latency + ✓ REACH ChatGPT Codex HTTP 401 · median 260 ms + ✓ REACH Claude Messages API Claude Code HTTP 403 · median 220 ms -Result: GOOD +◆ AI service results + ● Codex GOOD · ● Claude Code GOOD -Network bandwidth +◆ Network bandwidth Download 80.0 Mbps FAST Cloudflare, up to 2 MB Upload 16.0 Mbps FAST Cloudflare, up to 1 MB zero-filled - Advice Bandwidth is sufficient for everyday AI-assisted development. ``` ## Why ipcheck @@ -76,7 +73,7 @@ brew install ipcheck ```bash mkdir -p "$HOME/.local/bin" -curl -fsSL https://raw.githubusercontent.com/jacklv-coder/ipcheck/v0.7.0/bin/ipcheck \ +curl -fsSL https://raw.githubusercontent.com/jacklv-coder/ipcheck/v0.8.0/bin/ipcheck \ -o "$HOME/.local/bin/ipcheck" chmod +x "$HOME/.local/bin/ipcheck" ``` @@ -153,10 +150,14 @@ higher and `ADEQUATE` from 2 Mbps. Lower measurements are `SLOW`. The optional substantially more data than ipcheck's capped Cloudflare checks. The readiness score is a transparent heuristic, not a claim about user -percentiles. Rule v1 starts from the service-path verdict (`90` healthy, `70` -usable but delayed, `45` reachable but temporarily unavailable, `35` poor, or -`0` blocked). Completed bandwidth ratings can then add up to 5 points or deduct -up to 10; partial samples deduct 2 points each. JSON exposes `score`, +percentiles. Rule v2 gives the least healthy detected client up to 90 +service-path points: reachability and success rate contribute 35, median TTFB +35, P95 10, and jitter 10. Each bandwidth direction adds 5 points when fast, +3 when adequate, deducts 5 when slow, and stays neutral when unavailable; an +incomplete sample deducts 2 more without hiding its measured speed class. +Usable-but-delayed paths are capped at 89, temporarily unavailable or poor +paths at 64, and blocked paths at 0. Score labels are `COMFORTABLE` from 90, +`GOOD` from 75, `USABLE` from 65, and `LIMITED` below 65. JSON exposes `score`, `score_label`, `score_method`, and the per-component `score_breakdown` for automation. diff --git a/README.zh-CN.md b/README.zh-CN.md index d51e512..b93cf34 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -12,34 +12,31 @@ **Claude Code** 的真实网络路径。它会自动识别本机客户端以及不含密钥的路由 配置,测量可达性、首字节延迟(TTFB)中位数/P95、抖动和参考带宽,并给出 明确的 `GOOD`、`FAIR`、`POOR` 或 `BLOCKED` 结论。交互运行时会显示带颜色的 -实时进度,支持通过 `Ctrl+C` 友好取消,并直接告诉你“现在是否适合开发”以及 -规则化的开发适配分。 +动态进度,支持通过 `Ctrl+C` 友好取消,并用自适应终端仪表盘直接告诉你 +“现在是否适合开发”以及规则化的开发适配分。 ![ipcheck 中文终端动态演示](assets/ipcheck-demo-zh.gif) ```text $ ipcheck -ipcheck v0.7.0 — AI 编程网络诊断 +ipcheck v0.8.0 AI 编程网络体检 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -开发建议 - 现在适合开发吗?适合 - 开发适配分:95/100 · 舒适 - 当前网络适合进行 AI 辅助开发。 + ✓ 现在适合开发吗?适合 + 100/100 ████████████████████ 舒适 -Detected clients - Codex model=gpt-5.6-sol, route=https://chatgpt.com + https://api.openai.com - Claude Code model=deepseek-v4-flash, route=https://dashscope.aliyuncs.com/apps/anthropic + 当前网络适合进行 AI 辅助开发。 -Service results - Codex GOOD - Claude Code GOOD +◆ AI 服务延迟 + ✓ 可达 ChatGPT Codex HTTP 401 · 中位 260 ms + ✓ 可达 Claude Messages API Claude Code HTTP 403 · 中位 220 ms -Result: GOOD +◆ AI 服务结论 + ● Codex 良好 · ● Claude Code 良好 -网络带宽 +◆ 网络带宽 下载 80.0 Mbps 快 Cloudflare,最多 2 MB 上传 16.0 Mbps 快 Cloudflare,最多 1 MB 零字节 - 建议 当前带宽足以支持日常 AI 辅助开发。 ``` ## 核心能力 @@ -70,7 +67,7 @@ brew install ipcheck ```bash mkdir -p "$HOME/.local/bin" -curl -fsSL https://raw.githubusercontent.com/jacklv-coder/ipcheck/v0.7.0/bin/ipcheck \ +curl -fsSL https://raw.githubusercontent.com/jacklv-coder/ipcheck/v0.8.0/bin/ipcheck \ -o "$HOME/.local/bin/ipcheck" chmod +x "$HOME/.local/bin/ipcheck" ``` @@ -138,11 +135,14 @@ ipcheck --no-upload “慢”。可选的 `--system` 会交给 macOS `networkQuality` 测试,消耗的数据量 可能明显高于 ipcheck 自带的限量 Cloudflare 测试。 -开发适配分是透明的规则评分,不代表“超过多少用户”的百分位。规则 v1 根据 -服务链路结论从 90(健康)、70(可用但偏慢)、45(可达但服务暂不可用)、 -35(较差)或 0(阻断)起算;完整的上下行测速最多加 5 分或扣 10 分,部分 -样本每项扣 2 分。JSON 会输出 `score`、`score_label`、`score_method`,以及逐项的 -`score_breakdown`。 +开发适配分是透明的规则评分,不代表“超过多少用户”的百分位。规则 v2 对最弱的 +客户端链路计算最多 90 分:可达性和成功率占 35 分、TTFB 中位数占 35 分、 +P95 占 10 分、抖动占 10 分。下载和上传分别在“快”时加 5 分、“够用”时加 +3 分、“慢”时扣 5 分,不可用时不加不扣;部分样本会在保留实际速度等级的 +基础上再扣 2 分。可用但偏慢的链路最高 89 分,暂时不可用或较差的链路最高 +64 分,阻断链路为 0 分。90 分起为“舒适”、75 分起为“良好”、65 分起为 +“可用”,低于 65 分为“受限”。JSON 会输出 `score`、`score_label`、 +`score_method`,以及逐项的 `score_breakdown`。 运行 `ipcheck --help` 可以查看完整参数。 diff --git a/assets/ipcheck-demo-zh.gif b/assets/ipcheck-demo-zh.gif index e70305c..d5e0019 100644 Binary files a/assets/ipcheck-demo-zh.gif and b/assets/ipcheck-demo-zh.gif differ diff --git a/assets/ipcheck-demo.gif b/assets/ipcheck-demo.gif index 22a7ecf..bd57568 100644 Binary files a/assets/ipcheck-demo.gif and b/assets/ipcheck-demo.gif differ diff --git a/assets/ipcheck-preview-zh.png b/assets/ipcheck-preview-zh.png new file mode 100644 index 0000000..f85eafa Binary files /dev/null and b/assets/ipcheck-preview-zh.png differ diff --git a/assets/ipcheck-preview.png b/assets/ipcheck-preview.png new file mode 100644 index 0000000..60cae3d Binary files /dev/null and b/assets/ipcheck-preview.png differ diff --git a/bin/ipcheck b/bin/ipcheck index 6218974..e42a290 100755 --- a/bin/ipcheck +++ b/bin/ipcheck @@ -10,7 +10,7 @@ unset LC_ALL LC_NUMERIC=C export LC_NUMERIC -VERSION="0.7.0" +VERSION="0.8.0" SCHEMA_VERSION=1 SAMPLES=3 TIMEOUT=15 @@ -202,6 +202,7 @@ esac if [ "$USE_COLOR" -eq 1 ]; then C_RESET=$(printf '\033[0m') C_BOLD=$(printf '\033[1m') + C_DIM=$(printf '\033[2m') C_RED=$(printf '\033[31m') C_GREEN=$(printf '\033[32m') C_YELLOW=$(printf '\033[33m') @@ -209,12 +210,25 @@ if [ "$USE_COLOR" -eq 1 ]; then else C_RESET="" C_BOLD="" + C_DIM="" C_RED="" C_GREEN="" C_YELLOW="" C_CYAN="" fi +TERM_WIDTH="${COLUMNS:-}" +if { ! is_integer "$TERM_WIDTH" || [ "$TERM_WIDTH" -lt 40 ]; } && \ + [ "$MODE" = "human" ] && [ -t 1 ] && command -v tput >/dev/null 2>&1; then + TERM_WIDTH=$(tput cols 2>/dev/null || true) +fi +if ! is_integer "$TERM_WIDTH" || [ "$TERM_WIDTH" -lt 40 ]; then + TERM_WIDTH=100 +fi +[ "$TERM_WIDTH" -le 120 ] || TERM_WIDTH=120 +COMPACT_UI=0 +[ "$TERM_WIDTH" -ge 110 ] || COMPACT_UI=1 + PROGRESS_TTY=0 if [ -t 2 ] && [ "${TERM-}" != "dumb" ]; then PROGRESS_TTY=1 @@ -236,8 +250,24 @@ ui_service_name() { esac } +CURRENT_PROGRESS_MESSAGE="" +SPINNER_INDEX=0 + +draw_progress_line() { + local frame + [ "$SHOW_PROGRESS" -eq 1 ] || return 0 + [ "$PROGRESS_TTY" -eq 1 ] || return 0 + case $((SPINNER_INDEX % 8)) in + 0) frame="⠋" ;; 1) frame="⠙" ;; 2) frame="⠹" ;; 3) frame="⠸" ;; + 4) frame="⠼" ;; 5) frame="⠴" ;; 6) frame="⠦" ;; *) frame="⠧" ;; + esac + SPINNER_INDEX=$((SPINNER_INDEX + 1)) + printf '\r\033[2K %s%s%s %s' "$C_CYAN" "$frame" "$C_RESET" "$CURRENT_PROGRESS_MESSAGE" >&3 +} + progress_note() { [ "$SHOW_PROGRESS" -eq 1 ] || return 0 + CURRENT_PROGRESS_MESSAGE="$2" printf '%s%s%s %s\n' "$C_CYAN" "$1" "$C_RESET" "$2" >&2 } @@ -251,8 +281,9 @@ progress_sample() { progress_message=$(printf 'Testing %s · %s — sample %s/%s (timeout %ss)' \ "$(ui_service_name "$1")" "$2" "$3" "$4" "$TIMEOUT") fi + CURRENT_PROGRESS_MESSAGE="$progress_message" if [ "$PROGRESS_TTY" -eq 1 ]; then - printf '\r\033[2K %s●%s %s' "$C_CYAN" "$C_RESET" "$progress_message" >&2 + draw_progress_line else printf ' %s\n' "$progress_message" >&2 fi @@ -358,6 +389,12 @@ terminate_run() { run_managed() { (trap - INT TERM; exec "$@") & ACTIVE_CHILD_PID=$! + if [ "$SHOW_PROGRESS" -eq 1 ] && [ "$PROGRESS_TTY" -eq 1 ] && [ -n "$CURRENT_PROGRESS_MESSAGE" ]; then + while kill -0 "$ACTIVE_CHILD_PID" 2>/dev/null; do + draw_progress_line + sleep 0.12 + done + fi wait "$ACTIVE_CHILD_PID" managed_status=$? ACTIVE_CHILD_PID="" @@ -790,10 +827,6 @@ p95_file() { }' } -format_ms() { - awk -v value="$1" 'BEGIN { printf "%.0f ms", value }' -} - format_mbps() { awk -v value="$1" 'BEGIN { printf "%.1f Mbps", value * 8 / 1000000 }' } @@ -838,6 +871,16 @@ localized_bandwidth_rating() { esac } +partial_sample_note() { + [ "$1" = "partial" ] || return 0 + case "$2:$UI_LANG" in + human:zh) printf ' · 部分样本 -2' ;; + human:*) printf ' · partial sample -2' ;; + markdown:zh) printf ',部分样本 -2' ;; + markdown:*) printf ', partial sample -2' ;; + esac +} + endpoint_count=0 TAB=$(printf '\t') if [ "$UI_LANG" = "zh" ]; then @@ -918,6 +961,7 @@ bandwidth_bytes=0 bandwidth_complete=false bandwidth_exit=0 download_rating="unavailable" +download_speed_rating="unavailable" if [ "$RUN_BANDWIDTH" -eq 1 ]; then if [ "$UI_LANG" = "zh" ]; then progress_note "●" "正在测试参考下载带宽(2 MB)…" @@ -950,7 +994,8 @@ EOF bandwidth_speed=0 fi if [ "$bandwidth_valid" -eq 1 ]; then - download_rating=$(bandwidth_rating download "$bandwidth_speed") + download_speed_rating=$(bandwidth_rating download "$bandwidth_speed") + download_rating="$download_speed_rating" [ "$bandwidth_complete" = true ] || download_rating="partial" if [ "$UI_LANG" = "zh" ]; then download_progress="下载 $(format_mbps "$bandwidth_speed")($(localized_bandwidth_rating "$download_rating"))" @@ -971,6 +1016,7 @@ upload_valid=0 upload_bytes=0 upload_complete=false upload_rating="unavailable" +upload_speed_rating="unavailable" if [ "$RUN_UPLOAD" -eq 1 ]; then if [ "$UI_LANG" = "zh" ]; then progress_note "●" "正在测试参考上传带宽(1 MB 零字节)…" @@ -1006,7 +1052,8 @@ EOF upload_bytes=0 upload_speed=0 else - upload_rating=$(bandwidth_rating upload "$upload_speed") + upload_speed_rating=$(bandwidth_rating upload "$upload_speed") + upload_rating="$upload_speed_rating" [ "$upload_complete" = true ] || upload_rating="partial" fi if [ "$upload_valid" -eq 1 ]; then @@ -1086,6 +1133,11 @@ system_throughput="" system_upload_throughput="" system_proxy_state="" if [ "$RUN_SYSTEM" -eq 1 ] && command -v networkQuality >/dev/null 2>&1; then + if [ "$UI_LANG" = "zh" ]; then + progress_note "●" "正在运行 macOS 网络质量测试…" + else + progress_note "●" "Running the macOS network quality test…" + fi system_file="$WORK_DIR/networkQuality.json" run_managed networkQuality -c -s -M "$TIMEOUT" > "$system_file" 2>/dev/null || true system_interface=$(sed -n 's/.*"interface_name" *: *"\([^"]*\)".*/\1/p' "$system_file" | head -n 1) @@ -1093,6 +1145,11 @@ if [ "$RUN_SYSTEM" -eq 1 ] && command -v networkQuality >/dev/null 2>&1; then system_throughput=$(sed -n 's/.*"dl_throughput" *: *\([0-9.]*\).*/\1/p' "$system_file" | head -n 1) system_upload_throughput=$(sed -n 's/.*"ul_throughput" *: *\([0-9.]*\).*/\1/p' "$system_file" | head -n 1) system_proxy_state=$(sed -n 's/.*"proxy_state" *: *"\([^"]*\)".*/\1/p' "$system_file" | head -n 1) + if [ -n "${system_throughput}${system_upload_throughput}" ]; then + progress_finish ok "macOS networkQuality" "$([ "$UI_LANG" = "zh" ] && printf '测试完成' || printf 'test complete')" + else + progress_finish warn "macOS networkQuality" "$([ "$UI_LANG" = "zh" ] && printf '未返回有效结果' || printf 'no valid result')" + fi fi reachable_count=0 @@ -1202,7 +1259,8 @@ while IFS="$TAB" read -r service display model base source; do primary_jitter="$service_jitter" primary_success_rate="$service_rate" fi - printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$service" "$service_result" "$service_reason" "$service_rate" "$service_ttfb" "$service_jitter" >> "$SERVICE_RESULT_FILE" + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$service" "$service_result" "$service_reason" "$service_rate" "$service_ttfb" "$service_jitter" "$service_usable" >> "$SERVICE_RESULT_FILE" done < "$SERVICES_FILE" if [ "$blocked_services" -eq "$service_count" ]; then @@ -1276,32 +1334,144 @@ case "$overall" in ;; esac -# A transparent heuristic, not a user percentile. Service-path quality carries -# most of the score; completed bandwidth samples can move it by at most 10. -case "$overall" in - good) readiness_score_base=90 ;; +# Rule v2 is a transparent heuristic, not a user percentile. The worst client +# service path contributes up to 90 points: reachability 35, median TTFB 35, +# P95 10, and jitter 10. Reference bandwidth can add up to 10 points; slow or +# incomplete transfers can deduct points without outweighing service quality. +score_latency_points() { + awk -v value="$1" 'BEGIN { + if (value <= 0) print 0 + else if (value < 800) print 35 + else if (value < 1500) print 30 + else if (value < 3000) print 22 + else if (value < 5000) print 14 + else print 6 + }' +} + +score_p95_points() { + awk -v value="$1" 'BEGIN { + if (value <= 0) print 0 + else if (value < 2000) print 10 + else if (value < 4000) print 7 + else if (value < 6000) print 4 + else print 0 + }' +} + +score_jitter_points() { + awk -v value="$1" 'BEGIN { + if (value < 0) print 0 + else if (value < 200) print 10 + else if (value < 500) print 7 + else if (value < 1000) print 4 + else print 0 + }' +} + +score_bandwidth_points() { + case "$1" in + fast) printf 5 ;; + adequate) printf 3 ;; + slow) printf -- -5 ;; + *) printf 0 ;; + esac +} + +score_service="" +score_service_path=0 +score_availability=0 +score_median_ttfb=0 +score_p95=0 +score_jitter=0 +score_service_seen=0 +score_service_rank=-1 +score_service_result="blocked" +score_service_usable=0 +score_service_is_usable=0 +score_success_rate=0 +score_ttfb_value=0 +score_jitter_value=0 +while IFS="$TAB" read -r service result reason success_rate ttfb jitter service_usable_count; do + case "$result" in + good) result_rank=0 ;; + fair) result_rank=1 ;; + poor) result_rank=2 ;; + *) result_rank=3 ;; + esac + if [ "$result" = "blocked" ]; then + availability_points=0 + elif [ "$success_rate" -ge 100 ]; then + availability_points=35 + elif [ "$success_rate" -ge 60 ]; then + availability_points=22 + elif [ "$success_rate" -gt 0 ]; then + availability_points=10 + else + availability_points=0 + fi + primary_index=$(awk -F '\t' -v service="$service" '$2 == service && $5 == "primary" { print $1; exit }' "$ENDPOINT_SUMMARY_FILE") + service_p95=$(awk -F '\t' -v idx="$primary_index" '$1 == idx { print $9; exit }' "$ENDPOINT_SUMMARY_FILE") + service_p95=${service_p95:-0} + median_points=$(score_latency_points "$ttfb") + p95_points=$(score_p95_points "$service_p95") + jitter_points=$(score_jitter_points "$jitter") + if [ "$result" = "blocked" ]; then + median_points=0 + p95_points=0 + jitter_points=0 + fi + service_points=$((availability_points + median_points + p95_points + jitter_points)) + service_is_usable=0 + [ "$service_usable_count" -eq 0 ] || service_is_usable=1 + select_scored_service=0 + if [ "$score_service_seen" -eq 0 ] || [ "$result_rank" -gt "$score_service_rank" ]; then + select_scored_service=1 + elif [ "$result_rank" -eq "$score_service_rank" ]; then + if [ "$service_is_usable" -lt "$score_service_is_usable" ]; then + select_scored_service=1 + elif [ "$service_is_usable" -eq "$score_service_is_usable" ] && [ "$service_points" -lt "$score_service_path" ]; then + select_scored_service=1 + fi + fi + if [ "$select_scored_service" -eq 1 ]; then + score_service="$service" + score_service_rank="$result_rank" + score_service_result="$result" + score_service_usable="$service_usable_count" + score_service_is_usable="$service_is_usable" + score_service_path="$service_points" + score_availability="$availability_points" + score_median_ttfb="$median_points" + score_p95="$p95_points" + score_jitter="$jitter_points" + score_success_rate="$success_rate" + score_ttfb_value="$ttfb" + score_jitter_value="$jitter" + score_service_seen=1 + fi +done < "$SERVICE_RESULT_FILE" + +download_speed_points=$(score_bandwidth_points "$download_speed_rating") +upload_speed_points=$(score_bandwidth_points "$upload_speed_rating") +download_partial_penalty=0 +upload_partial_penalty=0 +[ "$download_rating" != "partial" ] || download_partial_penalty=-2 +[ "$upload_rating" != "partial" ] || upload_partial_penalty=-2 +download_score=$((download_speed_points + download_partial_penalty)) +upload_score=$((upload_speed_points + upload_partial_penalty)) +readiness_score_before_cap=$((score_service_path + download_score + upload_score)) + +case "$score_service_result" in + good) readiness_score_cap=100 ;; fair) - if [ "$usable_services" -gt 0 ]; then readiness_score_base=70; else readiness_score_base=45; fi + if [ "$score_service_usable" -gt 0 ]; then readiness_score_cap=89; else readiness_score_cap=64; fi ;; - poor) readiness_score_base=35 ;; - *) readiness_score_base=0 ;; + poor) readiness_score_cap=64 ;; + *) readiness_score_cap=0 ;; esac -download_score_adjustment=0 -upload_score_adjustment=0 -readiness_score="$readiness_score_base" -if [ "$readiness_score" -gt 0 ]; then - case "$download_rating" in - fast) download_score_adjustment=2 ;; - slow) download_score_adjustment=-5 ;; - partial) download_score_adjustment=-2 ;; - esac - case "$upload_rating" in - fast) upload_score_adjustment=3 ;; - slow) upload_score_adjustment=-5 ;; - partial) upload_score_adjustment=-2 ;; - esac - readiness_score=$((readiness_score + download_score_adjustment + upload_score_adjustment)) -fi +readiness_score="$readiness_score_before_cap" +[ "$readiness_score" -le "$readiness_score_cap" ] || readiness_score="$readiness_score_cap" [ "$readiness_score" -ge 0 ] || readiness_score=0 [ "$readiness_score" -le 100 ] || readiness_score=100 if [ "$readiness_score" -ge 90 ]; then @@ -1310,7 +1480,7 @@ if [ "$readiness_score" -ge 90 ]; then elif [ "$readiness_score" -ge 75 ]; then readiness_score_label_en="GOOD" readiness_score_label_zh="良好" -elif [ "$readiness_score" -ge 60 ]; then +elif [ "$readiness_score" -ge 65 ]; then readiness_score_label_en="USABLE" readiness_score_label_zh="可用" else @@ -1343,64 +1513,185 @@ service_display() { awk -F '\t' -v service="$1" '$1 == service { print $2; exit }' "$SERVICES_FILE" } -signed_score() { - if [ "$1" -ge 0 ]; then printf '+%s' "$1"; else printf '%s' "$1"; fi +repeat_glyph() { + awk -v glyph="$1" -v count="$2" 'BEGIN { for (i = 0; i < count; i++) printf "%s", glyph }' +} + +ui_divider() { + divider_width=$TERM_WIDTH + [ "$divider_width" -le 76 ] || divider_width=76 + [ "$divider_width" -ge 40 ] || divider_width=40 + printf '%s%s%s\n' "$C_DIM" "$(repeat_glyph '─' "$divider_width")" "$C_RESET" +} + +section_title() { + printf '\n%s%s◆%s %s%s%s\n' "$C_BOLD" "$C_CYAN" "$C_RESET" "$C_BOLD" "$1" "$C_RESET" +} + +signed_points() { + if [ "$1" -gt 0 ]; then printf '+%s' "$1"; else printf '%s' "$1"; fi +} + +score_bar() { + bar_width=20 + [ "$COMPACT_UI" -eq 0 ] || bar_width=14 + filled=$(((readiness_score * bar_width + 99) / 100)) + [ "$filled" -le "$bar_width" ] || filled="$bar_width" + empty=$((bar_width - filled)) + printf '%s' "$score_color" + repeat_glyph '█' "$filled" + printf '%s%s' "$C_RESET" "$C_DIM" + repeat_glyph '░' "$empty" + printf '%s' "$C_RESET" +} + +format_ui_latency() { + awk -v value="$1" 'BEGIN { + if (value >= 1000) printf "%.1f s", value / 1000 + else printf "%.0f ms", value + }' +} + +localized_service_result() { + case "$UI_LANG:$1" in + zh:good) printf '良好' ;; + zh:fair) printf '一般' ;; + zh:poor) printf '较差' ;; + zh:blocked) printf '阻断' ;; + *) printf '%s' "$(printf '%s' "$1" | tr '[:lower:]' '[:upper:]')" ;; + esac +} + +render_primary_diagnosis() { + diagnosis_count=0 + if [ "$score_availability" -lt 35 ]; then + diagnosis_count=$((diagnosis_count + 1)) + if [ "$UI_LANG" = "zh" ]; then + printf ' %s!%s 链路可达率只有 %s%%,请求可能中断。\n' "$C_YELLOW" "$C_RESET" "$score_success_rate" + else + printf ' %s!%s Reachability is only %s%%; requests may be interrupted.\n' "$C_YELLOW" "$C_RESET" "$score_success_rate" + fi + fi + if [ "$score_median_ttfb" -lt 30 ] && awk -v value="$score_ttfb_value" 'BEGIN { exit !(value >= 1500) }'; then + diagnosis_count=$((diagnosis_count + 1)) + if [ "$UI_LANG" = "zh" ]; then + printf ' %s!%s 首字节中位延迟 %s,AI 回复启动会明显变慢。\n' "$C_YELLOW" "$C_RESET" "$(format_ui_latency "$score_ttfb_value")" + else + printf ' %s!%s Median TTFB is %s; AI responses will start noticeably slower.\n' "$C_YELLOW" "$C_RESET" "$(format_ui_latency "$score_ttfb_value")" + fi + fi + if [ "$score_jitter" -lt 7 ] && awk -v value="$score_jitter_value" 'BEGIN { exit !(value >= 500) }'; then + diagnosis_count=$((diagnosis_count + 1)) + if [ "$UI_LANG" = "zh" ]; then + printf ' %s!%s 抖动 %s,连续交互的等待时间不稳定。\n' "$C_YELLOW" "$C_RESET" "$(format_ui_latency "$score_jitter_value")" + else + printf ' %s!%s Jitter is %s; wait times will vary during an interactive session.\n' "$C_YELLOW" "$C_RESET" "$(format_ui_latency "$score_jitter_value")" + fi + fi + if [ "$download_speed_rating" = "slow" ]; then + diagnosis_count=$((diagnosis_count + 1)) + if [ "$UI_LANG" = "zh" ]; then + printf ' %s!%s 下载 %s,拉取仓库和安装依赖会偏慢。\n' "$C_YELLOW" "$C_RESET" "$(format_mbps "$bandwidth_speed")" + else + printf ' %s!%s Download is %s; clones and dependency installs will be slower.\n' "$C_YELLOW" "$C_RESET" "$(format_mbps "$bandwidth_speed")" + fi + fi + return 0 } render_score_breakdown() { - download_score_label=$(localized_bandwidth_rating "$download_rating") - upload_score_label=$(localized_bandwidth_rating "$upload_rating") + download_score_label=$(localized_bandwidth_rating "$download_speed_rating") + upload_score_label=$(localized_bandwidth_rating "$upload_speed_rating") if [ "$UI_LANG" = "zh" ]; then - printf ' 评分依据:服务链路 %s;下载 %s %s;上传 %s %s;总分 %s。\n' \ - "$readiness_score_base" "$download_score_label" "$(signed_score "$download_score_adjustment")" \ - "$upload_score_label" "$(signed_score "$upload_score_adjustment")" "$readiness_score" + printf ' %s评分构成%s\n' "$C_BOLD" "$C_RESET" + printf ' 服务链路 %2s/90 可达性 %s · TTFB %s · P95 %s · 抖动 %s\n' "$score_service_path" "$score_availability" "$score_median_ttfb" "$score_p95" "$score_jitter" + printf ' 下载 %3s %s · %s%s\n' "$(signed_points "$download_score")" "$(format_mbps "$bandwidth_speed")" "$download_score_label" "$(partial_sample_note "$download_rating" human)" + printf ' 上传 %3s %s · %s%s\n' "$(signed_points "$upload_score")" "$(format_mbps "$upload_speed")" "$upload_score_label" "$(partial_sample_note "$upload_rating" human)" + [ "$readiness_score_before_cap" -le "$readiness_score_cap" ] || printf ' 结论限幅 %s → %s\n' "$readiness_score_before_cap" "$readiness_score_cap" else - printf ' Score breakdown: service path %s; download %s %s; upload %s %s; total %s.\n' \ - "$readiness_score_base" "$download_score_label" "$(signed_score "$download_score_adjustment")" \ - "$upload_score_label" "$(signed_score "$upload_score_adjustment")" "$readiness_score" + printf ' %sScore breakdown%s\n' "$C_BOLD" "$C_RESET" + printf ' Service path %2s/90 reachability %s · TTFB %s · P95 %s · jitter %s\n' "$score_service_path" "$score_availability" "$score_median_ttfb" "$score_p95" "$score_jitter" + printf ' Download %3s %s · %s%s\n' "$(signed_points "$download_score")" "$(format_mbps "$bandwidth_speed")" "$download_score_label" "$(partial_sample_note "$download_rating" human)" + printf ' Upload %3s %s · %s%s\n' "$(signed_points "$upload_score")" "$(format_mbps "$upload_speed")" "$upload_score_label" "$(partial_sample_note "$upload_rating" human)" + [ "$readiness_score_before_cap" -le "$readiness_score_cap" ] || printf ' Verdict cap %s → %s\n' "$readiness_score_before_cap" "$readiness_score_cap" fi } render_human() { - if [ "$UI_LANG" = "zh" ]; then - printf '%sipcheck v%s — AI 编程网络诊断%s\n' "$C_BOLD$C_CYAN" "$VERSION" "$C_RESET" - printf '时间(UTC):%s\n网络路径:%s\n' "$timestamp" "$proxy_text" - printf '\n%s开发建议%s\n' "$C_BOLD" "$C_RESET" - printf ' 现在适合开发吗?%s%s%s\n' "$C_BOLD" "$readiness_answer" "$C_RESET" - printf ' 开发适配分:%s%s/100 · %s%s\n' "$C_BOLD" "$readiness_score" "$readiness_score_label" "$C_RESET" - [ "$EXPLAIN_SCORE" -eq 0 ] || render_score_breakdown - else - printf '%sipcheck v%s — AI coding network diagnostics%s\n' "$C_BOLD$C_CYAN" "$VERSION" "$C_RESET" - printf 'Time (UTC): %s\nNetwork path: %s\n' "$timestamp" "$proxy_text" - printf '\n%sDeveloper verdict%s\n' "$C_BOLD" "$C_RESET" - printf ' Ready to code? %s%s%s\n' "$C_BOLD" "$readiness_answer" "$C_RESET" - printf ' Readiness score: %s%s/100 · %s%s\n' "$C_BOLD" "$readiness_score" "$readiness_score_label" "$C_RESET" - [ "$EXPLAIN_SCORE" -eq 0 ] || render_score_breakdown - fi + case "$readiness_score" in + 9[0-9]|100) score_color="$C_GREEN" ;; + 6[5-9]|7[0-9]|8[0-9]) score_color="$C_YELLOW" ;; + *) score_color="$C_RED" ;; + esac + if [ "$readiness_ready" = true ]; then verdict_icon="✓"; else verdict_icon="✕"; fi case "$overall" in good) verdict_color="$C_GREEN" ;; fair) verdict_color="$C_YELLOW" ;; *) verdict_color="$C_RED" ;; esac - printf ' %s%s%s\n %s\n' "$verdict_color" "$readiness_summary" "$C_RESET" "$readiness_recommendation" + + if [ "$UI_LANG" = "zh" ]; then + printf '%s%sipcheck v%s%s %sAI 编程网络体检%s\n' "$C_BOLD" "$C_CYAN" "$VERSION" "$C_RESET" "$C_BOLD" "$C_RESET" + else + printf '%s%sipcheck v%s%s %sAI coding network check%s\n' "$C_BOLD" "$C_CYAN" "$VERSION" "$C_RESET" "$C_BOLD" "$C_RESET" + fi + ui_divider + if [ "$UI_LANG" = "zh" ]; then + printf '\n %s%s%s %s现在适合开发吗?%s%s%s\n' "$verdict_color" "$verdict_icon" "$C_RESET" "$C_BOLD" "$verdict_color" "$readiness_answer" "$C_RESET" + if [ "$COMPACT_UI" -eq 1 ]; then + printf ' %s%s/100%s · %s\n ' "$score_color$C_BOLD" "$readiness_score" "$C_RESET" "$readiness_score_label" + score_bar + printf '\n' + else + printf ' %s%s/100%s ' "$score_color$C_BOLD" "$readiness_score" "$C_RESET" + score_bar + printf ' %s\n' "$readiness_score_label" + fi + else + printf '\n %s%s%s %sReady to code? %s%s%s\n' "$verdict_color" "$verdict_icon" "$C_RESET" "$C_BOLD" "$verdict_color" "$readiness_answer" "$C_RESET" + if [ "$COMPACT_UI" -eq 1 ]; then + printf ' %s%s/100%s · %s\n ' "$score_color$C_BOLD" "$readiness_score" "$C_RESET" "$readiness_score_label" + score_bar + printf '\n' + else + printf ' %s%s/100%s ' "$score_color$C_BOLD" "$readiness_score" "$C_RESET" + score_bar + printf ' %s\n' "$readiness_score_label" + fi + fi + printf '\n %s%s%s\n %s\n' "$verdict_color" "$readiness_summary" "$C_RESET" "$readiness_recommendation" + render_primary_diagnosis + [ "$EXPLAIN_SCORE" -eq 0 ] || { printf '\n'; render_score_breakdown; } + if [ "$UI_LANG" = "zh" ]; then + printf '\n %s时间%s %s\n %s路径%s %s\n' "$C_DIM" "$C_RESET" "$timestamp" "$C_DIM" "$C_RESET" "$proxy_text" + else + printf '\n %sTime%s %s\n %sPath%s %s\n' "$C_DIM" "$C_RESET" "$timestamp" "$C_DIM" "$C_RESET" "$proxy_text" + fi + if [ "$UI_LANG" = "zh" ]; then - printf '\n%s检测到的客户端%s\n' "$C_BOLD" "$C_RESET" + section_title "检测到的客户端" else - printf '\n%sDetected clients%s\n' "$C_BOLD" "$C_RESET" + section_title "Detected clients" fi while IFS="$TAB" read -r service display model base source; do - printf ' %-12s model=%s, route=%s, source=%s\n' "$display" "$model" "$base" "$source" + if [ "$COMPACT_UI" -eq 1 ]; then + printf ' %s%s%s %s\n %s\n' "$C_BOLD" "$display" "$C_RESET" "$model" "$base" + else + printf ' %-12s %s%s%s %s%s%s\n' "$display" "$C_BOLD" "$model" "$C_RESET" "$C_DIM" "$base" "$C_RESET" + fi done < "$SERVICES_FILE" if [ -s "$WARNINGS_FILE" ]; then - if [ "$UI_LANG" = "zh" ]; then printf '\n%s注意事项%s\n' "$C_BOLD" "$C_RESET"; else printf '\n%sWarnings%s\n' "$C_BOLD" "$C_RESET"; fi + if [ "$UI_LANG" = "zh" ]; then section_title "注意事项"; else section_title "Warnings"; fi while IFS= read -r warning; do printf ' %s!%s %s\n' "$C_YELLOW" "$C_RESET" "$(localized_warning "$warning")" done < "$WARNINGS_FILE" fi if [ "$UI_LANG" = "zh" ]; then - printf '\n%sAI 服务延迟%s\n 首字节延迟(TTFB):中位/P95 越低越好;抖动越低越稳定。\n' "$C_BOLD" "$C_RESET" + section_title "AI 服务延迟" + printf ' %sTTFB:中位/P95 越低越好;抖动越低越稳定。%s\n' "$C_DIM" "$C_RESET" else - printf '\n%sAI service latency%s\n Time to first byte (TTFB): lower median/P95 is better; lower jitter is more stable.\n' "$C_BOLD" "$C_RESET" + section_title "AI service latency" + printf ' %sTTFB: lower median/P95 is better; lower jitter is more stable.%s\n' "$C_DIM" "$C_RESET" fi while IFS="$TAB" read -r index service label endpoint probe_type priority; do sample_file="$WORK_DIR/endpoint_${index}.tsv" @@ -1412,10 +1703,16 @@ EOF [ -n "$http_code" ] || http_code="000" display_name=$(service_display "$service") if [ "$ok_count" -gt 0 ]; then - if [ "$UI_LANG" = "zh" ]; then - printf ' %s正常%s [%-11s] %-25s HTTP %s,%s/%s 次,中位 %s,P95 %s,抖动 %s\n' "$C_GREEN" "$C_RESET" "$display_name" "$label" "$http_code" "$ok_count" "$SAMPLES" "$(format_ms "$ttfb_median")" "$(format_ms "$ttfb_p95")" "$(format_ms "$jitter")" + endpoint_color="$C_GREEN" + case "$http_code" in 404|429|5[0-9][0-9]) endpoint_color="$C_YELLOW" ;; esac + if [ "$UI_LANG" = "zh" ] && [ "$COMPACT_UI" -eq 1 ]; then + printf ' %s✓ 可达%s %s · %s\n HTTP %s · %s/%s 次 · 中位 %s · P95 %s · 抖动 %s\n' "$endpoint_color" "$C_RESET" "$label" "$display_name" "$http_code" "$ok_count" "$SAMPLES" "$(format_ui_latency "$ttfb_median")" "$(format_ui_latency "$ttfb_p95")" "$(format_ui_latency "$jitter")" + elif [ "$UI_LANG" = "zh" ]; then + printf ' %s✓ 可达%s %-23s %-11s HTTP %s · %s/%s · 中位 %s · P95 %s · 抖动 %s\n' "$endpoint_color" "$C_RESET" "$label" "$display_name" "$http_code" "$ok_count" "$SAMPLES" "$(format_ui_latency "$ttfb_median")" "$(format_ui_latency "$ttfb_p95")" "$(format_ui_latency "$jitter")" + elif [ "$COMPACT_UI" -eq 1 ]; then + printf ' %s✓ REACH%s %s · %s\n HTTP %s · %s/%s · median %s · P95 %s · jitter %s\n' "$endpoint_color" "$C_RESET" "$label" "$display_name" "$http_code" "$ok_count" "$SAMPLES" "$(format_ui_latency "$ttfb_median")" "$(format_ui_latency "$ttfb_p95")" "$(format_ui_latency "$jitter")" else - printf ' %sOK%s [%-11s] %-25s HTTP %s, %s/%s, median %s, p95 %s, jitter %s\n' "$C_GREEN" "$C_RESET" "$display_name" "$label" "$http_code" "$ok_count" "$SAMPLES" "$(format_ms "$ttfb_median")" "$(format_ms "$ttfb_p95")" "$(format_ms "$jitter")" + printf ' %s✓ REACH%s %-23s %-11s HTTP %s · %s/%s · median %s · P95 %s · jitter %s\n' "$endpoint_color" "$C_RESET" "$label" "$display_name" "$http_code" "$ok_count" "$SAMPLES" "$(format_ui_latency "$ttfb_median")" "$(format_ui_latency "$ttfb_p95")" "$(format_ui_latency "$jitter")" fi elif [ "$http_code" = "407" ]; then if [ "$UI_LANG" = "zh" ]; then printf ' %s认证%s [%-11s] %-25s HTTP 407,需要代理认证\n' "$C_YELLOW" "$C_RESET" "$display_name" "$label"; else printf ' %sAUTH%s [%-11s] %-25s HTTP 407, proxy authentication required\n' "$C_YELLOW" "$C_RESET" "$display_name" "$label"; fi @@ -1424,12 +1721,28 @@ EOF fi done < "$INDEXED_PROBES_FILE" - if [ "$UI_LANG" = "zh" ]; then printf '\n%sAI 服务结论%s\n' "$C_BOLD" "$C_RESET"; else printf '\n%sAI service results%s\n' "$C_BOLD" "$C_RESET"; fi - while IFS="$TAB" read -r service result reason success_rate ttfb jitter; do - printf ' %-12s %-7s %s\n' "$(service_display "$service")" "$(printf '%s' "$result" | tr '[:lower:]' '[:upper:]')" "$(localized_reason "$reason")" - done < "$SERVICE_RESULT_FILE" + if [ "$UI_LANG" = "zh" ]; then section_title "AI 服务结论"; else section_title "AI service results"; fi + if [ "$EXPLAIN_SCORE" -eq 0 ] && [ "$COMPACT_UI" -eq 0 ]; then + result_separator="" + printf ' ' + while IFS="$TAB" read -r service result reason success_rate ttfb jitter service_usable_count; do + case "$result" in good) result_color="$C_GREEN" ;; fair) result_color="$C_YELLOW" ;; *) result_color="$C_RED" ;; esac + printf '%s%s●%s %s %s%s%s' "$result_separator" "$result_color" "$C_RESET" "$(service_display "$service")" "$result_color" "$(localized_service_result "$result")" "$C_RESET" + result_separator=" · " + done < "$SERVICE_RESULT_FILE" + printf '\n' + else + while IFS="$TAB" read -r service result reason success_rate ttfb jitter service_usable_count; do + case "$result" in good) result_color="$C_GREEN" ;; fair) result_color="$C_YELLOW" ;; *) result_color="$C_RED" ;; esac + if [ "$EXPLAIN_SCORE" -eq 1 ]; then + printf ' %s●%s %-12s %s%-7s%s %s\n' "$result_color" "$C_RESET" "$(service_display "$service")" "$result_color" "$(localized_service_result "$result")" "$C_RESET" "$(localized_reason "$reason")" + else + printf ' %s●%s %s %s%s%s\n' "$result_color" "$C_RESET" "$(service_display "$service")" "$result_color" "$(localized_service_result "$result")" "$C_RESET" + fi + done < "$SERVICE_RESULT_FILE" + fi - if [ "$UI_LANG" = "zh" ]; then printf '\n%s网络带宽%s\n' "$C_BOLD" "$C_RESET"; else printf '\n%sNetwork bandwidth%s\n' "$C_BOLD" "$C_RESET"; fi + if [ "$UI_LANG" = "zh" ]; then section_title "网络带宽"; else section_title "Network bandwidth"; fi case "$download_rating" in fast) download_color="$C_GREEN" ;; adequate|partial) download_color="$C_YELLOW" ;; *) download_color="$C_RED" ;; esac case "$upload_rating" in fast) upload_color="$C_GREEN" ;; adequate|partial) upload_color="$C_YELLOW" ;; *) upload_color="$C_RED" ;; esac if [ "$RUN_BANDWIDTH" -eq 1 ] && [ "$bandwidth_valid" -eq 1 ]; then @@ -1447,7 +1760,7 @@ EOF if [ "$UI_LANG" = "zh" ]; then printf ' 上传 已跳过\n'; else printf ' Upload skipped\n'; fi fi if [ "$RUN_BANDWIDTH" -eq 1 ] || [ "$RUN_UPLOAD" -eq 1 ]; then - if [ "$UI_LANG" = "zh" ]; then printf ' 路径 %s\n 建议 %s\n' "$proxy_text" "$bandwidth_advice"; else printf ' Path %s\n Advice %s\n' "$proxy_text" "$bandwidth_advice"; fi + if [ "$UI_LANG" = "zh" ]; then printf ' %s建议%s %s\n' "$C_DIM" "$C_RESET" "$bandwidth_advice"; else printf ' %sAdvice%s %s\n' "$C_DIM" "$C_RESET" "$bandwidth_advice"; fi fi if [ -n "$system_throughput" ]; then printf ' macOS networkQuality: down %s' "$(format_bits_mbps "$system_throughput")" @@ -1458,13 +1771,9 @@ EOF fi if [ "$UI_LANG" = "zh" ]; then - printf '\n结果:%s%s%s\n%s\n' "$verdict_color" "$(printf '%s' "$overall" | tr '[:lower:]' '[:upper:]')" "$C_RESET" "$localized_overall_reason" - printf '说明:对编程智能体来说,首字节延迟、失败率和抖动比峰值带宽更重要。\n' - printf 'HTTP 401/403 表示链路可达;ipcheck 不会发送 API 凭据,也不会发起计费模型请求。\n' + printf '\n %sHTTP 401/403 代表链路可达;不使用凭据,不产生模型费用。%s\n' "$C_DIM" "$C_RESET" else - printf '\nResult: %s%s%s\n%s\n' "$verdict_color" "$(printf '%s' "$overall" | tr '[:lower:]' '[:upper:]')" "$C_RESET" "$localized_overall_reason" - printf 'Interpretation: first-byte latency, failures, and jitter matter more than peak bandwidth for coding agents.\n' - printf 'HTTP 401/403 counts as reachable; ipcheck never sends API credentials or billable model requests.\n' + printf '\n %sHTTP 401/403 means reachable; no credentials or billable model calls.%s\n' "$C_DIM" "$C_RESET" fi } @@ -1477,9 +1786,11 @@ render_markdown() { printf -- '- 现在适合开发吗?**%s** — %s\n\n' "$readiness_answer" "$readiness_summary" printf -- '- 开发适配分:**%s/100 · %s**(规则评分,不是用户百分位)\n\n' "$readiness_score" "$readiness_score_label" if [ "$EXPLAIN_SCORE" -eq 1 ]; then - printf -- '- 评分依据:服务链路 %s;下载 %s %s;上传 %s %s;总分 %s。\n\n' \ - "$readiness_score_base" "$(localized_bandwidth_rating "$download_rating")" "$(signed_score "$download_score_adjustment")" \ - "$(localized_bandwidth_rating "$upload_rating")" "$(signed_score "$upload_score_adjustment")" "$readiness_score" + printf -- '- 评分依据:服务链路 %s/90(可达性 %s + TTFB %s + P95 %s + 抖动 %s);下载 %s(%s%s);上传 %s(%s%s);限幅前 %s;结论上限 %s;总分 %s。\n\n' \ + "$score_service_path" "$score_availability" "$score_median_ttfb" "$score_p95" "$score_jitter" \ + "$download_score" "$(localized_bandwidth_rating "$download_speed_rating")" "$(partial_sample_note "$download_rating" markdown)" \ + "$upload_score" "$(localized_bandwidth_rating "$upload_speed_rating")" "$(partial_sample_note "$upload_rating" markdown)" \ + "$readiness_score_before_cap" "$readiness_score_cap" "$readiness_score" fi printf '## 检测到的客户端\n\n' printf '| 客户端 | 模型 | 路由 | 配置来源 | 结果 |\n|---|---|---|---|---|\n' @@ -1491,9 +1802,11 @@ render_markdown() { printf -- '- Ready to code? **%s** — %s\n\n' "$readiness_answer" "$readiness_summary" printf -- '- Readiness score: **%s/100 · %s** (rule-based, not a user percentile)\n\n' "$readiness_score" "$readiness_score_label" if [ "$EXPLAIN_SCORE" -eq 1 ]; then - printf -- '- Score breakdown: service path %s; download %s %s; upload %s %s; total %s.\n\n' \ - "$readiness_score_base" "$(localized_bandwidth_rating "$download_rating")" "$(signed_score "$download_score_adjustment")" \ - "$(localized_bandwidth_rating "$upload_rating")" "$(signed_score "$upload_score_adjustment")" "$readiness_score" + printf -- '- Score breakdown: service path %s/90 (reachability %s + TTFB %s + P95 %s + jitter %s); download %s (%s%s); upload %s (%s%s); pre-cap %s; verdict cap %s; total %s.\n\n' \ + "$score_service_path" "$score_availability" "$score_median_ttfb" "$score_p95" "$score_jitter" \ + "$download_score" "$(localized_bandwidth_rating "$download_speed_rating")" "$(partial_sample_note "$download_rating" markdown)" \ + "$upload_score" "$(localized_bandwidth_rating "$upload_speed_rating")" "$(partial_sample_note "$upload_rating" markdown)" \ + "$readiness_score_before_cap" "$readiness_score_cap" "$readiness_score" fi printf '## Detected clients\n\n' printf '| Client | Model | Route | Source | Result |\n|---|---|---|---|---|\n' @@ -1571,9 +1884,12 @@ render_json() { warnings_json="$warnings_json$separator\"$(json_escape "$warning")\"" separator="," done < "$WARNINGS_FILE" - printf '{"schema_version":%s,"version":"%s","timestamp":"%s","proxy":"%s","result":"%s","reason":"%s","developer_readiness":{"ready":%s,"level":"%s","score":%s,"score_label":"%s","score_method":"rule_v1","score_breakdown":{"service_path":%s,"download":%s,"upload":%s},"summary":"%s","recommendation":"%s"},"reachable_endpoints":%s,"primary_success_rate_pct":%s,"primary_ttfb_median_ms":%s,"primary_jitter_ms":%s,"privacy":{"credentials_used":false,"billable_requests":false,"upload_payload":"zero-filled"},"warnings":[%s],"bandwidth":{"enabled":%s,"available":%s,"http_code":"%s","bytes":%s,"bytes_per_second":%s,"path":"%s","download":{"enabled":%s,"available":%s,"complete":%s,"http_code":"%s","bytes":%s,"bytes_per_second":%s,"mbps":%s,"rating":"%s"},"upload":{"enabled":%s,"available":%s,"complete":%s,"http_code":"%s","bytes":%s,"bytes_per_second":%s,"mbps":%s,"rating":"%s"}},"system":{"enabled":%s,"interface":"%s","base_rtt_ms":%s,"download_bits_per_second":%s,"upload_bits_per_second":%s,"proxy_state":"%s"},"services":[%s],"endpoints":[%s]}\n' \ + printf '{"schema_version":%s,"version":"%s","timestamp":"%s","proxy":"%s","result":"%s","reason":"%s","developer_readiness":{"ready":%s,"level":"%s","score":%s,"score_label":"%s","score_method":"rule_v2","score_breakdown":{"scored_service":"%s","service_path":%s,"availability":%s,"median_ttfb":%s,"p95":%s,"jitter":%s,"download":%s,"download_speed":%s,"download_partial_penalty":%s,"upload":%s,"upload_speed":%s,"upload_partial_penalty":%s,"pre_cap_total":%s,"verdict_cap":%s},"summary":"%s","recommendation":"%s"},"reachable_endpoints":%s,"primary_success_rate_pct":%s,"primary_ttfb_median_ms":%s,"primary_jitter_ms":%s,"privacy":{"credentials_used":false,"billable_requests":false,"upload_payload":"zero-filled"},"warnings":[%s],"bandwidth":{"enabled":%s,"available":%s,"http_code":"%s","bytes":%s,"bytes_per_second":%s,"path":"%s","download":{"enabled":%s,"available":%s,"complete":%s,"http_code":"%s","bytes":%s,"bytes_per_second":%s,"mbps":%s,"rating":"%s"},"upload":{"enabled":%s,"available":%s,"complete":%s,"http_code":"%s","bytes":%s,"bytes_per_second":%s,"mbps":%s,"rating":"%s"}},"system":{"enabled":%s,"interface":"%s","base_rtt_ms":%s,"download_bits_per_second":%s,"upload_bits_per_second":%s,"proxy_state":"%s"},"services":[%s],"endpoints":[%s]}\n' \ "$SCHEMA_VERSION" "$VERSION" "$timestamp" "$(json_escape "$proxy_text")" "$overall" "$(json_escape "$overall_reason")" \ - "$readiness_ready" "$readiness_level" "$readiness_score" "$(json_escape "$readiness_score_label_en")" "$readiness_score_base" "$download_score_adjustment" "$upload_score_adjustment" "$(json_escape "$readiness_summary_en")" "$(json_escape "$readiness_recommendation_en")" \ + "$readiness_ready" "$readiness_level" "$readiness_score" "$(json_escape "$readiness_score_label_en")" \ + "$(json_escape "$score_service")" "$score_service_path" "$score_availability" "$score_median_ttfb" "$score_p95" "$score_jitter" \ + "$download_score" "$download_speed_points" "$download_partial_penalty" "$upload_score" "$upload_speed_points" "$upload_partial_penalty" "$readiness_score_before_cap" "$readiness_score_cap" \ + "$(json_escape "$readiness_summary_en")" "$(json_escape "$readiness_recommendation_en")" \ "$reachable_count" "$primary_success_rate" "$primary_ttfb" "$primary_jitter" "$warnings_json" \ "$([ "$RUN_BANDWIDTH" -eq 1 ] && printf true || printf false)" "$legacy_bandwidth_available" "$legacy_bandwidth_code" "$legacy_bandwidth_bytes" "$legacy_bandwidth_speed" "$(json_escape "$proxy_text")" \ "$([ "$RUN_BANDWIDTH" -eq 1 ] && printf true || printf false)" "$([ "$bandwidth_valid" -eq 1 ] && printf true || printf false)" "$bandwidth_complete" "$bandwidth_code" "$bandwidth_bytes" "$bandwidth_speed" "$download_mbps" "$download_rating" \ diff --git a/demo/render_demo.py b/demo/render_demo.py index 59319a7..7d95317 100644 --- a/demo/render_demo.py +++ b/demo/render_demo.py @@ -22,7 +22,11 @@ "en": ROOT / "assets" / "ipcheck-demo.gif", "zh": ROOT / "assets" / "ipcheck-demo-zh.gif", } -WIDTH, HEIGHT = 1200, 720 +SCREENSHOTS = { + "en": ROOT / "assets" / "ipcheck-preview.png", + "zh": ROOT / "assets" / "ipcheck-preview-zh.png", +} +WIDTH, HEIGHT = 1200, 810 BACKGROUND = "#0d1117" PANEL = "#161b22" TEXT = "#c9d1d9" @@ -38,6 +42,7 @@ def font(size: int, language: str, bold: bool = False) -> ImageFont.FreeTypeFont: if language == "zh": candidates = [ + Path("/System/Library/Fonts/Supplemental/Arial Unicode.ttf"), Path("/System/Library/Fonts/PingFang.ttc"), Path("/System/Library/Fonts/Hiragino Sans GB.ttc"), Path("/System/Library/Fonts/STHeiti Medium.ttc"), @@ -130,10 +135,11 @@ def demo_output(language: str) -> list[str]: "HTTPS_PROXY": "http://127.0.0.1:1080", "IPCHECK_LANG": language, "IPCHECK_PROGRESS": "always", + "COLUMNS": "100", } ) process = subprocess.run( - [str(ROOT / "bin" / "ipcheck"), "all", "--samples", "1", "--no-color", "--explain-score"], + [str(ROOT / "bin" / "ipcheck"), "all", "--samples", "1", "--no-color"], cwd=ROOT, env=environment, text=True, @@ -145,55 +151,87 @@ def demo_output(language: str) -> list[str]: if language == "zh": report_prefixes = ( "ipcheck v", - "开发建议", - " 现在适合开发吗?", - " 开发适配分:", - " 评分依据:", + "━", + "─", + " ✓ 现在适合开发吗?", + " ✕ 现在适合开发吗?", + " 100/100", + " 9", + " 8", + " 7", + " 6", + " 5", + " 4", + " 3", + " 2", + " 1", + " 0/100", + " █", + " 评分构成", + " 服务链路", + " 下载", + " 上传", " 当前网络", - "检测到的客户端", + " 可以正常开发", + "◆ 检测到的客户端", " Codex", " Claude Code", - "AI 服务延迟", - " 首字节延迟", - " 正常", - "AI 服务结论", - "网络带宽", + "◆ AI 服务延迟", + " TTFB", + " ✓ 可达", + " HTTP", + "◆ AI 服务结论", + " ●", + "◆ 网络带宽", " 下载", " 上传", - " 建议", - "结果:", - "说明:", ) else: report_prefixes = ( "ipcheck v", - "Developer verdict", - " Ready to code?", - " Readiness score:", - " Score breakdown:", + "━", + "─", + " ✓ Ready to code?", + " ✕ Ready to code?", + " 100/100", + " 9", + " 8", + " 7", + " 6", + " 5", + " 4", + " 3", + " 2", + " 1", + " 0/100", + " █", + " Score breakdown", + " Service path", + " Download", + " Upload", " This network", - "Detected clients", + " You can work normally", + "◆ Detected clients", " Codex", " Claude Code", - "AI service latency", - " Time to first byte", - " OK", - "AI service results", - "Network bandwidth", + "◆ AI service latency", + " TTFB", + " ✓ REACH", + " HTTP", + "◆ AI service results", + " ●", + "◆ Network bandwidth", " Download", " Upload", - " Advice", - "Result:", - "Interpretation:", ) report = [line for line in process.stdout.splitlines() if line.startswith(report_prefixes)] return [ANSI_RE.sub("", line).removeprefix("⌨ ") for line in progress + report] def line_color(line: str) -> str: - if any(token in line for token in ("95/100", "GOOD", "FAST", "Ready to code? YES", "适合", "舒适", " 快 ")): + if line.strip().startswith("█") or any(token in line for token in ("100/100", "Ready to code? YES", "现在适合开发吗?适合")): return GREEN - if line.startswith(("Developer verdict", "Detected clients", "AI service latency", "AI service results", "Network bandwidth", "开发建议", "检测到的客户端", "AI 服务延迟", "AI 服务结论", "网络带宽")): + if line.startswith(("◆ Detected clients", "◆ AI service latency", "◆ AI service results", "◆ Network bandwidth", "◆ 检测到的客户端", "◆ AI 服务延迟", "◆ AI 服务结论", "◆ 网络带宽", "ipcheck v")): return CYAN if line.startswith(("Checking", "Press Ctrl+C", "按 Ctrl+C")): return MUTED @@ -213,6 +251,24 @@ def clip_line(draw: ImageDraw.ImageDraw, line: str, body_font: ImageFont.ImageFo return clipped + "..." +def draw_status_line( + draw: ImageDraw.ImageDraw, + line: str, + position: tuple[int, int], + body_font: ImageFont.ImageFont, +) -> bool: + colors = {"✓": GREEN, "●": GREEN, "✕": RED, "!": YELLOW} + if not any(symbol in line for symbol in colors): + return False + x, y = position + for segment in re.split(r"([✓●✕!])", line): + if not segment: + continue + draw.text((x, y), segment, font=body_font, fill=colors.get(segment, TEXT)) + x += int(draw.textlength(segment, font=body_font)) + return True + + def frame(command: str, lines: list[str], language: str) -> Image.Image: body_font = font(21, language) body_bold = font(21, language, bold=True) @@ -229,19 +285,24 @@ def frame(command: str, lines: list[str], language: str) -> Image.Image: draw.text((48, 98), "$", font=body_bold, fill=GREEN) draw.text((76, 98), command, font=body_font, fill=TEXT) - max_lines = 24 + max_lines = 28 visible = lines[-max_lines:] y = 138 for line in visible: + if line and set(line) in ({"━"}, {"─"}): + draw.line((48, y + 10, WIDTH - 48, y + 10), fill=MUTED, width=2) + y += 23 + continue clipped = clip_line(draw, line, body_font) - draw.text((48, y), clipped, font=body_font, fill=line_color(clipped)) + if not draw_status_line(draw, clipped, (48, y), body_font): + draw.text((48, y), clipped, font=body_font, fill=line_color(clipped)) y += 23 return image def render_demo(language: str) -> None: output = demo_output(language) - command = "ipcheck --lang zh --explain-score" if language == "zh" else "ipcheck --explain-score" + command = "ipcheck --lang zh" if language == "zh" else "ipcheck" frames: list[Image.Image] = [] durations: list[int] = [] @@ -267,7 +328,10 @@ def render_demo(language: str) -> None: optimize=True, disposal=1, ) + screenshot_path = SCREENSHOTS[language] + frame(command, output, language).save(screenshot_path, optimize=True) print(f"Rendered {output_path} ({output_path.stat().st_size / 1024:.0f} KiB, {len(frames)} frames)") + print(f"Rendered {screenshot_path} ({screenshot_path.stat().st_size / 1024:.0f} KiB)") def main() -> None: diff --git a/package.json b/package.json index 74b05de..8239e15 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@jacklv-coder/ipcheck", "private": true, - "version": "0.7.0", + "version": "0.8.0", "description": "Diagnose proxy, latency, and API routes used by Codex and Claude Code.", "license": "MIT", "author": "Jintao", diff --git a/test/smoke.sh b/test/smoke.sh index 1b4ac60..3e2da06 100755 --- a/test/smoke.sh +++ b/test/smoke.sh @@ -29,6 +29,7 @@ is_rate_limited=0 is_server_error=0 is_mixed_status=0 is_anthropic=0 +is_score_scenario=0 [ "${1-}" = "-q" ] || { printf 'curl was not invoked with -q first\n' >&2 exit 64 @@ -48,6 +49,7 @@ for argument in "$@"; do *rate-limit.invalid*) is_rate_limited=1 ;; *server-error.invalid*) is_server_error=1 ;; *mixed-status.invalid*) is_mixed_status=1 ;; + *score-scenario.invalid*) is_score_scenario=1 ;; *anthropic*|*dashscope*|*/v1/messages*) is_anthropic=1 ;; esac done @@ -92,9 +94,21 @@ elif [ "$is_mixed_status" -eq 1 ]; then else printf '503\t0.001\t0.002\t0.003\t0.050\t0.050\t100\t1000' fi +elif [ "$is_score_scenario" -eq 1 ]; then + attempt_file="${IPCHECK_TEST_SCORE_FILE:?}" + attempt=$(($(sed -n '1p' "$attempt_file" 2>/dev/null || printf 0) + 1)) + printf '%s' "$attempt" > "$attempt_file" + case "$attempt" in + 1) first_byte=8.108 ;; + 2) first_byte=4.881 ;; + *) first_byte=2.250 ;; + esac + printf '401\t0.001\t0.002\t0.003\t%s\t%s\t151\t1000' "$first_byte" "$first_byte" elif [ "$is_anthropic" -eq 1 ]; then - first_byte=${IPCHECK_TEST_TTFB:-0.100} - printf '403\t0.001\t0.002\t0.003\t%s\t%s\t151\t1000' "$first_byte" "$first_byte" + [ "${IPCHECK_TEST_ANTHROPIC_BLOCKED:-0}" -eq 0 ] || exit 28 + first_byte=${IPCHECK_TEST_ANTHROPIC_TTFB:-${IPCHECK_TEST_TTFB:-0.100}} + anthropic_code=${IPCHECK_TEST_ANTHROPIC_CODE:-403} + printf '%s\t0.001\t0.002\t0.003\t%s\t%s\t151\t1000' "$anthropic_code" "$first_byte" "$first_byte" else first_byte=${IPCHECK_TEST_TTFB:-0.100} printf '401\t0.001\t0.002\t0.003\t%s\t%s\t151\t1000' "$first_byte" "$first_byte" @@ -185,7 +199,7 @@ run_ipcheck_direct() { bash -n "$PROJECT_DIR/bin/ipcheck" "$PROJECT_DIR/bin/ipcheck" --help | grep -q '^ipcheck - diagnose Codex and Claude Code' -[ "$("$PROJECT_DIR/bin/ipcheck" --version)" = "ipcheck 0.7.0" ] +[ "$("$PROJECT_DIR/bin/ipcheck" --version)" = "ipcheck 0.8.0" ] "$PROJECT_DIR/bin/ipcheck" --help | grep -q -- '--explain-score' : > "$CURL_LOG" @@ -202,7 +216,8 @@ printf '%s\n' "$report" | grep -q 'https://dashscope.aliyuncs.com/apps/anthropic printf '%s\n' "$report" | grep -q '"credentials_used":false' printf '%s\n' "$report" | grep -q '"billable_requests":false' printf '%s\n' "$report" | grep -q '"developer_readiness":{"ready":true,"level":"ready"' -printf '%s\n' "$report" | grep -q '"score":90,"score_label":"COMFORTABLE","score_method":"rule_v1"' +printf '%s\n' "$report" | grep -q '"score":90,"score_label":"COMFORTABLE","score_method":"rule_v2"' +printf '%s\n' "$report" | grep -q '"score_breakdown":{"scored_service":"codex","service_path":90,"availability":35,"median_ttfb":35,"p95":10,"jitter":10' if printf '%s\n' "$report" | grep -Eq 'fixture-secret|runtime-secret'; then printf 'Claude credential leaked into JSON report\n' >&2 exit 1 @@ -218,6 +233,33 @@ if command -v python3 >/dev/null 2>&1; then REPORT_JSON="$report" python3 -c 'import json, os; json.loads(os.environ["REPORT_JSON"])' fi +mixed_client_quality=$(IPCHECK_TEST_ANTHROPIC_TTFB=4.000 run_ipcheck --samples 1 --no-bandwidth --json) +printf '%s\n' "$mixed_client_quality" | grep -q '"result":"poor"' +printf '%s\n' "$mixed_client_quality" | grep -q '"score":63,"score_label":"LIMITED","score_method":"rule_v2"' +printf '%s\n' "$mixed_client_quality" | grep -q '"scored_service":"claude","service_path":63' + +for anthropic_status in 429 503; do + mixed_service_error=$(IPCHECK_TEST_ANTHROPIC_CODE="$anthropic_status" run_ipcheck --samples 1 --no-bandwidth --json) + printf '%s\n' "$mixed_service_error" | grep -q '"result":"fair"' + printf '%s\n' "$mixed_service_error" | grep -q '"score":64,"score_label":"LIMITED","score_method":"rule_v2"' + printf '%s\n' "$mixed_service_error" | grep -q '"scored_service":"claude","service_path":90' + printf '%s\n' "$mixed_service_error" | grep -q '"pre_cap_total":90,"verdict_cap":64' +done + +for anthropic_status in 429 503; do + mixed_fair_unavailable=$(IPCHECK_TEST_TTFB=1.000 IPCHECK_TEST_ANTHROPIC_CODE="$anthropic_status" run_ipcheck --samples 1 --no-bandwidth --json) + printf '%s\n' "$mixed_fair_unavailable" | grep -q '"result":"fair"' + printf '%s\n' "$mixed_fair_unavailable" | grep -q '"score":64,"score_label":"LIMITED","score_method":"rule_v2"' + printf '%s\n' "$mixed_fair_unavailable" | grep -q '"scored_service":"claude","service_path":85' + printf '%s\n' "$mixed_fair_unavailable" | grep -q '"pre_cap_total":85,"verdict_cap":64' +done + +mixed_blocked=$(IPCHECK_TEST_ANTHROPIC_BLOCKED=1 run_ipcheck --samples 1 --json) +printf '%s\n' "$mixed_blocked" | grep -q '"result":"poor"' +printf '%s\n' "$mixed_blocked" | grep -q '"score":0,"score_label":"LIMITED","score_method":"rule_v2"' +printf '%s\n' "$mixed_blocked" | grep -q '"scored_service":"claude","service_path":0' +printf '%s\n' "$mixed_blocked" | grep -q '"pre_cap_total":10,"verdict_cap":0' + AUTO_HOME="$STUB_DIR/auto-home" mkdir -p "$AUTO_HOME/codex" "$AUTO_HOME/claude" : > "$CURL_LOG" @@ -281,11 +323,23 @@ printf '%s\n' "$decimal_report" | grep -q '"successful_samples":8' fair_report=$(IPCHECK_TEST_TTFB=1.000 run_ipcheck --samples 1 --no-bandwidth --endpoint https://fair.invalid --json) printf '%s\n' "$fair_report" | grep -q '"result":"fair"' printf '%s\n' "$fair_report" | grep -q '"level":"with_caution"' -printf '%s\n' "$fair_report" | grep -q '"score":70,"score_label":"USABLE"' +printf '%s\n' "$fair_report" | grep -q '"score":85,"score_label":"GOOD"' fair_fast_bandwidth_report=$(IPCHECK_TEST_TTFB=1.000 run_ipcheck --samples 1 --endpoint https://fair.invalid --json) -printf '%s\n' "$fair_fast_bandwidth_report" | grep -q '"score":75,"score_label":"GOOD"' +printf '%s\n' "$fair_fast_bandwidth_report" | grep -q '"score":89,"score_label":"GOOD"' fair_slow_bandwidth_report=$(IPCHECK_TEST_TTFB=1.000 IPCHECK_TEST_BANDWIDTH_SPEED=100000 IPCHECK_TEST_UPLOAD_SPEED=100000 run_ipcheck --samples 1 --endpoint https://fair.invalid --json) -printf '%s\n' "$fair_slow_bandwidth_report" | grep -q '"score":60,"score_label":"USABLE"' +printf '%s\n' "$fair_slow_bandwidth_report" | grep -q '"score":75,"score_label":"GOOD"' + +just_below_poor=$(IPCHECK_TEST_TTFB=2.999 run_ipcheck --samples 1 --no-bandwidth --endpoint https://boundary.invalid --json) +printf '%s\n' "$just_below_poor" | grep -q '"score":74,"score_label":"USABLE"' +at_poor_boundary=$(IPCHECK_TEST_TTFB=3.000 run_ipcheck --samples 1 --no-bandwidth --endpoint https://boundary.invalid --json) +printf '%s\n' "$at_poor_boundary" | grep -q '"score":64,"score_label":"LIMITED"' + +SCORE_SCENARIO_FILE="$STUB_DIR/score-scenario-attempt" +: > "$SCORE_SCENARIO_FILE" +score_scenario=$(IPCHECK_TEST_SCORE_FILE="$SCORE_SCENARIO_FILE" IPCHECK_TEST_BANDWIDTH_SPEED=400000 IPCHECK_TEST_UPLOAD_SPEED=425000 run_ipcheck --samples 3 --endpoint https://score-scenario.invalid --json) +printf '%s\n' "$score_scenario" | grep -q '"result":"poor"' +printf '%s\n' "$score_scenario" | grep -q '"score":47,"score_label":"LIMITED","score_method":"rule_v2"' +printf '%s\n' "$score_scenario" | grep -q '"service_path":49,"availability":35,"median_ttfb":14,"p95":0,"jitter":0,"download":-5' fair_human=$(IPCHECK_TEST_TTFB=1.000 IPCHECK_LANG=en run_ipcheck --samples 1 --no-bandwidth --no-progress --endpoint https://fair.invalid) printf '%s\n' "$fair_human" | grep -q 'Ready to code? YES, WITH CAUTION' @@ -293,8 +347,13 @@ printf '%s\n' "$fair_human" | grep -q 'Ready to code? YES, WITH CAUTION' chinese_human=$(IPCHECK_LANG=zh run_ipcheck --samples 1 --no-bandwidth --no-progress --endpoint https://language.invalid) printf '%s\n' "$chinese_human" | grep -q '现在适合开发吗?适合' printf '%s\n' "$chinese_human" | grep -q '当前网络适合进行 AI 辅助开发' -printf '%s\n' "$chinese_human" | grep -q '^AI 服务延迟$' -printf '%s\n' "$chinese_human" | grep -q '首字节延迟(TTFB)' +printf '%s\n' "$chinese_human" | grep -q '◆ AI 服务延迟' +printf '%s\n' "$chinese_human" | grep -q 'TTFB:中位/P95' +printf '%s\n' "$chinese_human" | grep -q '✓ 可达' +if printf '%s\n' "$chinese_human" | grep -q '正常.*Custom endpoint'; then + printf 'reachable endpoint was misleadingly labelled normal\n' >&2 + exit 1 +fi english_override=$(LANG=zh_CN.UTF-8 IPCHECK_LANG=en run_ipcheck --samples 1 --no-bandwidth --no-progress --endpoint https://language.invalid) printf '%s\n' "$english_override" | grep -q 'Ready to code? YES' @@ -327,6 +386,19 @@ case "$colored_human" in *) printf 'forced color output did not contain ANSI color codes\n' >&2; exit 1 ;; esac +plain_human=$(IPCHECK_LANG=en run_ipcheck --samples 1 --no-bandwidth --no-color --endpoint https://plain.invalid) +case "$plain_human" in + *"$(printf '\033[')"*) printf 'no-color output contained ANSI escape codes\n' >&2; exit 1 ;; +esac +printf '%s\n' "$plain_human" | grep -q '█' + +narrow_human=$(COLUMNS=70 IPCHECK_LANG=en run_ipcheck --samples 1 --no-bandwidth --no-color --endpoint https://narrow.invalid) +printf '%s\n' "$narrow_human" | grep -q '^ HTTP 401 · 1/1 · median' +printf '%s\n' "$narrow_human" | grep -q '█████████████░' + +wide_human=$(COLUMNS=120 IPCHECK_LANG=en run_ipcheck --samples 1 --no-bandwidth --no-color --endpoint https://wide.invalid) +printf '%s\n' "$wide_human" | grep -q '✓ REACH.*HTTP 401.*median' + attempt_file="$STUB_DIR/attempt" printf 0 > "$attempt_file" flaky_report=$(IPCHECK_TEST_ATTEMPT_FILE="$attempt_file" run_ipcheck --samples 5 --no-bandwidth --endpoint https://flaky.invalid --json) @@ -336,7 +408,7 @@ printf '%s\n' "$flaky_report" | grep -q '"successful_samples":1' not_found_report=$(run_ipcheck --samples 1 --no-bandwidth --endpoint https://not-found.invalid/v1/messages --json) printf '%s\n' "$not_found_report" | grep -q '"result":"poor"' -printf '%s\n' "$not_found_report" | grep -q '"score":35,"score_label":"LIMITED"' +printf '%s\n' "$not_found_report" | grep -q '"score":64,"score_label":"LIMITED"' printf '%s\n' "$not_found_report" | grep -q 'configured API route returned HTTP 404' rate_limit_report=$(run_ipcheck --samples 1 --no-bandwidth --endpoint https://rate-limit.invalid --json) @@ -345,7 +417,7 @@ printf '%s\n' "$rate_limit_report" | grep -q '"ready":false,"level":"temporarily server_error_report=$(run_ipcheck --samples 1 --no-bandwidth --endpoint https://server-error.invalid --json) printf '%s\n' "$server_error_report" | grep -q '"result":"fair"' printf '%s\n' "$server_error_report" | grep -q '"ready":false,"level":"temporarily_unavailable"' -printf '%s\n' "$server_error_report" | grep -q '"score":45,"score_label":"LIMITED"' +printf '%s\n' "$server_error_report" | grep -q '"score":64,"score_label":"LIMITED"' server_error_progress="$STUB_DIR/server-error-progress.log" IPCHECK_PROGRESS=always run_ipcheck --samples 1 --no-bandwidth --endpoint https://server-error.invalid >/dev/null 2>"$server_error_progress" @@ -472,29 +544,33 @@ printf '%s\n' "$socks_report" | grep -q 'Claude Code does not document ALL_PROXY grep -q '^env:ALL_PROXY=$' "$CURL_LOG" bandwidth_report=$(run_ipcheck --samples 1 --json) -printf '%s\n' "$bandwidth_report" | grep -q '"score":95,"score_label":"COMFORTABLE","score_method":"rule_v1"' -printf '%s\n' "$bandwidth_report" | grep -q '"score_breakdown":{"service_path":90,"download":2,"upload":3}' +printf '%s\n' "$bandwidth_report" | grep -q '"score":100,"score_label":"COMFORTABLE","score_method":"rule_v2"' +printf '%s\n' "$bandwidth_report" | grep -q '"score_breakdown":{"scored_service":"codex","service_path":90,"availability":35,"median_ttfb":35,"p95":10,"jitter":10,"download":5,"download_speed":5,"download_partial_penalty":0,"upload":5' printf '%s\n' "$bandwidth_report" | grep -q '"bandwidth":{"enabled":true,"available":true,"http_code":"200"' printf '%s\n' "$bandwidth_report" | grep -q '"download":{"enabled":true,"available":true,"complete":true,"http_code":"200","bytes":2000000,"bytes_per_second":10000000,"mbps":80.0,"rating":"fast"}' printf '%s\n' "$bandwidth_report" | grep -q '"upload":{"enabled":true,"available":true,"complete":true,"http_code":"200","bytes":1000000,"bytes_per_second":2000000,"mbps":16.0,"rating":"fast"}' printf '%s\n' "$bandwidth_report" | grep -q '"upload_payload":"zero-filled"' bandwidth_human=$(IPCHECK_LANG=zh run_ipcheck --samples 1) -printf '%s\n' "$bandwidth_human" | grep -q '^网络带宽$' -printf '%s\n' "$bandwidth_human" | grep -q '开发适配分:95/100 · 舒适' +printf '%s\n' "$bandwidth_human" | grep -q '◆ 网络带宽' +printf '%s\n' "$bandwidth_human" | grep -q '100/100.*舒适' printf '%s\n' "$bandwidth_human" | grep -q '下载 80.0 Mbps.*快.*Cloudflare,最多 2 MB' printf '%s\n' "$bandwidth_human" | grep -q '上传 16.0 Mbps.*快.*Cloudflare,最多 1 MB 零字节' printf '%s\n' "$bandwidth_human" | grep -q '路径 HTTPS_PROXY=http://127.0.0.1:1080' score_explanation=$(IPCHECK_LANG=en run_ipcheck --samples 1 --explain-score) -printf '%s\n' "$score_explanation" | grep -q 'Score breakdown: service path 90; download FAST +2; upload FAST +3; total 95.' +printf '%s\n' "$score_explanation" | grep -q '^ Score breakdown$' +printf '%s\n' "$score_explanation" | grep -q 'Service path.*90/90.*reachability 35.*TTFB 35.*P95 10.*jitter 10' +printf '%s\n' "$score_explanation" | grep -q 'Download.*+5.*80.0 Mbps.*FAST' +printf '%s\n' "$score_explanation" | grep -q 'Upload.*+5.*16.0 Mbps.*FAST' score_explanation_zh=$(IPCHECK_LANG=zh run_ipcheck --samples 1 --markdown --explain-score) -printf '%s\n' "$score_explanation_zh" | grep -q '评分依据:服务链路 90;下载 快 +2;上传 快 +3;总分 95。' +printf '%s\n' "$score_explanation_zh" | grep -q '评分依据:服务链路 90/90(可达性 35 + TTFB 35 + P95 10 + 抖动 10);下载 5(快);上传 5(快);限幅前 100;结论上限 100;总分 100。' score_explanation_without_bandwidth=$(IPCHECK_LANG=en run_ipcheck --samples 1 --no-bandwidth --explain-score) -printf '%s\n' "$score_explanation_without_bandwidth" | grep -q 'Score breakdown: service path 90; download UNAVAILABLE +0; upload UNAVAILABLE +0; total 90.' +printf '%s\n' "$score_explanation_without_bandwidth" | grep -q 'Download.*0.*0.0 Mbps.*UNAVAILABLE' +printf '%s\n' "$score_explanation_without_bandwidth" | grep -q 'Upload.*0.*0.0 Mbps.*UNAVAILABLE' slow_upload_human=$(IPCHECK_TEST_UPLOAD_SPEED=100000 IPCHECK_LANG=en run_ipcheck --samples 1 --explain-score) printf '%s\n' "$slow_upload_human" | grep -q 'Upload.*0.8 Mbps.*SLOW' -printf '%s\n' "$slow_upload_human" | grep -q 'Readiness score: 87/100 · GOOD' -printf '%s\n' "$slow_upload_human" | grep -q 'upload SLOW -5; total 87.' +printf '%s\n' "$slow_upload_human" | grep -q '90/100.*COMFORTABLE' +printf '%s\n' "$slow_upload_human" | grep -q 'Upload.*-5.*0.8 Mbps.*SLOW' printf '%s\n' "$slow_upload_human" | grep -q 'Upload is slow; sending large code contexts may take longer.' invalid_bandwidth_report=$(IPCHECK_TEST_BANDWIDTH_CODE=407 run_ipcheck --samples 1 --json) printf '%s\n' "$invalid_bandwidth_report" | grep -q '"bandwidth":{"enabled":true,"available":false,"http_code":"407","bytes":0,"bytes_per_second":0,' @@ -506,12 +582,18 @@ printf '%s\n' "$no_upload_report" | grep -q '"upload":{"enabled":false,"availabl partial_upload_report=$(IPCHECK_TEST_UPLOAD_BYTES=500000 IPCHECK_TEST_UPLOAD_EXIT=28 run_ipcheck --samples 1 --json) printf '%s\n' "$partial_upload_report" | grep -q '"upload":{"enabled":true,"available":true,"complete":false,"http_code":"200","bytes":500000,"bytes_per_second":2000000,"mbps":16.0,"rating":"partial"}' -printf '%s\n' "$partial_upload_report" | grep -q '"score":90,"score_label":"COMFORTABLE"' +printf '%s\n' "$partial_upload_report" | grep -q '"score":98,"score_label":"COMFORTABLE"' partial_download_report=$(IPCHECK_TEST_BANDWIDTH_BYTES=500000 IPCHECK_TEST_BANDWIDTH_EXIT=28 run_ipcheck --samples 1 --json) printf '%s\n' "$partial_download_report" | grep -q '"bandwidth":{"enabled":true,"available":false,"http_code":"000","bytes":0,"bytes_per_second":0,' printf '%s\n' "$partial_download_report" | grep -q '"download":{"enabled":true,"available":true,"complete":false,"http_code":"200","bytes":500000,"bytes_per_second":10000000,"mbps":80.0,"rating":"partial"}' +complete_slow_download=$(IPCHECK_TEST_BANDWIDTH_SPEED=12500 run_ipcheck --samples 1 --json) +printf '%s\n' "$complete_slow_download" | grep -q '"score":90,"score_label":"COMFORTABLE"' +partial_slow_download=$(IPCHECK_TEST_BANDWIDTH_SPEED=12500 IPCHECK_TEST_BANDWIDTH_BYTES=500000 IPCHECK_TEST_BANDWIDTH_EXIT=28 run_ipcheck --samples 1 --json) +printf '%s\n' "$partial_slow_download" | grep -q '"score":88,"score_label":"GOOD"' +printf '%s\n' "$partial_slow_download" | grep -q '"download":-7,"download_speed":-5,"download_partial_penalty":-2' + partial_human=$(IPCHECK_TEST_UPLOAD_BYTES=500000 IPCHECK_TEST_UPLOAD_EXIT=28 IPCHECK_LANG=en run_ipcheck --samples 1) printf '%s\n' "$partial_human" | grep -q 'Upload.*16.0 Mbps.*ESTIMATE' printf '%s\n' "$partial_human" | grep -q 'partial estimates and do not prove bandwidth is sufficient' @@ -598,7 +680,7 @@ set -e [ "$blocked_exit" -eq 1 ] printf '%s\n' "$blocked_report" | grep -q '"http_code":"000"' printf '%s\n' "$blocked_report" | grep -q '"bandwidth":{"enabled":false,"available":false,"http_code":"000"' -printf '%s\n' "$blocked_report" | grep -q '"score":0,"score_label":"LIMITED","score_method":"rule_v1"' +printf '%s\n' "$blocked_report" | grep -q '"score":0,"score_label":"LIMITED","score_method":"rule_v2"' markdown=$(run_ipcheck --samples 1 --no-bandwidth --markdown) printf '%s\n' "$markdown" | grep -q '^# ipcheck: AI Coding Network Report'