ByeByeCode 状态栏 Token 读取原理深度解析

本文将详细解析 ByeByeCode 项目如何读取和显示 Claude Code 的 Token 使用信息,帮助你理解整个数据流的工作原理。


引言

当你使用 Claude Code 时,状态栏会显示类似 42.5% · 85.2k tokens 的信息。你是否好奇过这些数字是从哪里来的?它们是如何被计算和显示的?

ByeByeCode 是一个为 Claude Code 提供自定义状态栏的 Rust CLI 工具。它通过读取 Claude Code 传递的数据和对话记录文件,实时计算并显示 Token 使用情况。

本文将带你深入源码,理解整个 Token 信息读取的完整流程。


整体架构

┌─────────────┐     stdin (JSON)     ┌─────────────┐     stdout     ┌─────────────┐
│ Claude Code │ ──────────────────▶  │ ByeByeCode  │ ─────────────▶ │   状态栏    │
└─────────────┘                      └─────────────┘                └─────────────┘
                                           │
                                           │ 读取 transcript_path
                                           ▼
                                    ┌─────────────────┐
                                    │  JSONL 对话文件  │
                                    │  (会话记录)      │
                                    └─────────────────┘
                                           │
                                           │ 提取 usage 数据
                                           ▼
                                    ┌─────────────────┐
                                    │   Token 统计    │
                                    │ input/output/   │
                                    │ cache tokens    │
                                    └─────────────────┘

核心流程

  1. Claude Code 将 JSON 数据通过 stdin 传递给 ByeByeCode
  2. JSON 中包含 transcript_path,指向对话记录文件
  3. ByeByeCode 解析 JSONL 文件,提取 Token 使用数据
  4. 计算百分比,格式化后输出到 stdout

为什么是 Claude Code 主动传递数据?

你可能会问:为什么不让 ByeByeCode 自己去读取 Claude Code 的数据呢?

这是一个很好的问题,涉及到进程间通信的核心设计决策。

关键原因:数据所有权

