// ilink_client.cpp — 微信 ClawBot iLink 最小示例(C++17,依赖 libcurl) // 流程: 扫码登录 → 长轮询收消息 → 回声回复 // 编译: g++ -std=c++17 ilink_client.cpp -o ilink -lcurl // // 注意: 为控制依赖,JSON 用最朴素的字符串查找提取,仅适合演示。 // 正式项目请引入 nlohmann/json(单头文件),媒体加解密见 docs/03。 #include #include #include #include #include #include #include #include #include #include #include static const std::string BASE_URL = "https://ilinkai.weixin.qq.com"; static const std::string STATE_FILE = "bridge-state.json"; // ---------- 状态: 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(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 表示传输层错误(超时/断网);业务错误看响应内容 static std::optional http(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()); 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; 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+2: 扫码登录(get_bot_qrcode / get_qrcode_status) ---------- static void login() { auto qr = http("GET", "/ilink/bot/get_bot_qrcode?bot_type=3", "", 15); if (!qr) { std::cerr << "获取二维码失败\n"; exit(1); } std::string qrcode = pick(*qr, "qrcode"); std::cout << "请用微信扫码: " << pick(*qr, "qrcode_img_content") << "\n"; 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::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("POST", "/ilink/bot/sendmessage", body, 15); } } // ---------- 接口3: POST /ilink/bot/getupdates 长轮询 ---------- [[noreturn]] static void poll_loop() { int failures = 0; for (;;) { auto resp = http("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(); }