技术栈:HarmonyOS Stage 模型配置、module.json5、Node.js 22、AppGallery Connect 交付前核对
Demo:PermissionMap 任务:PM-403

一次发布前核对,为什么让两个人得出不同结论

发版前夕,经常出现这样的场景:客户端同事打开 module.json5,看见权限已经声明,就说“这部分做完了”;负责上架文案的人看的是另一张表,发现权限用途描述缺了一项,又说“资料还没齐”。两个人其实都说的是自己看到的事实,但他们检查的对象不同。一个是工程声明,另一个是业务用途与展示场景。中间没有对账动作,最容易把缺口留到最后一天。

我为这个问题做了小工具 PermissionMap,任务编号 PM-403。它不会连接 AppGallery Connect,也不尝试替代法律评估、权限授权弹窗或官方审核。做法只是读取工程里的一份 requestPermissions 声明样本,再对照产品团队维护的用途清单,形成可追溯的“哪些已写清楚、哪些尚未回答”报告。它最适合在提交材料之前给研发与运营做一次共同检查。

样本里的四项权限分别负责什么

示例应用是一个维修工单工具,权限声明包含 ohos.permission.CAMERA、ohos.permission.INTERNET、ohos.permission.MICROPHONE 和 ohos.permission.LOCATION。这些权限是为验证差异算法而构造的配置。用途清单给 CAMERA 写了“拍摄维修工单现场照片”,给 INTERNET 写了“上传用户主动提交的工单资料”;MICROPHONE 留空用途,LOCATION 则根本没有对应的业务清单项。

因此期待的不是所有权限都“通过”,而是 CAMERA PASS、INTERNET PASS、MICROPHONE HOLD / PURPOSE_EMPTY、LOCATION HOLD / UNDOCUMENTED。汇总为 PASS 2 / HOLD 2。尤其要注意:LOCATION 没写入这份清单,不等于代码中一定没有使用定位,只能说明当前交付材料没有建立用途关联。脚本将其标为待补充,不擅自判定违规,也不擅自删除权限。

在正式的 HarmonyOS 应用中,权限声明通常处于模块配置的 requestPermissions 节点。是否需要 reason、usedScene、动态申请以及可以在何种设备上使用,需要按照每个具体权限的授权类型和目标 SDK 文档判断。本文只做文本关联检查,不根据权限名称推导全部系统授权行为,更不会制造一个“统一的官方审核规则”。

对比的输入形式先做减法

样本放在 fixtures/module.json5 与 fixtures/purpose-map.json。前者故意采用严格 JSON 子集,虽然扩展名是 json5,但不含注释、尾随逗号或不带引号的字段。这样 Node.js 可以用内置 JSON.parse 正确读取当前样本,无须引入不受控制的文本替换。正式工程如果大量使用 JSON5 扩展语法,必须改用有版本锁定的专用 JSON5 解析器;简单移除斜线注释容易破坏字符串中的 URL。

我首先检查声明节点是否存在且确实为数组,并让语法错误直接失败。这种情况不能默默当成零权限,否则一个解析失败的文件反而被错误地记作检查通过。示例代码如下:

import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';

const TASK_ID = 'PM-403';
function readStrictJson(file) {
  // 示例样本虽然扩展名为 json5,内容限定为严格 JSON 子集。
  return JSON.parse(readFileSync(file, 'utf8'));
}
const moduleConfig = readStrictJson('fixtures/module.json5');
const purposes = readStrictJson('fixtures/purpose-map.json');
const entries = moduleConfig.module?.requestPermissions;
if (!Array.isArray(entries)) throw new Error('MISSING_REQUEST_PERMISSIONS');

readStrictJson 是刻意收窄能力范围的方法名,不要把它写成“JSON5 全语法支持”。这里没有跨 HAP 的声明汇总,也没有验证 reason 引用的多语言字符串是否真实存在。若实际项目由多个模块组成,需要先确定交付清单的聚合策略,不能仅检查 entry 后就宣布全应用已核对完毕。

规则不多,核心在定义失败的含义

第二段代码判断三种主要缺口:用途清单缺失、用途文字为空、场景文字为空。同一权限重复出现在工程声明中,也会显式标为 DUPLICATE_DECLARATION。缺口状态统一用 HOLD,但 reason 仍保留明确分类。这样对接人看到报告后知道该补的是业务目的还是入口场景,而不是被一条含糊的“权限错误”吓住。

function auditPermission(entry) {
  const name = String(entry.name || '');
  const intent = purposes[name];
  let status = 'PASS';
  let reason = 'MATCHED';
  if (!intent) { status = 'HOLD'; reason = 'UNDOCUMENTED'; }
  else if (!String(intent.purpose || '').trim()) {
    status = 'HOLD'; reason = 'PURPOSE_EMPTY';
  } else if (!String(intent.scene || '').trim()) {
    status = 'HOLD'; reason = 'SCENE_EMPTY';
  }
  return { name, status, reason, purpose: intent?.purpose || '', scene: intent?.scene || '' };
}

