本次提交新增了以下核心内容: 1. 后端管理模块:包含字体同步、插件元数据、插件用户设置、稍后读消息/共享列表的控制器、模型、验证器与多语言配置 2. Flutter数据同步模块:统一的事件总线与兼容层,替代分散的StreamController 3. 鸿蒙端路由适配:完整的路由定义、构建器与占位组件 4. 后端API接口:字体同步与插件更新的服务端API,支持自动建表与跨域请求 5. 鸿蒙权限校验脚本:用于校验module.json5与string.json的权限声明一致性
369 lines
12 KiB
Dart
369 lines
12 KiB
Dart
/// ============================================================
|
|
/// 闲言APP — 鸿蒙端权限一致性校验脚本
|
|
/// 创建时间: 2026-05-31
|
|
/// 更新时间: 2026-05-31
|
|
/// 作用: 校验 module.json5 与 string.json 之间的权限声明一致性
|
|
/// - 检查 module.json5 中权限的 reason 引用是否在 string.json 中存在
|
|
/// - 检查 string.json 中 permission_*_reason 键是否被 module.json5 引用
|
|
/// - 检查 user_grant 权限是否缺少 reason 说明
|
|
/// 上次更新: 初始创建
|
|
/// ============================================================
|
|
///
|
|
/// 用法:
|
|
/// dart run tools/check_ohos_permissions.dart
|
|
///
|
|
/// 退出码:
|
|
/// 0 — 校验通过,无错误
|
|
/// 1 — 存在错误(缺失引用或未使用的键)
|
|
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
// ── 路径常量 ──────────────────────────────────────────────────
|
|
|
|
const String projectRoot = 'e:\\project\\flutter\\f\\xianyan';
|
|
const String moduleJson5Path =
|
|
'$projectRoot\\ohos\\entry\\src\\main\\module.json5';
|
|
const String stringJsonPath =
|
|
'$projectRoot\\ohos\\entry\\src\\main\\resources\\base\\element\\string.json';
|
|
|
|
// ── 终端颜色 ──────────────────────────────────────────────────
|
|
|
|
const String _reset = '\x1B[0m';
|
|
const String _red = '\x1B[31m';
|
|
const String _green = '\x1B[32m';
|
|
const String _yellow = '\x1B[33m';
|
|
const String _cyan = '\x1B[36m';
|
|
const String _bold = '\x1B[1m';
|
|
|
|
// ── 数据模型 ──────────────────────────────────────────────────
|
|
|
|
/// module.json5 中的权限条目
|
|
class PermissionEntry {
|
|
final String name;
|
|
final String? reasonRef;
|
|
final String? when_;
|
|
|
|
PermissionEntry({
|
|
required this.name,
|
|
this.reasonRef,
|
|
this.when_,
|
|
});
|
|
}
|
|
|
|
// ── JSON5 解析 ────────────────────────────────────────────────
|
|
|
|
/// 将 JSON5 文本转换为标准 JSON
|
|
/// 处理: 单行注释、多行注释、尾逗号
|
|
String _json5ToJson(String source) {
|
|
final buffer = StringBuffer();
|
|
var i = 0;
|
|
|
|
while (i < source.length) {
|
|
// 单行注释 //
|
|
if (i < source.length - 1 && source[i] == '/' && source[i + 1] == '/') {
|
|
while (i < source.length && source[i] != '\n') {
|
|
i++;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// 多行注释 /* */
|
|
if (i < source.length - 1 && source[i] == '/' && source[i + 1] == '*') {
|
|
i += 2;
|
|
while (i < source.length - 1) {
|
|
if (source[i] == '*' && source[i + 1] == '/') {
|
|
i += 2;
|
|
break;
|
|
}
|
|
i++;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// 字符串字面量 — 原样保留,避免误判注释
|
|
if (source[i] == '"' || source[i] == "'") {
|
|
final quote = source[i];
|
|
buffer.write('"');
|
|
i++;
|
|
while (i < source.length && source[i] != quote) {
|
|
if (source[i] == '\\' && i + 1 < source.length) {
|
|
buffer.write(source[i]);
|
|
buffer.write(source[i + 1]);
|
|
i += 2;
|
|
continue;
|
|
}
|
|
if (source[i] == '"') {
|
|
buffer.write('\\"');
|
|
i++;
|
|
continue;
|
|
}
|
|
buffer.write(source[i]);
|
|
i++;
|
|
}
|
|
buffer.write('"');
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
buffer.write(source[i]);
|
|
i++;
|
|
}
|
|
|
|
var result = buffer.toString();
|
|
|
|
// 移除尾逗号: , 后跟 } 或 ]
|
|
result = result.replaceAll(RegExp(r',\s*([}\]])'), r'$1');
|
|
|
|
return result;
|
|
}
|
|
|
|
/// 解析 module.json5 文件,提取 requestPermissions 列表
|
|
List<PermissionEntry> _parseModuleJson5(String filePath) {
|
|
final file = File(filePath);
|
|
if (!file.existsSync()) {
|
|
stderr.writeln('${_red}错误: 文件不存在 $filePath$_reset');
|
|
exit(1);
|
|
}
|
|
|
|
final raw = file.readAsStringSync();
|
|
final jsonStr = _json5ToJson(raw);
|
|
|
|
late final Map<String, dynamic> root;
|
|
try {
|
|
root = json.decode(jsonStr) as Map<String, dynamic>;
|
|
} catch (e) {
|
|
stderr.writeln('${_red}错误: JSON5 解析失败 — $e$_reset');
|
|
exit(1);
|
|
}
|
|
|
|
final module = root['module'] as Map<String, dynamic>?;
|
|
if (module == null) {
|
|
stderr.writeln('${_red}错误: module.json5 缺少 module 根节点$_reset');
|
|
exit(1);
|
|
}
|
|
|
|
final permissions = module['requestPermissions'] as List<dynamic>?;
|
|
if (permissions == null) {
|
|
stderr.writeln('${_yellow}警告: module.json5 无 requestPermissions 声明$_reset');
|
|
return [];
|
|
}
|
|
|
|
return permissions.map((p) {
|
|
final map = p as Map<String, dynamic>;
|
|
String? reasonRef;
|
|
final reason = map['reason'];
|
|
if (reason is String && reason.startsWith('\$string:')) {
|
|
reasonRef = reason.substring('\$string:'.length);
|
|
}
|
|
|
|
String? when_;
|
|
final usedScene = map['usedScene'] as Map<String, dynamic>?;
|
|
if (usedScene != null) {
|
|
when_ = usedScene['when'] as String?;
|
|
}
|
|
|
|
return PermissionEntry(
|
|
name: map['name'] as String,
|
|
reasonRef: reasonRef,
|
|
when_: when_,
|
|
);
|
|
}).toList();
|
|
}
|
|
|
|
/// 解析 string.json 文件,提取所有 permission_*_reason 键
|
|
Map<String, String> _parseStringJson(String filePath) {
|
|
final file = File(filePath);
|
|
if (!file.existsSync()) {
|
|
stderr.writeln('${_red}错误: 文件不存在 $filePath$_reset');
|
|
exit(1);
|
|
}
|
|
|
|
final raw = file.readAsStringSync();
|
|
late final Map<String, dynamic> root;
|
|
try {
|
|
root = json.decode(raw) as Map<String, dynamic>;
|
|
} catch (e) {
|
|
stderr.writeln('${_red}错误: string.json 解析失败 — $e$_reset');
|
|
exit(1);
|
|
}
|
|
|
|
final strings = root['string'] as List<dynamic>;
|
|
final result = <String, String>{};
|
|
|
|
for (final item in strings) {
|
|
final map = item as Map<String, dynamic>;
|
|
final name = map['name'] as String;
|
|
if (name.startsWith('permission_') && name.endsWith('_reason')) {
|
|
result[name] = map['value'] as String;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// ── 已知的 user_grant 权限列表 ────────────────────────────────
|
|
|
|
/// 鸿蒙系统需要用户授权的权限
|
|
/// 参考: https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/permissions
|
|
const Set<String> _userGrantPermissions = {
|
|
'ohos.permission.CAMERA',
|
|
'ohos.permission.MICROPHONE',
|
|
'ohos.permission.LOCATION',
|
|
'ohos.permission.APPROXIMATELY_LOCATION',
|
|
'ohos.permission.READ_MEDIA',
|
|
'ohos.permission.WRITE_MEDIA',
|
|
'ohos.permission.ACCESS_BLUETOOTH',
|
|
'ohos.permission.NFC_TAG',
|
|
};
|
|
|
|
// ── 校验逻辑 ──────────────────────────────────────────────────
|
|
|
|
/// 校验结果
|
|
class CheckResult {
|
|
final List<String> matched = [];
|
|
final List<String> missingReasons = [];
|
|
final List<String> unusedReasons = [];
|
|
final List<String> permissionsWithoutReason = [];
|
|
|
|
bool get hasErrors => missingReasons.isNotEmpty || unusedReasons.isNotEmpty;
|
|
bool get hasWarnings => permissionsWithoutReason.isNotEmpty;
|
|
}
|
|
|
|
CheckResult _validate(
|
|
List<PermissionEntry> permissions,
|
|
Map<String, String> reasonStrings,
|
|
) {
|
|
final result = CheckResult();
|
|
|
|
// 收集 module.json5 中引用的所有 reason key
|
|
final referencedReasons = <String>{};
|
|
|
|
for (final perm in permissions) {
|
|
if (perm.reasonRef != null) {
|
|
referencedReasons.add(perm.reasonRef!);
|
|
|
|
if (reasonStrings.containsKey(perm.reasonRef)) {
|
|
result.matched.add(perm.name);
|
|
} else {
|
|
result.missingReasons.add(
|
|
'${perm.name} → \$string:${perm.reasonRef}',
|
|
);
|
|
}
|
|
} else {
|
|
// user_grant 权限应该有 reason
|
|
if (_userGrantPermissions.contains(perm.name)) {
|
|
result.permissionsWithoutReason.add(perm.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 检查 string.json 中未被引用的 reason key
|
|
for (final key in reasonStrings.keys) {
|
|
if (!referencedReasons.contains(key)) {
|
|
result.unusedReasons.add(key);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// ── 报告输出 ──────────────────────────────────────────────────
|
|
|
|
void _printReport(CheckResult result) {
|
|
stdout.writeln();
|
|
stdout.writeln('${_bold}╔══════════════════════════════════════════════════╗$_reset');
|
|
stdout.writeln('${_bold}║ 鸿蒙权限一致性校验报告 ║$_reset');
|
|
stdout.writeln('${_bold}╚══════════════════════════════════════════════════╝$_reset');
|
|
stdout.writeln();
|
|
|
|
// ✅ 匹配项
|
|
stdout.writeln(
|
|
'${_green}✅ 匹配的权限 (${result.matched.length})$_reset',
|
|
);
|
|
if (result.matched.isEmpty) {
|
|
stdout.writeln(' (无)');
|
|
} else {
|
|
for (final name in result.matched) {
|
|
stdout.writeln(' $name');
|
|
}
|
|
}
|
|
stdout.writeln();
|
|
|
|
// ❌ 缺失的 reason 字符串
|
|
stdout.writeln(
|
|
'${_red}❌ 缺失的 reason 字符串 (${result.missingReasons.length})$_reset',
|
|
);
|
|
if (result.missingReasons.isEmpty) {
|
|
stdout.writeln(' (无)');
|
|
} else {
|
|
for (final item in result.missingReasons) {
|
|
stdout.writeln(' $item');
|
|
}
|
|
}
|
|
stdout.writeln();
|
|
|
|
// ⚠️ 未使用的 reason 字符串
|
|
stdout.writeln(
|
|
'${_yellow}⚠️ 未使用的 reason 字符串 (${result.unusedReasons.length})$_reset',
|
|
);
|
|
if (result.unusedReasons.isEmpty) {
|
|
stdout.writeln(' (无)');
|
|
} else {
|
|
for (final key in result.unusedReasons) {
|
|
stdout.writeln(' $key');
|
|
}
|
|
}
|
|
stdout.writeln();
|
|
|
|
// ⚠️ 缺少 reason 的 user_grant 权限
|
|
stdout.writeln(
|
|
'${_yellow}⚠️ 缺少 reason 的 user_grant 权限 (${result.permissionsWithoutReason.length})$_reset',
|
|
);
|
|
if (result.permissionsWithoutReason.isEmpty) {
|
|
stdout.writeln(' (无)');
|
|
} else {
|
|
for (final name in result.permissionsWithoutReason) {
|
|
stdout.writeln(' $name');
|
|
}
|
|
}
|
|
stdout.writeln();
|
|
|
|
// 总结
|
|
stdout.writeln('${_cyan}──────────────────────────────────────────────────$_reset');
|
|
if (result.hasErrors) {
|
|
stdout.writeln(
|
|
'${_red}${_bold}结果: 失败 — 发现 ${result.missingReasons.length} 个缺失引用, ${result.unusedReasons.length} 个未使用键$_reset',
|
|
);
|
|
} else if (result.hasWarnings) {
|
|
stdout.writeln(
|
|
'${_yellow}${_bold}结果: 通过(有警告) — ${result.permissionsWithoutReason.length} 个 user_grant 权限缺少 reason$_reset',
|
|
);
|
|
} else {
|
|
stdout.writeln(
|
|
'${_green}${_bold}结果: 通过 — 所有权限声明一致$_reset',
|
|
);
|
|
}
|
|
stdout.writeln('${_cyan}──────────────────────────────────────────────────$_reset');
|
|
stdout.writeln();
|
|
}
|
|
|
|
// ── 入口 ──────────────────────────────────────────────────────
|
|
|
|
void main() {
|
|
stdout.writeln('${_cyan}正在解析 module.json5...$_reset');
|
|
final permissions = _parseModuleJson5(moduleJson5Path);
|
|
stdout.writeln(' 找到 ${permissions.length} 个权限声明');
|
|
|
|
stdout.writeln('${_cyan}正在解析 string.json...$_reset');
|
|
final reasonStrings = _parseStringJson(stringJsonPath);
|
|
stdout.writeln(' 找到 ${reasonStrings.length} 个 permission_*_reason 键');
|
|
|
|
final result = _validate(permissions, reasonStrings);
|
|
_printReport(result);
|
|
|
|
if (result.hasErrors) {
|
|
exit(1);
|
|
}
|
|
}
|