diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index c5e959f..5f3fe1e 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -26,5 +26,5 @@ idf_component_register( REQUIRES nvs_flash esp_wifi esp_netif esp_http_client esp_http_server esp_https_ota esp_event json spiffs console vfs app_update esp-tls - esp_timer + esp_timer esp_websocket_client ) diff --git a/main/channels/feishu/feishu_bot.c b/main/channels/feishu/feishu_bot.c index 5817e0d..3f3ae3a 100644 --- a/main/channels/feishu/feishu_bot.c +++ b/main/channels/feishu/feishu_bot.c @@ -10,9 +10,10 @@ #include "freertos/task.h" #include "esp_log.h" #include "esp_http_client.h" -#include "esp_http_server.h" +#include "esp_websocket_client.h" #include "esp_crt_bundle.h" #include "esp_timer.h" +#include "esp_event.h" #include "nvs.h" #include "cJSON.h" @@ -23,6 +24,7 @@ static const char *TAG = "feishu"; #define FEISHU_AUTH_URL FEISHU_API_BASE "/auth/v3/tenant_access_token/internal" #define FEISHU_SEND_MSG_URL FEISHU_API_BASE "/im/v1/messages" #define FEISHU_REPLY_MSG_URL FEISHU_API_BASE "/im/v1/messages/%s/reply" +#define FEISHU_WS_CONFIG_URL "https://open.feishu.cn/callback/ws/endpoint" /* ── Credentials & token state ─────────────────────────────── */ static char s_app_id[64] = MIMI_SECRET_FEISHU_APP_ID; @@ -30,6 +32,18 @@ static char s_app_secret[128] = MIMI_SECRET_FEISHU_APP_SECRET; static char s_tenant_token[512] = {0}; static int64_t s_token_expire_time = 0; +/* ── Feishu WebSocket state ────────────────────────────────── */ +static esp_websocket_client_handle_t s_ws_client = NULL; +static TaskHandle_t s_ws_task = NULL; +static char s_ws_url[512] = {0}; +static int s_ws_ping_interval_ms = 120000; +static int s_ws_reconnect_interval_ms = 30000; +static int s_ws_reconnect_nonce_ms = 30000; +static int s_ws_service_id = 0; +static bool s_ws_connected = false; + +static void handle_message_event(cJSON *event); + /* ── Message deduplication ─────────────────────────────────── */ #define FEISHU_DEDUP_CACHE_SIZE 64 @@ -86,6 +100,203 @@ static esp_err_t http_event_handler(esp_http_client_event_t *evt) return ESP_OK; } +/* ── Feishu WS frame (protobuf) ────────────────────────────── */ +typedef struct { + char key[32]; + char value[128]; +} ws_header_t; + +typedef struct { + uint64_t seq_id; + uint64_t log_id; + int32_t service; + int32_t method; + ws_header_t headers[16]; + size_t header_count; + const uint8_t *payload; + size_t payload_len; +} ws_frame_t; + +static bool pb_read_varint(const uint8_t *buf, size_t len, size_t *pos, uint64_t *out) +{ + uint64_t v = 0; + int shift = 0; + while (*pos < len && shift <= 63) { + uint8_t b = buf[(*pos)++]; + v |= ((uint64_t)(b & 0x7F)) << shift; + if ((b & 0x80) == 0) { + *out = v; + return true; + } + shift += 7; + } + return false; +} + +static bool pb_skip_field(const uint8_t *buf, size_t len, size_t *pos, uint8_t wire_type) +{ + uint64_t n = 0; + switch (wire_type) { + case 0: + return pb_read_varint(buf, len, pos, &n); + case 1: + if (*pos + 8 > len) return false; + *pos += 8; + return true; + case 2: + if (!pb_read_varint(buf, len, pos, &n)) return false; + if (*pos + (size_t)n > len) return false; + *pos += (size_t)n; + return true; + case 5: + if (*pos + 4 > len) return false; + *pos += 4; + return true; + default: + return false; + } +} + +static bool pb_parse_header_msg(const uint8_t *buf, size_t len, ws_header_t *h) +{ + memset(h, 0, sizeof(*h)); + size_t pos = 0; + while (pos < len) { + uint64_t tag = 0, slen = 0; + if (!pb_read_varint(buf, len, &pos, &tag)) return false; + uint32_t field = (uint32_t)(tag >> 3); + uint8_t wt = (uint8_t)(tag & 0x07); + if (wt != 2) { + if (!pb_skip_field(buf, len, &pos, wt)) return false; + continue; + } + if (!pb_read_varint(buf, len, &pos, &slen)) return false; + if (pos + (size_t)slen > len) return false; + if (field == 1) { + size_t n = (slen < sizeof(h->key) - 1) ? (size_t)slen : sizeof(h->key) - 1; + memcpy(h->key, buf + pos, n); + h->key[n] = '\0'; + } else if (field == 2) { + size_t n = (slen < sizeof(h->value) - 1) ? (size_t)slen : sizeof(h->value) - 1; + memcpy(h->value, buf + pos, n); + h->value[n] = '\0'; + } + pos += (size_t)slen; + } + return true; +} + +static bool pb_parse_frame(const uint8_t *buf, size_t len, ws_frame_t *f) +{ + memset(f, 0, sizeof(*f)); + size_t pos = 0; + while (pos < len) { + uint64_t tag = 0, v = 0, blen = 0; + if (!pb_read_varint(buf, len, &pos, &tag)) return false; + uint32_t field = (uint32_t)(tag >> 3); + uint8_t wt = (uint8_t)(tag & 0x07); + if (field == 1 && wt == 0) { + if (!pb_read_varint(buf, len, &pos, &f->seq_id)) return false; + } else if (field == 2 && wt == 0) { + if (!pb_read_varint(buf, len, &pos, &f->log_id)) return false; + } else if (field == 3 && wt == 0) { + if (!pb_read_varint(buf, len, &pos, &v)) return false; + f->service = (int32_t)v; + } else if (field == 4 && wt == 0) { + if (!pb_read_varint(buf, len, &pos, &v)) return false; + f->method = (int32_t)v; + } else if (field == 5 && wt == 2) { + if (!pb_read_varint(buf, len, &pos, &blen)) return false; + if (pos + (size_t)blen > len) return false; + if (f->header_count < 16) { + pb_parse_header_msg(buf + pos, (size_t)blen, &f->headers[f->header_count++]); + } + pos += (size_t)blen; + } else if (field == 8 && wt == 2) { + if (!pb_read_varint(buf, len, &pos, &blen)) return false; + if (pos + (size_t)blen > len) return false; + f->payload = buf + pos; + f->payload_len = (size_t)blen; + pos += (size_t)blen; + } else { + if (!pb_skip_field(buf, len, &pos, wt)) return false; + } + } + return true; +} + +static const char *frame_header_value(const ws_frame_t *f, const char *key) +{ + for (size_t i = 0; i < f->header_count; i++) { + if (strcmp(f->headers[i].key, key) == 0) { + return f->headers[i].value; + } + } + return NULL; +} + +static bool pb_write_varint(uint8_t *buf, size_t cap, size_t *pos, uint64_t value) +{ + do { + if (*pos >= cap) return false; + uint8_t byte = (uint8_t)(value & 0x7F); + value >>= 7; + if (value) byte |= 0x80; + buf[(*pos)++] = byte; + } while (value); + return true; +} + +static bool pb_write_tag(uint8_t *buf, size_t cap, size_t *pos, uint32_t field, uint8_t wt) +{ + return pb_write_varint(buf, cap, pos, ((uint64_t)field << 3) | wt); +} + +static bool pb_write_bytes(uint8_t *buf, size_t cap, size_t *pos, uint32_t field, const uint8_t *data, size_t len) +{ + if (!pb_write_tag(buf, cap, pos, field, 2)) return false; + if (!pb_write_varint(buf, cap, pos, len)) return false; + if (*pos + len > cap) return false; + memcpy(buf + *pos, data, len); + *pos += len; + return true; +} + +static bool pb_write_string(uint8_t *buf, size_t cap, size_t *pos, uint32_t field, const char *s) +{ + return pb_write_bytes(buf, cap, pos, field, (const uint8_t *)s, strlen(s)); +} + +static bool ws_encode_header(uint8_t *dst, size_t cap, size_t *out_len, const char *key, const char *value) +{ + size_t pos = 0; + if (!pb_write_string(dst, cap, &pos, 1, key)) return false; + if (!pb_write_string(dst, cap, &pos, 2, value)) return false; + *out_len = pos; + return true; +} + +static int ws_send_frame(const ws_frame_t *f, const uint8_t *payload, size_t payload_len, int timeout_ms) +{ + uint8_t out[2048]; + size_t pos = 0; + if (!pb_write_tag(out, sizeof(out), &pos, 1, 0) || !pb_write_varint(out, sizeof(out), &pos, f->seq_id)) return -1; + if (!pb_write_tag(out, sizeof(out), &pos, 2, 0) || !pb_write_varint(out, sizeof(out), &pos, f->log_id)) return -1; + if (!pb_write_tag(out, sizeof(out), &pos, 3, 0) || !pb_write_varint(out, sizeof(out), &pos, (uint32_t)f->service)) return -1; + if (!pb_write_tag(out, sizeof(out), &pos, 4, 0) || !pb_write_varint(out, sizeof(out), &pos, (uint32_t)f->method)) return -1; + + for (size_t i = 0; i < f->header_count; i++) { + uint8_t hb[256]; + size_t hlen = 0; + if (!ws_encode_header(hb, sizeof(hb), &hlen, f->headers[i].key, f->headers[i].value)) return -1; + if (!pb_write_bytes(out, sizeof(out), &pos, 5, hb, hlen)) return -1; + } + if (payload && payload_len > 0) { + if (!pb_write_bytes(out, sizeof(out), &pos, 8, payload, payload_len)) return -1; + } + return esp_websocket_client_send_bin(s_ws_client, (const char *)out, pos, timeout_ms); +} + /* ── Get / refresh tenant access token ─────────────────────── */ static esp_err_t feishu_get_tenant_token(void) { @@ -206,6 +417,251 @@ static char *feishu_api_call(const char *url, const char *method, const char *po return resp.buf; } +/* ── WS long connection ────────────────────────────────────── */ +static bool parse_query_param(const char *url, const char *key, char *out, size_t out_size) +{ + const char *q = strchr(url, '?'); + if (!q) return false; + q++; + size_t key_len = strlen(key); + while (*q) { + const char *eq = strchr(q, '='); + if (!eq) break; + const char *amp = strchr(eq + 1, '&'); + size_t name_len = (size_t)(eq - q); + if (name_len == key_len && strncmp(q, key, key_len) == 0) { + size_t val_len = amp ? (size_t)(amp - (eq + 1)) : strlen(eq + 1); + size_t n = (val_len < out_size - 1) ? val_len : out_size - 1; + memcpy(out, eq + 1, n); + out[n] = '\0'; + return true; + } + if (!amp) break; + q = amp + 1; + } + return false; +} + +static esp_err_t feishu_pull_ws_config(void) +{ + cJSON *body = cJSON_CreateObject(); + cJSON_AddStringToObject(body, "AppID", s_app_id); + cJSON_AddStringToObject(body, "AppSecret", s_app_secret); + char *json_str = cJSON_PrintUnformatted(body); + cJSON_Delete(body); + if (!json_str) return ESP_ERR_NO_MEM; + + http_resp_t resp = { .buf = calloc(1, 4096), .len = 0, .cap = 4096 }; + if (!resp.buf) { + free(json_str); + return ESP_ERR_NO_MEM; + } + + esp_http_client_config_t config = { + .url = FEISHU_WS_CONFIG_URL, + .event_handler = http_event_handler, + .user_data = &resp, + .timeout_ms = 15000, + .buffer_size = 2048, + .buffer_size_tx = 1024, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + esp_http_client_handle_t client = esp_http_client_init(&config); + if (!client) { + free(json_str); + free(resp.buf); + return ESP_FAIL; + } + + esp_http_client_set_method(client, HTTP_METHOD_POST); + esp_http_client_set_header(client, "Content-Type", "application/json"); + esp_http_client_set_header(client, "locale", "zh"); + esp_http_client_set_post_field(client, json_str, strlen(json_str)); + esp_err_t err = esp_http_client_perform(client); + int status = esp_http_client_get_status_code(client); + esp_http_client_cleanup(client); + free(json_str); + + if (err != ESP_OK || status != 200) { + ESP_LOGE(TAG, "WS config request failed: err=%s http=%d", esp_err_to_name(err), status); + free(resp.buf); + return ESP_FAIL; + } + + cJSON *root = cJSON_Parse(resp.buf); + free(resp.buf); + if (!root) return ESP_FAIL; + + cJSON *code = cJSON_GetObjectItem(root, "code"); + cJSON *data = cJSON_GetObjectItem(root, "data"); + cJSON *url = data ? cJSON_GetObjectItem(data, "URL") : NULL; + cJSON *ccfg = data ? cJSON_GetObjectItem(data, "ClientConfig") : NULL; + if (!code || code->valueint != 0 || !url || !cJSON_IsString(url)) { + ESP_LOGE(TAG, "Invalid WS config response"); + cJSON_Delete(root); + return ESP_FAIL; + } + + strncpy(s_ws_url, url->valuestring, sizeof(s_ws_url) - 1); + char sid[24] = {0}; + if (parse_query_param(s_ws_url, "service_id", sid, sizeof(sid))) { + s_ws_service_id = atoi(sid); + } + if (ccfg) { + cJSON *pi = cJSON_GetObjectItem(ccfg, "PingInterval"); + cJSON *ri = cJSON_GetObjectItem(ccfg, "ReconnectInterval"); + cJSON *rn = cJSON_GetObjectItem(ccfg, "ReconnectNonce"); + if (pi && cJSON_IsNumber(pi)) s_ws_ping_interval_ms = pi->valueint * 1000; + if (ri && cJSON_IsNumber(ri)) s_ws_reconnect_interval_ms = ri->valueint * 1000; + if (rn && cJSON_IsNumber(rn)) s_ws_reconnect_nonce_ms = rn->valueint * 1000; + } + cJSON_Delete(root); + ESP_LOGI(TAG, "WS config ready: service_id=%d ping=%dms", s_ws_service_id, s_ws_ping_interval_ms); + return ESP_OK; +} + +static void feishu_process_ws_event_json(const char *json, size_t len) +{ + cJSON *root = cJSON_ParseWithLength(json, len); + if (!root) return; + cJSON *event = cJSON_GetObjectItem(root, "event"); + cJSON *header = cJSON_GetObjectItem(root, "header"); + if (event && header) { + cJSON *event_type = cJSON_GetObjectItem(header, "event_type"); + if (event_type && cJSON_IsString(event_type) && + strcmp(event_type->valuestring, "im.message.receive_v1") == 0) { + handle_message_event(event); + } + } else if (event) { + handle_message_event(event); + } + cJSON_Delete(root); +} + +static void feishu_handle_ws_frame(const uint8_t *buf, size_t len) +{ + ws_frame_t frame = {0}; + if (!pb_parse_frame(buf, len, &frame)) { + ESP_LOGW(TAG, "WS frame parse failed"); + return; + } + + const char *type = frame_header_value(&frame, "type"); + if (frame.method == 0) { + if (type && strcmp(type, "pong") == 0 && frame.payload && frame.payload_len > 0) { + cJSON *cfg = cJSON_ParseWithLength((const char *)frame.payload, frame.payload_len); + if (cfg) { + cJSON *pi = cJSON_GetObjectItem(cfg, "PingInterval"); + if (pi && cJSON_IsNumber(pi)) s_ws_ping_interval_ms = pi->valueint * 1000; + cJSON_Delete(cfg); + } + } + return; + } + if (!type || strcmp(type, "event") != 0) return; + if (!frame.payload || frame.payload_len == 0) return; + + int code = 200; + feishu_process_ws_event_json((const char *)frame.payload, frame.payload_len); + + char ack[32]; + int ack_len = snprintf(ack, sizeof(ack), "{\"code\":%d}", code); + ws_frame_t resp = frame; + ws_send_frame(&resp, (const uint8_t *)ack, (size_t)ack_len, 1000); +} + +static void feishu_ws_event_handler(void *arg, esp_event_base_t base, int32_t event_id, void *event_data) +{ + (void)arg; + (void)base; + esp_websocket_event_data_t *e = (esp_websocket_event_data_t *)event_data; + static uint8_t *rx_buf = NULL; + static size_t rx_cap = 0; + if (event_id == WEBSOCKET_EVENT_CONNECTED) { + s_ws_connected = true; + ESP_LOGI(TAG, "Feishu WS connected"); + } else if (event_id == WEBSOCKET_EVENT_DISCONNECTED) { + s_ws_connected = false; + ESP_LOGW(TAG, "Feishu WS disconnected"); + } 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; + if (e->payload_offset == 0) { + if (rx_buf) free(rx_buf); + rx_cap = (e->payload_len > need) ? e->payload_len : need; + rx_buf = malloc(rx_cap); + if (!rx_buf) return; + } else if (!rx_buf || need > rx_cap) { + return; + } + memcpy(rx_buf + e->payload_offset, e->data_ptr, e->data_len); + if (need >= e->payload_len) { + feishu_handle_ws_frame(rx_buf, e->payload_len); + free(rx_buf); + rx_buf = NULL; + rx_cap = 0; + } + } +} + +static void feishu_ws_task(void *arg) +{ + (void)arg; + while (1) { + if (feishu_pull_ws_config() != ESP_OK) { + vTaskDelay(pdMS_TO_TICKS(5000)); + continue; + } + + esp_websocket_client_config_t ws_cfg = { + .uri = s_ws_url, + .buffer_size = 2048, + .task_stack = MIMI_FEISHU_POLL_STACK, + .reconnect_timeout_ms = s_ws_reconnect_interval_ms, + .network_timeout_ms = 10000, + .disable_auto_reconnect = false, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + + s_ws_client = esp_websocket_client_init(&ws_cfg); + if (!s_ws_client) { + vTaskDelay(pdMS_TO_TICKS(5000)); + continue; + } + esp_websocket_register_events(s_ws_client, WEBSOCKET_EVENT_ANY, feishu_ws_event_handler, NULL); + esp_websocket_client_start(s_ws_client); + + int64_t last_ping = 0; + while (s_ws_client) { + if (s_ws_connected) { + int64_t now = esp_timer_get_time() / 1000; + if (now - last_ping >= s_ws_ping_interval_ms) { + ws_frame_t ping = {0}; + ping.seq_id = 0; + ping.log_id = 0; + ping.service = s_ws_service_id; + ping.method = 0; + ping.header_count = 1; + strncpy(ping.headers[0].key, "type", sizeof(ping.headers[0].key) - 1); + strncpy(ping.headers[0].value, "ping", sizeof(ping.headers[0].value) - 1); + ws_send_frame(&ping, NULL, 0, 1000); + last_ping = now; + } + } + if (!esp_websocket_client_is_connected(s_ws_client) && !s_ws_connected) { + break; + } + vTaskDelay(pdMS_TO_TICKS(200)); + } + + esp_websocket_client_stop(s_ws_client); + esp_websocket_client_destroy(s_ws_client); + s_ws_client = NULL; + s_ws_connected = false; + vTaskDelay(pdMS_TO_TICKS(3000)); + } +} + /* ── Webhook event handler ─────────────────────────────────── */ /* @@ -329,139 +785,7 @@ static void handle_message_event(cJSON *event) cJSON_Delete(content_obj); } -static esp_err_t feishu_webhook_handler(httpd_req_t *req) -{ - /* Read request body */ - int content_len = req->content_len; - if (content_len <= 0 || content_len > MIMI_FEISHU_WEBHOOK_MAX_BODY) { - ESP_LOGW(TAG, "Invalid content length: %d", content_len); - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid body"); - return ESP_FAIL; - } - - char *body = calloc(1, content_len + 1); - if (!body) { - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OOM"); - return ESP_ERR_NO_MEM; - } - - int received = 0; - while (received < content_len) { - int ret = httpd_req_recv(req, body + received, content_len - received); - if (ret <= 0) { - free(body); - if (ret == HTTPD_SOCK_ERR_TIMEOUT) { - httpd_resp_send_err(req, HTTPD_408_REQ_TIMEOUT, "Timeout"); - } - return ESP_FAIL; - } - received += ret; - } - body[content_len] = '\0'; - - ESP_LOGD(TAG, "Webhook body: %.200s", body); - - cJSON *root = cJSON_Parse(body); - free(body); - - if (!root) { - ESP_LOGW(TAG, "Failed to parse webhook JSON"); - httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Bad JSON"); - return ESP_FAIL; - } - - /* URL verification challenge */ - cJSON *type_j = cJSON_GetObjectItem(root, "type"); - if (type_j && cJSON_IsString(type_j) && - strcmp(type_j->valuestring, "url_verification") == 0) { - cJSON *challenge = cJSON_GetObjectItem(root, "challenge"); - if (challenge && cJSON_IsString(challenge)) { - ESP_LOGI(TAG, "URL verification challenge received"); - cJSON *resp_obj = cJSON_CreateObject(); - cJSON_AddStringToObject(resp_obj, "challenge", challenge->valuestring); - char *resp_str = cJSON_PrintUnformatted(resp_obj); - cJSON_Delete(resp_obj); - if (resp_str) { - httpd_resp_set_type(req, "application/json"); - httpd_resp_sendstr(req, resp_str); - free(resp_str); - } - cJSON_Delete(root); - return ESP_OK; - } - } - - /* Event callback v2 */ - cJSON *header = cJSON_GetObjectItem(root, "header"); - cJSON *event = cJSON_GetObjectItem(root, "event"); - - if (header && event) { - /* Check for duplicate event_id */ - cJSON *event_id = cJSON_GetObjectItem(header, "event_id"); - if (event_id && cJSON_IsString(event_id)) { - if (dedup_check_and_record(event_id->valuestring)) { - ESP_LOGD(TAG, "Duplicate event %s, skipping", event_id->valuestring); - httpd_resp_sendstr(req, "{\"code\":0}"); - cJSON_Delete(root); - return ESP_OK; - } - } - - cJSON *event_type = cJSON_GetObjectItem(header, "event_type"); - if (event_type && cJSON_IsString(event_type)) { - const char *et = event_type->valuestring; - if (strcmp(et, "im.message.receive_v1") == 0) { - handle_message_event(event); - } else { - ESP_LOGI(TAG, "Unhandled event type: %s", et); - } - } - } - - /* Always respond 200 OK to Feishu to prevent retries */ - httpd_resp_set_type(req, "application/json"); - httpd_resp_sendstr(req, "{\"code\":0}"); - - cJSON_Delete(root); - return ESP_OK; -} - -/* ── Webhook HTTP server ───────────────────────────────────── */ -static httpd_handle_t s_webhook_server = NULL; - -static esp_err_t feishu_start_webhook_server(void) -{ - if (s_webhook_server) { - ESP_LOGW(TAG, "Webhook server already running"); - return ESP_OK; - } - - httpd_config_t config = HTTPD_DEFAULT_CONFIG(); - config.server_port = MIMI_FEISHU_WEBHOOK_PORT; - config.stack_size = MIMI_FEISHU_POLL_STACK; - config.max_uri_handlers = 4; - config.lru_purge_enable = true; - - esp_err_t err = httpd_start(&s_webhook_server, &config); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to start webhook server: %s", esp_err_to_name(err)); - return err; - } - - /* Register the webhook endpoint */ - httpd_uri_t webhook_uri = { - .uri = MIMI_FEISHU_WEBHOOK_PATH, - .method = HTTP_POST, - .handler = feishu_webhook_handler, - .user_ctx = NULL, - }; - httpd_register_uri_handler(s_webhook_server, &webhook_uri); - - ESP_LOGI(TAG, "Feishu webhook server started on port %d, path: %s", - MIMI_FEISHU_WEBHOOK_PORT, MIMI_FEISHU_WEBHOOK_PATH); - - return ESP_OK; -} +/* Webhook mode intentionally disabled: Feishu channel runs in WebSocket mode only. */ /* ── Public API ────────────────────────────────────────────── */ @@ -495,17 +819,27 @@ esp_err_t feishu_bot_init(void) esp_err_t feishu_bot_start(void) { if (s_app_id[0] == '\0' || s_app_secret[0] == '\0') { - ESP_LOGW(TAG, "Feishu not configured, skipping webhook server start"); + ESP_LOGW(TAG, "Feishu not configured, skipping WebSocket start"); return ESP_OK; } - - /* Pre-fetch tenant token so we're ready to send immediately */ - esp_err_t token_err = feishu_get_tenant_token(); - if (token_err != ESP_OK) { - ESP_LOGW(TAG, "Initial token fetch failed (will retry on first API call)"); + if (s_ws_task) { + ESP_LOGW(TAG, "Feishu WebSocket task already running"); + return ESP_OK; } - - return feishu_start_webhook_server(); + BaseType_t ok = xTaskCreatePinnedToCore( + feishu_ws_task, + "feishu_ws", + MIMI_FEISHU_POLL_STACK, + NULL, + MIMI_FEISHU_POLL_PRIO, + &s_ws_task, + MIMI_FEISHU_POLL_CORE); + if (ok != pdPASS) { + s_ws_task = NULL; + return ESP_FAIL; + } + ESP_LOGI(TAG, "Feishu WebSocket mode enabled"); + return ESP_OK; } esp_err_t feishu_send_message(const char *chat_id, const char *text) diff --git a/main/idf_component.yml b/main/idf_component.yml index 8afa67f..560da05 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -14,3 +14,4 @@ dependencies: # # `public` flag doesn't have an effect dependencies of the `main` component. # # All dependencies of `main` are public by default. # public: true + espressif/esp_websocket_client: ^1.4.0