const seen = new Set();
const results = entries.map(item => {
  if (seen.has(item.name)) return { name: item.name, status: 'HOLD', reason: 'DUPLICATE_DECLARATION' };
  seen.add(item.name);
  return auditPermission(item);
});

这段代码没有用“权限越多就越危险”之类难以验证的主观判断。相反,清单里是否存在明确目的,是一个可复现的文本事实。 CAMERA 和 INTERNET 的结果都是 MATCHED,但只表示这两份资料互相有记录,既不是系统弹窗授权成功,也不是上线审核结论。对于 MIC,脚本没有猜测它可能用于语音输入;对于 LOCATION,没有查到用途就停在待补充状态。

给交付节点一个稳定、可机器处理的结果

输出部分将 taskId、通过数、待补充数和逐项理由写入 JSON 文件。脚本输出顺序跟配置中的声明顺序一致;这让版本差异比对更直观,也让产品人员能够对应原配置找到权限。不影响本地手工查看的普通模式返回 0,带 --ci 的严格模式在存在 HOLD 时返回 2,便于流水线防止将不完整的业务说明误发给外部团队。

const report = { taskId: TASK_ID, pass: results.filter(r => r.status === 'PASS').length,
  hold: results.filter(r => r.status === 'HOLD').length, results };
mkdirSync('reports', { recursive: true });
writeFileSync('reports/permission-report.json', JSON.stringify(report, null, 2) + '\n');
for (const r of results) console.log(`${TASK_ID} ${r.name} ${r.status} ${r.reason}`);
console.log(`${TASK_ID} SUMMARY PASS=${report.pass} HOLD=${report.hold}`);
if (process.argv.includes('--ci') && report.hold) process.exitCode = 2;

这份压缩包里可以直接用 Node.js 执行 node --check tools/permission-audit.mjs 与 node tools/permission-audit.mjs,再打开 reports/permission-report.json 对照四条样本。构造场景的普通执行退出码为 0,CI 变体因为两条 HOLD 预期返回 2。本次已真实运行的是这个开发机上的 Node.js 工具。

检查结果放到手机示意图上有什么价值

我把四项结果放在一屏看得懂的卡片中,权限名作为主标题,下面直接写用途、场景以及 HOLD 的具体原因。操作按钮叫“导出复核清单”,不叫“提交审核”或“修复权限”,因为工具没有权限修改工程,更不会代替开发者向系统申请授权。PM-403 和 PASS 2 / HOLD 2 在移动端示意、Node.js 日志和 JSON 报表中保持一致。

真正需要修改时,我会建议研发先确认权限实际被哪个功能使用:如果功能已经下线,应检查代码引用并评估是否移除声明;如果功能仍在,产品和法务需要共同补充用途说明,并核对与用户可见交互是否一致。对于权限有 reason 与 usedScene 要求的情况,再去检查资源字符串和 Ability 场景映射。不能依赖一份用途清单替代系统权限测试。

测试矩阵与容易忽略的反例

为了防止脚本因顺序变化或空字符串误判,我另列了需要补充的测试:声明数组缺失、权限名称为空、重复声明、用途只有空格、清单含有未声明权限、多个模块声明同一名称、权限项使用非字符串字段,以及 JSON5 文件包含注释或尾逗号。当前实现能报告其中一部分错误,其余应该留在下一轮增强,而不是一次性夸大工具功能。

清单多出未声明项是值得特别处理的反例。它可能意味着产品已经删掉某项功能、代码改名,或打包目标与文案分支不同。本文程序以工程声明为遍历基准,不扫描清单孤儿项,所以不能保证发现这种缺口。正式 CI 应增加反向差集,同时记录输入文件哈希和 Git 提交号,避免拿旧分支结果证明新分支资料齐全。

研发和运营如何用同一份报告沟通

我会在提测清单中把检查状态分成“技术事实”“待解释事项”“正式审查结论”三个层次。第一层由脚本提供,例如 CAMERA 在两份文件中都有相应条目;第二层交给功能责任人解释,例如 LOCATION 为什么声明;第三层则需要依据平台当前要求、业务真实处理方式及必要的合规审阅独立判断。这样的分层能避免将“自动化检查 PASS”错误传播成“官方审核已通过”。

异常路径同样要保留:读文件失败应标记任务失败,而不是产出空的 PASS 列表;报告目录需明确访问权限,避免把含内部功能描述的文件随意上传到公开仓库。当前代码使用同步读取,适合少量配置文件;大量仓库并发扫描时要引入任务排队、超时和异常分组,并尽量不要在日志中输出真实用户数据。

小结和官方资料的使用边界

这次工程练习的收获不是发现了新平台限制,而是给“写了配置”和“说明清楚用途”之间加了一道可以复放的交付门禁。构造输入下,两个 PASS 与两个 HOLD 都能解释到具体字段;实际产品是否需要某个权限、怎样向用户申请、怎样填写上架资料,还必须回到当前版本官方文档逐项判断。

官方参考:华为 Remote Communication Kit 的 module.json5 权限声明示例;华为应用拆包工具中 reqPermissions 与 Stage requestPermissions 的关系;华为广告 Codelab 中动态授权配置示例。

Logo

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

更多推荐