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 秒轮询警告日志
This commit is contained in:
76
AGENTS.md
76
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,62 @@ 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. 在 `sdkconfig.defaults` 中添加配置项:
|
||||
```
|
||||
CONFIG_MIMI_MODULE_EXAMPLE=y
|
||||
```
|
||||
|
||||
2. 在 `main/CMakeLists.txt` 中条件编译:
|
||||
```cmake
|
||||
if(CONFIG_MIMI_MODULE_EXAMPLE)
|
||||
list(APPEND srcs "modules/example/example.c")
|
||||
endif()
|
||||
```
|
||||
|
||||
3. 在模块头文件中添加 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 +142,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
|
||||
@@ -110,11 +169,14 @@ config_reset # 重置为构建时配置
|
||||
|
||||
## 开发注意事项
|
||||
|
||||
## 开发注意事项
|
||||
|
||||
1. **内存限制**:ESP32-S3 内存有限,使用 `MALLOC_CAP_SPIRAM` 分配大内存
|
||||
2. **堆栈大小**:任务堆栈在 `mimi_config.h` 中定义
|
||||
3. **日志**:使用 `ESP_LOG` 宏,标签在每个文件中定义
|
||||
4. **错误处理**:使用 `esp_err_t` 返回码
|
||||
5. **JSON 处理**:使用 cJSON 库
|
||||
6. **模块开关**:新增模块或可选模块应在 `sdkconfig.defaults` 中添加配置项,并在头文件中添加 `#ifdef` stub
|
||||
|
||||
## 调试技巧
|
||||
|
||||
|
||||
28
changelog.md
28
changelog.md
@@ -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 格式和城市名)
|
||||
|
||||
@@ -1,34 +1,65 @@
|
||||
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"
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1012,6 +1012,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 = {
|
||||
@@ -1021,8 +1022,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);
|
||||
@@ -1045,6 +1051,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");
|
||||
@@ -1200,6 +1212,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 = {
|
||||
@@ -1220,6 +1233,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");
|
||||
@@ -1302,6 +1321,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 = {
|
||||
@@ -1311,6 +1331,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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
26
main/mimi.c
26
main/mimi.c
@@ -75,24 +75,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 {
|
||||
@@ -160,13 +172,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. */
|
||||
@@ -178,11 +196,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!");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,3 +6,23 @@ 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
|
||||
|
||||
122
taolun.md
122
taolun.md
@@ -124,3 +124,125 @@ 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 |
|
||||
|
||||
Reference in New Issue
Block a user