Claude Code 传递给 ByeByeCode 的数据(如 modelworkspacetranscript_pathcost只存在于 Claude Code 的进程内存中,外部程序根本无法独立获取这些信息。

┌─────────────────────────────────────────────────────────────────┐
│                    Claude Code 进程内存                          │
├─────────────────────────────────────────────────────────────────┤
│  model.id = "claude-opus-4-5"          ← 只存在于内存中          │
│  model.display_name = "Opus 4.5"       ← 没有对外暴露的文件      │
│  workspace.current_dir = "/home/..."   ← 实时运行状态            │
│  transcript_path = "~/.claude/..."     ← 当前会话路径            │
│  cost.total_cost_usd = 0.05            ← 累计费用统计            │
└─────────────────────────────────────────────────────────────────┘
                              │
                              │ 只能通过 IPC 传递
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                    ByeByeCode 进程                               │
│                                                                  │
│  无法直接访问 Claude Code 的内存空间                              │
│  无法知道当前使用的是哪个模型                                     │
│  无法知道当前工作目录                                             │
│  无法知道对话记录文件的位置                                       │
└─────────────────────────────────────────────────────────────────┘

进程间通信流程

Claude Code 使用经典的 fork-exec-pipe 模式来调用 ByeByeCode:

Claude Code 进程(父进程)
    │
    ├─ 1. 准备 JSON 数据
    │     从进程内存中获取:
    │     - model.id, model.display_name
    │     - workspace.current_dir
    │     - transcript_path(当前会话文件路径)
    │     - cost 统计数据
    │
    ├─ 2. fork() 创建子进程
    │
    ├─ 3. exec() 执行 byebyecode 命令
    │
    ├─ 4. 通过管道将 JSON 写入子进程的 stdin
    │     ┌──────────────────────────────────┐
    │     │ {"model":{"id":"..."},...}       │
    │     └──────────────────────────────────┘
    │
    └─ 5. 读取子进程的 stdout 作为状态栏内容
         ┌──────────────────────────────────┐
         │ ◉ 42.5% · 85.2k tokens           │
         └──────────────────────────────────┘
              │
              ▼
    ByeByeCode 进程(子进程)
         │
         ├─ 从 stdin 读取 JSON
         ├─ 解析数据、读取 JSONL 文件
         ├─ 渲染状态栏字符串
         └─ 输出到 stdout → 父进程捕获

两种方案对比

对比维度 stdin 传递(当前方案) 自行读取(假设方案)
数据完整性 ✅ 完整(Claude Code 提供所有数据) ❌ 不完整(无法获取内存中的运行时状态)
实时性 ✅ 每次都是最新数据 ❌ 可能读到过期文件
复杂度 ✅ 低(简单的管道通信) ❌ 高(需要轮询文件、处理竞态条件)
耦合度 ✅ 低(只依赖 JSON 格式) ❌ 高(依赖 Claude Code 内部文件结构)
可靠性 ✅ 高(同步调用) ❌ 低(文件可能被其他进程修改)

Unix 管道哲学

这种设计遵循了 Unix 的核心理念:

“Write programs that do one thing and do it well. Write programs to work together. Write programs to handle text streams, because that is a universal interface.” — Doug McIlroy

  • Claude Code:负责收集数据、管理会话(数据生产者)
  • ByeByeCode:负责格式化显示(数据消费者)
  • stdin/stdout:通用的文本流接口

这种 生产者-消费者模式 使得两个程序完全解耦,各自专注于自己的职责。


数据来源详解

Claude Code 如何调用 ByeByeCode

Claude Code 通过配置 settings.json 中的 statusLine 字段来调用外部程序:

{
  "statusLine": {
    "enabled": true,
    "command": "byebyecode"
  }
}

当 Claude Code 需要更新状态栏时,它会启动 byebyecode 进程,并通过 stdin 传入 JSON 数据。

stdin 传入的 JSON 数据结构

让我们看看 Claude Code 传入的数据结构:

// 文件: src/config/types.rs (第 117-123 行)

#[derive(Deserialize)]
pub struct InputData {
    pub model: Model,              // 当前使用的模型信息
    pub workspace: Workspace,      // 工作区信息
    pub transcript_path: String,   // 对话记录文件路径 ← 关键!
    pub cost: Option
<Cost>,        // 费用信息
    pub output_style: Option
<OutputStyle>, // 输出样式
}

关键字段说明

字段 类型 说明
model Model 包含模型 ID 和显示名称
workspace Workspace 包含当前工作目录
transcript_path String 对话记录文件的完整路径
cost Option
费用统计(可选)

stdin 读取代码

// 文件: src/main.rs (第 162-174 行)

// 从 stdin 读取 Claude Code 传入的 JSON 数据
let stdin = io::stdin();
let input: InputData = serde_json::from_reader(stdin.lock())?;

// 收集所有段落的数据
let segments_data = collect_all_segments(&config, &input);

// 渲染状态栏
let generator = StatusLineGenerator::new(config);
let statusline = generator.generate(segments_data);

// 输出到 stdout(Claude Code 会读取并显示)
println!("{}", statusline);

JSONL 对话文件解析

文件位置

transcript_path 指向的文件通常位于:

~/.claude/projects/<project-hash>/sessions/<session-uuid>.jsonl

文件格式

这是一个 JSONL(JSON Lines)格式的文件,每行是一个独立的 JSON 对象,记录了对话的每一轮交互:

{"type":"user","message":{"content":"你好"},"uuid":"abc-123"}
{"type":"assistant","message":{"content":"你好!","usage":{"input_tokens":100,"output_tokens":50}},"uuid":"def-456"}
{"type":"user","message":{"content":"帮我写代码"},"uuid":"ghi-789","parentUuid":"def-456"}
{"type":"assistant","message":{"content":"好的...","usage":{"input_tokens":500,"output_tokens":200}},"uuid":"jkl-012"}

TranscriptEntry 数据结构

// 文件: src/config/types.rs (第 409-419 行)

#[derive(Deserialize)]
pub struct TranscriptEntry {
    pub r#type: Option
<String>,    // 消息类型: "user" | "assistant" | "summary"
    pub message: Option
<Message>,  // 消息内容(包含 usage 数据)
    pub leaf_uuid: Option
<String>, // 叶子节点 UUID(用于 summary 类型)
    pub uuid: Option
<String>,      // 消息 UUID
    pub parent_uuid: Option
<String>, // 父消息 UUID
    pub summary: Option
<String>,   // 摘要内容
}

#[derive(Deserialize)]
pub struct Message {
    pub usage: Option
<Usage>,  // Token 使用统计 ← 这里包含我们需要的数据!
}

Token 数据结构

RawUsage:原始数据格式

由于需要兼容不同的 LLM 提供商(Anthropic、OpenAI 等),RawUsage 结构设计得非常灵活:

// 文件: src/config/types.rs (第 135-185 行)

#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct RawUsage {
    // ========== Anthropic 风格 ==========
    pub input_tokens: Option
<u32>,           // 输入 token 数
    pub output_tokens: Option
<u32>,          // 输出 token 数
    pub cache_creation_input_tokens: Option
<u32>,  // 缓存创建的 token
    pub cache_read_input_tokens: Option
<u32>,      // 从缓存读取的 token

    // ========== OpenAI 风格 ==========
    pub prompt_tokens: Option
<u32>,          // 提示词 token(对应 input_tokens)
    pub completion_tokens: Option
<u32>,      // 完成 token(对应 output_tokens)
    pub total_tokens: Option
<u32>,           // 总 token 数
    pub cached_tokens: Option
<u32>,          // 缓存 token

    // OpenAI 嵌套详情
    pub prompt_tokens_details: Option
<PromptTokensDetails>,

    // 未知字段(向前兼容)
    #[serde(flatten)]
    pub extra: HashMap<String, serde_json::Value>,
}

