// 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 pickAll(String json, String key) { var out = new ArrayList(); 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(); } }