Skip to content

Commit c718bc9

Browse files
committed
feat: 新增 sessionKey 模式,对齐 Hermes/OpenClaw 会话路由能力
- OpenCodeSessionKeys: 固定格式 sessionKey 工具(opencode:{agent}:{peer}[:{channel}]),对齐 OpenClawSessionKeys - OpenCodeHttpClient: 新增 ensureSession(sessionKey) + promptByKey/promptAsyncByKey 重载 - OpenCodeClient: 门面暴露 promptByKey/ensureSession,调用方无需手动管理 sessionId sessionKey 同时作为 session title,ensureSession 自动复用/创建 session,对调用方透明。
1 parent 13be7e5 commit c718bc9

3 files changed

Lines changed: 164 additions & 0 deletions

File tree

src/main/java/io/github/hiwepy/opencode/OpenCodeClient.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,51 @@ public PromptResult prompt(String sessionId, String text, String providerID, Str
110110
return httpClient.prompt(sessionId, PromptRequest.ofText(text, providerID, modelID));
111111
}
112112

113+
// ----------------------------------------------------------------
114+
// sessionKey 模式(对齐 Hermes/OpenClaw,自动复用/创建 session)
115+
// ----------------------------------------------------------------
116+
117+
/**
118+
* 按 sessionKey 发送 prompt 并同步等待 AI 响应。
119+
* <p>sessionKey 同时作为 session 的 title,{@code ensureSession} 保证 session 存在。
120+
* 建议用 {@link io.github.hiwepy.opencode.api.OpenCodeSessionKeys} 生成 sessionKey。</p>
121+
*
122+
* @param sessionKey 会话复用 key
123+
* @param request prompt 请求
124+
* @return AI 响应
125+
*/
126+
public PromptResult promptByKey(String sessionKey, PromptRequest request) {
127+
return httpClient.promptByKey(sessionKey, request);
128+
}
129+
130+
/**
131+
* 按 sessionKey 发送纯文本 prompt 并同步等待 AI 响应。
132+
*/
133+
public PromptResult promptByKey(String sessionKey, String text) {
134+
return httpClient.promptByKey(sessionKey, PromptRequest.ofText(text));
135+
}
136+
137+
/**
138+
* 按 sessionKey 发送纯文本 prompt 并指定模型。
139+
*/
140+
public PromptResult promptByKey(String sessionKey, String text, String providerID, String modelID) {
141+
return httpClient.promptByKey(sessionKey, PromptRequest.ofText(text, providerID, modelID));
142+
}
143+
144+
/**
145+
* 按 sessionKey 异步发送 prompt,不等待响应。
146+
*/
147+
public boolean promptAsyncByKey(String sessionKey, PromptRequest request) {
148+
return httpClient.promptAsyncByKey(sessionKey, request);
149+
}
150+
151+
/**
152+
* 确保指定 sessionKey 对应的 session 存在,返回其 sessionId。
153+
*/
154+
public String ensureSession(String sessionKey) {
155+
return httpClient.ensureSession(sessionKey);
156+
}
157+
113158
/**
114159
* 异步发送 prompt,不等待响应。
115160
*
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package io.github.hiwepy.opencode.api;
2+
3+
import java.util.Locale;
4+
import java.util.Objects;
5+
import java.util.regex.Pattern;
6+
7+
/**
8+
* OpenCode 会话 key 命名工具,对齐 {@code OpenClawSessionKeys} 的设计。
9+
*
10+
* <p>OpenCode Server 基于 session 模型,{@code sessionId} 由服务端生成({@code ses} 前缀),
11+
* 无法由客户端指定。本工具用 {@code sessionKey} 作为 session 的 {@code title},
12+
* 配合 {@code OpenCodeClient#prompt(String sessionKey, PromptRequest request)} 实现
13+
* 「按 sessionKey 自动复用/创建 session」的透明调用。</p>
14+
*
15+
* <ul>
16+
* <li><b>固定多轮会话</b>:{@code opencode:{agentId}:{peerId}}</li>
17+
* <li><b>按渠道隔离</b>:{@code opencode:{agentId}:{peerId}:{channel}}</li>
18+
* </ul>
19+
*
20+
* @author wandl
21+
* @since 2.7.x
22+
*/
23+
public final class OpenCodeSessionKeys {
24+
25+
/** 允许字母/数字开头,支持大小写字母、数字、下划线、点、破折号,最长128字节 */
26+
private static final Pattern SAFE_SEGMENT = Pattern.compile("^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$");
27+
28+
private OpenCodeSessionKeys() {
29+
}
30+
31+
/**
32+
* 固定多轮会话 key:{@code opencode:{agentId}:{peerId}}。
33+
* <p>同一 agent + 同一用户复用同一会话上下文。</p>
34+
*
35+
* @param agentId 路由 agent
36+
* @param peerId 业务 peer(如 userId)
37+
*/
38+
public static String forStableSession(String agentId, String peerId) {
39+
return "opencode:" + normalizeSegment(agentId, "agentId") + ":"
40+
+ normalizeSegment(peerId, "peerId");
41+
}
42+
43+
/**
44+
* 固定多轮会话 key(含渠道):{@code opencode:{agentId}:{peerId}:{channel}}。
45+
* <p>同一 agent + 同一用户 + 同一渠道复用同一会话上下文。</p>
46+
*
47+
* @param agentId 路由 agent
48+
* @param peerId 业务 peer(如 userId)
49+
* @param channel 渠道(如 xiaohongshu)
50+
*/
51+
public static String forStableSession(String agentId, String peerId, String channel) {
52+
return "opencode:" + normalizeSegment(agentId, "agentId") + ":"
53+
+ normalizeSegment(peerId, "peerId") + ":"
54+
+ normalizeSegment(channel, "channel");
55+
}
56+
57+
/**
58+
* 规范化 session key 片段:trim、小写,并校验不含 {@code :} 与非法字符。
59+
*/
60+
static String normalizeSegment(String value, String fieldName) {
61+
Objects.requireNonNull(value, fieldName);
62+
String normalized = value.trim().toLowerCase(Locale.ROOT);
63+
if (normalized.isEmpty()) {
64+
throw new IllegalArgumentException(fieldName + " must not be blank");
65+
}
66+
if (normalized.contains(":")) {
67+
throw new IllegalArgumentException(fieldName + " must not contain ':'");
68+
}
69+
if (!SAFE_SEGMENT.matcher(normalized).matches()) {
70+
throw new IllegalArgumentException(fieldName + " contains illegal characters: " + value);
71+
}
72+
return normalized;
73+
}
74+
}