为什么需要这么多字段?

不同的 API 返回格式不同:

提供商 输入字段 输出字段 缓存字段
Anthropic input_tokens output_tokens cache_read_input_tokens
OpenAI prompt_tokens completion_tokens cached_tokens

NormalizedUsage:标准化格式

为了统一处理,需要将不同格式标准化:

// 文件: src/config/types.rs (第 188-243 行)

#[derive(Debug, Clone, Serialize, Default, PartialEq)]
pub struct NormalizedUsage {
    pub input_tokens: u32,              // 标准化后的输入 token
    pub output_tokens: u32,             // 标准化后的输出 token
    pub total_tokens: u32,              // 总 token 数
    pub cache_creation_input_tokens: u32, // 缓存创建 token
    pub cache_read_input_tokens: u32,     // 缓存读取 token
    pub calculation_source: String,       // 计算来源(调试用)
    pub raw_data_available: Vec
<String>,  // 原始数据可用字段
}

三种 Token 计算方法

impl NormalizedUsage {
    /// 上下文窗口使用的 token(用于显示百分比)
    /// 包含:输入 + 缓存创建 + 缓存读取 + 输出
    pub fn context_tokens(&self) -> u32 {
        self.input_tokens
            + self.cache_creation_input_tokens
            + self.cache_read_input_tokens
            + self.output_tokens
    }

    /// 用于费用计算的总 token
    pub fn total_for_cost(&self) -> u32 {
        if self.total_tokens > 0 {
            self.total_tokens
        } else {
            self.input_tokens + self.output_tokens
                + self.cache_creation_input_tokens
                + self.cache_read_input_tokens
        }
    }

    /// 用于显示的 token 数(智能选择)
    pub fn display_tokens(&self) -> u32 {
        let context = self.context_tokens();
        if context > 0 { return context; }
        if self.total_tokens > 0 { return self.total_tokens; }
        self.input_tokens.max(self.output_tokens)  // 兜底方案
    }
}

核心解析逻辑

parse_transcript_usage 函数

这是解析 Token 数据的入口函数:

// 文件: src/core/segments/context_window.rs (第 86-102 行)

