Skip to content

Commit 96a9f00

Browse files
committed
feat: WS protocol enhancements, debug config refactor, integration tests
- OpenClawGatewayWsClient: device identity, richer WS protocol support - ChatSendParams/ConnectParams/SessionsSendParams: refactored builders - OpenClawDebugConfig: String-based log levels, new fields - OpenClawHttpClientConfig: new debug/stream config fields - ResponseRequest/ThinkOption/ThinkingLevel: enhanced models - New integration tests for HTTP and WS real endpoints
1 parent 2951549 commit 96a9f00

21 files changed

Lines changed: 1464 additions & 369 deletions

‎README.md‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,21 @@ Configuration is object-based (no Spring properties in this library). Three conf
195195
| `gatewayBaseUrl` | String | `http://localhost:18789` | Gateway base URL |
196196
| `gatewayAuthToken` | String | — | Control-plane token |
197197
| `gatewayAuthPassword` | String | — | Control-plane password mode |
198+
| `gatewayAuthBootstrapToken` | String | — | One-time device bootstrap token; requires `gatewayDeviceIdentity` |
199+
| `gatewayAuthDeviceToken` | String | — | Paired-device token; requires `gatewayDeviceIdentity` |
200+
| `gatewayApprovalRuntimeToken` | String | — | Local trusted approval-runtime token |
201+
| `gatewayAgentRuntimeIdentityToken` | String | — | Local backend Agent Runtime identity token |
202+
| `gatewayRole` | String | `operator` | WS role: `operator` or `node` |
203+
| `gatewayScopes` | List<String> | `operator.read/write` | WS handshake scopes; system-provenance fields require explicit `operator.admin`, and Gateway applies the final policy |
204+
| `gatewayClientId/displayName/version/platform/mode` | String | SDK defaults | Gateway client identity fields |
205+
| `gatewayClientDeviceFamily/modelIdentifier/instanceId` | String | — | Optional presence, audit, and device-signature metadata |
206+
| `gatewayCapabilities` | List<String> | empty | Declared Gateway capabilities |
207+
| `gatewayCommands` | List<String> | — | Commands exposed by a node client |
208+
| `gatewayPermissions` | Map<String, Boolean> | — | Host permission snapshot reported by a node client |
209+
| `gatewayPathEnv` | String | — | PATH snapshot reported by a node client |
210+
| `gatewayLocale` | String | JVM locale | Locale sent in the connect payload |
211+
| `gatewayUserAgent` | String | SDK version | User-Agent sent in the connect payload |
212+
| `gatewayDeviceIdentity` | `OpenClawGatewayDeviceIdentity` | — | Signs each server challenge dynamically with the device Ed25519 private key |
198213
| `hooksToken` | String | — | Webhook auth token |
199214
| `hooksPath` | String | `/hooks` | Webhook base path |
200215
| `hooksUseXOpenclawTokenHeader` | boolean | `false` | Send the hook token via `x-openclaw-token` header |
@@ -227,6 +242,8 @@ Custom request headers via `OpenClawHeaders.Builder`:
227242

228243
Auth priority: webhook (`/hooks/*`) uses `hooksToken`; control plane (`/v1/*`, `/tools/*`, WebSocket) uses `gatewayAuthToken` → `gatewayAuthPassword` → `hooksToken` → none.
229244

245+
Device authentication is not a static DTO-only field. When `gatewayDeviceIdentity` is configured, the SDK builds the OpenClaw v3 payload from the current challenge nonce, role, scopes, token and client metadata, then invokes `sign(payload)`. `gatewayAuthBootstrapToken` and `gatewayAuthDeviceToken` are rejected before the handshake when no device identity is configured. Runtime tokens are accepted by OpenClaw only for their trusted local backend scenarios; configuring an arbitrary value does not grant those privileges.
246+
230247
<a id="8-core-usage"></a>
231248
## 8. Core Usage
232249

