diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f8efdd9..b8d6a7d 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.3.0 + placeholder: ipcheck 0.4.0 validations: required: true - type: dropdown diff --git a/CHANGELOG.md b/CHANGELOG.md index 52fd62f..ab51c6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,23 @@ All notable changes to ipcheck are documented here. The project follows [Semantic Versioning](https://semver.org/). +## [0.4.0] - 2026-07-22 + +### Added + +- Live service and sample progress on interactive terminals, with a quiet mode + for scripts and structured reports. +- Automatic English/Chinese output based on the terminal or macOS system + language, plus `--lang` and `IPCHECK_LANG` overrides. +- A plain-language developer readiness verdict in human, Markdown, and JSON + reports. +- TTY-aware color output with `--color`, `--no-color`, and `NO_COLOR` support. + +### Changed + +- Human reports now lead with an actionable answer and recommendation. +- Numeric output always uses a dot decimal separator, independent of locale. + ## [0.3.0] - 2026-07-22 ### Added @@ -37,5 +54,6 @@ 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.4.0]: https://github.com/jacklv-coder/ipcheck/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/jacklv-coder/ipcheck/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/jacklv-coder/ipcheck/releases/tag/v0.2.0 diff --git a/README.md b/README.md index 30d7c61..7ea48d8 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,16 @@ 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. +`GOOD`, `FAIR`, `POOR`, or `BLOCKED` result. Interactive runs show live, +color-coded progress and finish with a direct answer to “Ready to code?”. ```text $ ipcheck --quick -ipcheck v0.3.0 — AI coding network diagnostics +ipcheck v0.4.0 — AI coding network diagnostics + +Developer verdict + Ready to code? YES, WITH CAUTION + Development is possible, but responses may feel slower than ideal. Detected clients Codex model=gpt-5.6-sol, route=https://chatgpt.com + https://api.openai.com @@ -62,7 +67,7 @@ brew install ipcheck ```bash mkdir -p "$HOME/.local/bin" -curl -fsSL https://raw.githubusercontent.com/jacklv-coder/ipcheck/v0.3.0/bin/ipcheck \ +curl -fsSL https://raw.githubusercontent.com/jacklv-coder/ipcheck/v0.4.0/bin/ipcheck \ -o "$HOME/.local/bin/ipcheck" chmod +x "$HOME/.local/bin/ipcheck" ``` @@ -101,8 +106,17 @@ ipcheck --samples 10 ipcheck --timeout 30 ipcheck --system ipcheck --endpoint https://your-gateway.example.com/health +ipcheck --lang zh +ipcheck --no-progress --no-color ``` +The human and Markdown reports default to the terminal/system language. +English and Simplified Chinese are currently supported. Override detection with +`--lang en`, `--lang zh`, or `IPCHECK_LANG=en|zh`. JSON field names, enum values, +and diagnostic reasons remain stable English for automation. Progress is written +to stderr only for human output and can be controlled with +`IPCHECK_PROGRESS=auto|always|never`. + Run `ipcheck --help` for the complete option and environment-variable list. ## Supported clients and routes diff --git a/README.zh-CN.md b/README.zh-CN.md index 6e5926d..e1918cd 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -11,11 +11,16 @@ `ipcheck` 是一个零依赖 Bash 命令行工具,面向 **Codex** 和 **Claude Code** 的真实网络路径。它会自动识别本机客户端以及不含密钥的路由 配置,测量可达性、首字节延迟(TTFB)中位数/P95、抖动和参考带宽,并给出 -明确的 `GOOD`、`FAIR`、`POOR` 或 `BLOCKED` 结论。 +明确的 `GOOD`、`FAIR`、`POOR` 或 `BLOCKED` 结论。交互运行时会显示带颜色的 +实时进度,并直接告诉你“现在是否适合开发”。 ```text $ ipcheck --quick -ipcheck v0.3.0 — AI coding network diagnostics +ipcheck v0.4.0 — AI 编程网络诊断 + +开发建议 + 现在适合开发吗?可以,但会有些慢 + 当前可以开发,但响应速度可能不够理想。 Detected clients Codex model=gpt-5.6-sol, route=https://chatgpt.com + https://api.openai.com @@ -56,7 +61,7 @@ brew install ipcheck ```bash mkdir -p "$HOME/.local/bin" -curl -fsSL https://raw.githubusercontent.com/jacklv-coder/ipcheck/v0.3.0/bin/ipcheck \ +curl -fsSL https://raw.githubusercontent.com/jacklv-coder/ipcheck/v0.4.0/bin/ipcheck \ -o "$HOME/.local/bin/ipcheck" chmod +x "$HOME/.local/bin/ipcheck" ``` @@ -93,8 +98,15 @@ ipcheck --samples 10 ipcheck --timeout 30 ipcheck --system ipcheck --endpoint https://your-gateway.example.com/health +ipcheck --lang en +ipcheck --no-progress --no-color ``` +终端和 Markdown 报告默认跟随终端/系统语言,目前支持中文和英文。可以使用 +`--lang zh`、`--lang en`,或 `IPCHECK_LANG=zh|en` 明确指定。JSON 的字段名、 +枚举值和诊断原因始终保持英文,方便自动化脚本稳定解析。实时进度只会在普通 +终端报告中写入 stderr,可通过 `IPCHECK_PROGRESS=auto|always|never` 控制。 + 运行 `ipcheck --help` 可以查看完整参数。 ## 支持的客户端与配置 diff --git a/bin/ipcheck b/bin/ipcheck index ed6fb58..51e8695 100755 --- a/bin/ipcheck +++ b/bin/ipcheck @@ -4,7 +4,13 @@ set -u -VERSION="0.3.0" +# Keep machine-readable numbers portable without losing the user's language hint. +IPCHECK_SYSTEM_LOCALE="${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" +unset LC_ALL +LC_NUMERIC=C +export LC_NUMERIC + +VERSION="0.4.0" SCHEMA_VERSION=1 SAMPLES=3 TIMEOUT=15 @@ -15,6 +21,9 @@ BANDWIDTH_URL="https://speed.cloudflare.com/__down?bytes=2000000" MODE="human" RUN_BANDWIDTH=1 RUN_SYSTEM=0 +COLOR_MODE="auto" +PROGRESS_MODE="${IPCHECK_PROGRESS:-auto}" +LANG_MODE="${IPCHECK_LANG:-auto}" usage() { cat <<'EOF' @@ -33,6 +42,11 @@ Options: --quick Use 1 sample, an 8s timeout, and skip bandwidth. --no-bandwidth Skip the 2 MB reference download test. --system Also run macOS networkQuality when available. + --color Always use terminal colors in human output. + --no-color Disable terminal colors. + --progress Always emit human-readable progress to stderr. + --no-progress Disable progress output. + --lang LANG Output language: auto, en, or zh (default: auto). -v, --version Show the ipcheck version. -h, --help Show this help. @@ -44,6 +58,9 @@ Environment: ANTHROPIC_BASE_URL Claude Code gateway base URL. HTTPS_PROXY/HTTP_PROXY Standard proxy settings respected by the clients. ALL_PROXY Also respected by curl; see Claude warnings in output. + NO_COLOR Disable terminal colors when set. + IPCHECK_PROGRESS auto, always, or never (default: auto). + IPCHECK_LANG auto, en, or zh (default: auto). No API key or authentication token is used, transmitted, or printed. EOF @@ -119,6 +136,15 @@ while [ "$#" -gt 0 ]; do ;; --no-bandwidth) RUN_BANDWIDTH=0 ;; --system) RUN_SYSTEM=1 ;; + --color) COLOR_MODE="always" ;; + --no-color) COLOR_MODE="never" ;; + --progress) PROGRESS_MODE="always" ;; + --no-progress) PROGRESS_MODE="never" ;; + --lang) + [ "$#" -ge 2 ] || die "--lang requires auto, en, or zh" + LANG_MODE="$2" + shift + ;; -v|--version) printf 'ipcheck %s\n' "$VERSION"; exit 0 ;; -h|--help) usage; exit 0 ;; *) die "unknown option: $1 (use --help)" ;; @@ -126,6 +152,150 @@ while [ "$#" -gt 0 ]; do shift done +case "$PROGRESS_MODE" in + auto|always|never) ;; + *) die "IPCHECK_PROGRESS must be auto, always, or never" ;; +esac + +case "$LANG_MODE" in + auto|en|zh) ;; + *) die "language must be auto, en, or zh" ;; +esac + +UI_LANG="$LANG_MODE" +if [ "$UI_LANG" = "auto" ]; then + locale_hint="$IPCHECK_SYSTEM_LOCALE" + case "$locale_hint" in + zh*|ZH*) UI_LANG="zh" ;; + en*|EN*) UI_LANG="en" ;; + *) + UI_LANG="en" + if [ "$(uname -s 2>/dev/null)" = "Darwin" ] && command -v defaults >/dev/null 2>&1; then + apple_language=$(defaults read -g AppleLanguages 2>/dev/null | sed -n 's/^[[:space:]]*"\([^"-]*\).*$/\1/p' | head -n 1) + case "$apple_language" in + zh*|ZH*) UI_LANG="zh" ;; + esac + fi + ;; + esac +fi + +USE_COLOR=0 +case "$COLOR_MODE" in + always) USE_COLOR=1 ;; + auto) + if [ "$MODE" = "human" ] && [ -t 1 ] && [ -z "${NO_COLOR+x}" ] && [ "${TERM-}" != "dumb" ]; then + USE_COLOR=1 + fi + ;; +esac + +if [ "$USE_COLOR" -eq 1 ]; then + C_RESET=$(printf '\033[0m') + C_BOLD=$(printf '\033[1m') + C_RED=$(printf '\033[31m') + C_GREEN=$(printf '\033[32m') + C_YELLOW=$(printf '\033[33m') + C_CYAN=$(printf '\033[36m') +else + C_RESET="" + C_BOLD="" + C_RED="" + C_GREEN="" + C_YELLOW="" + C_CYAN="" +fi + +PROGRESS_TTY=0 +if [ -t 2 ] && [ "${TERM-}" != "dumb" ]; then + PROGRESS_TTY=1 +fi +SHOW_PROGRESS=0 +if [ "$MODE" = "human" ]; then + case "$PROGRESS_MODE" in + always) SHOW_PROGRESS=1 ;; + auto) [ "$PROGRESS_TTY" -eq 0 ] || SHOW_PROGRESS=1 ;; + esac +fi + +ui_service_name() { + case "$1" in + codex) printf 'Codex' ;; + claude) printf 'Claude Code' ;; + custom) printf 'Custom' ;; + *) printf '%s' "$1" ;; + esac +} + +progress_note() { + [ "$SHOW_PROGRESS" -eq 1 ] || return 0 + printf '%s%s%s %s\n' "$C_CYAN" "$1" "$C_RESET" "$2" >&2 +} + +progress_sample() { + local progress_message + [ "$SHOW_PROGRESS" -eq 1 ] || return 0 + if [ "$UI_LANG" = "zh" ]; then + progress_message=$(printf '正在测试 %s · %s — 第 %s/%s 次(单次超时 %s 秒)' \ + "$(ui_service_name "$1")" "$2" "$3" "$4" "$TIMEOUT") + else + progress_message=$(printf 'Testing %s · %s — sample %s/%s (timeout %ss)' \ + "$(ui_service_name "$1")" "$2" "$3" "$4" "$TIMEOUT") + fi + if [ "$PROGRESS_TTY" -eq 1 ]; then + printf '\r\033[2K %s●%s %s' "$C_CYAN" "$C_RESET" "$progress_message" >&2 + else + printf ' %s\n' "$progress_message" >&2 + fi +} + +progress_finish() { + local color icon + [ "$SHOW_PROGRESS" -eq 1 ] || return 0 + if [ "$PROGRESS_TTY" -eq 1 ]; then + printf '\r\033[2K' >&2 + fi + case "$1" in + ok) color="$C_GREEN"; icon="✓" ;; + warn) color="$C_YELLOW"; icon="!" ;; + *) color="$C_RED"; icon="✗" ;; + esac + printf ' %s%s%s %s — %s\n' "$color" "$icon" "$C_RESET" "$2" "$3" >&2 +} + +localized_reason() { + if [ "$UI_LANG" != "zh" ]; then + printf '%s' "$1" + return + fi + case "$1" in + "No HTTP response reached the primary service endpoint.") printf '未能从主要服务端点收到 HTTP 响应。' ;; + "The proxy requires authentication (HTTP 407), so the service endpoint was not reached.") printf '代理要求身份验证(HTTP 407),尚未到达服务端点。' ;; + "The host is reachable, but the configured API route returned HTTP 404 in at least one sample. Check the base URL.") printf '主机可以访问,但配置的 API 路由至少一次返回 HTTP 404,请检查 Base URL。' ;; + "The primary service path is intermittent or unavailable for most samples.") printf '主要服务链路时断时续,或大多数测试均不可用。' ;; + "The API route is reachable, but at least one sample was rate limited (HTTP 429).") printf 'API 路由可以访问,但至少一次测试触发了限流(HTTP 429)。' ;; + "The API route is reachable, but at least one sample returned a server error.") printf 'API 路由可以访问,但至少一次测试返回服务器错误。' ;; + "The service path is reachable, but one or more sampled requests failed.") printf '服务链路可以访问,但一次或多次测试请求失败。' ;; + "The service path is reachable with acceptable first-byte latency and jitter.") printf '服务链路正常,首字节延迟和抖动都在适合开发的范围内。' ;; + "The service path is usable, but interactive responses may feel delayed.") printf '服务链路可用,但交互响应可能会感觉偏慢。' ;; + "The service path is reachable but has high latency or unstable responses.") printf '服务链路可以访问,但延迟较高或响应不稳定。' ;; + *) printf '%s' "$1" ;; + esac +} + +localized_warning() { + if [ "$UI_LANG" != "zh" ]; then + printf '%s' "$1" + return + fi + case "$1" in + "Claude Code does not support SOCKS proxies; configure an HTTP/HTTPS proxy or gateway.") printf 'Claude Code 不支持 SOCKS 代理;请配置 HTTP/HTTPS 代理或网关。' ;; + "Claude Code does not document ALL_PROXY; Claude probes ignore it unless HTTPS_PROXY or HTTP_PROXY is also configured.") printf 'Claude Code 未声明支持 ALL_PROXY;除非同时配置 HTTPS_PROXY 或 HTTP_PROXY,否则 Claude 测试会忽略它。' ;; + "A macOS system proxy is configured, but Claude Code expects HTTPS_PROXY/HTTP_PROXY; Claude probes use the direct path.") printf 'macOS 已配置系统代理,但 Claude Code 需要 HTTPS_PROXY/HTTP_PROXY;当前 Claude 测试使用直连链路。' ;; + *) printf '%s' "$1" ;; + esac +} + command -v curl >/dev/null 2>&1 || die "curl is required" command -v awk >/dev/null 2>&1 || die "awk is required" command -v sed >/dev/null 2>&1 || die "sed is required" @@ -570,6 +740,11 @@ format_bits_mbps() { endpoint_count=0 TAB=$(printf '\t') +if [ "$UI_LANG" = "zh" ]; then + progress_note "●" "开始检查 AI 编程服务链路…" +else + progress_note "●" "Checking AI coding service paths…" +fi while IFS="$TAB" read -r service label endpoint probe_type priority; do [ -n "$endpoint" ] || continue endpoint_count=$((endpoint_count + 1)) @@ -578,6 +753,7 @@ while IFS="$TAB" read -r service label endpoint probe_type priority; do : > "$sample_file" sample=1 while [ "$sample" -le "$SAMPLES" ]; do + progress_sample "$service" "$label" "$sample" "$SAMPLES" metrics=$(run_probe "$service" "$probe_type" "$endpoint" \ --proto '=http,https' --http1.1 -sS -o /dev/null \ -A "ipcheck/$VERSION" \ @@ -606,6 +782,27 @@ EOF "$curl_exit" "$http_code" "$dns" "$connect" "$tls" "$first_byte" "$total" "$bytes" "$speed" >> "$sample_file" sample=$((sample + 1)) done + progress_ok=$(awk '$1 == 0 && $2 != "000" && $2 != "407" { count++ } END { print count + 0 }' "$sample_file") + progress_http=$(awk '$2 != "000" { print $2; exit }' "$sample_file") + progress_has_404=$(awk '$2 == "404" { found=1 } END { print found + 0 }' "$sample_file") + progress_has_warning=$(awk '$2 == "429" || $2 ~ /^5[0-9][0-9]$/ { found=1 } END { print found + 0 }' "$sample_file") + [ -n "$progress_http" ] || progress_http="000" + if [ "$UI_LANG" = "zh" ]; then + progress_detail="HTTP ${progress_http},链路可达 $progress_ok/$SAMPLES 次" + else + progress_detail="HTTP $progress_http, reachable $progress_ok/$SAMPLES" + fi + if [ "$progress_has_404" -eq 1 ]; then + progress_finish fail "$(ui_service_name "$service") · $label" "$progress_detail" + elif [ "$progress_has_warning" -eq 1 ]; then + progress_finish warn "$(ui_service_name "$service") · $label" "$progress_detail" + elif [ "$progress_ok" -gt 0 ]; then + progress_finish ok "$(ui_service_name "$service") · $label" "$progress_detail" + elif [ "$progress_http" = "407" ]; then + progress_finish warn "$(ui_service_name "$service") · $label" "$progress_detail" + else + progress_finish fail "$(ui_service_name "$service") · $label" "$progress_detail" + fi done < "$PROBES_FILE" [ "$endpoint_count" -gt 0 ] || die "no endpoints configured" @@ -615,6 +812,11 @@ bandwidth_code=000 bandwidth_valid=0 bandwidth_bytes=0 if [ "$RUN_BANDWIDTH" -eq 1 ]; then + if [ "$UI_LANG" = "zh" ]; then + progress_note "●" "正在测试参考下载带宽(2 MB)…" + else + progress_note "●" "Testing reference download bandwidth (2 MB)…" + fi bandwidth_metrics=$(curl_for_service "custom" --proto '=https' --http1.1 -sS -o /dev/null \ -A "ipcheck/$VERSION" \ --connect-timeout "$TIMEOUT" --max-time "$TIMEOUT" \ @@ -637,6 +839,17 @@ EOF bandwidth_bytes=0 bandwidth_speed=0 fi + if [ "$bandwidth_valid" -eq 1 ]; then + if [ "$UI_LANG" = "zh" ]; then + progress_finish ok "Cloudflare" "参考带宽 $(format_mbps "$bandwidth_speed")" + else + progress_finish ok "Cloudflare" "reference bandwidth $(format_mbps "$bandwidth_speed")" + fi + elif [ "$UI_LANG" = "zh" ]; then + progress_finish warn "Cloudflare" "参考带宽测试不可用" + else + progress_finish warn "Cloudflare" "reference bandwidth unavailable" + fi fi system_interface="" @@ -684,6 +897,7 @@ while IFS="$TAB" read -r index service label endpoint probe_type priority; do done < "$INDEXED_PROBES_FILE" service_count=0 +usable_services=0 good_services=0 fair_services=0 poor_services=0 @@ -695,6 +909,8 @@ while IFS="$TAB" read -r service display model base source; do [ -n "$primary_index" ] || continue primary_sample_file="$WORK_DIR/endpoint_${primary_index}.tsv" service_ok=$(awk '$1 == 0 && $2 != "000" && $2 != "407" { count++ } END { print count + 0 }' "$primary_sample_file") + service_usable=$(awk '$1 == 0 && $2 != "000" && $2 != "407" && $2 != "404" && $2 != "429" && $2 !~ /^5[0-9][0-9]$/ { count++ } END { print count + 0 }' "$primary_sample_file") + [ "$service_usable" -eq 0 ] || usable_services=$((usable_services + 1)) service_rate=$(awk -v ok="$service_ok" -v total="$SAMPLES" 'BEGIN { printf "%.0f", ok * 100 / total }') service_has_407=$(awk '$2 == "407" { found=1 } END { print found + 0 }' "$primary_sample_file") service_has_404=$(awk '$2 == "404" { found=1 } END { print found + 0 }' "$primary_sample_file") @@ -776,6 +992,76 @@ else overall_reason="$good_services good, $fair_services fair, $poor_services poor, $blocked_services blocked service path(s)." fi +case "$overall" in + good) + readiness_ready=true + readiness_level="ready" + readiness_answer_en="YES" + readiness_summary_en="This network is ready for AI-assisted development." + readiness_recommendation_en="You can work normally; latency and stability are in a comfortable range." + readiness_answer_zh="适合" + readiness_summary_zh="当前网络适合进行 AI 辅助开发。" + readiness_recommendation_zh="可以正常开发,延迟和稳定性都在舒适范围内。" + ;; + fair) + if [ "$usable_services" -gt 0 ]; then + readiness_ready=true + readiness_level="with_caution" + readiness_answer_en="YES, WITH CAUTION" + readiness_summary_en="Development is possible, but responses may feel slower than ideal." + readiness_recommendation_en="You can keep working. If pauses become distracting, try a lower-latency proxy route." + readiness_answer_zh="可以,但会有些慢" + readiness_summary_zh="当前可以开发,但响应速度可能不够理想。" + readiness_recommendation_zh="可以继续工作;如果等待明显影响节奏,建议切换到延迟更低的代理节点。" + else + readiness_ready=false + readiness_level="temporarily_unavailable" + readiness_answer_en="NO" + readiness_summary_en="The network path is reachable, but the service is currently unavailable." + readiness_recommendation_en="Wait for rate limits or server errors to clear, or switch to another service route." + readiness_answer_zh="暂不适合" + readiness_summary_zh="网络链路可以访问,但服务当前不可用。" + readiness_recommendation_zh="请等待限流或服务器异常恢复,或切换其他服务节点。" + fi + ;; + poor) + readiness_ready=false + readiness_level="not_recommended" + readiness_answer_en="NO" + readiness_summary_en="This network is likely to interrupt productive AI-assisted development." + readiness_recommendation_en="Switch proxy routes or troubleshoot latency and failures before a long coding session." + readiness_answer_zh="暂不适合" + readiness_summary_zh="当前网络很可能影响 AI 辅助开发的连续性。" + readiness_recommendation_zh="建议先切换代理节点,或排查高延迟和请求失败,再开始较长时间的开发。" + ;; + *) + readiness_ready=false + readiness_level="blocked" + readiness_answer_en="NO" + readiness_summary_en="The AI coding service path is currently blocked." + readiness_recommendation_en="Check the proxy, gateway, and endpoint configuration before coding." + readiness_answer_zh="不适合" + readiness_summary_zh="当前无法连通 AI 编程服务。" + readiness_recommendation_zh="请先检查代理、网关和服务端点配置。" + ;; +esac + +if [ "$UI_LANG" = "zh" ]; then + readiness_answer="$readiness_answer_zh" + readiness_summary="$readiness_summary_zh" + readiness_recommendation="$readiness_recommendation_zh" + if [ "$service_count" -eq 1 ]; then + localized_overall_reason=$(localized_reason "$overall_reason") + else + localized_overall_reason="$good_services 条良好、$fair_services 条一般、$poor_services 条较差、$blocked_services 条阻断。" + fi +else + readiness_answer="$readiness_answer_en" + readiness_summary="$readiness_summary_en" + readiness_recommendation="$readiness_recommendation_en" + localized_overall_reason="$overall_reason" +fi + proxy_text=$(proxy_summary) timestamp=$(date -u '+%Y-%m-%dT%H:%M:%SZ') @@ -784,23 +1070,38 @@ service_display() { } render_human() { - cat < ipcheck does not read or transmit API keys and does not make billable model requests.\n' + if [ "$UI_LANG" = "zh" ]; then printf '\n> ipcheck 不会读取或传输 API 密钥,也不会发起计费模型请求。\n'; else printf '\n> ipcheck does not read or transmit API keys and does not make billable model requests.\n'; fi } render_json() { @@ -905,8 +1231,9 @@ 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","reachable_endpoints":%s,"primary_success_rate_pct":%s,"primary_ttfb_median_ms":%s,"primary_jitter_ms":%s,"privacy":{"credentials_used":false,"billable_requests":false},"warnings":[%s],"bandwidth":{"enabled":%s,"available":%s,"http_code":"%s","bytes":%s,"bytes_per_second":%s},"system":{"enabled":%s,"interface":"%s","base_rtt_ms":%s,"download_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","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},"warnings":[%s],"bandwidth":{"enabled":%s,"available":%s,"http_code":"%s","bytes":%s,"bytes_per_second":%s},"system":{"enabled":%s,"interface":"%s","base_rtt_ms":%s,"download_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" "$(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)" "$([ "$bandwidth_valid" -eq 1 ] && printf true || printf false)" "$bandwidth_code" "$bandwidth_bytes" "$bandwidth_speed" \ "$([ -n "$system_throughput" ] && printf true || printf false)" "$(json_escape "$system_interface")" "${system_rtt:-0}" "${system_throughput:-0}" "$(json_escape "$system_proxy_state")" \ diff --git a/package.json b/package.json index 58a3e24..7f17426 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@jacklv-coder/ipcheck", "private": true, - "version": "0.3.0", + "version": "0.4.0", "description": "Diagnose proxy, latency, and API routes used by Codex and Claude Code.", "license": "Apache-2.0", "author": "Jintao", diff --git a/test/smoke.sh b/test/smoke.sh index f22d4b3..1bc4370 100755 --- a/test/smoke.sh +++ b/test/smoke.sh @@ -2,6 +2,10 @@ set -eu +# Keep report-language assertions deterministic regardless of the host locale. +IPCHECK_LANG=en +export IPCHECK_LANG + PROJECT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) STUB_DIR=$(mktemp -d "${TMPDIR:-/tmp}/ipcheck-test.XXXXXX") FIXTURE_HOME="$STUB_DIR/home" @@ -137,7 +141,7 @@ run_ipcheck() { -u CODEX_NETWORK_ENDPOINTS -u CLAUDE_NETWORK_ENDPOINTS \ -u HTTP_PROXY -u http_proxy -u ALL_PROXY -u all_proxy \ PATH="$STUB_DIR:$PATH" HOME="$FIXTURE_HOME" CODEX_HOME="$CODEX_FIXTURE" CLAUDE_CONFIG_DIR="$CLAUDE_FIXTURE" \ - HTTPS_PROXY="http://127.0.0.1:1080" IPCHECK_TEST_CURL_LOG="$CURL_LOG" \ + HTTPS_PROXY="http://127.0.0.1:1080" IPCHECK_LANG="${IPCHECK_LANG:-en}" IPCHECK_TEST_CURL_LOG="$CURL_LOG" \ "$PROJECT_DIR/bin/ipcheck" "$@" } @@ -148,13 +152,13 @@ run_ipcheck_direct() { -u CODEX_NETWORK_ENDPOINTS -u CLAUDE_NETWORK_ENDPOINTS \ -u HTTPS_PROXY -u https_proxy -u HTTP_PROXY -u http_proxy -u ALL_PROXY -u all_proxy \ PATH="$STUB_DIR:$PATH" HOME="$FIXTURE_HOME" CODEX_HOME="$CODEX_FIXTURE" CLAUDE_CONFIG_DIR="$CLAUDE_FIXTURE" \ - IPCHECK_TEST_CURL_LOG="$CURL_LOG" \ + IPCHECK_LANG="${IPCHECK_LANG:-en}" IPCHECK_TEST_CURL_LOG="$CURL_LOG" \ "$PROJECT_DIR/bin/ipcheck" "$@" } 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.3.0" ] +[ "$("$PROJECT_DIR/bin/ipcheck" --version)" = "ipcheck 0.4.0" ] : > "$CURL_LOG" report=$(ANTHROPIC_AUTH_TOKEN="runtime-secret-must-never-appear" run_ipcheck --samples 3 --no-bandwidth --json) @@ -169,6 +173,7 @@ printf '%s\n' "$report" | grep -q 'https://dashscope.aliyuncs.com/apps/anthropic printf '%s\n' "$report" | grep -q 'https://dashscope.aliyuncs.com/apps/anthropic/v1/messages' 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"' if printf '%s\n' "$report" | grep -Eq 'fixture-secret|runtime-secret'; then printf 'Claude credential leaked into JSON report\n' >&2 exit 1 @@ -246,6 +251,33 @@ 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"' + +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' + +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 辅助开发' + +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' + +progress_log="$STUB_DIR/progress.log" +IPCHECK_PROGRESS=always IPCHECK_LANG=en run_ipcheck --samples 2 --no-bandwidth --endpoint https://progress.invalid >/dev/null 2>"$progress_log" +grep -q 'sample 1/2' "$progress_log" +grep -q 'sample 2/2' "$progress_log" +grep -q 'reachable 2/2' "$progress_log" + +json_progress_log="$STUB_DIR/json-progress.log" +IPCHECK_PROGRESS=always run_ipcheck --samples 1 --no-bandwidth --json > /dev/null 2>"$json_progress_log" +[ ! -s "$json_progress_log" ] + +colored_human=$(IPCHECK_LANG=en run_ipcheck --samples 1 --no-bandwidth --color --endpoint https://color.invalid) +case "$colored_human" in + *"$(printf '\033[32m')"*) ;; + *) printf 'forced color output did not contain ANSI color codes\n' >&2; exit 1 ;; +esac attempt_file="$STUB_DIR/attempt" printf 0 > "$attempt_file" @@ -260,14 +292,25 @@ printf '%s\n' "$not_found_report" | grep -q 'configured API route returned HTTP rate_limit_report=$(run_ipcheck --samples 1 --no-bandwidth --endpoint https://rate-limit.invalid --json) printf '%s\n' "$rate_limit_report" | grep -q '"result":"fair"' +printf '%s\n' "$rate_limit_report" | grep -q '"ready":false,"level":"temporarily_unavailable"' 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"' + +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" +grep -q '! Custom · Custom endpoint 1 — HTTP 503, reachable 1/1' "$server_error_progress" +if grep -q '✓ Custom · Custom endpoint 1' "$server_error_progress"; then + printf 'server error was incorrectly shown as successful progress\n' >&2 + exit 1 +fi mixed_status_file="$STUB_DIR/mixed-status-attempt" printf 0 > "$mixed_status_file" mixed_status_report=$(IPCHECK_TEST_MIXED_FILE="$mixed_status_file" run_ipcheck --samples 2 --no-bandwidth --endpoint https://mixed-status.invalid --json) printf '%s\n' "$mixed_status_report" | grep -q '"result":"fair"' printf '%s\n' "$mixed_status_report" | grep -q 'at least one sample returned a server error' +printf '%s\n' "$mixed_status_report" | grep -q '"ready":true,"level":"with_caution"' precedence_report=$(CODEX_NETWORK_ENDPOINTS="https://blocked.invalid" run_ipcheck --samples 1 --no-bandwidth --endpoint https://cli.invalid --json) printf '%s\n' "$precedence_report" | grep -q '"url":"https://cli.invalid"' @@ -406,6 +449,10 @@ printf '%s\n' "$markdown" | grep -q '^# ipcheck: AI Coding Network Report' printf '%s\n' "$markdown" | grep -q '| Claude Code |' printf '%s\n' "$markdown" | grep -q '| Jitter |' +chinese_markdown=$(IPCHECK_LANG=zh run_ipcheck --samples 1 --no-bandwidth --markdown) +printf '%s\n' "$chinese_markdown" | grep -q '^# ipcheck:AI 编程网络报告' +printf '%s\n' "$chinese_markdown" | grep -q '现在适合开发吗?' + quick_report=$(run_ipcheck --quick --json) printf '%s\n' "$quick_report" | grep -q '"samples":1' printf '%s\n' "$quick_report" | grep -q '"bandwidth":{"enabled":false'