feat: use websocket for feishu inbound

Signed-off-by: Asklv <boironic@gmail.com>
This commit is contained in:
Asklv
2026-03-04 19:16:00 +08:00
parent a9fd606672
commit 040f5bed0f
3 changed files with 478 additions and 143 deletions

View File

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

View File

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

View File

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