企业化设计的微信 iLink Bot Java SDK,支持二维码登录、消息收发、输入态管理、图片/文件/语音/视频发送、媒体下载与会话上下文管理。
wechat-ilink-sdk 是一个面向 Java 开发者的微信 iLink Bot SDK,目标是为业务系统提供一套清晰、稳定、易集成的客户端能力。
当前版本已经覆盖 iLink Bot 的核心主链路:
- 🔐 二维码登录与登录状态轮询
- 📨 长轮询获取消息
- 💬 基于最新
contextToken的文本发送 - 🖼️ 图片、文件、语音、视频发送
- ⌨️ 输入状态开始 / 停止
- ⏳ 带输入态发送文本
- 📥 媒体消息下载与 AES 解密
- 📝 会话上下文缓存与清理
- 🏗️ Builder 模式创建客户端
- ⚙️ 配置化、状态管理、心跳探测、监听器机制
SDK 采用 Builder 模式创建客户端,并内置统一配置、状态管理、异常体系、线程池与资源释放机制,适合在后端服务、机器人程序和集成型项目中使用。
<dependency>
<groupId>io.github.lith0924</groupId>
<artifactId>wechat-ilink-sdk</artifactId>
<version>2.3.3</version>
</dependency>implementation 'io.github.lith0924:wechat-ilink-sdk:2.3.3'- 🔐 二维码登录与异步登录结果获取
- 📊 登录状态与连接状态管理
- 🏗️ Builder 模式创建客户端
- 👂 登录、消息、心跳监听器
- ❤️ 自动心跳健康探测
- 💬 基于最新
contextToken的消息发送模型 - 📝 文本消息发送
- 🖼️ 图片消息发送
- 📄 文件消息发送
- 🎤 语音消息发送
- 📹 视频消息发送
- ⌨️ 输入状态开始 / 停止控制
- ⏳ 带输入态发送文本
- 📥 媒体下载与 AES 解密
- 📝 会话上下文清理
- ⚙️ 可配置超时、重试、退避与线程池参数
- 🔄
AutoCloseable生命周期支持
| 组件 | 说明 |
|---|---|
ILinkClient |
SDK 主入口,负责登录、消息、输入态、媒体与生命周期管理 |
ILinkClientBuilder |
客户端构建器,用于配置监听器和自定义参数 |
| 组件 | 说明 |
|---|---|
LoginService |
处理二维码获取与登录状态轮询 |
UpdateService |
处理 getUpdates 长轮询与消息获取 |
MessageService |
处理文本、图片、文件、语音、视频消息发送 |
TypingService |
处理输入状态控制 |
MediaService |
处理媒体上传、下载与 AES 解密 |
| 组件 | 说明 |
|---|---|
LoginStatus |
登录流程状态 |
ConnectionStatus |
客户端连接状态 |
LoginContext |
登录成功后的凭证与基础信息 |
ConversationContext |
单用户会话上下文,缓存最新 contextToken 与 typingTicket |
| 组件 | 说明 |
|---|---|
ILinkConfig |
客户端配置对象 |
ExecutorManager |
线程池管理 |
RetryPolicy |
重试策略 |
HeartbeatService |
心跳 / 健康探测 |
BusinessApiClient |
业务接口调用封装 |
HttpClientFacade |
HTTP 请求封装 |
ListenerRegistry |
监听器注册中心 |
ILinkClient client = ILinkClient.builder().build();ILinkClient client = ILinkClient.builder()
.onLogin(new OnLoginListener() {
@Override
public void onLoginSuccess(LoginContext context) {
System.out.println("登录成功,botId = " + context.getBotId());
}
@Override
public void onLoginFailure(Throwable throwable) {
System.err.println("登录失败: " + throwable.getMessage());
}
})
.onMessage(new OnMessageListener() {
@Override
public void onMessages(List<WeixinMessage> messages) {
for (WeixinMessage msg : messages) {
System.out.println("收到消息,fromUserId = " + msg.getFrom_user_id());
}
}
})
.build();String qrCodeContent = client.executeLogin();
System.out.println("请将以下内容渲染为二维码后扫码登录:");
System.out.println(qrCodeContent);
LoginContext context = client.getLoginFuture().get();
System.out.println("登录成功,botId = " + context.getBotId());List<WeixinMessage> messages = client.getUpdates();
for (WeixinMessage msg : messages) {
System.out.println("fromUserId = " + msg.getFrom_user_id());
System.out.println("contextToken = " + msg.getContext_token());
if (msg.getItem_list() != null) {
for (MessageItem item : msg.getItem_list()) {
if (item.getText_item() != null) {
System.out.println("text = " + item.getText_item().getText());
}
}
}
}client.sendText("user@im.wechat", "Hello, iLink!");client.sendTextWithTyping("user@im.wechat", "Hello with typing", 1500L);byte[] imageBytes = Files.readAllBytes(Paths.get("demo.png"));
client.sendImage("user@im.wechat", imageBytes, "demo.png", "这是一张测试图片");byte[] fileBytes = Files.readAllBytes(Paths.get("demo.pdf"));
client.sendFile("user@im.wechat", fileBytes, "demo.pdf", "这是一个测试文件");byte[] voiceBytes = Files.readAllBytes(Paths.get("demo.silk"));
client.sendVoice("user@im.wechat", voiceBytes, "demo.silk", 3000, 16000);byte[] videoBytes = Files.readAllBytes(Paths.get("demo.mp4"));
client.sendVideo("user@im.wechat", videoBytes, "demo.mp4", 5000, "这是一个测试视频");client.startTyping("user@im.wechat");client.stopTyping("user@im.wechat");byte[] bytes = client.downloadMedia(cdnMedia);
Files.write(Paths.get("download.bin"), bytes);List<WeixinMessage> messages = client.getUpdates();
for (WeixinMessage msg : messages) {
if (msg.getItem_list() == null) {
continue;
}
for (MessageItem item : msg.getItem_list()) {
if (item.getImage_item() != null
|| item.getFile_item() != null
|| item.getVoice_item() != null
|| item.getVideo_item() != null) {
byte[] bytes = client.downloadMediaFromMessageItem(item);
Files.write(Paths.get("download.bin"), bytes);
}
}
}byte[] imageBytes = client.downloadImageFromMessageItem(item);
byte[] fileBytes = client.downloadFileFromMessageItem(item);
byte[] voiceBytes = client.downloadVoiceFromMessageItem(item);
byte[] videoBytes = client.downloadVideoFromMessageItem(item);这一部分补充 iLink 协议本身的一些核心概念,便于理解 SDK 的设计方式。
普通用户 ID
格式: xxx@im.wechat
示例: abc123@im.wechat
机器人 ID
格式: xxx@im.bot
示例: ba36538a1eb2@im.bot
contextToken 是消息上下文标识,用于将发送消息与对应会话关联起来。
从接收到的消息对象中获取:
String contextToken = msg.getContext_token();发送文本、图片、文件、语音、视频,以及输入态控制时,底层都会依赖这个上下文标识。
新版 SDK 不要求你手动传入 contextToken,而是通过 getUpdates() 拉取消息后,自动把最新 contextToken 缓存到 ConversationContext 中,后续发送时自动使用。
cursor 是消息分页与增量拉取的关键机制。
首次调用时,cursor 为空字符串:
List<WeixinMessage> messages = client.getUpdates();SDK 内部会自动管理 cursor,业务层通常不需要手动维护。
- 新 cursor:获取该 cursor 之后的新消息
- 旧 cursor:可能返回该 cursor 及之后的历史数据
- 首次调用:cursor 为空字符串
新版 SDK 已在 UpdateService 内部封装了 cursor 存储与更新逻辑。
client_id 用于消息幂等控制,是每次发送消息时的唯一标识。
- 避免重复发送导致重复接收
- 用于服务端幂等判定
新版 SDK 内部自动生成 client_id,业务层通常不需要手动指定。
| 类型值 | 描述 |
|---|---|
1 |
图片 |
2 |
视频 |
3 |
文件 |
4 |
语音 |
新版 SDK 已按该类型映射到图片、视频、文件、语音的上传与发送逻辑。
| 状态 | 描述 |
|---|---|
WAIT / WAITING |
等待扫码 |
SCANED / SCANNED |
已扫码 |
CONFIRMED / LOGGED_IN |
登录成功 |
EXPIRED |
二维码已过期 |
不同接口与内部状态对象的命名可能略有差异,但语义一致。
SDK 当前采用缓存最新 contextToken 的发送模型。
- 当 SDK 通过
getUpdates()拉取到用户消息后,会从入站消息中提取最新的contextToken ConversationContext会按(botId, userId)维度缓存该用户最新上下文- 后续
sendText、sendImage、sendFile、sendVoice、sendVideo、startTyping、stopTyping等操作会基于该最新contextToken执行
发送消息前,目标用户必须先给 bot 发过消息,并且该消息已经被 SDK 通过 getUpdates() 拉取到;否则 SDK 无法获取该用户最新 contextToken,会抛出上下文缺失异常。
client.clearContext("user@im.wechat");
client.clearAllContexts();SDK 通过 ILinkConfig 统一管理客户端参数,包括但不限于:
- 连接超时
- 读取超时
- 写入超时
- HTTP 重试次数
- 重试基础退避时间
- 最大退避时间
- 是否启用抖动
- 登录超时
- 心跳间隔
- 是否启用心跳
- 线程池参数
channelVersionrouteTag
ILinkConfig config = ILinkConfig.builder()
.connectTimeoutMs(15000)
.readTimeoutMs(15000)
.writeTimeoutMs(15000)
.httpMaxRetries(5)
.retryBaseDelayMs(1000)
.retryMaxDelayMs(10000)
.heartbeatEnabled(true)
.heartbeatIntervalMs(30000)
.channelVersion("1.0.0")
.build();
ILinkClient client = ILinkClient.builder()
.config(config)
.build();重试时增加随机抖动的目的,是防止大量请求在同一时刻失败后,又在同一时刻重试,形成“重试风暴”。
queueCapacity 表示线程池任务队列容量,用于控制线程繁忙时最多允许多少任务排队等待处理。
new OnLoginListener() {
@Override
public void onLoginSuccess(LoginContext context) {
// 登录成功
}
@Override
public void onLoginFailure(Throwable throwable) {
// 登录失败
}
};new OnMessageListener() {
@Override
public void onMessages(List<WeixinMessage> messages) {
// 收到消息
}
};new OnDisconnectListener() {
@Override
public void onDisconnect(Throwable throwable) {
// 断线事件
}
};new OnHeartbeatListener() {
@Override
public void onHeartbeatSuccess() {
// 心跳成功
}
@Override
public void onHeartbeatFailure(Throwable throwable) {
// 心跳失败
}
};try {
client.sendText("user@im.wechat", "Hello");
} catch (NotLoginException e) {
System.err.println("请先登录: " + e.getMessage());
} catch (ILinkException e) {
System.err.println("SDK异常: " + e.getMessage());
} catch (IOException e) {
System.err.println("网络或IO异常: " + e.getMessage());
}客户端实现了 AutoCloseable,建议在业务结束时显式关闭:
client.close();或者:
try (ILinkClient client = ILinkClient.builder().build()) {
// 使用 client
}import com.github.wechat.ilink.sdk.ILinkClient;
import com.github.wechat.ilink.sdk.core.config.ILinkConfig;
import com.github.wechat.ilink.sdk.core.listener.OnLoginListener;
import com.github.wechat.ilink.sdk.core.listener.OnMessageListener;
import com.github.wechat.ilink.sdk.core.login.LoginContext;
import com.github.wechat.ilink.sdk.core.model.MessageItem;
import com.github.wechat.ilink.sdk.core.model.WeixinMessage;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
public class QuickStartExample {
public static void main(String[] args) throws Exception {
ILinkConfig config = ILinkConfig.builder()
.connectTimeoutMs(35000)
.readTimeoutMs(35000)
.writeTimeoutMs(35000)
.httpMaxRetries(3)
.retryBaseDelayMs(1000)
.retryMaxDelayMs(10000)
.heartbeatEnabled(true)
.heartbeatIntervalMs(30000)
.channelVersion("1.0.0")
.build();
ILinkClient client = ILinkClient.builder()
.config(config)
.onLogin(new OnLoginListener() {
@Override
public void onLoginSuccess(LoginContext context) {
System.out.println("登录成功,botId = " + context.getBotId());
}
@Override
public void onLoginFailure(Throwable throwable) {
System.err.println("登录失败: " + throwable.getMessage());
}
})
.onMessage(new OnMessageListener() {
@Override
public void onMessages(List<WeixinMessage> messages) {
for (WeixinMessage msg : messages) {
System.out.println("收到消息 fromUserId = " + msg.getFrom_user_id());
if (msg.getItem_list() != null) {
for (MessageItem item : msg.getItem_list()) {
if (item.getText_item() != null) {
System.out.println("text = " + item.getText_item().getText());
}
}
}
}
}
})
.build();
try {
String qrCodeContent = client.executeLogin();
System.out.println("请扫码登录:");
System.out.println(qrCodeContent);
LoginContext context = client.getLoginFuture().get();
System.out.println("登录完成,botId = " + context.getBotId());
List<WeixinMessage> messages = client.getUpdates();
System.out.println("首次拉取消息数 = " + messages.size());
String targetUserId = "这里替换成真实的 from_user_id";
client.sendText(targetUserId, "Hello, iLink!");
client.sendTextWithTyping(targetUserId, "这是一条带输入态的消息", 1500L);
byte[] imageBytes = Files.readAllBytes(Paths.get("demo.png"));
client.sendImage(targetUserId, imageBytes, "demo.png", "这是一张测试图片");
byte[] fileBytes = Files.readAllBytes(Paths.get("demo.pdf"));
client.sendFile(targetUserId, fileBytes, "demo.pdf", "这是一个测试文件");
byte[] voiceBytes = Files.readAllBytes(Paths.get("demo.silk"));
client.sendVoice(targetUserId, voiceBytes, "demo.silk", 3000, 16000);
byte[] videoBytes = Files.readAllBytes(Paths.get("demo.mp4"));
client.sendVideo(targetUserId, videoBytes, "demo.mp4", 5000, "这是一个测试视频");
} finally {
client.close();
}
}
}- iMoney - 一个入口简单,功能强大,智能便捷的记账工具
优先检查:
- 网络是否正常
- 二维码是否过期
- 登录轮询是否完整结束
- 是否成功拿到
LoginContext
原因通常是目标用户还没有被 getUpdates() 拉到最新上下文。
解决方式:
- 先让该用户给 bot 发消息
- 调用
getUpdates() - 再执行发送
优先检查:
- 文件是否能正常读取
getuploadurl是否成功返回- CDN 上传是否成功
contextToken是否存在
优先检查:
- 收到的消息项里是否真的包含媒体
CDNMedia.encrypt_query_param是否存在CDNMedia.aes_key是否存在- 下载后的内容是否被正确保存
当前产品模型下,登录状态失效后需要手动重新登录,不支持自动重新拉起二维码登录流程。
当前版本已经覆盖核心 SDK 主链路。后续如需增强,可考虑:
- 更智能的媒体文件名与扩展名识别
- 更丰富的示例代码
- 更完整的单元测试与集成测试
- 更细粒度的日志与观测能力
MIT License
- GitHub Issues:用于提交 Bug 与使用问题
- Pull Request:欢迎贡献文档、示例和代码改进
欢迎大家扫码进群交流问题