@@ -263,6 +280,23 @@ client.chatSend("Hello", new ChatStreamHandler() {
263280
});
264281
```
265282

283+
Use native `ChatSendParams` when a turn must override OpenClaw execution parameters:
284+
285+
```java
286+
client.ws().chatSend(ChatSendParams.builder()
287+
.sessionKey("agent:ops:conversation-42")
288+
.agentId("ops")
289+
.message("Analyze this turn")
290+
.thinkingLevel(ThinkingLevel.MINIMAL)
291+
.fastMode("auto")
292+
.fastAutoOnSeconds(10)
293+
.suppressCommandInterpretation(true)
294+
.idempotencyKey("turn-42")
295+
.build(), handler);
296+
```
297+
298+
`ChatSendParams` matches the complete OpenClaw `2026.7.1-2` `chat.send` schema: session/agent selection, per-turn thinking, `fastMode`, auto-fast cutoff, delivery origin, attachments, timeout, system provenance, command-interpretation suppression, routing contract and idempotency. HTTP Chat Completions continues to emit only the OpenAI fields OpenClaw explicitly supports. `ResponseRequest` separately models the accepted-but-currently-ignored compatibility fields `max_tool_calls`, `reasoning`, `metadata`, `store`, and `truncation`.
299+
266300
### 8.3 Local CLI
267301

268302
```java

‎README.zh-CN.md‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,21 @@ client.close();
194194
| `gatewayBaseUrl` | String | `http://localhost:18789` | Gateway 根地址 |
195195
| `gatewayAuthToken` | String | — | 控制面令牌 |
196196
| `gatewayAuthPassword` | String | — | 控制面密码模式 |
197+
| `gatewayAuthBootstrapToken` | String | — | 一次性设备引导令牌;必须配置 `gatewayDeviceIdentity` |
198+
| `gatewayAuthDeviceToken` | String | — | 已配对设备令牌;必须配置 `gatewayDeviceIdentity` |
199+
| `gatewayApprovalRuntimeToken` | String | — | 本地受信任审批运行时令牌 |
200+
| `gatewayAgentRuntimeIdentityToken` | String | — | 本地 backend Agent Runtime 身份令牌 |
201+
| `gatewayRole` | String | `operator` | WS 连接角色:`operator` 或 `node` |
202+
| `gatewayScopes` | List<String> | `operator.read/write` | WS 握手请求权限;系统来源字段需显式加入 `operator.admin`,最终权限由 Gateway 策略裁剪 |
203+
| `gatewayClientId/displayName/version/platform/mode` | String | SDK 默认值 | Gateway 客户端身份字段 |
204+
| `gatewayClientDeviceFamily/modelIdentifier/instanceId` | String | — | 可选 presence、审计和设备签名元数据 |
205+
| `gatewayCapabilities` | List<String> | 空列表 | 客户端声明的 Gateway 能力 |
206+
| `gatewayCommands` | List<String> | — | node 客户端向 Gateway 暴露的命令 |
207+
| `gatewayPermissions` | Map<String, Boolean> | — | node 客户端上报的宿主权限快照 |
208+
| `gatewayPathEnv` | String | — | node 客户端上报的 PATH 快照 |
209+
| `gatewayLocale` | String | JVM 默认区域 | connect 握手上报的区域设置 |
210+
| `gatewayUserAgent` | String | SDK 版本 | connect 握手上报的 User-Agent |
211+
| `gatewayDeviceIdentity` | `OpenClawGatewayDeviceIdentity` | — | 使用设备 Ed25519 私钥为每次服务端 challenge 动态签名 |
197212
| `hooksToken` | String | — | Webhook 鉴权令牌 |
198213
| `hooksPath` | String | `/hooks` | Webhook 基础路径 |
199214
| `hooksUseXOpenclawTokenHeader` | boolean | `false` | 用 `x-openclaw-token` 头传 Hook 令牌 |
@@ -226,6 +241,8 @@ client.close();
226241

227242
认证优先级:Webhook(`/hooks/*`)使用 `hooksToken`;控制面(`/v1/*`、`/tools/*`、WebSocket)为 `gatewayAuthToken` → `gatewayAuthPassword` → `hooksToken` → 空。
228243

244+
设备认证不是只做 DTO 字段映射。配置 `gatewayDeviceIdentity` 后,SDK 会使用本次 challenge 的 nonce、角色、权限范围、认证令牌和客户端元数据构造 OpenClaw v3 载荷,再调用 `sign(payload)`。配置 `gatewayAuthBootstrapToken` 或 `gatewayAuthDeviceToken` 却没有设备身份时,SDK 会在握手前拒绝请求。运行时令牌只在 OpenClaw 定义的本地受信任 backend 场景中生效,任意填写字符串不会获得对应权限。
245+
229246
<a id="8-core-usage"></a>
230247
## 8. 核心用法
231248

