ENZH
和 AI 讨论这篇文章
ChatGPTClaude

宠物的身体和大脑拆成了两个仓库

📊 幻灯片

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 连接:壳可以随便换、灵魂能带着记忆搬家身体和灵魂拆成两个仓库、用 HTTP 连接:壳可以随便换、灵魂能带着记忆搬家


为什么非得拆成两个 repo

大概三个原因。

第一,灵魂要能搬家。所有的记忆、性格偏好、信任等级,全存在灵魂那边,导出一个存档文件,换台电脑再导入,宠物还认得你。身体就是个壳,壳可以随便换。

第二,多平台就没障碍了。灵魂引擎就是个标准 HTTP 服务,任何装了 Node.js 的机器都能跑。今天身体是 Electron 桌面端,明天可以是 iOS app、Android widget、网页版,连的都是同一个灵魂。

第三,两边可以各自迭代。身体加新动画、加新主题、改交互,完全不碰 AI 逻辑;灵魂改进记忆、调 prompt、加人格维度,也完全不碰 UI。两边的发布节奏互不干扰。

还有一层考虑是,身体这个东西是会过时的。Electron 哪天可能就淘汰了,桌面端这个形态本身可能都不流行了。把身体和大脑绑死在一个仓库里,等于赌未来只有这一种形态,这个注我不想下。


灵魂引擎只有两个依赖

灵魂引擎的 package.json 里就两行依赖:

依赖用途
better-sqlite3SQLite 绑定
sqlite-vecSQLite 向量扩展

其余全部用 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 才像宠物在说话同一个 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 CodeCommand hook → HTTP POST~0ms
Codex CLIJSONL 日志轮询~1.5s
Copilot CLICommand hook(camelCase)~0ms
Gemini CLISession JSON 轮询~1.5s + 4s 完成窗口
Cursor Agentstdin/stdout JSON hook~0ms
Kiro CLIAgent config 注入~0ms
opencodeIn-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 个以上是建造;1 个子 agent 是抛球,2 个以上子 agent 是指挥。

状态之间有优先级:error(8) > notification(7) > sweeping(6) > attention(5) > carrying/juggling(4) > working(3) > thinking(2) > idle(1) > sleeping(0),高优先级直接抢占低的。

7 个 AI agent 会话汇进一只宠物,按优先级排队,error 直接抢占低优先级,只输出一个动画状态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 篇干脆花了一整篇讲怎么让它学会不帮忙,这事比教它有用难十倍。这篇讲的架构,就是把上面这些全撑起来的部分。

全部代码开源:

和 AI 讨论这篇文章
ChatGPTClaude

订阅更新

新文章发布时发到你的邮箱,不发别的。


© Xingfan Xia 2024 - 2026 · CC BY-NC 4.0