* Copyright (C) 2026 Xiaomi Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
* weixin_channel.c — WeChat (Weixin) channel for AI Agent
*
* Direct connection to Tencent iLink Bot API (ilinkai.weixin.qq.com).
* No intermediate gateway needed.
*
* Protocol: https://github.com/nicepkg/weixin-bot/docs/protocol-spec.md
*
* Long-polling flow:
* 1. POST /ilink/bot/getupdates (hold ≤35s)
* 2. Server blocks until message arrives or timeout
* 3. Parse msgs[] array, push each to message_bus inbound queue
* 4. Update cursor and repeat
*
* Outbound:
* POST /ilink/bot/sendmessage (with context_token)
*
* Login:
* GET /ilink/bot/get_bot_qrcode?bot_type=3
* GET /ilink/bot/get_qrcode_status?qrcode=<qrc>
*/
#include "channels/weixin_channel.h"
#include "core/message_bus.h"
#include "cJSON.h"
#include "infra/config_store.h"
#include "infra/url_parse.h"
#include "infra/vela_tls.h"
#include "agent_compat.h"
#include "agent_config.h"
#include <inttypes.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
static const char* TAG = "weixin";
#define WEIXIN_DEFAULT_HOST "ilinkai.weixin.qq.com"
#define WEIXIN_DEFAULT_PORT "443"
#define WEIXIN_RESP_BUF_SIZE (32 * 1024)
#define WEIXIN_MAX_MSGS_PER_POLL 16
#define WEIXIN_RECONNECT_DELAY_S 5
#define WEIXIN_POLL_TIMEOUT_MS 35000
#define WEIXIN_TOKEN_MAX 256
#define WEIXIN_HOST_MAX 128
#define WEIXIN_PORT_MAX 8
#define WEIXIN_CURSOR_MAX 512
#define WEIXIN_CHANNEL_VERSION "2.0.0"
static char s_host[WEIXIN_HOST_MAX] = WEIXIN_DEFAULT_HOST;
static char s_port[WEIXIN_PORT_MAX] = WEIXIN_DEFAULT_PORT;
static char s_token[WEIXIN_TOKEN_MAX] = "";
static pthread_t s_poll_thread;
static volatile int s_running = 0;
#define DEDUP_RING_SIZE 16
static char s_dedup_ring[DEDUP_RING_SIZE][24];
static int s_dedup_idx = 0;
static int dedup_check_and_record(const char* msg_id_str)
{
if (!msg_id_str || msg_id_str[0] == '\0')
return 0;
for (int i = 0; i < DEDUP_RING_SIZE; i++) {
if (strcmp(s_dedup_ring[i], msg_id_str) == 0)
return 1;
}
strncpy(s_dedup_ring[s_dedup_idx], msg_id_str,
sizeof(s_dedup_ring[0]) - 1);
s_dedup_ring[s_dedup_idx][sizeof(s_dedup_ring[0]) - 1] = '\0';
s_dedup_idx = (s_dedup_idx + 1) % DEDUP_RING_SIZE;
return 0;
}
static const char B64_CHARS[]
= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static int b64_encode(const uint8_t* src, size_t src_len,
char* dst, size_t dst_cap)
{
size_t out = 0;
for (size_t i = 0; i < src_len; i += 3) {
if (out + 4 >= dst_cap)
return -1;
uint32_t v = (uint32_t)src[i] << 16;
if (i + 1 < src_len)
v |= (uint32_t)src[i + 1] << 8;
if (i + 2 < src_len)
v |= (uint32_t)src[i + 2];
dst[out++] = B64_CHARS[(v >> 18) & 0x3f];
dst[out++] = B64_CHARS[(v >> 12) & 0x3f];
dst[out++] = (i + 1 < src_len) ? B64_CHARS[(v >> 6) & 0x3f] : '=';
dst[out++] = (i + 2 < src_len) ? B64_CHARS[v & 0x3f] : '=';
}
dst[out] = '\0';
return (int)out;
}
static void weixin_gen_uin(char* uin_buf, size_t uin_cap)
{
uint32_t val = ((uint32_t)rand() << 16) ^ (uint32_t)rand();
char dec[16];
snprintf(dec, sizeof(dec), "%" PRIu32, val);
b64_encode((const uint8_t*)dec, strlen(dec), uin_buf, uin_cap);
}
static void weixin_build_headers(vela_header_t* hdrs, size_t n,
char* auth_buf, size_t auth_cap,
char* uin_buf, size_t uin_cap)
{
if (n < 5)
return;
snprintf(auth_buf, auth_cap, "Bearer %s", s_token);
weixin_gen_uin(uin_buf, uin_cap);
hdrs[0].name = "Content-Type";
hdrs[0].value = "application/json";
hdrs[1].name = "AuthorizationType";
hdrs[1].value = "ilink_bot_token";
hdrs[2].name = "Authorization";
hdrs[2].value = auth_buf;
hdrs[3].name = "X-WECHAT-UIN";
hdrs[3].value = uin_buf;
hdrs[4].name = NULL;
hdrs[4].value = NULL;
}
static int weixin_post(const char* path,
const char* json_body,
char* resp_buf, size_t resp_cap)
{
char auth_buf[WEIXIN_TOKEN_MAX + 16];
char uin_buf[32];
vela_header_t hdrs[5];
weixin_build_headers(hdrs, 5, auth_buf, sizeof(auth_buf),
uin_buf, sizeof(uin_buf));
return vela_https_post_json(
s_host, s_port, path,
hdrs, json_body,
resp_buf, resp_cap);
}
static void weixin_add_base_info(cJSON* req)
{
cJSON* bi = cJSON_CreateObject();
if (bi) {
cJSON_AddStringToObject(bi, "channel_version", WEIXIN_CHANNEL_VERSION);
cJSON_AddItemToObject(req, "base_info", bi);
}
}
static void weixin_dispatch_message(cJSON* msg_obj)
{
cJSON* from_j = cJSON_GetObjectItem(msg_obj, "from_user_id");
cJSON* type_j = cJSON_GetObjectItem(msg_obj, "message_type");
cJSON* state_j = cJSON_GetObjectItem(msg_obj, "message_state");
cJSON* items_j = cJSON_GetObjectItem(msg_obj, "item_list");
cJSON* ctx_j = cJSON_GetObjectItem(msg_obj, "context_token");
cJSON* id_j = cJSON_GetObjectItem(msg_obj, "message_id");
int msg_type = cJSON_IsNumber(type_j) ? (int)type_j->valuedouble : 0;
int msg_state = cJSON_IsNumber(state_j) ? (int)state_j->valuedouble : 2;
const char* from_raw = cJSON_IsString(from_j) ? from_j->valuestring : "?";
* triggers NuttX float formatting and corrupts stack canary
* on real hardware (lib_stackchk.c:57 crash). */
char msg_id_str[24] = "";
if (cJSON_IsNumber(id_j)) {
char* tmp = cJSON_PrintUnformatted(id_j);
if (tmp) {
strncpy(msg_id_str, tmp, sizeof(msg_id_str) - 1);
free(tmp);
}
}
syslog(LOG_DEBUG, "[%s] dispatch: type=%d state=%d from=%.30s id=%s\n",
TAG, msg_type, msg_state, from_raw, msg_id_str);
if (msg_type != 1 || msg_state != 2) {
syslog(LOG_DEBUG, "[%s] skip: type=%d state=%d (want 1/2)\n",
TAG, msg_type, msg_state);
return;
}
if (dedup_check_and_record(msg_id_str)) {
syslog(LOG_DEBUG, "[%s] skip: dedup id=%s\n", TAG, msg_id_str);
return;
}
const char* from_user_id = cJSON_IsString(from_j) ? from_j->valuestring : "";
const char* ctx_token = cJSON_IsString(ctx_j) ? ctx_j->valuestring : "";
#define TEXT_BUF_SIZE 4096
char* text_buf = calloc(1, TEXT_BUF_SIZE);
if (!text_buf) {
syslog(LOG_ERR, "[%s] OOM: text_buf\n", TAG);
return;
}
if (cJSON_IsArray(items_j)) {
int n = cJSON_GetArraySize(items_j);
for (int i = 0; i < n; i++) {
cJSON* item = cJSON_GetArrayItem(items_j, i);
cJSON* itype = cJSON_GetObjectItem(item, "type");
cJSON* titem = cJSON_GetObjectItem(item, "text_item");
int itype_v = cJSON_IsNumber(itype)
? (int)itype->valuedouble
: 0;
if (itype_v == 1 && cJSON_IsObject(titem)) {
cJSON* txt = cJSON_GetObjectItem(titem, "text");
if (cJSON_IsString(txt) && txt->valuestring[0]) {
size_t cur = strlen(text_buf);
size_t rem = TEXT_BUF_SIZE - cur - 1;
if (cur > 0 && rem > 1) {
text_buf[cur] = ' ';
cur++;
rem--;
}
strncpy(text_buf + cur, txt->valuestring, rem);
text_buf[TEXT_BUF_SIZE - 1] = '\0';
}
}
}
}
if (text_buf[0] == '\0') {
free(text_buf);
return;
}
syslog(LOG_INFO, "[%s] inbound from=%s len=%zu\n",
TAG, from_user_id, strlen(text_buf));
char chat_id[128];
snprintf(chat_id, sizeof(chat_id), "%s|%s",
from_user_id, ctx_token);
agent_msg_t bus_msg;
memset(&bus_msg, 0, sizeof(bus_msg));
strncpy(bus_msg.channel, AGENT_CHAN_WEIXIN,
sizeof(bus_msg.channel) - 1);
strncpy(bus_msg.chat_id, chat_id, sizeof(bus_msg.chat_id) - 1);
bus_msg.content = strdup(text_buf);
free(text_buf);
if (!bus_msg.content) {
syslog(LOG_ERR, "[%s] OOM in weixin_dispatch_message\n", TAG);
return;
}
if (message_bus_push_inbound(&bus_msg) != OK) {
syslog(LOG_WARNING, "[%s] message_bus_push_inbound failed\n",
TAG);
free(bus_msg.content);
}
}
static void* weixin_poll_task(void* arg)
{
(void)arg;
syslog(LOG_INFO, "[%s] Long-poll started (%s:%s)\n",
TAG, s_host, s_port);
char cursor[WEIXIN_CURSOR_MAX] = "";
int err_backoff = WEIXIN_RECONNECT_DELAY_S;
char* resp_buf = malloc(WEIXIN_RESP_BUF_SIZE);
if (!resp_buf) {
syslog(LOG_ERR, "[%s] OOM: response buffer\n", TAG);
return NULL;
}
while (s_running) {
if (s_token[0] == '\0') {
syslog(LOG_WARNING, "[%s] No token, waiting...\n", TAG);
sleep(WEIXIN_RECONNECT_DELAY_S);
continue;
}
cJSON* req = cJSON_CreateObject();
if (!req) {
sleep(1);
continue;
}
cJSON_AddStringToObject(req, "get_updates_buf", cursor);
weixin_add_base_info(req);
char* req_str = cJSON_PrintUnformatted(req);
cJSON_Delete(req);
if (!req_str) {
sleep(1);
continue;
}
memset(resp_buf, 0, WEIXIN_RESP_BUF_SIZE);
int http_rc = weixin_post("/ilink/bot/getupdates",
req_str, resp_buf, WEIXIN_RESP_BUF_SIZE);
free(req_str);
if (http_rc < 0 || http_rc != 200) {
syslog(LOG_WARNING, "[%s] getupdates HTTP %d\n",
TAG, http_rc);
sleep(WEIXIN_RECONNECT_DELAY_S);
continue;
}
cJSON* resp = cJSON_Parse(resp_buf);
if (!resp) {
syslog(LOG_WARNING, "[%s] JSON parse failed\n", TAG);
sleep(1);
continue;
}
cJSON* ret_j = cJSON_GetObjectItem(resp, "ret");
cJSON* msgs_j = cJSON_GetObjectItem(resp, "msgs");
int ret;
if (cJSON_IsNumber(ret_j)) {
ret = (int)ret_j->valuedouble;
} else if (cJSON_IsArray(msgs_j)) {
* just "msgs" array directly. Treat as ret=0. */
ret = 0;
} else {
ret = -1;
}
if (ret == -14) {
syslog(LOG_WARNING, "[%s] Session expired (ret=-14), "
"need re-login\n",
TAG);
cursor[0] = '\0';
s_token[0] = '\0';
claw_config_set("weixin.token", "");
cJSON_Delete(resp);
sleep(WEIXIN_RECONNECT_DELAY_S);
continue;
}
if (ret != 0) {
syslog(LOG_WARNING, "[%s] getupdates ret=%d, "
"backoff %ds\n",
TAG, ret, err_backoff);
cJSON_Delete(resp);
sleep(err_backoff);
if (err_backoff < 60)
err_backoff *= 2;
continue;
}
err_backoff = WEIXIN_RECONNECT_DELAY_S;
cJSON* next_cursor_j = cJSON_GetObjectItem(resp, "get_updates_buf");
if (cJSON_IsString(next_cursor_j)
&& next_cursor_j->valuestring[0]) {
strncpy(cursor, next_cursor_j->valuestring,
sizeof(cursor) - 1);
cursor[sizeof(cursor) - 1] = '\0';
}
if (cJSON_IsArray(msgs_j)) {
int n = cJSON_GetArraySize(msgs_j);
syslog(n > 0 ? LOG_INFO : LOG_DEBUG,
"[%s] poll: ret=%d msgs=%d cursor=%.20s\n",
TAG, ret, n, cursor);
for (int i = 0; i < n && i < WEIXIN_MAX_MSGS_PER_POLL; i++) {
cJSON* m = cJSON_GetArrayItem(msgs_j, i);
if (cJSON_IsObject(m))
weixin_dispatch_message(m);
}
}
cJSON_Delete(resp);
}
free(resp_buf);
resp_buf = NULL;
syslog(LOG_INFO, "[%s] Poll task exited\n", TAG);
return NULL;
}
int weixin_channel_init(void)
{
char host_cfg[WEIXIN_HOST_MAX] = "";
char port_cfg[WEIXIN_PORT_MAX] = "";
char token_cfg[WEIXIN_TOKEN_MAX] = "";
claw_config_get("weixin.host", host_cfg, sizeof(host_cfg));
claw_config_get("weixin.port", port_cfg, sizeof(port_cfg));
claw_config_get("weixin.token", token_cfg, sizeof(token_cfg));
if (host_cfg[0]) {
strncpy(s_host, host_cfg, sizeof(s_host) - 1);
s_host[sizeof(s_host) - 1] = '\0';
}
if (port_cfg[0]) {
strncpy(s_port, port_cfg, sizeof(s_port) - 1);
s_port[sizeof(s_port) - 1] = '\0';
}
if (token_cfg[0]) {
strncpy(s_token, token_cfg, sizeof(s_token) - 1);
s_token[sizeof(s_token) - 1] = '\0';
}
srand((unsigned int)time(NULL));
syslog(LOG_INFO, "[%s] init: host=%s port=%s token=%s\n",
TAG, s_host, s_port, s_token[0] ? "set" : "(none)");
return 0;
}
int weixin_channel_start(void)
{
if (s_running) {
syslog(LOG_WARNING, "[%s] Already running\n", TAG);
return 0;
}
s_running = 1;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, AGENT_WEIXIN_STACK);
int rc = pthread_create(&s_poll_thread, &attr,
weixin_poll_task, NULL);
pthread_attr_destroy(&attr);
if (rc != 0) {
syslog(LOG_ERR, "[%s] pthread_create failed: %d\n", TAG, rc);
s_running = 0;
return -1;
}
syslog(LOG_INFO, "[%s] Poll task started\n", TAG);
return 0;
}
int weixin_channel_send(const char* to_user_id,
const char* context_token,
const char* text)
{
if (!to_user_id || !text)
return -1;
if (s_token[0] == '\0') {
syslog(LOG_WARNING, "[%s] send: no token\n", TAG);
return -1;
}
cJSON* root = cJSON_CreateObject();
cJSON* msg_obj = cJSON_CreateObject();
cJSON* items = cJSON_CreateArray();
cJSON* item = cJSON_CreateObject();
cJSON* txt_item = cJSON_CreateObject();
if (!root || !msg_obj || !items || !item || !txt_item) {
syslog(LOG_ERR, "[%s] send: cJSON OOM\n", TAG);
cJSON_Delete(root);
cJSON_Delete(msg_obj);
cJSON_Delete(items);
cJSON_Delete(item);
cJSON_Delete(txt_item);
return -1;
}
cJSON_AddStringToObject(msg_obj, "from_user_id", "");
cJSON_AddStringToObject(msg_obj, "to_user_id", to_user_id);
char client_id[64];
snprintf(client_id, sizeof(client_id),
"agent-%ld-%04x",
(long)time(NULL), (unsigned)(rand() & 0xffff));
cJSON_AddStringToObject(msg_obj, "client_id", client_id);
cJSON_AddNumberToObject(msg_obj, "message_type", 2);
cJSON_AddNumberToObject(msg_obj, "message_state", 2);
if (context_token && context_token[0])
cJSON_AddStringToObject(msg_obj, "context_token", context_token);
cJSON_AddStringToObject(txt_item, "text", text);
cJSON_AddNumberToObject(item, "type", 1);
cJSON_AddItemToObject(item, "text_item", txt_item);
cJSON_AddItemToArray(items, item);
cJSON_AddItemToObject(msg_obj, "item_list", items);
cJSON_AddItemToObject(root, "msg", msg_obj);
weixin_add_base_info(root);
char* req_str = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!req_str) {
syslog(LOG_ERR, "[%s] send: cJSON OOM\n", TAG);
return -1;
}
char resp_buf[1024] = "";
int http_rc = weixin_post("/ilink/bot/sendmessage",
req_str, resp_buf, sizeof(resp_buf));
free(req_str);
if (http_rc != 200) {
syslog(LOG_WARNING, "[%s] sendmessage HTTP %d to %s\n",
TAG, http_rc, to_user_id);
return -1;
}
if (resp_buf[0]) {
cJSON* resp = cJSON_Parse(resp_buf);
if (resp) {
cJSON* ec = cJSON_GetObjectItem(resp, "errcode");
if (ec && cJSON_IsNumber(ec) && ec->valueint != 0) {
cJSON* em = cJSON_GetObjectItem(resp, "errmsg");
syslog(LOG_WARNING,
"[%s] sendmessage errcode=%lld errmsg=%s\n",
TAG, (long long)ec->valueint,
(em && em->valuestring) ? em->valuestring : "?");
cJSON_Delete(resp);
return -1;
}
cJSON_Delete(resp);
}
}
syslog(LOG_INFO, "[%s] Sent to %s\n", TAG, to_user_id);
return 0;
}
void weixin_channel_stop(void)
{
if (!s_running)
return;
s_running = 0;
pthread_join(s_poll_thread, NULL);
syslog(LOG_INFO, "[%s] Channel stopped\n", TAG);
}
int weixin_channel_set_token(const char* token, unsigned int uin)
{
(void)uin;
if (!token || !token[0])
return -1;
strncpy(s_token, token, sizeof(s_token) - 1);
s_token[sizeof(s_token) - 1] = '\0';
claw_config_set("weixin.token", s_token);
syslog(LOG_INFO, "[%s] Token updated\n", TAG);
return 0;
}
int weixin_channel_login(char* qr_url, size_t qr_cap,
char* qrcode_id, size_t qrc_cap)
{
if (!qr_url || !qrcode_id)
return -1;
char resp_buf[2048] = "";
int rc = vela_https_get(s_host, s_port,
"/ilink/bot/get_bot_qrcode?bot_type=3",
resp_buf, sizeof(resp_buf));
if (rc != 200) {
syslog(LOG_WARNING, "[%s] get_bot_qrcode HTTP %d\n", TAG, rc);
return -1;
}
cJSON* resp = cJSON_Parse(resp_buf);
if (!resp) {
syslog(LOG_WARNING, "[%s] get_bot_qrcode JSON fail\n", TAG);
return -1;
}
cJSON* qrc_j = cJSON_GetObjectItem(resp, "qrcode");
cJSON* url_j = cJSON_GetObjectItem(resp, "qrcode_img_content");
if (!cJSON_IsString(qrc_j) || !cJSON_IsString(url_j)) {
syslog(LOG_WARNING, "[%s] get_bot_qrcode missing fields\n", TAG);
cJSON_Delete(resp);
return -1;
}
strncpy(qrcode_id, qrc_j->valuestring, qrc_cap - 1);
qrcode_id[qrc_cap - 1] = '\0';
strncpy(qr_url, url_j->valuestring, qr_cap - 1);
qr_url[qr_cap - 1] = '\0';
cJSON_Delete(resp);
syslog(LOG_INFO, "[%s] QR code ready: %s\n", TAG, qr_url);
return 0;
}
int weixin_channel_poll_login(const char* qrcode_id)
{
if (!qrcode_id || !qrcode_id[0])
return -1;
char path[256];
int n = snprintf(path, sizeof(path),
"/ilink/bot/get_qrcode_status?qrcode=%s", qrcode_id);
if (n < 0 || (size_t)n >= sizeof(path))
return -1;
char resp_buf[2048] = "";
int rc = vela_https_get(s_host, s_port, path,
resp_buf, sizeof(resp_buf));
if (rc != 200)
return -2;
cJSON* resp = cJSON_Parse(resp_buf);
if (!resp)
return -1;
cJSON* status_j = cJSON_GetObjectItem(resp, "status");
const char* status = cJSON_IsString(status_j)
? status_j->valuestring
: "";
int result;
if (strcmp(status, "confirmed") == 0) {
cJSON* tok_j = cJSON_GetObjectItem(resp, "bot_token");
cJSON* base_j = cJSON_GetObjectItem(resp, "baseurl");
if (cJSON_IsString(tok_j)) {
strncpy(s_token, tok_j->valuestring,
sizeof(s_token) - 1);
s_token[sizeof(s_token) - 1] = '\0';
claw_config_set("weixin.token", s_token);
}
if (cJSON_IsString(base_j) && base_j->valuestring[0]) {
parsed_url_t parsed;
if (url_parse(base_j->valuestring, &parsed) == 0) {
strncpy(s_host, parsed.host, sizeof(s_host) - 1);
s_host[sizeof(s_host) - 1] = '\0';
} else {
strncpy(s_host, base_j->valuestring, sizeof(s_host) - 1);
s_host[sizeof(s_host) - 1] = '\0';
}
claw_config_set("weixin.host", s_host);
}
syslog(LOG_INFO, "[%s] Login confirmed, token saved\n", TAG);
result = 1;
} else if (strcmp(status, "scaned") == 0) {
result = 2;
} else if (strcmp(status, "expired") == 0) {
result = -3;
} else {
result = 0;
}
cJSON_Delete(resp);
return result;
}