给 Claude Code CLI 做个 ESP32 电子宠物:TaoToken 统一 Key 接入与 BLE 监工搭子
1. 凌晨两点我盯着滚动的终端决定给 Claude Code 配个实体监工Claude Code CLI 是个好东西工具自动执行、Bash 命令一条接一条跑效率确实高。但问题也在这儿它太能跑了。你敲完claude回车终端就开始刷日志Bash、Write、Edit 轮番上阵你根本不知道它现在是在干活、卡住了还是正准备执行一条rm -rf级别的危险命令。审批提示一闪而过被日志刷过去是常事。我试过把终端窗口缩小放一边也试过开第二个 pane 专门盯日志但人眼盯滚动文本这件事本身就反人性。于是我想能不能把 Claude Code 的状态投射到一个物理设备上——一个放在桌角的 ESP32 小屏幕用 BLE 跟 PC 通信Claude 忙的时候它皱眉等审批的时候它瞪眼任务完成它跳个爱心。这就是「电子宠物监工搭子」的由来。这篇不是概念文是能跟做的搭建记录。核心要解决两件事一是多工具 Key 分散的问题用 TaoToken 统一 Key 收口二是状态不可见的问题用 ESP32 BLE asyncio 把 Claude Code CLI 的状态实时投射到实体屏幕。适合已经在用 Claude Code、手头有块 ESP32、想折腾点实体交互的开发者。下面从 Key 接入开始一步步到 BLE 状态回传验证。2. 前置TaoToken 统一 Key 与 Claude Code 的接入位置在动手写 ESP32 代码之前先把 Key 这件事理清楚。Claude Code CLI 本身支持通过环境变量或配置文件指定 API 端点但如果你同时用多个工具Claude Code、Cursor、各种 CLI Agent每个都配一遍 Key管理起来很乱。TaoToken 的作用就是提供一个统一的 Key 和端点让这些工具都指向同一个入口。TaoToken 的官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 这个不加 UTM。你需要先去控制台创建一个 API Key地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite Key 列表在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。拿到 Key 之后Claude Code 的接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各工具的配置示例。这里要强调一点TaoToken 是合规的 API 聚合入口不是那种灰色中转你配置的时候走的是标准 HTTP 端点Claude Code 的请求格式不变只是 base_url 和 key 换一下。对于 Claude Code 这种 CLI 工具最稳的方式是通过settings.json或环境变量注入而不是改源码。3. 可复制配置config.toml、settings.json 与 CC Switch 片段Claude Code CLI 的配置分两层一层是全局的~/.claude/settings.json一层是项目级的.claude/settings.json。我建议把 TaoToken 的 Key 放在全局配置里项目级只覆盖模型名之类的参数。下面是一个可复制的settings.json骨架{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的TaoTokenKey, ANTHROPIC_MODEL: claude-sonnet-4-20250514 }, permissions: { allow: [ Bash(git status), Bash(ls), Read ], ask: [ Bash(rm *), Write, Edit ] } }注意permissions.ask这一块它决定了哪些操作会触发审批。我们的电子宠物监工就是监听这些审批事件。如果你用的是 CC Switch 来管理多套配置可以在它的配置片段里这样写# ~/.cc-switch/config.toml [profiles.taotoken] name TaoToken 统一入口 base_url https://taotoken.net/api api_key_env TAOTOKEN_API_KEY default_model claude-sonnet-4-20250514 [profiles.taotoken.env] ANTHROPIC_BASE_URL https://taotoken.net/api ANTHROPIC_API_KEY ${TAOTOKEN_API_KEY}这样你在 CC Switch 里切换 profile 的时候Claude Code 会自动读取对应的端点。如果你不用 CC Switch直接在 shell 里 export 也行export ANTHROPIC_BASE_URLhttps://taotoken.net/api export ANTHROPIC_API_KEYsk-你的TaoTokenKey配置完之后先别急着接 ESP32用一条最简单的请求验证 Key 是否生效。Claude Code 的模型对话入口在 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 你可以先在网页端确认 Key 能正常调用模型再去配 CLI。4. 验证请求确认 Claude Code 走的是 TaoToken 端点配置写完之后怎么确认 Claude Code 真的走了 TaoToken 而不是默认端点最直接的办法是看请求日志。Claude Code 在 debug 模式下会打印实际请求的 URL。你可以这样启动claude --debug然后在另一个终端里触发一个简单操作比如让它读一个文件。如果 debug 输出里出现https://taotoken.net/api/v1/messages这样的 URL说明配置生效了。如果还是api.anthropic.com那说明环境变量没被读取检查一下settings.json的路径对不对或者 shell 里有没有覆盖。另一个验证方式是看 TaoToken 控制台的用量统计。你调用一次之后控制台的请求计数应该会增加。如果计数不动说明请求没打到 TaoToken。这一步很重要因为后面 ESP32 监工依赖的是 Claude Code 的 Hook 事件如果 Key 配置错了Hook 根本不会触发。验证通过之后你可以顺手测一下模型对话确认返回内容正常。TaoToken 的模型对话页面在 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 选 Claude 系列模型发一条消息看看响应速度和内容质量。这一步没问题就可以进入 ESP32 部分了。5. ESP32 BLE asyncio状态回传与审批拦截的完整链路现在进入硬件部分。整体架构是这样的Claude Code CLI 通过 Hook 把状态事件发给一个 PC 端的守护进程Python 写的守护进程通过 BLE 把状态推给 ESP32ESP32 收到后更新屏幕动画。反过来ESP32 上的触控按钮被按下时通过 BLE 把审批结果回传给守护进程守护进程再通知 Claude Code 放行或拒绝。先看 PC 端守护进程的核心逻辑。用 Python 的bleak库做 BLE 通信用asyncio做异步调度import asyncio import json from bleak import BleakServer, BleakCharacteristic NUS_SERVICE_UUID 6e400001-b5a3-f393-e0a9-e50e24dcca9e NUS_RX_UUID 6e400002-b5a3-f393-e0a9-e50e24dcca9e NUS_TX_UUID 6e400003-b5a3-f393-e0a9-e50e24dcca9e class BuddyServer: def __init__(self): self.clients set() self.pending_approval None async def send_state(self, state: str, detail: str ): payload json.dumps({state: state, detail: detail}) \n data payload.encode(utf-8) # BLE NUS 单包 20 字节分片发送 for i in range(0, len(data), 20): chunk data[i:i20] for client in self.clients: await client.write_gatt_char(NUS_TX_UUID, chunk) async def on_approval(self, approved: bool): if self.pending_approval: self.pending_approval.set_result(approved) self.pending_approval NoneESP32 端用 MicroPython 的aioble库开三个 asyncio 任务一个处理 BLE 连接和消息收发一个监听触控一个以 20FPS 渲染动画。关键点是 BLE 分片的透明拼接——ESP32 收到的数据可能是半截 JSON需要按\n做帧边界import asyncio import aioble import json buffer async def ble_task(): global buffer while True: connection await aioble.advertise(...) async for data in connection.characteristic(NUS_RX_UUID).subscribed(): buffer data.decode(utf-8) while \n in buffer: line, buffer buffer.split(\n, 1) msg json.loads(line) handle_message(msg)handle_message里根据state字段切换动画idle闭眼打哈欠busy皱眉工作approval瞪眼等确认。审批界面带 30 秒倒计时超时自动放行避免卡住 Claude Code 的任务。触控任务单独跑检测到按钮按下就通过 BLE 回传{approval: true}或{approval: false}。渲染任务固定 20FPS用asyncio.sleep(0.05)控制节奏保证动画不掉帧。6. 本篇常见错排查BLE 连不上、JSON 截断、审批超时第一个坑是 BLE 连不上。ESP32 的 NUS 服务 UUID 必须和 PC 端一致很多人抄代码的时候只改了服务 UUID 没改特征 UUID导致订阅失败。检查NUS_RX_UUID和NUS_TX_UUID是否和 ESP32 端定义的一致。另外Windows 上 BLE 需要先配对一次Linux 上可能需要bluetoothctl手动 trust。第二个坑是 JSON 截断。BLE NUS 单包 20 字节是硬限制如果你的 JSON 超过 20 字节比如带 detail 字段必须分片。分片的时候注意帧头标记我用的方案是每条消息以\n结尾接收端按\n切分。如果你发现收到的 JSON 解析报错先打印原始 buffer 看看是不是半截。第三个坑是审批超时。ESP32 上的倒计时如果没做Claude Code 会一直等任务卡死。我的做法是 30 秒无操作自动放行同时在 PC 端守护进程里也设一个超时双保险。如果你发现 Claude Code 卡在审批界面不动检查 ESP32 是否还在线BLE 断连的时候守护进程应该自动放行。第四个坑是 Key 配置没生效。前面验证请求那一步如果跳过了后面 Hook 事件可能根本不触发。Claude Code 的 Hook 需要在settings.json里显式开启确认hooks字段配置正确。如果 Hook 不触发ESP32 永远停在 idle 状态。7. 长期编码与 Agent 场景Coding Plan 与后续扩展如果你打算长期用 Claude Code 跑编码任务或者把电子宠物监工扩展到多设备、多 Agent 场景建议关注一下 TaoToken 的 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。它适合需要稳定调用、长期跑 Agent 的开发者Key 管理和用量统计比单次调用更清晰。后续扩展方向有几个一是把审批结果同步到手机用 BLE 中继或者 MQTT二是加个蜂鸣器审批请求来的时候响一声避免你戴着耳机错过三是把状态数据存到本地 SQLite跑一周之后看看 Claude Code 到底在哪些操作上花时间最多。这些都不难核心链路通了之后剩下的就是加外设和写逻辑。回到最初的问题为什么要给 Claude Code 配个实体监工因为终端里的日志是流动的而物理屏幕上的状态是凝固的。你瞟一眼就知道它在干嘛这种确定感是滚动日志给不了的。ESP32 不贵BLE 也不复杂花一个周末把这条链路搭通之后每次敲claude的时候桌角那只小猫都会替你盯着。