@@ -261,6 +278,23 @@ client.chatSend("你好", new ChatStreamHandler() {
261278
});
262279
```
263280

281+
需要逐轮覆盖 OpenClaw 执行参数时,使用原生 `ChatSendParams`:
282+
283+
```java
284+
client.ws().chatSend(ChatSendParams.builder()
285+
.sessionKey("agent:ops:conversation-42")
286+
.agentId("ops")
287+
.message("分析这一轮问题")
288+
.thinkingLevel(ThinkingLevel.MINIMAL)
289+
.fastMode("auto")
290+
.fastAutoOnSeconds(10)
291+
.suppressCommandInterpretation(true)
292+
.idempotencyKey("turn-42")
293+
.build(), handler);
294+
```
295+
296+
`ChatSendParams` 对齐 OpenClaw `2026.7.1-2` 的完整 `chat.send` Schema,包括会话/Agent、逐轮 thinking、`fastMode`、自动快速窗口、投递来源、附件、超时、系统来源证明、指令解释抑制、路由契约与幂等键。HTTP Chat Completions 继续只发送 OpenClaw 明确支持的 OpenAI 参数;`ResponseRequest` 另行建模了 OpenClaw 接受但当前忽略的 `max_tool_calls`、`reasoning`、`metadata`、`store`、`truncation`,便于兼容上层客户端。
297+
264298
### 8.3 本地 CLI
265299

266300
```java

‎pom.xml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,7 @@
472472
<junit-jupiter.version>5.11.4</junit-jupiter.version>
473473
<lombok.version>1.18.46</lombok.version>
474474
<okhttp3.version>4.12.0</okhttp3.version>
475+
<okhttp3-extension.version>1.0.x.20260730</okhttp3-extension.version>
475476
<slf4j.version>2.0.18</slf4j.version>
476477
<!-- Maven Plugin versions -->
477478
<maven-central-publishing-plugin.version>0.11.0</maven-central-publishing-plugin.version>
@@ -495,6 +496,11 @@
495496
<!-- 依赖版本统一管理(dependencyManagement) -->
496497
<dependencyManagement>
497498
<dependencies>
499+
<dependency>
500+
<groupId>io.github.easy4j</groupId>
501+
<artifactId>okhttp3-extension</artifactId>
502+
<version>${okhttp3-extension.version}</version>
503+
</dependency>
498504
<!-- For OkHttp -->
499505
<dependency>
500506
<groupId>com.squareup.okhttp3</groupId>
@@ -565,6 +571,10 @@
565571

566572
<!-- 项目依赖(dependencies) -->
567573
<dependencies>
574+
<dependency>
575+
<groupId>io.github.easy4j</groupId>
576+
<artifactId>okhttp3-extension</artifactId>
577+
</dependency>
568578
<!-- For OkHttp -->
569579
<dependency>
570580
<groupId>com.squareup.okhttp3</groupId>
Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
package io.github.easy4j.openclaw;
22

33
import lombok.Data;
4+
import okhttp3.extension.logging.HttpLogLevel;
45

56
/**
67
* OpenClaw SDK 统一调试配置,用于控制生命周期、请求头和正文日志。
78
*
89
* <p>调试默认关闭。正文日志始终受长度限制,认证头和敏感令牌仍由客户端脱敏。</p>
910
*
10-
* <p>{@link #level} 字段使用字符串表示日志级别。可选值:
11+
* <p>{@link #level} 使用 {@link HttpLogLevel} 表示日志级别。可选值:
1112
* <ul>
1213
* <li>{@code "NONE"} — 不输出</li>
1314
* <li>{@code "BASIC"} — 仅方法/路径/状态/耗时</li>
1415
* <li>{@code "HEADERS"} — BASIC + 请求/响应头</li>
1516
* <li>{@code "BODY"} — HEADERS + 请求/响应正文(受 {@link #maxContentLength} 限制)</li>
1617
* </ul>
17-
* 当 classpath 上有 {@code io.github.easy4j:okhttp3-extension} 时,{@link #level} 会按相同语义传递给底层日志拦截器;否则仅用于 SDK 自身 DEBUG 输出。</p>
18+
* 该类型继续作为 SDK 的公共 API,避免升级 SDK 后破坏既有调用方的源码和二进制兼容性。</p>
1819
*
1920
* @author <a href="https://github.com/loong10k">Loong Wan</a>
2021
* @since 1.0.0
@@ -25,26 +26,47 @@ public class OpenClawDebugConfig {
2526
/** 是否允许 SDK 输出调试诊断信息。 */
2627
private boolean enabled;
2728

