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,31 @@
|
||||
# examples — 多语言 iLink 调用示例
|
||||
|
||||
每个语言一份**单文件、可独立运行**的最小示例,覆盖同一套流程:
|
||||
|
||||
```
|
||||
扫码登录(get_bot_qrcode / get_qrcode_status) → 长轮询(getupdates) → 回声回复(sendmessage)
|
||||
```
|
||||
|
||||
AI 后端接入点统一是 `askYourAI` / `ask_your_ai` / `AskYourAI` 这一个函数,默认是回声(echo),换成你自己的项目调用即可。
|
||||
|
||||
| 语言 | 文件 | 依赖 | 运行 |
|
||||
|---|---|---|---|
|
||||
| Python | [python/ilink_bridge.py](python/ilink_bridge.py) | `requests` | `python3 ilink_bridge.py` |
|
||||
| Go | [go/main.go](go/main.go) | 仅标准库 | `go run main.go` |
|
||||
| Java | [java/IlinkBridge.java](java/IlinkBridge.java) | JDK 11+ | `javac IlinkBridge.java && java IlinkBridge` |
|
||||
| C | [c/ilink_client.c](c/ilink_client.c) | libcurl | `gcc ilink_client.c -o ilink -lcurl && ./ilink` |
|
||||
| C++ | [cpp/ilink_client.cpp](cpp/ilink_client.cpp) | libcurl | `g++ ilink_client.cpp -o ilink -lcurl && ./ilink` |
|
||||
|
||||
## 共同约定
|
||||
|
||||
- 首次运行会打印二维码链接,用微信扫码确认后拿到 `bot_token`,写入本目录的 `bridge-state.json`(权限 600,**不要提交 git**)
|
||||
- 之后重启免扫码,直接进入长轮询
|
||||
- 收到文本消息原样回声;游标 `get_updates_buf` 每轮落盘
|
||||
- `errcode=-14`(会话过期)时退出并提示重新扫码(删掉 state 文件再运行)
|
||||
|
||||
## 有意省略的部分(保持示例简单)
|
||||
|
||||
- **JSON 库**:Go/Python 用标准方案;Java/C/C++ 为了让示例零额外依赖,用了最朴素的字段提取,注释里标了位置——正式项目请换 Jackson / cJSON / nlohmann-json
|
||||
- **媒体消息**(图片/语音/文件的 AES-128-ECB + CDN):见 [../docs/03-messages-and-media.md](../docs/03-messages-and-media.md),字段细节以官方包源码 `src/cdn/` 为准
|
||||
- **sendtyping / getconfig**:非必需,协议见 [../docs/02-ilink-protocol.md](../docs/02-ilink-protocol.md) 第 9 节
|
||||
- 多 item(图文混排)只做了文本拼接,完整规范化模型见 03 文档第 7 节
|
||||
@@ -0,0 +1,203 @@
|
||||
/* ilink_client.c — 微信 ClawBot iLink 最小示例(C,依赖 libcurl)
|
||||
* 流程: 扫码登录 → 长轮询收消息 → 回声回复
|
||||
* 编译: gcc ilink_client.c -o ilink -lcurl
|
||||
*
|
||||
* 注意: 为控制依赖,JSON 用最朴素的字符串查找提取,仅适合演示。
|
||||
* 正式项目请引入 cJSON,媒体加解密见 docs/03(可用 OpenSSL AES-128-ECB)。
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#include <sys/stat.h>
|
||||
#include <curl/curl.h>
|
||||
|
||||
#define BASE_URL "https://ilinkai.weixin.qq.com"
|
||||
#define STATE_FILE "bridge-state.json"
|
||||
|
||||
/* ---------- 响应缓冲 ---------- */
|
||||
struct buf { char *data; size_t len; };
|
||||
|
||||
static size_t on_write(char *ptr, size_t size, size_t nmemb, void *ud) {
|
||||
struct buf *b = ud;
|
||||
size_t n = size * nmemb;
|
||||
b->data = realloc(b->data, b->len + n + 1);
|
||||
memcpy(b->data + b->len, ptr, n);
|
||||
b->len += n;
|
||||
b->data[b->len] = 0;
|
||||
return n;
|
||||
}
|
||||
|
||||
/* ---------- 状态: bot_token / 游标(极简 key=value 行存储) ---------- */
|
||||
static char g_token[512] = "", g_cursor[1024] = "";
|
||||
|
||||
static void load_state(void) {
|
||||
FILE *f = fopen(STATE_FILE, "r");
|
||||
if (!f) return;
|
||||
char line[2048], key[64], val[1900];
|
||||
while (fgets(line, sizeof line, f))
|
||||
if (sscanf(line, "%63[^=]=%1899[^\n]", key, val) == 2) {
|
||||
if (!strcmp(key, "bot_token")) snprintf(g_token, sizeof g_token, "%s", val);
|
||||
if (!strcmp(key, "cursor")) snprintf(g_cursor, sizeof g_cursor, "%s", val);
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
static void save_state(void) {
|
||||
FILE *f = fopen(STATE_FILE, "w");
|
||||
fprintf(f, "bot_token=%s\ncursor=%s\n", g_token, g_cursor);
|
||||
fclose(f);
|
||||
chmod(STATE_FILE, 0600);
|
||||
}
|
||||
|
||||
/* ---------- 通用请求: 鉴权头 + 防重放 X-WECHAT-UIN ---------- */
|
||||
static char *http(const char *method, const char *path, const char *json_body, long timeout) {
|
||||
CURL *c = curl_easy_init();
|
||||
struct buf b = {0};
|
||||
char url[512], uin[32], auth[600];
|
||||
snprintf(url, sizeof url, "%s%s", BASE_URL, path);
|
||||
|
||||
/* X-WECHAT-UIN: base64(十进制字符串(随机uint32)),简化用伪随机 */
|
||||
snprintf(uin, sizeof uin, "%u", (unsigned)(rand() ^ (unsigned)time(NULL)));
|
||||
char uin_b64[64];
|
||||
/* 极简 base64(仅数字串,查表法) */
|
||||
const char *T = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
size_t ul = strlen(uin), o = 0;
|
||||
for (size_t i = 0; i < ul; i += 3) {
|
||||
unsigned v = uin[i] << 16 | (i+1<ul ? uin[i+1] : 0) << 8 | (i+2<ul ? uin[i+2] : 0);
|
||||
uin_b64[o++] = T[v>>18&63]; uin_b64[o++] = T[v>>12&63];
|
||||
uin_b64[o++] = i+1<ul ? T[v>>6&63] : '=';
|
||||
uin_b64[o++] = i+2<ul ? T[v&63] : '=';
|
||||
}
|
||||
uin_b64[o] = 0;
|
||||
|
||||
struct curl_slist *hdrs = NULL;
|
||||
hdrs = curl_slist_append(hdrs, "Content-Type: application/json");
|
||||
hdrs = curl_slist_append(hdrs, "AuthorizationType: ilink_bot_token");
|
||||
char uin_hdr[96]; snprintf(uin_hdr, sizeof uin_hdr, "X-WECHAT-UIN: %s", uin_b64);
|
||||
hdrs = curl_slist_append(hdrs, uin_hdr);
|
||||
if (g_token[0]) {
|
||||
snprintf(auth, sizeof auth, "Authorization: Bearer %s", g_token);
|
||||
hdrs = curl_slist_append(hdrs, auth);
|
||||
}
|
||||
|
||||
curl_easy_setopt(c, CURLOPT_URL, url);
|
||||
curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, on_write);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEDATA, &b);
|
||||
curl_easy_setopt(c, CURLOPT_TIMEOUT, timeout);
|
||||
if (!strcmp(method, "POST")) curl_easy_setopt(c, CURLOPT_POSTFIELDS, json_body);
|
||||
|
||||
CURLcode rc = curl_easy_perform(c);
|
||||
curl_slist_free_all(hdrs);
|
||||
curl_easy_cleanup(c);
|
||||
if (rc != CURLE_OK) { free(b.data); return NULL; } /* 超时/网络错误返回 NULL */
|
||||
return b.data ? b.data : strdup("");
|
||||
}
|
||||
|
||||
/* ---------- 极简 JSON 字段提取(仅演示!正式项目用 cJSON) ---------- */
|
||||
static int pick(const char *json, const char *key, char *out, size_t outsz) {
|
||||
char pat[128]; snprintf(pat, sizeof pat, "\"%s\":\"", key);
|
||||
const char *p = strstr(json, pat);
|
||||
if (!p) return 0;
|
||||
p += strlen(pat);
|
||||
const char *e = strchr(p, '"');
|
||||
if (!e || (size_t)(e - p) >= outsz) return 0;
|
||||
memcpy(out, p, e - p); out[e - p] = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---------- 接口1+2: 扫码登录(get_bot_qrcode / get_qrcode_status) ---------- */
|
||||
static void login(void) {
|
||||
char *qr = http("GET", "/ilink/bot/get_bot_qrcode?bot_type=3", NULL, 15);
|
||||
char qrcode[256], qrurl[512];
|
||||
if (!qr || !pick(qr, "qrcode", qrcode, sizeof qrcode)) { fprintf(stderr, "获取二维码失败\n"); exit(1); }
|
||||
pick(qr, "qrcode_img_content", qrurl, sizeof qrurl);
|
||||
printf("请用微信扫码: %s\n", qrurl);
|
||||
free(qr);
|
||||
|
||||
char path[320], status[32], token[512];
|
||||
snprintf(path, sizeof path, "/ilink/bot/get_qrcode_status?qrcode=%s", qrcode);
|
||||
for (int i = 0; i < 480; i++) { /* 最多等 8 分钟 */
|
||||
char *s = http("GET", path, NULL, 15);
|
||||
if (s && pick(s, "status", status, sizeof status) && !strcmp(status, "confirmed")) {
|
||||
pick(s, "bot_token", token, sizeof token);
|
||||
snprintf(g_token, sizeof g_token, "%s", token);
|
||||
save_state();
|
||||
printf("登录成功\n");
|
||||
free(s);
|
||||
return;
|
||||
}
|
||||
free(s);
|
||||
sleep(1);
|
||||
}
|
||||
fprintf(stderr, "扫码超时\n"); exit(1);
|
||||
}
|
||||
|
||||
/* ---------- 你的 AI 后端: 替换成自己项目的调用 ---------- */
|
||||
static void ask_your_ai(const char *peer, const char *text, char *out, size_t n) {
|
||||
snprintf(out, n, "你说的是:%s", text);
|
||||
}
|
||||
|
||||
/* ---------- 接口4: POST /ilink/bot/sendmessage ---------- */
|
||||
static void send_text(const char *to, const char *text, const char *ctx_token) {
|
||||
/* 演示不处理 JSON 转义与分片,正式项目务必转义 \ " 换行 并按需分片 */
|
||||
char body[4096];
|
||||
snprintf(body, sizeof body,
|
||||
"{\"msg\":{\"to_user_id\":\"%s\",\"client_id\":\"c-%ld\","
|
||||
"\"message_type\":2,\"message_state\":2,\"context_token\":\"%s\","
|
||||
"\"item_list\":[{\"type\":1,\"text_item\":{\"text\":\"%s\"}}]}}",
|
||||
to, (long)time(NULL), ctx_token, text);
|
||||
free(http("POST", "/ilink/bot/sendmessage", body, 15));
|
||||
}
|
||||
|
||||
/* ---------- 接口3: POST /ilink/bot/getupdates 长轮询 ---------- */
|
||||
static void poll_loop(void) {
|
||||
int failures = 0;
|
||||
char body[1200];
|
||||
for (;;) {
|
||||
snprintf(body, sizeof body,
|
||||
"{\"get_updates_buf\":\"%s\",\"base_info\":{\"channel_version\":\"1.0.2\"}}",
|
||||
g_cursor);
|
||||
char *resp = http("POST", "/ilink/bot/getupdates", body, 45);
|
||||
if (!resp) { /* 超时/网络错误 */
|
||||
failures++;
|
||||
int wait = failures <= 3 ? 2 : 30;
|
||||
fprintf(stderr, "轮询异常(%d次),%ds 后重试\n", failures, wait);
|
||||
sleep(wait);
|
||||
continue;
|
||||
}
|
||||
if (strstr(resp, "\"errcode\":-14")) {
|
||||
fprintf(stderr, "会话过期(errcode=-14),请删除 %s 重新扫码\n", STATE_FILE);
|
||||
free(resp); exit(2);
|
||||
}
|
||||
char buf[1024];
|
||||
if (pick(resp, "get_updates_buf", buf, sizeof buf) && buf[0]) {
|
||||
snprintf(g_cursor, sizeof g_cursor, "%s", buf);
|
||||
save_state(); /* 游标立即落盘 */
|
||||
}
|
||||
/* 演示级提取: 仅取第一条消息的文本/context_token/from_user_id */
|
||||
char peer[128], token[512], text[1500];
|
||||
if (pick(resp, "from_user_id", peer, sizeof peer) &&
|
||||
pick(resp, "context_token", token, sizeof token) &&
|
||||
pick(resp, "text", text, sizeof text) &&
|
||||
!strstr(resp, "\"message_type\":2")) {
|
||||
char reply[1700];
|
||||
ask_your_ai(peer, text, reply, sizeof reply);
|
||||
send_text(peer, reply, token);
|
||||
}
|
||||
free(resp);
|
||||
failures = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
srand((unsigned)time(NULL));
|
||||
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||
load_state();
|
||||
if (!g_token[0]) login();
|
||||
poll_loop();
|
||||
curl_global_cleanup();
|
||||
return 0;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// main.go — 微信 ClawBot iLink 最小示例(Go,仅标准库)
|
||||
// 流程: 扫码登录 → 长轮询收消息 → 回声回复
|
||||
// 运行: go run main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
const baseURL = "https://ilinkai.weixin.qq.com"
|
||||
|
||||
var stateFile = filepath.Join(func() string { d, _ := os.Executable(); return filepath.Dir(d) }(), "bridge-state.json")
|
||||
|
||||
// ---------- 状态持久化: bot_token / 游标 / context_token ----------
|
||||
type State struct {
|
||||
BotToken string `json:"bot_token"`
|
||||
BotID string `json:"bot_id,omitempty"`
|
||||
Cursor string `json:"cursor"`
|
||||
Contexts map[string]string `json:"contexts"`
|
||||
}
|
||||
|
||||
var state = State{Contexts: map[string]string{}}
|
||||
|
||||
func loadState() {
|
||||
if b, err := os.ReadFile(stateFile); err == nil {
|
||||
json.Unmarshal(b, &state)
|
||||
}
|
||||
if state.Contexts == nil {
|
||||
state.Contexts = map[string]string{}
|
||||
}
|
||||
}
|
||||
|
||||
func saveState() {
|
||||
b, _ := json.MarshalIndent(state, "", " ")
|
||||
os.WriteFile(stateFile, b, 0o600)
|
||||
}
|
||||
|
||||
// ---------- 通用请求头: 鉴权 + 防重放 X-WECHAT-UIN ----------
|
||||
func headers(req *http.Request) {
|
||||
var u [4]byte
|
||||
rand.Read(u[:])
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("AuthorizationType", "ilink_bot_token")
|
||||
req.Header.Set("X-WECHAT-UIN", base64.StdEncoding.EncodeToString(
|
||||
[]byte(fmt.Sprint(binary.BigEndian.Uint32(u[:])))))
|
||||
if state.BotToken != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+state.BotToken)
|
||||
}
|
||||
}
|
||||
|
||||
func apiPost(path string, body any, timeout time.Duration) (map[string]any, error) {
|
||||
buf, _ := json.Marshal(body)
|
||||
req, err := http.NewRequest("POST", baseURL+path, bytes.NewReader(buf))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
headers(req)
|
||||
client := &http.Client{Timeout: timeout}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var out map[string]any
|
||||
return out, json.NewDecoder(resp.Body).Decode(&out)
|
||||
}
|
||||
|
||||
func apiGet(path string, extraHeaders map[string]string) (map[string]any, error) {
|
||||
req, err := http.NewRequest("GET", baseURL+path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range extraHeaders {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
resp, err := (&http.Client{Timeout: 15 * time.Second}).Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var out map[string]any
|
||||
return out, json.NewDecoder(resp.Body).Decode(&out)
|
||||
}
|
||||
|
||||
// ---------- 接口1+2: 扫码登录(get_bot_qrcode / get_qrcode_status) ----------
|
||||
func login() error {
|
||||
qr, err := apiGet("/ilink/bot/get_bot_qrcode?bot_type=3", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("请用微信扫码:", qr["qrcode_img_content"])
|
||||
deadline := time.Now().Add(8 * time.Minute)
|
||||
for time.Now().Before(deadline) {
|
||||
s, err := apiGet("/ilink/bot/get_qrcode_status?qrcode="+url(qr["qrcode"]),
|
||||
map[string]string{"iLink-App-ClientVersion": "1"})
|
||||
if err == nil && s["status"] == "confirmed" { // wait → scaned → confirmed
|
||||
state.BotToken, _ = s["bot_token"].(string)
|
||||
state.BotID, _ = s["ilink_bot_id"].(string)
|
||||
saveState()
|
||||
fmt.Println("登录成功, bot:", state.BotID)
|
||||
return nil
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
return fmt.Errorf("扫码超时")
|
||||
}
|
||||
|
||||
func url(v any) string { // 极简 query 转义(qrcode 内容一般安全,正式代码用 url.QueryEscape)
|
||||
s, _ := v.(string)
|
||||
return s
|
||||
}
|
||||
|
||||
// ---------- 你的 AI 后端: 替换成自己项目的调用 ----------
|
||||
func AskYourAI(peerID, text string) string {
|
||||
return "你说的是:" + text
|
||||
}
|
||||
|
||||
// ---------- 接口4: POST /ilink/bot/sendmessage ----------
|
||||
func sendText(toUserID, text, contextToken string) {
|
||||
if text == "" {
|
||||
text = " "
|
||||
}
|
||||
for i := 0; i < len(text); i += 1500 { // 长文本分片(按字节切仅适合演示,正式按 rune 切)
|
||||
end := i + 1500
|
||||
if end > len(text) {
|
||||
end = len(text)
|
||||
}
|
||||
apiPost("/ilink/bot/sendmessage", map[string]any{
|
||||
"msg": map[string]any{
|
||||
"to_user_id": toUserID,
|
||||
"client_id": fmt.Sprintf("%d-%d", time.Now().UnixNano(), i),
|
||||
"message_type": 2, // bot 消息
|
||||
"message_state": 2, // FINISH
|
||||
"context_token": contextToken,
|
||||
"item_list": []any{map[string]any{
|
||||
"type": 1, "text_item": map[string]string{"text": text[i:end]},
|
||||
}},
|
||||
},
|
||||
}, 15*time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 接口3: POST /ilink/bot/getupdates 长轮询 ----------
|
||||
func pollLoop() {
|
||||
failures := 0
|
||||
for {
|
||||
resp, err := apiPost("/ilink/bot/getupdates", map[string]any{
|
||||
"get_updates_buf": state.Cursor,
|
||||
"base_info": map[string]string{"channel_version": "1.0.2"},
|
||||
}, 45*time.Second)
|
||||
|
||||
if err != nil {
|
||||
if os.IsTimeout(err) { // 长轮询正常超时
|
||||
continue
|
||||
}
|
||||
failures++
|
||||
wait := 2 * time.Second
|
||||
if failures > 3 {
|
||||
wait = 30 * time.Second // 退避: 2s ×3 → 30s
|
||||
}
|
||||
fmt.Printf("轮询异常(%d次),%v 后重试: %v\n", failures, wait, err)
|
||||
time.Sleep(wait)
|
||||
continue
|
||||
}
|
||||
|
||||
if code, _ := resp["errcode"].(float64); code == -14 { // 会话过期
|
||||
fmt.Println("会话过期(errcode=-14),请删除 bridge-state.json 重新扫码")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if buf, ok := resp["get_updates_buf"].(string); ok && buf != "" {
|
||||
state.Cursor = buf // 游标立即落盘
|
||||
saveState()
|
||||
}
|
||||
|
||||
msgs, _ := resp["msgs"].([]any)
|
||||
for _, m := range msgs {
|
||||
msg, _ := m.(map[string]any)
|
||||
if t, _ := msg["message_type"].(float64); t != 1 { // 只处理用户消息
|
||||
continue
|
||||
}
|
||||
text := ""
|
||||
for _, it := range msg["item_list"].([]any) {
|
||||
item, _ := it.(map[string]any)
|
||||
if t, _ := item["type"].(float64); t == 1 {
|
||||
if ti, ok := item["text_item"].(map[string]any); ok {
|
||||
s, _ := ti["text"].(string)
|
||||
text += s + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
peer, _ := msg["from_user_id"].(string)
|
||||
token, _ := msg["context_token"].(string)
|
||||
state.Contexts[peer] = token
|
||||
saveState()
|
||||
sendText(peer, AskYourAI(peer, text), token)
|
||||
}
|
||||
failures = 0
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
loadState()
|
||||
if state.BotToken == "" {
|
||||
if err := login(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
pollLoop()
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// IlinkBridge.java — 微信 ClawBot iLink 最小示例(Java 11+,零第三方依赖)
|
||||
// 流程: 扫码登录 → 长轮询收消息 → 回声回复
|
||||
// 运行: javac IlinkBridge.java && java IlinkBridge
|
||||
//
|
||||
// 注意: 为保持零依赖,JSON 解析用最朴素的正则提取,仅适合演示。
|
||||
// 正式项目请换 Jackson / Gson,并按 docs/04 的模块划分组织代码。
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.regex.*;
|
||||
|
||||
public class IlinkBridge {
|
||||
static final String BASE_URL = "https://ilinkai.weixin.qq.com";
|
||||
static final Path STATE_FILE = Path.of("bridge-state.json");
|
||||
|
||||
// ---------- 状态: bot_token / 游标 / context_token(极简 key=value 存储) ----------
|
||||
static Properties state = new Properties();
|
||||
|
||||
static void loadState() throws Exception {
|
||||
if (Files.exists(STATE_FILE))
|
||||
try (var in = Files.newInputStream(STATE_FILE)) { state.load(in); }
|
||||
}
|
||||
|
||||
static void saveState() throws Exception {
|
||||
try (var out = Files.newOutputStream(STATE_FILE)) { state.store(out, "ilink bridge state"); }
|
||||
STATE_FILE.toFile().setReadable(false, false);
|
||||
STATE_FILE.toFile().setReadable(true, true); // 近似 600
|
||||
}
|
||||
|
||||
// ---------- 通用请求头: 鉴权 + 防重放 X-WECHAT-UIN ----------
|
||||
static HttpRequest.Builder req(String url) {
|
||||
var b = HttpRequest.newBuilder(URI.create(url))
|
||||
.timeout(Duration.ofSeconds(50))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("AuthorizationType", "ilink_bot_token")
|
||||
.header("X-WECHAT-UIN", Base64.getEncoder().encodeToString(
|
||||
Integer.toString(new SecureRandom().nextInt() >>> 1).getBytes()));
|
||||
String token = state.getProperty("bot_token");
|
||||
if (token != null) b.header("Authorization", "Bearer " + token);
|
||||
return b;
|
||||
}
|
||||
|
||||
static final HttpClient client = HttpClient.newHttpClient();
|
||||
|
||||
static String post(String path, String json) throws Exception {
|
||||
var r = client.send(req(BASE_URL + path)
|
||||
.POST(HttpRequest.BodyPublishers.ofString(json)).build(),
|
||||
HttpResponse.BodyHandlers.ofString());
|
||||
return r.body();
|
||||
}
|
||||
|
||||
// ---------- 极简 JSON 字段提取(仅演示!正式项目用 Jackson/Gson) ----------
|
||||
static String pick(String json, String key) {
|
||||
var m = Pattern.compile("\"" + key + "\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"").matcher(json);
|
||||
return m.find() ? m.group(1).replace("\\\"", "\"") : null;
|
||||
}
|
||||
|
||||
static List<String> pickAll(String json, String key) {
|
||||
var out = new ArrayList<String>();
|
||||
var m = Pattern.compile("\"" + key + "\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"").matcher(json);
|
||||
while (m.find()) out.add(m.group(1));
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------- 接口1+2: 扫码登录(get_bot_qrcode / get_qrcode_status) ----------
|
||||
static void login() throws Exception {
|
||||
var qr = client.send(req(BASE_URL + "/ilink/bot/get_bot_qrcode?bot_type=3")
|
||||
.GET().build(), HttpResponse.BodyHandlers.ofString()).body();
|
||||
String qrcode = pick(qr, "qrcode");
|
||||
System.out.println("请用微信扫码: " + pick(qr, "qrcode_img_content"));
|
||||
long deadline = System.currentTimeMillis() + 8 * 60_000;
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
var s = client.send(req(BASE_URL + "/ilink/bot/get_qrcode_status?qrcode=" + qrcode)
|
||||
.header("iLink-App-ClientVersion", "1").GET().build(),
|
||||
HttpResponse.BodyHandlers.ofString()).body();
|
||||
if ("confirmed".equals(pick(s, "status"))) { // wait → scaned → confirmed
|
||||
state.setProperty("bot_token", pick(s, "bot_token"));
|
||||
state.setProperty("bot_id", pick(s, "ilink_bot_id"));
|
||||
saveState();
|
||||
System.out.println("登录成功, bot: " + state.getProperty("bot_id"));
|
||||
return;
|
||||
}
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
throw new RuntimeException("扫码超时");
|
||||
}
|
||||
|
||||
// ---------- 你的 AI 后端: 替换成自己项目的调用 ----------
|
||||
static String askYourAI(String peerId, String text) {
|
||||
// 例: 用 client 调本地 HTTP 后端 POST http://127.0.0.1:8000/chat
|
||||
return "你说的是:" + text;
|
||||
}
|
||||
|
||||
static String esc(String s) { // 最小 JSON 转义
|
||||
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n");
|
||||
}
|
||||
|
||||
// ---------- 接口4: POST /ilink/bot/sendmessage ----------
|
||||
static void sendText(String toUserId, String text, String contextToken) throws Exception {
|
||||
for (int i = 0; i < Math.max(text.length(), 1); i += 1500) { // 长文本分片
|
||||
post("/ilink/bot/sendmessage", """
|
||||
{"msg":{
|
||||
"to_user_id":"%s",
|
||||
"client_id":"%s",
|
||||
"message_type":2,
|
||||
"message_state":2,
|
||||
"context_token":"%s",
|
||||
"item_list":[{"type":1,"text_item":{"text":"%s"}}]
|
||||
}}""".formatted(esc(toUserId), UUID.randomUUID(),
|
||||
esc(contextToken), esc(text.substring(i, Math.min(i + 1500, text.length())))));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 接口3: POST /ilink/bot/getupdates 长轮询 ----------
|
||||
static void pollLoop() throws Exception {
|
||||
int failures = 0;
|
||||
while (true) {
|
||||
try {
|
||||
String resp = post("/ilink/bot/getupdates",
|
||||
"{\"get_updates_buf\":\"" + esc(state.getProperty("cursor", "")) + "\"," +
|
||||
"\"base_info\":{\"channel_version\":\"1.0.2\"}}");
|
||||
|
||||
if ("-14".equals(pick(resp, "errcode"))) { // 会话过期
|
||||
System.err.println("会话过期(errcode=-14),请删除 bridge-state.json 重新扫码");
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
String buf = pick(resp, "get_updates_buf");
|
||||
if (buf != null && !buf.isEmpty()) { // 游标立即落盘
|
||||
state.setProperty("cursor", buf);
|
||||
saveState();
|
||||
}
|
||||
|
||||
// 演示级提取: 逐条消息的 from_user_id / context_token / text
|
||||
// (正则无法严谨配对嵌套结构,正式项目务必用 JSON 库按 msgs[] 解析)
|
||||
var peers = pickAll(resp, "from_user_id");
|
||||
var tokens = pickAll(resp, "context_token");
|
||||
var texts = pickAll(resp, "text");
|
||||
for (int i = 0; i < peers.size() && i < tokens.size(); i++) {
|
||||
String text = i < texts.size() ? texts.get(i) : "";
|
||||
if (text.isBlank()) continue;
|
||||
state.setProperty("ctx." + peers.get(i), tokens.get(i));
|
||||
saveState();
|
||||
try {
|
||||
sendText(peers.get(i), askYourAI(peers.get(i), text), tokens.get(i));
|
||||
} catch (Exception e) {
|
||||
System.err.println("处理消息失败: " + e);
|
||||
}
|
||||
}
|
||||
failures = 0;
|
||||
} catch (java.net.http.HttpTimeoutException e) {
|
||||
continue; // 长轮询正常超时
|
||||
} catch (Exception e) {
|
||||
failures++;
|
||||
long wait = failures <= 3 ? 2000 : 30_000; // 退避: 2s ×3 → 30s
|
||||
System.err.println("轮询异常(" + failures + "次)," + wait / 1000 + "s 后重试: " + e);
|
||||
Thread.sleep(wait);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
loadState();
|
||||
if (state.getProperty("bot_token") == null) login();
|
||||
pollLoop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
# ilink_bridge.py — 微信 ClawBot iLink 最小示例(Python)
|
||||
# 流程: 扫码登录 → 长轮询收消息 → 回声回复
|
||||
# 依赖: pip install requests
|
||||
import json, os, time, uuid, base64, random, requests
|
||||
|
||||
BASE_URL = "https://ilinkai.weixin.qq.com"
|
||||
STATE_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bridge-state.json")
|
||||
|
||||
# ---------- 状态持久化: bot_token / 游标 / context_token ----------
|
||||
state = {"bot_token": None, "cursor": "", "contexts": {}}
|
||||
if os.path.exists(STATE_FILE):
|
||||
state.update(json.load(open(STATE_FILE)))
|
||||
|
||||
def save_state():
|
||||
with open(STATE_FILE, "w") as f:
|
||||
json.dump(state, f, indent=2)
|
||||
os.chmod(STATE_FILE, 0o600)
|
||||
|
||||
# ---------- 通用请求头: 鉴权 + 防重放 X-WECHAT-UIN ----------
|
||||
def headers():
|
||||
h = {
|
||||
"Content-Type": "application/json",
|
||||
"AuthorizationType": "ilink_bot_token",
|
||||
"X-WECHAT-UIN": base64.b64encode(str(random.getrandbits(32)).encode()).decode(),
|
||||
}
|
||||
if state["bot_token"]:
|
||||
h["Authorization"] = f"Bearer {state['bot_token']}"
|
||||
return h
|
||||
|
||||
def api_post(path, body, timeout=45):
|
||||
return requests.post(BASE_URL + path, headers=headers(), json=body, timeout=timeout).json()
|
||||
|
||||
# ---------- 接口1: GET /ilink/bot/get_bot_qrcode 获取登录二维码 ----------
|
||||
# ---------- 接口2: GET /ilink/bot/get_qrcode_status 轮询扫码状态 ----------
|
||||
def login():
|
||||
qr = requests.get(f"{BASE_URL}/ilink/bot/get_bot_qrcode?bot_type=3").json()
|
||||
print("请用微信扫码:", qr["qrcode_img_content"])
|
||||
deadline = time.time() + 8 * 60
|
||||
while time.time() < deadline:
|
||||
s = requests.get(f"{BASE_URL}/ilink/bot/get_qrcode_status",
|
||||
params={"qrcode": qr["qrcode"]},
|
||||
headers={"iLink-App-ClientVersion": "1"}).json()
|
||||
if s.get("status") == "confirmed": # 状态流转: wait → scaned → confirmed
|
||||
state.update(bot_token=s["bot_token"], bot_id=s["ilink_bot_id"],
|
||||
owner_id=s["ilink_user_id"])
|
||||
save_state()
|
||||
print("登录成功, bot:", s["ilink_bot_id"])
|
||||
return
|
||||
time.sleep(1)
|
||||
raise RuntimeError("扫码超时")
|
||||
|
||||
# ---------- 你的 AI 后端: 替换成自己项目的调用 ----------
|
||||
def ask_your_ai(peer_id: str, text: str) -> str:
|
||||
# 例: requests.post("http://127.0.0.1:8000/chat",
|
||||
# json={"session_id": peer_id, "text": text}).json()["reply"]
|
||||
return f"你说的是:{text}"
|
||||
|
||||
# ---------- 接口4: POST /ilink/bot/sendmessage 发送消息 ----------
|
||||
def send_text(to_user_id, text, context_token):
|
||||
for i in range(0, max(len(text), 1), 1500): # 长文本分片
|
||||
api_post("/ilink/bot/sendmessage", {
|
||||
"msg": {
|
||||
"to_user_id": to_user_id,
|
||||
"client_id": str(uuid.uuid4()), # 去重 ID
|
||||
"message_type": 2, # 2 = bot 消息
|
||||
"message_state": 2, # 2 = FINISH
|
||||
"context_token": context_token, # 必填! 来自入站消息
|
||||
"item_list": [{"type": 1, "text_item": {"text": text[i:i+1500]}}],
|
||||
}
|
||||
})
|
||||
|
||||
# ---------- 接口3: POST /ilink/bot/getupdates 长轮询收消息 ----------
|
||||
def poll_loop():
|
||||
failures = 0
|
||||
while True:
|
||||
try:
|
||||
resp = api_post("/ilink/bot/getupdates", {
|
||||
"get_updates_buf": state["cursor"], # 同步游标
|
||||
"base_info": {"channel_version": "1.0.2"},
|
||||
})
|
||||
if resp.get("errcode") == -14: # 会话过期
|
||||
print("会话过期(errcode=-14),请删除 bridge-state.json 重新扫码")
|
||||
raise SystemExit(2)
|
||||
if resp.get("ret", 0) != 0:
|
||||
raise RuntimeError(f"getupdates 错误: {resp}")
|
||||
|
||||
if resp.get("get_updates_buf"): # 游标立即落盘
|
||||
state["cursor"] = resp["get_updates_buf"]
|
||||
save_state()
|
||||
|
||||
for msg in resp.get("msgs") or []:
|
||||
if msg.get("message_type") != 1: # 只处理用户消息
|
||||
continue
|
||||
text = "\n".join(
|
||||
it.get("text_item", {}).get("text", "")
|
||||
for it in msg.get("item_list", []) if it.get("type") == 1
|
||||
).strip()
|
||||
if not text:
|
||||
continue
|
||||
state["contexts"][msg["from_user_id"]] = msg["context_token"]
|
||||
save_state()
|
||||
try:
|
||||
send_text(msg["from_user_id"],
|
||||
ask_your_ai(msg["from_user_id"], text),
|
||||
msg["context_token"])
|
||||
except Exception as e:
|
||||
print("处理消息失败:", e)
|
||||
failures = 0
|
||||
except requests.exceptions.Timeout:
|
||||
continue # 长轮询正常超时
|
||||
except Exception as e:
|
||||
failures += 1
|
||||
wait = 2 if failures <= 3 else 30 # 退避: 2s ×3 → 30s
|
||||
print(f"轮询异常({failures}次),{wait}s 后重试:", e)
|
||||
time.sleep(wait)
|
||||
|
||||
if __name__ == "__main__":
|
||||
if not state["bot_token"]:
|
||||
login()
|
||||
poll_loop()
|
||||
Reference in new issue
Block a user