☰
Operit 市场包版本兼容性治理:客户端安装强制校验与双端一致化实践
2026/9/28 20:26:47 网站建设 项目流程
  • AI Agent
  • 人工智能
  • 大模型
  • AI 应用
  • 工具调用
  • 本地部署
  • MCP Clients
  • Agent 记忆

【免费下载链接】Operit

The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent

项目地址:https://gitcode.com/gh_mirrors/op/Operit
点击查看免费下载

本文聚焦 Operit 开源项目中"市场(Marketplace)条目版本兼容性"这一主题,讲解市场条目如何通过minAppVer/maxAppVer元数据声明自身支持的客户端版本区间,以及 Operit 2 与 Android 客户端如何从"应用不一致"走向"下载前强制校验 + 明确失败原因"的完整治理过程。读完本文,你将掌握市场安装命令的版本参数约定、版本区间校验的底层比较算法、Android 端不可用状态与详情页横幅的实现方式,并能在自己的包发布与安装流程中复刻这套校验模型。

一、问题背景:两个客户端对版本区间的"不一致应用"

在 market_version_compatibility_20260805/index.md 所记录的需求提出之前,市场条目其实已经发布minAppVer与maxAppVer两个字段,用于声明该条目(某个发布版本)所支持的 Operit 客户端版本下限与上限。然而,Operit 生态中的两个客户端对这一元数据的应用并不一致:

  • Operit 2(Flutter 客户端,对应assistance2代号):在界面上渲染了发布方声明的版本区间,但下载前并没有真正做版本比较——也就是说,即使当前客户端版本不在支持区间内,它依然会继续执行下载流程。
  • Android 客户端(Kotlin):虽然做了版本比较,但用户看到的只是被禁用的操作按钮和一个笼统的警告状态,不知道具体原因——是自己的版本太低,还是太高。

这种割裂导致两类典型问题:Operit 2 会接受不兼容的下载(下载后无法正常使用),Android 端则"静默禁用"却不解释原因,用户无从判断如何升级或降级。

本次治理的目标(Scope)明确为三点:

  1. 为 Operit 2 的市场安装命令强制要求传入客户端版本参数,并在下载资源之前拒绝不兼容条目;
  2. Flutter 把自身构建版本号传给该命令,在版本选择与直接安装两个入口被拦截时展示不兼容原因;
  3. Android 端在保持禁用下载行为的同时,用当前版本 + 被违反的边界值解释禁用原因。

二、预期行为与完成标准

根据 1-install-version-enforcement.md 的规划,两端最终的"预期行为"如下:

  • Rust 侧(市场命令实现):市场安装命令接收真实客户端版本,在任何下载开始之前校验所选条目的版本是否处于支持区间内;不兼容即失败,不产生任何网络资源下载。
  • Flutter 侧:将自身构建版本号传给 Rust 命令;在版本选择界面屏蔽不兼容的条目并给出原因;直接安装(绕过选择界面)时同样返回该原因。
  • Kotlin 侧:保留"禁用操作"的行为,但在详情页明确显示客户端是低于最低版本还是高于最高版本。

最终的完成结果(Result [DONE])确认了三件事:

  1. Rust 命令要求调用方携带客户端版本,并在解析任何资源之前检查请求的市场版本;
  2. Flutter 以2.0.0+5作为当前版本传入,对不兼容的选项给出原因并拦截,直接安装时返回同一原因;
  3. Android 保留不可用操作按钮,同时在详情页为"低于下限 / 高于上限"两类违规分别展示横幅。

其中2.0.0+5是 Flutter 构建版本x.y.z+n格式的实例——+5表示构建序号(build number),这个格式也正是 Android 端版本解析所要求的输入格式。

三、Android 端版本解析与比较:源码级实现

Android 端的能力集中在 ArtifactMarketModels.kt 中。这套实现把"版本字符串 -> 结构化版本 -> 区间比较 -> 违规类型"的整条链路串了起来。

3.1 版本字符串解析

private fun parseAppVersionOrNull(value: String?): AppVersion? { val normalized = value?.trim().orEmpty() if (normalized.isBlank()) return null val match = APP_VERSION_REGEX.matchEntire(normalized) ?: throw IllegalArgumentException("App version must use x.y.z or x.y.z+n format") return AppVersion( major = match.groupValues[1].toInt(), minor = match.groupValues[2].toInt(), patch = match.groupValues[3].toInt(), build = match.groupValues[4].takeIf { it.isNotBlank() }?.toInt() ) }

关键约束:

  • 版本字符串必须是x.y.z或x.y.z+n格式,其中n为可选的构建号(build);
  • 空白输入返回null(视为"未声明",不参与校验),格式非法则直接抛出IllegalArgumentException;
  • 解析结果被拆成major / minor / patch / build四个整数,为后续逐段比较提供结构化基础。

3.2 逐段比较算法

