feat(h5): allow guest access without login and retain history only for WeChat
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
# 基础信息记忆与肤质拖动
|
||||
|
||||
> 2026-10-13 调整:以下偏好缓存仅对微信登录生效。游客仅保留当前页面操作中的临时表单值,不写缓存、不回填历史;刷新后重新填写。详见《游客不留存与弹窗护理间距调整-2026-10-13.md》。
|
||||
|
||||
## 范围
|
||||
|
||||
- 出生年月、性别、地区每次点滚轮“确定”即保存,不必拍照或提交报告。
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# 游客不留存与弹窗护理间距调整
|
||||
|
||||
## 发版范围
|
||||
|
||||
本次已完成本地开发、截图与用户确认,按要求提交和部署。发布沿用 Git bundle 快进同步至 `/home/ubuntu/juzi` 并重启 `juzi-h5.service`;不代用户推送 origin,不删除既有服务器历史数据或更改微信配置。发布后校验静态资源哈希、服务健康、免登录入口、协议同意前后临时会话边界和游客空历史;不调用线上付费检测或写入真实报告。
|
||||
|
||||
- 欢迎弹窗保留完整句子「我已阅读并同意相关隐私协议」,仅「隐私协议」四字加粗并下划线;「相关」恢复为普通文字,点击协议不勾选同意。
|
||||
- 报告护理建议标题的下方间距由 18u 减为 9u(375px 屏宽对应 18px→9px),仍左对齐,不影响其他标题。
|
||||
- 仅微信登录保留个人偏好和历史报告。微信现有账号、最近 5 份完整报告、跨手机查询、后台记录与原始结果留存不变。
|
||||
- 游客入口改为「免登录体验」:不要求登录页勾选协议,不发起登录请求、不弹登录成功,直接进入首页。开始检测时仍须在欢迎弹窗明确同意隐私协议,之后才建立后台检测所需的临时会话。首页/报告列表的「切换登录」按钮删除,状态改为不可点击的「游客模式」/「微信已登录」。微信登录保留在原入口页,以及游客历史页的「微信登录,保存报告」入口。
|
||||
- 游客使用内存中临时身份和非持久 Cookie,不落盘账号/会话,不再发送或保存可恢复的游客凭据。浏览器旧游客身份、基础信息和早期本地报告缓存清除;不清空其他应用或微信的缓存。
|
||||
- 游客基础信息只在当前页面操作中使用,例如拍摄失败返回可继续重拍;不写 localStorage,刷新/新游客登录后重新填写。游客报告仅供本次查看;「我的报告」显示不保存提示,不请求历史列表、不展示旧游客报告。
|
||||
- 服务端同样限制:游客成功/失败均不新增 records.jsonl、照片、原始结果和完整报告快照;检测和评分在内存中正常进行。检测响应设为 no-store,游客不出现「保存失败」误提示。该规则指本项目不留存,不代表上游检测供应商的数据政策发生变化。
|
||||
- 已有服务器游客历史数据不自动删除(避免未经确认批量清理);旧持久游客凭据不能恢复身份或读取历史。后续若需清理存量数据,应另行确认范围与备份。
|
||||
|
||||
## 验证
|
||||
|
||||
使用隔离临时目录、固定图片/上游响应与模拟微信;不操作线上账号/数据,不调用付费检测。
|
||||
|
||||
```bash
|
||||
H5_ARTIFACT_DIR=/tmp/juzi-guest-privacy-20261013 node h5/test/guest-privacy.mjs
|
||||
node h5/test/accounts.js
|
||||
node h5/test/profile-store.js
|
||||
node h5/test/profile-interaction.mjs
|
||||
node h5/test/report-persistence.mjs
|
||||
node h5/test/wechat-browser.mjs
|
||||
node h5/test/customer-feedback-fourth.mjs
|
||||
node h5/test/login-performance.mjs
|
||||
node h5/test/report-touch-layout.mjs
|
||||
```
|
||||
|
||||
新增端到端测试比对游客成功/失败前后的整个临时数据目录字节,确保无身份、照片、档案、报告、检测原文或后台记录写入;同一服务随后切换微信,验证报告/照片/后台记录正常保存。既有测试更新为仅微信持久化,保留跨设备、历史快照不可变、账号隔离和基础信息/拍照/报告交互回归。
|
||||
|
||||
截图:guest-entry.png、guest-home.png、welcome-privacy.png、care-spacing.png、guest-history.png(均为本地界面,报告内容为固定样例)。最新版可运行 `H5_ARTIFACT_DIR=/tmp/juzi-guest-no-login-20261013 node h5/test/guest-privacy.mjs` 生成。
|
||||
@@ -4,7 +4,7 @@
|
||||
"2": "app.js",
|
||||
"3": "skin-score.js",
|
||||
"4": "customer-feedback-second.js",
|
||||
"5": "t",
|
||||
"5": "disposeUnusedWeightTensors",
|
||||
"6": "n",
|
||||
"7": "test/profile-store.js",
|
||||
"8": "admin.js",
|
||||
@@ -13,19 +13,19 @@
|
||||
"11": "lib/accounts.js",
|
||||
"12": "server.js",
|
||||
"13": "files",
|
||||
"14": ".draw",
|
||||
"15": "extractFaces",
|
||||
"14": "NeuralNetwork",
|
||||
"15": "customer-feedback-fourth.mjs",
|
||||
"16": "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范",
|
||||
"17": "report-persistence.mjs",
|
||||
"18": "1'me肌肤状态检测 H5",
|
||||
"19": "customer-feedback-fourth.js",
|
||||
"20": "build_fixtures.py",
|
||||
"21": "disposeUnusedWeightTensors",
|
||||
"21": "getEnv",
|
||||
"22": "k",
|
||||
"23": "facepp-skinanalyze(旷视皮肤分析专业版调用技能)",
|
||||
"24": "巨子测肤 · 1'me 语音播报内容设计文档 V1.0",
|
||||
"25": "mobile-select.iife.min.js",
|
||||
"26": "NeuralNetwork",
|
||||
"26": ".extractWeights",
|
||||
"27": "gen_voice.py",
|
||||
"28": "C",
|
||||
"29": "face-api.js 本地化依赖(h5/vendor/faceapi/)",
|
||||
@@ -42,10 +42,11 @@
|
||||
"40": "第四轮反馈修改与验收",
|
||||
"41": "showScreen",
|
||||
"42": "layer_at",
|
||||
"43": "TinyYolov2Base",
|
||||
"43": "extractAllFacesAndComputeResults",
|
||||
"44": "start-local.sh",
|
||||
"45": "PredictAllAgeAndGenderWithFaceAlignmentTask",
|
||||
"46": "PredictAllFaceExpressionsWithFaceAlignmentTask",
|
||||
"47": "Box",
|
||||
"48": "已修改",
|
||||
"49": "mobile-select/README.md",
|
||||
"50": "call_skinanalyze_3view.sh",
|
||||
@@ -53,7 +54,7 @@
|
||||
"52": "region-data.js",
|
||||
"53": "护理建议(全量文本,按需求摘抄)",
|
||||
"54": "UI 对齐 10.09 设计稿:调研与实施计划",
|
||||
"55": "FaceLandmarks68",
|
||||
"55": "guest-privacy.mjs",
|
||||
"56": "report-ui.js",
|
||||
"57": "regression.js",
|
||||
"58": ".forwardInput",
|
||||
@@ -62,9 +63,9 @@
|
||||
"61": "报告布局与跟手交互调整",
|
||||
"62": "提交前 Review(2026-10-11)",
|
||||
"63": "e2e-browser.mjs",
|
||||
"64": "start",
|
||||
"64": "recommendation.js",
|
||||
"65": "微信登录接入与公网部署说明",
|
||||
"66": ".runAndExtendWithFaceDetections",
|
||||
"66": "h5/README.md",
|
||||
"67": "ui-20261009.mjs",
|
||||
"68": "analyze-timeout.js",
|
||||
"69": "misans/README.md",
|
||||
@@ -76,14 +77,14 @@
|
||||
"75": "prepare_h5_assets.py",
|
||||
"76": "报告页滑动防冲突-2026-10-13.md",
|
||||
"77": "drawFaceExpressions",
|
||||
"78": ".toString",
|
||||
"78": "FaceLandmarks68",
|
||||
"79": "登录反馈与加载优化(2026-10-12)",
|
||||
"80": "线上微信登录部署记录",
|
||||
"81": "wechat-production.mjs",
|
||||
"82": "拍摄约束放宽与标题更新",
|
||||
"83": "10.09 新版 H5 实施与验收",
|
||||
"84": "微信登录联调:2026-10-12",
|
||||
"85": "test/accounts.js",
|
||||
"85": "head-pose.js",
|
||||
"86": "report-touch-layout.mjs",
|
||||
"87": "产品推荐规则:肤龄基准与边界调整",
|
||||
"88": "ui-20261012/manifest.json",
|
||||
@@ -93,11 +94,14 @@
|
||||
"92": "客户反馈修改(2026-10-12)",
|
||||
"93": ".runAndExtendWithFaceDetection",
|
||||
"94": "第三轮反馈:PSD 对齐、验收及上线记录",
|
||||
"95": "login-performance.mjs",
|
||||
"96": "production-loop.mjs",
|
||||
"99": "DetectSingleFaceLandmarksTask",
|
||||
"100": "extractAllFacesAndComputeResults",
|
||||
"102": "extractFaceTensors",
|
||||
"95": "pn",
|
||||
"96": "start",
|
||||
"97": "account-client.js",
|
||||
"98": "js/profile-store.js",
|
||||
"99": "capture-diagnostics.js",
|
||||
"100": ".computeFaceDescriptor",
|
||||
"101": "wechat-fixture.cjs",
|
||||
"102": "extractFaces",
|
||||
"103": "第四轮反馈/傅艳NEW与项思炼的会话 2026年10月12日.md",
|
||||
"104": "export_fourth_feedback_assets.py",
|
||||
"105": "capture-quality-browser.mjs",
|
||||
@@ -107,12 +111,17 @@
|
||||
"109": "submitPhotos",
|
||||
"110": "doCapture",
|
||||
"111": "i",
|
||||
"112": ".toString",
|
||||
"113": "swipe-track.js",
|
||||
"114": "optimize_delivery_images.py",
|
||||
"116": "convLayer",
|
||||
"115": "ao",
|
||||
"116": "TinyYolov2Base",
|
||||
"117": "startCamera",
|
||||
"118": "PredictSingleAgeAndGenderWithFaceAlignmentTask",
|
||||
"119": "PredictSingleFaceExpressionsWithFaceAlignmentTask",
|
||||
"120": "LabeledFaceDescriptors",
|
||||
"121": ".runAndExtendWithFaceDetections",
|
||||
"123": "评分圈内居中、基础信息页固定与正脸语音修复",
|
||||
"124": "TinyYolov2",
|
||||
"125": "报告等待页实拍背景-2026-10-12.md",
|
||||
"127": "height"
|
||||
"125": "报告等待页实拍背景-2026-10-12.md"
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"0": "e93608cf288712b8", "1": "cb6ea50bd4e4a5ca", "2": "3427a6e8a9c7cb3e", "3": "1f737ca8edb978f2", "4": "56bf12952b0de42b", "5": "7eecef0f27ae2903", "6": "23e6fea2f88a24d1", "7": "ca1cdb7d0ae4e71e", "8": "a689fcb62e7754af", "9": "5b3557ee5c2bf900", "10": "2b037735e1f5b0f6", "11": "4a490abd5e503318", "12": "aeca274694e158d2", "13": "7e3b558a587a4ebe", "14": "bf75bfc99d4f9e34", "15": "6dfa695990b6977c", "16": "95add6a975c0cb72", "17": "e042d7fe7351bcb9", "18": "f93d5d85e671fcee", "19": "39abb1eef19df707", "20": "aecf2fce44ba49c5", "21": "6079f456ac9d4b2e", "22": "02495fa16ac642c3", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "a8588d8e77a6ae93", "27": "eab427ce9a5219e9", "28": "037cf5fc8fd22dd0", "29": "c608f1c6492e1cb1", "30": "73c13793fca09f56", "31": "fbbcfc059751cc6a", "32": "f6bcf56c4589316b", "33": "75f750c5b7d9f63d", "34": "f024ff7942935690", "35": "241be5abeab1da6f", "36": "a7d375041779bad4", "37": "cb8f26249485324a", "38": "9a25b1230b5fe649", "39": "fdd95375b4a5ddb3", "40": "5e546676ab96de03", "41": "71cbe5098f4c1b6f", "42": "916170b9f5d7346b", "43": "110bce64414980d4", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "48": "cf2f2424bc00436f", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "68c52c80f38f8b2c", "56": "9ee2ad45dc27851d", "57": "f2a40b5f1b29f669", "58": "d8c090d6837ee23a", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "5209ce2a267bf33b", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "a01bccd7f7c24c78", "65": "bdee3c4a37df52f8", "66": "46204fb2e4d50c53", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "be0c8310abda2467", "71": "589a2d5a76f3880a", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "3c4c278d6026bc3a", "77": "b4e08c87a078a832", "78": "71f300fdfae6e5a6", "79": "86f177bf0a7dd07c", "80": "315325cde1352e98", "81": "37c559741825be74", "82": "200f25fbad7af8fb", "83": "1284897282eb47a3", "84": "2fb17901aa87e0e7", "85": "4bc370be8afd7db1", "86": "386c6fcc7be918e0", "87": "e5fa7717989eda80", "88": "8b5ab5232cef3457", "89": "9e3363b80eeceeea", "90": "dd1c246e19a2d032", "91": "83b0fd3b14363e4d", "92": "fced244d93677d90", "93": "5a9014c4a7f87699", "94": "713d04996c4627df", "95": "59c31f858c7b6970", "96": "4195478d5c528371", "99": "23f1d406f6c779ae", "100": "e42f384dfedcff89", "102": "4dba2da8e6a57bef", "103": "6ffbe07b521a061b", "104": "e114a7aebaaca2f1", "105": "af50c66d16c81bba", "106": "8bc72c179793e1c2", "107": "d9c195d9ccdb137e", "108": "e0fc0a42e33bda91", "109": "4afb1715f62865b1", "110": "52f28eb30e80851f", "111": "b34758de9b00285f", "113": "0d99f86b9e41c4bb", "114": "b9d7c266afa84437", "116": "d1ec12eee057e765", "117": "cc191db3bbde70d7", "123": "b091e3eb82b456eb", "124": "c96276581a8c98d4", "125": "e4f29216745ec758", "127": "746b316833e9c203"}
|
||||
{"0": "6518846594ff224e", "1": "cb6ea50bd4e4a5ca", "2": "b161d025e66218cd", "3": "1f737ca8edb978f2", "4": "56bf12952b0de42b", "5": "5d94cf57d5d1a6fb", "6": "09c43aeeb7124cf2", "7": "03b56b13a08b5f83", "8": "a689fcb62e7754af", "9": "4d10d0cc23541396", "10": "2b037735e1f5b0f6", "11": "ed0d13cfa8571d24", "12": "aeca274694e158d2", "13": "7e3b558a587a4ebe", "14": "5fb489abcf0ab827", "15": "fbc8a72e6fda6359", "16": "95add6a975c0cb72", "17": "e042d7fe7351bcb9", "18": "f93d5d85e671fcee", "19": "2becf835f050dc29", "20": "aecf2fce44ba49c5", "21": "edc5fd195ace167e", "22": "4103800758a55924", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "92ef1d4e1a67b77f", "27": "eab427ce9a5219e9", "28": "de5647b9c3703ba7", "29": "c608f1c6492e1cb1", "30": "73c13793fca09f56", "31": "fbbcfc059751cc6a", "32": "f6bcf56c4589316b", "33": "75f750c5b7d9f63d", "34": "f024ff7942935690", "35": "241be5abeab1da6f", "36": "a7d375041779bad4", "37": "cb8f26249485324a", "38": "9a25b1230b5fe649", "39": "fdd95375b4a5ddb3", "40": "5e546676ab96de03", "41": "95da3c47e9f484e6", "42": "916170b9f5d7346b", "43": "3c55135955ae8607", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "88346d1cde75f0df", "48": "cf2f2424bc00436f", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "812950f51be20e40", "56": "9ee2ad45dc27851d", "57": "f2a40b5f1b29f669", "58": "74f01de01fa1f70c", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "5209ce2a267bf33b", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "e594ecaa8e7d1538", "65": "bdee3c4a37df52f8", "66": "a36e5eb320ca30dc", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "be0c8310abda2467", "71": "589a2d5a76f3880a", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "3c4c278d6026bc3a", "77": "6cd5c5b10bc3e59b", "78": "68c52c80f38f8b2c", "79": "c6cfdfe181232ecc", "80": "315325cde1352e98", "81": "37c559741825be74", "82": "200f25fbad7af8fb", "83": "1284897282eb47a3", "84": "d8bb296741d3d05c", "85": "0a373ad2197fd1a6", "86": "386c6fcc7be918e0", "87": "e5fa7717989eda80", "88": "8b5ab5232cef3457", "89": "9e3363b80eeceeea", "90": "dd1c246e19a2d032", "91": "83b0fd3b14363e4d", "92": "fced244d93677d90", "93": "987a3393280b3d91", "94": "713d04996c4627df", "95": "57b2d61a5df6f056", "96": "a01bccd7f7c24c78", "97": "55f3c43a702bef8a", "98": "9b1cf1985223f90a", "99": "9380a89a868c76d2", "100": "444ef0744645f5ac", "101": "beb276de6301634c", "102": "4a34431a7e4bc59f", "103": "6ffbe07b521a061b", "104": "e114a7aebaaca2f1", "105": "af50c66d16c81bba", "106": "8bc72c179793e1c2", "107": "3877490226a78145", "108": "e0fc0a42e33bda91", "109": "4afb1715f62865b1", "110": "52f28eb30e80851f", "111": "dc9609eb7ae61b97", "112": "064a2b8ba0231c0a", "113": "0d99f86b9e41c4bb", "114": "b9d7c266afa84437", "115": "a3859fc25c864a81", "116": "a99a3157460afca4", "117": "cc191db3bbde70d7", "118": "01f68dfc34a29d32", "119": "8c471c7baeaaca54", "120": "f8b38aeaaab11ec4", "121": "46204fb2e4d50c53", "123": "b091e3eb82b456eb", "124": "c96276581a8c98d4", "125": "e4f29216745ec758"}
|
||||
+143
-94
@@ -1,16 +1,16 @@
|
||||
# Graph Report - juzi (2026-10-13)
|
||||
|
||||
## Corpus Check
|
||||
- 107 files · ~4,905,775 words
|
||||
- 110 files · ~4,907,117 words
|
||||
- Verdict: corpus is large enough that graph structure adds value.
|
||||
|
||||
## Summary
|
||||
- 1457 nodes · 2904 edges · 116 communities (91 shown, 25 thin omitted)
|
||||
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 210 edges (avg confidence: 0.57)
|
||||
- 1475 nodes · 2920 edges · 125 communities (101 shown, 24 thin omitted)
|
||||
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 209 edges (avg confidence: 0.57)
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
## Graph Freshness
|
||||
- Built from commit: `4c478b16`
|
||||
- Built from commit: `98935dd1`
|
||||
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
||||
- Run `graphify update .` after code changes (no API cost).
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
- app.js
|
||||
- skin-score.js
|
||||
- customer-feedback-second.js
|
||||
- t
|
||||
- disposeUnusedWeightTensors
|
||||
- n
|
||||
- test/profile-store.js
|
||||
- admin.js
|
||||
@@ -29,19 +29,19 @@
|
||||
- lib/accounts.js
|
||||
- server.js
|
||||
- files
|
||||
- .draw
|
||||
- extractFaces
|
||||
- NeuralNetwork
|
||||
- customer-feedback-fourth.mjs
|
||||
- 巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范
|
||||
- report-persistence.mjs
|
||||
- 1'me肌肤状态检测 H5
|
||||
- customer-feedback-fourth.js
|
||||
- build_fixtures.py
|
||||
- disposeUnusedWeightTensors
|
||||
- getEnv
|
||||
- k
|
||||
- facepp-skinanalyze(旷视皮肤分析专业版调用技能)
|
||||
- 巨子测肤 · 1'me 语音播报内容设计文档 V1.0
|
||||
- mobile-select.iife.min.js
|
||||
- NeuralNetwork
|
||||
- .extractWeights
|
||||
- gen_voice.py
|
||||
- C
|
||||
- face-api.js 本地化依赖(h5/vendor/faceapi/)
|
||||
@@ -56,16 +56,17 @@
|
||||
- 第四轮反馈修改与验收
|
||||
- showScreen
|
||||
- layer_at
|
||||
- TinyYolov2Base
|
||||
- extractAllFacesAndComputeResults
|
||||
- start-local.sh
|
||||
- PredictAllAgeAndGenderWithFaceAlignmentTask
|
||||
- PredictAllFaceExpressionsWithFaceAlignmentTask
|
||||
- Box
|
||||
- 已修改
|
||||
- mobile-select/README.md
|
||||
- call_skinanalyze_3view.sh
|
||||
- 护理建议(全量文本,按需求摘抄)
|
||||
- UI 对齐 10.09 设计稿:调研与实施计划
|
||||
- FaceLandmarks68
|
||||
- guest-privacy.mjs
|
||||
- report-ui.js
|
||||
- regression.js
|
||||
- .forwardInput
|
||||
@@ -73,9 +74,9 @@
|
||||
- 报告布局与跟手交互调整
|
||||
- 提交前 Review(2026-10-11)
|
||||
- e2e-browser.mjs
|
||||
- start
|
||||
- recommendation.js
|
||||
- 微信登录接入与公网部署说明
|
||||
- .runAndExtendWithFaceDetections
|
||||
- h5/README.md
|
||||
- ui-20261009.mjs
|
||||
- analyze-timeout.js
|
||||
- misans/README.md
|
||||
@@ -84,14 +85,14 @@
|
||||
- build_report_copy.py
|
||||
- 报告页滑动防冲突-2026-10-13.md
|
||||
- drawFaceExpressions
|
||||
- .toString
|
||||
- FaceLandmarks68
|
||||
- 登录反馈与加载优化(2026-10-12)
|
||||
- 线上微信登录部署记录
|
||||
- wechat-production.mjs
|
||||
- 拍摄约束放宽与标题更新
|
||||
- 10.09 新版 H5 实施与验收
|
||||
- 微信登录联调:2026-10-12
|
||||
- test/accounts.js
|
||||
- head-pose.js
|
||||
- report-touch-layout.mjs
|
||||
- 产品推荐规则:肤龄基准与边界调整
|
||||
- ui-20261012/manifest.json
|
||||
@@ -100,11 +101,14 @@
|
||||
- 客户反馈修改(2026-10-12)
|
||||
- .runAndExtendWithFaceDetection
|
||||
- 第三轮反馈:PSD 对齐、验收及上线记录
|
||||
- login-performance.mjs
|
||||
- production-loop.mjs
|
||||
- DetectSingleFaceLandmarksTask
|
||||
- extractAllFacesAndComputeResults
|
||||
- extractFaceTensors
|
||||
- pn
|
||||
- start
|
||||
- account-client.js
|
||||
- js/profile-store.js
|
||||
- capture-diagnostics.js
|
||||
- .computeFaceDescriptor
|
||||
- wechat-fixture.cjs
|
||||
- extractFaces
|
||||
- 第四轮反馈/傅艳NEW与项思炼的会话 2026年10月12日.md
|
||||
- 第三轮反馈20261012/傅艳NEW与项思炼的会话 2026年10月12日.md
|
||||
- 基础信息记忆与肤质拖动
|
||||
@@ -112,12 +116,17 @@
|
||||
- submitPhotos
|
||||
- doCapture
|
||||
- i
|
||||
- convLayer
|
||||
- .toString
|
||||
- ao
|
||||
- TinyYolov2Base
|
||||
- startCamera
|
||||
- PredictSingleAgeAndGenderWithFaceAlignmentTask
|
||||
- PredictSingleFaceExpressionsWithFaceAlignmentTask
|
||||
- LabeledFaceDescriptors
|
||||
- .runAndExtendWithFaceDetections
|
||||
- 评分圈内居中、基础信息页固定与正脸语音修复
|
||||
- TinyYolov2
|
||||
- 报告等待页实拍背景-2026-10-12.md
|
||||
- height
|
||||
|
||||
## God Nodes (most connected - your core abstractions)
|
||||
1. `__awaiter()` - 59 edges
|
||||
@@ -146,11 +155,11 @@
|
||||
## Import Cycles
|
||||
- None detected.
|
||||
|
||||
## Communities (116 total, 25 thin omitted)
|
||||
## Communities (125 total, 24 thin omitted)
|
||||
|
||||
### Community 0 - "face-api.js"
|
||||
Cohesion: 0.02
|
||||
Nodes (41): bgrToRgbTensor(), cn(), computeReshapedDimensions(), createBrowserEnv(), createFileSystem(), createNodejsEnv(), decodeBoxesLayer(), Dimensions() (+33 more)
|
||||
Nodes (36): cn(), createBrowserEnv(), createFileSystem(), createNodejsEnv(), decodeBoxesLayer(), dr(), en(), FaceDetection() (+28 more)
|
||||
|
||||
### Community 1 - "线上闭环测试(不含真实微信授权)"
|
||||
Cohesion: 0.25
|
||||
@@ -158,7 +167,7 @@ Nodes (7): 发现并修复, 可复现命令, 已通过的检查, 线上闭环测
|
||||
|
||||
### Community 2 - "app.js"
|
||||
Cohesion: 0.11
|
||||
Nodes (22): cancelSkinDrag(), esc(), goSkin(), openHistory(), renderHistoryList(), renderReport(), renderViz(), restartScanCycle() (+14 more)
|
||||
Nodes (21): cancelSkinDrag(), esc(), goSkin(), loginAllowed(), loginWechat(), openHistory(), renderHistoryList(), renderReport() (+13 more)
|
||||
|
||||
### Community 3 - "skin-score.js"
|
||||
Cohesion: 0.12
|
||||
@@ -168,33 +177,33 @@ Nodes (45): acneGrade(), buildViewModel(), calcAcne(), calcContour(), calcSens()
|
||||
Cohesion: 0.17
|
||||
Nodes (10): assert, context, face, fs, geometry, original, path, source (+2 more)
|
||||
|
||||
### Community 5 - "t"
|
||||
### Community 5 - "disposeUnusedWeightTensors"
|
||||
Cohesion: 0.09
|
||||
Nodes (31): be(), bi(), Bo(), ce(), cl(), de(), e(), Ef() (+23 more)
|
||||
Nodes (23): disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1(), extractParamsFromWeigthMap$2() (+15 more)
|
||||
|
||||
### Community 6 - "n"
|
||||
Cohesion: 0.14
|
||||
Nodes (37): a(), ae(), bf(), cf(), d(), drawContour(), dt(), ea() (+29 more)
|
||||
Cohesion: 0.17
|
||||
Nodes (38): a(), ae(), bf(), ce(), cf(), d(), de(), drawContour() (+30 more)
|
||||
|
||||
### Community 7 - "test/profile-store.js"
|
||||
Cohesion: 0.07
|
||||
Nodes (29): browser(), init(), read(), rememberKind(), request(), age(), clean(), create() (+21 more)
|
||||
Cohesion: 0.14
|
||||
Nodes (13): assert, data, empty, now, other, Profile, quota, regions (+5 more)
|
||||
|
||||
### Community 8 - "admin.js"
|
||||
Cohesion: 0.20
|
||||
Nodes (29): ageText(), api(), bind(), captureQualityHtml(), closeDetail(), collectFilters(), dimsHtml(), doRetry() (+21 more)
|
||||
|
||||
### Community 9 - "__awaiter"
|
||||
Cohesion: 0.13
|
||||
Nodes (12): __awaiter(), ComposableTask(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow(), __generator(), getModelUris() (+4 more)
|
||||
Cohesion: 0.18
|
||||
Nodes (9): __awaiter(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow(), __generator(), loadWeightMap(), Mtcnn() (+1 more)
|
||||
|
||||
### Community 10 - "wechat-browser.mjs"
|
||||
Cohesion: 0.22
|
||||
Nodes (6): createAccounts, h5, ReportUI, require, root, SkinScore
|
||||
|
||||
### Community 11 - "lib/accounts.js"
|
||||
Cohesion: 0.17
|
||||
Nodes (20): atomic(), body(), cookie(), cookies(), crypto, fs, json(), login() (+12 more)
|
||||
Cohesion: 0.07
|
||||
Nodes (34): atomic(), body(), cookie(), cookies(), crypto, fs, json(), login() (+26 more)
|
||||
|
||||
### Community 12 - "server.js"
|
||||
Cohesion: 0.08
|
||||
@@ -204,9 +213,13 @@ Nodes (52): accounts, appendRecord(), callFacepp(), CaptureDiagnostics, CRED, cr
|
||||
Cohesion: 0.12
|
||||
Nodes (15): files, analyze-fail, analyzing, countdown-321, hint-far, hint-front, hint-hold, hint-near (+7 more)
|
||||
|
||||
### Community 15 - "extractFaces"
|
||||
Cohesion: 0.17
|
||||
Nodes (18): awaitMediaLoaded(), bufferToImage(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractImagePatches(), getContext2dOrThrow(), getEnv() (+10 more)
|
||||
### Community 14 - "NeuralNetwork"
|
||||
Cohesion: 0.20
|
||||
Nodes (3): getModelUris(), NeuralNetwork(), TinyFaceDetector()
|
||||
|
||||
### Community 15 - "customer-feedback-fourth.mjs"
|
||||
Cohesion: 0.20
|
||||
Nodes (8): evaluate(), finite(), CaptureQuality, child, dataRoot, h5, ReportUI, require
|
||||
|
||||
### Community 16 - "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范"
|
||||
Cohesion: 0.15
|
||||
@@ -221,20 +234,20 @@ Cohesion: 0.13
|
||||
Nodes (15): 10.09 更新, 1'me肌肤状态检测 H5, Admin API(统一返回 `{ok:false,message}` 错误), Windows 本机部署(提测用), 后台查看(admin.html), 密钥配置, 已知占位清单, 技术说明 (+7 more)
|
||||
|
||||
### Community 19 - "customer-feedback-fourth.js"
|
||||
Cohesion: 0.06
|
||||
Nodes (37): evaluate(), finite(), assertPoints(), check(), clamp(), dist(), estimate(), makeFrontal() (+29 more)
|
||||
Cohesion: 0.14
|
||||
Nodes (15): assert, check(), Diagnostics, face(), HeadPose, held, ReportUI, normalized (+7 more)
|
||||
|
||||
### Community 20 - "build_fixtures.py"
|
||||
Cohesion: 0.40
|
||||
Nodes (9): build_case1(), build_case2(), build_truth(), col(), dump(), main(), parse_value(), set_path() (+1 more)
|
||||
|
||||
### Community 21 - "disposeUnusedWeightTensors"
|
||||
Cohesion: 0.05
|
||||
Nodes (39): conv(), conv$1(), convDown(), convLayer$1(), convNoRelu(), denseBlock3(), denseBlock4(), depthwiseSeparableConv() (+31 more)
|
||||
### Community 21 - "getEnv"
|
||||
Cohesion: 0.07
|
||||
Nodes (34): awaitMediaLoaded(), bufferToImage(), conv(), conv$1(), convDown(), convLayer$1(), convNoRelu(), createCanvas() (+26 more)
|
||||
|
||||
### Community 22 - "k"
|
||||
Cohesion: 0.10
|
||||
Nodes (21): b(), ba(), ca(), da(), fa(), gn(), k(), kn() (+13 more)
|
||||
Cohesion: 0.16
|
||||
Nodes (16): ba(), ca(), da(), fa(), k(), pa(), pd(), Pi() (+8 more)
|
||||
|
||||
### Community 23 - "facepp-skinanalyze(旷视皮肤分析专业版调用技能)"
|
||||
Cohesion: 0.20
|
||||
@@ -248,7 +261,7 @@ Nodes (8): 1. 音频格式约定(已定), 2. 播报清单(14 条), 3. 播放策
|
||||
Cohesion: 0.20
|
||||
Nodes (16): _arrayLikeToArray(), _arrayWithHoles(), _arrayWithoutHoles(), _classCallCheck(), _createClass(), _defineProperties(), h(), _iterableToArray() (+8 more)
|
||||
|
||||
### Community 26 - "NeuralNetwork"
|
||||
### Community 26 - ".extractWeights"
|
||||
Cohesion: 0.05
|
||||
Nodes (30): AgeGenderNet(), createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory() (+22 more)
|
||||
|
||||
@@ -258,7 +271,7 @@ Nodes (11): ndarray, Path, gen_countdown(), main(), normalize_clip(), 倒计时
|
||||
|
||||
### Community 28 - "C"
|
||||
Cohesion: 0.11
|
||||
Nodes (25): ao(), C(), cd(), co(), Do(), eo(), fo(), Ft() (+17 more)
|
||||
Nodes (28): Bo(), C(), cd(), cl(), co(), eo(), fo(), Ft() (+20 more)
|
||||
|
||||
### Community 29 - "face-api.js 本地化依赖(h5/vendor/faceapi/)"
|
||||
Cohesion: 0.29
|
||||
@@ -289,17 +302,25 @@ Cohesion: 0.22
|
||||
Nodes (8): PSD 来源, 前端参数及限制, 后台校准留痕, 已确认并实现, 生产部署及只读验收, 皱纹(紧塑力)原文, 第四轮反馈修改与验收, 验证
|
||||
|
||||
### Community 41 - "showScreen"
|
||||
Cohesion: 0.22
|
||||
Nodes (11): loadAppFont(), loginAllowed(), loginBrowser(), loginSucceeded(), loginWechat(), openOverlay(), prepareScanArtwork(), renderAccountStatus() (+3 more)
|
||||
Cohesion: 0.20
|
||||
Nodes (12): enterGuest(), loadAppFont(), loginSucceeded(), openOverlay(), prepareScanArtwork(), renderAccountStatus(), resetReport(), restartScanCycle() (+4 more)
|
||||
|
||||
### Community 42 - "layer_at"
|
||||
Cohesion: 0.18
|
||||
Nodes (11): list, bounds(), layer_at(), main(), save_image(), composite(), main(), sha256() (+3 more)
|
||||
|
||||
### Community 43 - "extractAllFacesAndComputeResults"
|
||||
Cohesion: 0.14
|
||||
Nodes (10): age(), extendWithAge(), extendWithFaceExpressions(), extendWithGender(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult(), PredictAllAgeAndGenderTask(), PredictAllFaceExpressionsTask() (+2 more)
|
||||
|
||||
### Community 44 - "start-local.sh"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): HOST, PORT, start-local.sh script
|
||||
|
||||
### Community 47 - "Box"
|
||||
Cohesion: 0.11
|
||||
Nodes (11): bgrToRgbTensor(), Box(), Dimensions(), extendWithFaceLandmarks(), FaceLandmarks(), isDimensions(), isValidNumber(), isValidProbablitiy() (+3 more)
|
||||
|
||||
### Community 48 - "已修改"
|
||||
Cohesion: 0.22
|
||||
Nodes (8): 12 日第二批:明确项修改, 公网部署与复核(2026-10-12,北京时间), 准备捕捉提醒, 字重, 已修改, 拍照展示, 首页高清批注, 验证与状态
|
||||
@@ -312,6 +333,10 @@ Nodes (19): 5\.1 轮廓(紧塑力), 5\.2 皱纹(紧塑力), 5\.3 肤色
|
||||
Cohesion: 0.11
|
||||
Nodes (17): 1. 结论和依据, 2. 统一比较口径, 3. 逐页差异和调整内容, 4. 报告数据如何接到新 UI, 5. 登录与“我的报告”的真实能力缺口, 6. 原稿中需要整理的内容, 7. 实施顺序与交付物, 8. 验收定义 (+9 more)
|
||||
|
||||
### Community 55 - "guest-privacy.mjs"
|
||||
Cohesion: 0.25
|
||||
Nodes (5): child, dataRoot, fx, h5, require
|
||||
|
||||
### Community 56 - "report-ui.js"
|
||||
Cohesion: 0.50
|
||||
Nodes (7): build(), count(), fill(), focusOf(), numeric(), scalar(), wrinkleDetails()
|
||||
@@ -321,8 +346,8 @@ Cohesion: 0.60
|
||||
Nodes (5): diffCtx(), loadJSON(), main(), okNum(), okStr()
|
||||
|
||||
### Community 58 - ".forwardInput"
|
||||
Cohesion: 0.19
|
||||
Nodes (8): FaceRecognitionNet(), iou(), nonMaxSuppression(), nonMaxSuppression$1(), scale(), stage1(), stage2(), stage3()
|
||||
Cohesion: 0.14
|
||||
Nodes (16): computeReshapedDimensions(), extractBoundingBoxes(), getSizesForScale(), iou(), isEven(), nonMaxSuppression(), nonMaxSuppression$1(), normalize$1() (+8 more)
|
||||
|
||||
### Community 61 - "报告布局与跟手交互调整"
|
||||
Cohesion: 0.33
|
||||
@@ -336,14 +361,18 @@ Nodes (4): 仍需公网/业务确认, 已发现并修复, 提交前 Review(202
|
||||
Cohesion: 0.29
|
||||
Nodes (4): EXPECTED, fails, pageErrors, pwRoot
|
||||
|
||||
### Community 64 - "start"
|
||||
Cohesion: 0.29
|
||||
Nodes (7): decode(), drain(), load(), preload(), start(), stopCur(), unlock()
|
||||
### Community 64 - "recommendation.js"
|
||||
Cohesion: 0.38
|
||||
Nodes (6): assert, check(), missing, raw, SS, UI
|
||||
|
||||
### Community 65 - "微信登录接入与公网部署说明"
|
||||
Cohesion: 0.29
|
||||
Nodes (7): 公网服务器配置, 公网真机验收清单, 反向代理与后台保护, 已完成的链路, 微信登录接入与公网部署说明, 数据、迁移与备份, 本机开发环境限制(不代表线上状态)
|
||||
|
||||
### Community 66 - "h5/README.md"
|
||||
Cohesion: 0.33
|
||||
Nodes (3): 本地待确认, 游客不留存与弹窗护理间距调整, 验证
|
||||
|
||||
### Community 68 - "analyze-timeout.js"
|
||||
Cohesion: 0.20
|
||||
Nodes (7): assert, end, fs, path, source, start, vm
|
||||
@@ -353,15 +382,11 @@ Cohesion: 0.29
|
||||
Nodes (3): errors, samples, unexpectedWrites
|
||||
|
||||
### Community 77 - "drawFaceExpressions"
|
||||
Cohesion: 0.12
|
||||
Nodes (10): DrawBox(), drawDetections(), drawFaceExpressions(), DrawFaceLandmarks(), FaceExpressions(), isWithFaceDetection(), isWithFaceExpressions(), isWithFaceLandmarks() (+2 more)
|
||||
|
||||
### Community 78 - ".toString"
|
||||
Cohesion: 0.12
|
||||
Nodes (11): FaceLandmarks5(), FaceMatch(), getCenterPoint(), la(), Point(), pr(), pt(), vt() (+3 more)
|
||||
Cohesion: 0.19
|
||||
Nodes (10): DrawBox(), drawDetections(), drawFaceExpressions(), DrawFaceLandmarks(), extendWithFaceDetection(), FaceExpressions(), isWithFaceDetection(), isWithFaceExpressions() (+2 more)
|
||||
|
||||
### Community 79 - "登录反馈与加载优化(2026-10-12)"
|
||||
Cohesion: 0.33
|
||||
Cohesion: 0.25
|
||||
Nodes (5): 体验变化, 公网部署后复测, 慢的原因与处理, 登录反馈与加载优化(2026-10-12), 验证
|
||||
|
||||
### Community 80 - "线上微信登录部署记录"
|
||||
@@ -381,12 +406,12 @@ Cohesion: 0.29
|
||||
Nodes (5): 10.09 新版 H5 实施与验收, 按确认项实现, 文档未定义部分的实施口径, 素材再生成, 验证命令
|
||||
|
||||
### Community 84 - "微信登录联调:2026-10-12"
|
||||
Cohesion: 0.22
|
||||
Cohesion: 0.33
|
||||
Nodes (6): 后续真机确认与体验优化, 微信登录联调:2026-10-12, 本轮修正, 白名单与凭据, 真实微信验收与排查, 自动验证
|
||||
|
||||
### Community 85 - "test/accounts.js"
|
||||
Cohesion: 0.22
|
||||
Nodes (9): assert, create, fs, http, os, path, ReportUI, SkinScore (+1 more)
|
||||
### Community 85 - "head-pose.js"
|
||||
Cohesion: 0.33
|
||||
Nodes (8): assertPoints(), check(), clamp(), dist(), estimate(), makeFrontal(), makeSide(), midpoint()
|
||||
|
||||
### Community 87 - "产品推荐规则:肤龄基准与边界调整"
|
||||
Cohesion: 0.33
|
||||
@@ -405,27 +430,47 @@ Cohesion: 0.33
|
||||
Nodes (5): 公网部署与复测, 客户反馈修改(2026-10-12), 皱纹数据说明, 逐项处理, 验证
|
||||
|
||||
### Community 93 - ".runAndExtendWithFaceDetection"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): allFacesMtcnn(), allFacesSsdMobilenetv1(), allFacesTinyYolov2(), detectAllFaces(), DetectSingleFaceTask()
|
||||
Cohesion: 0.23
|
||||
Nodes (7): allFacesMtcnn(), allFacesSsdMobilenetv1(), allFacesTinyYolov2(), ComposableTask(), detectAllFaces(), DetectSingleFaceTask(), Gf()
|
||||
|
||||
### Community 94 - "第三轮反馈:PSD 对齐、验收及上线记录"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): 原稿与导出可追溯性, 已完成, 本地验证, 生产部署及只读验收, 第三轮反馈:PSD 对齐、验收及上线记录
|
||||
|
||||
### Community 96 - "production-loop.mjs"
|
||||
### Community 95 - "pn"
|
||||
Cohesion: 0.22
|
||||
Nodes (8): bn(), fn(), ln(), pn(), un(), wl(), wn(), xn()
|
||||
|
||||
### Community 96 - "start"
|
||||
Cohesion: 0.29
|
||||
Nodes (7): decode(), drain(), load(), preload(), start(), stopCur(), unlock()
|
||||
|
||||
### Community 97 - "account-client.js"
|
||||
Cohesion: 0.57
|
||||
Nodes (7): browser(), clearGuestCache(), ensureGuest(), guest(), init(), rememberKind(), request()
|
||||
|
||||
### Community 98 - "js/profile-store.js"
|
||||
Cohesion: 0.67
|
||||
Nodes (3): clean(), create(), key()
|
||||
|
||||
### Community 99 - "capture-diagnostics.js"
|
||||
Cohesion: 0.38
|
||||
Nodes (6): assess(), CaptureQuality, HeadPose, normalize(), scalar(), views
|
||||
|
||||
### Community 100 - ".computeFaceDescriptor"
|
||||
Cohesion: 0.18
|
||||
Nodes (4): ComputeAllFaceDescriptorsTask(), ComputeSingleFaceDescriptorTask(), extendWithFaceDescriptor(), FaceRecognitionNet()
|
||||
|
||||
### Community 101 - "wechat-fixture.cjs"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): artifacts, errors, samples
|
||||
Nodes (3): crypto, fs, path
|
||||
|
||||
### Community 100 - "extractAllFacesAndComputeResults"
|
||||
Cohesion: 0.09
|
||||
Nodes (12): ComputeAllFaceDescriptorsTask(), ComputeSingleFaceDescriptorTask(), extendWithAge(), extendWithFaceDescriptor(), extendWithFaceExpressions(), extendWithGender(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult() (+4 more)
|
||||
|
||||
### Community 102 - "extractFaceTensors"
|
||||
Cohesion: 0.10
|
||||
Nodes (11): Box(), DetectAllFaceLandmarksTask(), extendWithFaceDetection(), extendWithFaceLandmarks(), extractFaceTensors(), FaceLandmarks(), isDimensions(), isTensor3D() (+3 more)
|
||||
### Community 102 - "extractFaces"
|
||||
Cohesion: 0.14
|
||||
Nodes (9): height(), DetectAllFaceLandmarksTask(), DetectSingleFaceLandmarksTask(), extractFaces(), extractFaceTensors(), FaceLandmark68NetBase(), imageTensorToCanvas(), isTensor3D() (+1 more)
|
||||
|
||||
### Community 107 - "基础信息记忆与肤质拖动"
|
||||
Cohesion: 0.33
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 回归命令, 基础信息记忆与肤质拖动, 肤质交互, 范围
|
||||
|
||||
### Community 108 - "profile-interaction.mjs"
|
||||
@@ -441,12 +486,20 @@ Cohesion: 0.17
|
||||
Nodes (22): armNudge(), busyWithFlow(), captureFromVideo(), captureHaptic(), captureStageHint(), checkStill(), doCapture(), dropHint() (+14 more)
|
||||
|
||||
### Community 111 - "i"
|
||||
Cohesion: 0.12
|
||||
Nodes (18): bn(), dn(), ee(), et(), fn(), i(), je(), ln() (+10 more)
|
||||
Cohesion: 0.09
|
||||
Nodes (29): b(), be(), bi(), dn(), ee(), Ef(), er(), et() (+21 more)
|
||||
|
||||
### Community 116 - "convLayer"
|
||||
Cohesion: 0.14
|
||||
Nodes (17): boxPredictionLayer(), convLayer(), convWithBatchNorm(), depthwiseConvLayer(), depthwiseSeparableConv$1(), fullyConnectedLayer(), getStridesForLayerIdx(), leaky() (+9 more)
|
||||
### Community 112 - ".toString"
|
||||
Cohesion: 0.18
|
||||
Nodes (7): FaceLandmarks5(), FaceMatch(), getCenterPoint(), la(), pr(), pt(), vt()
|
||||
|
||||
### Community 115 - "ao"
|
||||
Cohesion: 0.40
|
||||
Nodes (5): ao(), Do(), io(), so(), To()
|
||||
|
||||
### Community 116 - "TinyYolov2Base"
|
||||
Cohesion: 0.11
|
||||
Nodes (20): boxPredictionLayer(), convLayer(), convWithBatchNorm(), depthwiseConvLayer(), depthwiseSeparableConv$1(), fullyConnectedLayer(), getStridesForLayerIdx(), leaky() (+12 more)
|
||||
|
||||
### Community 117 - "startCamera"
|
||||
Cohesion: 0.15
|
||||
@@ -456,24 +509,20 @@ Nodes (17): applyMirror(), beginDetect(), detectFrame(), detTick(), effMirror(),
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 发布, 范围, 评分圈内居中、基础信息页固定与正脸语音修复, 验证
|
||||
|
||||
### Community 127 - "height"
|
||||
Cohesion: 0.38
|
||||
Nodes (5): height(), FaceLandmark68NetBase(), getSizesForScale(), normalize$1(), rescaleAndNormalize()
|
||||
|
||||
## Knowledge Gaps
|
||||
- **320 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+315 more)
|
||||
- **330 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+325 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components.
|
||||
- **25 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
- **24 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
|
||||
## Suggested Questions
|
||||
_Questions this graph is uniquely positioned to answer:_
|
||||
|
||||
- **Why does `height()` connect `height` to `app.js`, `customer-feedback.js`, `scan-placeholder.mjs`, `extractFaceTensors`, `showScreen`, `__awaiter`, `profile-interaction.mjs`, `extractFaces`, `.forwardInput`?**
|
||||
_High betweenness centrality (0.062) - this node is a cross-community bridge._
|
||||
- **Why does `height()` connect `extractFaces` to `app.js`, `customer-feedback.js`, `scan-placeholder.mjs`, `showScreen`, `profile-interaction.mjs`, `.forwardInput`?**
|
||||
_High betweenness centrality (0.050) - this node is a cross-community bridge._
|
||||
- **Why does `v()` connect `n` to `report-ui.js`, `face-api.js`, `app.js`, `C`?**
|
||||
_High betweenness centrality (0.045) - this node is a cross-community bridge._
|
||||
- **Why does `k()` connect `k` to `face-api.js`, `app.js`, `t`, `n`, `lib/accounts.js`?**
|
||||
_High betweenness centrality (0.039) - this node is a cross-community bridge._
|
||||
_High betweenness centrality (0.038) - this node is a cross-community bridge._
|
||||
- **Why does `r()` connect `n` to `face-api.js`, `server.js`, `i`, `.toString`, `getEnv`, `k`, `C`?**
|
||||
_High betweenness centrality (0.037) - this node is a cross-community bridge._
|
||||
- **Are the 24 inferred relationships involving `n()` (e.g. with `newRecordId()` and `ae()`) actually correct?**
|
||||
_`n()` has 24 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 27 inferred relationships involving `r()` (e.g. with `handleExport()` and `handleRecordsQuery()`) actually correct?**
|
||||
@@ -481,4 +530,4 @@ _Questions this graph is uniquely positioned to answer:_
|
||||
- **Are the 9 inferred relationships involving `i()` (e.g. with `da()` and `f()`) actually correct?**
|
||||
_`i()` has 9 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **What connects `source`, `document_size`, `pages` to the rest of the system?**
|
||||
_320 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
_330 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
File diff suppressed because one or more lines are too long
+1925
-1547
File diff suppressed because it is too large
Load Diff
+53
-38
@@ -40,8 +40,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/app.js": {
|
||||
"mtime": 1791823225.0391843,
|
||||
"ast_hash": "4787d84f22ac0257204e8ddbdd421140",
|
||||
"mtime": 1791875688.6889038,
|
||||
"ast_hash": "4d2624826f32e0c1af544e450117fcc6",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/head-pose.js": {
|
||||
@@ -55,8 +55,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/server.js": {
|
||||
"mtime": 1791800754.2112646,
|
||||
"ast_hash": "9983e6c978b3357e31bbfce59202dcde",
|
||||
"mtime": 1791873518.1209404,
|
||||
"ast_hash": "d2b79f3c00c8a4eaecc3c74b1c1e4dea",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/build_fixtures.py": {
|
||||
@@ -135,13 +135,13 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/README.md": {
|
||||
"mtime": 1791811100.15898,
|
||||
"ast_hash": "4b8b63aa8184ead90d868aff34e9a6e3",
|
||||
"mtime": 1791873518.1209404,
|
||||
"ast_hash": "4acf1f915e74619da8124d47eeb4d971",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/index.html": {
|
||||
"mtime": 1791821148.115547,
|
||||
"ast_hash": "770a726605078c6c0eebd95304e8e954",
|
||||
"mtime": 1791875481.336928,
|
||||
"ast_hash": "40dc2f9b93aceaa4283f27b550659731",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/vendor/faceapi/README.md": {
|
||||
@@ -770,8 +770,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/e2e-browser.mjs": {
|
||||
"mtime": 1791773638.010959,
|
||||
"ast_hash": "9327216df7c2e80f2615cb7f800b7f1a",
|
||||
"mtime": 1791875688.6889038,
|
||||
"ast_hash": "ca6e17bc320f31adb34e5b9e0ed28f81",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/svg/prod-badge.svg": {
|
||||
@@ -1230,8 +1230,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/account-client.js": {
|
||||
"mtime": 1791774062.6429472,
|
||||
"ast_hash": "3197b952f29f9e45176d4300082adaae",
|
||||
"mtime": 1791875481.336928,
|
||||
"ast_hash": "d296f1dfb67a31adfde3221d48439186",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/camera-geometry.js": {
|
||||
@@ -1250,23 +1250,23 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/lib/accounts.js": {
|
||||
"mtime": 1791806985.2188702,
|
||||
"ast_hash": "a8116f973f1e01dcbfbe2e1e26c7c826",
|
||||
"mtime": 1791872963.6609082,
|
||||
"ast_hash": "cfc1a9590dc7e1ee9a8faf2fa23d6eaa",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/accounts.js": {
|
||||
"mtime": 1791770576.0104883,
|
||||
"ast_hash": "d927f5d1486b56505ba4da49ec6a769c",
|
||||
"mtime": 1791873124.7889152,
|
||||
"ast_hash": "85fe766a7c305ffee3a8c904e038d8a6",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/report-persistence.mjs": {
|
||||
"mtime": 1791793231.819183,
|
||||
"ast_hash": "24636f96fd5cd576cde11ef9e911f3ff",
|
||||
"mtime": 1791873250.4369178,
|
||||
"ast_hash": "1d543844b6961902d3cfcafa713b9a3b",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/ui-20261009.mjs": {
|
||||
"mtime": 1791800811.939266,
|
||||
"ast_hash": "778edb5314c2d35ae75c05344b96f532",
|
||||
"mtime": 1791875688.6929038,
|
||||
"ast_hash": "22779f48c489e35943f89eae9a81c17b",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"tools/build_report_copy.py": {
|
||||
@@ -1530,8 +1530,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/wechat-browser.mjs": {
|
||||
"mtime": 1791800811.939266,
|
||||
"ast_hash": "616790aae9cf9b41d30ee8b8da979cb0",
|
||||
"mtime": 1791875570.3129308,
|
||||
"ast_hash": "3f5bcc6b56b3c28ecd7fb244ca645865",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/2026-10-11\u63d0\u4ea4\u524dReview.md": {
|
||||
@@ -1545,8 +1545,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/production-loop.mjs": {
|
||||
"mtime": 1791773638.010959,
|
||||
"ast_hash": "0d0d46ec3ecfce9e767e7f31050befe2",
|
||||
"mtime": 1791875833.91293,
|
||||
"ast_hash": "60cd5e73b5600b731b0e01b0e12e12f4",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u7ebf\u4e0a\u5fae\u4fe1\u767b\u5f55\u90e8\u7f72\u8bb0\u5f55-2026-10-11.md": {
|
||||
@@ -1555,8 +1555,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/browser-failures.mjs": {
|
||||
"mtime": 1791773638.010959,
|
||||
"ast_hash": "8cb2774864d3d59bd5a418ad61819129",
|
||||
"mtime": 1791875688.6929038,
|
||||
"ast_hash": "00147b9f66e099be169cb84bf6331e66",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u7ebf\u4e0a\u95ed\u73af\u6d4b\u8bd5-2026-10-11.md": {
|
||||
@@ -1585,8 +1585,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/login-performance.mjs": {
|
||||
"mtime": 1791800811.939266,
|
||||
"ast_hash": "ffe9df947564cdef24e8f842cbc577f1",
|
||||
"mtime": 1791875688.6889038,
|
||||
"ast_hash": "1f0d6e798cf1df853eb1309a171b2122",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/img/ui-20261009/bg-history-h5.webp": {
|
||||
@@ -1825,8 +1825,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/customer-feedback-fourth.mjs": {
|
||||
"mtime": 1791809137.130926,
|
||||
"ast_hash": "a07ebd722efaf2d8b6e382f566a5f649",
|
||||
"mtime": 1791875538.8449328,
|
||||
"ast_hash": "4aa0aa1bbb49a2272abd459014646b62",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"tools/export_fourth_feedback_assets.py": {
|
||||
@@ -1890,23 +1890,23 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/profile-store.js": {
|
||||
"mtime": 1791810317.830946,
|
||||
"ast_hash": "5723356fa753e9a66fdee448e617390e",
|
||||
"mtime": 1791872891.6128974,
|
||||
"ast_hash": "67650e3352c06e2a9ab8965d8da3d421",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/profile-interaction.mjs": {
|
||||
"mtime": 1791820047.336818,
|
||||
"ast_hash": "2efa96cee5dcf3671644b1b82f890ee3",
|
||||
"mtime": 1791873124.7889152,
|
||||
"ast_hash": "4f1fcae2e04530f3a85b33e5ee12453d",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/profile-store.js": {
|
||||
"mtime": 1791810317.8349462,
|
||||
"ast_hash": "66a0e7d6ddc5eee9cd69c097257746d1",
|
||||
"mtime": 1791873040.5609126,
|
||||
"ast_hash": "412a4fc48325d5ad886994c0e22adad0",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u57fa\u7840\u4fe1\u606f\u8bb0\u5fc6\u4e0e\u80a4\u8d28\u62d6\u52a8-2026-10-12.md": {
|
||||
"mtime": 1791810600.0989332,
|
||||
"ast_hash": "cec2f2e829751e8b5d2e5e40c348f5e4",
|
||||
"mtime": 1791873518.1209404,
|
||||
"ast_hash": "e6e11bfd6642c38c51d11ebe85499b98",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u62a5\u544a\u7b49\u5f85\u9875\u5b9e\u62cd\u80cc\u666f-2026-10-12.md": {
|
||||
@@ -1973,5 +1973,20 @@
|
||||
"mtime": 1791821522.1000154,
|
||||
"ast_hash": "517105aaf52427102a325e0450142446",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/guest-privacy.mjs": {
|
||||
"mtime": 1791875538.8409328,
|
||||
"ast_hash": "29ab19bbdfe2379c641554c2c68e526f",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/wechat-fixture.cjs": {
|
||||
"mtime": 1791873124.7849152,
|
||||
"ast_hash": "bf1d2d3b6c76743b42c9fcfa3f18444d",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u6e38\u5ba2\u4e0d\u7559\u5b58\u4e0e\u5f39\u7a97\u62a4\u7406\u95f4\u8ddd\u8c03\u6574-2026-10-13.md": {
|
||||
"mtime": 1791875833.91693,
|
||||
"ast_hash": "056111d384a7d74c51a9187063e03bdf",
|
||||
"semantic_hash": ""
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,8 @@ node server.js # 默认 8080,可用 PORT=xxx 修改
|
||||
|
||||
## 档案页与报告页
|
||||
|
||||
2026-10-13 当前留存规则:仅微信登录记住本机基础信息、在后端保留最近 5 份完整报告并支持跨设备查看。游客使用仅内存的临时会话,不持久化身份/基础信息/历史报告,也不新增后台检测记录、照片或原始结果;本次报告仍可正常生成并查看,刷新后基础信息不回填。旧服务器数据不自动删除。详见 [游客不留存与样式调整](../docs/游客不留存与弹窗护理间距调整-2026-10-13.md)。下方早期的 localStorage 报告历史说明已被此规则替代。
|
||||
|
||||
- **档案页**:出生年月 / 性别 / 所在地区三个滚轮选择器同一风格(`vendor/mobile-select`,MIT),出生年月自动换算年龄;地区为省–市两级联动,数据为 `js/region-data.js`(由 china-division 2.7.0 `pc.json` 生成,另含港澳台与「海外」)。当前账号在本机浏览器记住已确认的基础信息和日常肤质,再次进入自动回填,滚轮也定位到原值;年龄按出生年月重新计算。肤质支持跟手拖动、松手吸附、轻扫切换及边界回弹,纵向手势仍滚动页面。详见 [基础信息记忆与肤质拖动](../docs/基础信息记忆与肤质拖动-2026-10-12.md)。
|
||||
- **报告页**:无返回按钮与页头标题;总分环形图 + 肌肤年龄与「比实际年龄年轻/大 N 岁」;三维度分数 + 进度条(最低分标红并挂「优先关注」);维度详情为原布局——一张卡内紧塑力/活力感/稳定力三个 Tab,每个 Tab 两条「竖排标题 + 左图右文」;点检测图全屏放大,再点或按返回键回到报告。
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
#stat-score { display: flex; align-items: baseline; justify-content: center; }
|
||||
#stat-score b { display: block; letter-spacing: 0; }
|
||||
#stat-score i { position: static; margin-left: 2px; }
|
||||
.card-care .card-heading { text-align: left; }
|
||||
.card-care .card-heading { text-align: left; margin-bottom: calc(9 * var(--u)); }
|
||||
|
||||
/* The profile is a fixed page, not a scroll container. Keep normal-height PSD
|
||||
spacing; shrink only vertical whitespace/photo height on shorter viewports.
|
||||
|
||||
+4
-4
@@ -24,7 +24,7 @@
|
||||
<img class="login-brand" src="assets/img/ui-20261009/brand-login.png" alt="1’me 以我">
|
||||
<div class="login-bottom">
|
||||
<button class="login-button login-wechat" data-action="login-wechat"><img src="assets/img/ui-20261009/icon-wechat.png" alt="">微信登录</button>
|
||||
<button class="login-button login-browser" data-action="login-browser"><img src="assets/img/ui-20261009/icon-guest.png" alt="">游客登录</button>
|
||||
<button class="login-button login-browser" data-action="guest-experience"><img src="assets/img/ui-20261009/icon-guest.png" alt="">免登录体验</button>
|
||||
<label class="login-consent"><input type="checkbox" id="login-consent"><span>同意<button type="button" data-action="open-policy">用户协议、隐私政策和儿童保护政策</button></span></label>
|
||||
<p class="login-message" id="login-message" role="status">正在检查登录状态…</p>
|
||||
</div>
|
||||
@@ -32,7 +32,7 @@
|
||||
<section class="screen" id="s-history">
|
||||
<div class="history-content">
|
||||
<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="account-status account-status-history"><span data-auth-status></span></div>
|
||||
<div class="card history-count"><span>测试数量</span><p id="history-count">正在读取报告…</p></div>
|
||||
<p class="identity-note" id="identity-note"></p>
|
||||
<div id="history-list" aria-live="polite"></div>
|
||||
@@ -42,7 +42,7 @@
|
||||
</section>
|
||||
<section class="screen" id="s-splash">
|
||||
<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>
|
||||
<div class="account-status account-status-home"><span data-auth-status></span></div>
|
||||
<img class="splash-slogan" src="assets/img/ui-20261012/splash-slogan.png" width="718" height="217" loading="lazy" alt="ONE ME,FROM THE INSIDE OUT;以我为始 自有表现">
|
||||
<div class="splash-bottom">
|
||||
<div class="brand-row">
|
||||
@@ -291,7 +291,7 @@
|
||||
<p class="popup-body">我们将采集您的面部影像,为您生成专属肌肤分析与状态管理方案。所有数据严格保密,仅用于本次检测分析。</p>
|
||||
<label class="privacy-row" id="privacy-row">
|
||||
<span class="checkbox" id="privacy-check"></span>
|
||||
<span class="privacy-text">我已阅读并同意<a class="privacy-link" id="privacy-policy-link">相关隐私协议</a></span>
|
||||
<span class="privacy-text">我已阅读并同意相关<a class="privacy-link" id="privacy-policy-link">隐私协议</a></span>
|
||||
</label>
|
||||
<button class="btn btn-primary popup-btn" data-action="agree-continue">继续</button>
|
||||
</div>
|
||||
|
||||
+32
-15
@@ -1,9 +1,19 @@
|
||||
/* Browser identity is a random credential, never a fingerprint or a report. */
|
||||
/* Guest access uses an ephemeral server session, never a saved browser identity. */
|
||||
window.AccountClient = (function () {
|
||||
'use strict';
|
||||
var key = 'juzi.identity.v1', kindKey = 'juzi.auth.kind.v1', user = null;
|
||||
function read(name) { try { return localStorage.getItem(name || key); } catch (_) { return null; } }
|
||||
function rememberKind() { try { if (user) localStorage.setItem(kindKey, user.kind); } catch (_) {} }
|
||||
var kindKey = 'juzi.auth.kind.v1', user = null, guestReady = false;
|
||||
function clearGuestCache() {
|
||||
try {
|
||||
localStorage.removeItem('juzi.identity.v1');
|
||||
localStorage.removeItem('juzi.reports.v1'); // Retired local guest-report cache.
|
||||
Object.keys(localStorage).filter(function (name) { return name.indexOf('juzi.profile.v1.browser.') === 0; })
|
||||
.forEach(function (name) { localStorage.removeItem(name); });
|
||||
if (localStorage.getItem(kindKey) === 'browser') localStorage.removeItem(kindKey);
|
||||
} catch (_) {}
|
||||
}
|
||||
function rememberKind() {
|
||||
try { if (user && user.kind === 'wechat') localStorage.setItem(kindKey, 'wechat'); else localStorage.removeItem(kindKey); } catch (_) {}
|
||||
}
|
||||
function request(url, data) {
|
||||
var controller = new AbortController();
|
||||
// Bound only authentication waits. Full report snapshots contain photos and
|
||||
@@ -20,24 +30,31 @@ window.AccountClient = (function () {
|
||||
}).finally(function () { clearTimeout(timer); });
|
||||
}
|
||||
function browser(consent) {
|
||||
return request('api/auth/browser', { consent: consent, token: read() }).then(function (v) {
|
||||
try { localStorage.setItem(key, v.token); } catch (_) { /* session cookie still works */ }
|
||||
user = v.user; rememberKind(); return v;
|
||||
clearGuestCache();
|
||||
return request('api/auth/browser', { consent: consent }).then(function (v) {
|
||||
user = v.user; guestReady = true; rememberKind(); return v;
|
||||
});
|
||||
}
|
||||
function guest() {
|
||||
clearGuestCache();
|
||||
user = { id: 'guest-page-' + Date.now() + '-' + Math.random().toString(36).slice(2), kind: 'browser' };
|
||||
guestReady = false; rememberKind(); return user;
|
||||
}
|
||||
function ensureGuest() {
|
||||
// The visitor entry is entirely local. Create temporary server access only
|
||||
// after explicit consent in the welcome dialog, not on entry/button tap.
|
||||
if (!user || user.kind === 'wechat' || guestReady) return Promise.resolve({ user: user });
|
||||
return browser(true);
|
||||
}
|
||||
function init(options) {
|
||||
clearGuestCache();
|
||||
return request('api/auth/session').then(function (v) {
|
||||
user = v.user;
|
||||
guestReady = !!(user && user.kind === 'browser');
|
||||
rememberKind();
|
||||
// A missing WeChat session is not permission to switch to a browser account.
|
||||
// OAuth returns must also stay explicit when its cookie was not retained.
|
||||
if (!user && !(options && options.restoreBrowser === false) && read(kindKey) !== 'wechat' && read()) return browser(true).catch(function (e) {
|
||||
// Do not replace an existing credential on a transient server/network error.
|
||||
if (/身份已失效/.test(e.message)) { try { localStorage.removeItem(key); } catch (_) {} }
|
||||
throw e;
|
||||
});
|
||||
// Never restore a guest credential or silently fall back from WeChat.
|
||||
return v;
|
||||
});
|
||||
}
|
||||
return { request: request, init: init, browser: browser, user: function () { return user; } };
|
||||
return { request: request, init: init, browser: browser, guest: guest, ensureGuest: ensureGuest, user: function () { return user; } };
|
||||
})();
|
||||
|
||||
+33
-24
@@ -76,15 +76,13 @@
|
||||
function renderAccountStatus() {
|
||||
var user = AccountClient.user();
|
||||
document.querySelectorAll('[data-auth-status]').forEach(function (label) {
|
||||
label.textContent = user ? (user.kind === 'wechat' ? '微信已登录' : '游客已登录') : '尚未登录';
|
||||
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游客报告仅可在当前浏览器查看,不能跨手机同步。';
|
||||
$('#login-success-message').textContent = '当前使用:微信登录\n同一微信账号可跨手机查看报告。';
|
||||
openOverlay('p-login-success');
|
||||
document.querySelectorAll('.screen').forEach(function (screen) { screen.inert = true; });
|
||||
$('[data-action="login-confirm"]').focus();
|
||||
@@ -331,7 +329,11 @@
|
||||
}
|
||||
function restoreProfile() {
|
||||
var user = AccountClient.user();
|
||||
profileOwner = ProfileStore.key(user);
|
||||
var owner = user && user.kind === 'browser' ? 'guest:' + user.id : ProfileStore.key(user);
|
||||
// Keep the current form while a guest retries a capture in this page only.
|
||||
// Reload/new guest session discards it; saveProfile never writes this owner.
|
||||
if (user && user.kind === 'browser' && profileOwner === owner) return;
|
||||
profileOwner = owner;
|
||||
var saved = ProfileStore.clean(profileStore.read(user), window.REGION_DATA, SKINS.length);
|
||||
var y = Number(saved.birth.slice(0, 4)), m = Number(saved.birth.slice(5));
|
||||
if (saved.birth && YEARS.indexOf(y + '年') < 0) saved.birth = '';
|
||||
@@ -1073,6 +1075,7 @@
|
||||
}
|
||||
renderReport(vm, photos[0], photos[1], payload); // img1=正脸实拍,payload 供检测可视化取 face_maps/marks
|
||||
if (IS_MOCK) toast('演示报告,不保存至我的报告');
|
||||
else if (AccountClient.user() && AccountClient.user().kind !== 'wechat') toast('游客报告仅供本次查看,不保存历史记录');
|
||||
else if (!payload.report_saved) toast('报告生成成功,但保存失败,请勿关闭页面并联系管理员');
|
||||
photos = []; // 报告已渲染(图片 src 已挂):释放三连拍 dataURL 内存
|
||||
showScreen('s-report', 'replace'); // 报告顶替拍照页:返回键回档案页,不再回到相机
|
||||
@@ -1561,9 +1564,10 @@
|
||||
});
|
||||
|
||||
/* ─── 动作分发 ─── */
|
||||
var privacyBusy = false;
|
||||
var actions = {
|
||||
'open-policy': function () { openOverlay('p-policy'); },
|
||||
'login-browser': function () { loginBrowser(); },
|
||||
'guest-experience': function () { enterGuest(); },
|
||||
'login-wechat': function () { loginWechat(); },
|
||||
'login-confirm': closeLoginSuccess,
|
||||
'open-privacy': function () { if (!AccountClient.user()) { showScreen('s-login', 'replace'); return; } openOverlay('p-privacy'); },
|
||||
@@ -1571,8 +1575,13 @@
|
||||
'close-policy': function () { closeOverlay('p-policy'); },
|
||||
'agree-continue': function () {
|
||||
if (!agreed) { toast('请先阅读并同意隐私协议'); return; }
|
||||
closeOverlay('p-privacy');
|
||||
showScreen('s-profile', 'push');
|
||||
if (privacyBusy) return;
|
||||
privacyBusy = true;
|
||||
var button = $('#p-privacy [data-action="agree-continue"]'); button.disabled = true;
|
||||
AccountClient.ensureGuest().then(function () {
|
||||
if (!$('#p-privacy').classList.contains('show')) return;
|
||||
closeOverlay('p-privacy'); showScreen('s-profile', 'push');
|
||||
}).catch(function (e) { toast(e.message); }).finally(function () { privacyBusy = false; button.disabled = false; });
|
||||
},
|
||||
'submit-profile': function () {
|
||||
$('#f-age').value = ProfileStore.age($('#f-birth').value) || '';
|
||||
@@ -1609,10 +1618,10 @@
|
||||
},
|
||||
'history': function () { openHistory(); },
|
||||
'history-retry': function () { renderHistoryList(); },
|
||||
'switch-login': function () {
|
||||
'open-wechat-login': function () {
|
||||
historySeq++;
|
||||
$('#login-consent').checked = false;
|
||||
$('#login-message').textContent = '微信与游客为独立账号,报告不会自动合并。请选择登录方式;返回可保留当前登录。';
|
||||
$('#login-message').textContent = '微信登录后,后续检测报告可保存和跨手机查看。';
|
||||
showScreen('s-login', 'push');
|
||||
}
|
||||
};
|
||||
@@ -1628,13 +1637,17 @@
|
||||
var historySeq = 0;
|
||||
function renderHistoryList() {
|
||||
var seq = ++historySeq, box = $('#history-list');
|
||||
if (!AccountClient.user() || AccountClient.user().kind !== 'wechat') {
|
||||
$('#history-count').textContent = '游客模式不保存检测记录';
|
||||
$('#identity-note').textContent = '游客不保存个人信息和检测记录,报告仅供本次查看。微信登录后可保留历史报告。';
|
||||
box.innerHTML = '<p class="history-empty">暂无历史报告<br>请使用微信登录,后续检测报告会自动保存。<br><button class="history-retry" data-action="open-wechat-login">微信登录,保存报告</button></p>';
|
||||
return;
|
||||
}
|
||||
box.textContent = '正在读取报告…';
|
||||
AccountClient.request('api/reports').then(function (data) {
|
||||
if (seq !== historySeq) return;
|
||||
$('#history-count').textContent = '您最近已进行过' + data.total + '次面部问题检测';
|
||||
$('#identity-note').textContent = AccountClient.user().kind === 'wechat'
|
||||
? '保留最近 5 份报告,同一微信账号可跨手机查看。'
|
||||
: '保留最近 5 份报告。游客报告仅可在当前浏览器查看,不能跨手机同步;清除浏览器数据后无法找回。';
|
||||
$('#identity-note').textContent = '保留最近 5 份报告,同一微信账号可跨手机查看。';
|
||||
if (!data.items.length) { box.innerHTML = '<p class="history-empty">暂无报告<br>完成一次检测后,报告会自动保存在这里。</p>'; return; }
|
||||
box.innerHTML = data.items.map(function (item) {
|
||||
var date = new Date(item.created_at).toLocaleDateString('sv-SE');
|
||||
@@ -1669,19 +1682,15 @@
|
||||
if (!$('#login-consent').checked) { toast('请先阅读并同意用户协议和隐私政策'); return false; }
|
||||
return true;
|
||||
}
|
||||
function loginBrowser() {
|
||||
if (!loginAllowed()) return;
|
||||
loginBusy = true;
|
||||
$('#login-message').textContent = '正在登录…';
|
||||
AccountClient.browser(true).then(function () {
|
||||
loginSucceeded();
|
||||
}).catch(function (e) { $('#login-message').textContent = e.message; })
|
||||
.finally(function () { loginBusy = false; });
|
||||
function enterGuest() {
|
||||
if (loginBusy) return;
|
||||
AccountClient.guest();
|
||||
showScreen('s-splash', 'replace');
|
||||
}
|
||||
function loginWechat() {
|
||||
if (!loginAllowed()) return;
|
||||
if (!/MicroMessenger/i.test(navigator.userAgent)) {
|
||||
$('#login-message').textContent = '请在微信内打开本页面,或使用游客登录。';
|
||||
$('#login-message').textContent = '请在微信内打开本页面,或选择免登录体验。';
|
||||
return;
|
||||
}
|
||||
loginBusy = true;
|
||||
@@ -1742,14 +1751,14 @@
|
||||
$('#login-message').textContent = code === 'state'
|
||||
? '登录链接已过期或会话丢失,请在同一微信窗口重新点击微信登录。'
|
||||
: code === 'network' ? '暂时无法连接微信服务,请稍后重新登录。'
|
||||
: '微信授权未完成,请重试;如仍失败,请将错误页面截图发给管理员。也可选择游客登录。';
|
||||
: '微信授权未完成,请重试;如仍失败,请将错误页面截图发给管理员。也可选择免登录体验。';
|
||||
} else if (loginSuccess && (!AccountClient.user() || AccountClient.user().kind !== 'wechat')) {
|
||||
$('#login-message').textContent = '微信返回后登录会话未能保存,请在同一微信窗口重新登录。';
|
||||
} 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; })
|
||||
.finally(function () {
|
||||
loginBusy = false;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
})(typeof window !== 'undefined' ? window : this, function() {
|
||||
'use strict';
|
||||
function key(user) {
|
||||
return user && ['browser', 'wechat'].indexOf(user.kind) >= 0 && typeof user.id === 'string' && user.id.length <= 128 && user.id
|
||||
return user && user.kind === 'wechat' && typeof user.id === 'string' && user.id.length <= 128 && user.id
|
||||
? 'juzi.profile.v1.' + user.kind + '.' + encodeURIComponent(user.id) : null;
|
||||
}
|
||||
function age(birth, now) {
|
||||
|
||||
+26
-15
@@ -27,6 +27,7 @@ module.exports = function createAccounts(root, env = process.env) {
|
||||
const prefix = base ? base.pathname.replace(/\/?$/, '/') : '/';
|
||||
if (base) { base.pathname = prefix; base.search = ''; base.hash = ''; }
|
||||
const secure = !!(base && base.protocol === 'https:');
|
||||
const guestSessions = new Map(); // No guest identities/sessions are written to disk.
|
||||
const appId = env.WECHAT_APP_ID || '';
|
||||
const secret = env.WECHAT_APP_SECRET || '';
|
||||
const available = !!(appId && secret && secure && !/^(localhost|127\.|192\.168\.|10\.|172\.(1[6-9]|2\d|3[01])\.)/.test(base.hostname));
|
||||
@@ -34,14 +35,16 @@ module.exports = function createAccounts(root, env = process.env) {
|
||||
return Object.fromEntries((req.headers.cookie || '').split(';').map(s => s.trim().split('=')));
|
||||
}
|
||||
function cookie(res, name, value, age) {
|
||||
const line = `${name}=${value}; Path=${prefix}; HttpOnly; SameSite=Lax; Max-Age=${age}${secure ? '; Secure' : ''}`;
|
||||
const line = `${name}=${value}; Path=${prefix}; HttpOnly; SameSite=Lax${age == null ? '' : '; Max-Age=' + age}${secure ? '; Secure' : ''}`;
|
||||
const old = res.getHeader('Set-Cookie') || [];
|
||||
res.setHeader('Set-Cookie', [].concat(old, line));
|
||||
}
|
||||
function session(req) {
|
||||
const token = cookies(req).juzi_session;
|
||||
const guest = token && guestSessions.get(hash(token));
|
||||
if (guest && guest.expires > Date.now()) return guest.user;
|
||||
const row = token && db.sessions[hash(token)];
|
||||
return row && row.expires > Date.now() ? db.users.find(u => u.id === row.user) : null;
|
||||
return row && row.expires > Date.now() ? db.users.find(u => u.id === row.user && u.kind === 'wechat') : null;
|
||||
}
|
||||
function login(res, user) {
|
||||
for (const key of Object.keys(db.sessions)) if (db.sessions[key].expires < Date.now()) delete db.sessions[key];
|
||||
@@ -94,19 +97,22 @@ module.exports = function createAccounts(root, env = process.env) {
|
||||
if (!rate(req)) { json(res, 429, { ok: false, message: '操作频繁,请稍后再试' }); return true; }
|
||||
const data = await body(req);
|
||||
if (data.consent !== true) { json(res, 400, { ok: false, message: '请先阅读并同意协议' }); return true; }
|
||||
let token = data.token, user;
|
||||
if (token) {
|
||||
if (typeof token !== 'string' || !/^[a-f0-9]{64}$/.test(token)) token = null;
|
||||
user = token && db.users.find(u => u.kind === 'browser' && u.credential === hash(token));
|
||||
if (!user) { json(res, 401, { ok: false, message: '游客身份已失效,请重新登录' }); return true; }
|
||||
} else {
|
||||
token = random(); user = { id: crypto.randomUUID(), kind: 'browser', credential: hash(token), consent: new Date().toISOString(), reports: [] };
|
||||
db.users.push(user);
|
||||
}
|
||||
login(res, user); json(res, 200, { ok: true, user: publicUser(user), token });
|
||||
// Old persistent guest tokens cannot restore identity or history.
|
||||
if (data.token) { json(res, 401, { ok: false, message: '游客身份已失效,请重新登录' }); return true; }
|
||||
for (const [key, value] of guestSessions) if (value.expires < Date.now()) guestSessions.delete(key);
|
||||
const previous = cookies(req).juzi_session;
|
||||
if (previous) guestSessions.delete(hash(previous));
|
||||
const token = random(), user = { id: crypto.randomUUID(), kind: 'browser' };
|
||||
guestSessions.set(hash(token), { user, expires: Date.now() + 8 * 3600000 });
|
||||
cookie(res, 'juzi_session', token, null);
|
||||
json(res, 200, { ok: true, user: publicUser(user) });
|
||||
} else if (pathname === '/api/auth/logout' && req.method === 'POST') {
|
||||
const token = cookies(req).juzi_session; if (token) delete db.sessions[hash(token)];
|
||||
cookie(res, 'juzi_session', '', 0); save(); json(res, 200, { ok: true });
|
||||
const token = cookies(req).juzi_session;
|
||||
if (token) {
|
||||
guestSessions.delete(hash(token));
|
||||
if (db.sessions[hash(token)]) { delete db.sessions[hash(token)]; save(); }
|
||||
}
|
||||
cookie(res, 'juzi_session', '', 0); json(res, 200, { ok: true });
|
||||
} else if (pathname === '/api/auth/wechat' && req.method === 'POST') {
|
||||
const data = await body(req);
|
||||
if (data.consent !== true) { json(res, 400, { ok: false, message: '请先阅读并同意协议' }); return true; }
|
||||
@@ -147,6 +153,11 @@ module.exports = function createAccounts(root, env = process.env) {
|
||||
const user = session(req);
|
||||
if (!user) { json(res, 401, { ok: false, message: '请先登录' }); return true; }
|
||||
const id = pathname.slice('/api/reports/'.length);
|
||||
if (user.kind !== 'wechat') {
|
||||
json(res, pathname === '/api/reports' ? 200 : 404,
|
||||
pathname === '/api/reports' ? { ok: true, total: 0, items: [] } : { ok: false, message: '游客模式不保存历史报告' });
|
||||
return true;
|
||||
}
|
||||
if (pathname === '/api/reports') json(res, 200, { ok: true, total: user.reports.length, items: user.reports });
|
||||
else if (/^R[\w-]+$/.test(id) && user.reports.some(r => r.id === id)) {
|
||||
const file = path.join(snapshots, user.id, id + '.json');
|
||||
@@ -159,7 +170,7 @@ module.exports = function createAccounts(root, env = process.env) {
|
||||
}
|
||||
function storeReport(ownerId, snapshot) {
|
||||
const user = db.users.find(u => u.id === ownerId);
|
||||
if (!user || !/^R[\w-]+$/.test(snapshot.id)) throw new Error('Invalid report owner');
|
||||
if (!user || user.kind !== 'wechat' || !/^R[\w-]+$/.test(snapshot.id)) throw new Error('Invalid report owner');
|
||||
const dir = path.join(snapshots, user.id); fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||
atomic(path.join(dir, snapshot.id + '.json'), snapshot);
|
||||
user.reports = [{ id: snapshot.id, created_at: snapshot.created_at, total: snapshot.vm.total, skinAge: snapshot.vm.skinAgeDisplay }]
|
||||
|
||||
+21
-14
@@ -267,7 +267,7 @@ function scoreProfile(rec) {
|
||||
}
|
||||
|
||||
/* 检测成功:用 SkinScore.buildViewModel 计算并补全记录字段(复用同一份公式) */
|
||||
function fillSuccess(rec, fpJson) {
|
||||
function fillSuccess(rec, fpJson, persist = true) {
|
||||
// Observation only: do not block, re-shoot, retry or alter scoring based on it.
|
||||
rec.capture_quality = CaptureDiagnostics.assess(fpJson);
|
||||
const vm = SkinScore.buildViewModel(fpJson.result, {
|
||||
@@ -299,7 +299,7 @@ function fillSuccess(rec, fpJson) {
|
||||
rec.essence_line = vm.essence ? vm.essence.text : null;
|
||||
rec.trigger = (vm.essence && vm.essence.triggers) ? vm.essence.triggers.join(',') : null;
|
||||
rec.notes = vm.notes || [];
|
||||
rec.raw_file = dumpRaw(fpJson);
|
||||
rec.raw_file = persist ? dumpRaw(fpJson) : null;
|
||||
}
|
||||
|
||||
/* 检测失败:补失败字段 */
|
||||
@@ -311,7 +311,7 @@ function fillFailure(rec, code) {
|
||||
}
|
||||
|
||||
/* 执行一次检测:计时调用 Face++,按结果补全记录(成功/失败都落库)。返回 Face++ 原始返回 */
|
||||
async function runDetect(rec, front, left, right) {
|
||||
async function runDetect(rec, front, left, right, persist = true) {
|
||||
rec.detect_status = '检测中';
|
||||
rec.updated_at = nowIso();
|
||||
const t0 = Date.now();
|
||||
@@ -325,7 +325,7 @@ async function runDetect(rec, front, left, right) {
|
||||
fillFailure(rec, 'INTERNAL_ERROR');
|
||||
} else {
|
||||
try {
|
||||
fillSuccess(rec, fpJson);
|
||||
fillSuccess(rec, fpJson, persist);
|
||||
} catch (e) { // 评分异常按检测失败落库,但不改写 C 端成功响应(原始结果仍透传)
|
||||
console.log(`[analyze] 评分异常: ${e.message}`);
|
||||
fillFailure(rec, 'INTERNAL_ERROR');
|
||||
@@ -372,10 +372,13 @@ async function callFacepp(front, left, right) {
|
||||
}
|
||||
|
||||
async function handleAnalyze(req, res) {
|
||||
res.setHeader('Cache-Control', 'no-store');
|
||||
let rec = null; // 记录:正脸照片校验通过后创建(此前失败无从落库)
|
||||
let persist = false;
|
||||
try {
|
||||
const owner = accounts.session(req);
|
||||
if (!owner) return sendJson(res, 401, { ok: false, message: '请先登录后检测' });
|
||||
persist = owner.kind === 'wechat';
|
||||
if (!accounts.sameOrigin(req)) return sendJson(res, 403, { ok: false, message: '请求来源不匹配' });
|
||||
const body = JSON.parse((await readBody(req, 20 * 1024 * 1024)).toString('utf8'));
|
||||
// 三个 image 字段先做类型检查:非字符串视为缺失(左/右图缺失时仅发正脸,接口自动降级为单视图)
|
||||
@@ -394,7 +397,7 @@ async function handleAnalyze(req, res) {
|
||||
/* ── 记录落库:生成 record_id → 写照片 → 落一条 detect_status=检测中 ── */
|
||||
const profile = normalizeProfile(body.profile);
|
||||
const recordId = newRecordId();
|
||||
const photos = writePhotos(recordId, { front, left, right });
|
||||
const photos = persist ? writePhotos(recordId, { front, left, right }) : {};
|
||||
rec = {
|
||||
record_id: recordId,
|
||||
owner_id: owner.id,
|
||||
@@ -420,11 +423,13 @@ async function handleAnalyze(req, res) {
|
||||
retry_of: null, retry_count: 0
|
||||
};
|
||||
rec.capture_diagnostics = CaptureDiagnostics.normalize(body.capture_diagnostics);
|
||||
appendRecord(rec);
|
||||
console.log(`[record] 新检测记录 ${recordId}(${rec.shoot_status},照片${Object.keys(photos).length}/3)`);
|
||||
if (persist) {
|
||||
appendRecord(rec);
|
||||
console.log(`[record] 新检测记录 ${recordId}(${rec.shoot_status},照片${Object.keys(photos).length}/3)`);
|
||||
}
|
||||
|
||||
const fpJson = await runDetect(rec, front, left, right);
|
||||
updateRecord(rec);
|
||||
const fpJson = await runDetect(rec, front, left, right, persist);
|
||||
if (persist) updateRecord(rec);
|
||||
|
||||
if (fpJson.error_message) {
|
||||
const code = fpJson.error_message.split(':')[0];
|
||||
@@ -458,10 +463,12 @@ async function handleAnalyze(req, res) {
|
||||
try {
|
||||
snapshotVm = SkinScore.buildViewModel(result, payload.side, scoreProfile(rec));
|
||||
presentation = rec.presentation || ReportUI.build(snapshotVm, result);
|
||||
accounts.storeReport(owner.id, { id: recordId, created_at: rec.created_at,
|
||||
version: '2026-10-09', profile, vm: snapshotVm, presentation, payload,
|
||||
photos: [front, left, right].map(b64 => b64 ? toDataUrl(b64) : null) });
|
||||
reportSaved = true;
|
||||
if (persist) {
|
||||
accounts.storeReport(owner.id, { id: recordId, created_at: rec.created_at,
|
||||
version: '2026-10-09', profile, vm: snapshotVm, presentation, payload,
|
||||
photos: [front, left, right].map(b64 => b64 ? toDataUrl(b64) : null) });
|
||||
reportSaved = true;
|
||||
}
|
||||
} catch (e) { console.error('[report] 完整报告保存失败:', e.message); }
|
||||
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify({
|
||||
@@ -475,7 +482,7 @@ async function handleAnalyze(req, res) {
|
||||
}
|
||||
}));
|
||||
} catch (e) {
|
||||
if (rec) { // 记录已创建但流程异常:按检测失败补全,便于后台看到这条调用
|
||||
if (rec && persist) { // 仅微信检测失败记录落库;游客不保留异常记录。
|
||||
fillFailure(rec, e.message === 'TOO_LARGE' ? 'IMAGE_FILE_TOO_LARGE' : 'NETWORK_ERROR');
|
||||
rec.updated_at = nowIso();
|
||||
updateRecord(rec);
|
||||
|
||||
+13
-6
@@ -26,8 +26,16 @@ const ReportUI = require('../js/report-ui');
|
||||
assert.equal((await request('/api/reports')).status, 401);
|
||||
assert.equal((await request('/api/auth/browser', {})).status, 400);
|
||||
assert.equal((await request('/api/auth/browser', { consent: true }, '', { Origin: 'https://unrelated.example' })).status, 403);
|
||||
const a = await request('/api/auth/browser', { consent: true });
|
||||
const b = await request('/api/auth/browser', { consent: true });
|
||||
const guest = await request('/api/auth/browser', { consent: true });
|
||||
assert.equal(guest.data.token, undefined, 'guest credentials are never persisted by clients');
|
||||
assert(!guest.headers.get('set-cookie').includes('Max-Age='), 'guest cookie is session-only');
|
||||
assert.equal(fs.existsSync(path.join(root, 'identities.json')), false, 'guest login does not write an identity');
|
||||
assert.throws(() => accounts.storeReport(guest.data.user.id, { id: 'Rguest' }));
|
||||
assert.equal((await request('/api/reports', null, guest.cookie)).data.total, 0);
|
||||
accounts = create(root, {});
|
||||
assert.equal((await request('/api/auth/session', null, guest.cookie)).data.user, null, 'process restart forgets guests');
|
||||
const [a, b] = require('./wechat-fixture.cjs')(root, 2).map(entry => ({ ...entry, data: { user: entry.user }, headers: guest.headers }));
|
||||
accounts = create(root, {});
|
||||
assert.notEqual(a.data.user.id, b.data.user.id);
|
||||
assert(a.headers.get('set-cookie').includes('HttpOnly'));
|
||||
const vm = SkinScore.buildViewModel(SkinScore.MOCK_RESULT, SkinScore.MOCK_SIDE, { realAge: 30, skinType: '干性肌肤' });
|
||||
@@ -48,11 +56,10 @@ const ReportUI = require('../js/report-ui');
|
||||
assert.equal((await request('/api/reports/Rtest0', null, a.cookie)).status, 404);
|
||||
accounts = create(root, {}); // Process restart reloads sessions and reports.
|
||||
assert.equal((await request('/api/reports', null, a.cookie)).data.total, 5);
|
||||
const restored = await request('/api/auth/browser', { consent: true, token: a.data.token });
|
||||
assert.equal(restored.data.user.id, a.data.user.id);
|
||||
assert.equal((await request('/api/auth/session', null, a.cookie)).data.user.id, a.data.user.id);
|
||||
assert.equal((await request('/api/auth/browser', { consent: true, token: 'bad' })).status, 401);
|
||||
await request('/api/auth/logout', {}, restored.cookie);
|
||||
assert.equal((await request('/api/reports', null, restored.cookie)).status, 401);
|
||||
await request('/api/auth/logout', {}, a.cookie);
|
||||
assert.equal((await request('/api/reports', null, a.cookie)).status, 401);
|
||||
assert.equal((await request('/api/auth/config')).data.wechat, false);
|
||||
accounts = create(root, { WECHAT_APP_ID: 'test-app', WECHAT_APP_SECRET: 'test-secret', PUBLIC_BASE_URL: 'https://skin.example.com/state' });
|
||||
const start = await request('/api/auth/wechat', { consent: true });
|
||||
|
||||
@@ -40,9 +40,8 @@ try {
|
||||
await page.evaluate(() => { window.faceapi = undefined; });
|
||||
await page.waitForFunction(() => !document.querySelector('#login-message').textContent.includes('检查'));
|
||||
await page.check('#login-consent');
|
||||
await page.click('[data-action="login-browser"]');
|
||||
await page.click('[data-action="guest-experience"]');
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
await page.click('[data-action="login-confirm"]');
|
||||
await page.click('[data-action="open-privacy"]');
|
||||
await page.click('#privacy-check');
|
||||
await page.click('[data-action="agree-continue"]');
|
||||
|
||||
@@ -13,6 +13,7 @@ const ReportUI = require('../js/report-ui');
|
||||
const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href);
|
||||
const h5 = fileURLToPath(new URL('../', import.meta.url));
|
||||
const dataRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'juzi-fourth-test-'));
|
||||
const wechat = require('./wechat-fixture.cjs')(path.join(dataRoot, 'accounts'))[0];
|
||||
const artifacts = process.env.H5_ARTIFACT_DIR;
|
||||
const child = spawn(process.execPath, ['-e', `
|
||||
const fs=require('fs'), vm=require('vm'), w={};
|
||||
@@ -45,13 +46,14 @@ try {
|
||||
await page.goto(base); await page.waitForLoadState('networkidle');
|
||||
assert.equal(await page.title(), "1'me肌肤状态检测");
|
||||
assert.equal(await page.locator('meta[property="og:title"]').getAttribute('content'), "1'me肌肤状态检测");
|
||||
assert.equal((await page.locator('[data-action="login-browser"]').textContent()).trim(), '游客登录');
|
||||
assert.equal((await page.locator('[data-action="guest-experience"]').textContent()).trim(), '免登录体验');
|
||||
assert(!requests.some(p => /scan-(base|face)|\.woff2$|faceapi/.test(p)), 'no heavy/scanning entry requests');
|
||||
await page.check('#login-consent'); await page.click('[data-action="login-browser"]');
|
||||
await page.waitForSelector('#p-login-success.show');
|
||||
assert((await page.locator('#login-success-message').textContent()).includes('当前使用:游客登录'));
|
||||
await page.click('[data-action="login-confirm"]');
|
||||
assert.equal(await page.locator('#s-splash [data-auth-status]').textContent(), '游客已登录');
|
||||
await page.click('[data-action="guest-experience"]');
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
assert.equal(await page.locator('#p-login-success.show').count(), 0);
|
||||
assert.equal(await page.locator('#s-splash [data-auth-status]').textContent(), '游客模式');
|
||||
await page.context().addCookies([{ name: 'juzi_session', value: wechat.token, url: base }]);
|
||||
await page.evaluate(() => AccountClient.init()); // Persistence scenarios require WeChat.
|
||||
const api = page.context().request;
|
||||
await page.addScriptTag({ url: base + 'assets/mock/report-fixture.js' });
|
||||
const fixture = await page.evaluate(() => window.MOCK_FIXTURE);
|
||||
|
||||
@@ -117,9 +117,8 @@ try {
|
||||
// 3) 完整 mock 流程:启动 → 档案 → 三连拍 → 报告,并记录语音起播
|
||||
await page.goto(BASE + '/?mock=1', { waitUntil: 'networkidle' });
|
||||
await page.locator('#login-consent').check();
|
||||
await page.locator('[data-action="login-browser"]').click();
|
||||
await page.locator('[data-action="guest-experience"]').click();
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
await page.click('[data-action="login-confirm"]');
|
||||
await page.getByRole('button', { name: '开始检测' }).click();
|
||||
await page.locator('#privacy-check').click();
|
||||
await page.getByRole('button', { name: '继续' }).click();
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
// Isolated backend and fixture-backed analysis. No production data or paid API.
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import vm from 'node:vm';
|
||||
import { spawn, execSync } from 'node:child_process';
|
||||
import { once } from 'node:events';
|
||||
import { createRequire } from 'node:module';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
const require = createRequire(import.meta.url);
|
||||
const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href);
|
||||
const h5 = fileURLToPath(new URL('../', import.meta.url));
|
||||
const dataRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'juzi-guest-test-'));
|
||||
const wechat = require('./wechat-fixture.cjs')(path.join(dataRoot, 'accounts'))[0];
|
||||
const fx = {}; vm.runInNewContext(fs.readFileSync(path.join(h5, 'assets/mock/report-fixture.js'), 'utf8'), { window: fx });
|
||||
const fixture = fx.MOCK_FIXTURE;
|
||||
const child = spawn(process.execPath, ['-e', `
|
||||
const fs=require('fs'),vm=require('vm'),w={};
|
||||
vm.runInNewContext(fs.readFileSync('assets/mock/report-fixture.js','utf8'),{window:w});
|
||||
const f=w.MOCK_FIXTURE; let calls=0;
|
||||
global.fetch=async()=>({ok:true,status:200,json:async()=>++calls===2?{error_message:'INVALID_IMAGE_FACE'}:
|
||||
{request_id:'fixture',result:f.result,face_rectangle:f.face_rectangle,left_side_result:f.side.left,right_side_result:f.side.right}});
|
||||
require('./server.js');
|
||||
`], { cwd: h5, env: { ...process.env, PORT: '18115', HOST: '127.0.0.1', JUZI_DATA_ROOT: dataRoot,
|
||||
PUBLIC_BASE_URL: '', WECHAT_APP_ID: '', WECHAT_APP_SECRET: '', SKIN_API_KEY: 'fixture', SKIN_API_SECRET: 'fixture' }, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
let browser;
|
||||
function diskSnapshot(dir = dataRoot, prefix = '') {
|
||||
const files = {};
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const name = prefix + entry.name;
|
||||
if (entry.isDirectory()) Object.assign(files, diskSnapshot(path.join(dir, entry.name), name + '/'));
|
||||
else files[name] = fs.readFileSync(path.join(dir, entry.name)).toString('base64');
|
||||
}
|
||||
return files;
|
||||
}
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(Error('test startup timeout')), 10000);
|
||||
child.stdout.on('data', b => { if (b.toString().includes('已启动')) { clearTimeout(timer); resolve(); } });
|
||||
child.once('exit', code => { clearTimeout(timer); reject(Error('server exited ' + code)); });
|
||||
});
|
||||
const base = 'http://127.0.0.1:18115/';
|
||||
browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage({ viewport: { width: 375, height: 726 }, isMobile: true, hasTouch: true });
|
||||
const errors = []; page.on('pageerror', e => errors.push(e.message));
|
||||
const guestRequests = [];
|
||||
page.on('request', r => { if (r.url().endsWith('/api/auth/browser')) guestRequests.push(r.url()); });
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem('juzi.identity.v1', 'old-guest-token');
|
||||
localStorage.setItem('juzi.reports.v1', '[{"id":"old-report"}]');
|
||||
localStorage.setItem('juzi.profile.v1.browser.old', JSON.stringify({ gender: '男' }));
|
||||
});
|
||||
const baseline = diskSnapshot();
|
||||
await page.goto(base);
|
||||
await page.waitForFunction(() => !document.querySelector('#login-message').textContent.includes('检查'));
|
||||
assert.equal(await page.locator('#s-login.active').count(), 1, 'old guest token cannot silently restore login');
|
||||
const artifacts = process.env.H5_ARTIFACT_DIR;
|
||||
async function screenshot(name) {
|
||||
await page.evaluate(async () => { await document.fonts.ready; window.scrollTo(0, 0); });
|
||||
await page.waitForTimeout(400);
|
||||
await page.screenshot({ path: path.join(artifacts, name + '.png'), animations: 'disabled' });
|
||||
}
|
||||
if (artifacts) { fs.mkdirSync(artifacts, { recursive: true }); await screenshot('guest-entry'); }
|
||||
assert.equal(await page.locator('#login-consent').isChecked(), false);
|
||||
await page.click('[data-action="guest-experience"]');
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
assert.equal(await page.locator('#p-login-success.show').count(), 0, 'guest entry never shows login success');
|
||||
assert.equal(await page.locator('[data-action="switch-login"]').count(), 0, 'switch-login controls are removed');
|
||||
assert.equal(await page.locator('#s-splash [data-auth-status]').textContent(), '游客模式');
|
||||
assert.equal(guestRequests.length, 0, 'guest entry performs no authentication request');
|
||||
assert.equal(await page.evaluate(() => localStorage.getItem('juzi.identity.v1')), null);
|
||||
assert.equal(await page.evaluate(() => localStorage.getItem('juzi.reports.v1')), null);
|
||||
assert.equal(await page.evaluate(() => Object.keys(localStorage).some(k => k.startsWith('juzi.profile.v1.browser.'))), false);
|
||||
assert.equal((await page.context().cookies()).some(c => c.name === 'juzi_session'), false);
|
||||
if (artifacts) {
|
||||
fs.mkdirSync(artifacts, { recursive: true });
|
||||
await screenshot('guest-home');
|
||||
}
|
||||
await page.click('[data-action="open-privacy"]'); await page.waitForSelector('#p-privacy.show');
|
||||
assert.equal(await page.locator('#privacy-policy-link').textContent(), '隐私协议');
|
||||
const linkStyle = await page.locator('#privacy-policy-link').evaluate(el => ({ weight: getComputedStyle(el).fontWeight, decoration: getComputedStyle(el).textDecorationLine }));
|
||||
assert(Number(linkStyle.weight) >= 600 && linkStyle.decoration.includes('underline'));
|
||||
assert.equal(await page.locator('.privacy-text').evaluate(el => getComputedStyle(el).textDecorationLine), 'none');
|
||||
if (artifacts) await screenshot('welcome-privacy');
|
||||
await page.click('#privacy-policy-link'); await page.waitForSelector('#p-policy.show');
|
||||
assert.equal(await page.locator('#privacy-check').evaluate(el => el.classList.contains('checked')), false, 'opening agreement does not tick consent');
|
||||
await page.click('[data-action="close-policy"]');
|
||||
await page.click('[data-action="agree-continue"]');
|
||||
assert.equal(guestRequests.length, 0, 'no temporary server session before consent');
|
||||
await page.click('#privacy-check'); await page.click('[data-action="agree-continue"]');
|
||||
await page.waitForSelector('#s-profile.active');
|
||||
assert.equal(guestRequests.length, 1, 'temporary access is created only after welcome consent');
|
||||
const cookie = (await page.context().cookies()).find(c => c.name === 'juzi_session');
|
||||
assert.equal(cookie.expires, -1, 'temporary cookie is non-persistent');
|
||||
await page.reload(); await page.waitForSelector('#s-splash.active');
|
||||
const api = page.context().request;
|
||||
const data = { front_image: fixture.photos[0], left_image: fixture.photos[0], right_image: fixture.photos[0],
|
||||
profile: { real_age: 30, gender: '女', region: '北京市', daily_skin_type: '干性肌肤' } };
|
||||
const guestResponse = await api.post(base + 'api/skin-analyze', { data });
|
||||
assert.equal(guestResponse.headers()['cache-control'], 'no-store');
|
||||
const guest = await guestResponse.json();
|
||||
assert.equal(guest.ok, true); assert.equal(guest.report_saved, false); assert(guest.vm && guest.presentation);
|
||||
assert.equal((await (await api.get(base + 'api/reports')).json()).total, 0);
|
||||
assert.equal((await api.get(base + 'api/reports/' + guest.record_id)).status(), 404);
|
||||
const failed = await (await api.post(base + 'api/skin-analyze', { data })).json();
|
||||
assert.equal(failed.ok, false);
|
||||
await page.waitForTimeout(150); assert.deepEqual(diskSnapshot(), baseline, 'guest success/failure writes no identity, report, photo, profile, raw result or admin record');
|
||||
await page.click('[data-action="history"]');
|
||||
assert((await page.locator('#identity-note').textContent()).includes('不保存个人信息和检测记录'));
|
||||
assert.equal(await page.locator('[data-report-id]').count(), 0);
|
||||
if (artifacts) await screenshot('guest-history');
|
||||
// Same backend and payload: WeChat must continue to retain complete reports.
|
||||
await page.context().addCookies([{ name: 'juzi_session', value: wechat.token, url: base }]);
|
||||
const saved = await (await api.post(base + 'api/skin-analyze', { data })).json();
|
||||
assert.equal(saved.ok, true); assert.equal(saved.report_saved, true);
|
||||
assert.equal((await (await api.get(base + 'api/reports')).json()).total, 1);
|
||||
assert.equal((await (await api.get(base + 'api/reports/' + saved.record_id)).json()).report.id, saved.record_id);
|
||||
assert(fs.existsSync(path.join(dataRoot, 'records.jsonl')));
|
||||
assert(fs.existsSync(path.join(dataRoot, 'photos', saved.record_id, 'front.jpg')));
|
||||
// Visual sample only; not another analysis call.
|
||||
await page.goto(base + '?mock=1&screen=report');
|
||||
await page.waitForFunction(() => document.querySelector('#care-list').children.length > 1);
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await page.locator('.card-care').scrollIntoViewIfNeeded();
|
||||
const spacing = await page.locator('.card-care').evaluate(el => {
|
||||
const heading = el.querySelector('.card-heading'), body = el.querySelector('#care-list');
|
||||
return { margin: parseFloat(getComputedStyle(heading).marginBottom), gap: body.getBoundingClientRect().top - heading.getBoundingClientRect().bottom };
|
||||
});
|
||||
assert(Math.abs(spacing.margin - 9) < .1 && Math.abs(spacing.gap - 9) < 1, 'care title gap is halved from 18px to 9px');
|
||||
if (artifacts) await page.locator('.card-care').screenshot({ path: path.join(artifacts, 'care-spacing.png') });
|
||||
assert.deepEqual(errors, []);
|
||||
console.log('PASS: four-character bold/underlined privacy link, half care gap; guest success/failure no disk writes or saved history/identity; WeChat retains snapshots/photos/admin records; screenshots; no paid calls');
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
if (child.exitCode == null) { const stopped = once(child, 'exit'); child.kill(); await stopped; }
|
||||
fs.rmSync(dataRoot, { recursive: true, force: true });
|
||||
}
|
||||
@@ -72,13 +72,13 @@ try {
|
||||
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');
|
||||
await page.click('[data-action="guest-experience"]');
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
await page.waitForTimeout(400); // Allow the home entry animation to settle.
|
||||
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.screenshot({ path: path.join(artifacts, 'guest-no-login.png') });
|
||||
}
|
||||
await page.keyboard.press('Escape');
|
||||
assert.equal(await page.locator('#p-login-success.show').count(), 0);
|
||||
@@ -94,10 +94,11 @@ try {
|
||||
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(), '游客已登录');
|
||||
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('[data-action="switch-login"]').count(), 0);
|
||||
await page.click('#s-history [data-action="open-wechat-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');
|
||||
|
||||
+20
-32
@@ -35,22 +35,23 @@ try {
|
||||
await page.goto(base, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(() => !document.querySelector('#login-message').textContent.includes('检查'));
|
||||
await page.check('#login-consent');
|
||||
await page.click('[data-action="login-browser"]');
|
||||
await page.click('[data-action="guest-experience"]');
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
await page.click('[data-action="login-confirm"]');
|
||||
const api = context.request;
|
||||
assert.equal((await api.get(base + 'api/reports')).status(), 200);
|
||||
const cookie = (await context.cookies()).find(c => c.httpOnly);
|
||||
assert(cookie && cookie.secure && cookie.sameSite === 'Lax' && cookie.path === new URL(base).pathname);
|
||||
assert.equal((await api.post(base + 'api/skin-analyze', { data: {} })).status(), 400);
|
||||
assert.equal((await api.post(base + 'api/skin-analyze', { data: {}, headers: { Origin: 'https://invalid.example' } })).status(), 403);
|
||||
console.log('PASS: public HTTPS, secure scoped cookie, missing-image validation, cross-origin denial');
|
||||
assert.equal((await api.get(base + 'api/reports')).status(), 401);
|
||||
|
||||
await page.click('[data-action="open-privacy"]');
|
||||
await page.click('[data-action="agree-continue"]');
|
||||
assert.equal(await page.locator('#s-profile.active').count(), 0);
|
||||
await page.click('#privacy-check');
|
||||
await page.click('[data-action="agree-continue"]');
|
||||
await page.waitForSelector('#s-profile.active');
|
||||
assert.equal((await api.get(base + 'api/reports')).status(), 200);
|
||||
const cookie = (await context.cookies()).find(c => c.httpOnly);
|
||||
assert(cookie && cookie.secure && cookie.sameSite === 'Lax' && cookie.path === new URL(base).pathname);
|
||||
assert.equal((await api.post(base + 'api/skin-analyze', { data: {} })).status(), 400);
|
||||
assert.equal((await api.post(base + 'api/skin-analyze', { data: {}, headers: { Origin: 'https://invalid.example' } })).status(), 403);
|
||||
console.log('PASS: consent-gated temporary HTTPS session, secure scoped cookie, missing-image validation, cross-origin denial');
|
||||
await page.click('[data-action="submit-profile"]');
|
||||
assert.equal(await page.locator('#s-profile.active').count(), 1);
|
||||
for (const selector of ['#pick-birth', '#pick-gender', '#pick-region']) {
|
||||
@@ -79,17 +80,12 @@ try {
|
||||
assert.equal(response.status(), 200);
|
||||
const result = await response.json();
|
||||
assert.equal(result.ok, true, JSON.stringify({ code: result.code, message: result.message }));
|
||||
assert.equal(result.report_saved, true);
|
||||
assert.equal(result.report_saved, false, 'guest reports are deliberately not saved');
|
||||
assert.equal(analyzeCalls, 1);
|
||||
assert((await page.evaluate(() => window.__analyzeTimeouts)).includes(150000));
|
||||
await page.waitForSelector('#s-report.active');
|
||||
await page.waitForFunction(() => [...document.querySelectorAll('.ditem-img')].every(i => !i.hidden && i.naturalWidth > 0));
|
||||
const snap = (await (await api.get(base + 'api/reports/' + result.record_id)).json()).report;
|
||||
assert.equal(snap.photos.filter(Boolean).length, 3);
|
||||
assert.deepEqual(snap.presentation, result.presentation);
|
||||
assert.deepEqual(snap.payload.result.face_maps, result.result.face_maps);
|
||||
const images = await page.locator('.ditem-img').evaluateAll(nodes => nodes.map(i => i.src));
|
||||
const copy = await page.locator('.ditem-para').allTextContents();
|
||||
assert.equal((await api.get(base + 'api/reports/' + result.record_id)).status(), 404);
|
||||
for (const card of await page.locator('.card-detail').all()) {
|
||||
for (const tab of await card.locator('.dtab').all()) {
|
||||
await tab.click();
|
||||
@@ -102,31 +98,23 @@ try {
|
||||
await page.waitForSelector('#p-zoom.show', { state: 'hidden' });
|
||||
assert.equal(await page.locator('#s-report.active').count(), 1);
|
||||
await page.screenshot({ path: path.join(artifacts, 'real-report.png'), fullPage: true });
|
||||
console.log('PASS: three sample views → REAL Face++ → scoring → seven images/tabs → zoom/back → backend snapshot');
|
||||
console.log('PASS: three sample views → REAL Face++ → scoring → seven images/tabs → zoom/back; guest report is not saved');
|
||||
|
||||
// Cookie loss is restored using this browser's stored random identity, not a new account.
|
||||
// Cookie loss cannot restore a guest identity or any guest history.
|
||||
await context.clearCookies();
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('#s-login.active');
|
||||
await page.waitForFunction(() => !document.querySelector('#login-message').textContent.includes('检查'));
|
||||
await page.click('[data-action="guest-experience"]');
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
await page.click('[data-action="history"]');
|
||||
await page.click('[data-report-id="' + result.record_id + '"]');
|
||||
await page.waitForSelector('#s-report.active');
|
||||
await page.waitForFunction(() => [...document.querySelectorAll('.ditem-img')].every(i => !i.hidden && i.naturalWidth > 0));
|
||||
assert.deepEqual(await page.locator('.ditem-img').evaluateAll(nodes => nodes.map(i => i.src)), images);
|
||||
assert.deepEqual(await page.locator('.ditem-para').allTextContents(), copy);
|
||||
await page.goBack();
|
||||
await page.waitForSelector('#s-history.active');
|
||||
// Read failure + explicit retry must recover without losing the report.
|
||||
await page.route('**/api/reports', route => route.fulfill({ status: 503, contentType: 'application/json', body: JSON.stringify({ ok: false, message: '测试临时不可用' }) }));
|
||||
await page.click('[data-action="history-retry"]');
|
||||
await page.waitForFunction(() => document.querySelector('#history-list').textContent.includes('测试临时不可用'));
|
||||
await page.unroute('**/api/reports');
|
||||
await page.click('[data-action="history-retry"]');
|
||||
await page.waitForSelector('[data-report-id="' + result.record_id + '"]');
|
||||
assert.equal(await page.locator('[data-report-id]').count(), 0);
|
||||
assert((await page.locator('#identity-note').textContent()).includes('不保存'));
|
||||
await page.screenshot({ path: path.join(artifacts, 'real-history.png') });
|
||||
await page.goBack();
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
console.log('PASS: cookie restoration, report reload with identical seven images/copy, history error/retry, browser back');
|
||||
console.log('PASS: guest has no credential restoration or saved history; refresh/back navigation works');
|
||||
|
||||
const other = await browser.newContext();
|
||||
assert.equal((await other.request.get(base + 'api/reports/' + result.record_id)).status(), 401);
|
||||
@@ -139,7 +127,7 @@ try {
|
||||
assert.deepEqual(errors, []);
|
||||
fs.writeFileSync(path.join(artifacts, 'summary.json'), JSON.stringify({ record_id: result.record_id, analyzeCalls, errors, tested_at: new Date().toISOString() }, null, 2));
|
||||
console.log('PASS: cross-account isolation, admin/source/data protection, zero JS errors');
|
||||
console.log('Test report (dedicated identity): ' + result.record_id + '; artifacts: ' + artifacts);
|
||||
console.log('Temporary guest verification report (not saved): ' + result.record_id + '; artifacts: ' + artifacts);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ try {
|
||||
const context = await browser.newContext({ viewport: { width: 375, height: 726 }, isMobile: true, hasTouch: true,
|
||||
userAgent: 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 Chrome/130.0.0.0 Mobile Safari/537.36' });
|
||||
const page = await context.newPage(), errors = [], blockedWrites = [];
|
||||
let user = { id: 'profile-fixture-a', kind: 'browser' };
|
||||
let user = { id: 'profile-fixture-a', kind: 'wechat' };
|
||||
page.setDefaultTimeout(20000);
|
||||
page.on('pageerror', e => errors.push(e.message));
|
||||
await page.route('**/*', route => {
|
||||
@@ -88,7 +88,7 @@ try {
|
||||
user = { id: 'profile-fixture-b', kind: 'wechat' };
|
||||
await enter(); assert.deepEqual(await values(), blank, 'another account does not inherit profile/picker/skin values');
|
||||
await pick('#pick-gender'); assert.equal((await values()).gender, '女', 'new account wheel reset too');
|
||||
user = { id: 'profile-fixture-a', kind: 'browser' };
|
||||
user = { id: 'profile-fixture-a', kind: 'wechat' };
|
||||
await page.evaluate(() => AccountClient.init());
|
||||
await page.goBack(); await page.waitForSelector('#s-splash.active');
|
||||
await enter(false); assert.deepEqual(await values(), original, 'switching back without a reload restores the right account');
|
||||
@@ -182,6 +182,10 @@ try {
|
||||
await enter(); assert.equal((await values()).skin, 3, 'skin selection also survives reload');
|
||||
await page.click('[data-action="submit-profile"]');
|
||||
await page.waitForSelector('#p-photoguide.show');
|
||||
user = { id: 'profile-fixture-guest', kind: 'browser' };
|
||||
await enter(); assert.deepEqual(await values(), blank, 'guests never inherit WeChat preferences');
|
||||
await pick('#pick-gender', [1]); assert.equal((await values()).gender, '男');
|
||||
await enter(); assert.deepEqual(await values(), blank, 'guest preferences do not survive reload');
|
||||
assert.deepEqual(errors, []); assert.deepEqual(blockedWrites, [], 'no backend writes or paid requests');
|
||||
console.log('PASS: actual profile defaults/wheels/reload/account isolation; real mobile finger-follow/snap/flick/edges/cancel; fixed profile/no vertical movement/controls fit in seven viewport sizes; real birth-wheel vertical touch still works; arrows/dots/keyboard/submission; no JS errors/backend writes');
|
||||
} finally {
|
||||
|
||||
@@ -2,7 +2,7 @@ const assert = require('node:assert/strict');
|
||||
const Profile = require('../js/profile-store');
|
||||
const regions = [['北京市', ['北京市']], ['浙江省', ['杭州市', '宁波市']]];
|
||||
const now = new Date(2026, 9, 12);
|
||||
const user = { id: 'fixture-a', kind: 'browser' }, other = { id: 'fixture-b', kind: 'browser' };
|
||||
const user = { id: 'fixture-a', kind: 'wechat' }, other = { id: 'fixture-b', kind: 'wechat' };
|
||||
const valid = { birth: '1991-11', gender: '男', region: { province: '浙江省', city: '宁波市' }, skinIndex: 3 };
|
||||
const empty = { birth: '', gender: '', region: null, skinIndex: 0 };
|
||||
assert.equal(Profile.age(valid.birth, now), 34);
|
||||
@@ -30,7 +30,9 @@ assert.equal(store.read(user), null);
|
||||
store.write(user, valid);
|
||||
assert.deepEqual(Profile.create(() => storage).read(user), valid, 'survives a page reload');
|
||||
assert.equal(store.read(other), null, 'different accounts are isolated');
|
||||
assert.equal(store.read({ ...user, kind: 'wechat' }), null, 'login kinds are isolated');
|
||||
assert.equal(store.read({ ...user, kind: 'browser' }), null, 'guests cannot read saved preferences');
|
||||
store.write({ ...user, kind: 'browser' }, valid);
|
||||
assert.equal(Profile.key({ ...user, kind: 'browser' }), null);
|
||||
store.write(other, empty); assert.deepEqual(store.read(user), valid);
|
||||
store.write(null, valid); assert.equal(data.size, 2, 'no unauthenticated writes');
|
||||
for (const raw of ['broken', 'x'.repeat(4097)]) {
|
||||
|
||||
@@ -14,6 +14,7 @@ const ReportUI = require('../js/report-ui.js');
|
||||
const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href);
|
||||
const h5 = fileURLToPath(new URL('../', import.meta.url));
|
||||
const dataRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'juzi-report-test-'));
|
||||
const wechat = require('./wechat-fixture.cjs')(path.join(dataRoot, 'accounts'))[0];
|
||||
const fx = {};
|
||||
vm.runInNewContext(fs.readFileSync(path.join(h5, 'assets/mock/report-fixture.js'), 'utf8'), { window: fx });
|
||||
const fixture = fx.MOCK_FIXTURE;
|
||||
@@ -35,14 +36,11 @@ try {
|
||||
const base = 'http://127.0.0.1:18089/';
|
||||
browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] });
|
||||
const page = await browser.newPage({ viewport: { width: 375, height: 726 } });
|
||||
await page.context().addCookies([{ name: 'juzi_session', value: wechat.token, url: base }]);
|
||||
const errors = [];
|
||||
page.on('pageerror', e => errors.push(e.message));
|
||||
await page.goto(base);
|
||||
await page.waitForFunction(() => !document.querySelector('#login-message').textContent.includes('检查'));
|
||||
await page.check('#login-consent');
|
||||
await page.click('[data-action="login-browser"]');
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
await page.click('[data-action="login-confirm"]');
|
||||
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();
|
||||
assert.equal(result.ok, true);
|
||||
|
||||
@@ -17,27 +17,28 @@ await page.waitForFunction(() => !document.querySelector('#login-message').textC
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await page.waitForTimeout(400);
|
||||
await page.screenshot({ path: new URL('01-login.png', root).pathname });
|
||||
await page.click('[data-action="login-browser"]');
|
||||
await page.click('[data-action="login-wechat"]');
|
||||
assert(await page.locator('#s-login').evaluate(e => e.classList.contains('active')));
|
||||
await page.check('#login-consent');
|
||||
await page.click('[data-action="login-wechat"]');
|
||||
await page.waitForFunction(() => document.querySelector('#login-message').textContent.includes('游客'));
|
||||
await page.click('[data-action="login-browser"]');
|
||||
await page.waitForFunction(() => document.querySelector('#login-message').textContent.includes('免登录'));
|
||||
await page.click('[data-action="guest-experience"]');
|
||||
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"]');
|
||||
assert.equal(await page.locator('#p-login-success.show').count(), 0);
|
||||
await page.waitForTimeout(400);
|
||||
await page.waitForFunction(() => !document.querySelector('#toast').classList.contains('show'));
|
||||
await page.waitForTimeout(400);
|
||||
await page.screenshot({ path: new URL('02-home.png', root).pathname });
|
||||
await page.click('[data-action="history"]');
|
||||
await page.waitForFunction(() => document.querySelector('#history-count').textContent.includes('次'));
|
||||
await page.waitForFunction(() => document.querySelector('#history-count').textContent.includes('不保存'));
|
||||
await page.waitForTimeout(400);
|
||||
await page.screenshot({ path: new URL('03-history-empty.png', root).pathname });
|
||||
await page.goBack();
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
await page.reload();
|
||||
await page.waitForSelector('#s-login.active');
|
||||
await page.waitForFunction(() => !document.querySelector('#login-message').textContent.includes('检查'));
|
||||
await page.click('[data-action="guest-experience"]');
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
await page.click('[data-action="open-privacy"]');
|
||||
await page.waitForTimeout(400);
|
||||
|
||||
+22
-19
@@ -90,18 +90,19 @@ try {
|
||||
await page.waitForFunction(() => !document.querySelector('#login-message').textContent.includes('检查'));
|
||||
let browserId, browserToken;
|
||||
if (device === 0) {
|
||||
await page.check('#login-consent');
|
||||
await page.click('[data-action="login-browser"]');
|
||||
await page.click('[data-action="guest-experience"]');
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
await confirmLogin(page, 'browser');
|
||||
assert.equal(await page.locator('#p-login-success.show').count(), 0);
|
||||
browserId = await page.evaluate(() => AccountClient.user().id);
|
||||
browserToken = await page.evaluate(() => localStorage.getItem('juzi.identity.v1'));
|
||||
accounts.storeReport(browserId, { ...saved, id: 'RbrowserOriginal' });
|
||||
assert.equal(browserToken, null);
|
||||
assert.throws(() => accounts.storeReport(browserId, { ...saved, id: 'RbrowserOriginal' }));
|
||||
await page.click('[data-action="history"]');
|
||||
await page.waitForSelector('[data-report-id="RbrowserOriginal"]');
|
||||
await page.click('[data-action="switch-login"]');
|
||||
await page.waitForFunction(() => document.querySelector('#history-count').textContent.includes('不保存'));
|
||||
assert.equal(await page.locator('[data-report-id]').count(), 0);
|
||||
await page.click('[data-action="open-wechat-login"]');
|
||||
assert.equal(await page.locator('#login-consent').isChecked(), false);
|
||||
assert((await page.locator('#login-message').textContent()).includes('报告不会自动合并'));
|
||||
assert((await page.locator('#login-message').textContent()).includes('后续检测报告'));
|
||||
upstreamError = true;
|
||||
await page.check('#login-consent');
|
||||
await page.click('[data-action="login-wechat"]');
|
||||
@@ -140,26 +141,28 @@ try {
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
await confirmLogin(page, 'wechat');
|
||||
assert.equal(await page.evaluate(() => AccountClient.user().id), firstId);
|
||||
await page.click('[data-action="history"]');
|
||||
await page.click('[data-action="switch-login"]');
|
||||
await page.check('#login-consent');
|
||||
await page.click('[data-action="login-browser"]');
|
||||
assert.equal(await page.locator('[data-action="switch-login"]').count(), 0);
|
||||
await context.clearCookies(); await page.goto(base);
|
||||
await page.waitForFunction(() => !document.querySelector('#login-message').textContent.includes('检查'));
|
||||
await page.click('[data-action="guest-experience"]');
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
await confirmLogin(page, 'browser');
|
||||
assert.equal(await page.evaluate(() => AccountClient.user().id), browserId);
|
||||
assert.equal(await page.locator('#p-login-success.show').count(), 0);
|
||||
assert.notEqual(await page.evaluate(() => AccountClient.user().id), browserId, 'new guest login creates only a fresh temporary session');
|
||||
await page.click('[data-action="history"]');
|
||||
await page.waitForSelector('[data-report-id="RbrowserOriginal"]');
|
||||
await page.waitForFunction(() => document.querySelector('#history-count').textContent.includes('不保存'));
|
||||
assert.equal(await page.locator('[data-report-id]').count(), 0);
|
||||
assert.equal(await page.locator('[data-report-id="RwechatCrossDevice"]').count(), 0);
|
||||
assert.equal((await api(page, 'reports/' + saved.id)).status, 404);
|
||||
assert.equal((await api(page, 'reports/' + saved.id)).status, 401, 'no backend session before welcome consent');
|
||||
// An OAuth success URL with a lost cookie must not restore an unrelated browser credential.
|
||||
await context.clearCookies();
|
||||
await page.goto(base + '?login=success');
|
||||
await page.waitForFunction(() => document.querySelector('#login-message').textContent.includes('会话未能保存'));
|
||||
assert.equal((await api(page, 'auth/session')).data.user, null);
|
||||
await page.waitForFunction(() => !location.search.includes('login=success'));
|
||||
await page.reload(); // Ordinary browser restore is still supported.
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
assert.equal(await page.evaluate(() => AccountClient.user().id), browserId);
|
||||
await page.reload(); // A guest is never restored from browser storage.
|
||||
await page.waitForFunction(() => !document.querySelector('#login-message').textContent.includes('检查'));
|
||||
assert.equal(await page.locator('#s-login.active').count(), 1);
|
||||
assert.equal(await page.evaluate(() => AccountClient.user()), null);
|
||||
assert.equal(await page.locator('#p-login-success.show').count(), 0, 'session restore must not repeatedly open a success dialog');
|
||||
}
|
||||
assert.deepEqual(errors, []);
|
||||
@@ -179,7 +182,7 @@ try {
|
||||
assert.equal((await api(strangerPage, 'reports/' + saved.id)).status, 404);
|
||||
assert.equal((await api(strangerPage, 'reports')).data.total, 0);
|
||||
await stranger.close();
|
||||
console.log('PASS: HTTPS /state/ simulated WeChat login, Secure cookies, same identity/report across devices, different-user isolation, explicit account switching, preserved browser reports, OAuth failure/missing-cookie handling, no silent browser fallback');
|
||||
console.log('PASS: HTTPS /state/ simulated WeChat login, Secure cookies, same identity/report across devices, different-user isolation, explicit switching, ephemeral guests without history, OAuth failure/missing-cookie handling, no silent guest fallback');
|
||||
} finally {
|
||||
global.fetch = realFetch;
|
||||
if (browser) await browser.close();
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Test-only persisted WeChat sessions. Never import from production code.
|
||||
'use strict';
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const crypto = require('node:crypto');
|
||||
module.exports = function seedWechat(root, count = 1) {
|
||||
const file = path.join(root, 'identities.json');
|
||||
if (fs.existsSync(file)) throw Error('Wechat fixture requires an empty test store');
|
||||
const db = { users: [], sessions: {}, states: {} }, entries = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const user = { id: 'fixture-wechat-' + i, kind: 'wechat', identity: 'fixture-' + i, reports: [] };
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
db.users.push(user);
|
||||
db.sessions[crypto.createHash('sha256').update(token).digest('hex')] = { user: user.id, expires: Date.now() + 86400000 };
|
||||
entries.push({ user, token, cookie: 'juzi_session=' + token });
|
||||
}
|
||||
fs.mkdirSync(root, { recursive: true });
|
||||
fs.writeFileSync(file, JSON.stringify(db));
|
||||
return entries;
|
||||
};
|
||||
Reference in New Issue
Block a user