Compare commits

..

3 Commits

Author SHA1 Message Date
7dc4122778 feat: 添加时区设置功能,默认时区改为 CST-8
Some checks failed
Build / idf-build (push) Has been cancelled
Build & Release / build (push) Has been cancelled
- 新增 set_timezone LLM 工具,支持通过对话设置时区
- 新增 set_timezone / timezone_show CLI 命令
- 默认时区从 PST 改为 CST-8(中国标准时间 UTC+8)
- 支持 POSIX 格式和 18 个城市名映射(Asia/Shanghai 等)
- 时区通过 NVS 持久化存储(system_config namespace)
- config_show 中显示当前时区配置
- 更新 changelog.md 和 taolun.md 文档
2026-04-01 00:50:41 +08:00
eedc6757d8 使用中文提交内容
Some checks failed
Build / idf-build (push) Has been cancelled
适配ESP-IDF v6.0编译 补充项目相关文档

* 修复16处头文件缺失、flash配置错误、WiFi断开原因码兼容问题
* 新增ESP-IDF v6.0迁移适配文档
* 更新变更日志,补充v1.0.0功能清单及v1.1.0版本规划
* 整理讨论记录,新增v6.0适配及国内大模型接入内容
2026-03-31 21:34:59 +08:00
49d3a131b7 feat: 增加国内大模型厂商支持(硅基流动、火山方舟)
Some checks failed
Build / idf-build (push) Has been cancelled
- 创建通用提供商架构(llm_provider.h/c)
- 支持四个提供商:Anthropic、OpenAI、SiliconFlow、Volcengine
- 添加提供商特定的API密钥和Base URL配置
- 扩展CLI命令:set_siliconflow_key/url、set_volcengine_key/url
- 更新mimi_secrets.h.example配置模板
- 更新README.md文档说明
- 每个提供商支持独立的NVS存储配置
2026-03-31 19:37:15 +08:00
32 changed files with 1476 additions and 409 deletions

View File

