拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

【OpenClaw具身硬件】MiniClaw 阅读笔记—(4) 文件读写:把 SPIFFS 挂载到 TaoToken 的配置记录

【OpenClaw具身硬件】MiniClaw 阅读笔记—(4) 文件读写:把 SPIFFS 挂载到 TaoToken 的配置记录

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 OK

4.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 之后,读写一次通过。

返回列表