fn parse_transcript_usage<P: AsRef<Path>>(transcript_path: P) -> Option<u32> {
    let path = transcript_path.as_ref();

    // 尝试从当前对话文件解析
    if let Some(usage) = try_parse_transcript_file(path) {
        return Some(usage);
    }

    // 如果文件不存在,尝试从项目历史中查找
    if !path.exists() {
        if let Some(usage) = try_find_usage_from_project_history(path) {
            return Some(usage);
        }
    }

    None
}

try_parse_transcript_file:核心解析函数

// 文件: src/core/segments/context_window.rs (第 104-148 行)

fn try_parse_transcript_file(path: &Path) -> Option
<u32> {
    // 1. 打开并读取文件
    let file = fs::File::open(path).ok()?;
    let reader = BufReader::new(file);
    let lines: Vec
<String> = reader.lines()
        .collect::<Result<Vec<_>, _>>()
        .unwrap_or_default();

    if lines.is_empty() {
        return None;
    }

    // 2. 检查最后一行是否是 summary(摘要)类型
    let last_line = lines.last()?.trim();
    if let Ok(entry) = serde_json::from_str::
<TranscriptEntry>(last_line) {
        if entry.r#type.as_deref() == Some("summary") {
            // 如果是摘要,通过 leafUuid 查找原始消息
            if let Some(leaf_uuid) = &entry.leaf_uuid {
                let project_dir = path.parent()?;
                return find_usage_by_leaf_uuid(leaf_uuid, project_dir);
            }
        }
    }

    // 3. 正常情况:从后往前找最后一条 assistant 消息
    for line in lines.iter().rev() {
        let line = line.trim();
        if line.is_empty() { continue; }

        if let Ok(entry) = serde_json::from_str::
<TranscriptEntry>(line) {
            // 只处理 assistant 类型的消息
            if entry.r#type.as_deref() == Some("assistant") {
                if let Some(message) = &entry.message {
                    if let Some(raw_usage) = &message.usage {
                        // 4. 标准化 token 数据并返回
                        let normalized = raw_usage.clone().normalize();
                        return Some(normalized.display_tokens());
                    }
                }
            }
        }
    }

    None
}

解析流程图

┌─────────────────────────────────────────┐
│         读取 JSONL 文件                   │
└─────────────────┬───────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────┐
│    检查最后一行是否是 "summary" 类型      │
└─────────────────┬───────────────────────┘
                  │
         ┌────────┴────────┐
         │                 │
    是 summary         不是 summary
         │                 │
         ▼                 ▼
┌─────────────────┐  ┌─────────────────────┐
│ 通过 leafUuid   │  │ 从后往前遍历         │
│ 查找原始消息     │  │ 找 "assistant" 消息  │
└────────┬────────┘  └──────────┬──────────┘
         │                      │
         └──────────┬───────────┘
                    │
                    ▼
         ┌─────────────────────┐
         │ 提取 message.usage  │
         └──────────┬──────────┘
                    │
                    ▼
         ┌─────────────────────┐
         │ raw_usage.normalize()│
         └──────────┬──────────┘
                    │
                    ▼
         ┌─────────────────────┐
         │ display_tokens()    │
         │ 返回 token 数量      │
         └─────────────────────┘

normalize:Token 数据标准化

// 文件: src/config/types.rs (第 322-398 行)

impl RawUsage {
    pub fn normalize(self) -> NormalizedUsage {
        let mut result = NormalizedUsage::default();

        // 合并输入 token(优先级:Anthropic > OpenAI)
        let input = self.input_tokens
            .or(self.prompt_tokens)
            .unwrap_or(0);

        // 合并输出 token
        let output = self.output_tokens
            .or(self.completion_tokens)
            .unwrap_or(0);

        // 合并缓存创建 token
        let cache_creation = self.cache_creation_input_tokens
            .or(self.cache_creation_prompt_tokens)
            .unwrap_or(0);

        // 合并缓存读取 token(多种来源)
        let cache_read = self.cache_read_input_tokens
            .or(self.cache_read_prompt_tokens)
            .or(self.cached_tokens)
            .or_else(|| {
                // 回退到 OpenAI 嵌套格式
                self.prompt_tokens_details
                    .as_ref()
                    .and_then(|d| d.cached_tokens)
            })
            .unwrap_or(0);

        // 计算总数
        let total = self.total_tokens.unwrap_or_else(|| {
            input + output + cache_read + cache_creation
        });

        result.input_tokens = input;
        result.output_tokens = output;
        result.total_tokens = total;
        result.cache_creation_input_tokens = cache_creation;
        result.cache_read_input_tokens = cache_read;

        result
    }
}

