Demo:PermissionLedger | 示例任务:PERM-205
方向:应用上架前静态检查 / module.json5 / Node.js
声明:文中日志、界面、数据和配图均为构造示意,未声称完成 DevEco Studio 编译、真机授权或华为审核。

一个并不起眼的发布阻断点

应用准备上架时,开发同学通常会盯着包名、版本号、签名和截图。权限说明却容易变成“看上去配置过就算完成”的项目:module.json5 中存在 requestPermissions,每个权限项也写了 reason,但某个理由字符串在重命名资源后已经不存在。配置文件本身仍然是一段能够被解析的文本,代码审查也未必看得出异常,真正需要授权说明时才暴露引用失配。比起上架前靠肉眼在多个文件之间来回对照,增加一道可重复执行的本地静态门禁更符合工程习惯。

这次的 PermissionLedger 不申请权限,不弹授权框,也不发送包体给后台。它只做一件明确而可解释的事:取出项目 entry/src/main/module.json5 的 module.requestPermissions,筛选其中采用 $string:名称 格式的 reason,逐个检查 resources/base/element/string.json 的 string 数组是否有同名且非空的文案。输出区分 PASS 与 HOLD;只要存在无法解析、缺文件、缺键或文案为空,就让脚本非零退出,阻止构建流水线继续把这份资源快照当成可提交版本。

这里有一个必须先说明的边界:资源引用存在不代表权限确实必要,更不代表用户已经同意或审核必然通过。华为对某些敏感、受限权限的隐私说明、发布签名和使用场景另有要求。我们的工具只是把“资源名失联”这个窄小但高频的低级失误前移,并将业务合规判断交给清单审查人员,不能用它取代官方规则或者法律判断。

先把可复现输入固定下来

为避免截图和代码各说各话,演示项目只放两条构造权限。相机权限的理由指向 camera_reason,麦克风权限的理由指向 mic_reason。资源文件暂时只有相机理由,故预期是 CAMERA PASS、MICROPHONE HOLD,汇总 PASS 1 / HOLD 1,任务编号始终为 PERM-205。下面的字段组织来自 HarmonyOS 模块配置的常见结构,但示例只用于演示资源对账,并不替你决定申请哪项权限。

// entry/src/main/module.json5(只展示相关字段)
{
  module: {
    name: 'entry',
    type: 'entry',
    requestPermissions: [
      { name: 'ohos.permission.CAMERA',
        reason: '$string:camera_reason',
        usedScene: { abilities: ['EntryAbility'], when: 'inuse' } },
      { name: 'ohos.permission.MICROPHONE',
        reason: '$string:mic_reason',
        usedScene: { abilities: ['EntryAbility'], when: 'inuse' } }
    ]
  }
}

基础语言资源用 HarmonyOS 的数组结构,而非把整份文件误当成普通键值字典。camera_reason 的文本在这个 Demo 里写成“用于拍摄采集样片”,mic_reason 故意不出现。另一个细节是 Unicode、空格和不可见字符:资源名必须按照精确字符串匹配,不能为了“宽松兼容”擅自大小写折叠;文案内容则应做 trim,空白字符串不能算通过。特别是多人协作的工程,静态门禁应该给出原始资源键名,而不是只提示“配置错误”。

{
  "string": [
    { "name": "camera_reason", "value": "用于拍摄采集样片" },
    { "name": "app_name", "value": "PermissionLedger" }
  ]
}

脚本不是扫到字符串就结束

把逻辑写成独立的 Node.js CLI,是为了能放到开发机或者 CI 中重复运行。这里用成熟的 json5 解析器处理 JSON5 注释、尾逗号和非双引号键名,避免以正则表达式替代语言解析;用 Node 内置的 readFile 读取文件,用 Map 管理资源索引。每个权限项生成一条记录,再统一汇总,这样后续补做 CSV 报告或把错误定位回对应模块,也不需要重写核对核心。

// tools/permission-audit.mjs(运行前 npm install json5)
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import JSON5 from 'json5';

const id = 'PERM-205';
const root = process.cwd();
const configPath = path.join(root, 'entry/src/main/module.json5');
const resourcePath = path.join(root,
  'entry/src/main/resources/base/element/string.json');

async function audit() {
  const config = JSON5.parse(await readFile(configPath, 'utf8'));
  const res = JSON.parse(await readFile(resourcePath, 'utf8'));
  const names = new Map((res.string ?? [])
    .map(item => [item.name, String(item.value ?? '').trim()]));
  const rows = [];
  for (const perm of (config.module?.requestPermissions ?? [])) {
    const name = String(perm.name ?? 'UNKNOWN');
    const reason = String(perm.reason ?? '');
    if (!reason.startsWith('$string:')) {
      rows.push({ name, state: 'MANUAL', key: reason });
      continue;
    }
    const key = reason.slice('$string:'.length);
    const state = names.has(key) && names.get(key) ? 'PASS' : 'HOLD';
    rows.push({ name, state, key });
  }
  const pass = rows.filter(v => v.state === 'PASS').length;
  const hold = rows.filter(v => v.state === 'HOLD').length;
  for (const row of rows) console.log(`[${id}] ${row.name} ${row.state} ${row.key}`);
  console.log(`[${id}] PASS ${pass} / HOLD ${hold}`);
  if (hold > 0 || rows.some(v => v.state === 'MANUAL')) process.exitCode = 1;
}
audit().catch(e => {
  console.error(`[${id}] INPUT_ERROR ${e.message}`);
  process.exitCode = 2;
});

