徐勤民 7 hodín pred
rodič
commit
0314acc18e
77 zmenil súbory, kde vykonal 3804 pridanie a 391 odobranie
  1. 2 0
      .gitignore
  2. 7 2
      AndroidLibs/README.md
  3. 3 0
      AndroidLibs/commonsdk-compose/build.gradle.kts
  4. 9 1
      AndroidLibs/commonsdk-compose/src/main/AndroidManifest.xml
  5. 42 0
      AndroidLibs/commonsdk-compose/src/main/java/com/xuqm/sdk/compose/activity/WebViewPageActivity.kt
  6. 97 0
      AndroidLibs/commonsdk-compose/src/main/java/com/xuqm/sdk/compose/components/accordion/AccordionPanel.kt
  7. 103 0
      AndroidLibs/commonsdk-compose/src/main/java/com/xuqm/sdk/compose/components/image/AdaptiveImage.kt
  8. 85 0
      AndroidLibs/commonsdk-compose/src/main/java/com/xuqm/sdk/compose/components/refresh/SwipeRefreshContainer.kt
  9. 112 0
      AndroidLibs/commonsdk-compose/src/main/java/com/xuqm/sdk/compose/components/swipe/SwipeActionItem.kt
  10. 114 0
      AndroidLibs/commonsdk-compose/src/main/java/com/xuqm/sdk/compose/components/webview/CommonWebView.kt
  11. 1 0
      AndroidLibs/commonsdk-core/build.gradle.kts
  12. 58 5
      AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/CoreSDK.kt
  13. 72 0
      AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/cache/GlobalCache.kt
  14. 56 0
      AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/communication/PluginMessenger.kt
  15. 2 3
      AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/network/RetrofitManager.kt
  16. 81 0
      AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/permission/PermissionManager.kt
  17. 102 0
      AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/ui/DialogCenter.kt
  18. 1 1
      AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/ui/ToastCenter.kt
  19. 2 18
      AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/update/DownloadManager.kt
  20. 34 1
      AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/utils/DateTimeUtils.kt
  21. 109 0
      AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/utils/FileHelper.kt
  22. 49 0
      AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/utils/Logger.kt
  23. 31 0
      AndroidLibs/commonsdk-update/build.gradle.kts
  24. 1 0
      AndroidLibs/commonsdk-update/consumer-rules.pro
  25. 9 27
      AndroidLibs/commonsdk-update/src/main/java/com/xuqm/sdk/plugin/PluginPackageManager.kt
  26. 2 1
      AndroidLibs/commonsdk-update/src/main/java/com/xuqm/sdk/update/AppUpdater.kt
  27. 0 0
      AndroidLibs/commonsdk-update/src/main/java/com/xuqm/sdk/update/VersionComparator.kt
  28. 14 0
      AndroidLibs/commonsdk-update/src/main/java/com/xuqm/sdk/updatekit/UpdateSDK.kt
  29. 6 0
      AndroidLibs/gradle/libs.versions.toml
  30. 0 1
      AndroidLibs/lib-szyx/src/main/java/com/xuqm/szyx/login/SzyxLoginActivity.kt
  31. 2 1
      AndroidLibs/sample-app/build.gradle.kts
  32. 10 10
      AndroidLibs/sample-app/src/main/java/com/xuqm/sample/MainActivity.kt
  33. 1 0
      AndroidLibs/settings.gradle.kts
  34. 35 0
      doc/01-project-overview.md
  35. 37 0
      doc/02-architecture.md
  36. 65 0
      doc/03-frontend.md
  37. 56 0
      doc/04-backend.md
  38. 60 0
      doc/05-infrastructure.md
  39. 55 0
      doc/06-version-management.md
  40. 42 0
      doc/07-development-workflow.md
  41. 54 0
      doc/08-android-sdk.md
  42. 20 0
      doc/README.md
  43. 19 7
      frontend/README.md
  44. 1 0
      frontend/admin-platform/package.json
  45. 1 0
      frontend/ops-platform/package.json
  46. 2 17
      frontend/ops-platform/src/App.vue
  47. 82 8
      frontend/ops-platform/src/api/client.ts
  48. 16 5
      frontend/ops-platform/src/router/index.ts
  49. 39 0
      frontend/ops-platform/src/styles.css
  50. 297 0
      frontend/ops-platform/src/views/AppManagementView.vue
  51. 72 0
      frontend/ops-platform/src/views/LoginView.vue
  52. 11 0
      frontend/ops-platform/src/views/ModulePlaceholderView.vue
  53. 23 0
      frontend/ops-platform/src/views/OpsShellView.vue
  54. 14 0
      frontend/package.json
  55. 738 0
      frontend/yarn.lock
  56. 28 0
      server/pom.xml
  57. 5 0
      server/version-management-service/pom.xml
  58. 34 6
      server/version-management-service/src/main/java/com/xuqm/versionmanagement/config/DataInitializer.java
  59. 14 2
      server/version-management-service/src/main/java/com/xuqm/versionmanagement/controller/CompatibilityUpdateController.java
  60. 159 10
      server/version-management-service/src/main/java/com/xuqm/versionmanagement/controller/OpsVersionController.java
  61. 16 3
      server/version-management-service/src/main/java/com/xuqm/versionmanagement/controller/PublicAccountController.java
  62. 120 0
      server/version-management-service/src/main/java/com/xuqm/versionmanagement/model/PlatformData.java
  63. 11 0
      server/version-management-service/src/main/java/com/xuqm/versionmanagement/persistence/entity/AccountEntity.java
  64. 11 0
      server/version-management-service/src/main/java/com/xuqm/versionmanagement/persistence/entity/ApplicationEntity.java
  65. 100 0
      server/version-management-service/src/main/java/com/xuqm/versionmanagement/persistence/entity/PluginEntity.java
  66. 32 0
      server/version-management-service/src/main/java/com/xuqm/versionmanagement/persistence/entity/ReleaseEntity.java
  67. 2 0
      server/version-management-service/src/main/java/com/xuqm/versionmanagement/persistence/repository/AccountRepository.java
  68. 10 0
      server/version-management-service/src/main/java/com/xuqm/versionmanagement/persistence/repository/PluginRepository.java
  69. 2 0
      server/version-management-service/src/main/java/com/xuqm/versionmanagement/persistence/repository/ReleaseRepository.java
  70. 13 2
      server/version-management-service/src/main/java/com/xuqm/versionmanagement/service/AccountService.java
  71. 29 2
      server/version-management-service/src/main/java/com/xuqm/versionmanagement/service/PlatformMapper.java
  72. 239 8
      server/version-management-service/src/main/java/com/xuqm/versionmanagement/service/VersionManagementService.java
  73. 23 7
      server/version-management-service/src/main/resources/application.yml
  74. 0 68
      server/version-service/README.md
  75. 0 22
      server/version-service/data/version-config.json
  76. 0 12
      server/version-service/package.json
  77. 0 141
      server/version-service/src/index.js

+ 2 - 0
.gitignore

@@ -1,6 +1,7 @@
 .DS_Store
 .idea/
 .m2/
+.yarn/
 .gradle/
 .kotlin/
 build/
@@ -8,6 +9,7 @@ target/
 node_modules/
 dist/
 coverage/
+.pnp.*
 *.iml
 *.log
 AndroidLibs/.gradle-home/

+ 7 - 2
AndroidLibs/README.md

@@ -4,8 +4,9 @@
 
 ## 模块结构
 
-- `commonsdk-core`: SDK 核心,承载网络、共享缓存、插件管理、App 更新、设备信息与时间工具。
-- `commonsdk-compose`: Compose 扩展组件。
+- `commonsdk-core`: SDK 核心,承载网络、下载、文件、权限、缓存、日志、全局弹窗、设备与时间等基础能力。
+- `commonsdk-compose`: Compose 扩展组件,提供下拉刷新、折叠面板、滑动操作、WebView、图片等业务可复用组件。
+- `commonsdk-update`: 更新 SDK,独立承载 APK 更新、插件更新、版本比较与安装编排。
 - `lib-szyx`: 项目专属 SDK,承载真实登录接口、签名、业务 Header 与会话管理。
 - `sample-app`: 示例宿主应用。
 - `plugins/plugin-ui`: UI 演示插件,可独立运行,也可被宿主拉起。
@@ -48,6 +49,10 @@ nexus.password=your-password
 - `commonsdk-core` 提供:
   - `HttpManager / RetrofitManager`
   - `SharedCacheManager / SharedCacheProvider`
+  - `DownloadManager / FileHelper`
+  - `PermissionManager / GlobalCache / Logger / DialogCenter`
+- `commonsdk-update` 提供:
+  - `UpdateSDK`
   - `PluginPackageManager`
   - `AppUpdater`
 - `lib-szyx` 提供:

+ 3 - 0
AndroidLibs/commonsdk-compose/build.gradle.kts

@@ -32,8 +32,11 @@ dependencies {
     api(project(":commonsdk-core"))
     implementation(libs.androidx.core.ktx)
     implementation(libs.androidx.activity.compose)
+    implementation(libs.androidx.activity.ktx)
+    implementation(libs.androidx.webkit)
     implementation(platform(libs.androidx.compose.bom))
     implementation(libs.bundles.compose)
+    implementation(libs.coil.compose)
 
     debugImplementation(libs.bundles.compose.debug)
 }

+ 9 - 1
AndroidLibs/commonsdk-compose/src/main/AndroidManifest.xml

@@ -1,3 +1,11 @@
 <?xml version="1.0" encoding="utf-8"?>
-<manifest />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
 
+    <application>
+        <activity
+            android:name="com.xuqm.sdk.compose.activity.WebViewPageActivity"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:exported="false" />
+    </application>
+
+</manifest>

+ 42 - 0
AndroidLibs/commonsdk-compose/src/main/java/com/xuqm/sdk/compose/activity/WebViewPageActivity.kt

@@ -0,0 +1,42 @@
+package com.xuqm.sdk.compose.activity
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import com.xuqm.sdk.compose.components.webview.CommonWebView
+import com.xuqm.sdk.compose.components.webview.WebViewConfig
+import androidx.compose.material3.MaterialTheme
+
+class WebViewPageActivity : ComponentActivity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        val url = intent.getStringExtra(EXTRA_URL).orEmpty()
+        val title = intent.getStringExtra(EXTRA_TITLE)
+        setContent {
+            MaterialTheme {
+                CommonWebView(
+                    url = url,
+                    config = WebViewConfig(title = title),
+                )
+            }
+        }
+    }
+
+    companion object {
+        private const val EXTRA_URL = "extra_url"
+        private const val EXTRA_TITLE = "extra_title"
+
+        fun createIntent(
+            context: Context,
+            url: String,
+            title: String? = null,
+        ): Intent {
+            return Intent(context, WebViewPageActivity::class.java).apply {
+                putExtra(EXTRA_URL, url)
+                putExtra(EXTRA_TITLE, title)
+            }
+        }
+    }
+}

+ 97 - 0
AndroidLibs/commonsdk-compose/src/main/java/com/xuqm/sdk/compose/components/accordion/AccordionPanel.kt

@@ -0,0 +1,97 @@
+package com.xuqm.sdk.compose.components.accordion
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.ExpandLess
+import androidx.compose.material.icons.rounded.ExpandMore
+import androidx.compose.material3.Card
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import com.xuqm.sdk.compose.components.refresh.RefreshableLazyColumn
+
+data class AccordionItem<T>(
+    val id: String,
+    val title: String,
+    val data: T,
+    val initiallyExpanded: Boolean = false,
+)
+
+@Composable
+fun AccordionPanel(
+    title: String,
+    modifier: Modifier = Modifier,
+    initiallyExpanded: Boolean = false,
+    headerBackground: Color = MaterialTheme.colorScheme.surfaceVariant,
+    content: @Composable () -> Unit,
+) {
+    var expanded by remember { mutableStateOf(initiallyExpanded) }
+    Card(modifier = modifier.fillMaxWidth()) {
+        Column {
+            Row(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .background(headerBackground)
+                    .clickable { expanded = !expanded }
+                    .padding(horizontal = 16.dp, vertical = 14.dp),
+                horizontalArrangement = Arrangement.SpaceBetween,
+                verticalAlignment = Alignment.CenterVertically,
+            ) {
+                Text(title, style = MaterialTheme.typography.titleMedium)
+                Icon(
+                    imageVector = if (expanded) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore,
+                    contentDescription = null,
+                )
+            }
+            AnimatedVisibility(expanded) {
+                Box(modifier = Modifier.padding(16.dp)) {
+                    content()
+                }
+            }
+        }
+    }
+}
+
+@Composable
+fun <T> RefreshableAccordionList(
+    items: List<AccordionItem<T>>,
+    isRefreshing: Boolean,
+    onRefresh: () -> Unit,
+    modifier: Modifier = Modifier,
+    itemContent: @Composable (AccordionItem<T>) -> Unit,
+) {
+    RefreshableLazyColumn(
+        items = items,
+        isRefreshing = isRefreshing,
+        onRefresh = onRefresh,
+        modifier = modifier,
+        listState = rememberLazyListState(),
+        itemContent = { _, item ->
+            AccordionPanel(
+                title = item.title,
+                initiallyExpanded = item.initiallyExpanded,
+                modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
+            ) {
+                itemContent(item)
+            }
+        },
+    )
+}

+ 103 - 0
AndroidLibs/commonsdk-compose/src/main/java/com/xuqm/sdk/compose/components/image/AdaptiveImage.kt

@@ -0,0 +1,103 @@
+package com.xuqm.sdk.compose.components.image
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import coil.request.ImageRequest
+
+enum class ImageAdaptMode {
+    WIDTH_FIXED,
+    HEIGHT_FIXED,
+    BOTH_FIXED,
+    FILL_WIDTH,
+}
+
+@Composable
+fun AdaptiveImage(
+    model: Any?,
+    modifier: Modifier = Modifier,
+    adaptMode: ImageAdaptMode = ImageAdaptMode.FILL_WIDTH,
+    width: Dp? = null,
+    height: Dp? = null,
+    aspectRatio: Float = 1f,
+    shape: Shape = RoundedCornerShape(16.dp),
+    contentScale: ContentScale = ContentScale.Crop,
+    contentDescription: String? = null,
+) {
+    val imageModifier = when (adaptMode) {
+        ImageAdaptMode.WIDTH_FIXED -> Modifier.width(width ?: 160.dp).aspectRatio(aspectRatio)
+        ImageAdaptMode.HEIGHT_FIXED -> Modifier.height(height ?: 160.dp).aspectRatio(aspectRatio)
+        ImageAdaptMode.BOTH_FIXED -> Modifier.width(width ?: 160.dp).height(height ?: 160.dp)
+        ImageAdaptMode.FILL_WIDTH -> Modifier.fillMaxWidth().aspectRatio(aspectRatio)
+    }
+    AsyncImage(
+        model = ImageRequest.Builder(LocalContext.current).data(model).crossfade(true).build(),
+        contentDescription = contentDescription,
+        modifier = modifier.then(imageModifier).clip(shape),
+        contentScale = contentScale,
+    )
+}
+
+@Composable
+fun CircleImage(
+    model: Any?,
+    modifier: Modifier = Modifier,
+    size: Dp = 72.dp,
+    borderWidth: Dp = 0.dp,
+    borderColor: Color = Color.Transparent,
+    maskColor: Color = Color.Transparent,
+    overlayBrush: Brush? = null,
+    contentDescription: String? = null,
+) {
+    Box(
+        modifier = modifier
+            .width(size)
+            .height(size)
+            .clip(CircleShape)
+            .border(borderWidth, borderColor, CircleShape)
+            .background(borderColor),
+        contentAlignment = Alignment.Center,
+    ) {
+        AsyncImage(
+            model = ImageRequest.Builder(LocalContext.current).data(model).crossfade(true).build(),
+            contentDescription = contentDescription,
+            modifier = Modifier
+                .fillMaxSize()
+                .clip(CircleShape)
+                .background(MaterialTheme.colorScheme.surfaceVariant),
+            contentScale = ContentScale.Crop,
+        )
+        if (maskColor != Color.Transparent || overlayBrush != null) {
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .clip(CircleShape)
+                    .background(overlayBrush ?: Brush.verticalGradient(listOf(maskColor, maskColor))),
+            )
+        }
+    }
+}

+ 85 - 0
AndroidLibs/commonsdk-compose/src/main/java/com/xuqm/sdk/compose/components/refresh/SwipeRefreshContainer.kt

@@ -0,0 +1,85 @@
+package com.xuqm.sdk.compose.components.refresh
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.pulltorefresh.PullToRefreshBox
+import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SwipeRefreshContainer(
+    isRefreshing: Boolean,
+    onRefresh: () -> Unit,
+    modifier: Modifier = Modifier,
+    content: @Composable BoxScope.() -> Unit,
+) {
+    val refreshState = rememberPullToRefreshState()
+    PullToRefreshBox(
+        state = refreshState,
+        isRefreshing = isRefreshing,
+        onRefresh = onRefresh,
+        modifier = modifier.fillMaxSize(),
+        content = content,
+    )
+}
+
+@Composable
+fun <T> RefreshableLazyColumn(
+    items: List<T>,
+    isRefreshing: Boolean,
+    onRefresh: () -> Unit,
+    modifier: Modifier = Modifier,
+    listState: LazyListState = rememberLazyListState(),
+    enableLoadMore: Boolean = false,
+    isLoadingMore: Boolean = false,
+    hasMore: Boolean = true,
+    onLoadMore: () -> Unit = {},
+    emptyContent: (@Composable BoxScope.() -> Unit)? = null,
+    itemContent: @Composable (index: Int, item: T) -> Unit,
+) {
+    val shouldLoadMore by remember(items, enableLoadMore, isLoadingMore, hasMore, listState) {
+        derivedStateOf {
+            if (!enableLoadMore || isLoadingMore || !hasMore) return@derivedStateOf false
+            val layoutInfo = listState.layoutInfo
+            val lastVisible = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
+            lastVisible >= items.lastIndex - 1 && items.isNotEmpty()
+        }
+    }
+
+    LaunchedEffect(shouldLoadMore) {
+        if (shouldLoadMore) {
+            onLoadMore()
+        }
+    }
+
+    SwipeRefreshContainer(
+        isRefreshing = isRefreshing,
+        onRefresh = onRefresh,
+        modifier = modifier,
+    ) {
+        if (items.isEmpty() && emptyContent != null) {
+            Box(modifier = Modifier.fillMaxSize(), content = emptyContent)
+        } else {
+            LazyColumn(
+                modifier = Modifier.fillMaxSize(),
+                state = listState,
+            ) {
+                itemsIndexed(items) { index, item ->
+                    itemContent(index, item)
+                }
+            }
+        }
+    }
+}

+ 112 - 0
AndroidLibs/commonsdk-compose/src/main/java/com/xuqm/sdk/compose/components/swipe/SwipeActionItem.kt

@@ -0,0 +1,112 @@
+package com.xuqm.sdk.compose.components.swipe
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.gestures.detectHorizontalDragGestures
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.launch
+import kotlin.math.roundToInt
+
+data class SwipeAction(
+    val label: String,
+    val backgroundColor: Color,
+    val contentColor: Color = Color.White,
+    val onClick: () -> Unit,
+    val content: (@Composable () -> Unit)? = null,
+)
+
+@Composable
+fun SwipeActionItem(
+    modifier: Modifier = Modifier,
+    actions: List<SwipeAction>,
+    actionWidthDp: Int = 88,
+    content: @Composable () -> Unit,
+) {
+    val scope = rememberCoroutineScope()
+    val density = LocalDensity.current
+    var offsetX by remember { mutableFloatStateOf(0f) }
+    val maxOffset = with(density) { -(actions.size * actionWidthDp.dp.toPx()) }
+
+    Box(
+        modifier = modifier
+            .fillMaxWidth()
+            .height(IntrinsicSize.Min),
+    ) {
+        Row(
+            modifier = Modifier
+                .align(Alignment.CenterEnd)
+                .fillMaxHeight(),
+            horizontalArrangement = Arrangement.End,
+        ) {
+            actions.forEach { action ->
+                Box(
+                    modifier = Modifier
+                        .width(actionWidthDp.dp)
+                        .fillMaxHeight()
+                        .background(action.backgroundColor)
+                        .clickable {
+                            scope.launch { offsetX = 0f }
+                            action.onClick()
+                        },
+                    contentAlignment = Alignment.Center,
+                ) {
+                    if (action.content != null) {
+                        action.content.invoke()
+                    } else {
+                        Text(
+                            text = action.label,
+                            color = action.contentColor,
+                            style = MaterialTheme.typography.labelLarge,
+                            modifier = Modifier.padding(8.dp),
+                        )
+                    }
+                }
+            }
+        }
+        Box(
+            modifier = Modifier
+                .fillMaxWidth()
+                .offset { IntOffset(offsetX.roundToInt(), 0) }
+                .pointerInput(actions) {
+                    detectHorizontalDragGestures(
+                        onHorizontalDrag = { _, dragAmount ->
+                            offsetX = (offsetX + dragAmount).coerceIn(maxOffset, 0f)
+                        },
+                        onDragEnd = {
+                            scope.launch {
+                                offsetX = if (offsetX < maxOffset / 2) maxOffset else 0f
+                            }
+                        },
+                    )
+                }
+                .background(MaterialTheme.colorScheme.surface, RoundedCornerShape(16.dp)),
+        ) {
+            content()
+        }
+    }
+}

+ 114 - 0
AndroidLibs/commonsdk-compose/src/main/java/com/xuqm/sdk/compose/components/webview/CommonWebView.kt

@@ -0,0 +1,114 @@
+package com.xuqm.sdk.compose.components.webview
+
+import android.annotation.SuppressLint
+import android.graphics.Bitmap
+import android.webkit.WebChromeClient
+import android.webkit.WebResourceRequest
+import android.webkit.WebSettings
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.viewinterop.AndroidView
+
+data class WebViewConfig(
+    val title: String? = null,
+    val javaScriptEnabled: Boolean = true,
+    val domStorageEnabled: Boolean = true,
+    val userAgentString: String? = null,
+)
+
+interface WebViewCallback {
+    fun onPageStarted(url: String?) {}
+    fun onPageFinished(url: String?) {}
+    fun onProgressChanged(progress: Int) {}
+    fun onReceivedTitle(title: String?) {}
+    fun shouldOverrideUrlLoading(url: String?): Boolean = false
+}
+
+@SuppressLint("SetJavaScriptEnabled")
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CommonWebView(
+    url: String,
+    modifier: Modifier = Modifier,
+    config: WebViewConfig = WebViewConfig(),
+    callback: WebViewCallback? = null,
+    onClose: (() -> Unit)? = null,
+) {
+    var webView: WebView? by remember { mutableStateOf(null) }
+    var title by remember { mutableStateOf(config.title ?: "") }
+    var progress by remember { mutableIntStateOf(0) }
+
+    BackHandler(enabled = webView?.canGoBack() == true) {
+        webView?.goBack()
+    }
+
+    DisposableEffect(Unit) {
+        onDispose { webView?.destroy() }
+    }
+
+    Scaffold(
+        topBar = {
+            TopAppBar(
+                title = { Text(if (title.isBlank()) url else title) },
+            )
+        },
+    ) { padding ->
+        AndroidView(
+            factory = { context ->
+                WebView(context).apply {
+                    settings.javaScriptEnabled = config.javaScriptEnabled
+                    settings.domStorageEnabled = config.domStorageEnabled
+                    settings.cacheMode = WebSettings.LOAD_DEFAULT
+                    config.userAgentString?.let { settings.userAgentString = it }
+                    webViewClient = object : WebViewClient() {
+                        override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
+                            callback?.onPageStarted(url)
+                        }
+
+                        override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
+                            return callback?.shouldOverrideUrlLoading(request?.url?.toString()) ?: false
+                        }
+
+                        override fun onPageFinished(view: WebView?, url: String?) {
+                            callback?.onPageFinished(url)
+                        }
+                    }
+                    webChromeClient = object : WebChromeClient() {
+                        override fun onReceivedTitle(view: WebView?, newTitle: String?) {
+                            title = newTitle ?: config.title.orEmpty()
+                            callback?.onReceivedTitle(newTitle)
+                        }
+
+                        override fun onProgressChanged(view: WebView?, newProgress: Int) {
+                            progress = newProgress
+                            callback?.onProgressChanged(newProgress)
+                        }
+                    }
+                    loadUrl(url)
+                    webView = this
+                }
+            },
+            modifier = modifier
+                .fillMaxSize(),
+            update = { view ->
+                if (view.url != url) {
+                    view.loadUrl(url)
+                }
+            },
+        )
+    }
+}

