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

+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();
}
}