dsh-remote-bridge 0.0.0-stage → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.en.md +322 -0
- package/README.md +318 -2
- package/assets/app-qr-gitee.png +0 -0
- package/assets/app-qr-gitee.svg +1 -0
- package/assets/app-qr-github.png +0 -0
- package/assets/app-qr-github.svg +1 -0
- package/cordis.patch.yml +16 -0
- package/docs/api/asyncapi.yaml +86 -0
- package/docs/api/openapi.yaml +1065 -0
- package/kotlin-sdk/README.md +28 -0
- package/kotlin-sdk/build.gradle.kts +19 -0
- package/kotlin-sdk/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/kotlin-sdk/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/kotlin-sdk/gradlew +252 -0
- package/kotlin-sdk/gradlew.bat +94 -0
- package/kotlin-sdk/settings.gradle.kts +1 -0
- package/kotlin-sdk/src/main/kotlin/ai/deepseek/dsh/workspace/DshWorkspaceClient.kt +691 -0
- package/lib/client.cjs +30855 -0
- package/lib/index.d.ts +1261 -0
- package/lib/index.js +4008 -0
- package/package.json +108 -4
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
package ai.deepseek.dsh.workspace
|
|
2
|
+
|
|
3
|
+
import java.io.Closeable
|
|
4
|
+
import java.util.concurrent.Executors
|
|
5
|
+
import java.util.concurrent.ScheduledFuture
|
|
6
|
+
import java.util.concurrent.TimeUnit
|
|
7
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
8
|
+
import kotlinx.serialization.SerialName
|
|
9
|
+
import kotlinx.serialization.Serializable
|
|
10
|
+
import kotlinx.serialization.json.Json
|
|
11
|
+
import kotlinx.serialization.json.JsonElement
|
|
12
|
+
import okhttp3.HttpUrl
|
|
13
|
+
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
14
|
+
import okhttp3.MediaType.Companion.toMediaType
|
|
15
|
+
import okhttp3.OkHttpClient
|
|
16
|
+
import okhttp3.Request
|
|
17
|
+
import okhttp3.RequestBody.Companion.toRequestBody
|
|
18
|
+
import okhttp3.Response
|
|
19
|
+
import okhttp3.WebSocket
|
|
20
|
+
import okhttp3.WebSocketListener
|
|
21
|
+
|
|
22
|
+
const val API_VERSION: String = "v1"
|
|
23
|
+
|
|
24
|
+
@Serializable
|
|
25
|
+
data class HealthView(val ok: Boolean, val version: String, val pluginVersion: String? = null)
|
|
26
|
+
|
|
27
|
+
@Serializable
|
|
28
|
+
data class RootView(val id: String, val label: String, val createdAt: Long)
|
|
29
|
+
|
|
30
|
+
@Serializable
|
|
31
|
+
data class FileEntry(
|
|
32
|
+
val name: String,
|
|
33
|
+
val path: String,
|
|
34
|
+
val kind: String,
|
|
35
|
+
val size: Long,
|
|
36
|
+
val modifiedAt: Double,
|
|
37
|
+
val writable: Boolean,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
@Serializable
|
|
41
|
+
data class DirectoryPage(val path: String, val entries: List<FileEntry>, val nextCursor: String? = null)
|
|
42
|
+
|
|
43
|
+
@Serializable
|
|
44
|
+
data class DeviceView(
|
|
45
|
+
val id: String,
|
|
46
|
+
val name: String,
|
|
47
|
+
val scopes: List<String>,
|
|
48
|
+
val rootIds: List<String>,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@Serializable
|
|
52
|
+
data class PairingResult(val token: String, val device: DeviceView)
|
|
53
|
+
|
|
54
|
+
@Serializable
|
|
55
|
+
data class TrashEntry(
|
|
56
|
+
val id: String,
|
|
57
|
+
val rootId: String,
|
|
58
|
+
val path: String,
|
|
59
|
+
val kind: String,
|
|
60
|
+
val size: Long,
|
|
61
|
+
val createdAt: Long,
|
|
62
|
+
val status: String,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@Serializable
|
|
66
|
+
data class PendingApproval(
|
|
67
|
+
val id: String,
|
|
68
|
+
val sessionId: String,
|
|
69
|
+
val toolName: String,
|
|
70
|
+
val reason: String? = null,
|
|
71
|
+
val detail: String? = null,
|
|
72
|
+
val risk: String,
|
|
73
|
+
val requestedAt: Long,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@Serializable
|
|
77
|
+
data class CommandInput(val hint: String)
|
|
78
|
+
|
|
79
|
+
@Serializable
|
|
80
|
+
data class CommandDescriptor(
|
|
81
|
+
val name: String,
|
|
82
|
+
val description: String,
|
|
83
|
+
val input: CommandInput? = null,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
@Serializable
|
|
87
|
+
data class CommandResult(
|
|
88
|
+
val kind: String,
|
|
89
|
+
val text: String? = null,
|
|
90
|
+
val sourceEventSeq: Int? = null,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
@Serializable
|
|
94
|
+
data class CommandExecution(val commandId: String, val result: CommandResult)
|
|
95
|
+
|
|
96
|
+
@Serializable
|
|
97
|
+
data class ChatWorkspace(
|
|
98
|
+
val id: String,
|
|
99
|
+
val title: String,
|
|
100
|
+
val rootId: String,
|
|
101
|
+
val path: String,
|
|
102
|
+
val createdAt: String,
|
|
103
|
+
val updatedAt: String,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@Serializable
|
|
107
|
+
data class AgentPreset(
|
|
108
|
+
val id: String,
|
|
109
|
+
val name: String,
|
|
110
|
+
val description: String? = null,
|
|
111
|
+
val trust: String,
|
|
112
|
+
val isDefault: Boolean,
|
|
113
|
+
val available: Boolean,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
@Serializable
|
|
117
|
+
data class ModelSelection(val provider: String, val model: String, val reasoningEffort: String? = null)
|
|
118
|
+
|
|
119
|
+
@Serializable
|
|
120
|
+
data class ModelReasoningEffort(val id: String, val name: String, val description: String? = null)
|
|
121
|
+
|
|
122
|
+
@Serializable
|
|
123
|
+
data class ModelReasoning(
|
|
124
|
+
val efforts: List<ModelReasoningEffort>,
|
|
125
|
+
val defaultEffort: String? = null,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
@Serializable
|
|
129
|
+
data class ModelView(
|
|
130
|
+
val id: String,
|
|
131
|
+
val name: String,
|
|
132
|
+
val description: String? = null,
|
|
133
|
+
val contextWindow: Long? = null,
|
|
134
|
+
val maxTokens: Long? = null,
|
|
135
|
+
val reasoning: ModelReasoning? = null,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
@Serializable
|
|
139
|
+
data class ModelProviderGroup(val id: String, val name: String, val models: List<ModelView>)
|
|
140
|
+
|
|
141
|
+
@Serializable
|
|
142
|
+
data class ModelFailure(val provider: String, val message: String)
|
|
143
|
+
|
|
144
|
+
@Serializable
|
|
145
|
+
data class ModelCatalog(
|
|
146
|
+
val groups: List<ModelProviderGroup>,
|
|
147
|
+
val failures: List<ModelFailure> = emptyList(),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
@Serializable
|
|
151
|
+
data class SessionModels(
|
|
152
|
+
val current: ModelSelection,
|
|
153
|
+
val routable: Boolean,
|
|
154
|
+
val groups: List<ModelProviderGroup>,
|
|
155
|
+
val failures: List<ModelFailure> = emptyList(),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
@Serializable
|
|
159
|
+
data class ProviderModel(
|
|
160
|
+
val id: String,
|
|
161
|
+
val name: String? = null,
|
|
162
|
+
val contextWindow: Long? = null,
|
|
163
|
+
val maxTokens: Long? = null,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
@Serializable
|
|
167
|
+
data class ProviderConfig(
|
|
168
|
+
val baseURL: String? = null,
|
|
169
|
+
val api: String? = null,
|
|
170
|
+
val displayName: String? = null,
|
|
171
|
+
val thinking: String? = null,
|
|
172
|
+
val reasoningEffort: String? = null,
|
|
173
|
+
val models: List<ProviderModel> = emptyList(),
|
|
174
|
+
val modelsInherited: Boolean = false,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
@Serializable
|
|
178
|
+
data class CredentialState(
|
|
179
|
+
val ref: String,
|
|
180
|
+
val configured: Boolean,
|
|
181
|
+
val source: String? = null,
|
|
182
|
+
val writable: Boolean,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
@Serializable
|
|
186
|
+
data class ProviderView(
|
|
187
|
+
val id: String,
|
|
188
|
+
val displayName: String,
|
|
189
|
+
val active: Boolean,
|
|
190
|
+
val declared: Boolean? = null,
|
|
191
|
+
val configurable: Boolean,
|
|
192
|
+
val configured: Boolean,
|
|
193
|
+
val removable: Boolean,
|
|
194
|
+
val credential: CredentialState,
|
|
195
|
+
val config: ProviderConfig,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
@Serializable
|
|
199
|
+
data class ProviderSettings(
|
|
200
|
+
val writable: Boolean,
|
|
201
|
+
val revisionByNamespace: Map<String, Int>,
|
|
202
|
+
val customProvider: CustomProviderCapability,
|
|
203
|
+
val providers: List<ProviderView>,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
@Serializable
|
|
207
|
+
data class CustomProviderCapability(
|
|
208
|
+
val available: Boolean,
|
|
209
|
+
val protocols: List<String>,
|
|
210
|
+
val revision: Int? = null,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
@Serializable
|
|
214
|
+
data class ProviderPatch(
|
|
215
|
+
val displayName: String? = null,
|
|
216
|
+
val baseURL: String? = null,
|
|
217
|
+
val api: String? = null,
|
|
218
|
+
val apiKey: String? = null,
|
|
219
|
+
val thinking: String? = null,
|
|
220
|
+
val reasoningEffort: String? = null,
|
|
221
|
+
val models: List<ProviderModel>? = null,
|
|
222
|
+
val expectedRevision: Int? = null,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
@Serializable
|
|
226
|
+
data class CustomProviderCreate(
|
|
227
|
+
val id: String,
|
|
228
|
+
val displayName: String? = null,
|
|
229
|
+
val baseURL: String,
|
|
230
|
+
val api: String,
|
|
231
|
+
val apiKey: String? = null,
|
|
232
|
+
val models: List<ProviderModel>,
|
|
233
|
+
val expectedRevision: Int? = null,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
@Serializable
|
|
237
|
+
data class WorkspaceEvent(val id: String, val type: String, val time: Long, val data: JsonElement)
|
|
238
|
+
|
|
239
|
+
data class FileContent(val bytes: ByteArray, val etag: String, val contentType: String?)
|
|
240
|
+
|
|
241
|
+
class WorkspaceApiException(
|
|
242
|
+
val status: Int,
|
|
243
|
+
val code: String,
|
|
244
|
+
override val message: String,
|
|
245
|
+
) : RuntimeException(message)
|
|
246
|
+
|
|
247
|
+
class DshWorkspaceClient(
|
|
248
|
+
apiBase: String,
|
|
249
|
+
private val token: String? = null,
|
|
250
|
+
private val http: OkHttpClient = OkHttpClient(),
|
|
251
|
+
) {
|
|
252
|
+
private val json = Json { ignoreUnknownKeys = true }
|
|
253
|
+
private val base: HttpUrl = (apiBase.trimEnd('/') + "/").toHttpUrl()
|
|
254
|
+
|
|
255
|
+
fun health(): HealthView = executeJson("healthz", "GET", null, HealthView.serializer(), authenticated = false)
|
|
256
|
+
|
|
257
|
+
fun exchangePairing(code: String, deviceName: String): PairingResult {
|
|
258
|
+
val body = json.encodeToString(PairingRequest.serializer(), PairingRequest(code, deviceName))
|
|
259
|
+
return executeJson("pairings/exchange", "POST", body, PairingResult.serializer(), authenticated = false)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
fun listRoots(): List<RootView> = executeJson(
|
|
263
|
+
"roots",
|
|
264
|
+
"GET",
|
|
265
|
+
null,
|
|
266
|
+
RootList.serializer(),
|
|
267
|
+
).items
|
|
268
|
+
|
|
269
|
+
fun currentDevice(): DeviceView = executeJson("devices/self", "GET", null, DeviceView.serializer())
|
|
270
|
+
|
|
271
|
+
fun listEntries(rootId: String, path: String = "", cursor: String? = null): DirectoryPage {
|
|
272
|
+
val url = endpoint("roots/$rootId/entries").newBuilder()
|
|
273
|
+
.addQueryParameter("path", path)
|
|
274
|
+
.apply { if (cursor != null) addQueryParameter("cursor", cursor) }
|
|
275
|
+
.build()
|
|
276
|
+
return executeJson(url, "GET", null, DirectoryPage.serializer())
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
fun readFile(rootId: String, path: String, range: LongRange? = null): FileContent {
|
|
280
|
+
val url = endpoint("roots/$rootId/content").newBuilder().addQueryParameter("path", path).build()
|
|
281
|
+
val builder = request(url)
|
|
282
|
+
if (range != null) builder.header("Range", "bytes=${range.first}-${range.last}")
|
|
283
|
+
http.newCall(builder.get().build()).execute().use { response ->
|
|
284
|
+
ensureSuccess(response)
|
|
285
|
+
return FileContent(
|
|
286
|
+
bytes = response.body?.bytes() ?: ByteArray(0),
|
|
287
|
+
etag = response.header("ETag") ?: "",
|
|
288
|
+
contentType = response.header("Content-Type"),
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
fun writeFile(rootId: String, path: String, bytes: ByteArray, etag: String? = null): String {
|
|
294
|
+
val url = endpoint("roots/$rootId/content").newBuilder().addQueryParameter("path", path).build()
|
|
295
|
+
val builder = request(url)
|
|
296
|
+
.put(bytes.toRequestBody("application/octet-stream".toMediaType()))
|
|
297
|
+
if (etag == null) builder.header("If-None-Match", "*") else builder.header("If-Match", etag)
|
|
298
|
+
http.newCall(builder.build()).execute().use { response ->
|
|
299
|
+
ensureSuccess(response)
|
|
300
|
+
return response.header("ETag") ?: ""
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
fun createEntry(rootId: String, path: String, kind: String) {
|
|
305
|
+
val body = json.encodeToString(CreateEntryRequest.serializer(), CreateEntryRequest(path, kind))
|
|
306
|
+
executeUnit("roots/$rootId/entries", "POST", body)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
fun moveEntry(rootId: String, path: String, destinationPath: String) {
|
|
310
|
+
val body = json.encodeToString(MoveEntryRequest.serializer(), MoveEntryRequest(path, destinationPath))
|
|
311
|
+
executeUnit("roots/$rootId/entries", "PATCH", body)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
fun trashEntry(rootId: String, path: String) {
|
|
315
|
+
val url = endpoint("roots/$rootId/entries").newBuilder().addQueryParameter("path", path).build()
|
|
316
|
+
executeUnit(url, "DELETE", null)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
fun listTrash(): List<TrashEntry> = executeJson(
|
|
320
|
+
"trash",
|
|
321
|
+
"GET",
|
|
322
|
+
null,
|
|
323
|
+
TrashList.serializer(),
|
|
324
|
+
).items
|
|
325
|
+
|
|
326
|
+
fun restoreTrash(trashId: String) {
|
|
327
|
+
executeUnit("trash/$trashId/restore", "POST", null)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
fun listSessions(): JsonElement = executeJsonElement("chat/sessions", "GET", null)
|
|
331
|
+
|
|
332
|
+
fun listChatWorkspaces(): List<ChatWorkspace> = executeJson(
|
|
333
|
+
"chat/workspaces",
|
|
334
|
+
"GET",
|
|
335
|
+
null,
|
|
336
|
+
ChatWorkspaceList.serializer(),
|
|
337
|
+
).items
|
|
338
|
+
|
|
339
|
+
fun listAgentPresets(): List<AgentPreset> = executeJson(
|
|
340
|
+
"chat/agent-presets",
|
|
341
|
+
"GET",
|
|
342
|
+
null,
|
|
343
|
+
AgentPresetList.serializer(),
|
|
344
|
+
).items
|
|
345
|
+
|
|
346
|
+
fun selectSessionAgentPreset(sessionId: String, agentPreset: String): String {
|
|
347
|
+
val body = json.encodeToString(AgentPresetSelection.serializer(), AgentPresetSelection(agentPreset))
|
|
348
|
+
return executeJson(
|
|
349
|
+
"chat/sessions/$sessionId/agent-preset",
|
|
350
|
+
"PUT",
|
|
351
|
+
body,
|
|
352
|
+
SelectedAgentPresetEnvelope.serializer(),
|
|
353
|
+
).agentPreset
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
fun listSessionModels(sessionId: String): SessionModels = executeJson(
|
|
357
|
+
"chat/sessions/$sessionId/models",
|
|
358
|
+
"GET",
|
|
359
|
+
null,
|
|
360
|
+
SessionModels.serializer(),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
fun selectSessionModel(sessionId: String, selection: ModelSelection): ModelSelection {
|
|
364
|
+
val body = json.encodeToString(ModelSelection.serializer(), selection)
|
|
365
|
+
return executeJson(
|
|
366
|
+
"chat/sessions/$sessionId/model",
|
|
367
|
+
"PUT",
|
|
368
|
+
body,
|
|
369
|
+
SelectedModelEnvelope.serializer(),
|
|
370
|
+
).selected
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
fun listProviderSettings(): ProviderSettings = executeJson(
|
|
374
|
+
"settings/providers",
|
|
375
|
+
"GET",
|
|
376
|
+
null,
|
|
377
|
+
ProviderSettings.serializer(),
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
fun listModelCatalog(): ModelCatalog = executeJson(
|
|
381
|
+
"settings/models",
|
|
382
|
+
"GET",
|
|
383
|
+
null,
|
|
384
|
+
ModelCatalog.serializer(),
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
fun updateProvider(providerId: String, patch: ProviderPatch): ProviderSettings {
|
|
388
|
+
val body = json.encodeToString(ProviderPatch.serializer(), patch)
|
|
389
|
+
return executeJson("settings/providers/$providerId", "PATCH", body, ProviderSettings.serializer())
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
fun createCustomProvider(provider: CustomProviderCreate): ProviderSettings {
|
|
393
|
+
val body = json.encodeToString(CustomProviderCreate.serializer(), provider)
|
|
394
|
+
return executeJson("settings/providers", "POST", body, ProviderSettings.serializer())
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
fun discoverProviderModels(
|
|
398
|
+
providerId: String,
|
|
399
|
+
baseURL: String? = null,
|
|
400
|
+
api: String? = null,
|
|
401
|
+
apiKey: String? = null,
|
|
402
|
+
): List<ProviderModel> {
|
|
403
|
+
val body = json.encodeToString(
|
|
404
|
+
DiscoverModelsRequest.serializer(),
|
|
405
|
+
DiscoverModelsRequest(baseURL, api, apiKey),
|
|
406
|
+
)
|
|
407
|
+
return executeJson(
|
|
408
|
+
"settings/providers/$providerId/discover",
|
|
409
|
+
"POST",
|
|
410
|
+
body,
|
|
411
|
+
DiscoveredModelsEnvelope.serializer(),
|
|
412
|
+
).models
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
fun createSessionInWorkspace(
|
|
416
|
+
workspaceId: String,
|
|
417
|
+
clientRequestId: String,
|
|
418
|
+
agentPreset: String? = null,
|
|
419
|
+
): JsonElement {
|
|
420
|
+
val body = json.encodeToString(
|
|
421
|
+
WorkspaceSessionRequest.serializer(),
|
|
422
|
+
WorkspaceSessionRequest(workspaceId, agentPreset, clientRequestId),
|
|
423
|
+
)
|
|
424
|
+
return executeJsonElement("chat/sessions", "POST", body)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
fun createChatWorkspace(rootId: String, path: String): ChatWorkspace {
|
|
428
|
+
val body = json.encodeToString(CreateWorkspaceRequest.serializer(), CreateWorkspaceRequest(rootId, path))
|
|
429
|
+
return executeJson(
|
|
430
|
+
"chat/workspaces",
|
|
431
|
+
"POST",
|
|
432
|
+
body,
|
|
433
|
+
CreatedWorkspaceEnvelope.serializer(),
|
|
434
|
+
).workspace
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
fun renameChatWorkspace(workspaceId: String, title: String): ChatWorkspace {
|
|
438
|
+
val body = json.encodeToString(RenameRequest.serializer(), RenameRequest(title))
|
|
439
|
+
return executeJson(
|
|
440
|
+
"chat/workspaces/$workspaceId",
|
|
441
|
+
"PATCH",
|
|
442
|
+
body,
|
|
443
|
+
CreatedWorkspaceEnvelope.serializer(),
|
|
444
|
+
).workspace
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
fun deleteChatWorkspace(workspaceId: String) {
|
|
448
|
+
executeUnit("chat/workspaces/$workspaceId", "DELETE", null)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
fun createSession(rootId: String, path: String, clientRequestId: String): JsonElement {
|
|
452
|
+
val body = json.encodeToString(
|
|
453
|
+
CreateSessionRequest.serializer(),
|
|
454
|
+
CreateSessionRequest(rootId, path, clientRequestId),
|
|
455
|
+
)
|
|
456
|
+
return executeJsonElement("chat/sessions", "POST", body)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
fun listMessages(sessionId: String): JsonElement = executeJsonElement("chat/sessions/$sessionId/messages", "GET", null)
|
|
460
|
+
|
|
461
|
+
fun renameSession(sessionId: String, title: String) {
|
|
462
|
+
val body = json.encodeToString(RenameRequest.serializer(), RenameRequest(title))
|
|
463
|
+
executeUnit("chat/sessions/$sessionId", "PATCH", body)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
fun forkSession(sessionId: String, atSeq: Int? = null): String {
|
|
467
|
+
val body = json.encodeToString(ForkSessionRequest.serializer(), ForkSessionRequest(atSeq))
|
|
468
|
+
return executeJson(
|
|
469
|
+
"chat/sessions/$sessionId/fork",
|
|
470
|
+
"POST",
|
|
471
|
+
body,
|
|
472
|
+
ForkedSessionEnvelope.serializer(),
|
|
473
|
+
).sessionId
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
fun archiveSession(sessionId: String) {
|
|
477
|
+
executeUnit("chat/sessions/$sessionId/archive", "POST", null)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
fun listPendingApprovals(sessionId: String): List<PendingApproval> = executeJson(
|
|
481
|
+
"chat/sessions/$sessionId/approvals",
|
|
482
|
+
"GET",
|
|
483
|
+
null,
|
|
484
|
+
PendingApprovalList.serializer(),
|
|
485
|
+
).items
|
|
486
|
+
|
|
487
|
+
fun decideApproval(sessionId: String, approvalId: String, allowOnce: Boolean) {
|
|
488
|
+
val body = json.encodeToString(
|
|
489
|
+
ApprovalDecision.serializer(),
|
|
490
|
+
ApprovalDecision(if (allowOnce) "allowed-once" else "rejected"),
|
|
491
|
+
)
|
|
492
|
+
executeUnit("chat/sessions/$sessionId/approvals/$approvalId/decision", "POST", body)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
fun listSessionCommands(sessionId: String): List<CommandDescriptor> = executeJson(
|
|
496
|
+
"chat/sessions/$sessionId/commands",
|
|
497
|
+
"GET",
|
|
498
|
+
null,
|
|
499
|
+
CommandDescriptorList.serializer(),
|
|
500
|
+
).items
|
|
501
|
+
|
|
502
|
+
fun executeSessionCommand(sessionId: String, line: String): CommandExecution {
|
|
503
|
+
val body = json.encodeToString(CommandRequest.serializer(), CommandRequest(line))
|
|
504
|
+
return executeJson(
|
|
505
|
+
"chat/sessions/$sessionId/commands",
|
|
506
|
+
"POST",
|
|
507
|
+
body,
|
|
508
|
+
CommandExecutionEnvelope.serializer(),
|
|
509
|
+
).execution
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
fun sendMessage(sessionId: String, text: String, clientRequestId: String, steer: Boolean = false): JsonElement {
|
|
513
|
+
val body = json.encodeToString(
|
|
514
|
+
SendMessageRequest.serializer(),
|
|
515
|
+
SendMessageRequest(text, if (steer) "steer" else "queue", clientRequestId),
|
|
516
|
+
)
|
|
517
|
+
return executeJsonElement("chat/sessions/$sessionId/messages", "POST", body)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
fun cancel(sessionId: String) {
|
|
521
|
+
executeUnit("chat/runs/$sessionId/cancel", "POST", null)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
fun events(listener: WorkspaceEventListener): EventSubscription {
|
|
525
|
+
require(token != null) { "A paired device token is required for events." }
|
|
526
|
+
return EventSubscription(http, webSocketUrl(), token, json, listener)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
private fun webSocketUrl(): HttpUrl = base.newBuilder().addPathSegment("events").build()
|
|
530
|
+
|
|
531
|
+
private fun endpoint(relative: String): HttpUrl = base.resolve(relative)
|
|
532
|
+
?: throw IllegalArgumentException("Invalid API path: $relative")
|
|
533
|
+
|
|
534
|
+
private fun request(url: HttpUrl, authenticated: Boolean = true): Request.Builder {
|
|
535
|
+
val builder = Request.Builder().url(url).header("Accept", "application/json")
|
|
536
|
+
if (authenticated) builder.header("Authorization", "Bearer ${requireNotNull(token) { "A device token is required." }}")
|
|
537
|
+
return builder
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
private fun <T> executeJson(
|
|
541
|
+
relative: String,
|
|
542
|
+
method: String,
|
|
543
|
+
body: String?,
|
|
544
|
+
serializer: kotlinx.serialization.KSerializer<T>,
|
|
545
|
+
authenticated: Boolean = true,
|
|
546
|
+
): T = executeJson(endpoint(relative), method, body, serializer, authenticated)
|
|
547
|
+
|
|
548
|
+
private fun <T> executeJson(
|
|
549
|
+
url: HttpUrl,
|
|
550
|
+
method: String,
|
|
551
|
+
body: String?,
|
|
552
|
+
serializer: kotlinx.serialization.KSerializer<T>,
|
|
553
|
+
authenticated: Boolean = true,
|
|
554
|
+
): T {
|
|
555
|
+
val response = execute(url, method, body, authenticated)
|
|
556
|
+
response.use {
|
|
557
|
+
ensureSuccess(it)
|
|
558
|
+
return json.decodeFromString(serializer, it.body?.string() ?: "{}")
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
private fun executeJsonElement(relative: String, method: String, body: String?): JsonElement {
|
|
563
|
+
val response = execute(endpoint(relative), method, body, true)
|
|
564
|
+
response.use {
|
|
565
|
+
ensureSuccess(it)
|
|
566
|
+
return json.parseToJsonElement(it.body?.string() ?: "{}")
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
private fun executeUnit(relative: String, method: String, body: String?) = executeUnit(endpoint(relative), method, body)
|
|
571
|
+
|
|
572
|
+
private fun executeUnit(url: HttpUrl, method: String, body: String?) {
|
|
573
|
+
execute(url, method, body, true).use { ensureSuccess(it) }
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private fun execute(url: HttpUrl, method: String, body: String?, authenticated: Boolean): Response {
|
|
577
|
+
val requestBody = body?.toRequestBody("application/json".toMediaType())
|
|
578
|
+
return http.newCall(request(url, authenticated).method(method, requestBody).build()).execute()
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private fun ensureSuccess(response: Response) {
|
|
582
|
+
if (response.isSuccessful) return
|
|
583
|
+
val raw = response.body?.string().orEmpty()
|
|
584
|
+
val parsed = runCatching { json.decodeFromString(ErrorEnvelope.serializer(), raw) }.getOrNull()
|
|
585
|
+
throw WorkspaceApiException(
|
|
586
|
+
response.code,
|
|
587
|
+
parsed?.error?.code ?: "HTTP_ERROR",
|
|
588
|
+
parsed?.error?.message ?: "HTTP ${response.code}",
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
interface WorkspaceEventListener {
|
|
594
|
+
fun onEvent(event: WorkspaceEvent)
|
|
595
|
+
fun onConnectionChanged(connected: Boolean) {}
|
|
596
|
+
fun onPermanentFailure(error: Throwable) {}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
class EventSubscription internal constructor(
|
|
600
|
+
private val http: OkHttpClient,
|
|
601
|
+
private val url: HttpUrl,
|
|
602
|
+
private val token: String,
|
|
603
|
+
private val json: Json,
|
|
604
|
+
private val listener: WorkspaceEventListener,
|
|
605
|
+
) : Closeable {
|
|
606
|
+
private val closed = AtomicBoolean(false)
|
|
607
|
+
private val scheduler = Executors.newSingleThreadScheduledExecutor()
|
|
608
|
+
private var socket: WebSocket? = null
|
|
609
|
+
private var retry: ScheduledFuture<*>? = null
|
|
610
|
+
private var attempts = 0
|
|
611
|
+
|
|
612
|
+
init { connect() }
|
|
613
|
+
|
|
614
|
+
private fun connect() {
|
|
615
|
+
if (closed.get()) return
|
|
616
|
+
val request = Request.Builder().url(url).header("Authorization", "Bearer $token").build()
|
|
617
|
+
socket = http.newWebSocket(request, object : WebSocketListener() {
|
|
618
|
+
override fun onOpen(webSocket: WebSocket, response: Response) {
|
|
619
|
+
attempts = 0
|
|
620
|
+
listener.onConnectionChanged(true)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
override fun onMessage(webSocket: WebSocket, text: String) {
|
|
624
|
+
runCatching { json.decodeFromString(WorkspaceEvent.serializer(), text) }
|
|
625
|
+
.onSuccess(listener::onEvent)
|
|
626
|
+
.onFailure(listener::onPermanentFailure)
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
|
630
|
+
listener.onConnectionChanged(false)
|
|
631
|
+
if (code == 4001) listener.onPermanentFailure(IllegalStateException("Device token was revoked."))
|
|
632
|
+
else reconnect()
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
override fun onFailure(webSocket: WebSocket, error: Throwable, response: Response?) {
|
|
636
|
+
listener.onConnectionChanged(false)
|
|
637
|
+
if (response?.code == 401 || response?.code == 403) listener.onPermanentFailure(error) else reconnect()
|
|
638
|
+
}
|
|
639
|
+
})
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
private fun reconnect() {
|
|
643
|
+
if (closed.get()) return
|
|
644
|
+
val delaySeconds = minOf(30L, 1L shl minOf(attempts++, 5))
|
|
645
|
+
retry?.cancel(false)
|
|
646
|
+
retry = scheduler.schedule(::connect, delaySeconds, TimeUnit.SECONDS)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
override fun close() {
|
|
650
|
+
if (!closed.compareAndSet(false, true)) return
|
|
651
|
+
retry?.cancel(false)
|
|
652
|
+
socket?.close(1000, "Client closed")
|
|
653
|
+
scheduler.shutdownNow()
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
@Serializable private data class RootList(val items: List<RootView>)
|
|
658
|
+
@Serializable private data class TrashList(val items: List<TrashEntry>)
|
|
659
|
+
@Serializable private data class PendingApprovalList(val items: List<PendingApproval>)
|
|
660
|
+
@Serializable private data class ApprovalDecision(val outcome: String)
|
|
661
|
+
@Serializable private data class CommandDescriptorList(val items: List<CommandDescriptor>)
|
|
662
|
+
@Serializable private data class CommandRequest(val line: String)
|
|
663
|
+
@Serializable private data class CommandExecutionEnvelope(val execution: CommandExecution)
|
|
664
|
+
@Serializable private data class ChatWorkspaceList(val items: List<ChatWorkspace>)
|
|
665
|
+
@Serializable private data class CreatedWorkspaceEnvelope(val workspace: ChatWorkspace)
|
|
666
|
+
@Serializable private data class RenameRequest(val title: String)
|
|
667
|
+
@Serializable private data class ForkSessionRequest(val atSeq: Int? = null)
|
|
668
|
+
@Serializable private data class ForkedSessionEnvelope(val sessionId: String)
|
|
669
|
+
@Serializable private data class AgentPresetList(val items: List<AgentPreset>)
|
|
670
|
+
@Serializable private data class AgentPresetSelection(val agentPreset: String)
|
|
671
|
+
@Serializable private data class SelectedAgentPresetEnvelope(val agentPreset: String)
|
|
672
|
+
@Serializable private data class SelectedModelEnvelope(val selected: ModelSelection)
|
|
673
|
+
@Serializable private data class DiscoveredModelsEnvelope(val models: List<ProviderModel>)
|
|
674
|
+
@Serializable private data class DiscoverModelsRequest(
|
|
675
|
+
val baseURL: String? = null,
|
|
676
|
+
val api: String? = null,
|
|
677
|
+
val apiKey: String? = null,
|
|
678
|
+
)
|
|
679
|
+
@Serializable private data class PairingRequest(val code: String, val deviceName: String)
|
|
680
|
+
@Serializable private data class CreateEntryRequest(val path: String, val kind: String)
|
|
681
|
+
@Serializable private data class MoveEntryRequest(val path: String, val destinationPath: String)
|
|
682
|
+
@Serializable private data class CreateWorkspaceRequest(val rootId: String, val path: String)
|
|
683
|
+
@Serializable private data class CreateSessionRequest(val rootId: String, val path: String, val clientRequestId: String)
|
|
684
|
+
@Serializable private data class WorkspaceSessionRequest(
|
|
685
|
+
val workspaceId: String,
|
|
686
|
+
val agentPreset: String? = null,
|
|
687
|
+
val clientRequestId: String,
|
|
688
|
+
)
|
|
689
|
+
@Serializable private data class SendMessageRequest(val text: String, val mode: String, val clientRequestId: String)
|
|
690
|
+
@Serializable private data class ErrorEnvelope(val error: ApiErrorBody)
|
|
691
|
+
@Serializable private data class ApiErrorBody(val code: String, val message: String, val requestId: String)
|