Skip to content

Commit c120645

Browse files
committed
feat: cherry-pick 2.7.x workspace routing into feature/2.0.x
Adopt 2.7.x API additions for OpenCodeRequestContext: - OpenCodeClient: pass-through RequestContext for workspace routing - OpenCodeHttpClient: ensureSession/promptAsync accept RequestContext - OpenCodeSseClient: subscribe accepts RequestContext
1 parent 2348843 commit c120645

3 files changed

Lines changed: 73 additions & 13 deletions

File tree

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io.github.hiwepy.opencode.cli.OpenCodeCli;
77
import io.github.hiwepy.opencode.cli.OpenCodeCliExecutor;
88
import io.github.hiwepy.opencode.api.OpenCodeHttpClient;
9+
import io.github.hiwepy.opencode.api.OpenCodeRequestContext;
910
import io.github.hiwepy.opencode.api.OpenCodeSseClient;
1011
import okhttp3.OkHttpClient;
1112

@@ -242,13 +243,26 @@ public ChatResponse chatCompletionWithSession(ChatRequest request, String sessio
242243
* @return 流式响应(CompletableFuture,完成时携带完整文本)
243244
*/
244245
public ChatStreamingResponse chatCompletionStream(ChatRequest request, String sessionKey) {
245-
String sessionId = httpClient.ensureSession(sessionKey);
246+
return chatCompletionStream(request, sessionKey, null);
247+
}
248+
249+
/**
250+
* 在指定 OpenCode 工作目录中按 sessionKey 流式发送消息。
251+
*
252+
* @param request 请求
253+
* @param sessionKey 稳定会话键
254+
* @param context 包含受控工作目录的请求上下文
255+
* @return 流式响应
256+
*/
257+
public ChatStreamingResponse chatCompletionStream(ChatRequest request, String sessionKey,
258+
OpenCodeRequestContext context) {
259+
String sessionId = httpClient.ensureSession(sessionKey, context);
246260
PromptRequest promptRequest = ChatMessageMapper.toPromptRequest(request);
247261

248262
ChatStreamingResponse stream = new ChatStreamingResponse();
249263

250264
// 订阅全局 SSE,按 sessionId 过滤事件
251-
java.util.concurrent.BlockingQueue<Event> queue = sseClient.subscribeQueue();
265+
java.util.concurrent.BlockingQueue<Event> queue = sseClient.subscribeQueue(context);
252266

253267
// 异步消费事件
254268
java.util.concurrent.CompletableFuture.runAsync(() -> {
@@ -310,7 +324,7 @@ public ChatStreamingResponse chatCompletionStream(ChatRequest request, String se
310324
});
311325

312326
// 触发异步 prompt(不阻塞,SSE 事件驱动结果)
313-
httpClient.promptAsync(sessionId, promptRequest);
327+
httpClient.promptAsync(sessionId, promptRequest, context);
314328

315329
return stream;
316330
}

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

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
public class OpenCodeHttpClient implements AutoCloseable {
3131

3232
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
33+
private static final String HEADER_OPENCODE_DIRECTORY = "X-OpenCode-Directory";
3334

3435
private final OpenCodeHttpClientConfig config;
3536
private final OkHttpClient httpClient;
@@ -66,8 +67,12 @@ public HealthStatus health() {
6667
// ============================================================
6768

6869
public Session createSession(String title) {
70+
return createSession(title, null);
71+
}
72+
73+
public Session createSession(String title, OpenCodeRequestContext context) {
6974
Map<String, Object> body = title != null ? Collections.singletonMap("title", title) : Collections.emptyMap();
70-
return post("/session", body, Session.class);
75+
return post("/session", body, Session.class, context);
7176
}
7277

7378
public Session getSession(String sessionId) {
@@ -87,22 +92,31 @@ public List<Session> listSessions() {
8792
* @return 匹配的 session 列表
8893
*/
8994
public List<Session> listSessions(String search, Integer limit, Integer start) {
95+
return listSessions(search, limit, start, null);
96+
}
97+
98+
public List<Session> listSessions(String search, Integer limit, Integer start,
99+
OpenCodeRequestContext context) {
90100
HttpUrl.Builder urlBuilder = HttpUrl.get(url("/session")).newBuilder();
91101
if (search != null) urlBuilder.addQueryParameter("search", search);
92102
if (limit != null) urlBuilder.addQueryParameter("limit", String.valueOf(limit));
93103
if (start != null) urlBuilder.addQueryParameter("start", String.valueOf(start));
94-
Request request = authedRequest(urlBuilder.build().toString()).get().build();
104+
Request request = authedRequest(urlBuilder.build().toString(), context).get().build();
95105
return executeList(request, new TypeReference<List<Session>>() {});
96106
}
97107

98108
/**
99109
* 按 title 精确查找 session。
100110
*/
101111
public Optional<Session> findSessionByTitle(String title) {
112+
return findSessionByTitle(title, null);
113+
}
114+
115+
public Optional<Session> findSessionByTitle(String title, OpenCodeRequestContext context) {
102116
if (title == null || title.isEmpty()) {
103117
return Optional.empty();
104118
}
105-
return listSessions(title, 50, null).stream()
119+
return listSessions(title, 50, null, context).stream()
106120
.filter(s -> Objects.equals(title, s.getTitle()))
107121
.findFirst();
108122
}
@@ -136,22 +150,30 @@ public boolean chatCompletionWithSessionAsync(PromptRequest request, String sess
136150
}
137151

138152
public String ensureSession(String sessionKey) {
153+
return ensureSession(sessionKey, null);
154+
}
155+
156+
public String ensureSession(String sessionKey, OpenCodeRequestContext context) {
139157
try {
140-
Optional<Session> existing = findSessionByTitle(sessionKey);
158+
Optional<Session> existing = findSessionByTitle(sessionKey, context);
141159
if (existing.isPresent()) {
142160
return existing.get().getId();
143161
}
144162
} catch (Exception e) {
145163
log.debug("findSessionByTitle failed, sessionKey={}, error={}", sessionKey, e.getMessage());
146164
}
147-
Session session = createSession(sessionKey);
165+
Session session = createSession(sessionKey, context);
148166
return session.getId();
149167
}
150168

151169
public boolean promptAsync(String sessionId, PromptRequest request) {
170+
return promptAsync(sessionId, request, null);
171+
}
172+
173+
public boolean promptAsync(String sessionId, PromptRequest request, OpenCodeRequestContext context) {
152174
try {
153175
RequestBody body = RequestBody.create(objectMapper.writeValueAsBytes(request), JSON);
154-
Request httpReq = new Request.Builder().url(url("/session/" + sessionId + "/prompt_async"))
176+
Request httpReq = authedRequest(url("/session/" + sessionId + "/prompt_async"), context)
155177
.post(body).build();
156178
try (Response response = httpClient.newCall(httpReq).execute()) {
157179
return response.isSuccessful();
@@ -206,12 +228,20 @@ private String url(String path) {
206228
}
207229

208230
private Request.Builder authedRequest(String url) {
231+
return authedRequest(url, null);
232+
}
233+
234+
private Request.Builder authedRequest(String url, OpenCodeRequestContext context) {
209235
Request.Builder builder = new Request.Builder().url(url);
210236
String password = config.resolvePassword();
211237
if (!password.isEmpty()) {
212238
String credential = Credentials.basic(config.getUsername(), password);
213239
builder.header("Authorization", credential);
214240
}
241+
if (Objects.nonNull(context) && context.getDirectory() != null
242+
&& !context.getDirectory().trim().isEmpty()) {
243+
builder.header(HEADER_OPENCODE_DIRECTORY, context.getDirectory());
244+
}
215245
return builder;
216246
}
217247

@@ -228,7 +258,11 @@ private <T> T getList(String path, TypeReference<T> typeRef) {
228258

229259

230260
private <T> T post(String path, Object body, Class<T> type) {
231-
Request request = authedRequest(url(path))
261+
return post(path, body, type, null);
262+
}
263+
264+
private <T> T post(String path, Object body, Class<T> type, OpenCodeRequestContext context) {
265+
Request request = authedRequest(url(path), context)
232266
.post(RequestBody.create(toJson(body), JSON))
233267
.build();
234268
return execute(request, type);

src/main/java/io/github/hiwepy/opencode/api/OpenCodeSseClient.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@ private static OkHttpClient buildOkHttpClient(OpenCodeHttpClientConfig config) {
5858
* @return EventSource(可用于 cancel)
5959
*/
6060
public EventSource subscribe(Consumer<Event> consumer) {
61-
Request request = buildRequest();
61+
return subscribe(consumer, null);
62+
}
63+
64+
public EventSource subscribe(Consumer<Event> consumer, OpenCodeRequestContext context) {
65+
Request request = buildRequest(context);
6266
EventSourceListener listener = new EventSourceListener() {
6367
@Override
6468
public void onOpen(EventSource es, Response response) {
@@ -99,12 +103,16 @@ public void onFailure(EventSource es, Throwable t, Response response) {
99103
* 阻塞式订阅,返回一个 BlockingQueue,事件入队供外部消费。
100104
*/
101105
public BlockingQueue<Event> subscribeQueue() {
106+
return subscribeQueue(null);
107+
}
108+
109+
public BlockingQueue<Event> subscribeQueue(OpenCodeRequestContext context) {
102110
BlockingQueue<Event> queue = new LinkedBlockingQueue<>();
103-
subscribe(queue::add);
111+
subscribe(queue::add, context);
104112
return queue;
105113
}
106114

107-
private Request buildRequest() {
115+
private Request buildRequest(OpenCodeRequestContext context) {
108116
String url = config.getServerUrl() + "/event";
109117
Request.Builder builder = new Request.Builder().url(url)
110118
.header("Accept", "text/event-stream")
@@ -113,6 +121,10 @@ private Request buildRequest() {
113121
if (!password.isEmpty()) {
114122
builder.header("Authorization", Credentials.basic(config.getUsername(), password));
115123
}
124+
if (Objects.nonNull(context) && context.getDirectory() != null
125+
&& !context.getDirectory().trim().isEmpty()) {
126+
builder.header("X-OpenCode-Directory", context.getDirectory());
127+
}
116128
return builder.build();
117129
}
118130

0 commit comments

Comments
 (0)