fix(h5): sync latest report copy and deployment verification

This commit is contained in:
xsl
2026-10-11 22:59:21 +08:00
parent 3dda4ec7ff
commit 7518dc2307
14 changed files with 1981 additions and 863 deletions
@@ -2,6 +2,8 @@
更新:2026-10-11。当前环境为 Windows 宿主机 + 内网 WSL。
线上状态补充:`https://1me.xajuzi.com/state/` 已部署并配置真实微信凭据,服务器检查记录见 `线上微信登录部署记录-2026-10-11.md`。下文的内网缺配置说明针对本机开发环境。真实微信授权尚待客户端验证。
## 结论与当前阻碍
已完成微信网页授权代码,但**没有完成真实微信账号联调**。当前未配置 `WECHAT_APP_ID`、`WECHAT_APP_SECRET`、`PUBLIC_BASE_URL`,也没有可验证的公众号网页授权域名配置。内网 `http://192.168.3.25:8080/` 不能作为本次正式授权部署地址。因此本地启用「本机浏览器登录」,不会模拟微信登录成功。
@@ -0,0 +1,66 @@
# 线上微信登录部署记录
时间:2026-10-11(北京时间)。服务器:`ubuntu@1me.xajuzi.com`,页面:`https://1me.xajuzi.com/state/`。
## 后续更新:已配置真实凭据
用户提供凭据后,已将其写入 `/etc/juzi/wechat.env` 并重启服务。该文件仅 root 可读写(600),目录为 700;临时凭据文件已移除,未写入代码库或前端。
- 公网健康检查正常;`/state/api/auth/config` 返回 `wechat: true`。
- 真实登录入口返回微信授权 URL;AppID 与配置匹配,作用域为 `snsapi_base`,回调为 `https://1me.xajuzi.com/state/api/auth/wechat/callback`。
- OAuth 绑定 Cookie 确认为 HttpOnly、Secure、SameSite=Lax,路径 `/state/`;回调无效 code 后返回登录错误,重复 state 被拒绝,未错误发放登录会话。
- 微信网页授权换码接口使用诊断用无效 code 返回 `40029 invalid code`,未返回 IP 白名单错误。这只验证端点连通和错误处理,不能证明 AppSecret 正确或真实授权成功(无效 Secret 对照也返回相同错误)。
- 微信基础接口 `/cgi-bin/stable_token` 返回 `40164`,服务器出口 IP `49.233.222.47` 不在公众号 IP 白名单。须由公众号管理员加入后,才能完成这项基础凭据校验;不能直接把此错误认定为网页授权登录必然失败。
- 自动浏览器能够从真实页面发起微信跳转,但无法取得真实微信客户端授权 code,因此没有伪造生产微信身份或宣称真人登录已通过。
剩余验证:公众号管理员添加基础接口 IP 白名单;在真实微信内完成一次授权,确认权限与网页授权域名,并检查首页和报告列表。非登录功能的真实闭环结果见 `线上闭环测试-2026-10-11.md`。
### 用户确认加入白名单后的复测
2026-10-11 14:50 UTC,从同一生产服务器间隔复测:`stable_token` 和标准 `cgi-bin/token` 均仍返回 `40164`,提示 `49.233.222.47` 不在白名单。已确认服务器读取的 AppID 与用户提供的 `wx645d9a7573f1fe9a` 一致;未输出密钥或令牌。仍需核对是在该 AppID 对应账号的 **IP 白名单** 中添加并保存,而非网页授权域名/业务域名或其他微信应用的白名单。
网页授权端点返回的 HTML 仅做微信客户端环境判断,并未提供账号权限/域名是否合规的诊断结果;伪造 User-Agent 无法完成真实授权。换码负向测试仍正常,因此上述基础接口错误不能替代真实微信登录的验收结果。
## 已执行
- 确认服务器仓库为 `55b3a45`,包含 `ce8d0ff` 登录和 UI 更新;未覆盖服务器仓库文件。
- 原 Node 进程仍运行旧版本,重启 `juzi-h5.service` 后新账号接口生效。
- 新增 `/etc/systemd/system/juzi-h5.service.d/wechat.conf`,设置实际 HTTPS 站点地址、回环监听、代理信任和私有环境文件加载。
- 初次准备 `/etc/juzi/wechat.env` 时两个微信变量为空;随后已配置真实凭据。目录权限 700、文件 600,均为 root 所有。
- 修改 `/etc/nginx/sites-available/juzi`:为 `/state/api/admin/` 增加 Basic Auth(原先只有 `/api/admin/` 受保护);兼容旧路径,覆盖真实 IP 请求头;请求体上限调整为 20 MB,读取超时调整为 150 秒。
- `nginx -t` 通过后平滑 reload,原证书、管理员密码文件和域名验证文件未改动。
- Node 确认仅监听 `127.0.0.1:8080`。
备份:`/etc/juzi-deploy-backup.3ipdsA/`,含修改前 nginx 配置和 systemd 主服务文件。新增 systemd drop-in 为独立文件,可单独回退;数据目录未移动或删除。
## 初次部署的线上检查(历史结果)
| 检查 | 结果 |
|---|---|
| `/state/`、`/state/api/health` | 200 |
| `/state/api/auth/config` | 200,wechat=false,明确缺配置 |
| 匿名读取报告 | 401 |
| `/state/api/admin/stats`、`/api/admin/stats`、后台页面 | 401 |
| 数据目录、编码斜杠源码路径 | 404 |
| 同站但未同意协议的登录请求 | 400 |
| 跨站登录请求 | 403 |
| 微信登录请求(尚无凭据) | 503,未伪装登录成功 |
| 无效微信回调 | 302,返回 `/state/?login_error=state` |
| 根目录微信验证文件 | 200 |
## 初次部署时的凭据查找记录(现已由用户提供)
查找到的是 `MP_verify_kpx234XimQGtFPo6.txt`,只有 16 字符校验串;本地与服务器相关目录均未找到同名 `.md`。它用于域名验证,不包含 AppID/AppSecret,也不能用于换取微信用户身份。
在服务器运行 `sudoedit /etc/juzi/wechat.env`,填写对应公众号凭据:
```ini
WECHAT_APP_ID=公众号AppID
WECHAT_APP_SECRET=公众号AppSecret
```
保存后执行 `sudo systemctl restart juzi-h5.service`。无需在聊天中发送 AppSecret,也不要将该私有文件加入 Git。然后重新检查 `/state/api/auth/config`,并在微信内访问站点完成真实授权测试。
同时须确认该公众号具备网页授权权限,公众号后台的「网页授权域名」是 `1me.xajuzi.com`。目前仅确认校验文件可访问,尚无法确认公众号后台实际设置。回调 URL 已准备为 `https://1me.xajuzi.com/state/api/auth/wechat/callback`。
初次部署阶段没有触发收费 Face++ 检测,没有更改既有报告或真实用户账号。后续闭环测试使用独立测试身份调用一次真实 Face++,详见闭环测试记录。真实微信账号联调仍待完成,不能将接口部署成功视为微信授权成功。
+69
View File
@@ -0,0 +1,69 @@
# 线上闭环测试(不含真实微信授权)
测试时间:2026-10-11;站点:`https://1me.xajuzi.com/state/`。
## 结论
真实线上测肤、完整报告存储与回看已跑通。测试使用项目自带的 C3 三视角示例照片和新建的独立浏览器身份,没有修改或删除既有用户报告。除明确标注的故障注入/演示测试外,真实闭环经过公网 HTTPS、nginx、生产 Node 服务和真实 Face++ 接口。
## 发现并修复
前端原先 45 秒取消测肤请求;后端两次 60 秒请求加 4 秒重试间隔,最长约 124 秒,导致网络慢或重试时前端提前失败。前端调整为 150 秒,与已部署的 nginx 150 秒超时配合,并新增请求成功、超时、过期响应丢弃及计时器清理回归测试。
线上已同步 `h5/js/app.js`,原文件备份为 `/home/ubuntu/juzi-app-before-closedloop-20261011.js`。静态 JS 响应 `Cache-Control: no-cache`。该超时修复已包含在提交 `3dda4ec` 中;本次将新增测试和记录同步纳入 Git,统一本地和线上版本。
## 话术及仓库同步
以线上提交 `3dda4ec` 中的最新 `reference/AI测肤报告页话术修改.md` 为准,其 SHA-256 与用户附件完全一致(`c871de9ac3ddd40410653da904cb0b800e24bdf550df53d60602645bab2aafbb`)。本地拉取时先备份并恢复了原有测试、文档及知识图谱改动,未丢弃未提交工作。
运行 `python3 tools/build_report_copy.py` 重新生成 `h5/js/report-copy.js`,将水分「改善」结论中的「水分分 {water_score}」同步到页面实际使用的话术。评分和推荐算法不变。部署后重启服务,让服务端新报告使用更新文案;已保存的历史报告继续读取原有快照。
## 已通过的检查
| 场景 | 验证方式与结果 |
|---|---|
| 核心评分 | 服务器运行 83 项引擎自测、86 项回归断言,全部通过 |
| 账号和报告 | 服务器隔离测试验证授权同意、CSRF、账号隔离、完整快照、最近 5 份、重启持久化、身份恢复及 OAuth 状态绑定/防重放 |
| 公网页面 | Chromium 经真实 HTTPS 访问登录、首页、隐私协议、档案、报告列表和报告;320/390/480 宽度无横向溢出 |
| 三连拍与语音 | 模拟摄像头完整正脸→右脸→左脸流程;14 条音频可获取、引导顺序正确、三次完成播报、无语音截断 |
| 档案保护 | 未同意隐私协议不能继续;未填写档案不能启动拍摄 |
| 相机降级 | 注入相机拒绝及模型不可用,系统相机文件输入仍可完成三视角流程;坏图提示错误后可重新选择 |
| 真实测肤 | 三视角样例经页面压缩上传,真实 Face++ 返回成功,记录已完成,服务记录耗时 1281 ms,综合分 74 |
| 完整报告 | 七张检测图加载、七个标签切换、图片放大及浏览器返回通过;后端保存三张照片、完整可视化数据与文案 |
| 历史恢复 | 清除会话 Cookie 后由浏览器本地身份恢复;从服务端再次打开报告,七张图片与文案和首次展示逐项一致 |
| 历史异常 | 浏览器侧注入列表 503 错误,错误提示与刷新重试恢复正常 |
| 检测异常 | 浏览器侧分别注入业务失败、断网和超时,均关闭分析弹层、保留档案并允许重拍;不产生虚假报告 |
| 人脸模型 | 公网真实模型加载正常,示例图能实际识别人脸并输出 68 个关键点 |
| 权限隔离 | 匿名读报告 401,另一个已登录身份读测试报告 404;跨站检测请求 403;缺图请求 400 |
| 文件/后台保护 | 后台接口、后台页面需认证;数据、源码、编码斜杠访问被拒绝 |
| 服务状态 | nginx 配置检查通过;nginx 和 juzi-h5 均 active;Node 仅监听 127.0.0.1:8080 |
真实调用记录:`R20261011-193351-PD7P`。本次只有一笔真实 Face++ 检测请求;故障测试不调用收费接口。测试报告保留在独立测试身份下,未清理或改写其他数据。
## 可复现命令
```bash
node h5/test/accounts.js
node h5/js/skin-score.js
node h5/test/regression.js
node h5/test/analyze-timeout.js
node h5/test/report-persistence.mjs
node h5/test/wechat-browser.mjs
BASE_URL=https://1me.xajuzi.com/state node h5/test/e2e-browser.mjs
H5_BASE_URL=https://1me.xajuzi.com/state/ H5_ARTIFACT_DIR=/tmp/juzi-production-ui-20261011 node h5/test/ui-20261009.mjs
H5_BASE_URL=https://1me.xajuzi.com/state/ node h5/test/browser-failures.mjs
# 显式开启:会新增独立测试身份和报告,并实际消耗一次 Face++ 调用。
RUN_LIVE_ANALYZE=1 H5_BASE_URL=https://1me.xajuzi.com/state/ node h5/test/production-loop.mjs
```
浏览器测试依赖全局 Playwright 和 Chromium。`wechat-browser.mjs` 使用隔离 HTTPS 及模拟微信响应,只验证我们的跳转、会话和身份流程,不代表真实微信授权通过。
本机截图/结果:`/tmp/juzi-production-loop-LqVf32/`、`/tmp/juzi-production-ui-20261011/`、`/tmp/juzi-e2e-report.png`。不记录身份凭证或密钥,不将报告快照/照片加入 Git。
## 验证边界
- 真实微信授权尚未完成:后续用户提供 AppID/AppSecret 后已配置并启用;基础接口凭据校验受到 IP 白名单限制,网页授权换码负向测试正常,真实微信授权仍需客户端完成。详见微信部署记录。
- 自动化不等同于真实手机相机验收。微信内置浏览器的权限、前后镜头、实际光线/姿态下自动抓拍和系统相机交互,需要最终真机验证。
- 这次是功能闭环和异常恢复检查,不是高并发压力测试或完整安全审计。
+27 -21
View File
@@ -1,52 +1,52 @@
{
"0": "face-api.js",
"1": "t",
"1": "线上闭环测试(不含真实微信授权)",
"2": "app.js",
"3": "skin-score.js",
"4": "n",
"5": "normalize",
"6": "drawFaceExpressions",
"7": "Box",
"6": "getContext2dOrThrow",
"7": "extractFaces",
"8": "admin.js",
"9": "__awaiter",
"10": "AgeGenderNet",
"11": "lib/accounts.js",
"12": "server.js",
"13": "files",
"14": ".predictAgeAndGender",
"14": "FaceLandmarks",
"15": ".runAndExtendWithFaceDetection",
"16": "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范",
"17": "extractFaces",
"17": "getEnv",
"18": "1'me 在线肌肤测试 H5",
"19": "head-pose.js",
"20": "build_fixtures.py",
"21": "disposeUnusedWeightTensors",
"22": ".loadFromDisk",
"22": ".computeFaceDescriptor",
"23": "facepp-skinanalyze(旷视皮肤分析专业版调用技能)",
"24": "巨子测肤 · 1'me 语音播报内容设计文档 V1.0",
"25": "mobile-select.iife.min.js",
"26": "wi",
"27": "gen_voice.py",
"28": "i",
"28": "wn",
"29": "face-api.js 本地化依赖(h5/vendor/faceapi/)",
"30": "ui-20261009/manifest.json",
"31": ".toString",
"31": "browser-failures.mjs",
"32": "FaceMatcher",
"33": "build-fixture.py",
"34": "smoke-test.js",
"35": "DetectSingleFaceLandmarksTask",
"36": "C",
"36": "so",
"37": "UI 外发版 10.09 图片资源",
"38": "TinyYolov2",
"38": "PredictSingleAgeAndGenderWithFaceAlignmentTask",
"39": "UI交互界面调整",
"40": "ComputeAllFaceDescriptorsTask",
"41": ".draw",
"40": "extractAllFacesAndComputeResults",
"41": "initialize",
"42": "export_psd_assets.py",
"43": "FaceLandmark68Net",
"44": "start-local.sh",
"45": "PredictAllAgeAndGenderWithFaceAlignmentTask",
"46": "PredictAllFaceExpressionsWithFaceAlignmentTask",
"47": "k",
"47": "TinyYolov2Base",
"48": "PredictSingleFaceExpressionsWithFaceAlignmentTask",
"49": "mobile-select/README.md",
"50": "call_skinanalyze_3view.sh",
@@ -54,24 +54,30 @@
"52": "region-data.js",
"53": "护理建议(全量文本,按需求摘抄)",
"54": "UI 对齐 10.09 设计稿:调研与实施计划",
"55": "FaceLandmarks68",
"56": ".forwardInput",
"55": "NeuralNetwork",
"56": "convLayer",
"57": "regression.js",
"58": ".dispose",
"59": "call_skinanalyze.sh",
"60": "report-fixture.js",
"61": "NeuralNetwork",
"61": ".extractWeights",
"62": "提交前 Review(2026-10-11)",
"63": "e2e-browser.mjs",
"64": "account-client.js",
"65": "PredictSingleFaceExpressionsTask",
"66": ".runAndExtendWithFaceDetections",
"65": "tt",
"66": "cn",
"67": "ui-20261009.mjs",
"68": "ComputeSingleFaceDescriptorTask",
"68": "analyze-timeout.js",
"69": "misans/README.md",
"71": "extractFaceTensors",
"70": "decodeBoxesLayer",
"71": "dr",
"72": "build_report_copy.py",
"73": "camera-geometry.js",
"74": "report-copy.js",
"75": "prepare_h5_assets.py"
"75": "prepare_h5_assets.py",
"76": "TinyYolov2",
"77": "FaceDetection",
"79": "isValidNumber",
"80": "线上微信登录部署记录",
"83": "Yo"
}
+1 -1
View File
@@ -1 +1 @@
{"0": "796f85041a70fce3", "1": "966174ef01534051", "2": "9cf893b45c314643", "3": "271a5f5154afbdcd", "4": "e8b7c34edb5e6b25", "5": "163ce3e95fc46466", "6": "979dcc63399620ad", "7": "3a3e5d3e70ed45d6", "8": "cf2aba493368e557", "9": "94b2884fbd0d3d50", "10": "d982c7c094e0d01c", "11": "1c4f17c3d154962f", "12": "ab705784bbd1dbdc", "13": "7e3b558a587a4ebe", "14": "a2b13160223b5a98", "15": "5a9014c4a7f87699", "16": "95add6a975c0cb72", "17": "2405e1edb003a83e", "18": "5aa9c1fc3b3c8196", "19": "0a373ad2197fd1a6", "20": "aecf2fce44ba49c5", "21": "8835aeeaa165e9f9", "22": "b683919fcbe30647", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "3ca8c23d3e8220ec", "27": "eab427ce9a5219e9", "28": "536a3a2cb7285674", "29": "c608f1c6492e1cb1", "30": "73c13793fca09f56", "31": "cd6f168abba94fea", "32": "f6bcf56c4589316b", "33": "75f750c5b7d9f63d", "34": "f024ff7942935690", "35": "23f1d406f6c779ae", "36": "92f5f0a14da5908e", "37": "cb8f26249485324a", "38": "29790cf710874dd0", "39": "fdd95375b4a5ddb3", "40": "3dd68cce72af5314", "41": "bf75bfc99d4f9e34", "42": "ffd960136fde00ad", "43": "9a25b1230b5fe649", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "188dd5e33354e35d", "48": "8c471c7baeaaca54", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "15606ce5d8eec23a", "56": "880a74c6d255e5cc", "57": "f2a40b5f1b29f669", "58": "de5c2bc5e6fe8070", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "81661714a80b3097", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "86101547c05f96b9", "65": "cb5ac3401283c378", "66": "46204fb2e4d50c53", "67": "98f88c09f1236577", "68": "7ac475ea9bc280f8", "69": "e511846eeba03076", "71": "0b63a2ad1a11ce27", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884"}
{"0": "69ee47981e97a7eb", "1": "cb6ea50bd4e4a5ca", "2": "9cf893b45c314643", "3": "8ac814cdb72a1a7d", "4": "0f24ec9aab01e512", "5": "0e8200cefc11b932", "6": "1169f97bb458af6f", "7": "c1ae3952d970e510", "8": "cf2aba493368e557", "9": "8abf7998360e0ccc", "10": "c796af251148a471", "11": "79c3e26b93d030ce", "12": "ab705784bbd1dbdc", "13": "7e3b558a587a4ebe", "14": "186e1f68d4b2dfe9", "15": "5a9014c4a7f87699", "16": "95add6a975c0cb72", "17": "773ca6b3d5102a78", "18": "5aa9c1fc3b3c8196", "19": "0a373ad2197fd1a6", "20": "aecf2fce44ba49c5", "21": "5d94cf57d5d1a6fb", "22": "444ef0744645f5ac", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "83b0fd3b14363e4d", "27": "eab427ce9a5219e9", "28": "cf86030ab36386aa", "29": "c608f1c6492e1cb1", "30": "73c13793fca09f56", "31": "fbbcfc059751cc6a", "32": "f6bcf56c4589316b", "33": "75f750c5b7d9f63d", "34": "f024ff7942935690", "35": "23f1d406f6c779ae", "36": "c55f872b3e16ab6f", "37": "cb8f26249485324a", "38": "01f68dfc34a29d32", "39": "fdd95375b4a5ddb3", "40": "3a68564642f68c0d", "41": "eb3733e409dc7411", "42": "ffd960136fde00ad", "43": "9a25b1230b5fe649", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "41f3bef447e57f23", "48": "8c471c7baeaaca54", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "d96493a5f110bd4c", "56": "e649ddeadda89cbb", "57": "f2a40b5f1b29f669", "58": "73a31170f76a338b", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "400fa8ac8b726a66", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "86101547c05f96b9", "65": "3088ae48043ee39d", "66": "932f010dc6115fb4", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "c396c038e5f5007f", "71": "2bfc96d4090afd84", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "29790cf710874dd0", "77": "287d60115d9acda8", "79": "8f647d3ce08ecd2b", "80": "059d6ba8ca7ec3a0", "83": "a54008a71fdd44fa"}
+122 -92
View File
@@ -1,88 +1,94 @@
# Graph Report - juzi (2026-10-11)
## Corpus Check
- 59 files · ~4,180,488 words
- 64 files · ~4,182,459 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 1133 nodes · 2496 edges · 75 communities (57 shown, 18 thin omitted)
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 178 edges (avg confidence: 0.53)
- 1164 nodes · 2523 edges · 81 communities (63 shown, 18 thin omitted)
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 179 edges (avg confidence: 0.53)
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `ee744b6b`
- Built from commit: `3dda4ec7`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
## Community Hubs (Navigation)
- face-api.js
- t
- 线上闭环测试(不含真实微信授权)
- app.js
- skin-score.js
- n
- normalize
- drawFaceExpressions
- Box
- getContext2dOrThrow
- extractFaces
- admin.js
- __awaiter
- AgeGenderNet
- lib/accounts.js
- server.js
- files
- .predictAgeAndGender
- FaceLandmarks
- .runAndExtendWithFaceDetection
- 巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范
- extractFaces
- getEnv
- 1'me 在线肌肤测试 H5
- head-pose.js
- build_fixtures.py
- disposeUnusedWeightTensors
- .loadFromDisk
- .computeFaceDescriptor
- facepp-skinanalyze(旷视皮肤分析专业版调用技能)
- 巨子测肤 · 1'me 语音播报内容设计文档 V1.0
- mobile-select.iife.min.js
- wi
- gen_voice.py
- i
- wn
- face-api.js 本地化依赖(h5/vendor/faceapi/)
- ui-20261009/manifest.json
- .toString
- browser-failures.mjs
- FaceMatcher
- build-fixture.py
- DetectSingleFaceLandmarksTask
- C
- so
- UI 外发版 10.09 图片资源
- TinyYolov2
- PredictSingleAgeAndGenderWithFaceAlignmentTask
- UI交互界面调整
- ComputeAllFaceDescriptorsTask
- .draw
- extractAllFacesAndComputeResults
- initialize
- export_psd_assets.py
- FaceLandmark68Net
- start-local.sh
- PredictAllAgeAndGenderWithFaceAlignmentTask
- PredictAllFaceExpressionsWithFaceAlignmentTask
- k
- TinyYolov2Base
- PredictSingleFaceExpressionsWithFaceAlignmentTask
- mobile-select/README.md
- call_skinanalyze_3view.sh
- 护理建议(全量文本,按需求摘抄)
- UI 对齐 10.09 设计稿:调研与实施计划
- FaceLandmarks68
- .forwardInput
- NeuralNetwork
- convLayer
- regression.js
- .dispose
- call_skinanalyze.sh
- NeuralNetwork
- .extractWeights
- 提交前 Review(2026-10-11)
- e2e-browser.mjs
- account-client.js
- PredictSingleFaceExpressionsTask
- .runAndExtendWithFaceDetections
- tt
- cn
- ui-20261009.mjs
- ComputeSingleFaceDescriptorTask
- analyze-timeout.js
- misans/README.md
- extractFaceTensors
- decodeBoxesLayer
- dr
- build_report_copy.py
- TinyYolov2
- FaceDetection
- isValidNumber
- 线上微信登录部署记录
- Yo
## God Nodes (most connected - your core abstractions)
1. `__awaiter()` - 59 edges
@@ -111,15 +117,15 @@
## Import Cycles
- None detected.
## Communities (75 total, 18 thin omitted)
## Communities (81 total, 18 thin omitted)
### Community 0 - "face-api.js"
Cohesion: 0.02
Nodes (32): cn(), computeReshapedDimensions(), createBrowserEnv(), createFileSystem(), createNodejsEnv(), dr(), en(), extractBoundingBoxes() (+24 more)
Nodes (57): bn(), Bo(), ca(), cd(), ce(), computeReshapedDimensions(), dn(), ee() (+49 more)
### Community 1 - "t"
Cohesion: 0.08
Nodes (33): be(), bi(), Bo(), ce(), de(), e(), Ef(), er() (+25 more)
### Community 1 - "线上闭环测试(不含真实微信授权)"
Cohesion: 0.25
Nodes (7): 发现并修复, 可复现命令, 已通过的检查, 线上闭环测试(不含真实微信授权), 结论, 话术及仓库同步, 验证边界
### Community 2 - "app.js"
Cohesion: 0.06
@@ -130,20 +136,20 @@ Cohesion: 0.12
Nodes (44): acneGrade(), buildViewModel(), calcAcne(), calcContour(), calcSens(), calcSmooth(), calcTone(), calcWrinkles() (+36 more)
### Community 4 - "n"
Cohesion: 0.17
Nodes (32): a(), ae(), bf(), cf(), d(), drawContour(), dt(), ea() (+24 more)
Cohesion: 0.07
Nodes (88): a(), ae(), ao(), b(), ba(), be(), bf(), bi() (+80 more)
### Community 5 - "normalize"
Cohesion: 0.09
Nodes (26): conv(), conv$1(), convDown(), convLayer(), convLayer$1(), convNoRelu(), convWithBatchNorm(), denseBlock3() (+18 more)
Cohesion: 0.11
Nodes (17): conv(), conv$1(), convDown(), convLayer$1(), convNoRelu(), denseBlock3(), denseBlock4(), depthwiseSeparableConv() (+9 more)
### Community 6 - "drawFaceExpressions"
Cohesion: 0.22
Nodes (6): DrawBox(), drawDetections(), drawFaceExpressions(), DrawFaceLandmarks(), FaceExpressions(), isWithFaceExpressions()
### Community 6 - "getContext2dOrThrow"
Cohesion: 0.12
Nodes (15): DetectAllFacesTask(), DrawBox(), drawDetections(), drawFaceExpressions(), DrawFaceLandmarks(), DrawTextField(), extendWithFaceDetection(), FaceExpressions() (+7 more)
### Community 7 - "Box"
Cohesion: 0.13
Nodes (6): Box(), DetectAllFaceLandmarksTask(), extendWithFaceLandmarks(), FaceLandmarks(), minBbox(), ObjectDetection()
### Community 7 - "extractFaces"
Cohesion: 0.15
Nodes (5): Box(), DetectAllFaceLandmarksTask(), extractFaces(), extractFaceTensors(), isTensor3D()
### Community 8 - "admin.js"
Cohesion: 0.25
@@ -151,15 +157,15 @@ Nodes (23): api(), bind(), closeDetail(), collectFilters(), dimsHtml(), doRetry(
### Community 9 - "__awaiter"
Cohesion: 0.14
Nodes (12): __awaiter(), extendWithFaceDescriptor(), extractSingleFaceAndComputeResult(), FaceRecognitionNet(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow() (+4 more)
Nodes (12): __awaiter(), ComposableTask(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow(), __generator(), getModelUris() (+4 more)
### Community 10 - "AgeGenderNet"
Cohesion: 0.10
Nodes (6): AgeGenderNet(), extractParams$1(), FaceExpressionNet(), FaceLandmark68NetBase(), FaceLandmark68TinyNet(), FaceProcessor()
Cohesion: 0.11
Nodes (5): AgeGenderNet(), FaceExpressionNet(), FaceLandmark68NetBase(), FaceLandmark68TinyNet(), FaceProcessor()
### Community 11 - "lib/accounts.js"
Cohesion: 0.07
Nodes (40): build(), count(), fill(), numeric(), atomic(), body(), cookie(), cookies() (+32 more)
Cohesion: 0.06
Nodes (43): build(), count(), fill(), numeric(), atomic(), body(), cookie(), cookies() (+35 more)
### Community 12 - "server.js"
Cohesion: 0.08
@@ -169,9 +175,9 @@ Nodes (48): accounts, appendRecord(), callFacepp(), CRED, csvCell(), DATA_DIR, d
Cohesion: 0.12
Nodes (15): files, analyze-fail, analyzing, countdown-321, hint-far, hint-front, hint-hold, hint-near (+7 more)
### Community 14 - ".predictAgeAndGender"
Cohesion: 0.28
Nodes (4): extendWithAge(), extendWithGender(), PredictAllAgeAndGenderTask(), PredictSingleAgeAndGenderTask()
### Community 14 - "FaceLandmarks"
Cohesion: 0.10
Nodes (9): bgrToRgbTensor(), extendWithFaceLandmarks(), FaceLandmarks(), FaceLandmarks5(), FaceLandmarks68(), getCenterPoint(), isDimensions(), minBbox() (+1 more)
### Community 15 - ".runAndExtendWithFaceDetection"
Cohesion: 0.33
@@ -181,9 +187,9 @@ Nodes (5): allFacesMtcnn(), allFacesSsdMobilenetv1(), allFacesTinyYolov2(), dete
Cohesion: 0.15
Nodes (12): 1. 基准与尺寸换算, 2. 色彩规范, 3. 字体规范, 4. 组件切图(assets/svg/), 5. 图片素材(assets/images/,共 15 张), 6. 页面清单(design/ 下有整屏 PNG 预览), 7. 目录结构, 8. 还原建议 (+4 more)
### Community 17 - "extractFaces"
### Community 17 - "getEnv"
Cohesion: 0.19
Nodes (19): awaitMediaLoaded(), bufferToImage(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractImagePatches(), getContext2dOrThrow(), getEnv() (+11 more)
Nodes (14): awaitMediaLoaded(), bufferToImage(), createCanvas(), createCanvasFromMedia(), extractImagePatches(), getEnv(), getMediaDimensions(), imageTensorToCanvas() (+6 more)
### Community 18 - "1'me 在线肌肤测试 H5"
Cohesion: 0.07
@@ -199,11 +205,11 @@ Nodes (9): build_case1(), build_case2(), build_truth(), col(), dump(), main(), p
### Community 21 - "disposeUnusedWeightTensors"
Cohesion: 0.09
Nodes (24): disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1(), extractParamsFromWeigthMap$2() (+16 more)
Nodes (23): disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1(), extractParamsFromWeigthMap$2() (+15 more)
### Community 22 - ".loadFromDisk"
Cohesion: 0.25
Nodes (3): ComposableTask(), getModelUris(), TinyFaceDetector()
### Community 22 - ".computeFaceDescriptor"
Cohesion: 0.18
Nodes (4): ComputeAllFaceDescriptorsTask(), ComputeSingleFaceDescriptorTask(), extendWithFaceDescriptor(), FaceRecognitionNet()
### Community 23 - "facepp-skinanalyze(旷视皮肤分析专业版调用技能)"
Cohesion: 0.20
@@ -218,16 +224,16 @@ Cohesion: 0.20
Nodes (16): _arrayLikeToArray(), _arrayWithHoles(), _arrayWithoutHoles(), _classCallCheck(), _createClass(), _defineProperties(), h(), _iterableToArray() (+8 more)
### Community 26 - "wi"
Cohesion: 0.32
Nodes (8): Ci(), Ei(), Fi(), Ii(), ki(), ri(), wi(), Xt()
Cohesion: 0.28
Nodes (9): Ci(), Ei(), Fi(), Ii(), ki(), pe(), ri(), wi() (+1 more)
### Community 27 - "gen_voice.py"
Cohesion: 0.30
Nodes (11): ndarray, Path, gen_countdown(), main(), normalize_clip(), 倒计时单独按字生成再拼接,避免「三、二、一」整句被读成播报腔、与提示音气质漂移。 与提示音共用同一 speaker + instruct;句间留约…, 裁掉过长首尾静音,保留 ≤150ms 边沿,避免提示显迟钝。, 响度归一 −16 LUFS + 转 MP3 48k CBR mono 22050Hz。 (+3 more)
### Community 28 - "i"
Cohesion: 0.08
Nodes (32): ba(), bn(), ca(), cl(), da(), dn(), ee(), et() (+24 more)
### Community 28 - "wn"
Cohesion: 0.67
Nodes (3): wl(), wn(), xn()
### Community 29 - "face-api.js 本地化依赖(h5/vendor/faceapi/)"
Cohesion: 0.29
@@ -237,17 +243,13 @@ Nodes (6): face-api.js 本地化依赖(h5/vendor/faceapi/), 为什么本地
Cohesion: 0.33
Nodes (5): assets, document_size, layers, pages, source
### Community 31 - ".toString"
Cohesion: 0.33
Nodes (5): FaceMatch(), la(), pr(), pt(), vt()
### Community 33 - "build-fixture.py"
Cohesion: 0.60
Nodes (4): main(), 递归缩放 result 内全部坐标:{left,top,width,height} 矩形与 {x,y} 点,与原数据同为整数。, scale_coords(), to_data_url()
### Community 36 - "C"
Cohesion: 0.08
Nodes (29): ao(), C(), cd(), co(), Do(), eo(), fo(), Ft() (+21 more)
### Community 36 - "so"
Cohesion: 0.50
Nodes (4): Do(), io(), so(), To()
### Community 37 - "UI 外发版 10.09 图片资源"
Cohesion: 0.40
@@ -257,6 +259,14 @@ Nodes (4): UI 外发版 10.09 图片资源, 前端素材对应, 文件位置,
Cohesion: 0.40
Nodes (4): 1meAI测肤旷视UI问题反馈, UI交互界面调整, 交互, **设计**
### Community 40 - "extractAllFacesAndComputeResults"
Cohesion: 0.13
Nodes (9): extendWithAge(), extendWithFaceExpressions(), extendWithGender(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult(), PredictAllAgeAndGenderTask(), PredictAllFaceExpressionsTask(), PredictSingleAgeAndGenderTask() (+1 more)
### Community 41 - "initialize"
Cohesion: 0.29
Nodes (7): createBrowserEnv(), createFileSystem(), createNodejsEnv(), initialize(), isBrowser(), monkeyPatch(), setEnv()
### Community 42 - "export_psd_assets.py"
Cohesion: 0.70
Nodes (4): bounds(), layer_at(), main(), save_image()
@@ -265,10 +275,6 @@ Nodes (4): bounds(), layer_at(), main(), save_image()
Cohesion: 0.50
Nodes (3): HOST, PORT, start-local.sh script
### Community 47 - "k"
Cohesion: 0.18
Nodes (11): b(), gn(), k(), kn(), ne(), tt(), wo(), Yt() (+3 more)
### Community 53 - "护理建议(全量文本,按需求摘抄)"
Cohesion: 0.10
Nodes (19): 5\.1 轮廓(紧塑力), 5\.2 皱纹(紧塑力), 5\.3 肤色(透亮感), 5\.4 平滑度(透亮感), 5\.5 水分(透亮感), 5\.6 泛红(稳韧力), 5\.7 痘痘(稳韧力), 6\.1 轮廓 (+11 more)
@@ -277,25 +283,21 @@ Nodes (19): 5\.1 轮廓(紧塑力), 5\.2 皱纹(紧塑力), 5\.3 肤色
Cohesion: 0.11
Nodes (17): 1. 结论和依据, 2. 统一比较口径, 3. 逐页差异和调整内容, 4. 报告数据如何接到新 UI, 5. 登录与“我的报告”的真实能力缺口, 6. 原稿中需要整理的内容, 7. 实施顺序与交付物, 8. 验收定义 (+9 more)
### Community 55 - "FaceLandmarks68"
Cohesion: 0.19
Nodes (3): FaceLandmarks5(), FaceLandmarks68(), getCenterPoint()
### Community 56 - ".forwardInput"
Cohesion: 0.22
Nodes (9): boxPredictionLayer(), decodeBoxesLayer(), depthwiseConvLayer(), getCenterCoordinatesAndSizesLayer(), getStridesForLayerIdx(), mobileNetV1(), outputLayer(), pointwiseConvLayer() (+1 more)
### Community 56 - "convLayer"
Cohesion: 0.15
Nodes (17): boxPredictionLayer(), convLayer(), convWithBatchNorm(), depthwiseConvLayer(), depthwiseSeparableConv$1(), fullyConnectedLayer(), getStridesForLayerIdx(), leaky() (+9 more)
### Community 57 - "regression.js"
Cohesion: 0.60
Nodes (5): diffCtx(), loadJSON(), main(), okNum(), okStr()
### Community 58 - ".dispose"
Cohesion: 0.15
Nodes (12): getSizesForScale(), iou(), nonMaxSuppression(), nonMaxSuppression$1(), normalize$1(), rescaleAndNormalize(), scale(), SsdMobilenetv1() (+4 more)
Cohesion: 0.21
Nodes (11): getSizesForScale(), iou(), NetInput(), nonMaxSuppression(), nonMaxSuppression$1(), normalize$1(), rescaleAndNormalize(), scale() (+3 more)
### Community 61 - "NeuralNetwork"
Cohesion: 0.05
Nodes (32): bgrToRgbTensor(), createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), Dimensions() (+24 more)
### Community 61 - ".extractWeights"
Cohesion: 0.09
Nodes (25): createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory(), extractFCParamsFactory() (+17 more)
### Community 62 - "提交前 Review(2026-10-11)"
Cohesion: 0.40
@@ -309,24 +311,52 @@ Nodes (4): EXPECTED, fails, pageErrors, pwRoot
Cohesion: 0.90
Nodes (4): browser(), init(), read(), request()
### Community 71 - "extractFaceTensors"
Cohesion: 0.18
Nodes (9): extendWithFaceDetection(), extendWithFaceExpressions(), extractAllFacesAndComputeResults(), extractFaceTensors(), isTensor3D(), isWithFaceDetection(), isWithFaceLandmarks(), PredictAllFaceExpressionsTask() (+1 more)
### Community 65 - "tt"
Cohesion: 0.50
Nodes (4): kn(), tt(), wo(), z()
### Community 66 - "cn"
Cohesion: 0.67
Nodes (3): cn(), en(), yn()
### Community 68 - "analyze-timeout.js"
Cohesion: 0.20
Nodes (7): assert, end, fs, path, source, start, vm
### Community 70 - "decodeBoxesLayer"
Cohesion: 0.67
Nodes (3): decodeBoxesLayer(), getCenterCoordinatesAndSizesLayer(), outputLayer()
### Community 71 - "dr"
Cohesion: 0.67
Nodes (3): dr(), na(), ta()
### Community 79 - "isValidNumber"
Cohesion: 0.50
Nodes (4): Dimensions(), isValidNumber(), isValidProbablitiy(), isWithGender()
### Community 80 - "线上微信登录部署记录"
Cohesion: 0.29
Nodes (6): 初次部署时的凭据查找记录(现已由用户提供), 初次部署的线上检查(历史结果), 后续更新:已配置真实凭据, 已执行, 用户确认加入白名单后的复测, 线上微信登录部署记录
### Community 83 - "Yo"
Cohesion: 0.67
Nodes (3): qo(), Xo(), Yo()
## Knowledge Gaps
- **152 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+147 more)
- **173 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+168 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **18 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `k()` connect `k` to `face-api.js`, `t`, `app.js`, `n`, `lib/accounts.js`, `i`?**
_High betweenness centrality (0.078) - this node is a cross-community bridge._
- **Why does `renderReport()` connect `app.js` to `t`, `k`?**
_High betweenness centrality (0.058) - this node is a cross-community bridge._
- **Why does `rate()` connect `lib/accounts.js` to `k`?**
_High betweenness centrality (0.055) - this node is a cross-community bridge._
- **Why does `k()` connect `n` to `face-api.js`, `app.js`, `lib/accounts.js`?**
_High betweenness centrality (0.083) - this node is a cross-community bridge._
- **Why does `rate()` connect `lib/accounts.js` to `n`?**
_High betweenness centrality (0.061) - this node is a cross-community bridge._
- **Why does `renderReport()` connect `app.js` to `n`?**
_High betweenness centrality (0.056) - this node is a cross-community bridge._
- **Are the 24 inferred relationships involving `n()` (e.g. with `newRecordId()` and `ae()`) actually correct?**
_`n()` has 24 INFERRED edges - model-reasoned connections that need verification._
- **Are the 27 inferred relationships involving `r()` (e.g. with `handleExport()` and `handleRecordsQuery()`) actually correct?**
@@ -334,4 +364,4 @@ _Questions this graph is uniquely positioned to answer:_
- **Are the 7 inferred relationships involving `t()` (e.g. with `renderReport()` and `e()`) actually correct?**
_`t()` has 7 INFERRED edges - model-reasoned connections that need verification._
- **What connects `source`, `document_size`, `pages` to the rest of the system?**
_152 weakly-connected nodes found - possible documentation gaps or missing edges._
_173 weakly-connected nodes found - possible documentation gaps or missing edges._
File diff suppressed because one or more lines are too long
+1374 -733
View File
File diff suppressed because it is too large Load Diff
+35 -10
View File
@@ -40,8 +40,8 @@
"semantic_hash": ""
},
"h5/js/app.js": {
"mtime": 1791646682.4027648,
"ast_hash": "b838a3efa4351bb87d5f17644bf83ca7",
"mtime": 1791730674.7447748,
"ast_hash": "7fe1cc5b34a9367c3fed5d502d304e2b",
"semantic_hash": ""
},
"h5/js/head-pose.js": {
@@ -1240,8 +1240,8 @@
"semantic_hash": ""
},
"h5/js/report-copy.js": {
"mtime": 1791645357.4367466,
"ast_hash": "86f574571b8e882868fce221677cf097",
"mtime": 1791730702.7819743,
"ast_hash": "eedcef7cdd1a3f00fe7cb5dabcafb95c",
"semantic_hash": ""
},
"h5/js/report-ui.js": {
@@ -1265,8 +1265,8 @@
"semantic_hash": ""
},
"h5/test/ui-20261009.mjs": {
"mtime": 1791646682.4027648,
"ast_hash": "c2754fb520f28a48391662a49860687c",
"mtime": 1791730674.7561882,
"ast_hash": "70fb3ba9b32a4c2a9845dbf027bedabb",
"semantic_hash": ""
},
"tools/build_report_copy.py": {
@@ -1290,8 +1290,8 @@
"semantic_hash": ""
},
"docs/\u5fae\u4fe1\u767b\u5f55\u63a5\u5165\u4e0e\u516c\u7f51\u90e8\u7f72\u8bf4\u660e.md": {
"mtime": 1791710640.6026413,
"ast_hash": "70d447aea33b739f438e0c24503c2e49",
"mtime": 1791730674.7561882,
"ast_hash": "6e8f9f9a8b941790adccde55be9b125c",
"semantic_hash": ""
},
"h5/assets/fonts/misans/README.md": {
@@ -1300,8 +1300,8 @@
"semantic_hash": ""
},
"reference/AI\u6d4b\u80a4\u62a5\u544a\u9875\u8bdd\u672f\u4fee\u6539.md": {
"mtime": 1791644142.8049476,
"ast_hash": "36beb496de948671dd57252803103a9d",
"mtime": 1791730674.7447748,
"ast_hash": "7e6f8ef4ae4a1c010f3344ccdc088cf6",
"semantic_hash": ""
},
"design/ui-20261009/audit-current/01-splash.png": {
@@ -1538,5 +1538,30 @@
"mtime": 1791710850.0228443,
"ast_hash": "93bae80d0b6998a1d31616c512261e68",
"semantic_hash": ""
},
"h5/test/analyze-timeout.js": {
"mtime": 1791730674.763797,
"ast_hash": "c30bdb0ecb2de0b4c280335940e9d01f",
"semantic_hash": ""
},
"h5/test/production-loop.mjs": {
"mtime": 1791730674.763797,
"ast_hash": "0fa6706f54f034124287d29adc894f71",
"semantic_hash": ""
},
"docs/\u7ebf\u4e0a\u5fae\u4fe1\u767b\u5f55\u90e8\u7f72\u8bb0\u5f55-2026-10-11.md": {
"mtime": 1791730745.132436,
"ast_hash": "056f5b8e0ee8920285d9cf8b1764f3f5",
"semantic_hash": ""
},
"h5/test/browser-failures.mjs": {
"mtime": 1791730674.763797,
"ast_hash": "b5eaa9d78296f5c690566ed91a39d467",
"semantic_hash": ""
},
"docs/\u7ebf\u4e0a\u95ed\u73af\u6d4b\u8bd5-2026-10-11.md": {
"mtime": 1791730745.132436,
"ast_hash": "04095d09dce4e81349b9256a0e8cb540",
"semantic_hash": ""
}
}
+1 -1
View File
@@ -23,7 +23,7 @@
"优秀": "肌肤质感表现优异,表面细腻平滑,角质层排列整齐,无明显粗大毛孔与黑头。"
},
"water": {
"改善": "缺水面积 {water_area}%、肌肤缺水比较严重 {water_sev},肌肤含水量偏低,水润度不足,表面偏干燥,容易出现干纹。",
"改善": "水分分 {water_score};缺水面积 {water_area}%、肌肤缺水比较严重 {water_sev},肌肤含水量偏低,水润度不足,表面偏干燥,容易出现干纹。",
"良好": "水分分 {water_score};缺水面积 {water_area}%、肌肤含水量处于健康水平,整体水润度良好。",
"优秀": "水分分 {water_score};角质层含水量充足,肌肤屏障锁水能力强,表面饱满水润,无干燥紧绷表现。"
},
+54
View File
@@ -0,0 +1,54 @@
// Exercise the actual browser request function with a deterministic fake clock.
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const source = fs.readFileSync(path.join(__dirname, '../js/app.js'), 'utf8');
const start = source.indexOf(' var ANALYZE_TIMEOUT_MS =');
const end = source.indexOf(' /* ─── SkinScore viewModel', start);
assert(start >= 0 && end > start);
function setup() {
let timer, complete, signal, result;
const ctx = { IS_MOCK: false, camSeq: 1, AbortController,
setTimeout(fn, delay) { timer = { fn, delay }; return 1; },
clearTimeout() { timer = null; },
fetch(url, options) {
assert.equal(url, 'api/skin-analyze');
signal = options.signal;
return new Promise((resolve, reject) => {
complete = () => resolve({ json: async () => ({ ok: true, report_saved: true }) });
signal.addEventListener('abort', () => { const e = new Error('aborted'); e.name = 'AbortError'; reject(e); });
});
}
};
vm.createContext(ctx);
vm.runInContext(source.slice(start, end), ctx);
ctx.requestAnalyze(['front', 'left', 'right'], 1, {}, (...args) => { result = args; });
return { ctx, get timer() { return timer; }, get signal() { return signal; },
get result() { return result; }, finish() { complete(); } };
}
const flush = () => new Promise(resolve => setImmediate(resolve));
(async () => {
const delayed = setup();
assert.equal(delayed.timer.delay, 150000);
assert(delayed.timer.delay > 60000 * 2 + 4000, 'Allow both backend attempts plus retry wait');
delayed.finish();
await flush();
assert.equal(delayed.result[0], null);
assert.equal(delayed.result[1].report_saved, true);
assert.equal(delayed.timer, null);
assert.equal(delayed.signal.aborted, false);
const expired = setup();
expired.timer.fn();
await flush();
assert(expired.signal.aborted);
assert.match(expired.result[0], /检测服务开小差/);
const canceled = setup();
canceled.ctx.camSeq++;
canceled.finish();
await flush();
assert.equal(canceled.result, undefined);
assert.equal(canceled.timer, null);
console.log('PASS: frontend timeout covers backend retry budget; success, timeout, stale response, timer cleanup');
})().catch(e => { console.error(e); process.exitCode = 1; });
+79
View File
@@ -0,0 +1,79 @@
// Public-site error recovery and real model loading. Analysis requests are intercepted;
// no paid upstream call or report is created. A separate empty browser identity is used.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { execSync } from 'node:child_process';
import { fileURLToPath, pathToFileURL } from 'node:url';
const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href);
const base = process.env.H5_BASE_URL || 'http://127.0.0.1:8080/';
const samplePath = fileURLToPath(new URL('../../reference/facepp-skinanalyze/examples/images/C3_180.jpg', import.meta.url));
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 375, height: 812 } });
page.setDefaultTimeout(60000);
const errors = [];
page.on('pageerror', e => errors.push(e.message));
await page.addInitScript(() => {
navigator.mediaDevices.getUserMedia = () => Promise.reject(new DOMException('test denial', 'NotAllowedError'));
const original = window.setTimeout;
window.setTimeout = function (fn, delay, ...args) {
// Accelerate only the actual analyze deadline for the timeout branch.
return original(fn, window.__fastAnalyzeTimeout && delay === 150000 ? 150 : delay, ...args);
};
});
try {
await page.goto(base, { waitUntil: 'domcontentloaded' });
// Real models from production paths, actual landmark inference on the sample image.
const detected = await page.evaluate(async data => {
await Promise.all([
faceapi.nets.tinyFaceDetector.loadFromUri('vendor/faceapi/models'),
faceapi.nets.faceLandmark68Net.loadFromUri('vendor/faceapi/models')
]);
const img = await faceapi.fetchImage(data);
const d = await faceapi.detectSingleFace(img, new faceapi.TinyFaceDetectorOptions({ inputSize: 416, scoreThreshold: 0.3 })).withFaceLandmarks();
return d ? { score: d.detection.score, landmarks: d.landmarks.positions.length } : null;
}, 'data:image/jpeg;base64,' + fs.readFileSync(samplePath).toString('base64'));
assert(detected && detected.landmarks === 68 && detected.score > 0.3);
console.log('PASS: real production models load and detect sample face with 68 landmarks');
// Keep subsequent error-flow uploads independent of angle validation.
await page.evaluate(() => { window.faceapi = undefined; });
await page.waitForFunction(() => !document.querySelector('#login-message').textContent.includes('检查'));
await page.check('#login-consent');
await page.click('[data-action="login-browser"]');
await page.waitForSelector('#s-splash.active');
await page.click('[data-action="open-privacy"]');
await page.click('#privacy-check');
await page.click('[data-action="agree-continue"]');
for (const selector of ['#pick-birth', '#pick-gender', '#pick-region']) {
await page.click(selector);
await page.click('.ms-mobile-select.ms-show .ms-ensure');
await page.waitForTimeout(400);
}
const cases = [
['business', '未检测到人脸', route => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ ok: false, message: '未检测到人脸' }) })],
['network', '网络异常', route => route.abort('failed')],
['timeout', '检测服务开小差', async route => { await new Promise(resolve => setTimeout(resolve, 500)); await route.abort().catch(() => {}); }]
];
for (const [name, message, handler] of cases) {
await page.route('**/api/skin-analyze', handler);
await page.evaluate(fast => { window.__fastAnalyzeTimeout = fast; }, name === 'timeout');
await page.click('[data-action="submit-profile"]');
await page.click('[data-action="start-camera"]');
for (let i = 1; i <= 3; i++) {
await page.waitForFunction(n => document.querySelector('#cam-step').textContent.includes(n + '/3'), i);
await page.waitForTimeout(1600);
await page.setInputFiles('#cam-file', samplePath);
}
await page.waitForSelector('#s-profile.active');
assert((await page.locator('#toast').textContent()).includes(message), name);
assert.equal(await page.locator('#p-analyzing.show').count(), 0);
assert(await page.locator('#f-birth').inputValue());
await page.unroute('**/api/skin-analyze', handler);
console.log('PASS: ' + name + ' error → dismiss loading → retain profile → allow retry');
}
const reports = await (await page.context().request.get(base + 'api/reports')).json();
assert.equal(reports.total, 0);
assert.deepEqual(errors, []);
console.log('PASS: no spurious reports and no JS errors');
} finally {
await browser.close();
}
+144
View File
@@ -0,0 +1,144 @@
// Explicit opt-in: one real Face++ call with the repository's three-view sample.
// Creates a separate browser identity/report; never alters existing user data.
// RUN_LIVE_ANALYZE=1 H5_BASE_URL=https://host/state/ node h5/test/production-loop.mjs
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { execSync } from 'node:child_process';
import { fileURLToPath, pathToFileURL } from 'node:url';
assert.equal(process.env.RUN_LIVE_ANALYZE, '1', 'Requires explicit RUN_LIVE_ANALYZE=1 (paid upstream call)');
const base = process.env.H5_BASE_URL;
assert(base && base.endsWith('/'), 'Set H5_BASE_URL with trailing slash');
const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href);
const samples = fileURLToPath(new URL('../../reference/facepp-skinanalyze/examples/images/', import.meta.url));
const artifacts = fs.mkdtempSync(path.join(os.tmpdir(), 'juzi-production-loop-'));
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ viewport: { width: 390, height: 844 } });
const page = await context.newPage();
page.setDefaultTimeout(60000);
const errors = [];
page.on('pageerror', e => errors.push(e.message));
// Exercise the denied-camera fallback; hardware camera behavior needs a phone.
await context.addInitScript(() => {
navigator.mediaDevices.getUserMedia = () => Promise.reject(new DOMException('test denial', 'NotAllowedError'));
window.__analyzeTimeouts = [];
const original = window.setTimeout;
window.setTimeout = function (fn, delay, ...args) {
if (delay >= 45000) window.__analyzeTimeouts.push(delay);
return original(fn, delay, ...args);
};
});
// Test model-unavailable fallback as well, without relying on machine vision.
await page.route('**/models/**', route => route.abort());
try {
await page.goto(base, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => !document.querySelector('#login-message').textContent.includes('检查'));
await page.check('#login-consent');
await page.click('[data-action="login-browser"]');
await page.waitForSelector('#s-splash.active');
const api = context.request;
assert.equal((await api.get(base + 'api/reports')).status(), 200);
const cookie = (await context.cookies()).find(c => c.httpOnly);
assert(cookie && cookie.secure && cookie.sameSite === 'Lax' && cookie.path === new URL(base).pathname);
assert.equal((await api.post(base + 'api/skin-analyze', { data: {} })).status(), 400);
assert.equal((await api.post(base + 'api/skin-analyze', { data: {}, headers: { Origin: 'https://invalid.example' } })).status(), 403);
console.log('PASS: public HTTPS, secure scoped cookie, missing-image validation, cross-origin denial');
await page.click('[data-action="open-privacy"]');
await page.click('[data-action="agree-continue"]');
assert.equal(await page.locator('#s-profile.active').count(), 0);
await page.click('#privacy-check');
await page.click('[data-action="agree-continue"]');
await page.click('[data-action="submit-profile"]');
assert.equal(await page.locator('#s-profile.active').count(), 1);
for (const selector of ['#pick-birth', '#pick-gender', '#pick-region']) {
await page.click(selector);
await page.click('.ms-mobile-select.ms-show .ms-ensure');
await page.waitForTimeout(400);
}
await page.click('[data-action="submit-profile"]');
await page.click('[data-action="start-camera"]');
await page.waitForFunction(() => document.querySelector('#toast').textContent.includes('系统相机'));
// Bad file must keep us on the same step and allow selecting a valid photo.
await page.setInputFiles('#cam-file', { name: 'broken.jpg', mimeType: 'image/jpeg', buffer: Buffer.from('not an image') });
await page.waitForFunction(() => document.querySelector('#toast').textContent.includes('读取失败'));
assert((await page.locator('#cam-step').textContent()).includes('1/3'));
console.log('PASS: consent/profile guards, denied camera + missing model fallback, invalid-image recovery');
let analyzeCalls = 0;
page.on('request', req => { if (req.url() === base + 'api/skin-analyze') analyzeCalls++; });
const responsePromise = page.waitForResponse(res => res.url() === base + 'api/skin-analyze', { timeout: 165000 });
// Capture sequence front → right → left; app maps to API front/left/right fields.
for (const [i, name] of ['C3_180.jpg', 'C3_-135.jpg', 'C3_135.jpg'].entries()) {
await page.waitForFunction(n => document.querySelector('#cam-step').textContent.includes(n + '/3'), i + 1);
await page.waitForTimeout(1600);
await page.setInputFiles('#cam-file', path.join(samples, name));
}
const response = await responsePromise;
assert.equal(response.status(), 200);
const result = await response.json();
assert.equal(result.ok, true, JSON.stringify({ code: result.code, message: result.message }));
assert.equal(result.report_saved, true);
assert.equal(analyzeCalls, 1);
assert((await page.evaluate(() => window.__analyzeTimeouts)).includes(150000));
await page.waitForSelector('#s-report.active');
await page.waitForFunction(() => [...document.querySelectorAll('.ditem-img')].every(i => !i.hidden && i.naturalWidth > 0));
const snap = (await (await api.get(base + 'api/reports/' + result.record_id)).json()).report;
assert.equal(snap.photos.filter(Boolean).length, 3);
assert.deepEqual(snap.presentation, result.presentation);
assert.deepEqual(snap.payload.result.face_maps, result.result.face_maps);
const images = await page.locator('.ditem-img').evaluateAll(nodes => nodes.map(i => i.src));
const copy = await page.locator('.ditem-para').allTextContents();
for (const card of await page.locator('.card-detail').all()) {
for (const tab of await card.locator('.dtab').all()) {
await tab.click();
assert.equal(await card.locator('.dpanel.active').count(), 1);
}
}
await page.locator('.ditem-img:visible').first().click();
await page.waitForSelector('#p-zoom.show');
await page.goBack();
await page.waitForSelector('#p-zoom.show', { state: 'hidden' });
assert.equal(await page.locator('#s-report.active').count(), 1);
await page.screenshot({ path: path.join(artifacts, 'real-report.png'), fullPage: true });
console.log('PASS: three sample views → REAL Face++ → scoring → seven images/tabs → zoom/back → backend snapshot');
// Cookie loss is restored using this browser's stored random identity, not a new account.
await context.clearCookies();
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForSelector('#s-splash.active');
await page.click('[data-action="history"]');
await page.click('[data-report-id="' + result.record_id + '"]');
await page.waitForSelector('#s-report.active');
await page.waitForFunction(() => [...document.querySelectorAll('.ditem-img')].every(i => !i.hidden && i.naturalWidth > 0));
assert.deepEqual(await page.locator('.ditem-img').evaluateAll(nodes => nodes.map(i => i.src)), images);
assert.deepEqual(await page.locator('.ditem-para').allTextContents(), copy);
await page.goBack();
await page.waitForSelector('#s-history.active');
// Read failure + explicit retry must recover without losing the report.
await page.route('**/api/reports', route => route.fulfill({ status: 503, contentType: 'application/json', body: JSON.stringify({ ok: false, message: '测试临时不可用' }) }));
await page.click('[data-action="history-retry"]');
await page.waitForFunction(() => document.querySelector('#history-list').textContent.includes('测试临时不可用'));
await page.unroute('**/api/reports');
await page.click('[data-action="history-retry"]');
await page.waitForSelector('[data-report-id="' + result.record_id + '"]');
await page.screenshot({ path: path.join(artifacts, 'real-history.png') });
await page.goBack();
await page.waitForSelector('#s-splash.active');
console.log('PASS: cookie restoration, report reload with identical seven images/copy, history error/retry, browser back');
const other = await browser.newContext();
assert.equal((await other.request.get(base + 'api/reports/' + result.record_id)).status(), 401);
await other.request.post(base + 'api/auth/browser', { data: { consent: true } });
assert.equal((await other.request.get(base + 'api/reports/' + result.record_id)).status(), 404);
for (const route of ['api/admin/stats', 'admin.html', 'data/accounts/identities.json', '%2fserver.js', 'lib/accounts.js', 'test/accounts.js']) {
assert([401,403,404].includes((await other.request.get(base + route)).status()), route);
}
await other.close();
assert.deepEqual(errors, []);
fs.writeFileSync(path.join(artifacts, 'summary.json'), JSON.stringify({ record_id: result.record_id, analyzeCalls, errors, tested_at: new Date().toISOString() }, null, 2));
console.log('PASS: cross-account isolation, admin/source/data protection, zero JS errors');
console.log('Test report (dedicated identity): ' + result.record_id + '; artifacts: ' + artifacts);
} finally {
await browser.close();
}
+3 -1
View File
@@ -3,7 +3,9 @@ import { pathToFileURL } from 'node:url';
const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href);
import fs from 'node:fs';
import assert from 'node:assert/strict';
const root = new URL('../../design/ui-20261009/verification/', import.meta.url);
const root = process.env.H5_ARTIFACT_DIR
? pathToFileURL(process.env.H5_ARTIFACT_DIR.replace(/\/$/, '') + '/')
: new URL('../../design/ui-20261009/verification/', import.meta.url);
fs.mkdirSync(root, { recursive: true });
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] });
const page = await browser.newPage({ viewport: { width: 375, height: 726 } });