这篇不是把审核指南重新抄一遍。我做了一个叫 ReleaseGuard 的上架自检小工具,把最容易反复出错的几项——版本信息、权限声明、权限申请时机、三方 SDK 隐私披露——尽量变成机器能提前发现的问题。

有一次准备提交 HarmonyOS 应用,我在 DevEco Studio 里已经把功能跑通,测试包也没崩,真正到发布阶段却发现“能运行”和“适合提交审核”中间还隔着一层很具体的工程工作。

官方的 HarmonyOS 应用提交页面把发布前测试拆得很清楚:漏洞、隐私、兼容性、稳定性、性能都需要在提交前检查;应用基本信息、素材、隐私保护和审核资质也不是最后点一下按钮就自动完成。对于集成第三方 SDK 的应用,官方上架说明还特别强调,隐私政策需要逐一说明相关 SDK 收集个人信息的目的、方式和范围,否则可能影响审核。

这些要求本身并不难理解,真正麻烦的是它们散落在代码、配置、依赖和发布后台里。开发者经常是功能改了一处,发布信息忘了同步;依赖升级了,隐私声明没补;module.json5 里声明了权限,就误以为“权限这项已经处理完了”。

于是我建了一个小工程 ReleaseGuard。它不是官方审核工具,也不会替代 AppGallery Connect 的最终审核,目标只有一个:在我上传 APP 包以前,把能自动检查的低级问题先拦下来。

本文 Demo 固定使用下面这组数据:bundleName=com.example.releaseguard,versionName=1.4.2,versionCode=1040203。第一次执行一共 9 项检查,只通过 7 项;两个失败项分别是“隐私声明缺少 analytics-sdk@3.2.1”以及“用户同意前触发 CAMERA 权限申请”。修复之后再次执行,结果变成 9/9。

这里的 analytics-sdk@3.2.1 是我为了演示流程使用的虚构依赖名,不对应某个真实 SDK。

一、把“审核退回原因”翻译成工程规则

我没有一开始就写脚本,而是先把过去最容易出错的发布问题分成两类。

第一类是机器很擅长发现的确定性问题,比如版本号是否一致、某个依赖是否存在、声明的权限有哪些、隐私披露清单有没有对应条目、release 包是不是用了调试签名。这些规则非常适合放进 CI 或本地脚本。

第二类是机器只能辅助、人仍然要判断的问题,比如应用业务是否需要某项资质、隐私政策文字是否真正清楚、权限使用目的是否与实际功能匹配、页面是否存在误导性描述。这部分不能因为脚本显示绿色就当成“审核一定通过”。

ReleaseGuard 只处理第一类,第二类永远留给人工复核和官方最终审核。这个边界很重要,否则一个“9/9”很容易制造错误的安全感。

我给每条规则定义了同样的结果结构:

id / title / passed / evidence / message

evidence 是关键。检查脚本说“版本一致”还不够,它要告诉我读取的是哪个文件、值是什么;说“隐私披露缺项”,要列出缺的是哪个依赖。这样失败结果才真正能指导修改。

二、版本一致性是最适合自动检查的一项

HarmonyOS Stage 模型工程里,应用级配置通常位于 AppScope/app.json5,版本名称和版本编码会进入最终应用包。发布时后台填写的信息、你准备对外说明的版本、代码仓里的版本,最好在构建之前就对齐。

我的 Demo 要求这次发布固定为:

bundleName  com.example.releaseguard
versionName 1.4.2
versionCode 1040203

只要脚本读出来不是这组值,就直接失败,不继续生成“审核就绪”结果。

这段代码解决什么问题:构建前读取 app.json5,把包名和版本与发布配置进行硬校验。

// scripts/release-check.ts
import fs from 'node:fs'
import JSON5 from 'json5'

interface ReleaseTarget {
  bundleName: string
  versionName: string
  versionCode: number
}

const target: ReleaseTarget = {
  bundleName: 'com.example.releaseguard',
  versionName: '1.4.2',
  versionCode: 1040203
}

const appConfig = JSON5.parse(
  fs.readFileSync('AppScope/app.json5', 'utf-8')
)

const actual: ReleaseTarget = {
  bundleName: appConfig.app.bundleName,
  versionName: appConfig.app.versionName,
  versionCode: appConfig.app.versionCode
}

function assertEqual(name: keyof ReleaseTarget): void {
  if (actual[name] !== target[name]) {
    throw new Error(
      `[ReleaseCheck] ${name} mismatch: ` +
      `expected=${target[name]}, actual=${actual[name]}`
    )
  }
}

assertEqual('bundleName')
assertEqual('versionName')
assertEqual('versionCode')
console.info('[ReleaseCheck] version consistency: PASS')