这里没有引入不存在的 HarmonyOS 端侧权限扫描 API。node:fs/promises、node:path 是 Node.js 标准能力,json5 是需要明确安装的第三方解析器。把脚本放在 tools 下,通过 node tools/permission-audit.mjs 执行即可。完整示例与构造输入随压缩包一起给出;如果本地离线环境没有安装 json5,应先完成依赖安装再运行,而不是声称已验证实际输出。

结果要写得让人能修

针对给定的两个权限项,预期日志是 PERM-205 CAMERA PASS camera_reason 和 PERM-205 MICROPHONE HOLD mic_reason,最后汇总 PASS 1 / HOLD 1。这不是服务端审核反馈,也不是执行过的真机日志。处理者应根据 mic_reason 直接定位 string.json,补上真实、具体、与麦克风功能相符的使用理由;补齐后重新运行,并要求 HOLD 为零再进入人工复核。若产品根本没有录音功能,更合理的修复是先评审是否应该删除该权限声明,而不是为了通过脚本硬编一段理由。

预期结果也需要反向设计测试用例。将 camera_reason 改成全空格,脚本应报 HOLD;删除整个 string.json,应走 INPUT_ERROR 分支并非零退出;将 reason 改成直接文本,当前保守策略应标记 MANUAL,不擅自认为符合发布规范。再加入第三条权限但未写理由,也应交由人工判定具体权限是否需要理由,而非统一贴上“违规”标签。这样的分支设计看似不够自动化,却能避免错误规则把正常配置误判为合规。

真正接入项目时还要加几道保险

第一道保险是多语言。基础语言资源存在不等于英文或者其他地区的资源已经覆盖。可以将每一个需要发布的资源目录加入遍历范围,依照产品自己的本地化策略检查缺键和空文案;默认的兜底行为、语言合规要求需要另行确认,不能凭一个 base 检查就下结论。第二道保险是多模块。示例固定 entry 是为了讲清输入输出,实际工程若有 feature HAP,应根据打包清单遍历参与发布的模块,同时避免把其他模块的资源路径误当成当前模块资源。

第三道保险是报告可追溯性。提交时可以将模块路径、权限名、资源键、检查时间以及脚本版本写入文本报告,但不要把账号、秘钥或用户资料混入日志。权限使用场景视频、应用隐私说明、发布签名中的授权配置依然需要人工核对。尤其不能把“PASS 2”翻译成“可以通过应用市场审核”,这两个结论处于不同层级。工具脚本负责确定性事实,审核人员负责解释业务必要性和面向用户的说明是否充分。

交给 CI 之前再做一次反例演练

把门禁接进流水线时,推荐先在临时分支做三组最小反例:把有效理由删除、把文案设为空字符串、把整个模块配置写成非法 JSON5。三者应该分别落入资源缺失、资源空值和解析错误,而不应该都压成“校验失败”四个字。如果工程里存在条件编译和多套构建产物,还需要核查实际构建选用的模块目录;脚本读取仓库里某份配置,并不等价于检查了最终交付包的所有声明。检查入口最好与产物生成所使用的工作目录保持一致,并让工作流明确记录本次审核使用了哪个 Git 提交、哪个工具版本。这样即使后来修改文案,也能知道当时核对的是哪一份文件。

还有一条容易被忽略的规则:不要在检查失败时自动补写一个“默认理由”来让流水线变绿。权限理由是面向用户解释实际用途的业务承诺,脚本不具备理解采集目的的能力。自动填充会把原本可见的缺口悄悄掩盖,让最终审核者误以为文案已经审阅。把失败留在报告中,要求开发和产品说明是否继续保留权限,反而是更稳妥的发布方式。

小结与参考

PermissionLedger 最值得保留的不是两条 if,而是清楚的失败边界:资源键确实存在才 PASS,无法给出确定结论就 MANUAL,输入损坏则 INPUT_ERROR。团队可以在不依赖设备、也不接触个人数据的情况下,把权限理由的静态引用从发布前肉眼巡检变成可重复执行的构建门禁。进一步扩展前应先固定发布地区、模块集合与人工负责人的签字流程,避免把一个轻量工具拔高成政策判定系统。

核对资料:华为 AppGallery Connect《Configuring a Privacy Description》(2025-11-05 更新,https://developer.huawei.com/consumer/es/doc/app/agc-help-release-app-privacy-desc-0000002313477969);华为官方模块配置实例(https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/graphics-accelerate-gamebuddyservice-prepare)。示例未引用非公开审核规则,也未声称官方提供本文 CLI。

Logo

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

更多推荐