Files
weixin_ClawBot_Bridge/examples/cpp/ilink_client.cpp
T

195 lines
7.6 KiB
C++
Raw 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";
// ---------- 状态: 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 表示传输层错误(超时/断网);业务错误看响应内容
static std::optional<std::string> 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();
}