状态栏渲染

ContextWindowSegment:计算上下文使用率

// 文件: src/core/segments/context_window.rs (第 23-78 行)

impl Segment for ContextWindowSegment {
    fn collect(&self, input: &InputData) -> Option
<SegmentData> {
        // 1. 获取模型的上下文限制(如 200k)
        let context_limit = Self::get_context_limit_for_model(&input.model.id);

        // 2. 解析 token 使用量
        let context_used_token_opt = parse_transcript_usage(&input.transcript_path);

        // 3. 计算显示格式
        let (percentage_display, tokens_display) = match context_used_token_opt {
            Some(context_used_token) => {
                // 计算使用百分比
                let context_used_rate =
                    (context_used_token as f64 / context_limit as f64) * 100.0;

                // 格式化百分比:整数不带小数点,否则保留一位
                let percentage = if context_used_rate.fract() == 0.0 {
                    format!("{:.0}%", context_used_rate)
                } else {
                    format!("{:.1}%", context_used_rate)
                };

                // 格式化 token 数:超过 1000 用 k 表示
                let tokens = if context_used_token >= 1000 {
                    let k_value = context_used_token as f64 / 1000.0;
                    if k_value.fract() == 0.0 {
                        format!("{}k", k_value as u32)
                    } else {
                        format!("{:.1}k", k_value)
                    }
                } else {
                    context_used_token.to_string()
                };

                (percentage, tokens)
            }
            None => ("-".to_string(), "-".to_string()),
        };

        // 4. 返回段落数据
        Some(SegmentData {
            primary: format!("{} · {} tokens", percentage_display, tokens_display),
            secondary: String::new(),
            metadata: /* ... */,
        })
    }
}

显示格式示例

Token 数量 上下文限制 显示结果
85,200 200,000 42.6% · 85.2k tokens
150,000 200,000 75% · 150k tokens
500 200,000 0.3% · 500 tokens

StatusLineGenerator:最终渲染

// 文件: src/core/statusline.rs (第 217-283 行)

fn render_segment(&self, config: &SegmentConfig, data: &SegmentData) -> String {
    // 1. 获取图标
    let icon = self.get_icon(config);  // 如 "◉" 或 Nerd Font 图标

    // 2. 应用颜色
    let icon_colored = self.apply_color(&icon, config.colors.icon.as_ref());
    let text_styled = self.apply_style(
        &data.primary,  // "42.6% · 85.2k tokens"
        config.colors.text.as_ref(),
        config.styles.text_bold,
    );

    // 3. 组合输出
    format!("{} {}", icon_colored, text_styled)
    // 输出类似:"\x1b[38;5;208m◉\x1b[0m \x1b[37m42.6% · 85.2k tokens\x1b[0m"
}

完整数据流程图

┌──────────────────────────────────────────────────────────────────────────────┐
│                              完整数据流程                                      │
└──────────────────────────────────────────────────────────────────────────────┘

1. Claude Code 启动 ByeByeCode 进程
   ┌─────────────────────────────────────────────────────────────────────────┐
   │  $ byebyecode                                                            │
   │                                                                          │
   │  stdin ◄── JSON 数据:                                                    │
   │  {                                                                       │
   │    "model": {"id": "claude-3-7-sonnet", "display_name": "Claude 3.7"},   │
   │    "workspace": {"current_dir": "/home/user/project"},                   │
   │    "transcript_path": "~/.claude/projects/.../sessions/xxx.jsonl"        │
   │  }                                                                       │
   └─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
2. 反序列化 JSON 到 InputData
   ┌─────────────────────────────────────────────────────────────────────────┐
   │  let input: InputData = serde_json::from_reader(stdin.lock())?;         │
   └─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
