docs: weixin ClawBot bridge protocol and examples
This commit is contained in:
commit
4ea3957da9
13 files changed
+2158
No files matched your search
@@ -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();
|
||||
}
|
||||
Reference in new issue
Block a user