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 = _isLoggedIn private val _loginError = kotlinx.coroutines.flow.MutableStateFlow(null) val loginError: kotlinx.coroutines.flow.StateFlow = _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 = 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 = emptyMap(), method: String = "GET", formBody: String = "", jsonBody: JsonObject? = null ): Result = 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 = 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() } }