我没有让脚本去猜“下一个版本应该是多少”。版本策略属于团队规则,脚本只负责验证当前这次发布目标。目标值可以来自环境变量、发布配置文件或 CI 参数,重点是它必须只有一个可信来源。

另外,我会在生成 APP 后再做一次包级验证,而不是只读源码配置。原因很简单:真正上传的是构建产物,不是 app.json5。源码正确但构建分支、签名配置或产物选错,一样可能提交错包。

我还会把这组版本信息同步到发布说明草稿里,避免出现“包里是 1.4.2,运营文案还写 1.4.1”的尴尬。这个问题脚本未必能直接判断,但可以让发布配置生成一份 release-meta.json,应用包校验、CI 构建记录和运营侧都读取同一个版本源。对于多人协作项目,这比在群里发一句“这次版本号改成多少”可靠得多。

如果项目同时维护测试、灰度和正式渠道,我还会额外检查渠道对应的 bundle、签名和构建 profile,避免把内部测试产物误当成正式包。版本一致性看起来只是三个字段,实际上它是整条发布链路最适合建立“单一事实来源”的地方。

三、权限声明和权限申请时机是两件事

这是我觉得最容易被混在一起的地方。

module.json5 的 requestPermissions 解决的是“应用声明自己需要哪些权限”。真正涉及用户授权的权限,还要在业务需要时通过访问控制能力查询状态并发起授权请求。官方 FAQ 也把 getSelfPermissionStatus() 与 requestPermissionsFromUser() 作为定位授权状态的关键接口。

换成工程语言就是:配置文件回答“可能会用什么”,运行时代码回答“什么时候真的向用户要”。

如果应用一启动就把 CAMERA、麦克风、相册等权限连续弹出来,即使配置层面没有语法错误,用户体验和隐私合规都很难说合理。更稳妥的方式是把权限申请放到清晰的功能动作后面,例如用户点击“扫码”时再申请 CAMERA。

ReleaseGuard 的 Demo 里,我专门做了一个 PermissionGate。只有隐私提示已经完成,并且用户主动进入扫码功能时,才允许发起相机授权。

这段代码解决什么问题:避免应用启动时提前申请 CAMERA,把授权动作绑定到真实功能触发。

// entry/src/main/ets/service/PermissionGate.ets
import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit'

export class PermissionGate {
  private consentAccepted: boolean = false

  setPrivacyConsent(accepted: boolean): void {
    this.consentAccepted = accepted
  }

  async requestCameraForScan(
    context: common.UIAbilityContext
  ): Promise<boolean> {
    if (!this.consentAccepted) {
      console.warn('[PermissionGate] CAMERA blocked before consent')
      return false
    }

    const permission: Permissions = 'ohos.permission.CAMERA'
    const manager = abilityAccessCtrl.createAtManager()
    const status = manager.getSelfPermissionStatus(permission)

    if (status === abilityAccessCtrl.PermissionStatus.GRANTED) {
      return true
    }

    const result = await manager.requestPermissionsFromUser(
      context,
      [permission]
    )

    return result.authResults.length > 0 && result.authResults[0] === 0
  }
}

这里的重点不是“必须先放一个同意开关”这种固定模板,而是权限要有明确业务上下文。你的产品流程可能和我的 Demo 完全不同,但至少应该回答:用户刚刚做了什么操作?为什么此刻需要这项权限?拒绝之后功能如何降级?

如果这些问题说不清楚,脚本即使能检测到 requestPermissions,也不应该给“权限合规”打绿色勾。

四、我把 DevEco Studio 里的检查结果做成可读日志

这张图对应第一次执行 ReleaseGuard 的状态。右边模拟器显示 7/9,通过项包括版本一致性、签名信息和权限声明;失败项有两个:

  • 隐私声明缺少 analytics-sdk@3.2.1;
  • CAMERA 在用户同意前被触发。

底部 HiLog 也打印同样的信息:

[ReleaseCheck] version=1.4.2 code=1040203
[Privacy] analytics-sdk@3.2.1 disclosure missing
[PermissionGate] CAMERA blocked before consent
[ReleaseCheck] finish: 7 / 9 passed

我很在意“模拟器 UI”和“日志”必须说同一件事。开发工具页显示 7/9,日志就不能写 8/9;UI 写 1040203,脚本也必须读取到 1040203。发布类问题最怕多个来源互相矛盾,因为你最后很难判断应该信谁。

所以 ReleaseGuard 的页面本身不重新计算结果,它只渲染脚本/检查服务返回的 CheckResult[]。

五、三方 SDK 披露不能靠人肉记忆

官方上架说明已经把这件事说得很明确:集成第三方 SDK 时,需要在应用隐私政策中逐一明示 SDK 收集个人信息的目的、方式和范围。现实项目里最容易发生的情况是:依赖升级了,开发者知道,隐私文档维护者不知道。

