feat: 添加时区设置功能,默认时区改为 CST-8
- 新增 set_timezone LLM 工具,支持通过对话设置时区 - 新增 set_timezone / timezone_show CLI 命令 - 默认时区从 PST 改为 CST-8(中国标准时间 UTC+8) - 支持 POSIX 格式和 18 个城市名映射(Asia/Shanghai 等) - 时区通过 NVS 持久化存储(system_config namespace) - config_show 中显示当前时区配置 - 更新 changelog.md 和 taolun.md 文档
This commit is contained in:
@@ -20,6 +20,7 @@ idf_component_register(
|
||||
"tools/tool_cron.c"
|
||||
"tools/tool_web_search.c"
|
||||
"tools/tool_get_time.c"
|
||||
"tools/tool_set_timezone.c"
|
||||
"tools/tool_files.c"
|
||||
"tools/tool_gpio.c"
|
||||
"tools/gpio_policy.c"
|
||||
|
||||
@@ -14,8 +14,12 @@ static size_t append_file(char *buf, size_t size, size_t offset, const char *pat
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) return offset;
|
||||
|
||||
if (header && offset < size - 1) {
|
||||
offset += snprintf(buf + offset, size - offset, "\n## %s\n\n", header);
|
||||
if (offset >= size) return offset;
|
||||
|
||||
if (header) {
|
||||
int ret = snprintf(buf + offset, size - offset, "\n## %s\n\n", header);
|
||||
if (ret > 0) offset += (size_t)ret;
|
||||
if (offset >= size) { offset = size - 1; buf[offset] = '\0'; fclose(f); return offset; }
|
||||
}
|
||||
|
||||
size_t n = fread(buf + offset, 1, size - offset - 1, f);
|
||||
@@ -79,23 +83,35 @@ esp_err_t context_build_system_prompt(char *buf, size_t size)
|
||||
/* Long-term memory */
|
||||
char mem_buf[4096];
|
||||
if (memory_read_long_term(mem_buf, sizeof(mem_buf)) == ESP_OK && mem_buf[0]) {
|
||||
off += snprintf(buf + off, size - off, "\n## Long-term Memory\n\n%s\n", mem_buf);
|
||||
if (off < size) {
|
||||
int ret = snprintf(buf + off, size - off, "\n## Long-term Memory\n\n%s\n", mem_buf);
|
||||
if (ret > 0) off += (size_t)ret;
|
||||
if (off >= size) off = size - 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Recent daily notes (last 3 days) */
|
||||
char recent_buf[4096];
|
||||
if (memory_read_recent(recent_buf, sizeof(recent_buf), 3) == ESP_OK && recent_buf[0]) {
|
||||
off += snprintf(buf + off, size - off, "\n## Recent Notes\n\n%s\n", recent_buf);
|
||||
if (off < size) {
|
||||
int ret = snprintf(buf + off, size - off, "\n## Recent Notes\n\n%s\n", recent_buf);
|
||||
if (ret > 0) off += (size_t)ret;
|
||||
if (off >= size) off = size - 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Skills */
|
||||
char skills_buf[2048];
|
||||
size_t skills_len = skill_loader_build_summary(skills_buf, sizeof(skills_buf));
|
||||
if (skills_len > 0) {
|
||||
off += snprintf(buf + off, size - off,
|
||||
"\n## Available Skills\n\n"
|
||||
"Available skills (use read_file to load full instructions):\n%s\n",
|
||||
skills_buf);
|
||||
if (off < size) {
|
||||
int ret = snprintf(buf + off, size - off,
|
||||
"\n## Available Skills\n\n"
|
||||
"Available skills (use read_file to load full instructions):\n%s\n",
|
||||
skills_buf);
|
||||
if (ret > 0) off += (size_t)ret;
|
||||
if (off >= size) off = size - 1;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "System prompt built: %d bytes", (int)off);
|
||||
|
||||
@@ -583,6 +583,7 @@ static void feishu_ws_event_handler(void *arg, esp_event_base_t base, int32_t ev
|
||||
} else if (event_id == WEBSOCKET_EVENT_DISCONNECTED) {
|
||||
s_ws_connected = false;
|
||||
ESP_LOGW(TAG, "Feishu WS disconnected");
|
||||
if (rx_buf) { free(rx_buf); rx_buf = NULL; rx_cap = 0; }
|
||||
} else if (event_id == WEBSOCKET_EVENT_DATA) {
|
||||
if (e->op_code != WS_TRANSPORT_OPCODES_BINARY) return;
|
||||
size_t need = e->payload_offset + e->data_len;
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <ctype.h>
|
||||
#include <stdbool.h>
|
||||
#include <time.h>
|
||||
#include <dirent.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_console.h"
|
||||
@@ -654,6 +656,11 @@ static int cmd_config_show(int argc, char **argv)
|
||||
print_config_u16("Proxy Port", MIMI_NVS_PROXY, MIMI_NVS_KEY_PROXY_PORT, MIMI_SECRET_PROXY_PORT);
|
||||
print_config("Search Key", MIMI_NVS_SEARCH, MIMI_NVS_KEY_API_KEY, MIMI_SECRET_SEARCH_KEY, true);
|
||||
print_config("Tavily Key", MIMI_NVS_SEARCH, MIMI_NVS_KEY_TAVILY_KEY, MIMI_SECRET_TAVILY_KEY, true);
|
||||
|
||||
/* System configs */
|
||||
printf(" --- System configs ---\n");
|
||||
print_config("Timezone", "system_config", MIMI_NVS_KEY_TIMEZONE, MIMI_TIMEZONE, false);
|
||||
|
||||
printf("=============================\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -676,6 +683,87 @@ static int cmd_config_reset(int argc, char **argv)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- set_timezone command --- */
|
||||
static struct {
|
||||
struct arg_str *tz;
|
||||
struct arg_end *end;
|
||||
} timezone_args;
|
||||
|
||||
static int cmd_set_timezone(int argc, char **argv)
|
||||
{
|
||||
int nerrors = arg_parse(argc, argv, (void **)&timezone_args);
|
||||
if (nerrors != 0) {
|
||||
arg_print_errors(stderr, timezone_args.end, argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *tz_str = timezone_args.tz->sval[0];
|
||||
nvs_handle_t nvs;
|
||||
esp_err_t err = nvs_open("system_config", NVS_READWRITE, &nvs);
|
||||
if (err != ESP_OK) {
|
||||
printf("Failed to open NVS: %s\n", esp_err_to_name(err));
|
||||
return 1;
|
||||
}
|
||||
|
||||
err = nvs_set_str(nvs, MIMI_NVS_KEY_TIMEZONE, tz_str);
|
||||
if (err != ESP_OK) {
|
||||
printf("Failed to save timezone: %s\n", esp_err_to_name(err));
|
||||
nvs_close(nvs);
|
||||
return 1;
|
||||
}
|
||||
|
||||
err = nvs_commit(nvs);
|
||||
nvs_close(nvs);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
printf("Failed to commit NVS: %s\n", esp_err_to_name(err));
|
||||
return 1;
|
||||
}
|
||||
|
||||
setenv("TZ", tz_str, 1);
|
||||
tzset();
|
||||
|
||||
time_t now = time(NULL);
|
||||
struct tm tm_now;
|
||||
localtime_r(&now, &tm_now);
|
||||
char time_str[64];
|
||||
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S %Z", &tm_now);
|
||||
|
||||
printf("Timezone set to '%s'. Current time: %s. Restart to apply permanently.\n", tz_str, time_str);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- timezone_show command --- */
|
||||
static int cmd_timezone_show(int argc, char **argv)
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
char nvs_val[64] = {0};
|
||||
const char *source = "build";
|
||||
const char *display = MIMI_TIMEZONE;
|
||||
|
||||
nvs_handle_t nvs;
|
||||
if (nvs_open("system_config", NVS_READONLY, &nvs) == ESP_OK) {
|
||||
size_t len = sizeof(nvs_val);
|
||||
if (nvs_get_str(nvs, MIMI_NVS_KEY_TIMEZONE, nvs_val, &len) == ESP_OK && nvs_val[0]) {
|
||||
source = "NVS";
|
||||
display = nvs_val;
|
||||
}
|
||||
nvs_close(nvs);
|
||||
}
|
||||
|
||||
printf("Current timezone: %s [%s]\n", display, source);
|
||||
|
||||
time_t now = time(NULL);
|
||||
struct tm tm_now;
|
||||
localtime_r(&now, &tm_now);
|
||||
char time_str[64];
|
||||
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S %Z (%A)", &tm_now);
|
||||
printf("Local time: %s\n", time_str);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- heartbeat_trigger command --- */
|
||||
static int cmd_heartbeat_trigger(int argc, char **argv)
|
||||
{
|
||||
@@ -736,13 +824,22 @@ typedef struct {
|
||||
size_t output_size;
|
||||
esp_err_t err;
|
||||
SemaphoreHandle_t done;
|
||||
bool timed_out;
|
||||
} web_search_task_ctx_t;
|
||||
|
||||
static void web_search_task(void *arg)
|
||||
{
|
||||
web_search_task_ctx_t *task_ctx = (web_search_task_ctx_t *)arg;
|
||||
task_ctx->err = tool_web_search_execute(task_ctx->input_json, task_ctx->output, task_ctx->output_size);
|
||||
xSemaphoreGive(task_ctx->done);
|
||||
if (!task_ctx->timed_out) {
|
||||
xSemaphoreGive(task_ctx->done);
|
||||
} else {
|
||||
/* Main thread timed out and freed ctx, so we must clean up ourselves */
|
||||
free((void *)task_ctx->input_json);
|
||||
free(task_ctx->output);
|
||||
vSemaphoreDelete(task_ctx->done);
|
||||
free(task_ctx);
|
||||
}
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
@@ -838,10 +935,8 @@ static int cmd_web_search(int argc, char **argv)
|
||||
|
||||
if (xSemaphoreTake(ctx->done, pdMS_TO_TICKS(45000)) != pdTRUE) {
|
||||
printf("web_search status: timeout\n");
|
||||
vSemaphoreDelete(ctx->done);
|
||||
free(input_copy);
|
||||
free(ctx);
|
||||
free(output);
|
||||
ctx->timed_out = true;
|
||||
/* Task will clean up ctx, input_copy, output, and done on its own */
|
||||
return 1;
|
||||
}
|
||||
esp_err_t err = ctx->err;
|
||||
@@ -1163,6 +1258,25 @@ esp_err_t serial_cli_init(void)
|
||||
};
|
||||
esp_console_cmd_register(&config_reset_cmd);
|
||||
|
||||
/* set_timezone */
|
||||
timezone_args.tz = arg_str1(NULL, NULL, "<timezone>", "Timezone (e.g. CST-8, Asia/Shanghai)");
|
||||
timezone_args.end = arg_end(1);
|
||||
esp_console_cmd_t set_timezone_cmd = {
|
||||
.command = "set_timezone",
|
||||
.help = "Set system timezone (e.g. set_timezone CST-8)",
|
||||
.func = &cmd_set_timezone,
|
||||
.argtable = &timezone_args,
|
||||
};
|
||||
esp_console_cmd_register(&set_timezone_cmd);
|
||||
|
||||
/* timezone_show */
|
||||
esp_console_cmd_t timezone_show_cmd = {
|
||||
.command = "timezone_show",
|
||||
.help = "Show current timezone and local time",
|
||||
.func = &cmd_timezone_show,
|
||||
};
|
||||
esp_console_cmd_register(&timezone_show_cmd);
|
||||
|
||||
/* heartbeat_trigger */
|
||||
esp_console_cmd_t heartbeat_cmd = {
|
||||
.command = "heartbeat_trigger",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_http_server.h"
|
||||
#include "cJSON.h"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_http_client.h"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_http_client.h"
|
||||
#include "esp_crt_bundle.h"
|
||||
@@ -246,12 +247,25 @@ esp_err_t llm_proxy_init(void)
|
||||
nvs_close(nvs);
|
||||
}
|
||||
|
||||
/* Load provider-specific API key if available */
|
||||
/* Load provider-specific API key from NVS */
|
||||
const char *provider_api_key = llm_provider_get_api_key(s_provider);
|
||||
if (provider_api_key && provider_api_key[0]) {
|
||||
safe_copy(s_api_key, sizeof(s_api_key), provider_api_key);
|
||||
}
|
||||
|
||||
/* Fall back to build-time provider-specific API key if NVS key is empty */
|
||||
if (s_api_key[0] == '\0') {
|
||||
if (strcmp(s_provider, "siliconflow") == 0 && MIMI_SECRET_SILICONFLOW_API_KEY[0] != '\0') {
|
||||
safe_copy(s_api_key, sizeof(s_api_key), MIMI_SECRET_SILICONFLOW_API_KEY);
|
||||
} else if (strcmp(s_provider, "volcengine") == 0 && MIMI_SECRET_VOLCENGINE_API_KEY[0] != '\0') {
|
||||
safe_copy(s_api_key, sizeof(s_api_key), MIMI_SECRET_VOLCENGINE_API_KEY);
|
||||
} else if (strcmp(s_provider, "openai") == 0 && MIMI_SECRET_OPENAI_API_KEY[0] != '\0') {
|
||||
safe_copy(s_api_key, sizeof(s_api_key), MIMI_SECRET_OPENAI_API_KEY);
|
||||
} else if (strcmp(s_provider, "anthropic") == 0 && MIMI_SECRET_ANTHROPIC_API_KEY[0] != '\0') {
|
||||
safe_copy(s_api_key, sizeof(s_api_key), MIMI_SECRET_ANTHROPIC_API_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
if (s_api_key[0]) {
|
||||
ESP_LOGI(TAG, "LLM proxy initialized (provider: %s, model: %s)", s_provider, s_model);
|
||||
} else {
|
||||
|
||||
@@ -24,7 +24,7 @@ esp_err_t session_mgr_init(void)
|
||||
|
||||
esp_err_t session_append(const char *chat_id, const char *role, const char *content)
|
||||
{
|
||||
char path[64];
|
||||
char path[128];
|
||||
session_path(chat_id, path, sizeof(path));
|
||||
|
||||
FILE *f = fopen(path, "a");
|
||||
@@ -52,7 +52,7 @@ esp_err_t session_append(const char *chat_id, const char *role, const char *cont
|
||||
|
||||
esp_err_t session_get_history_json(const char *chat_id, char *buf, size_t size, int max_msgs)
|
||||
{
|
||||
char path[64];
|
||||
char path[128];
|
||||
session_path(chat_id, path, sizeof(path));
|
||||
|
||||
FILE *f = fopen(path, "r");
|
||||
@@ -127,7 +127,7 @@ esp_err_t session_get_history_json(const char *chat_id, char *buf, size_t size,
|
||||
|
||||
esp_err_t session_clear(const char *chat_id)
|
||||
{
|
||||
char path[64];
|
||||
char path[128];
|
||||
session_path(chat_id, path, sizeof(path));
|
||||
|
||||
if (remove(path) == 0) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
@@ -47,6 +47,26 @@
|
||||
#define MIMI_SECRET_TAVILY_KEY ""
|
||||
#endif
|
||||
|
||||
/* Provider-specific API keys (fallback when generic MIMI_SECRET_API_KEY is empty) */
|
||||
#ifndef MIMI_SECRET_ANTHROPIC_API_KEY
|
||||
#define MIMI_SECRET_ANTHROPIC_API_KEY ""
|
||||
#endif
|
||||
#ifndef MIMI_SECRET_OPENAI_API_KEY
|
||||
#define MIMI_SECRET_OPENAI_API_KEY ""
|
||||
#endif
|
||||
#ifndef MIMI_SECRET_SILICONFLOW_API_KEY
|
||||
#define MIMI_SECRET_SILICONFLOW_API_KEY ""
|
||||
#endif
|
||||
#ifndef MIMI_SECRET_SILICONFLOW_BASE_URL
|
||||
#define MIMI_SECRET_SILICONFLOW_BASE_URL "https://api.siliconflow.cn/v1"
|
||||
#endif
|
||||
#ifndef MIMI_SECRET_VOLCENGINE_API_KEY
|
||||
#define MIMI_SECRET_VOLCENGINE_API_KEY ""
|
||||
#endif
|
||||
#ifndef MIMI_SECRET_VOLCENGINE_BASE_URL
|
||||
#define MIMI_SECRET_VOLCENGINE_BASE_URL "https://ark.cn-beijing.volces.com/api/v3"
|
||||
#endif
|
||||
|
||||
/* WiFi */
|
||||
#define MIMI_WIFI_MAX_RETRY 10
|
||||
#define MIMI_WIFI_RETRY_BASE_MS 1000
|
||||
@@ -80,7 +100,7 @@
|
||||
#define MIMI_AGENT_SEND_WORKING_STATUS 1
|
||||
|
||||
/* Timezone (POSIX TZ format) */
|
||||
#define MIMI_TIMEZONE "PST8PDT,M3.2.0,M11.1.0"
|
||||
#define MIMI_TIMEZONE "CST-8"
|
||||
|
||||
/* LLM */
|
||||
#define MIMI_LLM_DEFAULT_MODEL "claude-opus-4-5"
|
||||
@@ -166,6 +186,9 @@
|
||||
#define MIMI_NVS_KEY_VOLCENGINE_API_KEY "volcengine_api_key"
|
||||
#define MIMI_NVS_KEY_VOLCENGINE_BASE_URL "volcengine_base_url"
|
||||
|
||||
/* System NVS Keys */
|
||||
#define MIMI_NVS_KEY_TIMEZONE "timezone"
|
||||
|
||||
/* WiFi Onboarding (Captive Portal) */
|
||||
#define MIMI_ONBOARD_AP_PREFIX "MimiClaw-"
|
||||
#define MIMI_ONBOARD_AP_PASS "" /* open network */
|
||||
|
||||
@@ -21,16 +21,32 @@
|
||||
#define MIMI_SECRET_FEISHU_APP_ID ""
|
||||
#define MIMI_SECRET_FEISHU_APP_SECRET ""
|
||||
|
||||
/* Anthropic API */
|
||||
/* LLM Configuration
|
||||
*
|
||||
* Method 1: Generic API key (works for any provider)
|
||||
* Set MIMI_SECRET_API_KEY + MIMI_SECRET_MODEL_PROVIDER + MIMI_SECRET_MODEL
|
||||
*
|
||||
* Method 2: Provider-specific API keys (recommended for multi-provider setups)
|
||||
* Set MIMI_SECRET_<PROVIDER>_API_KEY for each provider you want to use
|
||||
* Switch providers via onboard portal or CLI: set_model_provider <provider>
|
||||
*
|
||||
* Priority: NVS (runtime config) > provider-specific key > generic key
|
||||
*/
|
||||
#define MIMI_SECRET_API_KEY ""
|
||||
#define MIMI_SECRET_MODEL ""
|
||||
#define MIMI_SECRET_MODEL "claude-opus-4-5"
|
||||
#define MIMI_SECRET_MODEL_PROVIDER "anthropic"
|
||||
|
||||
/* SiliconFlow API (OpenAI-compatible) */
|
||||
/* Anthropic (Claude) */
|
||||
#define MIMI_SECRET_ANTHROPIC_API_KEY ""
|
||||
|
||||
/* OpenAI (GPT) */
|
||||
#define MIMI_SECRET_OPENAI_API_KEY ""
|
||||
|
||||
/* SiliconFlow (硅基流动, OpenAI-compatible) */
|
||||
#define MIMI_SECRET_SILICONFLOW_API_KEY ""
|
||||
#define MIMI_SECRET_SILICONFLOW_BASE_URL "https://api.siliconflow.cn/v1"
|
||||
|
||||
/* Volcengine API (OpenAI-compatible, ByteDance Doubao models) */
|
||||
/* Volcengine (火山引擎/豆包, OpenAI-compatible) */
|
||||
#define MIMI_SECRET_VOLCENGINE_API_KEY ""
|
||||
#define MIMI_SECRET_VOLCENGINE_BASE_URL "https://ark.cn-beijing.volces.com/api/v3"
|
||||
|
||||
|
||||
@@ -56,10 +56,14 @@ static const char ONBOARD_HTML[] =
|
||||
"<label>Model</label>"
|
||||
"<input id='model' placeholder='claude-opus-4-5' value='claude-opus-4-5'>"
|
||||
"<label>Provider</label>"
|
||||
"<select id='provider'>"
|
||||
"<select id='provider' onchange='onProviderChange()'>"
|
||||
"<option value='anthropic'>Anthropic</option>"
|
||||
"<option value='openai'>OpenAI</option>"
|
||||
"<option value='siliconflow'>SiliconFlow (硅基流动)</option>"
|
||||
"<option value='volcengine'>Volcengine (火山引擎)</option>"
|
||||
"</select>"
|
||||
"<label>Base URL</label>"
|
||||
"<input id='base_url' placeholder='https://api.example.com/v1'>"
|
||||
"</div></div>"
|
||||
|
||||
/* Telegram section */
|
||||
@@ -133,8 +137,15 @@ static const char ONBOARD_HTML[] =
|
||||
"btn.textContent='Scan WiFi Networks';btn.disabled=false;"
|
||||
"}).catch(()=>{btn.textContent='Scan WiFi Networks';btn.disabled=false})}"
|
||||
|
||||
"function onProviderChange(){"
|
||||
"var p=document.getElementById('provider').value;"
|
||||
"var u=document.getElementById('base_url');"
|
||||
"if(p==='siliconflow'){u.value='https://api.siliconflow.cn/v1'}"
|
||||
"else if(p==='volcengine'){u.value='https://ark.cn-beijing.volces.com/api/v3'}"
|
||||
"else{u.value=''}}"
|
||||
|
||||
"function save(){"
|
||||
"var fields=['ssid','password','api_key','model','provider','tg_token',"
|
||||
"var fields=['ssid','password','api_key','model','provider','base_url','tg_token',"
|
||||
"'feishu_app_id','feishu_app_secret','proxy_host','proxy_port','proxy_type','search_key','tavily_key'];"
|
||||
"var data={};"
|
||||
"fields.forEach(f=>{data[f]=document.getElementById(f).value.trim()});"
|
||||
|
||||
@@ -220,6 +220,40 @@ static esp_err_t http_get_config(httpd_req_t *req)
|
||||
json_add_effective_config(root, "api_key", MIMI_NVS_LLM, MIMI_NVS_KEY_API_KEY, MIMI_SECRET_API_KEY);
|
||||
json_add_effective_config(root, "model", MIMI_NVS_LLM, MIMI_NVS_KEY_MODEL, MIMI_SECRET_MODEL);
|
||||
json_add_effective_config(root, "provider", MIMI_NVS_LLM, MIMI_NVS_KEY_PROVIDER, MIMI_SECRET_MODEL_PROVIDER);
|
||||
|
||||
/* Provider-specific Base URL (load from current provider's NVS key) */
|
||||
{
|
||||
char base_url[256] = {0};
|
||||
bool found = false;
|
||||
char provider[32] = {0};
|
||||
nvs_handle_t nvs;
|
||||
if (nvs_open(MIMI_NVS_LLM, NVS_READONLY, &nvs) == ESP_OK) {
|
||||
size_t len = sizeof(provider);
|
||||
if (nvs_get_str(nvs, MIMI_NVS_KEY_PROVIDER, provider, &len) != ESP_OK || !provider[0]) {
|
||||
/* Fall back to build-time provider if NVS doesn't have one */
|
||||
strlcpy(provider, MIMI_SECRET_MODEL_PROVIDER, sizeof(provider));
|
||||
}
|
||||
/* Try to load provider-specific base URL from NVS */
|
||||
const char *url_key = NULL;
|
||||
if (strcmp(provider, "anthropic") == 0) url_key = MIMI_NVS_KEY_ANTHROPIC_BASE_URL;
|
||||
else if (strcmp(provider, "openai") == 0) url_key = MIMI_NVS_KEY_OPENAI_BASE_URL;
|
||||
else if (strcmp(provider, "siliconflow") == 0) url_key = MIMI_NVS_KEY_SILICONFLOW_BASE_URL;
|
||||
else if (strcmp(provider, "volcengine") == 0) url_key = MIMI_NVS_KEY_VOLCENGINE_BASE_URL;
|
||||
if (url_key) {
|
||||
len = sizeof(base_url);
|
||||
if (nvs_get_str(nvs, url_key, base_url, &len) == ESP_OK && base_url[0]) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
nvs_close(nvs);
|
||||
}
|
||||
if (!found) {
|
||||
/* Fall back to build-time defaults */
|
||||
if (strcmp(provider, "siliconflow") == 0) strlcpy(base_url, MIMI_SECRET_SILICONFLOW_BASE_URL, sizeof(base_url));
|
||||
else if (strcmp(provider, "volcengine") == 0) strlcpy(base_url, MIMI_SECRET_VOLCENGINE_BASE_URL, sizeof(base_url));
|
||||
}
|
||||
cJSON_AddStringToObject(root, "base_url", base_url);
|
||||
}
|
||||
json_add_effective_config(root, "tg_token", MIMI_NVS_TG, MIMI_NVS_KEY_TG_TOKEN, MIMI_SECRET_TG_TOKEN);
|
||||
json_add_effective_config(root, "feishu_app_id", MIMI_NVS_FEISHU, MIMI_NVS_KEY_FEISHU_APP_ID, MIMI_SECRET_FEISHU_APP_ID);
|
||||
json_add_effective_config(root, "feishu_app_secret", MIMI_NVS_FEISHU, MIMI_NVS_KEY_FEISHU_APP_SECRET, MIMI_SECRET_FEISHU_APP_SECRET);
|
||||
@@ -344,6 +378,37 @@ static esp_err_t http_post_save(httpd_req_t *req)
|
||||
nvs_sync_field(root, "model", MIMI_NVS_LLM, MIMI_NVS_KEY_MODEL);
|
||||
nvs_sync_field(root, "provider", MIMI_NVS_LLM, MIMI_NVS_KEY_PROVIDER);
|
||||
|
||||
/* Save API key and base URL to provider-specific NVS keys */
|
||||
{
|
||||
cJSON *provider_item = cJSON_GetObjectItem(root, "provider");
|
||||
cJSON *api_key_item = cJSON_GetObjectItem(root, "api_key");
|
||||
cJSON *base_url_item = cJSON_GetObjectItem(root, "base_url");
|
||||
if (provider_item && cJSON_IsString(provider_item) && provider_item->valuestring[0]) {
|
||||
const char *provider = provider_item->valuestring;
|
||||
const char *api_key_nvs = NULL;
|
||||
const char *base_url_nvs = NULL;
|
||||
if (strcmp(provider, "anthropic") == 0) {
|
||||
api_key_nvs = MIMI_NVS_KEY_ANTHROPIC_API_KEY;
|
||||
base_url_nvs = MIMI_NVS_KEY_ANTHROPIC_BASE_URL;
|
||||
} else if (strcmp(provider, "openai") == 0) {
|
||||
api_key_nvs = MIMI_NVS_KEY_OPENAI_API_KEY;
|
||||
base_url_nvs = MIMI_NVS_KEY_OPENAI_BASE_URL;
|
||||
} else if (strcmp(provider, "siliconflow") == 0) {
|
||||
api_key_nvs = MIMI_NVS_KEY_SILICONFLOW_API_KEY;
|
||||
base_url_nvs = MIMI_NVS_KEY_SILICONFLOW_BASE_URL;
|
||||
} else if (strcmp(provider, "volcengine") == 0) {
|
||||
api_key_nvs = MIMI_NVS_KEY_VOLCENGINE_API_KEY;
|
||||
base_url_nvs = MIMI_NVS_KEY_VOLCENGINE_BASE_URL;
|
||||
}
|
||||
if (api_key_nvs && api_key_item && cJSON_IsString(api_key_item)) {
|
||||
nvs_sync_field(root, "api_key", MIMI_NVS_LLM, api_key_nvs);
|
||||
}
|
||||
if (base_url_nvs && base_url_item && cJSON_IsString(base_url_item)) {
|
||||
nvs_sync_field(root, "base_url", MIMI_NVS_LLM, base_url_nvs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Telegram */
|
||||
nvs_sync_field(root, "tg_token", MIMI_NVS_TG, MIMI_NVS_KEY_TG_TOKEN);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <errno.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/time.h>
|
||||
@@ -246,7 +247,7 @@ static int open_socks5_tunnel(const char *host, int port, int timeout_ms)
|
||||
free(req);
|
||||
|
||||
/* Receive connect response */
|
||||
unsigned char resp[256];
|
||||
unsigned char resp[512];
|
||||
int resp_len = recv(sock, resp, sizeof(resp), 0);
|
||||
if (resp_len < 10) {
|
||||
ESP_LOGE(TAG, "No response from SOCKS5 proxy"); close(sock); return -1;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifndef GPIO_IS_VALID_GPIO
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <time.h>
|
||||
#include <sys/time.h>
|
||||
#include "esp_log.h"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "mimi_config.h"
|
||||
#include "tools/tool_web_search.h"
|
||||
#include "tools/tool_get_time.h"
|
||||
#include "tools/tool_set_timezone.h"
|
||||
#include "tools/tool_files.h"
|
||||
#include "tools/tool_cron.h"
|
||||
#include "tools/tool_gpio.h"
|
||||
@@ -83,6 +84,18 @@ esp_err_t tool_registry_init(void)
|
||||
};
|
||||
register_tool(>);
|
||||
|
||||
/* Register set_timezone */
|
||||
mimi_tool_t stz = {
|
||||
.name = "set_timezone",
|
||||
.description = "Set the system timezone. Accepts POSIX format (e.g. CST-8, EST5EDT,M3.2.0,M11.1.0) or city name (e.g. Asia/Shanghai, America/New_York).",
|
||||
.input_schema_json =
|
||||
"{\"type\":\"object\","
|
||||
"\"properties\":{\"timezone\":{\"type\":\"string\",\"description\":\"Timezone in POSIX format or city name (e.g. CST-8, Asia/Shanghai)\"}},"
|
||||
"\"required\":[\"timezone\"]}",
|
||||
.execute = tool_set_timezone_execute,
|
||||
};
|
||||
register_tool(&stz);
|
||||
|
||||
/* Register read_file */
|
||||
mimi_tool_t rf = {
|
||||
.name = "read_file",
|
||||
|
||||
145
main/tools/tool_set_timezone.c
Normal file
145
main/tools/tool_set_timezone.c
Normal file
@@ -0,0 +1,145 @@
|
||||
#include "tool_set_timezone.h"
|
||||
#include "mimi_config.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <time.h>
|
||||
#include "esp_log.h"
|
||||
#include "nvs.h"
|
||||
#include "cJSON.h"
|
||||
|
||||
static const char *TAG = "tool_timezone";
|
||||
|
||||
/* Common timezone mappings for user-friendly names */
|
||||
typedef struct {
|
||||
const char *name;
|
||||
const char *posix_tz;
|
||||
} tz_mapping_t;
|
||||
|
||||
static const tz_mapping_t tz_mappings[] = {
|
||||
{ "Asia/Shanghai", "CST-8" },
|
||||
{ "Asia/Beijing", "CST-8" },
|
||||
{ "Asia/Hong_Kong", "HKT-8" },
|
||||
{ "Asia/Tokyo", "JST-9" },
|
||||
{ "Asia/Seoul", "KST-9" },
|
||||
{ "Asia/Singapore", "SGT-8" },
|
||||
{ "Asia/Kolkata", "IST-5:30" },
|
||||
{ "Asia/Dubai", "GST-4" },
|
||||
{ "Europe/London", "GMT0BST,M3.5.0/1,M10.5.0" },
|
||||
{ "Europe/Paris", "CET-1CEST,M3.5.0,M10.5.0/3" },
|
||||
{ "Europe/Berlin", "CET-1CEST,M3.5.0,M10.5.0/3" },
|
||||
{ "America/New_York", "EST5EDT,M3.2.0,M11.1.0" },
|
||||
{ "America/Chicago", "CST6CDT,M3.2.0,M11.1.0" },
|
||||
{ "America/Denver", "MST7MDT,M3.2.0,M11.1.0" },
|
||||
{ "America/Los_Angeles", "PST8PDT,M3.2.0,M11.1.0" },
|
||||
{ "Australia/Sydney", "AEST-10AEDT,M10.1.0,M4.1.0/3" },
|
||||
{ "UTC", "UTC0" },
|
||||
{ "GMT", "GMT0" },
|
||||
};
|
||||
|
||||
static const char *resolve_timezone(const char *tz_str)
|
||||
{
|
||||
if (!tz_str || !tz_str[0]) return NULL;
|
||||
|
||||
for (size_t i = 0; i < sizeof(tz_mappings) / sizeof(tz_mappings[0]); i++) {
|
||||
if (strcmp(tz_str, tz_mappings[i].name) == 0) {
|
||||
return tz_mappings[i].posix_tz;
|
||||
}
|
||||
}
|
||||
|
||||
return tz_str;
|
||||
}
|
||||
|
||||
static bool validate_timezone(const char *tz_str)
|
||||
{
|
||||
if (!tz_str || !tz_str[0]) return false;
|
||||
if (strlen(tz_str) > 64) return false;
|
||||
|
||||
for (size_t i = 0; i < sizeof(tz_mappings) / sizeof(tz_mappings[0]); i++) {
|
||||
if (strcmp(tz_str, tz_mappings[i].name) == 0) return true;
|
||||
}
|
||||
|
||||
if (strchr(tz_str, '+') || strchr(tz_str, '-') ||
|
||||
strcmp(tz_str, "UTC0") == 0 || strcmp(tz_str, "GMT0") == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static esp_err_t save_timezone_nvs(const char *tz_str)
|
||||
{
|
||||
nvs_handle_t nvs;
|
||||
esp_err_t err = nvs_open("system_config", NVS_READWRITE, &nvs);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to open NVS: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
err = nvs_set_str(nvs, MIMI_NVS_KEY_TIMEZONE, tz_str);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to save timezone: %s", esp_err_to_name(err));
|
||||
nvs_close(nvs);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = nvs_commit(nvs);
|
||||
nvs_close(nvs);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to commit NVS: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t tool_set_timezone_execute(const char *input_json, char *output, size_t output_size)
|
||||
{
|
||||
ESP_LOGI(TAG, "Setting timezone...");
|
||||
|
||||
cJSON *root = cJSON_Parse(input_json);
|
||||
if (!root) {
|
||||
snprintf(output, output_size, "Error: invalid JSON input");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
cJSON *tz_item = cJSON_GetObjectItem(root, "timezone");
|
||||
if (!tz_item || !cJSON_IsString(tz_item)) {
|
||||
cJSON_Delete(root);
|
||||
snprintf(output, output_size, "Error: 'timezone' field required (string)");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
const char *input_tz = tz_item->valuestring;
|
||||
const char *resolved_tz = resolve_timezone(input_tz);
|
||||
|
||||
if (!resolved_tz || !validate_timezone(resolved_tz)) {
|
||||
cJSON_Delete(root);
|
||||
snprintf(output, output_size, "Error: invalid timezone format '%s'. Use POSIX format (e.g. CST-8) or city name (e.g. Asia/Shanghai)", input_tz);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
esp_err_t err = save_timezone_nvs(resolved_tz);
|
||||
if (err != ESP_OK) {
|
||||
cJSON_Delete(root);
|
||||
snprintf(output, output_size, "Error: failed to save timezone (%s)", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
setenv("TZ", resolved_tz, 1);
|
||||
tzset();
|
||||
|
||||
time_t now = time(NULL);
|
||||
struct tm tm_now;
|
||||
localtime_r(&now, &tm_now);
|
||||
char time_str[64];
|
||||
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S %Z", &tm_now);
|
||||
|
||||
cJSON_Delete(root);
|
||||
snprintf(output, output_size, "Timezone set to '%s'. Current time: %s", resolved_tz, time_str);
|
||||
ESP_LOGI(TAG, "Timezone set to: %s, current time: %s", resolved_tz, time_str);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
11
main/tools/tool_set_timezone.h
Normal file
11
main/tools/tool_set_timezone.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stddef.h>
|
||||
|
||||
/**
|
||||
* Execute set_timezone tool.
|
||||
* Sets the system timezone via NVS and updates the TZ environment variable.
|
||||
* Input JSON: {"timezone": "CST-8"} or {"timezone": "Asia/Shanghai"}
|
||||
*/
|
||||
esp_err_t tool_set_timezone_execute(const char *input_json, char *output, size_t output_size);
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "mimi_config.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <inttypes.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_wifi.h"
|
||||
@@ -12,6 +13,7 @@
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/event_groups.h"
|
||||
#include "freertos/timers.h"
|
||||
|
||||
static const char *TAG = "wifi";
|
||||
|
||||
@@ -20,6 +22,15 @@ static int s_retry_count = 0;
|
||||
static char s_ip_str[16] = "0.0.0.0";
|
||||
static bool s_connected = false;
|
||||
static bool s_reconnect_enabled = true;
|
||||
static TimerHandle_t s_retry_timer = NULL;
|
||||
|
||||
static void retry_timer_callback(TimerHandle_t xTimer)
|
||||
{
|
||||
(void)xTimer;
|
||||
if (s_reconnect_enabled && !s_connected) {
|
||||
esp_wifi_connect();
|
||||
}
|
||||
}
|
||||
|
||||
static const char *wifi_reason_to_str(wifi_err_reason_t reason)
|
||||
{
|
||||
@@ -77,9 +88,10 @@ static void event_handler(void *arg, esp_event_base_t event_base,
|
||||
}
|
||||
ESP_LOGW(TAG, "Disconnected, retry %d/%d in %" PRIu32 "ms",
|
||||
s_retry_count + 1, MIMI_WIFI_MAX_RETRY, delay_ms);
|
||||
vTaskDelay(pdMS_TO_TICKS(delay_ms));
|
||||
esp_wifi_connect();
|
||||
s_retry_count++;
|
||||
/* Use timer instead of blocking vTaskDelay in event handler */
|
||||
if (s_retry_timer) xTimerStop(s_retry_timer, 0);
|
||||
xTimerChangePeriod(s_retry_timer, pdMS_TO_TICKS(delay_ms), 0);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to connect after %d retries", MIMI_WIFI_MAX_RETRY);
|
||||
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
|
||||
@@ -98,6 +110,7 @@ static void event_handler(void *arg, esp_event_base_t event_base,
|
||||
esp_err_t wifi_manager_init(void)
|
||||
{
|
||||
s_wifi_event_group = xEventGroupCreate();
|
||||
s_retry_timer = xTimerCreate("wifi_retry", pdMS_TO_TICKS(1000), pdFALSE, NULL, retry_timer_callback);
|
||||
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
esp_netif_create_default_wifi_sta();
|
||||
|
||||
Reference in New Issue
Block a user