statusline-command.sh Claude code 自定义状态栏

一个 Claude Code 自定义状态栏脚本,显示模型名称、Git 分支、Context 使用进度条,以及会话累计 token 统计。

V1不带计费版本

效果预览

复制代码
LongCat-2.0[1m] | main | ██░░░░░░░░ 21% | [输出:128K 新增输入:630K 缓存:35M 命中率:98%]
LongCat-2.0[1m] | main | ████████░░ 85% | [输出:50K 新增输入:800K 缓存:720M 命中率:99%]
  • 进度条 ≤50%:灰色,51-80%:绿色,>80%:红色预警

  • used_percentage 时(部分 API 提供商不返回)显示 --%,仍可显示 token 统计

安装

1. 复制脚本

复制代码
cp statusline-command.sh ~/.claude/statusline-command.sh
chmod +x ~/.claude/statusline-command.sh

2. 配置 settings.json

编辑 ~/.claude/settings.json,添加:

复制代码
{
  "statusLine": {
    "type": "command",
    "command": "bash ~/.claude/statusline-command.sh"
  }
}

3. 依赖

  • jq(推荐,性能更好):用于解析 JSON

    • Windows:scoop install jq 或从 jqlang.github.io 下载放到 PATH 中

    • macOS:brew install jq

    • Linux:apt install jq / pacman -S jq

  • git(可选):显示当前分支名

  • 无 jq 时自动 fallback 到 grep/awk 解析(功能相同,transcript 解析稍慢)

4. 重启 Claude Code

重启会话后生效。

显示内容

区域 内容 示例
模型 当前模型名称 LongCat-2.0[1m]
分支 Git 当前分支 main / no-git
进度条 10 格 Context 使用率 ██░░░░░░░░ 21%
输出 会话累计输出 token 输出:128K
新增输入 会话累计非缓存输入 token 新增输入:630K
缓存 会话累计缓存读取 token 缓存:35M
命中率 缓存读取占总上下文的比例 命中率:98%

Token 说明

  • 新增输入 (input_tokens):每轮 API 调用中,未命中缓存、需要新处理的 token

  • 缓存 (cache_read_input_tokens):每轮 API 调用中,从缓存读取的 token

  • 命中率 = 缓存 / (新增输入 + 缓存) × 100%

注意:缓存是每轮 API 调用的累计值。由于上下文每轮都在增长,同一段历史内容会在后续每轮都被"读取",因此缓存累计值会远大于新增输入,这是正常的。

工作原理

  1. Claude Code 每次事件(消息、工具调用等)触发时,通过 stdin 传入 JSON

  2. 脚本解析 JSON 提取模型、目录、context 等信息

  3. transcript_path 指向的 JSONL 文件增量解析 token 用量(只读新增行)

  4. 缓存文件 ~/.claude/statusline-cache/statusline-cache-{session_id}.json 记录已解析行数,避免重复解析

  5. 7 天未更新的缓存文件自动清理

自定义

编辑 statusline-command.sh 顶部的配置:

复制代码
CACHE_DIR="${HOME}/.claude/statusline-cache"  # 缓存目录
CACHE_TTL_DAYS=7                               # 缓存保留天数

颜色阈值(约第 190 行):

复制代码
if [ "$pct_int" -gt 80 ]; then     # >80% 红色
elif [ "$pct_int" -gt 50 ]; then   # 51-80% 绿色
else                                # ≤50% 灰色
fi
bash 复制代码
#!/bin/bash
# Claude Code status line script
# Shows: model name, git branch, context bar, session token breakdown

input=$(cat)

# --- Config ---
CACHE_DIR="${HOME}/.claude/statusline-cache"
CACHE_TTL_DAYS=7

# --- jq resolver ---
# Windows paths (C:\Users) break jq because \U \v are invalid escapes.
# We must test jq against the REAL input, not a simple test string.
JQ=""
for candidate in "/c/Program Files/jq/jq.exe" "/c/Program Files/jq/jq" "$(command -v jq 2>/dev/null)"; do
  if [ -n "$candidate" ] && [ -x "$candidate" ]; then
    if printf '%s' "$input" | "$candidate" -r '.model.id' >/dev/null 2>&1; then
      JQ="$candidate"
      break
    fi
  fi
done

# --- Extract fields from stdin JSON ---
if [ -n "$JQ" ]; then
  model=$(printf '%s' "$input" | "$JQ" -r '.model.display_name // .model.id // "unknown"')
  cwd=$(printf '%s' "$input" | "$JQ" -r '.workspace.current_dir // empty')
  used_pct=$(printf '%s' "$input" | "$JQ" -r '.context_window.used_percentage // empty')
  total_size=$(printf '%s' "$input" | "$JQ" -r '.context_window.context_window_size // empty')
  total_input=$(printf '%s' "$input" | "$JQ" -r '.context_window.total_input_tokens // empty')
  total_output=$(printf '%s' "$input" | "$JQ" -r '.context_window.total_output_tokens // empty')
  cache_read=$(printf '%s' "$input" | "$JQ" -r '.context_window.current_usage.cache_read_input_tokens // empty')
  cache_create=$(printf '%s' "$input" | "$JQ" -r '.context_window.current_usage.cache_creation_input_tokens // empty')
  session_id=$(printf '%s' "$input" | "$JQ" -r '.session_id // empty')
  transcript_path=$(printf '%s' "$input" | "$JQ" -r '.transcript_path // empty')
