依赖安装成功,只能说明包被解析并落到了工程里。它不等于“当前版本可以被业务启用”,更不等于“预发布版本可以按稳定版本的规则自动放行”。

这篇文章把一个很小的检查脚本扩成可审计的能力门禁。Demo 名为 PluginRangeLab,诊断页是 CapabilityAuditPage,任务编号 SEMVER-CAP-0212。21:36,宿主声明稳定通道合同 >=2.8.0 <3.0.0,实际检测到 vision-bridge@3.0.0-beta.4。版本比 2.8.0 新,却没有进入稳定通道;只有显式切到 canary 通道,使用 >=3.0.0-beta.2 <3.0.0,才进入下一步能力协商。

文中结果是工程样例的确定性输出,不代表 ohpm 会自动执行这些业务策略。ohpm 负责 HarmonyOS 三方包的发布、安装和依赖管理;本文的通道、能力清单与降级矩阵属于项目自建门禁。

一、版本“更大”不代表范围“满足”

很多版本判断从字符串比较开始:3.0.0-beta.4 > 2.8.0,于是代码放行。这个结论跳过了 SemVer 对预发布标记的特殊语义。

node-semver 的官方说明很明确:带 prerelease 标记的版本,默认只有在比较器集合里存在相同 major/minor/patch 元组的预发布比较器时,才可能满足该集合。这个规则是有意设计的,因为 alpha、beta、rc 往往更新快,也可能包含未稳定的破坏性变化。

因此 3.0.0-beta.4 不满足 >=2.8.0 <3.0.0。<3.0.0 在排序上确实覆盖 beta,但默认范围匹配仍会排除没有被显式选择的预发布版本。若项目确实要试用,应该写出带预发布元组的 canary 范围,而不是把 includePrerelease: true 当成全局万能开关。

Demo 的基础事实如下:

  • 宿主:CatalogHost@5.4.0。
  • 插件:vision-bridge@3.0.0-beta.4。
  • 稳定通道范围:>=2.8.0 <3.0.0,结果 false。
  • canary 通道范围:>=3.0.0-beta.2 <3.0.0,结果 true。
  • 预发布标记:['beta', 4]。
  • 必需能力:search.preview,匹配 1/1。
  • 可选能力:search.batch,缺失 1。
  • 最终决定:DEGRADED_READY,只在 canary 通道启用预览,批处理入口隐藏。

我刻意把“版本范围”和“能力集合”分成两道门。版本范围回答团队是否接受这一发布线;能力集合回答当前二进制究竟能做什么。把两者混在一个 if 里,会让版本号承担它并未承诺的业务语义。

二、先严格解析,再决定通道

版本来自清单、生成报告或外部插件描述时,最危险的做法是先 coerce()。node-semver 的 coerce 会尽力从任意字符串中提取可用数字,例如可能把包含说明文字的内容转成三段版本。它适合清洗非合同输入,不适合发布门禁,因为修复动作会抹去原始错误。

门禁应该使用严格 valid() 和 validRange();不合法就报告原值,而不是猜测作者的意思。

这段代码解决版本字符串、范围和预发布标记的严格判定。

import * as semver from 'semver';

interface VersionDecision {
  rawVersion: string;
  normalized: string | null;
  channel: 'stable' | 'canary';
  range: string;
  prerelease: ReadonlyArray<string | number> | null;
  satisfies: boolean;
  reason: string;
}

function checkVersion(rawVersion: string,
  channel: 'stable' | 'canary'): VersionDecision {
  const stableRange = '>=2.8.0 <3.0.0';
  const canaryRange = '>=3.0.0-beta.2 <3.0.0';
  const range = channel === 'stable' ? stableRange : canaryRange;
  const normalized = semver.valid(rawVersion);
  const validRange = semver.validRange(range);
  if (normalized === null || validRange === null) {
    return {
      rawVersion, normalized: null, channel, range,
      prerelease: null, satisfies: false, reason: 'INVALID_CONTRACT'
    };
  }
  const pre = semver.prerelease(normalized);
  const accepted = semver.satisfies(normalized, validRange);
  return {
    rawVersion, normalized, channel, range,
    prerelease: pre, satisfies: accepted,
    reason: accepted ? 'RANGE_ACCEPTED' :
      (pre === null ? 'OUTSIDE_RANGE' : 'PRERELEASE_NOT_OPTED_IN')
  };
}

在稳定通道调用 checkVersion('3.0.0-beta.4', 'stable'),得到 PRERELEASE_NOT_OPTED_IN。切到 canary 后,范围显式写入 3.0.0-beta.2 这一元组,结果才变为 RANGE_ACCEPTED。

