/* ilink_client.c — 微信 ClawBot iLink 最小示例(C,依赖 libcurl) * 流程: 扫码登录 → 长轮询收消息 → 回声回复 * 编译: gcc ilink_client.c -o ilink -lcurl * * 注意: 为控制依赖,JSON 用最朴素的字符串查找提取,仅适合演示。 * 正式项目请引入 cJSON,媒体加解密见 docs/03(可用 OpenSSL AES-128-ECB)。 */ #include #include #include #include #include #include #include #define BASE_URL "https://ilinkai.weixin.qq.com" #define STATE_FILE "bridge-state.json" /* 扫码轮询 base;收到 scaned_but_redirect 时切换,get_bot_qrcode 仍走固定 BASE_URL */ static char g_base_url[256] = BASE_URL; /* ---------- 响应缓冲 ---------- */ 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 + iLink-App 标识 ---------- */ /* 注: get_bot_qrcode / get_qrcode_status(预绑定阶段)无 token 时不带 Authorization, 但 iLink-App-Id / iLink-App-ClientVersion / AuthorizationType / X-WECHAT-UIN 恒带。 */ static char *http(const char *base, 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, 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
    >18&63]; uin_b64[o++] = T[v>>12&63]; uin_b64[o++] = i+1
      >6&63] : '='; uin_b64[o++] = i+2
        = outsz) return 0; memcpy(out, p, e - p); out[e - p] = 0; return 1; } /* ---------- 接口1: POST /ilink/bot/get_bot_qrcode 获取登录二维码(官方姿势) ---------- */ static char *fetch_qrcode(void) { /* 官方为 POST + body {"local_token_list":[]},非裸 GET(裸 GET 也能拿到码但非官方姿势)。 首次绑定传空数组;若已持久化其它 bot_token 可收集传入(最多 10 个)。 */ return http(BASE_URL, "POST", "/ilink/bot/get_bot_qrcode?bot_type=3", "{\"local_token_list\":[]}", 15); } /* ---------- 接口2: GET /ilink/bot/get_qrcode_status 轮询扫码状态(完整状态机) ---------- */ static void login(void) { /* 状态机: wait / scaned / scaned_but_redirect / need_verifycode / verify_code_blocked / binded_redirect / confirmed / expired 两大坑(务必读 docs/02 §4.5): ① 码服务端 TTL≈60s,生成后须在窗口内扫(超窗服务端自返回 expired,此处换新码)。 ② 「换码竞态」:一旦收到 scaned(或 scaned_but_redirect / need_verifycode), 绝不能再换码——换码会砍掉"扫码→确认"会话,微信端显示成功但后端永远收不到 confirmed。 */ char *qr = fetch_qrcode(); 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); printf("(二维码内容即上面的 liteapp URL,请用二维码库编码展示;勿把 qrcode 字段当二维码)\n"); free(qr); char verify[32] = ""; char status[32], token[512]; for (int i = 0; i < 480; i++) { /* 最多等 8 分钟 */ char path[600]; snprintf(path, sizeof path, "/ilink/bot/get_qrcode_status?qrcode=%s", qrcode); if (verify[0]) { size_t len = strlen(path); snprintf(path + len, sizeof path - len, "&verify_code=%s", verify); } char *s = http(g_base_url, "GET", path, NULL, 15); if (s && pick(s, "status", status, sizeof status)) { if (!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; } else if (!strcmp(status, "scaned")) { verify[0] = 0; printf("已扫码,请在手机上确认...(此后绝不换码,避免砍掉确认会话)\n"); } else if (!strcmp(status, "scaned_but_redirect")) { char host[256]; verify[0] = 0; if (pick(s, "redirect_host", host, sizeof host) && host[0]) { snprintf(g_base_url, sizeof g_base_url, "https://%s", host); printf("IDC 重定向,切换轮询至 %s\n", g_base_url); } } else if (!strcmp(status, "need_verifycode")) { printf("请在手机微信上输入显示的数字后回车: "); fflush(stdout); if (fgets(verify, sizeof verify, stdin)) { char *nl = strchr(verify, '\n'); if (nl) *nl = 0; } continue; } else if (!strcmp(status, "verify_code_blocked")) { char *nq; verify[0] = 0; printf("配对码多次输入错误,重新获取二维码\n"); nq = fetch_qrcode(); if (!nq || !pick(nq, "qrcode", qrcode, sizeof qrcode)) { free(nq); fprintf(stderr, "获取二维码失败\n"); exit(1); } pick(nq, "qrcode_img_content", qrurl, sizeof qrurl); printf("新二维码: %s\n", qrurl); free(nq); continue; } else if (!strcmp(status, "binded_redirect")) { printf("该 bot 已绑定过本实例,视为连接成功\n"); free(s); return; } else if (!strcmp(status, "expired")) { char *nq; verify[0] = 0; /* 码过期(服务端作废)→ 换新码;仅在 expired 时换,scaned 后绝不换 */ printf("二维码已过期,重新获取\n"); nq = fetch_qrcode(); if (!nq || !pick(nq, "qrcode", qrcode, sizeof qrcode)) { free(nq); fprintf(stderr, "获取二维码失败\n"); exit(1); } pick(nq, "qrcode_img_content", qrurl, sizeof qrurl); printf("新二维码: %s\n", qrurl); free(nq); continue; } else { fprintf(stderr, "未知状态: %s\n", status); } } 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(BASE_URL, "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(BASE_URL, "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; }