docs/examples: 扫码登录改为官方 POST+local_token_list+iLink-App-Id/ClientVersion 头 + 完整状态机 + 换码竞态警告
This commit is contained in:
1 parent
38b776705d
commit
8a36915377
6 files changed
+566
-96
No files matched your search
@@ -19,6 +19,9 @@
|
||||
static const std::string BASE_URL = "https://ilinkai.weixin.qq.com";
|
||||
static const std::string STATE_FILE = "bridge-state.json";
|
||||
|
||||
// 扫码轮询 base;收到 scaned_but_redirect 时切换,get_bot_qrcode 仍走固定 BASE_URL
|
||||
static std::string apiBaseUrl = BASE_URL;
|
||||
|
||||
// ---------- 状态: bot_token / 游标(极简 key=value 行存储) ----------
|
||||
static std::string g_token, g_cursor;
|
||||
|
||||
@@ -63,8 +66,11 @@ static std::string b64(const std::string &in) { // 极简 base64
|
||||
}
|
||||
|
||||
// 返回空 optional 表示传输层错误(超时/断网);业务错误看响应内容
|
||||
static std::optional<std::string> http(const std::string &method, const std::string &path,
|
||||
const std::string &body = "", long timeout = 45) {
|
||||
// 注: get_bot_qrcode / get_qrcode_status(预绑定阶段)无 token 时不带 Authorization,
|
||||
// 但 iLink-App-Id / iLink-App-ClientVersion / AuthorizationType / X-WECHAT-UIN 恒带。
|
||||
static std::optional<std::string> http(const std::string &base, const std::string &method,
|
||||
const std::string &path, const std::string &body = "",
|
||||
long timeout = 45) {
|
||||
CURL *c = curl_easy_init();
|
||||
std::string resp;
|
||||
static thread_local std::mt19937 rng{std::random_device{}()};
|
||||
@@ -74,13 +80,16 @@ static std::optional<std::string> http(const std::string &method, const std::str
|
||||
hdrs = curl_slist_append(hdrs, "AuthorizationType: ilink_bot_token");
|
||||
std::string uin = "X-WECHAT-UIN: " + b64(std::to_string(rng())); // 防重放
|
||||
hdrs = curl_slist_append(hdrs, uin.c_str());
|
||||
// 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");
|
||||
std::string auth;
|
||||
if (!g_token.empty()) {
|
||||
auth = "Authorization: Bearer " + g_token;
|
||||
hdrs = curl_slist_append(hdrs, auth.c_str());
|
||||
}
|
||||
|
||||
std::string url = BASE_URL + path;
|
||||
std::string url = base + path;
|
||||
curl_easy_setopt(c, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, on_write);
|
||||
@@ -114,19 +123,77 @@ static std::string esc(const std::string &s) { // 最小 JSON 转义
|
||||
return o;
|
||||
}
|
||||
|
||||
// ---------- 接口1+2: 扫码登录(get_bot_qrcode / get_qrcode_status) ----------
|
||||
// ---------- 接口1: POST /ilink/bot/get_bot_qrcode 获取登录二维码(官方姿势) ----------
|
||||
static std::optional<std::string> fetch_qrcode() {
|
||||
// 官方为 POST + body {"local_token_list":[]},非裸 GET。首次绑定传空数组;
|
||||
// 若已持久化其它 bot_token 可收集传入(最多 10 个),官方据此识别触发 binded_redirect。
|
||||
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() {
|
||||
auto qr = http("GET", "/ilink/bot/get_bot_qrcode?bot_type=3", "", 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。
|
||||
auto qr = fetch_qrcode();
|
||||
if (!qr) { std::cerr << "获取二维码失败\n"; exit(1); }
|
||||
std::string qrcode = pick(*qr, "qrcode");
|
||||
std::cout << "请用微信扫码: " << pick(*qr, "qrcode_img_content") << "\n";
|
||||
std::cout << "(二维码内容即上面的 liteapp URL,请用二维码库编码展示;勿把 qrcode 字段当二维码)\n";
|
||||
|
||||
std::string verifyCode;
|
||||
for (int i = 0; i < 480; i++) { // 最多等 8 分钟
|
||||
auto s = http("GET", "/ilink/bot/get_qrcode_status?qrcode=" + qrcode, "", 15);
|
||||
if (s && pick(*s, "status") == "confirmed") { // wait → scaned → confirmed
|
||||
g_token = pick(*s, "bot_token");
|
||||
save_state();
|
||||
std::cout << "登录成功\n";
|
||||
return;
|
||||
std::string path = "/ilink/bot/get_qrcode_status?qrcode=" + qrcode;
|
||||
if (!verifyCode.empty()) path += "&verify_code=" + verifyCode;
|
||||
auto s = http(apiBaseUrl, "GET", path, "", 15);
|
||||
if (s) {
|
||||
std::string status = pick(*s, "status");
|
||||
if (status == "confirmed") {
|
||||
g_token = pick(*s, "bot_token");
|
||||
save_state();
|
||||
std::cout << "登录成功\n";
|
||||
return;
|
||||
} else if (status == "scaned") {
|
||||
verifyCode.clear();
|
||||
std::cout << "已扫码,请在手机上确认...(此后绝不换码,避免砍掉确认会话)\n";
|
||||
} else if (status == "scaned_but_redirect") {
|
||||
verifyCode.clear();
|
||||
std::string host = pick(*s, "redirect_host");
|
||||
if (!host.empty()) {
|
||||
apiBaseUrl = "https://" + host;
|
||||
std::cout << "IDC 重定向,切换轮询至 " << apiBaseUrl << "\n";
|
||||
}
|
||||
} else if (status == "need_verifycode") {
|
||||
std::cout << "请在手机微信上输入显示的数字后回车: " << std::flush;
|
||||
std::getline(std::cin, verifyCode);
|
||||
continue;
|
||||
} else if (status == "verify_code_blocked") {
|
||||
verifyCode.clear();
|
||||
std::cout << "配对码多次输入错误,重新获取二维码\n";
|
||||
qr = fetch_qrcode();
|
||||
if (!qr) { std::cerr << "获取二维码失败\n"; exit(1); }
|
||||
qrcode = pick(*qr, "qrcode");
|
||||
std::cout << "新二维码: " << pick(*qr, "qrcode_img_content") << "\n";
|
||||
continue;
|
||||
} else if (status == "binded_redirect") {
|
||||
std::cout << "该 bot 已绑定过本实例,视为连接成功\n";
|
||||
return;
|
||||
} else if (status == "expired") {
|
||||
// 码过期(服务端作废)→ 换新码;仅在 expired 时换,scaned 后绝不换
|
||||
verifyCode.clear();
|
||||
std::cout << "二维码已过期,重新获取\n";
|
||||
qr = fetch_qrcode();
|
||||
if (!qr) { std::cerr << "获取二维码失败\n"; exit(1); }
|
||||
qrcode = pick(*qr, "qrcode");
|
||||
std::cout << "新二维码: " << pick(*qr, "qrcode_img_content") << "\n";
|
||||
continue;
|
||||
} else {
|
||||
std::cerr << "未知状态: " << status << "\n";
|
||||
}
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
}
|
||||
@@ -147,7 +214,7 @@ static void send_text(const std::string &to, const std::string &text, const std:
|
||||
"\"message_type\":2,\"message_state\":2,\"context_token\":\"" + esc(token) + "\","
|
||||
"\"item_list\":[{\"type\":1,\"text_item\":{\"text\":\"" +
|
||||
esc(text.substr(i, 1500)) + "\"}}]}}";
|
||||
http("POST", "/ilink/bot/sendmessage", body, 15);
|
||||
http(BASE_URL, "POST", "/ilink/bot/sendmessage", body, 15);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +222,7 @@ static void send_text(const std::string &to, const std::string &text, const std:
|
||||
[[noreturn]] static void poll_loop() {
|
||||
int failures = 0;
|
||||
for (;;) {
|
||||
auto resp = http("POST", "/ilink/bot/getupdates",
|
||||
auto resp = http(BASE_URL, "POST", "/ilink/bot/getupdates",
|
||||
"{\"get_updates_buf\":\"" + esc(g_cursor) + "\","
|
||||
"\"base_info\":{\"channel_version\":\"1.0.2\"}}", 45);
|
||||
if (!resp) { // 超时/网络错误
|
||||
|
||||
Reference in new issue
Block a user