为什么不直接给 satisfies 传 { includePrerelease: true }?因为它会扩大整个范围的候选集合,审查者很难从一条通用范围看出团队到底接受哪条预发布线。显式 canary 范围更窄,也能作为代码评审中的可见意图。

还有一个细节:prerelease() 返回 null 表示稳定版本,返回数组表示预发布标记。不要只用字符串包含 -beta 判断。rc、自定义标记以及数字标识符都会让手写规则迅速失控。

三、版本门通过后,能力门才开始

版本符合范围并不证明插件实现了宿主需要的全部能力。特别是 beta 版本,功能可能分批交付。本文给插件清单增加独立的 capabilities 数组,不从版本号推断功能。

清单样例放在项目工具目录,由 Hvigor 前置任务或独立 Node.js 命令读取:

清单的关键值为:name=vision-bridge、version=3.0.0-beta.4,能力集合包含 search.preview、search.basic 与 telemetry.trace。这里故意没有 search.batch,用来验证可选能力缺失后的降级分支。

宿主策略把能力分成 required 和 optional。required 缺失必须阻断;optional 缺失则进入降级矩阵。这样版本升级和功能开关不再互相冒充。

这段代码解决必需能力、可选能力和降级动作的确定性计算。

interface CapabilityPolicy {
  required: string[];
  optional: string[];
}

interface CapabilityDecision {
  requiredMatched: number;
  requiredTotal: number;
  missingRequired: string[];
  missingOptional: string[];
  decision: 'BLOCKED' | 'FULL_READY' | 'DEGRADED_READY';
  disabledFeatures: string[];
}

function negotiateCapabilities(pluginCaps: string[],
  policy: CapabilityPolicy): CapabilityDecision {
  const actual = new Set(pluginCaps);
  const missingRequired = policy.required.filter((x: string) => !actual.has(x));
  const missingOptional = policy.optional.filter((x: string) => !actual.has(x));
  if (missingRequired.length > 0) {
    return {
      requiredMatched: policy.required.length - missingRequired.length,
      requiredTotal: policy.required.length,
      missingRequired, missingOptional,
      decision: 'BLOCKED', disabledFeatures: ['plugin.entry']
    };
  }
  return {
    requiredMatched: policy.required.length,
    requiredTotal: policy.required.length,
    missingRequired, missingOptional,
    decision: missingOptional.length === 0 ? 'FULL_READY' : 'DEGRADED_READY',
    disabledFeatures: missingOptional.map((cap: string) =>
      cap === 'search.batch' ? 'batch.action' : `feature:${cap}`)
  };
}

本批样例策略是 required=['search.preview']、optional=['search.batch']。插件提供预览能力,所以 required 为 1/1;缺少批处理能力,因此只关闭 batch.action,并保留预览入口。结果不是“兼容/不兼容”二选一,而是一条可解释降级路径。

容易出错的是把 optional 缺失记成 warning,却仍让组件自己决定是否展示。诊断报告和运行 UI 应消费同一份 disabledFeatures,否则构建日志说已降级,页面仍可能露出不可用按钮。

四、从 ohpm 安装事实提取审计输入

ohpm 是 OpenHarmony/HarmonyOS 生态中的三方包管理工具。项目清单和锁定结果告诉我们“请求了什么、解析成什么”,但业务插件的能力仍应来自受版本控制的元数据或插件自身清单。

工具链可以把三份输入合并:

  1. oh-package.json5:项目声明的依赖范围。
  2. 锁定或依赖清单:实际安装版本。
  3. plugin-capabilities.json:插件能力与宿主策略。

本文不假设 ohpm 内部自动理解 capabilities。它只是提供已安装包的事实;PluginRangeLab 在构建前把事实转成一份稳定 JSON 报告,再由 ArkUI 诊断页读取。

这段代码解决多份清单合并、状态推进和报告留证。

type AuditState =
  | 'DISCOVERED'
  | 'PARSED'
  | 'RANGE_REJECTED'
  | 'CANARY_OPT_IN'
  | 'CAPS_CHECKED'
  | 'DEGRADED_READY';

interface AuditReport {
  taskId: string;
  checkedAt: string;
  packageName: string;
  installedVersion: string;
  stableRange: string;
  canaryRange: string;
  states: AuditState[];
  stableAccepted: boolean;
  canaryAccepted: boolean;
  capability: CapabilityDecision;
}

