diff --git a/CHANGELOG.md b/CHANGELOG.md index 30b870da..d8a75dfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ## Unreleased -- **2026-07-28 至 2026-07-29,8 月 1 日 Android 受控内测候选包与法务链路,当前总体 No-Go**:补齐登录页协议默认不勾选与法务地址失败关闭、App-Plus 原生隐私配置生成/校验、APK 包名/版本/API/签名/哈希/包内资源一致性/敏感值门禁、DCloud 云证书自定义基座和独立测试包流程;《服务协议》《隐私政策》静态页已部署到 `/legal/`,发布前整站备份为 `/opt/wygj/backups/www-20260728224710`。客户端法务校验已移除 App-Plus 不保证支持的浏览器 `URL` 构造器,测试验证码也不再以源码常量进入安装包。当前独立 APK `com.yincheng.wygj 0.1.6 (106)` 的 `targetSdkVersion=35`,SHA-256 为 `A5F026B70F11327355597B9FEA3625C4168700297E6D7847F10B079CEB860914`;vivo V2443A(Android 15)已完成覆盖安装和冷启动,本机 `Medium_Phone` Android 15/API 35 虚拟机已完成最新包干净安装和冷启动,真实显示原生隐私弹窗,四项敏感权限保持未授权且未见崩溃。移动端 191 项测试、类型检查、H5/App 构建、管理端构建、P0 后端 39 项定向测试及 `ruoyi-aihr` 696 项全量测试通过;跨平台源码契约测试已统一换行符后复跑全绿。本次评估时工作区尚未提交,生产后端未包含本批 P0 回退语义,用户隐私选择、权限反例、认证态核心业务、正式内容和完整发布预检仍未完成,故 Go/No-Go 为 **No-Go**;本次除法务静态页外未发布后端、H5 业务资源或数据库。 +- **2026-07-28 至 2026-07-29,8 月 1 日 Android 受控内测候选包与法务链路,当前总体 No-Go**:补齐登录页协议默认不勾选与法务地址失败关闭、App-Plus 原生隐私配置生成/校验、APK 包名/版本/API/签名/哈希/包内资源一致性/敏感值门禁、DCloud 云证书自定义基座和独立测试包流程;《服务协议》《隐私政策》静态页已部署到 `/legal/`,发布前整站备份为 `/opt/wygj/backups/www-20260728224710`。客户端法务校验已移除 App-Plus 不保证支持的浏览器 `URL` 构造器,测试验证码也不再以源码常量进入安装包。当前独立 APK `com.yincheng.wygj 0.1.8 (108)` 的 `targetSdkVersion=35`,SHA-256 为 `E252140645F3AAD14B3594FD78E76BDF1A2122F7DC70EBD21CC5DF53223C7878`;新增 Android 原生网络安全配置并经 APK 反编译确认 `usesCleartextTraffic=false`、`networkSecurityConfig` 引用和系统证书信任配置均已进入安装包。本机 `Medium_Phone` Android 15/API 35 虚拟机已完成最新包干净安装和冷启动,真实显示原生隐私弹窗,四项敏感权限保持未授权且未见崩溃;`0.1.6` 已在 vivo V2443A(Android 15)完成覆盖安装和冷启动。移动端 193 项测试、类型检查和 App 构建预检通过;前一冻结批次的 H5/管理端构建及 `ruoyi-aihr` 696 项全量测试通过。生产后端仍未包含本批 P0 回退语义,用户隐私选择、权限反例、认证态核心业务、正式内容和完整发布预检仍未完成,故 Go/No-Go 为 **No-Go**;本次除法务静态页外未发布后端、H5 业务资源或数据库。 - **2026-07-26,阿里云百炼 ASR 生产热修复**:`POST /api/ai/asr` 新增 `dashscope/qianwen + qwen3-asr-flash` 的 `/compatible-mode/v1/chat/completions` + Base64 `input_audio` 分支,原 SiliconFlow `/audio/transcriptions` multipart 路径继续兼容,ASR 超时放宽至 30 秒。定向 Maven 测试 10 项通过;后端 JAR(SHA-256 `46c99cff75d21f8536a71c3c058b6437e99dbf9ff4bae3542b44471f3f34ad84`)已发布,生产 ASR 已切换至 `dashscope/qwen3-asr-flash`,旧 `siliconflow/TeleAI/TeleSpeechASR` 已停用。生产接口获得正确转写与受控 `ossId`,Android HBuilderX 标准基座“按住说话”请求成功,线上日志无 403/ASR 异常。回滚目录为 `/opt/wygj/backups/asr-hotfix-20260726210122`;本次未发布静态资源、未变更 schema,也不代表完整包一致性或原生 App 全项验收。 - **2026-07-26,移动端编译链升级与 uni-ui 解析修复,未发布**:本机 HBuilderX GUI 已升级为 `5.22.2026071707`,CLI 为 `5.22.2026072503-alpha`;`mobile-uni` 的 App、App-Plus、H5 与 Vite 编译包统一到对应的 `3.0.0-alpha-5020220260725001` 批次,消除 5.15/5.21 混用产生的两套 `uni-cli-shared` 状态隔离,使登录页 ``、`` 等组件重新由 `pages.json` easycom 规则正常解析。H5 与 App 均以 `Compiler version: 5.22(vue3)` 构建通过;浏览器和 vivo V2443A(Android 15)真机均确认手机号、验证码标签及输入框正常。HBuilder 5.22 标准基座 `versionName=15.22` 已自动更新并成功运行,测试手机号配合固定验证码登录成功,可进入员工首页并切换“问”页,未出现编译器/SDK 版本不匹配弹窗或组件解析告警。升级后仍须重建并安装 5.22 自定义运行基座完成原生权限全清单;`verify:app-release` 当前因缺少含正式法务 HTTPS 链接的 `androidPrivacy.json` 按预期阻断。本项未发布生产 H5,也未云打包 APK/IPA,手机上的 `com.yincheng.wygj` 正式包未被覆盖。 - **2026-07-26,全网 AI 本地库修复,未发布**:补齐 `reset-dev-db.sh` 遗漏的全网 AI 基表、数据库密钥和千问字段三项正式迁移,并增加迁移顺序及幂等回归,避免重置本地库后再次缺失 `aihr_web_search_provider`;当前本地库为 137 张表,管理端代理下的提供方列表接口验证通过。 diff --git a/README.md b/README.md index 6177be3d..feb8575d 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ - 品牌:管理端与员工端统一显示“帮道”,继续使用现有红色品牌图标。 - 基础链路:租户列表、验证码、`000000 / admin / admin123` 加密登录已验证。 - 管理端:除 AI 面试、训练、案例、SOP 和资料处理外,已包含大喇叭发布、单文件提炼与多岗位解读、五渠道直通反馈收件箱、内容/任务运营、问题榜运营、知识空间、全网 AI 配置和组织权限;员工个人训练、主管复盘、团队难题、成长证据与训练录音仅在认证 APP 按项目范围访问。 -- 用户侧:`mobile-uni/` 承载员工、候选人和主管三端;“问”已改为数字师傅 Agent,文字、ASR 转写和图片/视频统一先做意图规划,再按服务端权限调用知识、训练、待办、记忆、媒体或全网工具,寒暄和不完整表达不再机械落入 RAG。现有能力还包括手机号登录、30 分钟短期多轮、原文件/视频受控交付、大喇叭公司文件摘要/下载/多岗位解读/追问、五渠道直通反馈、“记一下”确认卡、三级字号、训练/题目反馈、问题榜与积分、岗位考试、案例、候选面试/资料、主管复盘/派发及学员模式。Agent 代码已随 2026-07-25 完整包部署;2026-07-29 当前候选包为 `0.1.6 (106)`,已通过 API 35、法务链接、包内资源一致性和敏感值门禁,并在 Android 15 真机完成覆盖安装、在本机 Android 15/API 35 虚拟机完成干净安装和冷启动。最新包已真实显示原生隐私弹窗,四项敏感权限保持未授权且未见崩溃;用户亲自执行的隐私选择、权限正反例、认证态核心业务全回归、生产依赖匹配和发布签认仍未完成,当前 Go/No-Go 仍为 No-Go。旧 `mobile/` 只用于历史回溯。 +- 用户侧:`mobile-uni/` 承载员工、候选人和主管三端;“问”已改为数字师傅 Agent,文字、ASR 转写和图片/视频统一先做意图规划,再按服务端权限调用知识、训练、待办、记忆、媒体或全网工具,寒暄和不完整表达不再机械落入 RAG。现有能力还包括手机号登录、30 分钟短期多轮、原文件/视频受控交付、大喇叭公司文件摘要/下载/多岗位解读/追问、五渠道直通反馈、“记一下”确认卡、三级字号、训练/题目反馈、问题榜与积分、岗位考试、案例、候选面试/资料、主管复盘/派发及学员模式。Agent 代码已随 2026-07-25 完整包部署;2026-07-29 当前候选包为 `0.1.8 (108)`,已通过 API 35、法务链接、包内资源一致性、敏感值、禁止明文 HTTP 和网络安全资源门禁,并在本机 Android 15/API 35 虚拟机完成干净安装和冷启动。最新包已真实显示原生隐私弹窗,四项敏感权限保持未授权且未见崩溃;`0.1.6` 已在 Android 15 真机完成覆盖安装。用户亲自执行的隐私选择、权限正反例、认证态核心业务全回归、生产依赖匹配和发布签认仍未完成,当前 Go/No-Go 仍为 No-Go。旧 `mobile/` 只用于历史回溯。 - 英雄路径:AI 面试(真模型出题/评分)、三角色对练(真 LLM 客户回复+评分,ASR/TTS 配置后支持语音)、案例沉淀(真实语音上传+ASR 转写+模型整理)均为真模型优先、未配置或失败回退本地兜底;生产短音频 ASR 当前为阿里云百炼 `qwen3-asr-flash`,SiliconFlow audio 接口保留兼容。SOP 知识库已接中文关键词 + MySQL Fulltext + Qdrant 混合召回 + rerank 重排,支持 txt/md/PDF/Word/Excel/PPT/图片/视频上传解析入库(图片走 vision OCR,视频走 ffmpeg 抽音轨转写+关键帧描述),资料处理中心支持 ZIP 异步展开后逐文件入队;模型配置页可查看向量库维度/点数并一键重建。 - 资料处理:已新增解析任务状态页,按 `aihr_knowledge_attach` 和 `aihr_knowledge_fragment` 展示等待解析、解析中、已完成、失败、片段数和向量化状态;任务列表按当前筛选结果前端分页(10/20/50 条,筛选或刷新回到第一页)。页面只保留“批量导入”,通过异步队列处理多文件或 ZIP,不提供浏览器目录选择和服务端目录导入入口。后端目录导入接口仅保留为运维/调试能力。 - 组织人员:`aihr_org_snapshot` 本地 reset 保留演示 seed;生产通过 `POST /api/aihr/org/sync` 同步外部组织。2026-07-25 记录的生产快照为 3417 人、2938 人在职且均有姓名、3392 个手机号映射、175 名在职主管和 20 名项目经理;姓名通过稳定 `employee_id` 定向补齐,未执行全量覆盖。当前上游 `employee_number` 与既有主体不一致且手机号大量脱敏,修复契约前禁止强制全量同步;人数属于时点事实,发布或验收前应重新查询。 diff --git a/docs/MOBILE_APP_DEVICE_ACCEPTANCE.md b/docs/MOBILE_APP_DEVICE_ACCEPTANCE.md index 723ed515..53b27c5e 100644 --- a/docs/MOBILE_APP_DEVICE_ACCEPTANCE.md +++ b/docs/MOBILE_APP_DEVICE_ACCEPTANCE.md @@ -24,6 +24,8 @@ 同日,本机 Android Studio `Medium_Phone` 虚拟机完成 Android 15/API 35 启动,物理分辨率 `1080×2400`、密度 `420`。在虚拟机此前未安装本应用的前提下,`0.1.6 (106)` 首次安装成功并从系统 Launcher 冷启动,`am start -W` 记录总耗时约 `6.4s`;最新包真实显示原生《服务协议和隐私政策》弹窗,四项相机、录音、图片和视频权限均为 `granted=false`,crash 缓冲区为空。截图和 UI 层级保存在忽略目录 `output/aug1-rc/android-emulator/`;验收未代替用户点击同意或拒绝。虚拟机证据用于补充干净安装和 API 35 权限基线,不能替代真实设备的媒体、WebRTC、弱网和系统浏览器交互。 +同日,候选包继续升级为 `0.1.8 (108)`,大小 `33,921,705` 字节,SHA-256 为 `E252140645F3AAD14B3594FD78E76BDF1A2122F7DC70EBD21CC5DF53223C7878`。新增的项目根原生清单与 `network_security_config.xml` 已进入实际云打包输入,193 项移动端测试、类型检查、App 构建及联网法务预检通过;APK 反编译确认包内资源与当前编译产物一致、`usesCleartextTraffic=false`、`networkSecurityConfig` 引用、系统证书信任配置、签名和敏感值扫描均通过。虚拟机卸载旧实例后全新安装,Launcher 冷启动总耗时 `2.904s`,最新原生隐私弹窗真实显示,四项敏感权限均为 `granted=false`,crash 缓冲区为空。仍未代替用户点击拒绝或同意。 + ## 已完成的 Android 基座证据 - [x] 自定义基座包名、版本、API 35、单签名、v2 签名和 SHA-256 可重复校验。 @@ -33,7 +35,7 @@ - [x] 独立 DCloud 云证书内测 APK 的包内法务配置、API 35、单签名、v2 签名和 SHA-256 可重复校验。 - [x] 清除本 App 测试数据后首次启动先显示原生隐私弹窗,服务协议与隐私政策链接均可在系统浏览器打开。 - [x] `0.1.6 (106)` 当前候选包完成同签名覆盖安装、冷启动和启动权限复核,包内资源与当前 App-Plus 编译产物一致。 -- [x] `0.1.6 (106)` 在本机 Android 15/API 35 虚拟机完成干净安装和冷启动,最新包原生隐私弹窗真实显示,四项敏感权限保持未授权且未见崩溃。 +- [x] `0.1.8 (108)` 在本机 Android 15/API 35 虚拟机完成卸载重装和冷启动,最新包原生隐私弹窗真实显示,四项敏感权限保持未授权、网络安全配置已进入 APK 且未见崩溃。 ## Android(最低 Android 10 + 一台当前主流机型) diff --git a/docs/MOBILE_APP_PACKAGING.md b/docs/MOBILE_APP_PACKAGING.md index 06896160..33b68940 100644 --- a/docs/MOBILE_APP_PACKAGING.md +++ b/docs/MOBILE_APP_PACKAGING.md @@ -42,13 +42,14 @@ npm --prefix mobile-uni run build:app npm --prefix mobile-uni run verify:app-release ``` -`configure:app-privacy` 会同时校验 HTTPS、公网域名、两个地址互不相同,并从模板生成被 Git 忽略的 `mobile-uni/src/androidPrivacy.json`;`build:app` 再将其复制到 `dist/build/app/androidPrivacy.json`。这两个位置都纳入门禁,避免 npm/Vite CLI 项目把配置误写到工程根目录而产生假绿。`verify:app-release` 还会确认: +`configure:app-privacy` 会同时校验 HTTPS、公网域名、两个地址互不相同,并从模板生成被 Git 忽略的 `mobile-uni/src/androidPrivacy.json`;`build:app` 再将其复制到 `dist/build/app/androidPrivacy.json`,并把项目根的 `AndroidManifest.xml` 与 `nativeResources/android/res/xml/network_security_config.xml` 同步到云打包输入目录。源码和编译产物都纳入门禁,避免 npm/Vite CLI 项目把原生配置留在源码但未进入安装包而产生假绿。`verify:app-release` 还会确认: - 首次与二次隐私弹窗使用同一组地址; - 原生弹窗地址与本次 App 编译使用的 `VITE_APP_TERMS_URL`、`VITE_APP_PRIVACY_URL` 完全一致; - 两个页面可通过公网 HTTPS 访问并包含可识别的协议正文; - `VITE_DEV_SMS_HINT_ENABLED` 与 `VITE_DEV_SMS_HINT_VALUE` 均未进入发布环境; - 编译产物不含手机号、固定测试验证码、私钥或常见云端/API 令牌格式。 +- Android 清单与网络安全配置均禁止明文 HTTP,并只信任系统证书。 登录页默认不勾选协议。协议地址缺失或不合规时,页面允许查看但禁止发送验证码和登录;测试验证码快捷提示只有开发环境同时显式设置 `VITE_DEV_SMS_HINT_ENABLED=true` 和本机 `VITE_DEV_SMS_HINT_VALUE` 才显示。源码不再内置固定验证码值。 @@ -64,23 +65,23 @@ HBuilderX 5.22 当前实测中,自定义调试基座不支持安心打包, ```powershell & '.\mobile-uni\scripts\verify-android-apk.ps1' ` - -ApkPath '.\mobile-uni\dist\debug\bangdao-0.1.6-dcloud-test.apk' ` + -ApkPath '.\mobile-uni\dist\debug\bangdao-0.1.8-dcloud-test.apk' ` -BuildKind dcloud-test ``` -APK 门禁还会比较包内与当前 `dist/build/app/app-service.js` 的 SHA-256,避免用同版本号的旧包冒充当前源码,并在反编译结果中复查敏感文件、手机号、固定测试验证码、私钥和常见令牌格式。 +APK 门禁还会比较包内与当前 `dist/build/app/app-service.js` 的 SHA-256,避免用同版本号的旧包冒充当前源码,并在反编译结果中复查敏感文件、手机号、固定测试验证码、私钥和常见令牌格式;`dcloud-test` 与 `release` 模式都拒绝 `usesCleartextTraffic=true`。 完成代码评审并提交冻结后,从仓库根目录执行统一 RC 证据脚本。默认拒绝脏工作区;`-AllowDirtyRehearsal -SkipBuild` 只能用于验证脚本和 APK,生成的证据会明确标记为不可发布: ```powershell .\scripts\prepare-aug1-rc.ps1 ` - -ApkPath '.\mobile-uni\dist\debug\bangdao-0.1.6-dcloud-test.apk' ` + -ApkPath '.\mobile-uni\dist\debug\bangdao-0.1.8-dcloud-test.apk' ` -BuildKind dcloud-test ``` 脚本会复跑移动端测试/类型检查/H5/App 构建、管理端构建、后端定向测试和打包,验证 APK 后在被 Git 忽略的 `output/aug1-rc/` 生成标准文件名、哈希及 JSON/Markdown 证据。它不执行生产同步、数据库迁移、服务重启或用户隐私选择。 -2026-07-28 最终自定义基座大小为 `21,158,193` 字节,SHA-256 为 `82D3352FF96A86F5DB4AC0C71DCB0ED6CEC98F2A785F6EAED2FE8D4FC0C5F704`。2026-07-29 当前独立内测候选包为 `bangdao-0.1.6-dcloud-test.apk`:包名 `com.yincheng.wygj`、版本 `0.1.6 (106)`、`targetSdkVersion=35`,大小 `33,921,279` 字节,SHA-256 为 `A5F026B70F11327355597B9FEA3625C4168700297E6D7847F10B079CEB860914`,签名证书 SHA-256 为 `AB06ECC0DE544DE73C385F3F0690B22966EBC12887FB8BEE1830CDBEFDEBEF5D`。APK 反编译门禁已确认包内业务资源等于当前 App-Plus 编译产物,法务配置和敏感值扫描通过;同签名覆盖安装和冷启动成功,启动后敏感权限保持未授权且未见 `FATAL EXCEPTION`。前一候选包的干净启动已显示原生 template 隐私弹窗,两个链接均通过系统浏览器打开线上正文。该包使用 DCloud 云端证书,且 DCloud 运行壳仍声明允许明文流量,只能作为 8 月 1 日受控内测包;企业正式 release 门禁仍会拒绝明文流量和非企业签名。 +2026-07-28 最终自定义基座大小为 `21,158,193` 字节,SHA-256 为 `82D3352FF96A86F5DB4AC0C71DCB0ED6CEC98F2A785F6EAED2FE8D4FC0C5F704`。2026-07-29 当前独立内测候选包为 `bangdao-0.1.8-dcloud-test.apk`:包名 `com.yincheng.wygj`、版本 `0.1.8 (108)`、`targetSdkVersion=35`,大小 `33,921,705` 字节,SHA-256 为 `E252140645F3AAD14B3594FD78E76BDF1A2122F7DC70EBD21CC5DF53223C7878`,签名证书 SHA-256 为 `AB06ECC0DE544DE73C385F3F0690B22966EBC12887FB8BEE1830CDBEFDEBEF5D`。APK 反编译门禁已确认包内业务资源等于当前 App-Plus 编译产物,法务配置、敏感值扫描、`usesCleartextTraffic=false`、`networkSecurityConfig` 引用和系统证书信任配置均通过;本机 Android 15/API 35 虚拟机完成卸载重装、冷启动和权限基线复核,总启动耗时约 `2.9s`,四项敏感权限保持未授权且 crash 缓冲区为空。该包仍使用 DCloud 云端测试证书,只能作为 8 月 1 日受控内测包;企业正式 release 还必须使用签认后的企业证书摘要。 企业正式签名 APK 还必须用发布负责人保管的证书摘要执行 release 模式核验;该模式会额外拒绝 `debuggable=true`、明文 HTTP 和签名摘要不一致: diff --git a/docs/帮道8月1日Android内测Go-No-Go-20260728.md b/docs/帮道8月1日Android内测Go-No-Go-20260728.md index 34c165d7..c7976ce4 100644 --- a/docs/帮道8月1日Android内测Go-No-Go-20260728.md +++ b/docs/帮道8月1日Android内测Go-No-Go-20260728.md @@ -14,17 +14,17 @@ ## 已通过 -| 检查项 | 2026-07-28 结果 | +| 检查项 | 当前结果 | |---|---| | 仓库同步 | 本地 `main` 与 `origin/main` 均为 `9ae5f750`,前后差异 `0/0` | -| 移动端自动化 | 191 项单元测试通过;TypeScript 类型检查通过 | +| 移动端自动化 | 193 项单元测试通过;TypeScript 类型检查通过 | | 构建 | `mobile-uni` H5、App-Plus、管理端生产构建通过;后端 `ruoyi-admin.jar` 重建通过 | | P0 后端语义 | `AihrSopSeedServiceTest` 39 项通过;`ruoyi-aihr` 696 项全量测试通过,0 失败、0 错误 | | App 资源门禁 | 正式法务 URL、原生隐私配置、API 35、图标和启动图检查通过 | -| 独立 APK | `com.yincheng.wygj`,`0.1.6 (106)`,`targetSdkVersion=35`,v2 签名;SHA-256 `A5F026B70F11327355597B9FEA3625C4168700297E6D7847F10B079CEB860914` | +| 独立 APK | `com.yincheng.wygj`,`0.1.8 (108)`,`targetSdkVersion=35`,v2 签名,明文 HTTP 关闭且网络安全资源已进入 APK;SHA-256 `E252140645F3AAD14B3594FD78E76BDF1A2122F7DC70EBD21CC5DF53223C7878` | | APK 内容门禁 | 包内业务资源等于当前 App-Plus 编译产物;法务链接、手机号、固定测试验证码、私钥和常见令牌扫描通过 | | 真机安装启动 | vivo V2443A(Android 15)完成 `0.1.6` 覆盖安装和冷启动;相机、录音、媒体权限保持未授权,未见 `FATAL EXCEPTION` | -| 虚拟机干净安装 | 本机 `Medium_Phone` Android 15/API 35 完成 `0.1.6` 首次安装和冷启动;最新包真实显示原生隐私弹窗,四项敏感权限未授权,crash 缓冲区为空 | +| 虚拟机干净安装 | 本机 `Medium_Phone` Android 15/API 35 完成 `0.1.8` 卸载重装和冷启动;最新包真实显示原生隐私弹窗,四项敏感权限未授权,crash 缓冲区为空 | | 首次隐私弹窗 | 最新包已在虚拟机干净启动显示原生弹窗;前一候选包的两个 HTTPS 链接已在 vivo 系统浏览器打开,仍需用户亲自执行拒绝/同意反例 | | 法务静态页 | `https://peilian.njzhmj.top/legal/terms.html` 与 `https://peilian.njzhmj.top/legal/privacy.html` 已部署并可访问;发布前站点备份为 `/opt/wygj/backups/www-20260728224710` | @@ -35,7 +35,7 @@ 3. **原生人工反例未完成。** 覆盖安装和冷启动已通过;仍缺用户亲自执行的同意/拒绝、拒绝后退出、同意后登录、短信失败与重新登录、麦克风允许/拒绝、媒体取消、前后台切换和弱网验证。 4. **核心业务真机闭环未完成。** 仍缺认证账号下完整“查/练/问”、五个直通渠道、五个正式对练场景、ASR/TTS、附件上传下载和越权反例。 5. **内容与业务验收不足。** 当前仅收到 1 份生活顾问访谈,距离计划中的 24 份访谈、30 道人工审核政策题、5 个正式场景及五渠道真实处理人配置均无完成证据。 -6. **发布与回滚未签认。** 尚无四个发布单元的版本矩阵、负责人签字、完整预检结果和回滚演练记录。当前 APK 使用 DCloud 测试证书且运行壳允许明文流量,只能用于受控内测;企业签名和公司法务正式签认仍是正式发布门槛。 +6. **发布与回滚未签认。** 本地已有 APK/后端最小版本矩阵、构建证据和回滚边界,但尚无负责人签字、完整预检通过和回滚演练记录。当前 APK 已关闭明文流量但仍使用 DCloud 测试证书,只能用于受控内测;企业签名和公司法务正式签认仍是正式发布门槛。 ## 转为 Go 的最短路径 diff --git a/docs/帮道8月1日Android安装包测试版任务计划-20260727.docx b/docs/帮道8月1日Android安装包测试版任务计划-20260727.docx index 1a853401..1cef18dc 100644 Binary files a/docs/帮道8月1日Android安装包测试版任务计划-20260727.docx and b/docs/帮道8月1日Android安装包测试版任务计划-20260727.docx differ diff --git a/docs/帮道8月1日Android安装包测试版任务计划-20260727.md b/docs/帮道8月1日Android安装包测试版任务计划-20260727.md index 8a95637d..ae83a23d 100644 --- a/docs/帮道8月1日Android安装包测试版任务计划-20260727.md +++ b/docs/帮道8月1日Android安装包测试版任务计划-20260727.md @@ -1,6 +1,6 @@ # 帮道 8 月 1 日 Android 安装包测试版任务计划 -> 文档版本:v1.9 +> 文档版本:v2.0 > > 制定日期:2026-07-27 > @@ -86,16 +86,16 @@ ### 3.2 RC 自动化门禁快照(2026-07-29) -本节记录当前工作区自动化和测试签名自定义基座结果,不代表企业签名 RC APK 或完整真机验收完成。 +本节记录当前工作区自动化、测试签名自定义基座和独立内测 APK 结果,不代表企业签名 RC APK 或完整真机验收完成。 | 门禁 | 结果 | 说明 | |---|---|---| -| `mobile-uni` 单元测试 | 通过 | 191/191;测试过程有本机 WebSocket 端口占用告警,但无失败 | +| `mobile-uni` 单元测试 | 通过 | 193/193;测试过程有本机 WebSocket 端口占用告警,但无失败 | | `mobile-uni` TypeScript 检查 | 通过 | `npm --prefix mobile-uni run typecheck` | | H5 资源构建 | 通过 | 仅证明 H5 资源可编译 | | App-Plus 资源构建 | 通过 | 仅生成 HBuilderX 出包资源,不等同于 APK | -| App 原生配置、图标与启动图校验 | 通过 | 固定包名、语义版本、API 35、双 ABI、启动权限策略、Camera/Record 模块、22 个图标和 14 个启动图 | -| 自定义基座 APK 结构与签名校验 | 通过 | `verify:android-apk` 已核对包名、版本、`targetSdkVersion`、单签名、v2 签名和 SHA-256 | +| App 原生配置、图标与启动图校验 | 通过 | 固定包名、语义版本、API 35、双 ABI、启动权限策略、Camera/Record 模块、禁止明文 HTTP、网络安全资源、22 个图标和 14 个启动图 | +| 独立内测 APK 结构与签名校验 | 通过 | `verify-android-apk.ps1 -BuildKind dcloud-test` 已核对包名、版本、`targetSdkVersion`、网络安全配置、包内业务资源、单签名、v2 签名和 SHA-256 | | `390×844` 未登录首屏与入口 | 通过 | 今日主操作、练、问、问制度快捷项、大喇叭和直通车登录门禁均在首屏可见;练、问已实际点击可达,无控制台错误 | | 登录后核心业务路径 | 未执行 | 当前没有可用的本地 APP 登录主体和后端会话;不得用伪造手机号或未认证预览冒充通过 | | 管理端生产构建 | 通过 | 存在大 chunk 告警,不阻断本轮构建 | @@ -122,7 +122,8 @@ - 2026-07-29 当前候选包升级为 `0.1.6 (106)`,位于 `mobile-uni/dist/debug/bangdao-0.1.6-dcloud-test.apk`,大小 `33,921,279` 字节,SHA-256 为 `A5F026B70F11327355597B9FEA3625C4168700297E6D7847F10B079CEB860914`。登录页法务校验已移除 App-Plus 不保证支持的浏览器 `URL` 构造器,测试验证码不再以源码常量进入包内;APK 反编译门禁已确认包内业务资源等于当前编译产物,并通过法务链接、签名和敏感值扫描。 - `0.1.6 (106)` 已在同一设备完成保留数据的覆盖安装和 Launcher 冷启动;相机、录音和媒体权限保持未授权,未见 `FATAL EXCEPTION`。覆盖安装保留了前一版本的隐私状态,因此最新包的拒绝/同意反例仍须由用户亲自执行。 - 本机 Android Studio `Medium_Phone` 虚拟机已启动为 Android 15/API 35(`1080×2400`、密度 `420`)。在此前未安装本应用的虚拟机上,`0.1.6 (106)` 首次安装和 Launcher 冷启动成功,最新包真实显示原生隐私弹窗,四项相机、录音、图片和视频权限均未授予,crash 缓冲区为空。虚拟机只补充干净安装和 API 35 权限基线,不替代真机媒体、WebRTC、弱网与系统浏览器验收;拒绝/同意仍由用户亲自执行。 -- 自定义基座和独立内测 APK 均使用 DCloud 云端测试证书,不是企业正式签名 APK;独立内测包的 DCloud 运行壳仍声明允许明文流量,因此只可用于 8 月 1 日受控内测。公司法务签认、企业签名、用户实际同意/拒绝、登录后权限反例仍是正式发布阻断。 +- `0.1.8 (108)` 进一步把 Android 原生清单和 `network_security_config.xml` 纳入 App 构建链及 APK 反编译门禁。独立内测 APK 大小 `33,921,705` 字节,SHA-256 为 `E252140645F3AAD14B3594FD78E76BDF1A2122F7DC70EBD21CC5DF53223C7878`;APK 反编译确认 `usesCleartextTraffic=false`、`networkSecurityConfig` 引用和系统证书信任配置均已进入安装包,本机虚拟机卸载旧实例后全新安装并在 `2.904s` 内冷启动,最新隐私弹窗真实显示,四项敏感权限未授予且 crash 缓冲区为空。 +- 自定义基座和独立内测 APK 均使用 DCloud 云端测试证书,不是企业正式签名 APK;当前 `0.1.8` 已关闭明文流量并内置网络安全配置,但仍只可用于 8 月 1 日受控内测。公司法务签认、企业签名、用户实际同意/拒绝、登录后权限反例仍是正式发布阻断。 ## 4. P0 工作分解 diff --git a/mobile-uni/AndroidManifest.xml b/mobile-uni/AndroidManifest.xml new file mode 100644 index 00000000..e5f3eb3d --- /dev/null +++ b/mobile-uni/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + diff --git a/mobile-uni/nativeResources/android/res/xml/network_security_config.xml b/mobile-uni/nativeResources/android/res/xml/network_security_config.xml new file mode 100644 index 00000000..683208ff --- /dev/null +++ b/mobile-uni/nativeResources/android/res/xml/network_security_config.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/mobile-uni/package-lock.json b/mobile-uni/package-lock.json index 69529b69..00b500d4 100644 --- a/mobile-uni/package-lock.json +++ b/mobile-uni/package-lock.json @@ -1,12 +1,12 @@ { "name": "aihr-mobile-uni", - "version": "0.1.6", + "version": "0.1.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aihr-mobile-uni", - "version": "0.1.6", + "version": "0.1.8", "dependencies": { "@dcloudio/uni-app": "3.0.0-alpha-5020220260725001", "@dcloudio/uni-app-plus": "3.0.0-alpha-5020220260725001", diff --git a/mobile-uni/package.json b/mobile-uni/package.json index 7009a6ba..4bf01be0 100644 --- a/mobile-uni/package.json +++ b/mobile-uni/package.json @@ -1,14 +1,14 @@ { "$schema": "https://json.schemastore.org/package", "name": "aihr-mobile-uni", - "version": "0.1.6", + "version": "0.1.8", "private": true, "type": "module", "scripts": { "dev": "uni -p h5", "dev:h5": "uni -p h5", "build:h5": "uni build -p h5", - "build:app": "uni build -p app", + "build:app": "uni build -p app && node scripts/sync-app-native-resources.mjs", "generate:app-assets": "sh scripts/generate-app-assets.sh", "configure:app-privacy": "node scripts/configure-android-privacy.mjs", "verify:app-assets": "node scripts/verify-app-assets.mjs", diff --git a/mobile-uni/scripts/sync-app-native-resources.mjs b/mobile-uni/scripts/sync-app-native-resources.mjs new file mode 100644 index 00000000..9a604169 --- /dev/null +++ b/mobile-uni/scripts/sync-app-native-resources.mjs @@ -0,0 +1,24 @@ +import { cpSync, existsSync, mkdirSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; + +const projectRoot = resolve(import.meta.dirname, '..'); +const mappings = [ + [ + resolve(projectRoot, 'AndroidManifest.xml'), + resolve(projectRoot, 'dist/build/app/AndroidManifest.xml') + ], + [ + resolve(projectRoot, 'nativeResources/android/res/xml/network_security_config.xml'), + resolve(projectRoot, 'dist/build/app/nativeResources/android/res/xml/network_security_config.xml') + ] +]; + +for (const [source, destination] of mappings) { + if (!existsSync(source)) { + throw new Error(`Missing required native App resource: ${source}`); + } + mkdirSync(dirname(destination), { recursive: true }); + cpSync(source, destination); +} + +console.log('Android native manifest and network security resources synchronized.'); diff --git a/mobile-uni/scripts/verify-android-apk.ps1 b/mobile-uni/scripts/verify-android-apk.ps1 index b7adfd43..349f902a 100644 --- a/mobile-uni/scripts/verify-android-apk.ps1 +++ b/mobile-uni/scripts/verify-android-apk.ps1 @@ -205,19 +205,45 @@ try { $application = $decodedManifest.manifest.application $debuggable = $application.GetAttribute('debuggable', $androidNamespace) -eq 'true' $usesCleartextTraffic = $application.GetAttribute('usesCleartextTraffic', $androidNamespace) -eq 'true' + $networkSecurityConfig = $application.GetAttribute('networkSecurityConfig', $androidNamespace) if ($BuildKind -eq 'custom-base' -and -not $debuggable) { throw 'Custom base APK must remain debuggable.' } if ($BuildKind -eq 'dcloud-test' -and $debuggable) { throw 'DCloud internal test APK must not be debuggable.' } + if ($BuildKind -in @('dcloud-test', 'release') -and $usesCleartextTraffic) { + throw "$BuildKind APK must not allow cleartext HTTP traffic." + } + if ($BuildKind -in @('dcloud-test', 'release')) { + if ($networkSecurityConfig -ne '@xml/network_security_config') { + throw "$BuildKind APK must reference @xml/network_security_config." + } + $decodedNetworkSecurityPath = Join-Path $decodePath 'res\xml\network_security_config.xml' + if (-not (Test-Path -LiteralPath $decodedNetworkSecurityPath)) { + throw "$BuildKind APK is missing res/xml/network_security_config.xml." + } + $decodedNetworkSecurity = Get-Content -Raw -Encoding UTF8 -LiteralPath $decodedNetworkSecurityPath + if ($decodedNetworkSecurity -notmatch 'cleartextTrafficPermitted=\"false\"') { + throw "$BuildKind APK network security config must reject cleartext traffic." + } + if ($decodedNetworkSecurity -match 'cleartextTrafficPermitted=\"true\"') { + throw "$BuildKind APK network security config must not contain a cleartext exception." + } + if ($decodedNetworkSecurity -notmatch '') { + throw "$BuildKind APK network security config must trust system certificates only." + } + if ($decodedNetworkSecurity -match ' failures.push(message); const compiledAppRoot = resolve(projectRoot, 'dist/build/app'); +const sourceAndroidManifestPath = resolve(projectRoot, 'AndroidManifest.xml'); +const compiledAndroidManifestPath = resolve(compiledAppRoot, 'AndroidManifest.xml'); +const sourceNetworkSecurityPath = resolve( + projectRoot, + 'nativeResources/android/res/xml/network_security_config.xml' +); +const compiledNetworkSecurityPath = resolve( + compiledAppRoot, + 'nativeResources/android/res/xml/network_security_config.xml' +); const releaseTextExtensions = new Set(['.css', '.html', '.js', '.json', '.txt', '.xml']); const releaseSensitiveFilePattern = /(^|[\\/])(?:\.env(?:\.|$)|id_rsa$)|\.(?:jks|keystore|p12|pfx|pem)$/i; const releaseSensitiveContentPatterns = [ @@ -126,7 +136,7 @@ if (app?.ssl?.untrustedca !== 'refuse') { fail('app-plus.ssl.untrustedca must remain refuse'); } if (!/^\d+\.\d+\.\d+$/.test(String(manifest.versionName || ''))) { - fail('versionName must use semantic numeric form such as 0.1.6'); + fail('versionName must use semantic numeric form such as 0.1.8'); } if (!/^[1-9]\d*$/.test(String(manifest.versionCode || ''))) { fail('versionCode must be a positive integer'); @@ -137,6 +147,9 @@ if (android?.packagename !== 'com.yincheng.wygj') { if (android?.targetSdkVersion !== 35) { fail('Android targetSdkVersion must be 35 for the August 1 test package'); } +if (android?.usesCleartextTraffic !== false) { + fail('Android manifest configuration must explicitly disable cleartext traffic'); +} const expectedAndroidAbis = ['arm64-v8a', 'armeabi-v7a']; const actualAndroidAbis = Array.isArray(android?.abiFilters) ? [...android.abiFilters].sort() @@ -162,6 +175,46 @@ for (const permission of ['android.permission.RECORD_AUDIO', 'android.permission } } +const verifyAndroidNetworkSecurity = (manifestPath, networkSecurityPath, label) => { + if (!existsSync(manifestPath)) { + fail(`${label} is missing AndroidManifest.xml`); + return; + } + if (!existsSync(networkSecurityPath)) { + fail(`${label} is missing network_security_config.xml`); + return; + } + const androidManifest = readFileSync(manifestPath, 'utf8'); + const networkSecurity = readFileSync(networkSecurityPath, 'utf8'); + if (!/android:usesCleartextTraffic\s*=\s*["']false["']/.test(androidManifest)) { + fail(`${label} AndroidManifest.xml must disable cleartext traffic`); + } + if (!/android:networkSecurityConfig\s*=\s*["']@xml\/network_security_config["']/.test(androidManifest)) { + fail(`${label} AndroidManifest.xml must reference network_security_config`); + } + if (!/cleartextTrafficPermitted\s*=\s*["']false["']/.test(networkSecurity)) { + fail(`${label} network security config must reject cleartext traffic`); + } + if (/cleartextTrafficPermitted\s*=\s*["']true["']/.test(networkSecurity)) { + fail(`${label} network security config must not contain a cleartext exception`); + } + if (!//.test(networkSecurity)) { + fail(`${label} network security config must trust system certificates only`); + } + if (/ assert.equal(android?.packagename, 'com.yincheng.wygj'); assert.equal(android?.targetSdkVersion, 35, '不得回退到 DCloud 默认的 targetSdkVersion 28 兼容模式'); + assert.equal(android?.usesCleartextTraffic, false, '发布配置不得允许明文 HTTP'); +}); + +test('Android 原生网络安全配置拒绝明文流量且只信任系统证书', async () => { + const androidManifest = await readFile( + new URL('../AndroidManifest.xml', import.meta.url), + 'utf8' + ); + const networkSecurity = await readFile( + new URL('../nativeResources/android/res/xml/network_security_config.xml', import.meta.url), + 'utf8' + ); + + assert.match(androidManifest, /android:usesCleartextTraffic="false"/); + assert.match(androidManifest, /android:networkSecurityConfig="@xml\/network_security_config"/); + assert.match(networkSecurity, /cleartextTrafficPermitted="false"/); + assert.match(networkSecurity, //); + assert.doesNotMatch(networkSecurity, /cleartextTrafficPermitted="true"/); + assert.doesNotMatch(networkSecurity, / { + const pkg = await readJson('../package.json'); + const syncScript = await readFile( + new URL('../scripts/sync-app-native-resources.mjs', import.meta.url), + 'utf8' + ); + + assert.match(pkg.scripts?.['build:app'] || '', /sync-app-native-resources\.mjs/); + assert.match(syncScript, /dist\/build\/app\/AndroidManifest\.xml/); + assert.match(syncScript, /dist\/build\/app\/nativeResources\/android\/res\/xml\/network_security_config\.xml/); }); test('Android 启动阶段不主动索取设备信息或外部存储权限', async () => { @@ -146,6 +178,12 @@ test('APK 门禁区分自定义调试基座与内置业务资源的测试包', a assert.match(verifier, /dist\\build\\app-plus\\app-service\.js/); assert.match(verifier, /fixed test SMS code/); assert.match(verifier, /'mobile number'\s*=/); + assert.match(verifier, /dcloud-test.*release.*usesCleartextTraffic/s); + assert.match(verifier, /res\\xml\\network_security_config\.xml/); + assert.match(verifier, /network security config must trust system certificates only/); + assert.match(verifier, /must not contain a cleartext exception/); + assert.match(verifier, /must not trust user or bundled certificates/); + assert.match(verifier, /must not contain debug trust overrides/); }); test('8 月 1 日 RC 脚本默认拒绝脏工作区且记录完整构建证据', async () => {