@@ -31,7 +31,7 @@ MimiClaw turns a tiny ESP32-S3 board into a personal AI assistant. Plug it into
![](assets/mimiclaw.png)
You send a message on Telegram. The ESP32-S3 picks it up over WiFi, feeds it into an agent loop — the LLM thinks, calls tools, reads memory — and sends the reply back. Supports both **Anthropic (Claude)** and **OpenAI (GPT)** as providers, switchable at runtime. Everything runs on a single $5 chip with all your data stored locally on flash.
You send a message on Telegram. The ESP32-S3 picks it up over WiFi, feeds it into an agent loop — the LLM thinks, calls tools, reads memory — and sends the reply back. Supports **Anthropic (Claude)**, **OpenAI (GPT)**, **SiliconFlow** (Chinese LLM providers), and **Volcengine** (ByteDance Doubao models) as providers, switchable at runtime. Everything runs on a single $5 chip with all your data stored locally on flash.
## Quick Start
@@ -40,7 +40,7 @@ You send a message on Telegram. The ESP32-S3 picks it up over WiFi, feeds it int
- An **ESP32-S3 dev board** with 16 MB flash and 8 MB PSRAM (e.g. Xiaozhi AI board, ~$10)
- A **USB Type-C cable**
- A **Telegram bot token** — talk to [@BotFather](https://t.me/BotFather) on Telegram to create one
- An **Anthropic API key** — from [console.anthropic.com](https://console.anthropic.com), or an **OpenAI API key** — from [platform.openai.com](https://platform.openai.com)
- An **Anthropic API key** — from [console.anthropic.com](https://console.anthropic.com), or an **OpenAI API key** — from [platform.openai.com](https://platform.openai.com), or a **SiliconFlow API key** — from [siliconflow.cn](https://siliconflow.cn), or a **Volcengine API key** — from [volcengine.com](https://volcengine.com)
### Install
@@ -128,11 +128,19 @@ Edit `main/mimi_secrets.h`:
#define MIMI_SECRET_WIFI_PASS "YourWiFiPassword"
#define MIMI_SECRET_TG_TOKEN "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
#define MIMI_SECRET_API_KEY "sk-ant-api03-xxxxx"
#define MIMI_SECRET_MODEL_PROVIDER "anthropic" // "anthropic" or "openai"
#define MIMI_SECRET_MODEL_PROVIDER "anthropic" // "anthropic", "openai", "siliconflow", or "volcengine"
#define MIMI_SECRET_SEARCH_KEY "" // optional: Brave Search API key
#define MIMI_SECRET_TAVILY_KEY "" // optional: Tavily API key (preferred)
#define MIMI_SECRET_PROXY_HOST "" // optional: e.g. "10.0.0.1"
#define MIMI_SECRET_PROXY_PORT "" // optional: e.g. "7897"
/* Optional: SiliconFlow API (OpenAI-compatible) */
#define MIMI_SECRET_SILICONFLOW_API_KEY ""
#define MIMI_SECRET_SILICONFLOW_BASE_URL "https://api.siliconflow.cn/v1"
/* Optional: Volcengine API (OpenAI-compatible, ByteDance Doubao models) */
#define MIMI_SECRET_VOLCENGINE_API_KEY ""
#define MIMI_SECRET_VOLCENGINE_BASE_URL "https://ark.cn-beijing.volces.com/api/v3"
```
Then build and flash:
@@ -169,12 +177,16 @@ Connect via serial to configure or debug. **Config commands** let you change set
mimi> wifi_set MySSID MyPassword # change WiFi network
mimi> set_tg_token 123456:ABC... # change Telegram bot token
mimi> set_api_key sk-ant-api03-... # change API key (Anthropic or OpenAI)
mimi> set_model_provider openai # switch provider (anthropic|openai)
mimi> set_model_provider openai # switch provider (anthropic|openai|siliconflow|volcengine)
mimi> set_model gpt-4o # change LLM model
mimi> set_proxy 127.0.0.1 7897 # set HTTP proxy
mimi> clear_proxy # remove proxy
mimi> set_search_key BSA... # set Brave Search API key
mimi> set_tavily_key tvly-... # set Tavily API key (preferred)
mimi> set_siliconflow_key sk-... # set SiliconFlow API key
mimi> set_siliconflow_url https://api.siliconflow.cn/v1 # set SiliconFlow Base URL
mimi> set_volcengine_key sk-... # set Volcengine API key
mimi> set_volcengine_url https://ark.cn-beijing.volces.com/api/v3 # set Volcengine Base URL
mimi> config_show # show all config (masked)
mimi> config_reset # clear NVS, revert to build-time defaults
```
@@ -250,7 +262,7 @@ MimiClaw stores everything as plain text files you can read and edit:
## Tools
MimiClaw supports tool calling for both Anthropic and OpenAI — the LLM can call tools during a conversation and loop until the task is done (ReAct pattern).
MimiClaw supports tool calling for all LLM providers — the LLM can call tools during a conversation and loop until the task is done (ReAct pattern). Supported providers include Anthropic (Claude), OpenAI (GPT), SiliconFlow, and Volcengine.
| Tool | Description |
|------|-------------|
@@ -280,7 +292,7 @@ This turns MimiClaw into a proactive assistant — write tasks to `HEARTBEAT.md`
- **OTA updates** — flash new firmware over WiFi, no USB needed
- **Dual-core** — network I/O and AI processing run on separate CPU cores
- **HTTP proxy** — CONNECT tunnel support for restricted networks
- **Multi-provider** — supports both Anthropic (Claude) and OpenAI (GPT), switchable at runtime
- **Multi-provider** — supports Anthropic (Claude), OpenAI (GPT), SiliconFlow, and Volcengine, switchable at runtime
- **Cron scheduler** — the AI can schedule its own recurring and one-shot tasks, persisted across reboots
- **Heartbeat** — periodically checks a task file and prompts the AI to act autonomously
- **Tool use** — ReAct agent loop with tool calling for both providers

View File

@@ -1,229 +1,56 @@
# 变更日志:增加国内大模型厂商接入
# 变更日志
## 版本信息
- **版本**v1.1.0(计划)
- **日期**2026-03-31
- **状态**:计划中
## v1.1.0(计划中)
## 功能概述
为 MimiClaw 项目增加国内大模型厂商接入支持,包括:
1. **硅基流动** (SiliconFlow) - 提供免费模型和多种高性能大模型
2. **火山方舟** (Volcengine Ark) - 字节跳动豆包模型系列
### 新增
- 国内大模型厂商接入支持(硅基流动、火山方舟)— 计划中
- **时区设置功能**
- 默认时区改为 `CST-8`(中国标准时间 UTC+8
- 新增 `set_timezone` CLI 命令(支持 POSIX 格式和城市名)
- 新增 `timezone_show` CLI 命令
- 新增 `set_timezone` LLM 工具(可通过对话设置时区)
- 时区通过 NVS 持久化存储(`system_config` namespace
- 支持城市名映射Asia/Shanghai → CST-8 等 18 个预设城市)
- `config_show` 中显示当前时区配置
## 实施计划
### 修复
- ESP-IDF v6.0 编译适配
- 修复 flash 大小配置2MB → 16MB
- 修复 WiFi 断开原因码未定义问题(添加 `#ifdef` 保护)
- 修复 CMakeLists.txt 缺少 `ota/ota_manager.c`
- 修复 16 处头文件缺失问题:
- `cli/serial_cli.c` 添加 `llm/llm_provider.h`
- `llm/llm_provider.c` 添加 `esp_http_client.h`
- `bus/message_bus.c` 添加 `freertos/FreeRTOS.h`, `freertos/queue.h`
- `wifi/wifi_manager.c` 添加 `esp_event.h`, `freertos/FreeRTOS.h`, `freertos/task.h`, `freertos/event_groups.h`
- `ota/ota_manager.c` 添加 `esp_system.h`
- `channels/telegram/telegram_bot.c` 添加 `freertos/FreeRTOS.h`, `freertos/task.h`
- `tools/tool_registry.c` 添加 `<stdlib.h>`
- `proxy/http_proxy.c` 添加 `<sys/time.h>`
- `gateway/ws_server.c` 添加 `<stdint.h>`
- 验证 ESP-IDF v6.0 API 兼容性esp_spiffs_info、esp_websocket_client_send_bin、esp_tls、console REPL 等均存在)
### 阶段一准备与设计1-2天
### 文档
- 新增 `docs/ESP-IDF-V6-MIGRATION.md` — ESP-IDF v6.0 迁移适配记录
- 更新 `taolun.md` — 讨论记录整理
#### 1.1 详细API调研
- [ ] 研究硅基流动API文档确认
- 具体的Base URL和端点
- 认证方式API Key格式
- 支持的模型列表和ID格式
- 工具调用兼容性
- 速率限制和配额
---
- [ ] 研究火山方舟API文档确认
- 具体的Base URL和端点
- 认证方式API Key格式
- 支持的模型列表和ID格式
- 工具调用兼容性
- 速率限制和配额
## v1.0.0
#### 1.2 架构设计
- [ ] 设计提供商扩展机制
- [ ] 确定配置管理方案
- [ ] 设计命令行接口扩展
- [ ] 评估内存影响
### 阶段二核心实现3-5天
#### 2.1 配置系统扩展
- [ ] 修改 `mimi_secrets.h.example` 添加新配置项:
```c
/* 国内大模型厂商配置 */
#define MIMI_SECRET_SILICONFLOW_API_KEY ""
#define MIMI_SECRET_SILICONFLOW_BASE_URL "https://api.siliconflow.cn/v1"
#define MIMI_SECRET_VOLCENGINE_API_KEY ""
#define MIMI_SECRET_VOLCENGINE_BASE_URL "https://ark.cn-beijing.volces.com/api/v3"
```
- [ ] 更新 `mimi_config.h` 添加相关默认值
#### 2.2 LLM代理扩展
- [ ] 修改 `llm_proxy.c` 支持新的提供商:
- 添加 `provider_is_siliconflow()` 函数
- 添加 `provider_is_volcengine()` 函数
- 扩展 `llm_api_url()` 函数支持新提供商
- 扩展 `llm_api_host()` 函数支持新提供商
- 扩展 `llm_api_path()` 函数支持新提供商
- [ ] 添加Base URL配置支持
```c
static char s_siliconflow_base_url[256] = "https://api.siliconflow.cn/v1";
static char s_volcengine_base_url[256] = "https://ark.cn-beijing.volces.com/api/v3";
```
- [ ] 修改HTTP请求头设置逻辑
- 硅基流动使用Bearer Token认证
- 火山方舟使用Bearer Token认证假设与OpenAI兼容
- [ ] 添加模型名称转换逻辑(如果需要)
#### 2.3 命令行界面扩展
- [ ] 在 `serial_cli.c` 添加新命令:
- `set_siliconflow_key <key>`设置硅基流动API密钥
- `set_siliconflow_url <url>`设置硅基流动Base URL
- `set_volcengine_key <key>`设置火山方舟API密钥
- `set_volcengine_url <url>`设置火山方舟Base URL
- [ ] 更新现有命令的帮助信息
- [ ] 更新 `config_show` 命令显示新配置
#### 2.4 提供商切换机制
- [ ] 修改 `set_model_provider` 命令支持新提供商:
- 支持值:`anthropic`, `openai`, `siliconflow`, `volcengine`
- [ ] 更新NVS存储键名
- 可能需要扩展 `MIMI_NVS_KEY_PROVIDER` 支持更多值
### 阶段三测试与优化2-3天
#### 3.1 功能测试
- [ ] 单元测试:
- 提供商检测函数测试
- API URL生成测试
- 请求头设置测试
- [ ] 集成测试:
- 硅基流动API连接测试
- 火山方舟API连接测试
- 工具调用功能测试
- 提供商切换测试
#### 3.2 性能优化
- [ ] 内存使用优化:
- 评估新增变量对内存的影响
- 优化字符串存储大小
- [ ] 网络性能:
- 测试国内网络环境下的连接稳定性
- 优化超时设置
#### 3.3 错误处理
- [ ] 添加详细的错误日志
- [ ] 处理API特定的错误响应
- [ ] 添加重试机制(如果需要)
### 阶段四文档与发布1天
#### 4.1 文档更新
- [ ] 更新 `README.md` 添加新功能说明
- [ ] 更新 `mimi_secrets.h.example` 添加配置示例
- [ ] 创建国内大模型厂商配置指南
- [ ] 更新串口CLI命令文档
#### 4.2 发布准备
- [ ] 代码审查
- [ ] 最终测试
- [ ] 创建发布标签
## 技术细节
### 提供商检测逻辑
```c
// 在 llm_proxy.c 中添加
static bool provider_is_siliconflow(void) {
return strcmp(s_provider, "siliconflow") == 0;
}
static bool provider_is_volcengine(void) {
return strcmp(s_provider, "volcengine") == 0;
}
```
### API URL 配置
```c
// 扩展 llm_api_url 函数
static const char *llm_api_url(void) {
if (provider_is_openai()) {
return MIMI_OPENAI_API_URL;
} else if (provider_is_siliconflow()) {
return s_siliconflow_base_url;
} else if (provider_is_volcengine()) {
return s_volcengine_base_url;
} else {
return MIMI_LLM_API_URL; // Anthropic
}
}
```
### 请求头设置
```c
// 扩展 HTTP 请求头设置
if (provider_is_openai() || provider_is_siliconflow() || provider_is_volcengine()) {
// OpenAI兼容的Bearer Token认证
if (s_api_key[0]) {
char auth[LLM_API_KEY_MAX_LEN + 16];
snprintf(auth, sizeof(auth), "Bearer %s", s_api_key);
esp_http_client_set_header(client, "Authorization", auth);
}
} else {
// Anthropic的x-api-key认证
esp_http_client_set_header(client, "x-api-key", s_api_key);
esp_http_client_set_header(client, "anthropic-version", MIMI_LLM_API_VERSION);
}
```
## 风险评估与缓解
### 风险1API兼容性问题
- **风险**国内厂商的API可能与OpenAI有细微差异
- **缓解**:详细测试,添加兼容性处理代码
### 风险2内存限制
- **风险**新增配置可能超出ESP32-S3内存限制
- **缓解**:优化字符串存储,使用固定大小数组
### 风险3网络连接问题
- **风险**国内网络环境可能影响API调用
- **缓解**:添加重试机制,优化超时设置
### 风险4认证安全
- **风险**API密钥存储和传输安全
- **缓解**使用现有的NVS加密存储确保安全传输
## 预期成果
1. **功能完成**:支持硅基流动和火山方舟两个国内大模型厂商
2. **配置灵活**:用户可以通过命令行或配置文件灵活配置
3. **向后兼容**不影响现有的Anthropic和OpenAI功能
4. **文档完整**:提供完整的配置和使用文档
## 时间估算
- **总时间**7-11个工作日
- **阶段一**1-2天
- **阶段二**3-5天
- **阶段三**2-3天
- **阶段四**1天
## 依赖项
1. **外部依赖**
- 硅基流动API访问权限
- 火山方舟API访问权限
- 稳定的网络连接
2. **内部依赖**
- 现有的LLM代理架构
- 配置管理系统
- 命令行界面系统
## 成功标准
1. 可以成功连接硅基流动API并获取响应
2. 可以成功连接火山方舟API并获取响应
3. 工具调用功能在两个新提供商上正常工作
4. 提供商切换功能正常
5. 内存使用在可接受范围内
6. 所有现有功能保持正常
### 功能
- Telegram 机器人长轮询
- Agent LoopReAct 工具调用,最多 10 轮迭代)
- Claude APIAnthropic Messages API
- OpenAI API 支持
- 工具注册 + web_searchBrave Search API
- 上下文构建器(系统提示 + 引导文件 + 记忆 + 工具指导
- 记忆存储MEMORY.md + 每日笔记)
- 会话管理JSONL per chat_id环形缓冲区历史
- WebSocket 网关(端口 18789JSON 协议)
- 串口 CLIesp_console调试/维护命令)
- HTTP CONNECT 代理支持
- OTA 更新
- WiFi 管理器(构建时凭证,指数退避)
- SPIFFS 存储
- 构建时配置(`mimi_secrets.h`+ 运行时 NVS 覆盖

View File

@@ -57,9 +57,17 @@ Telegram App (User)
│ Anthropic Messages API (HTTPS)
│ + Brave Search API (HTTPS)
───────────┐ ┌──────────────┐
│ Claude API │ │ Brave Search │
───────────┘ └──────────────┘
┌───────────┐ ┌──────────────┐ ┌──────────────┐
│ Claude API │ │ Brave Search │ │ Tavily Search│
└───────────┘ └──────────────┘ └──────────────┘
┌───────────┐ ┌──────────────┐
│ OpenAI API │ │ SiliconFlow │
└───────────┘ └──────────────┘
┌───────────┐ ┌──────────────┐
│ Volcengine │ │ Feishu Bot │
└───────────┘ └──────────────┘
```
---
@@ -106,15 +114,21 @@ main/
├── wifi/
│ ├── wifi_manager.h WiFi STA lifecycle API
│ └── wifi_manager.c Event handler, exponential backoff
│ └── wifi_manager.c Event handler, exponential backoff (timer-based retry)
├── telegram/
│ ├── telegram_bot.h Bot init/start, send_message API
── telegram_bot.c Long polling loop, JSON parsing, message splitting
├── channels/
│ ├── telegram/
│ ├── telegram_bot.h Bot init/start, send_message API
│ │ └── telegram_bot.c Long polling loop, JSON parsing, message splitting
│ └── feishu/
│ ├── feishu_bot.h Feishu bot API
│ └── feishu_bot.c WebSocket event handling, message send/recv
├── llm/
│ ├── llm_proxy.h llm_chat() + llm_chat_tools() API, tool_use types
── llm_proxy.c Anthropic Messages API (non-streaming), tool_use parsing
── llm_proxy.c Multi-provider LLM (Anthropic + OpenAI-compatible)
│ ├── llm_provider.h Provider registry + configuration API
│ └── llm_provider.c Provider configs: anthropic, openai, siliconflow, volcengine
├── agent/
│ ├── agent_loop.h Agent task init/start
@@ -125,8 +139,17 @@ main/
├── tools/
│ ├── tool_registry.h Tool definition struct, register/dispatch API
│ ├── tool_registry.c Tool registration, JSON schema builder, dispatch by name
│ ├── tool_web_search.h Web search tool API
── tool_web_search.c Brave Search API via HTTPS (direct + proxy)
│ ├── tool_web_search.h Web search tool API (Tavily + Brave)
── tool_web_search.c Brave/Tavily Search API via HTTPS
│ ├── tool_get_time.h Time tool API
│ ├── tool_get_time.c HTTP Date header parsing for time sync
│ ├── tool_cron.h Cron tool API
│ ├── tool_cron.c Cron job management
│ ├── tool_files.h File tool API
│ ├── tool_files.c read/write/edit/list files on SPIFFS
│ ├── tool_gpio.h GPIO tool API
│ ├── tool_gpio.c GPIO read/write
│ └── gpio_policy.c GPIO pin allowlist policy
├── memory/
│ ├── memory_store.h Long-term + daily memory API
@@ -140,12 +163,29 @@ main/
├── proxy/
│ ├── http_proxy.h Proxy connection API
│ └── http_proxy.c HTTP CONNECT tunnel + TLS via esp_tls
│ └── http_proxy.c HTTP CONNECT tunnel + SOCKS5 tunnel + TLS
├── cli/
│ ├── serial_cli.h CLI init API
│ └── serial_cli.c esp_console REPL with debug/maintenance commands
├── cron/
│ ├── cron_service.h Cron job API
│ └── cron_service.c Cron scheduler, job persistence, execution
├── heartbeat/
│ ├── heartbeat.h Heartbeat API
│ └── heartbeat.c Periodic heartbeat messages
├── onboard/
│ ├── wifi_onboard.h WiFi onboarding portal API
│ ├── wifi_onboard.c Captive portal + Soft AP + HTTP config page
│ └── onboard_html.h Embedded HTML/CSS/JS for setup page
├── skills/
│ ├── skill_loader.h Skill loader API
│ └── skill_loader.c Load skill files from SPIFFS
└── ota/
├── ota_manager.h OTA update API
└── ota_manager.c esp_https_ota wrapper
@@ -158,9 +198,13 @@ main/
| Task | Core | Priority | Stack | Description |
|--------------------|------|----------|--------|--------------------------------------|
| `tg_poll` | 0 | 5 | 12 KB | Telegram long polling (30s timeout) |
| `agent_loop` | 1 | 6 | 12 KB | Message processing + Claude API call |
| `outbound` | 0 | 5 | 8 KB | Route responses to Telegram / WS |
| `feishu_ws` | 0 | 5 | 12 KB | Feishu WebSocket event handling |
| `agent_loop` | 1 | 6 | 24 KB | Message processing + LLM API call |
| `outbound` | 0 | 5 | 12 KB | Route responses to channels |
| `serial_cli` | 0 | 3 | 4 KB | USB serial console REPL |
| `onboard_dns` | 0 | 5 | 4 KB | DNS hijack for captive portal |
| `cron_check` | 0 | 4 | 4 KB | Cron job scheduler |
| `heartbeat` | 0 | 4 | 4 KB | Periodic heartbeat |
| httpd (internal) | 0 | 5 | — | WebSocket server (esp_http_server) |
| wifi_event (IDF) | 0 | 8 | — | WiFi event handling (ESP-IDF) |
@@ -225,20 +269,66 @@ Session files are JSONL (one JSON object per line):
## Configuration
All configuration is done exclusively through `mimi_secrets.h` at build time. There is no runtime configuration — changing any setting requires `idf.py fullclean && idf.py build`.
Configuration uses a multi-layer priority system:
| Define | Description |
|------------------------------|-----------------------------------------|
| `MIMI_SECRET_WIFI_SSID` | WiFi SSID |
| `MIMI_SECRET_WIFI_PASS` | WiFi password |
| `MIMI_SECRET_TG_TOKEN` | Telegram Bot API token |
| `MIMI_SECRET_API_KEY` | Anthropic API key |
| `MIMI_SECRET_MODEL` | Model ID (default: claude-opus-4-6) |
| `MIMI_SECRET_PROXY_HOST` | HTTP proxy hostname/IP (optional) |
| `MIMI_SECRET_PROXY_PORT` | HTTP proxy port (optional) |
| `MIMI_SECRET_SEARCH_KEY` | Brave Search API key (optional) |
### Build-time (`mimi_secrets.h`)
Highest priority. Set in `mimi_secrets.h` (copy from `mimi_secrets.h.example`).
NVS is still initialized (required by ESP-IDF WiFi internals) but is not used for application configuration.
| Define | Description |
|-------------------------------------|--------------------------------------------|
| `MIMI_SECRET_WIFI_SSID` | WiFi SSID |
| `MIMI_SECRET_WIFI_PASS` | WiFi password |
| `MIMI_SECRET_TG_TOKEN` | Telegram Bot API token |
| `MIMI_SECRET_FEISHU_APP_ID` | Feishu App ID |
| `MIMI_SECRET_FEISHU_APP_SECRET` | Feishu App Secret |
| `MIMI_SECRET_API_KEY` | Generic LLM API key (fallback) |
| `MIMI_SECRET_MODEL` | Model ID (default: claude-opus-4-5) |
| `MIMI_SECRET_MODEL_PROVIDER` | LLM provider: anthropic/openai/siliconflow/volcengine |
| `MIMI_SECRET_ANTHROPIC_API_KEY` | Anthropic-specific API key |
| `MIMI_SECRET_OPENAI_API_KEY` | OpenAI-specific API key |
| `MIMI_SECRET_SILICONFLOW_API_KEY` | SiliconFlow (硅基流动) API key |
| `MIMI_SECRET_SILICONFLOW_BASE_URL` | SiliconFlow Base URL |
| `MIMI_SECRET_VOLCENGINE_API_KEY` | Volcengine (火山引擎) API key |
| `MIMI_SECRET_VOLCENGINE_BASE_URL` | Volcengine Base URL |
| `MIMI_SECRET_PROXY_HOST` | HTTP proxy hostname/IP (optional) |
| `MIMI_SECRET_PROXY_PORT` | HTTP proxy port (optional) |
| `MIMI_SECRET_PROXY_TYPE` | Proxy type: http/socks5 |
| `MIMI_SECRET_SEARCH_KEY` | Brave Search API key (optional) |
| `MIMI_SECRET_TAVILY_KEY` | Tavily Search API key (optional) |
### Runtime (NVS + Onboard Portal)
Set via serial CLI or the onboard configuration portal (192.168.4.1).
| CLI Command | Description |
|------------------------------------|--------------------------------------|
| `wifi_set <SSID> <Password>` | Set WiFi credentials |
| `set_tg_token <Token>` | Set Telegram Bot token |
| `set_api_key <Key>` | Set generic LLM API key |
| `set_model_provider <Provider>` | Set provider: anthropic/openai/siliconflow/volcengine |
| `set_model <Model>` | Set model name |
| `set_siliconflow_key <Key>` | Set SiliconFlow-specific API key |
| `set_siliconflow_url <URL>` | Set SiliconFlow Base URL |
| `set_volcengine_key <Key>` | Set Volcengine-specific API key |
| `set_volcengine_url <URL>` | Set Volcengine Base URL |
| `config_show` | Show current config (masked) |
| `config_reset` | Reset to build-time defaults |
### Priority Order (highest → lowest)
1. NVS runtime config (CLI or onboard portal)
2. Provider-specific NVS key (e.g. `siliconflow_api_key`)
3. Provider-specific build-time config (e.g. `MIMI_SECRET_SILICONFLOW_API_KEY`)
4. Generic build-time config (`MIMI_SECRET_API_KEY`, `MIMI_SECRET_MODEL_PROVIDER`)
## Supported LLM Providers
| Provider | API Compatible | Default Endpoint |
|-------------|----------------|-------------------------------------------------------|
| anthropic | Anthropic | https://api.anthropic.com/v1/messages |
| openai | OpenAI | https://api.openai.com/v1/chat/completions |
| siliconflow | OpenAI | https://api.siliconflow.cn/v1/chat/completions |
| volcengine | OpenAI | https://ark.cn-beijing.volces.com/api/v3/chat/completions |
All OpenAI-compatible providers use Bearer token authentication and the same message format.
---

View File

@@ -0,0 +1,82 @@
# ESP-IDF v6.0 编译适配记录
> 日期2026-03-31
> 目标芯片ESP32-S3
> ESP-IDF 版本v6.0
> 问题:从旧版本迁移到 ESP-IDF v6.0 后编译失败存在多处头文件缺失、配置错误、CMakeLists 遗漏
---
## 问题清单与修复
### 1. Flash 大小配置错误
**错误信息:**
```
Partitions tables occupies 16.0MB of flash which does not fit in configured flash size 2MB
```
**修复:** `sdkconfig` 中 flash 大小从 2MB 改为 16MB
- `CONFIG_ESPTOOLPY_FLASHSIZE_2MB=y``CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y`
- `CONFIG_ESPTOOLPY_FLASHSIZE="2MB"``CONFIG_ESPTOOLPY_FLASHSIZE="16MB"`
### 2. WiFi 断开原因码未定义
**错误信息:**
```
error: 'WIFI_REASON_ASSOC_EXPIRE' undeclared
```
**修复:** `main/wifi/wifi_manager.c` — 为所有 reason code 添加 `#ifdef` 保护ESP-IDF v6.0 移除了部分原因码)
### 3. CMakeLists.txt 缺少源文件
**修复:** `main/CMakeLists.txt`
- 添加 `"ota/ota_manager.c"` 到 SRCS 列表
### 4. 头文件缺失(共修复 16 处)
| 文件 | 缺失头文件 | 使用符号 |
|------|-----------|---------|
| `main/cli/serial_cli.c` | `llm/llm_provider.h` | `llm_provider_set_api_key` |
| `main/llm/llm_provider.c` | `esp_http_client.h` | `esp_http_client_set_header` |
| `main/bus/message_bus.c` | `freertos/FreeRTOS.h`, `freertos/queue.h` | `xQueueCreate`, `QueueHandle_t` |
| `main/wifi/wifi_manager.c` | `esp_event.h` | `esp_event_handler_instance_register` |
| `main/wifi/wifi_manager.c` | `freertos/FreeRTOS.h`, `freertos/task.h`, `freertos/event_groups.h` | `xEventGroupCreate`, `vTaskDelay` |
| `main/ota/ota_manager.c` | `esp_system.h` | `esp_restart` |
| `main/channels/telegram/telegram_bot.c` | `freertos/FreeRTOS.h`, `freertos/task.h` | `xTaskCreatePinnedToCore`, `vTaskDelay` |
| `main/channels/telegram/telegram_bot.c` | `esp_err.h` | `esp_err_to_name` |
| `main/tools/tool_registry.c` | `<stdlib.h>` | `free()` |
| `main/proxy/http_proxy.c` | `<sys/time.h>` | `struct timeval` |
| `main/gateway/ws_server.c` | `<stdint.h>` | `uint8_t` |
---
## ESP-IDF v6.0 API 兼容性验证
以下 API 在 v6.0 中**仍然可用**,无需修改:
| API | 位置 | 状态 |
|-----|------|------|
| `esp_spiffs_info()` | `esp_spiffs.h` | ✅ 存在 |
| `esp_websocket_client_send_bin()` | `esp_websocket_client.h` | ✅ 存在 |
| `esp_tls_set_conn_sockfd()` | `esp_tls.h` | ✅ 存在 |
| `esp_tls_set_conn_state()` | `esp_tls.h` | ✅ 存在 |
| `esp_console_new_repl_uart()` | `esp_console.h` | ✅ 存在 |
| `esp_console_new_repl_usb_serial_jtag()` | `esp_console.h` | ✅ 存在 |
| `esp_console_new_repl_usb_cdc()` | `esp_console.h` | ✅ 存在 |
| `esp_https_ota()` + `esp_https_ota_config_t` | `esp_https_ota.h` | ✅ 存在 |
| `esp_wifi_set_config()` | `esp_wifi.h` | ✅ 存在 |
---
## 烧录说明
ESP32-S3 烧录使用 **USB 口**(内置 USB Serial/JTAG 控制器):
```powershell
idf.py -p COMx flash monitor
```
-**USB 口**(标记为 `USB`),不是 UART 口
- 如遇连接失败,按住 **BOOT** 键再插线进入下载模式

View File

@@ -6,6 +6,7 @@ idf_component_register(
"channels/telegram/telegram_bot.c"
"channels/feishu/feishu_bot.c"
"llm/llm_proxy.c"
"llm/llm_provider.c"
"agent/agent_loop.c"
"agent/context_builder.c"
"memory/memory_store.c"
@@ -19,15 +20,17 @@ idf_component_register(
"tools/tool_cron.c"
"tools/tool_web_search.c"
"tools/tool_get_time.c"
"tools/tool_set_timezone.c"
"tools/tool_files.c"
"tools/tool_gpio.c"
"tools/gpio_policy.c"
"skills/skill_loader.c"
"onboard/wifi_onboard.c"
"ota/ota_manager.c"
INCLUDE_DIRS
"."
REQUIRES
nvs_flash esp_wifi esp_netif esp_http_client esp_http_server
esp_https_ota esp_event json spiffs console vfs app_update esp-tls
esp_https_ota esp_event cjson spiffs console vfs app_update esp-tls
esp_timer esp_websocket_client esp_driver_gpio
)

View File

@@ -14,8 +14,12 @@ static size_t append_file(char *buf, size_t size, size_t offset, const char *pat
FILE *f = fopen(path, "r");
if (!f) return offset;
if (header && offset < size - 1) {
offset += snprintf(buf + offset, size - offset, "\n## %s\n\n", header);
if (offset >= size) return offset;
if (header) {
int ret = snprintf(buf + offset, size - offset, "\n## %s\n\n", header);
if (ret > 0) offset += (size_t)ret;
if (offset >= size) { offset = size - 1; buf[offset] = '\0'; fclose(f); return offset; }
}
size_t n = fread(buf + offset, 1, size - offset - 1, f);
@@ -79,23 +83,35 @@ esp_err_t context_build_system_prompt(char *buf, size_t size)
/* Long-term memory */
char mem_buf[4096];
if (memory_read_long_term(mem_buf, sizeof(mem_buf)) == ESP_OK && mem_buf[0]) {
off += snprintf(buf + off, size - off, "\n## Long-term Memory\n\n%s\n", mem_buf);
if (off < size) {
int ret = snprintf(buf + off, size - off, "\n## Long-term Memory\n\n%s\n", mem_buf);
if (ret > 0) off += (size_t)ret;
if (off >= size) off = size - 1;
}
}
/* Recent daily notes (last 3 days) */
char recent_buf[4096];
if (memory_read_recent(recent_buf, sizeof(recent_buf), 3) == ESP_OK && recent_buf[0]) {
off += snprintf(buf + off, size - off, "\n## Recent Notes\n\n%s\n", recent_buf);
if (off < size) {
int ret = snprintf(buf + off, size - off, "\n## Recent Notes\n\n%s\n", recent_buf);
if (ret > 0) off += (size_t)ret;
if (off >= size) off = size - 1;
}
}
/* Skills */
char skills_buf[2048];
size_t skills_len = skill_loader_build_summary(skills_buf, sizeof(skills_buf));
if (skills_len > 0) {
off += snprintf(buf + off, size - off,
"\n## Available Skills\n\n"
"Available skills (use read_file to load full instructions):\n%s\n",
skills_buf);
if (off < size) {
int ret = snprintf(buf + off, size - off,
"\n## Available Skills\n\n"
"Available skills (use read_file to load full instructions):\n%s\n",
skills_buf);
if (ret > 0) off += (size_t)ret;
if (off >= size) off = size - 1;
}
}
ESP_LOGI(TAG, "System prompt built: %d bytes", (int)off);

View File

@@ -1,6 +1,8 @@
#include "message_bus.h"
#include "mimi_config.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include <string.h>
static const char *TAG = "bus";

View File

@@ -583,6 +583,7 @@ static void feishu_ws_event_handler(void *arg, esp_event_base_t base, int32_t ev
} else if (event_id == WEBSOCKET_EVENT_DISCONNECTED) {
s_ws_connected = false;
ESP_LOGW(TAG, "Feishu WS disconnected");
if (rx_buf) { free(rx_buf); rx_buf = NULL; rx_cap = 0; }
} else if (event_id == WEBSOCKET_EVENT_DATA) {
if (e->op_code != WS_TRANSPORT_OPCODES_BINARY) return;
size_t need = e->payload_offset + e->data_len;

View File

@@ -7,11 +7,14 @@
#include <stdlib.h>
#include <stdbool.h>
#include "esp_log.h"
#include "esp_err.h"
#include "esp_timer.h"
#include "esp_http_client.h"
#include "esp_crt_bundle.h"
#include "nvs.h"
#include "cJSON.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "telegram";

View File

@@ -4,6 +4,7 @@
#include "channels/telegram/telegram_bot.h"
#include "channels/feishu/feishu_bot.h"
#include "llm/llm_proxy.h"
#include "llm/llm_provider.h"
#include "memory/memory_store.h"
#include "memory/session_mgr.h"
#include "proxy/http_proxy.h"
@@ -16,6 +17,8 @@
#include <string.h>
#include <stdio.h>
#include <ctype.h>
#include <stdbool.h>
#include <time.h>
#include <dirent.h>
#include "esp_log.h"
#include "esp_console.h"
@@ -171,6 +174,80 @@ static int cmd_set_model_provider(int argc, char **argv)
return 0;
}
/* --- set_siliconflow_key command --- */
static struct {
struct arg_str *key;
struct arg_end *end;
} siliconflow_key_args;
static int cmd_set_siliconflow_key(int argc, char **argv)
{
int nerrors = arg_parse(argc, argv, (void **)&siliconflow_key_args);
if (nerrors != 0) {
arg_print_errors(stderr, siliconflow_key_args.end, argv[0]);
return 1;
}
llm_set_api_key(siliconflow_key_args.key->sval[0]);
llm_provider_set_api_key("siliconflow", siliconflow_key_args.key->sval[0]);
printf("SiliconFlow API key saved.\n");
return 0;
}
/* --- set_siliconflow_url command --- */
static struct {
struct arg_str *url;
struct arg_end *end;
} siliconflow_url_args;
static int cmd_set_siliconflow_url(int argc, char **argv)
{
int nerrors = arg_parse(argc, argv, (void **)&siliconflow_url_args);
if (nerrors != 0) {
arg_print_errors(stderr, siliconflow_url_args.end, argv[0]);
return 1;
}
llm_set_base_url("siliconflow", siliconflow_url_args.url->sval[0]);
printf("SiliconFlow Base URL saved.\n");
return 0;
}
/* --- set_volcengine_key command --- */
static struct {
struct arg_str *key;
struct arg_end *end;
} volcengine_key_args;
static int cmd_set_volcengine_key(int argc, char **argv)
{
int nerrors = arg_parse(argc, argv, (void **)&volcengine_key_args);
if (nerrors != 0) {
arg_print_errors(stderr, volcengine_key_args.end, argv[0]);
return 1;
}
llm_set_api_key(volcengine_key_args.key->sval[0]);
llm_provider_set_api_key("volcengine", volcengine_key_args.key->sval[0]);
printf("Volcengine API key saved.\n");
return 0;
}
/* --- set_volcengine_url command --- */
static struct {
struct arg_str *url;
struct arg_end *end;
} volcengine_url_args;
static int cmd_set_volcengine_url(int argc, char **argv)
{
int nerrors = arg_parse(argc, argv, (void **)&volcengine_url_args);
if (nerrors != 0) {
arg_print_errors(stderr, volcengine_url_args.end, argv[0]);
return 1;
}
llm_set_base_url("volcengine", volcengine_url_args.url->sval[0]);
printf("Volcengine Base URL saved.\n");
return 0;
}
/* --- memory_read command --- */
static int cmd_memory_read(int argc, char **argv)
{
@@ -563,10 +640,27 @@ static int cmd_config_show(int argc, char **argv)
print_config("API Key", MIMI_NVS_LLM, MIMI_NVS_KEY_API_KEY, MIMI_SECRET_API_KEY, true);
print_config("Model", MIMI_NVS_LLM, MIMI_NVS_KEY_MODEL, MIMI_SECRET_MODEL, false);
print_config("Provider", MIMI_NVS_LLM, MIMI_NVS_KEY_PROVIDER, MIMI_SECRET_MODEL_PROVIDER, false);
/* Provider-specific configurations */
printf(" --- Provider-specific configs ---\n");
print_config("Anthropic Key", MIMI_NVS_LLM, MIMI_NVS_KEY_ANTHROPIC_API_KEY, "", true);
print_config("Anthropic URL", MIMI_NVS_LLM, MIMI_NVS_KEY_ANTHROPIC_BASE_URL, "", false);
print_config("OpenAI Key", MIMI_NVS_LLM, MIMI_NVS_KEY_OPENAI_API_KEY, "", true);
print_config("OpenAI URL", MIMI_NVS_LLM, MIMI_NVS_KEY_OPENAI_BASE_URL, "", false);
print_config("SiliconFlow Key", MIMI_NVS_LLM, MIMI_NVS_KEY_SILICONFLOW_API_KEY, "", true);
print_config("SiliconFlow URL", MIMI_NVS_LLM, MIMI_NVS_KEY_SILICONFLOW_BASE_URL, "", false);
print_config("Volcengine Key", MIMI_NVS_LLM, MIMI_NVS_KEY_VOLCENGINE_API_KEY, "", true);
print_config("Volcengine URL", MIMI_NVS_LLM, MIMI_NVS_KEY_VOLCENGINE_BASE_URL, "", false);
print_config("Proxy Host", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_HOST, MIMI_SECRET_PROXY_HOST, false);
print_config_u16("Proxy Port", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_PORT, MIMI_SECRET_PROXY_PORT);
print_config("Search Key", MIMI_NVS_SEARCH, MIMI_NVS_KEY_API_KEY, MIMI_SECRET_SEARCH_KEY, true);
print_config("Tavily Key", MIMI_NVS_SEARCH, MIMI_NVS_KEY_TAVILY_KEY, MIMI_SECRET_TAVILY_KEY, true);
/* System configs */
printf(" --- System configs ---\n");
print_config("Timezone", "system_config", MIMI_NVS_KEY_TIMEZONE, MIMI_TIMEZONE, false);
printf("=============================\n");
return 0;
}
@@ -589,6 +683,87 @@ static int cmd_config_reset(int argc, char **argv)
return 0;
}
/* --- set_timezone command --- */
static struct {
struct arg_str *tz;
struct arg_end *end;
} timezone_args;
static int cmd_set_timezone(int argc, char **argv)
{
int nerrors = arg_parse(argc, argv, (void **)&timezone_args);
if (nerrors != 0) {
arg_print_errors(stderr, timezone_args.end, argv[0]);
return 1;
}
const char *tz_str = timezone_args.tz->sval[0];
nvs_handle_t nvs;
esp_err_t err = nvs_open("system_config", NVS_READWRITE, &nvs);
if (err != ESP_OK) {
printf("Failed to open NVS: %s\n", esp_err_to_name(err));
return 1;
}
err = nvs_set_str(nvs, MIMI_NVS_KEY_TIMEZONE, tz_str);
if (err != ESP_OK) {
printf("Failed to save timezone: %s\n", esp_err_to_name(err));
nvs_close(nvs);
return 1;
}
err = nvs_commit(nvs);
nvs_close(nvs);
if (err != ESP_OK) {
printf("Failed to commit NVS: %s\n", esp_err_to_name(err));
return 1;
}
setenv("TZ", tz_str, 1);
tzset();
time_t now = time(NULL);
struct tm tm_now;
localtime_r(&now, &tm_now);
char time_str[64];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S %Z", &tm_now);
printf("Timezone set to '%s'. Current time: %s. Restart to apply permanently.\n", tz_str, time_str);
return 0;
}
/* --- timezone_show command --- */
static int cmd_timezone_show(int argc, char **argv)
{
(void)argc;
(void)argv;
char nvs_val[64] = {0};
const char *source = "build";
const char *display = MIMI_TIMEZONE;
nvs_handle_t nvs;
if (nvs_open("system_config", NVS_READONLY, &nvs) == ESP_OK) {
size_t len = sizeof(nvs_val);
if (nvs_get_str(nvs, MIMI_NVS_KEY_TIMEZONE, nvs_val, &len) == ESP_OK && nvs_val[0]) {
source = "NVS";
display = nvs_val;
}
nvs_close(nvs);
}
printf("Current timezone: %s [%s]\n", display, source);
time_t now = time(NULL);
struct tm tm_now;
localtime_r(&now, &tm_now);
char time_str[64];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S %Z (%A)", &tm_now);
printf("Local time: %s\n", time_str);
return 0;
}
/* --- heartbeat_trigger command --- */
static int cmd_heartbeat_trigger(int argc, char **argv)
{
@@ -649,13 +824,22 @@ typedef struct {
size_t output_size;
esp_err_t err;
SemaphoreHandle_t done;
bool timed_out;
} web_search_task_ctx_t;
static void web_search_task(void *arg)
{
web_search_task_ctx_t *task_ctx = (web_search_task_ctx_t *)arg;
task_ctx->err = tool_web_search_execute(task_ctx->input_json, task_ctx->output, task_ctx->output_size);
xSemaphoreGive(task_ctx->done);
if (!task_ctx->timed_out) {
xSemaphoreGive(task_ctx->done);
} else {
/* Main thread timed out and freed ctx, so we must clean up ourselves */
free((void *)task_ctx->input_json);
free(task_ctx->output);
vSemaphoreDelete(task_ctx->done);
free(task_ctx);
}
vTaskDelete(NULL);
}
@@ -751,10 +935,8 @@ static int cmd_web_search(int argc, char **argv)
if (xSemaphoreTake(ctx->done, pdMS_TO_TICKS(45000)) != pdTRUE) {
printf("web_search status: timeout\n");
vSemaphoreDelete(ctx->done);
free(input_copy);
free(ctx);
free(output);
ctx->timed_out = true;
/* Task will clean up ctx, input_copy, output, and done on its own */
return 1;
}
esp_err_t err = ctx->err;
@@ -887,7 +1069,7 @@ esp_err_t serial_cli_init(void)
esp_console_cmd_register(&model_cmd);
/* set_model_provider */
provider_args.provider = arg_str1(NULL, NULL, "<provider>", "Model provider (anthropic|openai)");
provider_args.provider = arg_str1(NULL, NULL, "<provider>", "Model provider (anthropic|openai|siliconflow|volcengine)");
provider_args.end = arg_end(1);
esp_console_cmd_t provider_cmd = {
.command = "set_model_provider",
@@ -897,6 +1079,50 @@ esp_err_t serial_cli_init(void)
};
esp_console_cmd_register(&provider_cmd);
/* set_siliconflow_key */
siliconflow_key_args.key = arg_str1(NULL, NULL, "<key>", "SiliconFlow API key");
siliconflow_key_args.end = arg_end(1);
esp_console_cmd_t siliconflow_key_cmd = {
.command = "set_siliconflow_key",
.help = "Set SiliconFlow API key",
.func = &cmd_set_siliconflow_key,
.argtable = &siliconflow_key_args,
};
esp_console_cmd_register(&siliconflow_key_cmd);
/* set_siliconflow_url */
siliconflow_url_args.url = arg_str1(NULL, NULL, "<url>", "SiliconFlow Base URL");
siliconflow_url_args.end = arg_end(1);
esp_console_cmd_t siliconflow_url_cmd = {
.command = "set_siliconflow_url",
.help = "Set SiliconFlow Base URL",
.func = &cmd_set_siliconflow_url,
.argtable = &siliconflow_url_args,
};
esp_console_cmd_register(&siliconflow_url_cmd);
/* set_volcengine_key */
volcengine_key_args.key = arg_str1(NULL, NULL, "<key>", "Volcengine API key");
volcengine_key_args.end = arg_end(1);
esp_console_cmd_t volcengine_key_cmd = {
.command = "set_volcengine_key",
.help = "Set Volcengine API key",
.func = &cmd_set_volcengine_key,
.argtable = &volcengine_key_args,
};
esp_console_cmd_register(&volcengine_key_cmd);
/* set_volcengine_url */
volcengine_url_args.url = arg_str1(NULL, NULL, "<url>", "Volcengine Base URL");
volcengine_url_args.end = arg_end(1);
esp_console_cmd_t volcengine_url_cmd = {
.command = "set_volcengine_url",
.help = "Set Volcengine Base URL",
.func = &cmd_set_volcengine_url,
.argtable = &volcengine_url_args,
};
esp_console_cmd_register(&volcengine_url_cmd);
/* skill_list */
esp_console_cmd_t skill_list_cmd = {
.command = "skill_list",
@@ -1032,6 +1258,25 @@ esp_err_t serial_cli_init(void)
};
esp_console_cmd_register(&config_reset_cmd);
/* set_timezone */
timezone_args.tz = arg_str1(NULL, NULL, "<timezone>", "Timezone (e.g. CST-8, Asia/Shanghai)");
timezone_args.end = arg_end(1);
esp_console_cmd_t set_timezone_cmd = {
.command = "set_timezone",
.help = "Set system timezone (e.g. set_timezone CST-8)",
.func = &cmd_set_timezone,
.argtable = &timezone_args,
};
esp_console_cmd_register(&set_timezone_cmd);
/* timezone_show */
esp_console_cmd_t timezone_show_cmd = {
.command = "timezone_show",
.help = "Show current timezone and local time",
.func = &cmd_timezone_show,
};
esp_console_cmd_register(&timezone_show_cmd);
/* heartbeat_trigger */
esp_console_cmd_t heartbeat_cmd = {
.command = "heartbeat_trigger",

View File

@@ -4,6 +4,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>
#include "freertos/FreeRTOS.h"

View File

@@ -4,6 +4,9 @@
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include "esp_log.h"
#include "esp_http_server.h"
#include "cJSON.h"

View File

@@ -2,7 +2,7 @@
dependencies:
## Required IDF version
idf:
version: '>=5.5.0,<5.6.0'
version: ">=5.5.0,<7.0.0"
# # Put list of dependencies here
# # For components maintained by Espressif:
# component: "~1.0.0"
@@ -15,3 +15,4 @@ dependencies:
# # All dependencies of `main` are public by default.
# public: true
espressif/esp_websocket_client: ^1.4.0
espressif/cjson: ">=1.0.0"

300
main/llm/llm_provider.c Normal file
View File

@@ -0,0 +1,300 @@
#include "llm_provider.h"
#include "mimi_config.h"
#include "nvs.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include "esp_log.h"
#include "esp_err.h"
#include "esp_http_client.h"
static const char *TAG = "llm_provider";
#define LLM_API_KEY_MAX_LEN 320
/* Provider registry - all supported providers */
const llm_provider_config_t llm_providers[] = {
{
.name = "anthropic",
.default_api_url = MIMI_LLM_API_URL,
.default_host = "api.anthropic.com",
.default_path = "/v1/messages",
.is_openai_compatible = false,
},
{
.name = "openai",
.default_api_url = MIMI_OPENAI_API_URL,
.default_host = "api.openai.com",
.default_path = "/v1/chat/completions",
.is_openai_compatible = true,
},
{
.name = "siliconflow",
.default_api_url = MIMI_SILICONFLOW_API_URL,
.default_host = "api.siliconflow.cn",
.default_path = "/v1/chat/completions",
.is_openai_compatible = true,
},
{
.name = "volcengine",
.default_api_url = MIMI_VOLCENGINE_API_URL,
.default_host = "ark.cn-beijing.volces.com",
.default_path = "/v1/chat/completions",
.is_openai_compatible = true,
},
};
const int llm_provider_count = sizeof(llm_providers) / sizeof(llm_providers[0]);
/* Current provider state */
static const llm_provider_config_t *s_current_provider = &llm_providers[0]; /* Default to anthropic */
static char s_api_key[LLM_API_KEY_MAX_LEN] = {0};
static char s_base_url[256] = {0};
/* Helper function to get NVS key for provider API key */
static const char *get_provider_api_key_nvs_key(const char *provider_name) {
if (strcmp(provider_name, "anthropic") == 0) return MIMI_NVS_KEY_ANTHROPIC_API_KEY;
if (strcmp(provider_name, "openai") == 0) return MIMI_NVS_KEY_OPENAI_API_KEY;
if (strcmp(provider_name, "siliconflow") == 0) return MIMI_NVS_KEY_SILICONFLOW_API_KEY;
if (strcmp(provider_name, "volcengine") == 0) return MIMI_NVS_KEY_VOLCENGINE_API_KEY;
return NULL;
}
/* Helper function to get NVS key for provider Base URL */
static const char *get_provider_base_url_nvs_key(const char *provider_name) {
if (strcmp(provider_name, "anthropic") == 0) return MIMI_NVS_KEY_ANTHROPIC_BASE_URL;
if (strcmp(provider_name, "openai") == 0) return MIMI_NVS_KEY_OPENAI_BASE_URL;
if (strcmp(provider_name, "siliconflow") == 0) return MIMI_NVS_KEY_SILICONFLOW_BASE_URL;
if (strcmp(provider_name, "volcengine") == 0) return MIMI_NVS_KEY_VOLCENGINE_BASE_URL;
return NULL;
}
/* Find provider configuration by name */
const llm_provider_config_t *llm_provider_find(const char *name) {
if (!name) return NULL;
for (int i = 0; i < llm_provider_count; i++) {
if (strcmp(llm_providers[i].name, name) == 0) {
return &llm_providers[i];
}
}
return NULL;
}
/* Get current provider configuration */
const llm_provider_config_t *llm_provider_current(void) {
return s_current_provider;
}
/* Set current provider by name */
void llm_provider_set_current(const char *name) {
const llm_provider_config_t *provider = llm_provider_find(name);
if (provider) {
s_current_provider = provider;
/* Load provider-specific configuration */
llm_provider_init();
ESP_LOGI(TAG, "Current provider set to: %s", name);
} else {
ESP_LOGW(TAG, "Unknown provider: %s", name);
}
}
/* Get current provider name */
const char *llm_provider_current_name(void) {
return s_current_provider ? s_current_provider->name : "unknown";
}
/* Check if current provider is OpenAI-compatible */
bool llm_provider_is_openai_compatible(void) {
return s_current_provider ? s_current_provider->is_openai_compatible : false;
}
/* Get API URL for current provider (with dynamic Base URL support) */
const char *llm_provider_api_url(void) {
if (s_base_url[0] != '\0') {
/* Use configured Base URL */
static char full_url[512];
snprintf(full_url, sizeof(full_url), "%s%s", s_base_url, s_current_provider->default_path);
return full_url;
}
/* Use default API URL */
return s_current_provider ? s_current_provider->default_api_url : "";
}
/* Get hostname for current provider */
const char *llm_provider_host(void) {
if (s_base_url[0] != '\0') {
/* Extract hostname from Base URL */
static char hostname[256];
const char *start = s_base_url;
if (strncmp(start, "https://", 8) == 0) start += 8;
else if (strncmp(start, "http://", 7) == 0) start += 7;
const char *end = strchr(start, '/');
if (end) {
size_t len = end - start;
if (len < sizeof(hostname)) {
memcpy(hostname, start, len);
hostname[len] = '\0';
return hostname;
}
}
/* No path, copy whole string */
strncpy(hostname, start, sizeof(hostname) - 1);
hostname[sizeof(hostname) - 1] = '\0';
return hostname;
}
/* Use default host */
return s_current_provider ? s_current_provider->default_host : "";
}
/* Get API path for current provider */
const char *llm_provider_path(void) {
return s_current_provider ? s_current_provider->default_path : "";
}
/* Provider-specific API key management */
void llm_provider_set_api_key(const char *provider_name, const char *api_key) {
if (!provider_name || !api_key) return;
const char *nvs_key = get_provider_api_key_nvs_key(provider_name);
if (!nvs_key) {
ESP_LOGW(TAG, "No NVS key for provider: %s", provider_name);
return;
}
nvs_handle_t nvs;
if (nvs_open(MIMI_NVS_LLM, NVS_READWRITE, &nvs) == ESP_OK) {
nvs_set_str(nvs, nvs_key, api_key);
nvs_commit(nvs);
nvs_close(nvs);
ESP_LOGI(TAG, "API key saved for provider: %s", provider_name);
}
/* If this is the current provider, update in-memory key */
if (strcmp(provider_name, s_current_provider->name) == 0) {
strncpy(s_api_key, api_key, sizeof(s_api_key) - 1);
s_api_key[sizeof(s_api_key) - 1] = '\0';
}
}
/* Provider-specific Base URL management */
void llm_provider_set_base_url(const char *provider_name, const char *base_url) {
if (!provider_name || !base_url) return;
const char *nvs_key = get_provider_base_url_nvs_key(provider_name);
if (!nvs_key) {
ESP_LOGW(TAG, "No NVS key for provider: %s", provider_name);
return;
}
nvs_handle_t nvs;
if (nvs_open(MIMI_NVS_LLM, NVS_READWRITE, &nvs) == ESP_OK) {
nvs_set_str(nvs, nvs_key, base_url);
nvs_commit(nvs);
nvs_close(nvs);
ESP_LOGI(TAG, "Base URL saved for provider: %s", provider_name);
}
/* If this is the current provider, update in-memory Base URL */
if (strcmp(provider_name, s_current_provider->name) == 0) {
strncpy(s_base_url, base_url, sizeof(s_base_url) - 1);
s_base_url[sizeof(s_base_url) - 1] = '\0';
}
}
/* Get API key for a provider */
const char *llm_provider_get_api_key(const char *provider_name) {
if (!provider_name) return NULL;
/* If this is the current provider, return in-memory key */
if (strcmp(provider_name, s_current_provider->name) == 0) {
return s_api_key;
}
/* Otherwise, load from NVS */
static char key_buffer[LLM_API_KEY_MAX_LEN] = {0};
const char *nvs_key = get_provider_api_key_nvs_key(provider_name);
if (!nvs_key) return NULL;
nvs_handle_t nvs;
if (nvs_open(MIMI_NVS_LLM, NVS_READONLY, &nvs) == ESP_OK) {
size_t len = sizeof(key_buffer);
if (nvs_get_str(nvs, nvs_key, key_buffer, &len) == ESP_OK && key_buffer[0]) {
nvs_close(nvs);
return key_buffer;
}
nvs_close(nvs);
}
return NULL;
}
/* Get Base URL for a provider */
const char *llm_provider_get_base_url(const char *provider_name) {
if (!provider_name) return NULL;
/* If this is the current provider, return in-memory Base URL */
if (strcmp(provider_name, s_current_provider->name) == 0) {
return s_base_url[0] ? s_base_url : NULL;
}
/* Otherwise, load from NVS */
static char url_buffer[256] = {0};
const char *nvs_key = get_provider_base_url_nvs_key(provider_name);
if (!nvs_key) return NULL;
nvs_handle_t nvs;
if (nvs_open(MIMI_NVS_LLM, NVS_READONLY, &nvs) == ESP_OK) {
size_t len = sizeof(url_buffer);
if (nvs_get_str(nvs, nvs_key, url_buffer, &len) == ESP_OK && url_buffer[0]) {
nvs_close(nvs);
return url_buffer;
}
nvs_close(nvs);
}
return NULL;
}
/* Initialize provider system (load from NVS) */
void llm_provider_init(void) {
/* Load API key for current provider */
const char *api_key = llm_provider_get_api_key(s_current_provider->name);
if (api_key) {
strncpy(s_api_key, api_key, sizeof(s_api_key) - 1);
s_api_key[sizeof(s_api_key) - 1] = '\0';
} else {
s_api_key[0] = '\0';
}
/* Load Base URL for current provider */
const char *base_url = llm_provider_get_base_url(s_current_provider->name);
if (base_url) {
strncpy(s_base_url, base_url, sizeof(s_base_url) - 1);
s_base_url[sizeof(s_base_url) - 1] = '\0';
} else {
s_base_url[0] = '\0';
}
ESP_LOGI(TAG, "Provider initialized: %s (API key: %s, Base URL: %s)",
s_current_provider->name,
s_api_key[0] ? "set" : "not set",
s_base_url[0] ? s_base_url : "default");
}
/* Save provider configuration to NVS */
void llm_provider_save_config(const char *provider_name) {
/* This function is intentionally left empty -
individual set functions already save to NVS */
}
/* Common authentication header setup for OpenAI-compatible providers */
void llm_provider_set_auth_headers(esp_http_client_handle_t client, const char *api_key) {
if (!client || !api_key) return;
/* All OpenAI-compatible providers use Bearer token authentication */
char auth[LLM_API_KEY_MAX_LEN + 16];
snprintf(auth, sizeof(auth), "Bearer %s", api_key);
esp_http_client_set_header(client, "Authorization", auth);
}

68
main/llm/llm_provider.h Normal file
View File

@@ -0,0 +1,68 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Forward declaration for esp_http_client_handle_t */
typedef struct esp_http_client* esp_http_client_handle_t;
/* Provider configuration structure */
typedef struct {
const char *name; /* Provider name, e.g., "anthropic", "openai", "siliconflow", "volcengine" */
const char *default_api_url; /* Default API URL */
const char *default_host; /* Default hostname */
const char *default_path; /* Default API path */
bool is_openai_compatible; /* Whether this provider uses OpenAI-compatible API */
} llm_provider_config_t;
/* Provider registry - all supported providers */
extern const llm_provider_config_t llm_providers[];
extern const int llm_provider_count;
/* Find provider configuration by name */
const llm_provider_config_t *llm_provider_find(const char *name);
/* Get current provider configuration */
const llm_provider_config_t *llm_provider_current(void);
/* Set current provider by name */
void llm_provider_set_current(const char *name);
/* Get current provider name */
const char *llm_provider_current_name(void);
/* Check if current provider is OpenAI-compatible */
bool llm_provider_is_openai_compatible(void);
/* Get API URL for current provider (with dynamic Base URL support) */
const char *llm_provider_api_url(void);
/* Get hostname for current provider */
const char *llm_provider_host(void);
/* Get API path for current provider */
const char *llm_provider_path(void);
/* Provider-specific API key and Base URL management */
void llm_provider_set_api_key(const char *provider_name, const char *api_key);
void llm_provider_set_base_url(const char *provider_name, const char *base_url);
const char *llm_provider_get_api_key(const char *provider_name);
const char *llm_provider_get_base_url(const char *provider_name);
/* Initialize provider system (load from NVS) */
void llm_provider_init(void);
/* Save provider configuration to NVS */
void llm_provider_save_config(const char *provider_name);
/* Common authentication header setup for OpenAI-compatible providers */
void llm_provider_set_auth_headers(esp_http_client_handle_t client, const char *api_key);
#ifdef __cplusplus
}
#endif

View File

@@ -1,9 +1,11 @@
#include "llm_proxy.h"
#include "llm_provider.h"
#include "mimi_config.h"
#include "proxy/http_proxy.h"
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include "esp_log.h"
#include "esp_http_client.h"
#include "esp_crt_bundle.h"
@@ -184,28 +186,31 @@ static esp_err_t http_event_handler(esp_http_client_event_t *evt)
static bool provider_is_openai(void)
{
return strcmp(s_provider, "openai") == 0;
return llm_provider_is_openai_compatible();
}
static const char *llm_api_url(void)
{
return provider_is_openai() ? MIMI_OPENAI_API_URL : MIMI_LLM_API_URL;
return llm_provider_api_url();
}
static const char *llm_api_host(void)
{
return provider_is_openai() ? "api.openai.com" : "api.anthropic.com";
return llm_provider_host();
}
static const char *llm_api_path(void)
{
return provider_is_openai() ? "/v1/chat/completions" : "/v1/messages";
return llm_provider_path();
}
/* ── Init ─────────────────────────────────────────────────────── */
esp_err_t llm_proxy_init(void)
{
/* Initialize provider system */
llm_provider_init();
/* Start with build-time defaults */
if (MIMI_SECRET_API_KEY[0] != '\0') {
safe_copy(s_api_key, sizeof(s_api_key), MIMI_SECRET_API_KEY);
@@ -215,6 +220,8 @@ esp_err_t llm_proxy_init(void)
}
if (MIMI_SECRET_MODEL_PROVIDER[0] != '\0') {
safe_copy(s_provider, sizeof(s_provider), MIMI_SECRET_MODEL_PROVIDER);
/* Set current provider based on build-time default */
llm_provider_set_current(s_provider);
}
/* NVS overrides take highest priority (set via CLI) */
@@ -234,10 +241,31 @@ esp_err_t llm_proxy_init(void)
len = sizeof(provider_tmp);
if (nvs_get_str(nvs, MIMI_NVS_KEY_PROVIDER, provider_tmp, &len) == ESP_OK && provider_tmp[0]) {
safe_copy(s_provider, sizeof(s_provider), provider_tmp);
/* Set current provider based on NVS override */
llm_provider_set_current(s_provider);
}
nvs_close(nvs);
}
/* Load provider-specific API key from NVS */
const char *provider_api_key = llm_provider_get_api_key(s_provider);
if (provider_api_key && provider_api_key[0]) {
safe_copy(s_api_key, sizeof(s_api_key), provider_api_key);
}
/* Fall back to build-time provider-specific API key if NVS key is empty */
if (s_api_key[0] == '\0') {
if (strcmp(s_provider, "siliconflow") == 0 && MIMI_SECRET_SILICONFLOW_API_KEY[0] != '\0') {
safe_copy(s_api_key, sizeof(s_api_key), MIMI_SECRET_SILICONFLOW_API_KEY);
} else if (strcmp(s_provider, "volcengine") == 0 && MIMI_SECRET_VOLCENGINE_API_KEY[0] != '\0') {
safe_copy(s_api_key, sizeof(s_api_key), MIMI_SECRET_VOLCENGINE_API_KEY);
} else if (strcmp(s_provider, "openai") == 0 && MIMI_SECRET_OPENAI_API_KEY[0] != '\0') {
safe_copy(s_api_key, sizeof(s_api_key), MIMI_SECRET_OPENAI_API_KEY);
} else if (strcmp(s_provider, "anthropic") == 0 && MIMI_SECRET_ANTHROPIC_API_KEY[0] != '\0') {
safe_copy(s_api_key, sizeof(s_api_key), MIMI_SECRET_ANTHROPIC_API_KEY);
}
}
if (s_api_key[0]) {
ESP_LOGI(TAG, "LLM proxy initialized (provider: %s, model: %s)", s_provider, s_model);
} else {
@@ -265,13 +293,15 @@ static esp_err_t llm_http_direct(const char *post_data, resp_buf_t *rb, int *out
esp_http_client_set_method(client, HTTP_METHOD_POST);
esp_http_client_set_header(client, "Content-Type", "application/json");
if (provider_is_openai()) {
/* Use provider-specific authentication */
if (llm_provider_is_openai_compatible()) {
/* OpenAI-compatible providers use Bearer token authentication */
if (s_api_key[0]) {
char auth[LLM_API_KEY_MAX_LEN + 16];
snprintf(auth, sizeof(auth), "Bearer %s", s_api_key);
esp_http_client_set_header(client, "Authorization", auth);
llm_provider_set_auth_headers(client, s_api_key);
}
} else {
/* Anthropic uses x-api-key authentication */
esp_http_client_set_header(client, "x-api-key", s_api_key);
esp_http_client_set_header(client, "anthropic-version", MIMI_LLM_API_VERSION);
}
@@ -293,7 +323,9 @@ static esp_err_t llm_http_via_proxy(const char *post_data, resp_buf_t *rb, int *
int body_len = strlen(post_data);
char header[1024];
int hlen = 0;
if (provider_is_openai()) {
if (llm_provider_is_openai_compatible()) {
/* OpenAI-compatible providers use Bearer token authentication */
hlen = snprintf(header, sizeof(header),
"POST %s HTTP/1.1\r\n"
"Host: %s\r\n"
@@ -303,6 +335,7 @@ static esp_err_t llm_http_via_proxy(const char *post_data, resp_buf_t *rb, int *
"Connection: close\r\n\r\n",
llm_api_path(), llm_api_host(), s_api_key, body_len);
} else {
/* Anthropic uses x-api-key authentication */
hlen = snprintf(header, sizeof(header),
"POST %s HTTP/1.1\r\n"
"Host: %s\r\n"
@@ -559,13 +592,13 @@ esp_err_t llm_chat_tools(const char *system_prompt,
/* Build request body (non-streaming) */
cJSON *body = cJSON_CreateObject();
cJSON_AddStringToObject(body, "model", s_model);
if (provider_is_openai()) {
if (llm_provider_is_openai_compatible()) {
cJSON_AddNumberToObject(body, "max_completion_tokens", MIMI_LLM_MAX_TOKENS);
} else {
cJSON_AddNumberToObject(body, "max_tokens", MIMI_LLM_MAX_TOKENS);
}
if (provider_is_openai()) {
if (llm_provider_is_openai_compatible()) {
cJSON *openai_msgs = convert_messages_openai(system_prompt, messages);
cJSON_AddItemToObject(body, "messages", openai_msgs);
@@ -635,7 +668,7 @@ esp_err_t llm_chat_tools(const char *system_prompt,
return ESP_FAIL;
}
if (provider_is_openai()) {
if (llm_provider_is_openai_compatible()) {
cJSON *choices = cJSON_GetObjectItem(root, "choices");
cJSON *choice0 = choices && cJSON_IsArray(choices) ? cJSON_GetArrayItem(choices, 0) : NULL;
if (choice0) {
@@ -780,7 +813,11 @@ esp_err_t llm_set_api_key(const char *api_key)
nvs_close(nvs);
safe_copy(s_api_key, sizeof(s_api_key), api_key);
ESP_LOGI(TAG, "API key saved");
/* Also save to provider-specific NVS key */
llm_provider_set_api_key(s_provider, api_key);
ESP_LOGI(TAG, "API key saved for provider: %s", s_provider);
return ESP_OK;
}
@@ -806,6 +843,43 @@ esp_err_t llm_set_provider(const char *provider)
nvs_close(nvs);
safe_copy(s_provider, sizeof(s_provider), provider);
/* Update current provider in the provider system */
llm_provider_set_current(provider);
/* Load provider-specific API key if available */
const char *provider_api_key = llm_provider_get_api_key(provider);
if (provider_api_key && provider_api_key[0]) {
safe_copy(s_api_key, sizeof(s_api_key), provider_api_key);
} else {
s_api_key[0] = '\0';
}
ESP_LOGI(TAG, "Provider set to: %s", s_provider);
return ESP_OK;
}
esp_err_t llm_set_base_url(const char *provider, const char *base_url)
{
if (!provider || !base_url) return ESP_ERR_INVALID_ARG;
/* Save to provider-specific NVS key */
llm_provider_set_base_url(provider, base_url);
/* If this is the current provider, update in-memory Base URL */
if (strcmp(provider, s_provider) == 0) {
/* Reload provider configuration to pick up new Base URL */
llm_provider_init();
}
ESP_LOGI(TAG, "Base URL set for provider: %s", provider);
return ESP_OK;
}
const char *llm_get_base_url(const char *provider)
{
if (!provider) return NULL;
/* Get Base URL from provider system */
return llm_provider_get_base_url(provider);
}

View File

@@ -27,6 +27,16 @@ esp_err_t llm_set_provider(const char *provider);
*/
esp_err_t llm_set_model(const char *model);
/**
* Save the Base URL for a provider to NVS.
*/
esp_err_t llm_set_base_url(const char *provider, const char *base_url);
/**
* Get the Base URL for a provider from NVS.
*/
const char *llm_get_base_url(const char *provider);
/* ── Tool Use Support ──────────────────────────────────────────── */
typedef struct {

View File

@@ -24,7 +24,7 @@ esp_err_t session_mgr_init(void)
esp_err_t session_append(const char *chat_id, const char *role, const char *content)
{
char path[64];
char path[128];
session_path(chat_id, path, sizeof(path));
FILE *f = fopen(path, "a");
@@ -52,7 +52,7 @@ esp_err_t session_append(const char *chat_id, const char *role, const char *cont
esp_err_t session_get_history_json(const char *chat_id, char *buf, size_t size, int max_msgs)
{
char path[64];
char path[128];
session_path(chat_id, path, sizeof(path));
FILE *f = fopen(path, "r");
@@ -127,7 +127,7 @@ esp_err_t session_get_history_json(const char *chat_id, char *buf, size_t size,
esp_err_t session_clear(const char *chat_id)
{
char path[64];
char path[128];
session_path(chat_id, path, sizeof(path));
if (remove(path) == 0) {

View File

@@ -1,5 +1,6 @@
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"

View File

@@ -47,6 +47,26 @@
#define MIMI_SECRET_TAVILY_KEY ""
#endif
/* Provider-specific API keys (fallback when generic MIMI_SECRET_API_KEY is empty) */
#ifndef MIMI_SECRET_ANTHROPIC_API_KEY
#define MIMI_SECRET_ANTHROPIC_API_KEY ""
#endif
#ifndef MIMI_SECRET_OPENAI_API_KEY
#define MIMI_SECRET_OPENAI_API_KEY ""
#endif
#ifndef MIMI_SECRET_SILICONFLOW_API_KEY
#define MIMI_SECRET_SILICONFLOW_API_KEY ""
#endif
#ifndef MIMI_SECRET_SILICONFLOW_BASE_URL
#define MIMI_SECRET_SILICONFLOW_BASE_URL "https://api.siliconflow.cn/v1"
#endif
#ifndef MIMI_SECRET_VOLCENGINE_API_KEY
#define MIMI_SECRET_VOLCENGINE_API_KEY ""
#endif
#ifndef MIMI_SECRET_VOLCENGINE_BASE_URL
#define MIMI_SECRET_VOLCENGINE_BASE_URL "https://ark.cn-beijing.volces.com/api/v3"
#endif
/* WiFi */
#define MIMI_WIFI_MAX_RETRY 10
#define MIMI_WIFI_RETRY_BASE_MS 1000
@@ -80,7 +100,7 @@
#define MIMI_AGENT_SEND_WORKING_STATUS 1
/* Timezone (POSIX TZ format) */
#define MIMI_TIMEZONE "PST8PDT,M3.2.0,M11.1.0"
#define MIMI_TIMEZONE "CST-8"
/* LLM */
#define MIMI_LLM_DEFAULT_MODEL "claude-opus-4-5"
@@ -88,6 +108,8 @@
#define MIMI_LLM_MAX_TOKENS 4096
#define MIMI_LLM_API_URL "https://api.anthropic.com/v1/messages"
#define MIMI_OPENAI_API_URL "https://api.openai.com/v1/chat/completions"
#define MIMI_SILICONFLOW_API_URL "https://api.siliconflow.cn/v1/chat/completions"
#define MIMI_VOLCENGINE_API_URL "https://ark.cn-beijing.volces.com/api/v3/chat/completions"
#define MIMI_LLM_API_VERSION "2023-06-01"
#define MIMI_LLM_STREAM_BUF_SIZE (32 * 1024)
#define MIMI_LLM_LOG_VERBOSE_PAYLOAD 0
@@ -154,6 +176,19 @@
#define MIMI_NVS_KEY_PROXY_PORT "port"
#define MIMI_NVS_KEY_PROXY_TYPE "proxy_type"
/* Provider-specific NVS Keys */
#define MIMI_NVS_KEY_ANTHROPIC_API_KEY "anthropic_api_key"
#define MIMI_NVS_KEY_ANTHROPIC_BASE_URL "anthropic_base_url"
#define MIMI_NVS_KEY_OPENAI_API_KEY "openai_api_key"
#define MIMI_NVS_KEY_OPENAI_BASE_URL "openai_base_url"
#define MIMI_NVS_KEY_SILICONFLOW_API_KEY "siliconflow_api_key"
#define MIMI_NVS_KEY_SILICONFLOW_BASE_URL "siliconflow_base_url"
#define MIMI_NVS_KEY_VOLCENGINE_API_KEY "volcengine_api_key"
#define MIMI_NVS_KEY_VOLCENGINE_BASE_URL "volcengine_base_url"
/* System NVS Keys */
#define MIMI_NVS_KEY_TIMEZONE "timezone"
/* WiFi Onboarding (Captive Portal) */
#define MIMI_ONBOARD_AP_PREFIX "MimiClaw-"
#define MIMI_ONBOARD_AP_PASS "" /* open network */

View File

@@ -21,11 +21,35 @@
#define MIMI_SECRET_FEISHU_APP_ID ""
#define MIMI_SECRET_FEISHU_APP_SECRET ""
/* Anthropic API */
/* LLM Configuration
*
* Method 1: Generic API key (works for any provider)
* Set MIMI_SECRET_API_KEY + MIMI_SECRET_MODEL_PROVIDER + MIMI_SECRET_MODEL
*
* Method 2: Provider-specific API keys (recommended for multi-provider setups)
* Set MIMI_SECRET_<PROVIDER>_API_KEY for each provider you want to use
* Switch providers via onboard portal or CLI: set_model_provider <provider>
*
* Priority: NVS (runtime config) > provider-specific key > generic key
*/
#define MIMI_SECRET_API_KEY ""
#define MIMI_SECRET_MODEL ""
#define MIMI_SECRET_MODEL "claude-opus-4-5"
#define MIMI_SECRET_MODEL_PROVIDER "anthropic"
/* Anthropic (Claude) */
#define MIMI_SECRET_ANTHROPIC_API_KEY ""
/* OpenAI (GPT) */
#define MIMI_SECRET_OPENAI_API_KEY ""
/* SiliconFlow (硅基流动, OpenAI-compatible) */
#define MIMI_SECRET_SILICONFLOW_API_KEY ""
#define MIMI_SECRET_SILICONFLOW_BASE_URL "https://api.siliconflow.cn/v1"
/* Volcengine (火山引擎/豆包, OpenAI-compatible) */
#define MIMI_SECRET_VOLCENGINE_API_KEY ""
#define MIMI_SECRET_VOLCENGINE_BASE_URL "https://ark.cn-beijing.volces.com/api/v3"
/* HTTP Proxy (leave empty or set both) */
#define MIMI_SECRET_PROXY_HOST ""
#define MIMI_SECRET_PROXY_PORT ""

View File

@@ -56,10 +56,14 @@ static const char ONBOARD_HTML[] =
"<label>Model</label>"
"<input id='model' placeholder='claude-opus-4-5' value='claude-opus-4-5'>"
"<label>Provider</label>"
"<select id='provider'>"
"<select id='provider' onchange='onProviderChange()'>"
"<option value='anthropic'>Anthropic</option>"
"<option value='openai'>OpenAI</option>"
"<option value='siliconflow'>SiliconFlow (硅基流动)</option>"
"<option value='volcengine'>Volcengine (火山引擎)</option>"
"</select>"
"<label>Base URL</label>"
"<input id='base_url' placeholder='https://api.example.com/v1'>"
"</div></div>"
/* Telegram section */
@@ -133,8 +137,15 @@ static const char ONBOARD_HTML[] =
"btn.textContent='Scan WiFi Networks';btn.disabled=false;"
"}).catch(()=>{btn.textContent='Scan WiFi Networks';btn.disabled=false})}"
"function onProviderChange(){"
"var p=document.getElementById('provider').value;"
"var u=document.getElementById('base_url');"
"if(p==='siliconflow'){u.value='https://api.siliconflow.cn/v1'}"
"else if(p==='volcengine'){u.value='https://ark.cn-beijing.volces.com/api/v3'}"
"else{u.value=''}}"
"function save(){"
"var fields=['ssid','password','api_key','model','provider','tg_token',"
"var fields=['ssid','password','api_key','model','provider','base_url','tg_token',"
"'feishu_app_id','feishu_app_secret','proxy_host','proxy_port','proxy_type','search_key','tavily_key'];"
"var data={};"
"fields.forEach(f=>{data[f]=document.getElementById(f).value.trim()});"

View File

@@ -220,6 +220,40 @@ static esp_err_t http_get_config(httpd_req_t *req)
json_add_effective_config(root, "api_key", MIMI_NVS_LLM, MIMI_NVS_KEY_API_KEY, MIMI_SECRET_API_KEY);
json_add_effective_config(root, "model", MIMI_NVS_LLM, MIMI_NVS_KEY_MODEL, MIMI_SECRET_MODEL);
json_add_effective_config(root, "provider", MIMI_NVS_LLM, MIMI_NVS_KEY_PROVIDER, MIMI_SECRET_MODEL_PROVIDER);
/* Provider-specific Base URL (load from current provider's NVS key) */
{
char base_url[256] = {0};
bool found = false;
char provider[32] = {0};
nvs_handle_t nvs;
if (nvs_open(MIMI_NVS_LLM, NVS_READONLY, &nvs) == ESP_OK) {
size_t len = sizeof(provider);
if (nvs_get_str(nvs, MIMI_NVS_KEY_PROVIDER, provider, &len) != ESP_OK || !provider[0]) {
/* Fall back to build-time provider if NVS doesn't have one */
strlcpy(provider, MIMI_SECRET_MODEL_PROVIDER, sizeof(provider));
}
/* Try to load provider-specific base URL from NVS */
const char *url_key = NULL;
if (strcmp(provider, "anthropic") == 0) url_key = MIMI_NVS_KEY_ANTHROPIC_BASE_URL;
else if (strcmp(provider, "openai") == 0) url_key = MIMI_NVS_KEY_OPENAI_BASE_URL;
else if (strcmp(provider, "siliconflow") == 0) url_key = MIMI_NVS_KEY_SILICONFLOW_BASE_URL;
else if (strcmp(provider, "volcengine") == 0) url_key = MIMI_NVS_KEY_VOLCENGINE_BASE_URL;
if (url_key) {
len = sizeof(base_url);
if (nvs_get_str(nvs, url_key, base_url, &len) == ESP_OK && base_url[0]) {
found = true;
}
}
nvs_close(nvs);
}
if (!found) {
/* Fall back to build-time defaults */
if (strcmp(provider, "siliconflow") == 0) strlcpy(base_url, MIMI_SECRET_SILICONFLOW_BASE_URL, sizeof(base_url));
else if (strcmp(provider, "volcengine") == 0) strlcpy(base_url, MIMI_SECRET_VOLCENGINE_BASE_URL, sizeof(base_url));
}
cJSON_AddStringToObject(root, "base_url", base_url);
}
json_add_effective_config(root, "tg_token", MIMI_NVS_TG, MIMI_NVS_KEY_TG_TOKEN, MIMI_SECRET_TG_TOKEN);
json_add_effective_config(root, "feishu_app_id", MIMI_NVS_FEISHU, MIMI_NVS_KEY_FEISHU_APP_ID, MIMI_SECRET_FEISHU_APP_ID);
json_add_effective_config(root, "feishu_app_secret", MIMI_NVS_FEISHU, MIMI_NVS_KEY_FEISHU_APP_SECRET, MIMI_SECRET_FEISHU_APP_SECRET);
@@ -344,6 +378,37 @@ static esp_err_t http_post_save(httpd_req_t *req)
nvs_sync_field(root, "model", MIMI_NVS_LLM, MIMI_NVS_KEY_MODEL);
nvs_sync_field(root, "provider", MIMI_NVS_LLM, MIMI_NVS_KEY_PROVIDER);
/* Save API key and base URL to provider-specific NVS keys */
{
cJSON *provider_item = cJSON_GetObjectItem(root, "provider");
cJSON *api_key_item = cJSON_GetObjectItem(root, "api_key");
cJSON *base_url_item = cJSON_GetObjectItem(root, "base_url");
if (provider_item && cJSON_IsString(provider_item) && provider_item->valuestring[0]) {
const char *provider = provider_item->valuestring;
const char *api_key_nvs = NULL;
const char *base_url_nvs = NULL;
if (strcmp(provider, "anthropic") == 0) {
api_key_nvs = MIMI_NVS_KEY_ANTHROPIC_API_KEY;
base_url_nvs = MIMI_NVS_KEY_ANTHROPIC_BASE_URL;
} else if (strcmp(provider, "openai") == 0) {
api_key_nvs = MIMI_NVS_KEY_OPENAI_API_KEY;
base_url_nvs = MIMI_NVS_KEY_OPENAI_BASE_URL;
} else if (strcmp(provider, "siliconflow") == 0) {
api_key_nvs = MIMI_NVS_KEY_SILICONFLOW_API_KEY;
base_url_nvs = MIMI_NVS_KEY_SILICONFLOW_BASE_URL;
} else if (strcmp(provider, "volcengine") == 0) {
api_key_nvs = MIMI_NVS_KEY_VOLCENGINE_API_KEY;
base_url_nvs = MIMI_NVS_KEY_VOLCENGINE_BASE_URL;
}
if (api_key_nvs && api_key_item && cJSON_IsString(api_key_item)) {
nvs_sync_field(root, "api_key", MIMI_NVS_LLM, api_key_nvs);
}
if (base_url_nvs && base_url_item && cJSON_IsString(base_url_item)) {
nvs_sync_field(root, "base_url", MIMI_NVS_LLM, base_url_nvs);
}
}
}
/* Telegram */
nvs_sync_field(root, "tg_token", MIMI_NVS_TG, MIMI_NVS_KEY_TG_TOKEN);

View File

@@ -5,6 +5,7 @@
#include "esp_http_client.h"
#include "esp_crt_bundle.h"
#include "esp_https_ota.h"
#include "esp_system.h"
static const char *TAG = "ota";

View File

@@ -3,8 +3,10 @@
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netdb.h>
#include <unistd.h>
@@ -245,7 +247,7 @@ static int open_socks5_tunnel(const char *host, int port, int timeout_ms)
free(req);
/* Receive connect response */
unsigned char resp[256];
unsigned char resp[512];
int resp_len = recv(sock, resp, sizeof(resp), 0);
if (resp_len < 10) {
ESP_LOGE(TAG, "No response from SOCKS5 proxy"); close(sock); return -1;

View File

@@ -4,6 +4,7 @@
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#ifndef GPIO_IS_VALID_GPIO

View File

@@ -4,6 +4,7 @@
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
#include <sys/time.h>
#include "esp_log.h"

View File

@@ -2,11 +2,13 @@
#include "mimi_config.h"
#include "tools/tool_web_search.h"
#include "tools/tool_get_time.h"
#include "tools/tool_set_timezone.h"
#include "tools/tool_files.h"
#include "tools/tool_cron.h"
#include "tools/tool_gpio.h"
#include <string.h>
#include <stdlib.h>
#include "esp_log.h"
#include "cJSON.h"
@@ -82,6 +84,18 @@ esp_err_t tool_registry_init(void)
};
register_tool(&gt);
/* Register set_timezone */
mimi_tool_t stz = {
.name = "set_timezone",
.description = "Set the system timezone. Accepts POSIX format (e.g. CST-8, EST5EDT,M3.2.0,M11.1.0) or city name (e.g. Asia/Shanghai, America/New_York).",
.input_schema_json =
"{\"type\":\"object\","
"\"properties\":{\"timezone\":{\"type\":\"string\",\"description\":\"Timezone in POSIX format or city name (e.g. CST-8, Asia/Shanghai)\"}},"
"\"required\":[\"timezone\"]}",
.execute = tool_set_timezone_execute,
};
register_tool(&stz);
/* Register read_file */
mimi_tool_t rf = {
.name = "read_file",

View File

@@ -0,0 +1,145 @@
#include "tool_set_timezone.h"
#include "mimi_config.h"
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
#include "esp_log.h"
#include "nvs.h"
#include "cJSON.h"
static const char *TAG = "tool_timezone";
/* Common timezone mappings for user-friendly names */
typedef struct {
const char *name;
const char *posix_tz;
} tz_mapping_t;
static const tz_mapping_t tz_mappings[] = {
{ "Asia/Shanghai", "CST-8" },
{ "Asia/Beijing", "CST-8" },
{ "Asia/Hong_Kong", "HKT-8" },
{ "Asia/Tokyo", "JST-9" },
{ "Asia/Seoul", "KST-9" },
{ "Asia/Singapore", "SGT-8" },
{ "Asia/Kolkata", "IST-5:30" },
{ "Asia/Dubai", "GST-4" },
{ "Europe/London", "GMT0BST,M3.5.0/1,M10.5.0" },
{ "Europe/Paris", "CET-1CEST,M3.5.0,M10.5.0/3" },
{ "Europe/Berlin", "CET-1CEST,M3.5.0,M10.5.0/3" },
{ "America/New_York", "EST5EDT,M3.2.0,M11.1.0" },
{ "America/Chicago", "CST6CDT,M3.2.0,M11.1.0" },
{ "America/Denver", "MST7MDT,M3.2.0,M11.1.0" },
{ "America/Los_Angeles", "PST8PDT,M3.2.0,M11.1.0" },
{ "Australia/Sydney", "AEST-10AEDT,M10.1.0,M4.1.0/3" },
{ "UTC", "UTC0" },
{ "GMT", "GMT0" },
};
static const char *resolve_timezone(const char *tz_str)
{
if (!tz_str || !tz_str[0]) return NULL;
for (size_t i = 0; i < sizeof(tz_mappings) / sizeof(tz_mappings[0]); i++) {
if (strcmp(tz_str, tz_mappings[i].name) == 0) {
return tz_mappings[i].posix_tz;
}
}
return tz_str;
}
static bool validate_timezone(const char *tz_str)
{
if (!tz_str || !tz_str[0]) return false;
if (strlen(tz_str) > 64) return false;
for (size_t i = 0; i < sizeof(tz_mappings) / sizeof(tz_mappings[0]); i++) {
if (strcmp(tz_str, tz_mappings[i].name) == 0) return true;
}
if (strchr(tz_str, '+') || strchr(tz_str, '-') ||
strcmp(tz_str, "UTC0") == 0 || strcmp(tz_str, "GMT0") == 0) {
return true;
}
return false;
}
static esp_err_t save_timezone_nvs(const char *tz_str)
{
nvs_handle_t nvs;
esp_err_t err = nvs_open("system_config", NVS_READWRITE, &nvs);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to open NVS: %s", esp_err_to_name(err));
return err;
}
err = nvs_set_str(nvs, MIMI_NVS_KEY_TIMEZONE, tz_str);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to save timezone: %s", esp_err_to_name(err));
nvs_close(nvs);
return err;
}
err = nvs_commit(nvs);
nvs_close(nvs);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to commit NVS: %s", esp_err_to_name(err));
return err;
}
return ESP_OK;
}
esp_err_t tool_set_timezone_execute(const char *input_json, char *output, size_t output_size)
{
ESP_LOGI(TAG, "Setting timezone...");
cJSON *root = cJSON_Parse(input_json);
if (!root) {
snprintf(output, output_size, "Error: invalid JSON input");
return ESP_ERR_INVALID_ARG;
}
cJSON *tz_item = cJSON_GetObjectItem(root, "timezone");
if (!tz_item || !cJSON_IsString(tz_item)) {
cJSON_Delete(root);
snprintf(output, output_size, "Error: 'timezone' field required (string)");
return ESP_ERR_INVALID_ARG;
}
const char *input_tz = tz_item->valuestring;
const char *resolved_tz = resolve_timezone(input_tz);
if (!resolved_tz || !validate_timezone(resolved_tz)) {
cJSON_Delete(root);
snprintf(output, output_size, "Error: invalid timezone format '%s'. Use POSIX format (e.g. CST-8) or city name (e.g. Asia/Shanghai)", input_tz);
return ESP_ERR_INVALID_ARG;
}
esp_err_t err = save_timezone_nvs(resolved_tz);
if (err != ESP_OK) {
cJSON_Delete(root);
snprintf(output, output_size, "Error: failed to save timezone (%s)", esp_err_to_name(err));
return err;
}
setenv("TZ", resolved_tz, 1);
tzset();
time_t now = time(NULL);
struct tm tm_now;
localtime_r(&now, &tm_now);
char time_str[64];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S %Z", &tm_now);
cJSON_Delete(root);
snprintf(output, output_size, "Timezone set to '%s'. Current time: %s", resolved_tz, time_str);
ESP_LOGI(TAG, "Timezone set to: %s, current time: %s", resolved_tz, time_str);
return ESP_OK;
}

View File

@@ -0,0 +1,11 @@
#pragma once
#include "esp_err.h"
#include <stddef.h>
/**
* Execute set_timezone tool.
* Sets the system timezone via NVS and updates the TZ environment variable.
* Input JSON: {"timezone": "CST-8"} or {"timezone": "Asia/Shanghai"}
*/
esp_err_t tool_set_timezone_execute(const char *input_json, char *output, size_t output_size);

View File

@@ -2,12 +2,18 @@
#include "mimi_config.h"
#include <string.h>
#include <stdbool.h>
#include <inttypes.h>
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_netif.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_event.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "freertos/timers.h"
static const char *TAG = "wifi";
@@ -16,20 +22,49 @@ static int s_retry_count = 0;
static char s_ip_str[16] = "0.0.0.0";
static bool s_connected = false;
static bool s_reconnect_enabled = true;
static TimerHandle_t s_retry_timer = NULL;
static void retry_timer_callback(TimerHandle_t xTimer)
{
(void)xTimer;
if (s_reconnect_enabled && !s_connected) {
esp_wifi_connect();
}
}
static const char *wifi_reason_to_str(wifi_err_reason_t reason)
{
switch (reason) {
#ifdef WIFI_REASON_AUTH_EXPIRE
case WIFI_REASON_AUTH_EXPIRE: return "AUTH_EXPIRE";
#endif
#ifdef WIFI_REASON_AUTH_FAIL
case WIFI_REASON_AUTH_FAIL: return "AUTH_FAIL";
#endif
#ifdef WIFI_REASON_ASSOC_EXPIRE
case WIFI_REASON_ASSOC_EXPIRE: return "ASSOC_EXPIRE";
#endif
#ifdef WIFI_REASON_ASSOC_FAIL
case WIFI_REASON_ASSOC_FAIL: return "ASSOC_FAIL";
#endif
#ifdef WIFI_REASON_HANDSHAKE_TIMEOUT
case WIFI_REASON_HANDSHAKE_TIMEOUT: return "HANDSHAKE_TIMEOUT";
#endif
#ifdef WIFI_REASON_NO_AP_FOUND
case WIFI_REASON_NO_AP_FOUND: return "NO_AP_FOUND";
#endif
#ifdef WIFI_REASON_BEACON_TIMEOUT
case WIFI_REASON_BEACON_TIMEOUT: return "BEACON_TIMEOUT";
#endif
#ifdef WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT
case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT: return "4WAY_HANDSHAKE_TIMEOUT";
#endif
#ifdef WIFI_REASON_MIC_FAILURE
case WIFI_REASON_MIC_FAILURE: return "MIC_FAILURE";
#endif
#ifdef WIFI_REASON_CONNECTION_FAIL
case WIFI_REASON_CONNECTION_FAIL: return "CONNECTION_FAIL";
#endif
default: return "UNKNOWN";
}
}
@@ -53,9 +88,10 @@ static void event_handler(void *arg, esp_event_base_t event_base,
}
ESP_LOGW(TAG, "Disconnected, retry %d/%d in %" PRIu32 "ms",
s_retry_count + 1, MIMI_WIFI_MAX_RETRY, delay_ms);
vTaskDelay(pdMS_TO_TICKS(delay_ms));
esp_wifi_connect();
s_retry_count++;
/* Use timer instead of blocking vTaskDelay in event handler */
if (s_retry_timer) xTimerStop(s_retry_timer, 0);
xTimerChangePeriod(s_retry_timer, pdMS_TO_TICKS(delay_ms), 0);
} else {
ESP_LOGE(TAG, "Failed to connect after %d retries", MIMI_WIFI_MAX_RETRY);
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
@@ -74,6 +110,7 @@ static void event_handler(void *arg, esp_event_base_t event_base,
esp_err_t wifi_manager_init(void)
{
s_wifi_event_group = xEventGroupCreate();
s_retry_timer = xTimerCreate("wifi_retry", pdMS_TO_TICKS(1000), pdFALSE, NULL, retry_timer_callback);
ESP_ERROR_CHECK(esp_netif_init());
esp_netif_create_default_wifi_sta();

213
taolun.md
View File

@@ -1,145 +1,126 @@
# 讨论记录:增加国内大模型厂商接入
# 讨论记录
## 对话概述
---
## 讨论ESP-IDF v6.0 编译适配
**日期**2026-03-31
**目标**为 MimiClaw 项目增加国内大模型厂商的接入,特别是硅基流动和火山方舟(豆包模型)
**目标**解决 ESP-IDF v6.0 编译失败问题,完成固件烧录
## 对话内容
### 问题清单
### 用户需求
用户希望给当前项目增加国内大模型厂商的接入,包括:
1. 硅基流动的免费模型
2. 火山方舟的豆包模型
#### 1. Flash 大小配置错误
- **错误**:分区表需要 16MB但 sdkconfig 配置为 2MB
- **修复**`sdkconfig``CONFIG_ESPTOOLPY_FLASHSIZE` 改为 16MB
### 项目现状分析
#### 2. WiFi 断开原因码未定义
- **错误**`WIFI_REASON_ASSOC_EXPIRE` 等符号在 v6.0 中未定义
- **修复**`wifi_manager.c` 中所有 reason code 添加 `#ifdef` 保护
#### 项目基本信息
- **项目名称**MimiClaw
- **运行平台**ESP32-S3纯C语言
- **交互方式**Telegram机器人
- **功能**AI助手支持工具调用本地记忆
#### 3. CMakeLists.txt 缺少源文件
- **错误**`llm_provider.c` 未加入编译列表,导致链接错误
- **修复**:添加 `ota/ota_manager.c` 到 SRCS
#### 当前LLM集成状态
1. **支持的提供商**
- Anthropic (Claude)
- OpenAI (GPT)
2. **配置方式**
- 构建时配置:`main/mimi_secrets.h`
- 运行时配置串口CLI命令存储在NVS中
#### 4. 头文件缺失(共 16 处)
| 文件 | 缺失头文件 | 原因 |
|------|-----------|------|
| `cli/serial_cli.c` | `llm/llm_provider.h` | `llm_provider_set_api_key` |
| `llm/llm_provider.c` | `esp_http_client.h` | `esp_http_client_set_header` |
| `bus/message_bus.c` | `freertos/FreeRTOS.h`, `freertos/queue.h` | `xQueueCreate`, `QueueHandle_t` |
| `wifi/wifi_manager.c` | `esp_event.h` | `esp_event_handler_instance_register` |
| `wifi/wifi_manager.c` | `freertos/FreeRTOS.h`, `freertos/task.h`, `freertos/event_groups.h` | `xEventGroupCreate`, `vTaskDelay` |
| `ota/ota_manager.c` | `esp_system.h` | `esp_restart` |
| `channels/telegram/telegram_bot.c` | `freertos/FreeRTOS.h`, `freertos/task.h` | `xTaskCreatePinnedToCore`, `vTaskDelay` |
| `tools/tool_registry.c` | `<stdlib.h>` | `free()` |
| `proxy/http_proxy.c` | `<sys/time.h>` | `struct timeval` |
| `gateway/ws_server.c` | `<stdint.h>` | `uint8_t` |
3. **关键代码文件**
- `main/llm/llm_proxy.c`LLM代理核心实现
- `main/llm/llm_proxy.h`LLM代理头文件
- `main/mimi_config.h`:全局配置定义
- `main/cli/serial_cli.c`:命令行界面
### ESP-IDF v6.0 API 兼容性验证
4. **提供商检测机制**
- `provider_is_openai()`函数检查是否为OpenAI提供商
- 根据提供商选择不同的API URL、Host和Path
以下 API 在 v6.0 中**仍然存在**,无需修改
- `esp_spiffs_info()`
- `esp_websocket_client_send_bin()`
- `esp_tls_set_conn_sockfd()` / `esp_tls_set_conn_state()`
- `esp_console_new_repl_uart()` / `esp_console_new_repl_usb_serial_jtag()`
- `esp_https_ota()` + `esp_https_ota_config_t`
- `esp_wifi_set_config()`
### 国内大模型厂商API兼容性调研
### 烧录说明
#### 硅基流动 (SiliconFlow)
- **API兼容性**OpenAI兼容模式
- **Base URL**`https://api.siliconflow.cn/v1`
- **特点**
- 提供100+高性能大模型
- 新用户注册送免费额度
- 价格比OpenAI官方便宜80%+
- 支持OpenClaw等工具集成
ESP32-S3 使用 **USB 口**(内置 USB Serial/JTAG 控制器)烧录:
```powershell
idf.py -p COMx flash monitor
```
- 插 USB 口(标记为 `USB`),不是 UART 口
- 如遇连接失败,按住 BOOT 键再插线进入下载模式
#### 火山方舟 (字节跳动豆包模型)
- **API兼容性**兼容OpenAI SDK
- **Base URL**`https://ark.cn-beijing.volces.com/api/v3`
- **特点**
- 豆包大模型系列
- 新用户首次开通可享受50万token免费试用
- 支持函数调用、工具调用等高级功能
---
### 技术实现分析
## 讨论:增加国内大模型厂商接入
#### 当前架构特点
1. **提供商抽象**
- 使用`s_provider`变量存储提供商名称
- 通过`provider_is_openai()`函数区分提供商
- 根据提供商选择不同的API配置
**日期**2026-03-31
**目标**:为 MimiClaw 增加硅基流动和火山方舟(豆包模型)接入
2. **API调用流程**
- 构建请求体
- 设置请求头(根据提供商不同)
- 发送HTTP请求
- 解析响应(根据提供商不同)
### 项目现状
- 当前支持Anthropic (Claude)、OpenAI (GPT)
- 运行平台ESP32-S3纯 C 语言
- 交互方式Telegram 机器人
3. **工具调用支持**
- 支持Anthropic的tool_use格式
- 支持OpenAI的function calling格式
- 有格式转换函数`convert_tools_openai()`
### 国内厂商 API 兼容性
- **硅基流动**OpenAI 兼容Base URL `https://api.siliconflow.cn/v1`
- **火山方舟**OpenAI 兼容Base URL `https://ark.cn-beijing.volces.com/api/v3`
#### 实现方案讨论
由于硅基流动和火山方舟都提供OpenAI兼容API理论上可以复用现有OpenAI集成代码只需
1. 修改Base URL
2. 可能需要调整认证方式
3. 可能需要处理特定的模型名称
### 实现方案
由于两者都提供 OpenAI 兼容 API复用现有 OpenAI 集成代码,只需:
1. 修改 Base URL
2. 调整认证方式Bearer Token
3. 处理模型名称规范
## 待解决问题
### 待解决问题
1. 认证方式差异确认
2. 模型名称规范
3. 工具调用格式兼容性验证
1. **认证方式差异**
- 硅基流动使用API Key
- 火山方舟:可能使用不同的认证方式
---
2. **模型名称规范**
- 需要了解具体的模型ID格式
- 例如:硅基流动的`deepseek-ai/DeepSeek-V3`,火山方舟的豆包模型名称
## 讨论:时区设置功能
3. **功能支持差异**
- 工具调用格式是否完全兼容
- 上下文长度限制
- 特殊功能支持情况
**日期**2026-04-01
**目标**:为 MimiClaw 添加可配置的时区支持,默认改为中国时区
## 下一步计划
### 背景
- 原默认时区为 `PST8PDT,M3.2.0,M11.1.0`(太平洋时间)
- 需要支持用户自定义时区特别是中国用户UTC+8
- 交互方式从 Telegram 改为飞书
基于讨论,制定了以下实施计划:
### 实现方案
### 阶段一:准备与设计
1. 详细调研硅基流动和火山方舟的API文档
2. 确定具体的实现方案
3. 设计配置结构和命令行接口
#### 存储方式
- **NVS 存储**:使用 `system_config` namespacekey 为 `timezone`
- **Build-time 默认值**`MIMI_TIMEZONE` 改为 `"CST-8"`
- **优先级**NVS 值 > Build-time 值
### 阶段二:核心实现
1. 修改LLM代理以支持新的提供商
2. 添加配置管理功能
3. 更新命令行界面
#### CLI 命令
```
set_timezone <TZ> # 例如: set_timezone CST-8 或 set_timezone Asia/Shanghai
timezone_show # 显示当前时区配置和本地时间
```
### 阶段三:测试与优化
1. 功能测试
2. 性能优化
3. 文档更新
#### LLM 工具
- 新增 `set_timezone` 工具LLM 可通过对话设置时区
- 支持 POSIX 格式(`CST-8`)和城市名(`Asia/Shanghai`
- 内置 18 个城市名映射表
## 相关资源
### 改动文件
| 文件 | 操作 |
|------|------|
| `main/mimi_config.h` | 默认时区改为 `CST-8`,添加 `MIMI_NVS_KEY_TIMEZONE` |
| `main/tools/tool_set_timezone.h` | **新建** |
| `main/tools/tool_set_timezone.c` | **新建** |
| `main/tools/tool_registry.c` | include 新头文件 + 注册工具 |
| `main/cli/serial_cli.c` | 添加 `set_timezone` / `timezone_show` 命令 |
| `main/CMakeLists.txt` | 添加 `tool_set_timezone.c` 到 SRCS |
### 项目文件
- `main/llm/llm_proxy.c`LLM代理实现
- `main/llm/llm_proxy.h`LLM代理头文件
- `main/mimi_config.h`:配置定义
- `main/cli/serial_cli.c`:命令行界面
### 外部文档
- 硅基流动OpenClaw集成文档
- 火山方舟兼容OpenAI SDK文档
- ESP32-S3开发文档
## 技术要点总结
1. **复用现有架构**可以充分利用现有的OpenAI集成代码
2. **提供商扩展**:需要扩展提供商检测和配置机制
3. **配置管理**需要支持新的API密钥和Base URL配置
4. **兼容性处理**可能需要处理API响应格式的细微差异
## 风险与挑战
1. **API兼容性风险**:虽然声称兼容,但可能存在细微差异
2. **内存限制**ESP32-S3内存有限需要确保新功能不会导致内存不足
3. **网络稳定性**国内网络环境可能影响API调用稳定性
4. **认证安全性**需要确保API密钥的安全存储和传输
### 支持的时区格式
- POSIX: `CST-8`, `JST-9`, `EST5EDT,M3.2.0,M11.1.0`, `UTC0`
- 城市名: Asia/Shanghai, Asia/Tokyo, America/New_York 等 18 个预设