+ 1 - 0
AndroidLibs/commonsdk-core/build.gradle.kts

@@ -25,6 +25,7 @@ kotlin {
 
 dependencies {
     api(libs.androidx.core.ktx)
+    api(libs.androidx.activity.ktx)
     api(libs.bundles.network)
     api(libs.kotlinx.serialization.json)
     implementation(libs.kotlinx.coroutines.android)

+ 58 - 5
AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/CoreSDK.kt

@@ -1,17 +1,24 @@
 package com.xuqm.sdk
 
+import android.app.Activity
 import android.app.Application
 import android.content.Context
+import android.os.Bundle
+import com.xuqm.sdk.cache.GlobalCache
 import com.xuqm.sdk.network.HttpConfig
 import com.xuqm.sdk.network.HttpManager
-import com.xuqm.sdk.plugin.PluginPackageManager
-import com.xuqm.sdk.update.AppUpdater
+import com.xuqm.sdk.ui.DialogCenter
+import com.xuqm.sdk.ui.ToastCenter
 import com.xuqm.sdk.update.DownloadManager
 import com.xuqm.sdk.utils.DeviceUtils
+import com.xuqm.sdk.utils.FileHelper
+import com.xuqm.sdk.utils.Logger
+import java.lang.ref.WeakReference
 
 object CoreSDK {
     private var appContext: Context? = null
     private var config: SDKConfig = SDKConfig()
+    private var topActivityRef: WeakReference<Activity>? = null
 
     data class SDKConfig(
         val debugMode: Boolean = false,
@@ -20,20 +27,66 @@ object CoreSDK {
 
     fun init(context: Context, config: SDKConfig = SDKConfig()) {
         if (appContext != null) return
-        appContext = context.applicationContext
+        val applicationContext = context.applicationContext
+        appContext = applicationContext
         this.config = config
         HttpManager.init(HttpConfig(debugMode = config.debugMode))
+        Logger.init(config.debugMode)
+        GlobalCache.init(applicationContext)
+        ToastCenter.init(applicationContext)
+        DialogCenter.init(applicationContext)
+        FileHelper.init(applicationContext)
+        registerLifecycleCallbacks(applicationContext)
     }
 
     fun context(): Context = requireNotNull(appContext) { "CoreSDK not initialized" }
 
-    fun pluginPackageManager(): PluginPackageManager = PluginPackageManager.getInstance(context())
+    fun config(): SDKConfig = config
+
+    fun currentActivity(): Activity? = topActivityRef?.get()
 
     fun downloadManager(): DownloadManager = DownloadManager.getInstance(context())
 
-    fun appUpdater(): AppUpdater = AppUpdater.getInstance(context())
+    fun fileHelper(): FileHelper = FileHelper
+
+    fun cache(): GlobalCache = GlobalCache
+
+    fun logger(): Logger = Logger
+
+    fun dialogCenter(): DialogCenter = DialogCenter
 
     fun deviceId(): String = DeviceUtils.getDeviceId(context())
 
     fun deviceInfo() = DeviceUtils.getDeviceInfo(context())
+
+    private fun registerLifecycleCallbacks(context: Context) {
+        val application = context as? Application ?: return
+        application.registerActivityLifecycleCallbacks(
+            object : Application.ActivityLifecycleCallbacks {
+                override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
+                    topActivityRef = WeakReference(activity)
+                }
+
+                override fun onActivityStarted(activity: Activity) {
+                    topActivityRef = WeakReference(activity)
+                }
+
+                override fun onActivityResumed(activity: Activity) {
+                    topActivityRef = WeakReference(activity)
+                }
+
+                override fun onActivityPaused(activity: Activity) = Unit
+
+                override fun onActivityStopped(activity: Activity) = Unit
+
+                override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
+
+                override fun onActivityDestroyed(activity: Activity) {
+                    if (topActivityRef?.get() === activity) {
+                        topActivityRef = null
+                    }
+                }
+            },
+        )
+    }
 }

+ 72 - 0
AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/cache/GlobalCache.kt

@@ -0,0 +1,72 @@
+package com.xuqm.sdk.cache
+
+import android.content.Context
+import org.json.JSONArray
+import org.json.JSONObject
+
+object GlobalCache {
+    private const val DEFAULT_TTL = 24 * 60 * 60 * 1000L
+    private var cacheManager: SharedCacheManager? = null
+
+    internal fun init(context: Context) {
+        if (cacheManager == null) {
+            cacheManager = SharedCacheManager.getInstance(context.applicationContext)
+        }
+    }
+
+    fun putString(key: String, value: String, ttl: Long = DEFAULT_TTL) {
+        requireManager().put(key, value, ttl)
+    }
+
+    fun getString(key: String, defaultValue: String? = null): String? {
+        return requireManager().getSync(key) ?: defaultValue
+    }
+
+    fun putBoolean(key: String, value: Boolean, ttl: Long = DEFAULT_TTL) {
+        putString(key, value.toString(), ttl)
+    }
+
+    fun getBoolean(key: String, defaultValue: Boolean = false): Boolean {
+        return getString(key)?.toBooleanStrictOrNull() ?: defaultValue
+    }
+
+    fun putInt(key: String, value: Int, ttl: Long = DEFAULT_TTL) {
+        putString(key, value.toString(), ttl)
+    }
+
+    fun getInt(key: String, defaultValue: Int = 0): Int {
+        return getString(key)?.toIntOrNull() ?: defaultValue
+    }
+
+    fun putLong(key: String, value: Long, ttl: Long = DEFAULT_TTL) {
+        putString(key, value.toString(), ttl)
+    }
+
+    fun getLong(key: String, defaultValue: Long = 0L): Long {
+        return getString(key)?.toLongOrNull() ?: defaultValue
+    }
+
+    fun putJson(key: String, value: JSONObject, ttl: Long = DEFAULT_TTL) {
+        putString(key, value.toString(), ttl)
+    }
+
+    fun getJson(key: String): JSONObject? {
+        return getString(key)?.let { runCatching { JSONObject(it) }.getOrNull() }
+    }
+
+    fun putJsonArray(key: String, value: JSONArray, ttl: Long = DEFAULT_TTL) {
+        putString(key, value.toString(), ttl)
+    }
+
+    fun getJsonArray(key: String): JSONArray? {
+        return getString(key)?.let { runCatching { JSONArray(it) }.getOrNull() }
+    }
+
+    fun remove(key: String) {
+        requireManager().remove(key)
+    }
+
+    private fun requireManager(): SharedCacheManager {
+        return requireNotNull(cacheManager) { "GlobalCache not initialized. Call CoreSDK.init() first." }
+    }
+}

+ 56 - 0
AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/communication/PluginMessenger.kt

@@ -0,0 +1,56 @@
+package com.xuqm.sdk.communication
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import java.util.UUID
+
+data class PluginMessage(
+    val id: String = UUID.randomUUID().toString(),
+    val from: String,
+    val to: String? = null,
+    val topic: String,
+    val payload: String? = null,
+    val timestamp: Long = System.currentTimeMillis(),
+)
+
+object PluginMessenger {
+    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
+    private val bus = MutableSharedFlow<PluginMessage>(extraBufferCapacity = 64)
+
+    fun send(
+        from: String,
+        topic: String,
+        payload: String? = null,
+        to: String? = null,
+    ) {
+        bus.tryEmit(
+            PluginMessage(
+                from = from,
+                to = to,
+                topic = topic,
+                payload = payload,
+            ),
+        )
+    }
+
+    fun subscribe(
+        owner: String,
+        topic: String? = null,
+        onReceive: (PluginMessage) -> Unit,
+    ): Job {
+        return bus
+            .filter { message ->
+                val topicMatches = topic == null || topic == message.topic
+                val targetMatches = message.to.isNullOrBlank() || message.to == owner
+                topicMatches && targetMatches
+            }
+            .onEach(onReceive)
+            .launchIn(scope)
+    }
+}

+ 2 - 3
AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/network/RetrofitManager.kt

@@ -29,7 +29,7 @@ class RetrofitManager private constructor() {
     private val serviceCache = ConcurrentHashMap<String, Any>()
     private var globalConfig: HttpConfig = HttpConfig()
 
-    fun init(config: HttpConfig = HttpConfig()) {
+    internal fun init(config: HttpConfig = HttpConfig()) {
         globalConfig = config
     }
 
@@ -75,7 +75,7 @@ class RetrofitManager private constructor() {
 }
 
 object HttpManager {
-    fun init(config: HttpConfig = HttpConfig()) {
+    internal fun init(config: HttpConfig = HttpConfig()) {
         RetrofitManager.getInstance().init(config)
     }
 
@@ -83,4 +83,3 @@ object HttpManager {
         return RetrofitManager.getInstance().getService(baseUrl, serviceClass, config)
     }
 }
-

+ 81 - 0
AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/permission/PermissionManager.kt

@@ -0,0 +1,81 @@
+package com.xuqm.sdk.permission
+
+import androidx.activity.result.ActivityResultCaller
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import com.xuqm.sdk.ui.DialogCenter
+
+data class PermissionRequest(
+    val permission: String,
+    val title: String,
+    val description: String,
+)
+
+data class PermissionResult(
+    val granted: List<String>,
+    val denied: List<String>,
+) {
+    fun allGranted(): Boolean = denied.isEmpty()
+}
+
+class PermissionManager private constructor(
+    private val launcher: ActivityResultLauncher<Array<String>>,
+    private val callback: (PermissionResult) -> Unit,
+) {
+    private var pendingRequests: List<PermissionRequest> = emptyList()
+
+    fun request(
+        requests: List<PermissionRequest>,
+        confirmTitle: String = "权限申请说明",
+        confirmButton: String = "继续申请",
+        cancelButton: String = "取消",
+        onCancelled: (() -> Unit)? = null,
+    ) {
+        if (requests.isEmpty()) {
+            callback(PermissionResult(emptyList(), emptyList()))
+            return
+        }
+        pendingRequests = requests
+        val message = buildString {
+            appendLine("应用将申请以下权限:")
+            requests.forEach {
+                appendLine()
+                append("• ")
+                append(it.title)
+                append(":")
+                append(it.description)
+            }
+        }.trim()
+        val shown = DialogCenter.showConfirm(
+            title = confirmTitle,
+            message = message,
+            confirmText = confirmButton,
+            cancelText = cancelButton,
+            cancelable = true,
+            canceledOnTouchOutside = false,
+            onConfirm = {
+                launcher.launch(requests.map { it.permission }.toTypedArray())
+            },
+            onCancel = onCancelled,
+        )
+        if (!shown) {
+            onCancelled?.invoke()
+        }
+    }
+
+    companion object {
+        fun register(
+            caller: ActivityResultCaller,
+            onResult: (PermissionResult) -> Unit,
+        ): PermissionManager {
+            lateinit var manager: PermissionManager
+            val launcher = caller.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result ->
+                val granted = manager.pendingRequests.map { it.permission }.filter { result[it] == true }
+                val denied = manager.pendingRequests.map { it.permission }.filterNot { result[it] == true }
+                onResult(PermissionResult(granted = granted, denied = denied))
+            }
+            manager = PermissionManager(launcher = launcher, callback = onResult)
+            return manager
+        }
+    }
+}

+ 102 - 0
AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/ui/DialogCenter.kt

@@ -0,0 +1,102 @@
+package com.xuqm.sdk.ui
+
+import android.app.Activity
+import android.app.AlertDialog
+import android.app.Dialog
+import android.content.Context
+import android.widget.LinearLayout
+import android.widget.ProgressBar
+import android.widget.TextView
+import com.xuqm.sdk.CoreSDK
+
+object DialogCenter {
+    data class LoadingConfig(
+        val message: String = "加载中...",
+        val cancelable: Boolean = false,
+        val canceledOnTouchOutside: Boolean = false,
+        val onUserCancel: (() -> Unit)? = null,
+    )
+
+    private var appContext: Context? = null
+    private var currentDialog: AlertDialog? = null
+    private var loadingDialog: Dialog? = null
+
+    internal fun init(context: Context) {
+        appContext = context.applicationContext
+    }
+
+    fun showConfirm(
+        title: String,
+        message: String,
+        confirmText: String = "确定",
+        cancelText: String = "取消",
+        cancelable: Boolean = true,
+        canceledOnTouchOutside: Boolean = true,
+        onConfirm: () -> Unit,
+        onCancel: (() -> Unit)? = null,
+    ): Boolean {
+        val activity = currentActivity() ?: return false
+        activity.runOnUiThread {
+            dismissCurrentDialog()
+            currentDialog = AlertDialog.Builder(activity)
+                .setTitle(title)
+                .setMessage(message)
+                .setPositiveButton(confirmText) { dialog, _ ->
+                    dialog.dismiss()
+                    onConfirm()
+                }
+                .setNegativeButton(cancelText) { dialog, _ ->
+                    dialog.dismiss()
+                    onCancel?.invoke()
+                }
+                .create()
+                .apply {
+                    setCancelable(cancelable)
+                    setCanceledOnTouchOutside(canceledOnTouchOutside)
+                    show()
+                }
+        }
+        return true
+    }
+
+    fun showLoading(config: LoadingConfig = LoadingConfig()): Boolean {
+        val activity = currentActivity() ?: return false
+        activity.runOnUiThread {
+            dismissLoading()
+            loadingDialog = Dialog(activity).apply {
+                setCancelable(config.cancelable)
+                setCanceledOnTouchOutside(config.canceledOnTouchOutside)
+                setOnCancelListener { config.onUserCancel?.invoke() }
+                setContentView(
+                    LinearLayout(activity).apply {
+                        orientation = LinearLayout.HORIZONTAL
+                        val padding = 48
+                        setPadding(padding, padding, padding, padding)
+                        addView(ProgressBar(activity))
+                        addView(
+                            TextView(activity).apply {
+                                text = config.message
+                                textSize = 16f
+                                setPadding(24, 0, 0, 0)
+                            },
+                        )
+                    },
+                )
+                show()
+            }
+        }
+        return true
+    }
+
+    fun dismissLoading() {
+        loadingDialog?.dismiss()
+        loadingDialog = null
+    }
+
+    fun dismissCurrentDialog() {
+        currentDialog?.dismiss()
+        currentDialog = null
+    }
+
+    private fun currentActivity(): Activity? = CoreSDK.currentActivity()
+}

+ 1 - 1
AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/ui/ToastCenter.kt

@@ -9,7 +9,7 @@ object ToastCenter {
     private val handler = Handler(Looper.getMainLooper())
     private var appContext: Context? = null
 
-    fun init(context: Context) {
+    internal fun init(context: Context) {
         appContext = context.applicationContext
     }
 

+ 2 - 18
AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/update/DownloadManager.kt

@@ -1,11 +1,8 @@
 package com.xuqm.sdk.update
 
 import android.content.Context
-import android.content.Intent
-import android.net.Uri
-import android.os.Build
 import android.os.Environment
-import androidx.core.content.FileProvider
+import com.xuqm.sdk.utils.FileHelper
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
@@ -159,20 +156,7 @@ class DownloadManager private constructor(private val context: Context) {
     }
 
     fun installApk(file: File): Boolean {
-        return runCatching {
-            val authority = "${context.packageName}.sdk.fileprovider"
-            val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-                FileProvider.getUriForFile(context, authority, file)
-            } else {
-                Uri.fromFile(file)
-            }
-            context.startActivity(
-                Intent(Intent.ACTION_VIEW).apply {
-                    addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
-                    setDataAndType(uri, "application/vnd.android.package-archive")
-                },
-            )
-        }.isSuccess
+        return FileHelper.installApk(file)
     }
 
     private fun resolvePath(storagePath: StoragePath, customPath: String?): String {

+ 34 - 1
AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/utils/DateTimeUtils.kt

@@ -16,5 +16,38 @@ object DateTimeUtils {
     }
 
     fun now(pattern: String = "yyyy-MM-dd HH:mm:ss"): String = format(System.currentTimeMillis(), pattern)
-}
 
+    fun nowMillis(): Long = System.currentTimeMillis()
+
+    fun parse(
+        value: String,
+        pattern: String = "yyyy-MM-dd HH:mm:ss",
+        timeZone: TimeZone = TimeZone.getDefault(),
+        locale: Locale = Locale.getDefault(),
+    ): Long? {
+        return runCatching {
+            SimpleDateFormat(pattern, locale).apply { this.timeZone = timeZone }.parse(value)?.time
+        }.getOrNull()
+    }
+
+    fun isToday(timeMillis: Long): Boolean {
+        val today = format(System.currentTimeMillis(), "yyyy-MM-dd")
+        return format(timeMillis, "yyyy-MM-dd") == today
+    }
+
+    fun addDays(timeMillis: Long, days: Int): Long = timeMillis + days * 24L * 60L * 60L * 1000L
+
+    fun addHours(timeMillis: Long, hours: Int): Long = timeMillis + hours * 60L * 60L * 1000L
+
+    fun between(startMillis: Long, endMillis: Long): Long = endMillis - startMillis
+
+    fun toFriendlyText(timeMillis: Long): String {
+        val delta = System.currentTimeMillis() - timeMillis
+        return when {
+            delta < 60_000L -> "刚刚"
+            delta < 60L * 60L * 1000L -> "${delta / 60_000L}分钟前"
+            delta < 24L * 60L * 60L * 1000L -> "${delta / (60L * 60L * 1000L)}小时前"
+            else -> format(timeMillis, "yyyy-MM-dd")
+        }
+    }
+}

+ 109 - 0
AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/utils/FileHelper.kt

@@ -0,0 +1,109 @@
+package com.xuqm.sdk.utils
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.provider.MediaStore
+import androidx.activity.result.ActivityResultCaller
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.PickVisualMediaRequest
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.content.FileProvider
+import java.io.File
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+object FileHelper {
+    private var appContext: Context? = null
+
+    internal fun init(context: Context) {
+        appContext = context.applicationContext
+    }
+
+    fun installApk(file: File): Boolean {
+        val context = requireContext()
+        return runCatching {
+            val uri = toUri(file)
+            context.startActivity(
+                Intent(Intent.ACTION_VIEW).apply {
+                    addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
+                    setDataAndType(uri, "application/vnd.android.package-archive")
+                },
+            )
+        }.isSuccess
+    }
+
+    fun openFile(file: File, mimeType: String = "*/*"): Boolean {
+        val context = requireContext()
+        return runCatching {
+            context.startActivity(
+                Intent(Intent.ACTION_VIEW).apply {
+                    addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
+                    setDataAndType(toUri(file), mimeType)
+                },
+            )
+        }.isSuccess
+    }
+
+    fun createTempImageFile(prefix: String = "IMG"): File {
+        val context = requireContext()
+        val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
+        val directory = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) ?: context.cacheDir
+        return File(directory, "${prefix}_${timeStamp}.jpg").apply {
+            parentFile?.mkdirs()
+        }
+    }
+
+    fun toUri(file: File): Uri {
+        val context = requireContext()
+        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            FileProvider.getUriForFile(context, "${context.packageName}.sdk.fileprovider", file)
+        } else {
+            Uri.fromFile(file)
+        }
+    }
+
+    fun registerFilePicker(
+        caller: ActivityResultCaller,
+        onResult: (Uri?) -> Unit,
+    ): ActivityResultLauncher<Array<String>> {
+        return caller.registerForActivityResult(ActivityResultContracts.OpenDocument(), onResult)
+    }
+
+    fun registerImagePicker(
+        caller: ActivityResultCaller,
+        onResult: (Uri?) -> Unit,
+    ): ActivityResultLauncher<PickVisualMediaRequest> {
+        return caller.registerForActivityResult(ActivityResultContracts.PickVisualMedia(), onResult)
+    }
+
+    fun registerCameraCapture(
+        caller: ActivityResultCaller,
+        outputFileProvider: () -> File = { createTempImageFile() },
+        onResult: (Boolean, Uri?) -> Unit,
+    ): Pair<ActivityResultLauncher<Uri>, () -> Uri> {
+        var pendingUri: Uri? = null
+        val launcher = caller.registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
+            onResult(success, pendingUri)
+        }
+        val uriProvider = {
+            pendingUri = toUri(outputFileProvider())
+            requireNotNull(pendingUri)
+        }
+        return launcher to uriProvider
+    }
+
+    fun createImageCaptureIntent(outputUri: Uri): Intent {
+        return Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
+            putExtra(MediaStore.EXTRA_OUTPUT, outputUri)
+            addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
+        }
+    }
+
+    private fun requireContext(): Context {
+        return requireNotNull(appContext) { "FileHelper not initialized. Call CoreSDK.init() first." }
+    }
+}

+ 49 - 0
AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/utils/Logger.kt

@@ -0,0 +1,49 @@
+package com.xuqm.sdk.utils
+
+import android.util.Log
+import org.json.JSONArray
+import org.json.JSONObject
+
+object Logger {
+    private var debugEnabled: Boolean = false
+    private const val DEFAULT_TAG = "CoreSDK"
+
+    internal fun init(debug: Boolean) {
+        debugEnabled = debug
+    }
+
+    fun d(message: Any?, tag: String = DEFAULT_TAG) {
+        if (debugEnabled) {
+            Log.d(tag, stringify(message))
+        }
+    }
+
+    fun i(message: Any?, tag: String = DEFAULT_TAG) {
+        Log.i(tag, stringify(message))
+    }
+
+    fun w(message: Any?, tag: String = DEFAULT_TAG) {
+        Log.w(tag, stringify(message))
+    }
+
+    fun e(message: Any?, throwable: Throwable? = null, tag: String = DEFAULT_TAG) {
+        Log.e(tag, stringify(message), throwable)
+    }
+
+    fun json(json: String, tag: String = DEFAULT_TAG) {
+        val pretty = runCatching {
+            when {
+                json.trim().startsWith("{") -> JSONObject(json).toString(2)
+                json.trim().startsWith("[") -> JSONArray(json).toString(2)
+                else -> json
+            }
+        }.getOrDefault(json)
+        Log.d(tag, pretty)
+    }
+
+    private fun stringify(message: Any?): String = when (message) {
+        null -> "null"
+        is Throwable -> message.stackTraceToString()
+        else -> message.toString()
+    }
+}

+ 31 - 0
AndroidLibs/commonsdk-update/build.gradle.kts

@@ -0,0 +1,31 @@
+plugins {
+    alias(libs.plugins.android.library)
+}
+
+apply(from = rootProject.file("gradle/publishing.gradle.kts"))
+
+android {
+    namespace = "com.xuqm.sdk.updatekit"
+    compileSdk = libs.versions.compileSdk.get().toInt()
+
+    defaultConfig {
+        minSdk = libs.versions.minSdk.get().toInt()
+        consumerProguardFiles("consumer-rules.pro")
+    }
+
+    compileOptions {
+        sourceCompatibility = JavaVersion.VERSION_21
+        targetCompatibility = JavaVersion.VERSION_21
+    }
+}
+
+kotlin {
+    jvmToolchain(21)
+}
+
+dependencies {
+    api(project(":commonsdk-core"))
+    implementation(libs.kotlinx.coroutines.android)
+
+    testImplementation(libs.junit4)
+}

+ 1 - 0
AndroidLibs/commonsdk-update/consumer-rules.pro

@@ -0,0 +1 @@
+

+ 9 - 27
AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/plugin/PluginPackageManager.kt → AndroidLibs/commonsdk-update/src/main/java/com/xuqm/sdk/plugin/PluginPackageManager.kt

@@ -3,11 +3,9 @@ package com.xuqm.sdk.plugin
 import android.content.Context
 import android.content.Intent
 import android.content.pm.PackageManager
-import android.net.Uri
 import android.os.Build
-import androidx.core.content.FileProvider
 import com.xuqm.sdk.cache.CacheKeys
-import com.xuqm.sdk.cache.SharedCacheManager
+import com.xuqm.sdk.cache.GlobalCache
 import com.xuqm.sdk.update.DownloadDecision
 import com.xuqm.sdk.update.DownloadManager
 import com.xuqm.sdk.update.DownloadRequest
@@ -16,6 +14,7 @@ import com.xuqm.sdk.update.VersionCheckResult
 import com.xuqm.sdk.update.VersionCheckStrategy
 import com.xuqm.sdk.update.VersionComparator
 import com.xuqm.sdk.update.VersionInfo
+import com.xuqm.sdk.utils.FileHelper
 import org.json.JSONObject
 import java.io.File
 
@@ -41,7 +40,6 @@ class PluginPackageManager private constructor(private val context: Context) {
         }
     }
 
