1. 从一次串口报错说起:MiniClaw 文件读写到底难在哪
如果你正在 ESP32-S3 上折腾 OpenClaw 具身硬件,大概率会遇到这个场景:Telegram 消息进来了,LLM 决定调用read_file,结果串口打印E (12345) SPIFFS: mount failed, -10025,或者tool_result: file not found。这不是模型的问题,而是 SPIFFS 挂载点和路径映射没对齐。
MiniClaw 在 ESP32-S3 上的文件读写链路,核心就三件事:把 SPIFFS 分区挂到 VFS 挂载点、把逻辑路径映射到物理路径、用标准 POSIX API 做读写。听起来简单,但 ESP-IDF 的 SPIFFS 组件有几个坑:分区表里spiffs子类型必须写对、base_path和partition_label要匹配、挂载失败时esp_spiffs_format的调用时机不对会丢数据。
我实测下来,MiniClaw 的tool_read_file_execute最终落到fopen("/spiffs/skills/weather.md", "r"),中间经过 VFS 层把/spiffs前缀剥掉,再交给 SPIFFS 驱动去查分区表。所以只要挂载点、分区标签、路径前缀三者一致,读写就能通。这篇笔记就按这个顺序,把可复制的分区表、挂载代码、读写验证动作全部拆开。
适合谁看:已经在跑 ESP-IDF、手里有 ESP32-S3 开发板、想复现 MiniClaw 文件系统行为的嵌入式开发者。不需要你懂向量数据库,但需要你会用idf.py menuconfig和看串口日志。
2. TaoToken 前置:为什么文件读写链路要先接上模型服务
MiniClaw 的read_file不是孤立工具,它被 LLM 的tool_use触发。也就是说,你得先有一个能返回tool_use的模型服务,才能完整验证“LLM 决定读文件 → agent_loop 执行 → tool_result 回填”这条链路。TaoToken 在这里的角色是提供兼容 Anthropic 和 OpenAI 两种协议的 API 入口,MiniClaw 的llm_client.c里通过MIMI_LLM_PROVIDER宏切换。
你需要准备三样东西:Base URL、API Key、Model ID。Base URL 用https://taotoken.net/api,不要加 UTM 参数,这是给代码里http_client用的。API Key 在控制台生成,Model ID 根据你选的提供商填,比如claude-sonnet-4-20250514或gpt-4o。
这里有个容易踩的坑:MiniClaw 的context_builder.c会把工具列表塞进 system prompt,如果模型服务返回的tool_use格式和解析代码不匹配,你会看到tool_use字段为空,然后 agent_loop 直接跳过文件读取。所以接上模型服务后,先用curl验证一次tool_use返回结构,再烧录固件。
具体操作:打开 TaoToken 控制台,生成一个 API Key,复制下来。然后本地用 curl 测一下:
curl -X POST https://taotoken.net/api/v1/messages \ -H "Content-Type: application/json" \ -H "x-api-key: sk-你的Key" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-sonnet-4-20250514", "max_tokens": 1024, "tools": [{ "name": "read_file", "description": "Read a file from SPIFFS storage.", "input_schema": { "type": "object", "properties": { "path": {"type": "string"} }, "required": ["path"] } }], "messages": [{"role": "user", "content": "读取 /spiffs/skills/weather.md"}] }'如果返回 JSON 里有"type": "tool_use"和"name": "read_file",说明模型服务侧通了。这一步不做,后面 SPIFFS 挂载再成功,你也看不到完整的文件读写链路。
3. 可复制配置:SPIFFS 分区表与挂载代码逐行拆解
3.1 分区表 partitions.csv
MiniClaw 默认用 4MB Flash,分区表里 SPIFFS 分区通常放在factory之后。关键字段是Type和SubType,SPIFFS 必须写data和spiffs:
# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 0x300000, spiffs, data, spiffs, 0x310000,0xF0000,0x310000是 SPIFFS 起始地址,0xF0000是 960KB 空间。如果你板子是 8MB Flash,可以把factory和spiffs都放大,但注意Offset要连续,不能重叠。
3.2 menuconfig 里的关键项
运行idf.py menuconfig,进Component config → SPIFFS Configuration:
SPIFFS_MAX_PARTITIONS设为 3(够用)SPIFFS_USE_MAGIC打开,SPIFFS_USE_MAGIC_LENGTH也打开SPIFFS_OBJ_NAME_LEN设为 64,MiniClaw 的路径/spiffs/skills/weather.md有 28 字符,64 够SPIFFS_USE_MMAP关掉,ESP32-S3 上 mmap 对 SPIFFS 支持不完整
3.3 挂载代码 spiffs_init.c
这是 MiniClaw 里storage_init()的简化版,你可以直接复制:
#include "esp_spiffs.h" #include "esp_log.h" static const char *TAG = "SPIFFS_INIT"; esp_err_t spiffs_mount(void) { esp_vfs_spiffs_conf_t conf = { .base_path = "/spiffs", .partition_label = "spiffs", .max_files = 8, .format_if_mount_failed = true }; esp_err_t ret = esp_vfs_spiffs_register(&conf); if (ret != ESP_OK) { if (ret == ESP_FAIL) { ESP_LOGE(TAG, "Mount or format failed"); } else if (ret == ESP_ERR_NOT_FOUND) { ESP_LOGE(TAG, "Partition 'spiffs' not found"); } else { ESP_LOGE(TAG, "SPIFFS init failed: %s", esp_err_to_name(ret)); } return ret; } size_t total = 0, used = 0; ret = esp_spiffs_info("spiffs", &total, &used); if (ret == ESP_OK) { ESP_LOGI(TAG, "Partition size: total=%d, used=%d", total, used); } return ESP_OK; }注意base_path是/spiffs,partition_label是spiffs,这两个必须和分区表里的Name一致。format_if_mount_failed = true在开发阶段方便,但量产固件建议改成false,避免意外格式化丢数据。
3.4 路径映射规则
MiniClaw 的MIMI_SPIFFS_BASE宏定义为"/spiffs"。当 LLM 传path="/spiffs/skills/weather.md"时,tool_read_file_execute直接把这个字符串传给fopen。VFS 层看到/spiffs前缀,剥掉后交给 SPIFFS 驱动,驱动在分区里找skills/weather.md。所以你在 SPIFFS 里创建文件时,路径是/spiffs/skills/weather.md,但实际存储的 key 是skills/weather.md。
4. 验证请求:一次写入、读取、校验的完整动作
4.1 写入文件
在app_main里挂载成功后,先写一个测试文件:
#include "stdio.h" #include "string.h" void test_spiffs_write_read(void) { const char *path = "/spiffs/skills/weather.md"; const char *content = "# Weather Skill\n\n当用户问天气时,调用 get_weather 工具。\n"; FILE *f = fopen(path, "w"); if (f == NULL) { ESP_LOGE(TAG, "Failed to open file for writing"); return; } fwrite(content, 1, strlen(content), f); fclose(f); ESP_LOGI(TAG, "File written: %s", path); // 读取校验 f = fopen(path, "r"); if (f == NULL) { ESP_LOGE(TAG, "Failed to open file for reading"); return; } char buf[256] = {0}; size_t read_bytes = fread(buf, 1, sizeof(buf) - 1, f); fclose(f); ESP_LOGI(TAG, "Read %d bytes: %s", read_bytes, buf); if (strcmp(buf, content) == 0) { ESP_LOGI(TAG, "Verify OK"); } else { ESP_LOGE(TAG, "Verify FAILED"); } }烧录后串口应该打印:
I (1234) SPIFFS_INIT: Partition size: total=983040, used=0 I (1235) SPIFFS_INIT: File written: /spiffs/skills/weather.md I (1236) SPIFFS_INIT: Read 52 bytes: # Weather Skill... I (1237) SPIFFS_INIT: Verify OK4.2 通过 LLM 触发 read_file
文件写好后,把read_file工具注册进 agent_loop,然后发一条 Telegram 消息或串口模拟消息:“读一下 weather.md”。串口日志应该出现:
[agent] tool_use: read_file(path="/spiffs/skills/weather.md") [agent] tool_result: 52 bytes [agent] final answer: 文件内容是...如果tool_result是 0 bytes,检查fopen返回值;如果是file not found,检查路径前缀和分区挂载点。
4.3 校验动作
除了strcmp,还可以用esp_spiffs_info看used字节数变化。写入前used=0,写入后used=52(实际会按块对齐,可能显示 4096)。如果used没变,说明写入没落盘,检查fclose是否调用。
5. 本篇常见错排查:401、mount failed、reading choices、OAuth
5.1 401 Unauthorized
串口打印HTTP 401,通常是 API Key 没填对或 header 名字写错。MiniClaw 的llm_client.c里 Anthropic 用x-api-key,OpenAI 用Authorization: Bearer。检查MIMI_LLM_API_KEY宏是否被正确赋值,以及 TaoToken 控制台里 Key 是否被禁用。
5.2 mount failed, -10025
-10025是ESP_ERR_NOT_FOUND,意思是分区表里没找到spiffs分区。检查partitions.csv是否被idf.py menuconfig → Partition Table → Custom partition table CSV正确引用,以及SubType是否写成spiffs而不是fat。
5.3 reading choices 报错
如果串口打印failed to parse choices或reading choices,说明模型返回的 JSON 里tool_use字段结构和你代码里的cJSON_GetObjectItem路径不匹配。Anthropic 的tool_use在content数组里,OpenAI 的在choices[0].message.tool_calls。MiniClaw 用MIMI_LLM_PROVIDER区分,检查宏是否和实际 API 一致。
5.4 OAuth 相关错误
如果你看到OAuth token expired或invalid_grant,说明用了 OAuth 流程而不是 API Key。MiniClaw 的llm_client.c只支持 API Key 模式,OAuth 需要额外实现 token 刷新。建议直接用 TaoToken 控制台生成的 API Key,不要走 OAuth。
5.5 CC Switch / Cline MCP / Codex auth.json 三件套
如果你在 PC 侧用 CC Switch 或 Cline MCP 调试 MiniClaw 的 API 配置,需要写全三件套:
{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的Key", "model_id": "claude-sonnet-4-20250514" }Codex 的auth.json里对应字段是api_base、api_key、model。少任何一个,工具调用都会失败。
6. 语义一致 CTA:把文件读写链路接进你的 MiniClaw
文件读写链路跑通后,下一步是把read_file、write_file、list_dir三个工具都注册进 agent_loop,然后让 LLM 自己决定什么时候读、什么时候写。MiniClaw 的context_builder.c会把MEMORY.md和Skills目录页塞进 system prompt,LLM 看到目录后发起read_file,tool_result回填后再生成回答。
如果你还没接上模型服务,先去 TaoToken 控制台生成 API Key,然后参考接入文档把llm_client.c里的 Base URL 和 Key 填好。验证模型是否返回tool_use,可以用模型对话页面直接发一条带工具定义的请求,看返回 JSON 结构。长期跑编码或 Agent 场景,Coding Plan 的额度更划算,适合把 MiniClaw 当常驻设备用。
我踩过的坑是:SPIFFS 挂载成功后忘了调esp_vfs_spiffs_register的返回值检查,结果fopen一直返回 NULL,查了半天才发现是max_files设成了 0。把max_files改成 8 之后,读写一次通过。