210 lines
7.0 KiB
Kotlin
210 lines
7.0 KiB
Kotlin
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()
|
||
}
|
||
}
|