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 服务端示例。
