174 lines
7.9 KiB
Java
174 lines
7.9 KiB
Java
// 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();
|
||
}
|
||
}
|