Files

262 lines
11 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ilink_client.cpp — 微信 ClawBot iLink 最小示例(C++17,依赖 libcurl)
// 流程: 扫码登录 → 长轮询收消息 → 回声回复
// 编译: g++ -std=c++17 ilink_client.cpp -o ilink -lcurl
//
// 注意: 为控制依赖,JSON 用最朴素的字符串查找提取,仅适合演示。
// 正式项目请引入 nlohmann/json(单头文件),媒体加解密见 docs/03。
#include <curl/curl.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <optional>
#include <random>
#include <string>
#include <thread>
#include <sys/stat.h>
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;
static void load_state() {
std::ifstream f(STATE_FILE);
std::string line;
while (std::getline(f, line)) {
auto eq = line.find('=');
if (eq == std::string::npos) continue;
auto k = line.substr(0, eq), v = line.substr(eq + 1);
if (k == "bot_token") g_token = v;
if (k == "cursor") g_cursor = v;
}
}
static void save_state() {
std::ofstream f(STATE_FILE, std::ios::trunc);
f << "bot_token=" << g_token << "\ncursor=" << g_cursor << "\n";
f.close();
chmod(STATE_FILE.c_str(), 0600);
}
// ---------- HTTP 封装 ----------
static size_t on_write(char *p, size_t s, size_t n, void *ud) {
auto *out = static_cast<std::string *>(ud);
out->append(p, s * n);
return s * n;
}
static std::string b64(const std::string &in) { // 极简 base64
static const char *T = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string o;
for (size_t i = 0; i < in.size(); i += 3) {
unsigned v = (unsigned char)in[i] << 16 |
(i+1 < in.size() ? (unsigned char)in[i+1] : 0) << 8 |
(i+2 < in.size() ? (unsigned char)in[i+2] : 0);
o += T[v>>18&63]; o += T[v>>12&63];
o += i+1 < in.size() ? T[v>>6&63] : '=';
o += i+2 < in.size() ? T[v&63] : '=';
}
return o;
}
// 返回空 optional 表示传输层错误(超时/断网);业务错误看响应内容
// 注: 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{}()};
struct curl_slist *hdrs = nullptr;
hdrs = curl_slist_append(hdrs, "Content-Type: application/json");
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 + 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);
curl_easy_setopt(c, CURLOPT_WRITEDATA, &resp);
curl_easy_setopt(c, CURLOPT_TIMEOUT, timeout);
if (method == "POST") curl_easy_setopt(c, CURLOPT_POSTFIELDS, body.c_str());
CURLcode rc = curl_easy_perform(c);
curl_slist_free_all(hdrs);
curl_easy_cleanup(c);
if (rc != CURLE_OK) return std::nullopt;
return resp;
}
// ---------- 极简 JSON 字段提取(仅演示!正式项目用 nlohmann/json) ----------
static std::string pick(const std::string &json, const std::string &key) {
auto p = json.find("\"" + key + "\":\"");
if (p == std::string::npos) return "";
p += key.size() + 4;
auto e = json.find('"', p);
return e == std::string::npos ? "" : json.substr(p, e - p);
}
static std::string esc(const std::string &s) { // 最小 JSON 转义
std::string o;
for (char ch : s) {
if (ch == '"' || ch == '\\') { o += '\\'; o += ch; }
else if (ch == '\n') o += "\\n";
else o += ch;
}
return o;
}
// ---------- 接口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() {
// 状态机: 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 分钟
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));
}
std::cerr << "扫码超时\n"; exit(1);
}
// ---------- 你的 AI 后端: 替换成自己项目的调用 ----------
static std::string ask_your_ai(const std::string &peer, const std::string &text) {
return "你说的是:" + text;
}
// ---------- 接口4: POST /ilink/bot/sendmessage ----------
static void send_text(const std::string &to, const std::string &text, const std::string &token) {
for (size_t i = 0; i < std::max(text.size(), size_t(1)); i += 1500) { // 长文本分片
std::string body =
"{\"msg\":{\"to_user_id\":\"" + esc(to) + "\",\"client_id\":\"cpp-" +
std::to_string(std::time(nullptr)) + "-" + std::to_string(i) + "\","
"\"message_type\":2,\"message_state\":2,\"context_token\":\"" + esc(token) + "\","
"\"item_list\":[{\"type\":1,\"text_item\":{\"text\":\"" +
esc(text.substr(i, 1500)) + "\"}}]}}";
http(BASE_URL, "POST", "/ilink/bot/sendmessage", body, 15);
}
}
// ---------- 接口3: POST /ilink/bot/getupdates 长轮询 ----------
[[noreturn]] static void poll_loop() {
int failures = 0;
for (;;) {
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) { // 超时/网络错误
failures++;
int wait = failures <= 3 ? 2 : 30; // 退避: 2s ×3 → 30s
std::cerr << "轮询异常(" << failures << "次)," << wait << "s 后重试\n";
std::this_thread::sleep_for(std::chrono::seconds(wait));
continue;
}
if (resp->find("\"errcode\":-14") != std::string::npos) {
std::cerr << "会话过期(errcode=-14),请删除 " << STATE_FILE << " 重新扫码\n";
exit(2);
}
std::string buf = pick(*resp, "get_updates_buf");
if (!buf.empty()) { // 游标立即落盘
g_cursor = buf;
save_state();
}
// 演示级提取: 仅第一条文本消息(正式项目按 msgs[] 完整解析)
std::string peer = pick(*resp, "from_user_id");
std::string token = pick(*resp, "context_token");
std::string text = pick(*resp, "text");
if (!peer.empty() && !token.empty() && !text.empty() &&
resp->find("\"message_type\":2") == std::string::npos) {
send_text(peer, ask_your_ai(peer, text), token);
}
failures = 0;
}
}
int main() {
curl_global_init(CURL_GLOBAL_DEFAULT);
load_state();
if (g_token.empty()) login();
poll_loop();
}