Files
weixin_ClawBot_Bridge/examples/java/IlinkBridge.java
T

174 lines
7.9 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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();
}
}