我给项目加了一个自定义清单 release/privacy-sdk.json。注意,这不是 HarmonyOS 官方规定的文件格式,只是我自己的工程约定,目的是让“代码依赖”和“隐私披露”之间可以被脚本比较。

例如:

{
  "disclosed": [
    "network-core@2.4.0",
    "image-cache@1.8.3"
  ]
}

而依赖清单里存在 analytics-sdk@3.2.1,那脚本就直接把它列为缺项。

这段代码解决什么问题:从依赖集合中找出没有进入隐私披露清单的三方包。

// scripts/privacy-check.ts
import fs from 'node:fs'
import JSON5 from 'json5'

const pkg = JSON5.parse(
  fs.readFileSync('oh-package.json5', 'utf-8')
)
const privacy = JSON.parse(
  fs.readFileSync('release/privacy-sdk.json', 'utf-8')
)

const dependencyNames = Object.keys(pkg.dependencies ?? {})
const disclosedNames: string[] = privacy.disclosed ?? []

const missing = dependencyNames.filter((name) => {
  return !disclosedNames.some((item) => item.startsWith(`${name}@`))
})

if (missing.length > 0) {
  console.error(`[Privacy] disclosure missing: ${missing.join(', ')}`)
  process.exitCode = 1
} else {
  console.info('[Privacy] third-party disclosure: PASS')
}

真实项目里当然不能只比较“包名有没有出现”。还要确认版本、采集信息类型、使用目的、调用场景和供应商说明是否发生变化。这个脚本只负责把“完全忘了披露”这种错误提前抓出来,后面的内容核对仍然必须人工做。

我甚至建议把“依赖变更”纳入代码评审模板。只要 PR 修改了 oh-package.json5 或 lock 文件,就要求顺手确认隐私披露是否需要更新。这样比临近发布时靠一个人从头翻依赖靠谱得多。

六、7 / 9 这个页面的作用,是阻止我带病提交

手机页里我没有做很复杂的视觉设计,核心就是让失败项足够明显。当前结果 7/9,78%,底部“准备提交审核”按钮保持不可用。

两个红色标记对应两个完全不同的问题。

“隐私披露缺项”是静态工程问题,改文档和披露清单;“授权时机错误”是运行时业务问题,需要改代码流程。把它们都叫“隐私问题”太宽泛,真正修复时反而不知道动哪里。

我把检查项拆得尽量具体:

  • 版本一致性;
  • 签名信息;
  • 隐私声明;
  • 权限申请声明;
  • 权限申请时机;
  • 三方 SDK 披露;
  • 包信息;
  • 发布构建类型;
  • 调试标记检查。

这些项目不是官方审核条目的完整映射,只是 ReleaseGuard 的工程检查集合。官方审核规则和资质要求会更新,最终仍以 AppGallery Connect 当前页面、审核指南和审核意见为准。

七、修完以后,我要求 9 / 9 还要有证据

我先在隐私声明中补齐 Demo 依赖 analytics-sdk@3.2.1 的说明,再把 CAMERA 申请移动到用户完成隐私提示、点击“扫码”功能之后。重新执行检查后,ReleaseGuard 才允许显示“审核就绪”。

这次结果和第一次形成一一对应:

  • versionName=1.4.2、versionCode=1040203 没变;
  • 隐私披露记录在 2026-09-30 11:05 补齐;
  • CAMERA 授权流程在 2026-09-30 11:12 调整为按需申请;
  • 最终检测时间是 2026-09-30 11:18:21;
  • 9 项全部通过。

我特意在页面里保留“问题修复记录”,而不是只显示一个大绿勾。因为发布版本经常要回溯:这次为什么改了权限流程?为什么隐私政策突然多了一条 SDK?如果只有最终结果,过两周团队自己都忘了。

八、工具脚本最好接在构建前,而不是上传后

如果 ReleaseGuard 只能靠开发者想起来时手动点一下,它很快就会沦为摆设。我更倾向于把确定性检查放到发布构建前。

一个简单的顺序是:

准备 release 参数
→ 运行版本一致性检查
→ 运行依赖 / 隐私披露检查
→ 检查权限声明
→ 构建 release APP
→ 校验产物包信息和签名
→ 真机执行权限场景用例
→ 人工复核隐私政策、资质、截图和应用信息
→ 上传 AppGallery Connect

这样做的好处是,越便宜的问题越早失败。版本号错了,就不要浪费时间跑后面的真机回归;三方 SDK 披露缺项,也不要等包上传后才发现。

官方提交页面同样建议在发布前完成漏洞、隐私、兼容性、稳定性和性能等测试。ReleaseGuard 只是把其中一小部分工程检查左移,并不能替代云测试、云调试或正式审核。

九、我会为每次提交留一个“证据包”

