Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Latest commit

 

History

31 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

企业化设计的微信 iLink Bot Java SDK,支持二维码登录、消息收发、输入态管理、图片/文件/语音/视频发送、媒体下载与会话上下文管理。


📋 概述

wechat-ilink-sdk 是一个面向 Java 开发者的微信 iLink Bot SDK,目标是为业务系统提供一套清晰、稳定、易集成的客户端能力。

当前版本已经覆盖 iLink Bot 的核心主链路:

  • 🔐 二维码登录与登录状态轮询
  • 📨 长轮询获取消息
  • 💬 基于最新 contextToken 的文本发送
  • 🖼️ 图片、文件、语音、视频发送
  • ⌨️ 输入状态开始 / 停止
  • ⏳ 带输入态发送文本
  • 📥 媒体消息下载与 AES 解密
  • 📝 会话上下文缓存与清理
  • 🏗️ Builder 模式创建客户端
  • ⚙️ 配置化、状态管理、心跳探测、监听器机制

SDK 采用 Builder 模式创建客户端,并内置统一配置、状态管理、异常体系、线程池与资源释放机制,适合在后端服务、机器人程序和集成型项目中使用。


📦 依赖配置

Maven

<dependency>
    <groupId>io.github.lith0924</groupId>
    <artifactId>wechat-ilink-sdk</artifactId>
    <version>2.3.3</version>
</dependency>

Gradle

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 单用户会话上下文,缓存最新 contextTokentypingTicket

🏗️ 基础设施层

组件 说明
ILinkConfig 客户端配置对象
ExecutorManager 线程池管理
RetryPolicy 重试策略
HeartbeatService 心跳 / 健康探测
BusinessApiClient 业务接口调用封装
HttpClientFacade HTTP 请求封装
ListenerRegistry 监听器注册中心

🚀 快速开始

1. 创建客户端

默认配置

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

2. 执行登录

String qrCodeContent = client.executeLogin();
System.out.println("请将以下内容渲染为二维码后扫码登录:");
System.out.println(qrCodeContent);

LoginContext context = client.getLoginFuture().get();
System.out.println("登录成功,botId = " + context.getBotId());

3. 拉取消息

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("[email protected]", "Hello, iLink!");

带输入态发送文本

client.sendTextWithTyping("[email protected]", "Hello with typing", 1500L);

发送图片

byte[] imageBytes = Files.readAllBytes(Paths.get("demo.png"));
client.sendImage("[email protected]", imageBytes, "demo.png", "这是一张测试图片");

发送文件

byte[] fileBytes = Files.readAllBytes(Paths.get("demo.pdf"));
client.sendFile("[email protected]", fileBytes, "demo.pdf", "这是一个测试文件");

发送语音

byte[] voiceBytes = Files.readAllBytes(Paths.get("demo.silk"));
client.sendVoice("[email protected]", voiceBytes, "demo.silk", 3000, 16000);

发送视频

byte[] videoBytes = Files.readAllBytes(Paths.get("demo.mp4"));
client.sendVideo("[email protected]", videoBytes, "demo.mp4", 5000, "这是一个测试视频");

⌨️ 输入状态管理

开始输入态

client.startTyping("[email protected]");

停止输入态

client.stopTyping("[email protected]");

📥 媒体下载

直接下载媒体

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 的设计方式。

1. 用户 ID

普通用户 ID

机器人 ID

2. contextToken

contextToken 是消息上下文标识,用于将发送消息与对应会话关联起来。

来源

从接收到的消息对象中获取:

String contextToken = msg.getContext_token();

用途

发送文本、图片、文件、语音、视频,以及输入态控制时,底层都会依赖这个上下文标识。

SDK 中的处理方式

新版 SDK 不要求你手动传入 contextToken,而是通过 getUpdates() 拉取消息后,自动把最新 contextToken 缓存到 ConversationContext 中,后续发送时自动使用。

3. cursor 游标机制

cursor 是消息分页与增量拉取的关键机制。

首次拉取

首次调用时,cursor 为空字符串:

List<WeixinMessage> messages = client.getUpdates();

SDK 内部会自动管理 cursor,业务层通常不需要手动维护。