else
  _extract() { printf '%s' "$input" | grep -o "\"$1\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*"'"$1"'"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/'; }
  model=$(_extract display_name)
  [ -z "$model" ] && model=$(_extract id)
  [ -z "$model" ] && model="unknown"
  cwd=$(grep -o '"current_dir"[[:space:]]*:[[:space:]]*"[^"]*"' <<<"$input" | head -1 | sed 's/.*"current_dir"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/')
  used_pct=$(grep -o '"used_percentage"[[:space:]]*:[[:space:]]*[0-9.]*' <<<"$input" | head -1 | grep -o '[0-9.]*$')
  total_size=$(grep -o '"context_window_size"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$input" | head -1 | grep -o '[0-9]*$')
  total_input=$(grep -o '"total_input_tokens"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$input" | head -1 | grep -o '[0-9]*$')
  total_output=$(grep -o '"total_output_tokens"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$input" | head -1 | grep -o '[0-9]*$')
  cache_read=$(grep -o '"cache_read_input_tokens"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$input" | head -1 | grep -o '[0-9]*$')
  cache_create=$(grep -o '"cache_creation_input_tokens"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$input" | head -1 | grep -o '[0-9]*$')
  session_id=$(grep -o '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' <<<"$input" | head -1 | sed 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/')
  transcript_path=$(grep -o '"transcript_path"[[:space:]]*:[[:space:]]*"[^"]*"' <<<"$input" | head -1 | sed 's/.*"transcript_path"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/')
fi

# --- Session token accumulation (incremental, via cache file) ---
mkdir -p "$CACHE_DIR"

session_input=0
session_output=0
session_cache=0
session_context=0  # input + cache_read (total context per API call)

if [ -n "$session_id" ] && [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
  CACHE_FILE="${CACHE_DIR}/statusline-cache-${session_id}.json"

  # Read previous cache
  cached_lines=0
  cached_input=0
  cached_output=0
  cached_cache=0
  cached_context=0
  if [ -f "$CACHE_FILE" ]; then
    _cache_json=$(cat "$CACHE_FILE" 2>/dev/null)
    if [ -n "$JQ" ]; then
      cached_lines=$(printf '%s' "$_cache_json" | "$JQ" -r '.lines // 0')
      cached_input=$(printf '%s' "$_cache_json" | "$JQ" -r '.input // 0')
      cached_output=$(printf '%s' "$_cache_json" | "$JQ" -r '.output // 0')
      cached_cache=$(printf '%s' "$_cache_json" | "$JQ" -r '.cache // 0')
      cached_context=$(printf '%s' "$_cache_json" | "$JQ" -r '.context // 0')
    else
      cached_lines=$(grep -o '"lines"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
      cached_input=$(grep -o '"input"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
      cached_output=$(grep -o '"output"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
      cached_cache=$(grep -o '"cache"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
      cached_context=$(grep -o '"context"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
    fi
  fi

  # Count current lines in transcript
  current_lines=$(wc -l < "$transcript_path" 2>/dev/null || echo 0)

  # Only parse new lines
  if [ "$current_lines" -gt "$cached_lines" ] 2>/dev/null; then
    start_line=$(( cached_lines + 1 ))

    # Extract new lines to a temp file for batch processing
    _new_lines=$(sed -n "${start_line},\$p" "$transcript_path" 2>/dev/null)

    if [ -n "$JQ" ]; then
      # Fast path: jq single-pass aggregation over all new lines at once
      _agg=$(printf '%s\n' "$_new_lines" | "$JQ" -s '
        [.[] | select(.message.usage != null) | .message.usage] |
        { input: ([.[].input_tokens // 0] | add),
          output: ([.[].output_tokens // 0] | add),
          cache: ([.[].cache_read_input_tokens // 0] | add)
        }
      ' 2>/dev/null)
      new_input=$(printf '%s' "$_agg" | "$JQ" -r '.input // 0')
      new_output=$(printf '%s' "$_agg" | "$JQ" -r '.output // 0')
      new_cache=$(printf '%s' "$_agg" | "$JQ" -r '.cache // 0')
    else
      # Slow fallback: awk single-pass (no jq available)
      _agg=$(printf '%s\n' "$_new_lines" | awk '
        /"input_tokens"[[:space:]]*:[[:space:]]*[0-9]/ { match($0, /"input_tokens"[[:space:]]*:[[:space:]]*([0-9]+)/, a); i+=a[1] }
        /"output_tokens"[[:space:]]*:[[:space:]]*[0-9]/ { match($0, /"output_tokens"[[:space:]]*:[[:space:]]*([0-9]+)/, a); o+=a[1] }
        /"cache_read_input_tokens"/ { match($0, /"cache_read_input_tokens"[[:space:]]*:[[:space:]]*([0-9]+)/, a); c+=a[1] }
        END { printf "%d %d %d", i, o, c }
      ')
      new_input=$(echo "$_agg" | awk '{print $1}')
      new_output=$(echo "$_agg" | awk '{print $2}')
      new_cache=$(echo "$_agg" | awk '{print $3}')
    fi
    new_context=$(( new_input + new_cache ))

    session_input=$(( cached_input + new_input ))
    session_output=$(( cached_output + new_output ))
    session_cache=$(( cached_cache + new_cache ))
    session_context=$(( cached_context + new_context ))

    # Update cache (always write, regardless of jq or grep)
    printf '{"lines":%d,"input":%d,"output":%d,"cache":%d,"context":%d}' \
      "$current_lines" "$session_input" "$session_output" "$session_cache" "$session_context" > "$CACHE_FILE"
  else
    session_input=$cached_input
    session_output=$cached_output
    session_cache=$cached_cache
    session_context=$cached_context
  fi

  # Cleanup old cache files (older than CACHE_TTL_DAYS)
  find "$CACHE_DIR" -name "statusline-cache-*.json" -mtime +${CACHE_TTL_DAYS} -delete 2>/dev/null
fi

# --- Extract git branch ---
branch=""
if [ -n "$cwd" ]; then
  branch=$(git -C "$cwd" --no-optional-locks rev-parse --abbrev-ref HEAD 2>/dev/null)
fi
[ -z "$branch" ] && branch="no-git"

# --- Format token counts ---
_fmt_tokens() {
  local t=$1
  if [ -z "$t" ] || [ "$t" -eq 0 ] 2>/dev/null; then echo ""; return; fi
  if [ "$t" -ge 1000000 ]; then
    printf '%dM' $(( t / 1000000 ))
  elif [ "$t" -ge 1000 ]; then
    printf '%dK' $(( t / 1000 ))
  else
    printf '%d' "$t"
  fi
}

# --- Session cumulative values ---
sess_in_str=$(_fmt_tokens "$session_input")
sess_out_str=$(_fmt_tokens "$session_output")
sess_cache_str=$(_fmt_tokens "$session_cache")

# Cache hit rate = cache_read / (input + cache_read) per session
sess_hit_rate=""
if [ "$session_context" -gt 0 ] 2>/dev/null; then
  sess_hit_rate=$(( session_cache * 100 / session_context ))
fi

# Build detail: session cumulative
detail=""
[ -n "$sess_out_str" ] && detail="输出:${sess_out_str}"
[ -n "$sess_in_str" ] && detail="${detail:+$detail }新增输入:${sess_in_str}"
[ -n "$sess_cache_str" ] && detail="${detail:+$detail }缓存:${sess_cache_str}"
[ -n "$sess_hit_rate" ] && detail="${detail:+$detail }命中率:${sess_hit_rate}%"

# --- Build the 10-cell progress bar ---
bar=""
if [ -n "$used_pct" ] && [ "$used_pct" != "null" ]; then
  pct_int=$(printf "%.0f" "$used_pct")
  filled=$(( pct_int / 10 ))
  [ $filled -gt 10 ] && filled=10
  empty=$(( 10 - filled ))

  blocks=$(printf '█%.0s' $(seq 1 $filled); printf '░%.0s' $(seq 1 $empty))
  info="${pct_int}%"

  if [ "$pct_int" -gt 80 ] 2>/dev/null; then
    bar=$(printf '\033[31m%s\033[0m %s' "$blocks" "$info")
  elif [ "$pct_int" -gt 50 ] 2>/dev/null; then
    bar=$(printf '\033[32m%s\033[0m %s' "$blocks" "$info")
  else
    bar=$(printf '\033[90m%s\033[0m %s' "$blocks" "$info")
  fi
else
  bar="--%"
fi

# Append session detail
[ -n "$detail" ] && bar="${bar} | [${detail}]"

# --- Output ---
printf '%b\n' "${model} | ${branch} | ${bar}"

V1带计费版本

效果预览

复制代码
no-git | ██░░░░░░░░ 23% | [输出:204.79K 新增输入:1.68M 缓存:114.90M 命中率:98%] | LongCat-2.0[1m]:¥9.13
  • 进度条颜色:≤50% 灰色 | 51-80% 绿色 | >80% 红色预警

  • 费用根据 API 调用时间自动分段(高峰/非高峰)计价

  • 无价格配置时提示 (未配置价格)

价格配置

价格文件存放在 ~/.claude/price/,每个模型一个 JSON 文件。

配置格式

复制代码
{
  "name": "显示名",
  "currency": "¥",
  "unit": "per_million_tokens",
  "peak": {
    "hours": [[9,12],[14,18]],
    "cache_hit": 0.025,
    "cache_miss": 3.0,
    "output": 6.0
  },
  "offpeak": {
    "hours": [[0,9],[12,14],[18,24]],
    "cache_hit": 0.02,
    "cache_miss": 1.0,
    "output": 2.0
  }
}
字段 说明
peak.hours 高峰时段(北京时间),支持多段
peak.cache_hit 高峰缓存命中价格(元/百万 token)
peak.cache_miss 高峰缓存未命中价格
peak.output 高峰输出价格
offpeak.* 空闲时段价格

添加新模型

复制代码
cat > ~/.claude/price/新模型.json << 'EOF'
{
  "name": "新模型",
  "currency": "¥",
  "peak": {"hours": [[0,24]], "cache_hit": 0.1, "cache_miss": 2.0, "output": 5.0},
  "offpeak": {"hours": [[0,24]], "cache_hit": 0.1, "cache_miss": 2.0, "output": 5.0}
}
EOF

自动识别

脚本从 Claude Code 的 model.id 自动匹配价格文件:

  1. 精确匹配:模型名 == 价格文件名(忽略大小写和连字符)

  2. 模糊匹配:模型名包含文件名,或反之(取最长匹配)

  3. 兜底 :使用 ~/.claude/price/active 文件指定的模型

示例:mimo-v2.5-pro → 自动匹配 v2.5pro.json

费用计算原理

Token 分类

  • 新增输入 (input_tokens):未命中缓存、需要新处理的 token

  • 缓存 (cache_read_input_tokens):从缓存读取的 token

  • 命中率 = 缓存 / (新增输入 + 缓存) × 100%

高峰/非高峰分段

每条 API 调用记录包含 UTC 时间戳,脚本:

  1. 将 UTC 转换为北京时间(UTC+8)

  2. 根据价格配置判断该调用属于高峰/非高峰

  3. 分别累加各时段的 token 用量

计算公式

复制代码
费用 = 高峰(缓存×cache_hit + 输入×cache_miss + 输出×output) / 1,000,000
     + 空闲(缓存×cache_hit + 输入×cache_miss + 输出×output) / 1,000,000

增量缓存

  • 首次解析:读取整个 transcript,按时间分段累加

  • 后续刷新:只处理新增行,合并到缓存

  • 缓存文件:~/.claude/statusline-cache/statusline-cache-{session_id}.json

  • 7 天未更新的缓存自动清理

常见问题

Q: 会话开始显示 N/A? A: 需要发第一条消息后才有数据。

Q: 为什么缓存数值很大? A: 缓存是每轮 API 调用的累计值。上下文每轮增长,同一段历史在后续每轮都会被"读取"一次。

Q: 费用和实际账单不一致? A: 脚本使用配置的价格估算,实际价格以服务商为准。确保价格配置与实际一致。

Q: 支持多模型切换吗? A: 支持。脚本按每条 API 调用的实际时间分段计价,但所有 token 使用当前模型的价格文件。

bash 复制代码
#!/bin/bash
# Claude Code status line script
# Shows: model name, git branch, context bar, session token breakdown

input=$(cat)

# --- Config ---
CACHE_DIR="${HOME}/.claude/statusline-cache"
CACHE_TTL_DAYS=7

# --- jq resolver ---
# Windows paths (C:\Users) break jq because \U \v are invalid escapes.
# We must test jq against the REAL input, not a simple test string.
JQ=""
for candidate in "/c/Program Files/jq/jq.exe" "/c/Program Files/jq/jq" "$(command -v jq 2>/dev/null)"; do
  if [ -n "$candidate" ] && [ -x "$candidate" ]; then
    if printf '%s' "$input" | "$candidate" -r '.model.id' >/dev/null 2>&1; then
      JQ="$candidate"
      break
    fi
  fi
done

# --- Extract fields from stdin JSON ---
if [ -n "$JQ" ]; then
  model=$(printf '%s' "$input" | "$JQ" -r '.model.display_name // .model.id // "unknown"')
  cwd=$(printf '%s' "$input" | "$JQ" -r '.workspace.current_dir // empty')
  used_pct=$(printf '%s' "$input" | "$JQ" -r '.context_window.used_percentage // empty')
  total_size=$(printf '%s' "$input" | "$JQ" -r '.context_window.context_window_size // empty')
  total_input=$(printf '%s' "$input" | "$JQ" -r '.context_window.total_input_tokens // empty')
  total_output=$(printf '%s' "$input" | "$JQ" -r '.context_window.total_output_tokens // empty')
  cache_read=$(printf '%s' "$input" | "$JQ" -r '.context_window.current_usage.cache_read_input_tokens // empty')
  cache_create=$(printf '%s' "$input" | "$JQ" -r '.context_window.current_usage.cache_creation_input_tokens // empty')
  session_id=$(printf '%s' "$input" | "$JQ" -r '.session_id // empty')
  transcript_path=$(printf '%s' "$input" | "$JQ" -r '.transcript_path // empty')
else
  _extract() { printf '%s' "$input" | grep -o "\"$1\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*"'"$1"'"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/'; }
  model=$(_extract display_name)
  [ -z "$model" ] && model=$(_extract id)
  [ -z "$model" ] && model="unknown"
  cwd=$(grep -o '"current_dir"[[:space:]]*:[[:space:]]*"[^"]*"' <<<"$input" | head -1 | sed 's/.*"current_dir"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/')
  used_pct=$(grep -o '"used_percentage"[[:space:]]*:[[:space:]]*[0-9.]*' <<<"$input" | head -1 | grep -o '[0-9.]*$')
  total_size=$(grep -o '"context_window_size"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$input" | head -1 | grep -o '[0-9]*$')
  total_input=$(grep -o '"total_input_tokens"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$input" | head -1 | grep -o '[0-9]*$')
  total_output=$(grep -o '"total_output_tokens"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$input" | head -1 | grep -o '[0-9]*$')
  cache_read=$(grep -o '"cache_read_input_tokens"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$input" | head -1 | grep -o '[0-9]*$')
  cache_create=$(grep -o '"cache_creation_input_tokens"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$input" | head -1 | grep -o '[0-9]*$')
  session_id=$(grep -o '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' <<<"$input" | head -1 | sed 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/')
  transcript_path=$(grep -o '"transcript_path"[[:space:]]*:[[:space:]]*"[^"]*"' <<<"$input" | head -1 | sed 's/.*"transcript_path"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/')
fi

# --- Detect price file and extract peak hours (before transcript parsing) ---
PRICE_DIR_EARLY="$(dirname "$0")/price"
peak_hours_str="0,24"  # default: all peak
if [ -d "$PRICE_DIR_EARLY" ]; then
  # Find jq for price parsing
  PJQ_EARLY=""
  for candidate in "/c/Program Files/jq/jq.exe" "/c/Program Files/jq/jq" "$(command -v jq 2>/dev/null)"; do
    if [ -n "$candidate" ] && [ -x "$candidate" ]; then
      if echo '{"a":1}' | "$candidate" -r '.a' >/dev/null 2>&1; then
        PJQ_EARLY="$candidate"
        break
      fi
    fi
  done

  # Auto-detect price file (same logic as cost section)
  _norm() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -d '[:punct:]' | tr -d ' '; }
  _early_price=""
  price_matched=""
  model_norm=$(_norm "$model")
  for pf in "$PRICE_DIR_EARLY"/*.json; do
    [ -f "$pf" ] || continue
    pf_name=$(basename "$pf" .json)
    if [ "$model_norm" = "$(_norm "$pf_name")" ]; then _early_price="$pf"; price_matched=1; break; fi
  done
  if [ -z "$_early_price" ]; then
    best_len=0
    for pf in "$PRICE_DIR_EARLY"/*.json; do
      [ -f "$pf" ] || continue
      pf_name=$(basename "$pf" .json)
      pf_norm=$(_norm "$pf_name")
      if [[ "$model_norm" == *"$pf_norm"* ]] || [[ "$pf_norm" == *"$model_norm"* ]]; then
        if [ ${#pf_name} -gt $best_len ]; then best_len=${#pf_name}; _early_price="$pf"; price_matched=1; fi
      fi
    done
  fi
  if [ -z "$_early_price" ]; then
    _af="${PRICE_DIR_EARLY}/active"
    if [ -f "$_af" ]; then
      _am=$(cat "$_af" 2>/dev/null | tr -d '\r\n\t ')
      [ -f "${PRICE_DIR_EARLY}/${_am}.json" ] && _early_price="${PRICE_DIR_EARLY}/${_am}.json"
    fi
  fi

  # Extract peak hours from price config: [[9,12],[14,18]] → "9,12,14,18"
  if [ -n "$_early_price" ] && [ -f "$_early_price" ]; then
    if [ -n "$PJQ_EARLY" ]; then
      _ph=$("$PJQ_EARLY" -r '.peak.hours // [[0,24]]' "$_early_price" 2>/dev/null)
      peak_hours_str=$(printf '%s' "$_ph" | tr -d '[]\r\n' | tr -s ',' ' ' | sed 's/^ //;s/ $//;s/ /,/g')
    else
      _compact=$(tr -d '\n\t\r ' < "$_early_price")
      _ph_section=$(grep -o '"peak"[^}]*' <<<"$_compact" | head -1)
      peak_hours_str=$(grep -o '"hours"\[[^]]*\]' <<<"$_ph_section" | grep -o '[0-9]*' | tr '\n' ' ' | sed 's/^ //;s/ $//;s/ /,/g')
    fi
  fi
fi

# --- Session token accumulation (incremental, via cache file) ---
mkdir -p "$CACHE_DIR"

session_input=0
session_output=0
session_cache=0
session_context=0  # input + cache_read (total context per API call)
session_peak_input=0; session_peak_output=0; session_peak_cache=0
session_off_input=0; session_off_output=0; session_off_cache=0

if [ -n "$session_id" ] && [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
  CACHE_FILE="${CACHE_DIR}/statusline-cache-${session_id}.json"

  # Read previous cache
  cached_lines=0
  cached_input=0
  cached_output=0
  cached_cache=0
  cached_context=0
  cached_peak_input=0; cached_peak_output=0; cached_peak_cache=0
  cached_off_input=0; cached_off_output=0; cached_off_cache=0
  if [ -f "$CACHE_FILE" ]; then
    _cache_json=$(cat "$CACHE_FILE" 2>/dev/null)
    if [ -n "$JQ" ]; then
      cached_lines=$(printf '%s' "$_cache_json" | "$JQ" -r '.lines // 0')
      cached_input=$(printf '%s' "$_cache_json" | "$JQ" -r '.input // 0')
      cached_output=$(printf '%s' "$_cache_json" | "$JQ" -r '.output // 0')
      cached_cache=$(printf '%s' "$_cache_json" | "$JQ" -r '.cache // 0')
      cached_context=$(printf '%s' "$_cache_json" | "$JQ" -r '.context // 0')
      cached_peak_input=$(printf '%s' "$_cache_json" | "$JQ" -r '.peak_input // 0')
      cached_peak_output=$(printf '%s' "$_cache_json" | "$JQ" -r '.peak_output // 0')
      cached_peak_cache=$(printf '%s' "$_cache_json" | "$JQ" -r '.peak_cache // 0')
      cached_off_input=$(printf '%s' "$_cache_json" | "$JQ" -r '.off_input // 0')
      cached_off_output=$(printf '%s' "$_cache_json" | "$JQ" -r '.off_output // 0')
      cached_off_cache=$(printf '%s' "$_cache_json" | "$JQ" -r '.off_cache // 0')
    else
      cached_lines=$(grep -o '"lines"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
      cached_input=$(grep -o '"input"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
      cached_output=$(grep -o '"output"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
      cached_cache=$(grep -o '"cache"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
      cached_context=$(grep -o '"context"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
      cached_peak_input=$(grep -o '"peak_input"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
      cached_peak_output=$(grep -o '"peak_output"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
      cached_peak_cache=$(grep -o '"peak_cache"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
      cached_off_input=$(grep -o '"off_input"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
      cached_off_output=$(grep -o '"off_output"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
      cached_off_cache=$(grep -o '"off_cache"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
    fi
  fi

  # Count current lines in transcript
  current_lines=$(wc -l < "$transcript_path" 2>/dev/null || echo 0)

  # Only parse new lines
  if [ "$current_lines" -gt "$cached_lines" ] 2>/dev/null; then
    start_line=$(( cached_lines + 1 ))

    # Extract new lines to a temp file for batch processing
    _new_lines=$(sed -n "${start_line},\$p" "$transcript_path" 2>/dev/null)

    # Parse new lines: jq extracts per-line data, awk aggregates peak/offpeak
    # Peak hours come from the price config (e.g. [[9,12],[14,18]])
    # Pass as awk variable: "9,12,14,18" meaning ranges [9,12) and [14,18)
    # Fallback: if no peak_hours configured, treat all as peak
    if [ -z "$peak_hours_str" ]; then peak_hours_str="0,24"; fi

    if [ -n "$JQ" ]; then
      _agg=$(printf '%s\n' "$_new_lines" | "$JQ" -r '
        select(.message.usage != null) |
        [(.timestamp // "" | split("T")[1] | split(":")[0] | tonumber // 0),
         .message.usage.input_tokens // 0,
         .message.usage.output_tokens // 0,
         .message.usage.cache_read_input_tokens // 0] | @tsv
      ' 2>/dev/null | awk -F'\t' -v ph="$peak_hours_str" '
        function ispeak(h,   s,n,a,i) {
          n=split(ph,a,",")
          for(i=1;i<=n;i+=2) if(h>=a[i] && h<a[i+1]) return 1
          return 0
        }
        { h=($1+8)%24; i+=$2; o+=$3; c+=int($4)
          if(ispeak(h)){pi+=$2;po+=$3;pc+=int($4)} else {oi+=$2;oo+=$3;oc+=int($4)}
        } END { printf "%d %d %d %d %d %d %d %d %d", i, o, c, pi, po, pc, oi, oo, oc }')
    else
      _agg=$(printf '%s\n' "$_new_lines" | awk -v ph="$peak_hours_str" '
        function ispeak(h,   s,n,a,i) {
          n=split(ph,a,",")
          for(i=1;i<=n;i+=2) if(h>=a[i] && h<a[i+1]) return 1
          return 0
        }
        /"timestamp"/ { match($0, /"timestamp":"[^T]*T([0-9]+):/, a); h=(a[1]+8)%24 }
        /"input_tokens"[[:space:]]*:[[:space:]]*[0-9]/ { match($0, /"input_tokens"[[:space:]]*:[[:space:]]*([0-9]+)/, a); i+=a[1]; if(ispeak(h)) pi+=a[1]; else oi+=a[1] }
        /"output_tokens"[[:space:]]*:[[:space:]]*[0-9]/ { match($0, /"output_tokens"[[:space:]]*:[[:space:]]*([0-9]+)/, a); o+=a[1]; if(ispeak(h)) po+=a[1]; else oo+=a[1] }
        /"cache_read_input_tokens"/ { match($0, /"cache_read_input_tokens"[[:space:]]*:[[:space:]]*([0-9]+)/, a); c+=a[1]; if(ispeak(h)) pc+=a[1]; else oc+=a[1] }
        END { printf "%d %d %d %d %d %d %d %d %d", i, o, c, pi, po, pc, oi, oo, oc }
      ')
    fi
    new_input=$(echo "$_agg" | awk '{print $1}')
    new_output=$(echo "$_agg" | awk '{print $2}')
    new_cache=$(echo "$_agg" | awk '{print $3}')
    new_peak_input=$(echo "$_agg" | awk '{print $4}')
    new_peak_output=$(echo "$_agg" | awk '{print $5}')
    new_peak_cache=$(echo "$_agg" | awk '{print $6}')
    new_off_input=$(echo "$_agg" | awk '{print $7}')
    new_off_output=$(echo "$_agg" | awk '{print $8}')
    new_off_cache=$(echo "$_agg" | awk '{print $9}')
    new_context=$(( new_input + new_cache ))

    session_input=$(( cached_input + new_input ))
    session_output=$(( cached_output + new_output ))
    session_cache=$(( cached_cache + new_cache ))
    session_context=$(( cached_context + new_context ))
    session_peak_input=$(( cached_peak_input + new_peak_input ))
    session_peak_output=$(( cached_peak_output + new_peak_output ))
    session_peak_cache=$(( cached_peak_cache + new_peak_cache ))
    session_off_input=$(( cached_off_input + new_off_input ))
    session_off_output=$(( cached_off_output + new_off_output ))
    session_off_cache=$(( cached_off_cache + new_off_cache ))

    # Update cache
    printf '{"lines":%d,"input":%d,"output":%d,"cache":%d,"context":%d,"peak_input":%d,"peak_output":%d,"peak_cache":%d,"off_input":%d,"off_output":%d,"off_cache":%d}' \
      "$current_lines" "$session_input" "$session_output" "$session_cache" "$session_context" \
      "$session_peak_input" "$session_peak_output" "$session_peak_cache" \
      "$session_off_input" "$session_off_output" "$session_off_cache" > "$CACHE_FILE"
  else
    session_input=$cached_input
    session_output=$cached_output
    session_cache=$cached_cache
    session_context=$cached_context
    session_peak_input=$cached_peak_input
    session_peak_output=$cached_peak_output
    session_peak_cache=$cached_peak_cache
    session_off_input=$cached_off_input
    session_off_output=$cached_off_output
    session_off_cache=$cached_off_cache
  fi

  # Cleanup old cache files (older than CACHE_TTL_DAYS)
  find "$CACHE_DIR" -name "statusline-cache-*.json" -mtime +${CACHE_TTL_DAYS} -delete 2>/dev/null
fi

# --- Extract git branch ---
branch=""
if [ -n "$cwd" ]; then
  branch=$(git -C "$cwd" --no-optional-locks rev-parse --abbrev-ref HEAD 2>/dev/null)
fi
[ -z "$branch" ] && branch="no-git"

# --- Format token counts (2 decimal places) ---
_fmt_tokens() {
  local t=$1
  if [ -z "$t" ] || [ "$t" -eq 0 ] 2>/dev/null; then echo ""; return; fi
  if [ "$t" -ge 1000000 ]; then
    awk "BEGIN { printf \"%.2fM\", $t/1000000 }"
  elif [ "$t" -ge 1000 ]; then
    awk "BEGIN { printf \"%.2fK\", $t/1000 }"
  else
    printf '%d' "$t"
  fi
}

# --- Session cumulative values ---
sess_in_str=$(_fmt_tokens "$session_input")
sess_out_str=$(_fmt_tokens "$session_output")
sess_cache_str=$(_fmt_tokens "$session_cache")

# Cache hit rate = cache_read / (input + cache_read) per session
sess_hit_rate=""
if [ "$session_context" -gt 0 ] 2>/dev/null; then
  sess_hit_rate=$(( session_cache * 100 / session_context ))
fi

# Build detail: session cumulative
detail=""
[ -n "$sess_out_str" ] && detail="输出:${sess_out_str}"
[ -n "$sess_in_str" ] && detail="${detail:+$detail }新增输入:${sess_in_str}"
[ -n "$sess_cache_str" ] && detail="${detail:+$detail }缓存:${sess_cache_str}"
[ -n "$sess_hit_rate" ] && detail="${detail:+$detail }命中率:${sess_hit_rate}%"

# --- Cost calculation from price configs ---
PRICE_DIR="$(dirname "$0")/price"
cost_detail=""
if [ -d "$PRICE_DIR" ] && [ "$session_context" -gt 0 ] 2>/dev/null; then
  # Get current hour in Asia/Shanghai (Beijing time)
  # On Windows Git Bash, TZ override often fails. Try multiple methods.
  bj_hour=$(TZ='Asia/Shanghai' date +%H 2>/dev/null)
  # If TZ returned 00 but system hour is different, system is likely already in Beijing time
  sys_hour=$(date +%H)
  if [ "$bj_hour" = "00" ] && [ "$sys_hour" != "00" ]; then
    bj_hour="$sys_hour"
  fi
  # Validate: if still empty, fallback to system hour
  [ -z "$bj_hour" ] && bj_hour="$sys_hour"

  # Find a working jq for price files (price files have no Windows paths, so jq should always work)
  PJQ=""
  for candidate in "/c/Program Files/jq/jq.exe" "/c/Program Files/jq/jq" "$(command -v jq 2>/dev/null)"; do
    if [ -n "$candidate" ] && [ -x "$candidate" ]; then
      if echo '{"a":1}' | "$candidate" -r '.a' >/dev/null 2>&1; then
        PJQ="$candidate"
        break
      fi
    fi
  done

  # Auto-detect price file from current model name
  # Try: exact match > partial match > active file > first file
  # Normalize: lowercase, strip hyphens/dots/spaces for fuzzy matching
  _norm() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -d '[:punct:]' | tr -d ' '; }
  price_file=""
  model_norm=$(_norm "$model")

  # 1. Try exact match: normalized model name == normalized price file name
  for pf in "$PRICE_DIR"/*.json; do
    [ -f "$pf" ] || continue
    pf_name=$(basename "$pf" .json)
    if [ "$model_norm" = "$(_norm "$pf_name")" ]; then
      price_file="$pf"
      break
    fi
  done

  # 2. Try partial match: normalized model contains price name, or vice versa
  if [ -z "$price_file" ]; then
    best_len=0
    for pf in "$PRICE_DIR"/*.json; do
      [ -f "$pf" ] || continue
      pf_name=$(basename "$pf" .json)
      pf_norm=$(_norm "$pf_name")
      if [[ "$model_norm" == *"$pf_norm"* ]] || [[ "$pf_norm" == *"$model_norm"* ]]; then
        # Prefer longer match (more specific)
        if [ ${#pf_name} -gt $best_len ]; then
          best_len=${#pf_name}
          price_file="$pf"
        fi
      fi
    done
  fi

  # 3. Fall back to active file
  if [ -z "$price_file" ]; then
    ACTIVE_FILE="${PRICE_DIR}/active"
    if [ -f "$ACTIVE_FILE" ]; then
      active_model=$(cat "$ACTIVE_FILE" 2>/dev/null | tr -d '\r\n\t ')
      [ -f "${PRICE_DIR}/${active_model}.json" ] && price_file="${PRICE_DIR}/${active_model}.json"
    fi
  fi

  # 4. Last resort: first json file
  if [ -z "$price_file" ]; then
    for pf in "$PRICE_DIR"/*.json; do
      [ -f "$pf" ] && { price_file="$pf"; break; }
    done
  fi

  # Use only the matched price file
  if [ -n "$price_file" ] && [ -f "$price_file" ]; then
    price_files=("$price_file")
  else
    price_files=()
  fi

  for price_file in "${price_files[@]}"; do
    [ -f "$price_file" ] || continue

    # Read price file and compact (remove newlines/tabs for grep fallback)
    _pjson=$(cat "$price_file" 2>/dev/null)
    _pjson_compact=$(printf '%s' "$_pjson" | tr -d '\n\t\r')

    if [ -n "$PJQ" ]; then
      pname=$(printf '%s' "$_pjson" | "$PJQ" -r '.name // empty')
      currency=$(printf '%s' "$_pjson" | "$PJQ" -r '.currency // "¥"')
      # Extract both peak and offpeak rates
      pr_hit=$(printf '%s' "$_pjson" | "$PJQ" -r '.peak.cache_hit // 0')
      pr_miss=$(printf '%s' "$_pjson" | "$PJQ" -r '.peak.cache_miss // 0')
      pr_out=$(printf '%s' "$_pjson" | "$PJQ" -r '.peak.output // 0')
      or_hit=$(printf '%s' "$_pjson" | "$PJQ" -r '.offpeak.cache_hit // 0')
      or_miss=$(printf '%s' "$_pjson" | "$PJQ" -r '.offpeak.cache_miss // 0')
      or_out=$(printf '%s' "$_pjson" | "$PJQ" -r '.offpeak.output // 0')
    else
      # grep fallback: compact JSON is single-line, patterns work
      pname=$(grep -o '"name"[[:space:]]*:[[:space:]]*"[^"]*"' <<<"$_pjson_compact" | head -1 | sed 's/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/')
      currency=$(grep -o '"currency"[[:space:]]*:[[:space:]]*"[^"]*"' <<<"$_pjson_compact" | head -1 | sed 's/.*"currency"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/')
      [ -z "$currency" ] && currency="¥"

      # Extract both peak and offpeak rates
      peak_section=$(grep -o '"peak"[^}]*}' <<<"$_pjson_compact" | head -1)
      off_section=$(grep -o '"offpeak"[^}]*}' <<<"$_pjson_compact" | head -1)
      pr_hit=$(grep -o '"cache_hit"[[:space:]]*:[[:space:]]*[0-9.]*' <<<"$peak_section" | grep -o '[0-9.]*$')
      pr_miss=$(grep -o '"cache_miss"[[:space:]]*:[[:space:]]*[0-9.]*' <<<"$peak_section" | grep -o '[0-9.]*$')
      pr_out=$(grep -o '"output"[[:space:]]*:[[:space:]]*[0-9.]*' <<<"$peak_section" | grep -o '[0-9.]*$')
      or_hit=$(grep -o '"cache_hit"[[:space:]]*:[[:space:]]*[0-9.]*' <<<"$off_section" | grep -o '[0-9.]*$')
      or_miss=$(grep -o '"cache_miss"[[:space:]]*:[[:space:]]*[0-9.]*' <<<"$off_section" | grep -o '[0-9.]*$')
      or_out=$(grep -o '"output"[[:space:]]*:[[:space:]]*[0-9.]*' <<<"$off_section" | grep -o '[0-9.]*$')
    fi

    # Calculate cost: peak tokens × peak rate + offpeak tokens × offpeak rate
    if [ -n "$pname" ] && [ -n "$pr_hit" ] && [ -n "$pr_miss" ] && [ -n "$pr_out" ]; then
      total_cost=$(awk "BEGIN {
        peak = ($session_peak_cache * $pr_hit + $session_peak_input * $pr_miss + $session_peak_output * $pr_out) / 1000000
        off = ($session_off_cache * $or_hit + $session_off_input * $or_miss + $session_off_output * $or_out) / 1000000
        printf \"%.2f\", peak + off
      }")
      # Show original model name, with warning if using fallback (active) price
      if [ -n "$price_matched" ]; then
        cost_detail="${cost_detail:+$cost_detail }${model}:${currency}${total_cost}"
      else
        cost_detail="${cost_detail:+$cost_detail }${model}(未配置价格):${currency}${total_cost}"
      fi
    fi
  done
fi

# --- Build the 10-cell progress bar ---
bar=""
if [ -n "$used_pct" ] && [ "$used_pct" != "null" ]; then
  pct_int=$(printf "%.0f" "$used_pct")
  filled=$(( pct_int / 10 ))
  [ $filled -gt 10 ] && filled=10
  empty=$(( 10 - filled ))

  blocks=$(printf '█%.0s' $(seq 1 $filled); printf '░%.0s' $(seq 1 $empty))
  info="${pct_int}%"

  if [ "$pct_int" -gt 80 ] 2>/dev/null; then
    bar=$(printf '\033[31m%s\033[0m %s' "$blocks" "$info")
  elif [ "$pct_int" -gt 50 ] 2>/dev/null; then
    bar=$(printf '\033[32m%s\033[0m %s' "$blocks" "$info")
  else
    bar=$(printf '\033[90m%s\033[0m %s' "$blocks" "$info")
  fi
else
  bar="--%"
fi

# Append session detail
[ -n "$detail" ] && bar="${bar} | [${detail}]"

# Append cost detail
[ -n "$cost_detail" ] && bar="${bar} | ${cost_detail}"

# --- Output ---
printf '%b\n' "${branch} | ${bar}"
相关推荐
正在走向自律1 小时前
飞算JavaAI能搞定充电站故障处置吗?
java·ai编程·java开发·飞算javaai·java代码生成·ai coding模型·springboo
殷紫川2 小时前
AI Agent 安全防护实战:最小权限与 Human-in-the-loop 的工程防线
安全·agent·ai编程
子昕3 小时前
我把 DeepSeek Harness 跑了一遍,终于知道它和 Codex 差在哪了
ai编程
云边有个稻草人3 小时前
飞算JavaAI能处理SaaS套餐变更业务吗?
java·springboot·ai编程·java开发·飞算javaai·java代码生成·ai coding模型
颜进强3 小时前
04 - 一张图看懂 OpenSpec 变更的一生:从创建到归档
前端·后端·ai编程
Cobyte3 小时前
Claude Code 的 Task System 的实现原理
后端·aigc·ai编程
文心快码BaiduComate3 小时前
额度不足的痛,我们懂!文心快码Comate测试版不限量Token第二弹来了!
程序员·ai编程·创业
殷紫川4 小时前
DeepSeek Harness :当"一切皆插件"成为 Agent 的新底座
openai·ai编程·deepseek
沐泽__4 小时前
Vibe Coding 全流程:从需求到上线的 7 步
学习方法·ai编程