ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

CodePilot Buddy 游戏化系统深度解析:确定性伙伴生成、双模式心跳与定时任务调度的完整技术实现

CodePilot Buddy 游戏化系统深度解析:确定性伙伴生成、双模式心跳与定时任务调度的完整技术实现 人工智能AI 应用AI Agent交互助手MCP Clients本地部署【免费下载链接】CodePilotA multi-model AI agent desktop client — connect any AI provider, extend with MCP skills, control from your phone. Built with Electron Next.js.项目地址https://gitcode.com/gh_mirrors/co0dep/CodePilot点击查看免费下载本篇技术指南系统拆解 CodePilotElectron Next.js 多模型 AI 桌面客户端内置的 Buddy 游戏化系统从确定性 PRNG 伙伴生成与五级稀有度进化到双模式心跳机制与不可见标记协议再到 SQLite 持久化定时任务调度器、通知队列与按会话隔离的记忆提取。读者读完将掌握一套完整的、可直接复用的AI 伙伴工程实现方案包括数据流设计、多文件状态同步的防分叉策略以及 cron 边界解析的务实取舍。一、系统定位与总体架构Buddy 是 CodePilot 中一套将 AI 助理从工具转变为伙伴的游戏化子系统。它不是一个单纯的宠物皮肤而是贯穿记忆、心跳、定时任务、通知、进化的产品架构核心。技术实现完整记录于 docs/handover/buddy-gamification.md产品设计思考可参考 docs/insights/buddy-gamification.md。从源码结构看Buddy 系统横跨 6 个子系统核心数据流为Wizard/孵化 API → state.json(buddy) → context-assembler(性格 prompt) → DashboardPanel(看板卡) → ChatListPanel(侧栏) → MessageItem(聊天头像) → ChatEmptyState(空状态)1.1 数据存储全景Buddy 的状态分散存储在多个位置各有明确职责数据位置格式Buddy 属性{workspace}/.codepilot/state.json→buddy字段BuddyData(species/rarity/stats/buddyName/emoji)性格提示{workspace}/soul.md→## Buddy Trait节Markdown心跳状态state.json→lastHeartbeatDate/heartbeatEnabledISO date string / boolean定时任务SQLitescheduled_tasks表durable/ globalThis Mapsession-onlyScheduledTasktype通知队列globalThis ring buffer50 条上限QueuedNotification[]记忆提取计数globalThisMapsessionId, numberper-session counter其中BuddyData的核心结构定义在 src/lib/buddy.tsexport interface BuddyData { species: Species; // 16 种物种之一 rarity: Rarity; // common → legendary 五级 stats: RecordStatName, number; // creativity/patience/insight/humor/precision emoji: string; // 物种 emoji peakStat: StatName; // 峰值属性决定性格 hint hatchedAt: string; // 孵化时间进化天数计算的起点 buddyName?: string; // 用户自定义名字 }二、Buddy 生成与进化确定性 PRNG 与五级稀有度2.1 核心文件与生成算法Buddy 生成的唯一入口是 src/lib/buddy.ts核心是deterministic PRNG以 workspace path timestamp 拼接为种子先经 FNV-1a 风格哈希hashString再用 Mulberry32 算法驱动随机序列。generateBuddy(seed)的完整流程为hashString(seed :buddy-2026)产生 32 位哈希mulberry32(hash)创建 PRNG 实例rollRarity(rng)按权重表抽取稀有度pickRandom(rng, SPECIES)从 16 种物种中抽取rollStats(rng, rarity)生成 5 项属性并确定 peakStat这一设计的工程意义在于同一个工作区、同一个时间戳生成的 Buddy 永远一致避免了刷新页面 Buddy 就变了的 bug也让调试与测试完全可预测同一种子必得同一结果。2.2 稀有度权重与属性区间源码中的RARITY_WEIGHTS和RARITY_FLOORS定义了稀有度的概率与属性下限const RARITY_WEIGHTS { common: 60, uncommon: 25, rare: 10, epic: 4, legendary: 1 }; const RARITY_FLOORS { common: 5, uncommon: 15, rare: 25, epic: 35, legendary: 50 };对应概率为 60% / 25% / 10% / 4% / 1%。属性生成逻辑rollStats也随稀有度变化峰值属性peak statfloor 50 rand(0~30)上限 100短板属性dump statfloor - 10 rand(0~15)下限 1其余属性floor rand(0~40)均匀散布2.3 稀有度的功能性差异稀有度不是纯装饰——src/lib/buddy.ts 中的getRarityAbilities()将稀有度映射为实际能力能力CommonUncommonRareEpicLegendary称号title❌✅✅✅✅性格增强双属性 trait❌❌✅✅✅记忆提取加速每 2 轮替代每 3 轮❌❌❌✅✅传说特效shimmer 光效❌❌❌❌✅这印证了产品文档 docs/insights/buddy-gamification.md 中Epic 玩家的 Buddy 确实比 Common 更聪明的设计记忆提取频率翻倍让稀有度成为真实的差异化体验。2.4 进化系统checkEvolution(buddy, memoryCount, daysActive, conversationCount)返回{ canEvolve, nextRarity, requirements, current }。源码 src/lib/buddy.ts 中的EVOLUTION_REQUIREMENTS定义了三级进化门槛进化路径记忆数活跃天数对话数Common → Uncommon10720Uncommon → Rare302150Rare → Epic6045100Epic → Legendary10090200注意当前源码中checkEvolution的签名已收敛为(buddy, memoryCount)对话数通过conversationCount memoryCount * 3近似推导。evolveBuddy(buddy)执行进化稀有度 1全属性按稀有度 floor 差值提升newFloor - oldFloor上限 100。进化不是自动触发的——用户必须主动点检查进化这是一个刻意设计的主动仪式。2.5 API 路由POST /api/workspace/hatch-buddy— 生成或更名 buddy追加## Buddy Trait到 soul.md。完整实现在 src/app/api/workspace/hatch-buddy/route.ts种子为workspacePath : new Date().toISOString()若 buddy 已存在且传入buddyName仅更新名字并返回{ buddy, alreadyHatched: true }将峰值属性对应的性格 hint 追加到 soul.md兼容soul.md/Soul.md/SOUL.md三种大小写变体向最新会话注入一条带show-widget卡片的消息展示稀有度标签、属性条与传说 shimmer 光效POST /api/workspace/evolve-buddy— 检查并执行进化更新 soul.md trait 节2.6 3D 图片资源的使用位置Buddy 的视觉呈现依赖public/buddy/目录下的 3D 风格 PNG 资源src/lib/buddy.ts 中的SPECIES_IMAGE_URL映射。物种图与蛋图EGG_IMAGE_URL在多个组件中按有/无 Buddy两种状态切换位置有 Buddy无 Buddy文件Wizard Step 33D 物种图 渐变背景—OnboardingWizard.tsx聊天空状态助理3D 物种图 渐变背景3D 蛋图MessageList.tsx新建聊天入口—3D 蛋图 (24px/14px)ChatEmptyState.tsx侧栏推广卡—3D 蛋图 (20px)ChatEmptyState.tsx→AssistantPromoCard看板 Buddy 卡3D 物种图 渐变背景3D 蛋图DashboardPanel.tsx跨组件传递使用globalThis.__codepilot_buddy_info__由 ChatView 设置MessageList 读取。稀有度渐变背景由RARITY_BG_GRADIENT定义common 灰 → legendary 金。三、心跳系统双模式设计与不可见标记协议心跳是 Buddy 系统中迭代次数最多、状态管理最复杂的子系统产品文档记载经历了 5 轮审查才稳定。3.1 完整心跳触发链路早期的完整心跳autoTrigger 模式链路为useAssistantTrigger: 空会话 state.buddy 存在 data.needsHeartbeat (server-computed) → autoTrigger: true, content: 心跳检查 → context-assembler: isHeartbeatTrigger autoTrigger userPrompt.includes(心跳检查) → buildHeartbeatInstructions() (完整 tick) → route.ts (collectStreamResponse): isHeartbeatTurn autoTrigger content.includes(心跳检查) → HEARTBEAT_OK 处理 / 更新 lastHeartbeatDate3.2 软心跳链路普通对话附带当用户主动发消息且心跳过期时走软心跳路径context-assembler: !autoTrigger shouldRunHeartbeat(state) → 注入 buildSoftHeartbeatHint() → AI 回复中包含 !-- heartbeat-done -- 标记 route.ts (finally): fullText.includes(!-- heartbeat-done --) → 更新 lastHeartbeatDate route.ts (保存前): contentBlocks 中所有 text block 清除 !-- heartbeat-done -- → 持久化消息无标记痕迹3.3 关键演进从关键词检测到不可见标记产品文档 docs/insights/buddy-gamification.md 完整记录了三次迭代无检测心跳过期 turn 只要收到非空回复就标记完成——AI 根本没看 HEARTBEAT.md 也算完成关键词检测heartbeat|心跳|记忆|memory|检查|提醒等词误判率极高用户讨论 memory leak 也会命中收窄到HEARTBEAT.md|日常检查又太严AI 用自然语言表达时检测不到不可见标记最终方案在 soft hint 中指示 AI 完成检查后输出!-- heartbeat-done --后端检测标记更新lastHeartbeatDate持久化前从所有 contentBlock text 中清除——包括 JSON 序列化路径tool_use 场景这个方案的精妙之处在于AI 的行为与系统的状态机通过一个约定的信号量解耦。AI 有自由度决定检查方式自然语言、MCP 工具、简短一提系统只看做了没有的信号。心跳协议本身的定义在 src/lib/heartbeat.tsHEARTBEAT_TOKEN HEARTBEAT_OKclassifyHeartbeatOutcome()区分静默/发言两种结果。3.4needsHeartbeat单一数据源GET /api/settings/workspace返回needsHeartbeat: !!state.buddy shouldRunHeartbeat(state)前端useAssistantTrigger直接消费该字段不重新实现判定逻辑。这是针对前后端判定逻辑分叉问题的核心决策——早期前端手写版存在两个缺陷缺少 UTC 兼容日期处理、缺少activeHours只在工作时间触发支持跨午夜区间如 22:00-08:00见 src/lib/heartbeat.ts 的isWithinActiveHours。3.5 Buddy-welcome 与 Heartbeat 互斥两者共用autoTriggerflag冲突会导致新用户看到心跳检查而非领养引导或心跳指令注入领养 turn。src/hooks/useAssistantTrigger.ts 中的互斥逻辑const needsBuddyWelcome state.onboardingComplete !state.buddy initialMessages.length 0; const needsHeartbeat !!data.needsHeartbeat !!state.buddy initialMessages.length 0;!state.buddy与!!state.buddy天然互斥逻辑上不可能同时为 true。需要特别指出的是当前源码中前台自动触发的心跳已停用。查看 src/hooks/useAssistantTrigger.ts 的注释可以确认——心跳由后台调度器独占scheduled_tasks表中的sourceassistant_heartbeat任务由executeDueTask触发页面挂载只读取状态前台仅保留一次性 buddy-welcome 领养流程triggerMsg 请做自我介绍并引导用户领养伙伴。。这是为了避免完整 streamClaude turn 携带全部工具导致工具循环失控的历史教训。四、定时任务调度器Durable 与 Session-only 双轨制4.1 核心文件与启动时机核心实现位于 src/lib/task-scheduler.ts。调度器有三种启动时机覆盖冷启动与热路径src/instrumentation.ts— Next.js 服务启动时覆盖冷启动POST /api/chat— 首次聊天时冗余保障POST /api/tasks/schedule— 创建任务时轮询机制为 10 秒setInterval用 globalThis 防止 HMR热更新重复启动另有每小时一次的过期任务检查checkExpiredTasks。4.2 Durable vs Session-only 任务维度DurableSession-only存储SQLitescheduled_tasksglobalThisMapstring, ScheduledTask跨重启✅❌列出/api/tasks/list MCP 合并MCPcodepilot_list_tasks合并显示取消/api/tasks/{id}DELETEMCPcodepilot_cancel_task先查 Map失败退避SQLiteupdateScheduledTaskapplyBackoff内存中累加consecutive_errorsBACKOFF_DELAYS自动禁用10 次连续失败10 次连续失败 →status disabledexecuteDueTask(task, isSessionTask)是双轨制的心脏isSessionTask true跳过所有 SQLite 写入错误时 re-throw 让 poll loop 处理isSessionTask false走完整 SQLite 记录 computeNextRunapplyBackoff退避阶梯定义在源码顶部BACKOFF_DELAYS [30000, 60000, 300000, 900000]30 秒 / 1 分钟 / 5 分钟 / 15 分钟连续失败超过MAX_CONSECUTIVE_ERRORS10 次后自动禁用任务。4.3 Cron 表达式解析1461 天统一扫描 null 语义getNextCronTime(expression): Date | null是任务调度最精密的函数完整实现在 src/lib/task-scheduler.ts统一 4 年1461 天日级扫描每天先做 dom/month/dow 快速预检跳过不匹配的日再扫描该日 1440 分钟支持*、逗号1,15、范围1-5、步进*/5matchField实现4 年内无匹配返回null由调用方暂停任务status: paused或拒绝创建400 错误schedule/route.ts和notification-mcp.ts都 import 此函数不再有重复实现这个设计经历了 4 个版本的演进产品文档 docs/insights/buddy-gamification.md 有完整记录48 小时扫描漏周级 → 35 天漏闰年 2 月 29 日 → 366 天双路径仍漏0 * 29 2 *→ 最终统一 1461 天单路径。核心教训是调度器执行逻辑是next_run now就触发任何 fallback 日期1h / 30 天 / 4 年最终都会到期并错误执行null是唯一不会导致错误执行的返回值——当一个函数不知道答案时应该明确说我不知道返回 null / 抛异常而不是编造一个看似合理的答案。五、通知系统服务端队列 前端轮询 MCP 工具5.1 核心文件文件职责src/lib/notification-manager.ts服务端通知队列 sendNotification()src/app/api/tasks/notify/route.tsPOST 接收通知 / GET 轮询队列src/hooks/useNotificationPoll.ts前端 5 秒轮询 Toast 系统通知src/lib/notification-mcp.tsMCP 工具notify/schedule/list/cancel/hatch5.2 通知流服务端触发task-scheduler / notification-mcp ├─ sendNotification() → enqueueNotification() (globalThis ring buffer, max 50) │ → Telegram (urgent only) └─ 前端 useNotificationPoll (5s interval) └─ GET /api/tasks/notify → drainNotifications() ├─ showToast() (all priorities) └─ 系统通知 (normal/urgent): ├─ Electron: electronAPI.notification.show (IPC, 支持点击回到窗口) └─ Browser: new Notification() (dev fallback)5.3 端口问题修复多端口环境兼容notification-mcp.ts中所有 HTTP 调用已重构通知直接import(/lib/notification-manager).sendNotification()不走 HTTP消除端口依赖任务调度getBaseUrl()读process.env.PORT支持 worktree 和 Electron 非默认端口任务列表/取消同上 合并 session-only 任务task-scheduler.ts的sendTaskNotification同样改为直接 import。六、记忆提取按会话隔离与写入检测6.1 计数器隔离核心实现位于 src/lib/memory-extractor.ts。提取计数器使用MapsessionId, numberglobalThis 存储每个会话独立计数避免会话间互相干扰普通 buddy每 3 轮提取DEFAULT_EXTRACTION_INTERVAL 3Epic/Legendary每 2 轮提取shouldExtractMemory对 epic/legendary 返回 2export function shouldExtractMemory(buddyRarity?: string, sessionId?: string): boolean { const key sessionId || __default__; // 按 sessionId 独立计数epic/legendary 返回 2否则 3 }6.2 写入检测hasMemoryWritesInResponse(fullResponseJson)传入JSON.stringify(contentBlocks)含 tool_use/tool_result 块检查 memory 路径模式memory.md、memory/daily/、soul.md、user.md——如果 AI 本轮已通过工具写入 memory跳过自动提取避免双重写入。6.3 Symlink 安全codepilot_memory_getsrc/lib/memory-search-mcp.ts有两层防护词法检查path.relative()拒绝..前缀Symlink 检查fs.realpathSync()解析真实路径后再验证是否在 workspace 内防止通过符号链接逃逸出工作区读取任意文件。七、Buddy 重置流程PATCH /api/settings/workspacewith{ resetBuddy: true }执行三步state.buddy undefined清除 soul.md 中## Buddy Trait节正则移除到下一个##或文件末尾下次进入空会话 → 触发 buddy-welcome重新领养引导这保证了重新孵化的完整闭环状态、性格提示、领养流程全部归零。八、空状态视觉一致性矩阵Buddy 系统对空状态做了系统化的视觉统一避免各入口各画各的场景组件有 Buddy无 Buddy聊天空助理MessageList.tsx3D 物种图 渐变背景 名字3D 蛋 领养提示聊天空项目MessageList.tsx—CodePilot logo新建聊天页ChatEmptyState.tsx—3D 蛋图替代旧 Brain 图标侧栏推广卡AssistantPromoCard—3D 蛋图 (20px)看板 Buddy 卡DashboardPanel.tsx3D 物种图 渐变背景 状态行3D 蛋 孵化按钮九、关键设计决策记录工程经验沉淀本节整理文档与源码共同印证的设计决策每一条都对应真实踩坑后的结论。9.1 为什么心跳用!-- heartbeat-done --标记而非关键词检测背景最初尝试用关键词heartbeat、记忆、检查等检测 AI 是否做了心跳检查问题关键词太泛讨论 memory leak 也会命中太窄AI 用自然语言不一定提到文件名决策在 soft hint 中指示 AI 完成检查后输出不可见标记!-- heartbeat-done --持久化前从所有 contentBlock text 中清除含 JSON 序列化路径9.2 为什么 cron 无匹配返回 null 而非远期日期背景先后尝试 1h fallback → 30 天 fallback → 4 年 fallback都会导致提前触发问题调度器执行逻辑是next_run now任何 fallback 日期最终都会到期并错误执行决策返回null由调用方暂停任务或拒绝创建。这是唯一不会导致错误执行的方案9.3 为什么 needsHeartbeat 放在服务端而非前端计算背景前端重新实现了shouldRunHeartbeat的日期比较逻辑与服务端分叉问题UTC 兼容日期、activeHours 等条件只在服务端shouldRunHeartbeat()中实现前端手写版缺少这些决策GET /api/settings/workspace返回needsHeartbeat布尔值含 buddy 存在性检查前端直接用。单一数据源比两边各算一次然后祈祷结果一致可靠得多9.4 为什么 buddy-welcome 和 heartbeat 通过!!state.buddy互斥背景两者共用autoTriggerflag同时为 true 时只发一条消息问题heartbeat 优先 → 新用户看到心跳检查而非领养引导buddy-welcome 优先 → 心跳指令被注入到领养 turn 中决策buddy-welcome 要求!state.buddyheartbeat 要求!!state.buddy逻辑上不可能冲突十、文件清单与进一步阅读本轮新增文件用途src/hooks/useNotificationPoll.ts前端通知轮询 Toast 系统通知本轮主要修改核心实现入口文件改动src/lib/context-assembler.tsautoTrigger 参数、isHeartbeatTrigger 判定、buildSoftHeartbeatHint、3D 蛋图src/lib/task-scheduler.tssession 任务推进/退避/禁用、getNextCronTime→null、getSessionTasks 导出、冷启动src/lib/notification-manager.ts服务端通知队列 enqueue/drainsrc/lib/notification-mcp.tsgetBaseUrl() 直接 import session 任务合并src/lib/memory-extractor.ts按 sessionId 隔离计数器src/lib/memory-search-mcp.tssymlink 安全检查src/app/api/chat/route.ts软心跳检测/清除、autoTrigger 传递、记忆写入检测、heartbeat-done 清理src/app/api/tasks/notify/route.tsGET 轮询端点、POST 改用 sendNotificationsrc/app/api/tasks/schedule/route.ts改用 task-scheduler 导出函数、cron null 处理src/app/api/settings/workspace/route.tsneedsHeartbeat 字段、resetBuddy 清理 soul.mdsrc/hooks/useAssistantTrigger.tsheartbeat 触发恢复、server-computed needsHeartbeat、buddy 互斥src/components/layout/AppShell.tsxuseNotificationPoll 集成src/components/chat/MessageList.tsx助理空状态 3D buddy/eggsrc/components/chat/ChatEmptyState.tsx3D 蛋图替代 Brain 图标src/components/layout/panels/DashboardPanel.tsx紧凑状态行、进化反馈、设置按钮位置src/types/electron.d.tsnotification 接口类型src/instrumentation.ts冷启动调度器可复用的三大工程教训涉及多文件状态同步的功能应该在设计阶段画出完整的状态机图——列出所有状态变量、所有转换条件、所有判断点确认每个判断点看到的状态一致。心跳的 4 个文件 6 个判断点useAssistantTrigger、context-assembler、route.ts 触发检测、route.ts 完成检测、route.ts 消息保存、workspace API needsHeartbeat中任何一个与其他不一致都会产生 bug。函数不知道答案时应返回 null / 抛异常而不是编造合理答案——cron 的 fallback 日期会在不可预测的未来时间点制造 bug。引入新存储后端时所有假设只有一个存储后端的代码路径都需要审查——session-only 任务修复前SQLite 写入静默失败、错误计数永远为 0、退避永远不生效修复后executeDueTask(task, isSessionTask)按 flag 走不同路径执行/查询/删除/错误处理每条路径都要适配。Buddy 系统的完整产品叙事、稀有度心理学、与 Claude Code 宠物系统的设计对比以及拟人化界面层的远景思考见 docs/insights/buddy-gamification.md技术细节与更新记录见 docs/handover/buddy-gamification.md。赞分享人工智能AI 应用AI Agent交互助手MCP Clients本地部署【免费下载链接】CodePilotA multi-model AI agent desktop client — connect any AI provider, extend with MCP skills, control from your phone. Built with Electron Next.js.项目地址https://gitcode.com/gh_mirrors/co0dep/CodePilot点击查看免费下载相关推荐yudaocode/ruoyi-vue-pro定时任务调度系统深度解析yudaocode/ruoyi vue pro定时任务调度系统深度解析 概述 在现代企业级应用开发中定时任务调度是不可或缺的核心功能。yudaocode/r后端前端低代码认证鉴权企业应用Mill构建工具入门指南为什么说它是Java/Scala开发的终极选择Mill构建工具入门指南为什么说它是Java/Scala开发的终极选择 Mill是一款针对Java、Scala和Kotlin的现代化构建工具它比MavenKue 任务调度器实现构建定时任务系统Kue 任务调度器实现构建定时任务系统 你是否还在为手动处理定时任务而烦恼是否希望有一个高效可靠的系统来管理任务调度本文将带你深入了解 Kue 任务调度器任务调度后端消息队列上一篇终极指南如何使用stable-diffusion-webui从零开始创作AI艺术下一篇突破256K上下文Qwen3-VL-4B-Thinking长视频理解与处理指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进