function buildReport(version: string, caps: string[]): AuditReport {
  const stable = checkVersion(version, 'stable');
  const canary = checkVersion(version, 'canary');
  const capability = negotiateCapabilities(caps, {
    required: ['search.preview'], optional: ['search.batch']
  });
  return {
    taskId: 'SEMVER-CAP-0212', checkedAt: '21:36',
    packageName: 'vision-bridge', installedVersion: version,
    stableRange: stable.range, canaryRange: canary.range,
    states: ['DISCOVERED', 'PARSED', 'RANGE_REJECTED',
      'CANARY_OPT_IN', 'CAPS_CHECKED', 'DEGRADED_READY'],
    stableAccepted: stable.satisfies,
    canaryAccepted: canary.satisfies,
    capability
  };
}

状态序列保留了稳定通道被拒绝的事实。切换到 canary 不是“把失败改成成功”,而是一次显式策略变更,因此报告里同时保留 stableAccepted=false 和 canaryAccepted=true。这对发布复盘很关键:后来看到 DEGRADED_READY,仍能知道它不是稳定通道的常规启用。

开发配图同样是演示画面,不冒充实际 IDE 运行证据。画面中央是 checkVersion 与能力协商,右侧模拟器显示 PluginRangeLab,底部 HiLog 精确列出:stable=false、canary=true、required=1/1、optionalMissing=1、decision=DEGRADED_READY。

五、诊断页只展示报告,不重新判断

如果 ArkUI 页面重新实现一遍版本范围,很容易与构建脚本漂移。更稳妥的做法是:构建工具生成 plugin-audit.json,应用只读取并展示。发布构建可以在 BLOCKED 时直接失败;内部诊断包则保留页面,方便定位来源。

这段代码解决诊断页如何消费既有报告,而不是复制门禁逻辑。

@Entry
@Component
struct CapabilityAuditPage {
  @State report: AuditReport = buildReport('3.0.0-beta.4', [
    'search.preview', 'search.basic', 'telemetry.trace'
  ]);

  build() {
    Scroll() {
      Column({ space: 12 }) {
        Text('插件能力审计').fontSize(28).fontWeight(FontWeight.Bold)
        Text(this.report.taskId)
        Text(`${this.report.packageName}@${this.report.installedVersion}`)
        Row() {
          Text(`稳定通道 ${this.report.stableAccepted ? 'PASS' : 'REJECTED'}`)
          Blank()
          Text(`Canary ${this.report.canaryAccepted ? 'ACCEPTED' : 'REJECTED'}`)
        }.width('100%')
        Text(`必需能力 ${this.report.capability.requiredMatched}/${this.report.capability.requiredTotal}`)
        Text(`缺失可选能力 ${this.report.capability.missingOptional.length}`)
        Text(this.report.capability.decision).fontColor('#B3261E')
        ForEach(this.report.capability.disabledFeatures,
          (feature: string) => Text(`已关闭:${feature}`))
      }.padding(24).width('100%')
    }
  }
}

这里为了文章可读性直接调用 buildReport() 生成固定样例;真实发布工程应读取由门禁脚本落盘的报告,并验证报告摘要、策略版本和生成时间。页面不得把 REJECTED 改成 ACCEPTED,也不得用按钮绕过 required 缺失。

运行图显示 21:36、5G、79% 电量,任务 SEMVER-CAP-0212,插件 vision-bridge@3.0.0-beta.4。稳定通道被拒绝,canary 明确接受,最终进入 DEGRADED_READY。红色箭头只标出预发布范围和缺失的 search.batch,避免把整张页面做成批注海报。

六、四种版本输入,四种处理结果

一个门禁能否长期工作,取决于边界样本而不是顺利样本。本文至少保留四组黄金向量。

第一组是稳定版本 2.9.3。它满足 >=2.8.0 <3.0.0,若必需能力齐全,可以进入 stable 的 FULL_READY 或 DEGRADED_READY。

第二组是本文主样本 3.0.0-beta.4。稳定范围拒绝;canary 显式范围接受;再进入能力门。

第三组是 3.0.1-beta.1。即使它在排序上比 3.0.0-beta.2 新,默认也不能因为 canary 范围包含 3.0.0-beta.2 就顺便放行另一个 patch 元组。官方 prerelease 规则正是为了阻止这种无意扩大。

第四组是 release-3-beta。严格 valid() 返回 null。门禁应输出 INVALID_CONTRACT,保留原字符串,并要求上游修清单;不能 coerce 成一个貌似合理的稳定版本。

此外,还要测试构建元数据。3.0.0-beta.4+sha.91ad 的 precedence 与不带 build metadata 的对应版本相同,但报告仍可保留完整原值做溯源。不要把 build metadata 当成能力或风险级别。