28-
/**
29-
* 启用调试后的详细程度;字符串常量,对齐 {@code okhttp3.extension.logging.HttpLogLevel}。
30-
*/
31-
private String level = "BASIC";
29+
/** 启用调试后的详细程度。 */
30+
private HttpLogLevel level = HttpLogLevel.BASIC;
3231

3332
/** BODY 级别单项正文允许记录的最大字符数。 */
3433
private int maxContentLength = 2_000;
3534

3635
/**
3736
* 判断指定级别的日志是否允许输出。
38-
* <p>简单的字符串排序:{@code NONE &lt; BASIC &lt; HEADERS &lt; BODY}。</p>
37+
* @param required 待输出信息要求的最低级别
38+
* @return 调试已启用且当前级别满足要求时返回 {@code true}
39+
*/
40+
public boolean allows(HttpLogLevel required) {
41+
return enabled && level != null && required != null && level.allows(required);
42+
}
43+
44+
/**
45+
* 兼容短期使用字符串级别的调用方。
3946
*
40-
* @param required 待输出信息要求的最低级别(字符串)
47+
* @param required 待输出信息要求的最低级别名称
4148
* @return 调试已启用且当前级别满足要求时返回 {@code true}
4249
*/
4350
public boolean allows(String required) {
44-
if (!enabled || level == null || required == null) {
45-
return false;
46-
}
47-
return rank(level) >= rank(required);
51+
return required != null && allows(resolveLevel(required));
52+
}
53+
54+
/**
55+
* 设置日志级别,保持旧版 SDK 公共 API 的源码与二进制兼容性。
56+
*
57+
* @param level 日志级别
58+
*/
59+
public void setLevel(HttpLogLevel level) {
60+
this.level = level;
61+
}
62+
63+
/**
64+
* 兼容 Spring 配置及短期使用字符串级别的调用方。
65+
*
66+
* @param level 日志级别名称
67+
*/
68+
public void setLevel(String level) {
69+
this.level = resolveLevel(level);
4870
}
4971

5072
/**
@@ -56,13 +78,14 @@ public int resolveMaxContentLength() {
5678
return Math.max(1, maxContentLength);
5779
}
5880

59-
private static int rank(String level) {
60-
switch (level.toUpperCase()) {
61-
case "BODY": return 3;
62-
case "HEADERS": return 2;
63-
case "BASIC": return 1;
64-
case "NONE": return 0;
65-
default: return 1;
81+
private static HttpLogLevel resolveLevel(String level) {
82+
if (level == null) {
83+
return HttpLogLevel.BASIC;
84+
}
85+
try {
86+
return HttpLogLevel.valueOf(level.trim().toUpperCase());
87+
} catch (IllegalArgumentException ex) {
88+
return HttpLogLevel.BASIC;
6689
}
6790
}
68-
}
91+
}

‎src/main/java/io/github/easy4j/openclaw/OpenClawHttpClientConfig.java‎

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
package io.github.easy4j.openclaw;
22

33
import io.github.easy4j.openclaw.util.OpenClawStrings;
4+
import io.github.easy4j.openclaw.ws.OpenClawGatewayDeviceIdentity;
45
import lombok.Data;
6+
import lombok.ToString;
57

8+
import java.util.Arrays;
9+
import java.util.Collections;
10+
import java.util.List;
11+
import java.util.Map;
612
import java.util.Objects;
713

814
/**
@@ -61,13 +67,95 @@ public OpenClawHttpClientConfig(OpenClawDebugConfig debug) {
6167
/**
6268
* Gateway Bearer 认证令牌;解析认证头时优先于密码且日志必须脱敏。
6369
*/
70+
@ToString.Exclude
6471
private String gatewayAuthToken;
6572

6673
/**
6774
* Gateway 密码认证值;仅在未配置 token 时使用且日志必须脱敏。
6875
*/
76+
@ToString.Exclude
6977
private String gatewayAuthPassword;
7078