ReleaseGuard 做到后面,我又多加了一件很土但特别有用的事:每次真正准备提审时,把这次检查结果导出成一个小证据包。它不需要很复杂,至少包含应用包名、版本名、版本号、构建时间、依赖摘要、权限列表、检查结果和最终 APP 文件的哈希。

这么做不是为了给审核平台看,而是为了给团队自己留底。发布后的线上问题经常会问:“商店里的 1.4.2 到底是哪次构建?”如果只剩一个版本号,而 CI 当天又跑过好几次,很容易对不上。证据包能把“源码提交—构建产物—提交版本”串成一条线。

我还会把隐私披露清单的摘要放进去,例如本次构建识别到哪些三方依赖、哪些被标记为需要披露、人工复核人是谁。这里并不保存大段隐私政策正文,只保留版本和校验结果,避免证据包反过来变成维护负担。

权限也一样。工具可以把 module.json5 中声明的权限和测试场景记录下来,例如 CAMERA 是在“扫码”按钮触发后申请,而不是启动时申请。等以后某个版本又把权限弹窗提前了,回看两个证据包就能发现行为变化,而不只是凭印象争论“以前是不是也这样”。

这个做法还有一个附带收益:当审核反馈发生时,我可以直接在对应证据包上补一条“审核反馈—修改提交—再次检查”的链路。久而久之,团队会积累出真正属于自己业务的发布知识,而不是每次都从公开文档重新搜索。

十、CI 里的检查必须能阻断错误构建

很多团队其实已经有各种检查脚本,但最后都变成一排绿色日志,因为脚本失败了也不影响构建继续往下跑。我不希望 ReleaseGuard 变成这种“仅供参考”的装饰,所以确定性规则失败时会直接返回非零退出码。

例如版本号不一致、release 构建仍带调试标记、三方依赖完全没有进入披露清单,这些都应该在生成正式产物之前阻断。开发者可以修改规则,也可以在特殊情况下走审批后的 override,但不能默默忽略。

另外,CI 里不要把所有检查塞进一个巨大的 release-check。我更喜欢拆成 version-check、privacy-check、permission-check、package-check,最后再汇总。这样某一项失败时,日志定位更快,规则也更容易由不同负责人维护。

真正需要人工判断的项目则明确输出 REVIEW_REQUIRED,而不是假装 PASS。例如行业资质、隐私文案质量、商店截图是否与当前版本一致,都应该进入发布工单的人工勾选项。自动化最怕边界不清,什么都想判断,最后没人再相信它的结果。

我甚至会要求 CI 保存每一项检查的版本号。因为审核规则、SDK 和脚本都会变化。同样是“9/9”,2026 年 9 月的规则和半年后的规则可能不是同一套。把规则版本一起记录,之后回溯才有意义。

十一、还有三类问题我不会让脚本自动判“通过”

第一类是资质。某些业务面向中国大陆发布时会涉及 ICP、软件著作权或行业资质,具体要求会随业务类型和规则更新。脚本最多提醒“这一栏待人工确认”,不能根据几个关键词就替你判断是否具备合法资质。

第二类是隐私文字质量。脚本能发现“SDK 名字没写”,却很难判断你的收集目的描述是否清晰、是否和真实调用一致,也难判断某个业务是不是存在过度收集。这里必须由产品、法务和开发一起看实际流程。

第三类是页面内容与运营素材。截图、文案、登录流程、会员权益、广告展示、用户生成内容等都会影响审核。它们不是一个 JSON5 文件能覆盖的。

所以我给 ReleaseGuard 的 9/9 定义得非常窄:代表这 9 条工程规则都通过,不代表应用一定通过审核。

十二、上架稳定之后,真正省下来的不是一次审核时间

我以前会把上架问题看成发布当天的工作:版本做完了,开始填后台、补截图、检查隐私。现在更倾向于把它当成开发过程的一部分。

加一个依赖时,就顺手看它会不会影响隐私声明;增加一个权限时,同时设计申请时机和拒绝后的降级;准备版本时,版本号从同一个发布配置生成;真正要上传时,脚本只负责确认团队之前已经做过这些事情,而不是临时补课。

这也是 ReleaseGuard 这个小工具最有价值的地方。它没有什么复杂算法,不过是读几个配置、比几个集合、检查几个状态。但它把“我应该没忘吧”变成了“这里明确还有两项没过”。

上架审核很多时候并不是难在规则看不懂,而是难在工程变化和发布资料长期保持一致。把能自动化的确定性问题交给脚本,把需要判断的部分明确留给人,发布流程反而会简单很多。

最后留一句我现在每次提审前都会看的话:不要把“应用能跑”当成发布完成,真正的 release 版本还应该能解释自己的版本、权限、依赖和隐私边界。

参考资料

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