src/main/java/io/github/hiwepy/opencode/http/OpenCodeHttpClient.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,51 @@ public PromptResult prompt(String sessionId, PromptRequest request) {
121121
return post("/session/" + sessionId + "/message", request, PromptResult.class);
122122
}
123123

124+
/**
125+
* 按 sessionKey 发送消息并同步等待 AI 响应。
126+
* <p>sessionKey 作为 session 的 title,{@code ensureSession} 保证 session 存在(不存在则创建),
127+
* 对调用方透明。对齐 Hermes/OpenClaw 的 sessionKey 模式。</p>
128+
*
129+
* @param sessionKey 会话复用 key(建议用 {@code OpenCodeSessionKeys} 生成)
130+
* @param request prompt 请求
131+
* @return AI 响应
132+
*/
133+
public PromptResult promptByKey(String sessionKey, PromptRequest request) {
134+
String sessionId = ensureSession(sessionKey);
135+
return prompt(sessionId, request);
136+
}
137+
138+
/**
139+
* 按 sessionKey 异步发送消息,不等待响应。
140+
*/
141+
public boolean promptAsyncByKey(String sessionKey, PromptRequest request) {
142+
String sessionId = ensureSession(sessionKey);
143+
return promptAsync(sessionId, request);
144+
}
145+
146+
/**
147+
* 确保指定 sessionKey 对应的 session 存在,返回其 sessionId。
148+
* <p>先按 title 精确查找现有 session,不存在则创建。对齐 Hermes/OpenClaw 的 sessionKey 语义。</p>
149+
*
150+
* @param sessionKey 会话复用 key(同时作为 session title)
151+
* @return sessionId
152+
*/
153+
public String ensureSession(String sessionKey) {
154+
// 先按 title 查找现有 session
155+
try {
156+
Optional<Session> existing = findSessionByTitle(sessionKey);
157+
if (existing.isPresent()) {
158+
return existing.get().getId();
159+
}
160+
} catch (Exception e) {
161+
log.debug("findSessionByTitle failed, will create new session, sessionKey={}, error={}",
162+
sessionKey, e.getMessage());
163+
}
164+
// 不存在则创建
165+
Session session = createSession(sessionKey);
166+
return session.getId();
167+
}
168+
124169
/**
125170
* 异步发送消息,不等待响应(POST /session/:id/prompt_async)。
126171
*/

0 commit comments

Comments
 (0)