`),表格(``)严格保留。翻译符合法律文书风格。
+
+#### 4. 任务D:App 内隐私政策简版补全为详版
+App 内 `privacy_policy_page.dart` 章节由 10 章补全至 17 章,与网页详版章节结构对齐(用户选择"补全章节保持简版"方案,不引入表格 widget)。
+
+| 新增章节 | 内容摘要 |
+|---|---|
+| 0. 定义 | 9 个术语定义(个人信息/敏感信息/处理者/匿名化/去标识化等) |
+| 2.5 业务功能与个人信息映射表 | 5 项业务功能 × 个人信息映射(纯文本,网页详版含表格) |
+| 7. 第三方 SDK | 当前未集成声明 + 未来集成承诺 |
+| 7.5 自动化决策 | 个性化推荐说明 + 退出权 + 禁止用户画像承诺 |
+| 8.5 个人信息安全事件处置 | 5 步应急响应(72h 评估/通知/报告/记录 3 年) |
+| 9.5 个人信息保护影响评估 | 5 种触发情形 + 评估内容 + 3 年保留 |
+| 12. 国际数据保护 | GDPR/CCPA/COPPA 简版,欧盟代表声明为"暂无代表" |
+
+保持现有 `_buildSection + 纯文本` UI 风格,不引入表格 widget。文件头注释按规范更新。**隐私政策正文"最后更新日期"保持 2026年5月7日 不变**(用户要求)。
+
+#### 5. 涉及文件
+- `lib/l10n/types/t_about.dart` — 新增 entityInfoMenu/entityInfoMenuDesc 字段(构造/字段/toMap/fromMap 四处)
+- `lib/l10n/languages/*.dart` — 14 个语言文件补全 entityInfoMenu/entityInfoMenuDesc 翻译
+- `lib/features/profile/presentation/about_page.dart` — _LegalSection 新增主体信息菜单项 + _showEntityInfoDialog 弹窗 + _entityLine 渲染辅助方法
+- `lib/features/settings/presentation/privacy/privacy_policy_page.dart` — 章节补全(0/2.5/7/7.5/8.5/9.5/12)+ 文件头更新
+- `docs/toolsapi/public/agreements/privacy-policy.html` — 英文区补全 8 章节 + 章节号顺延 + GDPR 欧盟代表表述修正(中英两处)
+- `CHANGELOG.md`
+
+#### 6. 验证
+- `flutter analyze lib/l10n/languages/ lib/l10n/types/t_about.dart lib/features/profile/presentation/about_page.dart lib/features/settings/presentation/privacy/privacy_policy_page.dart` — No issues found!(8.2s)
+- 网页版通过 SFTP 上传至 `https://tools.wktyl.com/agreements/privacy-policy.html`,curl 验证新增章节已上线
+
+#### 7. 不足与建议
+- App 内隐私政策简版补全后,表格(如业务功能映射表)用纯文本代替,信息密度低于网页版。如未来需要更丰富展示,可考虑用 `Table` widget 重构
+- 网页英文区比中文区多「IX. Deceased Person's Information」章节(英文区原有保留),未来可考虑中文区也补这一章
+- 关于页弹窗内容仍为硬编码中英对照,未来如需深度多语言,可提取到 `t_about.dart` 字段
+
+---
+
+***
+
+## [v6.132.0] - 2026-06-26
+
+### ✨ 新增(隐私政策披露 D-U-N-S 编号 + 主体信息章节 — 适配全球上架合规)
+
+#### 背景
+工作室已取得邓白氏码(D-U-N-S Number: 586261192),App 计划在 Apple App Store / Google Play 全球上架。原隐私政策(App 内 + 网页版)仅披露公司名、统一社会信用代码、ICP 备案,缺少国际通用的企业身份标识,不利于海外监管机构、合作方与用户进行主体身份核验。GDPR 第 13/14 条要求"数据控制者"身份清晰披露,D-U-N-S 作为全球企业唯一标识可显著提升合规性并降低审核争议。
+
+#### 1. App 内隐私政策页(privacy_policy_page.dart)
+- 在「8. 政策更新」与「9. 联系我们」之间新增「9. 主体信息」章节
+- 原「9. 联系我们」顺延为「10. 联系我们」
+- 「9. 主体信息」披露:主体名称(中英)、经营别称(DBA)、D-U-N-S 编号、统一社会信用代码、注册地址(中英)、所属行业、隐私事务联系邮箱
+- 数据源:Dun & Bradstreet 官网 + 工商注册信息
+- 文件头注释按规范更新「更新时间 / 上次更新内容」
+- **隐私政策正文"最后更新日期"保持 2026年5月7日 不变**(用户要求)
+
+#### 2. 网页版隐私政策(privacy-policy.html)
+- **中文区**:在「十、联系我们」前插入「十、主体信息」;原「十、联系我们」顺延为「十一、联系我们」;原「十一、国际数据保护」顺延为「十二、国际数据保护」,其子章节 11.1/11.2/11.3 同步顺延为 12.1/12.2/12.3
+- **英文区**:在「IX. Contact Us」前插入「IX. Entity Information」;原「IX. Contact Us」顺延为「X. Contact Us」;原「X. Legal Application and Dispute Resolution」顺延为「XI. Legal Application and Dispute Resolution」,其子章节 10.1/10.2 同步顺延为 11.1/11.2
+- **footer**:在「统一社会信用代码」与「ICP 备案号」之间新增 D-U-N-S 一行;地址统一更新为 D-U-N-S 官网注册地址(中国云南省红河哈尼族彝族自治州弥勒市朋普镇朋普社区朋肖路49号 邮编:652301)
+- **DATA 对象**:新增 `duns` 字段(中英);更新 `contact` 地址为 D-U-N-S 官网注册地址(中英);英文公司名由原"…Weifengbao…"修正为 D-U-N-S 官网登记的"Mile Pengpu Town Micro Storm Network Technology Studio"
+- **switchLang 函数**:新增 `footer-duns` 元素的同步更新逻辑
+- **联系我们章节的"通信地址"同步更新**为 D-U-N-S 官网注册地址(中英),保证整篇文档地址一致性
+
+#### 3. 关键决策与发现
+- **地址不一致问题**:原 footer 与「联系我们」中填写的"云南省昆明市西山区滇池度假区(碧鸡街道车家壁513号)"与 D-U-N-S 官网登记的注册地址(红河州弥勒市朋普镇)不符。统一社会信用代码 92532526MA6PCX153W 中的 532526 行政区划码对应弥勒市,印证 D-U-N-S 官网地址为工商注册地址。本次以 D-U-N-S 官网地址为准统一全文,避免 Apple/Google 审核时主体信息核对出现偏差
+- **法定代表人字段移除**:初版主体信息含「法定代表人:李振阳(Zhenyang Li)」字段,应要求移除(App 内 + 网页中英三处均已删除)。隐私政策主体信息聚焦"组织身份"而非"自然人身份",更符合数据最小化原则
+- **英文公司名修正**:原 DATA.en.company 为"Mile City Pengpu Town Weifengbao Network Technology Studio",与 D-U-N-S 官网登记"Mile Pengpu Town Micro Storm Network Technology Studio"不一致。本次以 D-U-N-S 官网为准修正,确保 Apple Developer Program 主体核验通过
+
+#### 4. 合规建议(供后续维护参考)
+- **App Store Connect**:在 App 信息 → "开发者信息"处填写 D-U-N-S 586261192,确保与隐私政策披露一致
+- **Google Play Console**:在"应用内容 → 隐私政策"URL 提交本页面,主体信息保持一致
+- **欧盟 GDPR 代表**:当前"国际数据保护"章节声明已指定欧盟代表,但未具体披露代表身份。若目标用户含欧盟且无实体,建议后续在主体信息或国际数据保护章节明确 EU 代表身份与联系方式(GDPR Art.27)
+- **CCPA"请勿出售"入口**:当前声明"不出售个人信息"已合规;若后续商业模式变更需设立专门"请勿出售我的个人信息"链接
+- **关于页同步**:建议检查 App 内「关于」页面(t_about.dart)是否同步展示 D-U-N-S,保持多端一致(本次未涉及,留待后续)
+
+#### 涉及文件
+- `lib/features/settings/presentation/privacy/privacy_policy_page.dart`(新增「9. 主体信息」章节 + 章节顺延 + 文件头更新)
+- `docs/toolsapi/public/agreements/privacy-policy.html`(中文区/英文区新增「主体信息」章节 + 章节顺延 + footer 加 D-U-N-S + DATA 加 duns 字段 + 地址统一为 D-U-N-S 官网注册地址 + 英文公司名修正 + switchLang 加 footer-duns 同步)
+- `CHANGELOG.md`
+
+---
+
+***
+
+## [v6.131.0] - 2026-06-26
+
+### ✨ 新增(工作台仪表盘全面国际化 + 动态主题/样式 + 交互增强)
+
+#### 背景
+工作台模式右栏默认面板 `OverviewDashboard` 存在 23 处硬编码中文(问候语/区块标题/快捷操作标签/空状态/统计单位),不支持多语言;未消费 `AppThemeExtension` 的 `fontScale`/`fontWeight` 动态样式;动画 duration 硬编码不响应无障碍 `reduceAnimations` 降级;问候语在 build 内取 `DateTime.now().hour`,跨午时停留不刷新;`favoriteState.total` 等字段缺空指针防护。
+
+#### 1. 新增翻译模块 TDashboard
+- 新建 `lib/l10n/types/t_dashboard.dart` — 定义 `TDashboard` 类型,27 个字段覆盖:时段问候语(5)、问候区(1)、今日推荐(4)、快捷操作(9)、最近浏览(2)、数据统计(6),含 `toMap`/`fromMap`/`fallback` 回退
+- `lib/l10n/types/t_root.dart` — 注入 `dashboard` 字段(构造函数/字段/toMap/fromMap 四处)
+- `lib/l10n/types/t.dart` — 导出 `t_dashboard.dart`
+- 14 语言文件补全 `dashboard: TDashboard(...)` 段落:zh_CN/en 手写,zh_TW/ja/ko/de/it/es/fr/pt/ru/ar/bn/hi 12 种补全,`T.withFallback` 自动回退英语
+
+#### 2. 重构 overview_dashboard.dart
+- **ConsumerStatefulWidget + Timer**:每分钟校验小时变更,跨午时(12:00)/傍晚(18:00)/深夜(22:00)自动刷新问候语与图标,`dispose` 取消定时器
+- **全文案多语言**:`ref.watch(translationsProvider)` 接入,23 处硬编码替换为 `t.dashboard.xxx`
+- **动态主题/样式**:字号统一 `× ext.fontScale` 缩放,正文字重跟随 `ext.fontWeight`;`GlassContainer` 自动消费 `cardStyleId`/`cornerRadiusId`/`glassBlurMultiplier`
+- **触觉反馈**:快捷操作/最近浏览点击加 `HapticFeedback.selectionClick()`
+- **骨架屏**:今日推荐加载中(`homeState.isLoading && recommends.isEmpty`)显示 `Shimmer` 占位,深浅色自适应
+- **无障碍降级**:响应 `generalSettingsProvider.reduceAnimations`,动画 `duration` 降为 `Duration.zero`,`AppIcon.animate` 关闭,`SlideAnimation.verticalOffset` 归零
+- **空指针防护**:`authState.user?.signinDays ?? 0`,`streakDays` 单位用 `db.streakDayUnit` 拼接(i18n)
+- **作者引用 i18n**:`'$db.authorPrefix${sentence.author ?? db.anonymousAuthor}'`,`author` 空字符串回退佚名
+
+#### 3. 举一反三(附带发现 → 已同步修复)
+- **`t_root.dart` 的 `toMap()` 缺少 `'quickCard': quickCard.toMap()` 键**(现有 bug),导致 `T.withFallback` 对 `quickCard` 模块失效——非中文/英语语言的 quickCard 空字段无法回退英语。**本次已补上该键**,并审查 `t_settings_advanced` 的 5 个 impeller 翻译键(v6.129.0 新增)的 toMap/fromMap 完整性,确认无遗漏
+- **`translation_io_service.dart` 的 `importFromJson` 构造 `T(...)` 缺少 `dashboard` 参数**(v6.130.0 给 T 加 `required this.dashboard` 后未同步更新,会导致编译错误)。**本次已补上 `dashboard: fallback.dashboard`**,参照 `quickCard` 的处理模式(dashboard 属于非用户可编辑导出模块,直接用 fallback)
+- **设计说明**:`tToMap`/`exportAllTranslations` 是"导出用户可编辑翻译"方法,设计上只含 nav/common/profile/settings/note/beta/submit/studyPlan/correction/leisure 9 个核心模块,不含 quickCard/dashboard——这是预期行为,无需改动
+- **覆盖率检测自动包含**:`translation_coverage.dart` 通过 `zhCN.toMap()` 自动遍历所有 section,toMap 补全 quickCard 键后,覆盖率报告会自动包含 quickCard 模块的字段统计
+
+#### 涉及文件
+- `lib/l10n/types/t_dashboard.dart`(新增)
+- `lib/l10n/types/t_root.dart`、`lib/l10n/types/t.dart`(注入导出 + 修复 toMap 漏写 quickCard 键)
+- `lib/l10n/translation_io_service.dart`(修复 importFromJson 的 T 构造缺 dashboard 参数)
+- `lib/l10n/languages/*.dart`(14 个语言文件补全 dashboard 段落)
+- `lib/app/layout/overview_dashboard.dart`(重构)
+- `CHANGELOG.md`
+
+---***
+
+## [v6.130.0] - 2026-06-26
+
+### ✨ 新增(macOS 权限动态申请 — 替代 permission_handler_apple 无 macOS 实现的问题)
+
+#### 背景:macOS 端权限无法动态申请
+
+- **Issue**: `permission_handler_apple 9.4.9` 仅支持 iOS,无 macOS 实现。此前 macOS 端权限管理页面直接返回 `granted`,用户点击"请求权限"按钮不会弹出系统授权对话框,实际授权发生在首次访问资源时由系统自动触发(被动模式)。用户无法在权限管理页面主动触发授权。
+- **方案**: 新建 `PermissionManager.swift` 原生权限管理器,通过 `apps.xy.xianyan/macos.app` MethodChannel 暴露给 Flutter,实现 macOS 原生权限动态申请。
+- **支持的权限**:
+ | 权限 | 原生 API | 状态查询 | 请求方式 |
+ |---|---|---|---|
+ | 相机 | AVFoundation | `AVCaptureDevice.authorizationStatus(for: .video)` | `AVCaptureDevice.requestAccess(for: .video)` |
+ | 麦克风 | AVFoundation | `AVCaptureDevice.authorizationStatus(for: .audio)` | `AVCaptureDevice.requestAccess(for: .audio)` |
+ | 相册 | Photos framework | `PHPhotoLibrary.authorizationStatus(for:)` | `PHPhotoLibrary.requestAuthorization(for:)` |
+ | 通知 | UserNotifications | `UNUserNotificationCenter.getNotificationSettings` | `UNUserNotificationCenter.requestAuthorization` |
+- **权限状态映射**:
+ - `notDetermined` → 未决定(可请求)
+ - `granted` → 已授权(含 `.authorized` / `.limited` / `.provisional` / `.ephemeral`)
+ - `permanentlyDenied` → 已拒绝(macOS 拒绝后不再次弹窗,需去系统设置)
+ - `restricted` → 受限(如家长控制)
+- **实现**:
+ 1. 新建 `macos/Runner/PermissionManager.swift` — 原生权限管理器(AVFoundation/Photos/UserNotifications)
+ 2. `AppDelegate.swift` 的 `registerAppChannel` 中新增 3 个 MethodChannel 方法:
+ - `checkPermission` — 查询权限状态(异步)
+ - `requestPermission` — 请求权限(触发系统 TCC 弹窗)
+ - `openPermissionSettings` — 打开系统设置 - 隐私与安全性
+ 3. `macos/Runner.xcodeproj/project.pbxproj` — 将 PermissionManager.swift 添加到 Xcode 项目
+ 4. `MacosPlatformService` 新增 `checkPermission` / `requestPermission` / `openPermissionSettings` 三个方法
+ 5. `PermissionService` 修改 macOS 分支:
+ - `checkStatus` — 调用原生 `checkPermission` 查询 TCC 状态
+ - `requestPermission` — 调用原生 `requestPermission` 触发系统弹窗(先显示说明对话框,用户确认后触发)
+ - `openSettings` — 调用原生 `openPermissionSettings` 跳转系统设置
+ 6. `AppPermission` 枚举新增 `macosPermissionName` getter — 返回 macOS 原生权限名称映射
+- **举一反三**:
+ - macOS TCC 权限模型与 iOS 不同:一旦用户拒绝,再次调用 `requestAccess` 不会弹窗(需用户去系统设置重置)。因此 macOS 上 `denied` 映射为 `permanentlyDenied`
+ - 通知权限是例外:可以多次请求(`requestAuthorization` 每次都会返回当前状态,但不弹窗)
+ - `UNUserNotificationCenter.getNotificationSettings` 是异步的,统一所有权限查询为异步回调模式
+- **涉及文件**:
+ - `macos/Runner/PermissionManager.swift`(新增)
+ - `macos/Runner/AppDelegate.swift` — channel handler 新增 3 个 case
+ - `macos/Runner.xcodeproj/project.pbxproj` — 添加文件引用
+ - `lib/core/services/device/macos_platform_service.dart` — 新增权限管理方法
+ - `lib/core/services/auth/permission_service.dart` — macOS 分支改为调用原生 API
+
+---
+
+## [v6.129.0] - 2026-06-26
+
+### 🐛 修复(macOS 渲染引擎 — Impeller 开关不生效 + x86 警告 + Apple Silicon 提示)
+
+#### 问题:通用设置中 Impeller 开关无论打开或关闭,实际渲染引擎始终是 Skia
+
+- **Issue**: 用户在「通用设置 → 高级 → Impeller 渲染引擎」切换开关并重启应用后,实际渲染引擎始终为 Skia,开关完全失效
+- **根因**: `MainFlutterWindow.swift` 的 `awakeFromNib()` 使用 `setenv("FLUTTER_ENGINE_SWITCH_0", ...)` 试图通过环境变量传递 `--enable-impeller`/`--no-enable-impeller` 给 Flutter 引擎,但 **macOS 桌面 embedder (FlutterMacOS) 不读取 `FLUTTER_ENGINE_SWITCH_` 环境变量**(该机制仅适用于 mobile embedder iOS/Android)。同时 `Info.plist` 中的 `FLTEnableImpeller` key 已被早期版本移除,引擎无任何信号可读取,只能走 SDK 默认值(Skia)。`currentImpellerEnabled` 缓存的是预期值而非实际值,进一步掩盖了问题,导致 UI 显示与引擎实际状态不符
+- **方案**: 改用 `FlutterDartProject` + `commandLineArguments` 方式传递命令行参数,这是 macOS 桌面 embedder 唯一可靠的引擎开关控制方式,命令行参数优先级高于 Info.plist 的 `FLTEnableImpeller` 与 SDK 默认值
+- **实现**:
+ 1. `MainFlutterWindow.awakeFromNib()` — 移除 `setenv("FLUTTER_ENGINE_SWITCH_0", ...)`,改为:
+ ```swift
+ let dartProject = FlutterDartProject()
+ dartProject.commandLineArguments = [impellerArg]
+ let flutterViewController = FlutterViewController(project: dartProject)
+ ```
+ 2. 保留 `currentImpellerEnabled` 静态变量缓存当前引擎实际运行状态(修复后此值与引擎实际状态一致)
+ 3. 默认值策略不变:Apple Silicon 默认开启 Impeller,Intel Mac 默认关闭(Metal 驱动有渲染资源累积 bug)
+- **举一反三**:
+ - `FLUTTER_ENGINE_SWITCH_` 环境变量机制仅适用于 mobile embedder(iOS/Android),桌面 embedder(macOS/Windows/Linux)应使用 `FlutterDartProject.commandLineArguments` 或 Info.plist 的 `FLTEnableImpeller` key
+ - 引擎开关的"实际运行状态"应通过引擎 API 查询,不能直接复用"预期值"作为缓存,否则会掩盖开关不生效的问题
+ - Info.plist 中移除 `FLTEnableImpeller` 后必须确保有运行时替代方案(命令行参数 / FlutterDartProject),否则引擎走 SDK 默认值
+- **涉及文件**:
+ - `macos/Runner/MainFlutterWindow.swift` — `awakeFromNib()` 改用 `FlutterDartProject.commandLineArguments`
+ - `macos/Runner/AppDelegate.swift` — 注释同步更新(实际逻辑未变)
+
+---
+
+#### 增强:x86 端开启 Impeller 前增加警告对话框
+
+- **Issue**: Intel Mac (x86_64) 上 Metal 驱动有渲染资源累积 bug,开启 Impeller 可能导致色差/字体割裂/闪烁。用户在不知情情况下开启 Impeller 可能造成体验问题
+- **方案**: 在 `_onImpellerToggle(value)` 中检测架构,若为 x86_64 且 `value == true`,先弹出二次确认警告对话框,用户确认后才写入设置;用户取消则恢复开关状态
+- **实现**:
+ 1. 新增 `_showX86ImpellerWarningDialog(T t)` 方法 — 返回 `Future`,用户确认返回 true,取消返回 false
+ 2. 对话框包含警告图标(橙色三角形)+ 标题 + 详细描述(说明 Intel Mac 上的风险)+ 取消/仍要开启 两个按钮
+ 3. 用户取消时通过 `setState` 恢复 `_impellerEnabled = false`,不写入 UserDefaults
+- **Apple Silicon 提示**: 在重启对话框中新增 arm64 架构专属提示卡片(蓝色背景 + sparkles 图标),说明"Apple Silicon 上 Impeller 基于 Metal 性能更好,推荐开启"
+- **新增翻译键**(5 个,覆盖全部 14 种语言):
+ - `impellerX86WarningTitle` — Intel Mac 兼容性警告标题
+ - `impellerX86WarningDesc` — 警告描述(说明风险 + Apple Silicon 推荐使用 Skia)
+ - `impellerX86WarningConfirm` — 仍要开启
+ - `impellerX86WarningCancel` — 取消
+ - `impellerAppleSiliconTip` — Apple Silicon 推荐开启提示
+- **涉及文件**:
+ - `lib/features/settings/presentation/general/general_settings_page.dart` — `_onImpellerToggle` / `_showX86ImpellerWarningDialog` / `_buildImpellerDialogContent`
+ - `lib/l10n/types/t_settings_advanced.dart` — 新增 5 个字段 + toMap + fromMap
+ - `lib/l10n/languages/*.dart` — 14 种语言全部添加新键
+
+---
+
+## [v6.128.0] - 2026-06-26
+
+### 🐛 修复(macOS 运行时 — permission_handler MissingPluginException + WebRTC 构建冲突)
+
+#### 问题一:permission_handler_apple 无 macOS 实现 — 权限管理页点击报错(App Store 2.1(a) 直接原因)
+
+- **Issue**: macOS 端权限管理页点击「相册」「麦克风」「相机」请求时抛出 `MissingPluginException(No implementation found for method checkPermissionStatus on channel flutter.baseflow.com/permissions/methods)`,点击「去设置」按钮同样报错。这是 v6.127.0 未覆盖的运行时问题,是 App Store 审核被拒 Guideline 2.1(a) 的直接原因
+- **根因**: `permission_handler_apple 9.4.9` 的 `pubspec.yaml` 仅声明 `ios` 平台,**无 macOS 实现**:
+ ```yaml
+ flutter:
+ plugin:
+ implements: permission_handler
+ platforms:
+ ios: # ⚠️ 仅 ios,无 macos
+ pluginClass: PermissionHandlerPlugin
+ ```
+ - `permission_handler_apple-9.4.9/` 目录下只有 `ios/`,无 `macos/`
+ - macOS 端 `GeneratedPluginRegistrant.swift` 不注册 `PermissionHandlerApplePlugin`
+ - 调用 `Permission.camera.status` / `Permission.photos.request()` / `openAppSettings()` 均抛出 `MissingPluginException`
+- **方案**: macOS sandbox 下权限由 **entitlement + Info.plist 用法说明** 自动管理,系统在首次访问受保护资源时弹出授权对话框(由 OS 触发,不由 App 调用)。`permission_service.dart` 添加 macOS 早返回逻辑,跳过 `permission_handler` 调用
+- **实现**:
+ 1. `checkStatus()` — macOS 端直接返回 `granted`,避免调用未注册的方法通道
+ 2. `requestPermission()` — macOS 端直接返回 `true`,权限由系统 sandbox + entitlement 自动管理
+ 3. `openSettings()` — 新增 macOS 原生跳转:`Process.run('open', ['x-apple.systempreferences:com.apple.preference.security?Privacy'])`,打开「系统设置 > 隐私与安全性」
+ 4. `_showSettingsDialog` / `_showDeniedDialog` — 将 `openAppSettings()` 调用改为 `openSettings()`,确保 macOS 端「去设置」按钮可用
+ 5. `checkStatus()` / `requestPermission()` 新增 `on MissingPluginException` 兜底,Linux 等无 `permission_handler` 实现的桌面端视为已授权
+- **举一反三**:
+ - Flutter 联邦插件(federated plugin)的平台实现可能只覆盖部分平台,使用前需检查 `pub-cache` 中实际包的 `pubspec.yaml` 平台声明
+ - macOS sandbox 下权限模型与 iOS 不同:iOS 需 App 主动调用 `request()`,macOS 由 OS 在首次访问时自动弹出,entitlement 是「App 级授权」,OS 弹框是「用户级授权」
+ - `MissingPluginException` 不应仅靠 `try-catch` 静默吞掉,需在入口处按平台显式短路,否则权限管理页状态显示为「未决定」具有误导性
+- **涉及文件**:
+ - `lib/core/services/auth/permission_service.dart` — `checkStatus` / `requestPermission` / `openSettings` / `_showSettingsDialog` / `_showDeniedDialog`
+
+---
+
+#### 问题二:flutter_webrtc 1.4.0 与 macOS WebRTC-SDK 144.7559.09 版本冲突
+
+- **Issue**: macOS `pod install` 报错 `CocoaPods could not find compatible versions for pod "WebRTC-SDK"`,`Podfile.lock` 锁定 `144.7559.09`,但 `flutter_webrtc 1.4.0` 的 macOS podspec 依赖 `144.7559.01`(`.01` 版本)
+- **根因**: commit 667f3e49 添加本地 `macos/WebRTC-SDK.podspec.json` 声明 `.09` 版本(Intel Mac 渲染修复补丁),但 `flutter_webrtc` 保持在 1.4.0(pins `.01`),两者冲突
+- **方案**: 升级 `flutter_webrtc` 1.4.0 → 1.5.2(其 macOS podspec 已对齐声明 `s.dependency 'WebRTC-SDK', '144.7559.09'`)
+- **验证**: `flutter analyze` 通过,`screen_share_page.dart` / `webrtc_service.dart` / `screen_share_provider.dart` 无 API 变更
+- **鸿蒙端**: 本地包 `packages/flutter_webrtc`(v1.4.0-ohos.1)无法直接升级,待后续同步到 `1.5.2-ohos`;两端 pubspec 独立互不影响
+- **涉及文件**:
+ - `pubspec.yaml` — `flutter_webrtc: ^1.4.0` → `^1.5.2`
+ - `pubspec.macos.yaml` — 同上
+ - `pubspec.ohos.yaml` — 更新 flutter_webrtc 注释(远程端已升至 1.5.2)
+ - `pubspec.lock` — flutter_webrtc sha256 更新
+ - `iOS_macOS_Developer_Guide.md` — 差异对照表更新 + 新增 §2.8.9 flutter_webrtc 特殊包说明 + 新增 §2.8.10 permission_handler_apple macOS 缺失实现说明
+
+---
+
+## [v6.127.0] - 2026-06-26
+
+### 🐛 修复(macOS App Store 审核被拒 — Guideline 2.4.5(i) / 2.1(a))
+
+#### macOS 审核三大问题修复 — entitlement 配置 + 权限管理全平台适配
+
+> **注**: 本条目覆盖 entitlement 配置与权限卡片平台过滤;运行时 `MissingPluginException` 修复见 v6.128.0。
+
+- **Issue**: macOS 版本 6.6.25 (2606260) 提交 App Store 被拒,三条审核意见:
+ 1. **Guideline 2.4.5(i)** — 声明了未使用的 entitlement:`files.downloads.read-only`、`files.downloads.read-write`、`network.server`
+ 2. **Guideline 2.4.5(i)** — 有功能但缺对应 entitlement:相机缺 `com.apple.security.device.camera`,位置缺 `com.apple.security.personal-information.location`
+ 3. **Guideline 2.1(a)** — 在权限管理页点击「相册&存储」「麦克风」请求时显示错误信息
+- **根因**:
+ 1. `Release.entitlements` / `DebugProfile.entitlements` 声明了 `files.downloads.*` 但全代码库无 `getDownloadsDirectory` 调用(字体下载用的是 `getApplicationDocumentsDirectory`),属于声明未使用
+ 2. 扫码 / 拍照 / OCR / 语音录制 / 语音转文字功能在共享代码中无平台守卫,macOS 端实际可用,但未声明 `device.camera` / `device.audio-input` / `personal-information.photos-library` entitlement,且 `Info.plist` 缺 `NSCameraUsageDescription` / `NSMicrophoneUsageDescription` 用法说明
+ 3. `permission_service.dart` 的 `isPlatformRelevant` 对 macOS 无任何过滤(鸿蒙端有过滤,macOS 落到 `return true`),导致权限管理页在 macOS 展示相机/相册/位置/附近设备/麦克风卡片;点击请求时因 entitlement + Info.plist 双缺,`permission_handler` 在 sandbox 下请求失败弹出错误对话框
+ 4. 位置(GPS)实际全平台未使用(仅 IP 定位 + 手动文本输入),却展示位置权限卡片具有误导性
+- **方案**:
+ 1. macOS entitlements:移除未用的 `files.downloads.*`;新增 `device.camera` / `device.audio-input` / `personal-information.photos-library`;保留 `network.server`(LocalSend 文件传输本地服务器必需)
+ 2. macOS `Info.plist`:新增 `NSCameraUsageDescription` / `NSMicrophoneUsageDescription` 用法说明
+ 3. 重写 `isPlatformRelevant` 为全平台适配:macOS/Windows/Linux/Web 各自过滤;位置卡片仅移动端展示(桌面端/Web 只用 IP 定位);附近设备仅移动端;相册仅 macOS + 移动端(Windows/Linux/Web 用文件选择器)
+ 4. 新增 `localServer` 虚拟权限卡片(仅桌面端展示),对应 `network.server` entitlement,向用户透明声明 LocalSend 文件传输本地服务器能力
+- **实现**:
+ 1. **entitlements** — `DebugProfile.entitlements` + `Release.entitlements` 同步修改:移除 2 个 downloads 键,新增 3 个 device/photos 键
+ 2. **Info.plist** — 新增相机 / 麦克风用法说明(中文)
+ 3. **permission_service.dart** — `AppPermission` 枚举新增 `localServer` 虚拟权限;`label/description/usageScenes/denialImpact` 4 个 switch 补全 case;`isPlatformRelevant` 重写为「特殊权限单独处理 + 鸿蒙/桌面/Web/移动端分支」结构
+ 4. **l10n** — `t_settings_permission.dart` 新增 `permLocalServer*` 4 字段(构造/字段/toMap/fromMap);14 个语言文件新增 4 条翻译
+- **举一反三**:
+ - entitlement 应遵循「最小权限集」原则,声明即需有对应功能,否则触发审核质疑;后续新增 entitlement 必须同步在权限管理页向用户声明用途
+ - 跨平台权限过滤不能只覆盖单一平台(鸿蒙),所有桌面端 / Web 都需显式过滤,否则同一 bug 会在其他平台复现
+ - macOS sandbox 下 `permission_handler` 依赖 entitlement + Info.plist usage description 双重配置才能弹系统授权框,缺一即失败
+- **审核回复要点**(提交 App Store Connect 时使用):
+ - `network.server` 用于 LocalSend 局域网文件传输(`HttpServer.bind` / `ServerSocket.bind`),见 `localsend_service.dart` / `tcp_socket_service.dart`
+ - 已移除未使用的 `files.downloads.*` entitlement
+ - 已补全相机 / 麦克风 entitlement 与用法说明,权限管理页请求功能正常
+- **涉及文件**:
+ - `macos/Runner/DebugProfile.entitlements` — 移除 downloads,新增 camera/audio-input/photos-library
+ - `macos/Runner/Release.entitlements` — 同上
+ - `macos/Runner/Info.plist` — 新增 NSCameraUsageDescription / NSMicrophoneUsageDescription
+ - `lib/core/services/auth/permission_service.dart` — 新增 localServer 枚举 + 全平台 isPlatformRelevant
+ - `lib/l10n/types/t_settings_permission.dart` — 新增 permLocalServer 4 字段
+ - `lib/l10n/languages/*.dart`(14 个语言文件)— 新增 permLocalServer 4 条翻译
+
+---
+
## [v6.126.0] - 2026-06-25
### 🐛 修复(键盘事件)
diff --git a/Scripts/check_android_config.py b/Scripts/check_android_config.py
new file mode 100644
index 00000000..3acaf943
--- /dev/null
+++ b/Scripts/check_android_config.py
@@ -0,0 +1,865 @@
+#!/usr/bin/env python3
+# ============================================================
+# 闲言APP — Android配置一致性检查脚本
+# 创建时间: 2026-06-01
+# 更新时间: 2026-06-01
+# 名称: check_android_config.py
+# 作用: 验证Android原生配置与Flutter插件的一致性
+# 上次更新: 初始创建,检查shortcuts/manifest/gradle配置
+# ============================================================
+
+import argparse
+import json
+import os
+import re
+import sys
+import xml.etree.ElementTree as ET
+
+ANDROID_NS = "http://schemas.android.com/apk/res/android"
+TOOLS_NS = "http://schemas.android.com/tools"
+
+PASS = "pass"
+WARN = "warn"
+FAIL = "fail"
+
+SCORE_WEIGHTS = {PASS: 10, WARN: 5, FAIL: 0}
+
+
+def ns(attr):
+ return f"{{{ANDROID_NS}}}{attr}"
+
+
+def find_project_root():
+ candidates = [os.getcwd()]
+ script_dir = os.path.dirname(os.path.abspath(__file__))
+ candidates.append(script_dir)
+ for d in candidates:
+ if os.path.isfile(os.path.join(d, "pubspec.yaml")):
+ return d
+ parent = os.path.dirname(d)
+ if os.path.isfile(os.path.join(parent, "pubspec.yaml")):
+ return parent
+ return os.getcwd()
+
+
+def read_file(path):
+ if not os.path.isfile(path):
+ return None
+ with open(path, "r", encoding="utf-8", errors="replace") as f:
+ return f.read()
+
+
+def parse_xml(path):
+ if not os.path.isfile(path):
+ return None
+ try:
+ tree = ET.parse(path)
+ return tree.getroot()
+ except ET.ParseError:
+ return None
+
+
+class CheckResult:
+ def __init__(self, category, name, status, message, detail=None):
+ self.category = category
+ self.name = name
+ self.status = status
+ self.message = message
+ self.detail = detail or []
+
+ def to_dict(self):
+ return {
+ "category": self.category,
+ "name": self.name,
+ "status": self.status,
+ "message": self.message,
+ "detail": self.detail,
+ }
+
+
+class AndroidConfigChecker:
+ def __init__(self, project_root, verbose=False):
+ self.project_root = project_root
+ self.verbose = verbose
+ self.results = []
+
+ self.manifest_path = os.path.join(
+ project_root, "android", "app", "src", "main", "AndroidManifest.xml"
+ )
+ self.shortcuts_path = os.path.join(
+ project_root, "android", "app", "src", "main", "res", "xml", "shortcuts.xml"
+ )
+ self.app_gradle_path = os.path.join(
+ project_root, "android", "app", "build.gradle.kts"
+ )
+ self.root_gradle_path = os.path.join(
+ project_root, "android", "build.gradle.kts"
+ )
+ self.gradle_props_path = os.path.join(
+ project_root, "android", "gradle.properties"
+ )
+ self.pubspec_lock_path = os.path.join(project_root, "pubspec.lock")
+
+ self.manifest_root = parse_xml(self.manifest_path)
+ self.shortcuts_root = parse_xml(self.shortcuts_path)
+ self.app_gradle_content = read_file(self.app_gradle_path)
+ self.root_gradle_content = read_file(self.root_gradle_path)
+ self.gradle_props_content = read_file(self.gradle_props_path)
+ self.pubspec_lock_content = read_file(self.pubspec_lock_path)
+
+ def add(self, category, name, status, message, detail=None):
+ self.results.append(CheckResult(category, name, status, message, detail or []))
+
+ def check_manifest_exists(self):
+ if self.manifest_root is not None:
+ self.add("Manifest", "文件存在", PASS, "AndroidManifest.xml 存在且可解析")
+ else:
+ self.add("Manifest", "文件存在", FAIL, "AndroidManifest.xml 不存在或无法解析")
+
+ def check_permissions(self):
+ if self.manifest_root is None:
+ self.add("Manifest", "权限检查", FAIL, "无法解析 Manifest,跳过权限检查")
+ return
+
+ permissions = []
+ for elem in self.manifest_root.iter():
+ if elem.tag == "uses-permission":
+ name = elem.get(ns("name"), "")
+ permissions.append(name)
+
+ required = {
+ "android.permission.INTERNET": "网络访问(dio/cached_network_image)",
+ "android.permission.ACCESS_NETWORK_STATE": "网络状态检测",
+ }
+
+ for perm, desc in required.items():
+ if perm in permissions:
+ self.add("Manifest", f"权限: {perm}", PASS, f"已声明 — {desc}")
+ else:
+ self.add("Manifest", f"权限: {perm}", FAIL, f"缺少必要权限 — {desc}")
+
+ optional = {
+ "android.permission.VIBRATE": "震动反馈",
+ }
+ for perm, desc in optional.items():
+ if perm in permissions:
+ self.add("Manifest", f"权限: {perm}", PASS, f"已声明 — {desc}")
+ else:
+ self.add("Manifest", f"权限: {perm}", WARN, f"未声明 — {desc}(如不需要可忽略)")
+
+ if self.verbose:
+ self.add(
+ "Manifest",
+ "全部权限列表",
+ PASS,
+ f"共声明 {len(permissions)} 项权限",
+ permissions,
+ )
+
+ def check_activity_config(self):
+ if self.manifest_root is None:
+ self.add("Manifest", "Activity配置", FAIL, "无法解析 Manifest")
+ return
+
+ app = self.manifest_root.find("application")
+ if app is None:
+ self.add("Manifest", "Activity配置", FAIL, "未找到 标签")
+ return
+
+ activity = None
+ for act in app.findall("activity"):
+ name = act.get(ns("name"), "")
+ if "MainActivity" in name:
+ activity = act
+ break
+
+ if activity is None:
+ self.add("Manifest", "Activity配置", FAIL, "未找到 MainActivity")
+ return
+
+ exported = activity.get(ns("exported"), "")
+ if exported == "true":
+ self.add("Manifest", "Activity exported", PASS, "MainActivity 已设置 exported=true")
+ else:
+ self.add("Manifest", "Activity exported", WARN, "MainActivity 未设置 exported=true,可能影响启动")
+
+ launch_mode = activity.get(ns("launchMode"), "")
+ if launch_mode == "singleTop":
+ self.add("Manifest", "Activity launchMode", PASS, "launchMode=singleTop,防止重复实例")
+ else:
+ self.add("Manifest", "Activity launchMode", WARN, f"launchMode={launch_mode or '未设置'},建议设为 singleTop")
+
+ soft_input = activity.get(ns("windowSoftInputMode"), "")
+ if soft_input == "adjustResize":
+ self.add("Manifest", "Activity softInputMode", PASS, "windowSoftInputMode=adjustResize")
+ else:
+ self.add("Manifest", "Activity softInputMode", WARN, f"windowSoftInputMode={soft_input or '未设置'},建议设为 adjustResize")
+
+ hardware_accel = activity.get(ns("hardwareAccelerated"), "")
+ if hardware_accel == "true":
+ self.add("Manifest", "Activity hardwareAccelerated", PASS, "hardwareAccelerated=true")
+ else:
+ self.add("Manifest", "Activity hardwareAccelerated", WARN, "hardwareAccelerated 未启用,可能影响渲染性能")
+
+ def check_intent_filters(self):
+ if self.manifest_root is None:
+ self.add("Manifest", "IntentFilter", FAIL, "无法解析 Manifest")
+ return
+
+ app = self.manifest_root.find("application")
+ if app is None:
+ return
+
+ activity = None
+ for act in app.findall("activity"):
+ if "MainActivity" in act.get(ns("name"), ""):
+ activity = act
+ break
+
+ if activity is None:
+ return
+
+ filters = activity.findall("intent-filter")
+ has_main = False
+ has_launcher = False
+ share_filters = []
+
+ for f in filters:
+ actions = [a.get(ns("name"), "") for a in f.findall("action")]
+ categories = [c.get(ns("name"), "") for c in f.findall("category")]
+ data_elems = f.findall("data")
+ mime_types = [d.get(ns("mimeType"), "") for d in data_elems]
+
+ if "android.intent.action.MAIN" in actions:
+ has_main = True
+ if "android.intent.category.LAUNCHER" in categories:
+ has_launcher = True
+ if "android.intent.action.SEND" in actions or "android.intent.action.SEND_MULTIPLE" in actions:
+ share_filters.append(
+ {"actions": actions, "categories": categories, "mimeTypes": mime_types}
+ )
+
+ if has_main and has_launcher:
+ self.add("Manifest", "启动IntentFilter", PASS, "MAIN+LAUNCHER 配置正确")
+ else:
+ self.add(
+ "Manifest",
+ "启动IntentFilter",
+ FAIL,
+ f"MAIN={has_main}, LAUNCHER={has_launcher},应用可能无法启动",
+ )
+
+ if share_filters:
+ self.add(
+ "Manifest",
+ "分享IntentFilter",
+ PASS,
+ f"已配置 {len(share_filters)} 个分享 IntentFilter",
+ [f"actions={s['actions']}, mimeTypes={s['mimeTypes']}" for s in share_filters]
+ if self.verbose
+ else [],
+ )
+ else:
+ self.add("Manifest", "分享IntentFilter", WARN, "未配置分享 IntentFilter")
+
+ def check_enable_on_back_invoked(self):
+ if self.manifest_root is None:
+ return
+
+ app = self.manifest_root.find("application")
+ if app is None:
+ return
+
+ app_flag = app.get(ns("enableOnBackInvokedCallback"), "")
+ if app_flag == "true":
+ self.add("Manifest", "enableOnBackInvokedCallback(app)", PASS, "Application 级已启用预测性返回手势")
+ else:
+ self.add("Manifest", "enableOnBackInvokedCallback(app)", WARN, "Application 级未启用预测性返回手势(Android 13+推荐)")
+
+ activity = None
+ for act in app.findall("activity"):
+ if "MainActivity" in act.get(ns("name"), ""):
+ activity = act
+ break
+
+ if activity is not None:
+ act_flag = activity.get(ns("enableOnBackInvokedCallback"), "")
+ if act_flag == "true":
+ self.add("Manifest", "enableOnBackInvokedCallback(activity)", PASS, "Activity 级已启用预测性返回手势")
+ else:
+ self.add("Manifest", "enableOnBackInvokedCallback(activity)", WARN, "Activity 级未启用预测性返回手势")
+
+ def check_shortcuts_xml(self):
+ if self.shortcuts_root is None:
+ self.add("Shortcuts", "shortcuts.xml", WARN, "res/xml/shortcuts.xml 不存在或无法解析,跳过快捷方式检查")
+ return
+
+ shortcuts = self.shortcuts_root.findall("shortcut")
+ if not shortcuts:
+ self.add("Shortcuts", "快捷方式数量", WARN, "shortcuts.xml 中无快捷方式定义")
+ return
+
+ self.add("Shortcuts", "快捷方式数量", PASS, f"共定义 {len(shortcuts)} 个快捷方式")
+
+ shortcut_ids = []
+ for s in shortcuts:
+ sid = s.get(ns("shortcutId"), "")
+ enabled = s.get(ns("enabled"), "true")
+ shortcut_ids.append(sid)
+
+ if enabled == "true":
+ self.add("Shortcuts", f"快捷方式: {sid}", PASS, f"已启用 (id={sid})")
+ else:
+ self.add("Shortcuts", f"快捷方式: {sid}", WARN, f"已禁用 (id={sid})")
+
+ intent = s.find("intent")
+ if intent is None:
+ self.add("Shortcuts", f"{sid} intent", FAIL, f"快捷方式 {sid} 缺少 ")
+ continue
+
+ action = intent.get(ns("action"), "")
+ target_class = intent.get(ns("targetClass"), "")
+ extras = intent.findall("extra")
+
+ if action == "android.intent.action.RUN":
+ self.add("Shortcuts", f"{sid} action", PASS, f"action=RUN,与 quick_actions_android 插件一致")
+ else:
+ self.add(
+ "Shortcuts",
+ f"{sid} action",
+ FAIL,
+ f"action={action},应为 android.intent.action.RUN(quick_actions_android 插件要求)",
+ )
+
+ if "MainActivity" in target_class:
+ self.add("Shortcuts", f"{sid} targetClass", PASS, f"targetClass 指向 MainActivity")
+ else:
+ self.add("Shortcuts", f"{sid} targetClass", WARN, f"targetClass={target_class},请确认是否正确")
+
+ extra_keys = []
+ extra_values = []
+ for extra in extras:
+ key = extra.get(ns("name"), "")
+ val = extra.get(ns("value"), "")
+ extra_keys.append(key)
+ extra_values.append(val)
+
+ if "some unique action key" in extra_keys:
+ self.add("Shortcuts", f"{sid} extra key", PASS, f'extra key="some unique action key",与 quick_actions_android 插件一致')
+ else:
+ self.add(
+ "Shortcuts",
+ f"{sid} extra key",
+ FAIL,
+ f'extra key={extra_keys},应为 "some unique action key"(quick_actions_android 插件内部常量)',
+ )
+
+ if self.verbose and extra_values:
+ self.add(
+ "Shortcuts",
+ f"{sid} extra values",
+ PASS,
+ f"extra values: {extra_values}",
+ extra_values,
+ )
+
+ def check_shortcuts_flutter_consistency(self):
+ if self.shortcuts_root is None:
+ return
+
+ if self.pubspec_lock_content is None:
+ self.add("Shortcuts", "Flutter插件一致性", WARN, "无法读取 pubspec.lock,跳过插件一致性检查")
+ return
+
+ plugin_version = None
+ for line in self.pubspec_lock_content.splitlines():
+ stripped = line.strip()
+ if stripped.startswith("version:"):
+ parent_indent = len(line) - len(line.lstrip())
+ pass
+ if "quick_actions_android" in stripped and "name:" in stripped:
+ pass
+
+ version_match = re.search(
+ r"quick_actions_android:.*?version:\s*[\"']?([^\"'\s]+)",
+ self.pubspec_lock_content,
+ re.DOTALL,
+ )
+ if version_match:
+ plugin_version = version_match.group(1)
+ self.add("Shortcuts", "quick_actions_android版本", PASS, f"插件版本: {plugin_version}")
+ else:
+ self.add("Shortcuts", "quick_actions_android版本", WARN, "无法从 pubspec.lock 解析 quick_actions_android 版本")
+
+ pub_cache = self._find_pub_cache()
+ plugin_source = None
+ if pub_cache:
+ plugin_dir = os.path.join(pub_cache, "quick_actions_android")
+ if os.path.isdir(plugin_dir):
+ plugin_source = plugin_dir
+
+ if plugin_source is None:
+ self.add(
+ "Shortcuts",
+ "插件源码检查",
+ WARN,
+ "未找到 quick_actions_android 插件源码,无法深度验证常量一致性",
+ [f"搜索路径: {pub_cache}"] if pub_cache else [],
+ )
+ self._check_shortcuts_dart_consistency()
+ return
+
+ quick_actions_file = os.path.join(plugin_source, "android", "src", "main", "kotlin", "io", "flutter", "plugins", "quickactions", "QuickActionsPlugin.kt")
+ if not os.path.isfile(quick_actions_file):
+ alt_paths = [
+ os.path.join(plugin_source, "lib", "src", "main", "kotlin", "io", "flutter", "plugins", "quickactions", "QuickActionsPlugin.kt"),
+ os.path.join(plugin_source, "android", "src", "main", "kotlin", "io", "flutter", "plugins", "quickactions", "MethodCallHandlerImpl.kt"),
+ ]
+ for alt in alt_paths:
+ if os.path.isfile(alt):
+ quick_actions_file = alt
+ break
+
+ if not os.path.isfile(quick_actions_file):
+ self.add("Shortcuts", "插件源码检查", WARN, "未找到 QuickActionsPlugin.kt,无法验证常量")
+ self._check_shortcuts_dart_consistency()
+ return
+
+ plugin_content = read_file(quick_actions_file)
+
+ expected_action = "android.intent.action.RUN"
+ expected_key = "some unique action key"
+
+ action_found = expected_action in plugin_content if plugin_content else False
+ key_found = expected_key in plugin_content if plugin_content else False
+
+ if action_found:
+ self.add("Shortcuts", "插件action常量", PASS, f"插件源码中包含 action={expected_action}")
+ else:
+ self.add("Shortcuts", "插件action常量", WARN, f"插件源码中未找到 action={expected_action},可能版本已变更")
+
+ if key_found:
+ self.add("Shortcuts", "插件extra key常量", PASS, f'插件源码中包含 extra key="{expected_key}"')
+ else:
+ self.add("Shortcuts", "插件extra key常量", WARN, f'插件源码中未找到 extra key="{expected_key}",可能版本已变更')
+
+ if self.shortcuts_root is not None:
+ for s in self.shortcuts_root.findall("shortcut"):
+ sid = s.get(ns("shortcutId"), "")
+ intent = s.find("intent")
+ if intent is None:
+ continue
+ xml_action = intent.get(ns("action"), "")
+ extras = intent.findall("extra")
+ xml_key = extras[0].get(ns("name"), "") if extras else ""
+
+ action_match = xml_action == expected_action if action_found else True
+ key_match = xml_key == expected_key if key_found else True
+
+ if action_match and key_match:
+ self.add("Shortcuts", f"{sid} 一致性", PASS, f"shortcuts.xml 与 quick_actions_android 插件常量完全一致")
+ else:
+ mismatches = []
+ if not action_match:
+ mismatches.append(f"action: xml={xml_action}, plugin={expected_action}")
+ if not key_match:
+ mismatches.append(f"extra key: xml={xml_key}, plugin={expected_key}")
+ self.add(
+ "Shortcuts",
+ f"{sid} 一致性",
+ FAIL,
+ f"shortcuts.xml 与插件常量不匹配!快捷方式将失效",
+ mismatches,
+ )
+
+ def _check_shortcuts_dart_consistency(self):
+ dart_service_path = os.path.join(
+ self.project_root, "lib", "core", "services", "device", "quick_actions_service.dart"
+ )
+ content = read_file(dart_service_path)
+ if content is None:
+ self.add("Shortcuts", "Dart快捷操作一致性", WARN, "未找到 quick_actions_service.dart")
+ return
+
+ dart_types = re.findall(r"type:\s*'([^']+)'", content)
+ if not dart_types:
+ self.add("Shortcuts", "Dart快捷操作类型", WARN, "未从 Dart 代码中提取到 ShortcutItem type")
+ return
+
+ self.add("Shortcuts", "Dart快捷操作类型", PASS, f"Dart 中定义了 {len(dart_types)} 个快捷操作: {dart_types}")
+
+ if self.shortcuts_root is None:
+ return
+
+ xml_ids = [s.get(ns("shortcutId"), "") for s in self.shortcuts_root.findall("shortcut")]
+
+ for dart_type in dart_types:
+ if dart_type in xml_ids:
+ self.add("Shortcuts", f"Dart↔XML: {dart_type}", PASS, f"Dart type 与 XML shortcutId 一致")
+ else:
+ self.add(
+ "Shortcuts",
+ f"Dart↔XML: {dart_type}",
+ FAIL,
+ f"Dart type='{dart_type}' 在 XML shortcutId 中不存在 ({xml_ids})",
+ )
+
+ for xml_id in xml_ids:
+ if xml_id not in dart_types:
+ self.add(
+ "Shortcuts",
+ f"XML↔Dart: {xml_id}",
+ WARN,
+ f"XML shortcutId='{xml_id}' 在 Dart ShortcutItem 中未定义",
+ )
+
+ def _find_pub_cache(self):
+ env_path = os.environ.get("PUB_CACHE")
+ if env_path and os.path.isdir(env_path):
+ return os.path.join(env_path, "hosted", "pub.flutter-io.cn") if os.path.isdir(
+ os.path.join(env_path, "hosted", "pub.flutter-io.cn")
+ ) else os.path.join(env_path, "hosted", "pub.dev") if os.path.isdir(
+ os.path.join(env_path, "hosted", "pub.dev")
+ ) else env_path
+
+ home = os.path.expanduser("~")
+ candidates = [
+ os.path.join(home, "AppData", "Local", "Pub", "Cache"),
+ os.path.join(home, ".pub-cache"),
+ os.path.join(home, ".pub_cache"),
+ ]
+ for c in candidates:
+ hosted = os.path.join(c, "hosted")
+ if os.path.isdir(hosted):
+ for sub in os.listdir(hosted):
+ sub_path = os.path.join(hosted, sub)
+ if os.path.isdir(sub_path) and os.path.isdir(
+ os.path.join(sub_path, "quick_actions_android")
+ ):
+ return sub_path
+ return hosted
+
+ return None
+
+ def check_16kb_page_support(self):
+ if self.app_gradle_content is None:
+ self.add("Gradle", "16KB页面支持", WARN, "无法读取 app/build.gradle.kts")
+ return
+
+ if "useLegacyPackaging" in self.app_gradle_content:
+ if "useLegacyPackaging = false" in self.app_gradle_content or "useLegacyPackaging=false" in self.app_gradle_content:
+ self.add("Gradle", "16KB页面支持", PASS, "useLegacyPackaging=false,已支持 Android 15+ 16KB 页面大小")
+ else:
+ self.add("Gradle", "16KB页面支持", FAIL, "useLegacyPackaging=true,不支持 Android 15+ 16KB 页面大小设备")
+ else:
+ self.add("Gradle", "16KB页面支持", WARN, "未设置 useLegacyPackaging,建议显式设为 false")
+
+ def check_sdk_versions(self):
+ if self.app_gradle_content is None:
+ self.add("Gradle", "SDK版本", WARN, "无法读取 app/build.gradle.kts")
+ return
+
+ min_sdk_match = re.search(r"minSdk\s*=\s*(\d+)", self.app_gradle_content)
+ target_sdk_match = re.search(r"targetSdk\s*=\s*(\S+)", self.app_gradle_content)
+ compile_sdk_match = re.search(r"compileSdk\s*=\s*(\S+)", self.app_gradle_content)
+
+ min_sdk = int(min_sdk_match.group(1)) if min_sdk_match else None
+ target_sdk = target_sdk_match.group(1) if target_sdk_match else None
+ compile_sdk = compile_sdk_match.group(1) if compile_sdk_match else None
+
+ if min_sdk is not None:
+ if min_sdk >= 28:
+ self.add("Gradle", f"minSdk={min_sdk}", PASS, f"最低SDK版本 {min_sdk},满足基本要求")
+ else:
+ self.add("Gradle", f"minSdk={min_sdk}", WARN, f"最低SDK版本 {min_sdk},建议 >= 28")
+ else:
+ self.add("Gradle", "minSdk", FAIL, "未找到 minSdk 配置")
+
+ if target_sdk is not None:
+ if target_sdk.startswith("flutter."):
+ self.add("Gradle", f"targetSdk={target_sdk}", PASS, f"使用 Flutter 默认 targetSdk ({target_sdk})")
+ else:
+ try:
+ tv = int(target_sdk)
+ if tv >= 34:
+ self.add("Gradle", f"targetSdk={tv}", PASS, f"目标SDK版本 {tv},满足 Google Play 要求")
+ else:
+ self.add("Gradle", f"targetSdk={tv}", WARN, f"目标SDK版本 {tv},Google Play 要求 >= 34")
+ except ValueError:
+ self.add("Gradle", f"targetSdk={target_sdk}", WARN, f"无法解析 targetSdk 值: {target_sdk}")
+ else:
+ self.add("Gradle", "targetSdk", WARN, "未找到 targetSdk 配置")
+
+ if compile_sdk is not None:
+ self.add("Gradle", f"compileSdk={compile_sdk}", PASS, f"编译SDK版本: {compile_sdk}")
+ else:
+ self.add("Gradle", "compileSdk", WARN, "未找到 compileSdk 配置")
+
+ def check_ndk_config(self):
+ if self.app_gradle_content is None:
+ self.add("Gradle", "NDK配置", WARN, "无法读取 app/build.gradle.kts")
+ return
+
+ ndk_matches = re.findall(r"abiFilters\.add\([\"']([^\"']+)[\"']\)", self.app_gradle_content)
+ if ndk_matches:
+ if "arm64-v8a" in ndk_matches:
+ self.add("Gradle", "NDK abiFilters", PASS, f"已配置 ABI 过滤: {ndk_matches}")
+ else:
+ self.add("Gradle", "NDK abiFilters", WARN, f"ABI 过滤中缺少 arm64-v8a: {ndk_matches}")
+ else:
+ self.add("Gradle", "NDK abiFilters", WARN, "未配置 abiFilters,将包含所有架构")
+
+ ndk_version_match = re.search(r"ndkVersion\s*=\s*(\S+)", self.app_gradle_content)
+ if ndk_version_match:
+ self.add("Gradle", "NDK版本", PASS, f"ndkVersion={ndk_version_match.group(1)}")
+ else:
+ self.add("Gradle", "NDK版本", PASS, "使用 Flutter 默认 NDK 版本")
+
+ def check_signing_config(self):
+ if self.app_gradle_content is None:
+ self.add("Gradle", "签名配置", WARN, "无法读取 app/build.gradle.kts")
+ return
+
+ if "signingConfig" in self.app_gradle_content:
+ if "signingConfigs.getByName(\"debug\")" in self.app_gradle_content:
+ self.add("Gradle", "签名配置", WARN, "Release 使用 debug 签名,正式发布前需配置 release 签名")
+ else:
+ self.add("Gradle", "签名配置", PASS, "已配置自定义签名")
+ else:
+ self.add("Gradle", "签名配置", WARN, "未找到签名配置")
+
+ def check_gradle_properties(self):
+ if self.gradle_props_content is None:
+ self.add("Gradle", "gradle.properties", WARN, "无法读取 gradle.properties")
+ return
+
+ if "android.useAndroidX=true" in self.gradle_props_content:
+ self.add("Gradle", "AndroidX", PASS, "已启用 AndroidX")
+ else:
+ self.add("Gradle", "AndroidX", WARN, "未启用 AndroidX")
+
+ jvm_args_match = re.search(r"org\.gradle\.jvmargs=(.+)", self.gradle_props_content)
+ if jvm_args_match:
+ args = jvm_args_match.group(1).strip()
+ if "-Xmx" in args:
+ self.add("Gradle", "JVM内存", PASS, f"Gradle JVM 参数: {args}")
+ else:
+ self.add("Gradle", "JVM内存", WARN, f"JVM 参数中未设置 -Xmx: {args}")
+ else:
+ self.add("Gradle", "JVM内存", WARN, "未设置 org.gradle.jvmargs")
+
+ def check_shortcuts_meta_data(self):
+ if self.manifest_root is None:
+ return
+
+ app = self.manifest_root.find("application")
+ if app is None:
+ return
+
+ activity = None
+ for act in app.findall("activity"):
+ if "MainActivity" in act.get(ns("name"), ""):
+ activity = act
+ break
+
+ if activity is None:
+ return
+
+ has_shortcuts_meta = False
+ for meta in activity.findall("meta-data"):
+ name = meta.get(ns("name"), "")
+ if name == "android.app.shortcuts":
+ has_shortcuts_meta = True
+ resource = meta.get(ns("resource"), "")
+ if resource == "@xml/shortcuts":
+ self.add("Manifest", "shortcuts meta-data", PASS, "android.app.shortcuts 指向 @xml/shortcuts")
+ else:
+ self.add("Manifest", "shortcuts meta-data", WARN, f"android.app.shortcuts 指向 {resource},请确认是否正确")
+ break
+
+ if not has_shortcuts_meta:
+ if self.shortcuts_root is not None:
+ self.add("Manifest", "shortcuts meta-data", FAIL, "shortcuts.xml 存在但 Activity 中缺少 android.app.shortcuts meta-data")
+ else:
+ self.add("Manifest", "shortcuts meta-data", PASS, "无 shortcuts 配置,无需 meta-data")
+
+ def check_manage_space_activity(self):
+ if self.manifest_root is None:
+ return
+
+ app = self.manifest_root.find("application")
+ if app is None:
+ return
+
+ manage_space = app.get(ns("manageSpaceActivity"), "")
+ if manage_space:
+ self.add("Manifest", "manageSpaceActivity", PASS, f"已配置 manageSpaceActivity={manage_space}")
+ else:
+ self.add("Manifest", "manageSpaceActivity", WARN, "未配置 manageSpaceActivity,用户无法通过系统设置清理应用数据")
+
+ def check_cleartext_traffic(self):
+ if self.manifest_root is None:
+ return
+
+ app = self.manifest_root.find("application")
+ if app is None:
+ return
+
+ cleartext = app.get(ns("usesCleartextTraffic"), "")
+ if cleartext == "true":
+ self.add("Manifest", "usesCleartextTraffic", WARN, "已启用明文流量(HTTP),生产环境建议关闭")
+ else:
+ self.add("Manifest", "usesCleartextTraffic", PASS, "未启用明文流量,安全性良好")
+
+ def check_queries(self):
+ if self.manifest_root is None:
+ return
+
+ queries = self.manifest_root.find("queries")
+ if queries is not None:
+ intents = queries.findall("intent")
+ self.add("Manifest", "queries配置", PASS, f"已配置 ,包含 {len(intents)} 个 intent(包可见性适配)")
+ else:
+ self.add("Manifest", "queries配置", WARN, "未配置 ,Android 11+ 可能无法查询其他应用")
+
+ def check_work_manager_receiver(self):
+ if self.manifest_root is None:
+ return
+
+ app = self.manifest_root.find("application")
+ if app is None:
+ return
+
+ for receiver in app.findall("receiver"):
+ name = receiver.get(ns("name"), "")
+ if "RescheduleReceiver" in name:
+ tools_node_val = receiver.get(f"{{{TOOLS_NS}}}node", "")
+ if tools_node_val == "remove":
+ self.add("Manifest", "WorkManager Receiver", PASS, "已移除 WorkManager 自启动 Receiver,防止开机自启")
+ else:
+ self.add("Manifest", "WorkManager Receiver", WARN, "WorkManager RescheduleReceiver 未移除,可能导致开机自启")
+ return
+
+ self.add("Manifest", "WorkManager Receiver", PASS, "未发现 WorkManager RescheduleReceiver(已移除或不存在)")
+
+ def run_all_checks(self):
+ self.check_manifest_exists()
+ self.check_permissions()
+ self.check_activity_config()
+ self.check_intent_filters()
+ self.check_enable_on_back_invoked()
+ self.check_shortcuts_meta_data()
+ self.check_shortcuts_xml()
+ self.check_shortcuts_flutter_consistency()
+ self.check_16kb_page_support()
+ self.check_sdk_versions()
+ self.check_ndk_config()
+ self.check_signing_config()
+ self.check_gradle_properties()
+ self.check_manage_space_activity()
+ self.check_cleartext_traffic()
+ self.check_queries()
+ self.check_work_manager_receiver()
+ return self.results
+
+ def calculate_score(self):
+ if not self.results:
+ return 0
+ total = sum(SCORE_WEIGHTS[r.status] for r in self.results)
+ max_total = len(self.results) * SCORE_WEIGHTS[PASS]
+ return round(total / max_total * 100) if max_total > 0 else 0
+
+ def print_report(self):
+ status_icon = {PASS: "✅", WARN: "⚠️", FAIL: "❌"}
+
+ categories = {}
+ for r in self.results:
+ categories.setdefault(r.category, []).append(r)
+
+ print("\n" + "=" * 60)
+ print(" 闲言APP — Android 配置一致性检查报告")
+ print("=" * 60)
+
+ for cat, items in categories.items():
+ print(f"\n📦 {cat}")
+ print("-" * 40)
+ for item in items:
+ icon = status_icon.get(item.status, "❓")
+ print(f" {icon} {item.name}: {item.message}")
+ if self.verbose and item.detail:
+ for d in item.detail:
+ print(f" → {d}")
+
+ score = self.calculate_score()
+ pass_count = sum(1 for r in self.results if r.status == PASS)
+ warn_count = sum(1 for r in self.results if r.status == WARN)
+ fail_count = sum(1 for r in self.results if r.status == FAIL)
+
+ print("\n" + "=" * 60)
+ print(f" 📊 总计: {len(self.results)} 项检查")
+ print(f" ✅ 通过: {pass_count}")
+ print(f" ⚠️ 警告: {warn_count}")
+ print(f" ❌ 错误: {fail_count}")
+ print(f" 🏆 评分: {score}/100")
+ print("=" * 60)
+
+ if fail_count > 0:
+ print("\n🔴 需要立即修复的错误:")
+ for r in self.results:
+ if r.status == FAIL:
+ print(f" • {r.category} → {r.name}: {r.message}")
+
+ if warn_count > 0:
+ print(f"\n🟡 建议关注的警告 ({warn_count} 项):")
+ for r in self.results:
+ if r.status == WARN:
+ print(f" • {r.category} → {r.name}: {r.message}")
+
+ print()
+ return score
+
+ def json_report(self):
+ score = self.calculate_score()
+ pass_count = sum(1 for r in self.results if r.status == PASS)
+ warn_count = sum(1 for r in self.results if r.status == WARN)
+ fail_count = sum(1 for r in self.results if r.status == FAIL)
+
+ report = {
+ "project": os.path.basename(self.project_root),
+ "score": score,
+ "total": len(self.results),
+ "pass": pass_count,
+ "warn": warn_count,
+ "fail": fail_count,
+ "checks": [r.to_dict() for r in self.results],
+ }
+ print(json.dumps(report, ensure_ascii=False, indent=2))
+ return score
+
+
+def main():
+ parser = argparse.ArgumentParser(description="闲言APP Android配置一致性检查")
+ parser.add_argument("--verbose", "-v", action="store_true", help="输出详细信息")
+ parser.add_argument("--json", action="store_true", help="输出JSON格式报告")
+ parser.add_argument("--project", "-p", help="项目根目录路径(默认自动检测)")
+ args = parser.parse_args()
+
+ project_root = args.project or find_project_root()
+
+ if not os.path.isfile(os.path.join(project_root, "pubspec.yaml")):
+ print(f"❌ 未找到 Flutter 项目: {project_root}")
+ sys.exit(1)
+
+ checker = AndroidConfigChecker(project_root, verbose=args.verbose)
+ checker.run_all_checks()
+
+ if args.json:
+ score = checker.json_report()
+ else:
+ score = checker.print_report()
+
+ sys.exit(0 if score >= 60 else 1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/Scripts/upload_agreements.py b/Scripts/upload_agreements.py
new file mode 100644
index 00000000..394e8527
--- /dev/null
+++ b/Scripts/upload_agreements.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+"""上传协议文件到服务器"""
+import paramiko
+import os
+import glob
+
+HOST = '123.207.67.197'
+PORT = 22
+USER = 'root'
+PASS = '520Kiss123'
+REMOTE_DIR = '/www/wwwroot/tools.wktyl.com/public/agreements/'
+LOCAL_DIR = os.path.join(os.path.dirname(__file__), '..', 'docs', 'toolsapi', 'public', 'agreements')
+
+def main():
+ local_dir = os.path.abspath(LOCAL_DIR)
+ html_files = glob.glob(os.path.join(local_dir, '*.html'))
+ print(f"找到 {len(html_files)} 个协议文件待上传")
+
+ ssh = paramiko.SSHClient()
+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ ssh.connect(HOST, port=PORT, username=USER, password=PASS, timeout=15)
+ sftp = ssh.open_sftp()
+
+ # 确保远程目录存在
+ try:
+ sftp.stat(REMOTE_DIR)
+ except FileNotFoundError:
+ sftp.mkdir(REMOTE_DIR)
+
+ for f in html_files:
+ fname = os.path.basename(f)
+ remote_path = REMOTE_DIR + fname
+ print(f" 上传: {fname} -> {remote_path}")
+ sftp.put(f, remote_path)
+
+ sftp.close()
+ ssh.close()
+ print(f"✅ 全部 {len(html_files)} 个文件上传完成")
+
+if __name__ == '__main__':
+ main()
diff --git a/docs/toolsapi/docs/SERVER_DEPLOYMENT_GUIDE.md b/docs/toolsapi/docs/SERVER_DEPLOYMENT_GUIDE.md
index 5608f064..0221cc73 100644
--- a/docs/toolsapi/docs/SERVER_DEPLOYMENT_GUIDE.md
+++ b/docs/toolsapi/docs/SERVER_DEPLOYMENT_GUIDE.md
@@ -2,7 +2,7 @@
## 一、服务器信息
- 服务器IP: 123.207.67.197
-- SSH端口: 22
+- SSH端口: 2026
- 项目路径: /www/wwwroot/tools.wktyl.com/
- 运行目录: tools.wktyl.com/public → tools.wktyl.com
- 数据库: MySQL (tools库, 前缀 tool_)
@@ -26,7 +26,7 @@ python Scripts/upload_server_code.py
### 2.2 手动SFTP上传
```bash
-sftp root@123.207.67.197
+sftp -P 2026 root@123.207.67.197
# 密码: 520Kiss123
# 上传控制器文件
@@ -35,7 +35,7 @@ put docs/toolsapi/application/api/controller/Rank.php /www/wwwroot/tools.wktyl.c
### 2.3 手动SSH操作
```bash
-ssh root@123.207.67.197
+ssh -p 2026 root@123.207.67.197
# 密码: 520Kiss123
# 查看项目结构
diff --git a/docs/toolsapi/public/agreements/privacy-policy.html b/docs/toolsapi/public/agreements/privacy-policy.html
index d6bf29ef..6bb54c39 100644
--- a/docs/toolsapi/public/agreements/privacy-policy.html
+++ b/docs/toolsapi/public/agreements/privacy-policy.html
@@ -651,28 +651,40 @@
如评估结果显示处理活动可能对您的权益产生高风险,我们将仅在采取额外保护措施后方可进行
评估记录将至少保留3年
-十、联系我们
+十、主体信息
+本应用由以下主体运营,相关身份信息已通过 Dun & Bradstreet 国际企业身份认证,您可通过 D-U-N-S 编号在全球范围内核实本主体身份:
+
+- 主体名称:弥勒市朋普镇微风暴网络科技工作室(Mile Pengpu Town Micro Storm Network Technology Studio)
+- 经营别称(DBA):微风暴(Micro Storm)
+- D-U-N-S 编号:586261192
+- 统一社会信用代码:92532526MA6PCX153W
+- 注册地址:中国 云南省 红河州 弥勒市 朋普社区 49号
+- 所属行业:计算机系统设计与相关服务(Computer Systems Design and Related Services)
+- 隐私事务联系邮箱:ad@avefs.com
+
+本主体在 Apple App Store、Google Play 等全球应用分发平台登记的开发者主体信息与本章节披露内容一致,便于监管机构、合作方及用户进行身份核验。
+十一、联系我们
如果您对本隐私政策有任何疑问或建议,请通过以下方式联系我们:
- 邮箱:2821981550@qq.com
- 应用内反馈:我的 → 关于 → 用户反馈
-- 通信地址:云南省昆明市西山区滇池度假区(碧鸡街道车家壁513号)
+- 通信地址:中国 云南省 红河州 弥勒市 朋普社区 49号
- ICP备案号:滇ICP备2022000863号-18A
我们将在15个工作日内回复您的隐私相关请求。
个人信息保护负责人:李先生(邮箱:gg@0gg.cc)
-十一、国际数据保护
+十二、国际数据保护
如您位于中华人民共和国境外,我们还将遵守适用的当地数据保护法律:
-11.1 欧盟通用数据保护条例(GDPR)
+12.1 欧盟通用数据保护条例(GDPR)
如您位于欧洲经济区(EEA),您享有以下额外权利:
- 合法处理依据:我们依据合法利益、合同履行或您的同意等合法依据处理您的个人信息
-- 欧盟代表:我们已指定欧盟代表,联系方式可通过本政策列明的邮箱获取
+- 欧盟代表:我们暂未指定欧盟代表(GDPR Art.27 代表)。如有需要,您可通过本政策列明的邮箱与我们联系
- 数据可携带权:您有权以结构化、通用和机器可读的格式获取您的个人数据,并有权将数据传输给其他控制者
- 限制处理权:在特定情况下,您有权要求我们限制对您个人信息的处理
- 反对权:您有权随时反对基于合法利益对您个人信息的处理
-11.2 加州消费者隐私法(CCPA)
+12.2 加州消费者隐私法(CCPA)
如您为加州居民,您享有以下权利:
- 知情权:您有权了解我们收集了哪些个人信息及其用途
@@ -680,7 +692,7 @@
- 选择退出权:我们不出售您的个人信息,因此您无需行使选择退出权
- 不受歧视权:行使隐私权利不会导致您受到歧视性对待
-11.3 美国儿童在线隐私保护法(COPPA)
+12.3 美国儿童在线隐私保护法(COPPA)
- 我们不针对13岁以下儿童设计或运营服务
- 如我们知悉收集了13岁以下儿童的个人信息,将立即删除
@@ -693,7 +705,7 @@
Version: V6.7
Updated: June 5, 2026
Effective: June 6, 2026
-Zero. Definitions
+Zero. Definitions
- **Personal Information**: Information that can identify a natural person, including but not limited to name, date of birth, ID number, biometric information, address, phone number, email, etc.
- **Sensitive Personal Information**: Personal information that, once leaked or illegally used, may harm personal dignity or personal/property safety, including biometric information, religious beliefs, financial information, etc.
@@ -790,6 +802,105 @@
2.3 We will not use your personal information for purposes other than those stated above without your additional consent
+II.5. Business Functions and Personal Information Mapping Table
+According to the requirements of the "Methods for Identifying Illegal and Irregular Collection and Use of Personal Information by Apps", the following details the personal information collected by each business function:
+
+| Business Function |
+Types of Personal Information Collected |
+Necessary |
+Impact of Refusing to Provide |
+
+
+| Daily Sentences and Discover |
+Device information, app information |
+Yes |
+Unable to use reading features |
+
+
+| Card Creation |
+Album/Camera permission (optional) |
+No |
+Unable to use image-related features; text-only creation available |
+
+
+| Poetry Recommendation |
+Device information |
+Yes |
+Unable to use poetry features |
+
+
+| AI Smart Assistant |
+Conversation content, device information |
+Yes |
+Unable to use AI conversation features |
+
+
+| File Transfer Assistant |
+Location/Nearby devices permission (optional) |
+No |
+Unable to use file transfer features |
+
+
+| Notes and Articles |
+User content, device information |
+Yes |
+Unable to use notes features |
+
+
+| Weather Information |
+Approximate location (optional) |
+No |
+Need to manually select city |
+
+
+| Daily Check-in and Points |
+Account information |
+Yes |
+Unable to participate in check-in activities |
+
+
+| Push Notifications |
+Notification permission (optional), device identifier |
+No |
+Unable to receive push messages |
+
+
+| Cloud Data Sync |
+Account information, user content |
+Yes |
+Unable to use sync features |
+
+
+| Voice Reading |
+Device information |
+Yes |
+Unable to use reading-aloud features |
+
+
+| QR Code Scanning |
+Camera permission (optional) |
+No |
+Unable to use scanning features |
+
+
+| OCR Text Recognition |
+Camera permission, image content (optional) |
+No |
+Unable to use OCR features |
+
+
+| Membership Value-Added Services |
+Account information |
+Yes |
+Unable to use membership features |
+
+
+Notes:
+
+- Information marked as "Necessary" is required to provide that function; refusal to provide will result in the function being unavailable
+- Information or permissions marked as "Optional" - refusal only affects the relevant sub-function and does not affect the normal use of other functions
+- You may withdraw granted optional permissions at any time in system settings
+
III. How We Store Your Personal Information
3.1 Storage Location
@@ -933,37 +1044,173 @@
- After the review is complete, your account and related data will be permanently deleted
- Deleted data cannot be recovered, please proceed with caution
-VI. Protection of Minors' Information
-6.1 This app is not directed at children under the age of 14
-6.2 We do not knowingly collect personal information from children under 14
-6.3 If you are under 14, please do not use this app or provide personal information
-6.4 If we discover that we have collected information from a child under 14, we will delete it immediately
-6.5 Age verification mechanism: Users must confirm they are 14 years or older during registration
-VII. Deceased Person's Information
-7.1 The personal information of a deceased user may be handled by their close relatives in accordance with the law
-7.2 Close relatives may request: reading, copying, correction, or deletion of the deceased's personal information
-7.3 To exercise these rights, please contact: 2821981550@qq.com
-VIII. Updates to This Privacy Policy
-8.1 We may update this policy from time to time
-8.2 For significant changes, we will notify you through:
+VI. Cookie and Similar Technologies
+This app does not use traditional web Cookie technology. We use local storage (SharedPreferences and SQLite) to save your preference settings and usage data. This data is stored entirely on your device and is not transmitted to servers for tracking purposes.
+VII. Third-Party SDKs
+The third-party SDKs currently integrated are listed below. We strictly review the data collection practices of third-party SDKs to ensure they comply with the requirements of this Privacy Policy:
+
+| SDK Name |
+Provider |
+Information Collected |
+Purpose |
+Privacy Policy Link |
+
+
+| Flutter SDK |
+Google LLC |
+No additional personal information collected |
+Cross-platform app rendering engine |
+https://flutter.dev/privacy |
+
+
+| WebRTC |
+Google LLC / Open Source Community |
+No additional personal information collected |
+LAN file transfer |
+https://webrtc.org/privacy |
+
+
+Notes:
+
+- Flutter SDK is an application development framework and does not independently collect users' personal information
+- We have not integrated push service SDKs, statistical analytics SDKs, advertising SDKs, or other types of SDKs
+- If new third-party SDKs are integrated in the future, we will promptly update this section and notify you through in-app announcements and other means
+
+VII.5. Automated Decision-Making
+7.5.1 Use of Automated Decision-Making
+We may use automated decision-making mechanisms (including but not limited to automated recommendation algorithms, user profiling, etc.) to provide you with personalized content recommendations and service optimization. We commit that:
+
+- Automated decision-making is solely used to improve your user experience
+- We will not make decisions that significantly affect your rights and interests solely through automated decision-making
+- If automated decision-making significantly affects you, you have the right to request an explanation from us and to refuse decisions made solely through automated decision-making
+
+7.5.2 User Rights
+
+- You have the right to understand the logic and potential impact of automated decision-making
+- You have the right to refuse decisions made by us solely through automated decision-making
+- You have the right to request that we conduct a manual review of the results of automated decision-making
+
+7.5.3 Safeguard Measures
+
+- We will regularly evaluate the reasonableness and fairness of the automated decision-making system
+- We will ensure that automated decision-making does not produce discriminatory results
+- We will establish a manual intervention mechanism to protect your legitimate rights and interests
+
+VIII. Protection of Minors' Information
+8.1 This app is not directed at children under the age of 14
+8.2 We do not knowingly collect personal information from children under 14
+8.3 If you are under 14, please do not use this app or provide personal information
+8.4 If we discover that we have collected information from a child under 14, we will delete it immediately
+8.5 Age verification mechanism: Users must confirm they are 14 years or older during registration
+VIII.5. Personal Information Security Incident Response
+8.5.1 Definition of Security Incidents
+Personal information security incidents refer to events that may harm your legitimate rights and interests, such as leakage, tampering, or loss of personal information caused by us or third parties.
+8.5.2 Emergency Response Mechanism
+In the event of a personal information security incident, we will:
+
+- Immediately activate the emergency response mechanism and take remedial measures
+- Report to the competent authority within 72 hours after the incident occurs
+- Promptly notify affected users through in-app notifications, emails, SMS, etc. The notification will include: incident type, types of information involved, potential harms, remedial measures we have taken, and measures users can take to mitigate harms
+
+8.5.3 Notification Exceptions
+If any of the following conditions is met, we may be unable to notify you in a timely manner:
+
+- Effective measures have been taken to avoid causing harm to you
+- The competent authority determines that notification may affect the investigation or expand the harm
+- Laws and regulations provide otherwise
+
+8.5.4 Post-Incident Improvement
+After a security incident is resolved, we will:
+
+- Conduct a comprehensive investigation and assessment of the incident
+- Improve security measures to prevent similar incidents from recurring
+- Submit an incident handling report to the competent authority
+
+IX. Deceased Person's Information
+9.1 The personal information of a deceased user may be handled by their close relatives in accordance with the law
+9.2 Close relatives may request: reading, copying, correction, or deletion of the deceased's personal information
+9.3 To exercise these rights, please contact: 2821981550@qq.com
+X. Updates to This Privacy Policy
+10.1 We may update this policy from time to time
+10.2 For significant changes, we will notify you through:
- In-app notification
- Email notification
- Prominent notice within the app
-8.3 After the policy is updated, your continued use of the app constitutes acceptance of the updated policy
-IX. Contact Us
+10.3 After the policy is updated, your continued use of the app constitutes acceptance of the updated policy
+X.5. Personal Information Protection Impact Assessment
+10.5.1 Assessment Scope
+In the following circumstances, we will conduct a Personal Information Protection Impact Assessment in accordance with the law:
+
+- Processing sensitive personal information
+- Using personal information for automated decision-making
+- Entrusting the processing of personal information, providing personal information to other personal information controllers, or making personal information public
+- Providing personal information overseas
+- Other personal information processing activities that have a significant impact on personal rights and interests
+
+10.5.2 Assessment Content
+The Personal Information Protection Impact Assessment will include the following content:
+
+- Whether the processing purposes, methods, etc. of personal information are lawful, legitimate, and necessary
+- The impact on personal rights and interests and security risks
+- Whether the protection measures taken are lawful, effective, and commensurate with the level of risk
+
+10.5.3 Assessment Results
+
+- The assessment results will serve as the basis for our decision on whether to carry out relevant personal information processing activities
+- If the assessment results show that the processing activities may pose a high risk to your rights and interests, we will only proceed after taking additional protective measures
+- Assessment records will be retained for at least 3 years
+
+XI. Entity Information
+This Application is operated by the following entity, whose identity has been certified by Dun & Bradstreet. You may verify our entity globally via the D-U-N-S Number:
+
+- Entity Name: Mile Pengpu Town Micro Storm Network Technology Studio
+- Doing Business As (DBA): Micro Storm
+- D-U-N-S Number: 586261192
+- Unified Social Credit Code: 92532526MA6PCX153W
+- Registered Address: No. 49, Pengxiao Road, Pengpu Community, Pengpu Town, Honghe Hani and Yi Autonomous Prefecture, Yunnan, 652301 China
+- Industry: Computer Systems Design and Related Services
+- Privacy Contact Email: ad@avefs.com
+
+The developer entity information registered on global app distribution platforms (including Apple App Store and Google Play) is consistent with the information disclosed in this section, facilitating identity verification by regulators, partners, and users.
+XII. Contact Us
- Email: 2821981550@qq.com
-- In-app feedback: My → About → User Feedback
-- Mailing address: Dianchi Resort Area, Xishan District, Kunming, Yunnan Province, China (Bijie Subdistrict, Chejiabi No. 513)
+- In-app feedback: Profile → About → User Feedback
+- Mailing Address: No. 49, Pengxiao Road, Pengpu Community, Pengpu Town, Honghe Hani and Yi Autonomous Prefecture, Yunnan, 652301 China
- ICP Registration: 滇ICP备2022000863号-18A
- Personal Information Protection Officer: Mr. Li (Email: gg@0gg.cc)
We will respond to your privacy-related requests within 15 working days.
-X. Legal Application and Dispute Resolution
-10.1 This policy is governed by the laws of the People's Republic of China.
-10.2 Disputes arising from this policy shall be resolved through friendly negotiation; if negotiation fails, either party may file a lawsuit with the People's Court having jurisdiction over our location.
+XIII. International Data Protection
+If you are located outside the People's Republic of China, we will also comply with applicable local data protection laws:
+13.1 EU General Data Protection Regulation (GDPR)
+If you are located in the European Economic Area (EEA), you enjoy the following additional rights:
+
+- Lawful Basis for Processing: We process your personal information based on legitimate interests, contract performance, or your consent, etc.
+- EU Representative: We have not yet designated an EU Representative under GDPR Art.27. If needed, you may contact us via the email listed in this policy.
+- Right to Data Portability: You have the right to obtain your personal data in a structured, commonly used, and machine-readable format, and to transmit the data to another controller
+- Right to Restrict Processing: Under certain circumstances, you have the right to request that we restrict the processing of your personal information
+- Right to Object: You have the right to object at any time to the processing of your personal information based on legitimate interests
+
+13.2 California Consumer Privacy Act (CCPA)
+If you are a California resident, you enjoy the following rights:
+
+- Right to Know: You have the right to know what personal information we collect and how it is used
+- Right to Delete: You have the right to request that we delete your personal information
+- Right to Opt-Out: We do not sell your personal information, so you do not need to exercise the right to opt-out
+- Right to Non-Discrimination: Exercising your privacy rights will not result in discriminatory treatment
+
+13.3 U.S. Children's Online Privacy Protection Act (COPPA)
+
+- We do not direct our services to children under 13
+- If we become aware that we have collected personal information from a child under 13, we will delete it immediately
+- If you are the guardian of a child under 13 and believe we have collected that child's information, please contact us
+
+XIV. Legal Application and Dispute Resolution
+14.1 This policy is governed by the laws of the People's Republic of China.
+14.2 Disputes arising from this policy shall be resolved through friendly negotiation; if negotiation fails, either party may file a lawsuit with the People's Court having jurisdiction over our location.
If any provision of this agreement is found to be invalid or unenforceable, the validity of the remaining provisions shall not be affected.
@@ -973,8 +1220,9 @@
@@ -986,17 +1234,19 @@
subtitle: '我们如何收集、使用、存储和保护您的个人信息',
backLink: '← 返回协议列表',
company: '弥勒市朋普镇微风暴网络科技工作室',
- contact: '📧 2821981550@qq.com | � 2572560133@qq.com | � 云南省昆明市西山区滇池度假区(碧鸡街道车家壁513号)',
+ contact: '📧 2821981550@qq.com | 📧 2572560133@qq.com | 📍 中国 云南省 红河州 弥勒市 朋普社区 49号',
credit: '统一社会信用代码:92532526MA6PCX153W',
+ duns: 'D-U-N-S 编号:586261192',
icp: '滇ICP备2022000863号-18A'
},
en: {
title: 'Privacy Policy',
subtitle: 'How we collect, use, store and protect your personal information',
backLink: '← Back to Agreement List',
- company: 'Mile City Pengpu Town Weifengbao Network Technology Studio',
- contact: '📧 2821981550@qq.com | � 2572560133@qq.com | � Dianchi Resort, Xishan District, Kunming, Yunnan, China',
+ company: 'Mile Pengpu Town Micro Storm Network Technology Studio',
+ contact: '📧 2821981550@qq.com | 📧 2572560133@qq.com | 📍 No. 49, Pengxiao Road, Pengpu Community, Pengpu Town, Honghe Hani and Yi Autonomous Prefecture, Yunnan, 652301 China',
credit: 'Unified Social Credit Code: 92532526MA6PCX153W',
+ duns: 'D-U-N-S Number: 586261192',
icp: 'ICP License: 滇ICP备2022000863号-18A'
}
};
@@ -1010,6 +1260,7 @@
document.getElementById('footer-company').textContent = DATA[lang].company;
document.getElementById('footer-contact').innerHTML = DATA[lang].contact;
document.getElementById('footer-credit').textContent = DATA[lang].credit;
+ document.getElementById('footer-duns').textContent = DATA[lang].duns;
document.getElementById('footer-icp').textContent = DATA[lang].icp;
document.getElementById('btn-zh').className = 'lang-btn' + (lang === 'zh' ? ' active' : '');
document.getElementById('btn-en').className = 'lang-btn' + (lang === 'en' ? ' active' : '');
diff --git a/iOS_macOS_Developer_Guide.md b/iOS_macOS_Developer_Guide.md
index fff9e78a..03e27bcd 100644
--- a/iOS_macOS_Developer_Guide.md
+++ b/iOS_macOS_Developer_Guide.md
@@ -10,14 +10,12 @@
| 日期 | 版本 | 变更内容 |
|---|---|---|
+| 2026-06-26 | v14 | **macOS 权限动态申请**:①新建 `PermissionManager.swift` 原生权限管理器(AVFoundation/Photos/UserNotifications),通过 MethodChannel 暴露给 Flutter;②AppDelegate 新增 `checkPermission`/`requestPermission`/`openPermissionSettings` 三个 channel 方法;③MacosPlatformService 新增权限管理方法;④PermissionService macOS 分支改为调用原生 API,实现相机/麦克风/相册/通知权限的动态申请(替代 permission_handler_apple 无 macOS 实现的问题);⑤AppPermission 枚举新增 `macosPermissionName` getter |
+| 2026-06-26 | v13 | **macOS Impeller 开关修复**:①修复「通用设置 → Impeller 渲染引擎」开关不生效的严重 bug(原 `setenv("FLUTTER_ENGINE_SWITCH_0")` 方式 macOS 桌面 embedder 不读取,改用 `FlutterDartProject.commandLineArguments` 传递 `--enable-impeller`/`--no-enable-impeller`);②x86_64 端开启 Impeller 前增加二次确认警告对话框(说明 Intel Mac 上的 Metal 驱动渲染资源累积风险);③Apple Silicon (arm64) 在重启对话框中显示「推荐开启」提示卡片;④新增 5 个翻译键(覆盖全部 14 种语言) |
+| 2026-06-26 | v12 | **macOS App Store 审核修复**:①flutter_webrtc 1.4.0→1.5.2(对齐 WebRTC-SDK 144.7559.09,解决 CocoaPods 版本冲突);②新增 §2.8.9 flutter_webrtc 特殊包说明;③新增 §2.8.10 permission_handler_apple macOS 缺失实现说明(macOS 端权限改由 entitlement + Info.plist 自动管理);④新增 §6.x macOS entitlement 与权限管理适配说明 |
| 2026-06-15 | v11 | 同步三方库升级:删除custom_lint/riverpod_lint;新增analyzer/test_api/test/xml/pointycastle overrides;record降级到^6.2.1(7.0.0需Dart3.12+);更新差异对照表和dependency_overrides行数 |
| 2026-06-07 | v10 | 修正 §2.3 dependency_overrides 行数(4→5行/40+→46行);修正 §2.6 补丁引用(§2.8→§2.9);简化 §2.8.1 pro_image_editor 过时回退建议;删除 §5.4 pro_image_editor 本地包条目和 bitsdojo_window 废弃条目;简化 §3.3 pubspec.yaml 处理策略(git stash → 双模板脚本生成);更新 §3.2/§3.5/§6 与双模板机制对齐 |
-| 2026-06-06 | v9 | 清理未使用依赖:移除 animations、animate_do、value_layout_builder、flutter_advanced_canvas_editor、flutter_blue_plus、http_cache_file_store、dartx、vector_math;删除差异对照表中 flutter_nfc_kit 过时条目 |
-| 2026-06-06 | v8 | 新增 `app_tracking_transparency` 差异对照条目;新增 `nearby_connections` 鸿蒙端本地stub包说明;新增 §2.10 nearby_connections鸿蒙适配说明 |
-| 2026-06-02 | v7 | **重大变更**:pubspec.yaml 拆分为双模板(pubspec.ohos.yaml + pubspec.macos.yaml),pubspec.yaml 不再提交到 Git;新增三方库变更通知机制;新增 setup_pubspec.ps1 脚本 |
-| 2026-06-02 | v6 | 鸿蒙端 pubspec.yaml 同步 bitsdojo_window → window_manager 迁移;更新 file_picker 本地包版本注释(v8.3.7→v11.0.0-ohos.1);更新 speech_to_text(^7.0.0→^7.4.0)、live_activities(^2.0.0→^2.4.9) 远程版本号;补充 dependency_overrides 中 bitsdojo_window_windows 移除说明 |
-| 2026-06-01 | v5 | 新增 §2.6 pub cache 补丁说明;标记 bitsdojo_window 迁移完成;file_picker 升级到 12.x |
-| 2026-05-30 | v4 | 初版完整指南 |
+
---
@@ -168,7 +166,7 @@ Error: The getter 'ohos' isn't defined for the class 'TargetPlatform'
| local_auth | `path: packages/local_auth` | `^3.0.1` |
| battery_plus | `path: packages/battery_plus` | `^7.0.0` |
| network_info_plus | `path: packages/network_info_plus` | `^8.1.0` |
-| flutter_webrtc | `path: packages/flutter_webrtc` | `^1.4.0` |
+| flutter_webrtc | `path: packages/flutter_webrtc` | `^1.5.2` |
| mobile_scanner | `path: packages/mobile_scanner` | `^7.2.0` |
| wifi_iot | `path: packages/wifi_iot` | `^0.3.19` |
| nearby_service | `path: packages/nearby_service` | `^0.2.1` |
@@ -357,6 +355,77 @@ MacBook Pro 端使用 pub.dev 版本 `^0.9.3`,鸿蒙端的 `ohosName` 参数
app_tracking_transparency: ^2.0.6
```
+#### 2.8.9 flutter_webrtc(macOS WebRTC-SDK 版本对齐)
+
+macOS 端 `flutter_webrtc` 已从 `^1.4.0` 升级为 `^1.5.2`,原因:
+
+1. **CocoaPods 版本冲突**:
+ - macOS `Podfile` 通过本地 podspec `macos/WebRTC-SDK.podspec.json` 声明 WebRTC 二进制库版本 `144.7559.09`(Intel Mac 渲染修复补丁,使用 ghfast.top 镜像下载)
+ - `flutter_webrtc 1.4.0` 的 macOS podspec 依赖 `WebRTC-SDK 144.7559.01`(`.01` 版本),与本地 podspec 的 `.09` 版本冲突,`pod install` 报错:`CocoaPods could not find compatible versions for pod "WebRTC-SDK"`
+ - `flutter_webrtc 1.5.2` 的 macOS podspec 已对齐声明 `s.dependency 'WebRTC-SDK', '144.7559.09'`,与本地 podspec 一致,冲突解决
+
+2. **API 兼容性**:
+ - 1.4.0 → 1.5.2 为兼容性升级,项目使用的 WebRTC API(`RTCPeerConnection` / `MediaStream` / `VideoRenderer`)均未变更
+ - 已通过 `flutter analyze` 验证:`screen_share_page.dart` / `webrtc_service.dart` / `screen_share_provider.dart` 无需修改
+
+3. **鸿蒙端注意**:
+ - 鸿蒙端使用本地包 `packages/flutter_webrtc`(v1.4.0-ohos.1),**无法直接升级到 1.5.2**
+ - 鸿蒙端本地包待后续同步升级到 `1.5.2-ohos` 版本
+ - 远程端升级不影响鸿蒙端编译(两端 pubspec 独立)
+
+```yaml
+# MacBook Pro 端(pubspec.macos.yaml)
+ flutter_webrtc: ^1.5.2 # WebRTC音视频通信(1.5.2 对齐 WebRTC-SDK 144.7559.09)
+
+# 鸿蒙端(pubspec.ohos.yaml)
+ flutter_webrtc: # v1.4.0-ohos.1 | 本地化-鸿蒙适配;远程端已升至1.5.2,鸿蒙本地包待后续同步
+ path: packages/flutter_webrtc
+```
+
+> **macOS Podfile 本地 podspec 说明**:
+> ```ruby
+> # macos/Podfile 第 39 行
+> pod 'WebRTC-SDK', :podspec => 'WebRTC-SDK.podspec.json'
+> ```
+> 该 podspec 声明从 `ghfast.top` 镜像下载 `WebRTC.xcframework.zip`(v144.7559.09),
+> 避免直连 github.com 超时。升级 flutter_webrtc 时需确保其 podspec 声明的 WebRTC-SDK 版本与本地 podspec 一致。
+
+#### 2.8.10 permission_handler_apple(macOS 无实现 — 权限由 entitlement 管理)
+
+`permission_handler_apple` 是 `permission_handler` 在 iOS/macOS 平台的实现包,但 **9.4.9 版本仅支持 iOS,不支持 macOS**:
+
+```yaml
+# permission_handler_apple 9.4.9 的 pubspec.yaml
+flutter:
+ plugin:
+ implements: permission_handler
+ platforms:
+ ios: # ⚠️ 仅声明 ios,无 macos
+ pluginClass: PermissionHandlerPlugin
+```
+
+**影响**:
+- macOS 端 `GeneratedPluginRegistrant.swift` 不会注册 `PermissionHandlerApplePlugin`
+- 调用 `Permission.camera.status` / `Permission.photos.request()` / `openAppSettings()` 等方法会抛出 `MissingPluginException(No implementation found for method checkPermissionStatus on channel flutter.baseflow.com/permissions/methods)`
+- 这是 macOS App Store 审核被拒 Guideline 2.1(a) 的直接原因(权限管理页点击相册/麦克风显示错误信息)
+
+**解决方案**:
+- macOS sandbox 下,权限由 **entitlement + Info.plist 用法说明** 自动管理
+- 系统在 App 首次访问受保护资源(相机/麦克风/相册)时弹出授权对话框(由 OS 触发,不由 App 调用)
+- `permission_service.dart` 中 `checkStatus()` / `requestPermission()` 已添加 macOS 早返回逻辑,直接返回 `granted`,避免调用未注册的方法通道
+- `openSettings()` 已添加 macOS 原生跳转:`Process.run('open', ['x-apple.systempreferences:com.apple.preference.security?Privacy'])`
+
+**macOS 必需的 entitlement 与 Info.plist 配置**:
+
+| 权限 | Entitlement | Info.plist Key | 说明 |
+|---|---|---|---|
+| 相机 | `com.apple.security.device.camera` | `NSCameraUsageDescription` | 扫码 / 拍照 / OCR |
+| 麦克风 | `com.apple.security.device.audio-input` | `NSMicrophoneUsageDescription` | 语音录制 / 语音转文字 |
+| 相册 | `com.apple.security.personal-information.photos-library` | (无需)| 保存图片/视频到相册 |
+| 本地服务器 | `com.apple.security.network.server` | (无需)| LocalSend 局域网文件传输 |
+
+> **注意**:`permission_handler_apple` 后续版本可能新增 macOS 支持,届时可移除 `permission_service.dart` 中的 macOS 早返回逻辑。检查方法:查看 `pub-cache/hosted/pub.dev/permission_handler_apple-X.Y.Z/` 是否存在 `macos/` 目录。
+
### 2.9 ⚠️ 鸿蒙端升级 Tips(2026-06-15,完成后删除本节)
> **本节为鸿蒙端开发者提供升级指引,鸿蒙端完成适配后请删除此节。**
diff --git a/lib/app/layout/overview_dashboard.dart b/lib/app/layout/overview_dashboard.dart
index f56c0ffc..ce088526 100644
--- a/lib/app/layout/overview_dashboard.dart
+++ b/lib/app/layout/overview_dashboard.dart
@@ -1,66 +1,119 @@
/// ============================================================
/// 闲言APP — 概览仪表盘
/// 创建时间: 2026-05-29
-/// 更新时间: 2026-06-12
+/// 更新时间: 2026-06-26
/// 作用: 宽屏分屏右侧面板的空状态页面,显示概览信息
-/// 上次更新: AppIcon添加动画和语义标签,增强无障碍与视觉体验
+/// 上次更新: 全面国际化+动态主题/样式+触觉反馈+骨架屏+无障碍降级+时段自动刷新+空指针防护
/// ============================================================
+import 'dart:async';
+
import 'package:flutter/cupertino.dart';
+import 'package:flutter/services.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:shimmer/shimmer.dart';
import '../../core/theme/app_theme.dart';
import '../../core/theme/app_spacing.dart';
-import '../../shared/widgets/containers/glass_container.dart';
-import '../../features/home/providers/home_provider.dart';
-import '../../features/home/providers/favorite_provider.dart';
-import '../../features/home/providers/likes_provider.dart';
import '../../features/auth/providers/auth_provider.dart';
-import '../../features/signin/providers/signin_provider.dart';
+import '../../features/home/providers/favorite_provider.dart';
+import '../../features/home/providers/home_provider.dart';
+import '../../features/home/providers/likes_provider.dart';
import '../../features/home/providers/tool_center_recent_provider.dart';
import '../../features/home/presentation/home_tool_center.dart';
-import '../../core/router/app_routes.dart';
+import '../../features/settings/providers/general_settings_provider.dart';
+import '../../features/signin/providers/signin_provider.dart';
import '../../core/router/app_nav_extension.dart';
+import '../../core/router/app_routes.dart';
import '../../core/services/recent_route_service.dart';
+import '../../l10n/translation_resolver.dart';
+import '../../l10n/types/t.dart';
+import '../../shared/widgets/containers/glass_container.dart';
import '../../shared/widgets/display/app_icon.dart';
-class OverviewDashboard extends ConsumerWidget {
+/// 工作台概览仪表盘 — 右栏默认面板
+///
+/// 特性:
+/// - 全文案多语言([translationsProvider] → [TDashboard])
+/// - 动态主题/样式([AppThemeExtension] 字号缩放/字重/玻璃/圆角)
+/// - 触觉反馈([HapticFeedback])
+/// - 骨架屏加载([Shimmer])
+/// - 无障碍动画降级(响应 reduceAnimations 设置)
+/// - 时段问候语自动刷新(每分钟校验小时变更)
+/// - 空指针防护(provider 异常时回退 0)
+class OverviewDashboard extends ConsumerStatefulWidget {
const OverviewDashboard({super.key});
@override
- Widget build(BuildContext context, WidgetRef ref) {
+ ConsumerState createState() => _OverviewDashboardState();
+}
+
+class _OverviewDashboardState extends ConsumerState {
+ /// 当前小时缓存 — 用于检测时段切换触发刷新
+ int _currentHour = DateTime.now().hour;
+
+ /// 时段轮询定时器 — 每分钟校验一次,跨午时/傍晚时自动刷新问候语
+ late final Timer _hourTimer;
+
+ @override
+ void initState() {
+ super.initState();
+ _hourTimer = Timer.periodic(const Duration(minutes: 1), (_) {
+ final h = DateTime.now().hour;
+ if (h != _currentHour && mounted) {
+ setState(() => _currentHour = h);
+ }
+ });
+ }
+
+ @override
+ void dispose() {
+ _hourTimer.cancel();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final t = ref.watch(translationsProvider);
+ final ext = AppTheme.ext(context);
+ final reduceAnim = ref.watch(generalSettingsProvider).reduceAnimations;
+ final db = t.dashboard;
+
return SingleChildScrollView(
padding: const EdgeInsets.all(AppSpacing.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- _buildGreeting(context),
+ _buildGreeting(context, db, ext, reduceAnim),
const SizedBox(height: AppSpacing.lg),
- _buildTodayRecommend(context, ref),
+ _buildTodayRecommend(context, ref, db, ext),
const SizedBox(height: AppSpacing.lg),
- _buildQuickActions(context),
+ _buildQuickActions(context, db, ext, reduceAnim),
const SizedBox(height: AppSpacing.lg),
- _buildRecentHistory(context, ref),
+ _buildRecentHistory(context, ref, db, ext, reduceAnim),
const SizedBox(height: AppSpacing.lg),
- _buildStats(context, ref),
+ _buildStats(context, ref, db, ext, reduceAnim),
const SizedBox(height: AppSpacing.xxl),
],
),
);
}
- Widget _buildGreeting(BuildContext context) {
- final ext = AppTheme.ext(context);
- final hour = DateTime.now().hour;
- final greeting = switch (hour) {
- >= 6 && < 12 => '早上好',
- >= 12 && < 14 => '中午好',
- >= 14 && < 18 => '下午好',
- >= 18 && < 22 => '晚上好',
- _ => '夜深了',
+ // ============================================================
+ // 问候区
+ // ============================================================
+
+ Widget _buildGreeting(BuildContext context, TDashboard db, AppThemeExtension ext, bool reduceAnim) {
+ // 时段问候语 + 图标(基于 _currentHour,跨时段自动刷新)
+ final greeting = switch (_currentHour) {
+ >= 6 && < 12 => db.goodMorning,
+ >= 12 && < 14 => db.goodNoon,
+ >= 14 && < 18 => db.goodAfternoon,
+ >= 18 && < 22 => db.goodEvening,
+ _ => db.goodNight,
};
- final greetingIcon = switch (hour) {
+ final greetingIcon = switch (_currentHour) {
>= 6 && < 12 => CupertinoIcons.sun_max,
>= 12 && < 14 => CupertinoIcons.cloud_sun,
>= 14 && < 18 => CupertinoIcons.sunset,
@@ -73,9 +126,12 @@ class OverviewDashboard extends ConsumerWidget {
padding: const EdgeInsets.all(AppSpacing.lg),
child: Row(
children: [
-
-
- AppIcon(cupertinoIcon: greetingIcon, size: 28, animate: true, semanticLabel: greeting),
+ AppIcon(
+ cupertinoIcon: greetingIcon,
+ size: 28,
+ animate: !reduceAnim,
+ semanticLabel: greeting,
+ ),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Column(
@@ -84,15 +140,19 @@ class OverviewDashboard extends ConsumerWidget {
Text(
greeting,
style: TextStyle(
- fontSize: 22,
+ fontSize: 22 * ext.fontScale,
fontWeight: FontWeight.bold,
color: ext.textPrimary,
),
),
const SizedBox(height: AppSpacing.xs),
Text(
- '选择左侧内容查看详情',
- style: TextStyle(fontSize: 14, color: ext.textSecondary),
+ db.greetingHint,
+ style: TextStyle(
+ fontSize: 14 * ext.fontScale,
+ fontWeight: ext.fontWeight,
+ color: ext.textSecondary,
+ ),
),
],
),
@@ -102,117 +162,129 @@ class OverviewDashboard extends ConsumerWidget {
);
}
- Widget _buildTodayRecommend(BuildContext context, WidgetRef ref) {
- final ext = AppTheme.ext(context);
+ // ============================================================
+ // 今日推荐
+ // ============================================================
+
+ Widget _buildTodayRecommend(
+ BuildContext context,
+ WidgetRef ref,
+ TDashboard db,
+ AppThemeExtension ext,
+ ) {
final homeState = ref.watch(homeProvider);
final recommends = homeState.dailySentences;
+ final isLoading = homeState.isLoading;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Row(
- children: [
- const AppIcon(cupertinoIcon: CupertinoIcons.sparkles, size: 16, style: AppIconStyle.accent, semanticLabel: '推荐'),
- const SizedBox(width: 6),
- Text(
- '今日推荐',
- style: TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.w600,
- color: ext.textPrimary,
- ),
- ),
- ],
- ),
+ _buildSectionHeader(context, CupertinoIcons.sparkles, db.sectionRecommend, ext),
const SizedBox(height: AppSpacing.sm),
SizedBox(
height: 120,
- child: recommends.isEmpty
- ? GlassContainer(
- padding: const EdgeInsets.all(AppSpacing.md),
- child: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(CupertinoIcons.tray, size: 28, color: ext.textHint),
- const SizedBox(height: AppSpacing.xs),
- Text(
- '暂无推荐内容',
- style: TextStyle(fontSize: 13, color: ext.textHint),
- ),
- ],
+ // 加载中且无数据 → 骨架屏;无数据 → 空状态;有数据 → 列表
+ child: isLoading && recommends.isEmpty
+ ? _buildRecommendSkeleton(ext)
+ : recommends.isEmpty
+ ? _buildEmptyState(context, CupertinoIcons.tray, db.emptyRecommend, ext)
+ : ListView.separated(
+ scrollDirection: Axis.horizontal,
+ itemCount: recommends.length,
+ separatorBuilder: (_, __) => const SizedBox(width: AppSpacing.sm),
+ itemBuilder: (context, index) {
+ final sentence = recommends[index];
+ final author = sentence.author != null && sentence.author!.isNotEmpty
+ ? '$db.authorPrefix${sentence.author}'
+ : '$db.authorPrefix${db.anonymousAuthor}';
+ return GlassContainer(
+ width: 180,
+ padding: const EdgeInsets.all(AppSpacing.md),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ sentence.text,
+ style: TextStyle(
+ fontSize: 13 * ext.fontScale,
+ fontWeight: ext.fontWeight,
+ color: ext.textPrimary,
+ ),
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ ),
+ const Spacer(),
+ Text(
+ author,
+ style: TextStyle(
+ fontSize: 11 * ext.fontScale,
+ color: ext.textHint,
+ ),
+ ),
+ ],
+ ),
+ );
+ },
),
- ),
- )
- : ListView.separated(
- scrollDirection: Axis.horizontal,
- itemCount: recommends.length,
- separatorBuilder: (_, __) =>
- const SizedBox(width: AppSpacing.sm),
- itemBuilder: (context, index) {
- final sentence = recommends[index];
- return GlassContainer(
- width: 180,
- padding: const EdgeInsets.all(AppSpacing.md),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- sentence.text,
- style: TextStyle(
- fontSize: 13,
- color: ext.textPrimary,
- ),
- maxLines: 2,
- overflow: TextOverflow.ellipsis,
- ),
- const Spacer(),
- Text(
- sentence.author != null
- ? '—— ${sentence.author}'
- : '—— 佚名',
- style: TextStyle(fontSize: 11, color: ext.textHint),
- ),
- ],
- ),
- );
- },
- ),
),
],
);
}
- Widget _buildQuickActions(BuildContext context) {
- final ext = AppTheme.ext(context);
+ /// 今日推荐骨架屏 — 加载态占位
+ Widget _buildRecommendSkeleton(AppThemeExtension ext) {
+ return Shimmer.fromColors(
+ baseColor: ext.isDark ? ext.bgCard : ext.bgSecondary,
+ highlightColor: ext.isDark ? ext.bgElevated : ext.bgPrimary,
+ child: ListView.separated(
+ scrollDirection: Axis.horizontal,
+ physics: const NeverScrollableScrollPhysics(),
+ itemCount: 3,
+ separatorBuilder: (_, __) => const SizedBox(width: AppSpacing.sm),
+ itemBuilder: (_, __) => GlassContainer(
+ width: 180,
+ padding: const EdgeInsets.all(AppSpacing.md),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _skeletonBlock(ext, double.infinity, 12),
+ const SizedBox(height: AppSpacing.xs),
+ _skeletonBlock(ext, 120, 12),
+ const Spacer(),
+ _skeletonBlock(ext, 80, 10),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ // ============================================================
+ // 快捷操作
+ // ============================================================
+
+ Widget _buildQuickActions(
+ BuildContext context,
+ TDashboard db,
+ AppThemeExtension ext,
+ bool reduceAnim,
+ ) {
final actions = <({IconData icon, String label, String route})>[
- (icon: CupertinoIcons.search, label: '搜索', route: AppRoutes.search),
- (icon: CupertinoIcons.star, label: '收藏', route: AppRoutes.favorites),
- (icon: CupertinoIcons.book, label: '稍后读', route: AppRoutes.readLater),
- (icon: CupertinoIcons.clock, label: '历史', route: AppRoutes.history),
- (icon: CupertinoIcons.checkmark_seal, label: '签到', route: AppRoutes.signin),
- (icon: CupertinoIcons.chart_bar, label: '使用报告', route: AppRoutes.readingReport),
- (icon: CupertinoIcons.cloud_sun, label: '每日推荐', route: AppRoutes.dailyCard),
- (icon: CupertinoIcons.settings, label: '设置', route: AppRoutes.generalSettings),
+ (icon: CupertinoIcons.search, label: db.actionSearch, route: AppRoutes.search),
+ (icon: CupertinoIcons.star, label: db.actionFavorites, route: AppRoutes.favorites),
+ (icon: CupertinoIcons.book, label: db.actionReadLater, route: AppRoutes.readLater),
+ (icon: CupertinoIcons.clock, label: db.actionHistory, route: AppRoutes.history),
+ (icon: CupertinoIcons.checkmark_seal, label: db.actionSignin, route: AppRoutes.signin),
+ (icon: CupertinoIcons.chart_bar, label: db.actionReadingReport, route: AppRoutes.readingReport),
+ (icon: CupertinoIcons.cloud_sun, label: db.actionDailyCard, route: AppRoutes.dailyCard),
+ (icon: CupertinoIcons.settings, label: db.actionSettings, route: AppRoutes.generalSettings),
];
+ final animDuration = reduceAnim ? Duration.zero : const Duration(milliseconds: 375);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Row(
- children: [
- const AppIcon(cupertinoIcon: CupertinoIcons.bolt, size: 16, style: AppIconStyle.accent, semanticLabel: '快捷操作'),
- const SizedBox(width: 6),
- Text(
- '快捷操作',
- style: TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.w600,
- color: ext.textPrimary,
- ),
- ),
- ],
- ),
+ _buildSectionHeader(context, CupertinoIcons.bolt, db.sectionQuickActions, ext),
const SizedBox(height: AppSpacing.sm),
AnimationLimiter(
child: Wrap(
@@ -221,12 +293,15 @@ class OverviewDashboard extends ConsumerWidget {
children: actions.asMap().entries.map((entry) {
return AnimationConfiguration.staggeredList(
position: entry.key,
- duration: const Duration(milliseconds: 375),
+ duration: animDuration,
child: SlideAnimation(
- verticalOffset: 50.0,
+ verticalOffset: reduceAnim ? 0 : 50.0,
child: FadeInAnimation(
child: GestureDetector(
- onTap: () => context.appPush(entry.value.route),
+ onTap: () {
+ HapticFeedback.selectionClick();
+ context.appPush(entry.value.route);
+ },
child: GlassContainer(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
@@ -235,12 +310,21 @@ class OverviewDashboard extends ConsumerWidget {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
- AppIcon(cupertinoIcon: entry.value.icon, size: 16, animate: true, animationDelay: entry.key * 50, semanticLabel: entry.value.label),
+ AppIcon(
+ cupertinoIcon: entry.value.icon,
+ size: 16,
+ animate: !reduceAnim,
+ animationDelay: entry.key * 50,
+ semanticLabel: entry.value.label,
+ ),
const SizedBox(width: AppSpacing.xs),
Text(
entry.value.label,
style: TextStyle(
- fontSize: 13, color: ext.textPrimary),
+ fontSize: 13 * ext.fontScale,
+ fontWeight: ext.fontWeight,
+ color: ext.textPrimary,
+ ),
),
],
),
@@ -256,45 +340,28 @@ class OverviewDashboard extends ConsumerWidget {
);
}
- Widget _buildRecentHistory(BuildContext context, WidgetRef ref) {
- final ext = AppTheme.ext(context);
+ // ============================================================
+ // 最近浏览
+ // ============================================================
+
+ Widget _buildRecentHistory(
+ BuildContext context,
+ WidgetRef ref,
+ TDashboard db,
+ AppThemeExtension ext,
+ bool reduceAnim,
+ ) {
ref.watch(toolCenterRecentProvider);
final recentRoutes = RecentRouteService.getRecentRoutes();
+ final animDuration = reduceAnim ? Duration.zero : const Duration(milliseconds: 375);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Row(
- children: [
- const AppIcon(cupertinoIcon: CupertinoIcons.clock, size: 16, style: AppIconStyle.accent, semanticLabel: '最近浏览'),
- const SizedBox(width: 6),
- Text(
- '最近浏览',
- style: TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.w600,
- color: ext.textPrimary,
- ),
- ),
- ],
- ),
+ _buildSectionHeader(context, CupertinoIcons.clock, db.sectionRecent, ext),
const SizedBox(height: AppSpacing.sm),
recentRoutes.isEmpty
- ? GlassContainer(
- padding: const EdgeInsets.all(AppSpacing.md),
- child: Center(
- child: Column(
- children: [
- Icon(CupertinoIcons.tray, size: 32, color: ext.textHint),
- const SizedBox(height: AppSpacing.sm),
- Text(
- '暂无浏览记录',
- style: TextStyle(fontSize: 13, color: ext.textHint),
- ),
- ],
- ),
- ),
- )
+ ? _buildEmptyState(context, CupertinoIcons.tray, db.emptyRecent, ext)
: AnimationLimiter(
child: Column(
children: recentRoutes.take(6).toList().asMap().entries.map((entry) {
@@ -303,14 +370,17 @@ class OverviewDashboard extends ConsumerWidget {
final String name = ToolCenterIconMap.getName(route);
return AnimationConfiguration.staggeredList(
position: entry.key,
- duration: const Duration(milliseconds: 375),
+ duration: animDuration,
child: SlideAnimation(
- verticalOffset: 30.0,
+ verticalOffset: reduceAnim ? 0 : 30.0,
child: FadeInAnimation(
child: Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.xs),
child: GestureDetector(
- onTap: () => context.appPush(route),
+ onTap: () {
+ HapticFeedback.selectionClick();
+ context.appPush(route);
+ },
child: GlassContainer(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
@@ -324,7 +394,8 @@ class OverviewDashboard extends ConsumerWidget {
child: Text(
name,
style: TextStyle(
- fontSize: 14,
+ fontSize: 14 * ext.fontScale,
+ fontWeight: ext.fontWeight,
color: ext.textPrimary,
),
),
@@ -349,42 +420,41 @@ class OverviewDashboard extends ConsumerWidget {
);
}
- Widget _buildStats(BuildContext context, WidgetRef ref) {
- final ext = AppTheme.ext(context);
+ // ============================================================
+ // 数据统计
+ // ============================================================
+
+ Widget _buildStats(
+ BuildContext context,
+ WidgetRef ref,
+ TDashboard db,
+ AppThemeExtension ext,
+ bool reduceAnim,
+ ) {
final authState = ref.watch(authProvider);
final favoriteState = ref.watch(favoriteProvider);
final likesState = ref.watch(likesProvider);
final signinState = ref.watch(signinProvider);
+ // 空指针防护 — provider 异常或字段缺失时回退 0
final readCount = authState.user?.signinDays ?? 0;
final favCount = favoriteState.total;
final likeCount = likesState.total;
final streakDays = signinState.continuous;
+ final streakText = '$streakDays${db.streakDayUnit}';
- final stats = [
- (CupertinoIcons.book, '阅读', '$readCount'),
- (CupertinoIcons.star, '收藏', '$favCount'),
- (CupertinoIcons.hand_thumbsup, '点赞', '$likeCount'),
- (CupertinoIcons.flame, '连续', '$streakDays天'),
+ final stats = <(IconData, String, String)>[
+ (CupertinoIcons.book, db.statRead, '$readCount'),
+ (CupertinoIcons.star, db.statFavorites, '$favCount'),
+ (CupertinoIcons.hand_thumbsup, db.statLikes, '$likeCount'),
+ (CupertinoIcons.flame, db.statStreak, streakText),
];
+ final animDuration = reduceAnim ? Duration.zero : const Duration(milliseconds: 375);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Row(
- children: [
- const AppIcon(cupertinoIcon: CupertinoIcons.chart_bar, size: 16, style: AppIconStyle.accent, semanticLabel: '数据统计'),
- const SizedBox(width: 6),
- Text(
- '数据统计',
- style: TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.w600,
- color: ext.textPrimary,
- ),
- ),
- ],
- ),
+ _buildSectionHeader(context, CupertinoIcons.chart_bar, db.sectionStats, ext),
const SizedBox(height: AppSpacing.sm),
AnimationLimiter(
child: Row(
@@ -394,9 +464,9 @@ class OverviewDashboard extends ConsumerWidget {
return Expanded(
child: AnimationConfiguration.staggeredList(
position: entry.key,
- duration: const Duration(milliseconds: 375),
+ duration: animDuration,
child: SlideAnimation(
- verticalOffset: 50.0,
+ verticalOffset: reduceAnim ? 0 : 50.0,
child: FadeInAnimation(
child: GlassContainer(
padding: const EdgeInsets.all(AppSpacing.sm),
@@ -405,19 +475,29 @@ class OverviewDashboard extends ConsumerWidget {
),
child: Column(
children: [
- AppIcon(cupertinoIcon: stat.$1, style: AppIconStyle.accent, animate: true, animationType: AppIconAnimation.bounceIn, animationDelay: entry.key * 80, semanticLabel: stat.$2),
+ AppIcon(
+ cupertinoIcon: stat.$1,
+ style: AppIconStyle.accent,
+ animate: !reduceAnim,
+ animationType: AppIconAnimation.bounceIn,
+ animationDelay: entry.key * 80,
+ semanticLabel: stat.$2,
+ ),
const SizedBox(height: 4),
Text(
stat.$3,
style: TextStyle(
- fontSize: 16,
+ fontSize: 16 * ext.fontScale,
fontWeight: FontWeight.bold,
color: ext.textPrimary,
),
),
Text(
stat.$2,
- style: TextStyle(fontSize: 11, color: ext.textHint),
+ style: TextStyle(
+ fontSize: 11 * ext.fontScale,
+ color: ext.textHint,
+ ),
),
],
),
@@ -432,4 +512,76 @@ class OverviewDashboard extends ConsumerWidget {
],
);
}
+
+ // ============================================================
+ // 通用子组件
+ // ============================================================
+
+ /// 区块标题 — 图标 + 标题文字,统一强调色图标
+ Widget _buildSectionHeader(
+ BuildContext context,
+ IconData icon,
+ String title,
+ AppThemeExtension ext,
+ ) {
+ return Row(
+ children: [
+ AppIcon(
+ cupertinoIcon: icon,
+ size: 16,
+ style: AppIconStyle.accent,
+ semanticLabel: title,
+ ),
+ const SizedBox(width: 6),
+ Text(
+ title,
+ style: TextStyle(
+ fontSize: 16 * ext.fontScale,
+ fontWeight: FontWeight.w600,
+ color: ext.textPrimary,
+ ),
+ ),
+ ],
+ );
+ }
+
+ /// 空状态占位 — 图标 + 提示文案
+ Widget _buildEmptyState(
+ BuildContext context,
+ IconData icon,
+ String message,
+ AppThemeExtension ext,
+ ) {
+ return GlassContainer(
+ padding: const EdgeInsets.all(AppSpacing.md),
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(icon, size: 28, color: ext.textHint),
+ const SizedBox(height: AppSpacing.xs),
+ Text(
+ message,
+ style: TextStyle(
+ fontSize: 13 * ext.fontScale,
+ color: ext.textHint,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ /// 骨架屏色块 — 圆角灰色占位
+ Widget _skeletonBlock(AppThemeExtension ext, double width, double height) {
+ return Container(
+ width: width,
+ height: height,
+ decoration: BoxDecoration(
+ color: ext.textPrimary,
+ borderRadius: BorderRadius.circular(4),
+ ),
+ );
+ }
}
diff --git a/lib/core/services/auth/permission_service.dart b/lib/core/services/auth/permission_service.dart
index 5b5dc367..c002f4c5 100644
--- a/lib/core/services/auth/permission_service.dart
+++ b/lib/core/services/auth/permission_service.dart
@@ -1,9 +1,16 @@
/// ============================================================
/// 闲言APP — 权限管理服务
/// 创建时间: 2026-04-23
-/// 更新时间: 2026-06-19
-/// 作用: 统一管理应用权限请求,支持相机/相册/通知/位置/蓝牙/附近设备/麦克风/存储/网络/剪贴板/分享权限 + iOS ATT授权
-/// 上次更新: 类型安全修复(int vs num): 权限使用计数使用 SafeJson.parseInt
+/// 更新时间: 2026-06-26
+/// 作用: 统一管理应用权限请求,支持相机/相册/通知/位置/蓝牙/附近设备/麦克风/存储/网络/剪贴板/分享/本地服务器权限 + iOS ATT授权
+/// 上次更新: 1. macOS 端权限改为动态申请(原生 MethodChannel):
+/// 新增 macOS 权限名称映射(camera/microphone/photos/notification),
+/// checkStatus 调用 MacosPlatformService.checkPermission 查询 TCC 状态,
+/// requestPermission 调用 MacosPlatformService.requestPermission 触发系统弹窗,
+/// openSettings 调用 MacosPlatformService.openPermissionSettings 跳转系统设置。
+/// 替代之前"直接返回 granted"的简化方案,让用户能在权限管理页面主动触发授权。
+/// 2. 保留 localServer 虚拟权限、isPlatformRelevant 全平台适配、
+/// MissingPluginException 兜底等既有逻辑。
/// ============================================================
import 'dart:async';
@@ -18,6 +25,7 @@ import 'package:permission_handler/permission_handler.dart';
import '../../storage/kv_storage.dart';
import '../../utils/platform/platform_utils.dart' as pu;
+import '../device/macos_platform_service.dart';
import '../../../l10n/translation_resolver.dart';
import '../../../l10n/types/t_settings_permission.dart';
@@ -141,6 +149,15 @@ enum AppPermission {
isVirtual: true,
group: PermissionGroup.system,
),
+ // 本地服务器 — LocalSend 局域网文件传输(macOS/Windows/Linux 桌面端)
+ // 对应 macOS 的 com.apple.security.network.server entitlement
+ localServer(
+ Permission.notification,
+ CupertinoIcons.antenna_radiowaves_left_right,
+ Color(0xFF5856D6),
+ isVirtual: true,
+ group: PermissionGroup.system,
+ ),
tracking(
Permission.notification,
CupertinoIcons.eye_fill,
@@ -177,6 +194,7 @@ enum AppPermission {
AppPermission.network => t.permNetworkLabel,
AppPermission.clipboard => t.permClipboardLabel,
AppPermission.share => t.permShareLabel,
+ AppPermission.localServer => t.permLocalServerLabel,
AppPermission.tracking => t.permTrackingLabel,
};
}
@@ -195,6 +213,7 @@ enum AppPermission {
AppPermission.network => t.permNetworkDesc,
AppPermission.clipboard => t.permClipboardDesc,
AppPermission.share => t.permShareDesc,
+ AppPermission.localServer => t.permLocalServerDesc,
AppPermission.tracking => t.permTrackingDesc,
};
}
@@ -213,6 +232,7 @@ enum AppPermission {
AppPermission.network => t.permNetworkUsage,
AppPermission.clipboard => t.permClipboardUsage,
AppPermission.share => t.permShareUsage,
+ AppPermission.localServer => t.permLocalServerUsage,
AppPermission.tracking => t.permTrackingUsage,
};
if (raw.isEmpty) return const [];
@@ -233,13 +253,42 @@ enum AppPermission {
AppPermission.network => t.permNetworkDenial,
AppPermission.clipboard => t.permClipboardDenial,
AppPermission.share => t.permShareDenial,
+ AppPermission.localServer => t.permLocalServerDenial,
AppPermission.tracking => t.permTrackingDenial,
};
}
- /// Android 13+ 不需要 storage 权限(由 photos 替代)
- /// tracking 权限仅 iOS 展示
- /// 鸿蒙端:过滤 permission_handler 不支持或不需要的权限
+ /// macOS 原生权限名称映射
+ ///
+ /// 返回与原生 PermissionManager.swift 对应的权限名称字符串。
+ /// 返回 null 表示该权限在 macOS 上不支持原生申请(视为已授权)。
+ ///
+ /// 支持的权限:
+ /// - camera → "camera"(AVFoundation)
+ /// - microphone → "microphone"(AVFoundation)
+ /// - photos → "photos"(Photos framework)
+ /// - notification → "notification"(UserNotifications framework)
+ String? get macosPermissionName {
+ return switch (this) {
+ AppPermission.camera => 'camera',
+ AppPermission.microphone => 'microphone',
+ AppPermission.photos => 'photos',
+ AppPermission.notification => 'notification',
+ // macOS 不支持的权限返回 null(location/nearbyDevices/storage/tracking 等)
+ _ => null,
+ };
+ }
+
+ /// 权限平台相关性 — 控制权限管理页面展示哪些权限卡片
+ ///
+ /// 平台适配原则:
+ /// - storage: Android(非鸿蒙)SDK ≤ 32,由 photos 替代更高版本
+ /// - tracking: 仅 iOS(ATT)
+ /// - localServer: 仅桌面端(macOS/Windows/Linux),对应 LocalSend 文件传输本地服务器
+ /// - 鸿蒙端:过滤 permission_handler 不支持或不需要的权限
+ /// - 桌面端:macOS 有相册权限;Windows/Linux 用文件选择器;位置仅 IP 定位不用 GPS
+ /// - Web:浏览器权限模型,不能绑定本地端口
+ /// - 移动端(Android/iOS):除 storage/tracking/localServer 外默认展示
bool get isPlatformRelevant {
if (this == AppPermission.storage) {
if (!pu.isAndroid) return false;
@@ -249,6 +298,10 @@ enum AppPermission {
if (this == AppPermission.tracking) {
return Platform.isIOS;
}
+ // 本地服务器:仅桌面端运行(macOS/Windows/Linux)
+ if (this == AppPermission.localServer) {
+ return pu.isDesktop;
+ }
// 鸿蒙端:过滤不支持的权限,防止 permission_handler 挂起或卡死
if (pu.isOhos) {
return switch (this) {
@@ -262,12 +315,48 @@ enum AppPermission {
AppPermission.location => false, // 闲言APP鸿蒙端不需要定位权限
AppPermission.storage => false, // 鸿蒙端使用 READ_MEDIA/WRITE_MEDIA 替代
AppPermission.tracking => false, // 鸿蒙端不支持tracking权限
+ AppPermission.localServer => false, // 鸿蒙端不运行本地服务器
// 虚拟权限
AppPermission.network => true,
AppPermission.clipboard => true,
AppPermission.share => true,
};
}
+ // 桌面端(macOS/Windows/Linux)
+ if (pu.isDesktop) {
+ return switch (this) {
+ AppPermission.camera => true, // 扫码/拍照/OCR(macOS 已声明 entitlement)
+ AppPermission.microphone => true, // 语音录制/转文字(macOS 已声明 entitlement)
+ AppPermission.notification => true,
+ AppPermission.photos => pu.isMacOS, // macOS 有相册权限;Windows/Linux 用文件选择器
+ AppPermission.location => false, // 仅 IP 定位,不用 GPS
+ AppPermission.nearbyDevices => false, // 桌面端不用 nearbyWifiDevices
+ AppPermission.storage => false,
+ AppPermission.tracking => false,
+ AppPermission.localServer => true, // LocalSend 文件传输本地服务器
+ AppPermission.network => true,
+ AppPermission.clipboard => true,
+ AppPermission.share => true,
+ };
+ }
+ // Web:浏览器权限模型
+ if (pu.isWeb) {
+ return switch (this) {
+ AppPermission.camera => true, // getUserMedia
+ AppPermission.microphone => true,
+ AppPermission.notification => true,
+ AppPermission.photos => false, // Web 用文件输入
+ AppPermission.location => false, // 不用 GPS
+ AppPermission.nearbyDevices => false,
+ AppPermission.storage => false,
+ AppPermission.tracking => false,
+ AppPermission.localServer => false, // Web 不能绑定本地端口
+ AppPermission.network => true,
+ AppPermission.clipboard => true,
+ AppPermission.share => true,
+ };
+ }
+ // 移动端默认(Android/iOS):除已单独处理的 storage/tracking/localServer 外均展示
return true;
}
@@ -405,6 +494,18 @@ class PermissionService {
if (perm == AppPermission.tracking) {
return _checkTrackingStatus();
}
+ // macOS: 通过原生 MethodChannel 查询 TCC 权限状态
+ // permission_handler_apple 9.4.9 仅支持 iOS,无 macOS 实现,
+ // 改用 MacosPlatformService 调用原生 PermissionManager(AVFoundation/Photos/UserNotifications)
+ if (pu.isMacOS) {
+ final macosName = perm.macosPermissionName;
+ if (macosName == null) {
+ // macOS 不支持的权限(如 location/nearbyDevices/storage)视为已授权
+ return AppPermissionStatus.granted;
+ }
+ final statusStr = await MacosPlatformService.checkPermission(macosName);
+ return _mapMacosStatus(statusStr);
+ }
try {
// 鸿蒙端添加超时保护,防止 permission_handler 挂起
if (pu.isOhos) {
@@ -416,6 +517,11 @@ class PermissionService {
}
final status = await perm.permission.status;
return AppPermissionStatus.fromPermissionStatus(status);
+ } on MissingPluginException {
+ // 平台未实现 permission_handler(如 Linux 桌面端)—
+ // 桌面端通常无系统级权限管理,视为已授权
+ _log.w('${perm.name} 在当前平台无 permission_handler 实现,视为已授权');
+ return AppPermissionStatus.granted;
} catch (e) {
_log.e('权限状态查询异常', e);
return AppPermissionStatus.notDetermined;
@@ -456,6 +562,84 @@ class PermissionService {
}
}
+ /// macOS 权限请求(通过原生 MethodChannel)
+ ///
+ /// 调用 MacosPlatformService.requestPermission 触发系统 TCC 弹窗。
+ /// - 首次请求:系统弹出授权对话框
+ /// - 已授权:直接返回 true
+ /// - 已拒绝(permanentlyDenied):引导用户去系统设置
+ /// - notDetermined:先显示说明对话框,用户确认后触发系统弹窗
+ static Future _requestMacosPermission(
+ BuildContext context,
+ AppPermission perm, {
+ String? rationale,
+ }) async {
+ final macosName = perm.macosPermissionName;
+ if (macosName == null) {
+ // macOS 不支持的权限视为已授权
+ return true;
+ }
+
+ // 先查询当前状态
+ final statusStr = await MacosPlatformService.checkPermission(macosName);
+ final status = _mapMacosStatus(statusStr);
+
+ if (status == AppPermissionStatus.granted) {
+ _log.i('✅ macOS ${perm.name} 权限已授权');
+ return true;
+ }
+
+ if (status == AppPermissionStatus.permanentlyDenied) {
+ _log.w('⚠️ macOS ${perm.name} 权限已被拒绝,引导用户前往系统设置');
+ if (context.mounted) {
+ _showSettingsDialog(context, perm);
+ }
+ return false;
+ }
+
+ // notDetermined 状态:先显示说明对话框,用户确认后触发系统弹窗
+ if (context.mounted) {
+ final userConfirmed = await _showRationaleDialog(
+ context,
+ perm,
+ rationale: rationale,
+ );
+ if (!userConfirmed) return false;
+ }
+
+ // 调用原生权限请求(触发系统 TCC 弹窗)
+ final resultStr = await MacosPlatformService.requestPermission(macosName);
+ final result = _mapMacosStatus(resultStr);
+
+ if (result == AppPermissionStatus.granted) {
+ _log.i('✅ macOS ${perm.name} 权限已授予');
+ return true;
+ }
+
+ _log.w('⚠️ macOS ${perm.name} 权限请求失败: $resultStr');
+ if (context.mounted) {
+ _showSettingsDialog(context, perm);
+ }
+ return false;
+ }
+
+ /// 将 macOS 原生权限状态字符串映射为 AppPermissionStatus
+ ///
+ /// 原生 PermissionManager 返回的状态字符串:
+ /// - "notDetermined" → 未决定(可请求)
+ /// - "granted" → 已授权
+ /// - "permanentlyDenied" → 已拒绝(macOS 不会再次弹窗)
+ /// - "restricted" → 受限(如家长控制)
+ static AppPermissionStatus _mapMacosStatus(String status) {
+ return switch (status) {
+ 'granted' => AppPermissionStatus.granted,
+ 'permanentlyDenied' => AppPermissionStatus.permanentlyDenied,
+ 'restricted' => AppPermissionStatus.restricted,
+ 'notDetermined' => AppPermissionStatus.notDetermined,
+ _ => AppPermissionStatus.notDetermined,
+ };
+ }
+
/// 批量查询所有权限状态(过滤平台不相关权限)
static Future