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

+31
View File
@@ -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 节
+203
View File
@@ -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;
}
+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();
}
+223
View File
@@ -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()
}
+173
View File
@@ -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();
}
}
+121
View File
@@ -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()