宠物的身体和大脑拆成了两个仓库
Clawd Soul · 第 4 篇 / 共 5 篇
上一篇讲了让 AI 学会「没用」有多难。这篇讲底下撑着这一切的架构。东西其实不多,核心就是把身体和大脑拆成两个仓库,灵魂那边的代码加起来也就 2000 行左右。今天就讲讲这套东西是怎么拆的。
身体里一行 AI 代码都没有
整个系统我拆成了两个仓库,一个管身体,一个管大脑。clawd-on-desk(Electron)是身体,你看得见、点得到的部分全归它管:
- 像素动画,12 个动画状态:发呆、思考、打字、建造、抛球、指挥、报错、开心、通知、扫地、搬运、睡觉
- 点击、拖拽、语音气泡、聊天窗口
- 权限弹窗,在应用里直接点 Allow/Deny,不用切回终端
- 迷你模式,藏到屏幕边上,鼠标一悬停它就探头出来
- 眼球追踪,20fps 跟着光标走,最多偏 3px
- 睡眠序列,60 秒没操作就开始,打哈欠 → 打瞌睡 → 倒下 → 睡着
- 主题系统,有像素螃蟹 Clawd 和三花猫 Calico 两套
这个仓库里一行 AI 逻辑都没有,真的一行都没有。AI 的东西全在另一边,也就是 clawd-soul(HTTP :23456),它是大脑:
- 屏幕阅读,走 vision API,截图是 1920×1080 的 JPEG,质量 q85
- 性格系统,用散文写角色档案,有 5 种人格原型
- 记忆系统,SQLite 加向量检索,分三层
- 对话系统,用 JSONL 存,会自动压缩
- 情绪和信任引擎
- 日记,每天 23:00 自动写一篇
- 记忆整理,23:30 开始「做梦」
- 一共 11 个源文件,大概 2000 行代码
这么拆也不是我有代码洁癖,产品上就得这么拆,原因下面说。
身体和灵魂拆成两个仓库、用 HTTP 连接:壳可以随便换、灵魂能带着记忆搬家
为什么非得拆成两个 repo
原因大概有三个。第一,灵魂要能搬家。所有的记忆、性格偏好、信任等级,全存在灵魂那边。你导出一个存档文件,换台电脑再导进去,宠物还认得你。身体就是个壳,壳可以随便换。
第二,上别的平台很容易。灵魂引擎就是个普通的 HTTP 服务,装了 Node.js 的机器都能跑。今天身体是 Electron 桌面端,明天可以换成 iOS app、Android widget 或者网页版,连的都是同一个灵魂。
第三,两边可以各改各的。身体这边加动画、加主题、改交互,完全碰不到 AI 逻辑;灵魂那边改记忆、调 prompt、加人格维度,也碰不到 UI。两边什么时候发版,互相不影响。
我还有个考虑,身体是会过时的。Electron 说不定哪天就被淘汰了,桌面端这种形态可能都不流行了。要是把身体和大脑绑死在一个仓库里,就等于赌以后只有这一种形态,这个注我不想下。
灵魂引擎只有两个依赖
灵魂引擎的 package.json 里就两行依赖:
| 依赖 | 用途 |
|---|---|
better-sqlite3 | SQLite 绑定 |
sqlite-vec | SQLite 向量扩展 |
剩下的全用 Node.js 自带的模块:
| 功能 | 模块 |
|---|---|
| HTTP 服务器 | node:http |
| AI 服务商调用 | node:https(原始 HTTPS,不用 SDK) |
| 文件读写 | node:fs |
| 路径处理 | node:path |
| 加密 | node:crypto |
接的 4 家 AI 服务商(azure openai、openai、gemini、claude)全是直接发 HTTPS 请求。openai 包没装,@anthropic-ai/sdk 也没装,就是裸的 HTTP 请求。
不用 SDK,是因为 SDK 自带重试逻辑、错误处理那一套、响应格式化,这些东西一层层叠上去,对话读起来就像「一个 AI 系统在说话」,不像「宠物在说话」。所以栈越薄越好,中间能让角色变味的环节,少一个是一个。
同一个 AI 模型,穿过厚 SDK 抽象层会像系统在说话,穿过一条原始 HTTP 才像宠物在说话
顺带还有个很实在的好处,只有 2 个依赖,能出问题的也就这 2 个。整个灵魂引擎的代码,一个下午就能从头读到尾。
双窗口是 Windows 逼出来的
桌面宠物用了两个独立的顶层窗口:
- 渲染窗口:一个大的透明窗口,点击永远穿透过去(
setIgnoreMouseEvents(true)),只管显示 SVG 动画和眼球追踪。 - 输入窗口:一个小矩形,刚好盖住宠物能点的那块区域,可以获得焦点,所有指针事件都由它来接。
正常肯定是一个窗口搞定。但是 Windows 上有个拖拽 bug,WS_EX_NOACTIVATE、分层窗口、Chromium 子 HWND 这三样凑在一起,z-order 一切换,就会出现一条死掉的激活路径,点击全打到幽灵区域上,一点反应都没有。这个问题我查了好几个小时,最后只好把输入单独放进一个能获得焦点的窗口,渲染放在穿透窗口里,两个窗口各管各的。
代价是输入窗口会抢焦点,这个我知道。但不这么做,Windows 上拖拽就彻底坏了,那比抢焦点糟多了。
Windows 上这种事还不止一件。前台窗口会被锁住,我是靠 ALT 键的 trick,加上用 koffi FFI 去调 AllowSetForegroundWindow,再交给一个 PowerShell 辅助进程,才把它解决的。还有个语言子菜单被截断的 bug,我花了三个小时才定位到,原因是 Electron 和 Windows DWM 不兼容,修不了,最后只能在文档里标上「DO NOT TOUCH」。只能说做 Windows 兼容,大部分时间都在考古,查到最后往往也只是绕过去。
同时追踪 7 个 agent
桌面宠物能同时追踪 7 个 AI coding agent 的会话:
| Agent | 集成方式 | 延迟 |
|---|---|---|
| Claude Code | Command hook → HTTP POST | ~0ms |
| Codex CLI | JSONL 日志轮询 | ~1.5s |
| Copilot CLI | Command hook(camelCase) | ~0ms |
| Gemini CLI | Session JSON 轮询 | ~1.5s + 4s 完成窗口 |
| Cursor Agent | stdin/stdout JSON hook | ~0ms |
| Kiro CLI | Agent config 注入 | ~0ms |
| opencode | In-process Bun plugin | ~0ms |
每个 agent 的接法都不一样。claude code 用 command hook,调一个零依赖的 Node 脚本。codex 是增量轮询 JSONL 日志,还要给事件去重。opencode 最麻烦,因为它的 TUI 不对外开 HTTP,只能写一个 in-process 的 Bun plugin,在随机端口上起一个 HTTP 桥,认证用 randomBytes(32) 加 timingSafeEqual。
7 个 agent 可以同时跑,宠物会分开追踪每个会话。有 1 个会话在跑,它就打字;2 个就抛球;3 个以上就开始建造。子 agent 也一样,1 个是抛球,2 个以上是指挥。
这些状态有优先级,高的直接顶掉低的:error(8) > notification(7) > sweeping(6) > attention(5) > carrying/juggling(4) > working(3) > thinking(2) > idle(1) > sleeping(0)。
7 个 AI agent 会话汇进一只宠物,按优先级排队,error 直接抢占低优先级,只输出一个动画状态
我的想法大概是,现在程序员桌面上同时跑三四个 AI agent 已经很常见了,宠物不能只认一个工具,得全都认识,再用动画告诉你现在都在发生什么。
MIT,全本地
协议是 MIT。数据 100% 留在本地,截屏在内存里分析完就扔,不存下来;存档文件是你自己的数据,随时能导出。没有云端,没有账号,也没有遥测。
想自己 hack 也不难。写一套新主题,最少只要 1 个 SVG 加 7 个动画文件;加一个新人格原型,写一份散文档案就行;接一个新的 AI agent,做一个 HTTP 集成就够了。
到现在一共有 20 个贡献者。像素画用的是 @marciogranzotto 的 clawd-tank,三花猫主题是鹿鹿画的。
回头看这五篇
这个系列写到这里就完了。第 0 篇问了一个问题,AI 都在抢着帮你干活,有没有一个只是陪着你的?后面几篇其实都在回答它。第 1 篇讲性格用散文写,不调参数;第 2 篇讲记忆,三层存储,每晚还会做梦;到了第 3 篇,干脆花一整篇讲怎么让它学会不帮忙,这比教它有用难十倍。这篇讲的架构,就是底下把这些全撑起来的那部分。
全部代码开源:
- 灵魂引擎:clawd-soul
- 桌面端:clawd-on-desk