feat(auth): clarify login identity and speed up mobile entry
This commit is contained in:
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
更新:2026-10-12。当前环境为 Windows 宿主机 + 内网 WSL。
|
更新:2026-10-12。当前环境为 Windows 宿主机 + 内网 WSL。
|
||||||
|
|
||||||
线上状态补充:`https://1me.xajuzi.com/state/` 已部署并配置真实微信凭据,服务器检查记录见 `线上微信登录部署记录-2026-10-11.md`。下文的内网缺配置说明针对本机开发环境。真实微信授权尚待客户端验证。
|
线上状态补充:`https://1me.xajuzi.com/state/` 已部署并配置真实微信凭据,服务器检查记录见 `线上微信登录部署记录-2026-10-11.md`。2026-10-12 用户已确认手机微信登录成功,服务端亦记录成功回调。下文的内网缺配置说明针对本机开发环境。
|
||||||
|
|
||||||
2026-10-12 白名单复测已通过,基础凭据有效。最新修正和真机验证步骤见[微信登录联调记录](微信登录联调-2026-10-12.md)。已有本机用户可从「我的报告」→「切换登录方式」进入微信登录;微信会话失效不会自动恢复为本机账号,两个账号的报告不自动合并。
|
2026-10-12 白名单复测已通过,基础凭据有效。最新修正和真机验证步骤见[微信登录联调记录](微信登录联调-2026-10-12.md)。已有本机用户可从「我的报告」→「切换登录方式」进入微信登录;微信会话失效不会自动恢复为本机账号,两个账号的报告不自动合并。
|
||||||
|
|
||||||
## 结论与当前阻碍
|
## 本机开发环境限制(不代表线上状态)
|
||||||
|
|
||||||
已完成微信网页授权代码,但**没有完成真实微信账号联调**。当前未配置 `WECHAT_APP_ID`、`WECHAT_APP_SECRET`、`PUBLIC_BASE_URL`,也没有可验证的公众号网页授权域名配置。内网 `http://192.168.3.25:8080/` 不能作为本次正式授权部署地址。因此本地启用「本机浏览器登录」,不会模拟微信登录成功。
|
本机未配置生产用 `WECHAT_APP_ID`、`WECHAT_APP_SECRET`、`PUBLIC_BASE_URL`。内网 `http://192.168.3.25:8080/` 不能作为本次正式授权部署地址。因此本地启用「本机浏览器登录」,不会模拟微信登录成功。线上真实登录已通过,详见联调记录。
|
||||||
|
|
||||||
官方网页授权适用于具有相应权限的认证服务号等账号;需要在公众号后台配置网页授权域名。普通小程序 AppID、开放平台网站应用 AppID 不能直接替代本方案的服务号 AppID。本实现采用 `snsapi_base` 获取 OpenID 做账号关联,不请求头像、昵称。
|
官方网页授权适用于具有相应权限的认证服务号等账号;需要在公众号后台配置网页授权域名。普通小程序 AppID、开放平台网站应用 AppID 不能直接替代本方案的服务号 AppID。本实现采用 `snsapi_base` 获取 OpenID 做账号关联,不请求头像、昵称。
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
站点:`https://1me.xajuzi.com/state/`;服务器:`ubuntu@1me.xajuzi.com`。
|
站点:`https://1me.xajuzi.com/state/`;服务器:`ubuntu@1me.xajuzi.com`。
|
||||||
|
|
||||||
|
## 后续真机确认与体验优化
|
||||||
|
|
||||||
|
- 2026-10-12 用户已在手机微信内通过「切换登录方式」完成真实微信登录;生产服务 10:40:31 记录 `login_success`。下文“待真机验证”是此前预检时点的记录,不再代表当前登录状态。跨手机读取仍只有隔离测试覆盖,未声称用户已做跨手机实测。
|
||||||
|
- 新增「登录成功」弹框,说明当前使用微信或本机浏览器;首页右上角、我的报告顶部持续展示登录方式,并可点击切换。恢复有效会话不重复弹框;错误回调、丢失 Cookie 不显示成功。
|
||||||
|
- 登录加载慢的实测原因、优化及验证命令见[登录反馈与加载优化](登录反馈与加载优化-2026-10-12.md)。
|
||||||
|
|
||||||
## 白名单与凭据
|
## 白名单与凭据
|
||||||
|
|
||||||
- 用户再次确认已将服务器出口 IP `49.233.222.47` 加入公众号 IP 白名单。
|
- 用户再次确认已将服务器出口 IP `49.233.222.47` 加入公众号 IP 白名单。
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# 登录反馈与加载优化(2026-10-12)
|
||||||
|
|
||||||
|
## 体验变化
|
||||||
|
|
||||||
|
- 显式本机登录、微信授权成功且会话确认后,显示「登录成功」弹框,说明登录方式和报告访问范围。
|
||||||
|
- 首页右上角和「我的报告」顶部显示「微信已登录」或「本机浏览器已登录」;标识可点击切换。依据服务端会话的 `user.kind`,不根据浏览器是否处于微信来猜测。
|
||||||
|
- 刷新后恢复有效会话不重复弹框。授权失败、Cookie 丢失不会显示成功;不合并账号、不删除缓存身份或历史报告。
|
||||||
|
- 弹框支持确认按钮、Esc、键盘焦点约束,关闭后恢复页面交互。
|
||||||
|
|
||||||
|
## 慢的原因与处理
|
||||||
|
|
||||||
|
优化前,在开发机浏览器对公网页面的一次无缓存测量:HTML 首字节约 244 ms,登录约 5510 ms 可操作;同步排在应用前的 face-api.js 853487 字节、下载约 4319 ms。两份完整字体分别 4858624 和 5034212 字节,下载约 14.5/15.6 秒。额外还预加载了已不使用的旧首页背景(约 590 KB)。认证状态接口独立 curl 测量约 431 ms,不能将本次首屏等待主要归因为微信接口;手机网络、DNS/TLS 和真实授权往返仍会影响实际时间。
|
||||||
|
|
||||||
|
处理:
|
||||||
|
|
||||||
|
1. 人脸引擎改为进入拍照、实际可用相机或系统相机降级时才请求;加载失败可重试,20 秒超时后保留手动兜底。模型/自动抓拍算法不变。
|
||||||
|
2. 登录、首页、报告列表使用系统字体;进入档案/拍照/报告时再注册原始 MiSans 字体。字体原文件保持不变。官方许可不允许改编或二次开发,因此没有对子集化作授权推断。[官方许可](https://hyperos.mi.com/font/zh/download/)。
|
||||||
|
3. 保留 PSD 原导出和 JPEG,新增 WebP 分发背景(同分辨率),移除旧背景预加载,优先加载登录背景。生成方式:`python tools/prepare_h5_assets.py`。
|
||||||
|
4. Node 静态资源增加 gzip 和 ETag/304,继续 `Cache-Control: no-cache` 及时校验新版本;仅压缩静态公共文本,不缓存/压缩账号、报告等私有 API 响应。未改 nginx 后台保护配置。
|
||||||
|
5. 微信登录去掉多余的配置查询往返,直接请求受协议及同源校验保护的授权接口;禁用环境仍由服务端明确拒绝。仅登录鉴权请求增加 12 秒超时,失败后不清除身份凭据;不缩短含照片报告的下载等待时间。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
H5_ARTIFACT_DIR=/tmp/juzi-login-20261012 node h5/test/login-performance.mjs
|
||||||
|
node h5/test/wechat-browser.mjs
|
||||||
|
node h5/test/accounts.js
|
||||||
|
node h5/test/report-persistence.mjs
|
||||||
|
node h5/test/recommendation.js
|
||||||
|
node h5/test/analyze-timeout.js
|
||||||
|
H5_ARTIFACT_DIR=/tmp/juzi-login-ui-20261012 node h5/test/ui-20261009.mjs
|
||||||
|
node h5/test/browser-failures.mjs
|
||||||
|
node h5/test/e2e-browser.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
隔离性能测试:150 ms 网络延迟、约 2 Mbps 下载、CPU 四倍降速,冷登录可操作约 1598 ms,静态资源编码后共 259547 字节;登录、首页不请求完整字体、人脸引擎或模型。另验证 gzip 解码一致、304、拒绝 `gzip;q=0`、私有 API `no-store`、超时保留身份、320/375/390/480 宽度标识不遮挡文案。拍照时引擎失败重试后,真实模型能返回 68 点人脸特征。
|
||||||
|
|
||||||
|
以上是可重复的开发机模拟结果,不等于每部手机的耗时保证,也不能直接与未限速的公网测量作同条件性能比例比较。测试不调用收费 Face++。
|
||||||
@@ -3,24 +3,24 @@
|
|||||||
"1": "线上闭环测试(不含真实微信授权)",
|
"1": "线上闭环测试(不含真实微信授权)",
|
||||||
"2": "app.js",
|
"2": "app.js",
|
||||||
"3": "skin-score.js",
|
"3": "skin-score.js",
|
||||||
"4": "i",
|
"4": "k",
|
||||||
"5": "r",
|
"5": "r",
|
||||||
"6": ".runAndExtendWithFaceDetections",
|
"6": "extractFaceTensors",
|
||||||
"7": "Box",
|
"7": "ComputeSingleFaceDescriptorTask",
|
||||||
"8": "admin.js",
|
"8": "admin.js",
|
||||||
"9": "__awaiter",
|
"9": "__awaiter",
|
||||||
"10": "TinyYolov2Base",
|
"10": ".forwardInput",
|
||||||
"11": "lib/accounts.js",
|
"11": "lib/accounts.js",
|
||||||
"12": "server.js",
|
"12": "server.js",
|
||||||
"13": "files",
|
"13": "files",
|
||||||
"14": "FaceLandmarks",
|
"14": "AgeGenderNet",
|
||||||
"15": ".runAndExtendWithFaceDetection",
|
"15": ".runAndExtendWithFaceDetections",
|
||||||
"16": "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范",
|
"16": "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范",
|
||||||
"17": "extractFaces",
|
"17": "extractFaces",
|
||||||
"18": "1'me 在线肌肤测试 H5",
|
"18": "1'me 在线肌肤测试 H5",
|
||||||
"19": "head-pose.js",
|
"19": "head-pose.js",
|
||||||
"20": "build_fixtures.py",
|
"20": "build_fixtures.py",
|
||||||
"21": "AgeGenderNet",
|
"21": "disposeUnusedWeightTensors",
|
||||||
"22": ".loadFromDisk",
|
"22": ".loadFromDisk",
|
||||||
"23": "facepp-skinanalyze(旷视皮肤分析专业版调用技能)",
|
"23": "facepp-skinanalyze(旷视皮肤分析专业版调用技能)",
|
||||||
"24": "巨子测肤 · 1'me 语音播报内容设计文档 V1.0",
|
"24": "巨子测肤 · 1'me 语音播报内容设计文档 V1.0",
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
"40": ".predictAgeAndGender",
|
"40": ".predictAgeAndGender",
|
||||||
"41": "test/accounts.js",
|
"41": "test/accounts.js",
|
||||||
"42": "export_psd_assets.py",
|
"42": "export_psd_assets.py",
|
||||||
"43": ".toString",
|
"43": ".toBatchTensor",
|
||||||
"44": "start-local.sh",
|
"44": "start-local.sh",
|
||||||
"45": "PredictAllAgeAndGenderWithFaceAlignmentTask",
|
"45": "PredictAllAgeAndGenderWithFaceAlignmentTask",
|
||||||
"46": "PredictAllFaceExpressionsWithFaceAlignmentTask",
|
"46": "PredictAllFaceExpressionsWithFaceAlignmentTask",
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
"52": "region-data.js",
|
"52": "region-data.js",
|
||||||
"53": "护理建议(全量文本,按需求摘抄)",
|
"53": "护理建议(全量文本,按需求摘抄)",
|
||||||
"54": "UI 对齐 10.09 设计稿:调研与实施计划",
|
"54": "UI 对齐 10.09 设计稿:调研与实施计划",
|
||||||
"55": "pn",
|
"55": "i",
|
||||||
"56": "report-ui.js",
|
"56": "report-ui.js",
|
||||||
"57": "regression.js",
|
"57": "regression.js",
|
||||||
"58": ".dispose",
|
"58": ".dispose",
|
||||||
@@ -78,14 +78,18 @@
|
|||||||
"76": "TinyYolov2",
|
"76": "TinyYolov2",
|
||||||
"77": "drawFaceExpressions",
|
"77": "drawFaceExpressions",
|
||||||
"78": "n",
|
"78": "n",
|
||||||
"79": "isValidNumber",
|
"79": "微信登录联调-2026-10-12.md",
|
||||||
"80": "线上微信登录部署记录",
|
"80": "线上微信登录部署记录",
|
||||||
"81": "wechat-production.mjs",
|
"81": "wechat-production.mjs",
|
||||||
"82": "ComputeAllFaceDescriptorsTask",
|
"82": "ComputeAllFaceDescriptorsTask",
|
||||||
"83": "10.09 新版 H5 实施与验收",
|
"83": "10.09 新版 H5 实施与验收",
|
||||||
"84": "微信登录联调:2026-10-12",
|
"84": "微信登录联调:2026-10-12",
|
||||||
|
"85": "FaceLandmarks",
|
||||||
"87": "产品推荐规则:肤龄基准与边界调整",
|
"87": "产品推荐规则:肤龄基准与边界调整",
|
||||||
"92": "部署",
|
"91": "login-performance.mjs",
|
||||||
|
"94": "Box",
|
||||||
|
"95": "PredictAllFaceExpressionsTask",
|
||||||
"97": "PredictSingleAgeAndGenderWithFaceAlignmentTask",
|
"97": "PredictSingleAgeAndGenderWithFaceAlignmentTask",
|
||||||
"98": "PredictSingleFaceExpressionsWithFaceAlignmentTask"
|
"98": "PredictSingleFaceExpressionsWithFaceAlignmentTask",
|
||||||
|
"99": "PredictSingleFaceExpressionsTask"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"0": "4e953aa5accd7c7a", "1": "cb6ea50bd4e4a5ca", "2": "9cf893b45c314643", "3": "1f737ca8edb978f2", "4": "5f6112d5f635312b", "5": "eef22a8f3d286cb0", "6": "435ca504c453b5ca", "7": "cd3c9a7feaf8faee", "8": "1ecf47531e1d507b", "9": "abb09e22736891ca", "10": "b78b7363e38a9a47", "11": "4a490abd5e503318", "12": "d3e946c5907aa577", "13": "7e3b558a587a4ebe", "14": "b3282012327aedbb", "15": "8bd61518dd267641", "16": "95add6a975c0cb72", "17": "fceb16a66797d550", "18": "9dec2afb5aedd74b", "19": "0a373ad2197fd1a6", "20": "aecf2fce44ba49c5", "21": "e6363b2654e6584b", "22": "caf0f067881ef97e", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "83b0fd3b14363e4d", "27": "eab427ce9a5219e9", "28": "3c28e8d4be0a55e3", "29": "c608f1c6492e1cb1", "30": "73c13793fca09f56", "31": "fbbcfc059751cc6a", "32": "f6bcf56c4589316b", "33": "75f750c5b7d9f63d", "34": "f024ff7942935690", "35": "23f1d406f6c779ae", "36": "a3859fc25c864a81", "37": "cb8f26249485324a", "38": "9a25b1230b5fe649", "39": "fdd95375b4a5ddb3", "40": "a2b13160223b5a98", "41": "4bc370be8afd7db1", "42": "ffd960136fde00ad", "43": "15d6968c6dfb2e19", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "e594ecaa8e7d1538", "48": "e042d7fe7351bcb9", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "1ac1eedae197df20", "56": "91d7ab49459e0b92", "57": "f2a40b5f1b29f669", "58": "36e826942fb5c839", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "ed382bc83b2567aa", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "ce0e68dab611f022", "65": "6281db8274251503", "66": "769f05ad8584c01f", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "bf75bfc99d4f9e34", "71": "4195478d5c528371", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "c96276581a8c98d4", "77": "979dcc63399620ad", "78": "9c8639405659c6ae", "79": "1c27e5f448dc4f9c", "80": "315325cde1352e98", "81": "37c559741825be74", "82": "3dd68cce72af5314", "83": "1284897282eb47a3", "84": "3f747b286aec1e85", "87": "e5fa7717989eda80", "92": "7dc1445ea84a39a8", "97": "01f68dfc34a29d32", "98": "8c471c7baeaaca54"}
|
{"0": "bf880d08a452d7b9", "1": "cb6ea50bd4e4a5ca", "2": "e45f02a2d81f378b", "3": "1f737ca8edb978f2", "4": "68c8c4a53446cb29", "5": "d50faa21d10efe9c", "6": "21004e7ec716999e", "7": "7ac475ea9bc280f8", "8": "1ecf47531e1d507b", "9": "bb9def5316182bbf", "10": "880a74c6d255e5cc", "11": "4a490abd5e503318", "12": "53594b653ad48743", "13": "7e3b558a587a4ebe", "14": "d982c7c094e0d01c", "15": "4184c65898b38dd8", "16": "95add6a975c0cb72", "17": "d522c79a290badbf", "18": "38bb0b185ceed116", "19": "0a373ad2197fd1a6", "20": "aecf2fce44ba49c5", "21": "8835aeeaa165e9f9", "22": "af8f2189aee7bcc9", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "83b0fd3b14363e4d", "27": "eab427ce9a5219e9", "28": "08e85dab33772c23", "29": "c608f1c6492e1cb1", "30": "73c13793fca09f56", "31": "fbbcfc059751cc6a", "32": "f6bcf56c4589316b", "33": "75f750c5b7d9f63d", "34": "f024ff7942935690", "35": "23f1d406f6c779ae", "36": "a3859fc25c864a81", "37": "cb8f26249485324a", "38": "9a25b1230b5fe649", "39": "fdd95375b4a5ddb3", "40": "a2b13160223b5a98", "41": "4bc370be8afd7db1", "42": "ffd960136fde00ad", "43": "b434f4a4ecab270c", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "e594ecaa8e7d1538", "48": "e042d7fe7351bcb9", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "ac5257538f52da80", "56": "91d7ab49459e0b92", "57": "f2a40b5f1b29f669", "58": "16bec50ccad83d20", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "5f4ba3be5e4fe3af", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "ce0e68dab611f022", "65": "74ae017de0ffd36b", "66": "2b037735e1f5b0f6", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "bf75bfc99d4f9e34", "71": "4195478d5c528371", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "c96276581a8c98d4", "77": "d5ce16a18b491627", "78": "bff404f6e2fa3713", "79": "f31ac836426a58b6", "80": "315325cde1352e98", "81": "37c559741825be74", "82": "3dd68cce72af5314", "83": "1284897282eb47a3", "84": "d8bb296741d3d05c", "85": "1d30b164aa092dc6", "87": "e5fa7717989eda80", "91": "59c31f858c7b6970", "94": "c6eac02ad7178f8e", "95": "e800d832c255899a", "97": "01f68dfc34a29d32", "98": "8c471c7baeaaca54", "99": "cb5ac3401283c378"}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
# Graph Report - juzi (2026-10-12)
|
# Graph Report - juzi (2026-10-12)
|
||||||
|
|
||||||
## Corpus Check
|
## Corpus Check
|
||||||
- 68 files · ~4,185,405 words
|
- 70 files · ~4,212,892 words
|
||||||
- Verdict: corpus is large enough that graph structure adds value.
|
- Verdict: corpus is large enough that graph structure adds value.
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
- 1204 nodes · 2589 edges · 89 communities (72 shown, 17 thin omitted)
|
- 1222 nodes · 2613 edges · 93 communities (72 shown, 21 thin omitted)
|
||||||
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 179 edges (avg confidence: 0.53)
|
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 180 edges (avg confidence: 0.53)
|
||||||
- Token cost: 0 input · 0 output
|
- Token cost: 0 input · 0 output
|
||||||
|
|
||||||
## Graph Freshness
|
## Graph Freshness
|
||||||
- Built from commit: `0ec5e5dc`
|
- Built from commit: `85790ef9`
|
||||||
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
||||||
- Run `graphify update .` after code changes (no API cost).
|
- Run `graphify update .` after code changes (no API cost).
|
||||||
|
|
||||||
@@ -19,24 +19,24 @@
|
|||||||
- 线上闭环测试(不含真实微信授权)
|
- 线上闭环测试(不含真实微信授权)
|
||||||
- app.js
|
- app.js
|
||||||
- skin-score.js
|
- skin-score.js
|
||||||
- i
|
- k
|
||||||
- r
|
- r
|
||||||
- .runAndExtendWithFaceDetections
|
- extractFaceTensors
|
||||||
- Box
|
- ComputeSingleFaceDescriptorTask
|
||||||
- admin.js
|
- admin.js
|
||||||
- __awaiter
|
- __awaiter
|
||||||
- TinyYolov2Base
|
- .forwardInput
|
||||||
- lib/accounts.js
|
- lib/accounts.js
|
||||||
- server.js
|
- server.js
|
||||||
- files
|
- files
|
||||||
- FaceLandmarks
|
- AgeGenderNet
|
||||||
- .runAndExtendWithFaceDetection
|
- .runAndExtendWithFaceDetections
|
||||||
- 巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范
|
- 巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范
|
||||||
- extractFaces
|
- extractFaces
|
||||||
- 1'me 在线肌肤测试 H5
|
- 1'me 在线肌肤测试 H5
|
||||||
- head-pose.js
|
- head-pose.js
|
||||||
- build_fixtures.py
|
- build_fixtures.py
|
||||||
- AgeGenderNet
|
- disposeUnusedWeightTensors
|
||||||
- .loadFromDisk
|
- .loadFromDisk
|
||||||
- facepp-skinanalyze(旷视皮肤分析专业版调用技能)
|
- facepp-skinanalyze(旷视皮肤分析专业版调用技能)
|
||||||
- 巨子测肤 · 1'me 语音播报内容设计文档 V1.0
|
- 巨子测肤 · 1'me 语音播报内容设计文档 V1.0
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
- .predictAgeAndGender
|
- .predictAgeAndGender
|
||||||
- test/accounts.js
|
- test/accounts.js
|
||||||
- export_psd_assets.py
|
- export_psd_assets.py
|
||||||
- .toString
|
- .toBatchTensor
|
||||||
- start-local.sh
|
- start-local.sh
|
||||||
- PredictAllAgeAndGenderWithFaceAlignmentTask
|
- PredictAllAgeAndGenderWithFaceAlignmentTask
|
||||||
- PredictAllFaceExpressionsWithFaceAlignmentTask
|
- PredictAllFaceExpressionsWithFaceAlignmentTask
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
- call_skinanalyze_3view.sh
|
- call_skinanalyze_3view.sh
|
||||||
- 护理建议(全量文本,按需求摘抄)
|
- 护理建议(全量文本,按需求摘抄)
|
||||||
- UI 对齐 10.09 设计稿:调研与实施计划
|
- UI 对齐 10.09 设计稿:调研与实施计划
|
||||||
- pn
|
- i
|
||||||
- report-ui.js
|
- report-ui.js
|
||||||
- regression.js
|
- regression.js
|
||||||
- .dispose
|
- .dispose
|
||||||
@@ -87,16 +87,20 @@
|
|||||||
- TinyYolov2
|
- TinyYolov2
|
||||||
- drawFaceExpressions
|
- drawFaceExpressions
|
||||||
- n
|
- n
|
||||||
- isValidNumber
|
- 微信登录联调-2026-10-12.md
|
||||||
- 线上微信登录部署记录
|
- 线上微信登录部署记录
|
||||||
- wechat-production.mjs
|
- wechat-production.mjs
|
||||||
- ComputeAllFaceDescriptorsTask
|
- ComputeAllFaceDescriptorsTask
|
||||||
- 10.09 新版 H5 实施与验收
|
- 10.09 新版 H5 实施与验收
|
||||||
- 微信登录联调:2026-10-12
|
- 微信登录联调:2026-10-12
|
||||||
|
- FaceLandmarks
|
||||||
- 产品推荐规则:肤龄基准与边界调整
|
- 产品推荐规则:肤龄基准与边界调整
|
||||||
- 部署
|
- login-performance.mjs
|
||||||
|
- Box
|
||||||
|
- PredictAllFaceExpressionsTask
|
||||||
- PredictSingleAgeAndGenderWithFaceAlignmentTask
|
- PredictSingleAgeAndGenderWithFaceAlignmentTask
|
||||||
- PredictSingleFaceExpressionsWithFaceAlignmentTask
|
- PredictSingleFaceExpressionsWithFaceAlignmentTask
|
||||||
|
- PredictSingleFaceExpressionsTask
|
||||||
|
|
||||||
## God Nodes (most connected - your core abstractions)
|
## God Nodes (most connected - your core abstractions)
|
||||||
1. `__awaiter()` - 59 edges
|
1. `__awaiter()` - 59 edges
|
||||||
@@ -125,51 +129,47 @@
|
|||||||
## Import Cycles
|
## Import Cycles
|
||||||
- None detected.
|
- None detected.
|
||||||
|
|
||||||
## Communities (89 total, 17 thin omitted)
|
## Communities (93 total, 21 thin omitted)
|
||||||
|
|
||||||
### Community 0 - "face-api.js"
|
### Community 0 - "face-api.js"
|
||||||
Cohesion: 0.02
|
Cohesion: 0.02
|
||||||
Nodes (32): cn(), computeReshapedDimensions(), createBrowserEnv(), createFileSystem(), createNodejsEnv(), dr(), en(), extractBoundingBoxes() (+24 more)
|
Nodes (29): cn(), createBrowserEnv(), createFileSystem(), createNodejsEnv(), dr(), en(), FaceDetection(), ff() (+21 more)
|
||||||
|
|
||||||
### Community 1 - "线上闭环测试(不含真实微信授权)"
|
### Community 1 - "线上闭环测试(不含真实微信授权)"
|
||||||
Cohesion: 0.25
|
Cohesion: 0.25
|
||||||
Nodes (7): 发现并修复, 可复现命令, 已通过的检查, 线上闭环测试(不含真实微信授权), 结论, 话术及仓库同步, 验证边界
|
Nodes (7): 发现并修复, 可复现命令, 已通过的检查, 线上闭环测试(不含真实微信授权), 结论, 话术及仓库同步, 验证边界
|
||||||
|
|
||||||
### Community 2 - "app.js"
|
### Community 2 - "app.js"
|
||||||
Cohesion: 0.06
|
Cohesion: 0.05
|
||||||
Nodes (74): applyMirror(), armNudge(), beginDetect(), bindBackdropClose(), busyWithFlow(), captureFromVideo(), checkStill(), closeOverlay() (+66 more)
|
Nodes (79): applyMirror(), armNudge(), beginDetect(), bindBackdropClose(), busyWithFlow(), captureFromVideo(), checkStill(), closeLoginSuccess() (+71 more)
|
||||||
|
|
||||||
### Community 3 - "skin-score.js"
|
### Community 3 - "skin-score.js"
|
||||||
Cohesion: 0.12
|
Cohesion: 0.12
|
||||||
Nodes (45): acneGrade(), buildViewModel(), calcAcne(), calcContour(), calcSens(), calcSmooth(), calcTone(), calcWrinkles() (+37 more)
|
Nodes (45): acneGrade(), buildViewModel(), calcAcne(), calcContour(), calcSens(), calcSmooth(), calcTone(), calcWrinkles() (+37 more)
|
||||||
|
|
||||||
### Community 4 - "i"
|
### Community 4 - "k"
|
||||||
Cohesion: 0.09
|
Cohesion: 0.15
|
||||||
Nodes (27): b(), ba(), ca(), da(), dn(), ee(), et(), fa() (+19 more)
|
Nodes (15): b(), ba(), ca(), da(), fa(), k(), ne(), pa() (+7 more)
|
||||||
|
|
||||||
### Community 5 - "r"
|
### Community 5 - "r"
|
||||||
Cohesion: 0.15
|
Cohesion: 0.14
|
||||||
Nodes (34): a(), bf(), cf(), d(), drawContour(), dt(), e(), ea() (+26 more)
|
Nodes (34): a(), ae(), bf(), cf(), d(), drawContour(), dt(), ea() (+26 more)
|
||||||
|
|
||||||
### Community 6 - ".runAndExtendWithFaceDetections"
|
### Community 6 - "extractFaceTensors"
|
||||||
Cohesion: 0.33
|
Cohesion: 0.25
|
||||||
Nodes (5): DetectAllFacesTask(), extendWithFaceDetection(), isWithFaceDetection(), isWithFaceLandmarks(), resizeResults()
|
Nodes (4): DetectAllFaceLandmarksTask(), extractFaceTensors(), isTensor3D(), isTensor4D()
|
||||||
|
|
||||||
### Community 7 - "Box"
|
|
||||||
Cohesion: 0.16
|
|
||||||
Nodes (3): Box(), ComputeSingleFaceDescriptorTask(), extendWithFaceDescriptor()
|
|
||||||
|
|
||||||
### Community 8 - "admin.js"
|
### Community 8 - "admin.js"
|
||||||
Cohesion: 0.21
|
Cohesion: 0.21
|
||||||
Nodes (28): ageText(), api(), bind(), closeDetail(), collectFilters(), dimsHtml(), doRetry(), dSec() (+20 more)
|
Nodes (28): ageText(), api(), bind(), closeDetail(), collectFilters(), dimsHtml(), doRetry(), dSec() (+20 more)
|
||||||
|
|
||||||
### Community 9 - "__awaiter"
|
### Community 9 - "__awaiter"
|
||||||
Cohesion: 0.11
|
Cohesion: 0.14
|
||||||
Nodes (15): __awaiter(), bufferToImage(), ComposableTask(), extendWithFaceExpressions(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult(), FaceRecognitionNet(), fetchImage() (+7 more)
|
Nodes (14): __awaiter(), extendWithFaceDescriptor(), extendWithFaceExpressions(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult(), FaceRecognitionNet(), fetchImage(), fetchJson() (+6 more)
|
||||||
|
|
||||||
### Community 10 - "TinyYolov2Base"
|
### Community 10 - ".forwardInput"
|
||||||
Cohesion: 0.10
|
Cohesion: 0.22
|
||||||
Nodes (22): boxPredictionLayer(), convLayer(), convWithBatchNorm(), decodeBoxesLayer(), depthwiseConvLayer(), depthwiseSeparableConv$1(), fullyConnectedLayer(), getCenterCoordinatesAndSizesLayer() (+14 more)
|
Nodes (9): boxPredictionLayer(), decodeBoxesLayer(), depthwiseConvLayer(), getCenterCoordinatesAndSizesLayer(), getStridesForLayerIdx(), mobileNetV1(), outputLayer(), pointwiseConvLayer() (+1 more)
|
||||||
|
|
||||||
### Community 11 - "lib/accounts.js"
|
### Community 11 - "lib/accounts.js"
|
||||||
Cohesion: 0.17
|
Cohesion: 0.17
|
||||||
@@ -177,31 +177,31 @@ Nodes (20): atomic(), body(), cookie(), cookies(), crypto, fs, json(), login() (
|
|||||||
|
|
||||||
### Community 12 - "server.js"
|
### Community 12 - "server.js"
|
||||||
Cohesion: 0.08
|
Cohesion: 0.08
|
||||||
Nodes (49): accounts, appendRecord(), callFacepp(), CRED, csvCell(), DATA_DIR, displaySkinAge(), dumpRaw() (+41 more)
|
Nodes (51): accounts, appendRecord(), callFacepp(), CRED, crypto, csvCell(), DATA_DIR, displaySkinAge() (+43 more)
|
||||||
|
|
||||||
### Community 13 - "files"
|
### Community 13 - "files"
|
||||||
Cohesion: 0.12
|
Cohesion: 0.12
|
||||||
Nodes (15): files, analyze-fail, analyzing, countdown-321, hint-far, hint-front, hint-hold, hint-near (+7 more)
|
Nodes (15): files, analyze-fail, analyzing, countdown-321, hint-far, hint-front, hint-hold, hint-near (+7 more)
|
||||||
|
|
||||||
### Community 14 - "FaceLandmarks"
|
### Community 14 - "AgeGenderNet"
|
||||||
Cohesion: 0.05
|
Cohesion: 0.10
|
||||||
Nodes (25): conv(), conv$1(), convDown(), convLayer$1(), convNoRelu(), denseBlock3(), denseBlock4(), depthwiseSeparableConv() (+17 more)
|
Nodes (6): AgeGenderNet(), extractParams$1(), FaceExpressionNet(), FaceLandmark68NetBase(), FaceLandmark68TinyNet(), FaceProcessor()
|
||||||
|
|
||||||
### Community 15 - ".runAndExtendWithFaceDetection"
|
### Community 15 - ".runAndExtendWithFaceDetections"
|
||||||
Cohesion: 0.23
|
Cohesion: 0.18
|
||||||
Nodes (6): allFacesMtcnn(), allFacesSsdMobilenetv1(), allFacesTinyYolov2(), DetectAllFaceLandmarksTask(), detectAllFaces(), DetectSingleFaceTask()
|
Nodes (8): allFacesMtcnn(), allFacesSsdMobilenetv1(), allFacesTinyYolov2(), detectAllFaces(), DetectAllFacesTask(), DetectSingleFaceTask(), extendWithFaceDetection(), resizeResults()
|
||||||
|
|
||||||
### Community 16 - "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范"
|
### Community 16 - "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范"
|
||||||
Cohesion: 0.15
|
Cohesion: 0.15
|
||||||
Nodes (12): 1. 基准与尺寸换算, 2. 色彩规范, 3. 字体规范, 4. 组件切图(assets/svg/), 5. 图片素材(assets/images/,共 15 张), 6. 页面清单(design/ 下有整屏 PNG 预览), 7. 目录结构, 8. 还原建议 (+4 more)
|
Nodes (12): 1. 基准与尺寸换算, 2. 色彩规范, 3. 字体规范, 4. 组件切图(assets/svg/), 5. 图片素材(assets/images/,共 15 张), 6. 页面清单(design/ 下有整屏 PNG 预览), 7. 目录结构, 8. 还原建议 (+4 more)
|
||||||
|
|
||||||
### Community 17 - "extractFaces"
|
### Community 17 - "extractFaces"
|
||||||
Cohesion: 0.17
|
Cohesion: 0.25
|
||||||
Nodes (20): awaitMediaLoaded(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractFaceTensors(), extractImagePatches(), getContext2dOrThrow(), getEnv() (+12 more)
|
Nodes (16): awaitMediaLoaded(), bufferToImage(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractImagePatches(), getContext2dOrThrow(), getEnv() (+8 more)
|
||||||
|
|
||||||
### Community 18 - "1'me 在线肌肤测试 H5"
|
### Community 18 - "1'me 在线肌肤测试 H5"
|
||||||
Cohesion: 0.17
|
Cohesion: 0.13
|
||||||
Nodes (12): 10.09 更新, 1'me 在线肌肤测试 H5, Admin API(统一返回 `{ok:false,message}` 错误), 后台查看(admin.html), 密钥配置, 已知占位清单, 技术说明, 拍照交互(动态捕捉自动三连拍) (+4 more)
|
Nodes (15): 10.09 更新, 1'me 在线肌肤测试 H5, Admin API(统一返回 `{ok:false,message}` 错误), Windows 本机部署(提测用), 后台查看(admin.html), 密钥配置, 已知占位清单, 技术说明 (+7 more)
|
||||||
|
|
||||||
### Community 19 - "head-pose.js"
|
### Community 19 - "head-pose.js"
|
||||||
Cohesion: 0.33
|
Cohesion: 0.33
|
||||||
@@ -211,13 +211,13 @@ Nodes (8): assertPoints(), check(), clamp(), dist(), estimate(), makeFrontal(),
|
|||||||
Cohesion: 0.40
|
Cohesion: 0.40
|
||||||
Nodes (9): build_case1(), build_case2(), build_truth(), col(), dump(), main(), parse_value(), set_path() (+1 more)
|
Nodes (9): build_case1(), build_case2(), build_truth(), col(), dump(), main(), parse_value(), set_path() (+1 more)
|
||||||
|
|
||||||
### Community 21 - "AgeGenderNet"
|
### Community 21 - "disposeUnusedWeightTensors"
|
||||||
Cohesion: 0.05
|
Cohesion: 0.09
|
||||||
Nodes (28): AgeGenderNet(), disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1() (+20 more)
|
Nodes (24): disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1(), extractParamsFromWeigthMap$2() (+16 more)
|
||||||
|
|
||||||
### Community 22 - ".loadFromDisk"
|
### Community 22 - ".loadFromDisk"
|
||||||
Cohesion: 0.24
|
Cohesion: 0.22
|
||||||
Nodes (4): fetchJson(), getModelUris(), loadWeightMap(), TinyFaceDetector()
|
Nodes (4): ComposableTask(), getModelUris(), Gf(), TinyFaceDetector()
|
||||||
|
|
||||||
### Community 23 - "facepp-skinanalyze(旷视皮肤分析专业版调用技能)"
|
### Community 23 - "facepp-skinanalyze(旷视皮肤分析专业版调用技能)"
|
||||||
Cohesion: 0.20
|
Cohesion: 0.20
|
||||||
@@ -240,8 +240,8 @@ Cohesion: 0.30
|
|||||||
Nodes (11): ndarray, Path, gen_countdown(), main(), normalize_clip(), 倒计时单独按字生成再拼接,避免「三、二、一」整句被读成播报腔、与提示音气质漂移。 与提示音共用同一 speaker + instruct;句间留约…, 裁掉过长首尾静音,保留 ≤150ms 边沿,避免提示显迟钝。, 响度归一 −16 LUFS + 转 MP3 48k CBR mono 22050Hz。 (+3 more)
|
Nodes (11): ndarray, Path, gen_countdown(), main(), normalize_clip(), 倒计时单独按字生成再拼接,避免「三、二、一」整句被读成播报腔、与提示音气质漂移。 与提示音共用同一 speaker + instruct;句间留约…, 裁掉过长首尾静音,保留 ≤150ms 边沿,避免提示显迟钝。, 响度归一 −16 LUFS + 转 MP3 48k CBR mono 22050Hz。 (+3 more)
|
||||||
|
|
||||||
### Community 28 - "C"
|
### Community 28 - "C"
|
||||||
Cohesion: 0.10
|
Cohesion: 0.12
|
||||||
Nodes (30): Bo(), C(), cd(), cl(), co(), eo(), fo(), Ft() (+22 more)
|
Nodes (28): Bo(), C(), cd(), cl(), co(), e(), eo(), fo() (+20 more)
|
||||||
|
|
||||||
### Community 29 - "face-api.js 本地化依赖(h5/vendor/faceapi/)"
|
### Community 29 - "face-api.js 本地化依赖(h5/vendor/faceapi/)"
|
||||||
Cohesion: 0.29
|
Cohesion: 0.29
|
||||||
@@ -279,9 +279,9 @@ Nodes (9): assert, create, fs, http, os, path, ReportUI, SkinScore (+1 more)
|
|||||||
Cohesion: 0.70
|
Cohesion: 0.70
|
||||||
Nodes (4): bounds(), layer_at(), main(), save_image()
|
Nodes (4): bounds(), layer_at(), main(), save_image()
|
||||||
|
|
||||||
### Community 43 - ".toString"
|
### Community 43 - ".toBatchTensor"
|
||||||
Cohesion: 0.29
|
Cohesion: 0.09
|
||||||
Nodes (6): ae(), FaceMatch(), la(), pr(), pt(), vt()
|
Nodes (28): conv(), conv$1(), convDown(), convLayer(), convLayer$1(), convNoRelu(), convWithBatchNorm(), denseBlock3() (+20 more)
|
||||||
|
|
||||||
### Community 44 - "start-local.sh"
|
### Community 44 - "start-local.sh"
|
||||||
Cohesion: 0.50
|
Cohesion: 0.50
|
||||||
@@ -303,9 +303,9 @@ Nodes (19): 5\.1 轮廓(紧塑力), 5\.2 皱纹(紧塑力), 5\.3 肤色
|
|||||||
Cohesion: 0.11
|
Cohesion: 0.11
|
||||||
Nodes (17): 1. 结论和依据, 2. 统一比较口径, 3. 逐页差异和调整内容, 4. 报告数据如何接到新 UI, 5. 登录与“我的报告”的真实能力缺口, 6. 原稿中需要整理的内容, 7. 实施顺序与交付物, 8. 验收定义 (+9 more)
|
Nodes (17): 1. 结论和依据, 2. 统一比较口径, 3. 逐页差异和调整内容, 4. 报告数据如何接到新 UI, 5. 登录与“我的报告”的真实能力缺口, 6. 原稿中需要整理的内容, 7. 实施顺序与交付物, 8. 验收定义 (+9 more)
|
||||||
|
|
||||||
### Community 55 - "pn"
|
### Community 55 - "i"
|
||||||
Cohesion: 0.29
|
Cohesion: 0.10
|
||||||
Nodes (7): bn(), fn(), ln(), pn(), un(), wn(), xn()
|
Nodes (21): bn(), dn(), ee(), et(), fn(), i(), je(), ln() (+13 more)
|
||||||
|
|
||||||
### Community 56 - "report-ui.js"
|
### Community 56 - "report-ui.js"
|
||||||
Cohesion: 0.90
|
Cohesion: 0.90
|
||||||
@@ -316,12 +316,12 @@ Cohesion: 0.60
|
|||||||
Nodes (5): diffCtx(), loadJSON(), main(), okNum(), okStr()
|
Nodes (5): diffCtx(), loadJSON(), main(), okNum(), okStr()
|
||||||
|
|
||||||
### Community 58 - ".dispose"
|
### Community 58 - ".dispose"
|
||||||
Cohesion: 0.17
|
Cohesion: 0.14
|
||||||
Nodes (12): extendWithFaceLandmarks(), getSizesForScale(), iou(), nonMaxSuppression(), nonMaxSuppression$1(), normalize$1(), ObjectDetection(), rescaleAndNormalize() (+4 more)
|
Nodes (17): computeReshapedDimensions(), extractBoundingBoxes(), getSizesForScale(), iou(), isEven(), NetInput(), nonMaxSuppression(), nonMaxSuppression$1() (+9 more)
|
||||||
|
|
||||||
### Community 61 - "NeuralNetwork"
|
### Community 61 - "NeuralNetwork"
|
||||||
Cohesion: 0.07
|
Cohesion: 0.06
|
||||||
Nodes (26): createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory(), extractFCParamsFactory() (+18 more)
|
Nodes (28): createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory(), extractFCParamsFactory() (+20 more)
|
||||||
|
|
||||||
### Community 62 - "提交前 Review(2026-10-11)"
|
### Community 62 - "提交前 Review(2026-10-11)"
|
||||||
Cohesion: 0.40
|
Cohesion: 0.40
|
||||||
@@ -337,10 +337,10 @@ Nodes (5): browser(), init(), read(), rememberKind(), request()
|
|||||||
|
|
||||||
### Community 65 - "微信登录接入与公网部署说明"
|
### Community 65 - "微信登录接入与公网部署说明"
|
||||||
Cohesion: 0.22
|
Cohesion: 0.22
|
||||||
Nodes (7): 公网服务器配置, 公网真机验收清单, 反向代理与后台保护, 已完成的链路, 微信登录接入与公网部署说明, 数据、迁移与备份, 结论与当前阻碍
|
Nodes (7): 公网服务器配置, 公网真机验收清单, 反向代理与后台保护, 已完成的链路, 微信登录接入与公网部署说明, 数据、迁移与备份, 本机开发环境限制(不代表线上状态)
|
||||||
|
|
||||||
### Community 66 - "wechat-browser.mjs"
|
### Community 66 - "wechat-browser.mjs"
|
||||||
Cohesion: 0.25
|
Cohesion: 0.22
|
||||||
Nodes (6): createAccounts, h5, ReportUI, require, root, SkinScore
|
Nodes (6): createAccounts, h5, ReportUI, require, root, SkinScore
|
||||||
|
|
||||||
### Community 68 - "analyze-timeout.js"
|
### Community 68 - "analyze-timeout.js"
|
||||||
@@ -353,15 +353,15 @@ Nodes (3): artifacts, errors, samples
|
|||||||
|
|
||||||
### Community 77 - "drawFaceExpressions"
|
### Community 77 - "drawFaceExpressions"
|
||||||
Cohesion: 0.22
|
Cohesion: 0.22
|
||||||
Nodes (6): DrawBox(), drawDetections(), drawFaceExpressions(), DrawFaceLandmarks(), FaceExpressions(), isWithFaceExpressions()
|
Nodes (8): DrawBox(), drawDetections(), drawFaceExpressions(), DrawFaceLandmarks(), FaceExpressions(), isWithFaceDetection(), isWithFaceExpressions(), isWithFaceLandmarks()
|
||||||
|
|
||||||
### Community 78 - "n"
|
### Community 78 - "n"
|
||||||
Cohesion: 0.11
|
Cohesion: 0.10
|
||||||
Nodes (29): be(), bi(), ce(), de(), Ef(), er(), fe(), Ie() (+21 more)
|
Nodes (31): be(), bi(), ce(), de(), Ef(), er(), fe(), Ie() (+23 more)
|
||||||
|
|
||||||
### Community 79 - "isValidNumber"
|
### Community 79 - "微信登录联调-2026-10-12.md"
|
||||||
Cohesion: 0.33
|
Cohesion: 0.29
|
||||||
Nodes (5): bgrToRgbTensor(), Dimensions(), isValidNumber(), isValidProbablitiy(), isWithGender()
|
Nodes (4): 体验变化, 慢的原因与处理, 登录反馈与加载优化(2026-10-12), 验证
|
||||||
|
|
||||||
### Community 80 - "线上微信登录部署记录"
|
### Community 80 - "线上微信登录部署记录"
|
||||||
Cohesion: 0.33
|
Cohesion: 0.33
|
||||||
@@ -376,31 +376,35 @@ Cohesion: 0.29
|
|||||||
Nodes (5): 10.09 新版 H5 实施与验收, 按确认项实现, 文档未定义部分的实施口径, 素材再生成, 验证命令
|
Nodes (5): 10.09 新版 H5 实施与验收, 按确认项实现, 文档未定义部分的实施口径, 素材再生成, 验证命令
|
||||||
|
|
||||||
### Community 84 - "微信登录联调:2026-10-12"
|
### Community 84 - "微信登录联调:2026-10-12"
|
||||||
Cohesion: 0.29
|
Cohesion: 0.33
|
||||||
Nodes (5): 微信登录联调:2026-10-12, 本轮修正, 白名单与凭据, 真实微信验收与排查, 自动验证
|
Nodes (6): 后续真机确认与体验优化, 微信登录联调:2026-10-12, 本轮修正, 白名单与凭据, 真实微信验收与排查, 自动验证
|
||||||
|
|
||||||
|
### Community 85 - "FaceLandmarks"
|
||||||
|
Cohesion: 0.08
|
||||||
|
Nodes (12): bgrToRgbTensor(), Dimensions(), extendWithFaceLandmarks(), FaceLandmarks(), FaceLandmarks5(), FaceLandmarks68(), getCenterPoint(), isValidNumber() (+4 more)
|
||||||
|
|
||||||
### Community 87 - "产品推荐规则:肤龄基准与边界调整"
|
### Community 87 - "产品推荐规则:肤龄基准与边界调整"
|
||||||
Cohesion: 0.33
|
Cohesion: 0.33
|
||||||
Nodes (6): 产品推荐规则:肤龄基准与边界调整, 决策顺序, 后台与前端一致性, 实施与验证范围, 已明确的要求, 已确认的三个条件
|
Nodes (6): 产品推荐规则:肤龄基准与边界调整, 决策顺序, 后台与前端一致性, 实施与验证范围, 已明确的要求, 已确认的三个条件
|
||||||
|
|
||||||
### Community 92 - "部署"
|
### Community 94 - "Box"
|
||||||
Cohesion: 0.67
|
Cohesion: 0.20
|
||||||
Nodes (3): Windows 本机部署(提测用), 线上部署(juzi.xiangsilian.com), 部署
|
Nodes (3): Box(), isDimensions(), ObjectDetection()
|
||||||
|
|
||||||
## Knowledge Gaps
|
## Knowledge Gaps
|
||||||
- **190 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+185 more)
|
- **198 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+193 more)
|
||||||
These have ≤1 connection - possible missing edges or undocumented components.
|
These have ≤1 connection - possible missing edges or undocumented components.
|
||||||
- **17 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
- **21 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||||
|
|
||||||
## Suggested Questions
|
## Suggested Questions
|
||||||
_Questions this graph is uniquely positioned to answer:_
|
_Questions this graph is uniquely positioned to answer:_
|
||||||
|
|
||||||
- **Why does `k()` connect `i` to `face-api.js`, `app.js`, `r`, `.toString`, `lib/accounts.js`, `n`, `C`?**
|
- **Why does `r()` connect `r` to `face-api.js`, `.toBatchTensor`, `server.js`, `n`, `i`, `C`?**
|
||||||
_High betweenness centrality (0.063) - this node is a cross-community bridge._
|
_High betweenness centrality (0.063) - this node is a cross-community bridge._
|
||||||
- **Why does `r()` connect `r` to `face-api.js`, `i`, `.toString`, `server.js`, `n`, `FaceLandmarks`, `C`?**
|
- **Why does `renderReport()` connect `app.js` to `k`, `n`?**
|
||||||
_High betweenness centrality (0.055) - this node is a cross-community bridge._
|
_High betweenness centrality (0.051) - this node is a cross-community bridge._
|
||||||
- **Why does `renderReport()` connect `app.js` to `i`, `n`?**
|
- **Why does `k()` connect `k` to `face-api.js`, `app.js`, `r`, `lib/accounts.js`, `n`, `C`?**
|
||||||
_High betweenness centrality (0.048) - this node is a cross-community bridge._
|
_High betweenness centrality (0.047) - this node is a cross-community bridge._
|
||||||
- **Are the 24 inferred relationships involving `n()` (e.g. with `newRecordId()` and `ae()`) actually correct?**
|
- **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._
|
_`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?**
|
- **Are the 27 inferred relationships involving `r()` (e.g. with `handleExport()` and `handleRecordsQuery()`) actually correct?**
|
||||||
@@ -408,4 +412,4 @@ _Questions this graph is uniquely positioned to answer:_
|
|||||||
- **Are the 7 inferred relationships involving `t()` (e.g. with `renderReport()` and `e()`) actually correct?**
|
- **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._
|
_`t()` has 7 INFERRED edges - model-reasoned connections that need verification._
|
||||||
- **What connects `source`, `document_size`, `pages` to the rest of the system?**
|
- **What connects `source`, `document_size`, `pages` to the rest of the system?**
|
||||||
_190 weakly-connected nodes found - possible documentation gaps or missing edges._
|
_198 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||||
File diff suppressed because one or more lines are too long
+1344
-868
File diff suppressed because it is too large
Load Diff
+67
-32
@@ -40,8 +40,8 @@
|
|||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"h5/js/app.js": {
|
"h5/js/app.js": {
|
||||||
"mtime": 1791770434.3399014,
|
"mtime": 1791773775.2349546,
|
||||||
"ast_hash": "174b15c983614449ca67a7e5b81a8bbf",
|
"ast_hash": "cda77fb341a0f2573ed3a57c8e536fbb",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"h5/js/head-pose.js": {
|
"h5/js/head-pose.js": {
|
||||||
@@ -55,8 +55,8 @@
|
|||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"h5/server.js": {
|
"h5/server.js": {
|
||||||
"mtime": 1791735707.6717124,
|
"mtime": 1791773587.8349628,
|
||||||
"ast_hash": "6bdce03487aaaf9e3ab62f6b39f7a418",
|
"ast_hash": "4895534b06f0c2d29ada96d9b52b4693",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"h5/test/build_fixtures.py": {
|
"h5/test/build_fixtures.py": {
|
||||||
@@ -140,8 +140,8 @@
|
|||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"h5/index.html": {
|
"h5/index.html": {
|
||||||
"mtime": 1791770434.3399014,
|
"mtime": 1791773528.470933,
|
||||||
"ast_hash": "cfdedacf49c9a83c23155abb39fc7447",
|
"ast_hash": "a8248357be1c886421c44845b6038203",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"h5/vendor/faceapi/README.md": {
|
"h5/vendor/faceapi/README.md": {
|
||||||
@@ -770,8 +770,8 @@
|
|||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"h5/test/e2e-browser.mjs": {
|
"h5/test/e2e-browser.mjs": {
|
||||||
"mtime": 1791646391.2915454,
|
"mtime": 1791773638.010959,
|
||||||
"ast_hash": "dc47c78ddf5de136d59d73fb19d8da81",
|
"ast_hash": "9327216df7c2e80f2615cb7f800b7f1a",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"h5/assets/svg/prod-badge.svg": {
|
"h5/assets/svg/prod-badge.svg": {
|
||||||
@@ -1230,12 +1230,12 @@
|
|||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"h5/js/account-client.js": {
|
"h5/js/account-client.js": {
|
||||||
"mtime": 1791770434.3399014,
|
"mtime": 1791774062.6429472,
|
||||||
"ast_hash": "d14c4ec5374a8ada79e0e7cd1cc294a0",
|
"ast_hash": "3197b952f29f9e45176d4300082adaae",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"h5/js/camera-geometry.js": {
|
"h5/js/camera-geometry.js": {
|
||||||
"mtime": 1791646239.2012978,
|
"mtime": 1791773589.894928,
|
||||||
"ast_hash": "08048bc5e491ca47275afe067bedbc1c",
|
"ast_hash": "08048bc5e491ca47275afe067bedbc1c",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
@@ -1260,13 +1260,13 @@
|
|||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"h5/test/report-persistence.mjs": {
|
"h5/test/report-persistence.mjs": {
|
||||||
"mtime": 1791735750.0753446,
|
"mtime": 1791773638.010959,
|
||||||
"ast_hash": "2933add99f8983a590608ba203182096",
|
"ast_hash": "9876c659bb7c3513f871d576f0024e72",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"h5/test/ui-20261009.mjs": {
|
"h5/test/ui-20261009.mjs": {
|
||||||
"mtime": 1791730674.7561882,
|
"mtime": 1791773972.891023,
|
||||||
"ast_hash": "70fb3ba9b32a4c2a9845dbf027bedabb",
|
"ast_hash": "3a13f77513f40e5fc04aaeff06a906e8",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"tools/build_report_copy.py": {
|
"tools/build_report_copy.py": {
|
||||||
@@ -1275,8 +1275,8 @@
|
|||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"tools/prepare_h5_assets.py": {
|
"tools/prepare_h5_assets.py": {
|
||||||
"mtime": 1791646238.76662,
|
"mtime": 1791773528.470933,
|
||||||
"ast_hash": "6567fff1561b8cc43ae956b97adffb7e",
|
"ast_hash": "60087af4505ea0fae11a6f3449c7e21e",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"docs/10.09\u65b0\u7248H5\u5b9e\u65bd\u4e0e\u9a8c\u6536.md": {
|
"docs/10.09\u65b0\u7248H5\u5b9e\u65bd\u4e0e\u9a8c\u6536.md": {
|
||||||
@@ -1290,8 +1290,8 @@
|
|||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"docs/\u5fae\u4fe1\u767b\u5f55\u63a5\u5165\u4e0e\u516c\u7f51\u90e8\u7f72\u8bf4\u660e.md": {
|
"docs/\u5fae\u4fe1\u767b\u5f55\u63a5\u5165\u4e0e\u516c\u7f51\u90e8\u7f72\u8bf4\u660e.md": {
|
||||||
"mtime": 1791770706.5669558,
|
"mtime": 1791774038.1869662,
|
||||||
"ast_hash": "b290838863e54d0d6261cb4e43cc0904",
|
"ast_hash": "61af7cde98a3299fcc4619e4776cce2a",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"h5/assets/fonts/misans/README.md": {
|
"h5/assets/fonts/misans/README.md": {
|
||||||
@@ -1475,27 +1475,27 @@
|
|||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"h5/assets/img/ui-20261009/bg-history-h5.jpg": {
|
"h5/assets/img/ui-20261009/bg-history-h5.jpg": {
|
||||||
"mtime": 1791646239.0227692,
|
"mtime": 1791773589.4149413,
|
||||||
"ast_hash": "1dca9bdb3e19bdf837fc317e8dc3d65a",
|
"ast_hash": "1dca9bdb3e19bdf837fc317e8dc3d65a",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"h5/assets/img/ui-20261009/bg-login-h5.jpg": {
|
"h5/assets/img/ui-20261009/bg-login-h5.jpg": {
|
||||||
"mtime": 1791646238.9645536,
|
"mtime": 1791773588.8749564,
|
||||||
"ast_hash": "bbc72a85580326d228e92c0efd0293aa",
|
"ast_hash": "bbc72a85580326d228e92c0efd0293aa",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"h5/assets/img/ui-20261009/bg-profile-h5.jpg": {
|
"h5/assets/img/ui-20261009/bg-profile-h5.jpg": {
|
||||||
"mtime": 1791646238.9994829,
|
"mtime": 1791773589.1589484,
|
||||||
"ast_hash": "bbc72a85580326d228e92c0efd0293aa",
|
"ast_hash": "bbc72a85580326d228e92c0efd0293aa",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"h5/assets/img/ui-20261009/bg-report-h5.jpg": {
|
"h5/assets/img/ui-20261009/bg-report-h5.jpg": {
|
||||||
"mtime": 1791646239.1003904,
|
"mtime": 1791773589.5629373,
|
||||||
"ast_hash": "b49c61b68a714da65fdb706060e1a4f0",
|
"ast_hash": "b49c61b68a714da65fdb706060e1a4f0",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"h5/assets/img/ui-20261009/bg-splash-h5.jpg": {
|
"h5/assets/img/ui-20261009/bg-splash-h5.jpg": {
|
||||||
"mtime": 1791646238.933505,
|
"mtime": 1791773588.2109632,
|
||||||
"ast_hash": "0ab40c14387e86508639e733fea6d168",
|
"ast_hash": "0ab40c14387e86508639e733fea6d168",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
@@ -1530,8 +1530,8 @@
|
|||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"h5/test/wechat-browser.mjs": {
|
"h5/test/wechat-browser.mjs": {
|
||||||
"mtime": 1791770609.8948362,
|
"mtime": 1791773972.891023,
|
||||||
"ast_hash": "9e3344d5618e59e5e765d16d4b0903b0",
|
"ast_hash": "c27ca11dea3026358188f80affee2d6b",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"docs/2026-10-11\u63d0\u4ea4\u524dReview.md": {
|
"docs/2026-10-11\u63d0\u4ea4\u524dReview.md": {
|
||||||
@@ -1545,8 +1545,8 @@
|
|||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"h5/test/production-loop.mjs": {
|
"h5/test/production-loop.mjs": {
|
||||||
"mtime": 1791730674.763797,
|
"mtime": 1791773638.010959,
|
||||||
"ast_hash": "0fa6706f54f034124287d29adc894f71",
|
"ast_hash": "0d0d46ec3ecfce9e767e7f31050befe2",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"docs/\u7ebf\u4e0a\u5fae\u4fe1\u767b\u5f55\u90e8\u7f72\u8bb0\u5f55-2026-10-11.md": {
|
"docs/\u7ebf\u4e0a\u5fae\u4fe1\u767b\u5f55\u90e8\u7f72\u8bb0\u5f55-2026-10-11.md": {
|
||||||
@@ -1555,8 +1555,8 @@
|
|||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"h5/test/browser-failures.mjs": {
|
"h5/test/browser-failures.mjs": {
|
||||||
"mtime": 1791730674.763797,
|
"mtime": 1791773638.010959,
|
||||||
"ast_hash": "b5eaa9d78296f5c690566ed91a39d467",
|
"ast_hash": "8cb2774864d3d59bd5a418ad61819129",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"docs/\u7ebf\u4e0a\u95ed\u73af\u6d4b\u8bd5-2026-10-11.md": {
|
"docs/\u7ebf\u4e0a\u95ed\u73af\u6d4b\u8bd5-2026-10-11.md": {
|
||||||
@@ -1575,13 +1575,48 @@
|
|||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"docs/\u5fae\u4fe1\u767b\u5f55\u8054\u8c03-2026-10-12.md": {
|
"docs/\u5fae\u4fe1\u767b\u5f55\u8054\u8c03-2026-10-12.md": {
|
||||||
"mtime": 1791770818.4989572,
|
"mtime": 1791774038.1869662,
|
||||||
"ast_hash": "55253912b649c3ca707acdf2631a9e7f",
|
"ast_hash": "14292d8da8570ff0f8af2aac08d895b8",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
},
|
},
|
||||||
"h5/test/wechat-production.mjs": {
|
"h5/test/wechat-production.mjs": {
|
||||||
"mtime": 1791770851.3389251,
|
"mtime": 1791770851.3389251,
|
||||||
"ast_hash": "ba457695243430e271cfd7927ef22541",
|
"ast_hash": "ba457695243430e271cfd7927ef22541",
|
||||||
"semantic_hash": ""
|
"semantic_hash": ""
|
||||||
|
},
|
||||||
|
"h5/test/login-performance.mjs": {
|
||||||
|
"mtime": 1791773812.003017,
|
||||||
|
"ast_hash": "1ba88edfa7b259fc319de7f845925582",
|
||||||
|
"semantic_hash": ""
|
||||||
|
},
|
||||||
|
"h5/assets/img/ui-20261009/bg-history-h5.webp": {
|
||||||
|
"mtime": 1791773589.4829395,
|
||||||
|
"ast_hash": "9a886c304206c61fb50c4d8752bd8e7a",
|
||||||
|
"semantic_hash": ""
|
||||||
|
},
|
||||||
|
"h5/assets/img/ui-20261009/bg-login-h5.webp": {
|
||||||
|
"mtime": 1791773589.1229496,
|
||||||
|
"ast_hash": "bbd96b66102dd7af0f9f5b34b0c72cca",
|
||||||
|
"semantic_hash": ""
|
||||||
|
},
|
||||||
|
"h5/assets/img/ui-20261009/bg-profile-h5.webp": {
|
||||||
|
"mtime": 1791773589.3989418,
|
||||||
|
"ast_hash": "bbd96b66102dd7af0f9f5b34b0c72cca",
|
||||||
|
"semantic_hash": ""
|
||||||
|
},
|
||||||
|
"h5/assets/img/ui-20261009/bg-report-h5.webp": {
|
||||||
|
"mtime": 1791773589.8069305,
|
||||||
|
"ast_hash": "c7cbea632696ddb7703faef098f10f1a",
|
||||||
|
"semantic_hash": ""
|
||||||
|
},
|
||||||
|
"h5/assets/img/ui-20261009/bg-splash-h5.webp": {
|
||||||
|
"mtime": 1791773588.8429573,
|
||||||
|
"ast_hash": "9ba24f79e71ab8793eefd4e3271faa68",
|
||||||
|
"semantic_hash": ""
|
||||||
|
},
|
||||||
|
"docs/\u767b\u5f55\u53cd\u9988\u4e0e\u52a0\u8f7d\u4f18\u5316-2026-10-12.md": {
|
||||||
|
"mtime": 1791774062.6429472,
|
||||||
|
"ast_hash": "ffa81f4a1a2b47615d6a4b7a27de0d08",
|
||||||
|
"semantic_hash": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 439 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
/* Original MiSans files, unchanged; never needed for login/session restoration. */
|
||||||
|
@font-face { font-family: MiSans; src: url(../assets/fonts/misans/MiSans-Regular.woff2) format('woff2'); font-weight: 100 500; font-display: swap; }
|
||||||
|
@font-face { font-family: MiSans; src: url(../assets/fonts/misans/MiSans-Semibold.woff2) format('woff2'); font-weight: 600 900; font-display: swap; }
|
||||||
+22
-8
@@ -1,9 +1,12 @@
|
|||||||
/* 10.09 PSD: exclude the native WeChat header; retain flexible H5 viewport. */
|
/* 10.09 PSD: exclude the native WeChat header; retain flexible H5 viewport. */
|
||||||
@font-face { font-family: MiSans; src: url(../assets/fonts/misans/MiSans-Regular.woff2) format('woff2'); font-weight: 100 500; font-display: swap; }
|
/* Font faces are registered lazily by app.js on entering profile/camera/report.
|
||||||
@font-face { font-family: MiSans; src: url(../assets/fonts/misans/MiSans-Semibold.woff2) format('woff2'); font-weight: 600 900; font-display: swap; }
|
Hidden wheel-picker text can otherwise trigger font downloads at startup. */
|
||||||
:root { --font: "MiSans","PingFang SC","Microsoft YaHei",sans-serif; }
|
:root { --font: "MiSans","PingFang SC","Microsoft YaHei",sans-serif; }
|
||||||
[hidden] { display: none !important; }
|
[hidden] { display: none !important; }
|
||||||
#s-login { background: #142632 url(../assets/img/ui-20261009/bg-login-h5.jpg) center/cover no-repeat; }
|
/* Keep the original licensed font intact. Entry/account screens use system fonts
|
||||||
|
so login never competes with the two full Chinese font downloads (~9.5 MB). */
|
||||||
|
#s-login, #s-splash, #s-history, #p-login-success, #toast { --font: "PingFang SC","Microsoft YaHei",system-ui,sans-serif; font-family: var(--font); }
|
||||||
|
#s-login { background: #142632 url(../assets/img/ui-20261009/bg-login-h5.webp) center/cover no-repeat; }
|
||||||
.login-brand { position: absolute; width: calc(145 * var(--u)); left: 50%; top: 14%; transform: translateX(-50%); }
|
.login-brand { position: absolute; width: calc(145 * var(--u)); left: 50%; top: 14%; transform: translateX(-50%); }
|
||||||
.login-bottom { position: absolute; left: 0; right: 0; bottom: max(5%, env(safe-area-inset-bottom)); display: flex; flex-direction: column; align-items: center; gap: calc(12 * var(--u)); }
|
.login-bottom { position: absolute; left: 0; right: 0; bottom: max(5%, env(safe-area-inset-bottom)); display: flex; flex-direction: column; align-items: center; gap: calc(12 * var(--u)); }
|
||||||
.login-button { width: calc(270 * var(--u)); height: calc(40 * var(--u)); display: flex; align-items: center; justify-content: center; gap: calc(14 * var(--u)); border-radius: calc(5 * var(--u)); font-size: calc(14 * var(--u)); }
|
.login-button { width: calc(270 * var(--u)); height: calc(40 * var(--u)); display: flex; align-items: center; justify-content: center; gap: calc(14 * var(--u)); border-radius: calc(5 * var(--u)); font-size: calc(14 * var(--u)); }
|
||||||
@@ -14,16 +17,16 @@
|
|||||||
.login-consent input { accent-color: var(--yellow); width: 13px; height: 13px; }
|
.login-consent input { accent-color: var(--yellow); width: 13px; height: 13px; }
|
||||||
.login-consent button { color: var(--yellow); text-decoration: underline; font-size: inherit; }
|
.login-consent button { color: var(--yellow); text-decoration: underline; font-size: inherit; }
|
||||||
.login-message { min-height: calc(28 * var(--u)); max-width: 84%; color: #eee; text-align: center; font-size: calc(10 * var(--u)); line-height: 1.5; }
|
.login-message { min-height: calc(28 * var(--u)); max-width: 84%; color: #eee; text-align: center; font-size: calc(10 * var(--u)); line-height: 1.5; }
|
||||||
.splash-bg, .guide-bg { background-image: url(../assets/img/ui-20261009/bg-splash-h5.jpg); }
|
.splash-bg, .guide-bg { background-image: url(../assets/img/ui-20261009/bg-splash-h5.webp); }
|
||||||
.splash-slogan { top: 7.2%; font-size: calc(23 * var(--u)); line-height: 1.16; letter-spacing: 0; font-weight: 400; }
|
.splash-slogan { top: 7.2%; font-size: calc(23 * var(--u)); line-height: 1.16; letter-spacing: 0; font-weight: 400; }
|
||||||
.splash-bottom { bottom: 6.5%; gap: calc(9 * var(--u)); }
|
.splash-bottom { bottom: 6.5%; gap: calc(9 * var(--u)); }
|
||||||
.brand-row { margin-bottom: calc(7 * var(--u)); gap: calc(5 * var(--u)); }
|
.brand-row { margin-bottom: calc(7 * var(--u)); gap: calc(5 * var(--u)); }
|
||||||
.brand-logo { width: calc(48 * var(--u)); }
|
.brand-logo { width: calc(48 * var(--u)); }
|
||||||
.brand-text { font-size: calc(19 * var(--u)); letter-spacing: 0; }
|
.brand-text { font-size: calc(19 * var(--u)); letter-spacing: 0; }
|
||||||
.btn-history { background: #fff; }
|
.btn-history { background: #fff; }
|
||||||
.profile-bg { background: url(../assets/img/ui-20261009/bg-profile-h5.jpg) center/cover no-repeat; }
|
.profile-bg { background: url(../assets/img/ui-20261009/bg-profile-h5.webp) center/cover no-repeat; }
|
||||||
.skin-desc { max-width: calc(150 * var(--u)); font-size: calc(11.5 * var(--u)); }
|
.skin-desc { max-width: calc(150 * var(--u)); font-size: calc(11.5 * var(--u)); }
|
||||||
#s-history { background: #efefef url(../assets/img/ui-20261009/bg-history-h5.jpg) center/cover fixed; }
|
#s-history { background: #efefef url(../assets/img/ui-20261009/bg-history-h5.webp) center/cover fixed; }
|
||||||
.history-content { padding: calc(28 * var(--u)) calc(17 * var(--u)) calc(36 * var(--u)); }
|
.history-content { padding: calc(28 * var(--u)) calc(17 * var(--u)) calc(36 * var(--u)); }
|
||||||
.history-count { text-align: center; padding: calc(16 * var(--u)) calc(12 * var(--u)) calc(21 * var(--u)); }
|
.history-count { text-align: center; padding: calc(16 * var(--u)) calc(12 * var(--u)) calc(21 * var(--u)); }
|
||||||
.history-count > span { color: #888; font-size: calc(12 * var(--u)); }
|
.history-count > span { color: #888; font-size: calc(12 * var(--u)); }
|
||||||
@@ -35,7 +38,7 @@
|
|||||||
.history-item p span { color: #888; margin-right: calc(8 * var(--u)); }
|
.history-item p span { color: #888; margin-right: calc(8 * var(--u)); }
|
||||||
.history-detail { display: block; margin: calc(7 * var(--u)) 0 0 auto; background: var(--yellow); border-radius: calc(5 * var(--u)); width: calc(80 * var(--u)); font-size: calc(12 * var(--u)); min-height: calc(23 * var(--u)); }
|
.history-detail { display: block; margin: calc(7 * var(--u)) 0 0 auto; background: var(--yellow); border-radius: calc(5 * var(--u)); width: calc(80 * var(--u)); font-size: calc(12 * var(--u)); min-height: calc(23 * var(--u)); }
|
||||||
.history-retry { display: block; margin: 20px auto; color: #707070; text-decoration: underline; font-size: 12px; }
|
.history-retry { display: block; margin: 20px auto; color: #707070; text-decoration: underline; font-size: 12px; }
|
||||||
#s-report { background: #efefef url(../assets/img/ui-20261009/bg-report-h5.jpg) center top/100% auto; }
|
#s-report { background: #efefef url(../assets/img/ui-20261009/bg-report-h5.webp) center top/100% auto; }
|
||||||
.report-body { padding: calc(28 * var(--u)) calc(17 * var(--u)) calc(42 * var(--u)); }
|
.report-body { padding: calc(28 * var(--u)) calc(17 * var(--u)) calc(42 * var(--u)); }
|
||||||
.report-title { font-size: calc(23 * var(--u)); padding: 0; margin-bottom: calc(23 * var(--u)); }
|
.report-title { font-size: calc(23 * var(--u)); padding: 0; margin-bottom: calc(23 * var(--u)); }
|
||||||
.card { border-radius: calc(7 * var(--u)); }
|
.card { border-radius: calc(7 * var(--u)); }
|
||||||
@@ -74,11 +77,22 @@
|
|||||||
.prod-badge { width: calc(36 * var(--u)); margin-top: calc(20 * var(--u)); }
|
.prod-badge { width: calc(36 * var(--u)); margin-top: calc(20 * var(--u)); }
|
||||||
.prod-scroll { flex-basis: 50%; }
|
.prod-scroll { flex-basis: 50%; }
|
||||||
.prod-scroll img { object-fit: cover; aspect-ratio: 1 / 1; }
|
.prod-scroll img { object-fit: cover; aspect-ratio: 1 / 1; }
|
||||||
#p-privacy { background: linear-gradient(#0008,#0008),url(../assets/img/ui-20261009/bg-splash-h5.jpg) center/cover; }
|
#p-privacy { background: linear-gradient(#0008,#0008),url(../assets/img/ui-20261009/bg-splash-h5.webp) center/cover; }
|
||||||
#p-photoguide .popup { padding: calc(62 * var(--u)) calc(30 * var(--u)) calc(40 * var(--u)); }
|
#p-photoguide .popup { padding: calc(62 * var(--u)) calc(30 * var(--u)) calc(40 * var(--u)); }
|
||||||
.popup-title-center { text-align: left; margin: 0 0 calc(18 * var(--u)); }
|
.popup-title-center { text-align: left; margin: 0 0 calc(18 * var(--u)); }
|
||||||
.guide-description { font-size: calc(12 * var(--u)); line-height: 1.6; margin-bottom: calc(35 * var(--u)); }
|
.guide-description { font-size: calc(12 * var(--u)); line-height: 1.6; margin-bottom: calc(35 * var(--u)); }
|
||||||
.cam-feature-lines { fill: none; stroke: #fff; stroke-width: 2.5; stroke-dasharray: 5 5; filter: drop-shadow(0 1px 1px #000); }
|
.cam-feature-lines { fill: none; stroke: #fff; stroke-width: 2.5; stroke-dasharray: 5 5; filter: drop-shadow(0 1px 1px #000); }
|
||||||
#s-cam:not(.cam-live) .cam-feature-lines { stroke: #111; filter: none; }
|
#s-cam:not(.cam-live) .cam-feature-lines { stroke: #111; filter: none; }
|
||||||
.font-credit { display: block; text-align: center; color: #888; font-size: 10px; margin: 24px auto 0; }
|
.font-credit { display: block; text-align: center; color: #888; font-size: 10px; margin: 24px auto 0; }
|
||||||
|
.account-status { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; border-radius: 8px; font-size: 12px; }
|
||||||
|
.account-status span::before { content: ''; display: inline-block; width: 7px; height: 7px; margin-right: 7px; border-radius: 50%; background: #b7bac0; }
|
||||||
|
.account-status[data-kind="wechat"] span::before { background: #34ca7b; }
|
||||||
|
.account-status small { font-size: 11px; opacity: .8; }
|
||||||
|
.account-status-home { position: absolute; top: max(10px, env(safe-area-inset-top)); right: 12px; z-index: 2; background: #0008; color: #fff; }
|
||||||
|
#s-splash .splash-slogan { top: max(7.2%, calc(env(safe-area-inset-top) + 52px)); }
|
||||||
|
.account-status-history { width: 100%; background: #fff; margin-bottom: 14px; text-align: left; }
|
||||||
|
.login-success-popup { min-height: 0; text-align: center; gap: 18px; align-items: center; padding: 30px 24px; }
|
||||||
|
.login-success-icon { width: 48px; height: 48px; border-radius: 50%; background: #e9f8ef; color: #17994f; font-size: 30px; line-height: 48px; }
|
||||||
|
.login-success-popup h2 { font-size: 20px; font-weight: 600; }
|
||||||
|
.login-success-popup p { font-size: 14px; line-height: 1.7; color: #666; white-space: pre-line; }
|
||||||
@media (max-height: 560px) { .login-brand { top: 8%; } .login-bottom { bottom: 2%; } .splash-slogan { font-size: calc(18 * var(--u)); } }
|
@media (max-height: 560px) { .login-brand { top: 8%; } .login-bottom { bottom: 2%; } .splash-slogan { font-size: calc(18 * var(--u)); } }
|
||||||
|
|||||||
+12
-3
@@ -11,7 +11,7 @@
|
|||||||
<meta property="og:description" content="拍摄正面与左右侧面照片,即时获取六维肌肤状态分析与个性化护理建议。">
|
<meta property="og:description" content="拍摄正面与左右侧面照片,即时获取六维肌肤状态分析与个性化护理建议。">
|
||||||
<title>1'me 在线肌肤测试</title>
|
<title>1'me 在线肌肤测试</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
|
||||||
<link rel="preload" as="image" href="assets/img/bg-splash.jpg">
|
<link rel="preload" as="image" href="assets/img/ui-20261009/bg-login-h5.webp" fetchpriority="high">
|
||||||
<link rel="stylesheet" href="vendor/mobile-select/mobile-select.css">
|
<link rel="stylesheet" href="vendor/mobile-select/mobile-select.css">
|
||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<link rel="stylesheet" href="css/ui-20261009.css">
|
<link rel="stylesheet" href="css/ui-20261009.css">
|
||||||
@@ -31,16 +31,17 @@
|
|||||||
<section class="screen" id="s-history">
|
<section class="screen" id="s-history">
|
||||||
<div class="history-content">
|
<div class="history-content">
|
||||||
<h1 class="report-title">您的报告</h1>
|
<h1 class="report-title">您的报告</h1>
|
||||||
|
<button class="account-status account-status-history" data-action="switch-login" aria-label="切换登录方式"><span data-auth-status></span><small>切换登录方式 ›</small></button>
|
||||||
<div class="card history-count"><span>测试数量</span><p id="history-count">正在读取报告…</p></div>
|
<div class="card history-count"><span>测试数量</span><p id="history-count">正在读取报告…</p></div>
|
||||||
<p class="identity-note" id="identity-note"></p>
|
<p class="identity-note" id="identity-note"></p>
|
||||||
<div id="history-list" aria-live="polite"></div>
|
<div id="history-list" aria-live="polite"></div>
|
||||||
<button class="history-retry" data-action="history-retry">刷新列表</button>
|
<button class="history-retry" data-action="history-retry">刷新列表</button>
|
||||||
<button class="history-retry" data-action="switch-login">切换登录方式</button>
|
|
||||||
<small class="font-credit">本应用使用 MiSans 字体</small>
|
<small class="font-credit">本应用使用 MiSans 字体</small>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="screen" id="s-splash">
|
<section class="screen" id="s-splash">
|
||||||
<div class="splash-bg"></div>
|
<div class="splash-bg"></div>
|
||||||
|
<button class="account-status account-status-home" data-action="switch-login" aria-label="切换登录方式"><span data-auth-status></span><small>切换 ›</small></button>
|
||||||
<p class="splash-slogan">ONE ME,<br>FROM THE INSIDE OUT<br>以我为始 自有表现</p>
|
<p class="splash-slogan">ONE ME,<br>FROM THE INSIDE OUT<br>以我为始 自有表现</p>
|
||||||
<div class="splash-bottom">
|
<div class="splash-bottom">
|
||||||
<div class="brand-row">
|
<div class="brand-row">
|
||||||
@@ -461,7 +462,15 @@
|
|||||||
<!-- ═══════════ Toast ═══════════ -->
|
<!-- ═══════════ Toast ═══════════ -->
|
||||||
<div class="toast" id="toast"></div>
|
<div class="toast" id="toast"></div>
|
||||||
|
|
||||||
<script src="vendor/faceapi/face-api.js" defer></script>
|
<div class="overlay" id="p-login-success" role="dialog" aria-modal="true" aria-labelledby="login-success-title" aria-describedby="login-success-message">
|
||||||
|
<div class="popup login-success-popup">
|
||||||
|
<div class="login-success-icon" aria-hidden="true">✓</div>
|
||||||
|
<h2 id="login-success-title">登录成功</h2>
|
||||||
|
<p id="login-success-message"></p>
|
||||||
|
<button class="btn btn-primary" data-action="login-confirm">我知道了</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="js/head-pose.js" defer></script>
|
<script src="js/head-pose.js" defer></script>
|
||||||
<script src="js/skin-score.js" defer></script>
|
<script src="js/skin-score.js" defer></script>
|
||||||
<script src="vendor/mobile-select/mobile-select.iife.min.js" defer></script>
|
<script src="vendor/mobile-select/mobile-select.iife.min.js" defer></script>
|
||||||
|
|||||||
@@ -5,11 +5,19 @@ window.AccountClient = (function () {
|
|||||||
function read(name) { try { return localStorage.getItem(name || key); } catch (_) { return null; } }
|
function read(name) { try { return localStorage.getItem(name || key); } catch (_) { return null; } }
|
||||||
function rememberKind() { try { if (user) localStorage.setItem(kindKey, user.kind); } catch (_) {} }
|
function rememberKind() { try { if (user) localStorage.setItem(kindKey, user.kind); } catch (_) {} }
|
||||||
function request(url, data) {
|
function request(url, data) {
|
||||||
|
var controller = new AbortController();
|
||||||
|
// Bound only authentication waits. Full report snapshots contain photos and
|
||||||
|
// must retain their existing download budget on a slow mobile connection.
|
||||||
|
var timer = url.indexOf('api/auth/') === 0 ? setTimeout(function () { controller.abort(); }, 12000) : null;
|
||||||
return fetch(url, { credentials: 'same-origin', method: data ? 'POST' : 'GET',
|
return fetch(url, { credentials: 'same-origin', method: data ? 'POST' : 'GET',
|
||||||
|
signal: controller.signal,
|
||||||
headers: data ? { 'Content-Type': 'application/json' } : {},
|
headers: data ? { 'Content-Type': 'application/json' } : {},
|
||||||
body: data ? JSON.stringify(data) : undefined }).then(function (r) {
|
body: data ? JSON.stringify(data) : undefined }).then(function (r) {
|
||||||
return r.json().then(function (v) { if (!r.ok || !v.ok) throw new Error(v.message || '请求失败'); return v; });
|
return r.json().then(function (v) { if (!r.ok || !v.ok) throw new Error(v.message || '请求失败'); return v; });
|
||||||
});
|
}).catch(function (error) {
|
||||||
|
if (error.name === 'AbortError') throw new Error('网络响应超时,请检查网络后重试');
|
||||||
|
throw error;
|
||||||
|
}).finally(function () { clearTimeout(timer); });
|
||||||
}
|
}
|
||||||
function browser(consent) {
|
function browser(consent) {
|
||||||
return request('api/auth/browser', { consent: consent, token: read() }).then(function (v) {
|
return request('api/auth/browser', { consent: consent, token: read() }).then(function (v) {
|
||||||
|
|||||||
+75
-24
@@ -32,8 +32,38 @@
|
|||||||
|
|
||||||
/* ─── 屏幕管理:页面内不放返回按钮,前进时 pushState,微信/浏览器返回键经 popstate 回到上一屏 ─── */
|
/* ─── 屏幕管理:页面内不放返回按钮,前进时 pushState,微信/浏览器返回键经 popstate 回到上一屏 ─── */
|
||||||
var currentScreen = 's-login';
|
var currentScreen = 's-login';
|
||||||
|
var fontStylesLoaded = false;
|
||||||
|
function loadAppFont() {
|
||||||
|
if (fontStylesLoaded) return;
|
||||||
|
fontStylesLoaded = true;
|
||||||
|
var link = document.createElement('link');
|
||||||
|
link.rel = 'stylesheet'; link.href = 'css/fonts.css';
|
||||||
|
document.head.appendChild(link);
|
||||||
|
}
|
||||||
|
function renderAccountStatus() {
|
||||||
|
var user = AccountClient.user();
|
||||||
|
document.querySelectorAll('[data-auth-status]').forEach(function (label) {
|
||||||
|
label.textContent = user ? (user.kind === 'wechat' ? '微信已登录' : '本机浏览器已登录') : '尚未登录';
|
||||||
|
label.parentElement.dataset.kind = user ? user.kind : '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function loginSucceeded() {
|
||||||
|
showScreen('s-splash', 'replace');
|
||||||
|
$('#login-success-message').textContent = AccountClient.user().kind === 'wechat'
|
||||||
|
? '当前使用:微信登录\n同一微信账号可跨手机查看报告。'
|
||||||
|
: '当前使用:本机浏览器登录\n报告仅能通过当前浏览器身份查看。';
|
||||||
|
openOverlay('p-login-success');
|
||||||
|
document.querySelectorAll('.screen').forEach(function (screen) { screen.inert = true; });
|
||||||
|
$('[data-action="login-confirm"]').focus();
|
||||||
|
}
|
||||||
|
function closeLoginSuccess() {
|
||||||
|
closeOverlay('p-login-success');
|
||||||
|
document.querySelectorAll('.screen').forEach(function (screen) { screen.inert = false; });
|
||||||
|
$('#s-splash [data-action="open-privacy"]').focus({ preventScroll: true });
|
||||||
|
}
|
||||||
function showScreen(id, nav) { // nav: 'push' 新增历史 | 'replace' 替换当前历史 | 缺省不动历史
|
function showScreen(id, nav) { // nav: 'push' 新增历史 | 'replace' 替换当前历史 | 缺省不动历史
|
||||||
if (id !== 's-login' && !AccountClient.user() && !(/[?&]mock=1/.test(location.search))) id = 's-login';
|
if (id !== 's-login' && !AccountClient.user() && !(/[?&]mock=1/.test(location.search))) id = 's-login';
|
||||||
|
if (['s-profile', 's-cam', 's-report'].includes(id)) loadAppFont();
|
||||||
document.querySelectorAll('.screen').forEach(function (s) {
|
document.querySelectorAll('.screen').forEach(function (s) {
|
||||||
s.classList.toggle('active', s.id === id);
|
s.classList.toggle('active', s.id === id);
|
||||||
});
|
});
|
||||||
@@ -42,6 +72,7 @@
|
|||||||
document.body.scrollTop = 0;
|
document.body.scrollTop = 0;
|
||||||
sc.scrollTop = 0;
|
sc.scrollTop = 0;
|
||||||
currentScreen = id;
|
currentScreen = id;
|
||||||
|
renderAccountStatus();
|
||||||
if (id === 's-report') resetReport(); // 每次进入报告页都还原初始状态
|
if (id === 's-report') resetReport(); // 每次进入报告页都还原初始状态
|
||||||
if (nav === 'push') history.pushState({ screen: id }, '');
|
if (nav === 'push') history.pushState({ screen: id }, '');
|
||||||
else if (nav === 'replace') history.replaceState({ screen: id }, '');
|
else if (nav === 'replace') history.replaceState({ screen: id }, '');
|
||||||
@@ -52,6 +83,7 @@
|
|||||||
window.addEventListener('popstate', function (e) {
|
window.addEventListener('popstate', function (e) {
|
||||||
if (Date.now() < popSilentUntil) { popSilentUntil = 0; return; }
|
if (Date.now() < popSilentUntil) { popSilentUntil = 0; return; }
|
||||||
var to = (e.state && e.state.screen) || 's-splash';
|
var to = (e.state && e.state.screen) || 's-splash';
|
||||||
|
if ($('#p-login-success').classList.contains('show')) closeLoginSuccess();
|
||||||
['p-privacy', 'p-photoguide', 'p-policy', 'p-zoom'].forEach(closeOverlay);
|
['p-privacy', 'p-photoguide', 'p-policy', 'p-zoom'].forEach(closeOverlay);
|
||||||
if (currentScreen === 's-cam') leaveCamera();
|
if (currentScreen === 's-cam') leaveCamera();
|
||||||
if (to === 's-cam') { to = 's-profile'; history.replaceState({ screen: to }, ''); } // 「前进」回拍照页不自动开相机
|
if (to === 's-cam') { to = 's-profile'; history.replaceState({ screen: to }, ''); } // 「前进」回拍照页不自动开相机
|
||||||
@@ -86,6 +118,11 @@
|
|||||||
bindBackdropClose('p-policy');
|
bindBackdropClose('p-policy');
|
||||||
// ESC 关闭(分析中过渡不响应;放大图/协议层在最上,优先关)
|
// ESC 关闭(分析中过渡不响应;放大图/协议层在最上,优先关)
|
||||||
document.addEventListener('keydown', function (e) {
|
document.addEventListener('keydown', function (e) {
|
||||||
|
if ($('#p-login-success').classList.contains('show')) {
|
||||||
|
if (e.key === 'Escape' || e.key === 'Esc') closeLoginSuccess();
|
||||||
|
if (e.key === 'Tab') { e.preventDefault(); $('[data-action="login-confirm"]').focus(); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (e.key !== 'Escape' && e.key !== 'Esc') return;
|
if (e.key !== 'Escape' && e.key !== 'Esc') return;
|
||||||
var order = ['p-zoom', 'p-policy', 'p-privacy', 'p-photoguide'];
|
var order = ['p-zoom', 'p-policy', 'p-privacy', 'p-photoguide'];
|
||||||
for (var i = 0; i < order.length; i++) {
|
for (var i = 0; i < order.length; i++) {
|
||||||
@@ -323,7 +360,7 @@
|
|||||||
resetDetectState(); // 降级无检测、无补光:蒙版回到静态线框,快门出现
|
resetDetectState(); // 降级无检测、无补光:蒙版回到静态线框,快门出现
|
||||||
toast('已切换为系统相机拍摄');
|
toast('已切换为系统相机拍摄');
|
||||||
// 系统相机拍回的照片也要校验(有脸、角度对),模型照常预加载
|
// 系统相机拍回的照片也要校验(有脸、角度对),模型照常预加载
|
||||||
if (!IS_MOCK && typeof faceapi !== 'undefined') loadModels().catch(function () {});
|
if (!IS_MOCK) loadModels().catch(function () {});
|
||||||
}
|
}
|
||||||
// 从视频帧截取照片(完整帧、摄像头原始视角,压到最长边 1280 的 JPEG)
|
// 从视频帧截取照片(完整帧、摄像头原始视角,压到最长边 1280 的 JPEG)
|
||||||
// 抓完整帧而非取景框可见区:新 UI 轮廓外全黑,用户只关注轮廓内人脸;按可见区裁切在侧脸/偏站位时会切掉人脸(Face++ INVALID_IMAGE_FACE)
|
// 抓完整帧而非取景框可见区:新 UI 轮廓外全黑,用户只关注轮廓内人脸;按可见区裁切在侧脸/偏站位时会切掉人脸(Face++ INVALID_IMAGE_FACE)
|
||||||
@@ -545,7 +582,28 @@
|
|||||||
var CENTER_TOL = 0.18; // 人脸中心偏离取景中线超过屏宽 18% → 提示移入取景框
|
var CENTER_TOL = 0.18; // 人脸中心偏离取景中线超过屏宽 18% → 提示移入取景框
|
||||||
var LUMA_DARK = 65, LUMA_BRIGHT = 235; // 脸部平均亮度(0–255):过暗/过曝提示调整光线
|
var LUMA_DARK = 65, LUMA_BRIGHT = 235; // 脸部平均亮度(0–255):过暗/过曝提示调整光线
|
||||||
|
|
||||||
var modelsPromise = null, modelsOk = false, detectDisabled = false; // detectDisabled:face-api 缺失→全程手动;模型加载失败仅当次禁用,下次进拍照页重试
|
var modelsPromise = null, modelsOk = false, detectDisabled = false;
|
||||||
|
var faceApiPromise = null;
|
||||||
|
function loadFaceApi() {
|
||||||
|
if (window.faceapi) return Promise.resolve();
|
||||||
|
if (!faceApiPromise) {
|
||||||
|
faceApiPromise = new Promise(function (resolve, reject) {
|
||||||
|
var script = document.createElement('script');
|
||||||
|
var timer = setTimeout(function () { finish(new Error('检测引擎加载超时')); }, 20000);
|
||||||
|
function finish(error) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
script.onload = script.onerror = null;
|
||||||
|
if (error) { script.remove(); faceApiPromise = null; reject(error); }
|
||||||
|
else resolve();
|
||||||
|
}
|
||||||
|
script.src = 'vendor/faceapi/face-api.js';
|
||||||
|
script.onload = function () { finish(window.faceapi ? null : new Error('检测引擎不可用')); };
|
||||||
|
script.onerror = function () { finish(new Error('检测引擎加载失败')); };
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return faceApiPromise;
|
||||||
|
}
|
||||||
var detRunning = false, detTimer = null; // 检测循环
|
var detRunning = false, detTimer = null; // 检测循环
|
||||||
var okSince = 0; // 当前连续 ok 起点(0=未在累积)
|
var okSince = 0; // 当前连续 ok 起点(0=未在累积)
|
||||||
var nudgeTimer = null; // 12s 无进展提示
|
var nudgeTimer = null; // 12s 无进展提示
|
||||||
@@ -555,10 +613,10 @@
|
|||||||
// 模型只加载一次(缓存 promise);失败清空缓存,下次进拍照页重试
|
// 模型只加载一次(缓存 promise);失败清空缓存,下次进拍照页重试
|
||||||
function loadModels() {
|
function loadModels() {
|
||||||
if (!modelsPromise) {
|
if (!modelsPromise) {
|
||||||
modelsPromise = Promise.all([
|
modelsPromise = loadFaceApi().then(function () { return Promise.all([
|
||||||
faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL),
|
faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL),
|
||||||
faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL)
|
faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL)
|
||||||
]).then(function () { modelsOk = true; },
|
]); }).then(function () { modelsOk = true; },
|
||||||
function (err) { modelsPromise = null; throw err; });
|
function (err) { modelsPromise = null; throw err; });
|
||||||
}
|
}
|
||||||
return modelsPromise;
|
return modelsPromise;
|
||||||
@@ -1206,12 +1264,6 @@
|
|||||||
/* ─── 相机可用后加载检测模型(加载完成前快门即手动模式) ─── */
|
/* ─── 相机可用后加载检测模型(加载完成前快门即手动模式) ─── */
|
||||||
function beginDetect() {
|
function beginDetect() {
|
||||||
if (IS_MOCK || camFallback) { resetDetectState(); return; }
|
if (IS_MOCK || camFallback) { resetDetectState(); return; }
|
||||||
if (typeof faceapi === 'undefined') {
|
|
||||||
detectDisabled = true; // face-api 本身缺失:全程手动,不再重试
|
|
||||||
resetDetectState();
|
|
||||||
toast('自动识别不可用,已切换手动拍摄');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
detectDisabled = false; // 上次模型加载失败允许重试(modelsPromise 失败时已清空,loadModels 会重新拉取)
|
detectDisabled = false; // 上次模型加载失败允许重试(modelsPromise 失败时已清空,loadModels 会重新拉取)
|
||||||
resetDetectState();
|
resetDetectState();
|
||||||
if (!modelsOk) showCamStatus('检测引擎加载中…');
|
if (!modelsOk) showCamStatus('检测引擎加载中…');
|
||||||
@@ -1265,6 +1317,7 @@
|
|||||||
'open-policy': function () { openOverlay('p-policy'); },
|
'open-policy': function () { openOverlay('p-policy'); },
|
||||||
'login-browser': function () { loginBrowser(); },
|
'login-browser': function () { loginBrowser(); },
|
||||||
'login-wechat': function () { loginWechat(); },
|
'login-wechat': function () { loginWechat(); },
|
||||||
|
'login-confirm': closeLoginSuccess,
|
||||||
'open-privacy': function () { if (!AccountClient.user()) { showScreen('s-login', 'replace'); return; } openOverlay('p-privacy'); },
|
'open-privacy': function () { if (!AccountClient.user()) { showScreen('s-login', 'replace'); return; } openOverlay('p-privacy'); },
|
||||||
'close-privacy': function () { closeOverlay('p-privacy'); },
|
'close-privacy': function () { closeOverlay('p-privacy'); },
|
||||||
'close-policy': function () { closeOverlay('p-policy'); },
|
'close-policy': function () { closeOverlay('p-policy'); },
|
||||||
@@ -1370,25 +1423,20 @@
|
|||||||
loginBusy = true;
|
loginBusy = true;
|
||||||
$('#login-message').textContent = '正在登录…';
|
$('#login-message').textContent = '正在登录…';
|
||||||
AccountClient.browser(true).then(function () {
|
AccountClient.browser(true).then(function () {
|
||||||
showScreen('s-splash', 'replace');
|
loginSucceeded();
|
||||||
}).catch(function (e) { $('#login-message').textContent = e.message; })
|
}).catch(function (e) { $('#login-message').textContent = e.message; })
|
||||||
.finally(function () { loginBusy = false; });
|
.finally(function () { loginBusy = false; });
|
||||||
}
|
}
|
||||||
function loginWechat() {
|
function loginWechat() {
|
||||||
if (!loginAllowed()) return;
|
if (!loginAllowed()) return;
|
||||||
|
if (!/MicroMessenger/i.test(navigator.userAgent)) {
|
||||||
|
$('#login-message').textContent = '请在微信内打开本页面,或使用本机浏览器登录。';
|
||||||
|
return;
|
||||||
|
}
|
||||||
loginBusy = true;
|
loginBusy = true;
|
||||||
AccountClient.request('api/auth/config').then(function (data) {
|
$('#login-message').textContent = '正在前往微信授权…';
|
||||||
if (!data.wechat) {
|
AccountClient.request('api/auth/wechat', { consent: true }).then(function (data) {
|
||||||
$('#login-message').textContent = data.reason + '。报告仍会保存至服务端。';
|
location.assign(data.url);
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!/MicroMessenger/i.test(navigator.userAgent)) {
|
|
||||||
$('#login-message').textContent = '请在微信内打开本页面,或使用本机浏览器登录。';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return AccountClient.request('api/auth/wechat', { consent: true }).then(function (data) {
|
|
||||||
location.assign(data.url);
|
|
||||||
});
|
|
||||||
}).catch(function (e) { $('#login-message').textContent = e.message; })
|
}).catch(function (e) { $('#login-message').textContent = e.message; })
|
||||||
.finally(function () { loginBusy = false; });
|
.finally(function () { loginBusy = false; });
|
||||||
}
|
}
|
||||||
@@ -1446,7 +1494,10 @@
|
|||||||
: '微信授权未完成,请重试;如仍失败,请将错误页面截图发给管理员。也可选择本机浏览器登录。';
|
: '微信授权未完成,请重试;如仍失败,请将错误页面截图发给管理员。也可选择本机浏览器登录。';
|
||||||
} else if (loginSuccess && (!AccountClient.user() || AccountClient.user().kind !== 'wechat')) {
|
} else if (loginSuccess && (!AccountClient.user() || AccountClient.user().kind !== 'wechat')) {
|
||||||
$('#login-message').textContent = '微信返回后登录会话未能保存,请在同一微信窗口重新登录。';
|
$('#login-message').textContent = '微信返回后登录会话未能保存,请在同一微信窗口重新登录。';
|
||||||
} else if (AccountClient.user()) showScreen('s-splash', 'replace');
|
} else if (AccountClient.user()) {
|
||||||
|
if (loginSuccess) loginSucceeded();
|
||||||
|
else showScreen('s-splash', 'replace');
|
||||||
|
}
|
||||||
else $('#login-message').textContent = '微信登录可跨手机查看报告;本机登录仅限当前浏览器。';
|
else $('#login-message').textContent = '微信登录可跨手机查看报告;本机登录仅限当前浏览器。';
|
||||||
}).catch(function (e) { $('#login-message').textContent = e.message; })
|
}).catch(function (e) { $('#login-message').textContent = e.message; })
|
||||||
.finally(function () {
|
.finally(function () {
|
||||||
|
|||||||
+22
-5
@@ -13,6 +13,8 @@
|
|||||||
const http = require('http');
|
const http = require('http');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const zlib = require('zlib');
|
||||||
|
|
||||||
const PORT = Number(process.env.PORT) || 8080;
|
const PORT = Number(process.env.PORT) || 8080;
|
||||||
// 只绑回环:公网入口是 nginx(1me.xajuzi.com/state/),8080 不再对公网裸奔;需局域网调试时 HOST=0.0.0.0
|
// 只绑回环:公网入口是 nginx(1me.xajuzi.com/state/),8080 不再对公网裸奔;需局域网调试时 HOST=0.0.0.0
|
||||||
@@ -84,11 +86,26 @@ function serveStatic(req, res, pathname) {
|
|||||||
fs.readFile(file, (err, data) => {
|
fs.readFile(file, (err, data) => {
|
||||||
if (err) { res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); return res.end('Not Found'); }
|
if (err) { res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); return res.end('Not Found'); }
|
||||||
const ext = path.extname(file);
|
const ext = path.extname(file);
|
||||||
// html/css/js 不缓存(迭代期样式/脚本改动必须即时到端;图片/字体/音频走默认)
|
// Revalidate every time: updates remain immediate; unchanged resources use
|
||||||
const headers = { 'Content-Type': MIME[ext] || 'application/octet-stream' };
|
// 304 instead of downloading again. APIs/private reports never enter here.
|
||||||
if (['.html', '.css', '.js'].includes(ext)) headers['Cache-Control'] = 'no-cache';
|
const headers = { 'Content-Type': MIME[ext] || 'application/octet-stream',
|
||||||
res.writeHead(200, headers);
|
'Cache-Control': 'no-cache', ETag: 'W/"' + crypto.createHash('sha256').update(data).digest('hex') + '"' };
|
||||||
res.end(data);
|
const compressible = /\.(html|css|js|json|svg)$/.test(file);
|
||||||
|
if (compressible) headers.Vary = 'Accept-Encoding';
|
||||||
|
if ((req.headers['if-none-match'] || '').split(/\s*,\s*/).includes(headers.ETag)) {
|
||||||
|
res.writeHead(304, headers); return res.end();
|
||||||
|
}
|
||||||
|
// Compress static public text only, not authenticated JSON or photographs.
|
||||||
|
const gzipAccepted = (req.headers['accept-encoding'] || '').split(',').some(value => {
|
||||||
|
const parts = value.trim().split(';');
|
||||||
|
return parts[0] === 'gzip' && !parts.slice(1).some(p => /^\s*q=0(?:\.0*)?\s*$/.test(p));
|
||||||
|
});
|
||||||
|
if (compressible && data.length > 1024 && gzipAccepted) {
|
||||||
|
zlib.gzip(data, (error, compressed) => {
|
||||||
|
if (!error) headers['Content-Encoding'] = 'gzip';
|
||||||
|
res.writeHead(200, headers); res.end(error ? data : compressed);
|
||||||
|
});
|
||||||
|
} else { res.writeHead(200, headers); res.end(data); }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ await page.addInitScript(() => {
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await page.goto(base, { waitUntil: 'domcontentloaded' });
|
await page.goto(base, { waitUntil: 'domcontentloaded' });
|
||||||
|
await page.addScriptTag({ url: base + 'vendor/faceapi/face-api.js' });
|
||||||
// Real models from production paths, actual landmark inference on the sample image.
|
// Real models from production paths, actual landmark inference on the sample image.
|
||||||
const detected = await page.evaluate(async data => {
|
const detected = await page.evaluate(async data => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -35,11 +36,13 @@ try {
|
|||||||
assert(detected && detected.landmarks === 68 && detected.score > 0.3);
|
assert(detected && detected.landmarks === 68 && detected.score > 0.3);
|
||||||
console.log('PASS: real production models load and detect sample face with 68 landmarks');
|
console.log('PASS: real production models load and detect sample face with 68 landmarks');
|
||||||
// Keep subsequent error-flow uploads independent of angle validation.
|
// Keep subsequent error-flow uploads independent of angle validation.
|
||||||
|
await page.route('**/vendor/faceapi/face-api.js', route => route.abort());
|
||||||
await page.evaluate(() => { window.faceapi = undefined; });
|
await page.evaluate(() => { window.faceapi = undefined; });
|
||||||
await page.waitForFunction(() => !document.querySelector('#login-message').textContent.includes('检查'));
|
await page.waitForFunction(() => !document.querySelector('#login-message').textContent.includes('检查'));
|
||||||
await page.check('#login-consent');
|
await page.check('#login-consent');
|
||||||
await page.click('[data-action="login-browser"]');
|
await page.click('[data-action="login-browser"]');
|
||||||
await page.waitForSelector('#s-splash.active');
|
await page.waitForSelector('#s-splash.active');
|
||||||
|
await page.click('[data-action="login-confirm"]');
|
||||||
await page.click('[data-action="open-privacy"]');
|
await page.click('[data-action="open-privacy"]');
|
||||||
await page.click('#privacy-check');
|
await page.click('#privacy-check');
|
||||||
await page.click('[data-action="agree-continue"]');
|
await page.click('[data-action="agree-continue"]');
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ try {
|
|||||||
await page.locator('#login-consent').check();
|
await page.locator('#login-consent').check();
|
||||||
await page.locator('[data-action="login-browser"]').click();
|
await page.locator('[data-action="login-browser"]').click();
|
||||||
await page.waitForSelector('#s-splash.active');
|
await page.waitForSelector('#s-splash.active');
|
||||||
|
await page.click('[data-action="login-confirm"]');
|
||||||
await page.getByRole('button', { name: '开始检测' }).click();
|
await page.getByRole('button', { name: '开始检测' }).click();
|
||||||
await page.locator('#privacy-check').click();
|
await page.locator('#privacy-check').click();
|
||||||
await page.getByRole('button', { name: '继续' }).click();
|
await page.getByRole('button', { name: '继续' }).click();
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
// Isolated server/data; no production account, report or paid API call.
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import http from 'node:http';
|
||||||
|
import zlib from 'node:zlib';
|
||||||
|
import { spawn, execSync } from 'node:child_process';
|
||||||
|
import { once } from 'node:events';
|
||||||
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
|
const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href);
|
||||||
|
const dataRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'juzi-login-perf-'));
|
||||||
|
const child = spawn(process.execPath, ['server.js'], {
|
||||||
|
cwd: fileURLToPath(new URL('../', import.meta.url)),
|
||||||
|
env: { ...process.env, PORT: '18091', HOST: '127.0.0.1', JUZI_DATA_ROOT: dataRoot,
|
||||||
|
SKIN_API_KEY: 'fixture', SKIN_API_SECRET: 'fixture', PUBLIC_BASE_URL: '', WECHAT_APP_ID: '', WECHAT_APP_SECRET: '' },
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
const base = 'http://127.0.0.1:18091/';
|
||||||
|
let browser;
|
||||||
|
async function raw(route, headers = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
http.get(base + route, { headers }, response => {
|
||||||
|
const chunks = [];
|
||||||
|
response.on('data', data => chunks.push(data));
|
||||||
|
response.on('end', () => resolve({ status: response.statusCode, headers: response.headers, body: Buffer.concat(chunks) }));
|
||||||
|
}).on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => reject(new Error('test startup timeout')), 10000);
|
||||||
|
child.stdout.on('data', b => { if (b.toString().includes('已启动')) { clearTimeout(timer); resolve(); } });
|
||||||
|
child.once('exit', code => { clearTimeout(timer); reject(new Error('server exited: ' + code)); });
|
||||||
|
});
|
||||||
|
for (const route of ['index.html', 'js/app.js', 'css/ui-20261009.css', 'vendor/faceapi/face-api.js']) {
|
||||||
|
const plain = await raw(route);
|
||||||
|
const compressed = await raw(route, { 'Accept-Encoding': 'gzip' });
|
||||||
|
assert.equal(compressed.status, 200);
|
||||||
|
assert.equal(compressed.headers['content-encoding'], 'gzip');
|
||||||
|
assert.equal(compressed.headers.vary, 'Accept-Encoding');
|
||||||
|
assert.equal(compressed.headers['cache-control'], 'no-cache');
|
||||||
|
assert.deepEqual(zlib.gunzipSync(compressed.body), plain.body);
|
||||||
|
assert(compressed.body.length < plain.body.length * .5, route);
|
||||||
|
const cached = await raw(route, { 'Accept-Encoding': 'gzip', 'If-None-Match': compressed.headers.etag });
|
||||||
|
assert.equal(cached.status, 304); assert.equal(cached.body.length, 0);
|
||||||
|
assert.equal((await raw(route, { 'Accept-Encoding': 'gzip;q=0' })).headers['content-encoding'], undefined);
|
||||||
|
}
|
||||||
|
const session = await raw('api/auth/session', { 'Accept-Encoding': 'gzip' });
|
||||||
|
assert.equal(session.headers['cache-control'], 'no-store');
|
||||||
|
assert.equal(session.headers.etag, undefined);
|
||||||
|
assert.equal(session.headers['content-encoding'], undefined);
|
||||||
|
assert.equal((await raw('data/accounts/identities.json')).status, 403);
|
||||||
|
browser = await chromium.launch({ headless: true });
|
||||||
|
const page = await browser.newPage({ viewport: { width: 375, height: 726 } });
|
||||||
|
const errors = [], requests = [];
|
||||||
|
page.on('pageerror', error => errors.push(error.message));
|
||||||
|
page.on('request', request => requests.push(new URL(request.url()).pathname));
|
||||||
|
// If the old eager script is reintroduced, it will fail instead of silently
|
||||||
|
// passing on a fast developer machine. Login must not depend on this request.
|
||||||
|
await page.route('**/vendor/faceapi/face-api.js', route => route.abort());
|
||||||
|
const cdp = await page.context().newCDPSession(page);
|
||||||
|
await cdp.send('Network.enable');
|
||||||
|
await cdp.send('Network.emulateNetworkConditions', { offline: false, latency: 150,
|
||||||
|
downloadThroughput: 250000, uploadThroughput: 125000 });
|
||||||
|
await cdp.send('Emulation.setCPUThrottlingRate', { rate: 4 });
|
||||||
|
const start = Date.now();
|
||||||
|
await page.goto(base, { waitUntil: 'domcontentloaded' });
|
||||||
|
await page.waitForFunction(() => !document.querySelector('#login-message').textContent.includes('检查'));
|
||||||
|
const readyMs = Date.now() - start;
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
assert(!requests.some(p => /faceapi|models|\.woff2$|bg-splash\.jpg$/.test(p)), JSON.stringify(requests));
|
||||||
|
const resourceBytes = await page.evaluate(() => performance.getEntriesByType('resource').reduce((sum, r) => sum + r.encodedBodySize, 0));
|
||||||
|
assert(resourceBytes < 350000, 'login resource budget exceeded: ' + resourceBytes);
|
||||||
|
await page.check('#login-consent');
|
||||||
|
await page.click('[data-action="login-browser"]');
|
||||||
|
await page.waitForSelector('#p-login-success.show');
|
||||||
|
const artifacts = process.env.H5_ARTIFACT_DIR;
|
||||||
|
if (artifacts) {
|
||||||
|
fs.mkdirSync(artifacts, { recursive: true });
|
||||||
|
await page.screenshot({ path: path.join(artifacts, 'browser-login-success.png') });
|
||||||
|
}
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
assert.equal(await page.locator('#p-login-success.show').count(), 0);
|
||||||
|
assert.equal(await page.locator('#s-splash').evaluate(e => e.inert), false);
|
||||||
|
for (const width of [320, 375, 390, 480]) {
|
||||||
|
await page.setViewportSize({ width, height: 726 });
|
||||||
|
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
||||||
|
const badge = await page.locator('.account-status-home').boundingBox();
|
||||||
|
const slogan = await page.locator('#s-splash .splash-slogan').boundingBox();
|
||||||
|
assert(badge.y + badge.height <= slogan.y, 'account badge overlaps home slogan at ' + width);
|
||||||
|
}
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
assert(!requests.some(p => /faceapi|models|\.woff2$/.test(p)), JSON.stringify(requests));
|
||||||
|
if (artifacts) await page.screenshot({ path: path.join(artifacts, 'browser-home-status.png') });
|
||||||
|
await page.click('[data-action="history"]');
|
||||||
|
await page.waitForFunction(() => document.querySelector('#history-count').textContent.includes('次'));
|
||||||
|
assert.equal(await page.locator('#s-history [data-auth-status]').textContent(), '本机浏览器已登录');
|
||||||
|
if (artifacts) await page.screenshot({ path: path.join(artifacts, 'browser-history-status.png') });
|
||||||
|
await page.click('#s-history [data-action="switch-login"]');
|
||||||
|
assert.equal(await page.locator('#s-login.active').count(), 1);
|
||||||
|
assert.deepEqual(errors, []);
|
||||||
|
console.log('PASS: gzip/304/revalidation, no-store private API, login without fonts/face engine, success dialog/Escape/status/switching/layout; throttled cold login=' + readyMs + 'ms, resources=' + resourceBytes + ' bytes');
|
||||||
|
// Lazy engine loading must still support real models and retry after failure.
|
||||||
|
await cdp.send('Network.emulateNetworkConditions', { offline: false, latency: 0, downloadThroughput: -1, uploadThroughput: -1 });
|
||||||
|
await cdp.send('Emulation.setCPUThrottlingRate', { rate: 1 });
|
||||||
|
await page.goBack();
|
||||||
|
await page.goBack();
|
||||||
|
await page.waitForSelector('#s-splash.active');
|
||||||
|
await page.evaluate(() => { navigator.mediaDevices.getUserMedia = () => Promise.reject(new DOMException('test denial', 'NotAllowedError')); });
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
await page.click('[data-action="submit-profile"]');
|
||||||
|
await page.click('[data-action="start-camera"]');
|
||||||
|
await page.waitForFunction(() => document.querySelector('#toast').textContent.includes('系统相机'));
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
assert.equal(requests.filter(p => p.endsWith('/face-api.js')).length, 1, 'engine must load only when entering camera');
|
||||||
|
await page.goBack();
|
||||||
|
await page.waitForSelector('#s-profile.active');
|
||||||
|
await page.unroute('**/vendor/faceapi/face-api.js');
|
||||||
|
await page.click('[data-action="submit-profile"]');
|
||||||
|
await page.click('[data-action="start-camera"]');
|
||||||
|
await page.waitForFunction(() => window.faceapi && faceapi.nets.tinyFaceDetector.isLoaded && faceapi.nets.faceLandmark68Net.isLoaded);
|
||||||
|
assert.equal(requests.filter(p => p.endsWith('/face-api.js')).length, 2, 'failed lazy engine should be retried');
|
||||||
|
const sample = fs.readFileSync(fileURLToPath(new URL('../../reference/facepp-skinanalyze/examples/images/C3_180.jpg', import.meta.url)));
|
||||||
|
const face = await page.evaluate(async data => {
|
||||||
|
const image = await faceapi.fetchImage(data);
|
||||||
|
const found = await faceapi.detectSingleFace(image, new faceapi.TinyFaceDetectorOptions({ inputSize: 416, scoreThreshold: .3 })).withFaceLandmarks();
|
||||||
|
return found ? found.landmarks.positions.length : 0;
|
||||||
|
}, 'data:image/jpeg;base64,' + sample.toString('base64'));
|
||||||
|
assert.equal(face, 68, 'lazy-loaded engine must perform real landmark inference');
|
||||||
|
console.log('PASS: no engine at login; camera loads engine/models; failed script retries; real 68-point inference works');
|
||||||
|
// A stalled session request must leave a recoverable message, not spin forever
|
||||||
|
// or discard the saved browser credential. Only this test shortens the timeout.
|
||||||
|
const token = await page.evaluate(() => localStorage.getItem('juzi.identity.v1'));
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
const original = window.setTimeout;
|
||||||
|
window.setTimeout = (fn, ms, ...args) => original(fn, ms === 12000 ? 100 : ms, ...args);
|
||||||
|
});
|
||||||
|
await page.route('**/api/auth/session', () => {});
|
||||||
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||||
|
await page.waitForFunction(() => document.querySelector('#login-message').textContent.includes('网络响应超时'));
|
||||||
|
assert.equal(await page.evaluate(() => localStorage.getItem('juzi.identity.v1')), token);
|
||||||
|
assert.equal(await page.locator('#p-login-success.show').count(), 0);
|
||||||
|
console.log('PASS: stalled session timeout is visible and preserves browser identity');
|
||||||
|
} finally {
|
||||||
|
if (browser) await browser.close();
|
||||||
|
const stopped = once(child, 'exit'); child.kill(); await stopped;
|
||||||
|
fs.rmSync(dataRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
@@ -37,6 +37,7 @@ try {
|
|||||||
await page.check('#login-consent');
|
await page.check('#login-consent');
|
||||||
await page.click('[data-action="login-browser"]');
|
await page.click('[data-action="login-browser"]');
|
||||||
await page.waitForSelector('#s-splash.active');
|
await page.waitForSelector('#s-splash.active');
|
||||||
|
await page.click('[data-action="login-confirm"]');
|
||||||
const api = context.request;
|
const api = context.request;
|
||||||
assert.equal((await api.get(base + 'api/reports')).status(), 200);
|
assert.equal((await api.get(base + 'api/reports')).status(), 200);
|
||||||
const cookie = (await context.cookies()).find(c => c.httpOnly);
|
const cookie = (await context.cookies()).find(c => c.httpOnly);
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ try {
|
|||||||
await page.check('#login-consent');
|
await page.check('#login-consent');
|
||||||
await page.click('[data-action="login-browser"]');
|
await page.click('[data-action="login-browser"]');
|
||||||
await page.waitForSelector('#s-splash.active');
|
await page.waitForSelector('#s-splash.active');
|
||||||
|
await page.click('[data-action="login-confirm"]');
|
||||||
const api = page.context().request;
|
const api = page.context().request;
|
||||||
const result = await (await api.post(base + 'api/skin-analyze', { data: { front_image: fixture.photos[0], left_image: fixture.photos[0], right_image: fixture.photos[0], profile: { real_age: 60, daily_skin_type: '干性肌肤' } } })).json();
|
const result = await (await api.post(base + 'api/skin-analyze', { data: { front_image: fixture.photos[0], left_image: fixture.photos[0], right_image: fixture.photos[0], profile: { real_age: 60, daily_skin_type: '干性肌肤' } } })).json();
|
||||||
assert.equal(result.ok, true);
|
assert.equal(result.ok, true);
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ await page.click('[data-action="login-wechat"]');
|
|||||||
await page.waitForFunction(() => document.querySelector('#login-message').textContent.includes('本机浏览器'));
|
await page.waitForFunction(() => document.querySelector('#login-message').textContent.includes('本机浏览器'));
|
||||||
await page.click('[data-action="login-browser"]');
|
await page.click('[data-action="login-browser"]');
|
||||||
await page.waitForSelector('#s-splash.active');
|
await page.waitForSelector('#s-splash.active');
|
||||||
|
await page.waitForSelector('#p-login-success.show');
|
||||||
|
await page.screenshot({ path: new URL('02-login-success.png', root).pathname });
|
||||||
|
await page.click('[data-action="login-confirm"]');
|
||||||
|
await page.waitForTimeout(400);
|
||||||
await page.waitForFunction(() => !document.querySelector('#toast').classList.contains('show'));
|
await page.waitForFunction(() => !document.querySelector('#toast').classList.contains('show'));
|
||||||
await page.waitForTimeout(400);
|
await page.waitForTimeout(400);
|
||||||
await page.screenshot({ path: new URL('02-home.png', root).pathname });
|
await page.screenshot({ path: new URL('02-home.png', root).pathname });
|
||||||
|
|||||||
@@ -56,6 +56,23 @@ try {
|
|||||||
const saved = { id: 'RwechatCrossDevice', created_at: new Date().toISOString(), vm,
|
const saved = { id: 'RwechatCrossDevice', created_at: new Date().toISOString(), vm,
|
||||||
presentation: ReportUI.build(vm, SkinScore.MOCK_RESULT), photos: [null, null, null],
|
presentation: ReportUI.build(vm, SkinScore.MOCK_RESULT), photos: [null, null, null],
|
||||||
payload: { result: SkinScore.MOCK_RESULT, side: SkinScore.MOCK_SIDE } };
|
payload: { result: SkinScore.MOCK_RESULT, side: SkinScore.MOCK_SIDE } };
|
||||||
|
async function confirmLogin(page, kind) {
|
||||||
|
await page.waitForSelector('#p-login-success.show');
|
||||||
|
assert.equal(await page.locator('#login-success-title').textContent(), '登录成功');
|
||||||
|
assert((await page.locator('#login-success-message').textContent()).includes(kind === 'wechat' ? '当前使用:微信登录' : '当前使用:本机浏览器登录'));
|
||||||
|
assert.equal(await page.locator('#s-splash [data-auth-status]').textContent(), kind === 'wechat' ? '微信已登录' : '本机浏览器已登录');
|
||||||
|
if (process.env.H5_ARTIFACT_DIR) {
|
||||||
|
fs.mkdirSync(process.env.H5_ARTIFACT_DIR, { recursive: true });
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
await page.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, kind + '-login-success.png'), animations: 'disabled' });
|
||||||
|
}
|
||||||
|
assert.equal(await page.evaluate(() => document.activeElement.dataset.action), 'login-confirm');
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
assert.equal(await page.evaluate(() => document.activeElement.dataset.action), 'login-confirm');
|
||||||
|
await page.click('[data-action="login-confirm"]');
|
||||||
|
assert.equal(await page.locator('#p-login-success.show').count(), 0);
|
||||||
|
assert.equal(await page.locator('#s-splash').evaluate(e => e.inert), false);
|
||||||
|
}
|
||||||
for (let device = 0; device < 2; device++) {
|
for (let device = 0; device < 2; device++) {
|
||||||
const context = await browser.newContext({ ignoreHTTPSErrors: true, userAgent: 'Mozilla/5.0 MicroMessenger/8.0', viewport: { width: 375, height: 726 } });
|
const context = await browser.newContext({ ignoreHTTPSErrors: true, userAgent: 'Mozilla/5.0 MicroMessenger/8.0', viewport: { width: 375, height: 726 } });
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
@@ -76,6 +93,7 @@ try {
|
|||||||
await page.check('#login-consent');
|
await page.check('#login-consent');
|
||||||
await page.click('[data-action="login-browser"]');
|
await page.click('[data-action="login-browser"]');
|
||||||
await page.waitForSelector('#s-splash.active');
|
await page.waitForSelector('#s-splash.active');
|
||||||
|
await confirmLogin(page, 'browser');
|
||||||
browserId = await page.evaluate(() => AccountClient.user().id);
|
browserId = await page.evaluate(() => AccountClient.user().id);
|
||||||
browserToken = await page.evaluate(() => localStorage.getItem('juzi.identity.v1'));
|
browserToken = await page.evaluate(() => localStorage.getItem('juzi.identity.v1'));
|
||||||
accounts.storeReport(browserId, { ...saved, id: 'RbrowserOriginal' });
|
accounts.storeReport(browserId, { ...saved, id: 'RbrowserOriginal' });
|
||||||
@@ -89,12 +107,14 @@ try {
|
|||||||
await page.click('[data-action="login-wechat"]');
|
await page.click('[data-action="login-wechat"]');
|
||||||
await page.waitForFunction(() => document.querySelector('#login-message').textContent.includes('微信授权未完成'));
|
await page.waitForFunction(() => document.querySelector('#login-message').textContent.includes('微信授权未完成'));
|
||||||
assert.equal(await page.locator('#s-login.active').count(), 1, 'OAuth failure must not masquerade as browser login success');
|
assert.equal(await page.locator('#s-login.active').count(), 1, 'OAuth failure must not masquerade as browser login success');
|
||||||
|
assert.equal(await page.locator('#p-login-success.show').count(), 0);
|
||||||
await page.waitForFunction(() => !location.search.includes('login_error'));
|
await page.waitForFunction(() => !location.search.includes('login_error'));
|
||||||
upstreamError = false;
|
upstreamError = false;
|
||||||
}
|
}
|
||||||
await page.check('#login-consent');
|
await page.check('#login-consent');
|
||||||
await page.click('[data-action="login-wechat"]');
|
await page.click('[data-action="login-wechat"]');
|
||||||
await page.waitForSelector('#s-splash.active');
|
await page.waitForSelector('#s-splash.active');
|
||||||
|
await confirmLogin(page, 'wechat');
|
||||||
await page.waitForFunction(() => !location.search.includes('login=success'));
|
await page.waitForFunction(() => !location.search.includes('login=success'));
|
||||||
const user = await page.evaluate(() => AccountClient.user());
|
const user = await page.evaluate(() => AccountClient.user());
|
||||||
assert.equal(user.kind, 'wechat');
|
assert.equal(user.kind, 'wechat');
|
||||||
@@ -104,6 +124,7 @@ try {
|
|||||||
assert(cookie.secure && cookie.httpOnly && cookie.path === '/state/' && cookie.sameSite === 'Lax');
|
assert(cookie.secure && cookie.httpOnly && cookie.path === '/state/' && cookie.sameSite === 'Lax');
|
||||||
await page.click('[data-action="history"]');
|
await page.click('[data-action="history"]');
|
||||||
await page.waitForFunction(() => document.querySelector('#identity-note').textContent.includes('同一微信账号'));
|
await page.waitForFunction(() => document.querySelector('#identity-note').textContent.includes('同一微信账号'));
|
||||||
|
assert.equal(await page.locator('#s-history [data-auth-status]').textContent(), '微信已登录');
|
||||||
await page.waitForSelector('[data-report-id="RwechatCrossDevice"]');
|
await page.waitForSelector('[data-report-id="RwechatCrossDevice"]');
|
||||||
assert.equal(await page.locator('[data-report-id="RbrowserOriginal"]').count(), 0);
|
assert.equal(await page.locator('[data-report-id="RbrowserOriginal"]').count(), 0);
|
||||||
assert.deepEqual((await api(page, 'reports/' + saved.id)).data.report, JSON.parse(JSON.stringify(saved)));
|
assert.deepEqual((await api(page, 'reports/' + saved.id)).data.report, JSON.parse(JSON.stringify(saved)));
|
||||||
@@ -117,12 +138,14 @@ try {
|
|||||||
await page.check('#login-consent');
|
await page.check('#login-consent');
|
||||||
await page.click('[data-action="login-wechat"]');
|
await page.click('[data-action="login-wechat"]');
|
||||||
await page.waitForSelector('#s-splash.active');
|
await page.waitForSelector('#s-splash.active');
|
||||||
|
await confirmLogin(page, 'wechat');
|
||||||
assert.equal(await page.evaluate(() => AccountClient.user().id), firstId);
|
assert.equal(await page.evaluate(() => AccountClient.user().id), firstId);
|
||||||
await page.click('[data-action="history"]');
|
await page.click('[data-action="history"]');
|
||||||
await page.click('[data-action="switch-login"]');
|
await page.click('[data-action="switch-login"]');
|
||||||
await page.check('#login-consent');
|
await page.check('#login-consent');
|
||||||
await page.click('[data-action="login-browser"]');
|
await page.click('[data-action="login-browser"]');
|
||||||
await page.waitForSelector('#s-splash.active');
|
await page.waitForSelector('#s-splash.active');
|
||||||
|
await confirmLogin(page, 'browser');
|
||||||
assert.equal(await page.evaluate(() => AccountClient.user().id), browserId);
|
assert.equal(await page.evaluate(() => AccountClient.user().id), browserId);
|
||||||
await page.click('[data-action="history"]');
|
await page.click('[data-action="history"]');
|
||||||
await page.waitForSelector('[data-report-id="RbrowserOriginal"]');
|
await page.waitForSelector('[data-report-id="RbrowserOriginal"]');
|
||||||
@@ -137,6 +160,7 @@ try {
|
|||||||
await page.reload(); // Ordinary browser restore is still supported.
|
await page.reload(); // Ordinary browser restore is still supported.
|
||||||
await page.waitForSelector('#s-splash.active');
|
await page.waitForSelector('#s-splash.active');
|
||||||
assert.equal(await page.evaluate(() => AccountClient.user().id), browserId);
|
assert.equal(await page.evaluate(() => AccountClient.user().id), browserId);
|
||||||
|
assert.equal(await page.locator('#p-login-success.show').count(), 0, 'session restore must not repeatedly open a success dialog');
|
||||||
}
|
}
|
||||||
assert.deepEqual(errors, []);
|
assert.deepEqual(errors, []);
|
||||||
await context.close();
|
await context.close();
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ for name in ['bg-splash', 'bg-login', 'bg-profile', 'bg-history', 'bg-report']:
|
|||||||
# Native status/navigation area belongs to the host, not the H5 document.
|
# Native status/navigation area belongs to the host, not the H5 document.
|
||||||
top = 253 if name == 'bg-report' else 248
|
top = 253 if name == 'bg-report' else 248
|
||||||
source.crop((0, top, source.width, source.height)).save(root / (name + '-h5.jpg'), quality=92, optimize=True)
|
source.crop((0, top, source.width, source.height)).save(root / (name + '-h5.jpg'), quality=92, optimize=True)
|
||||||
|
# Mobile delivery variant; preserve the full-resolution PSD exports/JPEGs.
|
||||||
|
Image.open(root / (name + '-h5.jpg')).save(root / (name + '-h5.webp'), quality=85, method=6)
|
||||||
|
|
||||||
geometry = []
|
geometry = []
|
||||||
for direction in ['front', 'left', 'right']:
|
for direction in ['front', 'left', 'right']:
|
||||||
|
|||||||
Reference in New Issue
Block a user