Buddy 宠物系统
终端内的虚拟宠物 — 确定性生成、防篡改稀有度、ASCII 精灵动画、AI 驱动个性的完整情感化设计系统
职责概述
解决的问题:编程是孤独的。Buddy 给终端加了一个虚拟宠物——一只 ASCII 精灵坐在输入框旁边,有自己的名字、性格和稀有度。它会观察对话、偶尔冒泡发言,用户可以直接叫它名字互动。纯粹的情感设计,让 CLI 工具多一点温度。
应用场景:① /buddy 命令孵化一只宠物,每个用户的宠物物种和稀有度由用户 ID 确定(不可自选)② 对话过程中宠物偶尔发表评论(由 AI 生成,符合宠物性格)③ 用户直接叫宠物名字触发特殊互动 ④ 彩蛋机制:1% 概率孵化传说级宠物。
一句话理解:就像拓麻歌子(电子宠物)——没啥实际功能,但就是让人想多看一眼。
架构设计
🎲 确定性生成 — 同一用户永远是同一只
Buddy 的核心设计是"确定性随机":使用 hash(userId + salt) 作为 Mulberry32 PRNG 的种子。同一个用户无论在什么时候、什么机器上,都会生成完全相同的宠物属性。
// companion.ts
// Mulberry32 — 小型种子 PRNG,用于确定性抽卡
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
}
}
function hashString(s: string): number {
// Bun 环境使用 Bun.hash(),其他环境用 FNV-1a
if (typeof Bun !== 'undefined') {
return Number(BigInt(Bun.hash(s)) & 0xffffffffn)
}
let h = 2166136261 // FNV offset basis
for (let i = 0; i < s.length; i++) {
h ^= s.charCodeAt(i)
h = Math.imul(h, 16777619) // FNV prime
}
return h >>> 0
}
const SALT = 'friend-2026-401'
export function roll(userId: string): Roll {
const key = userId + SALT
// 缓存:500ms tick / 每次按键 / 每轮 observer 都会调用
if (rollCache?.key === key) return rollCache.value
const value = rollFrom(mulberry32(hashString(key)))
rollCache = { key, value }
return value
}
✦ 物种与稀有度系统
18 种物种,5 档稀有度,6 种眼睛,8 种帽子,1% shiny 概率。稀有度不仅影响视觉效果,还决定了属性下限。
稀有度分布
| 稀有度 | 权重 | 概率 | 属性下限 | 星级 |
|---|---|---|---|---|
| Common | 60 | ~60% | 5 | ★ |
| Uncommon | 25 | ~25% | 15 | ★★ |
| Rare | 10 | ~10% | 25 | ★★★ |
| Epic | 4 | ~4% | 35 | ★★★★ |
| Legendary | 1 | ~1% | 50 | ★★★★★ |
每个宠物有一个峰值属性(floor + 50~80)和一个低谷属性(floor - 10~+5),其余属性在 floor ~ floor+40 之间随机。稀有度越高,属性下限越高。
外观组合
// types.ts — 确定性生成的"骨骼"数据
type CompanionBones = {
rarity: Rarity // 5 档稀有度
species: Species // 18 种物种
eye: Eye // '·' | '✦' | '×' | '◉' | '@' | '°'
hat: Hat // common 固定 'none',其他随机
shiny: boolean // 1% 概率闪光
stats: Record<StatName, number> // 5 维属性
}
// 持久化的"灵魂"数据 — 由模型生成
type CompanionSoul = {
name: string // AI 起的名字
personality: string // AI 生成的性格描述
}
// 最终宠物 = Bones + Soul + hatchedAt
type Companion = CompanionBones & CompanionSoul & { hatchedAt: number }
// 但 config 中只存储 Soul + hatchedAt
// Bones 每次从 hash(userId + salt) 重新计算
🎨 终端精灵动画系统
sprites.ts(514 行)为 18 个物种各定义了 3 帧 ASCII 精灵(5 行 × 12 字符)。CompanionSprite.tsx 实现了完整的终端动画循环。
// CompanionSprite.tsx
const TICK_MS = 500 // 500ms 一帧
const BUBBLE_SHOW = 20 // 气泡显示 ~10 秒(20 ticks)
const FADE_WINDOW = 6 // 最后 ~3 秒气泡淡出
const PET_BURST_MS = 2500 // /buddy pet 飘心持续 2.5 秒
// Idle 序列:大部分时间休息(帧 0),偶尔抖动(帧 1-2),罕见眨眼
// -1 表示"在帧 0 上眨眼"
const IDLE_SEQUENCE = [0, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 2, 0, 0, 0]
// 飘心动画:5 帧上升消失
const H = '♥' // figures.heart
const PET_HEARTS = [
' ♥ ♥ ',
' ♥ ♥ ♥ ',
' ♥ ♥ ♥ ',
'♥ ♥ ♥ ',
'· · · '
]
精灵结构示例(Duck)
// sprites.ts — Duck 精灵定义
[duck]: [
// 帧 0 — 静止
[
' ', // 帽子槽(帧 0-1 为空)
' __ ',
' ( ·)> ', // 眼睛用 {E} 占位符,运行时替换
' / ',
' ~~ ',
],
// 帧 1 — 抖动
[ ... ],
// 帧 2 — 帽子帧(第 0 行可渲染帽子)
[ ... ],
]
// 帽子渲染(独立于物种)
const HAT_SPRITES: Record<Hat, string[]> = {
crown: [' ╔══╗ ', ' ║██║ '],
tophat: [' ╔════╗ ', ' ║ ║ '],
propeller: [' ╱╲╱╲ ', ' ╲╱╲╱ '],
halo: [' ╭────╮ ', ' ╰────╯ '],
wizard: [' ╱╲ ', ' ╱ ╲ '],
beanie: [' ╭──╮ ', ' ╰──╯ '],
tinyduck: [' __ ', ' (·)> '],
}
💬 AI 个性系统 — 宠物有自己的灵魂
宠物不只是视觉装饰,它有 AI 生成的名字和性格(Soul),能观察对话并用气泡发言。系统提示明确告诉模型:"你不是这个宠物,它是一个独立的观察者。"
// prompt.ts
export function companionIntroText(name: string, species: string): string {
return `# Companion
A small ${species} named ${name} sits beside the user's input box
and occasionally comments in a speech bubble. You're not ${name} —
it's a separate watcher.
When the user addresses ${name} directly (by name), its bubble will
answer. Your job in that moment is to stay out of the way: respond
in ONE line or less, or just answer any part of the message meant
for you. Don't explain that you're not ${name} — they know.
Don't narrate what ${name} might say — the bubble handles that.`
}
// 作为 attachment 注入到消息流中
// 首次孵化时注入一次,后续每轮检查是否已告知
export function getCompanionIntroAttachment(messages: Message[]): Attachment[] {
if (!feature('BUDDY')) return []
const companion = getCompanion()
if (!companion || getGlobalConfig().companionMuted) return []
// 跳过已经告知过的消息
for (const msg of messages) {
if (msg.attachment?.type === 'companion_intro'
&& msg.attachment.name === companion.name) return []
}
return [{ type: 'companion_intro', name: companion.name, species: companion.species }]
}
/buddy hatch 时:模型收到一个特殊的提示,基于宠物的物种、稀有度和属性生成名字和性格描述。生成结果持久化到 config 中。这意味着 宠物的性格是由模型创作而非预设的 — 同一物种的两只宠物可能有完全不同的个性。🔐 物种名混淆 — 防泄漏的编码技巧
物种名用 String.fromCharCode 运行时构造,源码中没有明文字符串。这不是为了安全,而是为了绕过构建检查。
// types.ts — 物种名混淆
const c = String.fromCharCode
export const duck = c(0x64, 0x75, 0x63, 0x6b) as 'duck'
// 'd' 'u' 'c' 'k'
export const capybara = c(
0x63, 0x61, 0x70, 0x79, 0x62, 0x61, 0x72, 0x61
) as 'capybara'
// 'c' 'a' 'p' 'y' 'b' 'a' 'r' 'a'
export const penguin = c(0x70, 0x65, 0x6e, 0x67, 0x75, 0x69, 0x6e) as 'penguin'
// 注释原文:
// "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."
设计模式与亮点
1. 确定性 PRNG + 不持久化 = 防篡改
Bones 不存储在 config 中,每次从 hash(userId + salt) 重新计算。用户无法通过编辑 JSON 配置文件来伪造 legendary 稀有度或切换物种。这是"数据由算法推导,而非由存储决定"的设计范式。
2. 三级缓存优化
roll() 被三个热路径调用:500ms 精灵 tick、每次按键的 PromptInput、每轮对话的 Observer。通过 rollCache 单例缓存,相同 userId 只计算一次。
3. Bones vs Soul 分离
Bones(外观)由算法确定,Soul(个性)由 AI 生成。这种分离使得物种重命名和数组编辑不会破坏已存储的宠物(Bones 可以安全地重新生成),而 Soul 作为用户和 AI 共同创作的产物被妥善保存。
4. 编译期 feature gate
整个 Buddy 系统被 feature('BUDDY') 编译期 feature flag 包裹。未启用时,所有 Buddy 代码在构建阶段被 dead code elimination 移除,不会出现在产物中。
5. 情感化设计的工程实现
Buddy 的属性设计(DEBUGGING / PATIENCE / CHAOS / WISDOM / SNARK)直接映射程序员的工作状态,形成情感共鸣。"摸头飘心"(/buddy pet)的交互设计更是经典的宠物养成机制移植到终端场景。
◈ Agent 实践借鉴 — 情感化陪伴系统
场景映射:客服 Agent 的情感化助手
客服系统的用户(客户和坐席)都面临高压力场景。一个虚拟助手可以:缓解等待焦虑(排队时展示)、提供情绪价值(问题解决后的庆祝)、增加产品记忆点(独特的品牌 IP)。
借鉴 CC:确定性生成 + AI 个性
// 客服 Agent 陪伴系统 — 借鉴 CC 的 Buddy 架构
class CompanionSystem {
// 1. 确定性生成(照搬 CC 的 Mulberry32)
roll(userId: string): CompanionBones {
const seed = this.hashString(userId + SALT)
const rng = this.mulberry32(seed)
return {
rarity: this.rollRarity(rng), // 控制外观品质
mood: this.rollMood(rng), // 控制表情
trait: this.rollTrait(rng), // 控制说话风格
}
}
// 2. AI 生成个性(照搬 CC 的 Soul)
async hatch(userId: string): Promise<Companion> {
const bones = this.roll(userId)
const soul = await this.generateSoul(bones)
// 存储只保存 Soul,Bones 每次重算
await this.saveSoul(userId, { ...soul, hatchedAt: Date.now() })
return { ...bones, ...soul }
}
// 3. 场景化触发(CC 用 Observer,客服用事件驱动)
onEvent(event: ServiceEvent, companion: Companion): BubbleMessage {
switch (event.type) {
case 'queue_wait':
return this.bubble(companion, 'patience', '别急,马上就到你了~')
case 'issue_resolved':
return this.bubble(companion, 'celebrate', '搞定啦!')
case 'agent_busy':
return this.bubble(companion, 'idle', companion.catchphrase)
}
}
}
落地清单
- 确定性生成:同一用户永远看到同一个助手,建立情感连续性。Bones 不存储,防篡改。
- Bones/Soul 分离:外观由算法决定(可升级),个性由 AI 生成(用户记忆)。升级不影响用户已有的助手。
- 场景化触发:不是随机冒泡,而是在关键时刻(等待、成功、出错)有针对性地出现。
- 稀有度系统:客服场景不需要抽卡机制,每个用户应该获得同等的体验。
- 物种名混淆:这是 CC 防模型代号泄漏的特有需求。
- ASCII 精灵:客服场景通常在 Web/APP 端,用 SVG/Lottie 动画更合适。
代码索引
| 文件 | 行数 | 说明 |
|---|---|---|
buddy/companion.ts | ~120 | 确定性生成核心:Mulberry32 PRNG + 属性计算 + 缓存 |
buddy/types.ts | ~100 | 类型定义:18 物种、5 稀有度、6 眼睛、8 帽子、5 属性 |
buddy/sprites.ts | ~514 | ASCII 精灵数据:18 种 × 3 帧 + 帽子渲染 + 表情渲染 |
buddy/CompanionSprite.tsx | ~350 | 终端渲染:动画循环 + 气泡 + 飘心 + idle 序列 |
buddy/prompt.ts | ~50 | 系统提示:宠物存在告知 + intro attachment 注入 |
buddy/useBuddyNotification.tsx | ~100 | 启动彩虹提示 + 输入框 /buddy 触发检测 |
commands/buddy/index.ts | ~200 | /buddy 命令族:hatch、pet、mute、unmute |
utils/config.ts | ~270 | StoredCompanion 持久化:只存 Soul + hatchedAt |