diff --git a/docs/DEVIATIONS.md b/docs/DEVIATIONS.md
index 831164a..3a99bc7 100644
--- a/docs/DEVIATIONS.md
+++ b/docs/DEVIATIONS.md
@@ -465,7 +465,49 @@
## SDK 二 S2
-暂无。
+### S2-PY/JAVA 1–3 2026-09-30
+
+1. **HiveMQ「websocket 模块」用 netty-codec-http 显式依赖**
+ - 原条款:DEVELOPMENT 2.3「HiveMQ MQTT Client,加上 websocket 模块」。
+ - 实际做法:依赖 `com.hivemq:hivemq-mqtt-client:1.3.5`,并额外声明 `io.netty:netty-codec-http`;连接时用 `webSocketConfig().subprotocol("mqtt")`。未使用独立 artifact `hivemq-mqtt-client-websocket`(Maven Central 上该坐标未作为独立稳定模块发布)。
+ - 原因:与 HiveMQ 官方 WebSocket 用法一致,满足子协议 `mqtt`。
+ - 备选方案:若日后官方拆出独立 websocket 模块再改坐标。
+ - 影响:无行为差异。
+
+2. **JSON 库选型**
+ - 原条款:未指定 Java JSON 库。
+ - 实际做法:Java 用 Gson(`disableHtmlEscaping`);Python 用标准库 `json`(`ensure_ascii=False`)。
+ - 原因:满足「不转义 HTML / 非 ASCII」;不引入过重依赖。
+ - 备选方案:Jackson。
+ - 影响:无。
+
+3. **假传输单测,未接真实服务器**
+ - 原条款:任务 1–3 单元测试用假传输;任务 4 才做接入清单。
+ - 实际做法:Python `FakeTransport`、Java `FakeTransport` 覆盖 Clean Start、去重再 ack、本地超限、令牌回调、重交不改 `send_at_ms` / 消息号;未做对真实服务器的接入清单(任务 4)。
+ - 原因:本波范围。
+ - 备选方案:无。
+ - 影响:真实联调留待 S2 任务 4。
+
+4. **Python 发布元数据**
+ - 原条款:`license = { file = "LICENSE" }` 与专有分类。
+ - 实际做法:`pyproject.toml` 已按此写;包内复制仓库根 `LICENSE`。未配置/执行 PyPI 发布。
+ - 原因:发布在阶段 3。
+ - 备选方案:无。
+ - 影响:无。
+
+5. **Java 编译器用 JDK 21,目标字节码 8**
+ - 原条款:字节码目标 Java 8。
+ - 实际做法:`maven.compiler.release=8`,本机用 Temurin 21 编译。
+ - 原因:环境已有 JDK 21。
+ - 备选方案:用 JDK 8 工具链。
+ - 影响:无。
+
+6. **Paho / HiveMQ 库内自动重连关闭,退避自管**
+ - 原条款:四种 SDK 同一套重连:1s 起加倍上限 30s ±30% 抖动,稳定 60s 恢复;每次 Clean Start、会话过期 0。
+ - 实际做法:两端均由 SDK 连接循环实现退避与停止条件;HiveMQ 不启库内 automaticReconnect;Paho 每次 `connect(..., clean_start=True)` 并设 `SessionExpiryInterval=0`。
+ - 原因:与 Go/JS 要求一致,避免两套重连。
+ - 备选方案:依赖库自带重连再改 Clean Start(易漏)。
+ - 影响:无。
## 测试交付 Q
diff --git a/sdk/java/.gitignore b/sdk/java/.gitignore
new file mode 100644
index 0000000..105499d
--- /dev/null
+++ b/sdk/java/.gitignore
@@ -0,0 +1,6 @@
+target/
+.idea/
+*.iml
+.classpath
+.project
+.settings/
diff --git a/sdk/java/.gitkeep b/sdk/java/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/sdk/java/LICENSE b/sdk/java/LICENSE
new file mode 100644
index 0000000..e1c9937
--- /dev/null
+++ b/sdk/java/LICENSE
@@ -0,0 +1,10 @@
+Copyright (c) 2026 Nixevol. All rights reserved.
+
+本仓库的源代码、文档、各语言 SDK 和构建产物(包括发布的软件包和 Docker 镜像)均为专有软件。
+源代码和发布物公开可读,不代表授予任何使用许可。未经版权所有者书面许可,不得使用、复制、
+修改、合并、发布、分发、再许可或出售其任何部分。
+
+This repository, including its source code, documentation, SDKs and build artifacts (including
+published packages and Docker images), is proprietary software. Public visibility does not grant
+any license. No part of it may be used, copied, modified, merged, published, distributed,
+sublicensed or sold without prior written permission from the copyright holder.
diff --git a/sdk/java/README.md b/sdk/java/README.md
new file mode 100644
index 0000000..c75d282
--- /dev/null
+++ b/sdk/java/README.md
@@ -0,0 +1,34 @@
+# NixMsg Java / Android SDK
+
+坐标:`asia.asio.nixmsg:nixmsg-sdk`
+包名:`asia.asio.nixmsg`
+字节码目标:Java 8
+接口:`CompletableFuture`
+
+## Android
+
+最低 API **24**。MQTT 长连接由应用自行放入**前台服务**,SDK 不创建也不托管服务生命周期。
+
+## 依赖
+
+Maven / Gradle 仓库:
+
+```
+https://git.asio.asia/api/packages/nixevol/maven
+```
+
+HiveMQ MQTT Client(含 WebSocket:`webSocketConfig` + `netty-codec-http`)。
+
+## 最小示例
+
+```java
+Client c = new Client();
+c.onSession(token -> { /* 应用保存 */ });
+c.onMessage(msg -> System.out.println(msg.id + " " + msg.body.data));
+c.connect("ws://127.0.0.1:7443/mqtt", "device-1", "secret", null)
+ .thenCompose(v -> c.send(new Types.Target("endpoint", "device-2"), new Types.Body("hello"), new Types.SendOptions()))
+ .join();
+c.close();
+```
+
+许可证见 `LICENSE`(专有)。
diff --git a/sdk/java/pom.xml b/sdk/java/pom.xml
new file mode 100644
index 0000000..bc5f659
--- /dev/null
+++ b/sdk/java/pom.xml
@@ -0,0 +1,90 @@
+
+
+ 4.0.0
+
+ asia.asio.nixmsg
+ nixmsg-sdk
+ 0.1.0
+ jar
+ nixmsg-sdk
+ NixMsg Java/Android SDK
+
+
+
+ Proprietary
+ https://git.asio.asia/nixevol/NixMsg/src/branch/main/LICENSE
+ repo
+
+
+
+
+ UTF-8
+ 8
+ 1.3.5
+ 4.13.2
+
+
+
+
+ com.hivemq
+ hivemq-mqtt-client
+ ${hivemq.mqtt.version}
+
+
+
+ io.netty
+ netty-codec-http
+ 4.1.118.Final
+
+
+ com.google.code.gson
+ gson
+ 2.11.0
+
+
+ junit
+ junit
+ ${junit.version}
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+ 8
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.2
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.4.2
+
+
+
+
+
+
+ central
+ https://repo.maven.apache.org/maven2
+
+
+
+
+
+ asio-gitea
+ https://git.asio.asia/api/packages/nixevol/maven
+
+
+
diff --git a/sdk/java/src/main/java/asia/asio/nixmsg/Client.java b/sdk/java/src/main/java/asia/asio/nixmsg/Client.java
new file mode 100644
index 0000000..19e9347
--- /dev/null
+++ b/sdk/java/src/main/java/asia/asio/nixmsg/Client.java
@@ -0,0 +1,1223 @@
+package asia.asio.nixmsg;
+
+import asia.asio.nixmsg.Types.Body;
+import asia.asio.nixmsg.Types.ConnectionEvent;
+import asia.asio.nixmsg.Types.ConnectionState;
+import asia.asio.nixmsg.Types.GroupEvent;
+import asia.asio.nixmsg.Types.HelloLimits;
+import asia.asio.nixmsg.Types.IncomingMessage;
+import asia.asio.nixmsg.Types.PresenceEvent;
+import asia.asio.nixmsg.Types.Receipt;
+import asia.asio.nixmsg.Types.RegisterOptions;
+import asia.asio.nixmsg.Types.RegisterResult;
+import asia.asio.nixmsg.Types.RecallResult;
+import asia.asio.nixmsg.Types.RevokedEvent;
+import asia.asio.nixmsg.Types.SendOptions;
+import asia.asio.nixmsg.Types.SendResult;
+import asia.asio.nixmsg.Types.Target;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * NixMsg Java/Android SDK。接口返回 {@link CompletableFuture}。
+ * Android 长连接由应用自行放入前台服务(最低 API 24)。
+ */
+public final class Client {
+ private static final Logger LOG = Logger.getLogger("nixmsg");
+ private static final String DELIVERED = "delivered";
+ private static final String ACKED = "acked";
+
+ private final Transport transport;
+ private final boolean autoAck;
+ private final int maxReceiveBytes;
+ private final String clientName;
+ private final long connectTimeoutMs;
+
+ private final Object lock = new Object();
+ private final Object cbLock = new Object();
+ private final AtomicLong ridSeq = new AtomicLong();
+ private final Map pending = new LinkedHashMap();
+ private final List sendQueue = new ArrayList();
+ private final LinkedHashMap dedup = new LinkedHashMap();
+ private final LinkedHashMap receiptSeen = new LinkedHashMap();
+
+ private volatile ConnectionState state = ConnectionState.OFFLINE;
+ private volatile boolean stopReconnect;
+ private volatile boolean closed;
+ private volatile boolean userClose;
+ private volatile boolean wantConnected;
+ private volatile boolean connectingWithToken;
+ private String url = "";
+ private String endpointId = "";
+ private String password;
+ private String sessionToken;
+ private boolean useTcp;
+ private HelloLimits limits = new HelloLimits();
+ private long clockSkewMs;
+ private long onlineSinceMs;
+ private long backoffMs = Types.BACKOFF_INITIAL_MS;
+ private int inflightSends;
+ private List watchIds;
+ private boolean watchAll;
+ private volatile Throwable handshakeError;
+ private volatile String authReason = "";
+ private final AtomicBoolean connReady = new AtomicBoolean(false);
+ private final Object connWait = new Object();
+ private Thread worker;
+ private final Object wake = new Object();
+
+ private Consumer sessionHandler;
+ private Consumer messageHandler;
+ private Consumer receiptHandler;
+ private Consumer revokedHandler;
+ private Consumer presenceHandler;
+ private Consumer groupHandler;
+ private Consumer connectionHandler;
+
+ public Client() {
+ this(new HiveMqTransport(), true, Types.DEFAULT_MAX_FRAME, Types.CLIENT_NAME, Types.CONNECT_TIMEOUT_MS);
+ }
+
+ public Client(Transport transport) {
+ this(transport, true, Types.DEFAULT_MAX_FRAME, Types.CLIENT_NAME, Types.CONNECT_TIMEOUT_MS);
+ }
+
+ public Client(Transport transport, boolean autoAck, int maxReceiveBytes, String clientName, long connectTimeoutMs) {
+ this.transport = transport;
+ this.autoAck = autoAck;
+ this.maxReceiveBytes = Math.max(Types.MIN_MAX_RECEIVE, maxReceiveBytes);
+ this.clientName = clientName;
+ this.connectTimeoutMs = connectTimeoutMs;
+ this.transport.setHandlers(this::onTransportConnected, this::onTransportDisconnected, this::onDown);
+ }
+
+ public void onSession(Consumer handler) { this.sessionHandler = handler; }
+ public void onMessage(Consumer handler) { this.messageHandler = handler; }
+ public void onReceipt(Consumer handler) { this.receiptHandler = handler; }
+ public void onRevoked(Consumer handler) { this.revokedHandler = handler; }
+ public void onPresence(Consumer handler) { this.presenceHandler = handler; }
+ public void onGroupEvent(Consumer handler) { this.groupHandler = handler; }
+ public void onConnection(Consumer handler) { this.connectionHandler = handler; }
+
+ public ConnectionState getState() { return state; }
+ public HelloLimits getLimits() { return limits; }
+ public String getSessionToken() { return sessionToken; }
+ public long getClockSkewMs() { return clockSkewMs; }
+
+ public CompletableFuture connect(String url, String endpointId, String password, String sessionToken) {
+ return connect(url, endpointId, password, sessionToken, false);
+ }
+
+ public CompletableFuture connect(String url, String endpointId, String password, String sessionToken, boolean useTcp) {
+ return CompletableFuture.runAsync(() -> connectSync(url, endpointId, password, sessionToken, useTcp));
+ }
+
+ public void connectSync(String url, String endpointId, String password, String sessionToken, boolean useTcp) {
+ if (password == null && sessionToken == null) {
+ throw new IllegalArgumentException("需要 password 或 sessionToken");
+ }
+ synchronized (lock) {
+ if (closed) {
+ throw new NixMsgException("closed", "已关闭");
+ }
+ this.url = useTcp ? url : Protocol.normalizeMqttWsUrl(url);
+ this.endpointId = endpointId;
+ this.password = password;
+ this.sessionToken = sessionToken;
+ this.useTcp = useTcp;
+ this.stopReconnect = false;
+ this.userClose = false;
+ this.wantConnected = true;
+ this.handshakeError = null;
+ connReady.set(false);
+ setState(ConnectionState.CONNECTING, "");
+ if (worker == null || !worker.isAlive()) {
+ worker = new Thread(this::runLoop, "nixmsg-client");
+ worker.setDaemon(true);
+ worker.start();
+ }
+ wakeUp();
+ }
+ long deadline = System.currentTimeMillis() + connectTimeoutMs + 5000;
+ synchronized (connWait) {
+ while (!connReady.get() && System.currentTimeMillis() < deadline) {
+ try {
+ connWait.wait(200);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+ }
+ if (handshakeError != null) {
+ if (handshakeError instanceof RuntimeException) {
+ throw (RuntimeException) handshakeError;
+ }
+ throw new NixMsgException("busy", handshakeError.getMessage());
+ }
+ if (state != ConnectionState.ONLINE) {
+ if (state == ConnectionState.AUTH_FAILED) {
+ throw new NixMsgException(authReason.isEmpty() ? "bad_credentials" : authReason, "认证失败");
+ }
+ if (state == ConnectionState.KICKED) {
+ throw new NixMsgException("taken_over", "会话被接管");
+ }
+ throw new NixMsgException("busy", "连接未成功: " + state);
+ }
+ }
+
+ public CompletableFuture closeAsync() {
+ return CompletableFuture.runAsync(this::close);
+ }
+
+ public void close() {
+ synchronized (lock) {
+ userClose = true;
+ wantConnected = false;
+ stopReconnect = true;
+ closed = true;
+ failAll(new NixMsgException("closed", "已关闭"));
+ setState(ConnectionState.OFFLINE, "");
+ }
+ try {
+ transport.disconnect();
+ } catch (Exception ignored) {
+ }
+ wakeUp();
+ }
+
+ public CompletableFuture logout() {
+ return CompletableFuture.runAsync(() -> {
+ try {
+ request(mapOf("type", "self.logout"), true);
+ } catch (Exception ignored) {
+ }
+ synchronized (lock) {
+ stopReconnect = true;
+ wantConnected = false;
+ sessionToken = null;
+ failAll(new NixMsgException("auth_failed", "已退出登录"));
+ }
+ try {
+ transport.disconnect();
+ } catch (Exception ignored) {
+ }
+ setState(ConnectionState.OFFLINE, "");
+ wakeUp();
+ });
+ }
+
+ public CompletableFuture send(Target to, Body body, SendOptions options) {
+ return CompletableFuture.supplyAsync(() -> sendSync(to, body, options));
+ }
+
+ public SendResult sendSync(Target to, Body body, SendOptions options) {
+ if (options == null) {
+ options = new SendOptions();
+ }
+ if (options.contentType != null) {
+ body.contentType = options.contentType;
+ }
+ final Pending pendingReq;
+ synchronized (lock) {
+ if (closed) {
+ throw new NixMsgException("closed", "已关闭");
+ }
+ if (sendQueue.size() >= Types.SEND_QUEUE_LIMIT) {
+ throw new NixMsgException("quota_exceeded", "发送队列已满");
+ }
+ int maxBody = limits.maxBodyBytes > 0 ? limits.maxBodyBytes : Types.DEFAULT_MAX_BODY;
+ int maxMeta = limits.maxMetaBytes > 0 ? limits.maxMetaBytes : Types.DEFAULT_MAX_META;
+ int maxFrame = limits.maxFrameBytes > 0 ? limits.maxFrameBytes : Types.DEFAULT_MAX_FRAME;
+ if (body.decodedSize() > maxBody) {
+ throw new NixMsgException("body_too_large", "正文超限");
+ }
+ Map meta = options.meta == null ? new LinkedHashMap() : options.meta;
+ byte[] metaRaw = Protocol.dumps(meta);
+ if (metaRaw.length > maxMeta) {
+ throw new NixMsgException("meta_too_large", "自定义字段超限");
+ }
+ String msgId = options.messageId != null ? options.messageId : Uuid7.next();
+ Map frame = new LinkedHashMap();
+ frame.put("v", 1);
+ frame.put("type", "send");
+ frame.put("id", msgId);
+ frame.put("to", to.toMap());
+ frame.put("body", body.toMap());
+ if (!meta.isEmpty()) {
+ frame.put("meta", meta);
+ }
+ if (options.sendAtMs != null) {
+ frame.put("send_at_ms", options.sendAtMs);
+ } else if (options.delayMs != null) {
+ frame.put("delay_ms", options.delayMs);
+ }
+ if (options.keep) {
+ Map offline = new LinkedHashMap();
+ offline.put("keep", true);
+ if (options.ttlSeconds != null) {
+ offline.put("ttl_seconds", options.ttlSeconds);
+ }
+ frame.put("offline", offline);
+ }
+ if (!options.receipt) {
+ frame.put("receipt", false);
+ }
+ if (options.talkPassword != null && !options.talkPassword.isEmpty()) {
+ frame.put("talk_password", options.talkPassword);
+ }
+ Map probe = new LinkedHashMap(frame);
+ probe.put("rid", "00000000");
+ if (Protocol.dumps(probe).length > maxFrame) {
+ throw new NixMsgException("frame_too_large", "整帧超限");
+ }
+ pendingReq = new Pending(true, msgId);
+ sendQueue.add(new SendItem(msgId, frame, pendingReq));
+ wakeUp();
+ }
+ await(pendingReq.future, 3600_000L);
+ if (pendingReq.error != null) {
+ if (pendingReq.error instanceof RuntimeException) {
+ throw (RuntimeException) pendingReq.error;
+ }
+ throw new NixMsgException("busy", pendingReq.error.getMessage());
+ }
+ Map data = asMap(pendingReq.response.get("data"));
+ return new SendResult(str(data.get("id"), pendingReq.messageId),
+ longVal(data.get("send_at_ms"), 0),
+ str(data.get("state"), ""));
+ }
+
+ public CompletableFuture ack(IncomingMessage message) {
+ return CompletableFuture.runAsync(() -> sendAck(message.from, message.id, true));
+ }
+
+ public CompletableFuture recall(String messageId) {
+ return CompletableFuture.supplyAsync(() -> {
+ Map resp = request(mapOf("type", "recall", "id", messageId), true);
+ Map data = asMap(resp.get("data"));
+ return new RecallResult(str(data.get("result"), ""),
+ (int) longVal(data.get("recalled"), 0),
+ (int) longVal(data.get("accepted"), 0),
+ (int) longVal(data.get("other"), 0));
+ });
+ }
+
+ public CompletableFuture