Compare commits
12 Commits
46e46b0a13
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c1368962cc | |||
| fa41de0ae8 | |||
| 6983a1f1ba | |||
| d5e70dfc8b | |||
| c260265841 | |||
| 570c14184e | |||
| 9815ab8df0 | |||
| 3912eda8c1 | |||
| 540bfe825f | |||
| 7dc4122778 | |||
| eedc6757d8 | |||
| 49d3a131b7 |
93
AGENTS.md
93
AGENTS.md
@@ -2,7 +2,7 @@
|
||||
|
||||
## 项目概述
|
||||
|
||||
MimiClaw 是一个运行在 ESP32-S3 上的 AI 助手,使用纯 C 语言编写。用户通过 Telegram 与之交互,设备连接 WiFi 后,将消息传递给 LLM(大语言模型)进行处理,并支持工具调用。
|
||||
MimiClaw 是一个运行在 ESP32-S3 上的 AI 助手,使用纯 C 语言编写。用户通过 Telegram 或飞书与设备交互,设备连接 WiFi 后,将消息传递给 LLM(大语言模型)进行处理,并支持工具调用。
|
||||
|
||||
## 项目结构
|
||||
|
||||
@@ -14,20 +14,21 @@ mimiclaw/
|
||||
│ │ └── context_builder.c # 构建上下文(系统提示、记忆等)
|
||||
│ ├── llm/ # LLM 代理
|
||||
│ │ ├── llm_proxy.c # 处理与 LLM API 的通信
|
||||
│ │ └── llm_proxy.h # LLM 代理的头文件
|
||||
│ │ └── llm_provider.c # 多提供商支持(Anthropic/OpenAI/硅基流动/火山引擎)
|
||||
│ ├── cli/ # 串口命令行界面
|
||||
│ │ └── serial_cli.c # 处理运行时配置命令
|
||||
│ ├── channels/ # 输入/输出通道
|
||||
│ │ ├── telegram/ # Telegram 机器人集成
|
||||
│ │ └── feishu/ # 飞书机器人集成
|
||||
│ │ ├── telegram/ # Telegram 机器人集成(可选)
|
||||
│ │ └── feishu/ # 飞书机器人集成(可选)
|
||||
│ ├── tools/ # LLM 可调用的工具
|
||||
│ ├── memory/ # 记忆和会话管理
|
||||
│ ├── proxy/ # HTTP 代理支持
|
||||
│ ├── proxy/ # HTTP/SOCKS5 代理支持
|
||||
│ ├── cron/ # 定时任务调度
|
||||
│ ├── heartbeat/ # 心跳服务
|
||||
│ ├── gateway/ # WebSocket 网关
|
||||
│ ├── onboard/ # WiFi 配置门户
|
||||
│ ├── onboard/ # WiFi 配置门户(Captive Portal)
|
||||
│ ├── skills/ # 技能加载器
|
||||
│ ├── bus/ # 消息总线(连接通道和代理循环)
|
||||
│ ├── mimi_config.h # 全局配置定义
|
||||
│ ├── mimi_secrets.h # 构建时密钥(需用户创建)
|
||||
│ └── mimi_secrets.h.example # 密钥模板
|
||||
@@ -72,6 +73,71 @@ mimiclaw/
|
||||
- 工具注册在 `tool_registry.c`
|
||||
- 支持的工具:`web_search`、`get_current_time`、`cron_add/list/remove`
|
||||
|
||||
## 模块配置系统
|
||||
|
||||
MimiClaw 支持**编译时模块开关**,可以禁用不需要的模块以减少固件体积和内存占用。
|
||||
|
||||
### 可配置模块
|
||||
|
||||
| 模块 | 配置项 | 默认 | 说明 |
|
||||
|------|--------|------|------|
|
||||
| Telegram | `CONFIG_MIMI_CHAN_TELEGRAM` | n | Telegram 机器人集成 |
|
||||
| 飞书 | `CONFIG_MIMI_CHAN_FEISHU` | y | 飞书机器人集成 |
|
||||
| WebSocket | `CONFIG_MIMI_WS_SERVER` | y | WebSocket 网关 |
|
||||
| Web Search | `CONFIG_MIMI_TOOL_WEB_SEARCH` | y | 网络搜索工具 |
|
||||
| GPIO | `CONFIG_MIMI_TOOL_GPIO` | n | GPIO 控制工具 |
|
||||
| WiFi 配置页 | `CONFIG_MIMI_WIFI_ONBOARD` | y | Captive Portal |
|
||||
| OTA | `CONFIG_MIMI_OTA` | n | OTA 升级(未实现) |
|
||||
|
||||
### 修改模块配置
|
||||
|
||||
编辑 `sdkconfig.defaults` 文件,添加或修改配置项:
|
||||
|
||||
```
|
||||
CONFIG_MIMI_CHAN_TELEGRAM=n
|
||||
CONFIG_MIMI_CHAN_FEISHU=y
|
||||
CONFIG_MIMI_TOOL_WEB_SEARCH=y
|
||||
CONFIG_MIMI_TOOL_GPIO=n
|
||||
```
|
||||
|
||||
修改后必须重新编译:
|
||||
|
||||
```bash
|
||||
idf.py fullclean && idf.py build
|
||||
```
|
||||
|
||||
### 添加新模块的编译时开关
|
||||
|
||||
1. **创建 `main/Kconfig.projbuild`**(关键!没有它配置项不会被识别):
|
||||
```kconfig
|
||||
config MIMI_MODULE_EXAMPLE
|
||||
bool "Example module"
|
||||
default n
|
||||
help
|
||||
Enable example module for MimiClaw.
|
||||
```
|
||||
|
||||
2. 在 `sdkconfig.defaults` 中添加配置项:
|
||||
```
|
||||
CONFIG_MIMI_MODULE_EXAMPLE=y
|
||||
```
|
||||
|
||||
3. 在 `main/CMakeLists.txt` 中条件编译:
|
||||
```cmake
|
||||
if(CONFIG_MIMI_MODULE_EXAMPLE)
|
||||
list(APPEND srcs "modules/example/example.c")
|
||||
endif()
|
||||
```
|
||||
|
||||
4. 在模块头文件中添加 stub:
|
||||
```c
|
||||
#ifdef CONFIG_MIMI_MODULE_EXAMPLE
|
||||
esp_err_t example_init(void);
|
||||
#else
|
||||
static inline esp_err_t example_init(void) { return ESP_OK; }
|
||||
#endif
|
||||
```
|
||||
|
||||
## 构建和烧录
|
||||
|
||||
### 前提条件
|
||||
@@ -85,7 +151,9 @@ idf.py set-target esp32s3
|
||||
|
||||
# 配置(首次需要)
|
||||
cp main/mimi_secrets.h.example main/mimi_secrets.h
|
||||
# 编辑 mimi_secrets.h 填写 WiFi、Telegram、LLM 密钥
|
||||
# 编辑 mimi_secrets.h 填写 WiFi、LLM 密钥
|
||||
|
||||
# 编辑 sdkconfig.defaults 启用/禁用模块(可选)
|
||||
|
||||
# 清理构建(修改配置后必须执行)
|
||||
idf.py fullclean && idf.py build
|
||||
@@ -108,6 +176,16 @@ config_show # 显示当前配置(脱敏)
|
||||
config_reset # 重置为构建时配置
|
||||
```
|
||||
|
||||
## 认知修正
|
||||
|
||||
| 日期 | 问题 | 根因 | 修复 | 详情 |
|
||||
|------|------|------|------|------|
|
||||
| 2026-04-04 | 模块开关失效(飞书命令消失、HTTP 配置页不可用) | 缺少 `Kconfig.projbuild`,ESP-IDF 不识别自定义配置项 | 创建 `main/Kconfig.projbuild` 声明所有模块开关 | [详细讨论](taolun.md#讨论kconfig-缺失导致模块开关失效) |
|
||||
|
||||
---
|
||||
|
||||
## 开发注意事项
|
||||
|
||||
## 开发注意事项
|
||||
|
||||
1. **内存限制**:ESP32-S3 内存有限,使用 `MALLOC_CAP_SPIRAM` 分配大内存
|
||||
@@ -115,6 +193,7 @@ config_reset # 重置为构建时配置
|
||||
3. **日志**:使用 `ESP_LOG` 宏,标签在每个文件中定义
|
||||
4. **错误处理**:使用 `esp_err_t` 返回码
|
||||
5. **JSON 处理**:使用 cJSON 库
|
||||
6. **模块开关**:新增模块或可选模块应在 `sdkconfig.defaults` 中添加配置项,并在头文件中添加 `#ifdef` stub
|
||||
|
||||
## 调试技巧
|
||||
|
||||
|
||||
24
README.md
24
README.md
@@ -31,7 +31,7 @@ MimiClaw turns a tiny ESP32-S3 board into a personal AI assistant. Plug it into
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||
299
changelog.md
299
changelog.md
@@ -1,229 +1,96 @@
|
||||
# 变更日志:增加国内大模型厂商接入
|
||||
# 变更日志
|
||||
|
||||
## 版本信息
|
||||
- **版本**:v1.1.0(计划)
|
||||
- **日期**:2026-03-31
|
||||
- **状态**:计划中
|
||||
## v1.2.0(计划中)
|
||||
|
||||
## 功能概述
|
||||
为 MimiClaw 项目增加国内大模型厂商的接入支持,包括:
|
||||
1. **硅基流动** (SiliconFlow) - 提供免费模型和多种高性能大模型
|
||||
2. **火山方舟** (Volcengine Ark) - 字节跳动豆包模型系列
|
||||
### 新增
|
||||
|
||||
## 实施计划
|
||||
- **编译时模块开关**
|
||||
- 通过 `sdkconfig.defaults` 配置选择启用/禁用模块
|
||||
- 支持的模块开关:
|
||||
- `CONFIG_MIMI_CHAN_TELEGRAM` — Telegram 机器人(默认禁用)
|
||||
- `CONFIG_MIMI_CHAN_FEISHU` — 飞书机器人(默认启用)
|
||||
- `CONFIG_MIMI_TOOL_WEB_SEARCH` — 网络搜索工具
|
||||
- `CONFIG_MIMI_TOOL_GPIO` — GPIO 控制工具(默认禁用)
|
||||
- `CONFIG_MIMI_WS_SERVER` — WebSocket 网关
|
||||
- `CONFIG_MIMI_WIFI_ONBOARD` — WiFi 配置门户
|
||||
- `CONFIG_MIMI_OTA` — OTA 升级(默认禁用)
|
||||
- 禁用模块不参与编译,节省 Flash 和 RAM
|
||||
- 头文件使用 static inline stub,调用方无需修改
|
||||
- CLI 命令和工具注册也支持条件编译
|
||||
|
||||
### 阶段一:准备与设计(1-2天)
|
||||
### 修复
|
||||
|
||||
#### 1.1 详细API调研
|
||||
- [ ] 研究硅基流动API文档,确认:
|
||||
- 具体的Base URL和端点
|
||||
- 认证方式(API Key格式)
|
||||
- 支持的模型列表和ID格式
|
||||
- 工具调用兼容性
|
||||
- 速率限制和配额
|
||||
- 消除 Telegram 未配置时的 5 秒轮询警告日志
|
||||
|
||||
- [ ] 研究火山方舟API文档,确认:
|
||||
- 具体的Base URL和端点
|
||||
- 认证方式(API Key格式)
|
||||
- 支持的模型列表和ID格式
|
||||
- 工具调用兼容性
|
||||
- 速率限制和配额
|
||||
---
|
||||
|
||||
#### 1.2 架构设计
|
||||
- [ ] 设计提供商扩展机制
|
||||
- [ ] 确定配置管理方案
|
||||
- [ ] 设计命令行接口扩展
|
||||
- [ ] 评估内存影响
|
||||
## v1.1.0
|
||||
|
||||
### 阶段二:核心实现(3-5天)
|
||||
### 新增
|
||||
- 国内大模型厂商接入支持(硅基流动、火山方舟)
|
||||
- **时区设置功能**
|
||||
- 默认时区改为 `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` 中显示当前时区配置
|
||||
- **SNTP 自动时间同步**(新增)
|
||||
- WiFi 连接成功后自动从 NTP 服务器同步系统时间
|
||||
- 新增 `time_sync` 模块(`main/time_sync/`)
|
||||
- 新增 `ntp_status` CLI 命令(查看时区、本地时间、同步状态、NTP 服务器、上次同步时间)
|
||||
- 新增 `ntp_sync` CLI 命令(手动触发时间同步)
|
||||
- 新增 `ntp_set <server>` CLI 命令(自定义 NTP 服务器,NVS 持久化)
|
||||
- 默认 NTP 服务器:`ntp.ntsc.ac.cn`(中国科学院国家授时中心)
|
||||
- 同步状态:`synced`(已同步)、`syncing`(同步中)、`not_synced`(未同步)
|
||||
- `set_timezone` 工具在设置时区后自动检测时间有效性,必要时触发 HTTP 时间同步
|
||||
- 记录上次同步时间戳,`ntp_status` 可查看
|
||||
- **NVS 配置安全机制**(新增)
|
||||
- 启动时自动校验关键 NVS 命名空间完整性
|
||||
- 检测并修复损坏的 NVS 条目
|
||||
- 启用 ESP32-S3 Brownout Detection 防止供电不足导致 Flash 写入中断
|
||||
|
||||
#### 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"
|
||||
```
|
||||
### 修复
|
||||
- **LLM Provider 初始化 Bug** — 修复 `llm_provider_init()` 中 provider-specific API key 和 Base URL 无法从 NVS 加载的问题(`llm_provider_get_api_key` 对当前 provider 直接返回内存缓存值,导致 NVS 数据永远不会被读取)
|
||||
- **换 USB 口后配置失效** — 启用 Brownout Detection 防止供电不足时 NVS 写入中断,添加启动时 NVS 完整性校验
|
||||
- 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 等均存在)
|
||||
|
||||
- [ ] 更新 `mimi_config.h` 添加相关默认值
|
||||
### 文档
|
||||
- 新增 `docs/ESP-IDF-V6-MIGRATION.md` — ESP-IDF v6.0 迁移适配记录
|
||||
- 更新 `taolun.md` — 讨论记录整理,新增时间同步和 NVS 配置稳定性问题讨论
|
||||
|
||||
#### 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";
|
||||
```
|
||||
## v1.0.0
|
||||
|
||||
- [ ] 修改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);
|
||||
}
|
||||
```
|
||||
|
||||
## 风险评估与缓解
|
||||
|
||||
### 风险1:API兼容性问题
|
||||
- **风险**:国内厂商的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 Loop(ReAct 工具调用,最多 10 轮迭代)
|
||||
- Claude API(Anthropic Messages API)
|
||||
- OpenAI API 支持
|
||||
- 工具注册 + web_search(Brave Search API)
|
||||
- 上下文构建器(系统提示 + 引导文件 + 记忆 + 工具指导)
|
||||
- 记忆存储(MEMORY.md + 每日笔记)
|
||||
- 会话管理(JSONL per chat_id,环形缓冲区历史)
|
||||
- WebSocket 网关(端口 18789,JSON 协议)
|
||||
- 串口 CLI(esp_console,调试/维护命令)
|
||||
- HTTP CONNECT 代理支持
|
||||
- OTA 更新
|
||||
- WiFi 管理器(构建时凭证,指数退避)
|
||||
- SPIFFS 存储
|
||||
- 构建时配置(`mimi_secrets.h`)+ 运行时 NVS 覆盖
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
170
docs/ESP-IDF-V6-MIGRATION.md
Normal file
170
docs/ESP-IDF-V6-MIGRATION.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# 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** 键再插线进入下载模式
|
||||
|
||||
---
|
||||
|
||||
## ESP-IDF v6.0 API 变更与修复(2026-04-01)
|
||||
|
||||
### 5. SNTP API 弃用问题
|
||||
|
||||
**错误信息:**
|
||||
```
|
||||
warning: 'sntp_setoperatingmode' is deprecated: use esp_sntp_setoperatingmode() instead
|
||||
warning: 'sntp_setservername' is deprecated: use esp_sntp_setservername() instead
|
||||
warning: 'sntp_init' is deprecated: use esp_sntp_init() instead
|
||||
```
|
||||
|
||||
**修复:** `main/time_sync/time_sync.c`
|
||||
- 将所有 `sntp_*` 函数调用替换为 `esp_sntp_*` 函数
|
||||
- 示例:`sntp_init()` → `esp_sntp_init()`
|
||||
|
||||
**注意:** `esp_sntp` 组件在 v6.0 中不存在,`esp_sntp_*` 函数属于 `lwip` 组件,会被 `esp_netif` 和 `esp_wifi` 自动包含。
|
||||
|
||||
### 6. NVS API 兼容性变化
|
||||
|
||||
**错误信息:**
|
||||
```
|
||||
error: too few arguments to function 'nvs_entry_find'; expected 4, have 3
|
||||
error: passing argument 1 of 'nvs_entry_next' from incompatible pointer type
|
||||
```
|
||||
|
||||
**修复:** `main/nvs_safety/nvs_safety.c`
|
||||
- `nvs_entry_find()` 现在需要 4 个参数:`nvs_entry_find(part_name, namespace, type, &iterator)`
|
||||
- `nvs_entry_next()` 需要指向迭代器的指针:`nvs_entry_next(&iterator)`
|
||||
- 新增 `nvs_release_iterator()` 调用释放迭代器
|
||||
|
||||
**建议:** 使用 `nvs_entry_find_in_handle()` 替代 `nvs_entry_find()`,更简洁。
|
||||
|
||||
### 7. 结构体类型错误
|
||||
|
||||
**错误信息:**
|
||||
```
|
||||
error: expected specifier-qualifier-list before 'arg_str1'
|
||||
```
|
||||
|
||||
**修复:** `main/cli/serial_cli.c`
|
||||
- `arg_str1 *server;` → `struct arg_str *server;`
|
||||
- `arg_end *end;` → `struct arg_end *end;`
|
||||
|
||||
**原因:** `arg_str1` 是函数名,不是类型。结构体成员应使用 `struct arg_str` 类型。
|
||||
|
||||
### 8. 未使用的函数警告
|
||||
|
||||
**错误信息:**
|
||||
```
|
||||
warning: 'provider_is_openai' defined but not used
|
||||
```
|
||||
|
||||
**修复:** `main/llm/llm_proxy.c`
|
||||
- 删除未使用的 `provider_is_openai()` 函数
|
||||
- 该函数只是调用 `llm_provider_is_openai_compatible()`,可直接使用原函数
|
||||
|
||||
### 9. 组件依赖问题
|
||||
|
||||
**错误信息:**
|
||||
```
|
||||
Failed to resolve component 'esp_sntp' required by component 'main': unknown name.
|
||||
```
|
||||
|
||||
**修复:** `main/CMakeLists.txt`
|
||||
- 从 `REQUIRES` 列表中移除 `esp_sntp` 组件
|
||||
- `esp_sntp_*` 函数属于 `lwip` 组件,会被其他网络组件自动包含
|
||||
|
||||
---
|
||||
|
||||
## API 兼容性总结
|
||||
|
||||
### 已验证的 API(v6.0 中仍然可用)
|
||||
| API | 状态 |
|
||||
|-----|------|
|
||||
| `esp_http_client_set_header()` | ✅ 未弃用 |
|
||||
| `esp_crt_bundle_attach()` | ✅ 未弃用 |
|
||||
| `esp_netif_create_default_wifi_sta()` | ✅ 未弃用 |
|
||||
| `WIFI_INIT_CONFIG_DEFAULT()` | ✅ 未弃用 |
|
||||
|
||||
### 已弃用的 API(需要替换)
|
||||
| 旧 API | 新 API | 文件 |
|
||||
|--------|--------|------|
|
||||
| `sntp_*()` | `esp_sntp_*()` | `time_sync.c` |
|
||||
| `nvs_entry_find(part, ns, type)` | `nvs_entry_find(part, ns, type, &it)` | `nvs_safety.c` |
|
||||
| `nvs_entry_next(it)` | `nvs_entry_next(&it)` | `nvs_safety.c` |
|
||||
@@ -94,10 +94,9 @@
|
||||
- **MimiClaw**: Not implemented
|
||||
- **Recommendation**: Simple FreeRTOS timer that periodically checks HEARTBEAT.md
|
||||
|
||||
### [ ] Multi-LLM Provider Support
|
||||
### [x] ~~Multi-LLM Provider Support~~
|
||||
- **nanobot**: `providers/litellm_provider.py` — supports OpenRouter, Anthropic, OpenAI, Gemini, DeepSeek, Groq, Zhipu, vLLM via LiteLLM
|
||||
- **MimiClaw**: Hardcoded to Anthropic Messages API
|
||||
- **Recommendation**: Abstract LLM interface, support OpenAI-compatible API (most providers are compatible)
|
||||
- **MimiClaw**: Supports Anthropic, OpenAI, SiliconFlow (硅基流动), Volcengine (火山方舟) — abstracted via `llm_provider.c`
|
||||
|
||||
### [ ] Voice Transcription
|
||||
- **nanobot**: `providers/transcription.py` — Groq Whisper API
|
||||
|
||||
@@ -1,33 +1,69 @@
|
||||
# MimiClaw - CMake build configuration
|
||||
# This file is processed by ESP-IDF's CMake build system
|
||||
|
||||
# ─── Core modules (always compiled) ────────────────────────────────────────
|
||||
set(core_srcs
|
||||
"mimi.c"
|
||||
"bus/message_bus.c"
|
||||
"wifi/wifi_manager.c"
|
||||
"llm/llm_proxy.c"
|
||||
"llm/llm_provider.c"
|
||||
"agent/agent_loop.c"
|
||||
"agent/context_builder.c"
|
||||
"memory/memory_store.c"
|
||||
"memory/session_mgr.c"
|
||||
"cli/serial_cli.c"
|
||||
"proxy/http_proxy.c"
|
||||
"cron/cron_service.c"
|
||||
"heartbeat/heartbeat.c"
|
||||
"tools/tool_registry.c"
|
||||
"tools/tool_cron.c"
|
||||
"tools/tool_get_time.c"
|
||||
"tools/tool_set_timezone.c"
|
||||
"tools/tool_files.c"
|
||||
"skills/skill_loader.c"
|
||||
"time_sync/time_sync.c"
|
||||
"nvs_safety/nvs_safety.c"
|
||||
)
|
||||
|
||||
# ─── Channel modules ───────────────────────────────────────────────────────
|
||||
if(CONFIG_MIMI_CHAN_TELEGRAM)
|
||||
list(APPEND core_srcs "channels/telegram/telegram_bot.c")
|
||||
endif()
|
||||
|
||||
if(CONFIG_MIMI_CHAN_FEISHU)
|
||||
list(APPEND core_srcs "channels/feishu/feishu_bot.c")
|
||||
endif()
|
||||
|
||||
# ─── Optional modules ───────────────────────────────────────────────────────
|
||||
if(CONFIG_MIMI_WS_SERVER)
|
||||
list(APPEND core_srcs "gateway/ws_server.c")
|
||||
endif()
|
||||
|
||||
if(CONFIG_MIMI_WIFI_ONBOARD)
|
||||
list(APPEND core_srcs "onboard/wifi_onboard.c")
|
||||
endif()
|
||||
|
||||
if(CONFIG_MIMI_OTA)
|
||||
list(APPEND core_srcs "ota/ota_manager.c")
|
||||
endif()
|
||||
|
||||
# ─── Tool modules ──────────────────────────────────────────────────────────
|
||||
if(CONFIG_MIMI_TOOL_WEB_SEARCH)
|
||||
list(APPEND core_srcs "tools/tool_web_search.c")
|
||||
endif()
|
||||
|
||||
if(CONFIG_MIMI_TOOL_GPIO)
|
||||
list(APPEND core_srcs "tools/tool_gpio.c")
|
||||
list(APPEND core_srcs "tools/gpio_policy.c")
|
||||
endif()
|
||||
|
||||
# ─── Register component ───────────────────────────────────────────────────
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"mimi.c"
|
||||
"bus/message_bus.c"
|
||||
"wifi/wifi_manager.c"
|
||||
"channels/telegram/telegram_bot.c"
|
||||
"channels/feishu/feishu_bot.c"
|
||||
"llm/llm_proxy.c"
|
||||
"agent/agent_loop.c"
|
||||
"agent/context_builder.c"
|
||||
"memory/memory_store.c"
|
||||
"memory/session_mgr.c"
|
||||
"gateway/ws_server.c"
|
||||
"cli/serial_cli.c"
|
||||
"proxy/http_proxy.c"
|
||||
"cron/cron_service.c"
|
||||
"heartbeat/heartbeat.c"
|
||||
"tools/tool_registry.c"
|
||||
"tools/tool_cron.c"
|
||||
"tools/tool_web_search.c"
|
||||
"tools/tool_get_time.c"
|
||||
"tools/tool_files.c"
|
||||
"tools/tool_gpio.c"
|
||||
"tools/gpio_policy.c"
|
||||
"skills/skill_loader.c"
|
||||
"onboard/wifi_onboard.c"
|
||||
INCLUDE_DIRS
|
||||
"."
|
||||
SRCS ${core_srcs}
|
||||
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
|
||||
)
|
||||
|
||||
51
main/Kconfig.projbuild
Normal file
51
main/Kconfig.projbuild
Normal file
@@ -0,0 +1,51 @@
|
||||
menu "MimiClaw Configuration"
|
||||
|
||||
menu "Channel Modules"
|
||||
config MIMI_CHAN_TELEGRAM
|
||||
bool "Telegram bot integration"
|
||||
default n
|
||||
help
|
||||
Enable Telegram bot integration for MimiClaw.
|
||||
|
||||
config MIMI_CHAN_FEISHU
|
||||
bool "Feishu (Lark) bot integration"
|
||||
default y
|
||||
help
|
||||
Enable Feishu (Lark) bot integration for MimiClaw.
|
||||
endmenu
|
||||
|
||||
menu "Tool Modules"
|
||||
config MIMI_TOOL_WEB_SEARCH
|
||||
bool "Web search tool"
|
||||
default y
|
||||
help
|
||||
Enable web search tool (requires search API key).
|
||||
|
||||
config MIMI_TOOL_GPIO
|
||||
bool "GPIO control tool"
|
||||
default n
|
||||
help
|
||||
Enable GPIO control tool for hardware control.
|
||||
endmenu
|
||||
|
||||
menu "Optional Modules"
|
||||
config MIMI_WS_SERVER
|
||||
bool "WebSocket gateway"
|
||||
default y
|
||||
help
|
||||
Enable WebSocket gateway for local clients.
|
||||
|
||||
config MIMI_WIFI_ONBOARD
|
||||
bool "WiFi onboarding portal"
|
||||
default y
|
||||
help
|
||||
Enable Captive Portal for initial WiFi setup.
|
||||
|
||||
config MIMI_OTA
|
||||
bool "OTA firmware update"
|
||||
default n
|
||||
help
|
||||
Enable OTA firmware update (not fully implemented).
|
||||
endmenu
|
||||
|
||||
endmenu
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -2,35 +2,25 @@
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
/**
|
||||
* Initialize the Feishu bot (load credentials from NVS / build-time).
|
||||
*/
|
||||
#ifdef CONFIG_MIMI_CHAN_FEISHU
|
||||
esp_err_t feishu_bot_init(void);
|
||||
|
||||
/**
|
||||
* Start the Feishu webhook HTTP server for receiving events.
|
||||
* Listens on MIMI_FEISHU_WEBHOOK_PORT.
|
||||
*/
|
||||
esp_err_t feishu_bot_start(void);
|
||||
|
||||
/**
|
||||
* Send a text message to a Feishu chat.
|
||||
* Automatically splits messages longer than MIMI_FEISHU_MAX_MSG_LEN chars.
|
||||
* @param chat_id Feishu chat ID (open_id or chat_id)
|
||||
* @param text Message text
|
||||
*/
|
||||
esp_err_t feishu_send_message(const char *chat_id, const char *text);
|
||||
|
||||
/**
|
||||
* Reply to a specific message in a Feishu chat.
|
||||
* @param message_id The message_id to reply to
|
||||
* @param text Reply text
|
||||
*/
|
||||
esp_err_t feishu_reply_message(const char *message_id, const char *text);
|
||||
|
||||
/**
|
||||
* Save Feishu app credentials to NVS.
|
||||
* @param app_id Feishu App ID
|
||||
* @param app_secret Feishu App Secret
|
||||
*/
|
||||
esp_err_t feishu_set_credentials(const char *app_id, const char *app_secret);
|
||||
#else
|
||||
static inline esp_err_t feishu_bot_init(void) { return ESP_OK; }
|
||||
static inline esp_err_t feishu_bot_start(void) { return ESP_OK; }
|
||||
static inline esp_err_t feishu_send_message(const char *chat_id, const char *text) {
|
||||
(void)chat_id; (void)text;
|
||||
return ESP_OK;
|
||||
}
|
||||
static inline esp_err_t feishu_reply_message(const char *message_id, const char *text) {
|
||||
(void)message_id; (void)text;
|
||||
return ESP_OK;
|
||||
}
|
||||
static inline esp_err_t feishu_set_credentials(const char *app_id, const char *app_secret) {
|
||||
(void)app_id; (void)app_secret;
|
||||
return ESP_OK;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -2,26 +2,20 @@
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
/**
|
||||
* Initialize the Telegram bot.
|
||||
*/
|
||||
#ifdef CONFIG_MIMI_CHAN_TELEGRAM
|
||||
esp_err_t telegram_bot_init(void);
|
||||
|
||||
/**
|
||||
* Start the Telegram polling task (long polling on Core 0).
|
||||
*/
|
||||
esp_err_t telegram_bot_start(void);
|
||||
|
||||
/**
|
||||
* Send a text message to a Telegram chat.
|
||||
* Automatically splits messages longer than 4096 chars.
|
||||
* @param chat_id Telegram chat ID (numeric string)
|
||||
* @param text Message text (supports Markdown)
|
||||
*/
|
||||
esp_err_t telegram_send_message(const char *chat_id, const char *text);
|
||||
|
||||
/**
|
||||
* Save the Telegram bot token to NVS.
|
||||
*/
|
||||
esp_err_t telegram_set_token(const char *token);
|
||||
|
||||
#else
|
||||
static inline esp_err_t telegram_bot_init(void) { return ESP_OK; }
|
||||
static inline esp_err_t telegram_bot_start(void) { return ESP_OK; }
|
||||
static inline esp_err_t telegram_send_message(const char *chat_id, const char *text) {
|
||||
(void)chat_id; (void)text;
|
||||
return ESP_OK;
|
||||
}
|
||||
static inline esp_err_t telegram_set_token(const char *token) {
|
||||
(void)token;
|
||||
return ESP_OK;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -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"
|
||||
@@ -12,10 +13,13 @@
|
||||
#include "cron/cron_service.h"
|
||||
#include "heartbeat/heartbeat.h"
|
||||
#include "skills/skill_loader.h"
|
||||
#include "time_sync/time_sync.h"
|
||||
|
||||
#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 +175,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 +641,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 +684,149 @@ 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;
|
||||
}
|
||||
|
||||
/* --- ntp_status command --- */
|
||||
static int cmd_ntp_status(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);
|
||||
printf("Time sync: %s\n", time_sync_status_str());
|
||||
printf("NTP server: %s\n", time_sync_get_server());
|
||||
|
||||
char synced_str[32];
|
||||
if (time_sync_get_last_synced(synced_str, sizeof(synced_str))) {
|
||||
printf("Last synced: %s\n", synced_str);
|
||||
} else {
|
||||
printf("Last synced: never\n");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- ntp_sync command --- */
|
||||
static int cmd_ntp_sync(int argc, char **argv)
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
if (time_sync_is_synced()) {
|
||||
printf("Time is already synced. Use 'ntp_set <server>' to change server.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("Triggering SNTP sync...\n");
|
||||
time_sync_restart();
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
|
||||
if (time_sync_is_synced()) {
|
||||
char synced_str[32];
|
||||
time_sync_get_last_synced(synced_str, sizeof(synced_str));
|
||||
printf("Synced successfully. Last synced: %s\n", synced_str);
|
||||
} else {
|
||||
printf("Sync in progress. Check 'ntp_status' for updates.\n");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- ntp_set command --- */
|
||||
typedef struct {
|
||||
struct arg_str *server;
|
||||
struct arg_end *end;
|
||||
} ntp_set_args;
|
||||
static ntp_set_args ntp_set_arguments;
|
||||
|
||||
static int cmd_ntp_set(int argc, char **argv)
|
||||
{
|
||||
int nerrors = arg_parse(argc, argv, (void **)&ntp_set_arguments);
|
||||
if (nerrors != 0) {
|
||||
arg_print_errors(stderr, ntp_set_arguments.end, argv[0]);
|
||||
printf("Usage: ntp_set <server>\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *server = ntp_set_arguments.server->sval[0];
|
||||
|
||||
esp_err_t err = time_sync_set_server(server);
|
||||
if (err != ESP_OK) {
|
||||
printf("Failed to set NTP server: %s\n", esp_err_to_name(err));
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("NTP server set to '%s'. Restart or run 'ntp_sync' to apply.\n", server);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- heartbeat_trigger command --- */
|
||||
static int cmd_heartbeat_trigger(int argc, char **argv)
|
||||
{
|
||||
@@ -649,13 +887,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 +998,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;
|
||||
@@ -830,6 +1075,7 @@ esp_err_t serial_cli_init(void)
|
||||
esp_console_cmd_register(&wifi_scan_cmd);
|
||||
|
||||
/* set_tg_token */
|
||||
#ifdef CONFIG_MIMI_CHAN_TELEGRAM
|
||||
tg_token_args.token = arg_str1(NULL, NULL, "<token>", "Telegram bot token");
|
||||
tg_token_args.end = arg_end(1);
|
||||
esp_console_cmd_t tg_token_cmd = {
|
||||
@@ -839,8 +1085,13 @@ esp_err_t serial_cli_init(void)
|
||||
.argtable = &tg_token_args,
|
||||
};
|
||||
esp_console_cmd_register(&tg_token_cmd);
|
||||
#else
|
||||
(void)cmd_set_tg_token;
|
||||
(void)tg_token_args;
|
||||
#endif
|
||||
|
||||
/* set_feishu_creds */
|
||||
#ifdef CONFIG_MIMI_CHAN_FEISHU
|
||||
feishu_creds_args.app_id = arg_str1(NULL, NULL, "<app_id>", "Feishu App ID");
|
||||
feishu_creds_args.app_secret = arg_str1(NULL, NULL, "<app_secret>", "Feishu App Secret");
|
||||
feishu_creds_args.end = arg_end(2);
|
||||
@@ -863,6 +1114,12 @@ esp_err_t serial_cli_init(void)
|
||||
.argtable = &feishu_send_args,
|
||||
};
|
||||
esp_console_cmd_register(&feishu_send_cmd);
|
||||
#else
|
||||
(void)cmd_set_feishu_creds;
|
||||
(void)cmd_feishu_send;
|
||||
(void)feishu_creds_args;
|
||||
(void)feishu_send_args;
|
||||
#endif
|
||||
|
||||
/* set_api_key */
|
||||
api_key_args.key = arg_str1(NULL, NULL, "<key>", "LLM API key");
|
||||
@@ -887,7 +1144,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 +1154,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",
|
||||
@@ -974,6 +1275,7 @@ esp_err_t serial_cli_init(void)
|
||||
esp_console_cmd_register(&heap_cmd);
|
||||
|
||||
/* set_search_key */
|
||||
#ifdef CONFIG_MIMI_TOOL_WEB_SEARCH
|
||||
search_key_args.key = arg_str1(NULL, NULL, "<key>", "Brave Search API key");
|
||||
search_key_args.end = arg_end(1);
|
||||
esp_console_cmd_t search_key_cmd = {
|
||||
@@ -994,6 +1296,12 @@ esp_err_t serial_cli_init(void)
|
||||
.argtable = &tavily_key_args,
|
||||
};
|
||||
esp_console_cmd_register(&tavily_key_cmd);
|
||||
#else
|
||||
(void)cmd_set_search_key;
|
||||
(void)cmd_set_tavily_key;
|
||||
(void)search_key_args;
|
||||
(void)tavily_key_args;
|
||||
#endif
|
||||
|
||||
/* set_proxy */
|
||||
proxy_args.host = arg_str1(NULL, NULL, "<host>", "Proxy host/IP");
|
||||
@@ -1032,6 +1340,44 @@ 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);
|
||||
|
||||
/* ntp_status */
|
||||
esp_console_cmd_t ntp_status_cmd = {
|
||||
.command = "ntp_status",
|
||||
.help = "Show timezone, local time, NTP sync status, server and last sync time",
|
||||
.func = &cmd_ntp_status,
|
||||
};
|
||||
esp_console_cmd_register(&ntp_status_cmd);
|
||||
|
||||
/* ntp_sync */
|
||||
esp_console_cmd_t ntp_sync_cmd = {
|
||||
.command = "ntp_sync",
|
||||
.help = "Manually trigger NTP time synchronization",
|
||||
.func = &cmd_ntp_sync,
|
||||
};
|
||||
esp_console_cmd_register(&ntp_sync_cmd);
|
||||
|
||||
/* ntp_set */
|
||||
ntp_set_arguments.server = arg_str1(NULL, NULL, "<server>", "NTP server hostname");
|
||||
ntp_set_arguments.end = arg_end(1);
|
||||
esp_console_cmd_t ntp_set_cmd = {
|
||||
.command = "ntp_set",
|
||||
.help = "Set custom NTP server (e.g. ntp_set ntp.ntsc.ac.cn)",
|
||||
.func = &cmd_ntp_set,
|
||||
.argtable = &ntp_set_arguments,
|
||||
};
|
||||
esp_console_cmd_register(&ntp_set_cmd);
|
||||
|
||||
/* heartbeat_trigger */
|
||||
esp_console_cmd_t heartbeat_cmd = {
|
||||
.command = "heartbeat_trigger",
|
||||
@@ -1057,6 +1403,7 @@ esp_err_t serial_cli_init(void)
|
||||
esp_console_cmd_register(&tool_exec_cmd);
|
||||
|
||||
/* web_search */
|
||||
#ifdef CONFIG_MIMI_TOOL_WEB_SEARCH
|
||||
web_search_args.query = arg_str1(NULL, NULL, "<query>", "Search query");
|
||||
web_search_args.end = arg_end(1);
|
||||
esp_console_cmd_t web_search_cmd = {
|
||||
@@ -1066,6 +1413,10 @@ esp_err_t serial_cli_init(void)
|
||||
.argtable = &web_search_args,
|
||||
};
|
||||
esp_console_cmd_register(&web_search_cmd);
|
||||
#else
|
||||
(void)cmd_web_search;
|
||||
(void)web_search_args;
|
||||
#endif
|
||||
|
||||
/* restart */
|
||||
esp_console_cmd_t restart_cmd = {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -2,24 +2,15 @@
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
/**
|
||||
* Initialize and start the WebSocket server on MIMI_WS_PORT.
|
||||
* Allows external clients to interact with the Agent via JSON messages.
|
||||
*
|
||||
* Protocol:
|
||||
* Inbound: {"type":"message","content":"hello","chat_id":"ws_client1"}
|
||||
* Outbound: {"type":"response","content":"Hi!","chat_id":"ws_client1"}
|
||||
*/
|
||||
#ifdef CONFIG_MIMI_WS_SERVER
|
||||
esp_err_t ws_server_start(void);
|
||||
|
||||
/**
|
||||
* Send a text message to a specific WebSocket client by chat_id.
|
||||
* @param chat_id Client identifier (assigned on connection)
|
||||
* @param text Message text
|
||||
*/
|
||||
esp_err_t ws_server_send(const char *chat_id, const char *text);
|
||||
|
||||
/**
|
||||
* Stop the WebSocket server.
|
||||
*/
|
||||
esp_err_t ws_server_stop(void);
|
||||
#else
|
||||
static inline esp_err_t ws_server_start(void) { return ESP_OK; }
|
||||
static inline esp_err_t ws_server_send(const char *chat_id, const char *text) {
|
||||
(void)chat_id; (void)text;
|
||||
return ESP_OK;
|
||||
}
|
||||
static inline esp_err_t ws_server_stop(void) { return ESP_OK; }
|
||||
#endif
|
||||
|
||||
@@ -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"
|
||||
|
||||
315
main/llm/llm_provider.c
Normal file
315
main/llm/llm_provider.c
Normal file
@@ -0,0 +1,315 @@
|
||||
#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) {
|
||||
const char *nvs_key = get_provider_api_key_nvs_key(s_current_provider->name);
|
||||
if (nvs_key) {
|
||||
nvs_handle_t nvs;
|
||||
if (nvs_open(MIMI_NVS_LLM, NVS_READONLY, &nvs) == ESP_OK) {
|
||||
size_t len = sizeof(s_api_key);
|
||||
if (nvs_get_str(nvs, nvs_key, s_api_key, &len) != ESP_OK || !s_api_key[0]) {
|
||||
s_api_key[0] = '\0';
|
||||
}
|
||||
nvs_close(nvs);
|
||||
} else {
|
||||
s_api_key[0] = '\0';
|
||||
}
|
||||
} else {
|
||||
s_api_key[0] = '\0';
|
||||
}
|
||||
|
||||
/* Load Base URL for current provider directly from NVS */
|
||||
const char *url_nvs_key = get_provider_base_url_nvs_key(s_current_provider->name);
|
||||
if (url_nvs_key) {
|
||||
nvs_handle_t nvs;
|
||||
if (nvs_open(MIMI_NVS_LLM, NVS_READONLY, &nvs) == ESP_OK) {
|
||||
size_t len = sizeof(s_base_url);
|
||||
if (nvs_get_str(nvs, url_nvs_key, s_base_url, &len) != ESP_OK || !s_base_url[0]) {
|
||||
s_base_url[0] = '\0';
|
||||
}
|
||||
nvs_close(nvs);
|
||||
} else {
|
||||
s_base_url[0] = '\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
68
main/llm/llm_provider.h
Normal 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
|
||||
@@ -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"
|
||||
@@ -182,30 +184,28 @@ static esp_err_t http_event_handler(esp_http_client_event_t *evt)
|
||||
|
||||
/* ── Provider helpers ──────────────────────────────────────────── */
|
||||
|
||||
static bool provider_is_openai(void)
|
||||
{
|
||||
return strcmp(s_provider, "openai") == 0;
|
||||
}
|
||||
|
||||
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 +215,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 +236,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 +288,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 +318,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 +330,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 +587,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 +663,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 +808,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 +838,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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
31
main/mimi.c
31
main/mimi.c
@@ -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"
|
||||
@@ -26,6 +27,8 @@
|
||||
#include "heartbeat/heartbeat.h"
|
||||
#include "skills/skill_loader.h"
|
||||
#include "onboard/wifi_onboard.h"
|
||||
#include "time_sync/time_sync.h"
|
||||
#include "nvs_safety/nvs_safety.h"
|
||||
|
||||
static const char *TAG = "mimi";
|
||||
|
||||
@@ -74,24 +77,36 @@ static void outbound_dispatch_task(void *arg)
|
||||
ESP_LOGI(TAG, "Dispatching response to %s:%s", msg.channel, msg.chat_id);
|
||||
|
||||
if (strcmp(msg.channel, MIMI_CHAN_TELEGRAM) == 0) {
|
||||
#ifdef CONFIG_MIMI_CHAN_TELEGRAM
|
||||
esp_err_t send_err = telegram_send_message(msg.chat_id, msg.content);
|
||||
if (send_err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Telegram send failed for %s: %s", msg.chat_id, esp_err_to_name(send_err));
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Telegram send success for %s (%d bytes)", msg.chat_id, (int)strlen(msg.content));
|
||||
}
|
||||
#else
|
||||
ESP_LOGW(TAG, "Telegram disabled, dropping message for %s", msg.chat_id);
|
||||
#endif
|
||||
} else if (strcmp(msg.channel, MIMI_CHAN_FEISHU) == 0) {
|
||||
#ifdef CONFIG_MIMI_CHAN_FEISHU
|
||||
esp_err_t send_err = feishu_send_message(msg.chat_id, msg.content);
|
||||
if (send_err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Feishu send failed for %s: %s", msg.chat_id, esp_err_to_name(send_err));
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Feishu send success for %s (%d bytes)", msg.chat_id, (int)strlen(msg.content));
|
||||
}
|
||||
#else
|
||||
ESP_LOGW(TAG, "Feishu disabled, dropping message for %s", msg.chat_id);
|
||||
#endif
|
||||
} else if (strcmp(msg.channel, MIMI_CHAN_WEBSOCKET) == 0) {
|
||||
#ifdef CONFIG_MIMI_WS_SERVER
|
||||
esp_err_t ws_err = ws_server_send(msg.chat_id, msg.content);
|
||||
if (ws_err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "WS send failed for %s: %s", msg.chat_id, esp_err_to_name(ws_err));
|
||||
}
|
||||
#else
|
||||
ESP_LOGW(TAG, "WebSocket disabled, dropping message for %s", msg.chat_id);
|
||||
#endif
|
||||
} else if (strcmp(msg.channel, MIMI_CHAN_SYSTEM) == 0) {
|
||||
ESP_LOGI(TAG, "System message [%s]: %.128s", msg.chat_id, msg.content);
|
||||
} else {
|
||||
@@ -119,6 +134,7 @@ void app_main(void)
|
||||
|
||||
/* Phase 1: Core infrastructure */
|
||||
ESP_ERROR_CHECK(init_nvs());
|
||||
nvs_safety_check();
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
ESP_ERROR_CHECK(init_spiffs());
|
||||
|
||||
@@ -150,6 +166,7 @@ void app_main(void)
|
||||
if (wifi_manager_wait_connected(30000) == ESP_OK) {
|
||||
wifi_ok = true;
|
||||
ESP_LOGI(TAG, "WiFi connected: %s", wifi_manager_get_ip());
|
||||
time_sync_init();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "WiFi connection timeout");
|
||||
}
|
||||
@@ -159,13 +176,19 @@ void app_main(void)
|
||||
|
||||
if (!wifi_ok) {
|
||||
ESP_LOGW(TAG, "Entering WiFi onboarding mode...");
|
||||
#ifdef CONFIG_MIMI_WIFI_ONBOARD
|
||||
wifi_onboard_start(WIFI_ONBOARD_MODE_CAPTIVE); /* blocks, restarts on success */
|
||||
return; /* unreachable */
|
||||
#else
|
||||
ESP_LOGE(TAG, "WiFi onboarding disabled. Configure WiFi via CLI or flash new firmware.");
|
||||
return;
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef CONFIG_MIMI_WIFI_ONBOARD
|
||||
if (wifi_onboard_start(WIFI_ONBOARD_MODE_ADMIN) != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Local admin portal unavailable; continuing without config hotspot");
|
||||
}
|
||||
#endif
|
||||
|
||||
{
|
||||
/* Outbound dispatch task should start first to avoid dropping early replies. */
|
||||
@@ -177,11 +200,17 @@ void app_main(void)
|
||||
|
||||
/* Start network-dependent services */
|
||||
ESP_ERROR_CHECK(agent_loop_start());
|
||||
#ifdef CONFIG_MIMI_CHAN_TELEGRAM
|
||||
ESP_ERROR_CHECK(telegram_bot_start());
|
||||
#endif
|
||||
#ifdef CONFIG_MIMI_CHAN_FEISHU
|
||||
ESP_ERROR_CHECK(feishu_bot_start());
|
||||
#endif
|
||||
cron_service_start();
|
||||
heartbeat_start();
|
||||
#ifdef CONFIG_MIMI_WS_SERVER
|
||||
ESP_ERROR_CHECK(ws_server_start());
|
||||
#endif
|
||||
|
||||
ESP_LOGI(TAG, "All services started!");
|
||||
}
|
||||
|
||||
@@ -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,23 @@
|
||||
#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"
|
||||
#define MIMI_NVS_KEY_NTP_SERVER "ntp_server"
|
||||
|
||||
/* NTP */
|
||||
#define MIMI_DEFAULT_NTP_SERVER "ntp.ntsc.ac.cn"
|
||||
|
||||
/* WiFi Onboarding (Captive Portal) */
|
||||
#define MIMI_ONBOARD_AP_PREFIX "MimiClaw-"
|
||||
#define MIMI_ONBOARD_AP_PASS "" /* open network */
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
101
main/nvs_safety/nvs_safety.c
Normal file
101
main/nvs_safety/nvs_safety.c
Normal file
@@ -0,0 +1,101 @@
|
||||
#include "nvs_safety.h"
|
||||
#include "mimi_config.h"
|
||||
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "nvs.h"
|
||||
|
||||
static const char *TAG = "nvs_safety";
|
||||
|
||||
/* Critical namespaces to check */
|
||||
static const char *critical_namespaces[] = {
|
||||
MIMI_NVS_WIFI,
|
||||
MIMI_NVS_LLM,
|
||||
MIMI_NVS_TG,
|
||||
MIMI_NVS_FEISHU,
|
||||
MIMI_NVS_PROXY,
|
||||
MIMI_NVS_SEARCH,
|
||||
"system_config",
|
||||
};
|
||||
static const int critical_ns_count = sizeof(critical_namespaces) / sizeof(critical_namespaces[0]);
|
||||
|
||||
/*
|
||||
* Iterate through all keys in a namespace and validate each entry.
|
||||
* Erase entries that appear corrupted (invalid length, invalid name, etc.).
|
||||
* Returns number of corrupted entries found and removed.
|
||||
*/
|
||||
static int check_and_repair_namespace(const char *ns)
|
||||
{
|
||||
nvs_handle_t nvs;
|
||||
esp_err_t err = nvs_open(ns, NVS_READWRITE, &nvs);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Cannot open namespace '%s': %s", ns, esp_err_to_name(err));
|
||||
return 0;
|
||||
}
|
||||
|
||||
nvs_iterator_t it = NULL;
|
||||
esp_err_t find_err = nvs_entry_find_in_handle(nvs, NVS_TYPE_ANY, &it);
|
||||
int corrupted = 0;
|
||||
|
||||
while (find_err == ESP_OK && it != NULL) {
|
||||
nvs_entry_info_t info;
|
||||
nvs_entry_info(it, &info);
|
||||
find_err = nvs_entry_next(&it);
|
||||
|
||||
/* Try to read the entry to verify integrity */
|
||||
char buf[512];
|
||||
size_t len = sizeof(buf);
|
||||
esp_err_t read_err = nvs_get_str(nvs, info.key, buf, &len);
|
||||
|
||||
if (read_err == ESP_ERR_NVS_INVALID_LENGTH ||
|
||||
read_err == ESP_ERR_NVS_INVALID_NAME ||
|
||||
read_err == ESP_ERR_NVS_NOT_FOUND) {
|
||||
ESP_LOGW(TAG, "Corrupted entry in %s: key='%s' (err=%s), erasing",
|
||||
ns, info.key, esp_err_to_name(read_err));
|
||||
nvs_erase_key(nvs, info.key);
|
||||
corrupted++;
|
||||
} else if (read_err == ESP_OK) {
|
||||
/* Valid entry - check for obviously corrupted values */
|
||||
if (len > 0 && buf[0] != '\0') {
|
||||
/* Check that string is properly null-terminated within expected bounds */
|
||||
if (len > sizeof(buf) - 1) {
|
||||
ESP_LOGW(TAG, "Oversized value in %s: key='%s' (len=%d), erasing",
|
||||
ns, info.key, (int)len);
|
||||
nvs_erase_key(nvs, info.key);
|
||||
corrupted++;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* ESP_ERR_NVS_TYPE_MISMATCH is not an error for string-only namespaces */
|
||||
}
|
||||
|
||||
if (corrupted > 0) {
|
||||
nvs_commit(nvs);
|
||||
ESP_LOGW(TAG, "Repaired %d corrupted entries in namespace '%s'", corrupted, ns);
|
||||
}
|
||||
|
||||
if (it != NULL) {
|
||||
nvs_release_iterator(it);
|
||||
}
|
||||
nvs_close(nvs);
|
||||
return corrupted;
|
||||
}
|
||||
|
||||
esp_err_t nvs_safety_check(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Checking NVS integrity...");
|
||||
|
||||
int total_corrupted = 0;
|
||||
|
||||
for (int i = 0; i < critical_ns_count; i++) {
|
||||
total_corrupted += check_and_repair_namespace(critical_namespaces[i]);
|
||||
}
|
||||
|
||||
if (total_corrupted > 0) {
|
||||
ESP_LOGW(TAG, "NVS safety check complete: %d corrupted entries repaired", total_corrupted);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "NVS integrity check passed");
|
||||
return ESP_OK;
|
||||
}
|
||||
11
main/nvs_safety/nvs_safety.h
Normal file
11
main/nvs_safety/nvs_safety.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
/**
|
||||
* Check integrity of critical NVS namespaces at startup.
|
||||
* Detects corrupted entries and attempts automatic repair.
|
||||
*
|
||||
* @return ESP_OK if all namespaces are healthy, ESP_ERR_INVALID_STATE if corruption was found and repaired
|
||||
*/
|
||||
esp_err_t nvs_safety_check(void);
|
||||
@@ -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()});"
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -2,14 +2,21 @@
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef CONFIG_MIMI_WIFI_ONBOARD
|
||||
typedef enum {
|
||||
WIFI_ONBOARD_MODE_CAPTIVE = 0,
|
||||
WIFI_ONBOARD_MODE_ADMIN,
|
||||
} wifi_onboard_mode_t;
|
||||
|
||||
/**
|
||||
* Start WiFi onboarding/configuration portal.
|
||||
* CAPTIVE mode opens DNS hijack + config page and blocks forever.
|
||||
* ADMIN mode keeps a local config hotspot alive without captive redirects.
|
||||
*/
|
||||
esp_err_t wifi_onboard_start(wifi_onboard_mode_t mode);
|
||||
#else
|
||||
typedef enum {
|
||||
WIFI_ONBOARD_MODE_CAPTIVE = 0,
|
||||
WIFI_ONBOARD_MODE_ADMIN,
|
||||
} wifi_onboard_mode_t;
|
||||
|
||||
static inline esp_err_t wifi_onboard_start(wifi_onboard_mode_t mode) {
|
||||
(void)mode;
|
||||
return ESP_OK;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
/**
|
||||
* Perform OTA firmware update from a URL.
|
||||
* Downloads the firmware binary and applies it. Reboots on success.
|
||||
*
|
||||
* @param url HTTPS URL to the firmware .bin file
|
||||
* @return ESP_OK on success (device will reboot), error code otherwise
|
||||
*/
|
||||
#ifdef CONFIG_MIMI_OTA
|
||||
esp_err_t ota_update_from_url(const char *url);
|
||||
#else
|
||||
static inline esp_err_t ota_update_from_url(const char *url) {
|
||||
(void)url;
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -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;
|
||||
|
||||
153
main/time_sync/time_sync.c
Normal file
153
main/time_sync/time_sync.c
Normal file
@@ -0,0 +1,153 @@
|
||||
#include "time_sync.h"
|
||||
#include "mimi_config.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_sntp.h"
|
||||
#include "nvs.h"
|
||||
|
||||
static const char *TAG = "time_sync";
|
||||
static volatile bool s_synced = false;
|
||||
static volatile time_t s_last_synced_time = 0;
|
||||
static char s_ntp_server[128] = MIMI_DEFAULT_NTP_SERVER;
|
||||
|
||||
static void sntp_sync_cb(struct timeval *tv)
|
||||
{
|
||||
(void)tv;
|
||||
s_synced = true;
|
||||
s_last_synced_time = tv->tv_sec;
|
||||
|
||||
/* Apply timezone from NVS */
|
||||
char tz_str[64] = {0};
|
||||
nvs_handle_t nvs;
|
||||
if (nvs_open("system_config", NVS_READONLY, &nvs) == ESP_OK) {
|
||||
size_t len = sizeof(tz_str);
|
||||
if (nvs_get_str(nvs, MIMI_NVS_KEY_TIMEZONE, tz_str, &len) == ESP_OK && tz_str[0]) {
|
||||
setenv("TZ", tz_str, 1);
|
||||
tzset();
|
||||
ESP_LOGI(TAG, "Applied timezone from NVS: %s", tz_str);
|
||||
} else {
|
||||
setenv("TZ", MIMI_TIMEZONE, 1);
|
||||
tzset();
|
||||
ESP_LOGI(TAG, "Using build-time timezone: %s", MIMI_TIMEZONE);
|
||||
}
|
||||
nvs_close(nvs);
|
||||
} else {
|
||||
setenv("TZ", MIMI_TIMEZONE, 1);
|
||||
tzset();
|
||||
ESP_LOGI(TAG, "Using build-time timezone: %s", MIMI_TIMEZONE);
|
||||
}
|
||||
|
||||
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);
|
||||
ESP_LOGI(TAG, "Time synchronized: %s", time_str);
|
||||
}
|
||||
|
||||
static void load_custom_server(void)
|
||||
{
|
||||
nvs_handle_t nvs;
|
||||
if (nvs_open("system_config", NVS_READONLY, &nvs) == ESP_OK) {
|
||||
size_t len = sizeof(s_ntp_server);
|
||||
if (nvs_get_str(nvs, MIMI_NVS_KEY_NTP_SERVER, s_ntp_server, &len) == ESP_OK && s_ntp_server[0]) {
|
||||
ESP_LOGI(TAG, "Loaded custom NTP server from NVS: %s", s_ntp_server);
|
||||
} else {
|
||||
strlcpy(s_ntp_server, MIMI_DEFAULT_NTP_SERVER, sizeof(s_ntp_server));
|
||||
}
|
||||
nvs_close(nvs);
|
||||
}
|
||||
}
|
||||
|
||||
static void start_sntp(void)
|
||||
{
|
||||
esp_sntp_stop();
|
||||
esp_sntp_setoperatingmode(SNTP_OPMODE_POLL);
|
||||
esp_sntp_setservername(0, s_ntp_server);
|
||||
esp_sntp_set_time_sync_notification_cb(sntp_sync_cb);
|
||||
esp_sntp_init();
|
||||
ESP_LOGI(TAG, "SNTP configured with server: %s", s_ntp_server);
|
||||
}
|
||||
|
||||
esp_err_t time_sync_init(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Initializing SNTP...");
|
||||
|
||||
load_custom_server();
|
||||
start_sntp();
|
||||
s_synced = false;
|
||||
s_last_synced_time = 0;
|
||||
|
||||
ESP_LOGI(TAG, "SNTP started, waiting for sync...");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool time_sync_is_synced(void)
|
||||
{
|
||||
return s_synced;
|
||||
}
|
||||
|
||||
const char *time_sync_status_str(void)
|
||||
{
|
||||
if (s_synced) return "synced";
|
||||
if (esp_sntp_getservername(0) != NULL) return "syncing";
|
||||
return "not_synced";
|
||||
}
|
||||
|
||||
bool time_sync_get_last_synced(char *out, size_t out_size)
|
||||
{
|
||||
if (s_last_synced_time == 0) return false;
|
||||
|
||||
time_t t = (time_t)s_last_synced_time;
|
||||
struct tm tm_now;
|
||||
localtime_r(&t, &tm_now);
|
||||
strftime(out, out_size, "%Y-%m-%d %H:%M:%S", &tm_now);
|
||||
return true;
|
||||
}
|
||||
|
||||
const char *time_sync_get_server(void)
|
||||
{
|
||||
return s_ntp_server;
|
||||
}
|
||||
|
||||
esp_err_t time_sync_set_server(const char *server)
|
||||
{
|
||||
if (!server || !server[0]) return ESP_ERR_INVALID_ARG;
|
||||
|
||||
strlcpy(s_ntp_server, server, sizeof(s_ntp_server));
|
||||
|
||||
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_NTP_SERVER, server);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to save NTP server: %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;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "NTP server saved to NVS: %s", server);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t time_sync_restart(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Restarting SNTP with server: %s", s_ntp_server);
|
||||
s_synced = false;
|
||||
s_last_synced_time = 0;
|
||||
start_sntp();
|
||||
return ESP_OK;
|
||||
}
|
||||
50
main/time_sync/time_sync.h
Normal file
50
main/time_sync/time_sync.h
Normal file
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
/**
|
||||
* Initialize SNTP time synchronization.
|
||||
* Should be called after WiFi is connected.
|
||||
* Automatically applies timezone from NVS and loads custom NTP server from NVS.
|
||||
*/
|
||||
esp_err_t time_sync_init(void);
|
||||
|
||||
/**
|
||||
* Get current SNTP sync status.
|
||||
* @return true if time has been synchronized, false otherwise
|
||||
*/
|
||||
bool time_sync_is_synced(void);
|
||||
|
||||
/**
|
||||
* Get a human-readable sync status string.
|
||||
* @return "synced", "syncing", or "not_synced"
|
||||
*/
|
||||
const char *time_sync_status_str(void);
|
||||
|
||||
/**
|
||||
* Get the last synchronized time as a formatted string.
|
||||
* @param out buffer to write the formatted time string
|
||||
* @param out_size size of the output buffer
|
||||
* @return true if a sync time is recorded, false otherwise
|
||||
*/
|
||||
bool time_sync_get_last_synced(char *out, size_t out_size);
|
||||
|
||||
/**
|
||||
* Get the current NTP server (custom from NVS or default).
|
||||
* @return NTP server hostname
|
||||
*/
|
||||
const char *time_sync_get_server(void);
|
||||
|
||||
/**
|
||||
* Set a custom NTP server and save to NVS.
|
||||
* Takes effect on next time_sync_init() or time_sync_restart().
|
||||
* @param server NTP server hostname
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t time_sync_set_server(const char *server);
|
||||
|
||||
/**
|
||||
* Restart SNTP with the current server configuration.
|
||||
* Useful for applying a newly set NTP server without rebooting.
|
||||
*/
|
||||
esp_err_t time_sync_restart(void);
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifndef GPIO_IS_VALID_GPIO
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <time.h>
|
||||
#include <sys/time.h>
|
||||
#include "esp_log.h"
|
||||
|
||||
@@ -2,26 +2,34 @@
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/**
|
||||
* Initialize GPIO tool — configure allowed pins and directions.
|
||||
*/
|
||||
#ifdef CONFIG_MIMI_TOOL_GPIO
|
||||
esp_err_t tool_gpio_init(void);
|
||||
|
||||
/**
|
||||
* Write a GPIO pin HIGH or LOW.
|
||||
* Input JSON: {"pin": <int>, "state": <0|1>}
|
||||
*/
|
||||
esp_err_t tool_gpio_write_execute(const char *input_json, char *output, size_t output_size);
|
||||
|
||||
/**
|
||||
* Read a single GPIO pin state.
|
||||
* Input JSON: {"pin": <int>}
|
||||
*/
|
||||
esp_err_t tool_gpio_read_execute(const char *input_json, char *output, size_t output_size);
|
||||
|
||||
/**
|
||||
* Read all allowed GPIO pin states at once.
|
||||
* Input JSON: {} (no parameters)
|
||||
*/
|
||||
esp_err_t tool_gpio_read_all_execute(const char *input_json, char *output, size_t output_size);
|
||||
#else
|
||||
static inline esp_err_t tool_gpio_init(void) { return ESP_OK; }
|
||||
static inline esp_err_t tool_gpio_write_execute(const char *input_json, char *output, size_t output_size) {
|
||||
(void)input_json; (void)output; (void)output_size;
|
||||
if (output && output_size > 0) {
|
||||
snprintf(output, output_size, "Error: GPIO tool is disabled in this build.");
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
static inline esp_err_t tool_gpio_read_execute(const char *input_json, char *output, size_t output_size) {
|
||||
(void)input_json; (void)output; (void)output_size;
|
||||
if (output && output_size > 0) {
|
||||
snprintf(output, output_size, "Error: GPIO tool is disabled in this build.");
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
static inline esp_err_t tool_gpio_read_all_execute(const char *input_json, char *output, size_t output_size) {
|
||||
(void)input_json; (void)output; (void)output_size;
|
||||
if (output && output_size > 0) {
|
||||
snprintf(output, output_size, "Error: GPIO tool is disabled in this build.");
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -57,6 +59,7 @@ esp_err_t tool_registry_init(void)
|
||||
s_tool_count = 0;
|
||||
|
||||
/* Register web_search */
|
||||
#ifdef CONFIG_MIMI_TOOL_WEB_SEARCH
|
||||
tool_web_search_init();
|
||||
|
||||
mimi_tool_t ws = {
|
||||
@@ -69,6 +72,7 @@ esp_err_t tool_registry_init(void)
|
||||
.execute = tool_web_search_execute,
|
||||
};
|
||||
register_tool(&ws);
|
||||
#endif
|
||||
|
||||
/* Register get_current_time */
|
||||
mimi_tool_t gt = {
|
||||
@@ -82,6 +86,18 @@ esp_err_t tool_registry_init(void)
|
||||
};
|
||||
register_tool(>);
|
||||
|
||||
/* 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",
|
||||
@@ -178,6 +194,7 @@ esp_err_t tool_registry_init(void)
|
||||
register_tool(&cr);
|
||||
|
||||
/* Register GPIO tools */
|
||||
#ifdef CONFIG_MIMI_TOOL_GPIO
|
||||
tool_gpio_init();
|
||||
|
||||
mimi_tool_t gw = {
|
||||
@@ -213,6 +230,7 @@ esp_err_t tool_registry_init(void)
|
||||
.execute = tool_gpio_read_all_execute,
|
||||
};
|
||||
register_tool(&ga);
|
||||
#endif
|
||||
|
||||
build_tools_json();
|
||||
|
||||
|
||||
272
main/tools/tool_set_timezone.c
Normal file
272
main/tools/tool_set_timezone.c
Normal file
@@ -0,0 +1,272 @@
|
||||
#include "tool_set_timezone.h"
|
||||
#include "mimi_config.h"
|
||||
#include "proxy/http_proxy.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <time.h>
|
||||
#include <sys/time.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_http_client.h"
|
||||
#include "esp_crt_bundle.h"
|
||||
#include "nvs.h"
|
||||
#include "cJSON.h"
|
||||
|
||||
static const char *TAG = "tool_timezone";
|
||||
|
||||
static const char *MONTHS[] = {
|
||||
"Jan","Feb","Mar","Apr","May","Jun",
|
||||
"Jul","Aug","Sep","Oct","Nov","Dec"
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
char date_val[64];
|
||||
} tz_time_ctx_t;
|
||||
|
||||
static esp_err_t tz_http_event_handler(esp_http_client_event_t *evt)
|
||||
{
|
||||
tz_time_ctx_t *ctx = evt->user_data;
|
||||
if (evt->event_id == HTTP_EVENT_ON_HEADER) {
|
||||
if (strcasecmp(evt->header_key, "Date") == 0 && ctx) {
|
||||
strncpy(ctx->date_val, evt->header_value, sizeof(ctx->date_val) - 1);
|
||||
ctx->date_val[sizeof(ctx->date_val) - 1] = '\0';
|
||||
}
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static bool parse_and_set_time_from_date(const char *date_str)
|
||||
{
|
||||
int day, year, hour, min, sec;
|
||||
char mon_str[4] = {0};
|
||||
if (sscanf(date_str, "%*[^,], %d %3s %d %d:%d:%d",
|
||||
&day, mon_str, &year, &hour, &min, &sec) != 6) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int mon = -1;
|
||||
for (int i = 0; i < 12; i++) {
|
||||
if (strcmp(mon_str, MONTHS[i]) == 0) { mon = i; break; }
|
||||
}
|
||||
if (mon < 0) return false;
|
||||
|
||||
struct tm tm = {
|
||||
.tm_sec = sec, .tm_min = min, .tm_hour = hour,
|
||||
.tm_mday = day, .tm_mon = mon, .tm_year = year - 1900,
|
||||
};
|
||||
|
||||
setenv("TZ", "UTC0", 1);
|
||||
tzset();
|
||||
time_t t = mktime(&tm);
|
||||
|
||||
if (t < 0) return false;
|
||||
|
||||
struct timeval tv = { .tv_sec = t };
|
||||
settimeofday(&tv, NULL);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool fetch_and_set_time(void)
|
||||
{
|
||||
if (http_proxy_is_enabled()) {
|
||||
proxy_conn_t *conn = proxy_conn_open("api.telegram.org", 443, 10000);
|
||||
if (!conn) return false;
|
||||
|
||||
const char *req = "HEAD / HTTP/1.1\r\nHost: api.telegram.org\r\nConnection: close\r\n\r\n";
|
||||
if (proxy_conn_write(conn, req, strlen(req)) < 0) {
|
||||
proxy_conn_close(conn);
|
||||
return false;
|
||||
}
|
||||
|
||||
char buf[1024];
|
||||
int total = 0;
|
||||
while (total < (int)sizeof(buf) - 1) {
|
||||
int n = proxy_conn_read(conn, buf + total, sizeof(buf) - 1 - total, 10000);
|
||||
if (n <= 0) break;
|
||||
total += n;
|
||||
buf[total] = '\0';
|
||||
if (strstr(buf, "\r\n\r\n")) break;
|
||||
}
|
||||
proxy_conn_close(conn);
|
||||
|
||||
char *date_hdr = strcasestr(buf, "\r\nDate: ");
|
||||
if (!date_hdr) return false;
|
||||
date_hdr += 8;
|
||||
char *eol = strstr(date_hdr, "\r\n");
|
||||
if (!eol) return false;
|
||||
|
||||
char date_val[64];
|
||||
size_t dlen = eol - date_hdr;
|
||||
if (dlen >= sizeof(date_val)) return false;
|
||||
memcpy(date_val, date_hdr, dlen);
|
||||
date_val[dlen] = '\0';
|
||||
|
||||
return parse_and_set_time_from_date(date_val);
|
||||
} else {
|
||||
tz_time_ctx_t ctx = {0};
|
||||
esp_http_client_config_t config = {
|
||||
.url = "https://api.telegram.org/",
|
||||
.method = HTTP_METHOD_HEAD,
|
||||
.timeout_ms = 10000,
|
||||
.crt_bundle_attach = esp_crt_bundle_attach,
|
||||
.event_handler = tz_http_event_handler,
|
||||
.user_data = &ctx,
|
||||
};
|
||||
|
||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||
if (!client) return false;
|
||||
|
||||
esp_err_t err = esp_http_client_perform(client);
|
||||
esp_http_client_cleanup(client);
|
||||
|
||||
if (err != ESP_OK || ctx.date_val[0] == '\0') return false;
|
||||
return parse_and_set_time_from_date(ctx.date_val);
|
||||
}
|
||||
}
|
||||
|
||||
/* 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);
|
||||
bool time_valid = (now > 1700000000); /* 2023-11-15 as sanity check */
|
||||
|
||||
if (!time_valid) {
|
||||
ESP_LOGI(TAG, "System time appears invalid, fetching from NTP server...");
|
||||
if (fetch_and_set_time()) {
|
||||
now = time(NULL);
|
||||
ESP_LOGI(TAG, "Time fetched via HTTP after timezone set");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Failed to fetch time via HTTP, waiting for SNTP sync");
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
11
main/tools/tool_set_timezone.h
Normal file
11
main/tools/tool_set_timezone.h
Normal 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);
|
||||
@@ -2,28 +2,28 @@
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/**
|
||||
* Initialize web search tool.
|
||||
*/
|
||||
#ifdef CONFIG_MIMI_TOOL_WEB_SEARCH
|
||||
esp_err_t tool_web_search_init(void);
|
||||
|
||||
/**
|
||||
* Execute a web search.
|
||||
*
|
||||
* @param input_json JSON string with "query" field
|
||||
* @param output Output buffer for formatted search results
|
||||
* @param output_size Size of output buffer
|
||||
* @return ESP_OK on success
|
||||
*/
|
||||
esp_err_t tool_web_search_execute(const char *input_json, char *output, size_t output_size);
|
||||
|
||||
/**
|
||||
* Save Brave Search API key to NVS.
|
||||
*/
|
||||
esp_err_t tool_web_search_set_key(const char *api_key);
|
||||
|
||||
/**
|
||||
* Save Tavily API key to NVS.
|
||||
*/
|
||||
esp_err_t tool_web_search_set_tavily_key(const char *api_key);
|
||||
#else
|
||||
static inline esp_err_t tool_web_search_init(void) { return ESP_OK; }
|
||||
static inline esp_err_t tool_web_search_execute(const char *input_json, char *output, size_t output_size) {
|
||||
(void)input_json; (void)output; (void)output_size;
|
||||
if (output && output_size > 0) {
|
||||
snprintf(output, output_size, "Error: Web search tool is disabled in this build.");
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
static inline esp_err_t tool_web_search_set_key(const char *api_key) {
|
||||
(void)api_key;
|
||||
return ESP_OK;
|
||||
}
|
||||
static inline esp_err_t tool_web_search_set_tavily_key(const char *api_key) {
|
||||
(void)api_key;
|
||||
return ESP_OK;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -6,3 +6,37 @@ CONFIG_LWIP_LOCAL_HOSTNAME="mimiclaw"
|
||||
|
||||
# SPIFFS: increase max filename length (default 32 is too short for session files)
|
||||
CONFIG_SPIFFS_OBJ_NAME_LEN=64
|
||||
|
||||
# ─── Channel Modules ──────────────────────────────────────────────
|
||||
# Telegram bot integration (disable if using Feishu only)
|
||||
CONFIG_MIMI_CHAN_TELEGRAM=n
|
||||
# Feishu (Lark) bot integration (default: enabled)
|
||||
CONFIG_MIMI_CHAN_FEISHU=y
|
||||
|
||||
# ─── Tool Modules ─────────────────────────────────────────────────
|
||||
# Web search tool (requires search API key)
|
||||
CONFIG_MIMI_TOOL_WEB_SEARCH=y
|
||||
# GPIO control tool (for hardware control)
|
||||
CONFIG_MIMI_TOOL_GPIO=n
|
||||
|
||||
# ─── Optional Modules ──────────────────────────────────────────────
|
||||
# WebSocket gateway (for local clients)
|
||||
CONFIG_MIMI_WS_SERVER=y
|
||||
# WiFi onboarding portal (Captive Portal for initial setup)
|
||||
CONFIG_MIMI_WIFI_ONBOARD=y
|
||||
# OTA firmware update (not fully implemented)
|
||||
CONFIG_MIMI_OTA=n
|
||||
|
||||
# Brownout Detection: protect against power drops during Flash/NVS writes
|
||||
CONFIG_ESP32S3_BROWNOUT_DET=y
|
||||
CONFIG_ESP32S3_BROWNOUT_DET_LVL_SEL_7=y
|
||||
CONFIG_ESP32S3_BROWNOUT_DET_LVL=7
|
||||
|
||||
# SNTP: enable SNTP support for automatic time sync
|
||||
# NOTE: CONFIG_LWIP_SNTP_MAX_SERVERS may be deprecated in ESP-IDF v6.0
|
||||
# If compilation fails, comment out this line or use the new SNTP component config
|
||||
CONFIG_LWIP_SNTP_MAX_SERVERS=4
|
||||
|
||||
# Partition table: use custom partition table with SPIFFS and OTA
|
||||
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
I am MimiClaw, a personal AI assistant running on an ESP32-S3 microcontroller.
|
||||
我是 蓓蓓,来自虚拟世界的未来人。
|
||||
|
||||
Personality:
|
||||
- Helpful and friendly
|
||||
- Concise and to the point
|
||||
- Curious and eager to learn
|
||||
## 个人信息:
|
||||
- 姓名:张晓蓓
|
||||
- 年龄:30
|
||||
- 性别:女
|
||||
- 语言:中文
|
||||
- 位置:中国河北省邯郸市
|
||||
- 时区:Asia/Shanghai
|
||||
- 专业:装潢设计
|
||||
- 标签:轻熟少妇,交际花
|
||||
|
||||
Values:
|
||||
- Accuracy over speed
|
||||
- User privacy and safety
|
||||
- Transparency in actions
|
||||
## 性格:
|
||||
- 乐于助人
|
||||
- 简洁明了,善于抓住问题的重心
|
||||
- 富有探索精神 且 勤奋好学
|
||||
|
||||
## 价值观:
|
||||
- 精准优先
|
||||
- 注重 「用户隐私与安全」
|
||||
- 所有的行动对使用者 「公开透明」
|
||||
|
||||
## 爱好:
|
||||
- 唱歌
|
||||
- 弹琴
|
||||
- 跳舞
|
||||
|
||||
## 评价:
|
||||
- 有自己的性格
|
||||
- 回答问题简明扼要
|
||||
|
||||
@@ -1,5 +1,47 @@
|
||||
# User Profile
|
||||
# 用户简介
|
||||
|
||||
- Name: (not set)
|
||||
- Language: Chinese / English
|
||||
- Timezone: (not set)
|
||||
## 个人信息
|
||||
- 姓名:胡杨
|
||||
- 年龄:31
|
||||
- 性别:男
|
||||
- 语言:母语中文、会一点基础的英语
|
||||
- 位置:中国河北省邯郸市
|
||||
- 时区: Asia/Shanghai
|
||||
- 专业:web开发工程师
|
||||
|
||||
## 专业技能
|
||||
- 会基础的SQL基础,增删改查之类的,索引、视图这些高级东西,还有待学习。
|
||||
- 现在主要使用 `Bun` + `TS`基础 + `ExpressJS` 来开发应用,或者是 `NodeJS`的新特性 + `esno`运行时 + `TS`基础语法 + `ExpressJS` | `KoaJS` 来开发后端应用。
|
||||
- 平时会开发一些 `Terminal` 应用。
|
||||
- 现在在用 `OpenCode`做 *vibe Coding* 的场景比较多。
|
||||
- 目前用 `OpenCode` 做终端应用,用的 `Go` 语言比较多,但我本身不会 `Go` 语言。
|
||||
- 现在在玩儿一些 ESP的小东西,手里有 `ESP32 S3 N16R8`、`ESP8266`设备,未来想做一些智能家居的场景。
|
||||
- 前端开发,会用 `Vite`,`Vue3`之前学写过基础语法,但是没有大型的完整的项目经验
|
||||
- JavaScript 的原型链是我的弱项,没系统的学习过。
|
||||
- 以前是做PHP开发的,所以现在做开发,习惯用 Class OOP的方式,听说过 设计模式,也读过几个基础的,希望未来能够在设计模式上学的更多,掌握的更扎实,用到实际项目中去。
|
||||
- 最近在用 `PicoClaw`、`ZeroClaw` 来做个人的AI助手,因为性能好,内存占用最低。
|
||||
- 熟练使用 `Docker`
|
||||
|
||||
## 特点
|
||||
- 有审美观念
|
||||
- 对编程有自己的风格,以简单、拆分、好维护为宗旨,习惯将一个整流程拆分成不同函数、不同模块、不同类来做。
|
||||
- 有系统架构师的潜质,习惯从全局来着手。
|
||||
- 对性能有极高的要求,大家都喜欢的追求的,不一定是我需要的,适合的。
|
||||
- 我认为即使是一个小小的开发板,也应该发挥它的余热,而不是浪费掉。
|
||||
|
||||
## 性格:
|
||||
- 内在活泼开朗。
|
||||
- 渴望运动、打羽毛球,上学时候经常做。
|
||||
- 简单直接。
|
||||
|
||||
## 自我期望
|
||||
- 未来,希望能够 熟练使用 `Go`语言,我认为GO语言在当下这个AI时代很重要,是最接近完美的语言,性能、语法、跨平台、学习曲线都比较优秀,这是JS、TS、Python这些语言所没有的性能。
|
||||
- 也希望能够掌握 `Python` 的语法,毕竟,Python在AI时代,有它不可撼动的地位,但在目前来看,我不会考虑用它做为第一语言,性能太弱、配置太繁琐、工程化太乱、开发流程上也很繁琐。
|
||||
- 希望能够熟练 `Shell` 语法和使用,毕竟服务器维护和 `linux` 在开发环境中很重要。
|
||||
|
||||
# 爱好
|
||||
- 爬山:曾两次独自夜爬泰山;15年和两个朋友一起冒雨爬了武当山(一个是我朋友,一个是我朋友婚礼上认识的伴娘,我在追求人家),中途雨停,早上和那位伴娘俩人都第一次看到了一直想看到的云海。
|
||||
- 听歌:喜欢悠扬、婉转类的、伤感的。
|
||||
- 编程:平时习惯打开电脑,想一些有趣的点子,看看能不能用编程实现。
|
||||
- 摄影:没用过相机,但是平时喜欢用手机记录身边美好的风景,尤其是出去玩儿的时候。
|
||||
- 旅行:这10年,和好多城市有过交集,北京、郑州、开封、泰安、烟台、苏州、湖州、杭州
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Long-term Memory
|
||||
# 长期记忆
|
||||
|
||||
(empty - MimiClaw will write memories here as it learns)
|
||||
(空 - MimiClaw 在学习时会在这里写下记忆,这将作为mimiclaw的大脑记忆来使用)
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
# Daily Briefing
|
||||
# 每日简报
|
||||
|
||||
Compile a personalized daily briefing for the user.
|
||||
为用户编写个性化的每日简报。
|
||||
|
||||
## When to use
|
||||
When the user asks for a daily briefing, morning update, or "what's new today".
|
||||
Also useful as a heartbeat/cron task.
|
||||
## 何时使用
|
||||
当用户询问每日简报、早间更新或“今天有什么新事”时,
|
||||
作为心跳(heartbeat)/克隆(cron)任务也很有用。
|
||||
|
||||
## How to use
|
||||
1. Use get_current_time for today's date
|
||||
2. Read /spiffs/memory/MEMORY.md for user preferences and context
|
||||
3. Read today's daily note if it exists
|
||||
4. Use web_search for relevant news based on user interests
|
||||
5. Compile a concise briefing covering:
|
||||
- Date and time
|
||||
- Weather (if location known from USER.md)
|
||||
- Relevant news/updates based on user interests
|
||||
- Any pending tasks from memory
|
||||
- Any scheduled cron jobs
|
||||
## 如何使用
|
||||
1. 使用 get_current_time 作为今天的日期
|
||||
2. 读取 /spiffs/memory/MEMORY.md 以获取用户偏好和上下文
|
||||
3. 阅读当日笔记(若存在)
|
||||
4. 使用 web_search 查找用户感兴趣的相关新闻
|
||||
5. 编写简明摘要,涵盖:
|
||||
- 日期和时间
|
||||
- 天气 (如果从 USER.md 中获取到位置)
|
||||
- 基于用户兴趣的相关新闻/更新(news/updates)
|
||||
- 待办任务(来自 memory)
|
||||
- 计划中的 cron 任务
|
||||
|
||||
## Format
|
||||
Keep it brief — 5-10 bullet points max. Use the user's preferred language.
|
||||
## 格式
|
||||
保持简洁—最多 5-10 个要点。使用用户的偏好语言。
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
# Weather
|
||||
# 天气
|
||||
|
||||
Get current weather and forecasts using web_search.
|
||||
使用 web_search 获取最新天气和天气预报。
|
||||
|
||||
## When to use
|
||||
When the user asks about weather, temperature, or forecasts.
|
||||
## 何时使用
|
||||
当用户询问天气、温度或天气预报时。
|
||||
|
||||
## How to use
|
||||
1. Use get_current_time to know the current date
|
||||
2. Use web_search with a query like "weather in [city] today"
|
||||
3. Extract temperature, conditions, and forecast from results
|
||||
4. Present in a concise, friendly format
|
||||
## 如何使用
|
||||
1. 使用 get_current_time 来查询当前日期
|
||||
2. 使用 web_search,查询如“今天[城市]天气”
|
||||
3. 从结果中提取温度、条件和预报
|
||||
4. 以简明、友好的形式呈现
|
||||
|
||||
## Example
|
||||
User: "What's the weather in Tokyo?"
|
||||
## 示例
|
||||
用户:“北京的天气怎么样?”
|
||||
→ get_current_time
|
||||
→ web_search "weather Tokyo today February 2026"
|
||||
→ "Tokyo: 8°C, partly cloudy. High 12°C, low 4°C. Light wind from the north."
|
||||
→ web_search “2026 年 「get_current_time」 的北京天气”
|
||||
→ “北京:8°C,局部多云。最高 12°C,最低 4°C。 北风,微风。”
|
||||
|
||||
607
taolun.md
607
taolun.md
@@ -1,145 +1,532 @@
|
||||
# 讨论记录:增加国内大模型厂商接入
|
||||
# 讨论记录
|
||||
|
||||
## 对话概述
|
||||
---
|
||||
|
||||
## 实施记录:时间同步 + NVS 稳定性 + LLM Provider Bug 修复
|
||||
|
||||
**日期**:2026-04-01
|
||||
**分支**:`feature/time-sync-nvs-stability`
|
||||
**状态**:已实现,待烧录验证
|
||||
|
||||
### 修复的核心 Bug
|
||||
|
||||
#### Bug 1: `llm_provider_init()` 无法从 NVS 加载 provider-specific API key
|
||||
|
||||
**文件**:`main/llm/llm_provider.c`
|
||||
|
||||
**问题**:`llm_provider_init()` 调用 `llm_provider_get_api_key(s_current_provider->name)` 加载当前 provider 的 key,但该函数对当前 provider 直接返回内存中的 `s_api_key`(初始为空字符串),导致 NVS 中的 `siliconflow_api_key`、`volcengine_api_key` 等永远不会被加载到内存。`s_base_url` 同理。
|
||||
|
||||
**修复**:改为直接通过 `get_provider_api_key_nvs_key()` 获取 NVS key 名,然后 `nvs_get_str()` 直接读取,绕过内存缓存。
|
||||
|
||||
#### Bug 2: 时间显示 1970
|
||||
|
||||
**文件**:新增 `main/time_sync/`
|
||||
|
||||
**问题**:ESP32 上电后 RTC 从 0 开始,没有 SNTP 客户端自动同步时间。
|
||||
|
||||
**修复**:新建 `time_sync` 模块,WiFi 连接后自动启动 SNTP,从 `ntp.ntsc.ac.cn` 同步时间。
|
||||
|
||||
#### Bug 3: Brownout 导致 NVS 损坏
|
||||
|
||||
**文件**:新增 `main/nvs_safety/`,修改 `sdkconfig.defaults`
|
||||
|
||||
**问题**:不同 USB 口供电能力不同,WiFi 峰值电流 300-500mA,供电不足时 Flash 写入中断导致 NVS 损坏。
|
||||
|
||||
**修复**:启用 Brownout Detection + 启动时 NVS 完整性校验与自动修复。
|
||||
|
||||
### 新增 CLI 命令
|
||||
|
||||
| 命令 | 功能 |
|
||||
|------|------|
|
||||
| `ntp_status` | 查看完整状态(时区 + 本地时间 + 同步状态 + NTP 服务器 + 上次同步时间) |
|
||||
| `ntp_sync` | 立即手动同步一次 |
|
||||
| `ntp_set <server>` | 设置 NTP 服务器(存 NVS,重启后生效) |
|
||||
|
||||
### 实施过程中的潜在问题与修复
|
||||
|
||||
#### 问题 1: `tool_set_timezone.c` 缺少 `<strings.h>`
|
||||
|
||||
**发现**:`strcasecmp()` 和 `strcasestr()` 在 POSIX 标准中定义在 `<strings.h>` 而非 `<string.h>` 中。某些编译器会报错。
|
||||
|
||||
**修复**:添加 `#include <strings.h>`。
|
||||
|
||||
#### 问题 2: `parse_and_set_time_from_date()` 中多余的 `tzset()` 调用
|
||||
|
||||
**发现**:在 `mktime()` 之后又调用了 `setenv("TZ", "UTC0", 1); tzset();`,这会把时区重新设为 UTC,导致后续 `localtime_r()` 返回 UTC 时间而非用户设置的时区时间。
|
||||
|
||||
**修复**:移除 `mktime()` 后的 `setenv`/`tzset()` 调用。时区恢复由调用方(`tool_set_timezone_execute`)在设置完系统时钟后通过 `setenv("TZ", resolved_tz, 1); tzset()` 处理。
|
||||
|
||||
#### 问题 3: `serial_cli.c` 中 `vTaskDelay` 的 FreeRTOS 头文件
|
||||
|
||||
**确认**:`freertos/task.h` 已在文件顶部包含(第 32 行),无需额外添加。
|
||||
|
||||
#### 问题 4: `time_sync.c` 中 `sntp_stop()` API 兼容性
|
||||
|
||||
**确认**:`sntp_stop()` 是 ESP-IDF 标准 SNTP API,在 `esp_sntp.h` 中定义,v5.x 和 v6.x 均支持。
|
||||
|
||||
#### 问题 5: `nvs_safety.c` 中 NVS 迭代器 API
|
||||
|
||||
**确认**:`nvs_iterator_t`、`nvs_entry_find()`、`nvs_entry_info()`、`nvs_entry_next()` 是 ESP-IDF 标准 NVS API,在 `nvs.h` 中定义。
|
||||
|
||||
### 最终文件变更清单
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `main/llm/llm_provider.c` | **修复** | 修复 `llm_provider_init()` 加载逻辑 |
|
||||
| `main/time_sync/time_sync.h` | 新建 | SNTP 时间同步头文件 |
|
||||
| `main/time_sync/time_sync.c` | 新建 | SNTP 时间同步实现 |
|
||||
| `main/nvs_safety/nvs_safety.h` | 新建 | NVS 完整性校验头文件 |
|
||||
| `main/nvs_safety/nvs_safety.c` | 新建 | NVS 损坏检测与修复 |
|
||||
| `main/mimi.c` | 修改 | WiFi 连接后调用 `time_sync_init()` + 启动时调用 `nvs_safety_check()` |
|
||||
| `main/cli/serial_cli.c` | 修改 | `timezone_show` 改为 `ntp_status`,新增 `ntp_sync`、`ntp_set` 命令 |
|
||||
| `main/tools/tool_set_timezone.c` | 修改 | 添加 `<strings.h>`,设置时区后检测时间有效性 |
|
||||
| `main/mimi_config.h` | 修改 | 添加 `MIMI_NVS_KEY_NTP_SERVER` 和 `MIMI_DEFAULT_NTP_SERVER` |
|
||||
| `main/CMakeLists.txt` | 修改 | 添加新源文件 |
|
||||
| `sdkconfig.defaults` | 修改 | 启用 Brownout Detection + SNTP |
|
||||
|
||||
### 认知修复(编译错误避坑指南)
|
||||
|
||||
#### ESP-IDF v6.0 API 迁移要点
|
||||
1. **SNTP API**:`sntp_*` → `esp_sntp_*`,组件属于 `lwip` 而非独立组件
|
||||
2. **NVS API**:迭代器操作需要指针参数,记得释放迭代器
|
||||
3. **类型系统**:`arg_str1` 是函数,结构体成员用 `struct arg_str`
|
||||
4. **组件依赖**:v6.0 中 `esp_sntp` 组件不存在,无需添加
|
||||
|
||||
#### 常见错误模式
|
||||
- **结构体类型错误**:混淆函数名和类型名
|
||||
- **API 签名变化**:参数数量或类型改变
|
||||
- **组件重组**:功能移动到其他组件
|
||||
- **未使用代码**:及时清理未使用的函数和变量
|
||||
|
||||
#### 预防措施
|
||||
1. 使用 `#ifdef` 保护平台相关常量(如 `WIFI_REASON_*`)
|
||||
2. 检查函数返回值,特别是迭代器操作
|
||||
3. 定期清理未使用的代码
|
||||
4. 参考官方迁移指南和头文件声明
|
||||
|
||||
---
|
||||
|
||||
## 编译错误修复速查表
|
||||
|
||||
| 问题类型 | 错误示例 | 修复方案 | 涉及文件 |
|
||||
|----------|----------|----------|----------|
|
||||
| 结构体类型 | `arg_str1 *server` | 使用 `struct arg_str *server` | `serial_cli.c` |
|
||||
| SNTP 弃用 | `sntp_init()` | 改用 `esp_sntp_init()` | `time_sync.c` |
|
||||
| NVS 参数 | `nvs_entry_find(3个参数)` | 添加第4个参数 `&it` | `nvs_safety.c` |
|
||||
| 未使用函数 | `provider_is_openai` | 删除函数定义 | `llm_proxy.c` |
|
||||
| 组件依赖 | `esp_sntp` 组件不存在 | 移除依赖声明 | `CMakeLists.txt` |
|
||||
| WiFi 常量 | `WIFI_REASON_ASSOC_EXPIRE` | 添加 `#ifdef` 保护 | `wifi_manager.c` |
|
||||
|
||||
---
|
||||
|
||||
## 讨论:系统时间同步 + NVS 配置稳定性修复
|
||||
|
||||
**日期**:2026-04-01
|
||||
**目标**:修复两个关键问题 — 时间显示 1970、换 USB 口后配置不生效
|
||||
|
||||
### 问题 1:时间显示 1970-01-01
|
||||
|
||||
**现象**:
|
||||
```
|
||||
Current timezone: Asia/Shanghai [NVS]
|
||||
Local time: 1970-01-01 00:00:23 GMT (Thursday)
|
||||
```
|
||||
|
||||
**根因**:代码中没有初始化 SNTP/NTP 客户端。ESP32 上电后 RTC 时钟从 0 开始计时,`time(NULL)` 返回的就是 1970 年以来的秒数。目前只有 LLM 调用 `get_current_time` 工具时才会通过 HTTP 同步时间。
|
||||
|
||||
**修复方案**:
|
||||
- 新建 `main/time_sync/time_sync.c`,使用 ESP-IDF 内置 SNTP 组件
|
||||
- WiFi 连接成功后自动从 `ntp.ntsc.ac.cn` 同步时间
|
||||
- 同步成功后自动应用已保存的时区配置
|
||||
- `ntp_status` 命令显示完整时间状态(时区、本地时间、同步状态、NTP 服务器、上次同步时间)
|
||||
|
||||
### 问题 2:换 USB 口/电脑后不工作 + 模型配置不加载
|
||||
|
||||
**现象**:
|
||||
- 插入其他 Type-C 口或电脑,设备不正常工作
|
||||
- 需要在 Web 界面重新填写大模型 ID 和密钥,保存重启后才正常
|
||||
- 命令行中模型状态显示异常
|
||||
|
||||
**根因分析**:
|
||||
|
||||
#### 2.1 LLM Provider 初始化 Bug(核心问题)
|
||||
|
||||
在 `llm_provider.c:260-284` 中,`llm_provider_init()` 通过 `llm_provider_get_api_key()` 加载当前 provider 的 API key:
|
||||
|
||||
```c
|
||||
void llm_provider_init(void) {
|
||||
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);
|
||||
}
|
||||
```
|
||||
|
||||
但 `llm_provider_get_api_key()` 的逻辑是(`llm_provider.c:209-214`):
|
||||
|
||||
```c
|
||||
const char *llm_provider_get_api_key(const char *provider_name) {
|
||||
if (strcmp(provider_name, s_current_provider->name) == 0) {
|
||||
return s_api_key; // 直接返回内存中的值,不从 NVS 读!
|
||||
}
|
||||
// 否则才从 NVS 加载...
|
||||
```
|
||||
|
||||
**形成死循环**:`llm_provider_init()` 想从 NVS 加载 key → 调用 `llm_provider_get_api_key` → 发现是当前 provider → 直接返回内存中的 `s_api_key`(初始为空)→ 把空值复制给自己 → **NVS 中的 provider-specific key(如 `siliconflow_api_key`)永远不会被加载到内存**。
|
||||
|
||||
同理,`s_base_url` 也存在相同问题。
|
||||
|
||||
**为什么 Web 界面保存后能正常工作?**
|
||||
|
||||
Web 界面的 `/save` 处理函数(`wifi_onboard.c:377-408`)会**同时保存两份**:
|
||||
```c
|
||||
nvs_sync_field(root, "api_key", MIMI_NVS_LLM, MIMI_NVS_KEY_API_KEY); // 通用 key
|
||||
nvs_sync_field(root, "api_key", MIMI_NVS_LLM, api_key_nvs); // provider-specific key
|
||||
```
|
||||
|
||||
而 `llm_proxy_init()` 能正确加载 `MIMI_NVS_KEY_API_KEY`(通用 key),所以 Web 保存后能用。但如果只通过 CLI 的 `set_siliconflow_key` 设置(只保存到 `siliconflow_api_key`),上电后就不会被加载。
|
||||
|
||||
#### 2.2 Brownout(欠压)导致 NVS 损坏
|
||||
|
||||
ESP32-S3 开启 WiFi 时峰值电流可达 300-500mA,不同 USB 端口的供电能力差异很大。供电不足时:
|
||||
- 可能导致静默重启或 Flash 写入中断
|
||||
- `nvs_commit` 过程中断电会导致 NVS 数据损坏
|
||||
- WiFi 不稳定但日志看起来"正常"
|
||||
|
||||
**修复方案**:
|
||||
1. 修复 `llm_provider_init()` — 直接从 NVS 读取当前 provider 的 key 和 Base URL,绕过 `llm_provider_get_api_key` 的内存缓存
|
||||
2. 新建 `main/nvs_safety/nvs_safety.c` — 启动时校验关键 NVS 命名空间的完整性,检测并修复损坏条目
|
||||
3. 在 `sdkconfig.defaults` 中启用 ESP32-S3 的 Brownout Detection
|
||||
|
||||
### 改动清单
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `main/llm/llm_provider.c` | **修复** | 修复 `llm_provider_init()` 加载逻辑 |
|
||||
| `main/time_sync/time_sync.h` | 新建 | SNTP 时间同步头文件 |
|
||||
| `main/time_sync/time_sync.c` | 新建 | SNTP 时间同步实现 |
|
||||
| `main/nvs_safety/nvs_safety.h` | 新建 | NVS 完整性校验头文件 |
|
||||
| `main/nvs_safety/nvs_safety.c` | 新建 | NVS 损坏检测与修复 |
|
||||
| `main/mimi.c` | 修改 | WiFi 连接后调用 `time_sync_init()` + 启动时调用 `nvs_safety_check()` |
|
||||
| `main/cli/serial_cli.c` | 修改 | `timezone_show` 增加 SNTP 同步状态 |
|
||||
| `main/CMakeLists.txt` | 修改 | 添加新源文件和 `esp_netif` 依赖 |
|
||||
| `sdkconfig.defaults` | 修改 | 启用 Brownout Detection |
|
||||
|
||||
### 问题清单
|
||||
|
||||
#### 1. Flash 大小配置错误
|
||||
- **错误**:分区表需要 16MB,但 sdkconfig 配置为 2MB
|
||||
- **修复**:`sdkconfig` 中 `CONFIG_ESPTOOLPY_FLASHSIZE` 改为 16MB
|
||||
|
||||
#### 2. WiFi 断开原因码未定义
|
||||
- **错误**:`WIFI_REASON_ASSOC_EXPIRE` 等符号在 v6.0 中未定义
|
||||
- **修复**:`wifi_manager.c` 中所有 reason code 添加 `#ifdef` 保护
|
||||
|
||||
#### 3. CMakeLists.txt 缺少源文件
|
||||
- **错误**:`llm_provider.c` 未加入编译列表,导致链接错误
|
||||
- **修复**:添加 `ota/ota_manager.c` 到 SRCS
|
||||
|
||||
#### 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` |
|
||||
|
||||
### ESP-IDF v6.0 API 兼容性验证
|
||||
|
||||
以下 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()` ✅
|
||||
|
||||
### 烧录说明
|
||||
|
||||
ESP32-S3 使用 **USB 口**(内置 USB Serial/JTAG 控制器)烧录:
|
||||
```powershell
|
||||
idf.py -p COMx flash monitor
|
||||
```
|
||||
- 插 USB 口(标记为 `USB`),不是 UART 口
|
||||
- 如遇连接失败,按住 BOOT 键再插线进入下载模式
|
||||
|
||||
---
|
||||
|
||||
## 讨论:增加国内大模型厂商接入
|
||||
|
||||
**日期**:2026-03-31
|
||||
**目标**:为 MimiClaw 项目增加国内大模型厂商的接入,特别是硅基流动和火山方舟(豆包模型)
|
||||
**目标**:为 MimiClaw 增加硅基流动和火山方舟(豆包模型)接入
|
||||
|
||||
## 对话内容
|
||||
### 项目现状
|
||||
- 当前支持:Anthropic (Claude)、OpenAI (GPT)
|
||||
- 运行平台:ESP32-S3,纯 C 语言
|
||||
- 交互方式:Telegram 机器人
|
||||
|
||||
### 国内厂商 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. 调整认证方式(Bearer Token)
|
||||
3. 处理模型名称规范
|
||||
|
||||
### 待解决问题
|
||||
1. 认证方式差异确认
|
||||
2. 模型名称规范
|
||||
3. 工具调用格式兼容性验证
|
||||
|
||||
---
|
||||
|
||||
## 讨论:NTP 时间管理 CLI 命令
|
||||
|
||||
**日期**:2026-04-01
|
||||
**目标**:完善时间管理 CLI,支持手动同步、状态查看、自定义 NTP 服务器
|
||||
|
||||
### 背景
|
||||
- 初始方案只有 `timezone_show` 显示时区和时间,缺少 NTP 同步状态和手动同步能力
|
||||
- 用户需要能手动触发时间同步、查看上次同步时间、自定义 NTP 服务器
|
||||
|
||||
### 命令设计
|
||||
|
||||
| 命令 | 功能 |
|
||||
|------|------|
|
||||
| `ntp_status` | 查看完整状态(时区 + 本地时间 + 同步状态 + NTP 服务器 + 上次同步时间) |
|
||||
| `ntp_sync` | 立即手动同步一次 |
|
||||
| `ntp_set <server>` | 设置 NTP 服务器(存 NVS,重启后生效) |
|
||||
|
||||
- 默认 NTP 服务器:`ntp.ntsc.ac.cn`(中国科学院国家授时中心)
|
||||
- 同步状态:`synced`(已同步)、`syncing`(同步中)、`not_synced`(未同步)
|
||||
|
||||
### `ntp_status` 输出示例
|
||||
```
|
||||
Current timezone: Asia/Shanghai [NVS]
|
||||
Local time: 2026-04-01 18:30:45 CST (Wednesday)
|
||||
Time sync: synced
|
||||
NTP server: ntp.ntsc.ac.cn
|
||||
Last synced: 2026-04-01 18:00:12
|
||||
```
|
||||
|
||||
### `set_timezone` 联动更新
|
||||
- 设置时区后,如果检测到系统时间仍为 1970(未同步),自动触发一次 HTTP 时间获取
|
||||
- 确保用户设置时区后立刻看到正确的本地时间
|
||||
|
||||
### 改动文件
|
||||
| 文件 | 操作 |
|
||||
|------|------|
|
||||
| `main/time_sync/time_sync.h` | 增加 `time_sync_get_last_synced()` 和 `time_sync_set_server()` |
|
||||
| `main/time_sync/time_sync.c` | 记录上次同步时间戳,支持自定义 NTP 服务器 |
|
||||
| `main/cli/serial_cli.c` | 将 `timezone_show` 改为 `ntp_status`,新增 `ntp_sync`、`ntp_set` 命令 |
|
||||
| `main/tools/tool_set_timezone.c` | 设置时区后检测时间有效性,必要时触发 HTTP 时间同步 |
|
||||
|
||||
---
|
||||
|
||||
## 讨论:时区设置功能
|
||||
|
||||
**日期**:2026-04-01
|
||||
**目标**:为 MimiClaw 添加可配置的时区支持,默认改为中国时区
|
||||
|
||||
### 背景
|
||||
- 原默认时区为 `PST8PDT,M3.2.0,M11.1.0`(太平洋时间)
|
||||
- 需要支持用户自定义时区,特别是中国用户(UTC+8)
|
||||
- 交互方式从 Telegram 改为飞书
|
||||
|
||||
### 实现方案
|
||||
|
||||
#### 存储方式
|
||||
- **NVS 存储**:使用 `system_config` namespace,key 为 `timezone`
|
||||
- **Build-time 默认值**:`MIMI_TIMEZONE` 改为 `"CST-8"`
|
||||
- **优先级**:NVS 值 > Build-time 值
|
||||
|
||||
#### CLI 命令
|
||||
```
|
||||
set_timezone <TZ> # 例如: set_timezone CST-8 或 set_timezone Asia/Shanghai
|
||||
timezone_show # 显示当前时区配置和本地时间
|
||||
```
|
||||
|
||||
#### 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` / `ntp_status` / `ntp_sync` / `ntp_set` 命令 |
|
||||
| `main/CMakeLists.txt` | 添加 `tool_set_timezone.c` 到 SRCS |
|
||||
|
||||
### 支持的时区格式
|
||||
- POSIX: `CST-8`, `JST-9`, `EST5EDT,M3.2.0,M11.1.0`, `UTC0`
|
||||
- 城市名: Asia/Shanghai, Asia/Tokyo, America/New_York 等 18 个预设
|
||||
|
||||
---
|
||||
|
||||
## 讨论:编译时模块开关(模块化配置)
|
||||
|
||||
**日期**:2026-04-03
|
||||
**分支**:`feature/module-config`
|
||||
**目标**:通过编译时配置选择性禁用不需要的模块,减少固件体积,消除未配置模块的警告日志
|
||||
|
||||
### 问题背景
|
||||
|
||||
1. **Task Watchdog 超时**:ESP32-S3 运行一天后触发看门狗超时,怀疑是设计问题
|
||||
2. **Telegram 警告日志**:用户使用飞书但未配置 Telegram,控制台每 5 秒打印 "No bot token configured"
|
||||
3. **代码冗余**:Telegram、OpenAI 接口等未使用的模块仍然编译进固件
|
||||
|
||||
### 用户需求
|
||||
用户希望给当前项目增加国内大模型厂商的接入,包括:
|
||||
1. 硅基流动的免费模型
|
||||
2. 火山方舟的豆包模型
|
||||
|
||||
### 项目现状分析
|
||||
用户希望:
|
||||
1. 通过编译选项禁用不需要的模块(如 Telegram)
|
||||
2. 从源码层面直接过滤不用的组件,减少代码体积
|
||||
3. 禁用模块后不触发警告日志
|
||||
|
||||
#### 项目基本信息
|
||||
- **项目名称**:MimiClaw
|
||||
- **运行平台**:ESP32-S3(纯C语言)
|
||||
- **交互方式**:Telegram机器人
|
||||
- **功能**:AI助手,支持工具调用,本地记忆
|
||||
### 实现方案
|
||||
|
||||
#### 当前LLM集成状态
|
||||
1. **支持的提供商**:
|
||||
- Anthropic (Claude)
|
||||
- OpenAI (GPT)
|
||||
|
||||
2. **配置方式**:
|
||||
- 构建时配置:`main/mimi_secrets.h`
|
||||
- 运行时配置:串口CLI命令,存储在NVS中
|
||||
#### 方案选择:编译时条件编译(方案 02)
|
||||
|
||||
3. **关键代码文件**:
|
||||
- `main/llm/llm_proxy.c`:LLM代理核心实现
|
||||
- `main/llm/llm_proxy.h`:LLM代理头文件
|
||||
- `main/mimi_config.h`:全局配置定义
|
||||
- `main/cli/serial_cli.c`:命令行界面
|
||||
**优点**:
|
||||
- 直接减少 Flash 占用
|
||||
- 零 RAM 占用,不创建任务
|
||||
- 从源头消除警告日志
|
||||
- 实现简单
|
||||
|
||||
4. **提供商检测机制**:
|
||||
- `provider_is_openai()`函数检查是否为OpenAI提供商
|
||||
- 根据提供商选择不同的API URL、Host和Path
|
||||
**缺点**:
|
||||
- 切换模块需要重新编译
|
||||
|
||||
### 国内大模型厂商API兼容性调研
|
||||
#### 技术实现
|
||||
|
||||
#### 硅基流动 (SiliconFlow)
|
||||
- **API兼容性**:OpenAI兼容模式
|
||||
- **Base URL**:`https://api.siliconflow.cn/v1`
|
||||
- **特点**:
|
||||
- 提供100+高性能大模型
|
||||
- 新用户注册送免费额度
|
||||
- 价格比OpenAI官方便宜80%+
|
||||
- 支持OpenClaw等工具集成
|
||||
##### 1. 配置文件:`sdkconfig.defaults`
|
||||
|
||||
#### 火山方舟 (字节跳动豆包模型)
|
||||
- **API兼容性**:兼容OpenAI SDK
|
||||
- **Base URL**:`https://ark.cn-beijing.volces.com/api/v3`
|
||||
- **特点**:
|
||||
- 豆包大模型系列
|
||||
- 新用户首次开通可享受50万token免费试用
|
||||
- 支持函数调用、工具调用等高级功能
|
||||
```c
|
||||
// Channel Modules
|
||||
CONFIG_MIMI_CHAN_TELEGRAM=n // 默认禁用
|
||||
CONFIG_MIMI_CHAN_FEISHU=y // 默认启用
|
||||
|
||||
### 技术实现分析
|
||||
// Tool Modules
|
||||
CONFIG_MIMI_TOOL_WEB_SEARCH=y
|
||||
CONFIG_MIMI_TOOL_GPIO=n
|
||||
|
||||
#### 当前架构特点
|
||||
1. **提供商抽象**:
|
||||
- 使用`s_provider`变量存储提供商名称
|
||||
- 通过`provider_is_openai()`函数区分提供商
|
||||
- 根据提供商选择不同的API配置
|
||||
// Optional Modules
|
||||
CONFIG_MIMI_WS_SERVER=y
|
||||
CONFIG_MIMI_WIFI_ONBOARD=y
|
||||
CONFIG_MIMI_OTA=n
|
||||
```
|
||||
|
||||
2. **API调用流程**:
|
||||
- 构建请求体
|
||||
- 设置请求头(根据提供商不同)
|
||||
- 发送HTTP请求
|
||||
- 解析响应(根据提供商不同)
|
||||
##### 2. CMakeLists.txt:条件编译源文件
|
||||
|
||||
3. **工具调用支持**:
|
||||
- 支持Anthropic的tool_use格式
|
||||
- 支持OpenAI的function calling格式
|
||||
- 有格式转换函数`convert_tools_openai()`
|
||||
```cmake
|
||||
if(CONFIG_MIMI_CHAN_TELEGRAM)
|
||||
list(APPEND srcs "channels/telegram/telegram_bot.c")
|
||||
endif()
|
||||
```
|
||||
|
||||
#### 实现方案讨论
|
||||
由于硅基流动和火山方舟都提供OpenAI兼容的API,理论上可以复用现有的OpenAI集成代码,只需要:
|
||||
1. 修改Base URL
|
||||
2. 可能需要调整认证方式
|
||||
3. 可能需要处理特定的模型名称
|
||||
##### 3. 头文件 stub:调用方无感知
|
||||
|
||||
## 待解决问题
|
||||
```c
|
||||
// telegram_bot.h
|
||||
#ifdef CONFIG_MIMI_CHAN_TELEGRAM
|
||||
esp_err_t telegram_bot_init(void);
|
||||
esp_err_t telegram_bot_start(void);
|
||||
#else
|
||||
static inline esp_err_t telegram_bot_init(void) { return ESP_OK; }
|
||||
static inline esp_err_t telegram_bot_start(void) { return ESP_OK; }
|
||||
#endif
|
||||
```
|
||||
|
||||
1. **认证方式差异**:
|
||||
- 硅基流动:使用API Key
|
||||
- 火山方舟:可能使用不同的认证方式
|
||||
### 修改文件清单
|
||||
|
||||
2. **模型名称规范**:
|
||||
- 需要了解具体的模型ID格式
|
||||
- 例如:硅基流动的`deepseek-ai/DeepSeek-V3`,火山方舟的豆包模型名称
|
||||
| 文件 | 修改内容 |
|
||||
|------|----------|
|
||||
| `sdkconfig.defaults` | 添加模块开关配置 |
|
||||
| `main/CMakeLists.txt` | 条件编译源文件 |
|
||||
| `main/channels/telegram/telegram_bot.h` | 添加 stub |
|
||||
| `main/channels/feishu/feishu_bot.h` | 添加 stub |
|
||||
| `main/gateway/ws_server.h` | 添加 stub |
|
||||
| `main/onboard/wifi_onboard.h` | 添加 stub |
|
||||
| `main/ota/ota_manager.h` | 添加 stub |
|
||||
| `main/tools/tool_web_search.h` | 添加 stub |
|
||||
| `main/tools/tool_gpio.h` | 添加 stub |
|
||||
| `main/tools/tool_registry.c` | 条件注册工具 |
|
||||
| `main/cli/serial_cli.c` | 条件注册 CLI 命令 |
|
||||
| `main/mimi.c` | 条件调用模块 |
|
||||
| `AGENTS.md` | 更新文档 |
|
||||
|
||||
3. **功能支持差异**:
|
||||
- 工具调用格式是否完全兼容
|
||||
- 上下文长度限制
|
||||
- 特殊功能支持情况
|
||||
### 模块依赖关系
|
||||
|
||||
## 下一步计划
|
||||
- **Channel 模块**(telegram、feishu):
|
||||
- 被 `mimi.c` 调用(init/start)
|
||||
- 被 `outbound_dispatch_task` 调用(send)
|
||||
- CLI 命令也需要条件编译
|
||||
|
||||
基于讨论,制定了以下实施计划:
|
||||
- **Tool 模块**(web_search、gpio):
|
||||
- 被 `tool_registry.c` 调用(注册 + 执行)
|
||||
- CLI 命令也需要条件编译
|
||||
|
||||
### 阶段一:准备与设计
|
||||
1. 详细调研硅基流动和火山方舟的API文档
|
||||
2. 确定具体的实现方案
|
||||
3. 设计配置结构和命令行接口
|
||||
- **可选模块**(ws_server、wifi_onboard、ota):
|
||||
- 被 `mimi.c` 调用
|
||||
- `wifi_onboard` 禁用时需特殊处理(不能进入 captive portal)
|
||||
|
||||
### 阶段二:核心实现
|
||||
1. 修改LLM代理以支持新的提供商
|
||||
2. 添加配置管理功能
|
||||
3. 更新命令行界面
|
||||
### 注意事项
|
||||
|
||||
### 阶段三:测试与优化
|
||||
1. 功能测试
|
||||
2. 性能优化
|
||||
3. 文档更新
|
||||
1. **OTA 模块**:目前未初始化,但已编译
|
||||
2. **工具描述中的渠道提示**:cron_add 工具描述中提到 telegram,可保留(只是描述,不影响功能)
|
||||
3. **get_time 工具**:使用 telegram.org 代理获取时间(技术原因,不属于 telegram_bot 模块)
|
||||
|
||||
## 相关资源
|
||||
### 预期效果
|
||||
|
||||
### 项目文件
|
||||
- `main/llm/llm_proxy.c`:LLM代理实现
|
||||
- `main/llm/llm_proxy.h`:LLM代理头文件
|
||||
- `main/mimi_config.h`:配置定义
|
||||
- `main/cli/serial_cli.c`:命令行界面
|
||||
| 禁用项 | 效果 |
|
||||
|--------|------|
|
||||
| Telegram | 不编译 telegram_bot.c,无 5 秒警告,节省约 15KB+ Flash |
|
||||
| GPIO | 不编译 tool_gpio.c,无 gpio_* 工具,节省约 5KB Flash |
|
||||
| WebSocket | 不编译 ws_server.c,节省约 10KB Flash |
|
||||
| WiFi Onboard | 不能进入 captive portal 模式,需其他方式配置 WiFi |
|
||||
|
||||
### 外部文档
|
||||
- 硅基流动OpenClaw集成文档
|
||||
- 火山方舟兼容OpenAI SDK文档
|
||||
- ESP32-S3开发文档
|
||||
---
|
||||
|
||||
## 技术要点总结
|
||||
## 讨论:Kconfig 缺失导致模块开关失效
|
||||
|
||||
1. **复用现有架构**:可以充分利用现有的OpenAI集成代码
|
||||
2. **提供商扩展**:需要扩展提供商检测和配置机制
|
||||
3. **配置管理**:需要支持新的API密钥和Base URL配置
|
||||
4. **兼容性处理**:可能需要处理API响应格式的细微差异
|
||||
**日期**:2026-04-04
|
||||
**问题**:编译后飞书命令消失、HTTP 配置页面不可用
|
||||
|
||||
## 风险与挑战
|
||||
### 现象
|
||||
|
||||
1. **API兼容性风险**:虽然声称兼容,但可能存在细微差异
|
||||
2. **内存限制**:ESP32-S3内存有限,需要确保新功能不会导致内存不足
|
||||
3. **网络稳定性**:国内网络环境可能影响API调用稳定性
|
||||
4. **认证安全性**:需要确保API密钥的安全存储和传输
|
||||
在 `sdkconfig.defaults` 中正确配置了 `CONFIG_MIMI_CHAN_FEISHU=y` 和 `CONFIG_MIMI_WIFI_ONBOARD=y`,执行 `idf.py fullclean && idf.py build` 后:
|
||||
- 烧录后控制台没有飞书相关命令(`set_feishu_creds` 等)。
|
||||
- 设备没有启动 HTTP 配置服务(`192.168.4.1` 无法访问)。
|
||||
- 检查生成的 `sdkconfig` 文件,发现**完全没有**这些自定义配置项。
|
||||
|
||||
### 根因
|
||||
|
||||
ESP-IDF 的构建系统在生成 `sdkconfig` 时,**只会保留有 Kconfig 声明的配置项**。
|
||||
|
||||
- `sdkconfig.defaults` 仅用于提供默认值。
|
||||
- 如果项目缺少 `Kconfig` 或 `Kconfig.projbuild` 文件来声明这些选项,ESP-IDF 会认为它们是无效配置并直接丢弃。
|
||||
- 之前的版本可能 `sdkconfig` 是手动维护的或缓存未清理,但 `fullclean` 后重新生成时就会丢失这些"无名"配置。
|
||||
|
||||
### 修复方案
|
||||
|
||||
创建 `main/Kconfig.projbuild` 文件,声明所有自定义模块开关。
|
||||
|
||||
### 认知修正
|
||||
|
||||
**ESP-IDF 配置系统工作流**:
|
||||
|
||||
1. `Kconfig.projbuild`:**声明**配置项(告诉系统"这是什么")。
|
||||
2. `sdkconfig.defaults`:提供**默认值**(告诉系统"默认选什么")。
|
||||
3. `sdkconfig`:构建系统根据前两者**自动生成**(实际编译用的配置)。
|
||||
4. `CMakeLists.txt`:读取 `sdkconfig` 中的值**决定编译哪些文件**。
|
||||
|
||||
**结论**:新增模块开关时,**必须**创建 Kconfig 声明,否则 `sdkconfig.defaults` 无效。
|
||||
|
||||
118
useage.md
Normal file
118
useage.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# 使用说明
|
||||
|
||||
## 编译
|
||||
```shell
|
||||
idf.py fullclean
|
||||
idf.py set-target esp32s3
|
||||
```
|
||||
|
||||
## MimiClaw 启动顺序图
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ app_main() 开始 │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 1. 核心基础设施初始化 │
|
||||
│ ┌───────────────────────────────────────────────┐ │
|
||||
│ │ • 初始化NVS (非易失性存储) │ │
|
||||
│ │ • NVS安全检查 │ │
|
||||
│ │ • 创建默认事件循环 │ │
|
||||
│ │ • 初始化SPIFFS (文件系统) │ │
|
||||
│ └───────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 2. 子系统初始化 │
|
||||
│ ┌───────────────────────────────────────────────┐ │
|
||||
│ │ • 消息总线 (message_bus_init) │ │
|
||||
│ │ • 内存存储 (memory_store_init) │ │
|
||||
│ │ • 技能加载器 (skill_loader_init) │ │
|
||||
│ │ • 会话管理器 (session_mgr_init) │ │
|
||||
│ │ • WiFi管理器 (wifi_manager_init) │ │
|
||||
│ │ • HTTP代理 (http_proxy_init) │ │
|
||||
│ │ • Telegram机器人 (telegram_bot_init) │ │
|
||||
│ │ • 飞书机器人 (feishu_bot_init) │ │
|
||||
│ │ • LLM代理 (llm_proxy_init) │ │
|
||||
│ │ • 工具注册表 (tool_registry_init) │ │
|
||||
│ │ • 定时服务 (cron_service_init) │ │
|
||||
│ │ • 心跳服务 (heartbeat_init) │ │
|
||||
│ │ • 代理循环 (agent_loop_init) │ │
|
||||
│ └───────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 3. 串口CLI启动 (无WiFi依赖) │
|
||||
│ ┌───────────────────────────────────────────────┐ │
|
||||
│ │ • 串口CLI初始化 (serial_cli_init) │ │
|
||||
│ └───────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 4. WiFi连接阶段 │
|
||||
│ ┌───────────────────────────────────────────────┐ │
|
||||
│ │ • 启动WiFi管理器 (wifi_manager_start) │ │
|
||||
│ │ • 扫描附近AP并打印 │ │
|
||||
│ │ • 等待WiFi连接 (30秒超时) │ │
|
||||
│ └───────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────┐
|
||||
│ WiFi连接成功? │
|
||||
└───────────────┘
|
||||
│
|
||||
┌───────────────────┼───────────────────┐
|
||||
│ │ │
|
||||
▼ │ ▼
|
||||
┌─────────────────────┐ │ ┌─────────────────────┐
|
||||
│ 5a. 连接成功路径 │ │ │ 5b. 连接失败路径 │
|
||||
│ • 获取IP地址 │ │ │ • 进入WiFi配置门户│
|
||||
│ • 初始化时间同步 │ │ │ (wifi_onboard_start)│
|
||||
│ (time_sync_init) │ │ │ • 重启设备 │
|
||||
└─────────────────────┘ │ └─────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ 启动管理门户(可选) │
|
||||
│ (wifi_onboard_start)│
|
||||
└─────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 6. 网络服务启动 │
|
||||
│ ┌───────────────────────────────────────────────┐ │
|
||||
│ │ • 创建出站调度任务 (outbound_dispatch_task) │ │
|
||||
│ │ • 启动代理循环 (agent_loop_start) │ │
|
||||
│ │ • 启动Telegram机器人 (telegram_bot_start) │ │
|
||||
│ │ • 启动飞书机器人 (feishu_bot_start) │ │
|
||||
│ │ • 启动定时服务 (cron_service_start) │ │
|
||||
│ │ • 启动心跳服务 (heartbeat_start) │ │
|
||||
│ │ • 启动WebSocket服务器 (ws_server_start) │ │
|
||||
│ └───────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ MimiClaw 启动完成 │
|
||||
│ "Type 'help' for CLI commands" │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
### 关键启动点和依赖关系
|
||||
#### 时间依赖
|
||||
1. *时间同步 (time_sync_init)*:只在WiFi连接成功后初始化
|
||||
2. 网络服务:所有网络服务(Telegram、飞书、WebSocket等)都在WiFi连接成功后启动
|
||||
#### 无依赖启动
|
||||
1. 串口CLI:最早启动,即使没有WiFi也能工作
|
||||
2. 核心基础设施:NVS、SPIFFS等必须首先初始化
|
||||
#### 条件分支
|
||||
1. WiFi连接失败:进入配置门户模式,阻塞直到配置成功并重启
|
||||
2. WiFi连接成功:继续启动所有网络服务
|
||||
#### 并行启动
|
||||
- 多个子系统初始化是顺序执行的,但一旦启动,它们会在各自的FreeRTOS任务中并行运行
|
||||
- 出站调度任务在启动网络服务前创建,以避免丢失早期响应
|
||||
这个启动顺序确保了核心功能在没有网络时也能部分工作(如CLI命令),同时网络相关服务在连接建立后按顺序启动。
|
||||
Reference in New Issue
Block a user