docs: weixin ClawBot bridge protocol and examples
This commit is contained in:
commit
4ea3957da9
13 files changed
+2158
No files matched your search
@@ -0,0 +1,203 @@
|
||||
/* ilink_client.c — 微信 ClawBot iLink 最小示例(C,依赖 libcurl)
|
||||
* 流程: 扫码登录 → 长轮询收消息 → 回声回复
|
||||
* 编译: gcc ilink_client.c -o ilink -lcurl
|
||||
*
|
||||
* 注意: 为控制依赖,JSON 用最朴素的字符串查找提取,仅适合演示。
|
||||
* 正式项目请引入 cJSON,媒体加解密见 docs/03(可用 OpenSSL AES-128-ECB)。
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#include <sys/stat.h>
|
||||
#include <curl/curl.h>
|
||||
|
||||
#define BASE_URL "https://ilinkai.weixin.qq.com"
|
||||
#define STATE_FILE "bridge-state.json"
|
||||
|
||||
/* ---------- 响应缓冲 ---------- */
|
||||
struct buf { char *data; size_t len; };
|
||||
|
||||
static size_t on_write(char *ptr, size_t size, size_t nmemb, void *ud) {
|
||||
struct buf *b = ud;
|
||||
size_t n = size * nmemb;
|
||||
b->data = realloc(b->data, b->len + n + 1);
|
||||
memcpy(b->data + b->len, ptr, n);
|
||||
b->len += n;
|
||||
b->data[b->len] = 0;
|
||||
return n;
|
||||
}
|
||||
|
||||
/* ---------- 状态: bot_token / 游标(极简 key=value 行存储) ---------- */
|
||||
static char g_token[512] = "", g_cursor[1024] = "";
|
||||
|
||||
static void load_state(void) {
|
||||
FILE *f = fopen(STATE_FILE, "r");
|
||||
if (!f) return;
|
||||
char line[2048], key[64], val[1900];
|
||||
while (fgets(line, sizeof line, f))
|
||||
if (sscanf(line, "%63[^=]=%1899[^\n]", key, val) == 2) {
|
||||
if (!strcmp(key, "bot_token")) snprintf(g_token, sizeof g_token, "%s", val);
|
||||
if (!strcmp(key, "cursor")) snprintf(g_cursor, sizeof g_cursor, "%s", val);
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
static void save_state(void) {
|
||||
FILE *f = fopen(STATE_FILE, "w");
|
||||
fprintf(f, "bot_token=%s\ncursor=%s\n", g_token, g_cursor);
|
||||
fclose(f);
|
||||
chmod(STATE_FILE, 0600);
|
||||
}
|
||||
|
||||
/* ---------- 通用请求: 鉴权头 + 防重放 X-WECHAT-UIN ---------- */
|
||||
static char *http(const char *method, const char *path, const char *json_body, long timeout) {
|
||||
CURL *c = curl_easy_init();
|
||||
struct buf b = {0};
|
||||
char url[512], uin[32], auth[600];
|
||||
snprintf(url, sizeof url, "%s%s", BASE_URL, path);
|
||||
|
||||
/* X-WECHAT-UIN: base64(十进制字符串(随机uint32)),简化用伪随机 */
|
||||
snprintf(uin, sizeof uin, "%u", (unsigned)(rand() ^ (unsigned)time(NULL)));
|
||||
char uin_b64[64];
|
||||
/* 极简 base64(仅数字串,查表法) */
|
||||
const char *T = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
size_t ul = strlen(uin), o = 0;
|
||||
for (size_t i = 0; i < ul; i += 3) {
|
||||
unsigned v = uin[i] << 16 | (i+1<ul ? uin[i+1] : 0) << 8 | (i+2<ul ? uin[i+2] : 0);
|
||||
uin_b64[o++] = T[v>>18&63]; uin_b64[o++] = T[v>>12&63];
|
||||
uin_b64[o++] = i+1<ul ? T[v>>6&63] : '=';
|
||||
uin_b64[o++] = i+2<ul ? T[v&63] : '=';
|
||||
}
|
||||
uin_b64[o] = 0;
|
||||
|
||||
struct curl_slist *hdrs = NULL;
|
||||
hdrs = curl_slist_append(hdrs, "Content-Type: application/json");
|
||||
hdrs = curl_slist_append(hdrs, "AuthorizationType: ilink_bot_token");
|
||||
char uin_hdr[96]; snprintf(uin_hdr, sizeof uin_hdr, "X-WECHAT-UIN: %s", uin_b64);
|
||||
hdrs = curl_slist_append(hdrs, uin_hdr);
|
||||
if (g_token[0]) {
|
||||
snprintf(auth, sizeof auth, "Authorization: Bearer %s", g_token);
|
||||
hdrs = curl_slist_append(hdrs, auth);
|
||||
}
|
||||
|
||||
curl_easy_setopt(c, CURLOPT_URL, url);
|
||||
curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, on_write);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEDATA, &b);
|
||||
curl_easy_setopt(c, CURLOPT_TIMEOUT, timeout);
|
||||
if (!strcmp(method, "POST")) curl_easy_setopt(c, CURLOPT_POSTFIELDS, json_body);
|
||||
|
||||
CURLcode rc = curl_easy_perform(c);
|
||||
curl_slist_free_all(hdrs);
|
||||
curl_easy_cleanup(c);
|
||||
if (rc != CURLE_OK) { free(b.data); return NULL; } /* 超时/网络错误返回 NULL */
|
||||
return b.data ? b.data : strdup("");
|
||||
}
|
||||
|
||||
/* ---------- 极简 JSON 字段提取(仅演示!正式项目用 cJSON) ---------- */
|
||||
static int pick(const char *json, const char *key, char *out, size_t outsz) {
|
||||
char pat[128]; snprintf(pat, sizeof pat, "\"%s\":\"", key);
|
||||
const char *p = strstr(json, pat);
|
||||
if (!p) return 0;
|
||||
p += strlen(pat);
|
||||
const char *e = strchr(p, '"');
|
||||
if (!e || (size_t)(e - p) >= outsz) return 0;
|
||||
memcpy(out, p, e - p); out[e - p] = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---------- 接口1+2: 扫码登录(get_bot_qrcode / get_qrcode_status) ---------- */
|
||||
static void login(void) {
|
||||
char *qr = http("GET", "/ilink/bot/get_bot_qrcode?bot_type=3", NULL, 15);
|
||||
char qrcode[256], qrurl[512];
|
||||
if (!qr || !pick(qr, "qrcode", qrcode, sizeof qrcode)) { fprintf(stderr, "获取二维码失败\n"); exit(1); }
|
||||
pick(qr, "qrcode_img_content", qrurl, sizeof qrurl);
|
||||
printf("请用微信扫码: %s\n", qrurl);
|
||||
free(qr);
|
||||
|
||||
char path[320], status[32], token[512];
|
||||
snprintf(path, sizeof path, "/ilink/bot/get_qrcode_status?qrcode=%s", qrcode);
|
||||
for (int i = 0; i < 480; i++) { /* 最多等 8 分钟 */
|
||||
char *s = http("GET", path, NULL, 15);
|
||||
if (s && pick(s, "status", status, sizeof status) && !strcmp(status, "confirmed")) {
|
||||
pick(s, "bot_token", token, sizeof token);
|
||||
snprintf(g_token, sizeof g_token, "%s", token);
|
||||
save_state();
|
||||
printf("登录成功\n");
|
||||
free(s);
|
||||
return;
|
||||
}
|
||||
free(s);
|
||||
sleep(1);
|
||||
}
|
||||
fprintf(stderr, "扫码超时\n"); exit(1);
|
||||
}
|
||||
|
||||
/* ---------- 你的 AI 后端: 替换成自己项目的调用 ---------- */
|
||||
static void ask_your_ai(const char *peer, const char *text, char *out, size_t n) {
|
||||
snprintf(out, n, "你说的是:%s", text);
|
||||
}
|
||||
|
||||
/* ---------- 接口4: POST /ilink/bot/sendmessage ---------- */
|
||||
static void send_text(const char *to, const char *text, const char *ctx_token) {
|
||||
/* 演示不处理 JSON 转义与分片,正式项目务必转义 \ " 换行 并按需分片 */
|
||||
char body[4096];
|
||||
snprintf(body, sizeof body,
|
||||
"{\"msg\":{\"to_user_id\":\"%s\",\"client_id\":\"c-%ld\","
|
||||
"\"message_type\":2,\"message_state\":2,\"context_token\":\"%s\","
|
||||
"\"item_list\":[{\"type\":1,\"text_item\":{\"text\":\"%s\"}}]}}",
|
||||
to, (long)time(NULL), ctx_token, text);
|
||||
free(http("POST", "/ilink/bot/sendmessage", body, 15));
|
||||
}
|
||||
|
||||
/* ---------- 接口3: POST /ilink/bot/getupdates 长轮询 ---------- */
|
||||
static void poll_loop(void) {
|
||||
int failures = 0;
|
||||
char body[1200];
|
||||
for (;;) {
|
||||
snprintf(body, sizeof body,
|
||||
"{\"get_updates_buf\":\"%s\",\"base_info\":{\"channel_version\":\"1.0.2\"}}",
|
||||
g_cursor);
|
||||
char *resp = http("POST", "/ilink/bot/getupdates", body, 45);
|
||||
if (!resp) { /* 超时/网络错误 */
|
||||
failures++;
|
||||
int wait = failures <= 3 ? 2 : 30;
|
||||
fprintf(stderr, "轮询异常(%d次),%ds 后重试\n", failures, wait);
|
||||
sleep(wait);
|
||||
continue;
|
||||
}
|
||||
if (strstr(resp, "\"errcode\":-14")) {
|
||||
fprintf(stderr, "会话过期(errcode=-14),请删除 %s 重新扫码\n", STATE_FILE);
|
||||
free(resp); exit(2);
|
||||
}
|
||||
char buf[1024];
|
||||
if (pick(resp, "get_updates_buf", buf, sizeof buf) && buf[0]) {
|
||||
snprintf(g_cursor, sizeof g_cursor, "%s", buf);
|
||||
save_state(); /* 游标立即落盘 */
|
||||
}
|
||||
/* 演示级提取: 仅取第一条消息的文本/context_token/from_user_id */
|
||||
char peer[128], token[512], text[1500];
|
||||
if (pick(resp, "from_user_id", peer, sizeof peer) &&
|
||||
pick(resp, "context_token", token, sizeof token) &&
|
||||
pick(resp, "text", text, sizeof text) &&
|
||||
!strstr(resp, "\"message_type\":2")) {
|
||||
char reply[1700];
|
||||
ask_your_ai(peer, text, reply, sizeof reply);
|
||||
send_text(peer, reply, token);
|
||||
}
|
||||
free(resp);
|
||||
failures = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
srand((unsigned)time(NULL));
|
||||
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||
load_state();
|
||||
if (!g_token[0]) login();
|
||||
poll_loop();
|
||||
curl_global_cleanup();
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user