docs: weixin ClawBot bridge protocol and examples

This commit is contained in:
AskaEth committed 2026-08-21 18:15:20 +08:00
commit 4ea3957da9
13 files changed
+2158

No files matched your search

+194
View File
@@ -0,0 +1,194 @@
// 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();
}