docs/examples: 扫码登录改为官方 POST+local_token_list+iLink-App-Id/ClientVersion 头 + 完整状态机 + 换码竞态警告

This commit is contained in:
AskaEth committed 2026-09-08 02:46:07 +08:00
1 parent 38b776705d
commit 8a36915377
6 files changed
+566 -96

No files matched your search

+89 -17
View File
@@ -16,6 +16,9 @@
#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; };
@@ -51,12 +54,14 @@ static void save_state(void) {
chmod(STATE_FILE, 0600);
}
/* ---------- 通用请求: 鉴权头 + 防重放 X-WECHAT-UIN ---------- */
static char *http(const char *method, const char *path, const char *json_body, long timeout) {
/* ---------- 通用请求: 鉴权头 + 防重放 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_URL, path);
snprintf(url, sizeof url, "%s%s", base, path);
/* X-WECHAT-UIN: base64(十进制字符串(随机uint32)),简化用伪随机 */
snprintf(uin, sizeof uin, "%u", (unsigned)(rand() ^ (unsigned)time(NULL)));
@@ -77,6 +82,9 @@ static char *http(const char *method, const char *path, const char *json_body, l
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);
/* 2.4.8 -> (2&0xff)<<16 | (4&0xff)<<8 | (8&0xff) = 132104(数字字符串,非 "1") */
hdrs = curl_slist_append(hdrs, "iLink-App-Id: bot");
hdrs = curl_slist_append(hdrs, "iLink-App-ClientVersion: 132104");
if (g_token[0]) {
snprintf(auth, sizeof auth, "Authorization: Bearer %s", g_token);
hdrs = curl_slist_append(hdrs, auth);
@@ -108,26 +116,90 @@ static int pick(const char *json, const char *key, char *out, size_t outsz) {
return 1;
}
/* ---------- 接口1+2: 扫码登录(get_bot_qrcode / get_qrcode_status) ---------- */
/* ---------- 接口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) {
char *qr = http("GET", "/ilink/bot/get_bot_qrcode?bot_type=3", NULL, 15);
/* 状态机: 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 path[320], status[32], token[512];
snprintf(path, sizeof path, "/ilink/bot/get_qrcode_status?qrcode=%s", qrcode);
char verify[32] = "";
char status[32], token[512];
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;
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);
@@ -149,7 +221,7 @@ static void send_text(const char *to, const char *text, const char *ctx_token) {
"\"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));
free(http(BASE_URL, "POST", "/ilink/bot/sendmessage", body, 15));
}
/* ---------- 接口3: POST /ilink/bot/getupdates 长轮询 ---------- */
@@ -160,7 +232,7 @@ static void poll_loop(void) {
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);
char *resp = http(BASE_URL, "POST", "/ilink/bot/getupdates", body, 45);
if (!resp) { /* 超时/网络错误 */
failures++;
int wait = failures <= 3 ? 2 : 30;