协议语义

  • 新 cursor:获取该 cursor 之后的新消息
  • 旧 cursor:可能返回该 cursor 及之后的历史数据
  • 首次调用:cursor 为空字符串

新版 SDK 已在 UpdateService 内部封装了 cursor 存储与更新逻辑。

4. client_id

client_id 用于消息幂等控制,是每次发送消息时的唯一标识。

作用

  • 避免重复发送导致重复接收
  • 用于服务端幂等判定

SDK 中的处理方式

新版 SDK 内部自动生成 client_id,业务层通常不需要手动指定。

5. 媒体类型

类型值 描述
1 图片
2 视频
3 文件
4 语音

新版 SDK 已按该类型映射到图片、视频、文件、语音的上传与发送逻辑。

6. 登录状态

状态 描述
WAIT / WAITING 等待扫码
SCANED / SCANNED 已扫码
CONFIRMED / LOGGED_IN 登录成功
EXPIRED 二维码已过期

不同接口与内部状态对象的命名可能略有差异,但语义一致。


📝 上下文机制说明

SDK 当前采用缓存最新 contextToken 的发送模型。

工作方式

  • 当 SDK 通过 getUpdates() 拉取到用户消息后,会从入站消息中提取最新的 contextToken
  • ConversationContext 会按 (botId, userId) 维度缓存该用户最新上下文
  • 后续 sendTextsendImagesendFilesendVoicesendVideostartTypingstopTyping 等操作会基于该最新 contextToken 执行

重要前提

发送消息前,目标用户必须先给 bot 发过消息,并且该消息已经被 SDK 通过 getUpdates() 拉取到;否则 SDK 无法获取该用户最新 contextToken,会抛出上下文缺失异常。

清理上下文

client.clearContext("[email protected]");
client.clearAllContexts();

⚙️ 配置说明

SDK 通过 ILinkConfig 统一管理客户端参数,包括但不限于:

  • 连接超时
  • 读取超时
  • 写入超时
  • HTTP 重试次数
  • 重试基础退避时间
  • 最大退避时间
  • 是否启用抖动
  • 登录超时
  • 心跳间隔
  • 是否启用心跳
  • 线程池参数
  • channelVersion
  • routeTag

自定义配置示例

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

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("[email protected]", "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();
        }
    }
}

🛠️ 基于本 SDK 开发的工具

  • iMoney - 一个入口简单,功能强大,智能便捷的记账工具

❓ 常见问题

1. 登录失败或登录结果为空

优先检查:

  • 网络是否正常
  • 二维码是否过期
  • 登录轮询是否完整结束
  • 是否成功拿到 LoginContext

2. 发送消息时报 missing latest context token

原因通常是目标用户还没有被 getUpdates() 拉到最新上下文。
解决方式:

  1. 先让该用户给 bot 发消息
  2. 调用 getUpdates()
  3. 再执行发送

3. 媒体发送失败

优先检查:

  • 文件是否能正常读取
  • getuploadurl 是否成功返回
  • CDN 上传是否成功
  • contextToken 是否存在

4. 媒体下载失败

优先检查:

  • 收到的消息项里是否真的包含媒体
  • CDNMedia.encrypt_query_param 是否存在
  • CDNMedia.aes_key 是否存在
  • 下载后的内容是否被正确保存

5. 登录掉线后如何恢复

当前产品模型下,登录状态失效后需要手动重新登录,不支持自动重新拉起二维码登录流程。


🛣️ 路线说明

当前版本已经覆盖核心 SDK 主链路。后续如需增强,可考虑:

  • 更智能的媒体文件名与扩展名识别
  • 更丰富的示例代码
  • 更完整的单元测试与集成测试
  • 更细粒度的日志与观测能力

📄 许可证

MIT License


🤝 支持与反馈

  • GitHub Issues:用于提交 Bug 与使用问题
  • Pull Request:欢迎贡献文档、示例和代码改进

💬 交流社群

欢迎大家扫码进群交流问题

762b713d7d31eead056074c247cfbf1f

About

微信 iLink Bot Java SDK,支持二维码登录、消息收发、媒体上传下载与AES加解密

Resources

Stars

101 stars

Watchers

3 watching

Forks

Releases

Packages

Contributors

Languages