feat(clawgent): Added Feishu and Anthropic components

This commit is contained in:
lvhaiyu
2026-04-07 20:37:49 +08:00
parent e313169248
commit c5a14bd136
27 changed files with 4420 additions and 76 deletions
@@ -4,7 +4,7 @@ Use this skill when the user wants the device to send an existing local non-imag
## When to use
- The user asks to send back a file, report, log, archive, JSON, CSV, or other non-image output.
- The target conversation is already the active Telegram, QQ, or WeChat chat, or the user provides an explicit target `chat_id`.
- The target conversation is already the active Telegram, QQ, Feishu, or WeChat chat, or the user provides an explicit target `chat_id`.
## Available tools
- `list_dir`: inspect device storage and confirm the file path
@@ -24,9 +24,9 @@ Use this skill when the user wants the device to send an existing local non-imag
## Sending rules
- Use this skill only for non-image files such as `.txt`, `.json`, `.log`, `.csv`, `.zip`, or archives.
- Use `cap_cli` with `cap call tg_send_file '<json>'` or `cap call qq_send_file '<json>'`.
- Use `cap_cli` with `cap call tg_send_file '<json>'`, `cap call qq_send_file '<json>'`, or `cap call feishu_send_file '<json>'`.
- The JSON payload should include an explicit `chat_id`, `path`, and optional `caption`.
- Use Telegram file capability for Telegram chats and QQ file capability for QQ chats.
- Use Telegram file capability for Telegram chats, QQ file capability for QQ chats, and Feishu file capability for Feishu chats.
- Do not claim WeChat generic file-send support unless a real `wechat_send_file` capability exists.
- Pass `caption` only when the user wants an accompanying message.
- The second argument of `cap call` must be one complete JSON string. Do not rewrite it as CLI flags or `key=value`.
@@ -43,10 +43,15 @@ Send a file to a QQ group through `cap_cli`:
cap call qq_send_file '{"chat_id":"group1234567890","path":"/spiffs/reports/status.json","caption":"Latest status report."}'
```
Send a file to a Feishu chat through `cap_cli`:
```text
cap call feishu_send_file '{"chat_id":"ou_xxx","path":"/fatfs/data/reports/status.json","caption":"Latest status report."}'
```
## Workflow
1. Confirm the target file exists with `list_dir` if needed.
2. Identify the target channel: Telegram, QQ, or WeChat.
3. Choose `tg_send_file` or `qq_send_file` based on the target channel.
2. Identify the target channel: Telegram, QQ, Feishu, or WeChat.
3. Choose `tg_send_file`, `qq_send_file`, or `feishu_send_file` based on the target channel.
4. For WeChat, stop and explain that this firmware variant does not expose `wechat_send_file`.
5. Execute the capability through `cap_cli` as `cap call <cap_name> '<json>'` with explicit `chat_id`, `path`, and optional `caption`.
6. Tell the user whether the send succeeded.
@@ -4,7 +4,7 @@ Use this skill when the user wants the device to send an existing local image ba
## When to use
- The user asks to send back a picture, image, photo, screenshot, camera frame, or visual result.
- The target conversation is already the active Telegram, QQ, or WeChat chat, or the user provides an explicit target `chat_id`.
- The target conversation is already the active Telegram, QQ, Feishu, or WeChat chat, or the user provides an explicit target `chat_id`.
## Available tools
- `list_dir`: inspect device storage and confirm the image path
@@ -17,9 +17,9 @@ Use this skill when the user wants the device to send an existing local image ba
## Sending rules
- Use this skill only for image files such as `.jpg`, `.jpeg`, `.png`, `.gif`, or `.webp`.
- Use `cap_cli` with `cap call tg_send_image '<json>'`, `cap call qq_send_image '<json>'`, or `cap call wechat_send_image '<json>'`.
- Use `cap_cli` with `cap call tg_send_image '<json>'`, `cap call qq_send_image '<json>'`, `cap call feishu_send_image '<json>'`, or `cap call wechat_send_image '<json>'`.
- The JSON payload should include an explicit `chat_id`, `path`, and optional `caption`.
- Use `tg_send_image` for Telegram chats, `qq_send_image` for QQ chats, and `wechat_send_image` for WeChat chats.
- Use `tg_send_image` for Telegram chats, `qq_send_image` for QQ chats, `feishu_send_image` for Feishu chats, and `wechat_send_image` for WeChat chats.
- Pass `caption` only when the user wants an accompanying message.
- The second argument of `cap call` must be one complete JSON string. Do not rewrite it as `--chat_id`, `path=...`, or other non-JSON forms.
@@ -35,6 +35,11 @@ Send an image to a QQ chat through `cap_cli`:
cap call qq_send_image '{"chat_id":"group123","path":"/spiffs/qq/capture.jpg","caption":"Here is the image."}'
```
Send an image to a Feishu chat through `cap_cli`:
```text
cap call feishu_send_image '{"chat_id":"ou_xxx","path":"/fatfs/data/inbox/feishu/photo.jpg","caption":"Here is the image."}'
```
Send an image to a WeChat chat through `cap_cli`:
```text
cap call wechat_send_image '{"chat_id":"room123","path":"/spiffs/wechat/capture.jpg","caption":"Here is the image."}'
@@ -42,8 +47,8 @@ cap call wechat_send_image '{"chat_id":"room123","path":"/spiffs/wechat/capture.
## Workflow
1. Confirm the target image exists with `list_dir` if needed.
2. Identify the target channel: Telegram, QQ, or WeChat.
3. Build the matching capability call: `tg_send_image`, `qq_send_image`, or `wechat_send_image`.
2. Identify the target channel: Telegram, QQ, Feishu, or WeChat.
3. Build the matching capability call: `tg_send_image`, `qq_send_image`, `feishu_send_image`, or `wechat_send_image`.
4. Execute it through `cap_cli` as `cap call <cap_name> '<json>'` with explicit `chat_id`, `path`, and optional `caption`.
5. Tell the user whether the send succeeded.
@@ -12,6 +12,7 @@ idf_component_register(
REQUIRES
cap_cli
cap_files
cap_im_feishu
cap_im_qq
cap_im_tg
cap_im_wechat
+64 -9
View File
@@ -12,41 +12,84 @@ config BASIC_DEMO_WIFI_PASSWORD
help
Default Wi-Fi password used to populate basic demo settings.
choice BASIC_DEMO_LLM_PROVIDER
prompt "Default LLM provider preset"
default BASIC_DEMO_LLM_PROVIDER_QWEN
help
Select the provider preset used to populate the shared LLM settings below.
OpenAI, Qwen Compatible, and Anthropic all reuse the same API key, model,
backend, profile, base URL, and auth fields.
config BASIC_DEMO_LLM_PROVIDER_OPENAI
bool "OpenAI"
config BASIC_DEMO_LLM_PROVIDER_QWEN
bool "Qwen Compatible"
config BASIC_DEMO_LLM_PROVIDER_ANTHROPIC
bool "Anthropic"
config BASIC_DEMO_LLM_PROVIDER_CUSTOM
bool "Custom"
endchoice
config BASIC_DEMO_LLM_API_KEY
string "Default LLM API key"
default ""
help
Default API key used to populate the basic demo LLM settings.
Default API key used to populate the shared LLM settings.
Anthropic reuses this field instead of having a dedicated provider-specific key.
config BASIC_DEMO_LLM_BACKEND_TYPE
string "Default LLM backend type"
default "openai_compatible"
default "openai_compatible" if BASIC_DEMO_LLM_PROVIDER_OPENAI
default "openai_compatible" if BASIC_DEMO_LLM_PROVIDER_QWEN
default "anthropic" if BASIC_DEMO_LLM_PROVIDER_ANTHROPIC
default "custom" if BASIC_DEMO_LLM_PROVIDER_CUSTOM
help
Default backend type for claw_core, for example openai_compatible or custom.
Default backend type for claw_core.
The provider preset above already fills this for standard providers.
config BASIC_DEMO_LLM_PROFILE
string "Default LLM profile"
default "qwen_compatible"
default "openai" if BASIC_DEMO_LLM_PROVIDER_OPENAI
default "qwen_compatible" if BASIC_DEMO_LLM_PROVIDER_QWEN
default "anthropic" if BASIC_DEMO_LLM_PROVIDER_ANTHROPIC
default "custom_openai_compatible" if BASIC_DEMO_LLM_PROVIDER_CUSTOM
help
Default runtime profile for claw_core, for example openai, qwen_compatible, or custom_openai_compatible.
Default runtime profile for claw_core.
The provider preset above already fills this for standard providers.
config BASIC_DEMO_LLM_MODEL
string "Default LLM model"
default "qwen3-max-2026-01-23"
default "" if BASIC_DEMO_LLM_PROVIDER_OPENAI
default "qwen3-max-2026-01-23" if BASIC_DEMO_LLM_PROVIDER_QWEN
default "" if BASIC_DEMO_LLM_PROVIDER_ANTHROPIC
default "" if BASIC_DEMO_LLM_PROVIDER_CUSTOM
help
Default model name for claw_core requests.
Set this explicitly for the provider you selected above.
config BASIC_DEMO_LLM_BASE_URL
string "Default LLM base URL"
default ""
default "https://api.openai.com" if BASIC_DEMO_LLM_PROVIDER_OPENAI
default "https://dashscope.aliyuncs.com" if BASIC_DEMO_LLM_PROVIDER_QWEN
default "https://api.anthropic.com" if BASIC_DEMO_LLM_PROVIDER_ANTHROPIC
default "" if BASIC_DEMO_LLM_PROVIDER_CUSTOM
help
Optional override for the selected LLM profile base URL.
Default base URL for the selected provider preset.
Leave the provider preset as Custom if you need to override this manually.
config BASIC_DEMO_LLM_AUTH_TYPE
string "Default LLM auth type"
default "bearer"
default "bearer" if BASIC_DEMO_LLM_PROVIDER_OPENAI
default "bearer" if BASIC_DEMO_LLM_PROVIDER_QWEN
default "none" if BASIC_DEMO_LLM_PROVIDER_ANTHROPIC
default "bearer" if BASIC_DEMO_LLM_PROVIDER_CUSTOM
help
Auth mode for the LLM backend, for example bearer, api-key, or none.
The provider preset above already fills this for standard providers.
config BASIC_DEMO_LLM_TIMEOUT_MS
string "Default LLM timeout ms"
@@ -66,6 +109,18 @@ config BASIC_DEMO_QQ_APP_SECRET
help
Default QQ app secret used by the IM cap.
config BASIC_DEMO_FEISHU_APP_ID
string "Default Feishu app ID"
default ""
help
Default Feishu app ID used by the IM cap.
config BASIC_DEMO_FEISHU_APP_SECRET
string "Default Feishu app secret"
default ""
help
Default Feishu app secret used by the IM cap.
config BASIC_DEMO_TG_BOT_TOKEN
string "Default Telegram bot token"
default ""
+93 -20
View File
@@ -5,11 +5,14 @@
*/
#include "app_clawgent.h"
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include "basic_demo_lua_modules.h"
#include "cap_cli.h"
#include "cap_files.h"
#include "cap_im_feishu.h"
#include "cap_im_qq.h"
#include "cap_im_tg.h"
#include "cap_im_wechat.h"
@@ -100,7 +103,7 @@ static esp_err_t init_skills(void)
static esp_err_t init_capabilities(const basic_demo_settings_t *settings)
{
claw_cap_config_t cap_config = {
.max_capabilities = 32,
.max_capabilities = 48,
.max_groups = 16,
};
@@ -215,6 +218,14 @@ static esp_err_t init_capabilities(const basic_demo_settings_t *settings)
}),
TAG,
"Failed to set WeChat attachment config");
ESP_RETURN_ON_ERROR(cap_im_feishu_set_attachment_config(
&(cap_im_feishu_attachment_config_t) {
.storage_root_dir = BASIC_DEMO_IM_ATTACHMENT_ROOT,
.max_inbound_file_bytes = BASIC_DEMO_IM_ATTACHMENT_MAX_BYTES,
.enable_inbound_attachments = true,
}),
TAG,
"Failed to set Feishu attachment config");
if (settings->qq_app_id[0] && settings->qq_app_secret[0]) {
ESP_RETURN_ON_ERROR(cap_im_qq_set_credentials(settings->qq_app_id,
@@ -223,6 +234,13 @@ static esp_err_t init_capabilities(const basic_demo_settings_t *settings)
"Failed to set QQ credentials");
}
if (settings->feishu_app_id[0] && settings->feishu_app_secret[0]) {
ESP_RETURN_ON_ERROR(cap_im_feishu_set_credentials(settings->feishu_app_id,
settings->feishu_app_secret),
TAG,
"Failed to set Feishu credentials");
}
if (settings->tg_bot_token[0]) {
ESP_RETURN_ON_ERROR(cap_im_tg_set_token(settings->tg_bot_token),
TAG,
@@ -254,6 +272,9 @@ static esp_err_t init_capabilities(const basic_demo_settings_t *settings)
}
ESP_RETURN_ON_ERROR(cap_im_qq_register_group(), TAG, "Failed to register QQ cap");
ESP_RETURN_ON_ERROR(cap_im_feishu_register_group(),
TAG,
"Failed to register Feishu cap");
ESP_RETURN_ON_ERROR(cap_im_tg_register_group(),
TAG,
"Failed to register Telegram cap");
@@ -293,23 +314,56 @@ static esp_err_t init_capabilities(const basic_demo_settings_t *settings)
return ESP_OK;
}
static const char *basic_demo_llm_provider_name(const basic_demo_settings_t *settings)
{
if (!settings) {
return "unknown";
}
if ((settings->llm_backend_type[0] && strcmp(settings->llm_backend_type, "anthropic") == 0) ||
(settings->llm_profile[0] && strcmp(settings->llm_profile, "anthropic") == 0)) {
return "Anthropic";
}
if (settings->llm_profile[0] &&
(strcmp(settings->llm_profile, "qwen") == 0 ||
strcmp(settings->llm_profile, "qwen_compatible") == 0)) {
return "Qwen Compatible";
}
if (settings->llm_profile[0] && strcmp(settings->llm_profile, "openai") == 0) {
return "OpenAI";
}
return "Custom";
}
static bool basic_demo_llm_is_configured(const basic_demo_settings_t *settings)
{
return settings &&
settings->llm_api_key[0] &&
settings->llm_model[0] &&
settings->llm_profile[0];
}
esp_err_t app_clawgent_start(const basic_demo_settings_t *settings)
{
claw_core_config_t core_config = {0};
claw_event_router_config_t router_config = {
.rules_path = BASIC_DEMO_AUTOMATION_RULES_PATH,
.task_stack_size = 6144,
.task_stack_size = 12 * 1024,
.task_priority = 5,
.task_core = tskNO_AFFINITY,
.core_submit_timeout_ms = 1000,
.core_receive_timeout_ms = 130000,
.default_route_messages_to_agent = true,
.default_route_messages_to_agent = false,
};
bool llm_enabled = false;
if (!settings) {
return ESP_ERR_INVALID_ARG;
}
llm_enabled = basic_demo_llm_is_configured(settings);
router_config.default_route_messages_to_agent = llm_enabled;
ESP_RETURN_ON_ERROR(claw_event_router_init(&router_config),
TAG,
"Failed to init event router");
@@ -319,6 +373,9 @@ esp_err_t app_clawgent_start(const basic_demo_settings_t *settings)
ESP_RETURN_ON_ERROR(claw_event_router_register_outbound_binding("qq", "qq_send_message"),
TAG,
"Failed to bind QQ outbound");
ESP_RETURN_ON_ERROR(claw_event_router_register_outbound_binding("feishu", "feishu_send_message"),
TAG,
"Failed to bind Feishu outbound");
ESP_RETURN_ON_ERROR(claw_event_router_register_outbound_binding("telegram", "tg_send_message"),
TAG,
"Failed to bind Telegram outbound");
@@ -344,23 +401,39 @@ esp_err_t app_clawgent_start(const basic_demo_settings_t *settings)
core_config.response_queue_len = 4;
core_config.max_context_providers = 5;
ESP_RETURN_ON_ERROR(claw_core_init(&core_config), TAG, "Failed to init claw_core");
ESP_RETURN_ON_ERROR(claw_core_add_context_provider(&claw_memory_long_term_provider),
TAG,
"Failed to add long-term memory provider");
ESP_RETURN_ON_ERROR(claw_core_add_context_provider(&claw_memory_session_history_provider),
TAG,
"Failed to add session history provider");
ESP_RETURN_ON_ERROR(claw_core_add_context_provider(&claw_skill_skills_list_provider),
TAG,
"Failed to add skills list provider");
ESP_RETURN_ON_ERROR(claw_core_add_context_provider(&claw_skill_active_skill_docs_provider),
TAG,
"Failed to add active skill docs provider");
ESP_RETURN_ON_ERROR(claw_core_add_context_provider(&claw_cap_tools_provider),
TAG,
"Failed to add cap tools provider");
ESP_RETURN_ON_ERROR(claw_core_start(), TAG, "Failed to start claw_core");
if (!llm_enabled) {
ESP_LOGW(TAG,
"LLM is not fully configured. Provider=%s profile=%s model=%s. "
"The demo will start without claw_core; ask, auto-route-to-agent, and image analysis stay disabled until LLM API key, profile, and model are set.",
basic_demo_llm_provider_name(settings),
settings->llm_profile[0] ? settings->llm_profile : "(empty)",
settings->llm_model[0] ? settings->llm_model : "(empty)");
} else {
ESP_LOGI(TAG,
"Starting LLM provider=%s profile=%s backend=%s model=%s",
basic_demo_llm_provider_name(settings),
settings->llm_profile,
settings->llm_backend_type[0] ? settings->llm_backend_type : "(default)",
settings->llm_model);
ESP_RETURN_ON_ERROR(claw_core_init(&core_config), TAG, "Failed to init claw_core");
ESP_RETURN_ON_ERROR(claw_core_add_context_provider(&claw_memory_long_term_provider),
TAG,
"Failed to add long-term memory provider");
ESP_RETURN_ON_ERROR(claw_core_add_context_provider(&claw_memory_session_history_provider),
TAG,
"Failed to add session history provider");
ESP_RETURN_ON_ERROR(claw_core_add_context_provider(&claw_skill_skills_list_provider),
TAG,
"Failed to add skills list provider");
ESP_RETURN_ON_ERROR(claw_core_add_context_provider(&claw_skill_active_skill_docs_provider),
TAG,
"Failed to add active skill docs provider");
ESP_RETURN_ON_ERROR(claw_core_add_context_provider(&claw_cap_tools_provider),
TAG,
"Failed to add cap tools provider");
ESP_RETURN_ON_ERROR(claw_core_start(), TAG, "Failed to start claw_core");
}
ESP_RETURN_ON_ERROR(claw_event_router_start(), TAG, "Failed to start event router");
ESP_RETURN_ON_ERROR(basic_demo_cli_start(), TAG, "Failed to start CLI");
@@ -9,6 +9,7 @@
#include <string.h>
#include "cap_im_qq.h"
#include "cmd_cap_im_feishu.h"
#include "cmd_cap_llm_inspect.h"
#include "cmd_cap_mcp_client.h"
#include "cmd_cap_mcp_server.h"
@@ -687,6 +688,7 @@ esp_err_t basic_demo_cli_start(void)
esp_console_register_help_command();
register_cap_im_qq();
register_cap_im_feishu();
register_cap_im_tg();
register_cap_im_wechat();
register_cap_lua();
@@ -44,6 +44,8 @@ static void settings_load_defaults(basic_demo_settings_t *settings)
settings_safe_copy(settings->llm_timeout_ms, sizeof(settings->llm_timeout_ms), BASIC_DEMO_LLM_TIMEOUT_MS);
settings_safe_copy(settings->qq_app_id, sizeof(settings->qq_app_id), BASIC_DEMO_QQ_APP_ID);
settings_safe_copy(settings->qq_app_secret, sizeof(settings->qq_app_secret), BASIC_DEMO_QQ_APP_SECRET);
settings_safe_copy(settings->feishu_app_id, sizeof(settings->feishu_app_id), BASIC_DEMO_FEISHU_APP_ID);
settings_safe_copy(settings->feishu_app_secret, sizeof(settings->feishu_app_secret), BASIC_DEMO_FEISHU_APP_SECRET);
settings_safe_copy(settings->tg_bot_token, sizeof(settings->tg_bot_token), BASIC_DEMO_TG_BOT_TOKEN);
settings_safe_copy(settings->wechat_token, sizeof(settings->wechat_token), BASIC_DEMO_WECHAT_TOKEN);
settings_safe_copy(settings->wechat_base_url, sizeof(settings->wechat_base_url), BASIC_DEMO_WECHAT_BASE_URL);
@@ -110,6 +112,8 @@ esp_err_t basic_demo_settings_load(basic_demo_settings_t *settings)
{ "llm_timeout_ms", settings->llm_timeout_ms, settings->llm_timeout_ms, sizeof(settings->llm_timeout_ms) },
{ "qq_app_id", settings->qq_app_id, settings->qq_app_id, sizeof(settings->qq_app_id) },
{ "qq_app_secret", settings->qq_app_secret, settings->qq_app_secret, sizeof(settings->qq_app_secret) },
{ "feishu_app_id", settings->feishu_app_id, settings->feishu_app_id, sizeof(settings->feishu_app_id) },
{ "feishu_app_secret", settings->feishu_app_secret, settings->feishu_app_secret, sizeof(settings->feishu_app_secret) },
{ "tg_bot_token", settings->tg_bot_token, settings->tg_bot_token, sizeof(settings->tg_bot_token) },
{ "wechat_token", settings->wechat_token, settings->wechat_token, sizeof(settings->wechat_token) },
{ "wechat_base_url", settings->wechat_base_url, settings->wechat_base_url, sizeof(settings->wechat_base_url) },
@@ -169,6 +173,8 @@ esp_err_t basic_demo_settings_save(const basic_demo_settings_t *settings)
{ "llm_timeout_ms", NULL, (char *)settings->llm_timeout_ms, sizeof(settings->llm_timeout_ms) },
{ "qq_app_id", NULL, (char *)settings->qq_app_id, sizeof(settings->qq_app_id) },
{ "qq_app_secret", NULL, (char *)settings->qq_app_secret, sizeof(settings->qq_app_secret) },
{ "feishu_app_id", NULL, (char *)settings->feishu_app_id, sizeof(settings->feishu_app_id) },
{ "feishu_app_secret", NULL, (char *)settings->feishu_app_secret, sizeof(settings->feishu_app_secret) },
{ "tg_bot_token", NULL, (char *)settings->tg_bot_token, sizeof(settings->tg_bot_token) },
{ "wechat_token", NULL, (char *)settings->wechat_token, sizeof(settings->wechat_token) },
{ "wechat_base_url", NULL, (char *)settings->wechat_base_url, sizeof(settings->wechat_base_url) },
@@ -7,7 +7,7 @@
#include "esp_err.h"
#define BASIC_DEMO_SETTINGS_STR_LEN 160
#define BASIC_DEMO_SETTINGS_STR_LEN 320
#define BASIC_DEMO_SETTINGS_LUA_DIR_LEN 64
#define BASIC_DEMO_SETTINGS_TIMEZONE_LEN 32
@@ -22,6 +22,8 @@
#define BASIC_DEMO_LLM_TIMEOUT_MS CONFIG_BASIC_DEMO_LLM_TIMEOUT_MS
#define BASIC_DEMO_QQ_APP_ID CONFIG_BASIC_DEMO_QQ_APP_ID
#define BASIC_DEMO_QQ_APP_SECRET CONFIG_BASIC_DEMO_QQ_APP_SECRET
#define BASIC_DEMO_FEISHU_APP_ID CONFIG_BASIC_DEMO_FEISHU_APP_ID
#define BASIC_DEMO_FEISHU_APP_SECRET CONFIG_BASIC_DEMO_FEISHU_APP_SECRET
#define BASIC_DEMO_TG_BOT_TOKEN CONFIG_BASIC_DEMO_TG_BOT_TOKEN
#define BASIC_DEMO_WECHAT_TOKEN CONFIG_BASIC_DEMO_WECHAT_TOKEN
#define BASIC_DEMO_WECHAT_BASE_URL CONFIG_BASIC_DEMO_WECHAT_BASE_URL
@@ -43,6 +45,8 @@ typedef struct {
char llm_timeout_ms[16];
char qq_app_id[32];
char qq_app_secret[BASIC_DEMO_SETTINGS_STR_LEN];
char feishu_app_id[64];
char feishu_app_secret[BASIC_DEMO_SETTINGS_STR_LEN];
char tg_bot_token[BASIC_DEMO_SETTINGS_STR_LEN];
char wechat_token[BASIC_DEMO_SETTINGS_STR_LEN];
char wechat_base_url[BASIC_DEMO_SETTINGS_STR_LEN];
@@ -190,6 +190,8 @@ static esp_err_t settings_to_json(httpd_req_t *req)
json_add_string(root, "llm_timeout_ms", settings.llm_timeout_ms);
json_add_string(root, "qq_app_id", settings.qq_app_id);
json_add_string(root, "qq_app_secret", settings.qq_app_secret);
json_add_string(root, "feishu_app_id", settings.feishu_app_id);
json_add_string(root, "feishu_app_secret", settings.feishu_app_secret);
json_add_string(root, "tg_bot_token", settings.tg_bot_token);
json_add_string(root, "wechat_token", settings.wechat_token);
json_add_string(root, "wechat_base_url", settings.wechat_base_url);
@@ -321,6 +323,8 @@ static esp_err_t config_post_handler(httpd_req_t *req)
json_read_string(root, "llm_timeout_ms", settings.llm_timeout_ms, sizeof(settings.llm_timeout_ms));
json_read_string(root, "qq_app_id", settings.qq_app_id, sizeof(settings.qq_app_id));
json_read_string(root, "qq_app_secret", settings.qq_app_secret, sizeof(settings.qq_app_secret));
json_read_string(root, "feishu_app_id", settings.feishu_app_id, sizeof(settings.feishu_app_id));
json_read_string(root, "feishu_app_secret", settings.feishu_app_secret, sizeof(settings.feishu_app_secret));
json_read_string(root, "tg_bot_token", settings.tg_bot_token, sizeof(settings.tg_bot_token));
json_read_string(root, "wechat_token", settings.wechat_token, sizeof(settings.wechat_token));
json_read_string(root, "wechat_base_url", settings.wechat_base_url, sizeof(settings.wechat_base_url));
+72
View File
@@ -10,6 +10,8 @@ const configFields = [
"llm_timeout_ms",
"qq_app_id",
"qq_app_secret",
"feishu_app_id",
"feishu_app_secret",
"tg_bot_token",
"wechat_token",
"wechat_base_url",
@@ -23,6 +25,26 @@ const configFields = [
let currentPath = "/";
let wechatLoginPollTimer = null;
const llmProviderPresets = {
openai: {
llm_backend_type: "openai_compatible",
llm_profile: "openai",
llm_base_url: "https://api.openai.com",
llm_auth_type: "bearer",
},
qwen: {
llm_backend_type: "openai_compatible",
llm_profile: "qwen_compatible",
llm_base_url: "https://dashscope.aliyuncs.com",
llm_auth_type: "bearer",
},
anthropic: {
llm_backend_type: "anthropic",
llm_profile: "anthropic",
llm_base_url: "https://api.anthropic.com",
llm_auth_type: "none",
},
};
function showBanner(id, message, isError = false) {
const banner = document.getElementById(id);
@@ -55,6 +77,46 @@ function fillConfigForm(data) {
input.value = data[field];
}
});
syncProviderPreset();
}
function detectProviderPreset() {
const backend = document.getElementById("llm_backend_type")?.value.trim();
const profile = document.getElementById("llm_profile")?.value.trim();
const baseUrl = document.getElementById("llm_base_url")?.value.trim();
const authType = document.getElementById("llm_auth_type")?.value.trim();
const match = Object.entries(llmProviderPresets).find(([, preset]) =>
preset.llm_backend_type === backend &&
preset.llm_profile === profile &&
preset.llm_base_url === baseUrl &&
preset.llm_auth_type === authType,
);
return match ? match[0] : "custom";
}
function syncProviderPreset() {
const select = document.getElementById("llm_provider_preset");
if (select) {
select.value = detectProviderPreset();
}
}
function applyProviderPreset(presetKey) {
const preset = llmProviderPresets[presetKey];
if (!preset) {
syncProviderPreset();
return;
}
Object.entries(preset).forEach(([field, value]) => {
const input = document.getElementById(field);
if (input) {
input.value = value;
}
});
syncProviderPreset();
}
function humanSize(value) {
@@ -114,6 +176,7 @@ async function saveConfig() {
throw new Error(result.error || "Failed to save settings");
}
showBanner("configMessage", result.message || "Settings saved");
syncProviderPreset();
} catch (error) {
showBanner("configMessage", error.message, true);
} finally {
@@ -388,6 +451,15 @@ async function deletePath(path) {
function bindEvents() {
document.getElementById("saveConfigButton").addEventListener("click", saveConfig);
document.getElementById("llm_provider_preset").addEventListener("change", (event) => {
applyProviderPreset(event.target.value);
});
["llm_backend_type", "llm_profile", "llm_base_url", "llm_auth_type"].forEach((field) => {
const input = document.getElementById(field);
if (input) {
input.addEventListener("input", syncProviderPreset);
}
});
document.getElementById("wechatLoginStartButton").addEventListener("click", startWechatLogin);
document.getElementById("wechatLoginCancelButton").addEventListener("click", cancelWechatLogin);
document.getElementById("refreshFilesButton").addEventListener("click", () => loadFiles().catch((error) => {
+37 -15
View File
@@ -46,29 +46,43 @@
<input id="llm_api_key" name="llm_api_key" type="password">
</label>
<label>
<span>LLM Backend</span>
<input id="llm_backend_type" name="llm_backend_type" placeholder="openai_compatible / custom">
</label>
<label>
<span>LLM Profile</span>
<input id="llm_profile" name="llm_profile" placeholder="openai / qwen_compatible">
<span>LLM Provider</span>
<select id="llm_provider_preset" name="llm_provider_preset">
<option value="openai">OpenAI</option>
<option value="qwen">Qwen Compatible</option>
<option value="anthropic">Anthropic</option>
<option value="custom">Custom</option>
</select>
</label>
<label>
<span>LLM Model</span>
<input id="llm_model" name="llm_model">
</label>
<label>
<span>LLM Base URL</span>
<input id="llm_base_url" name="llm_base_url" placeholder="https://api.openai.com">
</label>
<label>
<span>LLM Auth Type</span>
<input id="llm_auth_type" name="llm_auth_type" placeholder="bearer / api-key / none">
</label>
<label>
<span>LLM Timeout (ms)</span>
<input id="llm_timeout_ms" name="llm_timeout_ms" placeholder="120000">
</label>
<details class="advanced-panel" style="grid-column: 1 / -1;">
<summary>Advanced LLM options</summary>
<div class="form-grid advanced-grid">
<label>
<span>LLM Backend</span>
<input id="llm_backend_type" name="llm_backend_type" placeholder="openai_compatible / anthropic / custom">
</label>
<label>
<span>LLM Profile</span>
<input id="llm_profile" name="llm_profile" placeholder="openai / qwen_compatible / anthropic">
</label>
<label>
<span>LLM Base URL</span>
<input id="llm_base_url" name="llm_base_url" placeholder="https://api.openai.com">
</label>
<label>
<span>LLM Auth Type</span>
<input id="llm_auth_type" name="llm_auth_type" placeholder="bearer / api-key / none">
</label>
</div>
</details>
<label>
<span>QQ App ID</span>
<input id="qq_app_id" name="qq_app_id">
@@ -77,6 +91,14 @@
<span>QQ App Secret</span>
<input id="qq_app_secret" name="qq_app_secret" type="password">
</label>
<label>
<span>Feishu App ID</span>
<input id="feishu_app_id" name="feishu_app_id">
</label>
<label>
<span>Feishu App Secret</span>
<input id="feishu_app_secret" name="feishu_app_secret" type="password">
</label>
<label>
<span>Telegram Bot Token</span>
<input id="tg_bot_token" name="tg_bot_token" type="password">
@@ -131,7 +153,7 @@
<input id="time_timezone" name="time_timezone" placeholder="UTC0">
</label>
</form>
<p class="note">Changes are stored in NVS. Restart the device after updating Wi-Fi or core LLM settings.</p>
<p class="note">Changes are stored in NVS. Choose an LLM provider first, then fill the shared API key and model fields. OpenAI, Qwen, and Anthropic all reuse the same LLM settings. Restart the device after updating Wi-Fi, Feishu, or core LLM settings.</p>
</section>
<section class="panel">
+22 -2
View File
@@ -147,11 +147,13 @@ label {
}
input,
select,
button {
font: inherit;
}
input {
input,
select {
width: 100%;
padding: 13px 14px;
border: 1px solid var(--line);
@@ -160,11 +162,29 @@ input {
color: var(--ink);
}
input:focus {
input:focus,
select:focus {
outline: 2px solid rgba(182, 70, 31, 0.22);
border-color: rgba(182, 70, 31, 0.35);
}
.advanced-panel {
padding: 14px 16px;
border: 1px solid var(--line);
border-radius: 16px;
background: rgba(255, 255, 255, 0.72);
}
.advanced-panel summary {
cursor: pointer;
color: var(--accent-deep);
font-weight: 600;
}
.advanced-grid {
margin-top: 14px;
}
button {
border: none;
border-radius: 999px;
@@ -0,0 +1,18 @@
idf_component_register(
SRCS
"src/cap_im_feishu.c"
"src/cmd_cap_im_feishu.c"
INCLUDE_DIRS
"include"
REQUIRES
cap_im_attachment
claw_cap
claw_event_router
esp_event
esp_http_client
esp_timer
esp_websocket_client
freertos
json
console
)
@@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
const char *storage_root_dir;
size_t max_inbound_file_bytes;
bool enable_inbound_attachments;
} cap_im_feishu_attachment_config_t;
esp_err_t cap_im_feishu_register_group(void);
esp_err_t cap_im_feishu_set_credentials(const char *app_id, const char *app_secret);
esp_err_t cap_im_feishu_set_attachment_config(
const cap_im_feishu_attachment_config_t *config);
esp_err_t cap_im_feishu_start(void);
esp_err_t cap_im_feishu_stop(void);
esp_err_t cap_im_feishu_send_text(const char *chat_id, const char *text);
esp_err_t cap_im_feishu_send_image(const char *chat_id, const char *path, const char *caption);
esp_err_t cap_im_feishu_send_file(const char *chat_id, const char *path, const char *caption);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
void register_cap_im_feishu(void);
#ifdef __cplusplus
}
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,198 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "cmd_cap_im_feishu.h"
#include <stdio.h>
#include "argtable3/argtable3.h"
#include "cap_im_feishu.h"
#include "esp_console.h"
static struct {
struct arg_lit *set_credentials;
struct arg_lit *start;
struct arg_lit *stop;
struct arg_str *send_text;
struct arg_str *send_image;
struct arg_str *send_file;
struct arg_str *app_id;
struct arg_str *app_secret;
struct arg_str *text;
struct arg_str *path;
struct arg_str *caption;
struct arg_end *end;
} feishu_args;
static int cmd_feishu_set_credentials(const char *app_id, const char *app_secret)
{
esp_err_t err = cap_im_feishu_set_credentials(app_id, app_secret);
if (err != ESP_OK) {
printf("feishu_set_credentials failed: %s\n", esp_err_to_name(err));
return 1;
}
printf("Feishu credentials updated\n");
return 0;
}
static int cmd_feishu_start(void)
{
esp_err_t err = cap_im_feishu_start();
if (err != ESP_OK) {
printf("feishu_start failed: %s\n", esp_err_to_name(err));
return 1;
}
printf("Feishu gateway started\n");
return 0;
}
static int cmd_feishu_stop(void)
{
esp_err_t err = cap_im_feishu_stop();
if (err != ESP_OK) {
printf("feishu_stop failed: %s\n", esp_err_to_name(err));
return 1;
}
printf("Feishu gateway stopped\n");
return 0;
}
static int cmd_feishu_send_text(const char *chat_id, const char *text)
{
esp_err_t err = cap_im_feishu_send_text(chat_id, text);
if (err != ESP_OK) {
printf("feishu_send_text failed: %s\n", esp_err_to_name(err));
return 1;
}
printf("Feishu text sent\n");
return 0;
}
static int cmd_feishu_send_image(const char *chat_id, const char *path, const char *caption)
{
esp_err_t err = cap_im_feishu_send_image(chat_id, path, caption);
if (err != ESP_OK) {
printf("feishu_send_image failed: %s\n", esp_err_to_name(err));
return 1;
}
printf("Feishu image sent\n");
return 0;
}
static int cmd_feishu_send_file(const char *chat_id, const char *path, const char *caption)
{
esp_err_t err = cap_im_feishu_send_file(chat_id, path, caption);
if (err != ESP_OK) {
printf("feishu_send_file failed: %s\n", esp_err_to_name(err));
return 1;
}
printf("Feishu file sent\n");
return 0;
}
static int feishu_func(int argc, char **argv)
{
int nerrors = arg_parse(argc, argv, (void **)&feishu_args);
int operation_count;
if (nerrors != 0) {
arg_print_errors(stderr, feishu_args.end, argv[0]);
return 1;
}
operation_count = feishu_args.set_credentials->count +
feishu_args.start->count +
feishu_args.stop->count +
feishu_args.send_text->count +
feishu_args.send_image->count +
feishu_args.send_file->count;
if (operation_count != 1) {
printf("Exactly one operation must be specified\n");
return 1;
}
if (feishu_args.set_credentials->count) {
if (!feishu_args.app_id->count || !feishu_args.app_secret->count) {
printf("'--set-credentials' requires '--app-id' and '--app-secret'\n");
return 1;
}
return cmd_feishu_set_credentials(feishu_args.app_id->sval[0], feishu_args.app_secret->sval[0]);
}
if (feishu_args.start->count) {
return cmd_feishu_start();
}
if (feishu_args.stop->count) {
return cmd_feishu_stop();
}
if (feishu_args.send_text->count) {
if (!feishu_args.text->count) {
printf("'--send-text' requires '--text'\n");
return 1;
}
return cmd_feishu_send_text(feishu_args.send_text->sval[0], feishu_args.text->sval[0]);
}
if (!feishu_args.path->count) {
printf("'--send-image' and '--send-file' require '--path'\n");
return 1;
}
if (feishu_args.send_image->count) {
return cmd_feishu_send_image(feishu_args.send_image->sval[0],
feishu_args.path->sval[0],
feishu_args.caption->count ? feishu_args.caption->sval[0] : NULL);
}
return cmd_feishu_send_file(feishu_args.send_file->sval[0],
feishu_args.path->sval[0],
feishu_args.caption->count ? feishu_args.caption->sval[0] : NULL);
}
void register_cap_im_feishu(void)
{
feishu_args.set_credentials = arg_lit0(NULL, "set-credentials", "Set Feishu app credentials");
feishu_args.start = arg_lit0(NULL, "start", "Start the Feishu gateway");
feishu_args.stop = arg_lit0(NULL, "stop", "Stop the Feishu gateway");
feishu_args.send_text = arg_str0(NULL, "send-text", "<chat_id>", "Send text to a Feishu chat");
feishu_args.send_image = arg_str0(NULL, "send-image", "<chat_id>", "Send an image to a Feishu chat");
feishu_args.send_file = arg_str0(NULL, "send-file", "<chat_id>", "Send a file to a Feishu chat");
feishu_args.app_id = arg_str0(NULL, "app-id", "<app_id>", "Feishu app ID");
feishu_args.app_secret = arg_str0(NULL, "app-secret", "<app_secret>", "Feishu app secret");
feishu_args.text = arg_str0(NULL, "text", "<text>", "Text content");
feishu_args.path = arg_str0(NULL, "path", "<path>", "Local file path");
feishu_args.caption = arg_str0(NULL, "caption", "<caption>", "Optional caption sent as text");
feishu_args.end = arg_end(11);
const esp_console_cmd_t feishu_cmd = {
.command = "feishu",
.help = "Feishu operation.\n"
"Examples:\n"
" feishu --set-credentials --app-id cli_xxx --app-secret sec_xxx\n"
" feishu --start\n"
" feishu --stop\n"
" feishu --send-text ou_xxx --text \"hello\"\n"
" feishu --send-image ou_xxx --path /fatfs/data/inbox/pic.jpg --caption \"look\"\n"
" feishu --send-file ou_xxx --path /fatfs/data/docs/readme.txt\n",
.func = feishu_func,
.argtable = &feishu_args,
};
ESP_ERROR_CHECK(esp_console_cmd_register(&feishu_cmd));
}
@@ -9,7 +9,9 @@ idf_component_register(
"src"
REQUIRES
claw_cap
cap_mcp_server
esp_http_client
esp_netif
json
mdns
console
@@ -9,6 +9,8 @@
#include <stdio.h>
#include <string.h>
#include "cap_mcp_server.h"
#include "esp_netif.h"
#include "esp_log.h"
#include "lwip/ip_addr.h"
#include "mdns.h"
@@ -58,6 +60,139 @@ static const char *cap_mcp_pick_ip_string(const mdns_result_t *result,
return NULL;
}
static esp_err_t cap_mcp_append_device(cJSON *devices,
const char *instance,
const char *hostname,
const char *ip,
const char *url_host,
uint16_t port,
const char *endpoint)
{
cJSON *device = NULL;
char server_url[320];
char url[384];
const char *host_for_url = NULL;
if (!devices || !hostname || !hostname[0] || !endpoint || !endpoint[0] || port == 0) {
return ESP_ERR_INVALID_ARG;
}
host_for_url = (url_host && url_host[0]) ? url_host : ((ip && ip[0]) ? ip : hostname);
snprintf(server_url, sizeof(server_url), "http://%s:%u", host_for_url, (unsigned int)port);
snprintf(url, sizeof(url), "%s/%s", server_url, endpoint);
device = cJSON_CreateObject();
if (!device) {
return ESP_ERR_NO_MEM;
}
cJSON_AddStringToObject(device, "instance", (instance && instance[0]) ? instance : "(unknown)");
cJSON_AddStringToObject(device, "hostname", hostname);
cJSON_AddStringToObject(device, "ip", (ip && ip[0]) ? ip : "(unresolved)");
cJSON_AddNumberToObject(device, "port", port);
cJSON_AddStringToObject(device, "endpoint", endpoint);
cJSON_AddStringToObject(device, "server_url", server_url);
cJSON_AddStringToObject(device, "url", url);
cJSON_AddItemToArray(devices, device);
return ESP_OK;
}
static bool cap_mcp_devices_has_match(const cJSON *devices,
const char *hostname,
uint16_t port,
const char *endpoint)
{
const cJSON *device = NULL;
if (!cJSON_IsArray((cJSON *)devices) || !hostname || !hostname[0] || !endpoint || !endpoint[0]) {
return false;
}
cJSON_ArrayForEach(device, (cJSON *)devices) {
const cJSON *hostname_item = cJSON_GetObjectItemCaseSensitive((cJSON *)device, "hostname");
const cJSON *port_item = cJSON_GetObjectItemCaseSensitive((cJSON *)device, "port");
const cJSON *endpoint_item = cJSON_GetObjectItemCaseSensitive((cJSON *)device, "endpoint");
if (!cJSON_IsString((cJSON *)hostname_item) || !cJSON_IsNumber((cJSON *)port_item) ||
!cJSON_IsString((cJSON *)endpoint_item)) {
continue;
}
if (strcmp(hostname_item->valuestring, hostname) == 0 &&
(uint16_t)port_item->valueint == port &&
strcmp(endpoint_item->valuestring, endpoint) == 0) {
return true;
}
}
return false;
}
static const char *cap_mcp_get_self_ip_string(char *buf, size_t buf_size)
{
esp_netif_t *netif = NULL;
esp_netif_ip_info_t ip_info = {0};
if (!buf || buf_size == 0) {
return NULL;
}
buf[0] = '\0';
netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (!netif) {
return NULL;
}
if (esp_netif_get_ip_info(netif, &ip_info) != ESP_OK || ip_info.ip.addr == 0) {
return NULL;
}
snprintf(buf, buf_size, IPSTR, IP2STR(&ip_info.ip));
return buf;
}
static esp_err_t cap_mcp_append_self_device_if_needed(cJSON *devices, size_t *found)
{
cap_mcp_server_config_t config = {0};
bool started = false;
char hostname_local[96];
char ip_buf[64];
const char *hostname = NULL;
const char *instance = NULL;
const char *endpoint = NULL;
const char *ip = NULL;
esp_err_t err;
if (!devices || !found) {
return ESP_ERR_INVALID_ARG;
}
err = cap_mcp_server_get_config(&config, &started);
if (err != ESP_OK || !started) {
return err;
}
hostname = (config.hostname && config.hostname[0]) ? config.hostname : "clawgent";
instance = (config.instance_name && config.instance_name[0]) ? config.instance_name : "Clawgent";
endpoint = (config.endpoint && config.endpoint[0]) ? config.endpoint : CAP_MCP_DEFAULT_ENDPOINT;
ip = cap_mcp_get_self_ip_string(ip_buf, sizeof(ip_buf));
if (cap_mcp_devices_has_match(devices, hostname, config.server_port, endpoint)) {
return ESP_OK;
}
snprintf(hostname_local, sizeof(hostname_local), "%s.local", hostname);
err = cap_mcp_append_device(devices,
instance,
hostname,
ip,
ip ? ip : hostname_local,
config.server_port,
endpoint);
if (err == ESP_OK) {
(*found)++;
}
return err;
}
static esp_err_t cap_mcp_parse_discover_options(const char *input_json,
int *timeout_ms,
bool *include_self)
@@ -139,10 +274,7 @@ esp_err_t cap_mcp_discover_services(const char *input_json, cJSON **result_out)
const char *endpoint;
const char *hostname;
const char *instance;
const char *host_for_url;
cJSON *device;
char server_url[320];
char url[384];
esp_err_t append_err;
if (!include_self && result->hostname && strcmp(result->hostname, "clawgent") == 0) {
continue;
@@ -157,29 +289,30 @@ esp_err_t cap_mcp_discover_services(const char *input_json, cJSON **result_out)
hostname = (result->hostname && result->hostname[0]) ? result->hostname : "(unknown)";
instance = (result->instance_name && result->instance_name[0]) ?
result->instance_name : "(unknown)";
host_for_url = (ip && ip[0]) ? ip : hostname;
device = cJSON_CreateObject();
if (!device) {
append_err = cap_mcp_append_device(devices,
instance,
hostname,
ip,
NULL,
result->port,
endpoint);
if (append_err != ESP_OK) {
mdns_query_results_free(results);
cJSON_Delete(root);
return ESP_ERR_NO_MEM;
return append_err;
}
snprintf(server_url, sizeof(server_url), "http://%s:%u", host_for_url, result->port);
snprintf(url, sizeof(url), "%s/%s", server_url, endpoint);
cJSON_AddStringToObject(device, "instance", instance);
cJSON_AddStringToObject(device, "hostname", hostname);
cJSON_AddStringToObject(device, "ip", ip ? ip : "(unresolved)");
cJSON_AddNumberToObject(device, "port", result->port);
cJSON_AddStringToObject(device, "endpoint", endpoint);
cJSON_AddStringToObject(device, "server_url", server_url);
cJSON_AddStringToObject(device, "url", url);
cJSON_AddItemToArray(devices, device);
found++;
}
mdns_query_results_free(results);
if (include_self) {
err = cap_mcp_append_self_device_if_needed(devices, &found);
if (err != ESP_OK) {
cJSON_Delete(root);
return err;
}
}
cJSON_AddNumberToObject(root, "count", (double)found);
cJSON_AddItemToObject(root, "devices", devices);
*result_out = root;
@@ -5,6 +5,7 @@
*/
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
@@ -23,6 +24,7 @@ typedef struct {
esp_err_t cap_mcp_server_register_group(void);
esp_err_t cap_mcp_server_set_config(const cap_mcp_server_config_t *config);
esp_err_t cap_mcp_server_get_config(cap_mcp_server_config_t *config, bool *started);
#ifdef __cplusplus
}

Some files were not shown because too many files have changed in this diff Show More