fix: track Android data package (was hidden by root .gitignore 'data/')

This commit is contained in:
OpenClaw Dev 2026-10-25 04:29:42 +01:00
parent 399bcd46fb
commit 310243840d
4 changed files with 197 additions and 3 deletions

6
.gitignore vendored
View file

@ -1,6 +1,6 @@
data/
bin/
dist/
/data/
/bin/
/dist/
*.db
config.yaml
.env

View file

@ -0,0 +1,63 @@
package dev.byteclaw.client.data
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Session(
val id: String,
val title: String = "",
@SerialName("updated_at") val updatedAt: String = "",
)
@Serializable
data class ToolCall(val id: String = "", val name: String = "", val arguments: String = "")
@Serializable
data class Message(
val id: Long = 0,
val role: String,
val content: String = "",
@SerialName("tool_calls") val toolCalls: List<ToolCall> = emptyList(),
val name: String = "",
)
/** Event streamed by the server over the WebSocket. */
@Serializable
data class AgentEvent(
val type: String,
val text: String = "",
val tool: String = "",
val args: String = "",
@SerialName("call_id") val callId: String = "",
@SerialName("session_id") val sessionId: String = "",
)
@Serializable
data class OutgoingMessage(
val type: String = "message",
@SerialName("session_id") val sessionId: String = "",
val text: String,
)
/** GET /api/info: which LLM provider preset/model the server uses (no secrets). */
@Serializable
data class ServerInfo(
val version: String = "",
val provider: String = "",
val model: String = "",
@SerialName("base_url") val baseUrl: String = "",
)
/** Provider presets known to the server (configured server-side via llm.provider). */
object ProviderPresets {
val names = mapOf(
"openai" to "OpenAI",
"openrouter" to "OpenRouter",
"ollama" to "Ollama",
"dahl" to "Dahl (dahl.global)",
"gonkarouter" to "GonkaRouter",
"custom" to "Eigener Endpunkt",
)
fun label(name: String) = names[name] ?: name
}

View file

@ -0,0 +1,96 @@
package dev.byteclaw.client.data
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import java.io.IOException
import java.util.concurrent.TimeUnit
class ApiException(msg: String) : IOException(msg)
/** REST + WebSocket client for the ByteClaw Go backend. TLS verification uses system defaults. */
class ByteClawApi(private val settings: ServerSettings) {
private val json = Json { ignoreUnknownKeys = true; explicitNulls = false }
private val http = shared.newBuilder()
.addInterceptor { chain ->
chain.proceed(chain.request().newBuilder().header("Authorization", "Bearer ${settings.token}").build())
}.build()
private fun url(path: String) = (settings.serverUrl.trimEnd('/') + path).toHttpUrl()
private suspend fun call(req: Request): String = withContext(Dispatchers.IO) {
http.newCall(req).execute().use { r: Response ->
val body = r.body?.string().orEmpty()
if (!r.isSuccessful) throw ApiException("HTTP ${r.code}: ${body.take(200)}")
body
}
}
suspend fun health(): Boolean = withContext(Dispatchers.IO) {
http.newCall(Request.Builder().url(url("/healthz")).build()).execute().use { it.isSuccessful }
}
suspend fun sessions(): List<Session> = json.decodeFromString(call(Request.Builder().url(url("/api/sessions")).build()))
suspend fun messages(sessionId: String): List<Message> =
json.decodeFromString(call(Request.Builder().url(url("/api/sessions/$sessionId/messages")).build()))
suspend fun deleteSession(sessionId: String) {
call(Request.Builder().url(url("/api/sessions/$sessionId")).delete().build())
}
/**
* Opens a WebSocket, sends one message and emits streamed events until "done"/"error".
*/
fun stream(sessionId: String?, text: String): Flow<AgentEvent> = callbackFlow {
val wsUrl = url("/api/ws")
val ws = http.newWebSocket(Request.Builder().url(wsUrl).build(), object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
webSocket.send(json.encodeToString(OutgoingMessage.serializer(), OutgoingMessage(sessionId = sessionId.orEmpty(), text = text)))
}
override fun onMessage(webSocket: WebSocket, text: String) {
val ev = runCatching { json.decodeFromString(AgentEvent.serializer(), text) }.getOrNull() ?: return
trySend(ev)
if (ev.type == "done" || ev.type == "error") {
webSocket.close(1000, null)
close()
}
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
val msg = response?.let { "HTTP ${it.code}" } ?: (t.message ?: t.javaClass.simpleName)
trySend(AgentEvent(type = "error", text = msg))
close()
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { close() }
})
awaitClose { ws.cancel() }
}
companion object {
private val shared = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.SECONDS) // streaming responses can be long
.pingInterval(20, TimeUnit.SECONDS)
.build()
/** Only http(s) URLs are accepted; OkHttp maps them to ws(s) for WebSockets. */
fun validateUrl(raw: String): String? {
val u = raw.trim()
if (!(u.startsWith("https://") || u.startsWith("http://"))) return "URL muss mit https:// oder http:// beginnen"
return runCatching { u.toHttpUrl(); null }.getOrElse { "Ungültige URL" }
}
}
}

View file

@ -0,0 +1,35 @@
package dev.byteclaw.client.data
import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
data class ServerSettings(val serverUrl: String = "", val token: String = "") {
val isConfigured get() = serverUrl.isNotBlank() && token.isNotBlank()
}
/** Stores server URL and token encrypted with an Android Keystore-backed key. */
class SettingsStore(context: Context) {
private val prefs: SharedPreferences = EncryptedSharedPreferences.create(
context,
"byteclaw_secure_prefs",
MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)
fun load() = ServerSettings(
serverUrl = prefs.getString(KEY_URL, "") ?: "",
token = prefs.getString(KEY_TOKEN, "") ?: "",
)
fun save(s: ServerSettings) {
prefs.edit().putString(KEY_URL, s.serverUrl.trim().trimEnd('/')).putString(KEY_TOKEN, s.token.trim()).apply()
}
private companion object {
const val KEY_URL = "server_url"
const val KEY_TOKEN = "token"
}
}