Claude Code 里的“电子宠物”:Buddy 伙伴系统全解析
核心概念
Buddy 伙伴系统是 Claude Code 在 2026 年愚人节期间推出的一个“电子宠物”彩蛋(Easter Egg)。它会在终端输入框旁边显示一个 5 行高的 ASCII 艺术小生物,陪伴你写代码。它拥有自己的物种、稀有度、属性值(如“Debug 能力”、“耐心值”),甚至会因为你的敲击而眨眼,并在你通过 /buddy pet 宠溺它时冒出爱心。
源码级拆解
虽然它看起来只是一个花哨的终端组件,但其背后有一套严谨的“确定性生成”机制:
- 确定性哈希生成:Buddy 的外观和属性并非随机生成后存储,而是通过
hash(userId + "friend-2026-401")作为种子,喂给 Mulberry32 PRNG(一种轻量级伪随机数生成器)。这意味着每个用户的 Buddy 都是全宇宙唯一的,但只要 userId 不变,无论你重装多少次,你的 Buddy 永远是同一个。 - 稀有度与属性系统:系统预设了 18 个物种(如 duck, capybara, axolotl)和 5 种稀有度。稀有度权重分布极度不均:Common (60%) 到 Legendary (1%)。稀有度会直接拔高 Buddy 的属性保底值(RARITY_FLOOR)。
- 避开构建检测的技巧:有趣的是,源码为了绕过构建时的“敏感字符串检测”(排除掉类似内部代号的词),将物种名称(如
capybara)通过String.fromCharCode动态编码。这样在编译后的二进制文件中,这些单词不会以明文出现,从而避开安全扫描。 - 骨架与灵魂的分离:
- 骨架(Bones):包括物种、稀有度、属性等。这些从不持久化。每次启动时,代码都会根据 userId 重新“算”一遍。这防止了用户通过修改配置文件来强行把自己的 Buddy 改成传说级。
- 灵魂(Soul):包括名字和性格。这是在 Buddy 第一次“孵化”时由 LLM 生成的,并存储在全局配置中。
- 终端动画渲染:使用了 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 算法和确定性生成逻辑。
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 风格属性。
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 年愚人节期间的上线逻辑。
tsx
export function isBuddyTeaserWindow(): boolean {
if ("external" === 'ant') return true;
const d = new Date();
return d.getFullYear() === 2026 && d.getMonth() === 3 && d.getDate() <= 7;
}