79+
/**
80+
* 设备首次受控接入使用的一次性引导令牌;必须配合 {@link #gatewayDeviceIdentity} 使用。
81+
*/
82+
@ToString.Exclude
83+
private String gatewayAuthBootstrapToken;
84+
85+
/**
86+
* 已配对设备的认证令牌;必须配合 {@link #gatewayDeviceIdentity} 使用。
87+
*/
88+
@ToString.Exclude
89+
private String gatewayAuthDeviceToken;
90+
91+
/**
92+
* 本地审批运行时令牌,仅用于受信任的 backend gateway-client 和审批权限范围。
93+
*/
94+
@ToString.Exclude
95+
private String gatewayApprovalRuntimeToken;
96+
97+
/**
98+
* 本地 Agent Runtime 身份令牌,仅允许受信任的 backend gateway-client 使用。
99+
*/
100+
@ToString.Exclude
101+
private String gatewayAgentRuntimeIdentityToken;
102+
103+
/** Gateway connect 握手角色;OpenClaw 当前支持 {@code operator} 和 {@code node}。 */
104+
private String gatewayRole = "operator";
105+
106+
/**
107+
* WebSocket connect 握手请求的操作权限范围;Gateway 最终仍会按认证身份和服务端策略裁剪。
108+
*/
109+
private List<String> gatewayScopes = Arrays.asList("operator.read", "operator.write");
110+
111+
/** Gateway 协议识别的客户端 ID。 */
112+
private String gatewayClientId = "gateway-client";
113+
114+
/** Gateway presence 和审计记录中展示的客户端名称。 */
115+
private String gatewayClientDisplayName = "OpenClaw Java SDK";
116+
117+
/** Gateway presence 和兼容性判断使用的客户端版本。 */
118+
private String gatewayClientVersion = "1.0.0";
119+
120+
/** Gateway 记录和设备签名使用的客户端平台。 */
121+
private String gatewayClientPlatform = "java";
122+
123+
/** Gateway 客户端运行模式。 */
124+
private String gatewayClientMode = "backend";
125+
126+
/** 可选设备家族,会参与 v3 设备认证签名。 */
127+
private String gatewayClientDeviceFamily;
128+
129+
/** 可选设备型号标识,用于客户端 presence 元数据。 */
130+
private String gatewayClientModelIdentifier;
131+
132+
/** 可选客户端实例标识,用于区分同一程序的不同运行实例。 */
133+
private String gatewayClientInstanceId;
134+
135+
/** 客户端声明的 Gateway 能力列表。 */
136+
private List<String> gatewayCapabilities = Collections.emptyList();
137+
138+
/** node 类客户端可由 Gateway 调用的命令列表。 */
139+
private List<String> gatewayCommands;
140+
141+
/** 客户端上报的宿主权限快照。 */
142+
private Map<String, Boolean> gatewayPermissions;
143+
144+
/** node 类客户端执行命令时使用的 PATH 环境快照。 */
145+
private String gatewayPathEnv;
146+
147+
/** connect 握手上报的区域设置;为空时使用 JVM 默认区域。 */
148+
private String gatewayLocale;
149+
150+
/** connect 握手上报的 User-Agent;为空时使用 SDK 默认值。 */
151+
private String gatewayUserAgent;
152+
153+
/**
154+
* 可选 Gateway 设备身份签名器;配置后 SDK 会针对每次 challenge 动态生成签名。
155+
*/
156+
@ToString.Exclude
157+
private OpenClawGatewayDeviceIdentity gatewayDeviceIdentity;
158+
71159
/**
72160
* HTTPS/WSS 是否校验证书和主机名;生产环境应保持开启。
73161
*/

‎src/main/java/io/github/easy4j/openclaw/api/OpenClawResponsesClient.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ public CompletableFuture<ResponseResult> createResponseAsync(ResponseRequest req
8484
.topP(request.getTopP())
8585
.user(request.getUser())
8686
.previousResponseId(request.getPreviousResponseId())
87+
.maxToolCalls(request.getMaxToolCalls())
88+
.reasoning(request.getReasoning())
89+
.metadata(request.getMetadata())
90+
.store(request.getStore())
91+
.truncation(request.getTruncation())
8792
.build();
8893

8994
return postJsonAsync(OpenClawConstants.ENDPOINT_RESPONSES, normalized, headers, null)

0 commit comments

Comments
 (0)