fix(h5): show raw skin age, support Harmony touch pickers and add WeChat logout

This commit is contained in:
xsl
2026-10-13 16:12:54 +08:00
parent 4e42e33994
commit e71368db6d
23 changed files with 1984 additions and 1325 deletions
@@ -0,0 +1,29 @@
# 原始肌龄显示与鸿蒙滚轮兼容
## 范围
- 生物可视年龄直接展示算法原始肌龄,取消实际年龄 ±3 的展示修正。原始肌龄缺失显示 `--`,不以实际年龄或旧展示值补齐,不额外四舍五入。
- 新报告 `skinAgeDisplay` 与 `skinAgeRaw` 相同,保留兼容字段 `ageClampNote` 但值为空。推荐规则、评分和其他话术不变。
- 历史报告回看优先读取快照 `vm.skinAgeRaw`;更老的快照缺该字段时,从保存的 `payload.result.skin_age` 读取。忽略旧的钳制展示值,不改写存量快照,不重算历史推荐/文案。后台列表、详情、CSV 的展示肌龄也改为读取原始肌龄,避免前后台不一致。
- `mobile-select@1.4.0` 原来通过 UA 字符串选择 touch/mouse,无法识别不带 Android 的 OpenHarmony/ArkWeb。保留上游版本与 MIT 声明,添加本地补丁:通过 `navigator.maxTouchPoints > 0` 或 `ontouchstart` 能力选择触摸事件,无触摸能力则使用鼠标事件;不再按系统名称判断。
- 同时按事件的 `touches/changedTouches` 读取坐标,不依赖全局 `TouchEvent` 构造函数,避免无该构造函数的鼠标环境报错。弹层挂载、页面禁滑和滚轮联动逻辑不变。
## 回归
```bash
node h5/test/raw-skin-age.js
node h5/js/skin-score.js
node h5/test/recommendation.js
node h5/test/regression.js
node h5/test/report-persistence.mjs
node h5/test/harmony-picker.mjs
node h5/test/profile-interaction.mjs
node h5/test/guest-privacy.mjs
node h5/test/report-touch-layout.mjs
```
鸿蒙专项使用 Chromium 的真实触摸/鼠标分发配合不同 UA 与能力配置,逐项验证年、月、省、市四轮、地区联动、确认回填、重开定位和表单提交。覆盖 Android 触摸、OpenHarmony 5、OpenHarmony 6+微信、桌面 UA 的触摸设备、无 TouchEvent 构造函数的桌面鼠标、Android UA 的无触摸环境。
这属于 UA/能力兼容回归,不等同于鸿蒙真机或 ArkWeb 内核验证。测试使用隔离临时目录或拦截账号请求,不写线上报告,不调用付费检测。历史报告测试故意保留旧展示值 57 岁,确认 H5/后台改显示原始肌龄且原文件字节不变。
用户已确认发版。本次与《微信退出登录-2026-10-13.md》的修改一起提交,采用 Git bundle 快进同步到 `/home/ubuntu/juzi`,重启 `juzi-h5.service` 后校验线上资源、健康与浏览器交互。不代用户推送 origin,不改写既有报告或调用线上付费检测。摄像头切换不在本次修改范围内。
+11
View File
@@ -0,0 +1,11 @@
# 微信退出登录
- 首页与「我的报告」的状态区,仅微信已登录时显示「退出登录」。游客模式不显示,不恢复旧的「切换登录方式」按钮。
- 点击使用系统确认框提示:确认退出微信登录?历史报告将保留,重新微信登录后可查看。取消不调用退出接口。
- 确认后调用现有 `api/auth/logout`,撤销当前服务端会话和 Cookie;成功才清除前端登录状态并回到入口页。失败提示错误,保留当前页面/身份并允许重试;请求期间禁用退出按钮,防止重复提交。
- 不删除账号、报告快照或微信偏好缓存。退出后重新登录同一微信,仍可查看原有历史报告。
- 退出时清空历史列表并作废在途历史请求,限制旧报告的返回导航;旧微信请求迟到也不能在游客身份下打开报告。
验证:`node h5/test/wechat-browser.mjs`(模拟微信、真实 HTTPS Cookie)覆盖取消、接口失败、确认退出、服务端会话撤销、返回键、迟到报告、游客不显示退出及重新微信登录后快照文件字节不变。`node h5/test/guest-privacy.mjs`、`node h5/test/report-persistence.mjs` 回归游客与历史报告。
用户已确认与原始肌龄显示、鸿蒙滚轮兼容一起发版。发布沿用 Git bundle 快进同步和服务重启,不代用户推送 origin,不删除历史报告。微信退出流程使用隔离模拟账号验证;线上不操作真实微信账号、报告或付费接口。
+25 -22
View File
@@ -5,27 +5,27 @@
"3": "skin-score.js", "3": "skin-score.js",
"4": "customer-feedback-second.js", "4": "customer-feedback-second.js",
"5": "disposeUnusedWeightTensors", "5": "disposeUnusedWeightTensors",
"6": "n", "6": "f",
"7": "test/profile-store.js", "7": "test/profile-store.js",
"8": "admin.js", "8": "admin.js",
"9": "__awaiter", "9": "__awaiter",
"10": "wechat-browser.mjs", "10": "customer-feedback-third.mjs",
"11": "lib/accounts.js", "11": "lib/accounts.js",
"12": "server.js", "12": "server.js",
"13": "files", "13": "files",
"14": "NeuralNetwork", "14": "showScreen",
"15": "customer-feedback-fourth.mjs", "15": "customer-feedback-fourth.mjs",
"16": "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范", "16": "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范",
"17": "report-persistence.mjs", "17": "report-persistence.mjs",
"18": "1'me肌肤状态检测 H5", "18": "1'me肌肤状态检测 H5",
"19": "customer-feedback-fourth.js", "19": "customer-feedback-fourth.js",
"20": "build_fixtures.py", "20": "build_fixtures.py",
"21": "getEnv", "21": "extractFaces",
"22": "k", "22": "i",
"23": "facepp-skinanalyze(旷视皮肤分析专业版调用技能)", "23": "facepp-skinanalyze(旷视皮肤分析专业版调用技能)",
"24": "巨子测肤 · 1'me 语音播报内容设计文档 V1.0", "24": "巨子测肤 · 1'me 语音播报内容设计文档 V1.0",
"25": "mobile-select.iife.min.js", "25": "mobile-select.iife.min.js",
"26": ".extractWeights", "26": "NeuralNetwork",
"27": "gen_voice.py", "27": "gen_voice.py",
"28": "C", "28": "C",
"29": "face-api.js 本地化依赖(h5/vendor/faceapi/)", "29": "face-api.js 本地化依赖(h5/vendor/faceapi/)",
@@ -40,13 +40,13 @@
"38": "FaceLandmark68Net", "38": "FaceLandmark68Net",
"39": "UI交互界面调整", "39": "UI交互界面调整",
"40": "第四轮反馈修改与验收", "40": "第四轮反馈修改与验收",
"41": "vizCompose", "41": "FaceLandmarks68",
"42": "layer_at", "42": "layer_at",
"43": "extractAllFacesAndComputeResults", "43": "extractAllFacesAndComputeResults",
"44": "start-local.sh", "44": "start-local.sh",
"45": "PredictAllAgeAndGenderWithFaceAlignmentTask", "45": "PredictAllAgeAndGenderWithFaceAlignmentTask",
"46": "PredictAllFaceExpressionsWithFaceAlignmentTask", "46": "PredictAllFaceExpressionsWithFaceAlignmentTask",
"47": "Box", "47": "extractFaceTensors",
"48": "已修改", "48": "已修改",
"49": "mobile-select/README.md", "49": "mobile-select/README.md",
"50": "call_skinanalyze_3view.sh", "50": "call_skinanalyze_3view.sh",
@@ -76,8 +76,8 @@
"74": "report-copy.js", "74": "report-copy.js",
"75": "prepare_h5_assets.py", "75": "prepare_h5_assets.py",
"76": "报告页滑动防冲突-2026-10-13.md", "76": "报告页滑动防冲突-2026-10-13.md",
"77": "drawFaceExpressions", "77": "FaceLandmarks",
"78": "FaceLandmarks68", "78": ".toString",
"79": "登录反馈与加载优化(2026-10-12)", "79": "登录反馈与加载优化(2026-10-12)",
"80": "线上微信登录部署记录", "80": "线上微信登录部署记录",
"81": "wechat-production.mjs", "81": "wechat-production.mjs",
@@ -92,16 +92,16 @@
"90": "customer-feedback.mjs", "90": "customer-feedback.mjs",
"91": "wi", "91": "wi",
"92": "客户反馈修改(2026-10-12)", "92": "客户反馈修改(2026-10-12)",
"93": ".runAndExtendWithFaceDetection", "93": ".runAndExtendWithFaceDetections",
"94": "第三轮反馈:PSD 对齐、验收及上线记录", "94": "第三轮反馈:PSD 对齐、验收及上线记录",
"95": "pn", "95": "pn",
"96": "play", "96": "onDetect",
"97": "account-client.js", "97": "account-client.js",
"98": "js/profile-store.js", "98": "js/profile-store.js",
"99": "capture-diagnostics.js", "99": "capture-diagnostics.js",
"100": ".computeFaceDescriptor", "100": ".draw",
"101": "wechat-fixture.cjs", "101": "wechat-fixture.cjs",
"102": "extractFaces", "102": "DetectSingleFaceLandmarksTask",
"103": "第四轮反馈/傅艳NEW与项思炼的会话 2026年10月12日.md", "103": "第四轮反馈/傅艳NEW与项思炼的会话 2026年10月12日.md",
"104": "export_fourth_feedback_assets.py", "104": "export_fourth_feedback_assets.py",
"105": "capture-quality-browser.mjs", "105": "capture-quality-browser.mjs",
@@ -110,19 +110,22 @@
"108": "profile-interaction.mjs", "108": "profile-interaction.mjs",
"109": "submitPhotos", "109": "submitPhotos",
"110": "doCapture", "110": "doCapture",
"111": "i", "111": "n",
"112": ".toString", "112": "微信退出登录-2026-10-13.md",
"113": "swipe-track.js", "113": "swipe-track.js",
"114": "optimize_delivery_images.py", "114": "optimize_delivery_images.py",
"115": "ao", "115": "normalize",
"116": "TinyYolov2Base",
"117": "startCamera", "117": "startCamera",
"118": "PredictSingleAgeAndGenderWithFaceAlignmentTask", "118": "PredictSingleAgeAndGenderWithFaceAlignmentTask",
"119": "PredictSingleFaceExpressionsWithFaceAlignmentTask", "119": "PredictSingleFaceExpressionsWithFaceAlignmentTask",
"120": "LabeledFaceDescriptors", "122": "onPhotoCaptured",
"121": ".runAndExtendWithFaceDetections",
"122": "readPhotoFile",
"123": "评分圈内居中、基础信息页固定与正脸语音修复", "123": "评分圈内居中、基础信息页固定与正脸语音修复",
"124": "TinyYolov2", "124": "TinyYolov2",
"125": "报告等待页实拍背景-2026-10-12.md" "125": "报告等待页实拍背景-2026-10-12.md",
"126": ".forwardInput",
"128": "AgeGenderNet",
"130": ".predictExpressions",
"131": "原始肌龄显示与鸿蒙滚轮兼容",
"132": "harmony-picker.mjs",
"134": "height"
} }
+1 -1
View File
@@ -1 +1 @@
{"0": "6518846594ff224e", "1": "cb6ea50bd4e4a5ca", "2": "43be28fd2ee8685d", "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": "a005abe41943c7cd", "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": "9727b30d720298e3", "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": "0adfe3c7d77dcbe1", "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": "45259f492e4f2efb", "111": "dc9609eb7ae61b97", "112": "064a2b8ba0231c0a", "113": "0d99f86b9e41c4bb", "114": "b9d7c266afa84437", "115": "a3859fc25c864a81", "116": "a99a3157460afca4", "117": "02c78dd73c398bfa", "118": "01f68dfc34a29d32", "119": "8c471c7baeaaca54", "120": "f8b38aeaaab11ec4", "121": "46204fb2e4d50c53", "122": "4e91cfdf55362604", "123": "b091e3eb82b456eb", "124": "c96276581a8c98d4", "125": "e4f29216745ec758"} {"0": "b513c3d9e2ce0ee9", "1": "cb6ea50bd4e4a5ca", "2": "8bae62aa27da5427", "3": "125a85b2d8e78e52", "4": "56bf12952b0de42b", "5": "3fef2b47fc7c0327", "6": "f5fa3e3d7d829c42", "7": "03b56b13a08b5f83", "8": "a689fcb62e7754af", "9": "58f4243880289b8c", "10": "325690c89d34f602", "11": "8fe2f9d63516be5c", "12": "aeca274694e158d2", "13": "7e3b558a587a4ebe", "14": "767b88826fe02948", "15": "fbc8a72e6fda6359", "16": "95add6a975c0cb72", "17": "e042d7fe7351bcb9", "18": "f93d5d85e671fcee", "19": "2becf835f050dc29", "20": "aecf2fce44ba49c5", "21": "11ae63a0b8df3c18", "22": "57d1520d2b3138d5", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "d2f8327bf38ee13c", "27": "eab427ce9a5219e9", "28": "d5bc4d13da321cc8", "29": "c608f1c6492e1cb1", "30": "73c13793fca09f56", "31": "fbbcfc059751cc6a", "32": "f6bcf56c4589316b", "33": "75f750c5b7d9f63d", "34": "f024ff7942935690", "35": "0a737a97b7cae9fc", "36": "a7d375041779bad4", "37": "cb8f26249485324a", "38": "9a25b1230b5fe649", "39": "fdd95375b4a5ddb3", "40": "5e546676ab96de03", "41": "68c52c80f38f8b2c", "42": "916170b9f5d7346b", "43": "0f4ade1799be2fc4", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "24c736a2281aad6e", "48": "cf2f2424bc00436f", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "812950f51be20e40", "56": "9ee2ad45dc27851d", "57": "f2a40b5f1b29f669", "58": "1b6525c6404105b9", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "5209ce2a267bf33b", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "e594ecaa8e7d1538", "65": "bdee3c4a37df52f8", "66": "9727b30d720298e3", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "be0c8310abda2467", "71": "589a2d5a76f3880a", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "3c4c278d6026bc3a", "77": "f5b5a4d7c34325bf", "78": "c3f9847676ae2caf", "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": "a38797466ce43978", "94": "713d04996c4627df", "95": "96eb6de87b194490", "96": "69eb267b9ec22897", "97": "505ea719b994ac5e", "98": "9b1cf1985223f90a", "99": "9380a89a868c76d2", "100": "bf75bfc99d4f9e34", "101": "beb276de6301634c", "102": "23f1d406f6c779ae", "103": "6ffbe07b521a061b", "104": "e114a7aebaaca2f1", "105": "af50c66d16c81bba", "106": "8bc72c179793e1c2", "107": "3877490226a78145", "108": "e0fc0a42e33bda91", "109": "4afb1715f62865b1", "110": "47af540ec3714a5e", "111": "16df50aadff513fb", "112": "af995dd35403b4dd", "113": "0d99f86b9e41c4bb", "114": "b9d7c266afa84437", "115": "5a279d83dc286aa4", "117": "4cd7191f102eea22", "118": "01f68dfc34a29d32", "119": "8c471c7baeaaca54", "122": "f4bc824c2d55ef51", "123": "b091e3eb82b456eb", "124": "c96276581a8c98d4", "125": "e4f29216745ec758", "126": "880a74c6d255e5cc", "128": "4b44a21bafc4714c", "130": "3a86f59f8a877384", "131": "482dbb7ec33d8c23", "132": "3daacabc5062b554", "134": "aef6e0c179bd709d"}
+122 -119
View File
@@ -1,16 +1,16 @@
# Graph Report - juzi (2026-10-13) # Graph Report - juzi (2026-10-13)
## Corpus Check ## Corpus Check
- 110 files · ~4,907,127 words - 114 files · ~4,908,611 words
- Verdict: corpus is large enough that graph structure adds value. - Verdict: corpus is large enough that graph structure adds value.
## Summary ## Summary
- 1475 nodes · 2920 edges · 126 communities (102 shown, 24 thin omitted) - 1491 nodes · 2938 edges · 129 communities (102 shown, 27 thin omitted)
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 209 edges (avg confidence: 0.57) - Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 209 edges (avg confidence: 0.57)
- Token cost: 0 input · 0 output - Token cost: 0 input · 0 output
## Graph Freshness ## Graph Freshness
- Built from commit: `1fdbb575` - Built from commit: `4e42e339`
- Run `git rev-parse HEAD` and compare to check if the graph is stale. - Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost). - Run `graphify update .` after code changes (no API cost).
@@ -21,27 +21,27 @@
- skin-score.js - skin-score.js
- customer-feedback-second.js - customer-feedback-second.js
- disposeUnusedWeightTensors - disposeUnusedWeightTensors
- n - f
- test/profile-store.js - test/profile-store.js
- admin.js - admin.js
- __awaiter - __awaiter
- wechat-browser.mjs - customer-feedback-third.mjs
- lib/accounts.js - lib/accounts.js
- server.js - server.js
- files - files
- NeuralNetwork - showScreen
- customer-feedback-fourth.mjs - customer-feedback-fourth.mjs
- 巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范 - 巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范
- report-persistence.mjs - report-persistence.mjs
- 1'me肌肤状态检测 H5 - 1'me肌肤状态检测 H5
- customer-feedback-fourth.js - customer-feedback-fourth.js
- build_fixtures.py - build_fixtures.py
- getEnv - extractFaces
- k - i
- facepp-skinanalyze(旷视皮肤分析专业版调用技能) - facepp-skinanalyze(旷视皮肤分析专业版调用技能)
- 巨子测肤 · 1'me 语音播报内容设计文档 V1.0 - 巨子测肤 · 1'me 语音播报内容设计文档 V1.0
- mobile-select.iife.min.js - mobile-select.iife.min.js
- .extractWeights - NeuralNetwork
- gen_voice.py - gen_voice.py
- C - C
- face-api.js 本地化依赖(h5/vendor/faceapi/) - face-api.js 本地化依赖(h5/vendor/faceapi/)
@@ -54,13 +54,13 @@
- FaceLandmark68Net - FaceLandmark68Net
- UI交互界面调整 - UI交互界面调整
- 第四轮反馈修改与验收 - 第四轮反馈修改与验收
- vizCompose - FaceLandmarks68
- layer_at - layer_at
- extractAllFacesAndComputeResults - extractAllFacesAndComputeResults
- start-local.sh - start-local.sh
- PredictAllAgeAndGenderWithFaceAlignmentTask - PredictAllAgeAndGenderWithFaceAlignmentTask
- PredictAllFaceExpressionsWithFaceAlignmentTask - PredictAllFaceExpressionsWithFaceAlignmentTask
- Box - extractFaceTensors
- 已修改 - 已修改
- mobile-select/README.md - mobile-select/README.md
- call_skinanalyze_3view.sh - call_skinanalyze_3view.sh
@@ -84,8 +84,8 @@
- 报告顶部评分与年龄排版 - 报告顶部评分与年龄排版
- build_report_copy.py - build_report_copy.py
- 报告页滑动防冲突-2026-10-13.md - 报告页滑动防冲突-2026-10-13.md
- drawFaceExpressions - FaceLandmarks
- FaceLandmarks68 - .toString
- 登录反馈与加载优化(2026-10-12) - 登录反馈与加载优化(2026-10-12)
- 线上微信登录部署记录 - 线上微信登录部署记录
- wechat-production.mjs - wechat-production.mjs
@@ -99,35 +99,38 @@
- 傅艳NEW与项思炼的会话 2026年10月12日.md - 傅艳NEW与项思炼的会话 2026年10月12日.md
- wi - wi
- 客户反馈修改(2026-10-12) - 客户反馈修改(2026-10-12)
- .runAndExtendWithFaceDetection - .runAndExtendWithFaceDetections
- 第三轮反馈:PSD 对齐、验收及上线记录 - 第三轮反馈:PSD 对齐、验收及上线记录
- pn - pn
- play - onDetect
- account-client.js - account-client.js
- js/profile-store.js - js/profile-store.js
- capture-diagnostics.js - capture-diagnostics.js
- .computeFaceDescriptor - .draw
- wechat-fixture.cjs - wechat-fixture.cjs
- extractFaces - DetectSingleFaceLandmarksTask
- 第四轮反馈/傅艳NEW与项思炼的会话 2026年10月12日.md - 第四轮反馈/傅艳NEW与项思炼的会话 2026年10月12日.md
- 第三轮反馈20261012/傅艳NEW与项思炼的会话 2026年10月12日.md - 第三轮反馈20261012/傅艳NEW与项思炼的会话 2026年10月12日.md
- 基础信息记忆与肤质拖动 - 基础信息记忆与肤质拖动
- profile-interaction.mjs - profile-interaction.mjs
- submitPhotos - submitPhotos
- doCapture - doCapture
- i - n
- .toString - 微信退出登录-2026-10-13.md
- ao - normalize
- TinyYolov2Base
- startCamera - startCamera
- PredictSingleAgeAndGenderWithFaceAlignmentTask - PredictSingleAgeAndGenderWithFaceAlignmentTask
- PredictSingleFaceExpressionsWithFaceAlignmentTask - PredictSingleFaceExpressionsWithFaceAlignmentTask
- LabeledFaceDescriptors - onPhotoCaptured
- .runAndExtendWithFaceDetections
- readPhotoFile
- 评分圈内居中、基础信息页固定与正脸语音修复 - 评分圈内居中、基础信息页固定与正脸语音修复
- TinyYolov2 - TinyYolov2
- 报告等待页实拍背景-2026-10-12.md - 报告等待页实拍背景-2026-10-12.md
- .forwardInput
- AgeGenderNet
- .predictExpressions
- 原始肌龄显示与鸿蒙滚轮兼容
- harmony-picker.mjs
- height
## God Nodes (most connected - your core abstractions) ## God Nodes (most connected - your core abstractions)
1. `__awaiter()` - 59 edges 1. `__awaiter()` - 59 edges
@@ -156,35 +159,35 @@
## Import Cycles ## Import Cycles
- None detected. - None detected.
## Communities (126 total, 24 thin omitted) ## Communities (129 total, 27 thin omitted)
### Community 0 - "face-api.js" ### Community 0 - "face-api.js"
Cohesion: 0.02 Cohesion: 0.02
Nodes (36): cn(), createBrowserEnv(), createFileSystem(), createNodejsEnv(), decodeBoxesLayer(), dr(), en(), FaceDetection() (+28 more) Nodes (41): bgrToRgbTensor(), cn(), computeReshapedDimensions(), createBrowserEnv(), createFileSystem(), createNodejsEnv(), Dimensions(), dr() (+33 more)
### Community 1 - "线上闭环测试(不含真实微信授权)" ### Community 1 - "线上闭环测试(不含真实微信授权)"
Cohesion: 0.25 Cohesion: 0.25
Nodes (7): 发现并修复, 可复现命令, 已通过的检查, 线上闭环测试(不含真实微信授权), 结论, 话术及仓库同步, 验证边界 Nodes (7): 发现并修复, 可复现命令, 已通过的检查, 线上闭环测试(不含真实微信授权), 结论, 话术及仓库同步, 验证边界
### Community 2 - "app.js" ### Community 2 - "app.js"
Cohesion: 0.12 Cohesion: 0.09
Nodes (22): cancelSkinDrag(), enterGuest(), esc(), goSkin(), loadAppFont(), loginAllowed(), loginSucceeded(), loginWechat() (+14 more) Nodes (28): cancelSkinDrag(), decode(), drain(), esc(), goSkin(), load(), loginAllowed(), loginWechat() (+20 more)
### Community 3 - "skin-score.js" ### Community 3 - "skin-score.js"
Cohesion: 0.12 Cohesion: 0.10
Nodes (45): acneGrade(), buildViewModel(), calcAcne(), calcContour(), calcSens(), calcSmooth(), calcTone(), calcWrinkles() (+37 more) Nodes (49): acneGrade(), buildViewModel(), calcAcne(), calcContour(), calcSens(), calcSmooth(), calcTone(), calcWrinkles() (+41 more)
### Community 4 - "customer-feedback-second.js" ### Community 4 - "customer-feedback-second.js"
Cohesion: 0.17 Cohesion: 0.17
Nodes (10): assert, context, face, fs, geometry, original, path, source (+2 more) Nodes (10): assert, context, face, fs, geometry, original, path, source (+2 more)
### Community 5 - "disposeUnusedWeightTensors" ### Community 5 - "disposeUnusedWeightTensors"
Cohesion: 0.09 Cohesion: 0.10
Nodes (23): disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1(), extractParamsFromWeigthMap$2() (+15 more) Nodes (23): disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1(), extractParamsFromWeigthMap$2() (+15 more)
### Community 6 - "n" ### Community 6 - "f"
Cohesion: 0.17 Cohesion: 0.20
Nodes (38): a(), ae(), bf(), ce(), cf(), d(), de(), drawContour() (+30 more) Nodes (24): a(), cf(), d(), drawContour(), dt(), ea(), f(), g() (+16 more)
### Community 7 - "test/profile-store.js" ### Community 7 - "test/profile-store.js"
Cohesion: 0.14 Cohesion: 0.14
@@ -195,16 +198,16 @@ Cohesion: 0.20
Nodes (29): ageText(), api(), bind(), captureQualityHtml(), closeDetail(), collectFilters(), dimsHtml(), doRetry() (+21 more) Nodes (29): ageText(), api(), bind(), captureQualityHtml(), closeDetail(), collectFilters(), dimsHtml(), doRetry() (+21 more)
### Community 9 - "__awaiter" ### Community 9 - "__awaiter"
Cohesion: 0.18 Cohesion: 0.11
Nodes (9): __awaiter(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow(), __generator(), loadWeightMap(), Mtcnn() (+1 more) Nodes (17): __awaiter(), bufferToImage(), ComposableTask(), FaceRecognitionNet(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow() (+9 more)
### Community 10 - "wechat-browser.mjs" ### Community 10 - "customer-feedback-third.mjs"
Cohesion: 0.22 Cohesion: 0.14
Nodes (6): createAccounts, h5, ReportUI, require, root, SkinScore Nodes (9): actualLines(), errors, guideLines, manifest, rect(), requests, root, widths (+1 more)
### Community 11 - "lib/accounts.js" ### Community 11 - "lib/accounts.js"
Cohesion: 0.07 Cohesion: 0.06
Nodes (34): atomic(), body(), cookie(), cookies(), crypto, fs, json(), login() (+26 more) Nodes (40): atomic(), body(), cookie(), cookies(), crypto, fs, json(), login() (+32 more)
### Community 12 - "server.js" ### Community 12 - "server.js"
Cohesion: 0.08 Cohesion: 0.08
@@ -214,9 +217,9 @@ Nodes (52): accounts, appendRecord(), callFacepp(), CaptureDiagnostics, CRED, cr
Cohesion: 0.12 Cohesion: 0.12
Nodes (15): files, analyze-fail, analyzing, countdown-321, hint-far, hint-front, hint-hold, hint-near (+7 more) Nodes (15): files, analyze-fail, analyzing, countdown-321, hint-far, hint-front, hint-hold, hint-near (+7 more)
### Community 14 - "NeuralNetwork" ### Community 14 - "showScreen"
Cohesion: 0.20 Cohesion: 0.19
Nodes (3): getModelUris(), NeuralNetwork(), TinyFaceDetector() Nodes (13): enterGuest(), loadAppFont(), loginSucceeded(), logoutWechat(), openOverlay(), prepareScanArtwork(), renderAccountStatus(), resetReport() (+5 more)
### Community 15 - "customer-feedback-fourth.mjs" ### Community 15 - "customer-feedback-fourth.mjs"
Cohesion: 0.20 Cohesion: 0.20
@@ -242,13 +245,13 @@ Nodes (15): assert, check(), Diagnostics, face(), HeadPose, held, ReportUI, norm
Cohesion: 0.40 Cohesion: 0.40
Nodes (9): build_case1(), build_case2(), build_truth(), col(), dump(), main(), parse_value(), set_path() (+1 more) Nodes (9): build_case1(), build_case2(), build_truth(), col(), dump(), main(), parse_value(), set_path() (+1 more)
### Community 21 - "getEnv" ### Community 21 - "extractFaces"
Cohesion: 0.07 Cohesion: 0.22
Nodes (34): awaitMediaLoaded(), bufferToImage(), conv(), conv$1(), convDown(), convLayer$1(), convNoRelu(), createCanvas() (+26 more) Nodes (17): awaitMediaLoaded(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractImagePatches(), getContext2dOrThrow(), getEnv(), getMediaDimensions() (+9 more)
### Community 22 - "k" ### Community 22 - "i"
Cohesion: 0.16 Cohesion: 0.08
Nodes (16): ba(), ca(), da(), fa(), k(), pa(), pd(), Pi() (+8 more) Nodes (31): b(), ba(), ca(), cl(), da(), dn(), ee(), et() (+23 more)
### Community 23 - "facepp-skinanalyze(旷视皮肤分析专业版调用技能)" ### Community 23 - "facepp-skinanalyze(旷视皮肤分析专业版调用技能)"
Cohesion: 0.20 Cohesion: 0.20
@@ -262,17 +265,17 @@ Nodes (8): 1. 音频格式约定(已定), 2. 播报清单(14 条), 3. 播放策
Cohesion: 0.20 Cohesion: 0.20
Nodes (16): _arrayLikeToArray(), _arrayWithHoles(), _arrayWithoutHoles(), _classCallCheck(), _createClass(), _defineProperties(), h(), _iterableToArray() (+8 more) Nodes (16): _arrayLikeToArray(), _arrayWithHoles(), _arrayWithoutHoles(), _classCallCheck(), _createClass(), _defineProperties(), h(), _iterableToArray() (+8 more)
### Community 26 - ".extractWeights" ### Community 26 - "NeuralNetwork"
Cohesion: 0.05 Cohesion: 0.06
Nodes (30): AgeGenderNet(), createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory() (+22 more) Nodes (28): createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory(), extractFCParamsFactory() (+20 more)
### Community 27 - "gen_voice.py" ### Community 27 - "gen_voice.py"
Cohesion: 0.30 Cohesion: 0.30
Nodes (11): ndarray, Path, gen_countdown(), main(), normalize_clip(), 倒计时单独按字生成再拼接,避免「三、二、一」整句被读成播报腔、与提示音气质漂移。 与提示音共用同一 speaker + instruct;句间留约…, 裁掉过长首尾静音,保留 ≤150ms 边沿,避免提示显迟钝。, 响度归一 −16 LUFS + 转 MP3 48k CBR mono 22050Hz。 (+3 more) Nodes (11): ndarray, Path, gen_countdown(), main(), normalize_clip(), 倒计时单独按字生成再拼接,避免「三、二、一」整句被读成播报腔、与提示音气质漂移。 与提示音共用同一 speaker + instruct;句间留约…, 裁掉过长首尾静音,保留 ≤150ms 边沿,避免提示显迟钝。, 响度归一 −16 LUFS + 转 MP3 48k CBR mono 22050Hz。 (+3 more)
### Community 28 - "C" ### Community 28 - "C"
Cohesion: 0.11 Cohesion: 0.09
Nodes (28): Bo(), C(), cd(), cl(), co(), eo(), fo(), Ft() (+20 more) Nodes (31): ao(), Bo(), C(), cd(), co(), Do(), eo(), fo() (+23 more)
### Community 29 - "face-api.js 本地化依赖(h5/vendor/faceapi/)" ### Community 29 - "face-api.js 本地化依赖(h5/vendor/faceapi/)"
Cohesion: 0.29 Cohesion: 0.29
@@ -287,8 +290,8 @@ Cohesion: 0.60
Nodes (4): main(), 递归缩放 result 内全部坐标:{left,top,width,height} 矩形与 {x,y} 点,与原数据同为整数。, scale_coords(), to_data_url() Nodes (4): main(), 递归缩放 result 内全部坐标:{left,top,width,height} 矩形与 {x,y} 点,与原数据同为整数。, scale_coords(), to_data_url()
### Community 35 - "customer-feedback.js" ### Community 35 - "customer-feedback.js"
Cohesion: 0.06 Cohesion: 0.11
Nodes (27): alternate, approved, assert, crop, details, excellent, fs, model (+19 more) Nodes (18): alternate, approved, assert, crop, details, excellent, fs, model (+10 more)
### Community 37 - "UI 外发版 10.09 图片资源" ### Community 37 - "UI 外发版 10.09 图片资源"
Cohesion: 0.40 Cohesion: 0.40
@@ -302,25 +305,21 @@ Nodes (4): 1meAI测肤旷视UI问题反馈, UI交互界面调整, 交互, **设
Cohesion: 0.22 Cohesion: 0.22
Nodes (8): PSD 来源, 前端参数及限制, 后台校准留痕, 已确认并实现, 生产部署及只读验收, 皱纹(紧塑力)原文, 第四轮反馈修改与验收, 验证 Nodes (8): PSD 来源, 前端参数及限制, 后台校准留痕, 已确认并实现, 生产部署及只读验收, 皱纹(紧塑力)原文, 第四轮反馈修改与验收, 验证
### Community 41 - "vizCompose"
Cohesion: 0.18
Nodes (11): renderReport(), renderViz(), setDim(), vizCollectMarks(), vizCompose(), vizFaceCrop(), vizLoadImage(), vizMapURL() (+3 more)
### Community 42 - "layer_at" ### Community 42 - "layer_at"
Cohesion: 0.18 Cohesion: 0.18
Nodes (11): list, bounds(), layer_at(), main(), save_image(), composite(), main(), sha256() (+3 more) Nodes (11): list, bounds(), layer_at(), main(), save_image(), composite(), main(), sha256() (+3 more)
### Community 43 - "extractAllFacesAndComputeResults" ### Community 43 - "extractAllFacesAndComputeResults"
Cohesion: 0.14 Cohesion: 0.12
Nodes (10): age(), extendWithAge(), extendWithFaceExpressions(), extendWithGender(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult(), PredictAllAgeAndGenderTask(), PredictAllFaceExpressionsTask() (+2 more) Nodes (9): ComputeAllFaceDescriptorsTask(), ComputeSingleFaceDescriptorTask(), extendWithAge(), extendWithFaceDescriptor(), extendWithGender(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult(), PredictAllAgeAndGenderTask() (+1 more)
### Community 44 - "start-local.sh" ### Community 44 - "start-local.sh"
Cohesion: 0.50 Cohesion: 0.50
Nodes (3): HOST, PORT, start-local.sh script Nodes (3): HOST, PORT, start-local.sh script
### Community 47 - "Box" ### Community 47 - "extractFaceTensors"
Cohesion: 0.11 Cohesion: 0.14
Nodes (11): bgrToRgbTensor(), Box(), Dimensions(), extendWithFaceLandmarks(), FaceLandmarks(), isDimensions(), isValidNumber(), isValidProbablitiy() (+3 more) Nodes (6): Box(), DetectAllFaceLandmarksTask(), extendWithFaceLandmarks(), extractFaceTensors(), isTensor3D(), ObjectDetection()
### Community 48 - "已修改" ### Community 48 - "已修改"
Cohesion: 0.22 Cohesion: 0.22
@@ -347,8 +346,8 @@ Cohesion: 0.60
Nodes (5): diffCtx(), loadJSON(), main(), okNum(), okStr() Nodes (5): diffCtx(), loadJSON(), main(), okNum(), okStr()
### Community 58 - ".forwardInput" ### Community 58 - ".forwardInput"
Cohesion: 0.14 Cohesion: 0.23
Nodes (16): computeReshapedDimensions(), extractBoundingBoxes(), getSizesForScale(), iou(), isEven(), nonMaxSuppression(), nonMaxSuppression$1(), normalize$1() (+8 more) Nodes (7): iou(), nonMaxSuppression(), nonMaxSuppression$1(), scale(), stage1(), stage2(), stage3()
### Community 61 - "报告布局与跟手交互调整" ### Community 61 - "报告布局与跟手交互调整"
Cohesion: 0.33 Cohesion: 0.33
@@ -363,7 +362,7 @@ Cohesion: 0.29
Nodes (4): EXPECTED, fails, pageErrors, pwRoot Nodes (4): EXPECTED, fails, pageErrors, pwRoot
### Community 64 - "recommendation.js" ### Community 64 - "recommendation.js"
Cohesion: 0.38 Cohesion: 0.33
Nodes (6): assert, check(), missing, raw, SS, UI Nodes (6): assert, check(), missing, raw, SS, UI
### Community 65 - "微信登录接入与公网部署说明" ### Community 65 - "微信登录接入与公网部署说明"
@@ -382,9 +381,13 @@ Nodes (7): assert, end, fs, path, source, start, vm
Cohesion: 0.29 Cohesion: 0.29
Nodes (3): errors, samples, unexpectedWrites Nodes (3): errors, samples, unexpectedWrites
### Community 77 - "drawFaceExpressions" ### Community 77 - "FaceLandmarks"
Cohesion: 0.19 Cohesion: 0.12
Nodes (10): DrawBox(), drawDetections(), drawFaceExpressions(), DrawFaceLandmarks(), extendWithFaceDetection(), FaceExpressions(), isWithFaceDetection(), isWithFaceExpressions() (+2 more) Nodes (11): DrawBox(), drawDetections(), drawFaceExpressions(), DrawFaceLandmarks(), FaceExpressions(), FaceLandmarks(), isWithFaceDetection(), isWithFaceExpressions() (+3 more)
### Community 78 - ".toString"
Cohesion: 0.15
Nodes (8): FaceLandmarks5(), FaceMatch(), getCenterPoint(), la(), Point(), pr(), pt(), vt()
### Community 79 - "登录反馈与加载优化(2026-10-12)" ### Community 79 - "登录反馈与加载优化(2026-10-12)"
Cohesion: 0.25 Cohesion: 0.25
@@ -430,25 +433,25 @@ Nodes (9): Ci(), Ei(), Fi(), Ii(), ki(), pe(), ri(), wi() (+1 more)
Cohesion: 0.33 Cohesion: 0.33
Nodes (5): 公网部署与复测, 客户反馈修改(2026-10-12), 皱纹数据说明, 逐项处理, 验证 Nodes (5): 公网部署与复测, 客户反馈修改(2026-10-12), 皱纹数据说明, 逐项处理, 验证
### Community 93 - ".runAndExtendWithFaceDetection" ### Community 93 - ".runAndExtendWithFaceDetections"
Cohesion: 0.23 Cohesion: 0.21
Nodes (7): allFacesMtcnn(), allFacesSsdMobilenetv1(), allFacesTinyYolov2(), ComposableTask(), detectAllFaces(), DetectSingleFaceTask(), Gf() Nodes (7): allFacesMtcnn(), allFacesSsdMobilenetv1(), allFacesTinyYolov2(), detectAllFaces(), DetectAllFacesTask(), DetectSingleFaceTask(), extendWithFaceDetection()
### Community 94 - "第三轮反馈:PSD 对齐、验收及上线记录" ### Community 94 - "第三轮反馈:PSD 对齐、验收及上线记录"
Cohesion: 0.33 Cohesion: 0.33
Nodes (5): 原稿与导出可追溯性, 已完成, 本地验证, 生产部署及只读验收, 第三轮反馈:PSD 对齐、验收及上线记录 Nodes (5): 原稿与导出可追溯性, 已完成, 本地验证, 生产部署及只读验收, 第三轮反馈:PSD 对齐、验收及上线记录
### Community 95 - "pn" ### Community 95 - "pn"
Cohesion: 0.22 Cohesion: 0.20
Nodes (8): bn(), fn(), ln(), pn(), un(), wl(), wn(), xn() Nodes (9): bn(), fn(), ln(), pd(), pn(), un(), wl(), wn() (+1 more)
### Community 96 - "play" ### Community 96 - "onDetect"
Cohesion: 0.22 Cohesion: 0.25
Nodes (9): busyWithFlow(), decode(), drain(), load(), play(), preload(), start(), stopCur() (+1 more) Nodes (8): captureHaptic(), checkStill(), detectFrame(), detTick(), faceLuma(), faceOffsetX(), onDetect(), setMask()
### Community 97 - "account-client.js" ### Community 97 - "account-client.js"
Cohesion: 0.57 Cohesion: 0.53
Nodes (7): browser(), clearGuestCache(), ensureGuest(), guest(), init(), rememberKind(), request() Nodes (8): browser(), clearGuestCache(), ensureGuest(), guest(), init(), logout(), rememberKind(), request()
### Community 98 - "js/profile-store.js" ### Community 98 - "js/profile-store.js"
Cohesion: 0.67 Cohesion: 0.67
@@ -458,18 +461,10 @@ Nodes (3): clean(), create(), key()
Cohesion: 0.38 Cohesion: 0.38
Nodes (6): assess(), CaptureQuality, HeadPose, normalize(), scalar(), views 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" ### Community 101 - "wechat-fixture.cjs"
Cohesion: 0.50 Cohesion: 0.50
Nodes (3): crypto, fs, path Nodes (3): crypto, fs, path
### Community 102 - "extractFaces"
Cohesion: 0.14
Nodes (9): height(), DetectAllFaceLandmarksTask(), DetectSingleFaceLandmarksTask(), extractFaces(), extractFaceTensors(), FaceLandmark68NetBase(), imageTensorToCanvas(), isTensor3D() (+1 more)
### Community 107 - "基础信息记忆与肤质拖动" ### Community 107 - "基础信息记忆与肤质拖动"
Cohesion: 0.40 Cohesion: 0.40
Nodes (4): 回归命令, 基础信息记忆与肤质拖动, 肤质交互, 范围 Nodes (4): 回归命令, 基础信息记忆与肤质拖动, 肤质交互, 范围
@@ -483,51 +478,59 @@ Cohesion: 0.24
Nodes (12): bindBackdropClose(), closeLoginSuccess(), closeOverlay(), handleCamHidden(), leaveCamera(), photosByView(), popHistorySilently(), requestAnalyze() (+4 more) Nodes (12): bindBackdropClose(), closeLoginSuccess(), closeOverlay(), handleCamHidden(), leaveCamera(), photosByView(), popHistorySilently(), requestAnalyze() (+4 more)
### Community 110 - "doCapture" ### Community 110 - "doCapture"
Cohesion: 0.15 Cohesion: 0.26
Nodes (24): armNudge(), beginDetect(), captureFromVideo(), captureHaptic(), captureStageHint(), detectFrame(), detTick(), doCapture() (+16 more) Nodes (14): armNudge(), beginDetect(), captureFromVideo(), doCapture(), enableFallback(), hideNudge(), isManualMode(), loadFaceApi() (+6 more)
### Community 111 - "i" ### Community 111 - "n"
Cohesion: 0.09 Cohesion: 0.12
Nodes (29): b(), be(), bi(), dn(), ee(), Ef(), er(), et() (+21 more) Nodes (36): ae(), be(), bf(), bi(), ce(), de(), e(), Ef() (+28 more)
### Community 112 - ".toString" ### Community 115 - "normalize"
Cohesion: 0.18 Cohesion: 0.07
Nodes (7): FaceLandmarks5(), FaceMatch(), getCenterPoint(), la(), pr(), pt(), vt() Nodes (28): conv(), conv$1(), convDown(), convLayer(), convLayer$1(), convNoRelu(), convWithBatchNorm(), denseBlock3() (+20 more)
### 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" ### Community 117 - "startCamera"
Cohesion: 0.29 Cohesion: 0.29
Nodes (8): applyMirror(), effMirror(), getCamStream(), handleCamVisible(), onCamTrackEnded(), renderCamStep(), startCamera(), waitVideoStable() Nodes (7): applyMirror(), effMirror(), getCamStream(), handleCamVisible(), onCamTrackEnded(), startCamera(), waitVideoStable()
### Community 122 - "readPhotoFile" ### Community 122 - "onPhotoCaptured"
Cohesion: 0.40 Cohesion: 0.27
Nodes (5): checkStill(), faceLuma(), hideCamStatus(), readPhotoFile(), showCamStatus() Nodes (10): busyWithFlow(), captureStageHint(), dropHint(), hideCamStatus(), onPhotoCaptured(), play(), readPhotoFile(), renderCamStep() (+2 more)
### Community 123 - "评分圈内居中、基础信息页固定与正脸语音修复" ### Community 123 - "评分圈内居中、基础信息页固定与正脸语音修复"
Cohesion: 0.40 Cohesion: 0.40
Nodes (4): 发布, 范围, 评分圈内居中、基础信息页固定与正脸语音修复, 验证 Nodes (4): 发布, 范围, 评分圈内居中、基础信息页固定与正脸语音修复, 验证
### Community 126 - ".forwardInput"
Cohesion: 0.22
Nodes (9): boxPredictionLayer(), decodeBoxesLayer(), depthwiseConvLayer(), getCenterCoordinatesAndSizesLayer(), getStridesForLayerIdx(), mobileNetV1(), outputLayer(), pointwiseConvLayer() (+1 more)
### Community 130 - ".predictExpressions"
Cohesion: 0.15
Nodes (4): extendWithFaceExpressions(), FaceExpressionNet(), PredictAllFaceExpressionsTask(), PredictSingleFaceExpressionsTask()
### Community 131 - "原始肌龄显示与鸿蒙滚轮兼容"
Cohesion: 0.50
Nodes (3): 原始肌龄显示与鸿蒙滚轮兼容, 回归, 范围
### Community 134 - "height"
Cohesion: 0.32
Nodes (5): height(), FaceLandmark68NetBase(), getSizesForScale(), normalize$1(), rescaleAndNormalize()
## Knowledge Gaps ## Knowledge Gaps
- **330 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+325 more) - **339 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+334 more)
These have ≤1 connection - possible missing edges or undocumented components. These have ≤1 connection - possible missing edges or undocumented components.
- **24 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes. - **27 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions ## Suggested Questions
_Questions this graph is uniquely positioned to answer:_ _Questions this graph is uniquely positioned to answer:_
- **Why does `height()` connect `extractFaces` to `app.js`, `customer-feedback.js`, `scan-placeholder.mjs`, `profile-interaction.mjs`, `.forwardInput`?** - **Why does `height()` connect `height` to `app.js`, `scan-placeholder.mjs`, `__awaiter`, `customer-feedback-third.mjs`, `profile-interaction.mjs`, `showScreen`, `extractFaceTensors`, `extractFaces`, `.forwardInput`?**
_High betweenness centrality (0.050) - this node is a cross-community bridge._ _High betweenness centrality (0.051) - this node is a cross-community bridge._
- **Why does `v()` connect `n` to `report-ui.js`, `vizCompose`, `C`, `face-api.js`?** - **Why does `v()` connect `f` to `report-ui.js`, `face-api.js`, `app.js`, `C`?**
_High betweenness centrality (0.038) - this node is a cross-community bridge._ _High betweenness centrality (0.043) - this node is a cross-community bridge._
- **Why does `r()` connect `n` to `face-api.js`, `server.js`, `i`, `.toString`, `getEnv`, `k`, `C`?** - **Why does `fill()` connect `report-ui.js` to `f`?**
_High betweenness centrality (0.037) - this node is a cross-community bridge._ _High betweenness centrality (0.040) - this node is a cross-community bridge._
- **Are the 24 inferred relationships involving `n()` (e.g. with `newRecordId()` and `ae()`) actually correct?** - **Are the 24 inferred relationships involving `n()` (e.g. with `newRecordId()` and `ae()`) actually correct?**
_`n()` has 24 INFERRED edges - model-reasoned connections that need verification._ _`n()` has 24 INFERRED edges - model-reasoned connections that need verification._
- **Are the 27 inferred relationships involving `r()` (e.g. with `handleExport()` and `handleRecordsQuery()`) actually correct?** - **Are the 27 inferred relationships involving `r()` (e.g. with `handleExport()` and `handleRecordsQuery()`) actually correct?**
@@ -535,4 +538,4 @@ _Questions this graph is uniquely positioned to answer:_
- **Are the 9 inferred relationships involving `i()` (e.g. with `da()` and `f()`) actually correct?** - **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._ _`i()` has 9 INFERRED edges - model-reasoned connections that need verification._
- **What connects `source`, `document_size`, `pages` to the rest of the system?** - **What connects `source`, `document_size`, `pages` to the rest of the system?**
_330 weakly-connected nodes found - possible documentation gaps or missing edges._ _339 weakly-connected nodes found - possible documentation gaps or missing edges._
File diff suppressed because one or more lines are too long
+1508 -1125
View File
File diff suppressed because it is too large Load Diff
+44 -24
View File
@@ -40,8 +40,8 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/js/app.js": { "h5/js/app.js": {
"mtime": 1791875688.6889038, "mtime": 1791878787.1929805,
"ast_hash": "4d2624826f32e0c1af544e450117fcc6", "ast_hash": "63cd1c83937dee7144a854b559f9cfd5",
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/js/head-pose.js": { "h5/js/head-pose.js": {
@@ -50,13 +50,13 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/js/skin-score.js": { "h5/js/skin-score.js": {
"mtime": 1791735501.357809, "mtime": 1791878308.0809805,
"ast_hash": "783b6c9abbe2bf1832e8eafe5aead7e3", "ast_hash": "59d3331a75a32837e04a21afa24fb34a",
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/server.js": { "h5/server.js": {
"mtime": 1791873518.1209404, "mtime": 1791878239.2009728,
"ast_hash": "d2b79f3c00c8a4eaecc3c74b1c1e4dea", "ast_hash": "fd843f8a1ed62e33c8294acbe78b56d0",
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/test/build_fixtures.py": { "h5/test/build_fixtures.py": {
@@ -135,13 +135,13 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/README.md": { "h5/README.md": {
"mtime": 1791873518.1209404, "mtime": 1791878573.716988,
"ast_hash": "4acf1f915e74619da8124d47eeb4d971", "ast_hash": "74712e76c626625674a70aa0dcad5a63",
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/index.html": { "h5/index.html": {
"mtime": 1791876371.4129393, "mtime": 1791878732.5729809,
"ast_hash": "7d998c3c1ba3614130709ca23490946b", "ast_hash": "ac31b30a7a6a79a97355db95323784f9",
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/vendor/faceapi/README.md": { "h5/vendor/faceapi/README.md": {
@@ -830,8 +830,8 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/vendor/mobile-select/mobile-select.iife.min.js": { "h5/vendor/mobile-select/mobile-select.iife.min.js": {
"mtime": 1791559485.790093, "mtime": 1791878239.1849728,
"ast_hash": "52578c3b95933f96493d3b879d353421", "ast_hash": "9aeccc95862e05a14439c9cc906af5e5",
"semantic_hash": "" "semantic_hash": ""
}, },
"tools/export_psd_assets.py": { "tools/export_psd_assets.py": {
@@ -865,8 +865,8 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/vendor/mobile-select/README.md": { "h5/vendor/mobile-select/README.md": {
"mtime": 1791559485.790093, "mtime": 1791878239.2009728,
"ast_hash": "fe3287899b96f598c60f9bc030a1368e", "ast_hash": "f8b834ff5056a3402914e3c99d4807ff",
"semantic_hash": "" "semantic_hash": ""
}, },
"design/ui-20261009/assets-overview.jpg": { "design/ui-20261009/assets-overview.jpg": {
@@ -1230,8 +1230,8 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/js/account-client.js": { "h5/js/account-client.js": {
"mtime": 1791875481.336928, "mtime": 1791878732.5729809,
"ast_hash": "d296f1dfb67a31adfde3221d48439186", "ast_hash": "6e1c148f77744345313e2a7ecf1ea756",
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/js/camera-geometry.js": { "h5/js/camera-geometry.js": {
@@ -1260,8 +1260,8 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/test/report-persistence.mjs": { "h5/test/report-persistence.mjs": {
"mtime": 1791873250.4369178, "mtime": 1791878308.0849807,
"ast_hash": "1d543844b6961902d3cfcafa713b9a3b", "ast_hash": "00d39c0fc96afe463fb1fbfe08b70656",
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/test/ui-20261009.mjs": { "h5/test/ui-20261009.mjs": {
@@ -1530,8 +1530,8 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/test/wechat-browser.mjs": { "h5/test/wechat-browser.mjs": {
"mtime": 1791875570.3129308, "mtime": 1791879046.0249834,
"ast_hash": "3f5bcc6b56b3c28ecd7fb244ca645865", "ast_hash": "af64c3a7fcd5c9338f9b86a584f987b6",
"semantic_hash": "" "semantic_hash": ""
}, },
"docs/2026-10-11\u63d0\u4ea4\u524dReview.md": { "docs/2026-10-11\u63d0\u4ea4\u524dReview.md": {
@@ -1565,8 +1565,8 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/test/recommendation.js": { "h5/test/recommendation.js": {
"mtime": 1791735559.0619977, "mtime": 1791878308.0849807,
"ast_hash": "589f7046d94485dfc1df20a6129640e0", "ast_hash": "36742c00e21d68daae2e4678b185b3a1",
"semantic_hash": "" "semantic_hash": ""
}, },
"docs/\u4ea7\u54c1\u63a8\u8350\u89c4\u5219-\u80a4\u9f84\u7248.md": { "docs/\u4ea7\u54c1\u63a8\u8350\u89c4\u5219-\u80a4\u9f84\u7248.md": {
@@ -1975,8 +1975,8 @@
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/test/guest-privacy.mjs": { "h5/test/guest-privacy.mjs": {
"mtime": 1791876371.4169395, "mtime": 1791878787.1969805,
"ast_hash": "e00620b50cf127df5b774a8a650aaa03", "ast_hash": "b6143cba8c2aff80dd5c4687208b9f36",
"semantic_hash": "" "semantic_hash": ""
}, },
"h5/test/wechat-fixture.cjs": { "h5/test/wechat-fixture.cjs": {
@@ -1988,5 +1988,25 @@
"mtime": 1791876185.0449448, "mtime": 1791876185.0449448,
"ast_hash": "905d6f5ae894843f99e66a15ad3d8b33", "ast_hash": "905d6f5ae894843f99e66a15ad3d8b33",
"semantic_hash": "" "semantic_hash": ""
},
"h5/test/harmony-picker.mjs": {
"mtime": 1791878418.4889727,
"ast_hash": "299b3a47e9212d7e758fc89cfffc39f8",
"semantic_hash": ""
},
"h5/test/raw-skin-age.js": {
"mtime": 1791878418.4889727,
"ast_hash": "44f22ac698b2249c8611ff302528dbb9",
"semantic_hash": ""
},
"docs/\u539f\u59cb\u808c\u9f84\u663e\u793a\u4e0e\u9e3f\u8499\u6eda\u8f6e\u517c\u5bb9-2026-10-13.md": {
"mtime": 1791878573.720988,
"ast_hash": "7003663bd6ea0a0cf586cb2e3155d925",
"semantic_hash": ""
},
"docs/\u5fae\u4fe1\u9000\u51fa\u767b\u5f55-2026-10-13.md": {
"mtime": 1791878884.432984,
"ast_hash": "ea59ab0974069d858e08da9789b88bf2",
"semantic_hash": ""
} }
} }
+2 -2
View File
@@ -91,10 +91,10 @@ node server.js # 默认 8080,可用 PORT=xxx 修改
3. 评分由 `js/skin-score.js` 引擎完成,公式/锁值/话术/产品推荐以 `docs/三维度评分与话术生成需求文档_V2.3.docx` 为最高契约(V2.3 口径,含 §8.5 护理建议五段): 3. 评分由 `js/skin-score.js` 引擎完成,公式/锁值/话术/产品推荐以 `docs/三维度评分与话术生成需求文档_V2.3.docx` 为最高契约(V2.3 口径,含 §8.5 护理建议五段):
- 6 分类:轮廓 = min(眼袋映射分, 下颌线角度分);皱纹(12 分区);光滑(毛孔/黑头/纹理);肤色水润(色素×水润);敏感(泛红面积取 `red_spot.red_spot_area`/红区分);痘痘(分级映射×丘疹 `acne.count` 计数扣分);分组按实际年龄; - 6 分类:轮廓 = min(眼袋映射分, 下颌线角度分);皱纹(12 分区);光滑(毛孔/黑头/纹理);肤色水润(色素×水润);敏感(泛红面积取 `red_spot.red_spot_area`/红区分);痘痘(分级映射×丘疹 `acne.count` 计数扣分);分组按实际年龄;
- 三维度木桶 min:紧塑力(轮廓+皱纹)、活力感(光滑+肤色水润)、稳定力(敏感+痘痘); - 三维度木桶 min:紧塑力(轮廓+皱纹)、活力感(光滑+肤色水润)、稳定力(敏感+痘痘);
- S = 7 + 3×P/100;档位:良好(P≥90)/改善(80–90)/预警(<80);肤龄钳制到实际年龄 ±3 并留痕;三视图版 18 条话术; - S = 7 + 3×P/100;档位:良好(P≥90)/改善(80–90)/预警(<80);生物可视年龄直接展示算法原始肌龄,不再按实际年龄 ±3 修正;三视图版 18 条话术;
- 精华推荐三重判据(均分档/最差项约束/年龄组档)取最强档,triggers 记录命中判据。 - 精华推荐三重判据(均分档/最差项约束/年龄组档)取最强档,triggers 记录命中判据。
4. 前端 `SkinScore.buildViewModel(data.result, data.side, { realAge, skinType })` → viewModel 直渲报告页: 4. 前端 `SkinScore.buildViewModel(data.result, data.side, { realAge, skinType })` → viewModel 直渲报告页:
- 头部:综合评分(`vm.total`,缺失回退三维度 S 均分×10)、肤龄 `vm.skinAgeDisplay` + 钳制标记 `vm.ageClampNote`;三维度 S(1 位小数)+ 档位配色(良好绿/改善琥珀/预警红); - 头部:综合评分(`vm.total`,缺失回退三维度 S 均分×10)、肤龄读取 `SkinScore.reportSkinAge(vm, result)`;历史报告同样读取原始肌龄而非旧修正展示值,原值缺失显示 `--`;三维度 S(1 位小数)+ 档位配色(良好绿/改善琥珀/预警红);
- 三个 Tab 面板各展示所辖两分类:分类名、S + 档位徽章、话术正文(`vm.cats.*.text`);P 值不直接展示,`P=null` 的分类显示"本次未检出有效数据"; - 三个 Tab 面板各展示所辖两分类:分类名、S + 档位徽章、话术正文(`vm.cats.*.text`);P 值不直接展示,`P=null` 的分类显示"本次未检出有效数据";
- 报告底部:降级标注(`vm.notes` 中与侧脸/下颌线/补拍相关的条目,去掉接口质量码后展示)、护理建议五段(`vm.careAdvice`)、精华推荐卡(`vm.essence.name` + 命中判据;推荐正文已在护理建议第 4 段,卡内不重复); - 报告底部:降级标注(`vm.notes` 中与侧脸/下颌线/补拍相关的条目,去掉接口质量码后展示)、护理建议五段(`vm.careAdvice`)、精华推荐卡(`vm.essence.name` + 命中判据;推荐正文已在护理建议第 4 段,卡内不重复);
- 近期优先关注 = 最差维度(`vm.focus`);6 分类各配检测可视化图:轮廓=正脸实拍、皱纹=正脸压暗+`texture_enhanced_lines` 皱纹增强线(线图层几乎全透明,直挂白底不可读,故与压暗沉脸合成)、光滑=正脸+色斑/痣/痤疮标记叠加、肤色=`brown_area` 色沉图、敏感=`red_area` 红区图、痘痘=正脸+闭口粉刺/痤疮标记叠加(marks 按原像素坐标叠加到正脸实拍)。 - 近期优先关注 = 最差维度(`vm.focus`);6 分类各配检测可视化图:轮廓=正脸实拍、皱纹=正脸压暗+`texture_enhanced_lines` 皱纹增强线(线图层几乎全透明,直挂白底不可读,故与压暗沉脸合成)、光滑=正脸+色斑/痣/痤疮标记叠加、肤色=`brown_area` 色沉图、敏感=`red_area` 红区图、痘痘=正脸+闭口粉刺/痤疮标记叠加(marks 按原像素坐标叠加到正脸实拍)。
+1
View File
@@ -1,6 +1,7 @@
/* Third-round PSD, 2026-10-12. Measurements include text transforms. /* Third-round PSD, 2026-10-12. Measurements include text transforms.
Native 248px WeChat header is excluded. Fixed text sizes/line breaks are Native 248px WeChat header is excluded. Fixed text sizes/line breaks are
intentional; flexible photo backgrounds and safe areas accommodate phones. */ intentional; flexible photo backgrounds and safe areas accommodate phones. */
.account-logout { font-size: 11px; opacity: .8; white-space: nowrap; }
.splash-slogan { .splash-slogan {
left: calc(50% - 1.041667px); right: auto; left: calc(50% - 1.041667px); right: auto;
width: 249.305556px; height: 75.347222px; width: 249.305556px; height: 75.347222px;
+2 -2
View File
@@ -32,7 +32,7 @@
<section class="screen" id="s-history"> <section class="screen" id="s-history">
<div class="history-content"> <div class="history-content">
<h1 class="report-title">您的报告</h1> <h1 class="report-title">您的报告</h1>
<div class="account-status account-status-history"><span data-auth-status></span></div> <div class="account-status account-status-history"><span data-auth-status></span><button type="button" class="account-logout" data-action="logout-wechat" hidden>退出登录</button></div>
<div class="card history-count"><span>测试数量</span><p id="history-count">正在读取报告…</p></div> <div class="card history-count"><span>测试数量</span><p id="history-count">正在读取报告…</p></div>
<p class="identity-note" id="identity-note"></p> <p class="identity-note" id="identity-note"></p>
<div id="history-list" aria-live="polite"></div> <div id="history-list" aria-live="polite"></div>
@@ -42,7 +42,7 @@
</section> </section>
<section class="screen" id="s-splash"> <section class="screen" id="s-splash">
<div class="splash-bg"></div> <div class="splash-bg"></div>
<div class="account-status account-status-home"><span data-auth-status></span></div> <div class="account-status account-status-home"><span data-auth-status></span><button type="button" class="account-logout" data-action="logout-wechat" hidden>退出登录</button></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;以我为始 自有表现"> <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="splash-bottom">
<div class="brand-row"> <div class="brand-row">
+6 -1
View File
@@ -56,5 +56,10 @@ window.AccountClient = (function () {
return v; return v;
}); });
} }
return { request: request, init: init, browser: browser, guest: guest, ensureGuest: ensureGuest, user: function () { return user; } }; function logout() {
return request('api/auth/logout', {}).then(function (v) {
user = null; guestReady = false; rememberKind(); return v;
});
}
return { request: request, init: init, browser: browser, guest: guest, ensureGuest: ensureGuest, logout: logout, user: function () { return user; } };
})(); })();
+30 -4
View File
@@ -64,7 +64,7 @@
} }
/* ─── 屏幕管理:页面内不放返回按钮,前进时 pushState,微信/浏览器返回键经 popstate 回到上一屏 ─── */ /* ─── 屏幕管理:页面内不放返回按钮,前进时 pushState,微信/浏览器返回键经 popstate 回到上一屏 ─── */
var currentScreen = 's-login'; var currentScreen = 's-login', reportOwner = null, logoutBusy = false;
var fontStylesLoaded = false; var fontStylesLoaded = false;
function loadAppFont() { function loadAppFont() {
if (fontStylesLoaded) return; if (fontStylesLoaded) return;
@@ -79,6 +79,11 @@
label.textContent = user ? (user.kind === 'wechat' ? '微信已登录' : '游客模式') : '尚未登录'; label.textContent = user ? (user.kind === 'wechat' ? '微信已登录' : '游客模式') : '尚未登录';
label.parentElement.dataset.kind = user ? user.kind : ''; label.parentElement.dataset.kind = user ? user.kind : '';
}); });
document.querySelectorAll('[data-action="logout-wechat"]').forEach(function (button) {
button.hidden = !user || user.kind !== 'wechat';
button.disabled = logoutBusy;
button.textContent = logoutBusy ? '正在退出…' : '退出登录';
});
} }
function loginSucceeded() { function loginSucceeded() {
showScreen('s-splash', 'replace'); showScreen('s-splash', 'replace');
@@ -94,6 +99,7 @@
} }
function showScreen(id, nav) { // nav: 'push' 新增历史 | 'replace' 替换当前历史 | 缺省不动历史 function showScreen(id, nav) { // nav: 'push' 新增历史 | 'replace' 替换当前历史 | 缺省不动历史
if (id !== 's-login' && !AccountClient.user() && !(/[?&]mock=1/.test(location.search))) id = 's-login'; if (id !== 's-login' && !AccountClient.user() && !(/[?&]mock=1/.test(location.search))) id = 's-login';
if (id === 's-report' && !IS_MOCK && reportOwner !== (AccountClient.user() && AccountClient.user().id)) id = 's-splash';
if (id === 's-profile' || profileOwner !== ProfileStore.key(AccountClient.user())) restoreProfile(); if (id === 's-profile' || profileOwner !== ProfileStore.key(AccountClient.user())) restoreProfile();
if (['s-profile', 's-cam', 's-report'].includes(id)) loadAppFont(); if (['s-profile', 's-cam', 's-report'].includes(id)) loadAppFont();
if (id === 's-profile' || id === 's-cam') prepareScanArtwork(); if (id === 's-profile' || id === 's-cam') prepareScanArtwork();
@@ -1401,6 +1407,7 @@
} }
function renderReport(vm, frontPhoto, sidePhoto, payload) { function renderReport(vm, frontPhoto, sidePhoto, payload) {
reportOwner = AccountClient.user() && AccountClient.user().id;
// 头部:综合评分(vm.total 缺失时回退三维度 S 均分×10) // 头部:综合评分(vm.total 缺失时回退三维度 S 均分×10)
var total = vm.total; var total = vm.total;
if (total == null) { if (total == null) {
@@ -1412,10 +1419,11 @@
$('#stat-score').innerHTML = total != null ? '<b>' + total + '</b><i>分</i>' : '<b>--</b><i>分</i>'; $('#stat-score').innerHTML = total != null ? '<b>' + total + '</b><i>分</i>' : '<b>--</b><i>分</i>';
var ring = $('#ring-fg'); var ring = $('#ring-fg');
ring.style.strokeDashoffset = total != null ? 326.73 * (1 - Math.max(0, Math.min(100, total)) / 100) : 326.73; ring.style.strokeDashoffset = total != null ? 326.73 * (1 - Math.max(0, Math.min(100, total)) / 100) : 326.73;
$('#stat-age').innerHTML = vm.skinAgeDisplay != null ? '<b>' + vm.skinAgeDisplay + '</b><i>岁</i>' : '<b>--</b><i>岁</i>'; var displayedAge = SkinScore.reportSkinAge(vm, payload && payload.result);
$('#stat-age').innerHTML = displayedAge != null ? '<b>' + displayedAge + '</b><i>岁</i>' : '<b>--</b><i>岁</i>';
// 肌龄对比胶囊(新版设计「比实际年龄年轻2岁」);缺实际年龄或肌龄时不展示 // 肌龄对比胶囊(新版设计「比实际年龄年轻2岁」);缺实际年龄或肌龄时不展示
var pill = $('#stat-age-note'); var pill = $('#stat-age-note');
var diff = vm.skinAgeDisplay != null && vm.realAge ? vm.skinAgeDisplay - vm.realAge : null; var diff = displayedAge != null && vm.realAge ? displayedAge - vm.realAge : null;
pill.hidden = diff == null; pill.hidden = diff == null;
pill.textContent = diff == null ? '' : diff < 0 ? '比实际年龄年轻' + (-diff) + '岁' : diff > 0 ? '比实际年龄大' + diff + '岁' : '与实际年龄相当'; pill.textContent = diff == null ? '' : diff < 0 ? '比实际年龄年轻' + (-diff) + '岁' : diff > 0 ? '比实际年龄大' + diff + '岁' : '与实际年龄相当';
@@ -1618,6 +1626,7 @@
}, },
'history': function () { openHistory(); }, 'history': function () { openHistory(); },
'history-retry': function () { renderHistoryList(); }, 'history-retry': function () { renderHistoryList(); },
'logout-wechat': logoutWechat,
'open-wechat-login': function () { 'open-wechat-login': function () {
historySeq++; historySeq++;
$('#login-consent').checked = false; $('#login-consent').checked = false;
@@ -1668,8 +1677,9 @@
var btn = event.target.closest('[data-report-id]'); var btn = event.target.closest('[data-report-id]');
if (!btn || btn.disabled) return; if (!btn || btn.disabled) return;
btn.disabled = true; btn.disabled = true;
var seq = historySeq, owner = AccountClient.user() && AccountClient.user().id;
AccountClient.request('api/reports/' + encodeURIComponent(btn.dataset.reportId)).then(function (data) { AccountClient.request('api/reports/' + encodeURIComponent(btn.dataset.reportId)).then(function (data) {
if (currentScreen !== 's-history') return; if (currentScreen !== 's-history' || seq !== historySeq || owner !== (AccountClient.user() && AccountClient.user().id)) return;
var entry = data.report; var entry = data.report;
renderReport(entry.vm, entry.photos[0], entry.photos[1], Object.assign({}, entry.payload, { presentation: entry.presentation })); renderReport(entry.vm, entry.photos[0], entry.photos[1], Object.assign({}, entry.payload, { presentation: entry.presentation }));
showScreen('s-report', 'push'); showScreen('s-report', 'push');
@@ -1677,6 +1687,22 @@
}); });
var loginBusy = false; var loginBusy = false;
function logoutWechat() {
var user = AccountClient.user();
if (logoutBusy || !user || user.kind !== 'wechat') return;
if (!window.confirm('确认退出微信登录?\n历史报告将保留,重新微信登录后可查看。')) return;
logoutBusy = true; renderAccountStatus();
AccountClient.logout().then(function () {
historySeq++; reportOwner = null;
$('#history-list').textContent = ''; $('#history-count').textContent = ''; $('#identity-note').textContent = '';
$('#login-consent').checked = false;
$('#login-message').textContent = '已退出微信登录,历史报告已保留。';
showScreen('s-login', 'replace');
$('[data-action="login-wechat"]').focus({ preventScroll: true });
}).catch(function (e) { toast('退出失败:' + e.message); }).finally(function () {
logoutBusy = false; renderAccountStatus();
});
}
function loginAllowed() { function loginAllowed() {
if (loginBusy) return false; if (loginBusy) return false;
if (!$('#login-consent').checked) { toast('请先阅读并同意用户协议和隐私政策'); return false; } if (!$('#login-consent').checked) { toast('请先阅读并同意用户协议和隐私政策'); return false; }
+12 -8
View File
@@ -689,14 +689,11 @@
var notes = []; var notes = [];
var realAge = val(profile.realAge); var realAge = val(profile.realAge);
// 肌肤年龄:原值不改写,展示值钳制到 realAge±3 并留痕 // 生物可视年龄直接使用算法原始肌龄,不再按实际年龄钳制。
var skinAge = val(r.skin_age); var skinAge = val(r.skin_age);
var skinAgeDisplay = skinAge, ageClampNote = ''; var skinAgeDisplay = skinAge, ageClampNote = '';
if (realAge == null) { if (realAge == null) {
notes.push('未提供实际年龄:不修正展示肌龄;产品推荐仍使用接口原始肌龄'); notes.push('未提供实际年龄:肌龄展示与产品推荐均使用接口原始肌龄');
} else if (skinAge != null) {
skinAgeDisplay = Math.min(Math.max(skinAge, realAge - 3), realAge + 3);
if (skinAgeDisplay !== skinAge) ageClampNote = '触发(原' + skinAge + '→' + skinAgeDisplay + ')';
} }
var skinTypeCn = skinTypeCnOf(r, profile); // {skin_type_cn}:接口枚举优先,回退用户自选肤质 var skinTypeCn = skinTypeCnOf(r, profile); // {skin_type_cn}:接口枚举优先,回退用户自选肤质
@@ -898,7 +895,14 @@
right: { right_jawline_info: { right_jawline_angle: 113.4, right_jawline_angle_level: 1, right_jawline_quality: 1 } } // 偏离 2.6° → 94.8 right: { right_jawline_info: { right_jawline_angle: 113.4, right_jawline_angle_level: 1, right_jawline_quality: 1 } } // 偏离 2.6° → 94.8
}; };
// Historical snapshots may retain an old clamped display value. Read their
// original age without modifying the stored snapshot or recalculating copy.
function reportSkinAge(vm, result) {
if (vm && Object.prototype.hasOwnProperty.call(vm, 'skinAgeRaw')) return val(vm.skinAgeRaw);
return val(result && result.skin_age);
}
return { return {
reportSkinAge: reportSkinAge,
buildViewModel: buildViewModel, buildViewModel: buildViewModel,
recommendProduct: recommendProduct, recommendProduct: recommendProduct,
MOCK_RESULT: MOCK_RESULT, MOCK_RESULT: MOCK_RESULT,
@@ -941,7 +945,7 @@ if (typeof require === 'function' && typeof module !== 'undefined' && require.ma
ok(vm.cats.acne.P === 85, '痘痘P = min(分级映射85, 100−14.5=85.5) = 85'); ok(vm.cats.acne.P === 85, '痘痘P = min(分级映射85, 100−14.5=85.5) = 85');
ok(vm.cats.wrinkles.S === 7.69 && vm.cats.smooth.S === 9.45 && vm.cats.tone.S === 9.45, 'S=7+3×P/100 两位小数(含 9.445→9.45 浮点修正)'); ok(vm.cats.wrinkles.S === 7.69 && vm.cats.smooth.S === 9.45 && vm.cats.tone.S === 9.45, 'S=7+3×P/100 两位小数(含 9.445→9.45 浮点修正)');
ok(vm.dims.firm.P === 23 && vm.dims.vita.P === 81.5 && vm.dims.stab.P === 85, '维度木桶 min:紧塑力23 / 活力感81.5 / 稳定力85'); ok(vm.dims.firm.P === 23 && vm.dims.vita.P === 81.5 && vm.dims.stab.P === 85, '维度木桶 min:紧塑力23 / 活力感81.5 / 稳定力85');
ok(vm.skinAgeRaw === 20 && vm.skinAgeDisplay === 22 && vm.ageClampNote === '触发(原20→22)', '肌肤年龄钳制 20→22 留痕,原值不改写'); ok(vm.skinAgeRaw === 20 && vm.skinAgeDisplay === 20 && vm.ageClampNote === '', '肌肤年龄直接展示原值20,不按实际年龄钳制');
ok(vm.group === '25+组', '原始肌龄20 → 25+组'); ok(vm.group === '25+组', '原始肌龄20 → 25+组');
ok(vm.essence.name === '25+束紧精华' && vm.recommendation.tier === 1, ok(vm.essence.name === '25+束紧精华' && vm.recommendation.tier === 1,
'肌龄小于32,即使皱纹低分也固定25+,不再使用最差项升档'); '肌龄小于32,即使皱纹低分也固定25+,不再使用最差项升档');
@@ -974,14 +978,14 @@ if (typeof require === 'function' && typeof module !== 'undefined' && require.ma
&& vm2.dims.stab.P === Math.min(vm2.cats.sens.P, vm2.cats.acne.P), '三维度 P = min(旗下两分类)'); && vm2.dims.stab.P === Math.min(vm2.cats.sens.P, vm2.cats.acne.P), '三维度 P = min(旗下两分类)');
ok(vm2.group === '25+组', '实际年龄40不影响推荐,原始肌龄20仍为25+组'); ok(vm2.group === '25+组', '实际年龄40不影响推荐,原始肌龄20仍为25+组');
console.log('== T3 钳制边界 =='); console.log('== T3 原始肌龄展示 ==');
var r3 = clone(SS.MOCK_RESULT); var r3 = clone(SS.MOCK_RESULT);
r3.skin_age = { value: 24 }; r3.skin_age = { value: 24 };
var vm3 = SS.buildViewModel(r3, SS.MOCK_SIDE, { realAge: 25 }); var vm3 = SS.buildViewModel(r3, SS.MOCK_SIDE, { realAge: 25 });
ok(vm3.skinAgeDisplay === 24 && vm3.ageClampNote === '', '±3 以内不钳制、不留痕'); ok(vm3.skinAgeDisplay === 24 && vm3.ageClampNote === '', '±3 以内不钳制、不留痕');
r3.skin_age = { value: 40 }; r3.skin_age = { value: 40 };
var vm3b = SS.buildViewModel(r3, SS.MOCK_SIDE, { realAge: 25 }); var vm3b = SS.buildViewModel(r3, SS.MOCK_SIDE, { realAge: 25 });
ok(vm3b.skinAgeDisplay === 28 && vm3b.ageClampNote === '触发(原40→28)' && vm3b.skinAgeRaw === 40, '上边界钳制 40→28'); ok(vm3b.skinAgeDisplay === 40 && vm3b.ageClampNote === '' && vm3b.skinAgeRaw === 40, '原始肌龄40不因实际年龄25而改变');
console.log('== T4 最差项不再覆盖肤龄推荐 =='); console.log('== T4 最差项不再覆盖肤龄推荐 ==');
var r4 = clone(SS.MOCK_RESULT); var r4 = clone(SS.MOCK_RESULT);
+2 -4
View File
@@ -514,10 +514,8 @@ function photoUrls(rec) {
/* 列表用的字段子集(不含 raw_json 与文案全文) */ /* 列表用的字段子集(不含 raw_json 与文案全文) */
function displaySkinAge(rec) { function displaySkinAge(rec) {
if (Object.prototype.hasOwnProperty.call(rec, 'skin_age_display')) return rec.skin_age_display; // Match H5 even for old records; do not rewrite historical files.
// 旧记录没有展示值,沿用生成时的规则,不重算推荐或改写文件。 return SkinScore.reportSkinAge({ skinAgeRaw: rec.skin_age });
if (rec.skin_age == null || rec.real_age == null) return rec.skin_age;
return Math.min(Math.max(rec.skin_age, rec.real_age - 3), rec.real_age + 3);
} }
function listItem(rec) { function listItem(rec) {
return { return {
+1
View File
@@ -69,6 +69,7 @@ try {
assert.equal(await page.locator('#p-login-success.show').count(), 0, 'guest entry never shows login success'); 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('[data-action="switch-login"]').count(), 0, 'switch-login controls are removed');
assert.equal(await page.locator('#s-splash [data-auth-status]').textContent(), '游客模式'); assert.equal(await page.locator('#s-splash [data-auth-status]').textContent(), '游客模式');
assert.equal(await page.locator('[data-action="logout-wechat"]:visible').count(), 0, 'guests have no logout control');
assert.equal(guestRequests.length, 0, 'guest entry performs no authentication request'); 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.identity.v1')), null);
assert.equal(await page.evaluate(() => localStorage.getItem('juzi.reports.v1')), null); assert.equal(await page.evaluate(() => localStorage.getItem('juzi.reports.v1')), null);
+104
View File
@@ -0,0 +1,104 @@
// Real app + actual touch/mouse dispatch with varying UA/capabilities.
// Chromium UA regression, not a substitute for HarmonyOS hardware verification.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawn, execSync } from 'node:child_process';
import { once } from 'node:events';
import { fileURLToPath, pathToFileURL } from 'node:url';
const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href);
const h5 = fileURLToPath(new URL('../', import.meta.url));
const dataRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'juzi-harmony-test-'));
let child, browser;
let base = process.env.H5_BASE_URL;
try {
if (!base) {
base = 'http://127.0.0.1:18116/';
child = spawn(process.execPath, ['server.js'], { cwd: h5, env: { ...process.env, PORT: '18116', 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'] });
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)); });
});
}
browser = await chromium.launch({ headless: true });
const android = 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Mobile Safari/537.36';
const desktop = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36';
const cases = [
{ name: 'Android touch', ua: android, touch: true },
{ name: 'OpenHarmony 5 touch', ua: 'Mozilla/5.0 (Phone; OpenHarmony 5.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 ArkWeb/4.1.6.1 Mobile', touch: true },
{ name: 'OpenHarmony 6 WeChat touch', ua: 'Mozilla/5.0 (Phone;OpenHarmony 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 ArkWeb/6.0.0.42 Mobile MicroMessenger/8.0', touch: true },
{ name: 'Desktop UA on touch hardware', ua: desktop, touch: true },
{ name: 'Desktop mouse without TouchEvent', ua: desktop, touch: false },
{ name: 'Android UA without touch hardware', ua: android, touch: false }
];
for (const fixture of cases) {
const context = await browser.newContext({ viewport: { width: 375, height: 726 }, isMobile: fixture.touch, hasTouch: fixture.touch, userAgent: fixture.ua });
const page = await context.newPage(), errors = [], writes = [];
page.setDefaultTimeout(30000);
page.on('pageerror', e => errors.push(e.message));
if (!fixture.touch) await page.addInitScript(() => { delete window.TouchEvent; });
await page.route('**/*', route => {
const req = route.request();
if (req.method() !== 'GET') { writes.push(req.url()); return route.abort(); }
if (/\.woff2?(\?|$)/.test(req.url())) return route.abort();
if (req.url().endsWith('/api/auth/session')) return route.fulfill({ json: { ok: true, user: { id: 'harmony-picker-fixture', kind: 'wechat' } } });
return route.continue();
});
await page.route('**/mobile-select.iife.min.js', async route => {
const response = await route.fetch();
await route.fulfill({ response, body: await response.text() + ';var OriginalMobileSelect=MobileSelect;window.__pickers={};MobileSelect=function(c){var p=new OriginalMobileSelect(c);window.__pickers[c.trigger]=p;return p;};' });
});
await page.goto(base, { waitUntil: 'domcontentloaded' }); await page.waitForSelector('#s-splash.active');
await page.click('[data-action="open-privacy"]'); await page.click('#privacy-check'); await page.click('[data-action="agree-continue"]');
await page.waitForSelector('#s-profile.active');
const cdp = await context.newCDPSession(page);
for (const trigger of ['#pick-birth', '#pick-region']) {
await page.click(trigger); await page.waitForTimeout(350);
const events = await page.evaluate(t => __pickers[t].eventHandleMap.panel.event, trigger);
assert.deepEqual(events, fixture.touch ? ['touchstart', 'touchend', 'touchmove'] : ['mousedown', 'mousemove', 'mouseup'], fixture.name);
assert.equal(await page.locator('.ms-mobile-select.ms-show').evaluate(el => !!el.closest('#s-profile')), false, 'profile scroll lock cannot intercept picker touch');
for (const column of [0, 1]) {
if (trigger === '#pick-region' && column === 1) await page.evaluate(() => __pickers['#pick-region'].locatePosition(0, REGION_DATA.findIndex(p => p[0] === '浙江省')));
const before = await page.evaluate(t => JSON.stringify(__pickers[t].getCurValue()), trigger);
const wheel = page.locator('.ms-mobile-select.ms-show .ms-wheel').nth(column), box = await wheel.boundingBox();
const x = box.x + box.width / 2, y = box.y + box.height / 2;
if (fixture.touch) {
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x, y, id: 1 }] });
for (let i = 1; i <= 4; i++) {
await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x, y: y - i * 20, id: 1 }] });
await page.waitForTimeout(25);
}
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
} else {
await page.mouse.move(x, y); await page.mouse.down(); await page.mouse.move(x, y - 80, { steps: 4 }); await page.mouse.up();
}
await page.waitForTimeout(250);
assert.notEqual(await page.evaluate(t => JSON.stringify(__pickers[t].getCurValue()), trigger), before, fixture.name + '/' + trigger + '/' + column + ' must move');
}
const chosen = await page.evaluate(t => __pickers[t].getCurValue(), trigger);
await page.click('.ms-mobile-select.ms-show .ms-ensure'); await page.waitForTimeout(250);
if (trigger === '#pick-birth') {
const value = parseInt(chosen[0]) + '-' + String(parseInt(chosen[1])).padStart(2, '0');
assert.equal(await page.locator('#f-birth').inputValue(), value);
} else {
const value = await page.locator('#f-region').inputValue();
assert(value.includes(chosen[0].value) && value.includes(chosen[1].value));
}
await page.click(trigger); await page.waitForTimeout(300);
assert.deepEqual(await page.evaluate(t => __pickers[t].getCurValue(), trigger), chosen, 'reopening retains confirmed values');
await page.click('.ms-mobile-select.ms-show .ms-cancel'); await page.waitForTimeout(250);
}
await page.click('#pick-gender'); await page.click('.ms-mobile-select.ms-show .ms-ensure');
await page.click('[data-action="submit-profile"]'); await page.waitForSelector('#p-photoguide.show');
assert.deepEqual(errors, []); assert.deepEqual(writes, []);
console.log('PASS ' + fixture.name + ': year/month/province/city drag, cascade, confirm/reopen, profile validation');
await context.close();
}
} finally {
if (browser) await browser.close();
if (child && child.exitCode == null) { const stopped = once(child, 'exit'); child.kill(); await stopped; }
fs.rmSync(dataRoot, { recursive: true, force: true });
}
+22
View File
@@ -0,0 +1,22 @@
'use strict';
const assert = require('node:assert/strict');
const SS = require('../js/skin-score');
for (const raw of [20, 24, 28, 40, 50, 24.6, null]) {
for (const realAge of [25, 30, 60, undefined]) {
const result = structuredClone(SS.MOCK_RESULT);
if (raw == null) delete result.skin_age; else result.skin_age = { value: raw };
const before = JSON.stringify(result);
const vm = SS.buildViewModel(result, SS.MOCK_SIDE, { realAge });
assert.equal(vm.skinAgeRaw, raw); assert.equal(vm.skinAgeDisplay, raw);
assert.equal(vm.ageClampNote, ''); assert.equal(SS.reportSkinAge(vm, result), raw);
assert.equal(JSON.stringify(result), before);
}
}
const legacy = { skinAgeRaw: 24, skinAgeDisplay: 57, realAge: 60 };
assert.equal(SS.reportSkinAge(legacy), 24);
assert.equal(legacy.skinAgeDisplay, 57, 'historical VM is not mutated');
assert.equal(SS.reportSkinAge({ skinAgeDisplay: 57 }, { skin_age: { value: 24 } }), 24);
assert.equal(SS.reportSkinAge({ skinAgeDisplay: 57 }), null, 'clamped display is never a raw-age fallback');
assert.equal(SS.reportSkinAge({ skinAgeRaw: null, skinAgeDisplay: 57 }), null);
assert.equal(SS.reportSkinAge({}, { skin_age: '24.6' }), 24.6);
console.log('PASS: raw age across actual-age/decimal/missing cases; old display ignored; legacy payload fallback; no mutation or invented age');
+1 -1
View File
@@ -54,7 +54,7 @@ for (const realAge of [24, 40, 60, undefined]) {
assert.equal(vm.recommendation.skinAge, 24); assert.equal(vm.recommendation.skinAge, 24);
assert(Math.abs(vm.recommendation.scores.firm - 7.69) < 1e-12); assert(Math.abs(vm.recommendation.scores.firm - 7.69) < 1e-12);
assert.equal(UI.build(vm, raw).product.tier, 1); assert.equal(UI.build(vm, raw).product.tier, 1);
if (realAge === 60) assert.equal(vm.skinAgeDisplay, 57); // Display isn't the recommendation input. assert.equal(vm.skinAgeDisplay, 24); // Display and recommendation both use raw skin age.
count++; count++;
} }
delete raw.skin_age; delete raw.skin_age;
+10 -6
View File
@@ -47,8 +47,8 @@ try {
assert.equal(result.report_saved, true); assert.equal(result.report_saved, true);
assert(result.vm && result.presentation); assert(result.vm && result.presentation);
assert(result.vm.skinAgeRaw < 32); assert(result.vm.skinAgeRaw < 32);
assert.equal(result.vm.skinAgeDisplay, 57); assert.equal(result.vm.skinAgeDisplay, result.vm.skinAgeRaw);
assert.equal(result.vm.essence.tier, 1); // Raw age, not actual60 or display57. assert.equal(result.vm.essence.tier, 1); // Raw age, never actual60.
assert.equal(result.presentation.product.tier, 1); assert.equal(result.presentation.product.tier, 1);
const adminRecord = (await (await api.get(base + 'api/admin/records/' + result.record_id)).json()).record; const adminRecord = (await (await api.get(base + 'api/admin/records/' + result.record_id)).json()).record;
assert.deepEqual(adminRecord.presentation, result.presentation); assert.deepEqual(adminRecord.presentation, result.presentation);
@@ -58,7 +58,7 @@ try {
for (const key of ['firm', 'vita', 'stab']) assert.equal(adminRecord.dims[key].S, result.vm.dims[key].S); for (const key of ['firm', 'vita', 'stab']) assert.equal(adminRecord.dims[key].S, result.vm.dims[key].S);
const list = await (await api.get(base + 'api/admin/records')).json(); const list = await (await api.get(base + 'api/admin/records')).json();
assert.equal(list.items[0].skin_age, result.vm.skinAgeRaw); assert.equal(list.items[0].skin_age, result.vm.skinAgeRaw);
assert.equal(list.items[0].skin_age_display, 57); assert.equal(list.items[0].skin_age_display, result.vm.skinAgeRaw);
assert.deepEqual(list.items[0].recommendation, result.vm.recommendation); assert.deepEqual(list.items[0].recommendation, result.vm.recommendation);
const csv = await (await api.get(base + 'api/admin/export')).text(); const csv = await (await api.get(base + 'api/admin/export')).text();
assert(csv.includes('原始肌龄(推荐依据),前端展示肌龄')); assert(csv.includes('原始肌龄(推荐依据),前端展示肌龄'));
@@ -74,7 +74,7 @@ try {
const row = desktop.locator('#tbl-records tbody tr').first(); const row = desktop.locator('#tbl-records tbody tr').first();
await row.waitFor(); await row.waitFor();
assert((await row.textContent()).includes('原始 ' + result.vm.skinAgeRaw + '岁')); assert((await row.textContent()).includes('原始 ' + result.vm.skinAgeRaw + '岁'));
assert((await row.textContent()).includes('展示 57岁')); assert((await row.textContent()).includes('展示 ' + result.vm.skinAgeRaw + '岁'));
assert((await row.textContent()).includes("1'me 主场精华 I(25+)")); assert((await row.textContent()).includes("1'me 主场精华 I(25+)"));
for (const key of ['firm', 'vita', 'stab']) { for (const key of ['firm', 'vita', 'stab']) {
assert((await row.textContent()).includes(ReportUI.dimNames[key] + ' ' + result.vm.dims[key].S.toFixed(1) + ' ' + ReportUI.levelMap[result.vm.dims[key].level])); assert((await row.textContent()).includes(ReportUI.dimNames[key] + ' ' + result.vm.dims[key].S.toFixed(1) + ' ' + ReportUI.levelMap[result.vm.dims[key].level]));
@@ -93,7 +93,7 @@ try {
await desktop.waitForFunction(() => document.querySelector('#d-body').textContent.includes('推荐依据')); await desktop.waitForFunction(() => document.querySelector('#d-body').textContent.includes('推荐依据'));
const detailText = await desktop.locator('#d-body').textContent(); const detailText = await desktop.locator('#d-body').textContent();
assert(detailText.includes(result.vm.recommendation.reason)); assert(detailText.includes(result.vm.recommendation.reason));
assert(detailText.includes('前端展示肌龄 57岁')); assert(detailText.includes('前端展示肌龄 ' + result.vm.skinAgeRaw + '岁'));
for (const view of result.presentation.views) assert(detailText.includes(view.text)); for (const view of result.presentation.views) assert(detailText.includes(view.text));
for (const care of result.presentation.care) assert(detailText.includes(care.text)); for (const care of result.presentation.care) assert(detailText.includes(care.text));
if (process.env.H5_ARTIFACT_DIR) { if (process.env.H5_ARTIFACT_DIR) {
@@ -146,19 +146,21 @@ try {
const legacy = JSON.parse(fs.readFileSync(snapshotFile, 'utf8')); const legacy = JSON.parse(fs.readFileSync(snapshotFile, 'utf8'));
legacy.presentation.product = { ...ReportUI.products[1], image: 'assets/img/ui-20261009/product-35.png' }; legacy.presentation.product = { ...ReportUI.products[1], image: 'assets/img/ui-20261009/product-35.png' };
legacy.presentation.views[0].text = '历史报告原文,不应按新文案重新计算。'; legacy.presentation.views[0].text = '历史报告原文,不应按新文案重新计算。';
legacy.vm.skinAgeDisplay = 57; // Old snapshot retains the retired display clamp.
legacy.vm.essence = { name: '35+支撑精华', text: '旧版推荐', triggers: ['最差项约束'] }; legacy.vm.essence = { name: '35+支撑精华', text: '旧版推荐', triggers: ['最差项约束'] };
delete legacy.vm.recommendation; delete legacy.vm.recommendation;
fs.writeFileSync(snapshotFile, JSON.stringify(legacy)); fs.writeFileSync(snapshotFile, JSON.stringify(legacy));
record.essence = legacy.vm.essence; record.essence = legacy.vm.essence;
delete record.presentation; delete record.presentation;
delete record.recommendation; delete record.recommendation;
delete record.skin_age_display; record.skin_age_display = 57;
fs.writeFileSync(recordFile, JSON.stringify(record) + '\n'); fs.writeFileSync(recordFile, JSON.stringify(record) + '\n');
const storedBytes = fs.readFileSync(snapshotFile, 'utf8'); const storedBytes = fs.readFileSync(snapshotFile, 'utf8');
const recordBytes = fs.readFileSync(recordFile, 'utf8'); const recordBytes = fs.readFileSync(recordFile, 'utf8');
await page.click('[data-action="history"]'); await page.click('[data-action="history"]');
await page.click('[data-report-id]'); await page.click('[data-report-id]');
await page.waitForSelector('#s-report.active'); await page.waitForSelector('#s-report.active');
assert.equal(await page.locator('#stat-age b').textContent(), String(result.vm.skinAgeRaw), 'old saved display clamp must not override original skin age');
assert.equal(await page.locator('#product-title').textContent(), ReportUI.products[1].title); assert.equal(await page.locator('#product-title').textContent(), ReportUI.products[1].title);
assert.equal(await page.locator('#product-image').getAttribute('src'), ReportUI.products[1].image, 'refresh artwork crop, never the historical recommendation/copy'); assert.equal(await page.locator('#product-image').getAttribute('src'), ReportUI.products[1].image, 'refresh artwork crop, never the historical recommendation/copy');
assert((await page.locator('.ditem-para').allTextContents()).includes(legacy.presentation.views[0].text)); assert((await page.locator('.ditem-para').allTextContents()).includes(legacy.presentation.views[0].text));
@@ -166,6 +168,7 @@ try {
await desktop.locator('#tbl-records tbody tr').first().click(); await desktop.locator('#tbl-records tbody tr').first().click();
await desktop.waitForFunction(() => document.querySelector('#d-body').textContent.includes('历史报告原文')); await desktop.waitForFunction(() => document.querySelector('#d-body').textContent.includes('历史报告原文'));
assert((await desktop.locator('#d-body').textContent()).includes('旧版规则(历史记录)')); assert((await desktop.locator('#d-body').textContent()).includes('旧版规则(历史记录)'));
assert((await desktop.locator('#d-body').textContent()).includes('前端展示肌龄 ' + result.vm.skinAgeRaw + '岁'));
assert.equal(fs.readFileSync(snapshotFile, 'utf8'), storedBytes); assert.equal(fs.readFileSync(snapshotFile, 'utf8'), storedBytes);
assert.equal(fs.readFileSync(recordFile, 'utf8'), recordBytes); assert.equal(fs.readFileSync(recordFile, 'utf8'), recordBytes);
// Missing skin age must not leave the previous product visible in the H5. // Missing skin age must not leave the previous product visible in the H5.
@@ -181,6 +184,7 @@ try {
await page.waitForSelector('#s-report.active'); await page.waitForSelector('#s-report.active');
assert.equal(await page.locator('.card-product').isVisible(), false); assert.equal(await page.locator('.card-product').isVisible(), false);
assert.equal(await page.locator('#product-unavailable').isVisible(), true); assert.equal(await page.locator('#product-unavailable').isVisible(), true);
assert.equal(await page.locator('#stat-age b').textContent(), '--');
assert.deepEqual(errors, []); assert.deepEqual(errors, []);
console.log('PASS: raw-age recommendation; admin/H5 scores, ages, product, seven-view copy, care, CSV, filters; saved/reloaded and legacy immutable snapshots; missing-age UI; owner isolation/private files/back'); console.log('PASS: raw-age recommendation; admin/H5 scores, ages, product, seven-view copy, care, CSV, filters; saved/reloaded and legacy immutable snapshots; missing-age UI; owner isolation/private files/back');
} finally { } finally {
+43
View File
@@ -72,6 +72,7 @@ try {
await page.click('[data-action="login-confirm"]'); await page.click('[data-action="login-confirm"]');
assert.equal(await page.locator('#p-login-success.show').count(), 0); assert.equal(await page.locator('#p-login-success.show').count(), 0);
assert.equal(await page.locator('#s-splash').evaluate(e => e.inert), false); assert.equal(await page.locator('#s-splash').evaluate(e => e.inert), false);
assert.equal(await page.locator('#s-splash [data-action="logout-wechat"]').isVisible(), true);
} }
for (let device = 0; device < 2; device++) { for (let device = 0; device < 2; device++) {
const context = await browser.newContext({ ignoreHTTPSErrors: true, userAgent: 'Mozilla/5.0 MicroMessenger/8.0', viewport: { width: 375, height: 726 } }); const context = await browser.newContext({ ignoreHTTPSErrors: true, userAgent: 'Mozilla/5.0 MicroMessenger/8.0', viewport: { width: 375, height: 726 } });
@@ -165,6 +166,48 @@ try {
assert.equal(await page.evaluate(() => AccountClient.user()), null); 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.equal(await page.locator('#p-login-success.show').count(), 0, 'session restore must not repeatedly open a success dialog');
} }
if (device === 1) {
const reportFile = path.join(root, 'accounts/reports', user.id, saved.id + '.json');
const originalBytes = fs.readFileSync(reportFile, 'utf8');
const logout = page.locator('#s-history [data-action="logout-wechat"]');
assert.equal(await logout.textContent(), '退出登录');
page.once('dialog', dialog => { assert(dialog.message().includes('历史报告将保留')); dialog.dismiss(); });
await logout.click();
assert.equal((await api(page, 'auth/session')).data.user.id, user.id, 'cancel leaves session intact');
await page.route('**/api/auth/logout', route => route.fulfill({ status: 503, json: { ok: false, message: '退出测试失败' } }));
page.once('dialog', dialog => dialog.accept()); await logout.click();
await page.waitForFunction(() => document.querySelector('#toast').textContent.includes('退出失败'));
assert.equal(await page.evaluate(() => AccountClient.user().id), user.id, 'failure does not fake local logout');
assert.equal((await api(page, 'auth/session')).data.user.id, user.id);
await page.unroute('**/api/auth/logout');
await page.waitForFunction(() => !document.querySelector('#s-history [data-action="logout-wechat"]').disabled);
let heldReport, markHeld;
const reportHeld = new Promise(resolve => { markHeld = resolve; });
await page.route('**/api/reports/' + saved.id, route => { heldReport = route; markHeld(); }, { times: 1 });
await page.click('[data-report-id="RwechatCrossDevice"]'); await reportHeld;
page.once('dialog', dialog => dialog.accept()); await logout.click();
await page.waitForSelector('#s-login.active');
assert.equal(await page.evaluate(() => AccountClient.user()), null);
assert.equal((await api(page, 'auth/session')).data.user, null);
assert.equal((await api(page, 'reports/' + saved.id)).status, 401);
assert.equal(await page.locator('#login-consent').isChecked(), false);
assert.equal(fs.readFileSync(reportFile, 'utf8'), originalBytes, 'logout retains exact historical file');
await page.goBack(); await page.waitForSelector('#s-login.active');
await page.click('[data-action="guest-experience"]'); await page.waitForSelector('#s-splash.active');
await page.click('[data-action="history"]'); await page.waitForSelector('#s-history.active');
await heldReport.fulfill({ json: { ok: true, report: JSON.parse(originalBytes) } });
await page.waitForTimeout(250);
assert.equal(await page.locator('#s-report.active').count(), 0, 'late WeChat report response cannot open under guest identity');
await page.unroute('**/api/reports/' + saved.id);
await page.click('[data-action="open-wechat-login"]');
await page.check('#login-consent'); await page.click('[data-action="login-wechat"]');
await page.waitForSelector('#s-splash.active'); await confirmLogin(page, 'wechat');
assert.equal(await page.evaluate(() => AccountClient.user().id), user.id);
await page.click('[data-action="history"]'); await page.waitForSelector('[data-report-id="RwechatCrossDevice"]');
assert.deepEqual((await api(page, 'reports/' + saved.id)).data.report, JSON.parse(originalBytes));
assert.equal(fs.readFileSync(reportFile, 'utf8'), originalBytes);
console.log('PASS WeChat logout: confirm/cancel/failure, server session revoked, back guarded, re-login retains exact report');
}
assert.deepEqual(errors, []); assert.deepEqual(errors, []);
await context.close(); await context.close();
} }
+3 -1
View File
@@ -1,4 +1,6 @@
# mobile-select 1.4.0 # mobile-select 1.4.0
来源:npm `mobile-select@1.4.0`(MIT,见同目录 `LICENSE`),取 `dist/mobile-select.iife.min.js` 与 `dist/style/mobile-select.css`,未改动。 来源:npm `mobile-select@1.4.0`(MIT,见同目录 `LICENSE`),取 `dist/mobile-select.iife.min.js` 与 `dist/style/mobile-select.css`。
2026-10-13 本地兼容补丁:不再按 UA 名称判断触摸设备,改用 `navigator.maxTouchPoints > 0` 或 `ontouchstart` 能力;解决不带 Android 标识的 OpenHarmony/ArkWeb 被错误绑定鼠标事件的问题。事件坐标按 `touches/changedTouches` 读取,不依赖全局 `TouchEvent` 构造函数存在。上游版本和 MIT 声明保留,升级依赖时须保留此补丁并运行 `h5/test/harmony-picker.mjs` 回归。
档案页的出生年月 / 性别 / 地区滚轮由它渲染,样式覆盖写在 `css/style.css` 的 mobile-select 段。 档案页的出生年月 / 性别 / 地区滚轮由它渲染,样式覆盖写在 `css/style.css` 的 mobile-select 段。
File diff suppressed because one or more lines are too long