-    private val cacheManager = SharedCacheManager.getInstance(context)
     private val downloadManager = DownloadManager.getInstance(context)
 
     fun cacheCurrentUser(
@@ -57,11 +55,15 @@ class PluginPackageManager private constructor(private val context: Context) {
             put("timestamp", System.currentTimeMillis())
             extraData.forEach { (key, value) -> put(key, value) }
         }
-        cacheManager.put(CacheKeys.CURRENT_USER, json.toString(), 10 * 60 * 1000)
+        GlobalCache.putString(CacheKeys.CURRENT_USER, json.toString(), 10 * 60 * 1000)
     }
 
     fun getCachedUser(appPackageName: String? = null): String? {
-        return cacheManager.getSync(CacheKeys.CURRENT_USER, appPackageName)
+        return if (appPackageName.isNullOrBlank() || appPackageName == context.packageName) {
+            GlobalCache.getString(CacheKeys.CURRENT_USER)
+        } else {
+            com.xuqm.sdk.cache.SharedCacheManager.getInstance(context).getSync(CacheKeys.CURRENT_USER, appPackageName)
+        }
     }
 
     fun isPluginInstalled(packageName: String): Boolean {
@@ -92,11 +94,6 @@ class PluginPackageManager private constructor(private val context: Context) {
         }.getOrNull()
     }
 
-    fun compareVersion(packageName: String, remoteVersionCode: Long): Int {
-        val local = getLocalPluginInfo(packageName) ?: return 1
-        return remoteVersionCode.compareTo(local.versionCode)
-    }
-
     fun shouldDownloadPlugin(
         packageName: String,
         remoteVersionCode: Long? = null,
@@ -207,22 +204,7 @@ class PluginPackageManager private constructor(private val context: Context) {
         return runCatching { context.startActivity(launchIntent) }.isSuccess
     }
 
-    fun installPlugin(apkFile: File): Boolean {
-        return runCatching {
-            val authority = "${context.packageName}.sdk.fileprovider"
-            val uri: Uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-                FileProvider.getUriForFile(context, authority, apkFile)
-            } else {
-                Uri.fromFile(apkFile)
-            }
-            context.startActivity(
-                Intent(Intent.ACTION_VIEW).apply {
-                    setDataAndType(uri, "application/vnd.android.package-archive")
-                    addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
-                },
-            )
-        }.isSuccess
-    }
+    fun installPlugin(apkFile: File): Boolean = FileHelper.installApk(apkFile)
 
     fun loadPlugin(apkFile: File): Boolean = installPlugin(apkFile)
 

+ 2 - 1
AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/update/AppUpdater.kt → AndroidLibs/commonsdk-update/src/main/java/com/xuqm/sdk/update/AppUpdater.kt

@@ -3,6 +3,7 @@ package com.xuqm.sdk.update
 import android.content.Context
 import android.content.pm.PackageManager
 import android.os.Build
