package com.bricks.mp.actions import com.bricks.mp.core.* import com.bricks.mp.dev.DevLogStore import com.bricks.mp.dev.DevLogLevel import com.bricks.mp.dev.DevLogSource import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.contentOrNull /** * 事件分发器 - 处理 actiontype: urlwidget/method/script/registerfunction/event * 支持回调机制以更新 UI 状态。 * * HTTP 错误处理保持通用: * - 403:加载 /rbac/user/login.ui 并通过 onDialog 弹出; * - 401:通过 onMessage 显示服务端错误内容; * - 3xx:读取 Location 并按页面跳转加载目标 UI。 */ class ActionDispatcher( private val context: BricksContext, private val http: BricksHttp, private val scope: CoroutineScope ) { private val registeredFunctions = mutableMapOf Unit>() // 回调: 当加载新的 widget 时调用 var onWidgetLoaded: ((BricksWidget) -> Unit)? = null // 回调: 显示消息/错误 var onMessage: ((title: String, message: String, isError: Boolean) -> Unit)? = null // 回调: 弹出/关闭对话框 var onDialog: ((widget: BricksWidget?, show: Boolean) -> Unit)? = null /** * 403 时默认加载的登录 UI。应用可以改成自己的登录页路径。 */ var loginUiPath: String = "/rbac/user/login.ui" /** * 注册回调函数 (registerfunction) */ fun registerFunction(name: String, func: () -> Unit) { registeredFunctions[name] = func } /** * 分发事件 */ fun dispatch(bind: BricksBind) { DevLogStore.log( level = DevLogLevel.INFO, message = "Dispatch: actiontype=${bind.actiontype} event=${bind.event} target=${bind.target}", source = DevLogSource.ACTION ) try { when (bind.actiontype) { "urlwidget" -> handleUrlWidget(bind) "method" -> handleMethod(bind) "script" -> handleScript(bind) "registerfunction" -> handleRegisterFunction(bind) "event" -> handleEvent(bind) else -> { DevLogStore.log( level = DevLogLevel.WARN, message = "Unknown actiontype: ${bind.actiontype}", source = DevLogSource.ACTION ) println("[Bricks] Unknown actiontype: ${bind.actiontype}") } } } catch (e: Exception) { DevLogStore.log( level = DevLogLevel.ERROR, message = "Action dispatch failed: ${bind.actiontype}", details = "event=${bind.event}, target=${bind.target}", source = DevLogSource.ACTION, stackTrace = e.stackTraceToString() ) } } private fun handleUrlWidget(bind: BricksBind) { val url = bind.options["url"]?.let { if (it is JsonPrimitive) it.contentOrNull else null } ?: bind.url ?: return DevLogStore.log( level = DevLogLevel.INFO, message = "Loading URL widget: $url", source = DevLogSource.ACTION ) scope.launch { try { loadWidget(url, showAsDialog = false) } catch (e: Exception) { DevLogStore.log( level = DevLogLevel.ERROR, message = "Failed to load URL widget: $url", details = e.message, source = DevLogSource.ACTION, stackTrace = e.stackTraceToString() ) } } } private suspend fun loadWidget(url: String, showAsDialog: Boolean, redirectDepth: Int = 0) { val fullUrl = resolveUrl(url) try { val widget = http.fetchUi(fullUrl, authToken = context.authToken) if (showAsDialog) { onDialog?.invoke(widget, true) } else { onWidgetLoaded?.invoke(widget) } println("[Bricks] urlwidget loaded: $fullUrl") } catch (e: BricksHttpException) { handleHttpException(e, fullUrl, showAsDialog, redirectDepth) } catch (e: Exception) { println("[Bricks] urlwidget error: ${e.message}") onMessage?.invoke("Error", "Failed to load: ${e.message}", true) } } private suspend fun handleHttpException( error: BricksHttpException, requestUrl: String, showAsDialog: Boolean, redirectDepth: Int ) { when (error.statusCode) { 403 -> showLoginDialog(error) 401 -> onMessage?.invoke("Unauthorized", error.displayBody(), true) in 300..399 -> { val location = error.location if (location.isNullOrBlank()) { onMessage?.invoke("Redirect", "HTTP ${error.statusCode} without Location", true) return } if (redirectDepth >= MAX_REDIRECTS) { onMessage?.invoke("Redirect", "Too many redirects: $location", true) return } val nextUrl = resolveRedirectUrl(requestUrl, location) println("[Bricks] redirect ${error.statusCode}: $requestUrl -> $nextUrl") loadWidget(nextUrl, showAsDialog, redirectDepth + 1) } else -> onMessage?.invoke("HTTP ${error.statusCode}", error.displayBody(), true) } } private suspend fun showLoginDialog(error: BricksHttpException) { try { val loginWidget = http.fetchUi(resolveUrl(loginUiPath), authToken = context.authToken) onDialog?.invoke(loginWidget, true) println("[Bricks] HTTP 403: login dialog loaded from $loginUiPath") } catch (loginError: Exception) { val message = "${error.displayBody()}\nLogin UI load failed: ${loginError.message}" onMessage?.invoke("Forbidden", message, true) } } private fun BricksHttpException.displayBody(): String = responseBody.take(1000).ifBlank { message ?: "HTTP $statusCode" } private fun resolveUrl(url: String): String { if (url.startsWith("http://") || url.startsWith("https://")) return url return context.entireUrl(url) } private fun resolveRedirectUrl(requestUrl: String, location: String): String { if (location.startsWith("http://") || location.startsWith("https://")) return location val origin = requestUrl.originPart() return if (location.startsWith("/")) { origin + location } else { val parentPath = requestUrl.pathPart().substringBeforeLast('/', missingDelimiterValue = "") "$origin$parentPath/$location" } } private fun String.originPart(): String { val schemeEnd = indexOf("://") if (schemeEnd < 0) return "" val authorityStart = schemeEnd + 3 val authorityEnd = indexOf('/', startIndex = authorityStart).let { if (it < 0) length else it } return substring(0, authorityEnd) } private fun String.pathPart(): String { val schemeEnd = indexOf("://") val pathStart = if (schemeEnd >= 0) { indexOf('/', startIndex = schemeEnd + 3).let { if (it < 0) return "" else it } } else { 0 } return substring(pathStart).substringBefore('?').substringBefore('#') } private fun handleMethod(bind: BricksBind) { val methodName = bind.methodname ?: bind.target ?: return // 调用已注册的方法 registeredFunctions[methodName]?.invoke() println("[Bricks] method called: $methodName") } private fun handleScript(bind: BricksBind) { val script = bind.script ?: return println("[Bricks] script: $script") // 解析常见的 script 模式 when { script.contains("this.destroy()") -> { onDialog?.invoke(null, false) } script.contains("show_windows_panel") -> { println("[Bricks] show_windows_panel triggered") } else -> { // 执行已注册的 script handler registeredFunctions[script]?.invoke() } } } private fun handleRegisterFunction(bind: BricksBind) { val name = bind.target ?: return registeredFunctions[name]?.invoke() println("[Bricks] registerfunction invoked: $name") } private fun handleEvent(bind: BricksBind) { when (bind.event) { "submit" -> { // Form submit - trigger urlwidget if configured dispatch(bind.copy(actiontype = "urlwidget")) } "click" -> { // Click event val name = bind.target ?: return registeredFunctions[name]?.invoke() } "dismissed" -> { onDialog?.invoke(null, false) } else -> { println("[Bricks] event: ${bind.event} -> ${bind.actiontype}") } } } fun close() { registeredFunctions.clear() } private companion object { const val MAX_REDIRECTS = 5 } }