诊断详情图把四个向量、两条范围和能力矩阵放在同一条审计链上:

  • stable:>=2.8.0 <3.0.0,主样本 REJECTED。
  • canary:>=3.0.0-beta.2 <3.0.0,主样本 ACCEPTED。
  • required:search.preview,1/1。
  • optional:search.batch,missing 1。
  • disabled:batch.action。
  • decision:DEGRADED_READY。

七、发布门禁需要保留的五份证据

只把最终决定打印成一行日志不够。建议报告至少包含以下证据。

一是原始版本和规范化版本。两者不同就要说明原因;严格模式下通常不应默默改变。

二是通道与范围。stable、canary 不是环境变量里的随意字符串,而是审批策略的一部分。谁改了 canary 范围,应能从代码评审和报告摘要里追到。

三是 prerelease 数组。['beta', 4] 比手写字符串截取更可靠,也能区分稳定版本。

四是能力差集。报告不要只写 missing=1,要写出 search.batch,并映射到 batch.action。运维看到按钮隐藏时,才能把 UI 变化和插件事实对应起来。

五是工具版本与策略版本。node-semver 的行为由实现版本和 SemVer 规范共同决定;项目策略也会演进。黄金向量应在升级三方库时重新运行,避免工具更新改变边界结果而无人察觉。

在生命周期上,构建脚本是一次性进程,不涉及页面回调释放;诊断页若订阅文件变化或内部事件,则仍要在退场时 off。不要因为它是“内部工具页”就允许监听器常驻,热更新或反复进入会让同一报告被重复加载。

八、不要让 semver 替业务做它做不到的事

SemVer 范围可以表达版本接受区间,却不能证明 API 行为、数据格式、权限、性能和安全边界。能力清单能表达插件声明,却也不能替代集成测试。本文把两者串成门禁,是为了更早阻断明显不一致,而不是给“兼容性”盖最终章。

以下情况要直接扩大验证范围:插件跨 major;能力虽存在但参数合同变化;beta 版本包含本地 native 库;多个 HAP/HSP 引用了不同版本;宿主需要回滚;线上数据格式不可逆迁移。此时除了范围和能力,还要检查包体、ABI、迁移脚本与回滚路径。

本文也没有宣称 ohpm 会采用 node-semver 的全部细节。项目的依赖解析规则应以当前 ohpm 文档与实际锁定结果为准。node-semver 在这里是项目自建审计工具,负责解释插件业务合同;它不取代包管理器。

最后留下一个简单原则:稳定通道不要猜测团队愿意承担的预发布风险,canary 通道不要从版本号猜测功能。范围写出意图,能力写出事实,降级矩阵写出用户最终看到什么。

九、门禁本身也要有升级纪律

版本门禁一旦进入发布链,就不能被当成永远正确的黑盒。三方 semver 包会升级,团队策略会调整,ohpm 的依赖事实也可能因为锁文件或仓库变化而改变。真正可维护的做法,是把门禁看成一项有输入、有版本、有黄金向量的产品能力。

首先固定工具版本。审计脚本使用的 semver 版本应进入锁定文件,并在报告里记录。若只写 ^ 范围又不保存实际解析结果,同一份插件清单可能在不同构建节点上得到不同实现版本。本文不假设不同版本一定产生不同结论,但发布证据必须能回答“当时由哪一版工具判断”。

其次给策略单独编号,例如 plugin-policy-v4。稳定范围、canary 范围、required、optional 和能力到 UI 的映射都属于策略。只改一条范围也要提升策略版本,因为它改变了可发布集合。不要把这些值散在页面、脚本和配置中心三个位置。

再次保存原始输入摘要。报告应对项目依赖清单、实际安装清单、插件能力清单与策略文件分别计算摘要。这样回看 DEGRADED_READY 时,能够确认它对应哪四份输入,而不是只剩一张不可重现的截图。

1. 黄金向量要覆盖“看起来应该通过”的失败样本

很多测试只写明显正确和明显错误两端,恰好遗漏 prerelease 最容易误判的中间地带。除了文中的四个版本,还应该补充这些向量。

3.0.0-beta.1 低于 canary 下界 beta.2,必须拒绝。3.0.0-beta.10 高于 beta.4,但是否允许取决于范围,而不是字符串字典序。3.0.0 是稳定版本,却不满足 <3.0.0;这提醒团队 canary 合同在正式版发布时要显式迁移,而不能期待范围自动延续。

v3.0.0-beta.4 是否接受,要由输入规范决定。node-semver 为兼容历史会处理前导 v,但项目可以选择更严格的清单规则并拒绝它。关键是规则写进策略,不要一处接受、一处报错。