fun compareAppVersions(left: String, right: String): Int { val leftVersion = requireNotNull(parseAppVersionOrNull(left)) val rightVersion = requireNotNull(parseAppVersionOrNull(right)) if (leftVersion.major != rightVersion.major) { return leftVersion.major.compareTo(rightVersion.major) } if (leftVersion.minor != rightVersion.minor) { return leftVersion.minor.compareTo(rightVersion.minor) } if (leftVersion.patch != rightVersion.patch) { return leftVersion.patch.compareTo(rightVersion.patch) } val leftBuild = leftVersion.build ?: 0 val rightBuild = rightVersion.build ?: 0 return leftBuild.compareTo(rightBuild) }

比较顺序为major → minor → patch → build,与语义化版本(SemVer)的优先级规则一致;当一侧缺省 build 时按0参与比较,保证x.y.z与x.y.z+0等价。

3.3 区间校验与违规判定

fun resolveAppVersionCompatibility( appVersion: String, minSupportedAppVersion: String?, maxSupportedAppVersion: String? ): MarketAppVersionCompatibility? { val current = requireNotNull(parseAppVersionOrNull(appVersion)) { "Current app version must use x.y.z or x.y.z+n format" } val minimum = parseAppVersionOrNull(minSupportedAppVersion) if (minimum != null && compareAppVersions(current.toString(), minimum.toString()) < 0) { return MarketAppVersionCompatibility( kind = MarketAppVersionCompatibilityKind.BELOW_MINIMUM, currentVersion = current.toString(), requiredVersion = minimum.toString() ) } val maximum = parseAppVersionOrNull(maxSupportedAppVersion) if (maximum != null && compareAppVersions(current.toString(), maximum.toString()) > 0) { return MarketAppVersionCompatibility( kind = MarketAppVersionCompatibilityKind.ABOVE_MAXIMUM, currentVersion = current.toString(), requiredVersion = maximum.toString() ) } return null }

判定结果使用一个枚举 + 数据类建模:

enum class MarketAppVersionCompatibilityKind { BELOW_MINIMUM, ABOVE_MAXIMUM } data class MarketAppVersionCompatibility( val kind: MarketAppVersionCompatibilityKind, val currentVersion: String, val requiredVersion: String )

即返回结果要么是null(兼容),要么携带"当前版本 + 被违反的边界值"以及违规方向,这正好对应需求中"两端都必须点名当前版本与被违反的支持边界"的完成标准。

3.4 与当前 App 版本的集成

fun MarketV2Entry.resolveCurrentAppVersionCompatibility(): MarketAppVersionCompatibility? { val version = latestVersion ?: return null return resolveAppVersionCompatibility( appVersion = BuildConfig.VERSION_NAME, minSupportedAppVersion = version.minAppVer, maxSupportedAppVersion = version.maxAppVer ) } fun MarketV2Entry.isUnsupportedByCurrentAppVersion(): Boolean { return resolveCurrentAppVersionCompatibility() != null }

这里有两个要点:

  • 当前客户端版本直接取自BuildConfig.VERSION_NAME,即市场校验与 App 实际构建版本天然绑定,无需额外传参;
  • 校验对象是条目的latestVersion(最新发布版本)的minAppVer/maxAppVer字段,与 ArtifactMarketModels.kt 中resolveCurrentAppVersionCompatibility的实现一一对应。

此外,同一文件还提供了formatSupportedAppVersions(...)与normalizeAppVersionOrNull(...)等辅助函数,分别用于把区间格式化为"min - max"展示文本、以及把非标准写法归一化为规范的x.y.z/x.y.z+n字符串。

3.5 发布侧的区间合法性校验

版本区间不仅在安装侧校验,发布侧同样有约束。validateSupportedAppVersions要求最低支持版本必填,并且当上下限同时存在时,min ≤ max:

fun validateSupportedAppVersions( minSupportedAppVersion: String?, maxSupportedAppVersion: String? ) { val normalizedMin = normalizeAppVersionOrNull(minSupportedAppVersion) val normalizedMax = normalizeAppVersionOrNull(maxSupportedAppVersion) require(normalizedMin != null) { "Minimum supported app version is required" } if (normalizedMin != null && normalizedMax != null) { require(compareAppVersions(normalizedMin, normalizedMax) <= 0) { "Minimum supported app version cannot be greater than maximum supported app version" } } }

发布侧与安装侧共用同一套compareAppVersions比较逻辑,保证了"发布时声明的区间合法,安装时校验才可信"这一闭环。minAppVer/maxAppVer字段还出现在市场发布页面(ArtifactPublishScreen.kt)、发布 ViewModel(RepoMarketPublishViewModel.kt)以及发布服务(GitHubForgePublishService.kt)等整条发布链路中。

四、Android 详情页:禁用操作 + 原因横幅

Android 端的行为体现在 UnifiedMarketDetailEntryScreen.kt 中,核心是"保留不可用操作 + 用横幅解释原因"。

4.1 禁用状态从何而来