3. 读取 transcript_path 指向的 JSONL 文件
   ┌─────────────────────────────────────────────────────────────────────────┐
   │  ~/.claude/projects/.../sessions/xxx.jsonl:                             │
   │                                                                          │
   │  {"type":"user","message":{...},"uuid":"a1"}                            │
   │  {"type":"assistant","message":{"usage":{"input_tokens":100,...}},...}  │
   │  {"type":"user","message":{...},"uuid":"b2"}                            │
   │  {"type":"assistant","message":{"usage":{"input_tokens":500,...}},...}  │  ◄── 找这条!
   └─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
4. 从最后一条 assistant 消息提取 RawUsage
   ┌─────────────────────────────────────────────────────────────────────────┐
   │  RawUsage {                                                              │
   │    input_tokens: Some(500),                                              │
   │    output_tokens: Some(200),                                             │
   │    cache_read_input_tokens: Some(1000),                                  │
   │    ...                                                                   │
   │  }                                                                       │
   └─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
5. 标准化为 NormalizedUsage
   ┌─────────────────────────────────────────────────────────────────────────┐
   │  let normalized = raw_usage.normalize();                                 │
   │                                                                          │
   │  NormalizedUsage {                                                       │
   │    input_tokens: 500,                                                    │
   │    output_tokens: 200,                                                   │
   │    cache_read_input_tokens: 1000,                                        │
   │    total_tokens: 1700,                                                   │
   │  }                                                                       │
   └─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
6. 计算显示 token 数
   ┌─────────────────────────────────────────────────────────────────────────┐
   │  normalized.display_tokens()                                             │
   │  → context_tokens() = 500 + 0 + 1000 + 200 = 1700                       │
   └─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
7. ContextWindowSegment 计算百分比
   ┌─────────────────────────────────────────────────────────────────────────┐
   │  context_limit = 200000  (根据模型确定)                                   │
   │  percentage = (1700 / 200000) * 100 = 0.85%                              │
   │  display = "0.9% · 1.7k tokens"                                          │
   └─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
8. StatusLineGenerator 渲染带颜色的输出
   ┌─────────────────────────────────────────────────────────────────────────┐
   │  stdout ──▶ "\x1b[38;5;208m◉\x1b[0m \x1b[37m0.9% · 1.7k tokens\x1b[0m"  │
   │                                                                          │
   │  显示效果:◉ 0.9% · 1.7k tokens                                          │
   └─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
9. Claude Code 读取 stdout 并显示在状态栏
   ┌─────────────────────────────────────────────────────────────────────────┐
   │  ┌───────────────────────────────────────────────────────────────┐      │
   │  │ byebyecode │ ◆ main ● │ ◉ 0.9% · 1.7k tokens │ opus-4-5     │      │
   │  └───────────────────────────────────────────────────────────────┘      │
   └─────────────────────────────────────────────────────────────────────────┘

总结

核心要点

  1. 数据来源:Claude Code 通过 stdin 传递 JSON,其中 transcript_path 指向对话记录文件

  2. 文件格式:对话记录是 JSONL 格式,每条 assistant 消息的 message.usage 包含 token 统计

  3. 多格式兼容RawUsage 设计支持 Anthropic 和 OpenAI 两种格式,通过 normalize() 统一处理

  4. 智能显示display_tokens() 根据可用数据智能选择最合适的 token 数进行显示

  5. 管道通信:整个过程使用 Unix 管道(stdin/stdout)进行进程间通信,简洁高效

关键代码文件

文件 功能
src/main.rs 入口,stdin 读取
src/config/types.rs 数据结构定义
src/core/segments/context_window.rs Token 解析逻辑
src/core/statusline.rs 状态栏渲染

扩展阅读

如果你想深入了解:

  • serde 库如何实现 JSON 反序列化
  • ANSI 转义序列如何实现终端颜色
  • Rust 错误处理Option? 操作符的使用

本文基于 ByeByeCode 源码分析,希望能帮助你理解状态栏 Token 显示的工作原理。

发表回复

Your email address will not be published. Required fields are marked *.

*
*