Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.ai.assistance.operit.ui.features.packages.components.dialogs.content.
import com.ai.assistance.operit.ui.features.packages.components.dialogs.content.MCPServerDetailsContent
import com.ai.assistance.operit.ui.features.packages.components.dialogs.header.MCPServerDetailsHeader
import com.ai.assistance.operit.ui.features.packages.components.dialogs.tabs.MCPServerDetailsTabs
import com.ai.assistance.operit.data.api.MarketV2Entry
import com.ai.assistance.operit.data.mcp.MCPLocalServer

/**
Expand All @@ -52,7 +53,8 @@ fun MCPServerDetailsDialog(
pluginConfig: String = "",
onSaveConfig: () -> Unit = {},
onUpdateConfig: (String) -> Unit = {},
mdFontSize: Float = 14f
mdFontSize: Float = 14f,
onOpenMarketDetail: (MarketV2Entry) -> Unit = {}
) {
val isInstalled = server.isInstalled
val supportsLocalConfigEditing = server.type != "remote"
Expand Down Expand Up @@ -135,7 +137,9 @@ fun MCPServerDetailsDialog(
server = server,
isInstalled = isInstalled,
onInstall = onInstall,
onUninstall = onUninstall
onUninstall = onUninstall,
installedPath = installedPath,
onOpenMarketDetail = onOpenMarketDetail
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.ai.assistance.operit.R
import com.ai.assistance.operit.data.api.MarketV2Entry
import com.ai.assistance.operit.data.mcp.MCPLocalServer
import com.ai.assistance.operit.ui.features.packages.market.MarketUpdateCheckButton
import com.ai.assistance.operit.ui.features.packages.market.readMarketInstallMarkerForMcp

/**
* Actions component for the MCP server details diaAppLogger.
Expand All @@ -38,10 +41,11 @@ fun MCPServerDetailsActions(
server: MCPLocalServer.PluginMetadata,
isInstalled: Boolean,
onInstall: (MCPLocalServer.PluginMetadata) -> Unit,
onUninstall: (MCPLocalServer.PluginMetadata) -> Unit
onUninstall: (MCPLocalServer.PluginMetadata) -> Unit,
installedPath: String? = null,
onOpenMarketDetail: (MarketV2Entry) -> Unit = {}
) {
val context = LocalContext.current

Row(
modifier = Modifier
.fillMaxWidth()
Expand Down Expand Up @@ -69,7 +73,14 @@ fun MCPServerDetailsActions(
Text(text = stringResource(R.string.repo))
}
}

// 市场更新检查(仅已安装时可用)
if (isInstalled) {
MarketUpdateCheckButton(
markerProvider = { readMarketInstallMarkerForMcp(context, server.id, installedPath) },
onOpenMarketDetail = onOpenMarketDetail,
modifier = Modifier.weight(1f)
)
}
// Install/Uninstall button
if (isInstalled) {
Button(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,25 @@ import com.ai.assistance.operit.R
import com.ai.assistance.operit.core.tools.PackageTool
import com.ai.assistance.operit.core.tools.ToolPackage
import com.ai.assistance.operit.core.tools.packTool.PackageManager
import com.ai.assistance.operit.data.api.MarketStatsApiService
import com.ai.assistance.operit.data.api.MarketV2Entry
import com.ai.assistance.operit.ui.common.icons.rememberLogoPainter
import com.ai.assistance.operit.ui.features.packages.market.readMarketInstallMarkerForPackage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

private sealed interface PackageUpdateCheckState {
data object UpToDate : PackageUpdateCheckState
data class UpdateAvailable(
val entry: MarketV2Entry,
val latestVersion: String
) : PackageUpdateCheckState
data object NotFromMarket : PackageUpdateCheckState
data object EntryMissing : PackageUpdateCheckState
data object Failed : PackageUpdateCheckState
}

@Composable
fun PackageDetailsDialog(
packageName: String,
Expand All @@ -41,11 +55,60 @@ fun PackageDetailsDialog(
onRunScript: (String, PackageTool) -> Unit,
onOpenToolPkgPluginConfig: (String, String, String, Boolean) -> Unit,
onDismiss: () -> Unit,
onPackageDeleted: () -> Unit
onPackageDeleted: () -> Unit,
onOpenMarketDetail: (MarketV2Entry) -> Unit
) {
val context = LocalContext.current
var showDeleteConfirmDialog by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
var updateCheckState by remember(packageName) { mutableStateOf<PackageUpdateCheckState?>(null) }
var isCheckingUpdate by remember { mutableStateOf(false) }

fun runUpdateCheck() {
if (isCheckingUpdate) return
isCheckingUpdate = true
updateCheckState = null
scope.launch {
try {
val marker =
withContext(Dispatchers.IO) {
readMarketInstallMarkerForPackage(packageManager, packageName)
}
if (marker == null) {
updateCheckState = PackageUpdateCheckState.NotFromMarket
return@launch
}
val entryResult =
withContext(Dispatchers.IO) {
MarketStatsApiService().getEntry(marker.entryId)
}
if (entryResult.isFailure) {
updateCheckState = PackageUpdateCheckState.Failed
return@launch
}
val entry = entryResult.getOrNull()
if (entry == null) {
updateCheckState = PackageUpdateCheckState.EntryMissing
return@launch
}
val latest = entry.latestVersion
if (latest == null || latest.id.isBlank() || latest.id == marker.versionId) {
updateCheckState = PackageUpdateCheckState.UpToDate
} else {
updateCheckState =
PackageUpdateCheckState.UpdateAvailable(
entry = entry,
latestVersion = latest.version.ifBlank { latest.id }
)
}
} catch (error: Exception) {
AppLogger.e("PackageDetailsDialog", "Update check failed", error)
updateCheckState = PackageUpdateCheckState.Failed
} finally {
isCheckingUpdate = false
}
}
}

val resolvedPackage by produceState<ToolPackage?>(initialValue = toolPackage, packageName, toolPackage) {
value = toolPackage
Expand Down Expand Up @@ -185,6 +248,56 @@ fun PackageDetailsDialog(
)
}

val currentUpdateCheckState = updateCheckState
if (currentUpdateCheckState != null) {
AlertDialog(
onDismissRequest = { updateCheckState = null },
title = { Text(stringResource(R.string.pkg_check_update)) },
text = {
Text(
when (currentUpdateCheckState) {
is PackageUpdateCheckState.UpToDate ->
stringResource(R.string.pkg_check_update_up_to_date)
is PackageUpdateCheckState.UpdateAvailable ->
stringResource(
R.string.pkg_check_update_available,
currentUpdateCheckState.latestVersion
)
PackageUpdateCheckState.NotFromMarket ->
stringResource(R.string.pkg_check_update_not_market)
PackageUpdateCheckState.EntryMissing ->
stringResource(R.string.pkg_check_update_entry_missing)
PackageUpdateCheckState.Failed ->
stringResource(R.string.pkg_check_update_failed)
}
)
},
confirmButton = {
if (currentUpdateCheckState is PackageUpdateCheckState.UpdateAvailable) {
Button(
onClick = {
updateCheckState = null
onOpenMarketDetail(currentUpdateCheckState.entry)
}
) {
Text(stringResource(R.string.pkg_check_update_go))
}
} else {
Button(onClick = { updateCheckState = null }) {
Text(stringResource(R.string.pkg_ok))
}
}
},
dismissButton = {
if (currentUpdateCheckState is PackageUpdateCheckState.UpdateAvailable) {
TextButton(onClick = { updateCheckState = null }) {
Text(stringResource(R.string.pkg_cancel))
}
}
}
)
}

Dialog(onDismissRequest = onDismiss) {
Surface(
modifier = Modifier.fillMaxWidth().heightIn(max = 600.dp),
Expand Down Expand Up @@ -621,6 +734,25 @@ fun PackageDetailsDialog(
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End)
) {
if (metaPackage != null && !metaPackage.isBuiltIn) {
OutlinedButton(
onClick = { runUpdateCheck() },
enabled = !isCheckingUpdate
) {
if (isCheckingUpdate) {
CircularProgressIndicator(
modifier = Modifier.size(14.dp),
strokeWidth = 2.dp
)
} else {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
}
Spacer(modifier = Modifier.width(4.dp))
Text(stringResource(R.string.pkg_check_update))
}
OutlinedButton(
onClick = { showDeleteConfirmDialog = true },
colors = ButtonDefaults.outlinedButtonColors(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,47 @@ fun findInstalledMarketMarkerRoot(
return findInstalledMarketMarkerRoots(context, packageManager, entryId).firstOrNull()
}

fun readMarketInstallMarkerForPackage(
packageManager: PackageManager,
packageName: String
): MarketInstallMarker? {
return try {
readMarketInstallMarker(artifactMarketMarkerRoot(packageManager, packageName))
} catch (error: Exception) {
AppLogger.w(TAG, "Failed to read market install marker for package: $packageName", error)
null
}
}

/** 读取技能安装目录下的市场安装标记(无则返回 null) */
fun readMarketInstallMarkerForSkill(skillDirectory: File): MarketInstallMarker? {
return try {
readMarketInstallMarker(skillDirectory)
} catch (error: Exception) {
AppLogger.w(TAG, "Failed to read market install marker for skill: ${skillDirectory.absolutePath}", error)
null
}
}

/**
* 读取 MCP 的市场安装标记。MCP 有两种安装形态:
* 1) 纯配置安装:标记写在 market_install_markers/mcp_config/{serverId};
* 2) 物理安装:标记写在插件安装目录(installedPath)。
* 任一位置读到即返回。
*/
fun readMarketInstallMarkerForMcp(context: Context, serverId: String, installedPath: String?): MarketInstallMarker? {
try {
readMarketInstallMarker(mcpConfigMarketMarkerRoot(context, serverId))?.let { return it }
installedPath
?.trim()
?.takeIf { it.isNotBlank() }
?.let { path -> readMarketInstallMarker(File(path))?.let { return it } }
} catch (error: Exception) {
AppLogger.w(TAG, "Failed to read market install marker for MCP: $serverId", error)
}
return null
}

fun findInstalledMarketMarkerRoots(
context: Context,
packageManager: PackageManager,
Expand Down
Loading