val currentAppVersionCompatibility = entry.resolveCurrentAppVersionCompatibility()

详情页在组装时调用扩展函数resolveCurrentAppVersionCompatibility()得出当前兼容性结论,并以此把主操作标记为警告/禁用(isWarning = isCurrentAppVersionUnsupported),从而"保留禁用下载行为"。

4.2 原因横幅

当兼容性结论非空时,详情页渲染一个警告横幅(banner):

banner = if (currentAppVersionCompatibility != null) { UnifiedMarketDetailBanner( title = stringResource(R.string.market_version_incompatible_title), message = when (currentAppVersionCompatibility.kind) { MarketAppVersionCompatibilityKind.BELOW_MINIMUM -> stringResource( R.string.market_version_too_low_message, currentAppVersionCompatibility.currentVersion, currentAppVersionCompatibility.requiredVersion ) MarketAppVersionCompatibilityKind.ABOVE_MAXIMUM -> stringResource( R.string.market_version_too_high_message, currentAppVersionCompatibility.currentVersion, currentAppVersionCompatibility.requiredVersion ) }, icon = Icons.Default.Warning, containerColor = MaterialTheme.colorScheme.errorContainer, contentColor = MaterialTheme.colorScheme.onErrorContainer ) } else { ... }

可以看到:

  • 两类违规分别给出不同文案:market_version_too_low_message(低于最低版本)与market_version_too_high_message(高于最高版本),两条文案都带currentVersion与requiredVersion两个占位参数,正好呈现"当前版本 + 被违反的边界";
  • 视觉上使用 errorContainer / onErrorContainer,即 Material 3 错误语义配色,强化"此操作不可执行"的提示;
  • 版本区间的展示文本(minAppVer - maxAppVer)也在版本行中渲染,用户可以在同一页面对比"声明的区间"与"自己的版本"。

另外,详情页还会展示toolpkgApiVersion(工具包 API 版本)等附加信息,说明市场详情页是"版本兼容性 + 工具包 API 能力"的集中呈现入口。

五、Operit 2(Flutter/Rust)侧:安装前强制校验

按文档记录,Rust 侧的市场命令是校验的第一道闸门:

  1. 命令要求携带客户端版本参数——调用方(Flutter)必须显式传入自身版本,未携带时命令无法正确执行;
  2. 解析任何资源之前先校验版本——被选中的市场条目版本只有在通过区间检查后,才会进入资源解析与下载阶段,从而保证"不兼容的安装在任何网络资产下载之前失败";
  3. Flutter 传入2.0.0+5——这是 Operit 2 在当前迭代中的构建版本号,格式与 Android 端要求的x.y.z+n一致,说明两个客户端共享同一版本语义;
  4. 两个入口都返回原因——无论是从版本选择列表中选择安装,还是绕过列表直接指定版本安装,被拦截时都会把不兼容原因原样返回给用户界面。

这四点共同构成"Operit 2 拒绝不兼容下载"的行为闭环。从整体架构看,Rust 命令承担"服务端权威校验",Flutter 负责"版本号传递与原因展示",Android 走"本地 Kotlin 校验",三者协同实现了双端一致化。

六、设计与交付要点总结

  1. 校验前移:把版本比较放在"下载任何资源之前",是本次治理最关键的时序约束——无论哪一端,都不允许不兼容的条目进入网络下载阶段。
  2. 原因显性化:两端的完成标准都要求"点名当前版本 + 被违反的边界"。Android 通过MarketAppVersionCompatibility(currentVersion, requiredVersion)携带两个版本号,Operit 2 则在安装被拦截时返回同一原因,杜绝了"静默禁用"。
  3. 单一声明、多处消费:minAppVer/maxAppVer由发布侧写入并校验合法性(min ≤ max),安装侧(Rust 命令、Flutter UI、Kotlin 详情页)共同消费,形成"发布-安装"闭环。
  4. 共享版本语义:两端统一使用x.y.z/x.y.z+n格式(Flutter 实例2.0.0+5),Kotlin 侧以BuildConfig.VERSION_NAME作为当前版本基准,保证比较结果与真实 App 版本一致。

七、延伸阅读

  • 需求总览与范围定义:market_version_compatibility_20260805/index.md
  • 安装侧强制校验的完整规划与验收结果:1-install-version-enforcement.md
  • Android 端版本解析、比较与兼容性判定核心实现:ArtifactMarketModels.kt
  • Android 详情页禁用状态与原因横幅:UnifiedMarketDetailEntryScreen.kt
  • 市场发布侧版本区间输入与校验:ArtifactPublishScreen.kt、RepoMarketPublishViewModel.kt、GitHubForgePublishService.kt
  • AI Agent
  • 人工智能
  • 大模型
  • AI 应用
  • 工具调用
  • 本地部署
  • MCP Clients
  • Agent 记忆

【免费下载链接】Operit

The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent

项目地址:https://gitcode.com/gh_mirrors/op/Operit
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询