带 build metadata 的 3.0.0-beta.4+sha.91ad 在优先级比较中不因 metadata 提高或降低,但报告应保留完整原值。若发布平台把不同构建摘要视为不同制品,还需要在 semver 决定之后增加制品身份校验,不能把两者混为一谈。

2. 能力清单也可能撒谎

插件声明 search.preview,只说明它声称具备能力。宿主仍应有最小探针,例如加载固定输入并检查返回结构、超时和错误类型。探针失败时,最终决定应从 FULL_READY 或 DEGRADED_READY 降到 BLOCKED_RUNTIME_PROBE,而不是继续相信静态清单。

探针不要承载完整性能测试。它只回答接口是否能按最小合同工作;耗时、内存、精度与异常恢复应由独立集成测试负责。门禁层次越清楚,失败时越容易知道该找包维护者、宿主适配层还是发布流水线。

能力名称也要版本化。search.preview 的参数或结果语义发生不兼容变化时,不应继续沿用同一字符串。可以升级为 search.preview.v2,或在能力描述里增加独立 schemaVersion。仅把插件 major 升到 4,仍无法告诉宿主某项能力的精确数据合同。

十、回滚不是把版本号改小

当 canary 试用失败,最直觉的操作是把插件版本退回 2.9.3。但如果 beta 插件已经写入新格式缓存、索引或配置,降级安装并不等于数据可回滚。版本门禁必须与数据迁移证据相邻,而不能只盯着依赖行。

本文 Demo 没有不可逆数据,因此回滚矩阵很简单:关闭 canary 策略,恢复稳定版本,重新生成审计报告,确认 required 与 optional,再重新跑最小探针。真实插件若有数据写入,应额外声明 dataSchema、minReadableSchema 和迁移方向。

如果 beta 写入 schema 4,而稳定版最多读 schema 3,那么“退回 2.9.3”应该被门禁拒绝,除非存在已验证的降级迁移。此时最安全的操作可能是保持 beta 二进制但关闭功能,先导出或转换数据。依赖版本只是回滚计划的一部分。

多模块工程还要检查是否存在双版本。一个 HAP 引用稳定版,另一个 HSP 或测试模块引用 beta,可能让两套能力清单同时进入制品。ohpm 的递归依赖视图能帮助定位安装事实,业务审计则要按最终模块边界确认实际加载者。不能看到根清单只有一条依赖,就假设制品里只有一个实现。

发布流水线建议把决定分成四级。PASS 表示稳定通道、能力齐全、探针通过;DEGRADED 表示显式通道允许、必需能力齐全、可选能力有缺失且 UI 已同步关闭;BLOCKED 表示范围、必需能力或探针失败;INVALID 表示输入本身无法解释。四级比单一退出码更适合诊断,但真正发布时,只有策略明确允许的级别才能返回成功。

十一、一次版本审计应该怎样被读懂

拿到 SEMVER-CAP-0212 报告时,审查者不需要先阅读脚本。第一页就应回答:检查了哪个包、实际安装版本是什么、处于哪个通道、两条范围分别怎样、预发布标记是什么、required/optional 差集是什么、最终禁用了哪个入口。

第二页再展开证据:原始清单位置、锁定结果、工具版本、策略版本、摘要和黄金向量。这样日常排查只看结论,发布复盘仍能追到底层输入。把所有内容塞进一长串 HiLog,既不利于人读,也不利于机器比较。

日志字段最好保持稳定键名。stable=false、canary=true、required=1/1、optionalMissing=1、decision=DEGRADED_READY 已经足够表达主路径。不要今天写 status、明天写 result、后天改成自然语言,否则跨版本报告很难做差异比对。

审计页面也应标明它是生成报告的呈现,不是运行时事实源。若页面加载失败,发布门禁仍由构建报告决定;若构建报告阻断,页面上的绿色样式不能覆盖它。这种单向关系和本文第一篇的状态协调器类似:展示者消费事实,但不能自封为事实生产者。

最后,门禁失败不应该诱导开发者直接放宽范围。先判断失败属于哪一层:版本没有被通道选择、必需能力缺失、可选能力降级、清单无效、运行探针失败,还是数据不可回滚。只有明确原因后,修改范围才有意义。

这套流程的价值不在于多了一张诊断页,而在于把“装上了所以应该能用”改成一条可审计的推理链。版本规则、通道意图、能力事实、界面降级和回滚条件各自有位置,任何一层变化都能留下清晰证据。

十二、参考与版本边界

文中 stable/canary、capabilities、disabledFeatures 与审计状态机均为应用工程策略。实际项目应固定 semver 工具版本,使用严格输入,保存黄金向量,并以当前 ohpm 锁定结果验证安装事实。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