Java / Spring Boot SDK
Java 服務端 SDK 已釋出到 Maven Central,當前版本 1.0.0-alpha.3(MIT 許可證),包含 15 個服務、198 個 OpenAPI 操作。核心庫基於 Java 17 和 Jackson;Spring Boot Starter 提供配置屬性繫結、可以替換的 DRClient Bean 和可選的回撥接收器。Maven 的 groupId 為 cn.deeprespond,Java 的包名為 com.deeprespond.im。
Maven 安裝
在業務工程中新增依賴。使用 Spring Boot 的新增 Starter,不使用的新增核心庫 im-server-sdk:
<dependency>
<groupId>cn.deeprespond</groupId>
<artifactId>im-spring-boot-starter</artifactId>
<version>1.0.0-alpha.3</version>
</dependency>需要 JDK 17 以上;直接從 Maven Central 下載,不需要釋出令牌,也不需要先在本地 install。從原始碼構建和執行 Demo 見倉庫中的說明。
配置
deeprespond:
im:
base-url: ${IM_BASE_URL}
org-name: ${IM_ORG_NAME}
app-name: ${IM_APP_NAME}
client-id: ${IM_CLIENT_ID}
client-secret: ${IM_CLIENT_SECRET}在需要的地方通過建構函式注入 com.deeprespond.im.DRClient,用 im.users()、im.messages() 等呼叫介面。Client Secret 和 App Token 始終只儲存在後端。
var result = im.messages().send(Params.of("from", "alice")
.set("to_user", "bob").set("type", "text").set("body", Map.of("text", "你好")));
String messageId = result.data().messageId();需要匯入 com.deeprespond.im.Params 和 java.util.Map。響應的 data 是記錄型別(record),raw 保留服務端返回的全部欄位;不要把含敏感資訊的響應寫進日誌。
AI 流式回覆
AI 對話中,使用者給機器人發訊息後你會收到 ai.message_received 回撥,然後用 client.ai().startStream(...) 開始一條回覆,邊生成邊寫入。返回的 ReplyStream 可以被多個執行緒同時寫入,後臺執行緒替你處理增量的編號與合批(滿 20 毫秒或 4 KB 發一段)、持續寫入與按批寫入的切換、失敗重發;連續三次寫入失敗時以 fail("transport", "") 結束。機器人管理用 client.ai().createBot(Params) 等方法。
回撥處理函式應儘快返回,模型呼叫放到執行緒池中。withClientStreamId 用事件 ID,回撥重新投遞時不會開始第二條回覆:
handler.onAIMessage(event -> {
JsonNode data = event.path("data");
ReplyStream stream = client.ai().startStream(
StartStreamInput.of(data.path("bot").asText(), data.path("from").asText())
.withReplyToSeq(data.path("message").path("seq").asLong())
.withClientStreamId(event.path("id").asText()));
executor.submit(() -> reply(stream, data.path("message").path("body").path("text").asText()));
});withReplace(true) 先結束會話中正在生成的回覆;withBatchOnly(true) 只用按批寫入。
pipeOpenAI、pipeOpenAIResponses、pipeAnthropic、pipeGemini 讀完模型的流後自動 finish;模型返回錯誤時以 fail("model_error", 说明) 結束並丟擲。引數可以是 Flow.Publisher、Iterable、Iterator、java.util.stream.Stream(塊為官方 SDK 的物件、JsonNode、Map,或 SSE 文本),也可以是 SSE 格式的 InputStream、Reader 或 HttpResponse。四種轉換的規則與 Go SDK 相同。
void reply(ReplyStream stream, String question) {
try (stream; var response = openai.chat().completions().createStreaming(ChatCompletionCreateParams.builder()
.model("gpt-4.1").addUserMessage(question).build())) {
stream.pipeOpenAI(response.stream());
}
}手動寫入:
stream.writeReasoning("先查询天气。");
stream.writeTool("call_1", "weather", "查询天气", ReplyStream.TOOL_RUNNING);
stream.writeTool("call_1", "weather", "查询天气", ReplyStream.TOOL_DONE);
stream.writeText("明天上海**多云**,最高 23℃。");
stream.writeImage("https://cdn.example.com/a.png", 800, 600, "天气图");
AIStream result = stream.finish(new ReplyStream.FinishOptions("stop", "gpt-4.1", 120L, 380L));write*只放入記憶體,不等待網路;flush()立即發出並等待確認;finish先確認全部內容再結束。- 模型呼叫失敗時呼叫
stream.fail("model_error", "模型暂时不可用"),已生成的內容保留,使用者看到“生成中斷”。 close()(或 try-with-resources)在沒有結束時以fail("canceled", "")結束。- 使用者點停止生成、回覆超時或被新的回覆替換時,
stream.cancellation()(CompletableFuture<DRException>)完成,isCancelled()為真,之後的寫入丟擲stream_closed;pipe*停止讀取並關閉模型的流。CallbackHandler收到ai.stream_stopped時自動停止本程序中對應的流;自己處理回撥時呼叫client.ai().streamStopped(streamId)。
遊戲房間
遊戲房間給多人線上遊戲提供即時對戰通道。你的服務端用 im.game() 建房、給玩家簽發加入票據、對局結束時結束房間;玩家加入房間和對戰由客戶端 SDK 完成。下面是一局對戰在服務端要做的事(im 是注入的 DRClient):
/** 匹配成功后建房,给每个玩家签一张加入票据。 */
Map<String, String> startMatch(String matchId, List<String> players) {
try {
// 房间 ID 用你的对局 ID:重试或重复调用都只有一个房间
im.game().createRoom(matchId, Params.of("max_players", players.size())
.set("start_mode", "all_ready").set("ext", Map.of("mode", "ranked")));
} catch (DRException e) {
if ("game_capacity_unavailable".equals(e.reason())) throw new IllegalStateException("游戏服务繁忙,请稍后再试", e);
if ("open_rooms".equals(e.reason())) throw new IllegalStateException("同时进行的对局太多", e);
throw e;
}
List<Map<String, Object>> users = new ArrayList<>();
for (int seat = 0; seat < players.size(); seat++) users.add(Map.of("username", players.get(seat), "seat", seat));
// 不会自动重试;超时后直接再签一次,旧票据随之作废
var tickets = im.game().issueTickets(matchId, Params.of("users", users)).data().tickets();
Map<String, String> byUser = new HashMap<>();
for (var t : tickets) byUser.put(t.username(), t.ticket()); // 经你的业务接口分别交给每个玩家,不要写进日志
return byUser;
}
/** 对局结束时带上结算结果结束房间。 */
void finishMatch(String matchId, String winner) {
var res = im.game().closeRoom(matchId, Params.of("reason", "对局结束").set("result", Map.of("winner", winner)));
// 202:这次开始了结束;200:房间之前已在结束(例如超时自动结束),这次的原因和结果不生效
if (res.status() == 200) System.out.println(matchId + " 之前已在结束,这次的结果不生效");
}
/** 把作弊的玩家移出房间;返回 false 表示他已经不在房间里。 */
boolean kickCheater(String matchId, String username) {
return im.game().removeMember(matchId, username, Params.of("reason", "作弊")).status() == 202;
}需要匯入 com.deeprespond.im.DRException、com.deeprespond.im.Params 和 java.util.*。
| 要做什麼 | 方法 | 注意 |
|---|---|---|
| 建房 | createRoom(roomId, Params) | 房間 ID 用你的對局 ID。同樣的引數重複呼叫返回同一個房間(狀態碼 200,新建為 201),網路錯誤時自動重試 |
| 簽發加入票據 | issueTickets(roomId, Params) | 每次 1–10 人。不自動重試:超時後直接再籤一次,同一使用者之前沒用的票據隨之作廢。票據原文只返回這一次,不會出現在 toString() 中,也不要寫進日誌 |
| 手動開局 | startRoom(roomId, Params.empty()) | 只用於開局方式為 manual 的房間 |
| 結束房間 | closeRoom(roomId, Params) | 結算結果原樣發給玩家和 game_room.closed 回撥。狀態碼 202 表示這次開始了結束;200 表示房間之前已在結束,這次的原因和結果不生效 |
| 移出玩家 | removeMember(roomId, username, Params) | SDK 自動生成 operation_id,重試時不變,不改你傳入的 Params。狀態碼 200 且 member() 為 null 表示玩家不在房間裡 |
| 查詢 | getRoom、listRooms、listMembers、listRegions | 都傳 Params(沒有引數時用 Params.empty());listRooms 用 cursor 翻頁 |
- 每個方法都有帶
RequestOptions的過載;結果是Result<T>,status()是 HTTP 狀態碼,data()是記錄型別,raw()保留全部欄位。 - 常見錯誤丟擲
DRException:大區暫時沒有空閒的遊戲節點時code()為unavailable、reason()為game_capacity_unavailable,稍後重試,或在建房時用regions給出候選大區;同時進行的房間數達到上限為limit_exceeded(open_rooms)。全部錯誤見遊戲房間的錯誤。 - 擴充套件欄位
ext不超過 2KB,結算結果result不超過 4KB;上限和計費見控制台、計費與限制。 - 引數和返回的欄位見遊戲房間:服務端介面。遊戲房間的方法從 1.0.0-alpha.3 起提供。
遊戲房間回撥
CallbackHandler.on 按事件名註冊,收到的事件是 JsonNode,欄位在 data 中。大多數遊戲只需要處理 game_room.closed:
var handler = new CallbackHandler(id -> secrets.getOrDefault(id, List.of()), new MemoryCallbackStore());
handler.on("game_room.closed", event -> {
JsonNode data = event.path("data");
// result 是结束房间时给的结算结果;超时、全员离开等自动结束时为 null
saveMatch(data.path("room_id").asText(), data.path("close_reason").asText(), data.path("result"));
for (JsonNode m : data.path("members")) addPlayMinutes(m.path("username").asText(), m.path("billable_minutes").asLong());
});
handler.on("game_room.member_left", event -> {
JsonNode data = event.path("data");
System.out.printf("%s 离开了 %s:%s%n", data.path("username").asText(), data.path("room_id").asText(), data.path("reason").asText());
});- 7 種事件:
game_room.created、started、member_joined、member_connected、member_disconnected、member_left、closed,欄位見遊戲房間的回撥事件。member_connected和member_disconnected很頻繁,按需訂閱。 - 事件可能重複或亂序到達:處理器按事件 ID 去重,業務上用
data.version(房間版本)判斷新舊。 - 房間自動結束(超時、全員離開)時
result為null;billable_minutes是這一局按人向上取整的分鐘數,賬單按天彙總後取整。
行為邊界
已經提供的:
- App Token 快取在程序內,同時需要新令牌時只換取一次;
- 按介面的類別自動重試,可以設定整次呼叫的總時限,傳送訊息使用冪等鍵(
client_msg_id); - 游標分頁;檔案一次上傳和分片上傳,流式下載;
- 回撥用原始請求體校驗簽名,支援金鑰輪換、防重放、事件去重,同步回撥有時限檢查。
目前還沒有的:
- 請求引數統一用通用的
Params表示“不傳”“傳 null”“傳值”三種狀態,沒有為每個介面單獨定義引數型別; - 沒有多程序共用 App Token 的儲存(TokenStore),沒有內建 Redis,沒有按原因分組的完整限速,分片不併發上傳,下載地址過期後不會自動重新換取;
- 多個例項接收回調時,必須自己實現共享的
CallbackStore; - Spring Boot 2 和 4、WebFlux、GraalVM 原生映象還沒有測試過。
完整的引數、方法和實現範圍見倉庫中的 sdk/server/java/README.md 和共用的 OpenAPI 定義。執行示例見 Java 服務端示例。