+import com.xuqm.sdk.utils.FileHelper
 import kotlinx.coroutines.flow.StateFlow
 
 data class UpdateInfo(
@@ -119,7 +120,7 @@ class AppUpdater private constructor(private val context: Context) {
 
     fun clear(taskId: String): Boolean = downloadManager.clear(taskId)
 
-    fun installApk(file: java.io.File): Boolean = downloadManager.installApk(file)
+    fun installApk(file: java.io.File): Boolean = FileHelper.installApk(file)
 
     fun installFromTask(taskId: String): Boolean {
         val file = downloadManager.getDownloadedFile(taskId) ?: return false

+ 0 - 0
AndroidLibs/commonsdk-core/src/main/java/com/xuqm/sdk/update/VersionComparator.kt → AndroidLibs/commonsdk-update/src/main/java/com/xuqm/sdk/update/VersionComparator.kt


+ 14 - 0
AndroidLibs/commonsdk-update/src/main/java/com/xuqm/sdk/updatekit/UpdateSDK.kt

@@ -0,0 +1,14 @@
+package com.xuqm.sdk.updatekit
+
+import android.content.Context
+import com.xuqm.sdk.CoreSDK
+import com.xuqm.sdk.plugin.PluginPackageManager
+import com.xuqm.sdk.update.AppUpdater
+
+object UpdateSDK {
+    fun appUpdater(context: Context = CoreSDK.context()): AppUpdater = AppUpdater.getInstance(context)
+
+    fun pluginPackageManager(context: Context = CoreSDK.context()): PluginPackageManager {
+        return PluginPackageManager.getInstance(context)
+    }
+}

+ 6 - 0
AndroidLibs/gradle/libs.versions.toml

@@ -7,6 +7,7 @@ minSdk = "24"
 coreKtx = "1.18.0"
 lifecycle = "2.10.0"
 activityCompose = "1.13.0"
+activityKtx = "1.13.0"
 composeBom = "2026.03.00"
 coroutines = "1.10.2"
 datastore = "1.1.7"
@@ -14,6 +15,8 @@ retrofit = "3.0.0"
 okhttp = "5.3.2"
 gson = "2.13.2"
 jserialization = "1.9.0"
+webkit = "1.14.0"
+coil = "2.7.0"
 junit4 = "4.13.2"
 androidxJunit = "1.3.0"
 espresso = "3.7.0"
@@ -23,6 +26,7 @@ androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref =
 androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
 androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
 androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
 androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
 androidx-ui = { group = "androidx.compose.ui", name = "ui" }
 androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
@@ -34,6 +38,8 @@ androidx-material3 = { group = "androidx.compose.material3", name = "material3"
 androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
 androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
 kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "jserialization" }
+androidx-webkit = { group = "androidx.webkit", name = "webkit", version.ref = "webkit" }
+coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
 retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
 retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
 okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }

+ 0 - 1
AndroidLibs/lib-szyx/src/main/java/com/xuqm/szyx/login/SzyxLoginActivity.kt

@@ -31,7 +31,6 @@ class SzyxLoginActivity : ComponentActivity() {
         if (!SzyxSDK.isInitialized()) {
             SzyxSDK.init(this)
         }
-        ToastCenter.init(this)
         setContent { MaterialTheme { LoginScreen { finish() } } }
     }
 }

+ 2 - 1
AndroidLibs/sample-app/build.gradle.kts

@@ -17,7 +17,7 @@ android {
         manifestPlaceholders["authProviderAuthority"] = "com.xuqm.sample.auth"
         manifestPlaceholders["sharedCacheAuthority"] = "com.xuqm.sample.sdk.cache.provider"
         manifestPlaceholders["coreFileProviderAuthority"] = "com.xuqm.sample.sdk.fileprovider"
-        buildConfigField("String", "UPDATE_SERVER_BASE_URL", "\"http://192.168.116.9:3000/\"")
+        buildConfigField("String", "UPDATE_SERVER_BASE_URL", "\"http://192.168.113.162:8080/\"")
     }
 
     buildTypes {
@@ -48,6 +48,7 @@ kotlin {
 dependencies {
     implementation(project(":commonsdk-core"))
     implementation(project(":commonsdk-compose"))
+    implementation(project(":commonsdk-update"))
     implementation(project(":lib-szyx"))
     implementation(libs.androidx.core.ktx)
     implementation(libs.androidx.lifecycle.runtime.ktx)

+ 10 - 10
AndroidLibs/sample-app/src/main/java/com/xuqm/sample/MainActivity.kt

@@ -49,6 +49,7 @@ import com.xuqm.sdk.update.StoragePath
 import com.xuqm.sdk.update.UpdateInfo
 import com.xuqm.sdk.update.VersionCheckResult
 import com.xuqm.sdk.update.VersionCheckStrategy
+import com.xuqm.sdk.updatekit.UpdateSDK
 import com.xuqm.sdk.utils.DateTimeUtils
 import com.xuqm.sample.update.UpdateRepository
 import com.xuqm.szyx.SzyxSDK
@@ -89,7 +90,7 @@ class MainActivity : ComponentActivity() {
                 if (intent.action == Intent.ACTION_PACKAGE_ADDED && reloadPluginAfterInstall) {
                     reloadPluginAfterInstall = false
                     val pluginUpdateInfo = currentPluginUpdateInfo
-                    CoreSDK.pluginPackageManager().reloadPlugin(
+                    UpdateSDK.pluginPackageManager().reloadPlugin(
                         packageName = PLUGIN_PACKAGE_NAME,
                         entryActivity = pluginUpdateInfo?.entryActivity ?: PLUGIN_ENTRY_ACTIVITY,
                         extras = pluginUpdateInfo?.extras ?: mapOf("hostPackageName" to this@MainActivity.packageName),
@@ -117,7 +118,6 @@ class MainActivity : ComponentActivity() {
                 debugMode = true,
             ),
         )
-        ToastCenter.init(this)
         refreshState()
         ensureLoginOnLaunch()
         registerPackageChangeReceiver()
@@ -170,7 +170,7 @@ class MainActivity : ComponentActivity() {
 
     private fun refreshState() {
         refreshSession()
-        pluginInstalledState.value = CoreSDK.pluginPackageManager().isPluginInstalled(PLUGIN_PACKAGE_NAME)
+        pluginInstalledState.value = UpdateSDK.pluginPackageManager().isPluginInstalled(PLUGIN_PACKAGE_NAME)
     }
 
     private fun openLogin() {
@@ -199,7 +199,7 @@ class MainActivity : ComponentActivity() {
             return
         }
 
-        val pluginManager = CoreSDK.pluginPackageManager()
+        val pluginManager = UpdateSDK.pluginPackageManager()
         pluginManager.cacheCurrentUser(
             userId = session.loginModel.userId,
             sessionId = session.loginModel.sessionId,
@@ -224,7 +224,7 @@ class MainActivity : ComponentActivity() {
                         entryActivity = remoteUpdate.entryActivity ?: PLUGIN_ENTRY_ACTIVITY,
                         extras = mapOf("hostPackageName" to packageName),
                     )
-                    val checkResult = CoreSDK.pluginPackageManager().checkPluginUpdate(
+                    val checkResult = UpdateSDK.pluginPackageManager().checkPluginUpdate(
                         packageName = pluginUpdateInfo.packageName,
                         remoteVersionCode = pluginUpdateInfo.versionCode,
                         remoteVersionName = pluginUpdateInfo.versionName,
@@ -235,7 +235,7 @@ class MainActivity : ComponentActivity() {
                         return@onSuccess
                     }
                     currentPluginUpdateInfo = pluginUpdateInfo
-                    val taskId = CoreSDK.pluginPackageManager().downloadPlugin(
+                    val taskId = UpdateSDK.pluginPackageManager().downloadPlugin(
                         updateInfo = pluginUpdateInfo,
                         fileName = "plugin-ui-release.apk",
                         storagePath = StoragePath.EXTERNAL_FILES,
@@ -259,7 +259,7 @@ class MainActivity : ComponentActivity() {
             updateRepository.fetchLatestAppUpdate(packageName)
                 .onSuccess { updateInfo ->
                     when (
-                        val checkResult = CoreSDK.appUpdater().checkUpdate(
+                        val checkResult = UpdateSDK.appUpdater().checkUpdate(
                             updateInfo = updateInfo,
                             strategy = VersionCheckStrategy.VERSION_CODE_OR_NAME,
                         )
@@ -281,7 +281,7 @@ class MainActivity : ComponentActivity() {
     private fun confirmDownloadAppUpdate() {
         val updateInfo = pendingAppUpdateState.value ?: return
         pendingAppUpdateState.value = null
-        val taskId = CoreSDK.appUpdater().downloadUpdate(
+        val taskId = UpdateSDK.appUpdater().downloadUpdate(
             updateInfo = updateInfo,
             fileName = "sample-app-update.apk",
             storagePath = StoragePath.EXTERNAL_FILES,
@@ -308,7 +308,7 @@ class MainActivity : ComponentActivity() {
                     is DownloadState.Success -> {
                         ToastCenter.show("插件下载完成,准备重新加载")
                         reloadPluginAfterInstall = true
-                        if (!CoreSDK.pluginPackageManager().loadPlugin(state.file)) {
+                        if (!UpdateSDK.pluginPackageManager().loadPlugin(state.file)) {
                             reloadPluginAfterInstall = false
                             ToastCenter.show("插件加载拉起失败")
                         }
@@ -343,7 +343,7 @@ class MainActivity : ComponentActivity() {
                 when (state) {
                     is DownloadState.Success -> {
                         ToastCenter.show("安装包下载完成,准备安装")
-                        if (!CoreSDK.appUpdater().installApk(state.file)) {
+                        if (!UpdateSDK.appUpdater().installApk(state.file)) {
                             ToastCenter.show("应用安装拉起失败")
                         }
                         CoreSDK.downloadManager().clear(taskId)

+ 1 - 0
AndroidLibs/settings.gradle.kts

@@ -20,6 +20,7 @@ rootProject.name = "AndroidLibs"
 
 include(":commonsdk-core")
 include(":commonsdk-compose")
+include(":commonsdk-update")
 include(":lib-szyx")
 include(":sample-app")
 include(":plugins:plugin-ui")

+ 35 - 0
doc/01-project-overview.md

@@ -0,0 +1,35 @@
+# 01 项目概览
+
+## 项目目标
+
+本项目用于承载 Android SDK、示例宿主应用、插件能力、运营平台、管理平台以及配套的版本管理服务端。
+
+当前目标重点:
+
+- 保持 Android 端 `sample-app` 与 `plugin-ui` 的版本查询能力可继续使用
+- 增加运营平台与管理平台两个 Vue 3 前端
+- 服务端升级为 Spring Boot 微服务形态,便于后续继续扩展即时通讯、推送、版本管理等模块
+- 首期优先落地版本管理、插件化开关、灰度发布与用户钩子能力
+
+## 目录结构
+
+```text
+AndroidLibsGroup/
+├── AndroidLibs/                 Android SDK、示例宿主、插件工程
+├── frontend/                    Yarn workspace 前端工程
+│   ├── ops-platform/            运营平台
+│   └── admin-platform/          管理平台
+├── server/                      Spring Boot 服务端与旧版 Node 示例服务
+│   ├── version-management-service/
+│   └── version-service/
+└── doc/                         项目文档
+```
+
+## 当前已实现内容
+
+- Android SDK 与示例应用基础工程
+- Vue 3 运营平台与管理平台基础页面
+- Spring Boot 版本管理服务
+- MySQL 持久化
+- Redis 缓存灰度用户列表
+- Android 更新兼容接口

+ 37 - 0
doc/02-architecture.md

@@ -0,0 +1,37 @@
+# 02 架构设计
+
+## 总体分层
+
+### Android 侧
+
+- `commonsdk-core`:网络、下载、文件、权限、缓存、日志、全局弹窗、时间等基础能力
+- `commonsdk-compose`:下拉刷新、折叠面板、左滑操作、WebView、图片等 Compose 组件扩展
+- `commonsdk-update`:独立更新 SDK,负责宿主 APK / 插件版本比较、下载编排与安装拉起
+- `lib-szyx`:业务登录与会话管理
+- `sample-app`:宿主示例
+- `plugins/plugin-ui`:插件示例
+
+### 前端侧
+
+- `ops-platform`:运营使用,开放注册、版本上传、发布、灰度圈选
+- `admin-platform`:平台治理使用,审核主账户、禁用账户、管理子账户权限
+
+### 服务端
+
+- `version-management-service`:当前主服务
+- 后续可继续拆分为账号中心、推送中心、IM 服务、文件服务等独立微服务
+
+## 当前服务端结构
+
+- `controller`:接口层
+- `service`:业务编排层
+- `persistence/entity`:JPA 实体
+- `persistence/repository`:JPA 仓储
+- `config`:CORS、Redis、初始化数据等配置
+
+## 演进方向
+
+- 账号体系独立成 `account-service`
+- 版本包上传独立成文件存储服务,对接 MinIO 或对象存储
+- 灰度用户钩子改为对接真实用户平台 API,而不是初始化数据
+- 增加统一鉴权与租户隔离

+ 65 - 0
doc/03-frontend.md

@@ -0,0 +1,65 @@
+# 03 前端说明
+
+## 技术栈
+
+- Vue 3
+- Vue Router 4
+- Pinia
+- Vite 6
+- TypeScript 5
+- Yarn workspace
+
+## 包管理
+
+前端统一使用 Yarn,不再使用 npm。
+
+根目录:
+
+```bash
+cd frontend
+yarn install
+```
+
+开发命令:
+
+```bash
+yarn dev:ops
+yarn dev:admin
+```
+
+构建命令:
+
+```bash
+yarn build
+```
+
+## 项目职责
+
+### 运营平台 `ops-platform`
+
+- 主账户开放注册
+- 版本管理
+- 插件化开关
+- 版本包上传
+- 全量发布
+- 灰度发布
+- 灰度用户圈选
+
+### 管理平台 `admin-platform`
+
+- 查看运营主账户
+- 审核主账户
+- 禁用账户
+- 查看子账户
+- 管理子账户权限
+
+## 环境变量
+
+- `VITE_API_BASE_URL`:服务端接口地址,默认 `http://127.0.0.1:8080`
+
+## 后续规划
+
+- 引入统一 UI 组件体系
+- 增加登录鉴权与路由守卫
+- 加入页面级权限控制
+- 加入文件上传组件和上传进度状态

+ 56 - 0
doc/04-backend.md

@@ -0,0 +1,56 @@
+# 04 服务端说明
+
+## 当前主服务
+
+`server/version-management-service`
+
+## 技术基线
+
+- JDK 21
+- Spring Boot 3.4.4
+- Spring Web
+- Spring Data JPA
+- Spring Data Redis
+- MySQL 8+
+
+## 已实现接口能力
+
+### 公开接口
+
+- 运营平台主账户注册
+
+### 管理平台接口
+
+- 账户列表
+- 账户审核/禁用
+- 子账户权限更新
+
+### 运营平台接口
+
+- 应用列表
+- 插件化开关
+- 版本上传
+- 版本发布
+- 灰度用户列表
+- 分组列表
+- 快速选择列表
+
+### Android 兼容接口
+
+- `GET /api/v1/updates/app/latest`
+- `GET /api/v1/updates/plugin/latest`
+
+## 启动
+
+```bash
+cd server
+mvn -pl version-management-service spring-boot:run
+```
+
+## 后续演进建议
+
+- 增加 Spring Security + JWT
+- 引入 Flyway 管理表结构
+- 加入 OpenAPI/Swagger
+- 增加单元测试与集成测试
+- 版本包下载地址改为文件服务生成

+ 60 - 0
doc/05-infrastructure.md

@@ -0,0 +1,60 @@
+# 05 数据库与缓存
+
+## MySQL
+
+- Host: `xuqinmin.com`
+- Port: `3306`
+- Database: `androidLibsServer`
+- Username: `androidLibsServer`
+
+当前通过 Spring JPA 自动建表,后续建议切换到 Flyway 管理。
+
+当前 `version-management-service` 的数据库与 Redis 配置风格参考了
+`/Users/xuqinmin/Projects/TrustProjects/AppManager/AppManagerWeb/RuoYi-Vue`
+项目,重点对齐了:
+
+- MySQL JDBC 参数格式
+- Redis 基础连接项
+- Redis Lettuce 连接池参数
+- 数据库连接池的超时与空闲参数
+
+支持通过环境变量覆盖:
+
+- `DB_URL`
+- `DB_USERNAME`
+- `DB_PASSWORD`
+
+## Redis
+
+- Host: `redisdev.xuqinmin.com`
+- Port: `6379`
+- Database: `0`
+
+支持通过环境变量覆盖:
+
+- `REDIS_HOST`
+- `REDIS_PORT`
+- `REDIS_DATABASE`
+- `REDIS_PASSWORD`
+
+## 当前数据用途
+
+### MySQL
+
+- 运营主账户
+- 子账户
+- 应用配置
+- 版本记录
+- 灰度用户基础数据
+- 用户分组
+- 快速选择配置
+
+### Redis
+
+- 灰度用户列表查询缓存
+- 分组与快速选择组合查询结果缓存
+
+## 注意事项
+
+- 当前数据库与 Redis 密码已直接写入配置文件,仅适合当前内部开发阶段
+- 后续应迁移到环境变量、配置中心或密钥管理服务

+ 55 - 0
doc/06-version-management.md

@@ -0,0 +1,55 @@
+# 06 版本管理与灰度发布
+
+## 业务范围
+
+当前版本管理覆盖:
+
+- App 版本管理
+- 插件版本管理
+- 插件化开关
+- 版本包上传登记
+- 全量发布
+- 灰度发布
+
+## 灰度发布规则
+
+灰度发布基于用户平台钩子能力,当前支持三种圈选方式:
+
+- 分组选择
+- 快速选择
+- 单选用户
+
+系统返回的用户字段包含:
+
+- 用户 ID
+- 昵称
+- 手机号
+- 邮箱
+- 地区
+- 分组信息
+
+其中 ID、昵称、手机号、邮箱在前台展示时进行脱敏。
+
+## Android 拉取版本逻辑
+
+### 全量版本
+
+所有用户都可拉取到最新已发布版本。
+
+### 灰度版本
+
+只有命中灰度规则的用户才能拉取到灰度版本。
+
+支持命中条件:
+
+- 命中指定用户 ID
+- 命中指定用户分组
+- 命中指定快速选择集合
+
+## 后续升级项
+
+- 支持灰度比例发布
+- 支持按渠道、设备、地区、版本范围灰度
+- 支持灰度回滚
+- 支持发布时间窗口
+- 支持上传真实 APK/AAB/插件包并持久化元数据

+ 42 - 0
doc/07-development-workflow.md

@@ -0,0 +1,42 @@
+# 07 开发与交付流程
+
+## 本地开发
+
+### Android
+
+在 `AndroidLibs/` 下使用 Gradle 进行开发与构建。
+
+```bash
+cd AndroidLibs
+./gradlew :sample-app:assembleDebug :plugins:plugin-ui:assembleDebug
+```
+
+### 前端
+
+```bash
+cd frontend
+yarn install
+yarn dev:ops
+yarn dev:admin
+```
+
+### 服务端
+
+```bash
+cd server
+mvn -pl version-management-service spring-boot:run
+```
+
+## 提交流程
+
+- 功能开发完成后,同步更新 `doc/` 文档
+- 变更接口时,同时更新前端调用与服务端说明
+- 变更基础设施配置时,同时更新 `05-infrastructure.md`
+- 变更版本管理逻辑时,同时更新 `06-version-management.md`
+- 变更 Android SDK 模块结构或公共组件时,同时更新 `08-android-sdk.md`
+
+## 持续维护要求
+
+- `doc/` 为项目正式文档目录,后续新增能力必须持续补充
+- 若引入新微服务,需要新增单独文档章节
+- 若引入 CI/CD、容器化部署、测试基线,需要补充新的专题文档

+ 54 - 0
doc/08-android-sdk.md

@@ -0,0 +1,54 @@
+# 08 Android SDK 说明
+
+## 当前模块拆分
+
+- `commonsdk-core`
+  - 网络能力:`HttpManager`、`RetrofitManager`
+  - 下载能力:`DownloadManager`
+  - 文件能力:`FileHelper`
+  - 权限能力:`PermissionManager`
+  - 缓存能力:`GlobalCache`、`SharedCacheManager`
+  - 通信能力:`PluginMessenger`
+  - 通用 UI 能力:`ToastCenter`、`DialogCenter`
+  - 工具能力:`Logger`、`DateTimeUtils`
+- `commonsdk-compose`
+  - 下拉刷新:`SwipeRefreshContainer`、`RefreshableLazyColumn`
+  - 折叠面板:`AccordionPanel`、`RefreshableAccordionList`
+  - 左滑操作:`SwipeActionItem`
+  - WebView:`CommonWebView`、`WebViewPageActivity`
+  - 图片:`AdaptiveImage`、`CircleImage`
+- `commonsdk-update`
+  - 宿主更新:`AppUpdater`
+  - 插件更新:`PluginPackageManager`
+  - SDK 门面:`UpdateSDK`
+  - 版本比较:`VersionComparator`
+
+## 设计原则
+
+- 下载与文件安装归 `core`,保证后续任意业务 SDK 都能复用。
+- 宿主更新和插件更新归 `commonsdk-update`,避免核心模块承担业务升级策略。
+- Compose 组件保持业务无关,App 可直接引用或二次封装。
+
+## 接入方式
+
+### 宿主应用
+
+- 初始化基础 SDK:`CoreSDK.init(context, CoreSDK.SDKConfig(...))`
+- 使用更新能力:`UpdateSDK.appUpdater()`、`UpdateSDK.pluginPackageManager()`
+- 使用公共能力:`CoreSDK.fileHelper()`、`CoreSDK.cache()`、`CoreSDK.dialogCenter()`
+
+### WebView 页面
+
+- 内嵌组件:直接使用 `CommonWebView(url = "...")`
+- 独立页面:通过 `WebViewPageActivity.createIntent(context, url, title)` 跳转
+
+### 权限申请
+
+- 通过 `PermissionManager.register(...)` 注册权限请求器
+- 请求时传入权限标题和描述,框架会先弹出确认说明,再真正调用系统权限弹窗
+
+## 当前验证
+
+- 2026-03-27 已执行:
+  - `./gradlew --no-daemon --no-configuration-cache :sample-app:assembleDebug :plugins:plugin-ui:assembleDebug`
+- 结果:构建通过

+ 20 - 0
doc/README.md

@@ -0,0 +1,20 @@
+# AndroidLibsGroup 文档
+
+当前目录用于沉淀项目的长期文档,后续新增模块、接口、部署说明、发布流程都持续更新在这里。
+
+## 文档索引
+
+- [01-项目概览](./01-project-overview.md)
+- [02-架构设计](./02-architecture.md)
+- [03-前端说明](./03-frontend.md)
+- [04-服务端说明](./04-backend.md)
+- [05-数据库与缓存](./05-infrastructure.md)
+- [06-版本管理与灰度发布](./06-version-management.md)
+- [07-开发与交付流程](./07-development-workflow.md)
+- [08-Android SDK 说明](./08-android-sdk.md)
+
+## 维护约定
+
+- 新增模块时,同步更新对应章节。
+- 接口或数据结构发生变化时,同步更新服务端、前端和版本管理文档。
+- 部署地址、账号、依赖版本变更时,优先更新 `04-05` 章节。

+ 19 - 7
frontend/README.md

@@ -1,10 +1,17 @@
 # frontend
 
-当前目录新增两个 Vue 3 前端项目:
+当前目录新增两个 Vue 3 前端项目,并统一使用 Yarn workspace 管理
 
 - `ops-platform`:运营平台,提供开放注册、版本管理、插件化开关、全量/灰度发布。
 - `admin-platform`:管理平台,负责审核运营账户、禁用账户、管理子账户权限。
 
+## 安装依赖
+
+```bash
+cd frontend
+yarn install
+```
+
 ## 启动方式
 
 先启动后端:
@@ -17,15 +24,20 @@ mvn -pl version-management-service spring-boot:run
 再分别启动前端:
 
 ```bash
-cd frontend/ops-platform
-npm install
-npm run dev
+cd frontend
+yarn dev:ops
 ```
 
 ```bash
-cd frontend/admin-platform
-npm install
-npm run dev
+cd frontend
+yarn dev:admin
+```
+
+## 构建
+
+```bash
+cd frontend
+yarn build
 ```
 
 前端默认请求 `http://127.0.0.1:8080`,如需调整可通过 `VITE_API_BASE_URL` 覆盖。

+ 1 - 0
frontend/admin-platform/package.json

@@ -2,6 +2,7 @@
   "name": "admin-platform",
   "version": "0.1.0",
   "private": true,
+  "packageManager": "yarn@1.22.22",
   "type": "module",
   "scripts": {
     "dev": "vite",

+ 1 - 0
frontend/ops-platform/package.json

@@ -2,6 +2,7 @@
   "name": "ops-platform",
   "version": "0.1.0",
   "private": true,
+  "packageManager": "yarn@1.22.22",
   "type": "module",
   "scripts": {
     "dev": "vite",

+ 2 - 17
frontend/ops-platform/src/App.vue

@@ -1,22 +1,7 @@
 <template>
-  <div class="shell">
-    <aside class="sidebar">
-      <div>
-        <p class="eyebrow">Vue3 · 运营平台</p>
-        <h1>版本运营工作台</h1>
-        <p class="muted">开放注册、子账户、版本管理、灰度发布都集中在这里。</p>
-      </div>
-      <nav class="nav">
-        <RouterLink to="/register">平台注册</RouterLink>
-        <RouterLink to="/versions">版本管理</RouterLink>
-      </nav>
-    </aside>
-    <main class="content">
-      <RouterView />
-    </main>
-  </div>
+  <RouterView />
 </template>
 
 <script setup lang="ts">
-import { RouterLink, RouterView } from 'vue-router'
+import { RouterView } from 'vue-router'
 </script>

+ 82 - 8
frontend/ops-platform/src/api/client.ts

@@ -18,15 +18,36 @@ export interface Account {
   createdAt: string
 }
 
+export interface LoginResult {
+  account: Account
+  tenantAccountId: string
+}
+
+export interface PluginConfig {
+  id: string
+  appId: string
+  name: string
+  packageName: string
+  entryActivity?: string | null
+  description?: string | null
+  enabled: boolean
+}
+
 export interface ReleaseRecord {
   id: string
+  appId: string
+  pluginId?: string | null
   packageType: 'APP' | 'PLUGIN'
+  packageName: string
   versionCode: number
   versionName: string
   title: string
   changelog: string
   downloadUrl: string
   uploadedFileName: string
+  entryActivity?: string | null
+  minHostVersionCode?: number | null
+  minHostVersionName?: string | null
   status: 'DRAFT' | 'PUBLISHED' | 'GRAYSCALE'
   publishStrategy: string
   grayRule?: {
@@ -42,10 +63,12 @@ export interface ApplicationDetail {
     id: string
     name: string
     packageName: string
-    pluginPackageName: string
+    pluginPackageName?: string | null
+    description?: string | null
     pluginManagementEnabled: boolean
     businessModules: string[]
   }
+  plugins: PluginConfig[]
   releases: ReleaseRecord[]
 }
 
@@ -92,29 +115,80 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
 }
 
 export const api = {
-  registerAccount(payload: Pick<Account, 'accountName' | 'contactName' | 'email' | 'phone'>) {
+  registerAccount(payload: {
+    accountName: string
+    contactName: string
+    email: string
+    phone: string
+    password: string
+  }) {
     return request<Account>('/api/v1/open/accounts/register', {
       method: 'POST',
       body: JSON.stringify(payload),
     })
   },
+  login(payload: { email: string; password: string }) {
+    return request<LoginResult>('/api/v1/open/accounts/login', {
+      method: 'POST',
+      body: JSON.stringify(payload),
+    })
+  },
   listApplications() {
-    return request<ApplicationDetail[]>('/api/v1/ops/version/applications')
+    return request<ApplicationDetail[]>('/api/v1/ops/apps')
+  },
+  createApplication(payload: Record<string, unknown>) {
+    return request('/api/v1/ops/apps', {
+      method: 'POST',
+      body: JSON.stringify(payload),
+    })
+  },
+  updateApplication(appId: string, payload: Record<string, unknown>) {
+    return request(`/api/v1/ops/apps/${appId}`, {
+      method: 'PUT',
+      body: JSON.stringify(payload),
+    })
   },
   togglePluginManagement(appId: string, enabled: boolean) {
-    return request(`/api/v1/ops/version/applications/${appId}/plugin-management`, {
+    return request(`/api/v1/ops/apps/${appId}/plugin-management`, {
       method: 'PUT',
       body: JSON.stringify({ enabled }),
     })
   },
-  uploadRelease(appId: string, payload: Record<string, unknown>) {
-    return request<ReleaseRecord>(`/api/v1/ops/version/applications/${appId}/releases/upload`, {
+  listAppPackages(appId: string) {
+    return request<ReleaseRecord[]>(`/api/v1/ops/apps/${appId}/packages`)
+  },
+  uploadAppPackage(appId: string, payload: Record<string, unknown>) {
+    return request<ReleaseRecord>(`/api/v1/ops/apps/${appId}/packages`, {
+      method: 'POST',
+      body: JSON.stringify(payload),
+    })
+  },
+  listPlugins(appId: string) {
+    return request<PluginConfig[]>(`/api/v1/ops/apps/${appId}/plugins`)
+  },
+  createPlugin(appId: string, payload: Record<string, unknown>) {
+    return request<PluginConfig>(`/api/v1/ops/apps/${appId}/plugins`, {
+      method: 'POST',
+      body: JSON.stringify(payload),
+    })
+  },
+  updatePlugin(pluginId: string, payload: Record<string, unknown>) {
+    return request<PluginConfig>(`/api/v1/ops/plugins/${pluginId}`, {
+      method: 'PUT',
+      body: JSON.stringify(payload),
+    })
+  },
+  listPluginPackages(pluginId: string) {
+    return request<ReleaseRecord[]>(`/api/v1/ops/plugins/${pluginId}/packages`)
+  },
+  uploadPluginPackage(pluginId: string, payload: Record<string, unknown>) {
+    return request<ReleaseRecord>(`/api/v1/ops/plugins/${pluginId}/packages`, {
       method: 'POST',
       body: JSON.stringify(payload),
     })
   },
-  publishRelease(appId: string, releaseId: string, payload: Record<string, unknown>) {
-    return request<ReleaseRecord>(`/api/v1/ops/version/applications/${appId}/releases/${releaseId}/publish`, {
+  publishPackage(releaseId: string, payload: Record<string, unknown>) {
+    return request<ReleaseRecord>(`/api/v1/ops/packages/${releaseId}/publish`, {
       method: 'POST',
       body: JSON.stringify(payload),
     })

+ 16 - 5
frontend/ops-platform/src/router/index.ts

@@ -1,12 +1,23 @@
 import { createRouter, createWebHistory } from 'vue-router'
-import RegisterView from '../views/RegisterView.vue'
-import VersionManagementView from '../views/VersionManagementView.vue'
+import LoginView from '../views/LoginView.vue'
+import OpsShellView from '../views/OpsShellView.vue'
+import ModulePlaceholderView from '../views/ModulePlaceholderView.vue'
+import AppManagementView from '../views/AppManagementView.vue'
 
 export default createRouter({
   history: createWebHistory(),
   routes: [
-    { path: '/', redirect: '/register' },
-    { path: '/register', component: RegisterView },
-    { path: '/versions', component: VersionManagementView },
+    { path: '/', redirect: '/login' },
+    { path: '/login', component: LoginView },
+    {
+      path: '/console',
+      component: OpsShellView,
+      children: [
+        { path: '', redirect: '/console/apps' },
+        { path: 'im', component: ModulePlaceholderView, props: { title: 'IM 模块', description: '预留即时通讯管理控制台。' } },
+        { path: 'push', component: ModulePlaceholderView, props: { title: 'Push 模块', description: '预留推送策略、模板与发布能力。' } },
+        { path: 'apps', component: AppManagementView },
+      ],
+    },
   ],
 })

+ 39 - 0
frontend/ops-platform/src/styles.css

@@ -66,6 +66,13 @@ select {
   gap: 20px;
 }
 
+.auth-layout {
+  min-height: 100vh;
+  display: grid;
+  place-items: center;
+  padding: 24px;
+}
+
 .stack {
   grid-template-columns: 1.25fr 1fr;
   align-items: start;
@@ -167,6 +174,38 @@ button {
   flex-wrap: wrap;
 }
 
+.tab-row,
+.list-grid {
+  display: flex;
+  gap: 12px;
+  flex-wrap: wrap;
+  margin-top: 16px;
+}
+
+.list-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+}
+
+.list-card {
+  text-align: left;
+  display: grid;
+  gap: 6px;
+  background: #eef5fb;
+  color: #163454;
+}
+
+.list-card[data-active='true'] {
+  background: linear-gradient(135deg, #0d72ff, #11b8a5);
+  color: white;
+}
+
+.sub-panel {
+  margin-top: 20px;
+  padding-top: 20px;
+  border-top: 1px solid rgba(16, 35, 61, 0.08);
+}
+
 .chips span,
 .chip-button {
   padding: 6px 10px;

+ 297 - 0
frontend/ops-platform/src/views/AppManagementView.vue

@@ -0,0 +1,297 @@
+<template>
+  <section class="page stack">
+    <div class="panel">
+      <div class="section-head">
+        <div>
+          <p class="section-tag">App 管理</p>
+          <h2>应用列表</h2>
+        </div>
+        <button class="primary" @click="showCreateApp = !showCreateApp">创建 App</button>
+      </div>
+
+      <form v-if="showCreateApp" class="form-grid" @submit.prevent="createApp">
+        <label><span>名称</span><input v-model="appForm.name" required /></label>
+        <label><span>包名</span><input v-model="appForm.packageName" required /></label>
+        <label><span>插件包名前缀</span><input v-model="appForm.pluginPackageName" /></label>
+        <label class="full"><span>说明</span><input v-model="appForm.description" /></label>
+        <label class="toggle"><input type="checkbox" v-model="appForm.pluginManagementEnabled" />支持插件化</label>
+        <button class="primary">保存 App</button>
+      </form>
+
+      <div class="list-grid">
+        <button
+          v-for="item in applications"
+          :key="item.application.id"
+          class="list-card"
+          :data-active="selectedApp?.application.id === item.application.id"
+          @click="selectApp(item)"
+        >
+          <strong>{{ item.application.name }}</strong>
+          <span>{{ item.application.packageName }}</span>
+          <span>{{ item.application.pluginManagementEnabled ? '支持插件化' : '仅宿主' }}</span>
+        </button>
+      </div>
+    </div>
+
+    <div class="panel" v-if="selectedApp">
+      <div class="section-head">
+        <div>
+          <p class="section-tag">当前应用</p>
+          <h2>{{ selectedApp.application.name }}</h2>
+        </div>
+        <button class="secondary" @click="loadAll">刷新</button>
+      </div>
+
+      <form class="form-grid" @submit.prevent="saveApp">
+        <label><span>名称</span><input v-model="appEdit.name" required /></label>
+        <label><span>包名</span><input v-model="appEdit.packageName" required /></label>
+        <label><span>插件包名前缀</span><input v-model="appEdit.pluginPackageName" /></label>
+        <label class="full"><span>说明</span><input v-model="appEdit.description" /></label>
+        <label class="toggle"><input type="checkbox" v-model="appEdit.pluginManagementEnabled" />支持插件化</label>
+        <button class="primary">修改 App 信息</button>
+      </form>
+
+      <div class="tab-row">
+        <button :class="tab === 'packages' ? 'primary' : 'secondary'" @click="tab = 'packages'">安装包</button>
+        <button
+          v-if="selectedApp.application.pluginManagementEnabled"
+          :class="tab === 'plugins' ? 'primary' : 'secondary'"
+          @click="tab = 'plugins'"
+        >
+          插件列表
+        </button>
+      </div>
+
+      <template v-if="tab === 'packages'">
+        <form class="form-grid" @submit.prevent="uploadAppPackage">
+          <label><span>包名</span><input v-model="appPackageForm.packageName" required /></label>
+          <label><span>版本名</span><input v-model="appPackageForm.versionName" required /></label>
+          <label><span>版本码</span><input v-model.number="appPackageForm.versionCode" type="number" required /></label>
+          <label><span>文件名</span><input v-model="appPackageForm.uploadedFileName" required /></label>
+          <label class="full"><span>下载地址</span><input v-model="appPackageForm.downloadUrl" required /></label>
+          <label class="full"><span>标题</span><input v-model="appPackageForm.title" required /></label>
+          <label class="full"><span>更新说明</span><textarea v-model="appPackageForm.changelog" rows="3" /></label>
+          <button class="primary">上传安装包</button>
+        </form>
+
+        <table class="table">
+          <thead><tr><th>版本</th><th>状态</th><th>操作</th></tr></thead>
+          <tbody>
+            <tr v-for="release in appPackages" :key="release.id">
+              <td>{{ release.versionName }} ({{ release.versionCode }})</td>
+              <td>{{ release.status }}</td>
+              <td class="actions">
+                <button class="ghost" @click="publishFull(release.id)">发布当前安装包</button>
+                <button class="ghost" @click="prepareGray(release.id)">配置灰度</button>
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </template>
+
+      <template v-else>
+        <form class="form-grid" @submit.prevent="createPlugin">
+          <label><span>插件名</span><input v-model="pluginForm.name" required /></label>
+          <label><span>插件包名</span><input v-model="pluginForm.packageName" required /></label>
+          <label class="full"><span>入口 Activity</span><input v-model="pluginForm.entryActivity" /></label>
+          <label class="full"><span>说明</span><input v-model="pluginForm.description" /></label>
+          <button class="primary">新建插件</button>
+        </form>
+
+        <div class="list-grid">
+          <button
+            v-for="plugin in plugins"
+            :key="plugin.id"
+            class="list-card"
+            :data-active="selectedPlugin?.id === plugin.id"
+            @click="selectPlugin(plugin)"
+          >
+            <strong>{{ plugin.name }}</strong>
+            <span>{{ plugin.packageName }}</span>
+            <span>{{ plugin.enabled ? '已启用' : '未启用' }}</span>
+          </button>
+        </div>
+
+        <div v-if="selectedPlugin" class="sub-panel">
+          <h3>{{ selectedPlugin.name }} 安装包</h3>
+          <form class="form-grid" @submit.prevent="uploadPluginPackage">
+            <label><span>版本名</span><input v-model="pluginPackageForm.versionName" required /></label>
+            <label><span>版本码</span><input v-model.number="pluginPackageForm.versionCode" type="number" required /></label>
+            <label><span>宿主最低版本码</span><input v-model.number="pluginPackageForm.minHostVersionCode" type="number" /></label>
+            <label><span>宿主最低版本名</span><input v-model="pluginPackageForm.minHostVersionName" /></label>
+            <label><span>文件名</span><input v-model="pluginPackageForm.uploadedFileName" required /></label>
+            <label class="full"><span>下载地址</span><input v-model="pluginPackageForm.downloadUrl" required /></label>
+            <label class="full"><span>标题</span><input v-model="pluginPackageForm.title" required /></label>
+            <label class="full"><span>更新说明</span><textarea v-model="pluginPackageForm.changelog" rows="3" /></label>
+            <button class="primary">上传插件安装包</button>
+          </form>
+
+          <table class="table">
+            <thead><tr><th>版本</th><th>宿主最低版本</th><th>状态</th><th>操作</th></tr></thead>
+            <tbody>
+              <tr v-for="release in pluginPackages" :key="release.id">
+                <td>{{ release.versionName }} ({{ release.versionCode }})</td>
+                <td>{{ release.minHostVersionName || '-' }}</td>
+                <td>{{ release.status }}</td>
+                <td class="actions">
+                  <button class="ghost" @click="publishFull(release.id)">发版</button>
+                  <button class="ghost" @click="prepareGray(release.id)">灰度</button>
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+      </template>
+
+      <div v-if="grayReleaseId" class="sub-panel">
+        <h3>灰度信息配置</h3>
+        <div class="filters">
+          <label><span>分组</span>
+            <select v-model="filters.groupCode" @change="loadUsers">
+              <option value="">全部</option>
+              <option v-for="group in groups" :key="group.code" :value="group.code">{{ group.name }}</option>
+            </select>
+          </label>
+          <label><span>快选</span>
+            <select v-model="filters.quickSelectionCode" @change="loadUsers">
+              <option value="">全部</option>
+              <option v-for="item in quickSelections" :key="item.code" :value="item.code">{{ item.name }}</option>
+            </select>
+          </label>
+          <label class="grow"><span>搜索</span><input v-model="filters.keyword" @input="loadUsers" /></label>
+        </div>
+        <table class="table">
+          <thead><tr><th></th><th>ID</th><th>昵称</th><th>地区</th><th>分组</th></tr></thead>
+          <tbody>
+            <tr v-for="user in users" :key="user.id">
+              <td><input type="checkbox" :checked="selectedUsers.includes(user.id)" @change="toggleUser(user.id)" /></td>
+              <td>{{ user.id }}</td>
+              <td>{{ user.nickname }}</td>
+              <td>{{ user.region }}</td>
+              <td>{{ user.groupName }}</td>
+            </tr>
+          </tbody>
+        </table>
+        <div class="actions">
+          <button class="primary" @click="publishGray">确认灰度发布</button>
+        </div>
+      </div>
+    </div>
+  </section>
+</template>
+
+<script setup lang="ts">
+import { onMounted, reactive, ref } from 'vue'
+import { api, type ApplicationDetail, type AudienceGroup, type AudienceUser, type PluginConfig, type QuickSelection, type ReleaseRecord } from '../api/client'
+
+const applications = ref<ApplicationDetail[]>([])
+const selectedApp = ref<ApplicationDetail | null>(null)
+const selectedPlugin = ref<PluginConfig | null>(null)
+const appPackages = ref<ReleaseRecord[]>([])
+const pluginPackages = ref<ReleaseRecord[]>([])
+const plugins = ref<PluginConfig[]>([])
+const users = ref<AudienceUser[]>([])
+const groups = ref<AudienceGroup[]>([])
+const quickSelections = ref<QuickSelection[]>([])
+const selectedUsers = ref<string[]>([])
+const grayReleaseId = ref('')
+const showCreateApp = ref(false)
+const tab = ref<'packages' | 'plugins'>('packages')
+
+const filters = reactive({ keyword: '', groupCode: '', quickSelectionCode: '' })
+const appForm = reactive({ name: '', packageName: '', pluginPackageName: '', description: '', pluginManagementEnabled: true, businessModules: ['IM', 'PUSH', 'VERSION'] })
+const appEdit = reactive({ name: '', packageName: '', pluginPackageName: '', description: '', pluginManagementEnabled: true, businessModules: ['IM', 'PUSH', 'VERSION'] })
+const pluginForm = reactive({ name: '', packageName: '', entryActivity: '', description: '' })
+const appPackageForm = reactive({ packageName: '', versionCode: 1, versionName: '', title: '发现新版本', changelog: '', downloadUrl: '', uploadedFileName: '', entryActivity: '', forceUpdate: false })
+const pluginPackageForm = reactive({ versionCode: 1, versionName: '', title: '插件更新', changelog: '', downloadUrl: '', uploadedFileName: '', entryActivity: '', minHostVersionCode: 0, minHostVersionName: '' })
+
+async function loadAll() {
+  applications.value = await api.listApplications()
+  const [groupList, quickList] = await Promise.all([api.listAudienceGroups(), api.listQuickSelections()])
+  groups.value = groupList
+  quickSelections.value = quickList
+  if (applications.value.length > 0 && !selectedApp.value) {
+    await selectApp(applications.value[0])
+  }
+}
+
+async function selectApp(item: ApplicationDetail) {
+  selectedApp.value = item
+  selectedPlugin.value = null
+  tab.value = 'packages'
+  Object.assign(appEdit, item.application)
+  appPackageForm.packageName = item.application.packageName
+  appPackages.value = await api.listAppPackages(item.application.id)
+  plugins.value = await api.listPlugins(item.application.id)
+}
+
+async function createApp() {
+  await api.createApplication(appForm)
+  showCreateApp.value = false
+  await loadAll()
+}
+
+async function saveApp() {
+  if (!selectedApp.value) return
+  await api.updateApplication(selectedApp.value.application.id, appEdit)
+  await loadAll()
+}
+
+async function uploadAppPackage() {
+  if (!selectedApp.value) return
+  await api.uploadAppPackage(selectedApp.value.application.id, appPackageForm)
+  appPackages.value = await api.listAppPackages(selectedApp.value.application.id)
+}
+
+async function createPlugin() {
+  if (!selectedApp.value) return
+  await api.createPlugin(selectedApp.value.application.id, pluginForm)
+  plugins.value = await api.listPlugins(selectedApp.value.application.id)
+}
+
+async function selectPlugin(plugin: PluginConfig) {
+  selectedPlugin.value = plugin
+  pluginPackages.value = await api.listPluginPackages(plugin.id)
+}
+
+async function uploadPluginPackage() {
+  if (!selectedPlugin.value) return
+  await api.uploadPluginPackage(selectedPlugin.value.id, pluginPackageForm)
+  pluginPackages.value = await api.listPluginPackages(selectedPlugin.value.id)
+}
+
+async function publishFull(releaseId: string) {
+  await api.publishPackage(releaseId, { grayPublish: false })
+  if (selectedApp.value) appPackages.value = await api.listAppPackages(selectedApp.value.application.id)
+  if (selectedPlugin.value) pluginPackages.value = await api.listPluginPackages(selectedPlugin.value.id)
+}
+
+function prepareGray(releaseId: string) {
+  grayReleaseId.value = releaseId
+  void loadUsers()
+}
+
+async function loadUsers() {
+  users.value = await api.listAudienceUsers(filters)
+}
+
+function toggleUser(userId: string) {
+  selectedUsers.value = selectedUsers.value.includes(userId)
+    ? selectedUsers.value.filter(item => item !== userId)
+    : [...selectedUsers.value, userId]
+}
+
+async function publishGray() {
+  if (!grayReleaseId.value) return
+  await api.publishPackage(grayReleaseId.value, {
+    grayPublish: true,
+    hookName: 'user-platform-gray-hook',
+    groupCodes: filters.groupCode ? [filters.groupCode] : [],
+    quickSelectionCodes: filters.quickSelectionCode ? [filters.quickSelectionCode] : [],
+    userIds: selectedUsers.value,
+  })
+  grayReleaseId.value = ''
+}
+
+onMounted(() => { void loadAll() })
+</script>

+ 72 - 0
frontend/ops-platform/src/views/LoginView.vue

@@ -0,0 +1,72 @@
+<template>
+  <section class="auth-layout">
+    <div class="panel">
+      <p class="section-tag">运营平台</p>
+      <h1>登录或注册</h1>
+      <p class="muted">登录后进入 IM、Push、App 管理三大模块。</p>
+
+      <div class="tab-row">
+        <button :class="mode === 'login' ? 'primary' : 'secondary'" @click="mode = 'login'">登录</button>
+        <button :class="mode === 'register' ? 'primary' : 'secondary'" @click="mode = 'register'">注册</button>
+      </div>
+
+      <form class="form-grid" @submit.prevent="submit">
+        <label v-if="mode === 'register'">
+          企业名称
+          <input v-model="form.accountName" placeholder="星云运营中心" required />
+        </label>
+        <label v-if="mode === 'register'">
+          联系人
+          <input v-model="form.contactName" placeholder="林青" required />
+        </label>
+        <label>
+          邮箱
+          <input v-model="form.email" type="email" placeholder="ops@nebula.example" required />
+        </label>
+        <label v-if="mode === 'register'">
+          手机号
+          <input v-model="form.phone" placeholder="13800138000" required />
+        </label>
+        <label class="full">
+          密码
+          <input v-model="form.password" type="password" placeholder="请输入密码" required />
+        </label>
+        <button class="primary full">{{ mode === 'login' ? '登录并进入工作台' : '注册并登录' }}</button>
+      </form>
+
+      <p v-if="message" class="success-text">{{ message }}</p>
+      <p class="muted">内置测试账号:`ops@nebula.example / 123456`</p>
+    </div>
+  </section>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { api } from '../api/client'
+
+const router = useRouter()
+const mode = ref<'login' | 'register'>('login')
+const message = ref('')
+const form = reactive({
+  accountName: '',
+  contactName: '',
+  email: '',
+  phone: '',
+  password: '',
+})
+
+async function submit() {
+  message.value = ''
+  try {
+    if (mode.value === 'register') {
+      await api.registerAccount(form)
+    }
+    const result = await api.login({ email: form.email, password: form.password })
+    localStorage.setItem('ops-session', JSON.stringify(result))
+    await router.push('/console/apps')
+  } catch (error) {
+    message.value = error instanceof Error ? error.message : '操作失败'
+  }
+}
+</script>

+ 11 - 0
frontend/ops-platform/src/views/ModulePlaceholderView.vue

@@ -0,0 +1,11 @@
+<template>
+  <section class="panel">
+    <p class="section-tag">业务模块</p>
+    <h2>{{ title }}</h2>
+    <p class="muted">{{ description }}</p>
+  </section>
+</template>
+
+<script setup lang="ts">
+defineProps<{ title: string; description: string }>()
+</script>

+ 23 - 0
frontend/ops-platform/src/views/OpsShellView.vue

@@ -0,0 +1,23 @@
+<template>
+  <div class="shell">
+    <aside class="sidebar">
+      <div>
+        <p class="eyebrow">Vue3 · 运营平台</p>
+        <h1>运营控制台</h1>
+        <p class="muted">登录后统一管理 IM、Push 和 App/插件发布。</p>
+      </div>
+      <nav class="nav">
+        <RouterLink to="/console/im">IM</RouterLink>
+        <RouterLink to="/console/push">Push</RouterLink>
+        <RouterLink to="/console/apps">App 管理</RouterLink>
+      </nav>
+    </aside>
+    <main class="content">
+      <RouterView />
+    </main>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { RouterLink, RouterView } from 'vue-router'
+</script>

+ 14 - 0
frontend/package.json

@@ -0,0 +1,14 @@
+{
+  "name": "android-libs-frontend-workspace",
+  "private": true,
+  "packageManager": "yarn@1.22.22",
+  "workspaces": [
+    "ops-platform",
+    "admin-platform"
+  ],
+  "scripts": {
+    "dev:ops": "yarn workspace ops-platform dev",
+    "dev:admin": "yarn workspace admin-platform dev",
+    "build": "yarn workspaces run build"
+  }
+}

+ 738 - 0
frontend/yarn.lock

@@ -0,0 +1,738 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/helper-string-parser@^7.27.1":
+  version "7.27.1"
+  resolved "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
+  integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
+
+"@babel/helper-validator-identifier@^7.28.5":
+  version "7.28.5"
+  resolved "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4"
+  integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==
+
+"@babel/parser@^7.29.2":
+  version "7.29.2"
+  resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.2.tgz#58bd50b9a7951d134988a1ae177a35ef9a703ba1"
+  integrity sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==
+  dependencies:
+    "@babel/types" "^7.29.0"
+
+"@babel/types@^7.29.0":
+  version "7.29.0"
+  resolved "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7"
+  integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==
+  dependencies:
+    "@babel/helper-string-parser" "^7.27.1"
+    "@babel/helper-validator-identifier" "^7.28.5"
+
+"@esbuild/aix-ppc64@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz#80fcbe36130e58b7670511e888b8e88a259ed76c"
+  integrity sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==
+
+"@esbuild/android-arm64@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz#8aa4965f8d0a7982dc21734bf6601323a66da752"
+  integrity sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==
+
+"@esbuild/android-arm@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz#300712101f7f50f1d2627a162e6e09b109b6767a"
+  integrity sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==
+
+"@esbuild/android-x64@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz#87dfb27161202bdc958ef48bb61b09c758faee16"
+  integrity sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==
+
+"@esbuild/darwin-arm64@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz#79197898ec1ff745d21c071e1c7cc3c802f0c1fd"
+  integrity sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==
+
+"@esbuild/darwin-x64@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz#146400a8562133f45c4d2eadcf37ddd09718079e"
+  integrity sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==
+
+"@esbuild/freebsd-arm64@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz#1c5f9ba7206e158fd2b24c59fa2d2c8bb47ca0fe"
+  integrity sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==
+
+"@esbuild/freebsd-x64@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz#ea631f4a36beaac4b9279fa0fcc6ca29eaeeb2b3"
+  integrity sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==
+
+"@esbuild/linux-arm64@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz#e1066bce58394f1b1141deec8557a5f0a22f5977"
+  integrity sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==
+
+"@esbuild/linux-arm@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz#452cd66b20932d08bdc53a8b61c0e30baf4348b9"
+  integrity sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==
+
+"@esbuild/linux-ia32@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz#b24f8acc45bcf54192c7f2f3be1b53e6551eafe0"
+  integrity sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==
+
+"@esbuild/linux-loong64@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz#f9cfffa7fc8322571fbc4c8b3268caf15bd81ad0"
+  integrity sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==
+
+"@esbuild/linux-mips64el@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz#575a14bd74644ffab891adc7d7e60d275296f2cd"
+  integrity sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==
+
+"@esbuild/linux-ppc64@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz#75b99c70a95fbd5f7739d7692befe60601591869"
+  integrity sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==
+
+"@esbuild/linux-riscv64@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz#2e3259440321a44e79ddf7535c325057da875cd6"
+  integrity sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==
+
+"@esbuild/linux-s390x@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz#17676cabbfe5928da5b2a0d6df5d58cd08db2663"
+  integrity sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==
+
+"@esbuild/linux-x64@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz#0583775685ca82066d04c3507f09524d3cd7a306"
+  integrity sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==
+
+"@esbuild/netbsd-arm64@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz#f04c4049cb2e252fe96b16fed90f70746b13f4a4"
+  integrity sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==
+
+"@esbuild/netbsd-x64@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz#77da0d0a0d826d7c921eea3d40292548b258a076"
+  integrity sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==
+
+"@esbuild/openbsd-arm64@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz#6296f5867aedef28a81b22ab2009c786a952dccd"
+  integrity sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==
+
+"@esbuild/openbsd-x64@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz#f8d23303360e27b16cf065b23bbff43c14142679"
+  integrity sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==
+
+"@esbuild/openharmony-arm64@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz#49e0b768744a3924be0d7fd97dd6ce9b2923d88d"
+  integrity sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==
+
+"@esbuild/sunos-x64@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz#a6ed7d6778d67e528c81fb165b23f4911b9b13d6"
+  integrity sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==
+
+"@esbuild/win32-arm64@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz#9ac14c378e1b653af17d08e7d3ce34caef587323"
+  integrity sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==
+
+"@esbuild/win32-ia32@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz#918942dcbbb35cc14fca39afb91b5e6a3d127267"
+  integrity sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==
+
+"@esbuild/win32-x64@0.25.12":
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz#9bdad8176be7811ad148d1f8772359041f46c6c5"
+  integrity sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==
+
+"@jridgewell/sourcemap-codec@^1.5.5":
+  version "1.5.5"
+  resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
+  integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
+
+"@rollup/rollup-android-arm-eabi@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz#7e158ddfc16f78da99c0d5ccbae6cae403ef3284"
+  integrity sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==
+
+"@rollup/rollup-android-arm64@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz#49f4ae0e22b6f9ffbcd3818b9a0758fa2d10b1cd"
+  integrity sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==
+
+"@rollup/rollup-darwin-arm64@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz#bb200269069acf5c1c4d79ad142524f77e8b8236"
+  integrity sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==
+
+"@rollup/rollup-darwin-x64@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz#1bf7a92b27ebdd5e0d1d48503c7811160773be1a"
+  integrity sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==
+
+"@rollup/rollup-freebsd-arm64@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz#5ccf537b99c5175008444702193ad0b1c36f7f16"
+  integrity sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==
+
+"@rollup/rollup-freebsd-x64@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz#1196ecd7bf4e128624ef83cd1f9d785114474a77"
+  integrity sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==
+
+"@rollup/rollup-linux-arm-gnueabihf@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz#cc147633a4af229fee83a737bf2334fbac3dc28e"
+  integrity sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==
+
+"@rollup/rollup-linux-arm-musleabihf@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz#3559f9f060153ea54594a42c3b87a297bedcc26e"
+  integrity sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==
+
+"@rollup/rollup-linux-arm64-gnu@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz#e91f887b154123485cfc4b59befe2080fcd8f2df"
+  integrity sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==
+
+"@rollup/rollup-linux-arm64-musl@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz#660752f040df9ba44a24765df698928917c0bf21"
+  integrity sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==
+
+"@rollup/rollup-linux-loong64-gnu@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz#cb0e939a5fa479ccef264f3f45b31971695f869c"
+  integrity sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==
+
+"@rollup/rollup-linux-loong64-musl@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz#42f86fbc82cd1a81be2d346476dd3231cf5ee442"
+  integrity sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==
+
+"@rollup/rollup-linux-ppc64-gnu@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz#39776a647a789dc95ea049277c5ef8f098df77f9"
+  integrity sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==
+
+"@rollup/rollup-linux-ppc64-musl@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz#466f20029a8e8b3bb2954c7ddebc9586420cac2c"
+  integrity sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==
+
+"@rollup/rollup-linux-riscv64-gnu@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz#cff9877c78f12e7aa6246f6902ad913e99edb2b7"
+  integrity sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==
+
+"@rollup/rollup-linux-riscv64-musl@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz#9a762fb99b5a82a921017f56491b7e892b9fb17d"
+  integrity sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==
+
+"@rollup/rollup-linux-s390x-gnu@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz#9d25ad8ac7dab681935baf78ac5ea92d14629cdf"
+  integrity sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==
+
+"@rollup/rollup-linux-x64-gnu@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz#5e5139e11819fa38a052368da79422cb4afcf466"
+  integrity sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==
+
+"@rollup/rollup-linux-x64-musl@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz#b6211d46e11b1f945f5504cc794fce839331ed08"
+  integrity sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==
+
+"@rollup/rollup-openbsd-x64@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz#e6e09eebaa7012bb9c7331b437a9e992bd94ca35"
+  integrity sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==
+
+"@rollup/rollup-openharmony-arm64@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz#f7d99ae857032498e57a5e7259fb7100fd24a87e"
+  integrity sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==
+
+"@rollup/rollup-win32-arm64-msvc@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz#41e392f5d9f3bf1253fdaf2f6d6f6b1bfc452856"
+  integrity sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==
+
+"@rollup/rollup-win32-ia32-msvc@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz#f41b0490be0e5d3cf459b4dc076a192b532adea9"
+  integrity sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==
+
+"@rollup/rollup-win32-x64-gnu@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz#0fcf9f1fcb750f0317b13aac3b3231687e6397a5"
+  integrity sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==
+
+"@rollup/rollup-win32-x64-msvc@4.60.0":
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz#3afdb30405f6d4248df5e72e1ca86c5eab55fab8"
+  integrity sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==
+
+"@types/estree@1.0.8":
+  version "1.0.8"
+  resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
+  integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
+
+"@vitejs/plugin-vue@^5.2.3":
+  version "5.2.4"
+  resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz#9e8a512eb174bfc2a333ba959bbf9de428d89ad8"
+  integrity sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==
+
+"@volar/language-core@2.4.15":
+  version "2.4.15"
+  resolved "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.15.tgz#759d04cb4eab9920560b8bcfa4515d5b08a1b7ce"
+  integrity sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==
+  dependencies:
+    "@volar/source-map" "2.4.15"
+
+"@volar/source-map@2.4.15":
+  version "2.4.15"
+  resolved "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.15.tgz#18aba09994c0268e59a418f9d738e4a85302781d"
+  integrity sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==
+
+"@volar/typescript@2.4.15":
+  version "2.4.15"
+  resolved "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.15.tgz#1445d23f8e4f9ad821b6bfa58cf4a2b980dc5f97"
+  integrity sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==
+  dependencies:
+    "@volar/language-core" "2.4.15"
+    path-browserify "^1.0.1"
+    vscode-uri "^3.0.8"
+
+"@vue/compiler-core@3.5.31":
+  version "3.5.31"
+  resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.31.tgz#db20d99eb3e8e9ce3c008b8cc79bdb7dab3dfe61"
+  integrity sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==
+  dependencies:
+    "@babel/parser" "^7.29.2"
+    "@vue/shared" "3.5.31"
+    entities "^7.0.1"
+    estree-walker "^2.0.2"
+    source-map-js "^1.2.1"
+
+"@vue/compiler-dom@3.5.31", "@vue/compiler-dom@^3.5.0":
+  version "3.5.31"
+  resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.31.tgz#7d4b5688a8daef1513ee18f566ea129bded36ff7"
+  integrity sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==
+  dependencies:
+    "@vue/compiler-core" "3.5.31"
+    "@vue/shared" "3.5.31"
+
+"@vue/compiler-sfc@3.5.31":
+  version "3.5.31"
+  resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.31.tgz#ab3670bc81f0bf60ccd0766b16042ad5b334d821"
+  integrity sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q==
+  dependencies:
+    "@babel/parser" "^7.29.2"
+    "@vue/compiler-core" "3.5.31"
+    "@vue/compiler-dom" "3.5.31"
+    "@vue/compiler-ssr" "3.5.31"
+    "@vue/shared" "3.5.31"
+    estree-walker "^2.0.2"
+    magic-string "^0.30.21"
+    postcss "^8.5.8"
+    source-map-js "^1.2.1"
+
+"@vue/compiler-ssr@3.5.31":
+  version "3.5.31"
+  resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.31.tgz#bc68bb14cedab04ff5230460badfca983de5391b"
+  integrity sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog==
+  dependencies:
+    "@vue/compiler-dom" "3.5.31"
+    "@vue/shared" "3.5.31"
+
+"@vue/compiler-vue2@^2.7.16":
+  version "2.7.16"
+  resolved "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz#2ba837cbd3f1b33c2bc865fbe1a3b53fb611e249"
+  integrity sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==
+  dependencies:
+    de-indent "^1.0.2"
+    he "^1.2.0"
+
+"@vue/devtools-api@^6.6.4":
+  version "6.6.4"
+  resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
+  integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
+
+"@vue/devtools-api@^7.7.7":
+  version "7.7.9"
+  resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.9.tgz#999dbea50da6b00cf59a1336f11fdc2b43d9e063"
+  integrity sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==
+  dependencies:
+    "@vue/devtools-kit" "^7.7.9"
+
+"@vue/devtools-kit@^7.7.9":
+  version "7.7.9"
+  resolved "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz#bc218a815616e8987df7ab3e10fc1fb3b8706c58"
+  integrity sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==
+  dependencies:
+    "@vue/devtools-shared" "^7.7.9"
+    birpc "^2.3.0"
+    hookable "^5.5.3"
+    mitt "^3.0.1"
+    perfect-debounce "^1.0.0"
+    speakingurl "^14.0.1"
+    superjson "^2.2.2"
+
+"@vue/devtools-shared@^7.7.9":
+  version "7.7.9"
+  resolved "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz#fa4c096b744927081a7dda5fcf05f34b1ae6ca14"
+  integrity sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==
+  dependencies:
+    rfdc "^1.4.1"
+
+"@vue/language-core@2.2.12":
+  version "2.2.12"
+  resolved "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.12.tgz#d01f7e865f593f968cb65c12a13d8337e65641f0"
+  integrity sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==
+  dependencies:
+    "@volar/language-core" "2.4.15"
+    "@vue/compiler-dom" "^3.5.0"
+    "@vue/compiler-vue2" "^2.7.16"
+    "@vue/shared" "^3.5.0"
+    alien-signals "^1.0.3"
+    minimatch "^9.0.3"
+    muggle-string "^0.4.1"
+    path-browserify "^1.0.1"
+
+"@vue/reactivity@3.5.31":
+  version "3.5.31"
+  resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.31.tgz#c8deaab09bd26185d3153d3e4447e2c6590a6608"
+  integrity sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g==
+  dependencies:
+    "@vue/shared" "3.5.31"
+
+"@vue/runtime-core@3.5.31":
+  version "3.5.31"
+  resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.31.tgz#bbe07e3bda17caf3ca2ba289bdd2d22badf3dce4"
+  integrity sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q==
+  dependencies:
+    "@vue/reactivity" "3.5.31"
+    "@vue/shared" "3.5.31"
+
+"@vue/runtime-dom@3.5.31":
+  version "3.5.31"
+  resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.31.tgz#3fe39a7bbbbf3ef2cdd6c51f8a1ea63d13959ec6"
+  integrity sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g==
+  dependencies:
+    "@vue/reactivity" "3.5.31"
+    "@vue/runtime-core" "3.5.31"
+    "@vue/shared" "3.5.31"
+    csstype "^3.2.3"
+
+"@vue/server-renderer@3.5.31":
+  version "3.5.31"
+  resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.31.tgz#d5dbc14dedc37315d197d0a846ef817e02257778"
+  integrity sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA==
+  dependencies:
+    "@vue/compiler-ssr" "3.5.31"
+    "@vue/shared" "3.5.31"
+
+"@vue/shared@3.5.31", "@vue/shared@^3.5.0":
+  version "3.5.31"
+  resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.31.tgz#83276e1d450fea7d20dd15c3bbafbea5aada122d"
+  integrity sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==
+
+alien-signals@^1.0.3:
+  version "1.0.13"
+  resolved "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.13.tgz#8d6db73462f742ee6b89671fbd8c37d0b1727a7e"
+  integrity sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==
+
+balanced-match@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+  integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+birpc@^2.3.0:
+  version "2.9.0"
+  resolved "https://registry.npmmirror.com/birpc/-/birpc-2.9.0.tgz#b59550897e4cd96a223e2a6c1475b572236ed145"
+  integrity sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==
+
+brace-expansion@^2.0.2:
+  version "2.0.3"
+  resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.3.tgz#0493338bdd58e319b1039c67cf7ee439892c01d9"
+  integrity sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==
+  dependencies:
+    balanced-match "^1.0.0"
+
+copy-anything@^4:
+  version "4.0.5"
+  resolved "https://registry.npmmirror.com/copy-anything/-/copy-anything-4.0.5.tgz#16cabafd1ea4bb327a540b750f2b4df522825aea"
+  integrity sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==
+  dependencies:
+    is-what "^5.2.0"
+
+csstype@^3.2.3:
+  version "3.2.3"
+  resolved "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a"
+  integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==
+
+de-indent@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
+  integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==
+
+entities@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz#26e8a88889db63417dcb9a1e79a3f1bc92b5976b"
+  integrity sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==
+
+esbuild@^0.25.0:
+  version "0.25.12"
+  resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz#97a1d041f4ab00c2fce2f838d2b9969a2d2a97a5"
+  integrity sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==
+  optionalDependencies:
+    "@esbuild/aix-ppc64" "0.25.12"
+    "@esbuild/android-arm" "0.25.12"
+    "@esbuild/android-arm64" "0.25.12"
+    "@esbuild/android-x64" "0.25.12"
+    "@esbuild/darwin-arm64" "0.25.12"
+    "@esbuild/darwin-x64" "0.25.12"
+    "@esbuild/freebsd-arm64" "0.25.12"
+    "@esbuild/freebsd-x64" "0.25.12"
+    "@esbuild/linux-arm" "0.25.12"
+    "@esbuild/linux-arm64" "0.25.12"
+    "@esbuild/linux-ia32" "0.25.12"
+    "@esbuild/linux-loong64" "0.25.12"
+    "@esbuild/linux-mips64el" "0.25.12"
+    "@esbuild/linux-ppc64" "0.25.12"
+    "@esbuild/linux-riscv64" "0.25.12"
+    "@esbuild/linux-s390x" "0.25.12"
+    "@esbuild/linux-x64" "0.25.12"
+    "@esbuild/netbsd-arm64" "0.25.12"
+    "@esbuild/netbsd-x64" "0.25.12"
+    "@esbuild/openbsd-arm64" "0.25.12"
+    "@esbuild/openbsd-x64" "0.25.12"
+    "@esbuild/openharmony-arm64" "0.25.12"
+    "@esbuild/sunos-x64" "0.25.12"
+    "@esbuild/win32-arm64" "0.25.12"
+    "@esbuild/win32-ia32" "0.25.12"
+    "@esbuild/win32-x64" "0.25.12"
+
+estree-walker@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
+  integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
+
+fdir@^6.4.4, fdir@^6.5.0:
+  version "6.5.0"
+  resolved "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350"
+  integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
+
+fsevents@~2.3.2, fsevents@~2.3.3:
+  version "2.3.3"
+  resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+  integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
+he@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
+  integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+
+hookable@^5.5.3:
+  version "5.5.3"
+  resolved "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz#6cfc358984a1ef991e2518cb9ed4a778bbd3215d"
+  integrity sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==
+
+is-what@^5.2.0:
+  version "5.5.0"
+  resolved "https://registry.npmmirror.com/is-what/-/is-what-5.5.0.tgz#a3031815757cfe1f03fed990bf6355a2d3f628c4"
+  integrity sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==
+
+magic-string@^0.30.21:
+  version "0.30.21"
+  resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91"
+  integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==
+  dependencies:
+    "@jridgewell/sourcemap-codec" "^1.5.5"
+
+minimatch@^9.0.3:
+  version "9.0.9"
+  resolved "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz#9b0cb9fcb78087f6fd7eababe2511c4d3d60574e"
+  integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==
+  dependencies:
+    brace-expansion "^2.0.2"
+
+mitt@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
+  integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
+
+muggle-string@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz#3b366bd43b32f809dc20659534dd30e7c8a0d328"
+  integrity sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==
+
+nanoid@^3.3.11:
+  version "3.3.11"
+  resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
+  integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
+
+path-browserify@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
+  integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
+
+perfect-debounce@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a"
+  integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==
+
+picocolors@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
+  integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
+
+picomatch@^4.0.2, picomatch@^4.0.3:
+  version "4.0.4"
+  resolved "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589"
+  integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==
+
+pinia@^3.0.1:
+  version "3.0.4"
+  resolved "https://registry.npmmirror.com/pinia/-/pinia-3.0.4.tgz#75dde12784a61e34c1fa6abcd13c1a1061c360c0"
+  integrity sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==
+  dependencies:
+    "@vue/devtools-api" "^7.7.7"
+
+postcss@^8.5.3, postcss@^8.5.8:
+  version "8.5.8"
+  resolved "https://registry.npmmirror.com/postcss/-/postcss-8.5.8.tgz#6230ecc8fb02e7a0f6982e53990937857e13f399"
+  integrity sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==
+  dependencies:
+    nanoid "^3.3.11"
+    picocolors "^1.1.1"
+    source-map-js "^1.2.1"
+
+rfdc@^1.4.1:
+  version "1.4.1"
+  resolved "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca"
+  integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==
+
+rollup@^4.34.9:
+  version "4.60.0"
+  resolved "https://registry.npmmirror.com/rollup/-/rollup-4.60.0.tgz#d7d68c8cda873e96e08b2443505609b7e7be9eb8"
+  integrity sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==
+  dependencies:
+    "@types/estree" "1.0.8"
+  optionalDependencies:
+    "@rollup/rollup-android-arm-eabi" "4.60.0"
+    "@rollup/rollup-android-arm64" "4.60.0"
+    "@rollup/rollup-darwin-arm64" "4.60.0"
+    "@rollup/rollup-darwin-x64" "4.60.0"
+    "@rollup/rollup-freebsd-arm64" "4.60.0"
+    "@rollup/rollup-freebsd-x64" "4.60.0"
+    "@rollup/rollup-linux-arm-gnueabihf" "4.60.0"
+    "@rollup/rollup-linux-arm-musleabihf" "4.60.0"
+    "@rollup/rollup-linux-arm64-gnu" "4.60.0"
+    "@rollup/rollup-linux-arm64-musl" "4.60.0"
+    "@rollup/rollup-linux-loong64-gnu" "4.60.0"
+    "@rollup/rollup-linux-loong64-musl" "4.60.0"
+    "@rollup/rollup-linux-ppc64-gnu" "4.60.0"
+    "@rollup/rollup-linux-ppc64-musl" "4.60.0"
+    "@rollup/rollup-linux-riscv64-gnu" "4.60.0"
+    "@rollup/rollup-linux-riscv64-musl" "4.60.0"
+    "@rollup/rollup-linux-s390x-gnu" "4.60.0"
+    "@rollup/rollup-linux-x64-gnu" "4.60.0"
+    "@rollup/rollup-linux-x64-musl" "4.60.0"
+    "@rollup/rollup-openbsd-x64" "4.60.0"
+    "@rollup/rollup-openharmony-arm64" "4.60.0"
+    "@rollup/rollup-win32-arm64-msvc" "4.60.0"
+    "@rollup/rollup-win32-ia32-msvc" "4.60.0"
+    "@rollup/rollup-win32-x64-gnu" "4.60.0"
+    "@rollup/rollup-win32-x64-msvc" "4.60.0"
+    fsevents "~2.3.2"
+
+source-map-js@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
+  integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
+
+speakingurl@^14.0.1:
+  version "14.0.1"
+  resolved "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz#f37ec8ddc4ab98e9600c1c9ec324a8c48d772a53"
+  integrity sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==
+
+superjson@^2.2.2:
+  version "2.2.6"
+  resolved "https://registry.npmmirror.com/superjson/-/superjson-2.2.6.tgz#a223a3a988172a5f9656e2063fe5f733af40d099"
+  integrity sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==
+  dependencies:
+    copy-anything "^4"
+
+tinyglobby@^0.2.13:
+  version "0.2.15"
+  resolved "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2"
+  integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==
+  dependencies:
+    fdir "^6.5.0"
+    picomatch "^4.0.3"
+
+typescript@^5.8.2:
+  version "5.9.3"
+  resolved "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
+  integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
+
+vite@^6.2.2:
+  version "6.4.1"
+  resolved "https://registry.npmmirror.com/vite/-/vite-6.4.1.tgz#afbe14518cdd6887e240a4b0221ab6d0ce733f96"
+  integrity sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==
+  dependencies:
+    esbuild "^0.25.0"
+    fdir "^6.4.4"
+    picomatch "^4.0.2"
+    postcss "^8.5.3"
+    rollup "^4.34.9"
+    tinyglobby "^0.2.13"
+  optionalDependencies:
+    fsevents "~2.3.3"
+
+vscode-uri@^3.0.8:
+  version "3.1.0"
+  resolved "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz#dd09ec5a66a38b5c3fffc774015713496d14e09c"
+  integrity sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==
+
+vue-router@^4.5.0:
+  version "4.6.4"
+  resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz#a0a9cb9ef811a106d249e4bb9313d286718020d8"
+  integrity sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==
+  dependencies:
+    "@vue/devtools-api" "^6.6.4"
+
+vue-tsc@^2.2.8:
+  version "2.2.12"
+  resolved "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz#5f719b08ef7390a763c1a20169ca5c9d09d55688"
+  integrity sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==
+  dependencies:
+    "@volar/typescript" "2.4.15"
+    "@vue/language-core" "2.2.12"
+
+vue@^3.5.13:
+  version "3.5.31"
+  resolved "https://registry.npmmirror.com/vue/-/vue-3.5.31.tgz#ff20b2ca7893b4f9ae576a2064dbd3e2f5850118"
+  integrity sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==
+  dependencies:
+    "@vue/compiler-dom" "3.5.31"
+    "@vue/compiler-sfc" "3.5.31"
+    "@vue/runtime-dom" "3.5.31"
+    "@vue/server-renderer" "3.5.31"
+    "@vue/shared" "3.5.31"

+ 28 - 0
server/pom.xml

@@ -17,6 +17,11 @@
     <properties>
         <java.version>21</java.version>
         <spring-boot.version>3.4.4</spring-boot.version>
+        <maven.compiler.source>21</maven.compiler.source>
+        <maven.compiler.target>21</maven.compiler.target>
+        <maven.compiler.release>21</maven.compiler.release>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
     </properties>
 
     <dependencyManagement>
@@ -30,4 +35,27 @@
             </dependency>
         </dependencies>
     </dependencyManagement>
+
+    <build>
+        <pluginManagement>
+            <plugins>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-compiler-plugin</artifactId>
+                    <version>3.13.0</version>
+                    <configuration>
+                        <source>${maven.compiler.source}</source>
+                        <target>${maven.compiler.target}</target>
+                        <release>${maven.compiler.release}</release>
+                        <encoding>${project.build.sourceEncoding}</encoding>
+                    </configuration>
+                </plugin>
+                <plugin>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-maven-plugin</artifactId>
+                    <version>${spring-boot.version}</version>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+    </build>
 </project>

+ 5 - 0
server/version-management-service/pom.xml

@@ -15,6 +15,8 @@
     <description>Version management microservice for operator/admin platforms</description>
 
     <properties>
+        <maven.compiler.source>${java.version}</maven.compiler.source>
+        <maven.compiler.target>${java.version}</maven.compiler.target>
         <maven.compiler.release>${java.version}</maven.compiler.release>
     </properties>
 
@@ -62,8 +64,11 @@
                 <artifactId>spring-boot-maven-plugin</artifactId>
             </plugin>
             <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-compiler-plugin</artifactId>
                 <configuration>
+                    <source>${maven.compiler.source}</source>
+                    <target>${maven.compiler.target}</target>
                     <release>${java.version}</release>
                 </configuration>
             </plugin>

+ 34 - 6
server/version-management-service/src/main/java/com/xuqm/versionmanagement/config/DataInitializer.java

@@ -5,12 +5,14 @@ import com.xuqm.versionmanagement.persistence.entity.AccountEntity;
 import com.xuqm.versionmanagement.persistence.entity.ApplicationEntity;
 import com.xuqm.versionmanagement.persistence.entity.HookGroupEntity;
 import com.xuqm.versionmanagement.persistence.entity.HookUserEntity;
+import com.xuqm.versionmanagement.persistence.entity.PluginEntity;
 import com.xuqm.versionmanagement.persistence.entity.QuickSelectionEntity;
 import com.xuqm.versionmanagement.persistence.entity.ReleaseEntity;
 import com.xuqm.versionmanagement.persistence.repository.AccountRepository;
 import com.xuqm.versionmanagement.persistence.repository.ApplicationRepository;
 import com.xuqm.versionmanagement.persistence.repository.HookGroupRepository;
 import com.xuqm.versionmanagement.persistence.repository.HookUserRepository;
+import com.xuqm.versionmanagement.persistence.repository.PluginRepository;
 import com.xuqm.versionmanagement.persistence.repository.QuickSelectionRepository;
 import com.xuqm.versionmanagement.persistence.repository.ReleaseRepository;
 import java.time.LocalDateTime;
@@ -26,6 +28,7 @@ public class DataInitializer {
     CommandLineRunner seedVersionManagementData(
         AccountRepository accountRepository,
         ApplicationRepository applicationRepository,
+        PluginRepository pluginRepository,
         ReleaseRepository releaseRepository,
         HookUserRepository hookUserRepository,
         HookGroupRepository hookGroupRepository,
@@ -41,6 +44,7 @@ public class DataInitializer {
             main.setAccountName("星云运营中心");
             main.setContactName("林青");
             main.setEmail("ops@nebula.example");
+            main.setPassword("123456");
             main.setPhone("13800138000");
             main.setType(PlatformData.AccountType.MAIN);
             main.setStatus(PlatformData.AccountStatus.ACTIVE);
@@ -52,6 +56,7 @@ public class DataInitializer {
             sub.setAccountName("星云发布子账号");
             sub.setContactName("苏宁");
             sub.setEmail("release@nebula.example");
+            sub.setPassword("123456");
             sub.setPhone("13900139000");
             sub.setType(PlatformData.AccountType.SUB);
             sub.setStatus(PlatformData.AccountStatus.ACTIVE);
@@ -65,25 +70,42 @@ public class DataInitializer {
             app.setName("宿主 App");
             app.setPackageName("com.xuqm.sample");
             app.setPluginPackageName("com.xuqm.plugin.ui");
+            app.setDescription("Android 宿主应用,支持登录、检查更新、插件加载");
             app.setPluginManagementEnabled(true);
             app.setBusinessModules(List.of("IM", "PUSH", "VERSION"));
             app.setCreatedAt(LocalDateTime.of(2026, 3, 27, 9, 0));
             applicationRepository.save(app);
 
+            PluginEntity plugin = new PluginEntity();
+            plugin.setId("PLG-001");
+            plugin.setAppId(app.getId());
+            plugin.setName("UI 插件");
+            plugin.setPackageName("com.xuqm.plugin.ui");
+            plugin.setEntryActivity("com.xuqm.plugin.ui.PluginUiActivity");
+            plugin.setDescription("演示插件,支持宿主启动和独立安装");
+            plugin.setEnabled(true);
+            plugin.setCreatedAt(LocalDateTime.of(2026, 3, 27, 9, 5));
+            pluginRepository.save(plugin);
+
             releaseRepository.saveAll(List.of(
-                release("REL-APP-001", app.getId(), PlatformData.PackageType.APP, "com.xuqm.sample", 2, "0.2.0",
+                release("REL-APP-001", app.getId(), null, PlatformData.PackageType.APP, "com.xuqm.sample", 2, "0.2.0",
                     "发现新版本", "1. 新增统一下载管理\n2. 新增插件版本管理\n3. 优化宿主更新流程",
-                    "http://192.168.116.9:10223/app.apk", null, false, "sample-app-release-v0.2.0.apk",
+                    "http://192.168.116.9:10223/app.apk", null, null, null, false, "sample-app-release-v0.2.0.apk",
                     PlatformData.ReleaseStatus.PUBLISHED, "FULL", null, List.of(), List.of(), List.of(),
                     LocalDateTime.of(2026, 3, 27, 9, 30), LocalDateTime.of(2026, 3, 27, 9, 45)),
-                release("REL-PLUGIN-001", app.getId(), PlatformData.PackageType.PLUGIN, "com.xuqm.plugin.ui", 2, "0.2.0",
+                release("REL-PLUGIN-001", app.getId(), plugin.getId(), PlatformData.PackageType.PLUGIN, "com.xuqm.plugin.ui", 2, "0.2.0",
                     "插件 UI 更新", "1. 修复插件页面展示问题\n2. 优化宿主拉起体验",
-                    "http://192.168.116.9:10223/plugin-ui-release.apk", "com.xuqm.plugin.ui.PluginUiActivity", false, "plugin-ui-release-v0.2.0.apk",
+                    "http://192.168.116.9:10223/plugin-ui-release.apk", "com.xuqm.plugin.ui.PluginUiActivity", 1, "0.1.0", false, "plugin-ui-release-v0.2.0.apk",
                     PlatformData.ReleaseStatus.PUBLISHED, "FULL", null, List.of(), List.of(), List.of(),
                     LocalDateTime.of(2026, 3, 27, 9, 35), LocalDateTime.of(2026, 3, 27, 9, 50)),
-                release("REL-APP-002", app.getId(), PlatformData.PackageType.APP, "com.xuqm.sample", 3, "0.3.0-beta",
+                release("REL-PLUGIN-002", app.getId(), plugin.getId(), PlatformData.PackageType.PLUGIN, "com.xuqm.plugin.ui", 5, "0.5.0",
+                    "插件高版本", "1. 新增高版本插件能力\n2. 要求宿主至少升级到 0.2.0",
+                    "http://192.168.116.9:10223/plugin-ui-v0.5.0.apk", "com.xuqm.plugin.ui.PluginUiActivity", 2, "0.2.0", false, "plugin-ui-release-v0.5.0.apk",
+                    PlatformData.ReleaseStatus.PUBLISHED, "FULL", null, List.of(), List.of(), List.of(),
+                    LocalDateTime.of(2026, 3, 27, 10, 5), LocalDateTime.of(2026, 3, 27, 10, 18)),
+                release("REL-APP-002", app.getId(), null, PlatformData.PackageType.APP, "com.xuqm.sample", 3, "0.3.0-beta",
                     "0.3.0 灰度测试", "1. 新增版本平台灰度能力\n2. 支持用户分组圈选",
-                    "http://192.168.116.9:10223/app-beta.apk", null, false, "sample-app-release-v0.3.0-beta.apk",
+                    "http://192.168.116.9:10223/app-beta.apk", null, null, null, false, "sample-app-release-v0.3.0-beta.apk",
                     PlatformData.ReleaseStatus.GRAYSCALE, "GRAY", "user-platform-gray-hook", List.of("beta-core"), List.of("vip-seed"), List.of("USER-1001"),
                     LocalDateTime.of(2026, 3, 27, 10, 0), LocalDateTime.of(2026, 3, 27, 10, 15))
             ));
@@ -112,6 +134,7 @@ public class DataInitializer {
     private ReleaseEntity release(
         String id,
         String appId,
+        String pluginId,
         PlatformData.PackageType packageType,
         String packageName,
         int versionCode,
@@ -120,6 +143,8 @@ public class DataInitializer {
         String changelog,
         String downloadUrl,
         String entryActivity,
+        Integer minHostVersionCode,
+        String minHostVersionName,
         boolean forceUpdate,
         String uploadedFileName,
         PlatformData.ReleaseStatus status,
@@ -134,6 +159,7 @@ public class DataInitializer {
         ReleaseEntity entity = new ReleaseEntity();
         entity.setId(id);
         entity.setAppId(appId);
+        entity.setPluginId(pluginId);
         entity.setPackageType(packageType);
         entity.setPackageName(packageName);
         entity.setVersionCode(versionCode);
@@ -142,6 +168,8 @@ public class DataInitializer {
         entity.setChangelog(changelog);
         entity.setDownloadUrl(downloadUrl);
         entity.setEntryActivity(entryActivity);
+        entity.setMinHostVersionCode(minHostVersionCode);
+        entity.setMinHostVersionName(minHostVersionName);
         entity.setForceUpdate(forceUpdate);
         entity.setUploadedFileName(uploadedFileName);
         entity.setStatus(status);

+ 14 - 2
server/version-management-service/src/main/java/com/xuqm/versionmanagement/controller/CompatibilityUpdateController.java

@@ -43,15 +43,27 @@ public class CompatibilityUpdateController {
     @GetMapping("/api/v1/updates/plugin/latest")
     public ApiResponse<Map<String, Object>> latestPlugin(
         @RequestParam String packageName,
-        @RequestParam(required = false) String userId
+        @RequestParam(required = false) String userId,
+        @RequestParam(required = false) Integer hostVersionCode
     ) {
-        PlatformData.ReleaseRecord release = versionManagementService.getLatestPluginRelease(packageName, userId);
+        PlatformData.ReleaseRecord release = versionManagementService.getLatestPluginRelease(packageName, userId, hostVersionCode);
         Map<String, Object> payload = new LinkedHashMap<>();
         payload.put("packageName", release.getPackageName());
         payload.put("versionCode", release.getVersionCode());
         payload.put("versionName", release.getVersionName());
         payload.put("downloadUrl", release.getDownloadUrl());
         payload.put("entryActivity", release.getEntryActivity());
+        payload.put("minHostVersionCode", release.getMinHostVersionCode());
+        payload.put("minHostVersionName", release.getMinHostVersionName());
         return ApiResponse.success(payload);
     }
+
+    @GetMapping("/api/v1/updates/plugins/catalog")
+    public ApiResponse<java.util.List<VersionManagementService.PluginCatalogItem>> pluginCatalog(
+        @RequestParam String appPackageName,
+        @RequestParam(required = false) String userId,
+        @RequestParam long hostVersionCode
+    ) {
+        return ApiResponse.success(versionManagementService.getPluginCatalog(appPackageName, userId, hostVersionCode));
+    }
 }

+ 159 - 10
server/version-management-service/src/main/java/com/xuqm/versionmanagement/controller/OpsVersionController.java

@@ -5,7 +5,6 @@ import com.xuqm.versionmanagement.model.PlatformData;
 import com.xuqm.versionmanagement.service.UserHookService;
 import com.xuqm.versionmanagement.service.VersionManagementService;
 import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.NotNull;
 import java.util.List;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -19,7 +18,7 @@ import org.springframework.web.bind.annotation.RestController;
 
 @Validated
 @RestController
-@RequestMapping("/api/v1/ops/version")
+@RequestMapping("/api/v1/ops")
 public class OpsVersionController {
 
     private final VersionManagementService versionManagementService;
@@ -30,12 +29,46 @@ public class OpsVersionController {
         this.userHookService = userHookService;
     }
 
-    @GetMapping("/applications")
+    @GetMapping("/apps")
     public ApiResponse<List<VersionManagementService.ApplicationDetail>> listApplications() {
         return ApiResponse.success(versionManagementService.listApplications());
     }
 
-    @PutMapping("/applications/{appId}/plugin-management")
+    @PostMapping("/apps")
+    public ApiResponse<PlatformData.ApplicationConfig> createApplication(@RequestBody @Validated AppRequest request) {
+        PlatformData.ApplicationConfig application = versionManagementService.createApplication(
+            new VersionManagementService.CreateApplicationCommand(
+                request.name(),
+                request.packageName(),
+                request.pluginPackageName(),
+                request.description(),
+                request.pluginManagementEnabled(),
+                request.businessModules()
+            )
+        );
+        return ApiResponse.success(application, "应用创建成功");
+    }
+
+    @PutMapping("/apps/{appId}")
+    public ApiResponse<PlatformData.ApplicationConfig> updateApplication(
+        @PathVariable String appId,
+        @RequestBody @Validated AppRequest request
+    ) {
+        PlatformData.ApplicationConfig application = versionManagementService.updateApplication(
+            appId,
+            new VersionManagementService.UpdateApplicationCommand(
+                request.name(),
+                request.packageName(),
+                request.pluginPackageName(),
+                request.description(),
+                request.pluginManagementEnabled(),
+                request.businessModules()
+            )
+        );
+        return ApiResponse.success(application, "应用信息已更新");
+    }
+
+    @PutMapping("/apps/{appId}/plugin-management")
     public ApiResponse<PlatformData.ApplicationConfig> togglePluginManagement(
         @PathVariable String appId,
         @RequestBody TogglePluginManagementRequest request
@@ -44,7 +77,12 @@ public class OpsVersionController {
         return ApiResponse.success(config, "插件化能力已更新");
     }
 
-    @PostMapping("/applications/{appId}/releases/upload")
+    @GetMapping("/apps/{appId}/packages")
+    public ApiResponse<List<PlatformData.ReleaseRecord>> listAppPackages(@PathVariable String appId) {
+        return ApiResponse.success(versionManagementService.listAppPackages(appId));
+    }
+
+    @PostMapping("/apps/{appId}/packages")
     public ApiResponse<PlatformData.ReleaseRecord> uploadRelease(
         @PathVariable String appId,
         @RequestBody @Validated UploadReleaseRequest request
@@ -52,13 +90,16 @@ public class OpsVersionController {
         PlatformData.ReleaseRecord release = versionManagementService.uploadRelease(
             appId,
             new VersionManagementService.UploadReleaseCommand(
-                request.packageType(),
+                PlatformData.PackageType.APP,
+                request.packageName(),
                 request.versionCode(),
                 request.versionName(),
                 request.title(),
                 request.changelog(),
                 request.downloadUrl(),
                 request.entryActivity(),
+                null,
+                null,
                 request.forceUpdate(),
                 request.uploadedFileName()
             )
@@ -66,14 +107,79 @@ public class OpsVersionController {
         return ApiResponse.success(release, "版本包已上传");
     }
 
-    @PostMapping("/applications/{appId}/releases/{releaseId}/publish")
-    public ApiResponse<PlatformData.ReleaseRecord> publishRelease(
+    @GetMapping("/apps/{appId}/plugins")
+    public ApiResponse<List<PlatformData.PluginConfig>> listPlugins(@PathVariable String appId) {
+        return ApiResponse.success(versionManagementService.listPlugins(appId));
+    }
+
+    @PostMapping("/apps/{appId}/plugins")
+    public ApiResponse<PlatformData.PluginConfig> createPlugin(
         @PathVariable String appId,
+        @RequestBody @Validated PluginRequest request
+    ) {
+        PlatformData.PluginConfig plugin = versionManagementService.createPlugin(
+            appId,
+            new VersionManagementService.CreatePluginCommand(
+                request.name(),
+                request.packageName(),
+                request.entryActivity(),
+                request.description()
+            )
+        );
+        return ApiResponse.success(plugin, "插件创建成功");
+    }
+
+    @PutMapping("/plugins/{pluginId}")
+    public ApiResponse<PlatformData.PluginConfig> updatePlugin(
+        @PathVariable String pluginId,
+        @RequestBody @Validated PluginUpdateRequest request
+    ) {
+        PlatformData.PluginConfig plugin = versionManagementService.updatePlugin(
+            pluginId,
+            new VersionManagementService.UpdatePluginCommand(
+                request.name(),
+                request.packageName(),
+                request.entryActivity(),
+                request.description(),
+                request.enabled()
+            )
+        );
+        return ApiResponse.success(plugin, "插件信息已更新");
+    }
+
+    @GetMapping("/plugins/{pluginId}/packages")
+    public ApiResponse<List<PlatformData.ReleaseRecord>> listPluginPackages(@PathVariable String pluginId) {
+        return ApiResponse.success(versionManagementService.listPluginPackages(pluginId));
+    }
+
+    @PostMapping("/plugins/{pluginId}/packages")
+    public ApiResponse<PlatformData.ReleaseRecord> uploadPluginRelease(
+        @PathVariable String pluginId,
+        @RequestBody @Validated UploadPluginReleaseRequest request
+    ) {
+        PlatformData.ReleaseRecord release = versionManagementService.uploadPluginRelease(
+            pluginId,
+            new VersionManagementService.UploadPluginReleaseCommand(
+                request.versionCode(),
+                request.versionName(),
+                request.title(),
+                request.changelog(),
+                request.downloadUrl(),
+                request.entryActivity(),
+                request.minHostVersionCode(),
+                request.minHostVersionName(),
+                request.uploadedFileName()
+            )
+        );
+        return ApiResponse.success(release, "插件安装包已上传");
+    }
+
+    @PostMapping("/packages/{releaseId}/publish")
+    public ApiResponse<PlatformData.ReleaseRecord> publishRelease(
         @PathVariable String releaseId,
         @RequestBody PublishReleaseRequest request
     ) {
         PlatformData.ReleaseRecord release = versionManagementService.publishRelease(
-            appId,
             releaseId,
             new VersionManagementService.PublishReleaseCommand(
                 request.grayPublish(),
@@ -105,11 +211,24 @@ public class OpsVersionController {
         return ApiResponse.success(userHookService.getAudienceBundle(null, null, null).quickSelections());
     }
 
+    public record AppRequest(
+        @NotBlank(message = "不能为空") String name,
+        @NotBlank(message = "不能为空") String packageName,
+        String pluginPackageName,
+        String description,
+        boolean pluginManagementEnabled,
+        List<String> businessModules
+    ) {
+        public List<String> businessModules() {
+            return businessModules == null || businessModules.isEmpty() ? List.of("IM", "PUSH", "VERSION") : businessModules;
+        }
+    }
+
     public record TogglePluginManagementRequest(boolean enabled) {
     }
 
     public record UploadReleaseRequest(
-        @NotNull(message = "不能为空") PlatformData.PackageType packageType,
+        @NotBlank(message = "不能为空") String packageName,
         int versionCode,
         @NotBlank(message = "不能为空") String versionName,
         @NotBlank(message = "不能为空") String title,
@@ -121,6 +240,36 @@ public class OpsVersionController {
     ) {
     }
 
+    public record PluginRequest(
+        @NotBlank(message = "不能为空") String name,
+        @NotBlank(message = "不能为空") String packageName,
+        String entryActivity,
+        String description
+    ) {
+    }
+
+    public record PluginUpdateRequest(
+        @NotBlank(message = "不能为空") String name,
+        @NotBlank(message = "不能为空") String packageName,
+        String entryActivity,
+        String description,
+        boolean enabled
+    ) {
+    }
+
+    public record UploadPluginReleaseRequest(
+        int versionCode,
+        @NotBlank(message = "不能为空") String versionName,
+        @NotBlank(message = "不能为空") String title,
+        String changelog,
+        @NotBlank(message = "不能为空") String downloadUrl,
+        String entryActivity,
+        Integer minHostVersionCode,
+        String minHostVersionName,
+        @NotBlank(message = "不能为空") String uploadedFileName
+    ) {
+    }
+
     public record PublishReleaseRequest(
         boolean grayPublish,
         String hookName,

+ 16 - 3
server/version-management-service/src/main/java/com/xuqm/versionmanagement/controller/PublicAccountController.java

@@ -31,10 +31,16 @@ public class PublicAccountController {
                 request.accountName(),
                 request.contactName(),
                 request.email(),
-                request.phone()
+                request.phone(),
+                request.password()
             )
         );
-        return ApiResponse.success(account, "运营平台注册成功,等待管理平台审核");
+        return ApiResponse.success(account, "运营平台注册成功");
+    }
+
+    @PostMapping("/api/v1/open/accounts/login")
+    public ApiResponse<AccountService.LoginAccountView> login(@RequestBody @Validated LoginRequest request) {
+        return ApiResponse.success(accountService.login(request.email(), request.password()), "登录成功");
     }
 
     @PostMapping("/api/v1/ops/accounts/{accountId}/sub-accounts")
@@ -59,7 +65,14 @@ public class PublicAccountController {
         @NotBlank(message = "不能为空") String accountName,
         @NotBlank(message = "不能为空") String contactName,
         @Email(message = "格式不正确") String email,
-        @NotBlank(message = "不能为空") String phone
+        @NotBlank(message = "不能为空") String phone,
+        @NotBlank(message = "不能为空") String password
+    ) {
+    }
+
+    public record LoginRequest(
+        @Email(message = "格式不正确") String email,
+        @NotBlank(message = "不能为空") String password
     ) {
     }
 

+ 120 - 0
server/version-management-service/src/main/java/com/xuqm/versionmanagement/model/PlatformData.java

@@ -8,6 +8,7 @@ public class PlatformData {
 
     private List<Account> accounts = new ArrayList<>();
     private List<ApplicationConfig> applications = new ArrayList<>();
+    private List<PluginConfig> plugins = new ArrayList<>();
     private List<ReleaseRecord> releases = new ArrayList<>();
     private List<HookUser> hookUsers = new ArrayList<>();
     private List<HookGroup> hookGroups = new ArrayList<>();
@@ -29,6 +30,14 @@ public class PlatformData {
         this.applications = applications;
     }
 
+    public List<PluginConfig> getPlugins() {
+        return plugins;
+    }
+
+    public void setPlugins(List<PluginConfig> plugins) {
+        this.plugins = plugins;
+    }
+
     public List<ReleaseRecord> getReleases() {
         return releases;
     }
@@ -181,6 +190,7 @@ public class PlatformData {
         private String name;
         private String packageName;
         private String pluginPackageName;
+        private String description;
         private boolean pluginManagementEnabled;
         private List<String> businessModules = new ArrayList<>();
         private LocalDateTime createdAt;
@@ -217,6 +227,14 @@ public class PlatformData {
             this.pluginPackageName = pluginPackageName;
         }
 
+        public String getDescription() {
+            return description;
+        }
+
+        public void setDescription(String description) {
+            this.description = description;
+        }
+
         public boolean isPluginManagementEnabled() {
             return pluginManagementEnabled;
         }
@@ -242,9 +260,85 @@ public class PlatformData {
         }
     }
 
+    public static class PluginConfig {
+        private String id;
+        private String appId;
+        private String name;
+        private String packageName;
+        private String entryActivity;
+        private String description;
+        private boolean enabled;
+        private LocalDateTime createdAt;
+
+        public String getId() {
+            return id;
+        }
+
+        public void setId(String id) {
+            this.id = id;
+        }
+
+        public String getAppId() {
+            return appId;
+        }
+
+        public void setAppId(String appId) {
+            this.appId = appId;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public String getPackageName() {
+            return packageName;
+        }
+
+        public void setPackageName(String packageName) {
+            this.packageName = packageName;
+        }
+
+        public String getEntryActivity() {
+            return entryActivity;
+        }
+
+        public void setEntryActivity(String entryActivity) {
+            this.entryActivity = entryActivity;
+        }
+
+        public String getDescription() {
+            return description;
+        }
+
+        public void setDescription(String description) {
+            this.description = description;
+        }
+
+        public boolean isEnabled() {
+            return enabled;
+        }
+
+        public void setEnabled(boolean enabled) {
+            this.enabled = enabled;
+        }
+
+        public LocalDateTime getCreatedAt() {
+            return createdAt;
+        }
+
+        public void setCreatedAt(LocalDateTime createdAt) {
+            this.createdAt = createdAt;
+        }
+    }
+
     public static class ReleaseRecord {
         private String id;
         private String appId;
+        private String pluginId;
         private PackageType packageType;
         private String packageName;
         private int versionCode;
@@ -253,6 +347,8 @@ public class PlatformData {
         private String changelog;
         private String downloadUrl;
         private String entryActivity;
+        private Integer minHostVersionCode;
+        private String minHostVersionName;
         private boolean forceUpdate;
         private String uploadedFileName;
         private ReleaseStatus status;
@@ -277,6 +373,14 @@ public class PlatformData {
             this.appId = appId;
         }
 
+        public String getPluginId() {
+            return pluginId;
+        }
+
+        public void setPluginId(String pluginId) {
+            this.pluginId = pluginId;
+        }
+
         public PackageType getPackageType() {
             return packageType;
         }
@@ -341,6 +445,22 @@ public class PlatformData {
             this.entryActivity = entryActivity;
         }
 
+        public Integer getMinHostVersionCode() {
+            return minHostVersionCode;
+        }
+
+        public void setMinHostVersionCode(Integer minHostVersionCode) {
+            this.minHostVersionCode = minHostVersionCode;
+        }
+
+        public String getMinHostVersionName() {
+            return minHostVersionName;
+        }
+
+        public void setMinHostVersionName(String minHostVersionName) {
+            this.minHostVersionName = minHostVersionName;
+        }
+
         public boolean isForceUpdate() {
             return forceUpdate;
         }

+ 11 - 0
server/version-management-service/src/main/java/com/xuqm/versionmanagement/persistence/entity/AccountEntity.java

@@ -27,6 +27,9 @@ public class AccountEntity {
     @Column(nullable = false, length = 128)
     private String email;
 
+    @Column(nullable = false, length = 128)
+    private String password;
+
     @Column(nullable = false, length = 32)
     private String phone;
 
@@ -88,6 +91,14 @@ public class AccountEntity {
         this.phone = phone;
     }
 
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
     public PlatformData.AccountType getType() {
         return type;
     }

+ 11 - 0
server/version-management-service/src/main/java/com/xuqm/versionmanagement/persistence/entity/ApplicationEntity.java

@@ -24,6 +24,9 @@ public class ApplicationEntity {
     @Column(length = 128)
     private String pluginPackageName;
 
+    @Column(length = 512)
+    private String description;
+
     @Column(nullable = false)
     private boolean pluginManagementEnabled;
 
@@ -66,6 +69,14 @@ public class ApplicationEntity {
         this.pluginPackageName = pluginPackageName;
     }
 
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
     public boolean isPluginManagementEnabled() {
         return pluginManagementEnabled;
     }

+ 100 - 0
server/version-management-service/src/main/java/com/xuqm/versionmanagement/persistence/entity/PluginEntity.java

@@ -0,0 +1,100 @@
+package com.xuqm.versionmanagement.persistence.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "vm_plugin")
+public class PluginEntity {
+
+    @Id
+    private String id;
+
+    @Column(nullable = false, length = 64)
+    private String appId;
+
+    @Column(nullable = false, length = 128)
+    private String name;
+
+    @Column(nullable = false, length = 128)
+    private String packageName;
+
+    @Column(length = 256)
+    private String entryActivity;
+
+    @Column(length = 512)
+    private String description;
+
+    @Column(nullable = false)
+    private boolean enabled;
+
+    @Column(nullable = false)
+    private LocalDateTime createdAt;
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getAppId() {
+        return appId;
+    }
+
+    public void setAppId(String appId) {
+        this.appId = appId;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getPackageName() {
+        return packageName;
+    }
+
+    public void setPackageName(String packageName) {
+        this.packageName = packageName;
+    }
+
+    public String getEntryActivity() {
+        return entryActivity;
+    }
+
+    public void setEntryActivity(String entryActivity) {
+        this.entryActivity = entryActivity;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+
+    public LocalDateTime getCreatedAt() {
+        return createdAt;
+    }
+
+    public void setCreatedAt(LocalDateTime createdAt) {
+        this.createdAt = createdAt;
+    }
+}

+ 32 - 0
server/version-management-service/src/main/java/com/xuqm/versionmanagement/persistence/entity/ReleaseEntity.java

@@ -21,6 +21,9 @@ public class ReleaseEntity {
     @Column(nullable = false, length = 64)
     private String appId;
 
+    @Column(length = 64)
+    private String pluginId;
+
     @Enumerated(EnumType.STRING)
     @Column(nullable = false, length = 16)
     private PlatformData.PackageType packageType;
@@ -46,6 +49,11 @@ public class ReleaseEntity {
     @Column(length = 256)
     private String entryActivity;
 
+    private Integer minHostVersionCode;
+
+    @Column(length = 64)
+    private String minHostVersionName;
+
     @Column(nullable = false)
     private boolean forceUpdate;
 
@@ -95,6 +103,14 @@ public class ReleaseEntity {
         this.appId = appId;
     }
 
+    public String getPluginId() {
+        return pluginId;
+    }
+
+    public void setPluginId(String pluginId) {
+        this.pluginId = pluginId;
+    }
+
     public PlatformData.PackageType getPackageType() {
         return packageType;
     }
@@ -159,6 +175,22 @@ public class ReleaseEntity {
         this.entryActivity = entryActivity;
     }
 
+    public Integer getMinHostVersionCode() {
+        return minHostVersionCode;
+    }
+
+    public void setMinHostVersionCode(Integer minHostVersionCode) {
+        this.minHostVersionCode = minHostVersionCode;
+    }
+
+    public String getMinHostVersionName() {
+        return minHostVersionName;
+    }
+
+    public void setMinHostVersionName(String minHostVersionName) {
+        this.minHostVersionName = minHostVersionName;
+    }
+
     public boolean isForceUpdate() {
         return forceUpdate;
     }

+ 2 - 0
server/version-management-service/src/main/java/com/xuqm/versionmanagement/persistence/repository/AccountRepository.java

@@ -10,4 +10,6 @@ public interface AccountRepository extends JpaRepository<AccountEntity, String>
     List<AccountEntity> findByTypeOrderByCreatedAtAsc(PlatformData.AccountType type);
 
     List<AccountEntity> findByParentAccountIdOrderByCreatedAtAsc(String parentAccountId);
+
+    java.util.Optional<AccountEntity> findByEmailAndPassword(String email, String password);
 }

+ 10 - 0
server/version-management-service/src/main/java/com/xuqm/versionmanagement/persistence/repository/PluginRepository.java

@@ -0,0 +1,10 @@
+package com.xuqm.versionmanagement.persistence.repository;
+
+import com.xuqm.versionmanagement.persistence.entity.PluginEntity;
+import java.util.List;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface PluginRepository extends JpaRepository<PluginEntity, String> {
+
+    List<PluginEntity> findByAppIdOrderByCreatedAtAsc(String appId);
+}

+ 2 - 0
server/version-management-service/src/main/java/com/xuqm/versionmanagement/persistence/repository/ReleaseRepository.java

@@ -10,6 +10,8 @@ public interface ReleaseRepository extends JpaRepository<ReleaseEntity, String>
 
     List<ReleaseEntity> findByAppIdOrderByUploadedAtDesc(String appId);
 
+    List<ReleaseEntity> findByPluginIdOrderByUploadedAtDesc(String pluginId);
+
     List<ReleaseEntity> findByPackageNameAndPackageType(String packageName, PlatformData.PackageType packageType);
 
     Optional<ReleaseEntity> findByIdAndAppId(String id, String appId);

+ 13 - 2
server/version-management-service/src/main/java/com/xuqm/versionmanagement/service/AccountService.java

@@ -27,14 +27,22 @@ public class AccountService {
         entity.setAccountName(command.accountName());
         entity.setContactName(command.contactName());
         entity.setEmail(command.email());
+        entity.setPassword(command.password());
         entity.setPhone(command.phone());
         entity.setType(PlatformData.AccountType.MAIN);
-        entity.setStatus(PlatformData.AccountStatus.PENDING);
+        entity.setStatus(PlatformData.AccountStatus.ACTIVE);
         entity.setPermissions(List.of("version:read", "version:write", "release:publish", "subaccount:grant"));
         entity.setCreatedAt(LocalDateTime.now());
         return platformMapper.toAccount(accountRepository.save(entity));
     }
 
+    public LoginAccountView login(String email, String password) {
+        AccountEntity entity = accountRepository.findByEmailAndPassword(email, password)
+            .orElseThrow(() -> new IllegalArgumentException("账号或密码错误"));
+        PlatformData.Account account = platformMapper.toAccount(entity);
+        return new LoginAccountView(account, entity.getType() == PlatformData.AccountType.SUB ? entity.getParentAccountId() : entity.getId());
+    }
+
     @Transactional
     public PlatformData.Account createSubAccount(String parentAccountId, CreateSubAccountCommand command) {
         findAccount(parentAccountId);
@@ -90,7 +98,7 @@ public class AccountService {
         return prefix + "-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
     }
 
-    public record RegisterAccountCommand(String accountName, String contactName, String email, String phone) {
+    public record RegisterAccountCommand(String accountName, String contactName, String email, String phone, String password) {
     }
 
     public record CreateSubAccountCommand(
@@ -104,4 +112,7 @@ public class AccountService {
 
     public record AccountView(PlatformData.Account mainAccount, List<PlatformData.Account> subAccounts) {
     }
+
+    public record LoginAccountView(PlatformData.Account account, String tenantAccountId) {
+    }
 }

+ 29 - 2
server/version-management-service/src/main/java/com/xuqm/versionmanagement/service/PlatformMapper.java

@@ -5,6 +5,7 @@ import com.xuqm.versionmanagement.persistence.entity.AccountEntity;
 import com.xuqm.versionmanagement.persistence.entity.ApplicationEntity;
 import com.xuqm.versionmanagement.persistence.entity.HookGroupEntity;
 import com.xuqm.versionmanagement.persistence.entity.HookUserEntity;
+import com.xuqm.versionmanagement.persistence.entity.PluginEntity;
 import com.xuqm.versionmanagement.persistence.entity.QuickSelectionEntity;
 import com.xuqm.versionmanagement.persistence.entity.ReleaseEntity;
 import java.util.List;
@@ -34,16 +35,31 @@ public class PlatformMapper {
         config.setName(entity.getName());
         config.setPackageName(entity.getPackageName());
         config.setPluginPackageName(entity.getPluginPackageName());
+        config.setDescription(entity.getDescription());
         config.setPluginManagementEnabled(entity.isPluginManagementEnabled());
         config.setBusinessModules(entity.getBusinessModules());
         config.setCreatedAt(entity.getCreatedAt());
         return config;
     }
 
+    public PlatformData.PluginConfig toPlugin(PluginEntity entity) {
+        PlatformData.PluginConfig plugin = new PlatformData.PluginConfig();
+        plugin.setId(entity.getId());
+        plugin.setAppId(entity.getAppId());
+        plugin.setName(entity.getName());
+        plugin.setPackageName(entity.getPackageName());
+        plugin.setEntryActivity(entity.getEntryActivity());
+        plugin.setDescription(entity.getDescription());
+        plugin.setEnabled(entity.isEnabled());
+        plugin.setCreatedAt(entity.getCreatedAt());
+        return plugin;
+    }
+
     public PlatformData.ReleaseRecord toRelease(ReleaseEntity entity) {
         PlatformData.ReleaseRecord release = new PlatformData.ReleaseRecord();
         release.setId(entity.getId());
         release.setAppId(entity.getAppId());
+        release.setPluginId(entity.getPluginId());
         release.setPackageType(entity.getPackageType());
         release.setPackageName(entity.getPackageName());
         release.setVersionCode(entity.getVersionCode());
@@ -52,6 +68,8 @@ public class PlatformMapper {
         release.setChangelog(entity.getChangelog());
         release.setDownloadUrl(entity.getDownloadUrl());
         release.setEntryActivity(entity.getEntryActivity());
+        release.setMinHostVersionCode(entity.getMinHostVersionCode());
+        release.setMinHostVersionName(entity.getMinHostVersionName());
         release.setForceUpdate(entity.isForceUpdate());
         release.setUploadedFileName(entity.getUploadedFileName());
         release.setStatus(entity.getStatus());
@@ -98,18 +116,27 @@ public class PlatformMapper {
         return selection;
     }
 
-    public ReleaseEntity toReleaseEntity(String appId, PlatformData.ApplicationConfig app, VersionManagementService.UploadReleaseCommand command, String id) {
+    public ReleaseEntity toReleaseEntity(
+        String appId,
+        String pluginId,
+        PlatformData.ApplicationConfig app,
+        VersionManagementService.UploadReleaseCommand command,
+        String id
+    ) {
         ReleaseEntity entity = new ReleaseEntity();
         entity.setId(id);
         entity.setAppId(appId);
+        entity.setPluginId(pluginId);
         entity.setPackageType(command.packageType());
-        entity.setPackageName(command.packageType() == PlatformData.PackageType.PLUGIN ? app.getPluginPackageName() : app.getPackageName());
+        entity.setPackageName(command.packageName());
         entity.setVersionCode(command.versionCode());
         entity.setVersionName(command.versionName());
         entity.setTitle(command.title());
         entity.setChangelog(command.changelog());
         entity.setDownloadUrl(command.downloadUrl());
         entity.setEntryActivity(command.entryActivity());
+        entity.setMinHostVersionCode(command.minHostVersionCode());
+        entity.setMinHostVersionName(command.minHostVersionName());
         entity.setForceUpdate(command.forceUpdate());
         entity.setUploadedFileName(command.uploadedFileName());
         entity.setStatus(PlatformData.ReleaseStatus.DRAFT);

+ 239 - 8
server/version-management-service/src/main/java/com/xuqm/versionmanagement/service/VersionManagementService.java

@@ -2,8 +2,10 @@ package com.xuqm.versionmanagement.service;
 
 import com.xuqm.versionmanagement.model.PlatformData;
 import com.xuqm.versionmanagement.persistence.entity.ApplicationEntity;
+import com.xuqm.versionmanagement.persistence.entity.PluginEntity;
 import com.xuqm.versionmanagement.persistence.entity.ReleaseEntity;
 import com.xuqm.versionmanagement.persistence.repository.ApplicationRepository;
+import com.xuqm.versionmanagement.persistence.repository.PluginRepository;
 import com.xuqm.versionmanagement.persistence.repository.ReleaseRepository;
 import java.time.LocalDateTime;
 import java.util.Comparator;
@@ -16,17 +18,20 @@ import org.springframework.transaction.annotation.Transactional;
 public class VersionManagementService {
 
     private final ApplicationRepository applicationRepository;
+    private final PluginRepository pluginRepository;
     private final ReleaseRepository releaseRepository;
     private final UserHookService userHookService;
     private final PlatformMapper platformMapper;
 
     public VersionManagementService(
         ApplicationRepository applicationRepository,
+        PluginRepository pluginRepository,
         ReleaseRepository releaseRepository,
         UserHookService userHookService,
         PlatformMapper platformMapper
     ) {
         this.applicationRepository = applicationRepository;
+        this.pluginRepository = pluginRepository;
         this.releaseRepository = releaseRepository;
         this.userHookService = userHookService;
         this.platformMapper = platformMapper;
@@ -37,13 +42,43 @@ public class VersionManagementService {
             .map(platformMapper::toApplication)
             .map(app -> new ApplicationDetail(
                 app,
+                pluginRepository.findByAppIdOrderByCreatedAtAsc(app.getId()).stream()
+                    .map(platformMapper::toPlugin)
+                    .toList(),
                 releaseRepository.findByAppIdOrderByUploadedAtDesc(app.getId()).stream()
+                    .filter(release -> release.getPluginId() == null)
                     .map(platformMapper::toRelease)
                     .toList()
             ))
             .toList();
     }
 
+    @Transactional
+    public PlatformData.ApplicationConfig createApplication(CreateApplicationCommand command) {
+        ApplicationEntity entity = new ApplicationEntity();
+        entity.setId(nextId("APP"));
+        entity.setName(command.name());
+        entity.setPackageName(command.packageName());
+        entity.setPluginPackageName(command.pluginPackageName());
+        entity.setDescription(command.description());
+        entity.setPluginManagementEnabled(command.pluginManagementEnabled());
+        entity.setBusinessModules(command.businessModules());
+        entity.setCreatedAt(LocalDateTime.now());
+        return platformMapper.toApplication(applicationRepository.save(entity));
+    }
+
+    @Transactional
+    public PlatformData.ApplicationConfig updateApplication(String appId, UpdateApplicationCommand command) {
+        ApplicationEntity entity = findApplicationEntity(appId);
+        entity.setName(command.name());
+        entity.setPackageName(command.packageName());
+        entity.setPluginPackageName(command.pluginPackageName());
+        entity.setDescription(command.description());
+        entity.setPluginManagementEnabled(command.pluginManagementEnabled());
+        entity.setBusinessModules(command.businessModules());
+        return platformMapper.toApplication(applicationRepository.save(entity));
+    }
+
     @Transactional
     public PlatformData.ApplicationConfig togglePluginManagement(String appId, boolean enabled) {
         ApplicationEntity entity = findApplicationEntity(appId);
@@ -51,18 +86,93 @@ public class VersionManagementService {
         return platformMapper.toApplication(applicationRepository.save(entity));
     }
 
+    public List<PlatformData.ReleaseRecord> listAppPackages(String appId) {
+        findApplicationEntity(appId);
+        return releaseRepository.findByAppIdOrderByUploadedAtDesc(appId).stream()
+            .filter(item -> item.getPluginId() == null)
+            .map(platformMapper::toRelease)
+            .toList();
+    }
+
     @Transactional
     public PlatformData.ReleaseRecord uploadRelease(String appId, UploadReleaseCommand command) {
         PlatformData.ApplicationConfig app = platformMapper.toApplication(findApplicationEntity(appId));
-        ReleaseEntity entity = platformMapper.toReleaseEntity(appId, app, command, nextId("REL"));
+        ReleaseEntity entity = platformMapper.toReleaseEntity(appId, null, app, command, nextId("REL"));
         entity.setUploadedAt(LocalDateTime.now());
         return platformMapper.toRelease(releaseRepository.save(entity));
     }
 
+    public List<PlatformData.PluginConfig> listPlugins(String appId) {
+        findApplicationEntity(appId);
+        return pluginRepository.findByAppIdOrderByCreatedAtAsc(appId).stream()
+            .map(platformMapper::toPlugin)
+            .toList();
+    }
+
     @Transactional
-    public PlatformData.ReleaseRecord publishRelease(String appId, String releaseId, PublishReleaseCommand command) {
+    public PlatformData.PluginConfig createPlugin(String appId, CreatePluginCommand command) {
         findApplicationEntity(appId);
-        ReleaseEntity release = findReleaseEntity(appId, releaseId);
+        PluginEntity entity = new PluginEntity();
+        entity.setId(nextId("PLG"));
+        entity.setAppId(appId);
+        entity.setName(command.name());
+        entity.setPackageName(command.packageName());
+        entity.setEntryActivity(command.entryActivity());
+        entity.setDescription(command.description());
+        entity.setEnabled(true);
+        entity.setCreatedAt(LocalDateTime.now());
+        return platformMapper.toPlugin(pluginRepository.save(entity));
+    }
+
+    @Transactional
+    public PlatformData.PluginConfig updatePlugin(String pluginId, UpdatePluginCommand command) {
+        PluginEntity entity = findPluginEntity(pluginId);
+        entity.setName(command.name());
+        entity.setPackageName(command.packageName());
+        entity.setEntryActivity(command.entryActivity());
+        entity.setDescription(command.description());
+        entity.setEnabled(command.enabled());
+        return platformMapper.toPlugin(pluginRepository.save(entity));
+    }
+
+    public List<PlatformData.ReleaseRecord> listPluginPackages(String pluginId) {
+        findPluginEntity(pluginId);
+        return releaseRepository.findByPluginIdOrderByUploadedAtDesc(pluginId).stream()
+            .map(platformMapper::toRelease)
+            .toList();
+    }
+
+    @Transactional
+    public PlatformData.ReleaseRecord uploadPluginRelease(String pluginId, UploadPluginReleaseCommand command) {
+        PluginEntity plugin = findPluginEntity(pluginId);
+        PlatformData.ApplicationConfig app = platformMapper.toApplication(findApplicationEntity(plugin.getAppId()));
+        ReleaseEntity entity = platformMapper.toReleaseEntity(
+            plugin.getAppId(),
+            pluginId,
+            app,
+            new UploadReleaseCommand(
+                PlatformData.PackageType.PLUGIN,
+                plugin.getPackageName(),
+                command.versionCode(),
+                command.versionName(),
+                command.title(),
+                command.changelog(),
+                command.downloadUrl(),
+                command.entryActivity() == null || command.entryActivity().isBlank() ? plugin.getEntryActivity() : command.entryActivity(),
+                command.minHostVersionCode(),
+                command.minHostVersionName(),
+                false,
+                command.uploadedFileName()
+            ),
+            nextId("REL")
+        );
+        entity.setUploadedAt(LocalDateTime.now());
+        return platformMapper.toRelease(releaseRepository.save(entity));
+    }
+
+    @Transactional
+    public PlatformData.ReleaseRecord publishRelease(String releaseId, PublishReleaseCommand command) {
+        ReleaseEntity release = findReleaseEntity(releaseId);
         if (command.grayPublish()) {
             List<PlatformData.HookUser> matchedUsers = userHookService.findUsersByIds(command.userIds());
             if (!command.userIds().isEmpty() && matchedUsers.size() != command.userIds().size()) {
@@ -90,8 +200,41 @@ public class VersionManagementService {
         return selectLatestRelease(packageName, PlatformData.PackageType.APP, userId);
     }
 
-    public PlatformData.ReleaseRecord getLatestPluginRelease(String packageName, String userId) {
-        return selectLatestRelease(packageName, PlatformData.PackageType.PLUGIN, userId);
+    public PlatformData.ReleaseRecord getLatestPluginRelease(String packageName, String userId, Integer hostVersionCode) {
+        return selectLatestPluginRelease(packageName, userId, hostVersionCode, true);
+    }
+
+    public List<PluginCatalogItem> getPluginCatalog(String appPackageName, String userId, long hostVersionCode) {
+        ApplicationEntity app = applicationRepository.findAll().stream()
+            .filter(item -> appPackageName.equals(item.getPackageName()))
+            .findFirst()
+            .orElseThrow(() -> new IllegalArgumentException("应用不存在"));
+        PlatformData.HookUser user = userId == null ? null : userHookService.findUsersByIds(List.of(userId)).stream().findFirst().orElse(null);
+
+        return pluginRepository.findByAppIdOrderByCreatedAtAsc(app.getId()).stream()
+            .map(platformMapper::toPlugin)
+            .map(plugin -> {
+                List<PlatformData.ReleaseRecord> visibleReleases = releaseRepository.findByPluginIdOrderByUploadedAtDesc(plugin.getId()).stream()
+                    .map(platformMapper::toRelease)
+                    .filter(release -> release.getStatus() == PlatformData.ReleaseStatus.PUBLISHED
+                        || (release.getStatus() == PlatformData.ReleaseStatus.GRAYSCALE && userHookService.matchesGrayRule(user, release.getGrayRule())))
+                    .toList();
+
+                PlatformData.ReleaseRecord latest = visibleReleases.stream()
+                    .max(Comparator.comparingInt(PlatformData.ReleaseRecord::getVersionCode))
+                    .orElse(null);
+                PlatformData.ReleaseRecord compatible = visibleReleases.stream()
+                    .filter(release -> isHostCompatible(release, hostVersionCode))
+                    .max(Comparator.comparingInt(PlatformData.ReleaseRecord::getVersionCode))
+                    .orElse(null);
+
+                boolean blocked = latest != null && !isHostCompatible(latest, hostVersionCode);
+                String message = blocked
+                    ? "当前宿主版本过低,不能升级到插件最新版本"
+                    : compatible == null ? "暂无可安装插件版本" : "可升级或进入插件";
+                return new PluginCatalogItem(plugin, latest, compatible, blocked, message);
+            })
+            .toList();
     }
 
     private PlatformData.ReleaseRecord selectLatestRelease(String packageName, PlatformData.PackageType packageType, String userId) {
@@ -106,13 +249,35 @@ public class VersionManagementService {
             .orElseThrow(() -> new IllegalArgumentException("版本配置不存在"));
     }
 
+    private PlatformData.ReleaseRecord selectLatestPluginRelease(String packageName, String userId, Integer hostVersionCode, boolean compatibleOnly) {
+        PlatformData.HookUser user = userId == null ? null : userHookService.findUsersByIds(List.of(userId)).stream().findFirst().orElse(null);
+        return releaseRepository.findByPackageNameAndPackageType(packageName, PlatformData.PackageType.PLUGIN).stream()
+            .map(platformMapper::toRelease)
+            .filter(release -> release.getStatus() == PlatformData.ReleaseStatus.PUBLISHED
+                || (release.getStatus() == PlatformData.ReleaseStatus.GRAYSCALE && userHookService.matchesGrayRule(user, release.getGrayRule())))
+            .filter(release -> !compatibleOnly || isHostCompatible(release, hostVersionCode == null ? Long.MAX_VALUE : hostVersionCode.longValue()))
+            .max(Comparator.comparingInt(PlatformData.ReleaseRecord::getVersionCode)
+                .thenComparing(PlatformData.ReleaseRecord::getPublishedAt, Comparator.nullsLast(Comparator.naturalOrder())))
+            .orElseThrow(() -> new IllegalArgumentException("插件版本配置不存在"));
+    }
+
+    private boolean isHostCompatible(PlatformData.ReleaseRecord release, long hostVersionCode) {
+        Integer minHostVersionCode = release.getMinHostVersionCode();
+        return minHostVersionCode == null || hostVersionCode >= minHostVersionCode;
+    }
+
     private ApplicationEntity findApplicationEntity(String appId) {
         return applicationRepository.findById(appId)
             .orElseThrow(() -> new IllegalArgumentException("应用不存在"));
     }
 
-    private ReleaseEntity findReleaseEntity(String appId, String releaseId) {
-        return releaseRepository.findByIdAndAppId(releaseId, appId)
+    private PluginEntity findPluginEntity(String pluginId) {
+        return pluginRepository.findById(pluginId)
+            .orElseThrow(() -> new IllegalArgumentException("插件不存在"));
+    }
+
+    private ReleaseEntity findReleaseEntity(String releaseId) {
+        return releaseRepository.findById(releaseId)
             .orElseThrow(() -> new IllegalArgumentException("版本不存在"));
     }
 
@@ -120,22 +285,88 @@ public class VersionManagementService {
         return prefix + "-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
     }
 
-    public record ApplicationDetail(PlatformData.ApplicationConfig application, List<PlatformData.ReleaseRecord> releases) {
+    public record ApplicationDetail(
+        PlatformData.ApplicationConfig application,
+        List<PlatformData.PluginConfig> plugins,
+        List<PlatformData.ReleaseRecord> releases
+    ) {
+    }
+
+    public record PluginCatalogItem(
+        PlatformData.PluginConfig plugin,
+        PlatformData.ReleaseRecord latestRelease,
+        PlatformData.ReleaseRecord compatibleRelease,
+        boolean updateBlockedByHost,
+        String message
+    ) {
+    }
+
+    public record CreateApplicationCommand(
+        String name,
+        String packageName,
+        String pluginPackageName,
+        String description,
+        boolean pluginManagementEnabled,
+        List<String> businessModules
+    ) {
+    }
+
+    public record UpdateApplicationCommand(
+        String name,
+        String packageName,
+        String pluginPackageName,
+        String description,
+        boolean pluginManagementEnabled,
+        List<String> businessModules
+    ) {
+    }
+
+    public record CreatePluginCommand(
+        String name,
+        String packageName,
+        String entryActivity,
+        String description
+    ) {
+    }
+
+    public record UpdatePluginCommand(
+        String name,
+        String packageName,
+        String entryActivity,
+        String description,
+        boolean enabled
+    ) {
     }
 
     public record UploadReleaseCommand(
         PlatformData.PackageType packageType,
+        String packageName,
         int versionCode,
         String versionName,
         String title,
         String changelog,
         String downloadUrl,
         String entryActivity,
+        Integer minHostVersionCode,
+        String minHostVersionName,
         boolean forceUpdate,
         String uploadedFileName
     ) {
     }
 
+    public record UploadPluginReleaseCommand(
+        int versionCode,
+        String versionName,
+        String title,
+        String changelog,
+        String downloadUrl,
+        String entryActivity,
+        Integer minHostVersionCode,
+        String minHostVersionName,
+        String uploadedFileName
+    ) {
+    }
+
     public record PublishReleaseCommand(
         boolean grayPublish,
         String hookName,

+ 23 - 7
server/version-management-service/src/main/resources/application.yml

@@ -5,11 +5,20 @@ spring:
   application:
     name: version-management-service
   datasource:
-    url: jdbc:mysql://xuqinmin.com:3306/androidLibsServer?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
-    username: androidLibsServer
-    password: iXc8rHydtzRpYFHJ
+    url: ${DB_URL:jdbc:mysql://39.107.53.187:3306/android-libs?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8}
+    username: ${DB_USERNAME:android-libs}
+    password: ${DB_PASSWORD:fiiL8mxhxrDCHc86}
     driver-class-name: com.mysql.cj.jdbc.Driver
+    hikari:
+      minimum-idle: 10
+      maximum-pool-size: 20
+      connection-timeout: 30000
+      validation-timeout: 5000
+      idle-timeout: 300000
+      max-lifetime: 900000
+      connection-test-query: SELECT 1
   jpa:
+    database-platform: org.hibernate.dialect.MySQLDialect
     hibernate:
       ddl-auto: update
     open-in-view: false
@@ -18,10 +27,17 @@ spring:
         format_sql: true
   data:
     redis:
-      host: redisdev.xuqinmin.com
-      port: 6379
-      database: 0
-      password: xuqinmin1022
+      host: ${REDIS_HOST:redisdev.xuqinmin.com}
+      port: ${REDIS_PORT:6379}
+      database: ${REDIS_DATABASE:0}
+      password: ${REDIS_PASSWORD:xuqinmin1022}
+      timeout: 10s
+      lettuce:
+        pool:
+          min-idle: 0
+          max-idle: 8
+          max-active: 8
+          max-wait: -1ms
   cache:
     type: redis
     redis:

+ 0 - 68
server/version-service/README.md

@@ -1,68 +0,0 @@
-# version-service
-
-用于给 `sample-app` 和 `plugin-ui` 提供统一的版本管理接口。
-
-## 启动
-
-```bash
-cd __server__/version-service
-npm start
-```
-
-默认监听:
-
-```text
-http://0.0.0.0:3000
-```
-
-## 接口
-
-### 健康检查
-
-```bash
-curl http://127.0.0.1:3000/health
-```
-
-### 获取 App 最新版本
-
-```bash
-curl "http://127.0.0.1:3000/api/v1/updates/app/latest?packageName=com.xuqm.sample"
-```
-
-### 获取插件最新版本
-
-```bash
-curl "http://127.0.0.1:3000/api/v1/updates/plugin/latest?packageName=com.xuqm.plugin.ui"
-```
-
-### 更新 App 配置
-
-```bash
-curl --location --request PUT "http://127.0.0.1:3000/api/v1/admin/updates/app" \
---header "Content-Type: application/json" \
---data '{
-  "packageName": "com.xuqm.sample",
-  "versionCode": 2,
-  "versionName": "0.2.0",
-  "title": "发现新版本",
-  "changelog": "更新内容",
-  "downloadUrl": "http://192.168.116.9:10223/app.apk",
-  "forceUpdate": false
-}'
-```
-
-### 更新插件配置
-
-```bash
-curl --location --request PUT "http://127.0.0.1:3000/api/v1/admin/updates/plugin" \
---header "Content-Type: application/json" \
---data '{
-  "packageName": "com.xuqm.plugin.ui",
-  "versionCode": 2,
-  "versionName": "0.2.0",
-  "downloadUrl": "http://192.168.116.9:10223/plugin-ui-release.apk",
-  "entryActivity": "com.xuqm.plugin.ui.PluginUiActivity"
-}'
-```
-
-版本数据存放在 [`data/version-config.json`](./data/version-config.json)。

+ 0 - 22
server/version-service/data/version-config.json

@@ -1,22 +0,0 @@
-{
-  "apps": {
-    "com.xuqm.sample": {
-      "packageName": "com.xuqm.sample",
-      "versionCode": 2,
-      "versionName": "0.2.0",
-      "title": "发现新版本",
-      "changelog": "1. 新增统一下载管理\n2. 新增插件版本管理\n3. 优化宿主更新流程",
-      "downloadUrl": "http://192.168.116.9:10223/app.apk",
-      "forceUpdate": false
-    }
-  },
-  "plugins": {
-    "com.xuqm.plugin.ui": {
-      "packageName": "com.xuqm.plugin.ui",
-      "versionCode": 2,
-      "versionName": "0.2.0",
-      "downloadUrl": "http://192.168.116.9:10223/plugin-ui-release.apk",
-      "entryActivity": "com.xuqm.plugin.ui.PluginUiActivity"
-    }
-  }
-}

+ 0 - 12
server/version-service/package.json

@@ -1,12 +0,0 @@
-{
-  "name": "@xuqm/version-service",
-  "version": "0.1.0",
-  "private": true,
-  "description": "Version service for Android host app and plugin update management",
-  "scripts": {
-    "start": "node src/index.js"
-  },
-  "engines": {
-    "node": ">=18"
-  }
-}

+ 0 - 141
server/version-service/src/index.js

@@ -1,141 +0,0 @@
-const http = require("http");
-const fs = require("fs/promises");
-const path = require("path");
-const { URL } = require("url");
-
-const HOST = process.env.HOST || "0.0.0.0";
-const PORT = Number(process.env.PORT || 3000);
-const DATA_FILE = path.join(__dirname, "..", "data", "version-config.json");
-
-async function readConfig() {
-  const raw = await fs.readFile(DATA_FILE, "utf8");
-  return JSON.parse(raw);
-}
-
-async function writeConfig(config) {
-  await fs.writeFile(DATA_FILE, JSON.stringify(config, null, 2), "utf8");
-}
-
-function sendJson(res, statusCode, payload) {
-  res.writeHead(statusCode, {
-    "Content-Type": "application/json; charset=utf-8",
-    "Access-Control-Allow-Origin": "*",
-    "Access-Control-Allow-Methods": "GET,PUT,OPTIONS",
-    "Access-Control-Allow-Headers": "Content-Type"
-  });
-  res.end(JSON.stringify(payload));
-}
-
-function ok(data, message = "success") {
-  return { code: 200, status: "0", data, message };
-}
-
-function fail(statusCode, message) {
-  return {
-    statusCode,
-    payload: { code: statusCode, status: String(statusCode), data: null, message }
-  };
-}
-
-async function parseBody(req) {
-  const chunks = [];
-  for await (const chunk of req) chunks.push(chunk);
-  const raw = Buffer.concat(chunks).toString("utf8");
-  return raw ? JSON.parse(raw) : {};
-}
-
-async function handleGetLatestApp(url, res) {
-  const packageName = url.searchParams.get("packageName");
-  if (!packageName) {
-    const result = fail(400, "packageName is required");
-    return sendJson(res, result.statusCode, result.payload);
-  }
-
-  const config = await readConfig();
-  const app = config.apps[packageName];
-  if (!app) {
-    const result = fail(404, `app config not found for ${packageName}`);
-    return sendJson(res, result.statusCode, result.payload);
-  }
-  return sendJson(res, 200, ok(app));
-}
-
-async function handleGetLatestPlugin(url, res) {
-  const packageName = url.searchParams.get("packageName");
-  if (!packageName) {
-    const result = fail(400, "packageName is required");
-    return sendJson(res, result.statusCode, result.payload);
-  }
-
-  const config = await readConfig();
-  const plugin = config.plugins[packageName];
-  if (!plugin) {
-    const result = fail(404, `plugin config not found for ${packageName}`);
-    return sendJson(res, result.statusCode, result.payload);
-  }
-  return sendJson(res, 200, ok(plugin));
-}
-
-async function handleUpdateConfig(kind, req, res) {
-  const body = await parseBody(req);
-  const packageName = body.packageName;
-  if (!packageName) {
-    const result = fail(400, "packageName is required");
-    return sendJson(res, result.statusCode, result.payload);
-  }
-
-  const config = await readConfig();
-  const target = kind === "app" ? config.apps : config.plugins;
-  target[packageName] = body;
-  await writeConfig(config);
-  return sendJson(res, 200, ok(body, `${kind} config updated`));
-}
-
-const server = http.createServer(async (req, res) => {
-  try {
-    const url = new URL(req.url, `http://${req.headers.host}`);
-
-    if (req.method === "OPTIONS") {
-      res.writeHead(204, {
-        "Access-Control-Allow-Origin": "*",
-        "Access-Control-Allow-Methods": "GET,PUT,OPTIONS",
-        "Access-Control-Allow-Headers": "Content-Type"
-      });
-      return res.end();
-    }
-
-    if (req.method === "GET" && url.pathname === "/health") {
-      return sendJson(res, 200, ok({ status: "UP" }));
-    }
-
-    if (req.method === "GET" && url.pathname === "/api/v1/updates/app/latest") {
-      return handleGetLatestApp(url, res);
-    }
-
-    if (req.method === "GET" && url.pathname === "/api/v1/updates/plugin/latest") {
-      return handleGetLatestPlugin(url, res);
-    }
-
-    if (req.method === "PUT" && url.pathname === "/api/v1/admin/updates/app") {
-      return handleUpdateConfig("app", req, res);
-    }
-
-    if (req.method === "PUT" && url.pathname === "/api/v1/admin/updates/plugin") {
-      return handleUpdateConfig("plugin", req, res);
-    }
-
-    const result = fail(404, "route not found");
-    return sendJson(res, result.statusCode, result.payload);
-  } catch (error) {
-    return sendJson(res, 500, {
-      code: 500,
-      status: "500",
-      data: null,
-      message: error.message || "internal server error"
-    });
-  }
-});
-
-server.listen(PORT, HOST, () => {
-  console.log(`version-service is running at http://${HOST}:${PORT}`);
-});