Skip to content
源码分析手册

Claude Code 里的“电子宠物”:Buddy 伙伴系统全解析

核心概念

Buddy 伙伴系统是 Claude Code 在 2026 年愚人节期间推出的一个“电子宠物”彩蛋(Easter Egg)。它会在终端输入框旁边显示一个 5 行高的 ASCII 艺术小生物,陪伴你写代码。它拥有自己的物种、稀有度、属性值(如“Debug 能力”、“耐心值”),甚至会因为你的敲击而眨眼,并在你通过 /buddy pet 宠溺它时冒出爱心。

源码级拆解

虽然它看起来只是一个花哨的终端组件,但其背后有一套严谨的“确定性生成”机制:

  1. 确定性哈希生成:Buddy 的外观和属性并非随机生成后存储,而是通过 hash(userId + "friend-2026-401") 作为种子,喂给 Mulberry32 PRNG(一种轻量级伪随机数生成器)。这意味着每个用户的 Buddy 都是全宇宙唯一的,但只要 userId 不变,无论你重装多少次,你的 Buddy 永远是同一个。
  2. 稀有度与属性系统:系统预设了 18 个物种(如 duck, capybara, axolotl)和 5 种稀有度。稀有度权重分布极度不均:Common (60%) 到 Legendary (1%)。稀有度会直接拔高 Buddy 的属性保底值(RARITY_FLOOR)。
  3. 避开构建检测的技巧:有趣的是,源码为了绕过构建时的“敏感字符串检测”(排除掉类似内部代号的词),将物种名称(如 capybara)通过 String.fromCharCode 动态编码。这样在编译后的二进制文件中,这些单词不会以明文出现,从而避开安全扫描。
  4. 骨架与灵魂的分离
    • 骨架(Bones):包括物种、稀有度、属性等。这些从不持久化。每次启动时,代码都会根据 userId 重新“算”一遍。这防止了用户通过修改配置文件来强行把自己的 Buddy 改成传说级。
    • 灵魂(Soul):包括名字和性格。这是在 Buddy 第一次“孵化”时由 LLM 生成的,并存储在全局配置中。
  5. 终端动画渲染:使用了 Ink 框架,每个物种有 3 帧动画,配合 500ms 的 Tick 周期实现闲置抖动。眨眼周期通过 IDLE_SEQUENCE 索引控制,偶尔会将眼睛字符(如 ·)替换为 -

踩坑指南

  • 不可篡改性:别指望修改 ~/.claude/config.json 来获得稀有物种,那是徒劳的,因为骨架是由哈希值锁死的。
  • 时间锁定:Teaser 提示窗口被硬编码在 2026 年 4 月 1 日至 7 日。过了这个点,它不会再主动跳出来,但功能依然可以通过命令手动开启。
  • 非智能体:Buddy 本质上是一个观察者。虽然模型被告知了 Buddy 的存在(通过 companionIntroText),但模型并不“是”这个 Buddy。

延伸阅读

  • 如果你对 React 如何在终端里处理复杂布局感兴趣,阅读 src/buddy/CompanionSprite.tsx
  • 如果你想看那些藏起来的 ASCII 艺术字,去翻 src/buddy/sprites.ts

源码锚点

  • src/buddy/companion.ts:核心逻辑,包括 PRNG 算法和确定性生成逻辑。
📄 src/buddy/companion.ts — 核心逻辑,包括 PRNG 算法和确定性生成逻辑。L15-25 of 134
typescript
// Mulberry32 — tiny seeded PRNG, good enough for picking ducks
function mulberry32(seed: number): () => number {
  let a = seed >>> 0
  return function () {
    a |= 0
    a = (a + 0x6d2b79f5) | 0
    let t = Math.imul(a ^ (a >>> 15), 1 | a)
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296
  }
}
  • src/buddy/types.ts:定义了 18 个物种、5 种稀有度和 5 种 RPG 风格属性。
📄 src/buddy/types.ts — 定义了 18 个物种、5 种稀有度和 5 种 RPG 风格属性。L1-30 of 149
typescript
export const RARITIES = [
  'common',
  'uncommon',
  'rare',
  'epic',
  'legendary',
] as const
export type Rarity = (typeof RARITIES)[number]

// One species name collides with a model-codename canary in excluded-strings.txt.
// The check greps build output (not source), so runtime-constructing the value keeps
// the literal out of the bundle while the check stays armed for the actual codename.
// All species encoded uniformly; `as` casts are type-position only (erased pre-bundle).
const c = String.fromCharCode
// biome-ignore format: keep the species list compact

export const duck = c(0x64,0x75,0x63,0x6b) as 'duck'
export const goose = c(0x67, 0x6f, 0x6f, 0x73, 0x65) as 'goose'
export const blob = c(0x62, 0x6c, 0x6f, 0x62) as 'blob'
export const cat = c(0x63, 0x61, 0x74) as 'cat'
export const dragon = c(0x64, 0x72, 0x61, 0x67, 0x6f, 0x6e) as 'dragon'
export const octopus = c(0x6f, 0x63, 0x74, 0x6f, 0x70, 0x75, 0x73) as 'octopus'
export const owl = c(0x6f, 0x77, 0x6c) as 'owl'
export const penguin = c(0x70, 0x65, 0x6e, 0x67, 0x75, 0x69, 0x6e) as 'penguin'
export const turtle = c(0x74, 0x75, 0x72, 0x74, 0x6c, 0x65) as 'turtle'
export const snail = c(0x73, 0x6e, 0x61, 0x69, 0x6c) as 'snail'
export const ghost = c(0x67, 0x68, 0x6f, 0x73, 0x74) as 'ghost'
export const axolotl = c(0x61, 0x78, 0x6f, 0x6c, 0x6f, 0x74, 0x6c) as 'axolotl'
export const capybara = c(
  0x63,
  • src/buddy/useBuddyNotification.tsx:控制 2026 年愚人节期间的上线逻辑。
📄 src/buddy/useBuddyNotification.tsx — 控制 2026 年愚人节期间的上线逻辑。L12-16 of 98
tsx
export function isBuddyTeaserWindow(): boolean {
  if ("external" === 'ant') return true;
  const d = new Date();
  return d.getFullYear() === 2026 && d.getMonth() === 3 && d.getDate() <= 7;
}

基于 Claude Code v2.1.88 开源快照的深度分析