210 lines
7.0 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.bricks.mp.sage
import com.bricks.mp.core.BricksParser
import com.bricks.mp.core.BricksWidget
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.cookies.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.json.*
/**
* Sage 客户端 - 处理登录、Session 管理和 UI 加载
*
* Sage 使用 Cookie Session 认证:
* 1. POST /rbac/user/userpassword_login.dspy 登录获取 session cookie
* 2. 后续请求自动携带 cookie
* 3. GET /xxx.ui 获取 JSON 格式的 UI 描述
*/
class SageClient {
companion object {
const val DEFAULT_BASE_URL = "https://ai.atvoe.com"
}
private val cookieStorage = AcceptAllCookiesStorage()
private val client = HttpClient(CIO) {
install(HttpCookies) {
storage = cookieStorage
}
expectSuccess = false
followRedirects = true
}
var baseUrl: String = DEFAULT_BASE_URL
// 登录状态
private val _isLoggedIn = kotlinx.coroutines.flow.MutableStateFlow(false)
val isLoggedIn: kotlinx.coroutines.flow.StateFlow<Boolean> = _isLoggedIn
private val _loginError = kotlinx.coroutines.flow.MutableStateFlow<String?>(null)
val loginError: kotlinx.coroutines.flow.StateFlow<String?> = _loginError
private val mutex = Mutex()
/**
* 登录 Sage 服务器
* POST /rbac/user/userpassword_login.dspy
*/
suspend fun login(username: String, password: String): Boolean = mutex.withLock {
_loginError.value = null
try {
val url = "$baseUrl/rbac/user/userpassword_login.dspy"
val formBody = "username=${encodeURLParameter(username)}&passwd=${encodeURLParameter(password)}"
val response = client.post(url) {
contentType(ContentType.Application.FormUrlEncoded)
setBody(formBody)
}
val body = response.bodyAsText()
println("[Sage] Login response status: ${response.status}")
println("[Sage] Login response body: ${body.take(200)}")
// Sage 返回 UiMessage 格式: {"widgettype": "Message", "options": {...}}
// 或者返回错误: {"widgettype": "Error", "options": {...}}
val json = try {
Json.parseToJsonElement(body).jsonObject
} catch (e: Exception) {
_loginError.value = "登录响应格式错误: ${e.message}"
return@withLock false
}
val widgetType = json["widgettype"]?.jsonPrimitive?.content
if (widgetType == "Message" || widgetType == "UiMessage") {
// 检查是否有 session cookie
val cookies = cookieStorage.get(URLBuilder(baseUrl).build())
if (cookies.isNotEmpty()) {
println("[Sage] Login successful, got ${cookies.size} cookies")
_isLoggedIn.value = true
true
} else {
// 即使没有 cookie如果服务器返回成功也算登录成功
// 有些部署可能使用 token 而非 cookie
println("[Sage] Login successful (no cookies)")
_isLoggedIn.value = true
true
}
} else {
// 错误消息
val options = json["options"]?.jsonObject ?: JsonObject(emptyMap())
val message = options["message"]?.jsonPrimitive?.content
?: options["text"]?.jsonPrimitive?.content
?: body.take(100)
_loginError.value = message
println("[Sage] Login failed: $message")
false
}
} catch (e: Exception) {
_loginError.value = "登录失败: ${e.message}"
println("[Sage] Login error: ${e.message}")
false
}
}
/**
* 获取 .ui 文件JSON 格式的 UI 描述)
*/
suspend fun fetchUi(path: String): Result<BricksWidget> = mutex.withLock {
try {
val url = if (path.startsWith("http")) path else "$baseUrl/${path.trimStart('/')}"
println("[Sage] Fetching UI: $url")
val response = client.get(url)
val body = response.bodyAsText()
if (!response.status.isSuccess()) {
return@withLock Result.failure(Exception("HTTP ${response.status.value}: ${body.take(200)}"))
}
val widget = BricksParser.parse(body)
Result.success(widget)
} catch (e: Exception) {
println("[Sage] Fetch UI error: ${e.message}")
Result.failure(e)
}
}
/**
* 获取 .dspy API 返回的 JSON
*/
suspend fun fetchApi(
path: String,
params: Map<String, String> = emptyMap(),
method: String = "GET",
formBody: String = "",
jsonBody: JsonObject? = null
): Result<JsonObject> = mutex.withLock {
try {
val url = if (path.startsWith("http")) path else "$baseUrl/${path.trimStart('/')}"
val response = when (method.uppercase()) {
"POST" -> client.post(url) {
url {
params.forEach { (k, v) -> parameters.append(k, v) }
}
when {
jsonBody != null -> {
contentType(ContentType.Application.Json)
setBody(jsonBody)
}
formBody.isNotEmpty() -> {
contentType(ContentType.Application.FormUrlEncoded)
setBody(formBody)
}
}
}
else -> client.get(url) {
url {
params.forEach { (k, v) -> parameters.append(k, v) }
}
}
}
val body = response.bodyAsText()
val json = Json.parseToJsonElement(body).jsonObject
Result.success(json)
} catch (e: Exception) {
Result.failure(e)
}
}
/**
* 获取文本内容
*/
suspend fun fetchText(path: String): Result<String> = mutex.withLock {
try {
val url = if (path.startsWith("http")) path else "$baseUrl/${path.trimStart('/')}"
val response = client.get(url)
Result.success(response.bodyAsText())
} catch (e: Exception) {
Result.failure(e)
}
}
/**
* 退出登录
*/
suspend fun logout() = mutex.withLock {
try {
_isLoggedIn.value = false
println("[Sage] Logged out")
} catch (e: Exception) {
println("[Sage] Logout error: ${e.message}")
}
}
/**
* 关闭客户端
*/
fun close() {
client.close()
}
}