Compare commits

...

3 Commits

Author SHA1 Message Date
c1368962cc fix: 添加 Kconfig.projbuild 修复模块开关失效问题
Some checks failed
Build / idf-build (push) Has been cancelled
- 创建 main/Kconfig.projbuild 声明所有自定义模块配置项
- 修复 fullclean 后 sdkconfig 丢失 CONFIG_MIMI_* 配置的问题
- 更新 AGENTS.md 添加认知修正栏目和模块开关文档
- 记录 Kconfig 踩坑讨论到 taolun.md
2026-04-04 06:44:52 +08:00
fa41de0ae8 Merge branch 'feature/module-config' into main (resolved conflicts)
Some checks failed
Build / idf-build (push) Has been cancelled
2026-04-03 20:17:40 +08:00
6983a1f1ba feat: 添加编译时模块开关配置
通过 sdkconfig.defaults 选择性启用/禁用模块,减少固件体积:

新增模块开关:
- CONFIG_MIMI_CHAN_TELEGRAM (默认 n)
- CONFIG_MIMI_CHAN_FEISHU (默认 y)
- CONFIG_MIMI_TOOL_WEB_SEARCH (默认 y)
- CONFIG_MIMI_TOOL_GPIO (默认 n)
- CONFIG_MIMI_WS_SERVER (默认 y)
- CONFIG_MIMI_WIFI_ONBOARD (默认 y)
- CONFIG_MIMI_OTA (默认 n)

技术实现:
- CMakeLists.txt 条件编译源文件
- 头文件使用 static inline stub
- CLI 命令和工具注册也支持条件编译

消除 Telegram 未配置时的 5 秒轮询警告日志
2026-04-03 20:15:26 +08:00
16 changed files with 566 additions and 156 deletions

View File

@@ -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
## 调试技巧

View File

@@ -1,9 +1,33 @@
# 变更日志
## v1.1.0(计划中)
## v1.2.0(计划中)
### 新增
- 国内大模型厂商接入支持(硅基流动、火山方舟)— 计划中
- **编译时模块开关**
- 通过 `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 命令和工具注册也支持条件编译
### 修复
- 消除 Telegram 未配置时的 5 秒轮询警告日志
---
## v1.1.0
### 新增
- 国内大模型厂商接入支持(硅基流动、火山方舟)
- **时区设置功能**
- 默认时区改为 `CST-8`(中国标准时间 UTC+8
- 新增 `set_timezone` CLI 命令(支持 POSIX 格式和城市名)

View File

@@ -1,36 +1,67 @@
idf_component_register(
SRCS
# 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"
"channels/telegram/telegram_bot.c"
"channels/feishu/feishu_bot.c"
"llm/llm_proxy.c"
"llm/llm_provider.c"
"agent/agent_loop.c"
"agent/context_builder.c"
"memory/memory_store.c"
"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_set_timezone.c"
"tools/tool_files.c"
"tools/tool_gpio.c"
"tools/gpio_policy.c"
"skills/skill_loader.c"
"onboard/wifi_onboard.c"
"ota/ota_manager.c"
"time_sync/time_sync.c"
"nvs_safety/nvs_safety.c"
INCLUDE_DIRS
"."
)
# ─── 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 ${core_srcs}
INCLUDE_DIRS "."
REQUIRES
nvs_flash esp_wifi esp_netif esp_http_client esp_http_server
esp_https_ota esp_event cjson spiffs console vfs app_update esp-tls

51
main/Kconfig.projbuild Normal file
View 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

View File

@@ -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

View File

@@ -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

View File

@@ -1075,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 = {
@@ -1084,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);
@@ -1108,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");
@@ -1263,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 = {
@@ -1283,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");
@@ -1384,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 = {
@@ -1393,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 = {

View File

@@ -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

View File

@@ -77,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 {
@@ -164,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. */
@@ -182,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!");
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -59,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 = {
@@ -71,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 = {
@@ -192,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 = {
@@ -227,6 +230,7 @@ esp_err_t tool_registry_init(void)
.execute = tool_gpio_read_all_execute,
};
register_tool(&ga);
#endif
build_tools_json();

View File

@@ -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

View File

@@ -7,6 +7,26 @@ 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
@@ -16,3 +36,7 @@ CONFIG_ESP32S3_BROWNOUT_DET_LVL=7
# 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"

159
taolun.md
View File

@@ -371,3 +371,162 @@ timezone_show # 显示当前时区配置和本地时间
### 支持的时区格式
- 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. 通过编译选项禁用不需要的模块(如 Telegram
2. 从源码层面直接过滤不用的组件,减少代码体积
3. 禁用模块后不触发警告日志
### 实现方案
#### 方案选择:编译时条件编译(方案 02
**优点**
- 直接减少 Flash 占用
- 零 RAM 占用,不创建任务
- 从源头消除警告日志
- 实现简单
**缺点**
- 切换模块需要重新编译
#### 技术实现
##### 1. 配置文件:`sdkconfig.defaults`
```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
// Optional Modules
CONFIG_MIMI_WS_SERVER=y
CONFIG_MIMI_WIFI_ONBOARD=y
CONFIG_MIMI_OTA=n
```
##### 2. CMakeLists.txt条件编译源文件
```cmake
if(CONFIG_MIMI_CHAN_TELEGRAM)
list(APPEND srcs "channels/telegram/telegram_bot.c")
endif()
```
##### 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
```
### 修改文件清单
| 文件 | 修改内容 |
|------|----------|
| `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` | 更新文档 |
### 模块依赖关系
- **Channel 模块**telegram、feishu
-`mimi.c` 调用init/start
-`outbound_dispatch_task` 调用send
- CLI 命令也需要条件编译
- **Tool 模块**web_search、gpio
-`tool_registry.c` 调用(注册 + 执行)
- CLI 命令也需要条件编译
- **可选模块**ws_server、wifi_onboard、ota
-`mimi.c` 调用
- `wifi_onboard` 禁用时需特殊处理(不能进入 captive portal
### 注意事项
1. **OTA 模块**:目前未初始化,但已编译
2. **工具描述中的渠道提示**cron_add 工具描述中提到 telegram可保留只是描述不影响功能
3. **get_time 工具**:使用 telegram.org 代理获取时间(技术原因,不属于 telegram_bot 模块)
### 预期效果
| 禁用项 | 效果 |
|--------|------|
| 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 |
---
## 讨论Kconfig 缺失导致模块开关失效
**日期**2026-04-04
**问题**编译后飞书命令消失、HTTP 配置页面不可用
### 现象
`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` 无效。